Files
git.stella-ops.org/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Tests/FilesystemPackRunArtifactUploaderTests.cs

143 lines
5.8 KiB
C#

using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.TaskRunner.Core.Execution;
using StellaOps.TaskRunner.Core.Planning;
using StellaOps.TaskRunner.Infrastructure.Execution;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.TaskRunner.Tests;
public sealed class FilesystemPackRunArtifactUploaderTests : IDisposable
{
private readonly string artifactsRoot;
public FilesystemPackRunArtifactUploaderTests()
{
artifactsRoot = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("n"));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CopiesFileOutputs()
{
var sourceFile = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():n}.txt");
await File.WriteAllTextAsync(sourceFile, "artifact-content", CancellationToken.None);
var uploader = CreateUploader();
var output = CreateFileOutput("bundle", sourceFile);
var context = CreateContext();
var state = CreateState(context);
await uploader.UploadAsync(context, state, new[] { output }, CancellationToken.None);
var runPath = Path.Combine(artifactsRoot, context.RunId);
var filesDirectory = Path.Combine(runPath, "files");
var copiedFiles = Directory.GetFiles(filesDirectory);
Assert.Single(copiedFiles);
Assert.Equal("bundle.txt", Path.GetFileName(copiedFiles[0]));
Assert.Equal("artifact-content", await File.ReadAllTextAsync(copiedFiles[0], CancellationToken.None));
var manifest = await ReadManifestAsync(runPath);
Assert.Single(manifest.Outputs);
Assert.Equal("copied", manifest.Outputs[0].Status);
Assert.Equal("files/bundle.txt", manifest.Outputs[0].StoredPath);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task RecordsMissingFilesWithoutThrowing()
{
var uploader = CreateUploader();
var output = CreateFileOutput("missing", Path.Combine(Path.GetTempPath(), "does-not-exist.txt"));
var context = CreateContext();
var state = CreateState(context);
await uploader.UploadAsync(context, state, new[] { output }, CancellationToken.None);
var manifest = await ReadManifestAsync(Path.Combine(artifactsRoot, context.RunId));
Assert.Equal("missing", manifest.Outputs[0].Status);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task WritesExpressionOutputsAsJson()
{
var uploader = CreateUploader();
var output = CreateExpressionOutput("metadata", JsonNode.Parse("""{"foo":"bar"}""")!);
var context = CreateContext();
var state = CreateState(context);
await uploader.UploadAsync(context, state, new[] { output }, CancellationToken.None);
var expressionPath = Path.Combine(artifactsRoot, context.RunId, "expressions", "metadata.json");
Assert.True(File.Exists(expressionPath));
var manifest = await ReadManifestAsync(Path.Combine(artifactsRoot, context.RunId));
Assert.Equal("materialized", manifest.Outputs[0].Status);
Assert.Equal("expressions/metadata.json", manifest.Outputs[0].StoredPath);
}
private FilesystemPackRunArtifactUploader CreateUploader()
=> new(artifactsRoot, TimeProvider.System, NullLogger<FilesystemPackRunArtifactUploader>.Instance);
private static TaskPackPlanOutput CreateFileOutput(string name, string path)
=> new(
name,
Type: "file",
Path: new TaskPackPlanParameterValue(JsonValue.Create(path), null, null, false),
Expression: null);
private static TaskPackPlanOutput CreateExpressionOutput(string name, JsonNode expression)
=> new(
name,
Type: "object",
Path: null,
Expression: new TaskPackPlanParameterValue(expression, null, null, false));
private static PackRunExecutionContext CreateContext()
=> new("run-" + Guid.NewGuid().ToString("n"), CreatePlan(), DateTimeOffset.UtcNow);
private static PackRunState CreateState(PackRunExecutionContext context)
=> PackRunState.Create(
runId: context.RunId,
planHash: context.Plan.Hash,
context.Plan,
failurePolicy: new TaskPackPlanFailurePolicy(1, 1, false),
requestedAt: DateTimeOffset.UtcNow,
steps: new Dictionary<string, PackRunStepStateRecord>(StringComparer.Ordinal),
timestamp: DateTimeOffset.UtcNow);
private static TaskPackPlan CreatePlan()
{
return new TaskPackPlan(
new TaskPackPlanMetadata("sample-pack", "1.0.0", null, Array.Empty<string>()),
new Dictionary<string, JsonNode?>(StringComparer.Ordinal),
Array.Empty<TaskPackPlanStep>(),
hash: "hash",
approvals: Array.Empty<TaskPackPlanApproval>(),
secrets: Array.Empty<TaskPackPlanSecret>(),
outputs: Array.Empty<TaskPackPlanOutput>(),
failurePolicy: new TaskPackPlanFailurePolicy(1, 1, false));
}
private static async Task<ArtifactManifestModel> ReadManifestAsync(string runPath)
{
var json = await File.ReadAllTextAsync(Path.Combine(runPath, "artifact-manifest.json"), CancellationToken.None);
return JsonSerializer.Deserialize<ArtifactManifestModel>(json, new JsonSerializerOptions(JsonSerializerDefaults.Web))!;
}
public void Dispose()
{
if (Directory.Exists(artifactsRoot))
{
Directory.Delete(artifactsRoot, recursive: true);
}
}
private sealed record ArtifactManifestModel(string RunId, DateTimeOffset UploadedAt, List<ArtifactRecordModel> Outputs);
private sealed record ArtifactRecordModel(string Name, string Type, string? SourcePath, string? StoredPath, string Status, string? Notes);
}