From 0001344ff37b9b1a609da96f1b4d6373f8684a07 Mon Sep 17 00:00:00 2001 From: Chrison Simtian Date: Thu, 28 May 2026 17:41:20 +1200 Subject: [PATCH] test(consumers): add NUKE + Fallout consumer compatibility sentinels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three small build-project consumers under tests/Consumers/ that exercise Fallout's current public surface from real downstream-user perspectives. These compile against THIS commit's API shape — so any future PR that breaks consumer-facing types/namespaces/attributes will conflict with the consumer code on rebase/merge, surfacing the breakage at PR-time. Projects: - tests/Consumers/Nuke.Consumer/ — pre-rename NUKE consumer using `class Build : NukeBuild`, `[Solution]`, `Target` via the Nuke.Common / Nuke.Build / Nuke.Components transition shims. Includes a documented `using Target = Fallout.Common.Target;` alias because the shim generator skips delegates by C# language limit (SHIM002). - tests/Consumers/Fallout.Consumer.Local/ — Fallout consumer using direct ProjectReferences to this repo's local source. Tracks HEAD; catches breakage of the in-repo Fallout surface. - tests/Consumers/Fallout.Consumer.NuGet/ — Fallout consumer against the last published packages (pinned to 11.0.8) with ReplacePackageReferences=false + central package management off, so PackageReferences actually resolve against nuget.org. Catches packaging issues + upgrade-direction breakage when bumped after a release. All three are in fallout.slnx — `dotnet build fallout.slnx` (the existing CI gate) validates them. Compile failure ⇒ a consumer-facing API broke. Runtime smoke tests not included: spawning the consumers via `dotnet run` fails because Fallout's FalloutBuild.cctor() requires the full Fallout.Common.props/targets environment (Host activation reflects for IsRunningHost property and throws on minimal consumers). Reproducing that environment is more setup than the test's marginal value justifies. README documents this and points at how to revisit if needed. Co-Authored-By: Claude Opus 4.7 (1M context) --- fallout.slnx | 3 ++ .../Consumers/Fallout.Consumer.Local/Build.cs | 25 +++++++++ .../Fallout.Consumer.Local.csproj | 29 ++++++++++ .../Consumers/Fallout.Consumer.NuGet/Build.cs | 26 +++++++++ .../Fallout.Consumer.NuGet.csproj | 33 ++++++++++++ tests/Consumers/Nuke.Consumer/Build.cs | 35 ++++++++++++ .../Nuke.Consumer/Nuke.Consumer.csproj | 18 +++++++ tests/Consumers/README.md | 53 +++++++++++++++++++ ...nGeneratorTest.Test#Solution.g.verified.cs | 3 ++ 9 files changed, 225 insertions(+) create mode 100644 tests/Consumers/Fallout.Consumer.Local/Build.cs create mode 100644 tests/Consumers/Fallout.Consumer.Local/Fallout.Consumer.Local.csproj create mode 100644 tests/Consumers/Fallout.Consumer.NuGet/Build.cs create mode 100644 tests/Consumers/Fallout.Consumer.NuGet/Fallout.Consumer.NuGet.csproj create mode 100644 tests/Consumers/Nuke.Consumer/Build.cs create mode 100644 tests/Consumers/Nuke.Consumer/Nuke.Consumer.csproj create mode 100644 tests/Consumers/README.md diff --git a/fallout.slnx b/fallout.slnx index f8b402d0f..e0a18d91a 100644 --- a/fallout.slnx +++ b/fallout.slnx @@ -43,6 +43,9 @@ + + + diff --git a/tests/Consumers/Fallout.Consumer.Local/Build.cs b/tests/Consumers/Fallout.Consumer.Local/Build.cs new file mode 100644 index 000000000..2506d313d --- /dev/null +++ b/tests/Consumers/Fallout.Consumer.Local/Build.cs @@ -0,0 +1,25 @@ +// Copyright 2026 Maintainers of Fallout. +// Originally based on NUKE by Matthias Koch and contributors. +// Distributed under the MIT License. +// https://github.com/ChrisonSimtian/Fallout/blob/main/LICENSE +// +// Fallout consumer against this repo's local source. Catches breakage of the +// public Fallout surface in the current PR. + +using Fallout.Common; +using Fallout.Common.IO; +using Fallout.Common.ProjectModel; + +class Build : FalloutBuild +{ + public static int Main() => Execute(x => x.Default); + + [Solution] readonly Solution Solution; + + Target Default => _ => _ + .Executes(() => + { + Serilog.Log.Information("hello from fallout consumer (local source)"); + Serilog.Log.Information("solution name: {Name}", Solution?.Name ?? ""); + }); +} diff --git a/tests/Consumers/Fallout.Consumer.Local/Fallout.Consumer.Local.csproj b/tests/Consumers/Fallout.Consumer.Local/Fallout.Consumer.Local.csproj new file mode 100644 index 000000000..0ff56655b --- /dev/null +++ b/tests/Consumers/Fallout.Consumer.Local/Fallout.Consumer.Local.csproj @@ -0,0 +1,29 @@ + + + + net10.0 + Exe + false + Fallout.Consumer.Local + $(NoWarn);CS0649 + + + + + + + + + + + diff --git a/tests/Consumers/Fallout.Consumer.NuGet/Build.cs b/tests/Consumers/Fallout.Consumer.NuGet/Build.cs new file mode 100644 index 000000000..3a7cd83f3 --- /dev/null +++ b/tests/Consumers/Fallout.Consumer.NuGet/Build.cs @@ -0,0 +1,26 @@ +// Copyright 2026 Maintainers of Fallout. +// Originally based on NUKE by Matthias Koch and contributors. +// Distributed under the MIT License. +// https://github.com/ChrisonSimtian/Fallout/blob/main/LICENSE +// +// Fallout consumer against PUBLISHED Fallout.* packages (pinned in the csproj). +// Validates that the most-recent release's surface is intact from a clean +// consumer's perspective. + +using Fallout.Common; +using Fallout.Common.IO; +using Fallout.Common.ProjectModel; + +class Build : FalloutBuild +{ + public static int Main() => Execute(x => x.Default); + + [Solution] readonly Solution Solution; + + Target Default => _ => _ + .Executes(() => + { + Serilog.Log.Information("hello from fallout consumer (pinned nuget 11.0.8)"); + Serilog.Log.Information("solution name: {Name}", Solution?.Name ?? ""); + }); +} diff --git a/tests/Consumers/Fallout.Consumer.NuGet/Fallout.Consumer.NuGet.csproj b/tests/Consumers/Fallout.Consumer.NuGet/Fallout.Consumer.NuGet.csproj new file mode 100644 index 000000000..f753fbdf7 --- /dev/null +++ b/tests/Consumers/Fallout.Consumer.NuGet/Fallout.Consumer.NuGet.csproj @@ -0,0 +1,33 @@ + + + + net10.0 + Exe + false + Fallout.Consumer.NuGet + $(NoWarn);CS0649 + + + false + false + + + + + + + + + + + diff --git a/tests/Consumers/Nuke.Consumer/Build.cs b/tests/Consumers/Nuke.Consumer/Build.cs new file mode 100644 index 000000000..21c081670 --- /dev/null +++ b/tests/Consumers/Nuke.Consumer/Build.cs @@ -0,0 +1,35 @@ +// Copyright 2026 Maintainers of Fallout. +// Originally based on NUKE by Matthias Koch and contributors. +// Distributed under the MIT License. +// https://github.com/ChrisonSimtian/Fallout/blob/main/LICENSE +// +// Pre-rename NUKE consumer pattern, compiled against the Nuke.Common / +// Nuke.Components transition shims. If a typical NUKE 10.x Build.cs stops +// compiling against the latest Fallout, this fails — protecting upgrading +// users from silent breakage. + +using Nuke.Common; +using Nuke.Common.IO; +using Nuke.Common.ProjectModel; +using Nuke.Components; + +// The shim generator skips delegates by C# language limitation (see SHIM002 — +// can't subclass a delegate cross-assembly). `Target` is a delegate in +// Fallout.Common, so NUKE-era code referencing `Target` needs either +// `fallout-migrate` (which flips usings to Fallout.*) or this manual alias. +// Including it here keeps the rest of the file NUKE-shape. +using Target = Fallout.Common.Target; + +class Build : NukeBuild +{ + public static int Main() => Execute(x => x.Default); + + [Solution] readonly Solution Solution; + + Target Default => _ => _ + .Executes(() => + { + Serilog.Log.Information("hello from nuke consumer (via shim)"); + Serilog.Log.Information("solution name: {Name}", Solution?.Name ?? ""); + }); +} diff --git a/tests/Consumers/Nuke.Consumer/Nuke.Consumer.csproj b/tests/Consumers/Nuke.Consumer/Nuke.Consumer.csproj new file mode 100644 index 000000000..1a5744333 --- /dev/null +++ b/tests/Consumers/Nuke.Consumer/Nuke.Consumer.csproj @@ -0,0 +1,18 @@ + + + + net10.0 + Exe + false + Nuke.Consumer + + $(NoWarn);CS0649 + + + + + + + + + diff --git a/tests/Consumers/README.md b/tests/Consumers/README.md new file mode 100644 index 000000000..f75febc7d --- /dev/null +++ b/tests/Consumers/README.md @@ -0,0 +1,53 @@ +# Consumer compatibility tests + +Three small build-project consumers that exercise Fallout's public surface from the perspectives of real downstream users. If any of these stop **compiling**, we've broken something consumers depend on. + +## Projects + +| Project | What it exercises | Reference style | +|---|---|---| +| [`Nuke.Consumer`](Nuke.Consumer/) | A pre-rename NUKE consumer using `class Build : NukeBuild`, `[Solution]`, and `Target` via the `Nuke.Common` / `Nuke.Build` / `Nuke.Components` transition shims. Catches breakage of **shim coverage** — if a NUKE-shape symbol stops resolving, an upgrading NUKE user breaks. | `ProjectReference` to `src/Shims/Nuke.*/` | +| [`Fallout.Consumer.Local`](Fallout.Consumer.Local/) | A Fallout consumer using `class Build : FalloutBuild`, `[Solution]`, `Target` against **this repo's current source**. Catches breakage of the public Fallout surface **in the current PR**. | Direct `ProjectReference` to the in-repo `Fallout.*` projects | +| [`Fallout.Consumer.NuGet`](Fallout.Consumer.NuGet/) | A Fallout consumer against the **last published** `Fallout.*` packages (pinned in the csproj). Catches **packaging issues** (missing assemblies, wrong references) on the most-recent release, and catches upgrade-direction breakage when the pin is bumped after a release. | `PackageReference` to nuget.org with `false` and `false` so the smart-rewrite doesn't kick in | + +## How they're validated + +**Compile-time validation only.** All three projects are in `fallout.slnx`, so `dotnet build fallout.slnx` (the standard CI gate) compiles them. Any consumer-facing breaking change makes the build fail. + +### Why not runtime validation? + +A runtime smoke test layer was considered (spawn each consumer via `dotnet run`, assert exit code `0`). That hits a Fallout framework requirement: `FalloutBuild.cctor()` enumerates `Host` subclasses and reflects for a static `IsRunningHost` property — minimal consumers that don't pull in the full Fallout MSBuild props/targets (`Fallout.Common.props`, `Fallout.Common.targets`) trigger `System.ArgumentException: Host type 'Host' defines no property 'IsRunningHost'` at activation. Reproducing the full build-app environment for a smoke consumer is more setup than the test's marginal value justifies — compile-time already catches the bulk of breaking changes. + +If runtime validation becomes worth the cost: import `Fallout.Common.props` + `Fallout.Common.targets` from the consumer csprojs (the way `build/_build.csproj` does), provide a `FalloutRootDirectory`, and the framework should activate cleanly. + +## Catching breaking changes + +The intended flow: + +1. These consumer projects live on `main` and reflect the **current public consumer surface**. Any PR proposing a change to consumer-facing types/namespaces/attributes… +2. …will conflict with the consumer code in those projects (rebase or merge will surface the breakage). +3. Resolving the conflict means either: (a) the change isn't really breaking and the conflict is trivial, or (b) the consumer code needs updating to the new shape, **which is the migration path**. Update both, document the migration in `CHANGELOG.md` under `[Unreleased] — `, and you've simultaneously detected the breaking change AND demonstrated how consumers migrate. + +This works as designed only because the consumer code is **fixed to the current API shape**, not regenerated to match new code. Don't auto-update consumer Build.cs files to match a PR's renamed types — let them break, then fix them deliberately as part of the migration story. + +## Bumping the NuGet pin + +After a new `Fallout.*` release ships, edit `Fallout.Consumer.NuGet/Fallout.Consumer.NuGet.csproj` to bump the pinned version. If the consumer source still compiles, the upgrade is non-breaking. If it fails, the new release introduces a consumer-facing breaking change — record it in `CHANGELOG.md` and the migration path under `[Unreleased] — `. + +The `Nuke.Consumer` doesn't pin anything (it references the in-repo shim assemblies directly), so no bump cadence there — it always tracks HEAD's shim coverage. + +## What's deliberately tested + +- **NUKE-era `class Build : NukeBuild`** — basic class identity through the shim +- **`[Solution] readonly Solution Solution`** — value-injection attribute + facade type via shim +- **`Target Default => _ => _.Executes(...)`** — delegate type, default target lambda +- **`static int Main() => Execute(x => x.Default)`** — framework entry point + +## What's NOT tested + +- Actual build *execution* (see above — runtime activation is fragile) +- CI-host integration (GitHubActions, AzurePipelines, etc.) — covered by `tests/Fallout.Common.Tests/CI/` +- Tool wrappers — covered by their own generated tests +- Source generator behaviour — covered by `tests/Fallout.SourceGenerators.Tests/` + +These consumer projects are a **sentinel for shape changes**, not a place to demo features. Don't add consumer projects covering specific subsystems — those go in their own focused tests. diff --git a/tests/Fallout.SourceGenerators.Tests/StronglyTypedSolutionGeneratorTest.Test#Solution.g.verified.cs b/tests/Fallout.SourceGenerators.Tests/StronglyTypedSolutionGeneratorTest.Test#Solution.g.verified.cs index 92fb07118..b5cc90b84 100644 --- a/tests/Fallout.SourceGenerators.Tests/StronglyTypedSolutionGeneratorTest.Test#Solution.g.verified.cs +++ b/tests/Fallout.SourceGenerators.Tests/StronglyTypedSolutionGeneratorTest.Test#Solution.g.verified.cs @@ -17,6 +17,8 @@ internal class Solution(SolutionModel model, AbsolutePath path) : Fallout.Common public Fallout.Common.ProjectModel.Project Fallout_Common => this.GetProject("Fallout.Common"); public Fallout.Common.ProjectModel.Project Fallout_Common_Tests => this.GetProject("Fallout.Common.Tests"); public Fallout.Common.ProjectModel.Project Fallout_Components => this.GetProject("Fallout.Components"); + public Fallout.Common.ProjectModel.Project Fallout_Consumer_Local => this.GetProject("Fallout.Consumer.Local"); + public Fallout.Common.ProjectModel.Project Fallout_Consumer_NuGet => this.GetProject("Fallout.Consumer.NuGet"); public Fallout.Common.ProjectModel.Project Fallout_Migrate => this.GetProject("Fallout.Migrate"); public Fallout.Common.ProjectModel.Project Fallout_Migrate_Analyzers => this.GetProject("Fallout.Migrate.Analyzers"); public Fallout.Common.ProjectModel.Project Fallout_Migrate_Analyzers_Tests => this.GetProject("Fallout.Migrate.Analyzers.Tests"); @@ -44,6 +46,7 @@ internal class Solution(SolutionModel model, AbsolutePath path) : Fallout.Common public Fallout.Common.ProjectModel.Project Nuke_Common_Shim_Tests => this.GetProject("Nuke.Common.Shim.Tests"); public Fallout.Common.ProjectModel.Project Nuke_Components => this.GetProject("Nuke.Components"); public Fallout.Common.ProjectModel.Project Nuke_Components_Shim_Tests => this.GetProject("Nuke.Components.Shim.Tests"); + public Fallout.Common.ProjectModel.Project Nuke_Consumer => this.GetProject("Nuke.Consumer"); public _misc misc => Unsafe.As<_misc>(this.GetSolutionFolder("misc"));