diff --git a/src/Dotnet.Watch/Watch/AppModels/HotReloadAppModel.cs b/src/Dotnet.Watch/Watch/AppModels/HotReloadAppModel.cs index 2271f307808d..c1a4bf8104fd 100644 --- a/src/Dotnet.Watch/Watch/AppModels/HotReloadAppModel.cs +++ b/src/Dotnet.Watch/Watch/AppModels/HotReloadAppModel.cs @@ -64,39 +64,50 @@ internal static bool IsManagedAgentSupported(ProjectGraphNode project, ILogger l return false; } - // If property is not specified startup hook is enabled: - // https://github.com/dotnet/runtime/blob/4b0b7238ba021b610d3963313b4471517108d2bc/src/libraries/System.Private.CoreLib/src/System/StartupHookProvider.cs#L22 - // Startup hooks are not used for WASM projects. - // - // TODO: Remove once implemented: https://github.com/dotnet/runtime/issues/123778 - if (!project.ProjectInstance.GetBooleanPropertyValue(PropertyNames.StartupHookSupport, defaultValue: true) && - !project.GetCapabilities().Contains(ProjectCapability.WebAssembly)) - { - // Report which property is causing lack of support for startup hooks: - var (propertyName, propertyValue) = - project.ProjectInstance.GetBooleanPropertyValue(PropertyNames.PublishAot) - ? (PropertyNames.PublishAot, true) - : project.ProjectInstance.GetBooleanPropertyValue(PropertyNames.PublishTrimmed) - ? (PropertyNames.PublishTrimmed, true) - : (PropertyNames.StartupHookSupport, false); - - logger.Log(MessageDescriptor.ProjectDoesNotSupportHotReload_Property, propertyName, propertyValue.ToString(), PropertyNames.StartupHookSupport, "True"); - return false; - } - - // MetadataUpdaterSupport is the primary indicator of whether the runtime supports Hot Reload. - // It is however not correctly set prior to .NET 11, so we use Optimize and DebugSymbols instead for older frameworks. - // See https://github.com/dotnet/runtime/pull/127163 + // Since .NET 11 the SDK generates runtimeconfig.dev.json file that configures the runtime to support + // startup hooks and metadata update handlers in Debug builds. + // The file is generated when the property EnableHotReloadInRuntimeConfigDevFile is true. if (project.IsNetCoreApp(Versions.Version11_0)) { - if (!project.ProjectInstance.GetBooleanPropertyValue(PropertyNames.MetadataUpdaterSupport, defaultValue: true)) + if (!project.ProjectInstance.GetBooleanPropertyValue(PropertyNames.EnableHotReloadInRuntimeConfigDevFile, defaultValue: true)) { - logger.Log(MessageDescriptor.ProjectDoesNotSupportHotReload_Property, PropertyNames.MetadataUpdaterSupport, "False", PropertyNames.MetadataUpdaterSupport, "True"); - return false; + // If runtimeconfig.dev.json file is not generated MetadataUpdaterSupport and StartupHookSupport need to be enabled. + var metadataUpdaterSupported = project.ProjectInstance.GetBooleanPropertyValue(PropertyNames.MetadataUpdaterSupport, defaultValue: true); + if (!metadataUpdaterSupported || !StartupHookSupportedIfRequired()) + { + logger.Log( + MessageDescriptor.ProjectDoesNotSupportHotReload_Property, + // setting blocking Hot Reload: + metadataUpdaterSupported ? PropertyNames.StartupHookSupport : PropertyNames.MetadataUpdaterSupport, + "False", + // recommended setting: + PropertyNames.EnableHotReloadInRuntimeConfigDevFile, + "True"); + + return false; + } } } else { + if (!StartupHookSupportedIfRequired()) + { + // Report which property is causing lack of support for startup hooks: + var (propertyName, propertyValue) = + project.ProjectInstance.GetBooleanPropertyValue(PropertyNames.PublishAot) + ? (PropertyNames.PublishAot, true) + : project.ProjectInstance.GetBooleanPropertyValue(PropertyNames.PublishTrimmed) + ? (PropertyNames.PublishTrimmed, true) + : (PropertyNames.StartupHookSupport, false); + + logger.Log(MessageDescriptor.ProjectDoesNotSupportHotReload_Property, propertyName, propertyValue.ToString(), PropertyNames.StartupHookSupport, "True"); + return false; + } + + // Not checking MetadataUpdateSupport since it's not set correctly prior .NET 11. + // Use Optimize and DebugSymbols instead for older frameworks. + // See https://github.com/dotnet/runtime/pull/127163 + if (project.ProjectInstance.GetBooleanPropertyValue(PropertyNames.Optimize)) { logger.Log(MessageDescriptor.ProjectDoesNotSupportHotReload_Property, PropertyNames.Optimize, "True", PropertyNames.Optimize, "False"); @@ -111,5 +122,10 @@ internal static bool IsManagedAgentSupported(ProjectGraphNode project, ILogger l } return true; + + bool StartupHookSupportedIfRequired() + => project.ProjectInstance.GetBooleanPropertyValue(PropertyNames.StartupHookSupport, defaultValue: true) || + // Startup hooks are not used for WASM projects. + project.GetCapabilities().Contains(ProjectCapability.WebAssembly); } } diff --git a/src/Dotnet.Watch/Watch/Build/BuildNames.cs b/src/Dotnet.Watch/Watch/Build/BuildNames.cs index 0d294434fb45..d2cedc4644f6 100644 --- a/src/Dotnet.Watch/Watch/Build/BuildNames.cs +++ b/src/Dotnet.Watch/Watch/Build/BuildNames.cs @@ -29,6 +29,7 @@ internal static class PropertyNames public const string DebugSymbols = nameof(DebugSymbols); public const string PublishTrimmed = nameof(PublishTrimmed); public const string PublishAot = nameof(PublishAot); + public const string EnableHotReloadInRuntimeConfigDevFile = nameof(EnableHotReloadInRuntimeConfigDevFile); } internal static class ItemNames diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs index 2cb01f0d8774..b36a48dd19b7 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRuntimeConfigurationFiles.cs @@ -53,7 +53,18 @@ public class GenerateRuntimeConfigurationFiles : TaskBase, IMultiThreadableTask public bool WriteIncludedFrameworks { get; set; } - public bool GenerateRuntimeConfigDevFile { get; set; } + /// + /// True to generate probing paths to runtimeconfig.dev.json file. + /// + public bool GenerateProbingPathsToRuntimeConfigDevFile { get; set; } + + /// + /// True to generate switches that enable Hot Reload to runtimeconfig.dev.json file. + /// + public bool GenerateHotReloadRuntimeOptionsToRuntimeConfigDevFile { get; set; } + + private bool GenerateRuntimeConfigDevFile => + GenerateProbingPathsToRuntimeConfigDevFile || GenerateHotReloadRuntimeOptionsToRuntimeConfigDevFile; public bool AlwaysIncludeCoreFramework { get; set; } @@ -84,7 +95,7 @@ protected override void ExecuteCore() // If we want to generate the runtimeconfig.dev.json file // and we have additional probing paths to add to it // BUT the runtimeconfigdevpath is empty, log a warning. - if (GenerateRuntimeConfigDevFile && AdditionalProbingPaths?.Any() == true && string.IsNullOrEmpty(RuntimeConfigDevPath)) + if (GenerateProbingPathsToRuntimeConfigDevFile && AdditionalProbingPaths?.Any() == true && string.IsNullOrEmpty(RuntimeConfigDevPath)) { Log.LogWarning(Strings.SkippingAdditionalProbingPaths); } @@ -342,7 +353,17 @@ private void WriteDevRuntimeConfig(IList packageFolders) RuntimeOptions = new RuntimeOptions() }; - AddAdditionalProbingPaths(devConfig.RuntimeOptions, packageFolders); + if (GenerateProbingPathsToRuntimeConfigDevFile) + { + AddAdditionalProbingPaths(devConfig.RuntimeOptions, packageFolders); + } + + if (GenerateHotReloadRuntimeOptionsToRuntimeConfigDevFile) + { + JsonObject configProperties = GetConfigProperties(devConfig.RuntimeOptions); + configProperties["System.Reflection.Metadata.MetadataUpdater.IsSupported"] = true; + configProperties["System.StartupHookProvider.IsSupported"] = true; + } WriteToJsonFile(TaskEnvironment.GetAbsolutePath(RuntimeConfigDevPath), devConfig); _filesWritten.Add(new TaskItem(RuntimeConfigDevPath)); diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.targets b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.targets index 578169f70f00..b301ef6fed0f 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.targets +++ b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.targets @@ -74,11 +74,18 @@ Copyright (c) .NET Foundation. All rights reserved. + + + true + + - true - false + true + true + + true @@ -86,7 +93,7 @@ Copyright (c) .NET Foundation. All rights reserved. $(TargetDir)$(ProjectDepsFileName) $(AssemblyName).runtimeconfig.json $(TargetDir)$(ProjectRuntimeConfigFileName) - $(TargetDir)$(AssemblyName).runtimeconfig.dev.json + $(TargetDir)$(AssemblyName).runtimeconfig.dev.json true true @@ -354,6 +361,8 @@ Copyright (c) .NET Foundation. All rights reserved. <_GenerateRuntimeConfigurationPropertyInputsCacheToHash Include="$(SelfContained)"/> <_GenerateRuntimeConfigurationPropertyInputsCacheToHash Include="$(TargetFramework)"/> <_GenerateRuntimeConfigurationPropertyInputsCacheToHash Include="$(UserRuntimeConfig)"/> + <_GenerateRuntimeConfigurationPropertyInputsCacheToHash Include="$(GenerateProbingPathsToRuntimeConfigDevFile)"/> + <_GenerateRuntimeConfigurationPropertyInputsCacheToHash Include="$(EnableHotReloadInRuntimeConfigDevFile)"/> <_GenerateRuntimeConfigurationPropertyInputsCacheToHash Include="$(_WriteIncludedFrameworks)"/> @@ -415,7 +424,8 @@ Copyright (c) .NET Foundation. All rights reserved. AdditionalProbingPaths="@(AdditionalProbingPath)" IsSelfContained="$(SelfContained)" WriteIncludedFrameworks="$(_WriteIncludedFrameworks)" - GenerateRuntimeConfigDevFile="$(GenerateRuntimeConfigDevFile)" + GenerateProbingPathsToRuntimeConfigDevFile="$(GenerateProbingPathsToRuntimeConfigDevFile)" + GenerateHotReloadRuntimeOptionsToRuntimeConfigDevFile="$(EnableHotReloadInRuntimeConfigDevFile)" AlwaysIncludeCoreFramework="$(AlwaysIncludeCoreFrameworkInRuntimeConfig)"> diff --git a/test/Microsoft.DotNet.HotReload.Test.Utilities/WatchableApp.cs b/test/Microsoft.DotNet.HotReload.Test.Utilities/WatchableApp.cs index fc190fa25b25..bb34e461e0bc 100644 --- a/test/Microsoft.DotNet.HotReload.Test.Utilities/WatchableApp.cs +++ b/test/Microsoft.DotNet.HotReload.Test.Utilities/WatchableApp.cs @@ -119,31 +119,31 @@ public async ValueTask WaitUntilOutputContains(MessageDescriptor descrip return matchingLine; } - public Task WaitForOutputLineContaining(string text, [CallerFilePath] string? testPath = null, [CallerLineNumber] int testLine = 0) + public async Task WaitForOutputLineContaining(string text, [CallerFilePath] string? testPath = null, [CallerLineNumber] int testLine = 0) { LogWaitingForOutput(text, testPath, testLine); - var line = Process.GetRequiredOutputLineAsync(line => line.Contains(text)); + var line = await Process.GetRequiredOutputLineAsync(line => line.Contains(text)); LogFoundOutput(text, testPath, testLine); return line; } - public Task WaitForOutputLineContaining(MessageDescriptor descriptor, string? projectDisplay = null, [CallerLineNumber] int testLine = 0, [CallerFilePath] string? testPath = null) + public async Task WaitForOutputLineContaining(MessageDescriptor descriptor, string? projectDisplay = null, [CallerLineNumber] int testLine = 0, [CallerFilePath] string? testPath = null) { var pattern = GetPattern(descriptor, projectDisplay, out var patternDisplay); LogWaitingForOutput(patternDisplay, testPath, testLine); - var line = Process.GetRequiredOutputLineAsync(line => pattern.IsMatch(line)); + var line = await Process.GetRequiredOutputLineAsync(line => pattern.IsMatch(line)); LogFoundOutput(patternDisplay, testPath, testLine); return line; } - public Task WaitForOutputLineContaining(Regex pattern, [CallerFilePath] string? testPath = null, [CallerLineNumber] int testLine = 0) + public async Task WaitForOutputLineContaining(Regex pattern, [CallerFilePath] string? testPath = null, [CallerLineNumber] int testLine = 0) { var patternDisplay = pattern.ToString(); LogWaitingForOutput(patternDisplay, testPath, testLine); - var line = Process.GetRequiredOutputLineAsync(line => pattern.IsMatch(line)); + var line = await Process.GetRequiredOutputLineAsync(line => pattern.IsMatch(line)); LogFoundOutput(patternDisplay, testPath, testLine); return line; diff --git a/test/Microsoft.NET.Build.Tasks.Tests/GivenAGenerateRuntimeConfigMultiThreading.cs b/test/Microsoft.NET.Build.Tasks.Tests/GivenAGenerateRuntimeConfigMultiThreading.cs index 1eb6e85b6f5b..6a069c9d2548 100644 --- a/test/Microsoft.NET.Build.Tasks.Tests/GivenAGenerateRuntimeConfigMultiThreading.cs +++ b/test/Microsoft.NET.Build.Tasks.Tests/GivenAGenerateRuntimeConfigMultiThreading.cs @@ -38,7 +38,6 @@ public void ItWritesRuntimeConfigViaTaskEnvironment() RuntimeConfigPath = configRelativePath, RuntimeFrameworks = new ITaskItem[] { runtimeFramework }, IsSelfContained = false, - GenerateRuntimeConfigDevFile = false, }; // Execute — should write runtimeconfig.json under projectDir via TaskEnvironment @@ -143,7 +142,6 @@ private static (bool? result, MockBuildEngine engine, Exception? exception) RunT RuntimeConfigPath = runtimeConfigPath, RuntimeFrameworks = new ITaskItem[] { runtimeFramework }, IsSelfContained = false, - GenerateRuntimeConfigDevFile = false, AssetsFilePath = assetsFilePath, TaskEnvironment = taskEnvironment, }; @@ -300,6 +298,276 @@ public void UserRuntimeConfigProducesSameOutputInBothEnvironments() } } + [TestMethod] + public void ItGeneratesHotReloadPropertiesToDevConfig() + { + // When GenerateHotReloadRuntimeOptionsToRuntimeConfigDevFile = true, + // the task should write MetadataUpdater.IsSupported and StartupHookProvider.IsSupported + // to the runtimeconfig.dev.json file. + var projectDir = Path.Combine(Path.GetTempPath(), "rtconfig-hotreload-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(projectDir); + try + { + var configRelativePath = Path.Combine("bin", "test.runtimeconfig.json"); + var devConfigRelativePath = Path.Combine("bin", "test.runtimeconfig.dev.json"); + var devConfigAbsolutePath = Path.Combine(projectDir, devConfigRelativePath); + Directory.CreateDirectory(Path.Combine(projectDir, "bin")); + + var assetsFilePath = CreateMinimalAssetsFile(projectDir); + + var runtimeFramework = new TaskItem("Microsoft.NETCore.App"); + runtimeFramework.SetMetadata("FrameworkName", "Microsoft.NETCore.App"); + runtimeFramework.SetMetadata("Version", "8.0.0"); + + var mockEngine = new MockBuildEngine(); + var task = new GenerateRuntimeConfigurationFiles + { + BuildEngine = mockEngine, + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDir), + TargetFramework = "net8.0", + TargetFrameworkMoniker = ".NETCoreApp,Version=v8.0", + RuntimeConfigPath = configRelativePath, + RuntimeConfigDevPath = devConfigRelativePath, + RuntimeFrameworks = new ITaskItem[] { runtimeFramework }, + IsSelfContained = false, + AssetsFilePath = assetsFilePath, + GenerateHotReloadRuntimeOptionsToRuntimeConfigDevFile = true, + }; + + task.Execute().Should().BeTrue( + string.Join("; ", mockEngine.Errors.Select(e => e.Message))); + + File.Exists(devConfigAbsolutePath).Should().BeTrue( + "runtimeconfig.dev.json should be generated when hot reload options are enabled"); + + var devContent = File.ReadAllText(devConfigAbsolutePath); + devContent.Should().Contain("System.Reflection.Metadata.MetadataUpdater.IsSupported", + "hot reload MetadataUpdater switch should be present in dev config"); + devContent.Should().Contain("System.StartupHookProvider.IsSupported", + "hot reload StartupHookProvider switch should be present in dev config"); + } + finally + { + Directory.Delete(projectDir, true); + } + } + + [TestMethod] + public void ItDoesNotGenerateHotReloadPropertiesWhenDisabled() + { + // When GenerateHotReloadRuntimeOptionsToRuntimeConfigDevFile = false (default) + // and GenerateProbingPathsToRuntimeConfigDevFile = true, + // the dev config should NOT contain Hot Reload properties. + var projectDir = Path.Combine(Path.GetTempPath(), "rtconfig-nohotreload-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(projectDir); + try + { + var configRelativePath = Path.Combine("bin", "test.runtimeconfig.json"); + var devConfigRelativePath = Path.Combine("bin", "test.runtimeconfig.dev.json"); + var devConfigAbsolutePath = Path.Combine(projectDir, devConfigRelativePath); + Directory.CreateDirectory(Path.Combine(projectDir, "bin")); + + var assetsFilePath = CreateMinimalAssetsFile(projectDir); + + var runtimeFramework = new TaskItem("Microsoft.NETCore.App"); + runtimeFramework.SetMetadata("FrameworkName", "Microsoft.NETCore.App"); + runtimeFramework.SetMetadata("Version", "8.0.0"); + + var probingPath = new TaskItem("C:\\packages"); + + var mockEngine = new MockBuildEngine(); + var task = new GenerateRuntimeConfigurationFiles + { + BuildEngine = mockEngine, + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDir), + TargetFramework = "net8.0", + TargetFrameworkMoniker = ".NETCoreApp,Version=v8.0", + RuntimeConfigPath = configRelativePath, + RuntimeConfigDevPath = devConfigRelativePath, + RuntimeFrameworks = new ITaskItem[] { runtimeFramework }, + IsSelfContained = false, + AssetsFilePath = assetsFilePath, + GenerateProbingPathsToRuntimeConfigDevFile = true, + GenerateHotReloadRuntimeOptionsToRuntimeConfigDevFile = false, + AdditionalProbingPaths = new ITaskItem[] { probingPath }, + }; + + task.Execute().Should().BeTrue( + string.Join("; ", mockEngine.Errors.Select(e => e.Message))); + + File.Exists(devConfigAbsolutePath).Should().BeTrue( + "runtimeconfig.dev.json should be generated when probing paths are enabled"); + + var devContent = File.ReadAllText(devConfigAbsolutePath); + devContent.Should().NotContain("System.Reflection.Metadata.MetadataUpdater.IsSupported", + "hot reload MetadataUpdater switch should NOT be present when disabled"); + devContent.Should().NotContain("System.StartupHookProvider.IsSupported", + "hot reload StartupHookProvider switch should NOT be present when disabled"); + devContent.Should().Contain("additionalProbingPaths", + "probing paths should still be present"); + } + finally + { + Directory.Delete(projectDir, true); + } + } + + [TestMethod] + public void ItGeneratesBothHotReloadAndProbingPathsWhenBothEnabled() + { + // When both flags are true, the dev config should contain both + // hot reload properties and additional probing paths. + var projectDir = Path.Combine(Path.GetTempPath(), "rtconfig-both-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(projectDir); + try + { + var configRelativePath = Path.Combine("bin", "test.runtimeconfig.json"); + var devConfigRelativePath = Path.Combine("bin", "test.runtimeconfig.dev.json"); + var devConfigAbsolutePath = Path.Combine(projectDir, devConfigRelativePath); + Directory.CreateDirectory(Path.Combine(projectDir, "bin")); + + var assetsFilePath = CreateMinimalAssetsFile(projectDir); + + var runtimeFramework = new TaskItem("Microsoft.NETCore.App"); + runtimeFramework.SetMetadata("FrameworkName", "Microsoft.NETCore.App"); + runtimeFramework.SetMetadata("Version", "8.0.0"); + + var probingPath = new TaskItem("C:\\packages"); + + var mockEngine = new MockBuildEngine(); + var task = new GenerateRuntimeConfigurationFiles + { + BuildEngine = mockEngine, + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDir), + TargetFramework = "net8.0", + TargetFrameworkMoniker = ".NETCoreApp,Version=v8.0", + RuntimeConfigPath = configRelativePath, + RuntimeConfigDevPath = devConfigRelativePath, + RuntimeFrameworks = new ITaskItem[] { runtimeFramework }, + IsSelfContained = false, + AssetsFilePath = assetsFilePath, + GenerateProbingPathsToRuntimeConfigDevFile = true, + GenerateHotReloadRuntimeOptionsToRuntimeConfigDevFile = true, + AdditionalProbingPaths = new ITaskItem[] { probingPath }, + }; + + task.Execute().Should().BeTrue( + string.Join("; ", mockEngine.Errors.Select(e => e.Message))); + + File.Exists(devConfigAbsolutePath).Should().BeTrue( + "runtimeconfig.dev.json should be generated when both flags are enabled"); + + var devContent = File.ReadAllText(devConfigAbsolutePath); + devContent.Should().Contain("System.Reflection.Metadata.MetadataUpdater.IsSupported"); + devContent.Should().Contain("System.StartupHookProvider.IsSupported"); + devContent.Should().Contain("additionalProbingPaths"); + } + finally + { + Directory.Delete(projectDir, true); + } + } + + [TestMethod] + public void ItDoesNotGenerateDevConfigWhenBothFlagsAreFalse() + { + // When both GenerateProbingPathsToRuntimeConfigDevFile and + // GenerateHotReloadRuntimeOptionsToRuntimeConfigDevFile are false, + // the computed GenerateRuntimeConfigDevFile should be false, + // and no dev config file should be written. + var projectDir = Path.Combine(Path.GetTempPath(), "rtconfig-nodev-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(projectDir); + try + { + var configRelativePath = Path.Combine("bin", "test.runtimeconfig.json"); + var devConfigRelativePath = Path.Combine("bin", "test.runtimeconfig.dev.json"); + var devConfigAbsolutePath = Path.Combine(projectDir, devConfigRelativePath); + Directory.CreateDirectory(Path.Combine(projectDir, "bin")); + + var assetsFilePath = CreateMinimalAssetsFile(projectDir); + + var runtimeFramework = new TaskItem("Microsoft.NETCore.App"); + runtimeFramework.SetMetadata("FrameworkName", "Microsoft.NETCore.App"); + runtimeFramework.SetMetadata("Version", "8.0.0"); + + var mockEngine = new MockBuildEngine(); + var task = new GenerateRuntimeConfigurationFiles + { + BuildEngine = mockEngine, + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDir), + TargetFramework = "net8.0", + TargetFrameworkMoniker = ".NETCoreApp,Version=v8.0", + RuntimeConfigPath = configRelativePath, + RuntimeConfigDevPath = devConfigRelativePath, + RuntimeFrameworks = new ITaskItem[] { runtimeFramework }, + IsSelfContained = false, + AssetsFilePath = assetsFilePath, + GenerateProbingPathsToRuntimeConfigDevFile = false, + GenerateHotReloadRuntimeOptionsToRuntimeConfigDevFile = false, + }; + + task.Execute().Should().BeTrue( + string.Join("; ", mockEngine.Errors.Select(e => e.Message))); + + File.Exists(devConfigAbsolutePath).Should().BeFalse( + "runtimeconfig.dev.json should NOT be generated when both flags are false"); + } + finally + { + Directory.Delete(projectDir, true); + } + } + + /// + /// Creates a minimal project.assets.json file that is sufficient for + /// GenerateRuntimeConfigurationFiles to enter the assets-file code path. + /// The target key uses short form "net8.0" to match TargetFramework = "net8.0". + /// + private static string CreateMinimalAssetsFile(string projectDir) + { + var objDir = Path.Combine(projectDir, "obj"); + Directory.CreateDirectory(objDir); + var assetsPath = Path.Combine(objDir, "project.assets.json"); + var projectPath = Path.Combine(projectDir, "TestApp.csproj").Replace("\\", "\\\\"); + var packagesPath = Path.Combine(projectDir, ".nuget", "packages").Replace("\\", "\\\\"); + var outputPath = Path.Combine(projectDir, "obj").Replace("\\", "\\\\"); + + File.WriteAllText(assetsPath, $$""" + { + "version": 3, + "targets": { "net8.0": {} }, + "libraries": {}, + "projectFileDependencyGroups": { "net8.0": [] }, + "packageFolders": {}, + "project": { + "version": "1.0.0", + "restore": { + "projectUniqueName": "{{projectPath}}", + "projectName": "TestApp", + "projectPath": "{{projectPath}}", + "packagesPath": "{{packagesPath}}", + "outputPath": "{{outputPath}}", + "projectStyle": "PackageReference", + "fallbackFolders": [], + "configFilePaths": [], + "originalTargetFrameworks": [ "net8.0" ], + "sources": {}, + "frameworks": { "net8.0": { "targetAlias": "net8.0" } }, + "warningProperties": { "warnAsError": [ "NU1605" ] } + }, + "frameworks": { + "net8.0": { + "targetAlias": "net8.0" + } + } + } + } + """); + + // Return relative path since the task will resolve via TaskEnvironment + return Path.Combine("obj", "project.assets.json"); + } + [TestMethod] public void UserRuntimeConfigWithNonexistentFileProducesSameOutputInBothEnvironments() { diff --git a/test/Microsoft.NET.Build.Tests/AppHostTests.cs b/test/Microsoft.NET.Build.Tests/AppHostTests.cs index 3fa818484479..89dd3ff9cf08 100644 --- a/test/Microsoft.NET.Build.Tests/AppHostTests.cs +++ b/test/Microsoft.NET.Build.Tests/AppHostTests.cs @@ -7,7 +7,6 @@ using System.Reflection.PortableExecutable; using System.Text.RegularExpressions; using Microsoft.DotNet.Cli.Utils; -using NuGet.Frameworks; namespace Microsoft.NET.Build.Tests { @@ -23,17 +22,12 @@ private static string[] GetExpectedFilesFromBuild(TestAsset testAsset, string ta $"{testProjectName}.dll", $"{testProjectName}.pdb", $"{testProjectName}.deps.json", - $"{testProjectName}.runtimeconfig.json" + $"{testProjectName}.runtimeconfig.json", + // Debug builds always generate a runtimeconfig.dev.json file (probing paths for pre-net6.0 + // target frameworks and Hot Reload runtime options for net6.0+). + $"{testProjectName}.runtimeconfig.dev.json" }; - if (!string.IsNullOrEmpty(targetFramework)) - { - var parsedTargetFramework = NuGetFramework.Parse(targetFramework); - - if (parsedTargetFramework.Version.Major < 6) - expectedFiles.Add($"{testProjectName}.runtimeconfig.dev.json"); - } - return expectedFiles.ToArray(); } diff --git a/test/Microsoft.NET.Build.Tests/GivenThatWeWantAllResourcesInSatellite.cs b/test/Microsoft.NET.Build.Tests/GivenThatWeWantAllResourcesInSatellite.cs index cb8970fdf355..0ee155ac0f1a 100644 --- a/test/Microsoft.NET.Build.Tests/GivenThatWeWantAllResourcesInSatellite.cs +++ b/test/Microsoft.NET.Build.Tests/GivenThatWeWantAllResourcesInSatellite.cs @@ -82,6 +82,7 @@ internal static void TestSatelliteResources( outputFiles.Add("AllResourcesInSatellite.dll"); outputFiles.Add("AllResourcesInSatellite.deps.json"); outputFiles.Add("AllResourcesInSatellite.runtimeconfig.json"); + outputFiles.Add("AllResourcesInSatellite.runtimeconfig.dev.json"); command = new DotnetCommand(log, Path.Combine(outputDirectory.FullName, "AllResourcesInSatellite.dll")); } diff --git a/test/Microsoft.NET.Build.Tests/GivenThatWeWantBuildsToBeIncremental.cs b/test/Microsoft.NET.Build.Tests/GivenThatWeWantBuildsToBeIncremental.cs index be3ddeffb721..09f59669a285 100644 --- a/test/Microsoft.NET.Build.Tests/GivenThatWeWantBuildsToBeIncremental.cs +++ b/test/Microsoft.NET.Build.Tests/GivenThatWeWantBuildsToBeIncremental.cs @@ -6,7 +6,6 @@ namespace Microsoft.NET.Build.Tests [TestClass] public class GivenThatWeWantBuildsToBeIncremental : SdkTest { - [TestMethod] [DataRow("netcoreapp1.1")] [DataRow(ToolsetInfo.CurrentTargetFramework)] @@ -30,6 +29,54 @@ public void GenerateBuildRuntimeConfigurationFiles_runs_incrementally(string tar runtimeConfigDevJsonSecondModifiedTime.Should().Be(runtimeConfigDevJsonFirstModifiedTime); } + [TestMethod] + public void RuntimeConfigInputCache_changes_when_GenerateProbingPathsToRuntimeConfigDevFile_changes() + { + var testAsset = TestAssetsManager + .CopyTestAsset("HelloWorld", identifier: "ProbingPathsCacheTest") + .WithSource() + .WithTargetFramework(ToolsetInfo.CurrentTargetFramework); + + var buildCommand = new BuildCommand(testAsset); + var intermediateDirectory = buildCommand.GetIntermediateDirectory(ToolsetInfo.CurrentTargetFramework).FullName; + var cacheFilePath = Path.Combine(intermediateDirectory, "HelloWorld.genruntimeconfig.cache"); + + // Build with default (probing paths disabled for net6.0+) + buildCommand.Execute().Should().Pass(); + var hash1 = File.ReadAllText(cacheFilePath).Trim(); + + // Build with probing paths explicitly enabled + buildCommand.Execute("/p:GenerateProbingPathsToRuntimeConfigDevFile=true").Should().Pass(); + var hash2 = File.ReadAllText(cacheFilePath).Trim(); + + hash2.Should().NotBe(hash1, + "changing GenerateProbingPathsToRuntimeConfigDevFile should change the input cache hash"); + } + + [TestMethod] + public void RuntimeConfigInputCache_changes_when_EnableHotReloadInRuntimeConfigDevFile_changes() + { + var testAsset = TestAssetsManager + .CopyTestAsset("HelloWorld", identifier: "HotReloadCacheTest") + .WithSource() + .WithTargetFramework(ToolsetInfo.CurrentTargetFramework); + + var buildCommand = new BuildCommand(testAsset); + var intermediateDirectory = buildCommand.GetIntermediateDirectory(ToolsetInfo.CurrentTargetFramework).FullName; + var cacheFilePath = Path.Combine(intermediateDirectory, "HelloWorld.genruntimeconfig.cache"); + + // Build with hot reload disabled + buildCommand.Execute("/p:EnableHotReloadInRuntimeConfigDevFile=false").Should().Pass(); + var hash1 = File.ReadAllText(cacheFilePath).Trim(); + + // Build with hot reload enabled + buildCommand.Execute("/p:EnableHotReloadInRuntimeConfigDevFile=true").Should().Pass(); + var hash2 = File.ReadAllText(cacheFilePath).Trim(); + + hash2.Should().NotBe(hash1, + "changing EnableHotReloadInRuntimeConfigDevFile should change the input cache hash"); + } + [TestMethod] [DataRow("netcoreapp1.1")] [DataRow(ToolsetInfo.CurrentTargetFramework)] diff --git a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildAComServerLibrary.cs b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildAComServerLibrary.cs index 81d2cac48a07..e592a010210a 100644 --- a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildAComServerLibrary.cs +++ b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildAComServerLibrary.cs @@ -32,7 +32,8 @@ public void It_copies_the_comhost_to_the_output_directory() "ComServer.pdb", "ComServer.deps.json", "ComServer.comhost.dll", - "ComServer.runtimeconfig.json" + "ComServer.runtimeconfig.json", + "ComServer.runtimeconfig.dev.json" }); string runtimeConfigFile = Path.Combine(outputDirectory.FullName, "ComServer.runtimeconfig.json"); @@ -70,7 +71,8 @@ public void It_generates_a_regfree_com_manifest_when_requested() "ComServer.deps.json", "ComServer.comhost.dll", "ComServer.X.manifest", - "ComServer.runtimeconfig.json" + "ComServer.runtimeconfig.json", + "ComServer.runtimeconfig.dev.json" }); } @@ -102,7 +104,8 @@ public void It_embeds_the_clsidmap_in_the_comhost_when_rid_specified(string rid) "ComServer.pdb", "ComServer.deps.json", "ComServer.comhost.dll", - "ComServer.runtimeconfig.json" + "ComServer.runtimeconfig.json", + "ComServer.runtimeconfig.dev.json" }); } diff --git a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildACrossTargetedLibrary.cs b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildACrossTargetedLibrary.cs index 3088cf760bfd..5ae63c1fa1ab 100644 --- a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildACrossTargetedLibrary.cs +++ b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildACrossTargetedLibrary.cs @@ -28,6 +28,7 @@ public void It_builds_nondesktop_library_successfully_on_all_platforms() $"{ToolsetInfo.CurrentTargetFramework}/NetStandardAndNetCoreApp.dll", $"{ToolsetInfo.CurrentTargetFramework}/NetStandardAndNetCoreApp.pdb", $"{ToolsetInfo.CurrentTargetFramework}/NetStandardAndNetCoreApp.runtimeconfig.json", + $"{ToolsetInfo.CurrentTargetFramework}/NetStandardAndNetCoreApp.runtimeconfig.dev.json", $"{ToolsetInfo.CurrentTargetFramework}/NetStandardAndNetCoreApp.deps.json", $"{ToolsetInfo.CurrentTargetFramework}/Newtonsoft.Json.dll", $"{ToolsetInfo.CurrentTargetFramework}/NetStandardAndNetCoreApp.deps.json", diff --git a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildANetCoreApp.cs b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildANetCoreApp.cs index 14e6ff9856e6..68421aff5583 100644 --- a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildANetCoreApp.cs +++ b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildANetCoreApp.cs @@ -392,7 +392,9 @@ public void It_stops_generating_runtimeconfig_dev_json_after_net6(string targetF buildCommand.GetOutputDirectory(targetFramework).FullName, $"{proj.Name}.runtimeconfig.dev.json"); - buildCommand.Execute().StdOut + // Disable Hot Reload runtime options (which are emitted to runtimeconfig.dev.json for net6.0+ in + // Debug builds) so this test verifies the probing-paths behavior that was removed after net6.0. + buildCommand.Execute("/p:EnableHotReloadInRuntimeConfigDevFile=false").StdOut .Should() .NotContain("NETSDK1048"); diff --git a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildASolutionWithNonAnyCPUPlatform.cs b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildASolutionWithNonAnyCPUPlatform.cs index 28c491e530bd..5b597638fc6f 100644 --- a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildASolutionWithNonAnyCPUPlatform.cs +++ b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildASolutionWithNonAnyCPUPlatform.cs @@ -25,6 +25,7 @@ public void It_builds_solution_successfully() .Should() .OnlyHaveFiles(new[] { "x64SolutionBuild.runtimeconfig.json", + "x64SolutionBuild.runtimeconfig.dev.json", "x64SolutionBuild.deps.json", "x64SolutionBuild.dll", "x64SolutionBuild.pdb", diff --git a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildAnAppWithLibrariesAndRid.cs b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildAnAppWithLibrariesAndRid.cs index 8dedc4ecdb62..d7e220a8fc81 100644 --- a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildAnAppWithLibrariesAndRid.cs +++ b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildAnAppWithLibrariesAndRid.cs @@ -103,6 +103,7 @@ public void It_builds_a_framework_dependent_RID_specific_runnable_output() "App.pdb", "App.deps.json", "App.runtimeconfig.json", + "App.runtimeconfig.dev.json", "LibraryWithoutRid.dll", "LibraryWithoutRid.pdb", "LibraryWithRid.dll", diff --git a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildAnAppWithLibrary.cs b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildAnAppWithLibrary.cs index 823ab9ea261e..4750683038f9 100644 --- a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildAnAppWithLibrary.cs +++ b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildAnAppWithLibrary.cs @@ -50,6 +50,7 @@ void VerifyAppBuilds(TestAsset testAsset) $"TestApp{EnvironmentInfo.ExecutableExtension}", "TestApp.deps.json", "TestApp.runtimeconfig.json", + "TestApp.runtimeconfig.dev.json", "TestLibrary.dll", "TestLibrary.pdb", }); @@ -146,6 +147,7 @@ public void The_clean_target_removes_all_files_from_the_output_folder() $"TestApp{EnvironmentInfo.ExecutableExtension}", "TestApp.deps.json", "TestApp.runtimeconfig.json", + "TestApp.runtimeconfig.dev.json", "TestLibrary.dll", "TestLibrary.pdb" }); diff --git a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildAnAppWithTransitiveNonSdkProjectRefs.cs b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildAnAppWithTransitiveNonSdkProjectRefs.cs index 81a0a45ee5d2..24e3fb9670f7 100644 --- a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildAnAppWithTransitiveNonSdkProjectRefs.cs +++ b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildAnAppWithTransitiveNonSdkProjectRefs.cs @@ -172,6 +172,7 @@ private string VerifyAppBuilds(TestAsset testAsset, string prefix) $"TestApp{EnvironmentInfo.ExecutableExtension}", "TestApp.deps.json", "TestApp.runtimeconfig.json", + "TestApp.runtimeconfig.dev.json", prefix + "MainLibrary.dll", prefix + "MainLibrary.pdb", prefix + "AuxLibrary.dll", diff --git a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildAnAppWithTransitiveProjectRefs.cs b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildAnAppWithTransitiveProjectRefs.cs index 2e47c5b9c1ae..8a77b84a28de 100644 --- a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildAnAppWithTransitiveProjectRefs.cs +++ b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildAnAppWithTransitiveProjectRefs.cs @@ -37,6 +37,7 @@ void VerifyAppBuilds(TestAsset testAsset) $"TestApp{EnvironmentInfo.ExecutableExtension}", "TestApp.deps.json", "TestApp.runtimeconfig.json", + "TestApp.runtimeconfig.dev.json", "MainLibrary.dll", "MainLibrary.pdb", "AuxLibrary.dll", @@ -76,6 +77,7 @@ public void The_clean_target_removes_all_files_from_the_output_folder() $"TestApp{EnvironmentInfo.ExecutableExtension}", "TestApp.deps.json", "TestApp.runtimeconfig.json", + "TestApp.runtimeconfig.dev.json", "MainLibrary.dll", "MainLibrary.pdb", "AuxLibrary.dll", diff --git a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildAnAppWithoutTransitiveProjectRefs.cs b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildAnAppWithoutTransitiveProjectRefs.cs index 7be4397a93dc..cf29d4d3e419 100644 --- a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildAnAppWithoutTransitiveProjectRefs.cs +++ b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildAnAppWithoutTransitiveProjectRefs.cs @@ -75,6 +75,7 @@ public void It_cleans_the_project_successfully_with_static_graph_and_isolation() "1.pdb", "1.deps.json", "1.runtimeconfig.json", + "1.runtimeconfig.dev.json", $"1{EnvironmentInfo.ExecutableExtension}" }; @@ -164,6 +165,7 @@ public void It_builds_the_project_successfully_when_RAR_does_not_find_all_refere "1.pdb", "1.deps.json", "1.runtimeconfig.json", + "1.runtimeconfig.dev.json", "2.dll", "2.pdb", $"1{EnvironmentInfo.ExecutableExtension}", diff --git a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToCopyLocalDependencies.cs b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToCopyLocalDependencies.cs index d0b7d8957add..bb3c30883485 100644 --- a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToCopyLocalDependencies.cs +++ b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToCopyLocalDependencies.cs @@ -42,6 +42,7 @@ public void It_copies_local_package_dependencies_on_build() $"{ProjectName}.dll", $"{ProjectName}.pdb", $"{ProjectName}.runtimeconfig.json", + $"{ProjectName}.runtimeconfig.dev.json", "Newtonsoft.Json.dll", "runtimes/linux-x64/native/libsqlite3.so", "runtimes/osx-x64/native/libsqlite3.dylib", @@ -82,6 +83,7 @@ public void It_does_not_copy_local_package_dependencies_when_requested_not_to() $"{ProjectName}.dll", $"{ProjectName}.pdb", $"{ProjectName}.runtimeconfig.json", + $"{ProjectName}.runtimeconfig.dev.json", }); } @@ -121,6 +123,7 @@ public void It_copies_local_specific_runtime_package_dependencies_on_build() $"{ProjectName}.dll", $"{ProjectName}.pdb", $"{ProjectName}.runtimeconfig.json", + $"{ProjectName}.runtimeconfig.dev.json", "Newtonsoft.Json.dll", // NOTE: this may break in the future when the SDK supports platforms that libuv does not $"libuv{FileConstants.DynamicLibSuffix}" diff --git a/test/Microsoft.NET.Pack.Tests/GivenThatWeWantToPackACrossTargetedLibrary.cs b/test/Microsoft.NET.Pack.Tests/GivenThatWeWantToPackACrossTargetedLibrary.cs index 9f58ed1d28fe..721f2be23e08 100644 --- a/test/Microsoft.NET.Pack.Tests/GivenThatWeWantToPackACrossTargetedLibrary.cs +++ b/test/Microsoft.NET.Pack.Tests/GivenThatWeWantToPackACrossTargetedLibrary.cs @@ -28,6 +28,7 @@ public void It_packs_nondesktop_library_successfully_on_all_platforms() $"{ToolsetInfo.CurrentTargetFramework}/NetStandardAndNetCoreApp.dll", $"{ToolsetInfo.CurrentTargetFramework}/NetStandardAndNetCoreApp.pdb", $"{ToolsetInfo.CurrentTargetFramework}/NetStandardAndNetCoreApp.runtimeconfig.json", + $"{ToolsetInfo.CurrentTargetFramework}/NetStandardAndNetCoreApp.runtimeconfig.dev.json", $"{ToolsetInfo.CurrentTargetFramework}/NetStandardAndNetCoreApp.deps.json", $"{ToolsetInfo.CurrentTargetFramework}/Newtonsoft.Json.dll", $"{ToolsetInfo.CurrentTargetFramework}/NetStandardAndNetCoreApp{EnvironmentInfo.ExecutableExtension}", diff --git a/test/Microsoft.NET.Pack.Tests/GivenThatWeWantToPackASimpleLibrary.cs b/test/Microsoft.NET.Pack.Tests/GivenThatWeWantToPackASimpleLibrary.cs index 420c666499a1..b1bd08474fa2 100644 --- a/test/Microsoft.NET.Pack.Tests/GivenThatWeWantToPackASimpleLibrary.cs +++ b/test/Microsoft.NET.Pack.Tests/GivenThatWeWantToPackASimpleLibrary.cs @@ -34,6 +34,7 @@ public void It_packs_successfully() $"HelloWorld.pdb", $"HelloWorld.deps.json", $"HelloWorld.runtimeconfig.json", + $"HelloWorld.runtimeconfig.dev.json", $"HelloWorld{EnvironmentInfo.ExecutableExtension}", }); } diff --git a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmBuildIncrementalismTest.cs b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmBuildIncrementalismTest.cs index fb52fcfc8117..ceb814565e01 100644 --- a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmBuildIncrementalismTest.cs +++ b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmBuildIncrementalismTest.cs @@ -31,6 +31,7 @@ public void Build_IsIncremental() var filesToIgnore = new[] { Path.Combine(buildOutputDirectory, "blazorwasm.runtimeconfig.json"), + Path.Combine(buildOutputDirectory, "blazorwasm.runtimeconfig.dev.json"), Path.Combine(buildOutputDirectory, "RazorClassLibrary.staticwebassets.endpoints.json"), Path.Combine(buildOutputDirectory, "blazorwasm.staticwebassets.endpoints.json") }; diff --git a/test/dotnet-watch.Tests/HotReload/SourceFileUpdateTests.HotReloadNotSupported.cs b/test/dotnet-watch.Tests/HotReload/SourceFileUpdateTests.HotReloadNotSupported.cs index fc621eb004d2..aa2404a9d060 100644 --- a/test/dotnet-watch.Tests/HotReload/SourceFileUpdateTests.HotReloadNotSupported.cs +++ b/test/dotnet-watch.Tests/HotReload/SourceFileUpdateTests.HotReloadNotSupported.cs @@ -3,6 +3,8 @@ #nullable disable +using Combinatorial.MSTest; + namespace Microsoft.DotNet.Watch.UnitTests; [TestClass] @@ -13,22 +15,14 @@ public class SourceFileUpdateTests_HotReloadNotSupported : DotNetWatchTestBase [DataRow("PublishTrimmed", "True")] [DataRow("StartupHookSupport", "False")] [DataRow("Optimize", "True")] - [DataRow("MetadataUpdaterSupport", "False")] - public async Task ChangeFileInAotProject(string propertyName, string propertyValue) + public async Task ChangeFileInAotProject_PriorNet11(string propertyName, string propertyValue) { - var tfvParsed = Version.Parse(ToolsetInfo.CurrentTargetFrameworkVersion); - var isNet11OrNewer = tfvParsed.Major >= 11; - - // Optimize check only applies to < .NET 11; MetadataUpdaterSupport only to >= .NET 11. - if (propertyName == "Optimize" && isNet11OrNewer) - return; - if (propertyName == "MetadataUpdaterSupport" && !isNet11OrNewer) - return; - - var projectDisplay = $"WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})"; + var tfm = "net9.0"; + var projectDisplay = $"WatchHotReloadApp ({tfm})"; var testAsset = TestAssets.CopyTestAsset("WatchHotReloadApp", identifier: $"{propertyName};{propertyValue}") .WithSource() + .WithTargetFramework(tfm) .WithProjectChanges(project => { project.Root.Descendants() @@ -44,9 +38,9 @@ public async Task ChangeFileInAotProject(string propertyName, string propertyVal var (suggestedProperty, suggestedValue) = propertyName switch { "Optimize" => (PropertyNames.Optimize, "False"), - "MetadataUpdaterSupport" => (PropertyNames.MetadataUpdaterSupport, "True"), _ => (PropertyNames.StartupHookSupport, "True"), }; + var message = MessageDescriptor.ProjectDoesNotSupportHotReload_Property.GetMessage((propertyName, propertyValue, suggestedProperty, suggestedValue)); await App.WaitForOutputLineContaining($"[{projectDisplay}] {message}"); await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); @@ -58,6 +52,76 @@ public async Task ChangeFileInAotProject(string propertyName, string propertyVal await App.WaitForOutputLineContaining(""); } + [TestMethod] + [CombinatorialData] + public async Task ChangeFileInAotProject_Net11_DisabledInConfigDevFile(bool startupHookSupport) + { + var tfm = ToolsetInfo.CurrentTargetFramework; + var projectDisplay = $"WatchHotReloadApp ({tfm})"; + + var testAsset = TestAssets.CopyTestAsset("WatchHotReloadApp", identifier: $"{startupHookSupport}") + .WithSource() + .WithTargetFramework(tfm) + .WithProjectChanges(project => + { + project.Root.Descendants() + .First(e => e.Name.LocalName == "PropertyGroup") + .Add( + XElement.Parse($"false"), + XElement.Parse($"{!startupHookSupport}"), + XElement.Parse($"{startupHookSupport}")); + }); + + var programPath = Path.Combine(testAsset.Path, "Program.cs"); + + App.Start(testAsset, ["--non-interactive"]); + + var propertyName = startupHookSupport ? "MetadataUpdaterSupport" : "StartupHookSupport"; + var message = MessageDescriptor.ProjectDoesNotSupportHotReload_Property.GetMessage((propertyName, "False", "EnableHotReloadInRuntimeConfigDevFile", "True")); + await App.WaitForOutputLineContaining($"[{projectDisplay}] {message}"); + await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + App.Process.ClearOutput(); + + UpdateSourceFile(programPath, content => content.Replace("Console.WriteLine(\".\");", "Console.WriteLine(\"\");")); + + await App.WaitForOutputLineContaining($"[{projectDisplay}] [auto-restart] {programPath}(1,1): error ENC0097"); // Applying source changes while the application is running is not supported by the runtime. + await App.WaitForOutputLineContaining(""); + } + + [TestMethod] + [CombinatorialData] + public async Task ChangeFileInAotProject_Net11_EnabledInConfigFile(bool enabledInDevFile) + { + var tfm = ToolsetInfo.CurrentTargetFramework; + var projectDisplay = $"WatchHotReloadApp ({tfm})"; + + var testAsset = TestAssets.CopyTestAsset("WatchHotReloadApp", identifier: $"{enabledInDevFile}") + .WithSource() + .WithTargetFramework(tfm) + .WithProjectChanges(project => + { + project.Root.Descendants() + .First(e => e.Name.LocalName == "PropertyGroup") + .Add( + XElement.Parse($"{enabledInDevFile}"), + XElement.Parse($"{!enabledInDevFile}"), + XElement.Parse($"{!enabledInDevFile}")); + }); + + var programPath = Path.Combine(testAsset.Path, "Program.cs"); + + App.Start(testAsset, ["--non-interactive"]); + + await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + App.AssertOutputDoesNotContain("⚠"); + App.Process.ClearOutput(); + + UpdateSourceFile(programPath, content => content.Replace("Console.WriteLine(\".\");", "Console.WriteLine(\"\");")); + + await App.WaitForOutputLineContaining(MessageDescriptor.ManagedCodeChangesApplied); + await App.WaitForOutputLineContaining(""); + } + [TestMethod] public async Task ChangeFileInFSharpProject() {