Files
ClawdDotNet/tests/ClawdDotNet.Core.Tests/Scheduling/CronExpressionTests.cs
T
RichardandClaude Opus 5 8ab9d99abe Cron: Bereich mit Schritt (7-22/3) lesen, ungueltige Ausdruecke im Scanner melden (#597)
Der Nachrichten-Digest im Dauerbetrieb der Test-VM lief nie: CronExpression
verstand 'von-bis/schritt' nicht, TaskSchedule verschluckte den Fehler.
Version 0.1.5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 15:51:36 +02:00

112 lines
4.4 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 und Terminberechnung (deckt Teile von B6/B7 ab). Der bisher ungetestete
/// Basis-Scheduler bekommt hier sein Netz: gültige Ausdrücke rechnen richtig, ungültige
/// werfen eine saubere <see cref="FormatException"/> statt einer rohen Parser-Exception
/// oder einer Endlosschleife.
/// </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: korrekte nächste Zeitpunkte ───
[Fact]
public void Alle_30_Minuten()
{
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 Werktags_um_drei_Uhr_frueh()
{
var cron = CronExpression.Parse("0 3 * * 1-5"); // Mo–Fr
// 2026-08-01 ist ein Samstag → nächster Lauf Montag, 2026-08-03 03:00
cron.GetNextOccurrence(Local(2026, 8, 1, 4, 0)).ShouldBe(Local(2026, 8, 3, 3, 0));
}
[Fact]
public void Listen_von_Minuten()
{
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 Schaltjahr_29_Februar()
{
var cron = CronExpression.Parse("0 0 29 2 *"); // nur am 29.02.
// 2028 ist das nächste Schaltjahr
cron.GetNextOccurrence(Local(2026, 3, 1, 0, 0)).ShouldBe(Local(2028, 2, 29, 0, 0));
}
[Fact]
public void Bereich_mit_Schritt_alle_drei_Stunden_tagsueber()
{
// #597: "0 7-22/3 * * *" warf vorher eine FormatException, die Aufgabe lief nie.
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 Zahl_mit_Schritt_laeuft_bis_zum_Feldende()
{
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: Invariante ───
[Theory]
[InlineData("*/5 * * * *")]
[InlineData("0 3 * * 1-5")]
[InlineData("15,45 9-17 * * *")]
[InlineData("0 0 1 * *")]
public void Der_naechste_Termin_liegt_echt_in_der_Zukunft_und_passt(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: ungültige Ausdrücke ───
[Theory]
[InlineData("* * *")] // zu wenige Felder
[InlineData("* * * * * *")] // zu viele Felder
[InlineData("99 * * * *")] // Minute außerhalb 0-59
[InlineData("* 25 * * *")] // Stunde außerhalb 0-23
[InlineData("a b c d e")] // keine Zahlen
[InlineData("*/0 * * * *")] // Schritt 0 (wäre Endlosschleife)
[InlineData("5-2 * * * *")] // rückwärtiger Bereich
[InlineData("0 22-7/3 * * *")] // rückwärtiger Bereich mit Schritt
[InlineData("0 7-25/3 * * *")] // Bereichsende außerhalb 0-23
[InlineData("0 7-/3 * * *")] // halber Bereich
[InlineData("* * * * ")] // ein leeres Feld nach Trim → 4 Felder
public void Ungueltige_Ausdruecke_werfen_eine_saubere_FormatException(string expr)
=> Should.Throw<FormatException>(() => CronExpression.Parse(expr));
[Fact]
public void Ein_gueltiger_Ausdruck_mit_Schritt_bleibt_endlich()
{
// Regressionswächter: */0 hätte früher eine Endlosschleife erzeugt.
Should.CompleteIn(() => CronExpression.Parse("*/15 * * * *"), TimeSpan.FromSeconds(2));
}
}