diff --git a/.github/workflows/aot-smoke.yaml b/.github/workflows/aot-smoke.yaml new file mode 100644 index 0000000..9f0d2bc --- /dev/null +++ b/.github/workflows/aot-smoke.yaml @@ -0,0 +1,53 @@ +name: AOT Smoke + +# Native-AOT publish-and-run smoke consumer (#132). +# +# Publishes a tiny console consumer with PublishAot and runs the native binary, +# so the build fails if the library stops compiling or running under native AOT +# (e.g. the async-enumerable pipeline path stops being trimmer-safe). This is the +# *runtime* half of AOT verification: it exercises TestExtractor / TestTransformer +# / TestLoader end-to-end in a trimmed, natively-compiled binary. +# +# windows-latest ships the MSVC "Desktop development with C++" workload that the +# native linker requires, so no extra toolchain install is needed. +# +# Runs on every PR so AOT regressions block before merge. + +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: aot-smoke-${{ github.ref }} + cancel-in-progress: true + +jobs: + aot-smoke: + name: "AOT publish + run" + runs-on: windows-latest + timeout-minutes: 20 + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + + - name: Setup .NET + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6 + with: + dotnet-version: '10.0.x' + + - name: Publish (native AOT, win-x64) + run: dotnet publish aot-smoke/Wolfgang.Etl.TestKit.AotSmoke/Wolfgang.Etl.TestKit.AotSmoke.csproj -c Release -r win-x64 + + - name: Run native binary + shell: bash + run: | + EXE="aot-smoke/Wolfgang.Etl.TestKit.AotSmoke/bin/Release/net10.0/win-x64/publish/Wolfgang.Etl.TestKit.AotSmoke.exe" + echo "Running: $EXE" + "$EXE" diff --git a/aot-smoke/.editorconfig b/aot-smoke/.editorconfig new file mode 100644 index 0000000..df8728a --- /dev/null +++ b/aot-smoke/.editorconfig @@ -0,0 +1,14 @@ +# Isolation stub — companion to Directory.Build.props/.targets in this folder. +# +# .editorconfig is discovered by walking UP the directory tree, independently of +# MSBuild's Directory.Build.props isolation, so without this the repo-root +# .editorconfig's analyzer policy (CA2007 etc.) would still apply to the +# throwaway AOT-smoke consumer. `root = true` stops that walk-up so the fixture +# is fully exempt from repo lint — it is a publish-and-run smoke fixture, not +# shipped code. +root = true + +[*.cs] +# The consumer is a console app with no synchronization context, so ConfigureAwait +# is irrelevant; silence CA2007 explicitly in case SDK defaults ever enable it. +dotnet_diagnostic.CA2007.severity = none diff --git a/aot-smoke/Directory.Build.props b/aot-smoke/Directory.Build.props new file mode 100644 index 0000000..7f8c619 --- /dev/null +++ b/aot-smoke/Directory.Build.props @@ -0,0 +1,9 @@ + + + diff --git a/aot-smoke/Directory.Build.targets b/aot-smoke/Directory.Build.targets new file mode 100644 index 0000000..2e60f34 --- /dev/null +++ b/aot-smoke/Directory.Build.targets @@ -0,0 +1,3 @@ + + + diff --git a/aot-smoke/Wolfgang.Etl.TestKit.AotSmoke/Program.cs b/aot-smoke/Wolfgang.Etl.TestKit.AotSmoke/Program.cs new file mode 100644 index 0000000..de48da9 --- /dev/null +++ b/aot-smoke/Wolfgang.Etl.TestKit.AotSmoke/Program.cs @@ -0,0 +1,60 @@ +// Native-AOT smoke consumer for Wolfgang.Etl.TestKit (#132). +// +// Exercises the public surface after native-AOT publish and returns a non-zero +// exit code if any invariant is violated, so the CI job fails if the library +// stops being AOT-safe at publish/run time. Kept deliberately simple — this is a +// smoke test, not a behavioural test suite (those live in the unit-test project). + +using Wolfgang.Etl.TestKit; + +static int Fail(string message) +{ + Console.Error.WriteLine($"AOT smoke FAILED: {message}"); + return 1; +} + +// Extractor: yields the seeded items through the async-enumerable pipeline. +var extractor = new TestExtractor(new[] { 1, 2, 3 }); +var extracted = new List(); +await foreach (var item in extractor.ExtractAsync()) +{ + extracted.Add(item); +} +if (extracted.Count != 3) +{ + return Fail($"TestExtractor yielded {extracted.Count} items, expected 3"); +} + +// Transformer: pass-through over the extracted items. +var transformer = new TestTransformer(); +var transformed = new List(); +await foreach (var item in transformer.TransformAsync(ToAsync(extracted))) +{ + transformed.Add(item); +} +if (transformed.Count != extracted.Count) +{ + return Fail($"TestTransformer yielded {transformed.Count} items, expected {extracted.Count}"); +} + +// Loader: collects everything the pipeline pushed. +var loader = new TestLoader(collectItems: true); +await loader.LoadAsync(ToAsync(transformed)); +var collected = loader.GetCollectedItems(); +if (collected is null || collected.Count != transformed.Count) +{ + return Fail($"TestLoader collected {(collected is null ? "null" : collected.Count.ToString())} items, expected {transformed.Count}"); +} + +Console.WriteLine($"AOT smoke OK: extracted {extracted.Count}, transformed {transformed.Count}, loaded {collected.Count}"); +return 0; + +static async IAsyncEnumerable ToAsync(IEnumerable items) +{ + foreach (var item in items) + { + yield return item; + } + + await Task.CompletedTask; +} diff --git a/aot-smoke/Wolfgang.Etl.TestKit.AotSmoke/Wolfgang.Etl.TestKit.AotSmoke.csproj b/aot-smoke/Wolfgang.Etl.TestKit.AotSmoke/Wolfgang.Etl.TestKit.AotSmoke.csproj new file mode 100644 index 0000000..c0d7014 --- /dev/null +++ b/aot-smoke/Wolfgang.Etl.TestKit.AotSmoke/Wolfgang.Etl.TestKit.AotSmoke.csproj @@ -0,0 +1,29 @@ + + + + + Exe + net10.0 + enable + enable + latest + true + false + false + + true + + + + + + +