fix: Uhrzeit-Wraparound-Flakiness in MissingTeachingTime-Tests
CI / build-and-test (push) Canceled after 0s
CI / build-and-test (push) Canceled after 0s
TimeOnly.FromDateTime(DateTime.Now).AddHours(+-2) wickelt bei Mitternacht um (TimeOnly hat keinen Datumsanteil) - je nach Ausfuehrungsuhrzeit (ca. 22:00-02:00) landete "+2 Stunden" zeitlich vor statt nach "jetzt" oder umgekehrt, wodurch zwei Tests zufaellig rot wurden statt das eigentliche Verhalten (Wartezeit nach Unterrichtsende) zu pruefen. Per Gegenprobe auf unveraendertem main bestaetigt: dieselbe Flakiness besteht dort bereits. DashboardViewModel erhaelt einen optionalen Func<DateTime>? now-Konstruktorparameter (Default DateTime.Now, gleiches Muster wie die bereits bestehenden optionalen annualPlanEvents/ schoolWeather-Parameter) und nutzt ihn statt direkter DateTime.Now-Aufrufe in Load() und LoadMissingTeachingTime. Die beiden betroffenen Tests injizieren jetzt einen festen Referenzzeitpunkt (ein Feiertags-freier Dienstag im September) und sind damit unabhaengig von der tatsaechlichen Ausfuehrungsuhrzeit deterministisch gruen. Details siehe TODO.md, Abschnitt 9 (Dashboard).
This commit is contained in:
@@ -122,7 +122,7 @@ public sealed class DashboardViewModelTests
|
||||
FakeLessons? lessons = null, FakeSubstitutionEntries? substitutions = null,
|
||||
FakeSessions? sessions = null, FakeEntries? entries = null,
|
||||
FakeAnnualPlanEvents? annualPlanEvents = null, List<LearningGroup>? allGroups = null,
|
||||
FakeTimeEntries? timeEntries = null)
|
||||
FakeTimeEntries? timeEntries = null, Func<DateTime>? now = null)
|
||||
{
|
||||
lessons ??= new FakeLessons();
|
||||
lessons.Add(lesson);
|
||||
@@ -136,7 +136,8 @@ public sealed class DashboardViewModelTests
|
||||
new AttendanceBalanceService(), new SchoolYearService(), dashboardSettings ?? NewDashboardSettings(),
|
||||
schoolHolidays ?? new FakeSchoolHolidays(), new PublicHolidayService(), NewCalendarSettings(),
|
||||
substitutions ?? new FakeSubstitutionEntries(), timeEntries ?? new FakeTimeEntries(),
|
||||
TestSupport.BuildUntisHubService(), TestSupport.BuildWebUntisIntegrationService(), annualPlanEvents);
|
||||
TestSupport.BuildUntisHubService(), TestSupport.BuildWebUntisIntegrationService(), annualPlanEvents,
|
||||
annualPlanSync: null, schoolWeather: null, now: now);
|
||||
}
|
||||
|
||||
/// Montag einer Woche, die garantiert in der Zukunft liegt und innerhalb der
|
||||
@@ -278,20 +279,31 @@ public sealed class DashboardViewModelTests
|
||||
Assert.DoesNotContain(Items(vm, AttentionKind.MissingTeachingTime), i => MissingTimeDate(i) == pastDay);
|
||||
}
|
||||
|
||||
/// Fixer Referenzzeitpunkt fuer die beiden "heute, kurz vor/nach Ablauf der Wartezeit"-Tests
|
||||
/// unten. Regression: die Tests bauten "Unterrichtsende" bisher aus TimeOnly.FromDateTime(
|
||||
/// DateTime.Now).AddHours(±2) — TimeOnly wickelt bei Mitternacht um, ausgefuehrt zwischen ca.
|
||||
/// 22:00 und 02:00 Uhr wurde dadurch aus "+2h" ein Ende VOR "jetzt" (oder umgekehrt), je nach
|
||||
/// Tageszeit zufaellig rot. September ist bewusst gewaehlt: keiner der bundesweiten oder
|
||||
/// laenderspezifischen Feiertage (PublicHolidayService) faellt in diesen Monat, ein Dienstag
|
||||
/// ist garantiert kein Wochenende — die injizierte Uhr (DashboardViewModel now:-Parameter)
|
||||
/// macht "jetzt" fuer den Test unabhaengig von der tatsaechlichen Ausfuehrungsuhrzeit.
|
||||
private static readonly DateTime FixedNow = new(2026, 9, 8, 10, 0, 0);
|
||||
|
||||
[Fact]
|
||||
public void MissingTeachingTime_HeuteVorAblaufDerWartezeit_WirdNichtGemeldet()
|
||||
{
|
||||
var group = new LearningGroup { Name = "9c" };
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
var today = DateOnly.FromDateTime(FixedNow);
|
||||
var slots = new FakeTimetableSlots();
|
||||
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 1 });
|
||||
var periodSchedule = NewPeriodSchedule();
|
||||
// Unterrichtsende liegt garantiert noch keine 30 Minuten zurück.
|
||||
var futureEnd = TimeOnly.FromDateTime(DateTime.Now).AddHours(2);
|
||||
// Unterrichtsende liegt (relativ zur fixen Uhr FixedNow) noch keine 30 Minuten zurück.
|
||||
var futureEnd = TimeOnly.FromDateTime(FixedNow).AddHours(2);
|
||||
periodSchedule.SetPeriods([new PeriodTimeEntry
|
||||
{ PeriodNumber = 1, Start = futureEnd.AddHours(-1), End = futureEnd }]);
|
||||
|
||||
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, slots: slots, periodSchedule: periodSchedule);
|
||||
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, slots: slots,
|
||||
periodSchedule: periodSchedule, now: () => FixedNow);
|
||||
|
||||
Assert.DoesNotContain(Items(vm, AttentionKind.MissingTeachingTime), i => MissingTimeDate(i) == today);
|
||||
}
|
||||
@@ -300,16 +312,17 @@ public sealed class DashboardViewModelTests
|
||||
public void MissingTeachingTime_HeuteNachAblaufDerWartezeit_WirdGemeldet()
|
||||
{
|
||||
var group = new LearningGroup { Name = "9c" };
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
var today = DateOnly.FromDateTime(FixedNow);
|
||||
var slots = new FakeTimetableSlots();
|
||||
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 1 });
|
||||
var periodSchedule = NewPeriodSchedule();
|
||||
// Unterrichtsende liegt garantiert mehr als 30 Minuten zurück.
|
||||
var pastEnd = TimeOnly.FromDateTime(DateTime.Now).AddHours(-2);
|
||||
// Unterrichtsende liegt (relativ zur fixen Uhr FixedNow) mehr als 30 Minuten zurück.
|
||||
var pastEnd = TimeOnly.FromDateTime(FixedNow).AddHours(-2);
|
||||
periodSchedule.SetPeriods([new PeriodTimeEntry
|
||||
{ PeriodNumber = 1, Start = pastEnd.AddHours(-1), End = pastEnd }]);
|
||||
|
||||
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, slots: slots, periodSchedule: periodSchedule);
|
||||
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, slots: slots,
|
||||
periodSchedule: periodSchedule, now: () => FixedNow);
|
||||
|
||||
Assert.Contains(Items(vm, AttentionKind.MissingTeachingTime), i => MissingTimeDate(i) == today);
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ public partial class DashboardViewModel : ObservableObject
|
||||
private readonly SchoolWeatherService? _schoolWeather;
|
||||
private readonly UntisHubService _untisHub;
|
||||
private readonly WebUntisIntegrationService _webUntis;
|
||||
private readonly Func<DateTime> _now;
|
||||
|
||||
private const int OpenExcuseMaxAgeDays = 21;
|
||||
private const int SupportPlanDueWithinDays = 14;
|
||||
@@ -178,7 +179,8 @@ public partial class DashboardViewModel : ObservableObject
|
||||
ISubstitutionEntryRepository substitutions, ITimeEntryRepository timeEntries,
|
||||
UntisHubService untisHub, WebUntisIntegrationService webUntis,
|
||||
IAnnualPlanEventRepository? annualPlanEvents = null,
|
||||
AnnualPlanSyncService? annualPlanSync = null, SchoolWeatherService? schoolWeather = null)
|
||||
AnnualPlanSyncService? annualPlanSync = null, SchoolWeatherService? schoolWeather = null,
|
||||
Func<DateTime>? now = null)
|
||||
{
|
||||
_groups = groups; _subjects = subjects; _lessons = lessons; _exams = exams; _tasks = tasks;
|
||||
_examResults = examResults; _grades = grades; _reportGrades = reportGrades; _memberships = memberships;
|
||||
@@ -193,6 +195,11 @@ public partial class DashboardViewModel : ObservableObject
|
||||
_schoolWeather = schoolWeather;
|
||||
_untisHub = untisHub;
|
||||
_webUntis = webUntis;
|
||||
// Testbare Uhr statt direkter DateTime.Now-Aufrufe (siehe Load()/LoadMissingTeachingTime):
|
||||
// TimeOnly.AddHours()/AddMinutes() wickelt bei Mitternacht um, ohne injizierbares "jetzt"
|
||||
// waeren Tests fuer "kurz vor/nach Ablauf einer Wartezeit" je nach Ausfuehrungsuhrzeit
|
||||
// zufaellig rot oder gruen (siehe TODO.md, Abschnitt 9, Nachtrag Uhrzeit-Wraparound).
|
||||
_now = now ?? (() => DateTime.Now);
|
||||
if (annualPlanSync is not null)
|
||||
{
|
||||
annualPlanSync.DataChanged += () => Avalonia.Threading.Dispatcher.UIThread.Post(LoadCalendar);
|
||||
@@ -222,7 +229,7 @@ public partial class DashboardViewModel : ObservableObject
|
||||
|
||||
private void Load()
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
var now = _now();
|
||||
var today = DateOnly.FromDateTime(now);
|
||||
CurrentDate = now.ToString("dddd, d. MMMM yyyy", De);
|
||||
CurrentSchoolYear = _sy.CurrentSchoolYear();
|
||||
@@ -433,7 +440,7 @@ public partial class DashboardViewModel : ObservableObject
|
||||
.SelectMany(y => _publicHolidays.GetHolidays(y, _calendarSettings.State))
|
||||
.Select(h => h.Date).ToHashSet();
|
||||
var schoolHolidays = _schoolHolidays.GetAll();
|
||||
var nowTime = TimeOnly.FromDateTime(DateTime.Now);
|
||||
var nowTime = TimeOnly.FromDateTime(_now());
|
||||
|
||||
var items = new List<MissingTeachingTimeItem>();
|
||||
for (var date = firstDay; date <= today; date = date.AddDays(1))
|
||||
|
||||
@@ -3265,6 +3265,19 @@ Sitzungszustand (keine Persistenz in `dashboardsettings.json`, das dortige Forma
|
||||
Kachel-Liste ohne Platz für Sub-Filter je Art). "Klausurwochen" bleibt weiterhin eine eigene Kachel
|
||||
(Wochen-Aggregat, kein Einzelvorgang zum Abhaken/Anklicken).
|
||||
|
||||
**Nachtrag Uhrzeit-Wraparound-Bug (September 2026):** Zwei `MissingTeachingTime`-Tests bauten ihr
|
||||
"Unterrichtsende" bisher aus `TimeOnly.FromDateTime(DateTime.Now).AddHours(±2)`. `TimeOnly` wickelt
|
||||
bei Mitternacht um (kein Datumsanteil) — bei Testausführung zwischen ca. 22:00 und 02:00 Uhr wurde
|
||||
aus "+2 Stunden" dadurch ein Ende, das *vor* statt nach dem echten Ablauf des 30-Minuten-Fensters
|
||||
lag (oder umgekehrt bei "-2 Stunden"), je nach Ausführungsuhrzeit zufällig rot. `DashboardViewModel`
|
||||
erhält deshalb einen optionalen `Func<DateTime>? now`-Konstruktorparameter (Default `DateTime.Now`,
|
||||
gleiches Muster wie die bestehenden optionalen `annualPlanEvents`/`schoolWeather`-Parameter — DI
|
||||
löst unregistrierte optionale Parameter über ihren Default auf) und nutzt ihn in `Load()` sowie
|
||||
`LoadMissingTeachingTime` statt direkter `DateTime.Now`-Aufrufe. Die beiden betroffenen Tests
|
||||
injizieren jetzt einen festen Referenzzeitpunkt (`FixedNow`, ein Dienstag im September — bewusst
|
||||
gewählt, weil in diesem Monat kein bundesweiter oder länderspezifischer Feiertag liegt) und sind
|
||||
damit unabhängig von der tatsächlichen Ausführungsuhrzeit deterministisch grün.
|
||||
|
||||
---
|
||||
|
||||
## 10. Sync & Server
|
||||
|
||||
Reference in New Issue
Block a user