diff --git a/tests/NCronJob.Tests/NCronJob.Tests.csproj b/tests/NCronJob.Tests/NCronJob.Tests.csproj index 292f3e4..6f255cd 100644 --- a/tests/NCronJob.Tests/NCronJob.Tests.csproj +++ b/tests/NCronJob.Tests/NCronJob.Tests.csproj @@ -17,7 +17,6 @@ - all diff --git a/tests/NCronJob.Tests/NCronJobIntegrationTests.cs b/tests/NCronJob.Tests/NCronJobIntegrationTests.cs index 3cac40d..648383a 100644 --- a/tests/NCronJob.Tests/NCronJobIntegrationTests.cs +++ b/tests/NCronJob.Tests/NCronJobIntegrationTests.cs @@ -4,8 +4,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Time.Testing; using Shouldly; -using TimeProviderExtensions; namespace NCronJob.Tests; @@ -14,13 +14,14 @@ public sealed class NCronJobIntegrationTests : JobIntegrationBase [Fact] public async Task CronJobThatIsScheduledEveryMinuteShouldBeExecuted() { - var fakeTimer = TimeProviderFactory.GetTimeProvider(); + var fakeTimer = new FakeTimeProvider(); ServiceCollection.AddSingleton(fakeTimer); ServiceCollection.AddNCronJob(n => n.AddJob(p => p.WithCronExpression("* * * * *"))); var provider = CreateServiceProvider(); await provider.GetRequiredService().StartAsync(CancellationToken); + fakeTimer.Advance(TimeSpan.FromMinutes(1)); var jobFinished = await WaitForJobsOrTimeout(1); jobFinished.ShouldBeTrue(); } @@ -28,21 +29,37 @@ public async Task CronJobThatIsScheduledEveryMinuteShouldBeExecuted() [Fact] public async Task AdvancingTheWholeTimeShouldHaveTenEntries() { - var fakeTimer = TimeProviderFactory.GetTimeProvider(); + var fakeTimer = new FakeTimeProvider(); ServiceCollection.AddSingleton(fakeTimer); ServiceCollection.AddNCronJob(n => n.AddJob(p => p.WithCronExpression("* * * * *"))); var provider = CreateServiceProvider(); await provider.GetRequiredService().StartAsync(CancellationToken); - var jobFinished = await WaitForJobsOrTimeout(10); + void AdvanceTime() => fakeTimer.Advance(TimeSpan.FromMinutes(1)); + var jobFinished = await WaitForJobsOrTimeout(10, AdvanceTime); + jobFinished.ShouldBeTrue(); } + [Fact] + public async Task JobsShouldCancelOnCancellation() + { + var fakeTimer = new FakeTimeProvider(); + ServiceCollection.AddSingleton(fakeTimer); + ServiceCollection.AddNCronJob(n => n.AddJob(p => p.WithCronExpression("* * * * *"))); + var provider = CreateServiceProvider(); + + await provider.GetRequiredService().StartAsync(CancellationToken); + + var jobFinished = await DoNotWaitJustCancel(10); + jobFinished.ShouldBeFalse(); + } + [Fact] public async Task EachJobRunHasItsOwnScope() { - var fakeTimer = new ManualTimeProvider(); + var fakeTimer = new FakeTimeProvider(); var storage = new Storage(); ServiceCollection.AddSingleton(fakeTimer); ServiceCollection.AddSingleton(storage); @@ -63,7 +80,7 @@ public async Task EachJobRunHasItsOwnScope() [Fact] public async Task ExecuteAnInstantJob() { - var fakeTimer = TimeProviderFactory.GetTimeProvider(); + var fakeTimer = new FakeTimeProvider(); ServiceCollection.AddSingleton(fakeTimer); ServiceCollection.AddNCronJob(n => n.AddJob()); var provider = CreateServiceProvider(); @@ -78,13 +95,15 @@ public async Task ExecuteAnInstantJob() [Fact] public async Task CronJobShouldPassDownParameter() { - var fakeTimer = TimeProviderFactory.GetTimeProvider(); + var fakeTimer = new FakeTimeProvider(); ServiceCollection.AddSingleton(fakeTimer); ServiceCollection.AddNCronJob(n => n.AddJob(p => p.WithCronExpression("* * * * *").WithParameter("Hello World"))); var provider = CreateServiceProvider(); await provider.GetRequiredService().StartAsync(CancellationToken); + fakeTimer.Advance(TimeSpan.FromMinutes(1)); + var content = await CommunicationChannel.Reader.ReadAsync(CancellationToken); content.ShouldBe("Hello World"); } @@ -92,7 +111,7 @@ public async Task CronJobShouldPassDownParameter() [Fact] public async Task InstantJobShouldGetParameter() { - var fakeTimer = TimeProviderFactory.GetTimeProvider(); + var fakeTimer = new FakeTimeProvider(); ServiceCollection.AddSingleton(fakeTimer); ServiceCollection.AddNCronJob(n => n.AddJob()); var provider = CreateServiceProvider(); @@ -107,21 +126,25 @@ public async Task InstantJobShouldGetParameter() [Fact] public async Task CronJobThatIsScheduledEverySecondShouldBeExecuted() { - var fakeTimer = TimeProviderFactory.GetTimeProvider(TimeSpan.FromSeconds(1)); + var fakeTimer = new FakeTimeProvider(); + fakeTimer.Advance(TimeSpan.FromSeconds(1)); ServiceCollection.AddSingleton(fakeTimer); ServiceCollection.AddNCronJob(n => n.AddJob(p => p.WithCronExpression("* * * * * *", true))); var provider = CreateServiceProvider(); await provider.GetRequiredService().StartAsync(CancellationToken); - var jobFinished = await WaitForJobsOrTimeout(2); + void AdvanceTime() => fakeTimer.Advance(TimeSpan.FromSeconds(1)); + var jobFinished = await WaitForJobsOrTimeout(10, AdvanceTime); + jobFinished.ShouldBeTrue(); } [Fact] public async Task CanRunSecondPrecisionAndMinutePrecisionJobs() { - var fakeTimer = TimeProviderFactory.GetTimeProvider(TimeSpan.FromSeconds(1)); + var fakeTimer = new FakeTimeProvider(); + fakeTimer.Advance(TimeSpan.FromSeconds(1)); ServiceCollection.AddSingleton(fakeTimer); ServiceCollection.AddNCronJob(n => n.AddJob( p => p.WithCronExpression("* * * * * *", true).And.WithCronExpression("* * * * *"))); @@ -129,14 +152,15 @@ public async Task CanRunSecondPrecisionAndMinutePrecisionJobs() await provider.GetRequiredService().StartAsync(CancellationToken); - var jobFinished = await WaitForJobsOrTimeout(61); + void AdvanceTime() => fakeTimer.Advance(TimeSpan.FromSeconds(1)); + var jobFinished = await WaitForJobsOrTimeout(61, AdvanceTime); jobFinished.ShouldBeTrue(); } [Fact] public async Task LongRunningJobShouldNotBlockScheduler() { - var fakeTimer = TimeProviderFactory.GetTimeProvider(); + var fakeTimer = new FakeTimeProvider(); ServiceCollection.AddSingleton(fakeTimer); ServiceCollection.AddNCronJob(n => n .AddJob(p => p.WithCronExpression("* * * * *")) @@ -145,6 +169,7 @@ public async Task LongRunningJobShouldNotBlockScheduler() await provider.GetRequiredService().StartAsync(CancellationToken); + fakeTimer.Advance(TimeSpan.FromMinutes(1)); var jobFinished = await WaitForJobsOrTimeout(1); jobFinished.ShouldBeTrue(); } @@ -152,7 +177,7 @@ public async Task LongRunningJobShouldNotBlockScheduler() [Fact] public async Task NotRegisteredJobShouldNotAbortOtherRuns() { - var fakeTimer = TimeProviderFactory.GetTimeProvider(); + var fakeTimer = new FakeTimeProvider(); ServiceCollection.AddSingleton(fakeTimer); ServiceCollection.AddNCronJob(n => n.AddJob(p => p.WithCronExpression("* * * * *"))); ServiceCollection.AddTransient(); @@ -161,21 +186,22 @@ public async Task NotRegisteredJobShouldNotAbortOtherRuns() await provider.GetRequiredService().StartAsync(CancellationToken); + fakeTimer.Advance(TimeSpan.FromMinutes(1)); var jobFinished = await WaitForJobsOrTimeout(1); jobFinished.ShouldBeTrue(); } [Fact] - public void ThrowIfJobWithDependenciesIsNotRegistered() + public async Task ThrowIfJobWithDependenciesIsNotRegistered() { ServiceCollection .AddNCronJob(n => n.AddJob(p => p.WithCronExpression("* * * * *"))); var provider = CreateServiceProvider(); - Assert.Throws(() => + await Assert.ThrowsAsync(async () => { - using var executor = new JobExecutor(provider, NullLogger.Instance); - executor.RunJob(new RegistryEntry(typeof(JobWithDependency), new JobExecutionContext(null), null), CancellationToken.None); + using var executor = provider.CreateScope().ServiceProvider.GetRequiredService(); + await executor.RunJob(new RegistryEntry(typeof(JobWithDependency), new JobExecutionContext(null!, null), null), CancellationToken.None); }); } @@ -192,16 +218,23 @@ private sealed class Storage private sealed class SimpleJob(ChannelWriter writer) : IJob { public async Task RunAsync(JobExecutionContext context, CancellationToken token) - => await writer.WriteAsync(true, token); + { + try + { + context.Output = "Job Completed"; + await writer.WriteAsync(context.Output, token); + } + catch (Exception ex) + { + await writer.WriteAsync(ex, token); + } + } } - private sealed class LongRunningJob : IJob + private sealed class LongRunningJob(TimeProvider timeProvider) : IJob { - public Task RunAsync(JobExecutionContext context, CancellationToken token) - { - Task.Delay(1000, token).GetAwaiter().GetResult(); - return Task.CompletedTask; - } + public async Task RunAsync(JobExecutionContext context, CancellationToken token) => + await Task.Delay(TimeSpan.FromSeconds(10), timeProvider, token); } private sealed class ScopedServiceJob(ChannelWriter writer, Storage storage, GuidGenerator guidGenerator) : IJob @@ -224,4 +257,4 @@ private sealed class JobWithDependency(ChannelWriter writer, GuidGenerat public async Task RunAsync(JobExecutionContext context, CancellationToken token) => await writer.WriteAsync(guidGenerator.NewGuid, token); } -} +} \ No newline at end of file diff --git a/tests/NCronJob.Tests/NCronJobRetryTests.cs b/tests/NCronJob.Tests/NCronJobRetryTests.cs index 9028e00..f454f6b 100644 --- a/tests/NCronJob.Tests/NCronJobRetryTests.cs +++ b/tests/NCronJob.Tests/NCronJobRetryTests.cs @@ -24,9 +24,6 @@ public async Task JobShouldRetryOnFailure() fakeTimer.Advance(TimeSpan.FromMinutes(1)); - var jobFinished = await WaitForJobsOrTimeout(1); - jobFinished.ShouldBeTrue(); - // Validate that the job was retried the correct number of times // Fail 3 times = 3 retries + 1 success var attempts = await CommunicationChannel.Reader.ReadAsync(CancellationToken); @@ -47,9 +44,6 @@ public async Task JobWithCustomPolicyShouldRetryOnFailure() fakeTimer.Advance(TimeSpan.FromMinutes(1)); - var jobFinished = await WaitForJobsOrTimeout(1); - jobFinished.ShouldBeTrue(); - // Validate that the job was retried the correct number of times // Fail 3 times = 3 retries + 1 success var attempts = await CommunicationChannel.Reader.ReadAsync(CancellationToken); @@ -81,7 +75,7 @@ public async Task RunAsync(JobExecutionContext context, CancellationToken token) } - [RetryPolicy(3, 2)] + [RetryPolicy(3, 1)] private sealed class JobUsingCustomPolicy(ChannelWriter writer, MaxFailuresWrapper maxFailuresWrapper) : IJob { diff --git a/tests/NCronJob.Tests/TestHelper.cs b/tests/NCronJob.Tests/TestHelper.cs index 30e749c..046fbd9 100644 --- a/tests/NCronJob.Tests/TestHelper.cs +++ b/tests/NCronJob.Tests/TestHelper.cs @@ -1,6 +1,8 @@ +using System.Runtime.CompilerServices; using System.Threading.Channels; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Shouldly; namespace NCronJob.Tests; @@ -46,11 +48,24 @@ protected async Task WaitForJobsOrTimeout(int jobRuns) await Task.WhenAll(GetCompletionJobs(jobRuns, timeoutTcs.Token)); return true; } - catch (OperationCanceledException) + catch + { + return false; + } + } + + protected async Task WaitForJobsOrTimeout(int jobRuns, Action timeAdvancer) + { + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + try { + await foreach (var jobSuccessful in GetCompletionJobsAsync(jobRuns, timeAdvancer, timeoutCts.Token)) + { + jobSuccessful.ShouldBe("Job Completed"); + } return true; } - catch (Exception) + catch { return false; } @@ -77,4 +92,14 @@ protected IEnumerable GetCompletionJobs(int expectedJobCount, Cancellation yield return CommunicationChannel.Reader.ReadAsync(cancellationToken).AsTask(); } } + + private async IAsyncEnumerable GetCompletionJobsAsync(int expectedJobCount, Action timeAdvancer, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + for (var i = 0; i < expectedJobCount; i++) + { + timeAdvancer(); + var jobResult = await CommunicationChannel.Reader.ReadAsync(cancellationToken); + yield return jobResult; + } + } } diff --git a/tests/NCronJob.Tests/TimeProviderFactory.cs b/tests/NCronJob.Tests/TimeProviderFactory.cs deleted file mode 100644 index b9e4104..0000000 --- a/tests/NCronJob.Tests/TimeProviderFactory.cs +++ /dev/null @@ -1,16 +0,0 @@ -using TimeProviderExtensions; - -namespace NCronJob.Tests; - -internal static class TimeProviderFactory -{ - public static ManualTimeProvider GetTimeProvider(TimeSpan? advanceTime = null) - => new() - { - AutoAdvanceBehavior = new AutoAdvanceBehavior - { - UtcNowAdvanceAmount = advanceTime ?? TimeSpan.Zero, - TimerAutoTriggerCount = 1, - }, - }; -}