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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
17 changes: 17 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ members = [

"crates/tests/cgp-tests",
"crates/tests/cgp-macro-tests",
"crates/tests/cgp-test-crate-a",
"crates/tests/cgp-test-crate-b",
]

[workspace.package]
Expand Down Expand Up @@ -74,6 +76,9 @@ cgp-macro = { version = "0.7.0", path = "./crates/macros/cgp-m
cgp-macro-core = { version = "0.7.0", path = "./crates/macros/cgp-macro-core" }
cgp-macro-lib = { version = "0.7.0", path = "./crates/macros/cgp-macro-lib" }
cgp-macro-test-util = { version = "0.7.0", path = "./crates/macros/cgp-macro-test-util" }

cgp-test-crate-a = { version = "0.7.0", path = "./crates/tests/cgp-test-crate-a" }
cgp-test-crate-b = { version = "0.7.0", path = "./crates/tests/cgp-test-crate-b" }
cgp-macro-test-util-lib = { version = "0.7.0", path = "./crates/macros/cgp-macro-test-util-lib" }
cgp-extra-macro = { version = "0.7.0", path = "./crates/macros/cgp-extra-macro" }
cgp-extra-macro-lib = { version = "0.7.0", path = "./crates/macros/cgp-extra-macro-lib" }
153 changes: 153 additions & 0 deletions crates/tests/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# CLAUDE.md — maintaining the CGP test suite

This file governs the test crates under `crates/tests`. Read it before adding,
moving, or refactoring any test here. Invoke the `/cgp` skill first — every test
in this tree is CGP code, and the skill is the authoritative source for CGP
semantics and vocabulary.

The test suite has three jobs, split across crates:

- **`cgp-tests`** is the main suite: realistic example code that must **compile and
run**. A passing test is often just successful compilation, because much of CGP
is compile-time wiring. This is where behavior is verified and where the
user-facing macros are exercised end-to-end.
- **`cgp-macro-tests`** tests the **internals** of the CGP macros by calling the
functions in `cgp-macro-core` directly (parsers, AST types), and is the home for
**failure cases** — inputs CGP should reject, and cases where a macro currently
emits invalid or wrong code.
- **`cgp-test-crate-a` / `cgp-test-crate-b`** are auxiliary packages for
**cross-crate** behavior: whether a downstream crate can extend a namespace or
provide a provider for a component defined elsewhere, under Rust's coherence and
orphan rules.

## Organize by concept, not by construct

Group tests by the **CGP concept or feature** under test, never by the macro that
happens to appear. A single construct such as `delegate_components!` serves many
concepts — basic delegation, `open` dispatch, namespace headers, `UseDelegate`
tables — so a bucket named after the construct mixes unrelated concerns and hides
what is actually being verified. Name each group for the concept: `basic_delegation`,
`abstract_types`, `implicit_arguments`, `namespaces`, `higher_order_providers`, and
so on.

The right granularity is driven by the feature, its implementation complexity, and
how many cases are needed to cover it exhaustively — **not** by mirroring the
concept documents under `docs/concepts/`. The names may coincide, but the split is
chosen for coverage. **When a category accumulates too many test cases to stay
coherent, split it into finer categories** rather than letting it sprawl; prefer
splitting early.

## A test target is a "sub-crate"

Each concept is one **integration test target**, which Cargo compiles as its own
crate — so each concept has its own coherence scope, exactly like a separate crate.
A target is two things:

- an **entrypoint file** `tests/<concept>_tests.rs` — the `_tests` suffix marks it
as the target root; it carries a module doc comment, `#![allow(dead_code)]` when
the target is mostly compile-time wiring, and a single `pub mod <concept>;`;
- a **module directory** `tests/<concept>/` — the clean concept name — whose
`mod.rs` lists the unit-test modules, one `pub mod` per file.

`basic_delegation` is the reference implementation of this layout — copy its shape
when adding a concept.

## One unit test per file

Put each unit test in its own `.rs` file under the concept directory, and make the
file **self-contained**: define its own components, providers, and context types at
module scope. Do **not** separate unrelated units with `#[test]` functions or nested
`mod`s inside one file. CGP tests are dominated by type-level constructs and
compile-time wiring that live at module scope and cannot be isolated by a function
boundary; separate files are the only reliable isolation within a target. A file may
still contain a `#[test]` fn for its runtime assertions, plus the module-scope items
that test exercises.

## Explain what each test covers

Open every test file with a brief comment stating **what behavior it exercises**,
and annotate individual tricky cases inline. Where it helps a reader, link to the
owning reference document (for example `// see docs/reference/macros/cgp_impl.md`).
Tests link **to** the documentation; the documentation never links back to a test
(per `docs/CLAUDE.md`).

## Use macro snapshots sparingly

`cgp-macro-test-util` provides `snapshot_*!` macros (`snapshot_cgp_component!`,
`snapshot_cgp_impl!`, `snapshot_delegate_components!`, …). Each **emits the real
generated code** into the module *and* generates a `#[test]` that asserts a
pretty-printed inline `insta` snapshot of it — so adding or removing a snapshot
never changes the compile/runtime coverage, only the golden assertion. Always keep
the snapshot string **inline** in the file (`@"…"`).

The rule for when to snapshot: **snapshot a macro only in the concept target that
owns that macro's feature; everywhere else invoke the macro plainly.** Concretely,
each macro has one canonical full-expansion snapshot (plus snapshots for its
genuinely distinct variants) in its owning target, and nowhere else:

| Macro | Owning target(s) |
| --- | --- |
| `#[cgp_component]` | `basic_delegation` (+ generic variant in `generic_components`) |
| `#[cgp_impl]` | `basic_delegation` (+ `higher_order_providers`, `implicit_arguments` variants) |
| `#[cgp_type]` | `abstract_types` |
| `#[cgp_getter]` / `#[cgp_auto_getter]` | `getters` |
| `#[cgp_fn]` | `implicit_arguments`, `impl_side_dependencies` |
| `delegate_components!` | `basic_delegation` (basic), `namespaces` (open/namespace), `dispatching` (`UseDelegate`) |
| `check_components!` / `delegate_and_check_components!` | `checking` |
| `cgp_namespace!` | `namespaces` |
| `#[blanket_trait]` | `blanket_traits` |
| `#[derive(HasField)]` / `HasFields` / `CgpData` | `field_access` / `extensible_records` / `extensible_variants` |

When a file uses one of these macros as **incidental scaffolding** — a
`#[cgp_component]` needed to set up a `delegate_components!` test, say — write the
plain macro, not the snapshot form. The expansion is already pinned in the owning
target, and a redundant snapshot only adds golden output that breaks on unrelated
macro changes.

## Adding a failure case (in `cgp-macro-tests`)

CGP will have corner cases it does not yet handle. Do **not** try to fix them inline
while refactoring; capture them as failing-behavior tests instead, in a dedicated
failure-case target:

- **Input that should be rejected** — assert the `cgp-macro-core` parser rejects it,
using the `assert_rejects` helper pattern (see `ident_with_type_params`).
- **A macro that emits invalid Rust** — capture the expanded code as an `insta`
inline snapshot (the snapshot is a *string*, so it compiles even though the code
would not), and add a code comment explaining **why** the output is wrong and
**what the correct output should be**.

Every failure case must also be recorded in the reference document that owns the
construct, under its `## Known issues` section (the heading `docs/CLAUDE.md`
mandates), describing the behavior without referring to the test. Put a link from
the test's comment to that reference document.

## Keep the docs in sync

This suite is one of the four views of CGP's truth, alongside the macro
implementation in `cgp-macro-core`, the reference documents in `docs/reference`, and
the `/cgp` skill (see `docs/CLAUDE.md`). When a test reveals or pins a behavior
worth documenting, update the reference document to explain that behavior directly —
without referring to the test. When you move a test that a reference document's
`## Source` section links to, update the link in the same change.

## Running the suite

```
cargo nextest run -p cgp-tests # the main suite
cargo nextest run -p cgp-macro-tests # macro internals + failures
cargo nextest run --workspace # everything

cargo insta test -p cgp-tests --review # review snapshot diffs
cargo insta test -p cgp-tests --accept # accept intended snapshot changes
```

A snapshot test that fails prints a diff of the generated code; accept it with
`cargo insta` only after confirming the change is intended.

## Migration status

The suite was reorganized from a by-construct layout to this by-concept layout. As
categories grow, keep splitting them per the rule above, and keep expanding failure
coverage in `cgp-macro-tests` and cross-crate coverage in the `cgp-test-crate-*`
packages — these were established with representative cases and are meant to grow.
72 changes: 72 additions & 0 deletions crates/tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# CGP test suite

This directory holds the test suite for Context-Generic Programming. The tests
are organized **by CGP concept** — basic delegation, abstract types, implicit
arguments, namespaces, and so on — rather than by the macro that implements each
concept, because a single macro (for example `delegate_components!`) serves many
concepts at once. If you are maintaining or extending the suite, read
[CLAUDE.md](CLAUDE.md) first; it is the authoritative guide to the conventions.
This README is the map.

## The crates

The suite is split into three kinds of crate, each with a distinct job.

**`cgp-tests`** is the main suite: realistic example code that must compile and
run. Because much of CGP is compile-time wiring, a test here often passes simply
by compiling. It is also where the user-facing macros are exercised end-to-end and
where the canonical macro-expansion snapshots live.

**`cgp-macro-tests`** tests the macro internals directly against `cgp-macro-core`
(the parsers and AST types), and is the home for **failure cases** — inputs CGP
should reject, and cases where a macro currently emits invalid code.

**`cgp-test-crate-a`** and **`cgp-test-crate-b`** are auxiliary packages for
**cross-crate** behavior. Crate A defines components, a provider, and a namespaced
component; crate B (a downstream crate) wires them, supplies its own provider for a
foreign component, and participates in crate A's namespace — showing that CGP stays
within Rust's coherence and orphan rules across crate boundaries.

## How the tests are laid out

Inside `cgp-tests`, each concept is one **integration test target**, which Cargo
compiles as its own crate (its own coherence scope). A target is an entrypoint file
`tests/<concept>_tests.rs` plus a module directory `tests/<concept>/` holding one
`.rs` file per unit test. Each unit-test file is self-contained: it defines its own
components, providers, and context types at module scope, so the type-level wiring
of one test never leaks into another. `tests/basic_delegation/` is the reference
example of this layout.

The concept targets currently cover: basic delegation, impl-side dependencies,
implicit arguments, higher-order providers, generic components, abstract types,
getters, field access, extensible records, extensible variants, checking,
dispatching, namespaces, handlers, monadic handlers, async and Send bounds, and
blanket traits. This set grows and subdivides over time — when a concept
accumulates too many cases to stay coherent, it is split into finer targets.

`cgp-macro-tests` follows the same target/`_tests.rs` shape: `ident_with_type_params`
for parser corner cases, and the failure-case targets `parser_rejections` and
`invalid_expansion`.

## Snapshots

Many tests assert the exact code a macro generates, using the `snapshot_*!` macros
from `cgp-macro-test-util`. Each such macro emits the real generated code into the
module **and** generates a `#[test]` asserting a pretty-printed inline `insta`
snapshot of it. Snapshots are used deliberately: a macro's expansion is snapshotted
only in the concept target that owns that macro's feature, and written plainly
everywhere else (see [CLAUDE.md](CLAUDE.md) for the ownership rules).

## Running the tests

```
cargo nextest run -p cgp-tests # the main suite
cargo nextest run -p cgp-macro-tests # macro internals + failure cases
cargo nextest run --workspace # everything, including the aux crates

cargo insta test -p cgp-tests --review # review snapshot diffs interactively
cargo insta test -p cgp-tests --accept # accept intended snapshot changes
```

When a snapshot test fails it prints a diff of the generated code; accept the new
output with `cargo insta` only after confirming the change is intended.
2 changes: 2 additions & 0 deletions crates/tests/cgp-macro-tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ keywords = { workspace = true }
[dependencies]
cgp = { workspace = true }
cgp-macro-test-util = { workspace = true }
insta = { version = "1.48.0" }

[dev-dependencies]
cgp-macro-core = { workspace = true }
cgp-macro-lib = { workspace = true }
syn = { version = "2.0.95" }
quote = { version = "1.0.38" }
proc-macro2 = { version = "1.0.92" }

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
//! Entrypoint for parser corner-case tests of the `IdentWithTypeArgs` /
//! `IdentWithTypeGenerics` / `PathWithTypeArgs` grammars in `cgp-macro-core`.
//!
//! These call the `cgp-macro-core` parsers directly to pin what they accept,
//! reject, and round-trip.
#![allow(dead_code)]

pub mod ident_with_type_params;
7 changes: 7 additions & 0 deletions crates/tests/cgp-macro-tests/tests/invalid_expansion/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//! Failure cases where a CGP macro emits invalid or incorrect Rust.
//!
//! See the entrypoint `invalid_expansion_tests.rs` for the pattern to follow when
//! adding a case. This module is intentionally empty until the first genuine
//! invalid-expansion case is captured; keeping the target in place means the
//! harness is ready and a future agent only has to add a `pub mod <case>;` line
//! and the snapshot file.
21 changes: 21 additions & 0 deletions crates/tests/cgp-macro-tests/tests/invalid_expansion_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//! Entrypoint for the `invalid_expansion` failure-case target.
//!
//! This target captures cases where a CGP macro currently emits **invalid or
//! wrong Rust** — code that a user would reasonably expect to work but that the
//! macro mishandles. Because the captured expansion is stored as a *string*
//! `insta` snapshot, the test compiles even though the code it describes would
//! not.
//!
//! To add a case:
//! 1. produce the expansion by calling the matching `cgp-macro-lib` entrypoint,
//! pretty-printing it (see `cgp-macro-test-util-lib`'s `pretty_format`), and
//! asserting it against an inline `insta` snapshot;
//! 2. add a code comment explaining **why** the output is wrong and **what the
//! correct output should be**;
//! 3. record the limitation in the owning reference document's `## Known issues`
//! section (per docs/CLAUDE.md), and link from the test to that document.
//!
//! No cases are enumerated yet; see crates/tests/CLAUDE.md ("Migration status").
#![allow(dead_code)]

pub mod invalid_expansion;
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//! `#[cgp_component]` must be applied to a trait; applying it to another item
//! is rejected at parse time.
//!
//! See docs/reference/macros/cgp_component.md.

use quote::quote;

use super::assert_macro_rejects;

#[test]
fn rejects_non_trait_item() {
// A struct is not a trait, so the consumer-trait parser rejects it.
assert_macro_rejects("cgp_component on a struct", || {
cgp_macro_lib::cgp_component(
quote!(FooProvider),
quote!(
pub struct NotATrait;
),
)
});
}
25 changes: 25 additions & 0 deletions crates/tests/cgp-macro-tests/tests/parser_rejections/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//! Failure cases: inputs the CGP macros must reject.
//!
//! A rejection test drives a `cgp-macro-lib` entrypoint (or a `cgp-macro-core`
//! parser) with an invalid input and asserts it returns `Err` rather than
//! producing tokens. This is how we pin down which code CGP deliberately refuses,
//! and catch regressions where a macro starts accepting something it should not.
//!
//! To add a case:
//! 1. call the entrypoint, e.g. `cgp_macro_lib::cgp_component(attr, body)`;
//! 2. assert the result is `Err` with [`assert_macro_rejects`];
//! 3. if the rejection corresponds to a documented limitation, note it in the
//! owning reference document's `## Known issues` section and link to it here.

use proc_macro2::TokenStream;

/// Assert that a macro entrypoint rejects its input. `run` is the entrypoint call
/// (for example `|| cgp_macro_lib::cgp_component(attr.clone(), body.clone())`).
#[track_caller]
pub fn assert_macro_rejects(label: &str, run: impl FnOnce() -> syn::Result<TokenStream>) {
if let Ok(tokens) = run() {
panic!("expected `{label}` to be rejected, but it expanded to:\n{tokens}");
}
}

pub mod cgp_component;
11 changes: 11 additions & 0 deletions crates/tests/cgp-macro-tests/tests/parser_rejections_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//! Entrypoint for the `parser_rejections` failure-case target.
//!
//! This target collects inputs that the CGP macros must **reject** — malformed
//! syntax, or forms CGP deliberately disallows. Each case asserts that the
//! relevant `cgp-macro-core` parser (or a `cgp-macro-lib` entrypoint) returns an
//! error rather than silently accepting the input.
//!
//! See crates/tests/CLAUDE.md ("Adding a failure case").
#![allow(dead_code)]

pub mod parser_rejections;
Loading
Loading