diff --git a/src/BuiltInTools/Watch/HotReload/IncrementalMSBuildWorkspace.cs b/src/BuiltInTools/Watch/HotReload/IncrementalMSBuildWorkspace.cs index 2fc7e54a9b34..1581a57f3c66 100644 --- a/src/BuiltInTools/Watch/HotReload/IncrementalMSBuildWorkspace.cs +++ b/src/BuiltInTools/Watch/HotReload/IncrementalMSBuildWorkspace.cs @@ -3,6 +3,7 @@ using System.Collections.Immutable; using System.Diagnostics; +using System.Reflection; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.ExternalAccess.HotReload.Api; using Microsoft.CodeAnalysis.Host.Mef; @@ -76,7 +77,7 @@ public async Task UpdateProjectConeAsync(string rootProjectPath, CancellationTok continue; } - newSolution = HotReloadService.WithProjectInfo(newSolution, ProjectInfo.Create( + newSolution = HotReloadService.WithProjectInfo(newSolution, WithChecksumAlgorithm(ProjectInfo.Create( oldProjectId, newProjectInfo.Version, newProjectInfo.Name, @@ -93,7 +94,8 @@ public async Task UpdateProjectConeAsync(string rootProjectPath, CancellationTok MapDocuments(oldProjectId, newProjectInfo.AdditionalDocuments), isSubmission: false, hostObjectType: null, - outputRefFilePath: newProjectInfo.OutputRefFilePath) + outputRefFilePath: newProjectInfo.OutputRefFilePath), + GetChecksumAlgorithm(newProjectInfo)) .WithAnalyzerConfigDocuments(MapDocuments(oldProjectId, newProjectInfo.AnalyzerConfigDocuments)) .WithCompilationOutputInfo(newProjectInfo.CompilationOutputInfo)); } @@ -122,6 +124,20 @@ ImmutableArray MapDocuments(ProjectId mappedProjectId, IReadOnlyLi }).ToImmutableArray(); } + // TODO: remove + // workaround for https://github.com/dotnet/roslyn/pull/82051 + + private static MethodInfo? s_withChecksumAlgorithm; + private static PropertyInfo? s_getChecksumAlgorithm; + + private static ProjectInfo WithChecksumAlgorithm(ProjectInfo info, SourceHashAlgorithm algorithm) + => (ProjectInfo)(s_withChecksumAlgorithm ??= typeof(ProjectInfo).GetMethod("WithChecksumAlgorithm", BindingFlags.NonPublic | BindingFlags.Instance)!) + .Invoke(info, [algorithm])!; + + private static SourceHashAlgorithm GetChecksumAlgorithm(ProjectInfo info) + => (SourceHashAlgorithm)(s_getChecksumAlgorithm ??= typeof(ProjectInfo).GetProperty("ChecksumAlgorithm", BindingFlags.NonPublic | BindingFlags.Instance)!) + .GetValue(info)!; + public async ValueTask UpdateFileContentAsync(IEnumerable changedFiles, CancellationToken cancellationToken) { var updatedSolution = CurrentSolution; diff --git a/src/Cli/dotnet/Commands/Test/MTP/MicrosoftTestingPlatformTestCommand.cs b/src/Cli/dotnet/Commands/Test/MTP/MicrosoftTestingPlatformTestCommand.cs index 63d307b39fd6..2883069e62e3 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/MicrosoftTestingPlatformTestCommand.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/MicrosoftTestingPlatformTestCommand.cs @@ -80,7 +80,13 @@ private int RunInternal(ParseResult parseResult, bool isHelp) actionQueue.EnqueueCompleted(); // Don't inline exitCode variable. We want to always call WaitAllActions first. var exitCode = actionQueue.WaitAllActions(); - exitCode = _output.HasHandshakeFailure ? ExitCode.GenericFailure : exitCode; + + // If all test apps exited with 0 exit code, but we detected that handshake didn't happen correctly, map that to generic failure. + if (exitCode == ExitCode.Success && _output.HasHandshakeFailure) + { + exitCode = ExitCode.GenericFailure; + } + if (exitCode == ExitCode.Success && parseResult.HasOption(definition.MinimumExpectedTestsOption) && parseResult.GetValue(definition.MinimumExpectedTestsOption) is { } minimumExpectedTests && @@ -105,12 +111,23 @@ private void InitializeOutput(int degreeOfParallelism, ParseResult parseResult, // TODO: Replace this with proper CI detection that we already have in telemetry. https://github.com/microsoft/testfx/issues/5533#issuecomment-2838893327 bool inCI = string.Equals(Environment.GetEnvironmentVariable("TF_BUILD"), "true", StringComparison.OrdinalIgnoreCase) || string.Equals(Environment.GetEnvironmentVariable("GITHUB_ACTIONS"), "true", StringComparison.OrdinalIgnoreCase); + AnsiMode ansiMode = AnsiMode.AnsiIfPossible; + if (noAnsi) + { + // User explicitly specified --no-ansi. + // We should respect that. + ansiMode = AnsiMode.NoAnsi; + } + else if (inCI) + { + ansiMode = AnsiMode.SimpleAnsi; + } + _output = new TerminalTestReporter(console, new TerminalTestReporterOptions() { ShowPassedTests = showPassedTests, ShowProgress = !noProgress, - UseAnsi = !noAnsi, - UseCIAnsi = inCI, + AnsiMode = ansiMode, ShowAssembly = true, ShowAssemblyStartAndComplete = true, MinimumExpectedTests = parseResult.GetValue(definition.MinimumExpectedTestsOption), diff --git a/src/Cli/dotnet/Commands/Test/MTP/Terminal/TerminalTestReporter.cs b/src/Cli/dotnet/Commands/Test/MTP/Terminal/TerminalTestReporter.cs index 3ce010a078f0..ca8ea35e2d57 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/Terminal/TerminalTestReporter.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/Terminal/TerminalTestReporter.cs @@ -1,6 +1,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Concurrent; +using System.Diagnostics; using System.Globalization; using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; @@ -64,36 +65,34 @@ internal sealed partial class TerminalTestReporter : IDisposable public TerminalTestReporter(IConsole console, TerminalTestReporterOptions options) { _options = options; - TestProgressStateAwareTerminal terminalWithProgress; + bool showProgress = _options.ShowProgress; - // When not writing to ANSI we write the progress to screen and leave it there so we don't want to write it more often than every few seconds. - int nonAnsiUpdateCadenceInMs = 3_000; - // When writing to ANSI we update the progress in place and it should look responsive so we update every half second, because we only show seconds on the screen, so it is good enough. - int ansiUpdateCadenceInMs = 500; - if (!_options.UseAnsi) + ITerminal terminal; + if (_options.AnsiMode == AnsiMode.SimpleAnsi) { - terminalWithProgress = new TestProgressStateAwareTerminal(new NonAnsiTerminal(console), _options.ShowProgress, writeProgressImmediatelyAfterOutput: false, updateEvery: nonAnsiUpdateCadenceInMs); + // We are told externally that we are in CI, use simplified ANSI mode. + terminal = new SimpleAnsiTerminal(console); + showProgress = false; } else { - if (_options.UseCIAnsi) - { - // We are told externally that we are in CI, use simplified ANSI mode. - terminalWithProgress = new TestProgressStateAwareTerminal(new SimpleAnsiTerminal(console), _options.ShowProgress, writeProgressImmediatelyAfterOutput: true, updateEvery: nonAnsiUpdateCadenceInMs); - } - else + // We are not in CI, or in CI non-compatible with simple ANSI, autodetect terminal capabilities + // Autodetect. + (bool consoleAcceptsAnsiCodes, bool _, uint? originalConsoleMode) = NativeMethods.QueryIsScreenAndTryEnableAnsiColorCodes(); + _originalConsoleMode = originalConsoleMode; + + bool useAnsi = _options.AnsiMode switch { - // We are not in CI, or in CI non-compatible with simple ANSI, autodetect terminal capabilities - // Autodetect. - (bool consoleAcceptsAnsiCodes, bool _, uint? originalConsoleMode) = NativeMethods.QueryIsScreenAndTryEnableAnsiColorCodes(); - _originalConsoleMode = originalConsoleMode; - terminalWithProgress = consoleAcceptsAnsiCodes - ? new TestProgressStateAwareTerminal(new AnsiTerminal(console, _options.BaseDirectory), _options.ShowProgress, writeProgressImmediatelyAfterOutput: true, updateEvery: ansiUpdateCadenceInMs) - : new TestProgressStateAwareTerminal(new NonAnsiTerminal(console), _options.ShowProgress, writeProgressImmediatelyAfterOutput: false, updateEvery: nonAnsiUpdateCadenceInMs); - } + AnsiMode.NoAnsi => false, + AnsiMode.AnsiIfPossible => consoleAcceptsAnsiCodes, + _ => throw new UnreachableException(), + }; + + showProgress = showProgress && useAnsi; + terminal = useAnsi ? new AnsiTerminal(console, _options.BaseDirectory) : new NonAnsiTerminal(console); } - _terminalWithProgress = terminalWithProgress; + _terminalWithProgress = new TestProgressStateAwareTerminal(terminal, showProgress); } public void TestExecutionStarted(DateTimeOffset testStartTime, int workerCount, bool isDiscovery, bool isHelp, bool isRetry) diff --git a/src/Cli/dotnet/Commands/Test/MTP/Terminal/TerminalTestReporterOptions.cs b/src/Cli/dotnet/Commands/Test/MTP/Terminal/TerminalTestReporterOptions.cs index ebce31bfc35d..7345d637aca4 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/Terminal/TerminalTestReporterOptions.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/Terminal/TerminalTestReporterOptions.cs @@ -31,7 +31,8 @@ internal sealed class TerminalTestReporterOptions public int MinimumExpectedTests { get; init; } /// - /// Gets a value indicating whether we should write the progress periodically to screen. When ANSI is allowed we update the progress as often as we can. When ANSI is not allowed we update it every 3 seconds. + /// Gets a value indicating whether we should write the progress periodically to screen. When ANSI is allowed we update the progress as often as we can. + /// When ANSI is not allowed we never have progress. /// public bool ShowProgress { get; init; } @@ -41,13 +42,26 @@ internal sealed class TerminalTestReporterOptions public bool ShowActiveTests { get; init; } /// - /// Gets a value indicating whether we should use ANSI escape codes or disable them. When true the capabilities of the console are autodetected. + /// Gets a value indicating the ANSI mode. /// - public bool UseAnsi { get; init; } + public AnsiMode AnsiMode { get; init; } +} + +internal enum AnsiMode +{ + /// + /// Disable ANSI escape codes. + /// + NoAnsi, + + /// + /// Use simplified ANSI renderer, which colors output, but does not move cursor. + /// This is used in compatible CI environments. + /// + SimpleAnsi, /// - /// Gets a value indicating whether we are running in compatible CI, and should use simplified ANSI renderer, which colors output, but does not move cursor. - /// Setting to false will disable this option. + /// Enable ANSI escape codes, including cursor movement, when the capabilities of the console allow it. /// - public bool UseCIAnsi { get; init; } + AnsiIfPossible, } diff --git a/src/Cli/dotnet/Commands/Test/MTP/Terminal/TestProgressStateAwareTerminal.cs b/src/Cli/dotnet/Commands/Test/MTP/Terminal/TestProgressStateAwareTerminal.cs index 4b1deb703e3b..3a7fa52aa96d 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/Terminal/TestProgressStateAwareTerminal.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/Terminal/TestProgressStateAwareTerminal.cs @@ -6,7 +6,7 @@ namespace Microsoft.DotNet.Cli.Commands.Test.Terminal; /// /// Terminal that updates the progress in place when progress reporting is enabled. /// -internal sealed partial class TestProgressStateAwareTerminal(ITerminal terminal, bool showProgress, bool writeProgressImmediatelyAfterOutput, int updateEvery) : IDisposable +internal sealed partial class TestProgressStateAwareTerminal(ITerminal terminal, bool showProgress) : IDisposable { /// /// A cancellation token to signal the rendering thread that it should exit. @@ -20,8 +20,6 @@ internal sealed partial class TestProgressStateAwareTerminal(ITerminal terminal, private readonly ITerminal _terminal = terminal; private readonly bool _showProgress = showProgress; - private readonly bool _writeProgressImmediatelyAfterOutput = writeProgressImmediatelyAfterOutput; - private readonly int _updateEvery = updateEvery; private TestProgressState?[] _progressItems = []; /// @@ -37,7 +35,11 @@ private void ThreadProc() { try { - while (!_cts.Token.WaitHandle.WaitOne(_updateEvery)) + // When writing to ANSI, we update the progress in place and it should look responsive so we + // update every half second, because we only show seconds on the screen, so it is good enough. + // When writing to non-ANSI, we never show progress as the output can get long and messy. + const int AnsiUpdateCadenceInMs = 500; + while (!_cts.Token.WaitHandle.WaitOne(AnsiUpdateCadenceInMs)) { lock (_lock) { @@ -118,10 +120,7 @@ internal void WriteToTerminal(Action write) _terminal.StartUpdate(); _terminal.EraseProgress(); write(_terminal); - if (_writeProgressImmediatelyAfterOutput) - { - _terminal.RenderProgress(_progressItems); - } + _terminal.RenderProgress(_progressItems); } finally { diff --git a/src/Cli/dotnet/Commands/Test/MTP/TestApplicationHandler.cs b/src/Cli/dotnet/Commands/Test/MTP/TestApplicationHandler.cs index 8db706d7c382..9735d0ab72b2 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/TestApplicationHandler.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/TestApplicationHandler.cs @@ -15,6 +15,7 @@ internal sealed class TestApplicationHandler private readonly Dictionary _testSessionEventCountPerSessionUid = new(); private (string? TargetFramework, string? Architecture, string ExecutionId)? _handshakeInfo; + private bool _receivedTestHostHandshake; public TestApplicationHandler(TerminalTestReporter output, TestModule module, TestOptions options) { @@ -62,6 +63,7 @@ internal void OnHandshakeReceived(HandshakeMessage handshakeMessage, bool gotSup // https://github.com/microsoft/testfx/blob/2a9a353ec2bb4ce403f72e8ba1f29e01e7cf1fd4/src/Platform/Microsoft.Testing.Platform/Hosts/CommonTestHost.cs#L87-L97 if (hostType == "TestHost") { + _receivedTestHostHandshake = true; // AssemblyRunStarted counts "retry count", and writes to terminal "(Try ) Running tests from " // So, we want to call it only for test host, and not for test host controller (or orchestrator, if in future it will handshake as well) // Calling it for both test host and test host controllers means we will count retries incorrectly, and will messages twice. @@ -263,8 +265,10 @@ internal bool HasMismatchingTestSessionEventCount() internal void OnTestProcessExited(int exitCode, string outputData, string errorData) { - if (_handshakeInfo.HasValue) + if (_receivedTestHostHandshake && _handshakeInfo.HasValue) { + // If we received a handshake from TestHostController but not from TestHost, + // call HandshakeFailure instead of AssemblyRunCompleted _output.AssemblyRunCompleted(_handshakeInfo.Value.ExecutionId, exitCode, outputData, errorData); } else diff --git a/src/RazorSdk/Tool/GenerateCommand.cs b/src/RazorSdk/Tool/GenerateCommand.cs index d76d49b0eb10..ee542977d8d8 100644 --- a/src/RazorSdk/Tool/GenerateCommand.cs +++ b/src/RazorSdk/Tool/GenerateCommand.cs @@ -5,7 +5,6 @@ using System.Collections.Immutable; using System.Diagnostics; -using System.Threading; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.CSharp; using Microsoft.NET.Sdk.Razor.Tool.CommandLineUtils; diff --git a/test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs b/test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs index ee278251ebce..709f3fbf206d 100644 --- a/test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs +++ b/test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs @@ -73,7 +73,7 @@ public static void Print() App.AssertOutputContains("dotnet watch 🔥 Hot reload capabilities: AddExplicitInterfaceImplementation AddFieldRva AddInstanceFieldToExistingType AddMethodToExistingType AddStaticFieldToExistingType Baseline ChangeCustomAttributes GenericAddFieldToExistingType GenericAddMethodToExistingType GenericUpdateMethod NewTypeDefinition UpdateParameters."); } - [Fact] + [Fact(Skip = "https://github.com/dotnet/sdk/issues/52576")] public async Task ProjectChange_UpdateDirectoryBuildPropsThenUpdateSource() { var testAsset = TestAssets.CopyTestAsset("WatchAppWithProjectDeps")