Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.DotNet.Cli.Utils.Tests
{
public class TransientSdkResolutionErrorDetectorTests
{
[Fact]
public void TransientInBoxSdkResolutionFailureIsDetected()
{
string input =
"/datadisks/disk1/work/99FC086F/p/d/sdk/11.0.100-ci/Sdks/Microsoft.NET.Sdk.Razor/Sdk/Sdk.props(20,3): " +
"error : Could not resolve SDK \"Microsoft.NET.Sdk.StaticWebAssets\". Exactly one of the probing " +
"messages below indicates why we could not resolve the SDK.\r\n" +
"Sdk.props(20,3): error : SDK resolver \"Microsoft.DotNet.MSBuildWorkloadSdkResolver\" returned null.\r\n" +
"Sdk.props(20,3): error : The NuGetSdkResolver did not resolve this SDK because there was no version " +
"specified in the project or global.json.\r\n" +
"Sdk.props(20,11): error MSB4236: The SDK 'Microsoft.NET.Sdk.StaticWebAssets' specified could not be " +
"found. [/work/InstantiatePr.csproj]";
TransientSdkResolutionErrorDetector.IsTransientError(input).Should().BeTrue();
}

[Fact]
public void NullInputIsNotTransient()
{
TransientSdkResolutionErrorDetector.IsTransientError(null).Should().BeFalse();
}

[Fact]
public void SuccessfulBuildIsNotTransient()
{
string input =
" Determining projects to restore...\r\n" +
" Restored /work/InstantiatePr.csproj (in 131 ms).\r\n" +
" InstantiatePr -> /work/bin/Debug/net11.0/InstantiatePr.dll\r\n" +
"Build succeeded.\r\n 0 Warning(s)\r\n 0 Error(s)";
TransientSdkResolutionErrorDetector.IsTransientError(input).Should().BeFalse();
}

[Fact]
public void MissingVersionedSdkWithoutResolverNullIsNotTransient()
{
// A genuinely missing, version-specified SDK is a deterministic failure (no workload resolver
// "returned null" deferral), so it must not be treated as transient.
string input =
"error MSB4236: The SDK 'Contoso.Custom.Sdk' specified could not be found. [/work/project.csproj]";
TransientSdkResolutionErrorDetector.IsTransientError(input).Should().BeFalse();
}

[Fact]
public void OtherResolverReturningNullIsNotTransient()
{
// A "returned null" message from a different resolver must not trigger a retry: only the in-box
// workload resolver deferral (Microsoft.DotNet.MSBuildWorkloadSdkResolver) is the transient flake.
string input =
"Sdk.props(20,3): error : SDK resolver \"Contoso.CustomSdkResolver\" returned null.\r\n" +
"Sdk.props(20,11): error MSB4236: The SDK 'Microsoft.NET.Sdk.StaticWebAssets' specified could not be " +
"found. [/work/InstantiatePr.csproj]";
TransientSdkResolutionErrorDetector.IsTransientError(input).Should().BeFalse();
}
}
}
3 changes: 2 additions & 1 deletion test/Microsoft.NET.TestFramework/Commands/TestCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@ private bool ShouldStopRetry(CommandResult result)
return false;
}

return !NuGetTransientErrorDetector.IsTransientError(result.StdOut);
return !NuGetTransientErrorDetector.IsTransientError(result.StdOut)
&& !TransientSdkResolutionErrorDetector.IsTransientError(result.StdOut);
}

public virtual CommandResult Execute(IEnumerable<string> args)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.NET.TestFramework
{
/// <summary>
/// Detects transient failures to resolve an in-box .NET SDK (for example
/// <c>Microsoft.NET.Sdk.StaticWebAssets</c> imported by the Razor SDK).
/// </summary>
/// <remarks>
/// Under heavy parallel I/O (many concurrent <c>dotnet build</c>/<c>dotnet new</c> invocations sharing a single
/// SDK-under-test) the MSBuild default SDK resolver intermittently fails to probe the
/// <c>Sdks/&lt;name&gt;/Sdk</c> folder, producing an <c>MSB4236</c> "could not be found" error even though the
/// SDK is present on disk. These failures are not deterministic and succeed when retried, so they are treated as
/// transient for the purposes of <see cref="Commands.TestCommand"/> retry logic. A genuinely missing SDK still
/// fails after the retries are exhausted.
/// </remarks>
public static class TransientSdkResolutionErrorDetector
{
public static bool IsTransientError(string? errorMessage)
{
if (errorMessage is null)
{
return false;
}

// The combination below is specific to the transient in-box SDK resolution flake:
// - MSB4236 is raised for an SDK that "could not be found",
// - for an in-box SDK in the Microsoft.NET.Sdk family, and
// - after the .NET SDK workload resolver (Microsoft.DotNet.MSBuildWorkloadSdkResolver) deferred
// resolution by returning null.
// Keying on the specific resolver name (rather than a bare "returned null" substring) avoids
// accidental retries when some other resolver emits a similar message, and requiring all three
// avoids retrying legitimate failures such as a NuGet-versioned SDK that is intentionally absent.
return errorMessage.Contains("MSB4236")
&& errorMessage.Contains("Microsoft.NET.Sdk")
&& errorMessage.Contains("Microsoft.DotNet.MSBuildWorkloadSdkResolver")
&& errorMessage.Contains("returned null");
}
}
}
6 changes: 5 additions & 1 deletion test/dotnet-new.IntegrationTests/WebProjectsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ public void AllWebProjectsRestoreAndBuild(string testName, params string[] args)
string workingDir = Path.Combine(_fixture.BaseWorkingDirectory, testName);
Directory.CreateDirectory(workingDir);

new DotnetNewCommand(_log, args)
// Run instantiation without the implicit post-action restore: that restore is redundant with the
// explicit DotnetRestoreCommand below, and its transient in-box SDK resolution failures surface as
// stderr while 'dotnet new' still exits 0, which cannot be retried at the command level. The explicit
// restore and build (which retry transient SDK resolution failures) provide the actual coverage.
new DotnetNewCommand(_log, [.. args, "--no-restore"])
.WithCustomHive(_fixture.HomeDirectory)
.WithWorkingDirectory(workingDir)
.Execute()
Expand Down
Loading