From eed6a7c2be620f4158fb60ef1bb60f1b8b1668ef Mon Sep 17 00:00:00 2001 From: Jason Barden Date: Fri, 29 May 2026 14:22:44 +0100 Subject: [PATCH 1/4] update the existing dotnet.yml to add test reporting don't expect it to work but... --- .github/workflows/dotnet.yml | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 217f7cb..6cde4b7 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -1,14 +1,21 @@ -# This workflow will build a .NET project -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net - -name: .NET +name: .NET Build and Test on: + workflow_dispatch: push: branches: [ "main" ] pull_request: branches: [ "main" ] +permissions: + contents: read + checks: write + pull-requests: write + +concurrency: + group: dotnet-ci-${{ github.ref }} + cancel-in-progress: true + jobs: build: @@ -26,3 +33,18 @@ jobs: run: dotnet build --no-restore - name: Test run: dotnet test --no-build --verbosity normal + + - name: Publish test results + uses: EnricoMi/publish-unit-test-result-action@v2.23.0 + if: always() + with: + files: TestResults/**/*.trx + check_name: Test results + fail_on: test failures + + - name: Upload coverage + uses: codecov/codecov-action@v5.4.3 + if: always() + with: + files: TestResults/**/coverage.cobertura.xml + fail_ci_if_error: false From b99f4e4d002d8d3b42915d7b702981d1805b9aa6 Mon Sep 17 00:00:00 2001 From: Jason Barden Date: Fri, 29 May 2026 14:45:12 +0100 Subject: [PATCH 2/4] update the dotnet.yml and add the functional tests --- .github/workflows/dotnet.yml | 48 +++- .../AStar.Dev.CloudSyncFunctional.csproj | 40 ++++ .../appsettings.json | 1 + .../AStar.Dev.FunctionalParadigm.csproj | 1 - src/AStar.Dev.FunctionalParadigm/Option.cs | 44 ++++ .../OptionExtensions.cs | 72 ++++++ src/AStar.Dev.FunctionalParadigm/Program.cs | 2 - src/AStar.Dev.FunctionalParadigm/Result.cs | 24 ++ .../ResultExtensions.cs | 212 ++++++++++++++++++ src/AStar.Dev.FunctionalParadigm/Unit.cs | 8 + ...oudSyncFunctional.Tests.Integration.csproj | 3 +- ....Dev.CloudSyncFunctional.Tests.Unit.csproj | 23 +- ...r.Dev.FunctionalParadigm.Tests.Unit.csproj | 17 +- .../GivenAResult.cs | 26 +++ .../GivenAResultBind.cs | 26 +++ .../GivenAResultMap.cs | 26 +++ .../GivenAResultMatch.cs | 99 ++++++++ .../GivenAResultTap.cs | 28 +++ .../GivenAnOption.cs | 26 +++ .../GivenAnOptionBind.cs | 26 +++ .../GivenAnOptionMap.cs | 26 +++ .../GivenAnOptionMatch.cs | 26 +++ .../GivenAnOptionTap.cs | 28 +++ .../UnitTest1.cs | 10 - 24 files changed, 816 insertions(+), 26 deletions(-) create mode 100644 src/AStar.Dev.CloudSyncFunctional/appsettings.json create mode 100644 src/AStar.Dev.FunctionalParadigm/Option.cs create mode 100644 src/AStar.Dev.FunctionalParadigm/OptionExtensions.cs delete mode 100644 src/AStar.Dev.FunctionalParadigm/Program.cs create mode 100644 src/AStar.Dev.FunctionalParadigm/Result.cs create mode 100644 src/AStar.Dev.FunctionalParadigm/ResultExtensions.cs create mode 100644 src/AStar.Dev.FunctionalParadigm/Unit.cs create mode 100644 test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAResult.cs create mode 100644 test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAResultBind.cs create mode 100644 test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAResultMap.cs create mode 100644 test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAResultMatch.cs create mode 100644 test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAResultTap.cs create mode 100644 test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAnOption.cs create mode 100644 test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAnOptionBind.cs create mode 100644 test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAnOptionMap.cs create mode 100644 test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAnOptionMatch.cs create mode 100644 test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAnOptionTap.cs delete mode 100644 test/AStar.Dev.FunctionalParadigm.Tests.Unit/UnitTest1.cs diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 6cde4b7..4baad64 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -23,16 +23,43 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x + dotnet-version: 10.0.x + - name: Restore dependencies run: dotnet restore + - name: Build run: dotnet build --no-restore - - name: Test - run: dotnet test --no-build --verbosity normal + + build-test-coverage: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + + - name: Restore + run: dotnet restore + + - name: Install dotnet-coverage + run: dotnet tool install -g dotnet-coverage + + - name: Run tests with coverage (MTP v2 compatible) + run: | + mkdir -p coverage + dotnet-coverage collect \ + -o coverage/coverage.cobertura.xml \ + -f cobertura \ + "dotnet test --no-build --configuration Release" - name: Publish test results uses: EnricoMi/publish-unit-test-result-action@v2.23.0 @@ -42,9 +69,14 @@ jobs: check_name: Test results fail_on: test failures - - name: Upload coverage - uses: codecov/codecov-action@v5.4.3 - if: always() + - name: Upload coverage to GitHub + uses: actions/upload-artifact@v4 + with: + name: coverage + path: coverage/coverage.cobertura.xml + + - name: Publish coverage to GitHub Code Coverage + uses: actions/upload-code-coverage@v3 with: - files: TestResults/**/coverage.cobertura.xml - fail_ci_if_error: false + path: coverage/coverage.cobertura.xml + format: cobertura diff --git a/src/AStar.Dev.CloudSyncFunctional/AStar.Dev.CloudSyncFunctional.csproj b/src/AStar.Dev.CloudSyncFunctional/AStar.Dev.CloudSyncFunctional.csproj index ed9781c..24dad45 100644 --- a/src/AStar.Dev.CloudSyncFunctional/AStar.Dev.CloudSyncFunctional.csproj +++ b/src/AStar.Dev.CloudSyncFunctional/AStar.Dev.CloudSyncFunctional.csproj @@ -5,6 +5,46 @@ net10.0 enable enable + true + + + + + + + + + + + None + All + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/src/AStar.Dev.CloudSyncFunctional/appsettings.json b/src/AStar.Dev.CloudSyncFunctional/appsettings.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/src/AStar.Dev.CloudSyncFunctional/appsettings.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/AStar.Dev.FunctionalParadigm/AStar.Dev.FunctionalParadigm.csproj b/src/AStar.Dev.FunctionalParadigm/AStar.Dev.FunctionalParadigm.csproj index ed9781c..b760144 100644 --- a/src/AStar.Dev.FunctionalParadigm/AStar.Dev.FunctionalParadigm.csproj +++ b/src/AStar.Dev.FunctionalParadigm/AStar.Dev.FunctionalParadigm.csproj @@ -1,7 +1,6 @@  - Exe net10.0 enable enable diff --git a/src/AStar.Dev.FunctionalParadigm/Option.cs b/src/AStar.Dev.FunctionalParadigm/Option.cs new file mode 100644 index 0000000..95faa28 --- /dev/null +++ b/src/AStar.Dev.FunctionalParadigm/Option.cs @@ -0,0 +1,44 @@ +namespace AStar.Dev.FunctionalParadigm; + +public abstract record Option +{ + public static implicit operator TResult(Option option) => + option switch + { + Some some => some.Value, + _ => default! + }; + + public static implicit operator string(Option option) => + option switch + { + None => "missing", + Option.None => "missing", + _ => string.Empty + }; + + /// + /// Represents the absence of a value. + /// + public sealed record None : Option + { + /// + /// A helper method to create an instance of + /// + public static readonly None Instance = new(); + + private None() + { + } + + /// + /// Overrides the ToString method to return the type as a simple string. + /// + /// The overridden ToString + public override string ToString() => "None"; + } +} + +public record Some(TResult Value) : Option; + +public record None() : Option; diff --git a/src/AStar.Dev.FunctionalParadigm/OptionExtensions.cs b/src/AStar.Dev.FunctionalParadigm/OptionExtensions.cs new file mode 100644 index 0000000..bd2023b --- /dev/null +++ b/src/AStar.Dev.FunctionalParadigm/OptionExtensions.cs @@ -0,0 +1,72 @@ +namespace AStar.Dev.FunctionalParadigm; + +public static class OptionExtensions +{ + public static Option Tap(this Option option, Action onSome, Action? onNone = null) + { + if (option is Some some) + { + onSome(some.Value); + return some; + } + + if (option is None none) + { + onNone?.Invoke(none); + return none; + } + + throw new InvalidOperationException("Unexpected option type."); + } + + public static Option Map(this Option option, Func selector) + => option switch + { + Some some => new Some(selector(some.Value)), + None => new None(), + _ => throw new InvalidOperationException("Unexpected option type.") + }; + + public static Option Bind(this Option option, Func> binder) + => option switch + { + Some some => binder(some.Value), + None => new None(), + _ => throw new InvalidOperationException("Unexpected option type.") + }; + + public static TOut Match(this Option option, Func onSome, Func onNone) + => option switch + { + Some some => onSome(some.Value), + None none => onNone(none), + _ => throw new InvalidOperationException("Unexpected option type.") + }; + + public static TOut Match(this Option option, Func onSome) + => option switch + { + Some some => onSome(some.Value), + None none => new None(), + _ => throw new InvalidOperationException("Unexpected option type.") + }; + + public static async Task MatchAsync(this Task> task, Func onSome, Func onNone) + { + var option = await task.ConfigureAwait(false); + + return option.Match(onSome, onNone); + } + + public static async Task MatchAsync(this Task> task, Func> onSome, Func> onNone) + { + var option = await task.ConfigureAwait(false); + + return option switch + { + Some some => await onSome(some.Value).ConfigureAwait(false), + None none => await onNone(none).ConfigureAwait(false), + _ => throw new InvalidOperationException("Unexpected option type.") + }; + } +} \ No newline at end of file diff --git a/src/AStar.Dev.FunctionalParadigm/Program.cs b/src/AStar.Dev.FunctionalParadigm/Program.cs deleted file mode 100644 index 3751555..0000000 --- a/src/AStar.Dev.FunctionalParadigm/Program.cs +++ /dev/null @@ -1,2 +0,0 @@ -// See https://aka.ms/new-console-template for more information -Console.WriteLine("Hello, World!"); diff --git a/src/AStar.Dev.FunctionalParadigm/Result.cs b/src/AStar.Dev.FunctionalParadigm/Result.cs new file mode 100644 index 0000000..3a50626 --- /dev/null +++ b/src/AStar.Dev.FunctionalParadigm/Result.cs @@ -0,0 +1,24 @@ +namespace AStar.Dev.FunctionalParadigm; + +public abstract record Result +{ + public static implicit operator Result(TResult value) => new Ok(value); + public static implicit operator Result(TError error) => new Fail(error); + + public static implicit operator TResult(Result result) => + result switch + { + Ok ok => ok.Value, + _ => default! + }; + + public static implicit operator TError(Result result) => + result switch + { + Fail fail => fail.Error, + _ => default! + }; +} + +public record Ok(TResult Value) : Result; +public record Fail(TError Error) : Result; diff --git a/src/AStar.Dev.FunctionalParadigm/ResultExtensions.cs b/src/AStar.Dev.FunctionalParadigm/ResultExtensions.cs new file mode 100644 index 0000000..41c9d59 --- /dev/null +++ b/src/AStar.Dev.FunctionalParadigm/ResultExtensions.cs @@ -0,0 +1,212 @@ +namespace AStar.Dev.FunctionalParadigm; + +/// Extension methods for . +public static class ResultExtensions +{ + /// Applies a side-effect to the current result without changing it. + /// The success value type. + /// The error type. + /// The result to tap. + /// Action to invoke on success. + /// Optional action to invoke on failure. + /// The original result unchanged. + public static Result Tap(this Result result, Action onSuccess, Action? onFailure = null) + { + switch (result) + { + case Ok ok: + onSuccess(ok.Value); + return ok; + + case Fail fail: + onFailure?.Invoke(fail.Error); + return fail; + + default: + throw new InvalidOperationException("Unexpected result type."); + } + } + + /// Transforms the success value using the provided selector. + /// The input success type. + /// The output success type. + /// The error type. + /// The result to map. + /// The transformation function. + /// A new result with the transformed value, or the original failure. + public static Result Map(this Result result, Func selector) => result switch + { + Ok ok => new Ok(selector(ok.Value)), + Fail fail => new Fail(fail.Error), + _ => throw new InvalidOperationException("Unexpected result type.") + }; + + /// Chains an operation that also returns a result, propagating failure. + /// The input success type. + /// The output success type. + /// The error type. + /// The result to bind. + /// The chained operation. + /// The result of the chained operation, or the original failure. + public static Result Bind(this Result result, Func> binder) => result switch + { + Ok ok => binder(ok.Value), + Fail fail => new Fail(fail.Error), + _ => throw new InvalidOperationException("Unexpected result type.") + }; + + /// Collapses the result to a single output value by handling both cases. + /// The success value type. + /// The error type. + /// The output type. + /// The result to match. + /// Function invoked on success. + /// Function invoked on failure. + /// The value produced by whichever branch was taken. + public static TOut Match(this Result result, Func onSuccess, Func onFailure) => result switch + { + Ok ok => onSuccess(ok.Value), + Fail fail => onFailure(fail.Error), + _ => throw new InvalidOperationException("Unexpected result type.") + }; + + /// The task producing the result. + /// The success value type. + /// The error type. + extension(Task> taskResult) + { + /// Asynchronously handles both result cases as side-effects. + /// Action invoked on success. + /// Action invoked on failure. + /// A task that completes after the appropriate action runs. + public async Task MatchAsync(Action onSuccess, Action onFailure) + { + var result = await taskResult.ConfigureAwait(false); + switch (result) + { + case Ok ok: + onSuccess(ok.Value); + break; + case Fail fail: + onFailure(fail.Error); + break; + } + } + + /// Asynchronously collapses the result to a single output value by handling both cases. + /// The output type. + /// Function invoked on success. + /// Function invoked on failure. + /// A task that produces the value from whichever branch was taken. + public async Task MatchAsync(Func onSuccess, Func onFailure) + { + var result = await taskResult.ConfigureAwait(false); + + return result.Match(onSuccess, onFailure); + } + + /// Asynchronously collapses the result to a single output value by handling both cases. + /// The output type. + /// Function invoked on success. + /// Function invoked on failure. + /// A task that produces the value from whichever branch was taken. + /// + public async Task MatchAsync(Func> onSuccess, + Func> onFailure) + { + var result = await taskResult.ConfigureAwait(false); + + return result switch + { + Ok ok => await onSuccess(ok.Value).ConfigureAwait(false), + Fail fail => await onFailure(fail.Error).ConfigureAwait(false), + _ => throw new InvalidOperationException("Unexpected result type.") + }; + } + } + + /// Asynchronously chains an operation that also returns a result, propagating failure. + /// The input success type. + /// The output success type. + /// The error type. + /// The task producing the result to bind. + /// The async chained operation to invoke on success. + /// The result of the chained operation, or the original failure propagated as the new error type. + public static async Task> BindAsync(this Task> taskResult, Func>> binder) + { + var result = await taskResult.ConfigureAwait(false); + + return result switch + { + Ok ok => await binder(ok.Value).ConfigureAwait(false), + Fail fail => new Fail(fail.Error), + _ => throw new InvalidOperationException("Unexpected result type.") + }; + } + /// Chains an asynchronous operation that also returns a result, propagating failure. + /// The input success type. + /// The output success type. + /// The error type. + /// The result to bind. + /// The async chained operation to invoke on success. + /// The result of the chained operation, or the original failure. + public static async Task> BindAsync(this Result result, Func>> binder) => result switch + { + Ok ok => await binder(ok.Value).ConfigureAwait(false), + Fail fail => new Fail(fail.Error), + _ => throw new InvalidOperationException("Unexpected result type.") + }; + + /// Asynchronously transforms the success value using the provided selector. + /// The input success type. + /// The output success type. + /// The error type. + /// The result to map. + /// The transformation function. + /// A task containing a new result with the transformed value, or the original failure. + public static Task> MapAsync(this Result result, Func selector) + => Task.FromResult(result.Map(selector)); + + /// Asynchronously transforms the success value using the provided asynchronous selector. + /// The input success type. + /// The output success type. + /// The error type. + /// The result to map. + /// The async transformation function. + /// A task containing a new result with the transformed value, or the original failure. + public static async Task> MapAsync(this Result result, Func> selector) => result switch + { + Ok ok => new Ok(await selector(ok.Value).ConfigureAwait(false)), + Fail fail => new Fail(fail.Error), + _ => throw new InvalidOperationException("Unexpected result type.") + }; + + /// Asynchronously transforms the success value of a task result using the provided selector. + /// The input success type. + /// The output success type. + /// The error type. + /// The task producing the result to map. + /// The transformation function. + /// A task containing a new result with the transformed value, or the original failure. + public static async Task> MapAsync(this Task> taskResult, Func selector) + { + var result = await taskResult.ConfigureAwait(false); + + return result.Map(selector); + } + + /// Asynchronously transforms the success value of a task result using the provided asynchronous selector. + /// The input success type. + /// The output success type. + /// The error type. + /// The task producing the result to map. + /// The async transformation function. + /// A task containing a new result with the transformed value, or the original failure. + public static async Task> MapAsync(this Task> taskResult, Func> selector) + { + var result = await taskResult.ConfigureAwait(false); + + return await result.MapAsync(selector).ConfigureAwait(false); + } + +} diff --git a/src/AStar.Dev.FunctionalParadigm/Unit.cs b/src/AStar.Dev.FunctionalParadigm/Unit.cs new file mode 100644 index 0000000..c121b45 --- /dev/null +++ b/src/AStar.Dev.FunctionalParadigm/Unit.cs @@ -0,0 +1,8 @@ +namespace AStar.Dev.FunctionalParadigm; + +/// Represents the absence of a meaningful return value. +public record Unit +{ + /// Gets the singleton default instance. + public static Unit Default { get; } = new(); +} diff --git a/test/AStar.Dev.CloudSyncFunctional.Tests.Integration/AStar.Dev.CloudSyncFunctional.Tests.Integration.csproj b/test/AStar.Dev.CloudSyncFunctional.Tests.Integration/AStar.Dev.CloudSyncFunctional.Tests.Integration.csproj index 733b8d6..b8336ed 100644 --- a/test/AStar.Dev.CloudSyncFunctional.Tests.Integration/AStar.Dev.CloudSyncFunctional.Tests.Integration.csproj +++ b/test/AStar.Dev.CloudSyncFunctional.Tests.Integration/AStar.Dev.CloudSyncFunctional.Tests.Integration.csproj @@ -5,8 +5,9 @@ enable Exe AStar.Dev.CloudSyncFunctional.Tests.Integration - net8.0 + net10.0 true + true diff --git a/test/AStar.Dev.CloudSyncFunctional.Tests.Unit/AStar.Dev.CloudSyncFunctional.Tests.Unit.csproj b/test/AStar.Dev.CloudSyncFunctional.Tests.Unit/AStar.Dev.CloudSyncFunctional.Tests.Unit.csproj index c3f1265..7128f75 100644 --- a/test/AStar.Dev.CloudSyncFunctional.Tests.Unit/AStar.Dev.CloudSyncFunctional.Tests.Unit.csproj +++ b/test/AStar.Dev.CloudSyncFunctional.Tests.Unit/AStar.Dev.CloudSyncFunctional.Tests.Unit.csproj @@ -5,8 +5,10 @@ enable Exe AStar.Dev.CloudSyncFunctional.Tests.Unit - net8.0 + net10.0 true + true + NU1902;NU1903;CA1859 @@ -15,10 +17,27 @@ + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + - + + + + + \ No newline at end of file diff --git a/test/AStar.Dev.FunctionalParadigm.Tests.Unit/AStar.Dev.FunctionalParadigm.Tests.Unit.csproj b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/AStar.Dev.FunctionalParadigm.Tests.Unit.csproj index 1f4c96a..25592bb 100644 --- a/test/AStar.Dev.FunctionalParadigm.Tests.Unit/AStar.Dev.FunctionalParadigm.Tests.Unit.csproj +++ b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/AStar.Dev.FunctionalParadigm.Tests.Unit.csproj @@ -4,9 +4,12 @@ enable enable Exe - AStar.Dev.FunctionalParadigm.Tests.Unit - net8.0 + AStar.Dev.FunctionsParadigm.Tests.Unit + net10.0 true + true + true + NU1902;CA1859 @@ -15,10 +18,20 @@ + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + diff --git a/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAResult.cs b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAResult.cs new file mode 100644 index 0000000..f00e94c --- /dev/null +++ b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAResult.cs @@ -0,0 +1,26 @@ +using AStar.Dev.FunctionalParadigm; + +namespace AStar.Dev.FunctionsParadigm.Tests.Unit; + +public class GivenAResult +{ + [Fact] + public void when_an_ok_result_is_created_then_implicit_conversion_returns_the_value() + { + Result result = new Ok(42); + + int value = result; + + value.ShouldBe(42); + } + + [Fact] + public void when_a_fail_result_is_created_then_implicit_conversion_returns_the_error() + { + Result result = new Fail("boom"); + + string error = result; + + error.ShouldBe("boom"); + } +} diff --git a/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAResultBind.cs b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAResultBind.cs new file mode 100644 index 0000000..7720d55 --- /dev/null +++ b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAResultBind.cs @@ -0,0 +1,26 @@ +using AStar.Dev.FunctionalParadigm; + +namespace AStar.Dev.FunctionsParadigm.Tests.Unit; + +public class GivenAResultBind +{ + [Fact] + public void when_binding_an_ok_result_then_the_bound_result_is_returned() + { + Result result = new Ok(7); + + Result bound = result.Bind(value => new Ok(value + 5)); + + bound.Match(value => value, _ => -1).ShouldBe(12); + } + + [Fact] + public void when_binding_a_fail_result_then_the_failure_is_preserved() + { + Result result = new Fail("nope"); + + Result bound = result.Bind(value => new Ok(value + 1)); + + bound.Match(_ => string.Empty, error => error).ShouldBe("nope"); + } +} diff --git a/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAResultMap.cs b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAResultMap.cs new file mode 100644 index 0000000..6d5e9c8 --- /dev/null +++ b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAResultMap.cs @@ -0,0 +1,26 @@ +using AStar.Dev.FunctionalParadigm; + +namespace AStar.Dev.FunctionsParadigm.Tests.Unit; + +public class GivenAResultMap +{ + [Fact] + public void when_mapping_an_ok_result_then_the_value_is_transformed() + { + Result result = new Ok(7); + + Result mapped = result.Map(value => value * 3); + + mapped.Match(value => value, _ => -1).ShouldBe(21); + } + + [Fact] + public void when_mapping_a_fail_result_then_the_error_is_preserved() + { + Result result = new Fail("nope"); + + Result mapped = result.Map(value => value * 3); + + mapped.Match(_ => string.Empty, error => error).ShouldBe("nope"); + } +} diff --git a/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAResultMatch.cs b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAResultMatch.cs new file mode 100644 index 0000000..6a660af --- /dev/null +++ b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAResultMatch.cs @@ -0,0 +1,99 @@ +using AStar.Dev.FunctionalParadigm; + +namespace AStar.Dev.FunctionsParadigm.Tests.Unit; + +public class GivenAResultMatch +{ + [Fact] + public void when_matching_an_ok_result_then_the_success_handler_is_used() + { + Result result = new Ok(8); + + string matched = result.Match(value => $"ok:{value}", error => $"fail:{error}"); + + matched.ShouldBe("ok:8"); + } + + [Fact] + public void when_matching_a_fail_result_then_the_failure_handler_is_used() + { + Result result = new Fail("bad"); + + string matched = result.Match(value => $"ok:{value}", error => $"fail:{error}"); + + matched.ShouldBe("fail:bad"); + } + + // ... existing code ... + [Fact] + public async Task when_task_result_is_fail_then_func_overload_returns_failure_value() + { + Task> task = Task.FromResult>(new Fail("err")); + + var result = await task.MatchAsync( + ok => ok * 2, + _ => -1); + + result.ShouldBe(-1); + } + + [Fact] + public async Task when_task_result_is_ok_then_async_func_overload_returns_mapped_value() + { + Task> task = Task.FromResult>(new Ok(5)); + + var result = await task.MatchAsync( + ok => Task.FromResult(ok * 2), + _ => Task.FromResult(-1)); + + result.ShouldBe(10); + } + + [Fact] + public async Task when_task_result_is_fail_then_async_func_overload_returns_failure_value() + { + Task> task = Task.FromResult>(new Fail("err")); + + var result = await task.MatchAsync( + ok => Task.FromResult(ok * 2), + _ => Task.FromResult(-1)); + + result.ShouldBe(-1); + } + + [Fact] + public async Task when_task_result_is_ok_then_async_func_overload_does_not_call_failure_branch() + { + var failureCalled = false; + Task> task = Task.FromResult>(new Ok(5)); + + var result = await task.MatchAsync( + ok => Task.FromResult(ok * 2), + _ => + { + failureCalled = true; + return Task.FromResult(-1); + }); + + result.ShouldBe(10); + failureCalled.ShouldBeFalse(); + } + + [Fact] + public async Task when_task_result_is_fail_then_async_func_overload_does_not_call_success_branch() + { + var successCalled = false; + Task> task = Task.FromResult>(new Fail("err")); + + var result = await task.MatchAsync( + ok => + { + successCalled = true; + return Task.FromResult(ok * 2); + }, + _ => Task.FromResult(-1)); + + result.ShouldBe(-1); + successCalled.ShouldBeFalse(); + } +} diff --git a/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAResultTap.cs b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAResultTap.cs new file mode 100644 index 0000000..761e6fc --- /dev/null +++ b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAResultTap.cs @@ -0,0 +1,28 @@ +using AStar.Dev.FunctionalParadigm; + +namespace AStar.Dev.FunctionsParadigm.Tests.Unit; + +public class GivenAResultTap +{ + [Fact] + public void when_tapping_an_ok_result_then_the_success_action_runs() + { + Result result = new Ok(5); + var observed = 0; + + result.Tap(value => observed = value * 2); + + observed.ShouldBe(10); + } + + [Fact] + public void when_tapping_a_fail_result_then_the_failure_action_runs() + { + Result result = new Fail("boom"); + string? observed = null; + + result.Tap(_ => observed = "unexpected", error => observed = error); + + observed.ShouldBe("boom"); + } +} diff --git a/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAnOption.cs b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAnOption.cs new file mode 100644 index 0000000..6873a83 --- /dev/null +++ b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAnOption.cs @@ -0,0 +1,26 @@ +using AStar.Dev.FunctionalParadigm; + +namespace AStar.Dev.FunctionsParadigm.Tests.Unit; + +public class GivenAnOption +{ + [Fact] + public void when_a_some_option_is_created_then_implicit_conversion_returns_the_value() + { + Option result = new Some(42); + + int value = result; + + value.ShouldBe(42); + } + + [Fact] + public void when_a_none_option_is_created_then_implicit_conversion_returns_the_error() + { + Option result = new None(); + + string error = result; + + error.ShouldBe("missing"); + } +} diff --git a/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAnOptionBind.cs b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAnOptionBind.cs new file mode 100644 index 0000000..5c50d39 --- /dev/null +++ b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAnOptionBind.cs @@ -0,0 +1,26 @@ +using AStar.Dev.FunctionalParadigm; + +namespace AStar.Dev.FunctionsParadigm.Tests.Unit; + +public class GivenAnOptionBind +{ + [Fact] + public void when_binding_a_some_option_then_the_bound_result_is_returned() + { + Option result = new Some(7); + + var bound = result.Bind(value => new Some(value + 5)); + + bound.Match(value => value, _ => -1).ShouldBe(12); + } + + [Fact] + public void when_binding_a_none_option_then_the_failure_is_preserved() + { + Option result = new None(); + + var bound = result.Bind(value => new None()); + + bound.Match(_ => string.Empty, error => error).ShouldBe("missing"); + } +} diff --git a/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAnOptionMap.cs b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAnOptionMap.cs new file mode 100644 index 0000000..189922d --- /dev/null +++ b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAnOptionMap.cs @@ -0,0 +1,26 @@ +using AStar.Dev.FunctionalParadigm; + +namespace AStar.Dev.FunctionsParadigm.Tests.Unit; + +public class GivenAnOptionMap +{ + [Fact] + public void when_mapping_a_some_option_then_the_value_is_transformed() + { + Option result = new Some(7); + + var mapped = result.Map(value => value * 3); + + mapped.Match(value => value, _ => -1).ShouldBe(21); + } + + [Fact] + public void when_mapping_a_none_option_then_the_error_is_preserved() + { + Option result = new None(); + + var mapped = result.Map(value => value * 3); + + mapped.Match(_ => string.Empty, error => error).ShouldBe("missing"); + } +} diff --git a/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAnOptionMatch.cs b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAnOptionMatch.cs new file mode 100644 index 0000000..eb703a8 --- /dev/null +++ b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAnOptionMatch.cs @@ -0,0 +1,26 @@ +using AStar.Dev.FunctionalParadigm; + +namespace AStar.Dev.FunctionsParadigm.Tests.Unit; + +public class GivenAnOptionMatch +{ + [Fact] + public void when_matching_a_some_option_then_the_success_handler_is_used() + { + Option result = new Some(8); + + string matched = result.Match(value => $"ok:{value}", error => $"fail:{error}"); + + matched.ShouldBe("ok:8"); + } + + [Fact] + public void when_matching_a_none_option_then_the_failure_handler_is_used() + { + Option result = new None(); + + string matched = result.Match(value => $"ok:{value}", error => $"fail:{error}"); + + matched.ShouldBe("fail:missing"); + } +} diff --git a/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAnOptionTap.cs b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAnOptionTap.cs new file mode 100644 index 0000000..085fe07 --- /dev/null +++ b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAnOptionTap.cs @@ -0,0 +1,28 @@ +using AStar.Dev.FunctionalParadigm; + +namespace AStar.Dev.FunctionsParadigm.Tests.Unit; + +public class GivenAnOptionTap +{ + [Fact] + public void when_tapping_a_some_option_then_the_success_action_runs() + { + Option result = new Some(5); + var observed = 0; + + result.Tap(value => observed = value * 2, _ => observed = -1); + + observed.ShouldBe(10); + } + + [Fact] + public void when_tapping_a_none_option_then_the_failure_action_runs() + { + Option result = new None(); + string? observed = null; + + result.Tap(_ => observed = "unexpected", error => observed = "error"); + + observed.ShouldBe("error"); + } +} diff --git a/test/AStar.Dev.FunctionalParadigm.Tests.Unit/UnitTest1.cs b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/UnitTest1.cs deleted file mode 100644 index b87107e..0000000 --- a/test/AStar.Dev.FunctionalParadigm.Tests.Unit/UnitTest1.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace AStar.Dev.FunctionalParadigm.Tests.Unit; - -public class UnitTest1 -{ - [Fact] - public void Test1() - { - Assert.True(true); - } -} From b046b252816ed359628d6e3e83456eb883dd84c5 Mon Sep 17 00:00:00 2001 From: Jason Barden Date: Fri, 29 May 2026 16:29:10 +0100 Subject: [PATCH 3/4] reverting to a combined earlier version of the trys --- .github/workflows/dotnet.yml | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 4baad64..cdd197c 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -34,21 +34,6 @@ jobs: - name: Build run: dotnet build --no-restore - - build-test-coverage: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 10.0.x - - - name: Restore - run: dotnet restore - name: Install dotnet-coverage run: dotnet tool install -g dotnet-coverage @@ -74,9 +59,3 @@ jobs: with: name: coverage path: coverage/coverage.cobertura.xml - - - name: Publish coverage to GitHub Code Coverage - uses: actions/upload-code-coverage@v3 - with: - path: coverage/coverage.cobertura.xml - format: cobertura From c1be059f438a432c3c2f781fa0d3c8d37af7be47 Mon Sep 17 00:00:00 2001 From: Jason Barden Date: Fri, 29 May 2026 16:31:44 +0100 Subject: [PATCH 4/4] fine. just run the tests for now --- .github/workflows/dotnet.yml | 28 +++------------------------- 1 file changed, 3 insertions(+), 25 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index cdd197c..130b9be 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -34,28 +34,6 @@ jobs: - name: Build run: dotnet build --no-restore - - - name: Install dotnet-coverage - run: dotnet tool install -g dotnet-coverage - - - name: Run tests with coverage (MTP v2 compatible) - run: | - mkdir -p coverage - dotnet-coverage collect \ - -o coverage/coverage.cobertura.xml \ - -f cobertura \ - "dotnet test --no-build --configuration Release" - - - name: Publish test results - uses: EnricoMi/publish-unit-test-result-action@v2.23.0 - if: always() - with: - files: TestResults/**/*.trx - check_name: Test results - fail_on: test failures - - - name: Upload coverage to GitHub - uses: actions/upload-artifact@v4 - with: - name: coverage - path: coverage/coverage.cobertura.xml + + - name: Test + run: dotnet test --no-build --verbosity normal