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
1 change: 0 additions & 1 deletion src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="FluentAssertions" Version="8.10.0" />
<PackageVersion Include="NSubstitute" Version="6.0.0" />
<PackageVersion Include="Testcontainers.PostgreSql" Version="4.13.0" />
<PackageVersion Include="Testcontainers.MsSql" Version="4.13.0" />
Expand Down
23 changes: 14 additions & 9 deletions src/RunWay.Storage.Tests/ConcurrencyTestsBase.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using FluentAssertions;
using Kododo.RunWay.Core.Jobs;
using Kododo.RunWay.Core.Runners;
using Kododo.RunWay.Core.Store;
Expand Down Expand Up @@ -67,8 +66,10 @@ public async Task MultipleRunners_AllJobsProcessedExactlyOnce()
var remaining = await Store.GetCountAsync(
new JobsFilter { Statuses = [JobStatus.Scheduled, JobStatus.Running] }, CancellationToken.None);

succeeded.Should().Be(jobCount, "each job should be processed exactly once");
remaining.Should().Be(0, "no job should remain unprocessed");
// each job should be processed exactly once
Assert.Equal(jobCount, succeeded);
// no job should remain unprocessed
Assert.Equal(0, remaining);
}

[Fact]
Expand Down Expand Up @@ -107,12 +108,14 @@ async Task<bool> RaceAsync(RunnerInfo runnerInfo)

var results = await Task.WhenAll(RaceAsync(runnerInfo1), RaceAsync(runnerInfo2));

results.Count(r => r).Should().Be(1, "exactly one runner should win the race");
results.Count(r => !r).Should().Be(1, "the other runner should get ConcurrencyException");
// exactly one runner should win the race
Assert.Equal(1, results.Count(r => r));
// the other runner should get ConcurrencyException
Assert.Equal(1, results.Count(r => !r));

var count = await Store.GetCountAsync(
new JobsFilter { Statuses = [JobStatus.Succeeded] }, CancellationToken.None);
count.Should().Be(1);
Assert.Equal(1, count);
}

[Fact]
Expand Down Expand Up @@ -164,7 +167,8 @@ public async Task MultipleThreadsPerRunner_WithSemaphore_AllJobsProcessed()
var succeeded = await Store.GetCountAsync(
new JobsFilter { Statuses = [JobStatus.Succeeded] }, CancellationToken.None);

succeeded.Should().Be(jobCount, "the semaphore should prevent double-fetching of a job");
// the semaphore should prevent double-fetching of a job
Assert.Equal(jobCount, succeeded);
}

[Fact]
Expand All @@ -188,7 +192,7 @@ public async Task HighLoad_ManyRunners_ManyJobs_AllJobsSucceed()
var succeededCount = await Store.GetCountAsync(
new JobsFilter { Statuses = [JobStatus.Succeeded] }, CancellationToken.None);

succeededCount.Should().Be(jobCount);
Assert.Equal(jobCount, succeededCount);
}

[Fact]
Expand Down Expand Up @@ -239,6 +243,7 @@ await Task.WhenAll(
var succeededCount = await Store.GetCountAsync(
new JobsFilter { Statuses = [JobStatus.Succeeded] }, CancellationToken.None);

succeededCount.Should().Be(10, "each runner should process its 5 jobs");
// each runner should process its 5 jobs
Assert.Equal(10, succeededCount);
}
}
1 change: 0 additions & 1 deletion src/RunWay.Storage.Tests/RunWay.Storage.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

<ItemGroup>
<PackageReference Include="xunit" />
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
</ItemGroup>

Expand Down
59 changes: 29 additions & 30 deletions src/RunWay.Storage.Tests/StoreJobTestsBase.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using FluentAssertions;
using Kododo.RunWay.Core.Jobs;
using Kododo.RunWay.Core.Jobs.Audit;
using Kododo.RunWay.Core.Runners;
Expand Down Expand Up @@ -26,7 +25,7 @@ public override async Task InitializeAsync()
public async Task CreateAsync_ReturnsNonEmptyJobId()
{
var job = await Store.CreateAsync(NewJob(), CancellationToken.None);
job.Id.Value.Should().NotBeNullOrEmpty();
Assert.False(string.IsNullOrEmpty(job.Id.Value));
}

[Fact]
Expand All @@ -35,8 +34,8 @@ public async Task CreateAsync_PersistedJobCanBeFound()
var job = await Store.CreateAsync(NewJob(), CancellationToken.None);
var found = await Store.FindAsync(job.Id, CancellationToken.None);

found.Should().NotBeNull();
found.Id.Should().Be(job.Id);
Assert.NotNull(found);
Assert.Equal(job.Id, found.Id);
}

[Fact]
Expand All @@ -45,8 +44,8 @@ public async Task CreateAsync_CreatesAuditRecords()
var job = await Store.CreateAsync(NewJob(), CancellationToken.None);
var details = await Store.GetDetailsAsync(job.Id, CancellationToken.None);

details.Audit.Should().Contain(a => a.Type == JobAuditRecordType.Created);
details.Audit.Should().Contain(a => a.Type == JobAuditRecordType.Scheduled);
Assert.Contains(details.Audit, a => a.Type == JobAuditRecordType.Created);
Assert.Contains(details.Audit, a => a.Type == JobAuditRecordType.Scheduled);
}

[Fact]
Expand All @@ -59,14 +58,14 @@ public async Task CreateAsync_PersistsJobData()

var found = await Store.FindAsync(job.Id, CancellationToken.None);

found!.Data.Type.Should().Be("MyJobType");
Assert.Equal("MyJobType", found!.Data.Type);
}

[Fact]
public async Task FindAsync_WhenJobDoesNotExist_ReturnsNull()
{
var result = await Store.FindAsync(new JobId("999999"), CancellationToken.None);
result.Should().BeNull();
Assert.Null(result);
}

[Fact]
Expand All @@ -78,7 +77,7 @@ public async Task UpdateAsync_ChangesJobStatus()
await Store.UpdateAsync(job, CancellationToken.None);

var updated = await Store.FindAsync(job.Id, CancellationToken.None);
updated!.Status.Should().Be(JobStatus.Running);
Assert.Equal(JobStatus.Running, updated!.Status);
}

[Fact]
Expand All @@ -90,7 +89,7 @@ public async Task UpdateAsync_SavesAuditRecords()
await Store.UpdateAsync(job, CancellationToken.None);

var details = await Store.GetDetailsAsync(job.Id, CancellationToken.None);
details.Audit.Should().Contain(a => a.Type == JobAuditRecordType.Started);
Assert.Contains(details.Audit, a => a.Type == JobAuditRecordType.Started);
}

[Fact]
Expand All @@ -106,7 +105,7 @@ public async Task UpdateAsync_Started_Succeeded_StatusIsSucceeded()
await Store.UpdateAsync(running, CancellationToken.None);

var result = await Store.FindAsync(job.Id, CancellationToken.None);
result!.Status.Should().Be(JobStatus.Succeeded);
Assert.Equal(JobStatus.Succeeded, result!.Status);
}

[Fact]
Expand All @@ -119,8 +118,8 @@ public async Task GetManyAsync_ReturnsPaginatedResults()
new JobsQuery { Pagination = new Pagination(1, 3) },
CancellationToken.None);

result.Items.Should().HaveCount(3);
result.TotalItems.Should().Be(5);
Assert.Equal(3, result.Items.Count);
Assert.Equal(5, result.TotalItems);
}

[Fact]
Expand All @@ -133,7 +132,7 @@ public async Task GetManyAsync_SecondPage_ReturnsRemainingItems()
new JobsQuery { Pagination = new Pagination(2, 3) },
CancellationToken.None);

result.Items.Should().HaveCount(2);
Assert.Equal(2, result.Items.Count);
}

[Fact]
Expand All @@ -151,8 +150,8 @@ public async Task GetManyAsync_FiltersByStatus()
Filter = new JobsFilter { Statuses = [JobStatus.Running] }
}, CancellationToken.None);

result.Items.Should().ContainSingle();
result.Items.Single().Status.Should().Be(JobStatus.Running);
Assert.Single(result.Items);
Assert.Equal(JobStatus.Running, result.Items.Single().Status);
}

[Fact]
Expand All @@ -167,7 +166,7 @@ public async Task GetManyAsync_FiltersByMaxScheduledAt_ExcludesFuture()
Filter = new JobsFilter { MaxScheduledAt = DateTimeOffset.UtcNow }
}, CancellationToken.None);

result.Items.Should().ContainSingle();
Assert.Single(result.Items);
}

[Fact]
Expand All @@ -177,7 +176,7 @@ public async Task GetCountAsync_ReturnsCorrectCount()
await Store.CreateAsync(NewJob(), CancellationToken.None);

var count = await Store.GetCountAsync(new JobsFilter(), CancellationToken.None);
count.Should().Be(2);
Assert.Equal(2, count);
}

[Fact]
Expand All @@ -193,7 +192,7 @@ public async Task GetCountAsync_FiltersByStatus()
new JobsFilter { Statuses = [JobStatus.Scheduled] },
CancellationToken.None);

count.Should().Be(1);
Assert.Equal(1, count);
}

[Fact]
Expand All @@ -205,10 +204,10 @@ public async Task GetDetailsAsync_ReturnsJobWithFullAuditTrail()

var details = await Store.GetDetailsAsync(job.Id, CancellationToken.None);

details.Id.Should().Be(job.Id);
details.Audit.Should().Contain(a => a.Type == JobAuditRecordType.Created);
details.Audit.Should().Contain(a => a.Type == JobAuditRecordType.Scheduled);
details.Audit.Should().Contain(a => a.Type == JobAuditRecordType.Started);
Assert.Equal(job.Id, details.Id);
Assert.Contains(details.Audit, a => a.Type == JobAuditRecordType.Created);
Assert.Contains(details.Audit, a => a.Type == JobAuditRecordType.Scheduled);
Assert.Contains(details.Audit, a => a.Type == JobAuditRecordType.Started);
}

[Fact]
Expand All @@ -221,8 +220,8 @@ public async Task GetOneOrDefaultAsync_ReturnsJobMatchingFilter()
Filter = new JobsFilter { Statuses = [JobStatus.Scheduled] }
}, CancellationToken.None);

result.Should().NotBeNull();
result.Id.Should().Be(job.Id);
Assert.NotNull(result);
Assert.Equal(job.Id, result.Id);
}

[Fact]
Expand All @@ -233,7 +232,7 @@ public async Task GetOneOrDefaultAsync_WhenNoMatch_ReturnsNull()
Filter = new JobsFilter { Statuses = [JobStatus.Failed] }
}, CancellationToken.None);

result.Should().BeNull();
Assert.Null(result);
}

[Fact]
Expand All @@ -248,8 +247,8 @@ public async Task Failed_WithRetries_ReschedulesJobInStore()
await Store.UpdateAsync(running, CancellationToken.None);

var result = await Store.FindAsync(job.Id, CancellationToken.None);
result!.Status.Should().Be(JobStatus.Scheduled);
result.RetriesCount.Should().Be(1);
Assert.Equal(JobStatus.Scheduled, result!.Status);
Assert.Equal(1, result.RetriesCount);
}

[Fact]
Expand All @@ -261,7 +260,7 @@ public async Task DataPersistedAcrossStoreInstances()
var freshStore = freshScope.ServiceProvider.GetRequiredService<IStore>();
var result = await freshStore.FindAsync(job.Id, CancellationToken.None);

result.Should().NotBeNull();
result.Id.Should().Be(job.Id);
Assert.NotNull(result);
Assert.Equal(job.Id, result.Id);
}
}
29 changes: 14 additions & 15 deletions src/RunWay.Storage.Tests/StoreRunnerTestsBase.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using FluentAssertions;
using Kododo.RunWay.Core.Runners;
using Xunit;

Expand All @@ -13,7 +12,7 @@ protected StoreRunnerTestsBase(TFixture fixture) : base(fixture) { }
public async Task RegisterRunnerAsync_ReturnsNonEmptyRunnerId()
{
var runnerId = await Store.RegisterRunnerAsync("runner-1", ["JobA"], DateTimeOffset.UtcNow.AddMinutes(1), CancellationToken.None);
runnerId.Value.Should().NotBeNullOrEmpty();
Assert.False(string.IsNullOrEmpty(runnerId.Value));
}

[Fact]
Expand All @@ -23,7 +22,7 @@ public async Task RegisterRunnerAsync_RunnerAppearsInGetAll()

var runners = await Store.GetAllRunnersAsync(CancellationToken.None);

runners.Should().ContainSingle(r => r.Id == runnerId);
Assert.Single(runners, r => r.Id == runnerId);
}

[Fact]
Expand All @@ -33,14 +32,14 @@ public async Task RegisterRunnerAsync_PersistsName()

var runners = await Store.GetAllRunnersAsync(CancellationToken.None);

runners.Should().ContainSingle(r => r.Name == "my-runner");
Assert.Single(runners, r => r.Name == "my-runner");
}

[Fact]
public async Task GetAllRunnersAsync_WhenNoRunners_ReturnsEmpty()
{
var runners = await Store.GetAllRunnersAsync(CancellationToken.None);
runners.Should().BeEmpty();
Assert.Empty(runners);
}

[Fact]
Expand All @@ -51,8 +50,8 @@ public async Task GetAllRunnersAsync_ReturnsAllRegisteredRunners()

var runners = await Store.GetAllRunnersAsync(CancellationToken.None);

runners.Should().HaveCount(2);
runners.Select(r => r.Name).Should().BeEquivalentTo("runner-1", "runner-2");
Assert.Equal(2, runners.Count);
Assert.Equal(new[] { "runner-1", "runner-2" }, runners.Select(r => r.Name).Order(StringComparer.Ordinal));
}

[Fact]
Expand All @@ -62,7 +61,7 @@ public async Task GetAllRunnersAsync_RecentlyRegisteredRunner_IsOnline()

var runners = await Store.GetAllRunnersAsync(CancellationToken.None);

runners.Single().Status.Should().Be(RunnerStatus.Online);
Assert.Equal(RunnerStatus.Online, runners.Single().Status);
}

[Fact]
Expand All @@ -71,7 +70,7 @@ public async Task KeepAliveAsync_DoesNotThrow()
var runnerId = await Store.RegisterRunnerAsync("runner-1", ["JobA"], DateTimeOffset.UtcNow.AddMinutes(1), CancellationToken.None);

var act = async () => await Store.KeepAliveAsync(runnerId, DateTimeOffset.UtcNow.AddMinutes(1), CancellationToken.None);
await act.Should().NotThrowAsync();
Assert.Null(await Record.ExceptionAsync(act));
}

[Fact]
Expand All @@ -81,7 +80,7 @@ public async Task RemoveRunnerAsync_RunnerNoLongerInGetAll()
await Store.RemoveRunnerAsync(runnerId, CancellationToken.None);

var runners = await Store.GetAllRunnersAsync(CancellationToken.None);
runners.Should().NotContain(r => r.Id == runnerId);
Assert.DoesNotContain(runners, r => r.Id == runnerId);
}

[Fact]
Expand All @@ -93,7 +92,7 @@ public async Task RemoveRunnerAsync_OtherRunnersNotAffected()
await Store.RemoveRunnerAsync(runnerId1, CancellationToken.None);

var runners = await Store.GetAllRunnersAsync(CancellationToken.None);
runners.Should().ContainSingle(r => r.Id == runnerId2);
Assert.Single(runners, r => r.Id == runnerId2);
}

[Fact]
Expand All @@ -103,7 +102,7 @@ public async Task GetRunnerDetailsAsync_ReturnsCorrectName()

var details = await Store.GetRunnerDetailsAsync(runnerId, CancellationToken.None);

details.Name.Should().Be("details-runner");
Assert.Equal("details-runner", details.Name);
}

[Fact]
Expand All @@ -113,7 +112,7 @@ public async Task GetRunnerDetailsAsync_ReturnsRegisteredJobTypes()

var details = await Store.GetRunnerDetailsAsync(runnerId, CancellationToken.None);

details.JobTypes.Should().BeEquivalentTo("JobA", "JobB");
Assert.Equal(new[] { "JobA", "JobB" }, details.JobTypes.Order(StringComparer.Ordinal));
}

[Fact]
Expand All @@ -123,13 +122,13 @@ public async Task GetRunnerDetailsAsync_InitiallyHasNoJobs()

var details = await Store.GetRunnerDetailsAsync(runnerId, CancellationToken.None);

details.Jobs.Should().BeEmpty();
Assert.Empty(details.Jobs);
}

[Fact]
public async Task InitializeAsync_WhenCalledTwice_IsIdempotent()
{
var act = async () => await Store.InitializeAsync(CancellationToken.None);
await act.Should().NotThrowAsync();
Assert.Null(await Record.ExceptionAsync(act));
}
}
Loading
Loading