From 8e11bad221e95ca191970c0defc9eeeccb96d2bd Mon Sep 17 00:00:00 2001 From: Theaux Masquelier <43664045+Theauxm@users.noreply.github.com> Date: Mon, 4 May 2026 13:15:24 -0600 Subject: [PATCH 1/3] test: raise Trax.Core line coverage from 86% to 97% Cover Monad null-service throw paths across all 7 AddServices arities, the protected Chain/IChain/Extract/ShortCircuit/AddServices wrappers on Train (reachable only via overridden Junctions()), the MonadTask async wrapper surface, InitializeJunction error paths, ExtractTuple arities 2-7, ExtractLoggerFromLoggerFactory branches, and the Chain/ShortCircuit early-return guards (preset Exception, missing input, multi-ctor junction, unregistered IChain interface). --- .../Extensions/FunctionalAssertTests.cs | 64 +++ .../Extensions/LoggerFactoryProvidersTests.cs | 32 ++ .../MonadExtensionsEdgeCasesTests.cs | 146 ++++++ .../Extensions/MonadExtensionsTupleTests.cs | 108 +++++ .../Extensions/MoqExtensionsExtraTests.cs | 67 +++ .../Train/AddServicesNullParamTests.cs | 440 ++++++++++++++++++ .../Train/ChainShortCircuitEdgeCaseTests.cs | 116 +++++ .../UnitTests/Train/JunctionsApiTests.cs | 291 ++++++++++++ .../UnitTests/Train/MonadConstructorTests.cs | 53 +++ .../UnitTests/Train/MonadTaskTests.cs | 247 ++++++++++ 10 files changed, 1564 insertions(+) create mode 100644 tests/Trax.Core.Tests.Unit/UnitTests/Extensions/FunctionalAssertTests.cs create mode 100644 tests/Trax.Core.Tests.Unit/UnitTests/Extensions/LoggerFactoryProvidersTests.cs create mode 100644 tests/Trax.Core.Tests.Unit/UnitTests/Extensions/MonadExtensionsEdgeCasesTests.cs create mode 100644 tests/Trax.Core.Tests.Unit/UnitTests/Extensions/MonadExtensionsTupleTests.cs create mode 100644 tests/Trax.Core.Tests.Unit/UnitTests/Extensions/MoqExtensionsExtraTests.cs create mode 100644 tests/Trax.Core.Tests.Unit/UnitTests/Train/AddServicesNullParamTests.cs create mode 100644 tests/Trax.Core.Tests.Unit/UnitTests/Train/ChainShortCircuitEdgeCaseTests.cs create mode 100644 tests/Trax.Core.Tests.Unit/UnitTests/Train/JunctionsApiTests.cs create mode 100644 tests/Trax.Core.Tests.Unit/UnitTests/Train/MonadConstructorTests.cs create mode 100644 tests/Trax.Core.Tests.Unit/UnitTests/Train/MonadTaskTests.cs diff --git a/tests/Trax.Core.Tests.Unit/UnitTests/Extensions/FunctionalAssertTests.cs b/tests/Trax.Core.Tests.Unit/UnitTests/Extensions/FunctionalAssertTests.cs new file mode 100644 index 0000000..77299a8 --- /dev/null +++ b/tests/Trax.Core.Tests.Unit/UnitTests/Extensions/FunctionalAssertTests.cs @@ -0,0 +1,64 @@ +using FluentAssertions; +using Trax.Core.Extensions; + +namespace Trax.Core.Tests.Unit.UnitTests.Extensions; + +public class FunctionalAssertTests : TestSetup +{ + [Test] + public void AssertLoaded_NonNull_DoesNotThrow() + { + string? value = "loaded"; + + Action act = () => value.AssertLoaded(); + + act.Should().NotThrow(); + } + + [Test] + public void AssertLoaded_Null_ThrowsWithCallerExpression() + { + string? value = null; + + Action act = () => value.AssertLoaded(); + + act.Should().Throw().WithMessage("*value*has not been loaded*"); + } + + [Test] + public void AssertEachLoaded_AllNonNull_DoesNotThrow() + { + var items = new[] { new Holder("a"), new Holder("b") }; + + Action act = () => items.AssertEachLoaded(x => x.Value); + + act.Should().NotThrow(); + } + + [Test] + public void AssertEachLoaded_OneNull_Throws() + { + var items = new[] { new Holder("a"), new Holder(null) }; + + Action act = () => items.AssertEachLoaded(x => x.Value); + + act.Should().Throw().WithMessage("*has not been loaded*"); + } + + [Test] + public void AssertEachLoaded_EmptyCollection_DoesNotThrow() + { + var items = Array.Empty(); + + Action act = () => items.AssertEachLoaded(x => x.Value); + + act.Should().NotThrow(); + } + + private class Holder + { + public Holder(string? value) => Value = value; + + public string? Value { get; } + } +} diff --git a/tests/Trax.Core.Tests.Unit/UnitTests/Extensions/LoggerFactoryProvidersTests.cs b/tests/Trax.Core.Tests.Unit/UnitTests/Extensions/LoggerFactoryProvidersTests.cs new file mode 100644 index 0000000..572b429 --- /dev/null +++ b/tests/Trax.Core.Tests.Unit/UnitTests/Extensions/LoggerFactoryProvidersTests.cs @@ -0,0 +1,32 @@ +using FluentAssertions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Trax.Core.Extensions; + +namespace Trax.Core.Tests.Unit.UnitTests.Extensions; + +public class LoggerFactoryProvidersTests : TestSetup +{ + [Test] + public void GetLoggerProviders_FactoryWithNoProviders_ReturnsEmpty() + { + var factory = NullLoggerFactory.Instance; + + var providers = factory.GetLoggerProviders(); + + providers.Should().NotBeNull(); + providers.Should().BeEmpty(); + } + + [Test] + public void GetLoggerProviders_RealFactoryWithProvider_DoesNotThrow() + { + var factory = LoggerFactory.Create(b => b.AddProvider(NullLoggerProvider.Instance)); + + var providers = factory.GetLoggerProviders(); + + // The reflection-based reader may return [] for built-in factory shapes that + // changed between framework versions; what we care about is no exception. + providers.Should().NotBeNull(); + } +} diff --git a/tests/Trax.Core.Tests.Unit/UnitTests/Extensions/MonadExtensionsEdgeCasesTests.cs b/tests/Trax.Core.Tests.Unit/UnitTests/Extensions/MonadExtensionsEdgeCasesTests.cs new file mode 100644 index 0000000..efa1c53 --- /dev/null +++ b/tests/Trax.Core.Tests.Unit/UnitTests/Extensions/MonadExtensionsEdgeCasesTests.cs @@ -0,0 +1,146 @@ +using FluentAssertions; +using LanguageExt; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Trax.Core.Exceptions; +using Trax.Core.Extensions; +using Trax.Core.Train; + +namespace Trax.Core.Tests.Unit.UnitTests.Extensions; + +public class MonadExtensionsEdgeCasesTests : TestSetup +{ + [Test] + public void InitializeJunction_NonClass_RecordsTrainException() + { + var monad = new TestTrain().Activate(0); + + var result = monad.InitializeJunction(); + + result.Should().BeNull(); + monad.Exception.Should().BeOfType(); + monad.Exception!.Message.Should().Contain("must be a class"); + } + + [Test] + public void InitializeJunction_MultipleConstructors_RecordsTrainException() + { + var monad = new TestTrain().Activate(0); + + var result = monad.InitializeJunction(); + + result.Should().BeNull(); + monad.Exception.Should().BeOfType(); + monad.Exception!.Message.Should().Contain("single constructor"); + } + + [Test] + public void InitializeJunction_MissingDependencyInMemory_RecordsExceptionViaExtract() + { + var monad = new TestTrain().Activate(0); + + var result = monad.InitializeJunction(); + + result.Should().BeNull(); + monad.Exception.Should().NotBeNull(); + } + + [Test] + public void ExtractLoggerFromLoggerFactory_NonGenericILogger_ReturnsNull() + { + var monad = new TestTrain().Activate(0); + + var result = monad.ExtractLoggerFromLoggerFactory(typeof(string)); + + ((object?)result).Should().BeNull(); + } + + [Test] + public void ExtractLoggerFromLoggerFactory_NoLoggerFactoryInMemory_Throws() + { + var monad = new TestTrain().Activate(0); + + Action act = () => + monad.ExtractLoggerFromLoggerFactory(typeof(ILogger)); + + act.Should().Throw().WithMessage("*ILoggerFactory*"); + } + + [Test] + public void ExtractLoggerFromLoggerFactory_CreatesGenericLogger_FromFactory() + { + var factory = NullLoggerFactory.Instance; + var monad = new TestTrain().Activate(0).AddServices(factory); + + var logger = monad.ExtractLoggerFromLoggerFactory( + typeof(ILogger) + ); + + ((object?)logger).Should().NotBeNull(); + } + + [Test] + public void ExtractTuple_TwoElements_ReturnsValueTuple() + { + var monad = new TestTrain().Activate(0); + monad.AddServices(new Foo()); + monad.AddServices(new Bar()); + + var tuple = monad.ExtractTuple(typeof(ValueTuple)); + + ((object)tuple).Should().NotBeNull(); + } + + [Test] + public void ExtractTuple_OneElement_Throws() + { + var monad = new TestTrain().Activate(0); + monad.AddServices(new Foo()); + + Action act = () => monad.ExtractTuple(typeof(ValueTuple)); + + act.Should().Throw().WithMessage("*single length*"); + } + + [Test] + public void ExtractTuple_TooManyElements_Throws() + { + // ValueTuple of 8 has TRest, ExtractTypeTuples would not fill it sensibly, + // but we exercise the default switch arm by passing a non-tuple-shaped type + // that yields zero matched fields. Use Object which has no fields/properties. + var monad = new TestTrain().Activate(0); + + Action act = () => monad.ExtractTuple(typeof(object)); + + // ExtractTypeTuples returns 0 matched types for object → "Tuple of length 0" + act.Should().Throw(); + } + + private interface INotAClass { } + + private interface IFoo { } + + private interface IBar { } + + private class Foo : IFoo { } + + private class Bar : IBar { } + + private class MultiCtorJunction + { + public MultiCtorJunction() { } + + public MultiCtorJunction(int x) { } + } + + private class NeedsDependencyJunction + { + public NeedsDependencyJunction(IFoo foo) { } + } + + private class TestTrain : Train + { + protected override Task> RunInternal(int input) => + throw new NotImplementedException(); + } +} diff --git a/tests/Trax.Core.Tests.Unit/UnitTests/Extensions/MonadExtensionsTupleTests.cs b/tests/Trax.Core.Tests.Unit/UnitTests/Extensions/MonadExtensionsTupleTests.cs new file mode 100644 index 0000000..e4a1638 --- /dev/null +++ b/tests/Trax.Core.Tests.Unit/UnitTests/Extensions/MonadExtensionsTupleTests.cs @@ -0,0 +1,108 @@ +using FluentAssertions; +using LanguageExt; +using Trax.Core.Exceptions; +using Trax.Core.Extensions; +using Trax.Core.Train; + +namespace Trax.Core.Tests.Unit.UnitTests.Extensions; + +public class MonadExtensionsTupleTests : TestSetup +{ + [Test] + public void ExtractTuple_FourElements_ReturnsValueTuple() + { + var monad = NewActivated(); + monad.AddServices(new A()); + monad.AddServices(new B()); + monad.AddServices(new C()); + monad.AddServices(new D()); + + var tuple = monad.ExtractTuple(typeof(ValueTuple)); + + ((object)tuple).Should().NotBeNull(); + } + + [Test] + public void ExtractTuple_FiveElements_ReturnsValueTuple() + { + var monad = NewActivated(); + monad.AddServices(new A()); + monad.AddServices(new B()); + monad.AddServices(new C()); + monad.AddServices(new D()); + monad.AddServices(new E()); + + var tuple = monad.ExtractTuple(typeof(ValueTuple)); + + ((object)tuple).Should().NotBeNull(); + } + + [Test] + public void ExtractTuple_SixElements_ReturnsValueTuple() + { + var monad = NewActivated(); + monad.AddServices(new A()); + monad.AddServices(new B()); + monad.AddServices(new C()); + monad.AddServices(new D()); + monad.AddServices(new E()); + monad.AddServices(new F()); + + var tuple = monad.ExtractTuple(typeof(ValueTuple)); + + ((object)tuple).Should().NotBeNull(); + } + + [Test] + public void ExtractTuple_SevenElements_ReturnsValueTuple() + { + var monad = NewActivated(); + monad.AddServices(new A()); + monad.AddServices(new B()); + monad.AddServices(new C()); + monad.AddServices(new D()); + monad.AddServices(new E()); + monad.AddServices(new F()); + monad.AddServices(new G()); + + var tuple = monad.ExtractTuple(typeof(ValueTuple)); + + ((object)tuple).Should().NotBeNull(); + } + + private static Trax.Core.Monad.Monad NewActivated() => new TestTrain().Activate(0); + + private interface IA { } + + private interface IB { } + + private interface IC { } + + private interface ID { } + + private interface IE { } + + private interface IF { } + + private interface IG { } + + private class A : IA { } + + private class B : IB { } + + private class C : IC { } + + private class D : ID { } + + private class E : IE { } + + private class F : IF { } + + private class G : IG { } + + private class TestTrain : Train + { + protected override Task> RunInternal(int input) => + throw new NotImplementedException(); + } +} diff --git a/tests/Trax.Core.Tests.Unit/UnitTests/Extensions/MoqExtensionsExtraTests.cs b/tests/Trax.Core.Tests.Unit/UnitTests/Extensions/MoqExtensionsExtraTests.cs new file mode 100644 index 0000000..44f1d0d --- /dev/null +++ b/tests/Trax.Core.Tests.Unit/UnitTests/Extensions/MoqExtensionsExtraTests.cs @@ -0,0 +1,67 @@ +using FluentAssertions; +using Trax.Core.Extensions; + +namespace Trax.Core.Tests.Unit.UnitTests.Extensions; + +public class MoqExtensionsExtraTests : TestSetup +{ + [Test] + public void GetMockedTypeFromObject_NoMockProperty_ReturnsNull() + { + var ordinaryObject = new PlainClass(); + + var result = ordinaryObject.GetMockedTypeFromObject(); + + ((object?)result).Should().BeNull(); + } + + [Test] + public void GetMockedTypeFromObject_MockPropertyReturnsNull_ReturnsNull() + { + var obj = new HasNullMockProperty(); + + var result = obj.GetMockedTypeFromObject(); + + ((object?)result).Should().BeNull(); + } + + [Test] + public void GetMockedTypeFromObject_MockHasNoMockedTypeProperty_ReturnsNull() + { + var obj = new HasMockWithoutMockedType(); + + var result = obj.GetMockedTypeFromObject(); + + ((object?)result).Should().BeNull(); + } + + [Test] + public void GetMockedTypeFromObject_NullArgument_Throws() + { + object obj = null!; + + Action act = () => obj.GetMockedTypeFromObject(); + + act.Should().Throw(); + } + + [Test] + public void IsMoqProxy_NonMoqType_ReturnsFalse() + { + var type = typeof(string); + + type.IsMoqProxy().Should().BeFalse(); + } + + private class PlainClass { } + + private class HasNullMockProperty + { + public object? Mock => null; + } + + private class HasMockWithoutMockedType + { + public object Mock { get; } = new object(); + } +} diff --git a/tests/Trax.Core.Tests.Unit/UnitTests/Train/AddServicesNullParamTests.cs b/tests/Trax.Core.Tests.Unit/UnitTests/Train/AddServicesNullParamTests.cs new file mode 100644 index 0000000..d99f821 --- /dev/null +++ b/tests/Trax.Core.Tests.Unit/UnitTests/Train/AddServicesNullParamTests.cs @@ -0,0 +1,440 @@ +using FluentAssertions; +using LanguageExt; +using Trax.Core.Train; + +namespace Trax.Core.Tests.Unit.UnitTests.Train; + +public class AddServicesNullParamTests : TestSetup +{ + [Test] + public void AddServices_T1_NullService_Throws() + { + var monad = new TestTrain().Activate(0); + + Action act = () => monad.AddServices(null!); + + act.Should().Throw().WithMessage("*cannot be null*"); + } + + [Test] + public void AddServices_T2_NullSecond_Throws() + { + var monad = new TestTrain().Activate(0); + + Action act = () => + monad.AddServices(new TestService1(), null!); + + act.Should().Throw().WithMessage("*cannot be null*"); + } + + [Test] + public void AddServices_T3_NullThird_Throws() + { + var monad = new TestTrain().Activate(0); + + Action act = () => + monad.AddServices( + new TestService1(), + new TestService2(), + null! + ); + + act.Should().Throw().WithMessage("*cannot be null*"); + } + + [Test] + public void AddServices_T4_NullFourth_Throws() + { + var monad = new TestTrain().Activate(0); + + Action act = () => + monad.AddServices( + new TestService1(), + new TestService2(), + new TestService3(), + null! + ); + + act.Should().Throw().WithMessage("*cannot be null*"); + } + + [Test] + public void AddServices_T5_NullFifth_Throws() + { + var monad = new TestTrain().Activate(0); + + Action act = () => + monad.AddServices< + ITestService1, + ITestService2, + ITestService3, + ITestService4, + ITestService5 + >( + new TestService1(), + new TestService2(), + new TestService3(), + new TestService4(), + null! + ); + + act.Should().Throw().WithMessage("*cannot be null*"); + } + + [Test] + public void AddServices_T6_NullSixth_Throws() + { + var monad = new TestTrain().Activate(0); + + Action act = () => + monad.AddServices< + ITestService1, + ITestService2, + ITestService3, + ITestService4, + ITestService5, + ITestService6 + >( + new TestService1(), + new TestService2(), + new TestService3(), + new TestService4(), + new TestService5(), + null! + ); + + act.Should().Throw().WithMessage("*cannot be null*"); + } + + [Test] + public void AddServices_T7_NullSeventh_Throws() + { + var monad = new TestTrain().Activate(0); + + Action act = () => + monad.AddServices< + ITestService1, + ITestService2, + ITestService3, + ITestService4, + ITestService5, + ITestService6, + ITestService7 + >( + new TestService1(), + new TestService2(), + new TestService3(), + new TestService4(), + new TestService5(), + new TestService6(), + null! + ); + + act.Should().Throw().WithMessage("*cannot be null*"); + } + + [Test] + public void AddServices_T2_NullFirst_Throws() + { + var monad = new TestTrain().Activate(0); + + Action act = () => + monad.AddServices(null!, new TestService2()); + + act.Should().Throw().WithMessage("*cannot be null*"); + } + + [Test] + public void AddServices_T3_NullFirst_Throws() + { + var monad = new TestTrain().Activate(0); + + Action act = () => + monad.AddServices( + null!, + new TestService2(), + new TestService3() + ); + + act.Should().Throw().WithMessage("*cannot be null*"); + } + + [Test] + public void AddServices_T3_NullMiddle_Throws() + { + var monad = new TestTrain().Activate(0); + + Action act = () => + monad.AddServices( + new TestService1(), + null!, + new TestService3() + ); + + act.Should().Throw().WithMessage("*cannot be null*"); + } + + [Test] + public void AddServices_T4_NullEachPosition_Throws() + { + var monad = new TestTrain().Activate(0); + var s1 = new TestService1(); + var s2 = new TestService2(); + var s3 = new TestService3(); + var s4 = new TestService4(); + + Action a1 = () => + monad.AddServices( + null!, + s2, + s3, + s4 + ); + Action a2 = () => + monad.AddServices( + s1, + null!, + s3, + s4 + ); + Action a3 = () => + monad.AddServices( + s1, + s2, + null!, + s4 + ); + + a1.Should().Throw(); + a2.Should().Throw(); + a3.Should().Throw(); + } + + [Test] + public void AddServices_T5_NullEachPosition_Throws() + { + var monad = new TestTrain().Activate(0); + var s1 = new TestService1(); + var s2 = new TestService2(); + var s3 = new TestService3(); + var s4 = new TestService4(); + var s5 = new TestService5(); + + Action a1 = () => + monad.AddServices< + ITestService1, + ITestService2, + ITestService3, + ITestService4, + ITestService5 + >(null!, s2, s3, s4, s5); + Action a2 = () => + monad.AddServices< + ITestService1, + ITestService2, + ITestService3, + ITestService4, + ITestService5 + >(s1, null!, s3, s4, s5); + Action a3 = () => + monad.AddServices< + ITestService1, + ITestService2, + ITestService3, + ITestService4, + ITestService5 + >(s1, s2, null!, s4, s5); + Action a4 = () => + monad.AddServices< + ITestService1, + ITestService2, + ITestService3, + ITestService4, + ITestService5 + >(s1, s2, s3, null!, s5); + + a1.Should().Throw(); + a2.Should().Throw(); + a3.Should().Throw(); + a4.Should().Throw(); + } + + [Test] + public void AddServices_T6_NullEachPosition_Throws() + { + var monad = new TestTrain().Activate(0); + var s1 = new TestService1(); + var s2 = new TestService2(); + var s3 = new TestService3(); + var s4 = new TestService4(); + var s5 = new TestService5(); + var s6 = new TestService6(); + + Action a1 = () => + monad.AddServices< + ITestService1, + ITestService2, + ITestService3, + ITestService4, + ITestService5, + ITestService6 + >(null!, s2, s3, s4, s5, s6); + Action a2 = () => + monad.AddServices< + ITestService1, + ITestService2, + ITestService3, + ITestService4, + ITestService5, + ITestService6 + >(s1, null!, s3, s4, s5, s6); + Action a3 = () => + monad.AddServices< + ITestService1, + ITestService2, + ITestService3, + ITestService4, + ITestService5, + ITestService6 + >(s1, s2, null!, s4, s5, s6); + Action a4 = () => + monad.AddServices< + ITestService1, + ITestService2, + ITestService3, + ITestService4, + ITestService5, + ITestService6 + >(s1, s2, s3, null!, s5, s6); + Action a5 = () => + monad.AddServices< + ITestService1, + ITestService2, + ITestService3, + ITestService4, + ITestService5, + ITestService6 + >(s1, s2, s3, s4, null!, s6); + + a1.Should().Throw(); + a2.Should().Throw(); + a3.Should().Throw(); + a4.Should().Throw(); + a5.Should().Throw(); + } + + [Test] + public void AddServices_T7_NullEachPosition_Throws() + { + var monad = new TestTrain().Activate(0); + var s1 = new TestService1(); + var s2 = new TestService2(); + var s3 = new TestService3(); + var s4 = new TestService4(); + var s5 = new TestService5(); + var s6 = new TestService6(); + var s7 = new TestService7(); + + Action a1 = () => + monad.AddServices< + ITestService1, + ITestService2, + ITestService3, + ITestService4, + ITestService5, + ITestService6, + ITestService7 + >(null!, s2, s3, s4, s5, s6, s7); + Action a2 = () => + monad.AddServices< + ITestService1, + ITestService2, + ITestService3, + ITestService4, + ITestService5, + ITestService6, + ITestService7 + >(s1, null!, s3, s4, s5, s6, s7); + Action a3 = () => + monad.AddServices< + ITestService1, + ITestService2, + ITestService3, + ITestService4, + ITestService5, + ITestService6, + ITestService7 + >(s1, s2, null!, s4, s5, s6, s7); + Action a4 = () => + monad.AddServices< + ITestService1, + ITestService2, + ITestService3, + ITestService4, + ITestService5, + ITestService6, + ITestService7 + >(s1, s2, s3, null!, s5, s6, s7); + Action a5 = () => + monad.AddServices< + ITestService1, + ITestService2, + ITestService3, + ITestService4, + ITestService5, + ITestService6, + ITestService7 + >(s1, s2, s3, s4, null!, s6, s7); + Action a6 = () => + monad.AddServices< + ITestService1, + ITestService2, + ITestService3, + ITestService4, + ITestService5, + ITestService6, + ITestService7 + >(s1, s2, s3, s4, s5, null!, s7); + + a1.Should().Throw(); + a2.Should().Throw(); + a3.Should().Throw(); + a4.Should().Throw(); + a5.Should().Throw(); + a6.Should().Throw(); + } + + private interface ITestService1 { } + + private interface ITestService2 { } + + private interface ITestService3 { } + + private interface ITestService4 { } + + private interface ITestService5 { } + + private interface ITestService6 { } + + private interface ITestService7 { } + + private class TestService1 : ITestService1 { } + + private class TestService2 : ITestService2 { } + + private class TestService3 : ITestService3 { } + + private class TestService4 : ITestService4 { } + + private class TestService5 : ITestService5 { } + + private class TestService6 : ITestService6 { } + + private class TestService7 : ITestService7 { } + + private class TestTrain : Train + { + protected override Task> RunInternal(int input) => + throw new NotImplementedException(); + } +} diff --git a/tests/Trax.Core.Tests.Unit/UnitTests/Train/ChainShortCircuitEdgeCaseTests.cs b/tests/Trax.Core.Tests.Unit/UnitTests/Train/ChainShortCircuitEdgeCaseTests.cs new file mode 100644 index 0000000..505352a --- /dev/null +++ b/tests/Trax.Core.Tests.Unit/UnitTests/Train/ChainShortCircuitEdgeCaseTests.cs @@ -0,0 +1,116 @@ +using FluentAssertions; +using LanguageExt; +using LanguageExt.UnsafeValueAccess; +using Trax.Core.Exceptions; +using Trax.Core.Junction; +using Trax.Core.Monad; +using Trax.Core.Train; + +namespace Trax.Core.Tests.Unit.UnitTests.Train; + +public class ChainShortCircuitEdgeCaseTests : TestSetup +{ + [Test] + public async Task ChainJunction_ExceptionAlreadySet_ReturnsEarly() + { + var monad = new TestTrain().Activate(0); + monad.Exception = new TrainException("preset"); + + var (returned, result) = await monad.ChainJunction( + new EchoJunction(), + "x" + ); + + returned.Should().BeSameAs(monad); + result.IsLeft.Should().BeTrue(); + } + + [Test] + public async Task ChainJunctionFromMemory_InputMissing_ReturnsEarlyWithException() + { + var monad = new TestTrain().Activate(0); + + var returned = await monad.ChainJunction(new EchoJunction()); + + returned.Should().BeSameAs(monad); + monad.Exception.Should().NotBeNull(); + } + + [Test] + public async Task ChainAsync_ExceptionAlreadySet_DoesNotRunJunction() + { + var monad = new TestTrain().Activate(0); + monad.Exception = new TrainException("preset"); + + var either = await monad.Chain().Resolve(); + + either.IsLeft.Should().BeTrue(); + either.Swap().ValueUnsafe().Message.Should().Be("preset"); + } + + [Test] + public async Task ChainAsync_InitializeJunctionReturnsNull_ReturnsEarly() + { + var monad = new TestTrain().Activate(0); + + // MultiCtorJunction has multiple constructors → InitializeJunction returns null. + var either = await monad.Chain().Resolve(); + + either.IsLeft.Should().BeTrue(); + } + + [Test] + public async Task IChainAsync_InterfaceNotInMemory_ReturnsEarly() + { + var monad = new TestTrain().Activate(0); + + var either = await monad.IChain().Resolve(); + + either.IsLeft.Should().BeTrue(); + } + + [Test] + public async Task ShortCircuitJunction_ExceptionAlreadySet_ReturnsEarly() + { + var monad = new TestTrain().Activate(0); + monad.Exception = new TrainException("preset"); + + var (returned, result) = await monad.ShortCircuitJunction( + new EchoJunction(), + "x" + ); + + returned.Should().BeSameAs(monad); + result.IsLeft.Should().BeTrue(); + } + + [Test] + public async Task ShortCircuitAsync_InitializeJunctionReturnsNull_ReturnsEarly() + { + var monad = new TestTrain().Activate(0); + + var either = await monad.ShortCircuit().Resolve(); + + either.IsLeft.Should().BeTrue(); + } + + private interface IUnregistered : IJunction { } + + private class EchoJunction : Junction + { + public override Task Run(string input) => Task.FromResult(input); + } + + private class MultiCtorJunction + { + public MultiCtorJunction() { } + + public MultiCtorJunction(int x) { } + } + + private class TestTrain : Train + { + protected override Task> RunInternal(int input) => + throw new NotImplementedException(); + } +} diff --git a/tests/Trax.Core.Tests.Unit/UnitTests/Train/JunctionsApiTests.cs b/tests/Trax.Core.Tests.Unit/UnitTests/Train/JunctionsApiTests.cs new file mode 100644 index 0000000..245d462 --- /dev/null +++ b/tests/Trax.Core.Tests.Unit/UnitTests/Train/JunctionsApiTests.cs @@ -0,0 +1,291 @@ +using FluentAssertions; +using LanguageExt; +using LanguageExt.UnsafeValueAccess; +using Trax.Core.Junction; +using Trax.Core.Train; + +namespace Trax.Core.Tests.Unit.UnitTests.Train; + +/// +/// Exercises the protected Chain/IChain/ShortCircuit/Extract/AddServices wrappers on Train +/// that are reachable only by overriding Junctions() and using the bare protected methods +/// (which forward to the internal Monad set up by the default RunInternal). +/// +public class JunctionsApiTests : TestSetup +{ + [Test] + public async Task Junctions_DefaultBaseImpl_ThrowsNotImplemented() + { + var train = new EmptyJunctionsTrain(); + + var either = await train.RunEither("x"); + + either.IsLeft.Should().BeTrue(); + either.Swap().ValueUnsafe().Should().BeOfType(); + } + + [Test] + public async Task RunInternal_JunctionsThrows_CapturesIntoLeft() + { + var train = new ThrowingJunctionsTrain(); + + var either = await train.RunEither("x"); + + either.IsLeft.Should().BeTrue(); + either.Swap().ValueUnsafe().Message.Should().Be("boom"); + } + + [Test] + public async Task Junctions_ProtectedChainTJunctionInstance_RunsViaTrainWrapper() + { + var train = new ProtectedChainInstanceTrain(); + + var result = await train.Run("hi"); + + result.Should().Be("hi-done"); + } + + [Test] + public async Task Junctions_ProtectedChainTJunctionTypeOnly_RunsViaTrainWrapper() + { + var train = new ProtectedChainTypeOnlyTrain(); + + var result = await train.Run("hi"); + + result.Should().Be("hi-done"); + } + + [Test] + public async Task Junctions_ProtectedIChain_RunsRegisteredInterfaceJunction() + { + var train = new ProtectedIChainTrain(); + + var result = await train.Run("hi"); + + result.Should().Be("hi-done"); + } + + [Test] + public async Task Junctions_ProtectedExtract_PullsFieldFromMemory() + { + var train = new ProtectedExtractFromMemoryTrain(); + + var result = await train.Run("hi"); + + result.Should().Be("payload-extracted"); + } + + [Test] + public async Task Junctions_ProtectedExtractWithInput_PullsFieldFromInput() + { + var train = new ProtectedExtractFromInputTrain(); + + var result = await train.Run("hi"); + + result.Should().Be("payload-extracted"); + } + + [Test] + public async Task Junctions_ProtectedShortCircuitTypeOnly_EndsChainEarly() + { + var train = new ProtectedShortCircuitTypeOnlyTrain(); + + var result = await train.Run("hi"); + + result.Should().Be("short-circuited"); + } + + [Test] + public async Task Junctions_ProtectedShortCircuitInstance_EndsChainEarly() + { + var train = new ProtectedShortCircuitInstanceTrain(); + + var result = await train.Run("hi"); + + result.Should().Be("short-circuited"); + } + + [Test] + public async Task Junctions_ProtectedAddServicesT1ThroughT7_AreReachableFromTrain() + { + var train = new ProtectedAddServicesTrain(); + + var result = await train.Run("hi"); + + result.Should().Be("seven"); + } + + #region Test fixtures + + private interface IS1 { } + + private interface IS2 { } + + private interface IS3 { } + + private interface IS4 { } + + private interface IS5 { } + + private interface IS6 { } + + private interface IS7 { } + + private class S1 : IS1 { } + + private class S2 : IS2 { } + + private class S3 : IS3 { } + + private class S4 : IS4 { } + + private class S5 : IS5 { } + + private class S6 : IS6 { } + + private class S7 : IS7 { } + + private class DoneJunction : Junction + { + public override Task Run(string input) => Task.FromResult($"{input}-done"); + } + + private interface IDoneJunction : IJunction { } + + private class IDoneJunctionImpl : Junction, IDoneJunction + { + public override Task Run(string input) => Task.FromResult($"{input}-done"); + } + + private class HasInner + { + public string Inner { get; set; } = "payload"; + } + + private class ProduceHasInnerJunction : Junction + { + public override Task Run(string input) => Task.FromResult(new HasInner()); + } + + private class ExtractedJunction : Junction + { + public override Task Run(string input) => Task.FromResult($"{input}-extracted"); + } + + private class ShortCircuitJunctionShort : Junction + { + public override Task Run(string input) => Task.FromResult("short-circuited"); + } + + private class ConsumeAllServicesJunction : Junction + { + public ConsumeAllServicesJunction(IS1 s1, IS2 s2, IS3 s3, IS4 s4, IS5 s5, IS6 s6, IS7 s7) + { } + + public override Task Run(string input) => Task.FromResult("seven"); + } + + private class EmptyJunctionsTrain : Train { } + + private class ThrowingJunctionsTrain : Train + { + protected override Task> Junctions() => + throw new InvalidOperationException("boom"); + } + + private class ProtectedChainInstanceTrain : Train + { + protected override async Task> Junctions() => + await Chain(new DoneJunction()).Resolve(); + } + + private class ProtectedChainTypeOnlyTrain : Train + { + protected override async Task> Junctions() => + await Chain().Resolve(); + } + + private class ProtectedIChainTrain : Train + { + protected override async Task> Junctions() + { + AddServices(new IDoneJunctionImpl()); + return await IChain().Resolve(); + } + } + + private class ProtectedExtractFromMemoryTrain : Train + { + protected override async Task> Junctions() + { + await Chain(new ProduceHasInnerJunction()); + Extract(); + return await Chain(new ExtractedJunction()).Resolve(); + } + } + + private class ProtectedExtractFromInputTrain : Train + { + protected override async Task> Junctions() + { + Extract(new HasInner()); + return await Chain(new ExtractedJunction()).Resolve(); + } + } + + private class ProtectedShortCircuitTypeOnlyTrain : Train + { + protected override async Task> Junctions() => + await ShortCircuit().Chain(new DoneJunction()).Resolve(); + } + + private class ProtectedShortCircuitInstanceTrain : Train + { + protected override async Task> Junctions() => + await ShortCircuit(new ShortCircuitJunctionShort()).Chain(new DoneJunction()).Resolve(); + } + + private class ProtectedAddServicesTrain : Train + { + protected override async Task> Junctions() + { + AddServices(new S1()); + AddServices(new S1(), new S2()); + AddServices(new S1(), new S2(), new S3()); + AddServices(new S1(), new S2(), new S3(), new S4()); + AddServices(new S1(), new S2(), new S3(), new S4(), new S5()); + AddServices( + new S1(), + new S2(), + new S3(), + new S4(), + new S5(), + new S6() + ); + AddServices( + new S1(), + new S2(), + new S3(), + new S4(), + new S5(), + new S6(), + new S7() + ); + + return await Chain( + new ConsumeAllServicesJunction( + new S1(), + new S2(), + new S3(), + new S4(), + new S5(), + new S6(), + new S7() + ) + ) + .Resolve(); + } + } + + #endregion +} diff --git a/tests/Trax.Core.Tests.Unit/UnitTests/Train/MonadConstructorTests.cs b/tests/Trax.Core.Tests.Unit/UnitTests/Train/MonadConstructorTests.cs new file mode 100644 index 0000000..f45585b --- /dev/null +++ b/tests/Trax.Core.Tests.Unit/UnitTests/Train/MonadConstructorTests.cs @@ -0,0 +1,53 @@ +using FluentAssertions; +using LanguageExt; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Trax.Core.Extensions; +using Trax.Core.Monad; +using Trax.Core.Train; + +namespace Trax.Core.Tests.Unit.UnitTests.Train; + +public class MonadConstructorTests : TestSetup +{ + [Test] + public void Constructor_WithServiceProvider_StoresProviderInMemory() + { + var train = new TestTrain(); + var sp = new ServiceCollection().BuildServiceProvider(); + + var monad = new Monad(train, sp, CancellationToken.None); + + monad.Memory.Should().ContainKey(typeof(IServiceProvider)); + monad.Memory[typeof(IServiceProvider)].Should().BeSameAs(sp); + } + + [Test] + public void Constructor_WithoutServiceProvider_DoesNotContainProvider() + { + var train = new TestTrain(); + + var monad = new Monad(train, CancellationToken.None); + + monad.Memory.Should().NotContainKey(typeof(IServiceProvider)); + } + + [Test] + public void AddServices_MoqMockObject_StoresByMockedInterfaceType() + { + var train = new TestTrain(); + var monad = new Monad(train, CancellationToken.None).Activate(0); + var mock = new Mock(); + + monad.AddServices(mock.Object); + + monad.Memory.Should().ContainKey(typeof(IDisposable)); + monad.Memory[typeof(IDisposable)].Should().BeSameAs(mock.Object); + } + + private class TestTrain : Train + { + protected override Task> RunInternal(int input) => + throw new NotImplementedException(); + } +} diff --git a/tests/Trax.Core.Tests.Unit/UnitTests/Train/MonadTaskTests.cs b/tests/Trax.Core.Tests.Unit/UnitTests/Train/MonadTaskTests.cs new file mode 100644 index 0000000..65560db --- /dev/null +++ b/tests/Trax.Core.Tests.Unit/UnitTests/Train/MonadTaskTests.cs @@ -0,0 +1,247 @@ +using FluentAssertions; +using LanguageExt; +using Trax.Core.Junction; +using Trax.Core.Monad; +using Trax.Core.Train; + +namespace Trax.Core.Tests.Unit.UnitTests.Train; + +public class MonadTaskTests : TestSetup +{ + [Test] + public void MonadTask_ImplicitConversion_YieldsSourceTask() + { + var monad = new TestTrain().Activate("hi"); + var mt = Task.FromResult(monad).AsMonadTask(); + + Task> task = mt; + + task.Should().BeSameAs(mt.AsTask()); + } + + [Test] + public async Task MonadTask_AsTask_ReturnsAwaitableSource() + { + var monad = new TestTrain().Activate("hi"); + var mt = Task.FromResult(monad).AsMonadTask(); + + var resolved = await mt.AsTask(); + + resolved.Should().BeSameAs(monad); + } + + [Test] + public async Task MonadTask_ConfigureAwait_ReturnsAwaitable() + { + var monad = new TestTrain().Activate("hi"); + var mt = Task.FromResult(monad).AsMonadTask(); + + var resolved = await mt.ConfigureAwait(false); + + resolved.Should().BeSameAs(monad); + } + + [Test] + public async Task MonadTask_ChainExplicit_RunsJunction() + { + var monad = new TestTrain().Activate("hi"); + + var result = await Task.FromResult(monad) + .AsMonadTask() + .Chain(new DoneJunction()) + .Resolve(); + + result.IsRight.Should().BeTrue(); + } + + [Test] + public async Task MonadTask_ChainExplicitWithDefaultCtor_RunsJunction() + { + var monad = new TestTrain().Activate("hi"); + + var result = await Task.FromResult(monad) + .AsMonadTask() + .Chain() + .Resolve(); + + result.IsRight.Should().BeTrue(); + } + + [Test] + public async Task MonadTask_ChainUnit_RunsJunction() + { + var monad = new TestTrain().Activate("hi"); + + var result = await Task.FromResult(monad) + .AsMonadTask() + .Chain(new UnitJunction()) + .Resolve(Either.Right("done")); + + result.IsRight.Should().BeTrue(); + } + + [Test] + public async Task MonadTask_ChainUnitWithDefaultCtor_RunsJunction() + { + var monad = new TestTrain().Activate("hi"); + + var result = await Task.FromResult(monad) + .AsMonadTask() + .Chain() + .Resolve(Either.Right("done")); + + result.IsRight.Should().BeTrue(); + } + + [Test] + public async Task MonadTask_ShortCircuitTypeOnly_EndsChainEarly() + { + var monad = new TestTrain().Activate("hi"); + + var result = await Task.FromResult(monad) + .AsMonadTask() + .ShortCircuit() + .Resolve(); + + result.IsRight.Should().BeTrue(); + } + + [Test] + public async Task MonadTask_ShortCircuitInstance_EndsChainEarly() + { + var monad = new TestTrain().Activate("hi"); + + var result = await Task.FromResult(monad) + .AsMonadTask() + .ShortCircuit(new ShortCircuitJunction()) + .Resolve(); + + result.IsRight.Should().BeTrue(); + } + + [Test] + public async Task MonadTask_ExtractFromMemory_PullsField() + { + var monad = new TestTrain().Activate("hi", new HasInner()); + + var result = await Task.FromResult(monad) + .AsMonadTask() + .Extract() + .Resolve(Either.Right("ok")); + + result.IsRight.Should().BeTrue(); + } + + [Test] + public async Task MonadTask_ExtractFromInput_PullsField() + { + var monad = new TestTrain().Activate("hi"); + + var result = await Task.FromResult(monad) + .AsMonadTask() + .Extract(new HasInner()) + .Resolve(Either.Right("ok")); + + result.IsRight.Should().BeTrue(); + } + + [Test] + public async Task MonadTask_AddServices1Through7_AllReachable() + { + var monad = new TestTrain().Activate("hi"); + + var result = await Task.FromResult(monad) + .AsMonadTask() + .AddServices(new S1()) + .AddServices(new S2(), new S3()) + .AddServices(new S4(), new S5(), new S6()) + .AddServices(new S7()) + .AddServices(new S1(), new S2(), new S3(), new S4()) + .AddServices(new S1(), new S2(), new S3(), new S4(), new S5()) + .AddServices( + new S1(), + new S2(), + new S3(), + new S4(), + new S5(), + new S6() + ) + .AddServices( + new S1(), + new S2(), + new S3(), + new S4(), + new S5(), + new S6(), + new S7() + ) + .Resolve(Either.Right("ok")); + + result.IsRight.Should().BeTrue(); + } + + #region Test fixtures + + private class HasInner + { + public string Inner { get; set; } = "payload"; + } + + private class DoneJunction : Junction + { + public override Task Run(string input) => Task.FromResult($"{input}-done"); + + public DoneJunction() { } + } + + private class UnitJunction : Junction + { + public override Task Run(string input) => + Task.FromResult(LanguageExt.Unit.Default); + + public UnitJunction() { } + } + + private class ShortCircuitJunction : Junction + { + public override Task Run(string input) => Task.FromResult("shorted"); + + public ShortCircuitJunction() { } + } + + private interface IS1 { } + + private interface IS2 { } + + private interface IS3 { } + + private interface IS4 { } + + private interface IS5 { } + + private interface IS6 { } + + private interface IS7 { } + + private class S1 : IS1 { } + + private class S2 : IS2 { } + + private class S3 : IS3 { } + + private class S4 : IS4 { } + + private class S5 : IS5 { } + + private class S6 : IS6 { } + + private class S7 : IS7 { } + + private class TestTrain : Train + { + protected override Task> RunInternal(string input) => + throw new NotImplementedException(); + } + + #endregion +} From c9eb02266ad816ea9c9a56e98189e1b463e4e82b Mon Sep 17 00:00:00 2001 From: Theaux Masquelier <43664045+Theauxm@users.noreply.github.com> Date: Mon, 4 May 2026 16:31:20 -0600 Subject: [PATCH 2/3] docs: add coverage badge to README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 14fb296..07d76ff 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![NuGet Version](https://img.shields.io/nuget/v/Trax.Core)](https://www.nuget.org/packages/Trax.Core/) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) +[![Coverage](https://img.shields.io/badge/coverage-97.3%25-brightgreen?logo=codecov&logoColor=white)](#) Railway Oriented Programming for .NET. Build trains that carry data through a sequence of stops, with automatic derailment handling when something goes wrong. From 60df13be799c672c08a29911ef9a4b2140b0ebb5 Mon Sep 17 00:00:00 2001 From: Theaux Masquelier <43664045+Theauxm@users.noreply.github.com> Date: Mon, 4 May 2026 16:32:27 -0600 Subject: [PATCH 3/3] docs: switch to live Codecov badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 07d76ff..bc2802d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![NuGet Version](https://img.shields.io/nuget/v/Trax.Core)](https://www.nuget.org/packages/Trax.Core/) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) -[![Coverage](https://img.shields.io/badge/coverage-97.3%25-brightgreen?logo=codecov&logoColor=white)](#) +[![codecov](https://codecov.io/gh/TraxSharp/Trax.Core/branch/main/graph/badge.svg)](https://codecov.io/gh/TraxSharp/Trax.Core) Railway Oriented Programming for .NET. Build trains that carry data through a sequence of stops, with automatic derailment handling when something goes wrong.