diff --git a/.github/workflows/aot-smoke.yaml b/.github/workflows/aot-smoke.yaml new file mode 100644 index 0000000..457097f --- /dev/null +++ b/.github/workflows/aot-smoke.yaml @@ -0,0 +1,66 @@ +name: Native AOT smoke + +# Trim + Native AOT smoke test per issue #180. Publishes a tiny consumer +# console app (tests/Wolfgang.TryPattern.AotSmoke) with PublishAot + +# PublishTrimmed and runs the native binary. Two failure modes: +# +# 1. Trim / AOT analyzers warn on the library's public surface — +# TreatWarningsAsErrors in the smoke csproj promotes it to a build +# failure. Catches reflection or dynamic-code paths that would break +# silently under AOT. +# 2. The published native binary exits non-zero — the smoke's Program.cs +# exercises every public API and asserts the observed count matches +# the expected count, so a trimmed-away member manifests as an exit +# code, not a silent no-op. + +on: + pull_request: + branches: [main, vNext] + paths: + - 'src/**' + - 'tests/Wolfgang.TryPattern.AotSmoke/**' + - '.github/workflows/aot-smoke.yaml' + - 'Directory.Build.props' + push: + branches: [main] + paths: + - 'src/**' + - 'tests/Wolfgang.TryPattern.AotSmoke/**' + - '.github/workflows/aot-smoke.yaml' + - 'Directory.Build.props' + workflow_dispatch: + +permissions: + contents: read + +jobs: + aot-smoke: + name: Publish + run Native AOT binary + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout code + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + persist-credentials: false + + - name: Setup .NET + uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5 + with: + dotnet-version: '10.0.x' + + - name: Publish Native AOT (trim + AOT analyzers gate the build) + run: dotnet publish tests/Wolfgang.TryPattern.AotSmoke -c Release -r linux-x64 + + - name: Run the published native binary + # The published binary lives at + # tests/Wolfgang.TryPattern.AotSmoke/bin/Release/net10.0/linux-x64/publish/Wolfgang.TryPattern.AotSmoke + # `find` used to be resilient to TFM / RID moves. + run: | + BIN=$(find tests/Wolfgang.TryPattern.AotSmoke/bin -type f -name 'Wolfgang.TryPattern.AotSmoke' -path '*publish*' | head -n1) + if [ -z "$BIN" ]; then + echo "::error::Native AOT binary not produced." + exit 1 + fi + echo "Running: $BIN" + "$BIN" diff --git a/tests/Wolfgang.TryPattern.AotSmoke/Program.cs b/tests/Wolfgang.TryPattern.AotSmoke/Program.cs new file mode 100644 index 0000000..ca41294 --- /dev/null +++ b/tests/Wolfgang.TryPattern.AotSmoke/Program.cs @@ -0,0 +1,80 @@ +// Native AOT / trimming smoke test for the AOT-safe public surface of +// Wolfgang.TryPattern. Published with PublishAot + PublishTrimmed (see +// the .csproj) and run by CI: a trim/AOT-unsafe regression makes the +// analyzers warn (TreatWarningsAsErrors → build fails), and a runtime +// break (MissingMethodException / NotSupportedException / silent no-op) +// makes this program exit non-zero. +// +// Try-Pattern has no reflection, no dynamic-code paths, no serialization — +// the entire public surface is expected to be AOT-safe. Every public +// method is exercised at least once below, both success and failure +// paths, so a future refactor that inadvertently pulls in a +// reflection-dependent code path is caught here. + +using System.Globalization; +using Wolfgang.TryPattern; + +int expected = 0; +int actual = 0; + +// Try.Run(Action) — success + failure. +Result r1 = Try.Run(() => { }); +expected++; if (r1.Succeeded) actual++; + +Result r2 = Try.Run(() => throw new InvalidOperationException("boom")); +expected++; if (r2.Failed && string.Equals(r2.ErrorMessage, "boom", StringComparison.Ordinal)) actual++; + +// Try.Run(Func) — success + failure. T=int under nullable-enable and +// unconstrained T maps `Result` back to Result (int? in a +// value-type context with no `where T : struct`). +Result r3 = Try.Run(() => 42); +expected++; if (r3.Succeeded && r3.Value == 42) actual++; + +Result r4 = Try.Run(() => throw new InvalidOperationException("boom-t")); +expected++; if (r4.Failed && string.Equals(r4.ErrorMessage, "boom-t", StringComparison.Ordinal)) actual++; + +// Try.RunAsync(Action, CancellationToken) — success + failure. +Result r5 = await Try.RunAsync(() => { }); +expected++; if (r5.Succeeded) actual++; + +Result r6 = await Try.RunAsync(() => throw new InvalidOperationException("async-boom")); +expected++; if (r6.Failed && string.Equals(r6.ErrorMessage, "async-boom", StringComparison.Ordinal)) actual++; + +// Try.RunAsync(Func>, CancellationToken) — success + failure. +Result r7 = await Try.RunAsync(() => Task.FromResult("ok")); +expected++; if (r7.Succeeded && string.Equals(r7.Value, "ok", StringComparison.Ordinal)) actual++; + +Result r8 = await Try.RunAsync(() => throw new InvalidOperationException("async-boom-t")); +expected++; if (r8.Failed && string.Equals(r8.ErrorMessage, "async-boom-t", StringComparison.Ordinal)) actual++; + +// Result static factories + combinators. +Result r9 = Result.Success(); +expected++; if (r9.Succeeded) actual++; + +Result r10 = Result.Failure("nope"); +expected++; if (r10.Failed && string.Equals(r10.ErrorMessage, "nope", StringComparison.Ordinal)) actual++; + +Result r11 = Result.Flatten(Result.Success(), Result.Success()); +expected++; if (r11.Succeeded) actual++; + +Result r12 = Result.Flatten(Result.Success(), Result.Failure("nope-flat")); +expected++; if (r12.Failed) actual++; + +expected++; if (Result.AnyFailed(Result.Success(), Result.Failure("f"))) actual++; +expected++; if (Result.AllSucceeded(Result.Success(), Result.Success())) actual++; + +// Result factories. +Result r13 = Result.Success(7); +expected++; if (r13.Succeeded && r13.Value == 7) actual++; + +Result r14 = Result.Failure("nope-t"); +expected++; if (r14.Failed && string.Equals(r14.ErrorMessage, "nope-t", StringComparison.Ordinal)) actual++; + +if (actual != expected) +{ + System.Console.Error.WriteLine(string.Create(CultureInfo.InvariantCulture, $"FAIL: expected {expected} AOT-safe assertions, got {actual}.")); + return 1; +} + +System.Console.WriteLine(string.Create(CultureInfo.InvariantCulture, $"OK: AOT-safe surface passed {actual} assertions under Native AOT.")); +return 0; diff --git a/tests/Wolfgang.TryPattern.AotSmoke/Wolfgang.TryPattern.AotSmoke.csproj b/tests/Wolfgang.TryPattern.AotSmoke/Wolfgang.TryPattern.AotSmoke.csproj new file mode 100644 index 0000000..51fc948 --- /dev/null +++ b/tests/Wolfgang.TryPattern.AotSmoke/Wolfgang.TryPattern.AotSmoke.csproj @@ -0,0 +1,27 @@ + + + Exe + net10.0 + latest + enable + enable + + + true + + true + true + false + true + + false + + + + + + +