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
60 changes: 48 additions & 12 deletions TUnit.Pipeline/Modules/Abstract/TestBaseModule.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;
using ModularPipelines.Context;
using ModularPipelines.DotNet.Extensions;
using ModularPipelines.DotNet.Options;
Expand All @@ -12,28 +13,32 @@ public abstract class TestBaseModule : Module<IReadOnlyList<CommandResult>>
{
protected virtual IEnumerable<string> TestableFrameworks
{
get
{
yield return "net10.0";
yield return "net8.0";

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
yield return "net472";
}
}
get { yield return "net10.0"; }
}

/// <summary>True on Windows, where legacy .NET Framework TFMs (net4xx) can be tested.</summary>
protected static bool IsWindows => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);

protected sealed override async Task<IReadOnlyList<CommandResult>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
{
var results = new List<CommandResult>();

foreach (var framework in TestableFrameworks)
{
var testResult = await context.SubModule<CommandResult>(framework, async () =>
var (testOptions, executionOptions) = await GetTestOptions(context, framework, cancellationToken);

// Test projects no longer multi-target every TFM by default (see TestProject.props).
// Skip frameworks that this project did not actually build to avoid spurious
// "process cannot find the file" errors for missing per-TFM output binaries.
var configuration = testOptions.Configuration ?? "Release";
if (!HasFrameworkOutput(context.Logger, executionOptions, framework, configuration))
{
var (testOptions, executionOptions) = await GetTestOptions(context, framework, cancellationToken);
context.Logger.LogInformation("Skipping {Framework}: no build output found for this test project.", framework);
continue;
}

var testResult = await context.SubModule<CommandResult>(framework, async () =>
{
var finalExecutionOptions = SetDefaults(testOptions, executionOptions ?? new CommandExecutionOptions(), framework);

return await context.DotNet().Run(testOptions, finalExecutionOptions, cancellationToken);
Expand All @@ -45,6 +50,32 @@ protected virtual IEnumerable<string> TestableFrameworks
return results;
}

private static bool HasFrameworkOutput(ILogger logger, CommandExecutionOptions? executionOptions, string framework, string configuration)
{
var workingDirectory = executionOptions?.WorkingDirectory;
if (string.IsNullOrEmpty(workingDirectory))
{
// Cannot determine — fall through to attempt the run (preserves prior behaviour).
logger.LogWarning("Cannot probe build output for {Framework}: no WorkingDirectory set on execution options.", framework);
return true;
}

var binPath = Path.Combine(workingDirectory, "bin", configuration, framework);

// Probe for an actual built binary, not just the directory: a stale empty
// bin/<config>/<tfm> folder (e.g. from `dotnet clean`) would otherwise be treated
// as a successful build and trigger a misleading "process cannot find the file" later.
try
{
return Directory.EnumerateFiles(binPath, "*.dll", SearchOption.TopDirectoryOnly).Any()
|| Directory.EnumerateFiles(binPath, "*.exe", SearchOption.TopDirectoryOnly).Any();
}
catch (DirectoryNotFoundException)
{
return false;
}
}

private CommandExecutionOptions SetDefaults(DotNetRunOptions testOptions, CommandExecutionOptions executionOptions, string framework)
{
var envVars = executionOptions.EnvironmentVariables ?? new Dictionary<string, string?>();
Expand All @@ -70,5 +101,10 @@ private CommandExecutionOptions SetDefaults(DotNetRunOptions testOptions, Comman
};
}

/// <summary>
/// Called once per framework in <see cref="TestableFrameworks"/>, <em>before</em> the
/// missing-output skip check. Keep this cheap — expensive work (e.g. awaiting other modules)
/// is wasted on TFMs the project did not build for.
/// </summary>
protected abstract Task<(DotNetRunOptions Options, CommandExecutionOptions? ExecutionOptions)> GetTestOptions(IModuleContext context, string framework, CancellationToken cancellationToken);
}
17 changes: 17 additions & 0 deletions TUnit.Pipeline/Modules/RunPublicAPITestsModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,23 @@ namespace TUnit.Pipeline.Modules;
[NotInParallel("SnapshotTests")]
public class RunPublicAPITestsModule : TestBaseModule
{
// Public API snapshots cover every supported consumer TFM — keep aligned with
// <TargetFrameworks> in TUnit.PublicAPI.csproj.
protected override IEnumerable<string> TestableFrameworks
{
get
{
yield return "net10.0";
yield return "net9.0";
yield return "net8.0";

if (IsWindows)
{
yield return "net472";
}
}
}

protected override Task<(DotNetRunOptions Options, CommandExecutionOptions? ExecutionOptions)> GetTestOptions(IModuleContext context, string framework, CancellationToken cancellationToken)
{
var project = context.Git().RootDirectory.FindFile(x => x.Name == "TUnit.PublicAPI.csproj").AssertExists();
Expand Down
16 changes: 16 additions & 0 deletions TUnit.Pipeline/Modules/RunSourceGeneratorTestsModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,22 @@ namespace TUnit.Pipeline.Modules;
[NotInParallel("SnapshotTests")]
public class RunSourceGeneratorTestsModule : TestBaseModule
{
// Generator output snapshots verify behaviour across consumer TFMs — keep aligned with
// <TargetFrameworks> in TUnit.Core.SourceGenerator.Tests.csproj.
protected override IEnumerable<string> TestableFrameworks
{
get
{
yield return "net10.0";
yield return "net8.0";

if (IsWindows)
{
yield return "net472";
}
}
}

protected override Task<(DotNetRunOptions Options, CommandExecutionOptions? ExecutionOptions)> GetTestOptions(IModuleContext context, string framework, CancellationToken cancellationToken)
{
var project = context.Git().RootDirectory.FindFile(x => x.Name == "TUnit.Core.SourceGenerator.Tests.csproj").AssertExists();
Expand Down
5 changes: 2 additions & 3 deletions TUnit.Pipeline/Modules/TestNugetPackageModule.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Runtime.InteropServices;
using ModularPipelines.Attributes;
using ModularPipelines.Attributes;
using ModularPipelines.Configuration;
using ModularPipelines.Context;
using ModularPipelines.DotNet.Options;
Expand Down Expand Up @@ -44,7 +43,7 @@ protected override IEnumerable<string> TestableFrameworks
yield return "net10.0";
yield return "net8.0";

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
if (IsWindows)
{
yield return "net481";
yield return "net48";
Expand Down
Loading