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"));