Skip to content

Commit 2211fb5

Browse files
2026.11.8.1: mtest's test protocol in mcpp test, tests without main, one std module builder per entry, lock targets per plan, identity-key manifests (#906)
mcpp test runs the runner's side of the mtest test protocol (version 1): a program lists, selects and reports its cases, nested selections included; a test file of mtest's objects needs no main; mcpp new scaffolds tests with mtest 0.4.0. Two builds that find a std module missing compile it once. A framework that is a dependency and a dev-dependency offers its beginning once. main is detected after the preprocessor. Lock targets per plan, manifests keyed by identity, the target budget, verified openkal rows. Docs, e2e (1032–1039) and the ecosystem design (W8–W10). Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
1 parent 34406d6 commit 2211fb5

147 files changed

Lines changed: 5430 additions & 1430 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.agents/docs/2026-11-08-ecosystem-885-supplement-design.md‎

Lines changed: 242 additions & 0 deletions
Large diffs are not rendered by default.

‎.agents/docs/2026-11-08-ecosystem-top-level-design.md‎

Lines changed: 448 additions & 0 deletions
Large diffs are not rendered by default.

‎.agents/docs/README.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ superseded_by: 2026-09-07-....md # when status is superseded
1818
---
1919
```
2020

21-
353 records.
21+
355 records.
2222

2323
## By subject
2424

@@ -75,6 +75,8 @@ Records that declare one. Everything else is listed by date below.
7575

7676
### ecosystem
7777

78+
- [生态顶层方案:#885 收尾之后的工作流、原则与依赖](2026-11-08-ecosystem-top-level-design.md) — active
79+
- [#885 收尾补充方案:目标角色、平台事实、系统接口与版本书写](2026-11-08-ecosystem-885-supplement-design.md) — active
7880
- [生态 #885 收尾设计:核心、插件、openkal、语言服务与编辑器的一轮闭合](2026-11-07-ecosystem-885-closeout-design.md) — active
7981
- [生态设计 part 2:openkal 的最小原子与二进制映像、其上的 `mcpplibs.os`,以及 part 1 的收尾与回归记录](2026-10-26-ecosystem-part2-os-library-over-openkal-design.md) — active
8082
- [控制契约、并发与性能、单一 C++ 运行时,以及 IDE 生态(生态级,#838、#839)](2026-10-26-control-contract-runtime-and-ide-ecosystem-design.md) — active
@@ -177,6 +179,8 @@ Records that declare one. Everything else is listed by date below.
177179

178180
### 2026-11
179181

182+
- [生态顶层方案:#885 收尾之后的工作流、原则与依赖](2026-11-08-ecosystem-top-level-design.md) — active
183+
- [#885 收尾补充方案:目标角色、平台事实、系统接口与版本书写](2026-11-08-ecosystem-885-supplement-design.md) — active
180184
- [生态 #885 收尾设计:核心、插件、openkal、语言服务与编辑器的一轮闭合](2026-11-07-ecosystem-885-closeout-design.md) — active
181185
- [Launch configuration and named test runners](2026-11-06-launch-configuration-and-named-test-runners.md) — active
182186
- [Compiler attempt lifetime and output recovery](2026-11-06-compiler-attempt-recovery.md) — active
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
name: e2e-mtest
2+
description: >
3+
mtest for the e2e suite (design 2026-11-08 W8; tests/e2e/mtest.toml): the
4+
release this action pins, its sha256 checked, and the stores main's last
5+
successful CI run kept for this host, taken into tests/e2e/.mtest so that
6+
`--shard` divides the suite by measured time and a run orders its tests
7+
longest first. Exports E2E_MTEST for run_all.sh. A run with no store of main
8+
divides by count, and its own store becomes the next run's history.
9+
10+
inputs:
11+
host:
12+
description: the host's name in the stores' artifact names (linux, macos, windows)
13+
required: true
14+
version:
15+
description: the mtest release
16+
required: false
17+
default: '0.4.0'
18+
sha256:
19+
description: sha256 of mtest-<version>-openkal-bundle-all.tar.gz
20+
required: false
21+
default: 'b62e4ac7cd76b20dbdc48a5d041d377444e8815ee7a4ff1e7686145d13acc925'
22+
token:
23+
description: a token that reads this repository's actions
24+
required: false
25+
default: ${{ github.token }}
26+
27+
runs:
28+
using: composite
29+
steps:
30+
- name: mtest
31+
shell: bash
32+
env:
33+
MTEST_VERSION: ${{ inputs.version }}
34+
MTEST_SHA256: ${{ inputs.sha256 }}
35+
run: |
36+
set -euo pipefail
37+
archive="mtest-$MTEST_VERSION-openkal-bundle-all.tar.gz"
38+
temp="$RUNNER_TEMP"; python=python3
39+
if [ "$RUNNER_OS" = Windows ]; then temp=$(cygpath -u "$RUNNER_TEMP"); python=python; fi
40+
curl -fsSL --retry 5 --retry-all-errors --retry-delay 5 -o "$temp/$archive" \
41+
"https://github.com/mcpp-community/mtest/releases/download/v$MTEST_VERSION/$archive"
42+
actual=$("$python" -c "import hashlib,sys; print(hashlib.sha256(open(sys.argv[1],'rb').read()).hexdigest())" "$temp/$archive")
43+
[ "$actual" = "$MTEST_SHA256" ] || { echo "::error::$archive: sha256 $actual, the action pins $MTEST_SHA256"; exit 1; }
44+
mkdir -p "$temp/mtest" && tar xzf "$temp/$archive" -C "$temp/mtest"
45+
launcher="$temp/mtest/mtest-$MTEST_VERSION-openkal-bundle-all/bin/mtest"
46+
[ "$RUNNER_OS" = Windows ] && launcher="$launcher.exe"
47+
"$launcher" --version
48+
echo "E2E_MTEST=$launcher" >> "$GITHUB_ENV"
49+
# The pull request's change, for the `pr` set: the base's tip and HEAD's
50+
# parents, so that `git diff <base>...HEAD` has its merge base. When they
51+
# cannot be fetched, mtest selects every test and says why.
52+
- name: The base of the change
53+
if: github.base_ref != ''
54+
shell: bash
55+
run: |
56+
git fetch --no-tags --depth=1 origin "+refs/heads/${{ github.base_ref }}:refs/remotes/origin/${{ github.base_ref }}" || true
57+
git fetch --no-tags --deepen=1 origin || true
58+
- name: The stores of main's last CI run
59+
shell: bash
60+
env:
61+
GH_TOKEN: ${{ inputs.token }}
62+
HOST: ${{ inputs.host }}
63+
run: |
64+
set -euo pipefail
65+
run=$(gh run list -R "$GITHUB_REPOSITORY" --workflow ci.yml --branch main --status success --limit 1 \
66+
--json databaseId -q '.[0].databaseId' 2>/dev/null || true)
67+
if [ -z "$run" ]; then echo "no successful run of main: the suite is divided by count"; exit 0; fi
68+
rm -rf "$RUNNER_TEMP/e2e-stores"
69+
if ! gh run download "$run" -R "$GITHUB_REPOSITORY" -p "e2e-store-$HOST-*" -D "$RUNNER_TEMP/e2e-stores" 2>/dev/null; then
70+
echo "main's run $run kept no store for $HOST: the suite is divided by count"; exit 0
71+
fi
72+
for store in "$RUNNER_TEMP"/e2e-stores/*/; do
73+
(cd tests/e2e && "$E2E_MTEST" store import --from "$store")
74+
done
75+
echo "history of main's run $run: $(ls "$RUNNER_TEMP/e2e-stores" | wc -l) store(s)"
76+
# The coverage map of the last nightly measurement (e2e-coverage.yml), on
77+
# the host it was measured on: the sources each test's run executed there.
78+
# Another host's own sources (src/native/windows/...) run on no Linux test,
79+
# so its suite has no map and every test of it is affected.
80+
- name: The coverage map
81+
if: inputs.host == 'linux'
82+
shell: bash
83+
env:
84+
GH_TOKEN: ${{ inputs.token }}
85+
run: |
86+
set -euo pipefail
87+
run=$(gh run list -R "$GITHUB_REPOSITORY" --workflow e2e-coverage.yml --status success --limit 1 \
88+
--json databaseId -q '.[0].databaseId' 2>/dev/null || true)
89+
if [ -z "$run" ]; then echo "no coverage map: every test is affected"; exit 0; fi
90+
rm -rf "$RUNNER_TEMP/e2e-coverage"
91+
gh run download "$run" -R "$GITHUB_REPOSITORY" -n e2e-coverage-linux -D "$RUNNER_TEMP/e2e-coverage" || exit 0
92+
(cd tests/e2e && "$E2E_MTEST" coverage import --from "$RUNNER_TEMP/e2e-coverage/coverage.json")

‎.github/tools/check_e2e_coverage.py‎

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,15 @@
3030
An exception for a test that ran, or for a test that does not exist, also fails
3131
the check, so that the list cannot outlive the reason it records.
3232
33+
Some claims need a test to pass on particular hosts, not on some host: docs/21
34+
calls a target row `verified` when CI builds and runs its artefact, and the
35+
`*-openkal` rows claim that for every system. tests/e2e/host-requirements.tsv
36+
lists such a test with the hosts its result must come from; a listed test that
37+
did not pass on one of them, or that does not exist, fails the check.
38+
3339
With --timings-out DIR it writes `<host>.tsv`, the measured duration of every
3440
test that ran, merged across that host's shards, in the format run_all.sh reads
35-
from E2E_TIMINGS; refreshing tests/e2e/timings/ is copying those files. A report
41+
from E2E_TIMINGS (a run without mtest; CI's shards divide by mtest's history). A report
3642
file is named `e2e-report-<host>-<shard>.tsv`.
3743
3844
Usage:
@@ -64,6 +70,19 @@ def read_reports(directory: Path):
6470
yield m["host"], int(m["shard"]), parts[0], parts[1], ms, parts[3] if len(parts) > 3 else ""
6571

6672

73+
def read_host_requirements(path: Path) -> dict[str, set[str]]:
74+
"""`<test>\t<host> <host> ...\t<reason>` lines: the hosts a test must pass on."""
75+
out: dict[str, set[str]] = {}
76+
if not path.exists():
77+
return out
78+
for line in path.read_text(encoding="utf-8").splitlines():
79+
if not line.strip() or line.startswith("#"):
80+
continue
81+
parts = line.split("\t")
82+
out[parts[0].strip()] = set(parts[1].split()) if len(parts) > 1 else set()
83+
return out
84+
85+
6786
def read_exceptions(path: Path) -> dict[str, str]:
6887
out: dict[str, str] = {}
6988
if not path.exists():
@@ -109,8 +128,10 @@ def main() -> int:
109128
workflows = workflow_tokens([p.read_text(encoding="utf-8")
110129
for p in sorted((root / ".github" / "workflows").glob("*.yml"))])
111130
exceptions = read_exceptions(root / "tests" / "e2e" / "coverage-exceptions.tsv")
131+
required = read_host_requirements(root / "tests" / "e2e" / "host-requirements.tsv")
112132

113133
ran: dict[str, set[str]] = defaultdict(set)
134+
passed: dict[str, set[str]] = defaultdict(set)
114135
skipped: dict[str, set[str]] = defaultdict(set)
115136
shard_ms: dict[tuple[str, int], int] = defaultdict(int)
116137
timings: dict[str, dict[str, int]] = defaultdict(dict)
@@ -119,6 +140,8 @@ def main() -> int:
119140
reports += 1
120141
if status in RAN:
121142
ran[test].add(host)
143+
if status == "pass":
144+
passed[test].add(host)
122145
shard_ms[(host, shard)] += ms
123146
timings[host][test] = ms
124147
elif status == "skip":
@@ -143,6 +166,15 @@ def main() -> int:
143166
for test in exceptions:
144167
if test not in tests:
145168
stale.append(f"{test} is excused but does not exist")
169+
unmet = []
170+
for test, hosts in sorted(required.items()):
171+
if test not in tests:
172+
stale.append(f"{test} is required on hosts but does not exist")
173+
continue
174+
for host in sorted(hosts - passed[test]):
175+
why = [d for d in skipped[test] if d.startswith(f"{host}: ")]
176+
unmet.append(f"{test} did not pass on {host}"
177+
f"{' (' + '; '.join(why) + ')' if why else (' (it ran and did not pass)' if host in ran[test] else ' (no report of it there)')}")
146178

147179
print(f"{len(tests)} tests: {counts['ran']} ran on a shard, {counts['job']} run by a "
148180
f"dedicated job, {counts['excused']} excused, {len(uncovered)} uncovered")
@@ -152,14 +184,16 @@ def main() -> int:
152184
print(f"UNCOVERED: {line}")
153185
for line in stale:
154186
print(f"STALE EXCEPTION: {line}")
187+
for line in unmet:
188+
print(f"REQUIRED ON HOST: {line}")
155189

156190
if args.timings_out:
157191
out = Path(args.timings_out)
158192
out.mkdir(parents=True, exist_ok=True)
159193
for host, table in timings.items():
160194
(out / f"{host}.tsv").write_text(
161195
"".join(f"{t}\t{ms}\n" for t, ms in sorted(table.items())), encoding="utf-8")
162-
return 1 if uncovered or stale else 0
196+
return 1 if uncovered or stale or unmet else 0
163197

164198

165199
if __name__ == "__main__":

‎.github/tools/check_include_regions.sh‎

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env bash
2-
# check_include_regions.sh — headers are included only where a system has no
3-
# module for what is needed (design 2026-11-07 D10).
2+
# check_include_regions.sh — headers are included, and the preprocessor
3+
# chooses, only where a system has no module for what is needed (design
4+
# 2026-11-07 D10; design 2026-11-08 S4).
45
#
56
# mcpp imports the standard library (`import std`), its dependencies as modules
67
# (nlohmann.json, mcpplibs.os, the mwrap wrappers of zlib, zstd, xz and of the
@@ -10,6 +11,11 @@
1011
# directive anywhere else is a header a module should provide: depend on the
1112
# package that wraps it, or wrap it (mcpp-index docs/mwrap.md).
1213
#
14+
# The same holds for a conditional directive (`#if`, `#ifdef`, `#ifndef`,
15+
# `#elif`) and a macro definition (`#define`): a platform is chosen by
16+
# `if constexpr` over mcpplibs.os and by the per-system sources of mcpp.toml,
17+
# never by the preprocessor.
18+
#
1319
# Text that only looks like a directive is not one: a raw string (the source
1420
# mcpp writes for a launcher or a template) and a comment are skipped. Tests
1521
# (`tests/`, a module's `tests/`) are outside the rule.
@@ -20,10 +26,11 @@ import re, sys
2026
from pathlib import Path
2127
2228
ALLOWED = {Path("src/native/posix/lock.cpp"), Path("src/native/windows/lock.cpp")}
29+
DIRECTIVE = re.compile(r'#\s*(include|if|ifdef|ifndef|elif|elifdef|elifndef|define)\b')
2330
RAW = re.compile(r'(?:u8|u|U|L)?R"([^()\\ ]{0,16})\(')
2431
2532
def directives(text):
26-
"""Line numbers of `#include` directives outside comments and raw strings."""
33+
"""(line, directive) of each preprocessor directive outside comments and raw strings."""
2734
out, i, n, line = [], 0, len(text), 1
2835
at_line_start = True
2936
while i < n:
@@ -45,7 +52,8 @@ def directives(text):
4552
j += 2 if text[j] == '\\' else 1
4653
i = j + 1; at_line_start = False; continue
4754
if at_line_start and c == '#':
48-
if re.match(r'#\s*include\b', text[i:]): out.append(line)
55+
d = DIRECTIVE.match(text, i)
56+
if d: out.append((line, d.group(1)))
4957
if not c.isspace(): at_line_start = False
5058
i += 1
5159
return out
@@ -54,8 +62,9 @@ bad = 0
5462
for path in sorted(list(Path("src").rglob("*.cpp*")) + list(Path("modules").rglob("*.cpp*"))):
5563
if "tests" in path.parts or "test" in path.parts or path in ALLOWED:
5664
continue
57-
for line in directives(path.read_text(encoding="utf-8", errors="replace")):
58-
print(f"{path}:{line}: #include outside the system lock files; import a module instead")
65+
for line, d in directives(path.read_text(encoding="utf-8", errors="replace")):
66+
hint = "import a module instead" if d == "include" else "choose with if constexpr or a per-system source"
67+
print(f"{path}:{line}: #{d} outside the system lock files; {hint}")
5968
bad += 1
6069
for path in ALLOWED:
6170
if not path.exists():

‎.github/tools/check_modules_wiring.sh‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
# when they are forgotten:
1111
#
1212
# 1. modules/<x>/mcpp.toml the package itself
13-
# 2. mcpp.toml [dependencies.mcpp] what makes it importable
13+
# 2. mcpp.toml [dependencies] `mcpp.<x> = { path = "modules/<x>" }`,
14+
# what makes it importable
1415
# 3. mcpp.toml [workspace] members what makes `-p` and `mcpp test` see it
1516
# 4. the two xmake source lists the bootstrap and the benchmark, which
1617
# have no package manager and compile the
@@ -62,8 +63,9 @@ for d in "${dirs[@]}"; do
6263
|| bad "$d/mcpp.toml has namespace = \"${ns:-<none>}\"; modules of this project use \"mcpp\" so they do not squat bare registry names"
6364

6465
# (2) importable
65-
grep -qE "^[[:space:]]*$name[[:space:]]*=[[:space:]]*\{[[:space:]]*path[[:space:]]*=[[:space:]]*\"modules/$name\"" mcpp.toml \
66-
|| bad "mcpp.toml [dependencies.mcpp] has no entry for '$name' — it will not be importable"
66+
grep -qE "^[[:space:]]*mcpp\.$name[[:space:]]*=[[:space:]]*\{[[:space:]]*path[[:space:]]*=[[:space:]]*\"modules/$name\"" \
67+
<(sed -n '/^\[dependencies\]/,/^\[/p' mcpp.toml) \
68+
|| bad "mcpp.toml [dependencies] has no 'mcpp.$name = { path = \"modules/$name\" }' — it will not be importable"
6769

6870
# (3) addressable
6971
grep -qF "\"modules/$name\"" <(sed -n '/^\[workspace\]/,/^\[/p' mcpp.toml) \

‎.github/workflows/ci-linux-e2e.yml‎

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
name: ci-linux-e2e
22

33
# The e2e suite (tests/e2e/run_all.sh) on Linux, called by ci.yml beside
4-
# ci-linux.yml after the Linux build. Six shards, assigned by measured
5-
# duration (tests/e2e/timings/linux.tsv, rule R4 of the 2026-10-02 CI record):
6-
# the table holds about 72 minutes of tests (2026-10-30, with the target-set
7-
# and packaging tests of 2026.10.30.1), about 12 a shard,
4+
# ci-linux.yml after the Linux build. Eight shards, divided by mtest by the
5+
# measured history of main's stores (design 2026-11-08 W8; rule R4 of the
6+
# 2026-10-02 CI record): about 94 minutes of tests (2026-11-08), about 12 a
7+
# shard one test at a time and about 6 two at a time,
88
# each running the one binary build.yml produced (use-built-mcpp, rule R1).
99
# Each shard writes a per-test report that the e2e-coverage job of ci.yml reads
1010
# (rule R5).
@@ -17,16 +17,21 @@ on:
1717

1818
jobs:
1919
e2e:
20-
name: e2e ${{ matrix.shard }}/6 (linux x86_64, self-host)
20+
name: e2e ${{ matrix.shard }}/8 (linux x86_64, self-host)
2121
runs-on: ubuntu-24.04
2222
timeout-minutes: 45
2323
strategy:
2424
fail-fast: false
2525
matrix:
26-
shard: [1, 2, 3, 4, 5, 6]
26+
shard: [1, 2, 3, 4, 5, 6, 7, 8]
2727
env:
28-
E2E_SHARD: ${{ matrix.shard }}/6
29-
E2E_TIMINGS: tests/e2e/timings/linux.tsv
28+
E2E_SHARD: ${{ matrix.shard }}/8
29+
# By mtest (design 2026-11-08 W8; .github/actions/e2e-mtest): parts by
30+
# the measured history of main's stores, two tests at a time (measured on
31+
# one Linux shard: 220 s against 424 s one at a time, three no faster),
32+
# and on a pull request the tests its change affects.
33+
E2E_JOBS: '2'
34+
E2E_SET: ${{ github.event_name == 'pull_request' && 'pr' || '' }}
3035
E2E_REPORT: ${{ github.workspace }}/e2e-report-linux-${{ matrix.shard }}.tsv
3136
MCPP_HOME: /home/runner/.mcpp
3237
# NOTE: do NOT force MCPP_VERBOSE here. The e2e suite includes tests that
@@ -79,6 +84,10 @@ jobs:
7984
XLINGS_HOME="$MCPP_HOME/registry" "$MCPP_VENDORED_XLINGS" install \
8085
mcpplibs:cmdline@0.0.1 mcpplibs:cmdline@0.0.2 -y
8186
87+
- uses: ./.github/actions/e2e-mtest
88+
with:
89+
host: linux
90+
8291
- name: E2E suite
8392
# About twice the shard's budget of ten minutes (R4): reached only by a
8493
# hang. The per-test 600 s limit in run_all.sh names the test that hung.
@@ -98,6 +107,18 @@ jobs:
98107
"$MCPP" self config
99108
bash tests/e2e/run_all.sh
100109
110+
# The shard's store: main's are the history the next runs divide the suite by.
111+
- name: Upload the shard's store
112+
if: always()
113+
timeout-minutes: 5
114+
uses: actions/upload-artifact@v4
115+
with:
116+
name: e2e-store-linux-${{ matrix.shard }}
117+
path: tests/e2e/.mtest
118+
include-hidden-files: true
119+
if-no-files-found: ignore
120+
retention-days: 14
121+
101122
# One file, no glob: the report the e2e-coverage job of ci.yml reads.
102123
- name: Upload the shard's report
103124
if: always()

‎.github/workflows/ci-linux.yml‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,20 @@ jobs:
4848
with:
4949
host: linux-x86_64
5050

51+
# A pull request runs the tests its change affects (`mcpp test --since`,
52+
# docs/08; design 2026-11-08 W8): the base's tip is fetched, and when it
53+
# cannot be, every test runs.
54+
- name: The base of the change
55+
if: github.base_ref != ''
56+
shell: bash
57+
run: |
58+
if git fetch --no-tags --depth=1 origin "+refs/heads/$GITHUB_BASE_REF:refs/remotes/origin/$GITHUB_BASE_REF"; then
59+
echo "MCPP_TEST_SINCE=origin/$GITHUB_BASE_REF" >> "$GITHUB_ENV"
60+
fi
61+
5162
- name: Unit + integration tests via `mcpp test`
5263
run: |
53-
"$MCPP_FRESH" test
64+
"$MCPP_FRESH" test ${MCPP_TEST_SINCE:+--since "$MCPP_TEST_SINCE"}
5465
5566
# Each package under `modules/` carries its own tests, and they are built
5667
# as that package ALONE -- a configuration the root suite never produces,
@@ -81,7 +92,7 @@ jobs:
8192
echo "members: $(echo $members | tr '\n' ' ')"
8293
for m in $members; do
8394
echo "::group::mcpp test -p $m"
84-
"$MCPP_FRESH" test -p "$m"
95+
"$MCPP_FRESH" test -p "$m" ${MCPP_TEST_SINCE:+--since "$MCPP_TEST_SINCE"}
8596
echo "::endgroup::"
8697
done
8798

0 commit comments

Comments
 (0)