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
3 changes: 3 additions & 0 deletions fallout.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@
<Project Path="tests\Fallout.ProjectModel.Tests\Fallout.ProjectModel.Tests.csproj" />
<Project Path="tests\Fallout.SolutionModel.Tests\Fallout.SolutionModel.Tests.csproj" />
<Project Path="tests\Fallout.SourceGenerators.Tests\Fallout.SourceGenerators.Tests.csproj" />
<Project Path="tests\Consumers\Nuke.Consumer\Nuke.Consumer.csproj" />
<Project Path="tests\Consumers\Fallout.Consumer.Local\Fallout.Consumer.Local.csproj" />
<Project Path="tests\Consumers\Fallout.Consumer.NuGet\Fallout.Consumer.NuGet.csproj" />
<Project Path="tests\Fallout.Tooling.Tests\Fallout.Tooling.Tests.csproj" />
<Project Path="tests\Fallout.Utilities.Tests\Fallout.Utilities.Tests.csproj" />
</Solution>
25 changes: 25 additions & 0 deletions tests/Consumers/Fallout.Consumer.Local/Build.cs
Original file line number Diff line number Diff line change
@@ -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<Build>(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 ?? "<unbound>");
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<OutputType>Exe</OutputType>
<IsPackable>false</IsPackable>
<RootNamespace>Fallout.Consumer.Local</RootNamespace>
<NoWarn>$(NoWarn);CS0649</NoWarn>
</PropertyGroup>

<!--
Direct ProjectReferences against this repo's local source. Always tracks
HEAD. Catches breakage in the current PR.

NOTE: this references Fallout.SolutionModel (current main shape). When the
persistence-layering work lands (#248/#254), this csproj will need to
point at the new location (src/Persistence/Fallout.Solution/) and the
Build.cs `using Fallout.Common.ProjectModel;` will need to become
`using Fallout.Solutions;`. The PR doing that rename owns both updates;
see tests/Consumers/README.md → "Catching breaking changes".
-->
<ItemGroup>
<ProjectReference Include="..\..\..\src\Fallout.Common\Fallout.Common.csproj" />
<ProjectReference Include="..\..\..\src\Fallout.Build\Fallout.Build.csproj" />
<ProjectReference Include="..\..\..\src\Fallout.SolutionModel\Fallout.SolutionModel.csproj" />
<ProjectReference Include="..\..\..\src\Fallout.Components\Fallout.Components.csproj" />
</ItemGroup>

</Project>
26 changes: 26 additions & 0 deletions tests/Consumers/Fallout.Consumer.NuGet/Build.cs
Original file line number Diff line number Diff line change
@@ -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<Build>(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 ?? "<unbound>");
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<OutputType>Exe</OutputType>
<IsPackable>false</IsPackable>
<RootNamespace>Fallout.Consumer.NuGet</RootNamespace>
<NoWarn>$(NoWarn);CS0649</NoWarn>

<!-- Opt out of Directory.Build.targets' smart-rewrite so PackageReferences
actually resolve against nuget.org instead of in-solution ProjectReferences.
This is the whole point of this project: validate against the LAST PUBLISHED
package surface, not the current source tree.

Also opt out of central package management so the inline Version="..."
pins below take effect. -->
<ReplacePackageReferences>false</ReplacePackageReferences>
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
</PropertyGroup>

<!--
Pinned to the last release before any in-flight breaking change.
BUMP THIS after each release to catch upgrade-direction breaking changes.
See tests/Consumers/README.md → "Bumping the NuGet pin".
-->
<ItemGroup>
<PackageReference Include="Fallout.Common" Version="11.0.8" />
<PackageReference Include="Fallout.Build" Version="11.0.8" />
<PackageReference Include="Fallout.SolutionModel" Version="11.0.8" />
<PackageReference Include="Fallout.Components" Version="11.0.8" />
</ItemGroup>

</Project>
35 changes: 35 additions & 0 deletions tests/Consumers/Nuke.Consumer/Build.cs
Original file line number Diff line number Diff line change
@@ -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<Build>(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 ?? "<unbound>");
});
}
18 changes: 18 additions & 0 deletions tests/Consumers/Nuke.Consumer/Nuke.Consumer.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<OutputType>Exe</OutputType>
<IsPackable>false</IsPackable>
<RootNamespace>Nuke.Consumer</RootNamespace>
<!-- Build.cs uses readonly fields populated by Fallout's value injection. -->
<NoWarn>$(NoWarn);CS0649</NoWarn>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\src\Shims\Nuke.Common\Nuke.Common.csproj" />
<ProjectReference Include="..\..\..\src\Shims\Nuke.Build\Nuke.Build.csproj" />
<ProjectReference Include="..\..\..\src\Shims\Nuke.Components\Nuke.Components.csproj" />
</ItemGroup>

</Project>
53 changes: 53 additions & 0 deletions tests/Consumers/README.md
Original file line number Diff line number Diff line change
@@ -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 `<ReplacePackageReferences>false</ReplacePackageReferences>` and `<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>` 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] — <next-major>`, 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] — <next-major>`.

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<Build>(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.
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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"));

Expand Down
Loading