fix(stamdata): evaluate the profession validity window per call, not at type-load
Professions.ByProgram was a `static readonly` field filtered on DateTime.Today, so it evaluated once when the type first loaded. Two consequences, both real: - A long-running process kept serving the answer it computed at startup. A mapping whose geldigVan fell after boot never appeared; one whose geldigTot passed never disappeared. - Both branches of StamdataFile.ActiveOn were unreachable from this caller, which is why this table's validity window had no test at all. It is the cleanest single explanation for Stamdata's 71.7% branch coverage (BL-005). Adds ByProgramOn(DateOnly) — the peildatum as an argument, matching StamdataTable.RowsOn which already parameterizes it — and makes ByProgram a property delegating to it with today's date. Call sites (DiplomaRules) are unchanged and keep the same behaviour, now with a current date. ProfessionsTests covers both ActiveOn branches plus the regression itself: the same date must give the same answer, a different date a different one. Found by the testability pass (TE-009). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,14 +19,26 @@ public static class Professions
|
|||||||
/// <summary>Every mapping in the data-file, typed.</summary>
|
/// <summary>Every mapping in the data-file, typed.</summary>
|
||||||
public static readonly IReadOnlyList<ProfessionMapping> Mappings = StamdataFile.Load<ProfessionMapping>("professions");
|
public static readonly IReadOnlyList<ProfessionMapping> Mappings = StamdataFile.Load<ProfessionMapping>("professions");
|
||||||
|
|
||||||
/// <summary>The mappings valid today, as a program→beroep lookup. Consumers that don't
|
/// <summary>The mappings valid on <paramref name="on"/>, as a program→beroep lookup.
|
||||||
/// yet reason about a peildatum (e.g. <c>DiplomaRules.ProfessionFor</c>) use this — it
|
///
|
||||||
/// preserves the pre-valid-time behaviour exactly while the file's rows are all current.</summary>
|
/// Takes the peildatum as an argument rather than reading the clock. It used to be a
|
||||||
public static readonly IReadOnlyDictionary<string, string> ByProgram =
|
/// <c>static readonly</c> field filtered on <c>DateTime.Today</c>, which evaluated once at
|
||||||
Mappings.Where(m => StamdataFile.ActiveOn(m.GeldigVan, m.GeldigTot, DateOnly.FromDateTime(DateTime.Today)))
|
/// type-load: a long-running process kept yesterday's answer across midnight, and a mapping
|
||||||
|
/// whose <c>geldigVan</c> fell after startup never appeared at all. It also made both
|
||||||
|
/// branches of <see cref="StamdataFile.ActiveOn"/> permanently unreachable from here, which
|
||||||
|
/// is why this table's validity window was never exercised by a test.</summary>
|
||||||
|
public static IReadOnlyDictionary<string, string> ByProgramOn(DateOnly on) =>
|
||||||
|
Mappings.Where(m => StamdataFile.ActiveOn(m.GeldigVan, m.GeldigTot, on))
|
||||||
.ToDictionary(m => m.Program, m => m.Beroep, StringComparer.OrdinalIgnoreCase);
|
.ToDictionary(m => m.Program, m => m.Beroep, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
/// <summary>Distinct professions, in declaration order — the list a user may declare
|
/// <summary>The mappings valid today. Consumers that don't yet reason about a peildatum
|
||||||
/// for a manual (unlisted) diploma.</summary>
|
/// (e.g. <c>DiplomaRules.ProfessionFor</c>) use this — same behaviour as before, but
|
||||||
|
/// evaluated per call so the date is current.</summary>
|
||||||
|
public static IReadOnlyDictionary<string, string> ByProgram => ByProgramOn(Today());
|
||||||
|
|
||||||
|
/// <summary>Distinct professions valid today, in declaration order — the list a user may
|
||||||
|
/// declare for a manual (unlisted) diploma.</summary>
|
||||||
public static IReadOnlyList<string> All() => ByProgram.Values.Distinct().ToList();
|
public static IReadOnlyList<string> All() => ByProgram.Values.Distinct().ToList();
|
||||||
|
|
||||||
|
private static DateOnly Today() => DateOnly.FromDateTime(DateTime.Today);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using BigRegister.Stamdata;
|
||||||
|
|
||||||
|
namespace BigRegister.Tests.Domain;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The profession↔program map's validity window (TE-009). `ByProgram` used to be a
|
||||||
|
/// `static readonly` field filtered on `DateTime.Today` at type-load, so both branches of
|
||||||
|
/// `StamdataFile.ActiveOn` were unreachable from here and nothing asserted the window at all.
|
||||||
|
/// Now that the peildatum is a parameter, these are the two branches.
|
||||||
|
/// </summary>
|
||||||
|
public class ProfessionsTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void A_mapping_is_absent_before_its_geldigVan()
|
||||||
|
{
|
||||||
|
// Every seeded row starts 2000-01-01; nothing is valid the day before.
|
||||||
|
Assert.Empty(Professions.ByProgramOn(new DateOnly(1999, 12, 31)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void A_mapping_is_present_on_and_after_its_geldigVan()
|
||||||
|
{
|
||||||
|
Assert.Equal("Arts", Professions.ByProgramOn(new DateOnly(2000, 1, 1))["geneeskunde"]);
|
||||||
|
Assert.Equal("Arts", Professions.ByProgramOn(new DateOnly(2026, 8, 26))["geneeskunde"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void A_closed_mapping_is_absent_from_its_geldigTot_onwards()
|
||||||
|
{
|
||||||
|
// geldigTot is exclusive (`on < tot`), so the row drops out on the boundary date itself.
|
||||||
|
foreach (var m in Professions.Mappings.Where(m => m.GeldigTot is DateOnly))
|
||||||
|
{
|
||||||
|
var tot = m.GeldigTot!.Value;
|
||||||
|
Assert.True(Professions.ByProgramOn(tot.AddDays(-1)).ContainsKey(m.Program));
|
||||||
|
Assert.False(Professions.ByProgramOn(tot).ContainsKey(m.Program));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ByProgram_is_evaluated_per_call_not_captured_at_type_load()
|
||||||
|
{
|
||||||
|
// The regression this guards: a long-running process must not keep serving the answer
|
||||||
|
// it computed at startup. Same date in, same answer; different date in, different answer.
|
||||||
|
Assert.Equal(Professions.ByProgram.Count, Professions.ByProgramOn(DateOnly.FromDateTime(DateTime.Today)).Count);
|
||||||
|
Assert.NotEqual(Professions.ByProgram.Count, Professions.ByProgramOn(new DateOnly(1999, 12, 31)).Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user