Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
[![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.

Expand Down
Original file line number Diff line number Diff line change
@@ -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<InvalidOperationException>().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<InvalidOperationException>().WithMessage("*has not been loaded*");
}

[Test]
public void AssertEachLoaded_EmptyCollection_DoesNotThrow()
{
var items = Array.Empty<Holder>();

Action act = () => items.AssertEachLoaded(x => x.Value);

act.Should().NotThrow();
}

private class Holder
{
public Holder(string? value) => Value = value;

public string? Value { get; }
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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<INotAClass, int, string>();

result.Should().BeNull();
monad.Exception.Should().BeOfType<TrainException>();
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<MultiCtorJunction, int, string>();

result.Should().BeNull();
monad.Exception.Should().BeOfType<TrainException>();
monad.Exception!.Message.Should().Contain("single constructor");
}

[Test]
public void InitializeJunction_MissingDependencyInMemory_RecordsExceptionViaExtract()
{
var monad = new TestTrain().Activate(0);

var result = monad.InitializeJunction<NeedsDependencyJunction, int, string>();

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<MonadExtensionsEdgeCasesTests>));

act.Should().Throw<TrainException>().WithMessage("*ILoggerFactory*");
}

[Test]
public void ExtractLoggerFromLoggerFactory_CreatesGenericLogger_FromFactory()
{
var factory = NullLoggerFactory.Instance;
var monad = new TestTrain().Activate(0).AddServices<ILoggerFactory>(factory);

var logger = monad.ExtractLoggerFromLoggerFactory(
typeof(ILogger<MonadExtensionsEdgeCasesTests>)
);

((object?)logger).Should().NotBeNull();
}

[Test]
public void ExtractTuple_TwoElements_ReturnsValueTuple()
{
var monad = new TestTrain().Activate(0);
monad.AddServices<IFoo>(new Foo());
monad.AddServices<IBar>(new Bar());

var tuple = monad.ExtractTuple(typeof(ValueTuple<IFoo, IBar>));

((object)tuple).Should().NotBeNull();
}

[Test]
public void ExtractTuple_OneElement_Throws()
{
var monad = new TestTrain().Activate(0);
monad.AddServices<IFoo>(new Foo());

Action act = () => monad.ExtractTuple(typeof(ValueTuple<IFoo>));

act.Should().Throw<TrainException>().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<TrainException>();
}

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<int, string>
{
protected override Task<Either<Exception, string>> RunInternal(int input) =>
throw new NotImplementedException();
}
}
Original file line number Diff line number Diff line change
@@ -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<IA>(new A());
monad.AddServices<IB>(new B());
monad.AddServices<IC>(new C());
monad.AddServices<ID>(new D());

var tuple = monad.ExtractTuple(typeof(ValueTuple<IA, IB, IC, ID>));

((object)tuple).Should().NotBeNull();
}

[Test]
public void ExtractTuple_FiveElements_ReturnsValueTuple()
{
var monad = NewActivated();
monad.AddServices<IA>(new A());
monad.AddServices<IB>(new B());
monad.AddServices<IC>(new C());
monad.AddServices<ID>(new D());
monad.AddServices<IE>(new E());

var tuple = monad.ExtractTuple(typeof(ValueTuple<IA, IB, IC, ID, IE>));

((object)tuple).Should().NotBeNull();
}

[Test]
public void ExtractTuple_SixElements_ReturnsValueTuple()
{
var monad = NewActivated();
monad.AddServices<IA>(new A());
monad.AddServices<IB>(new B());
monad.AddServices<IC>(new C());
monad.AddServices<ID>(new D());
monad.AddServices<IE>(new E());
monad.AddServices<IF>(new F());

var tuple = monad.ExtractTuple(typeof(ValueTuple<IA, IB, IC, ID, IE, IF>));

((object)tuple).Should().NotBeNull();
}

[Test]
public void ExtractTuple_SevenElements_ReturnsValueTuple()
{
var monad = NewActivated();
monad.AddServices<IA>(new A());
monad.AddServices<IB>(new B());
monad.AddServices<IC>(new C());
monad.AddServices<ID>(new D());
monad.AddServices<IE>(new E());
monad.AddServices<IF>(new F());
monad.AddServices<IG>(new G());

var tuple = monad.ExtractTuple(typeof(ValueTuple<IA, IB, IC, ID, IE, IF, IG>));

((object)tuple).Should().NotBeNull();
}

private static Trax.Core.Monad.Monad<int, string> 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<int, string>
{
protected override Task<Either<Exception, string>> RunInternal(int input) =>
throw new NotImplementedException();
}
}
Loading
Loading