From b221121c5477b3011170eb09598e12dce275f51e Mon Sep 17 00:00:00 2001 From: Chrison Simtian Date: Thu, 28 May 2026 17:23:50 +1200 Subject: [PATCH] test(consumers): add NUKE + Fallout consumer compatibility projects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three small build-project consumers exercise Fallout's public surface from real downstream-user perspectives. Compile-time sentinels for breaking changes — if any of them stops compiling, we've broken something consumers depend on. 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 limitation (SHIM002). - tests/Consumers/Fallout.Consumer.Local/ — Fallout consumer using direct ProjectReferences to this repo's local source. Tracks HEAD, catches breakage in the current PR. - tests/Consumers/Fallout.Consumer.NuGet/ — Fallout consumer against the last published packages (pinned to 11.0.8, pre-rename namespaces). `ReplacePackageReferences=false` and central package management disabled so PackageReferences 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 the full build-app 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/build.schema.json | 52 +------------------ fallout.slnx | 3 ++ .../Consumers/Fallout.Consumer.Local/Build.cs | 26 ++++++++++ .../Fallout.Consumer.Local.csproj | 27 ++++++++++ .../Consumers/Fallout.Consumer.NuGet/Build.cs | 27 ++++++++++ .../Fallout.Consumer.NuGet.csproj | 33 ++++++++++++ tests/Consumers/Nuke.Consumer/Build.cs | 35 +++++++++++++ .../Nuke.Consumer/Nuke.Consumer.csproj | 18 +++++++ tests/Consumers/README.md | 43 +++++++++++++++ ...nGeneratorTest.Test#Solution.g.verified.cs | 3 ++ 10 files changed, 216 insertions(+), 51 deletions(-) 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/build.schema.json b/.fallout/build.schema.json index 47c6a4f95..a810b8e0e 100644 --- a/.fallout/build.schema.json +++ b/.fallout/build.schema.json @@ -24,24 +24,7 @@ "ExecutableTarget": { "type": "string", "enum": [ - "CheckoutExternalRepositories", - "Clean", - "Compile", - "CreateGitHubRelease", - "DeletePackages", - "DownloadLicenses", - "GeneratePublicApi", - "GenerateTools", - "Install", - "Pack", - "Publish", - "References", - "ReportCoverage", - "Restore", - "RunTargetInDockerImageTest", - "Test", - "UpdateContributors", - "UpdateStargazers" + "Default" ] }, "Verbosity": { @@ -122,42 +105,9 @@ "allOf": [ { "properties": { - "CodecovToken": { - "type": "string", - "default": "Secrets must be entered via 'nuke :secrets [profile]'" - }, - "Configuration": { - "type": "string", - "enum": [ - "Debug", - "Release" - ] - }, - "GitHubReleaseGitHubToken": { - "type": "string", - "default": "Secrets must be entered via 'nuke :secrets [profile]'" - }, - "IgnoreFailedSources": { - "type": "boolean", - "description": "Ignore unreachable sources during Restore" - }, - "Major": { - "type": "boolean" - }, - "NuGetApiKey": { - "type": "string", - "default": "Secrets must be entered via 'nuke :secrets [profile]'" - }, "Solution": { "type": "string", "description": "Path to a solution file that is automatically loaded" - }, - "TestDegreeOfParallelism": { - "type": "integer", - "format": "int32" - }, - "UseHttps": { - "type": "boolean" } } }, diff --git a/fallout.slnx b/fallout.slnx index d40d8e0f9..0c05b8183 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..6b97cee6e --- /dev/null +++ b/tests/Consumers/Fallout.Consumer.Local/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 this repo's local source (smart-rewrite makes the +// PackageReferences resolve to ProjectReferences). Catches breakage of the +// public Fallout surface in the current PR. + +using Fallout.Common; +using Fallout.Common.IO; +using Fallout.Solutions; + +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..0f1d18b91 --- /dev/null +++ b/tests/Consumers/Fallout.Consumer.Local/Fallout.Consumer.Local.csproj @@ -0,0 +1,27 @@ + + + + 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..d56b121c3 --- /dev/null +++ b/tests/Consumers/Fallout.Consumer.NuGet/Build.cs @@ -0,0 +1,27 @@ +// 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. Pre-#254 namespaces (Fallout.Common.ProjectModel) +// because the pinned 11.0.8 release predates the rename. + +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..3cab9aab2 --- /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..4bd294765 --- /dev/null +++ b/tests/Consumers/README.md @@ -0,0 +1,43 @@ +# 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 `src/Fallout.*/` and `src/Persistence/Fallout.Solution/` | +| [`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? + +Initial design had a `Consumers.SmokeTests/` xUnit project that spawned each consumer via `dotnet run` and asserted exit code `0`. That approach hits a Fallout framework runtime 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 a `System.ArgumentException: Host type 'Host' defines no property 'IsRunningHost'` at activation. Reproducing the full build-app environment for a smoke consumer is meaningfully more setup than the test's value justifies; for now the consumer projects are compile-only sentinels. + +If we ever need runtime validation: 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. Not done in this initial pass because the compile-time check already catches the bulk of breaking changes. + +## 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, IPack`** — 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 a1dc56a0d..06eae0c4d 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.Soluti public Fallout.Solutions.Project Fallout_Common => this.GetProject("Fallout.Common"); public Fallout.Solutions.Project Fallout_Common_Tests => this.GetProject("Fallout.Common.Tests"); public Fallout.Solutions.Project Fallout_Components => this.GetProject("Fallout.Components"); + public Fallout.Solutions.Project Fallout_Consumer_Local => this.GetProject("Fallout.Consumer.Local"); + public Fallout.Solutions.Project Fallout_Consumer_NuGet => this.GetProject("Fallout.Consumer.NuGet"); public Fallout.Solutions.Project Fallout_Migrate => this.GetProject("Fallout.Migrate"); public Fallout.Solutions.Project Fallout_Migrate_Analyzers => this.GetProject("Fallout.Migrate.Analyzers"); public Fallout.Solutions.Project Fallout_Migrate_Analyzers_Tests => this.GetProject("Fallout.Migrate.Analyzers.Tests"); @@ -44,6 +46,7 @@ internal class Solution(SolutionModel model, AbsolutePath path) : Fallout.Soluti public Fallout.Solutions.Project Nuke_Common_Shim_Tests => this.GetProject("Nuke.Common.Shim.Tests"); public Fallout.Solutions.Project Nuke_Components => this.GetProject("Nuke.Components"); public Fallout.Solutions.Project Nuke_Components_Shim_Tests => this.GetProject("Nuke.Components.Shim.Tests"); + public Fallout.Solutions.Project Nuke_Consumer => this.GetProject("Nuke.Consumer"); public _misc misc => Unsafe.As<_misc>(this.GetSolutionFolder("misc"));