Skip to content

Commit f20f74f

Browse files
committed
feat(workspace): workspace-aware mcpp test (-p / --workspace) + root-test fix
mcpp test gains workspace awareness, at parity with build: - mcpp test -p <member> — test one member (discovery scoped to its dir) - mcpp build|test --workspace — act on all members (continue-on-failure + per-member summary; non-zero if any failed) - bare mcpp test at a virtual workspace root now fans out over all members instead of erroring 'duplicate test name main' (it globbed every member's tests/**/*.cpp together). Root cause was missing member scoping, fixed by scoping discovery (shared project::resolve_member_dir), not a 2nd globber. - bare mcpp build at a virtual workspace root builds all members (was: one). src/project.cppm resolve_member_dir (shared match rule), execute.cppm run_tests scopes glob to member, cli.cppm + cmd_build.cppm flags + fan-out orchestration. e2e 90_workspace_test.sh; docs/06-workspace.md (+zh). Version -> 0.0.79.
1 parent 2d37223 commit f20f74f

10 files changed

Lines changed: 415 additions & 12 deletions

File tree

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
# Workspace-aware `mcpp test` + zero-shell self-contained mcpp-index (Design)
2+
3+
Two coupled deliverables toward "把对库的测试做成真实工程、基于 mcpp 自包含、消除所有
4+
shell":
5+
6+
- **Phase 1 (repo `mcpp`)** — make `mcpp test` workspace-aware: `mcpp test -p <member>`,
7+
`mcpp build|test --workspace`, and fix bare `mcpp test` at a workspace root.
8+
- **Phase 2 (repo `mcpp-index`)** — turn each `tests/<lib>/` into a real mcpp test
9+
project (`mcpp test` + behavioral assertions), migrate the `smoke_compat_*.sh`
10+
heredocs into them, and delete every shell driver — CI becomes `mcpp test --workspace`.
11+
12+
Pairs with the typed `import mcpp;` build library
13+
(`2026-06-30-l3-build-mcpp-implementation-design.md` §forward-note, task #20) as the
14+
**0.0.79 "workspace-test + build.mcpp library"** release.
15+
16+
---
17+
18+
## 1. Problem (grounded in the code)
19+
20+
`mcpp build` is workspace-aware; `mcpp test` is not.
21+
22+
- `mcpp build -p <member>` works (`src/cli.cppm` build subcommand has
23+
`.option("package").short_name('p')`; `cmd_build` copies it to
24+
`BuildOverrides::package_filter``src/cli/cmd_build.cppm`; `prepare_build`
25+
resolves the member and **reassigns `root` to the member dir**
26+
`src/build/prepare.cppm:415-509`).
27+
- `mcpp test` has **no** `-p` / `--workspace` (`src/cli.cppm` test subcommand).
28+
- **The bug:** `run_tests` (`src/build/execute.cppm:421-459`) calls
29+
`find_manifest_root()` → gets the **workspace** root → `expand_glob(*root,
30+
"tests/**/*.cpp")` → collects *every member's* `tests/.../main.cpp` →
31+
`seenNames.insert("main")` collides → `error: duplicate test name 'main'`. Test
32+
discovery runs **before** any member selection, on the unscoped workspace root.
33+
34+
Proof:
35+
```
36+
$ mcpp build # virtual ws root → "Workspace building member 'tests/examples/build-mcpp'" (picks ONE, arbitrarily)
37+
$ mcpp test # virtual ws root → error: duplicate test name 'main' (globs ALL, unscoped)
38+
```
39+
40+
## 2. Architecture principle
41+
42+
**Scope, don't duplicate.** The root-test bug is *missing member scoping*, not a
43+
missing second globber. The fix is to run the **same** member resolution `build`
44+
uses *before* test discovery, so `tests/**/*.cpp` is globbed from the **member**
45+
dir. `--workspace` is **thin orchestration**: a loop over members that calls the
46+
**existing per-member pipeline** once each — no parallel build/test path.
47+
48+
Concretely, extract today's inline member logic into one shared helper and have
49+
both `build` and `test` consume it:
50+
51+
```cpp
52+
// src/build/workspace.cppm (new, or fold into project.cppm)
53+
namespace mcpp::build {
54+
struct MemberRef { std::string name; std::filesystem::path dir; };
55+
// Resolve which members a command acts on, from the (already-loaded) root
56+
// manifest + overrides. Encapsulates the selection rules in §3.
57+
std::expected<std::vector<MemberRef>, std::string>
58+
select_members(const manifest::Manifest& root, const std::filesystem::path& rootDir,
59+
const BuildOverrides& ov, bool wantAll);
60+
}
61+
```
62+
63+
- `-p <name>` → 1 member (match by directory basename **or** member path — the
64+
rule already in `prepare.cppm:430-447`).
65+
- `--workspace` (or bare at a **virtual** workspace) → **all** members.
66+
- Bare at a **rooted** workspace (`[package]` + `[workspace]`) → the root package
67+
(members only via `--workspace`), matching today.
68+
69+
`prepare_build`'s existing per-member switch (load member manifest, `merge_workspace_deps`,
70+
inherit toolchain/target/indices, `root = memberDir`) stays as the **single-member**
71+
mechanism; `select_members` just decides the *set*, and the orchestration loop calls
72+
`prepare_build(package_filter = member.name)` per member.
73+
74+
## 3. Behavior matrix (decision)
75+
76+
| invocation | virtual workspace | rooted workspace | plain package |
77+
|---|---|---|---|
78+
| `mcpp build` / `mcpp test` | **all members** | root package | the package |
79+
| `... -p <m>` | member `<m>` | member `<m>` | error (no members) |
80+
| `... --workspace` | all members | all members (+root) | error |
81+
| `mcpp run` (+`-p`) | default/`-p` member | root/`-p` | the package |
82+
83+
**Change from today:** bare build/test at a *virtual* workspace goes from "pick one
84+
arbitrary member" → **all members** (Cargo-consistent; the pick-one was a
85+
placeholder). `run` stays single-target (no `--workspace`). This is the only
86+
behavior change; verify/adjust the workspace e2e (`tests/e2e/*workspace*`).
87+
88+
## 4. Phase 1 — implementation touch points
89+
90+
1. **CLI** (`src/cli.cppm`, test subcommand ~L244-256): add
91+
```cpp
92+
.option(cl::Option("package").short_name('p').takes_value().value_name("NAME")
93+
.help("Run tests only for the named workspace member"))
94+
.option(cl::Option("workspace").help("Run tests for all workspace members"))
95+
```
96+
and add `.option("workspace")` to the **build** subcommand (~L215-237).
97+
2. **Parse** (`src/cli/cmd_build.cppm`): in `cmd_test`, copy `-p`
98+
`ov.package_filter`; read `--workspace``bool all`. Same `--workspace` read in
99+
`cmd_build`.
100+
3. **`select_members` helper** (new `src/build/workspace.cppm`): the §2 logic,
101+
extracted from `prepare.cppm:422-453` so both paths share it.
102+
4. **`run_tests` scoping** (`src/build/execute.cppm:421-459`): before the
103+
`expand_glob`, resolve the member root for the single-member case (when
104+
`package_filter` set, via the shared helper) and glob from the **member dir**,
105+
not the workspace root. This kills the "duplicate main" bug by *scoping*.
106+
5. **`--workspace` orchestration** (`src/cli/cmd_build.cppm`): when `all`, call
107+
`select_members(..., wantAll=true)` and loop — `cmd_build` runs
108+
`prepare_build`+`run_build_plan` per member; `cmd_test` runs `run_tests` per
109+
member (each with `package_filter = member.name`). Aggregate exit codes
110+
(first non-zero wins; print a per-member summary). Continue-on-failure so one
111+
member's failure still reports the rest.
112+
6. **Tests** (`tests/e2e/90_workspace_test.sh`): a virtual workspace with 2 members
113+
each having `tests/<distinct>.cpp` asserting behavior; assert
114+
`mcpp test -p memberA` runs only A's tests, `mcpp test --workspace` runs both
115+
(no duplicate-stem error), bare `mcpp test` at the root runs both.
116+
7. **Docs** (`docs/06-workspace.md` + zh): document `-p`/`--workspace` for
117+
build/test and the bare-at-root semantics.
118+
119+
## 5. Phase 2 — mcpp-index zero-shell restructure (repo `mcpp-index`)
120+
121+
### Target layout
122+
```
123+
mcpp-index/
124+
pkgs/ # the index (recipes) — unchanged
125+
mcpp.toml # [workspace] members=tests/* ⊕ [indices] compat={path="."}
126+
tests/
127+
cjson/
128+
mcpp.toml # [package] cjson-tests; [dependencies] compat.cjson
129+
tests/parse.cpp # mcpp test — assert cJSON_Parse fields
130+
eigen/ tests/matmul.cpp # assert A*B values
131+
nlohmann.json/ tests/roundtrip.cpp # parse→dump→parse equality
132+
openblas/ # [target.'cfg(windows)'.dependencies] openblas; tests/dgemm.cpp asserts [19 22;43 50] (no-op gate off-Windows)
133+
build-mcpp/ # build.mcpp generates a source; tests/ assert it linked
134+
```
135+
136+
### Test mechanism
137+
mcpp's native `mcpp test` discovers `tests/**/*.cpp`, builds each as a test binary,
138+
runs it (non-zero = fail). Members use plain assertion `.cpp` (a failing `assert`/
139+
non-zero return), no external framework required — keeps members dependency-light
140+
and host-portable. (gtest remains available via `[dev-dependencies]` if a member
141+
wants richer output, but is not required.)
142+
143+
### Migration (delete all shell)
144+
- Convert each `smoke_compat_*.sh` heredoc body into the matching member's
145+
`tests/*.cpp` (the heredocs already contain the usage snippets).
146+
- **Delete** `tests/smoke_compat_{core,imgui,archive,imgui_window}.sh`,
147+
`tests/smoke_imgui_module.sh`, `tests/smoke_compat_portable.sh`,
148+
`tests/run_workspace.sh`, `tests/run_example.sh`.
149+
- **`validate.yml`** collapses: the per-suite jobs become one matrix or a single
150+
`mcpp test --workspace` step:
151+
```yaml
152+
- run: mcpp test --workspace # linux; the whole index, self-contained
153+
```
154+
Windows/macOS jobs run the same command (platform-gated members like openblas
155+
self-gate via `cfg(windows)`; the `detect` job can still narrow to `-p <lib>` on
156+
PRs touching one recipe). No shell driver remains.
157+
158+
### Result
159+
The index repo is simultaneously (a) the package index and (b) a mcpp workspace
160+
whose members really *use and test* every recipe — driven entirely by `mcpp`, no
161+
`.sh`. "基于 mcpp 自包含" achieved.
162+
163+
## 6. Typed `import mcpp;` library (task #20) — same 0.0.79
164+
165+
Independent of Phase 1; ships in the same release. Per the build.mcpp design doc's
166+
forward note: a typed module **bundled in the mcpp binary**, emitting the existing
167+
stdout `mcpp:` wire protocol, implemented with C-level I/O so neither it nor
168+
`build.mcpp` needs `import std;`. Converts the `build.mcpp` examples/docs to the
169+
modules-first `import mcpp;` form. The `build-mcpp` mcpp-index member adopts it in
170+
Phase 2.
171+
172+
## 7. Sequencing & releases
173+
174+
1. **mcpp PR A** — Phase 1 (workspace-aware test) + e2e + docs.
175+
2. **mcpp PR B** — typed `import mcpp;` library + convert build.mcpp docs/examples/test.
176+
3. **Release mcpp 0.0.79** (A+B) → full ecosystem loop (mirror → index → verify → pin).
177+
4. **mcpp-index PR C** — Phase 2 restructure on 0.0.79; delete all shell; CI =
178+
`mcpp test --workspace` → its CI green → merge.
179+
180+
## 8. Risks / soundness notes
181+
182+
- **Behavior change** (bare virtual-ws build/test → all members): the one
183+
compatibility-affecting change; covered by updating the workspace e2e and is the
184+
more-correct semantics. `run` is untouched (single-target).
185+
- **No parallel code path**: `--workspace`/`-p` are orchestration over the proven
186+
per-member `prepare_build`/`run_tests`; the helper centralizes selection so the
187+
rules can't drift between `build` and `test` (the class of bug we're fixing).
188+
- **Aggregate reporting**: `--workspace` must continue-on-failure and print a
189+
per-member pass/fail summary, else one red member hides the rest.
190+
- **Per-member dep isolation** is the reason for the workspace (vs one mega
191+
`[package]`): members resolve independently, so conflicting transitive deps and
192+
platform-only libs (openblas/Windows) don't couple. Recorded as the rejected
193+
single-project alternative.

docs/06-workspace.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,14 +153,24 @@ default = "clang@19.0"
153153

154154
## 5. Build Commands
155155

156-
### 5.1 Building from the Workspace Root
156+
### 5.1 Building & testing from the Workspace Root
157157

158158
```bash
159-
mcpp build # build the default target (auto-selects the member with a binary target)
159+
mcpp build # virtual workspace → builds ALL members; rooted → the root package
160160
mcpp build -p server # build a specific member and its dependencies
161-
mcpp build -p core # build a specific library member
161+
mcpp build --workspace # build every member explicitly
162+
mcpp test # virtual workspace → tests ALL members; rooted → the root package
163+
mcpp test -p core # test a single member
164+
mcpp test --workspace # test every member (one report per member; continues past failures)
162165
```
163166

167+
At a **virtual** workspace root (only `[workspace]`, no `[package]`), bare
168+
`mcpp build` / `mcpp test` act on **all** members. At a **rooted** workspace
169+
(`[package]` + `[workspace]`), they act on the root package; use `--workspace` to
170+
include all members. `mcpp test --workspace` builds + runs each member's
171+
`tests/**/*.cpp` independently — discovery is scoped per member, so two members may
172+
each have a `tests/main.cpp` without colliding.
173+
164174
### 5.2 Building from a Member Subdirectory
165175

166176
```bash
@@ -180,6 +190,11 @@ mcpp test -p core # matches libs/core
180190
mcpp run -p server -- --port 8080
181191
```
182192

193+
`--workspace` (on `build` and `test`) is the fan-out form: it acts on **every**
194+
member. `mcpp test --workspace` reports each member separately and continues past a
195+
failing member, exiting non-zero if any member failed — ideal as a single,
196+
shell-free CI step for a workspace that tests many libraries.
197+
183198
## 6. Directory Layout
184199

185200
The recommended directory layout for a workspace:

docs/zh/06-workspace.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,14 +153,22 @@ default = "clang@19.0"
153153

154154
## 5. 构建命令
155155

156-
### 5.1 从工作空间根目录构建
156+
### 5.1 从工作空间根目录构建与测试
157157

158158
```bash
159-
mcpp build # 构建默认目标(自动选择含二进制目标的成员)
159+
mcpp build # 虚拟工作空间 → 构建所有成员;带根包 → 构建根包
160160
mcpp build -p server # 构建指定成员及其依赖
161-
mcpp build -p core # 构建指定库成员
161+
mcpp build --workspace # 显式构建每个成员
162+
mcpp test # 虚拟工作空间 → 测试所有成员;带根包 → 测试根包
163+
mcpp test -p core # 测试单个成员
164+
mcpp test --workspace # 测试每个成员(逐成员汇报;遇失败继续)
162165
```
163166

167+
**虚拟工作空间**根(只有 `[workspace]`、无 `[package]`)下,裸 `mcpp build` /
168+
`mcpp test` 作用于**所有**成员;在**带根包工作空间**(`[package]` + `[workspace]`)下作用于
169+
根包,用 `--workspace` 纳入全部成员。`mcpp test --workspace` 独立构建+运行每个成员的
170+
`tests/**/*.cpp`——测试发现按成员隔离,因此两个成员各有一个 `tests/main.cpp` 也不会冲突。
171+
164172
### 5.2 从成员子目录构建
165173

166174
```bash
@@ -180,6 +188,10 @@ mcpp test -p core # 匹配 libs/core
180188
mcpp run -p server -- --port 8080
181189
```
182190

191+
`--workspace`(用于 `build``test`)是扇出形式:作用于**每个**成员。
192+
`mcpp test --workspace` 逐成员独立汇报、遇失败继续,只要有任一成员失败即非零退出——
193+
非常适合作为「一个测试众多库的工作空间」的单条、无 shell 的 CI 步骤。
194+
183195
## 6. 目录布局
184196

185197
工作空间推荐的目录布局:

mcpp.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mcpp"
3-
version = "0.0.78"
3+
version = "0.0.79"
44
description = "Modern C++ build & package management tool"
55
license = "Apache-2.0"
66
authors = ["mcpp-community"]

src/build/execute.cppm

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -426,8 +426,21 @@ export int run_tests(std::span<const std::string> passthrough,
426426
return 2;
427427
}
428428

429-
// 1. Discover test files.
430-
auto testFiles = mcpp::modgraph::expand_glob(*root, "tests/**/*.cpp");
429+
// Workspace scoping: discovery must run against the MEMBER, not the
430+
// workspace root — otherwise `tests/**/*.cpp` globs every member's tests
431+
// together (two `tests/main.cpp` → "duplicate test name 'main'"). When a
432+
// member is selected (via -p, threaded as package_filter), glob from its
433+
// dir; prepare_build below resolves the SAME member, so the two agree.
434+
// (--workspace fans out over members at the cmd layer, one call per member.)
435+
auto testRoot = *root;
436+
if (auto rm = mcpp::manifest::load(*root / "mcpp.toml"); rm) {
437+
auto member = mcpp::project::resolve_member_dir(*rm, *root, overrides.package_filter);
438+
if (!member) { mcpp::ui::error(member.error()); return 2; }
439+
if (!member->empty()) testRoot = *member;
440+
}
441+
442+
// 1. Discover test files (scoped to the member/package).
443+
auto testFiles = mcpp::modgraph::expand_glob(testRoot, "tests/**/*.cpp");
431444
if (testFiles.empty()) {
432445
std::println("no tests found in tests/");
433446
return 0;
@@ -447,8 +460,8 @@ export int run_tests(std::span<const std::string> passthrough,
447460
mcpp::manifest::Target t;
448461
t.name = name;
449462
t.kind = mcpp::manifest::Target::TestBinary;
450-
// Store as path relative to project root for portability of error messages.
451-
t.main = std::filesystem::relative(f, *root).string();
463+
// Relative to the member/package root prepare_build will operate on.
464+
t.main = std::filesystem::relative(f, testRoot).string();
452465
testTargets.push_back(std::move(t));
453466
}
454467

src/cli.cppm

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,8 @@ int run(int argc, char** argv) {
234234
.help("Pin capability providers (e.g. blas=openblas,lapack=mkl)"))
235235
.option(cl::Option("strict")
236236
.help("Treat manifest schema warnings (unknown feature/platform) as errors"))
237+
.option(cl::Option("workspace")
238+
.help("Build all workspace members"))
237239
.action(wrap_rc(cmd_build)))
238240
.subcommand(cl::App("run")
239241
.description("Build + run a binary target (after `--`, args are passed to it)")
@@ -251,6 +253,10 @@ int run(int argc, char** argv) {
251253
.help("Pin capability providers (e.g. blas=openblas,lapack=mkl)"))
252254
.option(cl::Option("strict")
253255
.help("Treat manifest schema warnings (unknown feature/platform) as errors"))
256+
.option(cl::Option("package").short_name('p').takes_value().value_name("NAME")
257+
.help("Run tests only for the named workspace member"))
258+
.option(cl::Option("workspace")
259+
.help("Run tests for all workspace members"))
254260
.action(wrap_rc([&passthrough](const cl::ParsedArgs& p) {
255261
return cmd_test(p, std::span<const std::string>(passthrough));
256262
})))

0 commit comments

Comments
 (0)