Port dotnet-watch to support MSBuild
This commit is contained in:
parent
8fd91043d3
commit
4698985846
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ using System.Linq;
|
|||
|
||||
namespace Microsoft.DotNet.Watcher.Internal
|
||||
{
|
||||
public class FileWatcher : IFileWatcher
|
||||
public class FileWatcher
|
||||
{
|
||||
private bool _disposed;
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
```
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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-*",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
{
|
||||
"buildOptions": {
|
||||
"emitEntryPoint": true
|
||||
},
|
||||
"dependencies": {
|
||||
"Dependency": {
|
||||
"target": "project"
|
||||
}
|
||||
},
|
||||
"frameworks": {
|
||||
"netcoreapp1.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.App": {
|
||||
"type": "platform",
|
||||
"version": "1.1.0-*"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"NETStandard.Library": "1.6.1-*"
|
||||
},
|
||||
"frameworks": {
|
||||
"netstandard1.5": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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-*"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
{
|
||||
"buildOptions": {
|
||||
"emitEntryPoint": true
|
||||
},
|
||||
"frameworks": {
|
||||
"netcoreapp1.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.App": {
|
||||
"type": "platform",
|
||||
"version": "1.1.0-*"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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() { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
}
|
||||
Loading…
Reference in New Issue