Port dotnet-watch to support MSBuild

This commit is contained in:
Nate McMaster 2016-10-28 18:56:03 -07:00 committed by Nate McMaster
parent 8fd91043d3
commit 4698985846
48 changed files with 1463 additions and 508 deletions

View File

@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="dotnet-core" value="https://dotnet.myget.org/F/dotnet-core/api/v3/index.json" />
<add key="nugetbuild" value="https://www.myget.org/F/nugetbuild/api/v3/index.json" />
<add key="AspNetCore" value="https://dotnet.myget.org/F/aspnetcore-ci-dev/api/v3/index.json" />
<add key="NuGet" value="https://api.nuget.org/v3/index.json" />
</packageSources>

View File

@ -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,

View File

@ -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<string> GetChangedFileAsync(CancellationToken cancellationToken)

View File

@ -8,7 +8,7 @@ using System.Linq;
namespace Microsoft.DotNet.Watcher.Internal
{
public class FileWatcher : IFileWatcher
public class FileWatcher
{
private bool _disposed;

View File

@ -182,6 +182,11 @@ namespace Microsoft.DotNet.Watcher.Internal
private void ForeachEntityInDirectory(DirectoryInfo dirInfo, Action<FileSystemInfo> fileAction)
{
if (!dirInfo.Exists)
{
return;
}
var entities = dirInfo.EnumerateFileSystemInfos("*.*");
foreach (var entity in entities)
{

View File

@ -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<string> OnFileChange;
void WatchDirectory(string directory);
}
}

View File

@ -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<string> ResolveFiles(this IncludeContext context)
{
Ensure.NotNull(context, nameof(context));
return IncludeFilesResolver
.GetIncludeFiles(context, "/", diagnostics: null)
.Select(f => f.SourcePath);
}
}
}

View File

@ -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<string> BuildFlags { get; } = new List<string>
{
"/nologo",
"/v:n",
"/t:GenerateWatchList",
"/p:DotNetWatchBuild=true", // extensibility point for users
"/p:DesignTimeBuild=true", // don't do expensive things
};
public async Task<IFileSet> 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);
}
}
}

View File

@ -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
{
/// <summary>
/// Finds a compatible MSBuild project.
/// <param name="searchBase">The base directory to search</param>
/// <param name="project">The filename of the project. Can be null.</param>
/// </summary>
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;
}
}
}

View File

@ -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<string> _lines = new List<string>();
public IEnumerable<string> 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));
}
}
}

View File

@ -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<string>() { 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<string> ProjectDependencies { get; private set; }
public IEnumerable<string> 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));
}
}
}

View File

@ -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<string> _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<string> 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<string>(FindFiles(), StringComparer.OrdinalIgnoreCase);
}
private IEnumerable<string> FindFiles()
{
var projects = new HashSet<string>(); // temporary store to prevent re-parsing a project multiple times
return GetProjectFilesClosure(_projectFile, projects);
}
private IEnumerable<string> GetProjectFilesClosure(string projectFile, ISet<string> 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;
}
}
}
}
}
}

View File

@ -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<IFileSet> 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}");
}
}
}
}
}

View File

@ -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();
}
}
}

View File

@ -24,6 +24,7 @@
</metadata>
<files>
<file src="bin/$configuration$/netcoreapp1.0/dotnet-watch.dll" target="lib\netcoreapp1.0\" />
<file src="bin/$configuration$/netcoreapp1.0/dotnet-watch.runtimeconfig.json" target="lib/netcoreapp1.0\" />
<file src="bin/$configuration$/netcoreapp1.0/dotnet-watch.runtimeconfig.json" target="lib\netcoreapp1.0\" />
<file src="tools/*.targets" target="tools\" />
</files>
</package>

View File

@ -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;
}

View File

@ -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);
/// <summary>
/// The project file '{path}' does not exist.
/// </summary>
internal static string Error_ProjectPath_NotFound
{
get { return GetString("Error_ProjectPath_NotFound"); }
}
/// <summary>
/// The project file '{path}' does not exist.
/// </summary>
internal static string FormatError_ProjectPath_NotFound(object path)
{
return string.Format(CultureInfo.CurrentCulture, GetString("Error_ProjectPath_NotFound", "path"), path);
}
/// <summary>
/// Multiple MSBuild project files found in '{projectPath}'. Specify which to use with the --project option.
/// </summary>
internal static string Error_MultipleProjectsFound
{
get { return GetString("Error_MultipleProjectsFound"); }
}
/// <summary>
/// Multiple MSBuild project files found in '{projectPath}'. Specify which to use with the --project option.
/// </summary>
internal static string FormatError_MultipleProjectsFound(object projectPath)
{
return string.Format(CultureInfo.CurrentCulture, GetString("Error_MultipleProjectsFound", "projectPath"), projectPath);
}
/// <summary>
/// Could not find a MSBuild project file in '{projectPath}'. Specify which project to use with the --project option.
/// </summary>
internal static string Error_NoProjectsFound
{
get { return GetString("Error_NoProjectsFound"); }
}
/// <summary>
/// Could not find a MSBuild project file in '{projectPath}'. Specify which project to use with the --project option.
/// </summary>
internal static string FormatError_NoProjectsFound(object projectPath)
{
return string.Format(CultureInfo.CurrentCulture, GetString("Error_NoProjectsFound", "projectPath"), projectPath);
}
/// <summary>
/// Cannot specify both '--quiet' and '--verbose' options.
/// </summary>

View File

@ -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
<ItemGroup>
<ProjectReference Include="..\ClassLibrary1\ClassLibrary1.csproj" Watch="false" />
</ItemGroup>
```
**Watch items**
dotnet-watch will watch all items in the "<kbd>Watch</kbd>" item group.
By default, this group inclues all items in "<kbd>Compile</kbd>" and "<kbd>EmbeddedResource</kbd>".
More items can be added to watch in a project file by adding items to 'Watch'.
Example:
```xml
<ItemGroup>
<!-- extends watching group to include *.js files -->
<Watch Include="**\*.js" Exclude="node_modules\**\*.js;$(DefaultExcludes)" />
</ItemGroup>
```
dotnet-watch will ignore Compile and EmbeddedResource items with the `Watch="false"` attribute.
Example:
```xml
<ItemGroup>
<!-- exclude Generated.cs from dotnet-watch -->
<Compile Include="Generated.cs" Watch="false" />
<EmbeddedResource Include="Generated.cs" Watch="false" />
</ItemGroup>
```
**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
<ItemGroup Condition="'$(DotNetWatchBuild)'=='true'">
<!-- design-time only items -->
</ItemGroup>
```

View File

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@ -117,6 +117,15 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Error_ProjectPath_NotFound" xml:space="preserve">
<value>The project file '{path}' does not exist.</value>
</data>
<data name="Error_MultipleProjectsFound" xml:space="preserve">
<value>Multiple MSBuild project files found in '{projectPath}'. Specify which to use with the --project option.</value>
</data>
<data name="Error_NoProjectsFound" xml:space="preserve">
<value>Could not find a MSBuild project file in '{projectPath}'. Specify which project to use with the --project option.</value>
</data>
<data name="Error_QuietAndVerboseSpecified" xml:space="preserve">
<value>Cannot specify both '--quiet' and '--verbose' options.</value>
</data>

View File

@ -0,0 +1,18 @@
<!-- This file is autogenerated by dotnet-watch. -->
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<_DotNetWatchTargetsFile Condition="'$(_DotNetWatchTargetsFile)' == ''">$(MSBuildThisFileFullPath)</_DotNetWatchTargetsFile>
</PropertyGroup>
<Import Project="$(_DotNetWatchTargetsLocation)\DotNetWatchCommon.targets"
Condition="Exists('$(_DotNetWatchTargetsLocation)\DotNetWatchCommon.targets')" />
<ImportGroup Condition="'$(TargetFramework)'==''">
<Import Project="$(_DotNetWatchTargetsLocation)\DotNetWatchOuter.targets"
Condition="Exists('$(_DotNetWatchTargetsLocation)\DotNetWatchOuter.targets')" />
</ImportGroup>
<ImportGroup Condition="'$(TargetFramework)'!=''">
<Import Project="$(_DotNetWatchTargetsLocation)\DotNetWatchInner.targets"
Condition="Exists('$(_DotNetWatchTargetsLocation)\DotNetWatchInner.targets')" />
</ImportGroup>
</Project>

View File

@ -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-*",

View File

@ -0,0 +1,28 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="_WriteGeneratedWatchList" >
<WriteLinesToFile Overwrite="true"
File="$(_DotNetWatchListFile)"
Lines="@(Watch -> '%(FullPath)')" />
</Target>
<!--
=========================================================================
_CollectWatchItems
Invokes _CoreCollectWatchItems on each distinct project in _DotNetWatchProjects.
Returns: @(Watch)
=========================================================================
-->
<Target Name="_CollectWatchItems">
<RemoveDuplicates Inputs="@(_DotNetWatchProjects)">
<Output TaskParameter="Filtered" ItemName="_DotNetWatchProjectsFiltered" />
</RemoveDuplicates>
<MSBuild
Targets="_CoreCollectWatchItems"
Projects="%(_DotNetWatchProjectsFiltered.FullPath)">
<Output TaskParameter="TargetOutputs" ItemName="Watch"/>
</MSBuild>
</Target>
</Project>

View File

@ -0,0 +1,70 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!--
=========================================================================
GenerateWatchList
Main target called by dotnet-watch. This is the single-tfm version.
For multi-tfm version, see the Outer.targets file for description of the
design of this target.
=========================================================================
-->
<Target Name="GenerateWatchList"
DependsOnTargets="_CollectWatchProjects;_CollectWatchItems;_WriteGeneratedWatchList" />
<!--
=========================================================================
_CoreCollectWatchItems
Creates item group with default set of files to watch.
Returns: @(Watch)
=========================================================================
-->
<Target Name="_CoreCollectWatchItems" Returns="@(Watch)">
<!-- message used to debug -->
<Message Importance="High" Text="Collecting watch items from '$(MSBuildProjectName)'" Condition="'$(_DotNetWatchTraceOutput)'=='true'" />
<ItemGroup>
<Watch Include="@(Compile->'%(FullPath)')" Condition="'%(Compile.Watch)' != 'false'" />
<Watch Include="@(EmbeddedResource->'%(FullPath)')" Condition="'%(EmbeddedResource.Watch)' != 'false'"/>
<Watch Include="$(MSBuildProjectFullPath)" />
</ItemGroup>
</Target>
<!--
=========================================================================
_CollectWatchProjects
Adds all ProjectReference items to _DotNetWatchProjects.
Copies the project file extension file to all project references.
Invokes '_CollectWatchProjects' on all referenced projects.
Returns: @(_DotNetWatchProjects)
=========================================================================
-->
<Target Name="_CollectWatchProjects"
Returns="@(_DotNetWatchProjects)">
<!-- message used to debug -->
<Message Importance="High" Text="Collecting referenced projects from '$(MSBuildProjectName)'" Condition="'$(_DotNetWatchTraceOutput)'=='true'" />
<ItemGroup>
<_DotNetWatchProjects Include="@(ProjectReference->'%(FullPath)')" Condition="'%(ProjectReference.Watch)' != 'false'" />
<_DotNetWatchImportsTargets Include="@(_DotNetWatchProjects->'%(RelativeDir)obj\%(FileName)%(Extension).dotnetwatch.targets')">
<TargetsFile>$(_DotNetWatchTargetsFile)</TargetsFile>
</_DotNetWatchImportsTargets>
</ItemGroup>
<Copy SourceFiles="@(_DotNetWatchImportsTargets->'%(TargetsFile)')"
DestinationFiles="@(_DotNetWatchImportsTargets)"
SkipUnchangedFiles="true" />
<MSBuild
Targets="_CollectWatchProjects"
Projects="%(_DotNetWatchProjects.FullPath)">
<Output TaskParameter="TargetOutputs" ItemName="_DotNetWatchProjects"/>
</MSBuild>
<ItemGroup>
<_DotNetWatchProjects Include="$(MSBuildProjectFullPath)"/>
</ItemGroup>
</Target>
</Project>

View File

@ -0,0 +1,69 @@
<Project
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!--
=========================================================================
GenerateWatchList
Main target called by dotnet-watch. This is the cross-targetting version.
For single-tfm, the 'GenerateWatchList' in Inner.targets is the main target.
Phase 1: _CollectWatchProjects:
traverses the project-to-project graph to resolve all projects referenced.
Phase 2: _CollectWatchItems:
for each unique project file from phase 1, extracts all 'Watch' items and
collects into a single item group. All values should be a fullpath.
Phase 3: _WriteGeneratedWatchList:
write all files to a file that can be read by dotnet-watch
=========================================================================
-->
<Target Name="GenerateWatchList"
DependsOnTargets="_CollectWatchProjects;_CollectWatchItems;_WriteGeneratedWatchList"
/>
<!--
=========================================================================
_CoreCollectWatchItems
Shim for cross-targetting builds to run _CoreCollectWatchItems for each target
framework.
Returns: @(Watch)
=========================================================================
-->
<Target Name="_CoreCollectWatchItems"
Returns="@(Watch)">
<ItemGroup>
<_TargetFramework Include="$(TargetFrameworks)" />
</ItemGroup>
<MSBuild Projects="$(MSBuildProjectFile)"
Condition="'$(TargetFrameworks)' != '' "
Targets="_CoreCollectWatchItems"
Properties="TargetFramework=%(_TargetFramework.Identity)">
<Output ItemName="Watch" TaskParameter="TargetOutputs" />
</MSBuild>
</Target>
<!--
=========================================================================
_CollectWatchProjects
Shim for cross-targetting builds to run _CollectWatchProjects for each target
framework.
Returns: @(_DotNetWatchProjects)
=========================================================================
-->
<Target Name="_CollectWatchProjects" Returns="@(_DotNetWatchProjects)">
<ItemGroup>
<_TargetFramework Include="$(TargetFrameworks)" />
</ItemGroup>
<MSBuild Projects="$(MSBuildProjectFile)"
Condition="'$(TargetFrameworks)' != '' "
Targets="_CollectWatchProjects"
Properties="TargetFramework=%(_TargetFramework.Identity)">
<Output ItemName="_DotNetWatchProjects" TaskParameter="TargetOutputs" />
</MSBuild>
</Target>
</Project>

View File

@ -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,

View File

@ -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,

View File

@ -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<string> args)
{
RunDotNetWatch(args, Path.Combine(_scenario.WorkFolder, TestAppName));
RunDotNetWatch(args, Path.Combine(Scenario.WorkFolder, TestAppName));
}
}
}

View File

@ -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();
}
}
}

View File

@ -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<string> arguments, string workDir, IDictionary<string, string> 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<string>();
@ -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)

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />
<PropertyGroup>
<TargetFramework>netcoreapp1.0</TargetFramework>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<Compile Include="**\*.cs" />
<EmbeddedResource Include="**\*.resx" />
<ProjectReference Include="..\Dependency\Dependency.csproj" />
<PackageReference Include="Microsoft.NET.Sdk" Version="1.0.0-*" PrivateAssets="All" />
<PackageReference Include="Microsoft.NETCore.App" Version="1.1.0-*" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@ -1,20 +0,0 @@
{
"buildOptions": {
"emitEntryPoint": true
},
"dependencies": {
"Dependency": {
"target": "project"
}
},
"frameworks": {
"netcoreapp1.0": {
"dependencies": {
"Microsoft.NETCore.App": {
"type": "platform",
"version": "1.1.0-*"
}
}
}
}
}

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />
<PropertyGroup>
<TargetFramework>netstandard1.5</TargetFramework>
<OutputType>Library</OutputType>
</PropertyGroup>
<ItemGroup>
<Compile Include="**\*.cs" />
<EmbeddedResource Include="**\*.resx" />
<PackageReference Include="NETStandard.Library" Version="1.6.1-*" />
<PackageReference Include="Microsoft.NET.Sdk" Version="1.0.0-*" PrivateAssets="All" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@ -1,8 +0,0 @@
{
"dependencies": {
"NETStandard.Library": "1.6.1-*"
},
"frameworks": {
"netstandard1.5": {}
}
}

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />
<PropertyGroup>
<TargetFramework>netcoreapp1.0</TargetFramework>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<Compile Include="Program.cs;include\*.cs" Exclude="exclude\*" />
<EmbeddedResource Include="**\*.resx" />
<PackageReference Include="Microsoft.NET.Sdk" Version="1.0.0-*" PrivateAssets="All" />
<PackageReference Include="Microsoft.NETCore.App" Version="1.1.0-*" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@ -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-*"
}
}
}
}
}

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />
<PropertyGroup>
<TargetFramework>netcoreapp1.0</TargetFramework>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<Compile Include="**\*.cs" />
<EmbeddedResource Include="**\*.resx" />
<PackageReference Include="Microsoft.NET.Sdk" Version="1.0.0-*" PrivateAssets="All" />
<PackageReference Include="Microsoft.NETCore.App" Version="1.1.0-*" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@ -1,15 +0,0 @@
{
"buildOptions": {
"emitEntryPoint": true
},
"frameworks": {
"netcoreapp1.0": {
"dependencies": {
"Microsoft.NETCore.App": {
"type": "platform",
"version": "1.1.0-*"
}
}
}
}
}

View File

@ -3,4 +3,9 @@
if not "%1" == "" (
echo "Deleting %1\TestProjects"
rmdir /s /q %1\TestProjects
)
echo "Deleting %1\tools"
rmdir /s /q %1\tools
)
mkdir %1\tools
copy ..\..\src\Microsoft.DotNet.Watcher.Tools\tools\*.targets %1\tools

View File

@ -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

View File

@ -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<string> expectedFiles, IEnumerable<string> actualFiles)
{
var expected = expectedFiles.Select(p => Path.Combine(root, p));
EqualFileList(expected, actualFiles);
}
public static void EqualFileList(IEnumerable<string> expectedFiles, IEnumerable<string> actualFiles)
{
Func<string, string> normalize = p => p.Replace('\\', '/');
var expected = new HashSet<string>(expectedFiles.Select(normalize));
Assert.True(expected.SetEquals(actualFiles.Select(normalize)), "File sets should be equal");
}
}
}

View File

@ -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<IFileSet> GetFileSet(TemporaryCSharpProject target)
=> GetFileSet(new MsBuildFileSetFactory(_logger, target.Path));
private async Task<IFileSet> 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();
}
}
}

View File

@ -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 =
@"<Project ToolsVersion=""15.0"" xmlns=""http://schemas.microsoft.com/developer/msbuild/2003"">
<Import Project=""$(MSBuildExtensionsPath)/$(MSBuildToolsVersion)/Microsoft.Common.props"" />
<PropertyGroup>
{0}
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
{1}
</ItemGroup>
<Import Project=""$(MSBuildToolsPath)/Microsoft.CSharp.targets"" />
</Project>";
private const string DefaultGlobs =
@"<Compile Include=""**/*.cs"" Exclude=""obj/**/*;bin/**/*"" />
<EmbeddedResource Include=""**/*.resx"" Exclude=""obj/**/*;bin/**/*"" />";
private readonly string _filename;
private readonly TemporaryDirectory _directory;
private string[] _tfms;
private List<string> _items = new List<string>();
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
? $"<TargetFramework>{_tfms[0]}</TargetFramework>"
: $"<TargetFrameworks>{string.Join(";", _tfms)}</TargetFrameworks>";
_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; }
}
}
}

View File

@ -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<TemporaryCSharpProject> _projects = new List<TemporaryCSharpProject>();
private List<TemporaryDirectory> _subdirs = new List<TemporaryDirectory>();
private List<string> _files = new List<string>();
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}'");
}
}
}
}

View File

@ -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<TemporaryCSharpProject> _onCreate;
private Dictionary<string, TemporaryCSharpProject> _projects = new Dictionary<string, TemporaryCSharpProject>();
public TestProjectGraph(TemporaryDirectory directory)
{
_directory = directory;
}
public void OnCreate(Action<TemporaryCSharpProject> 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;
}
}
}

View File

@ -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>(TState state)
=> NullScope.Instance;
public bool IsEnabled(LogLevel logLevel) => true;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
_output.WriteLine($"{logLevel}: {formatter(state, exception)}");
}
private class NullScope : IDisposable
{
private NullScope() { }
public static NullScope Instance = new NullScope();
public void Dispose() { }
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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"
}