Files
ClawdDotNet/tests/ClawdDotNet.Core.Tests/Scheduling/CronExpressionTests.cs
T
RichardandClaude Opus 5.5 837a634a22 Stufe 4c (#764): Kommentare in C#, WebUI-Skripten, CSS und HTML auf Englisch
Rund 2.600 Kommentarblöcke in 310 C#-Dateien übersetzt, Begründungen vollständig
erhalten: blockweise gleiche Anzahl wie vorher, kein Block unter 60 % seiner
Länge, der Code außerhalb der Kommentare per Tokenizer byte-identisch geprüft.
Zitate deutscher Oberflächentexte und Testdaten bleiben deutsch.

WebUI: Kommentare in JS/CSS/HTML übersetzt, JSDoc-@param an die in 4b
umbenannten Parameter angeglichen.

Dabei behoben:
- 4b hatte "tag" (HTML-Tag) als deutsches "Tag" gelesen und zu "dayValue"
  gemacht (makeElement, svg, Tagesübersicht) - zurück auf tag/day.
- Aufgaben "InProgress" bekamen die Plakettenart "laufend", für die es nie eine
  CSS-Klasse gab - jetzt "success" wie laufende Instanzen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 12:45:03 +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 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 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 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 *"); // 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 Bereich_mit_Schritt_alle_drei_Stunden_tagsueber()
{
// #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 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: invariant ───
[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: 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 Ungueltige_Ausdruecke_werfen_eine_saubere_FormatException(string expr)
=> Should.Throw<FormatException>(() => CronExpression.Parse(expr));
[Fact]
public void Ein_gueltiger_Ausdruck_mit_Schritt_bleibt_endlich()
{
// Regression guard: */0 would formerly have produced an endless loop.
Should.CompleteIn(() => CronExpression.Parse("*/15 * * * *"), TimeSpan.FromSeconds(2));
}
}