|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Text.Json;
using Aspire.TestUtilities;
using Xunit;
namespace Infrastructure.Tests;
/// <summary>
/// Tests for eng/scripts/split-test-matrix-by-deps.ps1
/// </summary>
public class SplitTestMatrixByDepsTests : IDisposable
{
private readonly TestTempDirectory _tempDir = new();
private readonly string _scriptPath;
private readonly string _githubOutputFile;
private readonly ITestOutputHelper _output;
public SplitTestMatrixByDepsTests(ITestOutputHelper output)
{
_output = output;
_scriptPath = Path.Combine(FindRepoRoot(), "eng", "scripts", "split-test-matrix-by-deps.ps1");
_githubOutputFile = Path.Combine(_tempDir.Path, "github_output.txt");
File.WriteAllText(_githubOutputFile, "");
}
public void Dispose() => _tempDir.Dispose();
[Fact]
[RequiresTools(["pwsh"])]
public async Task SplitsNoDepsTestsIntoNoNugetsBucket()
{
var matrixJson = BuildMatrixJson(
new { name = "A", shortname = "a", runs_on = "ubuntu-latest" },
new { name = "B", shortname = "b", runs_on = "ubuntu-latest" });
var result = await RunScript(allTestsMatrix: matrixJson);
result.EnsureSuccessful();
var outputs = ParseGitHubOutputFile();
Assert.Equal(2, outputs["tests_matrix_no_nugets"].Include.Length);
Assert.Empty(outputs["tests_matrix_requires_nugets"].Include);
Assert.Empty(outputs["tests_matrix_requires_cli_archive"].Include);
}
[Fact]
[RequiresTools(["pwsh"])]
public async Task SplitsRequiresNugetsTestsIntoRequiresNugetsBucket()
{
var matrixJson = BuildMatrixJson(
new { name = "Plain", shortname = "p", runs_on = "ubuntu-latest" },
new { name = "NugetTest", shortname = "nt", runs_on = "ubuntu-latest", requiresNugets = true });
var result = await RunScript(allTestsMatrix: matrixJson);
result.EnsureSuccessful();
var outputs = ParseGitHubOutputFile();
Assert.Single(outputs["tests_matrix_no_nugets"].Include);
Assert.Equal("Plain", outputs["tests_matrix_no_nugets"].Include[0].Name);
Assert.Single(outputs["tests_matrix_requires_nugets"].Include);
Assert.Equal("NugetTest", outputs["tests_matrix_requires_nugets"].Include[0].Name);
}
[Fact]
[RequiresTools(["pwsh"])]
public async Task SplitsRequiresCliArchiveTestsIntoCliArchiveBucket()
{
var matrixJson = BuildMatrixJson(
new { name = "Plain", shortname = "p", runs_on = "ubuntu-latest" },
new { name = "CliTest", shortname = "ct", runs_on = "ubuntu-latest", requiresCliArchive = true });
var result = await RunScript(allTestsMatrix: matrixJson);
result.EnsureSuccessful();
var outputs = ParseGitHubOutputFile();
Assert.Single(outputs["tests_matrix_no_nugets"].Include);
Assert.Single(outputs["tests_matrix_requires_cli_archive"].Include);
Assert.Equal("CliTest", outputs["tests_matrix_requires_cli_archive"].Include[0].Name);
}
[Fact]
[RequiresTools(["pwsh"])]
public async Task CliArchiveTakesPriorityOverNugets()
{
var matrixJson = BuildMatrixJson(
new { name = "Both", shortname = "b", runs_on = "ubuntu-latest", requiresNugets = true, requiresCliArchive = true });
var result = await RunScript(allTestsMatrix: matrixJson);
result.EnsureSuccessful();
var outputs = ParseGitHubOutputFile();
Assert.Single(outputs["tests_matrix_requires_cli_archive"].Include);
Assert.Empty(outputs["tests_matrix_requires_nugets"].Include);
}
[Fact]
[RequiresTools(["pwsh"])]
public async Task OverflowsEntriesBeyondThreshold()
{
var entries = Enumerable.Range(1, 8).Select(i =>
(object)new { name = $"T{i}", shortname = $"t{i}", runs_on = "ubuntu-latest" }).ToArray();
var matrixJson = BuildMatrixJson(entries);
var result = await RunScript(allTestsMatrix: matrixJson, overflowThreshold: 5);
result.EnsureSuccessful();
var outputs = ParseGitHubOutputFile();
Assert.Equal(5, outputs["tests_matrix_no_nugets"].Include.Length);
Assert.Equal(3, outputs["tests_matrix_no_nugets_overflow"].Include.Length);
}
[Fact]
[RequiresTools(["pwsh"])]
public async Task NoOverflowWhenBelowThreshold()
{
var entries = Enumerable.Range(1, 5).Select(i =>
(object)new { name = $"T{i}", shortname = $"t{i}", runs_on = "ubuntu-latest" }).ToArray();
var matrixJson = BuildMatrixJson(entries);
var result = await RunScript(allTestsMatrix: matrixJson, overflowThreshold: 10);
result.EnsureSuccessful();
var outputs = ParseGitHubOutputFile();
Assert.Equal(5, outputs["tests_matrix_no_nugets"].Include.Length);
Assert.Empty(outputs["tests_matrix_no_nugets_overflow"].Include);
}
[Fact]
[RequiresTools(["pwsh"])]
public async Task HandlesEmptyMatrix()
{
var matrixJson = BuildMatrixJson();
var result = await RunScript(allTestsMatrix: matrixJson);
result.EnsureSuccessful();
var outputs = ParseGitHubOutputFile();
Assert.Empty(outputs["tests_matrix_no_nugets"].Include);
Assert.Empty(outputs["tests_matrix_no_nugets_overflow"].Include);
Assert.Empty(outputs["tests_matrix_requires_nugets"].Include);
Assert.Empty(outputs["tests_matrix_requires_cli_archive"].Include);
}
[Fact]
[RequiresTools(["pwsh"])]
public async Task AllFourOutputKeysAlwaysPresent()
{
var matrixJson = BuildMatrixJson(
new { name = "T", shortname = "t", runs_on = "ubuntu-latest" });
var result = await RunScript(allTestsMatrix: matrixJson);
result.EnsureSuccessful();
var outputs = ParseGitHubOutputFile();
Assert.True(outputs.ContainsKey("tests_matrix_no_nugets"));
Assert.True(outputs.ContainsKey("tests_matrix_no_nugets_overflow"));
Assert.True(outputs.ContainsKey("tests_matrix_requires_nugets"));
Assert.True(outputs.ContainsKey("tests_matrix_requires_cli_archive"));
}
[Fact]
[RequiresTools(["pwsh"])]
public async Task ReadsMatrixFromFile()
{
var matrixJson = BuildMatrixJson(
new { name = "FromFile", shortname = "ff", runs_on = "ubuntu-latest" });
var matrixFile = Path.Combine(_tempDir.Path, "matrix.json");
File.WriteAllText(matrixFile, matrixJson);
var result = await RunScript(allTestsMatrixFile: matrixFile);
result.EnsureSuccessful();
var outputs = ParseGitHubOutputFile();
Assert.Single(outputs["tests_matrix_no_nugets"].Include);
Assert.Equal("FromFile", outputs["tests_matrix_no_nugets"].Include[0].Name);
}
[Fact]
[RequiresTools(["pwsh"])]
public async Task FailsWhenNoInputProvided()
{
using var cmd = new PowerShellCommand(_scriptPath, _output)
.WithTimeout(TimeSpan.FromMinutes(2))
.WithEnvironmentVariable("GITHUB_OUTPUT", _githubOutputFile);
var result = await cmd.ExecuteAsync("-OutputToGitHubEnv");
Assert.NotEqual(0, result.ExitCode);
}
[Fact]
[RequiresTools(["pwsh"])]
public async Task FailsWhenMatrixFileNotFound()
{
var nonExistentFile = Path.Combine(_tempDir.Path, "nonexistent.json");
using var cmd = new PowerShellCommand(_scriptPath, _output)
.WithTimeout(TimeSpan.FromMinutes(2))
.WithEnvironmentVariable("GITHUB_OUTPUT", _githubOutputFile);
var result = await cmd.ExecuteAsync(
"-AllTestsMatrixFile", $"\"{nonExistentFile}\"",
"-OutputToGitHubEnv");
Assert.NotEqual(0, result.ExitCode);
}
private async Task<CommandResult> RunScript(
string? allTestsMatrix = null,
string? allTestsMatrixFile = null,
int? overflowThreshold = null)
{
using var cmd = new PowerShellCommand(_scriptPath, _output)
.WithTimeout(TimeSpan.FromMinutes(2))
.WithEnvironmentVariable("GITHUB_OUTPUT", _githubOutputFile);
var args = new List<string>();
if (!string.IsNullOrEmpty(allTestsMatrix))
{
// Write JSON to a temp file to avoid command-line quoting issues
var tempMatrixFile = Path.Combine(_tempDir.Path, $"matrix_input_{Guid.NewGuid():N}.json");
File.WriteAllText(tempMatrixFile, allTestsMatrix);
args.Add("-AllTestsMatrixFile");
args.Add($"\"{tempMatrixFile}\"");
}
if (!string.IsNullOrEmpty(allTestsMatrixFile))
{
args.Add("-AllTestsMatrixFile");
args.Add($"\"{allTestsMatrixFile}\"");
}
if (overflowThreshold.HasValue)
{
args.Add("-OverflowThreshold");
args.Add(overflowThreshold.Value.ToString());
}
args.Add("-OutputToGitHubEnv");
return await cmd.ExecuteAsync(args.ToArray());
}
private static string BuildMatrixJson(params object[] entries)
{
var matrix = new { include = entries };
return JsonSerializer.Serialize(matrix);
}
private Dictionary<string, GitHubActionsMatrix> ParseGitHubOutputFile()
{
var results = new Dictionary<string, GitHubActionsMatrix>();
foreach (var line in File.ReadAllLines(_githubOutputFile))
{
var eqIndex = line.IndexOf('=');
if (eqIndex < 0)
{
continue;
}
var key = line[..eqIndex];
var value = line[(eqIndex + 1)..];
results[key] = ParseGitHubMatrixJson(value);
}
return results;
}
private static GitHubActionsMatrix ParseGitHubMatrixJson(string json)
{
var document = JsonDocument.Parse(json);
var include = document.RootElement.GetProperty("include");
ExpandedMatrixEntry[] entries;
if (include.ValueKind == JsonValueKind.Array)
{
entries = JsonSerializer.Deserialize<ExpandedMatrixEntry[]>(include.GetRawText(), new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
}) ?? [];
}
else if (include.ValueKind == JsonValueKind.Object)
{
var singleEntry = JsonSerializer.Deserialize<ExpandedMatrixEntry>(include.GetRawText(), new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
entries = singleEntry is not null ? [singleEntry] : [];
}
else
{
entries = [];
}
return new GitHubActionsMatrix { Include = entries };
}
private static string FindRepoRoot()
{
var dir = new DirectoryInfo(AppContext.BaseDirectory);
while (dir is not null)
{
if (File.Exists(Path.Combine(dir.FullName, "Aspire.slnx")))
{
return dir.FullName;
}
dir = dir.Parent;
}
throw new InvalidOperationException("Could not find repository root");
}
}
|