diff --git a/docs/plans/0040-compono-tunit-package-design.md b/docs/plans/0040-compono-tunit-package-design.md index 14993c9..388dc89 100644 --- a/docs/plans/0040-compono-tunit-package-design.md +++ b/docs/plans/0040-compono-tunit-package-design.md @@ -519,21 +519,27 @@ Each phase ships as its own PR, per `design-decisions.md`'s phase rule. ### Phase 2: Verification requiring the completed attribute family -**Status:** Not Started +**Status:** Done -- [ ] A real packaged-consumer sample project run (mirroring PLAN-0004 +- [x] A real packaged-consumer sample project run (mirroring PLAN-0004 Phase 3 / PLAN-0005 Phase 2's precedent exactly) — extends Phase 0's own minimal local-feed consumer to exercise the *complete* attribute family (`[Compose]`/`[Compose]`/`[Compose]`/ `[Shared]`) through the actual packaged `Compono.TUnit` → `Compono` dependency chain, not `Compono.TUnit.Tests`' own `ProjectReference`- - based calls. -- [ ] Final API-surface/approval test locking `Compono.TUnit`'s complete + based calls. `test/Compono.TUnit.SampleTests/ConfigProfileTests.cs` + (new) adds the missing `[Compose]` leg, mirroring + `Compono.XunitV3.SampleTests/ConfigProfileTests.cs` exactly — the + other three forms were already covered by Phase 0/1's own sample + files (`CompositionTests.cs`, `SharedTests.cs`, `NSubstituteTests.cs`). +- [x] Final API-surface/approval test locking `Compono.TUnit`'s complete public shape (`ComposeAttribute` family, `SharedAttribute`, and nothing else), matching `Compono.XunitV3.Tests`'/ - `Compono.NSubstitute.Tests`' existing pattern — Phase 0/1 may have - already started this file for their own incrementally-shipped shape; - this phase closes it out against the full family. + `Compono.NSubstitute.Tests`' existing pattern — new + `test/Compono.TUnit.Tests/PublicApiSurfaceTests.cs`, confirming the + exact four-type public surface (`ComposeAttribute`, + `` ComposeAttribute`1``, `` ComposeAttribute`2``, `SharedAttribute`) + Phase 0/1 already shipped, with no drift. ### Phase 3: Docs and skill consistency close-out @@ -910,3 +916,27 @@ with the shipped shape and the real stacked-attribute rejection behavior. Phase 1 is complete - every task checked off, full solution build/test green. + +**Phase 2 implementation (2026-08-12)**: PR #76 (Phase 1) merged to `main` +first, per this plan's own phase-PR rule. Two tasks, both scoped +verification-only (no new `src/Compono.TUnit` public API): +`test/Compono.TUnit.SampleTests/ConfigProfileTests.cs` (new) adds the +`[Compose]` leg the packaged-consumer sample project +was still missing - `RepositoryConsumer`/`RepositoryTestProfile`/ +`RepositoryTestConfig`, mirroring `Compono.XunitV3.SampleTests +/ConfigProfileTests.cs` exactly - proving the fourth and last +attribute-family form through the real packaged `Compono.TUnit` -> +`Compono` dependency chain (the other three were already covered by +Phase 0/1's own sample files). `test/Compono.TUnit.Tests +/PublicApiSurfaceTests.cs` (new) locks the exact four-type public surface +(`ComposeAttribute`, `` ComposeAttribute`1``, `` ComposeAttribute`2``, +`SharedAttribute`) via a hand-rolled `IsPublic || IsNestedPublic` +reflection check, matching `Compono.XunitV3.Tests`' own file byte-for-byte +in structure. Both projects' full test suites pass: +`Compono.TUnit.Tests` 52/52 (net10.0, including the new API-surface +test), `Compono.TUnit.SampleTests` 7/7 (net10.0, through the real +packaged pipeline - up from 5, the two new `ConfigProfileTests` cases). +No doc updates in this phase - Phase 3 is the docs/skill closing +consistency pass, not this one. + +Phase 2 is complete - every task checked off. diff --git a/test/Compono.TUnit.SampleTests/ConfigProfileTests.cs b/test/Compono.TUnit.SampleTests/ConfigProfileTests.cs new file mode 100644 index 0000000..91b10e7 --- /dev/null +++ b/test/Compono.TUnit.SampleTests/ConfigProfileTests.cs @@ -0,0 +1,58 @@ +namespace Compono.TUnit.SampleTests; + +// A finite-choice profile configuration argument uses an enum, not a string - ADR-0036's "no +// stringly typed configuration" principle, mirroring Compono.XunitV3.SampleTests.ConfigProfileTests +// exactly (PLAN-0040 Phase 2's own instance of that scenario, proving [Compose] +// through the real packaged Compono.TUnit -> Compono dependency chain, not just +// Compono.TUnit.Tests' ProjectReference-based GetData checks). +public enum RepositoryKind +{ + Player, + Game, +} + +public sealed record RepositoryTestConfig(RepositoryKind Repository); + +// Composed only as ConfigProfileTests' own [Compose]-attributed test methods' +// parameter type - no other Compose-family use of it anywhere else in this project. Proves a +// concrete parameter type reached only this way gets a real generated plan through the packaged +// dependency chain (Compono.XunitV3.SampleTests.RepositoryConsumer's own comment records the exact +// discovery gap this shape once caught for that package - PR #65). +public sealed class RepositoryConsumer +{ + public RepositoryConsumer(string repositoryName) => RepositoryName = repositoryName; + + public string RepositoryName { get; } +} + +public sealed class RepositoryTestProfile : ICompositionProfile +{ + public RepositoryTestProfile(RepositoryTestConfig config) => Config = config; + + public RepositoryTestConfig Config { get; } + + public void Configure(CompositionBuilder builder) => + builder.Register(() => Config.Repository switch + { + RepositoryKind.Player => "player-repository", + RepositoryKind.Game => "game-repository", + _ => throw new ArgumentOutOfRangeException(nameof(Config)), + }); +} + +public sealed class ConfigProfileTests +{ + [Test] + [Compose(RepositoryKind.Player)] + public async Task ComposesTheProfileBuiltFromConfigArguments(RepositoryConsumer consumer) + { + await Assert.That(consumer.RepositoryName).IsEqualTo("player-repository"); + } + + [Test] + [Compose(RepositoryKind.Game)] + public async Task DifferentConfigArguments_ProduceADifferentlyConfiguredProfile(RepositoryConsumer consumer) + { + await Assert.That(consumer.RepositoryName).IsEqualTo("game-repository"); + } +} diff --git a/test/Compono.TUnit.Tests/PublicApiSurfaceTests.cs b/test/Compono.TUnit.Tests/PublicApiSurfaceTests.cs new file mode 100644 index 0000000..8d86b80 --- /dev/null +++ b/test/Compono.TUnit.Tests/PublicApiSurfaceTests.cs @@ -0,0 +1,30 @@ +namespace Compono.TUnit.Tests; + +// Cheap insurance against accidental public-API drift (PLAN-0040 Phase 2's final API-surface lock) - +// locks the exact set of public types Compono.TUnit exposes, matching Compono.XunitV3's own +// PublicApiSurfaceTests.cs pattern (docs/public-api.md's "keep public APIs minimal" constraint). A +// hand-rolled exact-set assertion, not a Verify snapshot - same reasoning as that file: the expected +// shape is a fixed, short list, so a snapshot would add ceremony without adding coverage. +public sealed class PublicApiSurfaceTests +{ + [Test] + public async Task Assembly_ExposesExactlyTheDocumentedPublicTypes() + { + // IsPublic alone misses an accidentally-added nested public type - only a top-level type can + // be IsPublic, a nested one is IsNestedPublic instead (Compono.XunitV3.Tests' + // PublicApiSurfaceTests.cs, PR #26 review). Checking only IsPublic would let an unapproved + // `public class Foo { public class Bar { } }` addition slip through this exact-set assertion + // undetected. + var publicTypeNames = typeof(ComposeAttribute).Assembly.GetTypes() + .Where(static type => type.IsPublic || type.IsNestedPublic) + .Select(static type => type.FullName!); + + await Assert.That(publicTypeNames).IsEquivalentTo( + [ + "Compono.TUnit.ComposeAttribute", + "Compono.TUnit.ComposeAttribute`1", + "Compono.TUnit.ComposeAttribute`2", + "Compono.TUnit.SharedAttribute", + ]); + } +}