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
44 changes: 37 additions & 7 deletions docs/plans/0040-compono-tunit-package-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<TProfile>]`/`[Compose<TProfile, TConfig>]`/
`[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<TProfile, TConfig>]` 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

Expand Down Expand Up @@ -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<TProfile, TConfig>]` 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.
58 changes: 58 additions & 0 deletions test/Compono.TUnit.SampleTests/ConfigProfileTests.cs
Original file line number Diff line number Diff line change
@@ -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<TProfile, TConfig>]
// 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<TProfile, TConfig>]-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<RepositoryTestProfile, RepositoryTestConfig>(RepositoryKind.Player)]
public async Task ComposesTheProfileBuiltFromConfigArguments(RepositoryConsumer consumer)
{
await Assert.That(consumer.RepositoryName).IsEqualTo("player-repository");
}

[Test]
[Compose<RepositoryTestProfile, RepositoryTestConfig>(RepositoryKind.Game)]
public async Task DifferentConfigArguments_ProduceADifferentlyConfiguredProfile(RepositoryConsumer consumer)
{
await Assert.That(consumer.RepositoryName).IsEqualTo("game-repository");
}
}
30 changes: 30 additions & 0 deletions test/Compono.TUnit.Tests/PublicApiSurfaceTests.cs
Original file line number Diff line number Diff line change
@@ -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",
]);
}
}