diff --git a/FirstClassErrors.RequestBinder.UnitTests/BindingContractTests.cs b/FirstClassErrors.RequestBinder.UnitTests/BindingContractTests.cs index 663a401f..4fb5761d 100644 --- a/FirstClassErrors.RequestBinder.UnitTests/BindingContractTests.cs +++ b/FirstClassErrors.RequestBinder.UnitTests/BindingContractTests.cs @@ -33,8 +33,8 @@ public void ConverterMayFailWithAPrimaryPortError() { PropertySource body = bind.PropertiesOf(Request()); body.SimpleProperty(r => r.GuestEmail).AsRequired(_ => Outcome.Failure( - PrimaryPortError.Create(ErrorCode.Create("TEST_PORT_LEVEL_REJECTION"), Any.DiagnosticMessage(), Any.Transience()) - .WithPublicMessage(Any.ShortMessage()))); + PrimaryPortError.Create(ErrorCode.Create("TEST_PORT_LEVEL_REJECTION"), DiagnosticMessageFactory.Any(), TransienceFactory.Any()) + .WithPublicMessage(ShortMessageFactory.Any()))); Outcome outcome = bind.New(_ => "never"); Error invalid = outcome.Error!.InnerErrors.Single(); @@ -48,9 +48,9 @@ public void ConverterFailingWithAnotherFamilyIsABug() { PropertySource body = bind.PropertiesOf(Request()); InfrastructureError foreignFamily = - InfrastructureError.Create(Any.ErrorCode(), Any.DiagnosticMessage(), - Any.InteractionDirection(), Transience.Unknown) - .WithPublicMessage(Any.ShortMessage()); + InfrastructureError.Create(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any(), + InteractionDirectionFactory.Any(), Transience.Unknown) + .WithPublicMessage(ShortMessageFactory.Any()); InvalidOperationException exception = Assert.Throws( () => { body.SimpleProperty(r => r.GuestEmail).AsRequired(_ => Outcome.Failure(foreignFamily)); }); @@ -93,9 +93,9 @@ public void BareNestedElementFailureIsWrapped() { } private static PrimaryPortError LeafPortError() { - return PrimaryPortError.Create(ErrorCode.Create("TEST_NESTED_PORT_LEAF"), Any.DiagnosticMessage(), - Any.Transience()) - .WithPublicMessage(Any.ShortMessage()); + return PrimaryPortError.Create(ErrorCode.Create("TEST_NESTED_PORT_LEAF"), DiagnosticMessageFactory.Any(), + TransienceFactory.Any()) + .WithPublicMessage(ShortMessageFactory.Any()); } [Fact(DisplayName = "A required nested binding that fails with a bare PrimaryPortError leaf (not its build-terminal envelope) is wrapped, so the path survives.")] diff --git a/FirstClassErrors.RequestBinder.UnitTests/FirstClassErrors.RequestBinder.UnitTests.csproj b/FirstClassErrors.RequestBinder.UnitTests/FirstClassErrors.RequestBinder.UnitTests.csproj index 0aec0626..ec5f4f29 100644 --- a/FirstClassErrors.RequestBinder.UnitTests/FirstClassErrors.RequestBinder.UnitTests.csproj +++ b/FirstClassErrors.RequestBinder.UnitTests/FirstClassErrors.RequestBinder.UnitTests.csproj @@ -26,6 +26,9 @@ + + diff --git a/FirstClassErrors.RequestBinder.UnitTests/RequestBinderTests.cs b/FirstClassErrors.RequestBinder.UnitTests/RequestBinderTests.cs index cfd40384..c03d6621 100644 --- a/FirstClassErrors.RequestBinder.UnitTests/RequestBinderTests.cs +++ b/FirstClassErrors.RequestBinder.UnitTests/RequestBinderTests.cs @@ -27,8 +27,8 @@ private static Outcome BindStay(RequestBinder binder, StayDto dto) { private static Outcome AssembleStay(BookingDate checkIn, BookingDate checkOut) { return checkOut.Value > checkIn.Value ? Outcome.Success(new Stay(checkIn, checkOut)) - : Outcome.Failure(DomainError.Create(CheckOutNotAfterCheckIn, Any.DiagnosticMessage()) - .WithPublicMessage(Any.ShortMessage())); + : Outcome.Failure(DomainError.Create(CheckOutNotAfterCheckIn, DiagnosticMessageFactory.Any()) + .WithPublicMessage(ShortMessageFactory.Any())); } [Fact(DisplayName = "New assembles the command exactly once when every property bound.")] diff --git a/FirstClassErrors.UnitTests/ClockUseAnyTests.cs b/FirstClassErrors.Testing.UnitTests/ClockUseAnyTests.cs similarity index 86% rename from FirstClassErrors.UnitTests/ClockUseAnyTests.cs rename to FirstClassErrors.Testing.UnitTests/ClockUseAnyTests.cs index 8305adee..b68342a5 100644 --- a/FirstClassErrors.UnitTests/ClockUseAnyTests.cs +++ b/FirstClassErrors.Testing.UnitTests/ClockUseAnyTests.cs @@ -8,7 +8,7 @@ #endregion -namespace FirstClassErrors.UnitTests; +namespace FirstClassErrors.Testing.UnitTests; [TestSubject(typeof(Clock))] public sealed class ClockUseAnyTests { @@ -31,15 +31,15 @@ public void UseAnyFreezesASingleInstant() { } } - [Fact(DisplayName = "Inside Any.Reproducibly, Clock.UseAny picks the same instant for a given seed.")] + [Fact(DisplayName = "Inside Dummies.Any.Reproducibly, Clock.UseAny picks the same instant for a given seed.")] public void UseAnyIsReproducibleUnderAReproduciblyScope() { DateTimeOffset first = default; DateTimeOffset second = default; - Any.Reproducibly(42, () => { + Dummies.Any.Reproducibly(42, () => { using (Clock.UseAny()) { first = AnError().OccurredAt; } }); - Any.Reproducibly(42, () => { + Dummies.Any.Reproducibly(42, () => { using (Clock.UseAny()) { second = AnError().OccurredAt; } }); diff --git a/FirstClassErrors.Testing.UnitTests/FirstClassErrors.Testing.UnitTests.csproj b/FirstClassErrors.Testing.UnitTests/FirstClassErrors.Testing.UnitTests.csproj new file mode 100644 index 00000000..b847a7b3 --- /dev/null +++ b/FirstClassErrors.Testing.UnitTests/FirstClassErrors.Testing.UnitTests.csproj @@ -0,0 +1,35 @@ + + + + net10.0 + enable + enable + false + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + diff --git a/FirstClassErrors.UnitTests/InstanceIdsUseAnyTests.cs b/FirstClassErrors.Testing.UnitTests/InstanceIdsUseAnyTests.cs similarity index 88% rename from FirstClassErrors.UnitTests/InstanceIdsUseAnyTests.cs rename to FirstClassErrors.Testing.UnitTests/InstanceIdsUseAnyTests.cs index ee8097c7..666f0330 100644 --- a/FirstClassErrors.UnitTests/InstanceIdsUseAnyTests.cs +++ b/FirstClassErrors.Testing.UnitTests/InstanceIdsUseAnyTests.cs @@ -8,7 +8,7 @@ #endregion -namespace FirstClassErrors.UnitTests; +namespace FirstClassErrors.Testing.UnitTests; [TestSubject(typeof(InstanceIds))] public sealed class InstanceIdsUseAnyTests { @@ -31,20 +31,20 @@ public void UseAnyAssignsDistinctIds() { } } - [Fact(DisplayName = "Inside Any.Reproducibly, InstanceIds.UseAny reproduces the same id sequence for a given seed.")] + [Fact(DisplayName = "Inside Dummies.Any.Reproducibly, InstanceIds.UseAny reproduces the same id sequence for a given seed.")] public void UseAnyIsReproducibleUnderAReproduciblyScope() { Guid firstRunA = Guid.Empty; Guid firstRunB = Guid.Empty; Guid secondRunA = Guid.Empty; Guid secondRunB = Guid.Empty; - Any.Reproducibly(7, () => { + Dummies.Any.Reproducibly(7, () => { using (InstanceIds.UseAny()) { firstRunA = AnError().InstanceId; firstRunB = AnError().InstanceId; } }); - Any.Reproducibly(7, () => { + Dummies.Any.Reproducibly(7, () => { using (InstanceIds.UseAny()) { secondRunA = AnError().InstanceId; secondRunB = AnError().InstanceId; diff --git a/FirstClassErrors.Testing.UnitTests/MeaningfulEnumFactoryTests.cs b/FirstClassErrors.Testing.UnitTests/MeaningfulEnumFactoryTests.cs new file mode 100644 index 00000000..7eee9b26 --- /dev/null +++ b/FirstClassErrors.Testing.UnitTests/MeaningfulEnumFactoryTests.cs @@ -0,0 +1,31 @@ +#region Usings declarations + +using FirstClassErrors.Testing; + +using NFluent; + +#endregion + +namespace FirstClassErrors.Testing.UnitTests; + +/// +/// Contract tests for the meaningful-enum factories: they must never yield the Unknown sentinel, which is +/// the whole reason they exist next to a plain Dummies.Any.Enum<T>() draw. +/// +public sealed class MeaningfulEnumFactoryTests { + + [Fact(DisplayName = "TransienceFactory.Any never returns the Unknown sentinel.")] + public void TransienceExcludesUnknown() { + for (int i = 0; i < 200; i++) { + Check.That(TransienceFactory.Any()).IsNotEqualTo(Transience.Unknown); + } + } + + [Fact(DisplayName = "InteractionDirectionFactory.Any never returns the Unknown sentinel.")] + public void InteractionDirectionExcludesUnknown() { + for (int i = 0; i < 200; i++) { + Check.That(InteractionDirectionFactory.Any()).IsNotEqualTo(InteractionDirection.Unknown); + } + } + +} diff --git a/FirstClassErrors.Testing/Any.cs b/FirstClassErrors.Testing/Any.cs deleted file mode 100644 index 72c8e925..00000000 --- a/FirstClassErrors.Testing/Any.cs +++ /dev/null @@ -1,263 +0,0 @@ -namespace FirstClassErrors.Testing; - -/// -/// Supplies arbitrary, valid values for the parts of a test that are not under assertion — the "any" a -/// test needs so its Arrange stops advertising values it never checks. Reach for it when a test says "give -/// me some error code / message / instant" and the exact value is irrelevant: an explicit -/// call reads as "this is arbitrary" where a hand-picked literal reads as "this matters". -/// -/// -/// -/// Every value is drawn from a pseudo-random source. By default that source is unseeded, so each run produces -/// fresh values — which surfaces a test that secretly depends on one. Wrap a value-sensitive test in -/// Reproducibly to make a failing run replayable. The source flows with the current execution context, -/// so it never leaks across tests running in parallel — the same contract the rest of this package keeps. -/// -/// -/// The values are valid (an is never blank, an -/// is a real UTC instant) but deliberately recognizable as arbitrary (codes look like -/// ANY_CODE_7F3A9C), so an arbitrary value that surfaces in a failure message is easy to spot. -/// -/// -/// is the error-aware surface; the generic value engine it delegates to is internal, so a -/// test never depends on it directly. -/// -/// -/// -/// // The order id is what the test asserts on; the message is not — so it is Any. -/// Outcome<Order> outcome = Outcome<Order>.Failure( -/// DomainError.Create(ErrorCode.Create("ORDER_NOT_FOUND"), Any.DiagnosticMessage()) -/// .WithPublicMessage(Any.ShortMessage())); -/// -/// // Make a value-sensitive test replayable: the seed is reported on failure... -/// Any.Reproducibly(() => { /* arrange with Any, act, assert */ }); -/// // ...and replayed by passing it back: -/// Any.Reproducibly(1234, () => { /* ... */ }); -/// -/// -/// -public static class Any { - - #region Fields declarations - - private const string CodeAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - private const string TextAlphabet = "abcdefghijklmnopqrstuvwxyz0123456789"; - - #endregion - - /// - /// Runs with the arbitrary-value source pinned to a fresh seed and, if the body throws, - /// reports that seed before letting the exception propagate. This is how a test that draws on - /// stays reproducible: the values still vary between runs (which surfaces accidental dependencies), yet a failure - /// names the exact seed to replay. - /// - /// - /// - /// On failure the seed is written to (by default ), - /// with a message naming the Any.Reproducibly(seed, ...) call that reproduces the run. Pass your test - /// framework's output writer (for example xUnit's ITestOutputHelper.WriteLine) to route it there - /// instead. The original exception is rethrown unchanged, so the test still fails with its real message. - /// - /// - /// Reproducing a run needs the same sequence of calls, so a body whose call order depends - /// on non-deterministic external state is not fully replayable from the seed alone. - /// - /// - /// The test body to run under a reproducible arbitrary-value source. - /// The sink the seed is written to on failure. Defaults to when null. - /// Thrown when is null. - public static void Reproducibly(Action body, Action? report = null) { - Reproducibly(ArbitrarySource.NewSeed(), body, report); - } - - /// - /// Replays with the arbitrary-value source pinned to , so a run - /// first seen through the parameterless overload can be - /// reproduced exactly. If the body throws, the seed is reported before the exception propagates. - /// - /// The seed to replay — typically the one a previous failure reported. - /// The test body to run under the seeded arbitrary-value source. - /// The sink the seed is written to on failure. Defaults to when null. - /// Thrown when is null. - public static void Reproducibly(int seed, Action body, Action? report = null) { - if (body is null) { throw new ArgumentNullException(nameof(body)); } - - using (ArbitrarySource.UseSeed(seed)) { - try { - body(); - } catch { - Report(report, seed); - - throw; - } - } - } - - /// - /// Asynchronous counterpart of : awaits - /// under a fresh seed and reports it if the body faults. - /// - /// The asynchronous test body to run under a reproducible arbitrary-value source. - /// The sink the seed is written to on failure. Defaults to when null. - /// A task that completes when completes. - /// Thrown when is null. - public static Task Reproducibly(Func body, Action? report = null) { - return Reproducibly(ArbitrarySource.NewSeed(), body, report); - } - - /// - /// Asynchronous counterpart of : awaits - /// under and reports it if the body faults. - /// - /// The seed to replay — typically the one a previous failure reported. - /// The asynchronous test body to run under the seeded arbitrary-value source. - /// The sink the seed is written to on failure. Defaults to when null. - /// A task that completes when completes. - /// Thrown when is null. - public static async Task Reproducibly(int seed, Func body, Action? report = null) { - if (body is null) { throw new ArgumentNullException(nameof(body)); } - - using (ArbitrarySource.UseSeed(seed)) { - try { - await body().ConfigureAwait(false); - } catch { - Report(report, seed); - - throw; - } - } - } - - private static void Report(Action? report, int seed) { - (report ?? Console.Error.WriteLine)( - $"[FirstClassErrors.Testing] These arbitrary values were seeded with {seed}. Reproduce this run with Any.Reproducibly({seed}, ...)."); - } - - /// - /// Returns an arbitrary drawn from the full range of the type. - /// - /// An arbitrary integer, possibly negative. - public static int Int() { - return ArbitrarySource.Int(); - } - - /// - /// Returns an arbitrary . - /// - /// true or false, with even probability. - public static bool Bool() { - return ArbitrarySource.Bool(); - } - - /// - /// Returns an arbitrary . Unlike , it is drawn from - /// the seedable source, so it is reproducible inside an Any.Reproducibly(...) run. - /// - /// An arbitrary identifier. - public static Guid Guid() { - return ArbitrarySource.Guid(); - } - - /// - /// Returns an arbitrary UTC (offset ). - /// - /// - /// To make an error's OccurredAt arbitrary rather than a real wall-clock reading, prefer - /// Clock.UseAny(), which freezes the ambient clock to an arbitrary instant for a scope. - /// - /// An arbitrary instant, in UTC. - public static DateTimeOffset Instant() { - return ArbitrarySource.Instant(); - } - - /// - /// Returns an arbitrary, non-blank , prefixed with any- so it reads as arbitrary. - /// - /// An arbitrary string such as any-4f2a9c1b. - public static string String() { - return "any-" + ArbitrarySource.Token(TextAlphabet, 8); - } - - /// - /// Returns an arbitrary, valid — never blank, and shaped like a real - /// code (for example ANY_CODE_7F3A9C) so it is recognizable as arbitrary. - /// - /// An arbitrary error code. - public static ErrorCode ErrorCode() { - return FirstClassErrors.ErrorCode.Create("ANY_CODE_" + ArbitrarySource.Token(CodeAlphabet, 6)); - } - - /// - /// Returns an arbitrary, non-blank internal diagnostic message, suitable wherever a test needs a - /// diagnosticMessage it does not assert on. - /// - /// An arbitrary diagnostic message. - public static string DiagnosticMessage() { - return "Any diagnostic message " + ArbitrarySource.Token(CodeAlphabet, 6) + "."; - } - - /// - /// Returns an arbitrary, non-blank public short message, suitable wherever a test needs a shortMessage - /// it does not assert on. - /// - /// An arbitrary short message. - public static string ShortMessage() { - return "Any short message " + ArbitrarySource.Token(CodeAlphabet, 6) + "."; - } - - /// - /// Returns an arbitrary, non-blank public detailed message, suitable wherever a test needs a - /// detailedMessage it does not assert on. - /// - /// An arbitrary detailed message. - public static string DetailedMessage() { - return "Any detailed message " + ArbitrarySource.Token(CodeAlphabet, 6) + "."; - } - - /// - /// Returns an arbitrary value of the enum , uniformly across all its - /// declared members. - /// - /// - /// This can return a sentinel such as Unknown. When a test needs a meaningful value, prefer the - /// dedicated helpers (, ), which exclude the - /// Unknown sentinel. - /// - /// The enum type to draw a value from. - /// An arbitrary member of . - public static TEnum Enum() - where TEnum : struct, System.Enum { - return ArbitrarySource.Enum(); - } - - /// - /// Returns an arbitrary meaningful — - /// or - /// , never - /// . - /// - /// An arbitrary transience classification other than Unknown. - public static Transience Transience() { - return ArbitrarySource.EnumExcluding(FirstClassErrors.Transience.Unknown); - } - - /// - /// Returns an arbitrary , uniformly across all of its members. - /// - /// An arbitrary error origin. - public static ErrorOrigin ErrorOrigin() { - return ArbitrarySource.Enum(); - } - - /// - /// Returns an arbitrary meaningful — - /// or - /// , never - /// . - /// - /// An arbitrary interaction direction other than Unknown. - public static InteractionDirection InteractionDirection() { - return ArbitrarySource.EnumExcluding(FirstClassErrors.InteractionDirection.Unknown); - } - -} diff --git a/FirstClassErrors.Testing/ArbitrarySource.cs b/FirstClassErrors.Testing/ArbitrarySource.cs deleted file mode 100644 index 9c11c3c7..00000000 --- a/FirstClassErrors.Testing/ArbitrarySource.cs +++ /dev/null @@ -1,123 +0,0 @@ -namespace FirstClassErrors.Testing; - -/// -/// The generic, error-agnostic engine behind : a seedable, context-local source of arbitrary -/// primitive values (numbers, booleans, identifiers, instants, tokens, enum members). It carries no knowledge of -/// FirstClassErrors — layers the error-specific helpers on top, and the clock and instance-id -/// seams draw their arbitrary values from here — so it stays a self-contained unit that could be extracted as a -/// standalone utility unchanged. -/// -/// -/// The source is stored in an , so it flows with the current execution context and -/// never leaks across tests running in parallel. Outside a scope it is unseeded and every -/// run differs; inside one it is deterministic. -/// -internal static class ArbitrarySource { - - #region Fields declarations - - private static readonly DateTimeOffset Origin = new(2000, 1, 1, 0, 0, 0, TimeSpan.Zero); - private static readonly AsyncLocal Seeded = new(); - - #endregion - - internal static Random Current { - get { - Random? random = Seeded.Value; - if (random is null) { - random = new Random(NewSeed()); - Seeded.Value = random; - } - - return random; - } - } - - internal static int NewSeed() { - return System.Guid.NewGuid().GetHashCode(); - } - - internal static IDisposable UseSeed(int seed) { - Random? previous = Seeded.Value; - Seeded.Value = new Random(seed); - - return new SeedScope(previous); - } - - internal static int Int() { - return Current.Next(int.MinValue, int.MaxValue); - } - - internal static bool Bool() { - return Current.Next(2) == 0; - } - - internal static Guid Guid() { - return NewGuid(Current); - } - - internal static DateTimeOffset Instant() { - return NewInstant(Current); - } - - internal static string Token(string alphabet, int length) { - Random random = Current; - char[] chars = new char[length]; - for (int i = 0; i < length; i++) { - chars[i] = alphabet[random.Next(alphabet.Length)]; - } - - return new string(chars); - } - - internal static TEnum Enum() - where TEnum : struct, System.Enum { - TEnum[] values = (TEnum[])System.Enum.GetValues(typeof(TEnum)); - - return values[Current.Next(values.Length)]; - } - - internal static TEnum EnumExcluding(params TEnum[] excluded) - where TEnum : struct, System.Enum { - List pool = new(); - foreach (TEnum value in (TEnum[])System.Enum.GetValues(typeof(TEnum))) { - if (Array.IndexOf(excluded, value) < 0) { pool.Add(value); } - } - - return pool[Current.Next(pool.Count)]; - } - - private static Guid NewGuid(Random random) { - byte[] bytes = new byte[16]; - random.NextBytes(bytes); - - return new Guid(bytes); - } - - private static DateTimeOffset NewInstant(Random random) { - return Origin.AddSeconds(random.Next()); - } - - #region Nested types - - private sealed class SeedScope : IDisposable { - - private readonly Random? _previous; - private bool _disposed; - - internal SeedScope(Random? previous) { - _previous = previous; - } - - public void Dispose() { - if (_disposed) { return; } - - _disposed = true; - Seeded.Value = _previous; - } - - } - - #endregion - -} diff --git a/FirstClassErrors.Testing/Clock.cs b/FirstClassErrors.Testing/Clock.cs index 408abb7a..38efe447 100644 --- a/FirstClassErrors.Testing/Clock.cs +++ b/FirstClassErrors.Testing/Clock.cs @@ -55,13 +55,13 @@ public static IDisposable UseFixed(DateTimeOffset instant) { /// /// /// The instant is drawn once, when this method is called, and stays fixed for every error created within the - /// scope — the same freezing behavior as . The value comes from 's - /// source; run the test inside Any.Reproducibly(...) to make the chosen instant reproducible and reported - /// on failure. + /// scope — the same freezing behavior as . The value is drawn from Dummies' ambient + /// random context; run the test inside Dummies.Any.Reproducibly(...) to make the chosen instant + /// reproducible and reported on failure. /// /// A scope that restores the real system clock when disposed. public static IDisposable UseAny() { - return UseFixed(ArbitrarySource.Instant()); + return UseFixed(Dummies.Any.DateTimeOffset().Generate()); } } diff --git a/FirstClassErrors.Testing/DetailedMessageFactory.cs b/FirstClassErrors.Testing/DetailedMessageFactory.cs new file mode 100644 index 00000000..137a38d9 --- /dev/null +++ b/FirstClassErrors.Testing/DetailedMessageFactory.cs @@ -0,0 +1,16 @@ +namespace FirstClassErrors.Testing; + +/// +/// Supplies an arbitrary, non-blank public detailed message for tests that need some detailed text but do +/// not assert on it. The value reads as arbitrary (for example Any detailed message 7F3A9C.) and is drawn +/// from Dummies' ambient random context, so a test wrapped in Dummies.Any.Reproducibly(...) replays it. +/// +public static class DetailedMessageFactory { + + /// Returns an arbitrary, non-blank detailed message. + /// An arbitrary detailed message. + public static string Any() { + return "Any detailed message " + Dummies.Any.String().WithLength(6).Generate() + "."; + } + +} diff --git a/FirstClassErrors.Testing/DiagnosticMessageFactory.cs b/FirstClassErrors.Testing/DiagnosticMessageFactory.cs new file mode 100644 index 00000000..91ec652f --- /dev/null +++ b/FirstClassErrors.Testing/DiagnosticMessageFactory.cs @@ -0,0 +1,16 @@ +namespace FirstClassErrors.Testing; + +/// +/// Supplies an arbitrary, non-blank internal diagnostic message for tests that need some diagnostic text but +/// do not assert on it. The value reads as arbitrary (for example Any diagnostic message 7F3A9C.) and is +/// drawn from Dummies' ambient random context, so a test wrapped in Dummies.Any.Reproducibly(...) replays it. +/// +public static class DiagnosticMessageFactory { + + /// Returns an arbitrary, non-blank diagnostic message. + /// An arbitrary diagnostic message. + public static string Any() { + return "Any diagnostic message " + Dummies.Any.String().WithLength(6).Generate() + "."; + } + +} diff --git a/FirstClassErrors.Testing/ErrorCodeFactory.cs b/FirstClassErrors.Testing/ErrorCodeFactory.cs new file mode 100644 index 00000000..7fe92299 --- /dev/null +++ b/FirstClassErrors.Testing/ErrorCodeFactory.cs @@ -0,0 +1,27 @@ +#region Usings declarations + +using Dummies; + +#endregion + +namespace FirstClassErrors.Testing; + +/// +/// Supplies an arbitrary, valid for the parts of a test that need some code but +/// never assert on it — an explicit ErrorCodeFactory.Any() call reads as "this code is arbitrary" where a hand-picked +/// literal reads as "this matters". The value is shaped like ANY_CODE_7F3A9C so it is recognizable as +/// arbitrary in a failure message, and it is drawn from Dummies' ambient random context: wrap the test in +/// Dummies.Any.Reproducibly(...) to make the chosen code reproducible and reported on failure. +/// +public static class ErrorCodeFactory { + + /// + /// Returns an arbitrary, non-blank , recognizable as arbitrary (for example + /// ANY_CODE_7F3A9C). + /// + /// An arbitrary error code. + public static ErrorCode Any() { + return Dummies.Any.StringMatching("ANY_CODE_[A-Z0-9]{6}").As(ErrorCode.Create).Generate(); + } + +} diff --git a/FirstClassErrors.Testing/ErrorOriginFactory.cs b/FirstClassErrors.Testing/ErrorOriginFactory.cs new file mode 100644 index 00000000..c0b8ac4a --- /dev/null +++ b/FirstClassErrors.Testing/ErrorOriginFactory.cs @@ -0,0 +1,15 @@ +namespace FirstClassErrors.Testing; + +/// +/// Supplies an arbitrary , uniformly across all of its members, for tests that need +/// some origin but do not assert on it. Drawn from Dummies' ambient random context. +/// +public static class ErrorOriginFactory { + + /// Returns an arbitrary . + /// An arbitrary error origin. + public static ErrorOrigin Any() { + return Dummies.Any.Enum().Generate(); + } + +} diff --git a/FirstClassErrors.Testing/FirstClassErrors.Testing.csproj b/FirstClassErrors.Testing/FirstClassErrors.Testing.csproj index 79454d9f..a9dba11f 100644 --- a/FirstClassErrors.Testing/FirstClassErrors.Testing.csproj +++ b/FirstClassErrors.Testing/FirstClassErrors.Testing.csproj @@ -53,8 +53,24 @@ + + + + + $(TargetsForTfmSpecificBuildOutput);IncludeDummiesInPackage + + + + + + + + diff --git a/FirstClassErrors.UnitTests/OutcomeAssertionsTests.cs b/FirstClassErrors.UnitTests/OutcomeAssertionsTests.cs index c1600f5e..0b9969ac 100644 --- a/FirstClassErrors.UnitTests/OutcomeAssertionsTests.cs +++ b/FirstClassErrors.UnitTests/OutcomeAssertionsTests.cs @@ -68,7 +68,7 @@ public void ShouldFailWithMismatchingCodeThrows() { [Fact(DisplayName = "ShouldFail on a successful outcome throws.")] public void ShouldFailOnSuccessThrows() { - Outcome outcome = Outcome.Success(Any.Int()); + Outcome outcome = Outcome.Success(Dummies.Any.Int32().Generate()); Assert.Throws(() => outcome.ShouldFail()); } @@ -85,8 +85,8 @@ public void NonGenericOutcomeAssertions() { [Fact(DisplayName = "WithContextEntry checks the presence and value of a context entry.")] public void WithContextEntryChecksPresenceAndValue() { ErrorContextKey cardNetwork = ErrorContextKey.Create("CardNetwork", "The card network."); - DomainError error = DomainError.Create(Any.ErrorCode(), Any.DiagnosticMessage(), context => context.Add(cardNetwork, "VISA")) - .WithPublicMessage(Any.ShortMessage()); + DomainError error = DomainError.Create(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any(), context => context.Add(cardNetwork, "VISA")) + .WithPublicMessage(ShortMessageFactory.Any()); Outcome outcome = Outcome.Failure(error); // Passes for the present key and matching value. diff --git a/FirstClassErrors.UnitTests/OutcomeAsyncInstanceCoverageTests.cs b/FirstClassErrors.UnitTests/OutcomeAsyncInstanceCoverageTests.cs index dfcc8e8d..ef2f5a95 100644 --- a/FirstClassErrors.UnitTests/OutcomeAsyncInstanceCoverageTests.cs +++ b/FirstClassErrors.UnitTests/OutcomeAsyncInstanceCoverageTests.cs @@ -43,7 +43,7 @@ public void ThenToATypedOutcomeGuardsAgainstANullNextFunction() { public async Task TheAsyncThenToATypedOutcomeChainsOnSuccessAndPropagatesOnFailure() { // Setup CancellationToken token = TestContext.Current.CancellationToken; - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); // Exercise Outcome onSuccess = await Outcome.Success.Then((_) => Task.FromResult(Outcome.Success(9)), token); @@ -84,7 +84,7 @@ public void TheAsyncThenGuardsAgainstANullNextFunction() { public async Task TheAsyncRecoverPassesASuccessThroughAndRecoversAFailure() { // Setup CancellationToken token = TestContext.Current.CancellationToken; - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); // Exercise Outcome passed = await Outcome.Success.Recover((_, _) => Task.FromResult(Outcome.Failure(error)), token); @@ -112,7 +112,7 @@ public void TheAsyncRecoverGuardsAgainstANullFallbackFunction() { public async Task TheAsyncFinallyWithAValueResolvesTheBranchMatchingTheOutcome() { // Setup CancellationToken token = TestContext.Current.CancellationToken; - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); // Exercise string onSuccess = await Outcome.Success.Finally((_) => Task.FromResult("ok"), (_, _) => Task.FromResult("ko"), token); @@ -144,7 +144,7 @@ public void TheAsyncFinallyWithAValueGuardsAgainstNullHandlers() { public async Task TheAsyncFinallyWithActionsRunsTheBranchMatchingTheOutcome() { // Setup CancellationToken token = TestContext.Current.CancellationToken; - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); bool successOnOk = false; Error? capturedOnFail = null; @@ -233,7 +233,7 @@ public void TheAsyncThenThrowsWhenNextReturnsANullTask() { public void TheAsyncRecoverThrowsWhenFallbackReturnsANullTask() { // Setup CancellationToken token = TestContext.Current.CancellationToken; - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); Outcome outcome = Outcome.Failure(error); // Exercise & verify @@ -260,7 +260,7 @@ public void TheAsyncFinallyWithAValueThrowsWhenOnSuccessReturnsANullTask() { public void TheAsyncFinallyWithActionsThrowsWhenOnFailureReturnsANullTask() { // Setup CancellationToken token = TestContext.Current.CancellationToken; - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); Outcome outcome = Outcome.Failure(error); // Exercise & verify @@ -293,7 +293,7 @@ public void ThenToATypedOutcomeGuardsAgainstANullNextFunction() { public async Task TheAsyncThenToATypedOutcomeChainsOnSuccessAndPropagatesOnFailure() { // Setup CancellationToken token = TestContext.Current.CancellationToken; - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); // Exercise Outcome onSuccess = await Outcome.Success(4).Then((value, _) => Task.FromResult(Outcome.Success(value + 1)), token); @@ -321,7 +321,7 @@ public void TheAsyncThenToATypedOutcomeGuardsAgainstANullNextFunction() { public async Task TheAsyncThenDiscardingTheValueChainsOnSuccessAndPropagatesOnFailure() { // Setup CancellationToken token = TestContext.Current.CancellationToken; - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); // Exercise Outcome onSuccess = await Outcome.Success(4).Then((_, _) => Task.FromResult(Outcome.Success), token); @@ -349,7 +349,7 @@ public void TheAsyncThenDiscardingTheValueGuardsAgainstANullNextFunction() { public async Task TheAsyncToPropagatesTheErrorOnFailure() { // Setup CancellationToken token = TestContext.Current.CancellationToken; - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); Outcome outcome = Outcome.Failure(error); // Exercise @@ -379,7 +379,7 @@ public void TheAsyncToGuardsAgainstANullConverter() { [Fact(DisplayName = "Recover with an outcome fallback passes a success through and recovers a failure.")] public void RecoverWithAnOutcomeFallbackPassesASuccessThroughAndRecoversAFailure() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); Outcome success = Outcome.Success(1); Outcome failure = Outcome.Failure(error); @@ -395,7 +395,7 @@ public void RecoverWithAnOutcomeFallbackPassesASuccessThroughAndRecoversAFailure [Fact(DisplayName = "Recover with a value fallback passes a success through and recovers a failure.")] public void RecoverWithAValueFallbackPassesASuccessThroughAndRecoversAFailure() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); Outcome success = Outcome.Success(1); Outcome failure = Outcome.Failure(error); @@ -422,7 +422,7 @@ public void RecoverWithAValueFallbackGuardsAgainstANullFallbackFunction() { public async Task TheAsyncRecoverWithAnOutcomeFallbackPassesASuccessThroughAndRecoversAFailure() { // Setup CancellationToken token = TestContext.Current.CancellationToken; - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); Outcome success = Outcome.Success(1); // Exercise @@ -476,7 +476,7 @@ public void TheAsyncRecoverWithAValueFallbackGuardsAgainstANullFallbackFunction( [Fact(DisplayName = "Finally with actions runs the branch matching the outcome.")] public void FinallyWithActionsRunsTheBranchMatchingTheOutcome() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); int capturedValue = 0; Error? capturedOnFail = null; @@ -504,7 +504,7 @@ public void FinallyWithActionsGuardsAgainstNullHandlers() { [Fact(DisplayName = "Finally with a value resolves the failure branch.")] public void FinallyWithAValueResolvesTheFailureBranch() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), "boom"); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), "boom"); Outcome outcome = Outcome.Failure(error); // Exercise @@ -528,7 +528,7 @@ public void FinallyWithAValueGuardsAgainstANullOnFailureFunction() { public async Task TheAsyncFinallyWithAValueResolvesTheBranchMatchingTheOutcome() { // Setup CancellationToken token = TestContext.Current.CancellationToken; - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); // Exercise string onSuccess = await Outcome.Success(5).Finally((value, _) => Task.FromResult($"ok:{value}"), (_, _) => Task.FromResult("ko"), token); @@ -560,7 +560,7 @@ public void TheAsyncFinallyWithAValueGuardsAgainstNullHandlers() { public async Task TheAsyncFinallyWithActionsRunsTheBranchMatchingTheOutcome() { // Setup CancellationToken token = TestContext.Current.CancellationToken; - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); int capturedValue = 0; Error? capturedOnFail = null; @@ -625,7 +625,7 @@ public void TheAsyncThenDiscardingTheValueThrowsWhenNextReturnsANullTask() { public void TheAsyncRecoverWithAnOutcomeFallbackThrowsWhenFallbackReturnsANullTask() { // Setup CancellationToken token = TestContext.Current.CancellationToken; - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); Outcome outcome = Outcome.Failure(error); // Exercise & verify @@ -652,7 +652,7 @@ public void TheAsyncFinallyWithAValueThrowsWhenOnSuccessReturnsANullTask() { public void TheAsyncFinallyWithActionsThrowsWhenOnFailureReturnsANullTask() { // Setup CancellationToken token = TestContext.Current.CancellationToken; - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); Outcome outcome = Outcome.Failure(error); // Exercise & verify diff --git a/FirstClassErrors.UnitTests/OutcomeGenericAdditionalTests.cs b/FirstClassErrors.UnitTests/OutcomeGenericAdditionalTests.cs index c59edb2b..c6a859f7 100644 --- a/FirstClassErrors.UnitTests/OutcomeGenericAdditionalTests.cs +++ b/FirstClassErrors.UnitTests/OutcomeGenericAdditionalTests.cs @@ -16,7 +16,7 @@ public sealed class OutcomeGenericAdditionalTests { [Fact(DisplayName = "Then discarding the value chains to a non-generic Outcome on success.")] public void ThenDiscardingTheValueChainsToANonGenericOutcomeOnSuccess() { // Setup - Outcome outcome = Outcome.Success(Any.Int()); + Outcome outcome = Outcome.Success(Dummies.Any.Int32().Generate()); // Exercise Outcome result = outcome.Then(_ => Outcome.Success); @@ -82,7 +82,7 @@ public async Task AwaitingTheAsyncInstanceRecoverValueOverloadRecoversAFailure() [Fact(DisplayName = "Then throws an ArgumentNullException when the continuation is null.")] public void ThenThrowsAnArgumentNullExceptionWhenTheContinuationIsNull() { // Setup - Outcome outcome = Outcome.Success(Any.Int()); + Outcome outcome = Outcome.Success(Dummies.Any.Int32().Generate()); // Exercise & verify Check.ThatCode(() => outcome.Then((Func)null!)) @@ -92,7 +92,7 @@ public void ThenThrowsAnArgumentNullExceptionWhenTheContinuationIsNull() { [Fact(DisplayName = "Then (value mapping) throws an ArgumentNullException when the converter is null.")] public void ToThrowsAnArgumentNullExceptionWhenTheConverterIsNull() { // Setup - Outcome outcome = Outcome.Success(Any.Int()); + Outcome outcome = Outcome.Success(Dummies.Any.Int32().Generate()); // Exercise & verify Check.ThatCode(() => outcome.Then((Func)null!)) @@ -102,7 +102,7 @@ public void ToThrowsAnArgumentNullExceptionWhenTheConverterIsNull() { [Fact(DisplayName = "Recover throws an ArgumentNullException when the fallback is null.")] public void RecoverThrowsAnArgumentNullExceptionWhenTheFallbackIsNull() { // Setup - Outcome outcome = Outcome.Success(Any.Int()); + Outcome outcome = Outcome.Success(Dummies.Any.Int32().Generate()); // Exercise & verify Check.ThatCode(() => outcome.Recover((Func>)null!)) @@ -112,7 +112,7 @@ public void RecoverThrowsAnArgumentNullExceptionWhenTheFallbackIsNull() { [Fact(DisplayName = "Finally throws an ArgumentNullException when the success handler is null.")] public void FinallyThrowsAnArgumentNullExceptionWhenTheSuccessHandlerIsNull() { // Setup - Outcome outcome = Outcome.Success(Any.Int()); + Outcome outcome = Outcome.Success(Dummies.Any.Int32().Generate()); // Exercise & verify Check.ThatCode(() => outcome.Finally((Func)null!, _ => "ko")) diff --git a/FirstClassErrors.UnitTests/OutcomeNonGenericTests.cs b/FirstClassErrors.UnitTests/OutcomeNonGenericTests.cs index 0fb48fea..29e57489 100644 --- a/FirstClassErrors.UnitTests/OutcomeNonGenericTests.cs +++ b/FirstClassErrors.UnitTests/OutcomeNonGenericTests.cs @@ -27,7 +27,7 @@ public void OutcomeSuccessIsASuccessfulOutcome() { [Fact(DisplayName = "Outcome.Failure is a failed outcome exposing its error.")] public void OutcomeFailureIsAFailedOutcomeExposingItsError() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); // Exercise Outcome outcome = Outcome.Failure(error); @@ -57,7 +57,7 @@ public void ThrowIfFailureDoesNothingWhenTheOutcomeIsASuccess() { [Fact(DisplayName = "ThrowIfFailure throws the associated exception when the outcome is a failure.")] public void ThrowIfFailureThrowsTheAssociatedExceptionWhenTheOutcomeIsAFailure() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), "boom"); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), "boom"); Outcome outcome = Outcome.Failure(error); // Exercise & verify @@ -81,7 +81,7 @@ public void ThenRunsTheNextStepWhenTheOutcomeIsASuccess() { [Fact(DisplayName = "Then short-circuits and propagates the error when the outcome is a failure.")] public void ThenShortCircuitsAndPropagatesTheErrorWhenTheOutcomeIsAFailure() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); Outcome outcome = Outcome.Failure(error); bool called = false; @@ -101,7 +101,7 @@ public void ThenShortCircuitsAndPropagatesTheErrorWhenTheOutcomeIsAFailure() { [Fact(DisplayName = "Then to a typed outcome propagates the error on a failure.")] public void ThenToATypedOutcomePropagatesTheErrorOnAFailure() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); Outcome outcome = Outcome.Failure(error); // Exercise @@ -118,7 +118,7 @@ public void RecoverLeavesASuccessfulOutcomeUnchanged() { Outcome outcome = Outcome.Success; // Exercise - Outcome result = outcome.Recover(_ => Outcome.Failure(ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()))); + Outcome result = outcome.Recover(_ => Outcome.Failure(ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()))); // Verify Check.That(result).IsSameReferenceAs(outcome); @@ -128,7 +128,7 @@ public void RecoverLeavesASuccessfulOutcomeUnchanged() { [Fact(DisplayName = "Recover replaces a failure with the fallback result.")] public void RecoverReplacesAFailureWithTheFallbackResult() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); Outcome outcome = Outcome.Failure(error); Outcome fallback = Outcome.Success; @@ -155,7 +155,7 @@ public void FinallyResolvesTheSuccessBranchWhenTheOutcomeIsASuccess() { [Fact(DisplayName = "Finally resolves the failure branch when the outcome is a failure.")] public void FinallyResolvesTheFailureBranchWhenTheOutcomeIsAFailure() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), "boom"); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), "boom"); Outcome outcome = Outcome.Failure(error); // Exercise @@ -183,7 +183,7 @@ public void FinallyWithActionsExecutesTheSuccessBranchWhenTheOutcomeIsASuccess() [Fact(DisplayName = "Finally with actions executes the failure branch when the outcome is a failure.")] public void FinallyWithActionsExecutesTheFailureBranchWhenTheOutcomeIsAFailure() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); Outcome outcome = Outcome.Failure(error); bool successExecuted = false; Error? capturedError = null; @@ -241,7 +241,7 @@ public async Task AwaitingTheAsynchronousThenChainsTheNextStepWhenTheOutcomeIsAS [Fact(DisplayName = "Awaiting the asynchronous Then short-circuits and propagates the error on a failure.")] public async Task AwaitingTheAsynchronousThenShortCircuitsAndPropagatesTheErrorOnAFailure() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); Outcome outcome = Outcome.Failure(error); bool called = false; diff --git a/FirstClassErrors.UnitTests/OutcomeTaskExtensionsCoverageTests.cs b/FirstClassErrors.UnitTests/OutcomeTaskExtensionsCoverageTests.cs index 9cd19316..370ee03a 100644 --- a/FirstClassErrors.UnitTests/OutcomeTaskExtensionsCoverageTests.cs +++ b/FirstClassErrors.UnitTests/OutcomeTaskExtensionsCoverageTests.cs @@ -37,7 +37,7 @@ public async Task ThenOverASuccessfulTaskOutcomeToATypedOutcomeRunsTheContinuati [Fact(DisplayName = "Then over a failed Task to a typed outcome propagates the error.")] public async Task ThenOverAFailedTaskOutcomeToATypedOutcomePropagatesTheError() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); Task task = Task.FromResult(Outcome.Failure(error)); // Exercise @@ -58,7 +58,7 @@ public void ThenOverANullTaskOutcomeToATypedOutcomeThrows() { [Fact(DisplayName = "Then over a failed Task propagates the error unchanged.")] public async Task ThenOverAFailedTaskOutcomePropagatesTheErrorUnchanged() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); Task task = Task.FromResult(Outcome.Failure(error)); bool called = false; @@ -91,7 +91,7 @@ public async Task TheAsyncThenOverATaskOutcomeToATypedOutcomeRunsTheContinuation public async Task TheAsyncThenOverAFailedTaskOutcomeToATypedOutcomePropagatesTheError() { // Setup CancellationToken token = TestContext.Current.CancellationToken; - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); Task task = Task.FromResult(Outcome.Failure(error)); // Exercise @@ -127,7 +127,7 @@ public async Task TheAsyncThenOverASuccessfulTaskOutcomeRunsTheNonGenericContinu public async Task TheAsyncThenOverAFailedTaskOutcomePropagatesTheErrorUnchanged() { // Setup CancellationToken token = TestContext.Current.CancellationToken; - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); Task task = Task.FromResult(Outcome.Failure(error)); // Exercise @@ -153,7 +153,7 @@ public void TheAsyncThenOverANullNonGenericTaskOutcomeThrows() { [Fact(DisplayName = "Recover over a failed Task replaces the failure with the fallback.")] public async Task RecoverOverAFailedTaskOutcomeReplacesTheFailureWithTheFallback() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); Task task = Task.FromResult(Outcome.Failure(error)); // Exercise @@ -169,7 +169,7 @@ public async Task RecoverOverASuccessfulTaskOutcomeLeavesTheOutcomeUnchanged() { Task task = Task.FromResult(Outcome.Success); // Exercise - Outcome result = await task.Recover(_ => Outcome.Failure(ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()))); + Outcome result = await task.Recover(_ => Outcome.Failure(ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()))); // Verify Check.That(result.IsSuccess).IsTrue(); @@ -186,7 +186,7 @@ public void RecoverOverANullTaskOutcomeThrows() { public async Task TheAsyncRecoverOverAFailedTaskOutcomeReplacesTheFailureWithTheFallback() { // Setup CancellationToken token = TestContext.Current.CancellationToken; - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); Task task = Task.FromResult(Outcome.Failure(error)); // Exercise @@ -224,7 +224,7 @@ public async Task FinallyOverATaskOutcomeResolvesTheSuccessBranchToAValue() { [Fact(DisplayName = "Finally over a Task resolves the failure branch to a value.")] public async Task FinallyOverATaskOutcomeResolvesTheFailureBranchToAValue() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), "boom"); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), "boom"); Task task = Task.FromResult(Outcome.Failure(error)); // Exercise @@ -244,7 +244,7 @@ public void FinallyWithAValueOverANullTaskOutcomeThrows() { [Fact(DisplayName = "Finally with actions over a Task runs the branch matching the outcome.")] public async Task FinallyWithActionsOverATaskOutcomeRunsTheBranchMatchingTheOutcome() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); bool successOnOk = false; Error? capturedOnFail = null; @@ -268,7 +268,7 @@ public void FinallyWithActionsOverANullTaskOutcomeThrows() { public async Task TheAsyncFinallyOverATaskOutcomeResolvesBothBranchesToAValue() { // Setup CancellationToken token = TestContext.Current.CancellationToken; - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); // Exercise string onSuccess = await Task.FromResult(Outcome.Success) @@ -295,7 +295,7 @@ public void TheAsyncFinallyWithAValueOverANullTaskOutcomeThrows() { public async Task TheAsyncFinallyWithActionsOverATaskOutcomeRunsTheBranchMatchingTheOutcome() { // Setup CancellationToken token = TestContext.Current.CancellationToken; - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); bool successOnOk = false; Error? capturedOnFail = null; @@ -339,7 +339,7 @@ public async Task ThenDiscardingTheValueOverATaskOutcomeChainsToANonGenericOutco [Fact(DisplayName = "Then discarding the value over a failed Task> propagates the error.")] public async Task ThenDiscardingTheValueOverAFailedTaskOutcomePropagatesTheError() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); Task> task = Task.FromResult(Outcome.Failure(error)); // Exercise @@ -360,7 +360,7 @@ public void ThenDiscardingTheValueOverANullTaskOutcomeThrows() { public async Task TheAsyncThenOverAFailedTaskOutcomeToATypedOutcomePropagatesTheErrorGeneric() { // Setup CancellationToken token = TestContext.Current.CancellationToken; - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); Task> task = Task.FromResult(Outcome.Failure(error)); // Exercise @@ -374,7 +374,7 @@ public async Task TheAsyncThenOverAFailedTaskOutcomeToATypedOutcomePropagatesThe public async Task TheAsyncThenDiscardingTheValueOverATaskOutcomeChainsOnSuccessAndPropagatesOnFailure() { // Setup CancellationToken token = TestContext.Current.CancellationToken; - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); // Exercise Outcome onSuccess = await Task.FromResult(Outcome.Success(3)) @@ -404,7 +404,7 @@ public void TheAsyncThenDiscardingTheValueOverANullTaskOutcomeThrows() { [Fact(DisplayName = "Then (value mapping) over a failed Task> propagates the error.")] public async Task ToOverAFailedTaskOutcomePropagatesTheError() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); Task> task = Task.FromResult(Outcome.Failure(error)); // Exercise @@ -418,7 +418,7 @@ public async Task ToOverAFailedTaskOutcomePropagatesTheError() { public async Task TheAsyncToOverATaskOutcomeMapsOnSuccessAndPropagatesOnFailure() { // Setup CancellationToken token = TestContext.Current.CancellationToken; - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); // Exercise Outcome onSuccess = await Task.FromResult(Outcome.Success(6)) @@ -448,7 +448,7 @@ public void TheAsyncToOverANullTaskOutcomeThrows() { [Fact(DisplayName = "Recover with an outcome fallback over a failed Task> replaces the failure.")] public async Task RecoverWithAnOutcomeFallbackOverAFailedTaskOutcomeReplacesTheFailure() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); Task> task = Task.FromResult(Outcome.Failure(error)); // Exercise @@ -500,7 +500,7 @@ public void RecoverWithAValueFallbackOverANullTaskOutcomeThrows() { public async Task TheAsyncRecoverWithAnOutcomeFallbackOverATaskOutcomeRecoversAndPassesThrough() { // Setup CancellationToken token = TestContext.Current.CancellationToken; - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); // Exercise Outcome recovered = await Task.FromResult(Outcome.Failure(error)) @@ -527,7 +527,7 @@ public void TheAsyncRecoverWithAnOutcomeFallbackOverANullTaskOutcomeThrows() { public async Task TheAsyncRecoverWithAValueFallbackOverATaskOutcomeRecoversAndPassesThrough() { // Setup CancellationToken token = TestContext.Current.CancellationToken; - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); // Exercise Outcome recovered = await Task.FromResult(Outcome.Failure(error)) @@ -557,7 +557,7 @@ public void TheAsyncRecoverWithAValueFallbackOverANullTaskOutcomeThrows() { [Fact(DisplayName = "Finally over a failed Task> resolves the failure branch to a value.")] public async Task FinallyOverAFailedTaskOutcomeResolvesTheFailureBranchToAValue() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), "boom"); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), "boom"); Task> task = Task.FromResult(Outcome.Failure(error)); // Exercise @@ -577,7 +577,7 @@ public void FinallyWithAValueOverANullGenericTaskOutcomeThrows() { [Fact(DisplayName = "Finally with actions over a Task> runs the branch matching the outcome.")] public async Task FinallyWithActionsOverATaskGenericOutcomeRunsTheBranchMatchingTheOutcome() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); int capturedValue = 0; Error? capturedOnFail = null; @@ -601,7 +601,7 @@ public void FinallyWithActionsOverANullGenericTaskOutcomeThrows() { public async Task TheAsyncFinallyOverATaskGenericOutcomeResolvesBothBranchesToAValue() { // Setup CancellationToken token = TestContext.Current.CancellationToken; - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); // Exercise string onSuccess = await Task.FromResult(Outcome.Success(5)) @@ -628,7 +628,7 @@ public void TheAsyncFinallyWithAValueOverANullGenericTaskOutcomeThrows() { public async Task TheAsyncFinallyWithActionsOverATaskGenericOutcomeRunsTheBranchMatchingTheOutcome() { // Setup CancellationToken token = TestContext.Current.CancellationToken; - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); int capturedValue = 0; Error? capturedOnFail = null; diff --git a/FirstClassErrors.UnitTests/OutcomeTaskExtensionsEagerValidationTests.cs b/FirstClassErrors.UnitTests/OutcomeTaskExtensionsEagerValidationTests.cs index 6a85c18c..01679e8f 100644 --- a/FirstClassErrors.UnitTests/OutcomeTaskExtensionsEagerValidationTests.cs +++ b/FirstClassErrors.UnitTests/OutcomeTaskExtensionsEagerValidationTests.cs @@ -70,7 +70,7 @@ public void ToOverANullGenericTaskOutcomeThrowsSynchronously() { [Fact(DisplayName = "Recover over a null Task> throws synchronously, before the task is awaited.")] public void RecoverOverANullGenericTaskOutcomeThrowsSynchronously() { // Exercise & verify - Check.ThatCode(() => { _ = ((Task>)null!).Recover(_ => Outcome.Success(Any.Int())); }) + Check.ThatCode(() => { _ = ((Task>)null!).Recover(_ => Outcome.Success(Dummies.Any.Int32().Generate())); }) .Throws(); } @@ -139,7 +139,7 @@ public void FinallyWithANullFailureBranchThrowsSynchronously() { [Fact(DisplayName = "Then (value mapping) over a Task> with a null conversion throws synchronously.")] public void ToWithANullConversionThrowsSynchronously() { // Setup - Task> task = Task.FromResult(Outcome.Success(Any.Int())); + Task> task = Task.FromResult(Outcome.Success(Dummies.Any.Int32().Generate())); // Exercise & verify Check.ThatCode(() => { _ = task.Then((Func)null!); }) @@ -149,7 +149,7 @@ public void ToWithANullConversionThrowsSynchronously() { [Fact(DisplayName = "Recover over a Task> with a null fallback throws synchronously.")] public void GenericRecoverWithANullFallbackThrowsSynchronously() { // Setup - Task> task = Task.FromResult(Outcome.Success(Any.Int())); + Task> task = Task.FromResult(Outcome.Success(Dummies.Any.Int32().Generate())); // Exercise & verify Check.ThatCode(() => { _ = task.Recover((Func>)null!); }) @@ -159,7 +159,7 @@ public void GenericRecoverWithANullFallbackThrowsSynchronously() { [Fact(DisplayName = "Finally over a Task> with a null failure branch throws synchronously.")] public void GenericFinallyWithANullFailureBranchThrowsSynchronously() { // Setup - Task> task = Task.FromResult(Outcome.Success(Any.Int())); + Task> task = Task.FromResult(Outcome.Success(Dummies.Any.Int32().Generate())); // Exercise & verify Check.ThatCode(() => { _ = task.Finally(value => $"ok:{value}", (Func)null!); }) diff --git a/FirstClassErrors.UnitTests/OutcomeTaskExtensionsTests.cs b/FirstClassErrors.UnitTests/OutcomeTaskExtensionsTests.cs index 6cd8f40a..0868de95 100644 --- a/FirstClassErrors.UnitTests/OutcomeTaskExtensionsTests.cs +++ b/FirstClassErrors.UnitTests/OutcomeTaskExtensionsTests.cs @@ -29,7 +29,7 @@ public async Task AwaitingThenOverATaskOutcomeChainsTheNextStepOnSuccess() { [Fact(DisplayName = "Awaiting Then over a Task> propagates the error on failure.")] public async Task AwaitingThenOverATaskOutcomePropagatesTheErrorOnFailure() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); System.Threading.Tasks.Task> task = System.Threading.Tasks.Task.FromResult(Outcome.Failure(error)); bool called = false; @@ -61,7 +61,7 @@ public async Task AwaitingToOverATaskOutcomeMapsTheValueOnSuccess() { [Fact(DisplayName = "Awaiting Recover over a Task> recovers a failure into a success.")] public async Task AwaitingRecoverOverATaskOutcomeRecoversAFailureIntoASuccess() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); System.Threading.Tasks.Task> task = System.Threading.Tasks.Task.FromResult(Outcome.Failure(error)); // Exercise @@ -87,7 +87,7 @@ public async Task AwaitingFinallyOverATaskOutcomeResolvesTheSuccessBranch() { [Fact(DisplayName = "Awaiting Finally over a Task> resolves the failure branch.")] public async Task AwaitingFinallyOverATaskOutcomeResolvesTheFailureBranch() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), "boom"); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), "boom"); System.Threading.Tasks.Task> task = System.Threading.Tasks.Task.FromResult(Outcome.Failure(error)); // Exercise diff --git a/FirstClassErrors.UnitTests/OutcomeTests.cs b/FirstClassErrors.UnitTests/OutcomeTests.cs index ccc76ec1..cfa48dc7 100644 --- a/FirstClassErrors.UnitTests/OutcomeTests.cs +++ b/FirstClassErrors.UnitTests/OutcomeTests.cs @@ -16,7 +16,7 @@ public sealed class OutcomeTests { [Fact(DisplayName = "A successful outcome is marked as success.")] public void SuccessfulOutcomeIsMarkedAsSuccess() { // Exercise - Outcome outcome = Outcome.Success(Any.String()); + Outcome outcome = Outcome.Success(Dummies.Any.String().NonEmpty().Generate()); // Verify Check.That(outcome.IsSuccess).IsTrue(); @@ -52,7 +52,7 @@ public void SuccessfulOutcomeCannotBeCreatedFromANullValue() { [Fact(DisplayName = "A failed outcome is marked as failure.")] public void FailedOutcomeIsMarkedAsFailure() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); // Exercise Outcome outcome = Outcome.Failure(error); @@ -65,7 +65,7 @@ public void FailedOutcomeIsMarkedAsFailure() { [Fact(DisplayName = "A failed outcome exposes its error.")] public void AFailedOutcomeExposesItsError() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); // Exercise Outcome outcome = Outcome.Failure(error); @@ -84,7 +84,7 @@ public void AFailedOutcomeCannotBeCreatedFromANullError() { [Fact(DisplayName = "Accessing the value of a failed outcome throws the associated exception.")] public void AccessingTheValueOfAFailedOutcomeThrowsTheAssociatedException() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), "boom"); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), "boom"); Outcome outcome = Outcome.Failure(error); // Exercise & verify @@ -96,7 +96,7 @@ public void AccessingTheValueOfAFailedOutcomeThrowsTheAssociatedException() { [Fact(DisplayName = "Escalating a failed outcome throws the associated exception.")] public void EscalatingAFailedOutcomeThrowsTheAssociatedException() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), "boom"); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), "boom"); Outcome outcome = Outcome.Failure(error); // Exercise & verify @@ -108,7 +108,7 @@ public void EscalatingAFailedOutcomeThrowsTheAssociatedException() { [Fact(DisplayName = "A failed outcome preserves the original error instance.")] public void FailedOutcomePreservesTheOriginalErrorInstance() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); Outcome outcome = Outcome.Failure(error); // Exercise & verify @@ -119,7 +119,7 @@ public void FailedOutcomePreservesTheOriginalErrorInstance() { [Fact(DisplayName = "ThrowIfFailure throws the associated exception when the outcome is a failure.")] public void ThrowIfFailureThrowsTheAssociatedExceptionWhenTheOutcomeIsAFailure() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), "boom"); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), "boom"); Outcome outcome = Outcome.Failure(error); // Exercise & verify @@ -131,7 +131,7 @@ public void ThrowIfFailureThrowsTheAssociatedExceptionWhenTheOutcomeIsAFailure() [Fact(DisplayName = "ThrowIfFailure does nothing when the outcome is a success.")] public void ThrowIfFailureDoesNothingWhenTheOutcomeIsASuccess() { // Setup - Outcome outcome = Outcome.Success(Any.String()); + Outcome outcome = Outcome.Success(Dummies.Any.String().NonEmpty().Generate()); // Exercise & verify Check.ThatCode(() => outcome.ThrowIfFailure()).DoesNotThrow(); @@ -153,7 +153,7 @@ public void ThenChainsTheNextStepWhenTheOutcomeIsASuccess() { [Fact(DisplayName = "Then short-circuits and propagates the error when the outcome is a failure.")] public void ThenShortCircuitsAndPropagatesTheErrorWhenTheOutcomeIsAFailure() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); Outcome outcome = Outcome.Failure(error); bool called = false; @@ -185,7 +185,7 @@ public void ToMapsTheValueWhenTheOutcomeIsASuccess() { [Fact(DisplayName = "Then (value mapping) propagates the error without invoking the converter on a failure.")] public void ToPropagatesTheErrorWithoutInvokingTheConverterOnAFailure() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); Outcome outcome = Outcome.Failure(error); // Exercise @@ -199,7 +199,7 @@ public void ToPropagatesTheErrorWithoutInvokingTheConverterOnAFailure() { [Fact(DisplayName = "Recover replaces a failure with a guaranteed fallback value.")] public void RecoverReplacesAFailureWithAGuaranteedFallbackValue() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); Outcome outcome = Outcome.Failure(error); // Exercise @@ -237,7 +237,7 @@ public void FinallyResolvesTheSuccessBranchWhenTheOutcomeIsASuccess() { [Fact(DisplayName = "Finally resolves the failure branch when the outcome is a failure.")] public void FinallyResolvesTheFailureBranchWhenTheOutcomeIsAFailure() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), "boom"); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), "boom"); Outcome outcome = Outcome.Failure(error); // Exercise @@ -250,7 +250,7 @@ public void FinallyResolvesTheFailureBranchWhenTheOutcomeIsAFailure() { [Fact(DisplayName = "The non-generic Outcome.Then chains when successful and propagates on failure.")] public void NonGenericOutcomeThenChainsWhenSuccessfulAndPropagatesOnFailure() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); // Exercise Outcome chainedFromSuccess = Outcome.Success.Then(() => Outcome.Success); diff --git a/FirstClassErrors.UnitTests/PortInnerErrorsTests.cs b/FirstClassErrors.UnitTests/PortInnerErrorsTests.cs index 9724f291..d0fa9674 100644 --- a/FirstClassErrors.UnitTests/PortInnerErrorsTests.cs +++ b/FirstClassErrors.UnitTests/PortInnerErrorsTests.cs @@ -29,8 +29,8 @@ public void ComputeTransienceOfAnEmptyCollectionIsUnknown() { public void ComputeTransienceIsNonTransientWhenAnyEntryIsNonTransient() { // Setup PrimaryPortInnerErrors innerErrors = new PrimaryPortInnerErrors() - .Add(ErrorFactory.Primary(Any.ErrorCode(), Any.DiagnosticMessage(), Transience.Transient)) - .Add(ErrorFactory.Primary(Any.ErrorCode(), Any.DiagnosticMessage(), Transience.NonTransient)); + .Add(ErrorFactory.Primary(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any(), Transience.Transient)) + .Add(ErrorFactory.Primary(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any(), Transience.NonTransient)); // Exercise Transience transience = innerErrors.ComputeTransience(); @@ -43,8 +43,8 @@ public void ComputeTransienceIsNonTransientWhenAnyEntryIsNonTransient() { public void ComputeTransienceIsTransientWhenAtLeastOneEntryIsTransientAndNoneIsNonTransient() { // Setup PrimaryPortInnerErrors innerErrors = new PrimaryPortInnerErrors() - .Add(ErrorFactory.Primary(Any.ErrorCode(), Any.DiagnosticMessage(), Transience.Unknown)) - .Add(ErrorFactory.Primary(Any.ErrorCode(), Any.DiagnosticMessage(), Transience.Transient)); + .Add(ErrorFactory.Primary(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any(), Transience.Unknown)) + .Add(ErrorFactory.Primary(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any(), Transience.Transient)); // Exercise Transience transience = innerErrors.ComputeTransience(); @@ -57,8 +57,8 @@ public void ComputeTransienceIsTransientWhenAtLeastOneEntryIsTransientAndNoneIsN public void ComputeTransienceIsUnknownWhenTheCollectionContainsOnlyDomainErrors() { // Setup PrimaryPortInnerErrors innerErrors = new PrimaryPortInnerErrors() - .Add(ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage())) - .Add(ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage())); + .Add(ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any())) + .Add(ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any())); // Exercise Transience transience = innerErrors.ComputeTransience(); @@ -70,8 +70,8 @@ public void ComputeTransienceIsUnknownWhenTheCollectionContainsOnlyDomainErrors( [Fact(DisplayName = "Add ignores null domain and primary port errors.")] public void AddIgnoresNullDomainAndPrimaryPortErrors() { // Setup - DomainError domainError = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); - PrimaryPortError primaryPortError = ErrorFactory.Primary(Any.ErrorCode(), Any.DiagnosticMessage(), Any.Transience()); + DomainError domainError = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); + PrimaryPortError primaryPortError = ErrorFactory.Primary(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any(), TransienceFactory.Any()); PrimaryPortInnerErrors innerErrors = new PrimaryPortInnerErrors() .Add((DomainError)null!) @@ -80,7 +80,7 @@ public void AddIgnoresNullDomainAndPrimaryPortErrors() { .Add(primaryPortError); // Exercise - PrimaryPortError root = ErrorFactory.Primary(Any.ErrorCode(), Any.DiagnosticMessage(), innerErrors); + PrimaryPortError root = ErrorFactory.Primary(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any(), innerErrors); // Verify Check.That(root.InnerErrors).CountIs(2); @@ -90,8 +90,8 @@ public void AddIgnoresNullDomainAndPrimaryPortErrors() { public void AddReturnsTheSameInstanceToAllowFluentChaining() { // Setup PrimaryPortInnerErrors innerErrors = new(); - DomainError domainError = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); - PrimaryPortError portError = ErrorFactory.Primary(Any.ErrorCode(), Any.DiagnosticMessage(), Any.Transience()); + DomainError domainError = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); + PrimaryPortError portError = ErrorFactory.Primary(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any(), TransienceFactory.Any()); // Exercise & verify Check.That(innerErrors.Add(domainError)).IsSameReferenceAs(innerErrors); @@ -101,15 +101,15 @@ public void AddReturnsTheSameInstanceToAllowFluentChaining() { [Fact(DisplayName = "A primary port error built from inner errors exposes the aggregated transience and the inner errors.")] public void APrimaryPortErrorBuiltFromInnerErrorsExposesTheAggregatedTransienceAndTheInnerErrors() { // Setup - DomainError firstInnerError = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); - PrimaryPortError secondInnerError = ErrorFactory.Primary(Any.ErrorCode(), Any.DiagnosticMessage(), Transience.Transient); + DomainError firstInnerError = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); + PrimaryPortError secondInnerError = ErrorFactory.Primary(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any(), Transience.Transient); PrimaryPortInnerErrors innerErrors = new PrimaryPortInnerErrors() .Add(firstInnerError) .Add(secondInnerError); // Exercise - PrimaryPortError root = ErrorFactory.Primary(Any.ErrorCode(), Any.DiagnosticMessage(), innerErrors); + PrimaryPortError root = ErrorFactory.Primary(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any(), innerErrors); // Verify Check.That(root.Transience).IsEqualTo(innerErrors.ComputeTransience()); @@ -123,8 +123,8 @@ public void APrimaryPortErrorBuiltFromInnerErrorsExposesTheAggregatedTransienceA public void ToStringRendersThePluralizedCountOfAggregatedErrors() { // Setup PrimaryPortInnerErrors innerErrors = new PrimaryPortInnerErrors() - .Add(ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage())) - .Add(ErrorFactory.Primary(Any.ErrorCode(), Any.DiagnosticMessage(), Any.Transience())); + .Add(ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any())) + .Add(ErrorFactory.Primary(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any(), TransienceFactory.Any())); // Exercise & verify Check.That(innerErrors.ToString()).IsEqualTo("2 inner errors"); @@ -134,7 +134,7 @@ public void ToStringRendersThePluralizedCountOfAggregatedErrors() { public void ToStringRendersTheSingularFormForASingleAggregatedError() { // Setup PrimaryPortInnerErrors innerErrors = new PrimaryPortInnerErrors() - .Add(ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage())); + .Add(ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any())); // Exercise & verify Check.That(innerErrors.ToString()).IsEqualTo("1 inner error"); @@ -158,8 +158,8 @@ public sealed class SecondaryPortInnerErrorsTests { public void ComputeTransienceIsNonTransientWhenAnyEntryIsNonTransient() { // Setup SecondaryPortInnerErrors innerErrors = new SecondaryPortInnerErrors() - .Add(ErrorFactory.Secondary(Any.ErrorCode(), Any.DiagnosticMessage(), Transience.Transient)) - .Add(ErrorFactory.Secondary(Any.ErrorCode(), Any.DiagnosticMessage(), Transience.NonTransient)); + .Add(ErrorFactory.Secondary(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any(), Transience.Transient)) + .Add(ErrorFactory.Secondary(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any(), Transience.NonTransient)); // Exercise Transience transience = innerErrors.ComputeTransience(); @@ -172,8 +172,8 @@ public void ComputeTransienceIsNonTransientWhenAnyEntryIsNonTransient() { public void ComputeTransienceIsTransientWhenAtLeastOneEntryIsTransientAndNoneIsNonTransient() { // Setup SecondaryPortInnerErrors innerErrors = new SecondaryPortInnerErrors() - .Add(ErrorFactory.Secondary(Any.ErrorCode(), Any.DiagnosticMessage(), Transience.Unknown)) - .Add(ErrorFactory.Secondary(Any.ErrorCode(), Any.DiagnosticMessage(), Transience.Transient)); + .Add(ErrorFactory.Secondary(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any(), Transience.Unknown)) + .Add(ErrorFactory.Secondary(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any(), Transience.Transient)); // Exercise Transience transience = innerErrors.ComputeTransience(); @@ -186,8 +186,8 @@ public void ComputeTransienceIsTransientWhenAtLeastOneEntryIsTransientAndNoneIsN public void ComputeTransienceIsUnknownWhenTheCollectionContainsOnlyDomainErrors() { // Setup SecondaryPortInnerErrors innerErrors = new SecondaryPortInnerErrors() - .Add(ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage())) - .Add(ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage())); + .Add(ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any())) + .Add(ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any())); // Exercise Transience transience = innerErrors.ComputeTransience(); @@ -199,8 +199,8 @@ public void ComputeTransienceIsUnknownWhenTheCollectionContainsOnlyDomainErrors( [Fact(DisplayName = "Add ignores null domain and secondary port errors.")] public void AddIgnoresNullDomainAndSecondaryPortErrors() { // Setup - DomainError domainError = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); - SecondaryPortError secondaryPortError = ErrorFactory.Secondary(Any.ErrorCode(), Any.DiagnosticMessage(), Any.Transience()); + DomainError domainError = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); + SecondaryPortError secondaryPortError = ErrorFactory.Secondary(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any(), TransienceFactory.Any()); SecondaryPortInnerErrors innerErrors = new SecondaryPortInnerErrors() .Add((DomainError)null!) @@ -209,7 +209,7 @@ public void AddIgnoresNullDomainAndSecondaryPortErrors() { .Add(secondaryPortError); // Exercise - SecondaryPortError root = ErrorFactory.Secondary(Any.ErrorCode(), Any.DiagnosticMessage(), innerErrors); + SecondaryPortError root = ErrorFactory.Secondary(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any(), innerErrors); // Verify Check.That(root.InnerErrors).CountIs(2); @@ -219,8 +219,8 @@ public void AddIgnoresNullDomainAndSecondaryPortErrors() { public void AddReturnsTheSameInstanceToAllowFluentChaining() { // Setup SecondaryPortInnerErrors innerErrors = new(); - DomainError domainError = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); - SecondaryPortError portError = ErrorFactory.Secondary(Any.ErrorCode(), Any.DiagnosticMessage(), Any.Transience()); + DomainError domainError = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); + SecondaryPortError portError = ErrorFactory.Secondary(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any(), TransienceFactory.Any()); // Exercise & verify Check.That(innerErrors.Add(domainError)).IsSameReferenceAs(innerErrors); @@ -231,8 +231,8 @@ public void AddReturnsTheSameInstanceToAllowFluentChaining() { public void ToStringRendersThePluralizedCountOfAggregatedErrors() { // Setup SecondaryPortInnerErrors innerErrors = new SecondaryPortInnerErrors() - .Add(ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage())) - .Add(ErrorFactory.Secondary(Any.ErrorCode(), Any.DiagnosticMessage(), Any.Transience())); + .Add(ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any())) + .Add(ErrorFactory.Secondary(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any(), TransienceFactory.Any())); // Exercise & verify Check.That(innerErrors.ToString()).IsEqualTo("2 inner errors"); @@ -242,7 +242,7 @@ public void ToStringRendersThePluralizedCountOfAggregatedErrors() { public void ToStringRendersTheSingularFormForASingleAggregatedError() { // Setup SecondaryPortInnerErrors innerErrors = new SecondaryPortInnerErrors() - .Add(ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage())); + .Add(ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any())); // Exercise & verify Check.That(innerErrors.ToString()).IsEqualTo("1 inner error"); @@ -272,15 +272,15 @@ public void ComputeTransienceOfAnEmptyCollectionIsUnknown() { [Fact(DisplayName = "A secondary port error built from inner errors exposes the aggregated transience and the inner errors.")] public void ASecondaryPortErrorBuiltFromInnerErrorsExposesTheAggregatedTransienceAndTheInnerErrors() { // Setup - DomainError firstInnerError = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); - SecondaryPortError secondInnerError = ErrorFactory.Secondary(Any.ErrorCode(), Any.DiagnosticMessage(), Transience.NonTransient); + DomainError firstInnerError = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); + SecondaryPortError secondInnerError = ErrorFactory.Secondary(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any(), Transience.NonTransient); SecondaryPortInnerErrors innerErrors = new SecondaryPortInnerErrors() .Add(firstInnerError) .Add(secondInnerError); // Exercise - SecondaryPortError root = ErrorFactory.Secondary(Any.ErrorCode(), Any.DiagnosticMessage(), innerErrors); + SecondaryPortError root = ErrorFactory.Secondary(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any(), innerErrors); // Verify Check.That(root.Transience).IsEqualTo(innerErrors.ComputeTransience()); diff --git a/FirstClassErrors.UnitTests/ToExceptionTests.cs b/FirstClassErrors.UnitTests/ToExceptionTests.cs index c809f5a2..f6900c66 100644 --- a/FirstClassErrors.UnitTests/ToExceptionTests.cs +++ b/FirstClassErrors.UnitTests/ToExceptionTests.cs @@ -16,7 +16,7 @@ public sealed class ToExceptionTests { [Fact(DisplayName = "Converting a domain error produces a domain exception.")] public void ConvertingADomainErrorProducesADomainException() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); // Exercise DiagnosableException exception = error.ToException(); @@ -28,7 +28,7 @@ public void ConvertingADomainErrorProducesADomainException() { [Fact(DisplayName = "Converting an infrastructure error produces an infrastructure exception.")] public void ConvertingAnInfrastructureErrorProducesAnInfrastructureException() { // Setup - InfrastructureError error = ErrorFactory.Infrastructure(Any.ErrorCode(), Any.DiagnosticMessage(), Any.InteractionDirection(), Any.Transience()); + InfrastructureError error = ErrorFactory.Infrastructure(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any(), InteractionDirectionFactory.Any(), TransienceFactory.Any()); // Exercise DiagnosableException exception = error.ToException(); @@ -40,7 +40,7 @@ public void ConvertingAnInfrastructureErrorProducesAnInfrastructureException() { [Fact(DisplayName = "Converting a primary port error produces a primary port exception.")] public void ConvertingAPrimaryPortErrorProducesAPrimaryPortException() { // Setup - PrimaryPortError error = ErrorFactory.Primary(Any.ErrorCode(), Any.DiagnosticMessage(), Any.Transience()); + PrimaryPortError error = ErrorFactory.Primary(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any(), TransienceFactory.Any()); // Exercise DiagnosableException exception = error.ToException(); @@ -52,7 +52,7 @@ public void ConvertingAPrimaryPortErrorProducesAPrimaryPortException() { [Fact(DisplayName = "Converting a secondary port error produces a secondary port exception.")] public void ConvertingASecondaryPortErrorProducesASecondaryPortException() { // Setup - SecondaryPortError error = ErrorFactory.Secondary(Any.ErrorCode(), Any.DiagnosticMessage(), Any.Transience()); + SecondaryPortError error = ErrorFactory.Secondary(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any(), TransienceFactory.Any()); // Exercise DiagnosableException exception = error.ToException(); @@ -64,7 +64,7 @@ public void ConvertingASecondaryPortErrorProducesASecondaryPortException() { [Fact(DisplayName = "The converted exception carries the same error reference.")] public void TheConvertedExceptionCarriesTheSameErrorReference() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); // Exercise DiagnosableException exception = error.ToException(); @@ -76,7 +76,7 @@ public void TheConvertedExceptionCarriesTheSameErrorReference() { [Fact(DisplayName = "The converted exception message equals the error diagnostic message.")] public void TheConvertedExceptionMessageEqualsTheErrorDiagnosticMessage() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); // Exercise DiagnosableException exception = error.ToException(); @@ -88,7 +88,7 @@ public void TheConvertedExceptionMessageEqualsTheErrorDiagnosticMessage() { [Fact(DisplayName = "The converted exception has no inner exception even when the error has inner errors.")] public void TheConvertedExceptionHasNoInnerExceptionEvenWhenTheErrorHasInnerErrors() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage(), ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage())); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any(), ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any())); // Exercise DiagnosableException exception = error.ToException(); @@ -101,7 +101,7 @@ public void TheConvertedExceptionHasNoInnerExceptionEvenWhenTheErrorHasInnerErro [Fact(DisplayName = "Throwing and catching a converted error preserves the error reference.")] public void ThrowingAndCatchingAConvertedErrorPreservesTheErrorReference() { // Setup - DomainError error = ErrorFactory.Domain(Any.ErrorCode(), Any.DiagnosticMessage()); + DomainError error = ErrorFactory.Domain(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()); // Exercise Error? caughtError = null; diff --git a/FirstClassErrors.sln b/FirstClassErrors.sln index f28e430c..636043a4 100644 --- a/FirstClassErrors.sln +++ b/FirstClassErrors.sln @@ -57,6 +57,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FirstClassErrors.RequestBin EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FirstClassErrors.RequestBinder.Benchmarks", "FirstClassErrors.RequestBinder.Benchmarks\FirstClassErrors.RequestBinder.Benchmarks.csproj", "{1201EA34-8F50-41B8-B829-FCCB62A5F14B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FirstClassErrors.Testing.UnitTests", "FirstClassErrors.Testing.UnitTests\FirstClassErrors.Testing.UnitTests.csproj", "{74E68697-7C00-401C-84A9-7BF8DAFD9D34}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -67,42 +69,6 @@ Global Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {D12EE45B-78F8-4CD4-8E67-E4A9DDA01AE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D12EE45B-78F8-4CD4-8E67-E4A9DDA01AE6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D12EE45B-78F8-4CD4-8E67-E4A9DDA01AE6}.Debug|x64.ActiveCfg = Debug|Any CPU - {D12EE45B-78F8-4CD4-8E67-E4A9DDA01AE6}.Debug|x64.Build.0 = Debug|Any CPU - {D12EE45B-78F8-4CD4-8E67-E4A9DDA01AE6}.Debug|x86.ActiveCfg = Debug|Any CPU - {D12EE45B-78F8-4CD4-8E67-E4A9DDA01AE6}.Debug|x86.Build.0 = Debug|Any CPU - {D12EE45B-78F8-4CD4-8E67-E4A9DDA01AE6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D12EE45B-78F8-4CD4-8E67-E4A9DDA01AE6}.Release|Any CPU.Build.0 = Release|Any CPU - {D12EE45B-78F8-4CD4-8E67-E4A9DDA01AE6}.Release|x64.ActiveCfg = Release|Any CPU - {D12EE45B-78F8-4CD4-8E67-E4A9DDA01AE6}.Release|x64.Build.0 = Release|Any CPU - {D12EE45B-78F8-4CD4-8E67-E4A9DDA01AE6}.Release|x86.ActiveCfg = Release|Any CPU - {D12EE45B-78F8-4CD4-8E67-E4A9DDA01AE6}.Release|x86.Build.0 = Release|Any CPU - {04707ACA-FB2E-43BF-A12C-28E01BF5F60D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {04707ACA-FB2E-43BF-A12C-28E01BF5F60D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {04707ACA-FB2E-43BF-A12C-28E01BF5F60D}.Debug|x64.ActiveCfg = Debug|Any CPU - {04707ACA-FB2E-43BF-A12C-28E01BF5F60D}.Debug|x64.Build.0 = Debug|Any CPU - {04707ACA-FB2E-43BF-A12C-28E01BF5F60D}.Debug|x86.ActiveCfg = Debug|Any CPU - {04707ACA-FB2E-43BF-A12C-28E01BF5F60D}.Debug|x86.Build.0 = Debug|Any CPU - {04707ACA-FB2E-43BF-A12C-28E01BF5F60D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {04707ACA-FB2E-43BF-A12C-28E01BF5F60D}.Release|Any CPU.Build.0 = Release|Any CPU - {04707ACA-FB2E-43BF-A12C-28E01BF5F60D}.Release|x64.ActiveCfg = Release|Any CPU - {04707ACA-FB2E-43BF-A12C-28E01BF5F60D}.Release|x64.Build.0 = Release|Any CPU - {04707ACA-FB2E-43BF-A12C-28E01BF5F60D}.Release|x86.ActiveCfg = Release|Any CPU - {04707ACA-FB2E-43BF-A12C-28E01BF5F60D}.Release|x86.Build.0 = Release|Any CPU - {1201EA34-8F50-41B8-B829-FCCB62A5F14B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1201EA34-8F50-41B8-B829-FCCB62A5F14B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1201EA34-8F50-41B8-B829-FCCB62A5F14B}.Debug|x64.ActiveCfg = Debug|Any CPU - {1201EA34-8F50-41B8-B829-FCCB62A5F14B}.Debug|x64.Build.0 = Debug|Any CPU - {1201EA34-8F50-41B8-B829-FCCB62A5F14B}.Debug|x86.ActiveCfg = Debug|Any CPU - {1201EA34-8F50-41B8-B829-FCCB62A5F14B}.Debug|x86.Build.0 = Debug|Any CPU - {1201EA34-8F50-41B8-B829-FCCB62A5F14B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1201EA34-8F50-41B8-B829-FCCB62A5F14B}.Release|Any CPU.Build.0 = Release|Any CPU - {1201EA34-8F50-41B8-B829-FCCB62A5F14B}.Release|x64.ActiveCfg = Release|Any CPU - {1201EA34-8F50-41B8-B829-FCCB62A5F14B}.Release|x64.Build.0 = Release|Any CPU - {1201EA34-8F50-41B8-B829-FCCB62A5F14B}.Release|x86.ActiveCfg = Release|Any CPU - {1201EA34-8F50-41B8-B829-FCCB62A5F14B}.Release|x86.Build.0 = Release|Any CPU {8DA5E330-53BC-4EA3-A9B8-916DABA3878C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8DA5E330-53BC-4EA3-A9B8-916DABA3878C}.Debug|Any CPU.Build.0 = Debug|Any CPU {8DA5E330-53BC-4EA3-A9B8-916DABA3878C}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -317,6 +283,54 @@ Global {1FC2C501-8842-4ABE-845E-000ED6575DD6}.Release|x64.Build.0 = Release|Any CPU {1FC2C501-8842-4ABE-845E-000ED6575DD6}.Release|x86.ActiveCfg = Release|Any CPU {1FC2C501-8842-4ABE-845E-000ED6575DD6}.Release|x86.Build.0 = Release|Any CPU + {D12EE45B-78F8-4CD4-8E67-E4A9DDA01AE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D12EE45B-78F8-4CD4-8E67-E4A9DDA01AE6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D12EE45B-78F8-4CD4-8E67-E4A9DDA01AE6}.Debug|x64.ActiveCfg = Debug|Any CPU + {D12EE45B-78F8-4CD4-8E67-E4A9DDA01AE6}.Debug|x64.Build.0 = Debug|Any CPU + {D12EE45B-78F8-4CD4-8E67-E4A9DDA01AE6}.Debug|x86.ActiveCfg = Debug|Any CPU + {D12EE45B-78F8-4CD4-8E67-E4A9DDA01AE6}.Debug|x86.Build.0 = Debug|Any CPU + {D12EE45B-78F8-4CD4-8E67-E4A9DDA01AE6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D12EE45B-78F8-4CD4-8E67-E4A9DDA01AE6}.Release|Any CPU.Build.0 = Release|Any CPU + {D12EE45B-78F8-4CD4-8E67-E4A9DDA01AE6}.Release|x64.ActiveCfg = Release|Any CPU + {D12EE45B-78F8-4CD4-8E67-E4A9DDA01AE6}.Release|x64.Build.0 = Release|Any CPU + {D12EE45B-78F8-4CD4-8E67-E4A9DDA01AE6}.Release|x86.ActiveCfg = Release|Any CPU + {D12EE45B-78F8-4CD4-8E67-E4A9DDA01AE6}.Release|x86.Build.0 = Release|Any CPU + {04707ACA-FB2E-43BF-A12C-28E01BF5F60D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {04707ACA-FB2E-43BF-A12C-28E01BF5F60D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {04707ACA-FB2E-43BF-A12C-28E01BF5F60D}.Debug|x64.ActiveCfg = Debug|Any CPU + {04707ACA-FB2E-43BF-A12C-28E01BF5F60D}.Debug|x64.Build.0 = Debug|Any CPU + {04707ACA-FB2E-43BF-A12C-28E01BF5F60D}.Debug|x86.ActiveCfg = Debug|Any CPU + {04707ACA-FB2E-43BF-A12C-28E01BF5F60D}.Debug|x86.Build.0 = Debug|Any CPU + {04707ACA-FB2E-43BF-A12C-28E01BF5F60D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {04707ACA-FB2E-43BF-A12C-28E01BF5F60D}.Release|Any CPU.Build.0 = Release|Any CPU + {04707ACA-FB2E-43BF-A12C-28E01BF5F60D}.Release|x64.ActiveCfg = Release|Any CPU + {04707ACA-FB2E-43BF-A12C-28E01BF5F60D}.Release|x64.Build.0 = Release|Any CPU + {04707ACA-FB2E-43BF-A12C-28E01BF5F60D}.Release|x86.ActiveCfg = Release|Any CPU + {04707ACA-FB2E-43BF-A12C-28E01BF5F60D}.Release|x86.Build.0 = Release|Any CPU + {1201EA34-8F50-41B8-B829-FCCB62A5F14B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1201EA34-8F50-41B8-B829-FCCB62A5F14B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1201EA34-8F50-41B8-B829-FCCB62A5F14B}.Debug|x64.ActiveCfg = Debug|Any CPU + {1201EA34-8F50-41B8-B829-FCCB62A5F14B}.Debug|x64.Build.0 = Debug|Any CPU + {1201EA34-8F50-41B8-B829-FCCB62A5F14B}.Debug|x86.ActiveCfg = Debug|Any CPU + {1201EA34-8F50-41B8-B829-FCCB62A5F14B}.Debug|x86.Build.0 = Debug|Any CPU + {1201EA34-8F50-41B8-B829-FCCB62A5F14B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1201EA34-8F50-41B8-B829-FCCB62A5F14B}.Release|Any CPU.Build.0 = Release|Any CPU + {1201EA34-8F50-41B8-B829-FCCB62A5F14B}.Release|x64.ActiveCfg = Release|Any CPU + {1201EA34-8F50-41B8-B829-FCCB62A5F14B}.Release|x64.Build.0 = Release|Any CPU + {1201EA34-8F50-41B8-B829-FCCB62A5F14B}.Release|x86.ActiveCfg = Release|Any CPU + {1201EA34-8F50-41B8-B829-FCCB62A5F14B}.Release|x86.Build.0 = Release|Any CPU + {74E68697-7C00-401C-84A9-7BF8DAFD9D34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {74E68697-7C00-401C-84A9-7BF8DAFD9D34}.Debug|Any CPU.Build.0 = Debug|Any CPU + {74E68697-7C00-401C-84A9-7BF8DAFD9D34}.Debug|x64.ActiveCfg = Debug|Any CPU + {74E68697-7C00-401C-84A9-7BF8DAFD9D34}.Debug|x64.Build.0 = Debug|Any CPU + {74E68697-7C00-401C-84A9-7BF8DAFD9D34}.Debug|x86.ActiveCfg = Debug|Any CPU + {74E68697-7C00-401C-84A9-7BF8DAFD9D34}.Debug|x86.Build.0 = Debug|Any CPU + {74E68697-7C00-401C-84A9-7BF8DAFD9D34}.Release|Any CPU.ActiveCfg = Release|Any CPU + {74E68697-7C00-401C-84A9-7BF8DAFD9D34}.Release|Any CPU.Build.0 = Release|Any CPU + {74E68697-7C00-401C-84A9-7BF8DAFD9D34}.Release|x64.ActiveCfg = Release|Any CPU + {74E68697-7C00-401C-84A9-7BF8DAFD9D34}.Release|x64.Build.0 = Release|Any CPU + {74E68697-7C00-401C-84A9-7BF8DAFD9D34}.Release|x86.ActiveCfg = Release|Any CPU + {74E68697-7C00-401C-84A9-7BF8DAFD9D34}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/doc/handwritten/for-maintainers/adr/0006-supply-arbitrary-test-values-from-a-seedable-source.fr.md b/doc/handwritten/for-maintainers/adr/0006-supply-arbitrary-test-values-from-a-seedable-source.fr.md index 234c44f7..895981e5 100644 --- a/doc/handwritten/for-maintainers/adr/0006-supply-arbitrary-test-values-from-a-seedable-source.fr.md +++ b/doc/handwritten/for-maintainers/adr/0006-supply-arbitrary-test-values-from-a-seedable-source.fr.md @@ -2,8 +2,8 @@ 🌍 🇬🇧 [English](0006-supply-arbitrary-test-values-from-a-seedable-source.md) · 🇫🇷 Français (ce fichier) -**Statut :** Accepté -**Date :** 2026-07-16 +**Statut :** Remplacé par [ADR-0026](0026-rebase-testing-arbitrary-values-on-dummies.fr.md) +**Date :** 2026-07-19 **Décideurs :** Reefact ## Contexte diff --git a/doc/handwritten/for-maintainers/adr/0006-supply-arbitrary-test-values-from-a-seedable-source.md b/doc/handwritten/for-maintainers/adr/0006-supply-arbitrary-test-values-from-a-seedable-source.md index 71e89667..b2f308ae 100644 --- a/doc/handwritten/for-maintainers/adr/0006-supply-arbitrary-test-values-from-a-seedable-source.md +++ b/doc/handwritten/for-maintainers/adr/0006-supply-arbitrary-test-values-from-a-seedable-source.md @@ -2,8 +2,8 @@ 🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0006-supply-arbitrary-test-values-from-a-seedable-source.fr.md) -**Status:** Accepted -**Date:** 2026-07-16 +**Status:** Superseded by [ADR-0026](0026-rebase-testing-arbitrary-values-on-dummies.md) +**Date:** 2026-07-19 **Decision Makers:** Reefact ## Context diff --git a/doc/handwritten/for-maintainers/adr/0026-rebase-testing-arbitrary-values-on-dummies.fr.md b/doc/handwritten/for-maintainers/adr/0026-rebase-testing-arbitrary-values-on-dummies.fr.md new file mode 100644 index 00000000..fe3aacf3 --- /dev/null +++ b/doc/handwritten/for-maintainers/adr/0026-rebase-testing-arbitrary-values-on-dummies.fr.md @@ -0,0 +1,232 @@ +# ADR-0026 | Rebaser les valeurs de test arbitraires sur Dummies + +🌍 🇬🇧 [English](0026-rebase-testing-arbitrary-values-on-dummies.md) · 🇫🇷 Français (ce fichier) + +**Statut :** Accepté +**Date :** 2026-07-19 +**Décideurs :** Reefact + +## Contexte + +`FirstClassErrors.Testing` est un package compagnon publié (actuellement +`0.1.0-preview.1`). Il fournit les valeurs de test arbitraires au travers d'une +façade statique `Any` adossée à une source pseudo-aléatoire privée, à graine et +locale au contexte ; cette même source adosse aussi les variantes `UseAny()` de +ses deux autres points d'injection — une horloge gelable et des identifiants +d'instance gelables. Cette surface a été décidée par l'ADR-0006, dont l'action de +suivi anticipait l'extraction du moteur de valeurs générique dans un utilitaire +autonome et agnostique aux erreurs « si un second consommateur apparaît », et qui +maintenait le moteur séparable en interne à cette fin. + +Depuis l'ADR-0006, cet utilitaire a été livré. L'ADR-0011 a introduit `Dummies` : +une bibliothèque de générateurs fluides autonome, sans dépendance et agnostique +aux erreurs (`IAny`, matérialisée par `Generate()`), hébergée dans ce dépôt +mais ne référençant aucun projet FirstClassErrors — une frontière imposée par un +test d'architecture. L'ADR-0011 a nommé ses premiers consommateurs visés : « les +projets de tests de ce dépôt, et possiblement `FirstClassErrors.Testing` plus +tard », et a laissé une action de suivi explicitement ouverte : « Décider +séparément si `FirstClassErrors.Testing` réasseoit plus tard son moteur de valeurs +interne sur `Dummies`. » + +L'état actuel porte les faits qui pèsent sur cette décision : + +* **Deux moteurs indépendants coexistent.** `FirstClassErrors.Testing` puise dans + sa propre source à graine ; `Dummies` puise dans la sienne. Chacun expose sa + propre portée `Reproducibly`/graine, et les deux graines sont sans rapport : un + test qui mêle les deux façades ne peut pas être rejoué depuis une unique graine + signalée. +* **Les deux façades `Any` entrent en collision de noms de type.** + `FirstClassErrors.Testing.Any` et `Dummies.Any` sont toutes deux + `static class Any` ; un fichier de test important les deux espaces de noms ne + peut pas nommer `Any` sans qualification. +* **`Dummies` couvre déjà toutes les capacités dont le rebase a besoin.** Son + générateur d'énumération exclut des membres (`Except`, `DifferentFrom`, + `OneOf`) ; ses points d'entrée statiques `Any.*` puisent dans un contexte + ambiant que `Any.Reproducibly(...)` fixe pour une portée (avec `Any.WithSeed` + pour un contexte isolé) ; et `As`/`Combine` transforment des primitives sous + contrainte en valeurs métier. L'ADR-0020 a fait de `Generate()` l'unique + matérialisation, en retirant les conversions implicites. +* **Le vocabulaire d'erreur ne peut pas migrer dans `Dummies`.** Les utilitaires + `ErrorCode`, d'énumération significative (`Transience`, `InteractionDirection`) + et de messages de `Testing` référencent des types FirstClassErrors, que la + frontière de l'ADR-0011 interdit à `Dummies` de référencer. +* **`Dummies` n'est pas encore sur NuGet.** L'ADR-0011 ne lui donne aucun train de + release avant sa première publication ; au sein de ce dépôt, il n'est consommé + qu'au travers d'une référence de projet. + +## Décision + +`FirstClassErrors.Testing` puise toute valeur arbitraire dans `Dummies` plutôt que +dans un moteur privé : sa façade `Any` et sa source à graine sont retirées, ses +points d'injection d'horloge et d'identifiant d'instance puisent dans le contexte +ambiant reproductible de Dummies, et le vocabulaire d'erreur qu'il conserve est +exposé sous forme de fabriques métier nommées — `ErrorCodeFactory`, +`TransienceFactory`, `DiagnosticMessageFactory`, et consorts — chacune renvoyant +directement une valeur matérialisée (le cas courant) et exposant un générateur +`IAny` via une méthode distincte là où la composition est nécessaire. + +## Justification + +* **Un moteur, une histoire de graine — l'esprit « source unique » de l'ADR-0006 à + l'échelle du dépôt.** Deux moteurs signifiaient deux portées `Reproducibly` dont + les graines ne se composent pas, de sorte qu'un test puisant dans les deux + façades ne pouvait pas être rejoué depuis une seule graine. Puiser toute valeur + dans le contexte ambiant de Dummies place les primitives, les valeurs métier, + l'horloge et les identifiants d'instance sous un unique `Any.Reproducibly(...)`, + si bien qu'une seule graine signalée rejoue toute l'exécution. C'est la même + propriété « une source unique, ensemencée une fois » que l'ADR-0006 a choisie, + étendue au moteur générique qui vit désormais hors du package. +* **Elle réalise l'action de suivi anticipée par l'ADR-0006 *et* l'ADR-0011.** Le + moteur générique que l'ADR-0006 gardait « séparable en interne » existe désormais + sous le nom de `Dummies`, et le « second consommateur » qu'il attendait est + arrivé — les projets de tests de ce dépôt. Réasseoir `Testing` sur `Dummies` + résout la question ouverte de l'ADR-0011 au lieu de maintenir indéfiniment un + moteur parallèle. +* **Le rebase n'ajoute aucun manque de capacité.** Tout comportement dont le + package a besoin existe déjà dans Dummies : l'exclusion de membres pour les + énumérations significatives, un contexte ambiant reproductible pour l'horloge et + les identifiants, et `As`/`Combine` pour assembler des valeurs métier. Rien n'a à + être ajouté à Dummies comme préalable, si bien que le package est façonné par un + consommateur réel plutôt que par une finalisation prise dans l'abstrait. +* **Le vocabulaire reste dans `Testing`, mais en fabriques, pas en façade.** Parce + que la frontière de l'ADR-0011 interdit au vocabulaire d'erreur de vivre dans + `Dummies`, il demeure dans `Testing` ; le réexprimer en types `…Factory` nommés + plutôt qu'en un second `Any` supprime la collision de noms + `Testing.Any`/`Dummies.Any`, laissant `Dummies.Any` comme l'unique `Any` qu'un + test nomme, tandis que les valeurs métier proviennent de fabriques clairement + nommées. +* **Valeur par défaut, générateur à la demande — sans ranimer le hasard que + l'ADR-0020 a retiré.** L'appel dominant a besoin d'une valeur arbitraire, donc une + fabrique la renvoie directement — à l'image des utilitaires de valeurs de test que + le dépôt utilise déjà — et n'expose un générateur `IAny` via une méthode + distincte que pour la minorité de sites qui composent (`Any.ListOf`, `Combine`, + `OrNull`). Une méthode nommée qui appelle `Generate()` en interne n'est pas la + conversion implicite que l'ADR-0020 a retirée — le tirage est un appel explicite + et visible, non un élargissement déguisé en affectation — de sorte que la forme + concise pour la valeur ne coûte aucune des garanties de cette décision. Réserver + le nom simple du cas courant à la valeur fait écho à l'ADR-0005. +* **C'est le moment le moins cher.** `Testing` est en `0.1.0-preview.1`, un package + pré-stable sans garantie de compatibilité, donc retirer `Any` ne coûte + aujourd'hui aucune cérémonie de migration des consommateurs ; le même + raisonnement pré-1.0 que l'ADR-0020 a utilisé pour retirer les conversions de + Dummies s'applique ici. + +## Alternatives envisagées + +### Garder le moteur propre à `Testing` et laisser les projets de tests utiliser `Dummies` directement + +Envisagé parce que c'est le moindre travail et que cela livre aujourd'hui : rien +ne change dans `Testing`, et les projets de tests ajoutent simplement `Dummies` +pour les valeurs que sa façade ne couvre pas. Rejeté parce que cela +institutionnalise l'état à deux moteurs — deux graines `Reproducibly` qui ne se +composent pas et une collision de noms `Testing.Any`/`Dummies.Any` — soit la +fragmentation entre tests que l'esprit « source unique » de l'ADR-0006 existe pour +éviter, désormais reproduite à l'échelle du dépôt. + +### Déplacer le vocabulaire d'erreur dans `Dummies` + +Envisagé parce qu'il ne resterait qu'un seul `Any` et un seul foyer pour les +valeurs arbitraires. Rejeté parce que `Dummies` est agnostique aux erreurs par +l'ADR-0011, une frontière qu'un test d'architecture impose ; le vocabulaire +d'erreur référence des types FirstClassErrors et ne peut pas y vivre sans briser +cette promesse. + +### Supprimer le vocabulaire d'erreur et inliner `As(...).Generate()` sur les sites d'appel + +Envisagé parce que cela retire purement et simplement une surface publique. +Rejeté parce que cela disperse le vocabulaire d'erreur — et sa convention +« reconnaissable comme arbitraire » — sur chaque consommateur, et retire des +utilitaires livrés d'un package publié sans aucun gain par rapport au fait de les +garder comme fines fabriques. + +### Conserver le nom de façade `Any` dans `Testing`, seulement réassis + +Envisagé pour minimiser le remaniement des sites d'appel en préservant la forme +familière `Any.ErrorCode()`. Rejeté parce que deux `static class Any` dans deux +espaces de noms restent ambigus dès que les deux sont importés, ce qui abandonne +précisément la clarté que le rebase vise à apporter — un seul `Any`. + +### Renvoyer `IAny` uniformément depuis chaque fabrique + +Envisagé pour une cohérence stricte avec le modèle recette-contre-valeur de +Dummies. Rejeté parce que cela taxe le cas dominant « donne-moi une valeur +arbitraire » d'un `.Generate()` obligatoire sans bénéfice : une méthode de valeur +nommée n'est pas le hasard de conversion implicite que l'ADR-0020 a retiré, de +sorte que la valeur par défaut conserve les garanties de cette décision tout en +restant concise. + +## Conséquences + +### Positives + +* Un unique moteur de valeurs arbitraires et une unique portée `Reproducibly`/graine + à l'échelle du dépôt : un test mêlant primitives, valeurs métier, horloge et + identifiants d'instance se rejoue depuis une seule graine signalée. +* `Dummies.Any` est l'unique `Any` ; la collision de noms de type disparaît, et les + valeurs métier se lisent depuis des fabriques explicitement nommées. +* `Testing` cesse de maintenir un moteur de valeurs parallèle ; la machinerie + générique vit une seule fois, dans `Dummies`, façonnée par un consommateur réel + du dépôt. + +### Négatives + +* Un changement cassant sur un package publié : `Testing.Any` et son `Reproducibly` + sont retirés, et les consommateurs migrent vers `Dummies.Any` et les fabriques. + Acceptable en `0.1.0-preview.1`, et la raison pour laquelle la décision est prise + avant une release stable. +* `Testing` gagne une dépendance sur `Dummies`. Tant que `Dummies` n'est pas sur + NuGet, le package `Testing` doit embarquer `Dummies` dans son propre artefact + pour rester restaurable — un arrangement d'intérim, non l'état final. +* La suite de tests migre de `Any.*` vers `Dummies.Any.*().Generate()` et les + nouvelles fabriques, et la couverture de reproductibilité/déterminisme de la + façade retirée est retravaillée sur la nouvelle surface. + +### Risques + +* Embarquer `Dummies` dans l'artefact `Testing` devient un risque d'identité de + type dès l'instant où `Dummies` est référençable indépendamment sur NuGet — deux + assemblies `Dummies` de même identité mais d'origine distincte — précisément + parce que des types Dummies apparaissent dans l'API publique de `Testing`. + Atténué par l'action de suivi consistant à basculer vers une dépendance NuGet dès + la première publication de Dummies. +* Un appelant portant l'ancien modèle mental `Testing.Any` peut chercher un membre + retiré ; le risque est borné car l'omission est une erreur de compilation au + message actionnable, jamais une valeur erronée silencieuse — la même classe de + risque que l'ADR-0020 a acceptée. +* La règle « valeur par défaut, générateur via une méthode distincte » tient par la + revue et la documentation tant que, le cas échéant, l'outillage ne l'impose pas — + la même dépendance que l'ADR-0006 a déjà acceptée pour l'habitude « arbitraire ⇒ + utiliser la source ». + +## Actions de suivi + +* À l'acceptation, l'ADR-0006 est supersédé par le présent ADR (son statut passe à + *Superseded* avec un lien vers ici), et l'action de suivi ouverte de l'ADR-0011 — + décider si `Testing` se réasseoit sur `Dummies` — est résolue par cette décision. +* Basculer la dépendance `Dummies` que `Testing` embarque dans son package vers un + `PackageReference` NuGet dès la première publication de `Dummies`, dénouant + l'arrangement d'intérim et retirant le risque de double assembly. +* Mettre à jour le guide de test destiné aux utilisateurs et le README du package + `Testing`, en anglais et en français de manière synchronisée : `Any` est retiré, + les valeurs arbitraires proviennent de `Dummies`, et les fabriques métier sont + introduites. +* Ajouter un petit projet `FirstClassErrors.Testing.UnitTests` ne portant que les + tests de contrat que le package conserve — reproductibilité de l'horloge et des + identifiants d'instance sous `Any.Reproducibly`, et fabriques d'énumération + significative ne renvoyant jamais la sentinelle — et retirer les assertions de + valeur enveloppante désormais couvertes en transitif et par `Dummies.UnitTests`. + +## Références + +* ADR-0006 — Fournir les valeurs de test arbitraires depuis une source unique à + graine : la décision que celui-ci supersède, et l'action de suivi (extraire le + moteur générique pour un second consommateur) qu'il réalise. +* ADR-0011 — Héberger Dummies comme package autonome : la frontière agnostique aux + erreurs qui garde le vocabulaire dans `Testing`, et l'action de suivi ouverte que + celui-ci résout. +* ADR-0020 — Matérialiser les dummies uniquement par `Generate()` : le hasard de + conversion implicite qu'une fabrique de valeur nommée ne ranime pas, et le + raisonnement pré-1.0 réutilisé ici. +* ADR-0005 — Réserver le nom de fabrique simple à la variante renvoyant un Outcome : + la décision de nommage antérieure dans le même esprit, le nom simple servant le + cas courant. diff --git a/doc/handwritten/for-maintainers/adr/0026-rebase-testing-arbitrary-values-on-dummies.md b/doc/handwritten/for-maintainers/adr/0026-rebase-testing-arbitrary-values-on-dummies.md new file mode 100644 index 00000000..08d16fba --- /dev/null +++ b/doc/handwritten/for-maintainers/adr/0026-rebase-testing-arbitrary-values-on-dummies.md @@ -0,0 +1,211 @@ +# ADR-0026 | Rebase the testing package's arbitrary values on Dummies + +🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0026-rebase-testing-arbitrary-values-on-dummies.fr.md) + +**Status:** Accepted +**Date:** 2026-07-19 +**Decision Makers:** Reefact + +## Context + +`FirstClassErrors.Testing` is a published companion package (currently +`0.1.0-preview.1`). It supplies arbitrary test values through a static `Any` +facade backed by a private, context-local, seedable pseudo-random source, and +that same source also backs the `UseAny()` variants of its two other seams — a +freezable clock and freezable instance ids. This surface was decided in ADR-0006, +whose follow-up anticipated extracting the generic value engine into a +standalone, error-agnostic utility "if a second consumer appears", and which kept +the engine internally separable to that end. + +Since ADR-0006, that utility shipped. ADR-0011 introduced `Dummies`: a standalone, +dependency-free, error-agnostic fluent generator library (`IAny`, materialized +through `Generate()`), hosted in this repository but referencing no +FirstClassErrors project — a boundary enforced by an architecture test. ADR-0011 +named its intended early consumers as "this repository's own test projects, and +possibly `FirstClassErrors.Testing` later", and left an explicit open follow-up: +"Decide separately whether `FirstClassErrors.Testing` later re-bases its internal +value engine on `Dummies`." + +The current state carries the facts that bear on that decision: + +* **Two independent engines coexist.** `FirstClassErrors.Testing` draws from its + own seedable source; `Dummies` draws from its own. Each exposes its own + `Reproducibly`/seed scope, and the two seeds are unrelated: a test that mixes + both facades cannot be replayed from a single reported seed. +* **The two `Any` facades collide as type names.** `FirstClassErrors.Testing.Any` + and `Dummies.Any` are both `static class Any`; a test file importing both + namespaces cannot name `Any` unqualified. +* **`Dummies` already covers every capability the rebase needs.** Its enum + generator excludes members (`Except`, `DifferentFrom`, `OneOf`); its static + `Any.*` entry points draw from an ambient context that `Any.Reproducibly(...)` + pins for a scope (with `Any.WithSeed` for an isolated one); and `As`/`Combine` + turn constrained primitives into domain values. ADR-0020 made `Generate()` the + sole materialization, removing the implicit conversions. +* **The error vocabulary cannot move into `Dummies`.** `Testing`'s `ErrorCode`, + meaningful-enum (`Transience`, `InteractionDirection`), and message helpers + reference FirstClassErrors types, which the ADR-0011 boundary forbids `Dummies` + to reference. +* **`Dummies` is not yet on NuGet.** ADR-0011 gives it no release train until its + first publication; within this repository it is consumed only through a project + reference. + +## Decision + +`FirstClassErrors.Testing` sources every arbitrary value from `Dummies` instead +of a private engine: its `Any` facade and seedable source are removed, its +freezable clock and instance-id seams draw from Dummies' ambient reproducible +context, and the error vocabulary it still owns is exposed as named domain +factories — `ErrorCodeFactory`, `TransienceFactory`, `DiagnosticMessageFactory`, +and peers — each returning a materialized value directly (the common case) and +exposing an `IAny` generator through a distinct method where composition is +needed. + +## Rationale + +* **One engine, one seed story — the single-source spirit of ADR-0006 at repository + scale.** Two engines meant two `Reproducibly` scopes whose seeds do not compose, + so a test drawing on both facades could not be replayed from one seed. Sourcing + every value from Dummies' ambient context puts primitives, domain values, the + clock, and the instance ids under a single `Any.Reproducibly(...)`, so one + reported seed replays the whole run. This is the same "a single source, seeded + once" property ADR-0006 chose, extended to cover the generic engine that now + lives outside the package. +* **It realizes the follow-up ADR-0006 and ADR-0011 both anticipated.** The + generic engine ADR-0006 kept "internally separable" now exists as `Dummies`, and + the "second consumer" it waited for has arrived — this repository's test + projects. Re-basing `Testing` on `Dummies` resolves ADR-0011's open question + rather than maintaining a parallel engine indefinitely. +* **The rebase adds no capability gap.** Every behaviour the package needs already + exists in Dummies: member exclusion for meaningful enums, an ambient reproducible + context for the clock and ids, and `As`/`Combine` for assembling domain values. + Nothing has to be added to Dummies as a precondition, so the package is shaped by + a real consumer rather than a finalization taken in the abstract. +* **The vocabulary stays in `Testing`, but as factories, not a facade.** Because + the ADR-0011 boundary forbids the error vocabulary from living in `Dummies`, it + remains in `Testing`; re-expressing it as named `…Factory` types rather than a + second `Any` removes the `Testing.Any`/`Dummies.Any` type collision, leaving + `Dummies.Any` as the one `Any` a test names while domain values come from + clearly-named factories. +* **Value by default, generator on demand — without reviving the hazard ADR-0020 + removed.** The dominant call needs one arbitrary value, so a factory returns it + directly — matching the arbitrary-value test helpers the repository already uses — + and exposes an `IAny` generator through a distinct method only for the minority + of sites that compose (`Any.ListOf`, `Combine`, `OrNull`). A named method that + internally calls `Generate()` is not the implicit conversion ADR-0020 removed — + the draw is an explicit, visible call, not a widening disguised as an assignment — + so the terse value form costs none of that decision's guarantees. Reserving the + plain, common-case name for the value echoes ADR-0005. +* **This is the cheapest moment.** `Testing` is `0.1.0-preview.1`, a pre-stable + package with no compatibility guarantee, so removing `Any` costs no consumer + migration ceremony now; the same pre-1.0 reasoning ADR-0020 used to drop the + Dummies conversions applies here. + +## Alternatives Considered + +### Keep `Testing`'s own engine and let the test projects use `Dummies` directly + +Considered because it is the least work and ships today: nothing in `Testing` +changes, and the test projects simply add `Dummies` for the values its facade does +not cover. Rejected because it institutionalizes the two-engine state — two +`Reproducibly` seeds that do not compose and a `Testing.Any`/`Dummies.Any` type +collision — which is the cross-test fragmentation the single-source spirit of +ADR-0006 exists to avoid, now reproduced at repository scale. + +### Move the error vocabulary into `Dummies` + +Considered because it would leave exactly one `Any` and one home for arbitrary +values. Rejected because `Dummies` is error-agnostic by ADR-0011, a boundary an +architecture test enforces; the error vocabulary references FirstClassErrors types +and cannot live there without breaking that promise. + +### Delete the error vocabulary and inline `As(...).Generate()` at call sites + +Considered because it removes a public surface outright. Rejected because it +scatters the error vocabulary — and its "recognizable as arbitrary" convention — +across every consumer, and strips shipped helpers from a published package for no +gain over keeping them as thin factories. + +### Keep the `Any` facade name in `Testing`, only rebased + +Considered to minimize call-site churn by preserving the familiar `Any.ErrorCode()` +shape. Rejected because two `static class Any` in two namespaces remain ambiguous +whenever both are imported, which forfeits the very clarity the rebase is meant to +deliver — a single `Any`. + +### Return `IAny` uniformly from every factory + +Considered for strict consistency with Dummies' recipe-versus-value model. +Rejected because it taxes the dominant "give me one arbitrary value" case with a +mandatory `.Generate()` for no benefit: a named value method is not the +implicit-conversion hazard ADR-0020 removed, so value-by-default keeps that +decision's guarantees while staying terse. + +## Consequences + +### Positive + +* A single arbitrary-value engine and a single `Reproducibly`/seed scope across + the repository: a test mixing primitives, domain values, the clock, and instance + ids replays from one reported seed. +* `Dummies.Any` is the only `Any`; the type-name collision is gone, and domain + values are read from explicitly-named factories. +* `Testing` stops maintaining a parallel value engine; the generic machinery lives + once, in `Dummies`, shaped by a real in-repository consumer. + +### Negative + +* A breaking change to a published package: `Testing.Any` and its `Reproducibly` + are removed, and consumers move to `Dummies.Any` and the factories. Acceptable + at `0.1.0-preview.1`, and the reason the decision is taken before a stable + release. +* `Testing` gains a dependency on `Dummies`. While `Dummies` is not on NuGet, the + `Testing` package must carry `Dummies` inside its own artifact to stay + restorable — an interim arrangement, not the end state. +* The test suite migrates from `Any.*` to `Dummies.Any.*().Generate()` and the new + factories, and the reproducibility/determinism coverage of the removed facade is + reworked onto the new surface. + +### Risks + +* Carrying `Dummies` inside the `Testing` artifact becomes a type-identity hazard + the moment `Dummies` is independently referenceable on NuGet — two `Dummies` + assemblies of the same identity but distinct origin — precisely because Dummies + types appear in `Testing`'s public API. Mitigated by the follow-up to switch to a + real NuGet dependency at Dummies' first publication. +* A caller carrying the old `Testing.Any` mental model may reach for a removed + member; the risk is bounded because the omission is a compile-time error with an + actionable message, never a silent wrong value — the same class of risk ADR-0020 + accepted. +* The "value by default, generator through a distinct method" rule holds by review + and documentation until, if ever, tooling enforces it — the same reliance ADR-0006 + already accepted for the "arbitrary ⇒ use the source" habit. + +## Follow-up Actions + +* On acceptance, ADR-0006 is superseded by this ADR (its status flips to + *Superseded* with a link here), and ADR-0011's open follow-up — whether + `Testing` re-bases on `Dummies` — is resolved by this decision. +* Switch the `Dummies` dependency `Testing` carries inside its package to a NuGet + `PackageReference` the moment `Dummies` is first published, unwinding the interim + arrangement and removing the double-assembly hazard. +* Update the user testing guide and the `Testing` package README, in English and + French in lockstep: `Any` is removed, arbitrary values are sourced from + `Dummies`, and the domain factories are introduced. +* Add a small `FirstClassErrors.Testing.UnitTests` project holding only the + behavioural contract tests the package still owns — clock and instance-id + reproducibility under `Any.Reproducibly`, and meaningful-enum factories never + yielding the sentinel — and drop the wrapper-value assertions now covered + transitively and by `Dummies.UnitTests`. + +## References + +* ADR-0006 — Supply arbitrary test values from a single seedable source: the + decision this supersedes, and the follow-up (extract the generic engine for a + second consumer) this realizes. +* ADR-0011 — Host Dummies as a standalone package: the error-agnostic boundary that + keeps the vocabulary in `Testing`, and the open follow-up this resolves. +* ADR-0020 — Materialize dummies only through `Generate()`: the implicit-conversion + hazard a named value factory does not reintroduce, and the pre-1.0 reasoning + reused here. +* ADR-0005 — Reserve the plain factory name for the Outcome-returning variant: the + prior naming decision in the same spirit, the plain name serving the common case. diff --git a/doc/handwritten/for-maintainers/adr/README.md b/doc/handwritten/for-maintainers/adr/README.md index 991a808f..8e9ba546 100644 --- a/doc/handwritten/for-maintainers/adr/README.md +++ b/doc/handwritten/for-maintainers/adr/README.md @@ -188,7 +188,7 @@ Optional supporting material: | [ADR-0003](0003-unify-outcome-value-mapping-under-then.md) | Unify Outcome value mapping under Then | Accepted | | [ADR-0004](0004-check-every-pull-request-against-the-adr-base.md) | Check every pull request against the ADR base | Accepted | | [ADR-0005](0005-reserve-the-plain-factory-name-for-the-outcome-returning-variant.md) | Reserve the plain factory name for the Outcome-returning variant | Accepted | -| [ADR-0006](0006-supply-arbitrary-test-values-from-a-seedable-source.md) | Supply arbitrary test values from a single seedable source | Accepted | +| [ADR-0006](0006-supply-arbitrary-test-values-from-a-seedable-source.md) | Supply arbitrary test values from a single seedable source | Superseded | | [ADR-0007](0007-name-the-binder-terminals-new-and-create.md) | Name the binder terminals New and Create | Accepted | | [ADR-0008](0008-bind-nullable-value-type-properties-through-a-struct-constrained-overload.md) | Bind nullable value-type properties through a struct-constrained overload | Accepted | | [ADR-0009](0009-report-the-toolings-failures-as-first-class-errors.md) | Report the tooling's failures as first-class errors | Accepted | @@ -208,3 +208,4 @@ Optional supporting material: | [ADR-0023](0023-keep-expression-tree-selectors-for-the-v1-binder-api.md) | Keep expression-tree selectors for the v1 binder API | Accepted | | [ADR-0024](0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) | Allow a one-time editorial refactoring of accepted ADRs | Accepted | | [ADR-0025](0025-generate-strings-from-a-home-grown-regular-subset.md) | Generate matching strings from a home-grown regular subset | Proposed | +| [ADR-0026](0026-rebase-testing-arbitrary-values-on-dummies.md) | Rebase the testing package's arbitrary values on Dummies | Accepted | diff --git a/doc/handwritten/for-users/ArbitraryTestValues.en.md b/doc/handwritten/for-users/ArbitraryTestValues.en.md index 948539bb..45894314 100644 --- a/doc/handwritten/for-users/ArbitraryTestValues.en.md +++ b/doc/handwritten/for-users/ArbitraryTestValues.en.md @@ -3,9 +3,14 @@ 🌍 **Languages:** 🇬🇧 English (this file) | 🇫🇷 [Français](./ArbitraryTestValues.fr.md) -A large part of a test's `Arrange` is usually values the test never checks — an error code, a diagnostic message, an occurrence instant. Spelled out as literals they read as if they mattered, and a constant reused across a suite can let a test pass for the wrong reason. `Any` supplies a valid-but-arbitrary value instead, so the one input that matters stands out and the rest announce themselves as incidental. +A large part of a test's `Arrange` is usually values the test never checks — an error code, a diagnostic message, an occurrence instant. Spelled out as literals they read as if they mattered, and a constant reused across a suite can let a test pass for the wrong reason. An *arbitrary* value supplies a valid-but-incidental input instead, so the one input that matters stands out and the rest announce themselves as incidental. -`Any` lives in **`FirstClassErrors.Testing`**; it adds no dependency and, like the clock and instance-id overrides, is scoped, context-local, and safe under parallel tests. For freezing values a test *does* assert on, see [Deterministic Error Tests](DeterministicTesting.en.md). +Two sources cover this, and both draw from the same ambient random source: + +- **[`Dummies`](https://github.com/Reefact/first-class-errors)** — a fluent generator of arbitrary primitives (`Dummies.Any.Int32()`, `Dummies.Any.String()`, ...). A `Dummies.Any.*` call returns a *recipe*; call `.Generate()` to draw the value. +- **Domain factories** in **`FirstClassErrors.Testing`** — `ErrorCodeFactory.Any()`, `DiagnosticMessageFactory.Any()`, and peers — for the error vocabulary a raw primitive cannot express. Each returns the value directly. + +Because both flow through the same source, a single `Dummies.Any.Reproducibly(...)` makes a whole test replayable, and — like the clock and instance-id overrides — the source is scoped, context-local, and safe under parallel tests. For freezing values a test *does* assert on, see [Deterministic Error Tests](DeterministicTesting.en.md). ## Supply an arbitrary value @@ -21,60 +26,66 @@ Outcome.Failure(error).ShouldFail().WithCode("ORDER_NOT_FOUND"); ``` ```csharp -// 🙂 After — the code is the subject; the messages are Any. +// 🙂 After — the code is the subject; the messages are arbitrary. DomainError error = DomainError - .Create(ErrorCode.Create("ORDER_NOT_FOUND"), Any.DiagnosticMessage()) - .WithPublicMessage(Any.ShortMessage()); + .Create(ErrorCode.Create("ORDER_NOT_FOUND"), DiagnosticMessageFactory.Any()) + .WithPublicMessage(ShortMessageFactory.Any()); Outcome.Failure(error).ShouldFail().WithCode("ORDER_NOT_FOUND"); ``` -A value is only incidental when it cannot steer the code under test. If it feeds a branch, a validation, a serialization, or an ordering, it shapes the behavior even though the test never asserts it — and it cannot safely be left arbitrary. Reach for `Any` for inputs the test carries but does not act on. +A value is only incidental when it cannot steer the code under test. If it feeds a branch, a validation, a serialization, or an ordering, it shapes the behavior even though the test never asserts it — and it cannot safely be left arbitrary. Reach for an arbitrary value for inputs the test carries but does not act on. -## What `Any` offers +## The error vocabulary: domain factories -Every helper returns a value that is **valid for its type** — non-blank strings and messages, a real UTC instant, an error code that is never blank: +For the parts of an error a test needs but never asserts on, `FirstClassErrors.Testing` ships a factory per concept. Each `Any()` returns a value that is **valid for its type** — non-blank, and recognizable as arbitrary — drawn from the ambient source: -| Helper | Returns | +| Factory | Returns | | --- | --- | -| `Any.ErrorCode()` | a valid, non-blank code of the form `ANY_CODE_` + 6 uppercase alphanumerics | -| `Any.DiagnosticMessage()` / `Any.ShortMessage()` / `Any.DetailedMessage()` | a non-blank message of bounded length | -| `Any.String()` | a non-empty string of bounded length (`any-` + 8 lowercase alphanumerics); no whitespace | -| `Any.Guid()` | an arbitrary `Guid` | -| `Any.Instant()` | an arbitrary UTC instant (offset zero) between 1 January 2000 and around 2068 | -| `Any.Int()` | an arbitrary `int` — it may be negative or zero | -| `Any.Bool()` | `true` or `false` | -| `Any.Enum()` | any member of the enum — a sentinel such as `Unknown` included | -| `Any.Transience()` / `Any.InteractionDirection()` | a *meaningful* value — never the `Unknown` sentinel | -| `Any.ErrorOrigin()` | any `ErrorOrigin`; all three values are meaningful, so there is no sentinel to exclude | +| `ErrorCodeFactory.Any()` | a valid, non-blank `ErrorCode` of the form `ANY_CODE_` + 6 uppercase alphanumerics | +| `DiagnosticMessageFactory.Any()` / `ShortMessageFactory.Any()` / `DetailedMessageFactory.Any()` | a non-blank message, recognizable as arbitrary | +| `TransienceFactory.Any()` / `InteractionDirectionFactory.Any()` | a *meaningful* value — never the `Unknown` sentinel | +| `ErrorOriginFactory.Any()` | any `ErrorOrigin`; all its values are meaningful, so there is no sentinel to exclude | + +A factory returns the value directly — the common case needs no `.Generate()`. Use the meaningful-enum factories (`TransienceFactory`, `InteractionDirectionFactory`) when the test needs a value that actually drives behavior; reach for a plain `Dummies.Any.Enum()` draw only when any member — a sentinel included — will do. + +## Primitives: Dummies + +For arbitrary primitives, use **`Dummies`** directly. A `Dummies.Any.*` call returns a *generator* — an immutable recipe — and `.Generate()` draws one value from it: + +```csharp +int quantity = Dummies.Any.Int32().Generate(); +string reference = Dummies.Any.String().NonEmpty().Generate(); +Guid id = Dummies.Any.Guid().Generate(); +``` -The guarantees stop at type validity. A helper does not target a domain precondition — `Any.Int()` may be negative, `Any.String()` is not a well-formed email — so a value object with a stricter contract needs its own arbitrary factory rather than a raw primitive. +Constraints chained on the generator express what the surrounding code *requires* of the value — a length, a range, a prefix — never what the test asserts. The full generator surface (constraints, collections, composition through `As`/`Combine`, `.OrNull()`) is documented with `Dummies` itself. -Use `Any.Enum()` when any member will do — a sentinel included — and the named enum helpers when the test needs a value that actually drives the behavior under test. +The guarantees stop at type validity. A generator does not target a domain precondition — `Dummies.Any.Int32()` may be negative, `Dummies.Any.String()` is not a well-formed email — so a value object with a stricter contract is built by turning a constrained primitive into it: `Dummies.Any.String().StartingWith("ORD-").WithLength(12).As(OrderReference.Create).Generate()`. ## Reproduce a failing run The source is unseeded by default, so the values differ between runs. That is deliberate: a test that passes only for one particular value is relying on something it never states, and varying the value surfaces that coupling. -When a run matters enough to reproduce, wrap the test body in `Any.Reproducibly`. It pins a fresh seed for the run and, if the body throws, **reports that seed** before the failure propagates — so a red test tells you exactly how to replay it: +When a run matters enough to reproduce, wrap the test body in `Dummies.Any.Reproducibly`. It pins a fresh seed for the run and, if the body throws, **reports that seed** before the failure propagates — so a red test tells you exactly how to replay it: ```csharp [Fact] public void Some_value_sensitive_test() => - Any.Reproducibly(() => { - // ... arrange with Any, act, assert ... + Dummies.Any.Reproducibly(() => { + // ... arrange with the factories and Dummies.Any, act, assert ... }); ``` On failure the seed is written to `Console.Error` by default; pass your framework's writer (for example xUnit's `ITestOutputHelper.WriteLine`) to route it there instead. Replay the run by handing the reported seed back: ```csharp -Any.Reproducibly(1234, () => { +Dummies.Any.Reproducibly(1234, () => { // ... the same body ... }); ``` -Reproducing a run needs the same sequence of `Any` calls, so a body whose call order depends on non-deterministic external state is not fully replayable from the seed alone. There is also an asynchronous overload, `Any.Reproducibly(Func)`, for `async` test bodies. +Reproducing a run needs the same sequence of draws, so a body whose order depends on non-deterministic external state is not fully replayable from the seed alone. There is also an asynchronous overload, `Dummies.Any.Reproducibly(Func)`, for `async` test bodies. Because the factories, the primitives, and the clock and id seams below all draw from the same ambient source, one `Reproducibly` scope replays them together. ## Arbitrary `OccurredAt` and `InstanceId` @@ -82,7 +93,7 @@ Occurrence data is arbitrary in the same sense: a test often needs it stable wit ```csharp DomainError NewError() => - DomainError.Create(Any.ErrorCode(), Any.DiagnosticMessage()).WithPublicMessage(Any.ShortMessage()); + DomainError.Create(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()).WithPublicMessage(ShortMessageFactory.Any()); using (Clock.UseAny()) using (InstanceIds.UseAny()) { @@ -94,11 +105,11 @@ using (InstanceIds.UseAny()) { } ``` -Both draw from the same source as `Any`, so running them inside `Any.Reproducibly` makes their instant and ids reproducible too. To pin a *specific* instant or id instead, use `UseFixed` — see [Deterministic Error Tests](DeterministicTesting.en.md). +Both draw from the same ambient source as `Dummies.Any`, so running them inside `Dummies.Any.Reproducibly` makes their instant and ids reproducible too. To pin a *specific* instant or id instead, use `UseFixed` — see [Deterministic Error Tests](DeterministicTesting.en.md). ## Scope and parallel tests -`Any.Reproducibly`, `Clock.UseAny`, and `InstanceIds.UseAny` all take effect only for the run or `using` block they wrap, and the arbitrary source is restored when it exits. That source is stored in an `AsyncLocal`, so it follows the test's own execution flow and never leaks into other tests running at the same time. +`Dummies.Any.Reproducibly`, `Clock.UseAny`, and `InstanceIds.UseAny` all take effect only for the run or `using` block they wrap, and the arbitrary source is restored when it exits. That source is stored in an `AsyncLocal`, so it follows the test's own execution flow and never leaks into other tests running at the same time. ## Review checklist @@ -106,8 +117,8 @@ Before reaching for an arbitrary value, verify that: - the value does **not** change the functional path the test exercises — it must not feed a branch, a validation, a serialization, or an ordering, even indirectly; - the value is genuinely not checked by the test — otherwise use a literal; -- a named enum helper is used when the test needs a meaningful value, rather than `Any.Enum()`; -- a value-sensitive test is wrapped in `Any.Reproducibly` so a failing run reports the seed to replay; +- a meaningful-enum factory (`TransienceFactory`, `InteractionDirectionFactory`) is used when the test needs a meaningful value, rather than a plain `Dummies.Any.Enum()` draw; +- a value-sensitive test is wrapped in `Dummies.Any.Reproducibly` so a failing run reports the seed to replay; - `Clock.UseAny` / `InstanceIds.UseAny` are used for stable-but-irrelevant occurrence data, and `UseFixed` when the exact value is asserted. --- diff --git a/doc/handwritten/for-users/ArbitraryTestValues.fr.md b/doc/handwritten/for-users/ArbitraryTestValues.fr.md index dbeec299..f34fd2ea 100644 --- a/doc/handwritten/for-users/ArbitraryTestValues.fr.md +++ b/doc/handwritten/for-users/ArbitraryTestValues.fr.md @@ -3,9 +3,14 @@ 🌍 **Langues :** 🇫🇷 Français (ce fichier) | 🇬🇧 [English](./ArbitraryTestValues.en.md) -Une grande partie de l’`Arrange` d’un test est d’ordinaire faite de valeurs qu’il ne vérifie jamais — un code d’erreur, un message de diagnostic, un instant de survenue. Écrites en dur, elles se lisent comme si elles comptaient, et une constante réutilisée dans toute une suite peut faire passer un test pour une mauvaise raison. `Any` fournit à la place une valeur valide mais arbitraire : la seule entrée qui compte ressort, et les autres s’annoncent comme accessoires. +Une grande partie de l’`Arrange` d’un test est d’ordinaire faite de valeurs qu’il ne vérifie jamais — un code d’erreur, un message de diagnostic, un instant de survenue. Écrites en dur, elles se lisent comme si elles comptaient, et une constante réutilisée dans toute une suite peut faire passer un test pour une mauvaise raison. Une valeur *arbitraire* fournit à la place une entrée valide mais accessoire : la seule entrée qui compte ressort, et les autres s’annoncent comme accessoires. -`Any` vit dans **`FirstClassErrors.Testing`** ; il n’ajoute aucune dépendance et, comme les overrides d’horloge et d’identifiants, il est borné, local au contexte et sûr en tests parallèles. Pour figer les valeurs qu’un test *assertit*, voir [Tests d’erreur déterministes](DeterministicTesting.fr.md). +Deux sources couvrent ce besoin, et toutes deux tirent de la même source aléatoire ambiante : + +- **[`Dummies`](https://github.com/Reefact/first-class-errors)** — un générateur fluide de primitives arbitraires (`Dummies.Any.Int32()`, `Dummies.Any.String()`, ...). Un appel `Dummies.Any.*` renvoie une *recette* ; appelez `.Generate()` pour en tirer la valeur. +- Les **fabriques métier** de **`FirstClassErrors.Testing`** — `ErrorCodeFactory.Any()`, `DiagnosticMessageFactory.Any()`, et consorts — pour le vocabulaire d’erreur qu’une primitive brute ne peut pas exprimer. Chacune renvoie directement la valeur. + +Comme les deux passent par la même source, un unique `Dummies.Any.Reproducibly(...)` rend tout un test rejouable ; et — comme les overrides d’horloge et d’identifiants — la source est bornée, locale au contexte et sûre en tests parallèles. Pour figer les valeurs qu’un test *assertit*, voir [Tests d’erreur déterministes](DeterministicTesting.fr.md). ## Fournir une valeur arbitraire @@ -21,60 +26,66 @@ Outcome.Failure(error).ShouldFail().WithCode("ORDER_NOT_FOUND"); ``` ```csharp -// 🙂 Après — le code est le sujet ; les messages sont fournis par Any. +// 🙂 Après — le code est le sujet ; les messages sont arbitraires. DomainError error = DomainError - .Create(ErrorCode.Create("ORDER_NOT_FOUND"), Any.DiagnosticMessage()) - .WithPublicMessage(Any.ShortMessage()); + .Create(ErrorCode.Create("ORDER_NOT_FOUND"), DiagnosticMessageFactory.Any()) + .WithPublicMessage(ShortMessageFactory.Any()); Outcome.Failure(error).ShouldFail().WithCode("ORDER_NOT_FOUND"); ``` -Une valeur n’est accessoire que si elle ne peut pas orienter le code testé. Si elle alimente une branche, une validation, une sérialisation ou un classement, elle façonne le comportement même si le test ne l’assertit jamais — et elle ne peut alors pas être laissée arbitraire sans risque. Réservez `Any` aux entrées que le test transporte mais sur lesquelles il n’agit pas. +Une valeur n’est accessoire que si elle ne peut pas orienter le code testé. Si elle alimente une branche, une validation, une sérialisation ou un classement, elle façonne le comportement même si le test ne l’assertit jamais — et elle ne peut alors pas être laissée arbitraire sans risque. Réservez les valeurs arbitraires aux entrées que le test transporte mais sur lesquelles il n’agit pas. -## Ce que `Any` propose +## Le vocabulaire d’erreur : les fabriques métier -Chaque helper renvoie une valeur **valide pour son type** — des chaînes et messages non vides, un instant UTC réel, un code d’erreur jamais vide : +Pour les parties d’une erreur qu’un test doit fournir sans jamais les asserter, `FirstClassErrors.Testing` livre une fabrique par concept. Chaque `Any()` renvoie une valeur **valide pour son type** — non vide, et reconnaissable comme arbitraire — tirée de la source ambiante : -| Helper | Renvoie | +| Fabrique | Renvoie | | --- | --- | -| `Any.ErrorCode()` | un code valide non vide, de la forme `ANY_CODE_` + 6 caractères alphanumériques majuscules | -| `Any.DiagnosticMessage()` / `Any.ShortMessage()` / `Any.DetailedMessage()` | un message non vide, de longueur bornée | -| `Any.String()` | une chaîne non vide de longueur bornée (`any-` + 8 caractères alphanumériques minuscules) ; sans espace | -| `Any.Guid()` | un `Guid` arbitraire | -| `Any.Instant()` | un instant UTC arbitraire (offset zéro), entre le 1er janvier 2000 et environ 2068 | -| `Any.Int()` | un `int` arbitraire — éventuellement négatif ou nul | -| `Any.Bool()` | `true` ou `false` | -| `Any.Enum()` | un membre quelconque de l’enum — une sentinelle comme `Unknown` comprise | -| `Any.Transience()` / `Any.InteractionDirection()` | une valeur *significative* — jamais la sentinelle `Unknown` | -| `Any.ErrorOrigin()` | un `ErrorOrigin` quelconque ; les trois valeurs sont significatives, il n’y a donc pas de sentinelle à exclure | +| `ErrorCodeFactory.Any()` | un `ErrorCode` valide non vide, de la forme `ANY_CODE_` + 6 caractères alphanumériques majuscules | +| `DiagnosticMessageFactory.Any()` / `ShortMessageFactory.Any()` / `DetailedMessageFactory.Any()` | un message non vide, reconnaissable comme arbitraire | +| `TransienceFactory.Any()` / `InteractionDirectionFactory.Any()` | une valeur *significative* — jamais la sentinelle `Unknown` | +| `ErrorOriginFactory.Any()` | un `ErrorOrigin` quelconque ; toutes ses valeurs sont significatives, il n’y a donc pas de sentinelle à exclure | + +Une fabrique renvoie directement la valeur — le cas courant ne demande aucun `.Generate()`. Utilisez les fabriques d’enum significatif (`TransienceFactory`, `InteractionDirectionFactory`) quand le test a besoin d’une valeur qui déclenche réellement le comportement ; ne recourez à un tirage `Dummies.Any.Enum()` brut que lorsque n’importe quel membre — sentinelle comprise — convient. + +## Les primitives : Dummies + +Pour les primitives arbitraires, utilisez **`Dummies`** directement. Un appel `Dummies.Any.*` renvoie un *générateur* — une recette immuable — et `.Generate()` en tire une valeur : + +```csharp +int quantity = Dummies.Any.Int32().Generate(); +string reference = Dummies.Any.String().NonEmpty().Generate(); +Guid id = Dummies.Any.Guid().Generate(); +``` -Les garanties s’arrêtent à la validité du type. Un helper ne vise aucune précondition métier — `Any.Int()` peut être négatif, `Any.String()` n’est pas un e-mail bien formé — donc un value object au contrat plus strict a besoin de sa propre factory arbitraire, pas d’une primitive brute. +Les contraintes chaînées sur le générateur expriment ce que le code environnant *exige* de la valeur — une longueur, un intervalle, un préfixe — jamais ce que le test assertit. La surface complète des générateurs (contraintes, collections, composition via `As`/`Combine`, `.OrNull()`) est documentée avec `Dummies` lui-même. -Utilisez `Any.Enum()` quand n’importe quel membre convient — sentinelle comprise — et les helpers d’enum nommés quand le test a besoin d’une valeur qui déclenche réellement le comportement concerné. +Les garanties s’arrêtent à la validité du type. Un générateur ne vise aucune précondition métier — `Dummies.Any.Int32()` peut être négatif, `Dummies.Any.String()` n’est pas un e-mail bien formé — donc un value object au contrat plus strict se construit en transformant une primitive sous contrainte : `Dummies.Any.String().StartingWith("ORD-").WithLength(12).As(OrderReference.Create).Generate()`. ## Reproduire une exécution en échec La source n’est pas seedée par défaut : les valeurs diffèrent donc d’une exécution à l’autre. C’est délibéré : un test qui ne passe que pour une valeur particulière dépend de quelque chose qu’il n’énonce pas, et faire varier la valeur révèle ce couplage. -Quand une exécution mérite d’être reproduite, enveloppez le corps du test dans `Any.Reproducibly`. La méthode épingle une graine fraîche pour l’exécution et, si le corps lève une exception, **rapporte cette graine** avant de laisser l’échec se propager — un test rouge te dit ainsi exactement comment le rejouer : +Quand une exécution mérite d’être reproduite, enveloppez le corps du test dans `Dummies.Any.Reproducibly`. La méthode épingle une graine fraîche pour l’exécution et, si le corps lève une exception, **rapporte cette graine** avant de laisser l’échec se propager — un test rouge te dit ainsi exactement comment le rejouer : ```csharp [Fact] public void Some_value_sensitive_test() => - Any.Reproducibly(() => { - // ... arrange avec Any, act, assert ... + Dummies.Any.Reproducibly(() => { + // ... arrange avec les fabriques et Dummies.Any, act, assert ... }); ``` En cas d’échec, la graine est écrite sur `Console.Error` par défaut ; passe le writer de ton framework (par exemple l’`ITestOutputHelper.WriteLine` de xUnit) pour l’y router. Rejoue l’exécution en redonnant la graine rapportée : ```csharp -Any.Reproducibly(1234, () => { +Dummies.Any.Reproducibly(1234, () => { // ... le même corps ... }); ``` -Reproduire une exécution nécessite la **même séquence** d’appels à `Any` : un corps dont l’ordre des tirages dépend d’un état externe non déterministe n’est pas entièrement rejouable à partir de la seule graine. Une surcharge asynchrone, `Any.Reproducibly(Func)`, existe pour les corps de test `async`. +Reproduire une exécution nécessite la **même séquence** de tirages : un corps dont l’ordre dépend d’un état externe non déterministe n’est pas entièrement rejouable à partir de la seule graine. Une surcharge asynchrone, `Dummies.Any.Reproducibly(Func)`, existe pour les corps de test `async`. Comme les fabriques, les primitives et les seams d’horloge et d’identifiants ci-dessous tirent tous de la même source ambiante, un seul `Reproducibly` les rejoue ensemble. ## `OccurredAt` et `InstanceId` arbitraires @@ -82,7 +93,7 @@ Les données d’occurrence sont arbitraires au même sens : un test a souvent b ```csharp DomainError NewError() => - DomainError.Create(Any.ErrorCode(), Any.DiagnosticMessage()).WithPublicMessage(Any.ShortMessage()); + DomainError.Create(ErrorCodeFactory.Any(), DiagnosticMessageFactory.Any()).WithPublicMessage(ShortMessageFactory.Any()); using (Clock.UseAny()) using (InstanceIds.UseAny()) { @@ -94,11 +105,11 @@ using (InstanceIds.UseAny()) { } ``` -Les deux tirent de la même source qu’`Any` : les exécuter à l’intérieur d’un `Any.Reproducibly` rend donc leur instant et leurs identifiants reproductibles eux aussi. Pour épingler un instant ou un identifiant *précis*, utilisez `UseFixed` — voir [Tests d’erreur déterministes](DeterministicTesting.fr.md). +Les deux tirent de la même source ambiante que `Dummies.Any` : les exécuter à l’intérieur d’un `Dummies.Any.Reproducibly` rend donc leur instant et leurs identifiants reproductibles eux aussi. Pour épingler un instant ou un identifiant *précis*, utilisez `UseFixed` — voir [Tests d’erreur déterministes](DeterministicTesting.fr.md). ## Portée et tests parallèles -`Any.Reproducibly`, `Clock.UseAny` et `InstanceIds.UseAny` ne prennent effet que pour l’exécution ou le bloc `using` qu’ils enveloppent, et la source arbitraire est restaurée à leur sortie. Cette source est stockée dans un `AsyncLocal` : elle suit le flux d’exécution du test lui-même et ne fuit jamais dans d’autres tests s’exécutant en même temps. +`Dummies.Any.Reproducibly`, `Clock.UseAny` et `InstanceIds.UseAny` ne prennent effet que pour l’exécution ou le bloc `using` qu’ils enveloppent, et la source arbitraire est restaurée à leur sortie. Cette source est stockée dans un `AsyncLocal` : elle suit le flux d’exécution du test lui-même et ne fuit jamais dans d’autres tests s’exécutant en même temps. ## Checklist de revue @@ -106,8 +117,8 @@ Avant de recourir à une valeur arbitraire, vérifiez que : - la valeur ne **modifie pas** le chemin fonctionnel exercé par le test — elle ne doit alimenter ni une branche, ni une validation, ni une sérialisation, ni un classement, même indirectement ; - la valeur n’est réellement pas vérifiée par le test — sinon utilisez un littéral ; -- un helper d’enum nommé est utilisé quand le test a besoin d’une valeur significative, plutôt que `Any.Enum()` ; -- un test sensible aux valeurs est enveloppé dans `Any.Reproducibly`, pour qu’une exécution en échec rapporte la graine à rejouer ; +- une fabrique d’enum significatif (`TransienceFactory`, `InteractionDirectionFactory`) est utilisée quand le test a besoin d’une valeur significative, plutôt qu’un tirage `Dummies.Any.Enum()` brut ; +- un test sensible aux valeurs est enveloppé dans `Dummies.Any.Reproducibly`, pour qu’une exécution en échec rapporte la graine à rejouer ; - `Clock.UseAny` / `InstanceIds.UseAny` servent pour des données d’occurrence stables mais sans importance, et `UseFixed` lorsque la valeur exacte est assertée. ---