diff --git a/src/BuiltInTools/DotNetWatchTasks/DotNetWatchTasks.csproj b/src/BuiltInTools/DotNetWatchTasks/DotNetWatchTasks.csproj
index af178129da01..491d57555c28 100644
--- a/src/BuiltInTools/DotNetWatchTasks/DotNetWatchTasks.csproj
+++ b/src/BuiltInTools/DotNetWatchTasks/DotNetWatchTasks.csproj
@@ -10,7 +10,7 @@
-
+
diff --git a/src/BuiltInTools/dotnet-watch/Build/BuildNames.cs b/src/BuiltInTools/dotnet-watch/Build/BuildNames.cs
new file mode 100644
index 000000000000..a49547f3948d
--- /dev/null
+++ b/src/BuiltInTools/dotnet-watch/Build/BuildNames.cs
@@ -0,0 +1,47 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.DotNet.Watch;
+
+internal static class PropertyNames
+{
+ public const string TargetFramework = nameof(TargetFramework);
+ public const string TargetFrameworkIdentifier = nameof(TargetFrameworkIdentifier);
+ public const string TargetPath = nameof(TargetPath);
+ public const string EnableDefaultItems = nameof(EnableDefaultItems);
+ public const string TargetFrameworks = nameof(TargetFrameworks);
+ public const string WebAssemblyHotReloadCapabilities = nameof(WebAssemblyHotReloadCapabilities);
+ public const string TargetFrameworkVersion = nameof(TargetFrameworkVersion);
+ public const string TargetName = nameof(TargetName);
+ public const string IntermediateOutputPath = nameof(IntermediateOutputPath);
+ public const string HotReloadAutoRestart = nameof(HotReloadAutoRestart);
+ public const string DefaultItemExcludes = nameof(DefaultItemExcludes);
+ public const string CustomCollectWatchItems = nameof(CustomCollectWatchItems);
+ public const string UsingMicrosoftNETSdkRazor = nameof(UsingMicrosoftNETSdkRazor);
+ public const string DotNetWatchContentFiles = nameof(DotNetWatchContentFiles);
+ public const string DotNetWatchBuild = nameof(DotNetWatchBuild);
+ public const string DesignTimeBuild = nameof(DesignTimeBuild);
+ public const string SkipCompilerExecution = nameof(SkipCompilerExecution);
+ public const string ProvideCommandLineArgs = nameof(ProvideCommandLineArgs);
+}
+
+internal static class ItemNames
+{
+ public const string Watch = nameof(Watch);
+ public const string AdditionalFiles = nameof(AdditionalFiles);
+ public const string Compile = nameof(Compile);
+ public const string Content = nameof(Content);
+ public const string ProjectCapability = nameof(ProjectCapability);
+}
+
+internal static class MetadataNames
+{
+ public const string Watch = nameof(Watch);
+}
+
+internal static class TargetNames
+{
+ public const string Compile = nameof(Compile);
+ public const string Restore = nameof(Restore);
+ public const string GenerateComputedBuildStaticWebAssets = nameof(GenerateComputedBuildStaticWebAssets);
+}
diff --git a/src/BuiltInTools/dotnet-watch/Build/BuildReporter.cs b/src/BuiltInTools/dotnet-watch/Build/BuildReporter.cs
new file mode 100644
index 000000000000..5a06112ab5a1
--- /dev/null
+++ b/src/BuiltInTools/dotnet-watch/Build/BuildReporter.cs
@@ -0,0 +1,104 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Logging;
+
+namespace Microsoft.DotNet.Watch;
+
+internal sealed class BuildReporter(IReporter reporter, EnvironmentOptions environmentOptions)
+{
+ public IReporter Reporter => reporter;
+ public EnvironmentOptions EnvironmentOptions => environmentOptions;
+
+ public Loggers GetLoggers(string projectPath, string operationName)
+ => new(reporter, environmentOptions.GetTestBinLogPath(projectPath, operationName));
+
+ public void ReportWatchedFiles(Dictionary fileItems)
+ {
+ reporter.Verbose($"Watching {fileItems.Count} file(s) for changes");
+
+ if (environmentOptions.TestFlags.HasFlag(TestFlags.RunningAsTest))
+ {
+ foreach (var file in fileItems.Values)
+ {
+ reporter.Verbose(file.StaticWebAssetPath != null
+ ? $"> {file.FilePath}{Path.PathSeparator}{file.StaticWebAssetPath}"
+ : $"> {file.FilePath}");
+ }
+ }
+ }
+
+ public sealed class Loggers(IReporter reporter, string? binLogPath) : IEnumerable, IDisposable
+ {
+ private readonly BinaryLogger? _binaryLogger = binLogPath != null
+ ? new()
+ {
+ Verbosity = LoggerVerbosity.Diagnostic,
+ Parameters = "LogFile=" + binLogPath,
+ }
+ : null;
+
+ private readonly OutputLogger _outputLogger =
+ new(reporter)
+ {
+ Verbosity = LoggerVerbosity.Minimal
+ };
+
+ public void Dispose()
+ {
+ _outputLogger.Clear();
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ yield return _outputLogger;
+
+ if (_binaryLogger != null)
+ {
+ yield return _binaryLogger;
+ }
+ }
+
+ public void ReportOutput()
+ {
+ if (binLogPath != null)
+ {
+ reporter.Verbose($"Binary log: '{binLogPath}'");
+ }
+
+ _outputLogger.ReportOutput();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ => GetEnumerator();
+ }
+
+ private sealed class OutputLogger : ConsoleLogger
+ {
+ private readonly IReporter _reporter;
+ private readonly List _messages = [];
+
+ public OutputLogger(IReporter reporter)
+ {
+ WriteHandler = Write;
+ _reporter = reporter;
+ }
+
+ public IReadOnlyList Messages
+ => _messages;
+
+ public void Clear()
+ => _messages.Clear();
+
+ private void Write(string message)
+ => _messages.Add(new OutputLine(message.TrimEnd('\r', '\n'), IsError: false));
+
+ public void ReportOutput()
+ {
+ _reporter.Output($"MSBuild output:");
+ BuildOutput.ReportBuildOutput(_reporter, Messages, success: false, projectDisplay: null);
+ }
+ }
+}
diff --git a/src/BuiltInTools/dotnet-watch/Build/EvaluationResult.cs b/src/BuiltInTools/dotnet-watch/Build/EvaluationResult.cs
index 8204bc050199..ee209e05b03a 100644
--- a/src/BuiltInTools/dotnet-watch/Build/EvaluationResult.cs
+++ b/src/BuiltInTools/dotnet-watch/Build/EvaluationResult.cs
@@ -1,14 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Collections.Immutable;
using Microsoft.Build.Graph;
namespace Microsoft.DotNet.Watch;
-internal sealed class EvaluationResult(IReadOnlyDictionary files, ProjectGraph? projectGraph)
+internal sealed class EvaluationResult(IReadOnlyDictionary files, ProjectGraph projectGraph)
{
public readonly IReadOnlyDictionary Files = files;
- public readonly ProjectGraph? ProjectGraph = projectGraph;
+ public readonly ProjectGraph ProjectGraph = projectGraph;
public readonly FilePathExclusions ItemExclusions
= projectGraph != null ? FilePathExclusions.Create(projectGraph) : FilePathExclusions.Empty;
@@ -16,7 +17,7 @@ public readonly FilePathExclusions ItemExclusions
private readonly Lazy> _lazyBuildFiles
= new(() => projectGraph != null ? CreateBuildFileSet(projectGraph) : new HashSet());
- public static IReadOnlySet CreateBuildFileSet(ProjectGraph projectGraph)
+ private static IReadOnlySet CreateBuildFileSet(ProjectGraph projectGraph)
=> projectGraph.ProjectNodes.SelectMany(p => p.ProjectInstance.ImportPaths)
.Concat(projectGraph.ProjectNodes.Select(p => p.ProjectInstance.FullPath))
.ToHashSet(PathUtilities.OSSpecificPathComparer);
@@ -29,4 +30,138 @@ public void WatchFiles(FileWatcher fileWatcher)
fileWatcher.WatchContainingDirectories(Files.Keys, includeSubdirectories: true);
fileWatcher.WatchFiles(BuildFiles);
}
+
+ ///
+ /// Loads project graph and performs design-time build.
+ ///
+ public static EvaluationResult? TryCreate(
+ string rootProjectPath,
+ IEnumerable buildArguments,
+ IReporter reporter,
+ EnvironmentOptions environmentOptions,
+ bool restore,
+ CancellationToken cancellationToken)
+ {
+ var buildReporter = new BuildReporter(reporter, environmentOptions);
+
+ // See https://github.com/dotnet/project-system/blob/main/docs/well-known-project-properties.md
+
+ var globalOptions = CommandLineOptions.ParseBuildProperties(buildArguments)
+ .ToImmutableDictionary(keySelector: arg => arg.key, elementSelector: arg => arg.value)
+ .SetItem(PropertyNames.DotNetWatchBuild, "true")
+ .SetItem(PropertyNames.DesignTimeBuild, "true")
+ .SetItem(PropertyNames.SkipCompilerExecution, "true")
+ .SetItem(PropertyNames.ProvideCommandLineArgs, "true")
+ // F# targets depend on host path variable:
+ .SetItem("DOTNET_HOST_PATH", environmentOptions.MuxerPath);
+
+ var projectGraph = ProjectGraphUtilities.TryLoadProjectGraph(
+ rootProjectPath,
+ globalOptions,
+ reporter,
+ projectGraphRequired: true,
+ cancellationToken);
+
+ if (projectGraph == null)
+ {
+ return null;
+ }
+
+ var rootNode = projectGraph.GraphRoots.Single();
+
+ if (restore)
+ {
+ using (var loggers = buildReporter.GetLoggers(rootNode.ProjectInstance.FullPath, "Restore"))
+ {
+ if (!rootNode.ProjectInstance.Build([TargetNames.Restore], loggers))
+ {
+ reporter.Error($"Failed to restore project '{rootProjectPath}'.");
+ loggers.ReportOutput();
+ return null;
+ }
+ }
+ }
+
+ var fileItems = new Dictionary();
+
+ foreach (var project in projectGraph.ProjectNodesTopologicallySorted)
+ {
+ // Deep copy so that we can reuse the graph for building additional targets later on.
+ // If we didn't copy the instance the targets might duplicate items that were already
+ // populated by design-time build.
+ var projectInstance = project.ProjectInstance.DeepCopy();
+
+ // skip outer build project nodes:
+ if (projectInstance.GetPropertyValue(PropertyNames.TargetFramework) == "")
+ {
+ continue;
+ }
+
+ var customCollectWatchItems = projectInstance.GetStringListPropertyValue(PropertyNames.CustomCollectWatchItems);
+
+ using (var loggers = buildReporter.GetLoggers(projectInstance.FullPath, "DesignTimeBuild"))
+ {
+ if (!projectInstance.Build([TargetNames.Compile, .. customCollectWatchItems], loggers))
+ {
+ reporter.Error($"Failed to build project '{projectInstance.FullPath}'.");
+ loggers.ReportOutput();
+ return null;
+ }
+ }
+
+ var projectPath = projectInstance.FullPath;
+ var projectDirectory = Path.GetDirectoryName(projectPath)!;
+
+ // TODO: Compile and AdditionalItems should be provided by Roslyn
+ var items = projectInstance.GetItems(ItemNames.Compile)
+ .Concat(projectInstance.GetItems(ItemNames.AdditionalFiles))
+ .Concat(projectInstance.GetItems(ItemNames.Watch));
+
+ foreach (var item in items)
+ {
+ AddFile(item.EvaluatedInclude, staticWebAssetPath: null);
+ }
+
+ if (!environmentOptions.SuppressHandlingStaticContentFiles &&
+ projectInstance.GetBooleanPropertyValue(PropertyNames.UsingMicrosoftNETSdkRazor) &&
+ projectInstance.GetBooleanPropertyValue(PropertyNames.DotNetWatchContentFiles, defaultValue: true))
+ {
+ foreach (var item in projectInstance.GetItems(ItemNames.Content))
+ {
+ if (item.GetBooleanMetadataValue(MetadataNames.Watch, defaultValue: true))
+ {
+ var relativeUrl = item.EvaluatedInclude.Replace('\\', '/');
+ if (relativeUrl.StartsWith("wwwroot/"))
+ {
+ AddFile(item.EvaluatedInclude, staticWebAssetPath: relativeUrl);
+ }
+ }
+ }
+ }
+
+ void AddFile(string include, string? staticWebAssetPath)
+ {
+ var filePath = Path.GetFullPath(Path.Combine(projectDirectory, include));
+
+ if (!fileItems.TryGetValue(filePath, out var existingFile))
+ {
+ fileItems.Add(filePath, new FileItem
+ {
+ FilePath = filePath,
+ ContainingProjectPaths = [projectPath],
+ StaticWebAssetPath = staticWebAssetPath,
+ });
+ }
+ else if (!existingFile.ContainingProjectPaths.Contains(projectPath))
+ {
+ // linked files might be included to multiple projects:
+ existingFile.ContainingProjectPaths.Add(projectPath);
+ }
+ }
+ }
+
+ buildReporter.ReportWatchedFiles(fileItems);
+
+ return new EvaluationResult(fileItems, projectGraph);
+ }
}
diff --git a/src/BuiltInTools/dotnet-watch/Build/ProjectGraphUtilities.cs b/src/BuiltInTools/dotnet-watch/Build/ProjectGraphUtilities.cs
new file mode 100644
index 000000000000..5ffe84d2604c
--- /dev/null
+++ b/src/BuiltInTools/dotnet-watch/Build/ProjectGraphUtilities.cs
@@ -0,0 +1,163 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Immutable;
+using Microsoft.Build.Evaluation;
+using Microsoft.Build.Execution;
+using Microsoft.Build.Graph;
+using Microsoft.DotNet.Cli;
+
+namespace Microsoft.DotNet.Watch;
+
+internal static class ProjectGraphUtilities
+{
+ ///
+ /// Tries to create a project graph by running the build evaluation phase on the .
+ ///
+ public static ProjectGraph? TryLoadProjectGraph(
+ string rootProjectFile,
+ ImmutableDictionary globalOptions,
+ IReporter reporter,
+ bool projectGraphRequired,
+ CancellationToken cancellationToken)
+ {
+ var entryPoint = new ProjectGraphEntryPoint(rootProjectFile, globalOptions);
+ try
+ {
+ // Create a new project collection that does not reuse element cache
+ // to work around https://github.com/dotnet/msbuild/issues/12064:
+ var collection = new ProjectCollection(
+ globalProperties: globalOptions,
+ loggers: [],
+ remoteLoggers: [],
+ ToolsetDefinitionLocations.Default,
+ maxNodeCount: 1,
+ onlyLogCriticalEvents: false,
+ loadProjectsReadOnly: false,
+ useAsynchronousLogging: false,
+ reuseProjectRootElementCache: false);
+
+ return new ProjectGraph([entryPoint], collection, projectInstanceFactory: null, cancellationToken);
+ }
+ catch (Exception e)
+ {
+ reporter.Verbose("Failed to load project graph.");
+
+ if (e is AggregateException { InnerExceptions: var innerExceptions })
+ {
+ foreach (var inner in innerExceptions)
+ {
+ Report(inner);
+ }
+ }
+ else
+ {
+ Report(e);
+ }
+
+ void Report(Exception e)
+ {
+ if (projectGraphRequired)
+ {
+ reporter.Error(e.Message);
+ }
+ else
+ {
+ reporter.Warn(e.Message);
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public static string GetDisplayName(this ProjectGraphNode projectNode)
+ => $"{Path.GetFileNameWithoutExtension(projectNode.ProjectInstance.FullPath)} ({projectNode.GetTargetFramework()})";
+
+ public static string GetTargetFramework(this ProjectGraphNode projectNode)
+ => projectNode.ProjectInstance.GetPropertyValue(PropertyNames.TargetFramework);
+
+ public static IEnumerable GetTargetFrameworks(this ProjectGraphNode projectNode)
+ => projectNode.GetStringListPropertyValue(PropertyNames.TargetFrameworks);
+
+ public static Version? GetTargetFrameworkVersion(this ProjectGraphNode projectNode)
+ => EnvironmentVariableNames.TryParseTargetFrameworkVersion(projectNode.ProjectInstance.GetPropertyValue(PropertyNames.TargetFrameworkVersion));
+
+ public static IEnumerable GetWebAssemblyCapabilities(this ProjectGraphNode projectNode)
+ => projectNode.GetStringListPropertyValue(PropertyNames.WebAssemblyHotReloadCapabilities);
+
+ public static bool IsTargetFrameworkVersionOrNewer(this ProjectGraphNode projectNode, Version minVersion)
+ => projectNode.GetTargetFrameworkVersion() is { } version && version >= minVersion;
+
+ public static bool IsNetCoreApp(string identifier)
+ => string.Equals(identifier, ".NETCoreApp", StringComparison.OrdinalIgnoreCase);
+
+ public static bool IsNetCoreApp(this ProjectGraphNode projectNode)
+ => IsNetCoreApp(projectNode.ProjectInstance.GetPropertyValue(PropertyNames.TargetFrameworkIdentifier));
+
+ public static bool IsNetCoreApp(this ProjectGraphNode projectNode, Version minVersion)
+ => projectNode.IsNetCoreApp() && projectNode.IsTargetFrameworkVersionOrNewer(minVersion);
+
+ public static bool IsWebApp(this ProjectGraphNode projectNode)
+ => projectNode.GetCapabilities().Any(static value => value is "AspNetCore" or "WebAssembly");
+
+ public static string? GetOutputDirectory(this ProjectGraphNode projectNode)
+ => projectNode.ProjectInstance.GetPropertyValue(PropertyNames.TargetPath) is { Length: >0 } path ? Path.GetDirectoryName(Path.Combine(projectNode.ProjectInstance.Directory, path)) : null;
+
+ public static string GetAssemblyName(this ProjectGraphNode projectNode)
+ => projectNode.ProjectInstance.GetPropertyValue(PropertyNames.TargetName);
+
+ public static string? GetIntermediateOutputDirectory(this ProjectGraphNode projectNode)
+ => projectNode.ProjectInstance.GetPropertyValue(PropertyNames.IntermediateOutputPath) is { Length: >0 } path ? Path.Combine(projectNode.ProjectInstance.Directory, path) : null;
+
+ public static IEnumerable GetCapabilities(this ProjectGraphNode projectNode)
+ => projectNode.ProjectInstance.GetItems(ItemNames.ProjectCapability).Select(item => item.EvaluatedInclude);
+
+ public static bool IsAutoRestartEnabled(this ProjectGraphNode projectNode)
+ => projectNode.GetBooleanPropertyValue(PropertyNames.HotReloadAutoRestart);
+
+ public static bool AreDefaultItemsEnabled(this ProjectGraphNode projectNode)
+ => projectNode.GetBooleanPropertyValue(PropertyNames.EnableDefaultItems);
+
+ public static IEnumerable GetDefaultItemExcludes(this ProjectGraphNode projectNode)
+ => projectNode.GetStringListPropertyValue(PropertyNames.DefaultItemExcludes);
+
+ public static IEnumerable GetStringListPropertyValue(this ProjectGraphNode projectNode, string propertyName)
+ => projectNode.ProjectInstance.GetStringListPropertyValue(propertyName);
+
+ public static IEnumerable GetStringListPropertyValue(this ProjectInstance project, string propertyName)
+ => project.GetPropertyValue(propertyName).Split(';', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
+
+ public static bool GetBooleanPropertyValue(this ProjectGraphNode projectNode, string propertyName, bool defaultValue = false)
+ => GetBooleanPropertyValue(projectNode.ProjectInstance, propertyName, defaultValue);
+
+ public static bool GetBooleanPropertyValue(this ProjectInstance project, string propertyName, bool defaultValue = false)
+ => project.GetPropertyValue(propertyName) is { Length: >0 } value ? bool.TryParse(value, out var result) && result : defaultValue;
+
+ public static bool GetBooleanMetadataValue(this ProjectItemInstance item, string metadataName, bool defaultValue = false)
+ => item.GetMetadataValue(metadataName) is { Length: > 0 } value ? bool.TryParse(value, out var result) && result : defaultValue;
+
+ public static IEnumerable GetTransitivelyReferencingProjects(this IEnumerable projects)
+ {
+ var visited = new HashSet();
+ var queue = new Queue();
+ foreach (var project in projects)
+ {
+ queue.Enqueue(project);
+ }
+
+ while (queue.Count > 0)
+ {
+ var project = queue.Dequeue();
+ if (visited.Add(project))
+ {
+ foreach (var referencingProject in project.ReferencingProjects)
+ {
+ queue.Enqueue(referencingProject);
+ }
+ }
+ }
+
+ return visited;
+ }
+}
diff --git a/src/BuiltInTools/dotnet-watch/CommandLine/DotNetWatchContext.cs b/src/BuiltInTools/dotnet-watch/CommandLine/DotNetWatchContext.cs
index d0f597d134f8..53dd2f99dbde 100644
--- a/src/BuiltInTools/dotnet-watch/CommandLine/DotNetWatchContext.cs
+++ b/src/BuiltInTools/dotnet-watch/CommandLine/DotNetWatchContext.cs
@@ -12,13 +12,5 @@ internal sealed class DotNetWatchContext
public required ProcessRunner ProcessRunner { get; init; }
public required ProjectOptions RootProjectOptions { get; init; }
-
- public MSBuildFileSetFactory CreateMSBuildFileSetFactory()
- => new(
- RootProjectOptions.ProjectPath,
- RootProjectOptions.BuildArguments,
- EnvironmentOptions,
- ProcessRunner,
- Reporter);
}
}
diff --git a/src/BuiltInTools/dotnet-watch/CommandLine/EnvironmentOptions.cs b/src/BuiltInTools/dotnet-watch/CommandLine/EnvironmentOptions.cs
index 199c4d864fc2..846ddb54f30a 100644
--- a/src/BuiltInTools/dotnet-watch/CommandLine/EnvironmentOptions.cs
+++ b/src/BuiltInTools/dotnet-watch/CommandLine/EnvironmentOptions.cs
@@ -52,6 +52,8 @@ internal sealed record EnvironmentOptions(
TestOutput: EnvironmentVariables.TestOutputDir
);
+ private static int s_uniqueLogId;
+
public bool RunningAsTest { get => (TestFlags & TestFlags.RunningAsTest) != TestFlags.None; }
private static string GetMuxerPathFromEnvironment()
@@ -61,5 +63,10 @@ private static string GetMuxerPathFromEnvironment()
Debug.Assert(Path.GetFileNameWithoutExtension(muxerPath) == "dotnet", $"Invalid muxer path {muxerPath}");
return muxerPath;
}
+
+ public string? GetTestBinLogPath(string projectPath, string operationName)
+ => TestFlags.HasFlag(TestFlags.RunningAsTest)
+ ? Path.Combine(TestOutput, $"Watch.{operationName}.{Path.GetFileName(projectPath)}.{Interlocked.Increment(ref s_uniqueLogId)}.binlog")
+ : null;
}
}
diff --git a/src/BuiltInTools/dotnet-watch/FileWatcher/DirectoryWatcher.cs b/src/BuiltInTools/dotnet-watch/FileWatcher/DirectoryWatcher.cs
index 9891aa124193..1d7bb3137828 100644
--- a/src/BuiltInTools/dotnet-watch/FileWatcher/DirectoryWatcher.cs
+++ b/src/BuiltInTools/dotnet-watch/FileWatcher/DirectoryWatcher.cs
@@ -42,9 +42,6 @@ protected void NotifyError(Exception e)
OnError?.Invoke(this, e);
}
- public static DirectoryWatcher Create(string watchedDirectory, ImmutableHashSet watchedFileNames, bool includeSubdirectories)
- => Create(watchedDirectory, watchedFileNames, EnvironmentVariables.IsPollingEnabled, includeSubdirectories);
-
public static DirectoryWatcher Create(string watchedDirectory, ImmutableHashSet watchedFileNames, bool usePollingWatcher, bool includeSubdirectories)
{
return usePollingWatcher ?
diff --git a/src/BuiltInTools/dotnet-watch/FileWatcher/FileWatcher.cs b/src/BuiltInTools/dotnet-watch/FileWatcher/FileWatcher.cs
index 24669468b88f..b4fa7c55ddc8 100644
--- a/src/BuiltInTools/dotnet-watch/FileWatcher/FileWatcher.cs
+++ b/src/BuiltInTools/dotnet-watch/FileWatcher/FileWatcher.cs
@@ -6,7 +6,7 @@
namespace Microsoft.DotNet.Watch
{
- internal class FileWatcher(IReporter reporter) : IDisposable
+ internal class FileWatcher(IReporter reporter, EnvironmentOptions environmentOptions) : IDisposable
{
// Directory watcher for each watched directory tree.
// Keyed by full path to the root directory with a trailing directory separator.
@@ -40,7 +40,7 @@ public void Dispose()
protected virtual DirectoryWatcher CreateDirectoryWatcher(string directory, ImmutableHashSet fileNames, bool includeSubdirectories)
{
- var watcher = DirectoryWatcher.Create(directory, fileNames, includeSubdirectories);
+ var watcher = DirectoryWatcher.Create(directory, fileNames, environmentOptions.IsPollingEnabled, includeSubdirectories);
if (watcher is EventBasedDirectoryWatcher eventBasedWatcher)
{
eventBasedWatcher.Logger = message => reporter.Verbose(message);
@@ -204,9 +204,9 @@ void FileChangedCallback(ChangedPath change)
return change;
}
- public static async ValueTask WaitForFileChangeAsync(string filePath, IReporter reporter, Action? startedWatching, CancellationToken cancellationToken)
+ public static async ValueTask WaitForFileChangeAsync(string filePath, IReporter reporter, EnvironmentOptions environmentOptions, Action? startedWatching, CancellationToken cancellationToken)
{
- using var watcher = new FileWatcher(reporter);
+ using var watcher = new FileWatcher(reporter, environmentOptions);
watcher.WatchContainingDirectories([filePath], includeSubdirectories: false);
diff --git a/src/BuiltInTools/dotnet-watch/HotReload/AppModels/HotReloadAppModel.cs b/src/BuiltInTools/dotnet-watch/HotReload/AppModels/HotReloadAppModel.cs
index d37e00c38548..57e48c5c7f45 100644
--- a/src/BuiltInTools/dotnet-watch/HotReload/AppModels/HotReloadAppModel.cs
+++ b/src/BuiltInTools/dotnet-watch/HotReload/AppModels/HotReloadAppModel.cs
@@ -30,7 +30,7 @@ public bool TryGetStartupHookPath([NotNullWhen(true)] out string? path)
_ => "net10.0",
};
- path = Path.Combine(AppContext.BaseDirectory, "hotreload", hookTargetFramework, "Microsoft.Extensions.DotNetDeltaApplier.dll");
+ path = Path.Combine(Path.GetDirectoryName(typeof(HotReloadAppModel).Assembly.Location)!, "hotreload", hookTargetFramework, "Microsoft.Extensions.DotNetDeltaApplier.dll");
return true;
}
diff --git a/src/BuiltInTools/dotnet-watch/HotReload/HotReloadDotNetWatcher.cs b/src/BuiltInTools/dotnet-watch/HotReload/HotReloadDotNetWatcher.cs
index e958d41308d3..705a7abcb867 100644
--- a/src/BuiltInTools/dotnet-watch/HotReload/HotReloadDotNetWatcher.cs
+++ b/src/BuiltInTools/dotnet-watch/HotReload/HotReloadDotNetWatcher.cs
@@ -14,13 +14,12 @@ internal sealed class HotReloadDotNetWatcher
private readonly RestartPrompt? _rudeEditRestartPrompt;
private readonly DotNetWatchContext _context;
- private readonly MSBuildFileSetFactory _fileSetFactory;
+
+ internal Task? Test_FileChangesCompletedTask { get; set; }
public HotReloadDotNetWatcher(DotNetWatchContext context, IConsole console, IRuntimeProcessLauncherFactory? runtimeProcessLauncherFactory)
{
_context = context;
- _fileSetFactory = context.CreateMSBuildFileSetFactory();
-
_console = console;
_runtimeProcessLauncherFactory = runtimeProcessLauncherFactory;
if (!context.Options.NonInteractive)
@@ -62,7 +61,7 @@ public async Task WatchAsync(CancellationToken shutdownCancellationToken)
}
await using var browserConnector = new BrowserConnector(_context);
- using var fileWatcher = new FileWatcher(_context.Reporter);
+ using var fileWatcher = new FileWatcher(_context.Reporter, _context.EnvironmentOptions);
for (var iteration = 0; !shutdownCancellationToken.IsCancellationRequested; iteration++)
{
@@ -84,18 +83,25 @@ public async Task WatchAsync(CancellationToken shutdownCancellationToken)
try
{
var rootProjectOptions = _context.RootProjectOptions;
- var runtimeProcessLauncherFactory = _runtimeProcessLauncherFactory;
+
+ var (buildSucceeded, buildOutput, _) = await BuildProjectAsync(rootProjectOptions.ProjectPath, rootProjectOptions.BuildArguments, iterationCancellationToken);
+ BuildOutput.ReportBuildOutput(_context.Reporter, buildOutput, buildSucceeded, projectDisplay: rootProjectOptions.ProjectPath);
+ if (!buildSucceeded)
+ {
+ continue;
+ }
// Evaluate the target to find out the set of files to watch.
// In case the app fails to start due to build or other error we can wait for these files to change.
- evaluationResult = await EvaluateRootProjectAsync(iterationCancellationToken);
- Debug.Assert(evaluationResult.ProjectGraph != null);
+ // Avoid restore since the build above already restored the root project.
+ evaluationResult = await EvaluateRootProjectAsync(restore: false, iterationCancellationToken);
var rootProject = evaluationResult.ProjectGraph.GraphRoots.Single();
// use normalized MSBuild path so that we can index into the ProjectGraph
rootProjectOptions = rootProjectOptions with { ProjectPath = rootProject.ProjectInstance.FullPath };
+ var runtimeProcessLauncherFactory = _runtimeProcessLauncherFactory;
var rootProjectCapabilities = rootProject.GetCapabilities();
if (rootProjectCapabilities.Contains(AspireServiceFactory.AppHostProjectCapability))
{
@@ -105,13 +111,11 @@ public async Task WatchAsync(CancellationToken shutdownCancellationToken)
var projectMap = new ProjectNodeMap(evaluationResult.ProjectGraph, _context.Reporter);
compilationHandler = new CompilationHandler(_context.Reporter, _context.ProcessRunner);
- var scopedCssFileHandler = new ScopedCssFileHandler(_context.Reporter, projectMap, browserConnector);
+ var scopedCssFileHandler = new ScopedCssFileHandler(_context.Reporter, projectMap, browserConnector, _context.EnvironmentOptions);
var projectLauncher = new ProjectLauncher(_context, projectMap, browserConnector, compilationHandler, iteration);
evaluationResult.ItemExclusions.Report(_context.Reporter);
- var rootProjectNode = evaluationResult.ProjectGraph.GraphRoots.Single();
-
- runtimeProcessLauncher = runtimeProcessLauncherFactory?.TryCreate(rootProjectNode, projectLauncher, rootProjectOptions);
+ runtimeProcessLauncher = runtimeProcessLauncherFactory?.TryCreate(rootProject, projectLauncher, rootProjectOptions);
if (runtimeProcessLauncher != null)
{
var launcherEnvironment = runtimeProcessLauncher.GetEnvironmentVariables();
@@ -121,13 +125,6 @@ public async Task WatchAsync(CancellationToken shutdownCancellationToken)
};
}
- var (buildSucceeded, buildOutput, _) = await BuildProjectAsync(rootProjectOptions.ProjectPath, rootProjectOptions.BuildArguments, iterationCancellationToken);
- BuildOutput.ReportBuildOutput(_context.Reporter, buildOutput, buildSucceeded, projectDisplay: rootProjectOptions.ProjectPath);
- if (!buildSucceeded)
- {
- continue;
- }
-
rootRunningProject = await projectLauncher.TryLaunchProcessAsync(
rootProjectOptions,
rootProcessTerminationSource,
@@ -169,7 +166,7 @@ public async Task WatchAsync(CancellationToken shutdownCancellationToken)
return;
}
- await compilationHandler.Workspace.UpdateProjectConeAsync(_fileSetFactory.RootProjectFile, iterationCancellationToken);
+ await compilationHandler.Workspace.UpdateProjectConeAsync(rootProjectOptions.ProjectPath, iterationCancellationToken);
// Solution must be initialized after we load the solution but before we start watching for file changes to avoid race condition
// when the EnC session captures content of the file after the changes has already been made.
@@ -205,6 +202,11 @@ void FileChangedCallback(ChangedPath change)
{
try
{
+ if (Test_FileChangesCompletedTask != null)
+ {
+ await Test_FileChangesCompletedTask;
+ }
+
// Use timeout to batch file changes. If the process doesn't exit within the given timespan we'll check
// for accumulated file changes. If there are any we attempt Hot Reload. Otherwise we come back here to wait again.
_ = await rootRunningProject.RunningProcess.WaitAsync(TimeSpan.FromMilliseconds(extendTimeout ? 200 : 50), iterationCancellationToken);
@@ -441,12 +443,12 @@ async Task> CaptureChangedFilesSnapshot(ImmutableDict
_context.Reporter.Report(fileAdded ? MessageDescriptor.FileAdditionTriggeredReEvaluation : MessageDescriptor.ProjectChangeTriggeredReEvaluation);
// TODO: consider re-evaluating only affected projects instead of the whole graph.
- evaluationResult = await EvaluateRootProjectAsync(iterationCancellationToken);
+ evaluationResult = await EvaluateRootProjectAsync(restore: true, iterationCancellationToken);
- // additional directories may have been added:
+ // additional files/directories may have been added:
evaluationResult.WatchFiles(fileWatcher);
- await compilationHandler.Workspace.UpdateProjectConeAsync(_fileSetFactory.RootProjectFile, iterationCancellationToken);
+ await compilationHandler.Workspace.UpdateProjectConeAsync(rootProjectOptions.ProjectPath, iterationCancellationToken);
if (shutdownCancellationToken.IsCancellationRequested)
{
@@ -456,7 +458,8 @@ async Task> CaptureChangedFilesSnapshot(ImmutableDict
// Update files in the change set with new evaluation info.
changedFiles = [.. changedFiles
- .Select(f => evaluationResult.Files.TryGetValue(f.Item.FilePath, out var evaluatedFile) ? f with { Item = evaluatedFile } : f)];
+ .Select(f => evaluationResult.Files.TryGetValue(f.Item.FilePath, out var evaluatedFile) ? f with { Item = evaluatedFile } : f)
+ ];
_context.Reporter.Report(MessageDescriptor.ReEvaluationCompleted);
}
@@ -490,7 +493,8 @@ async Task> CaptureChangedFilesSnapshot(ImmutableDict
if (!evaluationRequired)
{
- // update the workspace to reflect changes in the file content:
+ // Update the workspace to reflect changes in the file content:.
+ // If the project was re-evaluated the Roslyn solution is already up to date.
await compilationHandler.Workspace.UpdateFileContentAsync(changedFiles, iterationCancellationToken);
}
@@ -568,7 +572,7 @@ private async ValueTask WaitForFileChangeBeforeRestarting(FileWatcher fileWatche
else
{
// evaluation cancelled - watch for any changes in the directory tree containing the root project:
- fileWatcher.WatchContainingDirectories([_fileSetFactory.RootProjectFile], includeSubdirectories: true);
+ fileWatcher.WatchContainingDirectories([_context.RootProjectOptions.ProjectPath], includeSubdirectories: true);
_ = await fileWatcher.WaitForFileChangeAsync(
acceptChange: change => AcceptChange(change),
@@ -577,9 +581,6 @@ private async ValueTask WaitForFileChangeBeforeRestarting(FileWatcher fileWatche
}
}
- private Predicate CreateChangeFilter(EvaluationResult evaluationResult)
- => new(change => AcceptChange(change, evaluationResult));
-
private bool AcceptChange(ChangedPath change, EvaluationResult evaluationResult)
{
var (path, kind) = change;
@@ -754,22 +755,29 @@ static string GetPluralMessage(ChangeKind kind)
};
}
- private async ValueTask EvaluateRootProjectAsync(CancellationToken cancellationToken)
+ private async ValueTask EvaluateRootProjectAsync(bool restore, CancellationToken cancellationToken)
{
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
- var result = await _fileSetFactory.TryCreateAsync(requireProjectGraph: true, cancellationToken);
+ var result = EvaluationResult.TryCreate(
+ _context.RootProjectOptions.ProjectPath,
+ _context.RootProjectOptions.BuildArguments,
+ _context.Reporter,
+ _context.EnvironmentOptions,
+ restore,
+ cancellationToken);
+
if (result != null)
{
- Debug.Assert(result.ProjectGraph != null);
return result;
}
await FileWatcher.WaitForFileChangeAsync(
- _fileSetFactory.RootProjectFile,
+ _context.RootProjectOptions.ProjectPath,
_context.Reporter,
+ _context.EnvironmentOptions,
startedWatching: () => _context.Reporter.Report(MessageDescriptor.FixBuildError),
cancellationToken);
}
@@ -780,6 +788,10 @@ await FileWatcher.WaitForFileChangeAsync(
{
var buildOutput = new List();
+ string[] binLogArguments = _context.EnvironmentOptions.GetTestBinLogPath(projectPath, "Build") is { } binLogPath
+ ? [$"-bl:{binLogPath}"]
+ : [];
+
var processSpec = new ProcessSpec
{
Executable = _context.EnvironmentOptions.MuxerPath,
@@ -792,7 +804,7 @@ await FileWatcher.WaitForFileChangeAsync(
}
},
// pass user-specified build arguments last to override defaults:
- Arguments = ["build", projectPath, "-consoleLoggerParameters:NoSummary;Verbosity=minimal", .. buildArguments]
+ Arguments = ["build", projectPath, "-consoleLoggerParameters:NoSummary;Verbosity=minimal", .. binLogArguments, .. buildArguments]
};
_context.Reporter.Output($"Building {projectPath} ...");
diff --git a/src/BuiltInTools/dotnet-watch/HotReload/ScopedCssFileHandler.cs b/src/BuiltInTools/dotnet-watch/HotReload/ScopedCssFileHandler.cs
index 6c30ef91c0d9..b0c11f9815a9 100644
--- a/src/BuiltInTools/dotnet-watch/HotReload/ScopedCssFileHandler.cs
+++ b/src/BuiltInTools/dotnet-watch/HotReload/ScopedCssFileHandler.cs
@@ -2,14 +2,13 @@
// The .NET Foundation licenses this file to you under the MIT license.
-using Microsoft.Build.Framework;
using Microsoft.Build.Graph;
namespace Microsoft.DotNet.Watch
{
- internal sealed class ScopedCssFileHandler(IReporter reporter, ProjectNodeMap projectMap, BrowserConnector browserConnector)
+ internal sealed class ScopedCssFileHandler(IReporter reporter, ProjectNodeMap projectMap, BrowserConnector browserConnector, EnvironmentOptions environmentOptions)
{
- private const string BuildTargetName = "GenerateComputedBuildStaticWebAssets";
+ private const string BuildTargetName = TargetNames.GenerateComputedBuildStaticWebAssets;
public async ValueTask HandleFileChangesAsync(IReadOnlyList files, CancellationToken cancellationToken)
{
@@ -54,20 +53,16 @@ public async ValueTask HandleFileChangesAsync(IReadOnlyList files,
return;
}
- var logger = reporter.IsVerbose ? new[] { new Build.Logging.ConsoleLogger(LoggerVerbosity.Minimal) } : null;
+ var buildReporter = new BuildReporter(reporter, environmentOptions);
var buildTasks = projectsToRefresh.Select(projectNode => Task.Run(() =>
{
- try
- {
- if (!projectNode.ProjectInstance.DeepCopy().Build(BuildTargetName, logger))
- {
- return null;
- }
- }
- catch (Exception e)
+ using var loggers = buildReporter.GetLoggers(projectNode.ProjectInstance.FullPath, BuildTargetName);
+
+ // Deep copy so that we don't pollute the project graph:
+ if (!projectNode.ProjectInstance.DeepCopy().Build(BuildTargetName, loggers))
{
- reporter.Error($"[{projectNode.GetDisplayName()}] Target {BuildTargetName} failed to build: {e}");
+ loggers.ReportOutput();
return null;
}
diff --git a/src/BuiltInTools/dotnet-watch/Program.cs b/src/BuiltInTools/dotnet-watch/Program.cs
index a028bdfd26f6..89eb9630a4dd 100644
--- a/src/BuiltInTools/dotnet-watch/Program.cs
+++ b/src/BuiltInTools/dotnet-watch/Program.cs
@@ -244,9 +244,8 @@ private async Task ListFilesAsync(ProcessRunner processRunner, Cancellation
var fileSetFactory = new MSBuildFileSetFactory(
rootProjectOptions.ProjectPath,
rootProjectOptions.BuildArguments,
- environmentOptions,
processRunner,
- reporter);
+ new BuildReporter(reporter, environmentOptions));
if (await fileSetFactory.TryCreateAsync(requireProjectGraph: null, cancellationToken) is not { } evaluationResult)
{
diff --git a/src/BuiltInTools/dotnet-watch/Properties/launchSettings.json b/src/BuiltInTools/dotnet-watch/Properties/launchSettings.json
index 219c522680d3..730989539fe7 100644
--- a/src/BuiltInTools/dotnet-watch/Properties/launchSettings.json
+++ b/src/BuiltInTools/dotnet-watch/Properties/launchSettings.json
@@ -2,15 +2,17 @@
"profiles": {
"dotnet-watch": {
"commandName": "Project",
- "commandLineArgs": "--verbose /bl:DotnetRun.binlog -lp http --non-interactive",
- "workingDirectory": "C:\\sdk1\\artifacts\\tmp\\Debug\\Aspire_BuildE---04F22750\\WatchAspire.AppHost",
+ "commandLineArgs": "--verbose /bl:DotnetRun.binlog",
+ "workingDirectory": "E:\\sdk\\artifacts\\tmp\\Debug\\Aspire_BuildE---04F22750\\WatchAspire.AppHost",
"environmentVariables": {
"DOTNET_WATCH_DEBUG_SDK_DIRECTORY": "$(RepoRoot)artifacts\\bin\\redist\\$(Configuration)\\dotnet\\sdk\\$(Version)",
"DCP_IDE_REQUEST_TIMEOUT_SECONDS": "100000",
"DCP_IDE_NOTIFICATION_TIMEOUT_SECONDS": "100000",
"DCP_IDE_NOTIFICATION_KEEPALIVE_SECONDS": "100000",
"DOTNET_WATCH_PROCESS_CLEANUP_TIMEOUT_MS": "0",
- "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "1"
+ "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "1",
+ "__DOTNET_WATCH_TEST_FLAGS": "RunningAsTest",
+ "__DOTNET_WATCH_TEST_OUTPUT_DIR": "$(RepoRoot)\\artifacts\\tmp\\Debug"
}
}
}
diff --git a/src/BuiltInTools/dotnet-watch/Utilities/ProjectGraphNodeExtensions.cs b/src/BuiltInTools/dotnet-watch/Utilities/ProjectGraphNodeExtensions.cs
deleted file mode 100644
index 780ae3477cdc..000000000000
--- a/src/BuiltInTools/dotnet-watch/Utilities/ProjectGraphNodeExtensions.cs
+++ /dev/null
@@ -1,89 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System.Collections.Immutable;
-using Microsoft.Build.Graph;
-using Microsoft.DotNet.Cli;
-
-namespace Microsoft.DotNet.Watch;
-
-internal static class ProjectGraphNodeExtensions
-{
- public static string GetDisplayName(this ProjectGraphNode projectNode)
- => $"{Path.GetFileNameWithoutExtension(projectNode.ProjectInstance.FullPath)} ({projectNode.GetTargetFramework()})";
-
- public static string GetTargetFramework(this ProjectGraphNode projectNode)
- => projectNode.ProjectInstance.GetPropertyValue("TargetFramework");
-
- public static Version? GetTargetFrameworkVersion(this ProjectGraphNode projectNode)
- => EnvironmentVariableNames.TryParseTargetFrameworkVersion(projectNode.ProjectInstance.GetPropertyValue("TargetFrameworkVersion"));
-
- public static IEnumerable GetWebAssemblyCapabilities(this ProjectGraphNode projectNode)
- => projectNode.GetStringListPropertyValue("WebAssemblyHotReloadCapabilities");
-
- public static bool IsTargetFrameworkVersionOrNewer(this ProjectGraphNode projectNode, Version minVersion)
- => GetTargetFrameworkVersion(projectNode) is { } version && version >= minVersion;
-
- public static bool IsNetCoreApp(string identifier)
- => string.Equals(identifier, ".NETCoreApp", StringComparison.OrdinalIgnoreCase);
-
- public static bool IsNetCoreApp(this ProjectGraphNode projectNode)
- => IsNetCoreApp(projectNode.ProjectInstance.GetPropertyValue("TargetFrameworkIdentifier"));
-
- public static bool IsNetCoreApp(this ProjectGraphNode projectNode, Version minVersion)
- => IsNetCoreApp(projectNode) && IsTargetFrameworkVersionOrNewer(projectNode, minVersion);
-
- public static bool IsWebApp(this ProjectGraphNode projectNode)
- => projectNode.GetCapabilities().Any(static value => value is "AspNetCore" or "WebAssembly");
-
- public static string? GetOutputDirectory(this ProjectGraphNode projectNode)
- => projectNode.ProjectInstance.GetPropertyValue("TargetPath") is { Length: >0 } path ? Path.GetDirectoryName(Path.Combine(projectNode.ProjectInstance.Directory, path)) : null;
-
- public static string GetAssemblyName(this ProjectGraphNode projectNode)
- => projectNode.ProjectInstance.GetPropertyValue("TargetName");
-
- public static string? GetIntermediateOutputDirectory(this ProjectGraphNode projectNode)
- => projectNode.ProjectInstance.GetPropertyValue("IntermediateOutputPath") is { Length: >0 } path ? Path.Combine(projectNode.ProjectInstance.Directory, path) : null;
-
- public static IEnumerable GetCapabilities(this ProjectGraphNode projectNode)
- => projectNode.ProjectInstance.GetItems("ProjectCapability").Select(item => item.EvaluatedInclude);
-
- public static bool IsAutoRestartEnabled(this ProjectGraphNode projectNode)
- => projectNode.GetBooleanPropertyValue("HotReloadAutoRestart");
-
- public static bool AreDefaultItemsEnabled(this ProjectGraphNode projectNode)
- => projectNode.GetBooleanPropertyValue("EnableDefaultItems");
-
- public static IEnumerable GetDefaultItemExcludes(this ProjectGraphNode projectNode)
- => projectNode.GetStringListPropertyValue("DefaultItemExcludes");
-
- private static IEnumerable GetStringListPropertyValue(this ProjectGraphNode projectNode, string propertyName)
- => projectNode.ProjectInstance.GetPropertyValue(propertyName).Split(';', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
-
- private static bool GetBooleanPropertyValue(this ProjectGraphNode projectNode, string propertyName)
- => bool.TryParse(projectNode.ProjectInstance.GetPropertyValue(propertyName), out var result) && result;
-
- public static IEnumerable GetTransitivelyReferencingProjects(this IEnumerable projects)
- {
- var visited = new HashSet();
- var queue = new Queue();
- foreach (var project in projects)
- {
- queue.Enqueue(project);
- }
-
- while (queue.Count > 0)
- {
- var project = queue.Dequeue();
- if (visited.Add(project))
- {
- foreach (var referencingProject in project.ReferencingProjects)
- {
- queue.Enqueue(referencingProject);
- }
- }
- }
-
- return visited;
- }
-}
diff --git a/src/BuiltInTools/dotnet-watch/Watch/BuildEvaluator.cs b/src/BuiltInTools/dotnet-watch/Watch/BuildEvaluator.cs
index 5a74aa5b032e..e056d122535b 100644
--- a/src/BuiltInTools/dotnet-watch/Watch/BuildEvaluator.cs
+++ b/src/BuiltInTools/dotnet-watch/Watch/BuildEvaluator.cs
@@ -24,7 +24,7 @@ internal class BuildEvaluator
private List<(string fileName, DateTime lastWriteTimeUtc)>? _msbuildFileTimestamps;
// result of the last evaluation, or null if no evaluation has been performed yet.
- private EvaluationResult? _evaluationResult;
+ private MSBuildFileSetFactory.EvaluationResult? _evaluationResult;
public bool RequiresRevaluation { get; set; }
@@ -35,7 +35,11 @@ public BuildEvaluator(DotNetWatchContext context)
}
protected virtual MSBuildFileSetFactory CreateMSBuildFileSetFactory()
- => _context.CreateMSBuildFileSetFactory();
+ => new(
+ _context.RootProjectOptions.ProjectPath,
+ _context.RootProjectOptions.BuildArguments,
+ _context.ProcessRunner,
+ new BuildReporter(_context.Reporter, _context.EnvironmentOptions));
public IReadOnlyList GetProcessArguments(int iteration)
{
@@ -57,7 +61,7 @@ public IReadOnlyList GetProcessArguments(int iteration)
return [_context.RootProjectOptions.Command, .. _context.RootProjectOptions.CommandArguments];
}
- public async ValueTask EvaluateAsync(ChangedFile? changedFile, CancellationToken cancellationToken)
+ public async ValueTask EvaluateAsync(ChangedFile? changedFile, CancellationToken cancellationToken)
{
if (_context.EnvironmentOptions.SuppressMSBuildIncrementalism)
{
@@ -83,7 +87,7 @@ public async ValueTask EvaluateAsync(ChangedFile? changedFile,
return _evaluationResult;
}
- private async ValueTask CreateEvaluationResult(CancellationToken cancellationToken)
+ private async ValueTask CreateEvaluationResult(CancellationToken cancellationToken)
{
while (true)
{
@@ -98,6 +102,7 @@ private async ValueTask CreateEvaluationResult(CancellationTok
await FileWatcher.WaitForFileChangeAsync(
_fileSetFactory.RootProjectFile,
_context.Reporter,
+ _context.EnvironmentOptions,
startedWatching: () => _context.Reporter.Report(MessageDescriptor.FixBuildError),
cancellationToken);
}
@@ -131,7 +136,7 @@ private bool RequiresMSBuildRevaluation(FileItem? changedFile)
return false;
}
- private List<(string fileName, DateTime lastModifiedUtc)> GetMSBuildFileTimeStamps(EvaluationResult result)
+ private List<(string fileName, DateTime lastModifiedUtc)> GetMSBuildFileTimeStamps(MSBuildFileSetFactory.EvaluationResult result)
{
var msbuildFiles = new List<(string fileName, DateTime lastModifiedUtc)>();
foreach (var (filePath, _) in result.Files)
diff --git a/src/BuiltInTools/dotnet-watch/Build/DotNetWatch.targets b/src/BuiltInTools/dotnet-watch/Watch/DotNetWatch.targets
similarity index 100%
rename from src/BuiltInTools/dotnet-watch/Build/DotNetWatch.targets
rename to src/BuiltInTools/dotnet-watch/Watch/DotNetWatch.targets
diff --git a/src/BuiltInTools/dotnet-watch/Watch/DotNetWatcher.cs b/src/BuiltInTools/dotnet-watch/Watch/DotNetWatcher.cs
index 11069f4450c6..1c2b25c8de25 100644
--- a/src/BuiltInTools/dotnet-watch/Watch/DotNetWatcher.cs
+++ b/src/BuiltInTools/dotnet-watch/Watch/DotNetWatcher.cs
@@ -77,7 +77,7 @@ public static async Task WatchAsync(DotNetWatchContext context, CancellationToke
using var currentRunCancellationSource = new CancellationTokenSource();
using var combinedCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(shutdownCancellationToken, currentRunCancellationSource.Token);
- using var fileSetWatcher = new FileWatcher(context.Reporter);
+ using var fileSetWatcher = new FileWatcher(context.Reporter, context.EnvironmentOptions);
fileSetWatcher.WatchContainingDirectories(evaluationResult.Files.Keys, includeSubdirectories: true);
diff --git a/src/BuiltInTools/dotnet-watch/Build/MSBuildFileSetResult.cs b/src/BuiltInTools/dotnet-watch/Watch/MSBuildFileSetResult.cs
similarity index 100%
rename from src/BuiltInTools/dotnet-watch/Build/MSBuildFileSetResult.cs
rename to src/BuiltInTools/dotnet-watch/Watch/MSBuildFileSetResult.cs
diff --git a/src/BuiltInTools/dotnet-watch/Build/MsBuildFileSetFactory.cs b/src/BuiltInTools/dotnet-watch/Watch/MsBuildFileSetFactory.cs
similarity index 70%
rename from src/BuiltInTools/dotnet-watch/Build/MsBuildFileSetFactory.cs
rename to src/BuiltInTools/dotnet-watch/Watch/MsBuildFileSetFactory.cs
index 008634208eee..3421ec498ec4 100644
--- a/src/BuiltInTools/dotnet-watch/Build/MsBuildFileSetFactory.cs
+++ b/src/BuiltInTools/dotnet-watch/Watch/MsBuildFileSetFactory.cs
@@ -1,12 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System.Collections.Concurrent;
+using System.Collections.Immutable;
using System.Diagnostics;
using System.Text.Json;
-using Microsoft.Build.Evaluation;
-using Microsoft.Build.Execution;
-using Microsoft.Build.FileSystem;
using Microsoft.Build.Graph;
namespace Microsoft.DotNet.Watch
@@ -17,20 +14,26 @@ namespace Microsoft.DotNet.Watch
/// Invokes msbuild to evaluate on the root project, which traverses all project dependencies and collects
/// items that are to be watched. The target can be customized by defining CustomCollectWatchItems target. This is currently done by Razor SDK
/// to collect razor and cshtml files.
- ///
/// Consider replacing with traversal (https://github.com/dotnet/sdk/issues/40214).
///
internal class MSBuildFileSetFactory(
string rootProjectFile,
IEnumerable buildArguments,
- EnvironmentOptions environmentOptions,
ProcessRunner processRunner,
- IReporter reporter)
+ BuildReporter buildReporter)
{
private const string TargetName = "GenerateWatchList";
private const string WatchTargetsFileName = "DotNetWatch.targets";
public string RootProjectFile => rootProjectFile;
+ private EnvironmentOptions EnvironmentOptions => buildReporter.EnvironmentOptions;
+ private IReporter Reporter => buildReporter.Reporter;
+
+ internal sealed class EvaluationResult(IReadOnlyDictionary files, ProjectGraph? projectGraph)
+ {
+ public readonly IReadOnlyDictionary Files = files;
+ public readonly ProjectGraph? ProjectGraph = projectGraph;
+ }
// Virtual for testing.
public virtual async ValueTask TryCreateAsync(bool? requireProjectGraph, CancellationToken cancellationToken)
@@ -44,7 +47,7 @@ internal class MSBuildFileSetFactory(
var processSpec = new ProcessSpec
{
- Executable = environmentOptions.MuxerPath,
+ Executable = EnvironmentOptions.MuxerPath,
WorkingDirectory = projectDir,
Arguments = arguments,
OnOutput = line =>
@@ -56,19 +59,19 @@ internal class MSBuildFileSetFactory(
}
};
- reporter.Verbose($"Running MSBuild target '{TargetName}' on '{rootProjectFile}'");
+ Reporter.Verbose($"Running MSBuild target '{TargetName}' on '{rootProjectFile}'");
- var exitCode = await processRunner.RunAsync(processSpec, reporter, isUserApplication: false, launchResult: null, cancellationToken);
+ var exitCode = await processRunner.RunAsync(processSpec, Reporter, isUserApplication: false, launchResult: null, cancellationToken);
var success = exitCode == 0 && File.Exists(watchList);
if (!success)
{
- reporter.Error($"Error(s) finding watch items project file '{Path.GetFileName(rootProjectFile)}'.");
- reporter.Output($"MSBuild output from target '{TargetName}':");
+ Reporter.Error($"Error(s) finding watch items project file '{Path.GetFileName(rootProjectFile)}'.");
+ Reporter.Output($"MSBuild output from target '{TargetName}':");
}
- BuildOutput.ReportBuildOutput(reporter, capturedOutput, success, projectDisplay: null);
+ BuildOutput.ReportBuildOutput(Reporter, capturedOutput, success, projectDisplay: null);
if (!success)
{
return null;
@@ -110,14 +113,8 @@ void AddFile(string filePath, string? staticWebAssetPath)
}
}
- reporter.Verbose($"Watching {fileItems.Count} file(s) for changes");
+ buildReporter.ReportWatchedFiles(fileItems);
#if DEBUG
-
- foreach (var file in fileItems.Values)
- {
- reporter.Verbose($" -> {file.FilePath} {file.StaticWebAssetPath}");
- }
-
Debug.Assert(fileItems.Values.All(f => Path.IsPathRooted(f.FilePath)), "All files should be rooted paths");
#endif
@@ -125,7 +122,10 @@ void AddFile(string filePath, string? staticWebAssetPath)
ProjectGraph? projectGraph = null;
if (requireProjectGraph != null)
{
- projectGraph = TryLoadProjectGraph(requireProjectGraph.Value, cancellationToken);
+ var globalOptions = CommandLineOptions.ParseBuildProperties(buildArguments)
+ .ToImmutableDictionary(keySelector: arg => arg.key, elementSelector: arg => arg.value);
+
+ projectGraph = ProjectGraphUtilities.TryLoadProjectGraph(rootProjectFile, globalOptions, Reporter, requireProjectGraph.Value, cancellationToken);
if (projectGraph == null && requireProjectGraph == true)
{
return null;
@@ -154,11 +154,9 @@ private IReadOnlyList GetMSBuildArguments(string watchListFilePath)
"/t:" + TargetName
};
-#if !DEBUG
- if (environmentOptions.TestFlags.HasFlag(TestFlags.RunningAsTest))
-#endif
+ if (EnvironmentOptions.GetTestBinLogPath(rootProjectFile, "GenerateWatchList") is { } binLogPath)
{
- arguments.Add($"/bl:{Path.Combine(environmentOptions.TestOutput, "DotnetWatch.GenerateWatchList.binlog")}");
+ arguments.Add($"/bl:{binLogPath}");
}
arguments.AddRange(buildArguments);
@@ -166,7 +164,7 @@ private IReadOnlyList GetMSBuildArguments(string watchListFilePath)
// Set dotnet-watch reserved properties after the user specified propeties,
// so that the former take precedence.
- if (environmentOptions.SuppressHandlingStaticContentFiles)
+ if (EnvironmentOptions.SuppressHandlingStaticContentFiles)
{
arguments.Add("/p:DotNetWatchContentFiles=false");
}
@@ -196,57 +194,5 @@ private static string FindTargetsFile()
var targetPath = searchPaths.Select(p => Path.Combine(p, WatchTargetsFileName)).FirstOrDefault(File.Exists);
return targetPath ?? throw new FileNotFoundException("Fatal error: could not find DotNetWatch.targets");
}
-
- // internal for testing
-
- ///
- /// Tries to create a project graph by running the build evaluation phase on the .
- ///
- internal ProjectGraph? TryLoadProjectGraph(bool projectGraphRequired, CancellationToken cancellationToken)
- {
- var globalOptions = new Dictionary();
-
- foreach (var (name, value) in CommandLineOptions.ParseBuildProperties(buildArguments))
- {
- globalOptions[name] = value;
- }
-
- var entryPoint = new ProjectGraphEntryPoint(rootProjectFile, globalOptions);
-
- try
- {
- return new ProjectGraph([entryPoint], ProjectCollection.GlobalProjectCollection, projectInstanceFactory: null, cancellationToken);
- }
- catch (Exception e)
- {
- reporter.Verbose("Failed to load project graph.");
-
- if (e is AggregateException { InnerExceptions: var innerExceptions })
- {
- foreach (var inner in innerExceptions)
- {
- Report(inner);
- }
- }
- else
- {
- Report(e);
- }
-
- void Report(Exception e)
- {
- if (projectGraphRequired)
- {
- reporter.Error(e.Message);
- }
- else
- {
- reporter.Warn(e.Message);
- }
- }
- }
-
- return null;
- }
}
}
diff --git a/src/BuiltInTools/dotnet-watch/dotnet-watch.csproj b/src/BuiltInTools/dotnet-watch/dotnet-watch.csproj
index a401dd5667ab..a17d1ca4d04a 100644
--- a/src/BuiltInTools/dotnet-watch/dotnet-watch.csproj
+++ b/src/BuiltInTools/dotnet-watch/dotnet-watch.csproj
@@ -29,7 +29,7 @@
-
+
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/Mocks/MockBuildEngine.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/Mocks/MockBuildEngine.cs
index 95c9b92ade73..bf648190f912 100644
--- a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/Mocks/MockBuildEngine.cs
+++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/Mocks/MockBuildEngine.cs
@@ -8,7 +8,11 @@
namespace Microsoft.NET.Build.Tasks.UnitTests
{
- public class MockBuildEngine : IBuildEngine4
+ ///
+ /// Must be internal to avoid loading msbuild assemblies before the test assembly,
+ /// which might need to set the msbuild location.
+ ///
+ internal class MockBuildEngine : IBuildEngine4
{
public int ColumnNumberOfTaskNode { get; set; }
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/Mocks/MockTaskItem.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/Mocks/MockTaskItem.cs
index f76842199892..59dda4ada48f 100644
--- a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/Mocks/MockTaskItem.cs
+++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/Mocks/MockTaskItem.cs
@@ -8,7 +8,11 @@
namespace Microsoft.NET.Build.Tasks.UnitTests
{
- public class MockTaskItem : ITaskItem
+ ///
+ /// Must be internal to avoid loading msbuild assemblies before the test assembly,
+ /// which might need to set the msbuild location.
+ ///
+ internal class MockTaskItem : ITaskItem
{
private Dictionary _metadata = new(StringComparer.OrdinalIgnoreCase);
diff --git a/test/Microsoft.NET.Build.Containers.UnitTests/Microsoft.NET.Build.Containers.UnitTests.csproj b/test/Microsoft.NET.Build.Containers.UnitTests/Microsoft.NET.Build.Containers.UnitTests.csproj
index 10b1abfc7ab5..8b77b42790a9 100644
--- a/test/Microsoft.NET.Build.Containers.UnitTests/Microsoft.NET.Build.Containers.UnitTests.csproj
+++ b/test/Microsoft.NET.Build.Containers.UnitTests/Microsoft.NET.Build.Containers.UnitTests.csproj
@@ -12,6 +12,7 @@
+
diff --git a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToFloatWarningLevels.cs b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToFloatWarningLevels.cs
index 1b8f6347860e..dce92c1248c8 100644
--- a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToFloatWarningLevels.cs
+++ b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToFloatWarningLevels.cs
@@ -45,7 +45,7 @@ static void Main()
};
var testAsset = _testAssetsManager
- .CreateTestProject(testProject, identifier: "warningLevelConsoleApp" + tfm, targetExtension: ".csproj");
+ .CreateTestProject(testProject, identifier: "warningLevelConsoleApp" + tfm);
var buildCommand = new GetValuesCommand(
Log,
@@ -89,7 +89,7 @@ static void Main()
};
testProject.AdditionalProperties.Add("WarningLevel", warningLevel?.ToString());
var testAsset = _testAssetsManager
- .CreateTestProject(testProject, identifier: "customWarningLevelConsoleApp", targetExtension: ".csproj");
+ .CreateTestProject(testProject, identifier: "customWarningLevelConsoleApp");
var buildCommand = new GetValuesCommand(
Log,
@@ -134,7 +134,7 @@ static void Main()
};
var testAsset = _testAssetsManager
- .CreateTestProject(testProject, identifier: "analysisLevelConsoleApp" + tfm, targetExtension: ".csproj");
+ .CreateTestProject(testProject, identifier: "analysisLevelConsoleApp" + tfm);
var buildCommand = new GetValuesCommand(
Log,
@@ -190,7 +190,7 @@ static void Main()
testProject.AdditionalProperties.Add("AnalysisLevel", "preview");
var testAsset = _testAssetsManager
- .CreateTestProject(testProject, identifier: "analysisLevelPreviewConsoleApp" + currentTFM, targetExtension: ".csproj");
+ .CreateTestProject(testProject, identifier: "analysisLevelPreviewConsoleApp" + currentTFM);
var buildCommand = new GetValuesCommand(
Log,
@@ -237,7 +237,7 @@ static void Main()
testProject.AdditionalProperties.Add("AnalysisLevel", analysisLevel);
var testAsset = _testAssetsManager
- .CreateTestProject(testProject, identifier: "analysisLevelPreviewConsoleApp" + ToolsetInfo.CurrentTargetFramework + analysisLevel, targetExtension: ".csproj");
+ .CreateTestProject(testProject, identifier: "analysisLevelPreviewConsoleApp" + ToolsetInfo.CurrentTargetFramework + analysisLevel);
var buildCommand = new GetValuesCommand(
Log,
@@ -333,7 +333,7 @@ static void Main()
testProject.AdditionalProperties.Add("CodeAnalysisTreatWarningsAsErrors", codeAnalysisTreatWarningsAsErrors);
var testAsset = _testAssetsManager
- .CreateTestProject(testProject, identifier: "analysisLevelPreviewConsoleApp" + ToolsetInfo.CurrentTargetFramework + analysisLevel + category, targetExtension: ".csproj");
+ .CreateTestProject(testProject, identifier: "analysisLevelPreviewConsoleApp" + ToolsetInfo.CurrentTargetFramework + analysisLevel + category);
var buildCommand = new GetValuesCommand(
Log,
@@ -458,7 +458,7 @@ public static void CA1031_All()
testProject.AdditionalProperties.Add("NoWarn", "CS9057");
var testAsset = _testAssetsManager
- .CreateTestProject(testProject, identifier: "analysisLevelConsoleApp" + ToolsetInfo.CurrentTargetFramework + analysisLevel + $"Warnaserror:{codeAnalysisTreatWarningsAsErrors}", targetExtension: ".csproj");
+ .CreateTestProject(testProject, identifier: "analysisLevelConsoleApp" + ToolsetInfo.CurrentTargetFramework + analysisLevel + $"Warnaserror:{codeAnalysisTreatWarningsAsErrors}");
var buildCommand = new BuildCommand(Log, Path.Combine(testAsset.TestRoot, testProject.Name));
var buildResult = buildCommand.Execute();
diff --git a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToUsePlatformAnalyzers.cs b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToUsePlatformAnalyzers.cs
index a22b1c84446b..758382bff059 100644
--- a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToUsePlatformAnalyzers.cs
+++ b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToUsePlatformAnalyzers.cs
@@ -43,7 +43,7 @@ static void Main()
};
var testAsset = _testAssetsManager
- .CreateTestProject(testProject, identifier: "analyzerConsoleApp", targetExtension: ".csproj");
+ .CreateTestProject(testProject, identifier: "analyzerConsoleApp");
var buildCommand = new GetValuesCommand(
Log,
@@ -89,7 +89,7 @@ static void Main()
testProject.AdditionalProperties.Add("AnalysisLevel", "4");
var testAsset = _testAssetsManager
- .CreateTestProject(testProject, identifier: "analyzerConsoleApp", targetExtension: ".csproj");
+ .CreateTestProject(testProject, identifier: "analyzerConsoleApp");
var buildCommand = new GetValuesCommand(
Log,
@@ -135,7 +135,7 @@ static void Main()
testProject.AdditionalProperties.Add("AnalysisLevel", "5");
var testAsset = _testAssetsManager
- .CreateTestProject(testProject, identifier: "analyzerConsoleApp", targetExtension: ".csproj");
+ .CreateTestProject(testProject, identifier: "analyzerConsoleApp");
var buildCommand = new GetValuesCommand(
Log,
@@ -181,7 +181,7 @@ static void Main()
testProject.AdditionalProperties.Add("EnableNETAnalyzers", "false");
var testAsset = _testAssetsManager
- .CreateTestProject(testProject, identifier: "analyzerConsoleApp", targetExtension: ".csproj");
+ .CreateTestProject(testProject, identifier: "analyzerConsoleApp");
var buildCommand = new GetValuesCommand(
Log,
@@ -227,7 +227,7 @@ static void Main()
testProject.AdditionalProperties.Add("EnableNETAnalyzers", "true");
var testAsset = _testAssetsManager
- .CreateTestProject(testProject, identifier: "analyzerConsoleApp", targetExtension: ".csproj");
+ .CreateTestProject(testProject, identifier: "analyzerConsoleApp");
var buildCommand = new GetValuesCommand(
Log,
@@ -274,7 +274,7 @@ static void Main()
testProject.AdditionalProperties.Add("CodeAnalysisTreatWarningsAsErrors", "false");
testProject.AdditionalProperties.Add("TreatWarningsAsErrors", "true");
var testAsset = _testAssetsManager
- .CreateTestProject(testProject, identifier: "analyzerConsoleApp", targetExtension: ".csproj");
+ .CreateTestProject(testProject, identifier: "analyzerConsoleApp");
var buildCommand = new BuildCommand(Log, Path.Combine(testAsset.TestRoot, testProject.Name));
buildCommand.Execute().Should().Pass();
@@ -312,7 +312,7 @@ static void Main()
testProject.AdditionalProperties.Add("AnalysisLevel", "none");
var testAsset = _testAssetsManager
- .CreateTestProject(testProject, identifier: "analyzerConsoleApp", targetExtension: ".csproj");
+ .CreateTestProject(testProject, identifier: "analyzerConsoleApp");
var buildCommand = new GetValuesCommand(
Log,
@@ -358,7 +358,7 @@ static void Main()
testProject.AdditionalProperties.Add("AnalysisLevel", "latest");
var testAsset = _testAssetsManager
- .CreateTestProject(testProject, identifier: "analyzerConsoleApp", targetExtension: ".csproj");
+ .CreateTestProject(testProject, identifier: "analyzerConsoleApp");
var buildCommand = new GetValuesCommand(
Log,
@@ -404,7 +404,7 @@ static void Main()
testProject.AdditionalProperties.Add("AnalysisLevel", "latest");
var testAsset = _testAssetsManager
- .CreateTestProject(testProject, identifier: "analyzerConsoleApp", targetExtension: ".csproj");
+ .CreateTestProject(testProject, identifier: "analyzerConsoleApp");
var buildCommand = new GetValuesCommand(
Log,
diff --git a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToUseVB.cs b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToUseVB.cs
index 4b0bc2639026..e3195b782763 100644
--- a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToUseVB.cs
+++ b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToUseVB.cs
@@ -37,6 +37,7 @@ public void It_builds_a_simple_vb_project(string targetFramework, bool isExe)
Name = "HelloWorld",
TargetFrameworks = targetFramework,
IsExe = isExe,
+ TargetExtension = ".vbproj",
SourceFiles =
{
["Program.vb"] = @"
@@ -68,7 +69,7 @@ End Module
};
var testAsset = _testAssetsManager
- .CreateTestProject(testProject, identifier: targetFramework + isExe, targetExtension: ".vbproj");
+ .CreateTestProject(testProject, identifier: targetFramework + isExe);
var buildCommand = new GetValuesCommand(
Log,
@@ -103,7 +104,7 @@ private static (VBRuntime, string[]) GetExpectedOutputs(string targetFramework,
"HelloWorld.exe.config",
"HelloWorld.pdb"
};
- if (TestProject.ReferenceAssembliesAreInstalled(TargetDotNetFrameworkVersion.Version471))
+ if (ToolLocationHelper.GetPathToDotNetFrameworkReferenceAssemblies(TargetDotNetFrameworkVersion.Version471) != null)
{
return (VBRuntime.Default, files);
}
diff --git a/test/Microsoft.NET.Build.Tests/RoslynBuildTaskTests.cs b/test/Microsoft.NET.Build.Tests/RoslynBuildTaskTests.cs
index ad760834bae2..ba14cd54eebc 100644
--- a/test/Microsoft.NET.Build.Tests/RoslynBuildTaskTests.cs
+++ b/test/Microsoft.NET.Build.Tests/RoslynBuildTaskTests.cs
@@ -105,6 +105,7 @@ End Module
{
Name = "App1",
IsExe = true,
+ TargetExtension = projExtension,
SourceFiles =
{
[sourceName] = sourceText,
@@ -118,7 +119,7 @@ End Module
}
configure?.Invoke(project);
- return _testAssetsManager.CreateTestProject(project, callingMethod: callingMethod, targetExtension: projExtension);
+ return _testAssetsManager.CreateTestProject(project, callingMethod: callingMethod);
}
private static void AddCompilersToolsetPackage(TestProject project)
diff --git a/test/Microsoft.NET.Restore.Tests/GivenThatWeWantAutomaticTargetingPackReferences.cs b/test/Microsoft.NET.Restore.Tests/GivenThatWeWantAutomaticTargetingPackReferences.cs
index 3ed666a35bba..b5cdde0bf1c5 100644
--- a/test/Microsoft.NET.Restore.Tests/GivenThatWeWantAutomaticTargetingPackReferences.cs
+++ b/test/Microsoft.NET.Restore.Tests/GivenThatWeWantAutomaticTargetingPackReferences.cs
@@ -3,10 +3,10 @@
#nullable disable
-using Microsoft.Build.Utilities;
using NuGet.Common;
using NuGet.Frameworks;
using NuGet.ProjectModel;
+using Microsoft.Build.Utilities;
namespace Microsoft.NET.Restore.Tests
{
@@ -46,7 +46,7 @@ public void It_restores_net_framework_project_successfully(string version)
LockFile lockFile = LockFileUtilities.GetLockFile(projectAssetsJsonPath, NullLogger.Instance);
var netFrameworkLibrary = lockFile.GetTarget(NuGetFramework.Parse(".NETFramework,Version=v" + version), null).Libraries.FirstOrDefault((file) => file.Name.Contains(targetFramework));
- if (TestProject.ReferenceAssembliesAreInstalled(targetFrameworkVersion))
+ if (ToolLocationHelper.GetPathToDotNetFrameworkReferenceAssemblies(targetFrameworkVersion) != null)
{
netFrameworkLibrary.Should().BeNull();
}
@@ -104,7 +104,7 @@ public void It_restores_multitargeted_net_framework_project_successfully(bool in
NullLogger.Instance);
var net471FrameworkLibrary = lockFile.GetTarget(NuGetFramework.Parse(".NETFramework,Version=v4.7.1"), null).Libraries.FirstOrDefault((file) => file.Name.Contains("net471"));
- if (TestProject.ReferenceAssembliesAreInstalled(TargetDotNetFrameworkVersion.Version471) && !includeExplicitReference)
+ if (ToolLocationHelper.GetPathToDotNetFrameworkReferenceAssemblies(TargetDotNetFrameworkVersion.Version471) != null && !includeExplicitReference)
{
net471FrameworkLibrary.Should().BeNull();
}
@@ -116,7 +116,7 @@ public void It_restores_multitargeted_net_framework_project_successfully(bool in
var net472FrameworkLibrary = lockFile.GetTarget(NuGetFramework.Parse(".NETFramework,Version=v4.7.2"), null).Libraries.FirstOrDefault((file) => file.Name.Contains("net472"));
- if (TestProject.ReferenceAssembliesAreInstalled(TargetDotNetFrameworkVersion.Version472) && !includeExplicitReference)
+ if (ToolLocationHelper.GetPathToDotNetFrameworkReferenceAssemblies(TargetDotNetFrameworkVersion.Version472) != null && !includeExplicitReference)
{
net472FrameworkLibrary.Should().BeNull();
}
@@ -189,7 +189,7 @@ public void It_fails_without_assembly_pack_reference()
var testAsset = _testAssetsManager.CreateTestProject(testProject);
var buildCommand = new BuildCommand(testAsset);
- if (TestProject.ReferenceAssembliesAreInstalled(TargetDotNetFrameworkVersion.Version472))
+ if (ToolLocationHelper.GetPathToDotNetFrameworkReferenceAssemblies(TargetDotNetFrameworkVersion.Version472) != null)
{
buildCommand.Execute()
.Should()
diff --git a/test/Microsoft.NET.Restore.Tests/Microsoft.NET.Restore.Tests.csproj b/test/Microsoft.NET.Restore.Tests/Microsoft.NET.Restore.Tests.csproj
index 9d797145d653..ba4c241ee8e7 100644
--- a/test/Microsoft.NET.Restore.Tests/Microsoft.NET.Restore.Tests.csproj
+++ b/test/Microsoft.NET.Restore.Tests/Microsoft.NET.Restore.Tests.csproj
@@ -25,6 +25,7 @@
+
diff --git a/test/Microsoft.NET.TestFramework/Microsoft.NET.TestFramework.csproj b/test/Microsoft.NET.TestFramework/Microsoft.NET.TestFramework.csproj
index 8237a1b38f1e..fef4cf243d82 100644
--- a/test/Microsoft.NET.TestFramework/Microsoft.NET.TestFramework.csproj
+++ b/test/Microsoft.NET.TestFramework/Microsoft.NET.TestFramework.csproj
@@ -40,7 +40,11 @@
-
+
+
diff --git a/test/Microsoft.NET.TestFramework/ProjectConstruction/TestProject.cs b/test/Microsoft.NET.TestFramework/ProjectConstruction/TestProject.cs
index 175b281218b9..9a1653b3e635 100644
--- a/test/Microsoft.NET.TestFramework/ProjectConstruction/TestProject.cs
+++ b/test/Microsoft.NET.TestFramework/ProjectConstruction/TestProject.cs
@@ -3,7 +3,6 @@
using System.Diagnostics;
using System.Runtime.CompilerServices;
-using Microsoft.Build.Utilities;
using NuGet.Frameworks;
namespace Microsoft.NET.TestFramework.ProjectConstruction
@@ -24,6 +23,8 @@ public TestProject([CallerMemberName] string? name = null)
///
public string? Name { get; set; }
+ public string TargetExtension { get; set; } = ".csproj";
+
public bool IsSdkProject { get; set; } = true;
public bool IsExe { get; set; }
@@ -56,7 +57,7 @@ public TestProject([CallerMemberName] string? name = null)
public bool UseArtifactsOutput { get; set; }
- public List ReferencedProjects { get; } = new List();
+ public List ReferencedProjects { get; } = [];
public List References { get; } = new List();
@@ -126,12 +127,12 @@ public bool BuildsOnNonWindows
}
}
- internal void Create(TestAsset targetTestAsset, string testProjectsSourceFolder, string targetExtension = ".csproj")
+ internal void Create(TestAsset targetTestAsset, string testProjectsSourceFolder)
{
string targetFolder = Path.Combine(targetTestAsset.Path, Name ?? string.Empty);
Directory.CreateDirectory(targetFolder);
- string targetProjectPath = Path.Combine(targetFolder, Name + targetExtension);
+ string targetProjectPath = Path.Combine(targetFolder, Name + TargetExtension);
string sourceProject;
string sourceProjectBase = Path.Combine(testProjectsSourceFolder, "ProjectConstruction");
@@ -139,7 +140,7 @@ internal void Create(TestAsset targetTestAsset, string testProjectsSourceFolder,
{
sourceProject = Path.Combine(sourceProjectBase, "SdkProject", "SdkProject.csproj");
}
- else if (targetExtension == ".vbproj")
+ else if (TargetExtension == ".vbproj")
{
sourceProject = Path.Combine(sourceProjectBase, "NetFrameworkProjectVB", "NetFrameworkProject.vbproj");
}
@@ -309,7 +310,7 @@ internal void Create(TestAsset targetTestAsset, string testProjectsSourceFolder,
foreach (var referencedProject in ReferencedProjects)
{
projectReferenceItemGroup.Add(new XElement(ns + "ProjectReference",
- new XAttribute("Include", $"../{referencedProject?.Name}/{referencedProject?.Name}.csproj")));
+ new XAttribute("Include", $"../{referencedProject.Name}/{referencedProject.Name}{referencedProject.TargetExtension}")));
}
}
@@ -425,7 +426,7 @@ static void Main(string[] args)
foreach (var dependency in ReferencedProjects)
{
- string? safeDependencyName = dependency?.Name?.Replace('.', '_');
+ string? safeDependencyName = dependency.Name?.Replace('.', '_');
source += $" Console.WriteLine({safeDependencyName}.{safeDependencyName}Class.Name);" + Environment.NewLine;
source += $" Console.WriteLine({safeDependencyName}.{safeDependencyName}Class.List);" + Environment.NewLine;
@@ -454,7 +455,7 @@ public class {safeThisName}Class
";
foreach (var dependency in ReferencedProjects)
{
- string? safeDependencyName = dependency?.Name?.Replace('.', '_');
+ string? safeDependencyName = dependency.Name?.Replace('.', '_');
source += $" public string {safeDependencyName}Name {{ get {{ return {safeDependencyName}.{safeDependencyName}Class.Name; }} }}" + Environment.NewLine;
source += $" public List {safeDependencyName}List {{ get {{ return {safeDependencyName}.{safeDependencyName}Class.List; }} }}" + Environment.NewLine;
@@ -464,22 +465,15 @@ public class {safeThisName}Class
@" }
}";
string sourcePath = Path.Combine(targetFolder, Name + ".cs");
-
File.WriteAllText(sourcePath, source);
}
-
- }
- else
- {
- foreach (var kvp in SourceFiles)
- {
- File.WriteAllText(Path.Combine(targetFolder, kvp.Key), kvp.Value);
- }
}
- foreach (var kvp in EmbeddedResources)
+ foreach (var kvp in SourceFiles.Concat(EmbeddedResources))
{
- File.WriteAllText(Path.Combine(targetFolder, kvp.Key), kvp.Value);
+ var targetPath = Path.Combine(targetFolder, kvp.Key);
+ Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!);
+ File.WriteAllText(targetPath, kvp.Value);
}
}
@@ -522,12 +516,6 @@ public Dictionary GetPropertyValues(string testRoot, string? tar
return propertyValues;
}
- public static bool ReferenceAssembliesAreInstalled(TargetDotNetFrameworkVersion targetFrameworkVersion)
- {
- var referenceAssemblies = ToolLocationHelper.GetPathToDotNetFrameworkReferenceAssemblies(targetFrameworkVersion);
- return referenceAssemblies != null;
- }
-
private OutputPathCalculator GetOutputPathCalculator(string testRoot)
{
return OutputPathCalculator.FromProject(Path.Combine(testRoot, Name ?? string.Empty, Name + ".csproj"), this);
diff --git a/test/Microsoft.NET.TestFramework/TestAssetsManager.cs b/test/Microsoft.NET.TestFramework/TestAssetsManager.cs
index 52809ebe67ca..097b17c004b7 100644
--- a/test/Microsoft.NET.TestFramework/TestAssetsManager.cs
+++ b/test/Microsoft.NET.TestFramework/TestAssetsManager.cs
@@ -56,19 +56,17 @@ public TestAsset CopyTestAsset(
/// Pass in the unique theory parameters that can indentify that theory from others.
/// The Identifier is used to distinguish between theory child tests. Generally it should be created using a combination of all of the theory parameter values.
/// This is distinct from the test project name and is used to prevent file collisions between theory tests that use the same test project.
- /// The extension type of the desired test project, e.g. .csproj, or .fsproj.
/// A new TestAsset directory for the TestProject.
public TestAsset CreateTestProject(
TestProject testProject,
[CallerMemberName] string callingMethod = "",
- string? identifier = "",
- string targetExtension = ".csproj")
+ string? identifier = "")
{
var testDestinationDirectory =
GetTestDestinationDirectoryPath(testProject.Name, callingMethod, identifier);
TestDestinationDirectories.Add(testDestinationDirectory);
- var testAsset = CreateTestProjectsInDirectory(new List() { testProject }, testDestinationDirectory, targetExtension);
+ var testAsset = CreateTestProjectsInDirectory(new List() { testProject }, testDestinationDirectory);
testAsset.TestProject = testProject;
return testAsset;
@@ -89,14 +87,13 @@ public TestAsset CreateTestProject(
public TestAsset CreateTestProjects(
IEnumerable testProjects,
[CallerMemberName] string callingMethod = "",
- string identifier = "",
- string targetExtension = ".csproj")
+ string identifier = "")
{
var testDestinationDirectory =
GetTestDestinationDirectoryPath(callingMethod, callingMethod, identifier);
TestDestinationDirectories.Add(testDestinationDirectory);
- var testAsset = CreateTestProjectsInDirectory(testProjects, testDestinationDirectory, targetExtension);
+ var testAsset = CreateTestProjectsInDirectory(testProjects, testDestinationDirectory);
var slnCreationResult = new DotnetNewCommand(Log, "sln")
.WithVirtualHive()
@@ -122,8 +119,7 @@ public TestAsset CreateTestProjects(
private TestAsset CreateTestProjectsInDirectory(
IEnumerable testProjects,
- string testDestinationDirectory,
- string targetExtension = ".csproj")
+ string testDestinationDirectory)
{
var testAsset = new TestAsset(testDestinationDirectory, TestContext.Current.SdkVersion, Log);
@@ -135,15 +131,12 @@ private TestAsset CreateTestProjectsInDirectory(
var project = projectStack.Pop();
if (!createdProjects.Contains(project))
{
- project.Create(testAsset, TestAssetsRoot, targetExtension);
+ project.Create(testAsset, TestAssetsRoot);
createdProjects.Add(project);
foreach (var referencedProject in project.ReferencedProjects)
{
- if(referencedProject is not null)
- {
- projectStack.Push(referencedProject);
- }
+ projectStack.Push(referencedProject);
}
}
}
diff --git a/test/Microsoft.NET.TestFramework/ToolsetInfo.cs b/test/Microsoft.NET.TestFramework/ToolsetInfo.cs
index 6290312c17d1..f1ef4c287e1d 100644
--- a/test/Microsoft.NET.TestFramework/ToolsetInfo.cs
+++ b/test/Microsoft.NET.TestFramework/ToolsetInfo.cs
@@ -10,6 +10,7 @@ public class ToolsetInfo
{
public const string CurrentTargetFramework = "net10.0";
public const string CurrentTargetFrameworkVersion = "10.0";
+ public const string CurrentTargetFrameworkMoniker = ".NETCoreApp,Version=v" + CurrentTargetFrameworkVersion;
public const string NextTargetFramework = "net11.0";
public const string NextTargetFrameworkVersion = "11.0";
@@ -404,18 +405,19 @@ private bool UsingFullMSBuildWithoutExtensionsTargets()
.Where(a => a.Key is not null && a.Key.EndsWith("PackageVersion"))
.Select(a => (a.Key ?? string.Empty, a.Value ?? string.Empty));
- private static readonly Lazy _NewtonsoftJsonPackageVersion = new Lazy(() => GetPackageVersionProperties().Single(p => p.versionPropertyName == "NewtonsoftJsonPackageVersion").version);
-
- public static string GetNewtonsoftJsonPackageVersion()
+ public static string GetPackageVersion(string packageName)
{
- return _NewtonsoftJsonPackageVersion.Value;
+ var propertyName = packageName.Replace(".", "") + "PackageVersion";
+ return GetPackageVersionProperties().Single(p => p.versionPropertyName == propertyName).version;
}
- private static readonly Lazy _SystemDataSqlClientPackageVersion = new(() => GetPackageVersionProperties().Single(p => p.versionPropertyName == "SystemDataSqlClientPackageVersion").version);
+ private static readonly Lazy s_newtonsoftJsonPackageVersion = new(() => GetPackageVersion("Newtonsoft.Json"));
+ private static readonly Lazy s_systemDataSqlClientPackageVersion = new(() => GetPackageVersion("System.Data.SqlClient"));
+
+ public static string GetNewtonsoftJsonPackageVersion()
+ =>s_newtonsoftJsonPackageVersion.Value;
public static string GetSystemDataSqlClientPackageVersion()
- {
- return _SystemDataSqlClientPackageVersion.Value;
- }
+ => s_systemDataSqlClientPackageVersion.Value;
}
}
diff --git a/test/TestAssets/TestProjects/WatchHotReloadApp/Program.cs b/test/TestAssets/TestProjects/WatchHotReloadApp/Program.cs
index 4162119930ba..f1d2a3b77bbd 100644
--- a/test/TestAssets/TestProjects/WatchHotReloadApp/Program.cs
+++ b/test/TestAssets/TestProjects/WatchHotReloadApp/Program.cs
@@ -26,8 +26,14 @@
while (true)
{
- Console.WriteLine(".");
+ F();
await Task.Delay(1000);
}
+void F()
+{
+ Console.WriteLine(".");
+}
+
class C { /* member placeholder */ }
+
diff --git a/test/dotnet-watch.Tests/Build/EvaluationTests.cs b/test/dotnet-watch.Tests/Build/EvaluationTests.cs
new file mode 100644
index 000000000000..3d6970ec5ac9
--- /dev/null
+++ b/test/dotnet-watch.Tests/Build/EvaluationTests.cs
@@ -0,0 +1,575 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Text.RegularExpressions;
+
+namespace Microsoft.DotNet.Watch.UnitTests
+{
+ public class EvaluationTests(ITestOutputHelper output)
+ {
+ private readonly TestReporter _reporter = new(output);
+ private readonly TestAssetsManager _testAssets = new(output);
+
+ private static string MuxerPath
+ => TestContext.Current.ToolsetUnderTest.DotNetHostPath;
+
+ private static string InspectPath(string path, string rootDir)
+ => path.Substring(rootDir.Length + 1).Replace("\\", "/");
+
+ private static IEnumerable Inspect(string rootDir, IReadOnlyDictionary files)
+ => files
+ .OrderBy(entry => entry.Key)
+ .Select(entry => $"{InspectPath(entry.Key, rootDir)}: [{string.Join(", ", entry.Value.ContainingProjectPaths.Select(p => InspectPath(p, rootDir)))}]");
+
+ private static readonly string s_emptyResx = """
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ """;
+
+ private static readonly string s_emptyProgram = """
+ return 1;
+ """;
+
+ [Fact]
+ public async Task FindsCustomWatchItems()
+ {
+ var project = new TestProject("Project1")
+ {
+ IsExe = true,
+ SourceFiles =
+ {
+ {"Program.cs", s_emptyProgram},
+ {"app.js", ""},
+ {"gulpfile.js", ""},
+ },
+ EmbeddedResources =
+ {
+ {"Strings.resx", s_emptyResx}
+ }
+ };
+
+ var testAsset = _testAssets.CreateTestProject(project)
+ .WithProjectChanges(d => d.Root!.Add(XElement.Parse("""
+
+
+
+ """)));
+
+ await VerifyEvaluation(testAsset,
+ [
+ new("Project1/Project1.csproj", targetsOnly: true),
+ new("Project1/Program.cs"),
+ new("Project1/app.js"),
+ new("Project1/Strings.resx", targetsOnly: true),
+ new($"Project1/obj/Debug/{ToolsetInfo.CurrentTargetFramework}/{ToolsetInfo.CurrentTargetFrameworkMoniker}.AssemblyAttributes.cs", graphOnly: true),
+ new($"Project1/obj/Debug/{ToolsetInfo.CurrentTargetFramework}/Project1.AssemblyInfo.cs", graphOnly: true),
+ ]);
+ }
+
+ [Fact]
+ public async Task ExcludesDefaultItemsWithWatchFalseMetadata()
+ {
+ var project = new TestProject("Project1")
+ {
+ IsExe = true,
+ AdditionalProperties =
+ {
+ ["EnableDefaultEmbeddedResourceItems"] = "false",
+ ["EnableDefaultCompileItems"] = "false",
+ },
+ AdditionalItems =
+ {
+ new("EmbeddedResource", new() { { "Include", "*.resx" }, { "Watch", "false" } }),
+ new("Compile", new() { { "Include", "Program.cs" } }),
+ new("Compile", new() { { "Include", "Class*.cs" }, { "Watch", "false" } }),
+ },
+ SourceFiles =
+ {
+ {"Program.cs", s_emptyProgram},
+ {"Class1.cs", ""},
+ {"Class2.cs", ""},
+ },
+ EmbeddedResources =
+ {
+ {"Strings.resx", s_emptyResx },
+ }
+ };
+
+ var testAsset = _testAssets.CreateTestProject(project);
+
+ await VerifyEvaluation(testAsset,
+ [
+ new("Project1/Project1.csproj", targetsOnly: true),
+ new("Project1/Program.cs"),
+ new("Project1/Class1.cs", graphOnly: true),
+ new("Project1/Class2.cs", graphOnly: true),
+ new($"Project1/obj/Debug/{ToolsetInfo.CurrentTargetFramework}/{ToolsetInfo.CurrentTargetFrameworkMoniker}.AssemblyAttributes.cs", graphOnly: true),
+ new($"Project1/obj/Debug/{ToolsetInfo.CurrentTargetFramework}/Project1.AssemblyInfo.cs", graphOnly: true),
+ ]);
+ }
+
+ [Theory]
+ [CombinatorialData]
+ public async Task StaticAssets(bool isWeb, [CombinatorialValues(true, false, null)] bool? enableContentFiles)
+ {
+ var project = new TestProject("Project1")
+ {
+ ProjectSdk = isWeb ? "Microsoft.NET.Sdk.Web" : "Microsoft.NET.Sdk",
+ IsExe = true,
+ SourceFiles =
+ {
+ {"Program.cs", s_emptyProgram},
+ {"wwwroot/css/app.css", ""},
+ {"wwwroot/js/site.js", ""},
+ {"wwwroot/favicon.ico", ""},
+ },
+ AdditionalProperties =
+ {
+ ["DotNetWatchContentFiles"] = enableContentFiles?.ToString() ?? "",
+ },
+ };
+
+ var testAsset = _testAssets.CreateTestProject(project, identifier: enableContentFiles.ToString());
+
+ await VerifyEvaluation(testAsset,
+ isWeb && enableContentFiles != false ?
+ [
+ new("Project1/Project1.csproj", targetsOnly: true),
+ new("Project1/Program.cs"),
+ new("Project1/wwwroot/css/app.css", staticAssetUrl: "wwwroot/css/app.css"),
+ new("Project1/wwwroot/js/site.js", staticAssetUrl: "wwwroot/js/site.js"),
+ new("Project1/wwwroot/favicon.ico", staticAssetUrl: "wwwroot/favicon.ico"),
+ new($"Project1/obj/Debug/{ToolsetInfo.CurrentTargetFramework}/{ToolsetInfo.CurrentTargetFrameworkMoniker}.AssemblyAttributes.cs", graphOnly: true),
+ new($"Project1/obj/Debug/{ToolsetInfo.CurrentTargetFramework}/Project1.AssemblyInfo.cs", graphOnly: true),
+ ] :
+ [
+ new("Project1/Project1.csproj", targetsOnly: true),
+ new("Project1/Program.cs"),
+ new($"Project1/obj/Debug/{ToolsetInfo.CurrentTargetFramework}/{ToolsetInfo.CurrentTargetFrameworkMoniker}.AssemblyAttributes.cs", graphOnly: true),
+ new($"Project1/obj/Debug/{ToolsetInfo.CurrentTargetFramework}/Project1.AssemblyInfo.cs", graphOnly: true),
+ ]);
+ }
+
+ [Fact]
+ public async Task RazorClassLibrary()
+ {
+ var projectRcl = new TestProject("RCL")
+ {
+ ProjectSdk = "Microsoft.NET.Sdk.Razor",
+ PackageReferences =
+ {
+ new("Microsoft.AspNetCore.Components.Web", ToolsetInfo.GetPackageVersion("Microsoft.AspNetCore.App.Ref")),
+ new("Microsoft.AspNetCore.Mvc", "2.3.0"),
+ },
+ SourceFiles =
+ {
+ {"Code.cs", ""},
+ {"Page1.razor", ""},
+ {"Page1.razor.css", ""},
+ {"Page2.cshtml", "" },
+ {"Page2.cshtml.css", "" },
+ {"wwwroot/lib.css", ""},
+ {"wwwroot/lib.js", ""},
+ }
+ };
+
+ var project = new TestProject("Project1")
+ {
+ ProjectSdk = "Microsoft.NET.Sdk.Web",
+ IsExe = true,
+ ReferencedProjects = { projectRcl },
+ SourceFiles =
+ {
+ {"Program.cs", s_emptyProgram},
+ {"wwwroot/css/app.css", ""},
+ {"wwwroot/js/site.js", ""},
+ {"wwwroot/favicon.ico", ""},
+ }
+ };
+
+ var testAsset = _testAssets.CreateTestProject(project);
+
+ await VerifyEvaluation(testAsset,
+ [
+ new($"Project1/obj/Debug/{ToolsetInfo.CurrentTargetFramework}/{ToolsetInfo.CurrentTargetFrameworkMoniker}.AssemblyAttributes.cs", graphOnly: true),
+ new($"Project1/obj/Debug/{ToolsetInfo.CurrentTargetFramework}/Project1.AssemblyInfo.cs", graphOnly: true),
+ new("Project1/Program.cs"),
+ new("Project1/Project1.csproj", targetsOnly: true),
+ new("Project1/wwwroot/css/app.css", "wwwroot/css/app.css"),
+ new("Project1/wwwroot/favicon.ico", "wwwroot/favicon.ico"),
+ new("Project1/wwwroot/js/site.js", "wwwroot/js/site.js"),
+ new("RCL/Code.cs"),
+ new($"RCL/obj/Debug/{ToolsetInfo.CurrentTargetFramework}/{ToolsetInfo.CurrentTargetFrameworkMoniker}.AssemblyAttributes.cs", graphOnly: true),
+ new($"RCL/obj/Debug/{ToolsetInfo.CurrentTargetFramework}/RCL.AssemblyInfo.cs", graphOnly: true),
+ new("RCL/Page1.razor"),
+ new("RCL/Page1.razor.css"),
+ new("RCL/Page2.cshtml"),
+ new("RCL/Page2.cshtml.css"),
+ new("RCL/RCL.csproj", targetsOnly: true),
+ new("RCL/wwwroot/lib.css", "wwwroot/lib.css"),
+ new("RCL/wwwroot/lib.js", "wwwroot/lib.js"),
+ ]);
+ }
+
+ [Fact]
+ public async Task ProjectReferences_OneLevel()
+ {
+ var project2 = new TestProject("Project2")
+ {
+ TargetFrameworks = "netstandard2.0",
+ };
+
+ var project1 = new TestProject("Project1")
+ {
+ TargetFrameworks = $"{ToolsetInfo.CurrentTargetFramework};net462",
+ ReferencedProjects = { project2 },
+ };
+
+ var testAsset = _testAssets.CreateTestProject(project1);
+
+ await VerifyEvaluation(testAsset, targetFramework: ToolsetInfo.CurrentTargetFramework,
+ [
+ new($"Project1/obj/Debug/{ToolsetInfo.CurrentTargetFramework}/{ToolsetInfo.CurrentTargetFrameworkMoniker}.AssemblyAttributes.cs", graphOnly: true),
+ new($"Project1/obj/Debug/{ToolsetInfo.CurrentTargetFramework}/Project1.AssemblyInfo.cs", graphOnly: true),
+ new("Project1/Project1.csproj", targetsOnly: true),
+ new("Project1/Project1.cs"),
+ new($"Project2/obj/Debug/netstandard2.0/.NETStandard,Version=v2.0.AssemblyAttributes.cs", graphOnly: true),
+ new($"Project2/obj/Debug/netstandard2.0/Project2.AssemblyInfo.cs", graphOnly: true),
+ new("Project2/Project2.csproj", targetsOnly: true),
+ new("Project2/Project2.cs"),
+ ]);
+ }
+
+ [Fact]
+ public async Task TransitiveProjectReferences_TwoLevels()
+ {
+ var project3 = new TestProject("Project3")
+ {
+ TargetFrameworks = "netstandard2.0",
+ };
+
+ var project2 = new TestProject("Project2")
+ {
+ TargetFrameworks = "netstandard2.0",
+ ReferencedProjects = { project3 },
+ };
+
+ var project1 = new TestProject("Project1")
+ {
+ TargetFrameworks = $"{ToolsetInfo.CurrentTargetFramework};net462",
+ ReferencedProjects = { project2 },
+ };
+
+ var testAsset = _testAssets.CreateTestProject(project1);
+
+ await VerifyEvaluation(testAsset, targetFramework: ToolsetInfo.CurrentTargetFramework,
+ [
+ new($"Project3/obj/Debug/netstandard2.0/.NETStandard,Version=v2.0.AssemblyAttributes.cs", graphOnly: true),
+ new($"Project3/obj/Debug/netstandard2.0/Project3.AssemblyInfo.cs", graphOnly: true),
+ new("Project3/Project3.csproj", targetsOnly: true),
+ new("Project3/Project3.cs"),
+ new($"Project2/obj/Debug/netstandard2.0/.NETStandard,Version=v2.0.AssemblyAttributes.cs", graphOnly: true),
+ new($"Project2/obj/Debug/netstandard2.0/Project2.AssemblyInfo.cs", graphOnly: true),
+ new("Project2/Project2.csproj", targetsOnly: true),
+ new("Project2/Project2.cs"),
+ new($"Project1/obj/Debug/{ToolsetInfo.CurrentTargetFramework}/{ToolsetInfo.CurrentTargetFrameworkMoniker}.AssemblyAttributes.cs", graphOnly: true),
+ new($"Project1/obj/Debug/{ToolsetInfo.CurrentTargetFramework}/Project1.AssemblyInfo.cs", graphOnly: true),
+ new("Project1/Project1.csproj", targetsOnly: true),
+ new("Project1/Project1.cs"),
+ ]);
+ }
+
+ [Theory]
+ [CombinatorialData]
+ public async Task SingleTargetRoot_MultiTargetedDependency(bool specifyTargetFramework)
+ {
+ var project2 = new TestProject("Project2")
+ {
+ TargetFrameworks = $"{ToolsetInfo.CurrentTargetFramework};net462",
+ };
+
+ var project1 = new TestProject("Project1")
+ {
+ TargetFrameworks = ToolsetInfo.CurrentTargetFramework,
+ ReferencedProjects = { project2 },
+ };
+
+ var testAsset = _testAssets.CreateTestProject(project1, identifier: specifyTargetFramework.ToString());
+
+ await VerifyEvaluation(testAsset, specifyTargetFramework ? ToolsetInfo.CurrentTargetFramework : null,
+ [
+ new($"Project1/obj/Debug/{ToolsetInfo.CurrentTargetFramework}/{ToolsetInfo.CurrentTargetFrameworkMoniker}.AssemblyAttributes.cs", graphOnly: true),
+ new($"Project1/obj/Debug/{ToolsetInfo.CurrentTargetFramework}/Project1.AssemblyInfo.cs", graphOnly: true),
+ new("Project1/Project1.csproj", targetsOnly: true),
+ new("Project1/Project1.cs"),
+ new($"Project2/obj/Debug/{ToolsetInfo.CurrentTargetFramework}/{ToolsetInfo.CurrentTargetFrameworkMoniker}.AssemblyAttributes.cs", graphOnly: true),
+ new($"Project2/obj/Debug/{ToolsetInfo.CurrentTargetFramework}/Project2.AssemblyInfo.cs", graphOnly: true),
+ new($"Project2/obj/Debug/net462/.NETFramework,Version=v4.6.2.AssemblyAttributes.cs", graphOnly: true),
+ new($"Project2/obj/Debug/net462/Project2.AssemblyInfo.cs", graphOnly: true),
+ new("Project2/Project2.csproj", targetsOnly: true),
+ new("Project2/Project2.cs"),
+ ]);
+ }
+
+ [Fact]
+ public async Task FSharpProjectDependency()
+ {
+ var projectFS = new TestProject("FSProj")
+ {
+ TargetExtension = ".fsproj",
+ AdditionalItems =
+ {
+ new("Compile", new() { { "Include", "Lib.fs" } })
+ },
+ SourceFiles =
+ {
+ { "Lib.fs", "module Lib" }
+ }
+ };
+
+ var projectCS = new TestProject("CSProj")
+ {
+ ReferencedProjects = { projectFS },
+ TargetExtension = ".csproj",
+ IsExe = true,
+ SourceFiles =
+ {
+ { "Program.cs", s_emptyProgram },
+ },
+ };
+
+ var testAsset = _testAssets.CreateTestProject(projectCS);
+
+ await VerifyEvaluation(testAsset,
+ [
+ new("CSProj/Program.cs"),
+ new($"CSProj/obj/Debug/{ToolsetInfo.CurrentTargetFramework}/{ToolsetInfo.CurrentTargetFrameworkMoniker}.AssemblyAttributes.cs", graphOnly: true),
+ new($"CSProj/obj/Debug/{ToolsetInfo.CurrentTargetFramework}/CSProj.AssemblyInfo.cs", graphOnly: true),
+ new("CSProj/CSProj.csproj", targetsOnly: true),
+ new("FSProj/FSProj.fsproj", targetsOnly: true),
+ new("FSProj/Lib.fs"),
+ new($"FSProj/obj/Debug/{ToolsetInfo.CurrentTargetFramework}/{ToolsetInfo.CurrentTargetFrameworkMoniker}.AssemblyAttributes.fs", graphOnly: true),
+ new($"FSProj/obj/Debug/{ToolsetInfo.CurrentTargetFramework}/FSProj.AssemblyInfo.fs", graphOnly: true),
+ ]);
+ }
+
+ [Fact]
+ public async Task VBProjectDependency()
+ {
+ var projectVB = new TestProject("VB")
+ {
+ TargetExtension = ".vbproj",
+ SourceFiles =
+ {
+ { "Lib.vb", """
+ Class C
+ End Class
+ """
+ }
+ }
+ };
+
+ var projectCS = new TestProject("CS")
+ {
+ ReferencedProjects = { projectVB },
+ TargetExtension = ".csproj",
+ IsExe = true,
+ SourceFiles =
+ {
+ { "Program.cs", s_emptyProgram },
+ },
+ };
+
+ var testAsset = _testAssets.CreateTestProject(projectCS);
+
+ await VerifyEvaluation(testAsset,
+ [
+ new("CS/Program.cs"),
+ new($"CS/obj/Debug/{ToolsetInfo.CurrentTargetFramework}/{ToolsetInfo.CurrentTargetFrameworkMoniker}.AssemblyAttributes.cs", graphOnly: true),
+ new($"CS/obj/Debug/{ToolsetInfo.CurrentTargetFramework}/CS.AssemblyInfo.cs", graphOnly: true),
+ new("CS/CS.csproj", targetsOnly: true),
+ new("VB/VB.vbproj", targetsOnly: true),
+ new("VB/Lib.vb"),
+ new($"VB/obj/Debug/{ToolsetInfo.CurrentTargetFramework}/{ToolsetInfo.CurrentTargetFrameworkMoniker}.AssemblyAttributes.vb", graphOnly: true),
+ new($"VB/obj/Debug/{ToolsetInfo.CurrentTargetFramework}/VB.AssemblyInfo.vb", graphOnly: true),
+ ]);
+ }
+
+ [Fact]
+ public async Task ProjectReferences_Graph()
+ {
+ // A->B,F,W(Watch=False)
+ // B->C,E
+ // C->D
+ // D->E
+ // F->E,G
+ // G->E
+ // W->U
+ // Y->B,F,Z
+ var testDirectory = _testAssets.CopyTestAsset("ProjectReferences_Graph")
+ .WithSource()
+ .Path;
+ var projectA = Path.Combine(testDirectory, "A", "A.csproj");
+
+ var options = TestOptions.GetEnvironmentOptions(workingDirectory: testDirectory, muxerPath: MuxerPath);
+ var processRunner = new ProcessRunner(options.ProcessCleanupTimeout, CancellationToken.None);
+ var buildReporter = new BuildReporter(_reporter, options);
+
+ var filesetFactory = new MSBuildFileSetFactory(projectA, buildArguments: ["/p:_DotNetWatchTraceOutput=true"], processRunner, buildReporter);
+
+ var result = await filesetFactory.TryCreateAsync(requireProjectGraph: null, CancellationToken.None);
+ Assert.NotNull(result);
+
+ AssertEx.SequenceEqual(
+ [
+ "A/A.cs: [A/A.csproj]",
+ "A/A.csproj: [A/A.csproj]",
+ "B/B.cs: [B/B.csproj]",
+ "B/B.csproj: [B/B.csproj]",
+ "C/C.cs: [C/C.csproj]",
+ "C/C.csproj: [C/C.csproj]",
+ "Common.cs: [A/A.csproj, G/G.csproj]",
+ "D/D.cs: [D/D.csproj]",
+ "D/D.csproj: [D/D.csproj]",
+ "E/E.cs: [E/E.csproj]",
+ "E/E.csproj: [E/E.csproj]",
+ "F/F.cs: [F/F.csproj]",
+ "F/F.csproj: [F/F.csproj]",
+ "G/G.cs: [G/G.csproj]",
+ "G/G.csproj: [G/G.csproj]",
+ ], Inspect(testDirectory, result.Files));
+
+ // ensure each project is only visited once for collecting watch items
+ AssertEx.SequenceEqual(
+ [
+ "Collecting watch items from 'A'",
+ "Collecting watch items from 'B'",
+ "Collecting watch items from 'C'",
+ "Collecting watch items from 'D'",
+ "Collecting watch items from 'E'",
+ "Collecting watch items from 'F'",
+ "Collecting watch items from 'G'",
+ ],
+ _reporter.Messages.Where(l => l.text.Contains("Collecting watch items from")).Select(l => l.text.Trim()).Order());
+ }
+
+ [Fact]
+ public async Task MsbuildOutput()
+ {
+ var project2 = new TestProject("Project2")
+ {
+ TargetFrameworks = "netstandard2.1",
+ };
+
+ var project1 = new TestProject("Project1")
+ {
+ TargetFrameworks = "net462",
+ ReferencedProjects = { project2 },
+ };
+
+ var testAsset = _testAssets.CreateTestProject(project1);
+ var project1Path = GetTestProjectPath(testAsset);
+
+ var options = TestOptions.GetEnvironmentOptions(workingDirectory: Path.GetDirectoryName(project1Path)!, muxerPath: MuxerPath);
+ var processRunner = new ProcessRunner(options.ProcessCleanupTimeout, CancellationToken.None);
+ var buildReporter = new BuildReporter(_reporter, options);
+
+ var factory = new MSBuildFileSetFactory(project1Path, buildArguments: [], processRunner, buildReporter);
+ var result = await factory.TryCreateAsync(requireProjectGraph: null, CancellationToken.None);
+ Assert.Null(result);
+
+ // note: msbuild prints errors to stdout, we match the pattern and report as error:
+ AssertEx.Equal(
+ (MessageSeverity.Error, $"{project1Path} : error NU1201: Project Project2 is not compatible with net462 (.NETFramework,Version=v4.6.2). Project Project2 supports: netstandard2.1 (.NETStandard,Version=v2.1)"),
+ _reporter.Messages.Single(l => l.text.Contains("error NU1201")));
+ }
+
+ private readonly struct ExpectedFile(string path, string? staticAssetUrl = null, bool targetsOnly = false, bool graphOnly = false)
+ {
+ public string Path { get; } = path;
+ public string? StaticAssetUrl { get; } = staticAssetUrl;
+ public bool TargetsOnly { get; } = targetsOnly;
+ public bool GraphOnly { get; } = graphOnly;
+ }
+
+ private Task VerifyEvaluation(TestAsset testAsset, ExpectedFile[] expectedFiles)
+ => VerifyEvaluation(testAsset, targetFramework: null, expectedFiles);
+
+ private async Task VerifyEvaluation(TestAsset testAsset, string? targetFramework, ExpectedFile[] expectedFiles)
+ {
+ var testDir = testAsset.Path;
+ var rootProjectPath = GetTestProjectPath(testAsset);
+
+ output.WriteLine("=== Evaluate using target ===");
+ await VerifyTargetsEvaluation();
+
+ output.WriteLine("=== Evaluate using project graph ===");
+ await VerifyProjectGraphEvaluation();
+
+ async Task VerifyTargetsEvaluation()
+ {
+ var options = TestOptions.GetEnvironmentOptions(workingDirectory: testDir, muxerPath: MuxerPath) with { TestOutput = testDir };
+ var processRunner = new ProcessRunner(options.ProcessCleanupTimeout, CancellationToken.None);
+ var buildArguments = targetFramework != null ? new[] { "/p:TargetFramework=" + targetFramework } : [];
+ var buildReporter = new BuildReporter(_reporter, options);
+ var factory = new MSBuildFileSetFactory(rootProjectPath, buildArguments, processRunner, buildReporter);
+ var targetsResult = await factory.TryCreateAsync(requireProjectGraph: null, CancellationToken.None);
+ Assert.NotNull(targetsResult);
+
+ var normalizedActual = Inspect(targetsResult.Files);
+ var normalizedExpected = expectedFiles.Where(f => !f.GraphOnly).Select(f => (f.Path, f.StaticAssetUrl)).OrderBy(f => f.Path);
+ AssertEx.SequenceEqual(normalizedExpected, normalizedActual);
+ }
+
+ async Task VerifyProjectGraphEvaluation()
+ {
+ // Needs to be executed in dotnet-watch process in order for msbuild to load from the correct location.
+
+ using var watchableApp = new WatchableApp(new DebugTestOutputLogger(output));
+ var arguments = targetFramework != null ? new[] { "-f", targetFramework } : [];
+ watchableApp.Start(testAsset, arguments, relativeProjectDirectory: testAsset.TestProject!.Name);
+ await watchableApp.AssertOutputLineStartsWith(MessageDescriptor.WaitingForFileChangeBeforeRestarting, failure: _ => false);
+
+ var normalizedActual = ParseOutput(watchableApp.Process.Output).OrderBy(f => f.relativePath);
+ var normalizedExpected = expectedFiles.Where(f => !f.TargetsOnly).Select(f => (f.Path, f.StaticAssetUrl)).OrderBy(f => f.Path);
+
+ AssertEx.SequenceEqual(normalizedExpected, normalizedActual);
+ }
+
+ string GetRelativePath(string fullPath)
+ => Path.GetRelativePath(testDir, fullPath).Replace('\\', '/');
+
+ IEnumerable<(string relativePath, string? staticAssetUrl)> Inspect(IReadOnlyDictionary files)
+ => files.Select(f => (relativePath: GetRelativePath(f.Key), staticAssetUrl: f.Value.StaticWebAssetPath)).OrderBy(f => f.relativePath);
+
+ IEnumerable<(string relativePath, string? staticAssetUrl)> ParseOutput(IEnumerable output)
+ {
+ foreach (var line in output.SkipWhile(l => !Regex.IsMatch(l, "dotnet watch ⌚ Watching ([0-9]+) file[(]s[)] for changes")).Skip(1))
+ {
+ var match = Regex.Match(line, $"> ([^{Path.PathSeparator}]*)({Path.PathSeparator}(.*))?");
+ if (!match.Success)
+ {
+ break;
+ }
+
+ yield return (GetRelativePath(match.Groups[1].Value), match.Groups[3].Value is { Length: > 0 } value ? value : null);
+ }
+ }
+ }
+
+ private static string GetTestProjectPath(TestAsset projectAsset)
+ => Path.Combine(projectAsset.Path, projectAsset.TestProject!.Name!, projectAsset.TestProject!.Name + ".csproj");
+ }
+}
diff --git a/test/dotnet-watch.Tests/Build/MsBuildFileSetFactoryTest.cs b/test/dotnet-watch.Tests/Build/MsBuildFileSetFactoryTest.cs
deleted file mode 100644
index 0088f35d5db2..000000000000
--- a/test/dotnet-watch.Tests/Build/MsBuildFileSetFactoryTest.cs
+++ /dev/null
@@ -1,432 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace Microsoft.DotNet.Watch.UnitTests
-{
- public class MsBuildFileSetFactoryTest(ITestOutputHelper output)
- {
- private readonly TestReporter _reporter = new(output);
- private readonly TestAssetsManager _testAssets = new(output);
-
- private static string MuxerPath
- => TestContext.Current.ToolsetUnderTest.DotNetHostPath;
-
- private static string InspectPath(string path, string rootDir)
- => path.Substring(rootDir.Length + 1).Replace("\\", "/");
-
- private static IEnumerable Inspect(string rootDir, IReadOnlyDictionary files)
- => files
- .OrderBy(entry => entry.Key)
- .Select(entry => $"{InspectPath(entry.Key, rootDir)}: [{string.Join(", ", entry.Value.ContainingProjectPaths.Select(p => InspectPath(p, rootDir)))}]");
-
- [Fact]
- public async Task FindsCustomWatchItems()
- {
- var project = _testAssets.CreateTestProject(new TestProject("Project1")
- {
- TargetFrameworks = ToolsetInfo.CurrentTargetFramework,
- });
-
- project.WithProjectChanges(d => d.Root!.Add(XElement.Parse(
-@"
-
-")));
-
- WriteFile(project, "Program.cs");
- WriteFile(project, "app.js");
- WriteFile(project, "gulpfile.js");
-
- var result = await Evaluate(project);
-
- AssertEx.EqualFileList(
- GetTestProjectDirectory(project),
- new[]
- {
- "Project1.csproj",
- "Project1.cs",
- "Program.cs",
- "app.js"
- },
- result.Files.Keys
- );
- }
-
- [Fact]
- public async Task ExcludesDefaultItemsWithWatchFalseMetadata()
- {
- var project = _testAssets.CreateTestProject(new TestProject("Project1")
- {
- TargetFrameworks = "net40",
- AdditionalProperties =
- {
- ["EnableDefaultEmbeddedResourceItems"] = "false",
- },
- });
-
- project.WithProjectChanges(d => d.Root!.Add(XElement.Parse(
-@"
-
-")));
-
- WriteFile(project, "Program.cs");
- WriteFile(project, "Strings.resx");
-
- var result = await Evaluate(project);
-
- AssertEx.EqualFileList(
- GetTestProjectDirectory(project),
- new[]
- {
- "Project1.csproj",
- "Project1.cs",
- "Program.cs",
- },
- result.Files.Keys
- );
- }
-
- [Fact]
- public async Task SingleTfm()
- {
- var project = _testAssets.CreateTestProject(new TestProject("Project1")
- {
- TargetFrameworks = ToolsetInfo.CurrentTargetFramework,
- AdditionalProperties =
- {
- ["BaseIntermediateOutputPath"] = "obj",
- },
- });
-
- WriteFile(project, "Program.cs");
- WriteFile(project, "Class1.cs");
- WriteFile(project, Path.Combine("obj", "Class1.cs"));
- WriteFile(project, Path.Combine("Properties", "Strings.resx"));
-
- var result = await Evaluate(project);
-
- AssertEx.EqualFileList(
- GetTestProjectDirectory(project),
- new[]
- {
- "Project1.csproj",
- "Project1.cs",
- "Program.cs",
- "Class1.cs",
- "Properties/Strings.resx",
- },
- result.Files.Keys
- );
- }
-
- [Fact]
- public async Task MultiTfm()
- {
- var project = _testAssets.CreateTestProject(new TestProject("Project1")
- {
- TargetFrameworks = $"{ToolsetInfo.CurrentTargetFramework};net462",
- AdditionalProperties =
- {
- ["EnableDefaultCompileItems"] = "false",
- },
- });
-
- project.WithProjectChanges(d => d.Root!.Add(XElement.Parse(
-$@"
-
-
-")));
-
- WriteFile(project, "Class1.netcore.cs");
- WriteFile(project, "Class1.desktop.cs");
- WriteFile(project, "Class1.notincluded.cs");
-
- var result = await Evaluate(project);
-
- AssertEx.EqualFileList(
- GetTestProjectDirectory(project),
- new[]
- {
- "Project1.csproj",
- "Class1.netcore.cs",
- "Class1.desktop.cs",
- },
- result.Files.Keys
- );
- }
-
- [Fact]
- public async Task IncludesContentFiles()
- {
- var testDir = _testAssets.CreateTestDirectory();
-
- var project = WriteFile(testDir, Path.Combine("Project1.csproj"),
-@"
-
- netstandard2.1
-
-");
- WriteFile(testDir, Path.Combine("Program.cs"));
-
- WriteFile(testDir, Path.Combine("wwwroot", "css", "app.css"));
- WriteFile(testDir, Path.Combine("wwwroot", "js", "site.js"));
- WriteFile(testDir, Path.Combine("wwwroot", "favicon.ico"));
-
- var result = await Evaluate(project);
-
- AssertEx.EqualFileList(
- testDir.Path,
- new[]
- {
- "Project1.csproj",
- "Program.cs",
- "wwwroot/css/app.css",
- "wwwroot/js/site.js",
- "wwwroot/favicon.ico",
- },
- result.Files.Keys
- );
- }
-
- [Fact]
- public async Task IncludesContentFilesFromRCL()
- {
- var testDir = _testAssets.CreateTestDirectory();
- WriteFile(
- testDir,
- Path.Combine("RCL1", "RCL1.csproj"),
- $"""
-
-
- netstandard2.1
-
-
- """);
-
- WriteFile(testDir, Path.Combine("RCL1", "wwwroot", "css", "app.css"));
- WriteFile(testDir, Path.Combine("RCL1", "wwwroot", "js", "site.js"));
- WriteFile(testDir, Path.Combine("RCL1", "wwwroot", "favicon.ico"));
-
- var projectPath = WriteFile(
- testDir,
- Path.Combine("Project1", "Project1.csproj"),
- """
-
-
- netstandard2.1
-
-
-
-
-
- """);
-
- WriteFile(testDir, Path.Combine("Project1", "Program.cs"));
-
- var result = await Evaluate(projectPath);
-
- AssertEx.EqualFileList(
- testDir.Path,
- new[]
- {
- "Project1/Project1.csproj",
- "Project1/Program.cs",
- "RCL1/RCL1.csproj",
- "RCL1/wwwroot/css/app.css",
- "RCL1/wwwroot/js/site.js",
- "RCL1/wwwroot/favicon.ico",
- },
- result.Files.Keys
- );
- }
-
- [Fact]
- public async Task ProjectReferences_OneLevel()
- {
- var project2 = _testAssets.CreateTestProject(new TestProject("Project2")
- {
- TargetFrameworks = "netstandard2.0",
- });
-
- var project1 = _testAssets.CreateTestProject(new TestProject("Project1")
- {
- TargetFrameworks = $"{ToolsetInfo.CurrentTargetFramework};net462",
- ReferencedProjects = { project2.TestProject, },
- });
-
- var result = await Evaluate(project1);
-
- AssertEx.EqualFileList(
- project1.TestRoot,
- new[]
- {
- "Project2/Project2.csproj",
- "Project2/Project2.cs",
- "Project1/Project1.csproj",
- "Project1/Project1.cs",
- },
- result.Files.Keys
- );
- }
-
- [Fact]
- public async Task TransitiveProjectReferences_TwoLevels()
- {
- var project3 = _testAssets.CreateTestProject(new TestProject("Project3")
- {
- TargetFrameworks = "netstandard2.0",
- });
-
- var project2 = _testAssets.CreateTestProject(new TestProject("Project2")
- {
- TargetFrameworks = "netstandard2.0",
- ReferencedProjects = { project3.TestProject },
- });
-
- var project1 = _testAssets.CreateTestProject(new TestProject("Project1")
- {
- TargetFrameworks = $"{ToolsetInfo.CurrentTargetFramework};net462",
- ReferencedProjects = { project2.TestProject },
- });
-
- var result = await Evaluate(project1);
-
- AssertEx.EqualFileList(
- project1.TestRoot,
- new[]
- {
- "Project3/Project3.csproj",
- "Project3/Project3.cs",
- "Project2/Project2.csproj",
- "Project2/Project2.cs",
- "Project1/Project1.csproj",
- "Project1/Project1.cs",
- },
- result.Files.Keys
- );
-
- Assert.All(result.Files.Values, f => Assert.False(f.IsStaticFile, $"File {f.FilePath} should not be a static file."));
- }
-
- [Fact]
- public async Task ProjectReferences_Graph()
- {
- // A->B,F,W(Watch=False)
- // B->C,E
- // C->D
- // D->E
- // F->E,G
- // G->E
- // W->U
- // Y->B,F,Z
- var testDirectory = _testAssets.CopyTestAsset("ProjectReferences_Graph")
- .WithSource()
- .Path;
- var projectA = Path.Combine(testDirectory, "A", "A.csproj");
-
- var options = TestOptions.GetEnvironmentOptions(workingDirectory: testDirectory, muxerPath: MuxerPath);
- var processRunner = new ProcessRunner(options.ProcessCleanupTimeout, CancellationToken.None);
-
- var filesetFactory = new MSBuildFileSetFactory(projectA, buildArguments: ["/p:_DotNetWatchTraceOutput=true"], options, processRunner, _reporter);
-
- var result = await filesetFactory.TryCreateAsync(requireProjectGraph: null, CancellationToken.None);
- Assert.NotNull(result);
-
- AssertEx.SequenceEqual(
- [
- "A/A.cs: [A/A.csproj]",
- "A/A.csproj: [A/A.csproj]",
- "B/B.cs: [B/B.csproj]",
- "B/B.csproj: [B/B.csproj]",
- "C/C.cs: [C/C.csproj]",
- "C/C.csproj: [C/C.csproj]",
- "Common.cs: [A/A.csproj, G/G.csproj]",
- "D/D.cs: [D/D.csproj]",
- "D/D.csproj: [D/D.csproj]",
- "E/E.cs: [E/E.csproj]",
- "E/E.csproj: [E/E.csproj]",
- "F/F.cs: [F/F.csproj]",
- "F/F.csproj: [F/F.csproj]",
- "G/G.cs: [G/G.csproj]",
- "G/G.csproj: [G/G.csproj]",
- ], Inspect(testDirectory, result.Files));
-
- // ensure each project is only visited once for collecting watch items
- AssertEx.SequenceEqual(
- [
- "Collecting watch items from 'A'",
- "Collecting watch items from 'B'",
- "Collecting watch items from 'C'",
- "Collecting watch items from 'D'",
- "Collecting watch items from 'E'",
- "Collecting watch items from 'F'",
- "Collecting watch items from 'G'",
- ],
- _reporter.Messages.Where(l => l.text.Contains("Collecting watch items from")).Select(l => l.text.Trim()).Order());
- }
-
- [Fact]
- public async Task MsbuildOutput()
- {
- var project2 = _testAssets.CreateTestProject(new TestProject("Project2")
- {
- TargetFrameworks = "netstandard2.1",
- });
-
- var project1 = _testAssets.CreateTestProject(new TestProject("Project1")
- {
- TargetFrameworks = $"net462",
- ReferencedProjects = { project2.TestProject, },
- });
-
- var project1Path = GetTestProjectPath(project1);
-
- var options = TestOptions.GetEnvironmentOptions(workingDirectory: Path.GetDirectoryName(project1Path)!, muxerPath: MuxerPath);
- var processRunner = new ProcessRunner(options.ProcessCleanupTimeout, CancellationToken.None);
-
- var factory = new MSBuildFileSetFactory(project1Path, buildArguments: [], options, processRunner, _reporter);
- var result = await factory.TryCreateAsync(requireProjectGraph: null, CancellationToken.None);
- Assert.Null(result);
-
- // note: msbuild prints errors to stdout, we match the pattern and report as error:
- AssertEx.Equal(
- (MessageSeverity.Error, $"{project1Path} : error NU1201: Project Project2 is not compatible with net462 (.NETFramework,Version=v4.6.2). Project Project2 supports: netstandard2.1 (.NETStandard,Version=v2.1)"),
- _reporter.Messages.Single(l => l.text.Contains("error NU1201")));
- }
-
- private Task Evaluate(TestAsset projectPath)
- => Evaluate(GetTestProjectPath(projectPath));
-
- private async Task Evaluate(string projectPath)
- {
- var options = TestOptions.GetEnvironmentOptions(workingDirectory: Path.GetDirectoryName(projectPath)!, muxerPath: MuxerPath);
- var processRunner = new ProcessRunner(options.ProcessCleanupTimeout, CancellationToken.None);
- var factory = new MSBuildFileSetFactory(projectPath, buildArguments: [], options, processRunner, _reporter);
- var result = await factory.TryCreateAsync(requireProjectGraph: null, CancellationToken.None);
- Assert.NotNull(result);
- return result;
- }
-
- private static string GetTestProjectPath(TestAsset target) => Path.Combine(GetTestProjectDirectory(target), target.TestProject?.Name + ".csproj");
-
- private static string WriteFile(TestAsset testAsset, string name, string contents = "")
- {
- var path = Path.Combine(GetTestProjectDirectory(testAsset), name);
- Directory.CreateDirectory(Path.GetDirectoryName(path)!);
- File.WriteAllText(path, contents);
-
- return path;
- }
-
- private static string WriteFile(TestDirectory testAsset, string name, string contents = "")
- {
- var path = Path.Combine(testAsset.Path, name);
- Directory.CreateDirectory(Path.GetDirectoryName(path)!);
- File.WriteAllText(path, contents);
-
- return path;
- }
-
- private static string GetTestProjectDirectory(TestAsset testAsset)
- => Path.Combine(testAsset.Path, testAsset.TestProject?.Name ?? string.Empty);
- }
-}
diff --git a/test/dotnet-watch.Tests/FileWatcher/FileWatcherTests.cs b/test/dotnet-watch.Tests/FileWatcher/FileWatcherTests.cs
index 980a19373dcb..2da6526e5bf2 100644
--- a/test/dotnet-watch.Tests/FileWatcher/FileWatcherTests.cs
+++ b/test/dotnet-watch.Tests/FileWatcher/FileWatcherTests.cs
@@ -70,7 +70,8 @@ private async Task TestOperation(
AssertEx.SequenceEqual(expectedChanges, filesChanged.OrderBy(x => x.Path));
}
- private sealed class TestFileWatcher(IReporter reporter) : FileWatcher(reporter)
+ private sealed class TestFileWatcher(IReporter reporter)
+ : FileWatcher(reporter, TestOptions.GetEnvironmentOptions())
{
public IReadOnlyDictionary DirectoryTreeWatchers => _directoryTreeWatchers;
public IReadOnlyDictionary DirectoryWatchers => _directoryWatchers;
diff --git a/test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs b/test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs
index d366a921bc6e..95c258b65945 100644
--- a/test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs
+++ b/test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs
@@ -310,7 +310,7 @@ public async Task DefaultItemExcludes_DefaultItemsDisabled()
await App.WaitUntilOutputContains($"dotnet watch ⌚ Ignoring change in output directory: Add '{objDirFilePath}'");
}
- [Fact(Skip = "https://github.com/dotnet/sdk/issues/49542")]
+ [Fact]
public async Task ProjectChange_GlobalUsings()
{
var testAsset = TestAssets.CopyTestAsset("WatchHotReloadApp")
@@ -340,9 +340,9 @@ public async Task ProjectChange_GlobalUsings()
"""));
- await App.AssertOutputLineStartsWith(MessageDescriptor.HotReloadSucceeded, $"WatchNoDepsApp ({ToolsetInfo.CurrentTargetFramework})");
+ await App.AssertOutputLineStartsWith(MessageDescriptor.HotReloadSucceeded, $"WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})", failure: _ => false);
- await App.WaitUntilOutputContains(">>> System.Linq.Enumerable");
+ await App.WaitUntilOutputContains(">>> System.Xml.Linq.XDocument");
App.AssertOutputContains(MessageDescriptor.ReEvaluationCompleted);
}
@@ -380,7 +380,7 @@ public async Task AutoRestartOnRudeEdit(bool nonInteractive)
await App.AssertOutputLineStartsWith(MessageDescriptor.WaitingForChanges, failure: _ => false);
App.AssertOutputContains("⌚ Restart is needed to apply the changes");
- App.AssertOutputContains($"⌚ [auto-restart] {programPath}(33,11): error ENC0023: Adding an abstract method or overriding an inherited method requires restarting the application.");
+ App.AssertOutputContains($"⌚ [auto-restart] {programPath}(38,11): error ENC0023: Adding an abstract method or overriding an inherited method requires restarting the application.");
App.AssertOutputContains($"[WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})] Exited");
App.AssertOutputContains($"[WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})] Launched");
App.Process.ClearOutput();
@@ -410,7 +410,7 @@ public async Task AutoRestartOnRudeEditAfterRestartPrompt()
await App.AssertOutputLineStartsWith(" ❔ Do you want to restart your app? Yes (y) / No (n) / Always (a) / Never (v)", failure: _ => false);
App.AssertOutputContains("⌚ Restart is needed to apply the changes.");
- App.AssertOutputContains($"❌ {programPath}(33,11): error ENC0023: Adding an abstract method or overriding an inherited method requires restarting the application.");
+ App.AssertOutputContains($"❌ {programPath}(38,11): error ENC0023: Adding an abstract method or overriding an inherited method requires restarting the application.");
App.Process.ClearOutput();
App.SendKey('a');
@@ -427,7 +427,7 @@ public async Task AutoRestartOnRudeEditAfterRestartPrompt()
await App.AssertOutputLineStartsWith(MessageDescriptor.WaitingForChanges, failure: _ => false);
App.AssertOutputContains("⌚ Restart is needed to apply the changes");
- App.AssertOutputContains($"⌚ [auto-restart] {programPath}(33,1): error ENC0033: Deleting method 'F()' requires restarting the application.");
+ App.AssertOutputContains($"⌚ [auto-restart] {programPath}(38,1): error ENC0033: Deleting method 'F()' requires restarting the application.");
App.AssertOutputContains($"[WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})] Exited");
App.AssertOutputContains($"[WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})] Launched");
}
@@ -1009,7 +1009,7 @@ public static void PrintDirectoryName([CallerFilePathAttribute] string filePath
Directory.CreateDirectory(oldSubdir);
File.WriteAllText(Path.Combine(oldSubdir, "Foo.cs"), source);
- App.Start(testAsset, [], "AppWithDeps");
+ App.Start(testAsset, ["--non-interactive"], "AppWithDeps");
await App.AssertWaitingForChanges();
@@ -1027,7 +1027,10 @@ public static void PrintDirectoryName([CallerFilePathAttribute] string filePath
Log($"Renamed '{oldSubdir}' to '{newSubdir}'.");
- await App.AssertOutputLineStartsWith("> NewSubdir");
+ // dotnet-watch may observe the delete separately from the new file write.
+ // If so, rude edit is reported, the app is auto-restarted and we should observe the final result.
+
+ await App.AssertOutputLineStartsWith("> NewSubdir", failure: _ => false);
}
[PlatformSpecificFact(TestPlatforms.Windows)] // "https://github.com/dotnet/sdk/issues/49307")
diff --git a/test/dotnet-watch.Tests/HotReload/CompilationHandlerTests.cs b/test/dotnet-watch.Tests/HotReload/CompilationHandlerTests.cs
index 615d62766238..f2759b7a7349 100644
--- a/test/dotnet-watch.Tests/HotReload/CompilationHandlerTests.cs
+++ b/test/dotnet-watch.Tests/HotReload/CompilationHandlerTests.cs
@@ -22,14 +22,7 @@ public async Task ReferenceOutputAssembly_False()
var processRunner = new ProcessRunner(environmentOptions.ProcessCleanupTimeout, CancellationToken.None);
- var factory = new MSBuildFileSetFactory(
- rootProjectFile: options.ProjectPath,
- buildArguments: [],
- environmentOptions: environmentOptions,
- processRunner,
- reporter);
-
- var projectGraph = factory.TryLoadProjectGraph(projectGraphRequired: false, CancellationToken.None);
+ var projectGraph = ProjectGraphUtilities.TryLoadProjectGraph(options.ProjectPath, globalOptions: [], reporter, projectGraphRequired: false, CancellationToken.None);
var handler = new CompilationHandler(reporter, processRunner);
await handler.Workspace.UpdateProjectConeAsync(hostProject, CancellationToken.None);
diff --git a/test/dotnet-watch.Tests/HotReload/RuntimeProcessLauncherTests.cs b/test/dotnet-watch.Tests/HotReload/RuntimeProcessLauncherTests.cs
index 50f4bdb8687f..e0989c49bc0b 100644
--- a/test/dotnet-watch.Tests/HotReload/RuntimeProcessLauncherTests.cs
+++ b/test/dotnet-watch.Tests/HotReload/RuntimeProcessLauncherTests.cs
@@ -1,11 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System.Collections.Immutable;
using System.Runtime.CompilerServices;
namespace Microsoft.DotNet.Watch.UnitTests;
+[Collection(nameof(InProcBuildTestCollection))]
public class RuntimeProcessLauncherTests(ITestOutputHelper logger) : DotNetWatchTestBase(logger)
{
public enum TriggerEvent
@@ -636,4 +636,50 @@ public async Task IgnoredChange(bool isExisting, bool isIncluded, DirectoryKind
throw new InvalidOperationException();
}
}
+
+ [Fact]
+ public async Task ProjectAndSourceFileChange()
+ {
+ var testAsset = CopyTestAsset("WatchHotReloadApp");
+
+ var workingDirectory = testAsset.Path;
+ var projectPath = Path.Combine(testAsset.Path, "WatchHotReloadApp.csproj");
+ var programPath = Path.Combine(testAsset.Path, "Program.cs");
+
+ await using var w = StartWatcher(testAsset, [], workingDirectory, projectPath);
+
+ var fileChangesCompleted = w.CreateCompletionSource();
+ w.Watcher.Test_FileChangesCompletedTask = fileChangesCompleted.Task;
+
+ var waitingForChanges = w.Reporter.RegisterSemaphore(MessageDescriptor.WaitingForChanges);
+
+ var changeHandled = w.Reporter.RegisterSemaphore(MessageDescriptor.HotReloadChangeHandled);
+
+ var hasUpdatedOutput = w.CreateCompletionSource();
+ w.Reporter.OnProcessOutput += line =>
+ {
+ if (line.Content.Contains("System.Xml.Linq.XDocument"))
+ {
+ hasUpdatedOutput.TrySetResult();
+ }
+ };
+
+ // let the host process start:
+ Log("Waiting for changes...");
+ await waitingForChanges.WaitAsync(w.ShutdownSource.Token);
+
+ // change the project and source files at the same time:
+
+ UpdateSourceFile(programPath, src => src.Replace("""Console.WriteLine(".");""", """Console.WriteLine(typeof(XDocument));"""));
+ UpdateSourceFile(projectPath, src => src.Replace("", """"""));
+
+ // done updating files:
+ fileChangesCompleted.TrySetResult();
+
+ Log("Waiting for change handled ...");
+ await changeHandled.WaitAsync(w.ShutdownSource.Token);
+
+ Log("Waiting for output 'System.Xml.Linq.XDocument'...");
+ await hasUpdatedOutput.Task;
+ }
}
diff --git a/test/dotnet-watch.Tests/TestUtilities/InProcBuildTestCollection.cs b/test/dotnet-watch.Tests/TestUtilities/InProcBuildTestCollection.cs
new file mode 100644
index 000000000000..f0054a2e5945
--- /dev/null
+++ b/test/dotnet-watch.Tests/TestUtilities/InProcBuildTestCollection.cs
@@ -0,0 +1,13 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.DotNet.Watch.UnitTests;
+
+///
+/// All tests that validate msbuild build in-proc must be included in this collection
+/// as mutliple builds can't run in parallel in the same process.
+///
+[CollectionDefinition(nameof(InProcBuildTestCollection), DisableParallelization = true)]
+public sealed class InProcBuildTestCollection
+{
+}
diff --git a/test/dotnet-watch.Tests/TestUtilities/MockFileSetFactory.cs b/test/dotnet-watch.Tests/TestUtilities/MockFileSetFactory.cs
index 86e9e9968ba0..2c2df0ef1144 100644
--- a/test/dotnet-watch.Tests/TestUtilities/MockFileSetFactory.cs
+++ b/test/dotnet-watch.Tests/TestUtilities/MockFileSetFactory.cs
@@ -1,19 +1,16 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-#nullable disable
-
namespace Microsoft.DotNet.Watch.UnitTests;
internal class MockFileSetFactory() : MSBuildFileSetFactory(
rootProjectFile: "test.csproj",
buildArguments: [],
- TestOptions.GetEnvironmentOptions(Environment.CurrentDirectory, "dotnet") is var options ? options : options,
- new ProcessRunner(options.ProcessCleanupTimeout, CancellationToken.None),
- NullReporter.Singleton)
+ new ProcessRunner((TestOptions.GetEnvironmentOptions(Environment.CurrentDirectory, "dotnet") is var options ? options : options).ProcessCleanupTimeout, CancellationToken.None),
+ new BuildReporter(NullReporter.Singleton, options))
{
- public Func TryCreateImpl;
+ public Func? TryCreateImpl;
- public override ValueTask TryCreateAsync(bool? requireProjectGraph, CancellationToken cancellationToken)
+ public override ValueTask TryCreateAsync(bool? requireProjectGraph, CancellationToken cancellationToken)
=> ValueTask.FromResult(TryCreateImpl?.Invoke());
}
diff --git a/test/dotnet-watch.Tests/TestUtilities/MockReporter.cs b/test/dotnet-watch.Tests/TestUtilities/MockReporter.cs
index 97ad1458a95c..5bd10b40fcf5 100644
--- a/test/dotnet-watch.Tests/TestUtilities/MockReporter.cs
+++ b/test/dotnet-watch.Tests/TestUtilities/MockReporter.cs
@@ -1,8 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using Microsoft.Build.Graph;
-
namespace Microsoft.DotNet.Watch.UnitTests;
internal class MockReporter : IReporter
diff --git a/test/dotnet-watch.Tests/TestUtilities/ModuleInitializer.cs b/test/dotnet-watch.Tests/TestUtilities/ModuleInitializer.cs
new file mode 100644
index 000000000000..1248a96695a4
--- /dev/null
+++ b/test/dotnet-watch.Tests/TestUtilities/ModuleInitializer.cs
@@ -0,0 +1,59 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.Loader;
+using Microsoft.Build.Locator;
+
+namespace Microsoft.DotNet.Watch.UnitTests;
+
+public static class ModuleInitializer
+{
+ [ModuleInitializer]
+ public static void Initialize()
+ {
+ // Ensure that we load the msbuild binaries from redist deployment. Otherwise, msbuild might use target files
+ // that do not match the implementations of the core tasks.
+
+ // If this throws make sure the test assembly, or any of its dependencies copied to this project's output directory,
+ // does not have any public type that has a dependency on msbuild.
+ // xUnit loads all public types and any reference to msbuild assembly will trigger its load.
+
+ var toolset = TestContext.Current.ToolsetUnderTest;
+ var sdkDir = toolset.SdkFolderUnderTest;
+ var watchDir = Path.Combine(sdkDir, "DotnetTools", "dotnet-watch", toolset.SdkVersion, "tools", ToolsetInfo.CurrentTargetFramework, "any");
+
+ MSBuildLocator.RegisterMSBuildPath(sdkDir);
+
+ // The project references of this project are set up so that some dependencies are not copied to the output directory.
+ // These need to be resolved in one of the Redist directories.
+
+ AssemblyLoadContext.Default.Resolving += (_, name) =>
+ {
+ var path = GetPath(name, sdkDir);
+ if (!File.Exists(path))
+ {
+ path = GetPath(name, watchDir);
+ if (!File.Exists(path))
+ {
+ return null;
+ }
+ }
+
+ return AssemblyLoadContext.Default.LoadFromAssemblyPath(path);
+
+ static string GetPath(AssemblyName name, string dir)
+ {
+ var path = dir;
+ if (name.CultureName != null)
+ {
+ path = Path.Combine(path, name.CultureName);
+ }
+
+ path = Path.Combine(path, name.Name + ".dll");
+ return path;
+ }
+ };
+ }
+}
diff --git a/test/dotnet-watch.Tests/TestUtilities/TestOptions.cs b/test/dotnet-watch.Tests/TestUtilities/TestOptions.cs
index 020a7597ec2a..20362ea5b413 100644
--- a/test/dotnet-watch.Tests/TestUtilities/TestOptions.cs
+++ b/test/dotnet-watch.Tests/TestUtilities/TestOptions.cs
@@ -17,7 +17,7 @@ public static EnvironmentOptions GetEnvironmentOptions(string workingDirectory =
// 0 timeout for process cleanup in tests. We can't send Ctrl+C on Windows, so process termination must be forced.
var processCleanupTimeout = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? TimeSpan.FromSeconds(0) : TimeSpan.FromSeconds(1);
- return new(workingDirectory, muxerPath, processCleanupTimeout, TestFlags: TestFlags.RunningAsTest, TestOutput: asset != null ? GetWatchTestOutputPath(asset) : "");
+ return new(workingDirectory, muxerPath, processCleanupTimeout, IsPollingEnabled: true, TestFlags: TestFlags.RunningAsTest, TestOutput: asset != null ? GetWatchTestOutputPath(asset) : "");
}
public static CommandLineOptions GetCommandLineOptions(string[] args)
diff --git a/test/dotnet-watch.Tests/TestUtilities/WatchableApp.cs b/test/dotnet-watch.Tests/TestUtilities/WatchableApp.cs
index dcbbc27b6073..76f26275b2ae 100644
--- a/test/dotnet-watch.Tests/TestUtilities/WatchableApp.cs
+++ b/test/dotnet-watch.Tests/TestUtilities/WatchableApp.cs
@@ -113,8 +113,10 @@ public Task AssertFileChanged()
public Task AssertExiting()
=> AssertOutputLineStartsWith(ExitingMessage);
- public void Start(TestAsset asset, IEnumerable arguments, string relativeProjectDirectory = null, string workingDirectory = null, TestFlags testFlags = TestFlags.RunningAsTest)
+ public void Start(TestAsset asset, IEnumerable arguments, string relativeProjectDirectory = null, string workingDirectory = null, TestFlags testFlags = TestFlags.None)
{
+ testFlags |= TestFlags.RunningAsTest;
+
var projectDirectory = (relativeProjectDirectory != null) ? Path.Combine(asset.Path, relativeProjectDirectory) : asset.Path;
var commandSpec = new DotnetCommand(Logger, ["watch", .. DotnetWatchArgs, .. arguments])
diff --git a/test/dotnet-watch.Tests/Watch/BuildEvaluatorTests.cs b/test/dotnet-watch.Tests/Watch/BuildEvaluatorTests.cs
index 33db911fe985..72bc322c57ff 100644
--- a/test/dotnet-watch.Tests/Watch/BuildEvaluatorTests.cs
+++ b/test/dotnet-watch.Tests/Watch/BuildEvaluatorTests.cs
@@ -5,7 +5,7 @@ namespace Microsoft.DotNet.Watch.UnitTests
{
public partial class BuildEvaluatorTests
{
- private static readonly EvaluationResult s_emptyEvaluationResult = new(new Dictionary(), projectGraph: null);
+ private static readonly MSBuildFileSetFactory.EvaluationResult s_emptyEvaluationResult = new(new Dictionary(), projectGraph: null);
private static DotNetWatchContext CreateContext(bool suppressMSBuildIncrementalism = false)
{
@@ -86,7 +86,7 @@ public async Task ProcessAsync_SetsEvaluationRequired_IfMSBuildFileChanges_ButIs
// 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.
- var result = new EvaluationResult(
+ var result = new MSBuildFileSetFactory.EvaluationResult(
new Dictionary()
{
{ "Controlller.cs", new FileItem { FilePath = "Controlller.cs", ContainingProjectPaths = []} },
diff --git a/test/dotnet-watch.Tests/dotnet-watch.Tests.csproj b/test/dotnet-watch.Tests/dotnet-watch.Tests.csproj
index 0bd6266b9abf..41b346c0a555 100644
--- a/test/dotnet-watch.Tests/dotnet-watch.Tests.csproj
+++ b/test/dotnet-watch.Tests/dotnet-watch.Tests.csproj
@@ -10,10 +10,15 @@
-
-
+
+
+
+
diff --git a/test/dotnet.Tests/CommandTests/Restore/GivenThatIWantToRestoreApp.cs b/test/dotnet.Tests/CommandTests/Restore/GivenThatIWantToRestoreApp.cs
index 66699f4bfd89..22e2f89acb30 100644
--- a/test/dotnet.Tests/CommandTests/Restore/GivenThatIWantToRestoreApp.cs
+++ b/test/dotnet.Tests/CommandTests/Restore/GivenThatIWantToRestoreApp.cs
@@ -53,6 +53,7 @@ public void ItRestoresLibToSpecificDirectory(bool useStaticGraphEvaluation, stri
{
Name = "RestoreToDir",
TargetFrameworks = ToolsetInfo.CurrentTargetFramework,
+ TargetExtension = extension,
};
testProject.PackageReferences.Add(new TestPackageReference("Newtonsoft.Json", ToolsetInfo.GetNewtonsoftJsonPackageVersion()));
@@ -61,7 +62,7 @@ public void ItRestoresLibToSpecificDirectory(bool useStaticGraphEvaluation, stri
testProject.PackageReferences.Add(new TestPackageReference("FSharp.Core", "6.0.1", updatePackageReference: true));
}
- var testAsset = _testAssetsManager.CreateTestProject(testProject, identifier: useStaticGraphEvaluation.ToString() + extension, targetExtension: extension);
+ var testAsset = _testAssetsManager.CreateTestProject(testProject, identifier: useStaticGraphEvaluation.ToString() + extension);
var rootPath = Path.Combine(testAsset.TestRoot, testProject.Name);