257 lines
8.9 KiB
C#
257 lines
8.9 KiB
C#
using System.Text;
|
|
using StellaOps.TaskRunner.Client.Models;
|
|
using StellaOps.TaskRunner.Client.Streaming;
|
|
using StellaOps.TaskRunner.Client.Pagination;
|
|
using StellaOps.TaskRunner.Client.Lifecycle;
|
|
|
|
|
|
using StellaOps.TestKit;
|
|
namespace StellaOps.TaskRunner.Tests;
|
|
|
|
public sealed class TaskRunnerClientTests
|
|
{
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task StreamingLogReader_ParsesNdjsonLines()
|
|
{
|
|
var ct = CancellationToken.None;
|
|
var ndjson = """
|
|
{"timestamp":"2025-01-01T00:00:00Z","level":"info","stepId":"step-1","message":"Starting","traceId":"abc123"}
|
|
{"timestamp":"2025-01-01T00:00:01Z","level":"error","stepId":"step-1","message":"Failed","traceId":"abc123"}
|
|
""";
|
|
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(ndjson));
|
|
|
|
var entries = await StreamingLogReader.CollectAsync(stream, ct);
|
|
|
|
Assert.Equal(2, entries.Count);
|
|
Assert.Equal("info", entries[0].Level);
|
|
Assert.Equal("error", entries[1].Level);
|
|
Assert.Equal("step-1", entries[0].StepId);
|
|
Assert.Equal("Starting", entries[0].Message);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task StreamingLogReader_SkipsEmptyLines()
|
|
{
|
|
var ct = CancellationToken.None;
|
|
var ndjson = """
|
|
{"timestamp":"2025-01-01T00:00:00Z","level":"info","stepId":"step-1","message":"Test","traceId":"abc123"}
|
|
|
|
{"timestamp":"2025-01-01T00:00:01Z","level":"info","stepId":"step-2","message":"Test2","traceId":"abc123"}
|
|
""";
|
|
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(ndjson));
|
|
|
|
var entries = await StreamingLogReader.CollectAsync(stream, ct);
|
|
|
|
Assert.Equal(2, entries.Count);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task StreamingLogReader_SkipsMalformedLines()
|
|
{
|
|
var ct = CancellationToken.None;
|
|
var ndjson = """
|
|
{"timestamp":"2025-01-01T00:00:00Z","level":"info","stepId":"step-1","message":"Valid","traceId":"abc123"}
|
|
not valid json
|
|
{"timestamp":"2025-01-01T00:00:01Z","level":"info","stepId":"step-2","message":"AlsoValid","traceId":"abc123"}
|
|
""";
|
|
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(ndjson));
|
|
|
|
var entries = await StreamingLogReader.CollectAsync(stream, ct);
|
|
|
|
Assert.Equal(2, entries.Count);
|
|
Assert.Equal("Valid", entries[0].Message);
|
|
Assert.Equal("AlsoValid", entries[1].Message);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task StreamingLogReader_FilterByLevel_FiltersCorrectly()
|
|
{
|
|
var ct = CancellationToken.None;
|
|
var entries = new List<RunLogEntry>
|
|
{
|
|
new(DateTimeOffset.UtcNow, "info", "step-1", "Info message", "trace1"),
|
|
new(DateTimeOffset.UtcNow, "error", "step-1", "Error message", "trace1"),
|
|
new(DateTimeOffset.UtcNow, "warning", "step-1", "Warning message", "trace1"),
|
|
};
|
|
|
|
var levels = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "error", "warning" };
|
|
var filtered = new List<RunLogEntry>();
|
|
|
|
await foreach (var entry in StreamingLogReader.FilterByLevelAsync(entries.ToAsyncEnumerable(), levels, ct))
|
|
{
|
|
filtered.Add(entry);
|
|
}
|
|
|
|
Assert.Equal(2, filtered.Count);
|
|
Assert.DoesNotContain(filtered, e => e.Level == "info");
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task StreamingLogReader_GroupByStep_GroupsCorrectly()
|
|
{
|
|
var ct = CancellationToken.None;
|
|
var entries = new List<RunLogEntry>
|
|
{
|
|
new(DateTimeOffset.UtcNow, "info", "step-1", "Message 1", "trace1"),
|
|
new(DateTimeOffset.UtcNow, "info", "step-2", "Message 2", "trace1"),
|
|
new(DateTimeOffset.UtcNow, "info", "step-1", "Message 3", "trace1"),
|
|
new(DateTimeOffset.UtcNow, "info", null, "Global message", "trace1"),
|
|
};
|
|
|
|
var groups = await StreamingLogReader.GroupByStepAsync(entries.ToAsyncEnumerable(), ct);
|
|
|
|
Assert.Equal(3, groups.Count);
|
|
Assert.Equal(2, groups["step-1"].Count);
|
|
Assert.Single(groups["step-2"]);
|
|
Assert.Single(groups["(global)"]);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task Paginator_IteratesAllPages()
|
|
{
|
|
var ct = CancellationToken.None;
|
|
var allItems = Enumerable.Range(1, 25).ToList();
|
|
var pageSize = 10;
|
|
var fetchCalls = 0;
|
|
|
|
var paginator = new Paginator<int>(
|
|
async (offset, limit, token) =>
|
|
{
|
|
fetchCalls++;
|
|
var items = allItems.Skip(offset).Take(limit).ToList();
|
|
var hasMore = offset + items.Count < allItems.Count;
|
|
return new PagedResponse<int>(items, allItems.Count, hasMore);
|
|
},
|
|
pageSize);
|
|
|
|
var collected = await paginator.CollectAsync(ct);
|
|
|
|
Assert.Equal(25, collected.Count);
|
|
Assert.Equal(3, fetchCalls); // 10, 10, 5 items
|
|
Assert.Equal(allItems, collected);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task Paginator_GetPage_ReturnsCorrectPage()
|
|
{
|
|
var ct = CancellationToken.None;
|
|
var allItems = Enumerable.Range(1, 25).ToList();
|
|
var pageSize = 10;
|
|
|
|
var paginator = new Paginator<int>(
|
|
async (offset, limit, token) =>
|
|
{
|
|
var items = allItems.Skip(offset).Take(limit).ToList();
|
|
var hasMore = offset + items.Count < allItems.Count;
|
|
return new PagedResponse<int>(items, allItems.Count, hasMore);
|
|
},
|
|
pageSize);
|
|
|
|
var page2 = await paginator.GetPageAsync(2, ct);
|
|
|
|
Assert.Equal(10, page2.Items.Count);
|
|
Assert.Equal(11, page2.Items[0]); // Items 11-20
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task PaginatorExtensions_TakeAsync_TakesCorrectNumber()
|
|
{
|
|
var ct = CancellationToken.None;
|
|
var items = Enumerable.Range(1, 100).ToAsyncEnumerable();
|
|
|
|
var taken = new List<int>();
|
|
await foreach (var item in items.TakeAsync(5, ct))
|
|
{
|
|
taken.Add(item);
|
|
}
|
|
|
|
Assert.Equal(5, taken.Count);
|
|
Assert.Equal(new[] { 1, 2, 3, 4, 5 }, taken);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task PaginatorExtensions_SkipAsync_SkipsCorrectNumber()
|
|
{
|
|
var ct = CancellationToken.None;
|
|
var items = Enumerable.Range(1, 10).ToAsyncEnumerable();
|
|
|
|
var skipped = new List<int>();
|
|
await foreach (var item in items.SkipAsync(5, ct))
|
|
{
|
|
skipped.Add(item);
|
|
}
|
|
|
|
Assert.Equal(5, skipped.Count);
|
|
Assert.Equal(new[] { 6, 7, 8, 9, 10 }, skipped);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void PackRunLifecycleHelper_TerminalStatuses_IncludesExpectedStatuses()
|
|
{
|
|
Assert.Contains("completed", PackRunLifecycleHelper.TerminalStatuses);
|
|
Assert.Contains("failed", PackRunLifecycleHelper.TerminalStatuses);
|
|
Assert.Contains("cancelled", PackRunLifecycleHelper.TerminalStatuses);
|
|
Assert.Contains("rejected", PackRunLifecycleHelper.TerminalStatuses);
|
|
Assert.DoesNotContain("running", PackRunLifecycleHelper.TerminalStatuses);
|
|
Assert.DoesNotContain("pending", PackRunLifecycleHelper.TerminalStatuses);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void PackRunModels_CreatePackRunRequest_SerializesCorrectly()
|
|
{
|
|
var request = new CreatePackRunRequest(
|
|
"my-pack",
|
|
"1.0.0",
|
|
new Dictionary<string, object> { ["key"] = "value" },
|
|
"tenant-1",
|
|
"corr-123");
|
|
|
|
Assert.Equal("my-pack", request.PackId);
|
|
Assert.Equal("1.0.0", request.PackVersion);
|
|
Assert.NotNull(request.Inputs);
|
|
Assert.Equal("value", request.Inputs["key"]);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void PackRunModels_SimulatedStep_HasCorrectProperties()
|
|
{
|
|
var loopInfo = new LoopInfo("{{ inputs.items }}", "item", 100);
|
|
var step = new SimulatedStep(
|
|
"step-1",
|
|
"loop",
|
|
"WillIterate",
|
|
loopInfo,
|
|
null,
|
|
null);
|
|
|
|
Assert.Equal("step-1", step.StepId);
|
|
Assert.Equal("loop", step.Kind);
|
|
Assert.NotNull(step.LoopInfo);
|
|
Assert.Equal("{{ inputs.items }}", step.LoopInfo.ItemsExpression);
|
|
}
|
|
}
|
|
|
|
internal static class AsyncEnumerableExtensions
|
|
{
|
|
public static async IAsyncEnumerable<T> ToAsyncEnumerable<T>(this IEnumerable<T> source)
|
|
{
|
|
foreach (var item in source)
|
|
{
|
|
yield return item;
|
|
}
|
|
await Task.CompletedTask;
|
|
}
|
|
}
|