Avoid running restores for dotnet-watch run (#23421)
* Tweaks to make dotnet-watch run faster * Previously dotnet-watch calculated the watch file list on every run by invoking MSBuild. This changes the tool to only calculate it if an MSBuild file (.targets, .props, .csproj etc) file changed * For dotnet watch run and dotnet watch test command, use --no-restore if changed file is not an MSBuild file. * Add opt-out switch * Update src/Tools/dotnet-watch/README.md * Fixup typo * Update src/Tools/dotnet-watch/README.md
This commit is contained in:
parent
769fc6d289
commit
763a18ee56
|
|
@ -29,6 +29,7 @@ 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. |
|
||||
| DOTNET_WATCH_SUPPRESS_MSBUILD_INCREMENTALISM | By default, `dotnet watch` optimizes the build by avoiding certain operations such as running restore or re-evaluating the set of watched files on every file change. If set to "1" or "true", these optimizations are disabled. |
|
||||
|
||||
### MSBuild
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
// 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 Microsoft.Extensions.Tools.Internal;
|
||||
|
||||
namespace Microsoft.DotNet.Watcher.Tools
|
||||
{
|
||||
public class DotNetWatchContext
|
||||
{
|
||||
public IReporter Reporter { get; set; } = NullReporter.Singleton;
|
||||
|
||||
public ProcessSpec ProcessSpec { get; set; }
|
||||
|
||||
public IFileSet FileSet { get; set; }
|
||||
|
||||
public int Iteration { get; set; }
|
||||
|
||||
public string ChangedFile { get; set; }
|
||||
|
||||
public bool RequiresMSBuildRevaluation { get; set; }
|
||||
|
||||
public bool SuppressMSBuildIncrementalism { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -3,9 +3,12 @@
|
|||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.DotNet.Watcher.Internal;
|
||||
using Microsoft.DotNet.Watcher.Tools;
|
||||
using Microsoft.Extensions.CommandLineUtils;
|
||||
using Microsoft.Extensions.Tools.Internal;
|
||||
|
||||
|
|
@ -15,33 +18,63 @@ namespace Microsoft.DotNet.Watcher
|
|||
{
|
||||
private readonly IReporter _reporter;
|
||||
private readonly ProcessRunner _processRunner;
|
||||
private readonly IWatchFilter[] _filters;
|
||||
|
||||
public DotNetWatcher(IReporter reporter)
|
||||
public DotNetWatcher(IReporter reporter, IFileSetFactory fileSetFactory)
|
||||
{
|
||||
Ensure.NotNull(reporter, nameof(reporter));
|
||||
|
||||
_reporter = reporter;
|
||||
_processRunner = new ProcessRunner(reporter);
|
||||
|
||||
_filters = new IWatchFilter[]
|
||||
{
|
||||
new MSBuildEvaluationFilter(fileSetFactory),
|
||||
new NoRestoreFilter(),
|
||||
};
|
||||
}
|
||||
|
||||
public async Task WatchAsync(ProcessSpec processSpec, IFileSetFactory fileSetFactory,
|
||||
CancellationToken cancellationToken)
|
||||
public async Task WatchAsync(ProcessSpec processSpec, CancellationToken cancellationToken)
|
||||
{
|
||||
Ensure.NotNull(processSpec, nameof(processSpec));
|
||||
|
||||
var cancelledTaskSource = new TaskCompletionSource<object>();
|
||||
cancellationToken.Register(state => ((TaskCompletionSource<object>) state).TrySetResult(null),
|
||||
var cancelledTaskSource = new TaskCompletionSource();
|
||||
cancellationToken.Register(state => ((TaskCompletionSource)state).TrySetResult(),
|
||||
cancelledTaskSource);
|
||||
|
||||
var iteration = 1;
|
||||
var initialArguments = processSpec.Arguments.ToArray();
|
||||
var suppressMSBuildIncrementalism = Environment.GetEnvironmentVariable("DOTNET_WATCH_SUPPRESS_MSBUILD_INCREMENTALISM");
|
||||
var context = new DotNetWatchContext
|
||||
{
|
||||
Iteration = -1,
|
||||
ProcessSpec = processSpec,
|
||||
Reporter = _reporter,
|
||||
SuppressMSBuildIncrementalism = suppressMSBuildIncrementalism == "1" || suppressMSBuildIncrementalism == "true",
|
||||
};
|
||||
|
||||
if (context.SuppressMSBuildIncrementalism)
|
||||
{
|
||||
_reporter.Verbose("MSBuild incremental optimizations suppressed.");
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
processSpec.EnvironmentVariables["DOTNET_WATCH_ITERATION"] = iteration.ToString(CultureInfo.InvariantCulture);
|
||||
iteration++;
|
||||
context.Iteration++;
|
||||
|
||||
var fileSet = await fileSetFactory.CreateAsync(cancellationToken);
|
||||
// Reset arguments
|
||||
processSpec.Arguments = initialArguments;
|
||||
|
||||
for (var i = 0; i < _filters.Length; i++)
|
||||
{
|
||||
await _filters[i].ProcessAsync(context, cancellationToken);
|
||||
}
|
||||
|
||||
// Reset for next run
|
||||
context.RequiresMSBuildRevaluation = false;
|
||||
|
||||
processSpec.EnvironmentVariables["DOTNET_WATCH_ITERATION"] = (context.Iteration + 1).ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
var fileSet = context.FileSet;
|
||||
if (fileSet == null)
|
||||
{
|
||||
_reporter.Error("Failed to find a list of files to watch");
|
||||
|
|
@ -91,10 +124,13 @@ namespace Microsoft.DotNet.Watcher
|
|||
return;
|
||||
}
|
||||
|
||||
context.ChangedFile = fileSetTask.Result;
|
||||
if (finishedTask == processTask)
|
||||
{
|
||||
// Process exited. Redo evaludation
|
||||
context.RequiresMSBuildRevaluation = true;
|
||||
// Now wait for a file to change before restarting process
|
||||
await fileSetWatcher.GetChangedFileAsync(cancellationToken, () => _reporter.Warn("Waiting for a file to change before restarting dotnet..."));
|
||||
context.ChangedFile = await fileSetWatcher.GetChangedFileAsync(cancellationToken, () => _reporter.Warn("Waiting for a file to change before restarting dotnet..."));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(fileSetTask.Result))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.DotNet.Watcher.Tools
|
||||
{
|
||||
public interface IWatchFilter
|
||||
{
|
||||
ValueTask ProcessAsync(DotNetWatchContext context, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// 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;
|
||||
|
|
@ -20,6 +20,8 @@ namespace Microsoft.DotNet.Watcher.Internal
|
|||
|
||||
public int Count => _files.Count;
|
||||
|
||||
public static IFileSet Empty = new FileSet(Array.Empty<string>());
|
||||
|
||||
public IEnumerator<string> GetEnumerator() => _files.GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => _files.GetEnumerator();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,124 @@
|
|||
// 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 System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.DotNet.Watcher.Tools
|
||||
{
|
||||
public class MSBuildEvaluationFilter : IWatchFilter
|
||||
{
|
||||
// File types that require an MSBuild re-evaluation
|
||||
private static readonly string[] _msBuildFileExtensions = new[]
|
||||
{
|
||||
".csproj", ".props", ".targets", ".fsproj", ".vbproj", ".vcxproj",
|
||||
};
|
||||
private static readonly int[] _msBuildFileExtensionHashes = _msBuildFileExtensions
|
||||
.Select(e => e.GetHashCode(StringComparison.OrdinalIgnoreCase))
|
||||
.ToArray();
|
||||
|
||||
private readonly IFileSetFactory _factory;
|
||||
|
||||
private List<(string fileName, DateTime lastWriteTimeUtc)> _msbuildFileTimestamps;
|
||||
|
||||
public MSBuildEvaluationFilter(IFileSetFactory factory)
|
||||
{
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
public async ValueTask ProcessAsync(DotNetWatchContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
if (context.SuppressMSBuildIncrementalism)
|
||||
{
|
||||
context.RequiresMSBuildRevaluation = true;
|
||||
context.FileSet = await _factory.CreateAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.Iteration == 0 || RequiresMSBuildRevaluation(context))
|
||||
{
|
||||
context.RequiresMSBuildRevaluation = true;
|
||||
}
|
||||
|
||||
if (context.RequiresMSBuildRevaluation)
|
||||
{
|
||||
context.Reporter.Verbose("Evaluating dotnet-watch file set.");
|
||||
|
||||
context.FileSet = await _factory.CreateAsync(cancellationToken);
|
||||
_msbuildFileTimestamps = GetMSBuildFileTimeStamps(context);
|
||||
}
|
||||
}
|
||||
|
||||
private bool RequiresMSBuildRevaluation(DotNetWatchContext context)
|
||||
{
|
||||
var changedFile = context.ChangedFile;
|
||||
if (!string.IsNullOrEmpty(changedFile) && IsMsBuildFileExtension(changedFile))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// The filewatcher may miss changes to files. For msbuild files, we can verify that they haven't been modified
|
||||
// since the previous iteration.
|
||||
// We do not have a way to identify renames or new additions that the file watcher did not pick up,
|
||||
// without performing an evaluation. We will start off by keeping it simple and comparing the timestamps
|
||||
// of known MSBuild files from previous run. This should cover the vast majority of cases.
|
||||
|
||||
foreach (var (file, lastWriteTimeUtc) in _msbuildFileTimestamps)
|
||||
{
|
||||
if (GetLastWriteTimeUtcSafely(file) != lastWriteTimeUtc)
|
||||
{
|
||||
context.Reporter.Verbose($"Re-evaluation needed due to changes in {file}.");
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private List<(string fileName, DateTime lastModifiedUtc)> GetMSBuildFileTimeStamps(DotNetWatchContext context)
|
||||
{
|
||||
var msbuildFiles = new List<(string fileName, DateTime lastModifiedUtc)>();
|
||||
foreach (var file in context.FileSet)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(file) && IsMsBuildFileExtension(file))
|
||||
{
|
||||
msbuildFiles.Add((file, GetLastWriteTimeUtcSafely(file)));
|
||||
}
|
||||
}
|
||||
|
||||
return msbuildFiles;
|
||||
}
|
||||
|
||||
protected virtual DateTime GetLastWriteTimeUtcSafely(string file)
|
||||
{
|
||||
try
|
||||
{
|
||||
return File.GetLastWriteTimeUtc(file);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
static bool IsMsBuildFileExtension(string fileName)
|
||||
{
|
||||
var extension = Path.GetExtension(fileName.AsSpan());
|
||||
var hashCode = string.GetHashCode(extension, StringComparison.OrdinalIgnoreCase);
|
||||
for (var i = 0; i < _msBuildFileExtensionHashes.Length; i++)
|
||||
{
|
||||
if (_msBuildFileExtensionHashes[i] == hashCode && extension.Equals(_msBuildFileExtensions[i], StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
// 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;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Tools.Internal;
|
||||
|
||||
namespace Microsoft.DotNet.Watcher.Tools
|
||||
{
|
||||
public sealed class NoRestoreFilter : IWatchFilter
|
||||
{
|
||||
private bool _canUseNoRestore;
|
||||
private string[] _noRestoreArguments;
|
||||
|
||||
public ValueTask ProcessAsync(DotNetWatchContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
if (context.SuppressMSBuildIncrementalism)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
if (context.Iteration == 0)
|
||||
{
|
||||
var arguments = context.ProcessSpec.Arguments;
|
||||
_canUseNoRestore = CanUseNoRestore(arguments, context.Reporter);
|
||||
if (_canUseNoRestore)
|
||||
{
|
||||
// Create run --no-restore <other args>
|
||||
_noRestoreArguments = arguments.Take(1).Append("--no-restore").Concat(arguments.Skip(1)).ToArray();
|
||||
context.Reporter.Verbose($"No restore arguments: {string.Join(" ", _noRestoreArguments)}");
|
||||
}
|
||||
}
|
||||
else if (_canUseNoRestore)
|
||||
{
|
||||
if (context.RequiresMSBuildRevaluation)
|
||||
{
|
||||
context.Reporter.Verbose("Cannot use --no-restore since msbuild project files have changed.");
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Reporter.Verbose("Modifying command to use --no-restore");
|
||||
context.ProcessSpec.Arguments = _noRestoreArguments;
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private static bool CanUseNoRestore(IEnumerable<string> arguments, IReporter reporter)
|
||||
{
|
||||
// For some well-known dotnet commands, we can pass in the --no-restore switch to avoid unnecessary restores between iterations.
|
||||
// For now we'll support the "run" and "test" commands.
|
||||
if (arguments.Any(a => string.Equals(a, "--no-restore", StringComparison.Ordinal)))
|
||||
{
|
||||
// Did the user already configure a --no-restore?
|
||||
return false;
|
||||
}
|
||||
|
||||
var dotnetCommand = arguments.FirstOrDefault();
|
||||
if (string.Equals(dotnetCommand, "run", StringComparison.Ordinal) || string.Equals(dotnetCommand, "test", StringComparison.Ordinal))
|
||||
{
|
||||
reporter.Verbose("Watch command can be configured to use --no-restore.");
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
reporter.Verbose($"Watch command will not use --no-restore. Unsupport dotnet-command '{dotnetCommand}'.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -162,8 +162,8 @@ namespace Microsoft.DotNet.Watcher
|
|||
_reporter.Output("Polling file watcher is enabled");
|
||||
}
|
||||
|
||||
await new DotNetWatcher(reporter)
|
||||
.WatchAsync(processInfo, fileSetFactory, cancellationToken);
|
||||
await new DotNetWatcher(reporter, fileSetFactory)
|
||||
.WatchAsync(processInfo, cancellationToken);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,9 @@
|
|||
// 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.Globalization;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Testing;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
|
|
@ -54,6 +53,51 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunsWithNoRestoreOnOrdinaryFileChanges()
|
||||
{
|
||||
_app.DotnetWatchArgs.Add("--verbose");
|
||||
|
||||
await _app.StartWatcherAsync(arguments: new[] { "wait" });
|
||||
var source = Path.Combine(_app.SourceDirectory, "Program.cs");
|
||||
const string messagePrefix = "watch : Running dotnet with the following arguments: run";
|
||||
|
||||
// Verify that the first run does not use --no-restore
|
||||
Assert.Contains(_app.Process.Output, p => string.Equals(messagePrefix + " -- wait", p.Trim()));
|
||||
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
File.SetLastWriteTime(source, DateTime.Now);
|
||||
var message = await _app.Process.GetOutputLineStartsWithAsync(messagePrefix, TimeSpan.FromMinutes(2));
|
||||
|
||||
Assert.Equal(messagePrefix + " --no-restore -- wait", message.Trim());
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunsWithRestoreIfCsprojChanges()
|
||||
{
|
||||
_app.DotnetWatchArgs.Add("--verbose");
|
||||
|
||||
await _app.StartWatcherAsync(arguments: new[] { "wait" });
|
||||
var source = Path.Combine(_app.SourceDirectory, "KitchenSink.csproj");
|
||||
const string messagePrefix = "watch : Running dotnet with the following arguments: run";
|
||||
|
||||
// Verify that the first run does not use --no-restore
|
||||
Assert.Contains(_app.Process.Output, p => string.Equals(messagePrefix + " -- wait", p.Trim()));
|
||||
|
||||
File.SetLastWriteTime(source, DateTime.Now);
|
||||
var message = await _app.Process.GetOutputLineStartsWithAsync(messagePrefix, TimeSpan.FromMinutes(2));
|
||||
|
||||
// csproj changed. Do not expect a --no-restore
|
||||
Assert.Equal(messagePrefix + " -- wait", message.Trim());
|
||||
|
||||
// regular file changed after csproj changes. Should use --no-restore
|
||||
File.SetLastWriteTime(Path.Combine(_app.SourceDirectory, "Program.cs"), DateTime.Now);
|
||||
message = await _app.Process.GetOutputLineStartsWithAsync(messagePrefix, TimeSpan.FromMinutes(2));
|
||||
Assert.Equal(messagePrefix + " --no-restore -- wait", message.Trim());
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_app.Dispose();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,142 @@
|
|||
// 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 System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.DotNet.Watcher.Internal;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.DotNet.Watcher.Tools
|
||||
{
|
||||
public class MSBuildEvaluationFilterTest
|
||||
{
|
||||
private readonly IFileSetFactory _fileSetFactory = Mock.Of<IFileSetFactory>(
|
||||
f => f.CreateAsync(It.IsAny<CancellationToken>()) == Task.FromResult<IFileSet>(new FileSet(Enumerable.Empty<string>())));
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_EvaluatesFileSetIfProjFileChanges()
|
||||
{
|
||||
// Arrange
|
||||
var filter = new MSBuildEvaluationFilter(_fileSetFactory);
|
||||
var context = new DotNetWatchContext
|
||||
{
|
||||
Iteration = 0,
|
||||
};
|
||||
|
||||
await filter.ProcessAsync(context, default);
|
||||
|
||||
context.Iteration++;
|
||||
context.ChangedFile = "Test.csproj";
|
||||
context.RequiresMSBuildRevaluation = false;
|
||||
|
||||
// Act
|
||||
await filter.ProcessAsync(context, default);
|
||||
|
||||
// Assert
|
||||
Assert.True(context.RequiresMSBuildRevaluation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_DoesNotEvaluateFileSetIfNonProjFileChanges()
|
||||
{
|
||||
// Arrange
|
||||
var filter = new MSBuildEvaluationFilter(_fileSetFactory);
|
||||
var context = new DotNetWatchContext
|
||||
{
|
||||
Iteration = 0,
|
||||
};
|
||||
|
||||
await filter.ProcessAsync(context, default);
|
||||
|
||||
context.Iteration++;
|
||||
context.ChangedFile = "Controller.cs";
|
||||
context.RequiresMSBuildRevaluation = false;
|
||||
|
||||
// Act
|
||||
await filter.ProcessAsync(context, default);
|
||||
|
||||
// Assert
|
||||
Assert.False(context.RequiresMSBuildRevaluation);
|
||||
Mock.Get(_fileSetFactory).Verify(v => v.CreateAsync(It.IsAny<CancellationToken>()), Times.Once());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_EvaluateFileSetOnEveryChangeIfOptimizationIsSuppressed()
|
||||
{
|
||||
// Arrange
|
||||
var filter = new MSBuildEvaluationFilter(_fileSetFactory);
|
||||
var context = new DotNetWatchContext
|
||||
{
|
||||
Iteration = 0,
|
||||
SuppressMSBuildIncrementalism = true,
|
||||
};
|
||||
|
||||
await filter.ProcessAsync(context, default);
|
||||
|
||||
context.Iteration++;
|
||||
context.ChangedFile = "Controller.cs";
|
||||
context.RequiresMSBuildRevaluation = false;
|
||||
|
||||
// Act
|
||||
await filter.ProcessAsync(context, default);
|
||||
|
||||
// Assert
|
||||
Assert.True(context.RequiresMSBuildRevaluation);
|
||||
Mock.Get(_fileSetFactory).Verify(v => v.CreateAsync(It.IsAny<CancellationToken>()), Times.Exactly(2));
|
||||
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_SetsEvaluationRequired_IfMSBuildFileChanges_ButIsNotChangedFile()
|
||||
{
|
||||
// There's a chance that the watcher does not correctly report edits to msbuild files on
|
||||
// concurrent edits. MSBuildEvaluationFilter uses timestamps to additionally track changes to these files.
|
||||
|
||||
// Arrange
|
||||
var fileSet = new FileSet(new[] { "Controlller.cs", "Proj.csproj" });
|
||||
var fileSetFactory = Mock.Of<IFileSetFactory>(f => f.CreateAsync(It.IsAny<CancellationToken>()) == Task.FromResult<IFileSet>(fileSet));
|
||||
|
||||
var filter = new TestableMSBuildEvaluationFilter(fileSetFactory)
|
||||
{
|
||||
Timestamps =
|
||||
{
|
||||
["Controller.cs"] = new DateTime(1000),
|
||||
["Proj.csproj"] = new DateTime(1000),
|
||||
}
|
||||
};
|
||||
var context = new DotNetWatchContext
|
||||
{
|
||||
Iteration = 0,
|
||||
};
|
||||
|
||||
await filter.ProcessAsync(context, default);
|
||||
context.RequiresMSBuildRevaluation = false;
|
||||
context.ChangedFile = "Controller.cs";
|
||||
context.Iteration++;
|
||||
filter.Timestamps["Proj.csproj"] = new DateTime(1007);
|
||||
|
||||
// Act
|
||||
await filter.ProcessAsync(context, default);
|
||||
|
||||
// Assert
|
||||
Assert.True(context.RequiresMSBuildRevaluation);
|
||||
}
|
||||
|
||||
public class TestableMSBuildEvaluationFilter : MSBuildEvaluationFilter
|
||||
{
|
||||
public TestableMSBuildEvaluationFilter(IFileSetFactory factory)
|
||||
: base(factory)
|
||||
{
|
||||
}
|
||||
|
||||
public Dictionary<string, DateTime> Timestamps { get; } = new Dictionary<string, DateTime>();
|
||||
|
||||
protected override DateTime GetLastWriteTimeUtcSafely(string file) => Timestamps[file];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
// 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.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.DotNet.Watcher.Tools
|
||||
{
|
||||
public class NoRestoreFilterTest
|
||||
{
|
||||
private readonly string[] _arguments = new[] { "run" };
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_LeavesArgumentsUnchangedOnFirstRun()
|
||||
{
|
||||
// Arrange
|
||||
var filter = new NoRestoreFilter();
|
||||
|
||||
var context = new DotNetWatchContext
|
||||
{
|
||||
ProcessSpec = new ProcessSpec
|
||||
{
|
||||
Arguments = _arguments,
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
await filter.ProcessAsync(context, default);
|
||||
|
||||
// Assert
|
||||
Assert.Same(_arguments, context.ProcessSpec.Arguments);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_LeavesArgumentsUnchangedIfMsBuildRevaluationIsRequired()
|
||||
{
|
||||
// Arrange
|
||||
var filter = new NoRestoreFilter();
|
||||
|
||||
var context = new DotNetWatchContext
|
||||
{
|
||||
Iteration = 0,
|
||||
ProcessSpec = new ProcessSpec
|
||||
{
|
||||
Arguments = _arguments,
|
||||
}
|
||||
};
|
||||
await filter.ProcessAsync(context, default);
|
||||
|
||||
context.ChangedFile = "Test.proj";
|
||||
context.RequiresMSBuildRevaluation = true;
|
||||
context.Iteration++;
|
||||
|
||||
// Act
|
||||
await filter.ProcessAsync(context, default);
|
||||
|
||||
// Assert
|
||||
Assert.Same(_arguments, context.ProcessSpec.Arguments);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_LeavesArgumentsUnchangedIfOptimizationIsSuppressed()
|
||||
{
|
||||
// Arrange
|
||||
var filter = new NoRestoreFilter();
|
||||
|
||||
var context = new DotNetWatchContext
|
||||
{
|
||||
Iteration = 0,
|
||||
ProcessSpec = new ProcessSpec
|
||||
{
|
||||
Arguments = _arguments,
|
||||
},
|
||||
SuppressMSBuildIncrementalism = true,
|
||||
};
|
||||
await filter.ProcessAsync(context, default);
|
||||
|
||||
context.ChangedFile = "Program.cs";
|
||||
context.Iteration++;
|
||||
|
||||
// Act
|
||||
await filter.ProcessAsync(context, default);
|
||||
|
||||
// Assert
|
||||
Assert.Same(_arguments, context.ProcessSpec.Arguments);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_AddsNoRestoreSwitch()
|
||||
{
|
||||
// Arrange
|
||||
var filter = new NoRestoreFilter();
|
||||
|
||||
var context = new DotNetWatchContext
|
||||
{
|
||||
Iteration = 0,
|
||||
ProcessSpec = new ProcessSpec
|
||||
{
|
||||
Arguments = _arguments,
|
||||
}
|
||||
};
|
||||
await filter.ProcessAsync(context, default);
|
||||
|
||||
context.ChangedFile = "Program.cs";
|
||||
context.Iteration++;
|
||||
|
||||
// Act
|
||||
await filter.ProcessAsync(context, default);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(new[] { "run", "--no-restore" }, context.ProcessSpec.Arguments);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_AddsNoRestoreSwitch_WithAdditionalArguments()
|
||||
{
|
||||
// Arrange
|
||||
var filter = new NoRestoreFilter();
|
||||
|
||||
var context = new DotNetWatchContext
|
||||
{
|
||||
Iteration = 0,
|
||||
ProcessSpec = new ProcessSpec
|
||||
{
|
||||
Arguments = new[] { "run", "-f", "net5.0", "--", "foo=bar" },
|
||||
}
|
||||
};
|
||||
await filter.ProcessAsync(context, default);
|
||||
|
||||
context.ChangedFile = "Program.cs";
|
||||
context.Iteration++;
|
||||
|
||||
// Act
|
||||
await filter.ProcessAsync(context, default);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(new[] { "run", "--no-restore", "-f", "net5.0", "--", "foo=bar" }, context.ProcessSpec.Arguments);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_AddsNoRestoreSwitch_ForTestCommand()
|
||||
{
|
||||
// Arrange
|
||||
var filter = new NoRestoreFilter();
|
||||
|
||||
var context = new DotNetWatchContext
|
||||
{
|
||||
Iteration = 0,
|
||||
ProcessSpec = new ProcessSpec
|
||||
{
|
||||
Arguments = new[] { "test", "--filter SomeFilter" },
|
||||
}
|
||||
};
|
||||
await filter.ProcessAsync(context, default);
|
||||
|
||||
context.ChangedFile = "Program.cs";
|
||||
context.Iteration++;
|
||||
|
||||
// Act
|
||||
await filter.ProcessAsync(context, default);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(new[] { "test", "--no-restore", "--filter SomeFilter" }, context.ProcessSpec.Arguments);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_DoesNotModifyArgumentsForUnknownCommands()
|
||||
{
|
||||
// Arrange
|
||||
var filter = new NoRestoreFilter();
|
||||
var arguments = new[] { "ef", "database", "update" };
|
||||
|
||||
var context = new DotNetWatchContext
|
||||
{
|
||||
Iteration = 0,
|
||||
ProcessSpec = new ProcessSpec
|
||||
{
|
||||
Arguments = arguments,
|
||||
}
|
||||
};
|
||||
await filter.ProcessAsync(context, default);
|
||||
|
||||
context.ChangedFile = "Program.cs";
|
||||
context.Iteration++;
|
||||
|
||||
// Act
|
||||
await filter.ProcessAsync(context, default);
|
||||
|
||||
// Assert
|
||||
Assert.Same(arguments, context.ProcessSpec.Arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -39,6 +39,8 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
|
|||
|
||||
public AwaitableProcess Process { get; protected set; }
|
||||
|
||||
public List<string> DotnetWatchArgs { get; } = new List<string>();
|
||||
|
||||
public string SourceDirectory { get; }
|
||||
|
||||
public Task HasRestarted()
|
||||
|
|
@ -86,6 +88,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
|
|||
{
|
||||
Scenario.DotNetWatchPath,
|
||||
};
|
||||
args.AddRange(DotnetWatchArgs);
|
||||
args.AddRange(arguments);
|
||||
|
||||
var dotnetPath = "dotnet";
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// 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.Diagnostics;
|
||||
using System.Threading;
|
||||
|
||||
namespace KitchenSink
|
||||
{
|
||||
|
|
@ -15,6 +16,12 @@ namespace KitchenSink
|
|||
Console.WriteLine($"Process identifier = {Process.GetCurrentProcess().Id}, {Process.GetCurrentProcess().StartTime:hh:mm:ss.FF}");
|
||||
Console.WriteLine("DOTNET_WATCH = " + Environment.GetEnvironmentVariable("DOTNET_WATCH"));
|
||||
Console.WriteLine("DOTNET_WATCH_ITERATION = " + Environment.GetEnvironmentVariable("DOTNET_WATCH_ITERATION"));
|
||||
|
||||
if (args.Length > 0 && args[0] == "wait")
|
||||
{
|
||||
Console.WriteLine("Waiting for process to be terminated.");
|
||||
Thread.Sleep(Timeout.Infinite);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue