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
66 changes: 66 additions & 0 deletions .github/workflows/aot-smoke.yaml
Original file line number Diff line number Diff line change
@@ -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"
80 changes: 80 additions & 0 deletions tests/Wolfgang.TryPattern.AotSmoke/Program.cs
Original file line number Diff line number Diff line change
@@ -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<T>(Func<T>) — success + failure. T=int under nullable-enable and
// unconstrained T maps `Result<T?>` back to Result<int> (int? in a
// value-type context with no `where T : struct`).
Result<int> r3 = Try.Run(() => 42);
expected++; if (r3.Succeeded && r3.Value == 42) actual++;

Result<int> r4 = Try.Run<int>(() => 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<T>(Func<Task<T>>, CancellationToken) — success + failure.
Result<string?> r7 = await Try.RunAsync<string>(() => Task.FromResult<string?>("ok"));
expected++; if (r7.Succeeded && string.Equals(r7.Value, "ok", StringComparison.Ordinal)) actual++;

Result<string?> r8 = await Try.RunAsync<string>(() => 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<T> factories.
Result<int> r13 = Result<int>.Success(7);
expected++; if (r13.Succeeded && r13.Value == 7) actual++;

Result<int> r14 = Result<int>.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;
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>

<!-- The whole point of this project: enable the trim + Native AOT analyzers so
any trim/AOT regression in the library's AOT-safe surface fails the build,
and let `dotnet publish` produce a real native binary the CI step runs. -->
<PublishAot>true</PublishAot>
<!-- PublishAot implies trimming, but set it explicitly so the project file
matches the documented intent and the trim analyzers are unambiguous. -->
<PublishTrimmed>true</PublishTrimmed>
<IsAotCompatible>true</IsAotCompatible>
<TrimmerSingleWarn>false</TrimmerSingleWarn>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>

<IsPackable>false</IsPackable>
<!-- Not a test project (no test SDK) — it is a compile+publish+run smoke. -->
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Wolfgang.TryPattern\Wolfgang.TryPattern.csproj" />
</ItemGroup>
</Project>