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
14 changes: 14 additions & 0 deletions .agents/docs/2026-11-08-mtest-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,3 +311,17 @@ mtest 不止是开发者写测试的接口:围绕测试结果的数据、分
- **M3** 已决定:首批项目 PR CI 墙钟中位数减半,发布 CI 关键路径缩短 30%,以迁移前测量的正式基线为准。
- **M4** 已决定:其他语言以工具支持与薄的 FFI 接口(C ABI 报告库)接入,不提供其他语言的完整测试库。
- **M5** 已决定:本地存储为按运行分文件的 NDJSON 加索引,可直接作为 CI 产物。

## 14. 用例的运行场所与机器能力(0.8.0)

**问题。** 0.7.0 的 `[capabilities]` 只由引擎在选择时求值。实现协议的其他运行器(`mcpp test`)与单独运行的程序不求值,使用方因而在用例上同时写标签 `requires:<词>`、在用例体内再次判断,并另维护一份探测表;用例必须通过的平台另写在 `[ci.coverage] hosts` 的表中,与用例分离。

**决定。**

1. **场所。** `only{...}` 与 `except{...}` 以 mcpplibs.os 的平台与架构陈述用例在何处运行:`only` 的平台之间、架构之间为"或",二者之间为"与";套件与用例的 `only` 取交集,`except` 累加。场所之外,用例跳过,原因为 `only on <场所> (this host: <系统> <架构>)` 或 `not on <场所> (...)`。库、Bash 与 Python 适配器由程序自行判定;引擎依据清单中的 `only`、`except` 同样判定,以覆盖只列出场所的实现(C ABI)。
2. **编译期选择。** 不新增宏或常量:mtest 重新导出 mcpplibs.os 的 `platform`、`architecture`、`can_run_on`、`host`、`on<P>`。`on<P>([&](auto){...})` 的函数体在不能运行于 `P` 的构建中被丢弃,承担 `if constexpr` 的作用;openkal 镜像在加载时得知 `host()`,与第 3.1 节一致。
3. **能力。** `capability` 以 `[capabilities]` 的同一组检验(系统、架构、PATH 上的程序、glob、命令、环境变量、`all`/`none`)加一个函数在代码中声明一次,由程序在每个进程中求值一次,并作为属性使用(`"x"_test[gcc]`,同时带标签 `requires:gcc`)。缺失时用例跳过,原因与引擎相同:`missing capability: <名> (<未成立的检验>)`。在模块接口中声明的能力先于导入它的单元初始化,因此命名空间作用域的套件可以使用。
4. **协议。** `case` 事件新增可选字段 `only`、`except`、`capabilities`(名到布尔)与 `reason`(`expect` 为 `skip` 时的原因)。引擎对程序已回答的能力词不再要求 `[capabilities]` 声明,并把答案记入运行记录;`[capabilities]` 继续服务于脚本程序。
5. **覆盖。** `mtest ci coverage` 要求陈述了 `only` 的用例在所收集运行覆盖到的每个场所上通过,且至少有一个运行位于其场所;场所由用例自身陈述,不再需要表格。

**兼容。** 新字段均为可选;未陈述场所与能力的程序、清单与配置的含义不变。
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,34 @@
# Changelog

## [0.8.0] - 2026-11-14

- **Where a case runs is stated by the case** (`only{}`, `except{}`). A case or a suite names the
platforms and architectures of `mcpplibs.os` it runs on (`only{platform::windows}`,
`only{platform::linux, architecture::x86_64}`) or not (`except{platform::macos}`); a suite's and
its case's narrow together. Elsewhere it is skipped with one reason, `only on windows (this host:
linux x86_64)`. The Bash adapter (`--only`, `--except`), the Python adapter (`only=`,
`excepted=`) and the C ABI (`mtest_case_only`, `mtest_case_except`) state the same; the engine
skips a listed case outside its places without starting it.
- **Code for one platform is selected when it is compiled.** mtest re-exports mcpplibs.os's
`platform`, `architecture`, `can_run_on`, `host` and `on<P>`: `on<platform::windows>([&](auto) {
... })` is a body that a build for another platform discards, and an ok image decides when it is
loaded.
- **What a machine can do is declared in code and answered by the program** (`capability`). The
tests of `[capabilities]` -- a system, an architecture, programs on PATH, a file a glob matches, a
command, environment values, other capabilities that must or must not hold -- and a function for
what they cannot state, answered once per process. A capability is an attribute
(`"links"_test[gcc]`, with the tag `requires:gcc`); a case whose machine lacks it is skipped,
`missing capability: gcc (no g++ on PATH)`, under any runner (`mtest`, `mcpp test`, the program
alone). The engine takes the program's answers: a word the program answers needs no
`[capabilities]` entry, and the run records it.
- **The listing says why a case is skipped, and where it runs** (P3.3): a `case` event may carry
`reason` (with `expect: skip`), `only`, `except` and `capabilities`; the schema states them. The
conformance suite checks them of every level-3 implementation (`placed`).
- **`mtest ci coverage` checks each case on its places**: a case that states `only` must pass on
each of its places that a collected run covered, and some run must have been on one -- what
`[ci.coverage] hosts` states of a program, stated by the case.
- Additive: a program, a listing and a configuration that state none of these mean what they did.

## [0.7.2] - 2026-11-13

- **Each call measured by itself** (`bench::each`, `mcpplibs.mtest.bench`). `bench::measure` times
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ int main() {

```toml
[dev-dependencies]
mcpplibs.mtest = "0.7.2"
mcpplibs.mtest = "0.8.0"
```

A failure names its place and both sides (`expected 2, actual 3`). Under a runner the same program
Expand All @@ -62,6 +62,12 @@ E2E cli{"cli", resource{"serial:home"}, timeout{std::chrono::minutes{2}}, [] {
}};
```

Where a case runs and what it needs of the machine are attributes too: `only{platform::windows}`,
`except{architecture::aarch64}`, and a `capability` declared once in code
(`const capability gcc{"gcc", {.program = {"g++"}}};`, then `"links"_test[gcc]`). The program
skips a case elsewhere, saying why, under any runner; `on<platform::windows>([&](auto) { ... })`
is code a build for another platform discards.

## Tests, with the engine

```bash
Expand Down
4 changes: 3 additions & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ int main() {

```toml
[dev-dependencies]
mcpplibs.mtest = "0.7.2"
mcpplibs.mtest = "0.8.0"
```

失败时给出位置与两侧的值(`expected 2, actual 3`)。在运行器之下,同一程序按协议报告;单独运行时每个用例输出一行。
Expand All @@ -57,6 +57,8 @@ E2E cli{"cli", resource{"serial:home"}, timeout{std::chrono::minutes{2}}, [] {
}};
```

用例在何处运行、需要机器具备什么,同样是属性:`only{platform::windows}`、`except{architecture::aarch64}`,以及在代码中声明一次的 `capability`(`const capability gcc{"gcc", {.program = {"g++"}}};`,再写 `"links"_test[gcc]`)。无论由哪个运行器启动,程序都在场所之外跳过用例并说明原因;`on<platform::windows>([&](auto) { ... })` 中的代码在其他平台的构建中被丢弃。

## 用引擎运行测试

```bash
Expand Down
2 changes: 1 addition & 1 deletion adapters/gtest/mcpp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
[package]
namespace = "mcpplibs.mtest"
name = "gtest"
version = "0.7.2"
version = "0.8.0"
description = "GoogleTest speaking the mtest protocol: a test main that lists, selects and reports GoogleTest's cases"
license = "Apache-2.0"

Expand Down
2 changes: 1 addition & 1 deletion adapters/gtest/src/protocol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ int mcpplibs_mtest_gtest_main(int argc, char** argv) {
{"protocol", 1},
{"level", 1},
{"framework", "googletest"},
{"version", "0.7.2"},
{"version", "0.8.0"},
{"isolation", {"process", "case"}},
{"guide", "https://github.com/mcpp-community/mtest/blob/main/adapters/gtest/docs/"
"writing-tests.md"}});
Expand Down
47 changes: 44 additions & 3 deletions adapters/script/mtest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,48 @@ _mtest_selected() {
# mtest_case ID FUNCTION [--tag T] [--kind K] [--clause C] [--resource R] [--artifact A] [--timeout SECONDS]
# [--isolate] [--retries N] [--diagnose-env NAME=VALUE] [--diagnose-timeout SECONDS]
# [--at-most METRIC=VALUE] [--at-least METRIC=VALUE]
# [--expect-fail REASON] [--skip REASON]
# [--expect-fail REASON] [--skip REASON] [--only PLACE] [--except PLACE]
# A PLACE is a platform (linux, macos, windows) or an architecture (x86_64, aarch64, riscv64): with
# --only the case runs only on one of the platforms named, when any is, and on one of the
# architectures, when any is; with --except it does not run on any named. Elsewhere it is skipped.
# A case that states no kind is of the program's (its directory's, docs/layout.md).
# This host's platform and architecture, in mcpplibs.os's names.
_mtest_host_platform() {
case "$(uname -s)" in
Linux*) echo linux ;; Darwin*) echo macos ;; MINGW*|MSYS*|CYGWIN*|Windows*) echo windows ;;
*) uname -s | tr '[:upper:]' '[:lower:]' ;;
esac
}
_mtest_host_arch() {
case "$(uname -m)" in
x86_64|amd64|AMD64) echo x86_64 ;; aarch64|arm64|ARM64) echo aarch64 ;; *) uname -m ;;
esac
}
_mtest_is_arch() { case "$1" in x86_64|aarch64|riscv64|wasm32) return 0 ;; esac; return 1; }

# _mtest_place_skip N ONLY... EXCEPT...: why the case does not run here, or nothing.
_mtest_place_skip() {
local n="$1"; shift
local system arch here words="" w platforms=0 archs=0 platform_ok=0 arch_ok=0 i=0
system="$(_mtest_host_platform)"; arch="$(_mtest_host_arch)"; here="$system $arch"
while [ $i -lt "$n" ]; do
w="$1"; shift; i=$((i + 1))
words="${words:+$words, }$w"
if _mtest_is_arch "$w"; then archs=1; [ "$w" = "$arch" ] && arch_ok=1
else platforms=1; [ "$w" = "$system" ] && platform_ok=1; fi
done
if { [ $platforms -eq 1 ] && [ $platform_ok -eq 0 ]; } || { [ $archs -eq 1 ] && [ $arch_ok -eq 0 ]; }; then
echo "only on $words (this host: $here)"; return 0
fi
for w in "$@"; do
if [ "$w" = "$system" ] || [ "$w" = "$arch" ]; then echo "not on $w (this host: $here)"; return 0; fi
done
}

mtest_case() {
local id="$1" fn="$2"; shift 2
local tags=() clauses=() resources=() artifacts=() kind="" limit="" xfail="" skip=""
local isolate="" retries="" diagnose=() diagnose_limit="" bounds=()
local isolate="" retries="" diagnose=() diagnose_limit="" bounds=() only=() except=()
while [ $# -gt 0 ]; do
case "$1" in
--tag) tags+=("$2"); shift 2 ;;
Expand All @@ -76,10 +112,13 @@ mtest_case() {
--at-least) bounds+=("min:$2"); shift 2 ;;
--expect-fail) xfail="$2"; shift 2 ;;
--skip) skip="$2"; shift 2 ;;
--only) only+=("$2"); shift 2 ;;
--except) except+=("$2"); shift 2 ;;
*) echo "mtest_case: unknown option $1" >&2; shift ;;
esac
done
_mtest_seen+=("$id")
[ -z "$skip" ] && skip="$(_mtest_place_skip "${#only[@]}" ${only[@]+"${only[@]}"} ${except[@]+"${except[@]}"})"
if [ "$_mtest_mode" = list ]; then
local ev
ev="\"e\":\"case\",\"id\":$(_mtest_json "$id"),\"name\":$(_mtest_json "$id"),\"file\":$(_mtest_json "${BASH_SOURCE[1]:-$0}"),\"line\":${BASH_LINENO[0]:-0},\"tags\":$(_mtest_list "${tags[@]}"),\"clauses\":$(_mtest_list "${clauses[@]}"),\"resources\":$(_mtest_list "${resources[@]}"),\"artifacts\":$(_mtest_list "${artifacts[@]}")"
Expand All @@ -103,7 +142,9 @@ mtest_case() {
done
ev+=",\"bounds\":[$list]"
fi
[ -n "$skip" ] && ev+=',"expect":"skip"'
[ ${#only[@]} -gt 0 ] && ev+=",\"only\":$(_mtest_list "${only[@]}")"
[ ${#except[@]} -gt 0 ] && ev+=",\"except\":$(_mtest_list "${except[@]}")"
[ -n "$skip" ] && ev+=",\"expect\":\"skip\",\"reason\":$(_mtest_json "$skip")"
[ -n "$xfail" ] && [ -z "$skip" ] && ev+=',"expect":"fail"'
_mtest_event "$ev"
return 0
Expand Down
39 changes: 37 additions & 2 deletions adapters/script/mtest_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def reads():
import inspect
import json
import os
import platform
import sys
import time

Expand Down Expand Up @@ -128,13 +129,42 @@ def _check_bounds(at_most, at_least):
_failure(f"metric {name} = {reported[name]:g}, under its bound {limit:g} (at_least)", depth=3)


_ARCHITECTURES = ("x86_64", "aarch64", "riscv64", "wasm32")


def _host():
"""This host's platform and architecture, in mcpplibs.os's names."""
system = {"Linux": "linux", "Darwin": "macos", "Windows": "windows"}.get(platform.system(), platform.system().lower())
machine = platform.machine()
arch = {"amd64": "x86_64", "AMD64": "x86_64", "arm64": "aarch64", "ARM64": "aarch64"}.get(machine, machine)
return system, arch


def _place_skip(only, excepted):
"""Why a case with these places does not run here, or None."""
system, arch = _host()
here = f"{system} {arch}"
platforms = [w for w in only if w not in _ARCHITECTURES]
archs = [w for w in only if w in _ARCHITECTURES]
if (platforms and system not in platforms) or (archs and arch not in archs):
return f"only on {', '.join(only)} (this host: {here})"
for w in excepted:
if w in (system, arch):
return f"not on {w} (this host: {here})"
return None


def case(id, tags=(), kind=None, clauses=(), resources=(), artifacts=(), expect_fail=None, skip_reason=None,
timeout=None, isolate=False, retries=None, diagnose=None, at_most=None, at_least=None):
timeout=None, isolate=False, retries=None, diagnose=None, at_most=None, at_least=None, only=(), excepted=()):
"""Declares a case: lists it, runs it, or passes over it, as the runner asks. A case that states no
kind is of its program's (its directory's, docs/layout.md). `timeout` (seconds), `isolate`,
`retries` and `diagnose` ({"env": {...}, "timeout": seconds}) are how a runner runs it (P3.3);
`at_most` and `at_least` ({metric: value}) bound its metrics (P6.5)."""
`at_most` and `at_least` ({metric: value}) bound its metrics (P6.5). `only` and `excepted` name
platforms (linux, macos, windows) and architectures (x86_64, aarch64, riscv64): the case runs only
on one of the platforms and one of the architectures `only` names, when it names any, and on none
`excepted` names; elsewhere it is skipped."""
at_most, at_least = dict(at_most or {}), dict(at_least or {})
skip_reason = skip_reason or _place_skip(list(only), list(excepted))
def declare(fn):
global _failed, _current
_seen.append(id)
Expand All @@ -154,8 +184,13 @@ def declare(fn):
fields["diagnose"] = diagnose
if at_most or at_least:
fields["bounds"] = _bounds_json(at_most, at_least)
if only:
fields["only"] = list(only)
if excepted:
fields["except"] = list(excepted)
if skip_reason:
fields["expect"] = "skip"
fields["reason"] = skip_reason
elif expect_fail:
fields["expect"] = "fail"
_event(**fields)
Expand Down
5 changes: 5 additions & 0 deletions adapters/script/tests/fixture.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ def holds():
pass


@mt.case("placed", only=["wasm32"])
def placed():
pass


@mt.case("bounded", at_most={"elapsed": 0.1})
def bounded():
mt.metric("elapsed", 0.5, "s")
Expand Down
1 change: 1 addition & 0 deletions adapters/script/tests/fixture.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ mtest_case "group/inner" nothing
mtest_case "measures" measures --kind bench
mtest_case "covers" nothing --kind cts --clause P3.1
mtest_case "holds" nothing --resource serial:db --artifact file:data/x.txt
mtest_case "placed" nothing --only wasm32
slow() { mtest_metric elapsed 0.5 s; }
mtest_case "bounded" slow --at-most elapsed=0.1
mtest_report
4 changes: 4 additions & 0 deletions capi/fixture/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ int main(void) {
mtest_case("holds", __FILE__, __LINE__, "unit");
mtest_case_resource("serial:db");
mtest_case_artifact("file:data/x.txt");
mtest_case("placed", __FILE__, __LINE__, "unit");
mtest_case_only("wasm32");
mtest_case("bounded", __FILE__, __LINE__, NULL);
mtest_bound("bounded", "elapsed", NAN, 0.1);
return mtest_done();
Expand All @@ -49,6 +51,8 @@ int main(void) {
run("measures", "pass", NULL, 0);
run("covers", "pass", NULL, 0);
run("holds", "pass", NULL, 0);
/* The C ABI lists places and leaves the decision to its program: this one runs nowhere else. */
run("placed", "skip", "only on wasm32", 0);
run("bounded", "pass", NULL, 0); /* its bound fails it */
return mtest_done();
}
6 changes: 6 additions & 0 deletions capi/include/mtest.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ void mtest_case_tag(const char* tag);
void mtest_case_clause(const char* clause);
void mtest_case_resource(const char* resource);
void mtest_case_artifact(const char* artifact);
/* Where the case last listed runs (0.8.0): a platform (linux, macos, windows) or an architecture
(x86_64, aarch64, riscv64). With `only` it runs only on one of the platforms named, when any is,
and on one of the architectures, when any is; with `except` on none named. The engine skips it
elsewhere, saying where; a program that runs alone decides for itself. */
void mtest_case_only(const char* place);
void mtest_case_except(const char* place);
/* How long the case last listed may run, in seconds (P3.3). */
void mtest_case_timeout(double seconds);
/* The case last listed runs in a process of its own (P3.3). */
Expand Down
2 changes: 1 addition & 1 deletion capi/mcpp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[package]
namespace = "mcpplibs.mtest"
name = "capi"
version = "0.7.2"
version = "0.8.0"
description = "mtest's C ABI: the protocol's events for any language with a C FFI"
license = "Apache-2.0"

Expand Down
14 changes: 12 additions & 2 deletions capi/src/mtest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ std::string text(const char* value) {

struct pending_case {
event body;
std::vector<std::string> tags, clauses, resources, artifacts;
std::vector<std::string> tags, clauses, resources, artifacts, only, except;
bool expect_fail = false;
double timeout = 0;
bool isolated = false;
Expand Down Expand Up @@ -78,6 +78,10 @@ struct state {
p.body["clauses"] = p.clauses;
p.body["resources"] = p.resources;
p.body["artifacts"] = p.artifacts;
if (!p.only.empty())
p.body["only"] = p.only;
if (!p.except.empty())
p.body["except"] = p.except;
if (p.expect_fail)
p.body["expect"] = "fail";
if (p.timeout > 0)
Expand Down Expand Up @@ -116,7 +120,7 @@ void mtest_init(const char* framework, int level) {
{"protocol", 1},
{"level", std::clamp(level, 1, 3)},
{"framework", text(framework)},
{"version", "0.7.2"},
{"version", "0.8.0"},
{"isolation", {"process", "case"}}});
}

Expand Down Expand Up @@ -161,6 +165,12 @@ void mtest_case_resource(const char* value) {
void mtest_case_artifact(const char* value) {
attribute(&pending_case::artifacts, value);
}
void mtest_case_only(const char* value) {
attribute(&pending_case::only, value);
}
void mtest_case_except(const char* value) {
attribute(&pending_case::except, value);
}

void mtest_case_timeout(double seconds) {
auto& s = the();
Expand Down
2 changes: 1 addition & 1 deletion conformance/mcpp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
[package]
namespace = "mcpplibs.mtest"
name = "conformance"
version = "0.7.2"
version = "0.8.0"
description = "The conformance suite of the mtest protocol, for every implementation of its program side"
license = "Apache-2.0"

Expand Down
1 change: 1 addition & 0 deletions conformance/src/fixture.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ int main() {
};
"covers"_test[kind::cts][clause{"P3.1"}] = [] {};
"holds"_test[resource{"serial:db"}][artifact{"file:data/x.txt"}] = [] {};
"placed"_test[only{architecture::wasm32}] = [] {};
"bounded"_test[at_most{"elapsed", 0.1}] = [] { metric("elapsed", 0.5, "s"); };
return report();
}
Loading
Loading