From c55b1e81df2d0938efaa039f56666015aeb5a4ea Mon Sep 17 00:00:00 2001 From: Tomas Matousek Date: Fri, 3 Oct 2025 10:25:32 -0700 Subject: [PATCH 1/3] Use ExternalAccess to access Roslyn internal APIs --- HotReloadUtils.slnx | 1 - .../BaselineArtifacts.cs | 3 +- .../BaselineProject.cs | 58 ++-- .../DeltaProject.cs | 25 +- .../EnC/ChangeMaker.cs | 51 ---- .../EnC/ChangeMakerService.cs | 249 ------------------ ...ft.DotNet.HotReload.Utils.Generator.csproj | 1 + .../Runner.cs | 20 +- .../Runners/LiveRunner.cs | 5 +- .../Runners/ScriptRunner.cs | 5 +- .../EncCapabilitiesCompat.cs | 74 ------ .../EnC.Tests/WatchHotReloadServiceTest.cs | 74 ------ 12 files changed, 66 insertions(+), 500 deletions(-) delete mode 100644 src/Microsoft.DotNet.HotReload.Utils.Generator/EnC/ChangeMaker.cs delete mode 100644 src/Microsoft.DotNet.HotReload.Utils.Generator/EnC/ChangeMakerService.cs delete mode 100644 tests/Acceptance/EncCapabilitiesCompat.Tests/EncCapabilitiesCompat.cs delete mode 100644 tests/HotReload.Generator/EnC.Tests/WatchHotReloadServiceTest.cs diff --git a/HotReloadUtils.slnx b/HotReloadUtils.slnx index a4a0e4d10..b950758de 100644 --- a/HotReloadUtils.slnx +++ b/HotReloadUtils.slnx @@ -1,6 +1,5 @@ - diff --git a/src/Microsoft.DotNet.HotReload.Utils.Generator/BaselineArtifacts.cs b/src/Microsoft.DotNet.HotReload.Utils.Generator/BaselineArtifacts.cs index 10ecae05b..d7648bdaf 100644 --- a/src/Microsoft.DotNet.HotReload.Utils.Generator/BaselineArtifacts.cs +++ b/src/Microsoft.DotNet.HotReload.Utils.Generator/BaselineArtifacts.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.ExternalAccess.HotReload.Api; namespace Microsoft.DotNet.HotReload.Utils.Generator; @@ -12,4 +13,4 @@ namespace Microsoft.DotNet.HotReload.Utils.Generator; /// BaselineOutputAsmPath: absolute path of the baseline assembly /// DocResolver: a map from document ids to documents /// ChangeMakerService: A stateful encapsulatio of the series of changes that have been made to the baseline -public record struct BaselineArtifacts (Solution BaselineSolution, ProjectId BaselineProjectId, string BaselineOutputAsmPath, DocResolver DocResolver, EnC.ChangeMakerService ChangeMakerService); +internal record struct BaselineArtifacts (Solution BaselineSolution, ProjectId BaselineProjectId, string BaselineOutputAsmPath, DocResolver DocResolver, HotReloadService HotReloadService); diff --git a/src/Microsoft.DotNet.HotReload.Utils.Generator/BaselineProject.cs b/src/Microsoft.DotNet.HotReload.Utils.Generator/BaselineProject.cs index 58f9629fe..1b4b52884 100644 --- a/src/Microsoft.DotNet.HotReload.Utils.Generator/BaselineProject.cs +++ b/src/Microsoft.DotNet.HotReload.Utils.Generator/BaselineProject.cs @@ -7,49 +7,57 @@ using System.IO; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.ExternalAccess.HotReload.Api; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.MSBuild; +using Microsoft.Build.Framework; +using System.Linq; +using System.Collections.Immutable; namespace Microsoft.DotNet.HotReload.Utils.Generator; -public record BaselineProject (Solution Solution, ProjectId ProjectId, EnC.ChangeMakerService ChangeMakerService) { - +internal record BaselineProject (Solution Solution, ProjectId ProjectId, HotReloadService HotReloadService) { public static async Task Make (Config config, EnC.EditAndContinueCapabilities capabilities, CancellationToken ct = default) { (var changeMakerService, var solution, var projectId) = await PrepareMSBuildProject(config, capabilities, ct); return new BaselineProject(solution, projectId, changeMakerService); } - static async Task<(EnC.ChangeMakerService, Solution, ProjectId)> PrepareMSBuildProject (Config config, EnC.EditAndContinueCapabilities capabilities, CancellationToken ct = default) + static async Task<(HotReloadService, Solution, ProjectId)> PrepareMSBuildProject (Config config, EnC.EditAndContinueCapabilities capabilities, CancellationToken ct = default) { - Microsoft.CodeAnalysis.MSBuild.MSBuildWorkspace msw; - // https://stackoverflow.com/questions/43386267/roslyn-project-configuration says I have to specify at least a Configuration property - // to get an output path, is that true? - var props = new Dictionary (config.Properties); - msw = Microsoft.CodeAnalysis.MSBuild.MSBuildWorkspace.Create(props); - msw.LoadMetadataForReferencedProjects = true; - _ = msw.RegisterWorkspaceFailedHandler(diag => { - bool warning = diag.Diagnostic.Kind == WorkspaceDiagnosticKind.Warning; - if (!warning) - Console.WriteLine ($"msbuild failed opening project {config.ProjectPath}"); - Console.WriteLine ($"MSBuildWorkspace {diag.Diagnostic.Kind}: {diag.Diagnostic.Message}"); - if (!warning) - throw new DiffyException ("failed workspace", 1); - }); - Microsoft.Build.Framework.ILogger? logger = null; + // https://stackoverflow.com/questions/43386267/roslyn-project-configuration says I have to specify at least a Configuration property + // to get an output path, is that true? + var props = new Dictionary (config.Properties); + var workspace = MSBuildWorkspace.Create(props); + workspace.LoadMetadataForReferencedProjects = true; + _ = workspace.RegisterWorkspaceFailedHandler(diag => { + bool warning = diag.Diagnostic.Kind == WorkspaceDiagnosticKind.Warning; + if (!warning) + Console.WriteLine ($"msbuild failed opening project {config.ProjectPath}"); + Console.WriteLine ($"MSBuildWorkspace {diag.Diagnostic.Kind}: {diag.Diagnostic.Message}"); + if (!warning) + throw new DiffyException ("failed workspace", 1); + }); + + ILogger? logger = null; #if false - logger = new Microsoft.Build.Logging.BinaryLogger () { - Parameters = "/tmp/enc.binlog" - }; + logger = new Microsoft.Build.Logging.BinaryLogger () { + Parameters = "/tmp/enc.binlog" + }; #endif - var project = await msw.OpenProjectAsync (config.ProjectPath, logger, null, ct); + var project = await workspace.OpenProjectAsync (config.ProjectPath, logger, null, ct); + + var service = new HotReloadService( + workspace.CurrentSolution.Services, + () => new([.. capabilities.ToString().Split(", ")])); - return (EnC.ChangeMakerService.Make (msw.Services, capabilities), msw.CurrentSolution, project.Id); + return (service, workspace.CurrentSolution, project.Id); } public async Task PrepareBaseline (CancellationToken ct = default) { - await ChangeMakerService.StartSessionAsync(Solution, ct); + await HotReloadService.StartSessionAsync(Solution, ct); var project = Solution.GetProject(ProjectId)!; // gets a snapshot of the text of the baseline document in memory @@ -70,7 +78,7 @@ public async Task PrepareBaseline (CancellationToken ct = def BaselineProjectId = ProjectId, BaselineOutputAsmPath = outputAsm, DocResolver = new DocResolver (project), - ChangeMakerService = ChangeMakerService + HotReloadService = HotReloadService }; await t; return artifacts; diff --git a/src/Microsoft.DotNet.HotReload.Utils.Generator/DeltaProject.cs b/src/Microsoft.DotNet.HotReload.Utils.Generator/DeltaProject.cs index 305d08259..00809f649 100644 --- a/src/Microsoft.DotNet.HotReload.Utils.Generator/DeltaProject.cs +++ b/src/Microsoft.DotNet.HotReload.Utils.Generator/DeltaProject.cs @@ -10,21 +10,22 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.ExternalAccess.HotReload.Api; using Microsoft.CodeAnalysis.Text; namespace Microsoft.DotNet.HotReload.Utils.Generator; /// Drives the creation of deltas from textual changes. -public class DeltaProject +internal class DeltaProject { - readonly EnC.ChangeMakerService _changeMakerService; + readonly HotReloadService _hotReloadService; readonly Solution _solution; readonly ProjectId _baseProjectId; readonly DeltaNaming _nextName; public DeltaProject(BaselineArtifacts artifacts) { - _changeMakerService = artifacts.ChangeMakerService; + _hotReloadService = artifacts.HotReloadService; _solution = artifacts.BaselineSolution; _baseProjectId = artifacts.BaselineProjectId; _nextName = new DeltaNaming(artifacts.BaselineOutputAsmPath, 1); @@ -32,7 +33,7 @@ public DeltaProject(BaselineArtifacts artifacts) { internal DeltaProject (DeltaProject prev, Solution newSolution) { - _changeMakerService = prev._changeMakerService; + _hotReloadService = prev._hotReloadService; _solution = newSolution; _baseProjectId = prev._baseProjectId; _nextName = prev._nextName.Next (); @@ -92,28 +93,28 @@ public async Task BuildDelta (Delta delta, bool ignoreUnchanged = Console.WriteLine ($"Found changes in {oldDocument.Name}"); - var updates2 = await _changeMakerService.EmitSolutionUpdateAsync (updatedSolution, ct); + var updates = await _hotReloadService.GetUpdatesAsync (updatedSolution, runningProjects: [], ct); - if (updates2.CompilationDiagnostics.Any(d => d.Severity == DiagnosticSeverity.Error)) { + if (updates.PersistentDiagnostics.Any(d => d.Severity == DiagnosticSeverity.Error)) { var sb = new StringBuilder(); - foreach (var diag in updates2.CompilationDiagnostics) { + foreach (var diag in updates.PersistentDiagnostics) { sb.AppendLine (diag.ToString ()); } throw new DiffyException ($"Failed to emit delta for {oldDocument.Name}: {sb}", exitStatus: 8); } - foreach (var fancyChange in updates2.ProjectUpdates) + foreach (var fancyChange in updates.ProjectUpdates) { Console.WriteLine("change service made {0}", fancyChange.ModuleId); } - _changeMakerService.CommitUpdate(); + _hotReloadService.CommitUpdate(); await using (var output = makeOutputs != null ? makeOutputs(dinfo) : DefaultMakeFileOutputs(dinfo)) { - if (updates2.ProjectUpdates.Length != 1) { - throw new DiffyException($"Expected only one module in the delta, got {updates2.ProjectUpdates.Length}", exitStatus: 10); + if (updates.ProjectUpdates.Length != 1) { + throw new DiffyException($"Expected only one module in the delta, got {updates.ProjectUpdates.Length}", exitStatus: 10); } - var update = updates2.ProjectUpdates.First(); + var update = updates.ProjectUpdates.First(); output.MetaStream.Write(update.MetadataDelta.AsSpan()); output.IlStream.Write(update.ILDelta.AsSpan()); output.PdbStream.Write(update.PdbDelta.AsSpan()); diff --git a/src/Microsoft.DotNet.HotReload.Utils.Generator/EnC/ChangeMaker.cs b/src/Microsoft.DotNet.HotReload.Utils.Generator/EnC/ChangeMaker.cs deleted file mode 100644 index 8fa2009fc..000000000 --- a/src/Microsoft.DotNet.HotReload.Utils.Generator/EnC/ChangeMaker.cs +++ /dev/null @@ -1,51 +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; -using System.Runtime.Loader; -using System.Reflection; - -namespace Microsoft.DotNet.HotReload.Utils.Generator.EnC; - -// -// Inspired by https://github.com/dotnet/roslyn/issues/8962 -public class ChangeMaker { - private const string codeAnalysisFeaturesAssemblyName = "Microsoft.CodeAnalysis.Features"; - - private const string capabilitiesTypeName = "Microsoft.CodeAnalysis.EditAndContinue.EditAndContinueCapabilities"; - - readonly record struct Reflected (Type Capabilities); - - private readonly Reflected _reflected; - - public Type EditAncContinueCapabilitiesType => _reflected.Capabilities; - - public ChangeMaker () { - _reflected = ReflectionInit(); - } - // Get all the Roslyn stuff we need - private static Reflected ReflectionInit () - { - - var an = new AssemblyName(codeAnalysisFeaturesAssemblyName); - var assm = AssemblyLoadContext.Default.LoadFromAssemblyName(an); - - var caps = assm.GetType (capabilitiesTypeName); - - if (caps == null) { - throw new Exception ("Couldn't find EditAndContinueCapabilities type"); - } - - return new Reflected() { Capabilities = caps, - }; - } - - /// Convert my EditAndContinueCapabilities enum value to - /// [Microsoft.CodeAnalysis.Features]Microsoft.CodeAnalysis.EditAndContinue.EditAndContinueCapabilities - public object ConvertCapabilities (EditAndContinueCapabilities myCaps) - { - int i = (int)myCaps; - object theirCaps = Enum.ToObject(_reflected.Capabilities, i); - return theirCaps; - } -} diff --git a/src/Microsoft.DotNet.HotReload.Utils.Generator/EnC/ChangeMakerService.cs b/src/Microsoft.DotNet.HotReload.Utils.Generator/EnC/ChangeMakerService.cs deleted file mode 100644 index 47c0acafe..000000000 --- a/src/Microsoft.DotNet.HotReload.Utils.Generator/EnC/ChangeMakerService.cs +++ /dev/null @@ -1,249 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Reflection; -using System.Runtime.Loader; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Host; - -namespace Microsoft.DotNet.HotReload.Utils.Generator.EnC; - -public class ChangeMakerService -{ - private const string csharpCodeAnalysisAssemblyName = "Microsoft.CodeAnalysis.Features"; - - private const string watchServiceName = "Microsoft.CodeAnalysis.ExternalAccess.Watch.Api.WatchHotReloadService"; - private readonly Type _watchServiceType; - private readonly object _watchHotReloadService; - private ChangeMakerService(Type watchServiceType, object watchHotReloadService) - { - _watchServiceType = watchServiceType; - _watchHotReloadService = watchHotReloadService; - } - - public static ChangeMakerService Make (HostWorkspaceServices hostWorkspaceServices, EditAndContinueCapabilities capabilities) { - ImmutableArray caps = CapabilitiesToStrings(capabilities); - Console.WriteLine("initializing ChangeMakerService with capabilities: " + string.Join(", ", caps)); - (var watchServiceType, var watchHotReloadService) = InstantiateWatchHotReloadService(hostWorkspaceServices, caps); - return new ChangeMakerService(watchServiceType, watchHotReloadService); - } - - public readonly record struct Update (Guid ModuleId, ImmutableArray ILDelta, ImmutableArray MetadataDelta, ImmutableArray PdbDelta, ImmutableArray UpdatedTypes); - - public enum Status - { - /// - /// No significant changes made that need to be applied. - /// - NoChangesToApply, - - /// - /// Changes can be applied either via updates or restart. - /// - ReadyToApply, - - /// - /// Some changes are errors that block rebuild of the module. - /// This means that the code is in a broken state that cannot be resolved by restarting the application. - /// - Blocked, - } - - public readonly struct Updates2 - { - /// - /// Status of the updates. - /// - public readonly Status Status { get; init; } - - /// - /// Syntactic, semantic and emit diagnostics. - /// - /// - /// is if these diagnostics contain any errors. - /// - public required ImmutableArray CompilationDiagnostics { get; init; } - - /// - /// Rude edits per project. - /// - public required ImmutableArray<(ProjectId project, ImmutableArray diagnostics)> RudeEdits { get; init; } - - /// - /// Updates to be applied to modules. Empty if there are blocking rude edits. - /// Only updates to projects that are not included in are listed. - /// - public ImmutableArray ProjectUpdates { get; init; } - - /// - /// Running projects that need to be restarted due to rude edits in order to apply changes. - /// - public ImmutableDictionary> ProjectsToRestart { get; init; } - - /// - /// Projects with changes that need to be rebuilt in order to apply changes. - /// - public ImmutableArray ProjectsToRebuild { get; init; } - } - - private static ImmutableArray CapabilitiesToStrings(EditAndContinueCapabilities capabilities) - { - var builder = ImmutableArray.CreateBuilder(); - var names = Enum.GetNames(typeof(EditAndContinueCapabilities)); - foreach (var name in names) - { - var val = Enum.Parse(name); - if (val == EditAndContinueCapabilities.None) - continue; - if (capabilities.HasFlag(val)) - { - builder.Add(name); - } - } - return builder.ToImmutable(); - } - - private static Update WrapUpdate (object update) - { - var updateType = update.GetType()!; - var moduleId = updateType.GetField("ModuleId")!.GetValue(update)!; - var ilDelta = updateType.GetField("ILDelta")!.GetValue(update)!; - var metadataDelta = updateType.GetField("MetadataDelta")!.GetValue(update)!; - var pdbDelta = updateType.GetField("PdbDelta")!.GetValue(update)!; - var updatedTypes = updateType.GetField("UpdatedTypes")!.GetValue(update)!; - return new Update((Guid)moduleId, (ImmutableArray)ilDelta, (ImmutableArray)metadataDelta, (ImmutableArray)pdbDelta, (ImmutableArray)updatedTypes); - - } - - private static Updates2 WrapUpdates(object updates) - { - return new Updates2 - { - Status = (Status)updates.GetType().GetProperty("Status")!.GetValue(updates)!, - CompilationDiagnostics = (ImmutableArray)updates.GetType().GetProperty("CompilationDiagnostics")!.GetValue(updates)!, - RudeEdits = (ImmutableArray<(ProjectId, ImmutableArray)>)updates.GetType().GetProperty("RudeEdits")!.GetValue(updates)!, - ProjectUpdates = [.. ((IEnumerable)updates.GetType().GetProperty("ProjectUpdates")!.GetValue(updates)!).Cast().Select(WrapUpdate)], - ProjectsToRestart = (ImmutableDictionary>)updates.GetType().GetProperty("ProjectsToRestart")!.GetValue(updates)!, - ProjectsToRebuild = (ImmutableArray)updates.GetType().GetProperty("ProjectsToRebuild")!.GetValue(updates)! - }; - } - public static (Type, object) InstantiateWatchHotReloadService(HostWorkspaceServices hostWorkspaceServices, ImmutableArray capabilities) - { - var an = new AssemblyName(csharpCodeAnalysisAssemblyName); - var assm = AssemblyLoadContext.Default.LoadFromAssemblyName(an); - if (assm == null) { - throw new Exception($"could not load assembly {an}"); - } - var type = assm.GetType(watchServiceName); - if (type == null) { - throw new Exception($"could not load type {watchServiceName}"); - } - var argTys = new Type[] { typeof(HostWorkspaceServices), typeof(ImmutableArray) }; - var ctor = type.GetConstructor(argTys); - if (ctor == null) { - throw new Exception ($"could not find ctor {watchServiceName} ({argTys[0]}, {argTys[1]})"); - } - object service = ctor!.Invoke(new object[] { hostWorkspaceServices, capabilities })!; - return (type, service); - } - - public Task StartSessionAsync (Solution solution, CancellationToken ct = default) - { - var mi = _watchServiceType.GetMethod("StartSessionAsync"); - if (mi == null) { - throw new Exception($"could not find method {watchServiceName}.StartSessionAsync"); - } - return (Task)mi.Invoke(_watchHotReloadService, new object[] { solution, ct })!; - } - - public void EndSession () - { - var mi = _watchServiceType.GetMethod("EndSession"); - if (mi == null) { - throw new Exception($"could not find method {watchServiceName}.EndSession"); - } - mi.Invoke(_watchHotReloadService, Array.Empty()); - } - - public void CommitUpdate() - { - var mi = _watchServiceType.GetMethod("CommitUpdate"); - if (mi == null) - { - throw new Exception($"could not find method {watchServiceName}.CommitUpdate"); - } - mi.Invoke(_watchHotReloadService, Array.Empty()); - } - - public Task EmitSolutionUpdateAsync(Solution solution, CancellationToken cancellationToken) - { - var runningProjectInfoType = _watchServiceType.GetNestedType("RunningProjectInfo", BindingFlags.Public)!; - var immutableDictionaryCreateRangeMethod = typeof(ImmutableDictionary).GetMethod( - "CreateRange", - BindingFlags.Static | BindingFlags.Public, - [ typeof(IEnumerable<>).MakeGenericType(Type.MakeGenericSignatureType(typeof(KeyValuePair<,>), Type.MakeGenericMethodParameter(0), Type.MakeGenericMethodParameter(1))) ])! - .MakeGenericMethod(typeof(ProjectId), runningProjectInfoType); - - var mi = _watchServiceType.GetMethod("GetUpdatesAsync", BindingFlags.Public | BindingFlags.Instance, [typeof(Solution), immutableDictionaryCreateRangeMethod.ReturnType, typeof(CancellationToken)]); - if (mi == null) { - throw new Exception($"could not find method {watchServiceName}.GetUpdatesAsync"); - } - - var kvpProjectIdRunningProjectInfoType = typeof(KeyValuePair<,>).MakeGenericType(typeof(ProjectId), runningProjectInfoType); - - var runningProjectsList = Array.CreateInstance(kvpProjectIdRunningProjectInfoType, solution.ProjectIds.Count); - - for (int i = 0; i < solution.ProjectIds.Count; i++) - { - var projectId = solution.ProjectIds[i]; - var runningProjectInfo = Activator.CreateInstance(runningProjectInfoType)!; - runningProjectsList.SetValue(Activator.CreateInstance(kvpProjectIdRunningProjectInfoType, projectId, runningProjectInfo)!, i); - } - - object resultTask = mi.Invoke(_watchHotReloadService, [solution, immutableDictionaryCreateRangeMethod.Invoke(null, [ runningProjectsList ])!, cancellationToken])!; - - // The task returns Updates2, except that - // the Update2 type is a nested struct in WatchHotReloadService, so we can't write the type directly. - // Instead we take apart the type and convert the first component to our own Update2 type. - // - // We basically want to do - // resultTask.ContinueWith ((t) => WrapUpdate(t.Result)); - // but then we need to make a Func that again mentions the internal Update type. - // - // So instead we do: - // - // var tcs = new TaskCompletionSource<...>(); - // var awaiter = resultTask.GetAwaiter(); - // awaiter.OnCompleted(delegate { - // object result = awaiter.GetResult(); - // tcs.SetResult(Wrap (result)); - // }); - // return tcs.Task; - // - // because OnCompleted only needs an Action and we can use reflection to take the result apart - - - var tcs = new TaskCompletionSource(); - - var awaiter = resultTask.GetType().GetMethod("GetAwaiter")!.Invoke(resultTask, Array.Empty())!; - - Action continuation = delegate { - try { - var result = awaiter.GetType().GetMethod("GetResult")!.Invoke(awaiter, Array.Empty())!; - tcs.SetResult (WrapUpdates (result)); - } catch (TaskCanceledException e) { - tcs.TrySetCanceled(e.CancellationToken); - } catch (Exception e) { - tcs.TrySetException (e); - } - }; - - awaiter.GetType().GetMethod("OnCompleted")!.Invoke(awaiter, new object[] { continuation }); - - return tcs.Task; - } -} diff --git a/src/Microsoft.DotNet.HotReload.Utils.Generator/Microsoft.DotNet.HotReload.Utils.Generator.csproj b/src/Microsoft.DotNet.HotReload.Utils.Generator/Microsoft.DotNet.HotReload.Utils.Generator.csproj index 620a62b84..e9692f59b 100644 --- a/src/Microsoft.DotNet.HotReload.Utils.Generator/Microsoft.DotNet.HotReload.Utils.Generator.csproj +++ b/src/Microsoft.DotNet.HotReload.Utils.Generator/Microsoft.DotNet.HotReload.Utils.Generator.csproj @@ -2,6 +2,7 @@ + diff --git a/src/Microsoft.DotNet.HotReload.Utils.Generator/Runner.cs b/src/Microsoft.DotNet.HotReload.Utils.Generator/Runner.cs index 1942b9954..54368ece5 100644 --- a/src/Microsoft.DotNet.HotReload.Utils.Generator/Runner.cs +++ b/src/Microsoft.DotNet.HotReload.Utils.Generator/Runner.cs @@ -10,7 +10,12 @@ namespace Microsoft.DotNet.HotReload.Utils.Generator; public abstract class Runner { + readonly protected Config config; + protected Runner(Config config) + { + this.config = config; + } public static Runner Make (Config config) { @@ -19,6 +24,7 @@ public static Runner Make (Config config) else return new Runners.ScriptRunner (config); } + public async Task Run (CancellationToken ct = default) { await PrepareToRun(ct); var capabilities = PrepareCapabilities(); @@ -33,11 +39,6 @@ public async Task Run (CancellationToken ct = default) { await OutputsDone (ct); } - readonly protected Config config; - protected Runner (Config config) { - this.config = config; - } - /// Delegate that is called to create the delta output streams. /// If not set, a default is used that writes the deltas to files. protected Func? MakeOutputs {get; set; } = null; @@ -49,8 +50,8 @@ protected Runner (Config config) { /// Called when all the outputs have been emitted. protected Func? OutputsDone {get; set;} = null; - public async Task SetupBaseline (EnC.EditAndContinueCapabilities capabilities, CancellationToken ct = default) { - BaselineProject baselineProject = await Microsoft.DotNet.HotReload.Utils.Generator.BaselineProject.Make (config, capabilities, ct); + private async Task SetupBaseline (EnC.EditAndContinueCapabilities capabilities, CancellationToken ct = default) { + BaselineProject baselineProject = await BaselineProject.Make (config, capabilities, ct); var baselineArtifacts = await baselineProject.PrepareBaseline(ct); @@ -100,9 +101,10 @@ protected EnC.EditAndContinueCapabilities DefaultCapabilities () ; return allCaps; } - public abstract IAsyncEnumerable SetupDeltas (BaselineArtifacts baselineArtifacts, CancellationToken ct = default); - public async Task GenerateDeltas (DeltaProject deltaProject, IAsyncEnumerable deltas, + private protected abstract IAsyncEnumerable SetupDeltas (BaselineArtifacts baselineArtifacts, CancellationToken ct = default); + + private async Task GenerateDeltas (DeltaProject deltaProject, IAsyncEnumerable deltas, Func? makeOutputs = null, Action? outputsReady = null, CancellationToken ct = default) diff --git a/src/Microsoft.DotNet.HotReload.Utils.Generator/Runners/LiveRunner.cs b/src/Microsoft.DotNet.HotReload.Utils.Generator/Runners/LiveRunner.cs index 72a479201..256a58273 100644 --- a/src/Microsoft.DotNet.HotReload.Utils.Generator/Runners/LiveRunner.cs +++ b/src/Microsoft.DotNet.HotReload.Utils.Generator/Runners/LiveRunner.cs @@ -11,11 +11,12 @@ namespace Microsoft.DotNet.HotReload.Utils.Generator.Runners; /// Generate deltas by watching for changes to the source files of the project -public class LiveRunner : Runner { +internal class LiveRunner : Runner { public LiveRunner (Config config) : base (config) { } protected override Task PrepareToRun (CancellationToken ct = default) => Task.CompletedTask; - public override IAsyncEnumerable SetupDeltas (BaselineArtifacts baselineArtifacts, CancellationToken ct = default) + + private protected override IAsyncEnumerable SetupDeltas (BaselineArtifacts baselineArtifacts, CancellationToken ct = default) { return Livecoding (baselineArtifacts, config.LiveCodingWatchDir, config.LiveCodingWatchPattern, ct); } diff --git a/src/Microsoft.DotNet.HotReload.Utils.Generator/Runners/ScriptRunner.cs b/src/Microsoft.DotNet.HotReload.Utils.Generator/Runners/ScriptRunner.cs index 31f0f16bb..1f6a56b8e 100644 --- a/src/Microsoft.DotNet.HotReload.Utils.Generator/Runners/ScriptRunner.cs +++ b/src/Microsoft.DotNet.HotReload.Utils.Generator/Runners/ScriptRunner.cs @@ -15,7 +15,7 @@ namespace Microsoft.DotNet.HotReload.Utils.Generator.Runners; /// Generate deltas by reading a script from a configuration file /// listing the changed versions of the project source files. -public class ScriptRunner : Runner { +internal class ScriptRunner : Runner { Script.ParsedScript? parsedScript; public ScriptRunner (Config config) : base (config) { @@ -69,7 +69,8 @@ protected override bool PrepareCapabilitiesCore (out EnC.EditAndContinueCapabili } return true; } - public override IAsyncEnumerable SetupDeltas (BaselineArtifacts baselineArtifacts, CancellationToken ct = default) + + private protected override IAsyncEnumerable SetupDeltas (BaselineArtifacts baselineArtifacts, CancellationToken ct = default) { if (parsedScript == null) return Util.AsyncEnumerableExtras.Empty(); diff --git a/tests/Acceptance/EncCapabilitiesCompat.Tests/EncCapabilitiesCompat.cs b/tests/Acceptance/EncCapabilitiesCompat.Tests/EncCapabilitiesCompat.cs deleted file mode 100644 index 437351bfd..000000000 --- a/tests/Acceptance/EncCapabilitiesCompat.Tests/EncCapabilitiesCompat.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; - -using Xunit; - -namespace EncCapabilitiesCompatTest { - public class EncCapabilitiesCompatTest { - - public struct CapDescriptor { - public string Name; - public int Value; - } - - static readonly Microsoft.DotNet.HotReload.Utils.Generator.EnC.ChangeMaker ChangeMaker = new (); - - internal static IReadOnlyList MakeDescriptor (Type ty) { - List l = new (); - if (!ty.IsEnum) { - throw new Exception($"Type {ty} is not an enumeration"); - } - var underlying = ty.GetEnumUnderlyingType(); - if (underlying != typeof(int)) { - throw new Exception($"Underlying type of {ty} is {underlying}, not Int32"); - } - foreach (var enumName in ty.GetEnumNames()) { - if (!Enum.TryParse(ty, enumName, out var v)) - throw new Exception ($"Could not get value of enumeration constant {enumName} of type {ty}"); - if (v == null) - throw new Exception ($"enumeration value of {ty}.{enumName} was null"); - int intVal = (int)v; - l.Add (new CapDescriptor {Name = enumName, Value = intVal}); - } - return l; - } - - public static IReadOnlyList GetGeneratorCapabilities() => MakeDescriptor(typeof(Microsoft.DotNet.HotReload.Utils.Generator.EnC.EditAndContinueCapabilities)); - public static IReadOnlyList GetRoslynCapabilities() => MakeDescriptor(ChangeMaker.EditAncContinueCapabilitiesType); - - [Fact] - public static void SameCapabilities () { - // Check that roslyn and hotreload-utils have the same EnC capabilities defined. - // Should help to keep hotreload-utils up to date when Roslyn pushes changes. - - IReadOnlyList generatorCaps = GetGeneratorCapabilities(); - IReadOnlyList roslynCaps = GetRoslynCapabilities(); - - // TODO: Maybe collect all mismatches and just assert once at the end? - // Or use a [Theory] for each cap that compares it against the whole list from the other set - foreach (var roslynCap in roslynCaps) { - bool found = false; - foreach (var generatorCap in generatorCaps) { - if (roslynCap.Name != generatorCap.Name) - continue; - found = true; - Assert.True (roslynCap.Value == generatorCap.Value, $"Capability {roslynCap.Name} in Roslyn has value {roslynCap.Value} and {generatorCap.Value} in hotreload-utils."); - } - Assert.True (found, $"Capability {roslynCap.Name} in Roslyn with value {roslynCap.Value} not present in hotreload-utils"); - } - - foreach (var generatorCap in generatorCaps) { - bool found = false; - foreach (var roslynCap in roslynCaps) { - if (roslynCap.Name == generatorCap.Name) { - // if it's in both it has the same value, by the previous loop - found = true; - break; - } - } - Assert.True (found, $"Capability {generatorCap.Name} in hotreload-utils with value {generatorCap.Value} not present in Roslyn"); - } - } - } -} diff --git a/tests/HotReload.Generator/EnC.Tests/WatchHotReloadServiceTest.cs b/tests/HotReload.Generator/EnC.Tests/WatchHotReloadServiceTest.cs deleted file mode 100644 index 2338717d1..000000000 --- a/tests/HotReload.Generator/EnC.Tests/WatchHotReloadServiceTest.cs +++ /dev/null @@ -1,74 +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; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; -using CancellationToken = System.Threading.CancellationToken; -using System.Linq; - -using System.Reflection.Metadata; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Emit; -using Microsoft.CodeAnalysis.Text; -using Microsoft.DotNet.HotReload.Utils.Generator.EnC; - -using Xunit; - -namespace EnC.Tests; - -public class WatchHotReloadServiceTest : TempMSBuildWorkspaceTest -{ - public WatchHotReloadServiceTest(GlobalFilesFixture globalFiles) : base(globalFiles) - { - } - - [Fact] - public async Task SanityCheckWatchService() - { - var cancellationToken = CancellationToken.None; - var project = await PrepareProject(cancellationToken); - var src1 = MakeText(""" - using System; - public class C1 { - public static void M1() { - Console.WriteLine("Hello"); - } - } - """); - WithBaselineSource(ref project, "Class1.cs", src1, out var documentId); - var comp = await project.GetCompilationAsync(cancellationToken); - Assert.NotNull(comp); - - using (var peStream = File.OpenWrite(project.CompilationOutputInfo.AssemblyPath!)) - using (var pdbStream = File.OpenWrite(Path.ChangeExtension(project.CompilationOutputInfo.AssemblyPath!, ".pdb"))) - { - var emitResult = comp.Emit(peStream, pdbStream, options: new EmitOptions(debugInformationFormat: DebugInformationFormat.PortablePdb), cancellationToken: TestContext.Current.CancellationToken); - ValidateEmitResult(emitResult); - } - - var hostWorkspaceServices = project.Solution.Workspace.Services; - var changeMakerService = ChangeMakerService.Make(hostWorkspaceServices, EditAndContinueCapabilities.Baseline); - - await changeMakerService.StartSessionAsync(project.Solution, cancellationToken); - - var src2 = MakeText(""" - using System; - public class C1 { - public static void M1() { - Console.WriteLine("Updated"); - } - } - """); - - var newSolution = project.Solution.WithDocumentText(documentId, src2); - - var update = await changeMakerService.EmitSolutionUpdateAsync(newSolution, cancellationToken); - Assert.NotEmpty(update.ProjectUpdates); - - changeMakerService.CommitUpdate(); - - changeMakerService.EndSession(); - } -} From 027da255cd16e5924db11c6eb74475979157cb94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20K=C3=B6plinger?= Date: Tue, 28 Oct 2025 17:52:12 +0100 Subject: [PATCH 2/3] Update Roslyn to version from https://github.com/dotnet/hotreload-utils/pull/634 --- eng/Version.Details.xml | 4 ++-- eng/Versions.props | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index bab54468c..65a4b120a 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,9 +1,9 @@ - + https://github.com/dotnet/roslyn - 9eb62b62a35c66d3a14862defa876a27ac2e2bb9 + e7d28d80af8c576206dacf05e09cde03a105fb38 diff --git a/eng/Versions.props b/eng/Versions.props index 43a5dab27..d70321b82 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -10,7 +10,7 @@ false - 5.1.0-1.25501.2 + 5.3.0-1.25524.13 11.0.0-beta.25477.2 11.0.0-beta.25477.2 From c3e4a8b0a83cca9f32b1b72b92509f9fd8332530 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20K=C3=B6plinger?= Date: Tue, 28 Oct 2025 18:13:07 +0100 Subject: [PATCH 3/3] Remove unnecessary test projects that run zero tests now --- .../EncCapabilitiesCompat.Tests.csproj | 15 -- .../EnC.Tests/Assembly.NoParallelism.cs | 7 - .../EnC.Tests/CommonFixtures.cs | 32 ----- .../EnC.Tests/EnC.Tests.csproj | 25 ---- .../EnC.Tests/TempMSBuildWorkspaceTest.cs | 133 ------------------ tests/HotReload.Generator/README.md | 7 - 6 files changed, 219 deletions(-) delete mode 100644 tests/Acceptance/EncCapabilitiesCompat.Tests/EncCapabilitiesCompat.Tests.csproj delete mode 100644 tests/HotReload.Generator/EnC.Tests/Assembly.NoParallelism.cs delete mode 100644 tests/HotReload.Generator/EnC.Tests/CommonFixtures.cs delete mode 100644 tests/HotReload.Generator/EnC.Tests/EnC.Tests.csproj delete mode 100644 tests/HotReload.Generator/EnC.Tests/TempMSBuildWorkspaceTest.cs delete mode 100644 tests/HotReload.Generator/README.md diff --git a/tests/Acceptance/EncCapabilitiesCompat.Tests/EncCapabilitiesCompat.Tests.csproj b/tests/Acceptance/EncCapabilitiesCompat.Tests/EncCapabilitiesCompat.Tests.csproj deleted file mode 100644 index 1f3dfc322..000000000 --- a/tests/Acceptance/EncCapabilitiesCompat.Tests/EncCapabilitiesCompat.Tests.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - Exe - - - - - - - - - - - diff --git a/tests/HotReload.Generator/EnC.Tests/Assembly.NoParallelism.cs b/tests/HotReload.Generator/EnC.Tests/Assembly.NoParallelism.cs deleted file mode 100644 index ac914968b..000000000 --- a/tests/HotReload.Generator/EnC.Tests/Assembly.NoParallelism.cs +++ /dev/null @@ -1,7 +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 Xunit; - -// MSBuildWorkspace uses MSBuild which can't have more than one build going at a time. -[assembly: CollectionBehavior(DisableTestParallelization = true)] diff --git a/tests/HotReload.Generator/EnC.Tests/CommonFixtures.cs b/tests/HotReload.Generator/EnC.Tests/CommonFixtures.cs deleted file mode 100644 index 7dbe496cd..000000000 --- a/tests/HotReload.Generator/EnC.Tests/CommonFixtures.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; - -namespace EnC.Tests; - -public class GlobalFilesFixture : IDisposable -{ - public string GlobalJsonContents { get; } - public string NugetConfigContents { get; } - - public GlobalFilesFixture() - { - using (var s = typeof(GlobalFilesFixture).Assembly.GetManifestResourceStream("projectData/global.json")) - { - if (s == null) - throw new Exception("Couldn't get global.json"); - using var sr = new System.IO.StreamReader(s); - GlobalJsonContents = sr.ReadToEnd(); - } - using (var s = typeof(GlobalFilesFixture).Assembly.GetManifestResourceStream("projectData/NuGet.config")) - { - if (s == null) - throw new Exception("Couldn't get NuGet.config"); - using var sr = new System.IO.StreamReader(s); - NugetConfigContents = sr.ReadToEnd(); - } - } - - public void Dispose() - { - } - -} diff --git a/tests/HotReload.Generator/EnC.Tests/EnC.Tests.csproj b/tests/HotReload.Generator/EnC.Tests/EnC.Tests.csproj deleted file mode 100644 index 3d63fb204..000000000 --- a/tests/HotReload.Generator/EnC.Tests/EnC.Tests.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - - Exe - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/HotReload.Generator/EnC.Tests/TempMSBuildWorkspaceTest.cs b/tests/HotReload.Generator/EnC.Tests/TempMSBuildWorkspaceTest.cs deleted file mode 100644 index dffc4f77f..000000000 --- a/tests/HotReload.Generator/EnC.Tests/TempMSBuildWorkspaceTest.cs +++ /dev/null @@ -1,133 +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; -using System.IO; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Emit; -using Microsoft.CodeAnalysis.MSBuild; - -using CancellationToken = System.Threading.CancellationToken; -using SourceText = Microsoft.CodeAnalysis.Text.SourceText; -using TempDirectory = Microsoft.DotNet.HotReload.Utils.Common.TempDirectory; - -using Xunit; - -namespace EnC.Tests; - -/// Each test gets its own temporary directory and MSBuildWorkspace -/// -/// The temporary directory gets a global.json and NuGet.config like the root of this git repo. -/// Each test gets its own MSBuildWorkspace, which is disposed after the test. -/// -/// The temp directory is normally deleted after the test is done. -/// Set TempDir.Keep = true if you want to keep the temp directory. -/// -/// -public class TempMSBuildWorkspaceTest : IClassFixture, IDisposable -{ - public MSBuildWorkspace Workspace { get; } - public GlobalFilesFixture GlobalFiles { get; } - private protected TempDirectory TempDir { get; } - public TempMSBuildWorkspaceTest(GlobalFilesFixture globalFiles) - { - GlobalFiles = globalFiles; - Workspace = MSBuildWorkspace.Create(); - _ = Workspace.RegisterWorkspaceFailedHandler(OnWorkspaceFailed); - TempDir = new TempDirectory(); - } - - public virtual void OnWorkspaceFailed(WorkspaceDiagnosticEventArgs e) - { - if (e.Diagnostic.Kind == WorkspaceDiagnosticKind.Warning) - return; - throw new Exception($"MSBuildWorkspace {e.Diagnostic.Kind}: {e.Diagnostic.Message}"); - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - Workspace?.Dispose(); - TempDir?.Dispose(); - } - } - - private void PrepareGlobalFiles() - { - string globalJsonPath = Path.Combine(TempDir.Path, "global.json"); - using (var globalJson = new StreamWriter(globalJsonPath)) - { - globalJson.Write(GlobalFiles.GlobalJsonContents); - } - string nugetConfigPath = Path.Combine(TempDir.Path, "NuGet.config"); - using (var nugetConfig = new StreamWriter(nugetConfigPath)) - { - nugetConfig.Write(GlobalFiles.NugetConfigContents); - } - } - - private async Task<(Solution, ProjectId)> CreateProject(string projectContent, CancellationToken cancellationToken = default) - { - PrepareGlobalFiles(); - string projectPath = Path.Combine(TempDir.Path, "project.csproj"); - using (var projectFile = new StreamWriter(projectPath)) - { - projectFile.Write(projectContent); - projectFile.Flush(); - } - var project = await Workspace.OpenProjectAsync(projectPath, cancellationToken: cancellationToken); - return (Workspace.CurrentSolution, project.Id); - } - - protected SourceText MakeText(string text) => SourceText.From(text, System.Text.Encoding.UTF8); - - protected async Task PrepareProject(CancellationToken cancellationToken = default) - { - string targetFramework = $"net{Environment.Version.Major}.{Environment.Version.Minor}"; - (var solution, var projectId) = await CreateProject($""" - - - Library - {targetFramework} - false - - - - - - """, cancellationToken); - Assert.NotNull(solution); - var project = solution.GetProject(projectId); - Assert.NotNull(project); - return project; - } - - protected void WithBaselineSource(ref Project project, string csFileName, SourceText src, out DocumentId docId) - { - var d = project.AddDocument(csFileName, src, filePath: Path.Combine(project.FilePath!, csFileName)); - project = d.Project; - docId = d.Id; - } - - protected static void ValidateEmitResult(EmitResult emitResult, [CallerMemberName] string? caller = null, [CallerLineNumber] int? line = null, [CallerFilePath] string? file = null) - { - if (!emitResult.Success) - { - foreach (var diag in emitResult.Diagnostics) - { - Console.WriteLine(diag); - } - Assert.Fail($"{file}:{line} EmitResult failed in {caller}"); - } - } - -} diff --git a/tests/HotReload.Generator/README.md b/tests/HotReload.Generator/README.md deleted file mode 100644 index 6bf6205cf..000000000 --- a/tests/HotReload.Generator/README.md +++ /dev/null @@ -1,7 +0,0 @@ - -# Tests for the Microsoft.DotNet.HotReload.Utils.Generator assembly - -Contents: - -- **EnC.Tests**: Unit tests for our wrappers around the Roslyn EnC APIs. - Primary goal is to have canaries for unexpected Roslyn API changes, since we're using private reflection.