diff --git a/NuGet.config b/NuGet.config index 0fd623ffdd..3957d769d2 100644 --- a/NuGet.config +++ b/NuGet.config @@ -1,6 +1,8 @@  + + diff --git a/src/Microsoft.DotNet.Watcher.Tools/CommandLineOptions.cs b/src/Microsoft.DotNet.Watcher.Tools/CommandLineOptions.cs index dfd27756bd..08c9b473f5 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/CommandLineOptions.cs +++ b/src/Microsoft.DotNet.Watcher.Tools/CommandLineOptions.cs @@ -12,6 +12,7 @@ namespace Microsoft.DotNet.Watcher { internal class CommandLineOptions { + public string Project { get; private set; } public bool IsHelp { get; private set; } public bool IsQuiet { get; private set; } public bool IsVerbose { get; private set; } @@ -30,6 +31,8 @@ namespace Microsoft.DotNet.Watcher }; app.HelpOption("-?|-h|--help"); + var optProjects = app.Option("-p|--project", "The project to watch", + CommandOptionType.SingleValue); // TODO multiple shouldn't be too hard to support var optQuiet = app.Option("-q|--quiet", "Suppresses all output except warnings and errors", CommandOptionType.NoValue); var optVerbose = app.Option("-v|--verbose", "Show verbose output", @@ -58,6 +61,7 @@ namespace Microsoft.DotNet.Watcher return new CommandLineOptions { + Project = optProjects.Value(), IsQuiet = optQuiet.HasValue(), IsVerbose = optVerbose.HasValue(), RemainingArguments = app.RemainingArguments, diff --git a/src/Microsoft.DotNet.Watcher.Tools/Internal/FileSetWatcher.cs b/src/Microsoft.DotNet.Watcher.Tools/Internal/FileSetWatcher.cs index a1f56d9650..b97b547a52 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/Internal/FileSetWatcher.cs +++ b/src/Microsoft.DotNet.Watcher.Tools/Internal/FileSetWatcher.cs @@ -10,13 +10,14 @@ namespace Microsoft.DotNet.Watcher.Internal { public class FileSetWatcher : IDisposable { - private readonly IFileWatcher _fileWatcher; + private readonly FileWatcher _fileWatcher = new FileWatcher(); private readonly IFileSet _fileSet; public FileSetWatcher(IFileSet fileSet) { + Ensure.NotNull(fileSet, nameof(fileSet)); + _fileSet = fileSet; - _fileWatcher = new FileWatcher(); } public async Task GetChangedFileAsync(CancellationToken cancellationToken) diff --git a/src/Microsoft.DotNet.Watcher.Tools/Internal/FileWatcher.cs b/src/Microsoft.DotNet.Watcher.Tools/Internal/FileWatcher.cs index 79036c28de..20840f40b9 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/Internal/FileWatcher.cs +++ b/src/Microsoft.DotNet.Watcher.Tools/Internal/FileWatcher.cs @@ -8,7 +8,7 @@ using System.Linq; namespace Microsoft.DotNet.Watcher.Internal { - public class FileWatcher : IFileWatcher + public class FileWatcher { private bool _disposed; diff --git a/src/Microsoft.DotNet.Watcher.Tools/Internal/FileWatcher/PollingFileWatcher.cs b/src/Microsoft.DotNet.Watcher.Tools/Internal/FileWatcher/PollingFileWatcher.cs index 9ca5084347..d26dcc54f2 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/Internal/FileWatcher/PollingFileWatcher.cs +++ b/src/Microsoft.DotNet.Watcher.Tools/Internal/FileWatcher/PollingFileWatcher.cs @@ -182,6 +182,11 @@ namespace Microsoft.DotNet.Watcher.Internal private void ForeachEntityInDirectory(DirectoryInfo dirInfo, Action fileAction) { + if (!dirInfo.Exists) + { + return; + } + var entities = dirInfo.EnumerateFileSystemInfos("*.*"); foreach (var entity in entities) { diff --git a/src/Microsoft.DotNet.Watcher.Tools/Internal/IFileWatcher.cs b/src/Microsoft.DotNet.Watcher.Tools/Internal/IFileWatcher.cs deleted file mode 100644 index 1e43363789..0000000000 --- a/src/Microsoft.DotNet.Watcher.Tools/Internal/IFileWatcher.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; - -namespace Microsoft.DotNet.Watcher.Internal -{ - public interface IFileWatcher : IDisposable - { - event Action OnFileChange; - - void WatchDirectory(string directory); - } -} diff --git a/src/Microsoft.DotNet.Watcher.Tools/Internal/IncludeContextExtensions.cs b/src/Microsoft.DotNet.Watcher.Tools/Internal/IncludeContextExtensions.cs deleted file mode 100644 index bd7cf0b8ad..0000000000 --- a/src/Microsoft.DotNet.Watcher.Tools/Internal/IncludeContextExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Collections.Generic; -using System.Linq; -using Microsoft.DotNet.ProjectModel.Files; - -namespace Microsoft.DotNet.Watcher.Internal -{ - internal static class IncludeContextExtensions - { - public static IEnumerable ResolveFiles(this IncludeContext context) - { - Ensure.NotNull(context, nameof(context)); - - return IncludeFilesResolver - .GetIncludeFiles(context, "/", diagnostics: null) - .Select(f => f.SourcePath); - } - } -} diff --git a/src/Microsoft.DotNet.Watcher.Tools/Internal/MsBuildFileSetFactory.cs b/src/Microsoft.DotNet.Watcher.Tools/Internal/MsBuildFileSetFactory.cs new file mode 100644 index 0000000000..f40a43732a --- /dev/null +++ b/src/Microsoft.DotNet.Watcher.Tools/Internal/MsBuildFileSetFactory.cs @@ -0,0 +1,164 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Watcher.Tools; +using Microsoft.Extensions.Logging; + +namespace Microsoft.DotNet.Watcher.Internal +{ + public class MsBuildFileSetFactory : IFileSetFactory + { + private const string ProjectExtensionFileExtension = ".dotnetwatch.targets"; + private const string WatchTargetsFileName = "DotNetWatchCommon.targets"; + private readonly ILogger _logger; + private readonly string _projectFile; + private readonly string _watchTargetsDir; + private readonly OutputSink _outputSink; + + public MsBuildFileSetFactory(ILogger logger, string projectFile) + : this(logger, projectFile, new OutputSink()) + { + } + + // output sink is for testing + internal MsBuildFileSetFactory(ILogger logger, string projectFile, OutputSink outputSink) + { + Ensure.NotNull(logger, nameof(logger)); + Ensure.NotNullOrEmpty(projectFile, nameof(projectFile)); + Ensure.NotNull(outputSink, nameof(outputSink)); + + _logger = logger; + _projectFile = projectFile; + _watchTargetsDir = FindWatchTargetsDir(); + _outputSink = outputSink; + } + + internal List BuildFlags { get; } = new List + { + "/nologo", + "/v:n", + "/t:GenerateWatchList", + "/p:DotNetWatchBuild=true", // extensibility point for users + "/p:DesignTimeBuild=true", // don't do expensive things + }; + + public async Task CreateAsync(CancellationToken cancellationToken) + { + EnsureInitialized(); + + var watchList = Path.GetTempFileName(); + try + { + var projectDir = Path.GetDirectoryName(_projectFile); + + while (true) + { + cancellationToken.ThrowIfCancellationRequested(); + +#if DEBUG + var stopwatch = new Stopwatch(); + stopwatch.Start(); +#endif + var capture = _outputSink.StartCapture(); + // TODO adding files doesn't currently work. Need to provide a way to detect new files + // find files + var exitCode = Command.CreateDotNet("msbuild", + new[] + { + _projectFile, + $"/p:_DotNetWatchTargetsLocation={_watchTargetsDir}", // add our dotnet-watch targets + $"/p:_DotNetWatchListFile={watchList}", + }.Concat(BuildFlags)) + .CaptureStdErr() + .CaptureStdOut() + .OnErrorLine(l => capture.WriteErrorLine(l)) + .OnOutputLine(l => capture.WriteOutputLine(l)) + .WorkingDirectory(projectDir) + .Execute() + .ExitCode; + + if (exitCode == 0) + { + var files = File.ReadAllLines(watchList) + .Select(l => l?.Trim()) + .Where(l => !string.IsNullOrEmpty(l)); + + var fileset = new FileSet(files); + +#if DEBUG + _logger.LogDebug(string.Join(Environment.NewLine, fileset)); + Debug.Assert(files.All(Path.IsPathRooted), "All files should be rooted paths"); + stopwatch.Stop(); + _logger.LogDebug("Gathered project information in {time}ms", stopwatch.ElapsedMilliseconds); +#endif + + return fileset; + } + + _logger.LogError($"Error(s) finding watch items project file '{Path.GetFileName(_projectFile)}': "); + _logger.LogError(capture.GetAllLines("[MSBUILD] : ")); + _logger.LogInformation("Fix the error to continue."); + + var fileSet = new FileSet(new[] { _projectFile }); + + using (var watcher = new FileSetWatcher(fileSet)) + { + await watcher.GetChangedFileAsync(cancellationToken); + + _logger.LogInformation($"File changed: {_projectFile}"); + } + } + } + finally + { + File.Delete(watchList); + } + } + + // Ensures file exists in $(MSBuildProjectExtensionsPath)/$(MSBuildProjectFile).dotnetwatch.targets + private void EnsureInitialized() + { + // default value for MSBuildProjectExtensionsPath. + var projectExtensionsPath = Path.Combine(Path.GetDirectoryName(_projectFile), "obj"); + + // see https://github.com/Microsoft/msbuild/blob/bf9b21cc7869b96ea2289ff31f6aaa5e1d525a26/src/XMakeTasks/Microsoft.Common.targets#L127 + var projectExtensionFile = Path.Combine(projectExtensionsPath, Path.GetFileName(_projectFile) + ProjectExtensionFileExtension); + + if (!File.Exists(projectExtensionFile)) + { + // ensure obj folder is available + Directory.CreateDirectory(Path.GetDirectoryName(projectExtensionFile)); + + using (var fileStream = new FileStream(projectExtensionFile, FileMode.Create)) + using (var assemblyStream = GetType().GetTypeInfo().Assembly.GetManifestResourceStream("dotnetwatch.targets")) + { + assemblyStream.CopyTo(fileStream); + } + } + } + + private string FindWatchTargetsDir() + { + var assemblyDir = Path.GetDirectoryName(GetType().GetTypeInfo().Assembly.Location); + var searchPaths = new[] + { + AppContext.BaseDirectory, + assemblyDir, + Path.Combine(assemblyDir, "../../tools"), // from nuget cache + Path.Combine(assemblyDir, "tools") // from local build + }; + + var targetPath = searchPaths.Select(p => Path.Combine(p, WatchTargetsFileName)).First(File.Exists); + return Path.GetDirectoryName(targetPath); + } + } +} diff --git a/src/Microsoft.DotNet.Watcher.Tools/Internal/MsBuildProjectFinder.cs b/src/Microsoft.DotNet.Watcher.Tools/Internal/MsBuildProjectFinder.cs new file mode 100644 index 0000000000..ff3f45b66b --- /dev/null +++ b/src/Microsoft.DotNet.Watcher.Tools/Internal/MsBuildProjectFinder.cs @@ -0,0 +1,57 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Linq; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Watcher.Tools; + +namespace Microsoft.DotNet.Watcher.Internal +{ + internal class MsBuildProjectFinder + { + /// + /// Finds a compatible MSBuild project. + /// The base directory to search + /// The filename of the project. Can be null. + /// + public static string FindMsBuildProject(string searchBase, string project) + { + Ensure.NotNullOrEmpty(searchBase, nameof(searchBase)); + + var projectPath = project ?? searchBase; + + if (!Path.IsPathRooted(projectPath)) + { + projectPath = Path.Combine(searchBase, projectPath); + } + + if (Directory.Exists(projectPath)) + { + var projects = Directory.EnumerateFileSystemEntries(projectPath, "*.*proj", SearchOption.TopDirectoryOnly) + .Where(f => !".xproj".Equals(Path.GetExtension(f), StringComparison.OrdinalIgnoreCase)) + .ToList(); + + if (projects.Count > 1) + { + throw new GracefulException(Resources.FormatError_MultipleProjectsFound(projectPath)); + } + + if (projects.Count == 0) + { + throw new GracefulException(Resources.FormatError_NoProjectsFound(projectPath)); + } + + return projects[0]; + } + + if (!File.Exists(projectPath)) + { + throw new GracefulException(Resources.FormatError_ProjectPath_NotFound(projectPath)); + } + + return projectPath; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Watcher.Tools/Internal/OutputSink.cs b/src/Microsoft.DotNet.Watcher.Tools/Internal/OutputSink.cs new file mode 100644 index 0000000000..aa764437ad --- /dev/null +++ b/src/Microsoft.DotNet.Watcher.Tools/Internal/OutputSink.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.DotNet.Watcher.Internal +{ + internal class OutputSink + { + public OutputCapture Current { get; private set; } + public OutputCapture StartCapture() + { + return (Current = new OutputCapture()); + } + + public class OutputCapture + { + private readonly List _lines = new List(); + public IEnumerable Lines => _lines; + public void WriteOutputLine(string line) => _lines.Add(line); + public void WriteErrorLine(string line) => _lines.Add(line); + public string GetAllLines(string prefix) => string.Join(Environment.NewLine, _lines.Select(l => prefix + l)); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Watcher.Tools/Internal/Project.cs b/src/Microsoft.DotNet.Watcher.Tools/Internal/Project.cs deleted file mode 100644 index 8485cc331b..0000000000 --- a/src/Microsoft.DotNet.Watcher.Tools/Internal/Project.cs +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Microsoft.DotNet.ProjectModel.Graph; - -namespace Microsoft.DotNet.Watcher.Internal -{ - public class Project - { - public Project(ProjectModel.Project runtimeProject) - { - ProjectFile = runtimeProject.ProjectFilePath; - ProjectDirectory = runtimeProject.ProjectDirectory; - - var compilerOptions = runtimeProject.GetCompilerOptions(targetFramework: null, configurationName: null); - - var filesToWatch = new List() { runtimeProject.ProjectFilePath }; - if (compilerOptions?.CompileInclude != null) - { - filesToWatch.AddRange(compilerOptions.CompileInclude.ResolveFiles()); - } - else - { - filesToWatch.AddRange(runtimeProject.Files.SourceFiles); - } - - if (compilerOptions?.EmbedInclude != null) - { - filesToWatch.AddRange(compilerOptions.EmbedInclude.ResolveFiles()); - } - else - { - // For resource files the key is the name of the file, not the value - filesToWatch.AddRange(runtimeProject.Files.ResourceFiles.Keys); - } - - filesToWatch.AddRange(runtimeProject.Files.SharedFiles); - filesToWatch.AddRange(runtimeProject.Files.PreprocessSourceFiles); - - Files = filesToWatch; - - var projectLockJsonPath = Path.Combine(runtimeProject.ProjectDirectory, "project.lock.json"); - - if (File.Exists(projectLockJsonPath)) - { - var lockFile = LockFileReader.Read(projectLockJsonPath, designTime: false); - ProjectDependencies = lockFile.ProjectLibraries - .Where(dep => !string.IsNullOrEmpty(dep.Path)) // The dependency path is null for xproj -> csproj reference - .Select(dep => GetProjectRelativeFullPath(dep.Path)) - .ToList(); - } - else - { - ProjectDependencies = new string[0]; - } - } - - public IEnumerable ProjectDependencies { get; private set; } - - public IEnumerable Files { get; private set; } - - public string ProjectFile { get; private set; } - - public string ProjectDirectory { get; private set; } - - private string GetProjectRelativeFullPath(string path) - { - return Path.GetFullPath(Path.Combine(ProjectDirectory, path)); - } - } -} diff --git a/src/Microsoft.DotNet.Watcher.Tools/Internal/ProjectJsonFileSet.cs b/src/Microsoft.DotNet.Watcher.Tools/Internal/ProjectJsonFileSet.cs deleted file mode 100644 index c9d67daf5c..0000000000 --- a/src/Microsoft.DotNet.Watcher.Tools/Internal/ProjectJsonFileSet.cs +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections; -using System.Collections.Generic; - -namespace Microsoft.DotNet.Watcher.Internal -{ - public class ProjectJsonFileSet : IFileSet - { - private readonly string _projectFile; - private ISet _currentFiles; - - public ProjectJsonFileSet(string projectFile) - { - _projectFile = projectFile; - } - - public bool Contains(string filePath) - { - // if it was in the original list of files we were watching - if (_currentFiles?.Contains(filePath) == true) - { - return true; - } - - // It's possible the new file was not in the old set but will be in the new set. - // Additions should be considered part of this. - RefreshFileList(); - - return _currentFiles.Contains(filePath); - } - - public IEnumerator GetEnumerator() - { - EnsureInitialized(); - return _currentFiles.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - EnsureInitialized(); - return _currentFiles.GetEnumerator(); - } - - private void EnsureInitialized() - { - if (_currentFiles == null) - { - RefreshFileList(); - } - } - - private void RefreshFileList() - { - _currentFiles = new HashSet(FindFiles(), StringComparer.OrdinalIgnoreCase); - } - - private IEnumerable FindFiles() - { - var projects = new HashSet(); // temporary store to prevent re-parsing a project multiple times - return GetProjectFilesClosure(_projectFile, projects); - } - - private IEnumerable GetProjectFilesClosure(string projectFile, ISet projects) - { - if (projects.Contains(projectFile)) - { - yield break; - } - - projects.Add(projectFile); - - Project project; - string errors; - - if (ProjectReader.TryReadProject(projectFile, out project, out errors)) - { - foreach (var file in project.Files) - { - yield return file; - } - - foreach (var dependency in project.ProjectDependencies) - { - foreach (var file in GetProjectFilesClosure(dependency, projects)) - { - yield return file; - } - } - } - } - } -} diff --git a/src/Microsoft.DotNet.Watcher.Tools/Internal/ProjectJsonFileSetFactory.cs b/src/Microsoft.DotNet.Watcher.Tools/Internal/ProjectJsonFileSetFactory.cs deleted file mode 100644 index 419776d4dc..0000000000 --- a/src/Microsoft.DotNet.Watcher.Tools/Internal/ProjectJsonFileSetFactory.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; - -namespace Microsoft.DotNet.Watcher.Internal -{ - public class ProjectJsonFileSetFactory : IFileSetFactory - { - private readonly ILogger _logger; - private readonly string _projectFile; - public ProjectJsonFileSetFactory(ILogger logger, string projectFile) - { - Ensure.NotNull(logger, nameof(logger)); - Ensure.NotNullOrEmpty(projectFile, nameof(projectFile)); - - _logger = logger; - _projectFile = projectFile; - } - - public async Task CreateAsync(CancellationToken cancellationToken) - { - while (true) - { - cancellationToken.ThrowIfCancellationRequested(); - - Project project; - string errors; - if (ProjectReader.TryReadProject(_projectFile, out project, out errors)) - { - return new ProjectJsonFileSet(_projectFile); - } - - _logger.LogError($"Error(s) reading project file '{_projectFile}': "); - _logger.LogError(errors); - _logger.LogInformation("Fix the error to continue."); - - var fileSet = new FileSet(new[] { _projectFile }); - - using (var watcher = new FileSetWatcher(fileSet)) - { - await watcher.GetChangedFileAsync(cancellationToken); - - _logger.LogInformation($"File changed: {_projectFile}"); - } - } - } - } -} diff --git a/src/Microsoft.DotNet.Watcher.Tools/Internal/ProjectReaderUtils.cs b/src/Microsoft.DotNet.Watcher.Tools/Internal/ProjectReaderUtils.cs deleted file mode 100644 index ffd2e012f4..0000000000 --- a/src/Microsoft.DotNet.Watcher.Tools/Internal/ProjectReaderUtils.cs +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Linq; -using System.Text; - -namespace Microsoft.DotNet.Watcher.Internal -{ - public class ProjectReader - { - public static bool TryReadProject(string projectFile, out Project project, out string errors) - { - errors = null; - project = null; - - ProjectModel.Project runtimeProject; - if (!TryGetProject(projectFile, out runtimeProject, out errors)) - { - return false; - } - - try - { - project = new Project(runtimeProject); - } - catch (Exception ex) - { - errors = CollectMessages(ex); - return false; - } - - return true; - } - - private static bool TryGetProject(string projectFile, out ProjectModel.Project project, out string errorMessage) - { - try - { - if (!ProjectModel.ProjectReader.TryGetProject(projectFile, out project)) - { - if (project?.Diagnostics != null && project.Diagnostics.Any()) - { - errorMessage = string.Join(Environment.NewLine, project.Diagnostics.Select(e => e.ToString())); - } - else - { - errorMessage = "Failed to read project.json"; - } - } - else - { - errorMessage = null; - return true; - } - } - catch (Exception ex) - { - errorMessage = CollectMessages(ex); - } - - project = null; - return false; - } - - private static string CollectMessages(Exception exception) - { - var builder = new StringBuilder(); - builder.AppendLine(exception.Message); - - var aggregateException = exception as AggregateException; - if (aggregateException != null) - { - foreach (var message in aggregateException.Flatten().InnerExceptions.Select(CollectMessages)) - { - builder.AppendLine(message); - } - } - - while (exception.InnerException != null) - { - builder.AppendLine(CollectMessages(exception.InnerException)); - exception = exception.InnerException; - } - return builder.ToString(); - } - } -} diff --git a/src/Microsoft.DotNet.Watcher.Tools/Microsoft.DotNet.Watcher.Tools.nuspec b/src/Microsoft.DotNet.Watcher.Tools/Microsoft.DotNet.Watcher.Tools.nuspec index 53b80837cd..259eaa5cfb 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/Microsoft.DotNet.Watcher.Tools.nuspec +++ b/src/Microsoft.DotNet.Watcher.Tools/Microsoft.DotNet.Watcher.Tools.nuspec @@ -24,6 +24,7 @@ - + + \ No newline at end of file diff --git a/src/Microsoft.DotNet.Watcher.Tools/Program.cs b/src/Microsoft.DotNet.Watcher.Tools/Program.cs index 14a115c978..06cedfa8cf 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/Program.cs +++ b/src/Microsoft.DotNet.Watcher.Tools/Program.cs @@ -17,15 +17,18 @@ namespace Microsoft.DotNet.Watcher private readonly CancellationToken _cancellationToken; private readonly TextWriter _stdout; private readonly TextWriter _stderr; + private readonly string _workingDir; - public Program(TextWriter consoleOutput, TextWriter consoleError, CancellationToken cancellationToken) + public Program(TextWriter consoleOutput, TextWriter consoleError, string workingDir, CancellationToken cancellationToken) { Ensure.NotNull(consoleOutput, nameof(consoleOutput)); Ensure.NotNull(consoleError, nameof(consoleError)); + Ensure.NotNullOrEmpty(workingDir, nameof(workingDir)); _cancellationToken = cancellationToken; _stdout = consoleOutput; _stderr = consoleError; + _workingDir = workingDir; } public static int Main(string[] args) @@ -50,7 +53,7 @@ namespace Microsoft.DotNet.Watcher try { - return new Program(Console.Out, Console.Error, ctrlCTokenSource.Token) + return new Program(Console.Out, Console.Error, Directory.GetCurrentDirectory(), ctrlCTokenSource.Token) .MainInternalAsync(args) .GetAwaiter() .GetResult(); @@ -92,8 +95,10 @@ namespace Microsoft.DotNet.Watcher loggerFactory.AddProvider(commandProvider); var logger = loggerFactory.CreateLogger(LoggerName); - var projectFile = Path.Combine(Directory.GetCurrentDirectory(), ProjectModel.Project.FileName); - var projectFileSetFactory = new ProjectJsonFileSetFactory(logger, projectFile); + // TODO multiple projects should be easy enough to add here + var projectFile = MsBuildProjectFinder.FindMsBuildProject(_workingDir, options.Project); + var fileSetFactory = new MsBuildFileSetFactory(logger, projectFile); + var processInfo = new ProcessSpec { Executable = new Muxer().MuxerPath, @@ -102,7 +107,7 @@ namespace Microsoft.DotNet.Watcher }; await new DotNetWatcher(logger) - .WatchAsync(processInfo, projectFileSetFactory, _cancellationToken); + .WatchAsync(processInfo, fileSetFactory, _cancellationToken); return 0; } diff --git a/src/Microsoft.DotNet.Watcher.Tools/Properties/Resources.Designer.cs b/src/Microsoft.DotNet.Watcher.Tools/Properties/Resources.Designer.cs index 5cdf225bb8..30c1d18cb7 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/Properties/Resources.Designer.cs +++ b/src/Microsoft.DotNet.Watcher.Tools/Properties/Resources.Designer.cs @@ -10,6 +10,54 @@ namespace Microsoft.DotNet.Watcher.Tools private static readonly ResourceManager _resourceManager = new ResourceManager("Microsoft.DotNet.Watcher.Tools.Resources", typeof(Resources).GetTypeInfo().Assembly); + /// + /// The project file '{path}' does not exist. + /// + internal static string Error_ProjectPath_NotFound + { + get { return GetString("Error_ProjectPath_NotFound"); } + } + + /// + /// The project file '{path}' does not exist. + /// + internal static string FormatError_ProjectPath_NotFound(object path) + { + return string.Format(CultureInfo.CurrentCulture, GetString("Error_ProjectPath_NotFound", "path"), path); + } + + /// + /// Multiple MSBuild project files found in '{projectPath}'. Specify which to use with the --project option. + /// + internal static string Error_MultipleProjectsFound + { + get { return GetString("Error_MultipleProjectsFound"); } + } + + /// + /// Multiple MSBuild project files found in '{projectPath}'. Specify which to use with the --project option. + /// + internal static string FormatError_MultipleProjectsFound(object projectPath) + { + return string.Format(CultureInfo.CurrentCulture, GetString("Error_MultipleProjectsFound", "projectPath"), projectPath); + } + + /// + /// Could not find a MSBuild project file in '{projectPath}'. Specify which project to use with the --project option. + /// + internal static string Error_NoProjectsFound + { + get { return GetString("Error_NoProjectsFound"); } + } + + /// + /// Could not find a MSBuild project file in '{projectPath}'. Specify which project to use with the --project option. + /// + internal static string FormatError_NoProjectsFound(object projectPath) + { + return string.Format(CultureInfo.CurrentCulture, GetString("Error_NoProjectsFound", "projectPath"), projectPath); + } + /// /// Cannot specify both '--quiet' and '--verbose' options. /// diff --git a/src/Microsoft.DotNet.Watcher.Tools/README.md b/src/Microsoft.DotNet.Watcher.Tools/README.md index 7a21544e70..ec769af3e6 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/README.md +++ b/src/Microsoft.DotNet.Watcher.Tools/README.md @@ -45,3 +45,61 @@ Some configuration options can be passed to `dotnet watch` through environment v | Variable | Effect | | ---------------------------------------------- | -------------------------------------------------------- | | DOTNET_USE_POLLING_FILE_WATCHER | If set to "1" or "true", `dotnet watch` will use a polling file watcher instead of CoreFx's `FileSystemWatcher`. Used when watching files on network shares or Docker mounted volumes. | + +### MSBuild + +dotnet-watch can be configured from the MSBuild project file being watched. + +**Project References** + +By default, dotnet-watch will scan the entire graph of project references and watch all files within those projects. + +dotnet-watch will ignore project references with the `Watch="false"` attribute. + +```xml + + + +``` + +**Watch items** + +dotnet-watch will watch all items in the "Watch" item group. +By default, this group inclues all items in "Compile" and "EmbeddedResource". + +More items can be added to watch in a project file by adding items to 'Watch'. + +Example: + +```xml + + + + +``` + +dotnet-watch will ignore Compile and EmbeddedResource items with the `Watch="false"` attribute. + +Example: + +```xml + + + + + +``` + + +**Advanced configuration** + +dotnet-watch performs a design-time build to find items to watch. +When this build is run, dotnet-watch will set the property `DotNetWatchBuild=true`. + +Example: + +```xml + + + +``` \ No newline at end of file diff --git a/src/Microsoft.DotNet.Watcher.Tools/Resources.resx b/src/Microsoft.DotNet.Watcher.Tools/Resources.resx index 34336a97ba..125a4cfea3 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/Resources.resx +++ b/src/Microsoft.DotNet.Watcher.Tools/Resources.resx @@ -1,17 +1,17 @@  - @@ -117,6 +117,15 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + The project file '{path}' does not exist. + + + Multiple MSBuild project files found in '{projectPath}'. Specify which to use with the --project option. + + + Could not find a MSBuild project file in '{projectPath}'. Specify which project to use with the --project option. + Cannot specify both '--quiet' and '--verbose' options. diff --git a/src/Microsoft.DotNet.Watcher.Tools/dotnetwatch.targets b/src/Microsoft.DotNet.Watcher.Tools/dotnetwatch.targets new file mode 100644 index 0000000000..d3e73557be --- /dev/null +++ b/src/Microsoft.DotNet.Watcher.Tools/dotnetwatch.targets @@ -0,0 +1,18 @@ + + + + <_DotNetWatchTargetsFile Condition="'$(_DotNetWatchTargetsFile)' == ''">$(MSBuildThisFileFullPath) + + + + + + + + + + + \ No newline at end of file diff --git a/src/Microsoft.DotNet.Watcher.Tools/project.json b/src/Microsoft.DotNet.Watcher.Tools/project.json index e54d7b3774..21d7ce1d69 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/project.json +++ b/src/Microsoft.DotNet.Watcher.Tools/project.json @@ -5,15 +5,29 @@ "tags": [ "dotnet", "watch" - ] + ], + "files": { + "include": "tools/*.targets" + } }, "buildOptions": { "outputName": "dotnet-watch", "warningsAsErrors": true, "emitEntryPoint": true, - "keyFile": "../../tools/Key.snk" + "debugType": "portable", + "keyFile": "../../tools/Key.snk", + "copyToOutput": "tools/*.targets", + "embed": { + "mappings": { + "dotnetwatch.targets": "dotnetwatch.targets" + } + } }, "dependencies": { + "Microsoft.DotNet.ProjectModel": { + "version": "1.0.0-*", + "exclude": "all" + }, "Microsoft.DotNet.Cli.Utils": "1.0.0-*", "Microsoft.Extensions.CommandLineUtils": "1.1.0-*", "Microsoft.Extensions.Logging": "1.1.0-*", diff --git a/src/Microsoft.DotNet.Watcher.Tools/tools/DotNetWatchCommon.targets b/src/Microsoft.DotNet.Watcher.Tools/tools/DotNetWatchCommon.targets new file mode 100644 index 0000000000..c3477393fc --- /dev/null +++ b/src/Microsoft.DotNet.Watcher.Tools/tools/DotNetWatchCommon.targets @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Microsoft.DotNet.Watcher.Tools/tools/DotNetWatchInner.targets b/src/Microsoft.DotNet.Watcher.Tools/tools/DotNetWatchInner.targets new file mode 100644 index 0000000000..9bdd83dd6d --- /dev/null +++ b/src/Microsoft.DotNet.Watcher.Tools/tools/DotNetWatchInner.targets @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + <_DotNetWatchProjects Include="@(ProjectReference->'%(FullPath)')" Condition="'%(ProjectReference.Watch)' != 'false'" /> + <_DotNetWatchImportsTargets Include="@(_DotNetWatchProjects->'%(RelativeDir)obj\%(FileName)%(Extension).dotnetwatch.targets')"> + $(_DotNetWatchTargetsFile) + + + + + + + + + + + <_DotNetWatchProjects Include="$(MSBuildProjectFullPath)"/> + + + \ No newline at end of file diff --git a/src/Microsoft.DotNet.Watcher.Tools/tools/DotNetWatchOuter.targets b/src/Microsoft.DotNet.Watcher.Tools/tools/DotNetWatchOuter.targets new file mode 100644 index 0000000000..f2dfb9e104 --- /dev/null +++ b/src/Microsoft.DotNet.Watcher.Tools/tools/DotNetWatchOuter.targets @@ -0,0 +1,69 @@ + + + + + + + + + <_TargetFramework Include="$(TargetFrameworks)" /> + + + + + + + + + + <_TargetFramework Include="$(TargetFrameworks)" /> + + + + + + diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/AppWithDepsTests.cs b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/AppWithDepsTests.cs index d36674a223..6428f7f6fe 100644 --- a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/AppWithDepsTests.cs +++ b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/AppWithDepsTests.cs @@ -4,6 +4,7 @@ using System; using System.IO; using Xunit; +using Xunit.Abstractions; namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests { @@ -11,11 +12,18 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests { private static readonly TimeSpan _defaultTimeout = TimeSpan.FromSeconds(30); + private readonly ITestOutputHelper _logger; + + public AppWithDepsTests(ITestOutputHelper logger) + { + _logger = logger; + } + // Change a file included in compilation [Fact] public void ChangeFileInDependency() { - using (var scenario = new AppWithDepsScenario()) + using (var scenario = new AppWithDepsScenario(_logger)) { scenario.Start(); using (var wait = new WaitForFileToChange(scenario.StartedFile)) @@ -36,18 +44,19 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests private const string AppWithDeps = "AppWithDeps"; private const string Dependency = "Dependency"; - public AppWithDepsScenario() + public AppWithDepsScenario(ITestOutputHelper logger) + : base(logger) { - StatusFile = Path.Combine(_scenario.TempFolder, "status"); + StatusFile = Path.Combine(Scenario.TempFolder, "status"); StartedFile = StatusFile + ".started"; - - _scenario.AddTestProjectFolder(AppWithDeps); - _scenario.AddTestProjectFolder(Dependency); - _scenario.Restore(); + Scenario.AddTestProjectFolder(AppWithDeps); + Scenario.AddTestProjectFolder(Dependency); - AppWithDepsFolder = Path.Combine(_scenario.WorkFolder, AppWithDeps); - DependencyFolder = Path.Combine(_scenario.WorkFolder, Dependency); + Scenario.Restore3(AppWithDeps); // restore3 should be transitive + + AppWithDepsFolder = Path.Combine(Scenario.WorkFolder, AppWithDeps); + DependencyFolder = Path.Combine(Scenario.WorkFolder, Dependency); } public void Start() @@ -55,7 +64,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests // Wait for the process to start using (var wait = new WaitForFileToChange(StatusFile)) { - RunDotNetWatch(new[] { "run", StatusFile }, Path.Combine(_scenario.WorkFolder, AppWithDeps)); + RunDotNetWatch(new[] { "run3", StatusFile }, Path.Combine(Scenario.WorkFolder, AppWithDeps)); wait.Wait(_defaultTimeout, expectedToChange: true, diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/GlobbingAppTests.cs b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/GlobbingAppTests.cs index 87ae1b6df3..31b0cb9167 100644 --- a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/GlobbingAppTests.cs +++ b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/GlobbingAppTests.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.IO; using System.Threading; using Xunit; +using Xunit.Abstractions; namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests { @@ -15,6 +16,13 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests private static readonly TimeSpan _negativeTestWaitTime = TimeSpan.FromSeconds(10); + private readonly ITestOutputHelper _logger; + + public GlobbingAppTests(ITestOutputHelper logger) + { + _logger = logger; + } + [Fact] public void ChangeCompiledFile_PollingWatcher() { @@ -30,7 +38,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests // Change a file included in compilation private void ChangeCompiledFile(bool usePollingWatcher) { - using (var scenario = new GlobbingAppScenario()) + using (var scenario = new GlobbingAppScenario(_logger)) using (var wait = new WaitForFileToChange(scenario.StartedFile)) { scenario.UsePollingWatcher = usePollingWatcher; @@ -53,7 +61,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests public void AddCompiledFile() { // Add a file in a folder that's included in compilation - using (var scenario = new GlobbingAppScenario()) + using (var scenario = new GlobbingAppScenario(_logger)) using (var wait = new WaitForFileToChange(scenario.StartedFile)) { scenario.Start(); @@ -71,7 +79,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests [Fact] public void DeleteCompiledFile() { - using (var scenario = new GlobbingAppScenario()) + using (var scenario = new GlobbingAppScenario(_logger)) using (var wait = new WaitForFileToChange(scenario.StartedFile)) { scenario.Start(); @@ -86,10 +94,10 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests } // Delete an entire folder - [Fact(Skip = "Blocking build")] + [Fact] public void DeleteSourceFolder() { - using (var scenario = new GlobbingAppScenario()) + using (var scenario = new GlobbingAppScenario(_logger)) using (var wait = new WaitForFileToChange(scenario.StartedFile)) { scenario.Start(); @@ -107,7 +115,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests [Fact] public void RenameCompiledFile() { - using (var scenario = new GlobbingAppScenario()) + using (var scenario = new GlobbingAppScenario(_logger)) using (var wait = new WaitForFileToChange(scenario.StatusFile)) { scenario.Start(); @@ -137,7 +145,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests // Add a file that's in a included folder but not matching the globbing pattern private void ChangeNonCompiledFile(bool usePollingWatcher) { - using (var scenario = new GlobbingAppScenario()) + using (var scenario = new GlobbingAppScenario(_logger)) { scenario.UsePollingWatcher = usePollingWatcher; @@ -162,7 +170,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests [Fact] public void ChangeExcludedFile() { - using (var scenario = new GlobbingAppScenario()) + using (var scenario = new GlobbingAppScenario(_logger)) { scenario.Start(); @@ -185,15 +193,16 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests { private const string TestAppName = "GlobbingApp"; - public GlobbingAppScenario() + public GlobbingAppScenario(ITestOutputHelper logger) + : base(logger) { - StatusFile = Path.Combine(_scenario.TempFolder, "status"); + StatusFile = Path.Combine(Scenario.TempFolder, "status"); StartedFile = StatusFile + ".started"; - _scenario.AddTestProjectFolder(TestAppName); - _scenario.Restore(); + Scenario.AddTestProjectFolder(TestAppName); + Scenario.Restore3(TestAppName); - TestAppFolder = Path.Combine(_scenario.WorkFolder, TestAppName); + TestAppFolder = Path.Combine(Scenario.WorkFolder, TestAppName); } public void Start() @@ -201,7 +210,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests // Wait for the process to start using (var wait = new WaitForFileToChange(StartedFile)) { - RunDotNetWatch(new[] { "run", StatusFile }, Path.Combine(_scenario.WorkFolder, TestAppName)); + RunDotNetWatch(new[] { "run3", StatusFile }, Path.Combine(Scenario.WorkFolder, TestAppName)); wait.Wait(_defaultTimeout, expectedToChange: true, diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/NoDepsAppTests.cs b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/NoDepsAppTests.cs index 023b57512f..7c385a6ed1 100644 --- a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/NoDepsAppTests.cs +++ b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/NoDepsAppTests.cs @@ -7,22 +7,29 @@ using System.Diagnostics; using System.IO; using System.Threading; using Xunit; +using Xunit.Abstractions; namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests { public class NoDepsAppTests { private static readonly TimeSpan _defaultTimeout = TimeSpan.FromSeconds(30); + private readonly ITestOutputHelper _logger; + + public NoDepsAppTests(ITestOutputHelper logger) + { + _logger = logger; + } [Fact] public void RestartProcessOnFileChange() { - using (var scenario = new NoDepsAppScenario()) + using (var scenario = new NoDepsAppScenario(_logger)) { // Wait for the process to start using (var wait = new WaitForFileToChange(scenario.StartedFile)) { - scenario.RunDotNetWatch(new[] { "run", scenario.StatusFile, "--no-exit" }); + scenario.RunDotNetWatch(new[] { "run3", "-f", "netcoreapp1.0", scenario.StatusFile, "--no-exit" }); wait.Wait(_defaultTimeout, expectedToChange: true, @@ -56,12 +63,12 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests [Fact] public void RestartProcessThatTerminatesAfterFileChange() { - using (var scenario = new NoDepsAppScenario()) + using (var scenario = new NoDepsAppScenario(_logger)) { // Wait for the process to start using (var wait = new WaitForFileToChange(scenario.StartedFile)) { - scenario.RunDotNetWatch(new[] { "run", scenario.StatusFile }); + scenario.RunDotNetWatch(new[] { "run3", "-f", "netcoreapp1.0", scenario.StatusFile }); wait.Wait(_defaultTimeout, expectedToChange: true, @@ -101,15 +108,16 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests { private const string TestAppName = "NoDepsApp"; - public NoDepsAppScenario() + public NoDepsAppScenario(ITestOutputHelper logger) + : base(logger) { - StatusFile = Path.Combine(_scenario.TempFolder, "status"); + StatusFile = Path.Combine(Scenario.TempFolder, "status"); StartedFile = StatusFile + ".started"; - _scenario.AddTestProjectFolder(TestAppName); - _scenario.Restore(); + Scenario.AddTestProjectFolder(TestAppName); + Scenario.Restore3(TestAppName); - TestAppFolder = Path.Combine(_scenario.WorkFolder, TestAppName); + TestAppFolder = Path.Combine(Scenario.WorkFolder, TestAppName); } public string StatusFile { get; private set; } @@ -118,7 +126,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests public void RunDotNetWatch(IEnumerable args) { - RunDotNetWatch(args, Path.Combine(_scenario.WorkFolder, TestAppName)); + RunDotNetWatch(args, Path.Combine(Scenario.WorkFolder, TestAppName)); } } } diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/Scenario/DotNetWatchScenario.cs b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/Scenario/DotNetWatchScenario.cs index bef42282fe..a49a0ed616 100644 --- a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/Scenario/DotNetWatchScenario.cs +++ b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/Scenario/DotNetWatchScenario.cs @@ -6,16 +6,21 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using Microsoft.Extensions.Internal; +using Xunit.Abstractions; namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests { public class DotNetWatchScenario : IDisposable { - protected ProjectToolScenario _scenario; - + protected ProjectToolScenario Scenario { get; } public DotNetWatchScenario() + : this(null) { - _scenario = new ProjectToolScenario(); + } + + public DotNetWatchScenario(ITestOutputHelper logger) + { + Scenario = new ProjectToolScenario(logger); } public Process WatcherProcess { get; private set; } @@ -33,7 +38,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests }; } - WatcherProcess = _scenario.ExecuteDotnetWatch(arguments, workingFolder, envVariables); + WatcherProcess = Scenario.ExecuteDotnetWatch(arguments, workingFolder, envVariables); } public virtual void Dispose() @@ -46,7 +51,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests } WatcherProcess.Dispose(); } - _scenario.Dispose(); + Scenario.Dispose(); } } } diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/Scenario/ProjectToolScenario.cs b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/Scenario/ProjectToolScenario.cs index c0c2ce0016..73eff54e4a 100644 --- a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/Scenario/ProjectToolScenario.cs +++ b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/Scenario/ProjectToolScenario.cs @@ -10,6 +10,7 @@ using System.Reflection; using System.Threading; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.ProjectModel; +using Xunit.Abstractions; namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests { @@ -17,13 +18,17 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests { private const string NugetConfigFileName = "NuGet.config"; private static readonly string TestProjectSourceRoot = Path.Combine(AppContext.BaseDirectory, "TestProjects"); - - private static readonly object _restoreLock = new object(); + private readonly ITestOutputHelper _logger; public ProjectToolScenario() + : this(null) { - Console.WriteLine($"The temporary test folder is {TempFolder}"); + } + public ProjectToolScenario(ITestOutputHelper logger) + { + _logger = logger; + _logger?.WriteLine($"The temporary test folder is {TempFolder}"); WorkFolder = Path.Combine(TempFolder, "work"); CreateTestDirectory(); @@ -37,7 +42,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests { var srcFolder = Path.Combine(TestProjectSourceRoot, projectName); var destinationFolder = Path.Combine(WorkFolder, Path.GetFileName(projectName)); - Console.WriteLine($"Copying project {srcFolder} to {destinationFolder}"); + _logger?.WriteLine($"Copying project {srcFolder} to {destinationFolder}"); Directory.CreateDirectory(destinationFolder); @@ -63,19 +68,40 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests project = Path.Combine(WorkFolder, project); } - // Tests are run in parallel and they try to restore tools concurrently. - // This causes issues because the deps json file for a tool is being written from - // multiple threads - which results in either sharing violation or corrupted json. - lock (_restoreLock) - { - var restore = Command - .CreateDotNet("restore", new[] { project }) - .Execute(); + _logger?.WriteLine($"Restoring project in {project}"); - if (restore.ExitCode != 0) - { - throw new Exception($"Exit code {restore.ExitCode}"); - } + var restore = Command + .CreateDotNet("restore", new[] { project }) + .CaptureStdErr() + .CaptureStdOut() + .OnErrorLine(l => _logger?.WriteLine(l)) + .OnOutputLine(l => _logger?.WriteLine(l)) + .Execute(); + + if (restore.ExitCode != 0) + { + throw new Exception($"Exit code {restore.ExitCode}"); + } + } + + public void Restore3(string project) + { + project = Path.Combine(WorkFolder, project); + + _logger?.WriteLine($"Restoring msbuild project in {project}"); + + var restore = Command + .CreateDotNet("restore3", new [] { "/v:m" }) + .WorkingDirectory(project) + .CaptureStdErr() + .CaptureStdOut() + .OnErrorLine(l => _logger?.WriteLine(l)) + .OnOutputLine(l => _logger?.WriteLine(l)) + .Execute(); + + if (restore.ExitCode != 0) + { + throw new Exception($"Exit code {restore.ExitCode}"); } } @@ -92,7 +118,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests public Process ExecuteDotnetWatch(IEnumerable arguments, string workDir, IDictionary environmentVariables = null) { - // this launches a new .NET Core process using the runtime of the current test app + // this launches a new .NET Core process using the runtime of the current test app // and the version of dotnet-watch that this test app is compiled against var thisAssembly = Path.GetFileNameWithoutExtension(GetType().GetTypeInfo().Assembly.Location); var args = new List(); @@ -108,12 +134,12 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests var argsStr = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(args.Concat(arguments)); - Console.WriteLine($"Running dotnet {argsStr} in {workDir}"); + _logger?.WriteLine($"Running dotnet {argsStr} in {workDir}"); var psi = new ProcessStartInfo(new Muxer().MuxerPath, argsStr) { UseShellExecute = false, - WorkingDirectory = workDir, + WorkingDirectory = workDir }; if (environmentVariables != null) diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/AppWithDeps/AppWithDeps.csproj b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/AppWithDeps/AppWithDeps.csproj new file mode 100644 index 0000000000..f06027243d --- /dev/null +++ b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/AppWithDeps/AppWithDeps.csproj @@ -0,0 +1,16 @@ + + + + + netcoreapp1.0 + Exe + + + + + + + + + + \ No newline at end of file diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/AppWithDeps/project.json b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/AppWithDeps/project.json deleted file mode 100644 index 8b1c2f6bab..0000000000 --- a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/AppWithDeps/project.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "buildOptions": { - "emitEntryPoint": true - }, - "dependencies": { - "Dependency": { - "target": "project" - } - }, - "frameworks": { - "netcoreapp1.0": { - "dependencies": { - "Microsoft.NETCore.App": { - "type": "platform", - "version": "1.1.0-*" - } - } - } - } -} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/Dependency/Dependency.csproj b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/Dependency/Dependency.csproj new file mode 100644 index 0000000000..243360cf92 --- /dev/null +++ b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/Dependency/Dependency.csproj @@ -0,0 +1,15 @@ + + + + + netstandard1.5 + Library + + + + + + + + + \ No newline at end of file diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/Dependency/project.json b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/Dependency/project.json deleted file mode 100644 index f697b65d49..0000000000 --- a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/Dependency/project.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "dependencies": { - "NETStandard.Library": "1.6.1-*" - }, - "frameworks": { - "netstandard1.5": {} - } -} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/GlobbingApp/GlobbingApp.csproj b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/GlobbingApp/GlobbingApp.csproj new file mode 100644 index 0000000000..cb088af5e3 --- /dev/null +++ b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/GlobbingApp/GlobbingApp.csproj @@ -0,0 +1,15 @@ + + + + + netcoreapp1.0 + Exe + + + + + + + + + \ No newline at end of file diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/GlobbingApp/project.json b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/GlobbingApp/project.json deleted file mode 100644 index 6418c6d199..0000000000 --- a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/GlobbingApp/project.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "buildOptions": { - "emitEntryPoint": true, - "compile": { - "include": [ - "Program.cs", - "include/*.cs" - ], - "exclude": [ - "exclude/*" - ] - } - }, - "frameworks": { - "netcoreapp1.0": { - "dependencies": { - "Microsoft.NETCore.App": { - "type": "platform", - "version": "1.1.0-*" - } - } - } - } -} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/NoDepsApp/NoDepsApp.csproj b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/NoDepsApp/NoDepsApp.csproj new file mode 100644 index 0000000000..67edbf1349 --- /dev/null +++ b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/NoDepsApp/NoDepsApp.csproj @@ -0,0 +1,15 @@ + + + + + netcoreapp1.0 + Exe + + + + + + + + + \ No newline at end of file diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/NoDepsApp/project.json b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/NoDepsApp/project.json deleted file mode 100644 index 4972d51a39..0000000000 --- a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/NoDepsApp/project.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "buildOptions": { - "emitEntryPoint": true - }, - "frameworks": { - "netcoreapp1.0": { - "dependencies": { - "Microsoft.NETCore.App": { - "type": "platform", - "version": "1.1.0-*" - } - } - } - } -} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/clean-assets.cmd b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/clean-assets.cmd index 63104d55c6..2d1d41f1a8 100644 --- a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/clean-assets.cmd +++ b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/clean-assets.cmd @@ -3,4 +3,9 @@ if not "%1" == "" ( echo "Deleting %1\TestProjects" rmdir /s /q %1\TestProjects -) \ No newline at end of file + echo "Deleting %1\tools" + rmdir /s /q %1\tools +) + +mkdir %1\tools +copy ..\..\src\Microsoft.DotNet.Watcher.Tools\tools\*.targets %1\tools \ No newline at end of file diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/clean-assets.sh b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/clean-assets.sh index 1a5e80fb71..5b096cd399 100755 --- a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/clean-assets.sh +++ b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/clean-assets.sh @@ -3,6 +3,12 @@ if [ -z $1 ]; then echo "Deleting $1/TestProjects" rm -rf $1/TestProjects + + echo "Deleting $1/tools" + rm -rf $1/tools fi +mkdir -p $1/tools +cp ../../src/Microsoft.DotNet.Watcher.Tools/tools/*.targets $1/tools + exit 0 \ No newline at end of file diff --git a/test/Microsoft.DotNet.Watcher.Tools.Tests/AssertEx.cs b/test/Microsoft.DotNet.Watcher.Tools.Tests/AssertEx.cs new file mode 100644 index 0000000000..48975a9175 --- /dev/null +++ b/test/Microsoft.DotNet.Watcher.Tools.Tests/AssertEx.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Xunit; + +namespace Microsoft.DotNetWatcher.Tools.Tests +{ + public static class AssertEx + { + public static void EqualFileList(string root, IEnumerable expectedFiles, IEnumerable actualFiles) + { + var expected = expectedFiles.Select(p => Path.Combine(root, p)); + EqualFileList(expected, actualFiles); + } + + public static void EqualFileList(IEnumerable expectedFiles, IEnumerable actualFiles) + { + Func normalize = p => p.Replace('\\', '/'); + var expected = new HashSet(expectedFiles.Select(normalize)); + Assert.True(expected.SetEquals(actualFiles.Select(normalize)), "File sets should be equal"); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Watcher.Tools.Tests/MsBuildFileSetFactoryTest.cs b/test/Microsoft.DotNet.Watcher.Tools.Tests/MsBuildFileSetFactoryTest.cs new file mode 100644 index 0000000000..555be13c70 --- /dev/null +++ b/test/Microsoft.DotNet.Watcher.Tools.Tests/MsBuildFileSetFactoryTest.cs @@ -0,0 +1,315 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.DotNet.Watcher; +using Microsoft.DotNet.Watcher.Internal; +using Microsoft.Extensions.Logging; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.DotNetWatcher.Tools.Tests +{ + using ItemSpec = TemporaryCSharpProject.ItemSpec; + + public class MsBuildFileSetFactoryTest : IDisposable + { + private ILogger _logger; + private readonly TemporaryDirectory _tempDir; + public MsBuildFileSetFactoryTest(ITestOutputHelper output) + { + _logger = new XunitLogger(output); + _tempDir = new TemporaryDirectory(); + } + + [Fact] + public async Task FindsCustomWatchItems() + { + TemporaryCSharpProject target; + _tempDir + .WithCSharpProject("Project1", out target) + .WithTargetFrameworks("netcoreapp1.0") + .WithDefaultGlobs() + .WithItem(new ItemSpec { Name = "Watch", Include = "*.js", Exclude = "gulpfile.js" }) + .Dir() + .WithFile("Program.cs") + .WithFile("app.js") + .WithFile("gulpfile.js"); + + var fileset = await GetFileSet(target); + + AssertEx.EqualFileList( + _tempDir.Root, + new[] + { + "Project1.csproj", + "Program.cs", + "app.js" + }, + fileset + ); + } + + [Fact] + public async Task ExcludesDefaultItemsWithWatchFalseMetadata() + { + TemporaryCSharpProject target; + _tempDir + .WithCSharpProject("Project1", out target) + .WithTargetFrameworks("net40") + .WithItem(new ItemSpec { Name = "Compile", Include = "*.cs" }) + .WithItem(new ItemSpec { Name = "EmbeddedResource", Include = "*.resx", Watch = false }) + .Dir() + .WithFile("Program.cs") + .WithFile("Strings.resx"); + + var fileset = await GetFileSet(target); + + AssertEx.EqualFileList( + _tempDir.Root, + new[] + { + "Project1.csproj", + "Program.cs", + }, + fileset + ); + } + + [Fact] + public async Task SingleTfm() + { + TemporaryCSharpProject target; + + _tempDir + .SubDir("src") + .SubDir("Project1") + .WithCSharpProject("Project1", out target) + .WithTargetFrameworks("netcoreapp1.0") + .WithDefaultGlobs() + .Dir() + .WithFile("Program.cs") + .WithFile("Class1.cs") + .SubDir("obj").WithFile("ignored.cs").Up() + .SubDir("Properties").WithFile("Strings.resx").Up() + .Up() + .Up() + .Create(); + + var fileset = await GetFileSet(target); + + AssertEx.EqualFileList( + _tempDir.Root, + new[] + { + "src/Project1/Project1.csproj", + "src/Project1/Program.cs", + "src/Project1/Class1.cs", + "src/Project1/Properties/Strings.resx", + }, + fileset + ); + } + + [Fact] + public async Task MultiTfm() + { + TemporaryCSharpProject target; + _tempDir + .SubDir("src") + .SubDir("Project1") + .WithCSharpProject("Project1", out target) + .WithTargetFrameworks("netcoreapp1.0", "net451") + .WithItem("Compile", "Class1.netcore.cs", "'$(TargetFramework)'=='netcoreapp1.0'") + .WithItem("Compile", "Class1.desktop.cs", "'$(TargetFramework)'=='net451'") + .Dir() + .WithFile("Class1.netcore.cs") + .WithFile("Class1.desktop.cs") + .WithFile("Class1.notincluded.cs"); + + var fileset = await GetFileSet(target); + + AssertEx.EqualFileList( + _tempDir.Root, + new[] + { + "src/Project1/Project1.csproj", + "src/Project1/Class1.netcore.cs", + "src/Project1/Class1.desktop.cs", + }, + fileset + ); + } + + [Fact] + public async Task ProjectReferences_OneLevel() + { + TemporaryCSharpProject target; + TemporaryCSharpProject proj2; + _tempDir + .SubDir("src") + .SubDir("Project2") + .WithCSharpProject("Project2", out proj2) + .WithTargetFrameworks("netstandard1.1") + .WithDefaultGlobs() + .Dir() + .WithFile("Class2.cs") + .Up() + .SubDir("Project1") + .WithCSharpProject("Project1", out target) + .WithTargetFrameworks("netcoreapp1.0", "net451") + .WithProjectReference(proj2) + .WithDefaultGlobs() + .Dir() + .WithFile("Class1.cs"); + + var fileset = await GetFileSet(target); + + AssertEx.EqualFileList( + _tempDir.Root, + new[] + { + "src/Project2/Project2.csproj", + "src/Project2/Class2.cs", + "src/Project1/Project1.csproj", + "src/Project1/Class1.cs", + }, + fileset + ); + } + + [Fact] + public async Task TransitiveProjectReferences_TwoLevels() + { + TemporaryCSharpProject target; + TemporaryCSharpProject proj2; + TemporaryCSharpProject proj3; + _tempDir + .SubDir("src") + .SubDir("Project3") + .WithCSharpProject("Project3", out proj3) + .WithTargetFrameworks("netstandard1.0") + .WithDefaultGlobs() + .Dir() + .WithFile("Class3.cs") + .Up() + .SubDir("Project2") + .WithCSharpProject("Project2", out proj2) + .WithTargetFrameworks("netstandard1.1") + .WithProjectReference(proj3) + .WithDefaultGlobs() + .Dir() + .WithFile("Class2.cs") + .Up() + .SubDir("Project1") + .WithCSharpProject("Project1", out target) + .WithTargetFrameworks("netcoreapp1.0", "net451") + .WithProjectReference(proj2) + .WithDefaultGlobs() + .Dir() + .WithFile("Class1.cs"); + + var fileset = await GetFileSet(target); + + AssertEx.EqualFileList( + _tempDir.Root, + new[] + { + "src/Project3/Project3.csproj", + "src/Project3/Class3.cs", + "src/Project2/Project2.csproj", + "src/Project2/Class2.cs", + "src/Project1/Project1.csproj", + "src/Project1/Class1.cs", + }, + fileset + ); + } + + [Fact] + public async Task ProjectReferences_Graph() + { + var graph = new TestProjectGraph(_tempDir); + graph.OnCreate(p => p.WithTargetFrameworks("net45").WithDefaultGlobs()); + var matches = Regex.Matches(@" + A->B B->C C->D D->E + B->E + A->F F->G G->E + F->E + W->U + Y->Z + Y->B + Y->F", + @"(\w)->(\w)"); + + Assert.Equal(13, matches.Count); + foreach (Match m in matches) + { + var target = graph.GetOrCreate(m.Groups[2].Value); + graph.GetOrCreate(m.Groups[1].Value).WithProjectReference(target); + } + + graph.Find("A").WithProjectReference(graph.Find("W"), watch: false); + + var output = new OutputSink(); + var filesetFactory = new MsBuildFileSetFactory(_logger, graph.GetOrCreate("A").Path, output) + { + // enables capturing markers to know which projects have been visited + BuildFlags = { "/p:_DotNetWatchTraceOutput=true" } + }; + + var fileset = await GetFileSet(filesetFactory); + + _logger.LogInformation(output.Current.GetAllLines("Sink output: ")); + + var includedProjects = new[] { "A", "B", "C", "D", "E", "F", "G" }; + AssertEx.EqualFileList( + _tempDir.Root, + includedProjects + .Select(p => $"{p}/{p}.csproj"), + fileset + ); + + // ensure unreachable projects exist but where not included + Assert.NotNull(graph.Find("W")); + Assert.NotNull(graph.Find("U")); + Assert.NotNull(graph.Find("Y")); + Assert.NotNull(graph.Find("Z")); + + // ensure each project is only visited once for collecting watch items + Assert.All(includedProjects, + projectName => + Assert.Single(output.Current.Lines, + line => line.Contains($"Collecting watch items from '{projectName}'")) + ); + + // ensure each project is only visited once to collect project references + Assert.All(includedProjects, + projectName => + Assert.Single(output.Current.Lines, + line => line.Contains($"Collecting referenced projects from '{projectName}'")) + ); + } + + private Task GetFileSet(TemporaryCSharpProject target) + => GetFileSet(new MsBuildFileSetFactory(_logger, target.Path)); + private async Task GetFileSet(MsBuildFileSetFactory filesetFactory) + { + _tempDir.Create(); + var createTask = filesetFactory.CreateAsync(CancellationToken.None); + var finished = await Task.WhenAny(createTask, Task.Delay(TimeSpan.FromSeconds(10))); + + Assert.Same(createTask, finished); + return createTask.Result; + } + + public void Dispose() + { + _tempDir.Dispose(); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Watcher.Tools.Tests/Utilities/TemporaryCSharpProject.cs b/test/Microsoft.DotNet.Watcher.Tools.Tests/Utilities/TemporaryCSharpProject.cs new file mode 100644 index 0000000000..bad0e0a949 --- /dev/null +++ b/test/Microsoft.DotNet.Watcher.Tools.Tests/Utilities/TemporaryCSharpProject.cs @@ -0,0 +1,106 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.DotNetWatcher.Tools.Tests +{ + public class TemporaryCSharpProject + { + private const string Template = + @" + + + {0} + Exe + + + {1} + + +"; + + private const string DefaultGlobs = +@" +"; + + private readonly string _filename; + private readonly TemporaryDirectory _directory; + private string[] _tfms; + private List _items = new List(); + + public TemporaryCSharpProject(string name, TemporaryDirectory directory) + { + Name = name; + _filename = name + ".csproj"; + _directory = directory; + } + + public string Name { get; } + public string Path => System.IO.Path.Combine(_directory.Root, _filename); + + public TemporaryCSharpProject WithTargetFrameworks(params string[] tfms) + { + _tfms = tfms; + return this; + } + + public TemporaryCSharpProject WithItem(string itemName, string include, string condition = null) + => WithItem(new ItemSpec { Name = itemName, Include = include, Condition = condition }); + + public TemporaryCSharpProject WithItem(ItemSpec item) + { + var sb = new StringBuilder("<"); + sb.Append(item.Name).Append(" "); + if (item.Include != null) sb.Append(" Include=\"").Append(item.Include).Append('"'); + if (item.Remove != null) sb.Append(" Remove=\"").Append(item.Remove).Append('"'); + if (item.Exclude != null) sb.Append(" Exclude=\"").Append(item.Exclude).Append('"'); + if (item.Condition != null) sb.Append(" Exclude=\"").Append(item.Condition).Append('"'); + if (!item.Watch) sb.Append(" Watch=\"false\" "); + sb.Append(" />"); + _items.Add(sb.ToString()); + return this; + } + + public TemporaryCSharpProject WithProjectReference(TemporaryCSharpProject reference, bool watch = true) + { + if (ReferenceEquals(this, reference)) + { + throw new InvalidOperationException("Can add project reference to self"); + } + + return WithItem(new ItemSpec { Name = "ProjectReference", Include = reference.Path, Watch = watch }); + } + + public TemporaryCSharpProject WithDefaultGlobs() + { + _items.Add(DefaultGlobs); + return this; + } + + public TemporaryDirectory Dir() => _directory; + + public void Create() + { + var tfm = _tfms == null || _tfms.Length == 0 + ? string.Empty + : _tfms.Length == 1 + ? $"{_tfms[0]}" + : $"{string.Join(";", _tfms)}"; + + _directory.CreateFile(_filename, string.Format(Template, tfm, string.Join("\r\n", _items))); + } + + public class ItemSpec + { + public string Name { get; set; } + public string Include { get; set; } + public string Exclude { get; set; } + public string Remove { get; set; } + public bool Watch { get; set; } = true; + public string Condition { get; set; } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Watcher.Tools.Tests/Utilities/TemporaryDirectory.cs b/test/Microsoft.DotNet.Watcher.Tools.Tests/Utilities/TemporaryDirectory.cs new file mode 100644 index 0000000000..ee90092e22 --- /dev/null +++ b/test/Microsoft.DotNet.Watcher.Tools.Tests/Utilities/TemporaryDirectory.cs @@ -0,0 +1,107 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; + +namespace Microsoft.DotNetWatcher.Tools.Tests +{ + public class TemporaryDirectory : IDisposable + { + private List _projects = new List(); + private List _subdirs = new List(); + private List _files = new List(); + private TemporaryDirectory _parent; + + public TemporaryDirectory() + { + Root = Path.Combine(Path.GetTempPath(), "dotnet-watch-tests", Guid.NewGuid().ToString("N")); + } + + private TemporaryDirectory(string path, TemporaryDirectory parent) + { + _parent = parent; + Root = path; + } + + public TemporaryDirectory SubDir(string name) + { + var subdir = new TemporaryDirectory(Path.Combine(Root, name), this); + _subdirs.Add(subdir); + return subdir; + } + + public string Root { get; } + + public TemporaryCSharpProject WithCSharpProject(string name) + { + var project = new TemporaryCSharpProject(name, this); + _projects.Add(project); + return project; + } + + public TemporaryCSharpProject WithCSharpProject(string name, out TemporaryCSharpProject project) + { + project = WithCSharpProject(name); + return project; + } + + public TemporaryDirectory WithFile(string name) + { + _files.Add(name); + return this; + } + + public TemporaryDirectory Up() + { + if (_parent == null) + { + throw new InvalidOperationException("This is the root directory"); + } + return _parent; + } + + public void Create() + { + Directory.CreateDirectory(Root); + + foreach (var dir in _subdirs) + { + dir.Create(); + } + + foreach (var project in _projects) + { + project.Create(); + } + + foreach (var file in _files) + { + CreateFile(file, string.Empty); + } + } + + public void CreateFile(string filename, string contents) + { + File.WriteAllText(Path.Combine(Root, filename), contents); + } + + public void Dispose() + { + if (Root == null || !Directory.Exists(Root) || _parent != null) + { + return; + } + + try + { + Directory.Delete(Root, recursive: true); + } + catch + { + Console.Error.WriteLine($"Test cleanup failed to delete '{Root}'"); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Watcher.Tools.Tests/Utilities/TestProjectGraph.cs b/test/Microsoft.DotNet.Watcher.Tools.Tests/Utilities/TestProjectGraph.cs new file mode 100644 index 0000000000..730f82f0db --- /dev/null +++ b/test/Microsoft.DotNet.Watcher.Tools.Tests/Utilities/TestProjectGraph.cs @@ -0,0 +1,41 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.DotNetWatcher.Tools.Tests +{ + public class TestProjectGraph + { + private readonly TemporaryDirectory _directory; + private Action _onCreate; + private Dictionary _projects = new Dictionary(); + public TestProjectGraph(TemporaryDirectory directory) + { + _directory = directory; + } + + public void OnCreate(Action onCreate) + { + _onCreate = onCreate; + } + + public TemporaryCSharpProject Find(string projectName) + => _projects.ContainsKey(projectName) + ? _projects[projectName] + : null; + + public TemporaryCSharpProject GetOrCreate(string projectName) + { + TemporaryCSharpProject sourceProj; + if (!_projects.TryGetValue(projectName, out sourceProj)) + { + sourceProj = _directory.SubDir(projectName).WithCSharpProject(projectName); + _onCreate?.Invoke(sourceProj); + _projects.Add(projectName, sourceProj); + } + return sourceProj; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Watcher.Tools.Tests/Utilities/XunitLogger.cs b/test/Microsoft.DotNet.Watcher.Tools.Tests/Utilities/XunitLogger.cs new file mode 100644 index 0000000000..640a4a0664 --- /dev/null +++ b/test/Microsoft.DotNet.Watcher.Tools.Tests/Utilities/XunitLogger.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace Microsoft.DotNetWatcher.Tools.Tests +{ + internal class XunitLogger : ILogger + { + private readonly ITestOutputHelper _output; + public XunitLogger(ITestOutputHelper output) + { + _output = output; + } + + public IDisposable BeginScope(TState state) + => NullScope.Instance; + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + _output.WriteLine($"{logLevel}: {formatter(state, exception)}"); + } + + private class NullScope : IDisposable + { + private NullScope() { } + public static NullScope Instance = new NullScope(); + public void Dispose() { } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Watcher.Tools.Tests/copyfiles.cmd b/test/Microsoft.DotNet.Watcher.Tools.Tests/copyfiles.cmd new file mode 100644 index 0000000000..812690859e --- /dev/null +++ b/test/Microsoft.DotNet.Watcher.Tools.Tests/copyfiles.cmd @@ -0,0 +1,9 @@ +@ECHO OFF +:again +if not "%1" == "" ( + echo "Deleting %1\tools" + rmdir /s /q %1\tools +) + +mkdir %1\tools +copy ..\..\src\Microsoft.DotNet.Watcher.Tools\tools\*.targets %1\tools \ No newline at end of file diff --git a/test/Microsoft.DotNet.Watcher.Tools.Tests/copyfiles.sh b/test/Microsoft.DotNet.Watcher.Tools.Tests/copyfiles.sh new file mode 100755 index 0000000000..8c12918ae7 --- /dev/null +++ b/test/Microsoft.DotNet.Watcher.Tools.Tests/copyfiles.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +if [ -z $1 ]; then + echo "Deleting $1/tools" + rm -rf $1/tools +fi + +mkdir -p $1/tools +echo "Copying ./../src/Microsoft.DotNet.Watcher.Tools/tools/*.targets" +cp ../../src/Microsoft.DotNet.Watcher.Tools/tools/*.targets $1/tools + +exit 0 \ No newline at end of file diff --git a/test/Microsoft.DotNet.Watcher.Tools.Tests/project.json b/test/Microsoft.DotNet.Watcher.Tools.Tests/project.json index a084f633f2..f938c008bf 100644 --- a/test/Microsoft.DotNet.Watcher.Tools.Tests/project.json +++ b/test/Microsoft.DotNet.Watcher.Tools.Tests/project.json @@ -1,7 +1,8 @@ { "buildOptions": { "warningsAsErrors": true, - "keyFile": "../../tools/Key.snk" + "keyFile": "../../tools/Key.snk", + "debugType": "portable" }, "dependencies": { "dotnet-test-xunit": "2.2.0-*", @@ -18,5 +19,8 @@ } } }, + "scripts": { + "precompile": "copyfiles %compile:OutputDir%" + }, "testRunner": "xunit" } \ No newline at end of file