Files
ClawdDotNet/tests/ClawdDotNet.Core.Tests/Scheduling/CronExpressionTests.cs
T
RichardandClaude Opus 5.5 77062facc9 Stufe 4c (#764): Bezeichner auf Englisch - Typen, Methoden, Felder, Parameter, Locals, Testnamen
Mit Roslyn umbenannt (Verweise folgen, Zeichenketten bleiben unberuehrt).
Nicht umbenannt, weil gespeichert oder ueber die API gebunden (Stufe 4e):
Properties, Record-Parameter, Enum-Member sowie die Parameter der
Endpunkt-Handler (Routen-/Query-Bindung ueber den Namen). Die JSON-Felder
anonymer Objekte bleiben ausdruecklich benannt (JsonFilter: pfad).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:18:43 +02:00

112 lines
4.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using ClawdDotNet.Core.Scheduling;
using Shouldly;
namespace ClawdDotNet.Core.Tests.Scheduling;
/// <summary>
/// Cron parsing and schedule calculation (covers parts of B6/B7). The previously untested
/// base scheduler gets its safety net here: valid expressions calculate correctly, invalid ones
/// throw a clean <see cref="FormatException"/> instead of a raw parser exception
/// or an endless loop.
/// </summary>
public sealed class CronExpressionTests
{
private static DateTime Local(int y, int mo, int d, int h, int mi)
=> new(y, mo, d, h, mi, 0, DateTimeKind.Local);
// ─── R1: correct next points in time ───
[Fact]
public void Every_30_minutes()
{
var cron = CronExpression.Parse("*/30 * * * *");
cron.GetNextOccurrence(Local(2026, 7, 31, 10, 5)).ShouldBe(Local(2026, 7, 31, 10, 30));
cron.GetNextOccurrence(Local(2026, 7, 31, 10, 45)).ShouldBe(Local(2026, 7, 31, 11, 0));
}
[Fact]
public void Weekdays_at_three_in_the_morning()
{
var cron = CronExpression.Parse("0 3 * * 1-5"); // Mo–Fr
// 2026-08-01 is a Saturday → next run Monday, 2026-08-03 03:00
cron.GetNextOccurrence(Local(2026, 8, 1, 4, 0)).ShouldBe(Local(2026, 8, 3, 3, 0));
}
[Fact]
public void Lists_of_minutes()
{
var cron = CronExpression.Parse("15,45 * * * *");
cron.GetNextOccurrence(Local(2026, 7, 31, 10, 20)).ShouldBe(Local(2026, 7, 31, 10, 45));
cron.GetNextOccurrence(Local(2026, 7, 31, 10, 50)).ShouldBe(Local(2026, 7, 31, 11, 15));
}
[Fact]
public void Leap_year_29_February()
{
var cron = CronExpression.Parse("0 0 29 2 *"); // only on 29 Feb
// 2028 is the next leap year
cron.GetNextOccurrence(Local(2026, 3, 1, 0, 0)).ShouldBe(Local(2028, 2, 29, 0, 0));
}
[Fact]
public void Range_with_step_every_three_hours_during_the_day()
{
// #597: "0 7-22/3 * * *" used to throw a FormatException, the task never ran.
var cron = CronExpression.Parse("0 7-22/3 * * *"); // 7, 10, 13, 16, 19, 22
cron.GetNextOccurrence(Local(2026, 9, 14, 12, 0)).ShouldBe(Local(2026, 9, 14, 13, 0));
cron.GetNextOccurrence(Local(2026, 9, 14, 22, 0)).ShouldBe(Local(2026, 9, 15, 7, 0));
cron.Matches(Local(2026, 9, 14, 8, 0)).ShouldBeFalse();
}
[Fact]
public void Number_with_step_runs_to_the_end_of_the_field()
{
var cron = CronExpression.Parse("10/20 * * * *"); // 10, 30, 50
cron.GetNextOccurrence(Local(2026, 9, 14, 12, 31)).ShouldBe(Local(2026, 9, 14, 12, 50));
cron.GetNextOccurrence(Local(2026, 9, 14, 12, 50)).ShouldBe(Local(2026, 9, 14, 13, 10));
}
// ─── R2: invariant ───
[Theory]
[InlineData("*/5 * * * *")]
[InlineData("0 3 * * 1-5")]
[InlineData("15,45 9-17 * * *")]
[InlineData("0 0 1 * *")]
public void The_next_occurrence_is_strictly_in_the_future_and_matches(string expr)
{
var cron = CronExpression.Parse(expr);
var now = Local(2026, 7, 31, 10, 23);
var next = cron.GetNextOccurrence(now);
next.ShouldNotBeNull();
next!.Value.ShouldBeGreaterThan(now);
cron.Matches(next.Value).ShouldBeTrue();
}
// ─── R3: invalid expressions ───
[Theory]
[InlineData("* * *")] // too few fields
[InlineData("* * * * * *")] // too many fields
[InlineData("99 * * * *")] // minute outside 0-59
[InlineData("* 25 * * *")] // hour outside 0-23
[InlineData("a b c d e")] // no numbers
[InlineData("*/0 * * * *")] // step 0 (would be an endless loop)
[InlineData("5-2 * * * *")] // backwards range
[InlineData("0 22-7/3 * * *")] // backwards range with step
[InlineData("0 7-25/3 * * *")] // range end outside 0-23
[InlineData("0 7-/3 * * *")] // half a range
[InlineData("* * * * ")] // an empty field after trim → 4 fields
public void Invalid_expressions_throw_a_clean_FormatException(string expr)
=> Should.Throw<FormatException>(() => CronExpression.Parse(expr));
[Fact]
public void A_valid_expression_with_step_stays_finite()
{
// Regression guard: */0 would formerly have produced an endless loop.
Should.CompleteIn(() => CronExpression.Parse("*/15 * * * *"), TimeSpan.FromSeconds(2));
}
}