diff --git a/.agents/docs/2026-11-09-target-model-design.md b/.agents/docs/2026-11-09-target-model-design.md new file mode 100644 index 000000000..5f2304581 --- /dev/null +++ b/.agents/docs/2026-11-09-target-model-design.md @@ -0,0 +1,413 @@ +--- +subject: target-model +status: active +--- + +# 目标与提供者:统一的目标模型(结合 mcpp#911) + +**日期:** 2026-11-09 +**状态:** 定稿(2026-11-09):Q1–Q7、R1–R8、第 15 节与第 17 节 T1–T6 已确认(2026-11-09);第 16 节为综合自审,第 18 节为任务与依赖 +**范围:** mcpp(目标的身份、行、提供者与选择;新 SPEC-016 及 SPEC-002、SPEC-013、SPEC-015 的修订);mcpp-index 描述符的 +匹配;openkal-image(openkal-run 的能力槽) +**输入:** 2026-11-09 关于交叉构建的讨论;mcpp#911(移植 ChenPi11/cmd 到 ok 映像、FreeBSD 与 i386 时测得的 G1–G12 与 B1–B3) + +## 1. 问题 + +SPEC-013 §0 规定引擎不持有任何平台的知识,§14 又规定宿主目标的约定留在引擎中、无平台包。两种体系因此并存:openkal 的目标由 +包定义,带版本、来源与覆盖;原生目标的事实写在引擎中:封闭的目标词汇(`triple.cppm`)、每行注入的参数 +(`freestanding/target.cppm`)、以代码判定的 hosted(`-ffreestanding`)、固定的链接方式、以本机为中心的运行时校验、封闭的描述 +符键。提供各层的来源与在来源之间的选择,散在三元组补全、目标表的 pin、平台列与 `toolchain_decision` 的拒绝中,彼此以代理条件 +推断。#911 的 G1–G6、G10,与 `--target x86_64-windows --toolchain llvm` 被拒绝,都由此而来。 + +## 2. 原则 + +**机制在引擎,事实在数据。** 引擎持有: + +- 轴:arch、os、env、对象格式、数据模型、C 环境(hosted 或 freestanding)、执行宿主; +- 层名的闭集(SPEC-002 §1.1); +- 编译器族的驱动知识(SPEC-002 §1.2:编译器不由包提供); +- 协议:合并、来源、校验、打包、运行器、选择规则。 + +哪些系统存在、某一行用何种代码模型、产物由哪个加载器启动、谁提供 C 库,皆为数据。原生约定与 openkal 平台以同一形式表达。 + +## 3. 概念 + +| 概念 | 定义 | +|---|---| +| 请求 | 用户写出的 `--target`、`[build] target` 或 `[target.X]` 的键:`arch-os` 或 `arch-os-env`,是一个模式 | +| 身份 | 产物是什么:由选择得到的完整三元组。决定输出目录、缓存键、`cfg(...)` 与 ABI 标签 | +| 行 | 一个身份的定义(第 4 节) | +| 提供者 | 一个身份的一份完整供给:工具链、各层的来源、链接模型、C 环境、执行环境、默认参数(第 5 节) | +| 选择 | 由请求得到身份与提供者的唯一规则(第 6 节) | + +## 4. 身份与行 + +- **I1. env 即 C 库。** 一个系统可能用到两种以上 C 库时,身份含 env。 +- **I2. 系统的原生身份沿用业界写法。** 业界原生标识无 env 段的系统(macOS、BSD),其原生身份为两段(`aarch64-macos`、 + `x86_64-freebsd`);其余 C 库以 env 区分(`aarch64-macos-musl`)。`-openkal` 为与系统无关的映像身份,两段。 + 其余系统的身份为三段(`x86_64-linux-gnu`、`x86_64-windows-msvc`)。 +- **I3. 行是数据。** 行的字段:`triple`、`arch`、`os`、`env`、`standard`(是否为业界标准身份)、`clang-triple`、 + `object-format`、`data-model`、`exe-suffix`、`loader`、`code-model`、`link-emulation`、`execution`。内置行随 mcpp 发布; + 平台包以 `[[platform.define]]` 定义新行,同一 schema(G1)。解析器接受任何已解析平台定义的 os 与 env;`cfg(os = …)` 按行 + 求值;32 位 x86 的内置行 `i686-none-elf`(空代码模型,`elf_i386`)随内置数据提供(G12)。 +- **I4. 身份与宿主无关。** 同一身份在任何宿主上是同一输出目录、缓存键与 `cfg`。 +- **I5. 身份如实。** 身份为选择的结果,含实际 C 库的 env;产物格式随目标系统:身份为 `-openkal` 时为 ok 映像,否则为 + 该系统的原生格式(ELF、PE、Mach-O),即使各层由 openkal 平台提供。 + +## 5. 提供者 + +提供者以 SPEC-013 §1 的 `[platform]` schema 表达,增加: + +| 键 | 语义 | 对应 | +|---|---|---| +| `serves` | 所服务的身份 | 取代目标表的 pin 与 `hosted.targets` | +| `priority` | 整数,越大越先;同一请求的候选按此排序(Q6) | 第 6 节的顺序 | +| `hosted` | C 环境是否 hosted;sysroot 或 c-abi 提供者亦可声明 `hosted-standard-library` | G3 | +| `[platform.link] driver` | 在每一行生效;`"compiler"` 交由编译器驱动加入启动文件与库 | G4 | +| `execution` | 产物的执行环境(系统、加载器) | G5、G11 | +| 默认参数 | 行的机器描述不可替换;机器不同的目标是另一行(G1、G12);每个参数的来源由 `mcpp why flags` 写出 | G2 | +| `pairs` | 可与之配对的编译器族(如 mingw 提供者:gcc、llvm) | 第 7 节 | + +**内置提供者。** 原生约定改写为随引擎发布的提供者:glibc payload(`*-linux-gnu`,native)、musl-gcc(`*-linux-musl`, +native)、Apple SDK(`*-macos`,native)、MSVC(`*-windows-msvc`,native)、mingw(`*-windows-gnu`,convention)、Android NDK、 +Emscripten SDK。openkal-platform 的 hosted 部分为提供者,服务 `*-linux-musl`、`*-windows-musl`、 +`*-macos-musl`(待验证,第 11 节)。 + +**存放。** 内置行与提供者为 mcpp 源码树中的数据文件,由构建程序(`build.mcpp`)编入引擎,不提交生成物;运行时不联网、 +不进入依赖解析,「每个 mcpp 固定一份」的可复现性不变。 + +## 6. 选择 + +1. **显式者优先。** `--platform`、`MCPP_TARGET__PLATFORM`、项目与 config 的 `platform`、显式工具链;依赖图提供的层亦属显式 + (层报告为 `graph`)。`--platform none` 只用 `native` 与 `convention` 提供者,不用平台包。 +2. **请求是模式。** 写出 env 的请求只匹配该身份;只写 arch-os 的请求匹配该系统的全部身份。I2 中原生身份为两段的系统,请求 + `aarch64-macos` 同时是该身份的写法与模式:候选的第一个是原生身份。 +3. **顺序是数据。** 每个提供者带一个整数 `priority`,同一请求的候选按它从大到小排序,相同者按行的顺序、再按提供者名。 + 内置数据给出默认值:各系统的标准工具链与 C 库(glibc、musl-gcc、Apple SDK、MSVC、BSD 的系统 libc)较高,跨系统平台 + (openkal-platform)居中,其他约定(mingw)较低。引擎不含类别,也不含任何实现名(SPEC-002 §1.1);所有提供者同一 + 结构,只有数值不同。平台包为自己的提供者声明 `priority`;项目与 config 可按选择器改写: + `[target..priority] = `,来源与其他值一样记录。优先级的调整(如 openkal-glibc 就绪后的 + `*-linux-gnu`)是一次数据改动,附 CHANGELOG。 +4. **取第一个可服务者。** 提供者服务该身份,且其 payload 或包在本宿主可得;工具链族被显式指定时,只取与该族配对的提供者 + (`pairs`)。 +5. **报告。** 状态行沿用现有格式:`Target <写法> → <身份>`,其下为各层的来源;发生回落(第一个候选在本宿主不可服务)时,`Target` + 行附一句原因(`<提供者> is not available here`)。 +6. **皆不可服务时拒绝。** 诊断写出该身份所需、每个候选提供者不可用的原因、可直接执行的命令。 +7. **原生构建**(不写 `--target`)取宿主的 `native` 提供者,结果与现在相同。 + +| 请求 | 候选(依次) | 典型结果 | +|---|---|---| +| `x86_64-linux` | `-linux-gnu`:glibc payload;`-linux-musl`:musl-gcc、openkal-musl | Linux:`-gnu`;无 glibc 与 musl-gcc payload 的宿主:`-musl`(openkal) | +| `x86_64-linux-musl` | musl-gcc、openkal-musl | Linux:musl-gcc;macOS:openkal | +| `x86_64-windows` | `-windows-msvc`:MSVC;`-windows-musl`:openkal;`-windows-gnu`:mingw | 有 MSVC 的 Windows 宿主:`-windows-msvc`;其余:`-windows-musl`(Q4) | +| `x86_64-windows-gnu` | mingw(gcc 或 llvm 配对) | mingw | +| `aarch64-macos` | `aarch64-macos`:Apple SDK;`aarch64-macos-musl`:openkal | macOS:原生;其他宿主:`-musl`(openkal) | +| `-openkal` | openkal-platform | ok 映像 | + +## 7. llvm 与 mingw payload + +本机实测(2026-11-09,clang 23.1.3,`xim-x-mingw-cross-gcc/16.1.0`):C++ 的静态程序与 `import std`(clang 预编译 libstdc++ 的 +`bits/std.cc`,`std::println`)均在 wine 下正常运行;导入表为 `KERNEL32.dll`、`msvcrt.dll`,与 mingw gcc 产物同一 ABI。参数: +`--no-default-config --target=x86_64-w64-mingw32 --sysroot= -stdlib=libstdc++ -rtlib=libgcc -fuse-ld=lld`;静态 +链接另加 `-pthread`(该 mingw 为 posix 线程模型,clang 驱动不自动加入 winpthread);`std::print` 的终端函数在 +`libstdc++exp.a`,与 gcc 一行的处理一致。 + +## 8. 与 #911 其余各项及分发的衔接 + +| 项 | 处置 | +|---|---| +| G6 描述符 | 源码制品一份、与目标无关;预编译 payload 按身份的轴匹配,含 `any` 与回落;`linux`、`macosx`、`windows`、`openkal`、`none` 块作为轴组合的别名读取,已有索引条目不变;`descriptor_keys` 照旧可读,不再必需 | +| G7 能力槽 | SPEC-002 的层机制推广到普通包:`requires` 加按系统的默认提供者;解析顺序为依赖图 > 平台 > 默认值。openkal-run 的清单随之改写 | +| G8 补丁根 | 包根(解析后的清单所在目录)或显式 `patch_root`;`mcpp why deps` 报告包根,补丁由 `git diff` 写出 | +| G9 按产物的编译变体 | `[targets.X.build] defines/cflags/cxxflags` 作用于该产物链接的本包翻译单元,在 `obj/_variant//` 编译变体;`[targets.X] defines/flags` 维持只作用于入口;模块接口每包一个,不做变体 | +| G10 载体与打包 | 任何已解析平台能构建的行,按平台声明作载体或打包的系统 | +| G11 执行宿主 | 运行器协议:传输、运行、输出流、退出码,由 runner 包实现(VM、ssh、设备);无运行器的异系统产物为「已构建,未运行」 | +| B1 | 生成的清单以 TOML 引号书写含 `.` 的目标名 | +| B2 | 平台规定的产物后缀不被宿主后缀规则覆盖;冲突在规划期报告 | +| B3 | 多个程序可打包时照旧打包(以包命名者,否则按名字的第一个)并说明其余程序与两种选择写法(`mcpp pack `、`--all-programs`) | +| 构建插件(SPEC-007) | SPEC-013 §1.2 的分工不变:行、提供者、层只能声明(解析工具链与依赖图时尚无程序可运行);`build`、`link`、`runner` 可由插件陈述,来源记录;构建程序的 `artifact-name` 不改变平台的产物格式 | + +## 9. 多视角自审 + +| 视角 | 发现 | 处置 | +|---|---|---| +| 一致性 | 第四版把「类别」同时放在行与提供者上,含义不清 | 行只标 `standard`(是否业界标准身份);排序只看提供者的 `priority`(Q6) | +| 一致性 | 「只有 openkal 是两段」与 macOS 原生无 env 并存 | I2:业界原生标识无 env 的系统,原生身份为两段;其余 C 库以 env 区分 | +| 一致性 | 依赖图提供 musl 而请求为 `x86_64-linux` 时,现有身份为 `-linux-gnu`,报告却写 `c-abi musl`(target_side.cpp 已记录此矛盾) | I5:身份取实际 C 库,为 `-linux-musl`。这是第二处默认行为变化(第 10 节),需确认(Q5) | +| 一致性 | 第四版把 MSVC 列为 Windows 的原生标准却未进入候选 | 候选按类别排序后 MSVC 在首位;无 MSVC 的宿主(Linux、macOS)自然落到 openkal,Windows 宿主的取向由 Q4 决定 | +| 兼容性 | 原生约定由代码移为数据,任何偏差都会改变产物 | 等价性测试:迁移前由现有代码表生成每行的编译与链接参数快照,迁移后由数据生成并逐行比对,二者相同才删除旧代码 | +| 兼容性 | 新清单键(`[[platform.define]]`、`serves`、`priority`、`execution`、`pairs`)在旧 mcpp 上 | 旧 mcpp 对未知键警告(SPEC-013 §1.1);使用新键的平台包声明 `[package] mcpp` 下限 | +| 可复现性 | 同一模糊请求在不同宿主可能解析为不同身份 | 身份与宿主无关(I4);需要确定 ABI 的场合写完整身份;构建报告与机器输出记录「请求 → 身份」;锁按身份记录目标(与 #903 一致) | +| 缓存与 ABI | `-linux-musl` 可由 musl-gcc 或 openkal-musl 提供;二者的预编译产物是否互换 | 缓存键已含工具链与平台的指纹,二者不共享缓存;索引中按身份匹配的预编译 payload 要求 C 层 ABI 兼容,openkal-musl 与 musl-gcc 的 musl 版本差异列为验证项 | +| 跨平台 | 从 macOS 构建 `-linux-gnu` 需要 glibc payload 能装在 macOS 宿主上 | glibc payload 是目标数据,属 G6 的按身份匹配;在其可得之前,该身份在 macOS 宿主上不可服务,请求 `x86_64-linux` 回落到 `-musl` | +| 跨平台 | macOS 无 SDK 的链接(dyld、libSystem 的存根) | 列为验证项(第 11 节);验证前 `aarch64-macos-musl` 在非 macOS 宿主上标为 preview | +| 安全 | 平台包可以定义行 | 平台包本就能注入参数;定义的行不能覆盖内置行;`mcpp why flags` 写出每个参数的来源层 | +| 性能 | 选择不得拉长构建的准备阶段 | 内置行与提供者由构建程序编为引擎中的静态表,运行时不解析任何文件;平台包只在被选中或已解析时读取(与现在相同);候选按优先级依次检查可得性(本地 store 的存在性),取到第一个即停;结果在一次构建内缓存;`--timings` 报告选择所用时间,e2e 断言其上界 | +| 用户体验 | 新增提示可能冗余 | 只在发生回落时附一句原因;其余沿用现有的「写法 → 身份」与层来源行 | +| 诊断 | 拒绝时用户不知道下一步 | 第 6 节第 6 条:所需、各候选的原因、可执行的命令 | + +## 10. 行为变化 + +1. 只写 arch-os 的 Windows 请求:在无 MSVC 的宿主上由 `-windows-gnu`(mingw)改为 `-windows-musl`(openkal)。输出目录与 + `cfg(env)` 随之改变;CHANGELOG 记载,简写解析时提示一次。 +2. 依赖图提供的 C 库与请求的默认 env 不同时,身份取实际 C 库(Q5):例如依赖 openkal-musl 的项目以 `x86_64-linux` 构建,身份由 + `x86_64-linux-gnu` 改为 `x86_64-linux-musl`,输出目录随之改变。 + +其余:现有身份字符串、输出目录与 `cfg` 结果不变;`[target.X]`、`descriptor_keys`、`platform`、`[platform.hosted]` 的写法照旧 +可读,`hosted.targets` 视为 `serves` 的别名。 + +## 11. 验证项 + +- `aarch64-macos-musl` 在非 macOS 宿主上的链接与运行(无 SDK); +- openkal-musl 与 musl-gcc 的 musl 在 C 层 ABI 上的兼容范围(预编译 payload 是否可互用); +- FreeBSD 平台定义的行:静态与动态链接、运行时校验、`cfg(os = "freebsd")`; +- i686 行:空代码模型与 `elf_i386`。 + +## 12. 决定 + +| 编号 | 事项 | 状态 | +|---|---|---| +| Q1 | 所有目标由提供者服务,原生约定改写为内置提供者 | 已确认 | +| Q2 | 新 SPEC-016「目标与提供者」;SPEC-013 §0、§3、§14,SPEC-002 §3.4、§3.5,SPEC-015 §1.2 改为引用之;docs/21、22、35 同步 | 已确认 | +| Q3 | mcpp 一个 release PR 实现第 4–8 节与第 9 节的处置;openkal-image(openkal-run 的能力槽)一个 PR,依赖 mcpp 的发布 | 已确认 | +| Q4 | 宿主有原生提供者时取原生;Windows 宿主有 MSVC 时只写 arch-os 的 Windows 请求取 MSVC,否则取 openkal 平台(优先级次之)。即第 6 节规则的直接结果,无 Windows 专门规则 | 已确认 | +| Q5 | 身份取实际 C 库(第 10 节第 2 条)。理由:预编译 payload 按身份的轴匹配(G6),身份不如实即取错预编译库;同一系统的 C 库增多(openkal-glibc、BSD)后 `cfg(env)` 与 ABI 标签须如实;现有报告的不一致由此消除 | 已确认 | +| Q6 | 提供者在结构上统一,只以整数 `priority` 排序;默认值是数据,平台包、项目与 config 可以改写;引擎不含类别与实现名 | 已确认 | +| Q7 | #906(2026.11.8.1)先合入并发布;本设计为其后的版本,单独一个 PR | 已确认;2211fb5c 合入,release 已触发 | + +## 13. 实现与测试 + +| 部分 | 位置 | +|---|---| +| 行与内置提供者的数据、编入引擎的构建程序 | 新数据目录;`build.mcpp` | +| 三元组:开放的 os 与 env,按行求值 | `modules/toolchain-model/src/triple.cppm` | +| 选择 | `prepare/toolchain.cpp`(`step1_resolve_target_triple_request` 起)、`prepare/platform.cpp`、`platform.cppm` | +| 提供者的编译与链接 | `toolchain/hostflags`、`toolchain/linkmodel`、`build/flags.cppm`、`toolchain/stdmod.cppm`、`freestanding/*` | +| hosted、`driver = "compiler"`、执行环境与运行时校验 | `freestanding/target.cppm`、`build/ninja_backend.cppm`、`build/runtime_validation.cppm` | +| 描述符的轴匹配、补丁根、按产物的变体、载体与打包、运行器协议 | `pm/*`、`pm/patch.cppm`、`build/plan.cppm`、`pack/*`、`run/*` | +| 诊断与报告 | `prepare/toolchain_decision.cpp`、`ui`、机器输出、`mcpp why target` | +| 统一的包引用与 git 的缺省引用 | 依赖的引用解析器(`pm/*`)、平台与运行器的引用读取、命令行与环境变量的字符串形式 | +| B1–B3 | `pack/pack.cppm`、`pack/manifest_emit.cppm`、`pack/distribution.cppm`、`pack/pipeline.cppm` | +| 规范与文档 | SPEC-016(新);SPEC-001、SPEC-002、SPEC-013、SPEC-015 的修订;docs/05、20、21、22、30、35(中英) | +| 测试 | 等价性测试(第 9 节);e2e:只写 arch-os 的交叉构建(payload 可得与不可得)、`-windows-gnu` 配 llvm(含 `import std`,wine 运行)、显式身份不被替换、原生构建不变、显式平台与依赖图提供者、平台定义的 FreeBSD 行、i686 行、hosted 声明、`driver = "compiler"`、异系统产物的运行时校验、能力槽、补丁根、按产物的变体、B1–B3、拒绝时的诊断;引用:命令行的 `git+` 引用(缺省、`tag`、`branch`)、`path` 引用、`[indices]` 中未进索引的描述符、清单引用入锁与命令行引用不入锁、`--locked` 拒绝未锁定的分支、离线时的行为 | + +## 14. 使用者视角的自审 + +| 视角 | 关心 | 发现 | 处置 | +|---|---|---|---| +| 应用开发者 | 写一个 target 就能构建;结果可预期 | 同一模糊请求在不同宿主可能得到不同身份,团队协作时不易察觉 | R1:锁按请求记录解析出的身份;另一宿主解析不同时写出一行说明(请求、锁中的身份、本次的身份),`--locked` 下拒绝 | +| 应用开发者 | 出错时知道怎么办 | 拒绝的诊断已有原则(第 6 节第 6 条),但没有「先问后建」的入口 | R2:`mcpp why target [<请求>]`(与现有的 `why toolchain/runtime/deps` 同族)列出候选、各自的优先级与可得性、选中者与原因;`--format json` | +| 应用开发者 | 不必学新概念 | 「行」「提供者」「优先级」是实现与平台作者的概念 | R3:用户文档只用「请求的 target」「构建的 target」「提供者」三个词;「行」「priority」只出现在 SPEC-016 与平台作者文档 | +| 应用开发者、平台开发者 | 指定用谁 | 「选一个提供者」现在有 `platform`、`toolchain`、`--platform none` 三种写法,语义交叠 | R4:内置提供者有名字(如 `builtin:msvc`、`builtin:mingw`、`builtin:glibc`);`platform` 选择一个提供者,名字或包引用皆可;`--platform none` 表示不用平台包、只用内置提供者,含义不变;`toolchain` 只选编译器族与版本,与提供者按 `pairs` 配对。不新增键或标志 | +| 库开发者 | 条件依赖与预编译 payload 按 target 匹配 | Q5 使依赖图供给 C 库的项目身份改变,以 `cfg(env = "gnu")` 写的条件可能不再成立 | R5:CHANGELOG 与 docs/22 写出迁移:按 C 库写的条件应以实际 C 库为准;`mcpp why target` 显示 `cfg` 求值所用的身份 | +| 平台开发者 | 用最少的键写出一个平台 | `[platform]` 与 `[platform.hosted]` 是两个特例的提供者,第三个提供者没有写法 | R6:一般形式 `[[platform.provider]]`(`serves`、`priority`、`toolchain`、`layers`、`link`、`hosted`、`execution`、`pairs`);`[platform]` 与 `[platform.hosted]` 为其两种简写,照旧可读。`mcpp new --template platform` 生成一个最小平台;读取平台清单时校验行与提供者的一致与 `priority` 冲突;`mcpp why platform` 列出它服务的身份 | +| 构建插件开发者 | 构建程序读到的 target 与引擎一致 | `mcpp::target_os()` 等访问器的文档以封闭集合描述取值;新系统的 os 值须照样流过 | R7:访问器返回身份的轴(字符串,取值由行定义);新增 `mcpp::target_provider()`(`MCPP_TARGET_PROVIDER`);docs/30 的取值表改为「由行定义,内置行为……」 | +| agent | 可脚本化、确定、可解释 | 构建的机器输出只有身份,没有请求与提供者;拒绝没有稳定的代码 | R8:机器输出的 target 记录为 `{request, identity, provider, priority, fallback}`;选择的拒绝有稳定代码(`target-unserved`、`target-ambiguous` 等,进 SPEC-016 的拒绝码表);文档建议自动化写完整身份 | +| mcpp 开发者 | 迁移不引入回归,代码不再散落 | 原生约定由代码移为数据是一次大改 | 第 9 节的等价性快照为合入条件;选择逻辑集中于一个模块,旧的代理条件(目标表 pin、`toolchain_decision` 的拒绝、三元组补全中的推断)随迁移删除,不保留双轨 | + +**简洁性。** 概念:请求、身份、提供者(用户文档三个词);数据:行、提供者(同一 schema);规则:一条(显式者优先,其余按 priority 取第一个可服务者)。新增的用户面:`mcpp why target`、`[target..priority]`、内置提供者的名字;其余为已有写法的推广。 + +## 15. 统一的包引用 + +凡是「指名一个要取得的东西」的位置,使用依赖的同一套引用文法与解析机制(SPEC-001): + +| 位置 | 现在 | 统一后 | +|---|---|---| +| `[dependencies]` | 版本、`path`、`git`(SPEC-001) | 不变,为文法的来源 | +| `platform`(含 `--platform`、环境变量) | 包引用(字符串或表,含 `path`、`git`);命令行 `path:` | 同一文法;增加 `builtin:`(R4)与命令行的 git 形式 | +| `[platform.layers]`、`tools`、`carrier` | 包引用 | 不变 | +| 运行器包(G11) | 无 | 同一文法 | +| `toolchain`(含 `--toolchain`) | `<族>@<版本>`、`xim:<包>@<版本>`、`path:` | 不变(编译器不由包提供,SPEC-002 §1.2);`<族>@<版本>` 为 `xim:` 的简写 | + +**文法。** TOML 中为字符串或表(`name`、`version`、`path`、`git` 与 `tag`/`rev`/`branch`、`features`),即现有的包引用;命令行与环境变量 +为字符串:`[.]@<约束>`、`path:`(或以 `/`、`./`、`../`、`~`、盘符开头的路径)、 +`git+[#tag=|rev=|branch=]`、`builtin:`。`git+` 之后接 `https://`、`ssh://` 或 scp 形式 +(`git@host:owner/repo`)的地址。 + +**缺省的引用。** git 引用不写 `tag`、`rev`、`branch` 时,取远端的默认分支:由 `git ls-remote --symref HEAD` 得到其名字(`main`、 +`master` 或其他),不在引擎中写死分支名;远端不通告默认分支时,依次尝试 `main` 与 `master`,并在报告中写出所用的分支。缺省引用 +按 `branch` 处理(锁记录解析得到的提交)。`[dependencies]` 的 git 依赖同样适用:SPEC-001 由「`tag`、`rev`、`branch` 恰好一个」改为 +「至多一个,省略即默认分支」;原先被拒绝的写法由此成为合法,已有清单不受影响。 + +**未分发的仓库。** 符合 mcpp-index 分发模型、含 `mcpp.toml` 的仓库或目录,以 `path`、`git` 引用,身份取其清单(SPEC-001 §5.4), +与索引中的包同样解析、获取与锁定。只有 xpkg 描述符(`.lua`)而未进索引的,以项目的 `[indices]`(`path` 或 `git`)声明为一个索引, +其中的包以普通的 `.@<版本>` 引用。不新增第三种来源。 + +**锁。** 清单中写出的引用记入 `mcpp.lock`(git 记录解析得到的提交);只在命令行或环境变量中出现的引用是一次构建的选择,不改写锁, +记入构建报告与机器输出(R8)。 + +**跨平台。** 路径按 SPEC-013 §3.2 的规则识别(含 Windows 盘符);git 取得与 `[dependencies]` 的 git 依赖同一实现与缓存。 + +**稳定性与安全。** `branch` 引用不可复现:报告写出解析得到的提交,`--locked` 下拒绝未锁定的 `branch`;引用带来的构建程序(平台插件) +与 git 依赖受同样的信任规则(`untrusted_because`)。 + +**性能。** git 引用按(仓库、提交)缓存于 store,浅克隆;`path` 引用不复制;同一来源在一次构建中只解析一次。 + +## 16. 综合自审(全文) + +| 视角 | 检查 | 结论或修订 | +|---|---|---| +| 一致性 | 引用文法是否处处同一 | 依赖、平台、层、工具、载体、运行器包同一文法与解析器;`builtin:` 只用于提供者;`toolchain` 保留自己的写法,理由在 SPEC-002 §1.2 | +| 一致性 | 第 5 节的提供者 schema 与 R6 的 `[[platform.provider]]` | 第 5 节的键即 `[[platform.provider]]` 的键;`[platform]`、`[platform.hosted]` 为其两种简写(R6) | +| 一致性 | git 的缺省引用与依赖 | 两处同一规则(第 15 节),SPEC-001 随之修订 | +| 可复现性 | 缺省引用与 `branch` 会移动 | 清单中的引用由锁固定到提交;命令行的引用每次按远端解析,报告写出提交;`--locked` 下拒绝未锁定者 | +| 离线 | 无网络时 | 锁中已有提交且 store 中已取得:直接使用;否则以「需要网络解析 」拒绝,并给出 `--offline` 的含义(只用已取得的) | +| 安全 | 命令行的 git 引用带来构建程序 | 运行前写出来源与提交;信任规则同 git 依赖(`untrusted_because`) | +| 跨平台 | 路径、URL 形式 | 路径识别含 Windows 盘符(SPEC-013 §3.2);git 地址的三种形式在各系统一致 | +| 性能 | 选择与引用不得拉长准备阶段 | 内置数据为静态表;可得性检查只读本地 store;git 按(仓库、提交)缓存、浅克隆,缺省引用每次一次 `ls-remote`;一次构建内每个来源只解析一次;`--timings` 报告选择与引用解析的时间 | +| 用户体验 | 新增的写法是否必要 | 新增的用户面:`mcpp why target`、`[target..priority]`、`builtin:`、命令行的 `git+` 形式;其余为已有写法的推广 | +| agent | 可脚本化 | 机器输出的 target 记录(R8)与引用记录(来源、解析得到的提交、是否来自命令行);拒绝码稳定 | +| 可维护性 | 实现集中 | 选择一个模块、引用一个解析器(依赖的那一个);旧的代理条件随迁移删除;等价性快照为合入条件 | +| 规范 | 受影响的规范 | 新 SPEC-016;修订 SPEC-001(git 的缺省引用;引用文法的适用范围)、SPEC-002(§3.4、§3.5、hosted 声明、能力槽)、SPEC-013(§0、§1 的 provider 一般形式、§3、§14)、SPEC-015(§1.2);docs/05、20、21、22、30、35 | + + +## 17. 测试与构建基础设施(2026-11-09 补充) + +同一 mcpp 发布包含以下几项。它们与目标模型同属引擎的契约,不另开 PR。 + +### 17.1 单元测试迁移到 mtest + +mcpp 的单元测试(`tests/unit/` 166 个文件,以及各成员的 `modules//tests/`)由 GoogleTest 改写为 mtest 的用例对象: +- 每个 `TEST` 改写为一个 `UTest` 对象,标识为 `Suite/Case`,文件中不写 `main`; +- 夹具改写为派生自它的用例; +- 只在失败时才有效的消息改为惰性求值; +- 启动自身副本的测试以用例标识选择副本,并清除副本中的协议变量; +- 标准流捕获使用 mcpplibs.os 0.4.1 的 `stdio::capture`。 + +已实现,全部用例通过(根 166、成员 10)。 + +### 17.2 构建缓存的分级 + +实测(mcpp 仓库,一个配置): +- 配置目录共 5.5G,其中 `bin/` 占 5.1G:166 个测试程序,每个约 31MB,因为每个都链接了全部 483 个目标文件; +- `obj` 为 164M,`gcm` 为 154M; +- `mcpp test -p <成员>` 另开一个配置目录,重编译成员; +- 全局缓存中的对象以复制方式进入配置目录。 + +分级与规则: + +| 层 | 位置 | 键 | 内容 | +|---|---|---|---| +| global | `~/.mcpp/build-cache/v1` | 包身份与包配置指纹 | 索引包、std 模块、宿主工具;以硬链接(或 reflink)物化,不复制 | +| workspace | `target/.cache/pkg/<包>@<指纹>` | 成员自身的配置指纹 | 工作区成员(path 包)的对象、BMI 与静态库形态;根构建、`-p`、build 与 test 共用 | +| package | 同上一条目 | — | 一个包的编译结果 | +| target | `target//<配置指纹>/` | 消费者配置 | 仅链接产物 | + +测试程序链接各包的静态库形态,加 `--gc-sections`,只拉入被引用的部分。 +- 实测 test_diag:由 32.1MB 降至 14.7MB,代码段由 17.5MB 降至 2.7MB。 +- 支持时使用 `-gsplit-dwarf`,调试信息按对象只存一份。 +- 依靠静态构造自注册、无符号被引用的对象,以 whole-archive 链入。 +- `[test] link = "objects"` 保留逐对象链接作为退路。 + +配置目录与 workspace 缓存按指纹失效和 LRU 回收,由 `mcpp cache gc` 与 `mcpp clean --stale` 执行。 + +### 17.3 测试程序的组成与用例级调度 + +- 一个测试目录中只声明对象、编译与链接配置相同的文件,链接为一个测试程序(每包每目录一个)。缺省即此。 +- 自带 `main` 或有特殊参数的文件单独成程序;`[test] program = "file"` 恢复逐文件成程序。 +- 调度粒度为用例:引擎以协议列出用例,以同一程序的 N 个进程执行,每个进程由 `MTEST_SELECT` 选取用例。 + - 顺序按历史耗时,长者在前; + - 遵守 `machine`、`serial:` 与内存容量(17.5)。 +- `mcpp test` 将每次运行写入项目的 mtest store(`[test] store`,缺省 `.mtest`),带每个程序的构建区间。由此 `mtest report|analyze|graph|trend` 读得到 `mcpp test` 的运行。 + +### 17.4 运行是一张有向无环图;报告 + +- 一次测试运行是一张 DAG: + - 节点为编译、链接、程序启动、用例、run 作用域夹具的建立与回收; + - 边为构建依赖、"程序构建 → 其用例"、同一串行资源的先后持有者、worker 槽位的释放与占用。 +- 关键路径是该 DAG 的最长加权路径(权为实测墙钟)。`mtest analyze path` 由"最长的程序"改为这一定义。 +- `mtest report --format md`: + - 章节可选:summary、kinds、slowest、failures、metrics、flaky、trend、graph、path、gantt、resources; + - 范围可选:run、set、program、kind; + - 选择方式为参数或 `mtest.toml` 的 `[report]`; + - 图为 mermaid:DAG 中标出关键路径,并给出按 worker 与资源分行的 gantt。 +- `mtest run --report md:FILE` 在运行结束时写出报告;`mtest ci annotate` 将其写入 job summary。 +- 各 job 上传本地报告与 store;汇总 job 以 `mtest ci collect` 合并后,生成整体报告。 +- `mtest ci` 可读取 GitHub 的 job 时间线,给出流水线层的 gantt 与关键路径。 + +### 17.5 资源监控 + +| 级别 | 内容 | 开销 | +|---|---|---| +| `off` | 无 | — | +| `basic`(缺省) | 进程级:墙钟、user/sys CPU、峰值内存;用例级:墙钟、CPU | 进程与用例边界各一次系统调用 | +| `detailed` | 另加用例级内存(峰值、增量)、IO 字节、分配次数与字节(可选模块) | 小 | +| `trace` | 另加 CPU% 与内存的时间序列,采样间隔可设 | 随采样间隔 | + +- 写法有三种:用例属性 `monitor{.memory = true, .sample = 10ms}`、`mtest.toml` 的 `[monitor]`、`--monitor <级别>`。代码优先。 +- 数据以标准指标名进入协议 P6,每项带精度(exact、process、approx):`cpu.user`、`cpu.sys`、`mem.peak`、`mem.delta`、`io.read`、`io.write`、`net.sent`、`net.recv`、`alloc.count`、`alloc.bytes`、`proc.spawned`。 +- 网络:经 mcpplibs.os 的套接字为 exact,其余为 approx。 +- 现有的上下界(`at_most{"mem.peak", 64_MiB}`)与趋势分析直接适用。调度以历史峰值内存作为容量约束。 +- 进程资源用量由 mcpplibs.os 提供:子进程与自身的 CPU、峰值内存、IO 计数。 + +### 17.6 可复用测试组件 + +- 组件是一个包,声明 `provides = ["mtest:component="]`,向框架注册: + - 夹具及其作用域(case、suite、program、run); + - 它管理的资源与容量; + - 新的用例类型与属性; + - 它贡献的指标与报告章节。 +- 注册方式与用例相同(静态初始化)。`list --static` 与协议的 `components` 事件列出组件。 +- run 作用域的夹具由引擎在首个使用者之前启动、在最后一个使用者之后回收,端点以 `MTEST_FIXTURE_` 交给用例;建立与回收是 DAG 的节点。 +- 组件一律经进程与协议交互,与 adapters 相同,与语言无关。 +- mcpp 的组件:沙箱 home、假 xpkg 索引、git 服务、工程树与 golden、标准流捕获、假工具链载荷。mcppls 的组件:一致性夹具。 + +### 17.7 决定 + +| 编号 | 事项 | 状态 | +|---|---|---| +| T1 | 17.2 缓存分级与测试的静态库链接进入本 PR | 已确认 | +| T2 | 17.3 按目录组成程序(缺省)与用例级调度;mtest 提供多种方式供开发者选择 | 已确认 | +| T3 | mcppls #79 的 CI 编排(已发布的 mcpp、按用例分片、store 与报告) | 已确认 | +| T4 | 17.4 的 DAG、关键路径、Markdown 报告与汇总在 mtest 0.5.0;`mcpp test` 写 mtest store | 已确认 | +| T5 | 17.5 资源监控:缺省 `basic`;网络经 mcpplibs.os 为 exact,其余为 approx | 已确认 | +| T6 | 17.6 组件机制在 mtest 0.5.0;mcpp 自身测试的共用部分在本 PR 改为组件 | 已确认 | + +## 18. 任务与依赖 + +| 编号 | 仓库 | 任务 | 依赖 | +|---|---|---|---| +| A1 | mcpp | 行与内置提供者为数据(build.mcpp 编入静态表)、等价性快照 | — (已完成) | +| A2 | mcpp | 选择:行的登记、开放的 os 与 env、`select`、prepare 接入(pin、平台、可服务判断、报告) | A1(进行中) | +| A3 | mcpp | 身份取实际 C 库(重新准备)、显式身份与图冲突的拒绝 | A2 | +| A4 | mcpp | `[[platform.define]]`、`[[platform.provider]]` 的读取与登记;`[platform]`、`[platform.hosted]` 为简写;清单读取时的语句校验、`mcpp why platform`、`mcpp new --template platform` | A2 | +| A5 | mcpp | llvm 配 mingw 载荷 | A2 | +| A6 | mcpp | #911 G2–G11、B1–B3 | A4 | +| A7 | mcpp | 统一的包引用(`git+`、缺省分支、`builtin:`);SPEC-001 修订 | — | +| A8 | mcpp | 锁记录"请求 → 身份"、`--locked`;`mcpp why target`;机器输出;`mcpp::target_provider()`;拒绝码 | A2 | +| B1 | mcpplibs/os | 0.4.1:`stdio::capture`、进程资源用量 | — (#20 进行中) | +| B2 | mcpp | 单元测试迁移到 mtest | B1 (已完成,待 os 0.4.1 发布后改锁) | +| C1 | mtest | 0.5.0:DAG、关键路径、Markdown 报告、汇总、监控、组件、多进程用例调度所需的协议增补 | B1 | +| C2 | mcpp | 缓存分级、静态库链接、按目录组成程序、用例级调度、写 mtest store、资源采集 | C1 | +| C3 | mcpp | e2e 由 mtest 引擎作为唯一入口;共用部分为组件 | C1 | +| D1 | openkal-platform | hosted 部分的服务改写为 musl 身份 | A4 | +| D2 | openkal-image | openkal-run 的能力槽(G7) | A6、mcpp 发布 | +| E1 | mcppls | #79:已发布的 mcpp 2026.11.8.1、CI 编排、mtest 0.5.0 报告 | C1 | +| F1 | 全部 | 规范:SPEC-016(新),SPEC-001、002、013、015 的修订;docs 05、08、20、21、22、30、35、90(中英) | A2–C3 | +| G1 | 全部 | CI 全绿、生态级自审、合入与发布、xim-pkgindex、clean-room 验证、关闭 issue | 以上全部 | + +可以并行的部分:B1、C1 的 CI 运行期间推进 A2–A8;mcpp 的 CI 运行期间推进 D1、E1。发布顺序为 os → mtest → mcpp → openkal-platform 与 openkal-image → mcppls。 + + +## 19. 实现中的取舍(2026-11-09,按“通用、必要、符合规范”评估) + +| 项 | 结论 | 理由 | +|---|---|---| +| G2 `replace` | 撤回 | 有了 G1 与 G12,机器不同的目标即另一行;追加参数时编译器取后者。替换机制是多余的核心复杂度 | +| G2 解释 | `mcpp why flags`,不新增 `explain flags` | `why` 是来源解释的既有命令族;`explain` 只解释错误码 | +| 显式身份与图冲突 | 报告,不拒绝(SPEC-002 §3.4 规则四) | 拒绝曾实测打破把宿主身份写作 `x86_64-linux-gnu` 的配置;只有未写 env 的请求随 C 库 | +| 拒绝码 | 沿用 `host-cannot-serve`、`unknown-target`、`tier-planned` | 不新增与既有码同义的 `target-unserved` 等 | +| B2 | 只约束构建程序的 `artifact-name` | 项目与本机对产物名的陈述是显式选择,照常生效 | +| G9 | 新表 `[targets.X.build]` | 现有键的作用范围不变,升级无感 | +| G8 `mcpp patch edit/save` | 不采纳 | 核心保留的是补丁契约(`patches`、包根、`patch_root`);编辑与生成是通用工具的事:`mcpp why deps` 报告包根,`git diff` 写出补丁,无功能缺口 | +| `mcpp check platform` | 不采纳,并入清单读取与 `mcpp why platform` | 一级子命令须有独立的场景;平台语句的校验在读取清单时进行,作者与使用者在构建中即得到;所服务的身份属来源解释,与 `why target`、`why flags` 同族 | +| 测试程序 | 按目录组成为可选(`[test] programs`);`[test] split` 按文件分进程为缺省;测试只保留可达部分(`--gc-sections`) | 缺省不改变既有工程的程序划分与命名;按文件分进程同时保留隔离与并行 | +| 归档的测试 | 由 `mcpp test` 自身的运行器运行(协议、按文件分进程、工作线程、存储) | 一份归档与一次构建的测试是同一批程序,报告不应因运行方式而异;清单记录构建时判定的协议 | +| 测试的模式 | 匹配程序名,或运行所示的部分名(`unit/test_home`) | 用户看到什么名字就能用什么名字选择;规则只有一处(`test_matches`) | +| 链接丢掉的协议标记 | 从链接所用的目标文件读取 | 0.5.1 之前的 mtest 与 gtest 适配器的标记无人引用;默认链接方式改变后既有工程照常被识别(无感升级) | +| 被构建的包出现在自身测试图中 | 由该包自身代表(仅经 `[dev-dependencies]` 到达的边) | 测试框架本身使用被测库(mtest 与 mcpplibs.os)时,第二份副本会让每个模块在一个程序中出现两次;库自身依赖图中的回边仍是环并被拒绝 | +| 版本要求与根的路径钉 | 维持拒绝 | 版本要求是对图的承诺;改为由 mtest 声明 `^0.4.1`、os 以 0.4.2 发布(增量 API 在 0.4 内兼容) | +| 交叉目标的 `[target.] toolchain` | 构建程序取被它替换的规格 | 与行的 pin 同一规则;该段名的是目标的编译器 | +| mcppls 的自启动测试 | mtest `child` 对象;需独立缓存的两个测试放在 `tests/isolated/` | mcpplibs.os 不修改进程自身环境,隔离缓存只能由新进程获得 | +| openkal-run 的实现 | 能力槽加按系统默认(D2,openkal-image 0.5.4) | 图或平台已有的实现优先,FreeBSD 等新系统无需修补该包 | +| B3 无同名程序 | 照旧打包按名字的第一个并说明,不拒绝 | 拒绝曾打破既有工程的 `mcpp pack`(mcppls);问题在于不说明,不在于选择本身 | diff --git a/.agents/docs/README.md b/.agents/docs/README.md index 91f26e835..54b0b8728 100644 --- a/.agents/docs/README.md +++ b/.agents/docs/README.md @@ -18,7 +18,7 @@ superseded_by: 2026-09-07-....md # when status is superseded --- ``` -355 records. +356 records. ## By subject @@ -138,6 +138,10 @@ Records that declare one. Everything else is listed by date below. - [#674 设计方案评审:`-include unistd.h` 在 Windows + `presents = "posix"` 上的可行性](2026-09-20-issue-674-design-review.md) — active - [`__cxa_thread_atexit` 在 openkal-Windows 上:两层都已定位并修复](2026-09-20-cxa-thread-atexit-finding.md) — landed +### target-model + +- [目标与提供者:统一的目标模型(结合 mcpp#911)](2026-11-09-target-model-design.md) — active + ### targets - [A verified Web run that asked the host for node](2026-09-12-a-verified-web-run-that-asked-the-host-for-node.md) — landed @@ -179,6 +183,7 @@ Records that declare one. Everything else is listed by date below. ### 2026-11 +- [目标与提供者:统一的目标模型(结合 mcpp#911)](2026-11-09-target-model-design.md) — active - [生态顶层方案:#885 收尾之后的工作流、原则与依赖](2026-11-08-ecosystem-top-level-design.md) — active - [#885 收尾补充方案:目标角色、平台事实、系统接口与版本书写](2026-11-08-ecosystem-885-supplement-design.md) — active - [生态 #885 收尾设计:核心、插件、openkal、语言服务与编辑器的一轮闭合](2026-11-07-ecosystem-885-closeout-design.md) — active diff --git a/.edited b/.edited deleted file mode 100644 index e69de29bb..000000000 diff --git a/.gitattributes b/.gitattributes index 24725dce8..647206538 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,4 @@ # The cross-host admission compares the exact source bytes compiled by each host. tests/fixtures/openkal-hosted-threads/src/main.cpp.in text eol=lf +# The rows' snapshot is compared line by line with what the engine answers, in its own bytes. +tests/fixtures/target-rows.snapshot text eol=lf diff --git a/.github/actions/bootstrap-mcpp/action.yml b/.github/actions/bootstrap-mcpp/action.yml index 5d6b49e1c..24c11601e 100644 --- a/.github/actions/bootstrap-mcpp/action.yml +++ b/.github/actions/bootstrap-mcpp/action.yml @@ -107,6 +107,7 @@ runs: env: XLINGS_NON_INTERACTIVE: '1' XLINGS_VERSION: ${{ inputs.xlings-version }} + GITHUB_TOKEN: ${{ github.token }} run: | REPO_DIR="$(pwd)" # Always install the pinned version — the cache may hold an older @@ -231,6 +232,7 @@ runs: env: XLINGS_NON_INTERACTIVE: '1' XLINGS_VERSION: ${{ inputs.xlings-version }} + GITHUB_TOKEN: ${{ github.token }} run: | REPO_DIR="$(pwd)" # FAST PATH: see the unix leg for the reasoning. The cost on Windows is diff --git a/.github/actions/e2e-mtest/action.yml b/.github/actions/e2e-mtest/action.yml index 4a52676c7..a4427d4ee 100644 --- a/.github/actions/e2e-mtest/action.yml +++ b/.github/actions/e2e-mtest/action.yml @@ -1,7 +1,7 @@ name: e2e-mtest description: > mtest for the e2e suite (design 2026-11-08 W8; tests/e2e/mtest.toml): the - release this action pins, its sha256 checked, and the stores main's last + release this action pins, installed by xlings, and the stores main's last successful CI run kept for this host, taken into tests/e2e/.mtest so that `--shard` divides the suite by measured time and a run orders its tests longest first. Exports E2E_MTEST for run_all.sh. A run with no store of main @@ -12,13 +12,13 @@ inputs: description: the host's name in the stores' artifact names (linux, macos, windows) required: true version: - description: the mtest release + description: the mtest release, an xlings package (xim:mtest), whose sha256 the index states required: false - default: '0.4.0' + default: '0.5.2' sha256: - description: sha256 of mtest--openkal-bundle-all.tar.gz + description: sha256 of mtest--openkal-bundle-all.tar.gz, for the release taken when xlings cannot install it required: false - default: 'b62e4ac7cd76b20dbdc48a5d041d377444e8815ee7a4ff1e7686145d13acc925' + default: 'be443f8c546b3f4628dcd3f1eb6f45b4d698a7f805232f966349faf8250863c6' token: description: a token that reads this repository's actions required: false @@ -27,24 +27,41 @@ inputs: runs: using: composite steps: + # THE ENGINE IS AN XLINGS PACKAGE (xim:mtest), installed as the job installs + # mcpp, verified by the index's sha256, and kept in the job's xlings cache: + # by the configured mirror (GLOBAL), then by CN (GitCode first), each with a + # refreshed index; else the release bundle itself (fetch_release.sh: the + # web address, then the API's), checked against the pinned sha256. - name: mtest shell: bash env: MTEST_VERSION: ${{ inputs.version }} MTEST_SHA256: ${{ inputs.sha256 }} + GITHUB_TOKEN: ${{ inputs.token }} run: | set -euo pipefail - archive="mtest-$MTEST_VERSION-openkal-bundle-all.tar.gz" - temp="$RUNNER_TEMP"; python=python3 - if [ "$RUNNER_OS" = Windows ]; then temp=$(cygpath -u "$RUNNER_TEMP"); python=python; fi - curl -fsSL --retry 5 --retry-all-errors --retry-delay 5 -o "$temp/$archive" \ - "https://github.com/mcpp-community/mtest/releases/download/v$MTEST_VERSION/$archive" - actual=$("$python" -c "import hashlib,sys; print(hashlib.sha256(open(sys.argv[1],'rb').read()).hexdigest())" "$temp/$archive") - [ "$actual" = "$MTEST_SHA256" ] || { echo "::error::$archive: sha256 $actual, the action pins $MTEST_SHA256"; exit 1; } - mkdir -p "$temp/mtest" && tar xzf "$temp/$archive" -C "$temp/mtest" - launcher="$temp/mtest/mtest-$MTEST_VERSION-openkal-bundle-all/bin/mtest" - [ "$RUNNER_OS" = Windows ] && launcher="$launcher.exe" - "$launcher" --version + launcher="" + for mirror in GLOBAL CN; do + xlings config --mirror "$mirror" > /dev/null 2>&1 || true + xlings update > /dev/null 2>&1 || true + if xlings install "mtest@$MTEST_VERSION" -y; then launcher=$(command -v mtest); break; fi + done + xlings config --mirror GLOBAL > /dev/null 2>&1 || true + if [ -z "$launcher" ]; then + echo "::warning::xlings could not install mtest $MTEST_VERSION; taking the release itself" + archive="mtest-$MTEST_VERSION-openkal-bundle-all.tar.gz" + temp="$RUNNER_TEMP"; python=python3 + if [ "$RUNNER_OS" = Windows ]; then temp=$(cygpath -u "$RUNNER_TEMP"); python=python; fi + bash "$GITHUB_WORKSPACE/.github/tools/fetch_release.sh" \ + "https://github.com/mcpp-community/mtest/releases/download/v$MTEST_VERSION/$archive" "$temp/$archive" + actual=$("$python" -c "import hashlib,sys; print(hashlib.sha256(open(sys.argv[1],'rb').read()).hexdigest())" "$temp/$archive") + [ "$actual" = "$MTEST_SHA256" ] || { echo "::error::$archive: sha256 $actual, the action pins $MTEST_SHA256"; exit 1; } + mkdir -p "$temp/mtest" && tar xzf "$temp/$archive" -C "$temp/mtest" + launcher="$temp/mtest/mtest-$MTEST_VERSION-openkal-bundle-all/bin/mtest" + [ "$RUNNER_OS" = Windows ] && launcher="$launcher.exe" + fi + "$launcher" --version | grep -qF "$MTEST_VERSION" \ + || { echo "::error::the mtest at $launcher is not $MTEST_VERSION: $("$launcher" --version)"; exit 1; } echo "E2E_MTEST=$launcher" >> "$GITHUB_ENV" # The pull request's change, for the `pr` set: the base's tip and HEAD's # parents, so that `git diff ...HEAD` has its merge base. When they diff --git a/.github/actions/setup-macos-llvm/action.yml b/.github/actions/setup-macos-llvm/action.yml index 0c6336922..12ec05e2b 100644 --- a/.github/actions/setup-macos-llvm/action.yml +++ b/.github/actions/setup-macos-llvm/action.yml @@ -51,17 +51,25 @@ runs: env: XLINGS_NON_INTERACTIVE: '1' XLINGS_VERSION: ${{ inputs.xlings-version }} + GITHUB_TOKEN: ${{ github.token }} run: | - WORK=$(mktemp -d) - tarball="xlings-${XLINGS_VERSION}-macosx-arm64.tar.gz" - # Retried and verified — .github/tools/fetch_release.sh. A bare curl - # here is the `curl: (52) Empty reply from server` flake. - bash "$GITHUB_WORKSPACE/.github/tools/fetch_release.sh" \ - "https://github.com/d2learn/xlings/releases/download/v${XLINGS_VERSION}/${tarball}" \ - "${WORK}/${tarball}" - tar -xzf "${WORK}/${tarball}" -C "${WORK}" - XLINGS_DIR="${WORK}/xlings-${XLINGS_VERSION}-macosx-arm64" - "$XLINGS_DIR/subos/default/bin/xlings" self install + # THE CACHE ALREADY HOLDS THE PINNED XLINGS (its key names the version): + # nothing is downloaded, as bootstrap-mcpp's fast path does. This step + # fetched the release on every run, a cache hit or not, and a 504 of + # that fetch failed the job (2026-11-09). + XL="$HOME/.xlings/subos/default/bin/xlings" + if ! { [ -x "$XL" ] && "$XL" --version 2>/dev/null | head -1 | grep -qF "$XLINGS_VERSION"; }; then + WORK=$(mktemp -d) + tarball="xlings-${XLINGS_VERSION}-macosx-arm64.tar.gz" + # Retried and verified, and by the API's address when the web's + # fails — .github/tools/fetch_release.sh. + bash "$GITHUB_WORKSPACE/.github/tools/fetch_release.sh" \ + "https://github.com/d2learn/xlings/releases/download/v${XLINGS_VERSION}/${tarball}" \ + "${WORK}/${tarball}" + tar -xzf "${WORK}/${tarball}" -C "${WORK}" + XLINGS_DIR="${WORK}/xlings-${XLINGS_VERSION}-macosx-arm64" + "$XLINGS_DIR/subos/default/bin/xlings" self install + fi export PATH="$HOME/.xlings/subos/default/bin:$PATH" xlings --version echo "PATH=$HOME/.xlings/subos/default/bin:$PATH" >> "$GITHUB_ENV" diff --git a/.github/tools/check_e2e_coverage.py b/.github/tools/check_e2e_coverage.py index fae518366..077d34521 100644 --- a/.github/tools/check_e2e_coverage.py +++ b/.github/tools/check_e2e_coverage.py @@ -18,6 +18,10 @@ under tests/e2e: ran some report says pass, fail or timeout; + unaffected no report ran it, and a run narrowed to a set (`E2E_SET`, the + `pr` set of a pull request) says the change does not affect it: + the full runs of main are its coverage, and this run's claim is + about the tests the change reaches; job no report ran it, but a workflow names it outside a comment, as a whole token: by file name, by name without `.sh`, or by its number as an `E2E_ONLY` pattern such as `239_*.sh`. A dedicated job runs it @@ -34,7 +38,8 @@ calls a target row `verified` when CI builds and runs its artefact, and the `*-openkal` rows claim that for every system. tests/e2e/host-requirements.tsv lists such a test with the hosts its result must come from; a listed test that -did not pass on one of them, or that does not exist, fails the check. +did not pass on one of them, or that does not exist, fails the check; one a +set left out on that host as unaffected meets it for that run. With --timings-out DIR it writes `.tsv`, the measured duration of every test that ran, merged across that host's shards, in the format run_all.sh reads @@ -133,6 +138,7 @@ def main() -> int: ran: dict[str, set[str]] = defaultdict(set) passed: dict[str, set[str]] = defaultdict(set) skipped: dict[str, set[str]] = defaultdict(set) + unaffected: dict[str, set[str]] = defaultdict(set) shard_ms: dict[tuple[str, int], int] = defaultdict(int) timings: dict[str, dict[str, int]] = defaultdict(dict) reports = 0 @@ -146,6 +152,8 @@ def main() -> int: timings[host][test] = ms elif status == "skip": skipped[test].add(f"{host}: {detail}") + elif status == "unaffected": + unaffected[test].add(host) if reports == 0: print(f"no report under {args.reports}: nothing can be said about coverage") return 1 @@ -157,6 +165,8 @@ def main() -> int: counts["ran"] += 1 if test in exceptions: stale.append(f"{test} is excused but ran on {', '.join(sorted(ran[test]))}") + elif test in unaffected: + counts["unaffected"] += 1 elif named_by_a_workflow(test, workflows): counts["job"] += 1 elif test in exceptions: @@ -171,13 +181,14 @@ def main() -> int: if test not in tests: stale.append(f"{test} is required on hosts but does not exist") continue - for host in sorted(hosts - passed[test]): + for host in sorted(hosts - passed[test] - unaffected[test]): why = [d for d in skipped[test] if d.startswith(f"{host}: ")] unmet.append(f"{test} did not pass on {host}" f"{' (' + '; '.join(why) + ')' if why else (' (it ran and did not pass)' if host in ran[test] else ' (no report of it there)')}") - print(f"{len(tests)} tests: {counts['ran']} ran on a shard, {counts['job']} run by a " - f"dedicated job, {counts['excused']} excused, {len(uncovered)} uncovered") + print(f"{len(tests)} tests: {counts['ran']} ran on a shard, {counts['unaffected']} unaffected by " + f"the change, {counts['job']} run by a dedicated job, {counts['excused']} excused, " + f"{len(uncovered)} uncovered") for (host, shard), ms in sorted(shard_ms.items()): print(f" {host} shard {shard}: {ms / 60000:.1f} min of tests") for line in uncovered: diff --git a/.github/tools/check_target_tiers.py b/.github/tools/check_target_tiers.py index d156cf9c5..07e8a1373 100755 --- a/.github/tools/check_target_tiers.py +++ b/.github/tools/check_target_tiers.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 """Every documented tier agrees with the target table. -WHY THIS EXISTS. `kKnownTargets` in modules/toolchain-model/src/triple.cppm is -the single source for a row's tier, and four documents restate it: both +WHY THIS EXISTS. The built-in rows, modules/toolchain-model/data/builtin.toml +(SPEC-016 §2.2), are the single source for a row's tier, and four documents restate it: both READMEs, both copies of docs/21. When `wasm32-emscripten` became `verified` and the Android rows gained tiers, docs/21 was updated and the READMEs were not -- so the front page told a reader that three targets were `planned` while the @@ -16,17 +16,19 @@ """ import re import sys +import tomllib from pathlib import Path ROOT = Path(__file__).resolve().parents[2] -TABLE = ROOT / "modules/toolchain-model/src/triple.cppm" +TABLE = ROOT / "modules/toolchain-model/data/builtin.toml" TIERS = ("verified", "preview", "planned") # The engine's answer. rows = {} -for m in re.finditer(r'^\s*\{\s*"([a-z0-9_.+-]+)",\s*"(verified|preview|planned)"', - TABLE.read_text(), re.M): - rows[m.group(1)] = m.group(2) +with TABLE.open("rb") as f: + for row in tomllib.load(f).get("platform", {}).get("define", []): + if row.get("tier") in TIERS: + rows[row["triple"]] = row["tier"] if len(rows) < 20: sys.exit(f"ERROR: only {len(rows)} rows parsed from {TABLE.name}; " "the pattern no longer matches the table") diff --git a/.github/tools/check_windows_home_legacy.py b/.github/tools/check_windows_home_legacy.py index 3e9b11052..cdcbe05f9 100644 --- a/.github/tools/check_windows_home_legacy.py +++ b/.github/tools/check_windows_home_legacy.py @@ -14,22 +14,23 @@ assert original.count(marker) == 1, 'identify the test-only code-page setting explicitly' -def run_case(pattern, passed_case): - result = subprocess.run([os.environ['MCPP'], '--offline', 'test', pattern], cwd=root, +def run_case(passed_case): + # The case by its identifier (mtest's): it runs in the process of its file. + result = subprocess.run([os.environ['MCPP'], '--offline', 'test', '--case', passed_case], cwd=root, capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=600) sys.stdout.write(result.stdout) sys.stderr.write(result.stderr) assert result.returncode == 0, result.returncode output = re.sub(r'\x1b\[[0-9;]*m', '', result.stdout + result.stderr) - assert '[ OK ] ' + passed_case in output, \ + assert re.search(r'^\s*ok\s+' + re.escape(passed_case) + r'\s*$', output, re.M), \ 'the legacy ACP case did not pass; a UTF-8-only skip is not proof' try: manifest.write_bytes(original.replace(marker, b'windows_code_page = "legacy"', 1)) - run_case('home', 'HomeSelection.UnicodeLocatorRoundTripDoesNotNarrowTheSelectedPath') - run_case('bmi_cache', 'BMICacheMaintenance.RunnerCachePathDoesNotUseTheWindowsAnsiCodePage') + run_case('HomeSelection/UnicodeLocatorRoundTripDoesNotNarrowTheSelectedPath') + run_case('BMICacheMaintenance/RunnerCachePathDoesNotUseTheWindowsAnsiCodePage') finally: manifest.write_bytes(original) print('PASS: home and runner-cache paths ran under legacy Windows ACP; manifest restored') diff --git a/.github/tools/fetch_release.sh b/.github/tools/fetch_release.sh index 2478d6f61..33e71e7a3 100755 --- a/.github/tools/fetch_release.sh +++ b/.github/tools/fetch_release.sh @@ -45,6 +45,33 @@ verify() { esac } +# THE SAME FILE BY GITHUB'S API. The web address of a release asset +# (`github.com///releases/download//`) answered 504 for +# minutes at a time while the API's address of the same asset answered 200 +# (measured 2026-11-09 on xlings-2026.10.26.1-macosx-arm64.tar.gz, three times +# each). A failed web attempt is followed by the API's, with the job's token +# where there is one (the API's anonymous limit is 60 requests an hour). +api_fetch() { + local rest="${url#https://github.com/}" + [ "$rest" != "$url" ] || return 1 + local owner repo releases download tag name + IFS=/ read -r owner repo releases download tag name <<<"$rest" + [ "$releases/$download" = "releases/download" ] && [ -n "$name" ] || return 1 + local auth=() + local token="${GITHUB_TOKEN:-${GH_TOKEN:-}}" + [ -z "$token" ] || auth=(-H "Authorization: Bearer $token") + local id + id=$(curl -fsSL --retry 3 --retry-all-errors --connect-timeout 20 --max-time 60 "${auth[@]}" \ + "https://api.github.com/repos/$owner/$repo/releases/tags/$tag" \ + | python3 -c 'import json,sys; n=sys.argv[1]; print(next((str(a["id"]) for a in json.load(sys.stdin)["assets"] if a["name"] == n), ""))' "$name") \ + || return 1 + [ -n "$id" ] || return 1 + echo "fetch_release: the web address failed; asking the API for asset $id" >&2 + curl -fsSL --retry 3 --retry-delay 2 --retry-all-errors --connect-timeout 20 --max-time 600 \ + "${auth[@]}" -H "Accept: application/octet-stream" \ + -o "$dest" "https://api.github.com/repos/$owner/$repo/releases/assets/$id" +} + for i in $(seq 1 "$attempts"); do rm -f "$dest" # --retry-all-errors is what covers exit 52; the rest bound how long a single @@ -53,7 +80,7 @@ for i in $(seq 1 "$attempts"); do if curl -fsSL \ --retry 3 --retry-delay 2 --retry-all-errors \ --connect-timeout 20 --max-time 600 \ - -o "$dest" "$url"; then + -o "$dest" "$url" || { rm -f "$dest"; api_fetch; }; then if verify; then [ "$i" -eq 1 ] || echo "fetch_release: succeeded on attempt $i" >&2 exit 0 diff --git a/.github/tools/install_pinned_mcpp.sh b/.github/tools/install_pinned_mcpp.sh index df8c2451c..cf3dc3802 100755 --- a/.github/tools/install_pinned_mcpp.sh +++ b/.github/tools/install_pinned_mcpp.sh @@ -64,9 +64,46 @@ XL="$XL_HOME/.xlings/subos/default/bin/xlings${EXE}" echo "FAIL: no xlings at $XL_HOME/.xlings/subos/default/bin nor on PATH" >&2 exit 1; } -"$XL" install -y -u -g >&2 +PIN=$(grep -oE '"mcpp"[[:space:]]*:[[:space:]]*"[^"]+"' "$REPO_DIR/.xlings.json" \ + | grep -oE '[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?' | head -1 || true) +# XLINGS FIRST, ITS MIRRORS IN TURN, THE RELEASE LAST. A job's xlings comes from +# the cache with the index of when the cache was saved, and xlings keeps that +# index when its refresh fails: during GitHub's incident of 2026-11-09 +# ("timeouts due to surge in anonymous users") the anonymous download of the +# index answered 504 and a pin published after the cache was "not found". +# 1-2 the configured mirror (GLOBAL), the index refreshed before the second; +# 3-4 the CN mirror (GitCode first, GitHub after), refreshed; the fourth by +# git, which xlings otherwise declines for an artifact-managed index; +# then the release itself (install.sh: GitHub, then GitCode, sha256 checked). MCPP="$XL_HOME/.xlings/subos/default/bin/mcpp${EXE}" +installed=no +for attempt in 1 2 3 4; do + case "$attempt" in + 2) "$XL" update >&2 || true ;; + 3) "$XL" config --mirror CN >&2 || true; "$XL" update >&2 || true ;; + 4) XLINGS_INDEX_SOURCE=git "$XL" update >&2 || true ;; + esac + if "$XL" install -y -u -g >&2; then installed=yes; break; fi + sleep $(( attempt * 10 )) +done +"$XL" config --mirror GLOBAL >&2 || true +if [ "$installed" = no ]; then + echo "::warning::xlings could not install mcpp $PIN; taking the release itself" >&2 + prefix="${RUNNER_TEMP:-$(mktemp -d)}/mcpp-release-$PIN" + if [ -n "$EXE" ]; then + zip="mcpp-$PIN-windows-x86_64.zip" + mkdir -p "$prefix" + bash "$REPO_DIR/.github/tools/fetch_release.sh" \ + "https://github.com/mcpp-community/mcpp/releases/download/v$PIN/$zip" "$prefix/$zip" >&2 + (cd "$prefix" && unzip -qo "$zip" && rm -f "$zip") + MCPP=$(find "$prefix" -type f -name "mcpp.exe" -path '*/bin/*' | head -1) + else + MCPP_VERSION="$PIN" MCPP_PREFIX="$prefix" MCPP_NO_PATH=1 bash "$REPO_DIR/install.sh" >&2 + MCPP="$prefix/bin/mcpp" + fi + [ -z "${GITHUB_PATH:-}" ] || dirname "$MCPP" >> "$GITHUB_PATH" +fi [ -x "$MCPP" ] || { echo "FAIL: no mcpp at $MCPP after install" >&2; exit 1; } # Assert the outcome rather than trust it. This is the ONLY place the pin is @@ -84,8 +121,6 @@ MCPP="$XL_HOME/.xlings/subos/default/bin/mcpp${EXE}" # — it just silently truncates it to "2026.7.27" on both sides, so the # comparison below would accept any release of that day and this guard would # quietly stop guarding. -PIN=$(grep -oE '"mcpp"[[:space:]]*:[[:space:]]*"[^"]+"' "$REPO_DIR/.xlings.json" \ - | grep -oE '[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?' | head -1 || true) GOT=$("$MCPP" --version 2>/dev/null | head -1 \ | grep -oE '[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?' | head -1 || true) # Exact comparison: a substring match would let a pin of 0.0.10 be satisfied by diff --git a/.github/workflows/ci-aarch64-fresh-install.yml b/.github/workflows/ci-aarch64-fresh-install.yml index 9625fc3ad..5fa97a62b 100644 --- a/.github/workflows/ci-aarch64-fresh-install.yml +++ b/.github/workflows/ci-aarch64-fresh-install.yml @@ -342,7 +342,7 @@ jobs: mkdir -p "$evidence/$(dirname "$relative")" cp "$binary" "$evidence/$relative" readelf -aW "$binary" > "$evidence/$relative.readelf.txt" - done < <(find target/aarch64-linux-gnu -type f -path '*/bin/unit/test_elf_runtime' -print0) + done < <(find target/aarch64-linux-gnu -type f -path '*/bin/unit' -print0) while IFS= read -r -d '' plan; do relative="${plan#target/}" mkdir -p "$evidence/$(dirname "$relative")" diff --git a/.github/workflows/ci-target-matrix.yml b/.github/workflows/ci-target-matrix.yml index b6bdcb4bc..495ffddb3 100644 --- a/.github/workflows/ci-target-matrix.yml +++ b/.github/workflows/ci-target-matrix.yml @@ -123,9 +123,9 @@ jobs: # payload (it backs the host row), so that job uses `check` and the # test is guaranteed to be exercised somewhere on every run. Without # that, a reason accepted everywhere is a test that runs nowhere. - check_or_declared_skip() { # log ok-line acceptable-skip-substring + check_or_declared_skip() { # log ok-line acceptable-skip-pattern (ERE) grep -qF "$2" "$1".log && { echo " ok $1"; return 0; } - if grep -q '^SKIP:' "$1".log && grep -qF "$3" "$1".log; then + if grep -q '^SKIP:' "$1".log && grep -qE "$3" "$1".log; then echo " ok $1 (declared skip: $(grep -m1 '^SKIP:' "$1".log))" return 0 fi @@ -153,11 +153,13 @@ jobs: check 298_overriding_a_convention_requires_replacing_it.sh \ "OK: a convention may be overridden, but not merely removed" || fail=1 else - # Non-Linux rows may restore a cache with no declared LLVM. + # Non-Linux rows may restore a cache with no declared LLVM, and a + # macOS host serves every gcc-pinned row through openkal-platform, + # whose compiler is llvm: no row there is overridden by llvm. # Both Linux hosts require the assertion after native provisioning. check_or_declared_skip 298_overriding_a_convention_requires_replacing_it.sh \ "OK: a convention may be overridden, but not merely removed" \ - "llvm is not installed here" || fail=1 + "llvm is not installed here|served by a non-llvm convention" || fail=1 fi # ── 2026.8.26.2: an answer mcpp already had, now used ───────────── diff --git a/.github/workflows/cross-build-test.yml b/.github/workflows/cross-build-test.yml index 709cd4dc6..867b9ded7 100644 --- a/.github/workflows/cross-build-test.yml +++ b/.github/workflows/cross-build-test.yml @@ -359,6 +359,14 @@ jobs: export MCPP_VENDORED_XLINGS="$XLINGS_BIN" bash tests/e2e/102_mingw_cross_wine.sh + # SPEC-016 §6: clang with the MinGW-w64 payload as its sysroot, run + # under wine. Named here for the reason 102 is: `# requires: mingw-cross + # wine`, which no Linux shard has. + - name: "e2e: clang pairs with the MinGW payload" + run: | + export MCPP_VENDORED_XLINGS="$XLINGS_BIN" + bash tests/e2e/1044_clang_pairs_with_the_mingw_payload.sh + # mcpp#365. This is the only job with a MinGW cross toolchain, so it is # the only place the GNU half of resource compilation (windres -O coff, # because GNU ld cannot consume a .res) can run at all — the Linux e2e diff --git a/.github/workflows/openkal-cross.yml b/.github/workflows/openkal-cross.yml index 06c015660..689aeae08 100644 --- a/.github/workflows/openkal-cross.yml +++ b/.github/workflows/openkal-cross.yml @@ -292,7 +292,9 @@ jobs: # `openkal-llvm-runtime`'s own CI under qemu; it has no runner here to # execute on, and a build-only cell in a workflow whose point is # running would be the weaker claim. - for t in x86_64-linux-gnu aarch64-macos x86_64-windows-gnu; do + # The macOS identity over openkal is `aarch64-macos-musl` on every host (SPEC-016 §4): on a + # macOS host `aarch64-macos` is the Apple SDK's. + for t in x86_64-linux-gnu aarch64-macos-musl x86_64-windows-gnu; do rm -rf target # The upstream manifest may pin an older LLVM line. The invocation # selects this candidate explicitly, including on the ARM64 host. @@ -301,12 +303,12 @@ jobs: [ -n "$a" ] || { echo "::error::$t produced no artefact on ${{ matrix.host }}"; exit 1; } case "$t" in x86_64-windows-gnu) cp "$a" "$RUNNER_TEMP/out/windows.exe" ;; - aarch64-macos) cp "$a" "$RUNNER_TEMP/out/macos" ;; + aarch64-macos-musl) cp "$a" "$RUNNER_TEMP/out/macos" ;; *) cp "$a" "$RUNNER_TEMP/out/linux" ;; esac "$READOBJ" --file-headers "$a" > "$RUNNER_TEMP/out/$t.headers.txt" case "$t" in - aarch64-macos) arch=aarch64 ;; + aarch64-macos-musl) arch=aarch64 ;; *) arch=x86_64 ;; esac grep -q "Arch: $arch" "$RUNNER_TEMP/out/$t.headers.txt" \ @@ -319,7 +321,7 @@ jobs: thread_bin=$(cat "$RUNNER_TEMP/threads-binary.txt") case "$t" in x86_64-windows-gnu) thread_output=windows-threads.exe ;; - aarch64-macos) thread_output=macos-threads ;; + aarch64-macos-musl) thread_output=macos-threads ;; *) thread_output=linux-threads ;; esac cp "$thread_bin" "$RUNNER_TEMP/out/$thread_output" @@ -355,7 +357,7 @@ jobs: # costs. # # LEG 3 IS THE ENGINE: the same idiom, over the openkal stack, for - # `aarch64-macos`, built by the mcpp under test. Measured on the token + # `aarch64-macos-musl`, built by the mcpp under test. Measured on the token # this step was written for, the two readings are # # -fno-builtin-memset_pattern16 1 reference, and the link fails @@ -444,10 +446,10 @@ jobs: [ "$(refs "$W/narrow.o")" = 1 ] || { echo "::error::leg 2: clang now honours -fno-builtin-memset_pattern16, and mcpp.toolchain.cenv can emit the narrower token"; exit 1; } [ "$(refs "$W/blunt.o")" = 0 ] || { echo "::error::leg 2: -fno-builtin no longer withdraws the pattern fill"; exit 1; } - (cd "$W" && "$MCPP_UNDER_TEST" build --target aarch64-macos) + (cd "$W" && "$MCPP_UNDER_TEST" build --target aarch64-macos-musl) obj=$(find "$W/target" -name 'main.o' | head -1) [ -n "$obj" ] || { echo "::error::leg 3 produced no object to read"; exit 1; } - echo "engine, aarch64-macos over openkal: $(refs "$obj")" + echo "engine, aarch64-macos-musl over openkal: $(refs "$obj")" [ "$(refs "$obj")" = 0 ] || { echo "::error::leg 3: builtins = \"iso\" did not withdraw memset_pattern16"; exit 1; } - uses: actions/upload-artifact@v4 diff --git a/.xlings.json b/.xlings.json index f3a65ae90..e044bb304 100644 --- a/.xlings.json +++ b/.xlings.json @@ -1,5 +1,5 @@ { "workspace": { - "mcpp": "2026.11.5.1" + "mcpp": "2026.11.8.1" } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 832353891..5bd2c846d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,130 @@ > Each `## []` section is that release's notes. Entries are written in English > from 2026.9.28.3 on; earlier entries remain as written. +## [2026.11.9.1] - 2026-11-09 + +### Added + +- **Targets and providers (SPEC-016).** A target request is a pattern: the + rows it reaches, each with the providers that serve it, are ordered by + priority, then by row, then by provider name, and the first one available on + this host is taken; the build reports the request, the identity and, when the + first candidate is not available, why. The built-in rows and providers are + data (`modules/toolchain-model/data/builtin.toml`, compiled in by the + package's build program). `[target..priority] = ` + changes the order; `platform = "builtin:"` chooses a built-in provider. +- A platform package defines rows and providers: `[[platform.define]]` (triple, + clang-triple, object format, family, execution, ...) and + `[[platform.provider]]` (serves, priority, hosts, pairs, toolchain, layers, + link, hosted). `cfg(os = ...)` answers by a defined row; the parser accepts + the systems and C libraries the resolved platforms define (mcpp#911 G1). +- `mcpp why target` lists a request's candidates with their priorities and + availability; `mcpp why flags []` lists each word of a unit's command + line and of the link line with the layer that gave it (row, platform, + toolchain, package, engine; mcpp#911 G2). A row's defaults are not replaced + in place: a platform whose machine differs defines its own row (G1). Both take `--format json`. The build + record and the `plan` event carry `{request, identity, provider, priority, + fallback}`; build programs read `mcpp::target_provider()` + (`MCPP_TARGET_PROVIDER`). +- `mcpp.lock` records `[target.""]` with the identity and the + provider; `--locked` refuses a build that selects another identity. +- A sysroot or a provider that states a hosted C environment turns off + `-ffreestanding` (G3); a platform's `link.driver = "compiler"` links any row + (G4); an artifact for another system is not judged by this machine's loader + (G5); a target-independent source artifact serves every target (G6). +- `[defaults.""] = `: a package's default + provider of a capability it requires, taken when neither the graph nor the + platform provides it (G7). +- A patch names files from the package's root; `patch_root` names another + directory, and a patch written from the archive's top applies as before. + `mcpp why deps` reports each dependency's root, from which `git diff` writes + a patch (G8). +- `[targets..build]` compiles the shared sources an artifact links with + its own defines and flags, as the artifact's variant (G9). +- A row a platform defines is packed and carries as a built-in row does; + `--systems all` adds the systems the platforms define (G10). An artifact for + another system is built, not run, until a runner -- a runner package's tool + -- or an execution host states how it runs (G11). +- The row `i686-none-elf` (G12). +- A test program that speaks the test protocol runs as one process per file of + its cases (`[test] split = "file" | "case" | "none"`), and a test keeps what + it reaches: units are compiled with one section per function and per object + and tests link with `--gc-sections` (`[test] link = "objects"` keeps every + object whole); a protocol marker nothing references is read from the objects + the test was linked from. A run is kept in the mtest store's form under + `target/mtest` (`[test] store`), which `mtest store import` takes in for + `mtest report`, `analyze` and `graph`. The tests of an archive run by the + same runner (`mcpp test --archive`): their protocol, recorded in + `mcpp-tests.json`, a process per file of their cases, the workers and the + store. A pattern names a part as the run names it (`mcpp test unit/test_home`). + mcpp's own unit tests are one program per directory: 166 + programs of 32 MB became one of 37 MB, run as 166 processes. +- clang pairs with the MinGW-w64 payload for `x86_64-windows-gnu`: the payload + is clang's sysroot and libstdc++ its C++ library, `import std` included. +- `mcpp new --template platform`. Reading a platform's manifest refuses a + definition of a built-in row and a row defined twice, and warns of a provider + that serves no row and of two providers at one priority for one identity; + `mcpp why platform` lists the rows a platform defines and the identities it + serves, with their providers and priorities. +- `mcpp pack --all-programs` ships every program of a package. +- The package being built stands for itself in its own graph: a test framework + that uses the package under test (mtest and mcpplibs.os) is built against it, + and the root's `[dev-dependencies]` close no cycle of packages. +- A git reference that names no tag, rev or branch follows the remote's + default branch; `--platform git+[#tag=|rev=|branch=]`. + +### Changed + +- A request that names only a system takes the first available identity of + that system: `x86_64-windows` on a host without MSVC is + `x86_64-windows-musl` (openkal-platform), no longer `x86_64-windows-gnu`; + `aarch64-macos` and `x86_64-macos` on another host are the `-musl` + identities; `aarch64-linux` on an x86_64 host is `aarch64-linux-musl`. An + identity written whole is not replaced. +- The identity follows the C library: a project whose graph supplies musl + builds `x86_64-linux` as `x86_64-linux-musl`; an explicit identity is the + project's spelling and is kept, and a disagreement with the graph's C library + is reported. +- A git dependency names at most one of rev, tag and branch. +- `mcpp pack` with several programs packs the one named after the package, + else the first by name as before, and names the others with + `mcpp pack ` and `--all-programs`. +- mcpp's own unit tests are mtest cases (mtest 0.5.2, mcpplibs.os 0.4.1); a + skeleton's tests use mtest 0.5.2. + +### Fixed + +- A manifest `mcpp pack` generates quotes a target name with a dot (B1). +- A platform's artifact names stand over a build program's `artifact-name`; + the conflict is reported (B2). +- `mcpp pack` no longer packs one program of several without a word (B3). +- A static ELF is not run to learn its closure. +- A build program under a cross `--target` whose `[target.] toolchain` + names the target's compiler is compiled by the spec that section replaced, + as under a row's pin: mcpp's own tree, built on Windows for + `x86_64-linux-musl`, compiled its build program with the musl cross gcc. +- A test name that is another's directory (`unit` and `unit/own`) is refused, + naming both; the two programs were a file and a directory of one path. +- The repository's stray empty `.edited` is removed; e2e 859 fails naming the + missing record rather than writing one in the working directory. +- CI: a pull request's e2e shards report the scripts the `pr` set leaves out as + `unaffected`, and the check that every script runs somewhere accepts them + for that run; with main's coverage map in place, it had read them as + uncovered. A shard the set gives no script is not a failure of mtest. +- A C library of the graph (`mcpp:c-abi`, openkal-musl) leads a Mach-O image's + link. The loader runs initialisers in link order before the entry, and the + program's objects came first: a global constructor that allocated (an mtest + case's registration) stopped in openkal-musl's allocator before the library + was up, on hosted macOS (e2e 1053). +- `x86_64-windows-gnu` with llvm on a host the MinGW-w64 payload does not + serve (linux-aarch64, macOS) is refused as `host-cannot-serve`, as with gcc; + the paired payload's failed install was an unnamed refusal. +- The target matrix's expected table states the cells the target model + changes, measured on the four hosts; e2e 1044 runs in the job that has wine. +- e2e 298 picks a row whose compiler on this host is its convention's: a macOS + host serves `aarch64-linux-musl` through openkal-platform, whose compiler is + llvm, and there llvm alone overrides nothing. + ## [2026.11.8.1] - 2026-11-08 ### Added diff --git a/README.md b/README.md index 9c3c8f01f..186b6a041 100644 --- a/README.md +++ b/README.md @@ -446,11 +446,12 @@ list` reports for this machine): | `x86_64-windows-msvc` | `msvc@system` (detected VS/BuildTools) or llvm ¹ *(Windows default with Visual Studio)* | verified | | `x86_64-windows-musl` | llvm 23 — a PE with a musl C library, which no gcc emits; the system comes from the dependency graph | preview | | `aarch64-macos` | llvm *(macOS default)* | verified | +| `aarch64-macos-musl` · `x86_64-macos-musl` | llvm 23 — a Mach-O with a musl C library from the dependency graph, built on any host *(what `aarch64-macos` is off macOS)* | preview | | `riscv64-none-elf` · `riscv32-none-elf` | llvm 23 — bare metal, `xim:picolibc-riscv` ² | verified | | `thumbv6m-none-eabi` · `thumbv7m-none-eabi` | llvm 23 — Cortex-M0/M0+/M1, Cortex-M3 ² | verified | | `thumbv7em-none-eabihf` · `thumbv8m.main-none-eabi` | llvm 23 — Cortex-M4F/M7F hard float, Cortex-M33/M55 soft float ² | verified | | `armv7a-none-eabi` · `armv7a-none-eabihf` | llvm 23 — Cortex-A 32-bit, the first row with an MMU ² | verified | -| `aarch64-none-elf` · `x86_64-none-elf` | llvm 23 — bare metal, no C library by default ² | preview | +| `aarch64-none-elf` · `x86_64-none-elf` · `i686-none-elf` | llvm 23 — bare metal, no C library by default ² | preview | | `thumbv7em-none-eabi` · `thumbv8m.base-none-eabi` · `thumbv8m.main-none-eabihf` | llvm 23 — Cortex-M4/M7 soft float, M23, M33F/M55F ² | preview | | `aarch64-linux-gnu` | `llvm@23.1.3` | preview | | `riscv64-linux-musl` · `x86_64-macos` | — | planned | diff --git a/README.zh-CN.md b/README.zh-CN.md index d2673588d..a336981eb 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -395,11 +395,12 @@ mcpp 的身份模型有两条正交的轴:**工具链**是 `family@version`( | `x86_64-windows-msvc` | `msvc@system`(探测 VS/BuildTools)或 llvm ¹(*有 Visual Studio 时的 Windows 默认*) | verified | | `x86_64-windows-musl` | llvm 23;带 musl C 库的 PE,gcc 无法产出;系统部分由依赖图提供 | preview | | `aarch64-macos` | llvm(*macOS 默认*) | verified | +| `aarch64-macos-musl` · `x86_64-macos-musl` | llvm 23;带 musl C 库的 Mach-O,系统部分由依赖图提供,任意宿主可构建(*非 macOS 宿主上的 `aarch64-macos`*) | preview | | `riscv64-none-elf` · `riscv32-none-elf` | llvm 23;裸机,`xim:picolibc-riscv` ² | verified | | `thumbv6m-none-eabi` · `thumbv7m-none-eabi` | llvm 23;Cortex-M0/M0+/M1、Cortex-M3 ² | verified | | `thumbv7em-none-eabihf` · `thumbv8m.main-none-eabi` | llvm 23;Cortex-M4F/M7F 硬浮点,Cortex-M33/M55 软浮点 ² | verified | | `armv7a-none-eabi` · `armv7a-none-eabihf` | llvm 23;Cortex-A 32 位,第一个带 MMU 的目标 ² | verified | -| `aarch64-none-elf` · `x86_64-none-elf` | llvm 23;裸机,默认不带 C 库 ² | preview | +| `aarch64-none-elf` · `x86_64-none-elf` · `i686-none-elf` | llvm 23;裸机,默认不带 C 库 ² | preview | | `thumbv7em-none-eabi` · `thumbv8m.base-none-eabi` · `thumbv8m.main-none-eabihf` | llvm 23;Cortex-M4/M7 软浮点、M23、M33F/M55F ² | preview | | `aarch64-linux-gnu` | `llvm@23.1.3` | preview | | `riscv64-linux-musl` · `x86_64-macos` | — | planned | diff --git a/docs/04-mcpp-toml.md b/docs/04-mcpp-toml.md index f8d6e28b2..a53bc234f 100644 --- a/docs/04-mcpp-toml.md +++ b/docs/04-mcpp-toml.md @@ -506,6 +506,7 @@ required_features = ["gui"] # only built when feature `gui` is |---|---| | `defines` | Preprocessor macros (`name` or `name=value`); desugar to `-D` on both the C and C++ entry compile. | | `cxxflags` / `cflags` | Extra compile flags for this target. Do **not** put `-std=...` here — use `[package].standard`. | +| `build` *(2026.11.9.1+)* | A table, `[targets..build]` with `defines`, `cxxflags` and `cflags`: the flags of every translation unit of this package the target links — its entry and the shared implementation sources. Where they differ from the package's, the units are compiled as the target's own variant (`obj/_variant//`); the other targets link the package's objects. A module interface is one per package and is not varied. | | `required_features` | The target is emitted only when **every** listed feature is active in the build; otherwise it is silently skipped. A gate only — it does not activate features (use `--features` / `[features].default`). **One exception, and it is not a second rule:** when this target is requested as a host tool (`tools = [...]`, §2.14), the target is what was *asked for*, so its `required_features` become the sub-build's *inputs*. Same field, one meaning — the resolution just runs in the opposite direction. | | `windows_subsystem` *(2026.9.12.2+)* | The PE subsystem of an executable: `"console"` (the default) or `"windows"`, a GUI program that starts without a console. Reaches this target's link and no other, and renders nothing on a target that is not PE. See the section above. | | `windows_entry` *(2026.9.12.2+)* | The entry function the program defines: `"main"` (the default), `"wmain"`, `"WinMain"` or `"wWinMain"`. See the section above. An entry target states the function its translation unit defines, and the program that selects it inherits it. | @@ -519,7 +520,8 @@ required_features = ["gui"] # only built when feature `gui` is > **Scope (important):** `defines` / `cxxflags` / `cflags` on a target apply **only to that > target's exclusive entry source** (its `main`) — never to shared module/impl objects, which -> are compiled once and linked into every target (mcpp's compile-once model). They are the right +> are compiled once and linked into every target (mcpp's compile-once model); the shared sources +> of one target take its own flags through `[targets..build]`. They are the right > tool when the flag only needs to affect a single binary's (or test's) own entry — for example a > per-test contract evaluation semantic (`-fcontract-evaluation-semantic=observe`) for a test whose > `main` exercises the violation, a feature macro the entry alone reads, or a local warning @@ -2154,6 +2156,9 @@ discover = ["tests/**/*.cpp"] # the default | `main` | `{ from = "/" }` | the entry target every test program begins in; each test file is then an ordinary translation unit of its program and defines no `main` (§2.2, `kind = "entry"`) *(2026.10.26.2+)* | | `start` | symbol | the symbol every test image is entered at, on a freestanding target (§2.2, `start`) *(2026.10.26.2+)* | | `programs` | `"file"` or `"directory"` | `"file"` (the default): every discovered file is a program; `"directory"`: the files of one directory below a glob's fixed directory are one program, named by the directory, beginning at the file that defines `main` when one does *(2026.11.8.1+)* | +| `split` | `"file"`, `"case"` or `"none"` | how a program that speaks the test protocol is run: a process per file of its cases (the default), per case, or once *(2026.11.9.1+)* | +| `link` | `"used"` or `"objects"` | `"used"` (the default): a test links what it reaches (`--gc-sections`, `-dead_strip`); `"objects"`: every object whole *(2026.11.9.1+)* | +| `store` | directory or `"none"` | where a run is kept in the mtest store's form, relative to the project; the default is `target/mtest`; `"none"` keeps nothing *(2026.11.9.1+)* | The globs use the vocabulary of `[build] sources`. A test's name is its path relative to the fixed directory of the first glob that matched it, without the diff --git a/docs/05-dependencies.md b/docs/05-dependencies.md index 5e0db2e78..2c66524c1 100644 --- a/docs/05-dependencies.md +++ b/docs/05-dependencies.md @@ -67,8 +67,8 @@ not deprecated: ```toml [dev-dependencies] -mcpplibs.mtest = "0.4.0" -"mcpplibs.mtest.gtest" = "0.4.0" +mcpplibs.mtest = "0.5.2" +"mcpplibs.mtest.gtest" = "0.5.2" ``` ```toml @@ -78,10 +78,11 @@ mylib = { path = "../mylib" } ``` ```toml -# Git dependency — pick exactly one of tag / branch / rev +# Git dependency — at most one of tag / branch / rev; none follows the default branch [dependencies] mylib = { git = "https://github.com/user/mylib.git", tag = "v1.0.0" } applib = { git = "https://github.com/user/applib.git", branch = "develop" } +tiplib = { git = "https://github.com/user/tiplib.git" } # the remote's default branch ``` ```toml @@ -103,6 +104,12 @@ records the same resolution (2026.10.21.1+). LF and CRLF line endings are accept without rewriting an otherwise identical file. This does not skip dependency resolution or change which dependencies the lock pins. +A git dependency that names none of `tag`, `branch` and `rev` follows the remote's +default branch, whose name the remote announces (`main`, `master`, `trunk`, ...; +2026.11.9.1+); it is a `branch` from then on, locked by commit. The same string +forms reach a platform or a tool on the command line: +`--platform git+https://example.org/acme/acme-platform.git#tag=v1`. + **Git dependencies and `mcpp.lock`**: a `tag` or `rev` already names a fixed point in history, but a `branch` moves. The first build resolves the branch to a commit and records it in `mcpp.lock`, and every later build rebuilds **that** commit — the @@ -447,6 +454,16 @@ gtest = "1.15.2" `mcpp build` ignores these; `mcpp test` resolves and uses them, for the test units only: the package's own sources compile under `mcpp test` as under `mcpp build` (2026.11.8.1+, [08](08-testing.md#tests-that-reach-packages-the-artifact-does-not)). `mcpp test` automatically discovers `tests/**/*.cpp` and compiles them into test binaries. The runner is framework-agnostic: each file is an independent binary judged by exit code — a bare `main`, gtest (via `[dev-dependencies]` + `gtest_main`), or any other framework all work identically, and `-- args` are forwarded to every test binary (e.g. `-- --gtest_filter=...`). Note: synthesized test target names may contain `/` (`tests/00-a/0.cpp` → `00-a/0`), unlike `[targets.*]` names — the two namespaces are intentionally separate (test targets never enter the manifest or publishing). Tests are named by their `tests/`-relative path (`tests/00-a/0.cpp` → `00-a/0`), each test compiles in isolation (a broken test fails alone; package/dep breakage is reported as a build error instead), and `mcpp test ` / `--message-format json` filter and machine-format the run. +**The package being built stands for itself in its own graph** (2026.11.9.1+). +A dependency reached through the package's `[dev-dependencies]` may name the +package being built: a library tested with a framework that itself uses the +library (`mcpplibs.os` under mtest). The +edge is the package itself, whatever version it asked for, and a version it did +not ask for is said; a second copy would provide every module of the package +twice in one program, and what is under test is the package as it is here. The +root's `[dev-dependencies]` serve its tests only, so the framework reaches the +library without a cycle of packages. + ## `[build-dependencies]` — Build-Time Dependencies (mcpp 2026.8.29.1+) ```toml @@ -666,6 +683,26 @@ verified source. searched for elsewhere and no line ending is converted. Paths in a patch carry one leading directory (`a/`, `b/`), which is removed. Renames, copies and binary changes are refused. +- A patch names files from the **package's root**: the directory of the + package's resolved manifest inside its archive (`run/` of an archive whose + descriptor reads `mcpp = "*/run/mcpp.toml"`), so that one patch applies to + every version whose layout is the same. `patch_root = ""` on the edge + names another directory, relative to the package's root and inside its + source. A patch written from the archive's top, as before 2026.11.9, applies + as written, and the build says so. +- `mcpp why deps` reports each dependency's root as the build reads it (its + patches applied). A patch is the difference git writes from a copy of that + tree: + + ```sh + cp -R /tmp/zlib && cd /tmp/zlib + git init -q && git add -A && git commit -qm base + # edit, then + git diff > /patches/0001-fix.patch + ``` + + and the edge lists it in `patches`. A further patch is written the same way + from the root the patched build reports, and is listed after the first. - Version and git dependencies may carry patches; a `path` dependency is a tree its consumer owns and is refused with them. - `mcpp.lock` names the variant and each patch by its path and SHA-256; diff --git a/docs/08-testing.md b/docs/08-testing.md index 03bb0160c..b046d20da 100644 --- a/docs/08-testing.md +++ b/docs/08-testing.md @@ -61,10 +61,38 @@ below a glob's fixed directory one program, named by the directory it, its cases are all of theirs, and it begins at the file that defines `main` when one does, else as a file that defines none does. Two files of one directory that define `main` are refused, naming both; a file directly in the fixed -directory stays a program of its own. A program of many files is linked once +directory stays a program of its own. A test's name is the path of its program, +so a name that is another's directory (`unit` and `unit/own`) is refused, +naming both (2026.11.9.1+). A program of many files is linked once where a program per file is linked once per file, which is what a suite of many small files spends its build time on. +A program that speaks the test protocol runs as **one process per file of its +cases** (2026.11.9.1+): mcpp lists the cases of a program of several files, and +each file's run selects its own (P4) and is scheduled across the workers as a program is, named +`/` -- the isolation and the parallelism of a program per file, +with one link. `[test] split = "case"` gives each case its own process, and +`"none"` runs the program once. A pattern names a program or a part of one as +the run names it: `mcpp test unit/test_home` (or `home`) runs that file's part. + +A test program keeps what it reaches (2026.11.9.1+): units are compiled with one +section per function and per object, and a test links with `--gc-sections` +(`-dead_strip` on Mach-O), so a test of one module does not carry its package's +every function (mcpp's own unit tests: 32 MB each to 14 MB). Static +constructors are the collection's roots. `[test] link = "objects"` keeps every +object whole. + +A run is kept in the form of the mtest store (2026.11.9.1+): `target/mtest/` +holds `runs//run.json`, a line per process in `programs.ndjson` (its +status, duration, start, worker and selected cases) and a line per case in +`cases.ndjson`, and `index.ndjson` lists the runs. `mtest store import --from +target/mtest` takes them into mtest's store, and an `mtest.toml` whose +`[project] store` is `target/mtest` reads them where they are; `mtest report`, +`analyze` and `graph` then read them as mtest's own runs (the Markdown report, +the slowest cases, the critical path and the schedule). In CI, `commit` and +`group` are `GITHUB_SHA` and `GITHUB_RUN_ID`. `[test] store = ""` names +another directory, relative to the project; `"none"` keeps nothing. + A suite compiled from several sources of its own is also a package: a workspace member whose `[build] sources` carries the suite and whose one test program drives it, selected with `mcpp test -p `. @@ -275,9 +303,10 @@ mcpp test --archive tests.tar.gz # in a checkout: ru ``` The archive holds each leg's test programs and `mcpp-tests.json`, which names -each test's leg, arguments and working directory. Reading it builds nothing; -the tests run in the checkout, through this machine's runner or system, and the -report is `mcpp test`'s. A failed test exits 1, and so does an archive of which +each test's leg, arguments, working directory and the test protocol it +implements. Reading it builds nothing; the tests run in the checkout, through +this machine's runner or system, by `mcpp test`'s own runner: its cases, a +process per file of them, the workers and the store (2026.11.9.1+). A failed test exits 1, and so does an archive of which no test ran. Legs this machine does not reach are reported as not run ([SPEC-015 §4](specs/target-sets.md)). The archive is a tar compressed as its name states: `.tar.gz` (or `.tgz`), `.tar.zst`, `.tar.xz` or `.tar`; another @@ -307,7 +336,9 @@ test-protocol = 1 ``` or when its file carries the marker `mtest-protocol=1`, as a program built -with mtest's libraries does. A program that carries the marker and writes no +with mtest's libraries does; of a test that keeps what it reaches, an object it +was linked from carrying the marker counts as well, so a marker nothing +references (mtest before 0.5.1) is not lost at the link. A program that carries the marker and writes no `program` event spoke no protocol: a file of plain `main` linked beside a framework's library is one case, judged by its exit status, run and listed. mcpp then gives the program the protocol's variables (`MTEST_PROTOCOL`, `MTEST_MODE`, `MTEST_REPORT`, `MTEST_TMPDIR`, and diff --git a/docs/09-commands-by-scenario.md b/docs/09-commands-by-scenario.md index 91e375f63..be8d8d42d 100644 --- a/docs/09-commands-by-scenario.md +++ b/docs/09-commands-by-scenario.md @@ -216,9 +216,11 @@ dependency graph: requested by huxdemo.comp@0.1.0 as 'fw' in [dependencies] linked static (default) instances: target + root: /work/fw ``` -`instances` states whether the package is compiled for the target, for the +`root` is the package's root as the build reads it, a patched dependency's +being its variant's (2026.11.9.1+; docs/05). `instances` states whether the package is compiled for the target, for the build programs, or for both (2026.10.11.1+). `mcpp why build-programs` lists the build programs' graphs: each program, the libraries it uses, the identity it was compiled from and its binary. `mcpp why compat` lists every use of a @@ -227,8 +229,14 @@ deprecated reading with the release that removes it (51 — Supported Versions). The same graph is recorded under `graph` in `target///resolution.json`, one entry per package with the root first: `package` (canonical identity, namespace, name, version, source), -`root`, `requested_by` (`requester`, `key`, `table`), and, for a library, -`link` (`form`, `reason`). +`root`, `requested_by` (`requester`, `key`, `table`), `manifest_dir` (the +package's root), and, for a library, `link` (`form`, `reason`). + +`mcpp why target` lists a request's candidates in order (docs/21), `mcpp why +flags []` each word of a unit's command line with the layer that gave it, +and `mcpp why platform` the rows a platform defines and the identities it +serves (docs/35); the last two are not part of the report without a topic +(2026.11.9.1+). The topic is `toolchain`, `runtime`, `deps` or `runners`, and all four report when none is named. `--target` and `--toolchain` turn the report into a query @@ -765,7 +773,7 @@ mcpp is involved and its documentation will not help. ## Current limitations - `mcpp why --format json` is defined for the `toolchain`, `sources`, `tool`, - `payload`, `build-programs` and `compat` topics. The other topics report + `payload`, `build-programs`, `compat`, `target`, `flags` and `platform` topics. The other topics report `'' has no machine-readable shape yet` and exit non-zero. - `mcpp search` matches a substring; there is no field selector, and no way to restrict a search to one namespace. diff --git a/docs/20-toolchains.md b/docs/20-toolchains.md index f0119782b..f4db148ee 100644 --- a/docs/20-toolchains.md +++ b/docs/20-toolchains.md @@ -307,6 +307,15 @@ Before 2026.8.3.3 a mingw build on a Windows host emitted `foo.lib` — a GNU archive wearing an MSVC name, which MSVC cannot consume. A script that globs `*.lib` out of a `windows-gnu` build, it needs to glob `*.a` now. +### clang with the MinGW-w64 payload (2026.11.9.1+) + +The MinGW-w64 provider pairs with gcc and with llvm. With llvm declared +(`--toolchain llvm@`, or `[target.x86_64-windows-gnu] toolchain`), the +payload is installed as for gcc and becomes clang's sysroot: its C library, +libstdc++ (whose `bits/std.cc` is the std module, so `import std` works) and +the GCC runtime, linked by lld. The build reports `Pairs clang ... with the +gcc@ payload`. + ## Linux ELF from Windows (`x86_64-linux-musl`, no WSL required) The mirror of the section above: a Windows machine producing a **fully static diff --git a/docs/21-the-target-triple.md b/docs/21-the-target-triple.md index 42f91e62b..97eb0e197 100644 --- a/docs/21-the-target-triple.md +++ b/docs/21-the-target-triple.md @@ -146,40 +146,43 @@ A `.js` file carries no shebang, so an emsdk payload installed before its descriptor gained a `runner` field needs the index refresh [chapter 20](20-toolchains.md) describes before `mcpp run` finds one. -## Declining The Third Segment +## Declining The Third Segment: A Request Names A System (2026.11.9.1+) -`-` is a complete target on every platform: +A target written whole is an **identity**: the output directory, the cache key +and the subject of a `cfg()` predicate. A target that names only `-` +is a **request**: it reaches every identity of that system, and the build takes +the first one whose **provider** -- the toolchain and the target side that +serve it -- is available on this machine (SPEC-016 §4). ```bash -mcpp build --target x86_64-linux # = x86_64-linux-gnu -mcpp build --target x86_64-windows # = x86_64-windows-gnu +mcpp build --target x86_64-linux # x86_64-linux-gnu on Linux; -musl where no glibc payload is +mcpp build --target x86_64-windows # -msvc with MSVC installed, else -musl (openkal-platform) +mcpp build --target aarch64-macos # the Apple identity on macOS, else aarch64-macos-musl mcpp build --target riscv64-none # = riscv64-none-elf -mcpp build --target aarch64-linux # = aarch64-linux-gnu -mcpp build --target aarch64-macos # macOS has no segment to decline ``` -Declining it changes no identity. The output directory, the cache key and the -subject of a `cfg()` predicate are all the canonical form, so the two spellings -share one fingerprint and the second build is a cache hit rather than a second -full build. +The candidates are ordered by the providers' priorities (data the engine ships: +a system's own toolchain and C library first, openkal-platform next, other +conventions after), then by the rows' order. The report says which identity +was taken, and why when the first candidate is not available here: -What differs is the record of what was asked for. A triple serves as an -identity, which must be total, and as a request, which must be able to say -nothing; mcpp keeps both, filling the segment for the identity while recording -that the fill was a fill. - -### The completion is chosen from the vocabulary, not from a fixed word - -A request with an omitted environment segment is completed against the -known-target table. A supported lexical default takes precedence; otherwise, -a unique supported sibling is selected. If none is supported, the lexical -form is retained and the diagnostic names the registered siblings. An -ambiguous request lists the supported candidates. +```text + Target x86_64-windows → x86_64-windows-musl + msvc is not available on linux-x86_64 +``` -`aarch64-linux` selects `aarch64-linux-gnu`. The native Linux ARM64 payload -uses LLVM 23.1.3 and glibc. `aarch64-linux-musl` remains the explicit spelling -for a static musl artifact. A written environment segment is retained. +`mcpp why target [--target ]` lists the candidates in order with their +priorities and availability (`--format json` for scripts). A project changes +the order with `[target..priority] = `, or chooses a +built-in provider by name with `platform = "builtin:"`. A written +identity is never replaced: `--target x86_64-linux-gnu` builds that identity or +is refused naming what is missing. `mcpp.lock` records the identity each +request was given, and `--locked` refuses a build that selects another. +When the dependency graph supplies the C library (`mcpp:c-abi`) and the request +names none, the identity is the C library's: `x86_64-linux` over a graph's musl +builds `x86_64-linux-musl`. A written segment the graph contradicts is reported +and kept. ### The Spelling To Use @@ -525,10 +528,13 @@ other's rows. | `x86_64-windows-msvc` | verified | — | — | — | — | system | | `aarch64-macos` | verified | — | — | — | SDK | — | | `x86_64-macos` | planned | — | planned | planned | planned | planned | +| `aarch64-macos-musl` | preview | `llvm@23.1.3` | graph | graph | graph | graph | +| `x86_64-macos-musl` | preview | `llvm@23.1.3` | graph | graph | graph | graph | | `riscv64-none-elf` | verified | `llvm@23.1.3` | payload | payload | payload | payload | | `riscv32-none-elf` | verified | `llvm@23.1.3` | payload | payload | payload | payload | | `aarch64-none-elf` | preview | `llvm@23.1.3` | payload | payload | payload | payload | | `x86_64-none-elf` | preview | `llvm@23.1.3` | payload | payload | payload | payload | +| `i686-none-elf` | preview | `llvm@23.1.3` | payload | payload | payload | payload | | `thumbv6m-none-eabi` | verified | `llvm@23.1.3` | payload | payload | payload | payload | | `thumbv7m-none-eabi` | verified | `llvm@23.1.3` | payload | payload | payload | payload | | `thumbv7em-none-eabi` | preview | `llvm@23.1.3` | payload | payload | payload | payload | diff --git a/docs/22-target-side.md b/docs/22-target-side.md index f61b5a92c..facc2a442 100644 --- a/docs/22-target-side.md +++ b/docs/22-target-side.md @@ -297,6 +297,27 @@ The check runs before compilation begins. The combination it rejects otherwise fails inside the runtime's own headers, in a message naming a file the reader has never opened and no decision mcpp made. +### Default providers (2026.11.9.1+) + +A package that requires a capability states the providers it takes when nothing +else provides it, per target (SPEC-002 §3.6): + +```toml +[package] +requires = ["mcpp:kernel-abi=openkal"] + +[defaults."mcpp:kernel-abi=openkal"] +linux = "openkal-linux@^0.25" +macos = "openkal-macos@^0.25" +"*" = "openkal-posix@^0.25" +``` + +A provider in the graph -- the consumer's dependency, or the package the +platform adds for its layer -- is used instead; otherwise the default whose +selector matches the target joins the graph as the package's dependency, and +the build reports it (`Default ... for ... of ...`). A selector that names the +target wins over `*`. + The same statement is the recipe for a package whose install hook compiles a static library from source. The library is compiled against one C++ standard library and cannot be linked into a program that uses another, and the store diff --git a/docs/30-build-mcpp.md b/docs/30-build-mcpp.md index 044ea940e..e1c8116cc 100644 --- a/docs/30-build-mcpp.md +++ b/docs/30-build-mcpp.md @@ -1345,6 +1345,7 @@ The running program receives the build context as `MCPP_*` variables | `MCPP_TARGET_OS` *(0.0.100+)* | `mcpp::target_os()` | the target's OS segment (`linux`/`macos`/`windows`) — no need to hand-split `MCPP_TARGET` | | `MCPP_TARGET_ARCH` *(0.0.100+)* | `mcpp::target_arch()` | the target's arch segment (GNU spelling: `x86_64`, `aarch64`, …) | | `MCPP_TARGET_ENV` *(0.0.100+)* | `mcpp::target_env()` | the target's env segment (`gnu`/`musl`/`msvc`); empty string when the triple has none (macOS) | +| `MCPP_TARGET_PROVIDER` *(2026.11.9.1+)* | `mcpp::target_provider()` | the provider the selection gave the target's identity (`openkal`, `musl-gcc`, `msvc`, ...; SPEC-016 §4); empty string for the host's own build | | `MCPP_HOST` | `mcpp::host()` | the host triple | | `MCPP_PROFILE` | `mcpp::profile()` | effective profile name (`dev`/`release`/…) | | `MCPP_TOOLCHAIN_SYSROOT` *(2026.9.5.2+)* | `mcpp::toolchain_sysroot()` | the `--sysroot` mcpp passes to its own compiler; empty when it passes none. For a rule package that runs a **second** compiler — see "Driving a second compiler" above | diff --git a/docs/35-platforms.md b/docs/35-platforms.md index 01af845d5..d14989174 100644 --- a/docs/35-platforms.md +++ b/docs/35-platforms.md @@ -160,6 +160,12 @@ On Windows the arrangement is the GNU ABI's row (`--target x86_64-windows-gnu`); a triple outside `[platform.hosted] targets` is refused, naming the row of the same system and architecture that it serves. +A C library of the graph (`mcpp:c-abi`) leads a Mach-O image's link +(2026.11.9.1+). The loader of that format runs an image's initialisers before +its entry, in link order, and the C library is brought up by an initialiser of +its own; with its objects first, it is up before any global constructor of the +program runs, as a C runtime's start objects precede a program on ELF. + ## The version group `[platform.versions]` names one version of each package of a family. From mcpp @@ -284,6 +290,43 @@ platform package, with no `--target`, builds its tests for each of its architecture and those an execution host is named for (2026.10.29.1+), and reports the others as built and not run. +## Defining targets and providers (2026.11.9.1+) + +A platform may serve a system no built-in row names. `[[platform.define]]` +states the row, `[[platform.provider]]` the provider that serves it: + +```toml +[[platform.define]] +triple = "x86_64-freebsd" +clang-triple = "x86_64-unknown-freebsd14.1" +object-format = "elf" +family = "unix" +execution = "freebsd" # the system its programs run on + +[[platform.provider]] +serves = ["x86_64-freebsd"] # patterns: `*` is any one segment +priority = 100 +hosted = true # the C environment has `main` +``` + +With the platform selected (`[target.x86_64-freebsd] platform = {...}` or +`--platform`), `--target x86_64-freebsd` builds into `target/x86_64-freebsd/`, +`cfg(os = "freebsd")` holds, and a program whose system is not this machine's +is built, not run, until a runner states how it runs (docs/41). The row's +machine description is the platform's: a target whose machine differs from a +built-in row is another row. `[platform]` and `[platform.hosted]` remain the two +shorthands of a provider. + +`mcpp new --template platform` writes a platform package defining +`-`. Reading a platform's manifest checks its statement against the +rows: a definition of a built-in row, or a row defined twice, is an error +wherever the platform is read; a provider that serves no row, and two providers +that serve one identity at one priority, are warnings of the platform's own +build. `mcpp why platform` lists the rows the platform defines and the +identities it serves, each with its provider and priority: in the platform's +directory its own, in a project the one its target selects (`--target`; +`--format json`). + ## Inspecting a configuration ```sh diff --git a/docs/41-devices.md b/docs/41-devices.md index db3222791..ab52f6be0 100644 --- a/docs/41-devices.md +++ b/docs/41-devices.md @@ -142,6 +142,28 @@ A runner that executes the artifact where it lies ignores the variable. A runner that moves the artifact — to a device, a container, a remote host — copies each listed file to the destination beside the moved artifact. +### Runner packages and the systems a platform defines (2026.11.9.1+) + +A runner is a program; a package that builds it is a **runner package** (a virtual +machine, a remote host over ssh, a device). The project names the package as one +of the target's tools and the runner by the tool's name: + +```toml +[target.x86_64-freebsd] +runner = ["freebsd-vm", "{}"] + +[target.x86_64-freebsd.tools] +freebsd-vm = { git = "https://example.org/freebsd-vm.git", tag = "v1" } +``` + +The tool is built for this machine and started with the argv and the +environment above: it transfers the artifact and the files `MCPP_RUNTIME_FILES` +lists, runs the argv, streams the program's output, and exits with the +program's status. An artifact of a row a platform defines whose `execution` is +another system than this machine's, with no runner and no execution host +(`run_under`), is **built, not run**: `mcpp run` refuses it naming the keys that +state a runner, and `mcpp test` reports its tests as not run. + ## Running a distributable `mcpp run --pkg-format ` packs the distributable `` and runs it. Without diff --git a/docs/50-machine-output.md b/docs/50-machine-output.md index 0dfe54ca2..371a44988 100644 --- a/docs/50-machine-output.md +++ b/docs/50-machine-output.md @@ -513,6 +513,22 @@ carries `status` (`ok` or `refused`) and: | `mcpp.why.build-programs` | `graphs[]` | one entry per build programs' graph: `workspace` (its directory) and `programs[]`, each `{member, package, source, libraries[], identity, binary}` | | `mcpp.why.compat` | `uses[]` | one entry per use of a deprecated reading: `{id, where, detail, warned, since, floor, canonical, docs}`; `warned` is false in a manifest the build does not edit | +### `mcpp.why.target`, `mcpp.why.flags` and `mcpp.why.platform` *(mcpp 2026.11.9.1+)* + +``` +mcpp why target [--target ] --format json +mcpp why flags [] [--target ] --format json +mcpp why platform [--target ] --format json +``` + +`data` carries `status` and: + +| kind | field | content | +|---|---|---| +| `mcpp.why.target` | `target` | `{request, identity, provider, priority, fallback, why, candidates[]}`, each candidate `{identity, provider, priority, available, why}` (SPEC-016 §7) | +| `mcpp.why.flags` | `flags` | `{identity, units[], link[]}`: each unit `{file, package, words[]}`, each word `{word, layer, detail}`, the layer one of compiler, row, platform, toolchain, package, engine | +| `mcpp.why.platform` | `platform` | `{package, version, defines[], serves[], warnings[]}`, each served `{identity, provider, priority}`; `null` when the build uses no platform package | + `engineModules` in `mcpp.env` is the directory of the engine interface package of this mcpp version, which a build program's graph depends on (30 — `build.mcpp`); it exists once a build program has been compiled. diff --git a/docs/90-build-from-source.md b/docs/90-build-from-source.md index 7c2f966b8..bb3d5aacd 100644 --- a/docs/90-build-from-source.md +++ b/docs/90-build-from-source.md @@ -81,19 +81,23 @@ src/ └── scaffold/ `mcpp new` and templates tests/ -├── unit/ 156 C++ tests, discovered by `mcpp test` -└── e2e/ 649 shell scripts against a real binary +├── unit/ 166 files of mtest cases, one test program (`[test] programs = "directory"`) +└── e2e/ 711 shell scripts against a real binary ``` ## Test Organization Two layers, and they answer different questions. -**Unit tests** (`tests/unit/`, 156 files) are C++ programs `mcpp test` -discovers. They exercise a module's contract with no binary and no filesystem -state. +**Unit tests** (`tests/unit/`, 166 files, and each member's `modules//tests/`) +are C++ programs `mcpp test` discovers. They exercise a module's contract with no +binary and no filesystem state. Each is a file of [mtest](https://github.com/mcpp-community/mtest) +cases: an object `UTest name{"Suite/Case", [] { ... }};` is one case, with no `main` +(`mcpp test --guide`). `mcpp test --case 'TargetSelect/*'` runs the cases a pattern +names; `mcpp test -p ` runs a member's. The files are one program, linked once and run as +one process per file. -**End-to-end tests** (`tests/e2e/NN_.sh`, 649 files) run a real `mcpp` +**End-to-end tests** (`tests/e2e/NN_.sh`, 721 files) run a real `mcpp` binary against a real project. `run_all.sh` is the CI entry point. `mcpp test` does **not** run them. @@ -122,7 +126,9 @@ runs beside it; a script that changes the shared home's configuration says so). divides by the measured history of the store (`tests/e2e/.mtest`, which CI takes from main's last run), `E2E_JOBS` runs that many scripts at once, and `E2E_SET=pr` runs the scripts a change affects by the nightly coverage map -(`.github/workflows/e2e-coverage.yml`). `mtest explain` in `tests/e2e` says where +(`.github/workflows/e2e-coverage.yml`); a script the set leaves out is reported +`unaffected`, which the check that every script runs somewhere (rule R5) accepts +for that run, main's full runs being its coverage. `mtest explain` in `tests/e2e` says where a script failed and how to run it again. ## Writing a check that measures something diff --git a/docs/README.md b/docs/README.md index 0d442e937..9232b8567 100644 --- a/docs/README.md +++ b/docs/README.md @@ -178,3 +178,4 @@ downstream tooling. - [SPEC-013 — Platforms: the configuration of a class of targets, its selection, merge, graph completion, link, seal, runner and carrier](specs/platforms.md) - [SPEC-014 — Packaging and distribution: link, closure and container, legs, the staged tree, the platform's tree, run coverage and single-file packages](specs/packaging.md) - [SPEC-015 — Target sets and legs: spelling and expansion, commands over several legs, test archives](specs/target-sets.md) + - [SPEC-016 — Targets and providers: rows, providers, the selection, platform-defined rows, reports and the lock](specs/targets-and-providers.md) diff --git a/docs/specs/README.md b/docs/specs/README.md index 1e7047588..49407cd9c 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -45,6 +45,7 @@ | [SPEC-013](platforms.md) | 平台:`[platform]` 的七组键、版本组与 `hosted`,三种写法与层次,目标图与宿主图的选择,`cfg(platform)`,依赖图的补全,链接、命名与封装,运行器与载体,平台阶段与协议 17 | 评审中 v1.5 | 2026-11-07 | mcpp >= 2026.10.27.1;§10 的执行宿主与 §11 的载体架构 mcpp >= 2026.10.29.1;§7.5 版本组参与解析与 §14 平台系列 mcpp >= 2026.11.7.1;§14 内置平台为确切版本 mcpp >= 2026.11.7.2 | | [SPEC-014](packaging.md) | 打包与分发:链接、闭包、容器三个维度与构建腿,腿的合并,暂存树与 `mcpp-package.json`,平台的树、运行覆盖、单文件、polyglot、容器列表、签名、压缩、合并与分发描述 | 评审中 v2.1 | 2026-10-31 | mcpp >= 2026.10.31.1;逐条标注 | | [SPEC-015](target-sets.md) | 目标集合与腿:集合的写法与展开,命令对多条腿的语义,串行规划与一起构建,测试归档,机器输出 | 评审中 v1.0 | 2026-10-30 | mcpp >= 2026.10.30.1 | +| [SPEC-016](targets-and-providers.md) | 目标与提供者:行、提供者、选择规则、平台定义的行、C 环境与执行、报告、机器输出与锁 | 评审中 v1.0 | 2026-11-09 | mcpp >= 2026.11.9.1 | ## 文档约定 diff --git a/docs/specs/package-identity.md b/docs/specs/package-identity.md index fc754ae02..8fdaf93ee 100644 --- a/docs/specs/package-identity.md +++ b/docs/specs/package-identity.md @@ -6,7 +6,7 @@ | **标题** | 包身份(`package.namespace` / `package.name`)、`[dependencies]` 选择器与匹配机制 | | **状态** | **评审中(Review)** —— 已实现 | | **版本** | 1.7 | -| **最后修改** | 2026-10-17 | +| **最后修改** | 2026-11-09 | | **最低实现版本** | 描述符身份:mcpp **0.0.106**;精确 selector:mcpp **2026.8.10.1**(xlings >= 0.4.69) | | **作者/维护** | mcpp-community | | **相关设计文档** | `.agents/docs/2026-06-20-package-resolution-architecture.md` §4
`.agents/docs/2026-06-26-identity-first-resolution-no-filename.md`
`.agents/docs/2026-07-25-issue278-descriptor-name-form-canonicalization-design.md`
`.agents/docs/2026-07-25-name-namespace-bidirectional-verification-report.md`
`.agents/docs/2026-07-25-name-namespace-canonical-implementation-spec.md` | @@ -318,6 +318,17 @@ cancellation = { path = "modules/cancellation" }`)。告警在解析完成之后 采用而非拒绝:来源已由声明行固定,§4.2 反对候选搜索的理由(索引状态改变依赖的指向) 在这里不成立。 +### 5.5 git 引用 已实现(mcpp >= 2026.11.9.1) + +- git 来源**至多**写出 `rev`、`tag`、`branch` 之一;写出两个以上**必须**被拒绝。 +- 一个都不写时,引用为远端的**缺省分支**:名字取自远端的通告(`git ls-remote --symref HEAD`), + 远端不通告时依次尝试 `main`、`master`,报告写出所用分支;引擎**禁止**写死分支名。缺省分支按 `branch` + 处理:锁记录解析得到的提交,离线时由锁回答。 +- 命令行与环境变量中引用的字符串形式为 `[.]@<约束>`、`path:`(或路径)、 + `git+[#tag=|rev=|branch=]`、`builtin:`(只用于提供者,SPEC-016 §3)。只以 URL 指名的 + 仓库,其包名取 URL 的最后一段(去掉 `.git`),取得后的清单名按 §5.4 核对。 +- 清单中写出的引用记入锁;只在命令行或环境变量中出现的引用不改写锁。 + 清单未声明 `namespace` 时,身份的命名空间取键的命名空间(§5.3 的兼容边界)。此时同一 来源经两个命名空间不同的键到达,就是同一来源上的两个身份;实现**必须**在扫描源码之前 拒绝,点名二者。 @@ -541,6 +552,7 @@ mcpp 侧的使用: | 版本 | 日期 | 变更 | |---|---|---| +| 1.8 | 2026-11-09 | 新增 §5.5:git 引用至多一个 ref、缺省分支、命令行的字符串形式(mcpp 2026.11.9.1) | | 1.5 | 2026-09-27 | 新增 §10.1:版本位的回答与 xlings 一致(安装记录优先,其次按 xlings 版本文法选择;已记录而缺失的载荷不算已安装),mcpp#712、#716;新增 §10.2:打包修订 `revision` 的含义与 mcpp 侧的三处使用(mcpp 2026.9.27.1,xlings 2026.9.27.1) | | 1.7 | 2026-10-17 | §10.2:新增 `floor`,区分可用与当前;工具链、运行时与宿主工具按使用/供给两种意图解析,使用命令不因修订号替换可用载荷;工具链载荷的修订号进入驱动身份;替换失败的语义。§10.3 第 3 条把修订号一致移出完整性(mcpp 2026.10.17.1,xlings 2026.10.17.1,libxpkg 0.0.65) | | 1.6 | 2026-10-16 | 新增 §10.3:载荷的身份含修订号,身份相同的载荷可以经链接条目在 home 之间共用;载荷所在的 store 由服务它的索引决定;完整性由提供者回答,mcpp 不写入载荷(mcpp 2026.10.16.1,xlings 2026.10.16.1) | diff --git a/docs/specs/platforms.md b/docs/specs/platforms.md index 01885a671..8b375fcb3 100644 --- a/docs/specs/platforms.md +++ b/docs/specs/platforms.md @@ -4,9 +4,9 @@ |---|---| | 规范编号 | SPEC-013 | | 标题 | 平台:一类目标的可复用配置,其声明、选择、合并、对依赖图的补全、链接与封装、运行器与载体 | -| 状态 | 评审中 v1.5 | +| 状态 | 评审中 v1.6 | | 版本 | 1.5 | -| 最后修改 | 2026-11-07 | +| 最后修改 | 2026-11-09 | | 对应实现 | mcpp >= 2026.10.27.1;§10 的执行宿主与 §11 的载体架构 mcpp >= 2026.10.29.1;`runner.hosts`、载体列表、`descriptor_keys` 与 `universal` 的弃用 mcpp >= 2026.10.30.1;§7.5 版本组参与解析、§14 平台系列与 §15.1 系列的锁 mcpp >= 2026.11.7.1;§14 内置平台为确切版本与提示 mcpp >= 2026.11.7.2;逐条标注 | | 相关设计文档 | `.agents/docs/2026-10-28-platforms-design.md`、`.agents/docs/2026-10-28-platforms-part2-packaging-and-distribution-design.md`、`.agents/docs/2026-10-29-openkal-run-test-pack-design.md` | | 相关 issue | mcpp#846、mcpp#847、mcpp#848 | @@ -35,8 +35,12 @@ ## 1. `[platform]` 段 -一个包的清单含 `[platform]` 段时,该包是平台包。`[platform]` **必须**含 `targets`;否则清单被拒绝。 -**已实现** +一个包的清单含 `[platform]` 段时,该包是平台包。`[platform]` **必须**含 `targets` 或至少一个 +`[[platform.provider]]`;否则清单被拒绝。**已实现** + +**行与提供者的一般形式(mcpp >= 2026.11.9.1)。** `[[platform.define]]` 定义平台的行,`[[platform.provider]]` 声明 +服务行的提供者(SPEC-016 §2、§3、§5);`[platform]` 是服务 `targets` 的提供者,`[platform.hosted]` 是服务宿主三元组的 +hosted 提供者,二者为一般形式的简写,照旧可读。服务某身份的 `[[platform.provider]]` 的字段代替平台语句的同名字段。 ```toml [platform] @@ -56,6 +60,9 @@ plugin = "build.mcpp" [platform.link] driver, flags, start, script, inputs, exe, shared, seal [platform.versions] = "" [platform.hosted] targets, toolchain, layers + +[[platform.define]] triple, clang-triple, object-format, family, execution, ... (SPEC-016 §2.1) +[[platform.provider]] serves, priority, hosts, pairs, toolchain, sysroot, layers, link, hosted, base, execution ``` ### 1.1 键 @@ -280,6 +287,10 @@ plugin = "build.mcpp" | `shared` | 共享产物的名字模式,`{name}` 恰好一次,不含目录 | | `seal` | 封装步骤,每步一个 argv(§9.3) | +`driver = "compiler"` 在每一行生效,包括行本身以 `link-emulation` 直接调用 `ld.lld` 的裸机行(mcpp >= 2026.11.9.1)。 +平台的 `exe`、`shared` 中 `{name}` 之后的部分是平台的格式:构建程序的 `artifact-name` 改变它时被报告,平台的模式保留; +项目与本机的陈述照常生效(mcpp >= 2026.11.9.1)。 + `driver = "lld"` 时引擎在编译器旁依次查找 `ld.lld`、`lld`、`lld-link`(Windows 载荷只带 `lld-link`);都没有时拒绝 (`lld-required-absent`),消息写出平台的工具链。程序名不是 `ld.lld` 时引擎加 `-flavor gnu`。链接或封装命名的程序既非图中为本机构建、也不在本机可找到时拒绝 (`platform-tool-absent`),消息写出负责构建它的包,或写出 `[target.'cfg(platform = "")'.tools]` 的写法。 @@ -352,6 +363,9 @@ run_under = ["qemu-aarch64"] 需要载体的打包被拒绝并写出该键;本 mcpp 不构建程序的行(SPEC-014 §4.4)上的载体在生成工程之前被拒绝,写出系统、架构与行 (2026.10.29.1)。 +**平台定义的系统(mcpp >= 2026.11.9.1)。** `--systems` 与载体项的 `hosts` 接受任何系统词:`-` 的 +`` 为内置系统或已解析平台定义的系统,`all` 包含后者。平台定义的系统上的程序建于定义它的平台之上,为该行的静态程序。 + **载体项的选择(mcpp >= 2026.10.30.1)。** 为 `-` 构建载体程序时(上述三处与 §10 的执行宿主),所用的是 `hosts` 列出该对的第一项,否则为第一个未写 `hosts` 的项;两者都没有时,如同平台未声明 `carrier`,打包或运行被拒绝,写出 该对。`build` 不是 `"row"` 的项留给将来的构建模型(一个文件服务多个系统与架构的载体,mcpp#849),本 mcpp 读到时跳过并警告。 @@ -532,3 +546,4 @@ openkal-platform 的 `targets` 因此不含 WebAssembly。Web 经 `wasm32-emscri | 1.3 | 2026-10-30 | mcpp 2026.10.30.1(设计 2026-10-29 §4.3–§4.5、§13.3):§1.1 `universal` 弃用、不起作用;`runner.hosts`;`carrier` 接受列表,每项的 `hosts` 与 `build`;`descriptor_keys`;§10 执行宿主的运行器程序按 `hosts` 选择载体项;§11 携带映像的载体只属于单文件,树携带运行器形式;判据 13 改写(e2e 961)。 | | 1.5 | 2026-11-07 | mcpp 2026.11.7.2(设计 2026-11-07 §4.10,D20):§14 目标表的平台列为确切版本 0.24.3,锁中不满足的记录不被采用并写出;使用内置版本的构建失败且索引有更新版本时给出提示;判据 15 新增(e2e 1025)。 | | 1.4 | 2026-11-07 | mcpp 2026.11.7.1(设计 2026-11-07 §4.1.4、§4.2.2 A2/A3):§7.5 版本组参与解析,解析后的检查保留为断言;§3.2 平台引用接受 SemVer 约束;§14 目标表的平台列为系列 `^0.24`,系列取引擎可读的最新版本;§15.1 系列从锁中读回,`mcpp update` 移动它;判据 5 改写、14 新增(e2e 1020、1021)。 | +| 1.6 | 2026-11-09 | mcpp 2026.11.9.1(SPEC-016,mcpp#911):§1 `[[platform.define]]` 与 `[[platform.provider]]` 为一般形式,`[platform]`、`[platform.hosted]` 为其简写;§9.2 `driver = "compiler"` 在每一行生效,平台的产物名模式不被构建程序改变;§11 平台定义的系统上的载体与 `--systems`;主机对为任意系统词。 | diff --git a/docs/specs/target-sets.md b/docs/specs/target-sets.md index eb1b31e78..15c25a337 100644 --- a/docs/specs/target-sets.md +++ b/docs/specs/target-sets.md @@ -4,9 +4,9 @@ |---|---| | 规范编号 | SPEC-015 | | 标题 | 目标集合与腿:集合的写法与展开,命令对集合的语义,腿的规划与构建,测试归档,机器输出 | -| 状态 | 评审中 v1.0 | -| 版本 | 1.0 | -| 最后修改 | 2026-10-30 | +| 状态 | 评审中 v1.1 | +| 版本 | 1.1 | +| 最后修改 | 2026-11-09 | | 对应实现 | mcpp >= 2026.10.30.1 | | 相关设计文档 | `.agents/docs/2026-10-29-ecosystem-target-sets-packaging-and-editors-design.md`(§3) | | 相关 issue | mcpp#847 | @@ -85,10 +85,10 @@ ## 4. 测试归档 - **写**:`mcpp test [--target ...] --no-run --archive ` 构建各腿的测试程序,与清单 `mcpp-tests.json`(每个测试的腿、 - 参数、工作目录)一起写为一个 tar 归档。归档是引擎的格式,与平台的容器无关。压缩方式由文件名决定 + 参数、工作目录、所实现的测试协议与其自身的文件数)一起写为一个 tar 归档。测试协议按构建时的规则确定一次,写入清单。归档是引擎的格式,与平台的容器无关。压缩方式由文件名决定 (`.tar.gz`/`.tgz`、`.tar.zst`、`.tar.xz`、`.tar`),读写同一规则;其他名称在构建前拒绝(退出码 2)。 -- **读**:`mcpp test --archive ` 在同一项目的检出中运行归档内本机能到达的测试,不构建;工作目录为项目检出;报告与 - `mcpp test` 相同。 +- **读**:`mcpp test --archive ` 在同一项目的检出中运行归档内本机能到达的测试,不构建;工作目录为项目检出;归档的测试由 `mcpp test` 自身的运行器运行: + 测试协议、按用例所在文件分进程、工作线程与 mtest 存储与构建所得的测试**必须**相同。 - 读取时:有测试失败则退出码 1;一个测试都没有运行则退出码 1 并写明;本机到达不了的腿报告为"未运行"。 - 原生交叉同样适用:在 Linux 上构建的 `x86_64-windows-gnu` 测试可以在 Windows 上运行。 @@ -121,3 +121,4 @@ | 版本 | 日期 | 变更 | |---|---|---| | 1.0 | 2026-10-30 | 首版(mcpp 2026.10.30.1,设计 2026-10-29 §3):集合的识别、补全与展开,一条命令的腿,`build`、`run`、`test`、`self env` 对多条腿的语义,串行规划与一起构建,测试归档,机器输出;判据 1–8(e2e 963、964,unit `TargetSet`、`BuildLegs`)。 | +| 1.1 | 2026-11-09 | mcpp 2026.11.9.1:归档清单记录每个测试的协议与自身文件数;读取时由 `mcpp test` 的运行器运行(协议、分进程、工作线程、存储)。 | diff --git a/docs/specs/target-side.md b/docs/specs/target-side.md index d5d7a232e..af25370bc 100644 --- a/docs/specs/target-side.md +++ b/docs/specs/target-side.md @@ -4,8 +4,8 @@ |---|---| | 规范编号 | SPEC-002 | | 标题 | 目标侧模型与能力声明(`mcpp:` 保留命名空间) | -| 状态 | 评审中 v1.3 | -| 最后修改 | 2026-10-26 | +| 状态 | 评审中 v1.5 | +| 最后修改 | 2026-11-09 | | 对应实现 | mcpp >= 2026.8.24.2;§2.2 的对象形式与 §7 mcpp >= 2026.10.26.1 | | 相关设计文档 | `.agents/docs/2026-08-24-target-side-design.md`、`.agents/docs/2026-10-23-target-base-and-execution-host-design.md`、`.agents/docs/2026-10-26-control-contract-runtime-and-ide-ecosystem-design.md` §6 | | 使用文档 | [docs/22 - 目标侧](../zh/22-target-side.md) | @@ -171,6 +171,20 @@ mcpp:<层名>[=<实现名>] 规范化会把 `x86_64-linux` 写成 `x86_64-linux-gnu`,因为身份必须是全的。 请求**必须**在规范化之前捕获;报告**应当**显示工程书写的拼写。 +段缺席而 `c-abi` 来自图时,身份取实际 C 库的行(`x86_64-linux` 依赖 musl 时为 +`x86_64-linux-musl`),输出目录随之(SPEC-016 §4.5;mcpp >= 2026.11.9.1)。 + +### 3.6 能力的缺省提供者 已实现(mcpp >= 2026.11.9.1) + +层的机制推广到普通包:包以 `requires` 声明所需能力,以 `[defaults."<能力>"] <选择器> = <包>` 声明缺省 +提供者。解析顺序为依赖图 > 平台 > 缺省:图中已有包提供该能力(使用者的选择,或平台为其层加入的包)时 +**禁止**加入缺省;否则取与目标匹配的选择器的缺省,`*` 在其余选择器之后。缺省提供者是声明它的包的依赖。 + +### 3.7 C 环境的声明 已实现(mcpp >= 2026.11.9.1) + +hosted 与否是 C 环境的一个声明:提供 `hosted-standard-library` 的 sysroot、`c-abi` 供给者或 `hosted = true` +的提供者使裸机行不加 `-ffreestanding`,`main` 不按 freestanding 处理。 + ### 3.5 目标表的约定何时生效 已实现 目标行的 `pin` 命名的是**供给该目标 C 库的载荷**,不是偏好的编译器。 @@ -338,4 +352,5 @@ mcpp:<层名>[=<实现名>] | v1.1 | 2026-09-26 | 规则三增补:`c-abi` 来自图时,链接不搜索宿主库目录(ELF 已实现,mcpp#696)。 | | v1.3 | 2026-10-26 | 随 mcpp 2026.10.26.1:§2.2 增加 `requires` 的对象形式与 `visibility`、`[target.<选择器>] requires`;新增 §7 一个进程一套 C++ 运行时(U/I/H、孤岛条件、外来运行时按类型识别、依赖图供给的 libstdc++)(mcpp#838)。 | | v1.4 | 2026-11-03 | 随 mcpp 2026.11.3.1:§2.2 选择器引用层的 `requires` 在层解析之后检查(此前未检查即被丢弃)。 | +| v1.5 | 2026-11-09 | 随 mcpp 2026.11.9.1:§3.4 身份随 C 库;新增 §3.6 能力的缺省提供者、§3.7 C 环境的声明(mcpp#911 G3、G7)。 | | v1.2 | 2026-10-23 | 随 mcpp 2026.10.24.1:§1.3 `prebuilt` 放宽为被点名、按内容识别的预制目录;§2.4 改为由测量回答、`provides` 声明或核对,标为已实现;规则一增加基座是一个供给者与 c-abi 独占律;规则二增加基座上的编译器运行库与可测量的下限;规则三增加可叠加层;新增 §6 目标侧执行(组合、运行器契约、物理边界)。 | diff --git a/docs/specs/targets-and-providers.md b/docs/specs/targets-and-providers.md new file mode 100644 index 000000000..9cab7479c --- /dev/null +++ b/docs/specs/targets-and-providers.md @@ -0,0 +1,124 @@ +# SPEC-016:目标与提供者 + +| 项 | 值 | +|---|---| +| 规范编号 | SPEC-016 | +| 标题 | 目标与提供者:行、提供者、选择、平台定义的行、报告与锁 | +| 状态 | 评审中 v1.0 | +| 版本 | 1.0 | +| 最后修改 | 2026-11-09 | +| 对应实现 | mcpp >= 2026.11.9.1 | +| 相关设计文档 | `.agents/docs/2026-11-09-target-model-design.md` | +| 相关 issue | mcpp#911 | +| 相关规范 | [SPEC-001](package-identity.md)(引用文法)、[SPEC-002](target-side.md)(层与能力)、[SPEC-013](platforms.md)(平台的语句)、[SPEC-014](packaging.md)(打包)、[SPEC-015](target-sets.md)(目标集合) | +| 使用文档 | [21 —— 目标三元组](../zh/21-the-target-triple.md)、[35 —— 平台](../zh/35-platforms.md) | + +用语按 RFC 2119:**必须 / 禁止**为强制,**应当**为建议,**可以**为可选。 + +## 0. 范围 + +本规范规定一次构建的目标如何确定:用户写出的**请求**、构建所用的**身份**、为身份提供工具链与目标侧各层的**提供者**,以及由 +请求得到身份与提供者的唯一规则。它同时规定平台包如何定义新的身份与提供者,以及选择的结果如何报告、记录与锁定。 + +## 1. 术语 + +| 术语 | 定义 | +|---|---| +| 请求 | `--target`、`[build] target`、config 的缺省目标中写出的字符串 | +| 身份 | 一个行的三元组;决定输出目录、缓存键、`cfg` 求值与锁中的记录 | +| 行 | 身份的数据:段、档位、机器描述、对象格式、执行系统等(§2) | +| 提供者 | 一个身份的一份完整供给:工具链、各层、链接、C 环境(§3) | +| 选择 | 由请求得到身份与提供者的规则(§4) | + +## 2. 行 + +### 2.1 字段 + +行的字段:`triple`(`-[-]`)、`tier`(verified、preview、planned)、`standard`、`exact`、`march`、`mabi`、 +`code-model`、`multilib`、`flags`、`link-emulation`、`clang-triple`、`object-format`、`family`、`data-model`、`exe-suffix`、 +`shared-suffix`、`loader`、`execution`、`descriptor-keys`。 + +- 一个系统可能用到两种以上 C 库时,身份**必须**含 env;env 即 C 库。业界原生标识无 env 段的系统,其原生身份为两段 + (`aarch64-macos`),其余 C 库以 env 区分(`aarch64-macos-musl`)。 +- 身份与宿主无关:同一身份在任何宿主上是同一输出目录、缓存键与 `cfg`。 +- `exact = true` 的行只由写出其全部段的请求得到(Android 的行)。 +- `march`、`mabi`、`code-model`、`flags`、`link-emulation` 是行的机器描述,引擎原样使用,**不可**被替换;机器不同的目标以 + 另一行表达(§5)。 + +### 2.2 内置行 + +内置行是 mcpp 源码树中的数据(`modules/toolchain-model/data/builtin.toml`),由该包的构建程序编入引擎;运行时不读取文件, +不联网。行的顺序是数据:同一系统中标准身份在前。 + +## 3. 提供者 + +| 键 | 语义 | +|---|---| +| `name` | 提供者名;内置提供者以 `builtin:` 指名 | +| `serves` | 所服务的身份;逐段匹配的模式,`*` 代表任一段(`*-linux-musl`) | +| `priority` | 整数,越大越先;内置数据给出缺省值 | +| `hosts` | 提供者在其上可得的宿主(`-`,`*` 与 `{arch}` 为模式) | +| `pairs` | 可与之配对的编译器族;声明了编译器族的构建只取与之配对的提供者 | +| `toolchain`、`sysroot`、`linkage`、`layers`、`link` | 同 SPEC-013 §1 | +| `hosted` | C 环境是否 hosted | +| `execution` | 产物运行的系统 | + +- 引擎不含提供者的类别,也不含实现名;所有提供者同一结构,只有数据不同。 +- 内置提供者:glibc 载荷、musl-gcc、MinGW-w64(与 gcc、llvm 配对)、MSVC、Apple SDK、Android NDK、Emscripten SDK、裸机、 + openkal-platform(ok 映像,及 `*-linux-musl`、`*-windows-musl`、`*-macos-musl` 的 hosted 部分)。 +- 与声明的编译器族配对的提供者提供系统:例如 llvm 与 MinGW-w64 配对时,该提供者的载荷是 clang 的 sysroot、C 库与 C++ 运行时 + (libstdc++)。 + +## 4. 选择 + +1. **显式者优先。** `--platform`、`MCPP_TARGET__PLATFORM`、项目与 config 的 `platform`(含 `builtin:`)只保留所指的 + 提供者;`--platform none` 不用平台包实现的提供者;声明的编译器族只保留与之配对的提供者。 +2. **请求是模式。** 写出 env 的请求只匹配该行;只写 `-` 的请求匹配该系统的全部行(`exact` 的行除外)。 +3. **顺序是数据。** 候选(行与服务它的提供者)按 `priority` 从大到小,相同者按行的顺序,再按提供者名排序。 + `[target..priority] = ` 改写所匹配行上的优先级;项目先于 config。 +4. **取第一个可得者。** 提供者服务该行、在本宿主可得(`hosts`)、探测成立(MSVC)、与声明的编译器族配对,即为可得。皆不可得 + 时取第一个候选,由缺少之处拒绝,拒绝写出各候选及其不可得的原因(`host-cannot-serve`)。 +5. **身份随 C 库。** 依赖图提供 C 库(`mcpp:c-abi`)而请求未写 env 时,身份取实际 C 库的行,重新准备一次。写出的 env 是项目的 + 拼写,不被替换;与图不一致时报告(目标报告的 `asks for the … C ABI`),不拒绝。 + +## 5. 平台定义的行与提供者 + +平台包以 `[[platform.define]]` 定义行(§2.1 的字段),以 `[[platform.provider]]` 声明提供者(§3 的键);`[platform]` 与 +`[platform.hosted]` 是两种简写。 + +- 已解析平台定义的 os 与 env 为解析器接受的词;`cfg(os = …)` 按定义的行求值。 +- 读取平台清单时按行检查其语句:与内置行同名的定义、同一行的两次定义**必须**被拒绝;不服务任何行的提供者、以同一优先级 + 服务同一身份的两个提供者为警告,按清单警告的规则只在平台自身的构建中给出。 +- 平台定义的行不取宿主载荷的 C 库:除非平台或项目声明 sysroot,该行没有 C 库。 +- 首次遇到只在索引中的平台时,请求以临时行进入第一次准备,平台取得后重新准备。 + +## 6. C 环境、链接与执行 + +- **hosted。** 声明 `hosted-standard-library` 的 sysroot,或 `hosted = true` 的提供者,使裸机行不加 `-ffreestanding`。 +- **链接驱动。** 平台的 `link.driver = "compiler"` 在每一行生效,包括行本身以 `link-emulation` 直接调用 ld.lld 的行。 +- **执行。** 行的 `execution` 不是本机系统时:构建不以本机加载器检查产物的运行时闭包;`mcpp run` 与 `mcpp test` 在无运行器、 + 无执行主机时报告「已构建,未运行」。运行器包是目标的工具(`[target..tools]`),以 docs/41 的运行协议启动。 + +## 7. 报告、机器输出与锁 + +- 状态行:`Target <请求> → <身份>`;回落时附原因。 +- `mcpp why target [--format json]` 写出请求、身份、提供者、优先级、回落原因与按序的候选;`mcpp why flags []` 写出每个 + 参数及其来源层(行、平台、工具链、包、引擎);`mcpp why platform` 写出平台定义的行与所服务的身份,各附提供者与优先级 + (根包是平台包时为其自身,否则为目标所选的平台)。 +- 构建记录与 `plan` 事件的 `configuration.target` 为 `{request, identity, provider, priority, fallback}`;本机构建为 `null`。 +- 构建程序读 `MCPP_TARGET_PROVIDER`(`mcpp::target_provider()`)。 +- `mcpp.lock` 以 `[target.""] identity, provider` 记录每个请求得到的身份;`--locked` 下选择另一身份的构建被拒绝, + 并写出锁中的与本次的身份。 + +## 8. 兼容性 + +- 已有身份字符串、输出目录与 `cfg` 结果不变;`[target.X]`、`descriptor_keys`、`[platform.hosted]` 照旧可读。 +- 行为变化:只写 `-` 的请求在本宿主不可得其标准身份时取下一个可得的身份(无 MSVC 的宿主上 `x86_64-windows` 为 + `x86_64-windows-musl`;非 macOS 宿主上 `aarch64-macos` 为 `aarch64-macos-musl`;x86_64 宿主上 `aarch64-linux` 为 + `aarch64-linux-musl`);依赖图提供 musl 而请求未写 env 时身份为 `-musl`。 + +## 变更记录 + +| 版本 | 日期 | 变更 | +|---|---|---| +| 1.0 | 2026-11-09 | 初版:行、提供者、选择、平台定义的行、C 环境、执行、报告与锁 | diff --git a/docs/zh/04-mcpp-toml.md b/docs/zh/04-mcpp-toml.md index c2df23c3f..588411ff4 100644 --- a/docs/zh/04-mcpp-toml.md +++ b/docs/zh/04-mcpp-toml.md @@ -478,6 +478,7 @@ required_features = ["gui"] # only built when feature `gui` is |---|---| | `defines` | 预处理宏(`name` 或 `name=value`);在 C 与 C++ 两种入口编译上都脱糖为 `-D`。 | | `cxxflags` / `cflags` | 这个目标专用的额外编译旗标。**不要**把 `-std=...` 写在这里——用 `[package].standard`。 | +| `build` *(2026.11.9.1+)* | 一个表 `[targets..build]`,含 `defines`、`cxxflags`、`cflags`:本包中该目标链接的每个翻译单元的参数,即其入口与共享的实现源文件。与包的参数不同处,这些单元编译为该目标自己的变体(`obj/_variant//`);其他目标照旧链接包的目标文件。模块接口每包一个,不做变体。 | | `required_features` | 只有当构建中**每一个**列出的 feature 都被激活时,这个目标才会被产出;否则被静默跳过。它只是一道闸——不会激活 feature(用 `--features` / `[features].default`)。**一个例外,但它不是第二条规则:** 当这个目标作为 host 工具被请求时(`tools = [...]`,§2.14),这个目标就是被**请求**的那一个,于是它的 `required_features` 变成子构建的**输入**。同一个字段、同一个含义——只是解析的方向反过来了。 | | `windows_subsystem` *(2026.9.12.2+)* | 可执行文件的 PE subsystem:`"console"`(默认)或 `"windows"`,一个启动时没有控制台的 GUI 程序。只到达这个目标的链接,别处不受影响,在非 PE 的目标上不渲染任何东西。见上一节。 | | `windows_entry` *(2026.9.12.2+)* | 程序定义的入口函数:`"main"`(默认)、`"wmain"`、`"WinMain"` 或 `"wWinMain"`。见上一节。入口目标陈述其翻译单元定义的函数,由选择它的程序继承。 | @@ -1970,6 +1971,9 @@ discover = ["tests/**/*.cpp"] # the default | `main` | `{ from = "<依赖>/<入口>" }` | 每个测试程序的起点所在的入口目标;测试文件随之成为程序的普通翻译单元,不定义 `main`(§2.2,`kind = "entry"`) *(2026.10.26.2+)* | | `start` | 符号 | 独立目标上每个测试镜像的入口符号(§2.2,`start`) *(2026.10.26.2+)* | | `programs` | `"file"` 或 `"directory"` | `"file"`(默认):每个被发现的文件是一个程序;`"directory"`:glob 固定目录之下同一目录的文件是一个程序,以目录命名,有文件定义 `main` 时以它为起点 *(2026.11.8.1+)* | +| `split` | `"file"`、`"case"` 或 `"none"` | 使用测试协议的程序的运行方式:按用例所在文件各一个进程(默认)、每个用例一个进程,或只运行一次 *(2026.11.9.1+)* | +| `link` | `"used"` 或 `"objects"` | `"used"`(默认):测试只链接它用到的部分(`--gc-sections`、`-dead_strip`);`"objects"`:保留每个目标文件的全部内容 *(2026.11.9.1+)* | +| `store` | 目录或 `"none"` | 运行以 mtest 存储的形式保存的位置,相对项目;默认 `target/mtest`;`"none"` 不保存 *(2026.11.9.1+)* | 这些 glob 使用与 `[build] sources` 相同的词汇。一个测试的名字,是它 相对第一条匹配到它的 glob 所在的固定目录的路径,去掉扩展名。两个 diff --git a/docs/zh/05-dependencies.md b/docs/zh/05-dependencies.md index 249b29ecf..7c83a09e6 100644 --- a/docs/zh/05-dependencies.md +++ b/docs/zh/05-dependencies.md @@ -62,8 +62,8 @@ mcpp 把它读作这些点所拼写的选择器,警告该拼法已弃用(自 ```toml [dev-dependencies] -mcpplibs.mtest = "0.4.0" -"mcpplibs.mtest.gtest" = "0.4.0" +mcpplibs.mtest = "0.5.2" +"mcpplibs.mtest.gtest" = "0.5.2" ``` ```toml @@ -73,7 +73,7 @@ mylib = { path = "../mylib" } ``` ```toml -# Git dependency — pick exactly one of tag / branch / rev +# Git dependency — tag / branch / rev 至多一个;都不写即远端的缺省分支 [dependencies] mylib = { git = "https://github.com/user/mylib.git", tag = "v1.0.0" } applib = { git = "https://github.com/user/applib.git", branch = "develop" } @@ -94,6 +94,10 @@ widget = { version = "1.0", backend = "glfw_opengl3" } # Sugar for: features=[" 构建记录相同的解析结果时,`mcpp.lock` 保留原内容与修改时间(2026.10.21.1+)。 仅 LF 与 CRLF 行结束形式不同不会触发改写。这不跳过依赖解析,也不改变锁定范围。 +不写 `tag`、`branch`、`rev` 的 git 依赖跟随远端的缺省分支,分支名取远端的通告(`main`、`master`、`trunk` 等; +2026.11.9.1+),此后按 `branch` 处理并以 commit 锁定。命令行上的平台与工具使用相同的字符串形式: +`--platform git+https://example.org/acme/acme-platform.git#tag=v1`。 + **Git 依赖与 `mcpp.lock`**:`tag` 和 `rev` 已经指向历史中的一个固定点,而 `branch` 是会移动的。首次构建把分支解析成一个 commit 并写进 `mcpp.lock`, 此后每次构建都重新构建**那个** commit —— lock 是权威来源而不是缓存提示, @@ -403,6 +407,11 @@ gtest = "1.15.2" 错误),`mcpp test ` 与 `--message-format json` 分别提供过滤与 机器可读输出。 +**正在构建的包在其自身的依赖图中代表它自己**(2026.11.9.1+)。经包的 `[dev-dependencies]` 到达的依赖可以指名正在构建的包:用一个本身使用 +该库的测试框架测试该库(mtest 下的 `mcpplibs.os`)。这条边即该包自身,无论它要求哪个版本,未要求的版本会被说明; +第二份副本会在一个程序中把包的每个模块提供两次,而受测的是此处的这个包。根的 `[dev-dependencies]` 只服务它的测试, +因此框架到达该库而不形成包的环。 + ## `[build-dependencies]` —— 构建期依赖(mcpp 2026.8.29.1+) ```toml @@ -575,6 +584,20 @@ updater = { path = "../updater", artifacts = ["updater"] } - 应用器是 mcpp 自己的,而且是精确的:一个 hunk 只在其头部所写的位置应用,每一行上下文与每一行 删除都必须逐字节相符,否则构建被拒绝,并指出补丁、文件、hunk 与行号。不在别处查找,不转换换行。 补丁中的路径带一级前导目录(`a/`、`b/`),应用时去掉。改名、复制与二进制改动被拒绝。 +- 补丁中的文件以**包根**为起点:包解析得到的清单在归档中所在的目录(描述符写 + `mcpp = "*/run/mcpp.toml"` 时为归档中的 `run/`),因而布局相同的各版本可共用一个补丁。边上的 + `patch_root = ""` 另指一个目录,相对包根,且在包的源码之内。2026.11.9 之前以归档顶层为起点写的 + 补丁按原写法应用,构建给出说明。 +- `mcpp why deps` 报告每个依赖在构建中所读的包根(已应用其补丁)。补丁即 git 对该目录树的一份副本写出的差异: + + ```sh + cp -R <包根> /tmp/zlib && cd /tmp/zlib + git init -q && git add -A && git commit -qm base + # 修改之后 + git diff > <项目>/patches/0001-fix.patch + ``` + + 并由边的 `patches` 列出。再一个补丁以同样方式从打补丁后的构建所报告的包根写出,列在前者之后。 - 版本依赖与 git 依赖可以带补丁;`path` 依赖是使用者自己拥有的源码树,带补丁时被拒绝。 - `mcpp.lock` 记录变体,以及每个补丁的路径与 SHA-256;`mcpp emit sbom` 把它们写作组件的 pedigree。 - 一个程序只持有一个包的一个变体。同一配置中的两个程序(工作区的成员)可以各用自己的变体;一个 diff --git a/docs/zh/08-testing.md b/docs/zh/08-testing.md index 5668a216b..6038bebe6 100644 --- a/docs/zh/08-testing.md +++ b/docs/zh/08-testing.md @@ -54,9 +54,28 @@ discover = ["checks/**/*.cpp", "!checks/fixtures/**"] 一个程序,以目录命名(`tests/unit/*.cpp` 为程序 `unit`):每个文件是它的一个翻译单元, 它的用例是这些文件的全部用例;有文件定义 `main` 时以该文件为起点,否则按不定义 `main` 的文件的规则开始。同一目录中两个定义 `main` 的文件会被拒绝并点出两者;直接位于固定 -目录中的文件仍各自是一个程序。多文件的程序只链接一次,而每文件一个程序时每个文件链接 +目录中的文件仍各自是一个程序。测试的名字是其程序的路径,因此一个名字是另一个名字的目录 +(`unit` 与 `unit/own`)时被拒绝并点出两者(2026.11.9.1+)。多文件的程序只链接一次,而每文件一个程序时每个文件链接 一次,后者正是由许多小文件组成的套件在构建上花费的时间。 +使用测试协议的程序**按其用例所在文件各起一个进程**运行(2026.11.9.1+):mcpp 先列出多文件程序的用例, +每个文件的那次运行只选取自己的用例(P4),像一个程序一样在各工作线程间调度,名为 +`<程序>/<文件>`——兼得每文件一个程序的隔离与并行,而只链接一次。`[test] split = "case"` +使每个用例一个进程,`"none"` 只运行一次。模式按运行所用的名字指名程序或其一部分:`mcpp test unit/test_home`(或 `home`) +运行该文件的那一部分。 + +测试程序只保留它用到的部分(2026.11.9.1+):各单元以每函数、每数据对象一个节编译, +测试以 `--gc-sections` 链接(Mach-O 为 `-dead_strip`),测某一模块的测试不再带上整个包 +的全部函数(mcpp 自身的单元测试:每个由 32 MB 降至 14 MB)。静态构造是回收的根。 +`[test] link = "objects"` 保留每个目标文件的全部内容。 + +一次运行以 mtest 存储的形式保存(2026.11.9.1+):`target/mtest/` 中有 `runs//run.json`, +`programs.ndjson` 每个进程一行(状态、用时、开始时刻、工作线程与所选用例),`cases.ndjson` +每个用例一行,`index.ndjson` 列出各次运行。`mtest store import --from target/mtest` 把它们取入 mtest 的存储, +`[project] store` 为 `target/mtest` 的 `mtest.toml` 则就地读取;`mtest report`、`analyze` 与 `graph` 随之像读取 +mtest 自身的运行一样读取它们(Markdown 报告、最慢的用例、关键路径与调度)。在 CI 中 `commit` 与 `group` 取 +`GITHUB_SHA` 与 `GITHUB_RUN_ID`。`[test] store = ""` 另指一个目录,相对项目;`"none"` 不保存。 + 由自身多个源文件编译而成的测试套件也可以是一个包:一个工作区成员,它的 `[build] sources` 承载整套套件,其唯一的测试程序驱动套件,用 `mcpp test -p ` 选中。 @@ -231,8 +250,9 @@ mcpp test --target openkal --no-run --archive tests.tar.gz # 构建各腿的 mcpp test --archive tests.tar.gz # 在检出中运行本机能到达的部分 ``` -归档含各腿的测试程序与 `mcpp-tests.json`(每个测试的腿、参数与工作目录)。读取 -时不构建;测试在检出中经本机的运行器或系统运行,报告与 `mcpp test` 相同。有测试 +归档含各腿的测试程序与 `mcpp-tests.json`(每个测试的腿、参数、工作目录与所实现的测试协议)。读取 +时不构建;测试在检出中经本机的运行器或系统,由 `mcpp test` 自身的运行器运行:用例、按用例所在文件分进程、 +工作线程与存储(2026.11.9.1+)。有测试 失败时退出码为 1,一个测试都没运行的归档也是 1。本机到达不了的腿报告为未运行 ([SPEC-015 §4](../specs/target-sets.md))。归档是 tar,压缩方式由文件名决定:`.tar.gz`(或 `.tgz`)、`.tar.zst`、`.tar.xz` 或 `.tar`;其他名称在构建前即被拒绝。 @@ -259,7 +279,8 @@ main = "src/main.cpp" test-protocol = 1 ``` -程序文件中带有标记 `mtest-protocol=1` 时同样如此,以 mtest 的库构建的程序即带有该标记。 +程序文件中带有标记 `mtest-protocol=1` 时同样如此,以 mtest 的库构建的程序即带有该标记;只保留可达部分的测试, +其链接所用的目标文件带有该标记也算在内,因此无人引用的标记(0.5.1 之前的 mtest)不会在链接时丢失。 带有标记却没有写出 `program` 事件的程序没有使用协议:与框架的库链接在一起的普通 `main` 文件是一个用例,按退出状态判定,运行与列出时都是如此。 mcpp 随之把协议的变量(`MTEST_PROTOCOL`、`MTEST_MODE`、`MTEST_REPORT`、`MTEST_TMPDIR`, diff --git a/docs/zh/09-commands-by-scenario.md b/docs/zh/09-commands-by-scenario.md index ed8d3fab9..834408f35 100644 --- a/docs/zh/09-commands-by-scenario.md +++ b/docs/zh/09-commands-by-scenario.md @@ -185,17 +185,21 @@ dependency graph: requested by huxdemo.comp@0.1.0 as 'fw' in [dependencies] linked static (default) instances: target + root: /work/fw ``` -`instances` 说明这个包是为目标编译、为构建程序编译,还是两者都有(2026.10.11.1+)。 +`root` 是构建所读的包根,打了补丁的依赖为其变体的包根(2026.11.9.1+;docs/05)。`instances` 说明这个包是为目标编译、为构建程序编译,还是两者都有(2026.10.11.1+)。 `mcpp why build-programs` 列出构建程序图:每个程序、它使用的库、编译所依据的身份与其二进制。 `mcpp why compat` 列出每次使用弃用读法的位置与移除它的版本(51 —— 支持的版本)。 同一张图记录在 `target///resolution.json` 的 `graph` 下, 每个包一条,根在最前:`package`(规范身份、命名空间、名字、版本、来源)、 -`root`、`requested_by`(`requester`、`key`、`table`),对库还有 `link` +`root`、`requested_by`(`requester`、`key`、`table`)、`manifest_dir`(包根),对库还有 `link` (`form`、`reason`)。 +`mcpp why target` 按序列出请求的候选(docs/21),`mcpp why flags []` 列出单元命令行的每个参数及给出它的层, +`mcpp why platform` 列出平台定义的行与所服务的身份(docs/35);后两者不属于不给话题时的报告(2026.11.9.1+)。 + 话题是 `toolchain`、`runtime`、`deps` 或 `runners`,不给话题时四者全报。 `--target` 与 `--toolchain` 把报告变成对当前目录并不使用的那一对的查询, 一个目标矩阵正是这样逐格提问的。 @@ -629,7 +633,7 @@ side_effect = false ## 当前边界 -- `mcpp why --format json` 对 `toolchain`、`sources`、`tool`、`payload`、`build-programs` 与 `compat` 话题有定义。其余话题报告 `'' has no machine-readable shape yet` 并以非零退出。 +- `mcpp why --format json` 对 `toolchain`、`sources`、`tool`、`payload`、`build-programs`、`compat`、`target`、`flags` 与 `platform` 话题有定义。其余话题报告 `'' has no machine-readable shape yet` 并以非零退出。 - `mcpp search` 按子串匹配;没有字段选择器,也没有把搜索限定到单个命名 空间的办法。 - `mcpp clean --stale` 读 `target/.build_cache`,它保存的近期条目数量 diff --git a/docs/zh/20-toolchains.md b/docs/zh/20-toolchains.md index a7dc2e063..c52afa4fc 100644 --- a/docs/zh/20-toolchains.md +++ b/docs/zh/20-toolchains.md @@ -285,6 +285,13 @@ OS: 归档顶着 MSVC 的名字,MSVC 拿不去用。若有脚本按 `*.lib` 收集 `windows-gnu` 构建的产物,现在要改成 `*.a`。 + +### clang 配 MinGW-w64 载荷(2026.11.9.1+) + +MinGW-w64 提供者与 gcc、llvm 配对。声明 llvm 时(`--toolchain llvm@` 或 `[target.x86_64-windows-gnu] toolchain`), +该载荷按 gcc 的方式安装,并成为 clang 的 sysroot:其 C 库、libstdc++(`bits/std.cc` 即 std 模块,`import std` 可用)与 GCC +运行库,由 lld 链接。构建报告 `Pairs clang ... with the gcc@ payload`。 + ## Windows 上产出 Linux ELF(`x86_64-linux-musl`,无需 WSL) 上一节的镜像:一台 Windows 机器直接产出**完全静态的 Linux 二进制**,不需要 diff --git a/docs/zh/21-the-target-triple.md b/docs/zh/21-the-target-triple.md index b8a0f3f1c..61fd58e16 100644 --- a/docs/zh/21-the-target-triple.md +++ b/docs/zh/21-the-target-triple.md @@ -134,36 +134,33 @@ target spec 设置 `exe_suffix: ".js"`。 的 emsdk payload,需要先经过[第 20 章](20-toolchains.md)所述的那次索引 刷新,`mcpp run` 才能找到 runner。 -## 省略第三段 +## 省略第三段:请求只写系统(2026.11.9.1+) -`-` 在每个平台上都是一个完整的目标: +写全的目标是**身份**:输出目录、缓存键与 `cfg()` 判断的对象。只写 `-` 的目标是**请求**:它包含该系统的 +全部身份,构建取第一个其**提供者**(服务它的工具链与目标侧)在本机可得的身份(SPEC-016 §4)。 ```bash -mcpp build --target x86_64-linux # = x86_64-linux-gnu -mcpp build --target x86_64-windows # = x86_64-windows-gnu +mcpp build --target x86_64-linux # Linux 上为 x86_64-linux-gnu;无 glibc 载荷处为 -musl +mcpp build --target x86_64-windows # 装有 MSVC 时为 -msvc,否则为 -musl(openkal-platform) +mcpp build --target aarch64-macos # macOS 上为 Apple 身份,否则为 aarch64-macos-musl mcpp build --target riscv64-none # = riscv64-none-elf -mcpp build --target aarch64-linux # = aarch64-linux-gnu -mcpp build --target aarch64-macos # macOS has no segment to decline ``` -省略它不改变任何身份。输出目录、缓存键,以及 `cfg()` 谓词的对象,都取规范 -形式,因此两种拼法共用同一个指纹,第二次构建是缓存命中,而不是又一次 -完整构建。 +候选按提供者的优先级排序(引擎附带的数据:各系统自己的工具链与 C 库在前,openkal-platform 其次,其他约定在后), +再按行的顺序。报告写出所取的身份,第一个候选在本机不可得时附其原因: -不同的是**请求被记录成了什么**。三元组既充当身份 —— 身份必须完整 —— -又充当请求 —— 请求必须能够什么都不说;mcpp 两者都保留:为身份填上那一 -段,同时记住这次填充只是一次填充。 - -### 补全取自词汇表,而不是取自一个固定的词 - -省略环境段的请求按已知目标表补全。受支持的词法默认值优先;否则采用 -唯一受支持的同族行。没有受支持的行时保留词法形式,并在诊断中列出 -已登记的同族行。歧义请求列出受支持的候选。 +```text + Target x86_64-windows → x86_64-windows-musl + msvc is not available on linux-x86_64 +``` -`aarch64-linux` 选择 `aarch64-linux-gnu`。原生 Linux ARM64 载荷采用 -LLVM 23.1.3 与 glibc。`aarch64-linux-musl` 仍是静态 musl 产物的显式 -拼法。显式写出的环境段保持不变。 +`mcpp why target [--target ]` 按序列出候选、优先级与可得性(脚本用 `--format json`)。工程以 +`[target.<选择器>.priority] <提供者> = ` 改变顺序,或以 `platform = "builtin:<名>"` 指名一个内置提供者。写全的身份 +从不被替换:`--target x86_64-linux-gnu` 构建该身份,或被拒绝并写出所缺之物。`mcpp.lock` 记录每个请求得到的身份, +`--locked` 拒绝选到另一身份的构建。 +依赖图提供 C 库(`mcpp:c-abi`)而请求未写 C 库时,身份取该 C 库:依赖 musl 的 `x86_64-linux` 构建为 +`x86_64-linux-musl`。写出而与图矛盾的段被报告并保留。 ### 采用的拼法 @@ -490,10 +487,13 @@ docs/22。 | `x86_64-windows-msvc` | verified | — | — | — | — | 系统 | | `aarch64-macos` | verified | — | — | — | SDK | — | | `x86_64-macos` | planned | — | planned | planned | planned | planned | +| `aarch64-macos-musl` | preview | `llvm@23.1.3` | 图 | 图 | 图 | 图 | +| `x86_64-macos-musl` | preview | `llvm@23.1.3` | 图 | 图 | 图 | 图 | | `riscv64-none-elf` | verified | `llvm@23.1.3` | 载荷 | 载荷 | 载荷 | 载荷 | | `riscv32-none-elf` | verified | `llvm@23.1.3` | 载荷 | 载荷 | 载荷 | 载荷 | | `aarch64-none-elf` | preview | `llvm@23.1.3` | 载荷 | 载荷 | 载荷 | 载荷 | | `x86_64-none-elf` | preview | `llvm@23.1.3` | 载荷 | 载荷 | 载荷 | 载荷 | +| `i686-none-elf` | preview | `llvm@23.1.3` | 载荷 | 载荷 | 载荷 | 载荷 | | `thumbv6m-none-eabi` | verified | `llvm@23.1.3` | 载荷 | 载荷 | 载荷 | 载荷 | | `thumbv7m-none-eabi` | verified | `llvm@23.1.3` | 载荷 | 载荷 | 载荷 | 载荷 | | `thumbv7em-none-eabi` | preview | `llvm@23.1.3` | 载荷 | 载荷 | 载荷 | 载荷 | diff --git a/docs/zh/22-target-side.md b/docs/zh/22-target-side.md index 242b863bf..b72ee5e2d 100644 --- a/docs/zh/22-target-side.md +++ b/docs/zh/22-target-side.md @@ -271,6 +271,24 @@ requires = ["mcpp:c++-abi=libstdc++"] 另一种变体构建进同一个存储目录,否则第一个消费者就会替之后所有消费者决定 变体。 + +### 缺省提供者(2026.11.9.1+) + +需要某能力的包为每个目标陈述在无人提供时所取的提供者(SPEC-002 §3.6): + +```toml +[package] +requires = ["mcpp:kernel-abi=openkal"] + +[defaults."mcpp:kernel-abi=openkal"] +linux = "openkal-linux@^0.25" +macos = "openkal-macos@^0.25" +"*" = "openkal-posix@^0.25" +``` + +依赖图中的提供者——使用者的依赖,或平台为其层加入的包——优先;否则与目标匹配的选择器的缺省作为该包的依赖进入 +依赖图,构建报告之(`Default ... for ... of ...`)。指名目标的选择器先于 `*`。 + ### 表形式与按目标的 `requires`(mcpp 2026.10.26.1+) `requires` 的条目可以是一张表: diff --git a/docs/zh/30-build-mcpp.md b/docs/zh/30-build-mcpp.md index 5012d956c..ca1ab3e06 100644 --- a/docs/zh/30-build-mcpp.md +++ b/docs/zh/30-build-mcpp.md @@ -1147,6 +1147,7 @@ mcpp 会把它自己构建时用的**同一份** std 模块暂存过来,缓存 | `MCPP_TARGET_OS` *(0.0.100+)* | `mcpp::target_os()` | 目标的 OS 段(`linux`/`macos`/`windows`)——不必再手撕 `MCPP_TARGET` | | `MCPP_TARGET_ARCH` *(0.0.100+)* | `mcpp::target_arch()` | 目标的 arch 段(GNU 拼写:`x86_64`、`aarch64`…) | | `MCPP_TARGET_ENV` *(0.0.100+)* | `mcpp::target_env()` | 目标的 env 段(`gnu`/`musl`/`msvc`);三元组无 env 段(macOS)时为空串 | +| `MCPP_TARGET_PROVIDER` *(2026.11.9.1+)* | `mcpp::target_provider()` | 选择为目标身份给出的提供者(`openkal`、`musl-gcc`、`msvc` 等;SPEC-016 §4);宿主自身的构建为空串 | | `MCPP_HOST` | `mcpp::host()` | 宿主三元组 | | `MCPP_PROFILE` | `mcpp::profile()` | 生效 profile 名(`dev`/`release`/…) | | `MCPP_TOOLCHAIN_SYSROOT` *(2026.9.5.2+)* | `mcpp::toolchain_sysroot()` | mcpp 传给自己那个编译器的 `--sysroot`;不传时为空串。供运行**第二个**编译器的规则包使用 —— 见上文「驱动第二个编译器」 | diff --git a/docs/zh/35-platforms.md b/docs/zh/35-platforms.md index ff5140f99..e84d6e0d6 100644 --- a/docs/zh/35-platforms.md +++ b/docs/zh/35-platforms.md @@ -137,6 +137,10 @@ platform = "openkal-platform@0.24.0" Windows 上这一安排对应 GNU ABI 的行(`--target x86_64-windows-gnu`);`[platform.hosted] targets` 之外的 三元组被拒绝,消息给出它所服务的同系统同架构的行。 +图中的 C 库(`mcpp:c-abi`)位于 Mach-O 映像链接的最前(2026.11.9.1+)。该格式的加载器在入口之前按链接顺序运行 +映像的初始化函数,而 C 库由它自己的一个初始化函数启动;其目标文件在前,C 库便在程序的任何全局构造函数运行之前 +就绪,如同 ELF 上 C 运行时的启动目标文件位于程序之前。 + ## 版本组 `[platform.versions]` 为一个家族列出每个包的一个版本。从 mcpp 2026.11.7.1 起由版本组选择版本:平台是依赖图解析的 @@ -234,6 +238,34 @@ seal = [["okld", "note", "{}"], ["okld", "place", "{}"], ["okld", "check", "{} `mcpp test` 而不给 `--target` 时,以该包为平台为其每个 `targets` 构建测试,运行本机架构的那些与给出了执行宿主的 那些(2026.10.29.1+),其余报告为已构建、未运行。 +## 定义目标与提供者(2026.11.9.1+) + +平台可以服务内置行未命名的系统。`[[platform.define]]` 陈述行,`[[platform.provider]]` 陈述服务它的提供者: + +```toml +[[platform.define]] +triple = "x86_64-freebsd" +clang-triple = "x86_64-unknown-freebsd14.1" +object-format = "elf" +family = "unix" +execution = "freebsd" # 其程序运行的系统 + +[[platform.provider]] +serves = ["x86_64-freebsd"] # 模式:`*` 代表任一段 +priority = 100 +hosted = true # C 环境有 `main` +``` + +选中该平台(`[target.x86_64-freebsd] platform = {...}` 或 `--platform`)后,`--target x86_64-freebsd` 构建到 +`target/x86_64-freebsd/`,`cfg(os = "freebsd")` 成立;程序的系统不是本机时,在运行器陈述运行方式之前为「已构建、 +未运行」(docs/41)。行的机器描述属于平台:机器与内置行不同的目标是另一行。`[platform]` 与 `[platform.hosted]` 仍是 +提供者的两种简写。 + +`mcpp new <名> --template platform` 生成一个定义 `-<名>` 的平台包。读取平台清单时按行检查其语句:定义内置行、 +同一行定义两次是错误,无论平台在何处被读取;不服务任何行的提供者、以同一优先级服务同一身份的两个提供者是平台自身构建的 +警告。`mcpp why platform` 列出平台定义的行与所服务的身份,各附提供者与优先级:在平台目录中是其自身,在项目中是其目标所选 +的平台(`--target`;`--format json`)。 + ## 查看一个配置 ```sh diff --git a/docs/zh/41-devices.md b/docs/zh/41-devices.md index 76ab42c4e..d152a22b9 100644 --- a/docs/zh/41-devices.md +++ b/docs/zh/41-devices.md @@ -123,6 +123,23 @@ libfw.so/abs/project/target///bin/libfw.so 容器或远程主机——会把列出的每个文件复制到被移走产物旁边对应的目的 路径。 +### runner 包与平台定义的系统(2026.11.9.1+) + +runner 是一个程序;构建它的包是 **runner 包**(虚拟机、经 ssh 的远程主机、设备)。工程把该包写为目标的工具之一, +以工具名指定 runner: + +```toml +[target.x86_64-freebsd] +runner = ["freebsd-vm", "{}"] + +[target.x86_64-freebsd.tools] +freebsd-vm = { git = "https://example.org/freebsd-vm.git", tag = "v1" } +``` + +该工具为本机构建,以上述 argv 与环境启动:它传送产物及 `MCPP_RUNTIME_FILES` 所列文件,执行 argv,转发程序的输出, +并以程序的退出码退出。平台定义的行中,`execution` 为本机以外系统的产物,若无 runner 也无执行主机(`run_under`), +即**已构建、未运行**:`mcpp run` 拒绝并给出声明 runner 的键,`mcpp test` 把其测试报告为未运行。 + ## 运行一个可分发物 `mcpp run --pkg-format ` 打包可分发物 `` 并运行它。不带 `--runner` diff --git a/docs/zh/50-machine-output.md b/docs/zh/50-machine-output.md index 54eaf655d..39257f39e 100644 --- a/docs/zh/50-machine-output.md +++ b/docs/zh/50-machine-output.md @@ -450,6 +450,22 @@ mcpp why compat --format json | `mcpp.why.build-programs` | `graphs[]` | 每张构建程序图一项:`workspace`(其目录)与 `programs[]`,每个程序为 `{member, package, source, libraries[], identity, binary}` | | `mcpp.why.compat` | `uses[]` | 每次使用弃用读法一项:`{id, where, detail, warned, since, floor, canonical, docs}`;在构建不编辑的清单中 `warned` 为 false | +### `mcpp.why.target`、`mcpp.why.flags` 与 `mcpp.why.platform` *(mcpp 2026.11.9.1+)* + +``` +mcpp why target [--target ] --format json +mcpp why flags [] [--target ] --format json +mcpp why platform [--target ] --format json +``` + +`data` 带 `status`,以及: + +| kind | 字段 | 内容 | +|---|---|---| +| `mcpp.why.target` | `target` | `{request, identity, provider, priority, fallback, why, candidates[]}`,每个候选 `{identity, provider, priority, available, why}`(SPEC-016 §7) | +| `mcpp.why.flags` | `flags` | `{identity, units[], link[]}`:每个单元 `{file, package, words[]}`,每个参数 `{word, layer, detail}`,层为 compiler、row、platform、toolchain、package、engine 之一 | +| `mcpp.why.platform` | `platform` | `{package, version, defines[], serves[], warnings[]}`,每个所服务的身份 `{identity, provider, priority}`;构建不用平台包时为 `null` | + `mcpp.env` 的 `engineModules` 是本版本 mcpp 的引擎接口包的目录,构建程序图依赖它(30 —— `build.mcpp`);第一次编译构建程序之后它才存在。 diff --git a/docs/zh/90-build-from-source.md b/docs/zh/90-build-from-source.md index ba766fbdf..5d9731e3c 100644 --- a/docs/zh/90-build-from-source.md +++ b/docs/zh/90-build-from-source.md @@ -83,18 +83,21 @@ src/ └── scaffold/ `mcpp new` and templates tests/ -├── unit/ 156 C++ tests, discovered by `mcpp test` -└── e2e/ 649 shell scripts against a real binary +├── unit/ 166 个 mtest 用例文件,组成一个测试程序(`[test] programs = "directory"`) +└── e2e/ 711 shell scripts against a real binary ``` ## 测试组织 两层,回答的是不同的问题。 -**单元测试**(`tests/unit/`,156 个文件)是 `mcpp test` 发现的 C++ -程序。它们检验一个模块的契约,不需要二进制、也不需要文件系统状态。 +**单元测试**(`tests/unit/`,166 个文件,以及各成员的 `modules//tests/`)是 `mcpp test` +发现的 C++ 程序。它们检验一个模块的契约,不需要二进制、也不需要文件系统状态。每个文件由 +[mtest](https://github.com/mcpp-community/mtest) 的用例组成:一个对象 `UTest name{"Suite/Case", [] { ... }};` +即一个用例,文件不写 `main`(`mcpp test --guide`)。`mcpp test --case 'TargetSelect/*'` 运行模式所指的用例; +`mcpp test -p <成员>` 运行一个成员的用例。这些文件组成一个程序,只链接一次,按文件各起一个进程运行。 -**端到端测试**(`tests/e2e/NN_.sh`,649 个文件)拿真实的 `mcpp` +**端到端测试**(`tests/e2e/NN_.sh`,721 个文件)拿真实的 `mcpp` 二进制跑真实工程。`run_all.sh` 是 CI 的入口。`mcpp test` **不**运行它们。 ```bash @@ -119,7 +122,8 @@ MCPP= bash tests/e2e/02_new_build_run.sh 行是它运行时占有的资源(`machine`:没有别的脚本与它并行;改动共享 home 配置的脚本这样声明)。 `run_all.sh` 在 `E2E_MTEST=` 下经由它运行整套:`E2E_SHARD` 按存储(`tests/e2e/.mtest`, CI 取自 main 的上一次运行)中测得的历史划分,`E2E_JOBS` 同时运行多少个脚本,`E2E_SET=pr` -按夜间的覆盖映射(`.github/workflows/e2e-coverage.yml`)运行变更影响的脚本。在 `tests/e2e` +按夜间的覆盖映射(`.github/workflows/e2e-coverage.yml`)运行变更影响的脚本;集合留下的脚本报告为 +`unaffected`,「每个脚本都在某处运行」的检查(规则 R5)对该次运行接受它,其覆盖由 main 的完整运行承担。在 `tests/e2e` 中执行 `mtest explain` 可得知脚本在哪里失败以及如何再次运行。 ## 写一条真的在测东西的判据 diff --git a/docs/zh/README.md b/docs/zh/README.md index 6f41ba2e8..9246be87c 100644 --- a/docs/zh/README.md +++ b/docs/zh/README.md @@ -173,3 +173,4 @@ - [SPEC-013 —— 平台:一类目标的配置,其选择、合并、对依赖图的补全、链接、封装、运行器与载体](../specs/platforms.md) - [SPEC-014 —— 打包与分发:链接、闭包与容器,构建腿,暂存树,平台的树、运行覆盖与单文件包](../specs/packaging.md) - [SPEC-015 —— 目标集合与腿:写法与展开,命令对多条腿的语义,测试归档](../specs/target-sets.md) + - [SPEC-016 —— 目标与提供者:行、提供者、选择规则、平台定义的行、报告与锁](../specs/targets-and-providers.md) diff --git a/mcpp.toml b/mcpp.toml index e505fe556..1f52bd5dd 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.11.8.1" +version = "2026.11.9.1" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] @@ -59,6 +59,7 @@ ldflags = ["--coverage"] [test] windows_code_page = "utf-8" +programs = "directory" # Each entry equals its row's Default (SPEC-009 §12): the LLVM line moved to # 23.1.3 — the first release carrying the macOS 27 arm64e.x1 linker fix @@ -111,7 +112,7 @@ mcpplibs.mwrap.xz = "5.8.3" # The operating system is mcpplibs.os's (design 2026-10-26 §25): mcpp keeps no # layer of its own. -mcpplibs.os = "0.4.0" +mcpplibs.os = "0.4.1" # `modules/` holds what gets linked into this binary; `src/` is what has not # been separated yet, and shrinking it is the direction. Each entry is an @@ -163,9 +164,8 @@ members = [ ] # `mcpp build` ignores [dev-dependencies]; only `mcpp test` resolves them. -# Namespace omission means exactly mcpplibs, so the compat wrapper is explicit. [dev-dependencies] -compat.gtest = "1.15.2" +mcpplibs.mtest = "0.5.2" # Everything else uses M5.0 conventions: # - target = mcpp (kind=bin inferred from src/main.cpp) diff --git a/modules/buildmcpp/mcpp.toml b/modules/buildmcpp/mcpp.toml index 30dad4d63..db89e2db3 100644 --- a/modules/buildmcpp/mcpp.toml +++ b/modules/buildmcpp/mcpp.toml @@ -41,4 +41,4 @@ mcpp.source-kind = { path = "../source-kind" } mcpp.toolchain-model = { path = "../toolchain-model" } [dev-dependencies] -compat.gtest = "1.15.2" +mcpplibs.mtest = "0.5.2" diff --git a/modules/buildmcpp/tests/test_glob_inputs.cpp b/modules/buildmcpp/tests/test_glob_inputs.cpp index 312401a4b..01f74b811 100644 --- a/modules/buildmcpp/tests/test_glob_inputs.cpp +++ b/modules/buildmcpp/tests/test_glob_inputs.cpp @@ -1,25 +1,27 @@ -#include import std; +import mcpplibs.mtest; import mcpp.build.directives; import mcpp.toolchain.fingerprint; +using namespace mcpplibs::mtest; + namespace dirs = mcpp::build::directives; namespace { -struct GlobInputs : testing::Test { +struct GlobInputs { std::filesystem::path tree; std::filesystem::path root; - void SetUp() override { + void SetUp() { tree = std::filesystem::temp_directory_path() / "mcpp_parent_glob_inputs"; std::filesystem::remove_all(tree); root = tree / "workspace" / "app"; std::filesystem::create_directories(root); } - void TearDown() override { + void TearDown() { std::error_code ec; std::filesystem::remove_all(tree, ec); } @@ -38,105 +40,168 @@ struct GlobInputs : testing::Test { } // namespace -TEST_F(GlobInputs, ParentDirectoryMembershipInvalidatesTheFingerprint) { +UTest parent_directory_membership_invalidates_the_fingerprint{"GlobInputs/ParentDirectoryMembershipInvalidatesTheFingerprint", [] { + struct Case : GlobInputs { + Case() { SetUp(); } + ~Case() { TearDown(); } + void body() { write("../inputs/a.in"); const auto one = fingerprint("../inputs/**/*.in"); - EXPECT_NE(one, mcpp::toolchain::hash_string("")); + expect(ne(one, mcpp::toolchain::hash_string(""))); write("../inputs/nested/b.in"); - EXPECT_NE(fingerprint("../inputs/**/*.in"), one); + expect(ne(fingerprint("../inputs/**/*.in"), one)); std::filesystem::remove(root / "../inputs/nested/b.in"); - EXPECT_EQ(fingerprint("../inputs/**/*.in"), one); + expect(eq(fingerprint("../inputs/**/*.in"), one)); write("../inputs/a.in", "different contents"); write("../inputs/ignored.txt"); write("../unrelated/c.in"); - EXPECT_EQ(fingerprint("../inputs/**/*.in"), one); + expect(eq(fingerprint("../inputs/**/*.in"), one)); } - -TEST_F(GlobInputs, MultipleParentLevelsAndExactPathsAreWatched) { + } fixture; + fixture.body(); +}}; + +UTest multiple_parent_levels_and_exact_paths_are_watched{"GlobInputs/MultipleParentLevelsAndExactPathsAreWatched", [] { + struct Case : GlobInputs { + Case() { SetUp(); } + ~Case() { TearDown(); } + void body() { const auto before = fingerprint("../../inputs/*.in"); write("../../inputs/a.in"); - EXPECT_NE(fingerprint("../../inputs/*.in"), before); - EXPECT_NE(fingerprint("../../inputs/a.in"), before); + expect(ne(fingerprint("../../inputs/*.in"), before)); + expect(ne(fingerprint("../../inputs/a.in"), before)); std::filesystem::remove(root / "../../inputs/a.in"); - EXPECT_EQ(fingerprint("../../inputs/*.in"), before); + expect(eq(fingerprint("../../inputs/*.in"), before)); } - -TEST_F(GlobInputs, MissingLiteralDirectoryDoesNotHideLaterFiles) { + } fixture; + fixture.body(); +}}; + +UTest missing_literal_directory_does_not_hide_later_files{"GlobInputs/MissingLiteralDirectoryDoesNotHideLaterFiles", [] { + struct Case : GlobInputs { + Case() { SetUp(); } + ~Case() { TearDown(); } + void body() { const auto before = fingerprint("../missing/**/*.in"); write("../missing/ignored.txt"); - EXPECT_EQ(fingerprint("../missing/**/*.in"), before); + expect(eq(fingerprint("../missing/**/*.in"), before)); write("../missing/nested/a.in"); - EXPECT_NE(fingerprint("../missing/**/*.in"), before); + expect(ne(fingerprint("../missing/**/*.in"), before)); } - -TEST_F(GlobInputs, LiteralPrefixDoesNotBypassExcludedDirectories) { + } fixture; + fixture.body(); +}}; + +UTest literal_prefix_does_not_bypass_excluded_directories{"GlobInputs/LiteralPrefixDoesNotBypassExcludedDirectories", [] { + struct Case : GlobInputs { + Case() { SetUp(); } + ~Case() { TearDown(); } + void body() { const auto before = fingerprint("**"); write("target/generated.in"); write(".git/index.in"); write("../inputs/target/generated.in"); write("../inputs/.git/index.in"); - EXPECT_EQ(fingerprint("**"), before); + expect(eq(fingerprint("**"), before)); for (const auto pattern : {"target/**", ".git/**", "../inputs/target/**", "../inputs/.git/**", "../inputs/**"}) - EXPECT_EQ(fingerprint(pattern), mcpp::toolchain::hash_string("")) << pattern; + expect(eq(fingerprint(pattern), mcpp::toolchain::hash_string(""))) << pattern; } - -TEST_F(GlobInputs, ParentPatternsRetainPathSetSemantics) { + } fixture; + fixture.body(); +}}; + +UTest parent_patterns_retain_path_set_semantics{"GlobInputs/ParentPatternsRetainPathSetSemantics", [] { + struct Case : GlobInputs { + Case() { SetUp(); } + ~Case() { TearDown(); } + void body() { write("../inputs/a.in"); const auto before = fingerprint("../inputs/**"); write("../inputs/a.in", "longer content"); std::filesystem::last_write_time(root / "../inputs/a.in", std::filesystem::file_time_type::clock::now() + std::chrono::hours(1)); - EXPECT_EQ(fingerprint("../inputs/**"), before); + expect(eq(fingerprint("../inputs/**"), before)); write("../inputs/nested/b.in"); - EXPECT_NE(fingerprint("../inputs/**"), before); + expect(ne(fingerprint("../inputs/**"), before)); } - -TEST_F(GlobInputs, DirectorySymlinksAreFollowedAsTheSourceScanFollowsThem) { + } fixture; + fixture.body(); +}}; + +UTest directory_symlinks_are_followed_as_the_source_scan_follows_them{"GlobInputs/DirectorySymlinksAreFollowedAsTheSourceScanFollowsThem", [] { + struct Case : GlobInputs { + Case() { SetUp(); } + ~Case() { TearDown(); } + void body() { write("../real/nested/a.in"); std::error_code ec; std::filesystem::create_directory_symlink(tree / "workspace" / "real", root / "../link", ec); - if (ec) GTEST_SKIP() << "Directory symlinks are unavailable: " << ec.message(); + if (ec) skip([&] { std::ostringstream text; text << "Directory symlinks are unavailable: " << ec.message(); return text.str(); }()); const auto before = fingerprint("../link/**/*.in"); - EXPECT_NE(before, mcpp::toolchain::hash_string("")); - EXPECT_NE(fingerprint("../link/nested/*.in"), mcpp::toolchain::hash_string("")); + expect(ne(before, mcpp::toolchain::hash_string(""))); + expect(ne(fingerprint("../link/nested/*.in"), mcpp::toolchain::hash_string(""))); write("../real/nested/b.in"); - EXPECT_NE(fingerprint("../link/**/*.in"), before); + expect(ne(fingerprint("../link/**/*.in"), before)); } - -TEST_F(GlobInputs, ALinkCycleEndsTheWalk) { + } fixture; + fixture.body(); +}}; + +UTest a_link_cycle_ends_the_walk{"GlobInputs/ALinkCycleEndsTheWalk", [] { + struct Case : GlobInputs { + Case() { SetUp(); } + ~Case() { TearDown(); } + void body() { write("../loop/a.in"); std::error_code ec; std::filesystem::create_directory_symlink(tree / "workspace" / "loop", root / "../loop/again", ec); - if (ec) GTEST_SKIP() << "Directory symlinks are unavailable: " << ec.message(); - EXPECT_NE(fingerprint("../loop/**/*.in"), mcpp::toolchain::hash_string("")); + if (ec) skip([&] { std::ostringstream text; text << "Directory symlinks are unavailable: " << ec.message(); return text.str(); }()); + expect(ne(fingerprint("../loop/**/*.in"), mcpp::toolchain::hash_string(""))); } - -TEST_F(GlobInputs, AbsolutePatternsMatchAbsolutePaths) { + } fixture; + fixture.body(); +}}; + +UTest absolute_patterns_match_absolute_paths{"GlobInputs/AbsolutePatternsMatchAbsolutePaths", [] { + struct Case : GlobInputs { + Case() { SetUp(); } + ~Case() { TearDown(); } + void body() { write("../inputs/a.in"); const auto dir = (tree / "workspace" / "inputs").lexically_normal().generic_string(); const auto before = fingerprint(dir + "/**/*.in"); - EXPECT_NE(before, mcpp::toolchain::hash_string("")); + expect(ne(before, mcpp::toolchain::hash_string(""))); write("../inputs/nested/b.in"); - EXPECT_NE(fingerprint(dir + "/**/*.in"), before); + expect(ne(fingerprint(dir + "/**/*.in"), before)); } - -TEST(GlobInputBoundary, PatternsThatLeaveThePackageAreRecognised) { - EXPECT_FALSE(dirs::glob_leaves_package("proto/**/*.proto")); - EXPECT_FALSE(dirs::glob_leaves_package("a/../b/*.in")); - EXPECT_TRUE(dirs::glob_leaves_package("../inputs/**/*.in")); - EXPECT_TRUE(dirs::glob_leaves_package("a/../../inputs/*.in")); - EXPECT_TRUE(dirs::glob_leaves_package("/usr/share/data/*.in")); - EXPECT_TRUE(dirs::glob_leaves_package("C:/data/*.in")); -} - -TEST_F(GlobInputs, AnUnwatchablePatternSaysWhy) { - EXPECT_FALSE(dirs::glob_unwatchable(root, "proto/**", "target").has_value()); - EXPECT_FALSE(dirs::glob_unwatchable(root, "../inputs/**", "target").has_value()); + } fixture; + fixture.body(); +}}; + +UTest patterns_that_leave_the_package_are_recognised{"GlobInputBoundary/PatternsThatLeaveThePackageAreRecognised", [] { + expect(!dirs::glob_leaves_package("proto/**/*.proto")); + expect(!dirs::glob_leaves_package("a/../b/*.in")); + expect(bool(dirs::glob_leaves_package("../inputs/**/*.in"))); + expect(bool(dirs::glob_leaves_package("a/../../inputs/*.in"))); + expect(bool(dirs::glob_leaves_package("/usr/share/data/*.in"))); + expect(bool(dirs::glob_leaves_package("C:/data/*.in"))); +}}; + +UTest an_unwatchable_pattern_says_why{"GlobInputs/AnUnwatchablePatternSaysWhy", [] { + struct Case : GlobInputs { + Case() { SetUp(); } + ~Case() { TearDown(); } + void body() { + expect(!(dirs::glob_unwatchable(root, "proto/**", "target").has_value())); + expect(!(dirs::glob_unwatchable(root, "../inputs/**", "target").has_value())); for (const auto pattern : {"target/**", ".git/HEAD", "../inputs/.mcpp/*", "/*.in"}) - EXPECT_TRUE(dirs::glob_unwatchable(root, pattern, "target").has_value()) << pattern; + expect(dirs::glob_unwatchable(root, pattern, "target").has_value()) << pattern; } + } fixture; + fixture.body(); +}}; diff --git a/modules/buildmcpp/tests/test_protocol.cpp b/modules/buildmcpp/tests/test_protocol.cpp index 090096182..1f3d653f7 100644 --- a/modules/buildmcpp/tests/test_protocol.cpp +++ b/modules/buildmcpp/tests/test_protocol.cpp @@ -1,8 +1,10 @@ -#include import std; +import mcpplibs.mtest; import mcpp.build.program_protocol; +using namespace mcpplibs::mtest; + // A SUBSYSTEM-LEVEL test: it knows only this package, and it is the only place // the protocol's own invariants can be stated without an engine around them. // @@ -12,13 +14,13 @@ import mcpp.build.program_protocol; namespace pp = mcpp::build::program_protocol; -TEST(ProgramProtocol, TheVersionIsPositiveAndMonotone) { +UTest the_version_is_positive_and_monotone{"ProgramProtocol/TheVersionIsPositiveAndMonotone", [] { // A protocol version of zero would make "declared nothing" and "declared // version zero" the same observation, which is the distinction the whole // unknown-directive rule rests on. - EXPECT_GT(pp::kProtocolVersion, 0); -} + expect(gt(pp::kProtocolVersion, 0)); +}}; -TEST(ProgramProtocol, TheCacheEpochIsPositive) { - EXPECT_GT(pp::kCacheEpoch, 0); -} +UTest the_cache_epoch_is_positive{"ProgramProtocol/TheCacheEpochIsPositive", [] { + expect(gt(pp::kCacheEpoch, 0)); +}}; diff --git a/modules/graph/mcpp.toml b/modules/graph/mcpp.toml index 5d25978dc..ca92d5875 100644 --- a/modules/graph/mcpp.toml +++ b/modules/graph/mcpp.toml @@ -17,4 +17,4 @@ description = "Algorithms over index-addressed directed graphs: topological orde license = "Apache-2.0" [dev-dependencies] -compat.gtest = "1.15.2" +mcpplibs.mtest = "0.5.2" diff --git a/modules/graph/tests/test_graph.cpp b/modules/graph/tests/test_graph.cpp index fabae372f..02fb9133e 100644 --- a/modules/graph/tests/test_graph.cpp +++ b/modules/graph/tests/test_graph.cpp @@ -1,8 +1,10 @@ -#include import std; +import mcpplibs.mtest; import mcpp.graph; +using namespace mcpplibs::mtest; + // SUBSYSTEM-LEVEL: this package answers "what order / what depth / what does // this reach" for an index-addressed directed graph, and nothing else. The // migrated callers (src/modgraph/graph.cppm, src/build/prepare/features.cpp, @@ -21,28 +23,28 @@ g::AdjacencyList chain(std::size_t n) { } } -TEST(TopologicalOrder, EmptyGraphOrdersNothing) { +UTest empty_graph_orders_nothing{"TopologicalOrder/EmptyGraphOrdersNothing", [] { auto r = g::topological_order({}); - ASSERT_TRUE(r.has_value()); - EXPECT_TRUE(r->empty()); -} + expect(fatal(r.has_value())); + expect(r->empty()); +}}; -TEST(TopologicalOrder, SingleNodeNoDeps) { +UTest single_node_no_deps{"TopologicalOrder/SingleNodeNoDeps", [] { auto r = g::topological_order({{}}); - ASSERT_TRUE(r.has_value()); - EXPECT_EQ(*r, (std::vector{0})); -} + expect(fatal(r.has_value())); + expect(eq(*r, (std::vector{0}))); +}}; -TEST(TopologicalOrder, UnconstrainedNodesComeOutInInputIndexOrder) { +UTest unconstrained_nodes_come_out_in_input_index_order{"TopologicalOrder/UnconstrainedNodesComeOutInInputIndexOrder", [] { // No edges at all: every node is ready from the start, and ties are // broken by index, so the order is the identity permutation. g::AdjacencyList deps(5); auto r = g::topological_order(deps); - ASSERT_TRUE(r.has_value()); - EXPECT_EQ(*r, (std::vector{0, 1, 2, 3, 4})); -} + expect(fatal(r.has_value())); + expect(eq(*r, (std::vector{0, 1, 2, 3, 4}))); +}}; -TEST(TopologicalOrder, TiesAreBrokenByInputIndexEvenAgainstDeclarationOrder) { +UTest ties_are_broken_by_input_index_even_against_declaration_order{"TopologicalOrder/TiesAreBrokenByInputIndexEvenAgainstDeclarationOrder", [] { // Node 2 depends on nothing but is declared last among the roots; nodes // 0 and 1 depend on nothing either. All three become ready at once, so // the tie-break (lowest index first) — not the order dependents were @@ -50,22 +52,22 @@ TEST(TopologicalOrder, TiesAreBrokenByInputIndexEvenAgainstDeclarationOrder) { g::AdjacencyList deps(4); deps[3] = {2, 0, 1}; // declared in a scrambled order on purpose auto r = g::topological_order(deps); - ASSERT_TRUE(r.has_value()); - EXPECT_EQ(*r, (std::vector{0, 1, 2, 3})); -} + expect(fatal(r.has_value())); + expect(eq(*r, (std::vector{0, 1, 2, 3}))); +}}; -TEST(TopologicalOrder, Diamond) { +UTest diamond{"TopologicalOrder/Diamond", [] { // 3 depends on {1, 2}; 1 and 2 both depend on {0}. g::AdjacencyList deps(4); deps[1] = {0}; deps[2] = {0}; deps[3] = {1, 2}; auto r = g::topological_order(deps); - ASSERT_TRUE(r.has_value()); - EXPECT_EQ(*r, (std::vector{0, 1, 2, 3})); -} + expect(fatal(r.has_value())); + expect(eq(*r, (std::vector{0, 1, 2, 3}))); +}}; -TEST(TopologicalOrder, DisconnectedComponentsOrderByIndexNotByComponent) { +UTest disconnected_components_order_by_index_not_by_component{"TopologicalOrder/DisconnectedComponentsOrderByIndexNotByComponent", [] { // Two unrelated chains, 0 -> 2 and 1 -> 3 (node 2 depends on node 0, // node 3 depends on node 1). Both roots are ready at once, so the // tie-break processes 0 then 1 before either child — the two @@ -75,26 +77,26 @@ TEST(TopologicalOrder, DisconnectedComponentsOrderByIndexNotByComponent) { deps[2] = {0}; deps[3] = {1}; auto r = g::topological_order(deps); - ASSERT_TRUE(r.has_value()); - EXPECT_EQ(*r, (std::vector{0, 1, 2, 3})); -} + expect(fatal(r.has_value())); + expect(eq(*r, (std::vector{0, 1, 2, 3}))); +}}; -TEST(TopologicalOrder, LongChainIsTheIdentityPermutation) { +UTest long_chain_is_the_identity_permutation{"TopologicalOrder/LongChainIsTheIdentityPermutation", [] { auto deps = chain(6); auto r = g::topological_order(deps); - ASSERT_TRUE(r.has_value()); - EXPECT_EQ(*r, (std::vector{0, 1, 2, 3, 4, 5})); -} + expect(fatal(r.has_value())); + expect(eq(*r, (std::vector{0, 1, 2, 3, 4, 5}))); +}}; -TEST(TopologicalOrder, SelfLoopIsACycleOfLengthOne) { +UTest self_loop_is_a_cycle_of_length_one{"TopologicalOrder/SelfLoopIsACycleOfLengthOne", [] { g::AdjacencyList deps(1); deps[0] = {0}; auto r = g::topological_order(deps); - ASSERT_FALSE(r.has_value()); - EXPECT_EQ(r.error().cycle, (std::vector{0, 0})); -} + expect(fatal(!r.has_value())); + expect(eq(r.error().cycle, (std::vector{0, 0}))); +}}; -TEST(TopologicalOrder, LongerCycleReportsTheExactPath) { +UTest longer_cycle_reports_the_exact_path{"TopologicalOrder/LongerCycleReportsTheExactPath", [] { // a(0) -> b(1) -> c(2) -> a(0), plus an unrelated node 3 that depends on // nothing and must not appear in the ring. g::AdjacencyList deps(4); @@ -102,50 +104,50 @@ TEST(TopologicalOrder, LongerCycleReportsTheExactPath) { deps[1] = {2}; // b depends on c deps[2] = {0}; // c depends on a auto r = g::topological_order(deps); - ASSERT_FALSE(r.has_value()); + expect(fatal(!r.has_value())); // The path starts wherever the DFS first re-enters the ring; what is // pinned down is that it IS the ring a -> b -> c -> a, walked in one // direction, closed at both ends by the same node. const auto& cycle = r.error().cycle; - ASSERT_GE(cycle.size(), 2u); - EXPECT_EQ(cycle.front(), cycle.back()); + expect(fatal(ge(cycle.size(), 2u))); + expect(eq(cycle.front(), cycle.back())); for (std::size_t i = 0; i + 1 < cycle.size(); ++i) { auto u = cycle[i], v = cycle[i + 1]; - EXPECT_TRUE(std::ranges::find(deps[u], v) != deps[u].end()) + expect(std::ranges::find(deps[u], v) != deps[u].end()) << u << " does not declare a dependency on " << v; } // Exactly the three ring members, not the unrelated fourth node. std::set members(cycle.begin(), cycle.end()); - EXPECT_EQ(members, (std::set{0, 1, 2})); -} + expect(eq(members, (std::set{0, 1, 2}))); +}}; -TEST(TopologicalOrder, CycleAmongOtherwiseOrderableNodes) { +UTest cycle_among_otherwise_orderable_nodes{"TopologicalOrder/CycleAmongOtherwiseOrderableNodes", [] { // 0 is a normal leaf; 1 and 2 cycle on each other; 3 depends on 1. g::AdjacencyList deps(4); deps[1] = {2}; deps[2] = {1}; deps[3] = {1}; auto r = g::topological_order(deps); - ASSERT_FALSE(r.has_value()); + expect(fatal(!r.has_value())); std::set members(r.error().cycle.begin(), r.error().cycle.end()); - EXPECT_EQ(members, (std::set{1, 2})); -} + expect(eq(members, (std::set{1, 2}))); +}}; -TEST(Levels, LeavesAreLevelZero) { +UTest leaves_are_level_zero{"Levels/LeavesAreLevelZero", [] { g::AdjacencyList deps(3); auto r = g::levels(deps); - ASSERT_TRUE(r.has_value()); - EXPECT_EQ(*r, (std::vector{0, 0, 0})); -} + expect(fatal(r.has_value())); + expect(eq(*r, (std::vector{0, 0, 0}))); +}}; -TEST(Levels, ChainCountsUp) { +UTest chain_counts_up{"Levels/ChainCountsUp", [] { auto deps = chain(4); auto r = g::levels(deps); - ASSERT_TRUE(r.has_value()); - EXPECT_EQ(*r, (std::vector{0, 1, 2, 3})); -} + expect(fatal(r.has_value())); + expect(eq(*r, (std::vector{0, 1, 2, 3}))); +}}; -TEST(Levels, DiamondTakesTheLongerArm) { +UTest diamond_takes_the_longer_arm{"Levels/DiamondTakesTheLongerArm", [] { // 4 depends on {1, 3}; 1 depends on {0}; 3 depends on {2}, {2} depends // on {0}. The two arms from 0 to 4 have different lengths (2 and 3), and // the level is the LONGER one. @@ -155,79 +157,79 @@ TEST(Levels, DiamondTakesTheLongerArm) { deps[3] = {2}; deps[4] = {1, 3}; auto r = g::levels(deps); - ASSERT_TRUE(r.has_value()); - EXPECT_EQ((*r)[0], 0u); - EXPECT_EQ((*r)[1], 1u); - EXPECT_EQ((*r)[2], 1u); - EXPECT_EQ((*r)[3], 2u); - EXPECT_EQ((*r)[4], 3u); -} - -TEST(Levels, DisconnectedNodesAreIndependent) { + expect(fatal(r.has_value())); + expect(eq((*r)[0], 0u)); + expect(eq((*r)[1], 1u)); + expect(eq((*r)[2], 1u)); + expect(eq((*r)[3], 2u)); + expect(eq((*r)[4], 3u)); +}}; + +UTest disconnected_nodes_are_independent{"Levels/DisconnectedNodesAreIndependent", [] { g::AdjacencyList deps(3); deps[2] = {1}; // node 0 is isolated and must not affect node 2's level. auto r = g::levels(deps); - ASSERT_TRUE(r.has_value()); - EXPECT_EQ((*r)[0], 0u); - EXPECT_EQ((*r)[1], 0u); - EXPECT_EQ((*r)[2], 1u); -} + expect(fatal(r.has_value())); + expect(eq((*r)[0], 0u)); + expect(eq((*r)[1], 0u)); + expect(eq((*r)[2], 1u)); +}}; -TEST(Levels, SameCycleErrorAsTopologicalOrder) { +UTest same_cycle_error_as_topological_order{"Levels/SameCycleErrorAsTopologicalOrder", [] { g::AdjacencyList deps(2); deps[0] = {1}; deps[1] = {0}; auto r = g::levels(deps); - ASSERT_FALSE(r.has_value()); + expect(fatal(!r.has_value())); std::set members(r.error().cycle.begin(), r.error().cycle.end()); - EXPECT_EQ(members, (std::set{0, 1})); -} + expect(eq(members, (std::set{0, 1}))); +}}; -TEST(Closure, DirectNeighboursAreJustAdjacency) { +UTest direct_neighbours_are_just_adjacency{"Closure/DirectNeighboursAreJustAdjacency", [] { g::AdjacencyList deps(3); deps[0] = {1, 2}; - EXPECT_EQ(deps[0], (std::vector{1, 2})); -} + expect(eq(deps[0], (std::vector{1, 2}))); +}}; -TEST(Closure, ExcludesRootsByDefault) { +UTest excludes_roots_by_default{"Closure/ExcludesRootsByDefault", [] { // 0 -> 1 -> 2 -> 3 (a chain of dependencies). g::AdjacencyList deps(4); deps[0] = {1}; deps[1] = {2}; deps[2] = {3}; auto c = g::closure(deps, std::array{std::size_t{0}}); - EXPECT_EQ(c, (std::vector{1, 2, 3})); -} + expect(eq(c, (std::vector{1, 2, 3}))); +}}; -TEST(Closure, IncludeRootsAddsThemBack) { +UTest include_roots_adds_them_back{"Closure/IncludeRootsAddsThemBack", [] { g::AdjacencyList deps(4); deps[0] = {1}; deps[1] = {2}; deps[2] = {3}; auto c = g::closure(deps, std::array{std::size_t{0}}, /*include_roots=*/true); - EXPECT_EQ(c, (std::vector{0, 1, 2, 3})); -} + expect(eq(c, (std::vector{0, 1, 2, 3}))); +}}; -TEST(Closure, MultipleRootsUnionAndDeduplicate) { +UTest multiple_roots_union_and_deduplicate{"Closure/MultipleRootsUnionAndDeduplicate", [] { // 0 -> 2, 1 -> 2 -> 3: both roots reach 2, so it appears once. g::AdjacencyList deps(4); deps[0] = {2}; deps[1] = {2}; deps[2] = {3}; auto c = g::closure(deps, std::array{std::size_t{0}, std::size_t{1}}); - EXPECT_EQ(c, (std::vector{2, 3})); -} + expect(eq(c, (std::vector{2, 3}))); +}}; -TEST(Closure, DisconnectedNodeIsNeverReached) { +UTest disconnected_node_is_never_reached{"Closure/DisconnectedNodeIsNeverReached", [] { g::AdjacencyList deps(3); deps[0] = {1}; // node 2 is isolated: no root can reach it. auto c = g::closure(deps, std::array{std::size_t{0}}); - EXPECT_EQ(c, (std::vector{1})); -} + expect(eq(c, (std::vector{1}))); +}}; -TEST(Closure, TolerantOfACycleReachableFromARoot) { +UTest tolerant_of_a_cycle_reachable_from_a_root{"Closure/TolerantOfACycleReachableFromARoot", [] { // 0 -> 1 -> 2 -> 1 (a cycle downstream of the root). Closure does not // reject it; it just returns the reachable set once. g::AdjacencyList deps(3); @@ -235,14 +237,14 @@ TEST(Closure, TolerantOfACycleReachableFromARoot) { deps[1] = {2}; deps[2] = {1}; auto c = g::closure(deps, std::array{std::size_t{0}}); - EXPECT_EQ(c, (std::vector{1, 2})); -} + expect(eq(c, (std::vector{1, 2}))); +}}; -TEST(Closure, RootWithNoEdgesHasAnEmptyClosure) { +UTest root_with_no_edges_has_an_empty_closure{"Closure/RootWithNoEdgesHasAnEmptyClosure", [] { g::AdjacencyList deps(2); auto c = g::closure(deps, std::array{std::size_t{0}}); - EXPECT_TRUE(c.empty()); -} + expect(c.empty()); +}}; // ── stack_topological_order: the module graph's unit order ────────────────── // @@ -266,31 +268,31 @@ std::vector reference_stack_order( } } // namespace -TEST(StackTopologicalOrder, IsTheModuleGraphsHistoricalOrder) { +UTest is_the_module_graphs_historical_order{"StackTopologicalOrder/IsTheModuleGraphsHistoricalOrder", [] { const std::vector> edges = { {3, 0}, {4, 0}, {4, 1}, {5, 3}, {5, 4}, {6, 2}, {7, 6}, {7, 5}, {1, 2}, }; auto order = g::stack_topological_order(8, edges); - ASSERT_TRUE(order.has_value()); - EXPECT_EQ(*order, reference_stack_order(8, edges)); + expect(fatal(order.has_value())); + expect(eq(*order, reference_stack_order(8, edges))); // It is not the stable order: ties go to the most recently readied node. g::AdjacencyList deps(8); for (auto [c, p] : edges) deps[c].push_back(p); - EXPECT_NE(*order, *g::topological_order(deps)); -} + expect(ne(*order, *g::topological_order(deps))); +}}; -TEST(StackTopologicalOrder, ACycleIsReportedAsItsPath) { +UTest a_cycle_is_reported_as_its_path{"StackTopologicalOrder/ACycleIsReportedAsItsPath", [] { const std::vector> edges = {{0, 1}, {1, 2}, {2, 0}}; auto order = g::stack_topological_order(3, edges); - ASSERT_FALSE(order.has_value()); + expect(fatal(!order.has_value())); auto const& ring = order.error().cycle; - ASSERT_GE(ring.size(), 2u); - EXPECT_EQ(ring.front(), ring.back()); -} + expect(fatal(ge(ring.size(), 2u))); + expect(eq(ring.front(), ring.back())); +}}; // ── depth_first_order: the host-module orders ─────────────────────────────── -TEST(DepthFirstOrder, DependenciesFirstFromEachRootInOrder) { +UTest dependencies_first_from_each_root_in_order{"DepthFirstOrder/DependenciesFirstFromEachRootInOrder", [] { // 0 -> {2, 1}, 1 -> {3}, 2 -> {3}: from root 0, dependency 2 is visited // before 1 because that is the order `deps[0]` lists them in. g::AdjacencyList deps(4); @@ -298,35 +300,35 @@ TEST(DepthFirstOrder, DependenciesFirstFromEachRootInOrder) { deps[1] = {3}; deps[2] = {3}; auto order = g::depth_first_order(deps, std::array{std::size_t{0}}); - ASSERT_TRUE(order.has_value()); - EXPECT_EQ(*order, (std::vector{3, 2, 1, 0})); -} + expect(fatal(order.has_value())); + expect(eq(*order, (std::vector{3, 2, 1, 0}))); +}}; -TEST(DepthFirstOrder, OnlyWhatTheRootsReachIsEmitted) { +UTest only_what_the_roots_reach_is_emitted{"DepthFirstOrder/OnlyWhatTheRootsReachIsEmitted", [] { g::AdjacencyList deps(4); deps[1] = {0}; deps[3] = {2}; auto order = g::depth_first_order(deps, std::array{std::size_t{1}}); - ASSERT_TRUE(order.has_value()); - EXPECT_EQ(*order, (std::vector{0, 1})); -} + expect(fatal(order.has_value())); + expect(eq(*order, (std::vector{0, 1}))); +}}; -TEST(DepthFirstOrder, ACycleIsAnErrorNamingTheRing) { +UTest a_cycle_is_an_error_naming_the_ring{"DepthFirstOrder/ACycleIsAnErrorNamingTheRing", [] { g::AdjacencyList deps(3); deps[0] = {1}; deps[1] = {2}; deps[2] = {1}; auto order = g::depth_first_order(deps, std::array{std::size_t{0}}); - ASSERT_FALSE(order.has_value()); - EXPECT_EQ(order.error().cycle, (std::vector{1, 2, 1})); -} + expect(fatal(!order.has_value())); + expect(eq(order.error().cycle, (std::vector{1, 2, 1}))); +}}; -TEST(DepthFirstOrder, ACycleCanBeSkipped) { +UTest a_cycle_can_be_skipped{"DepthFirstOrder/ACycleCanBeSkipped", [] { g::AdjacencyList deps(2); deps[0] = {1}; deps[1] = {0}; auto order = g::depth_first_order(deps, std::array{std::size_t{0}, std::size_t{1}}, g::Cycles::Skip); - ASSERT_TRUE(order.has_value()); - EXPECT_EQ(*order, (std::vector{1, 0})); -} + expect(fatal(order.has_value())); + expect(eq(*order, (std::vector{1, 0}))); +}}; diff --git a/modules/invocation/mcpp.toml b/modules/invocation/mcpp.toml index bb2ca60f9..1bb29ed19 100644 --- a/modules/invocation/mcpp.toml +++ b/modules/invocation/mcpp.toml @@ -13,4 +13,4 @@ description = "The environment of one invocation of mcpp: statements over the st license = "Apache-2.0" [dependencies] -mcpplibs.os = "0.4.0" +mcpplibs.os = "0.4.1" diff --git a/modules/manifest/mcpp.toml b/modules/manifest/mcpp.toml index 80892ae6b..21d52fd13 100644 --- a/modules/manifest/mcpp.toml +++ b/modules/manifest/mcpp.toml @@ -24,10 +24,10 @@ mcpp.source-kind = { path = "../source-kind" } mcpp.toolchain-model = { path = "../toolchain-model" } mcpp.versioning = { path = "../versioning" } -mcpplibs.os = "0.4.0" +mcpplibs.os = "0.4.1" # Subsystem-level tests. They see only this package, which is what lets them # state its contract in isolation; the root `tests/unit/` states the facts two # layers must agree on. [dev-dependencies] -compat.gtest = "1.15.2" +mcpplibs.mtest = "0.5.2" diff --git a/modules/manifest/src/dep_spec.cppm b/modules/manifest/src/dep_spec.cppm index d89b7dcfe..6c7869701 100644 --- a/modules/manifest/src/dep_spec.cppm +++ b/modules/manifest/src/dep_spec.cppm @@ -124,6 +124,10 @@ struct DependencySpec { // edges only: a `path` dependency is a tree its consumer already owns. std::vector patches; std::string patchesDigest; + // `patch_root` (mcpp#911 G8): the directory the patches name their files + // from, relative to the package's root (the directory of its resolved + // manifest); empty: that root. + std::string patchRoot; // `embed = true`: the dependency's sources (as its description selects // them, less `exclude`) are compiled INTO this consumer with the // consumer's flags and the dependency's include directories and macros, diff --git a/modules/manifest/src/manifest.cppm b/modules/manifest/src/manifest.cppm index 874bb0153..fe4699884 100644 --- a/modules/manifest/src/manifest.cppm +++ b/modules/manifest/src/manifest.cppm @@ -24,6 +24,7 @@ export import mcpp.manifest.flag_words; export import mcpp.manifest.cfg_selector; export import mcpp.manifest.platform; export import mcpp.manifest.platform_toml; +export import mcpp.manifest.platform_providers; export import mcpp.manifest.key_registry; export import mcpp.pm.compat.workspace_position; export import mcpp.pm.compat.registry; diff --git a/modules/manifest/src/platform.cppm b/modules/manifest/src/platform.cppm index 9b5680418..9ac684653 100644 --- a/modules/manifest/src/platform.cppm +++ b/modules/manifest/src/platform.cppm @@ -191,11 +191,19 @@ struct Carrier { // The `-` vocabulary of `runner.hosts`, `carrier.hosts` and a // package's coverage: a system mcpp builds programs for, and an architecture. +// `-`: a system's word and an architecture's. The system is any a +// row names -- the built-in ones and those a platform defines (mcpp#911 G10); +// so the word is checked for its form here, and for being a system where a +// pair is used. inline bool is_host_pair(std::string_view s) { const auto dash = s.find('-'); - if (dash == std::string_view::npos || dash + 1 == s.size()) return false; - const auto os = s.substr(0, dash); - return (os == "linux" || os == "macos" || os == "windows") + if (dash == 0 || dash == std::string_view::npos || dash + 1 == s.size()) return false; + auto word = [](std::string_view w) { + return std::ranges::all_of(w, [](char c) { + return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_'; + }); + }; + return word(s.substr(0, dash)) && word(s.substr(dash + 1)) && s.find('-', dash + 1) == std::string_view::npos; } @@ -228,6 +236,40 @@ struct Hosted { }; // Everything one source states about a platform, in the seven groups of §4. +// A ROW A PLATFORM DEFINES (`[[platform.define]]`, SPEC-016 §2): an identity the +// engine's table does not have, with the facts a build of it needs. +struct RowSpec { + std::string triple; + std::string tier = "preview", note, clangTriple, objectFormat = "elf", family, dataModel, + exeSuffix, sharedSuffix, loader, execution; + std::string march, mabi, codeModel, multilib, linkEmulation; + std::vector flags, descriptorKeys; + bool standard = true, exact = false; + Origin origin; +}; + +// A PROVIDER (`[[platform.provider]]`, SPEC-016 §3): the identities it serves and +// the part of the platform's statement that applies to them. `[platform]` is the +// provider of `targets`, and `[platform.hosted]` the hosted provider of its +// `targets`; both remain their shorthands. +struct ProviderSpec { + std::string name; + std::vector serves, hosts{"*"}, pairs; + int priority = 100; + std::string toolchain, sysroot, linkage; + // `platform`: the platform's own statement applies, with this provider's in + // its place; `none`: only what the provider states, and the version group -- + // the form `[platform.hosted]` is the shorthand of. + std::string base = "platform"; + // The C environment (G3): hosted (a C library with `main`), or freestanding; + // unset: hosted where a C library is present. + std::optional hosted; + std::map layers; + Link link; + std::string execution; + Origin origin; +}; + struct Statement { // [platform] Words targets; @@ -260,13 +302,22 @@ struct Statement { // [platform.hosted] Hosted hosted; + // SPEC-016: the rows the platform defines, its providers, and the priority of + // the provider `[platform]` itself is. + // The C environment the provider that applies states (G3); unset: unstated. + std::optional cHosted; + std::vector defines; + std::vector providers; + int priority = 100; + bool prioritySet = false; + bool empty() const { return !targets.set && !toolchain.set && !sysroot.set && !runner.set && runners.empty() && carriers.empty() && !descriptorKeys.set && !plugin.set && !universal.set && layers.empty() && tools.empty() && build.empty() && buildCfg.empty() && !link.driver.set && !link.flags.set && !link.start.set && !link.script.set && !link.inputsSet && !link.exe.set && !link.shared.set && !link.sealSet - && versions.empty() && !hosted.set; + && versions.empty() && !hosted.set && defines.empty() && providers.empty(); } }; @@ -278,6 +329,11 @@ struct Override { Origin platformOrigin; bool platformSet = false; bool platformNone = false; // `platform = "none"` + std::string platformBuiltin; // `platform = "builtin:"`: a built-in provider + // `[target..priority] = ` (SPEC-016 §4.3): the + // priority of a provider for the rows the selector matches. + std::map priority; + Origin priorityOrigin; Statement fields; }; @@ -296,6 +352,16 @@ constexpr bool is_layer(std::string_view k) { inline constexpr std::string_view kPlatformKeys[] = { "targets", "toolchain", "sysroot", "runner", "runners", "carrier", "plugin", "universal", "descriptor_keys", "layers", "tools", "build", "link", "versions", "hosted", + "define", "provider", "priority", +}; +inline constexpr std::string_view kDefineKeys[] = { + "triple", "tier", "note", "standard", "exact", "clang-triple", "object-format", "family", + "data-model", "exe-suffix", "shared-suffix", "loader", "execution", "march", "mabi", + "code-model", "multilib", "flags", "link-emulation", "descriptor-keys", +}; +inline constexpr std::string_view kProviderKeys[] = { + "name", "serves", "priority", "hosts", "pairs", "toolchain", "sysroot", "linkage", "base", + "hosted", "layers", "link", "execution", }; inline constexpr std::string_view kBuildKeys[] = { "cflags", "cxxflags", "asmflags", "ldflags" }; inline constexpr std::string_view kLinkKeys[] = { @@ -307,7 +373,7 @@ inline constexpr std::string_view kHostedKeys[] = { "targets", "toolchain", "lay // override a platform. `runner`, `runners`, `toolchain` and `sysroot` are the // table's existing keys and keep their parsers. inline constexpr std::string_view kOverrideKeys[] = { - "platform", "layers", "tools", "carrier", "link", + "platform", "priority", "layers", "tools", "carrier", "link", }; // The carrier entry of `s` that serves `pair` (`-`): the first whose @@ -323,8 +389,31 @@ inline const Carrier* carrier_for(const Statement& s, std::string_view pair) { // `"[.]@"` -> the ref; the error names what is // missing. `none` is not a ref; callers test for it first. +// THE STRING FORM OF A REFERENCE (SPEC-016 §8, SPEC-001 §5): `[.]@`, +// or `git+[#tag=|rev=|branch=]` -- no fragment is the remote's +// default branch. A path is read by the caller, which knows the directory. inline std::expected parse_ref_text(std::string_view text) { PackageRef r; + if (text.starts_with("git+")) { + auto rest = text.substr(4); + const auto hash = rest.find('#'); + r.spec.git = std::string(rest.substr(0, hash)); + if (r.spec.git.empty()) + return std::unexpected(std::format("'{}' names no repository after `git+`", text)); + r.spec.gitRefKind = "branch"; // the default branch, resolved when it is fetched + if (hash != std::string_view::npos) { + const auto frag = rest.substr(hash + 1); + const auto eq = frag.find('='); + const auto kind = frag.substr(0, eq); + if (eq == std::string_view::npos || eq + 1 == frag.size() + || (kind != "tag" && kind != "rev" && kind != "branch")) + return std::unexpected(std::format( + "'{}': after `#` write tag=, rev= or branch=", text)); + r.spec.gitRefKind = std::string(kind); + r.spec.gitRev = std::string(frag.substr(eq + 1)); + } + return r; + } const auto at = text.rfind('@'); if (at == std::string_view::npos || at == 0 || at + 1 == text.size()) return std::unexpected(std::format( @@ -366,10 +455,12 @@ inline std::string identity(const Override& o) { std::string s = o.selector; auto add = [&](const Origin& g) { s += std::format("|{}:{}:{}", g.file, g.line, g.key); }; if (o.platformSet) { - s += o.platformNone ? std::string("|none") : "|" + display(o.platform); + s += !o.platformBuiltin.empty() ? "|builtin:" + o.platformBuiltin + : o.platformNone ? std::string("|none") : "|" + display(o.platform); add(o.platformOrigin); } auto const& f = o.fields; + for (auto const& [k, n] : o.priority) s += std::format("|priority:{}={}", k, n); for (auto const& [k, e] : f.layers) add(e.origin); for (auto const& [k, e] : f.tools) add(e.origin); for (auto const& [k, w] : f.runners) add(w.origin); diff --git a/modules/manifest/src/platform_providers.cppm b/modules/manifest/src/platform_providers.cppm new file mode 100644 index 000000000..691bc147a --- /dev/null +++ b/modules/manifest/src/platform_providers.cppm @@ -0,0 +1,149 @@ +// mcpp.manifest.platform_providers — what a platform's statement provides +// (SPEC-016 §3, §5). +// +// A statement's providers are its `[[platform.provider]]` tables, `[platform]` +// as the provider of its `targets`, and `[platform.hosted]` as the hosted +// provider of the systems it names. The engine registers these providers, the +// reader checks them against the rows, and `mcpp why platform` reports the +// identities they serve; all three read this one list. + +export module mcpp.manifest.platform_providers; + +import std; +import mcpp.manifest.platform; +import mcpp.toolchain.triple; +import mcpp.toolchain.target_schema; +import mcpp.toolchain.builtin_targets; + +export namespace mcpp::manifest::platform { + +struct StatedProvider { + std::string name; + std::vector serves, hosts, pairs; + int priority = 100; + std::string toolchain, sysroot, linkage; + bool shorthand = false; // `[platform]` or `[platform.hosted]` +}; + +// `name` is the platform's name (`platform_name`), the name of every provider +// that states none. +std::vector stated_providers(const Statement& s, std::string_view name); + +// One identity a provider serves, with the provider's priority. +struct ServedIdentity { + std::string identity, provider; + int priority = 0; +}; + +// The identities the statement's providers serve among the built-in rows and the +// rows it defines, by identity, then priority (highest first), then provider. +std::vector served_identities(const Statement& s, std::string_view name); + +// The statement checked against the rows: a row defined twice, or a definition +// of a built-in row, is an error; a provider that serves no row, and two +// providers that serve one identity at one priority, are warnings. +struct StatementFindings { + std::vector errors, warnings; +}; +StatementFindings check_statement(const Statement& s, std::string_view name); + +} // namespace mcpp::manifest::platform + +namespace mcpp::manifest::platform { + +namespace { + +namespace target = mcpp::toolchain::target; + +bool builtin_row(std::string_view triple) { + return std::ranges::any_of(mcpp::toolchain::builtin::kRows, + [&](auto const& r) { return r.triple == triple; }); +} + +// The rows a pattern matches: the statement's own, then the built-in ones. +std::vector rows_matching(const Statement& s, std::string_view pattern) { + std::vector out; + for (auto const& d : s.defines) + if (target::serves_triple(pattern, d.triple) && !std::ranges::contains(out, d.triple)) + out.push_back(d.triple); + for (auto const& r : mcpp::toolchain::builtin::kRows) + if (target::serves_triple(pattern, r.triple) && !std::ranges::contains(out, r.triple)) + out.emplace_back(r.triple); + return out; +} + +} // namespace + +std::vector stated_providers(const Statement& s, std::string_view name) { + std::vector out; + for (auto const& p : s.providers) + out.push_back({ p.name.empty() ? std::string(name) : p.name, p.serves, p.hosts, p.pairs, p.priority, + p.toolchain.empty() ? s.toolchain.value : p.toolchain, p.sysroot, p.linkage, false }); + if (s.targets.set) + out.push_back({ std::string(name), s.targets.value, {"*"}, {}, s.prioritySet ? s.priority : 100, + s.toolchain.value, {}, {}, true }); + // A HOSTED ENTRY NAMES A SYSTEM; THE C LIBRARY NAMES THE ENV: it serves every + // identity of that architecture and system. + if (s.hosted.set) { + std::vector serves; + for (auto const& w : s.hosted.targets.value) + if (auto t = mcpp::toolchain::triple::parse(w)) { + serves.push_back(std::format("{}-{}-*", t->arch, t->os)); + serves.push_back(std::format("{}-{}", t->arch, t->os)); + } + if (!s.hosted.targets.set) serves = {"*-*-*"}; + out.push_back({ std::string(name), std::move(serves), {"*"}, {}, s.prioritySet ? s.priority - 50 : 50, + s.hosted.toolchain.set ? s.hosted.toolchain.value : s.toolchain.value, {}, {}, true }); + } + return out; +} + +// The rows one provider serves, each once. A sorted vector and not a set: GCC +// 16.1 left a set's constructor that this unit instantiated undefined for an +// importer (mcpp#905). +std::vector rows_served(const Statement& s, const StatedProvider& p) { + std::vector rows; + for (auto const& pattern : p.serves) std::ranges::copy(rows_matching(s, pattern), std::back_inserter(rows)); + std::ranges::sort(rows); + rows.erase(std::unique(rows.begin(), rows.end()), rows.end()); + return rows; +} + +std::vector served_identities(const Statement& s, std::string_view name) { + std::vector out; + for (auto const& p : stated_providers(s, name)) + for (auto const& id : rows_served(s, p)) out.push_back({ id, p.name, p.priority }); + std::ranges::sort(out, [](auto const& a, auto const& b) { + return std::tie(a.identity, b.priority, a.provider) < std::tie(b.identity, a.priority, b.provider); + }); + return out; +} + +StatementFindings check_statement(const Statement& s, std::string_view name) { + StatementFindings f; + std::set own; + for (auto const& d : s.defines) { + if (builtin_row(d.triple)) + f.errors.push_back(std::format("[[platform.define]] '{}' is a built-in row; a platform serves it " + "with [[platform.provider]] serves = [\"{}\"]", d.triple, d.triple)); + else if (!own.insert(d.triple).second) + f.errors.push_back(std::format("[[platform.define]] '{}' is defined twice", d.triple)); + } + for (auto const& p : stated_providers(s, name)) { + if (p.shorthand) continue; + if (std::ranges::none_of(p.serves, [&](auto const& pattern) { return !rows_matching(s, pattern).empty(); })) + f.warnings.push_back(std::format("[[platform.provider]] '{}' serves no row ({})", p.name, + p.serves.empty() ? std::string("serves is empty") : p.serves.front())); + } + const auto served = served_identities(s, name); + for (std::size_t i = 1; i < served.size(); ++i) { + auto const& a = served[i - 1]; + auto const& b = served[i]; + if (a.identity == b.identity && a.priority == b.priority && a.provider != b.provider) + f.warnings.push_back(std::format("'{}' is served by '{}' and '{}' at priority {}: the name orders them", + b.identity, a.provider, b.provider, b.priority)); + } + return f; +} + +} // namespace mcpp::manifest::platform diff --git a/modules/manifest/src/platform_toml.cppm b/modules/manifest/src/platform_toml.cppm index 0e8c2727f..8a1788cfc 100644 --- a/modules/manifest/src/platform_toml.cppm +++ b/modules/manifest/src/platform_toml.cppm @@ -68,6 +68,8 @@ struct Reader { // `[platform]` of a platform package. std::expected statement(const mcpp::libs::toml::Table& t) const; + std::expected define(const mcpp::libs::toml::Table& t, std::string_view where) const; + std::expected provider(const mcpp::libs::toml::Table& t, std::string_view where) const; // The platform part of one `[target.]` table (the keys of // `kOverrideKeys`, plus `runner` and `runners`, which the table already @@ -475,8 +477,8 @@ Reader::host_pairs(const toml::Value& v, std::string_view where) const { for (auto const& p : *w) if (!mcpp::manifest::platform::is_host_pair(p)) return std::unexpected(std::format( - "{}: '{}' is not a system and an architecture; write \"-\" with " - " one of linux, macos, windows, for example \"linux-x86_64\"", where, p)); + "{}: '{}' is not a system and an architecture; write \"-\", " + "for example \"linux-x86_64\"", where, p)); return w; } @@ -549,6 +551,94 @@ std::expected Reader::carrier(const toml::Value& v, std::s return c; } +std::expected Reader::define(const toml::Table& t, std::string_view where) const { + RowSpec r; + r.origin = Origin{ tier, file.generic_string(), 0, std::string(where) }; + auto text = [&](std::string_view key, std::string& to) -> std::expected { + auto it = t.find(key); + if (it == t.end()) return {}; + if (!it->second.is_string()) + return std::unexpected(std::format("{}.{} must be a string", where, key)); + to = it->second.as_string(); + return {}; + }; + for (auto const& [k, v] : t) + if (!contains(kDefineKeys, k)) + warn(std::format("{}: '{}' is not a key of a row (ignored); the keys are {}", where, k, + list_of(kDefineKeys))); + for (auto [key, to] : std::initializer_list>{ + {"triple", &r.triple}, {"tier", &r.tier}, {"note", &r.note}, {"clang-triple", &r.clangTriple}, + {"object-format", &r.objectFormat}, {"family", &r.family}, {"data-model", &r.dataModel}, + {"exe-suffix", &r.exeSuffix}, {"shared-suffix", &r.sharedSuffix}, {"loader", &r.loader}, + {"execution", &r.execution}, {"march", &r.march}, {"mabi", &r.mabi}, + {"code-model", &r.codeModel}, {"multilib", &r.multilib}, {"link-emulation", &r.linkEmulation}}) + if (auto e = text(key, *to); !e) return std::unexpected(e.error()); + for (auto [key, to] : std::initializer_list*>>{ + {"flags", &r.flags}, {"descriptor-keys", &r.descriptorKeys}}) + if (auto it = t.find(key); it != t.end()) { + auto w = strings(it->second, std::format("{}.{}", where, key)); + if (!w) return std::unexpected(w.error()); + *to = std::move(*w); + } + for (auto [key, to] : std::initializer_list>{ + {"standard", &r.standard}, {"exact", &r.exact}}) + if (auto it = t.find(key); it != t.end()) { + if (!it->second.is_bool()) return std::unexpected(std::format("{}.{} must be true or false", where, key)); + *to = it->second.as_bool(); + } + // `arch-os` or `arch-os-env`, each segment of letters, digits, `_` and `.`. + std::size_t segments = 1; + for (char c : r.triple) segments += c == '-'; + if (r.triple.empty() || segments < 2 || segments > 3 || r.triple.starts_with('-') || r.triple.ends_with('-') + || r.triple.find("--") != std::string::npos) + return std::unexpected(std::format("{}.triple must be `arch-os` or `arch-os-env`, for example " + "\"x86_64-freebsd\"", where)); + if (r.tier != "verified" && r.tier != "preview" && r.tier != "planned") + return std::unexpected(std::format("{}.tier is verified, preview or planned", where)); + if (r.objectFormat != "elf" && r.objectFormat != "mach-o" && r.objectFormat != "pe" && r.objectFormat != "wasm") + return std::unexpected(std::format("{}.object-format is elf, mach-o, pe or wasm", where)); + return r; +} + +std::expected Reader::provider(const toml::Table& t, std::string_view where) const { + ProviderSpec p; + p.origin = Origin{ tier, file.generic_string(), 0, std::string(where) }; + for (auto const& [k, v] : t) { + const auto key = std::format("{}.{}", where, k); + if (k == "name" || k == "toolchain" || k == "sysroot" || k == "linkage" || k == "execution" + || k == "base") { + if (!v.is_string()) return std::unexpected(std::format("{} must be a string", key)); + (k == "name" ? p.name : k == "toolchain" ? p.toolchain : k == "sysroot" ? p.sysroot + : k == "linkage" ? p.linkage : k == "base" ? p.base : p.execution) = v.as_string(); + } else if (k == "serves" || k == "hosts" || k == "pairs") { + auto w = strings(v, key); + if (!w) return std::unexpected(w.error()); + (k == "serves" ? p.serves : k == "hosts" ? p.hosts : p.pairs) = std::move(*w); + } else if (k == "priority") { + if (!v.is_int()) return std::unexpected(std::format("{} must be an integer", key)); + p.priority = static_cast(v.as_int()); + } else if (k == "hosted") { + if (!v.is_bool()) return std::unexpected(std::format("{} must be true or false", key)); + p.hosted = v.as_bool(); + } else if (k == "layers") { + if (auto r = layers(v, key, p.layers); !r) return std::unexpected(r.error()); + } else if (k == "link") { + if (!v.is_table()) return std::unexpected(std::format("{} must be a table", key)); + if (auto r = link(v.as_table(), key, p.link); !r) return std::unexpected(r.error()); + } else { + warn(std::format("{}: '{}' is not a key of a provider (ignored); the keys are {}", where, k, + list_of(kProviderKeys))); + } + } + if (p.serves.empty()) + return std::unexpected(std::format("{} serves nothing: write serves = [\"\", ...]", where)); + if (!p.linkage.empty() && p.linkage != "static" && p.linkage != "dynamic") + return std::unexpected(std::format("{}.linkage is static or dynamic", where)); + if (p.base != "platform" && p.base != "none") + return std::unexpected(std::format("{}.base is \"platform\" or \"none\"", where)); + return p; +} + std::expected Reader::statement(const toml::Table& t) const { Statement s; for (auto const& [k, v] : t) { @@ -606,10 +696,13 @@ std::expected Reader::statement(const toml::Table& t) co auto w = strings(v, key); if (!w) return std::unexpected(w.error()); if (w->empty()) return std::unexpected(std::format("{} names no key", key)); + // The keys are the blocks a descriptor may have: the built-in ones, and + // any a platform's rows name (SPEC-016 §7); a key is a word. for (auto const& d : *w) - if (std::ranges::find(kDescriptorKeys, std::string_view(d)) == std::end(kDescriptorKeys)) + if (d.empty() || std::ranges::any_of(d, [](char c) { + return !std::isalnum(static_cast(c)) && c != '_' && c != '-'; })) return std::unexpected(std::format( - "{}: '{}' is not a descriptor platform; the keys are {}", key, d, + "{}: '{}' is not a descriptor block name; the built-in ones are {}", key, d, list_of(kDescriptorKeys))); s.descriptorKeys = Words{ std::move(*w), at(v, key), true }; } else if (k == "plugin") { @@ -645,6 +738,29 @@ std::expected Reader::statement(const toml::Table& t) co s.versions[vk] = vv.as_string(); } s.versionsOrigin = at(v, "[platform.versions]"); + } else if (k == "priority") { + if (!v.is_int()) + return std::unexpected(std::format("{} must be an integer", key)); + s.priority = static_cast(v.as_int()); + s.prioritySet = true; + } else if (k == "define" || k == "provider") { + if (!v.is_array()) + return std::unexpected(std::format( + "{} is an array of tables: write [[platform.{}]]", key, k)); + std::size_t i = 0; + for (auto const& e : v.as_array()) { + const auto where = std::format("[[platform.{}]] #{}", k, ++i); + if (!e.is_table()) return std::unexpected(std::format("{} must be a table", where)); + if (k == "define") { + auto r = define(e.as_table(), where); + if (!r) return std::unexpected(r.error()); + s.defines.push_back(std::move(*r)); + } else { + auto r = provider(e.as_table(), where); + if (!r) return std::unexpected(r.error()); + s.providers.push_back(std::move(*r)); + } + } } else if (k == "hosted") { if (!v.is_table()) return std::unexpected(std::format("{} must be a table", key)); s.hosted.set = true; @@ -686,6 +802,12 @@ Reader::override_(const toml::Table& t, std::string_view selector) const { if (k == "platform") { if (v.is_string() && v.as_string() == "none") { o.platformNone = true; + } else if (v.is_string() && v.as_string().starts_with("builtin:")) { + // A built-in provider by name (SPEC-016 §4): no platform package. + o.platformNone = true; + o.platformBuiltin = v.as_string().substr(8); + if (o.platformBuiltin.empty()) + return std::unexpected(std::format("{}: `builtin:` names no provider", key)); } else { auto r = ref(v, key, false, false); if (!r) return std::unexpected(r.error()); @@ -693,6 +815,16 @@ Reader::override_(const toml::Table& t, std::string_view selector) const { } o.platformOrigin = at(v, key); o.platformSet = true; + } else if (k == "priority" && !selector.empty()) { + if (!v.is_table()) + return std::unexpected(std::format( + "{} must be a table of = , for example mingw = 200", key)); + for (auto const& [pk, pv] : v.as_table()) { + if (!pv.is_int()) + return std::unexpected(std::format("{}.{} must be an integer", key, pk)); + o.priority[pk] = static_cast(pv.as_int()); + } + o.priorityOrigin = at(v, key); } else if (selector.empty()) { // `[host]` selects the host graph's platform and nothing else. warn(std::format( @@ -709,6 +841,7 @@ Reader::override_(const toml::Table& t, std::string_view selector) const { } else if (k == "link") { if (!v.is_table()) return std::unexpected(std::format("{} must be a table", key)); if (auto r = link(v.as_table(), key, o.fields.link); !r) return std::unexpected(r.error()); + } else if (k == "runner") { auto r = runner(v, key); if (!r) return std::unexpected(r.error()); diff --git a/modules/manifest/src/targetside_model.cppm b/modules/manifest/src/targetside_model.cppm index 304b59366..f2fbe7d3d 100644 --- a/modules/manifest/src/targetside_model.cppm +++ b/modules/manifest/src/targetside_model.cppm @@ -1098,13 +1098,23 @@ inline std::string format_layers(const TargetSide& ts, bool verbose) { } inline std::string format_report(const TargetSide& ts, std::string_view targetName, - bool verbose = false) { + bool verbose = false, std::string_view request = {}, + std::string_view fallback = {}) { // The head carries no verb of its own: the caller supplies one through the // status line's own padding, and the layer lines below are indented to sit - // under it. - std::string head = (ts.llvmTriple.empty() || ts.llvmTriple == targetName) - ? std::format("{}", targetName) - : std::format("{} → {}", targetName, ts.llvmTriple); + // under it. A request that is a pattern reads ` → ` + // (SPEC-016 §4.5), with the compiler's triple beside it; a selection that + // passed over its first candidate says why, on the same line. + std::string head; + if (!request.empty() && request != targetName) { + head = std::format("{} → {}", request, targetName); + if (!ts.llvmTriple.empty() && ts.llvmTriple != targetName) head += std::format(" ({})", ts.llvmTriple); + } else { + head = (ts.llvmTriple.empty() || ts.llvmTriple == targetName) + ? std::format("{}", targetName) + : std::format("{} → {}", targetName, ts.llvmTriple); + } + if (!fallback.empty()) head += std::format("; {}", fallback); // WHEN THE SEGMENT IS NOT A C LIBRARY, SAY WHAT IT IS — HERE, WHERE THE // READER IS LOOKING AT IT. diff --git a/modules/manifest/src/toml.cppm b/modules/manifest/src/toml.cppm index bb4ff76ac..cf91b4b25 100644 --- a/modules/manifest/src/toml.cppm +++ b/modules/manifest/src/toml.cppm @@ -7,6 +7,7 @@ import mcpp.manifest.key_registry; import mcpp.manifest.cfg_selector; import mcpp.manifest.platform; import mcpp.manifest.platform_toml; +import mcpp.manifest.platform_providers; import mcpp.targetside; import std; import mcpp.invocation; @@ -1054,6 +1055,10 @@ std::expected parse_document(const t::Document& documen // 2026-10-29 §4.5): the platform reader type-checks every entry. "platform.carrier", "target.*.carrier", + // `[[platform.define]]` and `[[platform.provider]]` (SPEC-016 §2, §3): + // the platform reader type-checks every entry. + "platform.define", + "platform.provider", }; if (auto badPath = find_disallowed_array_of_tables(doc->root(), "", kAllowedArraysOfTables)) { return std::unexpected(error(origin, std::format( @@ -1061,7 +1066,7 @@ std::expected parse_document(const t::Document& documen "array-of-tables syntax is only supported for [[build.flags]], " "[[features..flags]], [[runtime.requirements]], " "[[runtime.artifacts]], [[runtime.deploy]], [[platform.link.inputs]], " - "[[platform.carrier]] and [xlings] deps entries", + "[[platform.carrier]], [[platform.define]], [[platform.provider]] and [xlings] deps entries", *badPath, *badPath))); } @@ -2181,6 +2186,29 @@ std::expected parse_document(const t::Document& documen read_list("cxxflags", t.cxxflags); read_list("defines", t.defines); read_list("required_features", t.requiredFeatures); + // `[targets..build]`: the artifact's own compile of the + // package's translation units it links (mcpp#911 G9). + if (auto it = tt.find("build"); it != tt.end()) { + if (!it->second.is_table()) + return std::unexpected(error(origin, std::format( + "[targets.{}].build must be a table of cflags, cxxflags and defines", tname))); + for (auto const& [bk, bv] : it->second.as_table()) { + std::vector* dst = bk == "cflags" ? &t.buildCflags + : bk == "cxxflags" ? &t.buildCxxflags + : bk == "defines" ? &t.buildDefines : nullptr; + if (!dst || !bv.is_array()) + return std::unexpected(error(origin, std::format( + "[targets.{}.build].{} is not read: the keys are cflags, cxxflags and defines, " + "each a list", tname, bk))); + for (auto const& v : bv.as_array()) + if (v.is_string()) dst->push_back(v.as_string()); + } + for (auto const& flag : t.buildCxxflags) + if (starts_with_std_flag(flag)) + return std::unexpected(error(origin, std::format( + "[targets.{}.build].cxxflags contains '{}'; use [package].standard to " + "configure the C++ language standard", tname, flag))); + } // `windows_subsystem` / `windows_entry` (#618). A closed set each, so a // misspelling is refused by name rather than rendered as nothing. auto read_choice = [&](const char* key, std::string& out, bool subsystem) @@ -2268,7 +2296,7 @@ std::expected parse_document(const t::Document& documen // at the right axis (workspace / features / profile). static constexpr std::string_view kKnownTargetKeys[] = { "kind", "linkage", "main", "soname", "exports", "windows_auto_export", - "cflags", "cxxflags", "defines", "required_features", + "cflags", "cxxflags", "defines", "required_features", "build", "windows_entry", "windows_subsystem", "windows_code_page", "windows_long_paths", "start", "test-protocol", "test-main", "test-guide", }; @@ -2327,7 +2355,7 @@ std::expected parse_document(const t::Document& documen || k == "backend" || k == "tools" || k == "artifacts" || k == "host-module" || k == "reexport" || k == "linkage" - || k == "patches" || k == "embed" || k == "exclude"; + || k == "patches" || k == "patch_root" || k == "embed" || k == "exclude"; }; // What makes a table an inline dep spec is that it names a SOURCE. This // used to be "every key is known", which quietly coupled two unrelated @@ -2540,6 +2568,13 @@ std::expected parse_document(const t::Document& documen spec.patches.push_back(pp.lexically_normal().generic_string()); } } + if (auto it = sub.find("patch_root"); it != sub.end()) { + const std::filesystem::path pr = it->second.is_string() ? it->second.as_string() : std::string{}; + if (!it->second.is_string() || pr.is_absolute()) + return std::unexpected(error(origin, std::format( + "[{}.\"{}\"] patch_root must be a directory relative to the package's root", section, fqName))); + spec.patchRoot = pr.lexically_normal().generic_string(); + } if (auto it = sub.find("embed"); it != sub.end()) { if (!it->second.is_bool()) return std::unexpected(error(origin, std::format( @@ -2568,6 +2603,12 @@ std::expected parse_document(const t::Document& documen if (auto it = sub.find("backend"); it != sub.end() && it->second.is_string()) { spec.features.push_back("backend-" + it->second.as_string()); } + // AT MOST ONE OF `rev`, `tag`, `branch` (SPEC-001 §5); none is the + // remote's default branch, resolved when the dependency is fetched. + if (int refs = int(sub.contains("rev")) + int(sub.contains("tag")) + int(sub.contains("branch")); refs > 1) + return std::unexpected(error(origin, std::format( + "[{}.\"{}\"] names {} of rev / tag / branch; a git dependency names at most one", + section, fqName, refs))); if (auto it = sub.find("rev"); it != sub.end() && it->second.is_string()) { spec.gitRev = it->second.as_string(); spec.gitRefKind = "rev"; @@ -2600,10 +2641,7 @@ std::expected parse_document(const t::Document& documen return std::unexpected(error(origin, std::format( "[{}.\"{}\"] must specify 'path', 'version', or 'git'", section, fqName))); } - if (!spec.git.empty() && spec.gitRev.empty()) { - return std::unexpected(error(origin, std::format( - "[{}.\"{}\"] git dep requires one of: rev / tag / branch", section, fqName))); - } + if (!spec.git.empty() && spec.gitRefKind.empty()) spec.gitRefKind = "branch"; // the default branch return {}; }; @@ -3665,10 +3703,34 @@ std::expected parse_document(const t::Document& documen m.testPrograms = value.as_string(); continue; } + if (key == "store") { + if (!value.is_string() || value.as_string().empty()) + return std::unexpected(error(origin, + "[test] store is a directory relative to the project, or \"none\"")); + m.testStore = value.as_string(); + continue; + } + if (key == "split") { + if (!value.is_string() || (value.as_string() != "file" && value.as_string() != "case" + && value.as_string() != "none")) + return std::unexpected(error(origin, + "[test] split is \"file\" (a process per file of a program's cases, the default), " + "\"case\" (a process per case) or \"none\" (one process)")); + m.testSplit = value.as_string(); + continue; + } + if (key == "link") { + if (!value.is_string() || (value.as_string() != "used" && value.as_string() != "objects")) + return std::unexpected(error(origin, + "[test] link is \"used\" (the linker keeps what a test reaches, the default) or " + "\"objects\" (every object whole)")); + m.testLink = value.as_string(); + continue; + } if (key != "discover") { m.schemaWarnings.push_back(std::format( "[test] has unsupported key '{}' (ignored). Supported keys: " - "discover, main, programs, start, windows_code_page, windows_long_paths.", key)); + "discover, link, main, programs, split, start, store, windows_code_page, windows_long_paths.", key)); continue; } bool ok = value.is_array(); @@ -3863,9 +3925,17 @@ std::expected parse_document(const t::Document& documen &m.schemaWarnings}; auto st = rd.statement(*pt); if (!st) return std::unexpected(error(origin, st.error())); - if (!st->targets.set) + if (!st->targets.set && st->providers.empty()) return std::unexpected(error(origin, - "[platform] names the targets it serves: targets = [\"\", ...]")); + "[platform] names the targets it serves: targets = [\"\", ...], or its " + "providers: [[platform.provider]] serves = [...]")); + // The rows it defines and the providers it states, checked against the + // rows as the engine reads them (SPEC-016 §5). + auto findings = mcpp::manifest::platform::check_statement( + *st, mcpp::manifest::platform::platform_name(m.package.name)); + if (!findings.errors.empty()) + return std::unexpected(error(origin, findings.errors.front())); + for (auto& w : findings.warnings) m.schemaWarnings.push_back(std::move(w)); m.platform = std::move(*st); m.platformDeclared = true; } @@ -3876,6 +3946,25 @@ std::expected parse_document(const t::Document& documen if (!ov) return std::unexpected(error(origin, ov.error())); m.hostPlatform = std::move(*ov); } + // [defaults.""] — the provider of a required capability when + // nothing else provides it (mcpp#911 G7): ` = "@"` + // or a dependency table, the selector a target selector (`linux`, + // `cfg(os = "freebsd")`, a triple, `*`). + if (auto* dt = doc->get_table("defaults")) { + mcpp::manifest::platform::Reader rd{origin, mcpp::manifest::platform::Tier::Manifest, + &m.schemaWarnings}; + for (auto const& [cap, body] : *dt) { + if (!body.is_table()) + return std::unexpected(error(origin, std::format( + "[defaults.\"{}\"] must be a table of = , for example " + "linux = \"openkal-linux@^0.25\"", cap), body.position)); + for (auto const& [sel, v] : body.as_table()) { + auto r = rd.ref(v, std::format("[defaults.\"{}\"].{}", cap, toml_header_key(sel)), false, false); + if (!r) return std::unexpected(error(origin, r.error(), v.position)); + m.capabilityDefaults.push_back({ cap, sel, std::move(*r), static_cast(v.position.line) }); + } + } + } // [lib] — library root convention (cargo-style). if (auto v = doc->get_string("lib.path")) { @@ -4216,6 +4305,14 @@ std::expected parse_document(const t::Document& documen "[target.{}].link must be a table, e.g. [target.{}.link] start = \"\"", triple, triple))); platformKeys = true; } + // The priority of a provider for the rows this selector matches + // (SPEC-016 §4.3): data the selection orders candidates by. + if (auto it = body.find("priority"); it != body.end()) { + if (!it->second.is_table()) + return std::unexpected(error(origin, std::format( + "[target.{}].priority must be a table, e.g. [target.{}.priority] mingw = 200", triple, triple))); + platformKeys = true; + } if (platformKeys || body.contains("runner") || body.contains("runners")) { mcpp::manifest::platform::Reader rd{origin, mcpp::manifest::platform::Tier::Manifest, &m.schemaWarnings}; @@ -4268,7 +4365,7 @@ std::expected parse_document(const t::Document& documen static constexpr std::string_view kKnownTargetTables[] = { "abi", "build", "build-dependencies", "carrier", "dependencies", "dev-dependencies", "feature-deps", "feature-requires-abi", - "feature-xlings", "layers", "link", "requires_abi", "runners", "runtime", + "feature-xlings", "layers", "link", "priority", "requires_abi", "runners", "runtime", "targets", "tools", "xlings", }; for (auto& [key, value] : body) { @@ -4304,7 +4401,7 @@ std::expected parse_document(const t::Document& documen "[target.{}] has unsupported key '{}' (ignored). Supported keys: " "cxx_runtime, linkage, min_api_level, platform, run_under, runner, sysroot, " "toolchain, plus the [target..runners] table for named runners and " - "the platform's layers, tools, carrier and link tables. " + "the platform's layers, tools, carrier, link and priority tables. " "Per-role contracts go in [build].cxx_runtime's table form.", triple, key)); } @@ -5670,18 +5767,20 @@ upsert_dependency_text(std::string_view source, } } + auto quoted_list = [](const std::vector& items) { + std::string list; + for (auto const& item : items) { + if (!list.empty()) list += ", "; + list += std::format("\"{}\"", item); + } + return list; + }; std::string value; if (edit.features.empty()) { value = std::format("\"{}\"", edit.version); } else { - std::string featureList; - for (auto const& feature : edit.features) { - if (!featureList.empty()) featureList += ", "; - featureList += std::format("\"{}\"", feature); - } - value = std::format( - "{{ version = \"{}\", features = [{}] }}", - edit.version, featureList); + value = std::format("{{ version = \"{}\", features = [{}] }}", edit.version, + quoted_list(edit.features)); } struct SectionSpan { @@ -5785,9 +5884,13 @@ upsert_dependency_text(std::string_view source, // Retained flat dotted spellings are removed before canonical namespace // subtable emission. The parsed identity gate above ensures this edits the // exact PackageId, not a short-name substring match. - if (!defaultNamespace) { + // The default namespace's dotted spelling (`mcpplibs.cmdline = ...`) is + // the same package as the bare one and is removed as well. + { + const std::string dotted = defaultNamespace + ? std::format("{}.{}", edit.namespace_, edit.shortName) : spelling; if (auto base = find_section(text, baseHeader)) { - if (auto legacy = key_line_span(text, *base, spelling)) { + if (auto legacy = key_line_span(text, *base, dotted)) { auto eraseEnd = legacy->second; if (eraseEnd < text.size() && text[eraseEnd] == '\n') ++eraseEnd; diff --git a/modules/manifest/src/types.cppm b/modules/manifest/src/types.cppm index 503d308d9..e92dfc21b 100644 --- a/modules/manifest/src/types.cppm +++ b/modules/manifest/src/types.cppm @@ -290,6 +290,17 @@ struct Target { std::vector cflags; std::vector cxxflags; std::vector defines; + // `[targets..build]` (mcpp#911 G9): flags and defines of every + // translation unit of this package the artifact links -- its entry and the + // shared implementation sources -- compiled as the artifact's own variant + // where they differ from the package's. A module interface stays one per + // package and is not varied. + std::vector buildCflags; + std::vector buildCxxflags; + std::vector buildDefines; + bool has_build_variant() const { + return !buildCflags.empty() || !buildCxxflags.empty() || !buildDefines.empty(); + } // Build gate: this target is emitted ONLY when every listed feature is // active in the current build (otherwise it is silently skipped). Gate // only — it does not activate features (use --features / [features].default). @@ -2240,6 +2251,19 @@ struct Manifest { // "directory" (the files of one directory below a discover glob's fixed // directory are one program, named by the directory). Empty: "file". std::string testPrograms; + // `[test] link` (design 2026-11-09 §17.2): "used" (the default) -- the + // linker keeps what a test reaches (`--gc-sections`, `-dead_strip`), so a + // test of one module does not carry the package's every function --, or + // "objects", every object whole. + std::string testLink; + // `[test] split` (design 2026-11-09 §17.3): how a test program that speaks + // the protocol is run -- "file" (the default: a process per file of its + // cases), "case" (a process per case) or "none" (one process). + std::string testSplit; + // `[test] store` (design 2026-11-09 §17.3): the directory `mcpp test` + // writes its runs to in the mtest store's form (mtest P12), relative to the + // project; empty: `target/mtest`; "none": no store. + std::string testStore; // Set once a workspace root's own package has received `[workspace.package]` // and `[workspace.build]` (`mcpp::project::inherit_as_root_package`). The // manifest is read on two paths before it is planned, and an appended @@ -2301,6 +2325,17 @@ struct Manifest { // alone: one image has one C library (openkal's boundary). Unioned from // `[package]` and every matching `[target.]`, as `requires_` is. std::vector privateRequires; + // `[defaults.""] = ` (mcpp#911 G7; SPEC-002 + // §3.6): the provider of a capability this package requires when neither + // the graph nor the platform provides it, per target selector. The layer + // mechanism of the platform, for any package: graph > platform > default. + struct CapabilityDefault { + std::string capability; + std::string selector; + mcpp::manifest::platform::PackageRef ref; + int line = 0; + }; + std::vector capabilityDefaults; // `requires_abi = { threads = true, exceptions = true }` at package // level, and per feature. A statement that the ARTEFACT's ABI has a // switch on, compared at resolution with the root's diff --git a/modules/manifest/src/xpkg.cppm b/modules/manifest/src/xpkg.cppm index c2b0571f1..70bae6560 100644 --- a/modules/manifest/src/xpkg.cppm +++ b/modules/manifest/src/xpkg.cppm @@ -1001,9 +1001,45 @@ xpkg_name_form_violation_from_lua(std::string_view luaContent) extract_xpkg_name(luaContent)); } +namespace { +std::vector +entries_of_first_block(std::string_view luaContent, + const mcpp::toolchain::platform::PlatformKey& platformAxis); +} + +// THE ARTIFACT THAT IS ONE FOR EVERY TARGET (mcpp#911 G6; SPEC-016 §7). A target +// reads the blocks its row names; when the descriptor has none of them, a +// version whose artifact is the same in every block it has -- the source archive +// of a package built from source -- is the target's too, and its versions are +// those. A prebuilt payload differs per block and stays the block's. std::vector list_xpkg_version_entries(std::string_view luaContent, const mcpp::toolchain::platform::PlatformKey& platformAxis) { + auto direct = entries_of_first_block(luaContent, platformAxis); + if (!direct.empty()) return direct; + std::vector> blocks; + for (auto key : mcpp::toolchain::platform::xpkg_platforms) { + auto one = entries_of_first_block(luaContent, + mcpp::toolchain::platform::TargetPlatform::with_keys({std::string(key)})); + if (!one.empty()) blocks.push_back(std::move(one)); + } + if (blocks.empty()) return direct; + std::vector out; + for (auto const& e : blocks.front()) { + const bool same = std::ranges::all_of(blocks, [&](auto const& b) { + auto it = std::ranges::find(b, e.version, &XpkgVersionEntry::version); + return it != b.end() && it->alias == e.alias && it->sha256 == e.sha256 + && (e.alias || !e.sha256.empty()); + }); + if (same) out.push_back(e); + } + return out; +} + +namespace { +std::vector +entries_of_first_block(std::string_view luaContent, + const mcpp::toolchain::platform::PlatformKey& platformAxis) { // Locate `xpm = { ... = { ["X.Y.Z"] = {...}, ... } ... }`, the // platform being the first of the axis's keys the table has a block for. // We work on a sanitized copy so quoted version keys remain locatable @@ -1147,6 +1183,7 @@ list_xpkg_version_entries(std::string_view luaContent, } return versions; } +} // namespace std::vector list_xpkg_versions(std::string_view luaContent, diff --git a/modules/manifest/tests/test_compat_registry.cpp b/modules/manifest/tests/test_compat_registry.cpp index 55c781a19..e2dd3130e 100644 --- a/modules/manifest/tests/test_compat_registry.cpp +++ b/modules/manifest/tests/test_compat_registry.cpp @@ -1,9 +1,11 @@ -#include import std; +import mcpplibs.mtest; import mcpp.pm.compat.registry; import mcpp.version; +using namespace mcpplibs::mtest; + // THE DEPRECATION REGISTRY (mcpp#797, docs/51 §Deprecation). Every reading a // user can write and that is to be removed is a row; the warning, `mcpp why // compat`, docs/51 and the removal itself are derived from it. These tests @@ -15,64 +17,63 @@ using mcpp::pm::compat::kDeprecations; // THE FORCING FUNCTION. Once mcpp reaches a row's floor, the reading must be // gone from the code, not merely past its date: this fails the build of the // release that would otherwise ship it, naming the row. -TEST(CompatRegistry, NoReadingOutlivesItsFloor) { +UTest no_reading_outlives_its_floor{"CompatRegistry/NoReadingOutlivesItsFloor", [] { for (auto const& d : kDeprecations) - EXPECT_FALSE(mcpp::pm::compat::past_floor(d, mcpp::MCPP_VERSION)) + expect(!mcpp::pm::compat::past_floor(d, mcpp::MCPP_VERSION)) << "mcpp " << mcpp::MCPP_VERSION << " is at or past the floor " << d.floor << " of COMPAT(" << d.id << "): remove the reading and its row"; -} +}}; // docs/51: at least two whole calendar months between the release that // deprecates a reading and the floor from which it is removed. -TEST(CompatRegistry, EveryFloorLeavesTwoWholeMonths) { +UTest every_floor_leaves_two_whole_months{"CompatRegistry/EveryFloorLeavesTwoWholeMonths", [] { for (auto const& d : kDeprecations) { auto since = calver_month(d.since); auto floor = calver_month(d.floor); - ASSERT_TRUE(since && floor) << d.id; + expect(fatal(since && floor)) << d.id; const int months = (floor->first - since->first) * 12 + (floor->second - since->second); // The month of `since` itself is not whole: two more must follow it. - EXPECT_GE(months, 3) << d.id << ": " << d.since << " -> " << d.floor; + expect(ge(months, 3)) << d.id << ": " << d.since << " -> " << d.floor; } -} +}}; -TEST(CompatRegistry, RowsAreCompleteAndUnique) { +UTest rows_are_complete_and_unique{"CompatRegistry/RowsAreCompleteAndUnique", [] { std::set ids; for (auto const& d : kDeprecations) { - EXPECT_TRUE(ids.insert(d.id).second) << "duplicate id " << d.id; - EXPECT_FALSE(d.what.empty()) << d.id; - EXPECT_FALSE(d.canonical.empty()) << d.id; - EXPECT_FALSE(d.domain.empty()) << d.id; - EXPECT_TRUE(d.docs.starts_with("docs/")) << d.id; - EXPECT_NE(mcpp::pm::compat::find_deprecation(d.id), nullptr); + expect(bool(ids.insert(d.id).second)) << "duplicate id " << d.id; + expect(!d.what.empty()) << d.id; + expect(!d.canonical.empty()) << d.id; + expect(!d.domain.empty()) << d.id; + expect(d.docs.starts_with("docs/")) << d.id; + expect(ne(mcpp::pm::compat::find_deprecation(d.id), nullptr)); } - EXPECT_EQ(mcpp::pm::compat::find_deprecation("no-such-reading"), nullptr); -} + expect(eq(mcpp::pm::compat::find_deprecation("no-such-reading"), nullptr)); +}}; -TEST(CompatRegistry, TheClauseNamesBothVersions) { +UTest the_clause_names_both_versions{"CompatRegistry/TheClauseNamesBothVersions", [] { auto const* d = mcpp::pm::compat::find_deprecation("workspace-position"); - ASSERT_NE(d, nullptr); - EXPECT_EQ(mcpp::pm::compat::deprecation_clause(*d), - "deprecated since mcpp 2026.10.10.1 and removed from mcpp 2027.1"); -} + expect(fatal(ne(d, nullptr))); + expect(eq(mcpp::pm::compat::deprecation_clause(*d), "deprecated since mcpp 2026.10.10.1 and removed from mcpp 2027.1")); +}}; -TEST(CompatRegistry, CalverMonthOrdersVersionsAndFloors) { - EXPECT_EQ(calver_month("2026.10.11.1"), (std::pair{2026, 10})); - EXPECT_EQ(calver_month("2027.1"), (std::pair{2027, 1})); - EXPECT_FALSE(calver_month("1.0.0-rc").has_value()); +UTest calver_month_orders_versions_and_floors{"CompatRegistry/CalverMonthOrdersVersionsAndFloors", [] { + expect(eq(calver_month("2026.10.11.1"), (std::pair{2026, 10}))); + expect(eq(calver_month("2027.1"), (std::pair{2027, 1}))); + expect(!(calver_month("1.0.0-rc").has_value())); mcpp::pm::compat::Deprecation d{"x", "2026.10.1.1", "2027.1", "w", "c", "d", "docs/x"}; - EXPECT_FALSE(mcpp::pm::compat::past_floor(d, "2026.12.31.9")); - EXPECT_TRUE(mcpp::pm::compat::past_floor(d, "2027.1.2.1")); - EXPECT_TRUE(mcpp::pm::compat::past_floor(d, "2028.3.1.1")); -} + expect(!mcpp::pm::compat::past_floor(d, "2026.12.31.9")); + expect(bool(mcpp::pm::compat::past_floor(d, "2027.1.2.1"))); + expect(bool(mcpp::pm::compat::past_floor(d, "2028.3.1.1"))); +}}; -TEST(CompatRegistry, UsesAreRecordedOnceAndKeepTheWarning) { +UTest uses_are_recorded_once_and_keep_the_warning{"CompatRegistry/UsesAreRecordedOnceAndKeepTheWarning", [] { mcpp::pm::compat::record_use("workspace-position", "/w/mcpp.toml", "[toolchain]", false); mcpp::pm::compat::record_use("workspace-position", "/w/mcpp.toml", "[toolchain]", true); std::size_t n = 0; for (auto const& u : mcpp::pm::compat::recorded_uses()) if (u.where == "/w/mcpp.toml" && u.detail == "[toolchain]") { ++n; - EXPECT_TRUE(u.warned); + expect(bool(u.warned)); } - EXPECT_EQ(n, 1u); -} + expect(eq(n, 1u)); +}}; diff --git a/modules/manifest/tests/test_flag_words.cpp b/modules/manifest/tests/test_flag_words.cpp index e997fc238..41ae1c864 100644 --- a/modules/manifest/tests/test_flag_words.cpp +++ b/modules/manifest/tests/test_flag_words.cpp @@ -1,8 +1,10 @@ -#include import std; +import mcpplibs.mtest; import mcpp.manifest.flag_words; +using namespace mcpplibs::mtest; + // SUBSYSTEM-LEVEL. The words an element of a compile-flag list stands for are // part of what a manifest MEANS, so the syntax is stated here, with nothing // else present. That the build and the compile databases both use it is @@ -15,7 +17,7 @@ using Words = std::vector; // Each row is a spelling found in a manifest or a descriptor and the words it // stands for on every host. The first rows are the ones #655 measured. -TEST(FlagWords, TheSyntaxTable) { +UTest the_syntax_table{"FlagWords/TheSyntaxTable", [] { const std::vector> rows{ // libarchive's descriptor, the one quoted spelling the index publishes. {R"(-DPLATFORM_CONFIG_H=\"mcpp_libarchive_config.h\")", @@ -61,23 +63,23 @@ TEST(FlagWords, TheSyntaxTable) { {R"(-DV=a\)", {R"(-DV=a\)"}}, }; for (auto const& [element, words] : rows) - EXPECT_EQ(flag_words(element), words) << "element: " << element; -} + expect(eq(flag_words(element), words)) << "element: " << element; +}}; -TEST(FlagWords, AListIsTheConcatenationOfItsElements) { +UTest a_list_is_the_concatenation_of_its_elements{"FlagWords/AListIsTheConcatenationOfItsElements", [] { const std::vector list{"-include x.h", R"(-I"my dir")", "", "-O2"}; - EXPECT_EQ(flag_words(list), (Words{"-include", "x.h", "-Imy dir", "-O2"})); -} + expect(eq(flag_words(list), (Words{"-include", "x.h", "-Imy dir", "-O2"}))); +}}; -TEST(FlagWords, APlainWordIsItsOwnElement) { +UTest a_plain_word_is_its_own_element{"FlagWords/APlainWordIsItsOwnElement", [] { for (std::string_view w : {"-O2", "-DV=1", "-IC:/x", "/std:c++latest", "-Wl,-z,defs", "$x", R"(-IC:\sdk\include)", R"(C:\dir\)"}) - EXPECT_EQ(flag_element(w), w); -} + expect(eq(flag_element(w), w)); +}}; // The property the engine relies on when it inserts a value into a flag list: // the element it writes reads back as exactly that one word. -TEST(FlagWords, AnElementReadsBackAsItsWord) { +UTest an_element_reads_back_as_its_word{"FlagWords/AnElementReadsBackAsItsWord", [] { Words words{"", " ", "a b", "'", "\"", "\\", "a\\", "\\\\srv\\share", "it's", "-DV=\"def\"", "-DV='c'", "x\ty", "a\"b'c\\d e", "-DT=long long", "-DV=\"a b\"", "/DV=x y"}; @@ -95,15 +97,13 @@ TEST(FlagWords, AnElementReadsBackAsItsWord) { words.push_back(std::move(w)); } for (auto const& w : words) - EXPECT_EQ(flag_words(flag_element(w)), Words{w}) << "word: [" << w << "]"; -} + expect(eq(flag_words(flag_element(w)), Words{w})) << "word: [" << w << "]"; +}}; -TEST(HostCommandWords, PosixUndoesShellQuoting) { - EXPECT_EQ(host_command_words(R"(a 'b c' "d\"e" f\ g "h\x" -DV="mid")", false), - (Words{"a", "b c", "d\"e", "f g", "h\\x", "-DV=mid"})); -} +UTest posix_undoes_shell_quoting{"HostCommandWords/PosixUndoesShellQuoting", [] { + expect(eq(host_command_words(R"(a 'b c' "d\"e" f\ g "h\x" -DV="mid")", false), (Words{"a", "b c", "d\"e", "f g", "h\\x", "-DV=mid"}))); +}}; -TEST(HostCommandWords, WindowsFollowsTheRuntimeRules) { - EXPECT_EQ(host_command_words(R"(a "b c" "d\"e" C:\x\ "y\\" z\\\"w 'q')", true), - (Words{"a", "b c", "d\"e", "C:\\x\\", "y\\", "z\\\"w", "'q'"})); -} +UTest windows_follows_the_runtime_rules{"HostCommandWords/WindowsFollowsTheRuntimeRules", [] { + expect(eq(host_command_words(R"(a "b c" "d\"e" C:\x\ "y\\" z\\\"w 'q')", true), (Words{"a", "b c", "d\"e", "C:\\x\\", "y\\", "z\\\"w", "'q'"}))); +}}; diff --git a/modules/manifest/tests/test_selector_grammar.cpp b/modules/manifest/tests/test_selector_grammar.cpp index 7b32ff897..af0ad1d29 100644 --- a/modules/manifest/tests/test_selector_grammar.cpp +++ b/modules/manifest/tests/test_selector_grammar.cpp @@ -1,31 +1,33 @@ -#include import std; +import mcpplibs.mtest; import mcpp.pm.dependency_selector; +using namespace mcpplibs::mtest; + // SUBSYSTEM-LEVEL. A dependency selector is part of what a manifest MEANS, so // it lives here rather than in a package of its own -- and it is testable with // nothing else present, which is the check that the layering is real. -TEST(SelectorGrammar, ADottedSelectorSplitsIntoItsSegments) { +UTest a_dotted_selector_splits_into_its_segments{"SelectorGrammar/ADottedSelectorSplitsIntoItsSegments", [] { auto s = mcpp::pm::split_dependency_selector("mcpplibs.capi.lua"); - ASSERT_EQ(s.size(), 3u); - EXPECT_EQ(s[0], "mcpplibs"); - EXPECT_EQ(s[1], "capi"); - EXPECT_EQ(s[2], "lua"); -} + expect(fatal(eq(s.size(), 3u))); + expect(eq(s[0], "mcpplibs")); + expect(eq(s[1], "capi")); + expect(eq(s[2], "lua")); +}}; -TEST(SelectorGrammar, ABareNameIsOneSegmentAndNotAnError) { +UTest a_bare_name_is_one_segment_and_not_an_error{"SelectorGrammar/ABareNameIsOneSegmentAndNotAnError", [] { // The unqualified spelling is the common case and resolves through the // namespace ladder later. Treating it as malformed here would move that // decision into the grammar, where the ladder is not visible. auto s = mcpp::pm::split_dependency_selector("cmdline"); - ASSERT_EQ(s.size(), 1u); - EXPECT_EQ(s[0], "cmdline"); -} + expect(fatal(eq(s.size(), 1u))); + expect(eq(s[0], "cmdline")); +}}; -TEST(SelectorGrammar, JoiningIsTheInverseOfSplitting) { +UTest joining_is_the_inverse_of_splitting{"SelectorGrammar/JoiningIsTheInverseOfSplitting", [] { const std::string original = "mcpplibs.capi.lua"; auto s = mcpp::pm::split_dependency_selector(original); - EXPECT_EQ(mcpp::pm::join_dependency_segments(s, 0, s.size()), original); -} + expect(eq(mcpp::pm::join_dependency_segments(s, 0, s.size()), original)); +}}; diff --git a/modules/toolchain-model/build.mcpp b/modules/toolchain-model/build.mcpp new file mode 100644 index 000000000..b4b5b5e2d --- /dev/null +++ b/modules/toolchain-model/build.mcpp @@ -0,0 +1,218 @@ +// The built-in target rows and providers, compiled into the engine. +// +// data/builtin.toml is the source (the schema of `[[platform.define]]` and +// `[[platform.provider]]`, SPEC-016); this program writes the module +// mcpp.toolchain.builtin_targets, whose static tables the engine reads. A build +// therefore parses no file to know its targets. The output is written only when +// its content changes, so an unchanged table recompiles nothing. + +import std; +import mcpp; +import mcpp.libs.toml; + +namespace toml = mcpp::libs::toml; + +namespace { + +constexpr std::string_view kRowKeys[] = { + "triple", "tier", "note", "standard", "exact", "march", "mabi", "code-model", "multilib", "flags", + "link-emulation", +}; +constexpr std::string_view kProviderKeys[] = { + "name", "serves", "priority", "hosts", "probe", "toolchain", "pairs", "sysroot", "linkage", + "platform", +}; +constexpr std::string_view kTiers[] = { "verified", "preview", "planned" }; + +struct Failure { std::string text; }; + +std::string lit(std::string_view s) { + std::string o = "\""; + for (char c : s) { + if (c == '"' || c == '\\') o += '\\'; + o += c; + } + return o + "\""; +} + +std::string text(const toml::Table& t, std::string_view key, std::string_view where) { + auto it = t.find(key); + if (it == t.end()) return {}; + if (!it->second.is_string()) + throw Failure{std::format("{}: `{}` is not a string", where, key)}; + return it->second.as_string(); +} + +std::vector words(const toml::Table& t, std::string_view key, std::string_view where) { + std::vector out; + auto it = t.find(key); + if (it == t.end()) return out; + if (!it->second.is_array()) + throw Failure{std::format("{}: `{}` is not an array", where, key)}; + for (auto const& v : it->second.as_array()) { + if (!v.is_string()) throw Failure{std::format("{}: `{}` holds a non-string", where, key)}; + out.push_back(v.as_string()); + } + return out; +} + +void known_keys(const toml::Table& t, std::span keys, std::string_view where) { + for (auto const& [k, v] : t) + if (std::ranges::find(keys, k) == keys.end()) + throw Failure{std::format("{}: unknown key `{}`", where, k)}; +} + +// A named array of words in the generated module; the entry's field views it. +struct Arrays { + std::string decls; + int n = 0; + std::string add(const std::vector& v) { + if (v.empty()) return "{}"; + auto name = std::format("a{}", n++); + decls += std::format("inline constexpr std::string_view {}[] = {{", name); + for (std::size_t i = 0; i < v.size(); ++i) decls += (i ? ", " : "") + lit(v[i]); + decls += "};\n"; + return "d::" + name; + } +}; + +std::string row_entry(const toml::Table& t, Arrays& arrays, std::set& seen) { + known_keys(t, kRowKeys, "[[platform.define]]"); + const auto triple = text(t, "triple", "[[platform.define]]"); + const auto where = std::format("[[platform.define]] {}", triple); + if (triple.empty()) throw Failure{"[[platform.define]]: an entry without `triple`"}; + if (!seen.insert(triple).second) throw Failure{std::format("{}: defined twice", where)}; + const auto tier = text(t, "tier", where); + if (std::ranges::find(kTiers, tier) == std::end(kTiers)) + throw Failure{std::format("{}: tier `{}` is not verified, preview or planned", where, tier)}; + bool standard = true; + if (auto it = t.find("standard"); it != t.end()) { + if (!it->second.is_bool()) throw Failure{std::format("{}: `standard` is not a boolean", where)}; + standard = it->second.as_bool(); + } + // The segments of the canonical spelling: `arch-os` or `arch-os-env`. + std::vector seg; + for (std::size_t b = 0;;) { + const auto d = triple.find('-', b); + seg.push_back(triple.substr(b, d == std::string::npos ? std::string::npos : d - b)); + if (d == std::string::npos) break; + b = d + 1; + } + if (seg.size() < 2 || seg.size() > 3 || std::ranges::any_of(seg, [](auto const& x) { return x.empty(); })) + throw Failure{std::format("{}: a triple is `arch-os` or `arch-os-env`", where)}; + bool exact = false; + if (auto it = t.find("exact"); it != t.end()) { + if (!it->second.is_bool()) throw Failure{std::format("{}: `exact` is not a boolean", where)}; + exact = it->second.as_bool(); + } + return std::format( + " {{ .triple = {}, .arch = {}, .os = {}, .env = {}, .tier = {}, .note = {}, .standard = {}, " + ".exact = {}, .march = {}, .mabi = {}, " + ".codeModel = {}, .multilib = {}, .flags = {}, .linkEmulation = {} }},\n", + lit(triple), lit(seg[0]), lit(seg[1]), lit(seg.size() == 3 ? seg[2] : std::string{}), lit(tier), + lit(text(t, "note", where)), standard ? "true" : "false", exact ? "true" : "false", + lit(text(t, "march", where)), lit(text(t, "mabi", where)), + lit(text(t, "code-model", where)), lit(text(t, "multilib", where)), + arrays.add(words(t, "flags", where)), lit(text(t, "link-emulation", where))); +} + +std::string provider_entry(const toml::Table& t, Arrays& arrays, const std::set& rows) { + known_keys(t, kProviderKeys, "[[platform.provider]]"); + const auto name = text(t, "name", "[[platform.provider]]"); + const auto where = std::format("[[platform.provider]] {}", name); + if (name.empty()) throw Failure{"[[platform.provider]]: an entry without `name`"}; + const auto serves = words(t, "serves", where); + if (serves.empty()) throw Failure{std::format("{}: `serves` is empty", where)}; + // Every identity a built-in provider serves is a built-in row, so a typo in + // either table fails here rather than as a target nobody can select. + for (auto const& s : serves) { + const bool any = std::ranges::any_of(rows, [&](auto const& r) { + return r == s || (s.starts_with("*-") && r.size() > s.size() - 1 + && r.ends_with(s.substr(1))); + }); + if (!any) throw Failure{std::format("{}: serves `{}`, which no row defines", where, s)}; + } + std::int64_t priority = 0; + if (auto it = t.find("priority"); it != t.end()) { + if (!it->second.is_int()) throw Failure{std::format("{}: `priority` is not an integer", where)}; + priority = it->second.as_int(); + } + const auto linkage = text(t, "linkage", where); + if (!linkage.empty() && linkage != "static" && linkage != "dynamic") + throw Failure{std::format("{}: linkage `{}` is not static or dynamic", where, linkage)}; + return std::format( + " {{ .name = {}, .serves = {}, .priority = {}, .hosts = {}, .probe = {}, .toolchain = {}, " + ".pairs = {}, .sysroot = {}, .linkage = {}, .platform = {} }},\n", + lit(name), arrays.add(serves), priority, arrays.add(words(t, "hosts", where)), + lit(text(t, "probe", where)), lit(text(t, "toolchain", where)), + arrays.add(words(t, "pairs", where)), lit(text(t, "sysroot", where)), lit(linkage), + lit(text(t, "platform", where))); +} + +const toml::Array& entries(const toml::Document& doc, std::string_view path) { + static const toml::Array none; + auto* v = doc.get(path); + if (!v) return none; + if (!v->is_array()) throw Failure{std::format("`{}` is not an array of tables", path)}; + return v->as_array(); +} + +std::string generate(const toml::Document& doc) { + Arrays arrays; + std::set rowNames; + std::string rows, providers; + for (auto const& e : entries(doc, "platform.define")) { + if (!e.is_table()) throw Failure{"[[platform.define]]: an entry that is not a table"}; + rows += row_entry(e.as_table(), arrays, rowNames); + } + for (auto const& e : entries(doc, "platform.provider")) { + if (!e.is_table()) throw Failure{"[[platform.provider]]: an entry that is not a table"}; + providers += provider_entry(e.as_table(), arrays, rowNames); + } + return std::format( + "// Generated by modules/toolchain-model/build.mcpp from data/builtin.toml.\n" + "export module mcpp.toolchain.builtin_targets;\n\n" + "import std;\n" + "import mcpp.toolchain.target_schema;\n\n" + "namespace mcpp::toolchain::builtin::d {{\n{}}}\n\n" + "export namespace mcpp::toolchain::builtin {{\n\n" + "inline constexpr mcpp::toolchain::target::Row kRows[] = {{\n{}}};\n\n" + "inline constexpr mcpp::toolchain::target::Provider kProviders[] = {{\n{}}};\n\n" + "}} // namespace mcpp::toolchain::builtin\n", + arrays.decls, rows, providers); +} + +} // namespace + +int main() { + const std::filesystem::path source = std::filesystem::path(mcpp::manifest_dir()) / "data" / "builtin.toml"; + mcpp::rerun_if_changed(source.generic_string().c_str()); + std::ifstream in(source, std::ios::binary); + if (!in) { + std::println(std::cerr, "build.mcpp: cannot read {}", source.generic_string()); + return 1; + } + const std::string src{std::istreambuf_iterator(in), {}}; + auto doc = toml::parse(src); + if (!doc) { + std::println(std::cerr, "{}:{}: {}", source.generic_string(), doc.error().where.line, + doc.error().message); + return 1; + } + std::string out; + try { + out = generate(*doc); + } catch (const Failure& f) { + std::println(std::cerr, "{}: {}", source.generic_string(), f.text); + return 1; + } + const auto target = std::filesystem::path(mcpp::out_dir()) / "builtin_targets.cppm"; + std::string was; + if (std::ifstream old(target, std::ios::binary); old) was.assign(std::istreambuf_iterator(old), {}); + if (was != out) { + std::filesystem::create_directories(target.parent_path()); + std::ofstream(target, std::ios::binary) << out; + } + mcpp::generated(target.generic_string().c_str()); + return 0; +} diff --git a/modules/toolchain-model/data/builtin.toml b/modules/toolchain-model/data/builtin.toml new file mode 100644 index 000000000..07b132880 --- /dev/null +++ b/modules/toolchain-model/data/builtin.toml @@ -0,0 +1,476 @@ +# THE BUILT-IN TARGETS: THE ROWS mcpp KNOWS AND THE PROVIDERS THAT SERVE THEM. +# +# This file is data, written in the schema a platform package uses for its own +# rows and providers (`[[platform.define]]`, `[[platform.provider]]`; SPEC-016). +# The engine holds the mechanism -- the axes, the selection rule, the protocols -- +# and this file holds the facts. `build.mcpp` compiles it into static tables of the +# engine, so a build reads no file and parses nothing to know them. +# +# A ROW defines an identity: the triple a build produces, the output directory, +# the cache key and every `cfg(...)` answer. The order of the rows is the order of +# `mcpp toolchain list`, of a target set's members, and the tie-break of selection. +# +# A PROVIDER serves identities: the toolchain, the C library and the rest of the +# target's side, and the hosts on which it is available. A request names a +# pattern (`x86_64-windows`); its candidates are the providers of the rows it +# matches, ordered by `priority` (larger first), then by row, then by name; the +# first available on the host is taken (SPEC-016 §4). Several entries may carry +# one name when one provider serves rows with different facts. +# +# Row keys +# triple the canonical identity, `arch-os[-env]` +# tier verified (CI builds and runs it) | preview (builds and links) | +# planned (registered intent; refused) +# note the display note of `mcpp toolchain list` +# standard the identity is an industry-standard one; a combination that +# is not (musl on Windows) is mcpp's name for what is produced +# exact only a request that names the row whole reaches it; `arch-os` +# does not +# march, mabi, code-model, multilib, flags, link-emulation +# the ISA profile of a bare-metal row: `-march`, `-mabi`, +# `-mcmodel` (empty: the driver's default), the multilib +# directory of a C library, extra compile flags, and the +# `ld.lld -m` emulation when clang's driver cannot reach lld +# +# Provider keys +# name the provider's name; `builtin:` selects it +# serves the identities it serves; `*` stands for the architecture +# priority the order among the candidates of one request +# hosts where its payload exists: `-` of the build machine, +# `*` for any segment, `{arch}` for the target's architecture +# probe an engine probe that must also answer (`msvc`: an installed +# Visual Studio toolset) +# toolchain the toolchain it pins; empty: the host's default +# pairs the compiler families it pairs with; empty: any family, and a +# declared family that does not supply the C library is refused +# sysroot the C library it brings (a payload), or `toolchain` when the +# toolchain ships the whole sysroot itself +# linkage the default linkage, `static` or `dynamic` +# platform a platform package that implements it (SPEC-013) + +# ── Linux ──────────────────────────────────────────────────────────────────── + +[[platform.define]] +triple = "x86_64-linux-gnu" +tier = "verified" +standard = true + +[[platform.define]] +triple = "x86_64-linux-musl" +tier = "verified" +standard = true + +# The system's standard identity before its musl sibling, as on x86_64: a +# request `aarch64-linux` takes it where its provider is available. +# `preview` until the released engine is consumed on a clean ARM64 host +# (SPEC-009 §10.7). +[[platform.define]] +triple = "aarch64-linux-gnu" +tier = "preview" +standard = true + +[[platform.define]] +triple = "aarch64-linux-musl" +tier = "verified" +standard = true + +# ── Windows ────────────────────────────────────────────────────────────────── + +[[platform.define]] +triple = "x86_64-windows-gnu" +tier = "verified" +note = "PE" +standard = true + +# MUSL ON WINDOWS. No MinGW C runtime is linked into a build over openkal-musl +# (imports ntdll, KERNEL32, SHELL32; Itanium symbols only), so `gnu` would put the +# one thing the C library is not into the identity, the output directory, +# `cfg(env)` and the packed ABI tag. clang is still given `x86_64-w64-windows-gnu`, +# which selects the Itanium C++ ABI the C library was compiled for. +[[platform.define]] +triple = "x86_64-windows-musl" +tier = "preview" +note = "PE" +standard = false + +[[platform.define]] +triple = "x86_64-windows-msvc" +tier = "verified" +note = "PE" +standard = true + +# ── Apple, Linux continued ─────────────────────────────────────────────────── + +[[platform.define]] +triple = "aarch64-macos" +tier = "verified" +standard = true + +# MUSL ON macOS: openkal-platform's hosted part over openkal-macos. Its C library +# is openkal-musl rather than libSystem, which is what the env segment says. +[[platform.define]] +triple = "aarch64-macos-musl" +tier = "preview" +standard = false + +[[platform.define]] +triple = "riscv64-linux-musl" +tier = "planned" +standard = true + +[[platform.define]] +triple = "x86_64-macos" +tier = "planned" +standard = true + +[[platform.define]] +triple = "x86_64-macos-musl" +tier = "preview" +standard = false + +# ── Bare metal ─────────────────────────────────────────────────────────────── +# +# No loader, so static is the only linkage. The ISA profile is the row's: a +# correct object file from `--target ` alone, not every board. `x86-64` with +# a hyphen is the `-march` spelling of the `x86_64` segment; `aapcs` (not `lp64`) +# is the only `-mabi` aarch64 takes. `medany` because bare-metal RISC-V runs at +# 0x80000000, `small` for the low 2 GiB a `-kernel` image runs in; a higher-half +# kernel states `-mcmodel=kernel` itself. The multilib directory is +# `/` on RISC-V and the triple on ARM, where `-mabi` names the call +# standard and the float ABI is in the env segment. + +[[platform.define]] +triple = "riscv64-none-elf" +tier = "verified" +note = "bare" +standard = true +march = "rv64gc" +mabi = "lp64d" +code-model = "medany" +multilib = "rv64gc/lp64d" + +[[platform.define]] +triple = "riscv32-none-elf" +tier = "verified" +note = "bare" +standard = true +march = "rv32imac" +mabi = "ilp32" +code-model = "medany" +multilib = "rv32imac/ilp32" + +[[platform.define]] +triple = "aarch64-none-elf" +tier = "preview" +note = "bare" +standard = true +march = "armv8-a" +mabi = "aapcs" +code-model = "small" +multilib = "armv8-a/aapcs" + +# `-mno-red-zone`: on bare metal the processor pushes an interrupt frame into the +# 128 bytes below `rsp`, which a leaf function may use. clang's driver has no +# bare-metal x86_64 toolchain that reaches lld, so the row links with ld.lld +# directly under `elf_x86_64`. +[[platform.define]] +triple = "x86_64-none-elf" +tier = "preview" +note = "bare" +standard = true +march = "x86-64" +mabi = "sysv" +code-model = "small" +multilib = "x86-64/sysv" +flags = ["-mno-red-zone"] +link-emulation = "elf_x86_64" + +# `-mfpu=none` on the soft-float rows: the float ABI governs how values cross a +# call, not whether the compiler may use the FPU inside one (measured: `vmul.f32` +# under `thumbv7em-none-eabi`). + +[[platform.define]] +triple = "thumbv6m-none-eabi" +tier = "verified" +note = "bare" +standard = true +march = "armv6-m" +mabi = "aapcs" +multilib = "thumbv6m-none-eabi" +flags = ["-mfpu=none"] + +[[platform.define]] +triple = "thumbv7m-none-eabi" +tier = "verified" +note = "bare" +standard = true +march = "armv7-m" +mabi = "aapcs" +multilib = "thumbv7m-none-eabi" +flags = ["-mfpu=none"] + +[[platform.define]] +triple = "thumbv7em-none-eabi" +tier = "preview" +note = "bare" +standard = true +march = "armv7e-m" +mabi = "aapcs" +multilib = "thumbv7em-none-eabi" +flags = ["-mfpu=none"] + +[[platform.define]] +triple = "thumbv7em-none-eabihf" +tier = "verified" +note = "bare" +standard = true +march = "armv7e-m" +mabi = "aapcs" +multilib = "thumbv7em-none-eabihf" + +[[platform.define]] +triple = "thumbv8m.base-none-eabi" +tier = "preview" +note = "bare" +standard = true +march = "armv8-m.base" +mabi = "aapcs" +multilib = "thumbv8m.base-none-eabi" +flags = ["-mfpu=none"] + +[[platform.define]] +triple = "thumbv8m.main-none-eabi" +tier = "verified" +note = "bare" +standard = true +march = "armv8-m.main" +mabi = "aapcs" +multilib = "thumbv8m.main-none-eabi" +flags = ["-mfpu=none"] + +[[platform.define]] +triple = "thumbv8m.main-none-eabihf" +tier = "preview" +note = "bare" +standard = true +march = "armv8-m.main" +mabi = "aapcs" +multilib = "thumbv8m.main-none-eabihf" + +[[platform.define]] +triple = "armv7a-none-eabi" +tier = "verified" +note = "bare" +standard = true +march = "armv7-a" +mabi = "aapcs" +multilib = "armv7a-none-eabi" +flags = ["-mfpu=none"] + +[[platform.define]] +triple = "armv7a-none-eabihf" +tier = "verified" +note = "bare" +standard = true +march = "armv7-a" +mabi = "aapcs" +multilib = "armv7a-none-eabihf" + +# 32-BIT x86 (mcpp#911 G12). The code model is the driver's: clang refuses +# `-mcmodel=small` for every i386 spelling. clang's driver reaches no lld for a +# bare-metal x86 triple, so the row links with ld.lld directly under `elf_i386`. +[[platform.define]] +triple = "i686-none-elf" +tier = "preview" +note = "bare" +standard = true +march = "i686" +link-emulation = "elf_i386" + +# ── Android, iOS, the Web, ok images ───────────────────────────────────────── +# +# Android is Linux with bionic; `x86_64-linux` asks for a C library of Linux, not +# for an Android application, so the Android rows are reached by name only. + +[[platform.define]] +triple = "aarch64-linux-android" +tier = "verified" +standard = true +exact = true + +[[platform.define]] +triple = "x86_64-linux-android" +tier = "verified" +standard = true +exact = true + +[[platform.define]] +triple = "aarch64-ios" +tier = "preview" +standard = true + +[[platform.define]] +triple = "aarch64-ios-sim" +tier = "verified" +standard = true + +[[platform.define]] +triple = "x86_64-ios-sim" +tier = "verified" +standard = true + +# A new arch, a new os and a new object format; `em++` links `import std` with +# the standard library surface the toolchain ships. One module plus its +# JavaScript, no shared objects. +[[platform.define]] +triple = "wasm32-emscripten" +tier = "verified" +note = "wasm" +standard = true + +# OK IMAGES (openkal SPEC clause 12): one artifact per architecture, run by +# `okrun` on Linux, macOS and Windows of that architecture. +[[platform.define]] +triple = "x86_64-openkal" +tier = "verified" +note = "ok" +standard = true + +[[platform.define]] +triple = "aarch64-openkal" +tier = "verified" +note = "ok" +standard = true + +# ── Providers ──────────────────────────────────────────────────────────────── +# +# Priorities: a system's own toolchain and C library 100; openkal-platform, +# which serves the musl identities of every system, 50; other conventions 10. + +# glibc from the payload, at the host's default toolchain natively. A cross to +# another architecture's glibc pins llvm; the payload exists for the host's arch. +[[platform.provider]] +name = "glibc" +serves = ["x86_64-linux-gnu"] +priority = 100 +hosts = ["linux-{arch}"] + +[[platform.provider]] +name = "glibc" +serves = ["aarch64-linux-gnu"] +priority = 100 +hosts = ["linux-{arch}"] +toolchain = "llvm@23.1.3" + +# The musl cross payloads: native on each Linux arch, every arch from x86_64, +# and the Canadian cross on a Windows host of the same arch. +[[platform.provider]] +name = "musl-gcc" +serves = ["x86_64-linux-musl", "aarch64-linux-musl"] +priority = 100 +hosts = ["linux-{arch}", "linux-x86_64", "windows-{arch}"] +toolchain = "gcc@16.1.0" +linkage = "static" + +[[platform.provider]] +name = "musl-gcc" +serves = ["riscv64-linux-musl"] +priority = 100 +hosts = ["linux-{arch}", "linux-x86_64"] +linkage = "static" + +# MinGW-w64: gcc, or clang with the payload as its sysroot (SPEC-016 §6). +[[platform.provider]] +name = "mingw" +serves = ["x86_64-windows-gnu"] +priority = 10 +hosts = ["windows-*", "linux-x86_64"] +toolchain = "gcc@16.1.0" +pairs = ["gcc", "llvm"] +linkage = "static" + +[[platform.provider]] +name = "msvc" +serves = ["x86_64-windows-msvc"] +priority = 100 +hosts = ["windows-*"] +probe = "msvc" + +[[platform.provider]] +name = "apple" +serves = ["aarch64-macos", "x86_64-macos"] +priority = 100 +hosts = ["macos-*"] + +[[platform.provider]] +name = "apple" +serves = ["aarch64-ios", "aarch64-ios-sim", "x86_64-ios-sim"] +priority = 100 +hosts = ["macos-*"] +toolchain = "llvm@23.1.3" + +# The C library of a bare-metal RISC-V row is the target's, like the compiler: +# no bare-metal package has to name one. +[[platform.provider]] +name = "bare-metal" +serves = ["riscv64-none-elf", "riscv32-none-elf"] +priority = 100 +hosts = ["*"] +toolchain = "llvm@23.1.3" +pairs = ["llvm"] +sysroot = "xim:picolibc-riscv@1.8.12" +linkage = "static" + +[[platform.provider]] +name = "bare-metal" +serves = ["aarch64-none-elf", "x86_64-none-elf", "i686-none-elf", "thumbv6m-none-eabi", + "thumbv7m-none-eabi", "thumbv7em-none-eabi", "thumbv7em-none-eabihf", + "thumbv8m.base-none-eabi", "thumbv8m.main-none-eabi", "thumbv8m.main-none-eabihf", + "armv7a-none-eabi", "armv7a-none-eabihf"] +priority = 100 +hosts = ["*"] +toolchain = "llvm@23.1.3" +pairs = ["llvm"] +linkage = "static" + +# bionic is inside the NDK; no package adds it to another compiler. +[[platform.provider]] +name = "android-ndk" +serves = ["*-linux-android"] +priority = 100 +hosts = ["*"] +toolchain = "android-ndk@30.0.16248370" +pairs = ["android-ndk"] +sysroot = "toolchain" + +[[platform.provider]] +name = "emsdk" +serves = ["wasm32-emscripten"] +priority = 100 +hosts = ["*"] +toolchain = "emsdk@6.0.9" +pairs = ["emsdk"] +sysroot = "toolchain" +linkage = "static" + +# openkal-platform: the ok images, and through its hosted part the musl identity +# of each system it implements (its C library is openkal-musl). +[[platform.provider]] +name = "openkal" +serves = ["*-openkal"] +priority = 100 +hosts = ["*"] +toolchain = "llvm@23.1.3" +pairs = ["llvm"] +linkage = "static" +platform = "mcpplibs.openkal-platform@0.24.4" + +[[platform.provider]] +name = "openkal" +serves = ["x86_64-windows-musl", "*-linux-musl", "*-macos-musl"] +priority = 50 +hosts = ["*"] +toolchain = "llvm@23.1.3" +pairs = ["llvm"] +linkage = "static" +platform = "mcpplibs.openkal-platform@0.24.4" diff --git a/modules/toolchain-model/mcpp.toml b/modules/toolchain-model/mcpp.toml index a2bf5ff90..097c55ace 100644 --- a/modules/toolchain-model/mcpp.toml +++ b/modules/toolchain-model/mcpp.toml @@ -23,10 +23,15 @@ license = "Apache-2.0" mcpp.invocation = { path = "../invocation" } mcpp.versioning = { path = "../versioning" } -mcpplibs.os = "0.4.0" +mcpplibs.os = "0.4.1" # Subsystem-level tests. They see only this package, which is what lets them # state its contract in isolation; the root `tests/unit/` states the facts two # layers must agree on. [dev-dependencies] -compat.gtest = "1.15.2" +mcpplibs.mtest = "0.5.2" + +# build.mcpp compiles data/builtin.toml (the built-in target rows and providers) +# into a module of static tables, with mcpp's own TOML parser. +[build-dependencies] +mcpp.libs = { path = "../libs" } diff --git a/modules/toolchain-model/src/model.cppm b/modules/toolchain-model/src/model.cppm index 23870f190..50473e5ca 100644 --- a/modules/toolchain-model/src/model.cppm +++ b/modules/toolchain-model/src/model.cppm @@ -118,6 +118,15 @@ struct Toolchain { // and run-time type information off for every unit — right when nothing can // throw, and wrong when something can. bool targetCxxRuntime = false; + // THE C ENVIRONMENT IS HOSTED (mcpp#911 G3): a sysroot that provides + // `hosted-standard-library`, or a platform provider that states `hosted = + // true`, gives a bare-metal row a C library with `main`; `-ffreestanding` is + // then not added and `main` is not treated as freestanding. + bool hostedC = false; + // THE PAYLOAD A PAIRED PROVIDER SUPPLIES THE SYSTEM FROM (SPEC-016 §6): + // clang aimed at `x86_64-windows-gnu` takes the MinGW-w64 payload as its + // sysroot, C library and C++ runtime (libstdc++). Empty otherwise. + std::filesystem::path pairedSysroot; // DOES THE TARGET'S C LIBRARY COME FROM A DIRECTORY THAT EXISTED // BEFORE DEPENDENCY RESOLUTION? — `TargetSide::cAbi.prebuilt()`, recorded diff --git a/modules/toolchain-model/src/target_rows.cppm b/modules/toolchain-model/src/target_rows.cppm new file mode 100644 index 000000000..57b2a5bc3 --- /dev/null +++ b/modules/toolchain-model/src/target_rows.cppm @@ -0,0 +1,155 @@ +// mcpp.toolchain.target_rows — the rows and providers a process knows. +// +// The built-in table (data/builtin.toml) and the definitions of the platforms a +// build has read (`[[platform.define]]`, `[[platform.provider]]`; SPEC-016 §2, +// §3). A platform's rows extend the vocabulary for every later question in the +// process: the triple parser accepts the os and env a defined row names, and +// `cfg(os = ...)` evaluates against the row. A definition stays for the process, +// because one process plans one build and the configurations it derives from it. + +export module mcpp.toolchain.target_rows; + +import std; +import mcpp.toolchain.target_schema; +import mcpp.toolchain.builtin_targets; + +export namespace mcpp::toolchain::target { + +// THE STRINGS A DEFINITION OWNS. A row and a provider view strings; a defined +// one views the strings of the set that read it, which lives as long as the +// process does once it is defined. +struct DefinitionSet { + std::string source; // the platform package that stated it + // A row assumed for a request no definition knows yet, while the platform + // that may define it is fetched; a definition of the same row replaces it. + bool provisional = false; + std::deque strings; // stable addresses + std::deque> lists; + std::vector rows; + std::vector providers; + + std::string_view keep(std::string s) { return strings.emplace_back(std::move(s)); } + std::span keep(const std::vector& v) { + auto& out = lists.emplace_back(); + for (auto const& s : v) out.push_back(keep(s)); + return out; + } +}; + +// Registers a platform's definitions. A row the table or an earlier set already +// defines is not defined again; the answer is the first definition's. +void define(DefinitionSet set); + +// Every row: the built-in ones in the table's order, then the defined ones. +std::vector rows(); +std::vector providers(); +const Row* find_row(std::string_view triple); + +// Whether a defined row names this os, or this env under this os: the words the +// parser accepts beyond its own. +bool defines_os(std::string_view os); +bool defines_env(std::string_view os, std::string_view env); + +// The row a platform defined for this triple; null when none did (the cheap +// answer while nothing is defined, which is every build without such a platform). +const Row* defined_row(std::string_view triple); + +} // namespace mcpp::toolchain::target + +namespace mcpp::toolchain::target { + +namespace { + +struct Registry { + std::mutex lock; + std::deque sets; // stable addresses +}; + +std::atomic g_anyDefined{false}; + +Registry& registry() { + static Registry r; + return r; +} + +} // namespace + +void define(DefinitionSet set) { + auto& r = registry(); + std::lock_guard g(r.lock); + // A source defines once per process; a provisional row gives way to a real one. + for (auto const& s : r.sets) + if (!set.source.empty() && s.source == set.source && !s.provisional && !set.provisional) return; + if (!set.provisional) + for (auto& s : r.sets) + if (s.provisional) + std::erase_if(s.rows, [&](const Row& old) { + return std::ranges::any_of(set.rows, [&](const Row& n) { return n.triple == old.triple; }); + }); + std::erase_if(set.rows, [&](const Row& row) { + for (auto const& b : mcpp::toolchain::builtin::kRows) + if (b.triple == row.triple) return true; + for (auto const& s : r.sets) + for (auto const& d : s.rows) + if (d.triple == row.triple) return true; + return false; + }); + r.sets.push_back(std::move(set)); + g_anyDefined = true; +} + +const Row* defined_row(std::string_view triple) { + if (!g_anyDefined) return nullptr; + auto& r = registry(); + std::lock_guard g(r.lock); + for (auto const& s : r.sets) + for (auto const& d : s.rows) + if (d.triple == triple) return &d; + return nullptr; +} + +std::vector rows() { + std::vector out; + for (auto const& b : mcpp::toolchain::builtin::kRows) out.push_back(&b); + auto& r = registry(); + std::lock_guard g(r.lock); + for (auto const& s : r.sets) + for (auto const& d : s.rows) out.push_back(&d); + return out; +} + +std::vector providers() { + std::vector out; + for (auto const& b : mcpp::toolchain::builtin::kProviders) out.push_back(&b); + auto& r = registry(); + std::lock_guard g(r.lock); + for (auto const& s : r.sets) + for (auto const& p : s.providers) out.push_back(&p); + return out; +} + +const Row* find_row(std::string_view triple) { + for (auto const* r : rows()) + if (r->triple == triple) return r; + return nullptr; +} + +bool defines_os(std::string_view os) { + auto& r = registry(); + std::lock_guard g(r.lock); + for (auto const& s : r.sets) + for (auto const& d : s.rows) + if (d.os == os) return true; + return false; +} + +bool defines_env(std::string_view os, std::string_view env) { + auto& r = registry(); + std::lock_guard g(r.lock); + for (auto const& s : r.sets) + for (auto const& d : s.rows) + if (d.os == os && d.env == env) return true; + return false; +} + +} // namespace mcpp::toolchain::target diff --git a/modules/toolchain-model/src/target_schema.cppm b/modules/toolchain-model/src/target_schema.cppm new file mode 100644 index 000000000..a36e0a4a1 --- /dev/null +++ b/modules/toolchain-model/src/target_schema.cppm @@ -0,0 +1,108 @@ +// mcpp.toolchain.target_schema — the shape of a target row and of a provider. +// +// One schema for both sources of rows and providers: the built-in table +// (data/builtin.toml, compiled into mcpp.toolchain.builtin_targets by this +// package's build.mcpp) and a platform package's `[[platform.define]]` and +// `[[platform.provider]]` (SPEC-016). A built-in entry views static storage; an +// entry read from a platform package views the strings of the definition set +// that owns it. + +export module mcpp.toolchain.target_schema; + +import std; + +export namespace mcpp::toolchain::target { + +// A ROW DEFINES AN IDENTITY (SPEC-016 §2). The fields after `standard` are the +// facts of a row that the engine's conventions for a built-in system do not +// already answer: the ISA profile of a bare-metal row, and -- for a row a +// platform defines -- everything a build of an unfamiliar system needs. +struct Row { + std::string_view triple; // canonical `arch-os[-env]` + std::string_view arch, os, env; // its segments + std::string_view tier; // verified | preview | planned + std::string_view note; // display note + bool standard = true; // an industry-standard identity + bool exact = false; // reached only by a request that names it whole + std::string_view march; // -march= of a bare-metal row + std::string_view mabi; // -mabi=; empty: none given + std::string_view codeModel; // -mcmodel=; empty: the driver's + std::string_view multilib; // a C library's multilib directory + std::span flags; // extra compile flags + std::string_view linkEmulation; // `ld.lld -m`, when the driver reaches no lld + // The facts of a defined row (a platform's). Empty on a built-in row, + // whose system's conventions answer them. + std::string_view clangTriple; // `{version}` / `{version:}` + std::string_view objectFormat; // elf | mach-o | pe | wasm + std::string_view family; // unix | windows | "" + std::string_view dataModel; // lp64 | llp64 | ilp32 + std::string_view exeSuffix; + std::string_view sharedSuffix; + std::string_view loader; // the program interpreter a dynamic program names + std::span descriptorKeys; + // The system the artifact runs on: the row's os by default, `loader` for an + // image a loader binds, `none` for bare metal. + std::string_view execution; + bool defined = false; // stated by a platform package +}; + +// A PROVIDER SERVES IDENTITIES (SPEC-016 §3). Several entries may share a name +// when one provider serves rows whose facts differ. +struct Provider { + std::string_view name; + std::span serves; // `*` stands for the arch + int priority = 0; + std::span hosts; // `-`, `*`, `{arch}` + std::string_view probe; // msvc + std::string_view toolchain; // empty: the host's default + std::span pairs; // empty: any family + std::string_view sysroot; // a payload, or `toolchain` + std::string_view linkage; // static | dynamic + std::string_view platform; // a platform package reference +}; + +// THE PATTERN OF `serves`: segment by segment, `*` standing for any one segment +// (`*-linux-musl`, `x86_64-linux-*`); a pattern and a triple of different +// numbers of segments do not match. +constexpr bool serves_triple(std::string_view pattern, std::string_view triple) { + if (pattern == triple) return true; + for (;;) { + const auto pd = pattern.find('-'), td = triple.find('-'); + const auto ps = pattern.substr(0, pd), ts = triple.substr(0, td); + if (ps != "*" && ps != ts) return false; + if ((pd == std::string_view::npos) != (td == std::string_view::npos)) return false; + if (pd == std::string_view::npos) return true; + pattern.remove_prefix(pd + 1); + triple.remove_prefix(td + 1); + } +} + +constexpr bool serves(const Provider& p, std::string_view triple) { + for (auto s : p.serves) + if (serves_triple(s, triple)) return true; + return false; +} + +// WHERE A PROVIDER'S PAYLOAD EXISTS. `host` is `-` of the build machine; +// a pattern segment `*` matches anything, `{arch}` the target's architecture. +constexpr bool host_matches(std::string_view pattern, std::string_view host, + std::string_view targetArch) { + if (pattern == "*") return true; + const auto pd = pattern.find('-'); + const auto hd = host.find('-'); + if (pd == std::string_view::npos || hd == std::string_view::npos) return pattern == host; + const auto po = pattern.substr(0, pd), pa = pattern.substr(pd + 1); + const auto ho = host.substr(0, hd), ha = host.substr(hd + 1); + const bool os = po == "*" || po == ho; + const bool arch = pa == "*" || pa == ha || (pa == "{arch}" && ha == targetArch); + return os && arch; +} + +constexpr bool available_on(const Provider& p, std::string_view host, std::string_view targetArch) { + for (auto h : p.hosts) + if (host_matches(h, host, targetArch)) return true; + return false; +} + +} // namespace mcpp::toolchain::target + diff --git a/modules/toolchain-model/src/target_select.cppm b/modules/toolchain-model/src/target_select.cppm new file mode 100644 index 000000000..564a91831 --- /dev/null +++ b/modules/toolchain-model/src/target_select.cppm @@ -0,0 +1,155 @@ +// mcpp.toolchain.target_select — from a request to an identity and its provider. +// +// THE ONE RULE OF SPEC-016 §4. Explicit choices win: the caller passes a named +// provider or a declared compiler family, and only candidates that agree with +// them remain. Otherwise the candidates of the request -- each row the request +// matches, with each provider that serves it -- are ordered by priority (larger +// first), then by row, then by provider name, and the first one available on +// this host is taken. Nothing here names a system, a C library or a provider: +// the rows and providers are data (mcpp.toolchain.target_rows), and so are the +// priorities, which a project or a machine may change. +// +// A request names a pattern. One that writes an env segment matches that row +// only; one that writes `arch-os` matches every row of that system except those +// reached only by name (`exact`), so `x86_64-windows` is the MSVC, MinGW and +// musl identities and `aarch64-macos` is both the native identity and its musl +// sibling. When no candidate is available on the host the first is taken +// anyway, unavailable, and what refuses it is the toolchain or target-side step +// that finds the missing piece, as before a provider was a concept. + +export module mcpp.toolchain.target_select; + +import std; +import mcpp.toolchain.target_schema; +import mcpp.toolchain.target_rows; + +export namespace mcpp::toolchain::target { + +struct Candidate { + const Row* row = nullptr; + const Provider* provider = nullptr; // null: no provider serves the row + int priority = 0; // after the overrides + bool available = false; + std::string why; // why it is not available here +}; + +struct Context { + std::string host; // `-` of the build machine + // Answers an engine probe a provider names (`msvc`). + std::function probe; + // A project's or a machine's priority for a provider on a row, if one is set. + std::function(const Row&, std::string_view provider)> priority; + // A declared compiler family: only providers that pair with it serve. + std::string family; + // A provider chosen by name (`builtin:`): only it serves. + std::string provider; + // `--platform none`: no provider a platform package implements serves. + bool platformPackages = true; +}; + +struct Selection { + std::string request; // as written + const Row* row = nullptr; // the identity + const Provider* provider = nullptr; + int priority = 0; + bool fallback = false; // the first candidate is not available here + std::vector candidates; // in order + std::string identity() const { return row ? std::string(row->triple) : std::string{}; } +}; + +// The rows a request reaches, in the table's order. `arch`, `os`, `env` are the +// request's canonical segments; `envWritten` whether the request wrote the env. +std::vector rows_of(std::string_view arch, std::string_view os, std::string_view env, + bool envWritten); + +// The selection. Empty `candidates` when the request reaches no row. +Selection select(std::string_view request, std::string_view arch, std::string_view os, + std::string_view env, bool envWritten, const Context& ctx); + +// `-` of this machine, in the words `hosts` uses. +std::string host_pair(std::string_view os, std::string_view arch); + +} // namespace mcpp::toolchain::target + +namespace mcpp::toolchain::target { + +std::vector rows_of(std::string_view arch, std::string_view os, std::string_view env, + bool envWritten) { + std::vector out; + for (auto const* r : rows()) { + if (r->arch != arch || r->os != os) continue; + if (envWritten ? r->env != env : (r->exact && r->env != env)) continue; + out.push_back(r); + } + return out; +} + +std::string host_pair(std::string_view os, std::string_view arch) { + return std::format("{}-{}", os, arch); +} + +Selection select(std::string_view request, std::string_view arch, std::string_view os, + std::string_view env, bool envWritten, const Context& ctx) { + Selection s; + s.request = std::string(request); + const auto reached = rows_of(arch, os, env, envWritten); + // A planned row is a candidate only when the request reaches nothing else: + // it is then the answer, and the tier check refuses it by name. + const bool onlyPlanned = std::ranges::all_of(reached, [](const Row* r) { return r->tier == "planned"; }); + const auto all = providers(); + for (std::size_t i = 0; i < reached.size(); ++i) { + const Row& row = *reached[i]; + if (row.tier == "planned" && !onlyPlanned) continue; + bool served = false; + for (auto const* p : all) { + if (!serves(*p, row.triple)) continue; + if (!ctx.provider.empty() && p->name != ctx.provider) continue; + if (!ctx.platformPackages && !p->platform.empty()) continue; + served = true; + Candidate c{ &row, p, p->priority, true, {} }; + if (ctx.priority) + if (auto n = ctx.priority(row, p->name)) c.priority = *n; + if (!ctx.family.empty() && !p->pairs.empty() + && std::ranges::find(p->pairs, ctx.family) == p->pairs.end()) { + c.available = false; + std::string pairs; + for (auto f : p->pairs) pairs += std::format("{}{}", pairs.empty() ? "" : ", ", f); + c.why = std::format("{} pairs with {}, not {}", p->name, pairs, ctx.family); + } else if (!available_on(*p, ctx.host, row.arch)) { + c.available = false; + c.why = std::format("{} is not available on {}", p->name, ctx.host); + } else if (!p->probe.empty() && !(ctx.probe && ctx.probe(p->probe))) { + c.available = false; + c.why = std::format("{} is not installed here", p->name); + } else if (row.tier == "planned") { + c.available = false; + c.why = std::format("{} is planned", row.triple); + } + s.candidates.push_back(std::move(c)); + } + // A row no provider serves (a platform's row before its provider is + // read, or a provider chosen by name that does not serve it) is still a + // candidate when nothing was chosen by name: the platform serves it. + if (!served && ctx.provider.empty()) + s.candidates.push_back(Candidate{ &row, nullptr, 0, true, {} }); + } + std::ranges::stable_sort(s.candidates, [&](const Candidate& a, const Candidate& b) { + if (a.priority != b.priority) return a.priority > b.priority; + const auto ra = std::ranges::find(reached, a.row) - reached.begin(); + const auto rb = std::ranges::find(reached, b.row) - reached.begin(); + if (ra != rb) return ra < rb; + const auto na = a.provider ? a.provider->name : std::string_view{}; + const auto nb = b.provider ? b.provider->name : std::string_view{}; + return na < nb; + }); + if (s.candidates.empty()) return s; + auto chosen = std::ranges::find_if(s.candidates, [](const Candidate& c) { return c.available; }); + if (chosen == s.candidates.end()) chosen = s.candidates.begin(); + s.row = chosen->row; + s.provider = chosen->provider; + s.priority = chosen->priority; + s.fallback = chosen != s.candidates.begin(); + return s; +} + +} // namespace mcpp::toolchain::target diff --git a/modules/toolchain-model/src/triple.cppm b/modules/toolchain-model/src/triple.cppm index 58856106b..f65893f57 100644 --- a/modules/toolchain-model/src/triple.cppm +++ b/modules/toolchain-model/src/triple.cppm @@ -27,6 +27,9 @@ export module mcpp.toolchain.triple; import std; import mcpplibs.os.platform; import mcpp.toolchain.platform; +import mcpp.toolchain.target_schema; +import mcpp.toolchain.target_rows; +import mcpp.toolchain.builtin_targets; export namespace mcpp::toolchain::triple { @@ -151,6 +154,13 @@ struct Triple { // would let a caller supply one platform's answer for the other's. std::string llvm_triple(std::string_view minPlatformVersion = {}) const { if (empty()) return {}; + // A ROW A PLATFORM DEFINES STATES ITS OWN: `clang-triple`, with + // `{version}` the project's minimum system version and + // `{version:}` the row's when the project states none. + if (auto* d = defined()) { + if (d->clangTriple.empty()) return str(); + return expand_version(d->clangTriple, minPlatformVersion); + } if (os == "macos") { // Apple spells the 64-bit ARM architecture `arm64`, and the OS // component carries the deployment target: `arm64-apple-macos14`. @@ -248,6 +258,12 @@ struct Triple { // fallback would otherwise claim ELF for it -- the silent wrong answer this // whole axis exists to remove. ObjectFormat object_format() const { + if (auto* d = defined()) { + if (d->objectFormat == "pe") return ObjectFormat::Pe; + if (d->objectFormat == "mach-o") return ObjectFormat::MachO; + if (d->objectFormat == "wasm") return ObjectFormat::Wasm; + return ObjectFormat::Elf; + } if (os == "windows") return ObjectFormat::Pe; if (os == "macos" || os == "ios") return ObjectFormat::MachO; if (arch.starts_with("wasm")) return ObjectFormat::Wasm; @@ -286,9 +302,7 @@ struct Triple { // because the target decides the payload (see registry.cppm's // to_xim_package). If a row ever gains a second toolchain that does NOT // ship a sysroot, this has to move onto the toolchain. - bool has_own_sysroot() const { - return os == "emscripten" || os == "android" || env == "android"; - } + bool has_own_sysroot() const; // after the table: the row's provider answers // APPLE, AS ONE QUESTION. `os == "macos"` was the whole of it while macOS // was the only Apple row; iOS shares the object format, the linker, the @@ -377,10 +391,7 @@ struct Triple { // serve the row. `-D__BIONIC_CTYPE_INLINE=` and an API level fused onto // the triple are the NDK's own requirements, not flags a user can be // expected to supply to a different compiler. - bool pin_is_capability() const { - return is_freestanding() || is_openkal() || (is_pe() && is_musl()) || is_wasm() - || is_android(); - } + bool pin_is_capability() const; // after the table: the row's provider answers // cfg() `family` dimension: unix | windows. // @@ -392,6 +403,7 @@ struct Triple { // nothing, unchanged: `cfg(unix)` on bare metal would be false in a way no // source could act on. std::string family() const { + if (auto* d = defined()) return std::string(d->family); if (os == "windows") return "windows"; if (os == "linux" || os == "macos" || os == "ios" || os == "emscripten") return "unix"; @@ -442,6 +454,24 @@ struct Triple { // unequal to the same triple derived by `host_triple`, and the first thing // that breaks is the `host` tag in `mcpp toolchain list`, which compares // exactly those two. + // The row a platform defined for this triple, if one did (SPEC-016 §2): + // its facts answer where the engine's conventions for a system know none. + const mcpp::toolchain::target::Row* defined() const { + return mcpp::toolchain::target::defined_row(str()); + } + + static std::string expand_version(std::string_view pattern, std::string_view version) { + std::string out(pattern); + const auto at = out.find("{version"); + if (at == std::string::npos) return out; + const auto end = out.find('}', at); + if (end == std::string::npos) return out; + std::string fallback; + if (out[at + 8] == ':') fallback = out.substr(at + 9, end - at - 9); + out.replace(at, end - at + 1, version.empty() ? fallback : std::string(version)); + return out; + } + bool operator==(const Triple& o) const { return arch == o.arch && os == o.os && env == o.env; } @@ -490,429 +520,72 @@ struct TargetInfo { // another with `[target.] platform`, a machine in config.toml, // one invocation with `--platform`. std::string_view platform = {}; + // The row this view is of, and its default provider (null: none serves it). + const mcpp::toolchain::target::Row* row = nullptr; + const mcpp::toolchain::target::Provider* provider = nullptr; }; +// THE TABLE IS DATA (design 2026-11-09; SPEC-016). The rows and the providers +// that serve them are data/builtin.toml, compiled into +// mcpp.toolchain.builtin_targets by this package's build.mcpp; what this view +// adds is each row's DEFAULT provider -- the first built-in provider serving it, +// by priority and then by order -- whose toolchain, C library, linkage and +// platform are the columns every consumer of the table reads. Which provider a +// build USES is a selection (mcpp.toolchain.target_select), which also asks +// whether each candidate is available on the host; this view does not. +// // (note deliberately excludes "static" — the display layer derives that tag // from defaultStatic, so listing it here would duplicate it.) -inline constexpr TargetInfo kKnownTargets[] = { - // canonical tier note pin sysroot defaultStatic - { "x86_64-linux-gnu", "verified", "", "", "", false }, - { "x86_64-linux-musl", "verified", "", "gcc@16.1.0", "", true }, - { "aarch64-linux-musl", "verified", "", "gcc@16.1.0", "", true }, - { "x86_64-windows-gnu", "verified", "PE", "gcc@16.1.0", "", true }, - // musl ON WINDOWS. IT EXISTS, AND UNTIL THIS ROW mcpp HAD NO NAME FOR IT. - // - // LLVM's triple vocabulary offers `gnu` and `msvc` for Windows and both - // name an ABI, so a reader concludes there is no third possibility and - // calls a musl-based Windows build `-gnu`. The artefact disagrees. Measured - // on one built over `openkal-musl`: - // - // imports ntdll, KERNEL32, SHELL32 — no msvcrt, no ucrtbase - // `_Z…` symbols 4507 `?…` symbols 0 - // - // No MinGW C runtime is linked. That is musl on Windows, and calling it - // `gnu` put the one thing the C library is not into its identity, its - // output directory, its `cfg(env = …)` and its packed ABI tag. - // - // THE FIX IS A NAME, NOT A MECHANISM, AND THE REASON THE MISTAKE HELD SO - // LONG IS WORTH RECORDING. "LLVM cannot spell x86_64-windows-musl" is true - // and is about the string handed to CLANG. mcpp's canonical form is a - // different string — the build report prints both, either side of an arrow: - // - // Target x86_64-windows-gnu → x86_64-w64-windows-gnu - // ^ mcpp's identity ^ what clang is given - // - // Letting the compiler's vocabulary bound mcpp's own merged two axes into - // one. `llvm_triple()` already sends every non-MSVC Windows target to - // `…-w64-windows-gnu`, and that spelling is correct there and stays: it - // selects the Itanium C++ ABI, which is the ABI this C library was compiled - // for. mcpp's name answers a different question — which C library — and now - // it can. - // - // mcpp target → clang c-abi - // x86_64-linux-musl → x86_64-unknown-linux-musl musl - // x86_64-windows-gnu → x86_64-w64-windows-gnu MinGW CRT - // x86_64-windows-musl → x86_64-w64-windows-gnu musl - // - // The last two rows differ in the first column and agree in the second, - // which is the whole point. - // - // THE PIN IS `llvm`, AND IT IS NOT A PREFERENCE. The column names the - // payload that supplies this target's C library everywhere else in this - // table; here nothing supplies it, and what the column has to prevent is - // the OPPOSITE — a global default of gcc being carried onto a target no gcc - // can emit. Measured with `mcpp toolchain default gcc@16.1.0`: - // - // error: toolchain payload 'xim:musl-gcc@16.1.0' has no known C++ - // frontend in …/xim-x-musl-gcc/16.1.0/bin - // - // — a message about a missing frontend, for a target whose real problem is - // that only clang emits it at all. The bare-metal rows carry `llvm` for the - // same reason and say so in their own note. - // - // The C library still comes from the dependency graph. `host_can_serve` - // says no for this row on a non-Windows host — correctly, for the prebuilt - // system — and that refusal is diagnosed early and RELEASED once the graph - // is known (see the long note at prepare.cppm's `unservedTargetDiagnosis`), - // so a project whose C library comes from a dependency is not turned away. - // - // TIER IS `preview`, NOT `verified`. `verified` in this table means an - // artefact was built AND RUN. Running a PE on a Linux host needs wine, and - // openkal's CI has that step — so this is measurable, and the tier moves - // when it has been measured rather than when it seems likely. - { "x86_64-windows-musl", "preview", "PE", "llvm@23.1.3","", true }, - { "x86_64-windows-msvc", "verified", "PE", "", "", false }, - { "aarch64-macos", "verified", "", "", "", false }, - { "riscv64-linux-musl", "planned", "", "", "", true }, - // `preview` until the released engine is consumed on a clean ARM64 host - // (SPEC-009 §10.7): CI built and ran the native LLVM 23.1.3/glibc default - // from source, and the published chain (xlings client, glibc revision 3, - // the ARM LLVM resources, the CN mirrors) is what that consumption checks. - { "aarch64-linux-gnu", "preview", "", "llvm@23.1.3","", false }, - { "x86_64-macos", "planned", "", "", "", false }, - // Bare metal. `defaultStatic` is not a preference here — there is no - // loader, so there is no other option. The pin is llvm on every host - // because clang/lld are cross-compilers by construction: unlike the hosted - // rows above, these need no per-host cross payload at all. - // ISA profile (-march/-mabi/-mcmodel) lives in mcpp.freestanding.target, - // which is the single place that decision is made. - // The sysroot column is what keeps a bare-metal PACKAGE from having to - // name a libc: the C library is the target's, like the compiler. - { "riscv64-none-elf", "verified", "bare","llvm@23.1.3","xim:picolibc-riscv@1.8.12", true }, - { "riscv32-none-elf", "verified", "bare","llvm@23.1.3","xim:picolibc-riscv@1.8.12", true }, - // AN EMPTY SYSROOT COLUMN, AND IT IS A STATEMENT RATHER THAN AN OMISSION. - // - // The two rows above name a C library because a project targeting them - // ordinarily wants one. This row does not, because there is no aarch64 - // build of picolibc in the index — and, more to the point, because the - // first consumer of this row does not want one. `openarch` is a layer of - // machine mechanism: contexts, traps, page-table entries. It references no - // C library symbol, and a row that resolved one would make every project - // on this target carry a payload it never calls. - // - // An empty column here means exactly what `[target.].sysroot = ""` - // means in a manifest — the zero-libc tier: no headers on the compile line, - // no library directory on the link, and `#include ` does not - // resolve. A project that wants a C library on this target says so in its - // own manifest, which is also how it would choose a different one. - // - // The tier is `preview` and not `verified`: `verified` in this table - // means an image has been built AND RUN for the row, and running one needs - // an emulator. `xim:qemu-arm` provides `qemu-system-aarch64`; until a probe - // has actually booted under it, claiming `verified` would be claiming the - // measurement rather than reporting it. - { "aarch64-none-elf", "preview", "bare","llvm@23.1.3","", true }, - // THIS ROW EXISTS SO THAT A THIRD MACHINE CAN DISAGREE WITH THE FIRST - // TWO, WHICH IS THE ONLY THING THAT TELLS AN ABSTRACTION FROM A HABIT. - // - // riscv64 and aarch64 are both load/store RISC machines with a weak memory - // model and a fixed instruction width, so an interface that fits both may - // fit because it is right or because they are alike. x86_64 is neither: it - // has variable-length instructions, a total-store-order memory model under - // which three of openarch's four barriers need no instruction at all, and - // an interrupt mechanism that is a table of gates rather than a base - // register. What survives all three is an abstraction. - // - // The tier is `preview` for the same reason aarch64's is, and the reason - // is stricter than it sounds: `verified` here means an image was built AND - // RUN. `xim:qemu-x86` does not exist yet — the index carries no - // `qemu-system-x86_64` — so nothing on this row has booted. Claiming - // `verified` would be claiming a measurement that has not been made. - // - // The sysroot column is empty, the zero-libc tier, for the reason given - // above `aarch64-none-elf`: the first consumer is `openarch`, which - // references no C library symbol. - { "x86_64-none-elf", "preview", "bare","llvm@23.1.3","", true }, - // ── Cortex-M ──────────────────────────────────────────────────────────── - // - // SEVEN ROWS AND NOT ONE, BECAUSE "Cortex-M" IS NOT AN INSTRUCTION SET. - // - // Every other bare-metal family here is one row per architecture. M-profile - // is not: an object built for `thumbv7em` uses instructions a Cortex-M0 - // does not have, and one built for `thumbv6m` runs on both but leaves the - // larger part unused. The two spellings are not a preference a board - // expresses — they produce incompatible objects — so they are rows. - // - // The rule this table states about itself governs: it exists so that - // `--target ` ALONE is enough to produce a correct object file. A - // single `arm-none-eabi` row plus an `-mcpu` the project remembers would - // move a correctness decision out of the table and into every manifest. - // - // THE `eabi`/`eabihf` SUFFIX IS THE FLOAT ABI, AND CLANG ALREADY READS - // IT. Measured on llvm 22.1.8 (`-###`, `-cc1` line): `thumbv7em-none-eabi` - // gives `-mfloat-abi soft` and `-none-eabihf` gives `hard`, with no flag - // from us. So the ABI needs no entry in the ISA table's `extra` column — - // only the FPU does, and only on the soft rows. See `kThumbSoftExtra`. - // - // `sysroot` IS EMPTY ON EVERY ROW, AND THAT IS THE POINT RATHER THAN A - // GAP. The three older bare-metal families name an `xim:` payload here; a C - // library for these targets arrives from the DEPENDENCY GRAPH instead - // (`mcpp:c-abi=picolibc`, docs/14). A prebuilt payload would have to ship - // one multilib per row — seven here — and the `libdir` column would have to - // match its layout byte for byte, which is the defect #481 fixed. A source - // package is compiled with the consuming target's own flags, so the ABI - // agreement holds by construction and there is no multilib at all. - // - // THE TIER COLUMN RECORDS WHAT WAS RUN, NOT WHAT WAS REASONED. Measured - // 2026-09-04 under `xim:qemu-arm@9.2.4-1`: each `verified` row below built - // an image that BOOTED on the named machine and printed over semihosting — - // thumbv6m on `microbit`, thumbv7m on `mps2-an385`, thumbv7em-eabihf on - // `mps2-an386`, thumbv8m.main-eabi on `mps2-an505`. The three `preview` - // rows build and link; no emulator run has been recorded for them. - { "thumbv6m-none-eabi", "verified", "bare","llvm@23.1.3","", true }, - { "thumbv7m-none-eabi", "verified", "bare","llvm@23.1.3","", true }, - { "thumbv7em-none-eabi", "preview", "bare","llvm@23.1.3","", true }, - { "thumbv7em-none-eabihf", "verified", "bare","llvm@23.1.3","", true }, - { "thumbv8m.base-none-eabi","preview", "bare","llvm@23.1.3","", true }, - { "thumbv8m.main-none-eabi","verified", "bare","llvm@23.1.3","", true }, - { "thumbv8m.main-none-eabihf","preview","bare","llvm@23.1.3","", true }, - // ── ARMv7-A (Cortex-A, 32-bit) ────────────────────────────────────────── - // - // NOT A SECOND SPELLING OF THE M ROWS. A-profile has a memory management - // unit and a page-table walker; M-profile has an MPU and no page-table - // entry at all. It is the first 32-bit machine in this table on which an - // address space can be described, which is precisely the question the - // openarch layer has never been able to ask of a 32-bit target. - // - // `verified` records what was RUN. Measured 2026-09-04 under - // `xim:qemu-arm@9.2.4-1`: both rows booted on `-M virt -cpu cortex-a15` and - // printed over semihosting. The soft row carries `-mfpu=none` for the - // reason `kThumbSoftExtra` gives, measured again on this architecture - // rather than carried over from M-profile. - // - // `sysroot` is empty, the zero-libc tier, exactly as for the M rows: a C - // library for these targets arrives from the dependency graph. - { "armv7a-none-eabi", "verified", "bare","llvm@23.1.3","", true }, - { "armv7a-none-eabihf", "verified", "bare","llvm@23.1.3","", true }, +constexpr const mcpp::toolchain::target::Provider* default_provider_of(std::string_view triple) { + const mcpp::toolchain::target::Provider* best = nullptr; + for (auto const& p : mcpp::toolchain::builtin::kProviders) + if (mcpp::toolchain::target::serves(p, triple) && (!best || p.priority > best->priority)) + best = &p; + return best; +} - // ── The three platforms a package cannot add ──────────────────────────── - // - // A package can add a language, a tool, an action, a payload and a - // generated module. IT CANNOT ADD A TRIPLE: identity is these three - // strings and this table is compiled into the binary, so every layer above - // -- the `.apk` step, the `.app` step, the `.html`+`.wasm` step, the - // runner, the signing -- waits on a row here and on nothing else in the - // engine. Registering the rows is what turns each of those into a plugin - // that can be written rather than a plugin that has nowhere to attach. - // - // ALL FOUR ARE `planned`, WHICH IS A REFUSAL AND NOT A GAP. The tier gate - // refuses a planned row with `tier-planned` naming the row, so - // `mcpp build --target aarch64-linux-android` says the vocabulary has this - // target and nothing is wired yet -- rather than `unknown target`, which - // was false, or a build that resolves and produces nothing, which would be - // worse than either. What each row still needs is recorded in - // .agents/docs/2026-09-11-distribution-plugins-and-platform-decomposition.md - // section 3, and it is a payload in every case, never engine work. - // - // THE PREREQUISITE NOBODY LISTS IS ANSWERED FOR TWO OF THE THREE. mcpp is - // module-first, so a row whose toolchain cannot compile a module interface - // unit would be worse than its absence. Measured 2026-09-11: `import std` - // works on both the NDK's clang 18 and Emscripten's, and neither needs a - // fork or a compiler upgrade -- what both need is the generated module - // surface their vendor chose not to install (133 files, 620 KB, taken from - // the libc++ revision matching `_LIBCPP_VERSION`, which for Emscripten is - // NOT the version its clang reports). Apple's half is not measurable on a - // Linux host and is the one genuinely open question of the three. - - // ANDROID IS THE SMALLEST OF THE THREE, and the ranking is the opposite of - // the demand ranking. `aarch64` is already an arch, ELF is already the - // object format, and Linux is already the OS: what was missing is an `env` - // value and a sysroot that points at an NDK. No `pin`, because no cross - // payload exists yet -- `xim:android-ndk` is the row's whole remaining - // cost, and until it lands `[target.].sysroot` is the escape hatch - // for a machine that has an NDK already. - { "aarch64-linux-android", "verified", "", "android-ndk@30.0.16248370", "", false }, - // The emulator's row, and the one of the pair that could be EXECUTED. - // - // Not a convenience: x86_64 is what an Android emulator image runs, so a - // row for the device without one for the emulator describes a target - // nothing can execute. That argument is now measured rather than asserted. - // 2026-09-11, linux-x86_64, an API 24 x86_64 system image under the - // platform's own emulator with KVM: - // - // adb push /data/local/tmp/ - // adb shell ./andtest -> 1-2-3 exit 0 - // - // from `import std;` and no project vocabulary beyond `--target`. So this - // row is `verified` while `aarch64-linux-android` is `preview`, and the - // difference is execution rather than confidence in the build. - // - // AND THE DEVICE ROW FOLLOWED, BY A DIFFERENT VEHICLE. Google's emulator - // refuses a foreign guest outright -- "QEMU2 emulator does not support - // arm64 CPU architecture" -- so the arm64 image needs an arm64 host. The - // route that works from an x86_64 one is qemu-user over the system image's - // own bionic, and what blocked it was never the route: preparing it needs - // four files out of an ext4 partition image, `debugfs` was the tool, and - // `xim:e2fsprogs@1.47.3`'s debugfs is a broken build (SIGFPE on every - // filesystem-opening command, while dumpe2fs/e2fsck/tune2fs from the same - // payload work). `xim:7zip` reads ext4 directly. Measured 2026-09-11: - // - // 7zz x bin/linker64 lib64/lib{c,dl,m,c++}.so - // qemu-aarch64-static -L - // -> 1-2-3 exit 0 - // - // So both rows are `verified` and the vehicles differ: the platform's own - // emulator for x86_64, qemu-user plus the image's bionic for aarch64. A - // tier states that an artefact was built and RUN, not which emulator ran - // it. - // - // One linker warning is worth recording because a user will see it and it - // is not a defect: `unsupported flags DT_FLAGS_1=0x8000001`. API 24's - // bionic linker does not recognise the `DF_1_PIE` bit that lld sets, warns, - // and loads the program anyway. - { "x86_64-linux-android", "verified", "", "android-ndk@30.0.16248370", "", false }, - - // iOS IS NEXT. `aarch64-macos` is `verified`, so Mach-O, `arm64`, the - // linker and the Apple half of the toolchain model all exist; what is - // missing is an `os` value and the iPhoneOS SDK. - // - // THE COMPILER IS OURS AND ONLY THE SDK IS APPLE'S, which is the sentence - // that shrank this row from a packaging problem to a located directory. - // - // `llvm@23.1.3` -- any sufficiently new clang emits arm64 Mach-O for an - // iOS deployment target, and the C++ runtime comes from the SDK the way it - // does on every Apple platform. `aarch64-macos` is verified on exactly - // this split and is the precedent: `xim:llvm` compiles and the SDK is - // located. Xcode's own clang would also work and is the wrong default -- - // it would make the compiler a host dependency where the ecosystem - // already has one, and these would be the only Apple rows not using - // `xim:llvm`. - // - // A CONVENTION PIN, NOT A CAPABILITY ONE, which is why - // `pin_is_capability()` does not name these rows. It means "this is what - // `--target aarch64-ios` resolves when the project says nothing", and a - // project may still write `[target.aarch64-ios] toolchain = "..."`. The - // capability set is the rows NO other payload can serve, and the - // distinction matters for consistency with `aarch64-macos`: the same - // constraint (Darwin needs clang) holds there and that row is not a - // capability row either. - // - // THE SDK IS A LICENCE QUESTION AND NOT A PACKAGING ONE, which is why this - // row carries no `sysroot`. The NDK is Apache-2.0 and Emscripten is MIT, - // both redistributable; the iPhoneOS SDK is neither. The recipe should - // reach for the lowest of three tiers its licence allows -- redistribute, - // fetch from upstream without a mirror, or locate what the machine already - // has -- and say which tier it took, because a consumer reading "locator" - // needs to know that is a licence conclusion rather than an unfinished - // recipe. `msvc@system` is the shape of the third tier and mcpp already - // has it. - // - // The simulator is deliberately not a row. It has its own SDK and produces - // its own object, so folding it in would make two targets share an - // identity -- the mistake `x86_64-windows-musl` was added to undo. - // - // `preview`: BUILT, AND NOT RUN, AND NOTHING HERE CAN RUN IT. Measured - // 2026-09-11 on macos-15 (Xcode 16.4, iPhoneOS 18.5, - // `ios_deployment_target = "18.0"`): - // - // Mach-O 64-bit executable arm64 - // LC_BUILD_VERSION platform 2 (IOS) minos 18.0 sdk 18.5 - // - // `platform 2` against the simulator rows' `platform 7` is the reading - // worth having: a successful build cannot tell them apart, and an artefact - // reporting IOSSIMULATOR from this row is one no later step refuses. - // - // The row keeps `runner` unset and will stay `preview`. An artefact cannot - // be run off an iOS device without a signature the developer owns, which - // is not something a build tool or a package can supply -- so this is a - // tier bounded by a fact about the platform rather than by work not yet - // done. - { "aarch64-ios", "preview", "", "llvm@23.1.3","", false }, - // THE SIMULATOR'S TWO ROWS. Not a convenience and not a runner: a - // simulator build has its own SDK (`iPhoneSimulator.sdk`), produces its own - // object, and takes `-mios-simulator-version-min` rather than - // `-miphoneos-version-min`. Two targets sharing one identity is what the - // device row's own comment objected to, and the objection was to the - // absence of these rows rather than to their presence. - // - // BOTH ARCHES, for the reason the Android pair has both: the simulator runs - // the HOST's architecture, so an Apple-silicon machine needs `aarch64` and - // an Intel one needs `x86_64`. A single row would describe a simulator half - // the machines cannot run. - // - // BOTH `verified`, AND THE SECOND ONE WAS RUN ON ANOTHER MACHINE. - // - // `aarch64-ios-sim` was built AND RUN. Measured 2026-09-11 on macos-15 - // (Xcode 16.4, iPhoneSimulator 18.5, `ios_deployment_target = "18.0"`): - // - // artefact Mach-O 64-bit executable arm64 - // LC_BUILD_VERSION platform 7 (IOSSIMULATOR) minos 18.0 - // run xcrun simctl spawn -> 1-2-3 - // - // with no bundle, no signature and no Info.plist -- the measurement that - // made `simctl-run` a boot-and-spawn wrapper rather than a bundle builder. - // - // `x86_64-ios-sim` builds and its artefact is correct -- Mach-O x86_64, - // the same `platform 7`, the same floor -- and a SIMULATOR RUNS THE HOST'S - // ARCHITECTURE, so the Apple-silicon runner that builds it cannot run it. - // ci-macos-ios.yml builds it there and hands the artefact to a - // `macos-15-intel` job, which spawns it on that host's simulator and reads - // `1-2-3` and status 0 (design 2026-10-23 §2.6, S15). The row can be built - // because the SDK carries an x86_64 slice, which is measured; whether the - // vendor still sells the hardware is not a property of the row. - { "aarch64-ios-sim", "verified", "", "llvm@23.1.3","", false }, - { "x86_64-ios-sim", "verified", "", "llvm@23.1.3","", false }, - - // WEB IS THE OUTLIER, AND IT IS THE ONLY ONE OF THE THREE THAT CHANGES THE - // MODEL RATHER THAN EXTENDING A TABLE. A new arch (`wasm32`), a new os - // (`emscripten`), and -- the sharp part -- a new OBJECT FORMAT, which - // before `ObjectFormat` existed was not a field at all but a derivation - // repeated at every site that needed it. That is why this is - // https://github.com/mcpp-community/mcpp/issues/597 and not a table row. - // - // IT IS NOW ONLY THAT, AND THE STANDARD-LIBRARY HALF IS SIMPLER THAN THIS - // COMMENT FIRST SAID. Measured 2026-09-11 against Emscripten 6.0.9: - // `em++` compiles and links `import std` with NO additional flags and no - // generated surface at all, because the toolchain SHIPS one -- 134 files - // -- and `node app.js` printed the expected output. - // - // The version numbers here were two releases stale, in exactly the - // direction the design record warns about: they said llvm 20.1.7 and - // `_LIBCPP_VERSION 200100` against clang 22.0.0git. Emscripten 6.0.9 - // reports `220108` (llvm 22.1.8) and clang 24.0.0git. The rule those - // numbers were supporting is unaffected and is the reason to keep them - // accurate: the surface must match the LIBRARY, never the compiler, and a - // recipe's job is to pin the `_LIBCPP_VERSION` it measured and refuse a - // change. A stale number in a comment becomes a stale number in a - // diagnostic, and then in somebody's install command. - // - // `defaultStatic` is true because wasm has no dynamic loader in the sense - // the other rows mean: an Emscripten link produces one module plus its - // JavaScript, and there is no shared object for a search path to find. - // - // `verified` ASSERTS THE WHOLE LOOP, and here is what it was measured - // against (2026-09-11, Linux x86_64, xim:emsdk 6.0.9): - // - // mcpp build --target wasm32-emscripten on a source that imports std - // -> bin/ 65389 bytes the JavaScript - // bin/.wasm 447183 bytes the module - // node bin/ -> 1-2-3 - // - // Reaching it took five engine gates, and each one was found by the - // previous one's failure rather than by reading: the payload had to be - // chosen by the TARGET (registry.cppm), the frontend found outside `bin/` - // (frontendSubdir), the host's header set withheld (has_own_sysroot, in - // the shared producer and not at one of its three callers), the C-runtime - // group withheld from the LINK MODEL rather than from its two channels, - // and -- the same finding a second time -- the COMPILER's own runtime - // directories kept off the ARTIFACT's link line. For every row that - // predates this one those two are the same directory. - { "wasm32-emscripten", "verified", "wasm","emsdk@6.0.9","", true }, - // OK IMAGES (openkal SPEC clause 12; design 2026-10-26 part 2, §8.5). One - // artifact per architecture, run by `okrun` on Linux, macOS and Windows of - // that architecture. clang emits it from one binary for either row. What - // makes it an ok image -- its flags, its link, its note and check, its - // name, its runner, its C library and C++ run-time -- is the platform's, - // `openkal-platform` (SPEC-013), pinned here. The two architectures the - // loader relocates. - { "x86_64-openkal", "verified", "ok", "llvm@23.1.3","", true, - "mcpplibs.openkal-platform@0.24.4" }, - { "aarch64-openkal", "verified", "ok", "llvm@23.1.3","", true, - "mcpplibs.openkal-platform@0.24.4" }, -}; +constexpr TargetInfo info_of(const mcpp::toolchain::target::Row& r) { + TargetInfo t{ r.triple, r.tier, r.note, {}, {}, false }; + t.row = &r; + t.provider = default_provider_of(r.triple); + if (t.provider) { + t.pin = t.provider->toolchain; + t.sysroot = t.provider->sysroot == "toolchain" ? std::string_view{} : t.provider->sysroot; + t.defaultStatic = t.provider->linkage == "static"; + t.platform = t.provider->platform; + } + return t; +} + +inline constexpr auto kKnownTargets = [] { + std::array out{}; + for (std::size_t i = 0; i < out.size(); ++i) out[i] = info_of(mcpp::toolchain::builtin::kRows[i]); + return out; +}(); inline std::span known_targets() { return kKnownTargets; } +// A PROVIDER WHOSE TOOLCHAIN SHIPS THE WHOLE SYSROOT (`sysroot = "toolchain"`): +// the Emscripten and Android SDKs, which resolve their sysroot themselves. A +// triple outside the table answers by its system, as before the table was data. +inline bool Triple::has_own_sysroot() const { + const auto s = str(); + for (auto const& k : kKnownTargets) + if (k.canonical == s) return k.provider && k.provider->sysroot == "toolchain"; + return os == "emscripten" || os == "android" || env == "android"; +} + +// A PIN IS A CAPABILITY WHEN THE PROVIDER PAIRS WITH ONE COMPILER FAMILY ONLY: +// nothing else emits the target (bare metal, ok images, PE with musl, wasm) or +// supplies its system (bionic). A provider that pairs with several families +// (MinGW: gcc and clang) states a convention among them. +inline bool Triple::pin_is_capability() const { + const auto s = str(); + for (auto const& k : kKnownTargets) + if (k.canonical == s) return k.provider && k.provider->pairs.size() == 1; + return is_freestanding() || is_openkal() || (is_pe() && is_musl()) || is_wasm() || is_android(); +} + inline const TargetInfo* find_known_target(const Triple& t) { auto s = t.str(); for (auto& k : kKnownTargets) @@ -922,36 +595,13 @@ inline const TargetInfo* find_known_target(const Triple& t) { inline bool is_known_target(const Triple& t) { return find_known_target(t) != nullptr; } -// ── Completing a request that declined to name a C library ────────────────── -// -// `parse` FILLS THE ENV SEGMENT LEXICALLY, AND THE TIER GATE USED TO ASK -// ABOUT THE FILLED VALUE RATHER THAN ABOUT THE REQUEST. +// ── What a request reached ─────────────────────────────────────────────────── // -// The fill is an IDENTITY operation and has to stay exactly as it is: total, -// lexical, and independent of the host (see the note on `Triple::envExplicit` -// and the one beside the fill itself). `x86_64-linux` is the identity -// `x86_64-linux-gnu` on every machine, and a unit test says so. -// -// What it is NOT is an answer to "does mcpp support this". Measured on -// 2026.8.26.1: -// -// $ mcpp build --target aarch64-linux -// error: target 'aarch64-linux-gnu' is registered but not yet supported -// $ mcpp build --target aarch64-linux-musl -// Finished dev [unoptimized + debuginfo] in 0.99s -// -// The question asked was "aarch64, Linux". The question answered was -// "aarch64-linux-GNU", and the error even quotes a triple the user never typed. -// The same fill sends `riscv64-linux` to `riscv64-linux-gnu`, a row that does -// not exist at all, so a registered target family is reported as UNKNOWN. -// -// This function is the request's own completion, applied only where a request -// is read and only when the segment was not written. It consults the vocabulary -// — compile-time data, therefore the same on every host, so target identity -// still does not depend on where the build ran. -// -// Rule one now selects the supported `aarch64-linux-gnu` lexical default. -// Its promotion from `planned` required no change to this completion logic. +// `parse` fills the env segment lexically, so the identity stays total and +// independent of the host. Which row a request is given is the selection's +// (mcpp.toolchain.target_select, SPEC-016 §4); this record is what the tier +// gates read of it: the identity taken, whether it differs from the request, +// and the rows of the request's system with those this mcpp builds. struct RequestResolution { Triple triple; // the identity to use from here on // The lexical fill was replaced by a row from the vocabulary. For the @@ -965,81 +615,6 @@ struct RequestResolution { bool ambiguous = false; }; -inline RequestResolution resolve_request(const Triple& parsed) { - RequestResolution r; - r.triple = parsed; - // A written segment is a request, not a gap: honour it, including when it - // names a `planned` row (the tier gate is what refuses that, and its - // subject is then genuinely what the user typed). - if (parsed.envExplicit || parsed.arch.empty() || parsed.os.empty()) - return r; - - const std::string prefix = parsed.arch + "-" + parsed.os; - for (auto& k : kKnownTargets) { - // Exact (macOS rows carry no env) or `arch-os-`. The separator - // check is what keeps a prefix from spanning two different OS names. - const bool exact = k.canonical == prefix; - const bool sub = k.canonical.size() > prefix.size() - && k.canonical.starts_with(prefix) - && k.canonical[prefix.size()] == '-'; - if (!exact && !sub) continue; - // A BARE `arch-os` ASKS FOR A C LIBRARY TO BE FILLED IN, AND ANDROID - // IS NOT ONE OF THE ANSWERS. - // - // The candidates here are meant to be alternatives for the SAME - // platform -- `gnu` or `musl` for a Linux -- so that `aarch64-linux` - // can complete to the one this repository supports. `android` sits on - // the same `arch-os` prefix because its kernel IS Linux, which is the - // modelling decision that makes every Linux-shaped answer in the tree - // right about it; it is not an alternative C library for the same - // platform. It has a different loader path, a different SDK and an API - // level. - // - // It became visible the moment the Android rows stopped being - // `planned`: `aarch64-linux` then had TWO supported siblings and - // resolved as ambiguous, where before it completed to - // `aarch64-linux-musl`. Either outcome of an ambiguity would be wrong - // here -- refusing a request that has an obvious answer, or answering - // it with bionic. - // - // Excluded from `siblings` too, not just from `supported`. That list is - // what the diagnostic prints, and offering `aarch64-linux-android` to - // someone who typed `aarch64-linux` would be a suggestion to build for - // a different platform. - // - // A written `aarch64-linux-android` never reaches this loop: an - // explicit env returns above, which is the rule that an author's own - // spelling is a request and not a gap. - if (auto kt = parse(k.canonical); kt && kt->is_android()) continue; - r.siblings.push_back(k.canonical); - if (k.tier != "planned") r.supported.push_back(k.canonical); - } - - const std::string lexical = parsed.str(); - for (auto s : r.supported) - if (s == lexical) return r; // rule 1: the fill is supported - - if (r.supported.size() == 1) { // rule 2: the only supported row - auto only = r.supported.front(); - r.triple.env = only.size() > prefix.size() - ? std::string(only.substr(prefix.size() + 1)) - : std::string{}; - // STILL NOT EXPLICIT. `envExplicit` records what the PROJECT asked - // for and feeds the C-library-request check; mcpp choosing a row is not - // the project naming a C library. Setting it here would make - // `check_request` compare mcpp's own answer against itself, and would - // print `aarch64-linux-musl` where the user wrote `aarch64-linux`. - r.completedFromVocabulary = true; - return r; - } - // rule 4 before rule 3: several supported rows and the fill names none. - if (r.supported.size() > 1) r.ambiguous = true; - // rule 3: nothing supported (empty group, or every row `planned`). Keep the - // lexical identity and let the caller diagnose from `siblings`, which is - // what lets the message name a row that actually exists. - return r; -} - // The effective target C library for one build. // // SINGLE READ POINT, and it is one because it was two. `prepare_build` derived @@ -1207,6 +782,17 @@ struct ArtifactNaming { // the decision testable from any host (and keeps this module dependency-free). inline ArtifactNaming artifact_naming(const Triple& t, const ArtifactNaming& hostNaming) { if (t.empty()) return hostNaming; + if (auto* d = t.defined()) { + const bool pe = t.is_pe(); + return ArtifactNaming{ + .exeSuffix = d->exeSuffix, .libPrefix = "lib", + .staticLibExt = ".a", + .sharedLibExt = !d->sharedSuffix.empty() ? d->sharedSuffix + : pe ? std::string_view(".dll") + : t.is_mach_o() ? std::string_view(".dylib") : std::string_view(".so"), + .sharedNeedsImportLib = pe, + }; + } if (t.os == "windows") { // PE. The static-library convention splits on env, not on os. @@ -1512,6 +1098,16 @@ std::optional parse(std::string_view s) { if (t.os == "ios" && (k == "sim" || k == "simulator")) { t.env = "sim"; t.envExplicit = true; continue; } + // musl ON macOS (openkal-platform's hosted part, whose C library is + // openkal-musl rather than libSystem). The native identity keeps no env + // segment; this one names the C library it has. + if (t.os == "macos" && k == "musl") { t.env = "musl"; t.envExplicit = true; continue; } + // A SYSTEM A PLATFORM DEFINES (mcpp#911 G1; SPEC-016 §2): the os and + // env words of the rows a platform read in this process states. + if (!sawOs && mcpp::toolchain::target::defines_os(k)) { t.os = std::string(k); sawOs = true; continue; } + if (sawOs && t.env.empty() && mcpp::toolchain::target::defines_env(t.os, k)) { + t.env = std::string(k); t.envExplicit = true; continue; + } // Unrecognized segment (wasi, …): not in mcpp's target language — // treat as unparseable rather than guessing. return std::nullopt; @@ -1521,7 +1117,7 @@ std::optional parse(std::string_view s) { // macOS carries no env segment at all, so nothing was declined there. iOS // and Emscripten are the same shape: the platform layer is the whole of the // identity past the arch, and there is no C-library axis to decline. - if (t.os == "macos" || t.os == "emscripten" || t.os == "openkal" + if ((t.os == "macos" && t.env != "musl") || t.os == "emscripten" || t.os == "openkal" || (t.os == "ios" && t.env != "sim")) { t.env.clear(); t.envExplicit = false; } diff --git a/modules/toolchain-model/tests/test_platform_axis.cpp b/modules/toolchain-model/tests/test_platform_axis.cpp index f508437b8..8df684e2c 100644 --- a/modules/toolchain-model/tests/test_platform_axis.cpp +++ b/modules/toolchain-model/tests/test_platform_axis.cpp @@ -1,32 +1,34 @@ -#include import std; +import mcpplibs.mtest; import mcpp.toolchain.platform; +using namespace mcpplibs::mtest; + // SUBSYSTEM-LEVEL. This package imports nothing from mcpp, so these tests can // only see what the package itself promises — which is the point. The // engine-side tests in the root `tests/unit/` exercise the same vocabulary // through resolution, so a failure here means the vocabulary changed, while a // failure there means a consumer was relying on more than it was told. -TEST(PlatformAxis, TheHostPlatformKeyIsNeverEmpty) { +UTest the_host_platform_key_is_never_empty{"PlatformAxis/TheHostPlatformKeyIsNeverEmpty", [] { // Every descriptor lookup is keyed on this. An empty key would not fail — // it would match the section named "", which no descriptor has, and the // package would simply appear to be unavailable everywhere. auto host = mcpp::toolchain::platform::HostPlatform::current(); - EXPECT_FALSE(host.key().empty()); -} + expect(!(host.key().empty())); +}}; -TEST(PlatformAxis, TwoKeysCompareByValue) { +UTest two_keys_compare_by_value{"PlatformAxis/TwoKeysCompareByValue", [] { // `PlatformKey` is only obtainable through an axis type, so possession of // one already says which axis it came from. Equality therefore has to be // about the key and not the object. auto a = mcpp::toolchain::platform::HostPlatform::current(); auto b = mcpp::toolchain::platform::HostPlatform::current(); - EXPECT_TRUE(a == b); -} + expect(a == b); +}}; -TEST(PlatformAxis, ATargetReadsItsRowsBlocksAndNeverTheHosts) { +UTest a_target_reads_its_rows_blocks_and_never_the_hosts{"PlatformAxis/ATargetReadsItsRowsBlocksAndNeverTheHosts", [] { // Design 2026-10-29 §5.1. Before 2026.10.30.1 a token naming none of the // three fell back to the HOST's block (#851). using mcpp::toolchain::platform::TargetPlatform; @@ -34,17 +36,17 @@ TEST(PlatformAxis, ATargetReadsItsRowsBlocksAndNeverTheHosts) { auto t = TargetPlatform::for_os(os); return std::vector(t.keys().begin(), t.keys().end()); }; - EXPECT_EQ(keys("linux"), (std::vector{"linux"})); - EXPECT_EQ(keys("macos"), (std::vector{"macosx"})); - EXPECT_EQ(keys("windows"), (std::vector{"windows"})); - EXPECT_EQ(keys("emscripten"), (std::vector{"emscripten", "linux"})); - EXPECT_EQ(keys("ios"), (std::vector{"ios", "macosx"})); - EXPECT_EQ(keys("openkal"), (std::vector{"openkal", "linux"})); - EXPECT_EQ(keys("none"), (std::vector{"none"})); + expect(eq(keys("linux"), (std::vector{"linux"}))); + expect(eq(keys("macos"), (std::vector{"macosx"}))); + expect(eq(keys("windows"), (std::vector{"windows"}))); + expect(eq(keys("emscripten"), (std::vector{"emscripten", "linux"}))); + expect(eq(keys("ios"), (std::vector{"ios", "macosx"}))); + expect(eq(keys("openkal"), (std::vector{"openkal", "linux"}))); + expect(eq(keys("none"), (std::vector{"none"}))); // The primary key is the one messages name. - EXPECT_EQ(TargetPlatform::for_os("emscripten").key(), "emscripten"); + expect(eq(TargetPlatform::for_os("emscripten").key(), "emscripten")); // A platform's own keys replace the row's. auto stated = TargetPlatform::with_keys({"openkal"}); - EXPECT_EQ(stated.key(), "openkal"); - EXPECT_FALSE(stated == TargetPlatform::for_os("openkal")); -} + expect(eq(stated.key(), "openkal")); + expect(!(stated == TargetPlatform::for_os("openkal"))); +}}; diff --git a/modules/toolchain-model/tests/test_target_select.cpp b/modules/toolchain-model/tests/test_target_select.cpp new file mode 100644 index 000000000..17ced6af1 --- /dev/null +++ b/modules/toolchain-model/tests/test_target_select.cpp @@ -0,0 +1,139 @@ +// The selection rule of SPEC-016 §4 over the built-in rows and providers: a +// request is a pattern, explicit choices win, and the candidates are ordered by +// priority and taken by availability on the host. The host is a parameter here, +// so every host's answer is stated on every host. +import std; +import mcpplibs.mtest; +import mcpp.toolchain.target_schema; +import mcpp.toolchain.target_select; + +using namespace mcpplibs::mtest; +namespace target = mcpp::toolchain::target; + +namespace { + +struct Ask { + std::string_view request, arch, os, env; + bool envWritten = false; +}; + +target::Selection on(std::string_view host, Ask a, target::Context ctx = {}) { + ctx.host = std::string(host); + return target::select(a.request, a.arch, a.os, a.env, a.envWritten, ctx); +} + +std::string provider_of(const target::Selection& s) { + return s.provider ? std::string(s.provider->name) : std::string("-"); +} + +const Ask kLinux{"x86_64-linux", "x86_64", "linux", "gnu"}; +const Ask kWindows{"x86_64-windows", "x86_64", "windows", "gnu"}; +const Ask kMacos{"aarch64-macos", "aarch64", "macos", ""}; + +} // namespace + +UTest a_pattern_takes_the_hosts_own_provider{"TargetSelect/APatternTakesTheHostsOwnProvider", [] { + auto s = on("linux-x86_64", kLinux); + expect(eq(s.identity(), "x86_64-linux-gnu")); + expect(eq(provider_of(s), "glibc")); + expect(!s.fallback); + auto m = on("macos-aarch64", kMacos); + expect(eq(m.identity(), "aarch64-macos")); + expect(eq(provider_of(m), "apple")); +}}; + +UTest a_pattern_falls_back_to_the_next_available{"TargetSelect/APatternFallsBackToTheNextAvailable", [] { + // No glibc and no musl-gcc payload exists for a macOS host: openkal-platform + // serves the musl identity there, and the selection says it fell back. + auto s = on("macos-aarch64", kLinux); + expect(eq(s.identity(), "x86_64-linux-musl")); + expect(eq(provider_of(s), "openkal")); + expect(s.fallback); + auto m = on("linux-x86_64", kMacos); + expect(eq(m.identity(), "aarch64-macos-musl")); + expect(eq(provider_of(m), "openkal")); +}}; + +UTest windows_takes_msvc_then_openkal_then_mingw{"TargetSelect/WindowsTakesMsvcThenOpenkalThenMingw", [] { + target::Context withMsvc; + withMsvc.probe = [](std::string_view p) { return p == "msvc"; }; + expect(eq(on("windows-x86_64", kWindows, withMsvc).identity(), "x86_64-windows-msvc")); + // Q4: a Windows host without MSVC, and every other host, take the openkal + // platform, whose priority is above MinGW's. + expect(eq(on("windows-x86_64", kWindows).identity(), "x86_64-windows-musl")); + auto l = on("linux-x86_64", kWindows); + expect(eq(l.identity(), "x86_64-windows-musl")); + expect(eq(provider_of(l), "openkal")); +}}; + +UTest a_declared_family_keeps_the_providers_it_pairs_with{"TargetSelect/ADeclaredFamilyKeepsTheProvidersItPairsWith", [] { + target::Context gcc; + gcc.family = "gcc"; + auto s = on("linux-x86_64", kWindows, gcc); + expect(eq(s.identity(), "x86_64-windows-gnu")); + expect(eq(provider_of(s), "mingw")); + target::Context llvm; + llvm.family = "llvm"; + auto g = on("linux-x86_64", {"x86_64-windows-gnu", "x86_64", "windows", "gnu", true}, llvm); + expect(eq(g.identity(), "x86_64-windows-gnu")); + expect(eq(provider_of(g), "mingw")); +}}; + +UTest an_explicit_identity_is_not_replaced{"TargetSelect/AnExplicitIdentityIsNotReplaced", [] { + const Ask musl{"x86_64-linux-musl", "x86_64", "linux", "musl", true}; + expect(eq(provider_of(on("linux-x86_64", musl)), "musl-gcc")); + auto m = on("macos-aarch64", musl); + expect(eq(m.identity(), "x86_64-linux-musl")); + expect(eq(provider_of(m), "openkal")); + const Ask gnu{"x86_64-windows-gnu", "x86_64", "windows", "gnu", true}; + expect(eq(on("linux-x86_64", gnu).identity(), "x86_64-windows-gnu")); +}}; + +UTest a_provider_chosen_by_name_serves_alone{"TargetSelect/AProviderChosenByNameServesAlone", [] { + target::Context mingw; + mingw.provider = "mingw"; + expect(eq(on("linux-x86_64", kWindows, mingw).identity(), "x86_64-windows-gnu")); + target::Context none; + none.provider = "emsdk"; + expect(on("linux-x86_64", kWindows, none).candidates.empty()); +}}; + +UTest a_priority_is_data_a_project_may_change{"TargetSelect/APriorityIsDataAProjectMayChange", [] { + target::Context ctx; + ctx.priority = [](const target::Row&, std::string_view p) -> std::optional { + if (p == "mingw") return 200; + return std::nullopt; + }; + auto s = on("linux-x86_64", kWindows, ctx); + expect(eq(s.identity(), "x86_64-windows-gnu")); + expect(eq(s.priority, 200)); +}}; + +UTest android_is_reached_by_name_only{"TargetSelect/AndroidIsReachedByNameOnly", [] { + for (auto const& c : on("linux-x86_64", kLinux).candidates) + expect(eq(c.row->env == "android", false)) << c.row->triple; + const Ask android{"x86_64-linux-android", "x86_64", "linux", "android", true}; + expect(eq(on("linux-x86_64", android).identity(), "x86_64-linux-android")); +}}; + +UTest a_planned_row_is_the_answer_only_when_alone{"TargetSelect/APlannedRowIsTheAnswerOnlyWhenAlone", [] { + auto s = on("linux-x86_64", {"riscv64-linux", "riscv64", "linux", "gnu"}); + expect(fatal(s.row != nullptr)); + expect(eq(s.identity(), "riscv64-linux-musl")); + expect(eq(s.row->tier, "planned")); + // x86_64-macos is planned; the musl identity beside it is not. + expect(eq(on("macos-x86_64", {"x86_64-macos", "x86_64", "macos", ""}).identity(), "x86_64-macos-musl")); +}}; + +UTest an_arm_pattern_takes_the_standard_c_library{"TargetSelect/AnArmPatternTakesTheStandardCLibrary", [] { + // The glibc row of aarch64 outranks its musl sibling, as on x86_64; a + // written env is the row it names. + auto s = on("linux-aarch64", {"aarch64-linux", "aarch64", "linux", "gnu"}); + expect(eq(s.identity(), "aarch64-linux-gnu")); + expect(eq(provider_of(s), "glibc")); + expect(!s.fallback); + const Ask written{"aarch64-linux-gnu", "aarch64", "linux", "gnu", true}; + auto w = on("linux-aarch64", written); + expect(fatal(eq(w.candidates.size(), std::size_t(1)))); + expect(eq(w.identity(), "aarch64-linux-gnu")); +}}; diff --git a/modules/toolchain-model/tests/test_triple_vocabulary.cpp b/modules/toolchain-model/tests/test_triple_vocabulary.cpp index e750f863c..e2667d159 100644 --- a/modules/toolchain-model/tests/test_triple_vocabulary.cpp +++ b/modules/toolchain-model/tests/test_triple_vocabulary.cpp @@ -1,8 +1,10 @@ -#include import std; +import mcpplibs.mtest; import mcpp.toolchain.triple; +using namespace mcpplibs::mtest; + // SUBSYSTEM-LEVEL. This package is the toolchain VOCABULARY -- what a triple // is, what a dialect is -- separated from finding a toolchain on a machine. // These tests see no detection, no registry and no xlings, which is what makes @@ -10,49 +12,49 @@ import mcpp.toolchain.triple; namespace tr = mcpp::toolchain::triple; -TEST(TripleVocabulary, TheThreePartSpellingMeansTheSameAsTheFourPart) { +UTest the_three_part_spelling_means_the_same_as_the_four_part{"TripleVocabulary/TheThreePartSpellingMeansTheSameAsTheFourPart", [] { // Users write the short form; the resolver, the lock file and the cache key // all read the long one. If these ever stopped agreeing, one project would // build twice under two names. auto three = tr::parse("x86_64-linux-musl"); auto four = tr::parse("x86_64-unknown-linux-musl"); - ASSERT_TRUE(three.has_value()); - ASSERT_TRUE(four.has_value()); - EXPECT_EQ(three->arch, four->arch); - EXPECT_EQ(three->os, four->os); - EXPECT_EQ(three->env, four->env); -} + expect(fatal(three.has_value())); + expect(fatal(four.has_value())); + expect(eq(three->arch, four->arch)); + expect(eq(three->os, four->os)); + expect(eq(three->env, four->env)); +}}; -TEST(TripleVocabulary, TheHostTripleIsParseable) { +UTest the_host_triple_is_parseable{"TripleVocabulary/TheHostTripleIsParseable", [] { // `host_triple()` is the MCPP_HOST contract value handed to every build // program. A value this package cannot parse back is one no consumer can // act on. auto h = tr::host_triple(); - EXPECT_FALSE(h.arch.empty()); - EXPECT_FALSE(h.os.empty()); -} + expect(!h.arch.empty()); + expect(!h.os.empty()); +}}; -TEST(TripleVocabulary, AnUnknownTripleIsNotSilentlyKnown) { +UTest an_unknown_triple_is_not_silently_known{"TripleVocabulary/AnUnknownTripleIsNotSilentlyKnown", [] { // `is_known_target` gates the per-target table. Answering "yes" for // something absent from it would select an empty row rather than report an // unsupported target. auto t = tr::parse("nosucharch-unknown-nosuchos-nosuchenv"); - if (t.has_value()) EXPECT_FALSE(tr::is_known_target(*t)); -} + if (t.has_value()) expect(!tr::is_known_target(*t)); +}}; -TEST(TripleVocabulary, MsvcsX86IsWrittenI686AndTheOtherSpellingsKeepTheirMeaning) { +UTest msvcs_x86_is_written_i686_and_the_other_spellings_keep_their_meaning{"TripleVocabulary/MsvcsX86IsWrittenI686AndTheOtherSpellingsKeepTheirMeaning", [] { // `x86` is MSVC's name for the ISA and no LLVM tool takes it as an // architecture, so it is written the way `amd64` and `arm64` are: once, at // the parse. i386-i586 are different baseline CPUs to clang and stay. auto x86 = tr::parse("x86-windows-msvc"); - ASSERT_TRUE(x86.has_value()); - EXPECT_EQ(x86->str(), "i686-windows-msvc"); - EXPECT_EQ(x86->llvm_triple(), "i686-pc-windows-msvc"); - EXPECT_EQ(*tr::parse("x86-pc-windows-msvc"), *tr::parse("i686-windows-msvc")); - EXPECT_EQ(tr::parse("i386-windows-msvc")->llvm_triple(), "i386-pc-windows-msvc"); -} + expect(fatal(x86.has_value())); + expect(eq(x86->str(), "i686-windows-msvc")); + expect(eq(x86->llvm_triple(), "i686-pc-windows-msvc")); + expect(eq(*tr::parse("x86-pc-windows-msvc"), *tr::parse("i686-windows-msvc"))); + expect(eq(tr::parse("i386-windows-msvc")->llvm_triple(), "i386-pc-windows-msvc")); +}}; -TEST(TripleVocabulary, The32BitX86QuestionHasOneAnswer) { +UTest the32_bit_x86_question_has_one_answer{"TripleVocabulary/The32BitX86QuestionHasOneAnswer", [] { struct Row { std::string_view triple; bool x86_32; std::string_view msvc; }; for (auto const& r : { Row{"x86-windows-msvc", true, "x86"}, @@ -66,10 +68,10 @@ TEST(TripleVocabulary, The32BitX86QuestionHasOneAnswer) { Row{"arm64-apple-macos", false, "arm64"}, Row{"riscv64-linux-musl", false, ""}}) { auto t = tr::parse(r.triple); - ASSERT_TRUE(t.has_value()) << r.triple; - EXPECT_EQ(t->is_x86_32(), r.x86_32) << r.triple; - EXPECT_EQ(t->msvc_arch(), r.msvc) << r.triple; + expect(fatal(t.has_value())) << r.triple; + expect(eq(t->is_x86_32(), r.x86_32)) << r.triple; + expect(eq(t->msvc_arch(), r.msvc)) << r.triple; } - EXPECT_EQ(tr::parse("i586-linux-gnu")->nasm_format(), "elf32"); - EXPECT_EQ(tr::parse("x86-windows-msvc")->nasm_format(), "win32"); -} + expect(eq(tr::parse("i586-linux-gnu")->nasm_format(), "elf32")); + expect(eq(tr::parse("x86-windows-msvc")->nasm_format(), "win32")); +}}; diff --git a/modules/versioning/mcpp.toml b/modules/versioning/mcpp.toml index 88ea1d8b6..64dcffe88 100644 --- a/modules/versioning/mcpp.toml +++ b/modules/versioning/mcpp.toml @@ -25,4 +25,4 @@ license = "Apache-2.0" # state its contract in isolation; the root `tests/unit/` states the same facts # as the engine consumes them, and the pair is the cross-check. [dev-dependencies] -compat.gtest = "1.15.2" +mcpplibs.mtest = "0.5.2" diff --git a/modules/versioning/src/version.cppm b/modules/versioning/src/version.cppm index 3b234ff55..45773e7a9 100644 --- a/modules/versioning/src/version.cppm +++ b/modules/versioning/src/version.cppm @@ -31,6 +31,6 @@ import std; export namespace mcpp { -inline constexpr std::string_view MCPP_VERSION = "2026.11.8.1"; +inline constexpr std::string_view MCPP_VERSION = "2026.11.9.1"; } // namespace mcpp diff --git a/modules/versioning/tests/test_version_grammar.cpp b/modules/versioning/tests/test_version_grammar.cpp index 8b4e2a396..49864cd44 100644 --- a/modules/versioning/tests/test_version_grammar.cpp +++ b/modules/versioning/tests/test_version_grammar.cpp @@ -1,44 +1,46 @@ -#include import std; +import mcpplibs.mtest; import mcpp.version; import mcpp.version_req; +using namespace mcpplibs::mtest; + // SUBSYSTEM-LEVEL: the two modules here answer different questions and are // tested for that difference. `mcpp.version` is this binary's identity; // `mcpp.version_req` is the grammar every OTHER package's version is read // with. Nothing outside this package is involved. -TEST(Versioning, ThisBinaryDeclaresAFourSegmentDateVersion) { +UTest this_binary_declares_a_four_segment_date_version{"Versioning/ThisBinaryDeclaresAFourSegmentDateVersion", [] { // The same string lives in the root mcpp.toml, and // .github/tools/check_version_pins.sh keeps the two equal. This states the // SHAPE, which that checker cannot: a version that lost a segment would // still match the manifest and still be wrong. const std::string v{mcpp::MCPP_VERSION}; - EXPECT_FALSE(v.empty()); - EXPECT_EQ(std::ranges::count(v, '.'), 3) << "expected YYYY.M.D.N, got " << v; -} + expect(!v.empty()); + expect(eq(std::ranges::count(v, '.'), 3)) << "expected YYYY.M.D.N, got " << v; +}}; -TEST(Versioning, TheFourthSegmentSurvivesAParseAndPrint) { +UTest the_fourth_segment_survives_a_parse_and_print{"Versioning/TheFourthSegmentSurvivesAParseAndPrint", [] { // Round-tripping is load-bearing: the resolver rebuilds dependency version // strings from this, and they flow into the lock file and the xlings wire // address. A `.0` collapsing to three segments would rename a package. auto v = mcpp::version_req::parse_version("2026.8.29.1"); - ASSERT_TRUE(v.has_value()) << v.error(); - EXPECT_EQ(v->str(), "2026.8.29.1"); + if (auto&& failed = expect(fatal(v.has_value())); !failed) failed << v.error(); + expect(eq(v->str(), "2026.8.29.1")); auto z = mcpp::version_req::parse_version("2026.8.29.0"); - ASSERT_TRUE(z.has_value()) << z.error(); - EXPECT_EQ(z->str(), "2026.8.29.0"); -} + if (auto&& failed = expect(fatal(z.has_value())); !failed) failed << z.error(); + expect(eq(z->str(), "2026.8.29.0")); +}}; -TEST(Versioning, TheDateSchemeSortsAboveTheOldThreeSegmentOne) { +UTest the_date_scheme_sorts_above_the_old_three_segment_one{"Versioning/TheDateSchemeSortsAboveTheOldThreeSegmentOne", [] { // `0.0.109` < `2026.7.27.1`: the first segment goes from 0 to 2026 and // never comes back. Stated here because the ordering is what stops a // released mcpp from resolving backwards into the pre-date scheme. auto older = mcpp::version_req::parse_version("0.0.109"); auto newer = mcpp::version_req::parse_version("2026.7.27.1"); - ASSERT_TRUE(older.has_value()); - ASSERT_TRUE(newer.has_value()); - EXPECT_TRUE(*older < *newer); -} + expect(fatal(older.has_value())); + expect(fatal(newer.has_value())); + expect(*older < *newer); +}}; diff --git a/modules/versioning/tests/test_xpkg_version.cpp b/modules/versioning/tests/test_xpkg_version.cpp index 80ad988a4..ae8655801 100644 --- a/modules/versioning/tests/test_xpkg_version.cpp +++ b/modules/versioning/tests/test_xpkg_version.cpp @@ -1,8 +1,10 @@ -#include import std; +import mcpplibs.mtest; import mcpp.xpkg_version; +using namespace mcpplibs::mtest; + // SUBSYSTEM-LEVEL: the grammar xlings resolves an `[xlings]` address with, // stated on its own. The engine-level consequence (which payload directory // `mcpp::xpkg_dir` answers) is in tests/unit/test_freestanding.cpp. @@ -16,47 +18,47 @@ std::string best(std::initializer_list keys, std::string_view req) } } -TEST(XpkgVersion, FourSegmentKeysAreOrdinaryVersions) { +UTest four_segment_keys_are_ordinary_versions{"XpkgVersion/FourSegmentKeysAreOrdinaryVersions", [] { auto v = xv::parse("1.7.0.1"); - ASSERT_TRUE(v.has_value()); - EXPECT_EQ(v->components, 4); - EXPECT_GT(xv::compare_keys("1.7.0.1", "1.7.0"), 0); - EXPECT_GT(xv::compare_keys("0.0.100", "0.0.11"), 0); -} + expect(fatal(v.has_value())); + expect(eq(v->components, 4)); + expect(gt(xv::compare_keys("1.7.0.1", "1.7.0"), 0)); + expect(gt(xv::compare_keys("0.0.100", "0.0.11"), 0)); +}}; -TEST(XpkgVersion, ABareVersionOfOneOrTwoSegmentsIsAPrefixRange) { - EXPECT_EQ(best({"1.7.0.1", "1.8.0"}, "1.7"), "1.7.0.1"); - EXPECT_EQ(best({"1.2.0", "1.2.5", "1.3.0"}, "1.2"), "1.2.5"); - EXPECT_EQ(best({"15.1.0", "15.2.0", "16.1.0"}, "15"), "15.2.0"); -} +UTest a_bare_version_of_one_or_two_segments_is_a_prefix_range{"XpkgVersion/ABareVersionOfOneOrTwoSegmentsIsAPrefixRange", [] { + expect(eq(best({"1.7.0.1", "1.8.0"}, "1.7"), "1.7.0.1")); + expect(eq(best({"1.2.0", "1.2.5", "1.3.0"}, "1.2"), "1.2.5")); + expect(eq(best({"15.1.0", "15.2.0", "16.1.0"}, "15"), "15.2.0")); +}}; -TEST(XpkgVersion, ABareVersionOfThreeOrMoreSegmentsIsWrittenPrefixEquality) { - EXPECT_EQ(best({"12.9.1.4", "12.9.10"}, "12.9.1"), "12.9.1.4"); - EXPECT_EQ(best({"1.2.0", "1.2.5"}, "1.2.0"), "1.2.0"); - EXPECT_EQ(best({"1.8.13", "1.9.0"}, "1.8.12"), ""); - EXPECT_EQ(best({"2.15.0.1", "2.15.0.2"}, "2.15.0.1"), "2.15.0.1"); -} +UTest a_bare_version_of_three_or_more_segments_is_written_prefix_equality{"XpkgVersion/ABareVersionOfThreeOrMoreSegmentsIsWrittenPrefixEquality", [] { + expect(eq(best({"12.9.1.4", "12.9.10"}, "12.9.1"), "12.9.1.4")); + expect(eq(best({"1.2.0", "1.2.5"}, "1.2.0"), "1.2.0")); + expect(eq(best({"1.8.13", "1.9.0"}, "1.8.12"), "")); + expect(eq(best({"2.15.0.1", "2.15.0.2"}, "2.15.0.1"), "2.15.0.1")); +}}; -TEST(XpkgVersion, OperatorsAndConjunctionsCompareTheWholeVersion) { - EXPECT_EQ(best({"8.0.0", "8.5.0", "8.7.1"}, ">=8.5.0"), "8.7.1"); - EXPECT_EQ(best({"8.0.0", "8.5.0", "8.7.1"}, ">=8.0.0 <8.7.0"), "8.5.0"); - EXPECT_EQ(best({"2.15.0", "2.15.0.1"}, ">=2.15.0.1"), "2.15.0.1"); - EXPECT_EQ(best({"1.2.3", "1.9.0", "2.0.0"}, "^1.2.3"), "1.9.0"); - EXPECT_EQ(best({"1.2.3", "1.2.9", "1.3.0"}, "~1.2.3"), "1.2.9"); - EXPECT_EQ(best({"1.2.3", "1.2.9", "1.3.0"}, "1.2.*"), "1.2.9"); -} +UTest operators_and_conjunctions_compare_the_whole_version{"XpkgVersion/OperatorsAndConjunctionsCompareTheWholeVersion", [] { + expect(eq(best({"8.0.0", "8.5.0", "8.7.1"}, ">=8.5.0"), "8.7.1")); + expect(eq(best({"8.0.0", "8.5.0", "8.7.1"}, ">=8.0.0 <8.7.0"), "8.5.0")); + expect(eq(best({"2.15.0", "2.15.0.1"}, ">=2.15.0.1"), "2.15.0.1")); + expect(eq(best({"1.2.3", "1.9.0", "2.0.0"}, "^1.2.3"), "1.9.0")); + expect(eq(best({"1.2.3", "1.2.9", "1.3.0"}, "~1.2.3"), "1.2.9")); + expect(eq(best({"1.2.3", "1.2.9", "1.3.0"}, "1.2.*"), "1.2.9")); +}}; -TEST(XpkgVersion, APrereleaseIsExactAndRanksBelowItsRelease) { - EXPECT_EQ(best({"1.0.0-rc1", "1.0.0"}, "1.0.0-rc1"), "1.0.0-rc1"); - EXPECT_LT(xv::compare_keys("1.0.0-rc1", "1.0.0"), 0); -} +UTest a_prerelease_is_exact_and_ranks_below_its_release{"XpkgVersion/APrereleaseIsExactAndRanksBelowItsRelease", [] { + expect(eq(best({"1.0.0-rc1", "1.0.0"}, "1.0.0-rc1"), "1.0.0-rc1")); + expect(lt(xv::compare_keys("1.0.0-rc1", "1.0.0"), 0)); +}}; -TEST(XpkgVersion, NamesNeverWinAndBuildMetadataIsIgnored) { - EXPECT_EQ(best({"latest", "nightly", "1.0.0"}, ">=0.1"), "1.0.0"); - EXPECT_FALSE(xv::parse("nightly").has_value()); - EXPECT_GT(xv::compare_keys("1.0.0", "nightly"), 0); - EXPECT_EQ(best({"25.0.4+7"}, "25.0.4"), "25.0.4+7"); -} +UTest names_never_win_and_build_metadata_is_ignored{"XpkgVersion/NamesNeverWinAndBuildMetadataIsIgnored", [] { + expect(eq(best({"latest", "nightly", "1.0.0"}, ">=0.1"), "1.0.0")); + expect(!(xv::parse("nightly").has_value())); + expect(gt(xv::compare_keys("1.0.0", "nightly"), 0)); + expect(eq(best({"25.0.4+7"}, "25.0.4"), "25.0.4+7")); +}}; // THE SHARED STATEMENT. xlings publishes which version a request selects as // data (tests/data/semver-vectors.tsv in its repository), and this package @@ -89,9 +91,9 @@ std::vector split(std::string_view s, char sep) { } } -TEST(XpkgVersion, TheXlingsConformanceVectorsSelectTheSameVersion) { +UTest the_xlings_conformance_vectors_select_the_same_version{"XpkgVersion/TheXlingsConformanceVectorsSelectTheSameVersion", [] { const auto file = vectors_file(); - ASSERT_FALSE(file.empty()) << "semver-vectors.tsv not found above " + expect(fatal(!file.empty())) << "semver-vectors.tsv not found above " << std::filesystem::current_path(); std::ifstream in(file); std::string line; @@ -100,13 +102,12 @@ TEST(XpkgVersion, TheXlingsConformanceVectorsSelectTheSameVersion) { ++lineNo; if (line.empty() || line.front() == '#') continue; const auto col = split(line, '\t'); - ASSERT_EQ(col.size(), 4u) << file << ":" << lineNo; + expect(fatal(eq(col.size(), 4u))) << file << ":" << lineNo; if (col[2] != "-") continue; const auto available = split(col[1], ','); const auto request = col[0] == "-" ? std::string{} : col[0]; - EXPECT_EQ(xv::select_installed(available, request).value_or("none"), col[3]) - << file << ":" << lineNo << ": request '" << col[0] << "' among " << col[1]; + if (auto&& failed = expect(eq(xv::select_installed(available, request).value_or("none"), col[3])); !failed) failed << file << ":" << lineNo << ": request '" << col[0] << "' among " << col[1]; ++checked; } - EXPECT_GT(checked, 0); -} + expect(gt(checked, 0)); +}}; diff --git a/src/build/build_program.cppm b/src/build/build_program.cppm index 0ed6db28e..0de58c446 100644 --- a/src/build/build_program.cppm +++ b/src/build/build_program.cppm @@ -90,6 +90,9 @@ struct BuildProgramEnv { // refused (#766). bool sealedPackage = false; std::string targetTriple; // resolved canonical triple; "" = host + // The provider the selection gave the identity (SPEC-016 §4): `openkal`, + // `musl-gcc`, `msvc`, ...; "" for the host's own build. + std::string targetProvider; // The resolved toolchain's payload root and the target's own C library // root. Both exist so a package can ASK instead of DECLARE — see // hostprogram::toolchain_dir / sysroot_dir for why declaring was wrong. @@ -678,6 +681,7 @@ contract_env(const fs::path& root, const fs::path& outDir, const BuildProgramEnv // it has no graph-supplied target side, so the system is always on the // link and a package should supply nothing. e.emplace_back("MCPP_TARGET_REQUESTED", env.targetTriple); + e.emplace_back("MCPP_TARGET_PROVIDER", env.targetProvider); // Convenience splits of the resolved target (Cargo CARGO_CFG_TARGET_* // parity): parsed ONCE, in install_hook_env, through the canonical triple parser so every // build.mcpp stops hand-splitting MCPP_TARGET. MCPP_TARGET_ENV is "" when diff --git a/src/build/cache_key.cppm b/src/build/cache_key.cppm index 876e5e753..41796283c 100644 --- a/src/build/cache_key.cppm +++ b/src/build/cache_key.cppm @@ -494,7 +494,7 @@ BuildAxes build_axes(const mcpp::toolchain::Toolchain& tc, if (auto ft = mcpp::toolchain::triple::parse(tc.targetTriple)) if (auto spec = mcpp::freestanding::resolve(*ft)) b.targetImpliedFlags = - mcpp::freestanding::compile_flags(*spec, tc.targetCxxRuntime); + mcpp::freestanding::compile_flags(*spec, tc.targetCxxRuntime, tc.hostedC); b.stdlibId = tc.stdlibId; b.stdlibVersion = tc.stdlibVersion; diff --git a/src/build/execute.cppm b/src/build/execute.cppm index d1c01c8c2..6f918a707 100644 --- a/src/build/execute.cppm +++ b/src/build/execute.cppm @@ -94,8 +94,9 @@ namespace libc = mcpplibs::mwrap::libc::macros; // `mcpp-tests.json` (schema 1): `package`, `cwd` (the directory the tests run // in, relative to the project root: the package's own), and `legs`, each with // its `triple`, `programs` (environment name to path) and `tests` (`name`, -// `program`, the `files` beside it). Paths are relative to the archive's -// directory and use `/`. +// `program`, the `files` beside it, the test `protocol` it implements, 0 for +// none, as the build decided it, and `own_files`, the files that are its own: +// an archive is run by the same runner as a build's tests). Paths are relative to the archive's directory and use `/`. export namespace mcpp::build::test_archive { inline constexpr int kSchema = 1; @@ -105,6 +106,8 @@ struct Test { std::string name; std::string program; // relative to the archive's directory std::vector files; // beside it, relative to the archive's directory + int protocol = 0; + int ownFiles = 1; // the files that are the test's own }; struct Leg { @@ -170,7 +173,8 @@ nlohmann::json to_json(const Manifest& m) { for (auto const& [env, path] : l.programs) programs[env] = path; nlohmann::json tests = nlohmann::json::array(); for (auto const& t : l.tests) - tests.push_back({{"name", t.name}, {"program", t.program}, {"files", t.files}}); + tests.push_back({{"name", t.name}, {"program", t.program}, {"files", t.files}, + {"protocol", t.protocol}, {"own_files", t.ownFiles}}); legs.push_back({{"triple", l.triple}, {"programs", std::move(programs)}, {"tests", std::move(tests)}}); } @@ -195,7 +199,8 @@ std::expected from_json(const nlohmann::json& d) { l.programs.emplace_back(it.key(), it.value().get()); if (jl.contains("tests") && jl["tests"].is_array()) for (auto const& jt : jl["tests"]) { - Test t{jt.value("name", ""), jt.value("program", ""), {}}; + Test t{jt.value("name", ""), jt.value("program", ""), {}, jt.value("protocol", 0), + jt.value("own_files", 1)}; if (jt.contains("files") && jt["files"].is_array()) for (auto const& f : jt["files"]) t.files.push_back(f.get()); l.tests.push_back(std::move(t)); @@ -1356,9 +1361,29 @@ ExecutionHost locate_run_under(const mcpp::build::BuildContext& ctx, std::string // and the OS run only there. `--no-runner` states that the triple is native // here, and is believed. std::optional unreachable_execution(const mcpp::build::BuildContext& ctx, - std::string_view tripleKey, bool noRunner) { + std::string_view tripleKey, bool noRunner, + bool runnerDeclared = false) { if (noRunner || !ctx.runUnder.empty()) return std::nullopt; auto t = mcpp::toolchain::triple::parse(tripleKey); + // AN ARTIFACT OF A SYSTEM A PLATFORM DEFINES runs where its row's + // `execution` names (mcpp#911 G11): on this machine when that is this + // machine's system, else through a runner -- a runner package's program, + // which reaches a VM, a remote host or a device by the run protocol + // (docs/41) -- or an execution host. With neither it is built, not run. + if (auto const* d = t ? t->defined() : nullptr; d && !runnerDeclared) { + const std::string_view system = d->execution.empty() ? std::string_view(t->os) : d->execution; + const std::string_view here = mcpplibs::os::is_windows ? "windows" + : mcpplibs::os::is_macos ? "macos" : "linux"; + if (system != here) + return std::format( + "{} artifacts run on {}, not on this {} host: built, not run.\n" + " State how they run: [target.{}] runner = [\"\", \"{{}}\"], the program a " + "runner package builds\n" + " ([target.{}.tools] = {{ ... }}; the run protocol is docs/41), or an " + "execution host:\n" + " [target.{}] run_under = [\"remote-run\", \"\", \"--\"].", + tripleKey, system, here, tripleKey, tripleKey, tripleKey); + } // A RUNNER THAT RUNS THE ARTIFACT ON A MACHINE OF ITS ARCHITECTURE // (`runner = { ..., host = "same-arch" }`, SPEC-013): an ok image's loader // is a program of this machine, and an image of another architecture is @@ -1534,7 +1559,7 @@ export RunnerOnThisMachine runner_on_this_machine(const BuildContext& ctx) { RunnerOnThisMachine out; const auto choice = choose_runner(ctx); out.arch = std::string(mcpp::toolchain::platform::host_arch); - if (auto why = unreachable_execution(ctx, choice.tripleKey, false)) out.refusal = *why; + if (auto why = unreachable_execution(ctx, choice.tripleKey, false, !choice.tmpl.empty())) out.refusal = *why; if (!ctx.runUnder.empty()) { auto host = locate_run_under(ctx, choice.tripleKey); if (!host.error.empty()) out.refusal = host.error; @@ -1650,12 +1675,17 @@ mcpp::build::result::Record start_record(const BuildContext& ctx) { r.profile = ctx.profile; r.platform = mcpp::platform::to_json(ctx.plan.platform); r.hostPlatform = mcpp::platform::to_json(ctx.hostPlatform); + if (auto const& s = ctx.targetSelection; !s.request.empty()) + r.target = nlohmann::json{ {"request", s.request}, {"identity", s.identity}, + {"provider", s.provider}, {"priority", s.priority}, + {"fallback", s.fallback} }.dump(); mcpp::build::result::write(r); mcpp::build::result::emit("plan", {{"configuration", { {"triple", r.triple}, {"profile", r.profile}, {"fingerprint", r.fingerprint}, {"outputDir", r.outputDir.generic_string()}, {"platform", nlohmann::json::parse(r.platform, nullptr, false)}, - {"hostPlatform", nlohmann::json::parse(r.hostPlatform, nullptr, false)}}}}); + {"hostPlatform", nlohmann::json::parse(r.hostPlatform, nullptr, false)}, + {"target", nlohmann::json::parse(r.target, nullptr, false)}}}}); return r; } @@ -1703,6 +1733,7 @@ FastRecord fast_path_begin(const std::filesystem::path& projectRoot, f.record.profile = previous->profile; f.record.platform = previous->platform; f.record.hostPlatform = previous->hostPlatform; + f.record.target = previous->target; f.artifacts = std::move(previous->artifacts); } res::write(f.record); @@ -1710,7 +1741,8 @@ FastRecord fast_path_begin(const std::filesystem::path& projectRoot, {"triple", f.record.triple}, {"profile", f.record.profile}, {"fingerprint", f.record.fingerprint}, {"outputDir", f.record.outputDir.generic_string()}, {"platform", nlohmann::json::parse(f.record.platform, nullptr, false)}, - {"hostPlatform", nlohmann::json::parse(f.record.hostPlatform, nullptr, false)}}}}); + {"hostPlatform", nlohmann::json::parse(f.record.hostPlatform, nullptr, false)}, + {"target", nlohmann::json::parse(f.record.target, nullptr, false)}}}}); return f; } void fast_path_end(FastRecord& f, bool ok) { @@ -3310,7 +3342,7 @@ int run_artifact_via_runner(mcpp::build::BuildContext& ctx, return 126; } if (isRunSlot) { - if (auto why = unreachable_execution(ctx, choice.tripleKey, no_runner)) { + if (auto why = unreachable_execution(ctx, choice.tripleKey, no_runner, !choice.tmpl.empty())) { std::println(libc::stderr(), "error: {}", *why); return 2; } @@ -3497,8 +3529,9 @@ std::optional choose_run_leg(std::spantriple, ctx.error())); return std::nullopt; } - const auto key = choose_runner(*ctx, noRunner).tripleKey; - if (auto why = unreachable_execution(*ctx, key, noRunner)) { + const auto chosen = choose_runner(*ctx, noRunner); + const auto key = chosen.tripleKey; + if (auto why = unreachable_execution(*ctx, key, noRunner, !chosen.tmpl.empty())) { passed.push_back(std::format("{}: {}", leg->triple, why->substr(0, why->find('\n')))); continue; } @@ -4028,6 +4061,12 @@ struct TestResult { // said: each case's result, or its listing under `--cases`. int protocol = 0; mcpp::build::test_protocol::Report report; + // Where it ran in the run's schedule, for the mtest store (P12): its start + // after the command began, the worker that ran it, and the cases it + // selected (a program run as a process per file of its cases). + long long startedMs = -1; + int worker = -1; + std::vector select; // The program and the files it is compiled from, for a test built and not // run (`--no-run`): what a runner that selects by change reads (docs/50). std::string program; @@ -4036,6 +4075,76 @@ struct TestResult { std::string guide; }; +// THE RUN IN THE MTEST STORE (mtest spec P12; design 2026-11-09 §17.3): the +// run's record, a line per process of a program and a line per case, so that +// `mtest report|analyze|graph --store ` read `mcpp test`'s runs as the +// engine's own. Written whole into `runs//` and then appended to the index. +void write_mtest_store(const std::filesystem::path& dir, const std::vector& results, + std::chrono::system_clock::time_point started, long long ms, int jobs, + std::string_view selection) { + namespace fs = std::filesystem; + using nlohmann::json; + const auto finished = std::chrono::system_clock::now(); + auto iso = [](std::chrono::system_clock::time_point t) { + return std::format("{:%Y-%m-%dT%H:%M:%SZ}", std::chrono::floor(t)); + }; + static std::atomic serial{0}; + const auto id = std::format("{:%Y%m%dT%H%M%S}-{:04x}", std::chrono::floor(started), + (std::random_device{}() ^ serial++) & 0xffff); + const auto runDir = dir / "runs" / id; + std::error_code ec; + fs::create_directories(runDir, ec); + if (ec) return; + auto status_of = [](const TestResult& t) { + switch (t.status) { + case TestResult::St::Pass: return "pass"; + case TestResult::St::RunFail: return "fail"; + case TestResult::St::CompileFail: return "build-fail"; + case TestResult::St::NotRun: return "not-run"; + case TestResult::St::Built: return "built"; + } + return "fail"; + }; + std::map counts; + bool failed = false; + { + std::ofstream programs(runDir / "programs.ndjson", std::ios::binary); + std::ofstream cases(runDir / "cases.ndjson", std::ios::binary); + for (auto const& t : results) { + failed |= t.status == TestResult::St::RunFail || t.status == TestResult::St::CompileFail; + json p{{"name", t.name}, {"argv", json::array({t.program.empty() ? t.name : t.program})}, + {"status", status_of(t)}, {"exit", t.exitCode}, {"ms", t.durationMs}, + {"started_ms", t.startedMs}, {"worker", t.worker}, {"select", t.select}, + {"resources", json::array()}, {"fixtures", json::array()}}; + if (t.buildMs > 0) p["build_ms"] = t.buildMs; + programs << p.dump() << "\n"; + if (t.report.cases.empty()) { + ++counts[status_of(t)]; + continue; + } + for (auto const& c : t.report.cases) { + ++counts[c.status]; + json f = json::array(); + for (auto const& x : c.failures) + f.push_back({{"file", x.file}, {"line", x.line}, {"message", x.message}}); + cases << json{{"program", t.name}, {"id", c.id}, {"status", c.status}, {"ms", c.ms}, + {"reason", c.reason}, {"failures", f}}.dump() << "\n"; + } + } + } + json run{{"schema", "mtest.run/1"}, {"id", id}, {"started", iso(started)}, {"finished", iso(finished)}, + {"status", failed ? "fail" : "pass"}, + {"system", std::string(mcpplibs::os::name(mcpplibs::os::PLATFORM))}, + {"arch", std::string(mcpplibs::os::name(mcpplibs::os::ARCH))}, {"machine", ""}, + {"commit", mcpp::invocation::get("GITHUB_SHA").value_or("")}, + {"set", "mcpp test"}, {"selection", std::string(selection)}, + {"group", mcpp::invocation::get("GITHUB_RUN_ID").value_or("")}, {"monitor", "off"}, + {"ms", ms}, {"jobs", jobs}, {"counts", counts}}; + { std::ofstream(runDir / "run.json", std::ios::binary) << run.dump(2) << "\n"; } + std::ofstream(dir / "index.ndjson", std::ios::binary | std::ios::app) << run.dump() << "\n"; +} + + // Streaming NDJSON: one record per test, emitted as it finishes — a // consumer (e.g. the d2x provider) sees progress live, and a crash // mid-run still leaves the completed records on stdout. @@ -4174,7 +4283,64 @@ static void print_test_guide(const BuildContext& ctx, bool json) { // backend that drives it, how long planning and the build took, and how the // test binaries are executed. One per configuration group of a `mcpp test` // over several members; `run_tests` holds one for its one member. +// WHETHER A PATTERN NAMES A TEST: its program, or a part of one run as a +// process per file of its cases (`/'), as the run names them. +static bool test_matches(const LinkUnit& lu, std::string_view filter) { + if (filter.empty() || lu.targetName.find(filter) != std::string::npos) return true; + return std::ranges::any_of(lu.ownSources, [&](const std::filesystem::path& f) { + return std::format("{}/{}", lu.targetName, f.stem().generic_string()).find(filter) != std::string::npos; + }); +} + +// A TEST PROGRAM TO RUN: built by this command, or read from an archive. +struct TestRunnable { + std::string name; + std::vector argv; + std::vector> env; + long long buildMs = 0; // this test's own edges in .ninja_log + std::filesystem::path program; // the test's own artifact + // The test protocol the program implements (0: none, P1.1), and the + // files of its run under it: the report, the selection, the cases' + // directory (docs/08 "Cases"). + int protocol = 0; + std::filesystem::path protocolDir; + std::optional select; + std::vector sources; + std::string guide; // its framework's guide (docs/08 "Writing a test") + std::filesystem::path cwd; // empty: mcpp's own + // The files that are the test's own: a program of one file has no other + // file to run its cases in, and is not listed to be split by file. + std::size_t ownFiles = 1; +}; + +// THE PROTOCOL A TEST PROGRAM IMPLEMENTS (P2.3): what its entry states, else +// the marker its file carries. A test that keeps what it reaches may have lost +// a marker nothing references (a framework before mtest 0.5.1) at the link; +// the objects it was linked from still carry it, and the program is read as it +// was before the link dropped anything. `objectMarkers` holds each object's +// answer, so that one is read once per run. +static int test_protocol_of(const BuildContext& ctx, const LinkUnit& lu, + std::map& objectMarkers) { + namespace tp = mcpp::build::test_protocol; + if (lu.testProtocol) return lu.testProtocol; + if (const int v = tp::marker_version(ctx.outputDir / lu.output)) return v; + const bool collected = std::ranges::any_of(lu.linkFlags, [](std::string_view f) { + return f == "-Wl,--gc-sections" || f == "-Wl,-dead_strip"; + }); + if (!collected) return 0; + for (auto const& o : lu.objects) { + const auto path = o.is_absolute() ? o : ctx.outputDir / o; + auto [it, fresh] = objectMarkers.try_emplace(path, 0); + if (fresh) it->second = tp::marker_version(path); + if (it->second == tp::kVersion) return it->second; + } + return 0; +} + struct TestBuild { + // The tests of an archive (`mcpp test --archive`), composed by its reader: + // nothing is built, and these run in place of the plan's. + std::optional> archived; std::optional ctx; // The tests a change affects (`--since`, `--changed`); none: every test. std::optional> affected; @@ -4456,7 +4622,8 @@ static void test_resolve_runner(TestBuild& tb, const TestOptions& testOpts, bool } // Not run, with the reason, as a missing runner is: the tests built. Or, // where the caller asks, built and not run (a platform package's targets). - if (auto why = unreachable_execution(*ctx, runnerChoice.tripleKey, testOpts.noRunner)) { + if (auto why = unreachable_execution(*ctx, runnerChoice.tripleKey, testOpts.noRunner, + !runnerChoice.tmpl.empty())) { if (testOpts.unreachableIsBuilt) { tb.builtNotRunHere = true; if (!json) mcpp::ui::info("note", std::format("built, not run on this host: {}", *why)); @@ -4533,6 +4700,7 @@ static int test_run_member(TestBuild& tb, const TestOptions& testOpts, auto* ctx = &*tb.ctx; auto& backend = tb.backend; const auto tLoop = std::chrono::steady_clock::now(); + const auto testStartedWall = std::chrono::system_clock::now(); if (tb.runnerFatal) return tb.runnerFatal; const auto& runnerChoice = tb.runnerChoice; auto& runnerTmpl = tb.runnerTmpl; @@ -4549,8 +4717,7 @@ static int test_run_member(TestBuild& tb, const TestOptions& testOpts, return lu.kind == mcpp::build::LinkUnit::TestBinary && (owner.empty() || lu.memberOf == owner) && (!tb.affected || tb.affected->contains(lu.targetName)) - && (testOpts.filter.empty() - || lu.targetName.find(testOpts.filter) != std::string::npos); + && test_matches(lu, testOpts.filter); }; std::vector results; auto emit_json = [&](const TestResult& r) { @@ -4616,21 +4783,7 @@ static int test_run_member(TestBuild& tb, const TestOptions& testOpts, return j > 0 ? j : 1; }(); - struct Runnable { - std::string name; - std::vector argv; - std::vector> env; - long long buildMs = 0; // this test's own edges in .ninja_log - std::filesystem::path program; // the test's own artifact - // The test protocol the program implements (0: none, P1.1), and the - // files of its run under it: the report, the selection, the cases' - // directory (docs/08 "Cases"). - int protocol = 0; - std::filesystem::path protocolDir; - std::optional select; - std::vector sources; - std::string guide; // its framework's guide (docs/08 "Writing a test") - }; + using Runnable = TestRunnable; std::vector runnable; // Executes `list`, appending to `results` and emitting the per-test line. @@ -4713,11 +4866,22 @@ static int test_run_member(TestBuild& tb, const TestOptions& testOpts, std::atomic next{0}; std::mutex reportMutex; - auto worker = [&] { + auto worker = [&](int workerId) { for (;;) { std::size_t i = next.fetch_add(1); if (i >= list.size()) return; auto& r = list[i]; + // The schedule, recorded with the result (the mtest store). + const auto stamp_schedule = [&, startedAt = std::chrono::steady_clock::now()] { + auto& t = results.back(); + t.startedMs = std::chrono::duration_cast(startedAt - tLoop).count(); + t.worker = workerId; + if (r.select) { + std::ifstream in(*r.select); + for (std::string line; std::getline(in, line);) + if (!line.empty()) t.select.push_back(line); + } + }; // Stamped HERE, immediately before the exec — not when the // test was queued. @@ -4754,6 +4918,7 @@ static int test_run_member(TestBuild& tb, const TestOptions& testOpts, // it, stated for this run, and no other program (P1.2, P2.2). mcpp::run::Options runOpts; runOpts.env = r.env; + runOpts.cwd = r.cwd.string(); runOpts.input = mcpp::run::Input::inherit; runOpts.streams = capture ? mcpp::run::Streams::merged : mcpp::run::Streams::inherit; runOpts.total = deadline; @@ -4822,6 +4987,7 @@ static int test_run_member(TestBuild& tb, const TestOptions& testOpts, if (!json) mcpp::ui::plain(std::format("{} ... not run", r.name)); results.push_back({r.name, TestResult::St::NotRun, 0, {}, {}, ms, false, reason, r.buildMs}); + stamp_schedule(); std::fflush(libc::stdout()); emit_json(results.back()); continue; @@ -4845,6 +5011,7 @@ static int test_run_member(TestBuild& tb, const TestOptions& testOpts, } results.back().protocol = spoken; results.back().report = std::move(report); + stamp_schedule(); results.back().guide = r.guide; // The captured output belongs directly under its own line, or // it is attributable to nothing. @@ -4862,11 +5029,11 @@ static int test_run_member(TestBuild& tb, const TestOptions& testOpts, }; if (workers <= 1) { - worker(); + worker(0); } else { std::vector pool; pool.reserve(static_cast(workers)); - for (int w = 0; w < workers; ++w) pool.emplace_back(worker); + for (int w = 0; w < workers; ++w) pool.emplace_back(worker, w); for (auto& t : pool) t.join(); } // WALL time of the phase, not the sum of the per-test durations: with @@ -4929,9 +5096,16 @@ static int test_run_member(TestBuild& tb, const TestOptions& testOpts, if (!std::isalnum(static_cast(ch)) && ch != '-' && ch != '.') ch = '_'; return ctx->outputDir / "test-protocol" / leaf; }; + std::map objectMarkers; + // An archive's tests are composed by its reader; they run as the plan's do. + if (tb.archived) { + runnable = *tb.archived; + for (auto& r : runnable) + if (r.protocol) r.protocolDir = protocol_dir(r.name); + } for (auto& lu : ctx->plan.linkUnits) { - if (!filter_match(lu)) continue; + if (tb.archived || !filter_match(lu)) continue; auto tTest = std::chrono::steady_clock::now(); auto test_ms = [&tTest] { @@ -5046,14 +5220,14 @@ static int test_run_member(TestBuild& tb, const TestOptions& testOpts, // its file carries (P2.3). A version this mcpp does not speak runs as // a program that implements none. auto& added = runnable.back(); - const int version = lu.testProtocol ? lu.testProtocol - : mcpp::build::test_protocol::marker_version(exe); + const int version = test_protocol_of(*ctx, lu, objectMarkers); if (version == mcpp::build::test_protocol::kVersion) { added.protocol = version; added.protocolDir = protocol_dir(lu.targetName); } added.sources = sources_of(lu); added.guide = guide_of(lu, version); + added.ownFiles = std::max(lu.ownSources.size(), 1); } // `mcpp test --print-launch` (design 2026-10-30 part 2 §7.4): the one test the pattern // names, built, described as `mcpp run --print-launch` describes a program -- the `mcpp.launch` @@ -5131,6 +5305,7 @@ static int test_run_member(TestBuild& tb, const TestOptions& testOpts, } mcpp::run::Options listOpts; listOpts.env = r.env; + listOpts.cwd = r.cwd.string(); state_protocol(r, "list", listOpts); listOpts.streams = mcpp::run::Streams::merged; listOpts.total = std::chrono::milliseconds(static_cast(testOpts.timeoutSecs) * 1000); @@ -5192,8 +5367,75 @@ static int test_run_member(TestBuild& tb, const TestOptions& testOpts, if (!json && !invocationNotRunReason.empty() && !runnable.empty() && !std::exchange(tb.invocationReasonPrinted, true)) mcpp::ui::warning(invocationNotRunReason); + // A PROGRAM OF SEVERAL FILES RUNS AS SEVERAL PROCESSES (design + // 2026-11-09 §17.3): one per file of its cases, each selecting them + // (P4), scheduled across the workers as the programs are -- the + // parallelism and the isolation of a program per file, with one link. + // `[test] split = "case"` gives each case its own process; "none" runs + // the program once. + const auto split = ctx->manifest.testSplit.empty() ? std::string("file") : ctx->manifest.testSplit; + if (invocationNotRunReason.empty() && split != "none") { + std::vector expanded; + for (auto& r : runnable) { + if (!r.protocol || (split == "file" && r.ownFiles < 2)) { + expanded.push_back(std::move(r)); + continue; + } + mcpp::run::Options listOpts; + listOpts.env = r.env; + listOpts.cwd = r.cwd.string(); + state_protocol(r, "list", listOpts); + listOpts.streams = mcpp::run::Streams::merged; + listOpts.total = std::chrono::milliseconds(static_cast(testOpts.timeoutSecs) * 1000); + listOpts.report_spawn_failure = false; + auto listed = mcpp::run::execute(r.argv, listOpts); + auto report = mcpp::build::test_protocol::read_report(r.protocolDir / "list.ndjson", listed.exit_code); + if (listed.exit_code != 0 || !report.done || report.listed.empty()) { + expanded.push_back(std::move(r)); // its run reports what is wrong + continue; + } + std::map> groups; + for (auto const& c : report.listed) + groups[split == "case" ? c.id : (c.file.empty() ? std::string("-") : c.file)].push_back(c.id); + if (groups.size() < 2) { + expanded.push_back(std::move(r)); + continue; + } + std::vector>> ordered(groups.begin(), groups.end()); + // The largest first, so the longest work starts first. + std::ranges::stable_sort(ordered, std::greater{}, [](auto const& g) { return g.second.size(); }); + std::size_t n = 0; + // A pattern that named a part, not its program, keeps that part. + const bool whole = testOpts.filter.empty() || r.name.find(testOpts.filter) != std::string::npos; + for (auto& [key, ids] : ordered) { + Runnable part = r; + const auto label = split == "case" ? key + : std::filesystem::path(key).stem().generic_string(); + part.name = std::format("{}/{}", r.name, label); + if (!whole && part.name.find(testOpts.filter) == std::string::npos) continue; + part.protocolDir = r.protocolDir / "split" / std::to_string(n++); + std::error_code ec; + std::filesystem::create_directories(part.protocolDir, ec); + std::string lines; + for (auto const& id : ids) lines += id + "\n"; + part.select = part.protocolDir / "select.txt"; + std::ofstream(*part.select, std::ios::binary) << lines; + expanded.push_back(std::move(part)); + } + } + runnable = std::move(expanded); + } run_tests_now(runnable); } + // The run, kept in the mtest store's form (`[test] store`): one that ran a + // program, and not a listing. + if (const auto& st = ctx->manifest.testStore; st != "none" && !testOpts.listCases + && std::ranges::any_of(results, [](const TestResult& r) { return r.status != TestResult::St::Built; })) + write_mtest_store(st.empty() ? ctx->projectRoot / "target" / "mtest" : ctx->projectRoot / st, results, + testStartedWall, + std::chrono::duration_cast( + std::chrono::steady_clock::now() - tLoop).count(), + runJobs, testOpts.filter); summary.elapsedMs = carriedMs + std::chrono::duration_cast( std::chrono::steady_clock::now() - tLoop).count(); @@ -5316,16 +5558,19 @@ int write_test_archive(const TestOptions& o, std::span legs) { auto rel = cwd.lexically_relative(first.projectRoot.lexically_normal()); if (!rel.empty() && *rel.begin() != ".." && rel != ".") m.cwd = rel.generic_string(); std::vector copies; + std::map objectMarkers; std::size_t count = 0; for (auto const& l : legs) { ta::Leg leg; leg.triple = l.triple; for (auto const& lu : l.ctx->plan.linkUnits) { if (lu.kind != LinkUnit::TestBinary) continue; - if (!o.filter.empty() && lu.targetName.find(o.filter) == std::string::npos) continue; + if (!test_matches(lu, o.filter)) continue; const auto exe = l.ctx->outputDir / lu.output; if (!fs::is_regular_file(exe, ec)) continue; - ta::Test t{lu.targetName, ta::test_dest(l.triple, lu.targetName, exe.filename().string()), {}}; + ta::Test t{lu.targetName, ta::test_dest(l.triple, lu.targetName, exe.filename().string()), {}, + test_protocol_of(*l.ctx, lu, objectMarkers), + static_cast(std::max(lu.ownSources.size(), 1))}; copies.push_back({exe, t.program}); // The files of the test's member, placed in its product directory. std::vector> carried; @@ -5386,7 +5631,6 @@ export int run_test_archive(std::span passthrough, BuildOverr const auto cwd = opened->manifest.cwd.empty() ? *root : *root / opened->manifest.cwd; int passed = 0, failed = 0, notRun = 0, notReached = 0; std::vector reasons; - const auto deadline = std::chrono::milliseconds(static_cast(testOpts.timeoutSecs) * 1000); for (auto const& leg : opened->manifest.legs) { if (json) std::println("{{\"leg\":{{\"triple\":\"{}\"}}}}", test_json_escape(leg.triple)); else mcpp::ui::info("Testing", std::format("on {} (from {})", leg.triple, testOpts.archive.string())); @@ -5419,62 +5663,50 @@ export int run_test_archive(std::span passthrough, BuildOverr legEnv.emplace_back(env, (opened->root / path).lexically_normal().string()); legEnv.emplace_back("MCPP_RUN_TRIPLE", tb.runnerChoice.tripleKey); legEnv.emplace_back("MCPP_RUN_MODE", "test"); + // THE LEG'S TESTS, RUN AS A BUILD'S ARE: through the runner, the + // protocol each implements, a process per file of its cases, the + // workers, the store (SPEC-015 §4; docs/08). + std::vector runs; for (auto const& t : leg.tests) { if (!testOpts.filter.empty() && t.name.find(testOpts.filter) == std::string::npos) continue; - const auto exe = (opened->root / t.program).lexically_normal(); - std::vector argv; - if (tb.runnerTmpl.empty()) argv.push_back(exe.string()); - else argv = mcpp::freestanding::expand(tb.runnerTmpl, exe); - for (auto const& a : passthrough) argv.push_back(a); - argv.insert(argv.begin(), tb.runUnderPrefix.begin(), tb.runUnderPrefix.end()); - auto env = legEnv; + TestRunnable r; + r.name = t.name; + r.program = (opened->root / t.program).lexically_normal(); + if (tb.runnerTmpl.empty()) r.argv.push_back(r.program.string()); + else r.argv = mcpp::freestanding::expand(tb.runnerTmpl, r.program); + for (auto const& a : passthrough) r.argv.push_back(a); + r.argv.insert(r.argv.begin(), tb.runUnderPrefix.begin(), tb.runUnderPrefix.end()); + r.env = legEnv; // The files beside the program are its own: the loader searches its // directory, and a runner reads the list it is handed. macOS names // no such variable (its programs keep their rpaths); an entry of an // empty name is refused by the system, and every program of a macOS // leg was "invalid argument" (e2e 989). - std::array dir{exe.parent_path()}; + std::array dir{r.program.parent_path()}; if (const auto key = mcpp::runtime::launch_env::runtime_library_path_key(); !key.empty()) - env.emplace_back(key, mcpp::runtime::launch_env::prepend_path_list(key, dir)); + r.env.emplace_back(key, mcpp::runtime::launch_env::prepend_path_list(key, dir)); { - const auto list = exe.parent_path() / ".mcpp-runtime-files.tsv"; + const auto list = r.program.parent_path() / ".mcpp-runtime-files.tsv"; std::ofstream os(list, std::ios::binary | std::ios::trunc); for (auto const& f : t.files) os << fs::path(f).filename().generic_string() << '\t' << (opened->root / f).lexically_normal().string() << '\n'; - env.emplace_back(std::string(kRuntimeFilesEnv), list.string()); - } - const auto t0 = std::chrono::steady_clock::now(); - bool timedOut = false; - int spawnErr = 0; - const auto cwdText = cwd.string(); - auto rr = mcpp::run::capture_exec_deadline(argv, env, deadline, &timedOut, cwdText, &spawnErr); - const auto ms = std::chrono::duration_cast( - std::chrono::steady_clock::now() - t0).count(); - const bool ok = spawnErr == 0 && !timedOut && rr.exit_code == 0; - if (spawnErr != 0) { - ++notRun; - reasons.push_back(std::format("{}: {}", leg.triple, - mcpp::build::runner_lookup::spawn_failed_message(argv.front(), spawnErr, - rr.spawn_detail))); - } else if (ok) { - ++passed; - } else { - ++failed; - } - if (json) { - std::println("{{\"test\":\"{}\",\"triple\":\"{}\",\"status\":\"{}\",\"ms\":{}}}", - test_json_escape(t.name), test_json_escape(leg.triple), - spawnErr ? "not_run" : ok ? "pass" : timedOut ? "timeout" : "fail", ms); - } else { - mcpp::ui::plain(std::format("{} ... {} ({:.2f}s)", t.name, - spawnErr ? "NOT RUN" : ok ? "ok" : timedOut - ? std::format("FAIL (timeout after {}s)", testOpts.timeoutSecs) - : std::format("FAIL (exit {})", rr.exit_code), - static_cast(ms) / 1000.0)); - if (!ok && spawnErr == 0 && !rr.output.empty()) mcpp::ui::block(rr.output); + r.env.emplace_back(std::string(kRuntimeFilesEnv), list.string()); } + r.protocol = t.protocol == mcpp::build::test_protocol::kVersion ? t.protocol : 0; + r.ownFiles = static_cast(std::max(t.ownFiles, 1)); + r.cwd = cwd; + runs.push_back(std::move(r)); } + if (runs.empty()) continue; + tb.archived = std::move(runs); + TestRunSummary s; + test_run_member(tb, o, passthrough, json, opened->manifest.package, {}, 0, s); + passed += s.passed; + failed += s.failed; + notRun += s.notRun; + if (s.notRun && !s.notRunReason.empty()) + reasons.push_back(std::format("{}: {}", leg.triple, s.notRunReason.substr(0, s.notRunReason.find('\n')))); } if (json) { std::println("{{\"archive_summary\":{{\"passed\":{},\"failed\":{},\"not_run\":{}," @@ -5491,8 +5723,9 @@ export int run_test_archive(std::span passthrough, BuildOverr const auto counts = std::format("{} passed; {} failed{}{}", passed, failed, notReached ? std::format("; {} not run here", notReached) : std::string{}, notRun ? std::format("; {} not run", notRun) : std::string{}); - if (failed) { mcpp::ui::error(std::format("test result: FAILED. {}", counts)); return 1; } - if (!json) mcpp::ui::result("test result", std::format("ok. {}", counts)); + // The archive's result over its legs, each of which stated its own. + if (failed) { mcpp::ui::error(std::format("archive result: FAILED. {}", counts)); return 1; } + if (!json) mcpp::ui::result("archive result", std::format("ok. {}", counts)); return notRun ? 2 : 0; } @@ -5660,8 +5893,7 @@ export int run_tests(std::span passthrough, // filtered run must not clobber it down to one entry). auto filter_match = [&](const mcpp::build::LinkUnit& lu) { return lu.kind == mcpp::build::LinkUnit::TestBinary - && (testOpts.filter.empty() - || lu.targetName.find(testOpts.filter) != std::string::npos); + && test_matches(lu, testOpts.filter); }; if (!testOpts.filter.empty()) { bool any = false; @@ -5831,8 +6063,7 @@ export int run_test_legs(std::span passthrough, auto filter_match = [&](const mcpp::build::LinkUnit& lu) { return lu.kind == mcpp::build::LinkUnit::TestBinary - && (testOpts.filter.empty() - || lu.targetName.find(testOpts.filter) != std::string::npos); + && test_matches(lu, testOpts.filter); }; // ── built at the same time, a thread per leg ── @@ -6003,9 +6234,16 @@ export void run_workspace_tests(std::span passthrough, indexOf[members[i].path] = i; if (!members[i].error.empty()) { slots[i].error = members[i].error; continue; } if (members[i].targets.empty()) { slots[i].note = members[i].noTests; continue; } - if (!testOpts.filter.empty() - && std::ranges::none_of(members[i].targets, [&](auto const& t) { - return t.name.find(testOpts.filter) != std::string::npos; })) { + // As `test_matches' asks of a planned test: the program, or a part of it. + auto names_it = [&](const mcpp::manifest::Target& t) { + if (t.name.find(testOpts.filter) != std::string::npos) return true; + auto part = [&](const std::string& f) { + return std::format("{}/{}", t.name, std::filesystem::path(f).stem().generic_string()) + .find(testOpts.filter) != std::string::npos; + }; + return part(t.main) || std::ranges::any_of(t.testSources, part); + }; + if (!testOpts.filter.empty() && std::ranges::none_of(members[i].targets, names_it)) { slots[i].noMatch = true; continue; } @@ -6198,8 +6436,7 @@ export void run_workspace_tests(std::span passthrough, test_bulk(tb, testOpts, [&](const mcpp::build::LinkUnit& lu) { return lu.kind == mcpp::build::LinkUnit::TestBinary && runnable.contains(lu.memberOf) - && (testOpts.filter.empty() - || lu.targetName.find(testOpts.filter) != std::string::npos); + && test_matches(lu, testOpts.filter); }); if (!runnable.empty()) test_resolve_runner(tb, testOpts, json); } diff --git a/src/build/flag_origins.cpp b/src/build/flag_origins.cpp new file mode 100644 index 000000000..7b2083024 --- /dev/null +++ b/src/build/flag_origins.cpp @@ -0,0 +1,151 @@ +// mcpp.build.flag_origins — the attribution (see the interface). An +// implementation unit, so that the modules it reads are not reachable from +// the interface's importers: with them imported by the interface, GCC 16.1 +// ended with an internal compiler error (a segmentation fault) at +// `import mcpp.cli;` in main.cpp, which imports the interface through +// mcpp.doctor (measured 2026-11-09; mcpp#848). +module mcpp.build.flag_origins; + +import std; +import nlohmann.json; +import mcpp.source_kind; +import mcpp.build.plan; +import mcpp.build.flags; +import mcpp.build.compile_commands; +import mcpp.manifest.flag_words; +import mcpp.toolchain.model; +import mcpp.toolchain.triple; +import mcpp.freestanding.target; +import mcpp.platform; + +namespace mcpp::build { + +namespace { + + +// The options whose value is the next word: one word of the explanation. +constexpr std::string_view kTakesValue[] = { + "-isystem", "-I", "-iquote", "-idirafter", "-include", "-imacros", "-x", "-Xclang", + "-Xlinker", "-B", "-L", "-T", "-m", "-MF", "-MT", "-target", "-isysroot", "-arch", + "-e", "-z", "-o", +}; + +std::vector joined(const std::vector& tokens) { + std::vector out; + for (std::size_t i = 0; i < tokens.size(); ++i) { + if (i + 1 < tokens.size() && std::ranges::find(kTakesValue, tokens[i]) != std::end(kTakesValue)) { + out.push_back(tokens[i] + " " + tokens[i + 1]); + ++i; + } else { + out.push_back(tokens[i]); + } + } + return out; +} + +struct Layers { + // The row's machine description, as the line has it. + std::set row; + std::string rowName; + std::set platform; + std::string platformName; + std::vector toolchainRoots; + std::string toolchainName; +}; + +Layers layers_of(const BuildPlan& plan) { + Layers l; + auto const& tc = plan.toolchain; + l.rowName = tc.targetTriple; + if (auto spec = mcpp::freestanding::resolve(tc.targetTriple)) { + auto words = mcpp::freestanding::row_words(*spec); + for (auto& w : mcpp::freestanding::row_link_words(*spec)) words.push_back(std::move(w)); + l.row.insert(words.begin(), words.end()); + } + if (plan.platform.active) { + l.platformName = plan.platform.display(); + auto const& f = plan.platform.flags; + for (auto const* list : { &f.cflags, &f.cxxflags, &f.asmflags, &f.ldflags }) + for (auto const& w : joined(mcpp::manifest::flag_words(*list))) l.platform.insert(w); + for (auto const& w : joined(mcpp::manifest::flag_words(plan.platform.fields.link.flags.value))) + l.platform.insert(w); + } + if (!tc.binaryPath.empty()) + l.toolchainRoots.push_back(tc.binaryPath.parent_path().parent_path().generic_string()); + if (!tc.base.root.empty()) l.toolchainRoots.push_back(tc.base.root.generic_string()); + l.toolchainName = tc.label(); + return l; +} + +FlagWord attribute(const Layers& l, const std::string& w) { + if (l.row.contains(w)) return { w, "row", l.rowName }; + if (l.platform.contains(w)) return { w, "platform", l.platformName }; + for (auto const& root : l.toolchainRoots) { + auto generic = w; + std::ranges::replace(generic, '\\', '/'); + if (!root.empty() && generic.find(root) != std::string::npos) + return { w, "toolchain", l.toolchainName }; + } + return { w, "engine", {} }; +} + +} // namespace + +FlagOrigins flag_origins(const BuildPlan& plan, std::string_view unit) { + FlagOrigins o; + o.identity = plan.toolchain.targetTriple; + const auto flags = compute_flags(plan); + const auto layers = layers_of(plan); + const auto& root = plan.manifest.package.name; + for (auto const& inv : unit_invocations(plan, flags)) { + auto const& cu = *inv.unit; + const auto file = cu.source.generic_string(); + if (unit.empty() ? cu.packageName != root : !file.ends_with(unit)) continue; + UnitFlagOrigins u{ file, cu.packageName, {} }; + const bool isC = cu.kind == mcpp::SourceKind::C || cu.kind == mcpp::SourceKind::GasAsm; + const auto own = joined(isC ? mcpp::manifest::flag_words(cu.packageCflags) + : mcpp::manifest::flag_words(cu.packageCxxflags)); + const std::set ownSet(own.begin(), own.end()); + // The package's include directories, as the line spells them. + std::set includes; + for (auto inc : cu.localIncludeDirs) includes.insert("-I" + inc.make_preferred().string()); + for (auto inc : cu.localIncludeDirsAfter) includes.insert("-idirafter" + inc.make_preferred().string()); + const auto words = joined(inv.arguments); + for (std::size_t i = 0; i < words.size(); ++i) { + const auto& w = words[i]; + if (i == 0) { u.words.push_back({ w, "compiler", layers.toolchainName }); continue; } + if (w == "-c" || w == inv.file || w.starts_with("-o ")) continue; + // The defines the engine gives every unit of a target travel with + // the package's own flags. + if (w.starts_with("-D__MCPP_")) { u.words.push_back({ w, "engine", "the target's define" }); continue; } + if (ownSet.contains(w) || includes.contains(w)) { + u.words.push_back({ w, "package", cu.packageName }); + continue; + } + u.words.push_back(attribute(layers, w)); + } + o.units.push_back(std::move(u)); + } + const auto ldOwn = joined(mcpp::manifest::flag_words(plan.manifest.buildConfig.ldflags)); + const std::set ldOwnSet(ldOwn.begin(), ldOwn.end()); + for (auto const& w : joined(split_flags(flags.ld))) { + if (ldOwnSet.contains(w)) { o.link.push_back({ w, "package", root }); continue; } + o.link.push_back(attribute(layers, w)); + } + return o; +} + +nlohmann::json flag_origins_json(const FlagOrigins& o) { + auto words = [](const std::vector& ws) { + auto a = nlohmann::json::array(); + for (auto const& w : ws) + a.push_back({ { "word", w.word }, { "layer", w.layer }, { "detail", w.detail } }); + return a; + }; + auto units = nlohmann::json::array(); + for (auto const& u : o.units) + units.push_back({ { "file", u.file }, { "package", u.package }, { "words", words(u.words) } }); + return { { "identity", o.identity }, { "units", units }, { "link", words(o.link) } }; +} + +} // namespace mcpp::build diff --git a/src/build/flag_origins.cppm b/src/build/flag_origins.cppm new file mode 100644 index 000000000..453b87741 --- /dev/null +++ b/src/build/flag_origins.cppm @@ -0,0 +1,48 @@ +// mcpp.build.flag_origins — every word a unit is compiled with and its link +// line, each with the layer that contributed it (mcpp#911 G2; SPEC-016 §5.2). +// +// The command lines are the build's own (`unit_invocations`, `compute_flags`), +// so what is explained is what runs. A unit's line has three parts the engine +// writes in order: the package's include directories, the configuration's +// flags, and the package's own flags; the first and the last are the +// package's by construction. The configuration's flags are one string the +// engine composes from the row, the platform, the toolchain and its own +// conventions, and a word of it is attributed to the first of these that +// gives it: the row's machine description, the platform's `[platform.build]` and `[platform.link]`, a path inside +// the toolchain's payload, and otherwise the engine. A word two layers give +// is the first one's. + +export module mcpp.build.flag_origins; + +import std; +import nlohmann.json; +import mcpp.build.plan; + +export namespace mcpp::build { + +struct FlagWord { + std::string word; + std::string layer; // compiler | row | platform | toolchain | package | engine + std::string detail; // the row, the statement, the package, the toolchain +}; + +struct UnitFlagOrigins { + std::string file; + std::string package; + std::vector words; +}; + +struct FlagOrigins { + std::string identity; + std::vector units; + std::vector link; // the configuration's link flags +}; + +// The units whose source path ends with `unit` (all of the root package's when +// empty), and the link flags. +FlagOrigins flag_origins(const BuildPlan& plan, std::string_view unit); + +nlohmann::json flag_origins_json(const FlagOrigins& o); + +} // namespace mcpp::build + diff --git a/src/build/flags.cppm b/src/build/flags.cppm index 6cb371fbf..00ff86647 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -1213,6 +1213,13 @@ CompileFlags compute_flags_impl(const BuildPlan& plan, bool packageLinkFlags) { // `/LTCG` on the link and the archive; before, it was not spelled at all, // and a build with `lto = true` reported `+ lto` while doing none. if (prof.lto) opt_flag += isMsvcDialect ? " /GL" : " -flto"; + // ONE SECTION PER FUNCTION AND PER OBJECT (design 2026-11-09 §17.2), so + // that a link that keeps what it reaches (a test, `--gc-sections`) drops + // the rest of an object a module initializer pulls in. Measured on mcpp's + // unit tests: with whole objects the collection kept 15 of 17 MB of code. + // The freestanding prefix states it for its own units; cl.exe's `/Gy` is + // its default under optimisation. + if (!isMsvcDialect && !isFreestandingTarget) opt_flag += " -ffunction-sections -fdata-sections"; // `ndebug` (design 2026-10-23 §15.4): the profiles that produce what is // shipped define NDEBUG for every unit of the graph, as CMake's Release, // Meson's `b_ndebug=if-release` and Cargo's release do. @@ -1992,11 +1999,18 @@ CompileFlags compute_flags_impl(const BuildPlan& plan, bool packageLinkFlags) { // Not a new condition but the string that names the machine: a mingw // g++ has an empty `crossTargetFlag`, so this is the identity for the // case the paragraph was written about, and the fix for the other one. - f.ld = std::format("{}{}{}{}{}", crossTarget, link_intent_ld, + // CLANG PAIRED WITH THE MinGW-w64 PAYLOAD (SPEC-016 §6): the payload + // is the sysroot, libstdc++ and libgcc its runtime, lld the linker; + // `-pthread` because the payload's threads are winpthreads, which the + // clang driver does not add by itself. + const std::string paired = plan.toolchain.pairedSysroot.empty() ? std::string{} + : " --no-default-config --sysroot=" + ninjaEsc(plan.toolchain.pairedSysroot) + + " -stdlib=libstdc++ -rtlib=libgcc -fuse-ld=lld -pthread"; + f.ld = std::format("{}{}{}{}{}{}", crossTarget, paired, link_intent_ld, user_ldflags, mingw_stdexp, link_extra); // `-lstdc++exp` is named explicitly, so swapping g++ for gcc would not // drop it — the C line has to leave it out. - f.ldC = std::format("{}{}{}{}", crossTarget, link_intent_ld, + f.ldC = std::format("{}{}{}{}{}", crossTarget, paired, link_intent_ld, user_ldflags, link_extra); return f; } @@ -2456,7 +2470,7 @@ CompileFlags compute_flags_impl(const BuildPlan& plan, bool packageLinkFlags) { if (auto spec = mcpp::freestanding::resolve( *mcpp::toolchain::triple::parse(plan.toolchain.targetTriple))) { const auto prefix = mcpp::freestanding::compile_prefix( - *spec, plan.toolchain.targetCxxRuntime); + *spec, plan.toolchain.targetCxxRuntime, plan.toolchain.hostedC); f.cxx += prefix; f.cc += prefix; f.as += mcpp::freestanding::assemble_prefix(*spec); @@ -2512,7 +2526,14 @@ CompileFlags compute_flags_impl(const BuildPlan& plan, bool packageLinkFlags) { // needed either: the payload's clang config file is a DRIVER // config, and nothing here runs the driver. std::string fsLd; - if (!spec->lldEmulation.empty()) { + // A PLATFORM'S `link.driver = "compiler"` IS HONOURED ON EVERY ROW + // (mcpp#911 G4): the compiler driver then links, adding the start + // files and libraries it knows for the target, where the row alone + // would call ld.lld with its emulation. + const bool platformCompilerDriver = plan.platform.active + && plan.platform.fields.link.driver.set + && plan.platform.fields.link.driver.value == "compiler"; + if (!spec->lldEmulation.empty() && !platformCompilerDriver) { f.ldDriver = in.lld; fsLd = mcpp::freestanding::link_flags_direct(*spec, in, ninjaEsc); } else { diff --git a/src/build/hostprogram.cppm b/src/build/hostprogram.cppm index 2b17a310c..dbd481a7a 100644 --- a/src/build/hostprogram.cppm +++ b/src/build/hostprogram.cppm @@ -541,6 +541,9 @@ inline const char* target() { return env_or("MCPP_TARGET") inline const char* target_os() { return env_or("MCPP_TARGET_OS"); } inline const char* target_arch() { return env_or("MCPP_TARGET_ARCH"); } inline const char* target_env() { return env_or("MCPP_TARGET_ENV"); } +// The provider of the target's identity (SPEC-016 §4): `openkal`, `musl-gcc`, ...; +// "" for the host's own build. +inline const char* target_provider() { return env_or("MCPP_TARGET_PROVIDER"); } inline const char* host() { return env_or("MCPP_HOST"); } inline const char* profile() { return env_or("MCPP_PROFILE"); } // The device axis of this build: `cuda12.9+{sm_89} ptx>=89`, or "" when the diff --git a/src/build/plan.cppm b/src/build/plan.cppm index 7d4b44d53..fceb6604c 100644 --- a/src/build/plan.cppm +++ b/src/build/plan.cppm @@ -1932,6 +1932,9 @@ make_plan(const mcpp::manifest::Manifest& manifest, const mcpp::platform::Resolved* platform) { BuildPlan plan; + // The artifacts' own compiles of shared sources (mcpp#911 G9), joined to + // the plan's units after every link unit is formed. + std::vector artifactVariants; plan.manifest = manifest; plan.toolchain = tc; plan.fingerprint = fp; @@ -2036,6 +2039,23 @@ make_plan(const mcpp::manifest::Manifest& manifest, // than link.exe does, so "which compiler binary" is the wrong question. return targetOs == "windows" && mcpp::toolchain::is_msvc_target(tc); }(); + // A TEST KEEPS WHAT IT REACHES (design 2026-11-09 §17.2): every object of + // the package is on its link line, and a test of one module used to carry + // all of them -- 32 MB for each of mcpp's 166 unit tests. The linker drops + // the sections nothing references; static constructors are its roots, so + // a case that registers itself stays. `[test] link = "objects"` keeps + // every object whole. ELF and Mach-O through the compiler driver; MSVC's + // link.exe, a freestanding image, a platform's ld.lld and wasm keep their + // own link lines. + auto keep_what_tests_reach = [&](LinkUnit& lu, const mcpp::manifest::Manifest& owner) { + if (owner.testLink == "objects" || msvcTarget || targetTriple.is_freestanding()) return; + // A platform that links with ld.lld itself states its own link line + // (an ok image collects its sections there already). + if (platform && platform->active && platform->fields.link.driver.value == "lld") return; + if (!targetTriple.empty() && targetTriple.str().starts_with("wasm")) return; + const bool macho = targetTriple.empty() ? bool(mcpplibs::os::is_macos) : targetTriple.is_mach_o(); + lu.linkFlags.push_back(macho ? "-Wl,-dead_strip" : "-Wl,--gc-sections"); + }; bool experimentalStd = false; if (auto stdCfg = mcpp::manifest::normalize_cpp_standard(manifest.package.standard)) { @@ -3598,6 +3618,7 @@ make_plan(const mcpp::manifest::Manifest& manifest, lu.kind = LinkUnit::TestBinary; lu.output = target_output(t, naming); if (!t.main.empty()) lu.entryMain = projectRoot / t.main; + keep_what_tests_reach(lu, manifest); } else if (t.kind == mcpp::manifest::Target::Application && mcpp::toolchain::triple::application_form(targetTriple) == mcpp::toolchain::triple::ApplicationForm::SharedObject) { @@ -3800,6 +3821,25 @@ make_plan(const mcpp::manifest::Manifest& manifest, // Also include implementation .cpp/.cc/.cxx/.c units, but EXCLUDE any // file registered as another target's entryMain (each binary's main() // is exclusive to that binary). + // THE ARTIFACT'S OWN COMPILE (mcpp#911 G9): `[targets..build]` + // makes a variant of each of the package's implementation units this + // artifact links, under `obj/_variant//`, with the artifact's flags + // after the package's; the entry takes them in place. Module + // interfaces are one per package and are not varied. + const auto rootPackage = qualified_package_name(manifest); + auto vary = [&](CompileUnit& cu) { + for (auto const& d : t.buildDefines) { + const auto element = mcpp::manifest::flag_element("-D" + d); + cu.packageCflags.push_back(element); + cu.packageCxxflags.push_back(element); + } + for (auto const& f : t.buildCflags) cu.packageCflags.push_back(f); + for (auto const& f : t.buildCxxflags) cu.packageCxxflags.push_back(f); + }; + std::vector variants; + if (t.has_build_variant() && lu.entryMain) + for (auto& cu : plan.compileUnits) + if (cu.source == *lu.entryMain && cu.packageName == rootPackage) vary(cu); for (auto& cu : plan.compileUnits) { if (sharedDepPackages.contains(cu.packageName)) continue; if (placedInImage.contains(cu.packageName)) continue; @@ -3813,8 +3853,19 @@ make_plan(const mcpp::manifest::Manifest& manifest, // it supplies the entry (gtest-style). Works on every linker — the // object is linked directly, never relying on archive member pulling. if (entryDefinesMain && depMainSources.contains(cu.source)) continue; + if (t.has_build_variant() && cu.packageName == rootPackage && cu.providesModule.empty()) { + CompileUnit v = cu; + v.object = std::filesystem::path("obj") / "_variant" / sanitize(t.name) + / cu.object.lexically_relative("obj"); + vary(v); + lu.objects.push_back(v.object); + variants.push_back(std::move(v)); + continue; + } lu.objects.push_back(cu.object); } + // Added once every link unit is formed: no other artifact links them. + for (auto& v : variants) artifactVariants.push_back(std::move(v)); if (lu.kind == LinkUnit::Binary || lu.kind == LinkUnit::TestBinary || lu.kind == LinkUnit::SharedLibrary) { @@ -4120,6 +4171,7 @@ make_plan(const mcpp::manifest::Manifest& manifest, : place(target_output(t, naming)); lu.windowsSubsystem = t.windowsSubsystem; lu.windowsEntry = t.windowsEntry; + if (lu.kind == LinkUnit::TestBinary) keep_what_tests_reach(lu, pkg.manifest); } lu.loaderTagFlag = loader_tag_flag(lu.kind); for (auto const& other : plan.linkUnits) @@ -4517,17 +4569,38 @@ make_plan(const mcpp::manifest::Manifest& manifest, // unit's objects, a workspace's member archives held its closure: 11 433 // members for 2 666 objects, 5.3 GB (mcpp#874). An object that is not a // compile unit's -- an `object` action's -- is the package's own and stays. - { - std::map ownerOfObject; - for (auto const& cu : plan.compileUnits) ownerOfObject.emplace(cu.object, cu.packageName); - for (auto& lu : plan.linkUnits) { - if (lu.kind != LinkUnit::StaticLibrary) continue; - std::erase_if(lu.objects, [&](const std::filesystem::path& o) { - auto it = ownerOfObject.find(o); - return it != ownerOfObject.end() && it->second != lu.package; - }); - } + std::map ownerOfObject; + for (auto const& cu : plan.compileUnits) ownerOfObject.emplace(cu.object, cu.packageName); + for (auto& lu : plan.linkUnits) { + if (lu.kind != LinkUnit::StaticLibrary) continue; + std::erase_if(lu.objects, [&](const std::filesystem::path& o) { + auto it = ownerOfObject.find(o); + return it != ownerOfObject.end() && it->second != lu.package; + }); + } + // THE C LIBRARY LEADS A MACH-O IMAGE. This format's loader runs an image's + // initialisers before its entry, in link order, so a C library of the + // graph (`mcpp:c-abi`, openkal-musl) is brought up by an initialiser of + // its own (openkal-musl's port/src/mach/early_init.c). Linked after the + // program's objects, it came too late: the registration of a test, a + // global constructor that allocates, stopped in an allocator not yet + // started (mcpplibs/os#22, hosted macOS). Its objects precede every other + // package's, in their own order, as a C runtime's start objects do on ELF. + if (targetTriple.is_mach_o()) { + std::set cLibraries; + for (auto const& pkg : packages) + if (std::ranges::any_of(pkg.manifest.provides, + [](const std::string& p) { return p.starts_with("mcpp:c-abi"); })) + cLibraries.insert(qualified_package_name(pkg.manifest)); + auto of_c_library = [&](const std::filesystem::path& o) { + auto it = ownerOfObject.find(o); + return it != ownerOfObject.end() && cLibraries.contains(it->second); + }; + if (!cLibraries.empty()) + for (auto& lu : plan.linkUnits) + if (lu.kind != LinkUnit::StaticLibrary) std::ranges::stable_partition(lu.objects, of_c_library); } + for (auto& v : artifactVariants) plan.compileUnits.push_back(std::move(v)); return plan; } diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index 64050d600..c812f1082 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -458,6 +458,22 @@ export struct BuildProgramGraph { std::map programOf; }; +// One candidate of a request's selection, and the selection (SPEC-016 §4), for +// `mcpp why target` and the machine output. `request` empty: the build is the +// host's own, which no selection decides. +export struct TargetCandidate { + std::string identity, provider, why; + int priority = 0; + bool available = false; +}; + +export struct TargetSelection { + std::string request, identity, provider, why; + int priority = 0; + bool fallback = false; + std::vector candidates; +}; + export struct BuildContext { // The build programs' graphs this prepare planned to describe them // (`plan_only`; empty otherwise). @@ -592,6 +608,8 @@ export struct BuildContext { std::string replaced; // the spec displaced, when one was }; CompilerChoice compilerChoice; + // WHICH ROW AND WHICH PROVIDER THE REQUEST WAS GIVEN (SPEC-016 §4). + TargetSelection targetSelection; // THE SOURCES THIS BUILD USED (mcpp#755), one entry per subject. See // SourceDecision. std::vector sources; @@ -883,6 +901,11 @@ export struct BuildOverrides { std::shared_ptr inherited_runtime_binding; std::string target_triple; // empty = host triple, fall through to [toolchain] + // THE REQUEST, WHEN THE IDENTITY WAS GIVEN BY THE C LIBRARY (SPEC-016 §5): a + // first pass found that the graph or the platform supplies a C library the + // selected row does not name, and prepared again for the row that names it. + // The report and the lock name this request; `target_triple` is the identity. + std::optional target_request; // --accel: the device backends and architectures this build targets, in // the wire form mcpp.pack.abi_tag reads. Overrides `[build] accel`, the // same relationship --target has with [toolchain]. diff --git a/src/build/prepare/build_programs.cpp b/src/build/prepare/build_programs.cpp index f8335d737..5eac0da5e 100644 --- a/src/build/prepare/build_programs.cpp +++ b/src/build/prepare/build_programs.cpp @@ -95,6 +95,7 @@ std::expected step9_root_build_program(PrepareState& state) { if (!host) return std::unexpected(host.error()); mcpp::build::BuildProgramEnv bpEnv; bpEnv.targetTriple = state.resolvedTargetCanonical; + bpEnv.targetProvider = state.targetChoice.provider; // Everything the engine already knows and a build program would // otherwise hardcode: the payload ROOT (not the driver), the target's // C library, which compiler and which C++ standard library resolved, @@ -512,6 +513,7 @@ std::expected step9_member_build_programs(PrepareState& state auto& pkg = state.packages[i]; mcpp::build::BuildProgramEnv bpEnv; bpEnv.targetTriple = state.resolvedTargetCanonical; + bpEnv.targetProvider = state.targetChoice.provider; fill_target_build_env(bpEnv, *state.m, state.tc ? &*state.tc : nullptr, state.cfg_opt ? &*state.cfg_opt : nullptr); apply_graph_cxx_abi(bpEnv, state.packages); diff --git a/src/build/prepare/driver.cpp b/src/build/prepare/driver.cpp index c3213ce92..09fa5efe4 100644 --- a/src/build/prepare/driver.cpp +++ b/src/build/prepare/driver.cpp @@ -90,7 +90,18 @@ prepare_build(bool print_fingerprint, return prepare_build_pass(print_fingerprint, includeDevDeps, std::move(extraTargets), std::move(overrides)); auto first = prepare_build_pass(print_fingerprint, includeDevDeps, extraTargets, overrides); - if (first) { (void)take_toolchain_restart(); (void)take_platform_restart(); return first; } + if (first) { + (void)take_toolchain_restart(); + (void)take_platform_restart(); + (void)take_target_restart(); + return first; + } + if (auto identity = take_target_restart(); identity && !overrides.target_request) { + overrides.target_request = overrides.target_triple; + overrides.target_triple = std::move(*identity); + return prepare_build(print_fingerprint, includeDevDeps, std::move(extraTargets), + std::move(overrides)); + } // A platform that states a target base, fetched by this pass: the next // reads its `[platform]` before the base is decided (SPEC-013 §7). if (auto root = take_platform_restart(); root && overrides.platform_root.empty()) { diff --git a/src/build/prepare/features.cpp b/src/build/prepare/features.cpp index 4d0b75e64..8f5d7df8e 100644 --- a/src/build/prepare/features.cpp +++ b/src/build/prepare/features.cpp @@ -1856,6 +1856,7 @@ static std::expected step6_dependency_build_programs(PrepareS auto [req, depDefaultFeatures] = aggregatedRequest(state, i, Instance::Target); mcpp::build::BuildProgramEnv bpEnv; bpEnv.targetTriple = state.resolvedTargetCanonical; + bpEnv.targetProvider = state.targetChoice.provider; // Everything the engine already knows and a build program would // otherwise hardcode: the payload ROOT (not the driver), the // target's C library, which compiler and which C++ standard diff --git a/src/build/prepare/graph.cpp b/src/build/prepare/graph.cpp index 4f34bfa3d..3546c3306 100644 --- a/src/build/prepare/graph.cpp +++ b/src/build/prepare/graph.cpp @@ -1472,6 +1472,42 @@ step4b_acquire_dependency_source(PrepareState& state, WorklistItemCtx& ctx) { const std::string lockKey = rootDeclared ? name : mcpp::pm::compat::qualified_name( spec.namespace_, spec.shortName.empty() ? name : spec.shortName); + // THE DEFAULT BRANCH (SPEC-001 §5): a git reference that names no + // `tag`, `rev` or `branch` follows the remote's default branch, + // whose name the remote announces (`git ls-remote --symref`); the + // lock's entry for the same remote answers it offline. A remote + // that announces none is tried for `main`, then `master`. + if (spec.gitRefKind == "branch" && spec.gitRev.empty()) { + if (auto it = state.gitLockAnchors.find(lockKey); + it != state.gitLockAnchors.end() && it->second.url == spec.git + && it->second.refKind == "branch" && !it->second.ref.empty()) + spec.gitRev = it->second.ref; + else if (offline) + return refuse_offline("`git ls-remote`", + "its default branch is resolved from the remote, and mcpp.lock records none", + "resolve"); + else { + auto head = run_with_network_retry({ "git", "ls-remote", "--symref", spec.git, "HEAD" }); + if (head.exit_code == 0) + if (auto p = head.output.find("ref: refs/heads/"); p != std::string::npos) { + const auto from = p + 16; + const auto end = head.output.find_first_of("\t \r\n", from); + spec.gitRev = head.output.substr(from, end == std::string::npos ? end : end - from); + } + for (auto candidate : { "main", "master" }) { + if (!spec.gitRev.empty()) break; + auto r = run_with_network_retry({ "git", "ls-remote", spec.git, + std::format("refs/heads/{}", candidate) }); + if (r.exit_code == 0 && !r.output.empty()) spec.gitRev = candidate; + } + if (spec.gitRev.empty()) + return std::unexpected(std::format( + "git dependency '{}': '{}' announces no default branch and has neither " + "`main` nor `master`; name one with tag, rev or branch", name, spec.git)); + mcpp::ui::info("Resolved", std::format("{}: the default branch of {} is {}", + name, spec.git, spec.gitRev)); + } + } if (spec.gitRefKind == "branch") { auto it = state.gitLockAnchors.find(lockKey); if (it != state.gitLockAnchors.end() @@ -1644,9 +1680,15 @@ step4b_acquire_dependency_source(PrepareState& state, WorklistItemCtx& ctx) { const auto variant = (*cfg)->metaCacheDir / "src" / (key.ns.empty() ? key.shortName : key.ns + "." + key.shortName) / mcpp::pm::patch::variant_dir_name(resolvedGitRev.substr(0, 12), spec.patchesDigest); + if (std::filesystem::path(spec.patchRoot).lexically_normal().generic_string().starts_with("..")) + return std::unexpected(std::format( + "dependency '{}': patch_root '{}' leaves the package's source", name, spec.patchRoot)); + std::vector notes; if (auto r = mcpp::pm::patch::materialize(dep_root, spec.patches, - spec.patchesDigest, variant); !r) + spec.patchesDigest, variant, + spec.patchRoot, ¬es); !r) return std::unexpected(std::format("dependency '{}': {}", name, r.error())); + for (auto const& n : notes) mcpp::ui::note(std::format("dependency '{}': {}", name, n)); dep_root = variant; } } @@ -1748,7 +1790,7 @@ step4b_acquire_dependency_source(PrepareState& state, WorklistItemCtx& ctx) { auto loaded = spec.patches.empty() ? state.loadVersionDep(name, key.ns, key.shortName, spec.version) : state.loadVersionDepVariant(name, key.ns, key.shortName, spec.version, - spec.patches, spec.patchesDigest); + spec.patches, spec.patchesDigest, spec.patchRoot); if (!loaded) return std::unexpected(loaded.error()); dep_root = std::move(loaded->first); dep_manifest = std::move(loaded->second); @@ -2059,11 +2101,15 @@ static std::expected step4b_cycle_check(PrepareState& state) // IS RESOLVED (#649 E6). The build-cache key walk was the only reader that // noticed, and it runs for the global cache only, so the same manifest was // refused by default and built under `--cache=local`. Every edge counts, - // build-only ones included, as the key walk counts them. + // build-only ones included, as the key walk counts them -- but the root's + // own `[dev-dependencies]`: they serve its tests, which no package + // depends on, so a framework that uses the package under test reaches its + // library and not back through the tests (names_the_root, graph_edges.cpp). { mcpp::graph::AdjacencyList deps(state.packages.size()); for (auto const& e : state.dependencyEdges) { if (e.consumerPackageIndex >= deps.size()) continue; + if (e.consumerPackageIndex == 0 && e.devTable) continue; if (e.dependencyPackageIndex >= state.packages.size()) continue; deps[e.consumerPackageIndex].push_back(e.dependencyPackageIndex); } @@ -2397,6 +2443,7 @@ std::expected phase4b_graph_worklist(PrepareState& state) { if (auto r = step4b_resolve_identity(state, ctx); !r) return std::unexpected(r.error()); + if (names_the_root(state, ctx.item, ctx.key)) continue; if (auto it = state.resolved.find(ctx.key); it != state.resolved.end()) { if (auto r = step4b_handle_already_resolved(state, ctx, it); !r) @@ -2425,7 +2472,11 @@ std::expected phase4b_graph_worklist(PrepareState& state) { auto pushed = step4b_apply_platform(state); if (!pushed) return std::unexpected(pushed.error()); if (auto r = step4b_check_platform_versions(state); !r) return r; - if (!*pushed) break; + // AND A PACKAGE'S OWN DEFAULTS AFTER THE PLATFORM'S LAYERS (mcpp#911 + // G7): graph > platform > default. + auto defaulted = step4b_capability_defaults(state); + if (!defaulted) return std::unexpected(defaulted.error()); + if (!*pushed && !*defaulted) break; if (auto r = drain(); !r) return r; } if (auto r = step4b_check_platform_versions(state); !r) return r; diff --git a/src/build/prepare/graph_edges.cpp b/src/build/prepare/graph_edges.cpp index 71c7dcd98..85deca7bd 100644 --- a/src/build/prepare/graph_edges.cpp +++ b/src/build/prepare/graph_edges.cpp @@ -10,6 +10,7 @@ import std; import mcpp.manifest; import mcpp.modgraph.graph; import mcpp.pm.dep_spec; +import mcpp.ui; namespace mcpp::build { @@ -136,6 +137,28 @@ void recordDependencyEdge( }); } +// THE PACKAGE BEING BUILT STANDS FOR ITSELF IN ITS TESTS' GRAPH. A dependency +// reached through its `[dev-dependencies]` may name it: a library tested with a +// framework that itself uses the library (mcpplibs.os under mtest). A second +// copy from the index would provide every module of the package twice in one +// program, and what is under test is the package as it is here; so the edge is +// the root's, whatever version it asked for, and a version it did not ask for +// is said. Outside the tests' graph such an edge is a cycle, refused as one. +bool names_the_root(PrepareState& state, const WorkItem& item, const ResolvedKey& key) { + const auto& root = state.m->package; + if (!item.devOnly || item.consumerDepIndex == kMainConsumer || root.virtualRoot || !key.variant.empty() + || key.shortName != root.name || key.ns != root.namespace_) + return false; + recordDependencyEdge(state, item.consumerDepIndex, 0, item.spec, item.fromBuildTable, item.name); + if (!item.originalConstraint.empty() && item.originalConstraint != root.version + && item.originalConstraint != "*") + mcpp::ui::note(std::format( + "{} asks for {} {}; it is the package being built, {}, which stands for itself", + item.requestedBy, state.qualifiedKey(key), item.originalConstraint, root.version)); + return true; +} + + // The usage requirements every edge carries to its consumer: a dependency's // public usage reaches the consumer's own units (Private, Public), its users // (Public, Interface), or, on a dev edge, the consumer's tests only. A fixpoint diff --git a/src/build/prepare/graph_load.cpp b/src/build/prepare/graph_load.cpp index e17d76558..f527663e2 100644 --- a/src/build/prepare/graph_load.cpp +++ b/src/build/prepare/graph_load.cpp @@ -1057,6 +1057,7 @@ step4a_load_version_dep_fetch(PrepareState& state, LoadVersionDepCtx& ctx) { mcpp::build::BuildProgramEnv hookEnv; fill_target_build_env(hookEnv, *state.m, state.tc ? &*state.tc : nullptr, state.cfg_opt ? &*state.cfg_opt : nullptr); hookEnv.targetTriple = state.overrides.target_triple; + hookEnv.targetProvider = state.targetChoice.provider; // Six names, fixed by install_hook_env; one guard each. const auto hookVars = mcpp::build::install_hook_env(hookEnv); mcpp::invocation::ScopedEnv hookVar0(hookVars.at(0).first, hookVars.at(0).second); @@ -1378,7 +1379,8 @@ std::expected phase4a_graph_load(PrepareState& state) { const std::string& shortName, const std::string& version, const std::vector& patches, - const std::string& digest) + const std::string& digest, + const std::string& patchRoot) -> std::expected { auto cfg = state.get_cfg(true); @@ -1403,8 +1405,32 @@ std::expected phase4a_graph_load(PrepareState& state) { const auto qualified = ns.empty() ? shortName : ns + "." + shortName; const auto variant = (*cfg)->metaCacheDir / "src" / qualified / mcpp::pm::patch::variant_dir_name(exact, digest); - if (auto r = mcpp::pm::patch::materialize(*ctx.installed, patches, digest, variant); !r) + // THE PATCH ROOT IS THE PACKAGE'S (mcpp#911 G8): the directory of the + // manifest its descriptor names, inside the archive, and the edge's + // `patch_root` below it. + std::filesystem::path packageRoot; + if (!ctx.luaContent) ctx.luaContent = step4a_read_lua_content(state, ctx); + if (ctx.luaContent) { + const auto field = mcpp::manifest::extract_mcpp_field(*ctx.luaContent); + std::vector found; + if (field.kind == mcpp::manifest::McppField::StringPath) + found = mcpp::modgraph::expand_glob(*ctx.installed, field.value); + else if (field.kind != mcpp::manifest::McppField::TableBody) + for (auto pat : { "mcpp.toml", "*/mcpp.toml" }) + if (found = mcpp::modgraph::expand_glob(*ctx.installed, pat); !found.empty()) break; + if (found.size() == 1) + packageRoot = std::filesystem::relative(found.front().parent_path(), *ctx.installed); + } + if (packageRoot == ".") packageRoot.clear(); + const auto patchAt = (packageRoot / patchRoot).lexically_normal(); + if (patchAt.generic_string().starts_with("..")) + return std::unexpected(std::format( + "dependency '{}': patch_root '{}' leaves the package's source", depName, patchRoot)); + std::vector notes; + if (auto r = mcpp::pm::patch::materialize(*ctx.installed, patches, digest, variant, + patchAt == "." ? std::filesystem::path{} : patchAt, ¬es); !r) return std::unexpected(std::format("dependency '{}': {}", depName, r.error())); + for (auto const& n : notes) mcpp::ui::note(std::format("dependency '{}': {}", depName, n)); ctx.installed = variant; return step4a_load_version_dep_read_manifest(state, ctx); }; diff --git a/src/build/prepare/local_toolchain.cpp b/src/build/prepare/local_toolchain.cpp index dca3de3f6..2564196fb 100644 --- a/src/build/prepare/local_toolchain.cpp +++ b/src/build/prepare/local_toolchain.cpp @@ -366,6 +366,7 @@ std::expected step6_toolchain_phase(PrepareState& state) { if (!host) return std::unexpected(host.error()); mcpp::build::BuildProgramEnv bpEnv; bpEnv.targetTriple = state.resolvedTargetCanonical; + bpEnv.targetProvider = state.targetChoice.provider; fill_target_build_env(bpEnv, *state.m, state.tc ? &*state.tc : nullptr, state.cfg_opt ? &*state.cfg_opt : nullptr); apply_graph_cxx_abi(bpEnv, state.packages); diff --git a/src/build/prepare/plan.cpp b/src/build/prepare/plan.cpp index 556a08b3f..e34c1351b 100644 --- a/src/build/prepare/plan.cpp +++ b/src/build/prepare/plan.cpp @@ -1588,6 +1588,36 @@ static std::expected step13_assembly_units(PrepareState& stat } +// The root's own `[dev-dependencies]` serve its tests, not its library, which a +// framework that uses the package under test reaches (names_the_root, +// graph_edges.cpp): the root's key does not fold them in. +static bool serves_root_tests(const DependencyEdge& e) { + return e.consumerPackageIndex == 0 && e.devTable; +} + +// The package graph the keys fold along: the declared edges, and the imports no +// declared edge covers. An import a declared edge already covers adds nothing. +// One that a declared path runs the other way is a module this build generates +// for the importer below its consumer's root, filed under the consumer by its +// path (e2e 849): not an upstream, and left out of `importedFrom` rather than +// closing a cycle. +static mcpp::graph::AdjacencyList key_graph(const PrepareState& state, + std::vector>& importedFrom) { + mcpp::graph::AdjacencyList pkgDeps(state.packages.size()); + for (auto& e : state.dependencyEdges) + if (e.consumerPackageIndex < pkgDeps.size() && !serves_root_tests(e)) + pkgDeps[e.consumerPackageIndex].push_back(e.dependencyPackageIndex); + for (std::size_t p = 0; p < importedFrom.size(); ++p) + std::erase_if(importedFrom[p], [&](std::size_t q) { + if (std::ranges::find(pkgDeps[p], q) != pkgDeps[p].end()) return true; + const auto above = mcpp::graph::closure(pkgDeps, std::array{q}, /*include_roots=*/false); + return std::ranges::find(above, p) != above.end(); + }); + for (std::size_t p = 0; p < importedFrom.size(); ++p) + pkgDeps[p].insert(pkgDeps[p].end(), importedFrom[p].begin(), importedFrom[p].end()); + return pkgDeps; +} + static std::expected step13_dependency_cache(PrepareState& state, BuildContext& ctx) { // ─── Global dependency cache: per-package keys, hit → stage edges ── // @@ -1728,24 +1758,7 @@ static std::expected step13_dependency_cache(PrepareState& st // below walks `mcpp::graph::topological_order` — dependencies before // dependents — instead of recursing, with an explicit cycle check up // front standing in for the old in-progress guard. - mcpp::graph::AdjacencyList pkgDeps(state.packages.size()); - for (auto& e : state.dependencyEdges) - if (e.consumerPackageIndex < pkgDeps.size()) - pkgDeps[e.consumerPackageIndex].push_back(e.dependencyPackageIndex); - // An import a declared edge already covers adds nothing. One that a - // declared path runs the other way is a module this build generates - // for the importer below its consumer's root, filed under the - // consumer by its path (e2e 849): not an upstream, and left out - // rather than closing a cycle. - for (std::size_t p = 0; p < importedFrom.size(); ++p) - std::erase_if(importedFrom[p], [&](std::size_t q) { - if (std::ranges::find(pkgDeps[p], q) != pkgDeps[p].end()) return true; - const auto above = mcpp::graph::closure(pkgDeps, std::array{q}, /*include_roots=*/false); - return std::ranges::find(above, p) != above.end(); - }); - for (std::size_t p = 0; p < importedFrom.size(); ++p) - pkgDeps[p].insert(pkgDeps[p].end(), importedFrom[p].begin(), importedFrom[p].end()); - auto keyOrder = mcpp::graph::topological_order(pkgDeps); + auto keyOrder = mcpp::graph::topological_order(key_graph(state, importedFrom)); if (!keyOrder) { keyCycleError = std::format( "dependency cycle through package '{}' while computing " @@ -1796,7 +1809,7 @@ static std::expected step13_dependency_cache(PrepareState& st && state.dep_cache_identities[idx - 1].sourceKind == "version"; if (!selfIsIndex) localTaint[idx] = 1; for (auto& e : state.dependencyEdges) { - if (e.consumerPackageIndex != idx) continue; + if (e.consumerPackageIndex != idx || serves_root_tests(e)) continue; // `idx`'s dependencies precede it in `keyOrder`, so their // keys and taint are already folded in below. auto const& up = pkgKeys[e.dependencyPackageIndex]; @@ -2188,6 +2201,20 @@ std::expected phase13_finish(PrepareState& state) { ctx.runtimeBinding = state.targetRuntimeBinding ? *state.targetRuntimeBinding : state.runtimeBindingSnapshot; ctx.runUnder = state.runUnder; + { + auto const& c = state.targetChoice; + auto& s = ctx.targetSelection; + s.request = c.request; + s.identity = c.identity.empty() ? state.tc->targetTriple : c.identity; + s.provider = c.provider; + s.priority = c.priority; + s.fallback = c.fallback; + s.why = c.why; + for (auto const& k : c.candidates) + s.candidates.push_back({ k.row ? std::string(k.row->triple) : std::string{}, + k.provider ? std::string(k.provider->name) : std::string{}, + k.why, k.priority, k.available }); + } if (!state.runUnder.empty()) { auto const& o = state.runUnderOrigin; ctx.runUnderOrigin = o.originKind == "env" ? o.originKey diff --git a/src/build/prepare/platform.cpp b/src/build/prepare/platform.cpp index 1d92e7fd0..a283077ad 100644 --- a/src/build/prepare/platform.cpp +++ b/src/build/prepare/platform.cpp @@ -30,7 +30,12 @@ import mcpp.project; import mcpp.targetside; import mcpp.toolchain.triple; import mcpp.toolchain.platform; +import mcpp.toolchain.target_schema; +import mcpp.toolchain.target_rows; +import mcpp.toolchain.target_select; +import mcpp.toolchain.builtin_targets; import mcpp.ui; +import mcpp.diag; import mcpp.version_req; import mcpp.pm.resolver; import mcpp.xpkg_version; @@ -61,6 +66,16 @@ std::string known_short_name(const mp::PackageRef& r) { .shortName; return {}; } + // A repository named by its URL alone is the package its last segment + // names (`.../openkal-platform.git` is `openkal-platform`); the fetched + // manifest's own name is checked against it as any dependency's is. + if (!r.spec.git.empty()) { + std::string_view url = r.spec.git; + while (!url.empty() && (url.back() == '/' || url.back() == '\\')) url.remove_suffix(1); + if (url.ends_with(".git")) url.remove_suffix(4); + const auto cut = url.find_last_of("/\\:"); + return std::string(cut == std::string_view::npos ? url : url.substr(cut + 1)); + } return {}; } @@ -143,16 +158,168 @@ platform_selection_for(const mcpp::toolchain::triple::Triple& t, const BuildOver if (auto c = mcpp::config::load_or_init(/*quiet=*/true, {}, {}, mcpp::config::Initialization::ReadOnly)) config = c->platformOverrides; + // The built-in default is the platform of the provider the selection takes + // for this identity on this host (SPEC-016 §4), not of the row's first. std::string column; - if (auto* row = mcpp::toolchain::triple::find_known_target(t)) column = std::string(row->platform); + { + auto ctx = target_context(project, config, {}, {}, [] { return false; }); + auto s = mcpp::toolchain::target::select(t.str(), t.arch, t.os, t.env, true, ctx); + if (s.provider) column = std::string(s.provider->platform); + } mcpp::platform::ChoiceInputs in{ flag, env, envKey, project, config, column }; if (overrides.platform_choice.empty() && !stated_on_command_line("MCPP_PLATFORM")) in.flagKey = "MCPP_PLATFORM"; auto sel = mcpp::platform::select_target(in, matcher(cfgpred::context_for(t.str()))); if (!sel) return std::unexpected(sel.error()); + if (!sel->builtin.empty()) return "builtin:" + sel->builtin; return sel->none ? std::string{} : mp::display(sel->ref); } +// THE PLATFORM'S DEFINITIONS (SPEC-016 §2, §3): its rows, its providers, and +// `[platform]` and `[platform.hosted]` as the providers they are shorthands of, +// registered for the process so the parser, the selection and `cfg(os = …)` +// know them. `ref` is how a provider names the package (`.@`, +// or `path:`). +void define_platform(const mp::Statement& s, std::string_view shortName, std::string_view ref) { + namespace target = mcpp::toolchain::target; + target::DefinitionSet set; + set.source = std::string(ref); + const auto name = mp::platform_name(shortName); + for (auto const& d : s.defines) { + target::Row r; + r.triple = set.keep(d.triple); + std::vector seg; + for (auto part : std::views::split(d.triple, '-')) seg.emplace_back(part.begin(), part.end()); + r.arch = set.keep(seg[0]); + r.os = set.keep(seg[1]); + r.env = set.keep(seg.size() > 2 ? seg[2] : std::string{}); + r.tier = set.keep(d.tier); + r.note = set.keep(d.note); + r.standard = d.standard; + r.exact = d.exact; + r.march = set.keep(d.march); + r.mabi = set.keep(d.mabi); + r.codeModel = set.keep(d.codeModel); + r.multilib = set.keep(d.multilib); + r.flags = set.keep(d.flags); + r.linkEmulation = set.keep(d.linkEmulation); + r.clangTriple = set.keep(d.clangTriple); + r.objectFormat = set.keep(d.objectFormat); + r.family = set.keep(d.family); + r.dataModel = set.keep(d.dataModel); + r.exeSuffix = set.keep(d.exeSuffix); + r.sharedSuffix = set.keep(d.sharedSuffix); + r.loader = set.keep(d.loader); + r.descriptorKeys = set.keep(d.descriptorKeys); + r.execution = set.keep(d.execution.empty() ? seg[1] : d.execution); + r.defined = true; + set.rows.push_back(r); + } + auto provider = [&](std::string pname, const std::vector& serves, int priority, + std::string_view toolchain, const std::vector& hosts, + const std::vector& pairs, std::string_view sysroot, + std::string_view linkage) { + target::Provider p; + p.name = set.keep(std::move(pname)); + p.serves = set.keep(serves); + p.priority = priority; + p.hosts = set.keep(hosts.empty() ? std::vector{"*"} : hosts); + p.pairs = set.keep(pairs); + p.toolchain = set.keep(std::string(toolchain)); + p.sysroot = set.keep(std::string(sysroot)); + p.linkage = set.keep(std::string(linkage)); + p.platform = set.keep(std::string(ref)); + set.providers.push_back(p); + }; + // A platform the engine's own providers name (openkal-platform) is served by + // those; its shorthands are not registered twice. + const bool builtin = std::ranges::any_of(mcpp::toolchain::builtin::kProviders, [&](auto const& b) { + auto r = b.platform.empty() ? std::nullopt : std::optional(mp::parse_ref_text(b.platform)); + return r && *r && (*r)->spec.shortName == shortName; + }); + for (auto const& p : mp::stated_providers(s, name)) + if (!p.shorthand || !builtin) + provider(p.name, p.serves, p.priority, p.toolchain, p.hosts, p.pairs, p.sysroot, p.linkage); + target::define(std::move(set)); +} + +void define_early_platform(PrepareState& state) { + const auto raw = state.overrides.target_triple; + // The explicit choice for this request, by the words that need no parse: the + // flag, the environment and the tables whose selector is the request itself. + std::optional chosen; + std::string chosenText = !state.overrides.platform_choice.empty() ? state.overrides.platform_choice + : mcpp::invocation::get("MCPP_PLATFORM").value_or(""); + if (chosenText.empty() && !raw.empty()) { + std::string key = "MCPP_TARGET_"; + for (char c : raw) key += std::isalnum(static_cast(c)) ? char(std::toupper(static_cast(c))) : '_'; + chosenText = mcpp::invocation::get(key + "_PLATFORM").value_or(""); + } + if (!chosenText.empty()) { + if (auto sel = mcpp::platform::parse_choice(chosenText, mp::Origin{mp::Tier::Flag, {}, 0, "--platform"}); + sel && !sel->none) + chosen = sel->ref; + } else { + std::vector config; + if (auto cfg = state.get_cfg(false); cfg) config = (*cfg)->platformOverrides; + for (auto const* tables : { &state.m->platformOverrides, &config }) + for (auto const& o : *tables) + if (!chosen && o.selector == raw && o.platformSet && !o.platformNone) chosen = o.platform; + } + fs::path root = state.overrides.platform_root; + if (root.empty() && chosen && !chosen->spec.path.empty()) root = chosen->spec.path; + if (!root.empty()) + if (auto m = mcpp::manifest::load(root / "mcpp.toml"); m && m->platformDeclared) { + auto n = mcpp::pm::compat::resolve_package_name(m->package.name, m->package.namespace_); + define_platform(m->platform, n.shortName, "path:" + root.generic_string()); + } + // A request no definition knows, with a platform chosen for it that is not on + // this machine yet: a provisional row lets the first pass fetch the platform. + if (raw.empty() || mcpp::toolchain::triple::parse(raw) || !chosen) return; + std::vector seg; + for (auto part : std::views::split(std::string_view(raw), '-')) seg.emplace_back(part.begin(), part.end()); + if (seg.size() < 2 || seg.size() > 3 || std::ranges::any_of(seg, [](auto const& x) { return x.empty(); })) return; + namespace target = mcpp::toolchain::target; + target::DefinitionSet set; + set.provisional = true; + set.source = "provisional:" + raw; + target::Row r; + r.triple = set.keep(raw); + r.arch = set.keep(seg[0]); + r.os = set.keep(seg[1]); + r.env = set.keep(seg.size() > 2 ? seg[2] : std::string{}); + r.tier = set.keep("preview"); + r.objectFormat = set.keep("elf"); + r.family = set.keep("unix"); + r.execution = set.keep(seg[1]); + r.defined = true; + set.rows.push_back(r); + target::define(std::move(set)); + state.provisionalTarget = true; +} + +std::string named_provider(const PrepareState& state, const mcpp::toolchain::triple::Triple& t, + std::span config) { + const auto flag = !state.overrides.platform_choice.empty() + ? state.overrides.platform_choice + : mcpp::invocation::get("MCPP_PLATFORM").value_or(""); + const auto envKey = target_env_name(t, "PLATFORM"); + const auto env = mcpp::invocation::get(envKey).value_or(""); + const auto project = project_tables(*state.m); + mcpp::platform::ChoiceInputs in{ flag, env, envKey, project, config, {} }; + auto sel = mcpp::platform::select_target(in, matcher(cfgpred::context_for(t.str()))); + if (!sel || sel->origin.tier == mp::Tier::BuiltIn) return {}; + if (!sel->builtin.empty()) return sel->builtin; + if (sel->none) return "none"; + const auto chosen = known_short_name(sel->ref); + for (auto const* p : mcpp::toolchain::target::providers()) { + if (p->platform.empty()) continue; + auto ref = mp::parse_ref_text(p->platform); + if (ref && ref->spec.shortName == chosen) return std::string(p->name); + } + return {}; +} + cfgpred::Ctx platform_cfg_context(const PrepareState& state) { return cfgpred::context_for(target_of(state).str(), state.platformPackageShortName); } @@ -185,8 +352,10 @@ std::expected step1_select_platform(PrepareState& state) { auto project = project_tables(*state.m); std::vector config; if (auto cfg = state.get_cfg(false); cfg) config = (*cfg)->platformOverrides; + // The built-in default: the platform of the provider the selection took. std::string column; - if (auto* row = mcpp::toolchain::triple::find_known_target(t)) column = std::string(row->platform); + if (state.selectedTarget) column = std::string(state.selectedTarget->platform); + else if (auto* row = mcpp::toolchain::triple::find_known_target(t)) column = std::string(row->platform); mcpp::platform::ChoiceInputs in{ flag, env, envKey, project, config, column }; if (state.overrides.platform_choice.empty() && !stated_on_command_line("MCPP_PLATFORM")) in.flagKey = "MCPP_PLATFORM"; @@ -219,7 +388,7 @@ std::expected step1_select_platform(PrepareState& state) { early = state.platform.selection.ref.spec.path; if (!early.empty()) if (auto m = mcpp::manifest::load(early / "mcpp.toml"); m && m->platformDeclared) { - state.platformData = m->platform; + state.platformData = mcpp::platform::provider_view(m->platform, t.str()); state.platformEarly = true; } } @@ -318,6 +487,31 @@ std::expected step4b_apply_platform(PrepareState& state) { P.package = qualified(found->manifest); P.version = found->manifest.package.version; P.root = found->root; + define_platform(found->manifest.platform, state.platformPackageShortName, + ref.spec.path.empty() ? std::format("{}@{}", P.package, P.version) + : "path:" + ref.spec.path); + // A REQUEST ONLY A PLATFORM DEFINES (mcpp#911 G1): the first pass assumed + // the row while the platform was fetched; with its definitions read, the + // build is prepared again from the start, or refused when it defines none. + if (state.provisionalTarget) { + const auto* row = mcpp::toolchain::target::find_row(state.overrides.target_triple); + if (!row || !row->defined || std::ranges::none_of(found->manifest.platform.defines, [&](auto const& d) { + return d.triple == state.overrides.target_triple; })) { + std::string defined; + for (auto const& d : found->manifest.platform.defines) + defined += (defined.empty() ? "" : ", ") + d.triple; + refusal::record(refusal::Code::UnknownTarget); + return std::unexpected(std::format( + "unknown target '{}': no row defines it, and the platform {} ({}) defines {}.\n" + " a platform defines a target with [[platform.define]] triple = \"{}\" (SPEC-016 §2)", + state.overrides.target_triple, P.package, mcpp::platform::origin_text(P.selection.origin), + defined.empty() ? std::string("none") : defined, state.overrides.target_triple)); + } + if (!state.platformEarly) { + g_platformRestart = P.root; + return std::unexpected(std::string("mcpp:platform-restart")); + } + } auto hosted = mcpp::platform::serves(found->manifest.platform, P.package, t); if (!hosted) { refusal::record(refusal::Code::PlatformTargetUnserved); @@ -325,8 +519,10 @@ std::expected step4b_apply_platform(PrepareState& state) { mcpp::platform::origin_text(P.selection.origin))); } P.selection.hosted = *hosted; - state.platformData = *hosted ? mcpp::platform::hosted_view(found->manifest.platform) - : found->manifest.platform; + state.platformData = mcpp::platform::provider_of(found->manifest.platform, t.str()) + ? mcpp::platform::provider_view(found->manifest.platform, t.str()) + : *hosted ? mcpp::platform::hosted_view(found->manifest.platform) + : found->manifest.platform; P.fields = state.platformData; const auto ctx = platform_cfg_context(state); std::vector config; @@ -462,6 +658,73 @@ std::expected step4b_apply_platform(PrepareState& state) { return pushed; } +// THE CAPABILITY SLOTS OF ORDINARY PACKAGES (mcpp#911 G7; SPEC-002 §3.6). A +// package that requires a capability (`requires = ["mcpp:kernel-abi=openkal"]`) +// and states its default providers (`[defaults."mcpp:kernel-abi=openkal"] linux +// = "openkal-linux@^0.25"`) takes the default for the target only when no +// package of the graph provides the capability -- the consumer's choice, or +// the platform's layer, added in the rounds before. The default is the +// package's dependency, walked like one. +std::expected step4b_capability_defaults(PrepareState& state) { + std::set provided; + for (auto const& p : state.packages) + for (auto const& c : p.manifest.provides) provided.insert(c); + const auto ctx = platform_cfg_context(state); + const auto matches = matcher(ctx); + bool pushed = false; + for (std::size_t i = 0; i < state.packages.size(); ++i) { + auto const& pm = state.packages[i].manifest; + if (pm.capabilityDefaults.empty()) continue; + // A selector that names the target wins over `*`, which is the default + // of every other target. + std::vector order; + for (auto const& d : pm.capabilityDefaults) if (d.selector != "*") order.push_back(&d); + for (auto const& d : pm.capabilityDefaults) if (d.selector == "*") order.push_back(&d); + std::set done; + for (auto const* dp : order) { + auto const& d = *dp; + if (done.contains(d.capability) || provided.contains(d.capability)) continue; + if (std::ranges::find(pm.requires_, d.capability) == pm.requires_.end()) continue; + if (d.selector != "*" && !matches(d.selector)) continue; + done.insert(d.capability); + const auto key = item_key(d.ref); + const auto mark = std::format("default:{}:{}", pm.package.name, d.capability); + if (key.empty() || state.platformInjected.contains(mark)) continue; + WorkItem item; + item.name = key; + item.spec = d.ref.spec; + if (item.spec.shortName.empty()) item.spec.shortName = known_short_name(d.ref); + if (item.spec.namespace_.empty()) { + item.spec.namespace_ = std::string(mcpp::pm::kDefaultNamespace); + item.spec.namespaceOmitted = true; + } + if (item.spec.candidates.empty()) + item.spec.candidates = {{ .namespace_ = item.spec.namespace_, + .shortName = item.spec.shortName }}; + item.spec.declaredIn = std::format("{}'s default for {} ({})", pm.package.name, + d.capability, d.selector); + item.requestedBy = std::format("{} (default for {})", pm.package.name, d.capability); + item.originalConstraint = item.spec.version; + item.consumerDepIndex = i == 0 ? kMainConsumer : i - 1; + item.resolveRoot = i == 0 ? std::filesystem::path{} : state.packages[i].root; + // The package declares it, as it would by hand: the readers of its + // edges (the link's closure among them) see it. + if (i == 0) state.m->dependencies[key] = item.spec; + else { + state.packages[i].manifest.dependencies[key] = item.spec; + if (i - 1 < state.dep_manifests.size() && state.dep_manifests[i - 1]) + state.dep_manifests[i - 1]->dependencies[key] = item.spec; + } + state.platformInjected.insert(mark); + mcpp::ui::info("Default", std::format("{} for {} of {} ({})", mp::display(d.ref), + d.capability, pm.package.name, d.selector)); + state.worklist.push_back(std::move(item)); + pushed = true; + } + } + return pushed; +} + // THE PLATFORM'S VERSION GROUP IS A CONSTRAINT (SPEC-013 §7.5): a request of // one of its members, by version, resolves to the group's version when the // request admits it, and is refused naming both when it does not. A package @@ -689,9 +952,40 @@ std::vector split_words(std::string_view v) { } // namespace +// THE PLATFORM'S NAMES OF ITS ARTIFACTS STAND (mcpp#911 B2): what follows +// `{name}` in `link.exe` and `link.shared` is the platform's format (`.oke`, +// `.okso`), which its own steps read. A build program's `artifact-name` that +// would change it -- written for the host's suffix, without knowing the +// platform -- is reported, and the platform's pattern is kept. +namespace { +void keep_platform_naming(mp::Statement& s, const mp::Statement& platform, std::string_view display, + std::vector* conflicts) { + auto suffix = [](std::string_view pattern) { + const auto at = pattern.find("{name}"); + return at == std::string_view::npos ? pattern : pattern.substr(at + 6); + }; + for (auto [field, base, kind] : { std::tuple{ &s.link.exe, &platform.link.exe, "exe" }, + std::tuple{ &s.link.shared, &platform.link.shared, "shared" } }) { + if (!base->set || base->origin.tier != mp::Tier::Platform || suffix(base->value).empty()) continue; + // A build program's `artifact-name` only: the project and the machine + // state a name on purpose, and theirs stands as any field's does. + if (!field->set || field->origin.tier != mp::Tier::Program || suffix(field->value) == suffix(base->value)) + continue; + if (conflicts) + conflicts->push_back(std::format( + "{} names the {} `{}`, and the platform {} names it `{}`: the platform's name stands, " + "since its own steps read it", mcpp::platform::origin_text(field->origin) == "build-program" + ? std::string("a build program") : mcpp::platform::origin_text(field->origin), + kind == std::string_view("exe") ? "program" : "shared library", field->value, display, + base->value)); + *field = *base; + } +} +} // namespace + // ── The statement every reader uses: platform → build programs → this // machine → the project → the environment (§4.5) ────────────────────────── -mp::Statement platform_statement(const PrepareState& state) { +mp::Statement platform_statement(const PrepareState& state, std::vector* conflicts) { auto const& P = state.platform; const auto t = target_of(state); const auto cfgCtx = platform_cfg_context(state); @@ -723,6 +1017,7 @@ mp::Statement platform_statement(const PrepareState& state) { for (auto const* o : mcpp::platform::matching(config, matcher(cfgCtx))) mcpp::platform::merge(s, o->fields); auto project = project_tables(*state.m); for (auto const* o : mcpp::platform::matching(project, matcher(cfgCtx))) mcpp::platform::merge(s, o->fields); + if (P.active) keep_platform_naming(s, state.platformData, P.display(), conflicts); const auto envKey = target_env_name(t, "RUNNER"); if (auto e = mcpp::invocation::get(envKey); e && !e->empty()) { if (*e == "none") { @@ -738,7 +1033,9 @@ mp::Statement platform_statement(const PrepareState& state) { void step13_platform_statement(PrepareState& state) { auto& P = state.platform; - P.fields = platform_statement(state); + std::vector conflicts; + P.fields = platform_statement(state, &conflicts); + for (auto const& c : conflicts) mcpp::diag::warning("platform/artifact-name", c); P.flags = mcpp::platform::evaluate_flags(P.fields, matcher(platform_cfg_context(state))); } diff --git a/src/build/prepare/records.cpp b/src/build/prepare/records.cpp index 69958d1b4..413893ce1 100644 --- a/src/build/prepare/records.cpp +++ b/src/build/prepare/records.cpp @@ -101,6 +101,34 @@ static std::string lock_identity(const mcpp::lockfile::LockedPackage& p) { return p.namespace_ + "\x1f" + lock_short_name(p); } +// THE IDENTITY THIS CONFIGURATION'S REQUEST WAS GIVEN (SPEC-016 §4.5), into +// `lock`, and the other requests' as the lock had them. Returns what the lock +// had for this request. +static std::optional lock_targets(const PrepareState& state, + mcpp::lockfile::Lockfile& lock) { + std::optional priorTarget; + auto const& c = state.targetChoice; + if (auto prior = mcpp::lockfile::load(state.workRoot / "mcpp.lock")) + for (auto const& t : prior->targets) { + if (!c.request.empty() && t.request == c.request) priorTarget = t; + else lock.targets.push_back(t); + } + if (!c.request.empty()) lock.targets.push_back({ c.request, c.identity, c.provider }); + std::ranges::sort(lock.targets, {}, &mcpp::lockfile::LockedTarget::request); + return priorTarget; +} + +// The drift line of a request the lock gave another identity, if it did. +static std::optional target_drift(const PrepareState& state, + const std::optional& prior) { + auto const& c = state.targetChoice; + if (!prior || prior->identity == c.identity) return std::nullopt; + return std::format("target {}: {} ({}) -> {} ({}){}", c.request, prior->identity, + prior->provider.empty() ? "the platform's" : prior->provider, + c.identity, c.provider.empty() ? "the platform's" : c.provider, + c.why.empty() ? std::string{} : "; " + c.why); +} + std::expected step13_lockfile(PrepareState& state, BuildContext& ctx) { // Write/update mcpp.lock for any version-based deps that succeeded. // Path deps are intentionally NOT locked — their source is local filesystem. @@ -413,6 +441,7 @@ std::expected step13_lockfile(PrepareState& state, BuildConte } // One order whichever configuration wrote the file last: by identity. std::ranges::stable_sort(lock.packages, {}, [](auto const& p) { return lock_identity(p); }); + const auto priorTarget = lock_targets(state, lock); if (!lock.packages.empty() || !lock.indices.empty()) { auto lockPath = state.workRoot / "mcpp.lock"; // `--locked` ASSERTS THAT THIS RESOLUTION IS THE RECORDED ONE. @@ -446,6 +475,7 @@ std::expected step13_lockfile(PrepareState& state, BuildConte for (auto const& p : prior->packages) was[key(p)] = p.version; for (auto const& p : lock.packages) now[key(p)] = p.version; std::vector drift; + if (auto d = target_drift(state, priorTarget)) drift.push_back(std::move(*d)); for (auto const& [k, v] : now) { auto it = was.find(k); if (it == was.end()) drift.push_back(k + " " + v + " (not in the lock)"); diff --git a/src/build/prepare/retarget.cpp b/src/build/prepare/retarget.cpp index e1ea16711..2a721ebfc 100644 --- a/src/build/prepare/retarget.cpp +++ b/src/build/prepare/retarget.cpp @@ -24,6 +24,7 @@ import mcpp.toolchain.dialect; import mcpp.toolchain.fingerprint; import mcpp.toolchain.msvc; import mcpp.toolchain.registry; +import mcpp.toolchain.gcc; // the std module source of a paired libstdc++ import mcpp.toolchain.linkmodel; import mcpp.toolchain.lifecycle; import mcpp.toolchain.stdmod; @@ -291,6 +292,19 @@ step2_retarget_for_retargetable_driver(PrepareState& state) { } } } + // A ROW A PLATFORM DEFINES HAS THE PLATFORM'S SYSTEM (SPEC-016 §2): the + // host's C library the toolchain's payload carries is not its, so it + // has none but the sysroot the platform or the project states. + if (auto want = mcpp::toolchain::triple::parse(state.overrides.target_triple); + want && want->defined() && !want->is_freestanding()) { + auto stmt = base_statement(state, *want); + if (!stmt || !stmt->stated()) { + state.tc->base.payload.reset(); + state.tc->base.root.clear(); + state.tc->linkRuntimeDirs.clear(); + } + if (state.platformData.cHosted) state.tc->hostedC = *state.platformData.cHosted; + } if (auto want = mcpp::toolchain::triple::parse(state.overrides.target_triple); want && want->is_freestanding()) { @@ -313,6 +327,14 @@ step2_retarget_for_retargetable_driver(PrepareState& state) { // where the other one is. state.tc->clear_std_modules(); + // THE C ENVIRONMENT IS A DECLARED FACT (mcpp#911 G3): a sysroot + // that provides the hosted standard library, or the platform's + // provider stating `hosted = true`. + if (auto stmt = base_statement(state, *want); stmt) + state.tc->hostedC = std::ranges::find(stmt->provides, "hosted-standard-library") + != stmt->provides.end(); + if (state.platformData.cHosted) state.tc->hostedC = *state.platformData.cHosted; + // ── The target's C library, resolved like its compiler ───────── // // The row in kKnownTargets names it, exactly as it names the @@ -408,4 +430,50 @@ step2_retarget_for_retargetable_driver(PrepareState& state) { return {}; } +// THE SYSTEM A PAIRED PROVIDER SUPPLIES (SPEC-016 §6). The MinGW-w64 provider +// pairs with gcc and with llvm; with llvm, the payload its row names is +// installed as for gcc and becomes clang's sysroot: the C library, libstdc++ +// (whose `bits/std.cc` is the std module) and the GCC runtime. +std::expected step2_paired_system(PrepareState& state) { + state.tc->pairedSysroot.clear(); + if (state.pairedSystem.empty() || state.tc->compiler != mcpp::toolchain::CompilerId::Clang) return {}; + auto spec = mcpp::toolchain::parse_toolchain_spec(state.pairedSystem); + auto target = mcpp::toolchain::triple::parse(state.tc->targetTriple); + if (!spec || !target) return {}; + spec->target = *target; + const auto pkg = mcpp::toolchain::to_xim_package(*spec); + auto cfg = state.get_cfg(true); + if (!cfg) return std::unexpected(cfg.error()); + mcpp::fetcher::Fetcher fetcher(**cfg); + mcpp::fetcher::InstallProgressHandler progress; + auto payload = fetcher.resolve_xpkg_path(pkg.target(), mcpp::fetcher::PayloadIntent::Use, &progress); + if (!payload) + return std::unexpected(std::format( + "target '{}' with {} takes its system from the {} payload ({}), which is not available: {}", + state.tc->targetTriple, state.tc->label(), state.pairedSystem, pkg.target(), + payload.error().message)); + auto& tc = *state.tc; + tc.pairedSysroot = payload->root; + // The payload's own C++ driver names the target directory its libstdc++ + // is installed under (`//include/c++/`). + std::filesystem::path gxx; + std::error_code dirEc; + for (auto const& e : std::filesystem::directory_iterator(payload->root / "bin", dirEc)) { + const auto name = e.path().stem().string(); + if (name.ends_with("-g++") && (gxx.empty() || name < gxx.stem().string())) gxx = e.path(); + } + if (auto src = mcpp::toolchain::gcc::find_std_module_source(gxx, spec->version)) { + tc.stdlibId = "libstdc++"; + tc.stdlibVersion = spec->version; + tc.stdModuleSource = *src; + std::error_code ec; + const auto compat = src->parent_path() / "std.compat.cc"; + tc.stdCompatSource = std::filesystem::exists(compat, ec) ? compat : std::filesystem::path{}; + tc.hasImportStd = true; + } + mcpp::ui::info("Pairs", std::format("{} with the {} payload of {} ({})", tc.label(), state.pairedSystem, + state.tc->targetTriple, state.targetChoice.provider)); + return {}; +} + } // namespace mcpp::build diff --git a/src/build/prepare/scan.cpp b/src/build/prepare/scan.cpp index 19e950688..a85f9d581 100644 --- a/src/build/prepare/scan.cpp +++ b/src/build/prepare/scan.cpp @@ -589,7 +589,7 @@ step11_package_std_module_source(PrepareState& state) { if (auto fs = mcpp::toolchain::triple::parse(state.tc->targetTriple); fs && fs->is_freestanding()) { if (auto spec = mcpp::freestanding::resolve(*fs)) - flags += mcpp::freestanding::compile_prefix(*spec, true); + flags += mcpp::freestanding::compile_prefix(*spec, true, state.tc->hostedC); } else if (!state.tc->crossTargetFlag.empty()) { // `crossTargetFlag` and not `targetTriple`. The triple is mcpp's // vocabulary (`aarch64-macos`); the flag carries the spelling a diff --git a/src/build/prepare/state.cppm b/src/build/prepare/state.cppm index b795b50a5..9cf49d003 100644 --- a/src/build/prepare/state.cppm +++ b/src/build/prepare/state.cppm @@ -42,6 +42,7 @@ import mcpp.toolchain.stdmod; import mcpp.toolchain.post_install; import mcpp.toolchain.abi; import mcpp.toolchain.triple; +import mcpp.toolchain.target_select; import mcpp.toolchain.base; import mcpp.build.linkage_form; // #519 — which form each dependency takes import mcpp.build.plan; @@ -294,6 +295,26 @@ struct BuildProgramBinary { // Not copied: copying this by value would copy every dependency-graph and // plan structure prepare_build ever builds, silently, at whichever call // happened to pass it by value instead of by reference. +// WHICH ROW AND WHICH PROVIDER A REQUEST WAS GIVEN (SPEC-016 §4), as the report, +// the machine output, the lock and `mcpp why target` state it. +struct TargetChoice { + std::string request; // as written: `--target`, `[build] target`, the default + std::string identity; // the row taken + std::string provider; // its provider; empty when no provider serves it + int priority = 0; + bool fallback = false; // the first candidate was not available here + std::string why; // why the first candidate was not taken + std::vector candidates; +}; + +// The selection context of a configuration (prepare/target_choice.cpp): this host, +// MSVC's presence, a declared family, a provider chosen by name, and the +// priorities `[target..priority]` of the project and the machine set. +mcpp::toolchain::target::Context target_context(std::span project, + std::span config, + std::string family, std::string provider, + std::function msvcInstalled); + struct PrepareState { PrepareState(bool print_fingerprint_, bool includeDevDeps_, std::vector extraTargets_, @@ -333,7 +354,20 @@ struct PrepareState { std::string targetPinCandidate; bool targetPinIsCapability = false; std::string targetRowPin; + // A PROVIDER THAT PAIRS WITH THE DECLARED COMPILER SUPPLIES THE SYSTEM + // (SPEC-016 §6): the payload the row's own toolchain names (the MinGW-w64 + // payload of `x86_64-windows-gnu`) is then the declared clang's sysroot and + // C++ runtime. Empty when the compiler is the row's own. + std::string pairedSystem; std::string targetRowName; + TargetChoice targetChoice; + // The request names a row no definition knows yet, assumed while the platform + // chosen for it is fetched (mcpp#911 G1). + bool provisionalTarget = false; + // The row of the identity, with the facts of the provider that serves it + // (its toolchain, C library, linkage, platform) in place of the default + // provider's. + std::optional selectedTarget; bool tcSpecIsMsvc = false; std::optional root; std::optional effective; @@ -829,7 +863,8 @@ struct PrepareState { std::function, std::string>(const std::string&, const std::string&, const std::string&, const std::string&, - const std::vector&, const std::string&)> + const std::vector&, const std::string&, + const std::string&)> loadVersionDepVariant; std::map resolved; @@ -978,6 +1013,10 @@ void recordDependencyEdge(PrepareState& state, std::size_t consumerDepIndex, std::size_t dependencyPackageIndex, const mcpp::manifest::DependencySpec& spec, bool buildTable, const std::string& writtenKey); +// graph_edges.cpp: the package being built stands for itself in its tests' +// graph -- an edge reached through its `[dev-dependencies]` naming it is its +// own, recorded here; true when the request is so answered. +bool names_the_root(PrepareState& state, const WorkItem& item, const ResolvedKey& key); // graph_edges.cpp: defines `PrepareState::computeUsageRequirements`. void define_usage_requirements(PrepareState& state); std::expected phase4b_graph_worklist(PrepareState& state); @@ -1205,8 +1244,25 @@ std::expected step9_member_build_programs(PrepareState& state // The platform through the phases (prepare/platform.cpp). mcpp::build::cfgpred::Ctx platform_cfg_context(const PrepareState& state); std::expected step1_select_platform(PrepareState& state); +// The provider a configuration chose by name, from the explicit platform choices +// (SPEC-013 §3.1 steps 1-4): `builtin:`, or the provider a chosen platform +// package implements; "none" for `--platform none`; empty when nothing chose. +// Registers a platform package's rows and providers (SPEC-016 §2, §3). +void define_platform(const mcpp::manifest::platform::Statement& s, std::string_view shortName, + std::string_view ref); +// Before the request is parsed: the definitions of a platform chosen for it whose +// manifest is on this machine (a directory, or the second pass's root); and a +// provisional row for a request only a chosen platform can define. +void define_early_platform(PrepareState& state); +std::string named_provider(const PrepareState& state, const mcpp::toolchain::triple::Triple& t, + std::span config); + void step4b_seed_platform(PrepareState& state); std::expected step4b_apply_platform(PrepareState& state); +// The capabilities packages require and nothing provides: each takes the +// default its package states for the target (mcpp#911 G7). True when one was +// added to the walk. +std::expected step4b_capability_defaults(PrepareState& state); std::expected step4b_check_platform_versions(PrepareState& state); std::expected step4b_pin_to_platform_group(PrepareState& state, const WorkItem& item, const std::string& name, @@ -1218,13 +1274,18 @@ void step5_platform_toolchain(PrepareState& state); void step13_platform_statement(PrepareState& state); // The platform's statement and the compile words it states for a unit of C++, // as they stand now (the std module's are asked for before step 13). -mcpp::manifest::platform::Statement platform_statement(const PrepareState& state); +mcpp::manifest::platform::Statement platform_statement(const PrepareState& state, + std::vector* conflicts = nullptr); std::vector platform_unit_cxxflags(const PrepareState& state); std::expected step13_finish_platform(PrepareState& state, BuildContext& ctx); // retarget.cpp: a retargetable driver told its target, sysroot and multilib. std::expected step2_retarget_for_retargetable_driver(PrepareState& state); +// The system a paired provider supplies to the declared compiler (SPEC-016 §6). +std::expected step2_paired_system(PrepareState& state); // The platform root a first pass asks the second to read in P1, once. std::optional take_platform_restart(); +// The identity a first pass found the C library names (SPEC-016 §5), taken once. +std::optional take_target_restart(); std::expected load_host_platform(PrepareState& state); std::string host_platform_choice(const PrepareState& state); // The statement and bootstrap spec of a toolchain phase that asked prepare to diff --git a/src/build/prepare/target_choice.cpp b/src/build/prepare/target_choice.cpp new file mode 100644 index 000000000..96e1822f1 --- /dev/null +++ b/src/build/prepare/target_choice.cpp @@ -0,0 +1,50 @@ +// The selection context of a configuration (SPEC-016 §4): the host the +// candidates' availability is asked of, the probes, the declared family and the +// provider chosen by name, and the priorities the project and the machine set. + +module mcpp.build.prepare; +import :state; + +import std; +import mcpp.build.prepare_inputs; +import mcpp.manifest; +import mcpp.platform; +import mcpp.toolchain.platform; +import mcpp.toolchain.target_schema; +import mcpp.toolchain.target_select; + +namespace mcpp::build { + +namespace mp = mcpp::manifest::platform; + +mcpp::toolchain::target::Context target_context(std::span project, + std::span config, + std::string family, std::string provider, + std::function msvcInstalled) { + mcpp::toolchain::target::Context ctx; + ctx.host = mcpp::toolchain::target::host_pair(mcpp::toolchain::platform::name, + mcpp::toolchain::platform::host_arch); + ctx.probe = [msvc = std::move(msvcInstalled)](std::string_view p) { + return p == "msvc" && msvc && msvc(); + }; + ctx.family = std::move(family); + ctx.provider = std::move(provider); + // The project's tables over the machine's; within one, the most specific + // matching table that sets the provider's priority. + std::vector p(project.begin(), project.end()), c(config.begin(), config.end()); + ctx.priority = [p = std::move(p), c = std::move(c)](const mcpp::toolchain::target::Row& row, + std::string_view name) -> std::optional { + const auto cfg = cfgpred::context_for(row.triple); + auto matches = [&](std::string_view sel) { return cfgpred::matches(std::string(sel), cfg); }; + for (auto const* tables : { &p, &c }) { + std::optional found; + for (auto const* o : mcpp::platform::matching(*tables, matches)) + if (auto it = o->priority.find(std::string(name)); it != o->priority.end()) found = it->second; + if (found) return found; + } + return std::nullopt; + }; + return ctx; +} + +} // namespace mcpp::build diff --git a/src/build/prepare/target_side.cpp b/src/build/prepare/target_side.cpp index 995bcaef7..c6375763f 100644 --- a/src/build/prepare/target_side.cpp +++ b/src/build/prepare/target_side.cpp @@ -45,6 +45,7 @@ import mcpp.freestanding.linkline; // the ISA profile, for the std module comman import mcpp.toolchain.post_install; import mcpp.toolchain.abi; import mcpp.toolchain.triple; +import mcpp.toolchain.target_rows; import mcpp.build.linkage_form; // #519 — which form each dependency takes import mcpp.build.plan; import mcpp.build.flags; // compute_flags — the per-role contracts (#418) @@ -1040,6 +1041,46 @@ step9_kernel_abi_interface_enumeration(PrepareState& state) { return {}; } +namespace { +thread_local std::optional g_targetRestart; +} + +std::optional take_target_restart() { + auto r = std::move(g_targetRestart); + g_targetRestart.reset(); + return r; +} + +static std::expected cabi_identity(PrepareState& state) { + namespace tt = mcpp::toolchain::triple; + namespace tsd = mcpp::targetside; + auto const& ts = state.resolvedTargetSide; + if (ts.cAbi.absent() || !ts.cAbi.fromGraph() || ts.cAbi.interfaceName.empty()) return {}; + // A native build keeps its identity unless a platform supplies the C + // library (SPEC-016 §4.7): the graph's own C library in a native build is + // the project's arrangement, reported as such. + const bool requested = !state.overrides.target_triple.empty(); + if (!requested && !state.platform.active) return {}; + auto id = tt::parse(requested ? state.overrides.target_triple : tt::host_triple().str()); + if (!id || id->is_freestanding() || id->loader_supplies_os()) return {}; + if (tsd::c_abi_request_satisfied(id->env.empty() ? tsd::payload_libc_name(id->os, id->env) : id->env, + ts.cAbi.interfaceName)) + return {}; + const std::string env = ts.cAbi.interfaceName == "glibc" ? std::string("gnu") : ts.cAbi.interfaceName; + const auto row = std::format("{}-{}-{}", id->arch, id->os, env); + if (!mcpp::toolchain::target::find_row(row)) + return {}; + // A WRITTEN C LIBRARY IS THE PROJECT'S SPELLING AND STAYS: the mismatch is + // reported (the target report's `asks for the ... C ABI`), not refused -- + // refusing broke every configuration spelling the host's identity over a + // graph-supplied C library (e2e 282). Only a request that names none + // follows the C library. + if (!state.requestedCAbi.empty()) return {}; + if (state.overrides.target_request) return {}; // the second pass: the row it was given + g_targetRestart = row; + return std::unexpected(std::string("mcpp:target-restart")); +} + static std::expected step9_layering_and_requirement_checks(PrepareState& state, TargetSideGather& gather) { namespace tsd = mcpp::targetside; @@ -1093,6 +1134,12 @@ step9_layering_and_requirement_checks(PrepareState& state, TargetSideGather& gat // is the same with or without it. Refusing was tried and broke every // project spelling the host target `x86_64-linux-gnu` — which is what // `mcpp toolchain list` prints, and therefore what people write. + // THE IDENTITY FOLLOWS THE C LIBRARY (SPEC-016 §5, I5). A C library the + // graph or the platform supplies, under a system whose env names its C + // library, and a row that names it: a request that wrote no env is + // prepared again for that row; one that wrote another is refused, since + // an identity is not replaced and cannot name what is not linked. + if (auto conflict = cabi_identity(state); !conflict) return std::unexpected(conflict.error()); if (auto why = tsd::check_request(state.resolvedTargetSide)) mcpp::diag::warning("target", *why); @@ -1279,8 +1326,12 @@ step9_same_os_check_and_report(PrepareState& state) { // A program its loader runs is compiled for no operating system // by design (`-unknown-none-elf`): the loader is its environment. const bool okImage = want && got && want->loader_supplies_os() && got->is_freestanding(); + // A row a platform defines states the triple its compiler is given + // (SPEC-016 §2); that triple's system is the one to compare. + const bool stated = want && got && want->defined() + && [&] { auto c = mcpp::toolchain::triple::parse(want->llvm_triple()); return c && c->os == got->os; }(); if (want && got && !want->os.empty() && !got->os.empty() - && want->os != got->os && !okImage) { + && want->os != got->os && !okImage && !stated) { refusal::record(refusal::Code::OsMismatch); return std::unexpected(std::format( "target '{}' resolved to a toolchain for '{}'.\n" @@ -1312,8 +1363,14 @@ step9_same_os_check_and_report(PrepareState& state) { &state.resolvedTargetSide.cAbi, &state.resolvedTargetSide.cxx }) if (l->fromGraph() && injected(l->impl)) l->byPlatform = true; } + // The request as written and the identity it selected; the identity is + // the canonical name the rest of the report and the output directory use. + const auto& choice = state.targetChoice; + const bool selected = !choice.identity.empty(); mcpp::ui::info("Target", tsd::format_report( - state.resolvedTargetSide, reportedTargetName, mcpp::log::is_verbose())); + state.resolvedTargetSide, selected ? choice.identity : reportedTargetName, + mcpp::log::is_verbose(), selected ? choice.request : std::string_view{}, + choice.fallback ? std::string_view(choice.why) : std::string_view{})); return {}; } @@ -1722,6 +1779,11 @@ static void step9_define_graph_package_entry_closure(PrepareState& state) { // resolution.json only: the graph document a build program reads is // part of its re-run key, and must not change with when it is asked. if (!forBuildProgram) { + // The package's root as the build reads it (its patches applied): + // the tree a patch of it is written from (docs/05). + std::error_code ec; + entry["manifest_dir"] = std::filesystem::absolute(state.packages[i].root, ec) + .lexically_normal().generic_string(); static const std::vector none; // A member's program keeps its cache in the project (step 9). const bool member = state.m->package.virtualRoot && i != 0 diff --git a/src/build/prepare/toolchain.cpp b/src/build/prepare/toolchain.cpp index c8b7d17cd..e90995521 100644 --- a/src/build/prepare/toolchain.cpp +++ b/src/build/prepare/toolchain.cpp @@ -36,6 +36,9 @@ import mcpp.freestanding.linkline; // the ISA profile, for the std module comman import mcpp.toolchain.post_install; import mcpp.toolchain.abi; import mcpp.toolchain.triple; +import mcpp.toolchain.target_schema; +import mcpp.toolchain.target_select; +import mcpp.toolchain.target_rows; import mcpp.toolchain.base; import mcpp.toolchain.base_probe; import mcpp.home; @@ -518,6 +521,7 @@ struct TargetOverrideCtx { std::string requestedSpelling; std::optional parsed; mcpp::toolchain::triple::RequestResolution req; + mcpp::toolchain::target::Selection sel; // The `[target.]` section this request matched, or null. A // pointer rather than the map iterator the single-function version held, // since iterator validity is not this struct's business to reason about @@ -546,6 +550,12 @@ static void step1_apply_target_section(PrepareState& state, : state.overrides.target_triple, state.m->sourcePath.filename().string())); if (!e.toolchain.empty() && !state.tcFromCommandLine && !state.tcFromConsumer) { + // A CROSS TARGET'S SECTION NAMES THE TARGET'S COMPILER, NOT THE HOST'S: + // the spec it replaced is what the build program resolves, as a row's + // pin's is (`[target.x86_64-linux-musl] toolchain = "gcc@16.1.0"` on a + // Windows host whose `[toolchain] windows` is llvm). + if (!state.overrides.target_triple.empty() && state.tcSpec && *state.tcSpec != e.toolchain) + state.hostSpecBeforeRowPin = *state.tcSpec; state.tcSpec = e.toolchain; state.tcOrigin = TcOrigin::TargetSection; } @@ -566,6 +576,7 @@ step1_resolve_target_triple_request(PrepareState& state, TargetOverrideCtx& ctx) // registered but not yet supported", a string the reader never typed // and cannot find in their own command. ctx.requestedSpelling = state.overrides.target_triple; + define_early_platform(state); ctx.parsed = triple::parse(state.overrides.target_triple); // THE REQUEST IS COMPLETED FROM THE VOCABULARY BEFORE ANYTHING @@ -575,16 +586,69 @@ step1_resolve_target_triple_request(PrepareState& state, TargetOverrideCtx& ctx) // `parse` fills a missing env segment lexically so the identity stays // total — `x86_64-linux` IS `x86_64-linux-gnu`, and a unit test says so. // Every gate below then asked about the filled value instead of about - // the request. See `triple::resolve_request` for the two measurements. + // the request. The selection (mcpp.toolchain.target_select) now answers it. // // The lookup that follows keys on `parsed->str()`, so completing after // it would match sections against a triple this build is not going to // use. A project wanting the `planned` row keeps its escape hatch by // WRITING the segment: `--target aarch64-linux-gnu` skips completion // entirely, because a written segment is a request rather than a gap. + // THE SECTION THE REQUEST NAMES AS WRITTEN, before the selection: a + // `[target.] toolchain` is a declared family the selection keeps. + const mcpp::manifest::TargetEntry* written = nullptr; + if (auto w = state.m->targetOverrides.find(state.overrides.target_triple); + w != state.m->targetOverrides.end()) + written = &w->second; + + // THE SELECTION (SPEC-016 §4). The request is a pattern; the candidates are + // the rows it reaches with the providers that serve them, ordered by + // priority, and the first available on this host is the identity. + // Explicit choices win: a declared compiler family keeps the providers that + // pair with it, and a provider chosen by name (`builtin:`, or the + // platform package a provider names) serves alone. if (ctx.parsed) { - ctx.req = triple::resolve_request(*ctx.parsed); - ctx.parsed = ctx.req.triple; + std::string family; + auto declared = written && !written->toolchain.empty() ? std::optional(written->toolchain) + : (state.tcSpec && (state.tcFromCommandLine || tc_origin_is_user_explicit(state.tcOrigin))) + ? state.tcSpec : std::nullopt; + if (declared) + if (auto spec = mcpp::toolchain::parse_toolchain_spec(*declared)) + family = std::string(mcpp::toolchain::family_name(spec->family)); + std::vector config; + if (auto cfg = state.get_cfg(true); cfg) config = (*cfg)->platformOverrides; + const std::string provider = named_provider(state, *ctx.parsed, config); + auto sctx = target_context(state.m->platformOverrides, config, family, + provider == "none" ? std::string{} : provider, + state.msvc_usable_either_origin); + sctx.platformPackages = provider != "none"; + ctx.sel = mcpp::toolchain::target::select(state.overrides.target_triple, ctx.parsed->arch, + ctx.parsed->os, ctx.parsed->env, + ctx.parsed->envExplicit, sctx); + if (ctx.sel.row) { + if (auto chosen = triple::parse(ctx.sel.row->triple)) { + chosen->envExplicit = ctx.parsed->envExplicit; + ctx.parsed = *chosen; + } + } + // What the gates below ask of the request: the rows of its system, and + // of those the ones this mcpp builds. + ctx.req = triple::RequestResolution{}; + ctx.req.triple = *ctx.parsed; + for (auto const* r : mcpp::toolchain::target::rows_of( + ctx.parsed->arch, ctx.parsed->os, ctx.parsed->env, ctx.parsed->envExplicit)) { + ctx.req.siblings.push_back(r->triple); + if (r->tier != "planned") ctx.req.supported.push_back(r->triple); + } + ctx.req.completedFromVocabulary = ctx.sel.row && ctx.sel.row->triple != ctx.parsed->str(); + auto& choice = state.targetChoice; + choice = TargetChoice{}; + choice.request = state.overrides.target_request.value_or(state.overrides.target_triple); + choice.identity = ctx.parsed->str(); + choice.candidates = ctx.sel.candidates; + if (ctx.sel.provider) choice.provider = std::string(ctx.sel.provider->name); + choice.priority = ctx.sel.priority; + choice.fallback = ctx.sel.fallback; + if (ctx.sel.fallback && !ctx.sel.candidates.empty()) choice.why = ctx.sel.candidates.front().why; } // [target.X] lookup is spelling-independent: a section keyed @@ -604,6 +668,20 @@ step1_resolve_target_triple_request(PrepareState& state, TargetOverrideCtx& ctx) && !it->second.toolchain.empty(); ctx.known = ctx.parsed ? triple::find_known_target(*ctx.parsed) : nullptr; + // The row with the facts of the provider that serves it here, rather than + // of its default provider: the toolchain it pins, the C library it brings, + // the linkage and the platform package that implements it. + if (ctx.known && ctx.sel.provider && ctx.sel.provider != ctx.known->provider) { + auto view = *ctx.known; + auto const* p = ctx.sel.provider; + view.provider = p; + view.pin = p->toolchain; + view.sysroot = p->sysroot == "toolchain" ? std::string_view{} : p->sysroot; + view.defaultStatic = p->linkage == "static"; + view.platform = p->platform; + state.selectedTarget = view; + ctx.known = &*state.selectedTarget; + } return {}; } @@ -634,7 +712,9 @@ step1_validate_target_tier(PrepareState& state, TargetOverrideCtx& ctx) { " Name one of them.", ctx.requestedSpelling, opts)); } - if (!ctx.known && !ctx.hasExplicitSection) { + // A row a platform defines is known to this build by its definition + // (SPEC-016 §2), whether or not the project writes a section for it. + if (!ctx.known && !ctx.hasExplicitSection && !(ctx.parsed && ctx.parsed->defined())) { // "UNKNOWN" IS A CLAIM ABOUT THE VOCABULARY, AND IT WAS FALSE FOR // A WHOLE arch+os FAMILY. // @@ -895,31 +975,44 @@ step1_host_can_serve_check(PrepareState& state, TargetOverrideCtx& ctx) { // The escape hatch stays open on purpose: an explicit `[target.X]` // toolchain override means the author is supplying the cross toolchain // themselves, and mcpp's payload matrix has no standing to refuse it. + // THE SELECTION SAYS WHETHER ITS ANSWER IS AVAILABLE HERE (SPEC-016 §4.6): + // the candidate it took is the first available one, or, when none is, the + // first of all, which this diagnosis refuses unless the graph supplies the + // target's system after all (held until the graph is known). + const auto chosen = std::ranges::find_if(ctx.sel.candidates, [&](auto const& c) { + return c.row == ctx.sel.row && c.provider == ctx.sel.provider; + }); + const bool available = chosen != ctx.sel.candidates.end() ? chosen->available + : (ctx.parsed && mcpp::toolchain::host_can_serve(*ctx.parsed)); if (ctx.known && ctx.known->tier != "planned" && !ctx.hasToolchainOverride - && ctx.parsed - && !mcpp::toolchain::host_can_serve(*ctx.parsed)) { + && ctx.parsed && !available) { + std::string candidates; + for (auto const& c : ctx.sel.candidates) + candidates += std::format("\n {:<22} {:<12} priority {:<4} {}", c.row->triple, + c.provider ? c.provider->name : std::string_view("-"), c.priority, + c.available ? std::string("available") : c.why); std::string servable; - for (auto const& info : triple::known_targets()) { - auto t = triple::parse(info.canonical); - if (!t || info.tier == "planned") continue; - if (!mcpp::toolchain::host_can_serve(*t)) continue; + for (auto const* r : mcpp::toolchain::target::rows()) { + if (r->tier == "planned") continue; + auto one = mcpp::toolchain::target::select(r->triple, r->arch, r->os, r->env, true, + target_context(state.m->platformOverrides, {}, {}, {}, state.msvc_usable_either_origin)); + if (one.candidates.empty() || one.fallback || !one.candidates.front().available) continue; if (!servable.empty()) servable += ", "; - servable += t->str(); + servable += r->triple; } state.unservedTargetDiagnosis = std::format( - "target '{}' cannot be built on this host.\n" - " No toolchain payload here produces it, and nothing in " - "the dependency graph\n" - " supplies its system side.\n" - " this host can build with the payload alone: {}\n" - " To build it anyway, depend on a package that implements " - "the target's system\n" - " (its kernel interface and C library), or supply your own " - "cross toolchain with\n" - " an explicit [target.{}] toolchain = \"…\" section.", - ctx.parsed->str(), - servable.empty() ? "(nothing — `mcpp toolchain list`)" : servable, - ctx.parsed->str()); + "target '{}' cannot be built on this host: no provider of it is available here, and " + "nothing in the dependency graph supplies its system side.\n" + " candidates:{}\n" + " this host builds: {}\n" + " To build it anyway: depend on a package that implements the target's system " + "(its kernel interface and C library),\n" + " choose a platform that serves it (`--platform `), or supply your own " + "cross toolchain with [target.{}] toolchain = \"…\".\n" + " `mcpp why target {}` lists the candidates and why each is or is not available.", + ctx.parsed->str(), candidates.empty() ? std::string(" (none)") : candidates, + servable.empty() ? "(nothing — `mcpp toolchain list`)" : servable, ctx.parsed->str(), + ctx.requestedSpelling); } } @@ -1244,9 +1337,16 @@ static std::expected step1_device_axis_and_layer_merge(Prepar // The descriptor blocks the target reads (design 2026-10-29 §5.1): the // row's own, or those the platform states when it was read early (a // platform fetched in the first pass restarts the prepare so that it is). - state.targetPlatform = state.platformEarly && state.platformData.descriptorKeys.set - ? mcpp::toolchain::platform::TargetPlatform::with_keys(state.platformData.descriptorKeys.value) - : mcpp::toolchain::platform::TargetPlatform::for_os(state.cfgCtx().os); + state.targetPlatform = [&] { + if (state.platformEarly && state.platformData.descriptorKeys.set) + return mcpp::toolchain::platform::TargetPlatform::with_keys(state.platformData.descriptorKeys.value); + // A row a platform defines names its own blocks (SPEC-016 §2). + if (auto t = mcpp::toolchain::triple::parse(state.overrides.target_triple)) + if (auto* d = t->defined(); d && !d->descriptorKeys.empty()) + return mcpp::toolchain::platform::TargetPlatform::with_keys( + std::vector(d->descriptorKeys.begin(), d->descriptorKeys.end())); + return mcpp::toolchain::platform::TargetPlatform::for_os(state.cfgCtx().os); + }(); // ── L1: merge conditional [target.'cfg(...)'] sections ─────────────────── // Evaluated now (target resolved) against the resolved target — the @@ -2200,6 +2300,7 @@ std::expected phase2_define_toolchain_resolver(PrepareState& if (auto r = step2_detect_toolchain(state); !r) return std::unexpected(r.error()); if (auto r = step2_apply_local_toolchain(state); !r) return std::unexpected(r.error()); if (auto r = step2_retarget_for_retargetable_driver(state); !r) return std::unexpected(r.error()); + if (auto r = step2_paired_system(state); !r) return std::unexpected(r.error()); if (auto r = step2_bind_msvc_toolset(state); !r) return std::unexpected(r.error()); if (auto r = step2_resolve_base(state); !r) return std::unexpected(r.error()); if (auto r = step2_resolve_run_under(state); !r) return std::unexpected(r.error()); diff --git a/src/build/prepare/toolchain_decision.cpp b/src/build/prepare/toolchain_decision.cpp index 9d399c73e..ace2e45eb 100644 --- a/src/build/prepare/toolchain_decision.cpp +++ b/src/build/prepare/toolchain_decision.cpp @@ -20,6 +20,8 @@ import mcpp.toolchain.fingerprint; import mcpp.toolchain.registry; import mcpp.toolchain.linkmodel; import mcpp.toolchain.triple; +import mcpp.toolchain.target_rows; +import mcpp.toolchain.target_schema; // For `resolve_version_match` / `list_installed_versions`: a bare compiler // family named by the dependency graph resolves to a concrete version through // exactly the path `mcpp toolchain default ` uses. @@ -66,6 +68,97 @@ bool graph_supplies_system(const PrepareState& state) { } // namespace +// OVERRIDING THE CONVENTION IS ALLOWED; OVERRIDING IT AND SUPPLYING +// NOTHING IN ITS PLACE IS NOT, AND UNTIL THIS BLOCK IT LOOKED THE SAME. +// +// A hosted row's pin names the payload that supplies the target's C +// library. A project may name a different compiler — that is the escape +// hatch the whole convention/capability distinction exists to protect — +// and the ordinary reason to do so is that its dependency graph supplies +// the C library instead. `examples/06-openkal-cross` is exactly that: +// `llvm@22.1.8` plus `openkal-llvm-runtime`, and `graphSuppliesSystem` +// is true there. +// +// WITH NEITHER, THE BUILD USED TO RUN ANYWAY AND FAIL SOMEWHERE ELSE. +// Measured 2026-08-26 on Linux, `[toolchain] default = "llvm@22.1.8"` +// and no dependencies: +// +// --target x86_64-linux-musl +// hermetic link check failed … crtbeginT.o (bare name) +// --target x86_64-windows-gnu +// hermetic link check failed … +// /usr/lib/gcc/x86_64-w64-mingw32/13-win32/crtbegin.o (outside) +// +// Both are accurate about the symptom and silent about the decision: +// clang is retargetable and brings no C library, so it reached for a +// gcc installation — one that does not exist under the payload prefix +// in the first case, and that belongs to the HOST in the second. There +// is no llvm payload supplying either target's C library today. +// +// THE REFUSAL IS DECIDED HERE BECAUSE ONLY HERE ARE BOTH HALVES +// KNOWN. The row is read a thousand lines earlier and the graph does +// not exist then; `host_can_serve` is family-agnostic and would answer +// "yes, some payload here produces it" — the same shape as the family +// this release is about, a predicate answering a question narrower than +// the one it is asked. +static std::expected decide_convention(PrepareState& state, bool graphSuppliesSystem) { + if (!state.targetRowPin.empty() && !graphSuppliesSystem + && tc_origin_is_user_explicit(state.tcOrigin) && state.tcSpec.has_value()) { + auto declared = mcpp::toolchain::parse_toolchain_spec(*state.tcSpec); + auto rowTc = mcpp::toolchain::parse_toolchain_spec(state.targetRowPin); + // A provider that pairs with the declared family supplies the system + // with it (SPEC-016 §6): the row's payload becomes that compiler's. + const bool paired = declared && rowTc && declared->family != rowTc->family + && std::ranges::any_of(mcpp::toolchain::target::providers(), [&](auto const* p) { + return p->name == state.targetChoice.provider + && mcpp::toolchain::target::serves(*p, state.targetRowName) + && std::ranges::find(p->pairs, mcpp::toolchain::family_name(declared->family)) + != p->pairs.end(); + }); + // A PAIRED PROVIDER THIS HOST DOES NOT HAVE SUPPLIES NOTHING. The + // selection took it as the first candidate of all, none being + // available (§4.6); with the toolchain stated, the diagnosis of an + // unserved target was not composed, and the payload's install + // failed as an unnamed refusal (target matrix, linux-aarch64). + auto const& choice = state.targetChoice; + const bool pairedHere = std::ranges::any_of(choice.candidates, [&](auto const& k) { + return k.available && k.provider && k.provider->name == choice.provider + && k.row && k.row->triple == choice.identity; + }); + if (paired && !pairedHere) { + refusal::record(refusal::Code::HostCannotServe); + return std::unexpected(std::format( + "target '{}' with {} takes its system from the '{}' provider's {} payload, " + "which this host does not have.\n" + " `mcpp why target --target {}` lists the providers of the target and " + "where each is available; depend on a package that implements the " + "target's system to build it here.", + state.targetRowName, *state.tcSpec, choice.provider, state.targetRowPin, + state.targetRowName)); + } + if (paired) state.pairedSystem = state.targetRowPin; + else if (declared && rowTc && declared->family != rowTc->family) { + refusal::record(refusal::Code::ConventionUnreplaced); + return std::unexpected(std::format( + "target '{}' takes its C library from the '{}' payload, and " + "'{}' has none here.\n" + " The row's toolchain is a convention, so naming your " + "own compiler overrides it —\n" + " but the convention is what supplies this target's " + "headers and C library, and\n" + " nothing in the dependency graph supplies them " + "instead.\n" + " depend on a package that implements the target's C " + "library (openkal-musl and\n" + " openkal-llvm-runtime are the ones in the index), or " + "remove the `[toolchain]`\n" + " line so `{}` is used for this target.", + state.targetRowName, state.targetRowPin, *state.tcSpec, state.targetRowPin)); + } + } + return {}; +} + std::expected phase5_toolchain_after_graph(PrepareState& state) { // ─── The toolchain, resolved now that the graph exists ────────────────── @@ -390,62 +483,7 @@ std::expected phase5_toolchain_after_graph(PrepareState& stat } } } - // OVERRIDING THE CONVENTION IS ALLOWED; OVERRIDING IT AND SUPPLYING - // NOTHING IN ITS PLACE IS NOT, AND UNTIL THIS BLOCK IT LOOKED THE SAME. - // - // A hosted row's pin names the payload that supplies the target's C - // library. A project may name a different compiler — that is the escape - // hatch the whole convention/capability distinction exists to protect — - // and the ordinary reason to do so is that its dependency graph supplies - // the C library instead. `examples/06-openkal-cross` is exactly that: - // `llvm@22.1.8` plus `openkal-llvm-runtime`, and `graphSuppliesSystem` - // is true there. - // - // WITH NEITHER, THE BUILD USED TO RUN ANYWAY AND FAIL SOMEWHERE ELSE. - // Measured 2026-08-26 on Linux, `[toolchain] default = "llvm@22.1.8"` - // and no dependencies: - // - // --target x86_64-linux-musl - // hermetic link check failed … crtbeginT.o (bare name) - // --target x86_64-windows-gnu - // hermetic link check failed … - // /usr/lib/gcc/x86_64-w64-mingw32/13-win32/crtbegin.o (outside) - // - // Both are accurate about the symptom and silent about the decision: - // clang is retargetable and brings no C library, so it reached for a - // gcc installation — one that does not exist under the payload prefix - // in the first case, and that belongs to the HOST in the second. There - // is no llvm payload supplying either target's C library today. - // - // THE REFUSAL IS DECIDED HERE BECAUSE ONLY HERE ARE BOTH HALVES - // KNOWN. The row is read a thousand lines earlier and the graph does - // not exist then; `host_can_serve` is family-agnostic and would answer - // "yes, some payload here produces it" — the same shape as the family - // this release is about, a predicate answering a question narrower than - // the one it is asked. - if (!state.targetRowPin.empty() && !graphSuppliesSystem - && tc_origin_is_user_explicit(state.tcOrigin) && state.tcSpec.has_value()) { - auto declared = mcpp::toolchain::parse_toolchain_spec(*state.tcSpec); - auto rowTc = mcpp::toolchain::parse_toolchain_spec(state.targetRowPin); - if (declared && rowTc && declared->family != rowTc->family) { - refusal::record(refusal::Code::ConventionUnreplaced); - return std::unexpected(std::format( - "target '{}' takes its C library from the '{}' payload, and " - "'{}' has none here.\n" - " The row's toolchain is a convention, so naming your " - "own compiler overrides it —\n" - " but the convention is what supplies this target's " - "headers and C library, and\n" - " nothing in the dependency graph supplies them " - "instead.\n" - " depend on a package that implements the target's C " - "library (openkal-musl and\n" - " openkal-llvm-runtime are the ones in the index), or " - "remove the `[toolchain]`\n" - " line so `{}` is used for this target.", - state.targetRowName, state.targetRowPin, *state.tcSpec, state.targetRowPin)); - } - } + if (auto r = decide_convention(state, graphSuppliesSystem); !r) return r; if (auto r = state.resolve_target_toolchain(); !r) return std::unexpected(r.error()); } diff --git a/src/build/prepare/xlings.cpp b/src/build/prepare/xlings.cpp index 27e924c2a..5a58aeef3 100644 --- a/src/build/prepare/xlings.cpp +++ b/src/build/prepare/xlings.cpp @@ -121,10 +121,16 @@ step3_define_host_tc_closures_and_refresh_index(PrepareState& state) { if (!state.bootstrapSpec.empty()) return state.bootstrapSpec; if (!state.tcSpec) return {}; if (state.overrides.target_triple.empty()) return *state.tcSpec; - return (state.tcOrigin == TcOrigin::TargetPin && state.hostSpecBeforeRowPin.has_value() - && !state.hostSpecBeforeRowPin->empty() && *state.hostSpecBeforeRowPin != "system") - ? *state.hostSpecBeforeRowPin - : (state.tcOrigin == TcOrigin::TargetPin ? state.native_first_run_spec() : *state.tcSpec); + // The target's own choice -- a row's pin or a `[target.] toolchain` + // -- is not the host's: the spec it replaced is. A pin that replaced + // nothing gives the native default (#622); a section that replaced + // nothing names the only compiler the project states. + const bool targetChose = state.tcOrigin == TcOrigin::TargetPin + || state.tcOrigin == TcOrigin::TargetSection; + if (targetChose && state.hostSpecBeforeRowPin.has_value() && !state.hostSpecBeforeRowPin->empty() + && *state.hostSpecBeforeRowPin != "system") + return *state.hostSpecBeforeRowPin; + return state.tcOrigin == TcOrigin::TargetPin ? state.native_first_run_spec() : *state.tcSpec; }; state.host_tc_for_build_program = [&]() -> std::expected< std::pair, std::string> { diff --git a/src/build/result.cppm b/src/build/result.cppm index 279ebc32b..d96faaef7 100644 --- a/src/build/result.cppm +++ b/src/build/result.cppm @@ -83,6 +83,10 @@ struct Record { // host graph's, as `mcpp::platform::to_json` writes them; "null" for none. std::string platform = "null"; std::string hostPlatform = "null"; + // The selection the configuration's identity came from (SPEC-016 §4.5): + // `{request, identity, provider, priority, fallback}`; "null" for the + // host's own build. + std::string target = "null"; std::filesystem::path outputDir; std::filesystem::path projectRoot; std::filesystem::path compileDatabase; @@ -203,6 +207,7 @@ nlohmann::json to_json(const Record& r) { {"outputDir", r.outputDir.generic_string()}, {"platform", nlohmann::json::parse(r.platform, nullptr, false)}, {"hostPlatform", nlohmann::json::parse(r.hostPlatform, nullptr, false)}, + {"target", nlohmann::json::parse(r.target, nullptr, false)}, }}, {"compileDatabase", r.compileDatabase.empty() ? nlohmann::json(nullptr) : nlohmann::json(r.compileDatabase.generic_string())}, @@ -231,6 +236,7 @@ std::optional read(const std::filesystem::path& outputDir) { r.outputDir = c.value("outputDir", ""); if (c.contains("platform")) r.platform = c["platform"].dump(); if (c.contains("hostPlatform")) r.hostPlatform = c["hostPlatform"].dump(); + if (c.contains("target")) r.target = c["target"].dump(); } if (d.contains("compileDatabase") && d["compileDatabase"].is_string()) r.compileDatabase = d["compileDatabase"].get(); diff --git a/src/build/runtime_validation.cppm b/src/build/runtime_validation.cppm index 9e8cbcbd7..82b8cd6d6 100644 --- a/src/build/runtime_validation.cppm +++ b/src/build/runtime_validation.cppm @@ -684,6 +684,19 @@ ValidationReport validate_changed_artifacts( record_without_rules(plan, before); return report; } + // THE ARTIFACT'S OWN SYSTEM DECIDES (mcpp#911 G5): a row a platform defines + // states where its artifacts run, and an artifact for another system is + // judged by that system's loader, not this machine's. Its closure is not + // checked here, and the build says so rather than calling it a defect. + if (auto tt = mcpp::toolchain::triple::parse(plan.toolchain.targetTriple); tt) + if (auto const* d = tt->defined(); d && d->execution != "linux") { + if (!before.empty()) + mcpp::ui::info("note", std::format( + "the artifacts are for {}, not this machine: their runtime closure is not " + "checked here", d->execution.empty() ? tt->os : std::string(d->execution))); + record_without_rules(plan, before); + return report; + } auto doc = read_cache(plan.outputDir); bool changedCache = false; diff --git a/src/build/test_protocol.cppm b/src/build/test_protocol.cppm index 74ce73559..f936bebec 100644 --- a/src/build/test_protocol.cppm +++ b/src/build/test_protocol.cppm @@ -18,7 +18,7 @@ inline constexpr int kVersion = 1; // The framework mcpp scaffolds tests with and suggests where a package has // none (docs/01, docs/08): mtest, by this release, from its repository. -inline constexpr std::string_view kMtestRelease = "0.4.0"; +inline constexpr std::string_view kMtestRelease = "0.5.2"; inline constexpr std::string_view kMtestRepo = "https://github.com/mcpp-community/mtest"; // The protocol's variables, removed from the environment a program inherits @@ -30,7 +30,9 @@ inline constexpr std::array kVariables = { // The version the marker in a program's file states (P2.3): the bytes // `mtest-protocol=` and a version followed by a byte that is not a digit; 0 -// when the file carries none or cannot be read. +// when the file carries none or cannot be read. A file may carry the bytes as +// data too (a test of a runner holds `mtest-protocol=12` among its cases), so +// the version this runner speaks is taken wherever it occurs, else the first. int marker_version(const std::filesystem::path& program); // What a program's case is, as its listing states it (P3.3). @@ -124,6 +126,7 @@ int marker_version(const std::filesystem::path& program) { constexpr std::size_t block = 1 << 20; std::string window; std::vector buffer(block); + int first = 0; while (in) { in.read(buffer.data(), static_cast(buffer.size())); const auto got = static_cast(in.gcount()); @@ -138,12 +141,13 @@ int marker_version(const std::filesystem::path& program) { if (end == at + marker.size()) continue; int version = 0; std::from_chars(window.data() + at + marker.size(), window.data() + end, version); - if (version > 0) return version; + if (version == kVersion) return version; + if (version > 0 && first == 0) first = version; } const std::size_t keep = marker.size() + 8; if (window.size() > keep) window.erase(0, window.size() - keep); } - return 0; + return first; } bool Report::failed() const { diff --git a/src/build/test_targets.cppm b/src/build/test_targets.cppm index 732959ed1..d9c428f02 100644 --- a/src/build/test_targets.cppm +++ b/src/build/test_targets.cppm @@ -192,6 +192,17 @@ discover_test_targets(const std::filesystem::path& manifestRoot, } result.targets.push_back(std::move(target)); } + // A TEST'S NAME IS A PATH OF ITS PROGRAM (`bin/`): one name that is + // another's directory would make one program a directory of the other's. + // The names are sorted: the first name below `/` is the one to ask. + for (auto it = seenNames.begin(); it != seenNames.end(); ++it) { + auto next = seenNames.lower_bound(it->first + "/"); + if (next == seenNames.end() || !next->first.starts_with(it->first + "/")) continue; + return std::unexpected(std::format( + "test '{}' ({}) is a directory of test '{}' ({}): a test's name is the path of its " + "program, and one cannot be both; move one of them", + it->first, it->second.generic_string(), next->first, next->second.generic_string())); + } return result; } diff --git a/src/cli.cppm b/src/cli.cppm index 7a025a8c9..9d3fcc36f 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -680,7 +680,7 @@ int run(int argc, char** argv) { // (cmd_new validates presence for project creation itself). .arg(cl::Arg("name").help("Package directory name")) .option(cl::Option("template").short_name('t').takes_value().value_name("SPEC") - .help("bin (default) | lib | [ns.]pkg[@ver][:template] — exact package template")) + .help("bin (default) | lib | platform | [ns.]pkg[@ver][:template] — exact package template")) .option(cl::Option("list-templates").takes_value().value_name("PKG") .help("List templates from exact [ns.]pkg[@ver]")) .action(wrap_rc(cmd_new))) @@ -936,9 +936,9 @@ int run(int argc, char** argv) { .help("Keep unrecorded directories written more recently than this, e.g. 12h, 3d (default [build] keep_days, 3d; 0 keeps none; implies --stale)")) .action(wrap_rc(cmd_clean))) .subcommand(cl::App("why") - .description("Explain how the toolchain / runtime / deps / runners / sources were resolved") - .arg(cl::Arg("topic").help("toolchain | sysroot | runtime | deps | runners | sources | tool | payload | build-programs | compat (default: all)")) - .arg(cl::Arg("subject").help("for `tool` and `payload`: the name to look up")) + .description("Explain how the target / toolchain / runtime / deps / runners / sources / flags were resolved") + .arg(cl::Arg("topic").help("target | toolchain | sysroot | runtime | deps | runners | sources | tool | payload | build-programs | compat | flags | platform (default: all but flags and platform)")) + .arg(cl::Arg("subject").help("for `tool` and `payload`: the name to look up; for `flags`: the unit, by the end of its path")) // `--target` / `--toolchain` make this a QUERY rather than a // report on the current directory's default: "what would a build // for THIS pair resolve to" is the question the target matrix asks @@ -1070,6 +1070,8 @@ int run(int argc, char** argv) { .help("Output format: human (default) | json (one mcpp.pack envelope on stdout; narration on stderr)")) .option(cl::Option("no-strip") .help("Ship the artifacts as built (default: strip debug info)")) + .option(cl::Option("all-programs") + .help("Ship every program of the package: the one named after it (or the named one) and the others beside it")) .option(cl::Option("package").short_name('p').takes_value().multiple().value_name("NAME") .help("Pack the named workspace member (namespace.name or package name, then directory), as if run in its directory; repeat to pack several, planned and built once")) .option(cl::Option("workspace") diff --git a/src/cli/cmd_new.cppm b/src/cli/cmd_new.cppm index 0dab4e535..ff1564219 100644 --- a/src/cli/cmd_new.cppm +++ b/src/cli/cmd_new.cppm @@ -62,6 +62,7 @@ export int cmd_new(const mcpplibs::cmdline::ParsedArgs& parsed) { "--template gui is deprecated; use `--template imgui` " "(the template then ships with — and version-tracks — the library)"); } + if (tmpl == "platform") return mcpp::scaffold::create_platform_project(*project); if (tmpl != "bin" && tmpl != "gui" && tmpl != "lib") { auto spec = mcpp::scaffold::parse_template_spec(tmpl); if (!spec) { diff --git a/src/cli/cmd_publish.cppm b/src/cli/cmd_publish.cppm index 4fe754239..28e378f73 100644 --- a/src/cli/cmd_publish.cppm +++ b/src/cli/cmd_publish.cppm @@ -874,6 +874,7 @@ int cmd_pack_body(const mcpplibs::cmdline::ParsedArgs& parsed, parsed.value("profile").value_or(""), parsed.is_flag_set("release"), parsed.is_flag_set("dev")); if (parsed.is_flag_set("no-strip")) opts.strip = false; + if (parsed.is_flag_set("all-programs")) opts.allPrograms = true; if (auto v = parsed.value("debug-symbols")) opts.debugSymbols = *v; if (auto v = parsed.value("features")) opts.features = *v; diff --git a/src/cli/cmd_self.cppm b/src/cli/cmd_self.cppm index 0d564944a..cbc8e4120 100644 --- a/src/cli/cmd_self.cppm +++ b/src/cli/cmd_self.cppm @@ -127,11 +127,15 @@ export int cmd_why(const mcpplibs::cmdline::ParsedArgs& parsed) { if (topic == "build-programs" || topic == "compat") return mcpp::doctor::why_build_programs_or_compat_json( topic, parsed.option_or_empty("features").value()); + if (topic == "target" || topic == "flags" || topic == "platform") + return mcpp::doctor::why_target_or_flags_json( + topic, parsed.option_or_empty("target").value(), parsed.positional(1), + parsed.option_or_empty("features").value()); if (!topic.empty() && topic != "toolchain") { std::println(libc::stderr(), "error: --format json is defined for `mcpp why toolchain`, " - "`mcpp why sources|tool|payload`, `mcpp why build-programs` and " - "`mcpp why compat`; '{}' has no machine-readable " + "`mcpp why target`, `mcpp why flags`, `mcpp why sources|tool|payload`, " + "`mcpp why build-programs` and `mcpp why compat`; '{}' has no machine-readable " "shape yet", topic); return 2; } @@ -141,7 +145,8 @@ export int cmd_why(const mcpplibs::cmdline::ParsedArgs& parsed) { } return mcpp::doctor::why_report(parsed.positional(0), parsed.option_or_empty("features").value(), - parsed.positional(1)); + parsed.positional(1), + parsed.option_or_empty("target").value()); } // Also called directly by the dispatcher for the legacy `--explain CODE` form. diff --git a/src/doctor.cppm b/src/doctor.cppm index 17b1d5faf..27b6ed4eb 100644 --- a/src/doctor.cppm +++ b/src/doctor.cppm @@ -16,6 +16,7 @@ import mcpp.build.refusal; import mcpp.targetside; import mcpp.wire; import mcpp.build.plan; +import mcpp.build.flag_origins; import mcpp.build.runtime_validation; import mcpp.config; import mcpp.fallback.probe_sysroot; @@ -1173,6 +1174,8 @@ int print_stored_runtime_resolution() { // // It builds nothing. `prepare_build` stops before any compile, which is also // why the target matrix can afford to ask it for every cell. +nlohmann::json target_selection_json(const mcpp::build::BuildContext& ctx); + export int why_toolchain_json(std::string_view target, std::string_view tcSpec) { mcpp::build::BuildOverrides ov; ov.target_triple = std::string(target); @@ -1281,6 +1284,7 @@ export int why_toolchain_json(std::string_view target, std::string_view tcSpec) {"replaced", ctx->compilerChoice.replaced}, }}, }; + data["target"] = target_selection_json(*ctx); data["triple"] = { {"requested", std::string(target)}, {"toolchain", tc.targetTriple}, @@ -1508,6 +1512,83 @@ nlohmann::json compat_uses_json() { return uses; } +// The selection of a request (SPEC-016 §4) as the machine output states it. +nlohmann::json target_selection_json(const mcpp::build::BuildContext& ctx) { + auto const& s = ctx.targetSelection; + auto candidates = nlohmann::json::array(); + for (auto const& c : s.candidates) + candidates.push_back({ {"identity", c.identity}, {"provider", c.provider}, + {"priority", c.priority}, {"available", c.available}, {"why", c.why} }); + return { {"request", s.request}, {"identity", s.identity}, {"provider", s.provider}, + {"priority", s.priority}, {"fallback", s.fallback}, {"why", s.why}, + {"candidates", candidates} }; +} + +// THE PLATFORM AT HAND (SPEC-016 §5): the root package's own statement when it +// is a platform package, else the platform the build uses for its target. +struct PlatformAtHand { + std::string name, version; + const mcpp::manifest::platform::Statement* statement = nullptr; +}; + +PlatformAtHand platform_at_hand(const mcpp::build::BuildContext& ctx) { + if (ctx.manifest.platformDeclared) + return { ctx.manifest.package.name, ctx.manifest.package.version, &ctx.manifest.platform }; + if (ctx.plan.platform.active) { + auto const& p = ctx.plan.platform; + auto dot = p.package.rfind('.'); + return { dot == std::string::npos ? p.package : p.package.substr(dot + 1), p.version, &p.fields }; + } + return {}; +} + +nlohmann::json platform_json(const mcpp::build::BuildContext& ctx) { + namespace mp = mcpp::manifest::platform; + const auto at = platform_at_hand(ctx); + if (!at.statement) return nullptr; + const auto name = mp::platform_name(at.name); + auto rows = nlohmann::json::array(); + for (auto const& d : at.statement->defines) rows.push_back(d.triple); + auto served = nlohmann::json::array(); + for (auto const& s : mp::served_identities(*at.statement, name)) + served.push_back({ {"identity", s.identity}, {"provider", s.provider}, {"priority", s.priority} }); + const auto f = mp::check_statement(*at.statement, name); + return { {"package", at.name}, {"version", at.version}, {"defines", rows}, {"serves", served}, + {"warnings", f.warnings} }; +} + +// `mcpp why target|flags|platform --format json`. +export int why_target_or_flags_json(std::string_view topic, std::string_view target, + std::string_view unit, const std::string& features) { + const bool wasQuiet = mcpp::ui::is_quiet(); + mcpp::ui::set_quiet(true); + struct QuietGuard { + bool prev; + ~QuietGuard() { mcpp::ui::set_quiet(prev); } + } quietGuard{wasQuiet}; + mcpp::build::BuildOverrides ov; + ov.features = features; + ov.target_triple = std::string(target); + auto ctx = mcpp::build::prepare_build(/*print_fingerprint=*/false, + /*includeDevDeps=*/false, {}, ov); + nlohmann::json data; + std::vector diags; + data["status"] = ctx ? "ok" : "refused"; + if (!ctx) + diags.push_back({.code = "prepare.failed", .severity = mcpp::wire::Severity::Error, + .message = ctx.error()}); + else if (topic == "target") data["target"] = target_selection_json(*ctx); + else if (topic == "platform") data["platform"] = platform_json(*ctx); + else data["flags"] = mcpp::build::flag_origins_json(mcpp::build::flag_origins(ctx->plan, unit)); + mcpp::wire::emit({ + .kind = std::format("mcpp.why.{}", topic), + .effects = { mcpp::wire::Effect::ReadProject }, + .data = data, + .diagnostics = diags, + }); + return ctx ? 0 : 2; +} + export int why_build_programs_or_compat_json(std::string_view topic, const std::string& features) { const bool wasQuiet = mcpp::ui::is_quiet(); mcpp::ui::set_quiet(true); @@ -1537,16 +1618,16 @@ export int why_build_programs_or_compat_json(std::string_view topic, const std:: } export int why_report(const std::string& topic, const std::string& features, - const std::string& subject = {}) { + const std::string& subject = {}, const std::string& target = {}) { const bool all = topic.empty() || topic == "all"; static constexpr std::string_view kTopics[] = { - "all", "toolchain", "sysroot", "runtime", "deps", "runners", "sources", "tool", "payload", - "build-programs", "compat", + "all", "target", "toolchain", "sysroot", "runtime", "deps", "runners", "sources", "tool", + "payload", "build-programs", "compat", "flags", "platform", }; if (!topic.empty() && std::ranges::find(kTopics, std::string_view(topic)) == std::end(kTopics)) { std::println(libc::stderr(), "error: '{}' is not a topic of `mcpp why`; the topics are " - "toolchain, sysroot, runtime, deps, runners, sources, tool , " - "payload , build-programs and compat", topic); + "target, toolchain, sysroot, runtime, deps, runners, sources, tool , " + "payload , build-programs, compat, flags [] and platform", topic); return 2; } @@ -1560,11 +1641,70 @@ export int why_report(const std::string& topic, const std::string& features, // visible only in the `resolution.json` of that build. mcpp::build::BuildOverrides ov; ov.features = features; + ov.target_triple = target; auto ctx = mcpp::build::prepare_build(/*print_fingerprint=*/false, /*includeDevDeps=*/false, {}, ov); if (!ctx) { std::println(libc::stderr(), "error: {}", ctx.error()); return 2; } auto& tc = ctx->tc; + // THE SELECTION (SPEC-016 §4): the request, the identity and provider it + // was given, and every candidate in the order the rule took them. + if (all || topic == "target") { + auto const& s = ctx->targetSelection; + if (s.request.empty()) { + std::println("target: {} (the host's own build; no request to select for)", s.identity); + } else { + std::println("target: {} → {}", s.request, s.identity); + std::println(" provider: {}{}", s.provider.empty() ? std::string("(the platform's)") : s.provider, + s.provider.empty() ? std::string{} : std::format(" (priority {})", s.priority)); + if (s.fallback) std::println(" fallback: {}", s.why); + if (!s.candidates.empty()) std::println(" candidates, in order:"); + for (auto const& c : s.candidates) { + const bool taken = c.identity == s.identity && c.provider == s.provider; + std::println(" {:<26} {:<14} {:>4} {}", c.identity, + c.provider.empty() ? std::string("(platform)") : c.provider, c.priority, + taken ? std::string("taken") + : c.available ? std::string("available") : "not available: " + c.why); + } + } + } + // THE PLATFORM'S ROWS AND THE IDENTITIES IT SERVES (SPEC-016 §5), each with + // the provider and its priority. + if (topic == "platform") { + namespace mp = mcpp::manifest::platform; + const auto at = platform_at_hand(*ctx); + if (!at.statement) { + std::println("platform: none (the build of {} uses no platform package)", ctx->targetSelection.identity); + return 0; + } + const auto name = mp::platform_name(at.name); + std::println("platform: {} {}", at.name, at.version); + for (auto const& d : at.statement->defines) std::println(" defines {}", d.triple); + for (auto const& s : mp::served_identities(*at.statement, name)) + std::println(" serves {:<28} {:<20} priority {}", s.identity, s.provider, s.priority); + for (auto const& w : mp::check_statement(*at.statement, name).warnings) + std::println(" warning: {}", w); + return 0; + } + // EVERY WORD OF A UNIT'S COMMAND LINE AND WHO GAVE IT (mcpp#911 G2). + if (topic == "flags") { + const auto o = mcpp::build::flag_origins(ctx->plan, subject); + if (o.units.empty()) + std::println("flags: no unit {}", subject.empty() ? std::string("of the root package") + : std::format("ends with '{}'", subject)); + for (auto const& u : o.units) { + std::println("flags of {} (package {}, for {}):", u.file, u.package, o.identity); + for (auto const& w : u.words) + std::println(" {:<9} {}{}", w.layer, w.word, + w.detail.empty() ? std::string{} : std::format(" [{}]", w.detail)); + } + std::println("link flags (for {}):", o.identity); + for (auto const& w : o.link) + std::println(" {:<9} {}{}", w.layer, w.word, + w.detail.empty() ? std::string{} : std::format(" [{}]", w.detail)); + return 0; + } + if (all || topic == "toolchain") { const auto prof = mcpp::toolchain::abi_profile(tc); std::println("toolchain: {}", tc.label()); @@ -1734,6 +1874,8 @@ export int why_report(const std::string& topic, const std::string& features, list += (list.empty() ? "" : ", ") + x.get(); std::println(" instances: {}", list); } + if (!entry.value("root", false) && entry.contains("manifest_dir")) + std::println(" root: {}", entry.value("manifest_dir", std::string{})); // A package with no target instance is linked into nothing here. const auto inst = entry.value("instances", nlohmann::json::array()); const bool linked = !inst.is_array() || inst.empty() diff --git a/src/freestanding/linkline.cppm b/src/freestanding/linkline.cppm index 4e688d7a1..dddeb3b9d 100644 --- a/src/freestanding/linkline.cppm +++ b/src/freestanding/linkline.cppm @@ -43,10 +43,11 @@ export namespace mcpp::freestanding { // `-dumpmachine`) nothing about the driver's identity says which target is // wanted. Omit it and the build silently produces host objects — measured, and // the exact shape of the E1 defect this work exists to close. -inline std::string compile_prefix(const Spec& s, bool targetCxxRuntime = false) { +inline std::string compile_prefix(const Spec& s, bool targetCxxRuntime = false, + bool hostedC = false) { std::string out; out += " --target=" + std::string(s.triple); - for (auto const& f : compile_flags(s, targetCxxRuntime)) { out += ' '; out += f; } + for (auto const& f : compile_flags(s, targetCxxRuntime, hostedC)) { out += ' '; out += f; } return out; } @@ -127,7 +128,10 @@ inline std::string link_flags_direct(const Spec& s, const LinkInputs& in, const std::filesystem::path&)>& esc) { std::string out; - out += " -m " + std::string(s.lldEmulation); + for (auto const& w : row_link_words(s)) { + out += ' '; + out += w; + } out += " -Bstatic"; // No loader exists, so an image must not name one. The driver line reaches // this through `-static`; here it is said to the linker. diff --git a/src/freestanding/target.cppm b/src/freestanding/target.cppm index 17777fffe..b122e9e90 100644 --- a/src/freestanding/target.cppm +++ b/src/freestanding/target.cppm @@ -31,6 +31,7 @@ export module mcpp.freestanding.target; import std; import mcpp.toolchain.triple; +import mcpp.toolchain.builtin_targets; export namespace mcpp::freestanding { @@ -82,250 +83,26 @@ struct Spec { std::string_view lldEmulation; }; -// The flags each row's `extra` column points at. Named arrays rather than -// inline braces because a `span` must refer to storage that outlives it, and a -// temporary array in an aggregate initialiser does not. -inline constexpr std::string_view kX86_64NoneExtra[] = { - // WITHOUT THIS, EVERY TRAP HANDLER ON THIS TARGET CORRUPTS THE FUNCTION - // IT INTERRUPTED, AND NOTHING REPORTS IT. - // - // The System V x86-64 ABI reserves 128 bytes below `rsp` — the red zone — - // which a leaf function may use without adjusting the stack pointer, - // because on a hosted system nothing else writes there: the kernel switches - // to its own stack for interrupts, and signal frames are placed clear of - // it. - // - // On bare metal nothing does that for you. The processor pushes an - // interrupt frame at `rsp` — into the red zone — and the leaf function - // resumes to find its locals overwritten. There is no fault and no - // diagnostic; the value is simply wrong, and only sometimes, because it - // depends on whether an interrupt happened to arrive inside a leaf. - // - // This is a property of the target rather than of a project, which is why - // it is here: there is no bare-metal x86_64 program for which the red zone - // is safe. RISC-V and aarch64 have no equivalent, which is why the column - // did not exist until this row. - "-mno-red-zone", -}; - -// THE SOFT-FLOAT M-PROFILE ROWS, AND WHY THE FLOAT ABI IS NOT ENOUGH. -// -// `thumbv7em-none-eabi` and `-eabihf` differ in their float ABI, and clang -// derives that from the triple with no help from us (measured, `-###`: -// `-mfloat-abi soft` and `hard` respectively). It would be reasonable to stop -// there. Measured on llvm 22.1.8, that is wrong: -// -// clang --target=thumbv7em-none-eabi -O2 -S (float a*b+1.0f) -// vmul.f32 ← an FPU instruction -// -// The float ABI governs how floating-point values cross a function boundary. -// It does not govern whether the compiler may USE the FPU inside one, and the -// `thumbv7em` architecture implies FPv4-SP, so clang emits FPU instructions -// under either ABI. A plain Cortex-M4 — the part this row exists to serve, -// since the -eabihf row serves M4F — has no FPU, and the instruction faults. -// -// The failure is a hard fault at run time on real silicon, with a clean -// compile and a clean link. It is the shape this table exists to prevent. -// -// `-mfpu=none` restores the property the row's name claims. Measured: the -// instruction count returns to zero, and the `-eabihf` rows must NOT receive -// this flag — there it would discard the FPU the row exists to use. -// -// EVERY SOFT ROW CARRIES IT, INCLUDING THE ONES WHERE NOTHING WAS MEASURED. -// -// Only `thumbv7em-none-eabi` was observed emitting an FPU instruction; -// `thumbv6m`, `thumbv7m` and `thumbv8m.base` describe architectures with no FPU -// at all, and `thumbv8m.main-none-eabi` emits none today because its default -// CPU is `generic`. Applying the flag only where a count was non-zero would -// make the table record a MEASUREMENT rather than a GUARANTEE, and the rows -// would then differ for a reason no reader could see. -// -// A row states the property it promises. `-mfpu=none` is accepted on all four -// (measured), costs nothing where there is no FPU, and is what a unit test can -// quantify over — which is the only place this rule can be stated, since a -// build of any one row cannot speak for the others. -inline constexpr std::string_view kThumbSoftExtra[] = { - "-mfpu=none", -}; - -// AND THE SAME RULE ON A-PROFILE, WHERE IT WAS MEASURED SEPARATELY RATHER -// THAN ASSUMED TO CARRY OVER. -// -// `armv7a-none-eabi` is a different architecture from `thumbv7em`, so the -// M-profile measurement above says nothing about it. Measured on llvm 22.1.8, -// the same float multiply: -// -// clang --target=armv7a-none-eabi -march=armv7-a -O2 -S → 1 vmul/vmla -// …the same, with -mfpu=none → 0 -// -// A Cortex-A without a VFP is unusual and permitted, and a Cortex-R more so. -// The row states the property its name claims, exactly as the M rows do. -inline constexpr std::string_view kArmSoftExtra[] = { - "-mfpu=none", -}; - -// THE `libdir` COLUMN WAS EMPTY ON THE LAST TWO ROWS UNTIL 2026-08-21, AND -// THAT WAS CORRECT UNTIL THE DAY IT WAS NOT. -// -// It names the sub-directory of a multilib C library — picolibc's own -// convention, `/` — and those two rows had no C library in the -// index to point into, so an empty column said something true. -// -// `xim:picolibc-aarch64` and `xim:picolibc-x86` now exist. Measured with the -// column still empty: a project that declares -// `[target.aarch64-none-elf] sysroot = "xim:picolibc-aarch64@1.8.12"` gets -// `'stdio.h' file not found` — the package is installed and correct, and the -// engine cannot find the profile inside it. -// -// Filling this does NOT give those targets a C library by default. The -// column is consulted only when a sysroot has been resolved, and the target -// TABLE still binds none for these two rows — the zero-libc tier stays the -// default and the package stays opt-in. This makes the opt-in work; it does not -// take the opt-out away. -// -// Defaults, not the only possibility. `rv64gc/lp64d` is what qemu `virt` -// runs and what the first BSP targets; a board that needs `rv32imac/ilp32` -// selects it through its own manifest, not by editing this table. The table -// exists so that `--target riscv64-none-elf` alone is enough to produce a -// correct object file — not to enumerate every board. -inline constexpr Spec kTable[] = { - // triple march mabi mcmodel libdir - { "riscv64-none-elf", "rv64gc", "lp64d", "medany", "rv64gc/lp64d" }, - { "riscv32-none-elf", "rv32imac", "ilp32", "medany", "rv32imac/ilp32" }, - // `aapcs` AND NOT `lp64`. The two RISC-V rows above spell their ABI the - // way RISC-V does, and the obvious extrapolation to aarch64 is `lp64` — - // which is what LP64 aarch64 actually is, and which clang rejects: - // - // error: unknown target ABI 'lp64' - // - // aarch64's `-mabi` names a procedure call standard rather than a data - // model, and the only value it takes for this target is `aapcs`. Measured - // rather than reasoned: `-mabi=lp64` compiles on riscv64 and fails here, - // so a table filled in by analogy would have been wrong in exactly the - // place the analogy is strongest. - // - // `small` is the code model: it addresses ±4GiB PC-relative, which covers - // any single bare-metal image regardless of where the linker script places - // it. RISC-V needs `medany` for the corresponding reason — its default - // assumes the low 2GiB, and bare-metal RISC-V runs at 0x80000000. - // - // THE LIBDIR COLUMN IS EMPTY, AND THAT IS THE ROW'S CHARACTER. It names - // a C library's multilib directory, and this target's row in the target - // table resolves no C library: `aarch64-none-elf` is the zero-libc tier. - // The engine reads this column only when a sysroot exists, so an invented - // value would be a value nothing could ever check. - { "aarch64-none-elf", "armv8-a", "aapcs", "small", "armv8-a/aapcs" }, - // `x86-64` WITH A HYPHEN, WHICH IS THE ONE PLACE THIS TRIPLE'S TWO - // SPELLINGS DIVERGE. The triple segment is `x86_64` with an underscore and - // the `-march` value is `x86-64` with a hyphen; deriving one from the other - // is a substitution that looks harmless and produces `unknown target CPU`. - // - // `x86-64` is the baseline, not a modern microarchitecture level. A row - // that named `x86-64-v3` would produce images that fault on hardware and - // emulators older than roughly 2015, for a target whose whole audience is - // people who do not control what they run on. A project that knows its - // machine raises it in its own manifest. - // - // `sysv` is the only ABI this target has, and unlike aarch64's `aapcs` it - // is also the driver's default. It is written out because this table is the - // place the decision is made, not the place it is inherited. - // - // `small` places code and data in the low 2 GiB, which is where a - // `-kernel` image loaded by an emulator runs. A higher-half kernel — one - // linked at 0xFFFFFFFF80000000 — selects `-mcmodel=kernel` in its own - // manifest; that is a linker-script decision and cannot be a default, - // because the two are wrong for each other rather than merely suboptimal. - // - // THE LIBDIR IS EMPTY FOR THE SAME REASON aarch64's IS: this row - // resolves no C library, so a value here could never be checked. - { "x86_64-none-elf", "x86-64", "sysv", "small", "x86-64/sysv", kX86_64NoneExtra, - "elf_x86_64" }, - // ── Cortex-M ──────────────────────────────────────────────────────────── - // - // `mcmodel` is EMPTY on every row: 32-bit ARM has no `-mcmodel` axis, and - // `compile_flags` already omits the flag when this column is empty. - // - // `lldEmulation` is EMPTY on every row, and that was measured rather than - // assumed. clang has a BareMetal toolchain for arm, so these triples reach - // `ld.lld` through the driver exactly as the riscv and aarch64 rows do — - // the x86_64 row's problem does not recur here. Verified end to end: a - // freestanding thumb object links under `ld.lld` and boots under QEMU. - // - // `libdir` IS THE TRIPLE HERE, NOT `/` LIKE EVERY OTHER - // ROW, AND THE DIFFERENCE IS AN ABI SUBSTITUTION THAT NOTHING REPORTS. - // - // The column names the sub-directory a multilib C library uses, and on - // riscv `/` separates every profile because `mabi` there IS - // the float ABI — `lp64d` and `lp64` are different values. On ARM `mabi` - // names the PROCEDURE CALL STANDARD and is `aapcs` for both variants, while - // the float ABI lives in this triple's `eabi`/`eabihf` suffix. So - // `armv7e-m/aapcs` would name ONE directory for two incompatible libraries. - // - // Measured while building `xim:picolibc-arm`: under the sibling convention - // the seven profiles collapsed into five directories and - // `armv7e-m/aapcs/libc.a` came out carrying `Tag_ABI_HardFP_use` — the - // hard-float build, sitting exactly where a soft-float program would find - // it. Nothing failed at build time. - // - // Filling this does NOT give these rows a C library by default: the - // column is read only once a sysroot has been resolved, and the target - // table still binds none. The zero-libc tier stays the default and - // `[target.] sysroot = "xim:picolibc-arm@1.8.12"` is the opt-in. - // This makes the opt-in work; it does not take the opt-out away. - // - // `-mabi=aapcs`, matching the aarch64 row and for the same reason: on - // ARM `-mabi` names a procedure call standard, not a data model. - // triple march mabi mcmodel libdir extra - { "thumbv6m-none-eabi", "armv6-m", "aapcs", "", "thumbv6m-none-eabi", kThumbSoftExtra }, - { "thumbv7m-none-eabi", "armv7-m", "aapcs", "", "thumbv7m-none-eabi", kThumbSoftExtra }, - { "thumbv7em-none-eabi", "armv7e-m", "aapcs", "", "thumbv7em-none-eabi", kThumbSoftExtra }, - { "thumbv7em-none-eabihf", "armv7e-m", "aapcs", "", "thumbv7em-none-eabihf" }, - { "thumbv8m.base-none-eabi", "armv8-m.base","aapcs","", "thumbv8m.base-none-eabi", kThumbSoftExtra }, - { "thumbv8m.main-none-eabi", "armv8-m.main","aapcs","", "thumbv8m.main-none-eabi", kThumbSoftExtra }, - { "thumbv8m.main-none-eabihf","armv8-m.main","aapcs","", "thumbv8m.main-none-eabihf" }, - // ── ARMv7-A (Cortex-A, 32-bit) ────────────────────────────────────────── - // - // THE FIRST 32-BIT MACHINE WITH A MEMORY MANAGEMENT UNIT, AND THAT IS - // WHY IT IS HERE RATHER THAN BEING A SECOND SPELLING OF THE M ROWS. - // - // Every other 32-bit row in this table is M-profile: an MPU that describes - // regions by base and limit, and no page-table entry at all. A-profile has - // a real MMU with a page-table walker, so it is the first target on which - // an address-space abstraction has to answer what a 32-bit machine's entry - // looks like — short descriptors are 32 bits wide, long (LPAE) ones 64. - // That question cannot be asked on a machine with no entries. - // - // `mcmodel` and `libdir` are empty for the reasons the M rows give. - // `lldEmulation` is empty because clang's BareMetal toolchain covers arm: - // measured, the driver reaches `ld.lld` and the x86_64 row's problem does - // not recur. - // - // THE TIER IS `verified` AND THE MEASUREMENT IS NAMED. 2026-09-04 under - // `xim:qemu-arm@9.2.4-1`: both rows built an image that BOOTED on - // `-M virt -cpu cortex-a15`, printed over semihosting and reported its exit - // status. - // - // AND THE SEMIHOSTING EXIT CALL IS NOT SPELLED THE WAY M-PROFILE SPELLS - // IT. `SYS_EXIT` (0x18) on AArch32 takes the reason code in `r1` directly; - // the `{reason, code}` block every Cortex-M board here passes is - // `SYS_EXIT_EXTENDED` (0x20), which exists because a 32-bit `r1` cannot - // carry both. Passing the block to 0x18 prints correctly and then reports - // the WRONG exit status — measured: a program exiting 0 reported 1. That is - // a board fact rather than a target fact, recorded here because it is where - // the next person to write such a board will look. - // - // `libdir` IS THE TRIPLE HERE TOO, AND FOR THE SAME REASON AS THE - // M ROWS — even though no package carries an A-profile multilib yet. - // - // The column is a CONVENTION about where a sysroot puts a profile, not a - // claim that one exists: it is read only after a sysroot has been resolved, - // and an absent directory is skipped. Leaving it empty would leave the next - // A-profile payload free to choose `/` — which maps these two - // incompatible libraries onto one directory, exactly as it did for - // `armv7e-m` when `xim:picolibc-arm` was first built. - { "armv7a-none-eabi", "armv7-a", "aapcs", "", "armv7a-none-eabi", kArmSoftExtra }, - { "armv7a-none-eabihf", "armv7-a", "aapcs", "", "armv7a-none-eabihf" }, -}; +// THE ROWS ARE DATA (design 2026-11-09; SPEC-016): each bare-metal row of +// modules/toolchain-model/data/builtin.toml states its ISA profile -- +// `march`, `mabi`, `code-model`, `multilib`, `flags`, `link-emulation` -- with +// the measurements behind each value beside it. This table is that data in the +// shape the bare-metal code reads; a row with no `march` is not a bare-metal row. +inline constexpr auto kTable = [] { + constexpr auto& rows = mcpp::toolchain::builtin::kRows; + constexpr std::size_t n = [] { + std::size_t c = 0; + for (auto const& r : mcpp::toolchain::builtin::kRows) c += r.march.empty() ? 0 : 1; + return c; + }(); + std::array out{}; + std::size_t i = 0; + for (auto const& r : rows) { + if (r.march.empty()) continue; + out[i++] = Spec{ r.triple, r.march, r.mabi, r.codeModel, r.multilib, r.flags, r.linkEmulation }; + } + return out; +}(); // The single read point. Returns nullopt for anything that is not a known // bare-metal target — including hosted triples, so a caller can use this as @@ -356,17 +133,32 @@ inline std::optional resolve(std::string_view triple) { // case and named it as the point at which the exception/RTTI pair stops being // unconditional; the caller answers it from the capability the graph declares, // so the answer is the graph's rather than a guess about the target. -inline std::vector compile_flags(const Spec& s, - bool targetCxxRuntime = false) { +// THE ROW'S OWN WORDS: its machine description; everything `compile_flags` adds +// after them is the engine's convention (`mcpp why flags` tells them apart). +inline std::vector row_words(const Spec& s) { std::vector out; out.emplace_back(std::string("-march=") + std::string(s.march)); - out.emplace_back(std::string("-mabi=") + std::string(s.mabi)); + if (!s.mabi.empty()) + out.emplace_back(std::string("-mabi=") + std::string(s.mabi)); if (!s.mcmodel.empty()) out.emplace_back(std::string("-mcmodel=") + std::string(s.mcmodel)); // Whatever the three columns above could not say. Emitted before // `-ffreestanding` so the ordering of this function's output stays a // function of the table rather than of the row. for (auto flag : s.extra) out.emplace_back(flag); + return out; +} + +// The row's own words of a link by ld.lld itself: the emulation, one word. +inline std::vector row_link_words(const Spec& s) { + if (s.lldEmulation.empty()) return {}; + return { "-m " + std::string(s.lldEmulation) }; +} + +inline std::vector compile_flags(const Spec& s, + bool targetCxxRuntime = false, + bool hostedC = false) { + std::vector out = row_words(s); // ONE SECTION PER FUNCTION, SO THE LINKER CAN DROP WHAT NOTHING CALLS. // // These two flags do nothing on their own; they are the half of @@ -410,7 +202,7 @@ inline std::vector compile_flags(const Spec& s, // The alternative was to make every such program write `extern "C" int // main()`, which is a workaround for a claim the build was making on the // program's behalf and that was no longer true. - if (!targetCxxRuntime) out.emplace_back("-ffreestanding"); + if (!targetCxxRuntime && !hostedC) out.emplace_back("-ffreestanding"); // UNWIND TABLES, WHICH THE COMPILER TURNS OFF FOR THIS KIND OF TARGET // AND WHICH NOTHING IN THE BUILD OTHERWISE SAYS. // diff --git a/src/lockfile.cppm b/src/lockfile.cppm index b90aa00e9..1873e6ea9 100644 --- a/src/lockfile.cppm +++ b/src/lockfile.cppm @@ -17,6 +17,7 @@ export namespace mcpp::lockfile { using LockedIndex = mcpp::pm::LockedIndex; using LockedPackage = mcpp::pm::LockedPackage; +using LockedTarget = mcpp::pm::LockedTarget; using Lockfile = mcpp::pm::Lockfile; using LockError = mcpp::pm::LockError; diff --git a/src/pack/distribution.cppm b/src/pack/distribution.cppm index e620f8d5e..31c4e1386 100644 --- a/src/pack/distribution.cppm +++ b/src/pack/distribution.cppm @@ -17,6 +17,7 @@ import mcpp.pack.compress; import mcpp.pack.zip; import mcpp.pack.polyglot; // a polyglot file is refused by name (§8) import mcpp.toolchain.triple; // the system of a package's leg (§8) +import mcpp.libs.toml; // a generated manifest quotes its keys (mcpp#911 B1) import mcpplibs.os; export namespace mcpp::pack::distribution { @@ -1073,11 +1074,12 @@ std::string exe_launcher_manifest(std::string_view name, std::string_view versio return std::format( "# Generated by `mcpp pack`: the Windows launcher of {0} (design 2026-10-29 §4.3).\n" "[package]\nname = \"{0}\"\nversion = \"{1}\"\n\n" - "[targets.{0}]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n\n" + "[targets.{2}]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n\n" "# The OS library as one release, the one this mcpp was qualified with, so a\n" "# pack is the same at any later time (design 2026-11-07, D20).\n" - "[dependencies]\nmcpplibs.os = \"0.4.0\"\n", - name, version.empty() ? std::string_view("0.0.0") : version); + "[dependencies]\nmcpplibs.os = \"0.4.1\"\n", + name, version.empty() ? std::string_view("0.0.0") : version, + mcpp::libs::toml::escape_string(name)); } std::string exe_launcher_main(const Launch& l) { @@ -1420,10 +1422,11 @@ std::string launcher_manifest(std::string_view name, std::string_view version) { "# Generated by `mcpp pack --pkg-format single-file`: the launcher of {0}, which\n" "# carries the package as a zstd frame and runs it from the user's cache.\n" "[package]\nname = \"{0}\"\nversion = \"{1}\"\n\n" - "[targets.{0}]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n\n" + "[targets.{2}]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n\n" "[build]\nsources = [\"src/main.cpp\", \"src/carried.S\"]\n\n" - "[dependencies]\nmcpplibs.os = \"0.4.0\"\nmcpplibs.mwrap.zstd = \"1.5.7\"\n", - name, version.empty() ? std::string_view("0.0.0") : version); + "[dependencies]\nmcpplibs.os = \"0.4.1\"\nmcpplibs.mwrap.zstd = \"1.5.7\"\n", + name, version.empty() ? std::string_view("0.0.0") : version, + mcpp::libs::toml::escape_string(name)); } std::string_view launcher_main() { diff --git a/src/pack/manifest_emit.cppm b/src/pack/manifest_emit.cppm index 0e747d9a5..ed097cb0e 100644 --- a/src/pack/manifest_emit.cppm +++ b/src/pack/manifest_emit.cppm @@ -38,6 +38,7 @@ export module mcpp.pack.manifest_emit; import std; +import mcpp.libs.toml; // a generated manifest quotes its keys (mcpp#911 B1) export namespace mcpp::pack { @@ -194,7 +195,7 @@ std::string emit_package_manifest(const PackageDoc& doc) { o += std::format("exports = [{}]\n\n", join_quoted(doc.exportsModules)); } - o += std::format("[targets.{}]\n", doc.targetName); + o += std::format("[targets.{}]\n", mcpp::libs::toml::escape_string(doc.targetName)); o += std::format("kind = \"{}\"\n\n", doc.targetShared ? "shared" : "lib"); // ── how to link each leg ─────────────────────────────────────────── diff --git a/src/pack/pack.cppm b/src/pack/pack.cppm index 712b14351..b660920f7 100644 --- a/src/pack/pack.cppm +++ b/src/pack/pack.cppm @@ -59,9 +59,12 @@ import mcpp.run; import mcpp.provider; import mcpp.provider.select; import mcpp.manifest; +import mcpp.libs.toml; // a generated manifest quotes its keys (mcpp#911 B1) import mcpp.project; // the effective manifest a build reads import mcpp.build.prepare_inputs; // cfgpred: the target's conditional tables import mcpp.toolchain.triple; +import mcpp.toolchain.target_schema; +import mcpp.toolchain.target_rows; // the rows of a system a platform defines (G10) import mcpp.toolchain.base; import mcpp.ui; import mcpp.runtime.elf; @@ -250,6 +253,9 @@ struct Options { // the value `mcpp build --features` takes, carried as that string so the // parser that reads it stays the build's own (#641). std::string features; + // `--all-programs` (mcpp#911 B3): every program of the package, the + // chosen one and the others beside it. + bool allPrograms = false; }; // The strip decision for this run: `--strip`/`--no-strip` > `[pack] strip` > @@ -653,6 +659,23 @@ std::optional parse_mode(std::string_view s) { namespace detail { +// A single file is written where an earlier pack of another format staged its +// tree under the same name (`--`, beside its +// `.stage-manifest`): that tree is this command's own output and is replaced. +// Any other directory there is refused rather than removed. +inline std::string clear_staged_tree(const std::filesystem::path& out) { + std::error_code ec; + if (!std::filesystem::is_directory(out, ec)) return {}; + const auto manifest = mcpp::pack::stage_manifest_path(out); + if (!std::filesystem::exists(manifest, ec)) + return std::format("cannot write {}: a directory of that name is there, and no pack staged it", + out.string()); + std::filesystem::remove_all(out, ec); + if (!ec) std::filesystem::remove(manifest, ec); + return ec ? std::format("cannot replace the staged tree {}: {}", out.string(), ec.message()) + : std::string(); +} + // Helpers below are kept in a NAMED (non-exported) sub-namespace rather // than an anonymous one. Anonymous namespaces inside a module become // TU-local; types declared there can't appear in the signature of any @@ -2349,8 +2372,26 @@ std::string hosted_platform_for(const mcpp::platform::Resolved* P, std::string_v return std::format("{}@{}", P->package, P->version); } +// The platform a row a platform defines is built over, as `--platform` names it. +std::string platform_ref_of(const mcpp::platform::Resolved& P) { + if (!P.selection.ref.spec.path.empty()) return "path:" + P.root.generic_string(); + return std::format("{}@{}", P.package, P.version); +} + ProgramRow program_row(std::string_view system, std::string_view arch, bool single, const mcpp::platform::Resolved* P = nullptr) { + // A SYSTEM A PLATFORM DEFINES (mcpp#911 G10): its row of that architecture, + // built over the platform that defines it, as the platform's carrier and + // packs of every other row are. + if (system != "linux" && system != "macos" && system != "windows") { + if (P && P->active) + for (auto const& d : P->fields.defines) { + auto t = mcpp::toolchain::triple::parse(d.triple); + if (t && t->os == system && t->arch == arch) + return { t->str(), "static", platform_ref_of(*P) }; + } + return { std::format("{}-{}", arch, system), "static", {} }; + } if (system == "linux") { const auto triple = std::format("{}-linux-musl", arch); return { triple, "static", hosted_platform_for(P, triple) }; @@ -2484,6 +2525,7 @@ bool row_is_built(const ProgramRow& row) { if (row.triple.empty()) return true; // this machine's own row auto t = mcpp::toolchain::triple::parse(row.triple); if (!t) return false; + if (t->defined()) return !row.platform.empty(); // a platform's row, over that platform auto* k = mcpp::toolchain::triple::find_known_target(*t); return k && k->tier != "planned"; } @@ -2564,7 +2606,8 @@ carrier_build(const Plan& plan, const CarrierBuild& b) plan.binaryName, b.system, b.arch, b.carried.empty() ? "runs the image it is given" : "carries it and runs it", plan.platform.display(), b.stem, - plan.packageVersion.empty() ? "0.0.0" : plan.packageVersion, b.stem, + plan.packageVersion.empty() ? "0.0.0" : plan.packageVersion, + mcpp::libs::toml::escape_string(b.stem), dependency_entry(c.package), static_row_pin(row, plan.platform.fields.toolchain.set ? plan.platform.fields.toolchain.value : std::string())); @@ -3118,7 +3161,14 @@ run(const Plan& plan, const mcpp::config::GlobalConfig& cfg) " such linker to ask.", mcpp::pack::binfmt::format_name(fmt), plan.binaryName)); } - auto deps = ldd_parse(plan.builtBinary); + // AN ELF WITH NO LOADER AND NO NEEDED IMAGE HAS NO CLOSURE, and is + // not run to learn so: `ldd` of a static program that is not this + // machine's own -- a row a platform defines (mcpp#911 G10) -- + // executes it. + const auto facts = mcpp::runtime::elf::inspect_elf_runtime(plan.builtBinary); + const bool selfContained = facts && facts->interp.empty() && facts->needed.empty(); + auto deps = selfContained ? decltype(ldd_parse(plan.builtBinary)){} + : ldd_parse(plan.builtBinary); if (!deps) return std::unexpected(Error{std::format( "ldd failed on {}: {}", plan.builtBinary.string(), deps.error())}); @@ -3412,6 +3462,11 @@ std::vector systems_for(const std::vector& stated, std for (auto const& e : stated) { if (e == "all") { for (auto os : { "linux", "macos", "windows" }) add(os); + // And every system the platforms of this process define a row of + // for the architecture (mcpp#911 G10). + for (auto const* r : mcpp::toolchain::target::rows()) + if (r->defined && r->arch == arch && !r->os.empty() && r->os != "none") + add(std::string(r->os)); continue; } const auto dash = e.find('-'); @@ -3599,6 +3654,7 @@ std::expected, Error> single_file(const Plan& } const auto out = output_for(std::format("{}-{}", f.system, f.arch), suffix); fs::create_directories(out.parent_path(), ec); + if (auto e = detail::clear_staged_tree(out); !e.empty()) return std::unexpected(Error{e}); fs::copy_file(program, out, fs::copy_options::overwrite_existing, ec); if (ec) return std::unexpected(Error{std::format("cannot write {}: {}", out.string(), ec.message())}); written.push_back(out); @@ -3673,6 +3729,7 @@ std::expected, Error> single_file(const Plan& const auto program = *packed / (stem + suffix); const auto out = output_for(t ? t->str() : plan.triple, suffix); fs::create_directories(out.parent_path(), ec); + if (auto e = detail::clear_staged_tree(out); !e.empty()) return std::unexpected(Error{e}); fs::copy_file(program, out, fs::copy_options::overwrite_existing, ec); if (ec) return std::unexpected(Error{std::format("cannot write {}: {}", out.string(), ec.message())}); written.push_back(out); diff --git a/src/pack/pipeline.cppm b/src/pack/pipeline.cppm index a096f73a9..e592e01a6 100644 --- a/src/pack/pipeline.cppm +++ b/src/pack/pipeline.cppm @@ -537,18 +537,30 @@ std::optional member_plan(PackRun& run, GroupJob& g, MemberJob& m) { break; } } - if (mainBinary.empty()) { - // Fall back to the first binary target if package.name doesn't match. - for (auto& lu : ctx.plan.linkUnits) { - if (is_program_link_unit(lu)) { - mainBinary = ctx.outputDir / lu.output; - chosenLu = &lu; - break; - } - } + // SEVERAL PROGRAMS ARE NOT PACKED WITHOUT A WORD (mcpp#911 B3): the + // program named after the package is the package's; with none so named, + // the first by name, as before. Either way the others are said, with the + // two ways to choose otherwise. + std::vector programs; + for (auto const& lu : ctx.plan.linkUnits) + if (is_program_link_unit(lu)) programs.push_back(&lu); + const bool named = !mainBinary.empty(); + if (mainBinary.empty() && !programs.empty()) { + mainBinary = ctx.outputDir / programs.front()->output; + chosenLu = programs.front(); } if (mainBinary.empty()) return Refusal{1, about(run, m, "no binary target to pack")}; + if (programs.size() > 1 && !opts.allPrograms && run.wantTarget.empty()) { + std::string others; + for (auto const* lu : programs) + if (lu != chosenLu) others += (others.empty() ? "" : ", ") + lu->targetName; + mcpp::ui::note(std::format( + "packing {}, {}; its other programs are not packed ({}): `mcpp pack ` names " + "another, `mcpp pack --all-programs` ships them beside it", chosenLu->targetName, + named ? "the program named after the package" : "the first by name, as none is named after the package", + others)); + } // Passed to `make_plan` rather than re-derived from the file: `make_plan` // has only `mainBinary` and would otherwise have to ask the triple and // the manifest the same question a second time. @@ -633,6 +645,13 @@ std::optional member_plan(PackRun& run, GroupJob& g, MemberJob& m) { opts.runtimeFiles.push_back(productDir == "bin" ? u.output.lexically_relative("bin") : std::filesystem::path(u.output.filename())); + // `--all-programs`: the package's other programs, beside the chosen one. + if (opts.allPrograms) + for (auto const* u : programs) + if (u != chosenLu && u->kind == mcpp::build::LinkUnit::Binary) + opts.runtimeFiles.push_back(productDir == "bin" + ? u->output.lexically_relative("bin") + : std::filesystem::path(u->output.filename())); // #634 A3: the Android row reads its closure against the directories // its link declared -- a prebuilt library named through `[runtime] // link_library_dirs` is a file the link used and the device does not diff --git a/src/pack/route.cppm b/src/pack/route.cppm index 5c6fc72f0..6c9fd1804 100644 --- a/src/pack/route.cppm +++ b/src/pack/route.cppm @@ -134,14 +134,27 @@ std::expected route_pack_target(std::string_view request // refuse further in with "not a program in this build", which is a // pre-existing message for any target the application pipeline finds no // executable link unit for. + // + // SEVERAL PROGRAMS ARE NOT DECIDED HERE (mcpp#911 B3): the application + // pipeline takes the one named after the package, says which others it + // left, and refuses when none is so named (`--all-programs` ships them + // together). The first one was taken here before, without a word. const mcpp::manifest::Target* onlyLib = nullptr; - std::size_t libCount = 0; + const mcpp::manifest::Target* firstProgram = nullptr; + std::size_t libCount = 0, programCount = 0; for (auto const& t : m->targets) { - if (t.is_program()) - return PackRoute{ t.name, false, - t.kind == mcpp::manifest::Target::Application }; + if (t.is_program()) { + if (!firstProgram) firstProgram = &t; + ++programCount; + } if (is_library(t)) { onlyLib = &t; ++libCount; } } + if (programCount == 1) + return PackRoute{ firstProgram->name, false, + firstProgram->kind == mcpp::manifest::Target::Application }; + if (programCount > 1) + return PackRoute{ {}, false, + firstProgram->kind == mcpp::manifest::Target::Application }; if (libCount == 1) return PackRoute{ onlyLib->name, true }; if (libCount == 0) return std::unexpected("this package declares no program and no library to pack"); diff --git a/src/platform/platform.cppm b/src/platform/platform.cppm index ff8319c01..9976a6395 100644 --- a/src/platform/platform.cppm +++ b/src/platform/platform.cppm @@ -22,6 +22,7 @@ export module mcpp.platform; import std; import mcpp.manifest; import mcpp.toolchain.triple; +import mcpp.toolchain.target_schema; import mcpp.invocation; export namespace mcpp::platform { @@ -38,6 +39,9 @@ struct Selection { mcpp::manifest::platform::PackageRef ref; mcpp::manifest::platform::Origin origin; bool none = true; + // A built-in provider chosen by name (`builtin:`, SPEC-016 §4): no + // platform package, and the selection takes that provider alone. + std::string builtin; // A host triple selected the platform: only its `[platform.hosted]` part // applies (§5.4). bool hosted = false; @@ -88,6 +92,15 @@ void merge(mcpp::manifest::platform::Statement& base, const mcpp::manifest::plat // The part of a platform that applies when a host triple selects it: its // `[platform.hosted]` toolchain and layers, and nothing else (§5.4). mcpp::manifest::platform::Statement hosted_view(const mcpp::manifest::platform::Statement& platform); +// The statement that applies to `identity` (SPEC-016 §3): the platform's, with the +// fields of the `[[platform.provider]]` that serves it in place of the platform's +// own (its toolchain, layers and link), or the hosted view when that provider is +// hosted. The platform's own statement when no provider of it names the identity. +mcpp::manifest::platform::Statement provider_view(const mcpp::manifest::platform::Statement& platform, + std::string_view identity); +// The `[[platform.provider]]` of a platform that serves `identity`; null when none. +const mcpp::manifest::platform::ProviderSpec* provider_of(const mcpp::manifest::platform::Statement& platform, + std::string_view identity); // Whether the platform serves the triple, or refuses with the reason. A host // triple is served by a platform with a `[platform.hosted]` table. @@ -187,7 +200,7 @@ namespace { namespace mp = mcpp::manifest::platform; bool names_a_path(std::string_view v) { - if (v.empty()) return false; + if (v.empty() || v.starts_with("git+")) return false; // a repository (SPEC-016 §8) if (v.starts_with("path:")) return true; if (v.starts_with("/") || v.starts_with("./") || v.starts_with("../") || v.starts_with("~") || v == "." || v == "..") @@ -243,6 +256,13 @@ std::expected parse_choice(std::string_view value, mp::O Selection s; s.origin = std::move(origin); if (value == "none") return s; + if (value.starts_with("builtin:")) { + s.builtin = std::string(value.substr(8)); + if (s.builtin.empty()) + return std::unexpected(std::format("{}: `builtin:` names no provider; write builtin: " + "(`mcpp why target` lists them)", s.origin.key)); + return s; + } if (names_a_path(value)) { std::string dir(value.starts_with("path:") ? value.substr(5) : value); if (dir.starts_with("~")) @@ -286,6 +306,7 @@ std::expected select_target(const ChoiceInputs& in, cons s.origin = chosen->platformOrigin; if (s.origin.key.empty()) s.origin.key = std::format("[target.{}].platform", chosen->selector); + s.builtin = chosen->platformBuiltin; if (chosen->platformNone) return s; s.ref = chosen->platform; s.none = false; @@ -395,8 +416,45 @@ mp::Statement hosted_view(const mp::Statement& p) { return s; } +const mp::ProviderSpec* provider_of(const mp::Statement& p, std::string_view identity) { + for (auto const& pr : p.providers) + for (auto const& s : pr.serves) + if (mcpp::toolchain::target::serves_triple(s, identity)) return ≺ + return nullptr; +} + +mp::Statement provider_view(const mp::Statement& p, std::string_view identity) { + const auto* pr = provider_of(p, identity); + if (!pr) return p; + if (pr->base == "none") { + auto s = hosted_view(p); + s.cHosted = pr->hosted; + if (!pr->toolchain.empty()) s.toolchain = mp::Text{ pr->toolchain, pr->origin, true }; + if (!pr->layers.empty()) s.layers = pr->layers; + return s; + } + auto s = p; + s.cHosted = pr->hosted; + if (!pr->toolchain.empty()) s.toolchain = mp::Text{ pr->toolchain, pr->origin, true }; + if (!pr->layers.empty()) s.layers = pr->layers; + if (!pr->sysroot.empty()) s.sysroot = mp::Text{ pr->sysroot, pr->origin, true }; + auto const& l = pr->link; + auto text = [](mp::Text& to, const mp::Text& from) { if (from.set) to = from; }; + text(s.link.driver, l.driver); + text(s.link.start, l.start); + text(s.link.script, l.script); + text(s.link.exe, l.exe); + text(s.link.shared, l.shared); + if (l.flags.set) s.link.flags = l.flags; + if (l.inputsSet) { s.link.inputs = l.inputs; s.link.inputsOrigin = l.inputsOrigin; s.link.inputsSet = true; } + if (l.sealSet) { s.link.seal = l.seal; s.link.sealOrigin = l.sealOrigin; s.link.sealSet = true; } + return s; +} + std::expected serves(const mp::Statement& p, std::string_view package, const mcpp::toolchain::triple::Triple& t) { + // A provider of the platform that names the identity (SPEC-016 §3). + if (const auto* pr = provider_of(p, t.str())) return pr->base == "none"; for (auto const& w : p.targets.value) { auto pt = mcpp::toolchain::triple::parse(w); if (pt ? pt->str() == t.str() : w == t.str()) return false; @@ -407,8 +465,15 @@ serves(const mp::Statement& p, std::string_view package, const mcpp::toolchain:: if (!p.hosted.targets.set) return true; std::string hosts; for (auto const& w : p.hosted.targets.value) { + // A HOSTED ENTRY NAMES A SYSTEM, AND THE C LIBRARY NAMES THE ENV + // (SPEC-016 §2, I5): `x86_64-linux-gnu` in `hosted.targets` serves + // the build of that architecture and system whose identity follows + // the C library the hosted layers bring (`x86_64-linux-musl` for + // openkal-musl), so a platform written before the identity followed + // the C library keeps serving the same builds. auto pt = mcpp::toolchain::triple::parse(w); - if (pt ? pt->str() == t.str() : w == t.str()) return true; + if (pt ? (pt->str() == t.str() || (pt->arch == t.arch && pt->os == t.os)) : w == t.str()) + return true; if (!hosts.empty()) hosts += ", "; hosts += w; } diff --git a/src/pm/lock_io.cppm b/src/pm/lock_io.cppm index 7e486ae9d..1beade3e3 100644 --- a/src/pm/lock_io.cppm +++ b/src/pm/lock_io.cppm @@ -67,9 +67,22 @@ struct LockedGitSource { std::optional parse_git_source(std::string_view source); +// THE IDENTITY A REQUEST WAS GIVEN (SPEC-016 §4.5): `[target.""]` +// with the identity and its provider. A request that names a pattern +// (`x86_64-windows`) may select another identity on another machine; the +// record says which one this project's builds used, and `--locked` refuses a +// build that selects another. Written for every configuration a request +// selected, kept for the others; absent in a lock written before the table. +struct LockedTarget { + std::string request; + std::string identity; + std::string provider; +}; + struct Lockfile { int schemaVersion = 2; std::vector indices; + std::vector targets; std::vector packages; }; @@ -137,6 +150,18 @@ std::expected load(const std::filesystem::path& path) { } } + if (auto* tgt = doc->get_table("target")) { + for (auto& [k, v] : *tgt) { + if (!v.is_table()) continue; + auto& tt = v.as_table(); + LockedTarget lt; + lt.request = k; + if (auto it = tt.find("identity"); it != tt.end() && it->second.is_string()) lt.identity = it->second.as_string(); + if (auto it = tt.find("provider"); it != tt.end() && it->second.is_string()) lt.provider = it->second.as_string(); + if (!lt.identity.empty()) lock.targets.push_back(std::move(lt)); + } + } + // [[package]] arrays are not in our minimal parser. We use [package.] instead. // Or just iterate the root looking for top-level "package" table that contains a list. // For simplicity in M2: accept either format with top-level array of tables described @@ -200,6 +225,12 @@ std::string serialize(const Lockfile& lock) { out += std::format("rev = {}\n", t::escape_string(idx.rev)); } + for (auto& tg : lock.targets) { + out += std::format("\n[target.{}]\n", t::escape_string(tg.request)); + out += std::format("identity = {}\n", t::escape_string(tg.identity)); + if (!tg.provider.empty()) out += std::format("provider = {}\n", t::escape_string(tg.provider)); + } + // Blank line before packages if we had indices or just after version. if (!lock.packages.empty()) out += "\n"; diff --git a/src/pm/patch.cppm b/src/pm/patch.cppm index 309856518..e438aa3ef 100644 --- a/src/pm/patch.cppm +++ b/src/pm/patch.cppm @@ -37,10 +37,18 @@ std::expected apply(const std::filesystem::path& root, // The variant's tree: `source` copied to `variant` and the patches applied in // order, unless `variant` already holds the result for this digest. Written // beside `variant` and renamed into place, so a tree that exists is whole. +// +// THE PATCH ROOT IS THE PACKAGE'S (mcpp#911 G8): a patch names files relative +// to `patchRoot`, the directory of the package's resolved manifest (or the +// dependency's `patch_root`), relative to `source`. A patch written against +// `source` itself -- the store's version directory, as before -- still +// applies there, and `notes` receives that it did. std::expected materialize(const std::filesystem::path& source, const std::vector& patches, std::string_view digest, - const std::filesystem::path& variant); + const std::filesystem::path& variant, + const std::filesystem::path& patchRoot = {}, + std::vector* notes = nullptr); // The directory name of a variant of `version`: `+p`. Short, because Windows paths have a length. @@ -303,7 +311,9 @@ std::expected apply(const fs::path& root, std::string_view te std::expected materialize(const fs::path& source, const std::vector& patches, std::string_view digest, - const fs::path& variant) { + const fs::path& variant, + const fs::path& patchRoot, + std::vector* notes) { const fs::path marker = variant / ".mcpp-variant"; std::error_code ec; if (auto have = read_bytes(marker); have && *have == std::string(digest) + "\n") { @@ -331,7 +341,24 @@ std::expected materialize(const fs::path& source, for (auto const& p : patches) { auto text = read_bytes(p); if (!text) { fs::remove_all(work, ec); return std::unexpected(std::format("the patch '{}' cannot be read", p)); } - if (auto r = apply(work, *text, fs::path(p).filename().string()); !r) { + // The package's root, unless the patch names its first file from the + // source's own top and not from there (a patch written before G8). + fs::path root = work / patchRoot; + if (!patchRoot.empty()) { + if (auto parsed = parse(*text); parsed && !parsed->empty()) { + const auto& first = parsed->front(); + const std::string rel = first.oldPath.empty() ? first.newPath : first.oldPath; + if (!first.oldPath.empty() && !fs::exists(root / rel, ec) && fs::exists(work / rel, ec)) { + root = work; + if (notes) + notes->push_back(std::format( + "the patch '{}' names its files from the archive's top ('{}'); a patch names " + "them from the package's root ('{}'), and this one is read as written", + fs::path(p).filename().string(), rel, patchRoot.generic_string())); + } + } + } + if (auto r = apply(root, *text, fs::path(p).filename().string()); !r) { fs::remove_all(work, ec); return std::unexpected(std::format("{} (from {})", r.error(), p)); } diff --git a/src/scaffold/create.cppm b/src/scaffold/create.cppm index f333bf8df..016f0527e 100644 --- a/src/scaffold/create.cppm +++ b/src/scaffold/create.cppm @@ -17,6 +17,8 @@ import mcpp.scaffold; import mcpp.scaffold.project_name; import mcpp.ui; import mcpp.build.test_protocol; // the release of mtest the skeleton's tests use +import mcpp.toolchain.target_schema; +import mcpp.toolchain.target_rows; // the llvm a platform skeleton pins, as the built-in rows do namespace mcpp::scaffold { @@ -300,6 +302,11 @@ export int new_from_package_template( // gui alias) and a library (lib). export enum class builtin { bin, gui, lib }; +// `mcpp new --template platform` (SPEC-016 §2, §3): a platform package that +// defines one row, `-`, and the provider that serves it, with the +// llvm the built-in bare-metal rows pin. `mcpp why platform` lists what it serves. +export int create_platform_project(const mcpp::scaffold::PortableProjectName& project); + // What a project's library is named in C++: its module, the qualified name // with `-` written `_` (`acme.my-lib` -> `acme.my_lib`), and its namespace // (`acme::my_lib`). Empty when a component does not begin with a letter or @@ -564,4 +571,81 @@ UTest greeting{"hello greets by name", [] { return 0; } +int create_platform_project(const mcpp::scaffold::PortableProjectName& project) { + // The system's word: the package's name, letters and digits. + std::string os; + for (char c : project.name) + if (std::isalnum(static_cast(c))) os += static_cast(std::tolower(static_cast(c))); + if (os.empty() || !std::isalpha(static_cast(os.front()))) os = "example"; + std::string llvm; + for (auto const* p : mcpp::toolchain::target::providers()) + if (p->toolchain.starts_with("llvm@")) { llvm = std::string(p->toolchain); break; } + const std::string arch(mcpp::toolchain::platform::host_arch); + const std::string triple = std::format("{}-{}", arch, os); + + auto transaction = mcpp::scaffold::ScaffoldTransaction::begin( + std::filesystem::current_path(), project.directoryName); + if (!transaction) { + mcpp::ui::error(transaction.error()); + return 1; + } + auto tx = std::move(*transaction); + const auto& root = tx.staging_path(); + const std::string manifest = std::format(R"(# A platform (SPEC-016): the rows it defines and the provider that serves them. +# A project selects it for a target with [target.] platform = {{ path = "..." }}. +[package] +name = "{0}" +version = "0.1.0" +description = "The platform of {1}" + +[platform] +toolchain = "{2}" + +# The row: the system's triple, what the compiler is told, the object format, +# and the system its programs run on. +[[platform.define]] +triple = "{1}" +clang-triple = "{3}-unknown-none-elf" +object-format = "elf" +execution = "{4}" + +[[platform.provider]] +serves = ["{1}"] + +[platform.build] +cflags = ["-ffreestanding"] + +[platform.link] +driver = "compiler" + +# What the platform provides for the target side; `false`: nothing yet. +[platform.layers] +"kernel-abi" = false +"c-abi" = false +"c++-abi" = false +"compiler-runtime" = false +)", project.qualifiedName, triple, llvm, arch, os); + if (auto written = mcpp::scaffold::write_text_file(root / "mcpp.toml", manifest); !written) { + mcpp::ui::error(written.error()); + return 1; + } + if (auto written = mcpp::scaffold::write_text_file(root / ".gitignore", "target/\n.mcpp/\n"); !written) { + mcpp::ui::error(written.error()); + return 1; + } + auto generated = mcpp::manifest::load(root / "mcpp.toml"); + if (!generated) { + mcpp::ui::error(std::format("generated manifest validation failed: {}", generated.error().format())); + return 1; + } + if (auto committed = tx.commit(); !committed) { + mcpp::ui::error(committed.error()); + return 1; + } + mcpp::ui::line(std::format("Created platform package '{}' at {}, defining {}", project.qualifiedName, + tx.final_path().string(), triple)); + mcpp::ui::line(std::format("Next: cd {} && mcpp why platform", project.directoryName)); + return 0; +} + } // namespace mcpp::scaffold diff --git a/src/toolchain/hostflags.cppm b/src/toolchain/hostflags.cppm index 4f35871f2..fdd32723e 100644 --- a/src/toolchain/hostflags.cppm +++ b/src/toolchain/hostflags.cppm @@ -409,7 +409,14 @@ std::vector host_compile_tokens(const Toolchain& tc, // link on `__hash_memory`, which an inline function in the newer headers // names and the older dylib does not export (mcpp#630). const bool cxxFromPayload = !opt.cxxFromGraph && !opt.appleSdkCxxHeaders; - if (bypassCfg && !graphSuppliesTarget && cxxFromPayload) { + // A PAIRED PROVIDER'S PAYLOAD IS THE SYSTEM (SPEC-016 §6): its sysroot + // holds the C library, libstdc++ and the GCC runtime, and the driver + // finds them there once it is told the sysroot and the C++ library. + if (bypassCfg && !tc.pairedSysroot.empty()) { + out.push_back("--no-default-config"); + out.push_back("--sysroot=" + esc(tc.pairedSysroot)); + out.push_back("-stdlib=libstdc++"); + } else if (bypassCfg && !graphSuppliesTarget && cxxFromPayload) { for (auto& t : dm.compile_tokens(esc, opt.clangStdlibSelect)) out.push_back(t); // On AArch64 the runtime choice changes FMV/outline-atomics codegen. diff --git a/src/toolchain/provider.cppm b/src/toolchain/provider.cppm index 1bc32adf5..438be0a49 100644 --- a/src/toolchain/provider.cppm +++ b/src/toolchain/provider.cppm @@ -89,7 +89,8 @@ ProviderCapabilities capabilities_for(const Toolchain& tc) { bool msvc_target = is_msvc_target(tc); caps.has_scan_deps = true; // clang-scan-deps lives beside clang++ - caps.stdlib_id = msvc_target ? "msvc-stl" : "libc++"; + caps.stdlib_id = msvc_target ? "msvc-stl" + : !tc.pairedSysroot.empty() ? "libstdc++" : "libc++"; caps.archive_format = "llvm-ar"; break; } diff --git a/tests/e2e/1038_test_programs_of_a_directory_and_the_tests_a_change_affects.sh b/tests/e2e/1038_test_programs_of_a_directory_and_the_tests_a_change_affects.sh index 6e48b757d..d40766bf9 100644 --- a/tests/e2e/1038_test_programs_of_a_directory_and_the_tests_a_change_affects.sh +++ b/tests/e2e/1038_test_programs_of_a_directory_and_the_tests_a_change_affects.sh @@ -5,7 +5,8 @@ # # Criteria: # A. `[test] programs = "directory"`: the files of a directory below `tests/` are one program, -# named by the directory, whose cases are all of theirs; a directory whose file defines `main` +# named by the directory, whose cases are all of theirs, run as a process per file of its cases +# and, under `split = "none"`, once; a directory whose file defines `main` # begins there; two files of one directory that define `main` are refused, naming both. # B. A program built from a file of plain `main` that links a framework's library (it carries the # protocol's marker and speaks none) is one case, judged by its exit status, run and listed. @@ -50,6 +51,15 @@ printf 'data\n' > tests/data/f.txt # A printf '\n[test]\nprograms = "directory"\n' >> mcpp.toml "$MCPP" test > a.log 2>&1 || fail "A: the directory programs" a.log +# A program that speaks the protocol runs as a process per file of its cases (`split = "file"`). +grep -q 'unit/a \.\.\. ok (.*1 case)' a.log && grep -q 'unit/b \.\.\. ok (.*1 case)' a.log \ + || fail "A: unit does not run as a process per file" a.log +# A pattern names a part as the run names it. +"$MCPP" test unit/b > a-part.log 2>&1 || fail "A: a part named by its pattern" a-part.log +grep -q 'unit/b \.\.\. ok' a-part.log && ! grep -q 'unit/a \.\.\.' a-part.log \ + || fail "A: the pattern of a part did not run that part alone" a-part.log +printf 'split = "none"\n' >> mcpp.toml +"$MCPP" test > a.log 2>&1 || fail "A: the directory programs, run once" a.log grep -q 'unit \.\.\. ok (.*2 cases)' a.log || fail "A: unit is not one program of two cases" a.log grep -q '^e2e \.\.\. ok' a.log || fail "A: the directory of a file with main" a.log "$MCPP" test --cases > a-cases.log 2>&1 || fail "A: --cases" a-cases.log diff --git a/tests/e2e/1040_a_platform_defines_a_target.sh b/tests/e2e/1040_a_platform_defines_a_target.sh new file mode 100644 index 000000000..b9e62ec0f --- /dev/null +++ b/tests/e2e/1040_a_platform_defines_a_target.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash +# requires: elf llvm +source "$(dirname "${BASH_SOURCE[0]}")/_toolchain_env.sh" +# 1040 -- a platform defines a target the engine's table does not have (mcpp#911 G1; SPEC-016 §2). +# +# A platform package states `[[platform.define]]` rows: the triple, the triple its compiler is +# given, the object format, the family and the system the artifact runs on. The parser then accepts +# the row's system, `cfg(os = ...)` answers by it, the output directory is named after it, and the +# platform's provider serves it. The row here is a toy system whose artifact is a static Linux ELF, +# so the host runs it. +# +# Criteria: `--target x86_64-toyos` with the platform builds into target/x86_64-toyos, the +# `cfg(os = "toyos")` table applies, the program runs (`mcpp run`) and exits as written; without +# the platform the same request is an unknown target. +set -e +t=$(mktemp -d); trap 'rm -rf "$t"' EXIT +fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | tail -20; exit 1; } +mkdir -p "$t/toy" "$t/app/src" +cat > "$t/toy/mcpp.toml" < "$t/app/mcpp.toml" <<'EOF' +[package] +name = "toyapp" +version = "0.1.0" + +[build] +sources = ["src/*.c"] +ldflags = ["-nostdlib", "-static", "-no-pie"] + +[targets.toyapp] +kind = "bin" +main = "src/main.c" + +[target.x86_64-toyos] +platform = { path = "../toy" } + +[target.'cfg(os = "toyos")'.build] +cflags = ["-DTOY_SYSTEM=1"] +EOF +cat > "$t/app/src/main.c" <<'EOF' +#ifndef TOY_SYSTEM +#error "cfg(os = \"toyos\") did not apply" +#endif +void _start(void) { + __asm__ volatile("mov $60, %eax\n mov $7, %edi\n syscall"); +} +EOF +cd "$t/app" + +out=$("$MCPP" build --target x86_64-toyos 2>&1) || fail "the platform's row did not build" "$out" +grep -q "Target x86_64-toyos → x86_64-unknown-linux-gnu" <<<"$out" || fail "the report names no clang triple of the row" "$out" +grep -q "Platform toy-platform 0.1.0" <<<"$out" || fail "the platform is not reported" "$out" +exe=$(find target/x86_64-toyos -path '*/bin/toyapp' -type f | head -1) +[ -n "$exe" ] || fail "no artifact under target/x86_64-toyos" "$(ls -R target | head -30)" +if readelf -l "$exe" | grep -q INTERP; then fail "the artifact names a program interpreter: the host's C library leaked in"; fi +set +e; "$exe"; rc=$?; set -e +[ "$rc" = 7 ] || fail "the artifact exited $rc, not 7" +echo " ok the row builds into target/x86_64-toyos, cfg(os = \"toyos\") applies, and it runs" + +set +e; run=$("$MCPP" run --target x86_64-toyos 2>&1); rc=$?; set -e +[ "$rc" = 7 ] || fail "mcpp run ended $rc, not 7" "$run" +echo " ok mcpp run runs it on the system its row names (exit 7)" + +# A row whose artifacts run on another system: the build does not judge their runtime closure by +# this machine's loader, and says so. +sed -i 's/^execution = "linux"/execution = "toyos"/' "$t/toy/mcpp.toml" +out=$("$MCPP" build --target x86_64-toyos 2>&1) || fail "a row for another system did not build" "$out" +grep -q "the artifacts are for toyos, not this machine" <<<"$out" || fail "the build judged a foreign artifact by this machine" "$out" +echo " ok an artifact for another system is built and its closure is not judged here" + +# An artifact for another system is built, not run, without a runner; a runner states how it runs +# (mcpp#911 G11). +set +e; out=$("$MCPP" run --target x86_64-toyos 2>&1); rc=$?; set -e +[ "$rc" != 0 ] || fail "an artifact for toyos was run on this host" "$out" +grep -q "x86_64-toyos artifacts run on toyos, not on this .* host: built, not run" <<<"$out" \ + || fail "the refusal does not say built, not run" "$out" +sed -i 's|^platform = { path = "../toy" }$|&\nrunner = ["env", "{}"]|' mcpp.toml +set +e; out=$("$MCPP" run --target x86_64-toyos 2>&1); rc=$?; set -e +[ "$rc" = 7 ] || fail "the runner did not run it (exit $rc)" "$out" +sed -i '/^runner = \["env", "{}"\]$/d' mcpp.toml +echo " ok without a runner it is built, not run; a runner runs it" + +# The platform names its programs; a build program that names them for the host does not change +# the platform's suffix, and the build says so (mcpp#911 B2). +sed -i 's/^driver = "compiler"$/driver = "compiler"\nexe = "{name}.toy"/' "$t/toy/mcpp.toml" +cat > build.mcpp <<'EOF' +import std; +int main() { std::println("mcpp:artifact-name=exe:{{name}}"); } +EOF +out=$("$MCPP" build --target x86_64-toyos 2>&1) || fail "the build with the program's name did not succeed" "$out" +grep -q 'the platform toy-platform 0.1.0 names it `{name}.toy`: the platform.s name stands' <<<"$out" \ + || fail "the conflict of names is not reported" "$out" +[ -n "$(find target/x86_64-toyos -path '*/bin/toyapp.toy' -type f | head -1)" ] \ + || fail "the program is not named as the platform names it" "$(find target/x86_64-toyos -path '*/bin/*' | head)" +rm build.mcpp +echo " ok the platform's name of a program stands over a build program's" + +# A row a platform defines is packed as a built-in row is (mcpp#911 G10); a static program is not +# run to learn its closure. +out=$("$MCPP" pack --target x86_64-toyos 2>&1) || fail "the platform's row was not packed" "$out" +listing=$(tar tzf target/dist/toyapp-0.1.0-x86_64-toyos.tar.gz) +grep -q 'bin/toyapp.toy$' <<<"$listing" || fail "the package holds no program" "$listing" +echo " ok the platform's row is packed" + +sed -i '/^\[target.x86_64-toyos\]/,/^platform/d' mcpp.toml +out=$("$MCPP" build --target x86_64-toyos 2>&1 || true) +grep -q "unknown target 'x86_64-toyos'" <<<"$out" || fail "without the platform the request was not an unknown target" "$out" +echo " ok without the platform the request is an unknown target" +echo "PASS: 1040 a platform defines a target" diff --git a/tests/e2e/1041_a_32_bit_x86_bare_target.sh b/tests/e2e/1041_a_32_bit_x86_bare_target.sh new file mode 100755 index 000000000..22ac4be01 --- /dev/null +++ b/tests/e2e/1041_a_32_bit_x86_bare_target.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# requires: elf llvm +source "$(dirname "${BASH_SOURCE[0]}")/_toolchain_env.sh" +# 1041 -- 32-bit x86 without an operating system (mcpp#911 G12, G4; SPEC-016 §3). +# +# The row `i686-none-elf` compiles for the i686 instruction set and links with ld.lld directly +# under the emulation `elf_i386`. A platform package that states `link.driver = "compiler"` +# replaces the row's direct link: the compiler driver links, with the row's flags and ld.lld named +# by path. The program enters at `_start` and ends with the 32-bit system call `exit`, so an x86_64 +# Linux machine with IA-32 emulation runs it as it is. +# +# Criteria: the artifact is an ELF32 Intel 80386 executable; the link line is ld.lld's (`-m +# elf_i386`) without a platform and the compiler's (`--target=i686-none-elf ... -fuse-ld=`) with one; +# where the machine runs 32-bit programs, both artifacts exit 7. +set -e +t=$(mktemp -d); trap 'rm -rf "$t"' EXIT +fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | tail -20; exit 1; } +mkdir -p "$t/bsp" "$t/app/src" +cat > "$t/app/mcpp.toml" <<'EOF' +[package] +name = "boot32" +version = "0.1.0" + +[build] +sources = ["src/*.c"] + +[targets.boot32] +kind = "bin" +main = "src/main.c" +EOF +cat > "$t/app/src/main.c" <<'EOF' +#if !defined(__i386__) || defined(__x86_64__) +#error "not a 32-bit x86 compilation" +#endif +_Static_assert(sizeof(void*) == 4, "a 32-bit pointer"); +void _start(void) { + __asm__ volatile("mov $1, %eax\n mov $7, %ebx\n int $0x80"); +} +EOF +cd "$t/app" + +# A 32-bit program runs here when the kernel has IA-32 emulation; elsewhere the run is skipped. +runs32() { + [ "$(uname -m)" = x86_64 ] || return 1 + set +e; "$1"; local rc=$?; set -e + [ "$rc" = 7 ] && return 0 + [ "$rc" = 126 ] && return 1 + fail "the artifact exited $rc, not 7" +} +check() { # $1: what links; $2: the pattern of its link line + local exe ninja + exe=$(find target/i686-none-elf -path '*/bin/boot32' -type f | head -1) + [ -n "$exe" ] || fail "no artifact under target/i686-none-elf" + readelf -h "$exe" | grep -q 'ELF32' || fail "the artifact is not ELF32" "$(readelf -h "$exe")" + readelf -h "$exe" | grep -q 'Intel 80386' || fail "the artifact is not for i386" "$(readelf -h "$exe")" + ninja=$(find target/i686-none-elf -name build.ninja | head -1) + grep -qE "^c_ldflags += $2" "$ninja" || fail "$1 does not link" "$(grep -E '^c?_?ldflags' "$ninja")" + if runs32 "$exe"; then echo " ok $1 links an ELF32 i386 program that exits 7" + else echo " ok $1 links an ELF32 i386 program (this machine runs no 32-bit program)"; fi +} + +out=$("$MCPP" build --target i686-none-elf 2>&1) || fail "i686-none-elf did not build" "$out" +check "ld.lld under elf_i386" '-m elf_i386' + +cat > "$t/bsp/mcpp.toml" <> mcpp.toml <<'EOF' + +[target.i686-none-elf] +platform = { path = "../bsp" } +EOF +out=$("$MCPP" build --target i686-none-elf 2>&1) || fail "i686-none-elf with the platform did not build" "$out" +grep -q "Platform bsp32 0.1.0" <<<"$out" || fail "the platform is not reported" "$out" +check "the platform's compiler driver" '.*--target=i686-none-elf .*-fuse-ld=' +echo "PASS: 1041 a 32-bit x86 bare target" diff --git a/tests/e2e/1042_each_flag_names_its_layer_and_a_request_its_selection.sh b/tests/e2e/1042_each_flag_names_its_layer_and_a_request_its_selection.sh new file mode 100755 index 000000000..2063f83b4 --- /dev/null +++ b/tests/e2e/1042_each_flag_names_its_layer_and_a_request_its_selection.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# requires: elf llvm +source "$(dirname "${BASH_SOURCE[0]}")/_toolchain_env.sh" +# 1042 -- every flag names its layer, and a request its selection (mcpp#911 G2; SPEC-016 §4.5). +# +# `mcpp why flags ` lists each word of the unit's command line and of the link line with the +# layer that gave it: the row's machine description, the platform, the toolchain, the package, the +# engine. `mcpp why target` lists the candidates of a request in the order the selection took them. +# +# Criteria: `why flags` attributes `-march=x86-64` and `-mno-red-zone` to the row, `-ffreestanding` +# to the engine and the package's define to the package; `why target --format json` names the +# request, the identity and the candidates in order. +set -e +t=$(mktemp -d); trap 'rm -rf "$t"' EXIT +fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | tail -20; exit 1; } +mkdir -p "$t/src" +cat > "$t/mcpp.toml" <<'EOF' +[package] +name = "bare" +version = "0.1.0" + +[build] +sources = ["src/*.c"] +cflags = ["-DBARE_APP=1"] + +[targets.bare] +kind = "bin" +main = "src/main.c" +EOF +cat > "$t/src/main.c" <<'EOF' +void _start(void) { __asm__ volatile("mov $60, %eax\n mov $7, %edi\n syscall"); } +EOF +cd "$t" + +why=$("$MCPP" why flags main.c --target x86_64-none-elf 2>/dev/null) || fail "why flags failed" "$why" +grep -Eq '^ row +-march=x86-64 +\[x86_64-none-elf\]' <<<"$why" || fail "the row's word does not name the row" "$why" +grep -Eq '^ row +-mno-red-zone +\[x86_64-none-elf\]' <<<"$why" || fail "the row's flag does not name the row" "$why" +grep -Eq '^ engine +-ffreestanding' <<<"$why" || fail "the engine's convention does not name the engine" "$why" +grep -Eq '^ package +-DBARE_APP=1 +\[bare\]' <<<"$why" || fail "the package's define does not name the package" "$why" +grep -Eq '^ row +-m elf_x86_64' <<<"$why" || fail "the link's emulation does not name the row" "$why" +echo " ok why flags names the layer of each word" + +json=$("$MCPP" why target --target x86_64-windows --format json 2>/dev/null) || fail "why target failed" "$json" +python3 - "$json" <<'PY' || fail "why target --format json does not state the selection" "$json" +import json, sys +t = json.loads(sys.argv[1])["data"]["target"] +assert t["request"] == "x86_64-windows", t +assert t["identity"] in ("x86_64-windows-msvc", "x86_64-windows-musl", "x86_64-windows-gnu"), t +ids = [c["identity"] for c in t["candidates"]] +assert ids[0] == "x86_64-windows-msvc", ids +assert any(c["identity"] == t["identity"] and c["provider"] == t["provider"] for c in t["candidates"]), t +PY +echo " ok why target states the request, the identity and the candidates in order" +echo "PASS: 1042 each flag names its layer and a request its selection" diff --git a/tests/e2e/1043_several_programs_and_a_dotted_name_are_packed_as_stated.sh b/tests/e2e/1043_several_programs_and_a_dotted_name_are_packed_as_stated.sh new file mode 100755 index 000000000..5f9eff268 --- /dev/null +++ b/tests/e2e/1043_several_programs_and_a_dotted_name_are_packed_as_stated.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# requires: elf unix-shell +source "$(dirname "${BASH_SOURCE[0]}")/_toolchain_env.sh" +# 1043 -- several programs are packed as stated, and a program whose name has a dot is packed +# (mcpp#911 B1, B3). +# +# A package with several programs is not packed without a word: the one named after the package, +# else the first by name, is packed, and the others are said with the two ways to choose +# otherwise, `mcpp pack ` and `mcpp pack --all-programs`. A manifest `mcpp pack` +# generates quotes a target name with a dot (`COMMAND.COM`), so the generated project parses. +# +# Criteria: with none named after the package the first by name is packed and the others are said; +# `--all-programs` ships both under bin/; a single-file +# pack of `COMMAND.COM` builds its launcher and the file runs with the program's exit status. +set -e +t=$(mktemp -d); trap 'rm -rf "$t"' EXIT +fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | tail -20; exit 1; } +mkdir -p "$t/src" +cat > "$t/mcpp.toml" <<'EOF' +[package] +name = "multi" +version = "0.1.0" + +[targets.alpha] +kind = "bin" +main = "src/alpha.cpp" + +[targets."COMMAND.COM"] +kind = "bin" +main = "src/command.cpp" +EOF +echo 'int main() { return 0; }' > "$t/src/alpha.cpp" +echo 'int main() { return 3; }' > "$t/src/command.cpp" +cd "$t" + +out=$("$MCPP" pack 2>&1) || fail "several programs, none named after the package, were not packed" "$out" +grep -q "packing COMMAND.COM, the first by name, as none is named after the package" <<<"$out" \ + || fail "the program packed is not said" "$out" +grep -q "(alpha)" <<<"$out" && grep -q "mcpp pack --all-programs" <<<"$out" \ + || fail "the others and the ways to choose are not said" "$out" +rm -rf target/dist +echo " ok several programs, none named after the package: the first by name, the others said" + +out=$("$MCPP" pack --all-programs 2>&1) || fail "--all-programs did not pack" "$out" +listing=$(tar tzf target/dist/multi-0.1.0-*.tar.gz) +grep -q '/bin/alpha$' <<<"$listing" && grep -q '/bin/COMMAND.COM$' <<<"$listing" \ + || fail "--all-programs did not ship both programs" "$listing" +echo " ok --all-programs ships every program" + +rm -rf target/dist +out=$("$MCPP" pack COMMAND.COM --pkg-format single-file 2>&1) || fail "the single file of COMMAND.COM was not made" "$out" +file=$(find target/dist -maxdepth 1 -type f -name 'multi-0.1.0-*' | head -1) +[ -n "$file" ] || fail "no single file under target/dist" "$(ls target/dist)" +set +e; "$file"; rc=$?; set -e +[ "$rc" = 3 ] || fail "the single file exited $rc, not 3" +echo " ok a program named with a dot is packed into one file and runs" + +sed -i 's/^name = "multi"/name = "alpha"/' mcpp.toml +out=$("$MCPP" pack 2>&1) || fail "the program named after the package was not packed" "$out" +grep -q "packing alpha, the program named after the package; its other programs are not packed (COMMAND.COM)" <<<"$out" \ + || fail "the programs left out are not said" "$out" +echo " ok the program named after the package is packed and the others are said" +echo "PASS: 1043 several programs and a dotted name are packed as stated" diff --git a/tests/e2e/1044_clang_pairs_with_the_mingw_payload.sh b/tests/e2e/1044_clang_pairs_with_the_mingw_payload.sh new file mode 100755 index 000000000..e366d9291 --- /dev/null +++ b/tests/e2e/1044_clang_pairs_with_the_mingw_payload.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# requires: mingw-cross wine +source "$(dirname "${BASH_SOURCE[0]}")/_toolchain_env.sh" +# 1044 -- clang pairs with the MinGW-w64 payload (SPEC-016 §6). +# +# The `mingw` provider of `x86_64-windows-gnu` pairs with gcc and with llvm. With llvm declared, +# the payload its row names is installed as for gcc and is clang's sysroot: the C library, +# libstdc++ (whose `bits/std.cc` is the std module) and the GCC runtime; lld links. +# +# Criteria: the build reports the pairing; the compile line carries the payload as `--sysroot` +# and `-stdlib=libstdc++`; the program, which uses `import std` and `std::println`, runs under +# wine; its imports are the system's DLLs only. The llvm payload is installed by the build itself +# when this machine has none (the `--toolchain` request), so the test needs only the MinGW-w64 cross +# payload and wine. +set -e +t=$(mktemp -d); trap 'rm -rf "$t"' EXIT +fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | tail -20; exit 1; } +mkdir -p "$t/src" +cat > "$t/mcpp.toml" <<'EOF' +[package] +name = "paired" +version = "0.1.0" +EOF +cat > "$t/src/main.cpp" <<'EOF' +import std; +int main() { + std::vector words{"paired", "with", "mingw"}; + std::println("{}", words.size() == 3 ? "clang and libstdc++" : "wrong"); +} +EOF +cd "$t" + +out=$("$MCPP" build --target x86_64-windows-gnu --toolchain "llvm@${LLVM_VERSION}" 2>&1) || fail "the paired build failed" "$out" +grep -q "Pairs clang .* with the gcc@.* payload of x86_64-windows-gnu (mingw)" <<<"$out" || fail "the pairing is not reported" "$out" +ninja=$(find target/x86_64-windows-gnu -name build.ninja | head -1) +cxx=$(grep '^cxxflags' "$ninja") +grep -q -- "--sysroot=.*mingw-cross-gcc" <<<"$cxx" && grep -q -- "-stdlib=libstdc++" <<<"$cxx" \ + || fail "the compile line does not name the payload and libstdc++" "$cxx" +echo " ok clang compiles against the MinGW-w64 payload and its libstdc++" + +exe=$(find target/x86_64-windows-gnu -path '*/bin/paired.exe' -type f | head -1) +[ -n "$exe" ] || fail "no paired.exe" +run=$(wine "$exe" 2>/dev/null | tr -d '\r') || fail "the program did not run under wine" "$run" +[ "$run" = "clang and libstdc++" ] || fail "the program printed '$run'" +dlls=$("$_e2e_registry_base/xim-x-llvm/${LLVM_VERSION}/bin/llvm-objdump" -p "$exe") +if grep 'DLL Name' <<<"$dlls" | grep -viqE 'kernel32|msvcrt|api-ms-win|ucrtbase'; then + fail "the program needs a DLL of the payload" "$(grep 'DLL Name' <<<"$dlls")" +fi +echo " ok import std and std::println run under wine, with the system's DLLs only" +echo "PASS: 1044 clang pairs with the mingw payload" diff --git a/tests/e2e/1045_a_required_capability_takes_its_default_provider.sh b/tests/e2e/1045_a_required_capability_takes_its_default_provider.sh new file mode 100755 index 000000000..4ffb9c00f --- /dev/null +++ b/tests/e2e/1045_a_required_capability_takes_its_default_provider.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# requires: elf +source "$(dirname "${BASH_SOURCE[0]}")/_toolchain_env.sh" +# 1045 -- a required capability takes its package's default provider (mcpp#911 G7; SPEC-002 §3.6). +# +# A package requires a capability (`requires = ["acme:greeter"]`) and states its default providers +# per target (`[defaults."acme:greeter"] linux = ..., "*" = ...`). With no provider in the graph, +# the default for the target is the package's dependency; a selector that names the target wins +# over `*`. A provider the consumer brings is used instead, and no default is added. +# +# Criteria: the build reports the default; the program returns the default's value (1, Linux); +# with the consumer depending on the other provider it returns that one's (2) and no default is +# reported. +set -e +t=$(mktemp -d); trap 'rm -rf "$t"' EXIT +fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | tail -20; exit 1; } +mkdir -p "$t/greet-a/src" "$t/greet-b/src" "$t/lib/src" "$t/app/src" +for p in a b; do +cat > "$t/greet-$p/mcpp.toml" < "$t/greet-a/src/a.cpp" +echo 'extern "C" int greet() { return 2; }' > "$t/greet-b/src/b.cpp" +cat > "$t/lib/mcpp.toml" <<'EOF' +[package] +name = "lib" +version = "0.1.0" +requires = ["acme:greeter"] + +[targets.lib] +kind = "lib" + +[defaults."acme:greeter"] +linux = { path = "../greet-a" } +"*" = { path = "../greet-b" } +EOF +echo 'extern "C" int greet(); int lib_greet() { return greet(); }' > "$t/lib/src/lib.cpp" +cat > "$t/app/mcpp.toml" <<'EOF' +[package] +name = "app" +version = "0.1.0" + +[dependencies] +lib = { path = "../lib" } +EOF +echo 'int lib_greet(); int main() { return lib_greet(); }' > "$t/app/src/main.cpp" +cd "$t/app" + +out=$("$MCPP" build 2>&1) || fail "the build failed" "$out" +grep -q "Default .*greet-a for acme:greeter of lib (linux)" <<<"$out" || fail "the default is not reported" "$out" +exe=$(find target -path '*/bin/app' -type f | head -1) +set +e; "$exe"; rc=$?; set -e +[ "$rc" = 1 ] || fail "the program returned $rc, not the Linux default's 1" +echo " ok the target's default provides the capability, over \`*\`" + +printf 'greet-b = { path = "../greet-b" }\n' >> mcpp.toml +out=$("$MCPP" build 2>&1) || fail "the build with the consumer's provider failed" "$out" +grep -q "Default " <<<"$out" && fail "a default was added though the graph provides the capability" "$out" +set +e; "$exe"; rc=$?; set -e +[ "$rc" = 2 ] || fail "the program returned $rc, not the consumer's provider's 2" +echo " ok the consumer's provider is used, and no default is added" +echo "PASS: 1045 a required capability takes its default provider" diff --git a/tests/e2e/1046_a_patch_made_with_git_diff_from_the_package_root_is_applied.sh b/tests/e2e/1046_a_patch_made_with_git_diff_from_the_package_root_is_applied.sh new file mode 100755 index 000000000..019f5a1b1 --- /dev/null +++ b/tests/e2e/1046_a_patch_made_with_git_diff_from_the_package_root_is_applied.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# requires: elf gcc +source "$(dirname "${BASH_SOURCE[0]}")/_toolchain_env.sh" +# 1046 -- a patch made with `git diff` from the package's root is applied (mcpp#911 G8; docs/05). +# +# A patch names files from the package's root, the directory of its resolved manifest, which +# `mcpp why deps` reports. A copy of that tree under git, edited, gives the patch with +# `git diff`; the edge's `patches` lists it, and the build reads the patched variant, whose root +# `mcpp why deps` then reports. +# +# Criteria: `mcpp why deps` reports the dependency's root; the patch git writes from a copy of it +# applies; the variant holds the change and is the root reported afterwards. +set -e +t=$(mktemp -d); trap 'rm -rf "$t"' EXIT +fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | tail -20; exit 1; } +mkdir -p "$t/app/src" +cd "$t/app" +cat > mcpp.toml <<'EOF' +[package] +name = "app" +version = "0.1.0" + +[dependencies] +mcpplibs.cmdline = "0.0.1" +EOF +echo 'int main() { return 0; }' > src/main.cpp + +out=$("$MCPP" why deps 2>&1) || fail "why deps failed" "$out" +root=$(sed -n '/^ mcpplibs.cmdline[@+]/,/^ [^ ]/s/^ root: //p' <<<"$out" | head -1) +[ -n "$root" ] && [ -d "$root" ] || fail "the dependency's root is not reported" "$out" +echo " ok why deps reports the root" + +cp -R "$root" "$t/cmdline" +file=$(cd "$t/cmdline" && find . -name parse.cppm | head -1); file=${file#./} +[ -n "$file" ] || fail "the root holds no parse.cppm" "$(ls -R "$t/cmdline" | head)" +(cd "$t/cmdline" && git init -q && git add -A && git -c user.name=t -c user.email=t@t commit -qm base \ + && echo '// changed for the patch' >> "$file") +mkdir -p patches +(cd "$t/cmdline" && git diff) > patches/0001-change.patch +grep -q -- "--- a/$file" patches/0001-change.patch || fail "git did not name the file from the root" "$(head -3 patches/0001-change.patch)" +sed -i.bak 's|^mcpplibs.cmdline = "0.0.1"$|mcpplibs.cmdline = { version = "0.0.1", patches = ["patches/0001-change.patch"] }|' mcpp.toml + +out=$("$MCPP" build 2>&1) || fail "the patched build failed" "$out" +out=$("$MCPP" why deps 2>&1) || fail "why deps failed after the patch" "$out" +variant=$(sed -n '/^ mcpplibs.cmdline[@+]/,/^ [^ ]/s/^ root: //p' <<<"$out" | head -1) +[ -n "$variant" ] && [ "$variant" != "$root" ] || fail "the variant is not the reported root" "$out" +grep -q 'changed for the patch' "$variant/$file" || fail "the variant does not hold the change" +echo " ok the patch applies and the variant is the root the build reads" +echo "PASS: 1046 a patch made with git diff from the package root is applied" diff --git a/tests/e2e/1047_an_artifact_compiles_its_shared_sources_with_its_own_flags.sh b/tests/e2e/1047_an_artifact_compiles_its_shared_sources_with_its_own_flags.sh new file mode 100755 index 000000000..4e1c01e78 --- /dev/null +++ b/tests/e2e/1047_an_artifact_compiles_its_shared_sources_with_its_own_flags.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# requires: elf gcc +source "$(dirname "${BASH_SOURCE[0]}")/_toolchain_env.sh" +# 1047 -- an artifact compiles the shared sources it links with its own flags (mcpp#911 G9). +# +# `[targets..build]` states defines and flags for every translation unit of the package the +# artifact links: its entry and the shared implementation sources, compiled as the artifact's own +# variant under obj/_variant// where they differ. The other artifacts link the package's +# objects as before; a module interface stays one per package. +# +# Criteria: the program with the define returns the shared source's defined value (5), the other +# returns the plain one (4); a variant object exists for the varied artifact only; an +# implementation unit that imports the package's module builds in both. +set -e +t=$(mktemp -d); trap 'rm -rf "$t"' EXIT +fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | tail -20; exit 1; } +mkdir -p "$t/src" +cat > "$t/mcpp.toml" <<'EOF' +[package] +name = "shell" +version = "0.1.0" + +[targets.cmd] +kind = "bin" +main = "src/cmd.cpp" + +[targets.COMMAND] +kind = "bin" +main = "src/command.cpp" + +[targets.COMMAND.build] +defines = ["ENABLE_AUTOEXEC=1"] +EOF +cat > "$t/src/base.cppm" <<'EOF' +export module shell.base; +export int base_value() { return 4; } +EOF +cat > "$t/src/shell.cpp" <<'EOF' +import shell.base; +int autoexec() { +#ifdef ENABLE_AUTOEXEC + return base_value() + 1; +#else + return base_value(); +#endif +} +EOF +echo 'int autoexec(); int main() { return autoexec(); }' > "$t/src/cmd.cpp" +echo 'int autoexec(); int main() { return autoexec(); }' > "$t/src/command.cpp" +cd "$t" + +out=$("$MCPP" build 2>&1) || fail "the build failed" "$out" +dir=$(dirname "$(find target -path '*/bin/cmd' -type f | head -1)") +set +e; "$dir/cmd"; a=$?; "$dir/COMMAND"; b=$?; set -e +[ "$a" = 4 ] || fail "cmd returned $a, not the shared source's plain 4" +[ "$b" = 5 ] || fail "COMMAND returned $b, not 5 from its own define" +[ -n "$(find target -path '*/obj/_variant/COMMAND/*' -name 'shell.o' | head -1)" ] || fail "no variant object of COMMAND" +[ -z "$(find target -path '*/obj/_variant/cmd*' | head -1)" ] || fail "an artifact without flags of its own has a variant" +echo " ok COMMAND compiles the shared source with its define; cmd links the package's object" +echo "PASS: 1047 an artifact compiles its shared sources with its own flags" diff --git a/tests/e2e/1048_a_git_reference_follows_the_default_branch.sh b/tests/e2e/1048_a_git_reference_follows_the_default_branch.sh new file mode 100755 index 000000000..30319b0a5 --- /dev/null +++ b/tests/e2e/1048_a_git_reference_follows_the_default_branch.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +# requires: elf llvm +source "$(dirname "${BASH_SOURCE[0]}")/_toolchain_env.sh" +# 1048 -- a git reference with no tag, rev or branch follows the remote's default branch; a +# reference on the command line takes the same forms (SPEC-001 §5; SPEC-016 §8). +# +# The default branch is the one the remote announces (`git ls-remote --symref`), here `trunk`; the +# lock records it and its commit, and an offline build reads them. A dependency that names two of +# rev / tag / branch is refused. `--platform git+#tag=` selects a platform from a +# repository as a manifest's `{ git = ..., tag = ... }` does. +# +# Criteria: the build reports the default branch and runs; mcpp.lock names `#branch=trunk@`; +# an offline rebuild succeeds; two refs are refused; the platform from git builds its row. +set -e +t=$(mktemp -d); trap 'rm -rf "$t"' EXIT +fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | tail -20; exit 1; } +g() { git -c user.name=e2e -c user.email=e2e@example.invalid -c init.defaultBranch=trunk "$@"; } +mkdir -p "$t/lib/src" "$t/app/src" "$t/toy-platform" "$t/bare/src" +cat > "$t/lib/mcpp.toml" <<'EOF' +[package] +name = "gitlib" +version = "0.1.0" + +[targets.gitlib] +kind = "lib" +EOF +echo 'int gitlib_value() { return 9; }' > "$t/lib/src/lib.cpp" +(cd "$t/lib" && g init -q . && g add -A && g commit -qm init) +cat > "$t/app/mcpp.toml" < "$t/app/src/main.cpp" +cd "$t/app" + +out=$("$MCPP" build 2>&1) || fail "the build failed" "$out" +grep -q "the default branch of .* is trunk" <<<"$out" || fail "the default branch is not reported" "$out" +set +e; "$(find target -path '*/bin/app' -type f | head -1)"; rc=$?; set -e +[ "$rc" = 9 ] || fail "the program returned $rc, not 9" +grep -Eq 'source = "git\+.*#branch=trunk@[0-9a-f]{40}"' mcpp.lock || fail "the lock does not record the branch and its commit" "$(cat mcpp.lock)" +rm -rf target +out=$("$MCPP" build --offline 2>&1) || fail "the offline build did not read the lock" "$out" +echo " ok the default branch is followed, locked, and read offline" + +sed -i "s|gitlib = { git = \"$t/lib\" }|gitlib = { git = \"$t/lib\", tag = \"v1\", branch = \"trunk\" }|" mcpp.toml +out=$("$MCPP" build 2>&1 || true) +grep -q "names 2 of rev / tag / branch; a git dependency names at most one" <<<"$out" || fail "two refs were not refused" "$out" +echo " ok a dependency that names two refs is refused" + +cat > "$t/toy-platform/mcpp.toml" < "$t/bare/mcpp.toml" <<'EOF' +[package] +name = "bare" +version = "0.1.0" + +[build] +sources = ["src/*.c"] +ldflags = ["-nostdlib", "-static", "-no-pie"] + +[targets.bare] +kind = "bin" +main = "src/main.c" +EOF +printf 'void _start(void) { __asm__ volatile("mov $60, %%eax\\n mov $7, %%edi\\n syscall"); }\n' > "$t/bare/src/main.c" +cd "$t/bare" +out=$("$MCPP" build --target x86_64-toyos --platform "git+$t/toy-platform#tag=v1" 2>&1) || fail "the platform from git did not build its row" "$out" +grep -q "Platform toy-platform 0.1.0" <<<"$out" || fail "the platform is not reported" "$out" +echo " ok --platform git+#tag= selects the platform from its repository" +echo "PASS: 1048 a git reference follows the default branch" diff --git a/tests/e2e/1049_a_platform_skeleton_is_made_and_its_statement_read_as_the_engine_reads_it.sh b/tests/e2e/1049_a_platform_skeleton_is_made_and_its_statement_read_as_the_engine_reads_it.sh new file mode 100755 index 000000000..7594296b7 --- /dev/null +++ b/tests/e2e/1049_a_platform_skeleton_is_made_and_its_statement_read_as_the_engine_reads_it.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# requires: elf llvm +source "$(dirname "${BASH_SOURCE[0]}")/_toolchain_env.sh" +# 1049 -- a platform skeleton is made, and its statement is read as the engine reads it +# (SPEC-016 §5). +# +# `mcpp new --template platform` writes a platform package that defines `-` and +# the provider that serves it; `mcpp why platform` lists the identities it serves. Reading the +# platform's manifest checks the statement against the rows: a definition of a built-in row is an +# error wherever the platform is read, and a provider that serves no row is a warning of the +# platform's own build. +# +# Criteria: `mcpp why platform` lists - with its provider and priority, in the platform +# and in a project that selects it; a program builds for the row; a provider serving nothing is a +# warning of the platform's build; a redefined built-in row fails the project's build and names it. +set -e +t=$(mktemp -d); trap 'rm -rf "$t"' EXIT +fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | tail -20; exit 1; } +cd "$t" +arch=$(uname -m); [ "$arch" = arm64 ] && arch=aarch64 +out=$("$MCPP" new myos --template platform 2>&1) || fail "the skeleton was not made" "$out" +grep -q "mcpp why platform" <<<"$out" || fail "the next step is not named" "$out" +out=$(cd myos && "$MCPP" why platform 2>&1) || fail "the skeleton is not read" "$out" +grep -Eq "serves +$arch-myos +myos +priority 100" <<<"$out" || fail "the served row is not listed" "$out" +echo " ok the skeleton serves $arch-myos" + +mkdir -p app/src +cat > app/mcpp.toml < app/src/main.c +out=$(cd app && "$MCPP" build --target "$arch-myos" 2>&1) || fail "a program did not build for the row" "$out" +out=$(cd app && "$MCPP" why platform --target "$arch-myos" 2>&1) || fail "the selected platform is not read" "$out" +grep -Eq "serves +$arch-myos +myos +priority 100" <<<"$out" || fail "the project's platform is not listed" "$out" +out=$(cd app && "$MCPP" why platform --target "$arch-myos" --format json 2>&1) || fail "no JSON" "$out" +grep -Eq '"identity": *"'"$arch"'-myos"' <<<"$out" || fail "the JSON lacks the served identity" "$out" +echo " ok a program builds for the row; the project reports the platform it selects" + +cat >> myos/mcpp.toml <<'EOF' + +[[platform.provider]] +name = "nothing" +serves = ["riscv64-nosuch"] +EOF +out=$(cd myos && "$MCPP" build 2>&1) || fail "a warning ended the platform's build" "$out" +grep -q "\[\[platform.provider\]\] 'nothing' serves no row" <<<"$out" || fail "a provider serving nothing is not said" "$out" + +cat >> myos/mcpp.toml <<'EOF' + +[[platform.define]] +triple = "x86_64-linux-gnu" +EOF +set +e; out=$(cd app && "$MCPP" build --target "$arch-myos" 2>&1); rc=$?; set -e +[ "$rc" != 0 ] || fail "a redefined built-in row did not fail the build" "$out" +grep -q "'x86_64-linux-gnu' is a built-in row" <<<"$out" || fail "the redefinition is not named" "$out" +echo " ok a provider serving nothing is a warning; a redefined built-in row fails the build" +echo "PASS: 1049 a platform skeleton is made and its statement read as the engine reads it" diff --git a/tests/e2e/1050_a_test_run_is_kept_in_the_mtest_store.sh b/tests/e2e/1050_a_test_run_is_kept_in_the_mtest_store.sh new file mode 100755 index 000000000..4cfa6c801 --- /dev/null +++ b/tests/e2e/1050_a_test_run_is_kept_in_the_mtest_store.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# requires: gcc python3 +# 1050 -- a test run is kept in the mtest store's form (docs/08; mtest spec P12). +# +# `mcpp test` writes `target/mtest/runs//` -- `run.json`, a line per process in +# `programs.ndjson`, a line per case in `cases.ndjson` -- and appends the run to +# `target/mtest/index.ndjson`. `[test] store` names another directory; `"none"` keeps nothing. +# +# Criteria: a run is one record of the index with its counts; each process states its status, +# duration, start and worker; each case its program and status; a second run is a second record; +# the tests of an archive run as the build's do -- their cases reported, the run stored; +# `store = "none"` adds none; `store = ""` writes there. +set -e +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT +fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } +PY=python3; command -v python3 >/dev/null 2>&1 || PY=python + +cd "$TMP" +"$MCPP" new prog > new.log 2>&1 || fail "mcpp new" new.log +cd prog +"$MCPP" test > ../t1.log 2>&1 || fail "the first run failed" ../t1.log +[ -f target/mtest/index.ndjson ] || fail "no index" ../t1.log +"$PY" - <<'EOF' || fail "the stored run is not as stated" target/mtest/index.ndjson +import json, pathlib +store = pathlib.Path("target/mtest") +runs = [json.loads(l) for l in (store / "index.ndjson").read_text().splitlines() if l.strip()] +assert len(runs) == 1, runs +run = runs[0] +assert run["schema"] == "mtest.run/1" and run["status"] == "pass", run +assert run["counts"].get("pass", 0) >= 1, run +d = store / "runs" / run["id"] +assert json.loads((d / "run.json").read_text())["id"] == run["id"] +programs = [json.loads(l) for l in (d / "programs.ndjson").read_text().splitlines() if l.strip()] +assert programs and all(p["status"] == "pass" and p["ms"] >= 0 and p["started_ms"] >= 0 + and p["worker"] >= 0 for p in programs), programs +cases = [json.loads(l) for l in (d / "cases.ndjson").read_text().splitlines() if l.strip()] +assert cases and all(c["status"] == "pass" and c["program"] for c in cases), cases +EOF +echo " ok a run is kept with its processes and cases" + +"$MCPP" test > ../t2.log 2>&1 || fail "the second run failed" ../t2.log +[ "$(wc -l < target/mtest/index.ndjson | tr -d ' ')" = 2 ] || fail "a second run is not a second record" target/mtest/index.ndjson +"$MCPP" test --no-run --archive ../tests.tar.gz > ../a1.log 2>&1 || fail "the archive was not written" ../a1.log +"$MCPP" test --archive ../tests.tar.gz > ../a2.log 2>&1 || fail "the archive's tests failed" ../a2.log +grep -q 'test_smoke \.\.\. ok (.*1 case)' ../a2.log || fail "an archived protocol program does not report its case" ../a2.log +[ "$(wc -l < target/mtest/index.ndjson | tr -d ' ')" = 3 ] || fail "the archive's run is not stored" target/mtest/index.ndjson +echo " ok an archive's tests run as the build's: their case reported, the run stored" +printf '\n[test]\nstore = "none"\n' >> mcpp.toml +"$MCPP" test > ../t3.log 2>&1 || fail "the third run failed" ../t3.log +[ "$(wc -l < target/mtest/index.ndjson | tr -d ' ')" = 3 ] || fail "store = none kept a run" target/mtest/index.ndjson +sed -i.bak 's|^store = "none"$|store = "out/test-runs"|' mcpp.toml +"$MCPP" test > ../t4.log 2>&1 || fail "the fourth run failed" ../t4.log +[ -f out/test-runs/index.ndjson ] || fail "store = did not write there" ../t4.log +echo " ok each run is a record; store = none keeps nothing; store = writes there" +echo "PASS: 1050 a test run is kept in the mtest store" diff --git a/tests/e2e/1051_a_cross_targets_toolchain_section_is_not_the_build_programs.sh b/tests/e2e/1051_a_cross_targets_toolchain_section_is_not_the_build_programs.sh new file mode 100755 index 000000000..c36a6408c --- /dev/null +++ b/tests/e2e/1051_a_cross_targets_toolchain_section_is_not_the_build_programs.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# requires: elf llvm gcc musl +source "$(dirname "${BASH_SOURCE[0]}")/_toolchain_env.sh" +# 1051 -- a cross target's `[target.] toolchain` names the target's compiler, not the one the +# build program is compiled with. +# +# A build program is compiled and run on this machine. `[target.x86_64-linux-musl] toolchain = +# "gcc@16.1.0"` replaces the project's `[toolchain]` for the target; the build program takes the +# spec it replaced, as it takes the one a row's pin replaced (657). Measured on mcpp's own tree, +# built on Windows for x86_64-linux-musl: the build program was compiled by the musl cross gcc and +# the host could not start it (error 8, executable format error). +# +# Criteria: under `--target x86_64-linux-musl` the build program's compiler is the `[toolchain]` +# one (llvm), the target's is the section's (gcc), and the build program runs. +set -e +t=$(mktemp -d); trap 'rm -rf "$t"' EXIT +fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | tail -20; exit 1; } +[ "$(uname -m)" = x86_64 ] || { echo "SKIP: an x86_64 host"; exit 0; } +mkdir -p "$t/src" && cd "$t" +cat > mcpp.toml < build.mcpp <<'EOF' +import std; +import mcpp; +int main() { + std::ofstream(std::string(mcpp::out_dir()) + "/ran.txt") << "ran\n"; +} +EOF +echo 'int main() { return 0; }' > src/main.cpp +out=$("$MCPP" build --target x86_64-linux-musl 2>&1) || fail "the cross build failed" "$out" +grep -q 'host toolchain for build.mcpp: llvm' <<<"$out" \ + || grep -Eq 'Resolved host toolchain for build.mcpp: clang' <<<"$out" \ + || fail "the build program is not compiled by the [toolchain] compiler" "$out" +grep -q 'x86_64-linux-musl-g++' <<<"$out" || fail "the target is not compiled by the section's gcc" "$out" +find target -name ran.txt | grep -q . || fail "the build program did not run" "$(find target -maxdepth 4 | head)" +echo " ok the build program takes the [toolchain] compiler; the target takes its section's" +echo "PASS: 1051 a cross target's toolchain section is not the build program's" diff --git a/tests/e2e/1052_a_framework_that_uses_the_package_under_test_is_built_against_it.sh b/tests/e2e/1052_a_framework_that_uses_the_package_under_test_is_built_against_it.sh new file mode 100755 index 000000000..3151f4e8e --- /dev/null +++ b/tests/e2e/1052_a_framework_that_uses_the_package_under_test_is_built_against_it.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# requires: gcc +source "$(dirname "${BASH_SOURCE[0]}")/_toolchain_env.sh" +# 1052 -- a test framework that itself uses the package under test is built against that package. +# +# mcpplibs.os is tested with mtest, and mtest uses mcpplibs.os. A second copy of the package from +# the framework's own edge would provide every module of the package twice in one program; the +# package being built stands for itself in its own graph (docs/05), whatever version the edge asked +# for, and its tests reach the framework without a cycle of packages. +# +# Criteria: the root's test, written with a framework whose library imports the root's module, +# builds and passes; the root's module is compiled once; a version the edge did not ask for is said. +set -e +t=$(mktemp -d); trap 'rm -rf "$t"' EXIT +fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | tail -20; exit 1; } +mkdir -p "$t/core/src" "$t/core/tests" "$t/tf/src" +cat > "$t/core/mcpp.toml" <<'EOF' +[package] +namespace = "acme" +name = "core" +version = "0.2.0" + +[targets.core] +kind = "lib" + +[dev-dependencies] +acme.tf = { path = "../tf" } +EOF +cat > "$t/core/src/core.cppm" <<'EOF' +export module acme.core; +export int answer() { return 42; } +EOF +cat > "$t/core/tests/uses_tf.cpp" <<'EOF' +import acme.core; +import acme.tf; +int main() { return tf::check(answer() == 42) ? 0 : 1; } +EOF +cat > "$t/tf/mcpp.toml" <<'EOF' +[package] +namespace = "acme" +name = "tf" +version = "0.1.0" + +[targets.tf] +kind = "lib" + +[dependencies] +acme.core = { path = "../core", version = "0.1.0" } +EOF +cat > "$t/tf/src/tf.cppm" <<'EOF' +export module acme.tf; +import acme.core; +export namespace tf { bool check(bool held) { return held && answer() == 42; } } +EOF +cd "$t/core" +out=$("$MCPP" test 2>&1) || fail "the test of the package under its own framework failed" "$out" +grep -q 'uses_tf \.\.\. ok' <<<"$out" || fail "the test did not pass" "$out" +grep -q 'it is the package being built, 0.2.0, which stands for itself' <<<"$out" \ + || fail "the version the edge did not ask for is not said" "$out" +n=$(find target -name 'core.m.o' -o -name 'core.o' | grep -c 'acme_core\|/core' || true) +[ "$n" -le 1 ] || fail "the root's module was compiled more than once ($n)" "$(find target -name 'core*.o')" +echo " ok the framework is built against the package under test, and the test passes" +echo "PASS: 1052 a framework that uses the package under test is built against it" diff --git a/tests/e2e/1053_a_mach_o_images_c_library_is_up_before_its_initialisers.sh b/tests/e2e/1053_a_mach_o_images_c_library_is_up_before_its_initialisers.sh new file mode 100755 index 000000000..0b26c3bac --- /dev/null +++ b/tests/e2e/1053_a_mach_o_images_c_library_is_up_before_its_initialisers.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# requires: llvm unix-shell +# 1053 -- a Mach-O image's C library is up before the program's initialisers +# (2026.11.9.1). +# +# The loader of this format runs an image's initialisers before its entry, in +# the order the linker placed their objects. A C library of the graph +# (`mcpp:c-abi`: openkal-musl, in openkal-platform's hosted arrangement) is +# brought up by an initialiser of its own; the program's objects came first on +# the link line, and a global constructor that allocated -- the registration of +# an mtest case -- stopped in an allocator not yet started (mcpplibs/os#22, +# hosted macOS, EXC_BAD_ACCESS in mimalloc). The C library's objects now lead +# the link, as a C runtime's start objects do on ELF. +# +# Criteria: +# A. In the image built for `aarch64-macos-musl` over the hosted arrangement, the +# C library's initialiser (`okm_early_init`, openkal-musl's +# port/src/mach/early_init.c) precedes the program's own. +# B. On an arm64 macOS host the program runs, its global constructor +# allocating before `main`. +set -e +source "$(dirname "${BASH_SOURCE[0]}")/_toolchain_env.sh" + +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT +fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } + +mkdir -p "$TMP/app/src" +cd "$TMP/app" +printf '[package]\nname = "early"\nversion = "0.1.0"\n' > mcpp.toml +cat > src/main.cpp <<'CPP' +import std; +// A global constructor that allocates, as a test framework's registration does. +std::vector* registered = new std::vector{std::string(64, 'x')}; +int main() { + std::println("registered {}", registered->front().size()); + return 0; +} +CPP + +"$MCPP" build --target aarch64-macos-musl > build.log 2>&1 || fail "the hosted macOS build" build.log +grep -q "openkal-platform" build.log || { + echo "SKIP: aarch64-macos-musl is not served by openkal-platform's hosted arrangement here" + exit 0 +} +bin=$(find target -path '*/bin/early' -type f | head -1) +[ -n "$bin" ] || fail "no image was produced" build.log + +# A +objdump="$_e2e_registry_base/xim-x-llvm/${LLVM_VERSION}/bin/llvm-objdump" +nm="$_e2e_registry_base/xim-x-llvm/${LLVM_VERSION}/bin/llvm-nm" +[ -x "$objdump" ] && [ -x "$nm" ] || { echo "SKIP(A): no llvm@${LLVM_VERSION} payload to read the image with"; objdump=""; } +if [ -n "$objdump" ]; then + python3 - "$bin" "$objdump" "$nm" > a.log 2>&1 <<'PY' || fail "A: the order of the image's initialisers" a.log build.log +import subprocess, sys +image, objdump, nm = sys.argv[1:4] +dump = subprocess.run([objdump, "--macho", "-s", "-j", "__init_offsets", image], + capture_output=True, text=True, check=True).stdout +words = [] +for line in dump.splitlines(): + parts = line.split() + if len(parts) < 2 or not all(c in "0123456789abcdef" for c in parts[0]): + continue + for w in parts[1:5]: + if len(w) == 8 and all(c in "0123456789abcdef" for c in w): + words.append(int.from_bytes(bytes.fromhex(w), "little")) +symbols = [] +for line in subprocess.run([nm, "-n", image], capture_output=True, text=True, check=True).stdout.splitlines(): + parts = line.split() + if len(parts) == 3: + symbols.append((int(parts[0], 16), parts[2])) +def name_at(offset): + address = 0x100000000 + offset + best = None + for value, name in symbols: + if value > address: + break + best = name + return best or "?" +names = [name_at(o) for o in words] +print("initialisers:", names[:8]) +assert any("okm_early_init" in n for n in names), "the C library's initialiser is not in the image" +early = next(i for i, n in enumerate(names) if "okm_early_init" in n) +program = [i for i, n in enumerate(names) if "main.cpp" in n] +assert program, "the program's initialiser is not in the image" +assert early < program[0], f"the program's initialiser runs at {program[0]}, before the C library's at {early}" +PY + echo " ok the C library's initialiser precedes the program's" +fi + +# B +if [ "$(uname -s)-$(uname -m)" = Darwin-arm64 ]; then + "$bin" > b.log 2>&1 || fail "B: the image ended with $?" b.log + grep -q "registered 64" b.log || fail "B: the program's output" b.log + echo " ok the program runs, its constructor allocating before main" +fi + +echo "OK: a Mach-O image's C library is up before its initialisers" diff --git a/tests/e2e/284_env_segment_is_optional_everywhere.sh b/tests/e2e/284_env_segment_is_optional_everywhere.sh index 3c437bcdc..c92e0350a 100755 --- a/tests/e2e/284_env_segment_is_optional_everywhere.sh +++ b/tests/e2e/284_env_segment_is_optional_everywhere.sh @@ -92,7 +92,19 @@ check_pair() { rc=0 check_pair x86_64-linux x86_64-linux-gnu || rc=1 -check_pair x86_64-windows x86_64-windows-gnu || rc=1 +# `x86_64-windows` IS A PATTERN (SPEC-016 §4): its candidates are the MSVC, the +# musl (openkal-platform) and the MinGW identities in that order of priority, and +# the identity is the first available here -- MSVC on a Windows host that has it, +# the musl identity elsewhere. Whichever it is, it is one identity, one directory. +win=x86_64-windows-musl +case "$(uname -s)" in + MINGW*|MSYS*|CYGWIN*) + VSWHERE="/c/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe" + [ -f "$VSWHERE" ] && "$VSWHERE" -latest -products '*' \ + -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath \ + 2>/dev/null | grep -q . && win=x86_64-windows-msvc ;; +esac +check_pair x86_64-windows "$win" || rc=1 check_pair riscv64-none riscv64-none-elf || rc=1 [ "$rc" = 0 ] || exit 1 @@ -100,21 +112,12 @@ echo "OK: the env segment is optional on every platform, and declining it change # ── The Windows C-library axis has a name of its own ───────────────────────── # -# `x86_64-windows-musl` is a target mcpp names and LLVM cannot. Measured on -# llvm ${LLVM_VERSION}, handing `windows` with a `musl` environment to clang is not a -# diagnostic but an ICE inside the COFF writer: -# -# #5 llvm::MCWinCOFFStreamer::emitCGProfileEntry(...) -# -# so mcpp's canonical form and the triple passed to the compiler must be -# different strings — which they already are, either side of the arrow in the -# build report. -# -# THE ASSERTION IS THAT IT IS A KNOWN TARGET WITH A CORRECT DIAGNOSIS, NOT -# THAT IT BUILDS. This project names no dependency, so nothing supplies the -# target side and the build cannot succeed — correctly. What is under test is -# that mcpp recognises the name and says the true thing about it, rather than -# `unknown target`, which is what it said before the row existed. +# `x86_64-windows-musl` is a target mcpp names and LLVM cannot: clang is given +# `x86_64-w64-windows-gnu`, the Itanium C++ ABI the C library was compiled for, +# and mcpp's identity names the C library. Its provider is openkal-platform's +# hosted part (SPEC-016 §3), so naming it is enough to build it: the platform +# brings openkal-musl and the LLVM run-time, and pins clang -- the one family +# that emits it. A project names no dependency for it. out="$("$MCPP" build --target x86_64-windows-musl 2>&1 || true)" case "$out" in @@ -124,32 +127,17 @@ case "$out" in esac case "$out" in - *"nothing in the dependency graph"*) - echo " ok x86_64-windows-musl is known, and the refusal names the real reason" ;; + *"Platform openkal-platform"*"built-in, hosted"*) + echo " ok x86_64-windows-musl is served by openkal-platform's hosted part" ;; *) - echo "FAIL: the refusal did not name the missing supplier" - printf '%s\n' "$out" | grep -m2 -iE 'error|Target' + echo "FAIL: the build was not served by openkal-platform" + printf '%s\n' "$out" | grep -m3 -iE 'error|Target|Platform' exit 1 ;; esac -# AND THE PIN IS ASSERTED, BECAUSE THE PIN IS WHAT THE ROW IS FOR. -# -# The row names `llvm` not as a preference but because a global default of gcc -# would otherwise be carried onto a target no gcc can emit, and the error would -# then be about a missing C++ frontend in a musl-gcc payload — a true sentence -# about the wrong subject. So the toolchain line has to show the pin winning, -# and it has to show mcpp's own name being what was asked for: -# -# Resolved llvm@${LLVM_VERSION} → x86_64-windows-musl → …/xim-x-llvm/${LLVM_VERSION}/bin/clang++ -# target default for x86_64-windows-musl, replacing your gcc@16.1.0 -# -# THIS WAS WRITTEN WITH AN `*) : ;;` FALLBACK, WHICH MADE IT UNFAILABLE. -# A branch that accepts anything is not a check, and this file's whole subject -# is a name that used to be accepted by nothing. The fallback is gone; if the -# report's phrasing changes, this is meant to go red and be updated. case "$out" in - *"→ x86_64-windows-musl →"*) - echo " ok the toolchain line carries mcpp's own name" ;; + *"→ x86_64-windows-musl →"*"llvm"*|*"llvm"*"→ x86_64-windows-musl →"*) + echo " ok the toolchain line carries mcpp's own name, and clang" ;; *) echo "FAIL: the toolchain line did not carry mcpp's own name for the target" printf '%s\n' "$out" | grep -m2 -i 'resolv' @@ -157,12 +145,12 @@ case "$out" in esac case "$out" in - *"llvm@${LLVM_VERSION}"*) - echo " ok the row's pin decided the toolchain" ;; + *"Target x86_64-windows-musl → x86_64-w64-windows-gnu"*) + echo " ok the compiler is given the Itanium triple" ;; *) - echo "FAIL: the target table's pin did not decide the toolchain" - printf '%s\n' "$out" | grep -m2 -i 'resolv' + echo "FAIL: the report does not show the compiler's triple" + printf '%s\n' "$out" | grep -m2 'Target' exit 1 ;; esac -echo "OK: mcpp names a C library LLVM cannot spell, and diagnoses it honestly" +echo "OK: mcpp names a C library LLVM cannot spell, and its provider builds it" diff --git a/tests/e2e/298_overriding_a_convention_requires_replacing_it.sh b/tests/e2e/298_overriding_a_convention_requires_replacing_it.sh index f8a20b9e9..d3d4ae54e 100644 --- a/tests/e2e/298_overriding_a_convention_requires_replacing_it.sh +++ b/tests/e2e/298_overriding_a_convention_requires_replacing_it.sh @@ -82,18 +82,42 @@ fail=0 # change. hostArch="$("$MCPP" toolchain list --format json 2>/dev/null \ | jq -r '.data.host // "" | split("-")[0]' | tr -d '\r')" +# A ROW WHOSE PAYLOAD llvm PAIRS WITH IS NOT ONE. The MinGW-w64 payload is +# clang's sysroot as well as gcc's (SPEC-016 §3, e2e 1044): `x86_64-windows-gnu` +# with llvm alone is served, not overridden, and is left out here. +# +# AND A ROW IS OVERRIDDEN ONLY WHERE ITS CONVENTION IS WHAT SERVES IT HERE. Since +# 2026.11.9.1 a row is served by a provider chosen per host (SPEC-016 §4): +# `aarch64-linux-musl` takes the musl cross payload on a Linux host and +# openkal-platform, whose compiler is llvm, on a macOS host. There llvm alone is +# the row's own service, and nothing was removed. The compiler the selection +# takes with no compiler stated is that service, so a candidate counts only +# where it is not llvm (`compiler.family` names the driver's family: `clang`). +mkdir -p "$work/neutral" +printf '[package]\nname = "neutral"\nversion = "0.1.0"\n' > "$work/neutral/mcpp.toml" +served_by_its_convention() { + local fam + fam="$(cd "$work/neutral" && "$MCPP" why toolchain --target "$1" --format json 2>/dev/null \ + | jq -r '.data.compiler.family // ""' | tr -d '\r')" + [ -n "$fam" ] && [ "$fam" != llvm ] && [ "$fam" != clang ] +} pick() { - "$MCPP" toolchain list --format json 2>/dev/null \ - | jq -r --arg a "$1" '[.data.targets[] - | select(.pin | startswith("gcc")) - | select(.status != "planned") - | select($a == "" or (.target | startswith($a + "-"))) - | .target][0] // empty' | tr -d '\r' + local t + for t in $("$MCPP" toolchain list --format json 2>/dev/null \ + | jq -r --arg a "$1" '[.data.targets[] + | select(.pin | startswith("gcc")) + | select(.target != "x86_64-windows-gnu") + | select(.status != "planned") + | select($a == "" or (.target | startswith($a + "-"))) + | .target] | unique[]' | tr -d '\r'); do + served_by_its_convention "$t" && { echo "$t"; return 0; } + done + return 0 } pinned="$(pick "$hostArch")" [ -n "$pinned" ] || pinned="$(pick "")" if [ -z "$pinned" ]; then - echo "SKIP: no target row here pins a non-llvm toolchain" + echo "SKIP: no target row here is served by a non-llvm convention" exit 0 fi diff --git a/tests/e2e/299_a_request_that_named_no_c_library_resolves_to_a_row_that_exists.sh b/tests/e2e/299_a_request_that_named_no_c_library_resolves_to_a_row_that_exists.sh index a080f6cfd..92ea117f0 100755 --- a/tests/e2e/299_a_request_that_named_no_c_library_resolves_to_a_row_that_exists.sh +++ b/tests/e2e/299_a_request_that_named_no_c_library_resolves_to_a_row_that_exists.sh @@ -94,9 +94,23 @@ esac # AND THE ROW IS THE IDENTITY, NOT THE SPELLING. The output directory is the # identity. Asserting only "it did not refuse with tier-planned" would stay # green in a world where the completion picked some other row entirely. -case "$row" in - aarch64-linux-gnu|aarch64-unknown-linux-gnu) echo " ok and it resolved to the native GNU row ($row)" ;; - *) echo "FAIL: aarch64-linux resolved to '$row', not the GNU row"; exit 1 ;; +# SINCE 2026.11.9.1 THE REQUEST IS A PATTERN (SPEC-016 §4): it takes the GNU row +# where that row's provider is available -- an aarch64 Linux host -- and the +# next available row of the system elsewhere (`aarch64-linux-musl`, through the +# musl cross payload or openkal-platform), the report saying why. +hostpair="$(uname -s)-$(uname -m)" +case "$hostpair" in + Linux-aarch64) + case "$row" in + aarch64-linux-gnu|aarch64-unknown-linux-gnu) echo " ok and it resolved to the native GNU row ($row)" ;; + *) echo "FAIL: aarch64-linux resolved to '$row' on an aarch64 Linux host, not the GNU row"; exit 1 ;; + esac ;; + *) + case "$row" in + aarch64-linux-gnu|aarch64-unknown-linux-gnu|aarch64-linux-musl|aarch64-unknown-linux-musl) + echo " ok and it resolved to a row of the system available here ($row)" ;; + *) echo "FAIL: aarch64-linux resolved to '$row', no aarch64 Linux row"; exit 1 ;; + esac ;; esac case "$row" in aarch64*) ;; @@ -111,21 +125,20 @@ esac # "send every bare -linux to musl", which passes half one perfectly. hostr="$(reason_for x86_64-linux)" hostrow="$(resolved_row x86_64-linux)" -case "$hostrow" in - *-gnu) echo " ok and x86_64-linux is still gnu ($hostrow, reason '$hostr')" ;; - *) echo "FAIL: x86_64-linux resolved to '$hostrow' (reason '$hostr')"; exit 1 ;; +case "$hostpair:$hostrow" in + Linux-x86_64:*-gnu) echo " ok and x86_64-linux is still gnu on an x86_64 Linux host ($hostrow, reason '$hostr')" ;; + Linux-x86_64:*) echo "FAIL: x86_64-linux resolved to '$hostrow' on an x86_64 Linux host (reason '$hostr')"; exit 1 ;; + *:x86_64*linux*) echo " ok and x86_64-linux is a row of the system available here ($hostrow)" ;; + *) echo "FAIL: x86_64-linux resolved to '$hostrow' (reason '$hostr')"; exit 1 ;; esac # ── Half three: a written segment is a request, not a gap ──────────────── # -# An explicit segment names the same GNU identity. Host serviceability is a -# separate property and agrees with the omitted-segment query. -wr="$(reason_for aarch64-linux-gnu)" -if [ "$wr" = "$r" ] && [ "$(resolved_row aarch64-linux-gnu)" = "$row" ]; then - echo " ok and writing -gnu preserves the selected GNU row" -else - echo "FAIL: written and omitted GNU segments disagree ($wr / $r)" - exit 1 -fi +# An explicit segment names the identity whatever the host: it is never replaced +# by another row, served here or not. +case "$(resolved_row aarch64-linux-gnu)" in + aarch64-linux-gnu|aarch64-unknown-linux-gnu) echo " ok and writing -gnu keeps the GNU row" ;; + *) echo "FAIL: written -gnu resolved to '$(resolved_row aarch64-linux-gnu)'"; exit 1 ;; +esac echo "OK: a request that named no C library resolves to a row that exists" diff --git a/tests/e2e/746_macos_deployment_target_is_target_keyed_not_host_keyed.sh b/tests/e2e/746_macos_deployment_target_is_target_keyed_not_host_keyed.sh index 8cce94252..ff226d854 100755 --- a/tests/e2e/746_macos_deployment_target_is_target_keyed_not_host_keyed.sh +++ b/tests/e2e/746_macos_deployment_target_is_target_keyed_not_host_keyed.sh @@ -72,6 +72,9 @@ macos_deployment_target = "$1" [target.aarch64-macos] toolchain = "llvm@$LLVM_VERSION" +# The Apple provider by name: on a host without it, a request of the system +# would otherwise take the next available row, aarch64-macos-musl (SPEC-016 §4). +platform = "builtin:apple" EOF } diff --git a/tests/e2e/859_a_host_module_entry_is_reused_while_its_inputs_agree.sh b/tests/e2e/859_a_host_module_entry_is_reused_while_its_inputs_agree.sh index dfcf95b9e..f6338889a 100755 --- a/tests/e2e/859_a_host_module_entry_is_reused_while_its_inputs_agree.sh +++ b/tests/e2e/859_a_host_module_entry_is_reused_while_its_inputs_agree.sh @@ -117,7 +117,8 @@ rerun 3 c.log grep -q "T859 value two" c.log || fail "C: the program took the compile made before the header changed" c.log # D -rec=$(ls $HOST/*/programs.json | head -1) +rec=$(ls $HOST/*/programs.json 2>/dev/null | head -1) +[ -n "$rec" ] || fail "D: no build programs' record under $HOST" c.log sed 's/"identity": "[0-9a-f]*"/"identity": "0000000000000000"/' "$rec" > "$rec.edited" && mv "$rec.edited" "$rec" rerun 4 d.log [ "$(compiled d.log)" = 1 ] || fail "D: a record that disagrees was used without compiling" d.log diff --git a/tests/e2e/helpers/check_owned_schedule_lifetime.py b/tests/e2e/helpers/check_owned_schedule_lifetime.py index 9652014e7..e2cf88f21 100644 --- a/tests/e2e/helpers/check_owned_schedule_lifetime.py +++ b/tests/e2e/helpers/check_owned_schedule_lifetime.py @@ -45,7 +45,7 @@ def __init__(self, root, name, binary, kind="module", linked=False, **extra): self.out.parent.symlink_to(cache, target_is_directory=True) self.out.mkdir(parents=True) self.env = os.environ.copy() - self.env.update(GTEST_FILTER="DetachLifetime.SubprocessDriver", + self.env.update(MTEST_PROTOCOL="", MCPP_TEST_LIFETIME_ROLE="root", MCPP_TEST_LIFETIME_KIND=kind, MCPP_TEST_LIFETIME_OUTPUT=str(self.out)) self.env.update(extra) diff --git a/tests/e2e/helpers/check_shared_compiler_cap.py b/tests/e2e/helpers/check_shared_compiler_cap.py index 3cb630885..073f0d186 100644 --- a/tests/e2e/helpers/check_shared_compiler_cap.py +++ b/tests/e2e/helpers/check_shared_compiler_cap.py @@ -135,7 +135,7 @@ def update(start): env["MCPP_COMPILER_CAP"] = str(cap) # build.ninja names no cap; outside a generation it is this command = [ninja, "-j", str(cap * 6)] if owner_driver: - env.update(GTEST_FILTER="DetachLifetime.SubprocessDriver", + env.update(MTEST_PROTOCOL="", MCPP_TEST_LIFETIME_ROLE="root", MCPP_TEST_LIFETIME_OUTPUT=str(build), MCPP_TEST_LIFETIME_NINJA_JOBS=str(cap*6), MCPP_TEST_LIFETIME_CAP=str(cap)) command=[str(owner_driver)] diff --git a/tests/e2e/run_all.sh b/tests/e2e/run_all.sh index a3da4debe..16316fc5f 100755 --- a/tests/e2e/run_all.sh +++ b/tests/e2e/run_all.sh @@ -499,25 +499,34 @@ fi # the smoke and regression sets; a test the map does not know is affected). # E2E_ONLY and E2E_EXCLUDE select as below. Each result is reported as the # serial run reports it. +# The selection common to a run and its listings, in MTEST_SELECTION: this +# machine's capabilities, E2E_ONLY and E2E_EXCLUDE. +MTEST_SELECTION=() +for cap in "${KNOWN_CAPS[@]}"; do + [[ " ${CAPS[*]} " == *" $cap "* ]] || MTEST_SELECTION+=(--exclude-tag "requires:$cap") +done +[[ -n "${E2E_ONLY:-}" ]] && MTEST_SELECTION+=(--program "$E2E_ONLY") +[[ -n "${E2E_EXCLUDE:-}" ]] && MTEST_SELECTION+=(--exclude-program "$E2E_EXCLUDE") run_by_mtest() { - local args=(run --jobs "${E2E_JOBS:-1}" --timeout "${E2E_TEST_TIMEOUT}s" --report-json "$1") - local cap - for cap in "${KNOWN_CAPS[@]}"; do - [[ " ${CAPS[*]} " == *" $cap "* ]] || args+=(--exclude-tag "requires:$cap") - done + local args=(run --jobs "${E2E_JOBS:-1}" --timeout "${E2E_TEST_TIMEOUT}s" --report-json "$1" + ${MTEST_SELECTION[@]+"${MTEST_SELECTION[@]}"}) (( SHARD_TOTAL > 1 )) && args+=(--shard "$SHARD_IDX/$SHARD_TOTAL") [[ -n "${E2E_SET:-}" ]] && args+=(--set "$E2E_SET") # E2E_COVERAGE: the run measures the coverage map ([coverage] of mtest.toml). [[ -n "${E2E_COVERAGE:-}" ]] && args+=(--coverage) - [[ -n "${E2E_ONLY:-}" ]] && args+=(--program "$E2E_ONLY") - [[ -n "${E2E_EXCLUDE:-}" ]] && args+=(--exclude-program "$E2E_EXCLUDE") (cd "$HERE" && MCPP="$MCPP" "$E2E_MTEST" "${args[@]}") } +# The programs of a listing, one per line; the arguments narrow it. +mtest_programs() { + (cd "$HERE" && MCPP="$MCPP" "$E2E_MTEST" list --json ${MTEST_SELECTION[@]+"${MTEST_SELECTION[@]}"} "$@") \ + | "$py" -c 'import json, sys +for u in json.load(sys.stdin).get("units", []): print(u["program"])' | tr -d '\r' +} if [[ -n "${E2E_MTEST:-}" ]]; then + py=python3; command -v python3 >/dev/null 2>&1 || py=python record="$(mktemp)" run_by_mtest "$record" || true # One ` ` line per case of the run's record. - py=python3; command -v python3 >/dev/null 2>&1 || py=python results="$("$py" - "$record" <<'PY' import json, sys try: @@ -541,9 +550,28 @@ PY *) ((FAIL++)); FAILED_TESTS+=("$name"); report fail "$name" "$ms" "failed" ;; esac done <<<"$results" + # A SET LEAVES OUT WHAT THE CHANGE DOES NOT AFFECT, AND THE REPORT SAYS SO. + # The coverage check (rule R5) reads a test no report ran as uncovered; one + # the set left out is `unaffected`: the full runs of main cover it. Every + # shard lists the same programs, the shard being the set's own division. + if [[ -n "${E2E_SET:-}" && -n "${E2E_REPORT:-}" ]]; then + inSet=" $(mtest_programs --set "$E2E_SET" | tr '\n' ' ') " + while read -r name; do + [[ -n "$name" && "$inSet" != *" $name "* ]] || continue + report unaffected "$name" 0 "not in the set '$E2E_SET'" + done < <(mtest_programs) + fi + # A run the set gave nothing ran nothing; any other empty run is mtest's + # failure. if [[ $((PASS + FAIL)) -eq 0 ]]; then - echo "FATAL: mtest reported no case (E2E_MTEST=$E2E_MTEST)" - exit 1 + part=(); (( SHARD_TOTAL > 1 )) && part=(--shard "$SHARD_IDX/$SHARD_TOTAL") + if [[ -n "${E2E_SET:-}" ]] \ + && [[ -z "$(mtest_programs --set "$E2E_SET" ${part[@]+"${part[@]}"})" ]]; then + echo "The set '$E2E_SET' gives this run no test." + else + echo "FATAL: mtest reported no case (E2E_MTEST=$E2E_MTEST)" + exit 1 + fi fi fi diff --git a/tests/fixtures/target-rows.snapshot b/tests/fixtures/target-rows.snapshot new file mode 100644 index 000000000..afd0ade56 --- /dev/null +++ b/tests/fixtures/target-rows.snapshot @@ -0,0 +1,826 @@ +row x86_64-linux-gnu + table tier=verified note= pin= sysroot= static=0 platform= + str x86_64-linux-gnu + llvm x86_64-unknown-linux-gnu + llvm@15.0 x86_64-unknown-linux-gnu + format ELF + family unix + pe/macho/wasm 000 + apple/ios/sim/android 0000 + freestanding/openkal/own-sysroot/capability 0000 + musl/msvc/windows-gnu/x86_32 0000 + msvc-arch x64 + nasm elf64 + naming exe= lib=lib static=.a shared=.so import=0 sharedPrefix=- + application exe + android-abi x86_64 + platform-name linux + descriptor-keys linux + sysroot +row x86_64-linux-musl + table tier=verified note= pin=gcc@16.1.0 sysroot= static=1 platform= + str x86_64-linux-musl + llvm x86_64-unknown-linux-musl + llvm@15.0 x86_64-unknown-linux-musl + format ELF + family unix + pe/macho/wasm 000 + apple/ios/sim/android 0000 + freestanding/openkal/own-sysroot/capability 0000 + musl/msvc/windows-gnu/x86_32 1000 + msvc-arch x64 + nasm elf64 + naming exe= lib=lib static=.a shared=.so import=0 sharedPrefix=- + application exe + android-abi x86_64 + platform-name linux + descriptor-keys linux + sysroot +row aarch64-linux-gnu + table tier=preview note= pin=llvm@23.1.3 sysroot= static=0 platform= + str aarch64-linux-gnu + llvm aarch64-unknown-linux-gnu + llvm@15.0 aarch64-unknown-linux-gnu + format ELF + family unix + pe/macho/wasm 000 + apple/ios/sim/android 0000 + freestanding/openkal/own-sysroot/capability 0000 + musl/msvc/windows-gnu/x86_32 0000 + msvc-arch arm64 + nasm - + naming exe= lib=lib static=.a shared=.so import=0 sharedPrefix=- + application exe + android-abi arm64-v8a + platform-name linux + descriptor-keys linux + sysroot +row aarch64-linux-musl + table tier=verified note= pin=gcc@16.1.0 sysroot= static=1 platform= + str aarch64-linux-musl + llvm aarch64-unknown-linux-musl + llvm@15.0 aarch64-unknown-linux-musl + format ELF + family unix + pe/macho/wasm 000 + apple/ios/sim/android 0000 + freestanding/openkal/own-sysroot/capability 0000 + musl/msvc/windows-gnu/x86_32 1000 + msvc-arch arm64 + nasm - + naming exe= lib=lib static=.a shared=.so import=0 sharedPrefix=- + application exe + android-abi arm64-v8a + platform-name linux + descriptor-keys linux + sysroot +row x86_64-windows-gnu + table tier=verified note=PE pin=gcc@16.1.0 sysroot= static=1 platform= + str x86_64-windows-gnu + llvm x86_64-w64-windows-gnu + llvm@15.0 x86_64-w64-windows-gnu + format PE + family windows + pe/macho/wasm 100 + apple/ios/sim/android 0000 + freestanding/openkal/own-sysroot/capability 0000 + musl/msvc/windows-gnu/x86_32 0010 + msvc-arch x64 + nasm win64 + naming exe=.exe lib=lib static=.a shared=.dll import=1 sharedPrefix=- + application exe + android-abi x86_64 + platform-name windows + descriptor-keys windows + sysroot +row x86_64-windows-musl + table tier=preview note=PE pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.24.4 + str x86_64-windows-musl + llvm x86_64-w64-windows-gnu + llvm@15.0 x86_64-w64-windows-gnu + format PE + family windows + pe/macho/wasm 100 + apple/ios/sim/android 0000 + freestanding/openkal/own-sysroot/capability 0001 + musl/msvc/windows-gnu/x86_32 1000 + msvc-arch x64 + nasm win64 + naming exe=.exe lib=lib static=.a shared=.dll import=1 sharedPrefix=- + application exe + android-abi x86_64 + platform-name windows + descriptor-keys windows + sysroot +row x86_64-windows-msvc + table tier=verified note=PE pin= sysroot= static=0 platform= + str x86_64-windows-msvc + llvm x86_64-pc-windows-msvc + llvm@15.0 x86_64-pc-windows-msvc + format PE + family windows + pe/macho/wasm 100 + apple/ios/sim/android 0000 + freestanding/openkal/own-sysroot/capability 0000 + musl/msvc/windows-gnu/x86_32 0100 + msvc-arch x64 + nasm win64 + naming exe=.exe lib= static=.lib shared=.dll import=1 sharedPrefix=- + application exe + android-abi x86_64 + platform-name windows + descriptor-keys windows + sysroot +row aarch64-macos + table tier=verified note= pin= sysroot= static=0 platform= + str aarch64-macos + llvm arm64-apple-macos14.0 + llvm@15.0 arm64-apple-macos15.0 + format Mach-O + family unix + pe/macho/wasm 010 + apple/ios/sim/android 1000 + freestanding/openkal/own-sysroot/capability 0000 + musl/msvc/windows-gnu/x86_32 0000 + msvc-arch arm64 + nasm - + naming exe= lib=lib static=.a shared=.dylib import=0 sharedPrefix=- + application exe + android-abi arm64-v8a + platform-name macos + descriptor-keys macosx + sysroot +row aarch64-macos-musl + table tier=preview note= pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.24.4 + str aarch64-macos-musl + llvm arm64-apple-macos14.0 + llvm@15.0 arm64-apple-macos15.0 + format Mach-O + family unix + pe/macho/wasm 010 + apple/ios/sim/android 1000 + freestanding/openkal/own-sysroot/capability 0001 + musl/msvc/windows-gnu/x86_32 1000 + msvc-arch arm64 + nasm - + naming exe= lib=lib static=.a shared=.dylib import=0 sharedPrefix=- + application exe + android-abi arm64-v8a + platform-name macos + descriptor-keys macosx + sysroot +row riscv64-linux-musl + table tier=planned note= pin= sysroot= static=1 platform= + str riscv64-linux-musl + llvm riscv64-unknown-linux-musl + llvm@15.0 riscv64-unknown-linux-musl + format ELF + family unix + pe/macho/wasm 000 + apple/ios/sim/android 0000 + freestanding/openkal/own-sysroot/capability 0000 + musl/msvc/windows-gnu/x86_32 1000 + msvc-arch + nasm - + naming exe= lib=lib static=.a shared=.so import=0 sharedPrefix=- + application exe + android-abi riscv64 + platform-name linux + descriptor-keys linux + sysroot +row x86_64-macos + table tier=planned note= pin= sysroot= static=0 platform= + str x86_64-macos + llvm x86_64-apple-macos14.0 + llvm@15.0 x86_64-apple-macos15.0 + format Mach-O + family unix + pe/macho/wasm 010 + apple/ios/sim/android 1000 + freestanding/openkal/own-sysroot/capability 0000 + musl/msvc/windows-gnu/x86_32 0000 + msvc-arch x64 + nasm macho64 + naming exe= lib=lib static=.a shared=.dylib import=0 sharedPrefix=- + application exe + android-abi x86_64 + platform-name macos + descriptor-keys macosx + sysroot +row x86_64-macos-musl + table tier=preview note= pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.24.4 + str x86_64-macos-musl + llvm x86_64-apple-macos14.0 + llvm@15.0 x86_64-apple-macos15.0 + format Mach-O + family unix + pe/macho/wasm 010 + apple/ios/sim/android 1000 + freestanding/openkal/own-sysroot/capability 0001 + musl/msvc/windows-gnu/x86_32 1000 + msvc-arch x64 + nasm macho64 + naming exe= lib=lib static=.a shared=.dylib import=0 sharedPrefix=- + application exe + android-abi x86_64 + platform-name macos + descriptor-keys macosx + sysroot +row riscv64-none-elf + table tier=verified note=bare pin=llvm@23.1.3 sysroot=xim:picolibc-riscv@1.8.12 static=1 platform= + str riscv64-none-elf + llvm riscv64-none-elf + llvm@15.0 riscv64-none-elf + format ELF + family + pe/macho/wasm 000 + apple/ios/sim/android 0000 + freestanding/openkal/own-sysroot/capability 1001 + musl/msvc/windows-gnu/x86_32 0000 + msvc-arch + nasm - + naming exe=.host lib=hostlib static=.hosta shared=.hostso import=0 sharedPrefix=- + application exe + android-abi riscv64 + platform-name none + descriptor-keys none + sysroot xim:picolibc-riscv@1.8.12 + fs.spec march=rv64gc mabi=lp64d mcmodel=medany libdir=rv64gc/lp64d lld= + fs.compile -march=rv64gc -mabi=lp64d -mcmodel=medany -ffunction-sections -fdata-sections -ffreestanding -nostdinc++ -fno-exceptions -fno-rtti + fs.compile+rt -march=rv64gc -mabi=lp64d -mcmodel=medany -ffunction-sections -fdata-sections -fasynchronous-unwind-tables -nostdinc++ + fs.link --target=riscv64-none-elf -march=rv64gc -mabi=lp64d -mcmodel=medany -ffunction-sections -fdata-sections -ffreestanding -nostdinc++ -fno-exceptions -fno-rtti -nostdlib -nostartfiles -static -Wl,--gc-sections -fuse-ld=/lld -L/sysroot/lib -T /script.ld +row riscv32-none-elf + table tier=verified note=bare pin=llvm@23.1.3 sysroot=xim:picolibc-riscv@1.8.12 static=1 platform= + str riscv32-none-elf + llvm riscv32-none-elf + llvm@15.0 riscv32-none-elf + format ELF + family + pe/macho/wasm 000 + apple/ios/sim/android 0000 + freestanding/openkal/own-sysroot/capability 1001 + musl/msvc/windows-gnu/x86_32 0000 + msvc-arch + nasm - + naming exe=.host lib=hostlib static=.hosta shared=.hostso import=0 sharedPrefix=- + application exe + android-abi riscv32 + platform-name none + descriptor-keys none + sysroot xim:picolibc-riscv@1.8.12 + fs.spec march=rv32imac mabi=ilp32 mcmodel=medany libdir=rv32imac/ilp32 lld= + fs.compile -march=rv32imac -mabi=ilp32 -mcmodel=medany -ffunction-sections -fdata-sections -ffreestanding -nostdinc++ -fno-exceptions -fno-rtti + fs.compile+rt -march=rv32imac -mabi=ilp32 -mcmodel=medany -ffunction-sections -fdata-sections -fasynchronous-unwind-tables -nostdinc++ + fs.link --target=riscv32-none-elf -march=rv32imac -mabi=ilp32 -mcmodel=medany -ffunction-sections -fdata-sections -ffreestanding -nostdinc++ -fno-exceptions -fno-rtti -nostdlib -nostartfiles -static -Wl,--gc-sections -fuse-ld=/lld -L/sysroot/lib -T /script.ld +row aarch64-none-elf + table tier=preview note=bare pin=llvm@23.1.3 sysroot= static=1 platform= + str aarch64-none-elf + llvm aarch64-none-elf + llvm@15.0 aarch64-none-elf + format ELF + family + pe/macho/wasm 000 + apple/ios/sim/android 0000 + freestanding/openkal/own-sysroot/capability 1001 + musl/msvc/windows-gnu/x86_32 0000 + msvc-arch arm64 + nasm - + naming exe=.host lib=hostlib static=.hosta shared=.hostso import=0 sharedPrefix=- + application exe + android-abi arm64-v8a + platform-name none + descriptor-keys none + sysroot + fs.spec march=armv8-a mabi=aapcs mcmodel=small libdir=armv8-a/aapcs lld= + fs.compile -march=armv8-a -mabi=aapcs -mcmodel=small -ffunction-sections -fdata-sections -ffreestanding -nostdinc++ -fno-exceptions -fno-rtti + fs.compile+rt -march=armv8-a -mabi=aapcs -mcmodel=small -ffunction-sections -fdata-sections -fasynchronous-unwind-tables -nostdinc++ + fs.link --target=aarch64-none-elf -march=armv8-a -mabi=aapcs -mcmodel=small -ffunction-sections -fdata-sections -ffreestanding -nostdinc++ -fno-exceptions -fno-rtti -nostdlib -nostartfiles -static -Wl,--gc-sections -fuse-ld=/lld -L/sysroot/lib -T /script.ld +row x86_64-none-elf + table tier=preview note=bare pin=llvm@23.1.3 sysroot= static=1 platform= + str x86_64-none-elf + llvm x86_64-none-elf + llvm@15.0 x86_64-none-elf + format ELF + family + pe/macho/wasm 000 + apple/ios/sim/android 0000 + freestanding/openkal/own-sysroot/capability 1001 + musl/msvc/windows-gnu/x86_32 0000 + msvc-arch x64 + nasm - + naming exe=.host lib=hostlib static=.hosta shared=.hostso import=0 sharedPrefix=- + application exe + android-abi x86_64 + platform-name none + descriptor-keys none + sysroot + fs.spec march=x86-64 mabi=sysv mcmodel=small libdir=x86-64/sysv lld=elf_x86_64 + fs.compile -march=x86-64 -mabi=sysv -mcmodel=small -mno-red-zone -ffunction-sections -fdata-sections -ffreestanding -nostdinc++ -fno-exceptions -fno-rtti + fs.compile+rt -march=x86-64 -mabi=sysv -mcmodel=small -mno-red-zone -ffunction-sections -fdata-sections -fasynchronous-unwind-tables -nostdinc++ + fs.link --target=x86_64-none-elf -march=x86-64 -mabi=sysv -mcmodel=small -mno-red-zone -ffunction-sections -fdata-sections -ffreestanding -nostdinc++ -fno-exceptions -fno-rtti -nostdlib -nostartfiles -static -Wl,--gc-sections -fuse-ld=/lld -L/sysroot/lib -T /script.ld + fs.link-direct -m elf_x86_64 -Bstatic --no-dynamic-linker --gc-sections -L/sysroot/lib -T /script.ld +row thumbv6m-none-eabi + table tier=verified note=bare pin=llvm@23.1.3 sysroot= static=1 platform= + str thumbv6m-none-eabi + llvm thumbv6m-none-eabi + llvm@15.0 thumbv6m-none-eabi + format ELF + family + pe/macho/wasm 000 + apple/ios/sim/android 0000 + freestanding/openkal/own-sysroot/capability 1001 + musl/msvc/windows-gnu/x86_32 0000 + msvc-arch + nasm - + naming exe=.host lib=hostlib static=.hosta shared=.hostso import=0 sharedPrefix=- + application exe + android-abi thumbv6m + platform-name none + descriptor-keys none + sysroot + fs.spec march=armv6-m mabi=aapcs mcmodel= libdir=thumbv6m-none-eabi lld= + fs.compile -march=armv6-m -mabi=aapcs -mfpu=none -ffunction-sections -fdata-sections -ffreestanding -nostdinc++ -fno-exceptions -fno-rtti + fs.compile+rt -march=armv6-m -mabi=aapcs -mfpu=none -ffunction-sections -fdata-sections -fasynchronous-unwind-tables -nostdinc++ + fs.link --target=thumbv6m-none-eabi -march=armv6-m -mabi=aapcs -mfpu=none -ffunction-sections -fdata-sections -ffreestanding -nostdinc++ -fno-exceptions -fno-rtti -nostdlib -nostartfiles -static -Wl,--gc-sections -fuse-ld=/lld -L/sysroot/lib -T /script.ld +row thumbv7m-none-eabi + table tier=verified note=bare pin=llvm@23.1.3 sysroot= static=1 platform= + str thumbv7m-none-eabi + llvm thumbv7m-none-eabi + llvm@15.0 thumbv7m-none-eabi + format ELF + family + pe/macho/wasm 000 + apple/ios/sim/android 0000 + freestanding/openkal/own-sysroot/capability 1001 + musl/msvc/windows-gnu/x86_32 0000 + msvc-arch + nasm - + naming exe=.host lib=hostlib static=.hosta shared=.hostso import=0 sharedPrefix=- + application exe + android-abi thumbv7m + platform-name none + descriptor-keys none + sysroot + fs.spec march=armv7-m mabi=aapcs mcmodel= libdir=thumbv7m-none-eabi lld= + fs.compile -march=armv7-m -mabi=aapcs -mfpu=none -ffunction-sections -fdata-sections -ffreestanding -nostdinc++ -fno-exceptions -fno-rtti + fs.compile+rt -march=armv7-m -mabi=aapcs -mfpu=none -ffunction-sections -fdata-sections -fasynchronous-unwind-tables -nostdinc++ + fs.link --target=thumbv7m-none-eabi -march=armv7-m -mabi=aapcs -mfpu=none -ffunction-sections -fdata-sections -ffreestanding -nostdinc++ -fno-exceptions -fno-rtti -nostdlib -nostartfiles -static -Wl,--gc-sections -fuse-ld=/lld -L/sysroot/lib -T /script.ld +row thumbv7em-none-eabi + table tier=preview note=bare pin=llvm@23.1.3 sysroot= static=1 platform= + str thumbv7em-none-eabi + llvm thumbv7em-none-eabi + llvm@15.0 thumbv7em-none-eabi + format ELF + family + pe/macho/wasm 000 + apple/ios/sim/android 0000 + freestanding/openkal/own-sysroot/capability 1001 + musl/msvc/windows-gnu/x86_32 0000 + msvc-arch + nasm - + naming exe=.host lib=hostlib static=.hosta shared=.hostso import=0 sharedPrefix=- + application exe + android-abi thumbv7em + platform-name none + descriptor-keys none + sysroot + fs.spec march=armv7e-m mabi=aapcs mcmodel= libdir=thumbv7em-none-eabi lld= + fs.compile -march=armv7e-m -mabi=aapcs -mfpu=none -ffunction-sections -fdata-sections -ffreestanding -nostdinc++ -fno-exceptions -fno-rtti + fs.compile+rt -march=armv7e-m -mabi=aapcs -mfpu=none -ffunction-sections -fdata-sections -fasynchronous-unwind-tables -nostdinc++ + fs.link --target=thumbv7em-none-eabi -march=armv7e-m -mabi=aapcs -mfpu=none -ffunction-sections -fdata-sections -ffreestanding -nostdinc++ -fno-exceptions -fno-rtti -nostdlib -nostartfiles -static -Wl,--gc-sections -fuse-ld=/lld -L/sysroot/lib -T /script.ld +row thumbv7em-none-eabihf + table tier=verified note=bare pin=llvm@23.1.3 sysroot= static=1 platform= + str thumbv7em-none-eabihf + llvm thumbv7em-none-eabihf + llvm@15.0 thumbv7em-none-eabihf + format ELF + family + pe/macho/wasm 000 + apple/ios/sim/android 0000 + freestanding/openkal/own-sysroot/capability 1001 + musl/msvc/windows-gnu/x86_32 0000 + msvc-arch + nasm - + naming exe=.host lib=hostlib static=.hosta shared=.hostso import=0 sharedPrefix=- + application exe + android-abi thumbv7em + platform-name none + descriptor-keys none + sysroot + fs.spec march=armv7e-m mabi=aapcs mcmodel= libdir=thumbv7em-none-eabihf lld= + fs.compile -march=armv7e-m -mabi=aapcs -ffunction-sections -fdata-sections -ffreestanding -nostdinc++ -fno-exceptions -fno-rtti + fs.compile+rt -march=armv7e-m -mabi=aapcs -ffunction-sections -fdata-sections -fasynchronous-unwind-tables -nostdinc++ + fs.link --target=thumbv7em-none-eabihf -march=armv7e-m -mabi=aapcs -ffunction-sections -fdata-sections -ffreestanding -nostdinc++ -fno-exceptions -fno-rtti -nostdlib -nostartfiles -static -Wl,--gc-sections -fuse-ld=/lld -L/sysroot/lib -T /script.ld +row thumbv8m.base-none-eabi + table tier=preview note=bare pin=llvm@23.1.3 sysroot= static=1 platform= + str thumbv8m.base-none-eabi + llvm thumbv8m.base-none-eabi + llvm@15.0 thumbv8m.base-none-eabi + format ELF + family + pe/macho/wasm 000 + apple/ios/sim/android 0000 + freestanding/openkal/own-sysroot/capability 1001 + musl/msvc/windows-gnu/x86_32 0000 + msvc-arch + nasm - + naming exe=.host lib=hostlib static=.hosta shared=.hostso import=0 sharedPrefix=- + application exe + android-abi thumbv8m.base + platform-name none + descriptor-keys none + sysroot + fs.spec march=armv8-m.base mabi=aapcs mcmodel= libdir=thumbv8m.base-none-eabi lld= + fs.compile -march=armv8-m.base -mabi=aapcs -mfpu=none -ffunction-sections -fdata-sections -ffreestanding -nostdinc++ -fno-exceptions -fno-rtti + fs.compile+rt -march=armv8-m.base -mabi=aapcs -mfpu=none -ffunction-sections -fdata-sections -fasynchronous-unwind-tables -nostdinc++ + fs.link --target=thumbv8m.base-none-eabi -march=armv8-m.base -mabi=aapcs -mfpu=none -ffunction-sections -fdata-sections -ffreestanding -nostdinc++ -fno-exceptions -fno-rtti -nostdlib -nostartfiles -static -Wl,--gc-sections -fuse-ld=/lld -L/sysroot/lib -T /script.ld +row thumbv8m.main-none-eabi + table tier=verified note=bare pin=llvm@23.1.3 sysroot= static=1 platform= + str thumbv8m.main-none-eabi + llvm thumbv8m.main-none-eabi + llvm@15.0 thumbv8m.main-none-eabi + format ELF + family + pe/macho/wasm 000 + apple/ios/sim/android 0000 + freestanding/openkal/own-sysroot/capability 1001 + musl/msvc/windows-gnu/x86_32 0000 + msvc-arch + nasm - + naming exe=.host lib=hostlib static=.hosta shared=.hostso import=0 sharedPrefix=- + application exe + android-abi thumbv8m.main + platform-name none + descriptor-keys none + sysroot + fs.spec march=armv8-m.main mabi=aapcs mcmodel= libdir=thumbv8m.main-none-eabi lld= + fs.compile -march=armv8-m.main -mabi=aapcs -mfpu=none -ffunction-sections -fdata-sections -ffreestanding -nostdinc++ -fno-exceptions -fno-rtti + fs.compile+rt -march=armv8-m.main -mabi=aapcs -mfpu=none -ffunction-sections -fdata-sections -fasynchronous-unwind-tables -nostdinc++ + fs.link --target=thumbv8m.main-none-eabi -march=armv8-m.main -mabi=aapcs -mfpu=none -ffunction-sections -fdata-sections -ffreestanding -nostdinc++ -fno-exceptions -fno-rtti -nostdlib -nostartfiles -static -Wl,--gc-sections -fuse-ld=/lld -L/sysroot/lib -T /script.ld +row thumbv8m.main-none-eabihf + table tier=preview note=bare pin=llvm@23.1.3 sysroot= static=1 platform= + str thumbv8m.main-none-eabihf + llvm thumbv8m.main-none-eabihf + llvm@15.0 thumbv8m.main-none-eabihf + format ELF + family + pe/macho/wasm 000 + apple/ios/sim/android 0000 + freestanding/openkal/own-sysroot/capability 1001 + musl/msvc/windows-gnu/x86_32 0000 + msvc-arch + nasm - + naming exe=.host lib=hostlib static=.hosta shared=.hostso import=0 sharedPrefix=- + application exe + android-abi thumbv8m.main + platform-name none + descriptor-keys none + sysroot + fs.spec march=armv8-m.main mabi=aapcs mcmodel= libdir=thumbv8m.main-none-eabihf lld= + fs.compile -march=armv8-m.main -mabi=aapcs -ffunction-sections -fdata-sections -ffreestanding -nostdinc++ -fno-exceptions -fno-rtti + fs.compile+rt -march=armv8-m.main -mabi=aapcs -ffunction-sections -fdata-sections -fasynchronous-unwind-tables -nostdinc++ + fs.link --target=thumbv8m.main-none-eabihf -march=armv8-m.main -mabi=aapcs -ffunction-sections -fdata-sections -ffreestanding -nostdinc++ -fno-exceptions -fno-rtti -nostdlib -nostartfiles -static -Wl,--gc-sections -fuse-ld=/lld -L/sysroot/lib -T /script.ld +row armv7a-none-eabi + table tier=verified note=bare pin=llvm@23.1.3 sysroot= static=1 platform= + str armv7a-none-eabi + llvm armv7a-none-eabi + llvm@15.0 armv7a-none-eabi + format ELF + family + pe/macho/wasm 000 + apple/ios/sim/android 0000 + freestanding/openkal/own-sysroot/capability 1001 + musl/msvc/windows-gnu/x86_32 0000 + msvc-arch + nasm - + naming exe=.host lib=hostlib static=.hosta shared=.hostso import=0 sharedPrefix=- + application exe + android-abi armv7a + platform-name none + descriptor-keys none + sysroot + fs.spec march=armv7-a mabi=aapcs mcmodel= libdir=armv7a-none-eabi lld= + fs.compile -march=armv7-a -mabi=aapcs -mfpu=none -ffunction-sections -fdata-sections -ffreestanding -nostdinc++ -fno-exceptions -fno-rtti + fs.compile+rt -march=armv7-a -mabi=aapcs -mfpu=none -ffunction-sections -fdata-sections -fasynchronous-unwind-tables -nostdinc++ + fs.link --target=armv7a-none-eabi -march=armv7-a -mabi=aapcs -mfpu=none -ffunction-sections -fdata-sections -ffreestanding -nostdinc++ -fno-exceptions -fno-rtti -nostdlib -nostartfiles -static -Wl,--gc-sections -fuse-ld=/lld -L/sysroot/lib -T /script.ld +row armv7a-none-eabihf + table tier=verified note=bare pin=llvm@23.1.3 sysroot= static=1 platform= + str armv7a-none-eabihf + llvm armv7a-none-eabihf + llvm@15.0 armv7a-none-eabihf + format ELF + family + pe/macho/wasm 000 + apple/ios/sim/android 0000 + freestanding/openkal/own-sysroot/capability 1001 + musl/msvc/windows-gnu/x86_32 0000 + msvc-arch + nasm - + naming exe=.host lib=hostlib static=.hosta shared=.hostso import=0 sharedPrefix=- + application exe + android-abi armv7a + platform-name none + descriptor-keys none + sysroot + fs.spec march=armv7-a mabi=aapcs mcmodel= libdir=armv7a-none-eabihf lld= + fs.compile -march=armv7-a -mabi=aapcs -ffunction-sections -fdata-sections -ffreestanding -nostdinc++ -fno-exceptions -fno-rtti + fs.compile+rt -march=armv7-a -mabi=aapcs -ffunction-sections -fdata-sections -fasynchronous-unwind-tables -nostdinc++ + fs.link --target=armv7a-none-eabihf -march=armv7-a -mabi=aapcs -ffunction-sections -fdata-sections -ffreestanding -nostdinc++ -fno-exceptions -fno-rtti -nostdlib -nostartfiles -static -Wl,--gc-sections -fuse-ld=/lld -L/sysroot/lib -T /script.ld +row i686-none-elf + table tier=preview note=bare pin=llvm@23.1.3 sysroot= static=1 platform= + str i686-none-elf + llvm i686-none-elf + llvm@15.0 i686-none-elf + format ELF + family + pe/macho/wasm 000 + apple/ios/sim/android 0000 + freestanding/openkal/own-sysroot/capability 1001 + musl/msvc/windows-gnu/x86_32 0001 + msvc-arch x86 + nasm - + naming exe=.host lib=hostlib static=.hosta shared=.hostso import=0 sharedPrefix=- + application exe + android-abi i686 + platform-name none + descriptor-keys none + sysroot + fs.spec march=i686 mabi= mcmodel= libdir= lld=elf_i386 + fs.compile -march=i686 -ffunction-sections -fdata-sections -ffreestanding -nostdinc++ -fno-exceptions -fno-rtti + fs.compile+rt -march=i686 -ffunction-sections -fdata-sections -fasynchronous-unwind-tables -nostdinc++ + fs.link --target=i686-none-elf -march=i686 -ffunction-sections -fdata-sections -ffreestanding -nostdinc++ -fno-exceptions -fno-rtti -nostdlib -nostartfiles -static -Wl,--gc-sections -fuse-ld=/lld -L/sysroot/lib -T /script.ld + fs.link-direct -m elf_i386 -Bstatic --no-dynamic-linker --gc-sections -L/sysroot/lib -T /script.ld +row aarch64-linux-android + table tier=verified note= pin=android-ndk@30.0.16248370 sysroot= static=0 platform= + str aarch64-linux-android + llvm aarch64-unknown-linux-android + llvm@15.0 aarch64-unknown-linux-android15.0 + format ELF + family unix + pe/macho/wasm 000 + apple/ios/sim/android 0001 + freestanding/openkal/own-sysroot/capability 0011 + musl/msvc/windows-gnu/x86_32 0000 + msvc-arch arm64 + nasm - + naming exe= lib=lib static=.a shared=.so import=0 sharedPrefix=- + application shared + android-abi arm64-v8a + platform-name android + descriptor-keys linux + sysroot +row x86_64-linux-android + table tier=verified note= pin=android-ndk@30.0.16248370 sysroot= static=0 platform= + str x86_64-linux-android + llvm x86_64-unknown-linux-android + llvm@15.0 x86_64-unknown-linux-android15.0 + format ELF + family unix + pe/macho/wasm 000 + apple/ios/sim/android 0001 + freestanding/openkal/own-sysroot/capability 0011 + musl/msvc/windows-gnu/x86_32 0000 + msvc-arch x64 + nasm elf64 + naming exe= lib=lib static=.a shared=.so import=0 sharedPrefix=- + application shared + android-abi x86_64 + platform-name android + descriptor-keys linux + sysroot +row aarch64-ios + table tier=preview note= pin=llvm@23.1.3 sysroot= static=0 platform= + str aarch64-ios + llvm arm64-apple-ios + llvm@15.0 arm64-apple-ios15.0 + format Mach-O + family unix + pe/macho/wasm 010 + apple/ios/sim/android 1100 + freestanding/openkal/own-sysroot/capability 0000 + musl/msvc/windows-gnu/x86_32 0000 + msvc-arch arm64 + nasm - + naming exe=.host lib=hostlib static=.hosta shared=.hostso import=0 sharedPrefix=- + application exe + android-abi arm64-v8a + platform-name ios + descriptor-keys ios macosx + sysroot +row aarch64-ios-sim + table tier=verified note= pin=llvm@23.1.3 sysroot= static=0 platform= + str aarch64-ios-sim + llvm arm64-apple-ios-simulator + llvm@15.0 arm64-apple-ios15.0-simulator + format Mach-O + family unix + pe/macho/wasm 010 + apple/ios/sim/android 1110 + freestanding/openkal/own-sysroot/capability 0000 + musl/msvc/windows-gnu/x86_32 0000 + msvc-arch arm64 + nasm - + naming exe=.host lib=hostlib static=.hosta shared=.hostso import=0 sharedPrefix=- + application exe + android-abi arm64-v8a + platform-name ios + descriptor-keys ios macosx + sysroot +row x86_64-ios-sim + table tier=verified note= pin=llvm@23.1.3 sysroot= static=0 platform= + str x86_64-ios-sim + llvm x86_64-apple-ios-simulator + llvm@15.0 x86_64-apple-ios15.0-simulator + format Mach-O + family unix + pe/macho/wasm 010 + apple/ios/sim/android 1110 + freestanding/openkal/own-sysroot/capability 0000 + musl/msvc/windows-gnu/x86_32 0000 + msvc-arch x64 + nasm - + naming exe=.host lib=hostlib static=.hosta shared=.hostso import=0 sharedPrefix=- + application exe + android-abi x86_64 + platform-name ios + descriptor-keys ios macosx + sysroot +row wasm32-emscripten + table tier=verified note=wasm pin=emsdk@6.0.9 sysroot= static=1 platform= + str wasm32-emscripten + llvm wasm32-unknown-emscripten + llvm@15.0 wasm32-unknown-emscripten + format wasm + family unix + pe/macho/wasm 001 + apple/ios/sim/android 0000 + freestanding/openkal/own-sysroot/capability 0011 + musl/msvc/windows-gnu/x86_32 0000 + msvc-arch + nasm - + naming exe=.js lib=lib static=.a shared= import=1 sharedPrefix=- + application exe + android-abi wasm32 + platform-name emscripten + descriptor-keys emscripten linux + sysroot +row x86_64-openkal + table tier=verified note=ok pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.24.4 + str x86_64-openkal + llvm x86_64-unknown-none-elf + llvm@15.0 x86_64-unknown-none-elf + format ELF + family + pe/macho/wasm 000 + apple/ios/sim/android 0000 + freestanding/openkal/own-sysroot/capability 0101 + musl/msvc/windows-gnu/x86_32 0000 + msvc-arch x64 + nasm - + naming exe=.oke lib=lib static=.a shared=.okso import=0 sharedPrefix=- + application exe + android-abi x86_64 + platform-name openkal + descriptor-keys openkal linux + sysroot +row aarch64-openkal + table tier=verified note=ok pin=llvm@23.1.3 sysroot= static=1 platform=mcpplibs.openkal-platform@0.24.4 + str aarch64-openkal + llvm aarch64-unknown-none-elf + llvm@15.0 aarch64-unknown-none-elf + format ELF + family + pe/macho/wasm 000 + apple/ios/sim/android 0000 + freestanding/openkal/own-sysroot/capability 0101 + musl/msvc/windows-gnu/x86_32 0000 + msvc-arch arm64 + nasm - + naming exe=.oke lib=lib static=.a shared=.okso import=0 sharedPrefix=- + application exe + android-abi arm64-v8a + platform-name openkal + descriptor-keys openkal linux + sysroot +request x86_64-linux + parsed x86_64-linux-gnu envExplicit=0 + on linux-x86_64 -> x86_64-linux-gnu glibc fallback=0 | x86_64-linux-gnu/glibc/100 x86_64-linux-musl/musl-gcc/100 x86_64-linux-musl/openkal/50 + on macos-aarch64 -> x86_64-linux-musl openkal fallback=1 | x86_64-linux-gnu/glibc/100/x x86_64-linux-musl/musl-gcc/100/x x86_64-linux-musl/openkal/50 + on windows-x86_64 -> x86_64-linux-musl musl-gcc fallback=1 | x86_64-linux-gnu/glibc/100/x x86_64-linux-musl/musl-gcc/100 x86_64-linux-musl/openkal/50 +request aarch64-linux + parsed aarch64-linux-gnu envExplicit=0 + on linux-x86_64 -> aarch64-linux-musl musl-gcc fallback=1 | aarch64-linux-gnu/glibc/100/x aarch64-linux-musl/musl-gcc/100 aarch64-linux-musl/openkal/50 + on macos-aarch64 -> aarch64-linux-musl openkal fallback=1 | aarch64-linux-gnu/glibc/100/x aarch64-linux-musl/musl-gcc/100/x aarch64-linux-musl/openkal/50 + on windows-x86_64 -> aarch64-linux-musl openkal fallback=1 | aarch64-linux-gnu/glibc/100/x aarch64-linux-musl/musl-gcc/100/x aarch64-linux-musl/openkal/50 +request riscv64-linux + parsed riscv64-linux-gnu envExplicit=0 + on linux-x86_64 -> riscv64-linux-musl musl-gcc fallback=0 | riscv64-linux-musl/musl-gcc/100/x riscv64-linux-musl/openkal/50/x + on macos-aarch64 -> riscv64-linux-musl musl-gcc fallback=0 | riscv64-linux-musl/musl-gcc/100/x riscv64-linux-musl/openkal/50/x + on windows-x86_64 -> riscv64-linux-musl musl-gcc fallback=0 | riscv64-linux-musl/musl-gcc/100/x riscv64-linux-musl/openkal/50/x +request x86_64-windows + parsed x86_64-windows-gnu envExplicit=0 + on linux-x86_64 -> x86_64-windows-musl openkal fallback=1 | x86_64-windows-msvc/msvc/100/x x86_64-windows-musl/openkal/50 x86_64-windows-gnu/mingw/10 + on macos-aarch64 -> x86_64-windows-musl openkal fallback=1 | x86_64-windows-msvc/msvc/100/x x86_64-windows-musl/openkal/50 x86_64-windows-gnu/mingw/10/x + on windows-x86_64 -> x86_64-windows-musl openkal fallback=1 | x86_64-windows-msvc/msvc/100/x x86_64-windows-musl/openkal/50 x86_64-windows-gnu/mingw/10 +request i686-windows + parsed i686-windows-gnu envExplicit=0 + on linux-x86_64 -> - - fallback=0 | + on macos-aarch64 -> - - fallback=0 | + on windows-x86_64 -> - - fallback=0 | +request aarch64-windows + parsed aarch64-windows-gnu envExplicit=0 + on linux-x86_64 -> - - fallback=0 | + on macos-aarch64 -> - - fallback=0 | + on windows-x86_64 -> - - fallback=0 | +request x86_64-macos + parsed x86_64-macos envExplicit=0 + on linux-x86_64 -> x86_64-macos-musl openkal fallback=0 | x86_64-macos-musl/openkal/50 + on macos-aarch64 -> x86_64-macos-musl openkal fallback=0 | x86_64-macos-musl/openkal/50 + on windows-x86_64 -> x86_64-macos-musl openkal fallback=0 | x86_64-macos-musl/openkal/50 +request aarch64-macos + parsed aarch64-macos envExplicit=0 + on linux-x86_64 -> aarch64-macos-musl openkal fallback=1 | aarch64-macos/apple/100/x aarch64-macos-musl/openkal/50 + on macos-aarch64 -> aarch64-macos apple fallback=0 | aarch64-macos/apple/100 aarch64-macos-musl/openkal/50 + on windows-x86_64 -> aarch64-macos-musl openkal fallback=1 | aarch64-macos/apple/100/x aarch64-macos-musl/openkal/50 +request aarch64-ios + parsed aarch64-ios envExplicit=0 + on linux-x86_64 -> aarch64-ios apple fallback=0 | aarch64-ios/apple/100/x aarch64-ios-sim/apple/100/x + on macos-aarch64 -> aarch64-ios apple fallback=0 | aarch64-ios/apple/100 aarch64-ios-sim/apple/100 + on windows-x86_64 -> aarch64-ios apple fallback=0 | aarch64-ios/apple/100/x aarch64-ios-sim/apple/100/x +request x86_64-openkal + parsed x86_64-openkal envExplicit=0 + on linux-x86_64 -> x86_64-openkal openkal fallback=0 | x86_64-openkal/openkal/100 + on macos-aarch64 -> x86_64-openkal openkal fallback=0 | x86_64-openkal/openkal/100 + on windows-x86_64 -> x86_64-openkal openkal fallback=0 | x86_64-openkal/openkal/100 +request x86_64-w64-mingw32 + parsed x86_64-windows-gnu envExplicit=1 + on linux-x86_64 -> x86_64-windows-gnu mingw fallback=0 | x86_64-windows-gnu/mingw/10 + on macos-aarch64 -> x86_64-windows-gnu mingw fallback=0 | x86_64-windows-gnu/mingw/10/x + on windows-x86_64 -> x86_64-windows-gnu mingw fallback=0 | x86_64-windows-gnu/mingw/10 +request x86_64-unknown-linux-gnu + parsed x86_64-linux-gnu envExplicit=1 + on linux-x86_64 -> x86_64-linux-gnu glibc fallback=0 | x86_64-linux-gnu/glibc/100 + on macos-aarch64 -> x86_64-linux-gnu glibc fallback=0 | x86_64-linux-gnu/glibc/100/x + on windows-x86_64 -> x86_64-linux-gnu glibc fallback=0 | x86_64-linux-gnu/glibc/100/x +request x86_64-pc-windows-msvc + parsed x86_64-windows-msvc envExplicit=1 + on linux-x86_64 -> x86_64-windows-msvc msvc fallback=0 | x86_64-windows-msvc/msvc/100/x + on macos-aarch64 -> x86_64-windows-msvc msvc fallback=0 | x86_64-windows-msvc/msvc/100/x + on windows-x86_64 -> x86_64-windows-msvc msvc fallback=0 | x86_64-windows-msvc/msvc/100/x +request arm64-apple-darwin24.1.0 + parsed aarch64-macos envExplicit=0 + on linux-x86_64 -> aarch64-macos-musl openkal fallback=1 | aarch64-macos/apple/100/x aarch64-macos-musl/openkal/50 + on macos-aarch64 -> aarch64-macos apple fallback=0 | aarch64-macos/apple/100 aarch64-macos-musl/openkal/50 + on windows-x86_64 -> aarch64-macos-musl openkal fallback=1 | aarch64-macos/apple/100/x aarch64-macos-musl/openkal/50 +request arm64-apple-macosx15.0 + parsed aarch64-macos envExplicit=0 + on linux-x86_64 -> aarch64-macos-musl openkal fallback=1 | aarch64-macos/apple/100/x aarch64-macos-musl/openkal/50 + on macos-aarch64 -> aarch64-macos apple fallback=0 | aarch64-macos/apple/100 aarch64-macos-musl/openkal/50 + on windows-x86_64 -> aarch64-macos-musl openkal fallback=1 | aarch64-macos/apple/100/x aarch64-macos-musl/openkal/50 +request aarch64-unknown-linux-musl + parsed aarch64-linux-musl envExplicit=1 + on linux-x86_64 -> aarch64-linux-musl musl-gcc fallback=0 | aarch64-linux-musl/musl-gcc/100 aarch64-linux-musl/openkal/50 + on macos-aarch64 -> aarch64-linux-musl openkal fallback=1 | aarch64-linux-musl/musl-gcc/100/x aarch64-linux-musl/openkal/50 + on windows-x86_64 -> aarch64-linux-musl openkal fallback=1 | aarch64-linux-musl/musl-gcc/100/x aarch64-linux-musl/openkal/50 +request aarch64-linux-android21 + parsed aarch64-linux-android envExplicit=1 + on linux-x86_64 -> aarch64-linux-android android-ndk fallback=0 | aarch64-linux-android/android-ndk/100 + on macos-aarch64 -> aarch64-linux-android android-ndk fallback=0 | aarch64-linux-android/android-ndk/100 + on windows-x86_64 -> aarch64-linux-android android-ndk fallback=0 | aarch64-linux-android/android-ndk/100 +request wasm32-unknown-emscripten + parsed wasm32-emscripten envExplicit=0 + on linux-x86_64 -> wasm32-emscripten emsdk fallback=0 | wasm32-emscripten/emsdk/100 + on macos-aarch64 -> wasm32-emscripten emsdk fallback=0 | wasm32-emscripten/emsdk/100 + on windows-x86_64 -> wasm32-emscripten emsdk fallback=0 | wasm32-emscripten/emsdk/100 +request x86-windows-msvc + parsed i686-windows-msvc envExplicit=1 + on linux-x86_64 -> - - fallback=0 | + on macos-aarch64 -> - - fallback=0 | + on windows-x86_64 -> - - fallback=0 | +request i686-none-elf + parsed i686-none-elf envExplicit=1 + on linux-x86_64 -> i686-none-elf bare-metal fallback=0 | i686-none-elf/bare-metal/100 + on macos-aarch64 -> i686-none-elf bare-metal fallback=0 | i686-none-elf/bare-metal/100 + on windows-x86_64 -> i686-none-elf bare-metal fallback=0 | i686-none-elf/bare-metal/100 +request x86_64-freebsd + unparseable +request x86_64-linux-gnux32 + unparseable +did-you-mean x86_64-linux-mus x86_64-linux-musl +did-you-mean x86-64-linux-gnu x86_64-linux-gnu +did-you-mean aarch64-macso aarch64-macos +did-you-mean riscv64-none-eld riscv64-none-elf +did-you-mean thumbv7m-none - +set linux -> x86_64-linux-gnu aarch64-linux-gnu | +set linux-musl -> x86_64-linux-musl aarch64-linux-musl | +set windows -> x86_64-windows-gnu | +set windows-msvc -> x86_64-windows-msvc | +set macos -> aarch64-macos | +set openkal -> x86_64-openkal aarch64-openkal | mcpplibs.openkal-platform@0.24.4 +set none-elf -> riscv64-none-elf riscv32-none-elf aarch64-none-elf x86_64-none-elf i686-none-elf | +set none-eabihf -> thumbv7em-none-eabihf thumbv8m.main-none-eabihf armv7a-none-eabihf | +set android -> aarch64-linux-android x86_64-linux-android | +set ios -> aarch64-ios | +set ios-sim -> aarch64-ios-sim x86_64-ios-sim | +set emscripten -> wasm32-emscripten | +set none refused diff --git a/tests/matrix/expected.tsv b/tests/matrix/expected.tsv index 44a21b15c..9b6e34ca6 100644 --- a/tests/matrix/expected.tsv +++ b/tests/matrix/expected.tsv @@ -44,14 +44,26 @@ # 所以在 x86_64 上够得着、在 aarch64 上够不着。只写 `linux` 会让后跑的一台把先跑 # 的那台在这张表里的行「解释掉」,而覆盖的方向取决于谁后跑,不取决于谁对。 # -# 全部四台:2026-10-08 由 ci-target-matrix.yml 实测(mcpp 源码 e862c657)。 -# 每一行来自对应宿主,两种体系分别测量。记录运行 37761833670。 +# 全部四台:2026-11-09 由 ci-target-matrix.yml 实测(mcpp 2026.11.9.1 候选,源码 55d5ad5c)。 +# 每一行来自对应宿主,两种体系分别测量。记录运行 42581749209。 # # 各台格数: -# linux-x86_64 70 gcc + llvm(payload 48 / graph 22) -# linux-aarch64 66 gcc + llvm(payload 46 / graph 20) -# macos-arm64 38 llvm(payload 27 / graph 11) -# windows-x86_64 70 llvm + msvc@system(payload 48 / graph 22) +# linux-x86_64 80 gcc + llvm(payload 54 / graph 26) +# linux-aarch64 76 gcc + llvm(payload 52 / graph 24) +# macos-arm64 47 llvm(payload 32 / graph 15) +# windows-x86_64 80 llvm + msvc@system(payload 54 / graph 26) +# +# 2026.11.9.1 的目标模型(SPEC-016)改变的格: +# - 一个系统的 musl 身份,在本机没有其他提供者时由 openkal-platform 服务(优先级 50): +# 非 macOS 宿主上的 `x86_64-macos`(补全为 `x86_64-macos-musl`)、`*-windows-musl`、 +# aarch64 Linux 上的 `x86_64-linux-musl`、macOS 与 Windows 上的 Linux musl 行, +# payload 体系以 llvm 建出;gcc 与 msvc 是对平台工具链(能力陈述)的推翻,`capability-pin`。 +# - 同一批行在 graph 体系里由平台服务,图里钉的 openkal-musl 0.3.5 与 openkal-llvm-runtime +# 0.1.3 在平台版本组之外,`platform-version-group`;依赖图供给 musl 的 `aarch64-macos` +# 身份随实际 C 库取 `aarch64-macos-musl`(Q5),同理。 +# - `x86_64-windows-gnu` 配 llvm:MinGW-w64 载荷与 clang 配对(§6),有载荷的宿主建出; +# 没有该载荷的宿主是 `host-cannot-serve`,与 gcc 的格同一条规则。 +# - `i686-none-elf` 是新行:llvm 建出,gcc 与 msvc 是 `capability-pin`。 # # 中间五列是报告器输出,不参与支持判定。现有报告器把 aarch64-ios 的 # c-abi 输出为 glibc(payload),这不代表 iOS 产物实际链接 glibc。Apple @@ -71,15 +83,15 @@ graph linux-aarch64 wasm32-emscripten llvm@23.1.3 - - - - - unsupported capabili graph linux-aarch64 x86_64-linux-android gcc@16.1.0 - - - - - unsupported capability-pin graph linux-aarch64 x86_64-linux-android llvm@23.1.3 - - - - - unsupported capability-pin graph linux-aarch64 x86_64-linux-musl gcc@16.1.0 - - - - - unsupported host-cannot-serve -graph linux-aarch64 x86_64-linux-musl llvm@23.1.3 x86_64-unknown-linux-musl payload musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph linux-aarch64 x86_64-macos gcc@16.1.0 - - - - - unsupported tier-planned -graph linux-aarch64 x86_64-macos llvm@23.1.3 - - - - - unsupported tier-planned +graph linux-aarch64 x86_64-linux-musl llvm@23.1.3 - - - - - unsupported platform-version-group +graph linux-aarch64 x86_64-macos gcc@16.1.0 - - - - - unsupported capability-pin +graph linux-aarch64 x86_64-macos llvm@23.1.3 - - - - - unsupported platform-version-group graph linux-aarch64 x86_64-openkal gcc@16.1.0 - - - - - unsupported capability-pin graph linux-aarch64 x86_64-openkal llvm@23.1.3 - - - - - unsupported platform-version-group graph linux-aarch64 x86_64-windows-gnu gcc@16.1.0 - - - - - unsupported host-cannot-serve graph linux-aarch64 x86_64-windows-gnu llvm@23.1.3 x86_64-w64-windows-gnu none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none graph linux-aarch64 x86_64-windows-musl gcc@16.1.0 - - - - - unsupported capability-pin -graph linux-aarch64 x86_64-windows-musl llvm@23.1.3 x86_64-w64-windows-gnu payload musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph linux-aarch64 x86_64-windows-musl llvm@23.1.3 - - - - - unsupported platform-version-group graph linux-x86_64 aarch64-linux-android gcc@16.1.0 - - - - - unsupported capability-pin graph linux-x86_64 aarch64-linux-android llvm@23.1.3 - - - - - unsupported capability-pin graph linux-x86_64 aarch64-linux-gnu gcc@16.1.0 - - - - - unsupported layer-requirement @@ -98,32 +110,34 @@ graph linux-x86_64 x86_64-linux-gnu gcc@16.1.0 - - - - - unsupported layer-requi graph linux-x86_64 x86_64-linux-gnu llvm@23.1.3 x86_64-unknown-linux-gnu payload musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none graph linux-x86_64 x86_64-linux-musl gcc@16.1.0 - - - - - unsupported layer-requirement graph linux-x86_64 x86_64-linux-musl llvm@23.1.3 x86_64-unknown-linux-musl payload musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph linux-x86_64 x86_64-macos gcc@16.1.0 - - - - - unsupported tier-planned -graph linux-x86_64 x86_64-macos llvm@23.1.3 - - - - - unsupported tier-planned +graph linux-x86_64 x86_64-macos gcc@16.1.0 - - - - - unsupported capability-pin +graph linux-x86_64 x86_64-macos llvm@23.1.3 - - - - - unsupported platform-version-group graph linux-x86_64 x86_64-openkal gcc@16.1.0 - - - - - unsupported capability-pin graph linux-x86_64 x86_64-openkal llvm@23.1.3 - - - - - unsupported platform-version-group graph linux-x86_64 x86_64-windows-gnu gcc@16.1.0 - - - - - unsupported layer-requirement graph linux-x86_64 x86_64-windows-gnu llvm@23.1.3 x86_64-w64-windows-gnu none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none graph linux-x86_64 x86_64-windows-musl gcc@16.1.0 - - - - - unsupported capability-pin -graph linux-x86_64 x86_64-windows-musl llvm@23.1.3 x86_64-w64-windows-gnu payload musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph linux-x86_64 x86_64-windows-musl llvm@23.1.3 - - - - - unsupported platform-version-group graph macos-arm64 aarch64-linux-android llvm@23.1.3 - - - - - unsupported capability-pin graph macos-arm64 aarch64-linux-gnu llvm@23.1.3 aarch64-unknown-linux-gnu host musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph macos-arm64 aarch64-linux-musl llvm@23.1.3 aarch64-unknown-linux-musl host musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph macos-arm64 aarch64-macos llvm@23.1.3 arm64-apple-macos14.0 host musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph macos-arm64 aarch64-linux-musl llvm@23.1.3 - - - - - unsupported platform-version-group +graph macos-arm64 aarch64-macos llvm@23.1.3 - - - - - unsupported platform-version-group +graph macos-arm64 aarch64-macos-musl llvm@23.1.3 - - - - - unsupported platform-version-group graph macos-arm64 aarch64-openkal llvm@23.1.3 - - - - - unsupported platform-version-group graph macos-arm64 riscv64-linux-musl llvm@23.1.3 - - - - - unsupported tier-planned graph macos-arm64 wasm32-emscripten llvm@23.1.3 - - - - - unsupported capability-pin graph macos-arm64 x86_64-linux-android llvm@23.1.3 - - - - - unsupported capability-pin -graph macos-arm64 x86_64-linux-musl llvm@23.1.3 x86_64-unknown-linux-musl host musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph macos-arm64 x86_64-macos llvm@23.1.3 - - - - - unsupported tier-planned +graph macos-arm64 x86_64-linux-musl llvm@23.1.3 - - - - - unsupported platform-version-group +graph macos-arm64 x86_64-macos llvm@23.1.3 - - - - - unsupported platform-version-group +graph macos-arm64 x86_64-macos-musl llvm@23.1.3 - - - - - unsupported platform-version-group graph macos-arm64 x86_64-openkal llvm@23.1.3 - - - - - unsupported platform-version-group graph macos-arm64 x86_64-windows-gnu llvm@23.1.3 x86_64-w64-windows-gnu none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph macos-arm64 x86_64-windows-musl llvm@23.1.3 x86_64-w64-windows-gnu host musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph macos-arm64 x86_64-windows-musl llvm@23.1.3 - - - - - unsupported platform-version-group graph windows-x86_64 aarch64-linux-android llvm@23.1.3 - - - - - unsupported capability-pin graph windows-x86_64 aarch64-linux-android msvc@system - - - - - unsupported capability-pin graph windows-x86_64 aarch64-linux-gnu llvm@23.1.3 aarch64-unknown-linux-gnu none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none graph windows-x86_64 aarch64-linux-gnu msvc@system - - - - - unsupported host-tool-toolchain -graph windows-x86_64 aarch64-linux-musl llvm@23.1.3 aarch64-unknown-linux-musl none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph windows-x86_64 aarch64-linux-musl llvm@23.1.3 - - - - - unsupported platform-version-group graph windows-x86_64 aarch64-linux-musl msvc@system - - - - - unsupported host-tool-toolchain graph windows-x86_64 aarch64-openkal llvm@23.1.3 - - - - - unsupported platform-version-group graph windows-x86_64 aarch64-openkal msvc@system - - - - - unsupported capability-pin @@ -135,15 +149,15 @@ graph windows-x86_64 x86_64-linux-android llvm@23.1.3 - - - - - unsupported capa graph windows-x86_64 x86_64-linux-android msvc@system - - - - - unsupported capability-pin graph windows-x86_64 x86_64-linux-musl llvm@23.1.3 x86_64-unknown-linux-musl none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none graph windows-x86_64 x86_64-linux-musl msvc@system - - - - - unsupported host-tool-toolchain -graph windows-x86_64 x86_64-macos llvm@23.1.3 - - - - - unsupported tier-planned -graph windows-x86_64 x86_64-macos msvc@system - - - - - unsupported tier-planned +graph windows-x86_64 x86_64-macos llvm@23.1.3 - - - - - unsupported platform-version-group +graph windows-x86_64 x86_64-macos msvc@system - - - - - unsupported capability-pin graph windows-x86_64 x86_64-openkal llvm@23.1.3 - - - - - unsupported platform-version-group graph windows-x86_64 x86_64-openkal msvc@system - - - - - unsupported capability-pin graph windows-x86_64 x86_64-windows-gnu llvm@23.1.3 x86_64-w64-windows-gnu none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none graph windows-x86_64 x86_64-windows-gnu msvc@system - - - - - unsupported host-tool-toolchain graph windows-x86_64 x86_64-windows-msvc llvm@23.1.3 - - - - - unsupported std-module-precompile graph windows-x86_64 x86_64-windows-msvc msvc@system - - - - - unsupported host-tool-toolchain -graph windows-x86_64 x86_64-windows-musl llvm@23.1.3 x86_64-w64-windows-gnu none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph windows-x86_64 x86_64-windows-musl llvm@23.1.3 - - - - - unsupported platform-version-group graph windows-x86_64 x86_64-windows-musl msvc@system - - - - - unsupported capability-pin payload linux-aarch64 aarch64-linux-android gcc@16.1.0 - - - - - unsupported capability-pin payload linux-aarch64 aarch64-linux-android llvm@23.1.3 - - - - - unsupported capability-pin @@ -154,11 +168,13 @@ payload linux-aarch64 aarch64-linux-musl llvm@23.1.3 - - - - - unsupported conve payload linux-aarch64 aarch64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin payload linux-aarch64 aarch64-none-elf llvm@23.1.3 aarch64-none-elf payload - - - ok none payload linux-aarch64 aarch64-openkal gcc@16.1.0 - - - - - unsupported capability-pin -payload linux-aarch64 aarch64-openkal llvm@23.1.3 aarch64-unknown-none-elf payload musl(platform) libc++(platform) openkal-musl@0.22.3 ok none +payload linux-aarch64 aarch64-openkal llvm@23.1.3 aarch64-unknown-none-elf payload musl(platform) libc++(platform) openkal-llvm-runtime@0.19.2 ok none payload linux-aarch64 armv7a-none-eabi gcc@16.1.0 - - - - - unsupported capability-pin payload linux-aarch64 armv7a-none-eabi llvm@23.1.3 armv7a-none-eabi payload - - - ok none payload linux-aarch64 armv7a-none-eabihf gcc@16.1.0 - - - - - unsupported capability-pin payload linux-aarch64 armv7a-none-eabihf llvm@23.1.3 armv7a-none-eabihf payload - - - ok none +payload linux-aarch64 i686-none-elf gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-aarch64 i686-none-elf llvm@23.1.3 i686-none-elf payload - - - ok none payload linux-aarch64 riscv32-none-elf gcc@16.1.0 - - - - - unsupported capability-pin payload linux-aarch64 riscv32-none-elf llvm@23.1.3 riscv32-none-elf payload - - - ok none payload linux-aarch64 riscv64-linux-musl gcc@16.1.0 - - - - - unsupported tier-planned @@ -184,17 +200,17 @@ payload linux-aarch64 wasm32-emscripten llvm@23.1.3 - - - - - unsupported capabi payload linux-aarch64 x86_64-linux-android gcc@16.1.0 - - - - - unsupported capability-pin payload linux-aarch64 x86_64-linux-android llvm@23.1.3 - - - - - unsupported capability-pin payload linux-aarch64 x86_64-linux-musl gcc@16.1.0 - - - - - unsupported host-cannot-serve -payload linux-aarch64 x86_64-linux-musl llvm@23.1.3 - - - - - unsupported convention-unreplaced -payload linux-aarch64 x86_64-macos gcc@16.1.0 - - - - - unsupported tier-planned -payload linux-aarch64 x86_64-macos llvm@23.1.3 - - - - - unsupported tier-planned +payload linux-aarch64 x86_64-linux-musl llvm@23.1.3 x86_64-unknown-linux-musl payload musl(platform) libc++(platform) openkal-llvm-runtime@0.19.2 ok none +payload linux-aarch64 x86_64-macos gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-aarch64 x86_64-macos llvm@23.1.3 x86_64-apple-macos14.0 payload musl(platform) libc++(platform) openkal-llvm-runtime@0.19.2 ok none payload linux-aarch64 x86_64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin payload linux-aarch64 x86_64-none-elf llvm@23.1.3 x86_64-none-elf payload - - - ok none payload linux-aarch64 x86_64-openkal gcc@16.1.0 - - - - - unsupported capability-pin -payload linux-aarch64 x86_64-openkal llvm@23.1.3 x86_64-unknown-none-elf payload musl(platform) libc++(platform) openkal-musl@0.22.3 ok none +payload linux-aarch64 x86_64-openkal llvm@23.1.3 x86_64-unknown-none-elf payload musl(platform) libc++(platform) openkal-llvm-runtime@0.19.2 ok none payload linux-aarch64 x86_64-windows-gnu gcc@16.1.0 - - - - - unsupported host-cannot-serve -payload linux-aarch64 x86_64-windows-gnu llvm@23.1.3 - - - - - unsupported convention-unreplaced +payload linux-aarch64 x86_64-windows-gnu llvm@23.1.3 - - - - - unsupported host-cannot-serve payload linux-aarch64 x86_64-windows-musl gcc@16.1.0 - - - - - unsupported capability-pin -payload linux-aarch64 x86_64-windows-musl llvm@23.1.3 - - - - - unsupported host-cannot-serve +payload linux-aarch64 x86_64-windows-musl llvm@23.1.3 x86_64-w64-windows-gnu payload musl(platform) libc++(platform) openkal-llvm-runtime@0.19.2 ok none payload linux-x86_64 aarch64-linux-android gcc@16.1.0 - - - - - unsupported capability-pin payload linux-x86_64 aarch64-linux-android llvm@23.1.3 - - - - - unsupported capability-pin payload linux-x86_64 aarch64-linux-gnu gcc@16.1.0 - - - - - unsupported convention-unreplaced @@ -204,11 +220,13 @@ payload linux-x86_64 aarch64-linux-musl llvm@23.1.3 - - - - - unsupported conven payload linux-x86_64 aarch64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin payload linux-x86_64 aarch64-none-elf llvm@23.1.3 aarch64-none-elf payload - - - ok none payload linux-x86_64 aarch64-openkal gcc@16.1.0 - - - - - unsupported capability-pin -payload linux-x86_64 aarch64-openkal llvm@23.1.3 aarch64-unknown-none-elf payload musl(platform) libc++(platform) openkal-musl@0.22.3 ok none +payload linux-x86_64 aarch64-openkal llvm@23.1.3 aarch64-unknown-none-elf payload musl(platform) libc++(platform) openkal-llvm-runtime@0.19.2 ok none payload linux-x86_64 armv7a-none-eabi gcc@16.1.0 - - - - - unsupported capability-pin payload linux-x86_64 armv7a-none-eabi llvm@23.1.3 armv7a-none-eabi payload - - - ok none payload linux-x86_64 armv7a-none-eabihf gcc@16.1.0 - - - - - unsupported capability-pin payload linux-x86_64 armv7a-none-eabihf llvm@23.1.3 armv7a-none-eabihf payload - - - ok none +payload linux-x86_64 i686-none-elf gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-x86_64 i686-none-elf llvm@23.1.3 i686-none-elf payload - - - ok none payload linux-x86_64 riscv32-none-elf gcc@16.1.0 - - - - - unsupported capability-pin payload linux-x86_64 riscv32-none-elf llvm@23.1.3 riscv32-none-elf payload - - - ok none payload linux-x86_64 riscv64-linux-musl gcc@16.1.0 - - - - - unsupported tier-planned @@ -237,26 +255,28 @@ payload linux-x86_64 x86_64-linux-gnu gcc@16.1.0 x86_64-unknown-linux-gnu subos payload linux-x86_64 x86_64-linux-gnu llvm@23.1.3 x86_64-unknown-linux-gnu payload glibc(payload) libc++(payload) - ok none payload linux-x86_64 x86_64-linux-musl gcc@16.1.0 x86_64-unknown-linux-musl payload musl(payload) libstdc++(payload) - ok none payload linux-x86_64 x86_64-linux-musl llvm@23.1.3 - - - - - unsupported convention-unreplaced -payload linux-x86_64 x86_64-macos gcc@16.1.0 - - - - - unsupported tier-planned -payload linux-x86_64 x86_64-macos llvm@23.1.3 - - - - - unsupported tier-planned +payload linux-x86_64 x86_64-macos gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-x86_64 x86_64-macos llvm@23.1.3 x86_64-apple-macos14.0 payload musl(platform) libc++(platform) openkal-llvm-runtime@0.19.2 ok none payload linux-x86_64 x86_64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin payload linux-x86_64 x86_64-none-elf llvm@23.1.3 x86_64-none-elf payload - - - ok none payload linux-x86_64 x86_64-openkal gcc@16.1.0 - - - - - unsupported capability-pin -payload linux-x86_64 x86_64-openkal llvm@23.1.3 x86_64-unknown-none-elf payload musl(platform) libc++(platform) openkal-musl@0.22.3 ok none +payload linux-x86_64 x86_64-openkal llvm@23.1.3 x86_64-unknown-none-elf payload musl(platform) libc++(platform) openkal-llvm-runtime@0.19.2 ok none payload linux-x86_64 x86_64-windows-gnu gcc@16.1.0 x86_64-w64-windows-gnu none ucrt(payload) libstdc++(payload) - ok none -payload linux-x86_64 x86_64-windows-gnu llvm@23.1.3 - - - - - unsupported convention-unreplaced +payload linux-x86_64 x86_64-windows-gnu llvm@23.1.3 x86_64-w64-windows-gnu none ucrt(payload) libstdc++(payload) - ok none payload linux-x86_64 x86_64-windows-musl gcc@16.1.0 - - - - - unsupported capability-pin -payload linux-x86_64 x86_64-windows-musl llvm@23.1.3 - - - - - unsupported host-cannot-serve -payload macos-arm64 aarch64-ios llvm@23.1.3 arm64-apple-ios17.5 host glibc(payload) libc++(payload) - ok none -payload macos-arm64 aarch64-ios-sim llvm@23.1.3 arm64-apple-ios17.5-simulator host sim(payload) libc++(payload) - ok none +payload linux-x86_64 x86_64-windows-musl llvm@23.1.3 x86_64-w64-windows-gnu payload musl(platform) libc++(platform) openkal-llvm-runtime@0.19.2 ok none +payload macos-arm64 aarch64-ios llvm@23.1.3 arm64-apple-ios18.5 host glibc(payload) libc++(payload) - ok none +payload macos-arm64 aarch64-ios-sim llvm@23.1.3 arm64-apple-ios18.5-simulator host sim(payload) libc++(payload) - ok none payload macos-arm64 aarch64-linux-android llvm@23.1.3 - - - - - unsupported capability-pin payload macos-arm64 aarch64-linux-gnu llvm@23.1.3 - - - - - unsupported host-cannot-serve -payload macos-arm64 aarch64-linux-musl llvm@23.1.3 - - - - - unsupported convention-unreplaced +payload macos-arm64 aarch64-linux-musl llvm@23.1.3 aarch64-unknown-linux-musl host musl(platform) libc++(platform) openkal-llvm-runtime@0.19.2 ok none payload macos-arm64 aarch64-macos llvm@23.1.3 arm64-apple-macos14.0 host libSystem(payload) libc++(payload) - ok none +payload macos-arm64 aarch64-macos-musl llvm@23.1.3 arm64-apple-macos14.0 host musl(platform) libc++(platform) openkal-llvm-runtime@0.19.2 ok none payload macos-arm64 aarch64-none-elf llvm@23.1.3 aarch64-none-elf host - - - ok none -payload macos-arm64 aarch64-openkal llvm@23.1.3 aarch64-unknown-none-elf payload musl(platform) libc++(platform) openkal-musl@0.22.3 ok none +payload macos-arm64 aarch64-openkal llvm@23.1.3 aarch64-unknown-none-elf host musl(platform) libc++(platform) openkal-llvm-runtime@0.19.2 ok none payload macos-arm64 armv7a-none-eabi llvm@23.1.3 armv7a-none-eabi host - - - ok none payload macos-arm64 armv7a-none-eabihf llvm@23.1.3 armv7a-none-eabihf host - - - ok none +payload macos-arm64 i686-none-elf llvm@23.1.3 i686-none-elf host - - - ok none payload macos-arm64 riscv32-none-elf llvm@23.1.3 riscv32-none-elf host - - - ok none payload macos-arm64 riscv64-linux-musl llvm@23.1.3 - - - - - unsupported tier-planned payload macos-arm64 riscv64-none-elf llvm@23.1.3 riscv64-none-elf host - - - ok none @@ -268,28 +288,31 @@ payload macos-arm64 thumbv8m.base-none-eabi llvm@23.1.3 thumbv8m.base-none-eabi payload macos-arm64 thumbv8m.main-none-eabi llvm@23.1.3 thumbv8m.main-none-eabi host - - - ok none payload macos-arm64 thumbv8m.main-none-eabihf llvm@23.1.3 thumbv8m.main-none-eabihf host - - - ok none payload macos-arm64 wasm32-emscripten llvm@23.1.3 - - - - - unsupported capability-pin -payload macos-arm64 x86_64-ios-sim llvm@23.1.3 x86_64-apple-ios17.5-simulator host sim(payload) libc++(payload) - ok none +payload macos-arm64 x86_64-ios-sim llvm@23.1.3 x86_64-apple-ios18.5-simulator host sim(payload) libc++(payload) - ok none payload macos-arm64 x86_64-linux-android llvm@23.1.3 - - - - - unsupported capability-pin -payload macos-arm64 x86_64-linux-musl llvm@23.1.3 - - - - - unsupported convention-unreplaced -payload macos-arm64 x86_64-macos llvm@23.1.3 - - - - - unsupported tier-planned +payload macos-arm64 x86_64-linux-musl llvm@23.1.3 x86_64-unknown-linux-musl host musl(platform) libc++(platform) openkal-llvm-runtime@0.19.2 ok none +payload macos-arm64 x86_64-macos llvm@23.1.3 x86_64-apple-macos14.0 host musl(platform) libc++(platform) openkal-llvm-runtime@0.19.2 ok none +payload macos-arm64 x86_64-macos-musl llvm@23.1.3 x86_64-apple-macos14.0 host musl(platform) libc++(platform) openkal-llvm-runtime@0.19.2 ok none payload macos-arm64 x86_64-none-elf llvm@23.1.3 x86_64-none-elf host - - - ok none -payload macos-arm64 x86_64-openkal llvm@23.1.3 x86_64-unknown-none-elf payload musl(platform) libc++(platform) openkal-musl@0.22.3 ok none -payload macos-arm64 x86_64-windows-gnu llvm@23.1.3 - - - - - unsupported convention-unreplaced -payload macos-arm64 x86_64-windows-musl llvm@23.1.3 - - - - - unsupported host-cannot-serve +payload macos-arm64 x86_64-openkal llvm@23.1.3 x86_64-unknown-none-elf host musl(platform) libc++(platform) openkal-llvm-runtime@0.19.2 ok none +payload macos-arm64 x86_64-windows-gnu llvm@23.1.3 - - - - - unsupported host-cannot-serve +payload macos-arm64 x86_64-windows-musl llvm@23.1.3 x86_64-w64-windows-gnu host musl(platform) libc++(platform) openkal-llvm-runtime@0.19.2 ok none payload windows-x86_64 aarch64-linux-android llvm@23.1.3 - - - - - unsupported capability-pin payload windows-x86_64 aarch64-linux-android msvc@system - - - - - unsupported capability-pin payload windows-x86_64 aarch64-linux-gnu llvm@23.1.3 - - - - - unsupported host-cannot-serve payload windows-x86_64 aarch64-linux-gnu msvc@system - - - - - unsupported convention-unreplaced -payload windows-x86_64 aarch64-linux-musl llvm@23.1.3 - - - - - unsupported convention-unreplaced +payload windows-x86_64 aarch64-linux-musl llvm@23.1.3 aarch64-unknown-linux-musl none musl(platform) libc++(platform) openkal-llvm-runtime@0.19.2 ok none payload windows-x86_64 aarch64-linux-musl msvc@system - - - - - unsupported convention-unreplaced payload windows-x86_64 aarch64-none-elf llvm@23.1.3 aarch64-none-elf none - - - ok none payload windows-x86_64 aarch64-none-elf msvc@system - - - - - unsupported capability-pin -payload windows-x86_64 aarch64-openkal llvm@23.1.3 aarch64-unknown-none-elf payload musl(platform) libc++(platform) openkal-musl@0.22.3 ok none +payload windows-x86_64 aarch64-openkal llvm@23.1.3 aarch64-unknown-none-elf none musl(platform) libc++(platform) openkal-llvm-runtime@0.19.2 ok none payload windows-x86_64 aarch64-openkal msvc@system - - - - - unsupported capability-pin payload windows-x86_64 armv7a-none-eabi llvm@23.1.3 armv7a-none-eabi none - - - ok none payload windows-x86_64 armv7a-none-eabi msvc@system - - - - - unsupported capability-pin payload windows-x86_64 armv7a-none-eabihf llvm@23.1.3 armv7a-none-eabihf none - - - ok none payload windows-x86_64 armv7a-none-eabihf msvc@system - - - - - unsupported capability-pin +payload windows-x86_64 i686-none-elf llvm@23.1.3 - - - - - unsupported lld-required-absent +payload windows-x86_64 i686-none-elf msvc@system - - - - - unsupported capability-pin payload windows-x86_64 riscv32-none-elf llvm@23.1.3 riscv32-none-elf none - - - ok none payload windows-x86_64 riscv32-none-elf msvc@system - - - - - unsupported capability-pin payload windows-x86_64 riscv64-linux-musl llvm@23.1.3 - - - - - unsupported tier-planned @@ -316,15 +339,15 @@ payload windows-x86_64 x86_64-linux-android llvm@23.1.3 - - - - - unsupported ca payload windows-x86_64 x86_64-linux-android msvc@system - - - - - unsupported capability-pin payload windows-x86_64 x86_64-linux-musl llvm@23.1.3 - - - - - unsupported convention-unreplaced payload windows-x86_64 x86_64-linux-musl msvc@system - - - - - unsupported convention-unreplaced -payload windows-x86_64 x86_64-macos llvm@23.1.3 - - - - - unsupported tier-planned -payload windows-x86_64 x86_64-macos msvc@system - - - - - unsupported tier-planned +payload windows-x86_64 x86_64-macos llvm@23.1.3 x86_64-apple-macos14.0 none musl(platform) libc++(platform) openkal-llvm-runtime@0.19.2 ok none +payload windows-x86_64 x86_64-macos msvc@system - - - - - unsupported capability-pin payload windows-x86_64 x86_64-none-elf llvm@23.1.3 - - - - - unsupported lld-required-absent payload windows-x86_64 x86_64-none-elf msvc@system - - - - - unsupported capability-pin -payload windows-x86_64 x86_64-openkal llvm@23.1.3 x86_64-unknown-none-elf payload musl(platform) libc++(platform) openkal-musl@0.22.3 ok none +payload windows-x86_64 x86_64-openkal llvm@23.1.3 x86_64-unknown-none-elf none musl(platform) libc++(platform) openkal-llvm-runtime@0.19.2 ok none payload windows-x86_64 x86_64-openkal msvc@system - - - - - unsupported capability-pin -payload windows-x86_64 x86_64-windows-gnu llvm@23.1.3 - - - - - unsupported convention-unreplaced +payload windows-x86_64 x86_64-windows-gnu llvm@23.1.3 x86_64-w64-windows-gnu none ucrt(payload) libstdc++(payload) - ok none payload windows-x86_64 x86_64-windows-gnu msvc@system - - - - - unsupported convention-unreplaced payload windows-x86_64 x86_64-windows-msvc llvm@23.1.3 x86_64-pc-windows-msvc none msvc(payload) msvc-stl(payload) - ok none payload windows-x86_64 x86_64-windows-msvc msvc@system x86_64-pc-windows-msvc none msvc(payload) (payload) - ok none -payload windows-x86_64 x86_64-windows-musl llvm@23.1.3 - - - - - unsupported host-cannot-serve +payload windows-x86_64 x86_64-windows-musl llvm@23.1.3 x86_64-w64-windows-gnu none musl(platform) libc++(platform) openkal-llvm-runtime@0.19.2 ok none payload windows-x86_64 x86_64-windows-musl msvc@system - - - - - unsupported capability-pin diff --git a/tests/scripts/test_check_e2e_coverage.py b/tests/scripts/test_check_e2e_coverage.py index ab570ae4e..54233693a 100644 --- a/tests/scripts/test_check_e2e_coverage.py +++ b/tests/scripts/test_check_e2e_coverage.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 """Fixture tests for .github/tools/check_e2e_coverage.py (rule R5). -A test that ran, a test a workflow names, and a test the exceptions excuse are -covered; a test that is none of these fails the check, and so does an exception +A test that ran, a test a set left out as unaffected, a test a workflow names, +and a test the exceptions excuse are covered; a test that is none of these fails the check, and so does an exception for a test that ran or that does not exist. """ @@ -46,7 +46,7 @@ def run_check(self, *extra: str) -> subprocess.CompletedProcess: def test_ran_named_and_excused_tests_are_covered(self) -> None: r = self.run_check() self.assertEqual(r.returncode, 0, r.stdout) - self.assertIn("1 ran on a shard, 2 run by a dedicated job, 1 excused, 0 uncovered", r.stdout) + self.assertIn("1 ran on a shard, 0 unaffected by the change, 2 run by a dedicated job, 1 excused, 0 uncovered", r.stdout) def test_a_name_in_a_comment_or_inside_a_longer_name_does_not_count(self) -> None: e2e = self.root / "tests" / "e2e" @@ -71,6 +71,23 @@ def test_a_test_that_runs_nowhere_fails(self) -> None: self.assertEqual(r.returncode, 1) self.assertIn("UNCOVERED: 50_nowhere.sh: linux: missing capability: llvm", r.stdout) + def test_a_test_a_set_left_out_is_unaffected_and_meets_its_host(self) -> None: + e2e = self.root / "tests" / "e2e" + (e2e / "50_untouched.sh").write_text("# x\n# requires: unix-shell\n") + (e2e / "host-requirements.tsv").write_text("50_untouched.sh\tlinux\tverified rows\n") + (self.reports / "e2e-report-linux-2.tsv").write_text( + "unaffected\t50_untouched.sh\t0\tnot in the set 'pr'\n") + r = self.run_check() + self.assertEqual(r.returncode, 0, r.stdout) + self.assertIn("1 unaffected by the change", r.stdout) + # Another host's unaffected report does not meet a requirement of linux. + (self.reports / "e2e-report-linux-2.tsv").unlink() + (self.reports / "e2e-report-macos-1.tsv").write_text( + "unaffected\t50_untouched.sh\t0\tnot in the set 'pr'\n") + r = self.run_check() + self.assertEqual(r.returncode, 1) + self.assertIn("REQUIRED ON HOST: 50_untouched.sh did not pass on linux", r.stdout) + def test_an_exception_for_a_test_that_ran_fails(self) -> None: (self.reports / "e2e-report-macos-1.tsv").write_text("pass\t30_excused.sh\t10\t\n") r = self.run_check() diff --git a/tests/unit/test_abi.cpp b/tests/unit/test_abi.cpp index 75ec7ae59..7459b70f1 100644 --- a/tests/unit/test_abi.cpp +++ b/tests/unit/test_abi.cpp @@ -1,11 +1,13 @@ -#include import std; +import mcpplibs.mtest; import mcpp.toolchain.abi; import mcpp.toolchain.model; import mcpp.manifest; import mcpp.build.prepare; +using namespace mcpplibs::mtest; + using namespace mcpp::toolchain; namespace { @@ -22,82 +24,82 @@ Toolchain make_tc(std::string triple, std::string stdlib, CompilerId cc) { // ─── abi_profile: each dimension from its single canonical source ─────────── -TEST(AbiProfile, ClangLibcxxOnLinuxGnuIsGlibcAtLibcLevel) { +UTest clang_libcxx_on_linux_gnu_is_glibc_at_libc_level{"AbiProfile/ClangLibcxxOnLinuxGnuIsGlibcAtLibcLevel", [] { // The bug: a clang+libc++ toolchain targeting *-linux-gnu is glibc at the // libc level; only its C++ stdlib is libc++. These are independent dims. auto p = abi_profile(make_tc("x86_64-unknown-linux-gnu", "libc++", CompilerId::Clang)); - EXPECT_EQ(p.libc, "glibc"); - EXPECT_EQ(p.cxxStdlib, "libc++"); - EXPECT_EQ(p.arch, "x86_64"); - EXPECT_EQ(p.os, "linux"); - EXPECT_EQ(p.cxxAbi, "itanium"); -} - -TEST(AbiProfile, GccGlibc) { + expect(eq(p.libc, "glibc")); + expect(eq(p.cxxStdlib, "libc++")); + expect(eq(p.arch, "x86_64")); + expect(eq(p.os, "linux")); + expect(eq(p.cxxAbi, "itanium")); +}}; + +UTest gcc_glibc{"AbiProfile/GccGlibc", [] { auto p = abi_profile(make_tc("x86_64-linux-gnu", "libstdc++", CompilerId::GCC)); - EXPECT_EQ(p.libc, "glibc"); - EXPECT_EQ(p.cxxStdlib, "libstdc++"); -} + expect(eq(p.libc, "glibc")); + expect(eq(p.cxxStdlib, "libstdc++")); +}}; -TEST(AbiProfile, MuslTripleIsMusl) { +UTest musl_triple_is_musl{"AbiProfile/MuslTripleIsMusl", [] { auto p = abi_profile(make_tc("x86_64-linux-musl", "libstdc++", CompilerId::GCC)); - EXPECT_EQ(p.libc, "musl"); - EXPECT_EQ(p.os, "linux"); -} + expect(eq(p.libc, "musl")); + expect(eq(p.os, "linux")); +}}; // ─── abi_check: the regression + the genuine incompatibility ──────────────── -TEST(AbiCheck, GlibcCLibraryUnderLibcxxIsCompatible) { +UTest glibc_c_library_under_libcxx_is_compatible{"AbiCheck/GlibcCLibraryUnderLibcxxIsCompatible", [] { // Regression lock for the glfw-under-clang false positive. auto p = abi_profile(make_tc("x86_64-unknown-linux-gnu", "libc++", CompilerId::Clang)); auto c = parse_abi_capability("abi:glibc", "compat.glfw"); - ASSERT_TRUE(c.has_value()); - EXPECT_TRUE(abi_check(p, {*c}).empty()); -} + expect(fatal(c.has_value())); + expect(abi_check(p, {*c}).empty()); +}}; -TEST(AbiCheck, GlibcDepUnderMuslMismatchesOnLibcDim) { +UTest glibc_dep_under_musl_mismatches_on_libc_dim{"AbiCheck/GlibcDepUnderMuslMismatchesOnLibcDim", [] { auto p = abi_profile(make_tc("x86_64-linux-musl", "libstdc++", CompilerId::GCC)); auto c = parse_abi_capability("abi:glibc", "compat.glfw"); - ASSERT_TRUE(c.has_value()); + expect(fatal(c.has_value())); auto mm = abi_check(p, {*c}); - ASSERT_EQ(mm.size(), 1u); - EXPECT_EQ(mm[0].dim, AbiDim::Libc); - EXPECT_EQ(mm[0].need, "glibc"); - EXPECT_EQ(mm[0].got, "musl"); -} + expect(fatal(eq(mm.size(), 1u))); + expect(eq(mm[0].dim, AbiDim::Libc)); + expect(eq(mm[0].need, "glibc")); + expect(eq(mm[0].got, "musl")); +}}; -TEST(AbiCheck, UnspecifiedDimensionIsDontCare) { +UTest unspecified_dimension_is_dont_care{"AbiCheck/UnspecifiedDimensionIsDontCare", [] { // A cxxstdlib constraint must NOT be implied by a bare libc capability. auto p = abi_profile(make_tc("x86_64-unknown-linux-gnu", "libc++", CompilerId::Clang)); - EXPECT_TRUE(abi_check(p, {{AbiDim::Arch, "x86_64", "x"}}).empty()); // matches - EXPECT_FALSE(abi_check(p, {{AbiDim::Arch, "aarch64", "x"}}).empty()); // mismatches -} + expect(abi_check(p, {{AbiDim::Arch, "x86_64", "x"}}).empty()); // matches + expect(!(abi_check(p, {{AbiDim::Arch, "aarch64", "x"}}).empty())); // mismatches +}}; // ─── parse_abi_capability: legacy + dimensional forms ─────────────────────── -TEST(AbiCapability, LegacyBareFormIsLibc) { +UTest legacy_bare_form_is_libc{"AbiCapability/LegacyBareFormIsLibc", [] { auto c = parse_abi_capability("abi:glibc", "pkg"); - ASSERT_TRUE(c.has_value()); - EXPECT_EQ(c->dim, AbiDim::Libc); - EXPECT_EQ(c->value, "glibc"); - EXPECT_EQ(c->source, "pkg"); -} + expect(fatal(c.has_value())); + expect(eq(c->dim, AbiDim::Libc)); + expect(eq(c->value, "glibc")); + expect(eq(c->source, "pkg")); +}}; -TEST(AbiCapability, DimensionalForm) { +UTest dimensional_form{"AbiCapability/DimensionalForm", [] { auto c = parse_abi_capability("abi:cxxstdlib=libc++"); - ASSERT_TRUE(c.has_value()); - EXPECT_EQ(c->dim, AbiDim::CxxStdlib); - EXPECT_EQ(c->value, "libc++"); -} + expect(fatal(c.has_value())); + expect(eq(c->dim, AbiDim::CxxStdlib)); + expect(eq(c->value, "libc++")); +}}; -TEST(AbiCapability, NonAbiCapabilityIgnored) { - EXPECT_FALSE(parse_abi_capability("x11.display").has_value()); - EXPECT_FALSE(parse_abi_capability("opengl.glx.driver").has_value()); -} +UTest non_abi_capability_ignored{"AbiCapability/NonAbiCapabilityIgnored", [] { + expect(!(parse_abi_capability("x11.display").has_value())); + expect(!(parse_abi_capability("opengl.glx.driver").has_value())); +}}; -TEST(AbiCapability, UnknownDimensionIgnored) { - EXPECT_FALSE(parse_abi_capability("abi:nonsense=1").has_value()); -} +UTest unknown_dimension_ignored{"AbiCapability/UnknownDimensionIgnored", [] { + expect(!(parse_abi_capability("abi:nonsense=1").has_value())); +}}; // ─── `[target..abi] exceptions` and `requires_abi` on the target axis ── // @@ -126,7 +128,7 @@ const mcpp::manifest::ConditionalConfig* find_cc( } // namespace manifest_abi -TEST(AbiTableExceptions, ParsesAndReachesTheConditionalConfig) { +UTest parses_and_reaches_the_conditional_config{"AbiTableExceptions/ParsesAndReachesTheConditionalConfig", [] { constexpr auto src = R"( [package] name = "webrt" @@ -135,17 +137,17 @@ version = "0.1.0" exceptions = true )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); auto* cc = manifest_abi::find_cc(*m, "cfg(os = \"emscripten\")"); - ASSERT_NE(cc, nullptr); - EXPECT_TRUE(cc->abiExceptionsDeclared); - EXPECT_TRUE(cc->abiExceptions); + expect(fatal(ne(cc, nullptr))); + expect(bool(cc->abiExceptionsDeclared)); + expect(bool(cc->abiExceptions)); // `threads` is untouched by a table that only names `exceptions` -- // the two members are independent switches, not a package deal. - EXPECT_FALSE(cc->abiThreadsDeclared); -} + expect(!cc->abiThreadsDeclared); +}}; -TEST(AbiTableExceptions, BothMembersInOneTableParseIndependently) { +UTest both_members_in_one_table_parse_independently{"AbiTableExceptions/BothMembersInOneTableParseIndependently", [] { constexpr auto src = R"( [package] name = "webrt" @@ -155,16 +157,16 @@ threads = true exceptions = false )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); auto* cc = manifest_abi::find_cc(*m, "cfg(os = \"emscripten\")"); - ASSERT_NE(cc, nullptr); - EXPECT_TRUE(cc->abiThreadsDeclared); - EXPECT_TRUE(cc->abiThreads); - EXPECT_TRUE(cc->abiExceptionsDeclared); - EXPECT_FALSE(cc->abiExceptions); -} - -TEST(AbiTableExceptions, UnknownMemberNamesBothThreadsAndExceptions) { + expect(fatal(ne(cc, nullptr))); + expect(bool(cc->abiThreadsDeclared)); + expect(bool(cc->abiThreads)); + expect(bool(cc->abiExceptionsDeclared)); + expect(!cc->abiExceptions); +}}; + +UTest unknown_member_names_both_threads_and_exceptions{"AbiTableExceptions/UnknownMemberNamesBothThreadsAndExceptions", [] { constexpr auto src = R"( [package] name = "a" @@ -173,13 +175,12 @@ version = "0.1.0" frobnicate = true )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().message.find( - "has no member 'frobnicate'; the members are: threads, exceptions"), - std::string::npos) << m.error().message; -} + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().message.find( + "has no member 'frobnicate'; the members are: threads, exceptions"), std::string::npos)); !failed) failed << m.error().message; +}}; -TEST(AbiTableExceptions, ANonBooleanExceptionsIsRefused) { +UTest a_non_boolean_exceptions_is_refused{"AbiTableExceptions/ANonBooleanExceptionsIsRefused", [] { constexpr auto src = R"( [package] name = "a" @@ -188,14 +189,13 @@ version = "0.1.0" exceptions = 1 )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().message.find(".exceptions must be true, false, \"wasm\" or \"js\""), - std::string::npos) << m.error().message; -} + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().message.find(".exceptions must be true, false, \"wasm\" or \"js\""), std::string::npos)); !failed) failed << m.error().message; +}}; // ─── `requires_abi` as a direct key of `[target.]` ───────────────────── -TEST(RequiresAbiOnTargetAxis, SelectorScopedRequirementParsesIntoTheConditionalConfig) { +UTest selector_scoped_requirement_parses_into_the_conditional_config{"RequiresAbiOnTargetAxis/SelectorScopedRequirementParsesIntoTheConditionalConfig", [] { constexpr auto src = R"( [package] name = "adapter" @@ -204,14 +204,14 @@ version = "0.1.0" requires_abi = { threads = true } )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); auto* cc = manifest_abi::find_cc(*m, "cfg(linux)"); - ASSERT_NE(cc, nullptr); - EXPECT_TRUE(cc->requiresAbiThreads); - EXPECT_FALSE(cc->requiresAbiExceptions); -} + expect(fatal(ne(cc, nullptr))); + expect(bool(cc->requiresAbiThreads)); + expect(!cc->requiresAbiExceptions); +}}; -TEST(RequiresAbiOnTargetAxis, FeatureFormParsesUnderTheSelector) { +UTest feature_form_parses_under_the_selector{"RequiresAbiOnTargetAxis/FeatureFormParsesUnderTheSelector", [] { constexpr auto src = R"( [package] name = "adapter" @@ -220,18 +220,18 @@ version = "0.1.0" mt = { threads = true } )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); auto* cc = manifest_abi::find_cc(*m, "cfg(linux)"); - ASSERT_NE(cc, nullptr); - ASSERT_TRUE(cc->featureRequiresAbiThreads.contains("mt")); - EXPECT_TRUE(cc->featureRequiresAbiThreads.at("mt")); + expect(fatal(ne(cc, nullptr))); + expect(fatal(cc->featureRequiresAbiThreads.contains("mt"))); + expect(bool(cc->featureRequiresAbiThreads.at("mt"))); // The feature-requires-abi table registers the feature UNCONDITIONALLY // (the same rule feature-deps follows, #359), so it does not trip the // unknown-feature diagnostic on a build that never selects this target. - EXPECT_TRUE(m->featuresMap.contains("mt")); -} + expect(m->featuresMap.contains("mt")); +}}; -TEST(RequiresAbiOnTargetAxis, UnknownMemberInsideRequiresAbiNamesBoth) { +UTest unknown_member_inside_requires_abi_names_both{"RequiresAbiOnTargetAxis/UnknownMemberInsideRequiresAbiNamesBoth", [] { constexpr auto src = R"( [package] name = "adapter" @@ -240,13 +240,12 @@ version = "0.1.0" requires_abi = { thread = true } )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().message.find( - "requires_abi.thread: the members are `threads`, `exceptions`, booleans"), - std::string::npos) << m.error().message; -} + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().message.find( + "requires_abi.thread: the members are `threads`, `exceptions`, booleans"), std::string::npos)); !failed) failed << m.error().message; +}}; -TEST(RequiresAbiOnTargetAxis, UnknownMemberInsideFeatureFormNamesBoth) { +UTest unknown_member_inside_feature_form_names_both{"RequiresAbiOnTargetAxis/UnknownMemberInsideFeatureFormNamesBoth", [] { constexpr auto src = R"( [package] name = "adapter" @@ -255,12 +254,11 @@ version = "0.1.0" mt = { thread = true } )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().message.find( + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().message.find( "feature-requires-abi.mt].thread: the members are `threads`, " - "`exceptions`, booleans"), - std::string::npos) << m.error().message; -} + "`exceptions`, booleans"), std::string::npos)); !failed) failed << m.error().message; +}}; // ─── The union: a matching selector's requirement joins the package's ────── // @@ -273,7 +271,7 @@ namespace manifest_abi { mcpp::manifest::Manifest merged_for(std::string_view src, std::string_view triple) { auto m = mcpp::manifest::parse_string(src); - EXPECT_TRUE(m.has_value()) << (m.has_value() ? "" : m.error().format()); + if (auto&& failed = expect(m.has_value()); !failed) failed << (m.has_value() ? "" : m.error().format()); if (!m) return {}; mcpp::build::merge_conditional_config(*m, cfgpred::context_for(triple)); return *m; @@ -281,7 +279,7 @@ mcpp::manifest::Manifest merged_for(std::string_view src, std::string_view tripl } // namespace manifest_abi -TEST(RequiresAbiOnTargetAxis, MatchingSelectorUnionsIntoThePackagesRequirement) { +UTest matching_selector_unions_into_the_packages_requirement{"RequiresAbiOnTargetAxis/MatchingSelectorUnionsIntoThePackagesRequirement", [] { constexpr auto src = R"( [package] name = "adapter" @@ -290,12 +288,12 @@ version = "0.1.0" requires_abi = { threads = true } )"; auto lin = manifest_abi::merged_for(src, "x86_64-unknown-linux-gnu"); - ASSERT_EQ(lin.targetRequiresAbiThreads.size(), 1u); - EXPECT_EQ(lin.targetRequiresAbiThreads.front(), "cfg(linux)"); - EXPECT_TRUE(lin.targetRequiresAbiExceptions.empty()); -} + expect(fatal(eq(lin.targetRequiresAbiThreads.size(), 1u))); + expect(eq(lin.targetRequiresAbiThreads.front(), "cfg(linux)")); + expect(lin.targetRequiresAbiExceptions.empty()); +}}; -TEST(RequiresAbiOnTargetAxis, ASelectorThatDoesNotMatchTheHostImposesNothing) { +UTest a_selector_that_does_not_match_the_host_imposes_nothing{"RequiresAbiOnTargetAxis/ASelectorThatDoesNotMatchTheHostImposesNothing", [] { constexpr auto src = R"( [package] name = "adapter" @@ -308,10 +306,10 @@ requires_abi = { threads = true } // calls out explicitly (A6: "the Web build is unaffected ... in both // directions"). auto lin = manifest_abi::merged_for(src, "x86_64-unknown-linux-gnu"); - EXPECT_TRUE(lin.targetRequiresAbiThreads.empty()); -} + expect(lin.targetRequiresAbiThreads.empty()); +}}; -TEST(RequiresAbiOnTargetAxis, FeatureFormUnionsOnlyForTheMatchingSelector) { +UTest feature_form_unions_only_for_the_matching_selector{"RequiresAbiOnTargetAxis/FeatureFormUnionsOnlyForTheMatchingSelector", [] { constexpr auto src = R"( [package] name = "adapter" @@ -322,7 +320,7 @@ mt = { threads = true } mt = { exceptions = true } )"; auto lin = manifest_abi::merged_for(src, "x86_64-unknown-linux-gnu"); - ASSERT_TRUE(lin.targetFeatureRequiresAbiThreads.contains("mt")); - EXPECT_EQ(lin.targetFeatureRequiresAbiThreads.at("mt").front(), "cfg(linux)"); - EXPECT_FALSE(lin.targetFeatureRequiresAbiExceptions.contains("mt")); -} + expect(fatal(lin.targetFeatureRequiresAbiThreads.contains("mt"))); + expect(eq(lin.targetFeatureRequiresAbiThreads.at("mt").front(), "cfg(linux)")); + expect(!lin.targetFeatureRequiresAbiExceptions.contains("mt")); +}}; diff --git a/tests/unit/test_abi_tag_accel.cpp b/tests/unit/test_abi_tag_accel.cpp index 8cc35dcfd..ed866aa1e 100644 --- a/tests/unit/test_abi_tag_accel.cpp +++ b/tests/unit/test_abi_tag_accel.cpp @@ -1,8 +1,10 @@ -#include import std; +import mcpplibs.mtest; import mcpp.pack.abi_tag; +using namespace mcpplibs::mtest; + using mcpp::pack::AbiTag; using mcpp::pack::AccelSet; using mcpp::pack::tag_check; @@ -31,109 +33,109 @@ bool accepts(const AbiTag& published, const AbiTag& current) { // ─── The published side constrains; an absent dimension does not ─────────── -TEST(AbiTagAccel, PublishedWithoutAccelAcceptsAnyRequest) { +UTest published_without_accel_accepts_any_request{"AbiTagAccel/PublishedWithoutAccelAcceptsAnyRequest", [] { // A CPU-only library states nothing about accelerators, so it is usable // everywhere. This is the existing "a shorter tag IS the statement" rule // reaching one dimension further. - EXPECT_TRUE(accepts(cxx(), with({cuda({"sm_86"})}))); -} + expect(bool(accepts(cxx(), with({cuda({"sm_86"})})))); +}}; -TEST(AbiTagAccel, ARequestWithNoAcceleratorIsSatisfiedVacuously) { - EXPECT_TRUE(accepts(with({cuda({"sm_90"})}), cxx())); -} +UTest a_request_with_no_accelerator_is_satisfied_vacuously{"AbiTagAccel/ARequestWithNoAcceleratorIsSatisfiedVacuously", [] { + expect(bool(accepts(with({cuda({"sm_90"})}), cxx()))); +}}; // ─── Membership ──────────────────────────────────────────────────────────── -TEST(AbiTagAccel, ExactArchIsAccepted) { - EXPECT_TRUE(accepts(with({cuda({"sm_80", "sm_90"})}), with({cuda({"sm_90"})}))); -} +UTest exact_arch_is_accepted{"AbiTagAccel/ExactArchIsAccepted", [] { + expect(bool(accepts(with({cuda({"sm_80", "sm_90"})}), with({cuda({"sm_90"})})))); +}}; -TEST(AbiTagAccel, AnArchOutsideTheSetIsRefused) { +UTest an_arch_outside_the_set_is_refused{"AbiTagAccel/AnArchOutsideTheSetIsRefused", [] { auto bad = tag_check(with({cuda({"sm_80", "sm_90"})}), with({cuda({"sm_86"})})); - ASSERT_EQ(bad.size(), 1u); - EXPECT_EQ(bad[0].dimension, "accel"); - EXPECT_NE(bad[0].need.find("sm_80"), std::string::npos); - EXPECT_NE(bad[0].got.find("sm_86"), std::string::npos); -} + expect(fatal(eq(bad.size(), 1u))); + expect(eq(bad[0].dimension, "accel")); + expect(ne(bad[0].need.find("sm_80"), std::string::npos)); + expect(ne(bad[0].got.find("sm_86"), std::string::npos)); +}}; -TEST(AbiTagAccel, ADifferentBackendIsRefused) { +UTest a_different_backend_is_refused{"AbiTagAccel/ADifferentBackendIsRefused", [] { AccelSet rocm{ .backend = "rocm", .version = "6.4", .archs = {"gfx942"} }; auto bad = tag_check(with({rocm}), with({cuda({"sm_90"})})); - ASSERT_EQ(bad.size(), 1u); - EXPECT_NE(bad[0].need.find("rocm"), std::string::npos); -} + expect(fatal(eq(bad.size(), 1u))); + expect(ne(bad[0].need.find("rocm"), std::string::npos)); +}}; // ─── Family targets widen the published side ─────────────────────────────── -TEST(AbiTagAccel, AFamilyTargetCoversItsFamily) { +UTest a_family_target_covers_its_family{"AbiTagAccel/AFamilyTargetCoversItsFamily", [] { // sm_90f is compatible with the same major and an equal-or-higher minor. // This is why publishing a family target rather than one artifact per chip // is what keeps the variant matrix finite. - EXPECT_TRUE (accepts(with({cuda({"sm_90f"})}), with({cuda({"sm_90"})}))); - EXPECT_TRUE (accepts(with({cuda({"sm_90f"})}), with({cuda({"sm_93"})}))); - EXPECT_FALSE(accepts(with({cuda({"sm_90f"})}), with({cuda({"sm_89"})}))); - EXPECT_FALSE(accepts(with({cuda({"sm_90f"})}), with({cuda({"sm_100"})}))); -} + expect(bool(accepts(with({cuda({"sm_90f"})}), with({cuda({"sm_90"})})))); + expect(bool(accepts(with({cuda({"sm_90f"})}), with({cuda({"sm_93"})})))); + expect(!(accepts(with({cuda({"sm_90f"})}), with({cuda({"sm_89"})})))); + expect(!(accepts(with({cuda({"sm_90f"})}), with({cuda({"sm_100"})})))); +}}; -TEST(AbiTagAccel, AnArchitectureSpecificTargetCoversOnlyItself) { - EXPECT_TRUE (accepts(with({cuda({"sm_90a"})}), with({cuda({"sm_90"})}))); - EXPECT_FALSE(accepts(with({cuda({"sm_90a"})}), with({cuda({"sm_93"})}))); -} +UTest an_architecture_specific_target_covers_only_itself{"AbiTagAccel/AnArchitectureSpecificTargetCoversOnlyItself", [] { + expect(bool(accepts(with({cuda({"sm_90a"})}), with({cuda({"sm_90"})})))); + expect(!(accepts(with({cuda({"sm_90a"})}), with({cuda({"sm_93"})})))); +}}; // ─── The PTX floor is the second asymmetric dimension ────────────────────── -TEST(AbiTagAccel, EmbeddedPtxAcceptsAnythingAtOrAboveTheFloor) { +UTest embedded_ptx_accepts_anything_at_or_above_the_floor{"AbiTagAccel/EmbeddedPtxAcceptsAnythingAtOrAboveTheFloor", [] { // NVIDIA's own guidance for a distributed binary is cubins for the known // targets plus PTX for the newest, so that later hardware JITs. Recording // the floor is what lets tag_check say yes to a device the cubin set does // not name, instead of refusing an artifact that genuinely runs. - EXPECT_TRUE (accepts(with({cuda({"sm_80"}, "80")}), with({cuda({"sm_86"})}))); - EXPECT_TRUE (accepts(with({cuda({"sm_80"}, "80")}), with({cuda({"sm_90"})}))); - EXPECT_FALSE(accepts(with({cuda({"sm_86"}, "86")}), with({cuda({"sm_80"})}))); -} + expect(bool(accepts(with({cuda({"sm_80"}, "80")}), with({cuda({"sm_86"})})))); + expect(bool(accepts(with({cuda({"sm_80"}, "80")}), with({cuda({"sm_90"})})))); + expect(!(accepts(with({cuda({"sm_86"}, "86")}), with({cuda({"sm_80"})})))); +}}; -TEST(AbiTagAccel, WithoutPtxThereIsNoForwardCompatibility) { +UTest without_ptx_there_is_no_forward_compatibility{"AbiTagAccel/WithoutPtxThereIsNoForwardCompatibility", [] { // AMD has no PTX equivalent, so an empty floor must not silently widen the // set. Family coverage on the published side is how ROCm gets the same // effect, and that is a different mechanism. - EXPECT_FALSE(accepts(with({cuda({"sm_80"})}), with({cuda({"sm_86"})}))); -} + expect(!(accepts(with({cuda({"sm_80"})}), with({cuda({"sm_86"})})))); +}}; // ─── Toolkit version ─────────────────────────────────────────────────────── -TEST(AbiTagAccel, TheToolkitMajorMustAgree) { +UTest the_toolkit_major_must_agree{"AbiTagAccel/TheToolkitMajorMustAgree", [] { AccelSet built{ .backend = "cuda", .version = "12.8", .archs = {"sm_90"} }; AccelSet want12{ .backend = "cuda", .version = "12.0", .archs = {"sm_90"} }; AccelSet want13{ .backend = "cuda", .version = "13.0", .archs = {"sm_90"} }; // Minor version compatibility is real within a major release family. - EXPECT_TRUE (accepts(with({built}), with({want12}))); - EXPECT_FALSE(accepts(with({built}), with({want13}))); -} + expect(bool(accepts(with({built}), with({want12})))); + expect(!(accepts(with({built}), with({want13})))); +}}; // ─── Several backends in one artifact ────────────────────────────────────── -TEST(AbiTagAccel, OneArtifactCanCarrySeveralBackends) { +UTest one_artifact_can_carry_several_backends{"AbiTagAccel/OneArtifactCanCarrySeveralBackends", [] { AccelSet rocm{ .backend = "rocm", .version = "6.4", .archs = {"gfx942"} }; auto fat = with({cuda({"sm_90f"}), rocm}); - EXPECT_TRUE(accepts(fat, with({cuda({"sm_90"})}))); + expect(bool(accepts(fat, with({cuda({"sm_90"})})))); AccelSet wantRocm{ .backend = "rocm", .version = "6.4", .archs = {"gfx942"} }; - EXPECT_TRUE(accepts(fat, with({wantRocm}))); -} + expect(bool(accepts(fat, with({wantRocm})))); +}}; // ─── The other four dimensions still decide ──────────────────────────────── -TEST(AbiTagAccel, AccelDoesNotMaskTheCxxDimensions) { +UTest accel_does_not_mask_the_cxx_dimensions{"AbiTagAccel/AccelDoesNotMaskTheCxxDimensions", [] { auto published = with({cuda({"sm_90"})}); auto current = with({cuda({"sm_90"})}); current.compiler = "gcc15"; auto bad = tag_check(published, current); - ASSERT_EQ(bad.size(), 1u); - EXPECT_EQ(bad[0].dimension, "compiler"); -} + expect(fatal(eq(bad.size(), 1u))); + expect(eq(bad[0].dimension, "compiler")); +}}; // ─── The wire form round-trips ───────────────────────────────────────────── -TEST(AbiTagAccel, TheWireFormRoundTrips) { +UTest the_wire_form_round_trips{"AbiTagAccel/TheWireFormRoundTrips", [] { std::vector sets{ cuda({"sm_80", "sm_90f"}, "90"), mcpp::pack::AccelSet{ .backend = "rocm", .version = "6.4", @@ -141,24 +143,24 @@ TEST(AbiTagAccel, TheWireFormRoundTrips) { }; auto text = mcpp::pack::accel_str(sets); auto back = mcpp::pack::parse_accel(text); - ASSERT_EQ(back.size(), 2u); - EXPECT_EQ(back[0].backend, "cuda"); - EXPECT_EQ(back[0].version, "12.8"); - EXPECT_EQ(back[0].archs, (std::vector{"sm_80", "sm_90f"})); - EXPECT_EQ(back[0].ptxFloor, "90"); - EXPECT_EQ(back[1].backend, "rocm"); - EXPECT_EQ(back[1].archs, (std::vector{"gfx942", "gfx10-3-generic"})); - EXPECT_TRUE(back[1].ptxFloor.empty()); - EXPECT_EQ(mcpp::pack::accel_str(back), text); -} - -TEST(AbiTagAccel, UnparseableTextMeansNoDeviceCode) { + expect(fatal(eq(back.size(), 2u))); + expect(eq(back[0].backend, "cuda")); + expect(eq(back[0].version, "12.8")); + expect(eq(back[0].archs, (std::vector{"sm_80", "sm_90f"}))); + expect(eq(back[0].ptxFloor, "90")); + expect(eq(back[1].backend, "rocm")); + expect(eq(back[1].archs, (std::vector{"gfx942", "gfx10-3-generic"}))); + expect(back[1].ptxFloor.empty()); + expect(eq(mcpp::pack::accel_str(back), text)); +}}; + +UTest unparseable_text_means_no_device_code{"AbiTagAccel/UnparseableTextMeansNoDeviceCode", [] { // The safe answer, and the same one a descriptor that never mentioned the // dimension gives: an artifact that states nothing constrains nothing. - EXPECT_TRUE(mcpp::pack::parse_accel("").empty()); - EXPECT_TRUE(mcpp::pack::parse_accel("(none)").empty()); - EXPECT_TRUE(mcpp::pack::parse_accel("12.8+{sm_90}").empty()); // no backend -} + expect(mcpp::pack::parse_accel("").empty()); + expect(mcpp::pack::parse_accel("(none)").empty()); + expect(mcpp::pack::parse_accel("12.8+{sm_90}").empty()); // no backend +}}; // ── The grammar is open: backends mcpp has never heard of ────────────────── // @@ -168,100 +170,99 @@ TEST(AbiTagAccel, UnparseableTextMeansNoDeviceCode) { // just as importantly, that an unknown arch shape falls back to equality rather // than to some numeric guess. -TEST(AccelOpenGrammar, VulkanSpirvRoundTrips) { +UTest vulkan_spirv_round_trips{"AccelOpenGrammar/VulkanSpirvRoundTrips", [] { auto a = mcpp::pack::parse_accel("vulkan1.3+{spirv1.6}"); - ASSERT_EQ(a.size(), 1u); - EXPECT_EQ(a[0].backend, "vulkan"); - EXPECT_EQ(a[0].version, "1.3"); - ASSERT_EQ(a[0].archs.size(), 1u); - EXPECT_EQ(a[0].archs[0], "spirv1.6"); - EXPECT_EQ(mcpp::pack::accel_str(a), "vulkan1.3+{spirv1.6}"); -} - -TEST(AccelOpenGrammar, SyclTargetRoundTrips) { + expect(fatal(eq(a.size(), 1u))); + expect(eq(a[0].backend, "vulkan")); + expect(eq(a[0].version, "1.3")); + expect(fatal(eq(a[0].archs.size(), 1u))); + expect(eq(a[0].archs[0], "spirv1.6")); + expect(eq(mcpp::pack::accel_str(a), "vulkan1.3+{spirv1.6}")); +}}; + +UTest sycl_target_round_trips{"AccelOpenGrammar/SyclTargetRoundTrips", [] { auto a = mcpp::pack::parse_accel("sycl2020+{spir64,nvptx64-sm_89}"); - ASSERT_EQ(a.size(), 1u); - EXPECT_EQ(a[0].backend, "sycl"); - ASSERT_EQ(a[0].archs.size(), 2u); - EXPECT_EQ(a[0].archs[0], "spir64"); - EXPECT_EQ(a[0].archs[1], "nvptx64-sm_89"); -} - -TEST(AccelOpenGrammar, AnUnknownArchShapeComparesByEquality) { + expect(fatal(eq(a.size(), 1u))); + expect(eq(a[0].backend, "sycl")); + expect(fatal(eq(a[0].archs.size(), 2u))); + expect(eq(a[0].archs[0], "spir64")); + expect(eq(a[0].archs[1], "nvptx64-sm_89")); +}}; + +UTest an_unknown_arch_shape_compares_by_equality{"AccelOpenGrammar/AnUnknownArchShapeComparesByEquality", [] { // `gfx942` carries no leading number, so there is no ordering to read out // of it. Equality is the only question available and it is the one asked -- // rather than reading `942` out of the middle and inventing a level. auto pub = mcpp::pack::parse_accel("hip6.4+{gfx942}"); auto same = mcpp::pack::parse_accel("hip6.4+{gfx942}"); auto diff = mcpp::pack::parse_accel("hip6.4+{gfx1100}"); - EXPECT_TRUE (mcpp::pack::accel_accepts(pub, same)); - EXPECT_FALSE(mcpp::pack::accel_accepts(pub, diff)); -} + expect(bool(mcpp::pack::accel_accepts(pub, same))); + expect(!mcpp::pack::accel_accepts(pub, diff)); +}}; -TEST(AccelOpenGrammar, FloorHasABackendNeutralSpelling) { +UTest floor_has_a_backend_neutral_spelling{"AccelOpenGrammar/FloorHasABackendNeutralSpelling", [] { // `ptx>=` is CUDA's word and the one already published. `floor>=` says the // same thing without borrowing it, so a backend whose portable form is // SPIR-V does not have to write `ptx`. auto neutral = mcpp::pack::parse_accel("vulkan1.3+{spirv1.6} floor>=1.4"); - ASSERT_EQ(neutral.size(), 1u); - EXPECT_EQ(neutral[0].ptxFloor, "1.4"); + expect(fatal(eq(neutral.size(), 1u))); + expect(eq(neutral[0].ptxFloor, "1.4")); auto cuda = mcpp::pack::parse_accel("cuda12.9+{sm_80} ptx>=80"); - ASSERT_EQ(cuda.size(), 1u); - EXPECT_EQ(cuda[0].ptxFloor, "80"); -} + expect(fatal(eq(cuda.size(), 1u))); + expect(eq(cuda[0].ptxFloor, "80")); +}}; -TEST(AccelOpenGrammar, TheExistingPtxSpellingStillParses) { +UTest the_existing_ptx_spelling_still_parses{"AccelOpenGrammar/TheExistingPtxSpellingStillParses", [] { // The negative control for the change above: adding a second spelling must // not have moved the first. Descriptors published before it exist. auto a = mcpp::pack::parse_accel("cuda12.8+{sm_80,sm_90f} ptx>=90"); - ASSERT_EQ(a.size(), 1u); - EXPECT_EQ(a[0].ptxFloor, "90"); - EXPECT_EQ(mcpp::pack::accel_str(a), "cuda12.8+{sm_80,sm_90f} ptx>=90"); -} + expect(fatal(eq(a.size(), 1u))); + expect(eq(a[0].ptxFloor, "90")); + expect(eq(mcpp::pack::accel_str(a), "cuda12.8+{sm_80,sm_90f} ptx>=90")); +}}; -TEST(AccelRequest, RejectsIncompleteSetsAndUnconsumedInput) { +UTest rejects_incomplete_sets_and_unconsumed_input{"AccelRequest/RejectsIncompleteSetsAndUnconsumedInput", [] { for (auto input : {"sycl+{sm_89", "sycl+{sm_89}garbage", "sycl+sm_89", "sycl+{sm_89,,sm_90}", "sycl+{,sm_89}", "sycl+{sm_89,}", "sycl+{{sm_89}}", "sycl,", ",sycl", "sycl ptx>=", "12.8+{sm_89}"}) { const auto parsed = mcpp::pack::parse_accel_request(input); - ASSERT_FALSE(parsed) << input; - EXPECT_NE(parsed.error().find("byte"), std::string::npos) << input; + expect(fatal(!parsed)) << input; + expect(ne(parsed.error().find("byte"), std::string::npos)) << input; } -} +}}; -TEST(AccelRequest, DuplicateImplementationsCannotChangeCoverageByTheirOrder) { +UTest duplicate_implementations_cannot_change_coverage_by_their_order{"AccelRequest/DuplicateImplementationsCannotChangeCoverageByTheirOrder", [] { for (auto input : {"sycl+{sm_89},sycl+{sm_90}", "sycl+{sm_90},sycl+{sm_89}"}) { const auto parsed = mcpp::pack::parse_accel_request(input); - ASSERT_FALSE(parsed); - EXPECT_NE(parsed.error().find("appears more than once"), std::string::npos); - EXPECT_NE(parsed.error().find("sycl"), std::string::npos); + expect(fatal(!parsed)); + expect(ne(parsed.error().find("appears more than once"), std::string::npos)); + expect(ne(parsed.error().find("sycl"), std::string::npos)); } -} +}}; -TEST(AccelRequest, EquivalentSetsHaveOneCanonicalSpelling) { +UTest equivalent_sets_have_one_canonical_spelling{"AccelRequest/EquivalentSetsHaveOneCanonicalSpelling", [] { const auto a = mcpp::pack::parse_accel_request( "sycl+{spir64,nvptx64-sm_90,nvptx64-sm_89,spir64}, cuda12.9+{sm_90,sm_89} ptx>=90"); const auto b = mcpp::pack::parse_accel_request( "cuda12.9+{sm_89,sm_90} floor>=90, sycl+{nvptx64-sm_89,nvptx64-sm_90,spir64}"); - ASSERT_TRUE(a); - ASSERT_TRUE(b); - EXPECT_EQ(mcpp::pack::accel_str(*a), mcpp::pack::accel_str(*b)); - EXPECT_EQ(mcpp::pack::accel_str(*a), - "cuda12.9+{sm_89,sm_90} ptx>=90, sycl+{nvptx64-sm_89,nvptx64-sm_90,spir64}"); -} - -TEST(AccelRequest, RetainsEmptyAndUnconstrainedFormsAndOpenTargetVocabulary) { - EXPECT_TRUE(mcpp::pack::parse_accel_request("")->empty()); - EXPECT_TRUE(mcpp::pack::parse_accel_request(" (none) ")->empty()); + expect(fatal(bool(a))); + expect(fatal(bool(b))); + expect(eq(mcpp::pack::accel_str(*a), mcpp::pack::accel_str(*b))); + expect(eq(mcpp::pack::accel_str(*a), "cuda12.9+{sm_89,sm_90} ptx>=90, sycl+{nvptx64-sm_89,nvptx64-sm_90,spir64}")); +}}; + +UTest retains_empty_and_unconstrained_forms_and_open_target_vocabulary{"AccelRequest/RetainsEmptyAndUnconstrainedFormsAndOpenTargetVocabulary", [] { + expect(mcpp::pack::parse_accel_request("")->empty()); + expect(mcpp::pack::parse_accel_request(" (none) ")->empty()); const auto bare = mcpp::pack::parse_accel_request("sycl"); - ASSERT_TRUE(bare); - ASSERT_EQ(bare->size(), 1u); - EXPECT_TRUE(bare->front().archs.empty()); + expect(fatal(bool(bare))); + expect(fatal(eq(bare->size(), 1u))); + expect(bare->front().archs.empty()); const auto explicitEmpty = mcpp::pack::parse_accel_request("sycl+{}"); - ASSERT_TRUE(explicitEmpty); - EXPECT_EQ(mcpp::pack::accel_str(*bare), mcpp::pack::accel_str(*explicitEmpty)); + expect(fatal(bool(explicitEmpty))); + expect(eq(mcpp::pack::accel_str(*bare), mcpp::pack::accel_str(*explicitEmpty))); const auto opaque = mcpp::pack::parse_accel_request("newmodel3.1+{future-device-v2,domain:opaque}"); - ASSERT_TRUE(opaque); - ASSERT_EQ(opaque->front().archs.size(), 2u); -} + expect(fatal(bool(opaque))); + expect(fatal(eq(opaque->front().archs.size(), 2u))); +}}; diff --git a/tests/unit/test_action_progress.cpp b/tests/unit/test_action_progress.cpp index a7b72ba41..d600a9183 100644 --- a/tests/unit/test_action_progress.cpp +++ b/tests/unit/test_action_progress.cpp @@ -1,18 +1,20 @@ -#include import std; +import mcpplibs.mtest; import mcpp.build.action_progress; + +using namespace mcpplibs::mtest; namespace telemetry = mcpp::build::action_progress; -TEST(ActionProgress, AcceptsUnknownTotalsAndExplicitCounts) { +UTest accepts_unknown_totals_and_explicit_counts{"ActionProgress/AcceptsUnknownTotalsAndExplicitCounts", [] { const auto unknown = telemetry::parse(R"({"v":1,"seq":0,"phase":"configure","detail":"compiler checks"})"); - ASSERT_TRUE(unknown); - EXPECT_FALSE(unknown->total); - EXPECT_EQ(telemetry::describe(*unknown), "configure · compiler checks"); + expect(fatal(bool(unknown))); + expect(!unknown->total); + expect(eq(telemetry::describe(*unknown), "configure · compiler checks")); const auto known = telemetry::parse(R"({"v":1,"seq":7,"phase":"compile","done":3,"total":9})"); - ASSERT_TRUE(known); - EXPECT_EQ(telemetry::describe(*known), "compile 3/9"); -} -TEST(ActionProgress, RejectsInvalidSnapshotsWithoutInferringSuccess) { + expect(fatal(bool(known))); + expect(eq(telemetry::describe(*known), "compile 3/9")); +}}; +UTest rejects_invalid_snapshots_without_inferring_success{"ActionProgress/RejectsInvalidSnapshotsWithoutInferringSuccess", [] { for (const auto text : { R"({"v":2,"seq":1,"phase":"compile"})", R"({"v":1,"seq":-1,"phase":"compile"})", @@ -22,23 +24,23 @@ TEST(ActionProgress, RejectsInvalidSnapshotsWithoutInferringSuccess) { R"({"v":1,"seq":1,"phase":"compile\nforged"})", R"({"v":1,"seq":1,"phase":"compile","detail":"\u001b[31m"})", R"({"v":1,"seq":1,"phase":"compile")"}) - EXPECT_FALSE(telemetry::parse(text)) << text; - EXPECT_FALSE(telemetry::parse(std::string(telemetry::kMaxBytes + 1, ' '))); + expect(!telemetry::parse(text)) << text; + expect(!(telemetry::parse(std::string(telemetry::kMaxBytes + 1, ' ')))); // Finishing internal work is data, not a successful command or a stamp. - EXPECT_TRUE(telemetry::parse(R"({"v":1,"seq":2,"phase":"link","done":1,"total":1})")); -} -TEST(ActionProgress, AllocatesIndependentRunDirectoriesAndBoundsReads) { + expect(bool(telemetry::parse(R"({"v":1,"seq":2,"phase":"link","done":1,"total":1})"))); +}}; +UTest allocates_independent_run_directories_and_bounds_reads{"ActionProgress/AllocatesIndependentRunDirectoriesAndBoundsReads", [] { const auto base = std::filesystem::temp_directory_path(); const auto first = telemetry::make_directory(base); const auto second = telemetry::make_directory(base); - ASSERT_FALSE(first.empty()); ASSERT_FALSE(second.empty()); - EXPECT_NE(first, second); + expect(fatal(!first.empty())); expect(fatal(!second.empty())); + expect(ne(first, second)); const auto a = telemetry::channel(first, "obj/a.o"); - EXPECT_NE(a, telemetry::channel(first, "obj/b.o")); - EXPECT_NE(a, telemetry::channel(second, "obj/a.o")); + expect(ne(a, telemetry::channel(first, "obj/b.o"))); + expect(ne(a, telemetry::channel(second, "obj/a.o"))); std::ofstream(a) << std::string(telemetry::kMaxBytes + 1, ' '); - EXPECT_FALSE(telemetry::read(a)); + expect(!telemetry::read(a)); std::ofstream(a) << R"({"v":1,"seq":0,"phase":"work"})"; - EXPECT_TRUE(telemetry::read(a)); + expect(bool(telemetry::read(a))); std::filesystem::remove_all(first); std::filesystem::remove_all(second); -} +}}; diff --git a/tests/unit/test_artifact_naming.cpp b/tests/unit/test_artifact_naming.cpp index e14b539a0..88420e48b 100644 --- a/tests/unit/test_artifact_naming.cpp +++ b/tests/unit/test_artifact_naming.cpp @@ -1,10 +1,12 @@ -#include import std; +import mcpplibs.mtest; import mcpplibs.os.platform; import mcpp.toolchain.platform; import mcpp.toolchain.triple; +using namespace mcpplibs::mtest; + // How a built artifact is NAMED is a property of the TARGET, never of the // machine doing the build. // @@ -36,127 +38,127 @@ tr::Triple T(std::string_view s) { // ── The regression: naming must not come from the build host ──────────────── -TEST(ArtifactNaming, LinuxTargetIgnoresHostAnswer) { +UTest linux_target_ignores_host_answer{"ArtifactNaming/LinuxTargetIgnoresHostAnswer", [] { auto n = tr::artifact_naming(T("x86_64-linux-musl"), kBogusHost); - EXPECT_EQ(n.exeSuffix, ""); - EXPECT_EQ(n.libPrefix, "lib"); - EXPECT_EQ(n.staticLibExt, ".a"); - EXPECT_EQ(n.sharedLibExt, ".so"); - EXPECT_FALSE(n.sharedNeedsImportLib); -} - -TEST(ArtifactNaming, MacosTargetIgnoresHostAnswer) { + expect(eq(n.exeSuffix, "")); + expect(eq(n.libPrefix, "lib")); + expect(eq(n.staticLibExt, ".a")); + expect(eq(n.sharedLibExt, ".so")); + expect(!n.sharedNeedsImportLib); +}}; + +UTest macos_target_ignores_host_answer{"ArtifactNaming/MacosTargetIgnoresHostAnswer", [] { auto n = tr::artifact_naming(T("aarch64-macos"), kBogusHost); - EXPECT_EQ(n.exeSuffix, ""); - EXPECT_EQ(n.libPrefix, "lib"); - EXPECT_EQ(n.staticLibExt, ".a"); - EXPECT_EQ(n.sharedLibExt, ".dylib"); - EXPECT_FALSE(n.sharedNeedsImportLib); -} + expect(eq(n.exeSuffix, "")); + expect(eq(n.libPrefix, "lib")); + expect(eq(n.staticLibExt, ".a")); + expect(eq(n.sharedLibExt, ".dylib")); + expect(!n.sharedNeedsImportLib); +}}; // ── The (os, env) split — the part a single _WIN32 branch cannot express ───── // // windows-gnu uses the GNU convention. mcpp names this `foo.lib` today even on // a Windows host, so mingw's `ar` emits a GNU archive wearing an MSVC name. // That is a pre-existing defect, independent of cross compilation. -TEST(ArtifactNaming, WindowsGnuUsesGnuConvention) { +UTest windows_gnu_uses_gnu_convention{"ArtifactNaming/WindowsGnuUsesGnuConvention", [] { auto n = tr::artifact_naming(T("x86_64-windows-gnu"), kBogusHost); - EXPECT_EQ(n.exeSuffix, ".exe"); - EXPECT_EQ(n.libPrefix, "lib"); // NOT "" - EXPECT_EQ(n.staticLibExt, ".a"); // NOT ".lib" - EXPECT_EQ(n.sharedLibExt, ".dll"); - EXPECT_TRUE(n.sharedNeedsImportLib); -} - -TEST(ArtifactNaming, WindowsMsvcUsesMsvcConvention) { + expect(eq(n.exeSuffix, ".exe")); + expect(eq(n.libPrefix, "lib")); // NOT "" + expect(eq(n.staticLibExt, ".a")); // NOT ".lib" + expect(eq(n.sharedLibExt, ".dll")); + expect(bool(n.sharedNeedsImportLib)); +}}; + +UTest windows_msvc_uses_msvc_convention{"ArtifactNaming/WindowsMsvcUsesMsvcConvention", [] { auto n = tr::artifact_naming(T("x86_64-windows-msvc"), kBogusHost); - EXPECT_EQ(n.exeSuffix, ".exe"); - EXPECT_EQ(n.libPrefix, ""); - EXPECT_EQ(n.staticLibExt, ".lib"); - EXPECT_EQ(n.sharedLibExt, ".dll"); - EXPECT_TRUE(n.sharedNeedsImportLib); -} + expect(eq(n.exeSuffix, ".exe")); + expect(eq(n.libPrefix, "")); + expect(eq(n.staticLibExt, ".lib")); + expect(eq(n.sharedLibExt, ".dll")); + expect(bool(n.sharedNeedsImportLib)); +}}; // The legacy mingw spelling must resolve identically to the canonical one — // the triple parser is the single source of truth, not a substring match. -TEST(ArtifactNaming, LegacyMingwSpellingMatchesCanonical) { +UTest legacy_mingw_spelling_matches_canonical{"ArtifactNaming/LegacyMingwSpellingMatchesCanonical", [] { auto legacy = tr::artifact_naming(T("x86_64-w64-mingw32"), kBogusHost); auto canonical = tr::artifact_naming(T("x86_64-windows-gnu"), kBogusHost); - EXPECT_EQ(legacy.exeSuffix, canonical.exeSuffix); - EXPECT_EQ(legacy.libPrefix, canonical.libPrefix); - EXPECT_EQ(legacy.staticLibExt, canonical.staticLibExt); - EXPECT_EQ(legacy.sharedLibExt, canonical.sharedLibExt); -} + expect(eq(legacy.exeSuffix, canonical.exeSuffix)); + expect(eq(legacy.libPrefix, canonical.libPrefix)); + expect(eq(legacy.staticLibExt, canonical.staticLibExt)); + expect(eq(legacy.sharedLibExt, canonical.sharedLibExt)); +}}; // ── Host target: the one case where the host answer IS correct ────────────── -TEST(ArtifactNaming, EmptyTripleFallsBackToHost) { +UTest empty_triple_falls_back_to_host{"ArtifactNaming/EmptyTripleFallsBackToHost", [] { auto n = tr::artifact_naming(tr::Triple{}, kBogusHost); - EXPECT_EQ(n.exeSuffix, ".HOST"); - EXPECT_EQ(n.libPrefix, "HOST"); -} + expect(eq(n.exeSuffix, ".HOST")); + expect(eq(n.libPrefix, "HOST")); +}}; // An unparseable triple must not be guessed at — a wrong guess here silently // misnames every artifact of the build. -TEST(ArtifactNaming, UnknownOsFallsBackToHost) { +UTest unknown_os_falls_back_to_host{"ArtifactNaming/UnknownOsFallsBackToHost", [] { tr::Triple wasm; wasm.arch = "wasm32"; wasm.os = "unknown"; auto n = tr::artifact_naming(wasm, kBogusHost); - EXPECT_EQ(n.exeSuffix, ".HOST"); - EXPECT_EQ(n.staticLibExt, ".HOST"); -} + expect(eq(n.exeSuffix, ".HOST")); + expect(eq(n.staticLibExt, ".HOST")); +}}; // A bare-metal `none` OS is not the emscripten row and must keep falling back // to the host answer — the emscripten branch below must not widen the // fallback it sits next to. -TEST(ArtifactNaming, NoneOsFallsBackToHost) { +UTest none_os_falls_back_to_host{"ArtifactNaming/NoneOsFallsBackToHost", [] { tr::Triple none; none.arch = "aarch64"; none.os = "none"; auto n = tr::artifact_naming(none, kBogusHost); - EXPECT_EQ(n.exeSuffix, ".HOST"); - EXPECT_EQ(n.staticLibExt, ".HOST"); -} + expect(eq(n.exeSuffix, ".HOST")); + expect(eq(n.staticLibExt, ".HOST")); +}}; // ── wasm32-emscripten: the executable is the file a runner executes, and on // this row that file is the JavaScript launcher, not a bare ELF-shaped name. // See .agents/docs/2026-09-12-622-a-ui-framework-on-android-ios-and-web.md §2.5. // A Linux host must not leak its own (empty-suffix) naming onto the row. -TEST(ArtifactNaming, EmscriptenOnLinuxHostNamesJs) { +UTest emscripten_on_linux_host_names_js{"ArtifactNaming/EmscriptenOnLinuxHostNamesJs", [] { const tr::ArtifactNaming linuxHost{ .exeSuffix = "", .libPrefix = "lib", .staticLibExt = ".a", .sharedLibExt = ".so", .sharedNeedsImportLib = false, }; auto n = tr::artifact_naming(T("wasm32-emscripten"), linuxHost); - EXPECT_EQ(n.exeSuffix, ".js"); - EXPECT_EQ(n.libPrefix, "lib"); - EXPECT_EQ(n.staticLibExt, ".a"); -} + expect(eq(n.exeSuffix, ".js")); + expect(eq(n.libPrefix, "lib")); + expect(eq(n.staticLibExt, ".a")); +}}; // A Windows host must not leak `.exe` onto the row either — the row names one // file on every host. -TEST(ArtifactNaming, EmscriptenOnWindowsHostNamesJs) { +UTest emscripten_on_windows_host_names_js{"ArtifactNaming/EmscriptenOnWindowsHostNamesJs", [] { const tr::ArtifactNaming windowsHost{ .exeSuffix = ".exe", .libPrefix = "", .staticLibExt = ".lib", .sharedLibExt = ".dll", .sharedNeedsImportLib = true, }; auto n = tr::artifact_naming(T("wasm32-emscripten"), windowsHost); - EXPECT_EQ(n.exeSuffix, ".js"); - EXPECT_EQ(n.libPrefix, "lib"); - EXPECT_EQ(n.staticLibExt, ".a"); -} + expect(eq(n.exeSuffix, ".js")); + expect(eq(n.libPrefix, "lib")); + expect(eq(n.staticLibExt, ".a")); +}}; // A side module needs `-sSIDE_MODULE`, which mcpp does not render — the row // marks shared libraries unsupported rather than naming a file emcc's default // link would not actually produce as a working shared object. -TEST(ArtifactNaming, EmscriptenSharedIsUnsupported) { +UTest emscripten_shared_is_unsupported{"ArtifactNaming/EmscriptenSharedIsUnsupported", [] { auto n = tr::artifact_naming(T("wasm32-emscripten"), kBogusHost); - EXPECT_TRUE(n.sharedNeedsImportLib); - EXPECT_EQ(n.sharedLibExt, ""); -} + expect(bool(n.sharedNeedsImportLib)); + expect(eq(n.sharedLibExt, "")); +}}; // ── Host builds must be bit-for-bit unchanged ─────────────────────────────── // Passing the real host constants for the host target has to reproduce exactly // what the old code produced on this machine. -TEST(ArtifactNaming, RealHostConstantsRoundTrip) { +UTest real_host_constants_round_trip{"ArtifactNaming/RealHostConstantsRoundTrip", [] { const tr::ArtifactNaming host{ .exeSuffix = mcpplibs::os::exe_suffix, .libPrefix = mcpp::toolchain::platform::lib_prefix, @@ -165,11 +167,11 @@ TEST(ArtifactNaming, RealHostConstantsRoundTrip) { .sharedNeedsImportLib = mcpplibs::os::is_windows, }; auto n = tr::artifact_naming(tr::Triple{}, host); - EXPECT_EQ(n.exeSuffix, mcpplibs::os::exe_suffix); - EXPECT_EQ(n.libPrefix, mcpp::toolchain::platform::lib_prefix); - EXPECT_EQ(n.staticLibExt, mcpp::toolchain::platform::static_lib_ext); - EXPECT_EQ(n.sharedLibExt, mcpp::toolchain::platform::shared_lib_ext); -} + expect(eq(n.exeSuffix, mcpplibs::os::exe_suffix)); + expect(eq(n.libPrefix, mcpp::toolchain::platform::lib_prefix)); + expect(eq(n.staticLibExt, mcpp::toolchain::platform::static_lib_ext)); + expect(eq(n.sharedLibExt, mcpp::toolchain::platform::shared_lib_ext)); +}}; // ── #622 A3: the link form of `kind = "app"` is a property of the row ─────── // @@ -180,44 +182,38 @@ TEST(ArtifactNaming, RealHostConstantsRoundTrip) { // `bin`. The negative direction here is the positive direction of every OTHER // row — a form function with only one answer would not be a function. -TEST(ArtifactNaming, ApplicationFormIsSharedObjectOnAndroid) { - EXPECT_EQ(tr::application_form(T("aarch64-linux-android")), - tr::ApplicationForm::SharedObject); - EXPECT_EQ(tr::application_form(T("x86_64-linux-android")), - tr::ApplicationForm::SharedObject); -} +UTest application_form_is_shared_object_on_android{"ArtifactNaming/ApplicationFormIsSharedObjectOnAndroid", [] { + expect(eq(tr::application_form(T("aarch64-linux-android")), tr::ApplicationForm::SharedObject)); + expect(eq(tr::application_form(T("x86_64-linux-android")), tr::ApplicationForm::SharedObject)); +}}; -TEST(ArtifactNaming, ApplicationFormIsExecutableEverywhereElse) { - EXPECT_EQ(tr::application_form(T("x86_64-linux-gnu")), - tr::ApplicationForm::Executable); - EXPECT_EQ(tr::application_form(T("aarch64-ios-sim")), - tr::ApplicationForm::Executable); - EXPECT_EQ(tr::application_form(T("wasm32-emscripten")), - tr::ApplicationForm::Executable); - EXPECT_EQ(tr::application_form(T("x86_64-windows-msvc")), - tr::ApplicationForm::Executable); -} +UTest application_form_is_executable_everywhere_else{"ArtifactNaming/ApplicationFormIsExecutableEverywhereElse", [] { + expect(eq(tr::application_form(T("x86_64-linux-gnu")), tr::ApplicationForm::Executable)); + expect(eq(tr::application_form(T("aarch64-ios-sim")), tr::ApplicationForm::Executable)); + expect(eq(tr::application_form(T("wasm32-emscripten")), tr::ApplicationForm::Executable)); + expect(eq(tr::application_form(T("x86_64-windows-msvc")), tr::ApplicationForm::Executable)); +}}; // An empty (host) triple is not Android on any machine this engine runs on // today -- the negative direction that keeps a bare `kind = "app"` host build // linking an ordinary executable, exactly as it does before this feature. -TEST(ArtifactNaming, ApplicationFormOnTheHostTripleIsExecutable) { - EXPECT_EQ(tr::application_form(tr::Triple{}), tr::ApplicationForm::Executable); -} +UTest application_form_on_the_host_triple_is_executable{"ArtifactNaming/ApplicationFormOnTheHostTripleIsExecutable", [] { + expect(eq(tr::application_form(tr::Triple{}), tr::ApplicationForm::Executable)); +}}; // ── #630 A9: the Android ABI name of a row's architecture ─────────────────── // // The two rows mcpp currently produces a shared object for; a third // architecture keeps its GNU spelling rather than a guessed Android name. -TEST(AndroidAbi, TranslatesTheTwoKnownArchitectures) { - EXPECT_EQ(tr::android_abi(T("aarch64-linux-android")), "arm64-v8a"); - EXPECT_EQ(tr::android_abi(T("x86_64-linux-android")), "x86_64"); -} +UTest translates_the_two_known_architectures{"AndroidAbi/TranslatesTheTwoKnownArchitectures", [] { + expect(eq(tr::android_abi(T("aarch64-linux-android")), "arm64-v8a")); + expect(eq(tr::android_abi(T("x86_64-linux-android")), "x86_64")); +}}; -TEST(AndroidAbi, AnUntranslatedArchitectureKeepsItsGnuSpelling) { - EXPECT_EQ(tr::android_abi(T("riscv64-linux-android")), "riscv64"); -} +UTest an_untranslated_architecture_keeps_its_gnu_spelling{"AndroidAbi/AnUntranslatedArchitectureKeepsItsGnuSpelling", [] { + expect(eq(tr::android_abi(T("riscv64-linux-android")), "riscv64")); +}}; } // namespace @@ -225,27 +221,27 @@ TEST(AndroidAbi, AnUntranslatedArchitectureKeepsItsGnuSpelling) { // platform of its own wins. Every row the engine has maps into the closed // vocabulary `[package] platforms` accepts, so a claim can be made for every // row and for nothing else. -TEST(PlatformName, IsTheOsExceptForAndroid) { +UTest is_the_os_except_for_android{"PlatformName/IsTheOsExceptForAndroid", [] { using mcpp::toolchain::triple::parse; using mcpp::toolchain::triple::platform_name; - EXPECT_EQ(platform_name(*parse("x86_64-linux-gnu")), "linux"); - EXPECT_EQ(platform_name(*parse("aarch64-macos")), "macos"); - EXPECT_EQ(platform_name(*parse("x86_64-windows-msvc")), "windows"); - EXPECT_EQ(platform_name(*parse("aarch64-ios-sim")), "ios"); - EXPECT_EQ(platform_name(*parse("aarch64-linux-android")), "android"); - EXPECT_EQ(platform_name(*parse("wasm32-emscripten")), "emscripten"); -} - -TEST(PlatformName, EveryKnownRowIsInTheVocabulary) { + expect(eq(platform_name(*parse("x86_64-linux-gnu")), "linux")); + expect(eq(platform_name(*parse("aarch64-macos")), "macos")); + expect(eq(platform_name(*parse("x86_64-windows-msvc")), "windows")); + expect(eq(platform_name(*parse("aarch64-ios-sim")), "ios")); + expect(eq(platform_name(*parse("aarch64-linux-android")), "android")); + expect(eq(platform_name(*parse("wasm32-emscripten")), "emscripten")); +}}; + +UTest every_known_row_is_in_the_vocabulary{"PlatformName/EveryKnownRowIsInTheVocabulary", [] { using namespace mcpp::toolchain::triple; for (auto const& row : known_targets()) { auto t = parse(std::string(row.canonical)); - ASSERT_TRUE(t.has_value()) << row.canonical; + expect(fatal(t.has_value())) << row.canonical; if (t->os == "none") continue; // bare-metal rows claim no platform - EXPECT_TRUE(is_platform_name(platform_name(*t))) << row.canonical; + expect(is_platform_name(platform_name(*t))) << row.canonical; } - EXPECT_TRUE(is_platform_name("emscripten")); - EXPECT_FALSE(is_platform_name("web")); - EXPECT_TRUE(is_platform_name("openkal")); - EXPECT_EQ(platform_names_joined(), "linux | macos | windows | ios | android | emscripten | openkal"); -} + expect(is_platform_name("emscripten")); + expect(!is_platform_name("web")); + expect(is_platform_name("openkal")); + expect(eq(platform_names_joined(), "linux | macos | windows | ios | android | emscripten | openkal")); +}}; diff --git a/tests/unit/test_bmi_cache.cpp b/tests/unit/test_bmi_cache.cpp index 4214e4814..2970585e9 100644 --- a/tests/unit/test_bmi_cache.cpp +++ b/tests/unit/test_bmi_cache.cpp @@ -1,4 +1,3 @@ -#include #include #if !defined(_WIN32) #include @@ -6,6 +5,7 @@ #endif import std; +import mcpplibs.mtest; import mcpp.bmi_cache; import mcpp.bmi_cache.maintenance; import mcpp.home; @@ -15,19 +15,21 @@ import mcpplibs.os.env; import mcpplibs.os.platform; import mcpplibs.os.windows; +using namespace mcpplibs::mtest; + using namespace mcpp::bmi_cache; -TEST(BMICacheMaintenance, RunnerCachePathDoesNotUseTheWindowsAnsiCodePage) { +UTest runner_cache_path_does_not_use_the_windows_ansi_code_page{"BMICacheMaintenance/RunnerCachePathDoesNotUseTheWindowsAnsiCodePage", [] { if constexpr (!mcpplibs::os::is_windows) { - GTEST_SKIP() << "path::string() only converts through an ANSI code page on Windows"; + skip("path::string() only converts through an ANSI code page on Windows"); } else { const auto acp = mcpplibs::os::windows::code_page(); // GetACP, including a UTF-8 process manifest. - if (acp == 65001) GTEST_SKIP() << "UTF-8 ACP can represent every path"; + if (acp == 65001) skip("UTF-8 ACP can represent every path"); const std::filesystem::path name(std::wstring{wchar_t(0x0915), wchar_t(0x0916), wchar_t(0x0917)}); bool narrowing_throws = false; try { (void)name.string(); } catch (const std::exception&) { narrowing_throws = true; } - ASSERT_TRUE(narrowing_throws) << "the fixture must be unrepresentable in ACP " << acp; + expect(fatal(bool(narrowing_throws))) << "the fixture must be unrepresentable in ACP " << acp; const auto scratch = std::filesystem::temp_directory_path() / std::format("mcpp-runner-cache-{}", std::random_device{}()); @@ -42,12 +44,12 @@ TEST(BMICacheMaintenance, RunnerCachePathDoesNotUseTheWindowsAnsiCodePage) { // to find these names: gc still reports that outcome without throwing // while preparing the native executable path or its diagnostic. const auto where = mcpplibs::os::env::find_executable("where.exe"); - ASSERT_TRUE(where.has_value()); + expect(fatal(where.has_value())); std::filesystem::copy_file(*where, bin / "okrun.exe"); mcpp::invocation::ScopedEnv selected("MCPP_HOME", mcpp::home::spelling(home)); - EXPECT_NO_THROW({ EXPECT_EQ(cache_gc("", "3650d"), 0); }); + expect(nothrow([&] { { expect(eq(cache_gc("", "3650d"), 0)); }; })); } -} +}}; namespace { @@ -121,32 +123,32 @@ void seedProject(const std::filesystem::path& project) { } // namespace -TEST(BmiCache, EntryDirIsKeyedByPackageIdentityAndKeyNotByProject) { +UTest entry_dir_is_keyed_by_package_identity_and_key_not_by_project{"BmiCache/EntryDirIsKeyedByPackageIdentityAndKeyNotByProject", [] { auto k = makeKey("/home/u/.mcpp/build-cache/v1"); auto expected = std::filesystem::path( "/home/u/.mcpp/build-cache/v1/pkg/mcpplibs/mcpplibs.cmdline@0.0.1/deadbeef0123abcd"); - EXPECT_EQ(k.dir(), expected); - EXPECT_EQ(k.entryFile().filename().string(), "entry.json"); - EXPECT_EQ(k.bmiDir().filename().string(), "bmi"); - EXPECT_EQ(k.objDir().filename().string(), "obj"); -} + expect(eq(k.dir(), expected)); + expect(eq(k.entryFile().filename().string(), "entry.json")); + expect(eq(k.bmiDir().filename().string(), "bmi")); + expect(eq(k.objDir().filename().string(), "obj")); +}}; // The layout version must be a path segment: replacing the layout has to be a // new tree, never a migration of (or a deletion of) the old one. -TEST(BmiCache, EntryDirCarriesTheLayoutVersion) { +UTest entry_dir_carries_the_layout_version{"BmiCache/EntryDirCarriesTheLayoutVersion", [] { auto k = makeKey("/home/u/.mcpp/build-cache/v1"); auto s = k.dir().generic_string(); - EXPECT_NE(s.find("/build-cache/v1/pkg/"), std::string::npos) << s; + expect(ne(s.find("/build-cache/v1/pkg/"), std::string::npos)) << s; // And nothing may land under the pre-v1 root. - EXPECT_EQ(s.find("/.mcpp/bmi/"), std::string::npos) << s; -} + expect(eq(s.find("/.mcpp/bmi/"), std::string::npos)) << s; +}}; -TEST(BmiCache, IsCachedFalseWhenEntryMissing) { +UTest is_cached_false_when_entry_missing{"BmiCache/IsCachedFalseWhenEntryMissing", [] { Tmp t; - EXPECT_FALSE(is_cached(makeKey(t.path), oneOfEach())); -} + expect(!(is_cached(makeKey(t.path), oneOfEach()))); +}}; -TEST(BmiCache, PopulateWritesSelfDescribingEntry) { +UTest populate_writes_self_describing_entry{"BmiCache/PopulateWritesSelfDescribingEntry", [] { Tmp t; auto home = t.path / "home"; auto project = t.path / "proj" / "target"; @@ -154,106 +156,106 @@ TEST(BmiCache, PopulateWritesSelfDescribingEntry) { auto k = makeKey(home); auto pop = populate_from(k, project, oneOfEach()); - ASSERT_TRUE(pop) << pop.error(); + if (auto&& failed = expect(fatal(bool(pop))); !failed) failed << pop.error(); - ASSERT_TRUE(std::filesystem::exists(k.entryFile())); - EXPECT_TRUE(std::filesystem::exists(k.bmiDir() / "lib.gcm")); - EXPECT_TRUE(std::filesystem::exists(k.objDir() / "lib.m.o")); - EXPECT_TRUE(is_cached(k, oneOfEach())); + expect(fatal(std::filesystem::exists(k.entryFile()))); + expect(std::filesystem::exists(k.bmiDir() / "lib.gcm")); + expect(std::filesystem::exists(k.objDir() / "lib.m.o")); + expect(is_cached(k, oneOfEach())); // The entry has to carry the key AND the full inputs: a hit is validated // field by field, so a cache whose entries only listed files could never be // audited when a wrong hit was suspected. auto j = readJson(k.entryFile()); - EXPECT_EQ(j.value("key", std::string{}), "deadbeef0123abcd"); - EXPECT_EQ(j["inputs"], makeInputs()); - EXPECT_EQ(j["bmi"].size(), 1u); - EXPECT_EQ(j["obj"].size(), 1u); - EXPECT_TRUE(j.contains("created")); - EXPECT_TRUE(j.contains("accessed")); -} + expect(eq(j.value("key", std::string{}), "deadbeef0123abcd")); + expect(eq(j["inputs"], makeInputs())); + expect(eq(j["bmi"].size(), 1u)); + expect(eq(j["obj"].size(), 1u)); + expect(j.contains("created")); + expect(j.contains("accessed")); +}}; // The whole point of recording inputs: equal hashes are never trusted on their // own. Same key directory, different inputs ⇒ miss. -TEST(BmiCache, IsCachedFalseWhenRecordedInputsDiffer) { +UTest is_cached_false_when_recorded_inputs_differ{"BmiCache/IsCachedFalseWhenRecordedInputsDiffer", [] { Tmp t; auto home = t.path / "home"; auto project = t.path / "proj" / "target"; seedProject(project); auto written = makeKey(home, "samekey00000000", "base"); - ASSERT_TRUE(populate_from(written, project, oneOfEach())); - EXPECT_TRUE(is_cached(written, oneOfEach())); + expect(fatal(bool(populate_from(written, project, oneOfEach())))); + expect(is_cached(written, oneOfEach())); auto probed = makeKey(home, "samekey00000000", "different-flag"); - EXPECT_FALSE(is_cached(probed, oneOfEach())); -} + expect(!(is_cached(probed, oneOfEach()))); +}}; // An entry written by an older mcpp may carry extra keys, but every field the // current build cares about must be present. Missing ⇒ mismatch, never a pass. -TEST(BmiCache, IsCachedFalseWhenARequiredInputFieldIsAbsent) { +UTest is_cached_false_when_a_required_input_field_is_absent{"BmiCache/IsCachedFalseWhenARequiredInputFieldIsAbsent", [] { Tmp t; auto home = t.path / "home"; auto project = t.path / "proj" / "target"; seedProject(project); auto k = makeKey(home); - ASSERT_TRUE(populate_from(k, project, oneOfEach())); + expect(fatal(bool(populate_from(k, project, oneOfEach())))); auto j = readJson(k.entryFile()); j["inputs"].erase("profile"); std::ofstream(k.entryFile()) << j.dump(2); - EXPECT_FALSE(is_cached(k, oneOfEach())); -} + expect(!(is_cached(k, oneOfEach()))); +}}; -TEST(BmiCache, IsCachedFalseWhenSchemaDiffers) { +UTest is_cached_false_when_schema_differs{"BmiCache/IsCachedFalseWhenSchemaDiffers", [] { Tmp t; auto home = t.path / "home"; auto project = t.path / "proj" / "target"; seedProject(project); auto k = makeKey(home); - ASSERT_TRUE(populate_from(k, project, oneOfEach())); + expect(fatal(bool(populate_from(k, project, oneOfEach())))); auto j = readJson(k.entryFile()); j["schema"] = kEntrySchema + 1; std::ofstream(k.entryFile()) << j.dump(2); - EXPECT_FALSE(is_cached(k, oneOfEach())); -} + expect(!(is_cached(k, oneOfEach()))); +}}; -TEST(BmiCache, IsCachedFalseWhenSentinelExistsButFileMissing) { +UTest is_cached_false_when_sentinel_exists_but_file_missing{"BmiCache/IsCachedFalseWhenSentinelExistsButFileMissing", [] { Tmp t; auto home = t.path / "home"; auto project = t.path / "proj" / "target"; seedProject(project); auto k = makeKey(home); - ASSERT_TRUE(populate_from(k, project, oneOfEach())); - ASSERT_TRUE(is_cached(k, oneOfEach())); + expect(fatal(bool(populate_from(k, project, oneOfEach())))); + expect(fatal(is_cached(k, oneOfEach()))); std::filesystem::remove(k.objDir() / "lib.m.o"); - EXPECT_FALSE(is_cached(k, oneOfEach())); -} + expect(!(is_cached(k, oneOfEach()))); +}}; -TEST(BmiCache, ResolveCachedReportsTheArtifactsWithoutCopying) { +UTest resolve_cached_reports_the_artifacts_without_copying{"BmiCache/ResolveCachedReportsTheArtifactsWithoutCopying", [] { Tmp t; auto home = t.path / "home"; auto project = t.path / "proj" / "target"; seedProject(project); auto k = makeKey(home); - ASSERT_TRUE(populate_from(k, project, oneOfEach())); + expect(fatal(bool(populate_from(k, project, oneOfEach())))); auto arts = resolve_cached(k); - ASSERT_TRUE(arts) << arts.error(); - EXPECT_EQ(arts->bmiFiles, std::vector{"lib.gcm"}); - ASSERT_EQ(arts->objFiles.size(), 1u); - EXPECT_EQ(arts->objFiles[0].cacheRel, "lib.m.o"); + if (auto&& failed = expect(fatal(bool(arts))); !failed) failed << arts.error(); + expect(eq(arts->bmiFiles, std::vector{"lib.gcm"})); + expect(fatal(eq(arts->objFiles.size(), 1u))); + expect(eq(arts->objFiles[0].cacheRel, "lib.m.o")); // Read-back never yields a build path: the entry does not record one, which // is the whole point of mcpp#344. - EXPECT_TRUE(arts->objFiles[0].buildRel.empty()); + expect(arts->objFiles[0].buildRel.empty()); // resolve_cached must not write into a project dir. Staging is a ninja edge // now: copying artifacts in from outside the graph is exactly what made the @@ -261,17 +263,17 @@ TEST(BmiCache, ResolveCachedReportsTheArtifactsWithoutCopying) { // record for, so the copies were recompiled over on every fresh build dir. auto project2 = t.path / "proj2" / "target"; (void)resolve_cached(k); - EXPECT_FALSE(std::filesystem::exists(project2)); -} + expect(!std::filesystem::exists(project2)); +}}; -TEST(BmiCache, CachedPathsPointIntoTheEntry) { +UTest cached_paths_point_into_the_entry{"BmiCache/CachedPathsPointIntoTheEntry", [] { auto k = makeKey("/home/u/.mcpp/build-cache/v1"); - EXPECT_EQ(cached_bmi_path(k, "lib.gcm"), k.bmiDir() / "lib.gcm"); - EXPECT_EQ(cached_obj_path(k, "sub/lib.m.o"), k.objDir() / "sub" / "lib.m.o"); -} + expect(eq(cached_bmi_path(k, "lib.gcm"), k.bmiDir() / "lib.gcm")); + expect(eq(cached_obj_path(k, "sub/lib.m.o"), k.objDir() / "sub" / "lib.m.o")); +}}; // Nested object paths (mcpp#233's per-package object prefixes) must round-trip. -TEST(BmiCache, PopulateHandlesNestedObjectPaths) { +UTest populate_handles_nested_object_paths{"BmiCache/PopulateHandlesNestedObjectPaths", [] { Tmp t; auto home = t.path / "home"; auto project = t.path / "proj" / "target"; @@ -280,14 +282,14 @@ TEST(BmiCache, PopulateHandlesNestedObjectPaths) { auto k = makeKey(home); DepArtifacts arts { .objFiles = {{"zlib-1.3/compress.o", "obj/pkg_zlib/zlib-1.3/compress.o"}} }; - ASSERT_TRUE(populate_from(k, project, arts)); + expect(fatal(bool(populate_from(k, project, arts)))); // Stored at the ENTRY address, read from the BUILD path. The entry knows // nothing about the `pkg_zlib/` partition, which is consumer-side. - EXPECT_EQ(readFile(k.objDir() / "zlib-1.3" / "compress.o"), "NESTED"); - EXPECT_TRUE(is_cached(k, arts)); + expect(eq(readFile(k.objDir() / "zlib-1.3" / "compress.o"), "NESTED")); + expect(is_cached(k, arts)); auto j = readJson(k.entryFile()); - EXPECT_EQ(j["obj"][0].get(), "zlib-1.3/compress.o"); -} + expect(eq(j["obj"][0].get(), "zlib-1.3/compress.o")); +}}; // mcpp#344, the load-bearing case. Two consumers of ONE key may place the same // object at different build-dir paths (#233's basename disambiguation fires on @@ -296,7 +298,7 @@ TEST(BmiCache, PopulateHandlesNestedObjectPaths) { // get a clean MISS, with the divergence named — never a hit that goes on to // stage a file the entry never had, which is what ninja reported as // "missing and no known rule to make it" at graph load. -TEST(BmiCache, ProbeMissesWhenTheEntryDoesNotHoldTheRequestedArtifacts) { +UTest probe_misses_when_the_entry_does_not_hold_the_requested_artifacts{"BmiCache/ProbeMissesWhenTheEntryDoesNotHoldTheRequestedArtifacts", [] { Tmp t; auto home = t.path / "home"; auto project = t.path / "proj" / "target"; @@ -306,63 +308,63 @@ TEST(BmiCache, ProbeMissesWhenTheEntryDoesNotHoldTheRequestedArtifacts) { // The first consumer populated the entry under a DIFFERENT internal // address than the one this build is going to ask for. DepArtifacts written { .objFiles = {{"other/compress.o", "obj/compress.o"}} }; - ASSERT_TRUE(populate_from(k, project, written)); + expect(fatal(bool(populate_from(k, project, written)))); DepArtifacts wanted { .objFiles = {{"zlib-1.3/compress.o", "obj/compress.o"}} }; auto probe = probe_cached(k, wanted); - EXPECT_FALSE(probe.ok); - ASSERT_EQ(probe.layoutMismatch.size(), 1u); - EXPECT_EQ(probe.layoutMismatch[0], "zlib-1.3/compress.o"); -} + expect(!probe.ok); + expect(fatal(eq(probe.layoutMismatch.size(), 1u))); + expect(eq(probe.layoutMismatch[0], "zlib-1.3/compress.o")); +}}; // The same asymmetry on the read side: an entry that lists a file it does not // have on disk must not be reported as a layout divergence OR as a hit. -TEST(BmiCache, ProbeMissesWhenARequestedFileIsListedButAbsent) { +UTest probe_misses_when_a_requested_file_is_listed_but_absent{"BmiCache/ProbeMissesWhenARequestedFileIsListedButAbsent", [] { Tmp t; auto home = t.path / "home"; auto project = t.path / "proj" / "target"; seedProject(project); auto k = makeKey(home); - ASSERT_TRUE(populate_from(k, project, oneOfEach())); + expect(fatal(bool(populate_from(k, project, oneOfEach())))); std::filesystem::remove(k.objDir() / "lib.m.o"); auto probe = probe_cached(k, oneOfEach()); - EXPECT_FALSE(probe.ok); - ASSERT_EQ(probe.layoutMismatch.size(), 1u); - EXPECT_EQ(probe.layoutMismatch[0], "lib.m.o"); -} + expect(!probe.ok); + expect(fatal(eq(probe.layoutMismatch.size(), 1u))); + expect(eq(probe.layoutMismatch[0], "lib.m.o")); +}}; // An entry whose own validation fails (wrong inputs) is an ordinary miss and // carries NO layout complaint — the caller warns on layout divergence only, and // warning on every routine miss would make the signal worthless. -TEST(BmiCache, ProbeReportsNoLayoutMismatchWhenTheEntryItselfIsInvalid) { +UTest probe_reports_no_layout_mismatch_when_the_entry_itself_is_invalid{"BmiCache/ProbeReportsNoLayoutMismatchWhenTheEntryItselfIsInvalid", [] { Tmp t; auto home = t.path / "home"; auto project = t.path / "proj" / "target"; seedProject(project); - ASSERT_TRUE(populate_from(makeKey(home, "samekey00000000", "base"), - project, oneOfEach())); + expect(fatal(bool(populate_from(makeKey(home, "samekey00000000", "base"), + project, oneOfEach())))); auto probe = probe_cached(makeKey(home, "samekey00000000", "other"), oneOfEach()); - EXPECT_FALSE(probe.ok); - EXPECT_TRUE(probe.layoutMismatch.empty()); -} + expect(!probe.ok); + expect(probe.layoutMismatch.empty()); +}}; // touch_accessed is what makes `cache gc` an LRU rather than "drop what was // populated long ago". A use sets the time of the entry's record (mcpp::home:: // mark_used, design 2026-10-31 §4.6): the record's content and the artifacts // stay as they are -- the artifacts' mtimes participate in ninja's restat // handling -- and a second use within 12 hours writes nothing. -TEST(BmiCache, TouchAccessedMovesTheStampAndNotTheArtifacts) { +UTest touch_accessed_moves_the_stamp_and_not_the_artifacts{"BmiCache/TouchAccessedMovesTheStampAndNotTheArtifacts", [] { Tmp t; auto home = t.path / "home"; auto project = t.path / "proj" / "target"; seedProject(project); auto k = makeKey(home); - ASSERT_TRUE(populate_from(k, project, oneOfEach())); + expect(fatal(bool(populate_from(k, project, oneOfEach())))); const auto content0 = readJson(k.entryFile()).dump(); auto objTime0 = std::filesystem::last_write_time(k.objDir() / "lib.m.o"); @@ -374,36 +376,36 @@ TEST(BmiCache, TouchAccessedMovesTheStampAndNotTheArtifacts) { touch_accessed(k); const auto used = std::filesystem::last_write_time(k.entryFile()); - EXPECT_GT(used, old + std::chrono::hours(47)) << "a use must move the record's time"; - EXPECT_EQ(readJson(k.entryFile()).dump(), content0) << "a use must not rewrite the record"; - EXPECT_EQ(std::filesystem::last_write_time(k.objDir() / "lib.m.o"), objTime0); - EXPECT_EQ(std::filesystem::last_write_time(k.bmiDir() / "lib.gcm"), bmiTime0); - EXPECT_TRUE(is_cached(k, oneOfEach())) << "touching must not invalidate the entry"; + expect(gt(used, old + std::chrono::hours(47))) << "a use must move the record's time"; + expect(eq(readJson(k.entryFile()).dump(), content0)) << "a use must not rewrite the record"; + expect(eq(std::filesystem::last_write_time(k.objDir() / "lib.m.o"), objTime0)); + expect(eq(std::filesystem::last_write_time(k.bmiDir() / "lib.gcm"), bmiTime0)); + expect(is_cached(k, oneOfEach())) << "touching must not invalidate the entry"; // Within 12 hours a second use writes nothing. const auto recent = std::filesystem::file_time_type::clock::now() - std::chrono::hours(1); std::filesystem::last_write_time(k.entryFile(), recent); touch_accessed(k); - EXPECT_EQ(std::filesystem::last_write_time(k.entryFile()), recent); -} + expect(eq(std::filesystem::last_write_time(k.entryFile()), recent)); +}}; -TEST(BmiCache, RepopulatePreservesCreatedStamp) { +UTest repopulate_preserves_created_stamp{"BmiCache/RepopulatePreservesCreatedStamp", [] { Tmp t; auto home = t.path / "home"; auto project = t.path / "proj" / "target"; seedProject(project); auto k = makeKey(home); - ASSERT_TRUE(populate_from(k, project, oneOfEach())); + expect(fatal(bool(populate_from(k, project, oneOfEach())))); auto j0 = readJson(k.entryFile()); j0["created"] = "12345"; std::ofstream(k.entryFile()) << j0.dump(2); - ASSERT_TRUE(populate_from(k, project, oneOfEach())); - EXPECT_EQ(readJson(k.entryFile()).value("created", std::string{}), "12345"); -} + expect(fatal(bool(populate_from(k, project, oneOfEach())))); + expect(eq(readJson(k.entryFile()).value("created", std::string{}), "12345")); +}}; -TEST(BmiCache, PopulateFailsIfBuildOutputMissing) { +UTest populate_fails_if_build_output_missing{"BmiCache/PopulateFailsIfBuildOutputMissing", [] { Tmp t; auto home = t.path / "home"; auto project = t.path / "proj" / "target"; @@ -411,14 +413,14 @@ TEST(BmiCache, PopulateFailsIfBuildOutputMissing) { DepArtifacts arts { .bmiFiles = {"missing.gcm"} }; auto k = makeKey(home); auto pop = populate_from(k, project, arts); - EXPECT_FALSE(pop); - EXPECT_NE(pop.error().find("expected build output missing"), std::string::npos); -} + expect(!pop); + expect(ne(pop.error().find("expected build output missing"), std::string::npos)); +}}; // Two keys for the same package@version are independent entries — that is what // lets one machine hold a dev-profile and a release-profile build of the same // dependency at once. -TEST(BmiCache, DifferentKeysAreIndependentEntries) { +UTest different_keys_are_independent_entries{"BmiCache/DifferentKeysAreIndependentEntries", [] { Tmp t; auto home = t.path / "home"; auto project = t.path / "proj" / "target"; @@ -426,17 +428,17 @@ TEST(BmiCache, DifferentKeysAreIndependentEntries) { auto a = makeKey(home, "aaaaaaaaaaaaaaaa", "opt"); auto b = makeKey(home, "bbbbbbbbbbbbbbbb", "debug"); - ASSERT_TRUE(populate_from(a, project, oneOfEach())); - ASSERT_TRUE(populate_from(b, project, oneOfEach())); - EXPECT_NE(a.dir(), b.dir()); - EXPECT_TRUE(is_cached(a, oneOfEach())); - EXPECT_TRUE(is_cached(b, oneOfEach())); -} + expect(fatal(bool(populate_from(a, project, oneOfEach())))); + expect(fatal(bool(populate_from(b, project, oneOfEach())))); + expect(ne(a.dir(), b.dir())); + expect(is_cached(a, oneOfEach())); + expect(is_cached(b, oneOfEach())); +}}; #if !defined(_WIN32) // When an external holder takes the .lock, populate_from must skip (returns // success but does NOT clobber the entry). Uses flock(), POSIX-only. -TEST(BmiCache, PopulateSkipsWhenLockHeld) { +UTest populate_skips_when_lock_held{"BmiCache/PopulateSkipsWhenLockHeld", [] { Tmp t; auto home = t.path / "home"; auto project = t.path / "proj" / "target"; @@ -446,18 +448,18 @@ TEST(BmiCache, PopulateSkipsWhenLockHeld) { std::filesystem::create_directories(k.dir()); auto lockPath = k.dir() / ".lock"; int fd = ::open(lockPath.c_str(), O_CREAT | O_RDWR, 0644); - ASSERT_GE(fd, 0); - ASSERT_EQ(::flock(fd, LOCK_EX | LOCK_NB), 0); + expect(fatal(ge(fd, 0))); + expect(fatal(eq(::flock(fd, LOCK_EX | LOCK_NB), 0))); auto pop = populate_from(k, project, oneOfEach()); - EXPECT_TRUE(pop) << "should silently skip when lock is held"; - EXPECT_FALSE(std::filesystem::exists(k.entryFile())); + expect(bool(pop)) << "should silently skip when lock is held"; + expect(!(std::filesystem::exists(k.entryFile()))); ::flock(fd, LOCK_UN); ::close(fd); auto pop2 = populate_from(k, project, oneOfEach()); - ASSERT_TRUE(pop2) << pop2.error(); - EXPECT_TRUE(std::filesystem::exists(k.entryFile())); -} + if (auto&& failed = expect(fatal(bool(pop2))); !failed) failed << pop2.error(); + expect(std::filesystem::exists(k.entryFile())); +}}; #endif // !defined(_WIN32) diff --git a/tests/unit/test_bmi_equivalent.cpp b/tests/unit/test_bmi_equivalent.cpp index 0b8ffc8fe..01bd847b2 100644 --- a/tests/unit/test_bmi_equivalent.cpp +++ b/tests/unit/test_bmi_equivalent.cpp @@ -5,19 +5,23 @@ // would pass every "equivalent BMIs compare equal" test while silently // suppressing every legitimate cascade — which is a far worse bug than the one // it replaces. Half of these cases exist to catch that. -#include #include #include #include +import mcpplibs.mtest; +import mcpplibs.os.process; + import mcpp.build.stage; +using namespace mcpplibs::mtest; + namespace { std::filesystem::path tmpdir() { auto d = std::filesystem::temp_directory_path() / - ("mcpp-bmi-eq-" + std::to_string(::getpid())); + ("mcpp-bmi-eq-" + std::to_string(mcpplibs::os::process::self()->id)); std::filesystem::create_directories(d); return d; } @@ -38,73 +42,73 @@ std::string bmi_like(const std::string& stamp, const std::string& payload = "PAY } // namespace -TEST(BmiEquivalent, IdenticalFilesAreEquivalent) { +UTest identical_files_are_equivalent{"BmiEquivalent/IdenticalFilesAreEquivalent", [] { const auto d = tmpdir(); auto a = write(d / "a.gcm", bmi_like("2026/08/12 02:25:01 UTC")); auto b = write(d / "b.gcm", bmi_like("2026/08/12 02:25:01 UTC")); - EXPECT_TRUE(mcpp::build::stage::bmi_equivalent(a, b)); -} + expect(bool(mcpp::build::stage::bmi_equivalent(a, b))); +}}; // The whole reason this function exists: GCC stamps a wall clock into the BMI, // so two compiles of identical source differ by a few bytes and `cmp` reports // "changed" every single time. -TEST(BmiEquivalent, DifferingOnlyInTheEmbeddedTimestamp) { +UTest differing_only_in_the_embedded_timestamp{"BmiEquivalent/DifferingOnlyInTheEmbeddedTimestamp", [] { const auto d = tmpdir(); auto a = write(d / "t1.gcm", bmi_like("2026/08/12 02:25:01 UTC")); auto b = write(d / "t2.gcm", bmi_like("2026/08/12 02:25:33 UTC")); - EXPECT_TRUE(mcpp::build::stage::bmi_equivalent(a, b)); -} + expect(bool(mcpp::build::stage::bmi_equivalent(a, b))); +}}; // The side that must NOT be lost: a real interface change still cascades. -TEST(BmiEquivalent, DifferingPayloadIsNotEquivalent) { +UTest differing_payload_is_not_equivalent{"BmiEquivalent/DifferingPayloadIsNotEquivalent", [] { const auto d = tmpdir(); auto a = write(d / "p1.gcm", bmi_like("2026/08/12 02:25:01 UTC", "PAYLOAD")); auto b = write(d / "p2.gcm", bmi_like("2026/08/12 02:25:01 UTC", "PAYLOAX")); - EXPECT_FALSE(mcpp::build::stage::bmi_equivalent(a, b)); -} + expect(!mcpp::build::stage::bmi_equivalent(a, b)); +}}; // A payload difference must not be masked just because a timestamp is nearby. -TEST(BmiEquivalent, PayloadDifferenceIsNotHiddenByATimestampDifference) { +UTest payload_difference_is_not_hidden_by_a_timestamp_difference{"BmiEquivalent/PayloadDifferenceIsNotHiddenByATimestampDifference", [] { const auto d = tmpdir(); auto a = write(d / "m1.gcm", bmi_like("2026/08/12 02:25:01 UTC", "PAYLOAD")); auto b = write(d / "m2.gcm", bmi_like("2026/08/12 09:59:59 UTC", "PAYLOAX")); - EXPECT_FALSE(mcpp::build::stage::bmi_equivalent(a, b)); -} + expect(!mcpp::build::stage::bmi_equivalent(a, b)); +}}; -TEST(BmiEquivalent, DifferentSizesAreNeverEquivalent) { +UTest different_sizes_are_never_equivalent{"BmiEquivalent/DifferentSizesAreNeverEquivalent", [] { const auto d = tmpdir(); auto a = write(d / "s1.gcm", bmi_like("2026/08/12 02:25:01 UTC")); auto b = write(d / "s2.gcm", bmi_like("2026/08/12 02:25:01 UTC") + "EXTRA"); - EXPECT_FALSE(mcpp::build::stage::bmi_equivalent(a, b)); -} + expect(!mcpp::build::stage::bmi_equivalent(a, b)); +}}; // No stamps at all → strict comparison, which is the old behaviour. This is the // conservative fallback that keeps the function from ever being MORE permissive // than a byte compare on inputs it does not understand. -TEST(BmiEquivalent, WithoutStampsItIsAStrictCompare) { +UTest without_stamps_it_is_a_strict_compare{"BmiEquivalent/WithoutStampsItIsAStrictCompare", [] { const auto d = tmpdir(); auto a = write(d / "n1.gcm", "no stamps here"); auto b = write(d / "n2.gcm", "no stamps here"); auto c = write(d / "n3.gcm", "no stamps HERE"); - EXPECT_TRUE(mcpp::build::stage::bmi_equivalent(a, b)); - EXPECT_FALSE(mcpp::build::stage::bmi_equivalent(a, c)); -} + expect(bool(mcpp::build::stage::bmi_equivalent(a, b))); + expect(!mcpp::build::stage::bmi_equivalent(a, c)); +}}; // A value that merely follows the prefix but is not a timestamp must not be // masked — otherwise an attacker-shaped or simply unusual BMI could hide a real // difference behind the literal text "buildtime: ". -TEST(BmiEquivalent, NonTimestampAfterThePrefixIsNotMasked) { +UTest non_timestamp_after_the_prefix_is_not_masked{"BmiEquivalent/NonTimestampAfterThePrefixIsNotMasked", [] { const auto d = tmpdir(); auto a = write(d / "f1.gcm", "buildtime: not-a-timestamp-xxxxxA"); auto b = write(d / "f2.gcm", "buildtime: not-a-timestamp-xxxxxB"); - EXPECT_FALSE(mcpp::build::stage::bmi_equivalent(a, b)); -} + expect(!mcpp::build::stage::bmi_equivalent(a, b)); +}}; -TEST(BmiEquivalent, MissingFileIsNotEquivalent) { +UTest missing_file_is_not_equivalent{"BmiEquivalent/MissingFileIsNotEquivalent", [] { const auto d = tmpdir(); auto a = write(d / "e1.gcm", bmi_like("2026/08/12 02:25:01 UTC")); - EXPECT_FALSE(mcpp::build::stage::bmi_equivalent(a, d / "does-not-exist.gcm")); -} + expect(!mcpp::build::stage::bmi_equivalent(a, d / "does-not-exist.gcm")); +}}; // A real BMI carries exactly two stamps — one `buildtime:`, one `localtime:` — // checked across BMIs from 10 KiB to 645 KiB. A third one did not come from @@ -114,12 +118,12 @@ TEST(BmiEquivalent, MissingFileIsNotEquivalent) { // Asserted from BOTH sides. Checking only that the extra span is not masked // would also pass an implementation that masks nothing at all, which is the // bug this whole function exists to fix. -TEST(BmiEquivalent, MoreStampsThanGccEmitsFallsBackToStrictCompare) { +UTest more_stamps_than_gcc_emits_falls_back_to_strict_compare{"BmiEquivalent/MoreStampsThanGccEmitsFallsBackToStrictCompare", [] { const auto d = tmpdir(); // Two stamps: masked, so the differing clock does not count. auto two_a = write(d / "t1.gcm", bmi_like("2026/08/12 02:25:01 UTC")); auto two_b = write(d / "t2.gcm", bmi_like("2026/08/12 02:25:09 UTC")); - EXPECT_TRUE(mcpp::build::stage::bmi_equivalent(two_a, two_b)); + expect(bool(mcpp::build::stage::bmi_equivalent(two_a, two_b))); // The same two, plus a third stamp-shaped string in the payload that // differs. It must NOT be masked, so the two files are different. @@ -129,5 +133,5 @@ TEST(BmiEquivalent, MoreStampsThanGccEmitsFallsBackToStrictCompare) { auto three_b = write(d / "u2.gcm", bmi_like("2026/08/12 02:25:09 UTC", "buildtime: 2020/01/02 00:00:00 UTC")); - EXPECT_FALSE(mcpp::build::stage::bmi_equivalent(three_a, three_b)); -} + expect(!mcpp::build::stage::bmi_equivalent(three_a, three_b)); +}}; diff --git a/tests/unit/test_build_cache_record.cpp b/tests/unit/test_build_cache_record.cpp index 3727d66c1..3bf54c6e9 100644 --- a/tests/unit/test_build_cache_record.cpp +++ b/tests/unit/test_build_cache_record.cpp @@ -1,10 +1,12 @@ -#include import std; +import mcpplibs.mtest; import mcpp.build.execute; import mcpp.build.prepare; import mcpp.build.graph_shape; +using namespace mcpplibs::mtest; + using namespace mcpp::build; // The record of a build (`target/.build_cache`) and the one predicate every fast @@ -52,41 +54,41 @@ BuildCacheEntry minimal_entry() { // ─── the engine ───────────────────────────────────────────────────────────── -TEST(EngineIdentity, TheRunningEngineIsNotDeclined) { +UTest the_running_engine_is_not_declined{"EngineIdentity/TheRunningEngineIsNotDeclined", [] { const auto engine = running_engine(); - EXPECT_FALSE(engine.version.empty()); - EXPECT_FALSE(engine.exe.empty()); - EXPECT_FALSE(engine_declined_because(engine, engine).has_value()); -} + expect(!engine.version.empty()); + expect(!engine.exe.empty()); + expect(!(engine_declined_because(engine, engine).has_value())); +}}; -TEST(EngineIdentity, AnEntryWithoutTheFieldDeclinesAndSaysWhy) { +UTest an_entry_without_the_field_declines_and_says_why{"EngineIdentity/AnEntryWithoutTheFieldDeclinesAndSaysWhy", [] { const auto why = engine_declined_because(std::nullopt, running_engine()); - ASSERT_TRUE(why.has_value()); - EXPECT_NE(why->find("predates the engine identity"), std::string::npos) << *why; -} + expect(fatal(why.has_value())); + if (auto&& failed = expect(ne(why->find("predates the engine identity"), std::string::npos)); !failed) failed << *why; +}}; -TEST(EngineIdentity, AnotherVersionDeclinesAndNamesBoth) { +UTest another_version_declines_and_names_both{"EngineIdentity/AnotherVersionDeclinesAndNamesBoth", [] { const auto running = running_engine(); auto recorded = running; recorded.version = "2026.1.1.1"; const auto why = engine_declined_because(recorded, running); - ASSERT_TRUE(why.has_value()); - EXPECT_NE(why->find("2026.1.1.1"), std::string::npos) << *why; - EXPECT_NE(why->find(running.version), std::string::npos) << *why; -} + expect(fatal(why.has_value())); + if (auto&& failed = expect(ne(why->find("2026.1.1.1"), std::string::npos)); !failed) failed << *why; + if (auto&& failed = expect(ne(why->find(running.version), std::string::npos)); !failed) failed << *why; +}}; // The path alone is enough. A binary moved to another directory, or reinstalled // there, is the same version and leaves the same stale text in the graph that a // new version does: the graph names the executable by absolute path. -TEST(EngineIdentity, AnotherPathAtTheSameVersionDeclinesAndNamesBoth) { +UTest another_path_at_the_same_version_declines_and_names_both{"EngineIdentity/AnotherPathAtTheSameVersionDeclinesAndNamesBoth", [] { const auto running = running_engine(); auto recorded = running; recorded.exe = "/removed/install/bin/mcpp"; const auto why = engine_declined_because(recorded, running); - ASSERT_TRUE(why.has_value()); - EXPECT_NE(why->find("/removed/install/bin/mcpp"), std::string::npos) << *why; - EXPECT_NE(why->find(running.exe), std::string::npos) << *why; -} + expect(fatal(why.has_value())); + if (auto&& failed = expect(ne(why->find("/removed/install/bin/mcpp"), std::string::npos)); !failed) failed << *why; + if (auto&& failed = expect(ne(why->find(running.exe), std::string::npos)); !failed) failed << *why; +}}; // ─── the admission predicate ──────────────────────────────────────────────── @@ -94,37 +96,36 @@ TEST(EngineIdentity, AnotherPathAtTheSameVersionDeclinesAndNamesBoth) { // else about the record is read, so the reason is the engine's whatever else is // wrong with the entry. This is what makes the one predicate cover an upgrade // for the three fast paths that call it. -TEST(AdmitRecordedBuild, AnotherEngineIsDeclinedBeforeAnythingElseIsRead) { +UTest another_engine_is_declined_before_anything_else_is_read{"AdmitRecordedBuild/AnotherEngineIsDeclinedBeforeAnythingElseIsRead", [] { auto e = minimal_entry(); e.engine = running_engine(); e.engine->version = "2026.1.1.1"; const auto admitted = admit_recorded_build(e, ReplayAsk{.manifest = "mcpp.toml"}); - ASSERT_FALSE(admitted.has_value()); - EXPECT_NE(admitted.error().find("2026.1.1.1"), std::string::npos) << admitted.error(); -} + expect(fatal(!admitted.has_value())); + if (auto&& failed = expect(ne(admitted.error().find("2026.1.1.1"), std::string::npos)); !failed) failed << admitted.error(); +}}; -TEST(AdmitRecordedBuild, AnEntryWithoutTheEngineIsDeclined) { +UTest an_entry_without_the_engine_is_declined{"AdmitRecordedBuild/AnEntryWithoutTheEngineIsDeclined", [] { auto e = minimal_entry(); const auto admitted = admit_recorded_build(e, ReplayAsk{.manifest = "mcpp.toml"}); - ASSERT_FALSE(admitted.has_value()); - EXPECT_NE(admitted.error().find("predates the engine identity"), std::string::npos) - << admitted.error(); -} + expect(fatal(!admitted.has_value())); + if (auto&& failed = expect(ne(admitted.error().find("predates the engine identity"), std::string::npos)); !failed) failed << admitted.error(); +}}; // With the engine matching, the question moves on to the entry's other fields: // the decline is no longer the engine's. -TEST(AdmitRecordedBuild, TheSameEngineIsAskedTheNextQuestion) { +UTest the_same_engine_is_asked_the_next_question{"AdmitRecordedBuild/TheSameEngineIsAskedTheNextQuestion", [] { auto e = minimal_entry(); e.engine = running_engine(); const auto admitted = admit_recorded_build(e, ReplayAsk{.manifest = "mcpp.toml"}); - ASSERT_FALSE(admitted.has_value()); - EXPECT_EQ(admitted.error().find("engine"), std::string::npos) << admitted.error(); - EXPECT_NE(admitted.error().find("runtime binding"), std::string::npos) << admitted.error(); -} + expect(fatal(!admitted.has_value())); + if (auto&& failed = expect(eq(admitted.error().find("engine"), std::string::npos)); !failed) failed << admitted.error(); + if (auto&& failed = expect(ne(admitted.error().find("runtime binding"), std::string::npos)); !failed) failed << admitted.error(); +}}; // ─── the record, written and read ─────────────────────────────────────────── -TEST(BuildCacheRecord, TheEngineAndEachRootsTablesSurviveAWriteAndARead) { +UTest the_engine_and_each_roots_tables_survive_a_write_and_a_read{"BuildCacheRecord/TheEngineAndEachRootsTablesSurviveAWriteAndARead", [] { Tmp tmp; auto e = minimal_entry(); e.engine = EngineIdentity{"2026.10.2.1", "/opt/with space/bin/mcpp"}; @@ -139,20 +140,20 @@ TEST(BuildCacheRecord, TheEngineAndEachRootsTablesSurviveAWriteAndARead) { write_build_cache_entries(tmp.path / "target" / ".build_cache", {e}); const auto read = read_build_cache(tmp.path); - ASSERT_EQ(read.size(), 1u); - ASSERT_TRUE(read[0].engine.has_value()); - EXPECT_EQ(*read[0].engine, *e.engine); - EXPECT_TRUE(read[0].depSourceRootsRecorded); - EXPECT_EQ(read[0].depSourceRoots, e.depSourceRoots); + expect(fatal(eq(read.size(), 1u))); + expect(fatal(read[0].engine.has_value())); + expect(eq(*read[0].engine, *e.engine)); + expect(bool(read[0].depSourceRootsRecorded)); + expect(eq(read[0].depSourceRoots, e.depSourceRoots)); // The fields written after the two above were read too: the blocks do not // swallow one another. - EXPECT_TRUE(read[0].toolchainRecorded); - EXPECT_EQ(read[0].toolchainRequest, e.toolchainRequest); -} + expect(bool(read[0].toolchainRecorded)); + expect(eq(read[0].toolchainRequest, e.toolchainRequest)); +}}; // A path the line format cannot hold is not written at all, so the entry reads // as one that predates the list and declines, rather than as a shorter path. -TEST(BuildCacheRecord, ARootWithATabInItsPathIsLeftUnrecorded) { +UTest a_root_with_a_tab_in_its_path_is_left_unrecorded{"BuildCacheRecord/ARootWithATabInItsPathIsLeftUnrecorded", [] { Tmp tmp; auto e = minimal_entry(); e.depSourceRoots = {{"/work/a\tb", {".ixx"}, {}}}; @@ -161,28 +162,28 @@ TEST(BuildCacheRecord, ARootWithATabInItsPathIsLeftUnrecorded) { e.toolchainRequest = "cli=;default=gcc@16.1.0"; write_build_cache_entries(tmp.path / "target" / ".build_cache", {e}); const auto read = read_build_cache(tmp.path); - ASSERT_EQ(read.size(), 1u); - EXPECT_FALSE(read[0].depSourceRootsRecorded); - EXPECT_TRUE(read[0].depSourceRoots.empty()); - EXPECT_TRUE(read[0].toolchainRecorded); -} + expect(fatal(eq(read.size(), 1u))); + expect(!(read[0].depSourceRootsRecorded)); + expect(read[0].depSourceRoots.empty()); + expect(bool(read[0].toolchainRecorded)); +}}; -TEST(BuildCacheRecord, AnEmptyListOfRootsIsRecordedNotAbsent) { +UTest an_empty_list_of_roots_is_recorded_not_absent{"BuildCacheRecord/AnEmptyListOfRootsIsRecordedNotAbsent", [] { Tmp tmp; auto e = minimal_entry(); e.depSourceRootsRecorded = true; write_build_cache_entries(tmp.path / "target" / ".build_cache", {e}); const auto read = read_build_cache(tmp.path); - ASSERT_EQ(read.size(), 1u); - EXPECT_TRUE(read[0].depSourceRootsRecorded); - EXPECT_TRUE(read[0].depSourceRoots.empty()); - EXPECT_FALSE(read[0].engine.has_value()); -} + expect(fatal(eq(read.size(), 1u))); + expect(bool(read[0].depSourceRootsRecorded)); + expect(read[0].depSourceRoots.empty()); + expect(!(read[0].engine.has_value())); +}}; // An engine that recorded the paths alone wrote `depSourceRoots=`. Its roots // have no tables, so the block is read past and left unrecorded, and the fields // after it still parse. -TEST(BuildCacheRecord, TheOlderBlockOfPathsIsReadPastAndUnrecorded) { +UTest the_older_block_of_paths_is_read_past_and_unrecorded{"BuildCacheRecord/TheOlderBlockOfPathsIsReadPastAndUnrecorded", [] { Tmp tmp; write_file(tmp.path / "target" / ".build_cache", "[target=]\n" @@ -206,19 +207,19 @@ TEST(BuildCacheRecord, TheOlderBlockOfPathsIsReadPastAndUnrecorded) { "toolchain=cli=;default=gcc@16.1.0\n" "xlingsPayloads=0\n"); const auto read = read_build_cache(tmp.path); - ASSERT_EQ(read.size(), 1u); - EXPECT_FALSE(read[0].depSourceRootsRecorded); - EXPECT_TRUE(read[0].depSourceRoots.empty()); - EXPECT_FALSE(read[0].engine.has_value()); - EXPECT_EQ(read[0].profile, "dev"); - EXPECT_TRUE(read[0].toolchainRecorded); - EXPECT_EQ(read[0].toolchainRequest, "cli=;default=gcc@16.1.0"); - EXPECT_TRUE(read[0].xlingsPayloadsRecorded); -} + expect(fatal(eq(read.size(), 1u))); + expect(!(read[0].depSourceRootsRecorded)); + expect(read[0].depSourceRoots.empty()); + expect(!(read[0].engine.has_value())); + expect(eq(read[0].profile, "dev")); + expect(bool(read[0].toolchainRecorded)); + expect(eq(read[0].toolchainRequest, "cli=;default=gcc@16.1.0")); + expect(bool(read[0].xlingsPayloadsRecorded)); +}}; // A line of the block that is not a path and two lists leaves the block // unrecorded rather than read with a table nobody wrote. -TEST(BuildCacheRecord, AMalformedRootLineLeavesTheBlockUnrecorded) { +UTest a_malformed_root_line_leaves_the_block_unrecorded{"BuildCacheRecord/AMalformedRootLineLeavesTheBlockUnrecorded", [] { Tmp tmp; write_file(tmp.path / "target" / ".build_cache", "[target=]\n" @@ -231,9 +232,9 @@ TEST(BuildCacheRecord, AMalformedRootLineLeavesTheBlockUnrecorded) { "/work/rules\n" "runner=0\n"); const auto read = read_build_cache(tmp.path); - ASSERT_EQ(read.size(), 1u); - EXPECT_FALSE(read[0].depSourceRootsRecorded); -} + expect(fatal(eq(read.size(), 1u))); + expect(!(read[0].depSourceRootsRecorded)); +}}; // ─── the sweep ────────────────────────────────────────────────────────────── @@ -257,73 +258,73 @@ struct EditedProvider { // The defect of #756: the provider declares `.ixx` and the consumer does not. // The root is swept with its own package's table, so the edit is seen. -TEST(DepSourcesNewerThan, AProvidersOwnExtensionMakesItsEditCount) { +UTest a_providers_own_extension_makes_its_edit_count{"DepSourcesNewerThan/AProvidersOwnExtensionMakesItsEditCount", [] { EditedProvider p; - EXPECT_TRUE(dep_sources_newer_than({{p.tmp.path, {".ixx"}, {}}}, p.ninjaTime)); -} + expect(bool(dep_sources_newer_than({{p.tmp.path, {".ixx"}, {}}}, p.ninjaTime))); +}}; // A root whose package declares nothing about `.ixx` does not have an `.ixx` of // interest, whatever any other package declares: the table is the root's own. -TEST(DepSourcesNewerThan, APackageThatDeclaresNothingHasNoInterestInTheFile) { +UTest a_package_that_declares_nothing_has_no_interest_in_the_file{"DepSourcesNewerThan/APackageThatDeclaresNothingHasNoInterestInTheFile", [] { EditedProvider p; - EXPECT_FALSE(dep_sources_newer_than({{p.tmp.path, {}, {}}}, p.ninjaTime)); -} + expect(!dep_sources_newer_than({{p.tmp.path, {}, {}}}, p.ninjaTime)); +}}; // One root's table does not leak into the next: of two roots holding the same // file, the one whose package declares the extension is the one that answers. -TEST(DepSourcesNewerThan, EachRootIsClassifiedByItsOwnTable) { +UTest each_root_is_classified_by_its_own_table{"DepSourcesNewerThan/EachRootIsClassifiedByItsOwnTable", [] { EditedProvider declares; EditedProvider silent; const auto ninjaTime = declares.ninjaTime; - EXPECT_FALSE(dep_sources_newer_than({{silent.tmp.path, {}, {}}}, ninjaTime)); - EXPECT_TRUE(dep_sources_newer_than( - {{silent.tmp.path, {}, {}}, {declares.tmp.path, {".ixx"}, {}}}, ninjaTime)); -} + expect(!dep_sources_newer_than({{silent.tmp.path, {}, {}}}, ninjaTime)); + expect(bool(dep_sources_newer_than( + {{silent.tmp.path, {}, {}}, {declares.tmp.path, {".ixx"}, {}}}, ninjaTime))); +}}; // What was always swept still is, with any table. -TEST(DepSourcesNewerThan, TheBuiltInInterfaceExtensionIsSweptWithoutADeclaration) { +UTest the_built_in_interface_extension_is_swept_without_a_declaration{"DepSourcesNewerThan/TheBuiltInInterfaceExtensionIsSweptWithoutADeclaration", [] { EditedProvider p; write_file(p.tmp.path / "m.cppm", "export module m;\n"); std::filesystem::last_write_time(p.tmp.path / "m.cppm", p.ninjaTime + std::chrono::seconds(20)); - EXPECT_TRUE(dep_sources_newer_than({{p.tmp.path, {}, {}}}, p.ninjaTime)); -} + expect(bool(dep_sources_newer_than({{p.tmp.path, {}, {}}}, p.ninjaTime))); +}}; -TEST(DepSourcesNewerThan, ANewerManifestCounts) { +UTest a_newer_manifest_counts{"DepSourcesNewerThan/ANewerManifestCounts", [] { EditedProvider p; std::filesystem::last_write_time(p.tmp.path / "mcpp.toml", p.ninjaTime + std::chrono::seconds(20)); - EXPECT_TRUE(dep_sources_newer_than({{p.tmp.path, {}, {}}}, p.ninjaTime)); -} + expect(bool(dep_sources_newer_than({{p.tmp.path, {}, {}}}, p.ninjaTime))); +}}; // The graph names the engine it runs as `$mcpp`, escaped for ninja; the reader // returns the path the emitter escaped, so that it compares with // `mcpp_exe_path()` in generic form. -TEST(ReadEngineBinding, TheEscapedPathIsReadBack) { +UTest the_escaped_path_is_read_back{"ReadEngineBinding/TheEscapedPathIsReadBack", [] { Tmp tmp; write_file(tmp.path / "build.ninja", "# mcpp:graph=normal\ncxx = /opt/llvm/bin/clang++\n" "mcpp = C$:/Program$ Files/mcpp$$1/mcpp.exe\n\nrule cxx\n"); - EXPECT_EQ(read_engine_binding(tmp.path / "build.ninja"), "C:/Program Files/mcpp$1/mcpp.exe"); -} + expect(eq(read_engine_binding(tmp.path / "build.ninja"), "C:/Program Files/mcpp$1/mcpp.exe")); +}}; // The binding is a command word, quoted for the host (#832): a POSIX graph // writes single quotes, a Windows graph double quotes. -TEST(ReadEngineBinding, TheQuotedWordIsReadBack) { +UTest the_quoted_word_is_read_back{"ReadEngineBinding/TheQuotedWordIsReadBack", [] { Tmp tmp; write_file(tmp.path / "build.ninja", "mcpp = '/tmp/mc probe/it'\\''s/$$1/mcpp'\n"); - EXPECT_EQ(read_engine_binding(tmp.path / "build.ninja"), "/tmp/mc probe/it's/$1/mcpp"); + expect(eq(read_engine_binding(tmp.path / "build.ninja"), "/tmp/mc probe/it's/$1/mcpp")); write_file(tmp.path / "build.ninja", "mcpp = \"C:/Program Files/mcpp/mcpp.exe\"\n"); - EXPECT_EQ(read_engine_binding(tmp.path / "build.ninja"), "C:/Program Files/mcpp/mcpp.exe"); -} + expect(eq(read_engine_binding(tmp.path / "build.ninja"), "C:/Program Files/mcpp/mcpp.exe")); +}}; -TEST(ReadEngineBinding, AGraphWithoutTheBindingSaysNothing) { +UTest a_graph_without_the_binding_says_nothing{"ReadEngineBinding/AGraphWithoutTheBindingSaysNothing", [] { Tmp tmp; write_file(tmp.path / "build.ninja", "cxx = clang++\nmcpp_extra = x\nrule cxx\n"); - EXPECT_EQ(read_engine_binding(tmp.path / "build.ninja"), ""); -} + expect(eq(read_engine_binding(tmp.path / "build.ninja"), "")); +}}; -TEST(ReadEngineBinding, AMissingFileSaysNothing) { +UTest a_missing_file_says_nothing{"ReadEngineBinding/AMissingFileSaysNothing", [] { Tmp tmp; - EXPECT_EQ(read_engine_binding(tmp.path / "absent.ninja"), ""); -} + expect(eq(read_engine_binding(tmp.path / "absent.ninja"), "")); +}}; diff --git a/tests/unit/test_build_database.cpp b/tests/unit/test_build_database.cpp index 66a7a7306..826164b14 100644 --- a/tests/unit/test_build_database.cpp +++ b/tests/unit/test_build_database.cpp @@ -5,9 +5,9 @@ // command builders, so a builder that changes shape fails here rather than in // a document an editor reads. -#include import std; +import mcpplibs.mtest; import mcpp.build.build_database; import mcpp.modgraph.graph; import mcpplibs.os.platform; @@ -16,6 +16,8 @@ import mcpp.toolchain.gcc; import mcpp.toolchain.model; import mcpp.toolchain.msvc; +using namespace mcpplibs::mtest; + namespace db = mcpp::build::database; using mcpp::modgraph::ModuleDeclaration; using namespace mcpp::toolchain; @@ -34,55 +36,52 @@ bool contains(const std::vector& words, const std::string& w) { } // namespace -TEST(BuildDatabase, RoleOfEachDeclarationForm) { - EXPECT_EQ(db::role_name(ModuleDeclaration::None), "non-module"); - EXPECT_EQ(db::role_name(ModuleDeclaration::Interface), "module-interface"); - EXPECT_EQ(db::role_name(ModuleDeclaration::InterfacePartition), - "module-partition-interface"); - EXPECT_EQ(db::role_name(ModuleDeclaration::ImplementationPartition), - "module-partition-implementation"); - EXPECT_EQ(db::role_name(ModuleDeclaration::Implementation), "module-implementation"); - EXPECT_EQ(db::role_name(ModuleDeclaration::Unknown), "unknown"); -} - -TEST(BuildDatabase, StandardLibraryNames) { - EXPECT_EQ(db::stdlib_name("libstdc++"), "libstdc++"); - EXPECT_EQ(db::stdlib_name("libc++"), "libc++"); - EXPECT_EQ(db::stdlib_name("msvc-stl"), "msvc-stl"); - EXPECT_EQ(db::stdlib_name("newlib"), "other"); -} - -TEST(BuildDatabase, ToolchainIdNamesFamilyVersionAndTheCompilersTriple) { +UTest role_of_each_declaration_form{"BuildDatabase/RoleOfEachDeclarationForm", [] { + expect(eq(db::role_name(ModuleDeclaration::None), "non-module")); + expect(eq(db::role_name(ModuleDeclaration::Interface), "module-interface")); + expect(eq(db::role_name(ModuleDeclaration::InterfacePartition), "module-partition-interface")); + expect(eq(db::role_name(ModuleDeclaration::ImplementationPartition), "module-partition-implementation")); + expect(eq(db::role_name(ModuleDeclaration::Implementation), "module-implementation")); + expect(eq(db::role_name(ModuleDeclaration::Unknown), "unknown")); +}}; + +UTest standard_library_names{"BuildDatabase/StandardLibraryNames", [] { + expect(eq(db::stdlib_name("libstdc++"), "libstdc++")); + expect(eq(db::stdlib_name("libc++"), "libc++")); + expect(eq(db::stdlib_name("msvc-stl"), "msvc-stl")); + expect(eq(db::stdlib_name("newlib"), "other")); +}}; + +UTest toolchain_id_names_family_version_and_the_compilers_triple{"BuildDatabase/ToolchainIdNamesFamilyVersionAndTheCompilersTriple", [] { Toolchain tc; tc.compiler = CompilerId::Clang; tc.version = "22.1.8"; tc.targetTriple = "x86_64-windows-msvc"; // mcpp's family name in the opaque id (`llvm`, as in `llvm@22.1.8`), and the // triple as the compiler spells it; the S1 `family` field says `clang`. - EXPECT_EQ(db::toolchain_id(tc, "x86_64-pc-windows-msvc"), - "llvm-22.1.8-x86_64-pc-windows-msvc"); - EXPECT_EQ(tc.compiler_name(), "clang"); -} + expect(eq(db::toolchain_id(tc, "x86_64-pc-windows-msvc"), "llvm-22.1.8-x86_64-pc-windows-msvc")); + expect(eq(tc.compiler_name(), "clang")); +}}; -TEST(BuildDatabase, PosixWordsUndoShellQuoting) { +UTest posix_words_undo_shell_quoting{"BuildDatabase/PosixWordsUndoShellQuoting", [] { auto w = db::split_command_words( "cd '/c d' && env LD_LIBRARY_PATH='/r t' 'it'\\''s' \"a\\\"b\" x\\ y 2>&1", /*windows=*/false); std::vector want{"cd", "/c d", "&&", "env", "LD_LIBRARY_PATH=/r t", "it's", "a\"b", "x y", "2>&1"}; - EXPECT_EQ(w, want); -} + expect(eq(w, want)); +}}; -TEST(BuildDatabase, WindowsWordsFollowTheRuntimeRules) { +UTest windows_words_follow_the_runtime_rules{"BuildDatabase/WindowsWordsFollowTheRuntimeRules", [] { auto w = db::split_command_words( "cd /d \"C:\\a b\" && \"C:\\t\\clang++.exe\" \"x\\\\\\\"y\" C:\\s\\std.ixx 2>&1", /*windows=*/true); std::vector want{"cd", "/d", "C:\\a b", "&&", "C:\\t\\clang++.exe", "x\\\"y", "C:\\s\\std.ixx", "2>&1"}; - EXPECT_EQ(w, want); -} + expect(eq(w, want)); +}}; -TEST(BuildDatabase, RecoversTheGccStdUnitFromItsBuilder) { +UTest recovers_the_gcc_std_unit_from_its_builder{"BuildDatabase/RecoversTheGccStdUnitFromItsBuilder", [] { Toolchain tc; tc.compiler = CompilerId::GCC; tc.version = "16.1.0"; @@ -94,22 +93,22 @@ TEST(BuildDatabase, RecoversTheGccStdUnitFromItsBuilder) { const auto cache = host_path("/home/u/.mcpp/cache/std/k1", "C:\\u\\.mcpp\\std\\k1"); auto commands = gcc::std_module_build_commands(tc, cache, "", "-std=c++23"); - ASSERT_FALSE(commands.empty()); + expect(fatal(!commands.empty())); auto inv = db::recover_invocation(commands, tc.stdModuleSource, tc.binaryPath, "/unused", kWindows); - ASSERT_TRUE(inv.has_value()) << commands.front(); - EXPECT_EQ(inv->workDirectory, cache) << commands.front(); - ASSERT_FALSE(inv->arguments.empty()); - EXPECT_EQ(inv->arguments.front(), tc.binaryPath.string()) << commands.front(); - EXPECT_TRUE(contains(inv->arguments, tc.stdModuleSource.string())) << commands.front(); - EXPECT_TRUE(contains(inv->arguments, "-std=c++23")) << commands.front(); - EXPECT_FALSE(contains(inv->arguments, "2>&1")) << commands.front(); - EXPECT_FALSE(contains(inv->arguments, "env")) << commands.front(); + expect(fatal(inv.has_value())) << commands.front(); + expect(eq(inv->workDirectory, cache)) << commands.front(); + expect(fatal(!(inv->arguments.empty()))); + expect(eq(inv->arguments.front(), tc.binaryPath.string())) << commands.front(); + expect(contains(inv->arguments, tc.stdModuleSource.string())) << commands.front(); + expect(contains(inv->arguments, "-std=c++23")) << commands.front(); + expect(!contains(inv->arguments, "2>&1")) << commands.front(); + expect(!contains(inv->arguments, "env")) << commands.front(); for (auto const& a : inv->arguments) - EXPECT_FALSE(a.starts_with("LD_LIBRARY_PATH=")) << a; -} + expect(!a.starts_with("LD_LIBRARY_PATH=")) << a; +}}; -TEST(BuildDatabase, RecoversTheClangStdUnitsFromTheirBuilders) { +UTest recovers_the_clang_std_units_from_their_builders{"BuildDatabase/RecoversTheClangStdUnitsFromTheirBuilders", [] { Toolchain tc; tc.compiler = CompilerId::Clang; tc.version = "22.1.8"; @@ -125,21 +124,21 @@ TEST(BuildDatabase, RecoversTheClangStdUnitsFromTheirBuilders) { auto stdCommands = clang::std_module_build_commands(tc, cache, bmi, "", "-std=c++23"); auto inv = db::recover_invocation(stdCommands, tc.stdModuleSource, tc.binaryPath, cache, kWindows); - ASSERT_TRUE(inv.has_value()) << stdCommands.front(); - EXPECT_EQ(inv->arguments.front(), tc.binaryPath.string()); - EXPECT_TRUE(contains(inv->arguments, "--precompile")) << stdCommands.front(); - EXPECT_TRUE(contains(inv->arguments, tc.stdModuleSource.string())); - EXPECT_EQ(inv->workDirectory, cache); + expect(fatal(inv.has_value())) << stdCommands.front(); + expect(eq(inv->arguments.front(), tc.binaryPath.string())); + expect(contains(inv->arguments, "--precompile")) << stdCommands.front(); + expect(contains(inv->arguments, tc.stdModuleSource.string())); + expect(eq(inv->workDirectory, cache)); auto compatCommands = clang::std_compat_build_commands( tc, cache, clang::std_compat_bmi_path(cache), bmi, "", "-std=c++23"); auto compat = db::recover_invocation(compatCommands, tc.stdCompatSource, tc.binaryPath, cache, kWindows); - ASSERT_TRUE(compat.has_value()) << compatCommands.front(); - EXPECT_TRUE(contains(compat->arguments, tc.stdCompatSource.string())); -} + expect(fatal(compat.has_value())) << compatCommands.front(); + expect(contains(compat->arguments, tc.stdCompatSource.string())); +}}; -TEST(BuildDatabase, RecoversTheMsvcStdUnitsFromTheirBuilders) { +UTest recovers_the_msvc_std_units_from_their_builders{"BuildDatabase/RecoversTheMsvcStdUnitsFromTheirBuilders", [] { Toolchain tc; tc.compiler = CompilerId::MSVC; tc.version = "19.44.35207"; @@ -156,33 +155,33 @@ TEST(BuildDatabase, RecoversTheMsvcStdUnitsFromTheirBuilders) { auto stdCommands = msvc::std_module_build_commands(tc, cache, "/std:c++latest", "/MD"); auto inv = db::recover_invocation(stdCommands, tc.stdModuleSource, tc.binaryPath, "/unused", kWindows); - ASSERT_TRUE(inv.has_value()) << stdCommands.front(); - EXPECT_EQ(inv->workDirectory, cache) << stdCommands.front(); - EXPECT_EQ(inv->arguments.front(), tc.binaryPath.string()) << stdCommands.front(); - EXPECT_TRUE(contains(inv->arguments, "/std:c++latest")) << stdCommands.front(); - EXPECT_TRUE(contains(inv->arguments, tc.stdModuleSource.string())) << stdCommands.front(); - EXPECT_FALSE(contains(inv->arguments, "2>&1")) << stdCommands.front(); + expect(fatal(inv.has_value())) << stdCommands.front(); + expect(eq(inv->workDirectory, cache)) << stdCommands.front(); + expect(eq(inv->arguments.front(), tc.binaryPath.string())) << stdCommands.front(); + expect(contains(inv->arguments, "/std:c++latest")) << stdCommands.front(); + expect(contains(inv->arguments, tc.stdModuleSource.string())) << stdCommands.front(); + expect(!contains(inv->arguments, "2>&1")) << stdCommands.front(); auto compatCommands = msvc::std_compat_build_commands(tc, cache, "/std:c++latest", "/MD"); auto compat = db::recover_invocation(compatCommands, tc.stdCompatSource, tc.binaryPath, "/unused", kWindows); - ASSERT_TRUE(compat.has_value()) << compatCommands.front(); - EXPECT_TRUE(contains(compat->arguments, "/reference")) << compatCommands.front(); -} + expect(fatal(compat.has_value())) << compatCommands.front(); + expect(contains(compat->arguments, "/reference")) << compatCommands.front(); +}}; -TEST(BuildDatabase, NoCommandNamingTheSourceRecoversNothing) { +UTest no_command_naming_the_source_recovers_nothing{"BuildDatabase/NoCommandNamingTheSourceRecoversNothing", [] { auto inv = db::recover_invocation({"cd /c && /bin/g++ -c other.cc -o o.o"}, "/src/std.cc", "/bin/g++", "/c", false); - EXPECT_FALSE(inv.has_value()); -} + expect(!inv.has_value()); +}}; -TEST(BuildDatabase, AnUnquotedDriverWithASpaceIsRejoined) { +UTest an_unquoted_driver_with_a_space_is_rejoined{"BuildDatabase/AnUnquotedDriverWithASpaceIsRejoined", [] { auto inv = db::recover_invocation( {"C:\\Program Files\\LLVM\\bin\\clang++.exe -std=c++23 --precompile " "\"C:\\s\\std.cppm\" -o \"C:\\c\\std.pcm\""}, "C:\\s\\std.cppm", "C:\\Program Files\\LLVM\\bin\\clang++.exe", "C:\\c", /*windows=*/true); - ASSERT_TRUE(inv.has_value()); - EXPECT_EQ(inv->arguments.front(), "C:\\Program Files\\LLVM\\bin\\clang++.exe"); - EXPECT_EQ(inv->arguments[1], "-std=c++23"); -} + expect(fatal(inv.has_value())); + expect(eq(inv->arguments.front(), "C:\\Program Files\\LLVM\\bin\\clang++.exe")); + expect(eq(inv->arguments[1], "-std=c++23")); +}}; diff --git a/tests/unit/test_build_directives.cpp b/tests/unit/test_build_directives.cpp index 2d3d72578..468e8d64c 100644 --- a/tests/unit/test_build_directives.cpp +++ b/tests/unit/test_build_directives.cpp @@ -1,10 +1,12 @@ -#include import std; +import mcpplibs.mtest; import mcpp.build.directives; import mcpp.manifest; import mcpp.toolchain.dialect; +using namespace mcpplibs::mtest; + // The directive table is the single definition of what a `build.mcpp` // directive IS. Before it existed, a directive was defined in nine places and // adding one meant editing all of them — the failure mode being that you edit @@ -48,34 +50,34 @@ dirs::Directives parse(std::string_view text) { // ── Table integrity ──────────────────────────────────────────────────────── -TEST(BuildDirectives, EveryRowIsInternallyConsistent) { +UTest every_row_is_internally_consistent{"BuildDirectives/EveryRowIsInternallyConsistent", [] { for (auto const& def : dirs::kTable) { - EXPECT_FALSE(def.wire.empty()); - EXPECT_GT(def.sinceProtocol, 0) << def.wire; - EXPECT_LE(def.sinceProtocol, dirs::kProtocolVersion) << def.wire; + expect(!def.wire.empty()); + expect(gt(def.sinceProtocol, 0)) << def.wire; + expect(le(def.sinceProtocol, dirs::kProtocolVersion)) << def.wire; // A RerunKey directive feeds only the re-run key, so it must NOT be // persisted as a `d` record; everything else must be, or a cache hit // would silently apply less than the program asked for. if (def.scope == dirs::Scope::RerunKey) - EXPECT_TRUE(def.tag.empty()) << def.wire; + expect(def.tag.empty()) << def.wire; else - EXPECT_FALSE(def.tag.empty()) << def.wire; + expect(!def.tag.empty()) << def.wire; // The declared-output contract needs a diagnostic that says WHICH // contract was broken. if (def.mustExistAfterRun) { - EXPECT_FALSE(def.missingPrefix.empty()) << def.wire; - EXPECT_FALSE(def.missingSuffix.empty()) << def.wire; + expect(!def.missingPrefix.empty()) << def.wire; + expect(!def.missingSuffix.empty()) << def.wire; } } -} +}}; -TEST(BuildDirectives, WireNamesAreUnique) { +UTest wire_names_are_unique{"BuildDirectives/WireNamesAreUnique", [] { std::set seen; for (auto const& def : dirs::kTable) - EXPECT_TRUE(seen.insert(def.wire).second) << "duplicate wire " << def.wire; -} + expect(bool(seen.insert(def.wire).second)) << "duplicate wire " << def.wire; +}}; -TEST(BuildDirectives, RowsSharingATagShareASlot) { +UTest rows_sharing_a_tag_share_a_slot{"BuildDirectives/RowsSharingATagShareASlot", [] { // Cache deserialization resolves a tag to exactly one slot, so two rows // with the same tag but different slots would round-trip to the wrong // channel. link-lib and link-search legitimately share `ldflag`. @@ -83,36 +85,36 @@ TEST(BuildDirectives, RowsSharingATagShareASlot) { for (auto const& def : dirs::kTable) { if (def.tag.empty()) continue; auto [it, fresh] = slotOfTag.try_emplace(def.tag, def.slot); - if (!fresh) EXPECT_EQ(it->second, def.slot) << def.tag; + if (!fresh) expect(eq(it->second, def.slot)) << def.tag; } -} +}}; -TEST(BuildDirectives, LookupsAgree) { +UTest lookups_agree{"BuildDirectives/LookupsAgree", [] { for (auto const& def : dirs::kTable) { - ASSERT_NE(dirs::find_by_wire(def.wire), nullptr) << def.wire; - EXPECT_EQ(dirs::find_by_wire(def.wire)->slot, def.slot) << def.wire; + expect(fatal(ne(dirs::find_by_wire(def.wire), nullptr))) << def.wire; + expect(eq(dirs::find_by_wire(def.wire)->slot, def.slot)) << def.wire; if (!def.tag.empty()) { - ASSERT_NE(dirs::find_by_tag(def.tag), nullptr) << def.tag; - EXPECT_EQ(dirs::find_by_tag(def.tag)->slot, def.slot) << def.tag; + expect(fatal(ne(dirs::find_by_tag(def.tag), nullptr))) << def.tag; + expect(eq(dirs::find_by_tag(def.tag)->slot, def.slot)) << def.tag; } } - EXPECT_EQ(dirs::find_by_wire("no-such-directive"), nullptr); + expect(eq(dirs::find_by_wire("no-such-directive"), nullptr)); // The empty tag belongs to the rerun rows and must never resolve, or a // malformed cache line would be silently accepted into a slot. - EXPECT_EQ(dirs::find_by_tag(""), nullptr); -} + expect(eq(dirs::find_by_tag(""), nullptr)); +}}; // ── Parsing ──────────────────────────────────────────────────────────────── -TEST(BuildDirectives, NonDirectiveLinesAreIgnored) { +UTest non_directive_lines_are_ignored{"BuildDirectives/NonDirectiveLinesAreIgnored", [] { auto d = parse("hello\nmcpp is not a directive\n \n"); for (std::size_t i = 0; i < dirs::kSlotCount; ++i) - EXPECT_TRUE(d.slots[i].empty()); - EXPECT_TRUE(d.unknownKeys.empty()); - EXPECT_EQ(d.protocol, 0); -} + expect(d.slots[i].empty()); + expect(d.unknownKeys.empty()); + expect(eq(d.protocol, 0)); +}}; -TEST(BuildDirectives, TransformsAreAppliedOnceAtParseTime) { +UTest transforms_are_applied_once_at_parse_time{"BuildDirectives/TransformsAreAppliedOnceAtParseTime", [] { // An input that is already absolute ON THIS PLATFORM, so the "taken as-is" // half of the assertion tests what it claims to. const auto alreadyAbs = @@ -125,104 +127,100 @@ TEST(BuildDirectives, TransformsAreAppliedOnceAtParseTime) { "mcpp:include-dir=inc\n" "mcpp:include-dir-after={}\n", alreadyAbs.string())); - EXPECT_EQ(d.at(dirs::Slot::CxxFlags), (std::vector{"-Wall"})); + expect(eq(d.at(dirs::Slot::CxxFlags), (std::vector{"-Wall"}))); // link-lib and link-search share the ldflags slot, in emission order. - EXPECT_EQ(d.at(dirs::Slot::LdFlags), - (std::vector{"-lz", "-L" + under_root("vendor/lib")})); - EXPECT_EQ(d.at(dirs::Slot::Defines), (std::vector{"-DHAVE_X"})); + expect(eq(d.at(dirs::Slot::LdFlags), (std::vector{"-lz", "-L" + under_root("vendor/lib")}))); + expect(eq(d.at(dirs::Slot::Defines), (std::vector{"-DHAVE_X"}))); // Relative resolves against the package root; absolute is taken as-is. - EXPECT_EQ(d.at(dirs::Slot::IncludeDirs), - (std::vector{under_root("inc")})); - EXPECT_EQ(d.at(dirs::Slot::IncludeDirsAfter), - (std::vector{alreadyAbs.string()})); -} + expect(eq(d.at(dirs::Slot::IncludeDirs), (std::vector{under_root("inc")}))); + expect(eq(d.at(dirs::Slot::IncludeDirsAfter), (std::vector{alreadyAbs.string()}))); +}}; -TEST(BuildDirectives, DialectDecidesTheSpelling) { +UTest dialect_decides_the_spelling{"BuildDirectives/DialectDecidesTheSpelling", [] { dirs::Directives d; dirs::accept_output(d, mcpp::toolchain::msvc_dialect(), test_root(), "mcpp:link-lib=z\nmcpp:cfg=HAVE_X\n"); - EXPECT_EQ(d.at(dirs::Slot::LdFlags), (std::vector{"z.lib"})); - EXPECT_EQ(d.at(dirs::Slot::Defines), (std::vector{"/DHAVE_X"})); -} + expect(eq(d.at(dirs::Slot::LdFlags), (std::vector{"z.lib"}))); + expect(eq(d.at(dirs::Slot::Defines), (std::vector{"/DHAVE_X"}))); +}}; -TEST(BuildDirectives, ValuesMayContainSpacesAndEqualsSigns) { +UTest values_may_contain_spaces_and_equals_signs{"BuildDirectives/ValuesMayContainSpacesAndEqualsSigns", [] { // Everything after the FIRST '=' is the value — a path with a space or a // define with an '=' must survive intact. auto d = parse("mcpp:cxxflag=-DMSG=\"a b\"\n"); - EXPECT_EQ(d.at(dirs::Slot::CxxFlags), - (std::vector{"-DMSG=\"a b\""})); -} + expect(eq(d.at(dirs::Slot::CxxFlags), (std::vector{"-DMSG=\"a b\""}))); +}}; // ── Protocol ─────────────────────────────────────────────────────────────── -TEST(BuildDirectives, UnknownKeyWithoutAnAnnouncementIsTolerated) { +UTest unknown_key_without_an_announcement_is_tolerated{"BuildDirectives/UnknownKeyWithoutAnAnnouncementIsTolerated", [] { // A hand-written printf program announces nothing; its surface is frozen, // so an unknown key is a typo the engine warns about rather than a // forward-compat situation it must refuse. auto d = parse("mcpp:no-such-thing=1\n"); - EXPECT_EQ(d.protocol, 0); - EXPECT_EQ(d.unknownKeys, (std::vector{"no-such-thing"})); - EXPECT_FALSE(dirs::protocol_error(d).has_value()); -} + expect(eq(d.protocol, 0)); + expect(eq(d.unknownKeys, (std::vector{"no-such-thing"}))); + expect(!(dirs::protocol_error(d).has_value())); +}}; -TEST(BuildDirectives, UnknownKeyWithAnAnnouncementIsFatal) { +UTest unknown_key_with_an_announcement_is_fatal{"BuildDirectives/UnknownKeyWithAnAnnouncementIsFatal", [] { auto d = parse("mcpp:protocol=1\nmcpp:no-such-thing=1\n"); - EXPECT_EQ(d.protocol, 1); + expect(eq(d.protocol, 1)); auto err = dirs::protocol_error(d); - ASSERT_TRUE(err.has_value()); - EXPECT_NE(err->find("no-such-thing"), std::string::npos); -} + expect(fatal(err.has_value())); + expect(ne(err->find("no-such-thing"), std::string::npos)); +}}; -TEST(BuildDirectives, NewerProtocolIsFatalAndSaysWhatToDo) { +UTest newer_protocol_is_fatal_and_says_what_to_do{"BuildDirectives/NewerProtocolIsFatalAndSaysWhatToDo", [] { auto d = parse("mcpp:protocol=999\n"); auto err = dirs::protocol_error(d); - ASSERT_TRUE(err.has_value()); - EXPECT_NE(err->find("999"), std::string::npos); + expect(fatal(err.has_value())); + expect(ne(err->find("999"), std::string::npos)); // The message has to tell the user the actionable thing, not just that // something is wrong. - EXPECT_NE(err->find("upgrade"), std::string::npos); -} + expect(ne(err->find("upgrade"), std::string::npos)); +}}; -TEST(BuildDirectives, CurrentProtocolIsAccepted) { +UTest current_protocol_is_accepted{"BuildDirectives/CurrentProtocolIsAccepted", [] { auto d = parse(std::format("mcpp:protocol={}\nmcpp:cxxflag=-Wall\n", dirs::kProtocolVersion)); - EXPECT_FALSE(dirs::protocol_error(d).has_value()); - EXPECT_EQ(d.at(dirs::Slot::CxxFlags), (std::vector{"-Wall"})); -} + expect(!(dirs::protocol_error(d).has_value())); + expect(eq(d.at(dirs::Slot::CxxFlags), (std::vector{"-Wall"}))); +}}; -TEST(BuildDirectives, UnknownKeysAreDeduplicated) { +UTest unknown_keys_are_deduplicated{"BuildDirectives/UnknownKeysAreDeduplicated", [] { auto d = parse("mcpp:zzz=1\nmcpp:zzz=2\nmcpp:yyy=3\n"); - EXPECT_EQ(d.unknownKeys, (std::vector{"zzz", "yyy"})); -} + expect(eq(d.unknownKeys, (std::vector{"zzz", "yyy"}))); +}}; // #693: a directive whose text is not UTF-8 names a different file once it is // in build.ninja, so it is refused by key, whatever protocol the program // speaks, and nothing of it is applied. -TEST(BuildDirectives, ADirectiveThatIsNotUtf8IsRefusedByKey) { +UTest a_directive_that_is_not_utf8_is_refused_by_key{"BuildDirectives/ADirectiveThatIsNotUtf8IsRefusedByKey", [] { // "caf" + 0xE9: Latin-1, the shape a narrow program in a code page 1252 // process prints. auto d = parse("mcpp:include-dir=inc/caf\xE9\nmcpp:cxxflag=-Wall\n"); - EXPECT_TRUE(d.at(dirs::Slot::IncludeDirs).empty()); - EXPECT_EQ(d.at(dirs::Slot::CxxFlags), (std::vector{"-Wall"})); - EXPECT_EQ(d.nonUtf8Keys, (std::vector{"mcpp:include-dir"})); + expect(d.at(dirs::Slot::IncludeDirs).empty()); + expect(eq(d.at(dirs::Slot::CxxFlags), (std::vector{"-Wall"}))); + expect(eq(d.nonUtf8Keys, (std::vector{"mcpp:include-dir"}))); auto err = dirs::encoding_error(d); - ASSERT_TRUE(err.has_value()); - EXPECT_NE(err->find("mcpp:include-dir"), std::string::npos) << *err; - EXPECT_NE(err->find("UTF-8"), std::string::npos) << *err; -} + expect(fatal(err.has_value())); + if (auto&& failed = expect(ne(err->find("mcpp:include-dir"), std::string::npos)); !failed) failed << *err; + if (auto&& failed = expect(ne(err->find("UTF-8"), std::string::npos)); !failed) failed << *err; +}}; -TEST(BuildDirectives, UtfEightDirectivesAndOtherChatterPass) { +UTest utf_eight_directives_and_other_chatter_pass{"BuildDirectives/UtfEightDirectivesAndOtherChatterPass", [] { // U+00E9 in UTF-8 is accepted; a line that is not a directive is not // examined at all. auto d = parse("mcpp:include-dir=inc/caf\xC3\xA9\nnoise \xE9\n"); - EXPECT_EQ(d.at(dirs::Slot::IncludeDirs).size(), 1u); - EXPECT_TRUE(d.nonUtf8Keys.empty()); - EXPECT_FALSE(dirs::encoding_error(d).has_value()); -} + expect(eq(d.at(dirs::Slot::IncludeDirs).size(), 1u)); + expect(d.nonUtf8Keys.empty()); + expect(!(dirs::encoding_error(d).has_value())); +}}; // ── Cache round-trip ─────────────────────────────────────────────────────── -TEST(BuildDirectives, SerializeDeserializeRoundTrip) { +UTest serialize_deserialize_round_trip{"BuildDirectives/SerializeDeserializeRoundTrip", [] { auto d = parse("mcpp:cxxflag=-Wall\n" "mcpp:cflag=-std=c11\n" "mcpp:link-lib=z\n" @@ -245,21 +243,21 @@ TEST(BuildDirectives, SerializeDeserializeRoundTrip) { std::istringstream is(os.str()); std::string line; while (std::getline(is, line)) { - ASSERT_TRUE(line.starts_with("d ")); + expect(fatal(line.starts_with("d "))); auto rest = line.substr(2); auto sp = rest.find(' '); - ASSERT_NE(sp, std::string::npos); - EXPECT_TRUE(dirs::accept_cache_record(back, rest.substr(0, sp), - rest.substr(sp + 1))); + expect(fatal(ne(sp, std::string::npos))); + expect(bool(dirs::accept_cache_record(back, rest.substr(0, sp), + rest.substr(sp + 1)))); } // Every persisted slot survives verbatim; the rerun slots deliberately do // not (they are re-derived from their own cache records). for (auto const& def : dirs::kTable) { if (def.tag.empty()) continue; - EXPECT_EQ(back.at(def.slot), d.at(def.slot)) << def.wire; + expect(eq(back.at(def.slot), d.at(def.slot))) << def.wire; } -} +}}; // ── The probe channel ────────────────────────────────────────────────────── // @@ -268,45 +266,43 @@ TEST(BuildDirectives, SerializeDeserializeRoundTrip) { // so the version-floor check in prepare reads one list and never learns // which spelling a claim came in. -TEST(BuildDirectives, FactsAndFloorsAreClaimsThatFoldIntoRuntimeDeclarations) { +UTest facts_and_floors_are_claims_that_fold_into_runtime_declarations{"BuildDirectives/FactsAndFloorsAreClaimsThatFoldIntoRuntimeDeclarations", [] { auto d = parse("mcpp:fact=widget.driver=1.2\n" "mcpp:floor=widget.driver >= 2.0\n"); - EXPECT_EQ(d.at(dirs::Slot::Facts), (std::vector{"widget.driver=1.2"})); - EXPECT_EQ(d.at(dirs::Slot::Floors), (std::vector{"widget.driver >= 2.0"})); + expect(eq(d.at(dirs::Slot::Facts), (std::vector{"widget.driver=1.2"}))); + expect(eq(d.at(dirs::Slot::Floors), (std::vector{"widget.driver >= 2.0"}))); mcpp::manifest::Manifest m; dirs::apply(m, d); - ASSERT_EQ(m.runtimeConfig.provides.size(), 1u); - EXPECT_EQ(m.runtimeConfig.provides[0], "widget.driver=1.2"); - ASSERT_EQ(m.runtimeConfig.requirements.size(), 1u); - EXPECT_EQ(m.runtimeConfig.requirements[0].kind, "version-floor"); - EXPECT_EQ(m.runtimeConfig.requirements[0].value, "widget.driver >= 2.0"); + expect(fatal(eq(m.runtimeConfig.provides.size(), 1u))); + expect(eq(m.runtimeConfig.provides[0], "widget.driver=1.2")); + expect(fatal(eq(m.runtimeConfig.requirements.size(), 1u))); + expect(eq(m.runtimeConfig.requirements[0].kind, "version-floor")); + expect(eq(m.runtimeConfig.requirements[0].value, "widget.driver >= 2.0")); // A claim about the build, not about the link or the run: the phase says // when it is decided. - EXPECT_EQ(m.runtimeConfig.requirements[0].phase, "build"); -} + expect(eq(m.runtimeConfig.requirements[0].phase, "build")); +}}; -TEST(BuildDirectives, APackFormatIsANameCarriedAndNotInterpreted) { +UTest a_pack_format_is_a_name_carried_and_not_interpreted{"BuildDirectives/APackFormatIsANameCarriedAndNotInterpreted", [] { auto d = parse("mcpp:pack-format=appimage\n" "mcpp:pack-format=msi\n"); - EXPECT_EQ(d.at(dirs::Slot::PackFormats), - (std::vector{"appimage", "msi"})); + expect(eq(d.at(dirs::Slot::PackFormats), (std::vector{"appimage", "msi"}))); mcpp::manifest::Manifest m; dirs::apply(m, d); - EXPECT_EQ(m.buildConfig.packFormats, - (std::vector{"appimage", "msi"})); + expect(eq(m.buildConfig.packFormats, (std::vector{"appimage", "msi"}))); // The engine holds the DISPATCH and no format, so a name reaches neither // the compile line nor the link line. A row that leaked into either would // put dpkg's or WiX's vocabulary on a command line. - EXPECT_TRUE(m.buildConfig.cflags.empty()); - EXPECT_TRUE(m.buildConfig.cxxflags.empty()); - EXPECT_TRUE(m.buildConfig.ldflags.empty()); - EXPECT_TRUE(m.buildConfig.sources.empty()); - EXPECT_TRUE(m.buildConfig.actions.empty()); -} - -TEST(BuildDirectives, APackFormatDeclarationIsPersisted) { + expect(m.buildConfig.cflags.empty()); + expect(m.buildConfig.cxxflags.empty()); + expect(m.buildConfig.ldflags.empty()); + expect(m.buildConfig.sources.empty()); + expect(m.buildConfig.actions.empty()); +}}; + +UTest a_pack_format_declaration_is_persisted{"BuildDirectives/APackFormatDeclarationIsPersisted", [] { // THE HALF THAT WOULD OTHERWISE BE MISSED. A build program's result is // cached and a hit does not re-run it, and the pass that READS this set is // `mcpp pack`, which is never a project's first build. A declaration that @@ -318,48 +314,48 @@ TEST(BuildDirectives, APackFormatDeclarationIsPersisted) { // tagged row, so this only has to hold the field that puts the row in that // set -- which is the field a new row is most likely to be added without. const auto* def = dirs::find_by_wire("pack-format"); - ASSERT_NE(def, nullptr); - EXPECT_FALSE(def->tag.empty()) << "a tag-less row is not replayed on a cache hit"; - EXPECT_EQ(def->slot, dirs::Slot::PackFormats); - EXPECT_EQ(def->scope, dirs::Scope::Claim); -} + expect(fatal(ne(def, nullptr))); + expect(!(def->tag.empty())) << "a tag-less row is not replayed on a cache hit"; + expect(eq(def->slot, dirs::Slot::PackFormats)); + expect(eq(def->scope, dirs::Scope::Claim)); +}}; -TEST(BuildDirectives, AClaimReachesNeitherCompileNorLink) { +UTest a_claim_reaches_neither_compile_nor_link{"BuildDirectives/AClaimReachesNeitherCompileNorLink", [] { auto d = parse("mcpp:fact=widget.driver=1.2\n" "mcpp:floor=widget.driver >= 2.0\n"); mcpp::manifest::Manifest m; dirs::apply(m, d); - EXPECT_TRUE(m.buildConfig.cflags.empty()); - EXPECT_TRUE(m.buildConfig.cxxflags.empty()); - EXPECT_TRUE(m.buildConfig.ldflags.empty()); - EXPECT_TRUE(m.buildConfig.sources.empty()); + expect(m.buildConfig.cflags.empty()); + expect(m.buildConfig.cxxflags.empty()); + expect(m.buildConfig.ldflags.empty()); + expect(m.buildConfig.sources.empty()); for (auto const& def : dirs::kTable) if (def.slot == dirs::Slot::Facts || def.slot == dirs::Slot::Floors) - EXPECT_EQ(def.scope, dirs::Scope::Claim) << def.wire; -} + expect(eq(def.scope, dirs::Scope::Claim)) << def.wire; +}}; -TEST(BuildDirectives, RerunSlotsAreNotPersistedAsDirectives) { +UTest rerun_slots_are_not_persisted_as_directives{"BuildDirectives/RerunSlotsAreNotPersistedAsDirectives", [] { auto d = parse("mcpp:rerun-if-changed=config.h\n" "mcpp:rerun-if-env-changed=USE_FAST\n"); - EXPECT_EQ(d.at(dirs::Slot::RerunFiles), (std::vector{"config.h"})); - EXPECT_EQ(d.at(dirs::Slot::RerunEnv), (std::vector{"USE_FAST"})); + expect(eq(d.at(dirs::Slot::RerunFiles), (std::vector{"config.h"}))); + expect(eq(d.at(dirs::Slot::RerunEnv), (std::vector{"USE_FAST"}))); std::ostringstream os; dirs::serialize(os, d); - EXPECT_TRUE(os.str().empty()); -} + expect(os.str().empty()); +}}; -TEST(BuildDirectives, UnknownCacheTagIsRejectedRatherThanDropped) { +UTest unknown_cache_tag_is_rejected_rather_than_dropped{"BuildDirectives/UnknownCacheTagIsRejectedRatherThanDropped", [] { // A cache written by a NEWER mcpp carries tags this one does not know. // Silently skipping them would apply a strict subset of what the program // asked for; the caller turns `false` into "entry is stale". dirs::Directives d; - EXPECT_FALSE(dirs::accept_cache_record(d, "some-future-tag", "value")); - EXPECT_TRUE(dirs::accept_cache_record(d, "cxxflag", "-Wall")); -} + expect(!dirs::accept_cache_record(d, "some-future-tag", "value")); + expect(bool(dirs::accept_cache_record(d, "cxxflag", "-Wall"))); +}}; // ── Apply ────────────────────────────────────────────────────────────────── -TEST(BuildDirectives, ApplyRoutesEachSlotToItsManifestChannel) { +UTest apply_routes_each_slot_to_its_manifest_channel{"BuildDirectives/ApplyRoutesEachSlotToItsManifestChannel", [] { auto d = parse("mcpp:cxxflag=-Wall\n" "mcpp:cflag=-std=c11\n" "mcpp:link-lib=z\n" @@ -373,41 +369,31 @@ TEST(BuildDirectives, ApplyRoutesEachSlotToItsManifestChannel) { dirs::apply(m, d); auto const& bc = m.buildConfig; - EXPECT_NE(std::find(bc.cxxflags.begin(), bc.cxxflags.end(), "-Wall"), - bc.cxxflags.end()); - EXPECT_NE(std::find(bc.cflags.begin(), bc.cflags.end(), "-std=c11"), - bc.cflags.end()); - EXPECT_NE(std::find(bc.ldflags.begin(), bc.ldflags.end(), "-lz"), - bc.ldflags.end()); + expect(ne(std::find(bc.cxxflags.begin(), bc.cxxflags.end(), "-Wall"), bc.cxxflags.end())); + expect(ne(std::find(bc.cflags.begin(), bc.cflags.end(), "-std=c11"), bc.cflags.end())); + expect(ne(std::find(bc.ldflags.begin(), bc.ldflags.end(), "-lz"), bc.ldflags.end())); // A cfg define colours BOTH language channels — the one slot that fans out. - EXPECT_NE(std::find(bc.cflags.begin(), bc.cflags.end(), "-DHAVE_X"), - bc.cflags.end()); - EXPECT_NE(std::find(bc.cxxflags.begin(), bc.cxxflags.end(), "-DHAVE_X"), - bc.cxxflags.end()); + expect(ne(std::find(bc.cflags.begin(), bc.cflags.end(), "-DHAVE_X"), bc.cflags.end())); + expect(ne(std::find(bc.cxxflags.begin(), bc.cxxflags.end(), "-DHAVE_X"), bc.cxxflags.end())); // generated= and source= must reach BOTH source lists: the scanner walks // the legacy modules.sources mirror, and a file missing from it is // invisible to the module scan. for (auto const& s : {"src/gen.cpp", "vendor/pick.cpp"}) { - EXPECT_NE(std::find(bc.sources.begin(), bc.sources.end(), s), - bc.sources.end()) << s; - EXPECT_NE(std::find(m.modules.sources.begin(), m.modules.sources.end(), s), - m.modules.sources.end()) << s; + expect(ne(std::find(bc.sources.begin(), bc.sources.end(), s), bc.sources.end())) << s; + expect(ne(std::find(m.modules.sources.begin(), m.modules.sources.end(), s), m.modules.sources.end())) << s; } - EXPECT_NE(std::find(bc.includeDirs.begin(), bc.includeDirs.end(), - std::filesystem::path(under_root("inc"))), - bc.includeDirs.end()); - EXPECT_NE(std::find(bc.includeDirsAfter.begin(), bc.includeDirsAfter.end(), - std::filesystem::path(under_root("after"))), - bc.includeDirsAfter.end()); -} + expect(ne(std::find(bc.includeDirs.begin(), bc.includeDirs.end(), + std::filesystem::path(under_root("inc"))), bc.includeDirs.end())); + expect(ne(std::find(bc.includeDirsAfter.begin(), bc.includeDirsAfter.end(), + std::filesystem::path(under_root("after"))), bc.includeDirsAfter.end())); +}}; -TEST(BuildDirectives, ApplyAppendsRatherThanReplaces) { +UTest apply_appends_rather_than_replaces{"BuildDirectives/ApplyAppendsRatherThanReplaces", [] { mcpp::manifest::Manifest m; m.buildConfig.cxxflags.push_back("-O2"); dirs::apply(m, parse("mcpp:cxxflag=-Wall\n")); - EXPECT_EQ(m.buildConfig.cxxflags, - (std::vector{"-O2", "-Wall"})); -} + expect(eq(m.buildConfig.cxxflags, (std::vector{"-O2", "-Wall"}))); +}}; // ── Private-scope fold ───────────────────────────────────────────────────── @@ -422,7 +408,7 @@ struct FakeUsage { }; } // namespace -TEST(BuildDirectives, FoldMovesOnlyTheTailAndOnlyPrivateChannels) { +UTest fold_moves_only_the_tail_and_only_private_channels{"BuildDirectives/FoldMovesOnlyTheTailAndOnlyPrivateChannels", [] { mcpp::manifest::Manifest m; m.buildConfig.cxxflags.push_back("-O2"); // pre-existing, not a directive m.buildConfig.includeDirs.emplace_back("/pre"); @@ -437,16 +423,15 @@ TEST(BuildDirectives, FoldMovesOnlyTheTailAndOnlyPrivateChannels) { dirs::fold_private_tail(priv, m, before); // Only what the program added, and only the private channels. - EXPECT_EQ(priv.cxxflags, (std::vector{"-Wall", "-DHAVE_X"})); - EXPECT_EQ(priv.cflags, (std::vector{"-DHAVE_X"})); - EXPECT_EQ(priv.includeDirs, - (std::vector{under_root("inc")})); + expect(eq(priv.cxxflags, (std::vector{"-Wall", "-DHAVE_X"}))); + expect(eq(priv.cflags, (std::vector{"-DHAVE_X"}))); + expect(eq(priv.includeDirs, (std::vector{under_root("inc")}))); // Link flags are NOT private — they reach the final link through their own // path, and folding them here would double-apply them. - EXPECT_TRUE(priv.includeDirsAfter.empty()); -} + expect(priv.includeDirsAfter.empty()); +}}; -TEST(BuildDirectives, FoldIsIdempotentOnIncludeDirs) { +UTest fold_is_idempotent_on_include_dirs{"BuildDirectives/FoldIsIdempotentOnIncludeDirs", [] { // Include dirs are unique-appended: the same dir emitted twice, or a fold // replayed, must not grow the list. mcpp::manifest::Manifest m; @@ -455,9 +440,8 @@ TEST(BuildDirectives, FoldIsIdempotentOnIncludeDirs) { FakeUsage priv; dirs::fold_private_tail(priv, m, before); dirs::fold_private_tail(priv, m, before); - EXPECT_EQ(priv.includeDirs, - (std::vector{under_root("inc")})); -} + expect(eq(priv.includeDirs, (std::vector{under_root("inc")}))); +}}; // ── Run bound ────────────────────────────────────────────────────────────── @@ -469,7 +453,7 @@ TEST(BuildDirectives, FoldIsIdempotentOnIncludeDirs) { // With a plain `int` those two would be the same value, and since 0 MEANS "no // limit", every project that never mentions the key would silently lose its // bound. That is why both levels are optionals all the way down. -TEST(BuildDirectives, RunTimeoutPrecedenceIsEnvThenManifestThenDefault) { +UTest run_timeout_precedence_is_env_then_manifest_then_default{"BuildDirectives/RunTimeoutPrecedenceIsEnvThenManifestThenDefault", [] { using ms = std::chrono::milliseconds; constexpr auto def = ms(dirs::kDefaultRunTimeoutSecs * 1000); const std::optional unset; @@ -488,15 +472,14 @@ TEST(BuildDirectives, RunTimeoutPrecedenceIsEnvThenManifestThenDefault) { { 1, 1, ms(1'000) }, }; for (auto const& r : rows) { - EXPECT_EQ(dirs::run_timeout(r.env, r.manifest), r.want) - << "env=" << (r.env ? std::to_string(*r.env) : "unset") + if (auto&& failed = expect(eq(dirs::run_timeout(r.env, r.manifest), r.want)); !failed) failed << "env=" << (r.env ? std::to_string(*r.env) : "unset") << " manifest=" << (r.manifest ? std::to_string(*r.manifest) : "unset"); } // Default: bounded. An unbounded build program is how a build hangs with // no diagnostic at all. - EXPECT_GT(dirs::run_timeout(unset, unset).count(), 0); -} + expect(gt(dirs::run_timeout(unset, unset).count(), 0)); +}}; // ── Glob inputs (#359) ───────────────────────────────────────────────────── // @@ -531,20 +514,20 @@ struct GlobTree { } // namespace -TEST(BuildDirectives, GlobFingerprintChangesWhenAFileAppearsOrDisappears) { +UTest glob_fingerprint_changes_when_a_file_appears_or_disappears{"BuildDirectives/GlobFingerprintChangesWhenAFileAppearsOrDisappears", [] { GlobTree t{"mcpp_glob_fp_membership"}; t.write("proto/a.proto", "syntax=\"proto3\";"); auto one = t.fp("proto/**/*.proto"); t.write("proto/b.proto", "syntax=\"proto3\";"); auto two = t.fp("proto/**/*.proto"); - EXPECT_NE(one, two); + expect(ne(one, two)); std::filesystem::remove(t.root / "proto/b.proto"); - EXPECT_EQ(t.fp("proto/**/*.proto"), one); -} + expect(eq(t.fp("proto/**/*.proto"), one)); +}}; -TEST(BuildDirectives, GlobFingerprintIgnoresContentSizeAndTimestamp) { +UTest glob_fingerprint_ignores_content_size_and_timestamp{"BuildDirectives/GlobFingerprintIgnoresContentSizeAndTimestamp", [] { // Contents are covered by the ordinary `rerun-if-changed` entry for that // file. Folding them in here would only add false re-runs — and mtime is // unstable across git checkout, container builds and rsync, which this @@ -557,10 +540,10 @@ TEST(BuildDirectives, GlobFingerprintIgnoresContentSizeAndTimestamp) { std::filesystem::last_write_time( t.root / "proto/a.proto", std::filesystem::file_time_type::clock::now() + std::chrono::hours(1)); - EXPECT_EQ(t.fp("proto/**/*.proto"), before); -} + expect(eq(t.fp("proto/**/*.proto"), before)); +}}; -TEST(BuildDirectives, GlobFingerprintNeverWalksTheBuildOutputTree) { +UTest glob_fingerprint_never_walks_the_build_output_tree{"BuildDirectives/GlobFingerprintNeverWalksTheBuildOutputTree", [] { // A build program writes its outputs INSIDE the project. If a wide pattern // included them the set would change on every build and the program would // re-run forever — the classic Cargo footgun. Enforced by the engine @@ -570,34 +553,34 @@ TEST(BuildDirectives, GlobFingerprintNeverWalksTheBuildOutputTree) { auto before = t.fp("**"); t.write("target/.build-mcpp/out/a.pb.cc", "generated"); t.write("target/.build-mcpp/out/a.pb.h", "generated"); - EXPECT_EQ(t.fp("**"), before); + expect(eq(t.fp("**"), before)); // .git is excluded for the same reason: it changes on every commit and // never means the build program's inputs changed. t.write(".git/HEAD", "ref: refs/heads/main"); - EXPECT_EQ(t.fp("**"), before); -} + expect(eq(t.fp("**"), before)); +}}; -TEST(BuildDirectives, GlobFingerprintIsIndependentOfDirectoryIterationOrder) { +UTest glob_fingerprint_is_independent_of_directory_iteration_order{"BuildDirectives/GlobFingerprintIsIndependentOfDirectoryIterationOrder", [] { // The set is sorted before hashing, so two trees with the same members // agree regardless of the order the platform hands them back. GlobTree a{"mcpp_glob_fp_order_a"}; GlobTree b{"mcpp_glob_fp_order_b"}; for (auto n : { "z.proto", "a.proto", "m.proto" }) a.write(std::string("proto/") + n, "x"); for (auto n : { "a.proto", "m.proto", "z.proto" }) b.write(std::string("proto/") + n, "y"); - EXPECT_EQ(a.fp("proto/**/*.proto"), b.fp("proto/**/*.proto")); -} + expect(eq(a.fp("proto/**/*.proto"), b.fp("proto/**/*.proto"))); +}}; -TEST(BuildDirectives, GlobDirectiveParsesIntoItsOwnSlot) { +UTest glob_directive_parses_into_its_own_slot{"BuildDirectives/GlobDirectiveParsesIntoItsOwnSlot", [] { auto d = parse("mcpp:rerun-if-changed-glob=proto/**/*.proto\n"); - ASSERT_EQ(d.at(dirs::Slot::RerunGlobs).size(), 1u); - EXPECT_EQ(d.at(dirs::Slot::RerunGlobs)[0], "proto/**/*.proto"); + expect(fatal(eq(d.at(dirs::Slot::RerunGlobs).size(), 1u))); + expect(eq(d.at(dirs::Slot::RerunGlobs)[0], "proto/**/*.proto")); // A re-run key is not a build input, so it must not be persisted as a `d` // record — otherwise a cache hit would replay it as one. std::ostringstream os; dirs::serialize(os, d); - EXPECT_EQ(os.str().find("proto/**"), std::string::npos) << os.str(); -} + expect(eq(os.str().find("proto/**"), std::string::npos)) << os.str(); +}}; // ── link-script ──────────────────────────────────────────────────────────── // @@ -607,17 +590,17 @@ TEST(BuildDirectives, GlobDirectiveParsesIntoItsOwnSlot) { // `link-search` emits `-L`) — so without it the one thing a user cannot write // for themselves was the one thing they had to. -TEST(BuildDirectives, LinkScriptBecomesDashTWithAnAbsolutePath) { +UTest link_script_becomes_dash_t_with_an_absolute_path{"BuildDirectives/LinkScriptBecomesDashTWithAnAbsolutePath", [] { auto d = parse("mcpp:link-script=board/link.ld\n"); auto& ld = d.at(dirs::Slot::LdFlags); - ASSERT_EQ(ld.size(), 1u); + expect(fatal(eq(ld.size(), 1u))); // Absolute, because the link runs in the build directory: a relative path // resolves against the wrong root and lld answers "cannot find linker // script link.ld" (measured). - EXPECT_EQ(ld[0], "-T " + under_root("board/link.ld")); -} + expect(eq(ld[0], "-T " + under_root("board/link.ld"))); +}}; -TEST(BuildDirectives, LinkScriptIsLinkGlobalSoItReachesTheConsumer) { +UTest link_script_is_link_global_so_it_reaches_the_consumer{"BuildDirectives/LinkScriptIsLinkGlobalSoItReachesTheConsumer", [] { // The whole point: a dependency's build.mcpp must be able to widen the // LINK (as link-lib/link-search already do) without widening the public // COMPILE interface (which include-dir deliberately cannot). @@ -627,21 +610,21 @@ TEST(BuildDirectives, LinkScriptIsLinkGlobalSoItReachesTheConsumer) { if (def.wire == "link-script") script = &def; if (def.wire == "include-dir") incdir = &def; } - ASSERT_NE(script, nullptr); - ASSERT_NE(incdir, nullptr); - EXPECT_EQ(script->scope, dirs::Scope::LinkGlobal); - EXPECT_EQ(incdir->scope, dirs::Scope::PackagePrivate); -} + expect(fatal(ne(script, nullptr))); + expect(fatal(ne(incdir, nullptr))); + expect(eq(script->scope, dirs::Scope::LinkGlobal)); + expect(eq(incdir->scope, dirs::Scope::PackagePrivate)); +}}; -TEST(BuildDirectives, LinkScriptDoesNotClaimADeclaredOutput) { +UTest link_script_does_not_claim_a_declared_output{"BuildDirectives/LinkScriptDoesNotClaimADeclaredOutput", [] { // `mustExistAfterRun` assumes the value IS a path; this row's transformed // value is `-T `, so the check would test the wrong string and // reject a script that is right there. for (auto const& def : dirs::kTable) { if (def.wire != "link-script") continue; - EXPECT_FALSE(def.mustExistAfterRun); + expect(!def.mustExistAfterRun); } -} +}}; // ── runner ───────────────────────────────────────────────────────────────── // @@ -650,7 +633,7 @@ TEST(BuildDirectives, LinkScriptDoesNotClaimADeclaredOutput) { // the emulator lives in a package payload whose path carries a home and a // version, and only a build program can compute that. -TEST(BuildDirectives, RunnerBuildsAnArgvFromRepeatedLines) { +UTest runner_builds_an_argv_from_repeated_lines{"BuildDirectives/RunnerBuildsAnArgvFromRepeatedLines", [] { // argv is an ordered list and a directive is one line = one value, so the // list is built by repetition. Order is the emission order. auto d = parse("mcpp:runner=/payload/bin/qemu-system-riscv64\n" @@ -658,54 +641,54 @@ TEST(BuildDirectives, RunnerBuildsAnArgvFromRepeatedLines) { "mcpp:runner=virt\n" "mcpp:runner=-kernel\n"); auto& r = d.at(dirs::Slot::Runner); - ASSERT_EQ(r.size(), 4u); - EXPECT_EQ(r[0], "/payload/bin/qemu-system-riscv64"); - EXPECT_EQ(r[1], "-machine"); - EXPECT_EQ(r[3], "-kernel"); -} + expect(fatal(eq(r.size(), 4u))); + expect(eq(r[0], "/payload/bin/qemu-system-riscv64")); + expect(eq(r[1], "-machine")); + expect(eq(r[3], "-kernel")); +}}; -TEST(BuildDirectives, RunnerTokensAreVerbatim) { +UTest runner_tokens_are_verbatim{"BuildDirectives/RunnerTokensAreVerbatim", [] { // Not a path to normalize: `-bios` is a token, and the producer already // resolved the executable absolutely (a bare name resolves through PATH to // a shim that dispatches against its OWNER home — measured in CI). auto d = parse("mcpp:runner=-bios\nmcpp:runner=none\n"); auto& r = d.at(dirs::Slot::Runner); - ASSERT_EQ(r.size(), 2u); - EXPECT_EQ(r[0], "-bios"); - EXPECT_EQ(r[1], "none"); -} + expect(fatal(eq(r.size(), 2u))); + expect(eq(r[0], "-bios")); + expect(eq(r[1], "none")); +}}; -TEST(BuildDirectives, RunnerHasItsOwnSlotAndScope) { +UTest runner_has_its_own_slot_and_scope{"BuildDirectives/RunnerHasItsOwnSlotAndScope", [] { const dirs::Def* runner = nullptr; const dirs::Def* script = nullptr; for (auto const& def : dirs::kTable) { if (def.wire == "runner") runner = &def; if (def.wire == "link-script") script = &def; } - ASSERT_NE(runner, nullptr); - ASSERT_NE(script, nullptr); + expect(fatal(ne(runner, nullptr))); + expect(fatal(ne(script, nullptr))); // Its own slot: putting it in LdFlags would put an emulator's argv on the // linker command line. - EXPECT_EQ(runner->slot, dirs::Slot::Runner); - EXPECT_NE(runner->slot, script->slot); + expect(eq(runner->slot, dirs::Slot::Runner)); + expect(ne(runner->slot, script->slot)); // Its own scope: RunGlobal propagates like LinkGlobal but carries an // exactly-one-provider rule that LinkGlobal must not inherit — two // dependencies' link flags concatenate correctly, two runners cannot. - EXPECT_EQ(runner->scope, dirs::Scope::RunGlobal); - EXPECT_EQ(script->scope, dirs::Scope::LinkGlobal); -} + expect(eq(runner->scope, dirs::Scope::RunGlobal)); + expect(eq(script->scope, dirs::Scope::LinkGlobal)); +}}; -TEST(BuildDirectives, RunnerLandsInBuildConfigNotInLdflags) { +UTest runner_lands_in_build_config_not_in_ldflags{"BuildDirectives/RunnerLandsInBuildConfigNotInLdflags", [] { auto d = parse("mcpp:runner=/qemu\nmcpp:runner=-kernel\n" "mcpp:link-lib=c\n"); mcpp::manifest::Manifest m; dirs::apply(m, d); - ASSERT_EQ(m.buildConfig.runner.size(), 2u); - EXPECT_EQ(m.buildConfig.runner[0], "/qemu"); + expect(fatal(eq(m.buildConfig.runner.size(), 2u))); + expect(eq(m.buildConfig.runner[0], "/qemu")); // The link line must carry the library and nothing of the runner. for (auto const& f : m.buildConfig.ldflags) - EXPECT_EQ(f.find("qemu"), std::string::npos) << f; -} + expect(eq(f.find("qemu"), std::string::npos)) << f; +}}; // ── Build-graph node decoding: `mcpp:action=` ─────────────────────────────── // @@ -716,35 +699,35 @@ TEST(BuildDirectives, RunnerLandsInBuildConfigNotInLdflags) { // matches "the field was never set" — so an action a build program submitted // before this field existed decodes exactly as it always has. -TEST(BuildDirectives, DecodeActionRoundTripsDepfile) { +UTest decode_action_round_trips_depfile{"BuildDirectives/DecodeActionRoundTripsDepfile", [] { auto d = parse( "mcpp:action={\"id\":\"shader\",\"role\":\"source\"," "\"description\":\"\",\"blocking\":false,\"depfile\":\"out/shader.spv.d\"," "\"inputs\":[],\"outputs\":[\"out/shader.spv\"]," "\"command\":[\"glslc.sh\"],\"provides\":[],\"imports\":[],\"targets\":[]}\n"); - ASSERT_EQ(d.at(dirs::Slot::Actions).size(), 1u); + expect(fatal(eq(d.at(dirs::Slot::Actions).size(), 1u))); auto a = dirs::decode_action(d.at(dirs::Slot::Actions).front()); - ASSERT_TRUE(a.has_value()); - EXPECT_EQ(a->depfile, "out/shader.spv.d"); -} + expect(fatal(a.has_value())); + expect(eq(a->depfile, "out/shader.spv.d")); +}}; // THE CONTROL — and the common case, since `depfile` is optional and // `hostprogram.cppm`'s `submit()` omits the key entirely when it was never // set (byte-identical payload to before the field existed). A decoder that // required the key would reject every action submitted by that path. -TEST(BuildDirectives, DecodeActionDefaultsDepfileToEmptyWhenAbsent) { +UTest decode_action_defaults_depfile_to_empty_when_absent{"BuildDirectives/DecodeActionDefaultsDepfileToEmptyWhenAbsent", [] { auto d = parse( "mcpp:action={\"id\":\"gen\",\"role\":\"source\"," "\"description\":\"\",\"blocking\":false," "\"inputs\":[],\"outputs\":[\"out/gen.cpp\"]," "\"command\":[\"gen.sh\"],\"provides\":[],\"imports\":[],\"targets\":[]}\n"); - ASSERT_EQ(d.at(dirs::Slot::Actions).size(), 1u); + expect(fatal(eq(d.at(dirs::Slot::Actions).size(), 1u))); auto a = dirs::decode_action(d.at(dirs::Slot::Actions).front()); - ASSERT_TRUE(a.has_value()); - EXPECT_EQ(a->depfile, ""); -} + expect(fatal(a.has_value())); + expect(eq(a->depfile, "")); +}}; // The overflow marker. Until 2026.9.13.1 `mcpp::action` held its lists in // fixed arrays and set the marker when a declaration did not fit; the lists @@ -752,20 +735,20 @@ TEST(BuildDirectives, DecodeActionDefaultsDepfileToEmptyWhenAbsent) { // refusal stays -- an incomplete declaration must never be used -- and the // message has to say what is now true: nothing about a buffer size, nothing // recommending a response file for a list that no longer needs one. -TEST(BuildDirectives, OverflowMarkerIsRefusedAsAllocationFailure) { +UTest overflow_marker_is_refused_as_allocation_failure{"BuildDirectives/OverflowMarkerIsRefusedAsAllocationFailure", [] { auto d = parse( "mcpp:action={\"id\":\"wide\",\"role\":\"source\"," "\"description\":\"\",\"blocking\":false,\"overflow\":true," "\"inputs\":[],\"outputs\":[\"out/a.txt\"]," "\"command\":[\"gen\"],\"provides\":[],\"imports\":[],\"targets\":[]}\n"); auto err = dirs::action_error(d); - ASSERT_FALSE(err.empty()); - EXPECT_NE(err.find("could not be stored"), std::string::npos); - EXPECT_NE(err.find("out of memory"), std::string::npos); - EXPECT_EQ(err.find("fixed buffer"), std::string::npos); - EXPECT_EQ(err.find("response file"), std::string::npos); - EXPECT_EQ(err.find("did not fit"), std::string::npos); -} + expect(fatal(!err.empty())); + expect(ne(err.find("could not be stored"), std::string::npos)); + expect(ne(err.find("out of memory"), std::string::npos)); + expect(eq(err.find("fixed buffer"), std::string::npos)); + expect(eq(err.find("response file"), std::string::npos)); + expect(eq(err.find("did not fit"), std::string::npos)); +}}; // ── mcpp#702: the five action roles, and `output_dir` (`prepare` only) ───── // @@ -776,7 +759,7 @@ TEST(BuildDirectives, OverflowMarkerIsRefusedAsAllocationFailure) { // -- a cache-hit replay reaches the payload through `apply` alone, never // through `action_error` (see the epoch-3 comment in program_protocol.cppm). -TEST(BuildDirectives, DecodeActionAcceptsEachOfTheFiveRoles) { +UTest decode_action_accepts_each_of_the_five_roles{"BuildDirectives/DecodeActionAcceptsEachOfTheFiveRoles", [] { struct Row { const char* role; mcpp::manifest::BuildAction::Role want; @@ -797,65 +780,65 @@ TEST(BuildDirectives, DecodeActionAcceptsEachOfTheFiveRoles) { "\"inputs\":[],\"outputs\":[\"out/a\"]," "\"command\":[\"cmd\"],\"provides\":[],\"imports\":[],\"targets\":[]}}\n", row.role, row.extra)); - ASSERT_EQ(d.at(dirs::Slot::Actions).size(), 1u) << row.role; + expect(fatal(eq(d.at(dirs::Slot::Actions).size(), 1u))) << row.role; auto a = dirs::decode_action(d.at(dirs::Slot::Actions).front()); - ASSERT_TRUE(a.has_value()) << row.role; - EXPECT_EQ(a->role, row.want) << row.role; + expect(fatal(a.has_value())) << row.role; + expect(eq(a->role, row.want)) << row.role; } -} +}}; // The default when `role` is absent is still `source`, exactly as it was // before `prepare` and `output_dir` existed. -TEST(BuildDirectives, DecodeActionDefaultsRoleToSourceWhenAbsent) { +UTest decode_action_defaults_role_to_source_when_absent{"BuildDirectives/DecodeActionDefaultsRoleToSourceWhenAbsent", [] { auto d = parse( "mcpp:action={\"id\":\"gen\"," "\"description\":\"\",\"blocking\":false," "\"inputs\":[],\"outputs\":[\"out/gen.cpp\"]," "\"command\":[\"gen.sh\"],\"provides\":[],\"imports\":[],\"targets\":[]}\n"); auto a = dirs::decode_action(d.at(dirs::Slot::Actions).front()); - ASSERT_TRUE(a.has_value()); - EXPECT_EQ(a->role, mcpp::manifest::BuildAction::Role::Source); -} + expect(fatal(a.has_value())); + expect(eq(a->role, mcpp::manifest::BuildAction::Role::Source)); +}}; -TEST(BuildDirectives, ActionErrorRefusesAnUnknownRoleAndListsTheFive) { +UTest action_error_refuses_an_unknown_role_and_lists_the_five{"BuildDirectives/ActionErrorRefusesAnUnknownRoleAndListsTheFive", [] { auto d = parse( "mcpp:action={\"id\":\"a\",\"role\":\"chek\"," "\"description\":\"\",\"blocking\":false," "\"inputs\":[],\"outputs\":[\"out/a\"]," "\"command\":[\"cmd\"],\"provides\":[],\"imports\":[],\"targets\":[]}\n"); auto err = dirs::action_error(d); - ASSERT_FALSE(err.empty()); - EXPECT_NE(err.find("\"chek\""), std::string::npos) << err; + expect(fatal(!err.empty())); + expect(ne(err.find("\"chek\""), std::string::npos)) << err; for (auto const* role : {"source", "check", "object", "artifact", "prepare"}) - EXPECT_NE(err.find(role), std::string::npos) << err; -} + expect(ne(err.find(role), std::string::npos)) << err; +}}; -TEST(BuildDirectives, DecodeActionRefusesAnUnknownRoleEvenOutsideActionError) { +UTest decode_action_refuses_an_unknown_role_even_outside_action_error{"BuildDirectives/DecodeActionRefusesAnUnknownRoleEvenOutsideActionError", [] { auto d = parse( "mcpp:action={\"id\":\"a\",\"role\":\"chek\"," "\"description\":\"\",\"blocking\":false," "\"inputs\":[],\"outputs\":[\"out/a\"]," "\"command\":[\"cmd\"],\"provides\":[],\"imports\":[],\"targets\":[]}\n"); - EXPECT_FALSE(dirs::decode_action(d.at(dirs::Slot::Actions).front()).has_value()); -} + expect(!(dirs::decode_action(d.at(dirs::Slot::Actions).front()).has_value())); +}}; // `output_dir` round-trips like `depfile`: present when the typed builder // set it, and read back verbatim. -TEST(BuildDirectives, DecodeActionRoundTripsOutputDir) { +UTest decode_action_round_trips_output_dir{"BuildDirectives/DecodeActionRoundTripsOutputDir", [] { auto d = parse( "mcpp:action={\"id\":\"prep\",\"role\":\"prepare\"," "\"description\":\"\",\"blocking\":false,\"output_dir\":\"out/prefix\"," "\"inputs\":[],\"outputs\":[\"out/prep.stamp\"]," "\"command\":[\"install.sh\"],\"provides\":[],\"imports\":[],\"targets\":[]}\n"); auto a = dirs::decode_action(d.at(dirs::Slot::Actions).front()); - ASSERT_TRUE(a.has_value()); - EXPECT_EQ(a->outputDir, "out/prefix"); -} + expect(fatal(a.has_value())); + expect(eq(a->outputDir, "out/prefix")); +}}; // mcpp#708, protocol 13: `env` and `cwd` round-trip like `output_dir` -- // present when the typed builder set them, absent otherwise, and an action // without them decodes exactly as it did under protocol 12. -TEST(BuildDirectives, DecodeActionRoundTripsEnvAndCwd) { +UTest decode_action_round_trips_env_and_cwd{"BuildDirectives/DecodeActionRoundTripsEnvAndCwd", [] { auto d = parse( "mcpp:action={\"id\":\"gen\",\"role\":\"source\"," "\"description\":\"\",\"blocking\":false," @@ -863,11 +846,11 @@ TEST(BuildDirectives, DecodeActionRoundTripsEnvAndCwd) { "\"inputs\":[],\"outputs\":[\"out/gen.cpp\"]," "\"command\":[\"gen\"],\"provides\":[],\"imports\":[],\"targets\":[]}\n"); auto a = dirs::decode_action(d.at(dirs::Slot::Actions).front()); - ASSERT_TRUE(a.has_value()); - ASSERT_EQ(a->env.size(), 2u); - EXPECT_EQ(a->env[0], "GEN_MODE=fast"); - EXPECT_EQ(a->env[1], "EMPTY="); // an empty value is a value - EXPECT_EQ(a->cwd, "third_party/gen"); + expect(fatal(a.has_value())); + expect(fatal(eq(a->env.size(), 2u))); + expect(eq(a->env[0], "GEN_MODE=fast")); + expect(eq(a->env[1], "EMPTY=")); // an empty value is a value + expect(eq(a->cwd, "third_party/gen")); auto plain = parse( "mcpp:action={\"id\":\"gen\",\"role\":\"source\"," @@ -875,41 +858,41 @@ TEST(BuildDirectives, DecodeActionRoundTripsEnvAndCwd) { "\"inputs\":[],\"outputs\":[\"out/gen.cpp\"]," "\"command\":[\"gen\"],\"provides\":[],\"imports\":[],\"targets\":[]}\n"); auto b = dirs::decode_action(plain.at(dirs::Slot::Actions).front()); - ASSERT_TRUE(b.has_value()); - EXPECT_TRUE(b->env.empty()); - EXPECT_TRUE(b->cwd.empty()); -} + expect(fatal(b.has_value())); + expect(b->env.empty()); + expect(b->cwd.empty()); +}}; // An `env` entry with no name is refused, and the refusal names the entry. -TEST(BuildDirectives, AnEnvEntryWithoutANameIsRefused) { +UTest an_env_entry_without_a_name_is_refused{"BuildDirectives/AnEnvEntryWithoutANameIsRefused", [] { auto d = parse( "mcpp:action={\"id\":\"gen\",\"role\":\"source\"," "\"description\":\"\",\"blocking\":false,\"env\":[\"=oops\"]," "\"inputs\":[],\"outputs\":[\"out/gen.cpp\"]," "\"command\":[\"gen\"],\"provides\":[],\"imports\":[],\"targets\":[]}\n"); - EXPECT_FALSE(dirs::decode_action(d.at(dirs::Slot::Actions).front()).has_value()); + expect(!(dirs::decode_action(d.at(dirs::Slot::Actions).front()).has_value())); const auto why = dirs::action_error(d); - EXPECT_NE(why.find("=oops"), std::string::npos) << why; - EXPECT_NE(why.find("NAME=value"), std::string::npos) << why; -} + expect(ne(why.find("=oops"), std::string::npos)) << why; + expect(ne(why.find("NAME=value"), std::string::npos)) << why; +}}; // A `prepare` action with no `output_dir` is refused: it would be a stamp // and nothing else, indistinguishable from a `check` that forgot // `blocking = true`. -TEST(BuildDirectives, PrepareActionWithNoOutputDirIsRefused) { +UTest prepare_action_with_no_output_dir_is_refused{"BuildDirectives/PrepareActionWithNoOutputDirIsRefused", [] { auto d = parse( "mcpp:action={\"id\":\"prep\",\"role\":\"prepare\"," "\"description\":\"\",\"blocking\":false," "\"inputs\":[],\"outputs\":[\"out/prep.stamp\"]," "\"command\":[\"install.sh\"],\"provides\":[],\"imports\":[],\"targets\":[]}\n"); - EXPECT_FALSE(dirs::decode_action(d.at(dirs::Slot::Actions).front()).has_value()); - EXPECT_FALSE(dirs::action_error(d).empty()); -} + expect(!(dirs::decode_action(d.at(dirs::Slot::Actions).front()).has_value())); + expect(!(dirs::action_error(d).empty())); +}}; // Cache round trip: `mcpp:action=` is persisted (Scope::GraphNode), and a // replayed `prepare` payload decodes to the same role and output_dir a fresh // run would -- the cache hit must replay both exactly. -TEST(BuildDirectives, PrepareActionSurvivesTheCacheRoundTrip) { +UTest prepare_action_survives_the_cache_round_trip{"BuildDirectives/PrepareActionSurvivesTheCacheRoundTrip", [] { // The paths are JSON strings: a Windows path's backslashes are escaped, // as the bundled module's serializer escapes them. auto json_string = [](std::string_view v) { @@ -927,12 +910,12 @@ TEST(BuildDirectives, PrepareActionSurvivesTheCacheRoundTrip) { "\"command\":[\"install.sh\"],\"provides\":[],\"imports\":[],\"targets\":[]}}", json_string(under_root("prefix")), json_string(under_root("prep.stamp"))); dirs::Directives replayed; - ASSERT_TRUE(dirs::accept_cache_record(replayed, "action", payload)); + expect(fatal(bool(dirs::accept_cache_record(replayed, "action", payload)))); auto a = dirs::decode_action(replayed.at(dirs::Slot::Actions).front()); - ASSERT_TRUE(a.has_value()); - EXPECT_EQ(a->role, mcpp::manifest::BuildAction::Role::Prepare); - EXPECT_EQ(a->outputDir, under_root("prefix")); -} + expect(fatal(a.has_value())); + expect(eq(a->role, mcpp::manifest::BuildAction::Role::Prepare)); + expect(eq(a->outputDir, under_root("prefix"))); +}}; // ── #618: a named executable's subsystem and entry ────────────────────────── // @@ -957,27 +940,27 @@ mcpp::manifest::Manifest manifest_with_gui_and_core() { } // namespace -TEST(BuildDirectives, WindowsSubsystemReachesTheNamedExecutableOnly) { +UTest windows_subsystem_reaches_the_named_executable_only{"BuildDirectives/WindowsSubsystemReachesTheNamedExecutableOnly", [] { auto d = parse("mcpp:protocol=10\n" "mcpp:windows-subsystem=gui:windows\n" "mcpp:windows-entry=gui:wWinMain\n"); - ASSERT_FALSE(dirs::protocol_error(d).has_value()); + expect(fatal(!(dirs::protocol_error(d).has_value()))); auto m = manifest_with_gui_and_core(); - ASSERT_EQ(dirs::target_directive_error(m, d), ""); + expect(fatal(eq(dirs::target_directive_error(m, d), ""))); dirs::apply(m, d); - EXPECT_EQ(m.targets[0].windowsSubsystem, "windows"); - EXPECT_EQ(m.targets[0].windowsEntry, "wWinMain"); - EXPECT_TRUE(m.targets[1].windowsSubsystem.empty()); - EXPECT_TRUE(m.targets[1].windowsEntry.empty()); + expect(eq(m.targets[0].windowsSubsystem, "windows")); + expect(eq(m.targets[0].windowsEntry, "wWinMain")); + expect(m.targets[1].windowsSubsystem.empty()); + expect(m.targets[1].windowsEntry.empty()); // Nothing reaches a flag channel another target or a consumer reads. - EXPECT_TRUE(m.buildConfig.ldflags.empty()); - EXPECT_TRUE(m.buildConfig.cxxflags.empty()); -} + expect(m.buildConfig.ldflags.empty()); + expect(m.buildConfig.cxxflags.empty()); +}}; // #622 A3: `windows-subsystem`/`windows-entry` reach an `app` target exactly // as they reach a `bin` one -- `directives.cppm`'s `apply` and // `target_directive_error` ask `is_program()`, not `kind == Binary`. -TEST(BuildDirectives, WindowsSubsystemReachesAnApplicationTargetToo) { +UTest windows_subsystem_reaches_an_application_target_too{"BuildDirectives/WindowsSubsystemReachesAnApplicationTargetToo", [] { mcpp::manifest::Manifest m; m.package.name = "app"; mcpp::manifest::Target myapp; @@ -988,23 +971,23 @@ TEST(BuildDirectives, WindowsSubsystemReachesAnApplicationTargetToo) { auto d = parse("mcpp:protocol=10\n" "mcpp:windows-subsystem=myapp:windows\n" "mcpp:windows-entry=myapp:wWinMain\n"); - ASSERT_EQ(dirs::target_directive_error(m, d), ""); + expect(fatal(eq(dirs::target_directive_error(m, d), ""))); dirs::apply(m, d); - EXPECT_EQ(m.targets[0].windowsSubsystem, "windows"); - EXPECT_EQ(m.targets[0].windowsEntry, "wWinMain"); -} + expect(eq(m.targets[0].windowsSubsystem, "windows")); + expect(eq(m.targets[0].windowsEntry, "wWinMain")); +}}; -TEST(BuildDirectives, WindowsSubsystemRowsHaveTheTargetLinkScope) { +UTest windows_subsystem_rows_have_the_target_link_scope{"BuildDirectives/WindowsSubsystemRowsHaveTheTargetLinkScope", [] { for (auto wire : {"windows-subsystem", "windows-entry"}) { auto def = dirs::find_by_wire(wire); - ASSERT_NE(def, nullptr) << wire; - EXPECT_EQ(def->scope, dirs::Scope::TargetLink) << wire; - EXPECT_EQ(def->sinceProtocol, 10) << wire; - EXPECT_FALSE(def->tag.empty()) << wire; + expect(fatal(ne(def, nullptr))) << wire; + expect(eq(def->scope, dirs::Scope::TargetLink)) << wire; + expect(eq(def->sinceProtocol, 10)) << wire; + expect(!(def->tag.empty())) << wire; } -} +}}; -TEST(BuildDirectives, WindowsSubsystemRefusesWhatApplyCannotHonour) { +UTest windows_subsystem_refuses_what_apply_cannot_honour{"BuildDirectives/WindowsSubsystemRefusesWhatApplyCannotHonour", [] { const std::pair cases[] = { {"mcpp:windows-subsystem=windows\n", "which is not `:`"}, {"mcpp:windows-subsystem=gui:\n", "which is not `:`"}, @@ -1020,21 +1003,21 @@ TEST(BuildDirectives, WindowsSubsystemRefusesWhatApplyCannotHonour) { for (auto [text, expected] : cases) { auto d = parse(std::format("mcpp:protocol=10\n{}", text)); auto err = dirs::target_directive_error(manifest_with_gui_and_core(), d); - EXPECT_NE(err.find(expected), std::string::npos) << text << " -> " << err; + if (auto&& failed = expect(ne(err.find(expected), std::string::npos)); !failed) failed << text << " -> " << err; } -} +}}; -TEST(BuildDirectives, WindowsSubsystemThatContradictsTheManifestIsRefused) { +UTest windows_subsystem_that_contradicts_the_manifest_is_refused{"BuildDirectives/WindowsSubsystemThatContradictsTheManifestIsRefused", [] { auto m = manifest_with_gui_and_core(); m.targets[0].windowsSubsystem = "console"; auto d = parse("mcpp:protocol=10\nmcpp:windows-subsystem=gui:windows\n"); auto err = dirs::target_directive_error(m, d); - EXPECT_NE(err.find("[targets.gui] windows_subsystem = \"console\""), std::string::npos) + expect(ne(err.find("[targets.gui] windows_subsystem = \"console\""), std::string::npos)) << err; // Restating the manifest's own value is not a contradiction. m.targets[0].windowsSubsystem = "windows"; - EXPECT_EQ(dirs::target_directive_error(m, d), ""); -} + expect(eq(dirs::target_directive_error(m, d), "")); +}}; // ── #622 A4: `mcpp::deploy(from, to)` ─────────────────────────────────────── // @@ -1056,30 +1039,30 @@ mcpp::manifest::Manifest manifest_named(std::string name) { } // namespace -TEST(BuildDirectives, DeployRowIsProtocolElevenWithLinkGlobalScopeAndATag) { +UTest deploy_row_is_protocol_eleven_with_link_global_scope_and_a_tag{"BuildDirectives/DeployRowIsProtocolElevenWithLinkGlobalScopeAndATag", [] { auto def = dirs::find_by_wire("deploy"); - ASSERT_NE(def, nullptr); - EXPECT_EQ(def->scope, dirs::Scope::LinkGlobal); - EXPECT_EQ(def->sinceProtocol, 11); - EXPECT_FALSE(def->tag.empty()); - EXPECT_EQ(dirs::kProtocolVersion, 18); -} - -TEST(BuildDirectives, ProtocolEighteenIsAcceptedAndNineteenIsNot) { + expect(fatal(ne(def, nullptr))); + expect(eq(def->scope, dirs::Scope::LinkGlobal)); + expect(eq(def->sinceProtocol, 11)); + expect(!(def->tag.empty())); + expect(eq(dirs::kProtocolVersion, 18)); +}}; + +UTest protocol_eighteen_is_accepted_and_nineteen_is_not{"BuildDirectives/ProtocolEighteenIsAcceptedAndNineteenIsNot", [] { auto ok = parse("mcpp:protocol=18\n"); - EXPECT_FALSE(dirs::protocol_error(ok).has_value()); + expect(!(dirs::protocol_error(ok).has_value())); auto no = parse("mcpp:protocol=19\n"); - EXPECT_TRUE(dirs::protocol_error(no).has_value()); -} + expect(dirs::protocol_error(no).has_value()); +}}; // SPEC-013 §4.1: the platform's fields a program states (protocol 17) become a // platform statement at the build-program tier; malformed lines are refused. -TEST(BuildDirectives, PlatformFieldsAreProtocolSeventeenAndReachTheStatement) { +UTest platform_fields_are_protocol_seventeen_and_reach_the_statement{"BuildDirectives/PlatformFieldsAreProtocolSeventeenAndReachTheStatement", [] { for (auto wire : {"link-driver", "link-input", "artifact-name", "seal", "run-host"}) { auto def = dirs::find_by_wire(wire); - ASSERT_NE(def, nullptr) << wire; - EXPECT_EQ(def->sinceProtocol, 17) << wire; - EXPECT_FALSE(def->tag.empty()) << wire; + expect(fatal(ne(def, nullptr))) << wire; + expect(eq(def->sinceProtocol, 17)) << wire; + expect(!(def->tag.empty())) << wire; } mcpp::manifest::Manifest m; auto d = parse("mcpp:link-driver=lld\n" @@ -1091,116 +1074,116 @@ TEST(BuildDirectives, PlatformFieldsAreProtocolSeventeenAndReachTheStatement) { "mcpp:seal=0:okld\nmcpp:seal=0:note\nmcpp:seal=0:{}\n" "mcpp:seal=1:okld\nmcpp:seal=1:check\nmcpp:seal=1:{}\n" "mcpp:run-host=same-arch\n"); - EXPECT_EQ(dirs::target_directive_error(m, d), ""); + expect(eq(dirs::target_directive_error(m, d), "")); dirs::apply(m, d); auto const& s = m.buildConfig.programPlatform; - EXPECT_EQ(s.link.driver.value, "lld"); - ASSERT_EQ(s.link.inputs.size(), 2u); - EXPECT_EQ(s.link.inputs[0].as, "notes.S"); - EXPECT_EQ(s.link.inputs[0].from, (std::vector{"okld", "note-source"})); - EXPECT_EQ(s.link.inputs[1].file, "/opt/crt0.o"); - EXPECT_EQ(s.link.exe.value, "{name}.oke"); - EXPECT_EQ(s.link.shared.value, "lib{name}.okso"); - ASSERT_EQ(s.link.seal.size(), 2u); - EXPECT_EQ(s.link.seal[1], (std::vector{"okld", "check", "{}"})); - EXPECT_EQ(s.runner.host, "same-arch"); - EXPECT_EQ(s.link.driver.origin.tier, mcpp::manifest::platform::Tier::Program); + expect(eq(s.link.driver.value, "lld")); + expect(fatal(eq(s.link.inputs.size(), 2u))); + expect(eq(s.link.inputs[0].as, "notes.S")); + expect(eq(s.link.inputs[0].from, (std::vector{"okld", "note-source"}))); + expect(eq(s.link.inputs[1].file, "/opt/crt0.o")); + expect(eq(s.link.exe.value, "{name}.oke")); + expect(eq(s.link.shared.value, "lib{name}.okso")); + expect(fatal(eq(s.link.seal.size(), 2u))); + expect(eq(s.link.seal[1], (std::vector{"okld", "check", "{}"}))); + expect(eq(s.runner.host, "same-arch")); + expect(eq(s.link.driver.origin.tier, mcpp::manifest::platform::Tier::Program)); for (auto bad : {"mcpp:link-driver=gold\n", "mcpp:link-input=from:x\n", "mcpp:artifact-name=exe:lib{name}\n", "mcpp:artifact-name=dll:{name}\n", "mcpp:artifact-name=shared:lib/{name}.okso\n", "mcpp:seal=x:okld\n", "mcpp:run-host=far\n"}) - EXPECT_NE(dirs::target_directive_error(m, parse(bad)), "") << bad; -} + expect(ne(dirs::target_directive_error(m, parse(bad)), "")) << bad; +}}; // Design 2026-10-26 §24.3: a board's image entry symbol reaches the manifest's // link intent; a non-symbol and two different symbols are refused. -TEST(BuildDirectives, LinkStartIsProtocolSixteenAndReachesTheLinkIntent) { +UTest link_start_is_protocol_sixteen_and_reaches_the_link_intent{"BuildDirectives/LinkStartIsProtocolSixteenAndReachesTheLinkIntent", [] { auto def = dirs::find_by_wire("link-start"); - ASSERT_NE(def, nullptr); - EXPECT_EQ(def->scope, dirs::Scope::LinkGlobal); - EXPECT_EQ(def->sinceProtocol, 16); + expect(fatal(ne(def, nullptr))); + expect(eq(def->scope, dirs::Scope::LinkGlobal)); + expect(eq(def->sinceProtocol, 16)); mcpp::manifest::Manifest m; auto d = parse("mcpp:link-start=_reset\n"); - EXPECT_EQ(dirs::target_directive_error(m, d), ""); + expect(eq(dirs::target_directive_error(m, d), "")); dirs::apply(m, d); - EXPECT_EQ(m.runtimeConfig.linkIntent.start, "_reset"); - EXPECT_NE(dirs::target_directive_error(m, parse("mcpp:link-start=1bad\n")), ""); - EXPECT_NE(dirs::target_directive_error( - m, parse("mcpp:link-start=_reset\nmcpp:link-start=_start\n")), ""); -} + expect(eq(m.runtimeConfig.linkIntent.start, "_reset")); + expect(ne(dirs::target_directive_error(m, parse("mcpp:link-start=1bad\n")), "")); + expect(ne(dirs::target_directive_error( + m, parse("mcpp:link-start=_reset\nmcpp:link-start=_start\n")), "")); +}}; -TEST(BuildDirectives, DeployWithAnAbsoluteFromYieldsOneEntryOnTheManifest) { +UTest deploy_with_an_absolute_from_yields_one_entry_on_the_manifest{"BuildDirectives/DeployWithAnAbsoluteFromYieldsOneEntryOnTheManifest", [] { const std::string from = under_root("gen/x.bin"); auto d = parse(std::format("mcpp:deploy={}\tres\n", from)); auto m = manifest_named("app"); - ASSERT_EQ(dirs::deploy_directive_error(m, d), ""); + expect(fatal(eq(dirs::deploy_directive_error(m, d), ""))); dirs::apply(m, d); - ASSERT_EQ(m.runtimeConfig.linkIntent.deploy.size(), 1u); - EXPECT_EQ(m.runtimeConfig.linkIntent.deploy[0].from.string(), from); - EXPECT_EQ(m.runtimeConfig.linkIntent.deploy[0].to, "res"); -} + expect(fatal(eq(m.runtimeConfig.linkIntent.deploy.size(), 1u))); + expect(eq(m.runtimeConfig.linkIntent.deploy[0].from.string(), from)); + expect(eq(m.runtimeConfig.linkIntent.deploy[0].to, "res")); +}}; // The manifest-sourced `from` rule (`deploy_path_problem`) refuses an absolute // path. A directive's `from` is not run through that check at all: it is // resolved to an absolute path by the table's own transform instead, because // it may be an action's own declared output (#622 A4). This is the case that // rule cannot see and must not reject. -TEST(BuildDirectives, DeployFromIsResolvedToAbsoluteEvenWhenTheWireValueAlreadyWasOne) { +UTest deploy_from_is_resolved_to_absolute_even_when_the_wire_value_already_was_one{"BuildDirectives/DeployFromIsResolvedToAbsoluteEvenWhenTheWireValueAlreadyWasOne", [] { const std::string absFrom = under_root("out/gen/res.bin"); auto d = parse(std::format("mcpp:deploy={}\tapp.resources\n", absFrom)); auto m = manifest_named("app"); - EXPECT_EQ(dirs::deploy_directive_error(m, d), ""); + expect(eq(dirs::deploy_directive_error(m, d), "")); dirs::apply(m, d); - ASSERT_EQ(m.runtimeConfig.linkIntent.deploy.size(), 1u); - EXPECT_EQ(m.runtimeConfig.linkIntent.deploy[0].from.string(), absFrom); -} + expect(fatal(eq(m.runtimeConfig.linkIntent.deploy.size(), 1u))); + expect(eq(m.runtimeConfig.linkIntent.deploy[0].from.string(), absFrom)); +}}; // A relative `from` resolves against the package root, exactly as // `include-dir`'s AbsPath case does — the directive never leaves a relative // path for a later stage to guess the base of. -TEST(BuildDirectives, DeployFromRelativeToTheRootIsMadeAbsolute) { +UTest deploy_from_relative_to_the_root_is_made_absolute{"BuildDirectives/DeployFromRelativeToTheRootIsMadeAbsolute", [] { auto d = parse("mcpp:deploy=gen/x.bin\tres\n"); auto m = manifest_named("app"); - ASSERT_EQ(dirs::deploy_directive_error(m, d), ""); + expect(fatal(eq(dirs::deploy_directive_error(m, d), ""))); dirs::apply(m, d); - ASSERT_EQ(m.runtimeConfig.linkIntent.deploy.size(), 1u); - EXPECT_EQ(m.runtimeConfig.linkIntent.deploy[0].from.string(), under_root("gen/x.bin")); -} + expect(fatal(eq(m.runtimeConfig.linkIntent.deploy.size(), 1u))); + expect(eq(m.runtimeConfig.linkIntent.deploy[0].from.string(), under_root("gen/x.bin"))); +}}; // The negative direction (#622 A4, and rule 8 of the design record): a `to` // `apply` cannot honour is refused BEFORE apply, naming the directive and the // package -- not only the path problem, which `deploy_path_problem` already // states on its own. -TEST(BuildDirectives, DeployToDotDotIsRefusedNamingTheDirectiveAndThePackage) { +UTest deploy_to_dot_dot_is_refused_naming_the_directive_and_the_package{"BuildDirectives/DeployToDotDotIsRefusedNamingTheDirectiveAndThePackage", [] { auto d = parse(std::format("mcpp:deploy={}\t../x\n", under_root("gen/x.bin"))); auto m = manifest_named("widget"); auto err = dirs::deploy_directive_error(m, d); - EXPECT_NE(err.find("deploy"), std::string::npos) << err; - EXPECT_NE(err.find("widget"), std::string::npos) << err; - EXPECT_NE(err.find("`.` or `..` component"), std::string::npos) << err; -} + expect(ne(err.find("deploy"), std::string::npos)) << err; + expect(ne(err.find("widget"), std::string::npos)) << err; + expect(ne(err.find("`.` or `..` component"), std::string::npos)) << err; +}}; -TEST(BuildDirectives, DeployWithAMalformedWireValueIsRefused) { +UTest deploy_with_a_malformed_wire_value_is_refused{"BuildDirectives/DeployWithAMalformedWireValueIsRefused", [] { auto d = parse("mcpp:deploy=onlyfromnotab\n"); auto m = manifest_named("app"); auto err = dirs::deploy_directive_error(m, d); - EXPECT_NE(err.find("deploy"), std::string::npos) << err; - EXPECT_NE(err.find("not `"), std::string::npos) << err; -} + expect(ne(err.find("deploy"), std::string::npos)) << err; + expect(ne(err.find("not `"), std::string::npos)) << err; +}}; // A `to` that IS honourable does not stop the build, and the collected entry // is the only thing `apply` touches -- no flag channel gains anything, unlike // nothing here (deploy is not a compile/link flag). -TEST(BuildDirectives, DeployReachesOnlyTheRuntimeDeployListNotAnyFlagChannel) { +UTest deploy_reaches_only_the_runtime_deploy_list_not_any_flag_channel{"BuildDirectives/DeployReachesOnlyTheRuntimeDeployListNotAnyFlagChannel", [] { auto d = parse(std::format("mcpp:deploy={}\t.\n", under_root("gen/x.bin"))); auto m = manifest_named("app"); - ASSERT_EQ(dirs::deploy_directive_error(m, d), ""); + expect(fatal(eq(dirs::deploy_directive_error(m, d), ""))); dirs::apply(m, d); - ASSERT_EQ(m.runtimeConfig.linkIntent.deploy.size(), 1u); - EXPECT_EQ(m.runtimeConfig.linkIntent.deploy[0].to, "."); - EXPECT_TRUE(m.buildConfig.ldflags.empty()); - EXPECT_TRUE(m.buildConfig.cxxflags.empty()); -} + expect(fatal(eq(m.runtimeConfig.linkIntent.deploy.size(), 1u))); + expect(eq(m.runtimeConfig.linkIntent.deploy[0].to, ".")); + expect(m.buildConfig.ldflags.empty()); + expect(m.buildConfig.cxxflags.empty()); +}}; // ── v12: `mcpp::runtime_search_dir(dir)` ──────────────────────────────────── // @@ -1212,97 +1195,96 @@ TEST(BuildDirectives, DeployReachesOnlyTheRuntimeDeployListNotAnyFlagChannel) { // the RUNPATH rendering, `mcpp pack`'s closure search) see a // directive-declared entry without any of them changing. -TEST(BuildDirectives, RuntimeSearchDirRowIsProtocolTwelveWithLinkGlobalScopeAndATag) { +UTest runtime_search_dir_row_is_protocol_twelve_with_link_global_scope_and_a_tag{"BuildDirectives/RuntimeSearchDirRowIsProtocolTwelveWithLinkGlobalScopeAndATag", [] { auto def = dirs::find_by_wire("runtime-search-dir"); - ASSERT_NE(def, nullptr); - EXPECT_EQ(def->scope, dirs::Scope::LinkGlobal); - EXPECT_EQ(def->sinceProtocol, 12); - EXPECT_FALSE(def->tag.empty()); - EXPECT_EQ(def->transform, dirs::Transform::AbsPath); -} + expect(fatal(ne(def, nullptr))); + expect(eq(def->scope, dirs::Scope::LinkGlobal)); + expect(eq(def->sinceProtocol, 12)); + expect(!(def->tag.empty())); + expect(eq(def->transform, dirs::Transform::AbsPath)); +}}; // The manifest key's own field, not a new one -- this is the whole point of // the design: no downstream consumer has to learn a second field exists. NOT // `RuntimeConfig::libraryDirs`, the retiring vector #702 as first written // joined by mistake (design §5.3 point 1). -TEST(BuildDirectives, RuntimeSearchDirJoinsLinkIntentDirectlyNotTheLegacyVector) { +UTest runtime_search_dir_joins_link_intent_directly_not_the_legacy_vector{"BuildDirectives/RuntimeSearchDirJoinsLinkIntentDirectlyNotTheLegacyVector", [] { auto d = parse("mcpp:runtime-search-dir=rtlib\n"); mcpp::manifest::Manifest m; dirs::apply(m, d); - ASSERT_EQ(m.runtimeConfig.linkIntent.runtimeSearchDirs.size(), 1u); - EXPECT_EQ(m.runtimeConfig.linkIntent.runtimeSearchDirs[0].string(), under_root("rtlib")); - EXPECT_TRUE(m.runtimeConfig.libraryDirs.empty()); -} + expect(fatal(eq(m.runtimeConfig.linkIntent.runtimeSearchDirs.size(), 1u))); + expect(eq(m.runtimeConfig.linkIntent.runtimeSearchDirs[0].string(), under_root("rtlib"))); + expect(m.runtimeConfig.libraryDirs.empty()); +}}; // Relative resolves against the package root, exactly as `include-dir`'s // AbsPath case and `deploy`'s `from` do -- the directive never leaves a // relative path for a later stage to guess the base of. -TEST(BuildDirectives, RuntimeSearchDirRelativeToTheRootIsMadeAbsolute) { +UTest runtime_search_dir_relative_to_the_root_is_made_absolute{"BuildDirectives/RuntimeSearchDirRelativeToTheRootIsMadeAbsolute", [] { auto d = parse("mcpp:runtime-search-dir=vendor/qt/bin\n"); mcpp::manifest::Manifest m; dirs::apply(m, d); - ASSERT_EQ(m.runtimeConfig.linkIntent.runtimeSearchDirs.size(), 1u); - EXPECT_EQ(m.runtimeConfig.linkIntent.runtimeSearchDirs[0].string(), - under_root("vendor/qt/bin")); -} + expect(fatal(eq(m.runtimeConfig.linkIntent.runtimeSearchDirs.size(), 1u))); + expect(eq(m.runtimeConfig.linkIntent.runtimeSearchDirs[0].string(), under_root("vendor/qt/bin"))); +}}; -TEST(BuildDirectives, RuntimeSearchDirAcceptsAnAlreadyAbsoluteValue) { +UTest runtime_search_dir_accepts_an_already_absolute_value{"BuildDirectives/RuntimeSearchDirAcceptsAnAlreadyAbsoluteValue", [] { const std::string abs = under_root("prefix/lib"); auto d = parse(std::format("mcpp:runtime-search-dir={}\n", abs)); mcpp::manifest::Manifest m; dirs::apply(m, d); - ASSERT_EQ(m.runtimeConfig.linkIntent.runtimeSearchDirs.size(), 1u); - EXPECT_EQ(m.runtimeConfig.linkIntent.runtimeSearchDirs[0].string(), abs); -} + expect(fatal(eq(m.runtimeConfig.linkIntent.runtimeSearchDirs.size(), 1u))); + expect(eq(m.runtimeConfig.linkIntent.runtimeSearchDirs[0].string(), abs)); +}}; // Reaches only the run-time search field -- no compile or link flag channel // gains anything, unlike `link-search`, which shares the manifest's -L/-L // distinction. -TEST(BuildDirectives, RuntimeSearchDirReachesNoFlagChannel) { +UTest runtime_search_dir_reaches_no_flag_channel{"BuildDirectives/RuntimeSearchDirReachesNoFlagChannel", [] { auto d = parse("mcpp:runtime-search-dir=rtlib\n"); mcpp::manifest::Manifest m; dirs::apply(m, d); - EXPECT_TRUE(m.buildConfig.ldflags.empty()); - EXPECT_TRUE(m.buildConfig.cxxflags.empty()); - EXPECT_TRUE(m.runtimeConfig.linkIntent.linkLibraryDirs.empty()); -} + expect(m.buildConfig.ldflags.empty()); + expect(m.buildConfig.cxxflags.empty()); + expect(m.runtimeConfig.linkIntent.linkLibraryDirs.empty()); +}}; // Multiple directives accumulate, in emission order, the same as every other // repeated directive in this table. -TEST(BuildDirectives, RuntimeSearchDirDirectivesAccumulate) { +UTest runtime_search_dir_directives_accumulate{"BuildDirectives/RuntimeSearchDirDirectivesAccumulate", [] { auto d = parse("mcpp:runtime-search-dir=a\n" "mcpp:runtime-search-dir=b\n"); mcpp::manifest::Manifest m; dirs::apply(m, d); - ASSERT_EQ(m.runtimeConfig.linkIntent.runtimeSearchDirs.size(), 2u); - EXPECT_EQ(m.runtimeConfig.linkIntent.runtimeSearchDirs[0].string(), under_root("a")); - EXPECT_EQ(m.runtimeConfig.linkIntent.runtimeSearchDirs[1].string(), under_root("b")); -} + expect(fatal(eq(m.runtimeConfig.linkIntent.runtimeSearchDirs.size(), 2u))); + expect(eq(m.runtimeConfig.linkIntent.runtimeSearchDirs[0].string(), under_root("a"))); + expect(eq(m.runtimeConfig.linkIntent.runtimeSearchDirs[1].string(), under_root("b"))); +}}; // Persisted like `deploy`, `warning` and `pack-format`: the cache tag is // non-empty and round-trips through the same table-driven serialize/ // accept_cache_record pair, so a build.mcpp CACHE HIT replays the directive // rather than losing it on every build after the first. -TEST(BuildDirectives, RuntimeSearchDirIsPersistedAndRoundTrips) { +UTest runtime_search_dir_is_persisted_and_round_trips{"BuildDirectives/RuntimeSearchDirIsPersistedAndRoundTrips", [] { auto d = parse("mcpp:runtime-search-dir=rtlib\n"); std::ostringstream os; dirs::serialize(os, d); - EXPECT_NE(os.str().find("d runtime-search-dir "), std::string::npos) << os.str(); + expect(ne(os.str().find("d runtime-search-dir "), std::string::npos)) << os.str(); dirs::Directives replayed; - ASSERT_TRUE(dirs::accept_cache_record(replayed, "runtime-search-dir", - under_root("rtlib"))); + expect(fatal(bool(dirs::accept_cache_record(replayed, "runtime-search-dir", + under_root("rtlib"))))); mcpp::manifest::Manifest m; dirs::apply(m, replayed); - ASSERT_EQ(m.runtimeConfig.linkIntent.runtimeSearchDirs.size(), 1u); - EXPECT_EQ(m.runtimeConfig.linkIntent.runtimeSearchDirs[0].string(), under_root("rtlib")); -} + expect(fatal(eq(m.runtimeConfig.linkIntent.runtimeSearchDirs.size(), 1u))); + expect(eq(m.runtimeConfig.linkIntent.runtimeSearchDirs[0].string(), under_root("rtlib"))); +}}; -TEST(BuildDirectives, OptionalActionProgressDefaultsOffAndRejectsWrongType) { +UTest optional_action_progress_defaults_off_and_rejects_wrong_type{"BuildDirectives/OptionalActionProgressDefaultsOffAndRejectsWrongType", [] { const std::string prefix=R"({"id":"work","role":"object","command":["worker"],"outputs":["result.o"])"; const auto old=dirs::decode_action(prefix+"}"); - ASSERT_TRUE(old); EXPECT_FALSE(old->progress); + expect(fatal(bool(old))); expect(!old->progress); const auto opted=dirs::decode_action(prefix+R"(,"progress":true})"); - ASSERT_TRUE(opted); EXPECT_TRUE(opted->progress); - EXPECT_FALSE(dirs::decode_action(prefix+R"(,"progress":"true"})")); -} + expect(fatal(bool(opted))); expect(bool(opted->progress)); + expect(!(dirs::decode_action(prefix+R"(,"progress":"true"})"))); +}}; diff --git a/tests/unit/test_build_flags.cpp b/tests/unit/test_build_flags.cpp index 31948e14b..a3eb7cc86 100644 --- a/tests/unit/test_build_flags.cpp +++ b/tests/unit/test_build_flags.cpp @@ -1,12 +1,14 @@ -#include import std; +import mcpplibs.mtest; import mcpp.build.flags; import mcpp.build.distribution; import mcpp.modgraph.scanner; import mcpp.build.ninja; import mcpp.targetside; +using namespace mcpplibs::mtest; + namespace { struct Tmp { @@ -36,64 +38,64 @@ void touch(const std::filesystem::path& p) { constexpr bool kDynamic = false; // staticLink arg constexpr bool kStatic = true; -TEST(BuildFlagsAtomic, EmittedWhenLibatomicSharedPresent) { +UTest emitted_when_libatomic_shared_present{"BuildFlagsAtomic/EmittedWhenLibatomicSharedPresent", [] { Tmp dir; touch(dir.path / "libatomic.so"); auto flag = mcpp::build::atomic_link_flag({dir.path}, kDynamic); - EXPECT_NE(flag.find("-latomic"), std::string::npos); - EXPECT_NE(flag.find("--as-needed"), std::string::npos); -} + expect(ne(flag.find("-latomic"), std::string::npos)); + expect(ne(flag.find("--as-needed"), std::string::npos)); +}}; -TEST(BuildFlagsAtomic, EmittedWhenLibatomicArchivePresent) { +UTest emitted_when_libatomic_archive_present{"BuildFlagsAtomic/EmittedWhenLibatomicArchivePresent", [] { Tmp dir; touch(dir.path / "libatomic.a"); auto flag = mcpp::build::atomic_link_flag({dir.path}, kDynamic); - EXPECT_NE(flag.find("-latomic"), std::string::npos); -} + expect(ne(flag.find("-latomic"), std::string::npos)); +}}; -TEST(BuildFlagsAtomic, EmptyWhenLibatomicAbsent) { +UTest empty_when_libatomic_absent{"BuildFlagsAtomic/EmptyWhenLibatomicAbsent", [] { Tmp dir; touch(dir.path / "libc++.so.1"); // some other lib, but no libatomic auto flag = mcpp::build::atomic_link_flag({dir.path}, kDynamic); - EXPECT_TRUE(flag.empty()) << "got: " << flag; -} + expect(flag.empty()) << "got: " << flag; +}}; // `-latomic` resolves only `libatomic.so` / `libatomic.a` — a bare // soname-versioned `libatomic.so.1` (no dev symlink, no archive) is NOT // link-resolvable, so the flag must stay empty or the link breaks with // "cannot find -latomic". -TEST(BuildFlagsAtomic, EmptyWhenOnlySonameVersionedPresent) { +UTest empty_when_only_soname_versioned_present{"BuildFlagsAtomic/EmptyWhenOnlySonameVersionedPresent", [] { Tmp dir; touch(dir.path / "libatomic.so.1"); touch(dir.path / "libatomic.so.1.2.0"); auto flag = mcpp::build::atomic_link_flag({dir.path}, kDynamic); - EXPECT_TRUE(flag.empty()) << "got: " << flag; -} + expect(flag.empty()) << "got: " << flag; +}}; -TEST(BuildFlagsAtomic, ScansAllDirsNotJustFirst) { +UTest scans_all_dirs_not_just_first{"BuildFlagsAtomic/ScansAllDirsNotJustFirst", [] { Tmp a, b; touch(b.path / "libatomic.so"); // link-resolvable, in a later dir auto flag = mcpp::build::atomic_link_flag({a.path, b.path}, kDynamic); - EXPECT_NE(flag.find("-latomic"), std::string::npos); -} + expect(ne(flag.find("-latomic"), std::string::npos)); +}}; // Cross-platform / cross-linkage: a full-static link (`-static`, e.g. musl // targets) resolves `-latomic` only from `libatomic.a`. A lone `libatomic.so` // is not usable, so the flag must stay empty under static linkage to avoid // "cannot find -latomic". -TEST(BuildFlagsAtomic, StaticLinkEmptyWhenOnlySharedPresent) { +UTest static_link_empty_when_only_shared_present{"BuildFlagsAtomic/StaticLinkEmptyWhenOnlySharedPresent", [] { Tmp dir; touch(dir.path / "libatomic.so"); auto flag = mcpp::build::atomic_link_flag({dir.path}, kStatic); - EXPECT_TRUE(flag.empty()) << "got: " << flag; -} + expect(flag.empty()) << "got: " << flag; +}}; -TEST(BuildFlagsAtomic, StaticLinkEmittedWhenArchivePresent) { +UTest static_link_emitted_when_archive_present{"BuildFlagsAtomic/StaticLinkEmittedWhenArchivePresent", [] { Tmp dir; touch(dir.path / "libatomic.a"); auto flag = mcpp::build::atomic_link_flag({dir.path}, kStatic); - EXPECT_NE(flag.find("-latomic"), std::string::npos); -} + expect(ne(flag.find("-latomic"), std::string::npos)); +}}; // mcpp#226: normalize_include_flags generalizes the old -I-only // absolutize_include_flags to the whole include/lib-search-path family, in @@ -105,7 +107,7 @@ TEST(BuildFlagsAtomic, StaticLinkEmittedWhenArchivePresent) { // slashes keeps them on MSVC, and the old expectation `(root / "hdr").string()` // was itself the mixed `"/proj\hdr"` shape this family of bugs produced. // make_preferred() makes the expectation platform-correct on both sides. -TEST(BuildFlags, NormalizeIncludeFlagsRewritesFullIncludeFamily) { +UTest normalize_include_flags_rewrites_full_include_family{"BuildFlags/NormalizeIncludeFlagsRewritesFullIncludeFamily", [] { std::filesystem::path root = "/proj"; auto expected = [](const std::filesystem::path& p) { auto n = p; @@ -118,18 +120,18 @@ TEST(BuildFlags, NormalizeIncludeFlagsRewritesFullIncludeFamily) { mcpp::modgraph::normalize_include_flags(root, flags); - ASSERT_EQ(flags.size(), 5u); - EXPECT_EQ(flags[0], "-I" + expected(root / "hdr")); - EXPECT_EQ(flags[1], "-iquote" + expected(root / "hdr")); - EXPECT_EQ(flags[2], "-isystem"); // prefix itself untouched - EXPECT_EQ(flags[3], expected(root / "hdr")); // separated element rewritten - EXPECT_EQ(flags[4], "-idirafter" + expected(root / "hdr")); -} + expect(fatal(eq(flags.size(), 5u))); + expect(eq(flags[0], "-I" + expected(root / "hdr"))); + expect(eq(flags[1], "-iquote" + expected(root / "hdr"))); + expect(eq(flags[2], "-isystem")); // prefix itself untouched + expect(eq(flags[3], expected(root / "hdr"))); // separated element rewritten + expect(eq(flags[4], "-idirafter" + expected(root / "hdr"))); +}}; // Absolute paths and root-relative spellings are left alone (matches the // pre-#226 -I behavior), for both the joined and separated forms — only the // separator spelling is normalized to native (#390; a no-op on POSIX). -TEST(BuildFlags, NormalizeIncludeFlagsLeavesAbsolutePathsAlone) { +UTest normalize_include_flags_leaves_absolute_paths_alone{"BuildFlags/NormalizeIncludeFlagsLeavesAbsolutePathsAlone", [] { std::filesystem::path root = "/proj"; auto expected = [](const std::filesystem::path& p) { auto n = p; @@ -142,32 +144,32 @@ TEST(BuildFlags, NormalizeIncludeFlagsLeavesAbsolutePathsAlone) { mcpp::modgraph::normalize_include_flags(root, flags); - EXPECT_EQ(flags[0], "-I" + expected("/abs/hdr")); - EXPECT_EQ(flags[1], "-isystem"); - EXPECT_EQ(flags[2], expected("/abs/hdr")); - EXPECT_EQ(flags[3], "-DKEEP"); -} + expect(eq(flags[0], "-I" + expected("/abs/hdr"))); + expect(eq(flags[1], "-isystem")); + expect(eq(flags[2], expected("/abs/hdr"))); + expect(eq(flags[3], "-DKEEP")); +}}; } // namespace // #634, item 11 of the triage record: a search-path entry that begins with a // token the loader expands is relative to a loaded object, not to the package // that wrote it. Both ldflag normalisers ask this one predicate. -TEST(BuildFlagsLoaderTokens, EveryLoaderTokenIsLeftAsWritten) { +UTest every_loader_token_is_left_as_written{"BuildFlagsLoaderTokens/EveryLoaderTokenIsLeftAsWritten", [] { for (std::string_view entry : {"$ORIGIN", "${ORIGIN}", "$ORIGIN/../lib", "$LIB", "@executable_path", "@executable_path/../Frameworks", "@loader_path", "@loader_path/", "@rpath", "@rpath/sub"}) { - EXPECT_TRUE(mcpp::build::is_loader_relative_search_path(entry)) << entry; + expect(mcpp::build::is_loader_relative_search_path(entry)) << entry; } -} +}}; -TEST(BuildFlagsLoaderTokens, OrdinaryPathsAreNotTokens) { +UTest ordinary_paths_are_not_tokens{"BuildFlagsLoaderTokens/OrdinaryPathsAreNotTokens", [] { for (std::string_view entry : {"", "lib", "./lib", "/opt/lib", "rpath", "@executable_pathology", "@rpathx", "x/@rpath"}) { - EXPECT_FALSE(mcpp::build::is_loader_relative_search_path(entry)) << entry; + expect(!mcpp::build::is_loader_relative_search_path(entry)) << entry; } -} +}}; // #647 E3: which link line a (host, target) pair takes. The branch used to be // chosen by `if constexpr` on the host, so an Android row built on a macOS host @@ -181,35 +183,35 @@ using mcpp::build::link_shape; using Fmt = mcpp::build::dist::Format; } // namespace -TEST(LinkShape, LinuxHostTakesTheGenericLineForEveryTarget) { +UTest linux_host_takes_the_generic_line_for_every_target{"LinkShape/LinuxHostTakesTheGenericLineForEveryTarget", [] { for (auto f : {Fmt::Elf, Fmt::MachO, Fmt::Pe, Fmt::Wasm}) { - EXPECT_EQ(link_shape(LinkHost::Linux, f, false, true), LinkShape::Generic); - EXPECT_EQ(link_shape(LinkHost::Linux, f, false, false), LinkShape::Generic); + expect(eq(link_shape(LinkHost::Linux, f, false, true), LinkShape::Generic)); + expect(eq(link_shape(LinkHost::Linux, f, false, false), LinkShape::Generic)); } -} +}}; -TEST(LinkShape, MacOSHostKeepsTheAppleLineForMachOOnly) { +UTest mac_os_host_keeps_the_apple_line_for_mach_o_only{"LinkShape/MacOSHostKeepsTheAppleLineForMachOOnly", [] { // macOS for macOS, and the iOS rows: the Apple SDK line, as before. - EXPECT_EQ(link_shape(LinkHost::MacOS, Fmt::MachO, false, false), LinkShape::AppleSdk); - EXPECT_EQ(link_shape(LinkHost::MacOS, Fmt::MachO, false, true), LinkShape::AppleSdk); + expect(eq(link_shape(LinkHost::MacOS, Fmt::MachO, false, false), LinkShape::AppleSdk)); + expect(eq(link_shape(LinkHost::MacOS, Fmt::MachO, false, true), LinkShape::AppleSdk)); // x86_64-linux-android and wasm32-emscripten on a Mac: the line that // carries `--target`. - EXPECT_EQ(link_shape(LinkHost::MacOS, Fmt::Elf, false, true), LinkShape::Generic); - EXPECT_EQ(link_shape(LinkHost::MacOS, Fmt::Wasm, false, true), LinkShape::Generic); - EXPECT_EQ(link_shape(LinkHost::MacOS, Fmt::Elf, false, false), LinkShape::Generic); - EXPECT_EQ(link_shape(LinkHost::MacOS, Fmt::Pe, false, true), LinkShape::Generic); -} - -TEST(LinkShape, WindowsHostSeparatesPeFromTargetsNamedByFlag) { - EXPECT_EQ(link_shape(LinkHost::Windows, Fmt::Pe, true, false), LinkShape::MsvcLinkExe); - EXPECT_EQ(link_shape(LinkHost::Windows, Fmt::Pe, false, true), LinkShape::PeLld); - EXPECT_EQ(link_shape(LinkHost::Windows, Fmt::Pe, false, false), LinkShape::PeLld); + expect(eq(link_shape(LinkHost::MacOS, Fmt::Elf, false, true), LinkShape::Generic)); + expect(eq(link_shape(LinkHost::MacOS, Fmt::Wasm, false, true), LinkShape::Generic)); + expect(eq(link_shape(LinkHost::MacOS, Fmt::Elf, false, false), LinkShape::Generic)); + expect(eq(link_shape(LinkHost::MacOS, Fmt::Pe, false, true), LinkShape::Generic)); +}}; + +UTest windows_host_separates_pe_from_targets_named_by_flag{"LinkShape/WindowsHostSeparatesPeFromTargetsNamedByFlag", [] { + expect(eq(link_shape(LinkHost::Windows, Fmt::Pe, true, false), LinkShape::MsvcLinkExe)); + expect(eq(link_shape(LinkHost::Windows, Fmt::Pe, false, true), LinkShape::PeLld)); + expect(eq(link_shape(LinkHost::Windows, Fmt::Pe, false, false), LinkShape::PeLld)); // An SDK or retargetable clang aimed at an ELF row: named by `--target`. - EXPECT_EQ(link_shape(LinkHost::Windows, Fmt::Elf, false, true), LinkShape::Generic); + expect(eq(link_shape(LinkHost::Windows, Fmt::Elf, false, true), LinkShape::Generic)); // The canadian GCC cross to x86_64-linux-musl names its target by prefix // and keeps the line its CI job verifies. - EXPECT_EQ(link_shape(LinkHost::Windows, Fmt::Elf, false, false), LinkShape::PeLld); -} + expect(eq(link_shape(LinkHost::Windows, Fmt::Elf, false, false), LinkShape::PeLld)); +}}; // ── c_abi_absent_facility_advice — the manifest reads back at the link ────── // @@ -218,26 +220,25 @@ TEST(LinkShape, WindowsHostSeparatesPeFromTargetsNamedByFlag) { // deliberate limit of the environment. The manifest already distinguishes // those (design 2026-09-20 §5.7.5); these pin that it is read back. -TEST(CAbiAbsentAdvice, ALinkAbsenceNamedInTheManifestIsExplained) { +UTest a_link_absence_named_in_the_manifest_is_explained{"CAbiAbsentAdvice/ALinkAbsenceNamedInTheManifestIsExplained", [] { std::vector absent{ {"fork", mcpp::targetside::CAbiAbsentForm::Link, "openkal has no process image duplication"}, }; auto a = mcpp::build::c_abi_absent_facility_advice( "ld.lld: error: undefined symbol: fork\n", "musl", absent); - ASSERT_FALSE(a.empty()); - EXPECT_NE(a.find("fork"), std::string::npos); - EXPECT_NE(a.find("no process image duplication"), std::string::npos); + expect(fatal(!a.empty())); + expect(ne(a.find("fork"), std::string::npos)); + expect(ne(a.find("no process image duplication"), std::string::npos)); // THE WHOLE CLAUSE, NOT `find("musl")`. The name was interpolated from // two conditionals -- an opening paren and the name -- and the closing // one was never emitted, so every reader saw `(musl declares that it // does not supply ...`. `find("musl")` is true of that sentence too, // which is why it took a real link to notice. Assert the rendering. - EXPECT_NE(a.find("the C library in this graph (musl) declares"), - std::string::npos) << a; -} + expect(ne(a.find("the C library in this graph (musl) declares"), std::string::npos)) << a; +}}; -TEST(CAbiAbsentAdvice, AnUnnamedCLibraryLeavesNoEmptyParentheses) { +UTest an_unnamed_c_library_leaves_no_empty_parentheses{"CAbiAbsentAdvice/AnUnnamedCLibraryLeavesNoEmptyParentheses", [] { // The other side of the same substitution: with no name there must be no // parenthetical at all, rather than `graph () declares`. std::vector absent{ @@ -245,22 +246,21 @@ TEST(CAbiAbsentAdvice, AnUnnamedCLibraryLeavesNoEmptyParentheses) { }; auto a = mcpp::build::c_abi_absent_facility_advice( "ld.lld: error: undefined symbol: fork\n", "", absent); - ASSERT_FALSE(a.empty()); - EXPECT_NE(a.find("the C library in this graph declares"), - std::string::npos) << a; - EXPECT_EQ(a.find("()"), std::string::npos) << a; -} + expect(fatal(!a.empty())); + expect(ne(a.find("the C library in this graph declares"), std::string::npos)) << a; + expect(eq(a.find("()"), std::string::npos)) << a; +}}; -TEST(CAbiAbsentAdvice, TheGnuSpellingIsMatchedToo) { +UTest the_gnu_spelling_is_matched_too{"CAbiAbsentAdvice/TheGnuSpellingIsMatchedToo", [] { std::vector absent{ {"fork", mcpp::targetside::CAbiAbsentForm::Link, ""}, }; auto a = mcpp::build::c_abi_absent_facility_advice( "main.o: undefined reference to `fork'\n", "musl", absent); - EXPECT_FALSE(a.empty()); -} + expect(!a.empty()); +}}; -TEST(CAbiAbsentAdvice, AnAbsenceThatReachesTheProgramAtRunTimeIsNotMatched) { +UTest an_absence_that_reaches_the_program_at_run_time_is_not_matched{"CAbiAbsentAdvice/AnAbsenceThatReachesTheProgramAtRunTimeIsNotMatched", [] { // `enosys` and `accepted-no-effect` reach a program by construction while // it runs; matching them against a link diagnostic would report a // coincidence of spelling as an explanation. @@ -268,27 +268,27 @@ TEST(CAbiAbsentAdvice, AnAbsenceThatReachesTheProgramAtRunTimeIsNotMatched) { {"mprotect", mcpp::targetside::CAbiAbsentForm::Enosys, "no protection op"}, {"tcsetattr", mcpp::targetside::CAbiAbsentForm::AcceptedNoEffect, ""}, }; - EXPECT_TRUE(mcpp::build::c_abi_absent_facility_advice( + expect(mcpp::build::c_abi_absent_facility_advice( "ld.lld: error: undefined symbol: mprotect\n", "musl", absent).empty()); - EXPECT_TRUE(mcpp::build::c_abi_absent_facility_advice( + expect(mcpp::build::c_abi_absent_facility_advice( "undefined reference to `tcsetattr'\n", "musl", absent).empty()); -} +}}; -TEST(CAbiAbsentAdvice, ALinkFailureNamingSomethingElseGetsNoNote) { +UTest a_link_failure_naming_something_else_gets_no_note{"CAbiAbsentAdvice/ALinkFailureNamingSomethingElseGetsNoNote", [] { std::vector absent{ {"fork", mcpp::targetside::CAbiAbsentForm::Link, ""}, }; - EXPECT_TRUE(mcpp::build::c_abi_absent_facility_advice( + expect(mcpp::build::c_abi_absent_facility_advice( "ld.lld: error: undefined symbol: my_own_function\n", "musl", absent) .empty()); -} +}}; -TEST(CAbiAbsentAdvice, ALibraryThatEnumeratedNothingProducesNoNote) { - EXPECT_TRUE(mcpp::build::c_abi_absent_facility_advice( +UTest a_library_that_enumerated_nothing_produces_no_note{"CAbiAbsentAdvice/ALibraryThatEnumeratedNothingProducesNoNote", [] { + expect(mcpp::build::c_abi_absent_facility_advice( "ld.lld: error: undefined symbol: fork\n", "musl", {}).empty()); -} +}}; -TEST(CAbiAbsentAdvice, ALongerSymbolWithTheSamePrefixIsNotExplained) { +UTest a_longer_symbol_with_the_same_prefix_is_not_explained{"CAbiAbsentAdvice/ALongerSymbolWithTheSamePrefixIsNotExplained", [] { // `undefined symbol: open` is a prefix of `undefined symbol: opendir`. // A substring search would answer a link failure with a row that has // nothing to do with it, and an explanation that is confidently wrong is @@ -296,13 +296,13 @@ TEST(CAbiAbsentAdvice, ALongerSymbolWithTheSamePrefixIsNotExplained) { std::vector absent{ {"open", mcpp::targetside::CAbiAbsentForm::Link, "not supplied"}, }; - EXPECT_TRUE(mcpp::build::c_abi_absent_facility_advice( + expect(mcpp::build::c_abi_absent_facility_advice( "ld.lld: error: undefined symbol: opendir\n", "musl", absent).empty()); - EXPECT_FALSE(mcpp::build::c_abi_absent_facility_advice( - "ld.lld: error: undefined symbol: open\n", "musl", absent).empty()); -} + expect(!(mcpp::build::c_abi_absent_facility_advice( + "ld.lld: error: undefined symbol: open\n", "musl", absent).empty())); +}}; -TEST(CAbiAbsentAdvice, TheListSurvivesAWriteAndReadBesideBuildNinja) { +UTest the_list_survives_a_write_and_read_beside_build_ninja{"CAbiAbsentAdvice/TheListSurvivesAWriteAndReadBesideBuildNinja", [] { // The two paths that report a failed build --- the one with a plan and the // fast path, which has none by construction --- must give the same advice, // or it appears depending on whether build.ninja happened to be up to @@ -314,27 +314,27 @@ TEST(CAbiAbsentAdvice, TheListSurvivesAWriteAndReadBesideBuildNinja) { }; mcpp::build::write_c_abi_absent_sidecar(dir.path, "musl", absent); auto [name, back] = mcpp::build::read_c_abi_absent_sidecar(dir.path); - EXPECT_EQ(name, "musl"); - ASSERT_EQ(back.size(), 2u); - EXPECT_EQ(back[0].name, "fork"); - EXPECT_EQ(back[0].form, mcpp::targetside::CAbiAbsentForm::Link); - EXPECT_EQ(back[0].note, "no image duplication"); - EXPECT_EQ(back[1].form, mcpp::targetside::CAbiAbsentForm::Enosys); - EXPECT_TRUE(back[1].note.empty()); + expect(eq(name, "musl")); + expect(fatal(eq(back.size(), 2u))); + expect(eq(back[0].name, "fork")); + expect(eq(back[0].form, mcpp::targetside::CAbiAbsentForm::Link)); + expect(eq(back[0].note, "no image duplication")); + expect(eq(back[1].form, mcpp::targetside::CAbiAbsentForm::Enosys)); + expect(back[1].note.empty()); // And the advice built from the read-back list is the advice the plan // path would have given. - EXPECT_FALSE(mcpp::build::c_abi_absent_facility_advice( - "ld.lld: error: undefined symbol: fork\n", name, back).empty()); -} + expect(!(mcpp::build::c_abi_absent_facility_advice( + "ld.lld: error: undefined symbol: fork\n", name, back).empty())); +}}; -TEST(CAbiAbsentAdvice, ADirectoryNoBuildWroteToYieldsNothing) { +UTest a_directory_no_build_wrote_to_yields_nothing{"CAbiAbsentAdvice/ADirectoryNoBuildWroteToYieldsNothing", [] { Tmp dir; auto [name, back] = mcpp::build::read_c_abi_absent_sidecar(dir.path); - EXPECT_TRUE(name.empty()); - EXPECT_TRUE(back.empty()); -} + expect(name.empty()); + expect(back.empty()); +}}; -TEST(CAbiAbsentAdvice, AGraphThatDeclaresNoAbsenceLeavesNoSidecar) { +UTest a_graph_that_declares_no_absence_leaves_no_sidecar{"CAbiAbsentAdvice/AGraphThatDeclaresNoAbsenceLeavesNoSidecar", [] { // A build whose C library states nothing must not leave a file behind for // the next build to read: the fast path would then explain a failure with // a list the current graph never declared. @@ -343,5 +343,5 @@ TEST(CAbiAbsentAdvice, AGraphThatDeclaresNoAbsenceLeavesNoSidecar) { {{"fork", mcpp::targetside::CAbiAbsentForm::Link, ""}}); mcpp::build::write_c_abi_absent_sidecar(dir.path, "", {}); auto [name, back] = mcpp::build::read_c_abi_absent_sidecar(dir.path); - EXPECT_TRUE(back.empty()); -} + expect(back.empty()); +}}; diff --git a/tests/unit/test_build_legs.cpp b/tests/unit/test_build_legs.cpp index c1a452d44..7a7d915e2 100644 --- a/tests/unit/test_build_legs.cpp +++ b/tests/unit/test_build_legs.cpp @@ -1,30 +1,32 @@ -#include import std; +import mcpplibs.mtest; import mcpp.build.prepare; import mcpp.toolchain.triple; +using namespace mcpplibs::mtest; + namespace legs = mcpp::build::legs; -TEST(BuildLegs, NativeArchitectureFirstTheRestInOrder) { +UTest native_architecture_first_the_rest_in_order{"BuildLegs/NativeArchitectureFirstTheRestInOrder", [] { const auto host = mcpp::toolchain::triple::host_triple().arch; const std::string other = host == "x86_64" ? "aarch64" : "x86_64"; std::vector in{other + "-openkal", host + "-openkal", other + "-linux-musl", host + "-linux-musl"}; auto out = legs::native_first(in); - EXPECT_EQ(out, (std::vector{host + "-openkal", host + "-linux-musl", - other + "-openkal", other + "-linux-musl"})); -} + expect(eq(out, (std::vector{host + "-openkal", host + "-linux-musl", + other + "-openkal", other + "-linux-musl"}))); +}}; -TEST(BuildLegs, FirstFailureIsTheFirstNonZero) { - EXPECT_EQ(legs::first_failure(std::vector{0, 0}), 0); - EXPECT_EQ(legs::first_failure(std::vector{0, 2, 1}), 2); - EXPECT_EQ(legs::first_failure(std::vector{}), 0); -} +UTest first_failure_is_the_first_non_zero{"BuildLegs/FirstFailureIsTheFirstNonZero", [] { + expect(eq(legs::first_failure(std::vector{0, 0}), 0)); + expect(eq(legs::first_failure(std::vector{0, 2, 1}), 2)); + expect(eq(legs::first_failure(std::vector{}), 0)); +}}; // One directory is one ninja's: work in it runs one item after another, and // distinct directories run at the same time. -TEST(BuildLegs, OneDirectoryIsWorkedOneItemAtATime) { +UTest one_directory_is_worked_one_item_at_a_time{"BuildLegs/OneDirectoryIsWorkedOneItemAtATime", [] { std::vector dirs{"a", "b", "a", "b", "a"}; std::mutex m; std::map inside, most; @@ -43,8 +45,8 @@ TEST(BuildLegs, OneDirectoryIsWorkedOneItemAtATime) { ++total; return static_cast(i); }); - EXPECT_EQ(total.load(), 5); - EXPECT_EQ(most["a"], 1); - EXPECT_EQ(most["b"], 1); - for (std::size_t i = 0; i < results.size(); ++i) EXPECT_EQ(results[i], static_cast(i)); -} + expect(eq(total.load(), 5)); + expect(eq(most["a"], 1)); + expect(eq(most["b"], 1)); + for (std::size_t i = 0; i < results.size(); ++i) expect(eq(results[i], static_cast(i))); +}}; diff --git a/tests/unit/test_build_profile.cpp b/tests/unit/test_build_profile.cpp index e2e7fae3c..56959b894 100644 --- a/tests/unit/test_build_profile.cpp +++ b/tests/unit/test_build_profile.cpp @@ -11,15 +11,17 @@ // `mcpp build --release` followed by a bare `mcpp build` reported success in // 0.00s and left the release artifacts in place. -#include #include import std; +import mcpplibs.mtest; import mcpp.invocation; import mcpp.build.prepare; import mcpp.manifest; import mcpplibs.os.platform; +using namespace mcpplibs::mtest; + namespace { mcpp::manifest::Manifest base() { @@ -45,49 +47,48 @@ class ScopedDeploymentTargetEnv { // ── resolve_profile_name: the ONE rule, shared with the fast paths ─────────── -TEST(BuildProfile, OverrideBeatsManifestDefault) { +UTest override_beats_manifest_default{"BuildProfile/OverrideBeatsManifestDefault", [] { auto m = base(); m.buildConfig.defaultProfile = "release"; - EXPECT_EQ(mcpp::build::resolve_profile_name(m, "dev"), "dev"); -} + expect(eq(mcpp::build::resolve_profile_name(m, "dev"), "dev")); +}}; -TEST(BuildProfile, ManifestDefaultBeatsGlobalDefault) { +UTest manifest_default_beats_global_default{"BuildProfile/ManifestDefaultBeatsGlobalDefault", [] { auto m = base(); m.buildConfig.defaultProfile = "release"; - EXPECT_EQ(mcpp::build::resolve_profile_name(m, ""), "release"); -} + expect(eq(mcpp::build::resolve_profile_name(m, ""), "release")); +}}; -TEST(BuildProfile, GlobalDefaultIsDev) { - EXPECT_EQ(mcpp::build::resolve_profile_name(base(), ""), "dev"); -} +UTest global_default_is_dev{"BuildProfile/GlobalDefaultIsDev", [] { + expect(eq(mcpp::build::resolve_profile_name(base(), ""), "dev")); +}}; -TEST(BuildProfile, CustomProfileNamePassesThrough) { - EXPECT_EQ(mcpp::build::resolve_profile_name(base(), "contracts"), "contracts"); -} +UTest custom_profile_name_passes_through{"BuildProfile/CustomProfileNamePassesThrough", [] { + expect(eq(mcpp::build::resolve_profile_name(base(), "contracts"), "contracts")); +}}; // ── the fingerprint invariant ──────────────────────────────────────────────── -TEST(BuildProfile, OptLevelIsInTheCanonicalFlags) { +UTest opt_level_is_in_the_canonical_flags{"BuildProfile/OptLevelIsInTheCanonicalFlags", [] { auto a = base(); auto b = base(); b.buildConfig.optLevel = "0"; - EXPECT_NE(mcpp::build::canonical_configuration_flags(a), - mcpp::build::canonical_configuration_flags(b)); -} + expect(ne(mcpp::build::canonical_configuration_flags(a), mcpp::build::canonical_configuration_flags(b))); +}}; -TEST(BuildProfile, DebugLtoStripAreInTheCanonicalFlags) { +UTest debug_lto_strip_are_in_the_canonical_flags{"BuildProfile/DebugLtoStripAreInTheCanonicalFlags", [] { auto ref = mcpp::build::canonical_configuration_flags(base()); { auto m = base(); m.buildConfig.debug = true; - EXPECT_NE(mcpp::build::canonical_configuration_flags(m), ref); } + expect(ne(mcpp::build::canonical_configuration_flags(m), ref)); } { auto m = base(); m.buildConfig.lto = true; - EXPECT_NE(mcpp::build::canonical_configuration_flags(m), ref); } + expect(ne(mcpp::build::canonical_configuration_flags(m), ref)); } { auto m = base(); m.buildConfig.strip = true; - EXPECT_NE(mcpp::build::canonical_configuration_flags(m), ref); } -} + expect(ne(mcpp::build::canonical_configuration_flags(m), ref)); } +}}; // The exact shape the three built-in profiles resolve to (prepare_build's // profile block). Asserting on the flag string rather than on a fingerprint hex // keeps the failure legible when someone adds a knob and forgets to serialize it. -TEST(BuildProfile, BuiltInProfilesProduceDistinctCanonicalFlags) { +UTest built_in_profiles_produce_distinct_canonical_flags{"BuildProfile/BuiltInProfilesProduceDistinctCanonicalFlags", [] { auto dev = base(); dev.buildConfig.optLevel = "0"; dev.buildConfig.debug = true; @@ -103,35 +104,35 @@ TEST(BuildProfile, BuiltInProfilesProduceDistinctCanonicalFlags) { auto frelease = mcpp::build::canonical_configuration_flags(release); auto fdist = mcpp::build::canonical_configuration_flags(dist); - EXPECT_NE(fdev, frelease); - EXPECT_NE(frelease, fdist); - EXPECT_NE(fdev, fdist); -} + expect(ne(fdev, frelease)); + expect(ne(frelease, fdist)); + expect(ne(fdev, fdist)); +}}; -TEST(BuildProfile, CanonicalFlagsStillCoverTheNonProfileKnobs) { +UTest canonical_flags_still_cover_the_non_profile_knobs{"BuildProfile/CanonicalFlagsStillCoverTheNonProfileKnobs", [] { auto ref = mcpp::build::canonical_configuration_flags(base()); { auto m = base(); m.package.standard = "c++26"; - EXPECT_NE(mcpp::build::canonical_configuration_flags(m), ref); } + expect(ne(mcpp::build::canonical_configuration_flags(m), ref)); } { auto m = base(); m.buildConfig.dialectCxxflags = {"-freflection"}; - EXPECT_NE(mcpp::build::canonical_configuration_flags(m), ref); } + expect(ne(mcpp::build::canonical_configuration_flags(m), ref)); } { auto m = base(); m.buildConfig.dependencyLinkage = "shared"; - EXPECT_NE(mcpp::build::canonical_configuration_flags(m), ref); } -} + expect(ne(mcpp::build::canonical_configuration_flags(m), ref)); } +}}; // Workspace design 2026-09-29 §3: a package's own flags are attributes of its // node, not inputs of the configuration, so editing them keeps the build // directory and ninja rebuilds the edges whose commands changed. -TEST(BuildProfile, PackageFlagsAreNotInTheConfiguration) { +UTest package_flags_are_not_in_the_configuration{"BuildProfile/PackageFlagsAreNotInTheConfiguration", [] { auto ref = mcpp::build::canonical_configuration_flags(base()); { auto m = base(); m.buildConfig.cxxflags = {"-DFOO"}; - EXPECT_EQ(mcpp::build::canonical_configuration_flags(m), ref); } + expect(eq(mcpp::build::canonical_configuration_flags(m), ref)); } { auto m = base(); m.buildConfig.cflags = {"-DBAR"}; - EXPECT_EQ(mcpp::build::canonical_configuration_flags(m), ref); } + expect(eq(mcpp::build::canonical_configuration_flags(m), ref)); } { auto m = base(); m.buildConfig.ldflags = {"-lm"}; - EXPECT_EQ(mcpp::build::canonical_configuration_flags(m), ref); } + expect(eq(mcpp::build::canonical_configuration_flags(m), ref)); } { auto m = base(); m.buildConfig.cStandard = "c17"; - EXPECT_EQ(mcpp::build::canonical_configuration_flags(m), ref); } -} + expect(eq(mcpp::build::canonical_configuration_flags(m), ref)); } +}}; // ── macos_deployment_target: TARGET-keyed, not HOST-keyed (mcpp#685) ──────── // @@ -147,7 +148,7 @@ TEST(BuildProfile, PackageFlagsAreNotInTheConfiguration) { // `TargetIsMacosFoldsDeploymentTargetIntoTheFingerprint` on any non-Apple CI // runner. -TEST(BuildProfile, NonMacosTargetIgnoresDeploymentTargetInTheFingerprint) { +UTest non_macos_target_ignores_deployment_target_in_the_fingerprint{"BuildProfile/NonMacosTargetIgnoresDeploymentTargetInTheFingerprint", [] { ScopedDeploymentTargetEnv noEnv(nullptr); auto ref = base(); auto changed = base(); changed.buildConfig.macosDeploymentTarget = "11.0"; @@ -155,18 +156,15 @@ TEST(BuildProfile, NonMacosTargetIgnoresDeploymentTargetInTheFingerprint) { // one building for something other than macOS) must not see the // fingerprint move just because the manifest names a macOS floor it will // never apply. - EXPECT_EQ(mcpp::build::canonical_configuration_flags(ref), - mcpp::build::canonical_configuration_flags(changed)); - EXPECT_EQ(mcpp::build::canonical_configuration_flags(ref, /*targetIsMacos=*/false), - mcpp::build::canonical_configuration_flags(changed, /*targetIsMacos=*/false)); -} + expect(eq(mcpp::build::canonical_configuration_flags(ref), mcpp::build::canonical_configuration_flags(changed))); + expect(eq(mcpp::build::canonical_configuration_flags(ref, /*targetIsMacos=*/false), mcpp::build::canonical_configuration_flags(changed, /*targetIsMacos=*/false))); +}}; -TEST(BuildProfile, TargetIsMacosFoldsDeploymentTargetIntoTheFingerprint) { +UTest target_is_macos_folds_deployment_target_into_the_fingerprint{"BuildProfile/TargetIsMacosFoldsDeploymentTargetIntoTheFingerprint", [] { ScopedDeploymentTargetEnv noEnv(nullptr); auto ref = base(); auto changed = base(); changed.buildConfig.macosDeploymentTarget = "11.0"; - EXPECT_NE(mcpp::build::canonical_configuration_flags(ref, /*targetIsMacos=*/true), - mcpp::build::canonical_configuration_flags(changed, /*targetIsMacos=*/true)); + expect(ne(mcpp::build::canonical_configuration_flags(ref, /*targetIsMacos=*/true), mcpp::build::canonical_configuration_flags(changed, /*targetIsMacos=*/true))); // AND THE ENV OVERRIDE, folded the same way `deployment_target` resolves // it: with MACOSX_DEPLOYMENT_TARGET set, the manifest value stops @@ -175,31 +173,30 @@ TEST(BuildProfile, TargetIsMacosFoldsDeploymentTargetIntoTheFingerprint) { ScopedDeploymentTargetEnv env("12.3"); auto manifestA = base(); manifestA.buildConfig.macosDeploymentTarget = "11.0"; auto manifestB = base(); manifestB.buildConfig.macosDeploymentTarget = "13.0"; - EXPECT_EQ(mcpp::build::canonical_configuration_flags(manifestA, /*targetIsMacos=*/true), - mcpp::build::canonical_configuration_flags(manifestB, /*targetIsMacos=*/true)); -} + expect(eq(mcpp::build::canonical_configuration_flags(manifestA, /*targetIsMacos=*/true), mcpp::build::canonical_configuration_flags(manifestB, /*targetIsMacos=*/true))); +}}; // ── cache-mode parsing ────────────────────────────────────────────────────── -TEST(BuildProfile, CacheModeParsesTheThreeModes) { +UTest cache_mode_parses_the_three_modes{"BuildProfile/CacheModeParsesTheThreeModes", [] { using mcpp::build::CacheMode; - EXPECT_EQ(mcpp::build::parse_cache_mode("global"), CacheMode::Global); - EXPECT_EQ(mcpp::build::parse_cache_mode("local"), CacheMode::Local); - EXPECT_EQ(mcpp::build::parse_cache_mode("off"), CacheMode::Off); + expect(eq(mcpp::build::parse_cache_mode("global"), CacheMode::Global)); + expect(eq(mcpp::build::parse_cache_mode("local"), CacheMode::Local)); + expect(eq(mcpp::build::parse_cache_mode("off"), CacheMode::Off)); // `none` accepted as a synonym for off; anything else is refused rather // than silently meaning "global", which would be the worst default for a // typo to land on. - EXPECT_EQ(mcpp::build::parse_cache_mode("none"), CacheMode::Off); - EXPECT_FALSE(mcpp::build::parse_cache_mode("on").has_value()); - EXPECT_FALSE(mcpp::build::parse_cache_mode("").has_value()); - EXPECT_FALSE(mcpp::build::parse_cache_mode("GLOBAL").has_value()); -} + expect(eq(mcpp::build::parse_cache_mode("none"), CacheMode::Off)); + expect(!(mcpp::build::parse_cache_mode("on").has_value())); + expect(!(mcpp::build::parse_cache_mode("").has_value())); + expect(!(mcpp::build::parse_cache_mode("GLOBAL").has_value())); +}}; -TEST(BuildProfile, CacheModeNameRoundTrips) { +UTest cache_mode_name_round_trips{"BuildProfile/CacheModeNameRoundTrips", [] { using mcpp::build::CacheMode; for (auto m : {CacheMode::Global, CacheMode::Local, CacheMode::Off}) - EXPECT_EQ(mcpp::build::parse_cache_mode(mcpp::build::cache_mode_name(m)), m); -} + expect(eq(mcpp::build::parse_cache_mode(mcpp::build::cache_mode_name(m)), m)); +}}; // ── cache-mode resolution ─────────────────────────────────────────────────── // Same shape as resolve_profile_name and for the same reason: the fast paths @@ -208,48 +205,44 @@ TEST(BuildProfile, CacheModeNameRoundTrips) { // edges reading the cache) got replayed for a request that asked for `local` — // and ruling the cache out is `local`'s entire purpose. -TEST(BuildProfile, CacheModeOverrideBeatsManifest) { +UTest cache_mode_override_beats_manifest{"BuildProfile/CacheModeOverrideBeatsManifest", [] { auto m = base(); m.buildConfig.cacheMode = "local"; - EXPECT_EQ(mcpp::build::resolve_cache_mode(m, "global"), - mcpp::build::CacheMode::Global); -} + expect(eq(mcpp::build::resolve_cache_mode(m, "global"), mcpp::build::CacheMode::Global)); +}}; -TEST(BuildProfile, CacheModeManifestBeatsDefault) { +UTest cache_mode_manifest_beats_default{"BuildProfile/CacheModeManifestBeatsDefault", [] { auto m = base(); m.buildConfig.cacheMode = "off"; - EXPECT_EQ(mcpp::build::resolve_cache_mode(m, ""), mcpp::build::CacheMode::Off); -} + expect(eq(mcpp::build::resolve_cache_mode(m, ""), mcpp::build::CacheMode::Off)); +}}; -TEST(BuildProfile, CacheModeDefaultsToGlobal) { - EXPECT_EQ(mcpp::build::resolve_cache_mode(base(), ""), - mcpp::build::CacheMode::Global); -} +UTest cache_mode_defaults_to_global{"BuildProfile/CacheModeDefaultsToGlobal", [] { + expect(eq(mcpp::build::resolve_cache_mode(base(), ""), mcpp::build::CacheMode::Global)); +}}; // An unparseable value must not silently mean "global" at THIS level either: it // falls through to the next source, and prepare_build reports it separately. -TEST(BuildProfile, UnknownManifestCacheModeFallsThroughToDefault) { +UTest unknown_manifest_cache_mode_falls_through_to_default{"BuildProfile/UnknownManifestCacheModeFallsThroughToDefault", [] { auto m = base(); m.buildConfig.cacheMode = "bogus"; - EXPECT_EQ(mcpp::build::resolve_cache_mode(m, ""), - mcpp::build::CacheMode::Global); + expect(eq(mcpp::build::resolve_cache_mode(m, ""), mcpp::build::CacheMode::Global)); // ...and an unparseable override does not shadow a valid manifest value. m.buildConfig.cacheMode = "local"; - EXPECT_EQ(mcpp::build::resolve_cache_mode(m, "bogus"), - mcpp::build::CacheMode::Local); -} + expect(eq(mcpp::build::resolve_cache_mode(m, "bogus"), mcpp::build::CacheMode::Local)); +}}; // ── #649 E9: the override the command line states, one rule for every verb ─── // // `build` preferred `--profile` over the shorthands and `run` preferred the // shorthands, so one command line built two profiles. Every verb now reads this. -TEST(BuildProfile, ProfileOptionBeatsTheShorthands) { - EXPECT_EQ(mcpp::build::profile_override_from_flags("dev", true, false), "dev"); - EXPECT_EQ(mcpp::build::profile_override_from_flags("release", false, true), "release"); -} - -TEST(BuildProfile, ShorthandsNameTheirProfile) { - EXPECT_EQ(mcpp::build::profile_override_from_flags("", true, false), "release"); - EXPECT_EQ(mcpp::build::profile_override_from_flags("", false, true), "dev"); - EXPECT_EQ(mcpp::build::profile_override_from_flags("", false, false), ""); -} +UTest profile_option_beats_the_shorthands{"BuildProfile/ProfileOptionBeatsTheShorthands", [] { + expect(eq(mcpp::build::profile_override_from_flags("dev", true, false), "dev")); + expect(eq(mcpp::build::profile_override_from_flags("release", false, true), "release")); +}}; + +UTest shorthands_name_their_profile{"BuildProfile/ShorthandsNameTheirProfile", [] { + expect(eq(mcpp::build::profile_override_from_flags("", true, false), "release")); + expect(eq(mcpp::build::profile_override_from_flags("", false, true), "dev")); + expect(eq(mcpp::build::profile_override_from_flags("", false, false), "")); +}}; diff --git a/tests/unit/test_build_progress.cpp b/tests/unit/test_build_progress.cpp index 80cf11676..6d0e3a373 100644 --- a/tests/unit/test_build_progress.cpp +++ b/tests/unit/test_build_progress.cpp @@ -1,10 +1,13 @@ -#include import std; +import mcpplibs.mtest; +import mcpplibs.os.stdio; import mcpp.ui; import mcpp.build.progress; import mcpp.build.action_progress; +using namespace mcpplibs::mtest; + // The build's report (.agents/docs/2026-09-29-build-progress-display-design.md, // revised by .agents/docs/2026-09-30-build-output-refinement-design.md): its // readers, the step record, the measures of text the renderer uses, the bytes @@ -44,37 +47,37 @@ std::size_t count(std::string_view hay, std::string_view needle) { // ─── The readers ───────────────────────────────────────────────────────── -TEST(ProgressStatus, ReadsTheCountsAndTheEndTime) { +UTest reads_the_counts_and_the_end_time{"ProgressStatus/ReadsTheCountsAndTheEndTime", [] { auto s = parse_status("\x1b[0m@@mcpp 3 12 0.027@@ OBJ obj/a.o"); - ASSERT_TRUE(s.has_value()); - EXPECT_EQ(s->finished, 3u); - EXPECT_EQ(s->total, 12u); - EXPECT_EQ(s->endMs, 27); - EXPECT_EQ(s->text, "OBJ obj/a.o"); + expect(fatal(s.has_value())); + expect(eq(s->finished, 3u)); + expect(eq(s->total, 12u)); + expect(eq(s->endMs, 27)); + expect(eq(s->text, "OBJ obj/a.o")); auto long_ = parse_status("\x1b[0m@@mcpp 1203 1203 2407.968@@ LINK bin/gui.exe"); - ASSERT_TRUE(long_.has_value()); - EXPECT_EQ(long_->endMs, 2407968); + expect(fatal(long_.has_value())); + expect(eq(long_->endMs, 2407968)); // A nested ninja's status line, which the outer ninja relays with its // escape sequence stripped, is output, not a status line. // `%u`: the steps not yet started (revision 3, §5.8); absent in a line of // the three-field format. - EXPECT_FALSE(s->unstarted.has_value()); + expect(!(s->unstarted.has_value())); auto u = parse_status("\x1b[0m@@mcpp 700 707 41.2 0@@ LINK bin/xlings"); - ASSERT_TRUE(u.has_value()); - EXPECT_EQ(u->finished, 700u); - EXPECT_EQ(u->total, 707u); - ASSERT_TRUE(u->unstarted.has_value()); - EXPECT_EQ(*u->unstarted, 0u); - EXPECT_EQ(u->text, "LINK bin/xlings"); - EXPECT_FALSE(parse_status("@@mcpp 3 12 0.027@@ OBJ obj/a.o").has_value()); - EXPECT_FALSE(parse_status("[3/12] OBJ obj/a.o").has_value()); - EXPECT_FALSE(parse_status("src/a.cpp:1:2: error: @@mcpp").has_value()); - EXPECT_FALSE(parse_status("").has_value()); -} - -TEST(ProgressLog, GroupsTheEntriesOfOneStep) { + expect(fatal(u.has_value())); + expect(eq(u->finished, 700u)); + expect(eq(u->total, 707u)); + expect(fatal(u->unstarted.has_value())); + expect(eq(*u->unstarted, 0u)); + expect(eq(u->text, "LINK bin/xlings")); + expect(!(parse_status("@@mcpp 3 12 0.027@@ OBJ obj/a.o").has_value())); + expect(!(parse_status("[3/12] OBJ obj/a.o").has_value())); + expect(!(parse_status("src/a.cpp:1:2: error: @@mcpp").has_value())); + expect(!(parse_status("").has_value())); +}}; + +UTest groups_the_entries_of_one_step{"ProgressLog/GroupsTheEntriesOfOneStep", [] { // Measured on ninja 1.12.1: a module unit's object and BMI share start, // end and hash; std.o and std.compat.o ended in the same millisecond with // different hashes, and are two steps. @@ -89,45 +92,45 @@ TEST(ProgressLog, GroupsTheEntriesOfOneStep) { int version = 0; std::vector offsets; auto steps = parse_log(text, &consumed, &version, &offsets); - EXPECT_EQ(version, 6); - ASSERT_EQ(steps.size(), 3u); - EXPECT_EQ(steps[0].outputs, std::vector{"obj/std.o"}); - EXPECT_EQ(steps[1].outputs, std::vector{"obj/std.compat.o"}); - EXPECT_EQ(steps[2].outputs, (std::vector{"obj/lib2.m.o", "pcm.cache/pm_lib2.pcm"})); - EXPECT_EQ(steps[2].start, 8); - EXPECT_EQ(steps[2].end, 21); - EXPECT_EQ(consumed, text.rfind('\n') + 1); - ASSERT_EQ(offsets.size(), 3u); - EXPECT_EQ(text.substr(offsets[2], 3), "8\t2"); -} - -TEST(ProgressLog, ThisRunIsTheSuffixWhoseEndsTheStatusLinesStated) { + expect(eq(version, 6)); + expect(fatal(eq(steps.size(), 3u))); + expect(eq(steps[0].outputs, std::vector{"obj/std.o"})); + expect(eq(steps[1].outputs, std::vector{"obj/std.compat.o"})); + expect(eq(steps[2].outputs, (std::vector{"obj/lib2.m.o", "pcm.cache/pm_lib2.pcm"}))); + expect(eq(steps[2].start, 8)); + expect(eq(steps[2].end, 21)); + expect(eq(consumed, text.rfind('\n') + 1)); + expect(fatal(eq(offsets.size(), 3u))); + expect(eq(text.substr(offsets[2], 3), "8\t2")); +}}; + +UTest this_run_is_the_suffix_whose_ends_the_status_lines_stated{"ProgressLog/ThisRunIsTheSuffixWhoseEndsTheStatusLinesStated", [] { // A recompacted log: the old entries first, in no particular order, then // this run's, whose ends the status lines gave. std::vector steps{{0, 900, {"old/a.o"}}, {5, 40, {"old/b.o"}}, {0, 12, {"new/x.o"}}, {3, 40, {"new/y.o"}}}; - EXPECT_EQ(run_boundary(steps, {12, 40}), 2u); + expect(eq(run_boundary(steps, {12, 40}), 2u)); // A status line whose entry is not written yet does not move the boundary. - EXPECT_EQ(run_boundary(steps, {12, 40, 55}), 2u); - EXPECT_EQ(run_boundary(steps, {}), 4u); -} + expect(eq(run_boundary(steps, {12, 40, 55}), 2u)); + expect(eq(run_boundary(steps, {}), 4u)); +}}; -TEST(ProgressStarts, ReadsCompleteLinesOnly) { +UTest reads_complete_lines_only{"ProgressStarts/ReadsCompleteLinesOnly", [] { std::size_t consumed = 0; auto s = parse_starts("gen/vcpkg.stamp\t1700000000000\n" "./gen/b.stamp\t1700000000500\n" "gen/c.st", &consumed); - ASSERT_EQ(s.size(), 2u); - EXPECT_EQ(s[0].stamp, "gen/vcpkg.stamp"); - EXPECT_EQ(s[1].stamp, "gen/b.stamp"); - EXPECT_EQ(s[1].unixMs, 1700000000500); - EXPECT_EQ(consumed, std::string_view("gen/vcpkg.stamp\t1700000000000\n" - "./gen/b.stamp\t1700000000500\n").size()); -} + expect(fatal(eq(s.size(), 2u))); + expect(eq(s[0].stamp, "gen/vcpkg.stamp")); + expect(eq(s[1].stamp, "gen/b.stamp")); + expect(eq(s[1].unixMs, 1700000000500)); + expect(eq(consumed, std::string_view("gen/vcpkg.stamp\t1700000000000\n" + "./gen/b.stamp\t1700000000500\n").size())); +}}; // ─── The step record ───────────────────────────────────────────────────── -TEST(ProgressRecord, EveryStatementIsRecordedWithItsOwner) { +UTest every_statement_is_recorded_with_its_owner{"ProgressRecord/EveryStatementIsRecordedWithItsOwner", [] { Attribution a; a.owner("app"); a.statement("build obj/main.o | pcm.cache/app.pcm : cxx_obj src/main.cpp\n flags = -O2\n"); @@ -142,66 +145,66 @@ TEST(ProgressRecord, EveryStatementIsRecordedWithItsOwner) { a.statement("rule cxx_obj\n command = c++ $in\n"); const auto& steps = a.steps(); - ASSERT_EQ(steps.size(), 5u); - EXPECT_EQ(steps[0].outputs, (std::vector{"obj/main.o", "pcm.cache/app.pcm"})); - EXPECT_EQ(steps[0].rule, "cxx_obj"); - EXPECT_EQ(steps[1].outputs, std::vector{"bin/app.bin"}); - EXPECT_EQ(steps[2].outputs, std::vector{"obj/with space.o"}); - EXPECT_EQ(steps[3].outputs, std::vector{"C:/abs/gen.stamp"}); - EXPECT_EQ(steps[4].owner, ""); + expect(fatal(eq(steps.size(), 5u))); + expect(eq(steps[0].outputs, (std::vector{"obj/main.o", "pcm.cache/app.pcm"}))); + expect(eq(steps[0].rule, "cxx_obj")); + expect(eq(steps[1].outputs, std::vector{"bin/app.bin"})); + expect(eq(steps[2].outputs, std::vector{"obj/with space.o"})); + expect(eq(steps[3].outputs, std::vector{"C:/abs/gen.stamp"})); + expect(eq(steps[4].owner, "")); // The first explicit input, unescaped; an implicit input is not one. - EXPECT_EQ(steps[0].input, "src/main.cpp"); - EXPECT_EQ(steps[2].input, "src/with space.cpp"); - EXPECT_EQ(steps[3].input, "a.in"); + expect(eq(steps[0].input, "src/main.cpp")); + expect(eq(steps[2].input, "src/with space.cpp")); + expect(eq(steps[3].input, "a.in")); auto r = a.record({{"app", true, "app v0.1.0 (.)", 0, 0}}); - EXPECT_EQ(r.steps, 5u); - ASSERT_EQ(r.packages.size(), 2u); - EXPECT_EQ(r.packages[0].steps, 2u); - EXPECT_EQ(r.packages[0].subject, "app v0.1.0 (.)"); + expect(eq(r.steps, 5u)); + expect(fatal(eq(r.packages.size(), 2u))); + expect(eq(r.packages[0].steps, 2u)); + expect(eq(r.packages[0].subject, "app v0.1.0 (.)")); // An owner the plan did not declare is shown by its name, as a dependency. - EXPECT_EQ(r.packages[1].name, "mcpplibs.lib"); - EXPECT_FALSE(r.packages[1].requested); - EXPECT_EQ(r.packages[1].steps, 2u); - EXPECT_EQ(r.owner.at("pcm.cache/app.pcm"), 0u); - EXPECT_EQ(r.owner.at("obj/with space.o"), 1u); - EXPECT_FALSE(r.owner.contains("obj/mcpp_ios_init.o")); - EXPECT_EQ(r.actions.at("C:/abs/gen.stamp"), "vcpkg install"); + expect(eq(r.packages[1].name, "mcpplibs.lib")); + expect(!(r.packages[1].requested)); + expect(eq(r.packages[1].steps, 2u)); + expect(eq(r.owner.at("pcm.cache/app.pcm"), 0u)); + expect(eq(r.owner.at("obj/with space.o"), 1u)); + expect(!r.owner.contains("obj/mcpp_ios_init.o")); + expect(eq(r.actions.at("C:/abs/gen.stamp"), "vcpkg install")); // The outputs of one step share its identity. - EXPECT_EQ(r.step.at("obj/main.o"), r.step.at("pcm.cache/app.pcm")); - EXPECT_NE(r.step.at("obj/main.o"), r.step.at("bin/app.bin")); + expect(eq(r.step.at("obj/main.o"), r.step.at("pcm.cache/app.pcm"))); + expect(ne(r.step.at("obj/main.o"), r.step.at("bin/app.bin"))); r.packages[0].detail = "v0.1.0 (.)"; r.packages[0].source = "project"; auto back = parse_record(format_record(r)); - EXPECT_EQ(back.steps, r.steps); - ASSERT_EQ(back.packages.size(), 2u); - EXPECT_EQ(back.packages[0].subject, "app v0.1.0 (.)"); - EXPECT_EQ(back.packages[0].detail, "v0.1.0 (.)"); - EXPECT_EQ(back.packages[0].source, "project"); - EXPECT_TRUE(back.packages[0].requested); - EXPECT_EQ(back.owner, r.owner); - EXPECT_EQ(back.step, r.step); - EXPECT_EQ(back.actions, r.actions); -} - -TEST(ProgressRecord, AVersionOneRecordIsRead) { + expect(eq(back.steps, r.steps)); + expect(fatal(eq(back.packages.size(), 2u))); + expect(eq(back.packages[0].subject, "app v0.1.0 (.)")); + expect(eq(back.packages[0].detail, "v0.1.0 (.)")); + expect(eq(back.packages[0].source, "project")); + expect(bool(back.packages[0].requested)); + expect(eq(back.owner, r.owner)); + expect(eq(back.step, r.step)); + expect(eq(back.actions, r.actions)); +}}; + +UTest a_version_one_record_is_read{"ProgressRecord/AVersionOneRecordIsRead", [] { // 2026.9.29.5 wrote version 1: six fields to a package line, the subject // carrying the origin. The fast path of a newer mcpp reads it as it is. auto r = parse_record("# mcpp steps v1\t3\nP\txlings\t1\t0\t2\txlings (.)\n" "O\t0\t0\tobj/a.o\n"); - EXPECT_EQ(r.steps, 3u); - ASSERT_EQ(r.packages.size(), 1u); - EXPECT_EQ(r.packages[0].subject, "xlings (.)"); - EXPECT_TRUE(r.packages[0].detail.empty()); - EXPECT_TRUE(r.packages[0].source.empty()); - EXPECT_TRUE(r.descriptor.empty()); - EXPECT_TRUE(r.sources.empty()); - EXPECT_EQ(r.owner.at("obj/a.o"), 0u); -} - -TEST(ProgressRecord, ACompileIsNamedByItsSourceAndTheHeaderCarriesTheDescriptor) { + expect(eq(r.steps, 3u)); + expect(fatal(eq(r.packages.size(), 1u))); + expect(eq(r.packages[0].subject, "xlings (.)")); + expect(r.packages[0].detail.empty()); + expect(r.packages[0].source.empty()); + expect(r.descriptor.empty()); + expect(r.sources.empty()); + expect(eq(r.owner.at("obj/a.o"), 0u)); +}}; + +UTest a_compile_is_named_by_its_source_and_the_header_carries_the_descriptor{"ProgressRecord/ACompileIsNamedByItsSourceAndTheHeaderCarriesTheDescriptor", [] { // An absolute path on this host, spelled as build.ninja spells it. const bool drive = !std::filesystem::path("/w").is_absolute(); const std::string root = drive ? "C:/w" : "/w", ninjaRoot = drive ? "C$:/w" : "/w"; @@ -217,122 +220,119 @@ TEST(ProgressRecord, ACompileIsNamedByItsSourceAndTheHeaderCarriesTheDescriptor) a.statement("build obj/util/none.stamp: stamp || obj/util/src/u.o\n"); // no explicit input auto r = a.record({{"app", true, "app", 0, 0}}, {root + "/app", root + "/app/deps/util"}); - EXPECT_EQ(r.sources.at(r.step.at("obj/app/src/main.o")), "src/main.cpp"); + expect(eq(r.sources.at(r.step.at("obj/app/src/main.o")), "src/main.cpp")); // The innermost root that holds the file. - EXPECT_EQ(r.sources.at(r.step.at("obj/util/src/u.o")), "src/u.cpp"); - EXPECT_FALSE(r.sources.contains(r.step.at("obj/app/gen.o"))); - EXPECT_FALSE(r.sources.contains(r.step.at("obj/app/x.o"))); - EXPECT_FALSE(r.sources.contains(r.step.at("bin/app"))); - EXPECT_FALSE(r.sources.contains(r.step.at("obj/util/none.stamp"))); + expect(eq(r.sources.at(r.step.at("obj/util/src/u.o")), "src/u.cpp")); + expect(!(r.sources.contains(r.step.at("obj/app/gen.o")))); + expect(!(r.sources.contains(r.step.at("obj/app/x.o")))); + expect(!(r.sources.contains(r.step.at("bin/app")))); + expect(!(r.sources.contains(r.step.at("obj/util/none.stamp")))); r.descriptor = "unoptimized + debuginfo"; auto back = parse_record(format_record(r)); - EXPECT_EQ(back.sources, r.sources); - EXPECT_EQ(back.descriptor, "unoptimized + debuginfo"); + expect(eq(back.sources, r.sources)); + expect(eq(back.descriptor, "unoptimized + debuginfo")); // The fast path reads the header line alone. Tmp t; write_record(t.path, r); - EXPECT_EQ(read_descriptor(t.path), "unoptimized + debuginfo"); - EXPECT_EQ(read_descriptor(t.path / "absent"), ""); -} + expect(eq(read_descriptor(t.path), "unoptimized + debuginfo")); + expect(eq(read_descriptor(t.path / "absent"), "")); +}}; -TEST(ProgressRecord, PathsAreComparedNormalised) { - EXPECT_EQ(normalise("./obj\\a.o"), "obj/a.o"); - EXPECT_EQ(normalise("obj/../obj/b.o"), "obj/b.o"); -} +UTest paths_are_compared_normalised{"ProgressRecord/PathsAreComparedNormalised", [] { + expect(eq(normalise("./obj\\a.o"), "obj/a.o")); + expect(eq(normalise("obj/../obj/b.o"), "obj/b.o")); +}}; // ─── Measures of text ──────────────────────────────────────────────────── -TEST(ProgressText, DisplayWidthCountsColumns) { - EXPECT_EQ(mcpp::ui::display_width("abc"), 3u); - EXPECT_EQ(mcpp::ui::display_width("\033[1m\033[92mabc\033[0m"), 3u); - EXPECT_EQ(mcpp::ui::display_width("中文"), 4u); - EXPECT_EQ(mcpp::ui::display_width("a · b → c …"), 11u); -} - -TEST(ProgressText, FitCutsByColumnsAndClosesColour) { - EXPECT_EQ(mcpp::ui::fit("abcdef", 6), "abcdef"); - EXPECT_EQ(mcpp::ui::fit("abcdef", 4), "abc…"); - EXPECT_EQ(mcpp::ui::fit("中文路径", 5), "中文…"); +UTest display_width_counts_columns{"ProgressText/DisplayWidthCountsColumns", [] { + expect(eq(mcpp::ui::display_width("abc"), 3u)); + expect(eq(mcpp::ui::display_width("\033[1m\033[92mabc\033[0m"), 3u)); + expect(eq(mcpp::ui::display_width("中文"), 4u)); + expect(eq(mcpp::ui::display_width("a · b → c …"), 11u)); +}}; + +UTest fit_cuts_by_columns_and_closes_colour{"ProgressText/FitCutsByColumnsAndClosesColour", [] { + expect(eq(mcpp::ui::fit("abcdef", 6), "abcdef")); + expect(eq(mcpp::ui::fit("abcdef", 4), "abc…")); + expect(eq(mcpp::ui::fit("中文路径", 5), "中文…")); auto coloured = mcpp::ui::fit("\033[32mabcdef\033[0m", 3); - EXPECT_EQ(mcpp::ui::display_width(coloured), 3u); - EXPECT_TRUE(coloured.ends_with("\033[0m")); -} + expect(eq(mcpp::ui::display_width(coloured), 3u)); + expect(coloured.ends_with("\033[0m")); +}}; -TEST(ProgressText, OneFormatForEveryDuration) { +UTest one_format_for_every_duration{"ProgressText/OneFormatForEveryDuration", [] { using ms = std::chrono::milliseconds; - EXPECT_EQ(mcpp::ui::format_duration(ms(840)), "0.84s"); - EXPECT_EQ(mcpp::ui::format_duration(ms(12'340)), "12.34s"); - EXPECT_EQ(mcpp::ui::format_duration(ms(192'000)), "3m12s"); - EXPECT_EQ(mcpp::ui::format_duration(ms(3'720'000)), "1h02m"); - EXPECT_EQ(mcpp::ui::format_clock(ms(872'000)), "14:32"); - EXPECT_EQ(mcpp::ui::format_clock(ms(3'730'000)), "1:02:10"); -} - -TEST(ProgressText, TheStateStartsAtTheBlocksColumn) { + expect(eq(mcpp::ui::format_duration(ms(840)), "0.84s")); + expect(eq(mcpp::ui::format_duration(ms(12'340)), "12.34s")); + expect(eq(mcpp::ui::format_duration(ms(192'000)), "3m12s")); + expect(eq(mcpp::ui::format_duration(ms(3'720'000)), "1h02m")); + expect(eq(mcpp::ui::format_clock(ms(872'000)), "14:32")); + expect(eq(mcpp::ui::format_clock(ms(3'730'000)), "1:02:10")); +}}; + +UTest the_state_starts_at_the_blocks_column{"ProgressText/TheStateStartsAtTheBlocksColumn", [] { mcpp::ui::disable_color(); - EXPECT_EQ(mcpp::ui::step_line("Compiling", "core (core)", 16, "done 1.20s"), - " Compiling core (core) done 1.20s"); + expect(eq(mcpp::ui::step_line("Compiling", "core (core)", 16, "done 1.20s"), " Compiling core (core) done 1.20s")); // A longer subject pushes its state two columns further. - EXPECT_EQ(mcpp::ui::step_line("Compiling", "a-long-subject-here", 16, "failed"), - " Compiling a-long-subject-here failed"); -} + expect(eq(mcpp::ui::step_line("Compiling", "a-long-subject-here", 16, "failed"), " Compiling a-long-subject-here failed")); +}}; // ─── The region ────────────────────────────────────────────────────────── -TEST(ProgressRegion, TheStatusLineIsTheLastRowWithNoBlankRow) { +UTest the_status_line_is_the_last_row_with_no_blank_row{"ProgressRegion/TheStatusLineIsTheLastRowWithNoBlankRow", [] { mcpp::ui::Frame f{{}, " Building 612/1203 · 14:32"}; auto rows = mcpp::ui::region_rows({"bar"}, f, 10); - ASSERT_EQ(rows.size(), 2u); - EXPECT_EQ(rows[0], "bar"); - EXPECT_EQ(rows[1], " Building 612/1203 · 14:32"); -} + expect(fatal(eq(rows.size(), 2u))); + expect(eq(rows[0], "bar")); + expect(eq(rows[1], " Building 612/1203 · 14:32")); +}}; -TEST(ProgressRegion, LinesBeyondTheRoomAreSummarised) { +UTest lines_beyond_the_room_are_summarised{"ProgressRegion/LinesBeyondTheRoomAreSummarised", [] { mcpp::ui::Frame f; for (int i = 0; i < 14; ++i) f.lines.push_back(std::format("line {}", i)); f.status = "Building"; auto rows = mcpp::ui::region_rows({}, f, 10); - ASSERT_EQ(rows.size(), 11u); // 9 lines, the summary, the status - EXPECT_NE(rows[9].find("… 5 more"), std::string::npos); -} + expect(fatal(eq(rows.size(), 11u))); // 9 lines, the summary, the status + expect(ne(rows[9].find("… 5 more"), std::string::npos)); +}}; -TEST(ProgressRegion, AFrameOverwritesTheOldRowsInsteadOfErasingThem) { +UTest a_frame_overwrites_the_old_rows_instead_of_erasing_them{"ProgressRegion/AFrameOverwritesTheOldRowsInsteadOfErasingThem", [] { // From the first row of a region of two rows: the line over the first, // the new status row over the second, the tail of each cleared, and only // then what is left below. No row is erased before it is written // (revision 3, §9.2). - EXPECT_EQ(mcpp::ui::frame_bytes(2, " Compiling a\n", {"s"}), - "\r\033[1A Compiling a\033[K\n\033[?7ls\033[K\033[?7h\033[J"); + expect(eq(mcpp::ui::frame_bytes(2, " Compiling a\n", {"s"}), "\r\033[1A Compiling a\033[K\n\033[?7ls\033[K\033[?7h\033[J")); // A region drawn for the first time starts where the cursor is. - EXPECT_EQ(mcpp::ui::frame_bytes(0, {}, {"s"}), "\033[?7ls\033[K\033[?7h"); + expect(eq(mcpp::ui::frame_bytes(0, {}, {"s"}), "\033[?7ls\033[K\033[?7h")); // Erasing leaves the cursor at the region's first row. - EXPECT_EQ(mcpp::ui::frame_bytes(1, {}, {}), "\r\033[J"); + expect(eq(mcpp::ui::frame_bytes(1, {}, {}), "\r\033[J")); auto bytes = mcpp::ui::redraw_bytes(3, {"a", "status"}); - EXPECT_TRUE(bytes.starts_with("\r\033[2A\033[?7la")) << bytes; - EXPECT_EQ(count(bytes, "\033[J"), 1u); - EXPECT_TRUE(bytes.ends_with("\033[J")); -} + expect(bytes.starts_with("\r\033[2A\033[?7la")) << bytes; + expect(eq(count(bytes, "\033[J"), 1u)); + expect(bytes.ends_with("\033[J")); +}}; -TEST(ProgressText, AmbiguousCharactersCountTwoWhereTheTerminalDrawsThemWide) { +UTest ambiguous_characters_count_two_where_the_terminal_draws_them_wide{"ProgressText/AmbiguousCharactersCountTwoWhereTheTerminalDrawsThemWide", [] { mcpp::ui::set_ambiguous_wide(true); - EXPECT_EQ(mcpp::ui::display_width("·"), 2u); - EXPECT_EQ(mcpp::ui::display_width("━─"), 4u); - EXPECT_EQ(mcpp::ui::display_width("⣿⣀"), 2u); // braille is narrow everywhere + expect(eq(mcpp::ui::display_width("·"), 2u)); + expect(eq(mcpp::ui::display_width("━─"), 4u)); + expect(eq(mcpp::ui::display_width("⣿⣀"), 2u)); // braille is narrow everywhere mcpp::ui::set_ambiguous_wide(false); - EXPECT_EQ(mcpp::ui::display_width("·"), 1u); -} + expect(eq(mcpp::ui::display_width("·"), 1u)); +}}; -TEST(ProgressText, ThePhaseIsAlignedWithTheVerbs) { +UTest the_phase_is_aligned_with_the_verbs{"ProgressText/ThePhaseIsAlignedWithTheVerbs", [] { mcpp::ui::disable_color(); - EXPECT_EQ(mcpp::ui::status_line("Building", "612/707 · 0:35"), " Building 612/707 · 0:35"); - EXPECT_EQ(mcpp::ui::status_line("Running", "· 0:04"), " Running · 0:04"); -} + expect(eq(mcpp::ui::status_line("Building", "612/707 · 0:35"), " Building 612/707 · 0:35")); + expect(eq(mcpp::ui::status_line("Running", "· 0:04"), " Running · 0:04")); +}}; // ─── When a package is named (revision 3, §5.3) ────────────────────────── -TEST(ProgressModel, AnActionStartLoadsTheFastPathRecordBeforeNinjaWritesStdout) { +UTest an_action_start_loads_the_fast_path_record_before_ninja_writes_stdout{"ProgressModel/AnActionStartLoadsTheFastPathRecordBeforeNinjaWritesStdout", [] { namespace telemetry = mcpp::build::action_progress; Tmp tmp; Record rec; @@ -346,13 +346,13 @@ TEST(ProgressModel, AnActionStartLoadsTheFastPathRecordBeforeNinjaWritesStdout) build.pass_begin(); const auto env = build.environment(); const auto root = std::ranges::find(env, telemetry::kRootEnv, &decltype(env)::value_type::first); - ASSERT_NE(root, env.end()); + expect(fatal(ne(root, env.end()))); std::ofstream(telemetry::channel(root->second, "gen/check.stamp")) << R"({"v":1,"seq":1,"phase":"internal-check","done":1,"total":2})"; const auto unixMs = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()).count(); append(tmp.path / kStartsFile, std::format("gen/check.stamp\t{}\n", unixMs)); - testing::internal::CaptureStderr(); + auto stderrCapture = mcpplibs::os::stdio::capture::begin(mcpplibs::os::stdio::stream::error).value(); open(false); const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); std::string row; @@ -362,14 +362,14 @@ TEST(ProgressModel, AnActionStartLoadsTheFastPathRecordBeforeNinjaWritesStdout) std::this_thread::sleep_for(std::chrono::milliseconds(10)); } while (std::chrono::steady_clock::now() < deadline); close(); - (void)testing::internal::GetCapturedStderr(); - EXPECT_TRUE(build.has_record()); - EXPECT_NE(row.find("check-action"), std::string::npos) << row; - EXPECT_NE(row.find("internal-check 1/2"), std::string::npos) << row; + (void)stderrCapture.text(); + expect(build.has_record()); + expect(ne(row.find("check-action"), std::string::npos)) << row; + expect(ne(row.find("internal-check 1/2"), std::string::npos)) << row; // No Build::status() or completed ninja-log entry was supplied. -} +}}; -TEST(ProgressModel, ANewSnapshotRedrawsBeforeTheClockHeartbeat) { +UTest a_new_snapshot_redraws_before_the_clock_heartbeat{"ProgressModel/ANewSnapshotRedrawsBeforeTheClockHeartbeat", [] { namespace telemetry = mcpp::build::action_progress; Tmp tmp; Record rec; @@ -381,13 +381,13 @@ TEST(ProgressModel, ANewSnapshotRedrawsBeforeTheClockHeartbeat) { build.pass_begin(); const auto env = build.environment(); const auto root = std::ranges::find(env, telemetry::kRootEnv, &decltype(env)::value_type::first); - ASSERT_NE(root, env.end()); + expect(fatal(ne(root, env.end()))); const auto unixMs = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()).count(); append(tmp.path / kStartsFile, std::format("gen/redraw.stamp\t{}\n", unixMs)); const bool wasLive = mcpp::ui::live_progress(); mcpp::ui::set_live_progress(true); - testing::internal::CaptureStderr(); + auto stderrCapture = mcpplibs::os::stdio::capture::begin(mcpplibs::os::stdio::stream::error).value(); open(false); // Let the initial half-second display guard pass. This line then draws // the frame immediately before publication, resetting the clock redraw. @@ -397,13 +397,13 @@ TEST(ProgressModel, ANewSnapshotRedrawsBeforeTheClockHeartbeat) { << R"({"v":1,"seq":1,"phase":"snapshot-redraw","done":1,"total":2})"; std::this_thread::sleep_for(std::chrono::milliseconds(500)); close(); - const auto out = testing::internal::GetCapturedStderr(); + const auto out = stderrCapture.text(); mcpp::ui::set_live_progress(wasLive); - EXPECT_NE(out.find("snapshot-redraw 1/2"), std::string::npos) << out; + expect(ne(out.find("snapshot-redraw 1/2"), std::string::npos)) << out; // No Ninja stdout, completion or other narration requested a redraw. -} +}}; -TEST(ProgressModel, APackageIsNamedOnceWhenItsFirstStepFinishes) { +UTest a_package_is_named_once_when_its_first_step_finishes{"ProgressModel/APackageIsNamedOnceWhenItsFirstStepFinishes", [] { mcpp::ui::disable_color(); Tmp tmp; Record rec; @@ -416,7 +416,7 @@ TEST(ProgressModel, APackageIsNamedOnceWhenItsFirstStepFinishes) { rec.steps = 4; Build b(tmp.path); b.set_record(rec); - testing::internal::CaptureStderr(); + auto stderrCapture = mcpplibs::os::stdio::capture::begin(mcpplibs::os::stdio::stream::error).value(); b.pass_begin(); const auto log = tmp.path / ".ninja_log"; append(log, "# ninja log v6\n1\t10\t0\tobj/dep.o\taa\n"); @@ -427,22 +427,22 @@ TEST(ProgressModel, APackageIsNamedOnceWhenItsFirstStepFinishes) { b.status({4, 4, 50, 0, {}}); b.pass_end(); b.finish(true); - auto out = testing::internal::GetCapturedStderr(); + auto out = stderrCapture.text(); // The dependency first, as its first step finished first; each once; the // standard library module is the toolchain's and is not named. const auto dep = out.find("Compiling compat.dep v1.0.0"); const auto app = out.find("Compiling app v0.1.0 (.)"); - ASSERT_NE(dep, std::string::npos) << out; - ASSERT_NE(app, std::string::npos) << out; - EXPECT_LT(dep, app) << out; - EXPECT_EQ(count(out, "app v0.1.0"), 1u) << out; - EXPECT_EQ(out.find("std"), std::string::npos) << out; + expect(fatal(ne(dep, std::string::npos))) << out; + expect(fatal(ne(app, std::string::npos))) << out; + expect(lt(dep, app)) << out; + expect(eq(count(out, "app v0.1.0"), 1u)) << out; + expect(eq(out.find("std"), std::string::npos)) << out; // No outcome, no fold: the line states that the package compiles. - EXPECT_EQ(out.find("done"), std::string::npos) << out; - EXPECT_EQ(out.find("dependenc"), std::string::npos) << out; -} + expect(eq(out.find("done"), std::string::npos)) << out; + expect(eq(out.find("dependenc"), std::string::npos)) << out; +}}; -TEST(ProgressModel, APackageTheCacheServesIsNamedCachedWithItsUnits) { +UTest a_package_the_cache_serves_is_named_cached_with_its_units{"ProgressModel/APackageTheCacheServesIsNamedCachedWithItsUnits", [] { mcpp::ui::disable_color(); Tmp tmp; Record rec; @@ -451,16 +451,16 @@ TEST(ProgressModel, APackageTheCacheServesIsNamedCachedWithItsUnits) { rec.step = {{"obj/a.o", 0}, {"obj/b.o", 1}}; Build b(tmp.path); b.set_record(rec); - testing::internal::CaptureStderr(); + auto stderrCapture = mcpplibs::os::stdio::capture::begin(mcpplibs::os::stdio::stream::error).value(); b.pass_begin(); append(tmp.path / ".ninja_log", "# ninja log v6\n1\t2\t0\tobj/a.o\taa\n1\t2\t0\tobj/b.o\tbb\n"); b.status({2, 2, 2, 0, {}}); b.pass_end(); b.finish(true); - auto out = testing::internal::GetCapturedStderr(); - EXPECT_NE(out.find("Cached compat.ftxui v6.1.9 (73 units)"), std::string::npos) << out; - EXPECT_EQ(count(out, "compat.ftxui"), 1u) << out; -} + auto out = stderrCapture.text(); + expect(ne(out.find("Cached compat.ftxui v6.1.9 (73 units)"), std::string::npos)) << out; + expect(eq(count(out, "compat.ftxui"), 1u)) << out; +}}; // ─── What the count counts (build wall-time plan, W2) ──────────────────── // @@ -469,7 +469,7 @@ TEST(ProgressModel, APackageTheCacheServesIsNamedCachedWithItsUnits) { // scans, and read 967/1195 when its first compile began. A placement pass is // read but not counted, a scan pass shows as `Scanning f/t`, and the main pass // alone is `Building f/t`. -TEST(ProgressModel, OnlyTheMainPassIsCountedAsBuilding) { +UTest only_the_main_pass_is_counted_as_building{"ProgressModel/OnlyTheMainPassIsCountedAsBuilding", [] { mcpp::ui::disable_color(); Tmp tmp; Record rec; @@ -477,7 +477,7 @@ TEST(ProgressModel, OnlyTheMainPassIsCountedAsBuilding) { rec.steps = 1; Build b(tmp.path); b.set_record(rec); - testing::internal::CaptureStderr(); + auto stderrCapture = mcpplibs::os::stdio::capture::begin(mcpplibs::os::stdio::stream::error).value(); b.pass_begin(mcpp::build::progress::PassKind::Placement); b.status({503, 503, 1, 0, {}}); const auto placing = mcpp::build::progress::status_row(); @@ -491,15 +491,15 @@ TEST(ProgressModel, OnlyTheMainPassIsCountedAsBuilding) { const auto building = mcpp::build::progress::status_row(); b.pass_end(); b.finish(true); - testing::internal::GetCapturedStderr(); - EXPECT_EQ(placing.find("503"), std::string::npos) << placing; - EXPECT_NE(scanning.find("Scanning"), std::string::npos) << scanning; - EXPECT_NE(scanning.find("200/460"), std::string::npos) << scanning; - EXPECT_NE(building.find("Building"), std::string::npos) << building; - EXPECT_NE(building.find(" 1/232"), std::string::npos) << building; -} - -TEST(ProgressModel, AFailureNamesItsPackageOnce) { + stderrCapture.text(); + expect(eq(placing.find("503"), std::string::npos)) << placing; + expect(ne(scanning.find("Scanning"), std::string::npos)) << scanning; + expect(ne(scanning.find("200/460"), std::string::npos)) << scanning; + expect(ne(building.find("Building"), std::string::npos)) << building; + expect(ne(building.find(" 1/232"), std::string::npos)) << building; +}}; + +UTest a_failure_names_its_package_once{"ProgressModel/AFailureNamesItsPackageOnce", [] { mcpp::ui::disable_color(); Tmp tmp; Record rec; @@ -509,7 +509,7 @@ TEST(ProgressModel, AFailureNamesItsPackageOnce) { rec.step = {{"obj/a.o", 0}, {"obj/l.o", 1}}; Build b(tmp.path); b.set_record(rec); - testing::internal::CaptureStderr(); + auto stderrCapture = mcpplibs::os::stdio::capture::begin(mcpplibs::os::stdio::stream::error).value(); b.pass_begin(); // A failed step is not in ninja's log: the package is named from the // FAILED line. @@ -517,14 +517,14 @@ TEST(ProgressModel, AFailureNamesItsPackageOnce) { const auto second = b.failed("obj/a.o "); b.pass_end(); b.finish(false); - auto out = testing::internal::GetCapturedStderr(); - ASSERT_TRUE(first.has_value()); - EXPECT_EQ(*first, "lib v0.1.0 (lib)"); - EXPECT_FALSE(second.has_value()); - EXPECT_EQ(count(out, "Compiling lib v0.1.0 (lib)"), 1u) << out; -} - -TEST(ProgressModel, AStepWhoseEntriesAreReadInTwoPiecesCountsOnce) { + auto out = stderrCapture.text(); + expect(fatal(first.has_value())); + expect(eq(*first, "lib v0.1.0 (lib)")); + expect(!second.has_value()); + expect(eq(count(out, "Compiling lib v0.1.0 (lib)"), 1u)) << out; +}}; + +UTest a_step_whose_entries_are_read_in_two_pieces_counts_once{"ProgressModel/AStepWhoseEntriesAreReadInTwoPiecesCountsOnce", [] { // ninja writes one log entry per output and flushes each: a read can land // between a module unit's object and its BMI. mcpp::ui::disable_color(); @@ -537,7 +537,7 @@ TEST(ProgressModel, AStepWhoseEntriesAreReadInTwoPiecesCountsOnce) { rec.step = {{"obj/lib.m.o", 0}, {"pcm.cache/lib.pcm", 0}, {"bin/lib.a", 1}}; Build b(tmp.path); b.set_record(rec); - testing::internal::CaptureStderr(); + auto stderrCapture = mcpplibs::os::stdio::capture::begin(mcpplibs::os::stdio::stream::error).value(); b.pass_begin(); const auto log = tmp.path / ".ninja_log"; append(log, "# ninja log v6\n1\t10\t0\tobj/lib.m.o\taa\n"); @@ -546,15 +546,15 @@ TEST(ProgressModel, AStepWhoseEntriesAreReadInTwoPiecesCountsOnce) { b.status({1, 2, 10, {}, {}}); b.pass_end(); b.finish(true); - auto out = testing::internal::GetCapturedStderr(); - EXPECT_EQ(count(out, "Compiling mlib"), 1u) << out; -} + auto out = stderrCapture.text(); + expect(eq(count(out, "Compiling mlib"), 1u)) << out; +}}; -TEST(ProgressModel, APackageTwoConfigurationsShareIsAnnouncedOncePerCommand) { +UTest a_package_two_configurations_share_is_announced_once_per_command{"ProgressModel/APackageTwoConfigurationsShareIsAnnouncedOncePerCommand", [] { // A workspace builds one plan per configuration; a dependency they share // is the same line in each, and the command writes it once (L6). mcpp::ui::disable_color(); - testing::internal::CaptureStderr(); + auto stderrCapture = mcpplibs::os::stdio::capture::begin(mcpplibs::os::stdio::stream::error).value(); for (int config = 0; config < 2; ++config) { Tmp tmp; Record rec; @@ -569,19 +569,19 @@ TEST(ProgressModel, APackageTwoConfigurationsShareIsAnnouncedOncePerCommand) { b.pass_end(); b.finish(true); } - auto out = testing::internal::GetCapturedStderr(); - EXPECT_EQ(count(out, "Compiling shared v0.2.0 (shared)"), 1u) << out; -} + auto out = stderrCapture.text(); + expect(eq(count(out, "Compiling shared v0.2.0 (shared)"), 1u)) << out; +}}; // `Finished` is written once per command. `mcpp run --format ` packs, and // the pack states its build with `Finished`; the run then drives the same build // again, and its second `Finished` repeated the line with the first's time // counted twice (pack drive and selection design 2026-10-01, A3). -TEST(ProgressModel, FinishedIsWrittenOncePerCommand) { +UTest finished_is_written_once_per_command{"ProgressModel/FinishedIsWrittenOncePerCommand", [] { mcpp::ui::disable_color(); - testing::internal::CaptureStderr(); + auto stderrCapture = mcpplibs::os::stdio::capture::begin(mcpplibs::os::stdio::stream::error).value(); mcpp::build::progress::finished("release", "optimized"); mcpp::build::progress::finished("release", "optimized"); - auto out = testing::internal::GetCapturedStderr(); - EXPECT_EQ(count(out, "Finished"), 1u) << out; -} + auto out = stderrCapture.text(); + expect(eq(count(out, "Finished"), 1u)) << out; +}}; diff --git a/tests/unit/test_build_resources.cpp b/tests/unit/test_build_resources.cpp index 6d8369907..4e8e30456 100644 --- a/tests/unit/test_build_resources.cpp +++ b/tests/unit/test_build_resources.cpp @@ -1,36 +1,32 @@ -#include import std; +import mcpplibs.mtest; import mcpp.manifest; import mcpp.build.resources; import mcpplibs.os.platform; import mcpp.toolchain.detect; +using namespace mcpplibs::mtest; + namespace res = mcpp::build::resources; namespace fs = std::filesystem; -TEST(BuildResources, CoffTargetFollowsTheTargetInsteadOfTheHost) { +UTest coff_target_follows_the_target_instead_of_the_host{"BuildResources/CoffTargetFollowsTheTargetInsteadOfTheHost", [] { const res::RcTool llvm{"/selected/bin/llvm-windres.exe", "gnu", true}; - EXPECT_EQ(res::coff_target_flag(llvm, "i686-windows-msvc"), - "--target=i686-pc-windows-msvc"); - EXPECT_EQ(res::coff_target_flag(llvm, "x86_64-windows-msvc"), - "--target=x86_64-pc-windows-msvc"); - EXPECT_EQ(res::coff_target_flag(llvm, "aarch64-windows-msvc"), - "--target=aarch64-pc-windows-msvc"); + expect(eq(res::coff_target_flag(llvm, "i686-windows-msvc"), "--target=i686-pc-windows-msvc")); + expect(eq(res::coff_target_flag(llvm, "x86_64-windows-msvc"), "--target=x86_64-pc-windows-msvc")); + expect(eq(res::coff_target_flag(llvm, "aarch64-windows-msvc"), "--target=aarch64-pc-windows-msvc")); const res::RcTool gnu{"/selected/bin/windres", "gnu"}; for (const auto arch : {"x86", "i386", "i486", "i586", "i686"}) { - EXPECT_EQ(res::coff_target_flag(gnu, std::string(arch) + "-windows-gnu"), - "--target=pe-i386") << arch; + expect(eq(res::coff_target_flag(gnu, std::string(arch) + "-windows-gnu"), "--target=pe-i386")) << arch; } - EXPECT_EQ(res::coff_target_flag(llvm, "i386-windows-msvc"), - "--target=i386-pc-windows-msvc"); + expect(eq(res::coff_target_flag(llvm, "i386-windows-msvc"), "--target=i386-pc-windows-msvc")); // MSVC's spelling reaches LLVM as the triple LLVM accepts (#776 review). - EXPECT_EQ(res::coff_target_flag(llvm, "x86-windows-msvc"), - "--target=i686-pc-windows-msvc"); - EXPECT_EQ(res::coff_target_flag(gnu, "x86_64-windows-gnu"), "--target=pe-x86-64"); - EXPECT_TRUE(res::coff_target_flag({"rc.exe", "msvc"}, "i686-windows-msvc").empty()); - EXPECT_TRUE(res::coff_target_flag({"llvm-rc.exe", "msvc"}, "x86_64-windows-msvc").empty()); -} + expect(eq(res::coff_target_flag(llvm, "x86-windows-msvc"), "--target=i686-pc-windows-msvc")); + expect(eq(res::coff_target_flag(gnu, "x86_64-windows-gnu"), "--target=pe-x86-64")); + expect(res::coff_target_flag({"rc.exe", "msvc"}, "i686-windows-msvc").empty()); + expect(res::coff_target_flag({"llvm-rc.exe", "msvc"}, "x86_64-windows-msvc").empty()); +}}; namespace { @@ -66,38 +62,38 @@ struct TempDir { // ─── Which windres: decided where the tool is found ─────────────────────── -TEST(BuildResources, LlvmWindresIsRecognisedThroughTheSymlinkThatNamesIt) { +UTest llvm_windres_is_recognised_through_the_symlink_that_names_it{"BuildResources/LlvmWindresIsRecognisedThroughTheSymlinkThatNamesIt", [] { if constexpr (mcpplibs::os::is_windows) - GTEST_SKIP() << "creating a symlink needs a privilege a Windows runner may lack"; + skip("creating a symlink needs a privilege a Windows runner may lack"); TempDir d; const auto real = d.write("llvm-rc", ""); std::error_code ec; fs::create_symlink(real, d.path / "llvm-windres", ec); - ASSERT_FALSE(ec) << ec.message(); + expect(fatal(!ec)) << ec.message(); fs::create_symlink(d.path / "llvm-windres", d.path / "i686-w64-mingw32-windres", ec); - ASSERT_FALSE(ec) << ec.message(); + expect(fatal(!ec)) << ec.message(); const auto gnu = d.write("windres", ""); - EXPECT_TRUE(res::is_llvm_windres(d.path / "llvm-windres")); - EXPECT_TRUE(res::is_llvm_windres(d.path / "i686-w64-mingw32-windres")); - EXPECT_FALSE(res::is_llvm_windres(gnu)); - EXPECT_FALSE(res::is_llvm_windres(d.path / "absent-windres")); + expect(res::is_llvm_windres(d.path / "llvm-windres")); + expect(res::is_llvm_windres(d.path / "i686-w64-mingw32-windres")); + expect(!res::is_llvm_windres(gnu)); + expect(!res::is_llvm_windres(d.path / "absent-windres")); // And `find_rc_tool` records it: the llvm-mingw spelling gets the triple. mcpp::toolchain::Toolchain tc; tc.binaryPath = d.path / "clang"; tc.targetTriple = "i686-windows-gnu"; auto tool = res::find_rc_tool(tc, "gnu"); - ASSERT_TRUE(tool.has_value()); - EXPECT_EQ(tool->name(), "i686-w64-mingw32-windres"); - EXPECT_TRUE(tool->llvm); - EXPECT_EQ(res::coff_target_flag(*tool, tc.targetTriple), "--target=i686-w64-windows-gnu"); -} + expect(fatal(tool.has_value())); + expect(eq(tool->name(), "i686-w64-mingw32-windres")); + expect(bool(tool->llvm)); + expect(eq(res::coff_target_flag(*tool, tc.targetTriple), "--target=i686-w64-windows-gnu")); +}}; // ─── The build program's manifest object: the command is part of it ─────── -TEST(BuildResources, TheManifestObjectIsRemadeWhenItsCommandChanges) { +UTest the_manifest_object_is_remade_when_its_command_changes{"BuildResources/TheManifestObjectIsRemadeWhenItsCommandChanges", [] { if constexpr (mcpplibs::os::is_windows) - GTEST_SKIP() << "the stand-in resource compiler is a POSIX shell script"; + skip("the stand-in resource compiler is a POSIX shell script"); TempDir d; // A windres that writes the words it was given into its output. const auto tool = d.write("windres", @@ -114,23 +110,23 @@ TEST(BuildResources, TheManifestObjectIsRemadeWhenItsCommandChanges) { }; auto first = res::compile_utf8_manifest(tc, "gnu", d.path / "out", "m"); - ASSERT_TRUE(first.has_value()) << first.error(); - EXPECT_NE(read(*first).find("--target=pe-x86-64"), std::string::npos); + if (auto&& failed = expect(fatal(first.has_value())); !failed) failed << first.error(); + expect(ne(read(*first).find("--target=pe-x86-64"), std::string::npos)); // The same command reuses the object: a marker written into it survives. { std::ofstream(*first, std::ios::binary) << "reused"; } auto again = res::compile_utf8_manifest(tc, "gnu", d.path / "out", "m"); - ASSERT_TRUE(again.has_value()) << again.error(); - EXPECT_EQ(read(*again), "reused"); + if (auto&& failed = expect(fatal(again.has_value())); !failed) failed << again.error(); + expect(eq(read(*again), "reused")); // Another target is another command, so the object is made again. Before // 2026.10.5.3 only the manifest and the script were compared, and this // returned the x86-64 object. tc.targetTriple = "i686-windows-gnu"; auto other = res::compile_utf8_manifest(tc, "gnu", d.path / "out", "m"); - ASSERT_TRUE(other.has_value()) << other.error(); - EXPECT_NE(read(*other).find("--target=pe-i386"), std::string::npos); -} + if (auto&& failed = expect(fatal(other.has_value())); !failed) failed << other.error(); + expect(ne(read(*other).find("--target=pe-i386"), std::string::npos)); +}}; // ─── Synthesis: the mcpp#365 headline ───────────────────────────────────── // @@ -140,110 +136,110 @@ TEST(BuildResources, TheManifestObjectIsRemadeWhenItsCommandChanges) { // while GetFileVersionInfo looks up ordinal 1. What mcpp generates must // therefore never spell the macro. -TEST(BuildResources, SynthesizedScriptNamesTheVersionResourceByOrdinal) { +UTest synthesized_script_names_the_version_resource_by_ordinal{"BuildResources/SynthesizedScriptNamesTheVersionResourceByOrdinal", [] { mcpp::manifest::Resources r; auto rc = res::synthesize_rc(sample_package(), r, "myapp.exe", {}); - ASSERT_TRUE(rc) << rc.error(); - EXPECT_NE(rc->find("1 VERSIONINFO"), std::string::npos); - EXPECT_EQ(rc->find("VS_VERSION_INFO"), std::string::npos) + if (auto&& failed = expect(fatal(bool(rc))); !failed) failed << rc.error(); + expect(ne(rc->find("1 VERSIONINFO"), std::string::npos)); + expect(eq(rc->find("VS_VERSION_INFO"), std::string::npos)) << "the macro is only defined when was included; naming it " "here is the bug this feature exists to avoid"; -} +}}; -TEST(BuildResources, FileVersionComesFromThePackageVersion) { +UTest file_version_comes_from_the_package_version{"BuildResources/FileVersionComesFromThePackageVersion", [] { mcpp::manifest::Resources r; auto rc = res::synthesize_rc(sample_package(), r, "myapp.exe", {}); - ASSERT_TRUE(rc); - EXPECT_NE(rc->find("FILEVERSION 0,2,0,0"), std::string::npos); - EXPECT_NE(rc->find("PRODUCTVERSION 0,2,0,0"), std::string::npos); + expect(fatal(bool(rc))); + expect(ne(rc->find("FILEVERSION 0,2,0,0"), std::string::npos)); + expect(ne(rc->find("PRODUCTVERSION 0,2,0,0"), std::string::npos)); // The STRING fields keep the version verbatim, so a form the numeric // fields cannot hold (a pre-release) is still visible in the properties // dialog. - EXPECT_NE(rc->find("\"FileVersion\", \"0.2.0\""), std::string::npos); -} + expect(ne(rc->find("\"FileVersion\", \"0.2.0\""), std::string::npos)); +}}; -TEST(BuildResources, FourSegmentDateVersionsFitTheNumericFields) { +UTest four_segment_date_versions_fit_the_numeric_fields{"BuildResources/FourSegmentDateVersionsFitTheNumericFields", [] { auto pkg = sample_package(); pkg.version = "2026.8.7.1"; mcpp::manifest::Resources r; auto rc = res::synthesize_rc(pkg, r, "mcpp.exe", {}); - ASSERT_TRUE(rc) << rc.error(); - EXPECT_NE(rc->find("FILEVERSION 2026,8,7,1"), std::string::npos); -} + if (auto&& failed = expect(fatal(bool(rc))); !failed) failed << rc.error(); + expect(ne(rc->find("FILEVERSION 2026,8,7,1"), std::string::npos)); +}}; -TEST(BuildResources, AVersionFieldThatCannotFitIsAnErrorNotAClamp) { +UTest a_version_field_that_cannot_fit_is_an_error_not_a_clamp{"BuildResources/AVersionFieldThatCannotFitIsAnErrorNotAClamp", [] { auto pkg = sample_package(); pkg.version = "70000.0.0"; // > 65535: FILEVERSION fields are 16-bit mcpp::manifest::Resources r; auto rc = res::synthesize_rc(pkg, r, "myapp.exe", {}); - ASSERT_FALSE(rc); - EXPECT_NE(rc.error().find("65535"), std::string::npos); + expect(fatal(!rc)); + expect(ne(rc.error().find("65535"), std::string::npos)); // Clamping would put a version in the binary that is not the version that // was built — the failure mode is a wrong answer, so it must not be silent. - EXPECT_NE(rc.error().find("70000"), std::string::npos); -} + expect(ne(rc.error().find("70000"), std::string::npos)); +}}; -TEST(BuildResources, MetadataDefaultsFromPackageAndIsOverridable) { +UTest metadata_defaults_from_package_and_is_overridable{"BuildResources/MetadataDefaultsFromPackageAndIsOverridable", [] { mcpp::manifest::Resources r; auto rc = res::synthesize_rc(sample_package(), r, "myapp.exe", {}); - ASSERT_TRUE(rc); - EXPECT_NE(rc->find("\"CompanyName\", \"Acme\""), std::string::npos); - EXPECT_NE(rc->find("\"ProductName\", \"myapp\""), std::string::npos); - EXPECT_NE(rc->find("\"FileDescription\", \"My application\""), std::string::npos); - EXPECT_NE(rc->find("\"OriginalFilename\", \"myapp.exe\""), std::string::npos); + expect(fatal(bool(rc))); + expect(ne(rc->find("\"CompanyName\", \"Acme\""), std::string::npos)); + expect(ne(rc->find("\"ProductName\", \"myapp\""), std::string::npos)); + expect(ne(rc->find("\"FileDescription\", \"My application\""), std::string::npos)); + expect(ne(rc->find("\"OriginalFilename\", \"myapp.exe\""), std::string::npos)); r.info.company = "Other Co"; r.info.product = "Renamed"; auto rc2 = res::synthesize_rc(sample_package(), r, "myapp.exe", {}); - ASSERT_TRUE(rc2); - EXPECT_NE(rc2->find("\"CompanyName\", \"Other Co\""), std::string::npos); - EXPECT_NE(rc2->find("\"ProductName\", \"Renamed\""), std::string::npos); -} + expect(fatal(bool(rc2))); + expect(ne(rc2->find("\"CompanyName\", \"Other Co\""), std::string::npos)); + expect(ne(rc2->find("\"ProductName\", \"Renamed\""), std::string::npos)); +}}; -TEST(BuildResources, GeneratedTextIsAsciiSoItDoesNotDependOnTheCodepageFlag) { +UTest generated_text_is_ascii_so_it_does_not_depend_on_the_codepage_flag{"BuildResources/GeneratedTextIsAsciiSoItDoesNotDependOnTheCodepageFlag", [] { // A UTF-8 codepage is passed to the rc tool for the USER's metadata, but // text mcpp writes itself must not need it: an em dash in the default // copyright line failed llvm-rc outright ("Non-ASCII 8-bit codepoint"). mcpp::manifest::Resources r; auto rc = res::synthesize_rc(sample_package(), r, "myapp.exe", {}); - ASSERT_TRUE(rc); + expect(fatal(bool(rc))); auto generated = rc->substr(0, rc->find("VALUE \"CompanyName\"")); for (unsigned char c : generated) - EXPECT_LT(c, 0x80u) << "generated scaffolding must stay ASCII"; - EXPECT_NE(rc->find("\"LegalCopyright\", \"(C) Acme - MIT\""), std::string::npos); -} + expect(lt(c, 0x80u)) << "generated scaffolding must stay ASCII"; + expect(ne(rc->find("\"LegalCopyright\", \"(C) Acme - MIT\""), std::string::npos)); +}}; -TEST(BuildResources, IconIsEmittedAtOrdinalOne) { +UTest icon_is_emitted_at_ordinal_one{"BuildResources/IconIsEmittedAtOrdinalOne", [] { mcpp::manifest::Resources r; r.icon = "assets/app.ico"; auto rc = res::synthesize_rc(sample_package(), r, "myapp.exe", "/proj/assets/app.ico"); - ASSERT_TRUE(rc); + expect(fatal(bool(rc))); // Explorer shows the lowest-numbered icon group. - EXPECT_NE(rc->find("1 ICON \"/proj/assets/app.ico\""), std::string::npos); -} + expect(ne(rc->find("1 ICON \"/proj/assets/app.ico\""), std::string::npos)); +}}; // The 3-row rule: author-supplied scripts own the resource ID space, so mcpp // does not add a second VERSIONINFO behind their back — unless asked. -TEST(BuildResources, VersionInfoSynthesisFollowsTheThreeRowRule) { +UTest version_info_synthesis_follows_the_three_row_rule{"BuildResources/VersionInfoSynthesisFollowsTheThreeRowRule", [] { mcpp::manifest::Resources r; - EXPECT_TRUE(r.synthesize_version_info()); // nothing declared + expect(bool(r.synthesize_version_info())); // nothing declared r.files = {"res/app.rc"}; - EXPECT_FALSE(r.synthesize_version_info()); // author took over + expect(!r.synthesize_version_info()); // author took over r.versionInfo = true; - EXPECT_TRUE(r.synthesize_version_info()); // explicit opt-in wins + expect(bool(r.synthesize_version_info())); // explicit opt-in wins r.versionInfo = false; - EXPECT_FALSE(r.synthesize_version_info()); + expect(!r.synthesize_version_info()); r.files.clear(); - EXPECT_FALSE(r.synthesize_version_info()); // explicit opt-out wins -} + expect(!r.synthesize_version_info()); // explicit opt-out wins +}}; // ─── Scanning an author-written .rc ─────────────────────────────────────── -TEST(BuildResources, ScanCollectsQuotedIncludesAndDataFiles) { +UTest scan_collects_quoted_includes_and_data_files{"BuildResources/ScanCollectsQuotedIncludesAndDataFiles", [] { TempDir d; auto rc = d.write("app.rc", R"(#include "ids.h" #include @@ -260,25 +256,25 @@ END return std::any_of(s.inputs.begin(), s.inputs.end(), [&](const fs::path& p){ return p.filename() == leaf; }); }; - EXPECT_TRUE(has("ids.h")); - EXPECT_TRUE(has("app.ico")); - EXPECT_TRUE(has("blob.bin")); + expect(bool(has("ids.h"))); + expect(bool(has("app.ico"))); + expect(bool(has("blob.bin"))); // An application manifest is a file like the icon (#693): type 24 at the // TYPE position, spelt numerically, which no keyword names. - EXPECT_TRUE(has("app.manifest")); - EXPECT_TRUE(s.declaresManifest); + expect(bool(has("app.manifest"))); + expect(bool(s.declaresManifest)); // Angled includes belong to the toolchain: immutable for the life of a // build directory and already folded into the fingerprint. - EXPECT_FALSE(has("windows.h")); + expect(!has("windows.h")); // STRINGTABLE carries its data inline — nothing to track. - EXPECT_EQ(s.inputs.size(), 4u); -} + expect(eq(s.inputs.size(), 4u)); +}}; // A quoted include and a resource file are found where the resource compiler // finds them: beside the script, then in the include directories it is given. // A name found nowhere stays beside the script, where a file a build action // produces is ordered before the compile. -TEST(BuildResources, InputsAreResolvedThroughTheIncludeDirectories) { +UTest inputs_are_resolved_through_the_include_directories{"BuildResources/InputsAreResolvedThroughTheIncludeDirectories", [] { TempDir d; fs::create_directories(d.path / "include"); d.write("include/ids.h", "#define APP_ICON 101\n"); @@ -295,15 +291,15 @@ TEST(BuildResources, InputsAreResolvedThroughTheIncludeDirectories) { for (auto const& p : s.inputs) if (p.filename() == leaf) return p; return fs::path{}; }; - EXPECT_EQ(at("ids.h"), d.path / "include" / "ids.h"); - EXPECT_EQ(at("app.ico"), d.path / "include" / "app.ico"); - EXPECT_EQ(at("local.ico"), d.path / "local.ico"); - EXPECT_EQ(at("generated.ico"), d.path / "generated.ico"); -} + expect(eq(at("ids.h"), d.path / "include" / "ids.h")); + expect(eq(at("app.ico"), d.path / "include" / "app.ico")); + expect(eq(at("local.ico"), d.path / "local.ico")); + expect(eq(at("generated.ico"), d.path / "generated.ico")); +}}; // The numbers of a VERSIONINFO block contain 24 as well; only the TYPE // position of a statement declares a manifest. -TEST(BuildResources, ATwentyFourOutsideTheTypePositionIsNotAManifest) { +UTest a_twenty_four_outside_the_type_position_is_not_a_manifest{"BuildResources/ATwentyFourOutsideTheTypePositionIsNotAManifest", [] { TempDir d; auto rc = d.write("app.rc", R"(1 VERSIONINFO FILEVERSION 24,1,0,0 @@ -312,53 +308,52 @@ BEGIN END )"); auto s = res::scan_rc(rc); - EXPECT_FALSE(s.declaresManifest); - EXPECT_TRUE(s.inputs.empty()); -} + expect(!s.declaresManifest); + expect(s.inputs.empty()); +}}; // The manifest `windows_code_page = "utf-8"` embeds sits at ordinal 1 of type // 24, and nothing else is added to a script synthesised for it alone. -TEST(BuildResources, AManifestOnlyScriptCarriesTheManifestAndNothingElse) { +UTest a_manifest_only_script_carries_the_manifest_and_nothing_else{"BuildResources/AManifestOnlyScriptCarriesTheManifestAndNothingElse", [] { mcpp::manifest::Resources r; r.versionInfo = false; auto rc = res::synthesize_rc(sample_package(), r, "tool.exe", {}, fs::path("/b/res/tool.mcpp.manifest")); - ASSERT_TRUE(rc) << rc.error(); - EXPECT_NE(rc->find("1 24 \"/b/res/tool.mcpp.manifest\""), std::string::npos) << *rc; - EXPECT_EQ(rc->find("VERSIONINFO"), std::string::npos) << *rc; - EXPECT_EQ(rc->find("ICON"), std::string::npos) << *rc; - EXPECT_NE(res::utf8_code_page_manifest().find( - "UTF-8"), - std::string::npos); -} + if (auto&& failed = expect(fatal(bool(rc))); !failed) failed << rc.error(); + if (auto&& failed = expect(ne(rc->find("1 24 \"/b/res/tool.mcpp.manifest\""), std::string::npos)); !failed) failed << *rc; + if (auto&& failed = expect(eq(rc->find("VERSIONINFO"), std::string::npos)); !failed) failed << *rc; + if (auto&& failed = expect(eq(rc->find("ICON"), std::string::npos)); !failed) failed << *rc; + expect(ne(res::utf8_code_page_manifest().find( + "UTF-8"), std::string::npos)); +}}; // An application manifest states what was asked of it and nothing else; a // host tool's states both (design 2026-10-16 §11). -TEST(BuildResources, AnApplicationManifestStatesWhatWasAskedOfIt) { +UTest an_application_manifest_states_what_was_asked_of_it{"BuildResources/AnApplicationManifestStatesWhatWasAskedOfIt", [] { const std::string codePage = "UTF-8"; const std::string longPaths = "true"; const auto both = res::application_manifest(true, true); - EXPECT_NE(both.find(codePage), std::string::npos); - EXPECT_NE(both.find(longPaths), std::string::npos); + expect(ne(both.find(codePage), std::string::npos)); + expect(ne(both.find(longPaths), std::string::npos)); const auto onlyLong = res::application_manifest(false, true); - EXPECT_EQ(onlyLong.find("activeCodePage"), std::string::npos); - EXPECT_NE(onlyLong.find(longPaths), std::string::npos); - EXPECT_EQ(res::application_manifest(true, false).find("longPathAware"), std::string::npos); - EXPECT_EQ(res::utf8_code_page_manifest(), both); -} + expect(eq(onlyLong.find("activeCodePage"), std::string::npos)); + expect(ne(onlyLong.find(longPaths), std::string::npos)); + expect(eq(res::application_manifest(true, false).find("longPathAware"), std::string::npos)); + expect(eq(res::utf8_code_page_manifest(), both)); +}}; -TEST(BuildResources, ScanNamesWhatItCouldNotResolve) { +UTest scan_names_what_it_could_not_resolve{"BuildResources/ScanNamesWhatItCouldNotResolve", [] { TempDir d; auto rc = d.write("app.rc", "1 ICON APP_ICON\n"); auto s = res::scan_rc(rc); // A macro hides the file name. Reporting the gap is the whole point: a // silently untracked input leaves a stale resource in a shipped binary. - ASSERT_EQ(s.gaps.size(), 1u); - EXPECT_NE(s.gaps[0].find("APP_ICON"), std::string::npos); - EXPECT_TRUE(s.inputs.empty()); -} + expect(fatal(eq(s.gaps.size(), 1u))); + expect(ne(s.gaps[0].find("APP_ICON"), std::string::npos)); + expect(s.inputs.empty()); +}}; -TEST(BuildResources, ScanFlagsAVersionResourceWindowsWillNotFind) { +UTest scan_flags_a_version_resource_windows_will_not_find{"BuildResources/ScanFlagsAVersionResourceWindowsWillNotFind", [] { TempDir d; auto rc = d.write("bad.rc", R"(VS_VERSION_INFO VERSIONINFO FILEVERSION 0,2,0,0 @@ -366,13 +361,13 @@ BEGIN END )"); auto s = res::scan_rc(rc); - EXPECT_TRUE(s.versionInfoNamedByString); - EXPECT_EQ(s.versionInfoName, "VS_VERSION_INFO"); -} + expect(bool(s.versionInfoNamedByString)); + expect(eq(s.versionInfoName, "VS_VERSION_INFO")); +}}; // ─── Splitting a Windows environment list ───────────────────────────────── -TEST(BuildResources, EnvListSplitsOnSemicolonsOnly) { +UTest env_list_splits_on_semicolons_only{"BuildResources/EnvListSplitsOnSemicolonsOnly", [] { // The rc-tool search walks the toolchain's PATH override, which the MSVC // backend builds with `;` from real Windows paths. Splitting on ":" as well // cuts at the DRIVE COLON: `C:\...` becomes `C` plus a current-drive-relative @@ -381,33 +376,33 @@ TEST(BuildResources, EnvListSplitsOnSemicolonsOnly) { // became unfindable. auto v = res::split_env_list( R"(C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64;C:\VC\bin\Hostx64\x64)"); - ASSERT_EQ(v.size(), 2u); - EXPECT_EQ(v[0], R"(C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64)"); - EXPECT_EQ(v[1], R"(C:\VC\bin\Hostx64\x64)"); + expect(fatal(eq(v.size(), 2u))); + expect(eq(v[0], R"(C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64)")); + expect(eq(v[1], R"(C:\VC\bin\Hostx64\x64)")); // Empty entries (a trailing or doubled separator) are dropped rather than // becoming a probe of the current directory. auto e = res::split_env_list(R"(C:\a;;C:\b;)"); - ASSERT_EQ(e.size(), 2u); - EXPECT_EQ(e[0], R"(C:\a)"); - EXPECT_EQ(e[1], R"(C:\b)"); + expect(fatal(eq(e.size(), 2u))); + expect(eq(e[0], R"(C:\a)")); + expect(eq(e[1], R"(C:\b)")); - EXPECT_TRUE(res::split_env_list("").empty()); - EXPECT_EQ(res::split_env_list(R"(C:\only)").size(), 1u); -} + expect(res::split_env_list("").empty()); + expect(eq(res::split_env_list(R"(C:\only)").size(), 1u)); +}}; -TEST(BuildResources, ScanStaysQuietWhenTheMacroIsActuallyDefined) { +UTest scan_stays_quiet_when_the_macro_is_actually_defined{"BuildResources/ScanStaysQuietWhenTheMacroIsActuallyDefined", [] { TempDir d; // Either of these makes VS_VERSION_INFO real, so there is nothing to warn // about — the lint must not cry wolf at correct scripts. auto viaInclude = d.write("ok1.rc", "#include \n" "VS_VERSION_INFO VERSIONINFO\nBEGIN\nEND\n"); - EXPECT_FALSE(res::scan_rc(viaInclude).versionInfoNamedByString); + expect(!(res::scan_rc(viaInclude).versionInfoNamedByString)); auto viaDefine = d.write("ok2.rc", "#define VS_VERSION_INFO 1\n" "VS_VERSION_INFO VERSIONINFO\nBEGIN\nEND\n"); - EXPECT_FALSE(res::scan_rc(viaDefine).versionInfoNamedByString); + expect(!(res::scan_rc(viaDefine).versionInfoNamedByString)); auto literal = d.write("ok3.rc", "1 VERSIONINFO\nBEGIN\nEND\n"); - EXPECT_FALSE(res::scan_rc(literal).versionInfoNamedByString); -} + expect(!(res::scan_rc(literal).versionInfoNamedByString)); +}}; diff --git a/tests/unit/test_build_stage.cpp b/tests/unit/test_build_stage.cpp index 8823129e2..f8ba7d036 100644 --- a/tests/unit/test_build_stage.cpp +++ b/tests/unit/test_build_stage.cpp @@ -1,9 +1,11 @@ -#include import std; +import mcpplibs.mtest; import mcpp.source_kind; import mcpp.build.stage; +using namespace mcpplibs::mtest; + using namespace mcpp::build::stage; namespace { @@ -42,21 +44,21 @@ StageOptions no_retry(Verify v = Verify::Content) { } // namespace -TEST(BuildStage, CopiesWhenDestinationMissing) { +UTest copies_when_destination_missing{"BuildStage/CopiesWhenDestinationMissing", [] { Tmp tmp; auto src = tmp.path / "src.bin"; auto dst = tmp.path / "nested" / "dir" / "dst.bin"; write_file(src, "payload"); auto r = stage_file(src, dst, no_retry()); - ASSERT_TRUE(r.has_value()) << (r ? "" : r.error().message); - EXPECT_TRUE(r->copied); - EXPECT_EQ(read_file(dst), "payload"); -} + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << (r ? "" : r.error().message); + expect(bool(r->copied)); + expect(eq(read_file(dst), "payload")); +}}; // The core of #311: an already-equivalent destination must not be written at // all — that is what keeps a clangd-mapped std.pcm from failing the build. -TEST(BuildStage, EquivalentDestinationIsNotTouched) { +UTest equivalent_destination_is_not_touched{"BuildStage/EquivalentDestinationIsNotTouched", [] { Tmp tmp; auto src = tmp.path / "src.bin"; auto dst = tmp.path / "dst.bin"; @@ -69,17 +71,17 @@ TEST(BuildStage, EquivalentDestinationIsNotTouched) { auto recorded = std::filesystem::last_write_time(dst); auto r = stage_file(src, dst, no_retry()); - ASSERT_TRUE(r.has_value()); - EXPECT_FALSE(r->copied); + expect(fatal(r.has_value())); + expect(!r->copied); // Not "not bumped to now" — not changed at all. Any mtime change defeats // ninja's `restat = 1` and re-triggers the downstream rebuild cascade. - EXPECT_EQ(std::filesystem::last_write_time(dst), recorded); -} + expect(eq(std::filesystem::last_write_time(dst), recorded)); +}}; // Same size, different bytes must be copied by DEFAULT. Staging also carries // .dll payloads, where PE section padding makes equal sizes across a real // rebuild ordinary — so size-only equivalence cannot be the default. -TEST(BuildStage, SameSizeDifferentBytesIsCopiedByDefault) { +UTest same_size_different_bytes_is_copied_by_default{"BuildStage/SameSizeDifferentBytesIsCopiedByDefault", [] { Tmp tmp; auto src = tmp.path / "src.bin"; auto dst = tmp.path / "dst.bin"; @@ -87,15 +89,15 @@ TEST(BuildStage, SameSizeDifferentBytesIsCopiedByDefault) { write_file(dst, "BBBB"); auto r = stage_file(src, dst, no_retry()); - ASSERT_TRUE(r.has_value()); - EXPECT_TRUE(r->copied); - EXPECT_EQ(read_file(dst), "AAAA"); -} + expect(fatal(r.has_value())); + expect(bool(r->copied)); + expect(eq(read_file(dst), "AAAA")); +}}; // `--verify size` does not make a readable destination the source's: a // package's BMI is staged at a path named by the module, so after an upgrade // the previous version's BMI is there, often at the same size. -TEST(BuildStage, SameSizeDifferentBytesIsCopiedUnderSizeVerify) { +UTest same_size_different_bytes_is_copied_under_size_verify{"BuildStage/SameSizeDifferentBytesIsCopiedUnderSizeVerify", [] { Tmp tmp; auto src = tmp.path / "src.bin"; auto dst = tmp.path / "dst.bin"; @@ -103,15 +105,15 @@ TEST(BuildStage, SameSizeDifferentBytesIsCopiedUnderSizeVerify) { write_file(dst, "BBBB"); auto r = stage_file(src, dst, no_retry(Verify::Size)); - ASSERT_TRUE(r.has_value()); - EXPECT_TRUE(r->copied); - EXPECT_EQ(read_file(dst), "AAAA"); -} + expect(fatal(r.has_value())); + expect(bool(r->copied)); + expect(eq(read_file(dst), "AAAA")); +}}; // A destination that cannot be opened is taken as staged under `--verify size` // when its size is the source's: a holder that denies reading it denies // replacing it as well. -TEST(BuildStage, AnUnreadableSameSizeDestinationIsSkippedUnderSizeVerify) { +UTest an_unreadable_same_size_destination_is_skipped_under_size_verify{"BuildStage/AnUnreadableSameSizeDestinationIsSkippedUnderSizeVerify", [] { Tmp tmp; auto src = tmp.path / "src.bin"; auto dst = tmp.path / "dst.bin"; @@ -120,18 +122,18 @@ TEST(BuildStage, AnUnreadableSameSizeDestinationIsSkippedUnderSizeVerify) { std::filesystem::permissions(dst, std::filesystem::perms::none); if (std::ifstream(dst, std::ios::binary).is_open()) { std::filesystem::permissions(dst, std::filesystem::perms::owner_all); - GTEST_SKIP() << "permissions do not deny reading here"; + skip("permissions do not deny reading here"); } auto r = stage_file(src, dst, no_retry(Verify::Size)); auto content = r.has_value() ? std::string() : r.error().message; std::filesystem::permissions(dst, std::filesystem::perms::owner_all); - ASSERT_TRUE(r.has_value()) << content; - EXPECT_FALSE(r->copied); - EXPECT_EQ(read_file(dst), "BBBB"); -} + expect(fatal(r.has_value())) << content; + expect(!r->copied); + expect(eq(read_file(dst), "BBBB")); +}}; -TEST(BuildStage, DifferentSizeIsAlwaysCopied) { +UTest different_size_is_always_copied{"BuildStage/DifferentSizeIsAlwaysCopied", [] { Tmp tmp; auto src = tmp.path / "src.bin"; auto dst = tmp.path / "dst.bin"; @@ -139,66 +141,66 @@ TEST(BuildStage, DifferentSizeIsAlwaysCopied) { write_file(dst, "short"); auto r = stage_file(src, dst, no_retry()); - ASSERT_TRUE(r.has_value()); - EXPECT_TRUE(r->copied); - EXPECT_EQ(read_file(dst), "longer-payload"); -} + expect(fatal(r.has_value())); + expect(bool(r->copied)); + expect(eq(read_file(dst), "longer-payload")); +}}; // Workspace design 2026-09-29 §5.3: a shared library placed in two product // directories holds one copy of its bytes where the file system allows a link; // any other file is copied, so a program that writes a file beside itself // never writes through to its source. -TEST(BuildStage, ASharedLibraryIsPlacedByALinkAndADataFileByACopy) { +UTest a_shared_library_is_placed_by_a_link_and_a_data_file_by_a_copy{"BuildStage/ASharedLibraryIsPlacedByALinkAndADataFileByACopy", [] { Tmp tmp; auto lib = tmp.path / "store" / "libfoo.so.1"; auto data = tmp.path / "store" / "table.dat"; write_file(lib, "library"); write_file(data, "data"); for (auto const& dir : {"cli", "gui"}) { - ASSERT_TRUE(stage_file(lib, tmp.path / "bin" / dir / "libfoo.so.1", no_retry())); - ASSERT_TRUE(stage_file(data, tmp.path / "bin" / dir / "table.dat", no_retry())); + expect(fatal(bool(stage_file(lib, tmp.path / "bin" / dir / "libfoo.so.1", no_retry())))); + expect(fatal(bool(stage_file(data, tmp.path / "bin" / dir / "table.dat", no_retry())))); } std::error_code ec; const auto libLinks = std::filesystem::hard_link_count(lib, ec); - ASSERT_FALSE(ec); + expect(fatal(!ec)); // Three names on a file system with links (the source and two // placements), one where the link fell back to a copy. - EXPECT_TRUE(libLinks == 3 || libLinks == 1) << libLinks; + expect(libLinks == 3 || libLinks == 1) << libLinks; if (libLinks == 3) - EXPECT_TRUE(std::filesystem::equivalent(tmp.path / "bin" / "cli" / "libfoo.so.1", - tmp.path / "bin" / "gui" / "libfoo.so.1")); - EXPECT_EQ(std::filesystem::hard_link_count(data), 1u); - EXPECT_EQ(read_file(tmp.path / "bin" / "gui" / "table.dat"), "data"); -} + expect(bool(std::filesystem::equivalent(tmp.path / "bin" / "cli" / "libfoo.so.1", + tmp.path / "bin" / "gui" / "libfoo.so.1"))); + expect(eq(std::filesystem::hard_link_count(data), 1u)); + expect(eq(read_file(tmp.path / "bin" / "gui" / "table.dat"), "data")); +}}; // A linked placement is replaced out of place: a new source never rewrites the // bytes the earlier source shares with the placement. -TEST(BuildStage, ReplacingALinkedPlacementLeavesTheEarlierSourceIntact) { +UTest replacing_a_linked_placement_leaves_the_earlier_source_intact{"BuildStage/ReplacingALinkedPlacementLeavesTheEarlierSourceIntact", [] { Tmp tmp; auto first = tmp.path / "v1" / "foo.dll"; auto second = tmp.path / "v2" / "foo.dll"; auto dst = tmp.path / "bin" / "app" / "foo.dll"; write_file(first, "version one"); write_file(second, "version two!"); - ASSERT_TRUE(stage_file(first, dst, no_retry())); - ASSERT_TRUE(stage_file(second, dst, no_retry())); - EXPECT_EQ(read_file(dst), "version two!"); - EXPECT_EQ(read_file(first), "version one"); -} + expect(fatal(bool(stage_file(first, dst, no_retry())))); + expect(fatal(bool(stage_file(second, dst, no_retry())))); + expect(eq(read_file(dst), "version two!")); + expect(eq(read_file(first), "version one")); +}}; -TEST(BuildStage, MissingSourceIsAnError) { +UTest missing_source_is_an_error{"BuildStage/MissingSourceIsAnError", [] { Tmp tmp; auto r = stage_file(tmp.path / "nope.bin", tmp.path / "dst.bin", no_retry()); - ASSERT_FALSE(r.has_value()); - EXPECT_NE(r.error().message.find("nope.bin"), std::string::npos); -} + expect(fatal(!r.has_value())); + expect(ne(r.error().message.find("nope.bin"), std::string::npos)); +}}; // A read-only destination that must actually be replaced: POSIX can do it // (the directory entry is what gets rewritten, so temp-file + rename wins), // Windows cannot (replacing a read-only file is denied however you spell it) — // and there the contract is the loud failure, not a silent skip. Asserted per // platform rather than skipped, so neither side can rot unnoticed. -TEST(BuildStage, ReadOnlyDestinationOutcomeIsPlatformDefined) { +UTest read_only_destination_outcome_is_platform_defined{"BuildStage/ReadOnlyDestinationOutcomeIsPlatformDefined", [] { Tmp tmp; auto src = tmp.path / "src.bin"; auto dst = tmp.path / "dst.bin"; @@ -208,19 +210,19 @@ TEST(BuildStage, ReadOnlyDestinationOutcomeIsPlatformDefined) { auto r = stage_file(src, dst, no_retry()); #if defined(_WIN32) - ASSERT_FALSE(r.has_value()); - EXPECT_NE(r.error().message.find("help:"), std::string::npos); - EXPECT_EQ(read_file(dst), "old"); + expect(fatal(!r.has_value())); + expect(ne(r.error().message.find("help:"), std::string::npos)); + expect(eq(read_file(dst), "old")); #else - ASSERT_TRUE(r.has_value()) << r.error().message; - EXPECT_TRUE(r->copied); - EXPECT_EQ(read_file(dst), "new-content"); + expect(fatal(r.has_value())) << r.error().message; + expect(bool(r->copied)); + expect(eq(read_file(dst), "new-content")); #endif -} +}}; // When staging genuinely cannot proceed, the failure must name the file and // carry the actionable hint — never be downgraded to a warning. -TEST(BuildStage, UnwritableDestinationFailsWithActionableHint) { +UTest unwritable_destination_fails_with_actionable_hint{"BuildStage/UnwritableDestinationFailsWithActionableHint", [] { Tmp tmp; auto src = tmp.path / "src.bin"; write_file(src, "payload"); @@ -230,13 +232,13 @@ TEST(BuildStage, UnwritableDestinationFailsWithActionableHint) { write_file(dst / "child" / "keep.txt", "x"); auto r = stage_file(src, dst, no_retry()); - ASSERT_FALSE(r.has_value()); - EXPECT_NE(r.error().message.find("help:"), std::string::npos); - EXPECT_NE(r.error().message.find("clangd"), std::string::npos); - EXPECT_NE(r.error().message.find(dst.string()), std::string::npos); -} + expect(fatal(!r.has_value())); + expect(ne(r.error().message.find("help:"), std::string::npos)); + expect(ne(r.error().message.find("clangd"), std::string::npos)); + expect(ne(r.error().message.find(dst.string()), std::string::npos)); +}}; -TEST(BuildStage, SameContentComparesBytesNotJustSize) { +UTest same_content_compares_bytes_not_just_size{"BuildStage/SameContentComparesBytesNotJustSize", [] { Tmp tmp; auto a = tmp.path / "a.bin"; auto b = tmp.path / "b.bin"; @@ -245,22 +247,22 @@ TEST(BuildStage, SameContentComparesBytesNotJustSize) { write_file(b, std::string(300000, 'x')); write_file(c, std::string(300000, 'x').replace(299999, 1, "y")); - EXPECT_TRUE(same_content(a, b)); - EXPECT_FALSE(same_content(a, c)); - EXPECT_FALSE(same_content(a, tmp.path / "missing.bin")); -} + expect(bool(same_content(a, b))); + expect(!same_content(a, c)); + expect(!same_content(a, tmp.path / "missing.bin")); +}}; -TEST(BuildStage, VerifyModeParsing) { - EXPECT_EQ(parse_verify("content"), Verify::Content); - EXPECT_EQ(parse_verify("size"), Verify::Size); +UTest verify_mode_parsing{"BuildStage/VerifyModeParsing", [] { + expect(eq(parse_verify("content"), Verify::Content)); + expect(eq(parse_verify("size"), Verify::Size)); // Anything unrecognized must land on the SAFE mode, not the fast one. - EXPECT_EQ(parse_verify("nonsense"), Verify::Content); - EXPECT_EQ(parse_verify(""), Verify::Content); -} + expect(eq(parse_verify("nonsense"), Verify::Content)); + expect(eq(parse_verify(""), Verify::Content)); +}}; // ── SPEC-007 R4.2 (mcpp#723): a destination with more than one source ────── -TEST(BuildStageFiles, OneSourceIsIdenticalToStageFile) { +UTest one_source_is_identical_to_stage_file{"BuildStageFiles/OneSourceIsIdenticalToStageFile", [] { // The whole point of `stage_files`: a project with exactly one source per // destination -- every project before this feature -- must not notice a // difference. This is not asserted by string-matching a ninja line (that @@ -272,12 +274,12 @@ TEST(BuildStageFiles, OneSourceIsIdenticalToStageFile) { write_file(src, "payload"); auto r = stage_files({src}, dst, no_retry()); - ASSERT_TRUE(r.has_value()) << r.error().message; - EXPECT_TRUE(r->copied); - EXPECT_EQ(read_file(dst), "payload"); -} + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error().message; + expect(bool(r->copied)); + expect(eq(read_file(dst), "payload")); +}}; -TEST(BuildStageFiles, AgreeingSourcesArePlaced) { +UTest agreeing_sources_are_placed{"BuildStageFiles/AgreeingSourcesArePlaced", [] { Tmp tmp; auto a = tmp.path / "a.bin"; auto b = tmp.path / "b.bin"; @@ -288,12 +290,12 @@ TEST(BuildStageFiles, AgreeingSourcesArePlaced) { write_file(c, "shared bytes"); auto r = stage_files({a, b, c}, dst, no_retry()); - ASSERT_TRUE(r.has_value()) << r.error().message; - EXPECT_TRUE(r->copied); - EXPECT_EQ(read_file(dst), "shared bytes"); -} + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error().message; + expect(bool(r->copied)); + expect(eq(read_file(dst), "shared bytes")); +}}; -TEST(BuildStageFiles, AnAlreadyEquivalentDestinationIsNotTouched) { +UTest an_already_equivalent_destination_is_not_touched{"BuildStageFiles/AnAlreadyEquivalentDestinationIsNotTouched", [] { // The #311 property (BuildStage.EquivalentDestinationIsNotTouched) must // survive going through the multi-source entry point too. Tmp tmp; @@ -308,12 +310,12 @@ TEST(BuildStageFiles, AnAlreadyEquivalentDestinationIsNotTouched) { auto recorded = std::filesystem::last_write_time(dst); auto r = stage_files({a, b}, dst, no_retry()); - ASSERT_TRUE(r.has_value()); - EXPECT_FALSE(r->copied); - EXPECT_EQ(std::filesystem::last_write_time(dst), recorded); -} + expect(fatal(r.has_value())); + expect(!r->copied); + expect(eq(std::filesystem::last_write_time(dst), recorded)); +}}; -TEST(BuildStageFiles, DisagreeingSourcesFailNamingEveryOneAndTheDestination) { +UTest disagreeing_sources_fail_naming_every_one_and_the_destination{"BuildStageFiles/DisagreeingSourcesFailNamingEveryOneAndTheDestination", [] { Tmp tmp; auto a = tmp.path / "qtbase_zh_CN.qm"; auto b = tmp.path / "other" / "qtbase_zh_CN.qm"; @@ -322,19 +324,16 @@ TEST(BuildStageFiles, DisagreeingSourcesFailNamingEveryOneAndTheDestination) { write_file(b, "catalog B"); auto r = stage_files({a, b}, dst, no_retry()); - ASSERT_FALSE(r.has_value()); - EXPECT_NE(r.error().message.find(a.string()), std::string::npos) - << r.error().message; - EXPECT_NE(r.error().message.find(b.string()), std::string::npos) - << r.error().message; - EXPECT_NE(r.error().message.find(dst.string()), std::string::npos) - << r.error().message; + expect(fatal(!r.has_value())); + if (auto&& failed = expect(ne(r.error().message.find(a.string()), std::string::npos)); !failed) failed << r.error().message; + if (auto&& failed = expect(ne(r.error().message.find(b.string()), std::string::npos)); !failed) failed << r.error().message; + if (auto&& failed = expect(ne(r.error().message.find(dst.string()), std::string::npos)); !failed) failed << r.error().message; // Refused, not silently resolved to either source: the destination is // untouched (no partial write from a rejected attempt). - EXPECT_FALSE(std::filesystem::exists(dst)); -} + expect(!std::filesystem::exists(dst)); +}}; -TEST(BuildStageFiles, ThreeSourcesWhereOnlyTheLastDisagreesAreAllNamed) { +UTest three_sources_where_only_the_last_disagrees_are_all_named{"BuildStageFiles/ThreeSourcesWhereOnlyTheLastDisagreesAreAllNamed", [] { // A pairwise comparison against only the first neighbour would miss a // mismatch between the second and third; every source must be compared. Tmp tmp; @@ -347,12 +346,11 @@ TEST(BuildStageFiles, ThreeSourcesWhereOnlyTheLastDisagreesAreAllNamed) { write_file(c, "Y"); auto r = stage_files({a, b, c}, dst, no_retry()); - ASSERT_FALSE(r.has_value()); - EXPECT_NE(r.error().message.find(c.string()), std::string::npos) - << r.error().message; -} + expect(fatal(!r.has_value())); + if (auto&& failed = expect(ne(r.error().message.find(c.string()), std::string::npos)); !failed) failed << r.error().message; +}}; -TEST(BuildStageFiles, AMissingSourceIsNamedAsMissingNotAsADisagreement) { +UTest a_missing_source_is_named_as_missing_not_as_a_disagreement{"BuildStageFiles/AMissingSourceIsNamedAsMissingNotAsADisagreement", [] { Tmp tmp; auto a = tmp.path / "a.bin"; auto b = tmp.path / "never-written.bin"; @@ -360,15 +358,15 @@ TEST(BuildStageFiles, AMissingSourceIsNamedAsMissingNotAsADisagreement) { write_file(a, "X"); auto r = stage_files({a, b}, dst, no_retry()); - ASSERT_FALSE(r.has_value()); - EXPECT_NE(r.error().message.find("does not exist"), std::string::npos) << r.error().message; - EXPECT_NE(r.error().message.find(b.string()), std::string::npos) << r.error().message; - EXPECT_EQ(r.error().message.find("disagree"), std::string::npos) << r.error().message; - EXPECT_FALSE(std::filesystem::exists(dst)); -} - -TEST(BuildStageFiles, EmptySourceListIsAnError) { + expect(fatal(!r.has_value())); + if (auto&& failed = expect(ne(r.error().message.find("does not exist"), std::string::npos)); !failed) failed << r.error().message; + if (auto&& failed = expect(ne(r.error().message.find(b.string()), std::string::npos)); !failed) failed << r.error().message; + if (auto&& failed = expect(eq(r.error().message.find("disagree"), std::string::npos)); !failed) failed << r.error().message; + expect(!std::filesystem::exists(dst)); +}}; + +UTest empty_source_list_is_an_error{"BuildStageFiles/EmptySourceListIsAnError", [] { Tmp tmp; auto r = stage_files({}, tmp.path / "dst.bin", no_retry()); - ASSERT_FALSE(r.has_value()); -} + expect(fatal(!r.has_value())); +}}; diff --git a/tests/unit/test_c_standard_per_package.cpp b/tests/unit/test_c_standard_per_package.cpp index a8bc1f41e..6ff1607af 100644 --- a/tests/unit/test_c_standard_per_package.cpp +++ b/tests/unit/test_c_standard_per_package.cpp @@ -9,9 +9,9 @@ // per-unit C flags, which the compile edge, the scan edge and both build // databases read after the file-level flags. -#include import std; +import mcpplibs.mtest; import mcpp.build.plan; import mcpp.manifest; import mcpp.modgraph.graph; @@ -19,6 +19,8 @@ import mcpp.modgraph.scanner; import mcpp.source_kind; import mcpp.toolchain.model; +using namespace mcpplibs::mtest; + using namespace mcpp::build; namespace { @@ -143,7 +145,7 @@ Result plan_for(const Fixture& f) { auto plan = make_plan(app, f.tc, {}, graph, topo, packages, appRoot, appRoot / "target" / "t", {}, {}, {}); - EXPECT_TRUE(plan.has_value()) << (plan ? "" : plan.error()); + if (auto&& failed = expect(plan.has_value()); !failed) failed << (plan ? "" : plan.error()); Result r; if (!plan) return r; for (auto const& cu : plan->compileUnits) { @@ -166,48 +168,48 @@ bool has_std(const std::vector& v) { // The report's case: a dependency that declares `gnu11` compiles its C units at // `gnu11`, and not at the consumer's standard. -TEST(CStandardPerPackage, ADependencyCompilesAtItsOwnStandard) { +UTest a_dependency_compiles_at_its_own_standard{"CStandardPerPackage/ADependencyCompilesAtItsOwnStandard", [] { auto r = plan_for(Fixture{}); - ASSERT_FALSE(r.cflags["cdep.c"].empty()); - EXPECT_EQ(r.cflags["cdep.c"].back(), "-std=gnu11"); - EXPECT_EQ(r.cflags["more.c"].back(), "-std=gnu11"); -} + expect(fatal(!(r.cflags["cdep.c"].empty()))); + expect(eq(r.cflags["cdep.c"].back(), "-std=gnu11")); + expect(eq(r.cflags["more.c"].back(), "-std=gnu11")); +}}; // The consumer's standard reaches the consumer's own C units and no one else's. -TEST(CStandardPerPackage, TheRootsStandardStaysInTheRoot) { +UTest the_roots_standard_stays_in_the_root{"CStandardPerPackage/TheRootsStandardStaysInTheRoot", [] { Fixture f; f.rootStandard = "c99"; auto r = plan_for(f); - ASSERT_FALSE(r.cflags["app.c"].empty()); - EXPECT_EQ(r.cflags["app.c"].back(), "-std=c99"); - EXPECT_FALSE(has(r.cflags["plain.c"], "-std=c99")); - EXPECT_FALSE(has_std(r.cflags["plain.c"])) + expect(fatal(!(r.cflags["app.c"].empty()))); + expect(eq(r.cflags["app.c"].back(), "-std=c99")); + expect(!has(r.cflags["plain.c"], "-std=c99")); + expect(!has_std(r.cflags["plain.c"])) << "a package that declares nothing compiles at the file-level default"; - EXPECT_EQ(r.cflags["cdep.c"].back(), "-std=gnu11"); -} + expect(eq(r.cflags["cdep.c"].back(), "-std=gnu11")); +}}; // A declaration equal to the default adds nothing: the file-level line already // carries it, so the command line is unchanged. -TEST(CStandardPerPackage, DeclaringTheDefaultAddsNothing) { +UTest declaring_the_default_adds_nothing{"CStandardPerPackage/DeclaringTheDefaultAddsNothing", [] { Fixture f; f.depStandard = "c11"; auto r = plan_for(f); - EXPECT_FALSE(has_std(r.cflags["cdep.c"])); -} + expect(!has_std(r.cflags["cdep.c"])); +}}; // A C standard is a C setting: no C++ unit receives one. -TEST(CStandardPerPackage, NoCxxUnitReceivesACStandard) { +UTest no_cxx_unit_receives_ac_standard{"CStandardPerPackage/NoCxxUnitReceivesACStandard", [] { Fixture f; f.rootStandard = "c99"; auto r = plan_for(f); - EXPECT_FALSE(has_std(r.cxxflags["main.cpp"])); - EXPECT_FALSE(has_std(r.cflags["main.cpp"])); -} + expect(!has_std(r.cxxflags["main.cpp"])); + expect(!has_std(r.cflags["main.cpp"])); +}}; // cl.exe compiles C in its default mode and takes no C `/std:` from mcpp yet // (W3b). Nothing is added to a unit, and every declaring package that has C // units is recorded once, so the backend can say so in one line. -TEST(CStandardPerPackage, ClRecordsWhatItDoesNotApply) { +UTest cl_records_what_it_does_not_apply{"CStandardPerPackage/ClRecordsWhatItDoesNotApply", [] { Fixture f; f.tc = cl_like(); f.rootStandard = "c11"; @@ -215,29 +217,29 @@ TEST(CStandardPerPackage, ClRecordsWhatItDoesNotApply) { auto r = plan_for(f); for (auto const& [file, v] : r.cflags) for (auto const& flag : v) - EXPECT_FALSE(flag.starts_with("-std=") || flag.starts_with("/std:c")) + expect(!(flag.starts_with("-std=") || flag.starts_with("/std:c"))) << file << ": " << flag; - EXPECT_EQ(r.notApplied, (std::vector{"app (c11)", "cdep (gnu11)"})) + expect(eq(r.notApplied, (std::vector{"app (c11)", "cdep (gnu11)"}))) << "a package with no C units (quiet) is not reported"; -} +}}; // With no cl.exe in the build, nothing is recorded as unapplied. -TEST(CStandardPerPackage, GnuDriversApplyEverythingTheyAreGiven) { +UTest gnu_drivers_apply_everything_they_are_given{"CStandardPerPackage/GnuDriversApplyEverythingTheyAreGiven", [] { Fixture f; f.rootStandard = "c99"; auto r = plan_for(f); - EXPECT_TRUE(r.notApplied.empty()); -} + expect(r.notApplied.empty()); +}}; // A target's entry `main` written in C is a unit of the root package: it // compiles at the root's standard, as a scanned C unit does. -TEST(CStandardPerPackage, ACEntryMainTakesItsPackagesStandard) { +UTest ac_entry_main_takes_its_packages_standard{"CStandardPerPackage/ACEntryMainTakesItsPackagesStandard", [] { Fixture f; f.rootStandard = "c99"; f.cEntry = true; auto r = plan_for(f); - ASSERT_TRUE(r.cflags.contains("entry.c")) + expect(fatal(r.cflags.contains("entry.c"))) << "the plan has no unit for the entry main"; - ASSERT_FALSE(r.cflags["entry.c"].empty()); - EXPECT_EQ(r.cflags["entry.c"].back(), "-std=c99"); -} + expect(fatal(!(r.cflags["entry.c"].empty()))); + expect(eq(r.cflags["entry.c"].back(), "-std=c99")); +}}; diff --git a/tests/unit/test_cache_key.cpp b/tests/unit/test_cache_key.cpp index 4044e91ab..30c2ca567 100644 --- a/tests/unit/test_cache_key.cpp +++ b/tests/unit/test_cache_key.cpp @@ -1,12 +1,14 @@ -#include import std; +import mcpplibs.mtest; import mcpp.build.cache_key; import nlohmann.json; import mcpp.manifest; import mcpp.modgraph.scanner; import mcpp.toolchain.detect; +using namespace mcpplibs::mtest; + namespace ck = mcpp::build::cache_key; namespace { @@ -55,45 +57,45 @@ mcpp::modgraph::PackageRoot rootAt(const std::filesystem::path& at) { // an entry. Axes A–F contain nothing about the consumer, so there is no input // this test could vary to demonstrate the old behaviour — the closest positive // statement is that a dependency's key is a pure function of its own axes. -TEST(CacheKey, IsStableAcrossCallsForTheSameInputs) { - EXPECT_EQ(ck::key_hex(axes(), pkg()), ck::key_hex(axes(), pkg())); -} +UTest is_stable_across_calls_for_the_same_inputs{"CacheKey/IsStableAcrossCallsForTheSameInputs", [] { + expect(eq(ck::key_hex(axes(), pkg()), ck::key_hex(axes(), pkg()))); +}}; -TEST(CacheKey, KeyIsSixteenHexChars) { +UTest key_is_sixteen_hex_chars{"CacheKey/KeyIsSixteenHexChars", [] { auto k = ck::key_hex(axes(), pkg()); - EXPECT_EQ(k.size(), 16u); + expect(eq(k.size(), 16u)); for (char c : k) - EXPECT_TRUE(std::isxdigit(static_cast(c))) << k; -} + expect(bool(std::isxdigit(static_cast(c)))) << k; +}}; // ── C axis: the profile. This is the one that was missing entirely, and the // reason a --release build could be served -O0 -g dependency objects. ──────── -TEST(CacheKey, ProfileOptLevelChangesTheKey) { +UTest profile_opt_level_changes_the_key{"CacheKey/ProfileOptLevelChangesTheKey", [] { auto a = axes(); auto b = axes(); b.optLevel = "0"; - EXPECT_NE(ck::key_hex(a, pkg()), ck::key_hex(b, pkg())); -} + expect(ne(ck::key_hex(a, pkg()), ck::key_hex(b, pkg()))); +}}; -TEST(CacheKey, ProfileDebugLtoStripEachChangeTheKey) { +UTest profile_debug_lto_strip_each_change_the_key{"CacheKey/ProfileDebugLtoStripEachChangeTheKey", [] { auto base = ck::key_hex(axes(), pkg()); - { auto b = axes(); b.debug = true; EXPECT_NE(ck::key_hex(b, pkg()), base); } - { auto b = axes(); b.lto = true; EXPECT_NE(ck::key_hex(b, pkg()), base); } - { auto b = axes(); b.strip = true; EXPECT_NE(ck::key_hex(b, pkg()), base); } -} + { auto b = axes(); b.debug = true; expect(ne(ck::key_hex(b, pkg()), base)); } + { auto b = axes(); b.lto = true; expect(ne(ck::key_hex(b, pkg()), base)); } + { auto b = axes(); b.strip = true; expect(ne(ck::key_hex(b, pkg()), base)); } +}}; // ── A axis: toolchain identity ─────────────────────────────────────────────── -TEST(CacheKey, ToolchainIdentityChangesTheKey) { +UTest toolchain_identity_changes_the_key{"CacheKey/ToolchainIdentityChangesTheKey", [] { auto base = ck::key_hex(axes(), pkg()); - { auto b = axes(); b.compilerId = "clang"; EXPECT_NE(ck::key_hex(b, pkg()), base); } - { auto b = axes(); b.compilerVersion = "15.1.0"; EXPECT_NE(ck::key_hex(b, pkg()), base); } + { auto b = axes(); b.compilerId = "clang"; expect(ne(ck::key_hex(b, pkg()), base)); } + { auto b = axes(); b.compilerVersion = "15.1.0"; expect(ne(ck::key_hex(b, pkg()), base)); } { auto b = axes(); b.targetTriple = "x86_64-linux-musl"; - EXPECT_NE(ck::key_hex(b, pkg()), base); } - { auto b = axes(); b.stdlibId = "libc++"; EXPECT_NE(ck::key_hex(b, pkg()), base); } + expect(ne(ck::key_hex(b, pkg()), base)); } + { auto b = axes(); b.stdlibId = "libc++"; expect(ne(ck::key_hex(b, pkg()), base)); } // Two payloads that both call themselves "gcc 16.1.0" but are not the same // build must not share an entry. { auto b = axes(); b.driverIdentity = "ffffffffffffffff"; - EXPECT_NE(ck::key_hex(b, pkg()), base); } -} + expect(ne(ck::key_hex(b, pkg()), base)); } +}}; // The axis that exists because an UPGRADE broke a build. // @@ -107,135 +109,135 @@ TEST(CacheKey, ToolchainIdentityChangesTheKey) { // 'mcpplibs.riscv_virt_rt.pcm' but is currently disabled // // Measured on exactly that upgrade, on a project that had built once before. -TEST(CacheKey, TargetImpliedFlagsChangeTheKey) { +UTest target_implied_flags_change_the_key{"CacheKey/TargetImpliedFlagsChangeTheKey", [] { auto base = ck::key_hex(axes(), pkg()); { auto b = axes(); b.targetImpliedFlags = {"-ffreestanding"}; - EXPECT_NE(ck::key_hex(b, pkg()), base); } + expect(ne(ck::key_hex(b, pkg()), base)); } // Adding one flag to an existing set must move it too — the upgrade case // is never "empty becomes non-empty", it is "one more flag than before". auto before = axes(); before.targetImpliedFlags = {"-ffreestanding", "-nostdinc++"}; auto after = axes(); after.targetImpliedFlags = {"-ffreestanding", "-nostdinc++", "-fno-exceptions"}; - EXPECT_NE(ck::key_hex(after, pkg()), ck::key_hex(before, pkg())); -} + expect(ne(ck::key_hex(after, pkg()), ck::key_hex(before, pkg()))); +}}; // ── B axis: language and dialect ───────────────────────────────────────────── -TEST(CacheKey, LanguageAndDialectChangeTheKey) { +UTest language_and_dialect_change_the_key{"CacheKey/LanguageAndDialectChangeTheKey", [] { auto base = ck::key_hex(axes(), pkg()); - { auto b = axes(); b.cppStandard = "c++26"; EXPECT_NE(ck::key_hex(b, pkg()), base); } + { auto b = axes(); b.cppStandard = "c++26"; expect(ne(ck::key_hex(b, pkg()), base)); } { auto b = axes(); b.cppStandardFlag = "-std=c++2c"; - EXPECT_NE(ck::key_hex(b, pkg()), base); } + expect(ne(ck::key_hex(b, pkg()), base)); } // A dependency built at c++20 must never be handed to a c++23 graph: BMIs // are not compatible across levels (GCC: "language dialect differs"). { auto b = axes(); b.cppStandard = "c++20"; - b.cppStandardFlag = "-std=c++20"; EXPECT_NE(ck::key_hex(b, pkg()), base); } + b.cppStandardFlag = "-std=c++20"; expect(ne(ck::key_hex(b, pkg()), base)); } { auto b = axes(); b.dialectFlags = {"-freflection"}; - EXPECT_NE(ck::key_hex(b, pkg()), base); } - { auto b = axes(); b.cStandard = "c17"; EXPECT_NE(ck::key_hex(b, pkg()), base); } + expect(ne(ck::key_hex(b, pkg()), base)); } + { auto b = axes(); b.cStandard = "c17"; expect(ne(ck::key_hex(b, pkg()), base)); } // ONE SLOT, BOTH PLATFORMS. It was `macosDeploymentTarget`; Android's // minimum API level is the same quantity and is in this key for the same // reason -- it selects which bionic symbols are visible, so two levels are // two ABIs. A target is either Apple or Android, so one slot cannot be // asked to hold both at once. { auto b = axes(); b.minPlatformVersion = "14.0"; - EXPECT_NE(ck::key_hex(b, pkg()), base); } + expect(ne(ck::key_hex(b, pkg()), base)); } { auto b = axes(); b.minPlatformVersion = "24"; - EXPECT_NE(ck::key_hex(b, pkg()), base); } + expect(ne(ck::key_hex(b, pkg()), base)); } // AND TWO LEVELS ARE TWO KEYS, which is the property the Android row // depends on: without it `min_api_level = 21` and `= 24` would share a // build directory and the second build would serve the first's objects. { auto b21 = axes(); b21.minPlatformVersion = "21"; auto b24 = axes(); b24.minPlatformVersion = "24"; - EXPECT_NE(ck::key_hex(b21, pkg()), ck::key_hex(b24, pkg())); } -} + expect(ne(ck::key_hex(b21, pkg()), ck::key_hex(b24, pkg()))); } +}}; // ── D axis: package identity ───────────────────────────────────────────────── -TEST(CacheKey, PackageIdentityChangesTheKey) { +UTest package_identity_changes_the_key{"CacheKey/PackageIdentityChangesTheKey", [] { auto base = ck::key_hex(axes(), pkg()); - { auto p = pkg(); p.version = "1.3.1"; EXPECT_NE(ck::key_hex(axes(), p), base); } + { auto p = pkg(); p.version = "1.3.1"; expect(ne(ck::key_hex(axes(), p), base)); } { auto p = pkg(); p.packageName = "compat.zstd"; - EXPECT_NE(ck::key_hex(axes(), p), base); } - { auto p = pkg(); p.indexName = "other"; EXPECT_NE(ck::key_hex(axes(), p), base); } -} + expect(ne(ck::key_hex(axes(), p), base)); } + { auto p = pkg(); p.indexName = "other"; expect(ne(ck::key_hex(axes(), p), base)); } +}}; // ── E axis: the package's own config ───────────────────────────────────────── -TEST(CacheKey, OwnBuildConfigChangesTheKey) { +UTest own_build_config_changes_the_key{"CacheKey/OwnBuildConfigChangesTheKey", [] { auto base = ck::key_hex(axes(), pkg()); - { auto p = pkg(); p.cflags.push_back("-DEXTRA"); EXPECT_NE(ck::key_hex(axes(), p), base); } - { auto p = pkg(); p.cxxflags = {"-fno-rtti"}; EXPECT_NE(ck::key_hex(axes(), p), base); } - { auto p = pkg(); p.defines = {"FOO=1"}; EXPECT_NE(ck::key_hex(axes(), p), base); } - { auto p = pkg(); p.globFlags = {"glob:*.c"}; EXPECT_NE(ck::key_hex(axes(), p), base); } - { auto p = pkg(); p.generatedFiles = {"cfg.h=1"};EXPECT_NE(ck::key_hex(axes(), p), base); } + { auto p = pkg(); p.cflags.push_back("-DEXTRA"); expect(ne(ck::key_hex(axes(), p), base)); } + { auto p = pkg(); p.cxxflags = {"-fno-rtti"}; expect(ne(ck::key_hex(axes(), p), base)); } + { auto p = pkg(); p.defines = {"FOO=1"}; expect(ne(ck::key_hex(axes(), p), base)); } + { auto p = pkg(); p.globFlags = {"glob:*.c"}; expect(ne(ck::key_hex(axes(), p), base)); } + { auto p = pkg(); p.generatedFiles = {"cfg.h=1"};expect(ne(ck::key_hex(axes(), p), base)); } { auto p = pkg(); p.includeDirs = {"pub:/x"}; - EXPECT_NE(ck::key_hex(axes(), p), base); } + expect(ne(ck::key_hex(axes(), p), base)); } { auto p = pkg(); p.sources.push_back("zlib-1.3.2/gzread.c"); - EXPECT_NE(ck::key_hex(axes(), p), base); } - { auto p = pkg(); p.features = {"main"}; EXPECT_NE(ck::key_hex(axes(), p), base); } -} + expect(ne(ck::key_hex(axes(), p), base)); } + { auto p = pkg(); p.features = {"main"}; expect(ne(ck::key_hex(axes(), p), base)); } +}}; // ── F axis: upstream keys (Merkle) ─────────────────────────────────────────── // A dependency's artifacts are bound to the exact upstream artifacts they were // built against — GCC embeds a CRC of an imported module's BMI into the // importer's BMI, so a stale importer BMI fails with `module 'X' CRC mismatch`. // The key therefore folds in the upstream's key, not a summary of its interface. -TEST(CacheKey, UpstreamKeyChangesTheKey) { +UTest upstream_key_changes_the_key{"CacheKey/UpstreamKeyChangesTheKey", [] { auto base = ck::key_hex(axes(), pkg()); auto p = pkg(); p.upstreamKeys = {"1111111111111111"}; auto withUp = ck::key_hex(axes(), p); - EXPECT_NE(withUp, base); + expect(ne(withUp, base)); p.upstreamKeys = {"2222222222222222"}; - EXPECT_NE(ck::key_hex(axes(), p), withUp); -} + expect(ne(ck::key_hex(axes(), p), withUp)); +}}; -TEST(CacheKey, UpstreamOrderDoesNotMatterWhenSorted) { +UTest upstream_order_does_not_matter_when_sorted{"CacheKey/UpstreamOrderDoesNotMatterWhenSorted", [] { auto p1 = pkg(); p1.upstreamKeys = {"1111111111111111", "2222222222222222"}; auto p2 = pkg(); p2.upstreamKeys = {"1111111111111111", "2222222222222222"}; - EXPECT_EQ(ck::key_hex(axes(), p1), ck::key_hex(axes(), p2)); -} + expect(eq(ck::key_hex(axes(), p1), ck::key_hex(axes(), p2))); +}}; // Length-prefixed field joining: without it, ("a","bc") and ("ab","c") collide, // and for a cache key a collision means serving one package's objects for // another's. -TEST(CacheKey, AdjacentFieldsCannotCollide) { +UTest adjacent_fields_cannot_collide{"CacheKey/AdjacentFieldsCannotCollide", [] { auto p1 = pkg(); p1.packageName = "a"; p1.version = "bc"; auto p2 = pkg(); p2.packageName = "ab"; p2.version = "c"; - EXPECT_NE(ck::key_hex(axes(), p1), ck::key_hex(axes(), p2)); -} + expect(ne(ck::key_hex(axes(), p1), ck::key_hex(axes(), p2))); +}}; -TEST(CacheKey, ListBoundariesCannotCollide) { +UTest list_boundaries_cannot_collide{"CacheKey/ListBoundariesCannotCollide", [] { auto p1 = pkg(); p1.cflags = {"-DA", "-DB"}; auto p2 = pkg(); p2.cflags = {"-DA-DB"}; - EXPECT_NE(ck::key_hex(axes(), p1), ck::key_hex(axes(), p2)); -} + expect(ne(ck::key_hex(axes(), p1), ck::key_hex(axes(), p2))); +}}; // entry.json carries these inputs verbatim so a hit can be validated field by // field. Anything in the key must therefore also be in the JSON, or validation // would pass on an input the key distinguishes. -TEST(CacheKey, JsonCarriesEveryAxis) { +UTest json_carries_every_axis{"CacheKey/JsonCarriesEveryAxis", [] { auto j = ck::to_json(axes(), pkg()); - EXPECT_EQ(j["epoch"], ck::kCacheEpoch); + expect(eq(j["epoch"], ck::kCacheEpoch)); for (auto k : {"toolchain", "language", "profile", "package", "config", "upstream"}) - EXPECT_TRUE(j.contains(k)) << k; - EXPECT_EQ(j["toolchain"]["compiler"], "gcc"); - EXPECT_EQ(j["profile"]["opt_level"], "2"); - EXPECT_EQ(j["package"]["version"], "1.3.2"); + expect(j.contains(k)) << k; + expect(eq(j["toolchain"]["compiler"], "gcc")); + expect(eq(j["profile"]["opt_level"], "2")); + expect(eq(j["package"]["version"], "1.3.2")); for (auto k : {"features", "cflags", "cxxflags", "ldflags", "defines", "glob_flags", "generated_files", "include_dirs", "source_globs", "sources"}) - EXPECT_TRUE(j["config"].contains(k)) << k; -} + expect(j["config"].contains(k)) << k; +}}; -TEST(CacheKey, JsonDiffersWheneverTheKeyDiffers) { +UTest json_differs_whenever_the_key_differs{"CacheKey/JsonDiffersWheneverTheKeyDiffers", [] { auto b = axes(); b.optLevel = "0"; - EXPECT_NE(ck::to_json(axes(), pkg()), ck::to_json(b, pkg())); -} + expect(ne(ck::to_json(axes(), pkg()), ck::to_json(b, pkg()))); +}}; // Payload include dirs are absolute paths under /registry/data/xpkgs. // Left absolute, every entry would miss after a home relocation or on another // machine — which is most of the point of a cross-project cache. -TEST(CacheKey, IncludeDirsAreRelativizedAgainstTheStoreRoot) { +UTest include_dirs_are_relativized_against_the_store_root{"CacheKey/IncludeDirsAreRelativizedAgainstTheStoreRoot", [] { auto makeKeyFor = [](const std::filesystem::path& store) { auto pkgRoot = rootAt(store / "compat-x-compat.zlib" / "1.3.2"); pkgRoot.manifest.buildConfig.includeDirs = { @@ -248,26 +250,26 @@ TEST(CacheKey, IncludeDirsAreRelativizedAgainstTheStoreRoot) { }; auto [k1, dirs1] = makeKeyFor("/home/alice/.mcpp/registry/data/xpkgs"); auto [k2, dirs2] = makeKeyFor("/opt/ci/mcpp/registry/data/xpkgs"); - EXPECT_EQ(k1, k2); - ASSERT_FALSE(dirs1.empty()); - EXPECT_NE(dirs1.front().find(""), std::string::npos) << dirs1.front(); - EXPECT_EQ(dirs1, dirs2); -} + expect(eq(k1, k2)); + expect(fatal(!dirs1.empty())); + expect(ne(dirs1.front().find(""), std::string::npos)) << dirs1.front(); + expect(eq(dirs1, dirs2)); +}}; // A path that is under the package root but not under the store (a generated // directory, say) gets a package-relative placeholder for the same reason. -TEST(CacheKey, PackageRootRelativeDirsAreAlsoRelativized) { +UTest package_root_relative_dirs_are_also_relativized{"CacheKey/PackageRootRelativeDirsAreAlsoRelativized", [] { std::filesystem::path store = "/home/u/.mcpp/registry/data/xpkgs"; auto pkgRoot = rootAt("/somewhere/else/zlib"); pkgRoot.manifest.buildConfig.includeDirs = {"/somewhere/else/zlib/generated"}; ck::PackageAxes p; ck::fill_package_config(p, pkgRoot, store); - ASSERT_FALSE(p.includeDirs.empty()); - EXPECT_NE(p.includeDirs.front().find(""), std::string::npos) + expect(fatal(!p.includeDirs.empty())); + expect(ne(p.includeDirs.front().find(""), std::string::npos)) << p.includeDirs.front(); -} +}}; -TEST(CacheKey, FillPackageConfigCarriesFlagsAndGeneratedFiles) { +UTest fill_package_config_carries_flags_and_generated_files{"CacheKey/FillPackageConfigCarriesFlagsAndGeneratedFiles", [] { std::filesystem::path store = "/home/u/.mcpp/registry/data/xpkgs"; auto pkgRoot = rootAt(store / "compat-x-compat.zlib" / "1.3.2"); pkgRoot.manifest.buildConfig.cflags = {"-D_GNU_SOURCE"}; @@ -278,22 +280,22 @@ TEST(CacheKey, FillPackageConfigCarriesFlagsAndGeneratedFiles) { ck::PackageAxes p; ck::fill_package_config(p, pkgRoot, store); - EXPECT_NE(std::ranges::find(p.cflags, "-D_GNU_SOURCE"), p.cflags.end()); - EXPECT_NE(std::ranges::find(p.cxxflags, "-fno-exceptions"), p.cxxflags.end()); - EXPECT_NE(std::ranges::find(p.defines, "ZLIB_CONST"), p.defines.end()); - ASSERT_EQ(p.generatedFiles.size(), 1u); - EXPECT_EQ(p.generatedFiles.front(), "cfg.h=#define A 1"); + expect(ne(std::ranges::find(p.cflags, "-D_GNU_SOURCE"), p.cflags.end())); + expect(ne(std::ranges::find(p.cxxflags, "-fno-exceptions"), p.cxxflags.end())); + expect(ne(std::ranges::find(p.defines, "ZLIB_CONST"), p.defines.end())); + expect(fatal(eq(p.generatedFiles.size(), 1u))); + expect(eq(p.generatedFiles.front(), "cfg.h=#define A 1")); // A package's own C standard reaches its own C units, so it must be in the // key; the B axis carries only the engine default (#695). bool sawCStd = false; for (auto& f : p.cflags) if (f.find("c_standard=gnu11") != std::string::npos) sawCStd = true; - EXPECT_TRUE(sawCStd); -} + expect(bool(sawCStd)); +}}; // Declaring the default is declaring nothing: the unit's command is the same, // so the key is the same, and a package that spells `c11` shares its cached // objects with one that leaves the key out (#695). -TEST(CacheKey, DeclaringTheDefaultCStandardKeysNothing) { +UTest declaring_the_default_c_standard_keys_nothing{"CacheKey/DeclaringTheDefaultCStandardKeysNothing", [] { std::filesystem::path store = "/home/u/.mcpp/registry/data/xpkgs"; auto declared = rootAt(store / "compat-x-compat.zlib" / "1.3.2"); declared.manifest.buildConfig.cStandard = "c11"; @@ -302,12 +304,12 @@ TEST(CacheKey, DeclaringTheDefaultCStandardKeysNothing) { ck::PackageAxes a, b; ck::fill_package_config(a, declared, store); ck::fill_package_config(b, silent, store); - EXPECT_EQ(a.cflags, b.cflags); - for (auto& f : a.cflags) EXPECT_EQ(f.find("c_standard="), std::string::npos) << f; -} + expect(eq(a.cflags, b.cflags)); + for (auto& f : a.cflags) expect(eq(f.find("c_standard="), std::string::npos)) << f; +}}; // Generated files are a map; iteration order must not leak into the key. -TEST(CacheKey, GeneratedFilesAreOrderIndependent) { +UTest generated_files_are_order_independent{"CacheKey/GeneratedFilesAreOrderIndependent", [] { std::filesystem::path store = "/home/u/.mcpp/registry/data/xpkgs"; auto build = [&](std::vector> entries) { auto pkgRoot = rootAt(store / "p" / "1"); @@ -317,9 +319,8 @@ TEST(CacheKey, GeneratedFilesAreOrderIndependent) { ck::fill_package_config(p, pkgRoot, store); return ck::key_hex(axes(), p); }; - EXPECT_EQ(build({{"a.h", "1"}, {"b.h", "2"}}), - build({{"b.h", "2"}, {"a.h", "1"}})); -} + expect(eq(build({{"a.h", "1"}, {"b.h", "2"}}), build({{"b.h", "2"}, {"a.h", "1"}}))); +}}; // ── The key must describe the compilation that will happen ────────────────── // @@ -359,43 +360,42 @@ mcpp::toolchain::Toolchain freestanding_tc(bool cxxRuntimeFromGraph) { } } -TEST(CacheKey, AGraphSuppliedCxxRuntimeChangesTheFreestandingFlags) { +UTest a_graph_supplied_cxx_runtime_changes_the_freestanding_flags{"CacheKey/AGraphSuppliedCxxRuntimeChangesTheFreestandingFlags", [] { mcpp::manifest::Manifest m; m.package.standard = "c++23"; const auto without = ck::build_axes(freestanding_tc(false), m, "-std=c++23", {}, ""); const auto with = ck::build_axes(freestanding_tc(true), m, "-std=c++23", {}, ""); // The flags themselves differ — this is the fact the key has to carry. - EXPECT_NE(without.targetImpliedFlags, with.targetImpliedFlags); + expect(ne(without.targetImpliedFlags, with.targetImpliedFlags)); // And the one that has no C++ runtime is the one that gets the pair. const auto has = [](const std::vector& v, std::string_view f) { return std::ranges::find(v, f) != v.end(); }; - EXPECT_TRUE(has(without.targetImpliedFlags, "-fno-exceptions")); - EXPECT_FALSE(has(with.targetImpliedFlags, "-fno-exceptions")); -} + expect(bool(has(without.targetImpliedFlags, "-fno-exceptions"))); + expect(!has(with.targetImpliedFlags, "-fno-exceptions")); +}}; -TEST(CacheKey, TheTwoFreestandingConfigurationsDoNotShareASlot) { +UTest the_two_freestanding_configurations_do_not_share_a_slot{"CacheKey/TheTwoFreestandingConfigurationsDoNotShareASlot", [] { mcpp::manifest::Manifest m; m.package.standard = "c++23"; - EXPECT_NE(ck::key_hex(ck::build_axes(freestanding_tc(false), m, "-std=c++23", {}, ""), pkg()), - ck::key_hex(ck::build_axes(freestanding_tc(true), m, "-std=c++23", {}, ""), pkg())); -} + expect(ne(ck::key_hex(ck::build_axes(freestanding_tc(false), m, "-std=c++23", {}, ""), pkg()), ck::key_hex(ck::build_axes(freestanding_tc(true), m, "-std=c++23", {}, ""), pkg()))); +}}; // AND A HOSTED TARGET IS UNAFFECTED, so the fix cannot be read as "the key // now changes with something it should not". `freestanding::resolve` returns // nothing for a hosted triple, and the flags stay empty either way. -TEST(CacheKey, AHostedTargetHasNoTargetImpliedFlagsEitherWay) { +UTest a_hosted_target_has_no_target_implied_flags_either_way{"CacheKey/AHostedTargetHasNoTargetImpliedFlagsEitherWay", [] { mcpp::manifest::Manifest m; m.package.standard = "c++23"; auto tc = freestanding_tc(false); tc.targetTriple = "x86_64-linux-gnu"; auto a = ck::build_axes(tc, m, "-std=c++23", {}, ""); tc.targetCxxRuntime = true; auto b = ck::build_axes(tc, m, "-std=c++23", {}, ""); - EXPECT_TRUE(a.targetImpliedFlags.empty()) << a.targetImpliedFlags.size(); - EXPECT_EQ(a.targetImpliedFlags, b.targetImpliedFlags); -} + expect(a.targetImpliedFlags.empty()) << a.targetImpliedFlags.size(); + expect(eq(a.targetImpliedFlags, b.targetImpliedFlags)); +}}; // THE SAME CLASS OF DEFECT AS THE FREESTANDING ONE ABOVE, FOUND THE SAME WAY // — A CONFIG BIT THAT REACHES THE COMPILE BUT NOT THE KEY. @@ -419,7 +419,7 @@ TEST(CacheKey, AHostedTargetHasNoTargetImpliedFlagsEitherWay) { // exists to protect. `--cache=off`, or clearing the cache directory, // produced the correct numbers reproducibly — because both routes bypass // this key entirely, not because the key was telling the two apart. -TEST(CacheKey, TwoDifferentRealisedCEnvironmentsDoNotShareASlot) { +UTest two_different_realised_c_environments_do_not_share_a_slot{"CacheKey/TwoDifferentRealisedCEnvironmentsDoNotShareASlot", [] { std::filesystem::path store = "/home/u/.mcpp/registry/data/xpkgs"; auto build = [&](std::vector cEnvTokens) { auto pkgRoot = rootAt(store / "musl-x-openkal-musl" / "1.0.0"); @@ -441,8 +441,8 @@ TEST(CacheKey, TwoDifferentRealisedCEnvironmentsDoNotShareASlot) { }; auto lp64Key = build({"--target=x86_64-pc-cygwin", "-fno-short-wchar"}); auto llp64Key = build({}); - EXPECT_NE(lp64Key, llp64Key); -} + expect(ne(lp64Key, llp64Key)); +}}; // A DURABLE GUARD FOR THE WHOLE CLASS THE PREVIOUS TEST FOUND ONE INSTANCE // OF — this predates the c-abi wave (`targetSideUsage`'s own broadcast, and @@ -472,7 +472,7 @@ TEST(CacheKey, TwoDifferentRealisedCEnvironmentsDoNotShareASlot) { // day something does broadcast into either, this pair of assertions must // flip (start reading it) at the same time the broadcast is added, not // after. -TEST(CacheKey, EveryPrivateBuildBroadcastFieldReachesTheKey) { +UTest every_private_build_broadcast_field_reaches_the_key{"CacheKey/EveryPrivateBuildBroadcastFieldReachesTheKey", [] { std::filesystem::path store = "/home/u/.mcpp/registry/data/xpkgs"; auto keyFor = [&](auto mutate) { auto pkgRoot = rootAt(store / "p" / "1"); @@ -485,34 +485,34 @@ TEST(CacheKey, EveryPrivateBuildBroadcastFieldReachesTheKey) { }; const auto baseline = keyFor([](auto&) {}); - EXPECT_NE(baseline, keyFor([](auto& r) { + expect(ne(baseline, keyFor([](auto& r) { r.privateBuild.includeDirs = {"/somewhere/broadcast-only-include"}; - })) << "privateBuild.includeDirs"; - EXPECT_NE(baseline, keyFor([](auto& r) { + }))) << "privateBuild.includeDirs"; + expect(ne(baseline, keyFor([](auto& r) { r.privateBuild.includeDirsAfter = {"/somewhere/broadcast-only-after"}; - })) << "privateBuild.includeDirsAfter"; - EXPECT_NE(baseline, keyFor([](auto& r) { + }))) << "privateBuild.includeDirsAfter"; + expect(ne(baseline, keyFor([](auto& r) { r.privateBuild.cflags = {"-Dbroadcast_only_c"}; - })) << "privateBuild.cflags"; - EXPECT_NE(baseline, keyFor([](auto& r) { + }))) << "privateBuild.cflags"; + expect(ne(baseline, keyFor([](auto& r) { r.privateBuild.cxxflags = {"-Dbroadcast_only_cxx"}; - })) << "privateBuild.cxxflags"; - EXPECT_NE(baseline, keyFor([](auto& r) { + }))) << "privateBuild.cxxflags"; + expect(ne(baseline, keyFor([](auto& r) { r.privateBuild.asmflags = {"-Dbroadcast_only_asm"}; - })) << "privateBuild.asmflags"; + }))) << "privateBuild.asmflags"; // Documented gap, not an oversight — see the comment above. - EXPECT_EQ(baseline, keyFor([](auto& r) { + expect(eq(baseline, keyFor([](auto& r) { r.privateBuild.ldflags = {"-Wl,--broadcast-only"}; - })) << "privateBuild.ldflags is not yet a live broadcast channel; if " + }))) << "privateBuild.ldflags is not yet a live broadcast channel; if " "this starts failing, something now writes it, and " "fill_package_config must be taught to read it in the SAME change " "that flips this expectation"; - EXPECT_EQ(baseline, keyFor([](auto& r) { + expect(eq(baseline, keyFor([](auto& r) { r.privateBuild.modules = {"broadcast.only.module"}; - })) << "privateBuild.modules is not yet a live broadcast channel; see " + }))) << "privateBuild.modules is not yet a live broadcast channel; see " "the ldflags case above for what to do when it becomes one"; -} +}}; // THE HEADER SET THE DRIVER IS POINTED AT IS PART OF THE IDENTITY. // @@ -526,7 +526,7 @@ TEST(CacheKey, EveryPrivateBuildBroadcastFieldReachesTheKey) { // Measured (mcpp#514 §B): two host builds resolving glibc 2.39 and 2.44 hit one // entry, and the mixed BMIs crashed the clang frontend during deserialization // rather than producing a diagnostic. -TEST(CacheKey, TheHeaderSetIsPartOfTheKey) { +UTest the_header_set_is_part_of_the_key{"CacheKey/TheHeaderSetIsPartOfTheKey", [] { mcpp::manifest::Manifest m; m.package.standard = "c++23"; @@ -538,23 +538,22 @@ TEST(CacheKey, TheHeaderSetIsPartOfTheKey) { b.targetHeaderSet = { "-isystem/xim-x-glibc/2.44/include" }; a.targetHeaderSet = { "-isystem/xim-x-glibc/2.39/include" }; - EXPECT_NE(ck::key_hex(a, pkg()), ck::key_hex(b, pkg())); -} + expect(ne(ck::key_hex(a, pkg()), ck::key_hex(b, pkg()))); +}}; // ...and it travels in entry.json, so a suspected wrong hit can be read field // by field rather than guessed at. `is_cached` compares `inputs.toolchain` as // one object, which is also why an entry written before this axis existed is a // miss without needing `kCacheEpoch` to move. -TEST(CacheKey, TheHeaderSetIsRecordedInTheEntry) { +UTest the_header_set_is_recorded_in_the_entry{"CacheKey/TheHeaderSetIsRecordedInTheEntry", [] { mcpp::manifest::Manifest m; m.package.standard = "c++23"; auto a = ck::build_axes(freestanding_tc(false), m, "-std=c++23", {}, ""); a.targetHeaderSet = { "-isystem/xim-x-glibc/2.44/include" }; auto j = ck::to_json(a, pkg()); - ASSERT_TRUE(j["toolchain"].contains("target_header_set")); - EXPECT_EQ(j["toolchain"]["target_header_set"][0], - "-isystem/xim-x-glibc/2.44/include"); -} + expect(fatal(j["toolchain"].contains("target_header_set"))); + expect(eq(j["toolchain"]["target_header_set"][0], "-isystem/xim-x-glibc/2.44/include")); +}}; // AND THE PATHS IN IT ARE RELATIVE — INCLUDING THE ONES OUTSIDE ``. // @@ -573,7 +572,7 @@ TEST(CacheKey, TheHeaderSetIsRecordedInTheEntry) { // THE CRITERION IS THE ABSENCE OF THE HOME, not the presence of a tag. // Asserting `starts_with("")` would still pass if the rest of the string // carried the absolute path behind it. -TEST(CacheKey, TheHeaderSetCarriesNoAbsoluteHome) { +UTest the_header_set_carries_no_absolute_home{"CacheKey/TheHeaderSetCarriesNoAbsoluteHome", [] { const std::filesystem::path home = "/home/somebody/.mcpp/registry"; const std::filesystem::path store = home / "data" / "xpkgs"; @@ -590,18 +589,18 @@ TEST(CacheKey, TheHeaderSetCarriesNoAbsoluteHome) { // A denominator: with an empty axis every assertion below is vacuous, and // an empty axis is itself the bug in the other direction. - ASSERT_FALSE(a.targetHeaderSet.empty()); + expect(fatal(!a.targetHeaderSet.empty())); for (auto const& t : a.targetHeaderSet) - EXPECT_EQ(t.find(home.generic_string()), std::string::npos) << t; + expect(eq(t.find(home.generic_string()), std::string::npos)) << t; // Two subos on ONE home are still two keys — the relativization must not // flatten the distinction it exists to preserve. auto tc2 = tc; tc2.base.root = home / "subos" / "musl"; const auto b = ck::build_axes(tc2, m, "-std=c++23", {}, "", store); - EXPECT_NE(a.targetHeaderSet, b.targetHeaderSet); - EXPECT_NE(ck::key_hex(a, pkg()), ck::key_hex(b, pkg())); -} + expect(ne(a.targetHeaderSet, b.targetHeaderSet)); + expect(ne(ck::key_hex(a, pkg()), ck::key_hex(b, pkg()))); +}}; // Position-independent code was ABSENT from this key (issue #519). // @@ -612,26 +611,26 @@ TEST(CacheKey, TheHeaderSetCarriesNoAbsoluteHome) { // consumer can write `dependency_linkage = "shared"`, a hit across that line // hands non-PIC objects to a shared link, and the message names a relocation // in a file nobody edited. -TEST(CacheKey, PicChangesTheKey) { +UTest pic_changes_the_key{"CacheKey/PicChangesTheKey", [] { auto b = axes(); auto p = pkg(); const auto without = ck::key_hex(b, p); b.pic = true; - EXPECT_NE(ck::key_hex(b, p), without); -} + expect(ne(ck::key_hex(b, p), without)); +}}; -TEST(CacheKey, PicIsRecordedInEntryJsonSoAHitCanBeAudited) { +UTest pic_is_recorded_in_entry_json_so_a_hit_can_be_audited{"CacheKey/PicIsRecordedInEntryJsonSoAHitCanBeAudited", [] { auto b = axes(); b.pic = true; auto j = ck::to_json(b, pkg()); - ASSERT_TRUE(j["profile"].contains("pic")); - EXPECT_EQ(j["profile"]["pic"], true); -} + expect(fatal(j["profile"].contains("pic"))); + expect(eq(j["profile"]["pic"], true)); +}}; // The CRT model on the MSVC ABI is whole-build like PIC (#718): switching // `cxx_runtime` must not be served an object compiled against the other CRT. // Off the MSVC ABI the axis is empty, so no other entry's key moves. -TEST(CacheKey, TheMsvcCrtModelChangesTheKeyOnlyOnTheMsvcAbi) { +UTest the_msvc_crt_model_changes_the_key_only_on_the_msvc_abi{"CacheKey/TheMsvcCrtModelChangesTheKeyOnlyOnTheMsvcAbi", [] { auto key_for = [](std::string triple, mcpp::toolchain::CompilerId id, std::string cxxRuntime) { mcpp::toolchain::Toolchain tc; @@ -646,26 +645,26 @@ TEST(CacheKey, TheMsvcCrtModelChangesTheKeyOnlyOnTheMsvcAbi) { for (auto id : {CompilerId::Clang, CompilerId::MSVC}) { auto [crtStatic, keyStatic] = key_for("x86_64-pc-windows-msvc", id, "self-contained"); auto [crtDynamic, keyDynamic] = key_for("x86_64-pc-windows-msvc", id, "toolchain-coupled"); - EXPECT_FALSE(crtStatic.empty()); - EXPECT_NE(crtStatic, crtDynamic); - EXPECT_NE(keyStatic, keyDynamic); + expect(!crtStatic.empty()); + expect(ne(crtStatic, crtDynamic)); + expect(ne(keyStatic, keyDynamic)); } auto [crtA, keyA] = key_for("x86_64-linux-gnu", CompilerId::GCC, "self-contained"); auto [crtB, keyB] = key_for("x86_64-linux-gnu", CompilerId::GCC, "toolchain-coupled"); - EXPECT_TRUE(crtA.empty()); - EXPECT_TRUE(crtB.empty()); -} + expect(crtA.empty()); + expect(crtB.empty()); +}}; -TEST(CacheKey, PicDefaultsOffSoExistingEntriesKeepTheirIdentity) { +UTest pic_defaults_off_so_existing_entries_keep_their_identity{"CacheKey/PicDefaultsOffSoExistingEntriesKeepTheirIdentity", [] { ck::BuildAxes fresh; - EXPECT_FALSE(fresh.pic); -} + expect(!fresh.pic); +}}; // #641 item 2: a C++-layer provider's implementation units are compiled at the // level its manifest states, and the graph's level alone does not name it. The // entry must move when that statement moves, and nothing else's entry may move // because the statement exists. -TEST(CacheKey, ACxxLayerProvidersStatedStandardChangesTheKey) { +UTest a_cxx_layer_providers_stated_standard_changes_the_key{"CacheKey/ACxxLayerProvidersStatedStandardChangesTheKey", [] { const std::filesystem::path store = "/home/u/.mcpp/registry/data/xpkgs"; const auto pkgRoot = store / "llvm-x-libcxx/22.1.8.3/libcxx-22.1.8.3"; auto key_for = [&](std::vector provides, std::string standard) { @@ -683,19 +682,19 @@ TEST(CacheKey, ACxxLayerProvidersStatedStandardChangesTheKey) { const auto none = key_for({}, ""); // A provider that states nothing, and a non-provider that states a level, // hash exactly as a package that says neither. - EXPECT_EQ(key_for(layer, ""), none); - EXPECT_EQ(key_for({}, "c++23"), none); + expect(eq(key_for(layer, ""), none)); + expect(eq(key_for({}, "c++23"), none)); // A stated level on a provider moves the key, and two levels are two keys. - EXPECT_NE(key_for(layer, "c++23"), none); - EXPECT_NE(key_for(layer, "c++23"), key_for(layer, "c++26")); -} + expect(ne(key_for(layer, "c++23"), none)); + expect(ne(key_for(layer, "c++23"), key_for(layer, "c++26"))); +}}; // THE MSVC TOOLSET OF THE CLANG ROW IS IN THE KEY. Before it reached the // command line the driver found the STL by itself and `stdlibVersion` held // clang's version, so a Visual Studio update that changed the STL left the key // unchanged. The toolset directory carries its version, so two toolsets are // two keys. -TEST(CacheKey, TheMsvcToolsetOfTheClangRowChangesTheKey) { +UTest the_msvc_toolset_of_the_clang_row_changes_the_key{"CacheKey/TheMsvcToolsetOfTheClangRowChangesTheKey", [] { mcpp::toolchain::Toolchain tc; tc.compiler = mcpp::toolchain::CompilerId::Clang; tc.version = "20.1.7"; @@ -711,18 +710,16 @@ TEST(CacheKey, TheMsvcToolsetOfTheClangRowChangesTheKey) { bool present = false; for (auto const& t : a.targetHeaderSet) present = present || t.find("14.44.35207") != std::string::npos; - EXPECT_TRUE(present) << "the toolset directory is part of the header set"; + expect(bool(present)) << "the toolset directory is part of the header set"; auto tc2 = tc; tc2.base.msvc.toolsDir = "C:/VS/VC/Tools/MSVC/14.51.36014"; - EXPECT_NE(ck::key_hex(a, pkg()), - ck::key_hex(ck::build_axes(tc2, m, "-std=c++23", {}, ""), pkg())); + expect(ne(ck::key_hex(a, pkg()), ck::key_hex(ck::build_axes(tc2, m, "-std=c++23", {}, ""), pkg()))); auto tc3 = tc; tc3.base.msvc.sdkVersion = "10.0.22621.0"; - EXPECT_NE(ck::key_hex(a, pkg()), - ck::key_hex(ck::build_axes(tc3, m, "-std=c++23", {}, ""), pkg())); -} + expect(ne(ck::key_hex(a, pkg()), ck::key_hex(ck::build_axes(tc3, m, "-std=c++23", {}, ""), pkg()))); +}}; // THE BMI PATH CONVENTION MOVES THE KEY OF A PACKAGE THAT HAS ONE, AND NO // OTHER. A GCC BMI records the paths of the modules it imports as its module @@ -731,13 +728,13 @@ TEST(CacheKey, TheMsvcToolsetOfTheClangRowChangesTheKey) { // is set only for a package that provides a module, so a package of object // files keeps the key it had — the serialization of an unset axis is the // serialization from before the axis existed. -TEST(CacheKey, TheBmiPathConventionMovesOnlyThePackagesThatHaveOne) { +UTest the_bmi_path_convention_moves_only_the_packages_that_have_one{"CacheKey/TheBmiPathConventionMovesOnlyThePackagesThatHaveOne", [] { auto withModules = pkg(); withModules.bmiPathConvention = "$root=bmi-dir"; - EXPECT_NE(ck::key_hex(axes(), pkg()), ck::key_hex(axes(), withModules)); + expect(ne(ck::key_hex(axes(), pkg()), ck::key_hex(axes(), withModules))); const auto j = ck::to_json(axes(), withModules); - ASSERT_TRUE(j["language"].contains("bmi_path_convention")); - EXPECT_EQ(j["language"]["bmi_path_convention"], "$root=bmi-dir"); - EXPECT_FALSE(ck::to_json(axes(), pkg())["language"].contains("bmi_path_convention")); -} + expect(fatal(j["language"].contains("bmi_path_convention"))); + expect(eq(j["language"]["bmi_path_convention"], "$root=bmi-dir")); + expect(!(ck::to_json(axes(), pkg())["language"].contains("bmi_path_convention"))); +}}; diff --git a/tests/unit/test_capacity_jobs.cpp b/tests/unit/test_capacity_jobs.cpp index 391f71f54..268d93a90 100644 --- a/tests/unit/test_capacity_jobs.cpp +++ b/tests/unit/test_capacity_jobs.cpp @@ -1,13 +1,15 @@ +import mcpplibs.mtest; // mcpp.build.capacity::recommended_jobs — the `--jobs auto` formula. // // Asserted against SYNTHETIC capacities, not the machine running the test: a // test that asked the host how many cores it has would assert nothing (it would // just restate the answer) and would produce a different verdict on every CI // runner. -#include import mcpp.build.capacity; +using namespace mcpplibs::mtest; + using mcpp::build::capacity::HostCapacity; using mcpp::build::capacity::recommended_jobs; @@ -17,67 +19,67 @@ constexpr unsigned long long GiB = 1024ull * 1024 * 1024; // A homogeneous machine with ample RAM should use its logical CPUs: SMT siblings // still contribute, just less than a full core. -TEST(RecommendedJobs, HomogeneousAndAmpleMemoryUsesLogicalCores) { +UTest homogeneous_and_ample_memory_uses_logical_cores{"RecommendedJobs/HomogeneousAndAmpleMemoryUsesLogicalCores", [] { HostCapacity cap{.logicalCores = 16, .physicalCores = 8, .heterogeneous = false, .totalBytes = 64 * GiB, .availableBytes = 60 * GiB}; - EXPECT_EQ(recommended_jobs(cap), 16); -} + expect(eq(recommended_jobs(cap), 16)); +}}; // The case that motivated the whole function: 32 "cores" on a 13900K are 8 // P-cores + 16 E-cores, and treating them as 32 equal workers overestimates // usable parallelism by more than 2x. -TEST(RecommendedJobs, HeterogeneousFallsBackToPhysicalCores) { +UTest heterogeneous_falls_back_to_physical_cores{"RecommendedJobs/HeterogeneousFallsBackToPhysicalCores", [] { HostCapacity cap{.logicalCores = 32, .physicalCores = 24, .heterogeneous = true, .totalBytes = 64 * GiB, .availableBytes = 60 * GiB}; - EXPECT_EQ(recommended_jobs(cap), 24); -} + expect(eq(recommended_jobs(cap), 24)); +}}; // The dangerous shape: many cores, little RAM. ninja's default here would be 66 // concurrent compiles against ~0.75 GB each and the machine would swap. -TEST(RecommendedJobs, MemoryBoundMachineIsCappedByRam) { +UTest memory_bound_machine_is_capped_by_ram{"RecommendedJobs/MemoryBoundMachineIsCappedByRam", [] { HostCapacity cap{.logicalCores = 64, .physicalCores = 64, .heterogeneous = false, .totalBytes = 32 * GiB, .availableBytes = 32 * GiB}; // (32 - 2) / 0.75 = 40 → below the 64 CPUs, so memory decides. - EXPECT_EQ(recommended_jobs(cap), 40); - EXPECT_LT(recommended_jobs(cap), cap.logicalCores); -} + expect(eq(recommended_jobs(cap), 40)); + expect(lt(recommended_jobs(cap), cap.logicalCores)); +}}; // Must still make progress rather than refusing to build. -TEST(RecommendedJobs, TinyMemoryStillYieldsOneJob) { +UTest tiny_memory_still_yields_one_job{"RecommendedJobs/TinyMemoryStillYieldsOneJob", [] { HostCapacity cap{.logicalCores = 8, .physicalCores = 8, .heterogeneous = false, .totalBytes = 2 * GiB, .availableBytes = 1 * GiB}; - EXPECT_EQ(recommended_jobs(cap), 1); -} + expect(eq(recommended_jobs(cap), 1)); +}}; -TEST(RecommendedJobs, NeverExceedsTheCeiling) { +UTest never_exceeds_the_ceiling{"RecommendedJobs/NeverExceedsTheCeiling", [] { HostCapacity cap{.logicalCores = 256, .physicalCores = 256, .heterogeneous = false, .totalBytes = 1024 * GiB, .availableBytes = 1000 * GiB}; - EXPECT_EQ(recommended_jobs(cap), 64); - EXPECT_EQ(recommended_jobs(cap, 768ull * 1024 * 1024, 2 * GiB, /*ceiling=*/8), 8); -} + expect(eq(recommended_jobs(cap), 64)); + expect(eq(recommended_jobs(cap, 768ull * 1024 * 1024, 2 * GiB, /*ceiling=*/8), 8)); +}}; -TEST(RecommendedJobs, NeverReturnsZero) { +UTest never_returns_zero{"RecommendedJobs/NeverReturnsZero", [] { HostCapacity cap{.logicalCores = 0, .physicalCores = 0, .heterogeneous = false, .totalBytes = 0, .availableBytes = 0}; - EXPECT_GE(recommended_jobs(cap), 1); -} + expect(ge(recommended_jobs(cap), 1)); +}}; // A project with heavier translation units can say so without patching mcpp. -TEST(RecommendedJobs, PerJobEstimateIsAParameter) { +UTest per_job_estimate_is_a_parameter{"RecommendedJobs/PerJobEstimateIsAParameter", [] { HostCapacity cap{.logicalCores = 32, .physicalCores = 32, .heterogeneous = false, .totalBytes = 32 * GiB, .availableBytes = 32 * GiB}; const int light = recommended_jobs(cap, 256ull * 1024 * 1024); const int heavy = recommended_jobs(cap, 4ull * 1024 * 1024 * 1024); - EXPECT_GT(light, heavy); - EXPECT_LE(heavy, 8); -} + expect(gt(light, heavy)); + expect(le(heavy, 8)); +}}; // The real machine must at least produce something sane — a weak assertion on // purpose, since the value is host-dependent. -TEST(RecommendedJobs, HostQueryIsPlausible) { +UTest host_query_is_plausible{"RecommendedJobs/HostQueryIsPlausible", [] { const auto cap = mcpp::build::capacity::host_capacity(); - EXPECT_GE(cap.logicalCores, 1); - EXPECT_GE(cap.physicalCores, 1); - EXPECT_LE(cap.physicalCores, cap.logicalCores); - EXPECT_GE(recommended_jobs(cap), 1); -} + expect(ge(cap.logicalCores, 1)); + expect(ge(cap.physicalCores, 1)); + expect(le(cap.physicalCores, cap.logicalCores)); + expect(ge(recommended_jobs(cap), 1)); +}}; diff --git a/tests/unit/test_cenv.cpp b/tests/unit/test_cenv.cpp index 3cffcf73d..35ab11b07 100644 --- a/tests/unit/test_cenv.cpp +++ b/tests/unit/test_cenv.cpp @@ -6,12 +6,14 @@ // `mcpp.targetside::resolve` — so the whole mapping table §3.3 publishes can // be asserted here without compiling anything. -#include import std; +import mcpplibs.mtest; import mcpp.targetside; import mcpp.toolchain.cenv; +using namespace mcpplibs::mtest; + namespace ts = mcpp::targetside; namespace cenv = mcpp::toolchain::cenv; @@ -36,14 +38,14 @@ bool has(const std::vector& v, std::string_view tok) { // ── §3.3's own table: the three realisable rows ────────────────────────────── -TEST(CEnv, LinuxPosixArchDefaultIsANoOp) { +UTest linux_posix_arch_default_is_a_no_op{"CEnv/LinuxPosixArchDefaultIsANoOp", [] { auto d = decl(ts::CAbiPresents::Posix, ts::CAbiDataModel::ArchDefault, 32); auto r = cenv::realise(d, "linux", "x86_64", false); - ASSERT_TRUE(r.has_value()) << r.error(); - EXPECT_TRUE(r->tokens.empty()); - EXPECT_EQ(r->expectLongBytes, 8); - EXPECT_EQ(r->expectWcharBits, 32); -} + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); + expect(r->tokens.empty()); + expect(eq(r->expectLongBytes, 8)); + expect(eq(r->expectWcharBits, 32)); +}}; // NOT a no-op — a correction from the design's original text, caught by the // verification probe on a real build (coordinator report): Apple's clang @@ -53,14 +55,14 @@ TEST(CEnv, LinuxPosixArchDefaultIsANoOp) { // macOS's cost for that fact is one token, `-D__unix__`, same as a // freestanding target's (`FreestandingPosixDefinesUnix` below) and for the // identical reason: neither already has it. -TEST(CEnv, MacosPosixArchDefaultDefinesUnix) { +UTest macos_posix_arch_default_defines_unix{"CEnv/MacosPosixArchDefaultDefinesUnix", [] { auto d = decl(ts::CAbiPresents::Posix, ts::CAbiDataModel::ArchDefault, 32); auto r = cenv::realise(d, "macos", "aarch64", false); - ASSERT_TRUE(r.has_value()) << r.error(); - ASSERT_TRUE(has(r->tokens, "-D__unix__")); - ASSERT_TRUE(has(r->expectDefined, "__unix__")); - ASSERT_TRUE(has(r->expectUndefined, "_WIN32")); -} + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); + expect(fatal(bool(has(r->tokens, "-D__unix__")))); + expect(fatal(bool(has(r->expectDefined, "__unix__")))); + expect(fatal(bool(has(r->expectUndefined, "_WIN32")))); +}}; // The flagship case: Cygwin-flavoured Windows. // @@ -92,120 +94,120 @@ TEST(CEnv, MacosPosixArchDefaultDefinesUnix) { // against the PUBLISHED 0.13.0 on this engine, which fails on libunwind's // two `static_assert`s exactly as predicted. An ordering between // repositories has no assertion in either of them. -TEST(CEnv, WindowsPosixArchDefaultSubstitutesTheCygwinTriple) { +UTest windows_posix_arch_default_substitutes_the_cygwin_triple{"CEnv/WindowsPosixArchDefaultSubstitutesTheCygwinTriple", [] { auto d = decl(ts::CAbiPresents::Posix, ts::CAbiDataModel::ArchDefault, 32); auto r = cenv::realise(d, "windows", "x86_64", false); - ASSERT_TRUE(r.has_value()) << r.error(); - EXPECT_TRUE(has(r->tokens, "--target=x86_64-pc-cygwin")); + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); + expect(bool(has(r->tokens, "--target=x86_64-pc-cygwin"))); // The target's identity is not this module's to state: see above. - EXPECT_FALSE(has(r->tokens, "-D__MCPP_TARGET_WINDOWS__=1")); + expect(!has(r->tokens, "-D__MCPP_TARGET_WINDOWS__=1")); // The borrowed name is undefined on the compile line. `-U` rather than a // different triple: the Cygwin identity is what supplies `__unix__` and // suppresses `_WIN32`, which is the point of `presents = "posix"`; only // the NAME was unwanted. - EXPECT_TRUE(has(r->tokens, "-U__CYGWIN__")); - EXPECT_TRUE(has(r->tokens, "-U__CYGWIN32__")); + expect(bool(has(r->tokens, "-U__CYGWIN__"))); + expect(bool(has(r->tokens, "-U__CYGWIN32__"))); // wchar 32 differs from Cygwin's own default (16) — the flag is added. - EXPECT_TRUE(has(r->tokens, "-fno-short-wchar")); - EXPECT_EQ(r->expectLongBytes, 8); - EXPECT_EQ(r->expectWcharBits, 32); - ASSERT_TRUE(has(r->expectDefined, "__unix__")); - ASSERT_TRUE(has(r->expectUndefined, "_WIN32")); + expect(bool(has(r->tokens, "-fno-short-wchar"))); + expect(eq(r->expectLongBytes, 8)); + expect(eq(r->expectWcharBits, 32)); + expect(fatal(bool(has(r->expectDefined, "__unix__")))); + expect(fatal(bool(has(r->expectUndefined, "_WIN32")))); // THE PROBE CHECKS THESE, which is what keeps the tokens from being ones // the compiler could ignore: `cenv_probe::verify` compares the realised // configuration's predefines against these lists and refuses on a // mismatch. A `-U` that did not take effect is a verification failure, // not a silent one --- and silence is the whole risk here, since a `#if` // that finds the name still defined simply compiles the other branch. - ASSERT_TRUE(has(r->expectUndefined, "__CYGWIN__")); - ASSERT_TRUE(has(r->expectUndefined, "__CYGWIN32__")); - EXPECT_FALSE(has(r->expectDefined, "__CYGWIN__")); -} + expect(fatal(bool(has(r->expectUndefined, "__CYGWIN__")))); + expect(fatal(bool(has(r->expectUndefined, "__CYGWIN32__")))); + expect(!has(r->expectDefined, "__CYGWIN__")); +}}; // wchar = 16 on the Cygwin substitution matches Cygwin's own default, so no // `-f[no-]short-wchar` token is needed. -TEST(CEnv, WindowsPosixWchar16NeedsNoWcharFlag) { +UTest windows_posix_wchar16_needs_no_wchar_flag{"CEnv/WindowsPosixWchar16NeedsNoWcharFlag", [] { auto d = decl(ts::CAbiPresents::Posix, ts::CAbiDataModel::ArchDefault, 16); auto r = cenv::realise(d, "windows", "x86_64", false); - ASSERT_TRUE(r.has_value()) << r.error(); - EXPECT_FALSE(has(r->tokens, "-fno-short-wchar")); - EXPECT_FALSE(has(r->tokens, "-fshort-wchar")); -} + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); + expect(!has(r->tokens, "-fno-short-wchar")); + expect(!has(r->tokens, "-fshort-wchar")); +}}; // `presents = "windows"` on Windows is the base triple's own identity. -TEST(CEnv, WindowsWindowsArchDefaultIsANoOp) { +UTest windows_windows_arch_default_is_a_no_op{"CEnv/WindowsWindowsArchDefaultIsANoOp", [] { auto d = decl(ts::CAbiPresents::Windows, ts::CAbiDataModel::ArchDefault, 16); auto r = cenv::realise(d, "windows", "x86_64", false); - ASSERT_TRUE(r.has_value()) << r.error(); - EXPECT_TRUE(r->tokens.empty()); -} + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); + expect(r->tokens.empty()); +}}; // ── `builtins = "iso"` — the Apple case is the only one this survey found ──── -TEST(CEnv, BuiltinsIsoOnMacosDisablesMemsetPattern16) { +UTest builtins_iso_on_macos_disables_memset_pattern16{"CEnv/BuiltinsIsoOnMacosDisablesMemsetPattern16", [] { auto d = decl(ts::CAbiPresents::Posix, ts::CAbiDataModel::ArchDefault, 32, ts::CAbiBuiltins::Iso); auto r = cenv::realise(d, "macos", "x86_64", false); - ASSERT_TRUE(r.has_value()) << r.error(); + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); // `-fno-builtin`, NOT the per-function spelling. The narrower form was // emitted here and measured to do nothing: clang checks `-fno-builtin-X` // against its builtin table, `memset_pattern16` is an LLVM TLI libfunc, // and the call is produced by a pass that consults TLI. clang accepts an // unknown name in that family in silence, so the flag looked right and // was not. - EXPECT_TRUE(has(r->builtinsTokens, "-fno-builtin")); - EXPECT_FALSE(has(r->builtinsTokens, "-fno-builtin-memset_pattern16")); -} + expect(bool(has(r->builtinsTokens, "-fno-builtin"))); + expect(!has(r->builtinsTokens, "-fno-builtin-memset_pattern16")); +}}; -TEST(CEnv, BuiltinsIsoOnLinuxAddsNothingMeasurable) { +UTest builtins_iso_on_linux_adds_nothing_measurable{"CEnv/BuiltinsIsoOnLinuxAddsNothingMeasurable", [] { auto d = decl(ts::CAbiPresents::Posix, ts::CAbiDataModel::ArchDefault, 32, ts::CAbiBuiltins::Iso); auto r = cenv::realise(d, "linux", "x86_64", false); - ASSERT_TRUE(r.has_value()) << r.error(); - EXPECT_TRUE(r->builtinsTokens.empty()); -} + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); + expect(r->builtinsTokens.empty()); +}}; -TEST(CEnv, BuiltinsPlatformAddsNothingOnAnyTarget) { +UTest builtins_platform_adds_nothing_on_any_target{"CEnv/BuiltinsPlatformAddsNothingOnAnyTarget", [] { auto d = decl(ts::CAbiPresents::Posix, ts::CAbiDataModel::ArchDefault, 32, ts::CAbiBuiltins::Platform); auto r = cenv::realise(d, "macos", "x86_64", false); - ASSERT_TRUE(r.has_value()) << r.error(); - EXPECT_TRUE(r->builtinsTokens.empty()); -} + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); + expect(r->builtinsTokens.empty()); +}}; // ── refusals: everything outside the table §3.3 publishes ─────────────────── -TEST(CEnv, WindowsPosixOnANonX86_64ArchIsRefused) { +UTest windows_posix_on_a_non_x86_64_arch_is_refused{"CEnv/WindowsPosixOnANonX86_64ArchIsRefused", [] { auto d = decl(ts::CAbiPresents::Posix, ts::CAbiDataModel::ArchDefault, 32); auto r = cenv::realise(d, "windows", "aarch64", false); - ASSERT_FALSE(r.has_value()); - EXPECT_NE(r.error().find("x86_64"), std::string::npos) << r.error(); -} + expect(fatal(!r.has_value())); + if (auto&& failed = expect(ne(r.error().find("x86_64"), std::string::npos)); !failed) failed << r.error(); +}}; -TEST(CEnv, LinuxWindowsPresentsIsRefused) { +UTest linux_windows_presents_is_refused{"CEnv/LinuxWindowsPresentsIsRefused", [] { auto d = decl(ts::CAbiPresents::Windows, ts::CAbiDataModel::ArchDefault, 32); auto r = cenv::realise(d, "linux", "x86_64", false); - ASSERT_FALSE(r.has_value()); - EXPECT_NE(r.error().find("no known way"), std::string::npos) << r.error(); -} + expect(fatal(!r.has_value())); + if (auto&& failed = expect(ne(r.error().find("no known way"), std::string::npos)); !failed) failed << r.error(); +}}; -TEST(CEnv, WindowsPresentsNoneIsRefused) { +UTest windows_presents_none_is_refused{"CEnv/WindowsPresentsNoneIsRefused", [] { auto d = decl(ts::CAbiPresents::None, ts::CAbiDataModel::ArchDefault, 32); auto r = cenv::realise(d, "windows", "x86_64", false); - ASSERT_FALSE(r.has_value()); -} + expect(fatal(!r.has_value())); +}}; -TEST(CEnv, LlP64RequestedOnLinuxIsRefused) { +UTest ll_p64_requested_on_linux_is_refused{"CEnv/LlP64RequestedOnLinuxIsRefused", [] { auto d = decl(ts::CAbiPresents::Posix, ts::CAbiDataModel::Llp64, 32); auto r = cenv::realise(d, "linux", "x86_64", false); - ASSERT_FALSE(r.has_value()); - EXPECT_NE(r.error().find("data-model"), std::string::npos) << r.error(); -} + expect(fatal(!r.has_value())); + if (auto&& failed = expect(ne(r.error().find("data-model"), std::string::npos)); !failed) failed << r.error(); +}}; -TEST(CEnv, FreestandingAcceptsNone) { +UTest freestanding_accepts_none{"CEnv/FreestandingAcceptsNone", [] { auto ok = decl(ts::CAbiPresents::None, ts::CAbiDataModel::ArchDefault, 32); auto r1 = cenv::realise(ok, "none", "riscv64", true); - ASSERT_TRUE(r1.has_value()) << r1.error(); + if (auto&& failed = expect(fatal(r1.has_value())); !failed) failed << r1.error(); // The wchar branch ALWAYS emits on a freestanding target now (wave // 2026-09-18, openkal-llvm-runtime#24, Windows-host × riscv64-none-elf // measurement): the toolchain's host-contaminated default is not @@ -214,15 +216,15 @@ TEST(CEnv, FreestandingAcceptsNone) { // macros stay absent (the `none` presents value's whole point), // but the wchar realisation still applies — and so does the // expectation, which the probe then measures. - EXPECT_TRUE(has(r1->tokens, "-fno-short-wchar")); - EXPECT_TRUE(r1->expectUndefined.empty()) + expect(bool(has(r1->tokens, "-fno-short-wchar"))); + expect(r1->expectUndefined.empty()) << "presents = none declares NO environment-identity macros, " "neither defined nor undefined"; - EXPECT_TRUE(r1->expectDefined.empty()); + expect(r1->expectDefined.empty()); // Data-model is not checked on freestanding (no C library runtime to // present it), so `expectLongBytes` stays at 0. - EXPECT_EQ(r1->expectLongBytes, 0); -} + expect(eq(r1->expectLongBytes, 0)); +}}; // A freestanding target ALSO realises `presents = "posix"` (design §3.3, // coordinator revision — this used to be an unconditional refusal, which is @@ -230,14 +232,14 @@ TEST(CEnv, FreestandingAcceptsNone) { // before reaching any other target, for a fact this engine can in fact // deliver). Bare metal starts with neither `_WIN32` nor `__unix__` defined, // same as macOS's default triple, so it costs the identical one token. -TEST(CEnv, FreestandingPosixDefinesUnix) { +UTest freestanding_posix_defines_unix{"CEnv/FreestandingPosixDefinesUnix", [] { auto d = decl(ts::CAbiPresents::Posix, ts::CAbiDataModel::ArchDefault, 32); auto r = cenv::realise(d, "none", "riscv64", true); - ASSERT_TRUE(r.has_value()) << r.error(); - ASSERT_TRUE(has(r->tokens, "-D__unix__")); - ASSERT_TRUE(has(r->expectDefined, "__unix__")); - ASSERT_TRUE(has(r->expectUndefined, "_WIN32")); -} + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); + expect(fatal(bool(has(r->tokens, "-D__unix__")))); + expect(fatal(bool(has(r->expectDefined, "__unix__")))); + expect(fatal(bool(has(r->expectUndefined, "_WIN32")))); +}}; // A freestanding target with `wchar = 32` USED TO skip the // `-fno-short-wchar` token on the reasoning that the toolchain default was @@ -251,44 +253,44 @@ TEST(CEnv, FreestandingPosixDefinesUnix) { // the engine actually produced (32 bits, with the flag), not the host's // leak. Pinned here so a future "freestanding skips the flag" optimisation // cannot return without a regression test. -TEST(CEnv, FreestandingWchar32AlwaysEmitsNoShortWchar) { +UTest freestanding_wchar32_always_emits_no_short_wchar{"CEnv/FreestandingWchar32AlwaysEmitsNoShortWchar", [] { auto d = decl(ts::CAbiPresents::Posix, ts::CAbiDataModel::ArchDefault, 32); auto r = cenv::realise(d, "none", "riscv64", true); - ASSERT_TRUE(r.has_value()) << r.error(); - EXPECT_TRUE(has(r->tokens, "-fno-short-wchar")) + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); + expect(bool(has(r->tokens, "-fno-short-wchar"))) << "freestanding host leakage on Windows would compile a 16-bit " "wchar_t otherwise; the wave's measurement is the source for " "this assertion."; - EXPECT_EQ(r->expectWcharBits, 32); -} + expect(eq(r->expectWcharBits, 32)); +}}; // The matching case for `wchar = 16`: `-fshort-wchar` always emitted on // freestanding when the declaration asks for 16, so a Linux/macOS host // (which defaults to 32 bits on a freestanding target) is brought down to // the declaration. Mirrors the wave's measurement in the other direction. -TEST(CEnv, FreestandingWchar16AlwaysEmitsShortWchar) { +UTest freestanding_wchar16_always_emits_short_wchar{"CEnv/FreestandingWchar16AlwaysEmitsShortWchar", [] { auto d = decl(ts::CAbiPresents::Posix, ts::CAbiDataModel::ArchDefault, 16); auto r = cenv::realise(d, "none", "riscv64", true); - ASSERT_TRUE(r.has_value()) << r.error(); - EXPECT_TRUE(has(r->tokens, "-fshort-wchar")); - EXPECT_EQ(r->expectWcharBits, 16); -} + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); + expect(bool(has(r->tokens, "-fshort-wchar"))); + expect(eq(r->expectWcharBits, 16)); +}}; // `windows` still has no realisation on a freestanding target — there is no // operating system under it for that identity to belong to. Unaffected by // the `posix` fix above; pinned so it stays that way. -TEST(CEnv, FreestandingWindowsIsStillRefused) { +UTest freestanding_windows_is_still_refused{"CEnv/FreestandingWindowsIsStillRefused", [] { auto d = decl(ts::CAbiPresents::Windows, ts::CAbiDataModel::ArchDefault, 16); auto r = cenv::realise(d, "none", "riscv64", true); - ASSERT_FALSE(r.has_value()); -} + expect(fatal(!r.has_value())); +}}; // A 32-bit architecture's own native data model is ILP32 — the Cygwin // mechanism is x86_64-only, so `arch-default` there needs no substitution and // stays a no-op regardless. -TEST(CEnv, Ilp32ArchDefaultOnA32BitArchIsANoOp) { +UTest ilp32_arch_default_on_a32_bit_arch_is_a_no_op{"CEnv/Ilp32ArchDefaultOnA32BitArchIsANoOp", [] { auto d = decl(ts::CAbiPresents::Posix, ts::CAbiDataModel::ArchDefault, 32); auto r = cenv::realise(d, "linux", "arm", false); - ASSERT_TRUE(r.has_value()) << r.error(); - EXPECT_EQ(r->expectLongBytes, 4); -} + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); + expect(eq(r->expectLongBytes, 4)); +}}; diff --git a/tests/unit/test_cenv_probe.cpp b/tests/unit/test_cenv_probe.cpp index ab6e40147..00005019a 100644 --- a/tests/unit/test_cenv_probe.cpp +++ b/tests/unit/test_cenv_probe.cpp @@ -33,12 +33,14 @@ // parameter exists for; using it here is not a workaround, it is the // intended way to keep two callers' cached dumps apart. -#include import std; +import mcpplibs.mtest; import mcpp.toolchain.cenv_probe; import mcpp.run; +using namespace mcpplibs::mtest; + namespace cp = mcpp::toolchain::cenv_probe; namespace { @@ -153,77 +155,76 @@ struct TmpCache { } // namespace -TEST(CenvProbe, AMatchingDeclarationProducesNoMismatches) { - if (cxx().empty()) GTEST_SKIP() << "no C++ compiler found to probe"; +UTest a_matching_declaration_produces_no_mismatches{"CenvProbe/AMatchingDeclarationProducesNoMismatches", [] { + if (cxx().empty()) skip("no C++ compiler found to probe"); TmpCache cache; auto r = cp::verify(cxx(), {}, kHostWcharBits, kHostLongBytes, {}, {}, cache.dir); - ASSERT_TRUE(r.has_value()) << r.error(); - EXPECT_TRUE(r->ran) << "a fresh cache directory must actually probe"; - EXPECT_TRUE(r->mismatches.empty()); -} + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); + expect(bool(r->ran)) << "a fresh cache directory must actually probe"; + expect(r->mismatches.empty()); +}}; // THE CASE THE COORDINATOR ASKED TO SEE: a declared word width the compiler // disagrees with. This is exactly what a C library's [c-abi] block getting // `data-model` wrong would produce once it reached the probe, independent // of whether `cenv::realise`'s own mapping table would ever have accepted // that request in the first place. -TEST(CenvProbe, ADeclaredLongWidthTheCompilerDisagreesWithIsAMismatch) { - if (cxx().empty()) GTEST_SKIP() << "no C++ compiler found to probe"; +UTest a_declared_long_width_the_compiler_disagrees_with_is_a_mismatch{"CenvProbe/ADeclaredLongWidthTheCompilerDisagreesWithIsAMismatch", [] { + if (cxx().empty()) skip("no C++ compiler found to probe"); TmpCache cache; const int wrongLongBytes = kHostLongBytes == 8 ? 4 : 8; auto r = cp::verify(cxx(), {}, 0, wrongLongBytes, {}, {}, cache.dir); - ASSERT_TRUE(r.has_value()) << r.error(); - EXPECT_TRUE(r->ran); - ASSERT_EQ(r->mismatches.size(), 1u); - EXPECT_EQ(r->mismatches[0].fact, "sizeof(long)"); - EXPECT_EQ(r->mismatches[0].declared, std::to_string(wrongLongBytes)); - EXPECT_EQ(r->mismatches[0].measured, std::to_string(kHostLongBytes)); -} + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); + expect(bool(r->ran)); + expect(fatal(eq(r->mismatches.size(), 1u))); + expect(eq(r->mismatches[0].fact, "sizeof(long)")); + expect(eq(r->mismatches[0].declared, std::to_string(wrongLongBytes))); + expect(eq(r->mismatches[0].measured, std::to_string(kHostLongBytes))); +}}; -TEST(CenvProbe, ADeclaredWcharWidthTheCompilerDisagreesWithIsAMismatch) { - if (cxx().empty()) GTEST_SKIP() << "no C++ compiler found to probe"; +UTest a_declared_wchar_width_the_compiler_disagrees_with_is_a_mismatch{"CenvProbe/ADeclaredWcharWidthTheCompilerDisagreesWithIsAMismatch", [] { + if (cxx().empty()) skip("no C++ compiler found to probe"); TmpCache cache; const int wrongWcharBits = kHostWcharBits == 32 ? 16 : 32; auto r = cp::verify(cxx(), {}, wrongWcharBits, 0, {}, {}, cache.dir); - ASSERT_TRUE(r.has_value()) << r.error(); - ASSERT_EQ(r->mismatches.size(), 1u); - EXPECT_EQ(r->mismatches[0].fact, "__SIZEOF_WCHAR_T__ (bits)"); - EXPECT_EQ(r->mismatches[0].declared, std::to_string(wrongWcharBits)); - EXPECT_EQ(r->mismatches[0].measured, std::to_string(kHostWcharBits)); -} + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); + expect(fatal(eq(r->mismatches.size(), 1u))); + expect(eq(r->mismatches[0].fact, "__SIZEOF_WCHAR_T__ (bits)")); + expect(eq(r->mismatches[0].declared, std::to_string(wrongWcharBits))); + expect(eq(r->mismatches[0].measured, std::to_string(kHostWcharBits))); +}}; // `expectDefined` names a macro the declaration says must be there and // isn't — the shape a `presents = "posix"` declaration takes when the // compiler did not actually end up defining `__unix__`. -TEST(CenvProbe, AMacroDeclaredDefinedButAbsentIsAMismatch) { - if (cxx().empty()) GTEST_SKIP() << "no C++ compiler found to probe"; +UTest a_macro_declared_defined_but_absent_is_a_mismatch{"CenvProbe/AMacroDeclaredDefinedButAbsentIsAMismatch", [] { + if (cxx().empty()) skip("no C++ compiler found to probe"); TmpCache cache; auto r = cp::verify(cxx(), {}, 0, 0, {"__MCPP_TEST_MACRO_THAT_DOES_NOT_EXIST__"}, {}, cache.dir); - ASSERT_TRUE(r.has_value()) << r.error(); - ASSERT_EQ(r->mismatches.size(), 1u); - EXPECT_EQ(r->mismatches[0].fact, - "__MCPP_TEST_MACRO_THAT_DOES_NOT_EXIST__"); - EXPECT_EQ(r->mismatches[0].declared, "defined"); - EXPECT_EQ(r->mismatches[0].measured, "undefined"); -} + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); + expect(fatal(eq(r->mismatches.size(), 1u))); + expect(eq(r->mismatches[0].fact, "__MCPP_TEST_MACRO_THAT_DOES_NOT_EXIST__")); + expect(eq(r->mismatches[0].declared, "defined")); + expect(eq(r->mismatches[0].measured, "undefined")); +}}; // The reverse: a macro the declaration says must be ABSENT but the compiler // defines anyway — the shape `presents = "posix"` on a target whose // compiler still predefines `_WIN32` would take. `__cplusplus` is always // defined by a working C++ compiler, so this needs no special setup. -TEST(CenvProbe, AMacroDeclaredUndefinedButPresentIsAMismatch) { - if (cxx().empty()) GTEST_SKIP() << "no C++ compiler found to probe"; +UTest a_macro_declared_undefined_but_present_is_a_mismatch{"CenvProbe/AMacroDeclaredUndefinedButPresentIsAMismatch", [] { + if (cxx().empty()) skip("no C++ compiler found to probe"); TmpCache cache; auto r = cp::verify(cxx(), {}, 0, 0, {}, {"__cplusplus"}, cache.dir); - ASSERT_TRUE(r.has_value()) << r.error(); - ASSERT_EQ(r->mismatches.size(), 1u); - EXPECT_EQ(r->mismatches[0].fact, "__cplusplus"); - EXPECT_EQ(r->mismatches[0].declared, "undefined"); - EXPECT_EQ(r->mismatches[0].measured, "defined"); -} + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); + expect(fatal(eq(r->mismatches.size(), 1u))); + expect(eq(r->mismatches[0].fact, "__cplusplus")); + expect(eq(r->mismatches[0].declared, "undefined")); + expect(eq(r->mismatches[0].measured, "defined")); +}}; // THE EXACT TOKEN `cenv::realise` NOW PRODUCES FOR macOS AND A FREESTANDING // TARGET (design §3.3, coordinator revision) — `-D__unix__` — PASSED TO A @@ -240,48 +241,47 @@ TEST(CenvProbe, AMacroDeclaredUndefinedButPresentIsAMismatch) { // harmlessly redundant there rather than wrong, and the assertion is exactly // the same declared/measured pair regardless) still ends up matching what // was declared. -TEST(CenvProbe, TheDUnixTokenCenvRealiseProducesForMacosAndFreestandingSatisfiesItsOwnExpectation) { - if (cxx().empty()) GTEST_SKIP() << "no C++ compiler found to probe"; +UTest the_d_unix_token_cenv_realise_produces_for_macos_and_freestanding_satisfies_its_own_expectation{"CenvProbe/TheDUnixTokenCenvRealiseProducesForMacosAndFreestandingSatisfiesItsOwnExpectation", [] { + if (cxx().empty()) skip("no C++ compiler found to probe"); TmpCache cache; auto r = cp::verify(cxx(), {"-D__unix__"}, 0, 0, {"__unix__"}, {"_WIN32"}, cache.dir); - ASSERT_TRUE(r.has_value()) << r.error(); - EXPECT_TRUE(r->mismatches.empty()) - << (r->mismatches.empty() ? "" : r->mismatches[0].fact); -} + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); + if (auto&& failed = expect(r->mismatches.empty()); !failed) failed << (r->mismatches.empty() ? "" : r->mismatches[0].fact); +}}; // CACHED PER CONFIGURATION (module header) — a second call with the // identical compiler + argv must not recompile. Distinguished from the // first call via `ran`, exactly as `Result::ran`'s own doc comment says: a // cache hit still answers the mismatch question, it just does no work to. -TEST(CenvProbe, TheSecondCallForTheIdenticalConfigurationIsACacheHit) { - if (cxx().empty()) GTEST_SKIP() << "no C++ compiler found to probe"; +UTest the_second_call_for_the_identical_configuration_is_a_cache_hit{"CenvProbe/TheSecondCallForTheIdenticalConfigurationIsACacheHit", [] { + if (cxx().empty()) skip("no C++ compiler found to probe"); TmpCache cache; auto first = cp::verify(cxx(), {}, kHostWcharBits, kHostLongBytes, {}, {}, cache.dir); - ASSERT_TRUE(first.has_value()) << first.error(); - EXPECT_TRUE(first->ran); + if (auto&& failed = expect(fatal(first.has_value())); !failed) failed << first.error(); + expect(bool(first->ran)); auto second = cp::verify(cxx(), {}, kHostWcharBits, kHostLongBytes, {}, {}, cache.dir); - ASSERT_TRUE(second.has_value()) << second.error(); - EXPECT_FALSE(second->ran) << "identical compiler + argv must hit the cache"; - EXPECT_TRUE(second->mismatches.empty()); -} + if (auto&& failed = expect(fatal(second.has_value())); !failed) failed << second.error(); + expect(!second->ran) << "identical compiler + argv must hit the cache"; + expect(second->mismatches.empty()); +}}; // TWO different argv strings must NOT share a cache slot — the whole point // of hashing argv into the key. A wrong hit here would mean this build's // probe silently answered a DIFFERENT configuration's question. -TEST(CenvProbe, DifferentArgvDoesNotShareACacheSlot) { - if (cxx().empty()) GTEST_SKIP() << "no C++ compiler found to probe"; +UTest different_argv_does_not_share_a_cache_slot{"CenvProbe/DifferentArgvDoesNotShareACacheSlot", [] { + if (cxx().empty()) skip("no C++ compiler found to probe"); TmpCache cache; auto a = cp::verify(cxx(), {"-DMCPP_PROBE_TEST_A=1"}, 0, 0, {}, {}, cache.dir); auto b = cp::verify(cxx(), {"-DMCPP_PROBE_TEST_B=1"}, 0, 0, {}, {}, cache.dir); - ASSERT_TRUE(a.has_value()) << a.error(); - ASSERT_TRUE(b.has_value()) << b.error(); - EXPECT_TRUE(a->ran); - EXPECT_TRUE(b->ran) << "a different argv must not read A's cache entry"; -} + if (auto&& failed = expect(fatal(a.has_value())); !failed) failed << a.error(); + if (auto&& failed = expect(fatal(b.has_value())); !failed) failed << b.error(); + expect(bool(a->ran)); + expect(bool(b->ran)) << "a different argv must not read A's cache entry"; +}}; // ── assemble_argv — the invariant, reachable without a cross toolchain ──── // @@ -291,13 +291,13 @@ TEST(CenvProbe, DifferentArgvDoesNotShareACacheSlot) { // cannot live at the call site; it lives in the assembly, and these tests // reach it with no compiler at all. -TEST(CenvProbeArgv, AFreestandingTargetWithoutASelectionIsRefused) { +UTest a_freestanding_target_without_a_selection_is_refused{"CenvProbeArgv/AFreestandingTargetWithoutASelectionIsRefused", [] { // Exactly the 2026.9.18.3 shape: `crossTargetFlag` empty (it is set for // hosted targets only), no freestanding flags supplied, and `realise`'s // freestanding output, which carries no `--target`. auto r = cp::assemble_argv("", {}, {"-D__unix__", "-fno-short-wchar"}, {}, /*freestanding=*/true, "riscv64-none-elf"); - ASSERT_FALSE(r.has_value()) + expect(fatal(!r.has_value())) << "an argv with no target selection must be refused for a " "freestanding target; it measures the build host"; // THE CLAUSE, NOT ONLY THE TRIPLE. Two other refusals this wave added were @@ -308,56 +308,53 @@ TEST(CenvProbeArgv, AFreestandingTargetWithoutASelectionIsRefused) { // delimiters, so it has no such failure mode -- the assertion is widened // anyway, because that is a property of today's wording rather than of // the check. - EXPECT_NE(r.error().find("would have run with no target selection"), - std::string::npos) << r.error(); - EXPECT_NE(r.error().find("riscv64-none-elf"), std::string::npos) - << "the refusal must name the target it was for: " << r.error(); -} + expect(ne(r.error().find("would have run with no target selection"), std::string::npos)) << r.error(); + if (auto&& failed = expect(ne(r.error().find("riscv64-none-elf"), std::string::npos)); !failed) failed << "the refusal must name the target it was for: " << r.error(); +}}; -TEST(CenvProbeArgv, AFreestandingTargetWithItsCompilePrefixIsAccepted) { +UTest a_freestanding_target_with_its_compile_prefix_is_accepted{"CenvProbeArgv/AFreestandingTargetWithItsCompilePrefixIsAccepted", [] { std::vector fs{"--target=riscv64-none-elf", "-march=rv64gc", "-mabi=lp64d", "-ffreestanding"}; auto r = cp::assemble_argv("", fs, {"-D__unix__", "-fno-short-wchar"}, {}, true, "riscv64-none-elf"); - ASSERT_TRUE(r.has_value()) << r.error(); - EXPECT_NE(std::find(r->begin(), r->end(), "--target=riscv64-none-elf"), - r->end()); + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); + expect(ne(std::find(r->begin(), r->end(), "--target=riscv64-none-elf"), r->end())); // The ISA flags travel with the target: they change what the compiler // predefines (`__riscv_xlen`, the float ABI), so an argv that took the // triple and left them behind would measure a different machine again. - EXPECT_NE(std::find(r->begin(), r->end(), "-mabi=lp64d"), r->end()); -} + expect(ne(std::find(r->begin(), r->end(), "-mabi=lp64d"), r->end())); +}}; -TEST(CenvProbeArgv, ANativeHostedBuildNeedsNoSelection) { +UTest a_native_hosted_build_needs_no_selection{"CenvProbeArgv/ANativeHostedBuildNeedsNoSelection", [] { // The host IS the target; the absence is the decision rather than its // omission, and the probe must not be refused for it. openkal-musl on // Linux/x86_64 is this case: `realise` produces no tokens at all. auto r = cp::assemble_argv("", {}, {}, {}, false, "x86_64-linux-gnu"); - ASSERT_TRUE(r.has_value()) << r.error(); - EXPECT_TRUE(r->empty()); -} + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); + expect(r->empty()); +}}; -TEST(CenvProbeArgv, AHostedCrossTargetCarriesItsCrossFlag) { +UTest a_hosted_cross_target_carries_its_cross_flag{"CenvProbeArgv/AHostedCrossTargetCarriesItsCrossFlag", [] { auto r = cp::assemble_argv("--target=x86_64-w64-windows-gnu", {}, {"--target=x86_64-pc-cygwin", "-fno-short-wchar"}, {}, false, "x86_64-windows-gnu"); - ASSERT_TRUE(r.has_value()) << r.error(); + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); // Both are present and in this order: clang takes the LAST `--target`, // which is how the Cygwin-flavoured substitution overrides the base // triple without the producer of the base one knowing this module exists. - ASSERT_GE(r->size(), 2u); - EXPECT_EQ((*r)[0], "--target=x86_64-w64-windows-gnu"); - EXPECT_EQ((*r)[1], "--target=x86_64-pc-cygwin"); -} + expect(fatal(ge(r->size(), 2u))); + expect(eq((*r)[0], "--target=x86_64-w64-windows-gnu")); + expect(eq((*r)[1], "--target=x86_64-pc-cygwin")); +}}; -TEST(CenvProbeArgv, BuiltinsTokensAreCarriedLast) { +UTest builtins_tokens_are_carried_last{"CenvProbeArgv/BuiltinsTokensAreCarriedLast", [] { auto r = cp::assemble_argv("", {}, {"-D__unix__"}, {"-fno-builtin"}, false, "x86_64-apple-macos"); - ASSERT_TRUE(r.has_value()) << r.error(); - ASSERT_EQ(r->size(), 2u); - EXPECT_EQ((*r)[1], "-fno-builtin"); -} + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); + expect(fatal(eq(r->size(), 2u))); + expect(eq((*r)[1], "-fno-builtin")); +}}; // ── The argv must select the target (mcpp#674 review, 2026-09-20) ────────── // @@ -380,9 +377,9 @@ namespace { bool answers_for_riscv() { return !clangxx().empty(); } } // namespace -TEST(CenvProbe, AnArgvWithATargetMeasuresThatTargetAndNotTheHost) { - if (clangxx().empty()) GTEST_SKIP() << "no clang++ on PATH"; - if (!answers_for_riscv()) GTEST_SKIP() << "this clang does not answer for riscv64-none-elf"; +UTest an_argv_with_a_target_measures_that_target_and_not_the_host{"CenvProbe/AnArgvWithATargetMeasuresThatTargetAndNotTheHost", [] { + if (clangxx().empty()) skip("no clang++ on PATH"); + if (!answers_for_riscv()) skip("this clang does not answer for riscv64-none-elf"); TmpCache cache; // The freestanding probe's own shape, with the target selection the // caller (`mcpp.build.prepare`) now supplies. `__linux__` must be absent @@ -395,15 +392,15 @@ TEST(CenvProbe, AnArgvWithATargetMeasuresThatTargetAndNotTheHost) { std::vector defined{"__unix__", "__riscv"}; std::vector undefined{"__linux__", "_WIN32"}; auto r = cp::verify(clangxx(), argv, 32, 0, defined, undefined, cache.dir); - ASSERT_TRUE(r.has_value()) << r.error(); + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); for (auto const& mm : r->mismatches) - ADD_FAILURE() << mm.fact << ": declared " << mm.declared + expect(false) << mm.fact << ": declared " << mm.declared << ", measured " << mm.measured; -} +}}; -TEST(CenvProbe, TheSameArgvWithoutATargetMeasuresTheHost) { - if (clangxx().empty()) GTEST_SKIP() << "no clang++ on PATH"; - if (!answers_for_riscv()) GTEST_SKIP() << "this clang does not answer for riscv64-none-elf"; +UTest the_same_argv_without_a_target_measures_the_host{"CenvProbe/TheSameArgvWithoutATargetMeasuresTheHost", [] { + if (clangxx().empty()) skip("no clang++ on PATH"); + if (!answers_for_riscv()) skip("this clang does not answer for riscv64-none-elf"); TmpCache cache; // The mirror of the test above, and the one that makes it mean // something: with the target removed, the expectation that held for the @@ -411,9 +408,9 @@ TEST(CenvProbe, TheSameArgvWithoutATargetMeasuresTheHost) { // identically against a probe that ignores its argv. std::vector argv{"-ffreestanding", "-D__unix__", "-fno-short-wchar"}; auto r = cp::verify(clangxx(), argv, 0, 0, {"__riscv"}, {}, cache.dir); - ASSERT_TRUE(r.has_value()) << r.error(); - EXPECT_FALSE(r->mismatches.empty()) + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); + expect(!(r->mismatches.empty())) << "a probe with no target selection must not report the target's " "own predefines; if this passes, the argv is not reaching the " "compiler and the test above proves nothing"; -} +}}; diff --git a/tests/unit/test_cfg_accelerator.cpp b/tests/unit/test_cfg_accelerator.cpp index 953ebda48..891417eb6 100644 --- a/tests/unit/test_cfg_accelerator.cpp +++ b/tests/unit/test_cfg_accelerator.cpp @@ -1,8 +1,10 @@ -#include import std; +import mcpplibs.mtest; import mcpp.build.prepare_inputs; +using namespace mcpplibs::mtest; + namespace cfgpred = mcpp::build::cfgpred; namespace { @@ -34,34 +36,34 @@ bool m(std::string_view predicate, const cfgpred::Ctx& c) { // a = "rocm")` would then be unsatisfiable rather than "both are enabled". // Membership everywhere keeps `any`/`all`/`not` pure boolean combinators. -TEST(CfgAccelerator, BareKeyIsMembership) { +UTest bare_key_is_membership{"CfgAccelerator/BareKeyIsMembership", [] { auto cuda = resolved({"cuda"}); - EXPECT_TRUE (m(R"(cfg(accelerator = "cuda"))", cuda)); - EXPECT_FALSE(m(R"(cfg(accelerator = "rocm"))", cuda)); -} + expect(bool(m(R"(cfg(accelerator = "cuda"))", cuda))); + expect(!(m(R"(cfg(accelerator = "rocm"))", cuda))); +}}; -TEST(CfgAccelerator, MultipleBackendsAreAllMembers) { +UTest multiple_backends_are_all_members{"CfgAccelerator/MultipleBackendsAreAllMembers", [] { auto both = resolved({"cuda", "rocm"}); - EXPECT_TRUE(m(R"(cfg(accelerator = "cuda"))", both)); - EXPECT_TRUE(m(R"(cfg(accelerator = "rocm"))", both)); -} + expect(bool(m(R"(cfg(accelerator = "cuda"))", both))); + expect(bool(m(R"(cfg(accelerator = "rocm"))", both))); +}}; -TEST(CfgAccelerator, CombinatorsStayPure) { +UTest combinators_stay_pure{"CfgAccelerator/CombinatorsStayPure", [] { auto both = resolved({"cuda", "rocm"}); auto cuda = resolved({"cuda"}); - EXPECT_TRUE (m(R"(cfg(any(accelerator = "cuda", accelerator = "rocm")))", cuda)); - EXPECT_TRUE (m(R"(cfg(all(accelerator = "cuda", accelerator = "rocm")))", both)); - EXPECT_FALSE(m(R"(cfg(all(accelerator = "cuda", accelerator = "rocm")))", cuda)); - EXPECT_TRUE (m(R"(cfg(not(accelerator = "rocm")))", cuda)); -} + expect(bool(m(R"(cfg(any(accelerator = "cuda", accelerator = "rocm")))", cuda))); + expect(bool(m(R"(cfg(all(accelerator = "cuda", accelerator = "rocm")))", both))); + expect(!(m(R"(cfg(all(accelerator = "cuda", accelerator = "rocm")))", cuda))); + expect(bool(m(R"(cfg(not(accelerator = "rocm")))", cuda))); +}}; -TEST(CfgAccelerator, ComposesWithTripleKeys) { +UTest composes_with_triple_keys{"CfgAccelerator/ComposesWithTripleKeys", [] { auto cuda = resolved({"cuda"}); - EXPECT_TRUE (m(R"(cfg(all(linux, accelerator = "cuda")))", cuda)); - EXPECT_FALSE(m(R"(cfg(all(windows, accelerator = "cuda")))", cuda)); -} + expect(bool(m(R"(cfg(all(linux, accelerator = "cuda")))", cuda))); + expect(!(m(R"(cfg(all(windows, accelerator = "cuda")))", cuda))); +}}; -TEST(CfgAccelerator, AnsweredBeforeResolutionUnlikeTheOtherLayers) { +UTest answered_before_resolution_unlike_the_other_layers{"CfgAccelerator/AnsweredBeforeResolutionUnlikeTheOtherLayers", [] { // `accelerator` is answerable in the FIRST merge pass, and the difference // from the other five layer keys is the schedule, not the subject. Its // value is an input to the build -- `--accel`, or `[build] accel` -- read @@ -73,43 +75,43 @@ TEST(CfgAccelerator, AnsweredBeforeResolutionUnlikeTheOtherLayers) { // not be, and a CPU-only build of a project with a device island // downloaded the whole vendor toolkit. auto early = cfgpred::context_for("x86_64-linux-gnu"); - ASSERT_FALSE(early.layersKnown); + expect(fatal(!early.layersKnown)); early.accelerators = {"cuda"}; - EXPECT_TRUE (m(R"(cfg(accelerator = "cuda"))", early)); - EXPECT_FALSE(m(R"(cfg(accelerator = "vulkan"))", early)); + expect(bool(m(R"(cfg(accelerator = "cuda"))", early))); + expect(!(m(R"(cfg(accelerator = "vulkan"))", early))); // A resolved layer key in the same position still answers false, and must: // the second pass owns it and would otherwise contribute the same inputs // twice through `append()`. - EXPECT_FALSE(m(R"(cfg(c-abi = "glibc"))", early)); + expect(!(m(R"(cfg(c-abi = "glibc"))", early))); // No accel named: false for any backend, and `none` is how a section says // so without enumerating the backends it is not. auto nothing = cfgpred::context_for("x86_64-linux-gnu"); - EXPECT_FALSE(m(R"(cfg(accelerator = "cuda"))", nothing)); - EXPECT_TRUE (m(R"(cfg(accelerator = "none"))", nothing)); -} + expect(!(m(R"(cfg(accelerator = "cuda"))", nothing))); + expect(bool(m(R"(cfg(accelerator = "none"))", nothing))); +}}; -TEST(CfgAccelerator, IsInTheVocabularyButNotAResolvedLayer) { +UTest is_in_the_vocabulary_but_not_a_resolved_layer{"CfgAccelerator/IsInTheVocabularyButNotAResolvedLayer", [] { // Three separate questions, and only the middle one changed. It is a known // key (so a misspelling is still reported); it is not a triple key; and it // does NOT send its section to the pass that runs after resolution. - EXPECT_TRUE (cfgpred::unknown_tokens(R"(cfg(accelerator = "cuda"))").empty()); - EXPECT_FALSE(cfgpred::uses_layer(R"(cfg(accelerator = "cuda"))")); - EXPECT_FALSE(cfgpred::uses_layer(R"(cfg(arch = "x86_64"))")); - EXPECT_TRUE (cfgpred::uses_layer(R"(cfg(c-abi = "musl"))")); + expect(cfgpred::unknown_tokens(R"(cfg(accelerator = "cuda"))").empty()); + expect(!(cfgpred::uses_layer(R"(cfg(accelerator = "cuda"))"))); + expect(!(cfgpred::uses_layer(R"(cfg(arch = "x86_64"))"))); + expect(bool(cfgpred::uses_layer(R"(cfg(c-abi = "musl"))"))); // A predicate mixing the two belongs to the late pass, which can answer // both. Ownership is by membership, not by which leg matched. - EXPECT_TRUE (cfgpred::uses_layer( - R"(cfg(all(accelerator = "cuda", c-abi = "musl")))")); -} + expect(bool(cfgpred::uses_layer( + R"(cfg(all(accelerator = "cuda", c-abi = "musl")))"))); +}}; -TEST(CfgAccelerator, MisspellingIsReportedNotSilentlyFalse) { +UTest misspelling_is_reported_not_silently_false{"CfgAccelerator/MisspellingIsReportedNotSilentlyFalse", [] { // A predicate naming a token outside the vocabulary used to evaluate to // false in silence, which is indistinguishable from one that correctly did // not apply. auto unknown = cfgpred::unknown_tokens(R"(cfg(acclerator = "cuda"))"); - ASSERT_EQ(unknown.size(), 1u); - EXPECT_EQ(unknown[0], "acclerator"); - EXPECT_TRUE(cfgpred::unknown_tokens(R"(cfg(accelerator = "cuda"))").empty()); -} + expect(fatal(eq(unknown.size(), 1u))); + expect(eq(unknown[0], "acclerator")); + expect(cfgpred::unknown_tokens(R"(cfg(accelerator = "cuda"))").empty()); +}}; diff --git a/tests/unit/test_cfg_accelerator_none.cpp b/tests/unit/test_cfg_accelerator_none.cpp index 5c1f6e287..a03572b88 100644 --- a/tests/unit/test_cfg_accelerator_none.cpp +++ b/tests/unit/test_cfg_accelerator_none.cpp @@ -1,8 +1,10 @@ -#include import std; +import mcpplibs.mtest; import mcpp.build.prepare_inputs; +using namespace mcpplibs::mtest; + namespace cfgpred = mcpp::build::cfgpred; namespace { @@ -25,25 +27,25 @@ bool m(std::string_view predicate, const cfgpred::Ctx& c) { // package rather than an engine change. A fallback written by enumeration // therefore changes meaning the day a fifth backend exists, silently. -TEST(CfgAcceleratorNone, NoneIsTheEmptySet) { - EXPECT_TRUE (m(R"(cfg(accelerator = "none"))", with({}))); - EXPECT_FALSE(m(R"(cfg(accelerator = "none"))", with({"cuda"}))); - EXPECT_FALSE(m(R"(cfg(accelerator = "none"))", with({"cuda", "vulkan"}))); +UTest none_is_the_empty_set{"CfgAcceleratorNone/NoneIsTheEmptySet", [] { + expect(bool(m(R"(cfg(accelerator = "none"))", with({})))); + expect(!(m(R"(cfg(accelerator = "none"))", with({"cuda"})))); + expect(!(m(R"(cfg(accelerator = "none"))", with({"cuda", "vulkan"})))); // The negation is the other half a seam needs: "some device backend". - EXPECT_FALSE(m(R"(cfg(not(accelerator = "none")))", with({}))); - EXPECT_TRUE (m(R"(cfg(not(accelerator = "none")))", with({"vulkan"}))); -} + expect(!(m(R"(cfg(not(accelerator = "none")))", with({})))); + expect(bool(m(R"(cfg(not(accelerator = "none")))", with({"vulkan"})))); +}}; -TEST(CfgAcceleratorNone, NoneDoesNotDisturbMembership) { +UTest none_does_not_disturb_membership{"CfgAcceleratorNone/NoneDoesNotDisturbMembership", [] { // `none` must not become a member of the set it describes the emptiness // of, or `not(accelerator = "cuda")` would start answering for it. - EXPECT_TRUE (m(R"(cfg(accelerator = "cuda"))", with({"cuda"}))); - EXPECT_FALSE(m(R"(cfg(accelerator = "vulkan"))", with({"cuda"}))); + expect(bool(m(R"(cfg(accelerator = "cuda"))", with({"cuda"})))); + expect(!(m(R"(cfg(accelerator = "vulkan"))", with({"cuda"})))); // A build that named a backend literally spelled "none" is not a case this // engine has to serve; what matters is that the empty set stays the only // thing `none` reports, which the first test pins. -} +}}; // THE REASON THIS IS NOT A COSMETIC CHANGE. // @@ -51,19 +53,19 @@ TEST(CfgAcceleratorNone, NoneDoesNotDisturbMembership) { // added. That is the whole point, so the test simulates the addition rather // than describing it: `enumerated` is what a project wrote against the // two-backend world, `none` is what it should have written. -TEST(CfgAcceleratorNone, EnumerationRotsAndNoneDoesNot) { +UTest enumeration_rots_and_none_does_not{"CfgAcceleratorNone/EnumerationRotsAndNoneDoesNot", [] { constexpr auto enumerated = R"(cfg(not(any(accelerator = "cuda", accelerator = "vulkan"))))"; constexpr auto stable = R"(cfg(accelerator = "none"))"; // The world the fallback was written in: the two agree. - EXPECT_EQ(m(enumerated, with({})), m(stable, with({}))); - EXPECT_EQ(m(enumerated, with({"cuda"})), m(stable, with({"cuda"}))); + expect(eq(m(enumerated, with({})), m(stable, with({})))); + expect(eq(m(enumerated, with({"cuda"})), m(stable, with({"cuda"})))); // A fifth backend arrives. The enumeration now claims "no accelerator" for // a build that named one -- the CPU fallback would compile beside the // device implementation -- while `none` is unchanged. auto fifth = with({"ascend"}); - EXPECT_TRUE (m(enumerated, fifth)) << "the enumeration is expected to rot"; - EXPECT_FALSE(m(stable, fifth)) << "`none` must not rot"; -} + expect(bool(m(enumerated, fifth))) << "the enumeration is expected to rot"; + expect(!m(stable, fifth)) << "`none` must not rot"; +}}; diff --git a/tests/unit/test_cfg_os_only_platform.cpp b/tests/unit/test_cfg_os_only_platform.cpp index 7f1f0ea65..fbf76986c 100644 --- a/tests/unit/test_cfg_os_only_platform.cpp +++ b/tests/unit/test_cfg_os_only_platform.cpp @@ -1,8 +1,10 @@ -#include import std; +import mcpplibs.mtest; import mcpp.build.prepare_inputs; +using namespace mcpplibs::mtest; + // #630 item 7 — "a selector that names only an operating system is a // platform" (design record 2026-09-13-630 §8.2). `os_only_platforms` is the // function `mcpp emit xpkg` (`mcpp::pm::emit_xpkg`) asks to decide whether a @@ -27,109 +29,109 @@ bool has(const std::vector& v, std::string_view a) { // ── Positive: the seven OS-only forms ─────────────────────────────────────── -TEST(OsOnlyPlatforms, BareAliasWindows) { +UTest bare_alias_windows{"OsOnlyPlatforms/BareAliasWindows", [] { auto p = cfgpred::os_only_platforms("windows"); - EXPECT_EQ(p, std::vector({"windows"})); -} + expect(eq(p, std::vector({"windows"}))); +}}; -TEST(OsOnlyPlatforms, CfgWindows) { +UTest cfg_windows{"OsOnlyPlatforms/CfgWindows", [] { auto p = cfgpred::os_only_platforms("cfg(windows)"); - EXPECT_EQ(p, std::vector({"windows"})); -} + expect(eq(p, std::vector({"windows"}))); +}}; -TEST(OsOnlyPlatforms, CfgOsEqualsLinux) { +UTest cfg_os_equals_linux{"OsOnlyPlatforms/CfgOsEqualsLinux", [] { auto p = cfgpred::os_only_platforms(R"(cfg(os = "linux"))"); - EXPECT_EQ(p, std::vector({"linux"})); -} + expect(eq(p, std::vector({"linux"}))); +}}; -TEST(OsOnlyPlatforms, CfgLinux) { +UTest cfg_linux{"OsOnlyPlatforms/CfgLinux", [] { auto p = cfgpred::os_only_platforms("cfg(linux)"); - EXPECT_EQ(p, std::vector({"linux"})); -} + expect(eq(p, std::vector({"linux"}))); +}}; -TEST(OsOnlyPlatforms, CfgMacos) { +UTest cfg_macos{"OsOnlyPlatforms/CfgMacos", [] { auto p = cfgpred::os_only_platforms("cfg(macos)"); // The descriptor's spelling is `macosx`, not `macos` — see // `mcpp::pm::emit_xpkg`'s three blocks. - EXPECT_EQ(p, std::vector({"macosx"})); -} + expect(eq(p, std::vector({"macosx"}))); +}}; -TEST(OsOnlyPlatforms, CfgOsEqualsWindows) { +UTest cfg_os_equals_windows{"OsOnlyPlatforms/CfgOsEqualsWindows", [] { auto p = cfgpred::os_only_platforms(R"(cfg(os = "windows"))"); - EXPECT_EQ(p, std::vector({"windows"})); -} + expect(eq(p, std::vector({"windows"}))); +}}; -TEST(OsOnlyPlatforms, CfgOsEqualsMacos) { +UTest cfg_os_equals_macos{"OsOnlyPlatforms/CfgOsEqualsMacos", [] { auto p = cfgpred::os_only_platforms(R"(cfg(os = "macos"))"); - EXPECT_EQ(p, std::vector({"macosx"})); -} + expect(eq(p, std::vector({"macosx"}))); +}}; -TEST(OsOnlyPlatforms, CfgUnixNamesBothMacAndLinux) { +UTest cfg_unix_names_both_mac_and_linux{"OsOnlyPlatforms/CfgUnixNamesBothMacAndLinux", [] { // `unix` means `family == "unix"`, which macOS and Linux both satisfy — // the only one of the seven forms that names two blocks at once. auto p = cfgpred::os_only_platforms("cfg(unix)"); - EXPECT_EQ(p.size(), 2u); - EXPECT_TRUE(has(p, "linux")); - EXPECT_TRUE(has(p, "macosx")); - EXPECT_FALSE(has(p, "windows")); -} + expect(eq(p.size(), 2u)); + expect(bool(has(p, "linux"))); + expect(bool(has(p, "macosx"))); + expect(!has(p, "windows")); +}}; -TEST(OsOnlyPlatforms, BareAliasUnix) { +UTest bare_alias_unix{"OsOnlyPlatforms/BareAliasUnix", [] { auto p = cfgpred::os_only_platforms("unix"); - EXPECT_EQ(p.size(), 2u); - EXPECT_TRUE(has(p, "linux")); - EXPECT_TRUE(has(p, "macosx")); -} + expect(eq(p.size(), 2u)); + expect(bool(has(p, "linux"))); + expect(bool(has(p, "macosx"))); +}}; // ── Negative: everything the record says keeps the warning ───────────────── -TEST(OsOnlyPlatforms, ArchitectureIsNotOsOnly) { +UTest architecture_is_not_os_only{"OsOnlyPlatforms/ArchitectureIsNotOsOnly", [] { auto p = cfgpred::os_only_platforms(R"(cfg(target_arch = "aarch64"))"); - EXPECT_TRUE(p.empty()); -} + expect(p.empty()); +}}; -TEST(OsOnlyPlatforms, CombinatorOfOsAndArchIsNotOsOnly) { +UTest combinator_of_os_and_arch_is_not_os_only{"OsOnlyPlatforms/CombinatorOfOsAndArchIsNotOsOnly", [] { auto p = cfgpred::os_only_platforms( R"(cfg(all(linux, target_arch = "x86_64")))"); - EXPECT_TRUE(p.empty()); -} + expect(p.empty()); +}}; -TEST(OsOnlyPlatforms, NotWindowsIsNotOsOnly) { +UTest not_windows_is_not_os_only{"OsOnlyPlatforms/NotWindowsIsNotOsOnly", [] { // Syntactically this names only an OS, but `not(...)` answers an // unbounded set of platforms (everything this vocabulary does not yet // call "windows"), not the two blocks "linux and macosx" would be if this // were treated as their union. Kept conservative: any combinator // disqualifies, `not` included. auto p = cfgpred::os_only_platforms("cfg(not(windows))"); - EXPECT_TRUE(p.empty()); -} + expect(p.empty()); +}}; -TEST(OsOnlyPlatforms, EnvIsNotOsOnly) { +UTest env_is_not_os_only{"OsOnlyPlatforms/EnvIsNotOsOnly", [] { auto p = cfgpred::os_only_platforms(R"(cfg(env = "android"))"); - EXPECT_TRUE(p.empty()); -} + expect(p.empty()); +}}; -TEST(OsOnlyPlatforms, LayerKeyIsNotOsOnly) { +UTest layer_key_is_not_os_only{"OsOnlyPlatforms/LayerKeyIsNotOsOnly", [] { auto p = cfgpred::os_only_platforms(R"(cfg(compiler = "llvm"))"); - EXPECT_TRUE(p.empty()); -} + expect(p.empty()); +}}; -TEST(OsOnlyPlatforms, AnAllOsCombinatorIsStillNotOsOnly) { +UTest an_all_os_combinator_is_still_not_os_only{"OsOnlyPlatforms/AnAllOsCombinatorIsStillNotOsOnly", [] { // Even a combinator built ENTIRELY from OS terms is refused — see the // comment on `os_only_platforms` for why `any(linux, macos)` is not the // same claim as either block alone. auto p = cfgpred::os_only_platforms("cfg(any(linux, macos))"); - EXPECT_TRUE(p.empty()); -} + expect(p.empty()); +}}; -TEST(OsOnlyPlatforms, ExactTripleIsNotOsOnly) { +UTest exact_triple_is_not_os_only{"OsOnlyPlatforms/ExactTripleIsNotOsOnly", [] { // An exact triple shares no grammar with cfg()/the bare aliases; it names // one target, not a platform. auto p = cfgpred::os_only_platforms("x86_64-linux-musl"); - EXPECT_TRUE(p.empty()); -} + expect(p.empty()); +}}; -TEST(OsOnlyPlatforms, UnrecognisedOsValueIsNotOsOnly) { +UTest unrecognised_os_value_is_not_os_only{"OsOnlyPlatforms/UnrecognisedOsValueIsNotOsOnly", [] { auto p = cfgpred::os_only_platforms(R"(cfg(os = "freebsd"))"); - EXPECT_TRUE(p.empty()); -} + expect(p.empty()); +}}; diff --git a/tests/unit/test_cmdlimits.cpp b/tests/unit/test_cmdlimits.cpp index 8aaec1c67..be4963a2e 100644 --- a/tests/unit/test_cmdlimits.cpp +++ b/tests/unit/test_cmdlimits.cpp @@ -1,8 +1,10 @@ -#include import std; +import mcpplibs.mtest; import mcpp.build.cmdlimits; +using namespace mcpplibs::mtest; + // The command-length budget, as data. Seven builds have died because a command // outgrew a ceiling nobody tracked, and each was found by crashing at the end // of a build with an error that named neither the edge nor the cause. These @@ -15,89 +17,88 @@ namespace cl = mcpp::build::cmdlimits; namespace { -TEST(CmdLimits, EveryChannelIsInTheTable) { +UTest every_channel_is_in_the_table{"CmdLimits/EveryChannelIsInTheTable", [] { // A channel that is not in the table has no budget, which is exactly the // state that produced this defect family seven times. `limit_of` must find // a real row for each enumerator, not fall through to the placeholder. for (auto c : {cl::Channel::NinjaArgv, cl::Channel::PosixShell, cl::Channel::CmdWrapper, cl::Channel::RspContent, cl::Channel::RspLine}) { - EXPECT_EQ(cl::limit_of(c).channel, c); - EXPECT_FALSE(cl::limit_of(c).imposedBy.empty()); + expect(eq(cl::limit_of(c).channel, c)); + expect(!(cl::limit_of(c).imposedBy.empty())); } -} +}}; -TEST(CmdLimits, NumbersAreTheRealOnes) { +UTest numbers_are_the_real_ones{"CmdLimits/NumbersAreTheRealOnes", [] { // Each of these was learned the expensive way; a "tidier" number would be // wrong. MAX_ARG_STRLEN is 32 pages — NOT the 2 MiB ARG_MAX that #344 // spent a release believing in. - EXPECT_EQ(cl::limit_of(cl::Channel::PosixShell).bytes, 128u * 1024u); - EXPECT_EQ(cl::limit_of(cl::Channel::NinjaArgv).bytes, 32u * 1024u); - EXPECT_EQ(cl::limit_of(cl::Channel::CmdWrapper).bytes, 8191u); - EXPECT_EQ(cl::limit_of(cl::Channel::RspLine).bytes, 128u * 1024u); -} + expect(eq(cl::limit_of(cl::Channel::PosixShell).bytes, 128u * 1024u)); + expect(eq(cl::limit_of(cl::Channel::NinjaArgv).bytes, 32u * 1024u)); + expect(eq(cl::limit_of(cl::Channel::CmdWrapper).bytes, 8191u)); + expect(eq(cl::limit_of(cl::Channel::RspLine).bytes, 128u * 1024u)); +}}; -TEST(CmdLimits, EverySymptomIsRecorded) { +UTest every_symptom_is_recorded{"CmdLimits/EverySymptomIsRecorded", [] { // The costly part of this family has never been the fix — it has been // RECOGNISING it. `Argument list too long`, `LNK1170` and a bare 127 have // nothing in common, so each row has to carry what the user actually sees. for (auto c : {cl::Channel::NinjaArgv, cl::Channel::PosixShell, cl::Channel::CmdWrapper, cl::Channel::RspLine}) { auto const& l = cl::limit_of(c); - EXPECT_FALSE(l.symptom.empty()) << "channel has no symptom recorded"; - EXPECT_FALSE(l.remedy.empty()) << "channel has no remedy recorded"; + expect(!l.symptom.empty()) << "channel has no symptom recorded"; + expect(!l.remedy.empty()) << "channel has no remedy recorded"; } -} +}}; -TEST(CmdLimits, ShellIsTighterThanDirectSpawnOnWindows) { +UTest shell_is_tighter_than_direct_spawn_on_windows{"CmdLimits/ShellIsTighterThanDirectSpawnOnWindows", [] { // cmd.exe is a quarter of CreateProcess, which is why #261's redirection // (it forced a `cmd /c` wrapper) failed where the direct spawn would have // been fine. - EXPECT_LT(cl::inline_budget(/*win=*/true, /*shell=*/true), - cl::inline_budget(/*win=*/true, /*shell=*/false)); -} + expect(lt(cl::inline_budget(/*win=*/true, /*shell=*/true), cl::inline_budget(/*win=*/true, /*shell=*/false))); +}}; -TEST(CmdLimits, UnderBudgetPasses) { - EXPECT_FALSE(cl::check_inline(std::string(1000, 'x'), false, true)); - EXPECT_FALSE(cl::check_inline(std::string(1000, 'x'), true, false)); -} +UTest under_budget_passes{"CmdLimits/UnderBudgetPasses", [] { + expect(!(cl::check_inline(std::string(1000, 'x'), false, true))); + expect(!(cl::check_inline(std::string(1000, 'x'), true, false))); +}}; -TEST(CmdLimits, OverBudgetIsReportedWithBothNumbers) { +UTest over_budget_is_reported_with_both_numbers{"CmdLimits/OverBudgetIsReportedWithBothNumbers", [] { // POSIX: ninja wraps in `sh -c`, so the whole command is one argv entry. auto over = cl::check_inline(std::string(200u * 1024u, 'x'), false, true); - ASSERT_TRUE(over); - EXPECT_EQ(over->channel, cl::Channel::PosixShell); - EXPECT_EQ(over->actual, 200u * 1024u); - EXPECT_EQ(over->allowed, 128u * 1024u); -} + expect(fatal(bool(over))); + expect(eq(over->channel, cl::Channel::PosixShell)); + expect(eq(over->actual, 200u * 1024u)); + expect(eq(over->allowed, 128u * 1024u)); +}}; -TEST(CmdLimits, WindowsPicksTheChannelItActuallyTravels) { +UTest windows_picks_the_channel_it_actually_travels{"CmdLimits/WindowsPicksTheChannelItActuallyTravels", [] { // 50 781 bytes is #274's real measurement (FFmpeg's 2281 units): fine for // CreateProcess, fatal through cmd.exe. const std::string cmd(50781, 'x'); auto direct = cl::check_inline(cmd, /*win=*/true, /*shell=*/false); auto shell = cl::check_inline(cmd, /*win=*/true, /*shell=*/true); - ASSERT_TRUE(direct); - EXPECT_EQ(direct->channel, cl::Channel::NinjaArgv); - ASSERT_TRUE(shell); - EXPECT_EQ(shell->channel, cl::Channel::CmdWrapper); -} + expect(fatal(bool(direct))); + expect(eq(direct->channel, cl::Channel::NinjaArgv)); + expect(fatal(bool(shell))); + expect(eq(shell->channel, cl::Channel::CmdWrapper)); +}}; -TEST(CmdLimits, DiagnosticNamesTheEdgeTheNumbersAndTheWayOut) { +UTest diagnostic_names_the_edge_the_numbers_and_the_way_out{"CmdLimits/DiagnosticNamesTheEdgeTheNumbersAndTheWayOut", [] { // Every underlying error omits the edge; that omission is what made these // expensive. The message must supply it, plus enough to act without // reading the source. auto over = cl::check_inline(std::string(200u * 1024u, 'x'), false, true); - ASSERT_TRUE(over); + expect(fatal(bool(over))); const std::string msg = cl::explain("build edge 'bin/app' (rule cxx_link)", *over); - EXPECT_NE(msg.find("bin/app"), std::string::npos); - EXPECT_NE(msg.find("cxx_link"), std::string::npos); - EXPECT_NE(msg.find("204800"), std::string::npos); // actual - EXPECT_NE(msg.find("131072"), std::string::npos); // allowed - EXPECT_NE(msg.find("MAX_ARG_STRLEN"), std::string::npos); - EXPECT_NE(msg.find("response file"), std::string::npos); - EXPECT_NE(msg.find("2026-08-06-command-length-architecture"), std::string::npos); -} + expect(ne(msg.find("bin/app"), std::string::npos)); + expect(ne(msg.find("cxx_link"), std::string::npos)); + expect(ne(msg.find("204800"), std::string::npos)); // actual + expect(ne(msg.find("131072"), std::string::npos)); // allowed + expect(ne(msg.find("MAX_ARG_STRLEN"), std::string::npos)); + expect(ne(msg.find("response file"), std::string::npos)); + expect(ne(msg.find("2026-08-06-command-length-architecture"), std::string::npos)); +}}; } // namespace diff --git a/tests/unit/test_coff_exports.cpp b/tests/unit/test_coff_exports.cpp index f886cd6d4..1a7dd03fc 100644 --- a/tests/unit/test_coff_exports.cpp +++ b/tests/unit/test_coff_exports.cpp @@ -1,8 +1,10 @@ -#include import std; +import mcpplibs.mtest; import mcpp.build.coff_exports; +using namespace mcpplibs::mtest; + using mcpp::build::coff::Export; using mcpp::build::coff::read_exports; using mcpp::build::coff::write_def; @@ -91,76 +93,76 @@ std::vector names(const std::vector& v) { // ─── what is exported ────────────────────────────────────────────────────── -TEST(CoffExports, TakesExternalDefinedSymbols) { +UTest takes_external_defined_symbols{"CoffExports/TakesExternalDefinedSymbols", [] { auto obj = make_obj({ Sym{ .name = "f" } }); auto r = read_exports(obj); - ASSERT_TRUE(r) << r.error(); - EXPECT_EQ(names(*r), (std::vector{ "f" })); -} + if (auto&& failed = expect(fatal(bool(r))); !failed) failed << r.error(); + expect(eq(names(*r), (std::vector{ "f" }))); +}}; -TEST(CoffExports, SkipsStaticSymbols) { +UTest skips_static_symbols{"CoffExports/SkipsStaticSymbols", [] { // IMAGE_SYM_CLASS_STATIC (3). Exporting a TU-local symbol would put a name // in the DLL's surface that the author never made public. auto obj = make_obj({ Sym{ .name = "s", .storage = 3 } }); auto r = read_exports(obj); - ASSERT_TRUE(r); - EXPECT_TRUE(r->empty()); -} + expect(fatal(bool(r))); + expect(r->empty()); +}}; -TEST(CoffExports, SkipsUndefinedSymbols) { +UTest skips_undefined_symbols{"CoffExports/SkipsUndefinedSymbols", [] { // SectionNumber 0 is UNDEFINED — a symbol this object REFERENCES. Exporting // it would have the DLL claim to provide what it is asking someone else for. auto obj = make_obj({ Sym{ .name = "u", .section = 0 } }); auto r = read_exports(obj); - ASSERT_TRUE(r); - EXPECT_TRUE(r->empty()); -} + expect(fatal(bool(r))); + expect(r->empty()); +}}; -TEST(CoffExports, SkipsAbsoluteAndDebugSections) { +UTest skips_absolute_and_debug_sections{"CoffExports/SkipsAbsoluteAndDebugSections", [] { // -1 IMAGE_SYM_ABSOLUTE, -2 IMAGE_SYM_DEBUG: neither has an address to // export. auto obj = make_obj({ Sym{ .name = "a", .section = -1 }, Sym{ .name = "d", .section = -2 } }); auto r = read_exports(obj); - ASSERT_TRUE(r); - EXPECT_TRUE(r->empty()); -} + expect(fatal(bool(r))); + expect(r->empty()); +}}; // ─── DATA, which is the half that fails silently ─────────────────────────── -TEST(CoffExports, AVariableIsMarkedDATA) { +UTest a_variable_is_marked_data{"CoffExports/AVariableIsMarkedDATA", [] { // Without `DATA` the linker generates a call thunk for a variable and the // consumer reads the thunk instead of the value. auto obj = make_obj({ Sym{ .name = "v", .section = 2, .type = 0 } }, { kText, kData }); auto r = read_exports(obj); - ASSERT_TRUE(r); - ASSERT_EQ(r->size(), 1u); - EXPECT_TRUE((*r)[0].data); -} + expect(fatal(bool(r))); + expect(fatal(eq(r->size(), 1u))); + expect(bool((*r)[0].data)); +}}; -TEST(CoffExports, AConstInAReadOnlySectionIsStillDATA) { +UTest a_const_in_a_read_only_section_is_still_data{"CoffExports/AConstInAReadOnlySectionIsStillDATA", [] { // `.rdata` is not writable, so a rule keyed on MEM_WRITE alone calls this a // function. It is a variable, and consumers read it as one. auto obj = make_obj({ Sym{ .name = "c", .section = 2, .type = 0 } }, { kText, kRdata }); auto r = read_exports(obj); - ASSERT_TRUE(r); - ASSERT_EQ(r->size(), 1u); - EXPECT_TRUE((*r)[0].data); -} + expect(fatal(bool(r))); + expect(fatal(eq(r->size(), 1u))); + expect(bool((*r)[0].data)); +}}; -TEST(CoffExports, AFunctionIsNotDATA) { +UTest a_function_is_not_data{"CoffExports/AFunctionIsNotDATA", [] { auto obj = make_obj({ Sym{ .name = "f", .section = 1, .type = 0x20 } }, { kText }); auto r = read_exports(obj); - ASSERT_TRUE(r); - ASSERT_EQ(r->size(), 1u); - EXPECT_FALSE((*r)[0].data); -} + expect(fatal(bool(r))); + expect(fatal(eq(r->size(), 1u))); + expect(!((*r)[0].data)); +}}; // ─── the skip list, each entry for its own reason ────────────────────────── -TEST(CoffExports, SkipsDestructorThunksAndManagedArtefacts) { +UTest skips_destructor_thunks_and_managed_artefacts{"CoffExports/SkipsDestructorThunksAndManagedArtefacts", [] { auto obj = make_obj({ Sym{ .name = "??_Gx" }, // scalar-deleting destructor Sym{ .name = "??_Ex" }, // vector-deleting destructor @@ -168,11 +170,11 @@ TEST(CoffExports, SkipsDestructorThunksAndManagedArtefacts) { Sym{ .name = "keep" }, }); auto r = read_exports(obj); - ASSERT_TRUE(r); - EXPECT_EQ(names(*r), (std::vector{ "keep" })); -} + expect(fatal(bool(r))); + expect(eq(names(*r), (std::vector{ "keep" }))); +}}; -TEST(CoffExports, SkipsAuxiliaryRecords) { +UTest skips_auxiliary_records{"CoffExports/SkipsAuxiliaryRecords", [] { // An aux record follows its symbol and is NOT a symbol. Reading one as a // symbol yields a name made of raw section data — which is how a `.def` // ends up with entries no linker can resolve. @@ -180,39 +182,39 @@ TEST(CoffExports, SkipsAuxiliaryRecords) { Sym{ .name = "\x01\x02\x03" }, // the aux record Sym{ .name = "after" } }); auto r = read_exports(obj); - ASSERT_TRUE(r); - EXPECT_EQ(names(*r), (std::vector{ "after", "fn" })); -} + expect(fatal(bool(r))); + expect(eq(names(*r), (std::vector{ "after", "fn" }))); +}}; -TEST(CoffExports, StripsTheLeadingUnderscoreOnI386Only) { +UTest strips_the_leading_underscore_on_i386_only{"CoffExports/StripsTheLeadingUnderscoreOnI386Only", [] { auto i386 = make_obj({ Sym{ .name = "_f" } }, { kText }, 0x014c); auto r32 = read_exports(i386); - ASSERT_TRUE(r32); - EXPECT_EQ(names(*r32), (std::vector{ "f" })); + expect(fatal(bool(r32))); + expect(eq(names(*r32), (std::vector{ "f" }))); // On amd64 a leading underscore is part of the name. auto amd64 = make_obj({ Sym{ .name = "_f" } }, { kText }, 0x8664); auto r64 = read_exports(amd64); - ASSERT_TRUE(r64); - EXPECT_EQ(names(*r64), (std::vector{ "_f" })); -} + expect(fatal(bool(r64))); + expect(eq(names(*r64), (std::vector{ "_f" }))); +}}; // ─── refusals, because a partial answer here ships a broken DLL ──────────── -TEST(CoffExports, RefusesAnUnsupportedMachine) { +UTest refuses_an_unsupported_machine{"CoffExports/RefusesAnUnsupportedMachine", [] { auto obj = make_obj({ Sym{ .name = "f" } }, { kText }, 0x5032 /* RISC-V */); auto r = read_exports(obj); - ASSERT_FALSE(r); - EXPECT_NE(r.error().find("machine"), std::string::npos); -} + expect(fatal(!r)); + expect(ne(r.error().find("machine"), std::string::npos)); +}}; -TEST(CoffExports, RefusesATruncatedSymbolTable) { +UTest refuses_a_truncated_symbol_table{"CoffExports/RefusesATruncatedSymbolTable", [] { auto obj = make_obj({ Sym{ .name = "f" }, Sym{ .name = "g" } }); obj.resize(obj.size() - 20); // cut into the symbol table auto r = read_exports(obj); - ASSERT_FALSE(r) << "a truncated table must not read as a short one"; - EXPECT_NE(r.error().find("past the end"), std::string::npos); -} + expect(fatal(!r)) << "a truncated table must not read as a short one"; + expect(ne(r.error().find("past the end"), std::string::npos)); +}}; // ─── anonymous objects (2026.10.5.2) ──────────────────────────────────────── // @@ -268,7 +270,7 @@ std::vector make_bigobj(std::vector syms, } // namespace -TEST(CoffExports, ReadsABigobjObjectAsTheOrdinaryOneWithTheSameSymbols) { +UTest reads_a_bigobj_object_as_the_ordinary_one_with_the_same_symbols{"CoffExports/ReadsABigobjObjectAsTheOrdinaryOneWithTheSameSymbols", [] { const std::vector syms = { Sym{ .name = "f" }, Sym{ .name = "v", .section = 2, .type = 0 }, Sym{ .name = "s", .storage = 3 }, Sym{ .name = "u", .section = 0 }, @@ -276,65 +278,64 @@ TEST(CoffExports, ReadsABigobjObjectAsTheOrdinaryOneWithTheSameSymbols) { }; auto small = read_exports(make_obj(syms, { kText, kData })); auto big = read_exports(make_bigobj(syms, { kText, kData })); - ASSERT_TRUE(small) << small.error(); - ASSERT_TRUE(big) << big.error(); - EXPECT_EQ(*big, *small); - EXPECT_EQ(names(*big), (std::vector{ "f", "g", "h", "v" })); -} + if (auto&& failed = expect(fatal(bool(small))); !failed) failed << small.error(); + if (auto&& failed = expect(fatal(bool(big))); !failed) failed << big.error(); + expect(eq(*big, *small)); + expect(eq(names(*big), (std::vector{ "f", "g", "h", "v" }))); +}}; -TEST(CoffExports, NamesAGlObjectAndSaysWhatToDo) { +UTest names_a_gl_object_and_says_what_to_do{"CoffExports/NamesAGlObjectAndSaysWhatToDo", [] { auto r = read_exports(anonymous(1, kClGl)); - ASSERT_FALSE(r); - EXPECT_NE(r.error().find("/GL"), std::string::npos) << r.error(); - EXPECT_EQ(r.error().find("bigobj"), std::string::npos) << r.error(); - EXPECT_NE(r.error().find("windows_auto_export = false"), std::string::npos); - EXPECT_NE(r.error().find("dllexport"), std::string::npos); - EXPECT_FALSE(declares_exports(anonymous(1, kClGl))); -} - -TEST(CoffExports, NamesAnImportObjectAndAnUnknownAnonymousObject) { + expect(fatal(!r)); + if (auto&& failed = expect(ne(r.error().find("/GL"), std::string::npos)); !failed) failed << r.error(); + if (auto&& failed = expect(eq(r.error().find("bigobj"), std::string::npos)); !failed) failed << r.error(); + expect(ne(r.error().find("windows_auto_export = false"), std::string::npos)); + expect(ne(r.error().find("dllexport"), std::string::npos)); + expect(!(declares_exports(anonymous(1, kClGl)))); +}}; + +UTest names_an_import_object_and_an_unknown_anonymous_object{"CoffExports/NamesAnImportObjectAndAnUnknownAnonymousObject", [] { auto imp = read_exports(anonymous(0, {})); - ASSERT_FALSE(imp); - EXPECT_NE(imp.error().find("import object"), std::string::npos) << imp.error(); + expect(fatal(!imp)); + if (auto&& failed = expect(ne(imp.error().find("import object"), std::string::npos)); !failed) failed << imp.error(); auto other = read_exports(anonymous(3, {})); - ASSERT_FALSE(other); - EXPECT_NE(other.error().find("anonymous"), std::string::npos) << other.error(); -} + expect(fatal(!other)); + if (auto&& failed = expect(ne(other.error().find("anonymous"), std::string::npos)); !failed) failed << other.error(); +}}; -TEST(CoffExports, RefusesABigobjObjectWhoseCountsOverrunIt) { +UTest refuses_a_bigobj_object_whose_counts_overrun_it{"CoffExports/RefusesABigobjObjectWhoseCountsOverrunIt", [] { auto obj = make_bigobj({ Sym{ .name = "f" } }); put32(obj, 44, 0x00ffffffu); // sections no file of this size can hold auto r = read_exports(obj); - ASSERT_FALSE(r); - EXPECT_NE(r.error().find("past the end"), std::string::npos) << r.error(); -} + expect(fatal(!r)); + if (auto&& failed = expect(ne(r.error().find("past the end"), std::string::npos)); !failed) failed << r.error(); +}}; -TEST(CoffExports, RefusesSomethingThatIsNotAnObject) { +UTest refuses_something_that_is_not_an_object{"CoffExports/RefusesSomethingThatIsNotAnObject", [] { std::vector tiny(4, std::byte{0}); - EXPECT_FALSE(read_exports(tiny)); -} + expect(!read_exports(tiny)); +}}; // ─── the .def text ───────────────────────────────────────────────────────── -TEST(CoffExports, DefIsSortedDeduplicatedAndMarksData) { +UTest def_is_sorted_deduplicated_and_marks_data{"CoffExports/DefIsSortedDeduplicatedAndMarksData", [] { auto def = write_def("mathkit.dll", { { "zeta", false }, { "alpha", true }, { "zeta", false } }); - EXPECT_EQ(def, - "LIBRARY mathkit.dll\n" + expect(eq(def, "LIBRARY mathkit.dll\n" "EXPORTS\n" " alpha DATA\n" - " zeta\n"); -} + " zeta\n")); +}}; -TEST(CoffExports, TheExportCeilingIsPesNotOurs) { +UTest the_export_ceiling_is_pes_not_ours{"CoffExports/TheExportCeilingIsPesNotOurs", [] { // Stated as a constant a reader can check against the format, not as a // number chosen here: PE addresses exports by 16-bit ordinal. - EXPECT_EQ(kMaxExports, 65535u); -} + expect(eq(kMaxExports, 65535u)); +}}; // ─── a real object, from a real compiler ─────────────────────────────────── -TEST(CoffExports, ReadsARealMingwObject) { +UTest reads_a_real_mingw_object{"CoffExports/ReadsARealMingwObject", [] { // Committed fixture: macOS and Windows CI cannot produce a COFF object, and // a byte-level reader that is only ever fed its own test's output is exactly // the kind that agrees with itself and with nothing else. @@ -347,31 +348,31 @@ TEST(CoffExports, ReadsARealMingwObject) { for (char c; in.get(c); ) bytes.push_back(static_cast(c)); auto r = read_exports(bytes); - ASSERT_TRUE(r) << r.error(); + if (auto&& failed = expect(fatal(bool(r))); !failed) failed << r.error(); auto got = names(*r); // From `x86_64-w64-mingw32-nm --defined-only --extern-only` on the same // object; see tests/fixtures/coff/README.md. - EXPECT_NE(std::ranges::find(got, "exported_fn"), got.end()); - EXPECT_NE(std::ranges::find(got, "exported_data"), got.end()); - EXPECT_NE(std::ranges::find(got, "exported_const"), got.end()); - EXPECT_NE(std::ranges::find(got, "_ZN2ns10mangled_fnEi"), got.end()); + expect(ne(std::ranges::find(got, "exported_fn"), got.end())); + expect(ne(std::ranges::find(got, "exported_data"), got.end())); + expect(ne(std::ranges::find(got, "exported_const"), got.end())); + expect(ne(std::ranges::find(got, "_ZN2ns10mangled_fnEi"), got.end())); // `static` at namespace scope is internal linkage: not in the surface. - EXPECT_EQ(std::ranges::find(got, "_ZL9hidden_fni"), got.end()); + expect(eq(std::ranges::find(got, "_ZL9hidden_fni"), got.end())); for (auto const& e : *r) { if (e.name == "exported_data" || e.name == "exported_const") - EXPECT_TRUE(e.data) << e.name << " is a variable"; - if (e.name == "exported_fn") EXPECT_FALSE(e.data); + expect(bool(e.data)) << e.name << " is a variable"; + if (e.name == "exported_fn") expect(!e.data); } return; } - GTEST_SKIP() << "fixture not reachable from this working directory"; -} + skip("fixture not reachable from this working directory"); +}}; // ─── annotation wins ─────────────────────────────────────────────────────── -TEST(CoffExports, RecognisesAnObjectThatAlreadyDeclaresExports) { +UTest recognises_an_object_that_already_declares_exports{"CoffExports/RecognisesAnObjectThatAlreadyDeclaresExports", [] { // `__declspec(dllexport)` makes the compiler write `/EXPORT:` directives // into `.drectve`. Generating a `.def` on top of that would export the same // names twice (LNK4197) AND export everything else besides, replacing a @@ -387,13 +388,13 @@ TEST(CoffExports, RecognisesAnObjectThatAlreadyDeclaresExports) { if (!in) continue; std::vector bytes; for (char c; in.get(c); ) bytes.push_back(static_cast(c)); - EXPECT_TRUE(declares_exports(bytes)); + expect(bool(declares_exports(bytes))); return; } - GTEST_SKIP() << "fixture not reachable from this working directory"; -} + skip("fixture not reachable from this working directory"); +}}; -TEST(CoffExports, AnObjectWithNoDirectivesDeclaresNothing) { +UTest an_object_with_no_directives_declares_nothing{"CoffExports/AnObjectWithNoDirectivesDeclaresNothing", [] { // The negative half: without it, a `declares_exports` that always said true // would pass the test above and silently disable auto-export everywhere. for (auto const& base : { "tests/fixtures/coff/probe-amd64.obj", @@ -403,12 +404,12 @@ TEST(CoffExports, AnObjectWithNoDirectivesDeclaresNothing) { if (!in) continue; std::vector bytes; for (char c; in.get(c); ) bytes.push_back(static_cast(c)); - EXPECT_FALSE(declares_exports(bytes)); + expect(!declares_exports(bytes)); return; } - GTEST_SKIP() << "fixture not reachable from this working directory"; -} + skip("fixture not reachable from this working directory"); +}}; -TEST(CoffExports, SyntheticObjectsHaveNoDirectiveSection) { - EXPECT_FALSE(declares_exports(make_obj({ Sym{ .name = "f" } }))); -} +UTest synthetic_objects_have_no_directive_section{"CoffExports/SyntheticObjectsHaveNoDirectiveSection", [] { + expect(!(declares_exports(make_obj({ Sym{ .name = "f" } })))); +}}; diff --git a/tests/unit/test_command_words.cpp b/tests/unit/test_command_words.cpp index da91a5cb9..8709c6601 100644 --- a/tests/unit/test_command_words.cpp +++ b/tests/unit/test_command_words.cpp @@ -1,8 +1,10 @@ -#include import std; +import mcpplibs.mtest; import mcpp.build.command_line; +using namespace mcpplibs::mtest; + // The two readers of a command's words: a POSIX shell's, for the link flags as // ninja runs them, and a response file's, as GCC, Clang and their linkers read // one (`@`). @@ -11,23 +13,22 @@ using words = std::vector; using mcpp::build::command_line::response_file_words; using mcpp::build::command_line::shell_words; -TEST(CommandWords, AShellReadsQuotesAndBackslashes) { - EXPECT_EQ(shell_words(" -o 'a b' \"c d\" e\\ f "), (words{"-o", "a b", "c d", "e f"})); - EXPECT_EQ(shell_words("'a\\b'"), (words{"a\\b"})); - EXPECT_EQ(shell_words("\"a\\b\\\"c\""), (words{"a\\b\"c"})); - EXPECT_EQ(shell_words("'it'\\''s'"), (words{"it's"})); - EXPECT_EQ(shell_words("a\\\nb"), (words{"ab"})); - EXPECT_EQ(shell_words("''"), (words{""})); - EXPECT_EQ(shell_words(" \t\n"), (words{})); -} +UTest a_shell_reads_quotes_and_backslashes{"CommandWords/AShellReadsQuotesAndBackslashes", [] { + expect(eq(shell_words(" -o 'a b' \"c d\" e\\ f "), (words{"-o", "a b", "c d", "e f"}))); + expect(eq(shell_words("'a\\b'"), (words{"a\\b"}))); + expect(eq(shell_words("\"a\\b\\\"c\""), (words{"a\\b\"c"}))); + expect(eq(shell_words("'it'\\''s'"), (words{"it's"}))); + expect(eq(shell_words("a\\\nb"), (words{"ab"}))); + expect(eq(shell_words("''"), (words{""}))); + expect(eq(shell_words(" \t\n"), (words{}))); +}}; -TEST(CommandWords, AResponseFileTakesTheCharacterAfterEveryBackslash) { - EXPECT_EQ(response_file_words("/a/lib.a\n\"/b c/lib.a\"\r\n'x\\'y'"), - (words{"/a/lib.a", "/b c/lib.a", "x'y"})); - EXPECT_EQ(response_file_words("a\\ b \"c\\\\d\""), (words{"a b", "c\\d"})); -} +UTest a_response_file_takes_the_character_after_every_backslash{"CommandWords/AResponseFileTakesTheCharacterAfterEveryBackslash", [] { + expect(eq(response_file_words("/a/lib.a\n\"/b c/lib.a\"\r\n'x\\'y'"), (words{"/a/lib.a", "/b c/lib.a", "x'y"}))); + expect(eq(response_file_words("a\\ b \"c\\\\d\""), (words{"a b", "c\\d"}))); +}}; -TEST(CommandWords, AWordQuotedForAShellReadsBackAsItself) { +UTest a_word_quoted_for_a_shell_reads_back_as_itself{"CommandWords/AWordQuotedForAShellReadsBackAsItself", [] { for (std::string w : {"plain", "a b", "it's", "a\\b", "$x", "\"q\"", ""}) - EXPECT_EQ(shell_words(mcpp::build::command_line::quote_posix(w)), words{w}) << w; -} + expect(eq(shell_words(mcpp::build::command_line::quote_posix(w)), words{w})) << w; +}}; diff --git a/tests/unit/test_compile_commands.cpp b/tests/unit/test_compile_commands.cpp index e2a159080..589e2ab72 100644 --- a/tests/unit/test_compile_commands.cpp +++ b/tests/unit/test_compile_commands.cpp @@ -1,6 +1,6 @@ -#include import std; +import mcpplibs.mtest; import mcpp.source_kind; import mcpp.build.compile_commands; import mcpp.build.flags; @@ -10,6 +10,8 @@ import mcpp.manifest.flag_words; import mcpplibs.os.platform; import mcpp.toolchain.model; +using namespace mcpplibs::mtest; + using namespace mcpp::build; namespace { @@ -64,8 +66,8 @@ std::string cdb(std::initializer_list entries) { // (merge_compile_commands short-circuits to `fresh` on a discarded parse), // so the fixture itself is checked here rather than trusted. Tests that // WANT malformed input pass it directly, not through this builder. - EXPECT_FALSE(nlohmann::json::parse(s, nullptr, /*allow_exceptions=*/false) - .is_discarded()) << s; + expect(!(nlohmann::json::parse(s, nullptr, /*allow_exceptions=*/false) + .is_discarded())) << s; return s; } @@ -80,7 +82,7 @@ std::size_t count(std::string_view hay, std::string_view needle) { // The core regression guard: a plain `mcpp build` regenerates the CDB from a // plan that lacks test files, but it must NOT wipe out test entries a prior // `mcpp test` wrote — clangd would lose all completion in tests/ otherwise. -TEST(CompileCommandsMerge, PreservesPriorEntriesForFilesNotInFreshPlan) { +UTest preserves_prior_entries_for_files_not_in_fresh_plan{"CompileCommandsMerge/PreservesPriorEntriesForFilesNotInFreshPlan", [] { auto fresh = cdb({ entry("/p/src/main.cpp", "-O2-FRESH") }); auto existing = cdb({ entry("/p/src/main.cpp", "-O0-STALE"), entry("/p/tests/test_smoke.cpp", "-Igtest") }); @@ -89,16 +91,16 @@ TEST(CompileCommandsMerge, PreservesPriorEntriesForFilesNotInFreshPlan) { fresh, existing, [](const std::filesystem::path&) { return true; }); // main.cpp takes the fresh plan's flags, not the stale prior ones. - EXPECT_NE(merged.find("-O2-FRESH"), std::string::npos) << merged; - EXPECT_EQ(merged.find("-O0-STALE"), std::string::npos) << merged; + expect(ne(merged.find("-O2-FRESH"), std::string::npos)) << merged; + expect(eq(merged.find("-O0-STALE"), std::string::npos)) << merged; // The test entry, absent from the fresh plan, is preserved. - EXPECT_NE(merged.find("tests/test_smoke.cpp"), std::string::npos) << merged; - EXPECT_NE(merged.find("-Igtest"), std::string::npos) << merged; -} + expect(ne(merged.find("tests/test_smoke.cpp"), std::string::npos)) << merged; + expect(ne(merged.find("-Igtest"), std::string::npos)) << merged; +}}; // Prior entries for files that no longer exist on disk must be pruned, so the // CDB never accumulates dead references (e.g. a deleted test file). -TEST(CompileCommandsMerge, PrunesPriorEntriesWhoseFileNoLongerExists) { +UTest prunes_prior_entries_whose_file_no_longer_exists{"CompileCommandsMerge/PrunesPriorEntriesWhoseFileNoLongerExists", [] { auto fresh = cdb({ entry("/p/src/main.cpp", "-O2") }); auto existing = cdb({ entry("/p/tests/deleted.cpp", "-Igtest") }); @@ -106,21 +108,21 @@ TEST(CompileCommandsMerge, PrunesPriorEntriesWhoseFileNoLongerExists) { fresh, existing, [](const std::filesystem::path& p) { return p != "/p/tests/deleted.cpp"; }); - EXPECT_EQ(merged.find("deleted.cpp"), std::string::npos) << merged; -} + expect(eq(merged.find("deleted.cpp"), std::string::npos)) << merged; +}}; // Exactly one entry per file: the fresh plan wins, no duplicate accrues. -TEST(CompileCommandsMerge, FreshEntryWinsAndNoDuplicatePerFile) { +UTest fresh_entry_wins_and_no_duplicate_per_file{"CompileCommandsMerge/FreshEntryWinsAndNoDuplicatePerFile", [] { auto fresh = cdb({ entry("/p/a.cpp", "-FRESH") }); auto existing = cdb({ entry("/p/a.cpp", "-STALE") }); auto merged = merge_compile_commands( fresh, existing, [](const std::filesystem::path&) { return true; }); - EXPECT_EQ(count(merged, "/p/a.cpp"), 1u) << merged; - EXPECT_NE(merged.find("-FRESH"), std::string::npos) << merged; - EXPECT_EQ(merged.find("-STALE"), std::string::npos) << merged; -} + expect(eq(count(merged, "/p/a.cpp"), 1u)) << merged; + expect(ne(merged.find("-FRESH"), std::string::npos)) << merged; + expect(eq(merged.find("-STALE"), std::string::npos)) << merged; +}}; // §3.2 item 1: the within-configuration merge's identity is `file` resolved // against `directory`, not `file` alone. A prior entry with a RELATIVE `file` @@ -128,7 +130,7 @@ TEST(CompileCommandsMerge, FreshEntryWinsAndNoDuplicatePerFile) { // the standard-library units always have one, §3.3) must still be recognized // as the SAME file the fresh plan already covers, so it is dropped rather // than kept as a spurious duplicate. -TEST(CompileCommandsMerge, ResolvesFileAgainstDirectoryForTheDedupKey) { +UTest resolves_file_against_directory_for_the_dedup_key{"CompileCommandsMerge/ResolvesFileAgainstDirectoryForTheDedupKey", [] { auto fresh = cdb({ entry("/p/src/main.cpp", "-FRESH") }); nlohmann::json e; e["directory"] = "/p"; @@ -140,42 +142,42 @@ TEST(CompileCommandsMerge, ResolvesFileAgainstDirectoryForTheDedupKey) { auto merged = merge_compile_commands( fresh, existing, [](const std::filesystem::path&) { return true; }); - EXPECT_EQ(count(merged, "main.cpp"), 1u) << merged; - EXPECT_NE(merged.find("-FRESH"), std::string::npos) << merged; - EXPECT_EQ(merged.find("-STALE"), std::string::npos) << merged; -} + expect(eq(count(merged, "main.cpp"), 1u)) << merged; + expect(ne(merged.find("-FRESH"), std::string::npos)) << merged; + expect(eq(merged.find("-STALE"), std::string::npos)) << merged; +}}; // A malformed prior CDB must never break generation: fall back to fresh. -TEST(CompileCommandsMerge, MalformedExistingFallsBackToFresh) { +UTest malformed_existing_falls_back_to_fresh{"CompileCommandsMerge/MalformedExistingFallsBackToFresh", [] { auto fresh = cdb({ entry("/p/src/main.cpp", "-O2") }); auto merged = merge_compile_commands( fresh, "{ this is not valid json ][", [](const std::filesystem::path&) { return true; }); - EXPECT_NE(merged.find("src/main.cpp"), std::string::npos) << merged; - EXPECT_NE(merged.find("-O2"), std::string::npos) << merged; -} + expect(ne(merged.find("src/main.cpp"), std::string::npos)) << merged; + expect(ne(merged.find("-O2"), std::string::npos)) << merged; +}}; -TEST(CompileCommandsMerge, SortsFinalEntriesByFile) { +UTest sorts_final_entries_by_file{"CompileCommandsMerge/SortsFinalEntriesByFile", [] { auto fresh = cdb({entry("/p/z.cpp", "-DZ"), entry("/p/a.cpp", "-DA")}); auto merged = merge_compile_commands( fresh, "[]", [](const std::filesystem::path&) { return true; }); - EXPECT_LT(merged.find("/p/a.cpp"), merged.find("/p/z.cpp")); -} + expect(lt(merged.find("/p/a.cpp"), merged.find("/p/z.cpp"))); +}}; -TEST(CompileCommandsWriter, RejectsNonArrayFreshJson) { +UTest rejects_non_array_fresh_json{"CompileCommandsWriter/RejectsNonArrayFreshJson", [] { TempDir temp; auto result = publish_compile_commands( temp.path / "compile_commands.json", R"({"file":"not-an-array"})", [](const std::filesystem::path&) { return true; }); - ASSERT_FALSE(result.has_value()); - EXPECT_NE(result.error().message.find("JSON array"), std::string::npos); -} + expect(fatal(!result.has_value())); + expect(ne(result.error().message.find("JSON array"), std::string::npos)); +}}; -TEST(CompileCommandsWriter, PublishesFreshDatabaseWhenNoneExists) { +UTest publishes_fresh_database_when_none_exists{"CompileCommandsWriter/PublishesFreshDatabaseWhenNoneExists", [] { TempDir temp; auto path = temp.path / "compile_commands.json"; // deliberately absent auto content = cdb({entry("/p/src/main.cpp", "-O2")}); @@ -183,16 +185,16 @@ TEST(CompileCommandsWriter, PublishesFreshDatabaseWhenNoneExists) { auto result = publish_compile_commands( path, content, [](const std::filesystem::path&) { return true; }); - ASSERT_TRUE(result.has_value()) << result.error().message; - EXPECT_TRUE(result->changed); + if (auto&& failed = expect(fatal(result.has_value())); !failed) failed << result.error().message; + expect(bool(result->changed)); // The file is re-serialised via nlohmann (sorted + 2-space indent), so // assert on content, not on byte-equality with the raw input. auto published = read_file(path); - EXPECT_NE(published.find("/p/src/main.cpp"), std::string::npos) << published; - EXPECT_NE(published.find("-O2"), std::string::npos) << published; -} + expect(ne(published.find("/p/src/main.cpp"), std::string::npos)) << published; + expect(ne(published.find("-O2"), std::string::npos)) << published; +}}; -TEST(CompileCommandsWriter, UnchangedContentKeepsMtime) { +UTest unchanged_content_keeps_mtime{"CompileCommandsWriter/UnchangedContentKeepsMtime", [] { TempDir temp; auto path = temp.path / "compile_commands.json"; auto content = cdb({entry((temp.path / "a.cpp").string(), "-DOK")}); @@ -202,12 +204,12 @@ TEST(CompileCommandsWriter, UnchangedContentKeepsMtime) { auto result = publish_compile_commands( path, content, [](const std::filesystem::path&) { return true; }); - ASSERT_TRUE(result.has_value()) << result.error().message; - EXPECT_FALSE(result->changed); - EXPECT_EQ(std::filesystem::last_write_time(path), before); -} + if (auto&& failed = expect(fatal(result.has_value())); !failed) failed << result.error().message; + expect(!result->changed); + expect(eq(std::filesystem::last_write_time(path), before)); +}}; -TEST(CompileCommandsWriter, ReplacementFailurePreservesOldDatabase) { +UTest replacement_failure_preserves_old_database{"CompileCommandsWriter/ReplacementFailurePreservesOldDatabase", [] { TempDir temp; auto path = temp.path / "compile_commands.json"; auto oldContent = cdb({entry((temp.path / "old.cpp").string(), "-DOLD")}); @@ -224,11 +226,11 @@ TEST(CompileCommandsWriter, ReplacementFailurePreservesOldDatabase) { path, newContent, [](const std::filesystem::path&) { return true; }, failReplace); - ASSERT_FALSE(result.has_value()); - EXPECT_EQ(read_file(path), oldContent); - EXPECT_EQ(std::distance(std::filesystem::directory_iterator(temp.path), - std::filesystem::directory_iterator{}), 1); -} + expect(fatal(!result.has_value())); + expect(eq(read_file(path), oldContent)); + expect(eq(std::distance(std::filesystem::directory_iterator(temp.path), + std::filesystem::directory_iterator{}), 1)); +}}; // #390 self-heal: a CDB written BEFORE the mixed-separator fix spells the same // file differently from the way a fresh plan spells it. Because the merge key @@ -241,10 +243,10 @@ TEST(CompileCommandsWriter, ReplacementFailurePreservesOldDatabase) { // Windows the `/` separators need make_preferred on top. A stale value of just // `p.generic_string()` would be byte-identical to fresh on POSIX, and the // literal-string key this test exists to replace would pass it unchanged. -TEST(CompileCommandsMerge, NormalizedFileKeysHealStaleSeparatorSpellings) { +UTest normalized_file_keys_heal_stale_separator_spellings{"CompileCommandsMerge/NormalizedFileKeysHealStaleSeparatorSpellings", [] { auto p = std::filesystem::path("/p") / "generated" / "modules" / "a.cpp"; auto stale = "/p/generated/./modules/a.cpp"; - ASSERT_NE(p.string(), stale) << "fixture must differ from the fresh spelling"; + expect(fatal(ne(p.string(), stale))) << "fixture must differ from the fresh spelling"; auto fresh = cdb({ entry(p.string(), "-O2-FRESH") }); auto existing = cdb({ entry(stale, "-O0-STALE") }); @@ -252,10 +254,10 @@ TEST(CompileCommandsMerge, NormalizedFileKeysHealStaleSeparatorSpellings) { auto merged = merge_compile_commands( fresh, existing, [](const std::filesystem::path&) { return true; }); - EXPECT_EQ(count(merged, "generated"), 1u) << merged; - EXPECT_NE(merged.find("-O2-FRESH"), std::string::npos) << merged; - EXPECT_EQ(merged.find("-O0-STALE"), std::string::npos) << merged; -} + expect(eq(count(merged, "generated"), 1u)) << merged; + expect(ne(merged.find("-O2-FRESH"), std::string::npos)) << merged; + expect(eq(merged.find("-O0-STALE"), std::string::npos)) << merged; +}}; // ── CDB arguments must be argv, not shell words ───────────────────────────── // @@ -294,63 +296,63 @@ bool has_edge_quote(std::string_view s) { // The host's own reader: single quotes are quoting for `sh` and literal // characters for the MSVCRT rules, so this row holds on POSIX hosts only. -TEST(CompileCommandsArgs, StripsPosixQuotingFromAToken) { +UTest strips_posix_quoting_from_a_token{"CompileCommandsArgs/StripsPosixQuotingFromAToken", [] { if constexpr (mcpplibs::os::is_windows) { - GTEST_SKIP() << "single quotes are literal under the MSVCRT rules"; + skip("single quotes are literal under the MSVCRT rules"); } auto out = mcpp::build::split_flags("-O2 '-fprebuilt-module-path=/a/b' -g"); - ASSERT_EQ(out.size(), 3u); - EXPECT_EQ(out[1], "-fprebuilt-module-path=/a/b"); - for (auto const& t : out) EXPECT_FALSE(has_edge_quote(t)) << t; -} + expect(fatal(eq(out.size(), 3u))); + expect(eq(out[1], "-fprebuilt-module-path=/a/b")); + for (auto const& t : out) expect(!has_edge_quote(t)) << t; +}}; -TEST(CompileCommandsArgs, StripsWindowsQuotingFromAToken) { +UTest strips_windows_quoting_from_a_token{"CompileCommandsArgs/StripsWindowsQuotingFromAToken", [] { auto out = mcpp::build::split_flags(R"(-O2 "-IC:\Users\x\inc" -g)"); - ASSERT_EQ(out.size(), 3u); - EXPECT_EQ(out[1], R"(-IC:\Users\x\inc)"); - for (auto const& t : out) EXPECT_FALSE(has_edge_quote(t)) << t; -} + expect(fatal(eq(out.size(), 3u))); + expect(eq(out[1], R"(-IC:\Users\x\inc)")); + for (auto const& t : out) expect(!has_edge_quote(t)) << t; +}}; // The one that bit: a quoted path containing a space is ONE argument. The // space arrives ninja-escaped (`$ `) because flags.cppm escapes before it // quotes, so the un-escape has to happen INSIDE the quotes — do it in the // wrong order and the token splits exactly here. -TEST(CompileCommandsArgs, AQuotedPathWithASpaceStaysOneToken) { +UTest a_quoted_path_with_a_space_stays_one_token{"CompileCommandsArgs/AQuotedPathWithASpaceStaysOneToken", [] { // The quotes are the host's: flags.cppm quotes with `'` on POSIX and `"` // under the MSVCRT rules. const std::string_view line = mcpplibs::os::is_windows ? "-O2 \"-fmodule-file=std=/tmp/my$ project/std.pcm\" -g" : "-O2 '-fmodule-file=std=/tmp/my$ project/std.pcm' -g"; auto out = mcpp::build::split_flags(line); - ASSERT_EQ(out.size(), 3u) << "the quoted path was split"; - EXPECT_EQ(out[1], "-fmodule-file=std=/tmp/my project/std.pcm"); - for (auto const& t : out) EXPECT_FALSE(has_edge_quote(t)) << t; -} + expect(fatal(eq(out.size(), 3u))) << "the quoted path was split"; + expect(eq(out[1], "-fmodule-file=std=/tmp/my project/std.pcm")); + for (auto const& t : out) expect(!has_edge_quote(t)) << t; +}}; // Unquoted input: ninja's escapes are undone first, and then the host splits // the line. `$ ` is a plain space by the time the host reads it, so an // unquoted `-Idir$ with$ space` is three words to the compiler, and the // database says so. The engine quotes every word it writes (include_token, // ninja_command_word); the quoted spelling is the test above. -TEST(CompileCommandsArgs, UnquotedInputSplitsWhereTheHostSplits) { +UTest unquoted_input_splits_where_the_host_splits{"CompileCommandsArgs/UnquotedInputSplitsWhereTheHostSplits", [] { auto out = mcpp::build::split_flags("-IC$:/x -DA=1 -Idir$ with$ space"); - ASSERT_EQ(out.size(), 5u); - EXPECT_EQ(out[0], "-IC:/x"); - EXPECT_EQ(out[1], "-DA=1"); - EXPECT_EQ(out[2], "-Idir"); - EXPECT_EQ(out[3], "with"); - EXPECT_EQ(out[4], "space"); -} + expect(fatal(eq(out.size(), 5u))); + expect(eq(out[0], "-IC:/x")); + expect(eq(out[1], "-DA=1")); + expect(eq(out[2], "-Idir")); + expect(eq(out[3], "with")); + expect(eq(out[4], "space")); +}}; // A quote inside a word is quoting on both hosts' readers: `sh` and the MSVCRT // rules both hand the compiler `-DGREETING=hi` for this text. The version of // this test before #655 asserted the opposite, which pinned the compile // database to a reading no host applies. -TEST(CompileCommandsArgs, QuotesInsideAWordAreQuoting) { +UTest quotes_inside_a_word_are_quoting{"CompileCommandsArgs/QuotesInsideAWordAreQuoting", [] { auto out = mcpp::build::split_flags(R"(-DGREETING="hi")"); - ASSERT_EQ(out.size(), 1u); - EXPECT_EQ(out[0], "-DGREETING=hi"); -} + expect(fatal(eq(out.size(), 1u))); + expect(eq(out[0], "-DGREETING=hi")); +}}; // ── A unit's flag list: the edge and the databases agree (#655) ───────────── // @@ -395,31 +397,28 @@ std::vector random_words() { } // namespace -TEST(CompileCommandsArgs, TheEdgeAndTheDatabaseListTheSameWords) { +UTest the_edge_and_the_database_list_the_same_words{"CompileCommandsArgs/TheEdgeAndTheDatabaseListTheSameWords", [] { const auto words = mcpp::manifest::flag_words(kIssue655Elements); - EXPECT_EQ(mcpp::build::split_flags(mcpp::build::ninja_flag_list(kIssue655Elements)), - words); + expect(eq(mcpp::build::split_flags(mcpp::build::ninja_flag_list(kIssue655Elements)), words)); const std::vector want{ R"(-DPLATFORM_CONFIG_H="mcpp_libarchive_config.h")", "-DMID=mid", "-DSQ=sq", "-include", "mcpp_lua_platform_config.h", "-I/opt/my dir/include", R"(-IC:\Users\x\include)", "-DDOLLAR=a$b", R"(-DDEF="def")", "-DSPACE=a b"}; - EXPECT_EQ(words, want); -} + expect(eq(words, want)); +}}; -TEST(CompileCommandsArgs, AWordOnAnEdgeReadsBackAsTheWord) { +UTest a_word_on_an_edge_reads_back_as_the_word{"CompileCommandsArgs/AWordOnAnEdgeReadsBackAsTheWord", [] { for (auto const& w : random_words()) { - EXPECT_EQ(mcpp::build::split_flags(mcpp::build::ninja_command_word(w)), - std::vector{w}) - << "word: [" << w << "]"; + if (auto&& failed = expect(eq(mcpp::build::split_flags(mcpp::build::ninja_command_word(w)), std::vector{w})); !failed) failed << "word: [" << w << "]"; } -} +}}; // The reader above is mcpp's model of the host. On a POSIX host the host itself // is available, so the model is checked against it: `/bin/sh` receives the // quoted words and prints each one back. -TEST(CompileCommandsArgs, ShellReceivesTheQuotedWordsAsWritten) { +UTest shell_receives_the_quoted_words_as_written{"CompileCommandsArgs/ShellReceivesTheQuotedWordsAsWritten", [] { if constexpr (mcpplibs::os::is_windows) { - GTEST_SKIP() << "the POSIX shell is the reader measured here"; + skip("the POSIX shell is the reader measured here"); } TempDir temp; const auto out = temp.path / "words.txt"; @@ -432,14 +431,14 @@ TEST(CompileCommandsArgs, ShellReceivesTheQuotedWordsAsWritten) { command += mcpp::build::shell_quote_arg(w); } command += " > '" + out.string() + "'"; - ASSERT_EQ(std::system(command.c_str()), 0); + expect(fatal(eq(std::system(command.c_str()), 0))); std::ifstream in(out); std::vector printed; for (std::string line; std::getline(in, line);) printed.push_back(line); - EXPECT_EQ(printed, words); -} + expect(eq(printed, words)); +}}; -TEST(CompileCommandsEmit, AUnitListsTheWordsOfItsFlagList) { +UTest a_unit_lists_the_words_of_its_flag_list{"CompileCommandsEmit/AUnitListsTheWordsOfItsFlagList", [] { BuildPlan plan; plan.projectRoot = "/p"; plan.outputDir = "/p/target"; @@ -457,8 +456,8 @@ TEST(CompileCommandsEmit, AUnitListsTheWordsOfItsFlagList) { for (auto const& a : j[0]["arguments"]) args.push_back(a.get()); const auto words = mcpp::manifest::flag_words(kIssue655Elements); auto at = std::search(args.begin(), args.end(), words.begin(), words.end()); - EXPECT_NE(at, args.end()) << "the unit's words are not listed in order"; -} + expect(ne(at, args.end())) << "the unit's words are not listed in order"; +}}; // ── Emitted paths use native separators ───────────────────────────────────── // @@ -472,7 +471,7 @@ TEST(CompileCommandsEmit, AUnitListsTheWordsOfItsFlagList) { // STRINGS — split_flags(f.cxx) and the package cflags/cxxflags — pass through // untouched and stay the ingestion points' responsibility; normalizing an // arbitrary flag payload is not safe (`-DPATH="/etc/x"` holds real slashes). -TEST(CompileCommandsEmit, EmittedPathsUseNativeSeparators) { +UTest emitted_paths_use_native_separators{"CompileCommandsEmit/EmittedPathsUseNativeSeparators", [] { BuildPlan plan; plan.projectRoot = "/p"; plan.outputDir = "/p/target"; @@ -508,20 +507,20 @@ TEST(CompileCommandsEmit, EmittedPathsUseNativeSeparators) { // C3 (design 2026-09-26 §3.3): `directory` is the OUTPUT directory the // compiler runs in, not `plan.projectRoot` — here `/p/target`. if constexpr (std::filesystem::path::preferred_separator == '\\') { - EXPECT_EQ(e["file"].get(), "C:\\Users\\x\\src\\main.cpp"); - EXPECT_EQ(e["directory"].get(), "\\p\\target"); - EXPECT_EQ(e["output"].get(), "\\p\\target\\obj\\main.o"); - EXPECT_EQ(inc, "-IC:\\proj\\generated\\inc"); - EXPECT_EQ(incAfter, "-idirafterC:\\proj\\third_party\\inc"); + expect(eq(e["file"].get(), "C:\\Users\\x\\src\\main.cpp")); + expect(eq(e["directory"].get(), "\\p\\target")); + expect(eq(e["output"].get(), "\\p\\target\\obj\\main.o")); + expect(eq(inc, "-IC:\\proj\\generated\\inc")); + expect(eq(incAfter, "-idirafterC:\\proj\\third_party\\inc")); } else { - EXPECT_EQ(e["file"].get(), "C:/Users/x/src/main.cpp"); - EXPECT_EQ(e["directory"].get(), "/p/target"); - EXPECT_EQ(inc, "-IC:/proj/generated/inc"); - EXPECT_EQ(incAfter, "-idirafterC:/proj/third_party/inc"); + expect(eq(e["file"].get(), "C:/Users/x/src/main.cpp")); + expect(eq(e["directory"].get(), "/p/target")); + expect(eq(inc, "-IC:/proj/generated/inc")); + expect(eq(incAfter, "-idirafterC:/proj/third_party/inc")); } -} +}}; -TEST(CompileCommandsWriter, ReplacesSymlinkTargetWithoutRemovingLink) { +UTest replaces_symlink_target_without_removing_link{"CompileCommandsWriter/ReplacesSymlinkTargetWithoutRemovingLink", [] { TempDir temp; auto target = temp.path / "build" / "compile_commands.json"; auto link = temp.path / "compile_commands.json"; @@ -531,19 +530,19 @@ TEST(CompileCommandsWriter, ReplacesSymlinkTargetWithoutRemovingLink) { std::ofstream(target) << oldContent; std::error_code symlinkEc; std::filesystem::create_symlink(target, link, symlinkEc); - if (symlinkEc) GTEST_SKIP() << "symlink unavailable: " << symlinkEc.message(); + if (symlinkEc) skip([&] { std::ostringstream text; text << "symlink unavailable: " << symlinkEc.message(); return text.str(); }()); auto result = publish_compile_commands( link, newContent, [](const std::filesystem::path&) { return false; }); - ASSERT_TRUE(result.has_value()) << result.error().message; - EXPECT_TRUE(std::filesystem::is_symlink(link)); + if (auto&& failed = expect(fatal(result.has_value())); !failed) failed << result.error().message; + expect(std::filesystem::is_symlink(link)); auto published = read_file(target); - EXPECT_NE(published.find("-DNEW"), std::string::npos); - EXPECT_EQ(published.find("-DOLD"), std::string::npos); -} + expect(ne(published.find("-DNEW"), std::string::npos)); + expect(eq(published.find("-DOLD"), std::string::npos)); +}}; -TEST(CompileCommandsWriter, ExistingUnreadableDatabaseIsNotOverwritten) { +UTest existing_unreadable_database_is_not_overwritten{"CompileCommandsWriter/ExistingUnreadableDatabaseIsNotOverwritten", [] { TempDir temp; auto path = temp.path / "compile_commands.json"; std::ofstream(path) << "last-known-good"; @@ -551,30 +550,30 @@ TEST(CompileCommandsWriter, ExistingUnreadableDatabaseIsNotOverwritten) { std::filesystem::permissions( path, std::filesystem::perms::owner_read, std::filesystem::perm_options::remove, permissionEc); - if (permissionEc) GTEST_SKIP() << "cannot change permissions: " << permissionEc.message(); + if (permissionEc) skip([&] { std::ostringstream text; text << "cannot change permissions: " << permissionEc.message(); return text.str(); }()); std::ifstream probe(path); if (probe) { std::filesystem::permissions(path, std::filesystem::perms::owner_all, std::filesystem::perm_options::replace, permissionEc); - GTEST_SKIP() << "test user can still read restricted file"; + skip("test user can still read restricted file"); } auto result = publish_compile_commands( path, cdb({entry((temp.path / "new.cpp").string(), "-DNEW")}), [](const std::filesystem::path&) { return true; }); - ASSERT_FALSE(result.has_value()); - EXPECT_NE(result.error().message.find("read existing"), std::string::npos); + expect(fatal(!result.has_value())); + expect(ne(result.error().message.find("read existing"), std::string::npos)); std::filesystem::permissions(path, std::filesystem::perms::owner_all, std::filesystem::perm_options::replace, permissionEc); -} +}}; // ── One record, two databases ─────────────────────────────────────────────── // // The compile database and the build database render the same per-unit record, // so `arguments` cannot differ between them. The second half shows the // comparison can fail: the record under other flags is not the CDB's. -TEST(CompileCommandsEmit, UnitInvocationsAreTheCompileDatabaseArguments) { +UTest unit_invocations_are_the_compile_database_arguments{"CompileCommandsEmit/UnitInvocationsAreTheCompileDatabaseArguments", [] { BuildPlan plan; plan.projectRoot = "/p"; plan.outputDir = "/p/target"; @@ -600,27 +599,26 @@ TEST(CompileCommandsEmit, UnitInvocationsAreTheCompileDatabaseArguments) { auto j = nlohmann::json::parse(emit_compile_commands(plan, flags)); auto invs = unit_invocations(plan, flags); - ASSERT_EQ(j.size(), invs.size()); + expect(fatal(eq(j.size(), invs.size()))); for (std::size_t i = 0; i < invs.size(); ++i) { - EXPECT_EQ(j[i]["arguments"].get>(), invs[i].arguments); - EXPECT_EQ(j[i]["file"].get(), invs[i].file); - EXPECT_EQ(j[i]["directory"].get(), invs[i].directory); - EXPECT_EQ(j[i]["output"].get(), invs[i].output); - EXPECT_EQ(invs[i].unit, &plan.compileUnits[i]); + expect(eq(j[i]["arguments"].get>(), invs[i].arguments)); + expect(eq(j[i]["file"].get(), invs[i].file)); + expect(eq(j[i]["directory"].get(), invs[i].directory)); + expect(eq(j[i]["output"].get(), invs[i].output)); + expect(eq(invs[i].unit, &plan.compileUnits[i])); } CompileFlags other = flags; other.cxx = "-std=c++26"; - EXPECT_NE(j[0]["arguments"].get>(), - unit_invocations(plan, other)[0].arguments); -} + expect(ne(j[0]["arguments"].get>(), unit_invocations(plan, other)[0].arguments)); +}}; // ── C3: `directory` is the output directory ───────────────────────────────── // // The JSON format and S1-8-2 define the field as the directory the compiler // runs in; ninja runs every compile in `plan.outputDir`, never the project // root (design 2026-09-26 §3.3). -TEST(CompileCommandsEmit, AProjectUnitsDirectoryIsTheOutputDirectory) { +UTest a_project_units_directory_is_the_output_directory{"CompileCommandsEmit/AProjectUnitsDirectoryIsTheOutputDirectory", [] { BuildPlan plan; plan.projectRoot = "/p"; plan.outputDir = "/p/target/x86_64-linux-gnu/deadbeef"; @@ -634,12 +632,12 @@ TEST(CompileCommandsEmit, AProjectUnitsDirectoryIsTheOutputDirectory) { flags.cxxBinary = "/usr/bin/g++"; auto invs = unit_invocations(plan, flags); - ASSERT_EQ(invs.size(), 1u); + expect(fatal(eq(invs.size(), 1u))); // Compared as paths: the field is written in the native spelling, which // on Windows turns this fixture's `/` into `\`. - EXPECT_EQ(std::filesystem::path(invs[0].directory), plan.outputDir); - EXPECT_NE(std::filesystem::path(invs[0].directory), plan.projectRoot); -} + expect(eq(std::filesystem::path(invs[0].directory), plan.outputDir)); + expect(ne(std::filesystem::path(invs[0].directory), plan.projectRoot)); +}}; // ── C4: the module interface language flag ────────────────────────────────── // @@ -647,7 +645,7 @@ TEST(CompileCommandsEmit, AProjectUnitsDirectoryIsTheOutputDirectory) { // (immediately before `-c`) for every unit that actually provides a module; // a unit that does not (an ordinary source, here) carries no such flag at // all (design 2026-09-26 §3.4). -TEST(CompileCommandsEmit, InterfaceUnitCarriesTheLanguageFlagBeforeDashCAndImplementationDoesNot) { +UTest interface_unit_carries_the_language_flag_before_dash_c_and_implementation_does_not{"CompileCommandsEmit/InterfaceUnitCarriesTheLanguageFlagBeforeDashCAndImplementationDoesNot", [] { BuildPlan plan; plan.projectRoot = "/p"; plan.outputDir = "/p/target"; @@ -669,24 +667,24 @@ TEST(CompileCommandsEmit, InterfaceUnitCarriesTheLanguageFlagBeforeDashCAndImple flags.cxxBinary = "/usr/bin/clang++"; auto invs = unit_invocations(plan, flags); - ASSERT_EQ(invs.size(), 2u); + expect(fatal(eq(invs.size(), 2u))); auto& iface = invs[0].arguments; auto ifaceC = std::ranges::find(iface, std::string("-c")); - ASSERT_NE(ifaceC, iface.end()); - ASSERT_GE(std::distance(iface.begin(), ifaceC), 2); - EXPECT_EQ(*(ifaceC - 2), "-x"); - EXPECT_EQ(*(ifaceC - 1), "c++-module"); + expect(fatal(ne(ifaceC, iface.end()))); + expect(fatal(ge(std::distance(iface.begin(), ifaceC), 2))); + expect(eq(*(ifaceC - 2), "-x")); + expect(eq(*(ifaceC - 1), "c++-module")); auto& impl = invs[1].arguments; - EXPECT_EQ(std::ranges::find(impl, std::string("-x")), impl.end()); -} + expect(eq(std::ranges::find(impl, std::string("-x")), impl.end())); +}}; // MSVC's form (`/interface /TP`) stays out of the record until a Windows // measurement of clang-cl-mode clangd answers whether it accepts `/interface` // (design §3.4, "Open"); until then an MSVC plan carries no language flag at // all, matching the implementation-unit case above rather than guessing. -TEST(CompileCommandsEmit, MsvcCarriesNoLanguageFlagYet) { +UTest msvc_carries_no_language_flag_yet{"CompileCommandsEmit/MsvcCarriesNoLanguageFlagYet", [] { BuildPlan plan; plan.projectRoot = "/p"; plan.outputDir = "/p/target"; @@ -702,7 +700,6 @@ TEST(CompileCommandsEmit, MsvcCarriesNoLanguageFlagYet) { flags.cxxBinary = "/vs/cl.exe"; auto invs = unit_invocations(plan, flags); - ASSERT_EQ(invs.size(), 1u); - EXPECT_EQ(std::ranges::find(invs[0].arguments, std::string("/interface")), - invs[0].arguments.end()); -} + expect(fatal(eq(invs.size(), 1u))); + expect(eq(std::ranges::find(invs[0].arguments, std::string("/interface")), invs[0].arguments.end())); +}}; diff --git a/tests/unit/test_compiler_transaction.cpp b/tests/unit/test_compiler_transaction.cpp index 222234015..414dd9347 100644 --- a/tests/unit/test_compiler_transaction.cpp +++ b/tests/unit/test_compiler_transaction.cpp @@ -1,5 +1,5 @@ -#include import std; +import mcpplibs.mtest; import mcpp.build.schedule.compiler_transaction; import mcpp.toolchain.fingerprint; import mcpp.run; @@ -8,6 +8,8 @@ import mcpp.runtime.elf; import mcpplibs.os.platform; import mcpplibs.os.fs; +using namespace mcpplibs::mtest; + namespace tx = mcpp::build::schedule::transaction; namespace fs = std::filesystem; namespace { @@ -28,16 +30,16 @@ struct Transaction { return *a; } void admitted(const tx::Attempt& a) { - ASSERT_TRUE(tx::publish(a.directory/"controller","mcpp-controller-v1",{"1","1",tx::spelling(generation/"actors/helper.lock")})); - ASSERT_TRUE(tx::publish(a.directory/"unit","mcpp-unit-v1",{"opaque-test-reference","1","1",tx::spelling(generation/"actors/helper.lock")})); - ASSERT_TRUE(tx::publish(a.directory/"reservation","mcpp-reservation-pointer-v1",{tx::spelling(reservation)})); - ASSERT_TRUE(tx::publish(reservation,"mcpp-reservation-v1",{tx::spelling(a.directory)})); - ASSERT_TRUE(tx::publish(a.directory/"admitted","mcpp-admitted-v1",{})); + expect(fatal(bool(tx::publish(a.directory/"controller","mcpp-controller-v1",{"1","1",tx::spelling(generation/"actors/helper.lock")})))); + expect(fatal(bool(tx::publish(a.directory/"unit","mcpp-unit-v1",{"opaque-test-reference","1","1",tx::spelling(generation/"actors/helper.lock")})))); + expect(fatal(bool(tx::publish(a.directory/"reservation","mcpp-reservation-pointer-v1",{tx::spelling(reservation)})))); + expect(fatal(bool(tx::publish(reservation,"mcpp-reservation-v1",{tx::spelling(a.directory)})))); + expect(fatal(bool(tx::publish(a.directory/"admitted","mcpp-admitted-v1",{})))); } void completed(const tx::Attempt& a,int rc=0) { admitted(a); std::ofstream(bmi)<<"new compiler BMI";std::ofstream(object)<<"complete object"; - ASSERT_TRUE(a.publication());ASSERT_TRUE(a.retired(rc)); + expect(fatal(bool(a.publication())));expect(fatal(bool(a.retired(rc)))); std::ofstream(tx::suffix(slot,".rc"))<::min(),-1LL,0LL,1LL,std::numeric_limits::max()}) { const auto stamp=tx::journal_time(fs::file_time_type{Duration{static_cast(value)}}); - ASSERT_TRUE(stamp);EXPECT_EQ(*stamp,value); - EXPECT_TRUE(tx::output_stamp("1",std::to_string(*stamp))); + expect(fatal(bool(stamp)));expect(eq(*stamp, value)); + expect(bool(tx::output_stamp("1",std::to_string(*stamp)))); } if constexpr(std::numeric_limits::digits>std::numeric_limits::digits) { auto above=static_cast(std::numeric_limits::max());++above; auto below=static_cast(std::numeric_limits::min());--below; - EXPECT_FALSE(tx::journal_time(fs::file_time_type{Duration{above}})); - EXPECT_FALSE(tx::journal_time(fs::file_time_type{Duration{below}})); + expect(!tx::journal_time(fs::file_time_type{Duration{above}})); + expect(!tx::journal_time(fs::file_time_type{Duration{below}})); } -} -TEST(CompilerTransaction, PreparedIdentityRoundTripsTheActualFileTimestamp) { +}}; +UTest prepared_identity_round_trips_the_actual_file_timestamp{"CompilerTransaction/PreparedIdentityRoundTripsTheActualFileTimestamp", [] { Transaction t;const auto prepared=t.prepare(); - const auto ticks=tx::journal_time(fs::last_write_time(t.bmi));ASSERT_TRUE(ticks); - EXPECT_EQ(prepared.before_time,std::to_string(*ticks)); - const auto loaded=tx::Attempt::load(prepared.directory);ASSERT_TRUE(loaded); - EXPECT_TRUE(loaded->before_present);EXPECT_EQ(loaded->before_time,prepared.before_time); - EXPECT_TRUE(tx::validate(*loaded)); -} -TEST(CompilerTransaction, UnidentifiableExistingBmiIsNotRecordedAsMissing) { + const auto ticks=tx::journal_time(fs::last_write_time(t.bmi));expect(fatal(bool(ticks))); + expect(eq(prepared.before_time, std::to_string(*ticks))); + const auto loaded=tx::Attempt::load(prepared.directory);expect(fatal(bool(loaded))); + expect(bool(loaded->before_present));expect(eq(loaded->before_time, prepared.before_time)); + expect(bool(tx::validate(*loaded))); +}}; +UTest unidentifiable_existing_bmi_is_not_recorded_as_missing{"CompilerTransaction/UnidentifiableExistingBmiIsNotRecordedAsMissing", [] { Transaction t;fs::create_directory(t.bmi); - EXPECT_FALSE(tx::Attempt::prepare(t.generation,false,t.bmi,t.object,t.slot,t.dep)); - EXPECT_TRUE(fs::is_directory(t.bmi));EXPECT_FALSE(fs::exists(t.generation)); -} -TEST(CompilerTransaction, RecordsAreBoundedChecksummedAndSchemaChecked) { + expect(!tx::Attempt::prepare(t.generation,false,t.bmi,t.object,t.slot,t.dep)); + expect(fs::is_directory(t.bmi));expect(!fs::exists(t.generation)); +}}; +UTest records_are_bounded_checksummed_and_schema_checked{"CompilerTransaction/RecordsAreBoundedChecksummedAndSchemaChecked", [] { Transaction t;const auto record=t.root/"record"; - ASSERT_TRUE(tx::publish(record,"schema",{"a\nb", "", "unicode \xc3\xa9"})); - auto good=tx::read(record,"schema");ASSERT_TRUE(good&&*good);EXPECT_EQ((**good)[0],"a\nb"); - EXPECT_FALSE(tx::read(record,"another-schema")); + expect(fatal(bool(tx::publish(record,"schema",{"a\nb", "", "unicode \xc3\xa9"})))); + auto good=tx::read(record,"schema");expect(fatal(good&&*good));expect(eq((**good)[0], "a\nb")); + expect(!tx::read(record,"another-schema")); auto bytes=Transaction::contents(record);bytes[0]='X';std::ofstream(record)<prepare_backup());fs::remove(t.root/"alias");fs::create_directory_symlink(t.root/"second",t.root/"alias"); + auto a=tx::Attempt::prepare(t.generation,false,path,t.object,t.slot,t.dep);expect(fatal(bool(a))); + expect(fatal(bool(a->prepare_backup())));fs::remove(t.root/"alias");fs::create_directory_symlink(t.root/"second",t.root/"alias"); std::ofstream(t.root/"first/unit.gcm")<<"changed";std::ofstream(t.root/"second/unit.gcm")<<"unrelated"; - ASSERT_TRUE(tx::recover(*a).value_or(false));EXPECT_EQ(Transaction::contents(t.root/"first/unit.gcm"),"original"); - EXPECT_EQ(Transaction::contents(t.root/"second/unit.gcm"),"unrelated"); -} + expect(fatal(bool(tx::recover(*a).value_or(false))));expect(eq(Transaction::contents(t.root/"first/unit.gcm"), "original")); + expect(eq(Transaction::contents(t.root/"second/unit.gcm"), "unrelated")); +}}; namespace { void configuration_outputs(Transaction& t,std::string_view name="unit") { @@ -169,145 +171,145 @@ void configuration_outputs(Transaction& t,std::string_view name="unit") { t.slot=tx::suffix(t.object,".sched"); } } -TEST(CompilerTransaction, MissingCertifiedObjectIsInvalidatedBeforeNinjaAdmission) { - Transaction t;configuration_outputs(t);auto a=t.prepare();ASSERT_TRUE(a.prepare_backup());t.completed(a); - ASSERT_TRUE(tx::recover(a).value_or(false));fs::remove(t.object); - ASSERT_TRUE(tx::admit_cached_outputs(t.root));EXPECT_FALSE(fs::exists(t.bmi)); - EXPECT_EQ(Transaction::contents(tx::suffix(t.bmi,".bak")),"new compiler BMI"); - EXPECT_FALSE(fs::exists(tx::suffix(t.slot,".completed"))); - EXPECT_TRUE(tx::admit_cached_outputs(t.root)); -} -TEST(CompilerTransaction, TwoMissingCertifiedObjectsAreInvalidatedInOneAdmission) { - Transaction t;configuration_outputs(t,"first");auto first=t.prepare();ASSERT_TRUE(first.prepare_backup());t.completed(first); - ASSERT_TRUE(tx::recover(first).value_or(false)); - configuration_outputs(t,"second");auto second=t.prepare();ASSERT_TRUE(second.prepare_backup());t.completed(second); - ASSERT_TRUE(tx::recover(second).value_or(false));fs::remove(first.object);fs::remove(second.object); - ASSERT_TRUE(tx::admit_cached_outputs(t.root));EXPECT_FALSE(fs::exists(first.bmi));EXPECT_FALSE(fs::exists(second.bmi)); -} -TEST(CompilerTransaction, CorruptOrForeignCertificateCannotAuthorizeAdmissionRecovery) { - Transaction t;configuration_outputs(t);auto a=t.prepare();ASSERT_TRUE(a.prepare_backup());t.completed(a); - ASSERT_TRUE(tx::recover(a).value_or(false));fs::remove(t.object); +UTest missing_certified_object_is_invalidated_before_ninja_admission{"CompilerTransaction/MissingCertifiedObjectIsInvalidatedBeforeNinjaAdmission", [] { + Transaction t;configuration_outputs(t);auto a=t.prepare();expect(fatal(bool(a.prepare_backup())));t.completed(a); + expect(fatal(bool(tx::recover(a).value_or(false))));fs::remove(t.object); + expect(fatal(bool(tx::admit_cached_outputs(t.root))));expect(!fs::exists(t.bmi)); + expect(eq(Transaction::contents(tx::suffix(t.bmi,".bak")), "new compiler BMI")); + expect(!(fs::exists(tx::suffix(t.slot,".completed")))); + expect(bool(tx::admit_cached_outputs(t.root))); +}}; +UTest two_missing_certified_objects_are_invalidated_in_one_admission{"CompilerTransaction/TwoMissingCertifiedObjectsAreInvalidatedInOneAdmission", [] { + Transaction t;configuration_outputs(t,"first");auto first=t.prepare();expect(fatal(bool(first.prepare_backup())));t.completed(first); + expect(fatal(bool(tx::recover(first).value_or(false)))); + configuration_outputs(t,"second");auto second=t.prepare();expect(fatal(bool(second.prepare_backup())));t.completed(second); + expect(fatal(bool(tx::recover(second).value_or(false))));fs::remove(first.object);fs::remove(second.object); + expect(fatal(bool(tx::admit_cached_outputs(t.root))));expect(!fs::exists(first.bmi));expect(!fs::exists(second.bmi)); +}}; +UTest corrupt_or_foreign_certificate_cannot_authorize_admission_recovery{"CompilerTransaction/CorruptOrForeignCertificateCannotAuthorizeAdmissionRecovery", [] { + Transaction t;configuration_outputs(t);auto a=t.prepare();expect(fatal(bool(a.prepare_backup())));t.completed(a); + expect(fatal(bool(tx::recover(a).value_or(false))));fs::remove(t.object); const auto path=tx::suffix(t.slot,".completed");auto bytes=Transaction::contents(path);bytes[0]='X';std::ofstream(path)<(held),std::istreambuf_iterator()),"old"); -} -TEST(CompilerTransaction, NoReplacePublishesOnlyAnAbsentRecord) { + expect(eq(Transaction::contents(record), replaced?"new":"old")); + expect(eq(std::string(std::istreambuf_iterator(held),std::istreambuf_iterator()), "old")); +}}; +UTest no_replace_publishes_only_an_absent_record{"CompilerTransaction/NoReplacePublishesOnlyAnAbsentRecord", [] { Transaction t; const auto source=t.root/"new"; const auto target=t.root/"status"; std::ofstream(source)<<"ready"; - ASSERT_TRUE(mcpplibs::os::fs::rename(source,target,false)); - EXPECT_EQ(Transaction::contents(target),"ready"); + expect(fatal(bool(mcpplibs::os::fs::rename(source,target,false)))); + expect(eq(Transaction::contents(target), "ready")); std::ofstream(source)<<"later"; - EXPECT_FALSE(mcpplibs::os::fs::rename(source,target,false)); - EXPECT_EQ(Transaction::contents(target),"ready"); -} -TEST(CompilerTransaction, RecordOpenAppearanceDriver) { - if(!mcpp::invocation::get("MCPP_TEST_RECORD_OPEN_DRIVER"))GTEST_SKIP()<<"subprocess driver"; + expect(!mcpplibs::os::fs::rename(source,target,false)); + expect(eq(Transaction::contents(target), "ready")); +}}; +UTest record_open_appearance_driver{"CompilerTransaction/RecordOpenAppearanceDriver", [] { + if(!mcpp::invocation::get("MCPP_TEST_RECORD_OPEN_DRIVER"))skip("subprocess driver"); auto record=tx::read("record","schema"); - EXPECT_TRUE(fs::exists("record-open-interposed")); - ASSERT_TRUE(record&&*record); - EXPECT_EQ((**record)[0],"published"); -} -TEST(CompilerTransaction, RecordAppearanceBetweenOpenAndExistsIsNotCorruption) { - if constexpr (!mcpplibs::os::is_linux)GTEST_SKIP()<<"Linux libc open interposition"; + expect(fs::exists("record-open-interposed")); + expect(fatal(record&&*record)); + expect(eq((**record)[0], "published")); +}}; +UTest record_appearance_between_open_and_exists_is_not_corruption{"CompilerTransaction/RecordAppearanceBetweenOpenAndExistsIsNotCorruption", [] { + if constexpr (!mcpplibs::os::is_linux)skip("Linux libc open interposition"); const auto runtime=mcpp::runtime::elf::inspect_elf_runtime(mcpp::invocation::self_exe_path()); if(!runtime || runtime->interp.find("ld-linux")==std::string::npos) - GTEST_SKIP()<<"open interposition requires the dynamic glibc loader"; + skip("open interposition requires the dynamic glibc loader"); Transaction t; const auto library=t.root/"record-open-appearance.so"; const auto source=fs::current_path()/"tests/e2e/helpers/record_open_appearance.c"; auto compiled=mcpp::run::execute({"/usr/bin/cc","-shared","-fPIC","-Wall","-Wextra","-Werror", source.string(),"-o",library.string(),"-ldl"},{.total=std::chrono::seconds(10)}); - ASSERT_EQ(compiled.exit_code,0)< refused_driver(const fs::path& root,std::string_view filter, @@ -316,10 +318,11 @@ std::optional refused_driver(const fs::path& root,std::str const auto source=fs::current_path()/"tests/e2e/helpers/record_open_refusal.c"; auto compiled=mcpp::run::execute({"/usr/bin/cc","-shared","-fPIC","-Wall","-Wextra","-Werror", source.string(),"-o",library.string(),"-ldl"},{.total=std::chrono::seconds(10)}); - if(compiled.exit_code!=0) {ADD_FAILURE()<exit_code,0)<output; + expect(fatal(bool(result))); + if (auto&& failed = expect(eq(result->exit_code, 0)); !failed) failed <output; // Four refusals: the reader before 2026.11.7.1 retried once and failed. - EXPECT_EQ(refusals(t.root/"record-refused"),4u); -} -TEST(CompilerTransaction, PersistentRefusalIsABoundedError) { - if(!preloadable())GTEST_SKIP()<<"open interposition requires Linux and the dynamic glibc loader"; + expect(eq(refusals(t.root/"record-refused"), 4u)); +}}; +UTest persistent_refusal_is_a_bounded_error{"CompilerTransaction/PersistentRefusalIsABoundedError", [] { + if(!preloadable())skip("open interposition requires Linux and the dynamic glibc loader"); Transaction t; - ASSERT_TRUE(tx::publish(t.root/"record","schema",{"published"})); + expect(fatal(bool(tx::publish(t.root/"record","schema",{"published"})))); const auto result=refused_driver(t.root,"RecordRefusalDriver", {{"MCPP_TEST_RECORD_REFUSAL_DRIVER","1"},{"MCPP_TEST_REFUSE_NAME","record"},{"MCPP_TEST_REFUSE_COUNT","-1"}}); - ASSERT_TRUE(result); - EXPECT_EQ(result->exit_code,0)<output; - EXPECT_GE(refusals(t.root/"record-refused"),2u); -} -TEST(CompilerTransaction, UnreadableControllerRecordIsNotAnEndedController) { - if(!preloadable())GTEST_SKIP()<<"open interposition requires Linux and the dynamic glibc loader"; + expect(fatal(bool(result))); + if (auto&& failed = expect(eq(result->exit_code, 0)); !failed) failed <output; + expect(ge(refusals(t.root/"record-refused"), 2u)); +}}; +UTest unreadable_controller_record_is_not_an_ended_controller{"CompilerTransaction/UnreadableControllerRecordIsNotAnEndedController", [] { + if(!preloadable())skip("open interposition requires Linux and the dynamic glibc loader"); Transaction t; auto a=t.prepare(); - ASSERT_TRUE(tx::publish(a.directory/"controller","mcpp-controller-v1",{"1","1",tx::spelling(t.generation/"actors/helper.lock")})); + expect(fatal(bool(tx::publish(a.directory/"controller","mcpp-controller-v1",{"1","1",tx::spelling(t.generation/"actors/helper.lock")})))); const auto result=refused_driver(t.root,"ControllerRefusalDriver", {{"MCPP_TEST_CONTROLLER_REFUSAL_DRIVER","1"},{"MCPP_TEST_ATTEMPT",a.directory.string()}, {"MCPP_TEST_REFUSE_NAME",(a.directory/"controller").string()}, {"MCPP_TEST_REFUSE_COUNT","-1"}}); - ASSERT_TRUE(result); - EXPECT_EQ(result->exit_code,0)<output; -} -TEST(CompilerTransaction, FailedCloseCannotPublishOrAcknowledgeARecord) { - if constexpr (!mcpplibs::os::is_linux)GTEST_SKIP()<<"Linux libc close failure control"; + expect(fatal(bool(result))); + if (auto&& failed = expect(eq(result->exit_code, 0)); !failed) failed <output; +}}; +UTest failed_close_cannot_publish_or_acknowledge_a_record{"CompilerTransaction/FailedCloseCannotPublishOrAcknowledgeARecord", [] { + if constexpr (!mcpplibs::os::is_linux)skip("Linux libc close failure control"); const auto runtime=mcpp::runtime::elf::inspect_elf_runtime(mcpp::invocation::self_exe_path()); if(!runtime || runtime->interp.find("ld-linux")==std::string::npos) - GTEST_SKIP()<<"close interposition requires the dynamic glibc loader"; + skip("close interposition requires the dynamic glibc loader"); Transaction t; const auto library=t.root/"record-close-failure.so"; const auto source=fs::current_path()/"tests/e2e/helpers/record_close_failure.c"; auto compiled=mcpp::run::execute({"/usr/bin/cc","-shared","-fPIC",source.string(), "-o",library.string(),"-ldl"},{.total=std::chrono::seconds(10)}); - ASSERT_EQ(compiled.exit_code,0)< #include #include #include import std; +import mcpplibs.mtest; import mcpp.config; import mcpp.fallback.config_migration; import mcpp.pm.index_spec; +using namespace mcpplibs::mtest; + namespace { std::filesystem::path make_tempdir(std::string_view name) { @@ -19,16 +21,16 @@ std::filesystem::path make_tempdir(std::string_view name) { } // namespace -TEST(Config, ProjectXlingsDataRootsIncludeLegacyAndNestedLayouts) { +UTest project_xlings_data_roots_include_legacy_and_nested_layouts{"Config/ProjectXlingsDataRootsIncludeLegacyAndNestedLayouts", [] { auto project = std::filesystem::path{"/tmp/mcpp-project"}; auto roots = mcpp::config::project_xlings_data_roots(project); - ASSERT_EQ(roots.size(), 2u); - EXPECT_EQ(roots[0], project / ".mcpp" / "data"); - EXPECT_EQ(roots[1], project / ".mcpp" / ".xlings" / "data"); -} + expect(fatal(eq(roots.size(), 2u))); + expect(eq(roots[0], project / ".mcpp" / "data")); + expect(eq(roots[1], project / ".mcpp" / ".xlings" / "data")); +}}; -TEST(Config, ProjectIndexDataInitializedChecksNestedXlingsData) { +UTest project_index_data_initialized_checks_nested_xlings_data{"Config/ProjectIndexDataInitializedChecksNestedXlingsData", [] { auto project = make_tempdir("mcpp-config-index-state"); auto nestedIndexDir = project / ".mcpp" / ".xlings" / "data" / "xim-index-repos"; std::filesystem::create_directories(nestedIndexDir); @@ -37,22 +39,21 @@ TEST(Config, ProjectIndexDataInitializedChecksNestedXlingsData) { os << "{}"; } - EXPECT_TRUE(mcpp::config::project_index_data_initialized(project)); + expect(bool(mcpp::config::project_index_data_initialized(project))); std::filesystem::remove_all(project); -} +}}; -TEST(Config, ResolveProjectIndexPathUsesProjectRootForRelativeLocalIndex) { +UTest resolve_project_index_path_uses_project_root_for_relative_local_index{"Config/ResolveProjectIndexPathUsesProjectRootForRelativeLocalIndex", [] { auto project = std::filesystem::path{"/tmp/mcpp-project"}; mcpp::pm::IndexSpec spec; spec.name = "xlings"; spec.path = "mcpp"; - EXPECT_EQ(mcpp::config::resolve_project_index_path(project, spec), - (project / "mcpp").lexically_normal()); -} + expect(eq(mcpp::config::resolve_project_index_path(project, spec), (project / "mcpp").lexically_normal())); +}}; -TEST(Config, ProjectIndexJsonEscapesLocalIndexPath) { +UTest project_index_json_escapes_local_index_path{"Config/ProjectIndexJsonEscapesLocalIndexPath", [] { auto project = make_tempdir("mcpp-config-json-escape"); auto index = project / "local" / "index"; std::filesystem::create_directories(index / "pkgs"); @@ -70,24 +71,24 @@ TEST(Config, ProjectIndexJsonEscapesLocalIndexPath) { indices.emplace(remote.name, remote); mcpp::config::GlobalConfig cfg; - ASSERT_TRUE(mcpp::config::ensure_project_index_dir(cfg, project, indices)); + expect(fatal(bool(mcpp::config::ensure_project_index_dir(cfg, project, indices)))); std::ifstream is(project / ".mcpp" / ".xlings.json"); - ASSERT_TRUE(is); + expect(fatal(bool(is))); std::stringstream ss; ss << is.rdbuf(); auto content = ss.str(); - EXPECT_NE(content.find(R"("name": "local\"dev")"), std::string::npos) + expect(ne(content.find(R"("name": "local\"dev")"), std::string::npos)) << content; - EXPECT_NE(content.find(index.generic_string()), std::string::npos) + expect(ne(content.find(index.generic_string()), std::string::npos)) << content; - EXPECT_NE(content.find(R"(https://example.com/a\\b\"c)"), std::string::npos) + expect(ne(content.find(R"(https://example.com/a\\b\"c)"), std::string::npos)) << content; is.close(); std::filesystem::remove_all(project); -} +}}; // The official global `xim` index must NOT be injected/exposed into the project // scope. `xim` (and its dynamic sub-indexes) are xlings GLOBAL-default indices — @@ -96,7 +97,7 @@ TEST(Config, ProjectIndexJsonEscapesLocalIndexPath) { // store instead of the shared registry, breaking ELF loader resolution for // build-dep tools. `xim:*` resolves at global scope via the registry-local clone. // See .agents/docs/2026-07-09-project-index-scope-global-infra-fix.md. -TEST(Config, ProjectIndexDirDoesNotInjectOfficialXimIndex) { +UTest project_index_dir_does_not_inject_official_xim_index{"Config/ProjectIndexDirDoesNotInjectOfficialXimIndex", [] { auto project = make_tempdir("mcpp-config-project-xim-index"); auto registry = make_tempdir("mcpp-config-registry"); auto official = registry / "data" / "xim-pkgindex"; @@ -118,30 +119,30 @@ TEST(Config, ProjectIndexDirDoesNotInjectOfficialXimIndex) { mcpp::config::GlobalConfig cfg; cfg.registryDir = registry; - ASSERT_TRUE(mcpp::config::ensure_project_index_dir(cfg, project, indices)); + expect(fatal(bool(mcpp::config::ensure_project_index_dir(cfg, project, indices)))); // xim is NOT copied/exposed into the project data dir (it stays global). auto projectOfficial = project / ".mcpp" / ".xlings" / "data" / "xim-pkgindex"; - EXPECT_FALSE(std::filesystem::exists(projectOfficial / "pkgs" / "p" / "python.lua")); + expect(!std::filesystem::exists(projectOfficial / "pkgs" / "p" / "python.lua")); // The user-declared local `compat` index IS exposed project-locally. auto projectLocal = project / ".mcpp" / ".xlings" / "data" / "compat"; - EXPECT_TRUE(std::filesystem::exists(projectLocal / "pkgs")); + expect(std::filesystem::exists(projectLocal / "pkgs")); // The seeded project .xlings.json index_repos must not contain `xim`. auto projJson = project / ".mcpp" / ".xlings.json"; if (std::filesystem::exists(projJson)) { std::ifstream is(projJson); std::stringstream ss; ss << is.rdbuf(); - EXPECT_EQ(ss.str().find("\"xim\""), std::string::npos); + expect(eq(ss.str().find("\"xim\""), std::string::npos)); } std::filesystem::remove_all(project); std::filesystem::remove_all(registry); -} +}}; -TEST(Config, ProjectLocalIndexStaleCacheIsRemoved) { +UTest project_local_index_stale_cache_is_removed{"Config/ProjectLocalIndexStaleCacheIsRemoved", [] { auto project = make_tempdir("mcpp-config-local-index-cache"); auto localIndex = project / "compat"; std::filesystem::create_directories(localIndex / "pkgs" / "c"); @@ -158,11 +159,11 @@ TEST(Config, ProjectLocalIndexStaleCacheIsRemoved) { indices.emplace(local.name, local); mcpp::config::GlobalConfig cfg; - ASSERT_TRUE(mcpp::config::ensure_project_index_dir(cfg, project, indices)); - EXPECT_FALSE(std::filesystem::exists(localIndex / ".xlings-index-cache.json")); + expect(fatal(bool(mcpp::config::ensure_project_index_dir(cfg, project, indices)))); + expect(!std::filesystem::exists(localIndex / ".xlings-index-cache.json")); std::filesystem::remove_all(project); -} +}}; // ── #267-A/#269: index org migration + artifact adoption ──────────────────── @@ -176,38 +177,38 @@ std::string read_all(const std::filesystem::path& p) { } // namespace -TEST(ConfigIndexMigration, CanonicalizeRewritesOrgAndName) { +UTest canonicalize_rewrites_org_and_name{"ConfigIndexMigration/CanonicalizeRewritesOrgAndName", [] { mcpp::config::GlobalConfig cfg; cfg.defaultIndex = "mcpp-index"; cfg.indexRepos.push_back({ "mcpp-index", std::string(mcpp::config::kMcpplibsIndexUrlLegacy) }); mcpp::config::canonicalize_legacy_index_names(cfg); - ASSERT_EQ(cfg.indexRepos.size(), 1u); - EXPECT_EQ(cfg.defaultIndex, "mcpplibs"); - EXPECT_EQ(cfg.indexRepos[0].name, "mcpplibs"); - EXPECT_EQ(cfg.indexRepos[0].url, mcpp::config::kMcpplibsIndexUrl); -} + expect(fatal(eq(cfg.indexRepos.size(), 1u))); + expect(eq(cfg.defaultIndex, "mcpplibs")); + expect(eq(cfg.indexRepos[0].name, "mcpplibs")); + expect(eq(cfg.indexRepos[0].url, mcpp::config::kMcpplibsIndexUrl)); +}}; -TEST(ConfigIndexMigration, CanonicalizeFoldsLegacyAndDefaultEntries) { +UTest canonicalize_folds_legacy_and_default_entries{"ConfigIndexMigration/CanonicalizeFoldsLegacyAndDefaultEntries", [] { mcpp::config::GlobalConfig cfg; cfg.indexRepos.push_back({ "mcpplibs", std::string(mcpp::config::kMcpplibsIndexUrlLegacy) }); cfg.indexRepos.push_back({ "mcpplibs", std::string(mcpp::config::kMcpplibsIndexUrl) }); mcpp::config::canonicalize_legacy_index_names(cfg); - EXPECT_EQ(cfg.indexRepos.size(), 1u); -} + expect(eq(cfg.indexRepos.size(), 1u)); +}}; -TEST(ConfigIndexMigration, CanonicalizeLeavesForeignReposAlone) { +UTest canonicalize_leaves_foreign_repos_alone{"ConfigIndexMigration/CanonicalizeLeavesForeignReposAlone", [] { mcpp::config::GlobalConfig cfg; cfg.indexRepos.push_back({ "other", "https://example.com/other-index.git" }); mcpp::config::canonicalize_legacy_index_names(cfg); - ASSERT_EQ(cfg.indexRepos.size(), 1u); - EXPECT_EQ(cfg.indexRepos[0].name, "other"); - EXPECT_EQ(cfg.indexRepos[0].url, "https://example.com/other-index.git"); -} + expect(fatal(eq(cfg.indexRepos.size(), 1u))); + expect(eq(cfg.indexRepos[0].name, "other")); + expect(eq(cfg.indexRepos[0].url, "https://example.com/other-index.git")); +}}; -TEST(ConfigIndexMigration, MigrateConfigTomlRewritesOrgUrlIdempotently) { +UTest migrate_config_toml_rewrites_org_url_idempotently{"ConfigIndexMigration/MigrateConfigTomlRewritesOrgUrlIdempotently", [] { auto dir = make_tempdir("mcpp-migrate-toml"); auto p = dir / "config.toml"; { @@ -215,25 +216,25 @@ TEST(ConfigIndexMigration, MigrateConfigTomlRewritesOrgUrlIdempotently) { os << "[index]\ndefault = \"mcpplibs\"\n\n[index.repos.\"mcpplibs\"]\n" "url = \"https://github.com/mcpp-community/mcpp-index.git\"\n"; } - EXPECT_TRUE(mcpp::fallback::migrate_config_toml_index_names(p)); + expect(bool(mcpp::fallback::migrate_config_toml_index_names(p))); auto text = read_all(p); - EXPECT_NE(text.find("github.com/mcpplibs/mcpp-index.git"), std::string::npos); - EXPECT_EQ(text.find("mcpp-community/mcpp-index"), std::string::npos); - EXPECT_FALSE(mcpp::fallback::migrate_config_toml_index_names(p)); // idempotent - EXPECT_EQ(read_all(p), text); + expect(ne(text.find("github.com/mcpplibs/mcpp-index.git"), std::string::npos)); + expect(eq(text.find("mcpp-community/mcpp-index"), std::string::npos)); + expect(!mcpp::fallback::migrate_config_toml_index_names(p)); // idempotent + expect(eq(read_all(p), text)); std::filesystem::remove_all(dir); -} +}}; -TEST(ConfigIndexMigration, CanonicalizeFillsDefaultArtifactForOfficialRepo) { +UTest canonicalize_fills_default_artifact_for_official_repo{"ConfigIndexMigration/CanonicalizeFillsDefaultArtifactForOfficialRepo", [] { mcpp::config::GlobalConfig cfg; cfg.indexRepos.push_back({ "mcpplibs", std::string(mcpp::config::kMcpplibsIndexUrlLegacy) }); mcpp::config::canonicalize_legacy_index_names(cfg); - ASSERT_EQ(cfg.indexRepos.size(), 1u); - EXPECT_EQ(cfg.indexRepos[0].artifact, mcpp::config::kMcpplibsIndexArtifact); -} + expect(fatal(eq(cfg.indexRepos.size(), 1u))); + expect(eq(cfg.indexRepos[0].artifact, mcpp::config::kMcpplibsIndexArtifact)); +}}; -TEST(ConfigIndexMigration, CanonicalizeRespectsExplicitGitSourceAndCustomArtifact) { +UTest canonicalize_respects_explicit_git_source_and_custom_artifact{"ConfigIndexMigration/CanonicalizeRespectsExplicitGitSourceAndCustomArtifact", [] { mcpp::config::GlobalConfig cfg; mcpp::config::IndexRepo git{ "mcpplibs", std::string(mcpp::config::kMcpplibsIndexUrl) }; @@ -245,13 +246,13 @@ TEST(ConfigIndexMigration, CanonicalizeRespectsExplicitGitSourceAndCustomArtifac cfg.indexRepos.push_back(custom); cfg.indexRepos.push_back({ "other", "https://example.com/other-index.git" }); mcpp::config::canonicalize_legacy_index_names(cfg); - ASSERT_EQ(cfg.indexRepos.size(), 3u); - EXPECT_TRUE(cfg.indexRepos[0].artifact.empty()); // explicit git opt-out - EXPECT_EQ(cfg.indexRepos[1].artifact, "https://example.com/my-mirror"); - EXPECT_TRUE(cfg.indexRepos[2].artifact.empty()); // foreign repo untouched -} + expect(fatal(eq(cfg.indexRepos.size(), 3u))); + expect(cfg.indexRepos[0].artifact.empty()); // explicit git opt-out + expect(eq(cfg.indexRepos[1].artifact, "https://example.com/my-mirror")); + expect(cfg.indexRepos[2].artifact.empty()); // foreign repo untouched +}}; -TEST(ConfigIndexMigration, XlingsJsonHealsPrettyLegacyEntry) { +UTest xlings_json_heals_pretty_legacy_entry{"ConfigIndexMigration/XlingsJsonHealsPrettyLegacyEntry", [] { auto dir = make_tempdir("mcpp-migrate-xj"); auto p = dir / ".xlings.json"; { @@ -261,21 +262,20 @@ TEST(ConfigIndexMigration, XlingsJsonHealsPrettyLegacyEntry) { "\"https://github.com/mcpp-community/mcpp-index.git\" }\n" " ],\n \"subos\": \"default\",\n \"mirror\": \"auto\"\n}\n"; } - EXPECT_TRUE(mcpp::fallback::migrate_xlings_json_index_names(p)); + expect(bool(mcpp::fallback::migrate_xlings_json_index_names(p))); auto text = read_all(p); - EXPECT_NE(text.find( + expect(ne(text.find( "\"url\": \"https://github.com/mcpplibs/mcpp-index.git\", " "\"artifact\": { \"GLOBAL\": \"https://github.com/xlings-res/mcpp-index\", " - "\"CN\": \"https://gitcode.com/xlings-res/mcpp-index\" }"), - std::string::npos) << text; - EXPECT_NE(text.find("\"subos\": \"default\""), std::string::npos); - EXPECT_EQ(text.find("mcpp-community/mcpp-index"), std::string::npos); - EXPECT_FALSE(mcpp::fallback::migrate_xlings_json_index_names(p)); // idempotent - EXPECT_EQ(read_all(p), text); + "\"CN\": \"https://gitcode.com/xlings-res/mcpp-index\" }"), std::string::npos)) << text; + expect(ne(text.find("\"subos\": \"default\""), std::string::npos)); + expect(eq(text.find("mcpp-community/mcpp-index"), std::string::npos)); + expect(!mcpp::fallback::migrate_xlings_json_index_names(p)); // idempotent + expect(eq(read_all(p), text)); std::filesystem::remove_all(dir); -} +}}; -TEST(ConfigIndexMigration, XlingsJsonHealsCompactLegacyNameAndUrl) { +UTest xlings_json_heals_compact_legacy_name_and_url{"ConfigIndexMigration/XlingsJsonHealsCompactLegacyNameAndUrl", [] { auto dir = make_tempdir("mcpp-migrate-xjc"); auto p = dir / ".xlings.json"; { @@ -284,21 +284,20 @@ TEST(ConfigIndexMigration, XlingsJsonHealsCompactLegacyNameAndUrl) { "\"url\":\"https://github.com/mcpp-community/mcpp-index.git\"}]," "\"mirror\":\"auto\"}"; } - EXPECT_TRUE(mcpp::fallback::migrate_xlings_json_index_names(p)); + expect(bool(mcpp::fallback::migrate_xlings_json_index_names(p))); auto text = read_all(p); - EXPECT_NE(text.find("\"name\":\"mcpplibs\""), std::string::npos); - EXPECT_NE(text.find( + expect(ne(text.find("\"name\":\"mcpplibs\""), std::string::npos)); + expect(ne(text.find( "\"url\":\"https://github.com/mcpplibs/mcpp-index.git\", " "\"artifact\": { \"GLOBAL\": \"https://github.com/xlings-res/mcpp-index\", " - "\"CN\": \"https://gitcode.com/xlings-res/mcpp-index\" }"), - std::string::npos) << text; - EXPECT_FALSE(mcpp::fallback::migrate_xlings_json_index_names(p)); + "\"CN\": \"https://gitcode.com/xlings-res/mcpp-index\" }"), std::string::npos)) << text; + expect(!mcpp::fallback::migrate_xlings_json_index_names(p)); std::filesystem::remove_all(dir); -} +}}; // #648 A6. A home seeded with the GitHub-only default artifact gains the CN half // of the region object once; the migration is idempotent afterwards. -TEST(ConfigIndexMigration, XlingsJsonUpgradesTheFlatDefaultArtifactToTheRegionObject) { +UTest xlings_json_upgrades_the_flat_default_artifact_to_the_region_object{"ConfigIndexMigration/XlingsJsonUpgradesTheFlatDefaultArtifactToTheRegionObject", [] { auto dir = make_tempdir("mcpp-migrate-xja"); auto p = dir / ".xlings.json"; { @@ -309,17 +308,17 @@ TEST(ConfigIndexMigration, XlingsJsonUpgradesTheFlatDefaultArtifactToTheRegionOb "\"artifact\": \"https://github.com/xlings-res/mcpp-index\" }\n" " ]\n}\n"; } - EXPECT_TRUE(mcpp::fallback::migrate_xlings_json_index_names(p)); + expect(bool(mcpp::fallback::migrate_xlings_json_index_names(p))); auto text = read_all(p); - EXPECT_NE(text.find("\"artifact\": { \"GLOBAL\": \"https://github.com/xlings-res/mcpp-index\", " - "\"CN\": \"https://gitcode.com/xlings-res/mcpp-index\" }"), std::string::npos) << text; - EXPECT_FALSE(mcpp::fallback::migrate_xlings_json_index_names(p)); - EXPECT_EQ(read_all(p), text); + expect(ne(text.find("\"artifact\": { \"GLOBAL\": \"https://github.com/xlings-res/mcpp-index\", " + "\"CN\": \"https://gitcode.com/xlings-res/mcpp-index\" }"), std::string::npos)) << text; + expect(!mcpp::fallback::migrate_xlings_json_index_names(p)); + expect(eq(read_all(p), text)); std::filesystem::remove_all(dir); -} +}}; // An artifact base the user wrote is theirs: no CN half is added to it. -TEST(ConfigIndexMigration, XlingsJsonLeavesAUserArtifactBaseAlone) { +UTest xlings_json_leaves_a_user_artifact_base_alone{"ConfigIndexMigration/XlingsJsonLeavesAUserArtifactBaseAlone", [] { auto dir = make_tempdir("mcpp-migrate-xju"); auto p = dir / ".xlings.json"; std::string body = @@ -332,7 +331,7 @@ TEST(ConfigIndexMigration, XlingsJsonLeavesAUserArtifactBaseAlone) { std::ofstream os(p); os << body; } - EXPECT_FALSE(mcpp::fallback::migrate_xlings_json_index_names(p)); - EXPECT_EQ(read_all(p), body); + expect(!mcpp::fallback::migrate_xlings_json_index_names(p)); + expect(eq(read_all(p), body)); std::filesystem::remove_all(dir); -} +}}; diff --git a/tests/unit/test_configure.cpp b/tests/unit/test_configure.cpp index 4c5df7777..4712ad93f 100644 --- a/tests/unit/test_configure.cpp +++ b/tests/unit/test_configure.cpp @@ -1,11 +1,13 @@ -#include import std; +import mcpplibs.mtest; import mcpp.source_kind; import mcpp.build.configure; import mcpp.build.plan; import mcpp.toolchain.model; +using namespace mcpplibs::mtest; + namespace { struct TempDir { @@ -27,7 +29,7 @@ void write_file(const std::filesystem::path& path, std::string_view text) { out << text; } -TEST(ConfigurePrerequisites, StagesOnlyBmisNeededByLanguageTools) { +UTest stages_only_bmis_needed_by_language_tools{"ConfigurePrerequisites/StagesOnlyBmisNeededByLanguageTools", [] { TempDir temp; mcpp::build::BuildPlan plan; plan.outputDir = temp.path / "out"; @@ -48,14 +50,14 @@ TEST(ConfigurePrerequisites, StagesOnlyBmisNeededByLanguageTools) { plan.compileUnits.push_back(dep); auto staged = mcpp::build::stage_configure_prerequisites(plan); - ASSERT_TRUE(staged.has_value()) << staged.error(); - EXPECT_TRUE(std::filesystem::exists(plan.outputDir / "pcm.cache/std.pcm")); - EXPECT_TRUE(std::filesystem::exists(plan.outputDir / "pcm.cache/std.compat.pcm")); - EXPECT_TRUE(std::filesystem::exists(plan.outputDir / "pcm.cache/demo.dep.pcm")); - EXPECT_FALSE(std::filesystem::exists(plan.outputDir / dep.object)); -} + if (auto&& failed = expect(fatal(staged.has_value())); !failed) failed << staged.error(); + expect(std::filesystem::exists(plan.outputDir / "pcm.cache/std.pcm")); + expect(std::filesystem::exists(plan.outputDir / "pcm.cache/std.compat.pcm")); + expect(std::filesystem::exists(plan.outputDir / "pcm.cache/demo.dep.pcm")); + expect(!std::filesystem::exists(plan.outputDir / dep.object)); +}}; -TEST(ConfigurePrerequisites, MissingCachedBmiFailsBeforePublication) { +UTest missing_cached_bmi_fails_before_publication{"ConfigurePrerequisites/MissingCachedBmiFailsBeforePublication", [] { TempDir temp; mcpp::build::BuildPlan plan; plan.outputDir = temp.path / "out"; @@ -68,8 +70,8 @@ TEST(ConfigurePrerequisites, MissingCachedBmiFailsBeforePublication) { plan.compileUnits.push_back(dep); auto staged = mcpp::build::stage_configure_prerequisites(plan); - ASSERT_FALSE(staged.has_value()); - EXPECT_NE(staged.error().find("demo.dep"), std::string::npos); -} + expect(fatal(!staged.has_value())); + expect(ne(staged.error().find("demo.dep"), std::string::npos)); +}}; } // namespace diff --git a/tests/unit/test_core_vendor_probes.cpp b/tests/unit/test_core_vendor_probes.cpp index 833b62438..a6f7e6ea6 100644 --- a/tests/unit/test_core_vendor_probes.cpp +++ b/tests/unit/test_core_vendor_probes.cpp @@ -1,6 +1,8 @@ -#include import std; +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; // The engine owns no vendor probes. // @@ -54,10 +56,10 @@ std::string without_comments(std::string_view source) { } // namespace -TEST(CoreVendorProbes, TheEngineNamesNoVendorTool) { +UTest the_engine_names_no_vendor_tool{"CoreVendorProbes/TheEngineNamesNoVendorTool", [] { auto repo = std::filesystem::path(__FILE__).parent_path().parent_path().parent_path(); auto src = repo / "src"; - ASSERT_TRUE(std::filesystem::is_directory(src)) << src; + expect(fatal(std::filesystem::is_directory(src))) << src; // Tools, probes and locations. Each one is something only a rule package // has business invoking or reading. const std::vector tools = { @@ -86,11 +88,10 @@ TEST(CoreVendorProbes, TheEngineNamesNoVendorTool) { std::string lowered = tool; std::ranges::transform(lowered, lowered.begin(), [](unsigned char c) { return std::tolower(c); }); - EXPECT_EQ(lineText.find(lowered), std::string::npos) - << it->path() << ':' << line << " names a vendor tool: " << tool; + if (auto&& failed = expect(eq(lineText.find(lowered), std::string::npos)); !failed) failed << it->path() << ':' << line << " names a vendor tool: " << tool; } } } // The denominator: a scan that found no files would pass vacuously. - EXPECT_GT(files, 100u); -} + expect(gt(files, 100u)); +}}; diff --git a/tests/unit/test_cppfly.cpp b/tests/unit/test_cppfly.cpp index 45235068d..64fb0f621 100644 --- a/tests/unit/test_cppfly.cpp +++ b/tests/unit/test_cppfly.cpp @@ -3,12 +3,14 @@ // the c++latest/c++fly-aware std-flag spelling. // Table facts pinned by on-machine probes (gcc 16.1.0, 2026-07-14) — see // .agents/docs/2026-07-14-std-features-experimental-gate-design.md §2/§11-Q3. -#include import std; +import mcpplibs.mtest; import mcpp.toolchain.cppfly; import mcpp.toolchain.model; +using namespace mcpplibs::mtest; + using namespace mcpp::toolchain; namespace { @@ -29,93 +31,93 @@ const cppfly::FeatureState* feature(const cppfly::Resolution& r, std::string_vie } } // namespace -TEST(CppFly, CompilerMajor) { - EXPECT_EQ(cppfly::compiler_major(tc_of(CompilerId::GCC, "16.1.0")), 16); - EXPECT_EQ(cppfly::compiler_major(tc_of(CompilerId::Clang, "22.1.8")), 22); - EXPECT_EQ(cppfly::compiler_major(tc_of(CompilerId::GCC, "")), 0); -} +UTest cpp_fly_compiler_major{"CppFly/CompilerMajor", [] { + expect(eq(cppfly::compiler_major(tc_of(CompilerId::GCC, "16.1.0")), 16)); + expect(eq(cppfly::compiler_major(tc_of(CompilerId::Clang, "22.1.8")), 22)); + expect(eq(cppfly::compiler_major(tc_of(CompilerId::GCC, "")), 0)); +}}; -TEST(CppFly, LatestStdCanonical) { +UTest cpp_fly_latest_std_canonical{"CppFly/LatestStdCanonical", [] { int lvl = 0; - EXPECT_EQ(cppfly::latest_std_canonical(tc_of(CompilerId::GCC, "16.1.0"), &lvl), "c++26"); - EXPECT_EQ(lvl, 26); - EXPECT_EQ(cppfly::latest_std_canonical(tc_of(CompilerId::GCC, "13.2.0")), "c++23"); - EXPECT_EQ(cppfly::latest_std_canonical(tc_of(CompilerId::Clang, "22.1.8")), "c++2c"); - EXPECT_EQ(cppfly::latest_std_canonical(tc_of(CompilerId::MSVC, "19.44")), "c++26"); -} + expect(eq(cppfly::latest_std_canonical(tc_of(CompilerId::GCC, "16.1.0"), &lvl), "c++26")); + expect(eq(lvl, 26)); + expect(eq(cppfly::latest_std_canonical(tc_of(CompilerId::GCC, "13.2.0")), "c++23")); + expect(eq(cppfly::latest_std_canonical(tc_of(CompilerId::Clang, "22.1.8")), "c++2c")); + expect(eq(cppfly::latest_std_canonical(tc_of(CompilerId::MSVC, "19.44")), "c++26")); +}}; -TEST(CppFly, ResolveGcc16) { +UTest resolve_gcc16{"CppFly/ResolveGcc16", [] { auto r = cppfly::resolve(tc_of(CompilerId::GCC, "16.1.0")); - EXPECT_EQ(r.stdCanonical, "c++26"); - EXPECT_TRUE(has_flag(r.flags, "-freflection")); + expect(eq(r.stdCanonical, "c++26")); + expect(has_flag(r.flags, "-freflection")); auto* refl = feature(r, "reflection"); - ASSERT_NE(refl, nullptr); - EXPECT_TRUE(refl->enabled); - EXPECT_EQ(refl->paper, "P2996"); + expect(fatal(ne(refl, nullptr))); + expect(bool(refl->enabled)); + expect(eq(refl->paper, "P2996")); // Contracts: enabled by -std=c++26 alone on GCC 16 (probe-pinned) — no // extra flag, but reported enabled for the summary. auto* con = feature(r, "contracts"); - ASSERT_NE(con, nullptr); - EXPECT_TRUE(con->enabled); - EXPECT_TRUE(con->flags.empty()); + expect(fatal(ne(con, nullptr))); + expect(bool(con->enabled)); + expect(con->flags.empty()); // libstdc++ ships experimental bits ungated. - EXPECT_FALSE(has_flag(r.flags, "-fexperimental-library")); -} + expect(!has_flag(r.flags, "-fexperimental-library")); +}}; -TEST(CppFly, ResolveGcc15SkipsByVersion) { +UTest resolve_gcc15_skips_by_version{"CppFly/ResolveGcc15SkipsByVersion", [] { auto r = cppfly::resolve(tc_of(CompilerId::GCC, "15.1.0")); - EXPECT_TRUE(r.flags.empty()); + expect(r.flags.empty()); auto* refl = feature(r, "reflection"); - ASSERT_NE(refl, nullptr); - EXPECT_FALSE(refl->enabled); - EXPECT_NE(refl->reason.find("16"), std::string::npos); // names the version floor -} + expect(fatal(ne(refl, nullptr))); + expect(!refl->enabled); + expect(ne(refl->reason.find("16"), std::string::npos)); // names the version floor +}}; -TEST(CppFly, ResolveClangLibcxx) { +UTest resolve_clang_libcxx{"CppFly/ResolveClangLibcxx", [] { auto r = cppfly::resolve(tc_of(CompilerId::Clang, "22.1.8", "libc++")); - EXPECT_EQ(r.stdCanonical, "c++2c"); - EXPECT_TRUE(has_flag(r.flags, "-fexperimental-library")); + expect(eq(r.stdCanonical, "c++2c")); + expect(has_flag(r.flags, "-fexperimental-library")); auto* refl = feature(r, "reflection"); - ASSERT_NE(refl, nullptr); - EXPECT_FALSE(refl->enabled); + expect(fatal(ne(refl, nullptr))); + expect(!refl->enabled); auto* lib = feature(r, "experimental-library"); - ASSERT_NE(lib, nullptr); - EXPECT_TRUE(lib->enabled); -} + expect(fatal(ne(lib, nullptr))); + expect(bool(lib->enabled)); +}}; -TEST(CppFly, ResolveMsvcAllSkipped) { +UTest resolve_msvc_all_skipped{"CppFly/ResolveMsvcAllSkipped", [] { auto r = cppfly::resolve(tc_of(CompilerId::MSVC, "19.44", "msvc-stl")); - EXPECT_TRUE(r.flags.empty()); - for (auto& f : r.features) EXPECT_FALSE(f.enabled); -} + expect(r.flags.empty()); + for (auto& f : r.features) expect(!f.enabled); +}}; -TEST(CppFly, StdFlagResolvesLatestAndFly) { +UTest std_flag_resolves_latest_and_fly{"CppFly/StdFlagResolvesLatestAndFly", [] { auto gcc16 = tc_of(CompilerId::GCC, "16.1.0"); - EXPECT_EQ(cppfly::std_flag(gcc16, "c++fly", 1000), "-std=c++26"); + expect(eq(cppfly::std_flag(gcc16, "c++fly", 1000), "-std=c++26")); // standard = "c++latest" used to reach the GNU driver as the invalid // spelling -std=c++latest (design §11-Q5) — now resolves to the family // latest. - EXPECT_EQ(cppfly::std_flag(gcc16, "c++latest", 999), "-std=c++26"); - EXPECT_EQ(cppfly::std_flag(gcc16, "c++23", 23), "-std=c++23"); - EXPECT_EQ(cppfly::std_flag(gcc16, "gnu++23", 23), "-std=gnu++23"); - EXPECT_EQ(cppfly::std_flag(tc_of(CompilerId::Clang, "22.1.8"), "c++fly", 1000), "-std=c++2c"); - EXPECT_EQ(cppfly::std_flag(tc_of(CompilerId::MSVC, "19.44"), "c++fly", 1000), "/std:c++latest"); - EXPECT_EQ(cppfly::std_flag(tc_of(CompilerId::MSVC, "19.44"), "c++latest", 999), "/std:c++latest"); + expect(eq(cppfly::std_flag(gcc16, "c++latest", 999), "-std=c++26")); + expect(eq(cppfly::std_flag(gcc16, "c++23", 23), "-std=c++23")); + expect(eq(cppfly::std_flag(gcc16, "gnu++23", 23), "-std=gnu++23")); + expect(eq(cppfly::std_flag(tc_of(CompilerId::Clang, "22.1.8"), "c++fly", 1000), "-std=c++2c")); + expect(eq(cppfly::std_flag(tc_of(CompilerId::MSVC, "19.44"), "c++fly", 1000), "/std:c++latest")); + expect(eq(cppfly::std_flag(tc_of(CompilerId::MSVC, "19.44"), "c++latest", 999), "/std:c++latest")); // A plain level is passed through untouched — c++20 must NOT be lifted to // the family latest by the >= 999 branch. - EXPECT_EQ(cppfly::std_flag(gcc16, "c++20", 20), "-std=c++20"); - EXPECT_EQ(cppfly::std_flag(gcc16, "gnu++20", 20), "-std=gnu++20"); - EXPECT_EQ(cppfly::std_flag(tc_of(CompilerId::Clang, "22.1.8"), "c++20", 20), "-std=c++20"); - EXPECT_EQ(cppfly::std_flag(tc_of(CompilerId::MSVC, "19.44"), "c++20", 20), "/std:c++20"); -} + expect(eq(cppfly::std_flag(gcc16, "c++20", 20), "-std=c++20")); + expect(eq(cppfly::std_flag(gcc16, "gnu++20", 20), "-std=gnu++20")); + expect(eq(cppfly::std_flag(tc_of(CompilerId::Clang, "22.1.8"), "c++20", 20), "-std=c++20")); + expect(eq(cppfly::std_flag(tc_of(CompilerId::MSVC, "19.44"), "c++20", 20), "/std:c++20")); +}}; -TEST(CppFly, EffectiveDialectFlagsDedup) { +UTest effective_dialect_flags_dedup{"CppFly/EffectiveDialectFlagsDedup", [] { auto gcc16 = tc_of(CompilerId::GCC, "16.1.0"); // User hand-wrote -freflection too — the fly gate must not duplicate it. auto out = cppfly::effective_dialect_flags(gcc16, true, {"-freflection", "-fchar8_t"}); - EXPECT_EQ(std::count(out.begin(), out.end(), std::string("-freflection")), 1); - EXPECT_TRUE(has_flag(out, "-fchar8_t")); + expect(eq(std::count(out.begin(), out.end(), std::string("-freflection")), 1)); + expect(has_flag(out, "-fchar8_t")); // Not experimental → manifest flags pass through untouched. auto off = cppfly::effective_dialect_flags(gcc16, false, {"-fchar8_t"}); - EXPECT_EQ(off, (std::vector{"-fchar8_t"})); -} + expect(eq(off, (std::vector{"-fchar8_t"}))); +}}; diff --git a/tests/unit/test_cxx_layer_standard.cpp b/tests/unit/test_cxx_layer_standard.cpp index 82df82945..cb940a240 100644 --- a/tests/unit/test_cxx_layer_standard.cpp +++ b/tests/unit/test_cxx_layer_standard.cpp @@ -9,9 +9,9 @@ // the plan are assertions on the compile edge, the scan edge and both build // databases. -#include import std; +import mcpplibs.mtest; import mcpp.build.plan; import mcpp.manifest; import mcpp.modgraph.graph; @@ -19,6 +19,8 @@ import mcpp.modgraph.scanner; import mcpp.source_kind; import mcpp.toolchain.model; +using namespace mcpplibs::mtest; + using namespace mcpp::build; namespace { @@ -130,7 +132,7 @@ std::map> plan_flags(const Fixture& f) { auto plan = make_plan(root, clangLike(), {}, graph, topo, packages, projectRoot, projectRoot / "target" / "t", {}, {}, {}); - EXPECT_TRUE(plan.has_value()) << (plan ? "" : plan.error()); + if (auto&& failed = expect(plan.has_value()); !failed) failed << (plan ? "" : plan.error()); std::map> out; if (!plan) return out; for (auto const& cu : plan->compileUnits) @@ -144,73 +146,73 @@ bool has(const std::vector& v, std::string_view flag) { } // namespace -TEST(CxxLayerStandard, ProviderImplementationUnitGetsTheStatedLevel) { +UTest provider_implementation_unit_gets_the_stated_level{"CxxLayerStandard/ProviderImplementationUnitGetsTheStatedLevel", [] { auto flags = plan_flags(Fixture{}); - ASSERT_TRUE(flags.contains("new.cpp")); - ASSERT_FALSE(flags["new.cpp"].empty()); - EXPECT_EQ(flags["new.cpp"].back(), "-std=c++23"); -} + expect(fatal(flags.contains("new.cpp"))); + expect(fatal(!(flags["new.cpp"].empty()))); + expect(eq(flags["new.cpp"].back(), "-std=c++23")); +}}; -TEST(CxxLayerStandard, UnitsThatTouchAModuleKeepTheGraphLevel) { +UTest units_that_touch_a_module_keep_the_graph_level{"CxxLayerStandard/UnitsThatTouchAModuleKeepTheGraphLevel", [] { auto flags = plan_flags(Fixture{}); - EXPECT_FALSE(has(flags["iface.cppm"], "-std=c++23")); // provides a module - EXPECT_FALSE(has(flags["importer.cpp"], "-std=c++23")); // imports std - EXPECT_FALSE(has(flags["iface_impl.cpp"], "-std=c++23")); // `module M;` - EXPECT_FALSE(has(flags["c_part.c"], "-std=c++23")); // not C++ -} + expect(!has(flags["iface.cppm"], "-std=c++23")); // provides a module + expect(!has(flags["importer.cpp"], "-std=c++23")); // imports std + expect(!has(flags["iface_impl.cpp"], "-std=c++23")); // `module M;` + expect(!has(flags["c_part.c"], "-std=c++23")); // not C++ +}}; -TEST(CxxLayerStandard, OtherPackagesKeepTheOneStandardRule) { +UTest other_packages_keep_the_one_standard_rule{"CxxLayerStandard/OtherPackagesKeepTheOneStandardRule", [] { auto flags = plan_flags(Fixture{}); - EXPECT_FALSE(has(flags["plain.cpp"], "-std=c++23")); - EXPECT_FALSE(has(flags["app.cpp"], "-std=c++23")); -} + expect(!has(flags["plain.cpp"], "-std=c++23")); + expect(!has(flags["app.cpp"], "-std=c++23")); +}}; -TEST(CxxLayerStandard, AProviderThatStatesNothingIsUnchanged) { +UTest a_provider_that_states_nothing_is_unchanged{"CxxLayerStandard/AProviderThatStatesNothingIsUnchanged", [] { Fixture f; f.providerStandard.clear(); auto flags = plan_flags(f); for (auto const& [file, v] : flags) for (auto const& flag : v) - EXPECT_FALSE(flag.starts_with("-std=")) << file << ": " << flag; -} + expect(!flag.starts_with("-std=")) << file << ": " << flag; +}}; -TEST(CxxLayerStandard, OnlyTheCurrentLayerSpellingIsEnough) { +UTest only_the_current_layer_spelling_is_enough{"CxxLayerStandard/OnlyTheCurrentLayerSpellingIsEnough", [] { Fixture f; f.providerProvides = {"mcpp:c++-abi=libc++"}; auto flags = plan_flags(f); - ASSERT_FALSE(flags["new.cpp"].empty()); - EXPECT_EQ(flags["new.cpp"].back(), "-std=c++23"); -} + expect(fatal(!(flags["new.cpp"].empty()))); + expect(eq(flags["new.cpp"].back(), "-std=c++23")); +}}; -TEST(CxxLayerStandard, AStatedStandardWithoutTheLayerIsNotAnException) { +UTest a_stated_standard_without_the_layer_is_not_an_exception{"CxxLayerStandard/AStatedStandardWithoutTheLayerIsNotAnException", [] { Fixture f; f.providerProvides.clear(); auto flags = plan_flags(f); - EXPECT_FALSE(has(flags["new.cpp"], "-std=c++23")); -} + expect(!has(flags["new.cpp"], "-std=c++23")); +}}; -TEST(CxxLayerStandard, TheLevelIsExactlyTheStatedOneUnderAHigherGraph) { +UTest the_level_is_exactly_the_stated_one_under_a_higher_graph{"CxxLayerStandard/TheLevelIsExactlyTheStatedOneUnderAHigherGraph", [] { Fixture f; f.graphStandard = "c++26"; auto flags = plan_flags(f); - ASSERT_FALSE(flags["new.cpp"].empty()); - EXPECT_EQ(flags["new.cpp"].back(), "-std=c++23"); -} + expect(fatal(!(flags["new.cpp"].empty()))); + expect(eq(flags["new.cpp"].back(), "-std=c++23")); +}}; -TEST(CxxLayerStandard, TheSameLevelAsTheGraphAddsNothing) { +UTest the_same_level_as_the_graph_adds_nothing{"CxxLayerStandard/TheSameLevelAsTheGraphAddsNothing", [] { Fixture f; f.graphStandard = "c++23"; auto flags = plan_flags(f); - EXPECT_FALSE(has(flags["new.cpp"], "-std=c++23")); -} + expect(!has(flags["new.cpp"], "-std=c++23")); +}}; -TEST(CxxLayerStandard, ThePredicateNamesEitherSpellingOfTheLayer) { +UTest the_predicate_names_either_spelling_of_the_layer{"CxxLayerStandard/ThePredicateNamesEitherSpellingOfTheLayer", [] { mcpp::manifest::Manifest m; - EXPECT_FALSE(mcpp::manifest::provides_cxx_layer(m)); + expect(!mcpp::manifest::provides_cxx_layer(m)); m.provides = {"hosted-standard-library"}; - EXPECT_TRUE(mcpp::manifest::provides_cxx_layer(m)); + expect(bool(mcpp::manifest::provides_cxx_layer(m))); m.provides = {"mcpp:c++-abi=libc++"}; - EXPECT_TRUE(mcpp::manifest::provides_cxx_layer(m)); + expect(bool(mcpp::manifest::provides_cxx_layer(m))); m.provides = {"mcpp:c-abi=musl"}; - EXPECT_FALSE(mcpp::manifest::provides_cxx_layer(m)); -} + expect(!mcpp::manifest::provides_cxx_layer(m)); +}}; diff --git a/tests/unit/test_depfile.cpp b/tests/unit/test_depfile.cpp index 0405e9482..5f945fd18 100644 --- a/tests/unit/test_depfile.cpp +++ b/tests/unit/test_depfile.cpp @@ -1,53 +1,52 @@ // mcpp.build.depfile: the first record of a GNU depfile, which // `mcpp depfile-filter` keeps on Windows where POSIX runs the awk program // `NR==1{print;next} /^[^ ]/{exit} {print}` (ninja_backend.cppm, WS2). -#include import std; +import mcpplibs.mtest; import mcpp.build.depfile; +using namespace mcpplibs::mtest; + using mcpp::build::depfile::first_record; // GCC 16 with -fmodules, for a module interface with a purview #include: the // first record is the textual include graph, and the records after it (the // BMI having inputs, the phony module target) are what ninja rejects. -TEST(Depfile, KeepsTheIncludeGraphAndDropsGccsModuleRecords) { +UTest keeps_the_include_graph_and_drops_gccs_module_records{"Depfile/KeepsTheIncludeGraphAndDropsGccsModuleRecords", [] { const std::string raw = "obj/m.m.o gcm.cache/m.gcm: src/m.cppm \\\n" " src/vals.inc\n" "m.c++-module: gcm.cache/m.gcm\n" ".PHONY: m.c++-module\n" "gcm.cache/m.gcm:| obj/m.m.o\n"; - EXPECT_EQ(first_record(raw), - "obj/m.m.o gcm.cache/m.gcm: src/m.cppm \\\n" - " src/vals.inc\n"); -} + expect(eq(first_record(raw), "obj/m.m.o gcm.cache/m.gcm: src/m.cppm \\\n" + " src/vals.inc\n")); +}}; -TEST(Depfile, AnImportingUnitKeepsItsFirstRecordOnly) { +UTest an_importing_unit_keeps_its_first_record_only{"Depfile/AnImportingUnitKeepsItsFirstRecordOnly", [] { const std::string raw = "obj/main.o: src/main.cpp gcm.cache/std.gcm gcm.cache/m.gcm\n" "obj/main.o: m.c++-module std.c++-module\n" "CXX_IMPORTS += m.c++-module std.c++-module\n"; - EXPECT_EQ(first_record(raw), - "obj/main.o: src/main.cpp gcm.cache/std.gcm gcm.cache/m.gcm\n"); -} + expect(eq(first_record(raw), "obj/main.o: src/main.cpp gcm.cache/std.gcm gcm.cache/m.gcm\n")); +}}; // A plain depfile (clang's shape, or GCC's for a unit with no module) is // returned whole, including a final line without a newline. -TEST(Depfile, APlainDepfileIsKeptWhole) { +UTest a_plain_depfile_is_kept_whole{"Depfile/APlainDepfileIsKeptWhole", [] { const std::string raw = "obj/a.o: src/a.cpp \\\n src/a.h \\\n src/b.h"; - EXPECT_EQ(first_record(raw), raw); - EXPECT_EQ(first_record(""), ""); -} + expect(eq(first_record(raw), raw)); + expect(eq(first_record(""), "")); +}}; // Windows paths as a Windows GCC writes them: a drive letter and escaped // spaces do not end the record; only a line that starts with a non-space does. -TEST(Depfile, WindowsPathsAndCrlfLinesStayInTheRecord) { +UTest windows_paths_and_crlf_lines_stay_in_the_record{"Depfile/WindowsPathsAndCrlfLinesStayInTheRecord", [] { const std::string raw = "obj/a.o: C:/src/a.cpp \\\r\n" " C:/Program\\ Files/inc/a.h\r\n" "a.c++-module: gcm.cache/a.gcm\r\n"; - EXPECT_EQ(first_record(raw), - "obj/a.o: C:/src/a.cpp \\\r\n" - " C:/Program\\ Files/inc/a.h\r\n"); -} + expect(eq(first_record(raw), "obj/a.o: C:/src/a.cpp \\\r\n" + " C:/Program\\ Files/inc/a.h\r\n")); +}}; diff --git a/tests/unit/test_descriptor_keys.cpp b/tests/unit/test_descriptor_keys.cpp index f90f4139d..5b58cb977 100644 --- a/tests/unit/test_descriptor_keys.cpp +++ b/tests/unit/test_descriptor_keys.cpp @@ -3,12 +3,14 @@ // build machine takes no part. Both readers -- the version table under `xpm` // and the per-platform table of the `mcpp` segment -- follow the same order. -#include import std; +import mcpplibs.mtest; import mcpp.manifest; import mcpp.toolchain.platform; +using namespace mcpplibs::mtest; + namespace { using mcpp::toolchain::platform::TargetPlatform; @@ -47,31 +49,31 @@ std::vector versions(std::string_view lua, std::string_view os) { } // namespace -TEST(DescriptorKeys, AWebBuildReadsItsOwnBlockBeforeLinux) { - EXPECT_EQ(versions(kBoth, "emscripten"), (std::vector{"4.0.0"})); - EXPECT_EQ(versions(kBoth, "linux"), (std::vector{"4.1.0"})); -} +UTest a_web_build_reads_its_own_block_before_linux{"DescriptorKeys/AWebBuildReadsItsOwnBlockBeforeLinux", [] { + expect(eq(versions(kBoth, "emscripten"), (std::vector{"4.0.0"}))); + expect(eq(versions(kBoth, "linux"), (std::vector{"4.1.0"}))); +}}; -TEST(DescriptorKeys, AWebBuildFallsToLinuxOnlyByItsRowsOrderNotTheHost) { - EXPECT_EQ(versions(kLinuxOnly, "emscripten"), (std::vector{"4.1.0"})); - EXPECT_EQ(versions(kLinuxOnly, "openkal"), (std::vector{"4.1.0"})); -} +UTest a_web_build_falls_to_linux_only_by_its_rows_order_not_the_host{"DescriptorKeys/AWebBuildFallsToLinuxOnlyByItsRowsOrderNotTheHost", [] { + expect(eq(versions(kLinuxOnly, "emscripten"), (std::vector{"4.1.0"}))); + expect(eq(versions(kLinuxOnly, "openkal"), (std::vector{"4.1.0"}))); +}}; -TEST(DescriptorKeys, ABareMetalBuildReadsNoHostedBlock) { - EXPECT_TRUE(versions(kLinuxOnly, "none").empty()); - EXPECT_TRUE(versions(kLinuxOnly, "windows").empty()); -} +UTest a_bare_metal_build_reads_no_hosted_block{"DescriptorKeys/ABareMetalBuildReadsNoHostedBlock", [] { + expect(versions(kLinuxOnly, "none").empty()); + expect(versions(kLinuxOnly, "windows").empty()); +}}; -TEST(DescriptorKeys, TheSegmentTableFollowsTheSameOrder) { +UTest the_segment_table_follows_the_same_order{"DescriptorKeys/TheSegmentTableFollowsTheSameOrder", [] { auto web = mcpp::manifest::synthesize_from_xpkg_lua(kBoth, "taskflow", "4.0.0", TargetPlatform::for_os("emscripten")); - ASSERT_TRUE(web) << web.error().format(); + if (auto&& failed = expect(fatal(bool(web))); !failed) failed << web.error().format(); auto const& f = web->buildConfig.cxxflags; - EXPECT_NE(std::ranges::find(f, "-DWEB"), f.end()); - EXPECT_EQ(std::ranges::find(f, "-pthread"), f.end()); + expect(ne(std::ranges::find(f, "-DWEB"), f.end())); + expect(eq(std::ranges::find(f, "-pthread"), f.end())); auto ok = mcpp::manifest::synthesize_from_xpkg_lua(kLinuxOnly, "taskflow", "4.1.0", TargetPlatform::for_os("openkal")); - ASSERT_TRUE(ok) << ok.error().format(); - EXPECT_NE(std::ranges::find(ok->buildConfig.cxxflags, "-pthread"), ok->buildConfig.cxxflags.end()); -} + if (auto&& failed = expect(fatal(bool(ok))); !failed) failed << ok.error().format(); + expect(ne(std::ranges::find(ok->buildConfig.cxxflags, "-pthread"), ok->buildConfig.cxxflags.end())); +}}; diff --git a/tests/unit/test_detach_status.cpp b/tests/unit/test_detach_status.cpp index 3a9cdc96b..8f3ced644 100644 --- a/tests/unit/test_detach_status.cpp +++ b/tests/unit/test_detach_status.cpp @@ -1,6 +1,6 @@ -#include import std; +import mcpplibs.mtest; import mcpp.build.schedule.detach_codegen; import mcpp.run; import mcpp.invocation; @@ -8,6 +8,8 @@ import mcpplibs.os.fs; import mcpplibs.os.platform; import mcpplibs.os.process; +using namespace mcpplibs::mtest; + // A detached compile reports its status in `.rc`, which appears by a // rename once the number is written. One that holds no number is a write that // failed (a full disk); the waiter takes the unit as failed at once instead of @@ -29,67 +31,66 @@ struct Slot { }; } // namespace -TEST(DetachStatus, AStatusFileWithNoNumberIsAFailedUnit) { +UTest a_status_file_with_no_number_is_a_failed_unit{"DetachStatus/AStatusFileWithNoNumberIsAFailedUnit", [] { Slot s; std::ofstream(s.slot.string() + ".rc") << ""; const auto started = std::chrono::steady_clock::now(); - EXPECT_NE(detach::await_unit(s.slot, {}), 0); - EXPECT_LT(std::chrono::steady_clock::now() - started, std::chrono::seconds(5)); -} + expect(ne(detach::await_unit(s.slot, {}), 0)); + expect(lt(std::chrono::steady_clock::now() - started, std::chrono::seconds(5))); +}}; -TEST(DetachStatus, AWrittenStatusIsTheUnits) { +UTest a_written_status_is_the_units{"DetachStatus/AWrittenStatusIsTheUnits", [] { Slot s; std::ofstream(s.slot.string() + ".rc") << "3\n"; - EXPECT_EQ(detach::await_unit(s.slot, {}), 3); + expect(eq(detach::await_unit(s.slot, {}), 3)); Slot t; std::ofstream(t.slot.string() + ".rc") << "0\n"; - EXPECT_EQ(detach::await_unit(t.slot, {}), 0); -} + expect(eq(detach::await_unit(t.slot, {}), 0)); +}}; -TEST(DetachStatus, OrdinaryCompilerStartFailureReleasesItsPermit) { +UTest ordinary_compiler_start_failure_releases_its_permit{"DetachStatus/OrdinaryCompilerStartFailureReleasesItsPermit", [] { Slot s; const auto semaphore = s.dir / "permits"; - EXPECT_EQ(detach::run_capped({(s.dir / "missing-compiler").string()}, semaphore, 1), 127); + expect(eq(detach::run_capped({(s.dir / "missing-compiler").string()}, semaphore, 1), 127)); auto free = mcpplibs::os::fs::lock::acquire(semaphore / "slot-0.lock", mcpplibs::os::fs::lock::kind::exclusive, false); - EXPECT_TRUE(free); -} + expect(bool(free)); +}}; -TEST(DetachStatus, AnUnavailableSemaphoreRefusesUnboundedCompilation) { +UTest an_unavailable_semaphore_refuses_unbounded_compilation{"DetachStatus/AnUnavailableSemaphoreRefusesUnboundedCompilation", [] { Slot s; const auto semaphore = s.dir / "not-a-directory"; std::ofstream(semaphore) << "occupied"; - EXPECT_NE(detach::run_capped({"unused-compiler"}, semaphore, 1), 0); - EXPECT_TRUE(fs::is_regular_file(semaphore)); -} + expect(ne(detach::run_capped({"unused-compiler"}, semaphore, 1), 0)); + expect(fs::is_regular_file(semaphore)); +}}; -TEST(DetachStatus, KernelSlotsDistinguishContentionFromFailure) { +UTest kernel_slots_distinguish_contention_from_failure{"DetachStatus/KernelSlotsDistinguishContentionFromFailure", [] { Slot s; const auto path = s.dir / "slot.lock"; { auto held = mcpplibs::os::fs::lock::acquire(path, mcpplibs::os::fs::lock::kind::exclusive, false); - ASSERT_TRUE(held); + expect(fatal(bool(held))); auto busy = mcpplibs::os::fs::lock::acquire(path, mcpplibs::os::fs::lock::kind::exclusive, false); - ASSERT_FALSE(busy); - EXPECT_EQ(busy.error().code, mcpplibs::os::errc::again); + expect(fatal(!busy)); + expect(eq(busy.error().code, mcpplibs::os::errc::again)); } - EXPECT_TRUE(fs::exists(path)); + expect(fs::exists(path)); auto reused = mcpplibs::os::fs::lock::acquire(path, mcpplibs::os::fs::lock::kind::exclusive, false); - EXPECT_TRUE(reused); -} + expect(bool(reused)); +}}; namespace { std::string lifetime_env(std::string_view key, std::string fallback = {}) { return mcpp::invocation::get(key).value_or(std::move(fallback)); } -int lifetime_driver() { +int lifetime_driver(const std::vector& argv) { const auto role = lifetime_env("MCPP_TEST_LIFETIME_ROLE"); const auto self = mcpp::invocation::self_exe_path(); - const auto argv = ::testing::internal::GetArgvs(); if (argv.size() > 1 && argv[1] == "bmi-supervise") { auto value = [&](std::string_view key) { auto it = std::ranges::find(argv, key); @@ -177,48 +178,49 @@ int lifetime_driver() { } } // namespace -// This copy is an actual detached supervisor/phase1/ordinary compiler under -// Ninja. It is selected through GTEST_FILTER inherited by detached children. -TEST(DetachLifetime, SubprocessDriver) { - if (lifetime_env("MCPP_TEST_LIFETIME_ROLE").empty()) GTEST_SKIP() << "subprocess driver"; +// A COPY OF THIS PROGRAM IS A DETACHED SUPERVISOR, PHASE-ONE OR ORDINARY COMPILER +// UNDER NINJA, chosen by MCPP_TEST_LIFETIME_ROLE (the e2e helpers set it) before any +// case runs; the copy is mcpp to the code under test, so its arguments are mcpp's. +int main(int argc, char** argv) { + if (lifetime_env("MCPP_TEST_LIFETIME_ROLE").empty()) return mcpplibs::mtest::run(argc, argv); mcpp::run::install(); - const int rc = lifetime_driver(); + const int rc = lifetime_driver(std::vector(argv, argv + argc)); std::cout.flush(); std::cerr.flush(); std::_Exit(rc); } -TEST(DetachLifetime, OwnerCancellationAndImmediateRestart) { - if constexpr (!mcpplibs::os::is_linux) GTEST_SKIP() << "Linux signal and LD_PRELOAD controls"; +UTest owner_cancellation_and_immediate_restart{"DetachLifetime/OwnerCancellationAndImmediateRestart", [] { + if constexpr (!mcpplibs::os::is_linux) skip("Linux signal and LD_PRELOAD controls"); Slot temporary; const auto helper = fs::current_path() / "tests/e2e/helpers/check_owned_schedule_lifetime.py"; - ASSERT_TRUE(fs::exists(helper)); + expect(fatal(fs::exists(helper))); const auto result = mcpp::run::execute({"python3", helper.string(), mcpp::invocation::self_exe_path().string(), temporary.dir.string()}); - EXPECT_EQ(result.exit_code, 0) << result.output; -} + expect(eq(result.exit_code, 0)) << result.output; +}}; -TEST(DetachLifetime, TransactionRecoveryAtNativeSyscallBoundaries) { +UTest transaction_recovery_at_native_syscall_boundaries{"DetachLifetime/TransactionRecoveryAtNativeSyscallBoundaries", [] { if constexpr (!mcpplibs::os::is_linux || mcpplibs::os::ARCH != mcpplibs::os::architecture::x86_64) - GTEST_SKIP() << "Linux x86-64 syscall boundary control"; + skip("Linux x86-64 syscall boundary control"); Slot temporary; const auto helper=fs::current_path()/"tests/e2e/helpers/check_compiler_transaction_faults.py"; - ASSERT_TRUE(fs::exists(helper)); + expect(fatal(fs::exists(helper))); const auto result=mcpp::run::execute({"python3",helper.string(), mcpp::invocation::self_exe_path().string(),temporary.dir.string()}, {.total=std::chrono::seconds(120)}); - EXPECT_EQ(result.exit_code,0)< import std; +import mcpplibs.mtest; +import mcpplibs.os.stdio; import mcpp.diag; +using namespace mcpplibs::mtest; + using namespace mcpp::diag; namespace { -struct DiagTest : ::testing::Test { - void SetUp() override { reset(); } - void TearDown() override { reset(); } +struct DiagTest { + void SetUp() { reset(); } + void TearDown() { reset(); } }; } // namespace -TEST_F(DiagTest, WarningAndDegradedAreCountedSeparately) { +UTest warning_and_degraded_are_counted_separately{"DiagTest/WarningAndDegradedAreCountedSeparately", [] { + struct Case : DiagTest { + Case() { SetUp(); } + ~Case() { TearDown(); } + void body() { warning("manifest/schema", "unsupported key 'bogus'"); degraded("build/depfile", "no GNU depfile on this toolchain", "edits to purview-included files will not trigger a rebuild"); - EXPECT_EQ(count(Severity::Warning), 1u); - EXPECT_EQ(count(Severity::Degraded), 1u); - EXPECT_EQ(records().size(), 2u); + expect(eq(count(Severity::Warning), 1u)); + expect(eq(count(Severity::Degraded), 1u)); + expect(eq(records().size(), 2u)); } - -TEST_F(DiagTest, IdenticalReportsAreDeduplicated) { + } fixture; + fixture.body(); +}}; + +UTest identical_reports_are_deduplicated{"DiagTest/IdenticalReportsAreDeduplicated", [] { + struct Case : DiagTest { + Case() { SetUp(); } + ~Case() { TearDown(); } + void body() { for (int i = 0; i < 5; ++i) warning("modgraph/scan", "glob 'a/**' matched no source file"); - EXPECT_EQ(records().size(), 1u) + expect(eq(records().size(), 1u)) << "a repeated report should render and record once"; } - -TEST_F(DiagTest, SameDomainDifferentPayloadStaysDistinct) { + } fixture; + fixture.body(); +}}; + +UTest same_domain_different_payload_stays_distinct{"DiagTest/SameDomainDifferentPayloadStaysDistinct", [] { + struct Case : DiagTest { + Case() { SetUp(); } + ~Case() { TearDown(); } + void body() { // Two degradations in one domain that tell the user different things are // two records — dedup keys on the whole payload, not the domain. degraded("build/depfile", "no GNU depfile", "stale BMI possible"); degraded("build/depfile", "no GNU depfile", "stale object possible"); - EXPECT_EQ(records().size(), 2u); + expect(eq(records().size(), 2u)); } - -TEST_F(DiagTest, FormatKeepsWhatOnTheFirstLine) { + } fixture; + fixture.body(); +}}; + +UTest format_keeps_what_on_the_first_line{"DiagTest/FormatKeepsWhatOnTheFirstLine", [] { + struct Case : DiagTest { + Case() { SetUp(); } + ~Case() { TearDown(); } + void body() { // e2e assertions grep for the `what` substring; elaboration must not get // in front of it. warning("modgraph/scan", "[build].flags glob 'x/**' matched no source file"); auto rs = records(); - ASSERT_EQ(rs.size(), 1u); - EXPECT_EQ(rs[0].format(), "[build].flags glob 'x/**' matched no source file"); + expect(fatal(eq(rs.size(), 1u))); + expect(eq(rs[0].format(), "[build].flags glob 'x/**' matched no source file")); reset(); degraded("build/depfile", "what happened", "the impact", "the hint"); rs = records(); - ASSERT_EQ(rs.size(), 1u); - EXPECT_TRUE(rs[0].format().starts_with("what happened\n")); - EXPECT_NE(rs[0].format().find("impact: the impact"), std::string::npos); - EXPECT_NE(rs[0].format().find("help: the hint"), std::string::npos); + expect(fatal(eq(rs.size(), 1u))); + expect(rs[0].format().starts_with("what happened\n")); + expect(ne(rs[0].format().find("impact: the impact"), std::string::npos)); + expect(ne(rs[0].format().find("help: the hint"), std::string::npos)); } - -TEST_F(DiagTest, StrictPromotesDegradationsOnly) { + } fixture; + fixture.body(); +}}; + +UTest strict_promotes_degradations_only{"DiagTest/StrictPromotesDegradationsOnly", [] { + struct Case : DiagTest { + Case() { SetUp(); } + ~Case() { TearDown(); } + void body() { warning("manifest/schema", "just a warning"); - EXPECT_TRUE(flush(/*strict=*/true)) + expect(bool(flush(/*strict=*/true))) << "a plain warning must not fail a --strict run"; reset(); degraded("build/depfile", "w", "i"); - EXPECT_FALSE(flush(/*strict=*/true)); + expect(!flush(/*strict=*/true)); reset(); degraded("build/depfile", "w", "i"); - EXPECT_TRUE(flush(/*strict=*/false)); + expect(bool(flush(/*strict=*/false))); } - -TEST_F(DiagTest, FlushClearsRunState) { + } fixture; + fixture.body(); +}}; + +UTest flush_clears_run_state{"DiagTest/FlushClearsRunState", [] { + struct Case : DiagTest { + Case() { SetUp(); } + ~Case() { TearDown(); } + void body() { warning("a", "b"); degraded("c", "d", "e"); - EXPECT_TRUE(flush(/*strict=*/false)); - EXPECT_EQ(records().size(), 0u); - EXPECT_EQ(count(Severity::Degraded), 0u); + expect(bool(flush(/*strict=*/false))); + expect(eq(records().size(), 0u)); + expect(eq(count(Severity::Degraded), 0u)); } + } fixture; + fixture.body(); +}}; // Regression guard for the review finding on this batch: flush() is the ONLY // place the --strict policy is settled, and it was initially never called // outside this file — the channel reported degradations and then everyone // ignored them, which is precisely the failure mode it exists to prevent. // run_build_plan now calls it; this pins the contract flush() must honour. -TEST_F(DiagTest, FlushIsTheSolePolicyPointAndReportsFailureToTheCaller) { +UTest flush_is_the_sole_policy_point_and_reports_failure_to_the_caller{"DiagTest/FlushIsTheSolePolicyPointAndReportsFailureToTheCaller", [] { + struct Case : DiagTest { + Case() { SetUp(); } + ~Case() { TearDown(); } + void body() { // No records at all: strict must not fail a clean build. - EXPECT_TRUE(flush(/*strict=*/true)); + expect(bool(flush(/*strict=*/true))); // A degradation under --strict must tell the caller to fail. The caller // (run_build_plan) turns this into a non-zero exit. degraded("build/depfile", "no depfile on this toolchain", "stale BMI possible after editing an included file"); - EXPECT_FALSE(flush(/*strict=*/true)); + expect(!flush(/*strict=*/true)); // Same degradation without --strict: reported, not fatal. degraded("build/depfile", "no depfile on this toolchain", "stale BMI possible after editing an included file"); - EXPECT_TRUE(flush(/*strict=*/false)); + expect(bool(flush(/*strict=*/false))); } + } fixture; + fixture.body(); +}}; // WS3 of the 2026-09-28 design: one statement per fact per PROCESS. A // `--workspace` build flushes after each member, and a fact every member // shares -- a redundant word the workspace states -- was printed once per // member. The terminal prints it once; each run's record still holds it, so // `take()` gives every member's occurrence to a machine-readable envelope. -TEST_F(DiagTest, AFactIsPrintedOncePerProcessAndRecordedPerRun) { +UTest a_fact_is_printed_once_per_process_and_recorded_per_run{"DiagTest/AFactIsPrintedOncePerProcessAndRecordedPerRun", [] { + struct Case : DiagTest { + Case() { SetUp(); } + ~Case() { TearDown(); } + void body() { const std::string fact = "the CRT word is redundant ([workspace.build] dialect_cxxflags)"; - testing::internal::CaptureStderr(); + auto stderrCapture = mcpplibs::os::stdio::capture::begin(mcpplibs::os::stdio::stream::error).value(); for (int member = 0; member < 5; ++member) { warning("build/msvc-crt-word", fact); auto run = take(); - ASSERT_EQ(run.size(), 1u) << "member " << member << " lost its own record"; - EXPECT_EQ(run[0].what, fact); + expect(fatal(eq(run.size(), 1u))) << "member " << member << " lost its own record"; + expect(eq(run[0].what, fact)); (void)flush(/*strict=*/false); } - const auto printed = testing::internal::GetCapturedStderr(); + const auto printed = stderrCapture.text(); std::size_t count = 0; for (auto at = printed.find(fact); at != std::string::npos; at = printed.find(fact, at + 1)) ++count; - EXPECT_EQ(count, 1u) << printed; + expect(eq(count, 1u)) << printed; } - -TEST_F(DiagTest, NotesArePrintedAsNotesAndNeverPromoted) { - testing::internal::CaptureStderr(); + } fixture; + fixture.body(); +}}; + +UTest notes_are_printed_as_notes_and_never_promoted{"DiagTest/NotesArePrintedAsNotesAndNeverPromoted", [] { + struct Case : DiagTest { + Case() { SetUp(); } + ~Case() { TearDown(); } + void body() { + auto stderrCapture = mcpplibs::os::stdio::capture::begin(mcpplibs::os::stdio::stream::error).value(); note("build/runtime-placement", "a dependency ships the MSVC C++ runtime"); - const auto printed = testing::internal::GetCapturedStderr(); - EXPECT_NE(printed.find("note:"), std::string::npos) << printed; - EXPECT_EQ(printed.find("warning:"), std::string::npos) << printed; - EXPECT_EQ(count(Severity::Degraded), 0u); - EXPECT_TRUE(flush(/*strict=*/true)); + const auto printed = stderrCapture.text(); + expect(ne(printed.find("note:"), std::string::npos)) << printed; + expect(eq(printed.find("warning:"), std::string::npos)) << printed; + expect(eq(count(Severity::Degraded), 0u)); + expect(bool(flush(/*strict=*/true))); } + } fixture; + fixture.body(); +}}; diff --git a/tests/unit/test_dialect.cpp b/tests/unit/test_dialect.cpp index cdc97a2da..3730e52f6 100644 --- a/tests/unit/test_dialect.cpp +++ b/tests/unit/test_dialect.cpp @@ -1,55 +1,55 @@ -#include import std; +import mcpplibs.mtest; import mcpp.toolchain.dialect; import mcpp.toolchain.model; +using namespace mcpplibs::mtest; + // The MSVC row is unreachable in a real build until the cl.exe backend lands, // so these tests are the only thing keeping it honest. -TEST(Dialect, LibFlagHasBothShapes) { +UTest lib_flag_has_both_shapes{"Dialect/LibFlagHasBothShapes", [] { // GNU names a library by prefixing, MSVC by suffixing. A single // string_view prefix cannot express `z.lib`, which is why libFlag is a // format rather than a prefix. - EXPECT_EQ(mcpp::toolchain::lib_flag_for(mcpp::toolchain::gnu_dialect(), "z"), - "-lz"); - EXPECT_EQ(mcpp::toolchain::lib_flag_for(mcpp::toolchain::msvc_dialect(), "z"), - "z.lib"); -} - -TEST(Dialect, LibFlagHandlesDottedAndHyphenatedNames) { - EXPECT_EQ(mcpp::toolchain::lib_flag_for(mcpp::toolchain::gnu_dialect(), - "avcodec-60"), "-lavcodec-60"); - EXPECT_EQ(mcpp::toolchain::lib_flag_for(mcpp::toolchain::msvc_dialect(), - "avcodec-60"), "avcodec-60.lib"); -} + expect(eq(mcpp::toolchain::lib_flag_for(mcpp::toolchain::gnu_dialect(), "z"), "-lz")); + expect(eq(mcpp::toolchain::lib_flag_for(mcpp::toolchain::msvc_dialect(), "z"), "z.lib")); +}}; + +UTest lib_flag_handles_dotted_and_hyphenated_names{"Dialect/LibFlagHandlesDottedAndHyphenatedNames", [] { + expect(eq(mcpp::toolchain::lib_flag_for(mcpp::toolchain::gnu_dialect(), + "avcodec-60"), "-lavcodec-60")); + expect(eq(mcpp::toolchain::lib_flag_for(mcpp::toolchain::msvc_dialect(), + "avcodec-60"), "avcodec-60.lib")); +}}; // Every dialect field must be populated in both rows. An empty one silently // emits nothing, which for staticRuntime or forceCxxLang means the compile // changes meaning rather than failing. -TEST(Dialect, LinkAndLanguageFieldsPopulatedInBothRows) { +UTest link_and_language_fields_populated_in_both_rows{"Dialect/LinkAndLanguageFieldsPopulatedInBothRows", [] { for (auto const* d : { &mcpp::toolchain::gnu_dialect(), &mcpp::toolchain::msvc_dialect() }) { - EXPECT_FALSE(d->libFlag.empty()) << d->id; - EXPECT_FALSE(d->libSearchPrefix.empty()) << d->id; - EXPECT_FALSE(d->forceCxxLang.empty()) << d->id; - EXPECT_FALSE(d->staticRuntime.empty()) << d->id; - EXPECT_FALSE(d->outputExePrefix.empty()) << d->id; + if (auto&& failed = expect(!(d->libFlag.empty())); !failed) failed << d->id; + if (auto&& failed = expect(!(d->libSearchPrefix.empty())); !failed) failed << d->id; + if (auto&& failed = expect(!(d->forceCxxLang.empty())); !failed) failed << d->id; + if (auto&& failed = expect(!(d->staticRuntime.empty())); !failed) failed << d->id; + if (auto&& failed = expect(!(d->outputExePrefix.empty())); !failed) failed << d->id; // A `{}` placeholder is what makes lib_flag_for work at all. - EXPECT_NE(d->libFlag.find("{}"), std::string_view::npos) << d->id; + expect(ne(d->libFlag.find("{}"), std::string_view::npos)) << d->id; } -} +}}; // dialect_for must keep routing clang-targeting-MSVC to the gnu spellings: // that driver takes GNU flags even though its ABI and STL are Microsoft's. -TEST(Dialect, OnlyNativeClExeGetsTheMsvcRow) { +UTest only_native_cl_exe_gets_the_msvc_row{"Dialect/OnlyNativeClExeGetsTheMsvcRow", [] { mcpp::toolchain::Toolchain clangMsvc; clangMsvc.compiler = mcpp::toolchain::CompilerId::Clang; clangMsvc.targetTriple = "x86_64-pc-windows-msvc"; - EXPECT_EQ(mcpp::toolchain::dialect_for(clangMsvc).id, "gnu"); + expect(eq(mcpp::toolchain::dialect_for(clangMsvc).id, "gnu")); mcpp::toolchain::Toolchain cl; cl.compiler = mcpp::toolchain::CompilerId::MSVC; cl.targetTriple = "x86_64-pc-windows-msvc"; - EXPECT_EQ(mcpp::toolchain::dialect_for(cl).id, "msvc"); -} + expect(eq(mcpp::toolchain::dialect_for(cl).id, "msvc")); +}}; diff --git a/tests/unit/test_distribution.cpp b/tests/unit/test_distribution.cpp index b5869851d..8c47e7b36 100644 --- a/tests/unit/test_distribution.cpp +++ b/tests/unit/test_distribution.cpp @@ -6,11 +6,13 @@ // TABLE's totality and about each cell's identity — not about "the flags look // roughly right". -#include import std; +import mcpplibs.mtest; import mcpp.build.distribution; +using namespace mcpplibs::mtest; + namespace dist = mcpp::build::dist; namespace { @@ -41,106 +43,103 @@ dist::MechanismInput linux_gcc_input() { // 0.0.86 on, while the docs kept promising the opt-out. The table cannot // reproduce that bug because the role is an INPUT to one function rather than // a switch that picks between two independently-computed strings. -TEST(Distribution, HostCoupledReachesTestBinariesOnMacos) { +UTest host_coupled_reaches_test_binaries_on_macos{"Distribution/HostCoupledReachesTestBinariesOnMacos", [] { auto in = macos_input(); in.requested = dist::Contract::HostCoupled; for (auto role : {dist::Role::Distributable, dist::Role::Test}) { in.role = role; auto m = dist::resolve(in); - EXPECT_EQ(m.effective, dist::Contract::HostCoupled) << dist::to_string(role); - EXPECT_EQ(m.unitFlags, " -lc++") << dist::to_string(role); - EXPECT_FALSE(m.degraded) << dist::to_string(role); + expect(eq(m.effective, dist::Contract::HostCoupled)) << dist::to_string(role); + expect(eq(m.unitFlags, " -lc++")) << dist::to_string(role); + expect(!m.degraded) << dist::to_string(role); // No static libc++ means no Mach-O initializer-ordering problem. - EXPECT_FALSE(m.streamInitShim) << dist::to_string(role); + expect(!m.streamInitShim) << dist::to_string(role); } -} +}}; // The other half of #336: by default a test binary keeps the SAME // self-contained runtime as a shipped one. Flipping this default would // re-open #202 (system libc++ dylib against toolchain libc++ headers → // undefined __hash_memory on libc++ 22), so it is asserted, not assumed. -TEST(Distribution, TestsDefaultToSelfContained) { +UTest tests_default_to_self_contained{"Distribution/TestsDefaultToSelfContained", [] { for (auto fmt : {dist::Format::Elf, dist::Format::MachO, dist::Format::Pe}) { - EXPECT_EQ(dist::default_contract(dist::Role::Test, fmt), - dist::Contract::SelfContained); - EXPECT_EQ(dist::default_contract(dist::Role::Distributable, fmt), - dist::Contract::SelfContained); + expect(eq(dist::default_contract(dist::Role::Test, fmt), dist::Contract::SelfContained)); + expect(eq(dist::default_contract(dist::Role::Distributable, fmt), dist::Contract::SelfContained)); } auto in = macos_input(); in.role = dist::Role::Test; in.requested = dist::default_contract(dist::Role::Test, dist::Format::MachO); auto m = dist::resolve(in); - EXPECT_NE(m.unitFlags.find("-load_hidden"), std::string::npos); - EXPECT_TRUE(m.streamInitShim); -} + expect(ne(m.unitFlags.find("-load_hidden"), std::string::npos)); + expect(bool(m.streamInitShim)); +}}; // -load_hidden, not a plain by-path link: the visibility is the load-bearing // half (PR #117 — default-visibility statics get unified with the system // libc++ from dyld's shared cache and ostream<(fmt), sl, dist::to_string(role), archives); // Totality: every cell answers with a contract it actually delivered. - EXPECT_TRUE(m.effective == dist::Contract::SelfContained + expect(m.effective == dist::Contract::SelfContained || m.effective == dist::Contract::ToolchainCoupled || m.effective == dist::Contract::HostCoupled) << where; // Honesty: a downgrade is never silent. - if (m.degraded) EXPECT_FALSE(m.diagnostic.empty()) << where; + if (m.degraded) expect(!m.diagnostic.empty()) << where; if (m.effective != c && role != dist::Role::Intermediate) - EXPECT_TRUE(m.degraded) << where; + expect(bool(m.degraded)) << where; // The ordering shim is a Mach-O static-libc++ concern and nothing else. if (m.streamInitShim) { - EXPECT_EQ(fmt, dist::Format::MachO) << where; - EXPECT_EQ(m.effective, dist::Contract::SelfContained) << where; + expect(eq(fmt, dist::Format::MachO)) << where; + expect(eq(m.effective, dist::Contract::SelfContained)) << where; } } -} +}}; // The generated shim is load-bearing in three specific ways; a well-meaning // edit that drops any of them turns it into a silent no-op (or worse, an // unconditional link failure on a toolchain that spells the symbol // differently). See issue #336 for the disassembly this rests on. -TEST(Distribution, StreamInitShimKeepsItsThreeLoadBearingProperties) { +UTest stream_init_shim_keeps_its_three_load_bearing_properties{"Distribution/StreamInitShimKeepsItsThreeLoadBearingProperties", [] { auto src = std::string(dist::stream_init_shim_source()); // 1. weak_import — Mach-O's weak-UNDEFINED form. Plain `weak` is not it; // the first CI round proved that by failing every macOS link. - EXPECT_NE(src.find("__attribute__((weak_import))"), std::string::npos); + expect(ne(src.find("__attribute__((weak_import))"), std::string::npos)); // 2. ios_base::Init::Init, NOT DoIOSInit::DoIOSInit — the former is // guarded by __cxa_guard, so libc++'s own initializer later becomes a // no-op instead of placement-new'ing over live streams. // ...spelled with TWO leading underscores: an __asm__ label is used // verbatim, so Mach-O's global `_` prefix has to be written out. - EXPECT_NE(src.find("\"__ZNSt3__18ios_base4InitC1Ev\""), std::string::npos); - EXPECT_EQ(src.find("DoIOSInit"), std::string::npos); + expect(ne(src.find("\"__ZNSt3__18ios_base4InitC1Ev\""), std::string::npos)); + expect(eq(src.find("DoIOSInit"), std::string::npos)); // 3. a constructor, and it must be guarded by the weak null check. - EXPECT_NE(src.find("__attribute__((constructor))"), std::string::npos); - EXPECT_NE(src.find("if (mcpp_libcxx_ios_init)"), std::string::npos); + expect(ne(src.find("__attribute__((constructor))"), std::string::npos)); + expect(ne(src.find("if (mcpp_libcxx_ios_init)"), std::string::npos)); // C, not C++: no standard library, no module flags, no ABI of its own. - EXPECT_EQ(src.find("#include"), std::string::npos); -} + expect(eq(src.find("#include"), std::string::npos)); +}}; // The shim binds a libc++ INTERNAL symbol. If that symbol is not in the // archive, mcpp must NOT generate the reference — an undefined symbol fails // the link outright (ld64.lld does not treat a weak declaration as an // optional undefined). The absence is reported instead, because the startup // hazard is still there. -TEST(Distribution, ShimIsNotGeneratedWhenTheSymbolIsAbsent) { +UTest shim_is_not_generated_when_the_symbol_is_absent{"Distribution/ShimIsNotGeneratedWhenTheSymbolIsAbsent", [] { auto in = macos_input(); in.streamInitSymbolPresent = false; auto m = dist::resolve(in); - EXPECT_FALSE(m.streamInitShim); - EXPECT_FALSE(m.diagnostic.empty()); + expect(!m.streamInitShim); + expect(!m.diagnostic.empty()); // The contract itself is still honored — only the ordering aid is gone. - EXPECT_EQ(m.effective, dist::Contract::SelfContained); - EXPECT_NE(m.unitFlags.find("-load_hidden"), std::string::npos); -} + expect(eq(m.effective, dist::Contract::SelfContained)); + expect(ne(m.unitFlags.find("-load_hidden"), std::string::npos)); +}}; // --------------------------------------------------------------------------- // Shared libraries. @@ -513,64 +506,61 @@ TEST(Distribution, ShimIsNotGeneratedWhenTheSymbolIsAbsent) { // The whole table is asserted cell by cell rather than "the ELF case", because // the reason the other two formats keep the old answer is a real argument // about each of them and a regression there would be silent. -TEST(Distribution, SharedLibraryDefaultIsFormatSpecific) { - EXPECT_EQ(dist::default_contract(dist::Role::SharedLibrary, dist::Format::Elf), - dist::Contract::ToolchainCoupled); +UTest shared_library_default_is_format_specific{"Distribution/SharedLibraryDefaultIsFormatSpecific", [] { + expect(eq(dist::default_contract(dist::Role::SharedLibrary, dist::Format::Elf), dist::Contract::ToolchainCoupled)); // Mach-O: self-contained ALREADY means hidden (-load_hidden), so dyld // cannot unify the symbols; and toolchain-coupled is a documented dead end // there (#202). PE: no global namespace at all, imports resolve per-DLL. - EXPECT_EQ(dist::default_contract(dist::Role::SharedLibrary, dist::Format::MachO), - dist::Contract::SelfContained); - EXPECT_EQ(dist::default_contract(dist::Role::SharedLibrary, dist::Format::Pe), - dist::Contract::SelfContained); -} + expect(eq(dist::default_contract(dist::Role::SharedLibrary, dist::Format::MachO), dist::Contract::SelfContained)); + expect(eq(dist::default_contract(dist::Role::SharedLibrary, dist::Format::Pe), dist::Contract::SelfContained)); +}}; // The ELF mechanism for the new default: nothing. The driver links // libstdc++.so and the toolchain's lib directory is already an -L and an rpath // entry on the line. "No flag" has to be asserted or a future edit that adds // one back would look like an improvement. -TEST(Distribution, SharedLibraryOnElfEmbedsNothing) { +UTest shared_library_on_elf_embeds_nothing{"Distribution/SharedLibraryOnElfEmbedsNothing", [] { auto in = linux_gcc_input(); in.role = dist::Role::SharedLibrary; in.requested = dist::default_contract(dist::Role::SharedLibrary, dist::Format::Elf); auto m = dist::resolve(in); - EXPECT_EQ(m.effective, dist::Contract::ToolchainCoupled); - EXPECT_EQ(m.unitFlags, ""); - EXPECT_FALSE(m.degraded); - EXPECT_TRUE(m.diagnostic.empty()); -} + expect(eq(m.effective, dist::Contract::ToolchainCoupled)); + expect(eq(m.unitFlags, "")); + expect(!m.degraded); + expect(m.diagnostic.empty()); +}}; // The escape hatch stays usable — and is guarded. Asking for a self-contained // .so is legitimate; letting it export the embedded runtime is not. -TEST(Distribution, ExplicitSelfContainedSharedLibraryHidesTheEmbeddedRuntime) { +UTest explicit_self_contained_shared_library_hides_the_embedded_runtime{"Distribution/ExplicitSelfContainedSharedLibraryHidesTheEmbeddedRuntime", [] { auto in = linux_gcc_input(); in.role = dist::Role::SharedLibrary; in.requested = dist::Contract::SelfContained; in.explicitRequest = true; auto m = dist::resolve(in); - EXPECT_EQ(m.effective, dist::Contract::SelfContained); - EXPECT_NE(m.unitFlags.find("-static-libstdc++"), std::string::npos) << m.unitFlags; - EXPECT_NE(m.unitFlags.find("-Wl,--exclude-libs,libstdc++.a"), std::string::npos) + expect(eq(m.effective, dist::Contract::SelfContained)); + expect(ne(m.unitFlags.find("-static-libstdc++"), std::string::npos)) << m.unitFlags; + expect(ne(m.unitFlags.find("-Wl,--exclude-libs,libstdc++.a"), std::string::npos)) << m.unitFlags; // No diagnostic: this is honored exactly as asked, not degraded. - EXPECT_FALSE(m.degraded); -} + expect(!m.degraded); +}}; // The guard is for shared libraries only. An executable's static libstdc++ is // already local (ld exports only what a loaded object references and mcpp // passes no -rdynamic), so hiding it would be noise — and `--exclude-libs` on // an executable link is a flag whose absence is part of the contract. -TEST(Distribution, ExecutablesDoNotGetTheExcludeLibsGuard) { +UTest executables_do_not_get_the_exclude_libs_guard{"Distribution/ExecutablesDoNotGetTheExcludeLibsGuard", [] { auto in = linux_gcc_input(); in.role = dist::Role::Distributable; in.requested = dist::Contract::SelfContained; auto m = dist::resolve(in); - EXPECT_EQ(m.unitFlags, " -static-libstdc++"); -} + expect(eq(m.unitFlags, " -static-libstdc++")); +}}; // Same rule on the libc++/ELF path: the archives are linked by PATH, but // --exclude-libs matches the archive BASENAME, so the names are spelled out. -TEST(Distribution, LibcxxSelfContainedSharedLibraryHidesItsArchives) { +UTest libcxx_self_contained_shared_library_hides_its_archives{"Distribution/LibcxxSelfContainedSharedLibraryHidesItsArchives", [] { dist::MechanismInput in; in.format = dist::Format::Elf; in.stdlibId = "libc++"; @@ -584,32 +574,31 @@ TEST(Distribution, LibcxxSelfContainedSharedLibraryHidesItsArchives) { for (auto needle : {"-Wl,--exclude-libs,libc++.a", "-Wl,--exclude-libs,libc++abi.a", "-Wl,--exclude-libs,libunwind.a"}) - EXPECT_NE(m.unitFlags.find(needle), std::string::npos) << needle + expect(ne(m.unitFlags.find(needle), std::string::npos)) << needle << " / " << m.unitFlags; -} +}}; // An archive embeds no runtime, so the role returns before any mechanism runs. // Adding a fourth role must not have perturbed that early exit. -TEST(Distribution, IntermediateStillCarriesNoMechanism) { +UTest intermediate_still_carries_no_mechanism{"Distribution/IntermediateStillCarriesNoMechanism", [] { auto in = linux_gcc_input(); in.role = dist::Role::Intermediate; in.requested = dist::Contract::SelfContained; auto m = dist::resolve(in); - EXPECT_EQ(m.unitFlags, ""); - EXPECT_FALSE(m.degraded); -} + expect(eq(m.unitFlags, "")); + expect(!m.degraded); +}}; // `ldStdlibByRole` indexes by the enum value. If a role is ever inserted // rather than appended, every stored contract silently re-maps. -TEST(Distribution, RoleCountCoversEveryRole) { - EXPECT_EQ(static_cast(dist::Role::SharedLibrary) + 1, - dist::kRoleCount); +UTest role_count_covers_every_role{"Distribution/RoleCountCoversEveryRole", [] { + expect(eq(static_cast(dist::Role::SharedLibrary) + 1, dist::kRoleCount)); for (auto r : {dist::Role::Distributable, dist::Role::Test, dist::Role::Intermediate, dist::Role::SharedLibrary}) { - EXPECT_LT(static_cast(r), dist::kRoleCount); - EXPECT_FALSE(dist::to_string(r).empty()); + expect(lt(static_cast(r), dist::kRoleCount)); + expect(!(dist::to_string(r).empty())); } -} +}}; // ── The two short-circuits: when there is no C++ runtime to distribute WITH ── // @@ -619,7 +608,7 @@ TEST(Distribution, RoleCountCoversEveryRole) { // rather than merely unnecessary, and in both the archives the table would // reach for belong to the HOST. -TEST(Distribution, FreestandingCarriesNoRuntimeToDistribute) { +UTest freestanding_carries_no_runtime_to_distribute{"Distribution/FreestandingCarriesNoRuntimeToDistribute", [] { dist::MechanismInput in; in.format = dist::Format::Elf; in.stdlibId = "libc++"; @@ -630,31 +619,31 @@ TEST(Distribution, FreestandingCarriesNoRuntimeToDistribute) { in.libcxxArchive = "/tc/lib/libc++.a"; in.libcxxAbiArchive = "/tc/lib/libc++abi.a"; auto m = dist::resolve(in); - EXPECT_EQ(m.effective, dist::Contract::SelfContained); - EXPECT_EQ(m.unitFlags, " -nostdlib++"); - EXPECT_EQ(m.unitFlags.find("libc++.a"), std::string::npos); - EXPECT_FALSE(m.degraded); -} + expect(eq(m.effective, dist::Contract::SelfContained)); + expect(eq(m.unitFlags, " -nostdlib++")); + expect(eq(m.unitFlags.find("libc++.a"), std::string::npos)); + expect(!m.degraded); +}}; // The hosted form of the same fact. A package in the graph has compiled a // C++ runtime FOR THIS TARGET and its objects are already on the link line, so // there is no library to name and nothing to look for. Measured before this // existed: `ld64.lld: error: library not found for -lc++`. -TEST(Distribution, GraphSuppliedRuntimeCarriesNoLibraryToName) { +UTest graph_supplied_runtime_carries_no_library_to_name{"Distribution/GraphSuppliedRuntimeCarriesNoLibraryToName", [] { auto in = macos_input(); in.graphCxxRuntime = true; in.requested = dist::Contract::SelfContained; in.role = dist::Role::Distributable; auto m = dist::resolve(in); - EXPECT_EQ(m.effective, dist::Contract::SelfContained); - EXPECT_EQ(m.unitFlags, " -nostdlib++"); - EXPECT_EQ(m.unitFlags.find("-lc++"), std::string::npos); - EXPECT_EQ(m.unitFlags.find("load_hidden"), std::string::npos); -} + expect(eq(m.effective, dist::Contract::SelfContained)); + expect(eq(m.unitFlags, " -nostdlib++")); + expect(eq(m.unitFlags.find("-lc++"), std::string::npos)); + expect(eq(m.unitFlags.find("load_hidden"), std::string::npos)); +}}; // The same on every format, because the fact is about the graph and not about // the object format. -TEST(Distribution, GraphSuppliedRuntimeIsFormatIndependent) { +UTest graph_supplied_runtime_is_format_independent{"Distribution/GraphSuppliedRuntimeIsFormatIndependent", [] { for (auto fmt : {dist::Format::Elf, dist::Format::MachO, dist::Format::Pe}) { dist::MechanismInput in; in.format = fmt; @@ -663,15 +652,15 @@ TEST(Distribution, GraphSuppliedRuntimeIsFormatIndependent) { in.requested = dist::Contract::SelfContained; in.role = dist::Role::Distributable; auto m = dist::resolve(in); - EXPECT_EQ(m.unitFlags, " -nostdlib++") ; - EXPECT_EQ(m.effective, dist::Contract::SelfContained); + expect(eq(m.unitFlags, " -nostdlib++")) ; + expect(eq(m.effective, dist::Contract::SelfContained)); } -} +}}; // And it does not depend on the contract the project asked for: a // host-coupled request cannot be honoured by naming the system's runtime when // the graph's is already inside the artefact. -TEST(Distribution, GraphSuppliedRuntimeIgnoresTheRequestedContract) { +UTest graph_supplied_runtime_ignores_the_requested_contract{"Distribution/GraphSuppliedRuntimeIgnoresTheRequestedContract", [] { for (auto c : {dist::Contract::SelfContained, dist::Contract::ToolchainCoupled, dist::Contract::HostCoupled}) { @@ -680,9 +669,9 @@ TEST(Distribution, GraphSuppliedRuntimeIgnoresTheRequestedContract) { in.requested = c; in.role = dist::Role::Distributable; auto m = dist::resolve(in); - EXPECT_EQ(m.unitFlags, " -nostdlib++"); + expect(eq(m.unitFlags, " -nostdlib++")); } -} +}}; // ── format_for: which format a target produces ────────────────────────────── // @@ -692,44 +681,41 @@ TEST(Distribution, GraphSuppliedRuntimeIgnoresTheRequestedContract) { // second: the canonical spellings mcpp itself uses contain neither `apple` nor // `darwin`, and the answer must not depend on the fallback. -TEST(Distribution, FormatIsTakenFromTheTargetAndNotTheFallback) { +UTest format_is_taken_from_the_target_and_not_the_fallback{"Distribution/FormatIsTakenFromTheTargetAndNotTheFallback", [] { // Every fallback, so that a target the vocabulary knows can never be // decided by the machine doing the building. for (auto fb : {dist::Format::Elf, dist::Format::MachO, dist::Format::Pe}) { - EXPECT_EQ(dist::format_for("aarch64-macos", fb), dist::Format::MachO); - EXPECT_EQ(dist::format_for("x86_64-macos", fb), dist::Format::MachO); + expect(eq(dist::format_for("aarch64-macos", fb), dist::Format::MachO)); + expect(eq(dist::format_for("x86_64-macos", fb), dist::Format::MachO)); // iOS shares macOS's Mach-O format. Before `format_for` asked // `is_mach_o()` instead of `os == "macos"`, this triple matched none // of the branches inside the parsed-triple block and fell through to // `fb` itself -- so the assertion below would have failed for two of // the three fallbacks in this loop, the exact "decided by the // machine doing the building" defect this test exists to catch. - EXPECT_EQ(dist::format_for("aarch64-ios", fb), dist::Format::MachO); - EXPECT_EQ(dist::format_for("x86_64-windows-gnu", fb), dist::Format::Pe); - EXPECT_EQ(dist::format_for("x86_64-linux-gnu", fb), dist::Format::Elf); - EXPECT_EQ(dist::format_for("aarch64-linux-musl", fb), dist::Format::Elf); - EXPECT_EQ(dist::format_for("riscv64-none-elf", fb), dist::Format::Elf); + expect(eq(dist::format_for("aarch64-ios", fb), dist::Format::MachO)); + expect(eq(dist::format_for("x86_64-windows-gnu", fb), dist::Format::Pe)); + expect(eq(dist::format_for("x86_64-linux-gnu", fb), dist::Format::Elf)); + expect(eq(dist::format_for("aarch64-linux-musl", fb), dist::Format::Elf)); + expect(eq(dist::format_for("riscv64-none-elf", fb), dist::Format::Elf)); } -} +}}; // The `[target.X]` escape hatch: a spelling the vocabulary cannot parse is all // there is to go on, so LLVM's words are recognised there. -TEST(Distribution, FormatFallsBackToSpellingForAnUnparseableTriple) { - EXPECT_EQ(dist::format_for("arm64-apple-macos14.0", dist::Format::Elf), - dist::Format::MachO); - EXPECT_EQ(dist::format_for("x86_64-w64-mingw32", dist::Format::Elf), - dist::Format::Pe); - EXPECT_EQ(dist::format_for("x86_64-unknown-darwin", dist::Format::Elf), - dist::Format::MachO); -} +UTest format_falls_back_to_spelling_for_an_unparseable_triple{"Distribution/FormatFallsBackToSpellingForAnUnparseableTriple", [] { + expect(eq(dist::format_for("arm64-apple-macos14.0", dist::Format::Elf), dist::Format::MachO)); + expect(eq(dist::format_for("x86_64-w64-mingw32", dist::Format::Elf), dist::Format::Pe)); + expect(eq(dist::format_for("x86_64-unknown-darwin", dist::Format::Elf), dist::Format::MachO)); +}}; // And only then the host. A triple that says nothing at all is the one case // where the machine doing the building is the best available answer. -TEST(Distribution, FormatUsesTheFallbackOnlyWhenTheTripleSaysNothing) { - EXPECT_EQ(dist::format_for("", dist::Format::MachO), dist::Format::MachO); - EXPECT_EQ(dist::format_for("", dist::Format::Pe), dist::Format::Pe); - EXPECT_EQ(dist::format_for("nonsense", dist::Format::Elf), dist::Format::Elf); -} +UTest format_uses_the_fallback_only_when_the_triple_says_nothing{"Distribution/FormatUsesTheFallbackOnlyWhenTheTripleSaysNothing", [] { + expect(eq(dist::format_for("", dist::Format::MachO), dist::Format::MachO)); + expect(eq(dist::format_for("", dist::Format::Pe), dist::Format::Pe)); + expect(eq(dist::format_for("nonsense", dist::Format::Elf), dist::Format::Elf)); +}}; // --------------------------------------------------------------------------- // A SECOND C++ RUNTIME ON THE LINE (mcpp#596). @@ -751,7 +737,7 @@ TEST(Distribution, FormatUsesTheFallbackOnlyWhenTheTripleSaysNothing) { // The byte-level assertions distinguish the explicit static unwinder from // libgcc in the foreign-runtime branch; neither selects a second unwinder. -TEST(Distribution, LinuxLibcxxWithAForeignRuntimeTakesOneUnwinder) { +UTest linux_libcxx_with_a_foreign_runtime_takes_one_unwinder{"Distribution/LinuxLibcxxWithAForeignRuntimeTakesOneUnwinder", [] { dist::MechanismInput in; in.format = dist::Format::Elf; in.stdlibId = "libc++"; @@ -763,27 +749,25 @@ TEST(Distribution, LinuxLibcxxWithAForeignRuntimeTakesOneUnwinder) { // The explicit archive supplies the unwinder without a second driver // selection; the foreign-runtime branch still selects libgcc instead. auto alone = dist::resolve(in); - EXPECT_EQ(alone.unitFlags, - " -nostdlib++ /tc/libc++.a /tc/libc++abi.a /tc/libunwind.a --unwindlib=none"); + expect(eq(alone.unitFlags, " -nostdlib++ /tc/libc++.a /tc/libc++abi.a /tc/libunwind.a --unwindlib=none")); in.foreignCxxRuntime = true; auto shared = dist::resolve(in); // The contract is NOT degraded: the C++ runtime is still embedded, and // libgcc_s is in the process either way because libstdc++ needs it. - EXPECT_EQ(shared.effective, dist::Contract::SelfContained); - EXPECT_FALSE(shared.degraded); - EXPECT_TRUE(shared.diagnostic.empty()); - EXPECT_EQ(shared.unitFlags, - " -nostdlib++ /tc/libc++.a /tc/libc++abi.a" + expect(eq(shared.effective, dist::Contract::SelfContained)); + expect(!shared.degraded); + expect(shared.diagnostic.empty()); + expect(eq(shared.unitFlags, " -nostdlib++ /tc/libc++.a /tc/libc++abi.a" " -Wl,--exclude-libs,libc++.a -Wl,--exclude-libs,libc++abi.a" - " --unwindlib=libgcc"); + " --unwindlib=libgcc")); // The payload's unwinder archive is NOT on the line: linking it is what // creates the second unwinder. - EXPECT_EQ(shared.unitFlags.find("libunwind.a"), std::string::npos); -} + expect(eq(shared.unitFlags.find("libunwind.a"), std::string::npos)); +}}; // A shared library retains archive privacy and uses the explicit unwinder. -TEST(Distribution, ASharedLibraryStillHidesTheArchivesWithoutASecondRuntime) { +UTest a_shared_library_still_hides_the_archives_without_a_second_runtime{"Distribution/ASharedLibraryStillHidesTheArchivesWithoutASecondRuntime", [] { dist::MechanismInput in; in.format = dist::Format::Elf; in.stdlibId = "libc++"; @@ -793,11 +777,10 @@ TEST(Distribution, ASharedLibraryStillHidesTheArchivesWithoutASecondRuntime) { in.libcxxAbiArchive = "/tc/libc++abi.a"; in.libunwindArchive = "/tc/libunwind.a"; auto m = dist::resolve(in); - EXPECT_EQ(m.unitFlags, - " -nostdlib++ /tc/libc++.a /tc/libc++abi.a" + expect(eq(m.unitFlags, " -nostdlib++ /tc/libc++.a /tc/libc++abi.a" " -Wl,--exclude-libs,libc++.a -Wl,--exclude-libs,libc++abi.a" - " /tc/libunwind.a --unwindlib=none -Wl,--exclude-libs,libunwind.a"); -} + " /tc/libunwind.a --unwindlib=none -Wl,--exclude-libs,libunwind.a")); +}}; // THE NAMES `--exclude-libs` MATCHES ARE THE ARCHIVES THE LINKER OPENS // (#634 A6). The Android NDK's per-API `libc++.a` is a linker script, @@ -805,7 +788,7 @@ TEST(Distribution, ASharedLibraryStillHidesTheArchivesWithoutASecondRuntime) { // hiding `libc++.a` alone left 161 dynamic symbols in a self-contained shared // library where naming `libc++_static.a` left 4 (measured). The paths on the // line are unchanged; only the hidden names grow, and only when given. -TEST(Distribution, ALinkerScriptsArchivesAreTheNamesHidden) { +UTest a_linker_scripts_archives_are_the_names_hidden{"Distribution/ALinkerScriptsArchivesAreTheNamesHidden", [] { dist::MechanismInput in; in.format = dist::Format::Elf; in.stdlibId = "libc++"; @@ -816,18 +799,17 @@ TEST(Distribution, ALinkerScriptsArchivesAreTheNamesHidden) { in.libunwindArchive = "/ndk/lib/clang/21/lib/linux/x86_64/libunwind.a"; in.libcxxLinkedArchiveNames = {"libc++.a", "libc++abi.a", "libc++_static.a"}; auto m = dist::resolve(in); - EXPECT_EQ(m.unitFlags, - " -nostdlib++ /ndk/sysroot/usr/lib/x86_64-linux-android/24/libc++.a" + expect(eq(m.unitFlags, " -nostdlib++ /ndk/sysroot/usr/lib/x86_64-linux-android/24/libc++.a" " /ndk/sysroot/usr/lib/x86_64-linux-android/libc++abi.a" " -Wl,--exclude-libs,libc++.a -Wl,--exclude-libs,libc++abi.a" " -Wl,--exclude-libs,libc++_static.a" " /ndk/lib/clang/21/lib/linux/x86_64/libunwind.a" - " --unwindlib=none -Wl,--exclude-libs,libunwind.a"); + " --unwindlib=none -Wl,--exclude-libs,libunwind.a")); // An executable still carries no guard, whatever the names are. in.role = dist::Role::Test; - EXPECT_EQ(dist::resolve(in).unitFlags.find("exclude-libs"), std::string::npos); -} + expect(eq(dist::resolve(in).unitFlags.find("exclude-libs"), std::string::npos)); +}}; // ─── Format::Wasm, the member the module predicted and deferred ──────────── // @@ -835,17 +817,17 @@ TEST(Distribution, ALinkerScriptsArchivesAreTheNamesHidden) { // and reaches `hostFallback`, answering the MACHINE's format -- "the same // defect class its own header measured for macOS" -- and deferred the fourth // member "to whoever gives this module a mechanism for it". -TEST(Distribution, FormatAnswersWasmForAWasmTargetAndNotTheHosts) { +UTest format_answers_wasm_for_a_wasm_target_and_not_the_hosts{"Distribution/FormatAnswersWasmForAWasmTargetAndNotTheHosts", [] { using mcpp::build::dist::Format; using mcpp::build::dist::format_for; // Every host fallback, so this cannot pass by agreeing with the machine. for (auto fallback : {Format::Elf, Format::MachO, Format::Pe}) - EXPECT_EQ(format_for("wasm32-emscripten", fallback), Format::Wasm); + expect(eq(format_for("wasm32-emscripten", fallback), Format::Wasm)); // The three it already answered stay unchanged. - EXPECT_EQ(format_for("x86_64-linux-gnu", Format::Pe), Format::Elf); - EXPECT_EQ(format_for("aarch64-macos", Format::Elf), Format::MachO); - EXPECT_EQ(format_for("x86_64-windows-gnu", Format::Elf), Format::Pe); -} + expect(eq(format_for("x86_64-linux-gnu", Format::Pe), Format::Elf)); + expect(eq(format_for("aarch64-macos", Format::Elf), Format::MachO)); + expect(eq(format_for("x86_64-windows-gnu", Format::Elf), Format::Pe)); +}}; // THERE IS NOTHING TO BE COUPLED TO, so the contract is satisfied with // nothing added and there is no degradation to report. @@ -854,21 +836,21 @@ TEST(Distribution, FormatAnswersWasmForAWasmTargetAndNotTheHosts) { // `libc++.so` that cannot exist for the target, on an artefact that has no // run-time dependency of any kind. A diagnostic is for a BROKEN PROMISE; that // one was a promise about a mechanism the format does not have. -TEST(Distribution, WasmIsSelfContainedByConstructionAndSaysNothing) { +UTest wasm_is_self_contained_by_construction_and_says_nothing{"Distribution/WasmIsSelfContainedByConstructionAndSaysNothing", [] { mcpp::build::dist::MechanismInput in; in.format = mcpp::build::dist::Format::Wasm; in.stdlibId = "libc++"; in.requested = mcpp::build::dist::Contract::SelfContained; in.role = mcpp::build::dist::Role::Distributable; auto m = mcpp::build::dist::resolve(in); - EXPECT_EQ(m.effective, mcpp::build::dist::Contract::SelfContained); - EXPECT_FALSE(m.degraded); - EXPECT_TRUE(m.diagnostic.empty()) << m.diagnostic; + expect(eq(m.effective, mcpp::build::dist::Contract::SelfContained)); + expect(!m.degraded); + expect(m.diagnostic.empty()) << m.diagnostic; // And no flags: libc++ reaches a wasm link through `em++`'s own link line, // so naming archives from a sysroot this module did not resolve would be a // second answer to a question the driver has already answered. - EXPECT_TRUE(m.unitFlags.empty()) << m.unitFlags; -} + expect(m.unitFlags.empty()) << m.unitFlags; +}}; // --------------------------------------------------------------------------- // #646 F3a -- one process, one C++ runtime. @@ -878,77 +860,77 @@ TEST(Distribution, WasmIsSelfContainedByConstructionAndSaysNothing) { // held two C++ runtimes, and with llvm@22.1.8 it aborted with std::bad_cast. // An unstated program or test contract takes the shared library's when its // image loads a C++ shared library of the build. -TEST(Distribution, AProgramThatLoadsACxxSharedLibraryTakesItsContractOnElf) { +UTest a_program_that_loads_a_cxx_shared_library_takes_its_contract_on_elf{"Distribution/AProgramThatLoadsACxxSharedLibraryTakesItsContractOnElf", [] { const dist::ContractStatement nothing{}; auto c = dist::role_contracts(nothing, dist::Format::Elf, {.program = true, .tests = true}); - EXPECT_EQ(c.shared, dist::Contract::ToolchainCoupled); - EXPECT_EQ(c.program, dist::Contract::ToolchainCoupled); - EXPECT_EQ(c.tests, dist::Contract::ToolchainCoupled); + expect(eq(c.shared, dist::Contract::ToolchainCoupled)); + expect(eq(c.program, dist::Contract::ToolchainCoupled)); + expect(eq(c.tests, dist::Contract::ToolchainCoupled)); // The intermediate role links no runtime and keeps the program default. - EXPECT_EQ(c.intermediate, dist::Contract::SelfContained); - EXPECT_FALSE(c.programStated); - EXPECT_FALSE(dist::runtime_split(c, dist::Format::Elf, - {.program = true, .tests = true}).has_value()); -} + expect(eq(c.intermediate, dist::Contract::SelfContained)); + expect(!c.programStated); + expect(!(dist::runtime_split(c, dist::Format::Elf, + {.program = true, .tests = true}).has_value())); +}}; // Nothing moves for a program that loads no C++ shared library: the default // that makes a shipped binary portable is untouched. -TEST(Distribution, AProgramThatLoadsNoCxxSharedLibraryKeepsItsDefault) { +UTest a_program_that_loads_no_cxx_shared_library_keeps_its_default{"Distribution/AProgramThatLoadsNoCxxSharedLibraryKeepsItsDefault", [] { auto c = dist::role_contracts({}, dist::Format::Elf, {}); - EXPECT_EQ(c.program, dist::Contract::SelfContained); - EXPECT_EQ(c.tests, dist::Contract::SelfContained); + expect(eq(c.program, dist::Contract::SelfContained)); + expect(eq(c.tests, dist::Contract::SelfContained)); auto onlyTests = dist::role_contracts({}, dist::Format::Elf, {.tests = true}); - EXPECT_EQ(onlyTests.program, dist::Contract::SelfContained); - EXPECT_EQ(onlyTests.tests, dist::Contract::ToolchainCoupled); -} + expect(eq(onlyTests.program, dist::Contract::SelfContained)); + expect(eq(onlyTests.tests, dist::Contract::ToolchainCoupled)); +}}; // Mach-O and PE keep their table: Mach-O's shared default is already // self-contained (the F2 question is measured separately), and PE resolves // imports per DLL. -TEST(Distribution, TheFollowingRuleIsElfOnly) { +UTest the_following_rule_is_elf_only{"Distribution/TheFollowingRuleIsElfOnly", [] { for (auto f : {dist::Format::MachO, dist::Format::Pe, dist::Format::Wasm}) { auto c = dist::role_contracts({}, f, {.program = true, .tests = true}); - EXPECT_EQ(c.program, dist::Contract::SelfContained); - EXPECT_FALSE(dist::runtime_split(c, f, {.program = true}).has_value()); + expect(eq(c.program, dist::Contract::SelfContained)); + expect(!(dist::runtime_split(c, f, {.program = true}).has_value())); } -} +}}; // A STATED contract is never moved. A stated self-contained program over a // coupled C++ shared library is the split, and is named for the caller to // refuse; a stated self-contained project (the library self-contained too) is // the documented private-copy arrangement and is not. -TEST(Distribution, AStatedSelfContainedProgramOverACoupledLibraryIsASplit) { +UTest a_stated_self_contained_program_over_a_coupled_library_is_a_split{"Distribution/AStatedSelfContainedProgramOverACoupledLibraryIsASplit", [] { const dist::ContractStatement split{ .cxxRuntime = "self-contained", .cxxRuntimeShared = "toolchain-coupled"}; auto c = dist::role_contracts(split, dist::Format::Elf, {.program = true}); - EXPECT_EQ(c.program, dist::Contract::SelfContained); - EXPECT_TRUE(c.programStated); + expect(eq(c.program, dist::Contract::SelfContained)); + expect(bool(c.programStated)); auto role = dist::runtime_split(c, dist::Format::Elf, {.program = true}); - ASSERT_TRUE(role.has_value()); - EXPECT_EQ(*role, dist::Role::Distributable); + expect(fatal(role.has_value())); + expect(eq(*role, dist::Role::Distributable)); const dist::ContractStatement whole{.cxxRuntime = "self-contained"}; auto w = dist::role_contracts(whole, dist::Format::Elf, {.program = true}); - EXPECT_EQ(w.shared, dist::Contract::SelfContained); - EXPECT_FALSE(dist::runtime_split(w, dist::Format::Elf, {.program = true}).has_value()); + expect(eq(w.shared, dist::Contract::SelfContained)); + expect(!(dist::runtime_split(w, dist::Format::Elf, {.program = true}).has_value())); const dist::ContractStatement tests{.cxxRuntimeTests = "self-contained"}; auto t = dist::role_contracts(tests, dist::Format::Elf, {.tests = true}); auto testRole = dist::runtime_split(t, dist::Format::Elf, {.tests = true}); - ASSERT_TRUE(testRole.has_value()); - EXPECT_EQ(*testRole, dist::Role::Test); -} + expect(fatal(testRole.has_value())); + expect(eq(*testRole, dist::Role::Test)); +}}; // `static_stdlib = false` is a statement for every role: all of them are // host-coupled and nothing splits. -TEST(Distribution, TheOlderSpellingStatesEveryRole) { +UTest the_older_spelling_states_every_role{"Distribution/TheOlderSpellingStatesEveryRole", [] { const dist::ContractStatement host{.staticStdlib = false}; auto c = dist::role_contracts(host, dist::Format::Elf, {.program = true, .tests = true}); - EXPECT_EQ(c.program, dist::Contract::HostCoupled); - EXPECT_EQ(c.shared, dist::Contract::HostCoupled); - EXPECT_FALSE(dist::runtime_split(c, dist::Format::Elf, {.program = true}).has_value()); -} + expect(eq(c.program, dist::Contract::HostCoupled)); + expect(eq(c.shared, dist::Contract::HostCoupled)); + expect(!(dist::runtime_split(c, dist::Format::Elf, {.program = true}).has_value())); +}}; // --------------------------------------------------------------------------- // #649 E10, closed by #718. Clang on the MSVC ABI used to be given no CRT @@ -963,7 +945,7 @@ TEST(Distribution, TheOlderSpellingStatesEveryRole) { // explicit `toolchain-coupled` (prepare/plan.cpp), not a table cell, and an // undeclared one is resolved to `host-coupled` before this table ever runs // (`dist::msvc_abi_default_contract`). -TEST(Distribution, NoMechanismCellDistinguishesClangFromClOnTheMsvcAbi) { +UTest no_mechanism_cell_distinguishes_clang_from_cl_on_the_msvc_abi{"Distribution/NoMechanismCellDistinguishesClangFromClOnTheMsvcAbi", [] { dist::MechanismInput in; in.format = dist::Format::Pe; in.stdlibId = "msvc"; @@ -973,25 +955,25 @@ TEST(Distribution, NoMechanismCellDistinguishesClangFromClOnTheMsvcAbi) { in.explicitRequest = true; in.requested = dist::Contract::ToolchainCoupled; auto coupled = dist::resolve(in); - EXPECT_EQ(coupled.effective, dist::Contract::ToolchainCoupled); - EXPECT_TRUE(coupled.deployToolchainRuntime); - EXPECT_FALSE(coupled.degraded); - EXPECT_TRUE(coupled.diagnostic.empty()); + expect(eq(coupled.effective, dist::Contract::ToolchainCoupled)); + expect(bool(coupled.deployToolchainRuntime)); + expect(!coupled.degraded); + expect(coupled.diagnostic.empty()); // /MT, requesting (and delivering) the static CRT. in.msvcStaticCrt = true; in.requested = dist::Contract::SelfContained; auto selfContained = dist::resolve(in); - EXPECT_EQ(selfContained.effective, dist::Contract::SelfContained); - EXPECT_FALSE(selfContained.degraded); - EXPECT_TRUE(selfContained.diagnostic.empty()); + expect(eq(selfContained.effective, dist::Contract::SelfContained)); + expect(!selfContained.degraded); + expect(selfContained.diagnostic.empty()); // The undeclared default: quiet, whichever way `msvcStaticCrt` reads. in.explicitRequest = false; in.msvcStaticCrt = false; in.requested = dist::Contract::HostCoupled; auto quiet = dist::resolve(in); - EXPECT_EQ(quiet.effective, dist::Contract::HostCoupled); - EXPECT_FALSE(quiet.degraded); - EXPECT_TRUE(quiet.diagnostic.empty()); -} + expect(eq(quiet.effective, dist::Contract::HostCoupled)); + expect(!quiet.degraded); + expect(quiet.diagnostic.empty()); +}}; diff --git a/tests/unit/test_doctor_fixincludes.cpp b/tests/unit/test_doctor_fixincludes.cpp index 627c83eb8..d9bfbc6e3 100644 --- a/tests/unit/test_doctor_fixincludes.cpp +++ b/tests/unit/test_doctor_fixincludes.cpp @@ -1,8 +1,10 @@ -#include import std; +import mcpplibs.mtest; import mcpp.doctor; +using namespace mcpplibs::mtest; + using mcpp::doctor::find_fixincludes_banners; // mcpp#687: GCC's `fixincludes` step writes a frozen copy of the BUILD @@ -54,67 +56,66 @@ constexpr std::string_view kReadmeFixture = } // namespace -TEST(DoctorFixincludes, ABannerFileIsReportedWithItsSource) { +UTest a_banner_file_is_reported_with_its_source{"DoctorFixincludes/ABannerFileIsReportedWithItsSource", [] { auto root = make_tempdir("mcpp-fixincludes-banner"); write_file(root / "lib" / "gcc" / "x86_64-linux-gnu" / "13.3.0" / "include-fixed" / "pthread.h", kBannerFixture); auto findings = find_fixincludes_banners(root); - ASSERT_EQ(findings.size(), 1u); - EXPECT_EQ(findings[0].file, fs::path("lib") / "gcc" / "x86_64-linux-gnu" - / "13.3.0" / "include-fixed" / "pthread.h"); - EXPECT_EQ(findings[0].source, - "/home/xlings/.xlings_data/subos/linux/usr/include/pthread.h"); + expect(fatal(eq(findings.size(), 1u))); + expect(eq(findings[0].file, fs::path("lib") / "gcc" / "x86_64-linux-gnu" + / "13.3.0" / "include-fixed" / "pthread.h")); + expect(eq(findings[0].source, "/home/xlings/.xlings_data/subos/linux/usr/include/pthread.h")); fs::remove_all(root); -} +}}; // The exact 16.1.0 shape: an `include-fixed` directory that exists (fixincludes // always creates it) but holds only its own README, no fixed header at all. -TEST(DoctorFixincludes, AReadmeWithNoBannerIsNotReported) { +UTest a_readme_with_no_banner_is_not_reported{"DoctorFixincludes/AReadmeWithNoBannerIsNotReported", [] { auto root = make_tempdir("mcpp-fixincludes-readme-only"); write_file(root / "lib" / "gcc" / "x86_64-linux-gnu" / "16.1.0" / "include-fixed" / "README", kReadmeFixture); - EXPECT_TRUE(find_fixincludes_banners(root).empty()); + expect(find_fixincludes_banners(root).empty()); fs::remove_all(root); -} +}}; // A fixed header that carries no banner (some fixincludes edits do not add // one — e.g. a bare wrapped `limits.h`) is not a match either: the check is // specifically for the auto-edited-FROM-a-source-path shape mcpp#687 is // about, not for "any file under include-fixed". -TEST(DoctorFixincludes, AFixedHeaderWithoutTheBannerIsNotReported) { +UTest a_fixed_header_without_the_banner_is_not_reported{"DoctorFixincludes/AFixedHeaderWithoutTheBannerIsNotReported", [] { auto root = make_tempdir("mcpp-fixincludes-no-banner"); write_file(root / "lib" / "gcc" / "x86_64-linux-gnu" / "13.3.0" / "include-fixed" / "limits.h", "/* -*- Mode: C -*- */\n#ifndef _GCC_LIMITS_H_\n"); - EXPECT_TRUE(find_fixincludes_banners(root).empty()); + expect(find_fixincludes_banners(root).empty()); fs::remove_all(root); -} +}}; // A file that carries the banner text but sits OUTSIDE any `include-fixed` // directory does not count — the check is scoped to the one directory // fixincludes actually writes into, not to the whole payload tree. -TEST(DoctorFixincludes, ABannerOutsideIncludeFixedIsNotReported) { +UTest a_banner_outside_include_fixed_is_not_reported{"DoctorFixincludes/ABannerOutsideIncludeFixedIsNotReported", [] { auto root = make_tempdir("mcpp-fixincludes-outside"); write_file(root / "lib" / "gcc" / "x86_64-linux-gnu" / "13.3.0" / "include" / "pthread.h", kBannerFixture); - EXPECT_TRUE(find_fixincludes_banners(root).empty()); + expect(find_fixincludes_banners(root).empty()); fs::remove_all(root); -} +}}; // Every `include-fixed` directory under the payload root is scanned, not // only the first one found — a payload that ships more than one fixed // header (the common case: pthread.h plus others) is reported in full, and // two DIFFERENT triples under the same payload root (a cross payload's own // shape) are both covered by one call. -TEST(DoctorFixincludes, MultipleFindingsAreAllReported) { +UTest multiple_findings_are_all_reported{"DoctorFixincludes/MultipleFindingsAreAllReported", [] { auto root = make_tempdir("mcpp-fixincludes-multi"); write_file(root / "lib" / "gcc" / "x86_64-linux-gnu" / "13.3.0" / "include-fixed" / "pthread.h", kBannerFixture); @@ -122,14 +123,14 @@ TEST(DoctorFixincludes, MultipleFindingsAreAllReported) { / "include-fixed" / "stdio.h", kBannerFixture); auto findings = find_fixincludes_banners(root); - EXPECT_EQ(findings.size(), 2u); + expect(eq(findings.size(), 2u)); fs::remove_all(root); -} +}}; // A payload root that does not exist at all (nothing installed under that // name/version) is not an error — it is the same as "nothing to report". -TEST(DoctorFixincludes, ANonexistentPayloadRootReportsNothing) { - EXPECT_TRUE(find_fixincludes_banners( +UTest a_nonexistent_payload_root_reports_nothing{"DoctorFixincludes/ANonexistentPayloadRootReportsNothing", [] { + expect(find_fixincludes_banners( fs::temp_directory_path() / "mcpp-fixincludes-does-not-exist").empty()); -} +}}; diff --git a/tests/unit/test_doctor_runpath.cpp b/tests/unit/test_doctor_runpath.cpp index 54b1adae6..b51585c6a 100644 --- a/tests/unit/test_doctor_runpath.cpp +++ b/tests/unit/test_doctor_runpath.cpp @@ -1,13 +1,15 @@ -#include import std; +import mcpplibs.mtest; import mcpp.doctor; +using namespace mcpplibs::mtest; + using mcpp::doctor::parse_readelf_runpath; // `readelf -d` line shape (the case that motivated the check): clang++ with a // RUNPATH that includes a now-removed xim-x-zlib lib dir. -TEST(DoctorRunpath, ParsesRunpathColonSeparatedDirs) { +UTest parses_runpath_colon_separated_dirs{"DoctorRunpath/ParsesRunpathColonSeparatedDirs", [] { std::string dump = " 0x0000000000000001 (NEEDED) Shared library: [libz.so.1]\n" " 0x000000000000001d (RUNPATH) Library runpath: " @@ -17,37 +19,37 @@ TEST(DoctorRunpath, ParsesRunpathColonSeparatedDirs) { " 0x000000000000000c (INIT) 0x1000\n"; auto dirs = parse_readelf_runpath(dump); - ASSERT_EQ(dirs.size(), 3u); - EXPECT_EQ(dirs[0], "/home/u/.mcpp/data/xpkgs/xim-x-llvm/20.1.7/lib"); - EXPECT_EQ(dirs[1], "/home/u/.mcpp/data/xpkgs/xim-x-zlib/1.3.1/lib"); - EXPECT_EQ(dirs[2], "/home/u/.mcpp/registry/subos/default/lib"); -} + expect(fatal(eq(dirs.size(), 3u))); + expect(eq(dirs[0], "/home/u/.mcpp/data/xpkgs/xim-x-llvm/20.1.7/lib")); + expect(eq(dirs[1], "/home/u/.mcpp/data/xpkgs/xim-x-zlib/1.3.1/lib")); + expect(eq(dirs[2], "/home/u/.mcpp/registry/subos/default/lib")); +}}; // DT_RPATH (legacy) is parsed the same way as DT_RUNPATH. -TEST(DoctorRunpath, ParsesLegacyRpath) { +UTest parses_legacy_rpath{"DoctorRunpath/ParsesLegacyRpath", [] { std::string dump = " 0x000000000000000f (RPATH) Library rpath: [/opt/a/lib:/opt/b/lib]\n"; auto dirs = parse_readelf_runpath(dump); - ASSERT_EQ(dirs.size(), 2u); - EXPECT_EQ(dirs[0], "/opt/a/lib"); - EXPECT_EQ(dirs[1], "/opt/b/lib"); -} + expect(fatal(eq(dirs.size(), 2u))); + expect(eq(dirs[0], "/opt/a/lib")); + expect(eq(dirs[1], "/opt/b/lib")); +}}; // A binary with no RUNPATH/RPATH entry yields no dirs. -TEST(DoctorRunpath, NoRunpathYieldsEmpty) { +UTest no_runpath_yields_empty{"DoctorRunpath/NoRunpathYieldsEmpty", [] { std::string dump = " 0x0000000000000001 (NEEDED) Shared library: [libc.so.6]\n" " 0x000000000000000c (INIT) 0x1000\n"; - EXPECT_TRUE(parse_readelf_runpath(dump).empty()); -} + expect(parse_readelf_runpath(dump).empty()); +}}; // Empty path tokens (e.g. a trailing ':') are dropped, not reported as a // missing dir. -TEST(DoctorRunpath, DropsEmptyTokens) { +UTest drops_empty_tokens{"DoctorRunpath/DropsEmptyTokens", [] { std::string dump = " 0x000000000000001d (RUNPATH) Library runpath: [/a/lib::/b/lib:]\n"; auto dirs = parse_readelf_runpath(dump); - ASSERT_EQ(dirs.size(), 2u); - EXPECT_EQ(dirs[0], "/a/lib"); - EXPECT_EQ(dirs[1], "/b/lib"); -} + expect(fatal(eq(dirs.size(), 2u))); + expect(eq(dirs[0], "/a/lib")); + expect(eq(dirs[1], "/b/lib")); +}}; diff --git a/tests/unit/test_dots_screen.cpp b/tests/unit/test_dots_screen.cpp index f1bf2159c..dc0bdf031 100644 --- a/tests/unit/test_dots_screen.cpp +++ b/tests/unit/test_dots_screen.cpp @@ -1,8 +1,10 @@ -#include import std; +import mcpplibs.mtest; import mcpp.ui.dots_screen; +using namespace mcpplibs::mtest; + // The status row's screen and its animations // (.agents/docs/2026-09-30-build-output-refinement-design.md, §5.9 to §5.13): // 24 braille cells, a colour per cell, and four animations that take their @@ -22,7 +24,7 @@ std::vector cells(std::string_view s) { ++i; continue; } - EXPECT_EQ(b & 0xF0, 0xE0) << "not a three-byte code point at " << i; + expect(eq(b & 0xF0, 0xE0)) << "not a three-byte code point at " << i; const char32_t c = (static_cast(b & 0x0F) << 12) | (static_cast(s[i + 1] & 0x3F) << 6) | static_cast(s[i + 2] & 0x3F); @@ -53,63 +55,63 @@ std::string play(Animation& a, int frames, std::size_t perFrame, bool failAt = f } // namespace -TEST(DotsScreen, TheFourAnimationsAreBuiltIn) { +UTest the_four_animations_are_built_in{"DotsScreen/TheFourAnimationsAreBuiltIn", [] { auto names = mcpp::ui::dots_screen::names(); - ASSERT_EQ(names.size(), 4u); - EXPECT_EQ(names[0], "chomp"); - EXPECT_EQ(names[1], "snake"); - EXPECT_EQ(names[2], "stack"); - EXPECT_EQ(names[3], "ions"); - for (auto n : names) EXPECT_NE(make(n, 1), nullptr) << n; - EXPECT_EQ(make("runner", 1), nullptr); - EXPECT_EQ(make("", 1), nullptr); -} + expect(fatal(eq(names.size(), 4u))); + expect(eq(names[0], "chomp")); + expect(eq(names[1], "snake")); + expect(eq(names[2], "stack")); + expect(eq(names[3], "ions")); + for (auto n : names) expect(ne(make(n, 1), nullptr)) << n; + expect(eq(make("runner", 1), nullptr)); + expect(eq(make("", 1), nullptr)); +}}; -TEST(DotsScreen, TheScreenIsTwentyFourBrailleCells) { +UTest the_screen_is_twenty_four_braille_cells{"DotsScreen/TheScreenIsTwentyFourBrailleCells", [] { Screen sc; sc.set(0, 0, Colour::White); // the first dot of the first cell sc.set(47, 3, Colour::Red); // the last dot of the last cell sc.set(99, 0, Colour::White); // off the screen: ignored const auto text = sc.render(false); auto c = cells(text); - ASSERT_EQ(c.size(), 24u); - for (auto cp : c) EXPECT_TRUE(cp >= 0x2800 && cp <= 0x28FF); - EXPECT_EQ(c.front(), char32_t{0x2801}); - EXPECT_EQ(c.back(), char32_t{0x2880}); - EXPECT_EQ(text.find('\x1b'), std::string::npos); + expect(fatal(eq(c.size(), 24u))); + for (auto cp : c) expect(cp >= 0x2800 && cp <= 0x28FF); + expect(eq(c.front(), char32_t{0x2801})); + expect(eq(c.back(), char32_t{0x2880})); + expect(eq(text.find('\x1b'), std::string::npos)); const auto coloured = sc.render(true); - EXPECT_NE(coloured.find("\x1b[91m"), std::string::npos); - EXPECT_TRUE(coloured.ends_with("\x1b[0m")); -} + expect(ne(coloured.find("\x1b[91m"), std::string::npos)); + expect(coloured.ends_with("\x1b[0m")); +}}; -TEST(DotsScreen, AnAnimationIsPureGivenItsSeedAndInputs) { +UTest an_animation_is_pure_given_its_seed_and_inputs{"DotsScreen/AnAnimationIsPureGivenItsSeedAndInputs", [] { for (auto n : mcpp::ui::dots_screen::names()) { auto a = make(n, 42), b = make(n, 42); - EXPECT_EQ(play(*a, 60, 5), play(*b, 60, 5)) << n; + expect(eq(play(*a, 60, 5), play(*b, 60, 5))) << n; } -} +}}; -TEST(DotsScreen, EveryFrameIsTwentyFourCellsAndAFailureTurnsRed) { +UTest every_frame_is_twenty_four_cells_and_a_failure_turns_red{"DotsScreen/EveryFrameIsTwentyFourCellsAndAFailureTurnsRed", [] { for (auto n : mcpp::ui::dots_screen::names()) { auto a = make(n, 7); const auto plain = play(*a, 40, 3); - EXPECT_EQ(cells(plain).size(), 24u) << n; - EXPECT_EQ(plain.find('\n'), std::string::npos) << n; + expect(eq(cells(plain).size(), 24u)) << n; + expect(eq(plain.find('\n'), std::string::npos)) << n; auto f = make(n, 7); const auto failed = play(*f, 40, 3, /*failAt=*/true, /*colour=*/true); - EXPECT_NE(failed.find("\x1b[91m"), std::string::npos) << n; + expect(ne(failed.find("\x1b[91m"), std::string::npos)) << n; } -} +}}; -TEST(DotsScreen, TheSnakeCarriesTheColourOfThePackagesItAte) { +UTest the_snake_carries_the_colour_of_the_packages_it_ate{"DotsScreen/TheSnakeCarriesTheColourOfThePackagesItAte", [] { auto a = make("snake", 3); a->package(Source::Official); a->package(Source::Official); const auto frame = play(*a, 200, 2, false, /*colour=*/true); - EXPECT_NE(frame.find("\x1b[36m"), std::string::npos) << "no segment in the official colour"; -} + expect(ne(frame.find("\x1b[36m"), std::string::npos)) << "no segment in the official colour"; +}}; -TEST(DotsScreen, TheIonsDepositIsTheFraction) { +UTest the_ions_deposit_is_the_fraction{"DotsScreen/TheIonsDepositIsTheFraction", [] { auto a = make("ions", 5); Input in; in.dt = 0.1; @@ -121,10 +123,10 @@ TEST(DotsScreen, TheIonsDepositIsTheFraction) { for (int x = 0; x < kWidth - 4; ++x) for (int y = 0; y < kHeight; ++y) if (sc.at(x, y) != Colour::None) ++lit; - EXPECT_EQ(lit, (kWidth - 4) * kHeight / 2); -} + expect(eq(lit, (kWidth - 4) * kHeight / 2)); +}}; -TEST(DotsScreen, TheChomperStandsAtTheFraction) { +UTest the_chomper_stands_at_the_fraction{"DotsScreen/TheChomperStandsAtTheFraction", [] { auto a = make("chomp", 1); Input in; in.fraction = 1.0; @@ -135,9 +137,9 @@ TEST(DotsScreen, TheChomperStandsAtTheFraction) { bool pellet = false; for (int x = kWidth - 1 - 0; x < kWidth; ++x) if (sc.at(x, 2) == Colour::Grey) pellet = true; - EXPECT_FALSE(pellet); - EXPECT_EQ(sc.at(kWidth - 5, 1), Colour::Yellow); -} + expect(!pellet); + expect(eq(sc.at(kWidth - 5, 1), Colour::Yellow)); +}}; // ─── Every frame returns (build wall-time plan, F1 and W1) ────────────── // @@ -159,7 +161,7 @@ void returns_within(std::chrono::seconds limit, std::string what, done->set_value(); }).detach(); if (finished.wait_for(limit) != std::future_status::ready) { - ADD_FAILURE() << what << " did not return within " << limit.count() << " s"; + expect(false) << what << " did not return within " << limit.count() << " s"; std::fflush(nullptr); std::_Exit(1); } @@ -187,7 +189,7 @@ void play_build(Animation& a, std::uint64_t seed, std::size_t total, int tail) { } // namespace -TEST(DotsScreen, EveryAnimationReturnsFromEveryFrame) { +UTest every_animation_returns_from_every_frame{"DotsScreen/EveryAnimationReturnsFromEveryFrame", [] { for (auto name : mcpp::ui::dots_screen::names()) { const std::string n(name); returns_within(std::chrono::seconds(120), "an animation '" + n + "'", [n] { @@ -197,9 +199,9 @@ TEST(DotsScreen, EveryAnimationReturnsFromEveryFrame) { } }); } -} +}}; -TEST(DotsScreenGames, EveryGameReturnsFromEveryFrame) { +UTest every_game_returns_from_every_frame{"DotsScreenGames/EveryGameReturnsFromEveryFrame", [] { for (auto name : game_names()) { const std::string n(name); returns_within(std::chrono::seconds(120), "a game '" + n + "'", [n] { @@ -217,7 +219,7 @@ TEST(DotsScreenGames, EveryGameReturnsFromEveryFrame) { } }); } -} +}}; // ─── The games of --play-game (design §5.14) ───────────────────────────── @@ -237,28 +239,28 @@ void run(Game& g, double seconds) { } } // namespace -TEST(DotsScreenGames, TheThreeGamesAreBuiltIn) { +UTest the_three_games_are_built_in{"DotsScreenGames/TheThreeGamesAreBuiltIn", [] { auto names = game_names(); - ASSERT_EQ(names.size(), 3u); - EXPECT_EQ(names[0], "snake"); - EXPECT_EQ(names[1], "stack"); - EXPECT_EQ(names[2], "runner"); - for (auto n : names) EXPECT_NE(make_game(n, 1), nullptr) << n; - EXPECT_EQ(make_game("chomp", 1), nullptr); // an animation, not a game -} + expect(fatal(eq(names.size(), 3u))); + expect(eq(names[0], "snake")); + expect(eq(names[1], "stack")); + expect(eq(names[2], "runner")); + for (auto n : names) expect(ne(make_game(n, 1), nullptr)) << n; + expect(eq(make_game("chomp", 1), nullptr)); // an animation, not a game +}}; -TEST(DotsScreenGames, TheSnakeTurnsWithTheArrowsAndAWallEndsTheRound) { +UTest the_snake_turns_with_the_arrows_and_a_wall_ends_the_round{"DotsScreenGames/TheSnakeTurnsWithTheArrowsAndAWallEndsTheRound", [] { auto g = make_game("snake", 4); - EXPECT_FALSE(any_red(*g)); + expect(!any_red(*g)); g->key(Key::Up); // from row 1: row 0, then the wall run(*g, 0.3); - EXPECT_TRUE(any_red(*g)) << "the snake went through the wall"; + expect(bool(any_red(*g))) << "the snake went through the wall"; run(*g, 1.2); // a new round starts after a second - EXPECT_FALSE(any_red(*g)); - EXPECT_EQ(g->score(), 0); -} + expect(!any_red(*g)); + expect(eq(g->score(), 0)); +}}; -TEST(DotsScreenGames, TheStackPlacesPiecesAgainstTheLeftAndScoresClearedColumns) { +UTest the_stack_places_pieces_against_the_left_and_scores_cleared_columns{"DotsScreenGames/TheStackPlacesPiecesAgainstTheLeftAndScoresClearedColumns", [] { auto g = make_game("stack", 9); for (int i = 0; i < 400 && g->score() == 0; ++i) { // Spread the pieces over the four rows so that columns fill. @@ -270,18 +272,18 @@ TEST(DotsScreenGames, TheStackPlacesPiecesAgainstTheLeftAndScoresClearedColumns) g->draw(sc); bool left = false; for (int y = 0; y < kHeight; ++y) left = left || sc.at(0, y) != Colour::None; - EXPECT_TRUE(left) << "no piece rests against the left edge"; - EXPECT_GE(g->best(), g->score()); -} + expect(bool(left)) << "no piece rests against the left edge"; + expect(ge(g->best(), g->score())); +}}; -TEST(DotsScreenGames, TheRunnerJumpsOnSpaceAndACactusEndsTheRound) { +UTest the_runner_jumps_on_space_and_a_cactus_ends_the_round{"DotsScreenGames/TheRunnerJumpsOnSpaceAndACactusEndsTheRound", [] { auto g = make_game("runner", 2); // Without a key, a cactus reaches the runner within a few seconds. bool ended = false; Input in; in.dt = 0.05; for (double t = 0; t < 6 && !ended; t += in.dt) { g->update(in); ended = any_red(*g); } - EXPECT_TRUE(ended) << "no cactus ever touched the runner"; + expect(bool(ended)) << "no cactus ever touched the runner"; run(*g, 1.1); // Space lifts it: its head rises above row 1. g->key(Key::Space); @@ -290,5 +292,5 @@ TEST(DotsScreenGames, TheRunnerJumpsOnSpaceAndACactusEndsTheRound) { g->draw(sc); bool high = false; for (int x = 3; x < 7; ++x) high = high || sc.at(x, 0) == Colour::White; - EXPECT_TRUE(high) << "the runner did not jump"; -} + expect(bool(high)) << "the runner did not jump"; +}}; diff --git a/tests/unit/test_dyndep.cpp b/tests/unit/test_dyndep.cpp index 1a9280e12..997c2627d 100644 --- a/tests/unit/test_dyndep.cpp +++ b/tests/unit/test_dyndep.cpp @@ -1,18 +1,20 @@ -#include import std; +import mcpplibs.mtest; import mcpp.dyndep; +using namespace mcpplibs::mtest; + using namespace mcpp::dyndep; -TEST(Dyndep, BmiBasenameSanitizes) { - EXPECT_EQ(bmi_basename("foo"), "foo.gcm"); - EXPECT_EQ(bmi_basename("foo.bar"), "foo.bar.gcm"); - EXPECT_EQ(bmi_basename("foo:impl"), "foo-impl.gcm"); - EXPECT_EQ(bmi_basename("a.b:c"), "a.b-c.gcm"); -} +UTest bmi_basename_sanitizes{"Dyndep/BmiBasenameSanitizes", [] { + expect(eq(bmi_basename("foo"), "foo.gcm")); + expect(eq(bmi_basename("foo.bar"), "foo.bar.gcm")); + expect(eq(bmi_basename("foo:impl"), "foo-impl.gcm")); + expect(eq(bmi_basename("a.b:c"), "a.b-c.gcm")); +}}; -TEST(Dyndep, ParseDdiPickProvidesRequires) { +UTest parse_ddi_pick_provides_requires{"Dyndep/ParseDdiPickProvidesRequires", [] { constexpr const char* body = R"({ "rules": [ { @@ -23,16 +25,16 @@ TEST(Dyndep, ParseDdiPickProvidesRequires) { ] })"; auto u = parse_ddi(body); - ASSERT_TRUE(u) << u.error(); - EXPECT_EQ(u->primaryOutput.string(), "obj/foo.m.o"); - ASSERT_EQ(u->provides.size(), 1u); - EXPECT_EQ(u->provides[0], "foo"); - ASSERT_EQ(u->requires_.size(), 2u); - EXPECT_EQ(u->requires_[0], "std"); - EXPECT_EQ(u->requires_[1], "foo:impl"); -} - -TEST(Dyndep, EmitDyndepBasic) { + if (auto&& failed = expect(fatal(bool(u))); !failed) failed << u.error(); + expect(eq(u->primaryOutput.string(), "obj/foo.m.o")); + expect(fatal(eq(u->provides.size(), 1u))); + expect(eq(u->provides[0], "foo")); + expect(fatal(eq(u->requires_.size(), 2u))); + expect(eq(u->requires_[0], "std")); + expect(eq(u->requires_[1], "foo:impl")); +}}; + +UTest emit_dyndep_basic{"Dyndep/EmitDyndepBasic", [] { std::vector units = { { "obj/lib-greet.m.o", {"myapp.lib:greet"}, {"std"} }, { "obj/lib.m.o", {"myapp.lib"}, {"myapp.lib:greet", "std"} }, @@ -41,44 +43,42 @@ TEST(Dyndep, EmitDyndepBasic) { auto body = emit_dyndep(units, /*stdImports=*/{}); // Header - EXPECT_NE(body.find("ninja_dyndep_version = 1"), std::string::npos); + expect(ne(body.find("ninja_dyndep_version = 1"), std::string::npos)); // Entry per unit, with implicit inputs only (no implicit outputs — those // are declared statically in build.ninja). - EXPECT_NE(body.find( - "build obj/lib-greet.m.o: dyndep | gcm.cache/std.gcm"), std::string::npos); - EXPECT_NE(body.find( - "build obj/lib.m.o: dyndep | gcm.cache/myapp.lib-greet.gcm gcm.cache/std.gcm"), - std::string::npos); - EXPECT_NE(body.find( - "build obj/main.o: dyndep | gcm.cache/myapp.lib.gcm gcm.cache/std.gcm"), - std::string::npos); + expect(ne(body.find( + "build obj/lib-greet.m.o: dyndep | gcm.cache/std.gcm"), std::string::npos)); + expect(ne(body.find( + "build obj/lib.m.o: dyndep | gcm.cache/myapp.lib-greet.gcm gcm.cache/std.gcm"), std::string::npos)); + expect(ne(body.find( + "build obj/main.o: dyndep | gcm.cache/myapp.lib.gcm gcm.cache/std.gcm"), std::string::npos)); // Each entry must end with restat = 1. int restat_count = 0; for (std::size_t pos = 0; (pos = body.find("restat = 1", pos)) != std::string::npos; ++pos) ++restat_count; - EXPECT_EQ(restat_count, 3); -} + expect(eq(restat_count, 3)); +}}; -TEST(Dyndep, EmitDyndepNoRequires) { +UTest emit_dyndep_no_requires{"Dyndep/EmitDyndepNoRequires", [] { std::vector units = { { "obj/leaf.m.o", {"leaf"}, {} }, }; auto body = emit_dyndep(units, {}); // No '|' implicit-deps section when requires is empty. - EXPECT_NE(body.find("build obj/leaf.m.o: dyndep\n"), std::string::npos); -} + expect(ne(body.find("build obj/leaf.m.o: dyndep\n"), std::string::npos)); +}}; -TEST(Dyndep, EmitDyndepSelfProvideFiltered) { +UTest emit_dyndep_self_provide_filtered{"Dyndep/EmitDyndepSelfProvideFiltered", [] { // Defensive: even if a unit lists its own provides in requires // (shouldn't happen but be robust), we don't emit a self-loop. std::vector units = { { "obj/foo.m.o", {"foo"}, {"foo", "std"} }, }; auto body = emit_dyndep(units, {}); - EXPECT_NE(body.find( - "build obj/foo.m.o: dyndep | gcm.cache/std.gcm\n"), std::string::npos); - EXPECT_EQ(body.find("gcm.cache/foo.gcm"), std::string::npos); -} + expect(ne(body.find( + "build obj/foo.m.o: dyndep | gcm.cache/std.gcm\n"), std::string::npos)); + expect(eq(body.find("gcm.cache/foo.gcm"), std::string::npos)); +}}; // Two-phase (clang): one source, two edges — a BMI edge and an object edge — // and BOTH parse the same imports, so both need the same implicit inputs. @@ -86,7 +86,7 @@ TEST(Dyndep, EmitDyndepSelfProvideFiltered) { // command's `-o`), and an edge with no record is not a warning: ninja refuses // the whole graph with "'…pcm' not mentioned in its dyndep file", naming the // edge rather than the missing record. -TEST(Dyndep, SplitModuleEmitsARecordForBothEdges) { +UTest split_module_emits_a_record_for_both_edges{"Dyndep/SplitModuleEmitsARecordForBothEdges", [] { std::vector units = { { "obj/lib.m.o", {"myapp.lib"}, {"std"} }, }; @@ -96,16 +96,14 @@ TEST(Dyndep, SplitModuleEmitsARecordForBothEdges) { opts.splitModuleEdges = true; auto body = emit_dyndep(units, {}, opts); - EXPECT_NE(body.find("build pcm.cache/myapp.lib.pcm: dyndep | pcm.cache/std.pcm\n"), - std::string::npos) << body; - EXPECT_NE(body.find("build obj/lib.m.o: dyndep | pcm.cache/std.pcm\n"), - std::string::npos) << body; -} + expect(ne(body.find("build pcm.cache/myapp.lib.pcm: dyndep | pcm.cache/std.pcm\n"), std::string::npos)) << body; + expect(ne(body.find("build obj/lib.m.o: dyndep | pcm.cache/std.pcm\n"), std::string::npos)) << body; +}}; // A unit that provides nothing is NOT split — there is only one edge, so a // second record would name something nobody declared. Same failure text as the // missing-record case, from the opposite mistake. -TEST(Dyndep, SplitModuleLeavesNonProvidingUnitsAlone) { +UTest split_module_leaves_non_providing_units_alone{"Dyndep/SplitModuleLeavesNonProvidingUnitsAlone", [] { std::vector units = { { "obj/main.o", {}, {"myapp.lib"} }, }; @@ -119,15 +117,14 @@ TEST(Dyndep, SplitModuleLeavesNonProvidingUnitsAlone) { for (auto pos = body.find("build "); pos != std::string::npos; pos = body.find("build ", pos + 1)) ++records; - EXPECT_EQ(records, 1u) << body; - EXPECT_NE(body.find("build obj/main.o: dyndep | gcm.cache/myapp.lib.gcm\n"), - std::string::npos) << body; -} + expect(eq(records, 1u)) << body; + expect(ne(body.find("build obj/main.o: dyndep | gcm.cache/myapp.lib.gcm\n"), std::string::npos)) << body; +}}; // The single-unit path is what ninja actually runs (one .ddi → one .dd), and it // used to be a separate hand-written copy of the loop above. Pinned together so // a change to one shape cannot silently leave the other behind. -TEST(Dyndep, SplitModuleSingleFileMatchesTheBatchShape) { +UTest split_module_single_file_matches_the_batch_shape{"Dyndep/SplitModuleSingleFileMatchesTheBatchShape", [] { auto tmp = std::filesystem::temp_directory_path() / std::format("mcpp_dyndep_split_{}", std::random_device{}()); std::filesystem::create_directories(tmp); @@ -141,15 +138,15 @@ TEST(Dyndep, SplitModuleSingleFileMatchesTheBatchShape) { opts.splitModuleEdges = true; auto single = emit_dyndep_single(p, opts); - ASSERT_TRUE(single) << single.error(); + if (auto&& failed = expect(fatal(bool(single))); !failed) failed << single.error(); std::vector units = { { "obj/lib.m.o", {"myapp.lib"}, {"std"} } }; - EXPECT_EQ(*single, emit_dyndep(units, {}, opts)); + expect(eq(*single, emit_dyndep(units, {}, opts))); std::filesystem::remove_all(tmp); -} +}}; -TEST(Dyndep, EmitDyndepFromFiles) { +UTest dyndep_emit_dyndep_from_files{"Dyndep/EmitDyndepFromFiles", [] { auto tmp = std::filesystem::temp_directory_path() / std::format("mcpp_dyndep_test_{}", std::random_device{}()); std::filesystem::create_directories(tmp); @@ -166,54 +163,54 @@ TEST(Dyndep, EmitDyndepFromFiles) { })"; auto body = emit_dyndep_from_files({a, b}, {}); - ASSERT_TRUE(body) << body.error(); - EXPECT_NE(body->find("build obj/a.m.o: dyndep\n"), std::string::npos); - EXPECT_NE(body->find( - "build obj/b.m.o: dyndep | gcm.cache/a.gcm\n"), std::string::npos); + if (auto&& failed = expect(fatal(bool(body))); !failed) failed << body.error(); + expect(ne(body->find("build obj/a.m.o: dyndep\n"), std::string::npos)); + expect(ne(body->find( + "build obj/b.m.o: dyndep | gcm.cache/a.gcm\n"), std::string::npos)); std::filesystem::remove_all(tmp); -} +}}; // ── plan-vs-ddi reconciliation (scan_overrides auditor) ── -TEST(VerifyUnitExpectations, MatchPasses) { +UTest match_passes{"VerifyUnitExpectations/MatchPasses", [] { mcpp::dyndep::UnitInfo u; u.primaryOutput = "obj/fmt.o"; u.provides = {"fmt"}; u.requires_ = {"std"}; auto err = mcpp::dyndep::verify_unit_expectations(u, "fmt", {"std"}); - EXPECT_FALSE(err.has_value()) << *err; -} + if (auto&& failed = expect(!err.has_value()); !failed) failed << *err; +}}; -TEST(VerifyUnitExpectations, DivergenceReportsBothSides) { +UTest divergence_reports_both_sides{"VerifyUnitExpectations/DivergenceReportsBothSides", [] { mcpp::dyndep::UnitInfo u; u.primaryOutput = "obj/fmt.o"; u.provides = {"fmt"}; u.requires_ = {"std"}; auto err = mcpp::dyndep::verify_unit_expectations(u, "fmt", {}); - ASSERT_TRUE(err.has_value()); - EXPECT_NE(err->find("divergence"), std::string::npos); - EXPECT_NE(err->find("planned"), std::string::npos); - EXPECT_NE(err->find("std"), std::string::npos); -} + expect(fatal(err.has_value())); + expect(ne(err->find("divergence"), std::string::npos)); + expect(ne(err->find("planned"), std::string::npos)); + expect(ne(err->find("std"), std::string::npos)); +}}; -TEST(VerifyUnitExpectations, ExpectNoneMatchesEmptyUnit) { +UTest expect_none_matches_empty_unit{"VerifyUnitExpectations/ExpectNoneMatchesEmptyUnit", [] { mcpp::dyndep::UnitInfo u; u.primaryOutput = "obj/plain.o"; auto err = mcpp::dyndep::verify_unit_expectations(u, std::nullopt, {}); - EXPECT_FALSE(err.has_value()); -} + expect(!err.has_value()); +}}; // GCC's map names its repository with `$root`, and every relative path after // it lies below that directory: the dyndep step must name the BMI the compiler // reads, or ninja orders the compile before the BMI exists. -TEST(Dyndep, ParseModuleMapRootedAtTheBmiDirectory) { +UTest parse_module_map_rooted_at_the_bmi_directory{"Dyndep/ParseModuleMapRootedAtTheBmiDirectory", [] { auto m = parse_module_map("$root gcm.cache\na a.gcm\ndep.m dep/dep.m.gcm\nstd std.gcm\n"); - EXPECT_EQ(m.size(), 3u); - EXPECT_FALSE(m.contains("$root")); - EXPECT_EQ(m.at("a"), "gcm.cache/a.gcm"); - EXPECT_EQ(m.at("dep.m"), "gcm.cache/dep/dep.m.gcm"); + expect(eq(m.size(), 3u)); + expect(!m.contains("$root")); + expect(eq(m.at("a"), "gcm.cache/a.gcm")); + expect(eq(m.at("dep.m"), "gcm.cache/dep/dep.m.gcm")); // Without `$root` (clang's and MSVC's maps) a path is taken as written. auto plain = parse_module_map("m pcm.cache/core/m.pcm\n"); - EXPECT_EQ(plain.at("m"), "pcm.cache/core/m.pcm"); -} + expect(eq(plain.at("m"), "pcm.cache/core/m.pcm")); +}}; diff --git a/tests/unit/test_elf_runtime.cpp b/tests/unit/test_elf_runtime.cpp index fee1db493..94712b287 100644 --- a/tests/unit/test_elf_runtime.cpp +++ b/tests/unit/test_elf_runtime.cpp @@ -1,12 +1,14 @@ -#include import std; +import mcpplibs.mtest; import mcpplibs.os.platform; import mcpp.runtime.elf; import mcpp.runtime.binding; import mcpp.toolchain.post_install; import mcpp.build.symbol_provision; +using namespace mcpplibs::mtest; + namespace elf = mcpp::runtime::elf; namespace runtime = mcpp::runtime; namespace tc = mcpp::toolchain; @@ -198,7 +200,7 @@ elf::ElfRuntimeFacts facts(std::filesystem::path path, return out; } -TEST(RuntimePayload, ExactBindingWinsRegardlessOfDirectoryOrder) { +UTest exact_binding_wins_regardless_of_directory_order{"RuntimePayload/ExactBindingWinsRegardlessOfDirectoryOrder", [] { Tmp t; auto root = t.path / "xim-x-glibc"; std::filesystem::create_directories(root / "2.39" / "lib64"); @@ -207,11 +209,11 @@ TEST(RuntimePayload, ExactBindingWinsRegardlessOfDirectoryOrder) { std::ofstream(root / "2.44" / "lib64" / "ld-linux-x86-64.so.2") << "x"; auto selected = tc::select_glibc_payload_lib(root, "glibc@2.44"); - ASSERT_TRUE(selected.has_value()) << selected.error(); - EXPECT_EQ(*selected, root / "2.44" / "lib64"); -} + if (auto&& failed = expect(fatal(selected.has_value())); !failed) failed << selected.error(); + expect(eq(*selected, root / "2.44" / "lib64")); +}}; -TEST(RuntimePayload, MissingOrMalformedBindingIsAnError) { +UTest missing_or_malformed_binding_is_an_error{"RuntimePayload/MissingOrMalformedBindingIsAnError", [] { Tmp t; auto root = t.path / "xim-x-glibc"; std::filesystem::create_directories(root / "2.39" / "lib64"); @@ -219,46 +221,42 @@ TEST(RuntimePayload, MissingOrMalformedBindingIsAnError) { for (auto id : {"", "glibc", "glibc@2.44", "musl@1.2.5", "glibc@../2.39"}) { auto selected = tc::select_glibc_payload_lib(root, id); - EXPECT_FALSE(selected.has_value()) << id; - if (!selected) EXPECT_FALSE(selected.error().empty()); + expect(!selected.has_value()) << id; + if (!selected) expect(!(selected.error().empty())); } -} +}}; -TEST(ElfRuntime, ParsesProgramDynamicAndGnuVersionTablesInternally) { +UTest parses_program_dynamic_and_gnu_version_tables_internally{"ElfRuntime/ParsesProgramDynamicAndGnuVersionTablesInternally", [] { if constexpr (!mcpplibs::os::is_linux) - GTEST_SKIP() << "ELF table inspection is exercised on native Linux"; + skip("ELF table inspection is exercised on native Linux"); Tmp t; auto parsed = elf::inspect_elf_runtime(write_elf_fixture(t.path / "fixture")); - ASSERT_TRUE(parsed.has_value()) << parsed.error(); - EXPECT_EQ(parsed->interp, - "/store/glibc/2.44/lib64/ld-linux-x86-64.so.2"); - EXPECT_EQ(parsed->runpaths, - (std::vector{"/host/z", "/host/a"})); - EXPECT_EQ(parsed->needed, (std::vector{"libc.so.6"})); - EXPECT_EQ(parsed->requiredGlibcVersions, - (std::vector{"GLIBC_2.40"})); - EXPECT_EQ(parsed->definedGlibcVersions, - (std::vector{"GLIBC_2.44"})); -} - -TEST(ElfRuntime, RejectsUnsupportedOrTruncatedElfWithoutGuessing) { + if (auto&& failed = expect(fatal(parsed.has_value())); !failed) failed << parsed.error(); + expect(eq(parsed->interp, "/store/glibc/2.44/lib64/ld-linux-x86-64.so.2")); + expect(eq(parsed->runpaths, (std::vector{"/host/z", "/host/a"}))); + expect(eq(parsed->needed, (std::vector{"libc.so.6"}))); + expect(eq(parsed->requiredGlibcVersions, (std::vector{"GLIBC_2.40"}))); + expect(eq(parsed->definedGlibcVersions, (std::vector{"GLIBC_2.44"}))); +}}; + +UTest rejects_unsupported_or_truncated_elf_without_guessing{"ElfRuntime/RejectsUnsupportedOrTruncatedElfWithoutGuessing", [] { if constexpr (!mcpplibs::os::is_linux) - GTEST_SKIP() << "ELF table inspection is exercised on native Linux"; + skip("ELF table inspection is exercised on native Linux"); Tmp t; std::ofstream(t.path / "text") << "not an ELF"; auto text = elf::inspect_elf_runtime(t.path / "text"); - EXPECT_FALSE(text.has_value()); + expect(!text.has_value()); std::ofstream tiny(t.path / "tiny", std::ios::binary); tiny.write("\177ELF\2\1", 6); tiny.close(); auto truncated = elf::inspect_elf_runtime(t.path / "tiny"); - EXPECT_FALSE(truncated.has_value()); -} + expect(!truncated.has_value()); +}}; -TEST(ElfRuntime, ReusesAnAlreadyLoadedSonameAcrossDependencyRunpaths) { +UTest reuses_an_already_loaded_soname_across_dependency_runpaths{"ElfRuntime/ReusesAnAlreadyLoadedSonameAcrossDependencyRunpaths", [] { if constexpr (!mcpplibs::os::is_linux) - GTEST_SKIP() << "ELF/glibc runtime physics only apply on Linux"; + skip("ELF/glibc runtime physics only apply on Linux"); Tmp t; auto payload = t.path / "store"; auto glibc39 = payload / "2.39" / "lib64"; @@ -286,12 +284,11 @@ TEST(ElfRuntime, ReusesAnAlreadyLoadedSonameAcrossDependencyRunpaths) { }); auto resolution = elf::resolve_runtime_closure(app, binding_for(payload)); - ASSERT_TRUE(resolution.unresolved.empty()); - ASSERT_EQ(resolution.resolvedLibcs.size(), 1u) + expect(fatal(resolution.unresolved.empty())); + expect(fatal(eq(resolution.resolvedLibcs.size(), 1u))) << "a later dependency must reuse the process-global libc SONAME"; - EXPECT_EQ(resolution.resolvedLibcs.front(), - std::filesystem::weakly_canonical(glibc44 / "libc.so.6")); -} + expect(eq(resolution.resolvedLibcs.front(), std::filesystem::weakly_canonical(glibc44 / "libc.so.6"))); +}}; // DT_RPATH REACHES THE WHOLE CHAIN; DT_RUNPATH REACHES ONE OBJECT. // @@ -304,9 +301,9 @@ TEST(ElfRuntime, ReusesAnAlreadyLoadedSonameAcrossDependencyRunpaths) { // build naming eight libraries, and the artifact it had just linked resolved // seven of them -- failing only on the one that belongs to a driver the // machine does not have. -TEST(ElfRuntime, ADependencyInheritsTheExecutablesRpath) { +UTest a_dependency_inherits_the_executables_rpath{"ElfRuntime/ADependencyInheritsTheExecutablesRpath", [] { if constexpr (!mcpplibs::os::is_linux) - GTEST_SKIP() << "ELF/glibc runtime physics only apply on Linux"; + skip("ELF/glibc runtime physics only apply on Linux"); Tmp t; auto payload = t.path / "store"; auto glibc = payload / "2.44" / "lib64"; @@ -328,18 +325,18 @@ TEST(ElfRuntime, ADependencyInheritsTheExecutablesRpath) { }); auto resolution = elf::resolve_runtime_closure(app, binding_for(payload)); - EXPECT_TRUE(resolution.unresolvedSonames.empty()) + expect(resolution.unresolvedSonames.empty()) << "unresolved: " << (resolution.unresolvedSonames.empty() ? std::string{} : resolution.unresolvedSonames.front()); -} +}}; // …and the suppression, which is the half that makes the rule a rule. An // object carrying DT_RUNPATH uses no RPATH at all -- its own or inherited -- // so a test with only the leg above would also pass on an implementation that // inherited unconditionally. -TEST(ElfRuntime, ADependencyWithItsOwnRunpathDoesNotInheritOne) { +UTest a_dependency_with_its_own_runpath_does_not_inherit_one{"ElfRuntime/ADependencyWithItsOwnRunpathDoesNotInheritOne", [] { if constexpr (!mcpplibs::os::is_linux) - GTEST_SKIP() << "ELF/glibc runtime physics only apply on Linux"; + skip("ELF/glibc runtime physics only apply on Linux"); Tmp t; auto payload = t.path / "store"; auto glibc = payload / "2.44" / "lib64"; @@ -363,13 +360,13 @@ TEST(ElfRuntime, ADependencyWithItsOwnRunpathDoesNotInheritOne) { }); auto resolution = elf::resolve_runtime_closure(app, binding_for(payload)); - ASSERT_EQ(resolution.unresolvedSonames.size(), 1u); - EXPECT_EQ(resolution.unresolvedSonames.front(), "libdeep.so"); -} + expect(fatal(eq(resolution.unresolvedSonames.size(), 1u))); + expect(eq(resolution.unresolvedSonames.front(), "libdeep.so")); +}}; -TEST(RuntimePhysics, RuleBRejectsInterpreterAndLibcFromDifferentPayloads) { +UTest rule_b_rejects_interpreter_and_libc_from_different_payloads{"RuntimePhysics/RuleBRejectsInterpreterAndLibcFromDifferentPayloads", [] { if constexpr (!mcpplibs::os::is_linux) - GTEST_SKIP() << "ELF/glibc runtime physics only apply on Linux"; + skip("ELF/glibc runtime physics only apply on Linux"); Tmp t; auto b = binding_for(t.path / "store"); elf::RuntimeResolution r; @@ -380,13 +377,13 @@ TEST(RuntimePhysics, RuleBRejectsInterpreterAndLibcFromDifferentPayloads) { r.objects.push_back(facts(r.artifact.resolvedLibc, {}, {"GLIBC_2.39"})); auto verdict = elf::validate_runtime_artifact(r.artifact.artifact, b, r); - EXPECT_EQ(verdict.status, elf::RuntimeVerdict::Status::ProvenMismatch); - EXPECT_NE(verdict.explain().find("rule B"), std::string::npos); -} + expect(eq(verdict.status, elf::RuntimeVerdict::Status::ProvenMismatch)); + expect(ne(verdict.explain().find("rule B"), std::string::npos)); +}}; -TEST(RuntimePhysics, RuleBAcceptsSameSelectedPayload) { +UTest rule_b_accepts_same_selected_payload{"RuntimePhysics/RuleBAcceptsSameSelectedPayload", [] { if constexpr (!mcpplibs::os::is_linux) - GTEST_SKIP() << "ELF/glibc runtime physics only apply on Linux"; + skip("ELF/glibc runtime physics only apply on Linux"); Tmp t; auto b = binding_for(t.path / "store"); elf::RuntimeResolution r; @@ -397,12 +394,12 @@ TEST(RuntimePhysics, RuleBAcceptsSameSelectedPayload) { r.objects.push_back(facts(r.artifact.resolvedLibc, {}, {"GLIBC_2.39", "GLIBC_2.44"})); auto verdict = elf::validate_runtime_artifact(r.artifact.artifact, b, r); - EXPECT_EQ(verdict.status, elf::RuntimeVerdict::Status::Pass); -} + expect(eq(verdict.status, elf::RuntimeVerdict::Status::Pass)); +}}; -TEST(RuntimePhysics, RuleBRejectsTwoLibcsAcrossTheResolvedClosure) { +UTest rule_b_rejects_two_libcs_across_the_resolved_closure{"RuntimePhysics/RuleBRejectsTwoLibcsAcrossTheResolvedClosure", [] { if constexpr (!mcpplibs::os::is_linux) - GTEST_SKIP() << "ELF/glibc runtime physics only apply on Linux"; + skip("ELF/glibc runtime physics only apply on Linux"); Tmp t; auto b = binding_for(t.path / "store"); elf::RuntimeResolution r; @@ -416,14 +413,13 @@ TEST(RuntimePhysics, RuleBRejectsTwoLibcsAcrossTheResolvedClosure) { r.objects.push_back(facts(r.artifact.resolvedLibc, {}, {"GLIBC_2.44"})); auto verdict = elf::validate_runtime_artifact(r.artifact.artifact, b, r); - EXPECT_EQ(verdict.status, elf::RuntimeVerdict::Status::ProvenMismatch); - EXPECT_NE(verdict.explain().find("more than one libc payload"), - std::string::npos); -} + expect(eq(verdict.status, elf::RuntimeVerdict::Status::ProvenMismatch)); + expect(ne(verdict.explain().find("more than one libc payload"), std::string::npos)); +}}; -TEST(RuntimePhysics, RuleARejectsRequiredFloorAboveSelectedLibcExports) { +UTest rule_a_rejects_required_floor_above_selected_libc_exports{"RuntimePhysics/RuleARejectsRequiredFloorAboveSelectedLibcExports", [] { if constexpr (!mcpplibs::os::is_linux) - GTEST_SKIP() << "ELF/glibc runtime physics only apply on Linux"; + skip("ELF/glibc runtime physics only apply on Linux"); Tmp t; auto b = binding_for(t.path / "store", "2.39"); elf::RuntimeResolution r; @@ -435,14 +431,14 @@ TEST(RuntimePhysics, RuleARejectsRequiredFloorAboveSelectedLibcExports) { r.objects.push_back(facts("/lib64/libtinfo.so.6", {"GLIBC_2.42"})); auto verdict = elf::validate_runtime_artifact(r.artifact.artifact, b, r); - EXPECT_EQ(verdict.status, elf::RuntimeVerdict::Status::ProvenMismatch); - EXPECT_NE(verdict.explain().find("rule A"), std::string::npos); - EXPECT_NE(verdict.explain().find("libtinfo.so.6"), std::string::npos); -} + expect(eq(verdict.status, elf::RuntimeVerdict::Status::ProvenMismatch)); + expect(ne(verdict.explain().find("rule A"), std::string::npos)); + expect(ne(verdict.explain().find("libtinfo.so.6"), std::string::npos)); +}}; -TEST(RuntimePhysics, RuleAAcceptsEqualOrLowerFloor) { +UTest rule_a_accepts_equal_or_lower_floor{"RuntimePhysics/RuleAAcceptsEqualOrLowerFloor", [] { if constexpr (!mcpplibs::os::is_linux) - GTEST_SKIP() << "ELF/glibc runtime physics only apply on Linux"; + skip("ELF/glibc runtime physics only apply on Linux"); Tmp t; auto b = binding_for(t.path / "store"); elf::RuntimeResolution r; @@ -454,8 +450,8 @@ TEST(RuntimePhysics, RuleAAcceptsEqualOrLowerFloor) { r.objects.push_back(facts("/host/lib.so", {"GLIBC_2.39", "GLIBC_2.44"})); auto verdict = elf::validate_runtime_artifact(r.artifact.artifact, b, r); - EXPECT_EQ(verdict.status, elf::RuntimeVerdict::Status::Pass); -} + expect(eq(verdict.status, elf::RuntimeVerdict::Status::Pass)); +}}; // UNDER A HERMETIC BINDING, "not found" IS A MEASUREMENT. // @@ -465,12 +461,12 @@ TEST(RuntimePhysics, RuleAAcceptsEqualOrLowerFloor) { // the loader will give, and the program cannot start. Filing that under // `inconclusive` reports a proven failure as an absence of one, and it is how a // GL program that exited 127 was shipped as `validation: pass`. -TEST(RuntimePhysics, UnresolvedNeededUnderHermeticBindingIsProven) { +UTest unresolved_needed_under_hermetic_binding_is_proven{"RuntimePhysics/UnresolvedNeededUnderHermeticBindingIsProven", [] { if constexpr (!mcpplibs::os::is_linux) - GTEST_SKIP() << "ELF/glibc runtime physics only apply on Linux"; + skip("ELF/glibc runtime physics only apply on Linux"); Tmp t; auto b = binding_for(t.path / "store"); - ASSERT_TRUE(b.hermetic()) << "this test's premise is a private loader"; + expect(fatal(bool(b.hermetic()))) << "this test's premise is a private loader"; elf::RuntimeResolution r; r.artifactIsElf = true; r.artifact = facts(t.path / "app"); @@ -480,11 +476,11 @@ TEST(RuntimePhysics, UnresolvedNeededUnderHermeticBindingIsProven) { r.unresolvedSonames = {"libgpu-driver.so"}; // a NEEDED nothing provides auto verdict = elf::validate_runtime_artifact(r.artifact.artifact, b, r); - EXPECT_EQ(verdict.status, elf::RuntimeVerdict::Status::Unresolvable); - EXPECT_TRUE(verdict.blocking()) + expect(eq(verdict.status, elf::RuntimeVerdict::Status::Unresolvable)); + expect(bool(verdict.blocking())) << "a proven-unstartable artifact must fail the build, not warn"; - EXPECT_NE(verdict.explain().find("libgpu-driver.so"), std::string::npos); -} + expect(ne(verdict.explain().find("libgpu-driver.so"), std::string::npos)); +}}; // `allow_host_libs` opts out of BOTH phases, or it means two different things. // @@ -494,9 +490,9 @@ TEST(RuntimePhysics, UnresolvedNeededUnderHermeticBindingIsProven) { // the library is installed somewhere the private loader does look. It reports; // it does not block. (Without this, e2e 206's deliberate host-DSO control // failed its own build.) -TEST(RuntimePhysics, AllowHostLibsDowngradesTheProofToAReport) { +UTest allow_host_libs_downgrades_the_proof_to_a_report{"RuntimePhysics/AllowHostLibsDowngradesTheProofToAReport", [] { if constexpr (!mcpplibs::os::is_linux) - GTEST_SKIP() << "ELF/glibc runtime physics only apply on Linux"; + skip("ELF/glibc runtime physics only apply on Linux"); Tmp t; auto b = binding_for(t.path / "store"); elf::RuntimeResolution r; @@ -509,17 +505,17 @@ TEST(RuntimePhysics, AllowHostLibsDowngradesTheProofToAReport) { auto blocked = elf::validate_runtime_artifact(r.artifact.artifact, b, r, /*hostLibsAllowed=*/false); - EXPECT_EQ(blocked.status, elf::RuntimeVerdict::Status::Unresolvable); - EXPECT_TRUE(blocked.blocking()); + expect(eq(blocked.status, elf::RuntimeVerdict::Status::Unresolvable)); + expect(bool(blocked.blocking())); auto reported = elf::validate_runtime_artifact(r.artifact.artifact, b, r, /*hostLibsAllowed=*/true); - EXPECT_EQ(reported.status, elf::RuntimeVerdict::Status::Inconclusive); - EXPECT_FALSE(reported.blocking()); - EXPECT_NE(reported.explain().find("libtinfo.so.6"), std::string::npos); - EXPECT_NE(reported.explain().find("allow_host_libs"), std::string::npos) + expect(eq(reported.status, elf::RuntimeVerdict::Status::Inconclusive)); + expect(!reported.blocking()); + expect(ne(reported.explain().find("libtinfo.so.6"), std::string::npos)); + expect(ne(reported.explain().find("allow_host_libs"), std::string::npos)) << "a downgraded verdict must say what downgraded it"; -} +}}; // THE ARTIFACT'S FORMAT DECIDES, NOT THE BINDING'S. // @@ -527,12 +523,12 @@ TEST(RuntimePhysics, AllowHostLibsDowngradesTheProofToAReport) { // private loader, therefore hermetic — while producing a PE. The parse failure // ("not an ELF file") landed in `unresolved`, and the artifact was failed for a // missing shared library it could not possibly have. CI caught it; this pins it. -TEST(RuntimePhysics, CrossBuiltNonElfArtifactIsNotJudgedByElfRules) { +UTest cross_built_non_elf_artifact_is_not_judged_by_elf_rules{"RuntimePhysics/CrossBuiltNonElfArtifactIsNotJudgedByElfRules", [] { if constexpr (!mcpplibs::os::is_linux) - GTEST_SKIP() << "ELF/glibc runtime physics only apply on Linux"; + skip("ELF/glibc runtime physics only apply on Linux"); Tmp t; auto b = binding_for(t.path / "store"); - ASSERT_TRUE(b.hermetic()); + expect(fatal(bool(b.hermetic()))); elf::RuntimeResolution r; r.artifactIsElf = true; r.artifact = facts(t.path / "crosswin.exe"); @@ -541,18 +537,18 @@ TEST(RuntimePhysics, CrossBuiltNonElfArtifactIsNotJudgedByElfRules) { // and crucially NO unresolvedSonames — nothing was ever looked for. auto verdict = elf::validate_runtime_artifact(r.artifact.artifact, b, r); - EXPECT_EQ(verdict.status, elf::RuntimeVerdict::Status::Pass); - EXPECT_FALSE(verdict.blocking()); - EXPECT_NE(verdict.explain().find("not ELF"), std::string::npos); -} + expect(eq(verdict.status, elf::RuntimeVerdict::Status::Pass)); + expect(!verdict.blocking()); + expect(ne(verdict.explain().find("not ELF"), std::string::npos)); +}}; // The narrower half of the same conflation. Even on a genuine ELF, "I could not // read one of the objects" or "I stopped after 512" are statements about the // CHECK — a check that could not look has proven nothing, so it stays // inconclusive no matter how hermetic the binding is. -TEST(RuntimePhysics, UnreadableObjectIsInconclusiveEvenWhenHermetic) { +UTest unreadable_object_is_inconclusive_even_when_hermetic{"RuntimePhysics/UnreadableObjectIsInconclusiveEvenWhenHermetic", [] { if constexpr (!mcpplibs::os::is_linux) - GTEST_SKIP() << "ELF/glibc runtime physics only apply on Linux"; + skip("ELF/glibc runtime physics only apply on Linux"); Tmp t; auto b = binding_for(t.path / "store"); elf::RuntimeResolution r; @@ -564,21 +560,21 @@ TEST(RuntimePhysics, UnreadableObjectIsInconclusiveEvenWhenHermetic) { // no unresolvedSonames: it WAS found, it just could not be parsed auto verdict = elf::validate_runtime_artifact(r.artifact.artifact, b, r); - EXPECT_EQ(verdict.status, elf::RuntimeVerdict::Status::Inconclusive); - EXPECT_FALSE(verdict.blocking()); -} + expect(eq(verdict.status, elf::RuntimeVerdict::Status::Inconclusive)); + expect(!verdict.blocking()); +}}; // The other side of the same line. Without a private loader the artifact runs // under the HOST's, which also consults `ld.so.cache` — something mcpp // deliberately does not parse. There, unresolved really is unknown, and // claiming otherwise would fail builds that work. -TEST(RuntimePhysics, UnresolvedNeededWithoutAPrivateLoaderStaysInconclusive) { +UTest unresolved_needed_without_a_private_loader_stays_inconclusive{"RuntimePhysics/UnresolvedNeededWithoutAPrivateLoaderStaysInconclusive", [] { if constexpr (!mcpplibs::os::is_linux) - GTEST_SKIP() << "ELF/glibc runtime physics only apply on Linux"; + skip("ELF/glibc runtime physics only apply on Linux"); Tmp t; auto b = binding_for(t.path / "store"); b.loader.reset(); // host runtime: gcc@system and friends - ASSERT_FALSE(b.hermetic()); + expect(fatal(!b.hermetic())); elf::RuntimeResolution r; r.artifactIsElf = true; r.artifact = facts(t.path / "app"); @@ -586,18 +582,18 @@ TEST(RuntimePhysics, UnresolvedNeededWithoutAPrivateLoaderStaysInconclusive) { r.unresolved = {"libgpu-driver.so"}; auto verdict = elf::validate_runtime_artifact(r.artifact.artifact, b, r); - EXPECT_EQ(verdict.status, elf::RuntimeVerdict::Status::Inconclusive); - EXPECT_FALSE(verdict.blocking()); - EXPECT_NE(verdict.explain().find("libgpu-driver.so"), std::string::npos); -} + expect(eq(verdict.status, elf::RuntimeVerdict::Status::Inconclusive)); + expect(!verdict.blocking()); + expect(ne(verdict.explain().find("libgpu-driver.so"), std::string::npos)); +}}; // An undeclared SubOS on Linux is not "some other runtime" — it is a runtime // nobody described, and the two must not print the same sentence. Reporting the // second as the first sends the reader looking for a runtime they did not // select, and quietly counts "unknown" as "fine". -TEST(RuntimePhysics, UndeclaredBindingIsInconclusiveNotNotApplicable) { +UTest undeclared_binding_is_inconclusive_not_not_applicable{"RuntimePhysics/UndeclaredBindingIsInconclusiveNotNotApplicable", [] { if constexpr (!mcpplibs::os::is_linux) - GTEST_SKIP() << "ELF/glibc runtime physics only apply on Linux"; + skip("ELF/glibc runtime physics only apply on Linux"); Tmp t; auto b = binding_for(t.path / "store"); b.declared = false; @@ -608,14 +604,13 @@ TEST(RuntimePhysics, UndeclaredBindingIsInconclusiveNotNotApplicable) { r.artifact = facts(t.path / "app"); auto verdict = elf::validate_runtime_artifact(r.artifact.artifact, b, r); - EXPECT_EQ(verdict.status, elf::RuntimeVerdict::Status::Inconclusive); - EXPECT_NE(verdict.explain().find("does not describe its runtime"), - std::string::npos); -} + expect(eq(verdict.status, elf::RuntimeVerdict::Status::Inconclusive)); + expect(ne(verdict.explain().find("does not describe its runtime"), std::string::npos)); +}}; -TEST(RuntimePhysics, NonLinuxValidatorIsATypedNoop) { +UTest non_linux_validator_is_a_typed_noop{"RuntimePhysics/NonLinuxValidatorIsATypedNoop", [] { if constexpr (mcpplibs::os::is_linux) - GTEST_SKIP() << "the non-Linux boundary is exercised on native runners"; + skip("the non-Linux boundary is exercised on native runners"); Tmp t; auto b = binding_for(t.path / "store"); elf::RuntimeResolution r; @@ -625,9 +620,9 @@ TEST(RuntimePhysics, NonLinuxValidatorIsATypedNoop) { r.unresolved = {"libgpu-driver.so"}; auto verdict = elf::validate_runtime_artifact(r.artifact.artifact, b, r); - EXPECT_EQ(verdict.status, elf::RuntimeVerdict::Status::Pass); - EXPECT_NE(verdict.explain().find("non-Linux"), std::string::npos); -} + expect(eq(verdict.status, elf::RuntimeVerdict::Status::Pass)); + expect(ne(verdict.explain().find("non-Linux"), std::string::npos)); +}}; } // namespace @@ -638,46 +633,46 @@ TEST(RuntimePhysics, NonLinuxValidatorIsATypedNoop) { // the RULE is right about what a linker actually produces — and the rule was // wrong twice before it was measured. -TEST(ElfRuntime, ReadsThisBinarysOwnDynamicSymbols) { +UTest reads_this_binarys_own_dynamic_symbols{"ElfRuntime/ReadsThisBinarysOwnDynamicSymbols", [] { if constexpr (!mcpplibs::os::is_linux) - GTEST_SKIP() << "ELF dynamic symbol tables only exist on Linux here"; + skip("ELF dynamic symbol tables only exist on Linux here"); auto self = std::filesystem::path("/proc/self/exe"); - if (!std::filesystem::exists(self)) GTEST_SKIP() << "no /proc/self/exe"; + if (!std::filesystem::exists(self)) skip("no /proc/self/exe"); auto symbols = elf::inspect_dynamic_symbols(self); - ASSERT_TRUE(symbols.has_value()) << symbols.error(); + if (auto&& failed = expect(fatal(symbols.has_value())); !failed) failed << symbols.error(); if (!symbols->present) - GTEST_SKIP() << "this test binary is statically linked"; + skip("this test binary is statically linked"); // The DENOMINATOR has to be real. `exported = 0` out of an unread table // is the reading this whole area exists to make impossible. - EXPECT_GT(symbols->total, 0u); - EXPECT_TRUE(symbols->copyRelocationsKnown) + expect(gt(symbols->total, 0u)); + expect(bool(symbols->copyRelocationsKnown)) << "x86_64/aarch64/riscv64 must all be in the COPY relocation table"; -} +}}; -TEST(ElfRuntime, ThisBinaryExportsNothingOfItsOwn) { +UTest this_binary_exports_nothing_of_its_own{"ElfRuntime/ThisBinaryExportsNothingOfItsOwn", [] { if constexpr (!mcpplibs::os::is_linux) - GTEST_SKIP() << "ELF dynamic symbol tables only exist on Linux here"; + skip("ELF dynamic symbol tables only exist on Linux here"); auto self = std::filesystem::path("/proc/self/exe"); - if (!std::filesystem::exists(self)) GTEST_SKIP() << "no /proc/self/exe"; + if (!std::filesystem::exists(self)) skip("no /proc/self/exe"); auto facts = elf::inspect_elf_runtime(self); - ASSERT_TRUE(facts.has_value()) << facts.error(); + if (auto&& failed = expect(fatal(facts.has_value())); !failed) failed << facts.error(); // PT_INTERP, not the ELF type: a PIE executable is ET_DYN exactly like // a shared library, and whether this binary is PIE is the payload // compiler's default rather than mcpp's decision. if (facts->interp.empty()) - GTEST_SKIP() << "statically linked: no loader, so no flat namespace"; + skip("statically linked: no loader, so no flat namespace"); auto symbols = elf::inspect_dynamic_symbols(self); - ASSERT_TRUE(symbols.has_value()) << symbols.error(); - ASSERT_TRUE(symbols->present); + if (auto&& failed = expect(fatal(symbols.has_value())); !failed) failed << symbols.error(); + expect(fatal(bool(symbols->present))); auto exported = mcpp::build::symbol_provision::exported_definitions(*symbols); - ASSERT_TRUE(exported.has_value()); + expect(fatal(exported.has_value())); // Measured on every mcpp binary: the only defined entries are glibc's // copy relocations (environ, __environ, stdout, stderr, @@ -686,14 +681,13 @@ TEST(ElfRuntime, ThisBinaryExportsNothingOfItsOwn) { // name-keyed filter reports it in every executable ever built. std::vector names; for (auto const& e : *exported) names.push_back(e.name); - EXPECT_TRUE(names.empty()) - << "an ordinary mcpp-linked test binary must export nothing of its " + if (auto&& failed = expect(names.empty()); !failed) failed << "an ordinary mcpp-linked test binary must export nothing of its " "own; got: " << [&] { std::string s; for (auto const& n : names) { s += n; s += ' '; } return s; }(); -} +}}; // THE SURFACE A DEPENDENCY PUBLISHES FOR dlopen, WHICH NO ARTIFACT WALK REACHES. // @@ -708,9 +702,9 @@ TEST(ElfRuntime, ThisBinaryExportsNothingOfItsOwn) { // legitimately holds host-driver links that dangle on a machine with no // driver, and a check that failed there would turn a correct CPU-only // configuration into a failed build. -TEST(DlopenSurface, SeparatesAPackagingGapFromAMachineWithoutTheDriver) { +UTest separates_a_packaging_gap_from_a_machine_without_the_driver{"DlopenSurface/SeparatesAPackagingGapFromAMachineWithoutTheDriver", [] { if constexpr (!mcpplibs::os::is_linux) - GTEST_SKIP() << "ELF/glibc runtime physics only apply on Linux"; + skip("ELF/glibc runtime physics only apply on Linux"); Tmp t; auto payload = t.path / "store"; auto glibc = payload / "2.44" / "lib64"; @@ -738,11 +732,11 @@ TEST(DlopenSurface, SeparatesAPackagingGapFromAMachineWithoutTheDriver) { // BOTH DENOMINATORS. A surface that failed to build enumerates nothing and // every per-member assertion below then passes. - EXPECT_EQ(report.members, 4u); - EXPECT_EQ(report.walked, 3u) << "the dangling entry is not read, and the " + expect(eq(report.members, 4u)); + expect(eq(report.walked, 3u)) << "the dangling entry is not read, and the " "other three are"; - ASSERT_EQ(report.findings.size(), 2u); + expect(fatal(eq(report.findings.size(), 2u))); // Asserted as FIELDS, not as a substring of a message whose wording is // free to improve. const elf::DlopenSurfaceFinding* gap = nullptr; @@ -751,22 +745,22 @@ TEST(DlopenSurface, SeparatesAPackagingGapFromAMachineWithoutTheDriver) { if (f.soname == "libabsent.so.7") gap = &f; if (f.soname == "libdriver.so.1") machine = &f; } - ASSERT_NE(gap, nullptr); - ASSERT_NE(machine, nullptr); - EXPECT_FALSE(gap->dangling) << "nothing on the surface answers this name"; - EXPECT_EQ(gap->member.filename(), "libgap.so.1"); - EXPECT_TRUE(machine->dangling) + expect(fatal(ne(gap, nullptr))); + expect(fatal(ne(machine, nullptr))); + expect(!gap->dangling) << "nothing on the surface answers this name"; + expect(eq(gap->member.filename(), "libgap.so.1")); + expect(bool(machine->dangling)) << "the surface carries the entry; the target is the machine's answer"; -} +}}; // ONE LIBRARY, NOT ITS TWO NAMES. // // A farm links `libfoo.so.N` and `libfoo.so.N.M.P` to one file. Walking the // entries reports every finding twice and calls thirteen libraries twenty-six, // which is what the first published record did. -TEST(DlopenSurface, CountsALibraryOnceWhenTwoNamesLinkToIt) { +UTest counts_a_library_once_when_two_names_link_to_it{"DlopenSurface/CountsALibraryOnceWhenTwoNamesLinkToIt", [] { if constexpr (!mcpplibs::os::is_linux) - GTEST_SKIP() << "ELF/glibc runtime physics only apply on Linux"; + skip("ELF/glibc runtime physics only apply on Linux"); Tmp t; auto payload = t.path / "store"; auto glibc = payload / "2.44" / "lib64"; @@ -784,9 +778,9 @@ TEST(DlopenSurface, CountsALibraryOnceWhenTwoNamesLinkToIt) { std::vector search{farm, glibc}; auto report = elf::inspect_dlopen_surface(dirs, binding_for(payload), search); - EXPECT_EQ(report.members, 1u); - EXPECT_EQ(report.walked, 1u); - ASSERT_EQ(report.findings.size(), 1u); + expect(eq(report.members, 1u)); + expect(eq(report.walked, 1u)); + expect(fatal(eq(report.findings.size(), 1u))); // The SHORTEST name is kept: it is the one a dlopen asks for. - EXPECT_EQ(report.findings.front().member.filename(), "libtwo.so.1"); -} + expect(eq(report.findings.front().member.filename(), "libtwo.so.1")); +}}; diff --git a/tests/unit/test_fallback_legacy_dirs.cpp b/tests/unit/test_fallback_legacy_dirs.cpp index b38868241..eabed199a 100644 --- a/tests/unit/test_fallback_legacy_dirs.cpp +++ b/tests/unit/test_fallback_legacy_dirs.cpp @@ -1,8 +1,10 @@ -#include import std; +import mcpplibs.mtest; import mcpp.provider.xlings.legacy_dirs; +using namespace mcpplibs::mtest; + // The legacy install-dir scan must not cross namespaces. // // `Fetcher::install_path` contract is "the verdir for THIS package". Its @@ -31,43 +33,43 @@ std::filesystem::path make_xpkgs(std::initializer_list dirs) { } // namespace -TEST(FallbackLegacyDirs, DoesNotReturnAnotherNamespacesDirectory) { +UTest does_not_return_another_namespaces_directory{"FallbackLegacyDirs/DoesNotReturnAnotherNamespacesDirectory", [] { auto base = make_xpkgs({"compat-x-imgui"}); // Asking for ocornut:imgui. Only compat's directory exists, and it is NOT // an answer to this question. auto hit = mcpp::provider::xlings::scan_legacy_install_dirs( base, "ocornut.imgui", "imgui", {"ocornut", "mcpplibs"}); - EXPECT_FALSE(hit.has_value()) + expect(!hit.has_value()) << "returned '" << hit.value_or("") << "' for ocornut:imgui"; std::filesystem::remove_all(base); -} +}}; -TEST(FallbackLegacyDirs, StillFindsOwnNamespaceShortNameLayout) { +UTest still_finds_own_namespace_short_name_layout{"FallbackLegacyDirs/StillFindsOwnNamespaceShortNameLayout", [] { auto base = make_xpkgs({"compat-x-imgui", "ocornut-x-imgui"}); auto hit = mcpp::provider::xlings::scan_legacy_install_dirs( base, "ocornut.imgui", "imgui", {"ocornut", "mcpplibs"}); - ASSERT_TRUE(hit.has_value()); - EXPECT_EQ(*hit, "ocornut-x-imgui"); + expect(fatal(hit.has_value())); + expect(eq(*hit, "ocornut-x-imgui")); std::filesystem::remove_all(base); -} +}}; -TEST(FallbackLegacyDirs, StillFindsIndexPrefixedLegacyLayout) { +UTest still_finds_index_prefixed_legacy_layout{"FallbackLegacyDirs/StillFindsIndexPrefixedLegacyLayout", [] { // The old layout this arm exists for: -x-. auto base = make_xpkgs({"mcpplibs-x-tinyhttps"}); auto hit = mcpp::provider::xlings::scan_legacy_install_dirs( base, "mcpplibs.tinyhttps", "tinyhttps", {"mcpplibs", "mcpplibs"}); - ASSERT_TRUE(hit.has_value()); - EXPECT_EQ(*hit, "mcpplibs-x-tinyhttps"); + expect(fatal(hit.has_value())); + expect(eq(*hit, "mcpplibs-x-tinyhttps")); std::filesystem::remove_all(base); -} +}}; -TEST(FallbackLegacyDirs, QualifiedSuffixNeedsNoPrefixAllowance) { +UTest qualified_suffix_needs_no_prefix_allowance{"FallbackLegacyDirs/QualifiedSuffixNeedsNoPrefixAllowance", [] { // `-x-.` carries the namespace in the suffix itself, so it is // accepted whatever the prefix is — that is the legacy FQN layout. auto base = make_xpkgs({"someindex-x-ocornut.imgui"}); auto hit = mcpp::provider::xlings::scan_legacy_install_dirs( base, "ocornut.imgui", "imgui", {"ocornut"}); - ASSERT_TRUE(hit.has_value()); - EXPECT_EQ(*hit, "someindex-x-ocornut.imgui"); + expect(fatal(hit.has_value())); + expect(eq(*hit, "someindex-x-ocornut.imgui")); std::filesystem::remove_all(base); -} +}}; diff --git a/tests/unit/test_fetcher_install_path.cpp b/tests/unit/test_fetcher_install_path.cpp index 435d2bc9a..5e9a7d65f 100644 --- a/tests/unit/test_fetcher_install_path.cpp +++ b/tests/unit/test_fetcher_install_path.cpp @@ -3,12 +3,14 @@ // provider home's, which holds what was installed before that index had a // store of its own. -#include import std; +import mcpplibs.mtest; import mcpp.config; import mcpp.pm.package_fetcher; +using namespace mcpplibs::mtest; + namespace { struct TempDir { @@ -34,27 +36,27 @@ mcpp::config::GlobalConfig home_at(const std::filesystem::path& home) { return cfg; } -TEST(FetcherInstallPath, APackageIsFoundInItsIndexStore) { +UTest a_package_is_found_in_its_index_store{"FetcherInstallPath/APackageIsFoundInItsIndexStore", [] { TempDir temp; auto cfg = home_at(temp.path); const auto verdir = temp.path / "xpkgs" / "compat-x-gadget" / "1.0.0"; std::filesystem::create_directories(verdir); mcpp::pm::Fetcher f(cfg); const auto found = f.install_path("compat", "gadget", "1.0.0"); - ASSERT_TRUE(found); - EXPECT_EQ(found->lexically_normal(), verdir.lexically_normal()); -} + expect(fatal(bool(found))); + expect(eq(found->lexically_normal(), verdir.lexically_normal())); +}}; -TEST(FetcherInstallPath, APackageFromBeforeIsFoundInTheHomesStore) { +UTest a_package_from_before_is_found_in_the_homes_store{"FetcherInstallPath/APackageFromBeforeIsFoundInTheHomesStore", [] { TempDir temp; auto cfg = home_at(temp.path); const auto verdir = cfg.registryDir / "data" / "xpkgs" / "compat-x-gadget" / "1.0.0"; std::filesystem::create_directories(verdir); mcpp::pm::Fetcher f(cfg); const auto found = f.install_path("compat", "gadget", "1.0.0"); - ASSERT_TRUE(found); - EXPECT_EQ(found->lexically_normal(), verdir.lexically_normal()); - EXPECT_FALSE(f.install_path("compat", "gadget", "2.0.0")); -} + expect(fatal(bool(found))); + expect(eq(found->lexically_normal(), verdir.lexically_normal())); + expect(!f.install_path("compat", "gadget", "2.0.0")); +}}; } // namespace diff --git a/tests/unit/test_fingerprint.cpp b/tests/unit/test_fingerprint.cpp index 70e5e0eda..d64ae2aa8 100644 --- a/tests/unit/test_fingerprint.cpp +++ b/tests/unit/test_fingerprint.cpp @@ -1,9 +1,11 @@ -#include import std; +import mcpplibs.mtest; import mcpp.toolchain.detect; import mcpp.toolchain.fingerprint; +using namespace mcpplibs::mtest; + using namespace mcpp::toolchain; namespace { @@ -34,7 +36,7 @@ FingerprintInputs baseline() { // interpreter cannot provide, and nothing along the way said anything. This // is the prerequisite for building the same project under two subos, not a // refinement of it. -TEST(Fingerprint, RuntimeBindingIsPartOfTheIdentity) { +UTest runtime_binding_is_part_of_the_identity{"Fingerprint/RuntimeBindingIsPartOfTheIdentity", [] { mcpp::toolchain::FingerprintInputs a; a.toolchain.compiler = mcpp::toolchain::CompilerId::GCC; a.toolchain.version = "16.1.0"; @@ -44,89 +46,86 @@ TEST(Fingerprint, RuntimeBindingIsPartOfTheIdentity) { auto b = a; b.toolchain.runtimeBinding = "glibc@2.44"; - EXPECT_NE(mcpp::toolchain::compute_fingerprint(a).hex, - mcpp::toolchain::compute_fingerprint(b).hex) + expect(ne(mcpp::toolchain::compute_fingerprint(a).hex, mcpp::toolchain::compute_fingerprint(b).hex)) << "same toolchain, same triple, different libc — sharing target// " "would replay objects compiled against the other one"; -} +}}; // ...and an unset binding is its own identity, not a collision with any // particular version. -TEST(Fingerprint, EmptyRuntimeBindingIsDistinct) { +UTest empty_runtime_binding_is_distinct{"Fingerprint/EmptyRuntimeBindingIsDistinct", [] { mcpp::toolchain::FingerprintInputs a; a.toolchain.targetTriple = "x86_64-linux-gnu"; a.toolchain.runtimeBinding = "glibc@2.39"; auto b = a; b.toolchain.runtimeBinding.clear(); - EXPECT_NE(mcpp::toolchain::compute_fingerprint(a).hex, - mcpp::toolchain::compute_fingerprint(b).hex); -} + expect(ne(mcpp::toolchain::compute_fingerprint(a).hex, mcpp::toolchain::compute_fingerprint(b).hex)); +}}; -TEST(Fingerprint, FullRuntimeContractHashOverridesTheLibcLabel) { +UTest full_runtime_contract_hash_overrides_the_libc_label{"Fingerprint/FullRuntimeContractHashOverridesTheLibcLabel", [] { mcpp::toolchain::FingerprintInputs a; a.toolchain.targetTriple = "x86_64-linux-gnu"; a.toolchain.runtimeBinding = "glibc@2.39"; a.toolchain.runtimeContractHash = "subos-el8-contract"; auto b = a; b.toolchain.runtimeContractHash = "subos-dev-contract"; - EXPECT_NE(mcpp::toolchain::compute_fingerprint(a).hex, - mcpp::toolchain::compute_fingerprint(b).hex); -} + expect(ne(mcpp::toolchain::compute_fingerprint(a).hex, mcpp::toolchain::compute_fingerprint(b).hex)); +}}; } // namespace -TEST(Fingerprint, DeterministicForSameInputs) { +UTest deterministic_for_same_inputs{"Fingerprint/DeterministicForSameInputs", [] { auto a = compute_fingerprint(baseline()); auto b = compute_fingerprint(baseline()); - EXPECT_EQ(a.hex, b.hex); -} + expect(eq(a.hex, b.hex)); +}}; -TEST(Fingerprint, ProducesSixteenHexChars) { +UTest produces_sixteen_hex_chars{"Fingerprint/ProducesSixteenHexChars", [] { auto fp = compute_fingerprint(baseline()); - EXPECT_EQ(fp.hex.size(), 16u); + expect(eq(fp.hex.size(), 16u)); for (char c : fp.hex) { - EXPECT_TRUE((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')); + expect((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')); } -} - -#define EXPECT_DIFFERENT(field_mutation) do { \ - auto base = baseline(); \ - auto bfp = compute_fingerprint(base); \ - auto in = baseline(); \ - field_mutation; \ - auto fp = compute_fingerprint(in); \ - EXPECT_NE(bfp.hex, fp.hex) \ - << "Mutation '" #field_mutation "' did NOT change fingerprint"; \ - } while (0) - -TEST(Fingerprint, AllTenFieldsAffectHash) { - EXPECT_DIFFERENT(in.toolchain.compiler = CompilerId::Clang); - EXPECT_DIFFERENT(in.toolchain.version = "16.0.0"); - EXPECT_DIFFERENT(in.toolchain.driverIdent = "g++ (xim-x-gcc 15.1.0) 15.1.0"); - EXPECT_DIFFERENT(in.toolchain.targetTriple = "aarch64-linux-gnu"); - EXPECT_DIFFERENT(in.toolchain.stdlibId = "libc++"); - EXPECT_DIFFERENT(in.cppStandard = "c++26"); - // c++20 gets its own target dir for the same reason c++26 does — cross-level - // BMI reuse is rejected by the compiler itself. - EXPECT_DIFFERENT(in.cppStandard = "c++20"); - EXPECT_DIFFERENT(in.compileFlags = "-O3"); - // mcpp version is hardcoded inside compute_fingerprint, can't mutate from here. - EXPECT_DIFFERENT(in.dependencyLockHash = ""); - EXPECT_DIFFERENT(in.stdBmiHash = "ffffffffffffffff"); -} +}}; + +// Each field, changed alone from the baseline, changes the hash. +UTest all_ten_fields_affect_hash{"Fingerprint/AllTenFieldsAffectHash", [] { + using Mutation = std::pair>; + const Mutation mutations[] = { + {"toolchain.compiler", [](auto& in) { in.toolchain.compiler = CompilerId::Clang; }}, + {"toolchain.version", [](auto& in) { in.toolchain.version = "16.0.0"; }}, + {"toolchain.driverIdent", [](auto& in) { in.toolchain.driverIdent = "g++ (xim-x-gcc 15.1.0) 15.1.0"; }}, + {"toolchain.targetTriple", [](auto& in) { in.toolchain.targetTriple = "aarch64-linux-gnu"; }}, + {"toolchain.stdlibId", [](auto& in) { in.toolchain.stdlibId = "libc++"; }}, + {"cppStandard c++26", [](auto& in) { in.cppStandard = "c++26"; }}, + // c++20 gets its own target dir for the same reason c++26 does — cross-level + // BMI reuse is rejected by the compiler itself. + {"cppStandard c++20", [](auto& in) { in.cppStandard = "c++20"; }}, + {"compileFlags", [](auto& in) { in.compileFlags = "-O3"; }}, + // mcpp version is hardcoded inside compute_fingerprint, can't mutate from here. + {"dependencyLockHash", [](auto& in) { in.dependencyLockHash = ""; }}, + {"stdBmiHash", [](auto& in) { in.stdBmiHash = "ffffffffffffffff"; }}, + }; + const auto base = compute_fingerprint(baseline()); + for (auto const& [field, mutate] : mutations) { + auto in = baseline(); + mutate(in); + expect(ne(base.hex, compute_fingerprint(in).hex)) << "changing " << field << " did not change the fingerprint"; + } +}}; -TEST(Fingerprint, StableAcrossBinaryPathsWhenDriverIdentMatches) { +UTest stable_across_binary_paths_when_driver_ident_matches{"Fingerprint/StableAcrossBinaryPathsWhenDriverIdentMatches", [] { auto a = baseline(); auto b = baseline(); a.toolchain.binaryPath = "/home/speak/.mcpp/registry/data/xpkgs/xim-x-gcc/16.1.0/bin/g++"; b.toolchain.binaryPath = "/home/speak/.xlings/data/xpkgs/xim-x-mcpp/0.0.14/registry/data/xpkgs/xim-x-gcc/16.1.0/bin/g++"; - EXPECT_EQ(compute_fingerprint(a).hex, compute_fingerprint(b).hex); -} + expect(eq(compute_fingerprint(a).hex, compute_fingerprint(b).hex)); +}}; -TEST(Fingerprint, HashStringMatchesHashFile) { +UTest hash_string_matches_hash_file{"Fingerprint/HashStringMatchesHashFile", [] { auto h1 = hash_string("hello"); auto h2 = hash_string("hello"); - EXPECT_EQ(h1, h2); - EXPECT_NE(hash_string("hello"), hash_string("hellp")); -} + expect(eq(h1, h2)); + expect(ne(hash_string("hello"), hash_string("hellp"))); +}}; diff --git a/tests/unit/test_freestanding.cpp b/tests/unit/test_freestanding.cpp index f87bfe792..54198337a 100644 --- a/tests/unit/test_freestanding.cpp +++ b/tests/unit/test_freestanding.cpp @@ -1,6 +1,7 @@ -#include import std; +import mcpplibs.mtest; +import mcpplibs.os.process; import mcpp.freestanding.target; import mcpp.freestanding.linkline; import mcpp.freestanding.runner; @@ -8,70 +9,72 @@ import mcpp.toolchain.triple; import mcpp.provider.xlings; import mcpp.build.build_program; +using namespace mcpplibs::mtest; + using namespace mcpp::freestanding; // ── the target table ──────────────────────────────────────────────────────── -TEST(FreestandingTarget, ResolvesTheKnownBareMetalTriples) { +UTest resolves_the_known_bare_metal_triples{"FreestandingTarget/ResolvesTheKnownBareMetalTriples", [] { auto rv64 = resolve("riscv64-none-elf"); - ASSERT_TRUE(rv64.has_value()); - EXPECT_EQ(rv64->march, "rv64gc"); - EXPECT_EQ(rv64->mabi, "lp64d"); - EXPECT_EQ(rv64->libdir, "rv64gc/lp64d"); + expect(fatal(rv64.has_value())); + expect(eq(rv64->march, "rv64gc")); + expect(eq(rv64->mabi, "lp64d")); + expect(eq(rv64->libdir, "rv64gc/lp64d")); auto rv32 = resolve("riscv32-none-elf"); - ASSERT_TRUE(rv32.has_value()); - EXPECT_EQ(rv32->march, "rv32imac"); - EXPECT_EQ(rv32->mabi, "ilp32"); - EXPECT_EQ(rv32->libdir, "rv32imac/ilp32"); -} + expect(fatal(rv32.has_value())); + expect(eq(rv32->march, "rv32imac")); + expect(eq(rv32->mabi, "ilp32")); + expect(eq(rv32->libdir, "rv32imac/ilp32")); +}}; -TEST(FreestandingTarget, HostedTriplesResolveToNothing) { +UTest hosted_triples_resolve_to_nothing{"FreestandingTarget/HostedTriplesResolveToNothing", [] { // The same call answers "is this a target I know how to build // freestanding", so a hosted triple must not produce a Spec — a caller // that got one would put -ffreestanding on an ordinary Linux build. for (const char* s : { "x86_64-linux-gnu", "x86_64-linux-musl", "aarch64-macos", "x86_64-windows-gnu" }) { - EXPECT_FALSE(resolve(s).has_value()) << s; + expect(!(resolve(s).has_value())) << s; } -} +}}; -TEST(FreestandingTarget, UnknownBareMetalTripleIsNotInvented) { +UTest unknown_bare_metal_triple_is_not_invented{"FreestandingTarget/UnknownBareMetalTripleIsNotInvented", [] { // Parses as freestanding, but mcpp has no ISA profile for it. Returning a // Spec here would mean guessing an -march, and a wrong -march produces an // object file that links and then executes garbage. auto t = mcpp::toolchain::triple::parse("arm-none-eabi"); - ASSERT_TRUE(t.has_value()); - EXPECT_TRUE(t->is_freestanding()); - EXPECT_FALSE(resolve(*t).has_value()); -} + expect(fatal(t.has_value())); + expect(t->is_freestanding()); + expect(!(resolve(*t).has_value())); +}}; -TEST(FreestandingTarget, CompileFlagsCarryTheIsaAndFreestanding) { +UTest compile_flags_carry_the_isa_and_freestanding{"FreestandingTarget/CompileFlagsCarryTheIsaAndFreestanding", [] { auto s = resolve("riscv64-none-elf"); - ASSERT_TRUE(s.has_value()); + expect(fatal(s.has_value())); auto fl = compile_flags(*s); auto has = [&](std::string_view f) { return std::ranges::find(fl, f) != fl.end(); }; - EXPECT_TRUE(has("-march=rv64gc")); - EXPECT_TRUE(has("-mabi=lp64d")); - EXPECT_TRUE(has("-mcmodel=medany")); + expect(bool(has("-march=rv64gc"))); + expect(bool(has("-mabi=lp64d"))); + expect(bool(has("-mcmodel=medany"))); // -ffreestanding is a COMPILE flag, not a link one: it changes what the // compiler may assume about the library, and that has to hold for every // TU including a dependency's. - EXPECT_TRUE(has("-ffreestanding")); + expect(bool(has("-ffreestanding"))); // The toolchain's libc++ headers are the HOST's: `#include ` // resolves to libc++'s wrapper, which opens a `__config_site` generated // for the host and absent here. A target-side C++ library reaches a // consumer as a MODULE, never as an include path. - EXPECT_TRUE(has("-nostdinc++")); -} + expect(bool(has("-nostdinc++"))); +}}; // ── the link line ─────────────────────────────────────────────────────────── -TEST(FreestandingLink, CarriesTargetSelectionAndRefusesTheHostedWorld) { +UTest carries_target_selection_and_refuses_the_hosted_world{"FreestandingLink/CarriesTargetSelectionAndRefusesTheHostedWorld", [] { auto s = resolve("riscv64-none-elf"); - ASSERT_TRUE(s.has_value()); + expect(fatal(s.has_value())); LinkInputs in; in.lld = "/payload/bin/ld.lld"; auto line = link_flags(*s, in, [](const std::filesystem::path& p) { @@ -80,15 +83,15 @@ TEST(FreestandingLink, CarriesTargetSelectionAndRefusesTheHostedWorld) { // --target is the load-bearing one: clang is ONE binary for every target, // so without it the link silently produces host objects. - EXPECT_NE(line.find("--target=riscv64-none-elf"), std::string::npos); - EXPECT_NE(line.find("-nostdlib"), std::string::npos); - EXPECT_NE(line.find("-nostartfiles"), std::string::npos); - EXPECT_NE(line.find("-static"), std::string::npos); -} + expect(ne(line.find("--target=riscv64-none-elf"), std::string::npos)); + expect(ne(line.find("-nostdlib"), std::string::npos)); + expect(ne(line.find("-nostartfiles"), std::string::npos)); + expect(ne(line.find("-static"), std::string::npos)); +}}; -TEST(FreestandingLink, AddressesTheLinkerByPathNotByName) { +UTest addresses_the_linker_by_path_not_by_name{"FreestandingLink/AddressesTheLinkerByPathNotByName", [] { auto s = resolve("riscv64-none-elf"); - ASSERT_TRUE(s.has_value()); + expect(fatal(s.has_value())); LinkInputs in; in.lld = "/payload/bin/ld.lld"; auto line = link_flags(*s, in, [](const std::filesystem::path& p) { @@ -97,60 +100,59 @@ TEST(FreestandingLink, AddressesTheLinkerByPathNotByName) { // `-fuse-ld=lld` resolves through PATH and finds GNU ld on any machine // with binutils earlier on it, which then dies with "unrecognised // emulation mode: elf64lriscv". Reproduced 2026-08-19. - EXPECT_NE(line.find("-fuse-ld=/payload/bin/ld.lld"), std::string::npos); - EXPECT_EQ(line.find("-fuse-ld=lld "), std::string::npos); -} + expect(ne(line.find("-fuse-ld=/payload/bin/ld.lld"), std::string::npos)); + expect(eq(line.find("-fuse-ld=lld "), std::string::npos)); +}}; -TEST(FreestandingLink, LinkerScriptOnlyWhenOneIsSupplied) { +UTest linker_script_only_when_one_is_supplied{"FreestandingLink/LinkerScriptOnlyWhenOneIsSupplied", [] { auto s = resolve("riscv64-none-elf"); - ASSERT_TRUE(s.has_value()); + expect(fatal(s.has_value())); auto esc = [](const std::filesystem::path& p) { return p.string(); }; LinkInputs none; - EXPECT_EQ(link_flags(*s, none, esc).find(" -T "), std::string::npos); + expect(eq(link_flags(*s, none, esc).find(" -T "), std::string::npos)); LinkInputs with; with.linkerScript = "/board/link.ld"; - EXPECT_NE(link_flags(*s, with, esc).find(" -T /board/link.ld"), - std::string::npos); -} + expect(ne(link_flags(*s, with, esc).find(" -T /board/link.ld"), std::string::npos)); +}}; -TEST(FreestandingLink, LldIsIdentifiedByItsOwnVersionOutput) { +UTest lld_is_identified_by_its_own_version_output{"FreestandingLink/LldIsIdentifiedByItsOwnVersionOutput", [] { // By output, not by filename — the filename is exactly what was wrong in // the failure this guards. - EXPECT_TRUE(version_output_is_lld("LLD 22.1.8 (compatible with GNU linkers)")); - EXPECT_FALSE(version_output_is_lld("GNU ld (GNU Binutils for Ubuntu) 2.42")); -} + expect(version_output_is_lld("LLD 22.1.8 (compatible with GNU linkers)")); + expect(!(version_output_is_lld("GNU ld (GNU Binutils for Ubuntu) 2.42"))); +}}; // ── the runner ────────────────────────────────────────────────────────────── -TEST(FreestandingRunner, AppendsTheArtifactWhenNoPlaceholder) { +UTest appends_the_artifact_when_no_placeholder{"FreestandingRunner/AppendsTheArtifactWhenNoPlaceholder", [] { std::vector tmpl{ "qemu-system-riscv64", "-machine", "virt", "-kernel" }; auto argv = expand(tmpl, "/build/firmware"); - ASSERT_EQ(argv.size(), 5u); - EXPECT_EQ(argv.front(), "qemu-system-riscv64"); - EXPECT_EQ(argv.back(), "/build/firmware"); -} + expect(fatal(eq(argv.size(), 5u))); + expect(eq(argv.front(), "qemu-system-riscv64")); + expect(eq(argv.back(), "/build/firmware")); +}}; -TEST(FreestandingRunner, SubstitutesThePlaceholderInPlace) { +UTest substitutes_the_placeholder_in_place{"FreestandingRunner/SubstitutesThePlaceholderInPlace", [] { // The emulator that wants the image in the middle rather than at the end. std::vector tmpl{ "runner", "--image={}", "--verbose" }; auto argv = expand(tmpl, "/build/firmware"); - ASSERT_EQ(argv.size(), 3u); - EXPECT_EQ(argv[1], "--image=/build/firmware"); - EXPECT_EQ(argv[2], "--verbose"); // nothing appended when substituted -} + expect(fatal(eq(argv.size(), 3u))); + expect(eq(argv[1], "--image=/build/firmware")); + expect(eq(argv[2], "--verbose")); // nothing appended when substituted +}}; -TEST(FreestandingRunner, TheMissingRunnerMessageNamesTheKeyAndTheTriple) { +UTest the_missing_runner_message_names_the_key_and_the_triple{"FreestandingRunner/TheMissingRunnerMessageNamesTheKeyAndTheTriple", [] { // The user has just been told the build succeeded and the run did not; the // gap between those two is the whole content of the message, so it has to // carry something pasteable. auto msg = no_runner_message("riscv64-none-elf"); - EXPECT_NE(msg.find("[target.riscv64-none-elf]"), std::string::npos); - EXPECT_NE(msg.find("runner = ["), std::string::npos); - EXPECT_NE(msg.find("board-support package"), std::string::npos); -} + expect(ne(msg.find("[target.riscv64-none-elf]"), std::string::npos)); + expect(ne(msg.find("runner = ["), std::string::npos)); + expect(ne(msg.find("board-support package"), std::string::npos)); +}}; // ── `[xlings] deps` → payload directory ───────────────────────────────────── // @@ -159,33 +161,33 @@ TEST(FreestandingRunner, TheMissingRunnerMessageNamesTheKeyAndTheTriple) { // reconstructed `/data/xpkgs/-x-/` would be coupled // to store internals mcpp is free to change. -TEST(XpkgRef, ParsesEverySpellingAManifestMayWrite) { +UTest parses_every_spelling_a_manifest_may_write{"XpkgRef/ParsesEverySpellingAManifestMayWrite", [] { using mcpp::provider::xlings::paths::parse_xpkg_ref; auto full = parse_xpkg_ref("xim:picolibc-riscv@1.8.12"); - EXPECT_EQ(full.ns, "xim"); - EXPECT_EQ(full.name, "picolibc-riscv"); - EXPECT_EQ(full.version, "1.8.12"); + expect(eq(full.ns, "xim")); + expect(eq(full.name, "picolibc-riscv")); + expect(eq(full.version, "1.8.12")); // No namespace: the manifest's default, spelled once in the parser rather // than at each call site. auto bare = parse_xpkg_ref("ninja"); - EXPECT_EQ(bare.ns, "xim"); - EXPECT_EQ(bare.name, "ninja"); - EXPECT_TRUE(bare.version.empty()); + expect(eq(bare.ns, "xim")); + expect(eq(bare.name, "ninja")); + expect(bare.version.empty()); // A non-xim namespace is ordinary, not exotic: `local:` is what a package // under development resolves to. auto local = parse_xpkg_ref("local:qemu-riscv@9.2.4-1"); - EXPECT_EQ(local.ns, "local"); - EXPECT_EQ(local.name, "qemu-riscv"); - EXPECT_EQ(local.version, "9.2.4-1"); -} + expect(eq(local.ns, "local")); + expect(eq(local.name, "qemu-riscv")); + expect(eq(local.version, "9.2.4-1")); +}}; -TEST(XpkgPayload, APinnedRefResolvesToThatVersionOrToNothing) { +UTest a_pinned_ref_resolves_to_that_version_or_to_nothing{"XpkgPayload/APinnedRefResolvesToThatVersionOrToNothing", [] { namespace xp = mcpp::provider::xlings::paths; auto base = std::filesystem::temp_directory_path() - / std::format("mcpp-xpkg-test-{}", ::getpid()); + / std::format("mcpp-xpkg-test-{}", mcpplibs::os::process::self()->id); std::filesystem::remove_all(base); std::filesystem::create_directories(base / "xim-x-demo" / "1.0.0"); std::filesystem::create_directories(base / "xim-x-demo" / "2.0.0"); @@ -193,30 +195,30 @@ TEST(XpkgPayload, APinnedRefResolvesToThatVersionOrToNothing) { // The whole point: asking for 1.0.0 and silently getting 2.0.0 is an // answer only discovered later, in the artifact. auto pinned = xp::xpkg_payload_at(base, xp::parse_xpkg_ref("xim:demo@1.0.0")); - ASSERT_TRUE(pinned.has_value()); - EXPECT_EQ(pinned->filename(), "1.0.0"); + expect(fatal(pinned.has_value())); + expect(eq(pinned->filename(), "1.0.0")); - EXPECT_FALSE(xp::xpkg_payload_at(base, xp::parse_xpkg_ref("xim:demo@9.9.9")) - .has_value()); + expect(!(xp::xpkg_payload_at(base, xp::parse_xpkg_ref("xim:demo@9.9.9")) + .has_value())); std::filesystem::remove_all(base); -} +}}; -TEST(XpkgPayload, AnUnpinnedRefTakesTheHighestVersionNumerically) { +UTest an_unpinned_ref_takes_the_highest_version_numerically{"XpkgPayload/AnUnpinnedRefTakesTheHighestVersionNumerically", [] { namespace xp = mcpp::provider::xlings::paths; auto base = std::filesystem::temp_directory_path() - / std::format("mcpp-xpkg-test2-{}", ::getpid()); + / std::format("mcpp-xpkg-test2-{}", mcpplibs::os::process::self()->id); std::filesystem::remove_all(base); for (auto v : { "0.4.2", "0.4.9", "0.4.11" }) std::filesystem::create_directories(base / "xim-x-demo" / v); // A plain string sort answers 0.4.9; picking the wrong payload is silent. auto latest = xp::xpkg_payload_at(base, xp::parse_xpkg_ref("demo")); - ASSERT_TRUE(latest.has_value()); - EXPECT_EQ(latest->filename(), "0.4.11"); + expect(fatal(latest.has_value())); + expect(eq(latest->filename(), "0.4.11")); std::filesystem::remove_all(base); -} +}}; // A CONSTRAINT IN THE VERSION POSITION IS A CONSTRAINT, NOT A DIRECTORY NAME. // @@ -225,73 +227,73 @@ TEST(XpkgPayload, AnUnpinnedRefTakesTheHighestVersionNumerically) { // directory name -- so a range installed a payload and then reported that // nothing was installed. That is what made a rule package unable to state a // floor: it declared `>=8.5.0`, got it, and `xpkg_dir` answered "". -TEST(XpkgPayload, AConstrainedRefTakesTheHighestInstalledVersionSatisfyingIt) { +UTest a_constrained_ref_takes_the_highest_installed_version_satisfying_it{"XpkgPayload/AConstrainedRefTakesTheHighestInstalledVersionSatisfyingIt", [] { namespace xp = mcpp::provider::xlings::paths; auto base = std::filesystem::temp_directory_path() - / std::format("mcpp-xpkg-test3-{}", ::getpid()); + / std::format("mcpp-xpkg-test3-{}", mcpplibs::os::process::self()->id); std::filesystem::remove_all(base); for (auto v : { "8.0.0", "8.5.0", "8.7.1" }) std::filesystem::create_directories(base / "xim-x-demo" / v); auto floor = xp::xpkg_payload_at(base, xp::parse_xpkg_ref("xim:demo@>=8.5.0")); - ASSERT_TRUE(floor.has_value()); - EXPECT_EQ(floor->filename(), "8.7.1"); + expect(fatal(floor.has_value())); + expect(eq(floor->filename(), "8.7.1")); // Bounded on both sides, so the highest is NOT the answer -- a test that // only used a lower bound would pass on an implementation that ignored the // requirement and took the newest. Spelled the way xlings reads a // conjunction (space-separated), because xlings is what resolved it. auto ranged = xp::xpkg_payload_at(base, xp::parse_xpkg_ref("xim:demo@>=8.0.0 <8.7.0")); - ASSERT_TRUE(ranged.has_value()); - EXPECT_EQ(ranged->filename(), "8.5.0"); + expect(fatal(ranged.has_value())); + expect(eq(ranged->filename(), "8.5.0")); // Unsatisfiable is absent, not "the closest one". - EXPECT_FALSE(xp::xpkg_payload_at(base, xp::parse_xpkg_ref("xim:demo@>=9.0.0")) - .has_value()); + expect(!(xp::xpkg_payload_at(base, xp::parse_xpkg_ref("xim:demo@>=9.0.0")) + .has_value())); // AND AN EXACT PIN STILL MEANS EXACTLY THAT. The constrained path must not // capture a pinned-and-absent request: 8.6.0 is missing and 8.7.1 is right // there, and answering with it is the failure the pin exists to prevent. - EXPECT_FALSE(xp::xpkg_payload_at(base, xp::parse_xpkg_ref("xim:demo@8.6.0")) - .has_value()); + expect(!(xp::xpkg_payload_at(base, xp::parse_xpkg_ref("xim:demo@8.6.0")) + .has_value())); std::filesystem::remove_all(base); -} +}}; // An installed version whose directory name is not a SemVer is addressable by // its exact spelling. `8.0.RC1` is a real CANN version. -TEST(XpkgPayload, AnUnparseableVersionIsStillAddressableExactly) { +UTest an_unparseable_version_is_still_addressable_exactly{"XpkgPayload/AnUnparseableVersionIsStillAddressableExactly", [] { namespace xp = mcpp::provider::xlings::paths; auto base = std::filesystem::temp_directory_path() - / std::format("mcpp-xpkg-test4-{}", ::getpid()); + / std::format("mcpp-xpkg-test4-{}", mcpplibs::os::process::self()->id); std::filesystem::remove_all(base); std::filesystem::create_directories(base / "xim-x-demo" / "8.0.RC1"); auto exact = xp::xpkg_payload_at(base, xp::parse_xpkg_ref("xim:demo@8.0.RC1")); - ASSERT_TRUE(exact.has_value()); - EXPECT_EQ(exact->filename(), "8.0.RC1"); + expect(fatal(exact.has_value())); + expect(eq(exact->filename(), "8.0.RC1")); // xlings' grammar reads it as 8, 0, RC, 1, so a range can test it, and the // lookup answers what xlings selected. A key with no numeric segment at // all is a name, and a name never answers a range. auto ranged = xp::xpkg_payload_at(base, xp::parse_xpkg_ref("xim:demo@>=1.0")); - ASSERT_TRUE(ranged.has_value()); - EXPECT_EQ(ranged->filename(), "8.0.RC1"); + expect(fatal(ranged.has_value())); + expect(eq(ranged->filename(), "8.0.RC1")); std::filesystem::create_directories(base / "xim-x-named" / "nightly"); - EXPECT_FALSE(xp::xpkg_payload_at(base, xp::parse_xpkg_ref("xim:named@>=1.0")) - .has_value()); + expect(!(xp::xpkg_payload_at(base, xp::parse_xpkg_ref("xim:named@>=1.0")) + .has_value())); std::filesystem::remove_all(base); -} +}}; // A BARE VERSION MEANS WHAT XLINGS MEANT BY IT (#712). The address // `libglvnd@1.7` installed `1.7.0.1`: one or two segments are a prefix range, // three or more are written-prefix equality, and a fourth segment is an // ordinary version. The lookup answered "" for every one of these before. -TEST(XpkgPayload, ABareVersionSelectsWhatXlingsSelected) { +UTest a_bare_version_selects_what_xlings_selected{"XpkgPayload/ABareVersionSelectsWhatXlingsSelected", [] { namespace xp = mcpp::provider::xlings::paths; auto base = std::filesystem::temp_directory_path() - / std::format("mcpp-xpkg-test5-{}", ::getpid()); + / std::format("mcpp-xpkg-test5-{}", mcpplibs::os::process::self()->id); std::filesystem::remove_all(base); for (auto v : { "1.7.0.1", "1.2.0", "1.2.5", "1.3.0", "12.9.1.4", "12.9.10" }) std::filesystem::create_directories(base / "xim-x-demo" / v); @@ -301,48 +303,46 @@ TEST(XpkgPayload, ABareVersionSelectsWhatXlingsSelected) { xp::parse_xpkg_ref(std::format("xim:demo@{}", ver))); return p ? p->filename().string() : std::string{}; }; - EXPECT_EQ(pick("1.7"), "1.7.0.1"); // prefix range [1.7, 1.8) - EXPECT_EQ(pick("1.7.0"), "1.7.0.1"); // written prefix 1.7.0 - EXPECT_EQ(pick("1.2"), "1.2.5"); // the highest 1.2.x, never 1.3 - EXPECT_EQ(pick("1.2.0"), "1.2.0"); // three segments: 1.2.0 exactly - EXPECT_EQ(pick("12.9.1"), "12.9.1.4"); // not 12.9.10 - EXPECT_EQ(pick("1"), "1.7.0.1"); // [1, 2) - EXPECT_EQ(pick("1.2.4"), ""); // absent is absent - EXPECT_EQ(pick("1.8.12"), ""); // never slides to a later minor + expect(eq(pick("1.7"), "1.7.0.1")); // prefix range [1.7, 1.8) + expect(eq(pick("1.7.0"), "1.7.0.1")); // written prefix 1.7.0 + expect(eq(pick("1.2"), "1.2.5")); // the highest 1.2.x, never 1.3 + expect(eq(pick("1.2.0"), "1.2.0")); // three segments: 1.2.0 exactly + expect(eq(pick("12.9.1"), "12.9.1.4")); // not 12.9.10 + expect(eq(pick("1"), "1.7.0.1")); // [1, 2) + expect(eq(pick("1.2.4"), "")); // absent is absent + expect(eq(pick("1.8.12"), "")); // never slides to a later minor std::filesystem::remove_all(base); -} +}}; // The revision xlings recorded for a payload (openxlings/xlings#620): the // field when present, 0 for a record that predates it, and nothing at all when // there is no record -- which is the case mcpp judges by its marker alone. -TEST(XpkgPayload, InstalledRevisionReadsTheXlingsRecord) { +UTest installed_revision_reads_the_xlings_record{"XpkgPayload/InstalledRevisionReadsTheXlingsRecord", [] { namespace xp = mcpp::provider::xlings::paths; auto dir = std::filesystem::temp_directory_path() - / std::format("mcpp-xpkg-rev-{}", ::getpid()); + / std::format("mcpp-xpkg-rev-{}", mcpplibs::os::process::self()->id); std::filesystem::remove_all(dir); std::filesystem::create_directories(dir); - EXPECT_FALSE(xp::installed_revision(dir).has_value()); + expect(!(xp::installed_revision(dir).has_value())); std::ofstream(dir / ".xpkg-install.json") << R"({"os":"linux","version":"2.44.3"})"; - EXPECT_EQ(xp::installed_revision(dir), 0); + expect(eq(xp::installed_revision(dir), 0)); std::ofstream(dir / ".xpkg-install.json") << R"({"version":"2.44.3","revision":1})"; - EXPECT_EQ(xp::installed_revision(dir), 1); + expect(eq(xp::installed_revision(dir), 1)); std::filesystem::remove_all(dir); -} +}}; -TEST(XpkgEnvVar, BothSpellingsAreDerivedFromOneSanitizer) { +UTest both_spellings_are_derived_from_one_sanitizer{"XpkgEnvVar/BothSpellingsAreDerivedFromOneSanitizer", [] { using mcpp::build::xpkg_env_var; // The two sides of the channel must agree; drifting apart would make the // interface answer "" for a package that is right there. - EXPECT_EQ(xpkg_env_var("xim", "picolibc-riscv"), - "MCPP_XPKG_XIM_PICOLIBC_RISCV_DIR"); - EXPECT_EQ(xpkg_env_var("", "picolibc-riscv"), - "MCPP_XPKG_PICOLIBC_RISCV_DIR"); -} + expect(eq(xpkg_env_var("xim", "picolibc-riscv"), "MCPP_XPKG_XIM_PICOLIBC_RISCV_DIR")); + expect(eq(xpkg_env_var("", "picolibc-riscv"), "MCPP_XPKG_PICOLIBC_RISCV_DIR")); +}}; // ── the target owns its C library ─────────────────────────────────────────── -TEST(FreestandingTarget, BareMetalRowsNameTheirSysroot) { +UTest bare_metal_rows_name_their_sysroot{"FreestandingTarget/BareMetalRowsNameTheirSysroot", [] { // The axis that stops every bare-metal PACKAGE from naming a libc. // Before it, a board-support package and a standard-library subset each // had to carry `[xlings] deps = ["xim:picolibc-riscv@..."]`, which bound @@ -351,115 +351,115 @@ TEST(FreestandingTarget, BareMetalRowsNameTheirSysroot) { for (const char* s : { "riscv64-none-elf", "riscv32-none-elf" }) { auto* k = mcpp::toolchain::triple::find_known_target( *mcpp::toolchain::triple::parse(s)); - ASSERT_NE(k, nullptr) << s; - EXPECT_FALSE(k->sysroot.empty()) << s; + expect(fatal(ne(k, nullptr))) << s; + expect(!(k->sysroot.empty())) << s; // Same spelling the install channel takes, so the two cannot drift. - EXPECT_TRUE(k->sysroot.starts_with("xim:")) << s; + expect(k->sysroot.starts_with("xim:")) << s; } -} +}}; -TEST(FreestandingTarget, HostedRowsNameNone) { +UTest hosted_rows_name_none{"FreestandingTarget/HostedRowsNameNone", [] { // A hosted target's libc comes with the compiler payload or through the // runtime binding. Naming one here would resolve a second copy. for (const char* s : { "x86_64-linux-gnu", "x86_64-linux-musl", "aarch64-macos", "x86_64-windows-gnu" }) { auto* k = mcpp::toolchain::triple::find_known_target( *mcpp::toolchain::triple::parse(s)); - ASSERT_NE(k, nullptr) << s; - EXPECT_TRUE(k->sysroot.empty()) << s; + expect(fatal(ne(k, nullptr))) << s; + expect(k->sysroot.empty()) << s; } -} +}}; // ── whole-graph properties ────────────────────────────────────────────────── -TEST(FreestandingFlags, ExceptionsAndRttiAreOffForTheWholeGraph) { +UTest exceptions_and_rtti_are_off_for_the_whole_graph{"FreestandingFlags/ExceptionsAndRttiAreOffForTheWholeGraph", [] { auto spec = resolve("riscv64-none-elf"); - ASSERT_TRUE(spec.has_value()); + expect(fatal(spec.has_value())); auto f = compile_flags(*spec); // These live with the TARGET, not in a project's cxxflags, because a // BMI records them: a dependency compiled with exceptions cannot be // imported by a TU without them, and clang reports that as a .pcm // "configuration mismatch" rather than as a flag disagreement. - EXPECT_TRUE(std::ranges::find(f, "-fno-exceptions") != f.end()); - EXPECT_TRUE(std::ranges::find(f, "-fno-rtti") != f.end()); + expect(std::ranges::find(f, "-fno-exceptions") != f.end()); + expect(std::ranges::find(f, "-fno-rtti") != f.end()); // The company they keep — same argument, already shipped. - EXPECT_TRUE(std::ranges::find(f, "-ffreestanding") != f.end()); - EXPECT_TRUE(std::ranges::find(f, "-nostdinc++") != f.end()); -} + expect(std::ranges::find(f, "-ffreestanding") != f.end()); + expect(std::ranges::find(f, "-nostdinc++") != f.end()); +}}; -TEST(FreestandingFlags, HostedTargetsGetNoneOfThis) { +UTest hosted_targets_get_none_of_this{"FreestandingFlags/HostedTargetsGetNoneOfThis", [] { // The flags are a property of a freestanding triple, and the cache key // reads them through the same resolve(). A hosted target must produce no // Spec, or every hosted key would move for no reason. - EXPECT_FALSE(resolve("x86_64-linux-gnu").has_value()); -} + expect(!(resolve("x86_64-linux-gnu").has_value())); +}}; // ── engine floor: a package that needs a newer mcpp ───────────────────────── -TEST(BuildProgramCompatHint, RecognisesAllThreeFrontendSpellings) { +UTest recognises_all_three_frontend_spellings{"BuildProgramCompatHint/RecognisesAllThreeFrontendSpellings", [] { using mcpp::build::mentions_missing_mcpp_api; // Measured, not assumed: `if constexpr (requires { mcpp::runner("x"); })` // is a HARD ERROR when the name is absent, so a package cannot probe for a // newer API in-language. The compiler's error IS the compat channel, and // these are the three ways it arrives. - EXPECT_TRUE(mentions_missing_mcpp_api( - "build.mcpp:57:11: error: 'runner' is not a member of 'mcpp'")); // gcc - EXPECT_TRUE(mentions_missing_mcpp_api( - "build.mcpp:57:11: error: no member named 'runner' in namespace 'mcpp'"));// clang - EXPECT_TRUE(mentions_missing_mcpp_api( - "build.mcpp(57): error C2039: 'runner': is not a member of 'mcpp'")); // cl.exe -} - -TEST(BuildProgramCompatHint, StaysOffFailuresThatAreNotAboutOurApi) { + expect(bool(mentions_missing_mcpp_api( + "build.mcpp:57:11: error: 'runner' is not a member of 'mcpp'"))); // gcc + expect(bool(mentions_missing_mcpp_api( + "build.mcpp:57:11: error: no member named 'runner' in namespace 'mcpp'")));// clang + expect(bool(mentions_missing_mcpp_api( + "build.mcpp(57): error C2039: 'runner': is not a member of 'mcpp'"))); // cl.exe +}}; + +UTest stays_off_failures_that_are_not_about_our_api{"BuildProgramCompatHint/StaysOffFailuresThatAreNotAboutOurApi", [] { using mcpp::build::mentions_missing_mcpp_api; // The hint says "your mcpp may be too old". Attaching that to an ordinary // compile error would send the reader to the wrong place, so the match is // anchored on OUR namespace — a package's own missing symbol names its own. - EXPECT_FALSE(mentions_missing_mcpp_api( + expect(!mentions_missing_mcpp_api( "build.mcpp:12:5: error: 'runner' is not a member of 'board'")); - EXPECT_FALSE(mentions_missing_mcpp_api( + expect(!mentions_missing_mcpp_api( "build.mcpp:3:1: error: expected ';' after top level declarator")); - EXPECT_FALSE(mentions_missing_mcpp_api( + expect(!mentions_missing_mcpp_api( "build.mcpp:9:9: error: use of undeclared identifier 'mcpp_runner'")); -} +}}; // ── artifact set ─────────────────────────────────────────────────────────── -TEST(FreestandingArtifacts, SizeOutputIsParsedNotPassedThrough) { +UTest size_output_is_parsed_not_passed_through{"FreestandingArtifacts/SizeOutputIsParsedNotPassedThrough", [] { // Parsed so the printed shape is mcpp's, not the tool's: llvm-size and GNU // size differ in their headers, and a build whose output changed // appearance with the toolchain would be reporting the tool, not the size. auto s = parse_size_output( " text\t data\t bss\t dec\t hex\tfilename\n" " 8836\t 80\t 5668\t 14584\t 38f8\tfirmware\n"); - ASSERT_TRUE(s.has_value()); - EXPECT_EQ(s->text, 8836); - EXPECT_EQ(s->data, 80); - EXPECT_EQ(s->bss, 5668); + expect(fatal(s.has_value())); + expect(eq(s->text, 8836)); + expect(eq(s->data, 80)); + expect(eq(s->bss, 5668)); // The number that decides whether the image fits. - EXPECT_EQ(size_total(*s), 14584); -} + expect(eq(size_total(*s), 14584)); +}}; -TEST(FreestandingArtifacts, MalformedSizeOutputIsNotGuessedAt) { +UTest malformed_size_output_is_not_guessed_at{"FreestandingArtifacts/MalformedSizeOutputIsNotGuessedAt", [] { // An informational line has no standing to invent numbers. - EXPECT_FALSE(parse_size_output("").has_value()); - EXPECT_FALSE(parse_size_output("size: cannot open 'x'\n").has_value()); -} + expect(!(parse_size_output("").has_value())); + expect(!(parse_size_output("size: cannot open 'x'\n").has_value())); +}}; -TEST(FreestandingArtifacts, MapFlagNamesTheArtifact) { +UTest map_flag_names_the_artifact{"FreestandingArtifacts/MapFlagNamesTheArtifact", [] { auto f = map_flag("/build/bin/firmware", [](const std::filesystem::path& p) { return p.string(); }); - EXPECT_EQ(f, " -Wl,-Map=/build/bin/firmware.map"); -} + expect(eq(f, " -Wl,-Map=/build/bin/firmware.map")); +}}; -TEST(FreestandingArtifacts, ObjcopyOnlyResolvesForABareMetalTarget) { +UTest objcopy_only_resolves_for_a_bare_metal_target{"FreestandingArtifacts/ObjcopyOnlyResolvesForABareMetalTarget", [] { // A hosted binary is loaded by a loader that wants the ELF, so there is // nothing to convert — and resolving a tool for it would put an edge in // every hosted graph. - EXPECT_TRUE(resolve_objcopy("/payload/bin/clang++", "x86_64-linux-gnu").empty()); + expect(resolve_objcopy("/payload/bin/clang++", "x86_64-linux-gnu").empty()); // (The bare-metal case needs a real payload on disk, so it is asserted by // tests/e2e/130 instead: the artifact set has to actually appear.) -} +}}; // ── aarch64-none-elf: the second bare-metal architecture ──────────────────── // @@ -476,32 +476,32 @@ TEST(FreestandingArtifacts, ObjcopyOnlyResolvesForABareMetalTarget) { // default assumes the low 2GiB and bare-metal RISC-V runs at 0x80000000; // aarch64's `small` already addresses +/-4GiB PC-relative, which covers any // single image wherever a linker script places it. -TEST(FreestandingTarget, Aarch64UsesTheProcedureCallStandardNotADataModel) { +UTest aarch64_uses_the_procedure_call_standard_not_a_data_model{"FreestandingTarget/Aarch64UsesTheProcedureCallStandardNotADataModel", [] { auto t = mcpp::toolchain::triple::parse("aarch64-none-elf"); - ASSERT_TRUE(t.has_value()); + expect(fatal(t.has_value())); auto s = mcpp::freestanding::resolve(t->str()); - ASSERT_TRUE(s.has_value()); - EXPECT_EQ(s->mabi, "aapcs"); - EXPECT_NE(s->mabi, "lp64"); - EXPECT_EQ(s->march, "armv8-a"); - EXPECT_EQ(s->mcmodel, "small"); -} + expect(fatal(s.has_value())); + expect(eq(s->mabi, "aapcs")); + expect(ne(s->mabi, "lp64")); + expect(eq(s->march, "armv8-a")); + expect(eq(s->mcmodel, "small")); +}}; // The row resolves no C library, which is what makes it the zero-libc tier: the // first consumer of this target is a layer of machine mechanism that references // no C library symbol, and there is no aarch64 picolibc in the index to resolve // even if it wanted one. -TEST(FreestandingTarget, Aarch64ResolvesNoCLibrary) { +UTest aarch64_resolves_no_c_library{"FreestandingTarget/Aarch64ResolvesNoCLibrary", [] { auto t = mcpp::toolchain::triple::parse("aarch64-none-elf"); - ASSERT_TRUE(t.has_value()); - EXPECT_TRUE(mcpp::toolchain::triple::effective_sysroot(*t, nullptr).empty()); + expect(fatal(t.has_value())); + expect(mcpp::toolchain::triple::effective_sysroot(*t, nullptr).empty()); // The other side: a project may still ASK for one, and the override is // what carries the request. An empty column means "nothing by default", not // "nothing is possible". const std::string want = "xim:some-aarch64-libc@1.0"; - EXPECT_EQ(mcpp::toolchain::triple::effective_sysroot(*t, &want), want); -} + expect(eq(mcpp::toolchain::triple::effective_sysroot(*t, &want), want)); +}}; // THIS TEST USED TO ASSERT THE OPPOSITE, AND IT WAS RIGHT UNTIL 2026-08-21. // @@ -521,27 +521,27 @@ TEST(FreestandingTarget, Aarch64ResolvesNoCLibrary) { // `.agents/tools/build-baremetal-sysroot.sh` produces them from the same // march/mabi pair this table carries, so the two cannot drift silently — but // they are named here so a reader knows where the other half lives. -TEST(FreestandingTarget, MultilibDirectoriesMatchTheIndexPayloads) { +UTest multilib_directories_match_the_index_payloads{"FreestandingTarget/MultilibDirectoriesMatchTheIndexPayloads", [] { auto a = mcpp::freestanding::resolve("aarch64-none-elf"); - ASSERT_TRUE(a.has_value()); - EXPECT_EQ(a->libdir, "armv8-a/aapcs"); + expect(fatal(a.has_value())); + expect(eq(a->libdir, "armv8-a/aapcs")); // The directory is the march/mabi pair, so it cannot disagree with the // flags the same row produces. - EXPECT_EQ(a->libdir, std::string(a->march) + "/" + std::string(a->mabi)); + expect(eq(a->libdir, std::string(a->march) + "/" + std::string(a->mabi))); auto x = mcpp::freestanding::resolve("x86_64-none-elf"); - ASSERT_TRUE(x.has_value()); - EXPECT_EQ(x->libdir, "x86-64/sysv"); - EXPECT_EQ(x->libdir, std::string(x->march) + "/" + std::string(x->mabi)); + expect(fatal(x.has_value())); + expect(eq(x->libdir, "x86-64/sysv")); + expect(eq(x->libdir, std::string(x->march) + "/" + std::string(x->mabi))); // Filling this column does NOT give those targets a C library by // default. It is consulted only once a sysroot has been RESOLVED, and the // target table still binds none for these two rows — the zero-libc tier // stays the default and the package stays opt-in. auto t = mcpp::toolchain::triple::parse("aarch64-none-elf"); - ASSERT_TRUE(t.has_value()); - EXPECT_TRUE(mcpp::toolchain::triple::effective_sysroot(*t, nullptr).empty()); -} + expect(fatal(t.has_value())); + expect(mcpp::toolchain::triple::effective_sysroot(*t, nullptr).empty()); +}}; // ── The two tables keyed on the same triple must agree ─────────────────────── // @@ -559,28 +559,28 @@ TEST(FreestandingTarget, MultilibDirectoriesMatchTheIndexPayloads) { // The check is possible only because both are compile-time tables with a // single read point each. It costs fifteen lines and removes an entire class // of "added a target, forgot half of it". -TEST(FreestandingTarget, EveryBareRowInTheTargetTableHasAnIsaProfile) { +UTest every_bare_row_in_the_target_table_has_an_isa_profile{"FreestandingTarget/EveryBareRowInTheTargetTableHasAnIsaProfile", [] { for (const auto& info : mcpp::toolchain::triple::known_targets()) { if (info.note != "bare") continue; auto t = mcpp::toolchain::triple::parse(info.canonical); - ASSERT_TRUE(t.has_value()) << info.canonical; - EXPECT_TRUE(mcpp::freestanding::resolve(*t).has_value()) + expect(fatal(t.has_value())) << info.canonical; + expect(mcpp::freestanding::resolve(*t).has_value()) << info.canonical << " is a bare row in kKnownTargets with no row in freestanding::kTable"; } -} +}}; -TEST(FreestandingTarget, EveryIsaProfileHasABareRowInTheTargetTable) { +UTest every_isa_profile_has_a_bare_row_in_the_target_table{"FreestandingTarget/EveryIsaProfileHasABareRowInTheTargetTable", [] { for (const auto& spec : mcpp::freestanding::known()) { auto t = mcpp::toolchain::triple::parse(spec.triple); - ASSERT_TRUE(t.has_value()) << spec.triple; + expect(fatal(t.has_value())) << spec.triple; const auto* info = mcpp::toolchain::triple::find_known_target(*t); - ASSERT_NE(info, nullptr) + expect(fatal(ne(info, nullptr))) << spec.triple << " has an ISA profile but no row in kKnownTargets"; - EXPECT_EQ(info->note, "bare") + expect(eq(info->note, "bare")) << spec.triple << " has an ISA profile but is not marked bare"; } -} +}}; // ── Cortex-M ──────────────────────────────────────────────────────────────── // @@ -612,17 +612,17 @@ bool has_flag(const mcpp::freestanding::Spec& s, std::string_view f) { // floats cross a call boundary, not whether the compiler may use the FPU inside // one — and `thumbv7em` implies an FPU. Measured on llvm 22.1.8: without // `-mfpu=none` the soft row emits `vmul.f32`. -TEST(FreestandingTarget, SoftFloatArm32RowsDisableTheFpu) { +UTest soft_float_arm32_rows_disable_the_fpu{"FreestandingTarget/SoftFloatArm32RowsDisableTheFpu", [] { int soft = 0, hard = 0; for (const auto& spec : mcpp::freestanding::known()) { if (!is_arm32(spec.triple)) continue; if (spec.triple.ends_with("-eabihf")) { ++hard; - EXPECT_FALSE(has_flag(spec, "-mfpu=none")) + expect(!has_flag(spec, "-mfpu=none")) << spec.triple << " is a hard-float row and must not discard the FPU"; } else { ++soft; - EXPECT_TRUE(has_flag(spec, "-mfpu=none")) + expect(has_flag(spec, "-mfpu=none")) << spec.triple << " is a soft-float row without -mfpu=none: it will emit FPU" " instructions that fault on a part with no FPU"; @@ -630,47 +630,45 @@ TEST(FreestandingTarget, SoftFloatArm32RowsDisableTheFpu) { } // A denominator, because a loop that never ran satisfies every EXPECT // above. Both halves must be non-empty for the contrast to mean anything. - EXPECT_GT(soft, 0); - EXPECT_GT(hard, 0); + expect(gt(soft, 0)); + expect(gt(hard, 0)); // And the A-profile rows are in the denominator, which is the half a // spelling-based predicate lost without failing. int a_profile = 0; for (const auto& spec : mcpp::freestanding::known()) if (spec.triple.starts_with("armv")) ++a_profile; - EXPECT_GT(a_profile, 0); -} + expect(gt(a_profile, 0)); +}}; // 32-bit ARM has no `-mcmodel`, and clang has a BareMetal toolchain for arm so // its driver reaches `ld.lld` without the x86_64 row's workaround. Both columns // being empty is what says so; a filled one would be a value nothing checks. -TEST(FreestandingTarget, Arm32RowsNeedNoCodeModelAndNoDirectLldDriving) { +UTest arm32_rows_need_no_code_model_and_no_direct_lld_driving{"FreestandingTarget/Arm32RowsNeedNoCodeModelAndNoDirectLldDriving", [] { int rows = 0; for (const auto& spec : mcpp::freestanding::known()) { if (!is_arm32(spec.triple)) continue; ++rows; - EXPECT_TRUE(spec.mcmodel.empty()) << spec.triple << " has no -mcmodel axis"; - EXPECT_TRUE(spec.lldEmulation.empty()) + expect(spec.mcmodel.empty()) << spec.triple << " has no -mcmodel axis"; + expect(spec.lldEmulation.empty()) << spec.triple << " reaches ld.lld through the clang driver"; - EXPECT_EQ(spec.mabi, "aapcs") << spec.triple; + expect(eq(spec.mabi, "aapcs")) << spec.triple; } - EXPECT_GT(rows, 0); -} + expect(gt(rows, 0)); +}}; // The compile half of `--gc-sections`, which has to hold for EVERY freestanding // target rather than only the M-profile ones: a dependency's translation units // carry these flags too, and a project cannot reach those. -TEST(FreestandingTarget, EveryRowCompilesWithPerFunctionSections) { +UTest every_row_compiles_with_per_function_sections{"FreestandingTarget/EveryRowCompilesWithPerFunctionSections", [] { int rows = 0; for (const auto& spec : mcpp::freestanding::known()) { ++rows; auto flags = mcpp::freestanding::compile_flags(spec); - EXPECT_NE(std::find(flags.begin(), flags.end(), "-ffunction-sections"), - flags.end()) << spec.triple; - EXPECT_NE(std::find(flags.begin(), flags.end(), "-fdata-sections"), - flags.end()) << spec.triple; + expect(ne(std::find(flags.begin(), flags.end(), "-ffunction-sections"), flags.end())) << spec.triple; + expect(ne(std::find(flags.begin(), flags.end(), "-fdata-sections"), flags.end())) << spec.triple; } - EXPECT_GT(rows, 0); -} + expect(gt(rows, 0)); +}}; // THE MULTILIB KEY IS NOT `/` ON ARM, AND ASSUMING IT WAS // PRODUCES AN ABI SUBSTITUTION THAT NOTHING REPORTS. @@ -689,30 +687,30 @@ TEST(FreestandingTarget, EveryRowCompilesWithPerFunctionSections) { // So the assertion is not "libdir is non-empty": it is that the soft and hard // variants of ONE architecture name DIFFERENT directories, which is the // property the layout exists to preserve. -TEST(FreestandingTarget, Arm32SoftAndHardVariantsNameDifferentLibdirs) { +UTest arm32_soft_and_hard_variants_name_different_libdirs{"FreestandingTarget/Arm32SoftAndHardVariantsNameDifferentLibdirs", [] { std::map> byMarch; int rows = 0; for (const auto& spec : mcpp::freestanding::known()) { if (!is_arm32(spec.triple)) continue; ++rows; - EXPECT_FALSE(spec.libdir.empty()) + expect(!spec.libdir.empty()) << spec.triple << " has no libdir, so an opt-in sysroot cannot be found"; byMarch[std::string(spec.march)].push_back(std::string(spec.libdir)); } - EXPECT_GT(rows, 0); + expect(gt(rows, 0)); int pairs = 0; for (auto const& [march, dirs] : byMarch) { if (dirs.size() < 2) continue; ++pairs; std::set distinct(dirs.begin(), dirs.end()); - EXPECT_EQ(distinct.size(), dirs.size()) + expect(eq(distinct.size(), dirs.size())) << march << " maps " << dirs.size() << " rows onto " << distinct.size() << " directories: a soft-float program would " "link a hard-float library, silently"; } // A denominator. If no architecture had two rows the loop above would // assert nothing, and the property would be untested rather than held. - EXPECT_GT(pairs, 0) << "no architecture carries both float ABIs; the check " + expect(gt(pairs, 0)) << "no architecture carries both float ABIs; the check " "above quantified over nothing"; -} +}}; diff --git a/tests/unit/test_graph_shape.cpp b/tests/unit/test_graph_shape.cpp index b5ea4da73..cdc4ccef7 100644 --- a/tests/unit/test_graph_shape.cpp +++ b/tests/unit/test_graph_shape.cpp @@ -1,7 +1,9 @@ -#include import std; +import mcpplibs.mtest; import mcpp.build.graph_shape; +using namespace mcpplibs::mtest; + // The header line build.ninja carries is what the fast paths read before any // plan exists. Four facts ride it -- the graph's shape, the module-edge // schedule, (2026.9.5.3+) whether a `--accel` / `--no-accel` override chose @@ -37,72 +39,65 @@ std::filesystem::path write_graph(const std::string& first) { } // namespace -TEST(GraphShape, TheHeaderNamesShapeScheduleSelectionAndFormat) { - EXPECT_EQ(mcpp::build::header_line(mcpp::build::GraphShape::Normal, "none", false), - "# mcpp:graph=normal;schedule=none;accel=default;dist=none;request="); - EXPECT_EQ(mcpp::build::header_line(mcpp::build::GraphShape::WithTests, "two-phase", true), - "# mcpp:graph=test;schedule=two-phase;accel=override;dist=none;request="); +UTest the_header_names_shape_schedule_selection_and_format{"GraphShape/TheHeaderNamesShapeScheduleSelectionAndFormat", [] { + expect(eq(mcpp::build::header_line(mcpp::build::GraphShape::Normal, "none", false), "# mcpp:graph=normal;schedule=none;accel=default;dist=none;request=")); + expect(eq(mcpp::build::header_line(mcpp::build::GraphShape::WithTests, "two-phase", true), "# mcpp:graph=test;schedule=two-phase;accel=override;dist=none;request=")); // An empty format reads as "none" rather than as an empty field: the value // has to be a word, because `read_pack_format` returning "" already means // "this file predates the field", and the two must not collide. - EXPECT_EQ(mcpp::build::header_line(mcpp::build::GraphShape::Normal, "none", false, "appimage"), - "# mcpp:graph=normal;schedule=none;accel=default;dist=appimage;request="); -} + expect(eq(mcpp::build::header_line(mcpp::build::GraphShape::Normal, "none", false, "appimage"), "# mcpp:graph=normal;schedule=none;accel=default;dist=appimage;request=")); +}}; // Workspace design 2026-09-29 §3: the request a graph was planned for rides // the same line, because the features and the workspace members no longer // name the directory. One tag per request, whatever the spelling. -TEST(GraphShape, TheHeaderNamesTheRequest) { +UTest the_header_names_the_request{"GraphShape/TheHeaderNamesTheRequest", [] { using namespace mcpp::build; const auto tag = request_tag("cli\x1egui", "b, a"); - EXPECT_EQ(tag, request_tag("cli\x1egui", "a,b")); - EXPECT_NE(tag, request_tag("cli", "a,b")); - EXPECT_NE(tag, request_tag("cli\x1egui", "a")); - EXPECT_EQ(read_request(write_graph(header_line(GraphShape::Normal, "none", false, {}, tag))), - tag); - EXPECT_EQ(read_request(write_graph("# mcpp:graph=normal;schedule=none;accel=default;dist=none")), - ""); -} + expect(eq(tag, request_tag("cli\x1egui", "a,b"))); + expect(ne(tag, request_tag("cli", "a,b"))); + expect(ne(tag, request_tag("cli\x1egui", "a"))); + expect(eq(read_request(write_graph(header_line(GraphShape::Normal, "none", false, {}, tag))), tag)); + expect(eq(read_request(write_graph("# mcpp:graph=normal;schedule=none;accel=default;dist=none")), "")); +}}; -TEST(GraphShape, OnlyAPlainGraphWithTheManifestsVariantIsReplayed) { +UTest only_a_plain_graph_with_the_manifests_variant_is_replayed{"GraphShape/OnlyAPlainGraphWithTheManifestsVariantIsReplayed", [] { using namespace mcpp::build; - EXPECT_TRUE(is_plain_build_graph(write_graph(header_line(GraphShape::Normal, "none", false)))); + expect(is_plain_build_graph(write_graph(header_line(GraphShape::Normal, "none", false)))); // An override chose this variant: a plain build must not replay it. - EXPECT_FALSE(is_plain_build_graph(write_graph(header_line(GraphShape::Normal, "none", true)))); + expect(!(is_plain_build_graph(write_graph(header_line(GraphShape::Normal, "none", true))))); // The test-shaped graph was already refused. - EXPECT_FALSE(is_plain_build_graph(write_graph(header_line(GraphShape::WithTests, "none", false)))); + expect(!(is_plain_build_graph(write_graph(header_line(GraphShape::WithTests, "none", false))))); // A DISTRIBUTION EDGE IS NOT PART OF A PLAIN BUILD. `mcpp pack --format // appimage` makes a build program submit an artifact action consuming the // staged tree; replaying that graph for a plain build would produce a // distributable as a side effect of `mcpp build`, from a staged tree that // is no longer guaranteed to describe this build. - EXPECT_FALSE(is_plain_build_graph( - write_graph(header_line(GraphShape::Normal, "none", false, "appimage")))); - EXPECT_EQ(read_pack_format( - write_graph(header_line(GraphShape::Normal, "none", false, "appimage"))), - "appimage"); - EXPECT_EQ(read_pack_format( - write_graph(header_line(GraphShape::Normal, "none", false))), - "none"); -} + expect(!(is_plain_build_graph( + write_graph(header_line(GraphShape::Normal, "none", false, "appimage"))))); + expect(eq(read_pack_format( + write_graph(header_line(GraphShape::Normal, "none", false, "appimage"))), "appimage")); + expect(eq(read_pack_format( + write_graph(header_line(GraphShape::Normal, "none", false))), "none")); +}}; -TEST(GraphShape, AGraphThatPredatesTheFieldIsAMiss) { +UTest a_graph_that_predates_the_field_is_a_miss{"GraphShape/AGraphThatPredatesTheFieldIsAMiss", [] { using namespace mcpp::build; // Written by a 2026.9.5.2 mcpp: shape and schedule, no selection. It is // not known to be the manifest's variant, so it is prepared once more and // rewritten with the field -- a miss, never a guess. auto p = write_graph("# mcpp:graph=normal;schedule=none"); - EXPECT_EQ(read_shape(p), GraphShape::Normal); - EXPECT_EQ(read_accel_selection(p), ""); - EXPECT_EQ(read_pack_format(p), ""); - EXPECT_FALSE(is_plain_build_graph(p)); + expect(eq(read_shape(p), GraphShape::Normal)); + expect(eq(read_accel_selection(p), "")); + expect(eq(read_pack_format(p), "")); + expect(!is_plain_build_graph(p)); // Written by a 2026.9.10.2 mcpp: shape, schedule and selection, no // distribution field. Same rule one field later -- absent is a miss, and // must not be read as "none", or the very first build after an upgrade // would replay a graph this binary cannot describe. auto q = write_graph("# mcpp:graph=normal;schedule=none;accel=default"); - EXPECT_EQ(read_accel_selection(q), "default"); - EXPECT_EQ(read_pack_format(q), ""); - EXPECT_FALSE(is_plain_build_graph(q)); -} + expect(eq(read_accel_selection(q), "default")); + expect(eq(read_pack_format(q), "")); + expect(!is_plain_build_graph(q)); +}}; diff --git a/tests/unit/test_hermetic_graph_link.cpp b/tests/unit/test_hermetic_graph_link.cpp index f633179eb..96ee6e5b1 100644 --- a/tests/unit/test_hermetic_graph_link.cpp +++ b/tests/unit/test_hermetic_graph_link.cpp @@ -6,13 +6,15 @@ // (the check covers sandbox toolchains only) printing one linker line with the // search directories each case needs. The check itself is Linux-only. -#include import std; +import mcpplibs.mtest; import mcpp.build.hermetic; import mcpplibs.os.platform; import mcpp.toolchain.model; +using namespace mcpplibs::mtest; + namespace { struct Tmp { @@ -52,32 +54,31 @@ mcpp::toolchain::Toolchain fake_driver(const std::filesystem::path& root, } // namespace -TEST(HermeticGraphLink, AHostSearchDirectoryIsRefusedAndNamed) { - if constexpr (!mcpplibs::os::is_linux) GTEST_SKIP() << "the check runs on Linux hosts"; +UTest a_host_search_directory_is_refused_and_named{"HermeticGraphLink/AHostSearchDirectoryIsRefusedAndNamed", [] { + if constexpr (!mcpplibs::os::is_linux) skip("the check runs on Linux hosts"); Tmp t; auto tc = fake_driver(t.path, {"/usr/lib", (t.path / "xpkgs" / "musl" / "lib").string()}); auto r = mcpp::build::verify_hermetic_link(tc, "", t.path / "out", false, /*isolatedGraphLink=*/true, {}); - ASSERT_FALSE(r.has_value()); - EXPECT_NE(r.error().find("/usr/lib"), std::string::npos) << r.error(); - EXPECT_NE(r.error().find("mcpp#696"), std::string::npos) << r.error(); - EXPECT_EQ(r.error().find("xpkgs/musl"), std::string::npos) - << "a store directory is allowed and must not be named: " << r.error(); -} + expect(fatal(!r.has_value())); + if (auto&& failed = expect(ne(r.error().find("/usr/lib"), std::string::npos)); !failed) failed << r.error(); + if (auto&& failed = expect(ne(r.error().find("mcpp#696"), std::string::npos)); !failed) failed << r.error(); + if (auto&& failed = expect(eq(r.error().find("xpkgs/musl"), std::string::npos)); !failed) failed << "a store directory is allowed and must not be named: " << r.error(); +}}; -TEST(HermeticGraphLink, AllowHostLibsDowngradesTheRefusal) { - if constexpr (!mcpplibs::os::is_linux) GTEST_SKIP() << "the check runs on Linux hosts"; +UTest allow_host_libs_downgrades_the_refusal{"HermeticGraphLink/AllowHostLibsDowngradesTheRefusal", [] { + if constexpr (!mcpplibs::os::is_linux) skip("the check runs on Linux hosts"); Tmp t; auto tc = fake_driver(t.path, {"/usr/lib"}); auto r = mcpp::build::verify_hermetic_link(tc, "", t.path / "out", true, /*isolatedGraphLink=*/true, {}); - EXPECT_TRUE(r.has_value()) << (r ? "" : r.error()); -} + if (auto&& failed = expect(r.has_value()); !failed) failed << (r ? "" : r.error()); +}}; // The build directory holds the empty graph sysroot, and a package may add a // package-relative `-L` whose root is anywhere (a path dependency). -TEST(HermeticGraphLink, TheBuildAndTheGraphsPackagesAreAllowed) { - if constexpr (!mcpplibs::os::is_linux) GTEST_SKIP() << "the check runs on Linux hosts"; +UTest the_build_and_the_graphs_packages_are_allowed{"HermeticGraphLink/TheBuildAndTheGraphsPackagesAreAllowed", [] { + if constexpr (!mcpplibs::os::is_linux) skip("the check runs on Linux hosts"); Tmp t; const auto out = t.path / "out"; const auto pathDep = t.path / "elsewhere" / "openkal-musl"; @@ -86,16 +87,16 @@ TEST(HermeticGraphLink, TheBuildAndTheGraphsPackagesAreAllowed) { (t.path / "xpkgs" / "musl" / "lib").string()}); auto r = mcpp::build::verify_hermetic_link(tc, "", out, false, /*isolatedGraphLink=*/true, {pathDep}); - EXPECT_TRUE(r.has_value()) << (r ? "" : r.error()); -} + if (auto&& failed = expect(r.has_value()); !failed) failed << (r ? "" : r.error()); +}}; // A payload link keeps its payload's directories; the `-L` rule is for the // isolated graph link only. -TEST(HermeticGraphLink, APayloadLinkIsNotHeldToTheGraphRule) { - if constexpr (!mcpplibs::os::is_linux) GTEST_SKIP() << "the check runs on Linux hosts"; +UTest a_payload_link_is_not_held_to_the_graph_rule{"HermeticGraphLink/APayloadLinkIsNotHeldToTheGraphRule", [] { + if constexpr (!mcpplibs::os::is_linux) skip("the check runs on Linux hosts"); Tmp t; auto tc = fake_driver(t.path, {"/usr/lib"}); auto r = mcpp::build::verify_hermetic_link(tc, "", t.path / "out", false, /*isolatedGraphLink=*/false, {}); - EXPECT_TRUE(r.has_value()) << (r ? "" : r.error()); -} + if (auto&& failed = expect(r.has_value()); !failed) failed << (r ? "" : r.error()); +}}; diff --git a/tests/unit/test_home.cpp b/tests/unit/test_home.cpp index 872cb479a..690242c6d 100644 --- a/tests/unit/test_home.cpp +++ b/tests/unit/test_home.cpp @@ -1,7 +1,7 @@ -#include #include import std; +import mcpplibs.mtest; import mcpp.invocation; import mcpp.home; import mcpplibs.os.platform; @@ -10,6 +10,8 @@ import mcpp.toolchain.stdmod; import mcpp.config; import mcpp.libs.toml; +using namespace mcpplibs::mtest; + namespace { // Set/restore an environment variable for the duration of a test. @@ -23,79 +25,75 @@ class ScopedEnv { } // namespace -TEST(Home, ExplicitMcppHomeWins) { +UTest explicit_mcpp_home_wins{"Home/ExplicitMcppHomeWins", [] { ScopedEnv home("MCPP_HOME", "/tmp/mcpp-home-test"); const auto expected = std::filesystem::weakly_canonical(std::filesystem::absolute("/tmp/mcpp-home-test")); - EXPECT_EQ(mcpp::home::root(), expected); - EXPECT_EQ(mcpp::home::cache_root(), - expected / "build-cache" / "v1"); -} + expect(eq(mcpp::home::root(), expected)); + expect(eq(mcpp::home::cache_root(), expected / "build-cache" / "v1")); +}}; -TEST(Home, CacheRootIsAlwaysRootSlashBuildCacheV1) { +UTest cache_root_is_always_root_slash_build_cache_v1{"Home/CacheRootIsAlwaysRootSlashBuildCacheV1", [] { ScopedEnv home("MCPP_HOME", "/tmp/mcpp-home-test-2"); - EXPECT_EQ(mcpp::home::cache_root(), mcpp::home::root() / "build-cache" / "v1"); -} + expect(eq(mcpp::home::cache_root(), mcpp::home::root() / "build-cache" / "v1")); +}}; // The layout version has to be a path segment, not an implicit convention: // replacing the layout must never require migrating or deleting the old tree. -TEST(Home, CacheRootCarriesTheLayoutVersion) { +UTest cache_root_carries_the_layout_version{"Home/CacheRootCarriesTheLayoutVersion", [] { ScopedEnv home("MCPP_HOME", "/tmp/mcpp-home-test-2b"); - EXPECT_EQ(mcpp::home::cache_root().filename(), std::filesystem::path("v1")); - EXPECT_EQ(mcpp::home::cache_root().parent_path().filename(), - std::filesystem::path("build-cache")); + expect(eq(mcpp::home::cache_root().filename(), std::filesystem::path("v1"))); + expect(eq(mcpp::home::cache_root().parent_path().filename(), std::filesystem::path("build-cache"))); // Must NOT nest inside /cache: GlobalConfig's index metadata cache // owns that name and its reset path removes the whole directory. - EXPECT_NE(mcpp::home::cache_root().parent_path().filename(), - std::filesystem::path("cache")); + expect(ne(mcpp::home::cache_root().parent_path().filename(), std::filesystem::path("cache"))); // The pre-v1 root must stay resolvable and must be a DIFFERENT tree, so // `doctor` can report it and `cache clean --legacy` can reclaim it. - EXPECT_EQ(mcpp::home::legacy_bmi_root(), mcpp::home::root() / "bmi"); - EXPECT_NE(mcpp::home::legacy_bmi_root(), mcpp::home::cache_root()); -} + expect(eq(mcpp::home::legacy_bmi_root(), mcpp::home::root() / "bmi")); + expect(ne(mcpp::home::legacy_bmi_root(), mcpp::home::cache_root())); +}}; // The user-home fallback must land on `.mcpp`, never on the legacy flat // `.mcpp-bmi` sibling that the pre-#311 std-module resolver produced whenever // neither MCPP_HOME nor HOME resolved (the default on Windows PowerShell). -TEST(Home, FallbackUsesDotMcppNotLegacyFlatDir) { +UTest fallback_uses_dot_mcpp_not_legacy_flat_dir{"Home/FallbackUsesDotMcppNotLegacyFlatDir", [] { ScopedEnv mcppHome("MCPP_HOME", nullptr); ScopedEnv userHome("HOME", nullptr); #if defined(_WIN32) ScopedEnv profile("USERPROFILE", nullptr); #endif auto root = mcpp::home::root(); - EXPECT_EQ(root.filename(), std::filesystem::path(".mcpp")); - EXPECT_NE(mcpp::home::cache_root().filename(), - std::filesystem::path(".mcpp-bmi")); -} + expect(eq(root.filename(), std::filesystem::path(".mcpp"))); + expect(ne(mcpp::home::cache_root().filename(), std::filesystem::path(".mcpp-bmi"))); +}}; // #311's D2 regression gate: the std module cache and the dep cache must // resolve to ONE root. `default_cache_root()` used to be a private copy of the // home resolution that drifted (no USERPROFILE, no self-contained detection), // so on Windows the std BMI landed in the current working directory. -TEST(Home, StdModuleCacheRootEqualsHomeCacheRoot) { +UTest std_module_cache_root_equals_home_cache_root{"Home/StdModuleCacheRootEqualsHomeCacheRoot", [] { { ScopedEnv home("MCPP_HOME", "/tmp/mcpp-home-test-3"); - EXPECT_EQ(mcpp::toolchain::default_cache_root(), mcpp::home::cache_root()); + expect(eq(mcpp::toolchain::default_cache_root(), mcpp::home::cache_root())); } { ScopedEnv mcppHome("MCPP_HOME", nullptr); - EXPECT_EQ(mcpp::toolchain::default_cache_root(), mcpp::home::cache_root()); + expect(eq(mcpp::toolchain::default_cache_root(), mcpp::home::cache_root())); } -} +}}; namespace { -class HomeSelection : public ::testing::Test { +class HomeSelection { protected: std::filesystem::path scratch = std::filesystem::temp_directory_path() / std::format("mcpp-home-{}", std::chrono::steady_clock::now().time_since_epoch().count()); std::filesystem::path original = scratch / "original"; std::filesystem::path target = scratch / "target"; ScopedEnv env{"MCPP_HOME", mcpp::home::spelling(original).c_str()}; - void SetUp() override { + void SetUp() { std::filesystem::create_directories(original); std::filesystem::create_directories(target); } - void TearDown() override { + void TearDown() { std::error_code ec; std::filesystem::remove_all(scratch, ec); } @@ -106,111 +104,185 @@ class HomeSelection : public ::testing::Test { }; } -TEST_F(HomeSelection, DefaultOriginalAndEffectiveAreIdentical) { +UTest default_original_and_effective_are_identical{"HomeSelection/DefaultOriginalAndEffectiveAreIdentical", [] { + struct Case : HomeSelection { + Case() { SetUp(); } + ~Case() { TearDown(); } + void body() { const auto selected = mcpp::home::selection(); - EXPECT_EQ(selected.original, std::filesystem::weakly_canonical(original)); - EXPECT_EQ(selected.original, selected.effective); - EXPECT_FALSE(selected.redirected); + expect(eq(selected.original, std::filesystem::weakly_canonical(original))); + expect(eq(selected.original, selected.effective)); + expect(!selected.redirected); } + } fixture; + fixture.body(); +}}; -TEST_F(HomeSelection, EnvironmentSelectsTheLocatorAndOnlyOneHopIsRead) { +UTest environment_selects_the_locator_and_only_one_hop_is_read{"HomeSelection/EnvironmentSelectsTheLocatorAndOnlyOneHopIsRead", [] { + struct Case : HomeSelection { + Case() { SetUp(); } + ~Case() { TearDown(); } + void body() { const auto third = scratch / "third"; std::filesystem::create_directories(third); locator(original, target); locator(target, third); const auto selected = mcpp::home::selection(); - EXPECT_EQ(selected.original, std::filesystem::weakly_canonical(original)); - EXPECT_EQ(selected.effective, std::filesystem::weakly_canonical(target)); - EXPECT_EQ(mcpp::home::cache_root(), selected.effective / "build-cache" / "v1"); - EXPECT_TRUE(selected.redirected); + expect(eq(selected.original, std::filesystem::weakly_canonical(original))); + expect(eq(selected.effective, std::filesystem::weakly_canonical(target))); + expect(eq(mcpp::home::cache_root(), selected.effective / "build-cache" / "v1")); + expect(bool(selected.redirected)); } + } fixture; + fixture.body(); +}}; -TEST_F(HomeSelection, UnavailableTargetIsAnErrorInsteadOfAFallback) { +UTest unavailable_target_is_an_error_instead_of_a_fallback{"HomeSelection/UnavailableTargetIsAnErrorInsteadOfAFallback", [] { + struct Case : HomeSelection { + Case() { SetUp(); } + ~Case() { TearDown(); } + void body() { locator(original, scratch / "missing"); const auto selected = mcpp::home::inspect(original); - ASSERT_FALSE(selected); - EXPECT_NE(selected.error().find("unavailable"), std::string::npos); - EXPECT_FALSE(std::filesystem::exists(original / "config.toml")); + expect(fatal(!selected)); + expect(ne(selected.error().find("unavailable"), std::string::npos)); + expect(!std::filesystem::exists(original / "config.toml")); } + } fixture; + fixture.body(); +}}; -TEST_F(HomeSelection, ForceCanRepairAMalformedLocator) { +UTest force_can_repair_a_malformed_locator{"HomeSelection/ForceCanRepairAMalformedLocator", [] { + struct Case : HomeSelection { + Case() { SetUp(); } + ~Case() { TearDown(); } + void body() { std::ofstream(original / "home.toml") << "home = [\n"; - EXPECT_FALSE(mcpp::home::choose(target, false)); + expect(!mcpp::home::choose(target, false)); const auto selected = mcpp::home::choose(target, true); - ASSERT_TRUE(selected) << selected.error(); - EXPECT_TRUE(mcpp::home::save(*selected)); + if (auto&& failed = expect(fatal(bool(selected))); !failed) failed << selected.error(); + expect(bool(mcpp::home::save(*selected))); const auto restored = mcpp::home::inspect(original); - ASSERT_TRUE(restored) << restored.error(); - EXPECT_EQ(restored->effective, std::filesystem::weakly_canonical(target)); + if (auto&& failed = expect(fatal(bool(restored))); !failed) failed << restored.error(); + expect(eq(restored->effective, std::filesystem::weakly_canonical(target))); } + } fixture; + fixture.body(); +}}; -TEST_F(HomeSelection, ExistingHomeNeedsForceAndChangingItKeepsBothDataTrees) { +UTest existing_home_needs_force_and_changing_it_keeps_both_data_trees{"HomeSelection/ExistingHomeNeedsForceAndChangingItKeepsBothDataTrees", [] { + struct Case : HomeSelection { + Case() { SetUp(); } + ~Case() { TearDown(); } + void body() { std::ofstream(original / "config.toml") << "# existing\n"; std::ofstream(target / "sentinel") << "keep\n"; - EXPECT_FALSE(mcpp::home::choose(target, false)); + expect(!mcpp::home::choose(target, false)); const auto selected = mcpp::home::choose(target, true); - ASSERT_TRUE(selected) << selected.error(); - ASSERT_TRUE(mcpp::home::save(*selected)); - EXPECT_TRUE(std::filesystem::exists(original / "config.toml")); - EXPECT_TRUE(std::filesystem::exists(target / "sentinel")); - EXPECT_EQ(mcpp::invocation::get("MCPP_HOME").value_or(""), mcpp::home::spelling(original)); + if (auto&& failed = expect(fatal(bool(selected))); !failed) failed << selected.error(); + expect(fatal(bool(mcpp::home::save(*selected)))); + expect(std::filesystem::exists(original / "config.toml")); + expect(std::filesystem::exists(target / "sentinel")); + expect(eq(mcpp::invocation::get("MCPP_HOME").value_or(""), mcpp::home::spelling(original))); const auto restored = mcpp::home::inspect(original); - ASSERT_TRUE(restored); - EXPECT_EQ(restored->previous, std::filesystem::weakly_canonical(original)); + expect(fatal(bool(restored))); + expect(eq(restored->previous, std::filesystem::weakly_canonical(original))); } + } fixture; + fixture.body(); +}}; -TEST_F(HomeSelection, ReturningToOriginalRemovesOnlyTheLocator) { +UTest returning_to_original_removes_only_the_locator{"HomeSelection/ReturningToOriginalRemovesOnlyTheLocator", [] { + struct Case : HomeSelection { + Case() { SetUp(); } + ~Case() { TearDown(); } + void body() { locator(original, target); std::ofstream(target / "config.toml") << "# existing\n"; const auto selected = mcpp::home::choose(original, true); - ASSERT_TRUE(selected) << selected.error(); - ASSERT_TRUE(mcpp::home::save(*selected)); - EXPECT_FALSE(std::filesystem::exists(original / "home.toml")); - EXPECT_TRUE(std::filesystem::exists(target / "config.toml")); - EXPECT_EQ(mcpp::home::root(), std::filesystem::weakly_canonical(original)); + if (auto&& failed = expect(fatal(bool(selected))); !failed) failed << selected.error(); + expect(fatal(bool(mcpp::home::save(*selected)))); + expect(!std::filesystem::exists(original / "home.toml")); + expect(std::filesystem::exists(target / "config.toml")); + expect(eq(mcpp::home::root(), std::filesystem::weakly_canonical(original))); } + } fixture; + fixture.body(); +}}; -TEST_F(HomeSelection, NestedDirectoriesAreRefused) { - EXPECT_FALSE(mcpp::home::choose(original / "nested", true)); - EXPECT_FALSE(mcpp::home::choose(scratch, true)); +UTest nested_directories_are_refused{"HomeSelection/NestedDirectoriesAreRefused", [] { + struct Case : HomeSelection { + Case() { SetUp(); } + ~Case() { TearDown(); } + void body() { + expect(!mcpp::home::choose(original / "nested", true)); + expect(!mcpp::home::choose(scratch, true)); } + } fixture; + fixture.body(); +}}; -TEST_F(HomeSelection, ReadOnlyConfigurationDoesNotInitializeOrAcquireAProvider) { +UTest read_only_configuration_does_not_initialize_or_acquire_a_provider{"HomeSelection/ReadOnlyConfigurationDoesNotInitializeOrAcquireAProvider", [] { + struct Case : HomeSelection { + Case() { SetUp(); } + ~Case() { TearDown(); } + void body() { const auto cold = scratch / "cold"; ScopedEnv home("MCPP_HOME", mcpp::home::spelling(cold).c_str()); const auto config = mcpp::config::load_or_init(true, {}, {}, mcpp::config::Initialization::ReadOnly); - ASSERT_TRUE(config) << config.error().message; - EXPECT_EQ(config->mcppHome, std::filesystem::weakly_canonical(cold)); - EXPECT_FALSE(std::filesystem::exists(cold)); + if (auto&& failed = expect(fatal(bool(config))); !failed) failed << config.error().message; + expect(eq(config->mcppHome, std::filesystem::weakly_canonical(cold))); + expect(!std::filesystem::exists(cold)); } + } fixture; + fixture.body(); +}}; -TEST_F(HomeSelection, UserSourceSurvivesOriginalPathNormalization) { +UTest user_source_survives_original_path_normalization{"HomeSelection/UserSourceSurvivesOriginalPathNormalization", [] { + struct Case : HomeSelection { + Case() { SetUp(); } + ~Case() { TearDown(); } + void body() { const auto user = mcpp::home::spelling(scratch); ScopedEnv env("MCPP_HOME", nullptr); ScopedEnv posixHome("HOME", user.c_str()); ScopedEnv windowsHome("USERPROFILE", user.c_str()); const auto selected = mcpp::home::inspect(scratch / ".mcpp"); - ASSERT_TRUE(selected) << selected.error(); - EXPECT_EQ(selected->source, "user"); + if (auto&& failed = expect(fatal(bool(selected))); !failed) failed << selected.error(); + expect(eq(selected->source, "user")); } + } fixture; + fixture.body(); +}}; -TEST_F(HomeSelection, MissingUserDirectoryCannotPublishALocatorInTheWorkingDirectory) { +UTest missing_user_directory_cannot_publish_a_locator_in_the_working_directory{"HomeSelection/MissingUserDirectoryCannotPublishALocatorInTheWorkingDirectory", [] { + struct Case : HomeSelection { + Case() { SetUp(); } + ~Case() { TearDown(); } + void body() { ScopedEnv explicitHome("MCPP_HOME", nullptr); ScopedEnv posixHome("HOME", nullptr); ScopedEnv windowsHome("USERPROFILE", nullptr); const auto selected = mcpp::home::choose(target, true); - ASSERT_FALSE(selected); - EXPECT_NE(selected.error().find("user directory"), std::string::npos); + expect(fatal(!selected)); + expect(ne(selected.error().find("user directory"), std::string::npos)); } + } fixture; + fixture.body(); +}}; -TEST_F(HomeSelection, UnicodeLocatorRoundTripDoesNotNarrowTheSelectedPath) { +UTest unicode_locator_round_trip_does_not_narrow_the_selected_path{"HomeSelection/UnicodeLocatorRoundTripDoesNotNarrowTheSelectedPath", [] { + struct Case : HomeSelection { + Case() { SetUp(); } + ~Case() { TearDown(); } + void body() { if constexpr (mcpplibs::os::is_windows) { const auto acp = mcpplibs::os::windows::code_page(); - ASSERT_NE(acp, 0u); + expect(fatal(ne(acp, 0u))); if (acp == 65001) - GTEST_SKIP() << "UTF-8 ACP cannot prove the legacy-code-page path conversion boundary; " - "the actual CLI Unicode case still runs"; + skip([&] { std::ostringstream text; text << "UTF-8 ACP cannot prove the legacy-code-page path conversion boundary; " + "the actual CLI Unicode case still runs"; return text.str(); }()); } // Devanagari cannot be represented by any legacy Windows ANSI code page. // Explicit UTF-8 code units keep this independent of compiler source encoding. @@ -218,10 +290,13 @@ TEST_F(HomeSelection, UnicodeLocatorRoundTripDoesNotNarrowTheSelectedPath) { std::u8string(u8"selected space \u0915\u0916\u0917 \u65e5\u672c\u8a9e")); std::filesystem::create_directory(unicode); const auto selected = mcpp::home::choose(unicode, true); - ASSERT_TRUE(selected) << selected.error(); - ASSERT_TRUE(mcpp::home::save(*selected)); + if (auto&& failed = expect(fatal(bool(selected))); !failed) failed << selected.error(); + expect(fatal(bool(mcpp::home::save(*selected)))); const auto restored = mcpp::home::inspect(original); - ASSERT_TRUE(restored) << restored.error(); - EXPECT_EQ(restored->effective, std::filesystem::weakly_canonical(unicode)); - EXPECT_EQ(mcpp::home::from_utf8(mcpp::home::spelling(unicode)), unicode); + if (auto&& failed = expect(fatal(bool(restored))); !failed) failed << restored.error(); + expect(eq(restored->effective, std::filesystem::weakly_canonical(unicode))); + expect(eq(mcpp::home::from_utf8(mcpp::home::spelling(unicode)), unicode)); } + } fixture; + fixture.body(); +}}; diff --git a/tests/unit/test_hostflags.cpp b/tests/unit/test_hostflags.cpp index d8ab38e14..efe6b39ba 100644 --- a/tests/unit/test_hostflags.cpp +++ b/tests/unit/test_hostflags.cpp @@ -1,13 +1,15 @@ -#include #include import std; +import mcpplibs.mtest; import mcpplibs.os.platform; import mcpp.toolchain.dialect; import mcpp.toolchain.hostflags; import mcpp.toolchain.linkmodel; import mcpp.toolchain.model; +using namespace mcpplibs::mtest; + using mcpp::toolchain::CompilerId; using mcpp::toolchain::HostFlagOptions; @@ -39,29 +41,29 @@ constexpr CompilerId kFamilies[] = { // turns the principle into a compile-and-run check: a family that is missing // a dialect row, a module row, or host flags fails HERE, not at a user's // `mcpp build`. -TEST(HostFlags, EveryFamilyHasCompleteTables) { +UTest every_family_has_complete_tables{"HostFlags/EveryFamilyHasCompleteTables", [] { for (auto id : kFamilies) { auto tc = tc_for(id); const auto& d = mcpp::toolchain::dialect_for(tc); const auto t = mcpp::toolchain::bmi_traits(tc); - EXPECT_FALSE(d.id.empty()); + expect(!d.id.empty()); // Needed to compile a `.mcpp` at all: the extension is unknown to // every driver, so the language has to be forced. - EXPECT_FALSE(d.forceCxxLangArgv.empty()) << d.id; + expect(!d.forceCxxLangArgv.empty()) << d.id; // Needed to name the produced program. - EXPECT_FALSE(d.outputExePrefix.empty()) << d.id; - EXPECT_FALSE(d.objExt.empty()) << d.id; + expect(!d.outputExePrefix.empty()) << d.id; + expect(!d.objExt.empty()) << d.id; // Needed to build and reference the bundled `mcpp` module. - EXPECT_FALSE(t.bmiDir.empty()) << d.id; - EXPECT_FALSE(t.bmiExt.empty()) << d.id; + expect(!t.bmiDir.empty()) << d.id; + expect(!t.bmiExt.empty()) << d.id; } -} +}}; // The producer must answer for every family too — MSVC deliberately returns // nothing (cl.exe finds headers and libs through INCLUDE/LIB, not argv), but // it must be a decision, not a crash or an accident. -TEST(HostFlags, ProducerAnswersForEveryFamily) { +UTest producer_answers_for_every_family{"HostFlags/ProducerAnswersForEveryFamily", [] { HostFlagOptions opt; for (auto id : kFamilies) { auto tc = tc_for(id); @@ -70,15 +72,15 @@ TEST(HostFlags, ProducerAnswersForEveryFamily) { auto link = mcpp::toolchain::host_link_tokens( tc, opt, mcpp::toolchain::no_escape); if (id == CompilerId::MSVC) { - EXPECT_TRUE(compile.empty()); - EXPECT_TRUE(link.empty()); + expect(compile.empty()); + expect(link.empty()); } // No empty tokens anywhere: an empty argv element is an argument the // driver still has to interpret. - for (auto const& t : compile) EXPECT_FALSE(t.empty()); - for (auto const& t : link) EXPECT_FALSE(t.empty()); + for (auto const& t : compile) expect(!t.empty()); + for (auto const& t : link) expect(!t.empty()); } -} +}}; // ── The rendered string must not move ─────────────────────────────────────── // @@ -89,26 +91,24 @@ TEST(HostFlags, ProducerAnswersForEveryFamily) { // detail. This pins it; the first attempt at this refactor moved // `-stdlib=libc++` after the include flags and would have shipped exactly // that invalidation. -TEST(HostFlags, ClangCfgBypassStringIsStable) { +UTest clang_cfg_bypass_string_is_stable{"HostFlags/ClangCfgBypassStringIsStable", [] { mcpp::toolchain::ClangDriverModel dm; dm.hasCfg = true; dm.cxxIncludes = { "/llvm/include/c++/v1", "/llvm/include/tgt/c++/v1" }; - EXPECT_EQ(dm.compile_flags(mcpp::toolchain::no_escape), - " --no-default-config -nostdinc++" + expect(eq(dm.compile_flags(mcpp::toolchain::no_escape), " --no-default-config -nostdinc++" " -isystem/llvm/include/c++/v1" - " -isystem/llvm/include/tgt/c++/v1"); + " -isystem/llvm/include/tgt/c++/v1")); // With the stdlib selection the std module has always asked for, it lands // immediately after -nostdinc++ — not at the end. - EXPECT_EQ(mcpp::toolchain::render_tokens( - dm.compile_tokens(mcpp::toolchain::no_escape, true)), - " --no-default-config -nostdinc++ -stdlib=libc++" + expect(eq(mcpp::toolchain::render_tokens( + dm.compile_tokens(mcpp::toolchain::no_escape, true)), " --no-default-config -nostdinc++ -stdlib=libc++" " -isystem/llvm/include/c++/v1" - " -isystem/llvm/include/tgt/c++/v1"); -} + " -isystem/llvm/include/tgt/c++/v1")); +}}; -TEST(HostFlags, LinkModelStringsAreStable) { +UTest link_model_strings_are_stable{"HostFlags/LinkModelStringsAreStable", [] { mcpp::toolchain::ToolchainLinkModel lm; lm.mode = mcpp::toolchain::CLibMode::PayloadFirst; lm.clangDriver = true; @@ -117,35 +117,32 @@ TEST(HostFlags, LinkModelStringsAreStable) { lm.loader = "/glibc/lib/ld.so"; lm.systemIncludes = { "/glibc/include" }; - EXPECT_EQ(lm.compile_flags(mcpp::toolchain::no_escape), - " -nostdlibinc -isystem/glibc/include"); - EXPECT_EQ(lm.link_flags(mcpp::toolchain::no_escape), - " -B/glibc/lib -L/glibc/lib -Wl,-rpath,/glibc/lib" - " -Wl,--dynamic-linker=/glibc/lib/ld.so"); + expect(eq(lm.compile_flags(mcpp::toolchain::no_escape), " -nostdlibinc -isystem/glibc/include")); + expect(eq(lm.link_flags(mcpp::toolchain::no_escape), " -B/glibc/lib -L/glibc/lib -Wl,-rpath,/glibc/lib" + " -Wl,--dynamic-linker=/glibc/lib/ld.so")); // GCC takes -idirafter so libstdc++'s #include_next wrappers can still // reach libc. lm.clangDriver = false; - EXPECT_EQ(lm.compile_flags(mcpp::toolchain::no_escape), - " -idirafter/glibc/include"); -} + expect(eq(lm.compile_flags(mcpp::toolchain::no_escape), " -idirafter/glibc/include")); +}}; // ── bmi_reference_tokens ──────────────────────────────────────────────────── // // The traits store these for the ninja STRING channel, where one word vs two // makes no difference. argv consumers cannot be that relaxed. -TEST(HostFlags, BmiReferenceSplitsOnlyWhenTheSpellingHasASpace) { +UTest bmi_reference_splits_only_when_the_spelling_has_a_space{"HostFlags/BmiReferenceSplitsOnlyWhenTheSpellingHasASpace", [] { auto gnu = mcpp::toolchain::bmi_reference_tokens( " -fmodule-file=std=", std::filesystem::path("/tmp/std.pcm")); - ASSERT_EQ(gnu.size(), 1u); - EXPECT_EQ(gnu[0], "-fmodule-file=std=/tmp/std.pcm"); + expect(fatal(eq(gnu.size(), 1u))); + expect(eq(gnu[0], "-fmodule-file=std=/tmp/std.pcm")); auto msvc = mcpp::toolchain::bmi_reference_tokens( " /reference std=", std::filesystem::path("/tmp/std.ifc")); - ASSERT_EQ(msvc.size(), 2u); - EXPECT_EQ(msvc[0], "/reference"); - EXPECT_EQ(msvc[1], "std=/tmp/std.ifc"); -} + expect(fatal(eq(msvc.size(), 2u))); + expect(eq(msvc[0], "/reference")); + expect(eq(msvc[1], "std=/tmp/std.ifc")); +}}; // The general invariant, checked for every family: an argv element must never // contain a space. This bug has now appeared three times in the same shape — @@ -153,33 +150,33 @@ TEST(HostFlags, BmiReferenceSplitsOnlyWhenTheSpellingHasASpace) { // a table entry written for the ninja STRING channel was concatenated into an // argv element. cl.exe answers with "could not find module 'std'", which // names neither the flag nor the reason. -TEST(HostFlags, BmiReferencesNeverProduceATokenWithASpace) { +UTest bmi_references_never_produce_a_token_with_a_space{"HostFlags/BmiReferencesNeverProduceATokenWithASpace", [] { for (auto id : kFamilies) { auto tc = tc_for(id); auto t = mcpp::toolchain::bmi_traits(tc); for (auto prefix : { t.stdBmiUsePrefix, t.stdCompatBmiUsePrefix }) { for (auto const& tok : mcpp::toolchain::bmi_reference_tokens( prefix, std::filesystem::path("/tmp/x.bmi"))) { - EXPECT_EQ(tok.find(' '), std::string::npos) + expect(eq(tok.find(' '), std::string::npos)) << "family " << mcpp::toolchain::dialect_for(tc).id << " token: " << tok; } } } -} +}}; // Same invariant for the language-force spelling, which has both a positional // and a per-file form. -TEST(HostFlags, LanguageForceTokensNeverContainASpace) { +UTest language_force_tokens_never_contain_a_space{"HostFlags/LanguageForceTokensNeverContainASpace", [] { for (auto const* d : { &mcpp::toolchain::gnu_dialect(), &mcpp::toolchain::msvc_dialect() }) { for (auto f : d->forceCxxLangArgv) - EXPECT_EQ(f.find(' '), std::string_view::npos) << d->id << ": " << f; + expect(eq(f.find(' '), std::string_view::npos)) << d->id << ": " << f; for (auto f : d->alwaysFlagsArgv) - EXPECT_EQ(f.find(' '), std::string_view::npos) << d->id << ": " << f; - EXPECT_EQ(d->perFileCxxPrefix.find(' '), std::string_view::npos) << d->id; + expect(eq(f.find(' '), std::string_view::npos)) << d->id << ": " << f; + if (auto&& failed = expect(eq(d->perFileCxxPrefix.find(' '), std::string_view::npos)); !failed) failed << d->id; } -} +}}; // ── orphaned_reference (#604) ─────────────────────────────────────────────── // @@ -193,62 +190,61 @@ TEST(HostFlags, LanguageForceTokensNeverContainASpace) { // // The de-duplicator is gone. This is the invariant that says so, and the // diagnostic that would name the cause if it ever returns. -TEST(HostFlags, AnOrphanedModuleReferenceIsDetected) { +UTest an_orphaned_module_reference_is_detected{"HostFlags/AnOrphanedModuleReferenceIsDetected", [] { using mcpp::toolchain::orphaned_reference; // The defect, verbatim: two references appended, one switch surviving. - EXPECT_EQ(orphaned_reference({"cl.exe", "/std:c++20", "/reference", + expect(eq(orphaned_reference({"cl.exe", "/std:c++20", "/reference", "mcpp=C:/b/mcpp.ifc", "rules.sources=C:/b/rules.sources.ifc", - "/c", "build.mcpp"}), - std::optional{"rules.sources=C:/b/rules.sources.ifc"}); + "/c", "build.mcpp"}), std::optional{"rules.sources=C:/b/rules.sources.ifc"})); // The same argv with both switches present is well formed. - EXPECT_FALSE(orphaned_reference({"cl.exe", "/std:c++20", "/reference", + expect(!(orphaned_reference({"cl.exe", "/std:c++20", "/reference", "mcpp=C:/b/mcpp.ifc", "/reference", "rules.sources=C:/b/rules.sources.ifc", - "/c", "build.mcpp"}).has_value()); + "/c", "build.mcpp"}).has_value())); // First position is an orphan too -- there is nothing in front of it. - EXPECT_TRUE(orphaned_reference({"mcpp=C:/b/mcpp.ifc"}).has_value()); -} + expect(orphaned_reference({"mcpp=C:/b/mcpp.ifc"}).has_value()); +}}; // The rule must not fire on the two bare tokens that legitimately appear. -TEST(HostFlags, OrphanRuleAcceptsInputPathsAndOneWordReferences) { +UTest orphan_rule_accepts_input_paths_and_one_word_references{"HostFlags/OrphanRuleAcceptsInputPathsAndOneWordReferences", [] { using mcpp::toolchain::orphaned_reference; // Clang's form is ONE token and carries its own switch, so it can never be // orphaned -- which is exactly why the defect was MSVC-only. - EXPECT_FALSE(orphaned_reference( + expect(!(orphaned_reference( {"clang++", "-fmodule-file=mcpp=/b/mcpp.pcm", "-fmodule-file=rules.sources=/b/rules.sources.pcm", - "-c", "build.mcpp"}).has_value()); + "-c", "build.mcpp"}).has_value())); // GCC names nothing at all. - EXPECT_FALSE(orphaned_reference( - {"g++", "-fmodules", "-fmodules", "-c", "build.mcpp"}).has_value()); + expect(!(orphaned_reference( + {"g++", "-fmodules", "-fmodules", "-c", "build.mcpp"}).has_value())); // A source or object path that happens to contain `=` is an input, not a // reference: its `=` comes AFTER a directory separator. Without this the // rule would refuse a legal build in a directory a user is allowed to name. - EXPECT_FALSE(orphaned_reference( - {"g++", "/home/me/a=b/build.mcpp"}).has_value()); - EXPECT_FALSE(orphaned_reference( - {"cl.exe", "C:/b/x=y/build.mcpp"}).has_value()); -} + expect(!(orphaned_reference( + {"g++", "/home/me/a=b/build.mcpp"}).has_value())); + expect(!(orphaned_reference( + {"cl.exe", "C:/b/x=y/build.mcpp"}).has_value())); +}}; -TEST(HostFlags, BmiReferenceIsEmptyForAToolchainThatNamesNothing) { +UTest bmi_reference_is_empty_for_a_toolchain_that_names_nothing{"HostFlags/BmiReferenceIsEmptyForAToolchainThatNamesNothing", [] { // GCC finds BMIs implicitly under /gcm.cache — its prefix is empty // and must not produce a stray token. - EXPECT_TRUE(mcpp::toolchain::bmi_reference_tokens( + expect(mcpp::toolchain::bmi_reference_tokens( "", std::filesystem::path("/tmp/x.gcm")).empty()); -} +}}; // ── HostFlagOptions divergences ───────────────────────────────────────────── // // These knobs encode documented differences between the three consumers. A // test so that "why is this optional?" has an answer in code, not only prose. -TEST(HostFlags, CfgBypassLinuxOnlyDiffersFromAlwaysOffLinux) { +UTest cfg_bypass_linux_only_differs_from_always_off_linux{"HostFlags/CfgBypassLinuxOnlyDiffersFromAlwaysOffLinux", [] { auto tc = tc_for(CompilerId::Clang); HostFlagOptions always; always.cfgBypass = HostFlagOptions::CfgBypass::Always; HostFlagOptions linuxOnly; linuxOnly.cfgBypass = HostFlagOptions::CfgBypass::LinuxOnly; @@ -257,10 +253,9 @@ TEST(HostFlags, CfgBypassLinuxOnlyDiffersFromAlwaysOffLinux) { // empty here; the assertion that matters is that the option exists and is // honoured identically on Linux, where the host helper does bypass. if constexpr (mcpplibs::os::is_linux) { - EXPECT_EQ(mcpp::toolchain::host_compile_tokens(tc, always, mcpp::toolchain::no_escape), - mcpp::toolchain::host_compile_tokens(tc, linuxOnly, mcpp::toolchain::no_escape)); + expect(eq(mcpp::toolchain::host_compile_tokens(tc, always, mcpp::toolchain::no_escape), mcpp::toolchain::host_compile_tokens(tc, linuxOnly, mcpp::toolchain::no_escape))); } -} +}}; // ── Which exit of host_link_tokens names the toolchain's runtime dirs ────── // @@ -306,7 +301,7 @@ bool names_dir(const std::vector& tokens, std::string_view dir) { } // namespace -TEST(HostFlags, OnlyTheSpelledOutExitNamesTheToolchainRuntimeDirs) { +UTest only_the_spelled_out_exit_names_the_toolchain_runtime_dirs{"HostFlags/OnlyTheSpelledOutExitNamesTheToolchainRuntimeDirs", [] { FakeClangPayload payload{"runtime-dirs"}; auto tc = tc_for(CompilerId::Clang); tc.binaryPath = payload.root / "bin" / "clang++"; @@ -318,13 +313,13 @@ TEST(HostFlags, OnlyTheSpelledOutExitNamesTheToolchainRuntimeDirs) { // Spelled out: the payload's own runtime directories are named. opt.cfgBypass = HostFlagOptions::CfgBypass::Always; auto spelled = mcpp::toolchain::host_link_tokens(tc, opt, mcpp::toolchain::no_escape); - EXPECT_TRUE(names_dir(spelled, (payload.root / "lib").string())); + expect(bool(names_dir(spelled, (payload.root / "lib").string()))); // Trusting the cfg: they are NOT, and the comment above says what naming // them costs. opt.cfgBypass = HostFlagOptions::CfgBypass::Never; auto trusting = mcpp::toolchain::host_link_tokens(tc, opt, mcpp::toolchain::no_escape); - EXPECT_FALSE(names_dir(trusting, (payload.root / "lib").string())) + expect(!(names_dir(trusting, (payload.root / "lib").string()))) << "the cfg-trusting exit now points `-lc++` at the toolchain's own " "dylib; on Mach-O that link fails on the C++ ABI symbols the system " "libc++ re-exports"; @@ -333,8 +328,8 @@ TEST(HostFlags, OnlyTheSpelledOutExitNamesTheToolchainRuntimeDirs) { opt.runtimeLibDirs = false; opt.cfgBypass = HostFlagOptions::CfgBypass::Always; auto off = mcpp::toolchain::host_link_tokens(tc, opt, mcpp::toolchain::no_escape); - EXPECT_FALSE(names_dir(off, (payload.root / "lib").string())); -} + expect(!(names_dir(off, (payload.root / "lib").string()))); +}}; // TARGET-KEYED, NOT HOST-KEYED (mcpp#685). This used to read // `EXPECT_EQ(found, mcpplibs::os::is_macos)` — the flag's presence @@ -344,7 +339,7 @@ TEST(HostFlags, OnlyTheSpelledOutExitNamesTheToolchainRuntimeDirs) { // non-Apple host silently dropped the flag (and the iOS mirror: a macOS host // building `x86_64-linux-musl` wrongly kept it). Both targets are asked here // so the assertion holds independent of whichever host runs this suite. -TEST(HostFlags, DeploymentTargetOnlyOnAMacosTarget) { +UTest deployment_target_only_on_a_macos_target{"HostFlags/DeploymentTargetOnlyOnAMacosTarget", [] { HostFlagOptions opt; opt.macosDeploymentTarget = "14.0"; @@ -352,7 +347,7 @@ TEST(HostFlags, DeploymentTargetOnlyOnAMacosTarget) { macTc.targetTriple = "aarch64-macos"; auto macTokens = mcpp::toolchain::host_compile_tokens( macTc, opt, mcpp::toolchain::no_escape); - EXPECT_TRUE(std::ranges::any_of(macTokens, [](auto const& t) { + expect(std::ranges::any_of(macTokens, [](auto const& t) { return t == "-mmacosx-version-min=14.0"; })) << "a macOS target must carry the resolved deployment target " "regardless of the host running the build"; @@ -361,11 +356,11 @@ TEST(HostFlags, DeploymentTargetOnlyOnAMacosTarget) { linuxTc.targetTriple = "x86_64-linux-musl"; auto linuxTokens = mcpp::toolchain::host_compile_tokens( linuxTc, opt, mcpp::toolchain::no_escape); - EXPECT_FALSE(std::ranges::any_of(linuxTokens, [](auto const& t) { + expect(!(std::ranges::any_of(linuxTokens, [](auto const& t) { return t.starts_with("-mmacosx-version-min="); - })) << "a non-macOS target must never see a macOS deployment flag, even " + }))) << "a non-macOS target must never see a macOS deployment flag, even " "when built on a macOS host (the mirror defect #685 also fixes)"; -} +}}; // ── graph_runtime_compile_flags: what a `throw` and a `thread_local` compile // into, when the runtime comes from the dependency graph. @@ -401,11 +396,11 @@ bool has(const std::vector& v, std::string_view f) { // runtime supplies its own unwinder with it, and the two cannot be mixed inside // one image. A `thread_local` on PE is reached through `_tls_index`, which the // dynamic loader bootstraps and a self-contained image has no loader for. -TEST(GraphRuntimeFlags, PeTakesDwarfExceptionsAndEmulatedTls) { +UTest pe_takes_dwarf_exceptions_and_emulated_tls{"GraphRuntimeFlags/PeTakesDwarfExceptionsAndEmulatedTls", [] { auto f = mcpp::toolchain::graph_runtime_compile_flags(graph_tc("x86_64-windows-gnu")); - EXPECT_TRUE(has(f, "-fdwarf-exceptions")); - EXPECT_TRUE(has(f, "-femulated-tls")); -} + expect(bool(has(f, "-fdwarf-exceptions"))); + expect(bool(has(f, "-femulated-tls"))); +}}; // Mach-O: the exception mechanism is already DWARF, so only the thread-local // one applies — `_tlv_bootstrap` is the loader-bootstrapped name there. The @@ -413,73 +408,73 @@ TEST(GraphRuntimeFlags, PeTakesDwarfExceptionsAndEmulatedTls) { // definition is coalesced BY THE LOADER, which is machinery a self-contained // image has no use for and which produced a jump to address zero when it was // left in place. -TEST(GraphRuntimeFlags, MachOTakesEmulatedTlsAndHiddenVisibilityButNotDwarf) { +UTest mach_o_takes_emulated_tls_and_hidden_visibility_but_not_dwarf{"GraphRuntimeFlags/MachOTakesEmulatedTlsAndHiddenVisibilityButNotDwarf", [] { auto f = mcpp::toolchain::graph_runtime_compile_flags(graph_tc("aarch64-macos")); - EXPECT_FALSE(has(f, "-fdwarf-exceptions")); - EXPECT_TRUE(has(f, "-femulated-tls")); - EXPECT_TRUE(has(f, "-fvisibility=hidden")); - EXPECT_TRUE(has(f, "-fvisibility-inlines-hidden")); -} + expect(!has(f, "-fdwarf-exceptions")); + expect(bool(has(f, "-femulated-tls"))); + expect(bool(has(f, "-fvisibility=hidden"))); + expect(bool(has(f, "-fvisibility-inlines-hidden"))); +}}; // iOS shares macOS's object format (Mach-O, ld64), so it must share this // exact set of flags. Before `is_mach_o()` replaced `os == "macos"` here, an // iOS triple matched neither the PE nor the macOS branch and this function // silently returned no flags at all for it. -TEST(GraphRuntimeFlags, IosTakesTheSameFlagsAsMacOS) { +UTest ios_takes_the_same_flags_as_mac_os{"GraphRuntimeFlags/IosTakesTheSameFlagsAsMacOS", [] { auto f = mcpp::toolchain::graph_runtime_compile_flags(graph_tc("aarch64-ios")); - EXPECT_FALSE(has(f, "-fdwarf-exceptions")); - EXPECT_TRUE(has(f, "-femulated-tls")); - EXPECT_TRUE(has(f, "-fvisibility=hidden")); - EXPECT_TRUE(has(f, "-fvisibility-inlines-hidden")); -} + expect(!has(f, "-fdwarf-exceptions")); + expect(bool(has(f, "-femulated-tls"))); + expect(bool(has(f, "-fvisibility=hidden"))); + expect(bool(has(f, "-fvisibility-inlines-hidden"))); +}}; // A hosted Mach-O target whose C library is a located SDK (the iOS rows over // `llvm.libcxx`, mcpp#630) has dyld and the native TLS model with it, so the // emulated-TLS row does not apply; the visibility rows still do, since a // second libc++ in one process is kept apart by visibility on Mach-O. -TEST(GraphRuntimeFlags, MachOOverAPrebuiltCLibraryKeepsNativeTls) { +UTest mach_o_over_a_prebuilt_c_library_keeps_native_tls{"GraphRuntimeFlags/MachOOverAPrebuiltCLibraryKeepsNativeTls", [] { auto tc = graph_tc("aarch64-ios"); tc.cAbiPrebuilt = true; auto f = mcpp::toolchain::graph_runtime_compile_flags(tc); - EXPECT_FALSE(has(f, "-femulated-tls")); - EXPECT_FALSE(has(f, "-fdwarf-exceptions")); - EXPECT_TRUE(has(f, "-fvisibility=hidden")); - EXPECT_TRUE(has(f, "-fvisibility-inlines-hidden")); + expect(!has(f, "-femulated-tls")); + expect(!has(f, "-fdwarf-exceptions")); + expect(bool(has(f, "-fvisibility=hidden"))); + expect(bool(has(f, "-fvisibility-inlines-hidden"))); // And PE keeps it regardless: `_tls_index` is loader-bootstrapped there // whether or not the C library is the graph's. auto pe = graph_tc("x86_64-windows-gnu"); pe.cAbiPrebuilt = true; - EXPECT_TRUE(has(mcpp::toolchain::graph_runtime_compile_flags(pe), "-femulated-tls")); -} + expect(bool(has(mcpp::toolchain::graph_runtime_compile_flags(pe), "-femulated-tls"))); +}}; // ELF takes NONE of them, and that is a decision rather than an omission. // There a `thread_local` is a fixed offset from the thread pointer, which the // C library establishes itself; adding the flag would work, cost an // indirection on every access, and make ELF the only target whose thread // locals are laid out differently from every other build of the same target. -TEST(GraphRuntimeFlags, ElfTakesNone) { +UTest elf_takes_none{"GraphRuntimeFlags/ElfTakesNone", [] { auto f = mcpp::toolchain::graph_runtime_compile_flags(graph_tc("x86_64-linux-gnu")); - EXPECT_TRUE(f.empty()); -} + expect(f.empty()); +}}; // And nothing at all when the runtime is NOT the graph's, whatever the // target. The predicate is `targetCxxRuntime`; a native or payload-served build // of the same triple must be untouched. -TEST(GraphRuntimeFlags, PayloadServedTargetTakesNoneEvenOnPe) { +UTest payload_served_target_takes_none_even_on_pe{"GraphRuntimeFlags/PayloadServedTargetTakesNoneEvenOnPe", [] { mcpp::toolchain::Toolchain tc; tc.compiler = CompilerId::Clang; tc.targetTriple = "x86_64-windows-gnu"; tc.targetCxxRuntime = false; - EXPECT_TRUE(mcpp::toolchain::graph_runtime_compile_flags(tc).empty()); -} + expect(mcpp::toolchain::graph_runtime_compile_flags(tc).empty()); +}}; // A triple outside the vocabulary yields nothing rather than a guess: the // answer depends on the object format, and a spelling that cannot be parsed // does not name one. -TEST(GraphRuntimeFlags, UnparseableTripleTakesNone) { - EXPECT_TRUE(mcpp::toolchain::graph_runtime_compile_flags( +UTest unparseable_triple_takes_none{"GraphRuntimeFlags/UnparseableTripleTakesNone", [] { + expect(mcpp::toolchain::graph_runtime_compile_flags( graph_tc("not-a-triple-at-all")).empty()); -} +}}; // `--no-default-config` IS NOT PART OF THE PAYLOAD'S HEADER SET, AND WAS // BEING SUPPRESSED WITH IT. @@ -494,7 +489,7 @@ TEST(GraphRuntimeFlags, UnparseableTripleTakesNone) { // token, so clang read `bin/clang++.cfg`. That is also what made a hand-written // `-clang++.cfg` a working workaround for mcpp#514 — a workaround that // only existed because this token went missing. -TEST(HostFlags, TheCfgBypassSurvivesAGraphSuppliedTargetSide) { +UTest the_cfg_bypass_survives_a_graph_supplied_target_side{"HostFlags/TheCfgBypassSurvivesAGraphSuppliedTargetSide", [] { // A FIXTURE, NOT THE MACHINE'S OWN TOOLCHAIN. The first draft used a // synthetic `Toolchain` with no `binaryPath`, so `resolve_clang_driver` // reported no cfg and the whole test SKIPPED — a check that asserts @@ -520,7 +515,7 @@ TEST(HostFlags, TheCfgBypassSurvivesAGraphSuppliedTargetSide) { auto tc = tc_for(CompilerId::Clang); tc.binaryPath = root / "bin" / "clang++"; - ASSERT_TRUE(mcpp::toolchain::resolve_clang_driver(tc).hasCfg) + expect(fatal(bool(mcpp::toolchain::resolve_clang_driver(tc).hasCfg))) << "the fixture did not produce a cfg — the assertions below would be vacuous"; const auto has = [](const std::vector& v, std::string_view f) { @@ -542,13 +537,13 @@ TEST(HostFlags, TheCfgBypassSurvivesAGraphSuppliedTargetSide) { // The bypass is emitted on BOTH sides: the cfg is a per-machine, // per-install-path artifact, and reading it makes the command line depend // on what happened to be installed when the payload landed. - EXPECT_TRUE(has(a, "--no-default-config")); - EXPECT_TRUE(has(b, "--no-default-config")); + expect(bool(has(a, "--no-default-config"))); + expect(bool(has(b, "--no-default-config"))); // ...while the payload's own C++ headers stay withheld from the graph side, // which is the distinction this splits apart. - EXPECT_TRUE(has(a, "-nostdinc++")); - EXPECT_FALSE(has(b, "-nostdinc++")); -} + expect(bool(has(a, "-nostdinc++"))); + expect(!has(b, "-nostdinc++")); +}}; // "A TOOLCHAIN THAT SHIPS ITS OWN SYSROOT IS TOLD NOTHING" WAS ONE TOKEN TOO // STRONG, AND THIS FUNCTION ALREADY SAID SO FURTHER DOWN. @@ -573,7 +568,7 @@ TEST(HostFlags, TheCfgBypassSurvivesAGraphSuppliedTargetSide) { // // followed by eight cascading "use of undeclared identifier 'std'" lines, // which is what a reader sees first. -TEST(HostFlags, AnOwnSysrootTargetIsToldWhichTargetAndNothingElse) { +UTest an_own_sysroot_target_is_told_which_target_and_nothing_else{"HostFlags/AnOwnSysrootTargetIsToldWhichTargetAndNothingElse", [] { HostFlagOptions opt; for (auto name : {"aarch64-linux-android", "x86_64-linux-android", @@ -589,12 +584,12 @@ TEST(HostFlags, AnOwnSysrootTargetIsToldWhichTargetAndNothingElse) { // "contains", because the property is that nothing ELSE is emitted: // this host's glibc headers reaching a wasm compile is the measured // failure this gate exists for. - ASSERT_EQ(tokens.size(), 1u) << name << ": " << [&] { + expect(fatal(eq(tokens.size(), 1u))) << name << ": " << [&] { std::string all; for (auto const& t : tokens) { all += t; all += ' '; } return all; }(); - EXPECT_EQ(tokens[0], "--target=SENTINEL-TRIPLE") << name; + expect(eq(tokens[0], "--target=SENTINEL-TRIPLE")) << name; } // AND THE GATE IS STILL A GATE, DISCRIMINATED BY THE cfg BYPASS. @@ -633,8 +628,7 @@ TEST(HostFlags, AnOwnSysrootTargetIsToldWhichTargetAndNothingElse) { host.crossTargetFlag = "--target=x86_64-unknown-linux-gnu"; auto hostTokens = mcpp::toolchain::host_compile_tokens( host, bypass, mcpp::toolchain::no_escape); - EXPECT_NE(std::ranges::find(hostTokens, "--no-default-config"), - hostTokens.end()) + expect(ne(std::ranges::find(hostTokens, "--no-default-config"), hostTokens.end())) << "a hosted clang with a cfg beside it must reach the bypass"; for (auto name : {"aarch64-linux-android", "wasm32-emscripten"}) { @@ -644,21 +638,20 @@ TEST(HostFlags, AnOwnSysrootTargetIsToldWhichTargetAndNothingElse) { sdk.crossTargetFlag = "--target=SENTINEL-TRIPLE"; auto sdkTokens = mcpp::toolchain::host_compile_tokens( sdk, bypass, mcpp::toolchain::no_escape); - EXPECT_EQ(std::ranges::find(sdkTokens, "--no-default-config"), - sdkTokens.end()) + expect(eq(std::ranges::find(sdkTokens, "--no-default-config"), sdkTokens.end())) << name << ": the cfg bypass must be withheld from an SDK whose " "driver's job is to supply configuration"; - EXPECT_EQ(sdkTokens.size(), 1u) << name; + expect(eq(sdkTokens.size(), 1u)) << name; } // A row with no cross flag emits nothing at all rather than an empty // token: an empty argv element is an argument the driver must interpret. auto bare = tc_for(CompilerId::Clang); bare.targetTriple = "wasm32-emscripten"; - ASSERT_TRUE(bare.crossTargetFlag.empty()); - EXPECT_TRUE(mcpp::toolchain::host_compile_tokens( + expect(fatal(bare.crossTargetFlag.empty())); + expect(mcpp::toolchain::host_compile_tokens( bare, opt, mcpp::toolchain::no_escape).empty()); -} +}}; // ── The C++ headers are the C++ layer's question ───────────────────────────── // @@ -669,7 +662,7 @@ TEST(HostFlags, AnOwnSysrootTargetIsToldWhichTargetAndNothingElse) { // `llvm.libcxx`, mcpp#630) answered "payload" there and put two libc++ header // sets on one command line. The same fixture as the bypass test above: two // empty files beside an `include/c++/v1` are a complete driver model. -TEST(HostFlags, TheCxxLayerDecidesWhoseLibcxxHeadersAreEmitted) { +UTest the_cxx_layer_decides_whose_libcxx_headers_are_emitted{"HostFlags/TheCxxLayerDecidesWhoseLibcxxHeadersAreEmitted", [] { namespace fs = std::filesystem; const auto root = fs::temp_directory_path() / "mcpp_hostflags_cxx_fixture"; fs::remove_all(root); @@ -684,7 +677,7 @@ TEST(HostFlags, TheCxxLayerDecidesWhoseLibcxxHeadersAreEmitted) { auto tc = tc_for(CompilerId::Clang); tc.binaryPath = root / "bin" / "clang++"; - ASSERT_TRUE(mcpp::toolchain::resolve_clang_driver(tc).hasCfg) + expect(fatal(bool(mcpp::toolchain::resolve_clang_driver(tc).hasCfg))) << "the fixture did not produce a cfg — the assertions below would be vacuous"; const auto any_payload_cxx = [&](const std::vector& v) { @@ -704,9 +697,9 @@ TEST(HostFlags, TheCxxLayerDecidesWhoseLibcxxHeadersAreEmitted) { // The baseline: a prebuilt C library under the payload's own libc++ takes // the payload's headers, as every native build does. const auto a = mcpp::toolchain::host_compile_tokens(tc, payload, mcpp::toolchain::no_escape); - EXPECT_TRUE(any_payload_cxx(a)); - EXPECT_TRUE(has(a, "-nostdinc++")); - EXPECT_FALSE(has(a, "--rtlib=compiler-rt")); + expect(bool(any_payload_cxx(a))); + expect(bool(has(a, "-nostdinc++"))); + expect(!has(a, "--rtlib=compiler-rt")); // Runtime selection affects AArch64 PCM compatibility even on a compile // without a link. The host std producer and one-shot helper must agree. @@ -714,8 +707,8 @@ TEST(HostFlags, TheCxxLayerDecidesWhoseLibcxxHeadersAreEmitted) { arm.targetTriple = "aarch64-linux-gnu"; auto armCompile = mcpp::toolchain::host_compile_tokens(arm, payload, mcpp::toolchain::no_escape); auto armLink = mcpp::toolchain::host_link_tokens(arm, payload, mcpp::toolchain::no_escape); - EXPECT_TRUE(has(armCompile, "--rtlib=compiler-rt")); - EXPECT_TRUE(has(armLink, "--rtlib=compiler-rt")); + expect(bool(has(armCompile, "--rtlib=compiler-rt"))); + expect(bool(has(armLink, "--rtlib=compiler-rt"))); // A graph C++ runtime over the same prebuilt C library: the payload's // headers are withheld and the driver's own search is closed, since the @@ -723,13 +716,13 @@ TEST(HostFlags, TheCxxLayerDecidesWhoseLibcxxHeadersAreEmitted) { HostFlagOptions graph = payload; graph.cxxFromGraph = true; const auto b = mcpp::toolchain::host_compile_tokens(tc, graph, mcpp::toolchain::no_escape); - EXPECT_FALSE(any_payload_cxx(b)); - EXPECT_TRUE(has(b, "-nostdinc++")); - EXPECT_TRUE(has(b, "--no-default-config")); + expect(!any_payload_cxx(b)); + expect(bool(has(b, "-nostdinc++"))); + expect(bool(has(b, "--no-default-config"))); // A graph owns its runtime/codegen broadcast; payload selection must not // be introduced merely because the target architecture is AArch64. - EXPECT_FALSE(has(mcpp::toolchain::host_compile_tokens(arm, graph, mcpp::toolchain::no_escape), - "--rtlib=compiler-rt")); + expect(!(has(mcpp::toolchain::host_compile_tokens(arm, graph, mcpp::toolchain::no_escape), + "--rtlib=compiler-rt"))); // An Apple cross target whose runtime is the SDK's libc++ and whose graph // does not import `std`: prepare chose the SDK's headers, named @@ -739,9 +732,9 @@ TEST(HostFlags, TheCxxLayerDecidesWhoseLibcxxHeadersAreEmitted) { sdk.appleSdkRoot = fs::path("/Sdk/iPhoneSimulator.sdk"); sdk.appleSdkCxxHeaders = true; const auto c = mcpp::toolchain::host_compile_tokens(tc, sdk, mcpp::toolchain::no_escape); - EXPECT_FALSE(any_payload_cxx(c)); - EXPECT_TRUE(has(c, "-nostdinc++")); - EXPECT_TRUE(has(c, "-isystem" + (fs::path("/Sdk/iPhoneSimulator.sdk") / "usr" / "include" / "c++" / "v1").string())); + expect(!any_payload_cxx(c)); + expect(bool(has(c, "-nostdinc++"))); + expect(bool(has(c, "-isystem" + (fs::path("/Sdk/iPhoneSimulator.sdk") / "usr" / "include" / "c++" / "v1").string()))); // And with the graph runtime on that same target the SDK's headers are // not named either: one libc++ per command line, whichever it is. @@ -749,18 +742,18 @@ TEST(HostFlags, TheCxxLayerDecidesWhoseLibcxxHeadersAreEmitted) { sdkGraph.cxxFromGraph = true; sdkGraph.appleSdkCxxHeaders = false; const auto d = mcpp::toolchain::host_compile_tokens(tc, sdkGraph, mcpp::toolchain::no_escape); - EXPECT_FALSE(any_payload_cxx(d)); - EXPECT_FALSE(std::ranges::any_of(d, [](const std::string& t) { + expect(!any_payload_cxx(d)); + expect(!(std::ranges::any_of(d, [](const std::string& t) { return t.starts_with("-isystem") && t.find("iPhoneSimulator.sdk") != std::string::npos; - })); - EXPECT_TRUE(has(d, "-nostdinc++")); -} + }))); + expect(bool(has(d, "-nostdinc++"))); +}}; // The floating macros an Apple SDK leaves to once modules are on // (apple_float_macro_words). Stated for clang on an Apple target and for // nothing else, as the SDK's own spellings, and quotable by every reader. -TEST(HostFlags, AppleFloatMacrosAreStatedForClangOnAppleTargetsOnly) { +UTest apple_float_macros_are_stated_for_clang_on_apple_targets_only{"HostFlags/AppleFloatMacrosAreStatedForClangOnAppleTargetsOnly", [] { const std::vector words{ "-DINFINITY=HUGE_VALF", "-DNAN=__builtin_nanf(\"0x7fc00000\")"}; auto tc = [](CompilerId id, std::string triple) { @@ -769,14 +762,14 @@ TEST(HostFlags, AppleFloatMacrosAreStatedForClangOnAppleTargetsOnly) { t.targetTriple = std::move(triple); return t; }; - EXPECT_EQ(mcpp::toolchain::apple_float_macro_words(tc(CompilerId::Clang, "arm64-apple-darwin27.0.0")), words); - EXPECT_EQ(mcpp::toolchain::apple_float_macro_words(tc(CompilerId::Clang, "aarch64-macos")), words); - EXPECT_EQ(mcpp::toolchain::apple_float_macro_words(tc(CompilerId::Clang, "arm64-apple-ios18.0")), words); - EXPECT_TRUE(mcpp::toolchain::apple_float_macro_words(tc(CompilerId::Clang, "x86_64-linux-gnu")).empty()); - EXPECT_TRUE(mcpp::toolchain::apple_float_macro_words(tc(CompilerId::Clang, "wasm32-emscripten")).empty()); - EXPECT_TRUE(mcpp::toolchain::apple_float_macro_words(tc(CompilerId::GCC, "x86_64-linux-gnu")).empty()); - EXPECT_TRUE(mcpp::toolchain::apple_float_macro_words(tc(CompilerId::MSVC, "x86_64-pc-windows-msvc")).empty()); -} + expect(eq(mcpp::toolchain::apple_float_macro_words(tc(CompilerId::Clang, "arm64-apple-darwin27.0.0")), words)); + expect(eq(mcpp::toolchain::apple_float_macro_words(tc(CompilerId::Clang, "aarch64-macos")), words)); + expect(eq(mcpp::toolchain::apple_float_macro_words(tc(CompilerId::Clang, "arm64-apple-ios18.0")), words)); + expect(mcpp::toolchain::apple_float_macro_words(tc(CompilerId::Clang, "x86_64-linux-gnu")).empty()); + expect(mcpp::toolchain::apple_float_macro_words(tc(CompilerId::Clang, "wasm32-emscripten")).empty()); + expect(mcpp::toolchain::apple_float_macro_words(tc(CompilerId::GCC, "x86_64-linux-gnu")).empty()); + expect(mcpp::toolchain::apple_float_macro_words(tc(CompilerId::MSVC, "x86_64-pc-windows-msvc")).empty()); +}}; // ── #662: the C library's own host locations, and its C++ twin ───────────── // @@ -822,22 +815,22 @@ std::size_t count_of(const std::vector& v, std::string_view f) { // #511. Measured absent before this fix: the openkal windows-gnu row's // C units kept finding `/usr/x86_64-w64-mingw32/include` (the host's mingw) // ahead of musl's own headers. -TEST(HostFlags, GraphSuppliedCLibraryGetsNostdlibinc) { +UTest graph_supplied_c_library_gets_nostdlibinc{"HostFlags/GraphSuppliedCLibraryGetsNostdlibinc", [] { ClangCfgFixture fx("mcpp_hostflags_662_clib_fixture"); auto tc = fx.toolchain(); - ASSERT_TRUE(mcpp::toolchain::resolve_clang_driver(tc).hasCfg); + expect(fatal(bool(mcpp::toolchain::resolve_clang_driver(tc).hasCfg))); HostFlagOptions payload; payload.cfgBypass = HostFlagOptions::CfgBypass::Always; payload.cAbiPrebuilt = true; - EXPECT_FALSE(contains(mcpp::toolchain::host_compile_tokens( - tc, payload, mcpp::toolchain::no_escape), "-nostdlibinc")); + expect(!(contains(mcpp::toolchain::host_compile_tokens( + tc, payload, mcpp::toolchain::no_escape), "-nostdlibinc"))); HostFlagOptions graph = payload; graph.cAbiPrebuilt = false; - EXPECT_TRUE(contains(mcpp::toolchain::host_compile_tokens( + expect(contains(mcpp::toolchain::host_compile_tokens( tc, graph, mcpp::toolchain::no_escape), "-nostdlibinc")); -} +}}; // THE OPENKAL SHAPE ITSELF (#662): both layers come from the graph at once. // Before this fix `-nostdinc++` was conditioned on `!graphSuppliesTarget`, @@ -845,10 +838,10 @@ TEST(HostFlags, GraphSuppliedCLibraryGetsNostdlibinc) { // beside itself for the payload's libc++, found the HOST's libstdc++ // instead, and any unit reaching it transitively (not just `import std`) // carried two C++ standard libraries. -TEST(HostFlags, BothLayersFromGraphGetBothIsolationTokens) { +UTest both_layers_from_graph_get_both_isolation_tokens{"HostFlags/BothLayersFromGraphGetBothIsolationTokens", [] { ClangCfgFixture fx("mcpp_hostflags_662_both_fixture"); auto tc = fx.toolchain(); - ASSERT_TRUE(mcpp::toolchain::resolve_clang_driver(tc).hasCfg); + expect(fatal(bool(mcpp::toolchain::resolve_clang_driver(tc).hasCfg))); HostFlagOptions opt; opt.cfgBypass = HostFlagOptions::CfgBypass::Always; @@ -857,23 +850,23 @@ TEST(HostFlags, BothLayersFromGraphGetBothIsolationTokens) { const auto toks = mcpp::toolchain::host_compile_tokens( tc, opt, mcpp::toolchain::no_escape); - EXPECT_TRUE(contains(toks, "-nostdlibinc")); - EXPECT_TRUE(contains(toks, "-nostdinc++")); + expect(contains(toks, "-nostdlibinc")); + expect(contains(toks, "-nostdinc++")); // Exactly once each — the two branches that can add `-nostdinc++` // (graph C++, SDK C++) must not both fire for the same build. - EXPECT_EQ(count_of(toks, "-nostdinc++"), 1u); - EXPECT_EQ(count_of(toks, "-nostdlibinc"), 1u); -} + expect(eq(count_of(toks, "-nostdinc++"), 1u)); + expect(eq(count_of(toks, "-nostdlibinc"), 1u)); +}}; // THE REGRESSION GUARD: a build where both layers stay the payload's — every // native build, and every build over a prebuilt/payload C library — emits // the identical token sequence this fix touched nothing for. Compared as a // whole vector rather than by presence, so an accidental REORDERING (which // would still change the rendered command line) fails this test too. -TEST(HostFlags, PayloadServedCommandLineIsUnchanged) { +UTest payload_served_command_line_is_unchanged{"HostFlags/PayloadServedCommandLineIsUnchanged", [] { ClangCfgFixture fx("mcpp_hostflags_662_guard_fixture"); auto tc = fx.toolchain(); - ASSERT_TRUE(mcpp::toolchain::resolve_clang_driver(tc).hasCfg); + expect(fatal(bool(mcpp::toolchain::resolve_clang_driver(tc).hasCfg))); HostFlagOptions opt; opt.cfgBypass = HostFlagOptions::CfgBypass::Always; @@ -886,9 +879,9 @@ TEST(HostFlags, PayloadServedCommandLineIsUnchanged) { "--no-default-config", "-nostdinc++", "-isystem" + (fx.root / "include" / "c++" / "v1").string(), }; - EXPECT_EQ(toks, expected); - EXPECT_FALSE(contains(toks, "-nostdlibinc")); -} + expect(eq(toks, expected)); + expect(!contains(toks, "-nostdlibinc")); +}}; // GCC's fixture carries no `.cfg` (resolve_clang_driver only looks // for one beside a Clang binary), so `bypassCfg` is false and neither @@ -899,7 +892,7 @@ TEST(HostFlags, PayloadServedCommandLineIsUnchanged) { // contributes beside the sysroot) — so GCC stays exactly as it was before // #662: unisolated. No target row in this codebase's own target table pairs // a graph-supplied C library with GCC today, so nothing currently needs it. -TEST(HostFlags, GccEmitsNeitherIsolationTokenRegardlessOfGraphOrigin) { +UTest gcc_emits_neither_isolation_token_regardless_of_graph_origin{"HostFlags/GccEmitsNeitherIsolationTokenRegardlessOfGraphOrigin", [] { auto tc = tc_for(CompilerId::GCC); HostFlagOptions opt; @@ -908,16 +901,16 @@ TEST(HostFlags, GccEmitsNeitherIsolationTokenRegardlessOfGraphOrigin) { opt.cxxFromGraph = true; const auto toks = mcpp::toolchain::host_compile_tokens( tc, opt, mcpp::toolchain::no_escape); - EXPECT_FALSE(contains(toks, "-nostdlibinc")); - EXPECT_FALSE(contains(toks, "-nostdinc++")); -} + expect(!contains(toks, "-nostdlibinc")); + expect(!contains(toks, "-nostdinc++")); +}}; // ─── Clang on the MSVC ABI: one toolset for the compile, std module and link // // `host_compile_tokens` is read by the compile line, the std module precompile // and the build.mcpp host compile, so the toolset words appearing here is what // makes all three compile against the toolset prepare chose. -TEST(HostFlags, ClangOnTheMsvcAbiCompilesAndLinksAgainstTheChosenToolset) { +UTest clang_on_the_msvc_abi_compiles_and_links_against_the_chosen_toolset{"HostFlags/ClangOnTheMsvcAbiCompilesAndLinksAgainstTheChosenToolset", [] { mcpp::toolchain::Toolchain t; t.compiler = CompilerId::Clang; t.targetTriple = "x86_64-pc-windows-msvc"; @@ -934,15 +927,15 @@ TEST(HostFlags, ClangOnTheMsvcAbiCompilesAndLinksAgainstTheChosenToolset) { auto compile = mcpp::toolchain::host_compile_tokens(t, opt, mcpp::toolchain::no_escape); auto link = mcpp::toolchain::host_link_tokens(t, opt, mcpp::toolchain::no_escape); for (auto const* v : {&compile, &link}) { - EXPECT_TRUE(find_pair(*v, "-Xmicrosoft-visualc-tools-root", - "C:/VS/VC/Tools/MSVC/14.44.35207")); - EXPECT_TRUE(find_pair(*v, "-Xmicrosoft-windows-sdk-root", "C:/Kits/10")); - EXPECT_TRUE(find_pair(*v, "-Xmicrosoft-windows-sdk-version", "10.0.26100.0")); + expect(bool(find_pair(*v, "-Xmicrosoft-visualc-tools-root", + "C:/VS/VC/Tools/MSVC/14.44.35207"))); + expect(bool(find_pair(*v, "-Xmicrosoft-windows-sdk-root", "C:/Kits/10"))); + expect(bool(find_pair(*v, "-Xmicrosoft-windows-sdk-version", "10.0.26100.0"))); } // Without a recorded toolset nothing is said, which is the behaviour of // every build before a toolset is bound. t.base.msvc.toolsDir.clear(); for (auto const& w : mcpp::toolchain::host_compile_tokens(t, opt, mcpp::toolchain::no_escape)) - EXPECT_EQ(w.find("-Xmicrosoft"), std::string::npos) << w; -} + expect(eq(w.find("-Xmicrosoft"), std::string::npos)) << w; +}}; diff --git a/tests/unit/test_index_contract.cpp b/tests/unit/test_index_contract.cpp index 09cbe77f9..34f58b051 100644 --- a/tests/unit/test_index_contract.cpp +++ b/tests/unit/test_index_contract.cpp @@ -1,75 +1,77 @@ -#include import std; +import mcpplibs.mtest; import mcpp.pm.index_contract; -TEST(IndexContract, FloorViolationOrdering) { +using namespace mcpplibs::mtest; + +UTest floor_violation_ordering{"IndexContract/FloorViolationOrdering", [] { using mcpp::pm::floor_violation; - EXPECT_FALSE(floor_violation("0.0.85", "0.0.85").has_value()); - EXPECT_FALSE(floor_violation("0.0.85", "0.0.90").has_value()); - EXPECT_FALSE(floor_violation("0.0.85", "1.0.0").has_value()); + expect(!(floor_violation("0.0.85", "0.0.85").has_value())); + expect(!(floor_violation("0.0.85", "0.0.90").has_value())); + expect(!(floor_violation("0.0.85", "1.0.0").has_value())); auto v = floor_violation("0.0.85", "0.0.84"); - ASSERT_TRUE(v.has_value()); - EXPECT_NE(v->find("E0006"), std::string::npos); - EXPECT_NE(v->find("0.0.85"), std::string::npos); -} + expect(fatal(v.has_value())); + expect(ne(v->find("E0006"), std::string::npos)); + expect(ne(v->find("0.0.85"), std::string::npos)); +}}; // The Upgrade advice must tell the user which install method THEY have. A // single hardcoded install.sh one-liner misleads distro (AUR) users into // installing a second copy that does not update the running binary. -TEST(IndexContract, E0006MessageSwitchesUpgradeAdviceByLayout) { +UTest e0006_message_switches_upgrade_advice_by_layout{"IndexContract/E0006MessageSwitchesUpgradeAdviceByLayout", [] { using mcpp::pm::floor_violation; using mcpp::pm::e0006_message; auto violation = floor_violation("2026.8.3.3", "2026.7.28.2"); - ASSERT_TRUE(violation.has_value()); + expect(fatal(violation.has_value())); // Non-distro (install.sh / xlings) layout keeps the install.sh one-liner // and always appends the recommended-installer note. auto script = e0006_message(*violation, /*distroManaged=*/false); - EXPECT_NE(script.find("install.sh"), std::string::npos); - EXPECT_EQ(script.find("distro-managed"), std::string::npos); - EXPECT_NE(script.find("xlings update mcpp"), std::string::npos); - EXPECT_NE(script.find("E0006"), std::string::npos); + expect(ne(script.find("install.sh"), std::string::npos)); + expect(eq(script.find("distro-managed"), std::string::npos)); + expect(ne(script.find("xlings update mcpp"), std::string::npos)); + expect(ne(script.find("E0006"), std::string::npos)); // Distro (AUR) layout swaps in the package-manager advice. auto distro = e0006_message(*violation, /*distroManaged=*/true); - EXPECT_NE(distro.find("distro-managed"), std::string::npos); - EXPECT_EQ(distro.find("re-run the install.sh one-liner"), std::string::npos); - EXPECT_NE(distro.find("xlings update mcpp"), std::string::npos); - EXPECT_NE(distro.find("E0006"), std::string::npos); -} + expect(ne(distro.find("distro-managed"), std::string::npos)); + expect(eq(distro.find("re-run the install.sh one-liner"), std::string::npos)); + expect(ne(distro.find("xlings update mcpp"), std::string::npos)); + expect(ne(distro.find("E0006"), std::string::npos)); +}}; // The floor compares mcpp's OWN version, so the date scheme (YYYY.M.D.N) has // to be ordered on all four segments. While parse_version truncated at three, // every release of a given day compared equal and the floor let a too-old mcpp // straight through — the exact failure this check exists to prevent. -TEST(IndexContract, FloorViolationSeparatesSameDayReleases) { +UTest floor_violation_separates_same_day_releases{"IndexContract/FloorViolationSeparatesSameDayReleases", [] { using mcpp::pm::floor_violation; - EXPECT_FALSE(floor_violation("2026.7.27.1", "2026.7.27.1").has_value()); - EXPECT_FALSE(floor_violation("2026.7.27.1", "2026.7.27.2").has_value()); - EXPECT_FALSE(floor_violation("2026.7.27.9", "2026.7.27.10").has_value()); + expect(!(floor_violation("2026.7.27.1", "2026.7.27.1").has_value())); + expect(!(floor_violation("2026.7.27.1", "2026.7.27.2").has_value())); + expect(!(floor_violation("2026.7.27.9", "2026.7.27.10").has_value())); auto v = floor_violation("2026.7.27.5", "2026.7.27.1"); - ASSERT_TRUE(v.has_value()) << "a lower same-day ordinal must violate the floor"; - EXPECT_NE(v->find("E0006"), std::string::npos); - EXPECT_NE(v->find("2026.7.27.5"), std::string::npos); -} + expect(fatal(v.has_value())) << "a lower same-day ordinal must violate the floor"; + expect(ne(v->find("E0006"), std::string::npos)); + expect(ne(v->find("2026.7.27.5"), std::string::npos)); +}}; -TEST(IndexContract, FloorViolationAcrossVersionSchemes) { +UTest floor_violation_across_version_schemes{"IndexContract/FloorViolationAcrossVersionSchemes", [] { using mcpp::pm::floor_violation; // A date-scheme mcpp satisfies any legacy 0.0.x floor. - EXPECT_FALSE(floor_violation("0.0.109", "2026.7.27.1").has_value()); + expect(!(floor_violation("0.0.109", "2026.7.27.1").has_value())); // A legacy mcpp does not satisfy a date-scheme floor. - EXPECT_TRUE(floor_violation("2026.7.27.1", "0.0.109").has_value()); -} + expect(floor_violation("2026.7.27.1", "0.0.109").has_value()); +}}; -TEST(IndexContract, EmptyOrMalformedNeverBricks) { +UTest empty_or_malformed_never_bricks{"IndexContract/EmptyOrMalformedNeverBricks", [] { using mcpp::pm::floor_violation; - EXPECT_FALSE(floor_violation("", "0.0.84").has_value()); - EXPECT_FALSE(floor_violation("not-a-version", "0.0.84").has_value()); -} + expect(!(floor_violation("", "0.0.84").has_value())); + expect(!(floor_violation("not-a-version", "0.0.84").has_value())); +}}; -TEST(IndexContract, ReadContractRoundTrip) { +UTest read_contract_round_trip{"IndexContract/ReadContractRoundTrip", [] { auto dir = std::filesystem::temp_directory_path() / "mcpp_ic_test"; std::filesystem::create_directories(dir); { @@ -77,33 +79,33 @@ TEST(IndexContract, ReadContractRoundTrip) { os << "[index]\nspec = \"1\"\nmin_mcpp = \"0.0.85\"\nlatest_mcpp = \"0.0.86\"\n"; } auto c = mcpp::pm::read_index_contract(dir); - ASSERT_TRUE(c.has_value()); - EXPECT_EQ(c->spec, "1"); - EXPECT_EQ(c->minMcpp, "0.0.85"); - EXPECT_EQ(c->latestMcpp, "0.0.86"); + expect(fatal(c.has_value())); + expect(eq(c->spec, "1")); + expect(eq(c->minMcpp, "0.0.85")); + expect(eq(c->latestMcpp, "0.0.86")); std::filesystem::remove_all(dir); - EXPECT_FALSE(mcpp::pm::read_index_contract(dir).has_value()); -} + expect(!(mcpp::pm::read_index_contract(dir).has_value())); +}}; // The upgrade note appended to E0006 starts on its own line. It was appended // directly after the "Details:" line and read as one line. // The closing tip of a refresh names the version the index asks for, and // falls back to the advice alone when the contract could not be read. -TEST(IndexContract, FloorUpgradeAdviceNamesTheVersionWhenKnown) { +UTest floor_upgrade_advice_names_the_version_when_known{"IndexContract/FloorUpgradeAdviceNamesTheVersionWhenKnown", [] { auto known = mcpp::pm::index_floor_upgrade_advice("9999.9.9.9"); - EXPECT_NE(known.find("requires mcpp >= 9999.9.9.9"), std::string::npos) << known; - EXPECT_NE(known.find("Upgrade: "), std::string::npos) << known; + expect(ne(known.find("requires mcpp >= 9999.9.9.9"), std::string::npos)) << known; + expect(ne(known.find("Upgrade: "), std::string::npos)) << known; auto unknown = mcpp::pm::index_floor_upgrade_advice(""); - EXPECT_EQ(unknown.find(">="), std::string::npos) << unknown; - EXPECT_TRUE(unknown.starts_with("Upgrade: ")) << unknown; -} + expect(eq(unknown.find(">="), std::string::npos)) << unknown; + expect(unknown.starts_with("Upgrade: ")) << unknown; +}}; -TEST(IndexContract, E0006UpgradeNoteStartsOnItsOwnLine) { +UTest e0006_upgrade_note_starts_on_its_own_line{"IndexContract/E0006UpgradeNoteStartsOnItsOwnLine", [] { auto violation = mcpp::pm::floor_violation("2026.8.3.3", "2026.7.28.2"); - ASSERT_TRUE(violation.has_value()); + expect(fatal(violation.has_value())); auto text = mcpp::pm::e0006_message(*violation, /*distroManaged=*/false); auto at = text.find(" Upgrade: 'xlings update mcpp'"); - ASSERT_NE(at, std::string::npos) << text; - ASSERT_GT(at, 0u); - EXPECT_EQ(text[at - 1], '\n') << text; -} + expect(fatal(ne(at, std::string::npos))) << text; + expect(fatal(gt(at, 0u))); + expect(eq(text[at - 1], '\n')) << text; +}}; diff --git a/tests/unit/test_install_hook_env.cpp b/tests/unit/test_install_hook_env.cpp index bc8222ac7..14a98a3bf 100644 --- a/tests/unit/test_install_hook_env.cpp +++ b/tests/unit/test_install_hook_env.cpp @@ -1,8 +1,10 @@ -#include import std; +import mcpplibs.mtest; import mcpp.build.build_program; +using namespace mcpplibs::mtest; + // #613: the part of a build program's environment an install hook receives. // The names, their order, and the rule for each value are stated here once for // every platform; tests/e2e/648 shows a real install hook reading them. @@ -14,49 +16,49 @@ using HookEnv = std::vector>; std::string value_of(const HookEnv& env, std::string_view key) { for (auto const& [k, v] : env) if (k == key) return v; - ADD_FAILURE() << key << " is missing"; + expect(false) << key << " is missing"; return {}; } } // namespace -TEST(InstallHookEnv, NamesTheSixVariablesInOrder) { +UTest names_the_six_variables_in_order{"InstallHookEnv/NamesTheSixVariablesInOrder", [] { auto env = mcpp::build::install_hook_env(mcpp::build::BuildProgramEnv{}); std::vector names; for (auto const& [k, v] : env) names.push_back(k); - EXPECT_EQ(names, (std::vector{ + expect(eq(names, (std::vector{ "MCPP_COMPILER", "MCPP_CXX_STDLIB", "MCPP_TARGET", - "MCPP_TARGET_OS", "MCPP_TARGET_ARCH", "MCPP_TARGET_ENV"})); -} + "MCPP_TARGET_OS", "MCPP_TARGET_ARCH", "MCPP_TARGET_ENV"}))); +}}; -TEST(InstallHookEnv, WithoutAToolchainTheToolchainValuesArePresentAndEmpty) { +UTest without_a_toolchain_the_toolchain_values_are_present_and_empty{"InstallHookEnv/WithoutAToolchainTheToolchainValuesArePresentAndEmpty", [] { auto env = mcpp::build::install_hook_env(mcpp::build::BuildProgramEnv{}); - EXPECT_EQ(value_of(env, "MCPP_COMPILER"), ""); - EXPECT_EQ(value_of(env, "MCPP_CXX_STDLIB"), ""); -} + expect(eq(value_of(env, "MCPP_COMPILER"), "")); + expect(eq(value_of(env, "MCPP_CXX_STDLIB"), "")); +}}; -TEST(InstallHookEnv, ANativeBuildNamesTheHost) { +UTest a_native_build_names_the_host{"InstallHookEnv/ANativeBuildNamesTheHost", [] { mcpp::build::BuildProgramEnv bp; bp.compilerId = "gcc"; bp.cxxStdlib = "libstdc++"; auto env = mcpp::build::install_hook_env(bp); - EXPECT_EQ(value_of(env, "MCPP_COMPILER"), "gcc"); - EXPECT_EQ(value_of(env, "MCPP_CXX_STDLIB"), "libstdc++"); + expect(eq(value_of(env, "MCPP_COMPILER"), "gcc")); + expect(eq(value_of(env, "MCPP_CXX_STDLIB"), "libstdc++")); const auto target = value_of(env, "MCPP_TARGET"); - ASSERT_FALSE(target.empty()); - EXPECT_FALSE(value_of(env, "MCPP_TARGET_OS").empty()) << target; - EXPECT_TRUE(target.starts_with(value_of(env, "MCPP_TARGET_ARCH"))) << target; -} + expect(fatal(!target.empty())); + expect(!(value_of(env, "MCPP_TARGET_OS").empty())) << target; + expect(target.starts_with(value_of(env, "MCPP_TARGET_ARCH"))) << target; +}}; -TEST(InstallHookEnv, ACrossBuildSplitsTheRequestedTriple) { +UTest a_cross_build_splits_the_requested_triple{"InstallHookEnv/ACrossBuildSplitsTheRequestedTriple", [] { mcpp::build::BuildProgramEnv bp; bp.targetTriple = "aarch64-linux-android"; bp.compilerId = "clang"; bp.cxxStdlib = "libc++"; auto env = mcpp::build::install_hook_env(bp); - EXPECT_EQ(value_of(env, "MCPP_TARGET"), "aarch64-linux-android"); - EXPECT_EQ(value_of(env, "MCPP_TARGET_OS"), "linux"); - EXPECT_EQ(value_of(env, "MCPP_TARGET_ARCH"), "aarch64"); - EXPECT_EQ(value_of(env, "MCPP_TARGET_ENV"), "android"); - EXPECT_EQ(value_of(env, "MCPP_CXX_STDLIB"), "libc++"); -} + expect(eq(value_of(env, "MCPP_TARGET"), "aarch64-linux-android")); + expect(eq(value_of(env, "MCPP_TARGET_OS"), "linux")); + expect(eq(value_of(env, "MCPP_TARGET_ARCH"), "aarch64")); + expect(eq(value_of(env, "MCPP_TARGET_ENV"), "android")); + expect(eq(value_of(env, "MCPP_CXX_STDLIB"), "libc++")); +}}; diff --git a/tests/unit/test_key_registry.cpp b/tests/unit/test_key_registry.cpp index b096647b2..49e048b5c 100644 --- a/tests/unit/test_key_registry.cpp +++ b/tests/unit/test_key_registry.cpp @@ -1,9 +1,11 @@ -#include import std; +import mcpplibs.mtest; import mcpp.manifest; import mcpp.project; +using namespace mcpplibs::mtest; + // SPEC-004 §9.10: the key registry, the workspace layer and the merge rules // (W1-W3, W7). The end-to-end halves are tests/e2e/893-895 and 900. @@ -13,7 +15,7 @@ namespace { mm::Manifest parse(std::string_view text) { auto m = mm::parse_string(text); - if (!m) ADD_FAILURE() << m.error().format(); + if (!m) expect(false) << m.error().format(); return m ? *m : mm::Manifest{}; } @@ -29,18 +31,18 @@ std::string parse_error(std::string_view text) { // key fills. They must name the same keys; `linkage` is the one key // [workspace.build] reads that [build] does not (the target row and the // profile set it there). -TEST(KeyRegistry, TheSharedBuildKeysAreTheWorkspaceBuildKeys) { +UTest the_shared_build_keys_are_the_workspace_build_keys{"KeyRegistry/TheSharedBuildKeysAreTheWorkspaceBuildKeys", [] { std::set shared, table; for (auto const& row : mm::kKeyRegistry) if (row.table == "build" && row.shared) shared.emplace(row.key); for (auto const& row : mm::kWorkspaceBuildKeys) if (row.key != "linkage") table.emplace(row.key); - EXPECT_EQ(shared, table); -} + expect(eq(shared, table)); +}}; // Every configuration key of [build] is part of the value members are grouped // by: two members that differ in one are not one plan. -TEST(KeyRegistry, EveryConfigurationKeyEntersTheGroupingKey) { +UTest every_configuration_key_enters_the_grouping_key{"KeyRegistry/EveryConfigurationKeyEntersTheGroupingKey", [] { const auto key = mcpp::project::root_position_key(mm::Manifest{}); const std::map spelled = { {"profile", "default_profile"}, {"default-profile", "default_profile"}, @@ -50,12 +52,12 @@ TEST(KeyRegistry, EveryConfigurationKeyEntersTheGroupingKey) { if (row.table != "build" || row.scope != mm::KeyScope::Configuration) continue; auto it = spelled.find(row.key); const std::string name(it == spelled.end() ? row.key : it->second); - EXPECT_NE(key.find(name + "="), std::string::npos) << row.key; + expect(ne(key.find(name + "="), std::string::npos)) << row.key; } -} +}}; // The parser accepts every [build] key of the registry without a warning. -TEST(KeyRegistry, TheParserReadsTheRegistrysBuildKeys) { +UTest the_parser_reads_the_registrys_build_keys{"KeyRegistry/TheParserReadsTheRegistrysBuildKeys", [] { auto m = parse(R"( [package] name = "x" @@ -66,7 +68,7 @@ cxxflags = ["-O2"] dialect_cxxflags = ["-DX=1"] cache = "local" )"); - EXPECT_TRUE(m.schemaWarnings.empty()); + expect(m.schemaWarnings.empty()); auto w = parse(R"( [package] name = "x" @@ -74,12 +76,12 @@ version = "0.1.0" [build] cxxflagz = ["-O2"] )"); - ASSERT_EQ(w.schemaWarnings.size(), 1u); - EXPECT_NE(w.schemaWarnings.front().find("cxxflagz"), std::string::npos); -} + expect(fatal(eq(w.schemaWarnings.size(), 1u))); + expect(ne(w.schemaWarnings.front().find("cxxflagz"), std::string::npos)); +}}; // W2: [workspace.X] is read by the reader of X. -TEST(WorkspaceLayer, MirrorsAreReadByTheReaderOfTheTableTheyMirror) { +UTest mirrors_are_read_by_the_reader_of_the_table_they_mirror{"WorkspaceLayer/MirrorsAreReadByTheReaderOfTheTableTheyMirror", [] { auto ws = parse(R"( [workspace] members = ["a"] @@ -95,58 +97,57 @@ cxxflags = ["-DSHARED"] [workspace.xlings.workspace] cmake = "4.0.2" )"); - ASSERT_TRUE(ws.layer); + expect(fatal(bool(ws.layer))); auto const& layer = *ws.layer; - EXPECT_EQ(layer.toolchain.for_platform("linux").value_or(""), "llvm@23.1.3"); - ASSERT_TRUE(layer.profiles.contains("release")); - EXPECT_EQ(layer.profiles.at("release").optLevel, "3"); - EXPECT_TRUE(layer.profiles.at("release").optDeclared); - EXPECT_FALSE(layer.profiles.at("release").debugDeclared); - EXPECT_EQ(layer.targetOverrides.at("x86_64-linux-musl").linkage, "static"); - ASSERT_FALSE(layer.conditionalConfigs.empty()); - EXPECT_EQ(ws.workspace.layerTables, - (std::vector{"toolchain", "profile", "target", "xlings"})); + expect(eq(layer.toolchain.for_platform("linux").value_or(""), "llvm@23.1.3")); + expect(fatal(layer.profiles.contains("release"))); + expect(eq(layer.profiles.at("release").optLevel, "3")); + expect(bool(layer.profiles.at("release").optDeclared)); + expect(!(layer.profiles.at("release").debugDeclared)); + expect(eq(layer.targetOverrides.at("x86_64-linux-musl").linkage, "static")); + expect(fatal(!layer.conditionalConfigs.empty())); + expect(eq(ws.workspace.layerTables, (std::vector{"toolchain", "profile", "target", "xlings"}))); // The root itself states none of them for its own package. - EXPECT_TRUE(ws.toolchain.byPlatform.empty()); - EXPECT_TRUE(ws.targetOverrides.empty()); -} + expect(ws.toolchain.byPlatform.empty()); + expect(ws.targetOverrides.empty()); +}}; -TEST(WorkspaceLayer, RefusesWhatDescribesOnePackageAndWhatItDoesNotKnow) { - EXPECT_NE(parse_error(R"( +UTest refuses_what_describes_one_package_and_what_it_does_not_know{"WorkspaceLayer/RefusesWhatDescribesOnePackageAndWhatItDoesNotKnow", [] { + expect(ne(parse_error(R"( [workspace] members = ["a"] [workspace.tolchain] default = "gcc@16.1.0" -)").find("[workspace] has no key or table 'tolchain'"), std::string::npos); - EXPECT_NE(parse_error(R"( +)").find("[workspace] has no key or table 'tolchain'"), std::string::npos)); + expect(ne(parse_error(R"( [workspace] members = ["a"] [workspace.target.x86_64-linux-gnu.dependencies] fmt = "11.0.0" -)").find("describes one package"), std::string::npos); - EXPECT_NE(parse_error(R"( +)").find("describes one package"), std::string::npos)); + expect(ne(parse_error(R"( [workspace] members = ["a"] [workspace.target.x86_64-linux-gnu.build] sources = ["x.cpp"] -)").find("has no key 'sources'"), std::string::npos); - EXPECT_NE(parse_error(R"( +)").find("has no key 'sources'"), std::string::npos)); + expect(ne(parse_error(R"( [workspace] members = ["a"] [workspace.target.x86_64-linux-gnu.build] allow_host_libs = true -)").find("allow_host_libs is not inheritable"), std::string::npos); +)").find("allow_host_libs is not inheritable"), std::string::npos)); // On a root without [package], one table cannot speak to members twice. - EXPECT_NE(parse_error(R"( + expect(ne(parse_error(R"( [workspace] members = ["a"] [toolchain] default = "gcc@16.1.0" [workspace.toolchain] default = "llvm@23.1.3" -)").find("write it once, as [workspace.toolchain]"), std::string::npos); +)").find("write it once, as [workspace.toolchain]"), std::string::npos)); // With [package], the root-position table is the root package's own. - EXPECT_TRUE(parse_error(R"( + expect(parse_error(R"( [package] name = "root" version = "0.1.0" @@ -157,10 +158,10 @@ default = "gcc@16.1.0" [workspace.toolchain] default = "llvm@23.1.3" )").empty()); -} +}}; // W7: a package table on a root without [package] acts on nothing. -TEST(WorkspaceLayer, PackageTablesOnAVirtualRootAreRecorded) { +UTest package_tables_on_a_virtual_root_are_recorded{"WorkspaceLayer/PackageTablesOnAVirtualRootAreRecorded", [] { auto ws = parse(R"( [workspace] members = ["a"] @@ -169,9 +170,9 @@ cxxflags = ["-O2"] [target.'cfg(os = "linux")'.build] dialect_cxxflags = ["-DX=1"] )"); - ASSERT_EQ(ws.virtualRootPackageTables.size(), 2u); - EXPECT_NE(ws.virtualRootPackageTables[0].find("[build]"), std::string::npos); - EXPECT_NE(ws.virtualRootPackageTables[1].find("[workspace.target."), std::string::npos); + expect(fatal(eq(ws.virtualRootPackageTables.size(), 2u))); + expect(ne(ws.virtualRootPackageTables[0].find("[build]"), std::string::npos)); + expect(ne(ws.virtualRootPackageTables[1].find("[workspace.target."), std::string::npos)); auto rooted = parse(R"( [package] name = "root" @@ -181,12 +182,12 @@ members = ["a"] [build] cxxflags = ["-O2"] )"); - EXPECT_TRUE(rooted.virtualRootPackageTables.empty()); -} + expect(rooted.virtualRootPackageTables.empty()); +}}; // W3: scalars the member declared are the member's, named tables merge key by // key, lists put the workspace first, conditional rows come first. -TEST(WorkspaceLayer, TheMergeRuleIsTheKeys) { +UTest the_merge_rule_is_the_keys{"WorkspaceLayer/TheMergeRuleIsTheKeys", [] { auto ws = parse(R"( [workspace] members = ["a"] @@ -219,18 +220,17 @@ dialect_cxxflags = ["-DM=1"] )"); mcpp::project::inherit_workspace_root_position(member, ws, "/ws"); mcpp::project::inherit_workspace_layer_rows(member, ws, "/ws"); - EXPECT_EQ(member.toolchain.for_platform("linux").value_or(""), "gcc@16.1.0"); - EXPECT_EQ(member.toolchain.for_platform("macos").value_or(""), "llvm@23.1.3"); + expect(eq(member.toolchain.for_platform("linux").value_or(""), "gcc@16.1.0")); + expect(eq(member.toolchain.for_platform("macos").value_or(""), "llvm@23.1.3")); auto const& r = member.profiles.at("release"); - EXPECT_EQ(r.optLevel, "2"); - EXPECT_TRUE(r.lto); - EXPECT_EQ(r.cxxflags, (std::vector{"-DWS", "-DMEMBER"})); + expect(eq(r.optLevel, "2")); + expect(bool(r.lto)); + expect(eq(r.cxxflags, (std::vector{"-DWS", "-DMEMBER"}))); auto const& t = member.targetOverrides.at("x86_64-linux-musl"); - EXPECT_EQ(t.linkage, "dynamic"); - EXPECT_EQ(t.cxxRuntime, "self-contained"); - ASSERT_GE(member.conditionalConfigs.size(), 2u); - EXPECT_EQ(member.conditionalConfigs.front().dialectCxxflags, - (std::vector{"-DW=1"})); + expect(eq(t.linkage, "dynamic")); + expect(eq(t.cxxRuntime, "self-contained")); + expect(fatal(ge(member.conditionalConfigs.size(), 2u))); + expect(eq(member.conditionalConfigs.front().dialectCxxflags, (std::vector{"-DW=1"}))); // A member's own `default` speaks for every platform. auto own = parse(R"( @@ -241,11 +241,11 @@ version = "0.1.0" default = "gcc@16.1.0" )"); mcpp::project::inherit_workspace_root_position(own, ws, "/ws"); - EXPECT_EQ(own.toolchain.for_platform("macos").value_or(""), "gcc@16.1.0"); -} + expect(eq(own.toolchain.for_platform("macos").value_or(""), "gcc@16.1.0")); +}}; // W4: grouping counts a conditional configuration row as it evaluates. -TEST(WorkspaceLayer, GroupingReadsEvaluatedRows) { +UTest grouping_reads_evaluated_rows{"WorkspaceLayer/GroupingReadsEvaluatedRows", [] { auto a = parse(R"( [package] name = "a" @@ -260,6 +260,6 @@ version = "0.1.0" )"); auto linux = [](std::string_view p) { return p.find("linux") != std::string_view::npos; }; auto windows = [](std::string_view p) { return p.find("windows") != std::string_view::npos; }; - EXPECT_EQ(mcpp::project::root_position_key(a, linux), mcpp::project::root_position_key(b, linux)); - EXPECT_NE(mcpp::project::root_position_key(a, windows), mcpp::project::root_position_key(b, windows)); -} + expect(eq(mcpp::project::root_position_key(a, linux), mcpp::project::root_position_key(b, linux))); + expect(ne(mcpp::project::root_position_key(a, windows), mcpp::project::root_position_key(b, windows))); +}}; diff --git a/tests/unit/test_library_interface.cpp b/tests/unit/test_library_interface.cpp index 93b6ae562..71dcf8193 100644 --- a/tests/unit/test_library_interface.cpp +++ b/tests/unit/test_library_interface.cpp @@ -1,10 +1,12 @@ -#include import std; +import mcpplibs.mtest; import mcpp.manifest; import mcpp.modgraph.graph; import mcpp.modgraph.library_interface; +using namespace mcpplibs::mtest; + // ONE DEFINITION OF WHAT A LIBRARY OFFERS (SPEC-008 v0.2, mcpp#797): the // interface root and what it re-exports, the interface units of the active // features, and -- with no root -- every exported module. The target build, @@ -43,7 +45,7 @@ SourceUnit unit(const std::filesystem::path& p, std::string module, } // namespace -TEST(LibraryInterface, TheRootAndWhatItReExports) { +UTest the_root_and_what_it_re_exports{"LibraryInterface/TheRootAndWhatItReExports", [] { Tree t("root"); Graph g; g.units.push_back(unit(t.touch("src/lib.cppm"), "acme.lib", {"acme.lib.part"})); @@ -52,13 +54,12 @@ TEST(LibraryInterface, TheRootAndWhatItReExports) { mcpp::manifest::Manifest m; m.package.name = "lib"; auto iface = library_interface(g, m, t.root, "acme.lib", {}); - EXPECT_EQ(iface.rootModule, "acme.lib"); - EXPECT_EQ(iface.publicModules, (std::set{"acme.lib", "acme.lib.part"})); - EXPECT_EQ(iface.exportedModules, - (std::set{"acme.lib", "acme.lib.part", "acme.lib.detail"})); -} + expect(eq(iface.rootModule, "acme.lib")); + expect(eq(iface.publicModules, (std::set{"acme.lib", "acme.lib.part"}))); + expect(eq(iface.exportedModules, (std::set{"acme.lib", "acme.lib.part", "acme.lib.detail"}))); +}}; -TEST(LibraryInterface, NoRootStatesNoInterface) { +UTest no_root_states_no_interface{"LibraryInterface/NoRootStatesNoInterface", [] { Tree t("noroot"); Graph g; g.units.push_back(unit(t.touch("src/a.cppm"), "acme.a")); @@ -66,11 +67,11 @@ TEST(LibraryInterface, NoRootStatesNoInterface) { mcpp::manifest::Manifest m; m.package.name = "lib"; auto iface = library_interface(g, m, t.root, "acme.lib", {}); - EXPECT_TRUE(iface.rootModule.empty()); - EXPECT_EQ(iface.publicModules, iface.exportedModules); -} + expect(iface.rootModule.empty()); + expect(eq(iface.publicModules, iface.exportedModules)); +}}; -TEST(LibraryInterface, ADeclaredRootIsTheOneUsed) { +UTest a_declared_root_is_the_one_used{"LibraryInterface/ADeclaredRootIsTheOneUsed", [] { Tree t("declared"); Graph g; g.units.push_back(unit(t.touch("src/lib.cppm"), "acme.conventional")); @@ -79,12 +80,12 @@ TEST(LibraryInterface, ADeclaredRootIsTheOneUsed) { m.package.name = "lib"; m.lib.path = "rules/facade.cppm"; auto iface = library_interface(g, m, t.root, "acme.lib", {}); - EXPECT_TRUE(iface.rootDeclared); - EXPECT_EQ(iface.rootModule, "acme.facade"); - EXPECT_EQ(iface.publicModules, (std::set{"acme.facade"})); -} + expect(bool(iface.rootDeclared)); + expect(eq(iface.rootModule, "acme.facade")); + expect(eq(iface.publicModules, (std::set{"acme.facade"}))); +}}; -TEST(LibraryInterface, AnActiveFeatureAddsItsInterfaceUnits) { +UTest an_active_feature_adds_its_interface_units{"LibraryInterface/AnActiveFeatureAddsItsInterfaceUnits", [] { Tree t("features"); Graph g; g.units.push_back(unit(t.touch("src/lib.cppm"), "acme.lib")); @@ -95,10 +96,10 @@ TEST(LibraryInterface, AnActiveFeatureAddsItsInterfaceUnits) { m.buildConfig.featureSources["rules-cuda"] = {"rules/cuda.cppm"}; m.buildConfig.featureSources["rules-spirv"] = {"rules/spirv.cppm"}; auto iface = library_interface(g, m, t.root, "acme.lib", {"rules-cuda"}); - EXPECT_EQ(iface.publicModules, (std::set{"acme.lib", "acme.rules.cuda"})); -} + expect(eq(iface.publicModules, (std::set{"acme.lib", "acme.rules.cuda"}))); +}}; -TEST(LibraryInterface, OnlyThePackagesOwnUnitsCount) { +UTest only_the_packages_own_units_count{"LibraryInterface/OnlyThePackagesOwnUnitsCount", [] { Tree t("own"); Graph g; g.units.push_back(unit(t.touch("src/lib.cppm"), "acme.lib")); @@ -108,5 +109,5 @@ TEST(LibraryInterface, OnlyThePackagesOwnUnitsCount) { mcpp::manifest::Manifest m; m.package.name = "lib"; auto iface = library_interface(g, m, t.root, "acme.lib", {}); - EXPECT_FALSE(iface.exportedModules.contains("other.mod")); -} + expect(!iface.exportedModules.contains("other.mod")); +}}; diff --git a/tests/unit/test_link_intent_spelling.cpp b/tests/unit/test_link_intent_spelling.cpp index cbed0b613..d686a41b3 100644 --- a/tests/unit/test_link_intent_spelling.cpp +++ b/tests/unit/test_link_intent_spelling.cpp @@ -1,9 +1,11 @@ -#include import std; +import mcpplibs.mtest; import mcpp.build.flags; import mcpp.manifest; +using namespace mcpplibs::mtest; + using mcpp::build::LinkIntentFlavor; using mcpp::build::render_link_intent_flags; @@ -35,22 +37,22 @@ mcpp::manifest::LinkIntent leg_intent() { } // namespace -TEST(LinkIntentSpelling, MsvcGetsLibpathAndADotLib) { +UTest msvc_gets_libpath_and_a_dot_lib{"LinkIntentSpelling/MsvcGetsLibpathAndADotLib", [] { auto s = render_link_intent_flags(leg_intent(), LinkIntentFlavor::PeMsvc); // `/LIBPATH` without the colon: this is NINJA text, not a shell command // line, and `:` is escaped to `$:` because ninja reads a bare colon as the // outputs/inputs separator. Asserting the raw `/LIBPATH:` fails against a // perfectly correct renderer — which is what the first version of this test // did. - EXPECT_NE(s.find("/LIBPATH"), std::string::npos) << s; - EXPECT_NE(s.find("mathkit.lib"), std::string::npos) << s; + expect(ne(s.find("/LIBPATH"), std::string::npos)) << s; + expect(ne(s.find("mathkit.lib"), std::string::npos)) << s; // The GNU spelling must be absent, not merely outnumbered: `cl` stops at // the first `-L` it does not recognise. - EXPECT_EQ(s.find("-L"), std::string::npos) << s; - EXPECT_EQ(s.find("-lmathkit"), std::string::npos) << s; -} + expect(eq(s.find("-L"), std::string::npos)) << s; + expect(eq(s.find("-lmathkit"), std::string::npos)) << s; +}}; -TEST(LinkIntentSpelling, EveryDriverFlavourGetsTheGnuSpelling) { +UTest every_driver_flavour_gets_the_gnu_spelling{"LinkIntentSpelling/EveryDriverFlavourGetsTheGnuSpelling", [] { // PeGnu is not only MinGW: clang targeting the MSVC ABI lands here too, // because it is a compiler driver and takes `-L`. Handing it `/LIBPATH:` // because its OUTPUT is MSVC-ABI would be the mistake this test exists to @@ -58,25 +60,25 @@ TEST(LinkIntentSpelling, EveryDriverFlavourGetsTheGnuSpelling) { for (auto flavour : { LinkIntentFlavor::Elf, LinkIntentFlavor::MachO, LinkIntentFlavor::PeGnu }) { auto s = render_link_intent_flags(leg_intent(), flavour); - EXPECT_NE(s.find("-L"), std::string::npos) << s; - EXPECT_NE(s.find("-lmathkit"), std::string::npos) << s; - EXPECT_EQ(s.find("/LIBPATH:"), std::string::npos) << s; + expect(ne(s.find("-L"), std::string::npos)) << s; + expect(ne(s.find("-lmathkit"), std::string::npos)) << s; + expect(eq(s.find("/LIBPATH:"), std::string::npos)) << s; } -} +}}; -TEST(LinkIntentSpelling, AnExplicitTokenIsPassedThroughUntouched) { +UTest an_explicit_token_is_passed_through_untouched{"LinkIntentSpelling/AnExplicitTokenIsPassedThroughUntouched", [] { // A library named with a path or an extension is already a file, not a // name to decorate. Decorating it would produce `lib/x.lib.lib`. mcpp::manifest::LinkIntent i; i.libraries.push_back("lib/x86_64-windows-msvc/mathkit.lib"); auto s = render_link_intent_flags(i, LinkIntentFlavor::PeMsvc); - EXPECT_NE(s.find("mathkit.lib"), std::string::npos) << s; - EXPECT_EQ(s.find("mathkit.lib.lib"), std::string::npos) << s; -} + expect(ne(s.find("mathkit.lib"), std::string::npos)) << s; + expect(eq(s.find("mathkit.lib.lib"), std::string::npos)) << s; +}}; -TEST(LinkIntentSpelling, AnEmptyIntentRendersNothing) { +UTest an_empty_intent_renders_nothing{"LinkIntentSpelling/AnEmptyIntentRendersNothing", [] { // The generated manifest omits the block for legs that do not need it, and // an empty intent must not put a stray flag on the line. - EXPECT_TRUE(render_link_intent_flags({}, LinkIntentFlavor::PeMsvc).empty()); - EXPECT_TRUE(render_link_intent_flags({}, LinkIntentFlavor::Elf).empty()); -} + expect(render_link_intent_flags({}, LinkIntentFlavor::PeMsvc).empty()); + expect(render_link_intent_flags({}, LinkIntentFlavor::Elf).empty()); +}}; diff --git a/tests/unit/test_link_line.cpp b/tests/unit/test_link_line.cpp index 5a8d2def4..3f46a4bc2 100644 --- a/tests/unit/test_link_line.cpp +++ b/tests/unit/test_link_line.cpp @@ -9,11 +9,13 @@ // So these assertions are about the ORDER itself, not about "the flags look // roughly right". Each one names the failure it prevents. -#include import std; +import mcpplibs.mtest; import mcpp.build.link_line; +using namespace mcpplibs::mtest; + namespace ll = mcpp::build::link_line; namespace { @@ -23,7 +25,7 @@ namespace { // two huge numbers and no clue which one was missing. std::size_t pos_of(std::string_view haystack, std::string_view needle) { auto p = haystack.find(needle); - EXPECT_NE(p, std::string_view::npos) << "missing '" << needle << "' in: " << haystack; + expect(ne(p, std::string_view::npos)) << "missing '" << needle << "' in: " << haystack; return p; } @@ -42,66 +44,64 @@ ll::UnitTail full_tail() { // resolved against; the farm is a mutable view that may hold another build of // the same SONAME. Farm-first is how an already-linked artifact silently // starts loading a different library. -TEST(LinkLine, ArtifactDirectoryPrecedesTheRuntimeFallback) { +UTest artifact_directory_precedes_the_runtime_fallback{"LinkLine/ArtifactDirectoryPrecedesTheRuntimeFallback", [] { auto line = full_tail().render(); - EXPECT_LT(pos_of(line, "$ORIGIN"), pos_of(line, "subos/default/lib")) << line; -} + expect(lt(pos_of(line, "$ORIGIN"), pos_of(line, "subos/default/lib"))) << line; +}}; // ld honours the LAST `--enable/--disable-new-dtags` it sees, and both gcc // specs and clang config files supply the opposite of what mcpp wants. A slot // appended after this one silently changes RPATH into RUNPATH (or back), which // changes whether the tag applies to transitive dependencies at all. -TEST(LinkLine, LoaderTagIsLiterallyLast) { +UTest loader_tag_is_literally_last{"LinkLine/LoaderTagIsLiterallyLast", [] { auto line = full_tail().render(); - EXPECT_TRUE(line.ends_with("-Wl,--disable-new-dtags")) + expect(line.ends_with("-Wl,--disable-new-dtags")) << "something was emitted after the loader tag: " << line; - EXPECT_GT(pos_of(line, "--disable-new-dtags"), - pos_of(line, "subos/default/lib")) << line; -} + expect(gt(pos_of(line, "--disable-new-dtags"), pos_of(line, "subos/default/lib"))) << line; +}}; // The C++ runtime archive is processed after the dependencies it might // otherwise pre-empt: an archive member is pulled only for symbols still // undefined when the archive is reached. -TEST(LinkLine, CxxRuntimeFollowsDependenciesAndPrecedesTheFallback) { +UTest cxx_runtime_follows_dependencies_and_precedes_the_fallback{"LinkLine/CxxRuntimeFollowsDependenciesAndPrecedesTheFallback", [] { auto line = full_tail().render(); - EXPECT_LT(pos_of(line, "-lX11"), pos_of(line, "-static-libstdc++")) << line; - EXPECT_LT(pos_of(line, "-static-libstdc++"), pos_of(line, "subos/default/lib")) << line; -} + expect(lt(pos_of(line, "-lX11"), pos_of(line, "-static-libstdc++"))) << line; + expect(lt(pos_of(line, "-static-libstdc++"), pos_of(line, "subos/default/lib"))) << line; +}}; // PE has no rpath and no loader tag; Mach-O has no farm. A format that fills // only some slots must not acquire stray separators — the rendered line is // compared byte-for-byte against build.ninja by other tests. -TEST(LinkLine, EmptySlotsProduceNoStraySeparators) { +UTest empty_slots_produce_no_stray_separators{"LinkLine/EmptySlotsProduceNoStraySeparators", [] { ll::UnitTail t; t.cxxRuntime = " -static"; - EXPECT_EQ(t.render(), " -static"); + expect(eq(t.render(), " -static")); ll::UnitTail only_deps; only_deps.dependencies = " target/bin/foo.lib"; - EXPECT_EQ(only_deps.render(), " target/bin/foo.lib"); + expect(eq(only_deps.render(), " target/bin/foo.lib")); - EXPECT_EQ(ll::UnitTail{}.render(), ""); - EXPECT_TRUE(ll::UnitTail{}.empty()); - EXPECT_FALSE(full_tail().empty()); -} + expect(eq(ll::UnitTail{}.render(), "")); + expect(ll::UnitTail{}.empty()); + expect(!(full_tail().empty())); +}}; // A producer that forgets the leading space must not weld two flags together. // Every producer supplies it today; this keeps that from being load-bearing. -TEST(LinkLine, SlotWithoutLeadingSpaceGetsASeparator) { +UTest slot_without_leading_space_gets_a_separator{"LinkLine/SlotWithoutLeadingSpaceGetsASeparator", [] { ll::UnitTail t; t.dependencies = " -lfoo"; t.cxxRuntime = "-static-libstdc++"; // no leading space - EXPECT_EQ(t.render(), " -lfoo -static-libstdc++"); -} + expect(eq(t.render(), " -lfoo -static-libstdc++")); +}}; // Order is a property of the type, not of the caller's assignment order. -TEST(LinkLine, RenderOrderIsIndependentOfAssignmentOrder) { +UTest render_order_is_independent_of_assignment_order{"LinkLine/RenderOrderIsIndependentOfAssignmentOrder", [] { ll::UnitTail t; t.loaderTag = "-Wl,--disable-new-dtags"; t.runtimeFallback = " -Wl,-rpath,/farm"; t.cxxRuntime = " -static-libstdc++"; t.dependencies = " -Wl,-rpath,'$ORIGIN'"; - EXPECT_EQ(t.render(), - " -Wl,-rpath,'$ORIGIN' -static-libstdc++ -Wl,-rpath,/farm" - " -Wl,--disable-new-dtags"); -} + expect(eq(t.render(), " -Wl,-rpath,'$ORIGIN' -static-libstdc++ -Wl,-rpath,/farm" + " -Wl,--disable-new-dtags")); +}}; diff --git a/tests/unit/test_link_model_runtime_dirs.cpp b/tests/unit/test_link_model_runtime_dirs.cpp index a01914481..0bca89755 100644 --- a/tests/unit/test_link_model_runtime_dirs.cpp +++ b/tests/unit/test_link_model_runtime_dirs.cpp @@ -20,14 +20,16 @@ // passed with the defect in place. The link model takes its mode from its // input, so a test can name the mode instead of hoping for it. -#include import std; +import mcpplibs.mtest; import mcpp.build.flags; import mcpp.manifest.types; import mcpp.toolchain.linkmodel; import mcpp.toolchain.model; +using namespace mcpplibs::mtest; + namespace tc = mcpp::toolchain; namespace { @@ -80,48 +82,46 @@ std::string joined(const tc::ToolchainLinkModel& lm) { return s; } -TEST(LinkModelRuntimeDirs, PayloadFirstCarriesTheCompilerRuntime) { +UTest payload_first_carries_the_compiler_runtime{"LinkModelRuntimeDirs/PayloadFirstCarriesTheCompilerRuntime", [] { Payload p; auto lm = tc::resolve_link_model(p.toolchain(/*withSysroot=*/false)); - ASSERT_EQ(lm.mode, tc::CLibMode::PayloadFirst); + expect(fatal(eq(lm.mode, tc::CLibMode::PayloadFirst))); auto line = joined(lm); - EXPECT_NE(line.find("-Wl,-rpath," + p.gccLib.string()), std::string::npos) + expect(ne(line.find("-Wl,-rpath," + p.gccLib.string()), std::string::npos)) << "libgcc_s.so.1 lives here and nothing else will find it:\n" << line; -} +}}; -TEST(LinkModelRuntimeDirs, SysrootCarriesTheCompilerRuntime) { +UTest sysroot_carries_the_compiler_runtime{"LinkModelRuntimeDirs/SysrootCarriesTheCompilerRuntime", [] { Payload p; auto lm = tc::resolve_link_model(p.toolchain(/*withSysroot=*/true)); - ASSERT_EQ(lm.mode, tc::CLibMode::Sysroot); + expect(fatal(eq(lm.mode, tc::CLibMode::Sysroot))); auto line = joined(lm); - EXPECT_NE(line.find("-Wl,-rpath," + p.gccLib.string()), std::string::npos) + expect(ne(line.find("-Wl,-rpath," + p.gccLib.string()), std::string::npos)) << line; -} +}}; // The C library's own directory, in both modes, for the same reason. -TEST(LinkModelRuntimeDirs, BothModesCarryTheCLibrary) { +UTest both_modes_carry_the_c_library{"LinkModelRuntimeDirs/BothModesCarryTheCLibrary", [] { Payload p; for (bool withSysroot : {false, true}) { auto line = joined(tc::resolve_link_model(p.toolchain(withSysroot))); - EXPECT_NE(line.find("-Wl,-rpath," + p.glibcLib.string()), - std::string::npos) + expect(ne(line.find("-Wl,-rpath," + p.glibcLib.string()), std::string::npos)) << "withSysroot=" << withSysroot << "\n" << line; } -} +}}; // And the interpreter. A sysroot says where headers live; it says nothing // about which loader runs the result, and gcc's `*link:` no longer answers. -TEST(LinkModelRuntimeDirs, BothModesNameTheInterpreter) { +UTest both_modes_name_the_interpreter{"LinkModelRuntimeDirs/BothModesNameTheInterpreter", [] { Payload p; for (bool withSysroot : {false, true}) { auto line = joined(tc::resolve_link_model(p.toolchain(withSysroot))); - EXPECT_NE(line.find("--dynamic-linker=" + p.glibcLib.string()), - std::string::npos) + expect(ne(line.find("--dynamic-linker=" + p.glibcLib.string()), std::string::npos)) << "withSysroot=" << withSysroot << "\n" << line; } -} +}}; -TEST(LinkIntent, KeepsLinkSearchTransitiveSearchAndRuntimeSearchSeparate) { +UTest keeps_link_search_transitive_search_and_runtime_search_separate{"LinkIntent/KeepsLinkSearchTransitiveSearchAndRuntimeSearchSeparate", [] { mcpp::manifest::LinkIntent intent; intent.libraries = {"widget"}; intent.linkLibraryDirs = {"/contract/link"}; @@ -132,26 +132,26 @@ TEST(LinkIntent, KeepsLinkSearchTransitiveSearchAndRuntimeSearchSeparate) { auto elf = mcpp::build::render_link_intent_flags( intent, mcpp::build::LinkIntentFlavor::Elf); - EXPECT_NE(elf.find("-L/contract/link"), std::string::npos) << elf; - EXPECT_NE(elf.find("-Wl,-rpath-link,/contract/needed"), std::string::npos) + expect(ne(elf.find("-L/contract/link"), std::string::npos)) << elf; + expect(ne(elf.find("-Wl,-rpath-link,/contract/needed"), std::string::npos)) << elf; - EXPECT_NE(elf.find("-Wl,-rpath,/contract/run"), std::string::npos) << elf; - EXPECT_EQ(elf.find("-L/contract/run"), std::string::npos) << elf; - EXPECT_EQ(elf.find("widget.dll"), std::string::npos) << elf; + expect(ne(elf.find("-Wl,-rpath,/contract/run"), std::string::npos)) << elf; + expect(eq(elf.find("-L/contract/run"), std::string::npos)) << elf; + expect(eq(elf.find("widget.dll"), std::string::npos)) << elf; auto macho = mcpp::build::render_link_intent_flags( intent, mcpp::build::LinkIntentFlavor::MachO); - EXPECT_NE(macho.find("-Wl,-rpath,/contract/run"), std::string::npos) + expect(ne(macho.find("-Wl,-rpath,/contract/run"), std::string::npos)) << macho; - EXPECT_NE(macho.find("-framework WindowKit"), std::string::npos) << macho; - EXPECT_EQ(macho.find("rpath-link"), std::string::npos) << macho; + expect(ne(macho.find("-framework WindowKit"), std::string::npos)) << macho; + expect(eq(macho.find("rpath-link"), std::string::npos)) << macho; auto pe = mcpp::build::render_link_intent_flags( intent, mcpp::build::LinkIntentFlavor::PeMsvc); - EXPECT_NE(pe.find("/LIBPATH$:/contract/link"), std::string::npos) << pe; - EXPECT_EQ(pe.find("rpath"), std::string::npos) << pe; - EXPECT_EQ(pe.find("/contract/run"), std::string::npos) << pe; - EXPECT_EQ(pe.find("widget.dll"), std::string::npos) << pe; -} + expect(ne(pe.find("/LIBPATH$:/contract/link"), std::string::npos)) << pe; + expect(eq(pe.find("rpath"), std::string::npos)) << pe; + expect(eq(pe.find("/contract/run"), std::string::npos)) << pe; + expect(eq(pe.find("widget.dll"), std::string::npos)) << pe; +}}; } // namespace diff --git a/tests/unit/test_linkage_form.cpp b/tests/unit/test_linkage_form.cpp index 2a3099f02..d7e489720 100644 --- a/tests/unit/test_linkage_form.cpp +++ b/tests/unit/test_linkage_form.cpp @@ -6,11 +6,13 @@ // against measurements, so they are regression tests for a decision as much as // for code: the `-L` rule (§4.2) and the libc-linkage coupling (§12.3). -#include import std; +import mcpplibs.mtest; import mcpp.build.linkage_form; +using namespace mcpplibs::mtest; + namespace lf = mcpp::build::linkage_form; namespace { @@ -35,72 +37,71 @@ lf::Request want(lf::DepLinkage linkage) { // ── the default is byte-for-byte the old behaviour ───────────────────────── -TEST(LinkageForm, DefaultRequestIsStaticAndSilent) { +UTest default_request_is_static_and_silent{"LinkageForm/DefaultRequestIsStaticAndSilent", [] { lf::Request none; // nobody wrote anything - EXPECT_EQ(none.whole, lf::DepLinkage::Static); - EXPECT_FALSE(none.wholeIsExplicit); + expect(eq(none.whole, lf::DepLinkage::Static)); + expect(!none.wholeIsExplicit); auto pkg = source_package(); auto answer = lf::resolve(pkg, lf::admissible(pkg, hosted()), none); - EXPECT_EQ(answer.linkage, lf::DepLinkage::Static); - EXPECT_TRUE(answer.diagnostic.empty()); -} + expect(eq(answer.linkage, lf::DepLinkage::Static)); + expect(answer.diagnostic.empty()); +}}; -TEST(LinkageForm, ASourcePackageCanBeEitherForm) { +UTest a_source_package_can_be_either_form{"LinkageForm/ASourcePackageCanBeEitherForm", [] { auto pkg = source_package(); auto allowed = lf::admissible(pkg, hosted()); - EXPECT_TRUE(allowed.staticOk); - EXPECT_TRUE(allowed.sharedOk); - EXPECT_EQ(lf::resolve(pkg, allowed, want(lf::DepLinkage::Shared)).linkage, - lf::DepLinkage::Shared); -} + expect(bool(allowed.staticOk)); + expect(bool(allowed.sharedOk)); + expect(eq(lf::resolve(pkg, allowed, want(lf::DepLinkage::Shared)).linkage, lf::DepLinkage::Shared)); +}}; // ── `kind = "shared"` is a constraint; `kind = "lib"` is not ─────────────── -TEST(LinkageForm, DeclaredSharedIsAConstraintNotAPreference) { +UTest declared_shared_is_a_constraint_not_a_preference{"LinkageForm/DeclaredSharedIsAConstraintNotAPreference", [] { auto pkg = source_package(); pkg.declaredShared = true; auto allowed = lf::admissible(pkg, hosted()); - EXPECT_FALSE(allowed.staticOk); - EXPECT_TRUE(allowed.sharedOk); + expect(!allowed.staticOk); + expect(bool(allowed.sharedOk)); // Asking for static does not get it, and says so. auto answer = lf::resolve(pkg, allowed, want(lf::DepLinkage::Static)); - EXPECT_EQ(answer.linkage, lf::DepLinkage::Shared); - EXPECT_FALSE(answer.diagnostic.empty()); -} + expect(eq(answer.linkage, lf::DepLinkage::Shared)); + expect(!answer.diagnostic.empty()); +}}; -TEST(LinkageForm, NotDeclaringSharedIsNotAConstraintToBeStatic) { +UTest not_declaring_shared_is_not_a_constraint_to_be_static{"LinkageForm/NotDeclaringSharedIsNotAConstraintToBeStatic", [] { // `kind = "lib"` is the parser's DEFAULT, written as boilerplate by 84 of // the 130 packages in mcpp-index. Reading it as "must be static" would // lock the whole ecosystem out of this axis, so absence must stay silent. auto pkg = source_package(); - ASSERT_FALSE(pkg.declaredShared); - EXPECT_TRUE(lf::admissible(pkg, hosted()).sharedOk); -} + expect(fatal(!pkg.declaredShared)); + expect(bool(lf::admissible(pkg, hosted()).sharedOk)); +}}; // ── the `-L` rule (round-two correction) ─────────────────────────────────── -TEST(LinkageForm, ForeignLinkInputsAreRecognisedInEverySpelling) { - EXPECT_TRUE(lf::carries_foreign_link_inputs( - std::vector{"-Llib", "-l:libssl.a"})); - EXPECT_TRUE(lf::carries_foreign_link_inputs( - std::vector{"-L", "lib"})); - EXPECT_TRUE(lf::carries_foreign_link_inputs( - std::vector{"-Wl,-Llib"})); - EXPECT_TRUE(lf::carries_foreign_link_inputs( - std::vector{"/LIBPATH:lib"})); -} - -TEST(LinkageForm, SystemLibrariesAreNotForeignLinkInputs) { +UTest foreign_link_inputs_are_recognised_in_every_spelling{"LinkageForm/ForeignLinkInputsAreRecognisedInEverySpelling", [] { + expect(bool(lf::carries_foreign_link_inputs( + std::vector{"-Llib", "-l:libssl.a"}))); + expect(bool(lf::carries_foreign_link_inputs( + std::vector{"-L", "lib"}))); + expect(bool(lf::carries_foreign_link_inputs( + std::vector{"-Wl,-Llib"}))); + expect(bool(lf::carries_foreign_link_inputs( + std::vector{"/LIBPATH:lib"}))); +}}; + +UTest system_libraries_are_not_foreign_link_inputs{"LinkageForm/SystemLibrariesAreNotForeignLinkInputs", [] { // The 27 mcpp-index packages whose ldflags name only host libraries must // stay eligible: a shared object depending on libm is ordinary. - EXPECT_FALSE(lf::carries_foreign_link_inputs( + expect(!lf::carries_foreign_link_inputs( std::vector{"-lm", "-lpthread", "-ldl", "-lrt"})); - EXPECT_FALSE(lf::carries_foreign_link_inputs( + expect(!lf::carries_foreign_link_inputs( std::vector{"-lws2_32", "-lbcrypt", "-ladvapi32"})); -} +}}; -TEST(LinkageForm, APackageCarryingPrebuiltArchivesCannotBeShared) { +UTest a_package_carrying_prebuilt_archives_cannot_be_shared{"LinkageForm/APackageCarryingPrebuiltArchivesCannotBeShared", [] { // compat.openssl's shape: one anchor TU that mcpp compiles, plus `.a` // archives it ships. Wrapping somebody else's non-PIC archive in a shared // object is not something mcpp can do, so the request is refused WITH a @@ -109,35 +110,34 @@ TEST(LinkageForm, APackageCarryingPrebuiltArchivesCannotBeShared) { pkg.label = "compat.openssl@3.5.0"; pkg.carriesForeignLinkInputs = true; auto allowed = lf::admissible(pkg, hosted()); - EXPECT_TRUE(allowed.staticOk); - EXPECT_FALSE(allowed.sharedOk); - EXPECT_NE(allowed.sharedRefusal.find("-L"), std::string::npos); + expect(bool(allowed.staticOk)); + expect(!allowed.sharedOk); + expect(ne(allowed.sharedRefusal.find("-L"), std::string::npos)); auto answer = lf::resolve(pkg, allowed, want(lf::DepLinkage::Shared)); - EXPECT_EQ(answer.linkage, lf::DepLinkage::Static); - EXPECT_FALSE(answer.diagnostic.empty()); -} + expect(eq(answer.linkage, lf::DepLinkage::Static)); + expect(!answer.diagnostic.empty()); +}}; -TEST(LinkageForm, APackageWithNoSourcesOfItsOwnCannotBeShared) { +UTest a_package_with_no_sources_of_its_own_cannot_be_shared{"LinkageForm/APackageWithNoSourcesOfItsOwnCannotBeShared", [] { lf::PackageFacts pkg; pkg.label = "vendor.glib@2.80.0"; pkg.hasSources = false; - EXPECT_FALSE(lf::admissible(pkg, hosted()).sharedOk); -} + expect(!(lf::admissible(pkg, hosted()).sharedOk)); +}}; // ── the target's own veto (round-two correction) ─────────────────────────── -TEST(LinkageForm, AFreestandingTargetHasNothingToLoadASharedLibraryWith) { +UTest a_freestanding_target_has_nothing_to_load_a_shared_library_with{"LinkageForm/AFreestandingTargetHasNothingToLoadASharedLibraryWith", [] { auto pkg = source_package(); lf::TargetFacts bare; bare.hasLoader = false; auto allowed = lf::admissible(pkg, bare); - EXPECT_FALSE(allowed.sharedOk); - EXPECT_EQ(lf::resolve(pkg, allowed, want(lf::DepLinkage::Shared)).linkage, - lf::DepLinkage::Static); -} + expect(!allowed.sharedOk); + expect(eq(lf::resolve(pkg, allowed, want(lf::DepLinkage::Shared)).linkage, lf::DepLinkage::Static)); +}}; -TEST(LinkageForm, AFullyStaticLibcVetoesTheSharedForm) { +UTest a_fully_static_libc_vetoes_the_shared_form{"LinkageForm/AFullyStaticLibcVetoesTheSharedForm", [] { // The two axes named `linkage` are NOT independent. A `-static` image // has no interpreter, so it cannot load a shared object — and musl // defaults to `linkage = "static"`, which makes this the COMMON path @@ -146,16 +146,16 @@ TEST(LinkageForm, AFullyStaticLibcVetoesTheSharedForm) { lf::TargetFacts staticLibc; staticLibc.fullStaticLibc = true; auto allowed = lf::admissible(pkg, staticLibc); - EXPECT_FALSE(allowed.sharedOk); - EXPECT_NE(allowed.sharedRefusal.find("static"), std::string::npos); + expect(!allowed.sharedOk); + expect(ne(allowed.sharedRefusal.find("static"), std::string::npos)); auto answer = lf::resolve(pkg, allowed, want(lf::DepLinkage::Shared)); - EXPECT_EQ(answer.linkage, lf::DepLinkage::Static); + expect(eq(answer.linkage, lf::DepLinkage::Static)); // It must SAY why, or the user edits the wrong key forever. - EXPECT_FALSE(answer.diagnostic.empty()); -} + expect(!answer.diagnostic.empty()); +}}; -TEST(LinkageForm, TheTargetVetoIsReportedBeforeThePackageOne) { +UTest the_target_veto_is_reported_before_the_package_one{"LinkageForm/TheTargetVetoIsReportedBeforeThePackageOne", [] { // Order is the order of the diagnostic: a reason the user cannot fix by // editing the package must not point at the package. lf::PackageFacts pkg = source_package(); @@ -163,36 +163,35 @@ TEST(LinkageForm, TheTargetVetoIsReportedBeforeThePackageOne) { lf::TargetFacts bare; bare.hasLoader = false; auto allowed = lf::admissible(pkg, bare); - EXPECT_EQ(allowed.sharedRefusal.find("compat.zlib"), std::string::npos); -} + expect(eq(allowed.sharedRefusal.find("compat.zlib"), std::string::npos)); +}}; // ── distribution packages answer from what they ship ─────────────────────── -TEST(LinkageForm, ADistributionPackageOffersOnlyTheLegsItShips) { +UTest a_distribution_package_offers_only_the_legs_it_ships{"LinkageForm/ADistributionPackageOffersOnlyTheLegsItShips", [] { lf::PackageFacts pkg; pkg.label = "acme.mathkit@0.1.0"; pkg.isDistribution = true; pkg.shipsStatic = true; auto allowed = lf::admissible(pkg, hosted()); - EXPECT_TRUE(allowed.staticOk); - EXPECT_FALSE(allowed.sharedOk); - EXPECT_NE(allowed.sharedRefusal.find("mathkit"), std::string::npos); + expect(bool(allowed.staticOk)); + expect(!allowed.sharedOk); + expect(ne(allowed.sharedRefusal.find("mathkit"), std::string::npos)); pkg.shipsShared = true; - EXPECT_TRUE(lf::admissible(pkg, hosted()).sharedOk); -} + expect(bool(lf::admissible(pkg, hosted()).sharedOk)); +}}; // ── per-package request ──────────────────────────────────────────────────── -TEST(LinkageForm, APerPackageRequestOverridesTheWholeGraphDefault) { +UTest a_per_package_request_overrides_the_whole_graph_default{"LinkageForm/APerPackageRequestOverridesTheWholeGraphDefault", [] { auto pkg = source_package(); lf::Request request; // default static, not explicit request.perPackage["compat.zlib@1.3.2"] = lf::DepLinkage::Shared; - EXPECT_EQ(lf::resolve(pkg, lf::admissible(pkg, hosted()), request).linkage, - lf::DepLinkage::Shared); -} + expect(eq(lf::resolve(pkg, lf::admissible(pkg, hosted()), request).linkage, lf::DepLinkage::Shared)); +}}; -TEST(LinkageForm, AnUnaskedForRefusalStaysSilent) { +UTest an_unasked_for_refusal_stays_silent{"LinkageForm/AnUnaskedForRefusalStaysSilent", [] { // mcpp promised nothing when nobody asked. A default that cannot be // honoured is not a broken promise, and warning about it on every build // puts noise on correct manifests. @@ -200,31 +199,31 @@ TEST(LinkageForm, AnUnaskedForRefusalStaysSilent) { pkg.declaredShared = true; // constrained to shared lf::Request none; // default static, NOT explicit auto answer = lf::resolve(pkg, lf::admissible(pkg, hosted()), none); - EXPECT_EQ(answer.linkage, lf::DepLinkage::Shared); - EXPECT_TRUE(answer.diagnostic.empty()); -} + expect(eq(answer.linkage, lf::DepLinkage::Shared)); + expect(answer.diagnostic.empty()); +}}; // ── PIC ──────────────────────────────────────────────────────────────────── -TEST(LinkageForm, PicFollowsAnySharedFormInTheGraph) { +UTest pic_follows_any_shared_form_in_the_graph{"LinkageForm/PicFollowsAnySharedFormInTheGraph", [] { using L = lf::DepLinkage; std::vector allStatic{L::Static, L::Static}; std::vector oneShared{L::Static, L::Shared}; - EXPECT_FALSE(lf::needs_pic(allStatic, /*anyOwnSharedTarget=*/false)); - EXPECT_TRUE(lf::needs_pic(allStatic, /*anyOwnSharedTarget=*/true)); - EXPECT_TRUE(lf::needs_pic(oneShared, /*anyOwnSharedTarget=*/false)); -} + expect(!lf::needs_pic(allStatic, /*anyOwnSharedTarget=*/false)); + expect(bool(lf::needs_pic(allStatic, /*anyOwnSharedTarget=*/true))); + expect(bool(lf::needs_pic(oneShared, /*anyOwnSharedTarget=*/false))); +}}; // ── vocabulary ───────────────────────────────────────────────────────────── -TEST(LinkageForm, TheVocabularyIsClosed) { - EXPECT_EQ(lf::parse("static"), lf::DepLinkage::Static); - EXPECT_EQ(lf::parse("shared"), lf::DepLinkage::Shared); - EXPECT_FALSE(lf::parse("dynamic").has_value()); // the libc axis' word - EXPECT_FALSE(lf::parse("").has_value()); - EXPECT_EQ(lf::to_string(lf::DepLinkage::Static), "static"); - EXPECT_EQ(lf::to_string(lf::DepLinkage::Shared), "shared"); -} +UTest the_vocabulary_is_closed{"LinkageForm/TheVocabularyIsClosed", [] { + expect(eq(lf::parse("static"), lf::DepLinkage::Static)); + expect(eq(lf::parse("shared"), lf::DepLinkage::Shared)); + expect(!(lf::parse("dynamic").has_value())); // the libc axis' word + expect(!(lf::parse("").has_value())); + expect(eq(lf::to_string(lf::DepLinkage::Static), "static")); + expect(eq(lf::to_string(lf::DepLinkage::Shared), "shared")); +}}; // ── a package's default form (#642 E1) ───────────────────────────────────── @@ -240,71 +239,68 @@ lf::PackageFacts defaulting_to(lf::DepLinkage form) { } // namespace -TEST(LinkageForm, APackageDefaultIsAnsweredWhenNobodyAsks) { +UTest a_package_default_is_answered_when_nobody_asks{"LinkageForm/APackageDefaultIsAnsweredWhenNobodyAsks", [] { auto pkg = defaulting_to(lf::DepLinkage::Shared); auto answer = lf::resolve(pkg, lf::admissible(pkg, hosted()), lf::Request{}); - EXPECT_EQ(answer.linkage, lf::DepLinkage::Shared); - EXPECT_EQ(answer.reason, "package-default"); - EXPECT_TRUE(answer.diagnostic.empty()); - EXPECT_TRUE(answer.note.empty()); -} + expect(eq(answer.linkage, lf::DepLinkage::Shared)); + expect(eq(answer.reason, "package-default")); + expect(answer.diagnostic.empty()); + expect(answer.note.empty()); +}}; -TEST(LinkageForm, APackageDefaultIsNotAConstraint) { +UTest a_package_default_is_not_a_constraint{"LinkageForm/APackageDefaultIsNotAConstraint", [] { // The difference from `kind = "shared"` is the whole feature: both forms // stay admissible, so a request for the other one is not refused. auto pkg = defaulting_to(lf::DepLinkage::Shared); auto allowed = lf::admissible(pkg, hosted()); - EXPECT_TRUE(allowed.staticOk); - EXPECT_TRUE(allowed.sharedOk); - EXPECT_TRUE(allowed.constraint.empty()); -} + expect(bool(allowed.staticOk)); + expect(bool(allowed.sharedOk)); + expect(allowed.constraint.empty()); +}}; -TEST(LinkageForm, AnEdgeRequestOverridesThePackageDefaultAndNamesBothStatements) { +UTest an_edge_request_overrides_the_package_default_and_names_both_statements{"LinkageForm/AnEdgeRequestOverridesThePackageDefaultAndNamesBothStatements", [] { auto pkg = defaulting_to(lf::DepLinkage::Shared); lf::Request request; request.perPackage[pkg.label] = lf::DepLinkage::Static; auto answer = lf::resolve(pkg, lf::admissible(pkg, hosted()), request); - EXPECT_EQ(answer.linkage, lf::DepLinkage::Static); - EXPECT_EQ(answer.reason, "requested"); + expect(eq(answer.linkage, lf::DepLinkage::Static)); + expect(eq(answer.reason, "requested")); // Honoured, so not a degradation: `--strict` must accept it. - EXPECT_TRUE(answer.diagnostic.empty()); - EXPECT_NE(answer.note.find("`linkage = \"static\"` on this dependency"), - std::string::npos) << answer.note; - EXPECT_NE(answer.note.find("[targets.zlib] linkage = \"shared\""), - std::string::npos) << answer.note; -} + expect(answer.diagnostic.empty()); + expect(ne(answer.note.find("`linkage = \"static\"` on this dependency"), std::string::npos)) << answer.note; + expect(ne(answer.note.find("[targets.zlib] linkage = \"shared\""), std::string::npos)) << answer.note; +}}; -TEST(LinkageForm, AWrittenWholeGraphValueOutranksThePackageDefault) { +UTest a_written_whole_graph_value_outranks_the_package_default{"LinkageForm/AWrittenWholeGraphValueOutranksThePackageDefault", [] { auto pkg = defaulting_to(lf::DepLinkage::Shared); auto answer = lf::resolve(pkg, lf::admissible(pkg, hosted()), want(lf::DepLinkage::Static)); - EXPECT_EQ(answer.linkage, lf::DepLinkage::Static); - EXPECT_EQ(answer.reason, "requested"); - EXPECT_NE(answer.note.find("dependency_linkage = \"static\""), std::string::npos) + expect(eq(answer.linkage, lf::DepLinkage::Static)); + expect(eq(answer.reason, "requested")); + expect(ne(answer.note.find("dependency_linkage = \"static\""), std::string::npos)) << answer.note; -} +}}; -TEST(LinkageForm, AnUnwrittenWholeGraphValueDoesNotOutrankThePackageDefault) { +UTest an_unwritten_whole_graph_value_does_not_outrank_the_package_default{"LinkageForm/AnUnwrittenWholeGraphValueDoesNotOutrankThePackageDefault", [] { // `whole` is static by construction; only a human writing it makes it a // statement, and mcpp's own default is exactly what a package default // replaces. auto pkg = defaulting_to(lf::DepLinkage::Shared); lf::Request none; - ASSERT_EQ(none.whole, lf::DepLinkage::Static); - EXPECT_EQ(lf::resolve(pkg, lf::admissible(pkg, hosted()), none).linkage, - lf::DepLinkage::Shared); -} + expect(fatal(eq(none.whole, lf::DepLinkage::Static))); + expect(eq(lf::resolve(pkg, lf::admissible(pkg, hosted()), none).linkage, lf::DepLinkage::Shared)); +}}; -TEST(LinkageForm, ARequestThatAgreesWithTheDefaultPrintsNothing) { +UTest a_request_that_agrees_with_the_default_prints_nothing{"LinkageForm/ARequestThatAgreesWithTheDefaultPrintsNothing", [] { auto pkg = defaulting_to(lf::DepLinkage::Shared); lf::Request request; request.perPackage[pkg.label] = lf::DepLinkage::Shared; auto answer = lf::resolve(pkg, lf::admissible(pkg, hosted()), request); - EXPECT_EQ(answer.reason, "requested"); - EXPECT_TRUE(answer.note.empty()); -} + expect(eq(answer.reason, "requested")); + expect(answer.note.empty()); +}}; -TEST(LinkageForm, ADefaultTheTargetCannotHonourFallsBackWithoutAWord) { +UTest a_default_the_target_cannot_honour_falls_back_without_a_word{"LinkageForm/ADefaultTheTargetCannotHonourFallsBackWithoutAWord", [] { // Nobody asked, so there is no broken promise to report: the default is // the package's preference, and a fully static image cannot load a // shared library at all. @@ -312,8 +308,8 @@ TEST(LinkageForm, ADefaultTheTargetCannotHonourFallsBackWithoutAWord) { lf::TargetFacts target; target.fullStaticLibc = true; auto answer = lf::resolve(pkg, lf::admissible(pkg, target), lf::Request{}); - EXPECT_EQ(answer.linkage, lf::DepLinkage::Static); - EXPECT_EQ(answer.reason, "static-libc"); - EXPECT_TRUE(answer.diagnostic.empty()); - EXPECT_TRUE(answer.note.empty()); -} + expect(eq(answer.linkage, lf::DepLinkage::Static)); + expect(eq(answer.reason, "static-libc")); + expect(answer.diagnostic.empty()); + expect(answer.note.empty()); +}}; diff --git a/tests/unit/test_linkmodel.cpp b/tests/unit/test_linkmodel.cpp index bf5710b30..3674de779 100644 --- a/tests/unit/test_linkmodel.cpp +++ b/tests/unit/test_linkmodel.cpp @@ -1,9 +1,11 @@ -#include import std; +import mcpplibs.mtest; import mcpp.toolchain.linkmodel; import mcpp.toolchain.model; +using namespace mcpplibs::mtest; + // The toolchain link model is the single resolver for "how do we compile and // link against this toolchain's C library" (issue #195 / the hermetic link // model design doc). These tests pin its three contracts: @@ -37,52 +39,49 @@ void touch(const std::filesystem::path& p) { std::string ident(const std::filesystem::path& p) { return p.string(); } -TEST(LoaderFilename, TripleMap) { - EXPECT_EQ(tc::loader_filename("x86_64-unknown-linux-gnu"), "ld-linux-x86-64.so.2"); - EXPECT_EQ(tc::loader_filename("aarch64-linux-gnu"), "ld-linux-aarch64.so.1"); - EXPECT_EQ(tc::loader_filename("x86_64-linux-musl"), "ld-musl-x86_64.so.1"); - EXPECT_EQ(tc::loader_filename("aarch64-linux-musl"), "ld-musl-aarch64.so.1"); - EXPECT_EQ(tc::loader_filename("wasm32-unknown-unknown"), ""); -} - -TEST(DistroLoaderPath, LsbLayoutPerArch) { - EXPECT_EQ(tc::distro_loader_path("x86_64-linux-gnu"), "/lib64/ld-linux-x86-64.so.2"); - EXPECT_EQ(tc::distro_loader_path("aarch64-linux-gnu"), "/lib/ld-linux-aarch64.so.1"); - EXPECT_EQ(tc::distro_loader_path("mips64-unknown"), ""); -} - -TEST(ResolveLoader, TripleMapHit) { +UTest triple_map{"LoaderFilename/TripleMap", [] { + expect(eq(tc::loader_filename("x86_64-unknown-linux-gnu"), "ld-linux-x86-64.so.2")); + expect(eq(tc::loader_filename("aarch64-linux-gnu"), "ld-linux-aarch64.so.1")); + expect(eq(tc::loader_filename("x86_64-linux-musl"), "ld-musl-x86_64.so.1")); + expect(eq(tc::loader_filename("aarch64-linux-musl"), "ld-musl-aarch64.so.1")); + expect(eq(tc::loader_filename("wasm32-unknown-unknown"), "")); +}}; + +UTest lsb_layout_per_arch{"DistroLoaderPath/LsbLayoutPerArch", [] { + expect(eq(tc::distro_loader_path("x86_64-linux-gnu"), "/lib64/ld-linux-x86-64.so.2")); + expect(eq(tc::distro_loader_path("aarch64-linux-gnu"), "/lib/ld-linux-aarch64.so.1")); + expect(eq(tc::distro_loader_path("mips64-unknown"), "")); +}}; + +UTest triple_map_hit{"ResolveLoader/TripleMapHit", [] { Tmp dir; auto lib = dir.path / "lib64"; touch(lib / "ld-linux-x86-64.so.2"); - EXPECT_EQ(tc::resolve_loader(lib, "x86_64-unknown-linux-gnu"), - lib / "ld-linux-x86-64.so.2"); -} + expect(eq(tc::resolve_loader(lib, "x86_64-unknown-linux-gnu"), lib / "ld-linux-x86-64.so.2")); +}}; -TEST(ResolveLoader, GlobFallbackForUnknownTriple) { +UTest glob_fallback_for_unknown_triple{"ResolveLoader/GlobFallbackForUnknownTriple", [] { Tmp dir; auto lib = dir.path / "lib"; touch(lib / "ld-linux-aarch64.so.1"); auto got = tc::resolve_loader(lib, ""); // no triple info at all - EXPECT_EQ(got, lib / "ld-linux-aarch64.so.1"); -} + expect(eq(got, lib / "ld-linux-aarch64.so.1")); +}}; -TEST(ResolveLoader, EmptyWhenNothingFound) { +UTest empty_when_nothing_found{"ResolveLoader/EmptyWhenNothingFound", [] { Tmp dir; auto lib = dir.path / "lib64"; std::filesystem::create_directories(lib); - EXPECT_TRUE(tc::resolve_loader(lib, "x86_64-linux-gnu").empty()); -} + expect(tc::resolve_loader(lib, "x86_64-linux-gnu").empty()); +}}; -TEST(PayloadLibDir, PrefersLib64ThenLib) { +UTest prefers_lib64_then_lib{"PayloadLibDir/PrefersLib64ThenLib", [] { Tmp dir; touch(dir.path / "lib" / "ld-linux-x86-64.so.2"); - EXPECT_EQ(tc::payload_lib_dir_with_loader(dir.path, "x86_64-linux-gnu"), - dir.path / "lib"); + expect(eq(tc::payload_lib_dir_with_loader(dir.path, "x86_64-linux-gnu"), dir.path / "lib")); touch(dir.path / "lib64" / "ld-linux-x86-64.so.2"); - EXPECT_EQ(tc::payload_lib_dir_with_loader(dir.path, "x86_64-linux-gnu"), - dir.path / "lib64"); -} + expect(eq(tc::payload_lib_dir_with_loader(dir.path, "x86_64-linux-gnu"), dir.path / "lib64")); +}}; // Fabricate a bundled-LLVM-style toolchain: bin/clang++ + bin/clang++.cfg, // libc++ headers, and a glibc payload. @@ -111,37 +110,37 @@ struct FakeClangWithPayload { } }; -TEST(LinkModel, ClangCfgPayloadFirstCarriesCrtDiscovery) { +UTest clang_cfg_payload_first_carries_crt_discovery{"LinkModel/ClangCfgPayloadFirstCarriesCrtDiscovery", [] { FakeClangWithPayload fx; auto lm = tc::resolve_link_model(fx.t); - EXPECT_EQ(lm.mode, tc::CLibMode::PayloadFirst); - EXPECT_TRUE(lm.clangWithCfg); - EXPECT_EQ(lm.crtDir, fx.glibcLib); - EXPECT_EQ(lm.loader, fx.glibcLib / "ld-linux-x86-64.so.2"); + expect(eq(lm.mode, tc::CLibMode::PayloadFirst)); + expect(bool(lm.clangWithCfg)); + expect(eq(lm.crtDir, fx.glibcLib)); + expect(eq(lm.loader, fx.glibcLib / "ld-linux-x86-64.so.2")); auto link = lm.link_flags(ident); // -B is the #195 fix: CRT objects resolve via -B, never -L. - EXPECT_NE(link.find(" -B" + fx.glibcLib.string()), std::string::npos); - EXPECT_NE(link.find(" -L" + fx.glibcLib.string()), std::string::npos); - EXPECT_NE(link.find("--dynamic-linker=" + lm.loader.string()), std::string::npos); + expect(ne(link.find(" -B" + fx.glibcLib.string()), std::string::npos)); + expect(ne(link.find(" -L" + fx.glibcLib.string()), std::string::npos)); + expect(ne(link.find("--dynamic-linker=" + lm.loader.string()), std::string::npos)); auto compile = lm.compile_flags(ident); - EXPECT_NE(compile.find("-nostdlibinc"), std::string::npos); - EXPECT_NE(compile.find("-isystem"), std::string::npos); - EXPECT_EQ(compile.find("-idirafter"), std::string::npos); + expect(ne(compile.find("-nostdlibinc"), std::string::npos)); + expect(ne(compile.find("-isystem"), std::string::npos)); + expect(eq(compile.find("-idirafter"), std::string::npos)); // `-nostdlibinc` already closes clang's host search; no -isysroot. - EXPECT_EQ(compile.find("-isysroot"), std::string::npos); -} + expect(eq(compile.find("-isysroot"), std::string::npos)); +}}; -TEST(LinkModel, ClangDriverModelExposesCfgAndHeaders) { +UTest clang_driver_model_exposes_cfg_and_headers{"LinkModel/ClangDriverModelExposesCfgAndHeaders", [] { FakeClangWithPayload fx; auto dm = tc::resolve_clang_driver(fx.t); - EXPECT_TRUE(dm.hasCfg); - ASSERT_FALSE(dm.cxxIncludes.empty()); - EXPECT_NE(dm.compile_flags(ident).find("--no-default-config"), std::string::npos); -} + expect(bool(dm.hasCfg)); + expect(fatal(!dm.cxxIncludes.empty())); + expect(ne(dm.compile_flags(ident).find("--no-default-config"), std::string::npos)); +}}; -TEST(LinkModel, GccSysrootWinsOverPayload) { +UTest gcc_sysroot_wins_over_payload{"LinkModel/GccSysrootWinsOverPayload", [] { Tmp dir; auto sysroot = dir.path / "sysroot"; touch(sysroot / "usr" / "include" / "stdlib.h"); @@ -157,17 +156,17 @@ TEST(LinkModel, GccSysrootWinsOverPayload) { .linuxInclude = dir.path / "linux" / "include", }; auto lm = tc::resolve_link_model(t); - EXPECT_EQ(lm.mode, tc::CLibMode::Sysroot); - EXPECT_NE(lm.compile_flags(ident).find("--sysroot="), std::string::npos); - EXPECT_EQ(lm.compile_flags(ident).find("-nostdlibinc"), std::string::npos); + expect(eq(lm.mode, tc::CLibMode::Sysroot)); + expect(ne(lm.compile_flags(ident).find("--sysroot="), std::string::npos)); + expect(eq(lm.compile_flags(ident).find("-nostdlibinc"), std::string::npos)); // --sysroot already replaces the recorded sysroot. - EXPECT_EQ(lm.compile_flags(ident).find("-isysroot"), std::string::npos); - EXPECT_NE(lm.link_flags(ident).find("--sysroot="), std::string::npos); + expect(eq(lm.compile_flags(ident).find("-isysroot"), std::string::npos)); + expect(ne(lm.link_flags(ident).find("--sysroot="), std::string::npos)); // Kernel headers exist in the sysroot → no supplement. - EXPECT_TRUE(lm.systemIncludes.empty()); -} + expect(lm.systemIncludes.empty()); +}}; -TEST(LinkModel, GccSysrootSupplementsMissingKernelHeaders) { +UTest gcc_sysroot_supplements_missing_kernel_headers{"LinkModel/GccSysrootSupplementsMissingKernelHeaders", [] { Tmp dir; auto sysroot = dir.path / "sysroot"; touch(sysroot / "usr" / "include" / "stdlib.h"); // no linux/limits.h @@ -181,12 +180,12 @@ TEST(LinkModel, GccSysrootSupplementsMissingKernelHeaders) { .linuxInclude = dir.path / "linux" / "include", }; auto lm = tc::resolve_link_model(t); - ASSERT_EQ(lm.systemIncludes.size(), 1u); + expect(fatal(eq(lm.systemIncludes.size(), 1u))); // Sysroot-mode supplement renders as -isystem even for GCC. - EXPECT_NE(lm.compile_flags(ident).find("-isystem"), std::string::npos); -} + expect(ne(lm.compile_flags(ident).find("-isystem"), std::string::npos)); +}}; -TEST(LinkModel, GccPayloadEmitsIdirafterAndItsOwnAddressing) { +UTest gcc_payload_emits_idirafter_and_its_own_addressing{"LinkModel/GccPayloadEmitsIdirafterAndItsOwnAddressing", [] { Tmp dir; auto glibcLib = dir.path / "glibc" / "lib64"; touch(glibcLib / "ld-linux-x86-64.so.2"); @@ -199,20 +198,20 @@ TEST(LinkModel, GccPayloadEmitsIdirafterAndItsOwnAddressing) { .linuxInclude = {}, }; auto lm = tc::resolve_link_model(t); - EXPECT_EQ(lm.mode, tc::CLibMode::PayloadFirst); + expect(eq(lm.mode, tc::CLibMode::PayloadFirst)); // Headers still differ by driver: libstdc++'s #include_next wrappers need // -idirafter, and that has not changed. - EXPECT_NE(lm.compile_flags(ident).find("-idirafter"), std::string::npos); - EXPECT_EQ(lm.compile_flags(ident).find("-nostdlibinc"), std::string::npos); + expect(ne(lm.compile_flags(ident).find("-idirafter"), std::string::npos)); + expect(eq(lm.compile_flags(ident).find("-nostdlibinc"), std::string::npos)); // The sysroot GCC recorded when it was built is replaced, for headers // only, by the C library payload's root: `-isysroot ` as two argv // words, so `/usr/include` is never searched. auto tokens = lm.compile_tokens(ident); auto it = std::ranges::find(tokens, std::string("-isysroot")); - ASSERT_NE(it, tokens.end()); - ASSERT_NE(std::next(it), tokens.end()); - EXPECT_EQ(*std::next(it), (dir.path / "glibc").string()); + expect(fatal(ne(it, tokens.end()))); + expect(fatal(ne(std::next(it), tokens.end()))); + expect(eq(*std::next(it), (dir.path / "glibc").string())); // Addressing no longer differs. GCC used to be left to its install-time // specs here, which made the RUN side a per-toolchain-install decision @@ -220,21 +219,20 @@ TEST(LinkModel, GccPayloadEmitsIdirafterAndItsOwnAddressing) { // glibc versions as soon as a second one was installed. mcpp already // refuses to depend on clang's install-time cfg for exactly this reason. auto link = lm.link_flags(ident); - EXPECT_NE(link.find(" -B"), std::string::npos); - EXPECT_NE(link.find("--dynamic-linker=" + lm.loader.string()), - std::string::npos); - EXPECT_NE(link.find("-Wl,-rpath," + glibcLib.string()), std::string::npos); -} + expect(ne(link.find(" -B"), std::string::npos)); + expect(ne(link.find("--dynamic-linker=" + lm.loader.string()), std::string::npos)); + expect(ne(link.find("-Wl,-rpath," + glibcLib.string()), std::string::npos)); +}}; -TEST(LinkModel, NothingUsableYieldsNoneAndEmptyFlags) { +UTest nothing_usable_yields_none_and_empty_flags{"LinkModel/NothingUsableYieldsNoneAndEmptyFlags", [] { tc::Toolchain t; t.compiler = tc::CompilerId::Clang; t.targetTriple = "x86_64-linux-gnu"; auto lm = tc::resolve_link_model(t); - EXPECT_EQ(lm.mode, tc::CLibMode::None); - EXPECT_TRUE(lm.compile_flags(ident).empty()); - EXPECT_TRUE(lm.link_flags(ident).empty()); -} + expect(eq(lm.mode, tc::CLibMode::None)); + expect(lm.compile_flags(ident).empty()); + expect(lm.link_flags(ident).empty()); +}}; // 关于 `find_sibling_tool` 的顺序:**不在这里测**。 @@ -266,21 +264,21 @@ TEST(LinkModel, NothingUsableYieldsNoneAndEmptyFlags) { // correct: `wasm-ld: error: unknown argument: // --dynamic-linker=/lib64/ld-linux-x86-64.so.2` -- this host's loader // handed to a WebAssembly linker. -TEST(LinkModel, AnSdkTargetDescribesNoCLibrary) { +UTest an_sdk_target_describes_no_c_library{"LinkModel/AnSdkTargetDescribesNoCLibrary", [] { for (auto target : {"wasm32-emscripten", "aarch64-linux-android", "x86_64-linux-android"}) { auto tc = mcpp::toolchain::Toolchain{}; tc.compiler = mcpp::toolchain::CompilerId::Clang; tc.targetTriple = target; auto lm = mcpp::toolchain::resolve_link_model(tc); - EXPECT_EQ(lm.mode, mcpp::toolchain::CLibMode::None) << target; - EXPECT_TRUE(lm.libDirs.empty()) << target; - EXPECT_TRUE(lm.crtDir.empty()) << target; + expect(eq(lm.mode, mcpp::toolchain::CLibMode::None)) << target; + expect(lm.libDirs.empty()) << target; + expect(lm.crtDir.empty()) << target; // And the rendered flags are empty, which is what both channels emit. - EXPECT_TRUE(lm.link_flags([](const std::filesystem::path& p) { + expect(lm.link_flags([](const std::filesystem::path& p) { return p.string(); }).empty()) << target; } -} +}}; // ─── Clang on the MSVC ABI: the toolset and SDK, said to the driver ─────── @@ -296,7 +294,7 @@ tc::Toolchain clang_msvc() { } } // namespace -TEST(LinkModel, ClangOnTheMsvcAbiNamesItsToolsetAndSdk) { +UTest clang_on_the_msvc_abi_names_its_toolset_and_sdk{"LinkModel/ClangOnTheMsvcAbiNamesItsToolsetAndSdk", [] { auto t = clang_msvc(); auto words = tc::resolve_link_model(t).msvc_driver_tokens(ident); const std::vector expected = { @@ -304,49 +302,49 @@ TEST(LinkModel, ClangOnTheMsvcAbiNamesItsToolsetAndSdk) { "-Xmicrosoft-windows-sdk-root", t.base.msvc.sdkRoot.string(), "-Xmicrosoft-windows-sdk-version", "10.0.26100.0", }; - EXPECT_EQ(words, expected) << "separate words: the driver rejects the `=` form"; + expect(eq(words, expected)) << "separate words: the driver rejects the `=` form"; // Not a C-library mode: the other token sets stay empty. auto lm = tc::resolve_link_model(t); - EXPECT_EQ(lm.mode, tc::CLibMode::None); - EXPECT_TRUE(lm.compile_tokens(ident).empty()); -} + expect(eq(lm.mode, tc::CLibMode::None)); + expect(lm.compile_tokens(ident).empty()); +}}; // mcpp#746. The compiler version travels with the toolset: two runner images // held different cl.exe builds under one toolset directory name, and a key // that saw only the directory served one image's std module to the other. -TEST(LinkModel, TheCompilerVersionIsSaidWithTheToolset) { +UTest the_compiler_version_is_said_with_the_toolset{"LinkModel/TheCompilerVersionIsSaidWithTheToolset", [] { auto t = clang_msvc(); t.base.msvc.compilerVersion = "19.44.35211"; auto words = tc::resolve_link_model(t).msvc_driver_tokens(ident); - ASSERT_FALSE(words.empty()); - EXPECT_EQ(words.back(), "-fms-compatibility-version=19.44.35211"); + expect(fatal(!words.empty())); + expect(eq(words.back(), "-fms-compatibility-version=19.44.35211")); // A second build of cl.exe under the same directory is a different word. auto other = t; other.base.msvc.compilerVersion = "19.44.35217"; - EXPECT_NE(tc::resolve_link_model(other).msvc_driver_tokens(ident), words); -} + expect(ne(tc::resolve_link_model(other).msvc_driver_tokens(ident), words)); +}}; -TEST(LinkModel, TheToolsetWithoutAnSdkOmitsTheSdkWords) { +UTest the_toolset_without_an_sdk_omits_the_sdk_words{"LinkModel/TheToolsetWithoutAnSdkOmitsTheSdkWords", [] { auto t = clang_msvc(); t.base.msvc.sdkRoot.clear(); t.base.msvc.sdkVersion.clear(); auto words = tc::resolve_link_model(t).msvc_driver_tokens(ident); - ASSERT_EQ(words.size(), 2u); - EXPECT_EQ(words[0], "-Xmicrosoft-visualc-tools-root"); -} + expect(fatal(eq(words.size(), 2u))); + expect(eq(words[0], "-Xmicrosoft-visualc-tools-root")); +}}; -TEST(LinkModel, NoOtherRowReceivesMsvcDriverWords) { +UTest no_other_row_receives_msvc_driver_words{"LinkModel/NoOtherRowReceivesMsvcDriverWords", [] { // cl.exe carries the toolset in its own path and envOverrides; MinGW is // not MSVC at all; a row with no toolset recorded says nothing. auto cl = clang_msvc(); cl.compiler = tc::CompilerId::MSVC; - EXPECT_TRUE(tc::resolve_link_model(cl).msvc_driver_tokens(ident).empty()); + expect(tc::resolve_link_model(cl).msvc_driver_tokens(ident).empty()); auto mingw = clang_msvc(); mingw.targetTriple = "x86_64-w64-mingw32"; - EXPECT_TRUE(tc::resolve_link_model(mingw).msvc_driver_tokens(ident).empty()); + expect(tc::resolve_link_model(mingw).msvc_driver_tokens(ident).empty()); auto unbound = clang_msvc(); unbound.base.msvc.toolsDir.clear(); - EXPECT_TRUE(tc::resolve_link_model(unbound).msvc_driver_tokens(ident).empty()); -} + expect(tc::resolve_link_model(unbound).msvc_driver_tokens(ident).empty()); +}}; diff --git a/tests/unit/test_loader_contract.cpp b/tests/unit/test_loader_contract.cpp index 4dcf84256..5e386aee3 100644 --- a/tests/unit/test_loader_contract.cpp +++ b/tests/unit/test_loader_contract.cpp @@ -1,6 +1,6 @@ -#include import std; +import mcpplibs.mtest; import mcpp.build.loader_contract; import mcpp.build.graph_shape; import mcpp.pack.host_requirements; @@ -12,38 +12,40 @@ import mcpp.runtime.binding; import mcpplibs.os.platform; import nlohmann.json; +using namespace mcpplibs::mtest; + namespace { // ─── the loader-tag contract ──────────────────────────────────────────────── -TEST(LoaderContract, ExecutablesNeedRpathLibrariesNeedRunpath) { +UTest executables_need_rpath_libraries_need_runpath{"LoaderContract/ExecutablesNeedRpathLibrariesNeedRunpath", [] { using namespace mcpp::build::loader; - EXPECT_EQ(required_tag(Form::Executable), RequiredTag::Rpath); - EXPECT_EQ(required_tag(Form::SharedLibrary), RequiredTag::Runpath); - EXPECT_EQ(required_tag(Form::NotElf), RequiredTag::NotApplicable); -} + expect(eq(required_tag(Form::Executable), RequiredTag::Rpath)); + expect(eq(required_tag(Form::SharedLibrary), RequiredTag::Runpath)); + expect(eq(required_tag(Form::NotElf), RequiredTag::NotApplicable)); +}}; // The two producers must not each decide the spelling. If one of them ever // grows its own string literal, this is the test that notices. -TEST(LoaderContract, BothProducersSpellTheSameRuleFromOneSource) { +UTest both_producers_spell_the_same_rule_from_one_source{"LoaderContract/BothProducersSpellTheSameRuleFromOneSource", [] { using namespace mcpp::build::loader; auto exeLink = link_flag(required_tag(Form::Executable)); auto exePatch = patchelf_flag(required_tag(Form::Executable)); - ASSERT_TRUE(exeLink.has_value()); - ASSERT_TRUE(exePatch.has_value()); - EXPECT_EQ(*exeLink, "-Wl,--disable-new-dtags"); - EXPECT_EQ(*exePatch, "--force-rpath"); + expect(fatal(exeLink.has_value())); + expect(fatal(exePatch.has_value())); + expect(eq(*exeLink, "-Wl,--disable-new-dtags")); + expect(eq(*exePatch, "--force-rpath")); // Libraries take the default in BOTH producers -- forcing DT_RPATH onto a // library pushes its search path into every lookup below it and breaks // eglInitialize (openxlings/xlings#593). - EXPECT_FALSE(link_flag(required_tag(Form::SharedLibrary)).has_value()); - EXPECT_FALSE(patchelf_flag(required_tag(Form::SharedLibrary)).has_value()); -} + expect(!(link_flag(required_tag(Form::SharedLibrary)).has_value())); + expect(!(patchelf_flag(required_tag(Form::SharedLibrary)).has_value())); +}}; // ─── graph shape ──────────────────────────────────────────────────────────── -TEST(GraphShape, UnlabelledOrUnknownGraphIsNeverPlain) { +UTest unlabelled_or_unknown_graph_is_never_plain{"GraphShape/UnlabelledOrUnknownGraphIsNeverPlain", [] { using namespace mcpp::build; auto dir = std::filesystem::temp_directory_path() / "mcpp_graph_shape_test"; @@ -59,10 +61,10 @@ TEST(GraphShape, UnlabelledOrUnknownGraphIsNeverPlain) { return p; }; - EXPECT_TRUE(is_plain_build_graph( + expect(is_plain_build_graph( write("normal.ninja", "# banner\n# mcpp:graph=normal;schedule=none;accel=default;dist=none\nrule x\n"))); - EXPECT_FALSE(is_plain_build_graph( - write("test.ninja", "# banner\n# mcpp:graph=test;schedule=none;accel=default;dist=none\nrule x\n"))); + expect(!(is_plain_build_graph( + write("test.ninja", "# banner\n# mcpp:graph=test;schedule=none;accel=default;dist=none\nrule x\n")))); // A plain-shaped graph that `mcpp pack --format ` wrote (2026.9.11.1+): // it carries an artifact edge consuming a staged tree, which a plain build @@ -70,42 +72,42 @@ TEST(GraphShape, UnlabelledOrUnknownGraphIsNeverPlain) { // `header_line`, which is the point of this copy -- a reader of build.ninja // sees the text, and a field added to the producer without being added to // the reader would still pass a test that only compared the two. - EXPECT_FALSE(is_plain_build_graph( - write("dist.ninja", "# mcpp:graph=normal;schedule=none;accel=default;dist=appimage\n"))); + expect(!(is_plain_build_graph( + write("dist.ninja", "# mcpp:graph=normal;schedule=none;accel=default;dist=appimage\n")))); // A plain-shaped graph an `--accel` / `--no-accel` build wrote (2026.9.5.3+): // the variant a flag chose is not the variant a plain build produces. - EXPECT_FALSE(is_plain_build_graph( - write("override.ninja", "# mcpp:graph=normal;schedule=none;accel=override\n"))); + expect(!(is_plain_build_graph( + write("override.ninja", "# mcpp:graph=normal;schedule=none;accel=override\n")))); // A graph from 2026.9.5.2 and earlier: shape and schedule, no selection // field. Not known to be the manifest's variant, so a miss, not a guess. - EXPECT_FALSE(is_plain_build_graph( - write("no-selection.ninja", "# banner\n# mcpp:graph=normal\nrule x\n"))); + expect(!(is_plain_build_graph( + write("no-selection.ninja", "# banner\n# mcpp:graph=normal\nrule x\n")))); // A graph from 2026.9.10.2: shape, schedule and selection, no distribution // field. Same rule one field later. This assertion is what caught the // second copy of these lines when the field was added -- the line above it // was the CURRENT spelling in one file and became the LEGACY spelling in // both, and only a test that spells it out could say so. - EXPECT_FALSE(is_plain_build_graph( - write("no-dist.ninja", "# banner\n# mcpp:graph=normal;schedule=none;accel=default\nrule x\n"))); + expect(!(is_plain_build_graph( + write("no-dist.ninja", "# banner\n# mcpp:graph=normal;schedule=none;accel=default\nrule x\n")))); // A build.ninja from before the marker existed. It MUST read as a miss: // treating it as plain is precisely the replay #407 is about. - EXPECT_FALSE(is_plain_build_graph( - write("legacy.ninja", "# banner\nninja_required_version = 1.11\n"))); + expect(!(is_plain_build_graph( + write("legacy.ninja", "# banner\nninja_required_version = 1.11\n")))); // A shape this binary does not know. An older mcpp meeting a newer graph // must fall back, not guess. - EXPECT_FALSE(is_plain_build_graph( - write("future.ninja", "# mcpp:graph=coverage\n"))); + expect(!(is_plain_build_graph( + write("future.ninja", "# mcpp:graph=coverage\n")))); // Missing file. - EXPECT_FALSE(is_plain_build_graph(dir / "absent.ninja")); -} + expect(!is_plain_build_graph(dir / "absent.ninja")); +}}; -TEST(GraphShape, HeaderAndReaderAgree) { +UTest header_and_reader_agree{"GraphShape/HeaderAndReaderAgree", [] { using namespace mcpp::build; auto dir = std::filesystem::temp_directory_path() / "mcpp_graph_shape_rt"; std::filesystem::create_directories(dir); @@ -122,9 +124,9 @@ TEST(GraphShape, HeaderAndReaderAgree) { { std::ofstream out(p, std::ios::trunc); out << header_line(shape, sched) << "\n"; } auto read = read_shape(p); - ASSERT_TRUE(read.has_value()); - EXPECT_EQ(*read, shape); - EXPECT_EQ(read_schedule(p), sched); + expect(fatal(read.has_value())); + expect(eq(*read, shape)); + expect(eq(read_schedule(p), sched)); } } @@ -137,11 +139,11 @@ TEST(GraphShape, HeaderAndReaderAgree) { auto p = dir / "build.ninja"; { std::ofstream out(p, std::ios::trunc); out << "# mcpp:graph=normal\n"; } auto read = read_shape(p); - ASSERT_TRUE(read.has_value()); - EXPECT_EQ(*read, GraphShape::Normal); - EXPECT_TRUE(read_schedule(p).empty()); + expect(fatal(read.has_value())); + expect(eq(*read, GraphShape::Normal)); + expect(read_schedule(p).empty()); } -} +}}; // ─── host requirements: ONE derivation, two projections ───────────────────── @@ -151,7 +153,7 @@ mcpp::manifest::RuntimeConfig runtime_with(std::vector capabilities return rc; } -TEST(HostRequirements, OnlyRunPhaseCapabilitiesCount) { +UTest only_run_phase_capabilities_count{"HostRequirements/OnlyRunPhaseCapabilitiesCount", [] { mcpp::manifest::RuntimeConfig rc; rc.requirements.push_back({.kind = "capability", .value = "opengl.glx.driver", .phase = "run"}); @@ -164,59 +166,58 @@ TEST(HostRequirements, OnlyRunPhaseCapabilitiesCount) { .phase = "run"}); auto reqs = mcpp::pack::host_requirements_of(rc); - ASSERT_EQ(reqs.size(), 1u); - EXPECT_EQ(reqs[0].capability, "opengl.glx.driver"); -} + expect(fatal(eq(reqs.size(), 1u))); + expect(eq(reqs[0].capability, "opengl.glx.driver")); +}}; // The legacy vector carries the same meaning. A package that has not migrated // must not silently produce an empty list -- an empty HOST-REQUIREMENTS is a // CLAIM that nothing is needed. -TEST(HostRequirements, LegacyCapabilitiesVectorIsStillRead) { +UTest legacy_capabilities_vector_is_still_read{"HostRequirements/LegacyCapabilitiesVectorIsStillRead", [] { auto reqs = mcpp::pack::host_requirements_of( runtime_with({"vulkan.icd", "opengl.egl.driver"})); - ASSERT_EQ(reqs.size(), 2u); - EXPECT_EQ(reqs[0].capability, "opengl.egl.driver"); // sorted - EXPECT_EQ(reqs[1].capability, "vulkan.icd"); -} + expect(fatal(eq(reqs.size(), 2u))); + expect(eq(reqs[0].capability, "opengl.egl.driver")); // sorted + expect(eq(reqs[1].capability, "vulkan.icd")); +}}; -TEST(HostRequirements, DuplicatesAcrossBothFormsCollapse) { +UTest duplicates_across_both_forms_collapse{"HostRequirements/DuplicatesAcrossBothFormsCollapse", [] { mcpp::manifest::RuntimeConfig rc; rc.requirements.push_back({.kind = "capability", .value = "opengl.glx.driver", .phase = "run"}); rc.capabilities.push_back("opengl.glx.driver"); - EXPECT_EQ(mcpp::pack::host_requirements_of(rc).size(), 1u); -} + expect(eq(mcpp::pack::host_requirements_of(rc).size(), 1u)); +}}; // The mechanism is DECLARED, never inferred. mcpp inferring it from the // capability name would be provider-specific knowledge in mcpp's source -- // gated by test_runtime_contract, and wrong on its merits: the mechanism is // the provider's property and changes without mcpp. -TEST(HostRequirements, DiscoveryIsCarriedNotGuessed) { +UTest discovery_is_carried_not_guessed{"HostRequirements/DiscoveryIsCarriedNotGuessed", [] { mcpp::manifest::RuntimeConfig rc; rc.requirements.push_back({.kind = "capability", .value = "opengl.egl.driver", .phase = "run", .discovery = "json-dir"}); auto reqs = mcpp::pack::host_requirements_of(rc); - ASSERT_EQ(reqs.size(), 1u); - EXPECT_EQ(reqs[0].discovery, "json-dir"); + expect(fatal(eq(reqs.size(), 1u))); + expect(eq(reqs[0].discovery, "json-dir")); // Undeclared stays empty and renders as `unknown` -- saying "we do not // know how this is found" is information; guessing would be a claim. auto legacy = mcpp::pack::host_requirements_of( runtime_with({"opengl.glx.driver"})); - ASSERT_EQ(legacy.size(), 1u); - EXPECT_TRUE(legacy[0].discovery.empty()); - EXPECT_NE(mcpp::pack::render(legacy).find("discovery=unknown"), - std::string::npos); -} + expect(fatal(eq(legacy.size(), 1u))); + expect(legacy[0].discovery.empty()); + expect(ne(mcpp::pack::render(legacy).find("discovery=unknown"), std::string::npos)); +}}; -TEST(HostRequirements, RenderIsGreppable) { +UTest render_is_greppable{"HostRequirements/RenderIsGreppable", [] { mcpp::manifest::RuntimeConfig rc; rc.requirements.push_back({.kind = "capability", .value = "opengl.glx.driver", .phase = "run", .discovery = "rpath-of-dispatch"}); auto text = mcpp::pack::render(mcpp::pack::host_requirements_of(rc)); - EXPECT_NE(text.find("capability=opengl.glx.driver"), std::string::npos); - EXPECT_NE(text.find("discovery=rpath-of-dispatch"), std::string::npos); -} + expect(ne(text.find("capability=opengl.glx.driver"), std::string::npos)); + expect(ne(text.find("discovery=rpath-of-dispatch"), std::string::npos)); +}}; // ─── declared runtime artifact -> identity verdict ────────────────────────── // @@ -224,7 +225,7 @@ TEST(HostRequirements, RenderIsGreppable) { // symlink on disk still resolved into 0.1.1. Detectable as a pure path fact — // no knowledge of what the artifact does. -TEST(ArtifactIdentity, FourValuedAndSymlinkAware) { +UTest four_valued_and_symlink_aware{"ArtifactIdentity/FourValuedAndSymlinkAware", [] { using namespace mcpp::build::runtime_validation; namespace fs = std::filesystem; @@ -245,31 +246,31 @@ TEST(ArtifactIdentity, FourValuedAndSymlinkAware) { a.provenance = "xim:vendor@0.1.2"; // Nothing declared / nothing there. - EXPECT_EQ(artifact_identity_verdict(a), ArtifactVerdict::Missing); + expect(eq(artifact_identity_verdict(a), ArtifactVerdict::Missing)); a.path = root / "absent.so"; - EXPECT_EQ(artifact_identity_verdict(a), ArtifactVerdict::Missing); + expect(eq(artifact_identity_verdict(a), ArtifactVerdict::Missing)); // Declared version matches the resolved payload. a.path = root / "store" / "0.1.2" / "lib" / "libvendor.so"; - EXPECT_EQ(artifact_identity_verdict(a), ArtifactVerdict::Ok); + expect(eq(artifact_identity_verdict(a), ArtifactVerdict::Ok)); // THE case. A symlink that still points into the previous payload: reading // the declared path alone would confirm the promise against itself. fs::create_directory_symlink(root / "store" / "0.1.1", root / "current", ec); if (!ec) { a.path = root / "current" / "lib" / "libvendor.so"; - EXPECT_EQ(artifact_identity_verdict(a), ArtifactVerdict::Mismatch); + expect(eq(artifact_identity_verdict(a), ArtifactVerdict::Mismatch)); } // No version to check against is UNVERIFIED, never Ok — "not checked" and // "checked and fine" must not look the same. a.path = root / "store" / "0.1.2" / "lib" / "libvendor.so"; a.provenance = "xim:vendor"; - EXPECT_EQ(artifact_identity_verdict(a), ArtifactVerdict::Unverified); -} + expect(eq(artifact_identity_verdict(a), ArtifactVerdict::Unverified)); +}}; // A component match, not a substring: 0.1.1 must not satisfy 0.1.11. -TEST(ArtifactIdentity, VersionMatchIsAPathComponent) { +UTest version_match_is_a_path_component{"ArtifactIdentity/VersionMatchIsAPathComponent", [] { using namespace mcpp::build::runtime_validation; namespace fs = std::filesystem; auto root = fs::temp_directory_path() / "mcpp_artifact_identity_sub"; @@ -283,8 +284,8 @@ TEST(ArtifactIdentity, VersionMatchIsAPathComponent) { mcpp::manifest::RuntimeArtifact a; a.path = root / "0.1.11" / "lib.so"; a.provenance = "xim:vendor@0.1.1"; - EXPECT_EQ(artifact_identity_verdict(a), ArtifactVerdict::Mismatch); -} + expect(eq(artifact_identity_verdict(a), ArtifactVerdict::Mismatch)); +}}; // ─── a pass with nothing to say does not erase what a pass measured ───────── // @@ -302,10 +303,10 @@ TEST(ArtifactIdentity, VersionMatchIsAPathComponent) { // // Stated here because no end-to-end shape reaches it: it needs two drives over // one output directory where the SECOND one is the one that does not apply. -TEST(DlopenSurfaceRecord, ANonAnswerRepublishesTheAnswerAlreadyOnFile) { +UTest a_non_answer_republishes_the_answer_already_on_file{"DlopenSurfaceRecord/ANonAnswerRepublishesTheAnswerAlreadyOnFile", [] { namespace fs = std::filesystem; if constexpr (!mcpplibs::os::is_linux) { - SUCCEED() << "the record is ELF-shaped and this host links no ELF"; + /* passes */; return; } @@ -337,12 +338,12 @@ TEST(DlopenSurfaceRecord, ANonAnswerRepublishesTheAnswerAlreadyOnFile) { regenerate_resolution(); mcpp::build::runtime_validation::check_dlopen_surface(plan); auto stored = read_json(sidecar); - ASSERT_TRUE(stored.is_object()) << "the sidecar was not written"; - ASSERT_TRUE(stored.contains("dlopen_surface")); - EXPECT_FALSE(stored["dlopen_surface"].value("reason", "").empty()) + expect(fatal(stored.is_object())) << "the sidecar was not written"; + expect(fatal(stored.contains("dlopen_surface"))); + expect(!(stored["dlopen_surface"].value("reason", "").empty())) << "a published non-answer carries the reason it did not apply"; - EXPECT_FALSE(read_json(root / "resolution.json")["runtime"]["dlopen_surface"] - .value("reason", "").empty()) + expect(!(read_json(root / "resolution.json")["runtime"]["dlopen_surface"] + .value("reason", "").empty())) << "the documented place to look carries it too"; // A pass that DID apply now records a reading, under the key already on @@ -359,14 +360,14 @@ TEST(DlopenSurfaceRecord, ANonAnswerRepublishesTheAnswerAlreadyOnFile) { mcpp::build::runtime_validation::check_dlopen_surface(plan); auto after = read_json(sidecar)["dlopen_surface"]; - EXPECT_EQ(after.value("members", 0), 7) + expect(eq(after.value("members", 0), 7)) << "a non-answer replaced a reading taken under the same key"; - EXPECT_FALSE(after.contains("reason")); + expect(!after.contains("reason")); auto republished = read_json(root / "resolution.json")["runtime"]["dlopen_surface"]; - EXPECT_EQ(republished.value("members", 0), 7) + expect(eq(republished.value("members", 0), 7)) << "the reading survived in the sidecar but not where it is documented"; - EXPECT_EQ(republished.value("walked", 0), 7); -} + expect(eq(republished.value("walked", 0), 7)); +}}; } // namespace diff --git a/tests/unit/test_luban_root.cpp b/tests/unit/test_luban_root.cpp index 0a48b8250..8dbc26aa0 100644 --- a/tests/unit/test_luban_root.cpp +++ b/tests/unit/test_luban_root.cpp @@ -4,13 +4,15 @@ // root record names, which `[xlings] upstream = ["/xlings"]` means for a // system made on a host (whose home is not at /xlings). -#include import std; +import mcpplibs.mtest; import mcpp.provider; import mcpp.provider.select; import mcpp.fallback.probe_sysroot; +using namespace mcpplibs::mtest; + namespace fs = std::filesystem; namespace { @@ -18,7 +20,7 @@ namespace { struct Tree { fs::path dir; Tree() { - dir = fs::temp_directory_path() / std::format("mcpp-luban-root-{}-{}", ::testing::UnitTest::GetInstance()->random_seed(), + dir = fs::temp_directory_path() / std::format("mcpp-luban-root-{}-{}", mcpplibs::mtest::seed(), std::chrono::steady_clock::now().time_since_epoch().count()); fs::create_directories(dir); } @@ -31,36 +33,36 @@ struct Tree { } // namespace -TEST(LubanRoot, ALubanSystemsRootIsDeclared) { +UTest a_luban_systems_root_is_declared{"LubanRoot/ALubanSystemsRootIsDeclared", [] { Tree t; - EXPECT_FALSE(mcpp::provider::layout().declared_system_root(t.dir)); + expect(!(mcpp::provider::layout().declared_system_root(t.dir))); t.write("etc/os-release", "NAME=\"Luban\"\nID=luban\nVERSION_ID=2026.10.22.1\n"); - EXPECT_FALSE(mcpp::provider::layout().declared_system_root(t.dir)) << "its os-release alone is not enough"; + expect(!(mcpp::provider::layout().declared_system_root(t.dir))) << "its os-release alone is not enough"; t.write("usr/lib/xlings-abi/README", ""); - EXPECT_TRUE(mcpp::provider::layout().declared_system_root(t.dir)); + expect(bool(mcpp::provider::layout().declared_system_root(t.dir))); t.write("etc/os-release", "ID=\"debian\"\n"); - EXPECT_FALSE(mcpp::provider::layout().declared_system_root(t.dir)); -} + expect(!(mcpp::provider::layout().declared_system_root(t.dir))); +}}; -TEST(LubanRoot, ItsSysrootSlashIsNotForeign) { +UTest its_sysroot_slash_is_not_foreign{"LubanRoot/ItsSysrootSlashIsNotForeign", [] { Tree t; const fs::path registry = "/home/u/.mcpp/registry", project = "/home/u/work/demo"; - EXPECT_TRUE(mcpp::fallback::sysroot_is_foreign("/", registry, project, t.dir)) << "anywhere else, / is nothing declared"; + expect(mcpp::fallback::sysroot_is_foreign("/", registry, project, t.dir)) << "anywhere else, / is nothing declared"; t.write("etc/os-release", "ID=luban\n"); t.write("usr/lib/xlings-abi/README", ""); - EXPECT_FALSE(mcpp::fallback::sysroot_is_foreign("/", registry, project, t.dir)); - EXPECT_TRUE(mcpp::fallback::sysroot_is_foreign("/opt/other/sysroot", registry, project, t.dir)) << "only its own root"; -} + expect(!mcpp::fallback::sysroot_is_foreign("/", registry, project, t.dir)); + expect(mcpp::fallback::sysroot_is_foreign("/opt/other/sysroot", registry, project, t.dir)) << "only its own root"; +}}; -TEST(LubanRoot, ItsStoreIsTheHomeItsRootRecordNames) { +UTest its_store_is_the_home_its_root_record_names{"LubanRoot/ItsStoreIsTheHomeItsRootRecordNames", [] { Tree t; t.write("etc/os-release", "ID=luban\n"); t.write("usr/lib/xlings-abi/README", ""); - EXPECT_TRUE(mcpp::provider::layout().system_home(t.dir).empty()) << "no record"; + expect(mcpp::provider::layout().system_home(t.dir).empty()) << "no record"; const auto home = t.dir / "host-home"; fs::create_directories(home / "subos" / "box" / "root" / "usr"); t.write("etc/xlings/usr", (home / "subos" / "box" / "root" / "usr").string() + "\n"); - EXPECT_EQ(mcpp::provider::layout().system_home(t.dir), home.lexically_normal()); + expect(eq(mcpp::provider::layout().system_home(t.dir), home.lexically_normal())); t.write("etc/xlings/usr", "/somewhere/else/usr\n"); - EXPECT_TRUE(mcpp::provider::layout().system_home(t.dir).empty()) << "a record of another shape"; -} + expect(mcpp::provider::layout().system_home(t.dir).empty()) << "a record of another shape"; +}}; diff --git a/tests/unit/test_main_detection.cpp b/tests/unit/test_main_detection.cpp index 9603c2446..5110d696c 100644 --- a/tests/unit/test_main_detection.cpp +++ b/tests/unit/test_main_detection.cpp @@ -1,8 +1,10 @@ -#include import std; +import mcpplibs.mtest; import mcpp.build.plan; +using namespace mcpplibs::mtest; + using namespace mcpp::build; namespace { @@ -27,53 +29,53 @@ bool defines_main(std::string_view content, std::string_view tag) { } // namespace -TEST(MainDetection, RealMainIsDetected) { - EXPECT_TRUE(defines_main("import std;\nint main() { return 0; }\n", "real")); -} +UTest real_main_is_detected{"MainDetection/RealMainIsDetected", [] { + expect(bool(defines_main("import std;\nint main() { return 0; }\n", "real"))); +}}; -TEST(MainDetection, RealMainWithArgsIsDetected) { - EXPECT_TRUE(defines_main( - "int main(int argc, char** argv) { (void)argc; (void)argv; return 0; }\n", "args")); -} +UTest real_main_with_args_is_detected{"MainDetection/RealMainWithArgsIsDetected", [] { + expect(bool(defines_main( + "int main(int argc, char** argv) { (void)argc; (void)argv; return 0; }\n", "args"))); +}}; -TEST(MainDetection, AutoMainIsDetected) { - EXPECT_TRUE(defines_main("auto main() -> int { return 0; }\n", "automain")); -} +UTest auto_main_is_detected{"MainDetection/AutoMainIsDetected", [] { + expect(bool(defines_main("auto main() -> int { return 0; }\n", "automain"))); +}}; // The regression: test fixtures embed `"int main() {...}"` as a STRING — that // must NOT count as the test binary defining main (it doesn't). A false positive // chose archive linking → gtest_main.o not pulled by MSVC lld-link → LNK1561. -TEST(MainDetection, MainInsideStringLiteralIsIgnored) { - EXPECT_FALSE(defines_main( +UTest main_inside_string_literal_is_ignored{"MainDetection/MainInsideStringLiteralIsIgnored", [] { + expect(!(defines_main( "#include \n" "TEST(M, x) {\n" " auto src = \"int main() { return 0; }\\n\";\n" " EXPECT_FALSE(src.empty());\n" - "}\n", "strlit")); -} + "}\n", "strlit"))); +}}; -TEST(MainDetection, MainInsideRawStringIsIgnored) { - EXPECT_FALSE(defines_main( +UTest main_inside_raw_string_is_ignored{"MainDetection/MainInsideRawStringIsIgnored", [] { + expect(!(defines_main( "#include \n" "TEST(M, x) {\n" " auto src = R\"(\nint main() { return 0; }\n)\";\n" " EXPECT_FALSE(src.empty());\n" - "}\n", "rawstr")); -} + "}\n", "rawstr"))); +}}; -TEST(MainDetection, MainInsideCommentIsIgnored) { - EXPECT_FALSE(defines_main( +UTest main_inside_comment_is_ignored{"MainDetection/MainInsideCommentIsIgnored", [] { + expect(!(defines_main( "// int main() { return 0; }\n" "#include \n" - "TEST(M, x) { EXPECT_TRUE(true); }\n", "comment")); -} + "TEST(M, x) { EXPECT_TRUE(true); }\n", "comment"))); +}}; -TEST(MainDetection, NoMainGtestStyleIsFalse) { - EXPECT_FALSE(defines_main( +UTest no_main_gtest_style_is_false{"MainDetection/NoMainGtestStyleIsFalse", [] { + expect(!(defines_main( "#include \n" - "TEST(M, x) { EXPECT_EQ(1, 1); }\n", "nomain")); -} + "TEST(M, x) { EXPECT_EQ(1, 1); }\n", "nomain"))); +}}; -TEST(MainDetection, SimilarIdentifierIsNotMain) { - EXPECT_FALSE(defines_main("int mainHelper() { return 0; }\n", "helper")); -} +UTest similar_identifier_is_not_main{"MainDetection/SimilarIdentifierIsNotMain", [] { + expect(!(defines_main("int mainHelper() { return 0; }\n", "helper"))); +}}; diff --git a/tests/unit/test_mangle.cpp b/tests/unit/test_mangle.cpp index 5134529f5..f83ae1473 100644 --- a/tests/unit/test_mangle.cpp +++ b/tests/unit/test_mangle.cpp @@ -1,17 +1,19 @@ -#include import std; +import mcpplibs.mtest; import mcpp.pm.mangle; +using namespace mcpplibs::mtest; + using namespace mcpp::pm; -TEST(Mangle, NameFormat) { - EXPECT_EQ(mangle_name("json", "1.2.3"), "json__v1_2_3__mcpp"); - EXPECT_EQ(mangle_name("mcpplibs.cmdline","2.0"),"mcpplibs.cmdline__v2_0__mcpp"); - EXPECT_EQ(mangle_name("a", "0"), "a__v0__mcpp"); -} +UTest name_format{"Mangle/NameFormat", [] { + expect(eq(mangle_name("json", "1.2.3"), "json__v1_2_3__mcpp")); + expect(eq(mangle_name("mcpplibs.cmdline","2.0"), "mcpplibs.cmdline__v2_0__mcpp")); + expect(eq(mangle_name("a", "0"), "a__v0__mcpp")); +}}; -TEST(Mangle, DiscoversAuthoredModuleRootsWithoutPackageNameAssumptions) { +UTest discovers_authored_module_roots_without_package_name_assumptions{"Mangle/DiscoversAuthoredModuleRootsWithoutPackageNameAssumptions", [] { auto roots = declared_module_roots( "module;\n" "export module mcpplibs.cmdline;\n" @@ -19,75 +21,64 @@ TEST(Mangle, DiscoversAuthoredModuleRootsWithoutPackageNameAssumptions) { "module mcpplibs.cmdline:impl;\n" "import unrelated;\n" "export module vendor.extra;\n"); - EXPECT_EQ(roots, (std::vector{ - "mcpplibs.cmdline", "vendor.extra"})); -} + expect(eq(roots, (std::vector{ + "mcpplibs.cmdline", "vendor.extra"}))); +}}; -TEST(Mangle, RewriteEmpty) { +UTest rewrite_empty{"Mangle/RewriteEmpty", [] { std::map table; - EXPECT_EQ(rewrite_module_decls("", table), ""); - EXPECT_EQ(rewrite_module_decls("export module foo;\n", table), - "export module foo;\n") + expect(eq(rewrite_module_decls("", table), "")); + expect(eq(rewrite_module_decls("export module foo;\n", table), "export module foo;\n")) << "empty rename table → no change"; -} +}}; -TEST(Mangle, RewriteModuleDecl) { +UTest rewrite_module_decl{"Mangle/RewriteModuleDecl", [] { std::map table { {"json", "json__v2_0_0__mcpp"}, }; - EXPECT_EQ(rewrite_module_decls("export module json;\n", table), - "export module json__v2_0_0__mcpp;\n"); - EXPECT_EQ(rewrite_module_decls("module json;\n", table), - "module json__v2_0_0__mcpp;\n"); -} + expect(eq(rewrite_module_decls("export module json;\n", table), "export module json__v2_0_0__mcpp;\n")); + expect(eq(rewrite_module_decls("module json;\n", table), "module json__v2_0_0__mcpp;\n")); +}}; -TEST(Mangle, RewritePartitionDecl) { +UTest rewrite_partition_decl{"Mangle/RewritePartitionDecl", [] { std::map table { {"json", "json__v2_0_0__mcpp"}, }; - EXPECT_EQ(rewrite_module_decls("export module json:utils;\n", table), - "export module json__v2_0_0__mcpp:utils;\n"); - EXPECT_EQ(rewrite_module_decls("module json:impl;\n", table), - "module json__v2_0_0__mcpp:impl;\n"); -} + expect(eq(rewrite_module_decls("export module json:utils;\n", table), "export module json__v2_0_0__mcpp:utils;\n")); + expect(eq(rewrite_module_decls("module json:impl;\n", table), "module json__v2_0_0__mcpp:impl;\n")); +}}; -TEST(Mangle, RewriteImports) { +UTest rewrite_imports{"Mangle/RewriteImports", [] { std::map table { {"json", "json__v2_0_0__mcpp"}, }; - EXPECT_EQ(rewrite_module_decls("import json;\n", table), - "import json__v2_0_0__mcpp;\n"); - EXPECT_EQ(rewrite_module_decls("export import json;\n", table), - "export import json__v2_0_0__mcpp;\n"); - EXPECT_EQ(rewrite_module_decls("import json:utils;\n", table), - "import json__v2_0_0__mcpp:utils;\n"); -} + expect(eq(rewrite_module_decls("import json;\n", table), "import json__v2_0_0__mcpp;\n")); + expect(eq(rewrite_module_decls("export import json;\n", table), "export import json__v2_0_0__mcpp;\n")); + expect(eq(rewrite_module_decls("import json:utils;\n", table), "import json__v2_0_0__mcpp:utils;\n")); +}}; -TEST(Mangle, KeepBarePartitionImport) { +UTest keep_bare_partition_import{"Mangle/KeepBarePartitionImport", [] { std::map table { {"json", "json__v2_0_0__mcpp"}, }; // `import :P;` refers to the enclosing module's partition — not a // named module; must NOT be rewritten. - EXPECT_EQ(rewrite_module_decls("import :utils;\n", table), - "import :utils;\n"); - EXPECT_EQ(rewrite_module_decls("module ;\n", table), - "module ;\n") + expect(eq(rewrite_module_decls("import :utils;\n", table), "import :utils;\n")); + expect(eq(rewrite_module_decls("module ;\n", table), "module ;\n")) << "global module fragment opener has no name"; -} +}}; -TEST(Mangle, KeepNonMatching) { +UTest keep_non_matching{"Mangle/KeepNonMatching", [] { std::map table { {"json", "json__v2_0_0__mcpp"}, }; // Other modules in the same file shouldn't be touched. - EXPECT_EQ(rewrite_module_decls("import std;\n", table), "import std;\n"); - EXPECT_EQ(rewrite_module_decls("import other;\n", table), "import other;\n"); - EXPECT_EQ(rewrite_module_decls("export module yaml;\n", table), - "export module yaml;\n"); -} + expect(eq(rewrite_module_decls("import std;\n", table), "import std;\n")); + expect(eq(rewrite_module_decls("import other;\n", table), "import other;\n")); + expect(eq(rewrite_module_decls("export module yaml;\n", table), "export module yaml;\n")); +}}; -TEST(Mangle, MultipleLines) { +UTest multiple_lines{"Mangle/MultipleLines", [] { std::map table { {"json", "json__v2_0_0__mcpp"}, }; @@ -107,33 +98,28 @@ TEST(Mangle, MultipleLines) { "import std;\n" "import json__v2_0_0__mcpp:utils;\n" "export int answer() { return 42; }\n"; - EXPECT_EQ(rewrite_module_decls(in, table), expected); -} + expect(eq(rewrite_module_decls(in, table), expected)); +}}; -TEST(Mangle, LeadingWhitespace) { +UTest leading_whitespace{"Mangle/LeadingWhitespace", [] { std::map table { {"json", "json__v2_0_0__mcpp"}, }; - EXPECT_EQ(rewrite_module_decls(" export module json;\n", table), - " export module json__v2_0_0__mcpp;\n"); - EXPECT_EQ(rewrite_module_decls("\timport json;\n", table), - "\timport json__v2_0_0__mcpp;\n"); -} + expect(eq(rewrite_module_decls(" export module json;\n", table), " export module json__v2_0_0__mcpp;\n")); + expect(eq(rewrite_module_decls("\timport json;\n", table), "\timport json__v2_0_0__mcpp;\n")); +}}; -TEST(Mangle, NoTrailingNewline) { +UTest no_trailing_newline{"Mangle/NoTrailingNewline", [] { std::map table { {"json", "json__v2_0_0__mcpp"}, }; - EXPECT_EQ(rewrite_module_decls("export module json;", table), - "export module json__v2_0_0__mcpp;"); -} + expect(eq(rewrite_module_decls("export module json;", table), "export module json__v2_0_0__mcpp;")); +}}; -TEST(Mangle, DottedNames) { +UTest dotted_names{"Mangle/DottedNames", [] { std::map table { {"mcpplibs.cmdline", "mcpplibs.cmdline__v2_0_0__mcpp"}, }; - EXPECT_EQ(rewrite_module_decls("export module mcpplibs.cmdline;\n", table), - "export module mcpplibs.cmdline__v2_0_0__mcpp;\n"); - EXPECT_EQ(rewrite_module_decls("import mcpplibs.cmdline;\n", table), - "import mcpplibs.cmdline__v2_0_0__mcpp;\n"); -} + expect(eq(rewrite_module_decls("export module mcpplibs.cmdline;\n", table), "export module mcpplibs.cmdline__v2_0_0__mcpp;\n")); + expect(eq(rewrite_module_decls("import mcpplibs.cmdline;\n", table), "import mcpplibs.cmdline__v2_0_0__mcpp;\n")); +}}; diff --git a/tests/unit/test_manifest.cpp b/tests/unit/test_manifest.cpp index 07a2b4394..62a50599e 100644 --- a/tests/unit/test_manifest.cpp +++ b/tests/unit/test_manifest.cpp @@ -1,34 +1,36 @@ -#include import std; +import mcpplibs.mtest; import mcpp.manifest; import mcpp.targetside; import mcpp.libs.toml; import mcpp.pm.dep_spec; import mcpplibs.os.platform; import mcpp.toolchain.platform; + +using namespace mcpplibs::mtest; import mcpp.build.prepare; // merge_conditional_config: a row's statement replaces -TEST(Manifest, PeAutoExportDefaultsOnAndAcceptsAnExplicitOptOut) { +UTest pe_auto_export_defaults_on_and_accepts_an_explicit_opt_out{"Manifest/PeAutoExportDefaultsOnAndAcceptsAnExplicitOptOut", [] { for (auto const& declaration : {"", "windows_auto_export = true", "windows_auto_export = false"}) { auto m = mcpp::manifest::parse_string(std::format( "[package]\nname = \"dll\"\nversion = \"0.1.0\"\n" "[targets.dll]\nkind = \"shared\"\n{}\n", declaration)); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->targets.size(), 1u); - EXPECT_EQ(m->targets.front().windows_auto_export(), std::string_view(declaration) != "windows_auto_export = false"); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->targets.size(), 1u))); + expect(eq(m->targets.front().windows_auto_export(), std::string_view(declaration) != "windows_auto_export = false")); // An omitted key stays distinguishable from a stated `true`. - EXPECT_EQ(m->targets.front().windowsAutoExportDeclared, *declaration != '\0'); - EXPECT_TRUE(m->schemaWarnings.empty()); + expect(eq(m->targets.front().windowsAutoExportDeclared, *declaration != '\0')); + expect(m->schemaWarnings.empty()); } auto bad = mcpp::manifest::parse_string( "[package]\nname = \"dll\"\nversion = \"0.1.0\"\n" "[targets.dll]\nkind = \"shared\"\nwindows_auto_export = \"false\"\n"); - ASSERT_FALSE(bad.has_value()); - EXPECT_NE(bad.error().format().find("windows_auto_export must be a boolean"), std::string::npos); -} + expect(fatal(!bad.has_value())); + expect(ne(bad.error().format().find("windows_auto_export must be a boolean"), std::string::npos)); +}}; -TEST(SynthesizeFromXpkgLua, PeAutoExportHasTheSameDefaultAndBooleanContract) { +UTest pe_auto_export_has_the_same_default_and_boolean_contract{"SynthesizeFromXpkgLua/PeAutoExportHasTheSameDefaultAndBooleanContract", [] { for (auto const& declaration : {"", "windows_auto_export = true,", "windows_auto_export = false,"}) { auto m = mcpp::manifest::synthesize_from_xpkg_lua(std::format(R"( package = {{ @@ -37,10 +39,10 @@ package = {{ mcpp = {{ sources = {{ "*/api.cpp" }}, targets = {{ ["dll"] = {{ kind = "shared", {} }} }} }}, }} )", declaration), "dll", "0.1.0", mcpp::toolchain::platform::HostPlatform::current()); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->targets.size(), 1u); - EXPECT_EQ(m->targets.front().windows_auto_export(), std::string_view(declaration) != "windows_auto_export = false,"); - EXPECT_EQ(m->targets.front().windowsAutoExportDeclared, *declaration != '\0'); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->targets.size(), 1u))); + expect(eq(m->targets.front().windows_auto_export(), std::string_view(declaration) != "windows_auto_export = false,")); + expect(eq(m->targets.front().windowsAutoExportDeclared, *declaration != '\0')); } auto bad = mcpp::manifest::synthesize_from_xpkg_lua(R"( package = { @@ -49,64 +51,64 @@ package = { mcpp = { sources = { "*/api.cpp" }, targets = { ["dll"] = { kind = "shared", windows_auto_export = "false" } } }, } )", "dll", "0.1.0", mcpp::toolchain::platform::HostPlatform::current()); - ASSERT_FALSE(bad.has_value()); - EXPECT_NE(bad.error().format().find("windows_auto_export must be a boolean"), std::string::npos); -} + expect(fatal(!bad.has_value())); + expect(ne(bad.error().format().find("windows_auto_export must be a boolean"), std::string::npos)); +}}; -TEST(Manifest, CppFlyStandard) { +UTest cpp_fly_standard{"Manifest/CppFlyStandard", [] { // standard = "c++fly": latest level + all experimental gates (design // 2026-07-14-std-features-experimental-gate-design.md §5.1). auto cfg = mcpp::manifest::normalize_cpp_standard("c++fly"); - ASSERT_TRUE(cfg.has_value()); - EXPECT_EQ(cfg->canonical, "c++fly"); - EXPECT_EQ(cfg->level, 1000); // above c++latest's 999 - EXPECT_TRUE(cfg->experimental); - EXPECT_FALSE(mcpp::manifest::normalize_cpp_standard("c++latest")->experimental); - EXPECT_FALSE(mcpp::manifest::normalize_cpp_standard("c++26")->experimental); + expect(fatal(cfg.has_value())); + expect(eq(cfg->canonical, "c++fly")); + expect(eq(cfg->level, 1000)); // above c++latest's 999 + expect(bool(cfg->experimental)); + expect(!(mcpp::manifest::normalize_cpp_standard("c++latest")->experimental)); + expect(!(mcpp::manifest::normalize_cpp_standard("c++26")->experimental)); auto bad = mcpp::manifest::normalize_cpp_standard("c++flyy"); - ASSERT_FALSE(bad.has_value()); - EXPECT_NE(bad.error().find("c++fly"), std::string::npos); // listed in the allow-list message -} + expect(fatal(!bad.has_value())); + expect(ne(bad.error().find("c++fly"), std::string::npos)); // listed in the allow-list message +}}; -TEST(Manifest, Cpp20Standard) { +UTest cpp20_standard{"Manifest/Cpp20Standard", [] { // C++20 is the floor of the allow-list: named modules are a C++20 feature, // so nothing below it exists for this build model // (.agents/docs/2026-07-31-cpp20-standard-support-design.md). auto cfg = mcpp::manifest::normalize_cpp_standard("c++20"); - ASSERT_TRUE(cfg.has_value()); - EXPECT_EQ(cfg->canonical, "c++20"); - EXPECT_EQ(cfg->flag, "-std=c++20"); - EXPECT_EQ(cfg->level, 20); - EXPECT_FALSE(cfg->gnuDialect); - EXPECT_FALSE(cfg->experimental); + expect(fatal(cfg.has_value())); + expect(eq(cfg->canonical, "c++20")); + expect(eq(cfg->flag, "-std=c++20")); + expect(eq(cfg->level, 20)); + expect(!cfg->gnuDialect); + expect(!cfg->experimental); // Aliases, symmetric with the existing c++2b / c++2c handling. auto alias = mcpp::manifest::normalize_cpp_standard("C++2a"); // also case-folded - ASSERT_TRUE(alias.has_value()); - EXPECT_EQ(alias->canonical, "c++20"); - EXPECT_EQ(alias->level, 20); + expect(fatal(alias.has_value())); + expect(eq(alias->canonical, "c++20")); + expect(eq(alias->level, 20)); auto gnu = mcpp::manifest::normalize_cpp_standard("gnu++20"); - ASSERT_TRUE(gnu.has_value()); - EXPECT_EQ(gnu->canonical, "gnu++20"); - EXPECT_EQ(gnu->flag, "-std=gnu++20"); - EXPECT_EQ(gnu->level, 20); - EXPECT_TRUE(gnu->gnuDialect); - EXPECT_EQ(mcpp::manifest::normalize_cpp_standard("gnu++2a")->canonical, "gnu++20"); + expect(fatal(gnu.has_value())); + expect(eq(gnu->canonical, "gnu++20")); + expect(eq(gnu->flag, "-std=gnu++20")); + expect(eq(gnu->level, 20)); + expect(bool(gnu->gnuDialect)); + expect(eq(mcpp::manifest::normalize_cpp_standard("gnu++2a")->canonical, "gnu++20")); // Below the floor stays rejected, and the message advertises the floor. auto bad = mcpp::manifest::normalize_cpp_standard("c++17"); - ASSERT_FALSE(bad.has_value()); - EXPECT_NE(bad.error().find("c++20"), std::string::npos) << bad.error(); -} + expect(fatal(!bad.has_value())); + if (auto&& failed = expect(ne(bad.error().find("c++20"), std::string::npos)); !failed) failed << bad.error(); +}}; -TEST(Manifest, CppStandardLevelName) { - EXPECT_EQ(mcpp::manifest::cpp_standard_level_name(20), "c++20"); - EXPECT_EQ(mcpp::manifest::cpp_standard_level_name(23), "c++23"); - EXPECT_EQ(mcpp::manifest::cpp_standard_level_name(26), "c++26"); -} +UTest manifest_cpp_standard_level_name{"Manifest/CppStandardLevelName", [] { + expect(eq(mcpp::manifest::cpp_standard_level_name(20), "c++20")); + expect(eq(mcpp::manifest::cpp_standard_level_name(23), "c++23")); + expect(eq(mcpp::manifest::cpp_standard_level_name(26), "c++26")); +}}; -TEST(Manifest, MinimalValid) { +UTest minimal_valid{"Manifest/MinimalValid", [] { constexpr auto src = R"( [package] name = "hello" @@ -120,24 +122,24 @@ kind = "bin" main = "src/main.cpp" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_EQ(m->package.name, "hello"); - EXPECT_EQ(m->package.version, "0.1.0"); - EXPECT_EQ(m->language.standard, "c++23"); - EXPECT_TRUE(m->language.modules); - ASSERT_EQ(m->targets.size(), 1u); - EXPECT_EQ(m->targets[0].name, "hello"); - EXPECT_EQ(m->targets[0].kind, mcpp::manifest::Target::Binary); -} - -TEST(Manifest, XlingsSubosPreservesDeclarationPresence) { + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(eq(m->package.name, "hello")); + expect(eq(m->package.version, "0.1.0")); + expect(eq(m->language.standard, "c++23")); + expect(bool(m->language.modules)); + expect(fatal(eq(m->targets.size(), 1u))); + expect(eq(m->targets[0].name, "hello")); + expect(eq(m->targets[0].kind, mcpp::manifest::Target::Binary)); +}}; + +UTest xlings_subos_preserves_declaration_presence{"Manifest/XlingsSubosPreservesDeclarationPresence", [] { auto absent = mcpp::manifest::parse_string(R"( [package] name = "absent" version = "0.1.0" )"); - ASSERT_TRUE(absent.has_value()) << absent.error().format(); - EXPECT_FALSE(absent->xlings.subosDeclared); + if (auto&& failed = expect(fatal(absent.has_value())); !failed) failed << absent.error().format(); + expect(!(absent->xlings.subosDeclared)); auto explicitDefault = mcpp::manifest::parse_string(R"( [package] @@ -146,9 +148,9 @@ version = "0.1.0" [xlings] subos = "default" )"); - ASSERT_TRUE(explicitDefault.has_value()) << explicitDefault.error().format(); - EXPECT_TRUE(explicitDefault->xlings.subosDeclared); - EXPECT_EQ(explicitDefault->xlings.subos, "default"); + if (auto&& failed = expect(fatal(explicitDefault.has_value())); !failed) failed << explicitDefault.error().format(); + expect(bool(explicitDefault->xlings.subosDeclared)); + expect(eq(explicitDefault->xlings.subos, "default")); auto empty = mcpp::manifest::parse_string(R"( [package] @@ -157,12 +159,12 @@ version = "0.1.0" [xlings] subos = "" )"); - ASSERT_TRUE(empty.has_value()) << empty.error().format(); - EXPECT_TRUE(empty->xlings.subosDeclared); - EXPECT_TRUE(empty->xlings.subos.empty()); -} + if (auto&& failed = expect(fatal(empty.has_value())); !failed) failed << empty.error().format(); + expect(bool(empty->xlings.subosDeclared)); + expect(empty->xlings.subos.empty()); +}}; -TEST(Manifest, SharedTargetSoname) { +UTest shared_target_soname{"Manifest/SharedTargetSoname", [] { constexpr auto src = R"( [package] name = "dep" @@ -174,13 +176,13 @@ kind = "shared" soname = "libdep.so.1" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->targets.size(), 1u); - EXPECT_EQ(m->targets[0].kind, mcpp::manifest::Target::SharedLibrary); - EXPECT_EQ(m->targets[0].soname, "libdep.so.1"); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->targets.size(), 1u))); + expect(eq(m->targets[0].kind, mcpp::manifest::Target::SharedLibrary)); + expect(eq(m->targets[0].soname, "libdep.so.1")); +}}; -TEST(Manifest, RejectsSonameOnANonLibraryTarget) { +UTest rejects_soname_on_a_non_library_target{"Manifest/RejectsSonameOnANonLibraryTarget", [] { // NARROWED from "non-shared" to "non-library" (#519). A soname is the // name a library is FOUND by, and a package needs to be able to state it // while still being consumed as a static library — otherwise two copies @@ -196,12 +198,11 @@ main = "src/main.cpp" soname = "libapp.so.1" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().message.find("soname is only valid for library targets"), - std::string::npos); -} + expect(fatal(!m.has_value())); + expect(ne(m.error().message.find("soname is only valid for library targets"), std::string::npos)); +}}; -TEST(Manifest, PackageStandardCpp26AcceptedAndMirrored) { +UTest package_standard_cpp26_accepted_and_mirrored{"Manifest/PackageStandardCpp26AcceptedAndMirrored", [] { constexpr auto src = R"( [package] name = "hello26" @@ -214,12 +215,12 @@ kind = "bin" main = "src/main.cpp" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_EQ(m->package.standard, "c++26"); - EXPECT_EQ(m->language.standard, "c++26"); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(eq(m->package.standard, "c++26")); + expect(eq(m->language.standard, "c++26")); +}}; -TEST(Manifest, LegacyLanguageCpp2cNormalizesToCpp26) { +UTest legacy_language_cpp2c_normalizes_to_cpp26{"Manifest/LegacyLanguageCpp2cNormalizesToCpp26", [] { constexpr auto src = R"( [package] name = "hello26" @@ -233,12 +234,12 @@ kind = "bin" main = "src/main.cpp" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_EQ(m->package.standard, "c++26"); - EXPECT_EQ(m->language.standard, "c++26"); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(eq(m->package.standard, "c++26")); + expect(eq(m->language.standard, "c++26")); +}}; -TEST(Manifest, RejectsStdFlagInCxxflags) { +UTest rejects_std_flag_in_cxxflags{"Manifest/RejectsStdFlagInCxxflags", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "x" @@ -251,12 +252,11 @@ sources = ["src/**/*.cppm"] kind = "bin" main = "src/main.cpp" )"); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().message.find("[package].standard"), std::string::npos) - << m.error().message; -} + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().message.find("[package].standard"), std::string::npos)); !failed) failed << m.error().message; +}}; -TEST(Manifest, RejectMissingVersion) { +UTest reject_missing_version{"Manifest/RejectMissingVersion", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "x" @@ -266,11 +266,11 @@ sources = ["src/**/*.cppm"] kind = "bin" main = "src/main.cpp" )"); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().message.find("package.version"), std::string::npos); -} + expect(fatal(!m.has_value())); + expect(ne(m.error().message.find("package.version"), std::string::npos)); +}}; -TEST(Manifest, AcceptsCpp20WithImportStd) { +UTest accepts_cpp20_with_import_std{"Manifest/AcceptsCpp20WithImportStd", [] { // Was RejectImportStdWithoutCpp23: it asserted this manifest is rejected, // but the only thing rejecting it was the standard allow-list (there has // never been an import_std-specific rule) — so the test's name described @@ -290,14 +290,14 @@ sources = ["src/**/*.cppm"] kind = "bin" main = "src/main.cpp" )"); - ASSERT_TRUE(m.has_value()) << m.error().message; - EXPECT_EQ(m->package.standard, "c++20"); // canonical, mirrored from [language] - EXPECT_EQ(m->language.standard, "c++20"); - EXPECT_EQ(m->cppStandard.level, 20); - EXPECT_EQ(m->cppStandard.flag, "-std=c++20"); -} - -TEST(Manifest, RejectsStandardBelowCpp20) { + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().message; + expect(eq(m->package.standard, "c++20")); // canonical, mirrored from [language] + expect(eq(m->language.standard, "c++20")); + expect(eq(m->cppStandard.level, 20)); + expect(eq(m->cppStandard.flag, "-std=c++20")); +}}; + +UTest rejects_standard_below_cpp20{"Manifest/RejectsStandardBelowCpp20", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "x" @@ -307,12 +307,11 @@ standard = "c++17" kind = "bin" main = "src/main.cpp" )"); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().message.find("c++17"), std::string::npos) - << m.error().message; -} + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().message.find("c++17"), std::string::npos)); !failed) failed << m.error().message; +}}; -TEST(Manifest, RejectModulesFalse) { +UTest reject_modules_false{"Manifest/RejectModulesFalse", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "x" @@ -326,10 +325,10 @@ sources = ["src/**/*.cppm"] kind = "bin" main = "src/main.cpp" )"); - ASSERT_FALSE(m.has_value()); -} + expect(fatal(!m.has_value())); +}}; -TEST(Manifest, ParsesDependencies) { +UTest parses_dependencies{"Manifest/ParsesDependencies", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "x" @@ -344,13 +343,13 @@ main = "src/main.cpp" [dev-dependencies] "gtest" = "1.15.2" )"); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_EQ(m->dependencies.size(), 1u); - EXPECT_EQ(m->dependencies.at("mcpplibs.primitives").version, "0.0.1"); - EXPECT_EQ(m->devDependencies.at("gtest").version, "1.15.2"); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(eq(m->dependencies.size(), 1u)); + expect(eq(m->dependencies.at("mcpplibs.primitives").version, "0.0.1")); + expect(eq(m->devDependencies.at("gtest").version, "1.15.2")); +}}; -TEST(Manifest, ParsesDependencyVisibility) { +UTest parses_dependency_visibility{"Manifest/ParsesDependencyVisibility", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "x" @@ -365,18 +364,18 @@ imgui = { version = "1.92.8", visibility = "private" } glfw = { version = "3.4", visibility = "interface" } opengl = "2026.05.31" )"); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->dependencies.size(), 3u); - EXPECT_EQ(m->dependencies.at("compat.imgui").visibility, "private"); - EXPECT_EQ(m->dependencies.at("compat.glfw").visibility, "interface"); - EXPECT_EQ(m->dependencies.at("compat.opengl").visibility, "public"); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->dependencies.size(), 3u))); + expect(eq(m->dependencies.at("compat.imgui").visibility, "private")); + expect(eq(m->dependencies.at("compat.glfw").visibility, "interface")); + expect(eq(m->dependencies.at("compat.opengl").visibility, "public")); +}}; // #242 — consumer-side `default-features = false`: a consumer opts out of a // dependency's own [features].default set (Cargo parity). The flag parses into // DependencySpec.defaultFeatures; explicitly requested `features = [...]` still // come through. Default (omitted) stays true. -TEST(Manifest, ParsesDependencyDefaultFeaturesOptOut) { +UTest parses_dependency_default_features_opt_out{"Manifest/ParsesDependencyDefaultFeaturesOptOut", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "x" @@ -390,19 +389,19 @@ main = "src/main.cpp" ffmpeg = { version = "6.1", default-features = false, features = ["avcodec"] } zlib = "1.3" )"); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->dependencies.size(), 2u); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->dependencies.size(), 2u))); // Opt-out dependency: flag is false, but the explicit feature still parses. const auto& ff = m->dependencies.at("compat.ffmpeg"); - EXPECT_FALSE(ff.defaultFeatures); - ASSERT_EQ(ff.features.size(), 1u); - EXPECT_EQ(ff.features[0], "avcodec"); - EXPECT_EQ(ff.version, "6.1"); + expect(!ff.defaultFeatures); + expect(fatal(eq(ff.features.size(), 1u))); + expect(eq(ff.features[0], "avcodec")); + expect(eq(ff.version, "6.1")); // Bare-string dependency: defaultFeatures defaults to true. - EXPECT_TRUE(m->dependencies.at("compat.zlib").defaultFeatures); -} + expect(bool(m->dependencies.at("compat.zlib").defaultFeatures)); +}}; -TEST(Manifest, RejectsInvalidDependencyVisibility) { +UTest rejects_invalid_dependency_visibility{"Manifest/RejectsInvalidDependencyVisibility", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "x" @@ -410,18 +409,18 @@ version = "0.1.0" [dependencies.compat] imgui = { version = "1.92.8", visibility = "implementation" } )"); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().message.find("visibility"), std::string::npos); -} + expect(fatal(!m.has_value())); + expect(ne(m.error().message.find("visibility"), std::string::npos)); +}}; -TEST(Manifest, DefaultTemplateRoundTrip) { +UTest default_template_round_trip{"Manifest/DefaultTemplateRoundTrip", [] { auto src = mcpp::manifest::default_template("hello"); auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_EQ(m->package.name, "hello"); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(eq(m->package.name, "hello")); +}}; -TEST(ManifestEditor, UpsertsCanonicalDependencyByExactPackageId) { +UTest upserts_canonical_dependency_by_exact_package_id{"ManifestEditor/UpsertsCanonicalDependencyByExactPackageId", [] { constexpr std::string_view source = R"([package] name = "app" version = "0.1.0" @@ -435,21 +434,19 @@ widget = "1.0.0" .version = "2.0.0", .features = {"gui", "vulkan"}, }); - ASSERT_TRUE(edited.has_value()) << edited.error(); - EXPECT_NE(edited->find("[dependencies.acme]\nwidget = { version = \"2.0.0\", features = [\"gui\", \"vulkan\"] }"), - std::string::npos); + if (auto&& failed = expect(fatal(edited.has_value())); !failed) failed << edited.error(); + expect(ne(edited->find("[dependencies.acme]\nwidget = { version = \"2.0.0\", features = [\"gui\", \"vulkan\"] }"), std::string::npos)); auto parsed = mcpp::manifest::parse_string(*edited); - ASSERT_TRUE(parsed.has_value()) << parsed.error().format(); - ASSERT_EQ(parsed->dependencies.size(), 2u); - EXPECT_EQ(parsed->dependencies.at("acme.widget").namespace_, "acme"); - EXPECT_EQ(parsed->dependencies.at("acme.widget").shortName, "widget"); - EXPECT_EQ(parsed->dependencies.at("acme.widget").version, "2.0.0"); - EXPECT_EQ(parsed->dependencies.at("acme.widget").features, - (std::vector{"gui", "vulkan"})); - EXPECT_EQ(parsed->dependencies.at("compat.widget").version, "1.0.0"); -} - -TEST(ManifestEditor, ReplacesCanonicalEntryAndRejectsInvalidSource) { + if (auto&& failed = expect(fatal(parsed.has_value())); !failed) failed << parsed.error().format(); + expect(fatal(eq(parsed->dependencies.size(), 2u))); + expect(eq(parsed->dependencies.at("acme.widget").namespace_, "acme")); + expect(eq(parsed->dependencies.at("acme.widget").shortName, "widget")); + expect(eq(parsed->dependencies.at("acme.widget").version, "2.0.0")); + expect(eq(parsed->dependencies.at("acme.widget").features, (std::vector{"gui", "vulkan"}))); + expect(eq(parsed->dependencies.at("compat.widget").version, "1.0.0")); +}}; + +UTest replaces_canonical_entry_and_rejects_invalid_source{"ManifestEditor/ReplacesCanonicalEntryAndRejectsInvalidSource", [] { constexpr std::string_view source = R"([package] name = "app" version = "0.1.0" @@ -462,12 +459,11 @@ widget = "1.0.0" .shortName = "widget", .version = "3.0.0", }); - ASSERT_TRUE(edited.has_value()) << edited.error(); + if (auto&& failed = expect(fatal(edited.has_value())); !failed) failed << edited.error(); auto parsed = mcpp::manifest::parse_string(*edited); - ASSERT_TRUE(parsed.has_value()) << parsed.error().format(); - EXPECT_EQ(parsed->dependencies.at("acme.widget").version, "3.0.0"); - EXPECT_EQ(std::ranges::count(*edited, '\n'), - std::ranges::count(source, '\n')); + if (auto&& failed = expect(fatal(parsed.has_value())); !failed) failed << parsed.error().format(); + expect(eq(parsed->dependencies.at("acme.widget").version, "3.0.0")); + expect(eq(std::ranges::count(*edited, '\n'), std::ranges::count(source, '\n'))); auto invalid = mcpp::manifest::upsert_dependency_text( "[package\nname = \"broken\"", { @@ -475,11 +471,11 @@ widget = "1.0.0" .shortName = "widget", .version = "1.0.0", }); - ASSERT_FALSE(invalid.has_value()); - EXPECT_NE(invalid.error().find("invalid manifest"), std::string::npos); -} + expect(fatal(!invalid.has_value())); + expect(ne(invalid.error().find("invalid manifest"), std::string::npos)); +}}; -TEST(ListXpkgVersions, MultipleEntriesAcrossPlatforms) { +UTest multiple_entries_across_platforms{"ListXpkgVersions/MultipleEntriesAcrossPlatforms", [] { constexpr auto src = R"( package = { name = "foo", @@ -490,35 +486,62 @@ package = { ["1.0.0"] = { url = "u3", sha256 = "z" }, }, macosx = { - ["0.1.0"] = { url = "u1m", sha256 = "x" }, + ["0.1.0"] = { url = "u1m", sha256 = "xm" }, }, }, } )"; auto linux = mcpp::manifest::list_xpkg_versions(src, mcpp::toolchain::platform::TargetPlatform::for_lint_of("linux")); - ASSERT_EQ(linux.size(), 3u); - EXPECT_EQ(linux[0], "0.1.0"); - EXPECT_EQ(linux[1], "0.2.0"); - EXPECT_EQ(linux[2], "1.0.0"); + expect(fatal(eq(linux.size(), 3u))); + expect(eq(linux[0], "0.1.0")); + expect(eq(linux[1], "0.2.0")); + expect(eq(linux[2], "1.0.0")); auto mac = mcpp::manifest::list_xpkg_versions(src, mcpp::toolchain::platform::TargetPlatform::for_lint_of("macosx")); - ASSERT_EQ(mac.size(), 1u); - EXPECT_EQ(mac[0], "0.1.0"); + expect(fatal(eq(mac.size(), 1u))); + expect(eq(mac[0], "0.1.0")); + // No block of its own, and no artifact the same in every block: nothing. auto win = mcpp::manifest::list_xpkg_versions(src, mcpp::toolchain::platform::TargetPlatform::for_lint_of("windows")); - EXPECT_TRUE(win.empty()); + expect(win.empty()); +}}; + +// THE ARTIFACT THAT IS ONE FOR EVERY TARGET (mcpp#911 G6): a target whose block +// the descriptor lacks takes the versions whose artifact is the same in every +// block it has -- a source archive -- and not those built per system. +UTest an_artifact_the_same_in_every_block_serves_every_target{"ListXpkgVersions/AnArtifactTheSameInEveryBlockServesEveryTarget", [] { + constexpr auto src = R"( +package = { + name = "foo", + xpm = { + linux = { + ["1.0.0"] = { url = "src.tar.gz", sha256 = "s" }, + ["1.1.0"] = { url = "linux.tar.gz", sha256 = "l" }, + }, + macosx = { + ["1.0.0"] = { url = "src.tar.gz", sha256 = "s" }, + ["1.1.0"] = { url = "macos.tar.gz", sha256 = "m" }, + }, + }, } +)"; + auto any = mcpp::manifest::list_xpkg_versions(src, mcpp::toolchain::platform::TargetPlatform::with_keys({"toyos"})); + expect(fatal(eq(any.size(), 1u))); + expect(eq(any[0], "1.0.0")); + auto linux = mcpp::manifest::list_xpkg_versions(src, mcpp::toolchain::platform::TargetPlatform::for_lint_of("linux")); + expect(eq(linux.size(), 2u)); +}}; -TEST(ListXpkgVersions, MissingXpmReturnsEmpty) { +UTest missing_xpm_returns_empty{"ListXpkgVersions/MissingXpmReturnsEmpty", [] { constexpr auto src = R"(package = { name = "foo" })"; - EXPECT_TRUE(mcpp::manifest::list_xpkg_versions(src, mcpp::toolchain::platform::TargetPlatform::for_lint_of("linux")).empty()); -} + expect(mcpp::manifest::list_xpkg_versions(src, mcpp::toolchain::platform::TargetPlatform::for_lint_of("linux")).empty()); +}}; // #363: `["25.0.4"] = { ref = "25.0.4.7.1" }` is a POINTER at another entry, // not a release. Shape copied from the real jdk-corretto / jdk-temurin // descriptors. Treating it as a version gave range resolution a candidate that // ties with (or truncates to something other than) its own target. -TEST(ListXpkgVersions, AliasEntriesAreFlagged) { +UTest alias_entries_are_flagged{"ListXpkgVersions/AliasEntriesAreFlagged", [] { constexpr auto src = R"( package = { name = "jdk", @@ -533,26 +556,26 @@ package = { )"; auto e = mcpp::manifest::list_xpkg_version_entries( src, mcpp::toolchain::platform::TargetPlatform::for_lint_of("linux")); - ASSERT_EQ(e.size(), 3u); - EXPECT_EQ(e[0].version, "latest"); EXPECT_TRUE (e[0].alias); - EXPECT_EQ(e[1].version, "25.0.4"); EXPECT_TRUE (e[1].alias); - EXPECT_EQ(e[2].version, "25.0.4.7.1"); EXPECT_FALSE(e[2].alias); - EXPECT_TRUE(e[0].sha256.empty()); - EXPECT_TRUE(e[1].sha256.empty()); - EXPECT_EQ(e[2].sha256, "z"); + expect(fatal(eq(e.size(), 3u))); + expect(eq(e[0].version, "latest")); expect(bool(e[0].alias)); + expect(eq(e[1].version, "25.0.4")); expect(bool(e[1].alias)); + expect(eq(e[2].version, "25.0.4.7.1")); expect(!(e[2].alias)); + expect(e[0].sha256.empty()); + expect(e[1].sha256.empty()); + expect(eq(e[2].sha256, "z")); // The keys-only view is unchanged for every existing caller. auto keys = mcpp::manifest::list_xpkg_versions( src, mcpp::toolchain::platform::TargetPlatform::for_lint_of("linux")); - ASSERT_EQ(keys.size(), 3u); - EXPECT_EQ(keys[2], "25.0.4.7.1"); -} + expect(fatal(eq(keys.size(), 3u))); + expect(eq(keys[2], "25.0.4.7.1")); +}}; // openxlings/xlings#620, mcpp#524 A: a version entry's `revision` is read the // way the reference implementation reads it -- a non-negative integer, and 0 // for anything else or for its absence -- including the per-arch shape, whose // arch tables must not be mistaken for the revision. -TEST(ListXpkgVersions, RevisionIsReadPerEntry) { +UTest revision_is_read_per_entry{"ListXpkgVersions/RevisionIsReadPerEntry", [] { constexpr auto src = R"( package = { name = "rev", @@ -570,19 +593,19 @@ package = { )"; auto e = mcpp::manifest::list_xpkg_version_entries( src, mcpp::toolchain::platform::TargetPlatform::for_lint_of("linux")); - ASSERT_EQ(e.size(), 6u); - EXPECT_EQ(e[0].revision, 0); // an alias carries none - EXPECT_EQ(e[1].revision, 0); - EXPECT_EQ(e[2].revision, 2); - EXPECT_EQ(e[3].revision, 1); - EXPECT_EQ(e[4].revision, 0); // a string is not a revision - EXPECT_EQ(e[5].revision, 0); // nor is a negative number -} + expect(fatal(eq(e.size(), 6u))); + expect(eq(e[0].revision, 0)); // an alias carries none + expect(eq(e[1].revision, 0)); + expect(eq(e[2].revision, 2)); + expect(eq(e[3].revision, 1)); + expect(eq(e[4].revision, 0)); // a string is not a revision + expect(eq(e[5].revision, 0)); // nor is a negative number +}}; // The scanner used to walk the platform table character by character, so a // bracket key nested inside a version's own body (mirror tables write // `["GLOBAL"] = "https://..."`) counted as a published version. -TEST(ListXpkgVersions, NestedBracketKeysAreNotVersions) { +UTest nested_bracket_keys_are_not_versions{"ListXpkgVersions/NestedBracketKeysAreNotVersions", [] { constexpr auto src = R"( package = { name = "foo", @@ -599,12 +622,12 @@ package = { )"; auto keys = mcpp::manifest::list_xpkg_versions( src, mcpp::toolchain::platform::TargetPlatform::for_lint_of("linux")); - ASSERT_EQ(keys.size(), 2u); - EXPECT_EQ(keys[0], "1.0.0"); - EXPECT_EQ(keys[1], "1.1.0"); -} + expect(fatal(eq(keys.size(), 2u))); + expect(eq(keys[0], "1.0.0")); + expect(eq(keys[1], "1.1.0")); +}}; -TEST(Manifest, BuildCflagsCxxflagsAndCStandard) { +UTest build_cflags_cxxflags_and_c_standard{"Manifest/BuildCflagsCxxflagsAndCStandard", [] { constexpr auto src = R"( [package] name = "x" @@ -619,23 +642,23 @@ c_standard = "c11" kind = "lib" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->buildConfig.cflags.size(), 2u); - EXPECT_EQ(m->buildConfig.cflags[0], "-Wall"); - EXPECT_EQ(m->buildConfig.cflags[1], "-DFOO=1"); - ASSERT_EQ(m->buildConfig.cxxflags.size(), 1u); - EXPECT_EQ(m->buildConfig.cxxflags[0], "-Wextra"); - ASSERT_EQ(m->buildConfig.ldflags.size(), 2u); - EXPECT_EQ(m->buildConfig.ldflags[0], "-lfoo"); - EXPECT_EQ(m->buildConfig.ldflags[1], "-Wl,--as-needed"); - EXPECT_EQ(m->buildConfig.cStandard, "c11"); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->buildConfig.cflags.size(), 2u))); + expect(eq(m->buildConfig.cflags[0], "-Wall")); + expect(eq(m->buildConfig.cflags[1], "-DFOO=1")); + expect(fatal(eq(m->buildConfig.cxxflags.size(), 1u))); + expect(eq(m->buildConfig.cxxflags[0], "-Wextra")); + expect(fatal(eq(m->buildConfig.ldflags.size(), 2u))); + expect(eq(m->buildConfig.ldflags[0], "-lfoo")); + expect(eq(m->buildConfig.ldflags[1], "-Wl,--as-needed")); + expect(eq(m->buildConfig.cStandard, "c11")); +}}; // #296: [build].defines is sugar for `-D` on both C and C++ channels; it // must parse into buildConfig.defines so prepare_build can fold it into flags // before the P1689 scan and fingerprint. Order is preserved because the fold // appends in declaration order and "last flag wins" is the override semantics. -TEST(Manifest, BuildDefinesParsesIntoSeparateVector) { +UTest build_defines_parses_into_separate_vector{"Manifest/BuildDefinesParsesIntoSeparateVector", [] { constexpr auto src = R"( [package] name = "x" @@ -644,15 +667,15 @@ version = "0.1.0" defines = ["TEST_USE_MODULES", "VALUE=42"] )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->buildConfig.defines.size(), 2u); - EXPECT_EQ(m->buildConfig.defines[0], "TEST_USE_MODULES"); - EXPECT_EQ(m->buildConfig.defines[1], "VALUE=42"); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->buildConfig.defines.size(), 2u))); + expect(eq(m->buildConfig.defines[0], "TEST_USE_MODULES")); + expect(eq(m->buildConfig.defines[1], "VALUE=42")); // A well-formed [build] must not warn — the unknown-key guard below is // only allowed to fire on keys the parser genuinely does not read. - EXPECT_TRUE(m->schemaWarnings.empty()) + expect(m->schemaWarnings.empty()) << (m->schemaWarnings.empty() ? "" : m->schemaWarnings[0]); -} +}}; // ── #540: the three vocabularies, and the drift that keeps happening ──────── // @@ -675,7 +698,7 @@ defines = ["TEST_USE_MODULES", "VALUE=42"] // let this happen twice. Add a key to the parser, add it here; if you forget, // the negative control at the bottom of each test still passes and this one // goes red. -TEST(Manifest, EveryBuildKeyTheParserReadsIsAccepted) { +UTest every_build_key_the_parser_reads_is_accepted{"Manifest/EveryBuildKeyTheParserReadsIsAccepted", [] { constexpr auto src = R"( [package] name = "x" @@ -710,23 +733,23 @@ std-compat-module = "gen/std.compat.cppm" std-module-flags = ["-D_GNU_SOURCE"] )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); std::string all; for (auto const& w : m->schemaWarnings) { all += w; all += '\n'; } - EXPECT_TRUE(m->schemaWarnings.empty()) + expect(m->schemaWarnings.empty()) << "a key the parser reads was reported as unsupported:\n" << all; // The three that were absent, asserted individually so a regression names // itself rather than arriving as "some warning appeared". - EXPECT_EQ(m->stdModule, "gen/std.cppm"); - EXPECT_EQ(m->stdCompatModule, "gen/std.compat.cppm"); - ASSERT_EQ(m->buildConfig.stdModuleFlags.size(), 1u); - EXPECT_EQ(m->buildConfig.stdModuleFlags[0], "-D_GNU_SOURCE"); -} + expect(eq(m->stdModule, "gen/std.cppm")); + expect(eq(m->stdCompatModule, "gen/std.compat.cppm")); + expect(fatal(eq(m->buildConfig.stdModuleFlags.size(), 1u))); + expect(eq(m->buildConfig.stdModuleFlags[0], "-D_GNU_SOURCE")); +}}; // The negative control. Without it the test above passes against a parser whose // unknown-key check has been deleted outright, which is the failure mode a // "no warnings" assertion invites. -TEST(Manifest, AnUnknownBuildKeyIsStillReported) { +UTest an_unknown_build_key_is_still_reported{"Manifest/AnUnknownBuildKeyIsStillReported", [] { constexpr auto src = R"( [package] name = "x" @@ -735,18 +758,18 @@ version = "0.1.0" std_module = "gen/std.cppm" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->schemaWarnings.size(), 1u); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->schemaWarnings.size(), 1u))); // The underscore spelling is the plausible typo for the hyphenated key, so // it is the one worth holding: accepting it silently is how `schedule` got // in while `bmi_schedule` was being reported as unsupported. - EXPECT_NE(m->schemaWarnings[0].find("std_module"), std::string::npos) + expect(ne(m->schemaWarnings[0].find("std_module"), std::string::npos)) << m->schemaWarnings[0]; // The message must carry the SAME list the check uses — it used to be a // third hand-written copy and had drifted from both others. - EXPECT_NE(m->schemaWarnings[0].find("std-module"), std::string::npos) + expect(ne(m->schemaWarnings[0].find("std-module"), std::string::npos)) << "the message must name the supported keys it actually checks against"; -} +}}; // `kKnownConditionalBuildKeys`' own comment says its vocabulary "is exactly // that struct's members" and "MUST stay in sync with the reads above and with @@ -755,7 +778,7 @@ std_module = "gen/std.cppm" // `target_cfg` block, the other grammar for this same axis. So one spelling of // a conditional private include directory worked and the other reported the // key as unsupported. -TEST(Manifest, EveryConditionalBuildKeyTheAxisCarriesIsAccepted) { +UTest every_conditional_build_key_the_axis_carries_is_accepted{"Manifest/EveryConditionalBuildKeyTheAxisCarriesIsAccepted", [] { constexpr auto src = R"( [package] name = "x" @@ -772,24 +795,24 @@ sources = ["src/extra.cpp"] std-module-flags = ["-D_GNU_SOURCE"] )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); std::string all; for (auto const& w : m->schemaWarnings) { all += w; all += '\n'; } - EXPECT_TRUE(m->schemaWarnings.empty()) + expect(m->schemaWarnings.empty()) << "a BuildInputs member the axis carries was reported unsupported:\n" << all; - ASSERT_EQ(m->conditionalConfigs.size(), 1u); + expect(fatal(eq(m->conditionalConfigs.size(), 1u))); auto const& in = m->conditionalConfigs[0].inputs; // #494 moved `std-module-flags` onto BuildInputs specifically so this axis // could carry it — "membership here is what makes the cfg axis carry it", // with -D_GNU_SOURCE right for musl and glibc and wrong for picolibc as the // stated case. The member and the merge landed; the READ did not. - ASSERT_EQ(in.stdModuleFlags.size(), 1u); - EXPECT_EQ(in.stdModuleFlags[0], "-D_GNU_SOURCE"); - ASSERT_EQ(in.privateIncludeDirs.size(), 1u); - EXPECT_EQ(in.privateIncludeDirs[0].generic_string(), "internal"); -} + expect(fatal(eq(in.stdModuleFlags.size(), 1u))); + expect(eq(in.stdModuleFlags[0], "-D_GNU_SOURCE")); + expect(fatal(eq(in.privateIncludeDirs.size(), 1u))); + expect(eq(in.privateIncludeDirs[0].generic_string(), "internal")); +}}; -TEST(Manifest, AnUnknownConditionalBuildKeyIsStillReported) { +UTest an_unknown_conditional_build_key_is_still_reported{"Manifest/AnUnknownConditionalBuildKeyIsStillReported", [] { constexpr auto src = R"( [package] name = "x" @@ -798,15 +821,14 @@ version = "0.1.0" static_stdlib = true )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->schemaWarnings.size(), 1u); - EXPECT_NE(m->schemaWarnings[0].find("static_stdlib"), std::string::npos) - << m->schemaWarnings[0]; + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->schemaWarnings.size(), 1u))); + if (auto&& failed = expect(ne(m->schemaWarnings[0].find("static_stdlib"), std::string::npos)); !failed) failed << m->schemaWarnings[0]; // Built from the list, not written beside it: the prose form named eight // keys and was two behind the check it was describing. - EXPECT_NE(m->schemaWarnings[0].find("std-module-flags"), std::string::npos) + expect(ne(m->schemaWarnings[0].find("std-module-flags"), std::string::npos)) << "the message must be generated from the same list the check uses"; -} +}}; // #717: `dialect_cxxflags` under `[target..build]` used to be // reported as "unsupported key 'dialect_cxxflags' (ignored)" and reached no @@ -814,7 +836,7 @@ static_stdlib = true // and parsed into `ConditionalConfig::dialectCxxflags` -- a member of its own, // NOT of `BuildInputs` (`.inputs`), because the key is graph-wide rather than // a per-package additive input (design 2026-09-27 §6.2). -TEST(Manifest, ConditionalDialectCxxflagsAcceptedAndKeptOffBuildInputs) { +UTest conditional_dialect_cxxflags_accepted_and_kept_off_build_inputs{"Manifest/ConditionalDialectCxxflagsAcceptedAndKeptOffBuildInputs", [] { constexpr auto src = R"( [package] name = "x" @@ -824,24 +846,23 @@ cxxflags = ["-DORDINARY"] dialect_cxxflags = ["-DDIALECT_A", "-DDIALECT_B"] )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_TRUE(m->schemaWarnings.empty()) - << (m->schemaWarnings.empty() ? "" : m->schemaWarnings[0]); - ASSERT_EQ(m->conditionalConfigs.size(), 1u); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + if (auto&& failed = expect(m->schemaWarnings.empty()); !failed) failed << (m->schemaWarnings.empty() ? "" : m->schemaWarnings[0]); + expect(fatal(eq(m->conditionalConfigs.size(), 1u))); auto const& cc = m->conditionalConfigs[0]; - ASSERT_EQ(cc.dialectCxxflags.size(), 2u); - EXPECT_EQ(cc.dialectCxxflags[0], "-DDIALECT_A"); - EXPECT_EQ(cc.dialectCxxflags[1], "-DDIALECT_B"); + expect(fatal(eq(cc.dialectCxxflags.size(), 2u))); + expect(eq(cc.dialectCxxflags[0], "-DDIALECT_A")); + expect(eq(cc.dialectCxxflags[1], "-DDIALECT_B")); // An ordinary per-package build input written in the SAME section still // lands on BuildInputs, as always -- only `dialect_cxxflags` is diverted. - ASSERT_EQ(cc.inputs.cxxflags.size(), 1u); - EXPECT_EQ(cc.inputs.cxxflags[0], "-DORDINARY"); -} + expect(fatal(eq(cc.inputs.cxxflags.size(), 1u))); + expect(eq(cc.inputs.cxxflags[0], "-DORDINARY")); +}}; // The negative control for the positive test above: a section carrying ONLY // `dialect_cxxflags` must still be recorded, the same emptiness-gate rule // `ConditionalSectionWithOnlyDefinesIsRecorded` states for `defines`. -TEST(Manifest, ConditionalSectionWithOnlyDialectCxxflagsIsRecorded) { +UTest conditional_section_with_only_dialect_cxxflags_is_recorded{"Manifest/ConditionalSectionWithOnlyDialectCxxflagsIsRecorded", [] { constexpr auto src = R"( [package] name = "x" @@ -850,12 +871,12 @@ version = "0.1.0" dialect_cxxflags = ["-DONLY_DIALECT"] )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_TRUE(m->schemaWarnings.empty()); - ASSERT_EQ(m->conditionalConfigs.size(), 1u); - ASSERT_EQ(m->conditionalConfigs[0].dialectCxxflags.size(), 1u); - EXPECT_EQ(m->conditionalConfigs[0].dialectCxxflags[0], "-DONLY_DIALECT"); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(m->schemaWarnings.empty()); + expect(fatal(eq(m->conditionalConfigs.size(), 1u))); + expect(fatal(eq(m->conditionalConfigs[0].dialectCxxflags.size(), 1u))); + expect(eq(m->conditionalConfigs[0].dialectCxxflags[0], "-DONLY_DIALECT")); +}}; // The resolution rules: only a matching row contributes, a non-matching one // contributes nothing, and the order is the root's own `[build]` first, then @@ -866,7 +887,7 @@ namespace manifest_dialect { mcpp::manifest::Manifest merged_for(std::string_view src, std::string_view triple) { auto m = mcpp::manifest::parse_string(src); - EXPECT_TRUE(m.has_value()) << (m.has_value() ? "" : m.error().format()); + if (auto&& failed = expect(m.has_value()); !failed) failed << (m.has_value() ? "" : m.error().format()); if (!m) return {}; mcpp::build::merge_conditional_config(*m, mcpp::build::cfgpred::context_for(triple)); return *m; @@ -874,7 +895,7 @@ mcpp::manifest::Manifest merged_for(std::string_view src, std::string_view tripl } // namespace manifest_dialect -TEST(ConditionalDialectCxxflags, RootBuildPrecedesTheMatchingRow) { +UTest root_build_precedes_the_matching_row{"ConditionalDialectCxxflags/RootBuildPrecedesTheMatchingRow", [] { constexpr auto src = R"( [package] name = "x" @@ -887,10 +908,10 @@ dialect_cxxflags = ["-DLINUX_ONE"] dialect_cxxflags = ["-DWINDOWS_ONLY"] )"; auto lin = manifest_dialect::merged_for(src, "x86_64-unknown-linux-gnu"); - ASSERT_EQ(lin.buildConfig.dialectCxxflags.size(), 2u); - EXPECT_EQ(lin.buildConfig.dialectCxxflags[0], "-DBASE"); - EXPECT_EQ(lin.buildConfig.dialectCxxflags[1], "-DLINUX_ONE"); -} + expect(fatal(eq(lin.buildConfig.dialectCxxflags.size(), 2u))); + expect(eq(lin.buildConfig.dialectCxxflags[0], "-DBASE")); + expect(eq(lin.buildConfig.dialectCxxflags[1], "-DLINUX_ONE")); +}}; // Two DIFFERENT selectors that both match the same resolved triple both // contribute -- this is not "last matching wins" (that rule belongs to a @@ -900,7 +921,7 @@ dialect_cxxflags = ["-DWINDOWS_ONLY"] // is populated from the underlying `Table` (`std::map`, ordered by selector // text), not by source position, which is a property of the parser this // feature inherits rather than one it introduces or could change. -TEST(ConditionalDialectCxxflags, TwoMatchingSelectorsBothContribute) { +UTest two_matching_selectors_both_contribute{"ConditionalDialectCxxflags/TwoMatchingSelectorsBothContribute", [] { constexpr auto src = R"( [package] name = "x" @@ -913,17 +934,16 @@ dialect_cxxflags = ["-DLINUX_ONE"] dialect_cxxflags = ["-DX86"] )"; auto lin = manifest_dialect::merged_for(src, "x86_64-unknown-linux-gnu"); - ASSERT_EQ(lin.buildConfig.dialectCxxflags.size(), 3u); - EXPECT_EQ(lin.buildConfig.dialectCxxflags[0], "-DBASE") - << "the root's own unconditional [build] value always leads: it is " + expect(fatal(eq(lin.buildConfig.dialectCxxflags.size(), 3u))); + if (auto&& failed = expect(eq(lin.buildConfig.dialectCxxflags[0], "-DBASE")); !failed) failed << "the root's own unconditional [build] value always leads: it is " "set before the conditional-merge loop runs, regardless of which " "matching row the loop visits first"; auto const& flags = lin.buildConfig.dialectCxxflags; - EXPECT_NE(std::find(flags.begin(), flags.end(), "-DLINUX_ONE"), flags.end()); - EXPECT_NE(std::find(flags.begin(), flags.end(), "-DX86"), flags.end()); -} + expect(ne(std::find(flags.begin(), flags.end(), "-DLINUX_ONE"), flags.end())); + expect(ne(std::find(flags.begin(), flags.end(), "-DX86"), flags.end())); +}}; -TEST(ConditionalDialectCxxflags, ASelectorThatDoesNotMatchContributesNothing) { +UTest a_selector_that_does_not_match_contributes_nothing{"ConditionalDialectCxxflags/ASelectorThatDoesNotMatchContributesNothing", [] { constexpr auto src = R"( [package] name = "x" @@ -934,15 +954,15 @@ dialect_cxxflags = ["-DBASE"] dialect_cxxflags = ["-DWINDOWS_ONLY"] )"; auto lin = manifest_dialect::merged_for(src, "x86_64-unknown-linux-gnu"); - ASSERT_EQ(lin.buildConfig.dialectCxxflags.size(), 1u); - EXPECT_EQ(lin.buildConfig.dialectCxxflags[0], "-DBASE"); -} + expect(fatal(eq(lin.buildConfig.dialectCxxflags.size(), 1u))); + expect(eq(lin.buildConfig.dialectCxxflags[0], "-DBASE")); +}}; // #540: `[features]` was the one structured section with no schema check at // all, so a misplaced `include_dirs` inside a feature built successfully with // zero diagnostics — while the identical misplacement in `[build]` or // `[target..build]` is reported. -TEST(Manifest, EveryFeatureKeyTheParserReadsIsAccepted) { +UTest every_feature_key_the_parser_reads_is_accepted{"Manifest/EveryFeatureKeyTheParserReadsIsAccepted", [] { constexpr auto src = R"( [package] name = "x" @@ -952,14 +972,14 @@ full = { implies = ["fast"], forward = ["dep/feat"], defines = ["WITH_FULL"], so fast = [] )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); std::string all; for (auto const& w : m->schemaWarnings) { all += w; all += '\n'; } - EXPECT_TRUE(m->schemaWarnings.empty()) + expect(m->schemaWarnings.empty()) << "a feature key the parser reads was reported as unsupported:\n" << all; -} +}}; -TEST(Manifest, AnUnknownFeatureKeyIsReported) { +UTest an_unknown_feature_key_is_reported{"Manifest/AnUnknownFeatureKeyIsReported", [] { constexpr auto src = R"( [package] name = "x" @@ -968,19 +988,18 @@ version = "0.1.0" fast = { implies = [], include_dirs = ["nope"] } )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->schemaWarnings.size(), 1u); - EXPECT_NE(m->schemaWarnings[0].find("include_dirs"), std::string::npos) - << m->schemaWarnings[0]; - EXPECT_NE(m->schemaWarnings[0].find("fast"), std::string::npos) + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->schemaWarnings.size(), 1u))); + if (auto&& failed = expect(ne(m->schemaWarnings[0].find("include_dirs"), std::string::npos)); !failed) failed << m->schemaWarnings[0]; + expect(ne(m->schemaWarnings[0].find("fast"), std::string::npos)) << "the message must name WHICH feature carries the key"; -} +}}; // `deps` is reserved rather than wrong — the parser's own comment has promised // it since Feature System v2 — so it is reported as reserved. Saying // "unsupported" would deny a documented plan; saying nothing is what let it // look implemented. -TEST(Manifest, AReservedFeatureKeySaysItIsReserved) { +UTest a_reserved_feature_key_says_it_is_reserved{"Manifest/AReservedFeatureKeySaysItIsReserved", [] { constexpr auto src = R"( [package] name = "x" @@ -989,18 +1008,17 @@ version = "0.1.0" fast = { implies = [], deps = ["zlib"] } )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->schemaWarnings.size(), 1u); - EXPECT_NE(m->schemaWarnings[0].find("reserved"), std::string::npos) - << m->schemaWarnings[0]; -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->schemaWarnings.size(), 1u))); + if (auto&& failed = expect(ne(m->schemaWarnings[0].find("reserved"), std::string::npos)); !failed) failed << m->schemaWarnings[0]; +}}; // #296: `defines` is a BuildInputs member, NOT a BuildConfig-only field — so // the cfg axis carries it like any other build input. This is the regression // that matters: a platform-only macro must be expressible, because a key that // parses unconditionally but vanishes under `[target.'cfg(...)'.build]` is the // exact silent-drop failure #296 was filed for, just one section over. -TEST(Manifest, ConditionalBuildDefinesAreCarriedByTheCfgAxis) { +UTest conditional_build_defines_are_carried_by_the_cfg_axis{"Manifest/ConditionalBuildDefinesAreCarriedByTheCfgAxis", [] { constexpr auto src = R"( [package] name = "x" @@ -1011,21 +1029,20 @@ defines = ["BASE"] defines = ["USE_WIN32", "WINVER=0x0A00"] )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->conditionalConfigs.size(), 1u); - EXPECT_EQ(m->conditionalConfigs[0].predicate, "cfg(windows)"); - ASSERT_EQ(m->conditionalConfigs[0].inputs.defines.size(), 2u); - EXPECT_EQ(m->conditionalConfigs[0].inputs.defines[0], "USE_WIN32"); - EXPECT_EQ(m->conditionalConfigs[0].inputs.defines[1], "WINVER=0x0A00"); - EXPECT_TRUE(m->schemaWarnings.empty()) - << (m->schemaWarnings.empty() ? "" : m->schemaWarnings[0]); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->conditionalConfigs.size(), 1u))); + expect(eq(m->conditionalConfigs[0].predicate, "cfg(windows)")); + expect(fatal(eq(m->conditionalConfigs[0].inputs.defines.size(), 2u))); + expect(eq(m->conditionalConfigs[0].inputs.defines[0], "USE_WIN32")); + expect(eq(m->conditionalConfigs[0].inputs.defines[1], "WINVER=0x0A00")); + if (auto&& failed = expect(m->schemaWarnings.empty()); !failed) failed << (m->schemaWarnings.empty() ? "" : m->schemaWarnings[0]); +}}; // A conditional section carrying ONLY `defines` must still be recorded — the // emptiness gate that decides whether to push a ConditionalConfig has to know // about every BuildInputs member, or the section is dropped before it is ever // evaluated. -TEST(Manifest, ConditionalSectionWithOnlyDefinesIsRecorded) { +UTest conditional_section_with_only_defines_is_recorded{"Manifest/ConditionalSectionWithOnlyDefinesIsRecorded", [] { constexpr auto src = R"( [package] name = "x" @@ -1034,32 +1051,32 @@ version = "0.1.0" defines = ["ONLY_DEFINE"] )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->conditionalConfigs.size(), 1u); - ASSERT_EQ(m->conditionalConfigs[0].inputs.defines.size(), 1u); - EXPECT_EQ(m->conditionalConfigs[0].inputs.defines[0], "ONLY_DEFINE"); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->conditionalConfigs.size(), 1u))); + expect(fatal(eq(m->conditionalConfigs[0].inputs.defines.size(), 1u))); + expect(eq(m->conditionalConfigs[0].inputs.defines[0], "ONLY_DEFINE")); +}}; // The single additive merge must carry `defines` too — merge_conditional_ // build_inputs routes the cfg axis through append(), so a member missing here // is a member that silently never reaches the build. -TEST(Manifest, AppendBuildInputsMergesDefines) { +UTest append_build_inputs_merges_defines{"Manifest/AppendBuildInputsMergesDefines", [] { mcpp::manifest::BuildInputs dst, src; dst.defines = {"BASE"}; src.defines = {"COND"}; mcpp::manifest::append(dst, src); - ASSERT_EQ(dst.defines.size(), 2u); - EXPECT_EQ(dst.defines[0], "BASE"); + expect(fatal(eq(dst.defines.size(), 2u))); + expect(eq(dst.defines[0], "BASE")); // Conditional entries land AFTER the base ones so GNU last-wins gives the // conditional rule precedence (an off-OS `-U` after the base `-D`). - EXPECT_EQ(dst.defines[1], "COND"); -} + expect(eq(dst.defines[1], "COND")); +}}; // #296 root cause: an unknown [build] key used to vanish without a word, and // the build then failed much later with a module-graph divergence naming // neither the key nor the manifest. Same policy as [targets.]: a schema // warning (an error under --strict). -TEST(Manifest, UnknownBuildKeyIsReportedNotSilentlyDropped) { +UTest unknown_build_key_is_reported_not_silently_dropped{"Manifest/UnknownBuildKeyIsReportedNotSilentlyDropped", [] { constexpr auto src = R"( [package] name = "x" @@ -1069,19 +1086,17 @@ cxxflags = ["-Wall"] defnes = ["TYPO"] )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->schemaWarnings.size(), 1u); - EXPECT_NE(m->schemaWarnings[0].find("defnes"), std::string::npos) - << m->schemaWarnings[0]; - EXPECT_NE(m->schemaWarnings[0].find("[build]"), std::string::npos) - << m->schemaWarnings[0]; + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->schemaWarnings.size(), 1u))); + if (auto&& failed = expect(ne(m->schemaWarnings[0].find("defnes"), std::string::npos)); !failed) failed << m->schemaWarnings[0]; + if (auto&& failed = expect(ne(m->schemaWarnings[0].find("[build]"), std::string::npos)); !failed) failed << m->schemaWarnings[0]; // The key is still ignored — this is a warning, not a parse failure. - EXPECT_EQ(m->buildConfig.cxxflags.size(), 1u); -} + expect(eq(m->buildConfig.cxxflags.size(), 1u)); +}}; // The conditional axis may only contribute build INPUTS, so a selection knob // under it is inexpressible by construction. Say so rather than dropping it. -TEST(Manifest, UnknownConditionalBuildKeyIsReported) { +UTest unknown_conditional_build_key_is_reported{"Manifest/UnknownConditionalBuildKeyIsReported", [] { constexpr auto src = R"( [package] name = "x" @@ -1091,18 +1106,16 @@ cxxflags = ["-DOK"] static_stdlib = false )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->schemaWarnings.size(), 1u); - EXPECT_NE(m->schemaWarnings[0].find("static_stdlib"), std::string::npos) - << m->schemaWarnings[0]; - EXPECT_NE(m->schemaWarnings[0].find("cfg(windows)"), std::string::npos) - << m->schemaWarnings[0]; -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->schemaWarnings.size(), 1u))); + if (auto&& failed = expect(ne(m->schemaWarnings[0].find("static_stdlib"), std::string::npos)); !failed) failed << m->schemaWarnings[0]; + if (auto&& failed = expect(ne(m->schemaWarnings[0].find("cfg(windows)"), std::string::npos)); !failed) failed << m->schemaWarnings[0]; +}}; // Every key the [build] parser actually reads must be absent from the // unknown-key warning — a guard that fires on a supported key is worse than // no guard, because --strict turns it into a build failure. -TEST(Manifest, EverySupportedBuildKeyPassesTheUnknownKeyGuard) { +UTest every_supported_build_key_passes_the_unknown_key_guard{"Manifest/EverySupportedBuildKeyPassesTheUnknownKeyGuard", [] { constexpr auto src = R"( [package] name = "x" @@ -1125,14 +1138,13 @@ profile = "dev" macos_deployment_target = "14.0" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_TRUE(m->schemaWarnings.empty()) - << (m->schemaWarnings.empty() ? "" : m->schemaWarnings[0]); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + if (auto&& failed = expect(m->schemaWarnings.empty()); !failed) failed << (m->schemaWarnings.empty() ? "" : m->schemaWarnings[0]); +}}; // `default-profile` is the canonical spelling of the `profile` alias; both // must clear the guard. -TEST(Manifest, BuildDefaultProfileSpellingPassesTheUnknownKeyGuard) { +UTest build_default_profile_spelling_passes_the_unknown_key_guard{"Manifest/BuildDefaultProfileSpellingPassesTheUnknownKeyGuard", [] { constexpr auto src = R"( [package] name = "x" @@ -1141,18 +1153,17 @@ version = "0.1.0" default-profile = "dev" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_EQ(m->buildConfig.defaultProfile, "dev"); - EXPECT_TRUE(m->schemaWarnings.empty()) - << (m->schemaWarnings.empty() ? "" : m->schemaWarnings[0]); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(eq(m->buildConfig.defaultProfile, "dev")); + if (auto&& failed = expect(m->schemaWarnings.empty()); !failed) failed << (m->schemaWarnings.empty() ? "" : m->schemaWarnings[0]); +}}; // #249: `[build] include_dirs_after` parses into buildConfig.includeDirsAfter // — the -idirafter channel (searched AFTER the toolchain's system dirs), so // an extracted-tarball root containing a file named like a standard header // (ffmpeg's VERSION vs libc++'s on case-insensitive macOS) can't // shadow it while its real headers stay findable. -TEST(Manifest, BuildIncludeDirsAfterToml) { +UTest build_include_dirs_after_toml{"Manifest/BuildIncludeDirsAfterToml", [] { constexpr auto src = R"( [package] name = "x" @@ -1165,19 +1176,19 @@ include_dirs_after = ["*", "vendor/include"] kind = "lib" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->buildConfig.includeDirs.size(), 1u); - EXPECT_EQ(m->buildConfig.includeDirs[0], "include"); - ASSERT_EQ(m->buildConfig.includeDirsAfter.size(), 2u); - EXPECT_EQ(m->buildConfig.includeDirsAfter[0], "*"); - EXPECT_EQ(m->buildConfig.includeDirsAfter[1], "vendor/include"); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->buildConfig.includeDirs.size(), 1u))); + expect(eq(m->buildConfig.includeDirs[0], "include")); + expect(fatal(eq(m->buildConfig.includeDirsAfter.size(), 2u))); + expect(eq(m->buildConfig.includeDirsAfter[0], "*")); + expect(eq(m->buildConfig.includeDirsAfter[1], "vendor/include")); +}}; // Feature System v2 Stage 1: a [features] entry may be a TABLE carrying // package-owned `defines` (and `implies`), while the array shorthand keeps // meaning "implied features". See // .agents/docs/2026-06-29-feature-capability-model-design.md. -TEST(Manifest, FeatureTableFormDefinesAndImplies) { +UTest feature_table_form_defines_and_implies{"Manifest/FeatureTableFormDefinesAndImplies", [] { constexpr auto src = R"( [package] name = "x" @@ -1190,31 +1201,31 @@ base = [] accel = { defines = ["APP_ACCEL=1", "APP_FAST"], implies = ["base"] } )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); // Array shorthand still registers an implied-feature list. - ASSERT_TRUE(m->featuresMap.contains("default")); - ASSERT_EQ(m->featuresMap["default"].size(), 1u); - EXPECT_EQ(m->featuresMap["default"][0], "base"); + expect(fatal(m->featuresMap.contains("default"))); + expect(fatal(eq(m->featuresMap["default"].size(), 1u))); + expect(eq(m->featuresMap["default"][0], "base")); // Table form: `implies` flows into featuresMap, `defines` into featureDefines. - ASSERT_TRUE(m->featuresMap.contains("accel")); - ASSERT_EQ(m->featuresMap["accel"].size(), 1u); - EXPECT_EQ(m->featuresMap["accel"][0], "base"); + expect(fatal(m->featuresMap.contains("accel"))); + expect(fatal(eq(m->featuresMap["accel"].size(), 1u))); + expect(eq(m->featuresMap["accel"][0], "base")); - ASSERT_TRUE(m->buildConfig.featureDefines.contains("accel")); - ASSERT_EQ(m->buildConfig.featureDefines["accel"].size(), 2u); - EXPECT_EQ(m->buildConfig.featureDefines["accel"][0], "APP_ACCEL=1"); - EXPECT_EQ(m->buildConfig.featureDefines["accel"][1], "APP_FAST"); + expect(fatal(m->buildConfig.featureDefines.contains("accel"))); + expect(fatal(eq(m->buildConfig.featureDefines["accel"].size(), 2u))); + expect(eq(m->buildConfig.featureDefines["accel"][0], "APP_ACCEL=1")); + expect(eq(m->buildConfig.featureDefines["accel"][1], "APP_FAST")); // A feature with no defines contributes no featureDefines entry. - EXPECT_FALSE(m->buildConfig.featureDefines.contains("base")); -} + expect(!(m->buildConfig.featureDefines.contains("base"))); +}}; // Feature System v2 Stage 3: capabilities. A feature may `requires`/`provides` // abstract capabilities; the package may `provides` them at package level; and // the root [capabilities] table pins providers. -TEST(Manifest, CapabilitiesProvidesRequiresAndPins) { +UTest capabilities_provides_requires_and_pins{"Manifest/CapabilitiesProvidesRequiresAndPins", [] { constexpr auto src = R"( [package] name = "x" @@ -1230,25 +1241,25 @@ provide_lap = { provides = ["lapack"] } blas = "openblas" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); - ASSERT_EQ(m->provides.size(), 1u); - EXPECT_EQ(m->provides[0], "blas"); + expect(fatal(eq(m->provides.size(), 1u))); + expect(eq(m->provides[0], "blas")); - ASSERT_TRUE(m->featureRequires.contains("use_blas")); - ASSERT_EQ(m->featureRequires["use_blas"].size(), 1u); - EXPECT_EQ(m->featureRequires["use_blas"][0], "blas"); + expect(fatal(m->featureRequires.contains("use_blas"))); + expect(fatal(eq(m->featureRequires["use_blas"].size(), 1u))); + expect(eq(m->featureRequires["use_blas"][0], "blas")); - ASSERT_TRUE(m->featureProvides.contains("provide_lap")); - EXPECT_EQ(m->featureProvides["provide_lap"][0], "lapack"); + expect(fatal(m->featureProvides.contains("provide_lap"))); + expect(eq(m->featureProvides["provide_lap"][0], "lapack")); - ASSERT_TRUE(m->capabilityPins.contains("blas")); - EXPECT_EQ(m->capabilityPins["blas"], "openblas"); -} + expect(fatal(m->capabilityPins.contains("blas"))); + expect(eq(m->capabilityPins["blas"], "openblas")); +}}; // The Lua descriptor surface (index packages) parses package-level `provides` // and feature-scoped `requires`/`provides`/`defines`. -TEST(SynthesizeFromXpkgLua, CapabilitiesAndFeatureDefines) { +UTest capabilities_and_feature_defines{"SynthesizeFromXpkgLua/CapabilitiesAndFeatureDefines", [] { constexpr auto lua = R"( package = { spec = "1", @@ -1266,26 +1277,26 @@ package = { } )"; auto m = mcpp::manifest::synthesize_from_xpkg_lua(lua, "compat.eigen", "1.0.0", mcpp::toolchain::platform::HostPlatform::current()); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); - ASSERT_EQ(m->provides.size(), 1u); - EXPECT_EQ(m->provides[0], "blas"); + expect(fatal(eq(m->provides.size(), 1u))); + expect(eq(m->provides[0], "blas")); - ASSERT_TRUE(m->buildConfig.featureDefines.contains("use_blas")); - EXPECT_EQ(m->buildConfig.featureDefines["use_blas"][0], "EIGEN_USE_BLAS"); - ASSERT_TRUE(m->featureRequires.contains("use_blas")); - EXPECT_EQ(m->featureRequires["use_blas"][0], "blas"); - ASSERT_TRUE(m->featureProvides.contains("eigen_blas")); - EXPECT_EQ(m->featureProvides["eigen_blas"][0], "blas"); + expect(fatal(m->buildConfig.featureDefines.contains("use_blas"))); + expect(eq(m->buildConfig.featureDefines["use_blas"][0], "EIGEN_USE_BLAS")); + expect(fatal(m->featureRequires.contains("use_blas"))); + expect(eq(m->featureRequires["use_blas"][0], "blas")); + expect(fatal(m->featureProvides.contains("eigen_blas"))); + expect(eq(m->featureProvides["eigen_blas"][0], "blas")); // sources still gated as before. - ASSERT_TRUE(m->buildConfig.featureSources.contains("eigen_blas")); -} + expect(fatal(m->buildConfig.featureSources.contains("eigen_blas"))); +}}; // #253: a [features] table-form entry may carry `flags` — per-feature per-glob // compile flags, the same entry grammar as [build].flags. Parsed into // buildConfig.featureFlags keyed by feature; base globFlags stay untouched at // parse time (activation folds them in later, in prepare_build). -TEST(Manifest, FeatureFlagsTomlTableForm) { +UTest feature_flags_toml_table_form{"Manifest/FeatureFlagsTomlTableForm", [] { constexpr auto src = R"( [package] name = "x" @@ -1301,28 +1312,28 @@ simd = { sources = ["src/simd/**"], flags = [ { glob = "src/simd/**/*.neon.cpp", cxxflags = ["-mfpu=neon"] } ] } )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); - ASSERT_TRUE(m->buildConfig.featureFlags.contains("simd")); + expect(fatal(m->buildConfig.featureFlags.contains("simd"))); auto& ff = m->buildConfig.featureFlags["simd"]; - ASSERT_EQ(ff.size(), 2u); // declaration order preserved - EXPECT_EQ(ff[0].glob, "src/simd/**/*.avx2.cpp"); - ASSERT_EQ(ff[0].cxxflags.size(), 1u); - EXPECT_EQ(ff[0].cxxflags[0], "-mavx2"); - ASSERT_EQ(ff[0].defines.size(), 1u); - EXPECT_EQ(ff[0].defines[0], "HAVE_AVX2"); - EXPECT_EQ(ff[1].glob, "src/simd/**/*.neon.cpp"); + expect(fatal(eq(ff.size(), 2u))); // declaration order preserved + expect(eq(ff[0].glob, "src/simd/**/*.avx2.cpp")); + expect(fatal(eq(ff[0].cxxflags.size(), 1u))); + expect(eq(ff[0].cxxflags[0], "-mavx2")); + expect(fatal(eq(ff[0].defines.size(), 1u))); + expect(eq(ff[0].defines[0], "HAVE_AVX2")); + expect(eq(ff[1].glob, "src/simd/**/*.neon.cpp")); // Parse-time base globFlags carry ONLY the [build].flags entry. - ASSERT_EQ(m->buildConfig.globFlags.size(), 1u); - EXPECT_EQ(m->buildConfig.globFlags[0].glob, "src/base/**"); + expect(fatal(eq(m->buildConfig.globFlags.size(), 1u))); + expect(eq(m->buildConfig.globFlags[0].glob, "src/base/**")); // Gated sources still land in featureSources alongside. - ASSERT_TRUE(m->buildConfig.featureSources.contains("simd")); -} + expect(fatal(m->buildConfig.featureSources.contains("simd"))); +}}; // #253: feature `flags` entries share [build].flags' closed grammar — a // missing `glob` and an unknown entry key are hard errors naming the // feature-scoped anchoring key. -TEST(Manifest, FeatureFlagsTomlErrors) { +UTest feature_flags_toml_errors{"Manifest/FeatureFlagsTomlErrors", [] { constexpr auto missingGlob = R"( [package] name = "x" @@ -1333,10 +1344,9 @@ kind = "lib" simd = { flags = [{ cxxflags = ["-mavx2"] }] } )"; auto m1 = mcpp::manifest::parse_string(missingGlob); - ASSERT_FALSE(m1.has_value()); - EXPECT_NE(m1.error().message.find("[features].simd.flags"), std::string::npos) - << m1.error().message; - EXPECT_NE(m1.error().message.find("glob"), std::string::npos); + expect(fatal(!m1.has_value())); + if (auto&& failed = expect(ne(m1.error().message.find("[features].simd.flags"), std::string::npos)); !failed) failed << m1.error().message; + expect(ne(m1.error().message.find("glob"), std::string::npos)); constexpr auto badKey = R"( [package] @@ -1348,15 +1358,14 @@ kind = "lib" simd = { flags = [{ glob = "src/**", ldflags = ["-lfoo"] }] } )"; auto m2 = mcpp::manifest::parse_string(badKey); - ASSERT_FALSE(m2.has_value()); - EXPECT_NE(m2.error().message.find("ldflags"), std::string::npos) - << m2.error().message; -} + expect(fatal(!m2.has_value())); + if (auto&& failed = expect(ne(m2.error().message.find("ldflags"), std::string::npos)); !failed) failed << m2.error().message; +}}; // #253 Lua descriptor surface: `features..flags` parses into // featureFlags (same entry grammar as the build-level `flags` key) and is a // KNOWN subfield — it must not land in xpkgUnknownKeys. -TEST(SynthesizeFromXpkgLua, FeatureFlagsParse) { +UTest feature_flags_parse{"SynthesizeFromXpkgLua/FeatureFlagsParse", [] { constexpr auto lua = R"( package = { spec = "1", @@ -1381,19 +1390,19 @@ package = { } )"; auto m = mcpp::manifest::synthesize_from_xpkg_lua(lua, "compat.opencv", "1.0.0", mcpp::toolchain::platform::HostPlatform::current()); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_TRUE(m->xpkgUnknownKeys.empty()); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(m->xpkgUnknownKeys.empty()); - ASSERT_TRUE(m->buildConfig.featureFlags.contains("dnn")); + expect(fatal(m->buildConfig.featureFlags.contains("dnn"))); auto& ff = m->buildConfig.featureFlags["dnn"]; - ASSERT_EQ(ff.size(), 2u); - EXPECT_EQ(ff[0].glob, "**/3rdparty/mlas/**"); - ASSERT_EQ(ff[0].defines.size(), 2u); - EXPECT_EQ(ff[0].defines[0], "BUILD_MLAS_NO_ONNXRUNTIME=1"); - EXPECT_EQ(ff[1].glob, "**/modules/dnn/**/*.avx2.cpp"); + expect(fatal(eq(ff.size(), 2u))); + expect(eq(ff[0].glob, "**/3rdparty/mlas/**")); + expect(fatal(eq(ff[0].defines.size(), 2u))); + expect(eq(ff[0].defines[0], "BUILD_MLAS_NO_ONNXRUNTIME=1")); + expect(eq(ff[1].glob, "**/modules/dnn/**/*.avx2.cpp")); // Base per-glob flags untouched at parse time. - ASSERT_EQ(m->buildConfig.globFlags.size(), 1u); - EXPECT_EQ(m->buildConfig.globFlags[0].glob, "*/core/*.cpp"); + expect(fatal(eq(m->buildConfig.globFlags.size(), 1u))); + expect(eq(m->buildConfig.globFlags[0].glob, "*/core/*.cpp")); // Unknown entry subkey is a hard error carrying the feature-scoped label. constexpr auto bad = R"( @@ -1408,17 +1417,16 @@ package = { } )"; auto mb = mcpp::manifest::synthesize_from_xpkg_lua(bad, "compat.opencv", "1.0.0", mcpp::toolchain::platform::HostPlatform::current()); - ASSERT_FALSE(mb.has_value()); - EXPECT_NE(mb.error().message.find("features.dnn.flags"), std::string::npos) - << mb.error().message; -} + expect(fatal(!mb.has_value())); + if (auto&& failed = expect(ne(mb.error().message.find("features.dnn.flags"), std::string::npos)); !failed) failed << mb.error().message; +}}; // #253: per-OS descriptor sections participate in `features` via the same // textual-splice additive overlay every mcpp-segment key gets — same-named // features merge per-subkey by APPEND (base body first, OS body second), and a // per-OS section may register an OS-only feature. Locked here because the // common/delta pattern (neutral common payload + per-OS delta) depends on it. -TEST(SynthesizeFromXpkgLua, PerOsFeaturesAdditiveMerge) { +UTest per_os_features_additive_merge{"SynthesizeFromXpkgLua/PerOsFeaturesAdditiveMerge", [] { constexpr auto lua = R"( package = { spec = "1", @@ -1452,34 +1460,34 @@ package = { // linux leg: neutral payload + linux delta, macosx section invisible. auto ml = mcpp::manifest::synthesize_from_xpkg_lua( lua, "compat.opencv", "1.0.0", mcpp::toolchain::platform::TargetPlatform::for_lint_of("linux")); - ASSERT_TRUE(ml.has_value()) << ml.error().format(); - ASSERT_TRUE(ml->buildConfig.featureSources.contains("dnn")); + if (auto&& failed = expect(fatal(ml.has_value())); !failed) failed << ml.error().format(); + expect(fatal(ml->buildConfig.featureSources.contains("dnn"))); { auto& srcs = ml->buildConfig.featureSources["dnn"]; - ASSERT_EQ(srcs.size(), 2u); // append order: neutral first, OS delta after - EXPECT_EQ(srcs[0], "*/modules/dnn/src/*.cpp"); - EXPECT_EQ(srcs[1], "*/3rdparty/mlas/lib/x86_64/*.S"); + expect(fatal(eq(srcs.size(), 2u))); // append order: neutral first, OS delta after + expect(eq(srcs[0], "*/modules/dnn/src/*.cpp")); + expect(eq(srcs[1], "*/3rdparty/mlas/lib/x86_64/*.S")); } - ASSERT_TRUE(ml->buildConfig.featureFlags.contains("dnn")); - EXPECT_EQ(ml->buildConfig.featureFlags["dnn"][0].glob, "**/3rdparty/mlas/**"); + expect(fatal(ml->buildConfig.featureFlags.contains("dnn"))); + expect(eq(ml->buildConfig.featureFlags["dnn"][0].glob, "**/3rdparty/mlas/**")); // OS-only feature registered (requestable) on this leg only. - EXPECT_TRUE(ml->featuresMap.contains("vaapi")); - ASSERT_TRUE(ml->buildConfig.featureDefines.contains("vaapi")); - EXPECT_EQ(ml->buildConfig.featureDefines["vaapi"][0], "HAVE_VAAPI"); + expect(ml->featuresMap.contains("vaapi")); + expect(fatal(ml->buildConfig.featureDefines.contains("vaapi"))); + expect(eq(ml->buildConfig.featureDefines["vaapi"][0], "HAVE_VAAPI")); // macosx leg: NEON delta instead, no mlas flags, no vaapi. auto mm = mcpp::manifest::synthesize_from_xpkg_lua( lua, "compat.opencv", "1.0.0", mcpp::toolchain::platform::TargetPlatform::for_lint_of("macosx")); - ASSERT_TRUE(mm.has_value()) << mm.error().format(); + if (auto&& failed = expect(fatal(mm.has_value())); !failed) failed << mm.error().format(); { auto& srcs = mm->buildConfig.featureSources["dnn"]; - ASSERT_EQ(srcs.size(), 2u); - EXPECT_EQ(srcs[0], "*/modules/dnn/src/*.cpp"); - EXPECT_EQ(srcs[1], "*/modules/dnn/neon/*.cpp"); + expect(fatal(eq(srcs.size(), 2u))); + expect(eq(srcs[0], "*/modules/dnn/src/*.cpp")); + expect(eq(srcs[1], "*/modules/dnn/neon/*.cpp")); } - EXPECT_FALSE(mm->buildConfig.featureFlags.contains("dnn")); - EXPECT_FALSE(mm->featuresMap.contains("vaapi")); -} + expect(!(mm->buildConfig.featureFlags.contains("dnn"))); + expect(!(mm->featuresMap.contains("vaapi"))); +}}; // Feature System v2 Stage 2a: optional deps activated by a feature. // TOML surface uses a dedicated [feature-deps.] section. @@ -1488,7 +1496,7 @@ package = { // form was in the documented `[feature-deps]` example, and because a fixture // only has to PARSE — nothing here ever asked the index for that package. It // reached a published package (`std-freestanding` 0.3.0) before it was measured. -TEST(Manifest, FeatureDepsTomlSection) { +UTest feature_deps_toml_section{"Manifest/FeatureDepsTomlSection", [] { constexpr auto src = R"( [package] name = "x" @@ -1502,15 +1510,15 @@ backend = [] zlib = "1.3" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_TRUE(m->featureDeps.contains("backend")); - EXPECT_EQ(m->featureDeps["backend"].size(), 1u); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(m->featureDeps.contains("backend"))); + expect(eq(m->featureDeps["backend"].size(), 1u)); // The optional dep is NOT in the always-on dependency set. - EXPECT_FALSE(m->dependencies.contains("zlib")); -} + expect(!(m->dependencies.contains("zlib"))); +}}; // Lua descriptor surface: a feature carries `deps` (nested table) + `implies`. -TEST(SynthesizeFromXpkgLua, FeatureDepsAndImplies) { +UTest feature_deps_and_implies{"SynthesizeFromXpkgLua/FeatureDepsAndImplies", [] { constexpr auto lua = R"( package = { spec = "1", @@ -1527,22 +1535,22 @@ package = { } )"; auto m = mcpp::manifest::synthesize_from_xpkg_lua(lua, "compat.eigen", "1.0.0", mcpp::toolchain::platform::HostPlatform::current()); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); // implies recorded in featuresMap - ASSERT_TRUE(m->featuresMap.contains("backend-openblas")); - ASSERT_EQ(m->featuresMap["backend-openblas"].size(), 1u); - EXPECT_EQ(m->featuresMap["backend-openblas"][0], "use_blas"); + expect(fatal(m->featuresMap.contains("backend-openblas"))); + expect(fatal(eq(m->featuresMap["backend-openblas"].size(), 1u))); + expect(eq(m->featuresMap["backend-openblas"][0], "use_blas")); // deps recorded in featureDeps, NOT in the always-on dependency set - ASSERT_TRUE(m->featureDeps.contains("backend-openblas")); - EXPECT_EQ(m->featureDeps["backend-openblas"].size(), 1u); - EXPECT_TRUE(m->dependencies.empty()); -} + expect(fatal(m->featureDeps.contains("backend-openblas"))); + expect(eq(m->featureDeps["backend-openblas"].size(), 1u)); + expect(m->dependencies.empty()); +}}; // Feature System v2 #243: dep/feat forwarding (Cargo parity). A `[features]` // implied-feature token containing '/' means "when this feature is active, // request from dependency ". It is split out of the // implies list into featureForwards; plain names stay implies. -TEST(Manifest, FeatureForwardTomlArrayAndImplies) { +UTest feature_forward_toml_array_and_implies{"Manifest/FeatureForwardTomlArrayAndImplies", [] { constexpr auto src = R"( [package] name = "x" @@ -1556,23 +1564,23 @@ dnn = ["dnn-local", "compat.opencv/dnn"] compat.opencv = { version = "0.0.3", default-features = false } )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); // The plain name stays an implied feature. - ASSERT_TRUE(m->featuresMap.contains("dnn")); - ASSERT_EQ(m->featuresMap["dnn"].size(), 1u); - EXPECT_EQ(m->featuresMap["dnn"][0], "dnn-local"); + expect(fatal(m->featuresMap.contains("dnn"))); + expect(fatal(eq(m->featuresMap["dnn"].size(), 1u))); + expect(eq(m->featuresMap["dnn"][0], "dnn-local")); // The `dep/feat` token becomes a forward (depKey verbatim = stableMapKey). - ASSERT_TRUE(m->featureForwards.contains("dnn")); - ASSERT_EQ(m->featureForwards["dnn"].size(), 1u); - EXPECT_EQ(m->featureForwards["dnn"][0].first, "compat.opencv"); - EXPECT_EQ(m->featureForwards["dnn"][0].second, "dnn"); -} + expect(fatal(m->featureForwards.contains("dnn"))); + expect(fatal(eq(m->featureForwards["dnn"].size(), 1u))); + expect(eq(m->featureForwards["dnn"][0].first, "compat.opencv")); + expect(eq(m->featureForwards["dnn"][0].second, "dnn")); +}}; // The table form accepts forwards both mixed into `implies` and via the // dedicated self-documenting `forward` key; both feed featureForwards. -TEST(Manifest, FeatureForwardTableForms) { +UTest feature_forward_table_forms{"Manifest/FeatureForwardTableForms", [] { constexpr auto src = R"( [package] name = "x" @@ -1586,28 +1594,28 @@ b = { forward = ["dep.two/y", "dep.two/z"] } base = [] )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); // `implies` keeps the plain name, forwards the slashed token. - ASSERT_EQ(m->featuresMap["a"].size(), 1u); - EXPECT_EQ(m->featuresMap["a"][0], "base"); - ASSERT_TRUE(m->featureForwards.contains("a")); - ASSERT_EQ(m->featureForwards["a"].size(), 1u); - EXPECT_EQ(m->featureForwards["a"][0].first, "dep.one"); - EXPECT_EQ(m->featureForwards["a"][0].second, "x"); + expect(fatal(eq(m->featuresMap["a"].size(), 1u))); + expect(eq(m->featuresMap["a"][0], "base")); + expect(fatal(m->featureForwards.contains("a"))); + expect(fatal(eq(m->featureForwards["a"].size(), 1u))); + expect(eq(m->featureForwards["a"][0].first, "dep.one")); + expect(eq(m->featureForwards["a"][0].second, "x")); // Dedicated `forward` key: all tokens are forwards; feature has no implies. - EXPECT_TRUE(m->featuresMap["b"].empty()); - ASSERT_TRUE(m->featureForwards.contains("b")); - ASSERT_EQ(m->featureForwards["b"].size(), 2u); - EXPECT_EQ(m->featureForwards["b"][0].first, "dep.two"); - EXPECT_EQ(m->featureForwards["b"][0].second, "y"); - EXPECT_EQ(m->featureForwards["b"][1].second, "z"); -} + expect(m->featuresMap["b"].empty()); + expect(fatal(m->featureForwards.contains("b"))); + expect(fatal(eq(m->featureForwards["b"].size(), 2u))); + expect(eq(m->featureForwards["b"][0].first, "dep.two")); + expect(eq(m->featureForwards["b"][0].second, "y")); + expect(eq(m->featureForwards["b"][1].second, "z")); +}}; // The Lua descriptor surface splits `dep/feat` tokens the same way (one shared // split point, two grammars). -TEST(SynthesizeFromXpkgLua, FeatureForwardParse) { +UTest feature_forward_parse{"SynthesizeFromXpkgLua/FeatureForwardParse", [] { constexpr auto lua = R"( package = { spec = "1", @@ -1623,29 +1631,29 @@ package = { } )"; auto m = mcpp::manifest::synthesize_from_xpkg_lua(lua, "opencv", "0.0.3", mcpp::toolchain::platform::HostPlatform::current()); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); // The forward token left featuresMap for featureForwards. - EXPECT_TRUE(m->featuresMap["dnn"].empty()); - ASSERT_TRUE(m->featureForwards.contains("dnn")); - ASSERT_EQ(m->featureForwards["dnn"].size(), 1u); - EXPECT_EQ(m->featureForwards["dnn"][0].first, "compat.opencv"); - EXPECT_EQ(m->featureForwards["dnn"][0].second, "dnn"); + expect(m->featuresMap["dnn"].empty()); + expect(fatal(m->featureForwards.contains("dnn"))); + expect(fatal(eq(m->featureForwards["dnn"].size(), 1u))); + expect(eq(m->featureForwards["dnn"][0].first, "compat.opencv")); + expect(eq(m->featureForwards["dnn"][0].second, "dnn")); // sources still gated normally (untouched by the split). - ASSERT_TRUE(m->buildConfig.featureSources.contains("dnn")); -} + expect(fatal(m->buildConfig.featureSources.contains("dnn"))); +}}; // mcpp#237: an unknown mcpp-segment key is collected (so the build path can // surface it) and the did-you-mean helper maps well-known confusables + // edit-distance typos back to the closed key vocabulary. -TEST(XpkgUnknownKeys, CollectedAndDidYouMean) { +UTest collected_and_did_you_mean{"XpkgUnknownKeys/CollectedAndDidYouMean", [] { // `dependencies` is the canonical wrong spelling of `deps` (issue #237). - EXPECT_EQ(mcpp::manifest::closest_known_xpkg_key("dependencies"), "deps"); - EXPECT_EQ(mcpp::manifest::closest_known_xpkg_key("dependency"), "deps"); + expect(eq(mcpp::manifest::closest_known_xpkg_key("dependencies"), "deps")); + expect(eq(mcpp::manifest::closest_known_xpkg_key("dependency"), "deps")); // single-typo of a real key resolves via edit distance. - EXPECT_EQ(mcpp::manifest::closest_known_xpkg_key("cxxflag"), "cxxflags"); - EXPECT_EQ(mcpp::manifest::closest_known_xpkg_key("feature"), "features"); + expect(eq(mcpp::manifest::closest_known_xpkg_key("cxxflag"), "cxxflags")); + expect(eq(mcpp::manifest::closest_known_xpkg_key("feature"), "features")); // a genuinely unrelated key has no suggestion. - EXPECT_EQ(mcpp::manifest::closest_known_xpkg_key("wibble"), ""); + expect(eq(mcpp::manifest::closest_known_xpkg_key("wibble"), "")); // The build-path synthesizer records the unknown key rather than dropping // it silently. @@ -1661,16 +1669,16 @@ package = { } )"; auto m = mcpp::manifest::synthesize_from_xpkg_lua(lua, "x", "1.0.0", mcpp::toolchain::platform::HostPlatform::current()); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->xpkgUnknownKeys.size(), 1u); - EXPECT_EQ(m->xpkgUnknownKeys[0], "dependencies"); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->xpkgUnknownKeys.size(), 1u))); + expect(eq(m->xpkgUnknownKeys[0], "dependencies")); +}}; // A feature's UNKNOWN sub-key (e.g. an unsupported `include_dirs`) is recorded // as `features..` instead of being silently swallowed — the // adoption-site warning (warn_unknown_xpkg_keys) then names it. Known sub-keys // keep parsing around it. -TEST(XpkgUnknownKeys, FeatureSubKeyRecorded) { +UTest feature_sub_key_recorded{"XpkgUnknownKeys/FeatureSubKeyRecorded", [] { constexpr auto lua = R"( package = { name = "x", @@ -1688,15 +1696,15 @@ package = { } )"; auto m = mcpp::manifest::synthesize_from_xpkg_lua(lua, "x", "1.0.0", mcpp::toolchain::platform::HostPlatform::current()); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->xpkgUnknownKeys.size(), 1u); - EXPECT_EQ(m->xpkgUnknownKeys[0], "features.opt.include_dirs"); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->xpkgUnknownKeys.size(), 1u))); + expect(eq(m->xpkgUnknownKeys[0], "features.opt.include_dirs")); // the known sub-key after the unknown one still parsed. - ASSERT_TRUE(m->buildConfig.featureDefines.contains("opt")); - EXPECT_EQ(m->buildConfig.featureDefines.at("opt").size(), 1u); -} + expect(fatal(m->buildConfig.featureDefines.contains("opt"))); + expect(eq(m->buildConfig.featureDefines.at("opt").size(), 1u)); +}}; -TEST(Manifest, BuildMacosDeploymentTarget) { +UTest build_macos_deployment_target{"Manifest/BuildMacosDeploymentTarget", [] { constexpr auto src = R"( [package] name = "x" @@ -1707,11 +1715,11 @@ macos_deployment_target = "11.0" kind = "lib" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_EQ(m->buildConfig.macosDeploymentTarget, "11.0"); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(eq(m->buildConfig.macosDeploymentTarget, "11.0")); +}}; -TEST(Manifest, BuildMacosDeploymentTargetDefaultsEmpty) { +UTest build_macos_deployment_target_defaults_empty{"Manifest/BuildMacosDeploymentTargetDefaultsEmpty", [] { constexpr auto src = R"( [package] name = "x" @@ -1720,11 +1728,11 @@ version = "0.1.0" kind = "lib" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_TRUE(m->buildConfig.macosDeploymentTarget.empty()); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(m->buildConfig.macosDeploymentTarget.empty()); +}}; -TEST(Manifest, RuntimeConfig) { +UTest runtime_config{"Manifest/RuntimeConfig", [] { constexpr auto src = R"( [package] name = "x" @@ -1735,19 +1743,19 @@ dlopen_libs = ["libGLX.so.0", "libGL.so.1"] capabilities = ["x11.display", "opengl.glx.driver"] )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->runtimeConfig.libraryDirs.size(), 2u); - EXPECT_EQ(m->runtimeConfig.libraryDirs[0], "runtime/lib"); - EXPECT_EQ(m->runtimeConfig.libraryDirs[1], "plugins"); - ASSERT_EQ(m->runtimeConfig.dlopenLibs.size(), 2u); - EXPECT_EQ(m->runtimeConfig.dlopenLibs[0], "libGLX.so.0"); - EXPECT_EQ(m->runtimeConfig.dlopenLibs[1], "libGL.so.1"); - ASSERT_EQ(m->runtimeConfig.capabilities.size(), 2u); - EXPECT_EQ(m->runtimeConfig.capabilities[0], "x11.display"); - EXPECT_EQ(m->runtimeConfig.capabilities[1], "opengl.glx.driver"); -} - -TEST(SynthesizeFromXpkgLua, CflagsCxxflagsLdflagsAndCStandard) { + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->runtimeConfig.libraryDirs.size(), 2u))); + expect(eq(m->runtimeConfig.libraryDirs[0], "runtime/lib")); + expect(eq(m->runtimeConfig.libraryDirs[1], "plugins")); + expect(fatal(eq(m->runtimeConfig.dlopenLibs.size(), 2u))); + expect(eq(m->runtimeConfig.dlopenLibs[0], "libGLX.so.0")); + expect(eq(m->runtimeConfig.dlopenLibs[1], "libGL.so.1")); + expect(fatal(eq(m->runtimeConfig.capabilities.size(), 2u))); + expect(eq(m->runtimeConfig.capabilities[0], "x11.display")); + expect(eq(m->runtimeConfig.capabilities[1], "opengl.glx.driver")); +}}; + +UTest cflags_cxxflags_ldflags_and_c_standard{"SynthesizeFromXpkgLua/CflagsCxxflagsLdflagsAndCStandard", [] { constexpr auto src = R"( package = { spec = "1", @@ -1764,20 +1772,20 @@ package = { } )"; auto m = mcpp::manifest::synthesize_from_xpkg_lua(src, "tinyc", "1.0.0", mcpp::toolchain::platform::HostPlatform::current()); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->buildConfig.cflags.size(), 2u); - EXPECT_EQ(m->buildConfig.cflags[0], "-Wall"); - EXPECT_EQ(m->buildConfig.cflags[1], "-Dunused"); - ASSERT_EQ(m->buildConfig.cxxflags.size(), 1u); - EXPECT_EQ(m->buildConfig.cxxflags[0], "-Wextra"); - ASSERT_EQ(m->buildConfig.ldflags.size(), 1u); - EXPECT_EQ(m->buildConfig.ldflags[0], "-ltinyc"); - EXPECT_EQ(m->buildConfig.cStandard, "c11"); - ASSERT_EQ(m->modules.sources.size(), 1u); - EXPECT_EQ(m->modules.sources[0], "*/src/*.c"); -} - -TEST(SynthesizeFromXpkgLua, SharedTargetSoname) { + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->buildConfig.cflags.size(), 2u))); + expect(eq(m->buildConfig.cflags[0], "-Wall")); + expect(eq(m->buildConfig.cflags[1], "-Dunused")); + expect(fatal(eq(m->buildConfig.cxxflags.size(), 1u))); + expect(eq(m->buildConfig.cxxflags[0], "-Wextra")); + expect(fatal(eq(m->buildConfig.ldflags.size(), 1u))); + expect(eq(m->buildConfig.ldflags[0], "-ltinyc")); + expect(eq(m->buildConfig.cStandard, "c11")); + expect(fatal(eq(m->modules.sources.size(), 1u))); + expect(eq(m->modules.sources[0], "*/src/*.c")); +}}; + +UTest synthesize_from_xpkg_lua_shared_target_soname{"SynthesizeFromXpkgLua/SharedTargetSoname", [] { constexpr auto src = R"( package = { spec = "1", @@ -1790,13 +1798,13 @@ package = { } )"; auto m = mcpp::manifest::synthesize_from_xpkg_lua(src, "tinyshared", "1.0.0", mcpp::toolchain::platform::HostPlatform::current()); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->targets.size(), 1u); - EXPECT_EQ(m->targets[0].kind, mcpp::manifest::Target::SharedLibrary); - EXPECT_EQ(m->targets[0].soname, "libtinyshared.so.1"); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->targets.size(), 1u))); + expect(eq(m->targets[0].kind, mcpp::manifest::Target::SharedLibrary)); + expect(eq(m->targets[0].soname, "libtinyshared.so.1")); +}}; -TEST(SynthesizeFromXpkgLua, FeatureGatedSources) { +UTest feature_gated_sources{"SynthesizeFromXpkgLua/FeatureGatedSources", [] { // gtest-style: gtest_main.cc listed in base `sources` (old-mcpp compat) AND // under the `main` feature → featureSources records it; the feature is // registered in featuresMap. prepare_build later gates it (off by default). @@ -1815,17 +1823,17 @@ package = { } )"; auto m = mcpp::manifest::synthesize_from_xpkg_lua(src, "gtestlike", "1.0.0", mcpp::toolchain::platform::HostPlatform::current()); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); // base sources keep both (old mcpp ignores `features` → no regression) - ASSERT_EQ(m->buildConfig.sources.size(), 2u); + expect(fatal(eq(m->buildConfig.sources.size(), 2u))); // the `main` feature is registered + carries its gated source - ASSERT_TRUE(m->featuresMap.contains("main")); - ASSERT_TRUE(m->buildConfig.featureSources.contains("main")); - ASSERT_EQ(m->buildConfig.featureSources.at("main").size(), 1u); - EXPECT_EQ(m->buildConfig.featureSources.at("main")[0], "*/src/main.cc"); -} + expect(fatal(m->featuresMap.contains("main"))); + expect(fatal(m->buildConfig.featureSources.contains("main"))); + expect(fatal(eq(m->buildConfig.featureSources.at("main").size(), 1u))); + expect(eq(m->buildConfig.featureSources.at("main")[0], "*/src/main.cc")); +}}; -TEST(SynthesizeFromXpkgLua, RuntimeConfig) { +UTest synthesize_from_xpkg_lua_runtime_config{"SynthesizeFromXpkgLua/RuntimeConfig", [] { constexpr auto src = R"( package = { spec = "1", @@ -1843,18 +1851,18 @@ package = { } )"; auto m = mcpp::manifest::synthesize_from_xpkg_lua(src, "glfw", "3.4", mcpp::toolchain::platform::HostPlatform::current()); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->runtimeConfig.libraryDirs.size(), 1u); - EXPECT_EQ(m->runtimeConfig.libraryDirs[0], "mcpp_generated/runtime/lib"); - ASSERT_EQ(m->runtimeConfig.dlopenLibs.size(), 2u); - EXPECT_EQ(m->runtimeConfig.dlopenLibs[0], "libGLX.so.0"); - EXPECT_EQ(m->runtimeConfig.dlopenLibs[1], "libGL.so.1"); - ASSERT_EQ(m->runtimeConfig.capabilities.size(), 2u); - EXPECT_EQ(m->runtimeConfig.capabilities[0], "x11.display"); - EXPECT_EQ(m->runtimeConfig.capabilities[1], "opengl.glx.driver"); -} - -TEST(SynthesizeFromXpkgLua, AppliesCurrentPlatformMcppOverlay) { + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->runtimeConfig.libraryDirs.size(), 1u))); + expect(eq(m->runtimeConfig.libraryDirs[0], "mcpp_generated/runtime/lib")); + expect(fatal(eq(m->runtimeConfig.dlopenLibs.size(), 2u))); + expect(eq(m->runtimeConfig.dlopenLibs[0], "libGLX.so.0")); + expect(eq(m->runtimeConfig.dlopenLibs[1], "libGL.so.1")); + expect(fatal(eq(m->runtimeConfig.capabilities.size(), 2u))); + expect(eq(m->runtimeConfig.capabilities[0], "x11.display")); + expect(eq(m->runtimeConfig.capabilities[1], "opengl.glx.driver")); +}}; + +UTest applies_current_platform_mcpp_overlay{"SynthesizeFromXpkgLua/AppliesCurrentPlatformMcppOverlay", [] { constexpr auto src = R"( package = { spec = "1", @@ -1888,7 +1896,7 @@ package = { } )"; auto m = mcpp::manifest::synthesize_from_xpkg_lua(src, "tinyc", "1.0.0", mcpp::toolchain::platform::HostPlatform::current()); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); std::string expectedSource = "*/src/linux.c"; std::string expectedInclude = "*/src/linux"; @@ -1906,31 +1914,31 @@ package = { expectedDep = "compat.win32"; } - ASSERT_EQ(m->modules.sources.size(), 2u); - EXPECT_EQ(m->modules.sources[0], "*/src/common.c"); - EXPECT_EQ(m->modules.sources[1], expectedSource); - ASSERT_EQ(m->buildConfig.includeDirs.size(), 2u); - EXPECT_EQ(m->buildConfig.includeDirs[0], "*/include"); - EXPECT_EQ(m->buildConfig.includeDirs[1], expectedInclude); - ASSERT_EQ(m->buildConfig.cflags.size(), 2u); - EXPECT_EQ(m->buildConfig.cflags[0], "-DCOMMON=1"); - EXPECT_EQ(m->buildConfig.cflags[1], expectedCflag); - EXPECT_EQ(m->dependencies.count("compat.base"), 1u); - EXPECT_EQ(m->dependencies.count(expectedDep), 1u); + expect(fatal(eq(m->modules.sources.size(), 2u))); + expect(eq(m->modules.sources[0], "*/src/common.c")); + expect(eq(m->modules.sources[1], expectedSource)); + expect(fatal(eq(m->buildConfig.includeDirs.size(), 2u))); + expect(eq(m->buildConfig.includeDirs[0], "*/include")); + expect(eq(m->buildConfig.includeDirs[1], expectedInclude)); + expect(fatal(eq(m->buildConfig.cflags.size(), 2u))); + expect(eq(m->buildConfig.cflags[0], "-DCOMMON=1")); + expect(eq(m->buildConfig.cflags[1], expectedCflag)); + expect(eq(m->dependencies.count("compat.base"), 1u)); + expect(eq(m->dependencies.count(expectedDep), 1u)); // osOverride seam (`mcpp xpkg parse --all-os`): a foreign OS section is // spliced on request, independent of the running host — the build path // (empty override) above stays host-selected. Reuses this fixture. auto win = mcpp::manifest::synthesize_from_xpkg_lua(src, "tinyc", "1.0.0", mcpp::toolchain::platform::TargetPlatform::for_lint_of("windows")); - ASSERT_TRUE(win.has_value()) << win.error().format(); - ASSERT_EQ(win->modules.sources.size(), 2u); - EXPECT_EQ(win->modules.sources[1], "*/src/win32.c"); - ASSERT_EQ(win->buildConfig.cflags.size(), 2u); - EXPECT_EQ(win->buildConfig.cflags[1], "-DWINDOWS=1"); - EXPECT_EQ(win->dependencies.count("compat.win32"), 1u); -} + if (auto&& failed = expect(fatal(win.has_value())); !failed) failed << win.error().format(); + expect(fatal(eq(win->modules.sources.size(), 2u))); + expect(eq(win->modules.sources[1], "*/src/win32.c")); + expect(fatal(eq(win->buildConfig.cflags.size(), 2u))); + expect(eq(win->buildConfig.cflags[1], "-DWINDOWS=1")); + expect(eq(win->dependencies.count("compat.win32"), 1u)); +}}; -TEST(SynthesizeFromXpkgLua, GeneratedFiles) { +UTest synthesize_from_xpkg_lua_generated_files{"SynthesizeFromXpkgLua/GeneratedFiles", [] { constexpr auto src = R"( package = { spec = "1", @@ -1947,17 +1955,17 @@ package = { } )"; auto m = mcpp::manifest::synthesize_from_xpkg_lua(src, "tinyc", "1.0.0", mcpp::toolchain::platform::HostPlatform::current()); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->buildConfig.generatedFiles.size(), 1u); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->buildConfig.generatedFiles.size(), 1u))); auto it = m->buildConfig.generatedFiles.find("mcpp_generated/include/config.h"); - ASSERT_NE(it, m->buildConfig.generatedFiles.end()); - EXPECT_EQ(it->second, "#pragma once\n#define TINYC_OK 1\n"); -} + expect(fatal(ne(it, m->buildConfig.generatedFiles.end()))); + expect(eq(it->second, "#pragma once\n#define TINYC_OK 1\n")); +}}; // #249: the xpkg descriptor body accepts `include_dirs_after` alongside // `include_dirs` and keeps the two channels separate (after-dirs are never // upgraded to -I). -TEST(SynthesizeFromXpkgLua, IncludeDirsAfter) { +UTest synthesize_from_xpkg_lua_include_dirs_after{"SynthesizeFromXpkgLua/IncludeDirsAfter", [] { constexpr auto src = R"( package = { spec = "1", @@ -1972,15 +1980,15 @@ package = { } )"; auto m = mcpp::manifest::synthesize_from_xpkg_lua(src, "tinyc", "1.0.0", mcpp::toolchain::platform::HostPlatform::current()); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->buildConfig.includeDirs.size(), 1u); - EXPECT_EQ(m->buildConfig.includeDirs[0], "*/include"); - ASSERT_EQ(m->buildConfig.includeDirsAfter.size(), 1u); - EXPECT_EQ(m->buildConfig.includeDirsAfter[0], "*"); - EXPECT_TRUE(m->xpkgUnknownKeys.empty()); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->buildConfig.includeDirs.size(), 1u))); + expect(eq(m->buildConfig.includeDirs[0], "*/include")); + expect(fatal(eq(m->buildConfig.includeDirsAfter.size(), 1u))); + expect(eq(m->buildConfig.includeDirsAfter[0], "*")); + expect(m->xpkgUnknownKeys.empty()); +}}; -TEST(Manifest, DependenciesFlatDefaultNamespace) { +UTest dependencies_flat_default_namespace{"Manifest/DependenciesFlatDefaultNamespace", [] { constexpr auto src = R"( [package] name = "x" @@ -1990,19 +1998,19 @@ gtest = "1.15.2" foo = { path = "../foo" } )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->dependencies.size(), 2u); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->dependencies.size(), 2u))); auto& g = m->dependencies.at("gtest"); - EXPECT_EQ(g.namespace_, "mcpplibs"); - EXPECT_EQ(g.shortName, "gtest"); - EXPECT_EQ(g.version, "1.15.2"); + expect(eq(g.namespace_, "mcpplibs")); + expect(eq(g.shortName, "gtest")); + expect(eq(g.version, "1.15.2")); auto& f = m->dependencies.at("foo"); - EXPECT_EQ(f.namespace_, "mcpplibs"); - EXPECT_EQ(f.shortName, "foo"); - EXPECT_EQ(f.path, "../foo"); -} + expect(eq(f.namespace_, "mcpplibs")); + expect(eq(f.shortName, "foo")); + expect(eq(f.path, "../foo")); +}}; -TEST(Manifest, DependenciesNamespacedSubtable) { +UTest dependencies_namespaced_subtable{"Manifest/DependenciesNamespacedSubtable", [] { constexpr auto src = R"( [package] name = "x" @@ -2016,27 +2024,27 @@ templates = { version = "0.0.1" } gtest = "1.15.2" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->dependencies.size(), 3u); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->dependencies.size(), 3u))); auto& cmdline = m->dependencies.at("mcpplibs.cmdline"); - EXPECT_EQ(cmdline.namespace_, "mcpplibs"); - EXPECT_EQ(cmdline.shortName, "cmdline"); - EXPECT_EQ(cmdline.version, "0.0.2"); - EXPECT_FALSE(cmdline.legacyDottedKey); + expect(eq(cmdline.namespace_, "mcpplibs")); + expect(eq(cmdline.shortName, "cmdline")); + expect(eq(cmdline.version, "0.0.2")); + expect(!cmdline.legacyDottedKey); auto& tmpl = m->dependencies.at("mcpplibs.templates"); - EXPECT_EQ(tmpl.namespace_, "mcpplibs"); - EXPECT_EQ(tmpl.shortName, "templates"); - EXPECT_EQ(tmpl.version, "0.0.1"); + expect(eq(tmpl.namespace_, "mcpplibs")); + expect(eq(tmpl.shortName, "templates")); + expect(eq(tmpl.version, "0.0.1")); auto& gtest = m->dependencies.at("gtest"); - EXPECT_EQ(gtest.namespace_, "mcpplibs"); - EXPECT_EQ(gtest.shortName, "gtest"); - EXPECT_EQ(gtest.version, "1.15.2"); -} + expect(eq(gtest.namespace_, "mcpplibs")); + expect(eq(gtest.shortName, "gtest")); + expect(eq(gtest.version, "1.15.2")); +}}; -TEST(Manifest, DependenciesLegacyDottedKeyStillParsed) { +UTest dependencies_legacy_dotted_key_still_parsed{"Manifest/DependenciesLegacyDottedKeyStillParsed", [] { // Pre-namespace-aware mcpp.toml: quoted dotted key. constexpr auto src = R"( [package] @@ -2047,16 +2055,16 @@ version = "0.1.0" "mcpplibs.cmdline" = "0.0.2" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->dependencies.size(), 1u); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->dependencies.size(), 1u))); auto& s = m->dependencies.at("mcpplibs.cmdline"); - EXPECT_EQ(s.namespace_, "mcpplibs"); - EXPECT_EQ(s.shortName, "cmdline"); - EXPECT_EQ(s.version, "0.0.2"); - EXPECT_TRUE(s.legacyDottedKey); -} + expect(eq(s.namespace_, "mcpplibs")); + expect(eq(s.shortName, "cmdline")); + expect(eq(s.version, "0.0.2")); + expect(bool(s.legacyDottedKey)); +}}; -TEST(Manifest, DependenciesDottedSelectorIsOneExactIdentity) { +UTest dependencies_dotted_selector_is_one_exact_identity{"Manifest/DependenciesDottedSelectorIsOneExactIdentity", [] { constexpr auto src = R"( [package] name = "x" @@ -2066,21 +2074,21 @@ version = "0.1.0" capi.lua = "0.0.3" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->dependencies.size(), 1u); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->dependencies.size(), 1u))); auto& s = m->dependencies.at("capi.lua"); - EXPECT_EQ(s.namespace_, "capi"); - EXPECT_EQ(s.shortName, "lua"); - EXPECT_EQ(s.version, "0.0.3"); - EXPECT_FALSE(s.legacyDottedKey); - EXPECT_TRUE(s.legacyCandidateSearch); - ASSERT_EQ(s.candidates.size(), 1u); - EXPECT_EQ(s.candidates[0].namespace_, "capi"); - EXPECT_EQ(s.candidates[0].shortName, "lua"); -} - -TEST(Manifest, DependencySelectorsRejectUnsafeOrAmbiguousSegments) { + expect(eq(s.namespace_, "capi")); + expect(eq(s.shortName, "lua")); + expect(eq(s.version, "0.0.3")); + expect(!s.legacyDottedKey); + expect(bool(s.legacyCandidateSearch)); + expect(fatal(eq(s.candidates.size(), 1u))); + expect(eq(s.candidates[0].namespace_, "capi")); + expect(eq(s.candidates[0].shortName, "lua")); +}}; + +UTest dependency_selectors_reject_unsafe_or_ambiguous_segments{"Manifest/DependencySelectorsRejectUnsafeOrAmbiguousSegments", [] { const std::vector invalid{ "acme/widget", "acme widget", "acme[widget", "acme=widget", "acme#widget", "acme..widget", @@ -2094,13 +2102,12 @@ version = "0.1.0" "{}" = "1.0.0" )", selector); auto m = mcpp::manifest::parse_string(src); - EXPECT_FALSE(m.has_value()) << selector; - if (!m) EXPECT_NE(m.error().message.find("invalid package selector"), - std::string::npos) << m.error().format(); + expect(!m.has_value()) << selector; + if (!m) expect(ne(m.error().message.find("invalid package selector"), std::string::npos)) << m.error().format(); } -} +}}; -TEST(Manifest, DependenciesNamespacedSubtableNestedDottedKeyIsCanonical) { +UTest dependencies_namespaced_subtable_nested_dotted_key_is_canonical{"Manifest/DependenciesNamespacedSubtableNestedDottedKeyIsCanonical", [] { constexpr auto src = R"( [package] name = "x" @@ -2110,21 +2117,21 @@ version = "0.1.0" capi.lua = "0.0.3" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->dependencies.size(), 1u); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->dependencies.size(), 1u))); auto& s = m->dependencies.at("mcpplibs.capi.lua"); - EXPECT_EQ(s.namespace_, "mcpplibs.capi"); - EXPECT_EQ(s.shortName, "lua"); - EXPECT_EQ(s.version, "0.0.3"); - EXPECT_FALSE(s.legacyDottedKey); - EXPECT_FALSE(s.legacyCandidateSearch); - ASSERT_EQ(s.candidates.size(), 1u); - EXPECT_EQ(s.candidates[0].namespace_, "mcpplibs.capi"); - EXPECT_EQ(s.candidates[0].shortName, "lua"); -} - -TEST(Manifest, DependenciesInlineSpecCoexistsWithSubtable) { + expect(eq(s.namespace_, "mcpplibs.capi")); + expect(eq(s.shortName, "lua")); + expect(eq(s.version, "0.0.3")); + expect(!s.legacyDottedKey); + expect(!s.legacyCandidateSearch); + expect(fatal(eq(s.candidates.size(), 1u))); + expect(eq(s.candidates[0].namespace_, "mcpplibs.capi")); + expect(eq(s.candidates[0].shortName, "lua")); +}}; + +UTest dependencies_inline_spec_coexists_with_subtable{"Manifest/DependenciesInlineSpecCoexistsWithSubtable", [] { // `bar = { git = "...", tag = "..." }` looks like a subtable but has // only dep-spec keys → treated as inline spec under default ns. // `[dependencies.acme]` is a real namespace subtable. @@ -2140,22 +2147,22 @@ bar = { git = "https://example.com/bar.git", tag = "v1" } util = "2.0.0" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->dependencies.size(), 2u); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->dependencies.size(), 2u))); auto& bar = m->dependencies.at("bar"); - EXPECT_EQ(bar.namespace_, "mcpplibs"); - EXPECT_EQ(bar.shortName, "bar"); - EXPECT_EQ(bar.git, "https://example.com/bar.git"); - EXPECT_EQ(bar.gitRev, "v1"); - EXPECT_EQ(bar.gitRefKind, "tag"); + expect(eq(bar.namespace_, "mcpplibs")); + expect(eq(bar.shortName, "bar")); + expect(eq(bar.git, "https://example.com/bar.git")); + expect(eq(bar.gitRev, "v1")); + expect(eq(bar.gitRefKind, "tag")); auto& util = m->dependencies.at("acme.util"); - EXPECT_EQ(util.namespace_, "acme"); - EXPECT_EQ(util.shortName, "util"); - EXPECT_EQ(util.version, "2.0.0"); -} + expect(eq(util.namespace_, "acme")); + expect(eq(util.shortName, "util")); + expect(eq(util.version, "2.0.0")); +}}; -TEST(SynthesizeFromXpkgLua, DepsKeySplitNamespace) { +UTest deps_key_split_namespace{"SynthesizeFromXpkgLua/DepsKeySplitNamespace", [] { constexpr auto src = R"( package = { spec = "1", @@ -2172,21 +2179,21 @@ package = { } )"; auto m = mcpp::manifest::synthesize_from_xpkg_lua(src, "consumer", "1.0.0", mcpp::toolchain::platform::HostPlatform::current()); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->dependencies.size(), 2u); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->dependencies.size(), 2u))); auto& a = m->dependencies.at("mbedtls"); - EXPECT_EQ(a.namespace_, "mcpplibs"); - EXPECT_EQ(a.shortName, "mbedtls"); - EXPECT_EQ(a.version, "3.6.1"); + expect(eq(a.namespace_, "mcpplibs")); + expect(eq(a.shortName, "mbedtls")); + expect(eq(a.version, "3.6.1")); auto& b = m->dependencies.at("mcpplibs.cmdline"); - EXPECT_EQ(b.namespace_, "mcpplibs"); - EXPECT_EQ(b.shortName, "cmdline"); - EXPECT_EQ(b.version, "0.0.2"); -} + expect(eq(b.namespace_, "mcpplibs")); + expect(eq(b.shortName, "cmdline")); + expect(eq(b.version, "0.0.2")); +}}; -TEST(SynthesizeFromXpkgLua, DepsDottedSelectorsAreExact) { +UTest deps_dotted_selectors_are_exact{"SynthesizeFromXpkgLua/DepsDottedSelectorsAreExact", [] { constexpr auto src = R"( package = { spec = "1", @@ -2204,38 +2211,38 @@ package = { } )"; auto m = mcpp::manifest::synthesize_from_xpkg_lua(src, "consumer", "1.0.0", mcpp::toolchain::platform::HostPlatform::current()); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->dependencies.size(), 3u); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->dependencies.size(), 3u))); auto& lua = m->dependencies.at("capi.lua"); - EXPECT_EQ(lua.namespace_, "capi"); - EXPECT_EQ(lua.shortName, "lua"); - EXPECT_EQ(lua.version, "0.0.3"); - EXPECT_TRUE(lua.legacyCandidateSearch); - ASSERT_EQ(lua.candidates.size(), 1u); - EXPECT_EQ(lua.candidates[0].namespace_, "capi"); - EXPECT_EQ(lua.candidates[0].shortName, "lua"); + expect(eq(lua.namespace_, "capi")); + expect(eq(lua.shortName, "lua")); + expect(eq(lua.version, "0.0.3")); + expect(bool(lua.legacyCandidateSearch)); + expect(fatal(eq(lua.candidates.size(), 1u))); + expect(eq(lua.candidates[0].namespace_, "capi")); + expect(eq(lua.candidates[0].shortName, "lua")); auto& imgui = m->dependencies.at("imgui.core"); - EXPECT_EQ(imgui.namespace_, "imgui"); - EXPECT_EQ(imgui.shortName, "core"); - EXPECT_EQ(imgui.version, "0.0.1"); - EXPECT_TRUE(imgui.legacyCandidateSearch); - ASSERT_EQ(imgui.candidates.size(), 1u); - EXPECT_EQ(imgui.candidates[0].namespace_, "imgui"); - EXPECT_EQ(imgui.candidates[0].shortName, "core"); + expect(eq(imgui.namespace_, "imgui")); + expect(eq(imgui.shortName, "core")); + expect(eq(imgui.version, "0.0.1")); + expect(bool(imgui.legacyCandidateSearch)); + expect(fatal(eq(imgui.candidates.size(), 1u))); + expect(eq(imgui.candidates[0].namespace_, "imgui")); + expect(eq(imgui.candidates[0].shortName, "core")); auto& gtest = m->dependencies.at("compat.gtest"); - EXPECT_EQ(gtest.namespace_, "compat"); - EXPECT_EQ(gtest.shortName, "gtest"); - EXPECT_EQ(gtest.version, "1.15.2"); - EXPECT_TRUE(gtest.legacyCandidateSearch); - ASSERT_EQ(gtest.candidates.size(), 1u); - EXPECT_EQ(gtest.candidates[0].namespace_, "compat"); - EXPECT_EQ(gtest.candidates[0].shortName, "gtest"); -} + expect(eq(gtest.namespace_, "compat")); + expect(eq(gtest.shortName, "gtest")); + expect(eq(gtest.version, "1.15.2")); + expect(bool(gtest.legacyCandidateSearch)); + expect(fatal(eq(gtest.candidates.size(), 1u))); + expect(eq(gtest.candidates[0].namespace_, "compat")); + expect(eq(gtest.candidates[0].shortName, "gtest")); +}}; -TEST(SynthesizeFromXpkgLua, InvalidDependencySelectorIsAnError) { +UTest invalid_dependency_selector_is_an_error{"SynthesizeFromXpkgLua/InvalidDependencySelectorIsAnError", [] { constexpr auto src = R"( package = { spec = "1", @@ -2249,12 +2256,11 @@ package = { )"; auto m = mcpp::manifest::synthesize_from_xpkg_lua( src, "consumer", "1.0.0", mcpp::toolchain::platform::HostPlatform::current()); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().message.find("invalid package selector"), - std::string::npos) << m.error().format(); -} + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().message.find("invalid package selector"), std::string::npos)); !failed) failed << m.error().format(); +}}; -TEST(Manifest, WorkspaceSectionParsed) { +UTest workspace_section_parsed{"Manifest/WorkspaceSectionParsed", [] { constexpr auto src = R"( [workspace] members = ["libs/core", "libs/http", "apps/server"] @@ -2268,21 +2274,21 @@ gtest = "1.15.2" mbedtls = "3.6.1" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_TRUE(m->workspace.present); - ASSERT_EQ(m->workspace.members.size(), 3u); - EXPECT_EQ(m->workspace.members[0], "libs/core"); - EXPECT_EQ(m->workspace.members[1], "libs/http"); - EXPECT_EQ(m->workspace.members[2], "apps/server"); - ASSERT_EQ(m->workspace.exclude.size(), 1u); - EXPECT_EQ(m->workspace.exclude[0], "libs/experimental"); - ASSERT_EQ(m->workspace.dependencies.size(), 3u); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(bool(m->workspace.present)); + expect(fatal(eq(m->workspace.members.size(), 3u))); + expect(eq(m->workspace.members[0], "libs/core")); + expect(eq(m->workspace.members[1], "libs/http")); + expect(eq(m->workspace.members[2], "apps/server")); + expect(fatal(eq(m->workspace.exclude.size(), 1u))); + expect(eq(m->workspace.exclude[0], "libs/experimental")); + expect(fatal(eq(m->workspace.dependencies.size(), 3u))); auto& gt = m->workspace.dependencies.at("compat.gtest"); - EXPECT_EQ(gt.version, "1.15.2"); - EXPECT_EQ(gt.namespace_, "compat"); -} + expect(eq(gt.version, "1.15.2")); + expect(eq(gt.namespace_, "compat")); +}}; -TEST(Manifest, WorkspaceDependenciesUseExactDottedSelectors) { +UTest workspace_dependencies_use_exact_dotted_selectors{"Manifest/WorkspaceDependenciesUseExactDottedSelectors", [] { constexpr auto src = R"( [workspace] members = ["libs/core"] @@ -2294,47 +2300,47 @@ compat.gtest = "1.15.2" mcpplibs.templates = "0.0.1" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->workspace.dependencies.size(), 4u); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->workspace.dependencies.size(), 4u))); auto& lua = m->workspace.dependencies.at("capi.lua"); - EXPECT_EQ(lua.namespace_, "capi"); - EXPECT_EQ(lua.shortName, "lua"); - EXPECT_EQ(lua.version, "0.0.3"); - EXPECT_TRUE(lua.legacyCandidateSearch); - ASSERT_EQ(lua.candidates.size(), 1u); - EXPECT_EQ(lua.candidates[0].namespace_, "capi"); - EXPECT_EQ(lua.candidates[0].shortName, "lua"); + expect(eq(lua.namespace_, "capi")); + expect(eq(lua.shortName, "lua")); + expect(eq(lua.version, "0.0.3")); + expect(bool(lua.legacyCandidateSearch)); + expect(fatal(eq(lua.candidates.size(), 1u))); + expect(eq(lua.candidates[0].namespace_, "capi")); + expect(eq(lua.candidates[0].shortName, "lua")); auto& imgui = m->workspace.dependencies.at("imgui.core"); - EXPECT_EQ(imgui.namespace_, "imgui"); - EXPECT_EQ(imgui.shortName, "core"); - EXPECT_EQ(imgui.version, "0.0.1"); - EXPECT_TRUE(imgui.legacyCandidateSearch); - ASSERT_EQ(imgui.candidates.size(), 1u); - EXPECT_EQ(imgui.candidates[0].namespace_, "imgui"); - EXPECT_EQ(imgui.candidates[0].shortName, "core"); + expect(eq(imgui.namespace_, "imgui")); + expect(eq(imgui.shortName, "core")); + expect(eq(imgui.version, "0.0.1")); + expect(bool(imgui.legacyCandidateSearch)); + expect(fatal(eq(imgui.candidates.size(), 1u))); + expect(eq(imgui.candidates[0].namespace_, "imgui")); + expect(eq(imgui.candidates[0].shortName, "core")); auto& gt = m->workspace.dependencies.at("compat.gtest"); - EXPECT_EQ(gt.namespace_, "compat"); - EXPECT_EQ(gt.shortName, "gtest"); - EXPECT_EQ(gt.version, "1.15.2"); - EXPECT_TRUE(gt.legacyCandidateSearch); - ASSERT_EQ(gt.candidates.size(), 1u); - EXPECT_EQ(gt.candidates[0].namespace_, "compat"); - EXPECT_EQ(gt.candidates[0].shortName, "gtest"); + expect(eq(gt.namespace_, "compat")); + expect(eq(gt.shortName, "gtest")); + expect(eq(gt.version, "1.15.2")); + expect(bool(gt.legacyCandidateSearch)); + expect(fatal(eq(gt.candidates.size(), 1u))); + expect(eq(gt.candidates[0].namespace_, "compat")); + expect(eq(gt.candidates[0].shortName, "gtest")); auto& tmpl = m->workspace.dependencies.at("mcpplibs.templates"); - EXPECT_EQ(tmpl.namespace_, "mcpplibs"); - EXPECT_EQ(tmpl.shortName, "templates"); - EXPECT_EQ(tmpl.version, "0.0.1"); - EXPECT_FALSE(tmpl.legacyCandidateSearch); - ASSERT_EQ(tmpl.candidates.size(), 1u); - EXPECT_EQ(tmpl.candidates[0].namespace_, "mcpplibs"); - EXPECT_EQ(tmpl.candidates[0].shortName, "templates"); -} + expect(eq(tmpl.namespace_, "mcpplibs")); + expect(eq(tmpl.shortName, "templates")); + expect(eq(tmpl.version, "0.0.1")); + expect(!tmpl.legacyCandidateSearch); + expect(fatal(eq(tmpl.candidates.size(), 1u))); + expect(eq(tmpl.candidates[0].namespace_, "mcpplibs")); + expect(eq(tmpl.candidates[0].shortName, "templates")); +}}; -TEST(Manifest, WorkspaceTrueInDependency) { +UTest workspace_true_in_dependency{"Manifest/WorkspaceTrueInDependency", [] { constexpr auto src = R"( [package] name = "x" @@ -2343,14 +2349,14 @@ version = "0.1.0" mbedtls = { workspace = true } )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); auto& s = m->dependencies.at("compat.mbedtls"); - EXPECT_TRUE(s.inheritWorkspace); - EXPECT_EQ(s.namespace_, "compat"); - EXPECT_EQ(s.shortName, "mbedtls"); -} + expect(bool(s.inheritWorkspace)); + expect(eq(s.namespace_, "compat")); + expect(eq(s.shortName, "mbedtls")); +}}; -TEST(Manifest, IndicesDefaultKeyNormalizesToDefaultNamespace) { +UTest indices_default_key_normalizes_to_default_namespace{"Manifest/IndicesDefaultKeyNormalizesToDefaultNamespace", [] { // R6: `[indices] default = {...}` (the canonical spelling) is a // redirect for the DEFAULT namespace (bare `gtest = "1.15.2"` deps), // not a literal index named "default". It must be stored under @@ -2368,17 +2374,16 @@ sources = ["src/**/*.cppm"] default = { path = "../local-pkgs" } )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); auto it = m->indices.find(std::string(mcpp::manifest::kDefaultNamespace)); - ASSERT_NE(it, m->indices.end()) - << "[indices] default = {...} must normalize to kDefaultNamespace"; - EXPECT_EQ(it->second.path.string(), "../local-pkgs"); - EXPECT_FALSE(it->second.is_builtin()); - EXPECT_EQ(m->indices.find("default"), m->indices.end()) + if (auto&& failed = expect(fatal(ne(it, m->indices.end()))); !failed) failed << "[indices] default = {...} must normalize to kDefaultNamespace"; + expect(eq(it->second.path.string(), "../local-pkgs")); + expect(!(it->second.is_builtin())); + expect(eq(m->indices.find("default"), m->indices.end())) << "the literal key \"default\" must not survive normalization"; -} +}}; -TEST(Manifest, IndicesEmptyStringKeyNormalizesToDefaultNamespace) { +UTest indices_empty_string_key_normalizes_to_default_namespace{"Manifest/IndicesEmptyStringKeyNormalizesToDefaultNamespace", [] { // The empty-quoted key `""` is also accepted as a spelling of the // default-namespace redirect. constexpr auto src = R"( @@ -2393,14 +2398,13 @@ sources = ["src/**/*.cppm"] "" = { path = "../local-pkgs" } )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); auto it = m->indices.find(std::string(mcpp::manifest::kDefaultNamespace)); - ASSERT_NE(it, m->indices.end()) - << "[indices] \"\" = {...} must normalize to kDefaultNamespace"; - EXPECT_EQ(it->second.path.string(), "../local-pkgs"); -} + if (auto&& failed = expect(fatal(ne(it, m->indices.end()))); !failed) failed << "[indices] \"\" = {...} must normalize to kDefaultNamespace"; + expect(eq(it->second.path.string(), "../local-pkgs")); +}}; -TEST(Manifest, IndicesDefaultKeyWithUrlIsRejected) { +UTest indices_default_key_with_url_is_rejected{"Manifest/IndicesDefaultKeyWithUrlIsRejected", [] { // R6's design goal is redirecting the default namespace to a LOCAL // index checkout (`path = ...`), so the index repo can test module // packages. The `url` form is not a design requirement, and silently @@ -2422,15 +2426,13 @@ sources = ["src/**/*.cppm"] default = { url = "https://example.com/mcpp-index.git" } )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_FALSE(m.has_value()) - << "[indices] default = { url = ... } must be rejected, not silently " + if (auto&& failed = expect(fatal(!m.has_value())); !failed) failed << "[indices] default = { url = ... } must be rejected, not silently " "accepted as a no-op redirect"; - EXPECT_NE(m.error().format().find("path"), std::string::npos) - << "error should mention that only 'path' is supported for the " + if (auto&& failed = expect(ne(m.error().format().find("path"), std::string::npos)); !failed) failed << "error should mention that only 'path' is supported for the " "default-namespace redirect; got: " << m.error().format(); -} +}}; -TEST(Manifest, IndicesMcpplibsUrlPinStillAccepted) { +UTest indices_mcpplibs_url_pin_still_accepted{"Manifest/IndicesMcpplibsUrlPinStillAccepted", [] { // A literal `[indices] mcpplibs = { url = ..., rev = ... }` is a // DIFFERENT key spelling from the `default`/`""` alias — it pins the // real builtin registry to a commit, not a default-namespace redirect @@ -2448,15 +2450,15 @@ sources = ["src/**/*.cppm"] mcpplibs = { url = "https://example.com/mcpp-index.git", rev = "abc123" } )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); auto it = m->indices.find(std::string(mcpp::manifest::kDefaultNamespace)); - ASSERT_NE(it, m->indices.end()); - EXPECT_EQ(it->second.url, "https://example.com/mcpp-index.git"); - EXPECT_EQ(it->second.rev, "abc123"); - EXPECT_TRUE(it->second.is_builtin()); -} + expect(fatal(ne(it, m->indices.end()))); + expect(eq(it->second.url, "https://example.com/mcpp-index.git")); + expect(eq(it->second.rev, "abc123")); + expect(it->second.is_builtin()); +}}; -TEST(Manifest, IndicesDefaultAndMcpplibsCollisionRejected) { +UTest indices_default_and_mcpplibs_collision_rejected{"Manifest/IndicesDefaultAndMcpplibsCollisionRejected", [] { // `default`, `""`, and a literal `mcpplibs` key all normalize to the // same map slot (kDefaultNamespace). Declaring more than one must be a // parse error, not a silent last-writer-wins clobber. @@ -2473,14 +2475,12 @@ mcpplibs = { path = "../local-pkgs-a" } default = { path = "../local-pkgs-b" } )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_FALSE(m.has_value()) - << "[indices] mcpplibs + default together must be rejected as a " + if (auto&& failed = expect(fatal(!m.has_value())); !failed) failed << "[indices] mcpplibs + default together must be rejected as a " "collision on the same default-namespace slot"; - EXPECT_NE(m.error().format().find("collide"), std::string::npos) - << "error should mention the collision; got: " << m.error().format(); -} + if (auto&& failed = expect(ne(m.error().format().find("collide"), std::string::npos)); !failed) failed << "error should mention the collision; got: " << m.error().format(); +}}; -TEST(Manifest, WorkspaceDependenciesPathFormFlatKey) { +UTest workspace_dependencies_path_form_flat_key{"Manifest/WorkspaceDependenciesPathFormFlatKey", [] { // #224: `[workspace.dependencies] ylib = { path = "ylib" }` — a plain // (non-namespaced, non-dotted) top-level key with an inline path table // — must parse to a path-form DependencySpec, not get misrouted into @@ -2494,26 +2494,26 @@ members = ["a"] ylib = { path = "ylib" } )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_TRUE(m->workspace.dependencies.contains("ylib")) + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(m->workspace.dependencies.contains("ylib"))) << "workspace.dependencies must contain a flat 'ylib' key"; auto& s = m->workspace.dependencies.at("ylib"); - EXPECT_EQ(s.path, "ylib"); - EXPECT_TRUE(s.version.empty()); -} + expect(eq(s.path, "ylib")); + expect(s.version.empty()); +}}; -TEST(Manifest, NoWorkspaceSectionMeansNotPresent) { +UTest no_workspace_section_means_not_present{"Manifest/NoWorkspaceSectionMeansNotPresent", [] { constexpr auto src = R"( [package] name = "x" version = "0.1.0" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()); - EXPECT_FALSE(m->workspace.present); -} + expect(fatal(m.has_value())); + expect(!(m->workspace.present)); +}}; -TEST(Manifest, LibRootInferredFromPackageName) { +UTest lib_root_inferred_from_package_name{"Manifest/LibRootInferredFromPackageName", [] { constexpr auto src = R"( [package] name = "mcpplibs.tinyhttps" @@ -2522,14 +2522,14 @@ version = "0.2.0" kind = "lib" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_TRUE(m->lib.path.empty()); - EXPECT_TRUE(mcpp::manifest::has_lib_target(*m)); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(m->lib.path.empty()); + expect(mcpp::manifest::has_lib_target(*m)); auto root = mcpp::manifest::resolve_lib_root_path(*m); - EXPECT_EQ(root, std::filesystem::path("src/tinyhttps.cppm")); -} + expect(eq(root, std::filesystem::path("src/tinyhttps.cppm"))); +}}; -TEST(Manifest, LibRootBareNameNoNamespace) { +UTest lib_root_bare_name_no_namespace{"Manifest/LibRootBareNameNoNamespace", [] { constexpr auto src = R"( [package] name = "gtest" @@ -2538,12 +2538,12 @@ version = "1.0.0" kind = "lib" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); auto root = mcpp::manifest::resolve_lib_root_path(*m); - EXPECT_EQ(root, std::filesystem::path("src/gtest.cppm")); -} + expect(eq(root, std::filesystem::path("src/gtest.cppm"))); +}}; -TEST(Manifest, LibRootExplicitOverride) { +UTest lib_root_explicit_override{"Manifest/LibRootExplicitOverride", [] { constexpr auto src = R"( [package] name = "mcpplibs.tinyhttps" @@ -2554,13 +2554,13 @@ path = "src/api.cppm" kind = "lib" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_EQ(m->lib.path.string(), "src/api.cppm"); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(eq(m->lib.path.string(), "src/api.cppm")); auto root = mcpp::manifest::resolve_lib_root_path(*m); - EXPECT_EQ(root.string(), "src/api.cppm"); -} + expect(eq(root.string(), "src/api.cppm")); +}}; -TEST(Manifest, HasLibTargetFalseForBareBinaryManifest) { +UTest has_lib_target_false_for_bare_binary_manifest{"Manifest/HasLibTargetFalseForBareBinaryManifest", [] { // No [targets.*] declared → parse_string leaves targets empty. // load() would later infer a bin/lib from sources, but parse_string // alone leaves it bare; either way no lib target. @@ -2570,11 +2570,11 @@ name = "mcpp" version = "0.0.2" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_FALSE(mcpp::manifest::has_lib_target(*m)); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(!mcpp::manifest::has_lib_target(*m)); +}}; -TEST(Manifest, ParsesPerTargetFlagsAndRequiredFeatures) { +UTest parses_per_target_flags_and_required_features{"Manifest/ParsesPerTargetFlagsAndRequiredFeatures", [] { constexpr auto src = R"( [package] name = "app" @@ -2588,22 +2588,22 @@ cflags = ["-DPURE_C"] required_features = ["server"] )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->targets.size(), 1u); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->targets.size(), 1u))); auto& t = m->targets[0]; - ASSERT_EQ(t.defines.size(), 2u); - EXPECT_EQ(t.defines[0], "BUILD_SERVER=1"); - EXPECT_EQ(t.defines[1], "PORT=8080"); - ASSERT_EQ(t.cxxflags.size(), 1u); - EXPECT_EQ(t.cxxflags[0], "-fno-exceptions"); - ASSERT_EQ(t.cflags.size(), 1u); - EXPECT_EQ(t.cflags[0], "-DPURE_C"); - ASSERT_EQ(t.requiredFeatures.size(), 1u); - EXPECT_EQ(t.requiredFeatures[0], "server"); - EXPECT_TRUE(m->schemaWarnings.empty()); -} + expect(fatal(eq(t.defines.size(), 2u))); + expect(eq(t.defines[0], "BUILD_SERVER=1")); + expect(eq(t.defines[1], "PORT=8080")); + expect(fatal(eq(t.cxxflags.size(), 1u))); + expect(eq(t.cxxflags[0], "-fno-exceptions")); + expect(fatal(eq(t.cflags.size(), 1u))); + expect(eq(t.cflags[0], "-DPURE_C")); + expect(fatal(eq(t.requiredFeatures.size(), 1u))); + expect(eq(t.requiredFeatures[0], "server")); + expect(m->schemaWarnings.empty()); +}}; -TEST(Manifest, WarnsOnUnsupportedTargetKey) { +UTest warns_on_unsupported_target_key{"Manifest/WarnsOnUnsupportedTargetKey", [] { constexpr auto src = R"( [package] name = "app" @@ -2614,14 +2614,14 @@ main = "src/main.cpp" cxxfalgs = ["-DTYPO"] )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->schemaWarnings.size(), 1u); - EXPECT_NE(m->schemaWarnings[0].find("cxxfalgs"), std::string::npos); - EXPECT_NE(m->schemaWarnings[0].find("unsupported key"), std::string::npos); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->schemaWarnings.size(), 1u))); + expect(ne(m->schemaWarnings[0].find("cxxfalgs"), std::string::npos)); + expect(ne(m->schemaWarnings[0].find("unsupported key"), std::string::npos)); +}}; // #618: `windows_subsystem` / `windows_entry` on an executable target. -TEST(Manifest, ParsesWindowsSubsystemAndEntryOnABinaryTarget) { +UTest parses_windows_subsystem_and_entry_on_a_binary_target{"Manifest/ParsesWindowsSubsystemAndEntryOnABinaryTarget", [] { constexpr auto src = R"( [package] name = "app" @@ -2633,16 +2633,16 @@ windows_subsystem = "windows" windows_entry = "wWinMain" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->targets.size(), 1u); - EXPECT_EQ(m->targets[0].windowsSubsystem, "windows"); - EXPECT_EQ(m->targets[0].windowsEntry, "wWinMain"); - EXPECT_TRUE(m->schemaWarnings.empty()); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->targets.size(), 1u))); + expect(eq(m->targets[0].windowsSubsystem, "windows")); + expect(eq(m->targets[0].windowsEntry, "wWinMain")); + expect(m->schemaWarnings.empty()); +}}; // #693: `windows_code_page` states the process code page of an executable, in // the vocabulary of the manifest's `activeCodePage` element. -TEST(Manifest, ParsesWindowsCodePageOnABinaryTarget) { +UTest parses_windows_code_page_on_a_binary_target{"Manifest/ParsesWindowsCodePageOnABinaryTarget", [] { for (std::string_view value : {"utf-8", "legacy"}) { const auto src = std::format(R"( [package] @@ -2654,16 +2654,16 @@ main = "src/main.cpp" windows_code_page = "{}" )", value); auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->targets.size(), 1u); - EXPECT_EQ(m->targets[0].windowsCodePage, value); - EXPECT_TRUE(m->schemaWarnings.empty()); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->targets.size(), 1u))); + expect(eq(m->targets[0].windowsCodePage, value)); + expect(m->schemaWarnings.empty()); } -} +}}; // `windows_long_paths` (design 2026-10-16 §11): an executable's statement // that it handles paths past MAX_PATH; unset when the author says nothing. -TEST(Manifest, ParsesWindowsLongPathsOnABinaryTargetAndForTests) { +UTest parses_windows_long_paths_on_a_binary_target_and_for_tests{"Manifest/ParsesWindowsLongPathsOnABinaryTargetAndForTests", [] { for (bool value : {true, false}) { const auto src = std::format(R"( [package] @@ -2677,32 +2677,31 @@ windows_long_paths = {0} windows_long_paths = {0} )", value); auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->targets.size(), 1u); - ASSERT_TRUE(m->targets[0].windowsLongPaths.has_value()); - EXPECT_EQ(*m->targets[0].windowsLongPaths, value); - ASSERT_TRUE(m->testWindowsLongPaths.has_value()); - EXPECT_EQ(*m->testWindowsLongPaths, value); - EXPECT_TRUE(m->schemaWarnings.empty()); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->targets.size(), 1u))); + expect(fatal(m->targets[0].windowsLongPaths.has_value())); + expect(eq(*m->targets[0].windowsLongPaths, value)); + expect(fatal(m->testWindowsLongPaths.has_value())); + expect(eq(*m->testWindowsLongPaths, value)); + expect(m->schemaWarnings.empty()); } auto unset = mcpp::manifest::parse_string( "[package]\nname = \"app\"\nversion = \"0.1.0\"\n[targets.app]\nkind = \"bin\"\nmain = \"m.cpp\"\n"); - ASSERT_TRUE(unset.has_value()); - EXPECT_FALSE(unset->targets[0].windowsLongPaths.has_value()); + expect(fatal(unset.has_value())); + expect(!(unset->targets[0].windowsLongPaths.has_value())); auto wrong = mcpp::manifest::parse_string( "[package]\nname = \"app\"\nversion = \"0.1.0\"\n[targets.app]\nkind = \"bin\"\nmain = \"m.cpp\"\n" "windows_long_paths = \"yes\"\n"); - ASSERT_FALSE(wrong.has_value()); - EXPECT_NE(wrong.error().message.find("windows_long_paths must be true or false"), std::string::npos); + expect(fatal(!wrong.has_value())); + expect(ne(wrong.error().message.find("windows_long_paths must be true or false"), std::string::npos)); auto library = mcpp::manifest::parse_string( "[package]\nname = \"app\"\nversion = \"0.1.0\"\n[targets.core]\nkind = \"lib\"\n" "windows_long_paths = true\n"); - ASSERT_FALSE(library.has_value()); - EXPECT_NE(library.error().message.find("targets.core.windows_long_paths"), std::string::npos) - << library.error().message; -} + expect(fatal(!library.has_value())); + if (auto&& failed = expect(ne(library.error().message.find("targets.core.windows_long_paths"), std::string::npos)); !failed) failed << library.error().message; +}}; -TEST(Manifest, RefusesAnUnknownWindowsCodePageNamingTheAcceptedOnes) { +UTest refuses_an_unknown_windows_code_page_naming_the_accepted_ones{"Manifest/RefusesAnUnknownWindowsCodePageNamingTheAcceptedOnes", [] { constexpr auto src = R"( [package] name = "app" @@ -2713,12 +2712,11 @@ main = "src/main.cpp" windows_code_page = "65001" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().message.find("\"65001\" is not one of \"utf-8\", \"legacy\""), - std::string::npos) << m.error().message; -} + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().message.find("\"65001\" is not one of \"utf-8\", \"legacy\""), std::string::npos)); !failed) failed << m.error().message; +}}; -TEST(Manifest, RefusesWindowsKeysOnALibraryNamingTheTargetAndTheKey) { +UTest refuses_windows_keys_on_a_library_naming_the_target_and_the_key{"Manifest/RefusesWindowsKeysOnALibraryNamingTheTargetAndTheKey", [] { const std::pair keys[] = { {"windows_subsystem", "windows"}, {"windows_entry", "wmain"}, {"windows_code_page", "utf-8"}}; @@ -2733,19 +2731,17 @@ kind = "{}" {} = "{}" )", kind, key, value); auto m = mcpp::manifest::parse_string(src); - ASSERT_FALSE(m.has_value()) << kind << " " << key; - EXPECT_NE(m.error().message.find(std::format("targets.core.{}", key)), - std::string::npos) << m.error().message; - EXPECT_NE(m.error().message.find("executable"), std::string::npos) - << m.error().message; + expect(fatal(!m.has_value())) << kind << " " << key; + if (auto&& failed = expect(ne(m.error().message.find(std::format("targets.core.{}", key)), std::string::npos)); !failed) failed << m.error().message; + if (auto&& failed = expect(ne(m.error().message.find("executable"), std::string::npos)); !failed) failed << m.error().message; } } -} +}}; // #622 A3: `kind = "app"` -- "the thing a user launches", on every row. Its // link form is a property of the row (`toolchain::triple::application_form`), // not of the manifest; see test_artifact_naming.cpp for that half. -TEST(Manifest, KindAppParsesToApplication) { +UTest kind_app_parses_to_application{"Manifest/KindAppParsesToApplication", [] { constexpr auto src = R"( [package] name = "myapp" @@ -2755,16 +2751,16 @@ kind = "app" main = "src/main.cpp" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->targets.size(), 1u); - EXPECT_EQ(m->targets[0].kind, mcpp::manifest::Target::Application); - EXPECT_EQ(m->targets[0].main, "src/main.cpp"); - EXPECT_TRUE(m->targets[0].is_program()); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->targets.size(), 1u))); + expect(eq(m->targets[0].kind, mcpp::manifest::Target::Application)); + expect(eq(m->targets[0].main, "src/main.cpp")); + expect(m->targets[0].is_program()); +}}; // `is_program()` is exactly {Binary, Application} -- the negative direction, // asserted on every OTHER kind so the predicate cannot silently widen. -TEST(Manifest, IsProgramIsExactlyBinaryAndApplication) { +UTest is_program_is_exactly_binary_and_application{"Manifest/IsProgramIsExactlyBinaryAndApplication", [] { auto kind_of = [](std::string_view kind) { auto src = std::format(R"( [package] @@ -2780,15 +2776,15 @@ kind = "{}" for (auto [kind, expected] : {std::pair{"bin", true}, {"app", true}, {"lib", false}, {"shared", false}}) { auto m = kind_of(kind); - ASSERT_TRUE(m.has_value()) << kind << ": " << m.error().format(); - ASSERT_EQ(m->targets.size(), 1u) << kind; - EXPECT_EQ(m->targets[0].is_program(), expected) << kind; + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << kind << ": " << m.error().format(); + expect(fatal(eq(m->targets.size(), 1u))) << kind; + expect(eq(m->targets[0].is_program(), expected)) << kind; } -} +}}; // The refusal for an unrecognized `kind` lists all five accepted spellings -- // including the newest -- so a typo's message never falls behind the parser. -TEST(Manifest, KindRefusalListsAllFiveKinds) { +UTest kind_refusal_lists_all_five_kinds{"Manifest/KindRefusalListsAllFiveKinds", [] { constexpr auto src = R"( [package] name = "p" @@ -2797,15 +2793,14 @@ version = "0.1.0" kind = "framework" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().message.find("'bin', 'app', 'lib', 'shared' or 'entry'"), - std::string::npos) << m.error().message; -} + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().message.find("'bin', 'app', 'lib', 'shared' or 'entry'"), std::string::npos)); !failed) failed << m.error().message; +}}; // `main` is required for `app` exactly as it is for `bin` -- on Android it // becomes a translation unit of the shared library rather than an // executable's entry, but it is still named the same way. -TEST(Manifest, ApplicationRequiresMainField) { +UTest application_requires_main_field{"Manifest/ApplicationRequiresMainField", [] { constexpr auto src = R"( [package] name = "myapp" @@ -2814,16 +2809,15 @@ version = "0.1.0" kind = "app" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().message.find("requires 'main'"), std::string::npos) - << m.error().message; -} + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().message.find("requires 'main'"), std::string::npos)); !failed) failed << m.error().message; +}}; // `windows_subsystem` / `windows_entry` accept `app` exactly as they accept // `bin` (#622 A3) -- the negative direction is // RefusesWindowsKeysOnALibraryNamingTheTargetAndTheKey above, which still // covers `lib` and `shared`. -TEST(Manifest, WindowsKeysAreAcceptedOnAnApplicationTarget) { +UTest windows_keys_are_accepted_on_an_application_target{"Manifest/WindowsKeysAreAcceptedOnAnApplicationTarget", [] { constexpr auto src = R"( [package] name = "app" @@ -2835,13 +2829,13 @@ windows_subsystem = "windows" windows_entry = "wWinMain" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->targets.size(), 1u); - EXPECT_EQ(m->targets[0].windowsSubsystem, "windows"); - EXPECT_EQ(m->targets[0].windowsEntry, "wWinMain"); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->targets.size(), 1u))); + expect(eq(m->targets[0].windowsSubsystem, "windows")); + expect(eq(m->targets[0].windowsEntry, "wWinMain")); +}}; -TEST(Manifest, RefusesAnUnknownWindowsValueNamingTheAcceptedOnes) { +UTest refuses_an_unknown_windows_value_naming_the_accepted_ones{"Manifest/RefusesAnUnknownWindowsValueNamingTheAcceptedOnes", [] { constexpr auto subsystem = R"( [package] name = "app" @@ -2852,9 +2846,8 @@ main = "src/main.cpp" windows_subsystem = "gui" )"; auto m = mcpp::manifest::parse_string(subsystem); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().message.find("\"gui\" is not one of \"console\", \"windows\""), - std::string::npos) << m.error().message; + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().message.find("\"gui\" is not one of \"console\", \"windows\""), std::string::npos)); !failed) failed << m.error().message; constexpr auto entry = R"( [package] @@ -2866,15 +2859,14 @@ main = "src/main.cpp" windows_entry = "mainCRTStartup" )"; auto e = mcpp::manifest::parse_string(entry); - ASSERT_FALSE(e.has_value()); - EXPECT_NE(e.error().message.find( - "is not one of \"main\", \"wmain\", \"WinMain\", \"wWinMain\""), - std::string::npos) << e.error().message; -} + expect(fatal(!e.has_value())); + if (auto&& failed = expect(ne(e.error().message.find( + "is not one of \"main\", \"wmain\", \"WinMain\", \"wWinMain\""), std::string::npos)); !failed) failed << e.error().message; +}}; // The key list in the warning is generated from the list the parser accepts, // so a key the parser reads cannot be missing from the message. -TEST(Manifest, UnsupportedTargetKeyWarningListsEveryAcceptedKey) { +UTest unsupported_target_key_warning_lists_every_accepted_key{"Manifest/UnsupportedTargetKeyWarningListsEveryAcceptedKey", [] { constexpr auto src = R"( [package] name = "app" @@ -2885,14 +2877,14 @@ main = "src/main.cpp" bogus = "x" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->schemaWarnings.size(), 1u); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->schemaWarnings.size(), 1u))); for (auto key : {"exports", "windows_subsystem", "windows_entry", "required_features"}) - EXPECT_NE(m->schemaWarnings[0].find(key), std::string::npos) + expect(ne(m->schemaWarnings[0].find(key), std::string::npos)) << key << " missing from: " << m->schemaWarnings[0]; -} +}}; -TEST(Manifest, RejectsStdFlagInTargetCxxflags) { +UTest rejects_std_flag_in_target_cxxflags{"Manifest/RejectsStdFlagInTargetCxxflags", [] { constexpr auto src = R"( [package] name = "app" @@ -2903,11 +2895,11 @@ main = "src/main.cpp" cxxflags = ["-std=c++20"] )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().message.find("[package].standard"), std::string::npos); -} + expect(fatal(!m.has_value())); + expect(ne(m.error().message.find("[package].standard"), std::string::npos)); +}}; -TEST(ListXpkgVersions, IgnoresCommentedEntries) { +UTest ignores_commented_entries{"ListXpkgVersions/IgnoresCommentedEntries", [] { constexpr auto src = R"( package = { xpm = { @@ -2919,9 +2911,9 @@ package = { } )"; auto v = mcpp::manifest::list_xpkg_versions(src, mcpp::toolchain::platform::TargetPlatform::for_lint_of("linux")); - ASSERT_EQ(v.size(), 1u); - EXPECT_EQ(v[0], "0.1.0"); -} + expect(fatal(eq(v.size(), 1u))); + expect(eq(v[0], "0.1.0")); +}}; // ─── xpkg_lua_identity_matches — descriptor identity gate ─────────── // @@ -2949,72 +2941,72 @@ package = { )"; } // namespace -TEST(XpkgIdentity, CompatDescriptorMatchesCompatRequest) { - EXPECT_TRUE(mcpp::manifest::xpkg_lua_identity_matches( +UTest compat_descriptor_matches_compat_request{"XpkgIdentity/CompatDescriptorMatchesCompatRequest", [] { + expect(mcpp::manifest::xpkg_lua_identity_matches( kCompatZlibLua, "compat", "zlib")); -} +}}; -TEST(XpkgIdentity, UpstreamBareZlibDoesNotMatchCompatRequest) { +UTest upstream_bare_zlib_does_not_match_compat_request{"XpkgIdentity/UpstreamBareZlibDoesNotMatchCompatRequest", [] { // The crux of the incident: a foreign bare `zlib.lua` must be rejected for // a `compat.zlib` request, no matter that the candidate filename matched. - EXPECT_FALSE(mcpp::manifest::xpkg_lua_identity_matches( + expect(!mcpp::manifest::xpkg_lua_identity_matches( kUpstreamZlibLua, "compat", "zlib")); -} +}}; -TEST(XpkgIdentity, DescriptorDeclaringNamespaceMatchesOnlyThatNamespace) { +UTest descriptor_declaring_namespace_matches_only_that_namespace{"XpkgIdentity/DescriptorDeclaringNamespaceMatchesOnlyThatNamespace", [] { // When a descriptor DECLARES its namespace, it matches that namespace and no // other. (Index-owned namespace for *no-namespace* descriptors — attributing // a bare `zlib.lua` to its owning index as `(xim, zlib)` — is deferred to the // §4.1 follow-up, since the content-only gate cannot know a file's index.) constexpr std::string_view ximZlib = R"(package = { namespace = "xim", name = "zlib" })"; - EXPECT_TRUE(mcpp::manifest::xpkg_lua_identity_matches( + expect(mcpp::manifest::xpkg_lua_identity_matches( ximZlib, "xim", "zlib", /*allowLegacyBareDefault=*/false)); - EXPECT_FALSE(mcpp::manifest::xpkg_lua_identity_matches( + expect(!mcpp::manifest::xpkg_lua_identity_matches( ximZlib, "compat", "zlib")); -} +}}; -TEST(XpkgIdentity, NoDeclaredNameIsAcceptedLeniently) { +UTest no_declared_name_is_accepted_leniently{"XpkgIdentity/NoDeclaredNameIsAcceptedLeniently", [] { // A descriptor without a package.name cannot be verified → accepted. constexpr std::string_view noName = R"(package = { version = "1.0.0" })"; - EXPECT_TRUE(mcpp::manifest::xpkg_lua_identity_matches(noName, "compat", "zlib")); -} + expect(mcpp::manifest::xpkg_lua_identity_matches(noName, "compat", "zlib")); +}}; -TEST(XpkgIdentity, DefaultNamespaceBareNameGatedByFlag) { +UTest default_namespace_bare_name_gated_by_flag{"XpkgIdentity/DefaultNamespaceBareNameGatedByFlag", [] { // Default-namespace (mcpplibs) bare-named legacy descriptor: accepted only // when the legacy-bare flag is on. constexpr std::string_view bareDefault = R"(package = { name = "cmdline" })"; - EXPECT_TRUE(mcpp::manifest::xpkg_lua_identity_matches( + expect(mcpp::manifest::xpkg_lua_identity_matches( bareDefault, "mcpplibs", "cmdline", /*allowLegacyBareDefault=*/true)); - EXPECT_FALSE(mcpp::manifest::xpkg_lua_identity_matches( + expect(!mcpp::manifest::xpkg_lua_identity_matches( bareDefault, "mcpplibs", "cmdline", /*allowLegacyBareDefault=*/false)); -} +}}; -TEST(XpkgIdentity, EmptyNamespaceDiscoveryMatchesNamespacedDescriptor) { +UTest empty_namespace_discovery_matches_namespaced_descriptor{"XpkgIdentity/EmptyNamespaceDiscoveryMatchesNamespacedDescriptor", [] { // `mcpp new --template llmapi` reads with an empty (unknown) namespace and // derives the real namespace from the file. A namespaced descriptor must // still be discoverable by its short name. constexpr std::string_view llmapi = R"( package = { namespace = "mcpplibs", name = "mcpplibs.llmapi", version = "0.2.8" } )"; - EXPECT_TRUE(mcpp::manifest::xpkg_lua_identity_matches(llmapi, "", "llmapi")); + expect(mcpp::manifest::xpkg_lua_identity_matches(llmapi, "", "llmapi")); // ...but it is NOT a match for a different short name. - EXPECT_FALSE(mcpp::manifest::xpkg_lua_identity_matches(llmapi, "", "zlib")); -} + expect(!mcpp::manifest::xpkg_lua_identity_matches(llmapi, "", "zlib")); +}}; -TEST(XpkgIdentity, DefaultNamespaceRequestDoesNotMatchCompatPackage) { +UTest default_namespace_request_does_not_match_compat_package{"XpkgIdentity/DefaultNamespaceRequestDoesNotMatchCompatPackage", [] { // Namespace omission now means exactly mcpplibs. A compat package must be // selected as `compat.gtest`, never smuggled through an identity alias. constexpr std::string_view compatGtest = R"(package = { namespace = "compat", name = "compat.gtest", version = "1.15.2" })"; - EXPECT_FALSE(mcpp::manifest::xpkg_lua_identity_matches( + expect(!mcpp::manifest::xpkg_lua_identity_matches( compatGtest, "mcpplibs", "gtest")); - EXPECT_FALSE(mcpp::manifest::xpkg_lua_identity_matches( + expect(!mcpp::manifest::xpkg_lua_identity_matches( compatGtest, "mcpplibs", "gtest", /*allowLegacyBareDefault=*/false)); // But a different default-ns name does not match it. - EXPECT_FALSE(mcpp::manifest::xpkg_lua_identity_matches( + expect(!mcpp::manifest::xpkg_lua_identity_matches( compatGtest, "mcpplibs", "zlib")); -} +}}; // ─── xpkg_wire_address — the address xlings is asked to install ───── // @@ -3026,87 +3018,87 @@ TEST(XpkgIdentity, DefaultNamespaceRequestDoesNotMatchCompatPackage) { // read `compat.gtest`, so a hardcoded `compat.` retry caught it. The // index's SPEC-001 short-name migration removed that coincidence. -TEST(XpkgWireAddress, CompatDescriptorAddressesCompat) { +UTest compat_descriptor_addresses_compat{"XpkgWireAddress/CompatDescriptorAddressesCompat", [] { constexpr std::string_view compatGtest = R"(package = { namespace = "compat", name = "gtest", version = "1.15.2" })"; auto addr = mcpp::manifest::xpkg_wire_address(compatGtest, "compat", "gtest"); - EXPECT_EQ(addr.ns, "compat"); - EXPECT_EQ(addr.name, "gtest"); - EXPECT_EQ(addr.target, "compat:gtest"); // NOT "mcpplibs:gtest" -} + expect(eq(addr.ns, "compat")); + expect(eq(addr.name, "gtest")); + expect(eq(addr.target, "compat:gtest")); // NOT "mcpplibs:gtest" +}}; -TEST(XpkgWireAddress, DefaultNamespaceDescriptorIsUnchanged) { +UTest default_namespace_descriptor_is_unchanged{"XpkgWireAddress/DefaultNamespaceDescriptorIsUnchanged", [] { // The 8 mcpplibs descriptors in the live index must address exactly as // before — this fix moves only the packages that were already broken. constexpr std::string_view cmdline = R"(package = { namespace = "mcpplibs", name = "cmdline", version = "0.0.1" })"; auto addr = mcpp::manifest::xpkg_wire_address(cmdline, "mcpplibs", "cmdline"); - EXPECT_EQ(addr.target, "mcpplibs:cmdline"); -} + expect(eq(addr.target, "mcpplibs:cmdline")); +}}; -TEST(XpkgWireAddress, LegacyFqnDescriptorKeepsItsLiteralName) { +UTest legacy_fqn_descriptor_keeps_its_literal_name{"XpkgWireAddress/LegacyFqnDescriptorKeepsItsLiteralName", [] { // Pre-SPEC-001 spelling. The literal `name` IS the index's key, so it goes // out verbatim — only the namespace half is corrected. constexpr std::string_view legacy = R"(package = { namespace = "compat", name = "compat.zlib", version = "1.3.2" })"; auto addr = mcpp::manifest::xpkg_wire_address(legacy, "mcpplibs", "zlib"); - EXPECT_EQ(addr.ns, "compat"); - EXPECT_EQ(addr.name, "compat.zlib"); - EXPECT_EQ(addr.target, "compat:compat.zlib"); -} + expect(eq(addr.ns, "compat")); + expect(eq(addr.name, "compat.zlib")); + expect(eq(addr.target, "compat:compat.zlib")); +}}; -TEST(XpkgWireAddress, QualifiedRequestIsUnchanged) { +UTest qualified_request_is_unchanged{"XpkgWireAddress/QualifiedRequestIsUnchanged", [] { // `[dependencies.compat] gtest = ...` already worked (this is the spelling // every mcpp-index example uses, which is why the index CI stayed green). constexpr std::string_view compatGtest = R"(package = { namespace = "compat", name = "gtest", version = "1.15.2" })"; auto addr = mcpp::manifest::xpkg_wire_address(compatGtest, "compat", "gtest"); - EXPECT_EQ(addr.target, "compat:gtest"); -} + expect(eq(addr.target, "compat:gtest")); +}}; -TEST(XpkgWireAddress, NestedNamespaceIsUnchanged) { +UTest nested_namespace_is_unchanged{"XpkgWireAddress/NestedNamespaceIsUnchanged", [] { constexpr std::string_view capiLua = R"(package = { namespace = "mcpplibs.capi", name = "lua", version = "5.4.7" })"; auto addr = mcpp::manifest::xpkg_wire_address(capiLua, "mcpplibs.capi", "lua"); - EXPECT_EQ(addr.target, "mcpplibs.capi:lua"); -} + expect(eq(addr.target, "mcpplibs.capi:lua")); +}}; -TEST(XpkgWireAddress, DescriptorWithoutNamespaceInheritsTheRequest) { +UTest descriptor_without_namespace_inherits_the_request{"XpkgWireAddress/DescriptorWithoutNamespaceInheritsTheRequest", [] { // A no-namespace descriptor inherits the namespace owned by the index it // was found in — at this seam, the request coordinate. Same rule the // identity gate used to accept it. constexpr std::string_view bare = R"(package = { name = "cmdline" })"; auto addr = mcpp::manifest::xpkg_wire_address(bare, "mcpplibs", "cmdline"); - EXPECT_EQ(addr.ns, "mcpplibs"); - EXPECT_EQ(addr.target, "mcpplibs:cmdline"); -} + expect(eq(addr.ns, "mcpplibs")); + expect(eq(addr.target, "mcpplibs:cmdline")); +}}; -TEST(XpkgWireAddress, NamespacelessRequestAddressesTheBareLiteral) { +UTest namespaceless_request_addresses_the_bare_literal{"XpkgWireAddress/NamespacelessRequestAddressesTheBareLiteral", [] { // xim upstream packages (`opencv`) have no namespace on either side. constexpr std::string_view opencv = R"(package = { name = "opencv" })"; auto addr = mcpp::manifest::xpkg_wire_address(opencv, "", "opencv"); - EXPECT_EQ(addr.ns, ""); - EXPECT_EQ(addr.target, "opencv"); -} + expect(eq(addr.ns, "")); + expect(eq(addr.target, "opencv")); +}}; -TEST(XpkgWireAddress, NoDescriptorKeepsTheHistoricalDerivation) { +UTest no_descriptor_keeps_the_historical_derivation{"XpkgWireAddress/NoDescriptorKeepsTheHistoricalDerivation", [] { // Index not synced yet: mcpp has nothing to read, so it must NOT invent a // new address — the failure has to stay exactly where it was. auto addr = mcpp::manifest::xpkg_wire_address("", "mcpplibs", "gtest"); - EXPECT_EQ(addr.name, "mcpplibs.gtest"); - EXPECT_EQ(addr.target, "mcpplibs:mcpplibs.gtest"); + expect(eq(addr.name, "mcpplibs.gtest")); + expect(eq(addr.target, "mcpplibs:mcpplibs.gtest")); auto bare = mcpp::manifest::xpkg_wire_address("", "", "opencv"); - EXPECT_EQ(bare.target, "opencv"); -} + expect(eq(bare.target, "opencv")); +}}; -TEST(XpkgWireAddress, DescriptorWithoutNameFallsBackToDerivation) { +UTest descriptor_without_name_falls_back_to_derivation{"XpkgWireAddress/DescriptorWithoutNameFallsBackToDerivation", [] { // A descriptor that declares no `name` cannot supply a wire key (the // identity gate accepts it leniently, so it can reach here). constexpr std::string_view noName = R"(package = { version = "1.0.0" })"; auto addr = mcpp::manifest::xpkg_wire_address(noName, "mcpplibs", "gtest"); - EXPECT_EQ(addr.target, "mcpplibs:mcpplibs.gtest"); -} + expect(eq(addr.target, "mcpplibs:mcpplibs.gtest")); +}}; // ─── INV-NAME: xpkg_name_form_violation (#278) ────────────────────── // @@ -3114,68 +3106,65 @@ TEST(XpkgWireAddress, DescriptorWithoutNameFallsBackToDerivation) { // `package.namespace` (SPEC-001 §3.2). The legacy fully-qualified spelling // (`namespace="compat", name="compat.zlib"`) stays accepted for compatibility. -TEST(XpkgNameForm, ShortNameIsCanonical) { +UTest short_name_is_canonical{"XpkgNameForm/ShortNameIsCanonical", [] { // SPEC-001 form: namespace carries the path, name is one segment. - EXPECT_FALSE(mcpp::manifest::xpkg_name_form_violation("chriskohlhoff", "asio") - .has_value()); - EXPECT_FALSE(mcpp::manifest::xpkg_name_form_violation("mcpplibs.capi", "lua") - .has_value()); -} - -TEST(XpkgNameForm, FqnFormIsClean) { - EXPECT_FALSE(mcpp::manifest::xpkg_name_form_violation( - "chriskohlhoff", "chriskohlhoff.asio").has_value()); -} - -TEST(XpkgNameForm, NoNamespaceIsClean) { + expect(!(mcpp::manifest::xpkg_name_form_violation("chriskohlhoff", "asio") + .has_value())); + expect(!(mcpp::manifest::xpkg_name_form_violation("mcpplibs.capi", "lua") + .has_value())); +}}; + +UTest fqn_form_is_clean{"XpkgNameForm/FqnFormIsClean", [] { + expect(!(mcpp::manifest::xpkg_name_form_violation( + "chriskohlhoff", "chriskohlhoff.asio").has_value())); +}}; + +UTest no_namespace_is_clean{"XpkgNameForm/NoNamespaceIsClean", [] { // xim upstream packages declare no namespace and are keyed by the bare // name — legal, and outside INV-NAME's scope. - EXPECT_FALSE(mcpp::manifest::xpkg_name_form_violation("", "opencv") - .has_value()); -} + expect(!(mcpp::manifest::xpkg_name_form_violation("", "opencv") + .has_value())); +}}; -TEST(XpkgNameForm, NoNameIsClean) { +UTest no_name_is_clean{"XpkgNameForm/NoNameIsClean", [] { // Cannot verify → lenient, matching the identity gate's stance. - EXPECT_FALSE(mcpp::manifest::xpkg_name_form_violation("compat", "") - .has_value()); -} + expect(!(mcpp::manifest::xpkg_name_form_violation("compat", "") + .has_value())); +}}; -TEST(XpkgNameForm, CompatAliasIsClean) { +UTest compat_alias_is_clean{"XpkgNameForm/CompatAliasIsClean", [] { // Legacy descriptors repeat the declared namespace in package.name. That // source shape remains valid even though consumers now select compat // explicitly. - EXPECT_FALSE(mcpp::manifest::xpkg_name_form_violation("compat", "compat.gtest") - .has_value()); -} + expect(!(mcpp::manifest::xpkg_name_form_violation("compat", "compat.gtest") + .has_value())); +}}; -TEST(XpkgNameForm, NestedNamespaceIsClean) { - EXPECT_FALSE(mcpp::manifest::xpkg_name_form_violation( - "mcpplibs.capi", "mcpplibs.capi.lua").has_value()); -} +UTest nested_namespace_is_clean{"XpkgNameForm/NestedNamespaceIsClean", [] { + expect(!(mcpp::manifest::xpkg_name_form_violation( + "mcpplibs.capi", "mcpplibs.capi.lua").has_value())); +}}; -TEST(XpkgNameForm, PrefixWithoutShortSegmentIsAViolation) { +UTest prefix_without_short_segment_is_a_violation{"XpkgNameForm/PrefixWithoutShortSegmentIsAViolation", [] { // `name` equal to the namespace itself carries no short segment. - EXPECT_TRUE(mcpp::manifest::xpkg_name_form_violation("compat", "compat.") + expect(mcpp::manifest::xpkg_name_form_violation("compat", "compat.") .has_value()); -} +}}; -TEST(XpkgNameForm, FromLuaAcceptsBothCanonicalAndLegacy) { +UTest from_lua_accepts_both_canonical_and_legacy{"XpkgNameForm/FromLuaAcceptsBothCanonicalAndLegacy", [] { constexpr std::string_view shortLua = R"(package = { namespace = "aimol", name = "tensorvia-cpu" })"; constexpr std::string_view legacyFqnLua = R"(package = { namespace = "aimol", name = "aimol.tensorvia-cpu" })"; // Canonical short-name form and the legacy FQN spelling both resolve to // the same identity, so both are accepted. - EXPECT_FALSE( - mcpp::manifest::xpkg_name_form_violation_from_lua(shortLua).has_value()); - EXPECT_FALSE( - mcpp::manifest::xpkg_name_form_violation_from_lua(legacyFqnLua).has_value()); + expect(!(mcpp::manifest::xpkg_name_form_violation_from_lua(shortLua).has_value())); + expect(!(mcpp::manifest::xpkg_name_form_violation_from_lua(legacyFqnLua).has_value())); // But a short name that still carries a dot is rejected. constexpr std::string_view nonAtomic = R"(package = { namespace = "mcpplibs", name = "capi.lua" })"; - EXPECT_TRUE( - mcpp::manifest::xpkg_name_form_violation_from_lua(nonAtomic).has_value()); -} + expect(mcpp::manifest::xpkg_name_form_violation_from_lua(nonAtomic).has_value()); +}}; // ─── canonical_xpkg_identity — the unified (ns, name) model (§4.2) ─── // @@ -3192,79 +3181,75 @@ mcpp::manifest::XpkgIdentity want(std::string n, std::string s) { } } // namespace -TEST(CanonicalIdentity, PrefixEmbeddedNameCollapses) { +UTest prefix_embedded_name_collapses{"CanonicalIdentity/PrefixEmbeddedNameCollapses", [] { // ns=compat, name=compat.zlib → (compat, zlib) - EXPECT_EQ(id("compat", "compat.zlib"), want("compat", "zlib")); -} + expect(eq(id("compat", "compat.zlib"), want("compat", "zlib"))); +}}; -TEST(CanonicalIdentity, BareNameCombinesWithNamespace) { +UTest bare_name_combines_with_namespace{"CanonicalIdentity/BareNameCombinesWithNamespace", [] { // ns=mcpplibs, name=cmdline → (mcpplibs, cmdline) - EXPECT_EQ(id("mcpplibs", "cmdline"), want("mcpplibs", "cmdline")); -} + expect(eq(id("mcpplibs", "cmdline"), want("mcpplibs", "cmdline"))); +}}; -TEST(CanonicalIdentity, AlreadyQualifiedNameIsIdempotent) { - EXPECT_EQ(id("mcpplibs", "mcpplibs.cmdline"), want("mcpplibs", "cmdline")); -} +UTest already_qualified_name_is_idempotent{"CanonicalIdentity/AlreadyQualifiedNameIsIdempotent", [] { + expect(eq(id("mcpplibs", "mcpplibs.cmdline"), want("mcpplibs", "cmdline"))); +}}; -TEST(CanonicalIdentity, NoNamespaceInheritsOwningIndex) { +UTest no_namespace_inherits_owning_index{"CanonicalIdentity/NoNamespaceInheritsOwningIndex", [] { // ns=∅, name=zlib, index=xim → (xim, zlib) - EXPECT_EQ(id("", "zlib", "xim"), want("xim", "zlib")); - EXPECT_EQ(id("", "tinycfg", "local-dev"), want("local-dev", "tinycfg")); -} + expect(eq(id("", "zlib", "xim"), want("xim", "zlib"))); + expect(eq(id("", "tinycfg", "local-dev"), want("local-dev", "tinycfg"))); +}}; -TEST(CanonicalIdentity, DeclaredNamespaceWinsOverIndexDefault) { - EXPECT_EQ(id("compat", "compat.zlib", "xim"), want("compat", "zlib")); -} +UTest declared_namespace_wins_over_index_default{"CanonicalIdentity/DeclaredNamespaceWinsOverIndexDefault", [] { + expect(eq(id("compat", "compat.zlib", "xim"), want("compat", "zlib"))); +}}; -TEST(CanonicalIdentity, DottedNameWithNoNamespaceIsNotReinterpreted) { +UTest dotted_name_with_no_namespace_is_not_reinterpreted{"CanonicalIdentity/DottedNameWithNoNamespaceIsNotReinterpreted", [] { // SPEC-001: identity is exactly what the descriptor declares. A dotted // `name` with no namespace is NOT silently split into one — that inferred // a namespace nobody wrote. It stays literal here and is rejected by // xpkg_name_form_violation instead. - EXPECT_EQ(id("", "a.b"), want("", "a.b")); - EXPECT_EQ(id("", "x.y.z"), want("", "x.y.z")); - EXPECT_TRUE(mcpp::manifest::xpkg_name_form_violation("", "a.b").has_value()); -} + expect(eq(id("", "a.b"), want("", "a.b"))); + expect(eq(id("", "x.y.z"), want("", "x.y.z"))); + expect(mcpp::manifest::xpkg_name_form_violation("", "a.b").has_value()); +}}; -TEST(CanonicalIdentity, HierarchicalNamespaceIsSupported) { +UTest hierarchical_namespace_is_supported{"CanonicalIdentity/HierarchicalNamespaceIsSupported", [] { // ns=a.b, name=c → (a.b, c) ; name is the single trailing segment. - EXPECT_EQ(id("a.b", "c"), want("a.b", "c")); - EXPECT_EQ(id("mcpplibs.capi", "lua"), want("mcpplibs.capi", "lua")); + expect(eq(id("a.b", "c"), want("a.b", "c"))); + expect(eq(id("mcpplibs.capi", "lua"), want("mcpplibs.capi", "lua"))); // Legacy FQN spelling under the SAME namespace strips the prefix. - EXPECT_EQ(id("mcpplibs.capi", "mcpplibs.capi.lua"), - want("mcpplibs.capi", "lua")); + expect(eq(id("mcpplibs.capi", "mcpplibs.capi.lua"), want("mcpplibs.capi", "lua"))); // But a shorter declared namespace no longer "absorbs" the extra segment: // the descriptor said mcpplibs, so the identity is mcpplibs — the leftover // dot makes it a form violation rather than a silent (mcpplibs.capi, lua). - EXPECT_EQ(id("mcpplibs", "mcpplibs.capi.lua"), want("mcpplibs", "capi.lua")); - EXPECT_TRUE(mcpp::manifest::xpkg_name_form_violation( + expect(eq(id("mcpplibs", "mcpplibs.capi.lua"), want("mcpplibs", "capi.lua"))); + expect(mcpp::manifest::xpkg_name_form_violation( "mcpplibs", "mcpplibs.capi.lua").has_value()); -} +}}; -TEST(CanonicalIdentity, BareNameNoNamespaceNoIndexStaysRootless) { +UTest bare_name_no_namespace_no_index_stays_rootless{"CanonicalIdentity/BareNameNoNamespaceNoIndexStaysRootless", [] { // The incident villain: a foreign bare descriptor with nothing to anchor it. - EXPECT_EQ(id("", "zlib"), want("", "zlib")); -} + expect(eq(id("", "zlib"), want("", "zlib"))); +}}; -TEST(CanonicalIdentity, FromLuaReadsDeclaredFields) { +UTest from_lua_reads_declared_fields{"CanonicalIdentity/FromLuaReadsDeclaredFields", [] { constexpr std::string_view lua = R"(package = { namespace = "compat", name = "compat.zlib" })"; - EXPECT_EQ(mcpp::manifest::canonical_xpkg_identity_from_lua(lua), - want("compat", "zlib")); + expect(eq(mcpp::manifest::canonical_xpkg_identity_from_lua(lua), want("compat", "zlib"))); // No-namespace descriptor + owning index. constexpr std::string_view bare = R"(package = { name = "tinycfg" })"; - EXPECT_EQ(mcpp::manifest::canonical_xpkg_identity_from_lua(bare, "local-dev"), - want("local-dev", "tinycfg")); + expect(eq(mcpp::manifest::canonical_xpkg_identity_from_lua(bare, "local-dev"), want("local-dev", "tinycfg"))); // No declared name → empty identity. constexpr std::string_view noName = R"(package = { version = "1.0" })"; - EXPECT_EQ(mcpp::manifest::canonical_xpkg_identity_from_lua(noName), - want("", "")); -} + expect(eq(mcpp::manifest::canonical_xpkg_identity_from_lua(noName), want("", ""))); +}}; // ── Descriptor grammar v2: Lua long-bracket strings + block comments ── // Design: .agents/docs/2026-07-08-index-version-semantics-and-descriptor-grammar-design.md (D1) -TEST(XpkgLongBracket, GeneratedFilesMultiline) { +UTest generated_files_multiline{"XpkgLongBracket/GeneratedFilesMultiline", [] { // Content carries braces, quotes, an embedded `]]` (level-2 brackets), // and a leading newline that Lua semantics strip. constexpr auto lua = R"( @@ -3285,13 +3270,12 @@ int f() { return "x]]y"[0]; } } )"; auto m = mcpp::manifest::synthesize_from_xpkg_lua(lua, "lbtest", "1.0.0", mcpp::toolchain::platform::HostPlatform::current()); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_TRUE(m->buildConfig.generatedFiles.contains("mcpp_generated/gen.cc")); - EXPECT_EQ(m->buildConfig.generatedFiles["mcpp_generated/gen.cc"], - "export module gen;\nint f() { return \"x]]y\"[0]; }\n"); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(m->buildConfig.generatedFiles.contains("mcpp_generated/gen.cc"))); + expect(eq(m->buildConfig.generatedFiles["mcpp_generated/gen.cc"], "export module gen;\nint f() { return \"x]]y\"[0]; }\n")); +}}; -TEST(XpkgLongBracket, BlockCommentWithBracesAndFakeMcpp) { +UTest block_comment_with_braces_and_fake_mcpp{"XpkgLongBracket/BlockCommentWithBracesAndFakeMcpp", [] { // A block comment containing braces and the literal text `mcpp = {` // must neither corrupt the structural depth count nor false-match // the segment locator. @@ -3311,12 +3295,12 @@ package = { } )"; auto m = mcpp::manifest::synthesize_from_xpkg_lua(lua, "cmt", "1.0.0", mcpp::toolchain::platform::HostPlatform::current()); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->modules.sources.size(), 1u); - EXPECT_EQ(m->modules.sources[0], "src/*.cpp"); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->modules.sources.size(), 1u))); + expect(eq(m->modules.sources[0], "src/*.cpp")); +}}; -TEST(XpkgLongBracket, NoLeadingNewlineKeptVerbatim) { +UTest no_leading_newline_kept_verbatim{"XpkgLongBracket/NoLeadingNewlineKeptVerbatim", [] { // Without a newline after the opening bracket, content starts // immediately; CRLF right after the bracket is stripped as one break. constexpr auto lua = @@ -3331,12 +3315,12 @@ TEST(XpkgLongBracket, NoLeadingNewlineKeptVerbatim) { " },\n" "}\n"; auto m = mcpp::manifest::synthesize_from_xpkg_lua(lua, "nl", "1.0.0", mcpp::toolchain::platform::HostPlatform::current()); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_EQ(m->buildConfig.generatedFiles["g.cc"], "inline"); - EXPECT_EQ(m->buildConfig.generatedFiles["h.cc"], "crlf"); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(eq(m->buildConfig.generatedFiles["g.cc"], "inline")); + expect(eq(m->buildConfig.generatedFiles["h.cc"], "crlf")); +}}; -TEST(XpkgLongBracket, UnknownKeysRecordedSchemaAccepted) { +UTest unknown_keys_recorded_schema_accepted{"XpkgLongBracket/UnknownKeysRecordedSchemaAccepted", [] { constexpr auto lua = R"( package = { spec = "1", @@ -3352,16 +3336,16 @@ package = { } )"; auto m = mcpp::manifest::synthesize_from_xpkg_lua(lua, "uk", "1.0.0", mcpp::toolchain::platform::HostPlatform::current()); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->xpkgUnknownKeys.size(), 2u); - EXPECT_EQ(m->xpkgUnknownKeys[0], "bogus_key"); - EXPECT_EQ(m->xpkgUnknownKeys[1], "future_table"); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->xpkgUnknownKeys.size(), 2u))); + expect(eq(m->xpkgUnknownKeys[0], "bogus_key")); + expect(eq(m->xpkgUnknownKeys[1], "future_table")); +}}; // ── scan_overrides: author-asserted scan results ── // Design: .agents/docs/2026-07-08-scanner-backend-abstraction-design.md §3-pre -TEST(ScanOverrides, XpkgSegmentParses) { +UTest xpkg_segment_parses{"ScanOverrides/XpkgSegmentParses", [] { constexpr auto lua = R"( package = { spec = "1", @@ -3379,17 +3363,17 @@ package = { } )"; auto m = mcpp::manifest::synthesize_from_xpkg_lua(lua, "fmtlib.fmt", "12.2.0", mcpp::toolchain::platform::HostPlatform::current()); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->modules.scanOverrides.size(), 1u); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->modules.scanOverrides.size(), 1u))); auto& ov = m->modules.scanOverrides.at("*/src/fmt.cc"); - ASSERT_EQ(ov.provides.size(), 1u); - EXPECT_EQ(ov.provides[0], "fmt"); - ASSERT_EQ(ov.imports.size(), 1u); - EXPECT_EQ(ov.imports[0], "std"); - EXPECT_TRUE(m->xpkgUnknownKeys.empty()); -} - -TEST(ScanOverrides, XpkgRejectsEmptyDeclaration) { + expect(fatal(eq(ov.provides.size(), 1u))); + expect(eq(ov.provides[0], "fmt")); + expect(fatal(eq(ov.imports.size(), 1u))); + expect(eq(ov.imports[0], "std")); + expect(m->xpkgUnknownKeys.empty()); +}}; + +UTest xpkg_rejects_empty_declaration{"ScanOverrides/XpkgRejectsEmptyDeclaration", [] { constexpr auto lua = R"( package = { spec = "1", @@ -3403,12 +3387,11 @@ package = { } )"; auto m = mcpp::manifest::synthesize_from_xpkg_lua(lua, "so", "1.0.0", mcpp::toolchain::platform::HostPlatform::current()); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().message.find("neither provides nor imports"), - std::string::npos) << m.error().format(); -} + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().message.find("neither provides nor imports"), std::string::npos)); !failed) failed << m.error().format(); +}}; -TEST(ScanOverrides, TomlParses) { +UTest toml_parses{"ScanOverrides/TomlParses", [] { constexpr auto src = R"( [package] name = "app" @@ -3425,19 +3408,19 @@ kind = "bin" main = "src/main.cpp" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->modules.scanOverrides.size(), 1u); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->modules.scanOverrides.size(), 1u))); auto& ov = m->modules.scanOverrides.at("vendor/fmt.cc"); - EXPECT_EQ(ov.provides, std::vector{"fmt"}); - EXPECT_EQ(ov.imports, std::vector{"std"}); -} + expect(eq(ov.provides, std::vector{"fmt"})); + expect(eq(ov.imports, std::vector{"std"})); +}}; // ── declarative source-set parity: mcpp.toml ↔ xpkg descriptor ─────────────── // One data model, two grammars (manifest.cppm): what a descriptor can express // through `features..sources` / `generated_files`, mcpp.toml must express // identically. This test locks the symmetry as a contract. -TEST(Manifest, FeatureSourcesParseFromToml) { +UTest feature_sources_parse_from_toml{"Manifest/FeatureSourcesParseFromToml", [] { constexpr auto src = R"( [package] name = "sym" @@ -3447,14 +3430,13 @@ default = [] compiled = { sources = ["src/impl/**"], defines = ["SYM_COMPILED"] } )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_TRUE(m->buildConfig.featureSources.contains("compiled")); - EXPECT_EQ(m->buildConfig.featureSources.at("compiled"), - (std::vector{"src/impl/**"})); - ASSERT_TRUE(m->buildConfig.featureDefines.contains("compiled")); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(m->buildConfig.featureSources.contains("compiled"))); + expect(eq(m->buildConfig.featureSources.at("compiled"), (std::vector{"src/impl/**"}))); + expect(fatal(m->buildConfig.featureDefines.contains("compiled"))); +}}; -TEST(Manifest, GeneratedFilesParseFromToml) { +UTest generated_files_parse_from_toml{"Manifest/GeneratedFilesParseFromToml", [] { constexpr auto src = R"( [package] name = "genf" @@ -3467,15 +3449,15 @@ export module wrap; """ )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->buildConfig.generatedFiles.size(), 1u); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->buildConfig.generatedFiles.size(), 1u))); auto it = m->buildConfig.generatedFiles.find( std::filesystem::path("src/gen/wrap.cppm")); - ASSERT_NE(it, m->buildConfig.generatedFiles.end()); - EXPECT_EQ(it->second, "module;\nexport module wrap;\n"); -} + expect(fatal(ne(it, m->buildConfig.generatedFiles.end()))); + expect(eq(it->second, "module;\nexport module wrap;\n")); +}}; -TEST(Manifest, GeneratedFilesRejectEscapingPaths) { +UTest generated_files_reject_escaping_paths{"Manifest/GeneratedFilesRejectEscapingPaths", [] { for (auto path : { "\"../evil.cpp\"", "\"/abs/evil.cpp\"", "\"a/../../evil.cpp\"" }) { auto src = std::format(R"( [package] @@ -3485,11 +3467,11 @@ version = "1.0.0" {} = "x" )", path); auto m = mcpp::manifest::parse_string(src); - EXPECT_FALSE(m.has_value()) << "path accepted: " << path; + expect(!m.has_value()) << "path accepted: " << path; } -} +}}; -TEST(Manifest, TomlAndXpkgFeatureSourcesAreSymmetric) { +UTest toml_and_xpkg_feature_sources_are_symmetric{"Manifest/TomlAndXpkgFeatureSourcesAreSymmetric", [] { constexpr auto toml = R"( [package] name = "sym" @@ -3512,16 +3494,16 @@ mcpp = { } )"; auto mt = mcpp::manifest::parse_string(toml); - ASSERT_TRUE(mt.has_value()) << mt.error().format(); + if (auto&& failed = expect(fatal(mt.has_value())); !failed) failed << mt.error().format(); auto mx = mcpp::manifest::synthesize_from_xpkg_lua(lua, "sym", "1.0.0", mcpp::toolchain::platform::HostPlatform::current()); - ASSERT_TRUE(mx.has_value()) << mx.error().format(); + if (auto&& failed = expect(fatal(mx.has_value())); !failed) failed << mx.error().format(); - EXPECT_EQ(mt->buildConfig.featureSources, mx->buildConfig.featureSources); - EXPECT_EQ(mt->buildConfig.featureDefines, mx->buildConfig.featureDefines); - EXPECT_EQ(mt->buildConfig.generatedFiles, mx->buildConfig.generatedFiles); -} + expect(eq(mt->buildConfig.featureSources, mx->buildConfig.featureSources)); + expect(eq(mt->buildConfig.featureDefines, mx->buildConfig.featureDefines)); + expect(eq(mt->buildConfig.generatedFiles, mx->buildConfig.generatedFiles)); +}}; -TEST(Manifest, CfgConditionalSourcesParseFromToml) { +UTest cfg_conditional_sources_parse_from_toml{"Manifest/CfgConditionalSourcesParseFromToml", [] { constexpr auto src = R"( [package] name = "condsrc" @@ -3532,16 +3514,16 @@ sources = ["src/x86/**/*.asm", "!src/x86/legacy/**"] cxxflags = ["-DHAVE_X86=1"] )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->conditionalConfigs.size(), 1u); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->conditionalConfigs.size(), 1u))); auto& cc = m->conditionalConfigs[0]; - EXPECT_EQ(cc.predicate, "cfg(arch = \"x86_64\")"); - EXPECT_EQ(cc.inputs.sources, (std::vector{ - "src/x86/**/*.asm", "!src/x86/legacy/**"})); - EXPECT_EQ(cc.inputs.cxxflags, (std::vector{"-DHAVE_X86=1"})); -} + expect(eq(cc.predicate, "cfg(arch = \"x86_64\")")); + expect(eq(cc.inputs.sources, (std::vector{ + "src/x86/**/*.asm", "!src/x86/legacy/**"}))); + expect(eq(cc.inputs.cxxflags, (std::vector{"-DHAVE_X86=1"}))); +}}; -TEST(Manifest, TargetCfgParsesFromXpkgSymmetrically) { +UTest target_cfg_parses_from_xpkg_symmetrically{"Manifest/TargetCfgParsesFromXpkgSymmetrically", [] { constexpr auto lua = R"( package = { name = "condsrc" } mcpp = { @@ -3558,17 +3540,15 @@ mcpp = { } )"; auto mx = mcpp::manifest::synthesize_from_xpkg_lua(lua, "condsrc", "1.0.0", mcpp::toolchain::platform::HostPlatform::current()); - ASSERT_TRUE(mx.has_value()) << mx.error().format(); - ASSERT_EQ(mx->conditionalConfigs.size(), 2u); - EXPECT_EQ(mx->conditionalConfigs[0].predicate, "cfg(arch = \"x86_64\")"); - EXPECT_EQ(mx->conditionalConfigs[0].inputs.sources, - (std::vector{"src/x86/**/*.asm"})); - EXPECT_EQ(mx->conditionalConfigs[1].predicate, "cfg(windows)"); - EXPECT_EQ(mx->conditionalConfigs[1].inputs.ldflags, - (std::vector{"-lws2_32"})); -} + if (auto&& failed = expect(fatal(mx.has_value())); !failed) failed << mx.error().format(); + expect(fatal(eq(mx->conditionalConfigs.size(), 2u))); + expect(eq(mx->conditionalConfigs[0].predicate, "cfg(arch = \"x86_64\")")); + expect(eq(mx->conditionalConfigs[0].inputs.sources, (std::vector{"src/x86/**/*.asm"}))); + expect(eq(mx->conditionalConfigs[1].predicate, "cfg(windows)")); + expect(eq(mx->conditionalConfigs[1].inputs.ldflags, (std::vector{"-lws2_32"}))); +}}; -TEST(Manifest, PerGlobFlagsParseOrderedFromToml) { +UTest per_glob_flags_parse_ordered_from_toml{"Manifest/PerGlobFlagsParseOrderedFromToml", [] { constexpr auto src = R"( [package] name = "globf" @@ -3582,18 +3562,18 @@ flags = [ ] )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); auto& gfs = m->buildConfig.globFlags; - ASSERT_EQ(gfs.size(), 3u); + expect(fatal(eq(gfs.size(), 3u))); // Declaration order IS the data (override semantics ride on it). - EXPECT_EQ(gfs[0].glob, "third_party/**"); - EXPECT_EQ(gfs[0].cxxflags, (std::vector{"-w"})); - EXPECT_EQ(gfs[1].glob, "src/simd/**/*.avx2.cpp"); - EXPECT_EQ(gfs[1].defines, (std::vector{"HAVE_AVX2"})); - EXPECT_EQ(gfs[2].asmflags, (std::vector{"-DPIC"})); -} - -TEST(Manifest, PerGlobFlagsRejectUnknownKeysAndMissingGlob) { + expect(eq(gfs[0].glob, "third_party/**")); + expect(eq(gfs[0].cxxflags, (std::vector{"-w"}))); + expect(eq(gfs[1].glob, "src/simd/**/*.avx2.cpp")); + expect(eq(gfs[1].defines, (std::vector{"HAVE_AVX2"}))); + expect(eq(gfs[2].asmflags, (std::vector{"-DPIC"}))); +}}; + +UTest per_glob_flags_reject_unknown_keys_and_missing_glob{"Manifest/PerGlobFlagsRejectUnknownKeysAndMissingGlob", [] { auto bad1 = mcpp::manifest::parse_string(R"( [package] name = "globf" @@ -3601,7 +3581,7 @@ version = "1.0.0" [build] flags = [{ glob = "src/**", ldflags = ["-lfoo"] }] )"); - EXPECT_FALSE(bad1.has_value()); // ldflags is not per-TU — closed grammar + expect(!bad1.has_value()); // ldflags is not per-TU — closed grammar auto bad2 = mcpp::manifest::parse_string(R"( [package] @@ -3610,10 +3590,10 @@ version = "1.0.0" [build] flags = [{ cxxflags = ["-w"] }] )"); - EXPECT_FALSE(bad2.has_value()); // missing glob -} + expect(!bad2.has_value()); // missing glob +}}; -TEST(Manifest, PerGlobFlagsParseFromXpkgSymmetrically) { +UTest per_glob_flags_parse_from_xpkg_symmetrically{"Manifest/PerGlobFlagsParseFromXpkgSymmetrically", [] { constexpr auto lua = R"( package = { name = "globf" } mcpp = { @@ -3625,16 +3605,14 @@ mcpp = { } )"; auto mx = mcpp::manifest::synthesize_from_xpkg_lua(lua, "globf", "1.0.0", mcpp::toolchain::platform::HostPlatform::current()); - ASSERT_TRUE(mx.has_value()) << mx.error().format(); - ASSERT_EQ(mx->buildConfig.globFlags.size(), 2u); - EXPECT_EQ(mx->buildConfig.globFlags[0].glob, "third_party/**"); - EXPECT_EQ(mx->buildConfig.globFlags[1].cxxflags, - (std::vector{"-mavx2"})); - EXPECT_EQ(mx->buildConfig.globFlags[1].defines, - (std::vector{"HAVE_AVX2"})); -} - -TEST(Manifest, AllowsBuildFlagsArrayOfTablesSyntax) { + if (auto&& failed = expect(fatal(mx.has_value())); !failed) failed << mx.error().format(); + expect(fatal(eq(mx->buildConfig.globFlags.size(), 2u))); + expect(eq(mx->buildConfig.globFlags[0].glob, "third_party/**")); + expect(eq(mx->buildConfig.globFlags[1].cxxflags, (std::vector{"-mavx2"}))); + expect(eq(mx->buildConfig.globFlags[1].defines, (std::vector{"HAVE_AVX2"}))); +}}; + +UTest allows_build_flags_array_of_tables_syntax{"Manifest/AllowsBuildFlagsArrayOfTablesSyntax", [] { // `[[build.flags]]` header form must still parse at the manifest layer // (not just the lower libs/toml layer) — the closed-grammar guard must // allowlist exactly this path. @@ -3652,14 +3630,14 @@ glob = "src/x86/**/*.asm" asmflags = ["-DPIC"] )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); auto& gfs = m->buildConfig.globFlags; - ASSERT_EQ(gfs.size(), 2u); - EXPECT_EQ(gfs[0].glob, "third_party/**"); - EXPECT_EQ(gfs[0].cxxflags, (std::vector{"-w"})); - EXPECT_EQ(gfs[1].glob, "src/x86/**/*.asm"); - EXPECT_EQ(gfs[1].asmflags, (std::vector{"-DPIC"})); -} + expect(fatal(eq(gfs.size(), 2u))); + expect(eq(gfs[0].glob, "third_party/**")); + expect(eq(gfs[0].cxxflags, (std::vector{"-w"}))); + expect(eq(gfs[1].glob, "src/x86/**/*.asm")); + expect(eq(gfs[1].asmflags, (std::vector{"-DPIC"}))); +}}; // Closed-grammar guard (review fix on #227/#228): `[[dependencies]]` is a // doubled-bracket typo for `[dependencies]`. Before this guard it parsed @@ -3667,7 +3645,7 @@ asmflags = ["-DPIC"] // [dependencies] via get_table(), which returns nullptr for a non-table // Value, so the whole section (all dependencies) silently vanished with no // error and no warning. It must now be a loud parse error. -TEST(Manifest, RejectsArrayOfTablesTypoOnDependencies) { +UTest rejects_array_of_tables_typo_on_dependencies{"Manifest/RejectsArrayOfTablesTypoOnDependencies", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "x" @@ -3676,16 +3654,14 @@ version = "0.1.0" [[dependencies]] gtest = "1.15.2" )"); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().message.find("array-of-tables"), std::string::npos) - << m.error().message; - EXPECT_NE(m.error().message.find("dependencies"), std::string::npos) - << m.error().message; -} + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().message.find("array-of-tables"), std::string::npos)); !failed) failed << m.error().message; + if (auto&& failed = expect(ne(m.error().message.find("dependencies"), std::string::npos)); !failed) failed << m.error().message; +}}; // Same guard, a different non-allowlisted path — confirms the check isn't // hardcoded to just "dependencies". -TEST(Manifest, RejectsArrayOfTablesTypoOnToolchain) { +UTest rejects_array_of_tables_typo_on_toolchain{"Manifest/RejectsArrayOfTablesTypoOnToolchain", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "x" @@ -3694,17 +3670,16 @@ version = "0.1.0" [[toolchain]] linux = "gcc@15.1.0" )"); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().message.find("array-of-tables"), std::string::npos) - << m.error().message; -} + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().message.find("array-of-tables"), std::string::npos)); !failed) failed << m.error().message; +}}; // #258: `[target.'cfg()'.build]` accepts the same per-glob `flags` array // `[build]` does. Before this, the conditional reader knew exactly four keys // and dropped everything else silently, so a per-OS flag table was // inexpressible in mcpp.toml while the xpkg descriptor's mcpp. sections // supported it — manifest-built packages were second-class. -TEST(Manifest, ConditionalSectionCarriesPerGlobFlags) { +UTest conditional_section_carries_per_glob_flags{"Manifest/ConditionalSectionCarriesPerGlobFlags", [] { auto tmp = std::filesystem::temp_directory_path() / "mcpp_cond_flags"; std::filesystem::create_directories(tmp); auto path = tmp / "mcpp.toml"; @@ -3727,30 +3702,28 @@ flags = [ )"; } auto m = mcpp::manifest::load(path); - ASSERT_TRUE(m) << (m ? "" : m.error().message); + if (auto&& failed = expect(fatal(bool(m))); !failed) failed << (m ? "" : m.error().message); // Base table is unconditional. - ASSERT_EQ(m->buildConfig.globFlags.size(), 1u); - EXPECT_EQ(m->buildConfig.globFlags[0].glob, "src/zlib/**"); + expect(fatal(eq(m->buildConfig.globFlags.size(), 1u))); + expect(eq(m->buildConfig.globFlags[0].glob, "src/zlib/**")); - ASSERT_EQ(m->conditionalConfigs.size(), 1u); + expect(fatal(eq(m->conditionalConfigs.size(), 1u))); auto& cc = m->conditionalConfigs[0]; - EXPECT_EQ(cc.predicate, "cfg(windows)"); - ASSERT_EQ(cc.inputs.globFlags.size(), 1u); - EXPECT_EQ(cc.inputs.globFlags[0].glob, "src/zlib/**"); - EXPECT_EQ(cc.inputs.globFlags[0].defines, - (std::vector{"NO_FSEEKO"})); + expect(eq(cc.predicate, "cfg(windows)")); + expect(fatal(eq(cc.inputs.globFlags.size(), 1u))); + expect(eq(cc.inputs.globFlags[0].glob, "src/zlib/**")); + expect(eq(cc.inputs.globFlags[0].defines, (std::vector{"NO_FSEEKO"}))); // The removal case: a windows-only -U that must land AFTER the base -D. - EXPECT_EQ(cc.inputs.globFlags[0].cflags, - (std::vector{"-UHAVE_UNISTD_H"})); - ASSERT_EQ(cc.inputs.includeDirs.size(), 1u); - EXPECT_EQ(cc.inputs.includeDirs[0], std::filesystem::path("vendor/win/include")); + expect(eq(cc.inputs.globFlags[0].cflags, (std::vector{"-UHAVE_UNISTD_H"}))); + expect(fatal(eq(cc.inputs.includeDirs.size(), 1u))); + expect(eq(cc.inputs.includeDirs[0], std::filesystem::path("vendor/win/include"))); std::filesystem::remove_all(tmp); -} +}}; // The array-of-tables spelling must reach the same place, and must be // allowlisted by the #227 closed-grammar guard. -TEST(Manifest, ConditionalPerGlobFlagsAcceptArrayOfTablesSpelling) { +UTest conditional_per_glob_flags_accept_array_of_tables_spelling{"Manifest/ConditionalPerGlobFlagsAcceptArrayOfTablesSpelling", [] { auto tmp = std::filesystem::temp_directory_path() / "mcpp_cond_flags_aot"; std::filesystem::create_directories(tmp); auto path = tmp / "mcpp.toml"; @@ -3767,20 +3740,19 @@ defines = ["ON_LINUX=1"] )"; } auto m = mcpp::manifest::load(path); - ASSERT_TRUE(m) << (m ? "" : m.error().message); - ASSERT_EQ(m->conditionalConfigs.size(), 1u); - ASSERT_EQ(m->conditionalConfigs[0].inputs.globFlags.size(), 1u); - EXPECT_EQ(m->conditionalConfigs[0].inputs.globFlags[0].defines, - (std::vector{"ON_LINUX=1"})); + if (auto&& failed = expect(fatal(bool(m))); !failed) failed << (m ? "" : m.error().message); + expect(fatal(eq(m->conditionalConfigs.size(), 1u))); + expect(fatal(eq(m->conditionalConfigs[0].inputs.globFlags.size(), 1u))); + expect(eq(m->conditionalConfigs[0].inputs.globFlags[0].defines, (std::vector{"ON_LINUX=1"}))); std::filesystem::remove_all(tmp); -} +}}; // Parity: the xpkg descriptor's target_cfg gets the same key, and its // unknown-key policy stays a HARD ERROR. Sharing a parser must not be read // as licence to relax a grammar that was already closed (#263 tracks making // the policies consistent across sections; this test pins that #258 did not // quietly change one). -TEST(Manifest, XpkgTargetCfgCarriesPerGlobFlagsAndStillRejectsUnknownKeys) { +UTest xpkg_target_cfg_carries_per_glob_flags_and_still_rejects_unknown_keys{"Manifest/XpkgTargetCfgCarriesPerGlobFlagsAndStillRejectsUnknownKeys", [] { // Custom delimiter: the descriptor contains `)"` inside ["cfg(windows)"], // which would terminate a plain R"( ... )". const char* lua = R"LUA( @@ -3795,10 +3767,10 @@ mcpp = { } )LUA"; auto m = mcpp::manifest::synthesize_from_xpkg_lua(lua, "p", "1.0.0", mcpp::toolchain::platform::HostPlatform::current()); - ASSERT_TRUE(m) << (m ? "" : m.error().message); - ASSERT_EQ(m->conditionalConfigs.size(), 1u); - ASSERT_EQ(m->conditionalConfigs[0].inputs.globFlags.size(), 1u); - EXPECT_EQ(m->conditionalConfigs[0].inputs.globFlags[0].glob, "src/z/**"); + if (auto&& failed = expect(fatal(bool(m))); !failed) failed << (m ? "" : m.error().message); + expect(fatal(eq(m->conditionalConfigs.size(), 1u))); + expect(fatal(eq(m->conditionalConfigs[0].inputs.globFlags.size(), 1u))); + expect(eq(m->conditionalConfigs[0].inputs.globFlags[0].glob, "src/z/**")); const char* bad = R"LUA( mcpp = { @@ -3807,8 +3779,8 @@ mcpp = { } )LUA"; auto bm = mcpp::manifest::synthesize_from_xpkg_lua(bad, "p", "1.0.0", mcpp::toolchain::platform::HostPlatform::current()); - EXPECT_FALSE(bm) << "unknown target_cfg key must stay a hard error"; -} + expect(!bm) << "unknown target_cfg key must stay a hard error"; +}}; // #254: the host and target platform axes are distinct types, so an API // states which one it wants and a call site must name the one it supplies. @@ -3816,7 +3788,7 @@ mcpp = { // which is exactly how every xpkg per-OS splice ended up keyed on a // compile-time HOST constant while [target.'cfg(...)'] was evaluated against // the resolved TARGET. -TEST(PlatformAxis, HostAndTargetAreNotInterchangeable) { +UTest host_and_target_are_not_interchangeable{"PlatformAxis/HostAndTargetAreNotInterchangeable", [] { using mcpp::toolchain::platform::HostPlatform; using mcpp::toolchain::platform::TargetPlatform; @@ -3831,20 +3803,20 @@ TEST(PlatformAxis, HostAndTargetAreNotInterchangeable) { // The triple vocabulary says "macos"; xpkg descriptors say "macosx". // That translation belongs to the axis, not to every call site. - EXPECT_EQ(TargetPlatform::for_os("macos").key(), "macosx"); - EXPECT_EQ(TargetPlatform::for_os("macosx").key(), "macosx"); - EXPECT_EQ(TargetPlatform::for_os("windows").key(), "windows"); - EXPECT_EQ(TargetPlatform::for_os("linux").key(), "linux"); + expect(eq(TargetPlatform::for_os("macos").key(), "macosx")); + expect(eq(TargetPlatform::for_os("macosx").key(), "macosx")); + expect(eq(TargetPlatform::for_os("windows").key(), "windows")); + expect(eq(TargetPlatform::for_os("linux").key(), "linux")); // A token naming none of the three reads its own block, never the // host's (design 2026-10-29 §5.1, #851). - EXPECT_EQ(TargetPlatform::for_os("emscripten").key(), "emscripten"); - EXPECT_EQ(TargetPlatform::for_os("none").key(), "none"); -} + expect(eq(TargetPlatform::for_os("emscripten").key(), "emscripten")); + expect(eq(TargetPlatform::for_os("none").key(), "none")); +}}; // The behaviour half: a descriptor's per-OS section must be spliced for the // TARGET. Native builds cannot observe this (host == target), which is why // three-platform CI never caught it. -TEST(Manifest, XpkgPerOsSectionSplicesForTheTargetNotTheHost) { +UTest xpkg_per_os_section_splices_for_the_target_not_the_host{"Manifest/XpkgPerOsSectionSplicesForTheTargetNotTheHost", [] { const char* lua = R"LUA( mcpp = { sources = { "base.cpp" }, @@ -3864,23 +3836,23 @@ mcpp = { {"windows", "windows_only.cpp"}}) { auto m = mcpp::manifest::synthesize_from_xpkg_lua( lua, "p", "1.0.0", mcpp::toolchain::platform::TargetPlatform::for_os(tripleOs)); - ASSERT_TRUE(m) << (m ? "" : m.error().message); - EXPECT_TRUE(has(*m, expected)) + if (auto&& failed = expect(fatal(bool(m))); !failed) failed << (m ? "" : m.error().message); + expect(bool(has(*m, expected))) << "target os " << tripleOs << " must splice its own section"; - EXPECT_TRUE(has(*m, "base.cpp")); + expect(bool(has(*m, "base.cpp"))); // and must NOT carry the other legs for (std::string_view other : {"linux_only.cpp", "macos_only.cpp", "windows_only.cpp"}) { if (other == expected) continue; - EXPECT_FALSE(has(*m, other)) + expect(!has(*m, other)) << "target os " << tripleOs << " must not carry " << other; } } -} +}}; // Zero behaviour change for native builds is the safety net for #254: when // host == target the splice must be byte-identical to what it always was. -TEST(Manifest, NativeBuildSpliceIsUnchanged) { +UTest native_build_splice_is_unchanged{"Manifest/NativeBuildSpliceIsUnchanged", [] { const char* lua = R"LUA( mcpp = { sources = { "base.cpp" }, @@ -3895,13 +3867,13 @@ mcpp = { lua, "p", "1.0.0", mcpp::toolchain::platform::TargetPlatform::for_lint_of( mcpp::toolchain::platform::HostPlatform::current().key())); - ASSERT_TRUE(host); - ASSERT_TRUE(nativeTarget); - EXPECT_EQ(host->modules.sources, nativeTarget->modules.sources); - EXPECT_EQ(host->buildConfig.cxxflags, nativeTarget->buildConfig.cxxflags); -} + expect(fatal(bool(host))); + expect(fatal(bool(nativeTarget))); + expect(eq(host->modules.sources, nativeTarget->modules.sources)); + expect(eq(host->buildConfig.cxxflags, nativeTarget->buildConfig.cxxflags)); +}}; -TEST(Manifest, IndicesArtifactAndSourceParsedAndPinSuppresses) { +UTest indices_artifact_and_source_parsed_and_pin_suppresses{"Manifest/IndicesArtifactAndSourceParsedAndPinSuppresses", [] { // #269: [indices] entries may declare an xlings >= 0.4.68 artifact // source. rev/tag/branch pins force git (the artifact channel only // tracks the latest pointer), which artifact_applicable() encodes. @@ -3918,22 +3890,22 @@ fast = { url = "https://example.com/i.git", artifact = "https://example.com/re pinned = { url = "https://example.com/i.git", artifact = "https://example.com/res/i", rev = "abc123" } )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); auto& fast = m->indices.at("fast"); - EXPECT_EQ(fast.artifact, "https://example.com/res/i"); - EXPECT_EQ(fast.source, "auto"); - EXPECT_TRUE(fast.artifact_applicable()); + expect(eq(fast.artifact, "https://example.com/res/i")); + expect(eq(fast.source, "auto")); + expect(bool(fast.artifact_applicable())); auto& pinned = m->indices.at("pinned"); - EXPECT_EQ(pinned.artifact, "https://example.com/res/i"); - EXPECT_FALSE(pinned.artifact_applicable()); -} + expect(eq(pinned.artifact, "https://example.com/res/i")); + expect(!pinned.artifact_applicable()); +}}; // #359: `reexport = true` on a dependency edge — the knob that lets a library // hand its build-time provisions (tools, host module, dependency dir) to its // own consumers. Off by default, deliberately: the edge's `visibility` already // defaults to "public", so riding that would have made every dependency at // every depth able to push entries into a consumer's tool namespace silently. -TEST(Manifest, DependencyReexportIsOptIn) { +UTest dependency_reexport_is_opt_in{"Manifest/DependencyReexportIsOptIn", [] { auto tmp = std::filesystem::temp_directory_path() / "mcpp_dep_reexport"; std::filesystem::create_directories(tmp); auto path = tmp / "mcpp.toml"; @@ -3950,7 +3922,7 @@ zlib = { version = "1.3.1", tools = ["minigzip"] } )"; } auto m = mcpp::manifest::load(path); - ASSERT_TRUE(m) << (m ? "" : m.error().message); + if (auto&& failed = expect(fatal(bool(m))); !failed) failed << (m ? "" : m.error().message); const mcpp::pm::DependencySpec* pb = nullptr; const mcpp::pm::DependencySpec* zl = nullptr; @@ -3958,11 +3930,11 @@ zlib = { version = "1.3.1", tools = ["minigzip"] } if (spec.shortName == "protobuf") pb = &spec; if (spec.shortName == "zlib") zl = &spec; } - ASSERT_NE(pb, nullptr); - ASSERT_NE(zl, nullptr); - EXPECT_TRUE(pb->reexport); - EXPECT_FALSE(zl->reexport); -} + expect(fatal(ne(pb, nullptr))); + expect(fatal(ne(zl, nullptr))); + expect(bool(pb->reexport)); + expect(!zl->reexport); +}}; // #359 D3a: the conditional channel carried three of the four dependency maps // and silently lacked `feature-deps`. A library that puts a host tool behind a @@ -3973,7 +3945,7 @@ zlib = { version = "1.3.1", tools = ["minigzip"] } // The FEATURE is registered regardless of the predicate — only what it pulls // in is conditional — so requesting it on a non-matching platform is not an // unknown-feature error. -TEST(Manifest, ConditionalFeatureDepsAreParsedAndTheFeatureIsRegistered) { +UTest conditional_feature_deps_are_parsed_and_the_feature_is_registered{"Manifest/ConditionalFeatureDepsAreParsedAndTheFeatureIsRegistered", [] { auto tmp = std::filesystem::temp_directory_path() / "mcpp_cond_feature_deps"; std::filesystem::create_directories(tmp); auto path = tmp / "mcpp.toml"; @@ -3989,24 +3961,24 @@ version = "0.1.0" )"; } auto m = mcpp::manifest::load(path); - ASSERT_TRUE(m) << (m ? "" : m.error().message); + if (auto&& failed = expect(fatal(bool(m))); !failed) failed << (m ? "" : m.error().message); - EXPECT_TRUE(m->featuresMap.contains("codegen")); + expect(m->featuresMap.contains("codegen")); // Not folded into the unconditional map: that happens in prepare, after // the predicate is evaluated against the RESOLVED target. - EXPECT_TRUE(m->featureDeps.empty()); + expect(m->featureDeps.empty()); - ASSERT_EQ(m->conditionalConfigs.size(), 1u); + expect(fatal(eq(m->conditionalConfigs.size(), 1u))); auto const& cc = m->conditionalConfigs[0]; - EXPECT_EQ(cc.predicate, "cfg(not(windows))"); - ASSERT_TRUE(cc.featureDeps.contains("codegen")); - ASSERT_EQ(cc.featureDeps.at("codegen").size(), 1u); + expect(eq(cc.predicate, "cfg(not(windows))")); + expect(fatal(cc.featureDeps.contains("codegen"))); + expect(fatal(eq(cc.featureDeps.at("codegen").size(), 1u))); auto const& spec = cc.featureDeps.at("codegen").begin()->second; - EXPECT_EQ(spec.shortName, "protobuf"); - ASSERT_EQ(spec.tools.size(), 1u); - EXPECT_EQ(spec.tools[0], "protoc"); - EXPECT_TRUE(spec.reexport); -} + expect(eq(spec.shortName, "protobuf")); + expect(fatal(eq(spec.tools.size(), 1u))); + expect(eq(spec.tools[0], "protoc")); + expect(bool(spec.reexport)); +}}; // A dependency-spec key must be listed in TWO places in the TOML reader: the // predicate that tells an inline spec from a nested namespace table, and the @@ -4017,7 +3989,7 @@ version = "0.1.0" // // One manifest using every key at once is the cheapest way to keep the two in // sync: a key added to the filler but not the predicate fails this test. -TEST(Manifest, EveryDependencySpecKeyIsAccepted) { +UTest every_dependency_spec_key_is_accepted{"Manifest/EveryDependencySpecKeyIsAccepted", [] { auto tmp = std::filesystem::temp_directory_path() / "mcpp_dep_spec_keys"; std::filesystem::create_directories(tmp); auto path = tmp / "mcpp.toml"; @@ -4035,39 +4007,37 @@ bypath = { path = "../sibling" } )"; } auto m = mcpp::manifest::load(path); - ASSERT_TRUE(m) << (m ? "" : m.error().message); + if (auto&& failed = expect(fatal(bool(m))); !failed) failed << (m ? "" : m.error().message); const mcpp::pm::DependencySpec* all = nullptr; for (auto const& [k, spec] : m->dependencies) if (spec.shortName == "everything") all = &spec; - ASSERT_NE(all, nullptr); - EXPECT_EQ(all->version, "1.0.0"); - EXPECT_EQ(all->visibility, "private"); - EXPECT_FALSE(all->defaultFeatures); - EXPECT_TRUE(all->hostModule); - EXPECT_TRUE(all->reexport); - ASSERT_EQ(all->tools.size(), 1u); - EXPECT_EQ(all->tools[0], "t"); - ASSERT_EQ(all->artifacts.size(), 1u); - EXPECT_EQ(all->artifacts[0], "a"); + expect(fatal(ne(all, nullptr))); + expect(eq(all->version, "1.0.0")); + expect(eq(all->visibility, "private")); + expect(!all->defaultFeatures); + expect(bool(all->hostModule)); + expect(bool(all->reexport)); + expect(fatal(eq(all->tools.size(), 1u))); + expect(eq(all->tools[0], "t")); + expect(fatal(eq(all->artifacts.size(), 1u))); + expect(eq(all->artifacts[0], "a")); // `backend = "openblas"` is sugar for requesting the backend- feature. - EXPECT_NE(std::find(all->features.begin(), all->features.end(), "backend-openblas"), - all->features.end()); - EXPECT_EQ(all->linkage, "shared"); + expect(ne(std::find(all->features.begin(), all->features.end(), "backend-openblas"), all->features.end())); + expect(eq(all->linkage, "shared")); // design 2026-11-02 §14.1: a patch is named relative to the manifest that // carries it, and read as an absolute path. - ASSERT_EQ(all->patches.size(), 1u); - EXPECT_EQ(std::filesystem::path(all->patches[0]), - (tmp / "p" / "one.patch").lexically_normal()); - EXPECT_TRUE(all->embed); - ASSERT_EQ(all->exclude.size(), 1u); - EXPECT_EQ(all->exclude[0], "src/prim.c"); -} + expect(fatal(eq(all->patches.size(), 1u))); + expect(eq(std::filesystem::path(all->patches[0]), (tmp / "p" / "one.patch").lexically_normal())); + expect(bool(all->embed)); + expect(fatal(eq(all->exclude.size(), 1u))); + expect(eq(all->exclude[0], "src/prim.c")); +}}; // design 2026-11-02 §14.1: `exclude` without `embed` would change what a // library built on its own compiles for every consumer, which is a patch's to // state; the manifest is refused, naming the edge. -TEST(Manifest, ExcludeWithoutEmbedIsRefused) { +UTest exclude_without_embed_is_refused{"Manifest/ExcludeWithoutEmbedIsRefused", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "x" @@ -4076,10 +4046,9 @@ version = "0.1.0" [dependencies] "compat.zlib" = { version = "1.3.2", exclude = ["gzlib.c"] } )"); - ASSERT_FALSE(m); - EXPECT_NE(m.error().message.find("exclude is allowed only with embed"), std::string::npos) - << m.error().message; -} + expect(fatal(!m)); + if (auto&& failed = expect(ne(m.error().message.find("exclude is allowed only with embed"), std::string::npos)); !failed) failed << m.error().message; +}}; // ── #519: the dependency-form axis ──────────────────────────────────────── @@ -4097,17 +4066,17 @@ load_inline(std::string_view name, std::string_view body) { } // namespace -TEST(Manifest, DependencyLinkageDefaultsToEmptyMeaningStatic) { +UTest dependency_linkage_defaults_to_empty_meaning_static{"Manifest/DependencyLinkageDefaultsToEmptyMeaningStatic", [] { auto m = load_inline("deplinkage_default", R"( [package] name = "x" version = "0.1.0" )"); - ASSERT_TRUE(m); - EXPECT_TRUE(m->buildConfig.dependencyLinkage.empty()); -} + expect(fatal(bool(m))); + expect(m->buildConfig.dependencyLinkage.empty()); +}}; -TEST(Manifest, DependencyLinkageIsAClosedVocabulary) { +UTest dependency_linkage_is_a_closed_vocabulary{"Manifest/DependencyLinkageIsAClosedVocabulary", [] { auto ok = load_inline("deplinkage_ok", R"( [package] name = "x" @@ -4115,8 +4084,8 @@ version = "0.1.0" [build] dependency_linkage = "shared" )"); - ASSERT_TRUE(ok); - EXPECT_EQ(ok->buildConfig.dependencyLinkage, "shared"); + expect(fatal(bool(ok))); + expect(eq(ok->buildConfig.dependencyLinkage, "shared")); // "dynamic" is the LIBC axis' word. Accepting it here would silently // mean "static", because that is what an unparsed value resolves to. @@ -4127,10 +4096,10 @@ version = "0.1.0" [build] dependency_linkage = "dynamic" )"); - EXPECT_FALSE(bad); -} + expect(!bad); +}}; -TEST(Manifest, ProfileDependencyLinkageIsOptionalSoItDoesNotResetBuild) { +UTest profile_dependency_linkage_is_optional_so_it_does_not_reset_build{"Manifest/ProfileDependencyLinkageIsOptionalSoItDoesNotResetBuild", [] { // Resolving a profile REPLACES the whole Profile struct, so a plain field // would make any declared profile silently undo `[build]`. auto m = load_inline("deplinkage_profile", R"( @@ -4144,15 +4113,15 @@ opt = "0" [profile.fast] dependency_linkage = "static" )"); - ASSERT_TRUE(m); - ASSERT_TRUE(m->profiles.contains("dev")); - EXPECT_FALSE(m->profiles.at("dev").dependencyLinkageDeclared); - ASSERT_TRUE(m->profiles.contains("fast")); - ASSERT_TRUE(m->profiles.at("fast").dependencyLinkageDeclared); - EXPECT_EQ(m->profiles.at("fast").dependencyLinkage, "static"); -} - -TEST(Manifest, ADependencyEdgeLinkageIsAClosedVocabularyToo) { + expect(fatal(bool(m))); + expect(fatal(m->profiles.contains("dev"))); + expect(!(m->profiles.at("dev").dependencyLinkageDeclared)); + expect(fatal(m->profiles.contains("fast"))); + expect(fatal(bool(m->profiles.at("fast").dependencyLinkageDeclared))); + expect(eq(m->profiles.at("fast").dependencyLinkage, "static")); +}}; + +UTest a_dependency_edge_linkage_is_a_closed_vocabulary_too{"Manifest/ADependencyEdgeLinkageIsAClosedVocabularyToo", [] { auto bad = load_inline("edge_linkage_bad", R"( [package] name = "x" @@ -4160,10 +4129,10 @@ version = "0.1.0" [dependencies] zlib = { version = "1.0.0", linkage = "dynamic" } )"); - EXPECT_FALSE(bad); -} + expect(!bad); +}}; -TEST(Manifest, ALibraryTargetMayDeclareASoname) { +UTest a_library_target_may_declare_a_soname{"Manifest/ALibraryTargetMayDeclareASoname", [] { // This used to make the WHOLE MANIFEST FAIL TO LOAD, in both parsers. // A soname is the name a library is FOUND by, and it is the only way two // copies of one library can resolve to a single file — so a package has @@ -4177,12 +4146,12 @@ version = "0.1.0" kind = "lib" soname = "libz.so.1" )"); - ASSERT_TRUE(m) << (m ? "" : m.error().message); - ASSERT_EQ(m->targets.size(), 1u); - EXPECT_EQ(m->targets[0].soname, "libz.so.1"); -} + if (auto&& failed = expect(fatal(bool(m))); !failed) failed << (m ? "" : m.error().message); + expect(fatal(eq(m->targets.size(), 1u))); + expect(eq(m->targets[0].soname, "libz.so.1")); +}}; -TEST(Manifest, ASonameOnABinaryTargetIsStillRefused) { +UTest a_soname_on_a_binary_target_is_still_refused{"Manifest/ASonameOnABinaryTargetIsStillRefused", [] { auto m = load_inline("soname_on_bin", R"( [package] name = "z" @@ -4192,10 +4161,10 @@ kind = "bin" main = "src/main.cpp" soname = "libz.so.1" )"); - EXPECT_FALSE(m); -} + expect(!m); +}}; -TEST(Manifest, ASonameMustStillBeABasename) { +UTest a_soname_must_still_be_a_basename{"Manifest/ASonameMustStillBeABasename", [] { auto m = load_inline("soname_path", R"( [package] name = "z" @@ -4204,8 +4173,8 @@ version = "0.1.0" kind = "lib" soname = "lib/libz.so.1" )"); - EXPECT_FALSE(m); -} + expect(!m); +}}; // #359: a manifest written for a NEWER mcpp must still load. // @@ -4220,7 +4189,7 @@ soname = "lib/libz.so.1" // The discriminator is now "does it name a source", which a namespace table // cannot accidentally satisfy, and an unrecognized key is a recorded // degradation (--strict still refuses it). -TEST(Manifest, UnknownDependencyKeyDegradesInsteadOfFailingTheLoad) { +UTest unknown_dependency_key_degrades_instead_of_failing_the_load{"Manifest/UnknownDependencyKeyDegradesInsteadOfFailingTheLoad", [] { auto tmp = std::filesystem::temp_directory_path() / "mcpp_dep_future_key"; std::filesystem::create_directories(tmp); auto path = tmp / "mcpp.toml"; @@ -4236,24 +4205,24 @@ zlib = { version = "1.3.1", some-future-field = true } )"; } auto m = mcpp::manifest::load(path); - ASSERT_TRUE(m) << (m ? "" : m.error().message); + if (auto&& failed = expect(fatal(bool(m))); !failed) failed << (m ? "" : m.error().message); const mcpp::pm::DependencySpec* z = nullptr; for (auto const& [k, spec] : m->dependencies) if (spec.shortName == "zlib") z = &spec; - ASSERT_NE(z, nullptr); - EXPECT_EQ(z->version, "1.3.1"); // the part this reader understands still applies + expect(fatal(ne(z, nullptr))); + expect(eq(z->version, "1.3.1")); // the part this reader understands still applies bool warned = false; for (auto const& w : m->schemaWarnings) if (w.find("some-future-field") != std::string::npos) warned = true; - EXPECT_TRUE(warned) << "the ignored key must be reported, not swallowed"; -} + expect(bool(warned)) << "the ignored key must be reported, not swallowed"; +}}; // The discriminator must not mistake a namespace subtable for an inline spec. // `[dependencies.compat] zlib = "1.3.1"` has one key, `zlib`, which names no // source — so it is a namespace, and the entry below it is the dependency. -TEST(Manifest, NamespaceSubtableIsNotMistakenForAnInlineSpec) { +UTest namespace_subtable_is_not_mistaken_for_an_inline_spec{"Manifest/NamespaceSubtableIsNotMistakenForAnInlineSpec", [] { auto tmp = std::filesystem::temp_directory_path() / "mcpp_ns_vs_spec"; std::filesystem::create_directories(tmp); auto path = tmp / "mcpp.toml"; @@ -4270,20 +4239,20 @@ gtest = { version = "1.15.2", features = ["main"] } )"; } auto m = mcpp::manifest::load(path); - ASSERT_TRUE(m) << (m ? "" : m.error().message); + if (auto&& failed = expect(fatal(bool(m))); !failed) failed << (m ? "" : m.error().message); std::set got; for (auto const& [k, spec] : m->dependencies) { got.insert(spec.shortName); - EXPECT_EQ(spec.namespace_, "compat") << spec.shortName; + expect(eq(spec.namespace_, "compat")) << spec.shortName; } - EXPECT_TRUE(got.contains("zlib")); - EXPECT_TRUE(got.contains("gtest")); -} + expect(got.contains("zlib")); + expect(got.contains("gtest")); +}}; // `cxx_runtime` gained a third role key. The table is validated by an explicit // whitelist, so a new spelling that nobody added there is rejected with a // message naming the key — which is the behaviour, not an accident. -TEST(Manifest, CxxRuntimeAcceptsThePerRoleTableIncludingShared) { +UTest cxx_runtime_accepts_the_per_role_table_including_shared{"Manifest/CxxRuntimeAcceptsThePerRoleTableIncludingShared", [] { constexpr auto src = R"( [package] name = "app" @@ -4292,18 +4261,18 @@ version = "0.1.0" cxx_runtime = { default = "self-contained", tests = "host-coupled", shared = "toolchain-coupled" } )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_EQ(m->buildConfig.cxxRuntime, "self-contained"); - EXPECT_EQ(m->buildConfig.cxxRuntimeTests, "host-coupled"); - EXPECT_EQ(m->buildConfig.cxxRuntimeShared, "toolchain-coupled"); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(eq(m->buildConfig.cxxRuntime, "self-contained")); + expect(eq(m->buildConfig.cxxRuntimeTests, "host-coupled")); + expect(eq(m->buildConfig.cxxRuntimeShared, "toolchain-coupled")); // And it must not be reported as an unsupported [build] key. The whole // feature is reachable only through this spelling, so a warning saying it // is "ignored" would be both noisy and false. - EXPECT_TRUE(m->schemaWarnings.empty()) + expect(m->schemaWarnings.empty()) << (m->schemaWarnings.empty() ? "" : m->schemaWarnings[0]); -} +}}; -TEST(Manifest, CxxRuntimeRejectsAnUnknownRoleKey) { +UTest cxx_runtime_rejects_an_unknown_role_key{"Manifest/CxxRuntimeRejectsAnUnknownRoleKey", [] { constexpr auto src = R"( [package] name = "app" @@ -4312,13 +4281,12 @@ version = "0.1.0" cxx_runtime = { sharedd = "self-contained" } )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().format().find("sharedd"), std::string::npos) - << m.error().format(); -} + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().format().find("sharedd"), std::string::npos)); !failed) failed << m.error().format(); +}}; // The scalar spelling still means "every role", including shared libraries. -TEST(Manifest, CxxRuntimeScalarStillAppliesToEveryRole) { +UTest cxx_runtime_scalar_still_applies_to_every_role{"Manifest/CxxRuntimeScalarStillAppliesToEveryRole", [] { constexpr auto src = R"( [package] name = "app" @@ -4327,13 +4295,13 @@ version = "0.1.0" cxx_runtime = "host-coupled" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_EQ(m->buildConfig.cxxRuntime, "host-coupled"); - EXPECT_TRUE(m->buildConfig.cxxRuntimeShared.empty()); - EXPECT_TRUE(m->schemaWarnings.empty()); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(eq(m->buildConfig.cxxRuntime, "host-coupled")); + expect(m->buildConfig.cxxRuntimeShared.empty()); + expect(m->schemaWarnings.empty()); +}}; -TEST(Manifest, BmiScheduleKey) { +UTest bmi_schedule_key{"Manifest/BmiScheduleKey", [] { // The KEY SPELLING, not just the field. `[build] bmi_schedule` reaches the // build only through this one string in mcpp.manifest.toml, and nothing // downstream can tell a mistyped key from an absent one: the value silently @@ -4351,8 +4319,8 @@ version = "0.1.0" bmi_schedule = "on" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_EQ(m->buildConfig.bmiSchedule, "on"); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(eq(m->buildConfig.bmiSchedule, "on")); // And the old spelling must NOT still work: leaving it accepted would mean // two keys for one switch, which is how a rename ends up half-done. @@ -4364,8 +4332,8 @@ version = "0.1.0" schedule = "on" )"; auto o = mcpp::manifest::parse_string(old_key); - ASSERT_TRUE(o.has_value()) << o.error().format(); - EXPECT_TRUE(o->buildConfig.bmiSchedule.empty()) + if (auto&& failed = expect(fatal(o.has_value())); !failed) failed << o.error().format(); + expect(o->buildConfig.bmiSchedule.empty()) << "the pre-rename key `schedule` is still being read"; // AND THE PARSER MUST NOT WARN ABOUT ITS OWN KEY. @@ -4381,16 +4349,16 @@ schedule = "on" // feature on announced that it had been ignored, and the dead `schedule` // was accepted in silence. Both halves of the rename are pinned now. for (const auto& w : m->schemaWarnings) - EXPECT_EQ(w.find("bmi_schedule"), std::string::npos) + expect(eq(w.find("bmi_schedule"), std::string::npos)) << "the parser reads `bmi_schedule` but also warns about it: " << w; - EXPECT_TRUE(std::ranges::any_of(o->schemaWarnings, + expect(std::ranges::any_of(o->schemaWarnings, [](const std::string& w) { return w.find("schedule") != std::string::npos; })) << "the dead key `schedule` is accepted silently — an unread key that " "produces no diagnostic is a typo that costs a debugging session"; -} +}}; // #418 — an unsupported SCALAR under `[target.]` is reported; the // conditional sub-TABLES are not. @@ -4408,7 +4376,7 @@ schedule = "on" // own comments on #258 and #359). The first version of this check did exactly // that and warned about a documented feature. Scalars only; tables are skipped // on principle, not by enumeration. -TEST(Manifest, PerTargetUnknownScalarWarnsButSubTablesDoNot) { +UTest per_target_unknown_scalar_warns_but_sub_tables_do_not{"Manifest/PerTargetUnknownScalarWarnsButSubTablesDoNot", [] { constexpr auto src = R"( [package] name = "t" @@ -4425,7 +4393,7 @@ cxx_runtime_tests = "host-coupled" defines = ["FOO=1"] )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); auto mentions = [&](std::string_view needle) { return std::ranges::any_of(m->schemaWarnings, [&](const std::string& w) { @@ -4433,23 +4401,22 @@ defines = ["FOO=1"] }); }; - EXPECT_TRUE(mentions("cxx_runtime_tests")) + expect(bool(mentions("cxx_runtime_tests"))) << "a per-target scalar that nothing reads must be reported, not dropped"; // The conditional channel is not a typo, and warning about it would be a // false positive on a documented feature (e2e 195 uses it). - EXPECT_FALSE(mentions("'dependencies'")) + expect(!mentions("'dependencies'")) << "[target..dependencies] is the conditional channel, not an unknown key"; - EXPECT_FALSE(mentions("'build'")) - << "[target..build] is the conditional channel, not an unknown key"; + if (auto&& failed = expect(!mentions("'build'")); !failed) failed << "[target..build] is the conditional channel, not an unknown key"; // ...and the field itself is gone, so nothing downstream can read it. - EXPECT_EQ(m->targetOverrides.at("x86_64-linux-gnu").cxxRuntime, ""); -} + expect(eq(m->targetOverrides.at("x86_64-linux-gnu").cxxRuntime, "")); +}}; // ── [target.].runner ──────────────────────────────────────────────── -TEST(Manifest, TargetRunnerParsesAndDoesNotWarn) { +UTest target_runner_parses_and_does_not_warn{"Manifest/TargetRunnerParsesAndDoesNotWarn", [] { constexpr auto src = R"( [package] name = "fw" @@ -4459,21 +4426,21 @@ version = "0.1.0" runner = ["qemu-system-riscv64", "-machine", "virt", "-kernel"] )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); auto it = m->targetOverrides.find("riscv64-none-elf"); - ASSERT_NE(it, m->targetOverrides.end()); - ASSERT_EQ(it->second.runner.size(), 4u); - EXPECT_EQ(it->second.runner.front(), "qemu-system-riscv64"); - EXPECT_EQ(it->second.runner.back(), "-kernel"); + expect(fatal(ne(it, m->targetOverrides.end()))); + expect(fatal(eq(it->second.runner.size(), 4u))); + expect(eq(it->second.runner.front(), "qemu-system-riscv64")); + expect(eq(it->second.runner.back(), "-kernel")); // The unknown-key sweep is about SCALARS ("a scalar that does // nothing"). It skipped tables but not arrays, so this key was reported as // "unsupported key 'runner' (ignored)" while in fact being honoured — // worse than either statement being true. Seen in CI. - EXPECT_TRUE(m->schemaWarnings.empty()) + expect(m->schemaWarnings.empty()) << (m->schemaWarnings.empty() ? "" : m->schemaWarnings[0]); -} +}}; -TEST(Manifest, TargetRunnerRejectsShapesThatWouldRunNothing) { +UTest target_runner_rejects_shapes_that_would_run_nothing{"Manifest/TargetRunnerRejectsShapesThatWouldRunNothing", [] { // An empty template would exec nothing and report success. constexpr auto empty = R"( [package] @@ -4482,7 +4449,7 @@ version = "0.1.0" [target.riscv64-none-elf] runner = [] )"; - EXPECT_FALSE(mcpp::manifest::parse_string(empty).has_value()); + expect(!(mcpp::manifest::parse_string(empty).has_value())); // A bare string is the shape a user reaches for first; taking it would // mean guessing where the word boundaries are. @@ -4493,8 +4460,8 @@ version = "0.1.0" [target.riscv64-none-elf] runner = "qemu-system-riscv64 -kernel" )"; - EXPECT_FALSE(mcpp::manifest::parse_string(scalar).has_value()); -} + expect(!(mcpp::manifest::parse_string(scalar).has_value())); +}}; // ── [target.].sysroot — the target's C library, per project ────────── // @@ -4503,7 +4470,7 @@ runner = "qemu-system-riscv64 -kernel" // project on a vendor SDK wants that SDK's newlib, and a `std-freestanding` // stack over openkal wants openkal's C library. -TEST(Manifest, TargetSysrootOverrideIsParsed) { +UTest target_sysroot_override_is_parsed{"Manifest/TargetSysrootOverrideIsParsed", [] { constexpr auto src = R"( [package] name = "x" @@ -4512,18 +4479,17 @@ version = "0.1.0" sysroot = "xim:newlib-riscv@4.4" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); auto it = m->targetOverrides.find("riscv64-none-elf"); - ASSERT_NE(it, m->targetOverrides.end()); - ASSERT_TRUE(it->second.sysrootDeclared); - EXPECT_EQ(it->second.sysroot, "xim:newlib-riscv@4.4"); - EXPECT_TRUE(m->schemaWarnings.empty()) - << (m->schemaWarnings.empty() ? "" : m->schemaWarnings[0]); -} + expect(fatal(ne(it, m->targetOverrides.end()))); + expect(fatal(bool(it->second.sysrootDeclared))); + expect(eq(it->second.sysroot, "xim:newlib-riscv@4.4")); + if (auto&& failed = expect(m->schemaWarnings.empty()); !failed) failed << (m->schemaWarnings.empty() ? "" : m->schemaWarnings[0]); +}}; // The distinction this test pins is the reason the field is an optional. // An empty string is a REQUEST (no C library), not an absence. -TEST(Manifest, TargetSysrootEmptyStringIsRecordedAsPresent) { +UTest target_sysroot_empty_string_is_recorded_as_present{"Manifest/TargetSysrootEmptyStringIsRecordedAsPresent", [] { constexpr auto src = R"( [package] name = "x" @@ -4532,14 +4498,14 @@ version = "0.1.0" sysroot = "" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); auto it = m->targetOverrides.find("riscv64-none-elf"); - ASSERT_NE(it, m->targetOverrides.end()); - ASSERT_TRUE(it->second.sysrootDeclared); - EXPECT_TRUE(it->second.sysroot.empty()); -} + expect(fatal(ne(it, m->targetOverrides.end()))); + expect(fatal(bool(it->second.sysrootDeclared))); + expect(it->second.sysroot.empty()); +}}; -TEST(Manifest, TargetSysrootAbsentIsNotDeclared) { +UTest target_sysroot_absent_is_not_declared{"Manifest/TargetSysrootAbsentIsNotDeclared", [] { constexpr auto src = R"( [package] name = "x" @@ -4548,15 +4514,15 @@ version = "0.1.0" linkage = "static" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); auto it = m->targetOverrides.find("riscv64-none-elf"); - ASSERT_NE(it, m->targetOverrides.end()); - EXPECT_FALSE(it->second.sysrootDeclared); -} + expect(fatal(ne(it, m->targetOverrides.end()))); + expect(!(it->second.sysrootDeclared)); +}}; // A bare name is the plausible typo, and accepting it would install nothing // and then fail much later naming a missing libc. -TEST(Manifest, TargetSysrootRejectsANonXpkgReference) { +UTest target_sysroot_rejects_a_non_xpkg_reference{"Manifest/TargetSysrootRejectsANonXpkgReference", [] { constexpr auto src = R"( [package] name = "x" @@ -4565,13 +4531,13 @@ version = "0.1.0" sysroot = "newlib" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().format().find("is not a sysroot"), std::string::npos); -} + expect(fatal(!m.has_value())); + expect(ne(m.error().format().find("is not a sysroot"), std::string::npos)); +}}; // The table form names a directory, absolute against the manifest's own // directory, and carries what it supplies (design 2026-10-23 §3.1). -TEST(Manifest, TargetSysrootTableNamesADirectory) { +UTest target_sysroot_table_names_a_directory{"Manifest/TargetSysrootTableNamesADirectory", [] { constexpr auto src = R"( [package] name = "x" @@ -4583,19 +4549,18 @@ run_under = ["remote-run", "team-mac", "--"] // An absolute origin on this host (a drive letter on Windows). const auto proj = (std::filesystem::temp_directory_path() / "proj").lexically_normal(); auto m = mcpp::manifest::parse_string(src, proj / "mcpp.toml"); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); auto it = m->targetOverrides.find("x86_64-linux-gnu"); - ASSERT_NE(it, m->targetOverrides.end()); - EXPECT_TRUE(it->second.sysrootDeclared); - EXPECT_EQ(it->second.sysroot, - "path:" + (proj / "third_party" / "sysroot").lexically_normal().generic_string()); - ASSERT_EQ(it->second.sysrootProvides.size(), 1u); - EXPECT_EQ(it->second.runUnder, (std::vector{"remote-run", "team-mac", "--"})); + expect(fatal(ne(it, m->targetOverrides.end()))); + expect(bool(it->second.sysrootDeclared)); + expect(eq(it->second.sysroot, "path:" + (proj / "third_party" / "sysroot").lexically_normal().generic_string())); + expect(fatal(eq(it->second.sysrootProvides.size(), 1u))); + expect(eq(it->second.runUnder, (std::vector{"remote-run", "team-mac", "--"}))); for (auto const& w : m->schemaWarnings) - EXPECT_EQ(w.find("sysroot"), std::string::npos) << w; -} + expect(eq(w.find("sysroot"), std::string::npos)) << w; +}}; -TEST(Manifest, TargetSysrootTableRefusesOtherKeys) { +UTest target_sysroot_table_refuses_other_keys{"Manifest/TargetSysrootTableRefusesOtherKeys", [] { constexpr auto src = R"( [package] name = "x" @@ -4604,13 +4569,13 @@ version = "0.1.0" sysroot = { path = "/opt/s", libc = "glibc" } )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().format().find("`path`"), std::string::npos) << m.error().format(); -} + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().format().find("`path`"), std::string::npos)); !failed) failed << m.error().format(); +}}; // The unknown-scalar sweep must not report a key it now honours — the exact // shape of #418, where a key was reported as ignored while being applied. -TEST(Manifest, TargetSysrootIsNotReportedAsUnsupported) { +UTest target_sysroot_is_not_reported_as_unsupported{"Manifest/TargetSysrootIsNotReportedAsUnsupported", [] { constexpr auto src = R"( [package] name = "x" @@ -4619,10 +4584,10 @@ version = "0.1.0" sysroot = "" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); for (auto const& w : m->schemaWarnings) - EXPECT_EQ(w.find("sysroot"), std::string::npos) << w; -} + expect(eq(w.find("sysroot"), std::string::npos)) << w; +}}; // ── On an MSVC-ABI row the sysroot is an MSVC toolset ───────────────────────── // @@ -4643,39 +4608,38 @@ sysroot = "{}" } } // namespace -TEST(Manifest, MsvcRowSysrootTakesTheMsvcSpellings) { +UTest msvc_row_sysroot_takes_the_msvc_spellings{"Manifest/MsvcRowSysrootTakesTheMsvcSpellings", [] { for (auto triple : {"x86_64-windows-msvc", "x86_64-pc-windows-msvc"}) { for (auto value : {"msvc@system", "msvc@14.44.35207", "msvc@14.44", "xim:msvc@14.44.35207"}) { auto m = msvc_row(triple, value); - ASSERT_TRUE(m.has_value()) << triple << " " << value << ": " + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << triple << " " << value << ": " << m.error().format(); auto it = m->targetOverrides.find(triple); - ASSERT_NE(it, m->targetOverrides.end()); - ASSERT_TRUE(it->second.sysrootDeclared); - EXPECT_EQ(it->second.sysroot, value); + expect(fatal(ne(it, m->targetOverrides.end()))); + expect(fatal(bool(it->second.sysrootDeclared))); + expect(eq(it->second.sysroot, value)); } } -} +}}; -TEST(Manifest, MsvcRowSysrootRefusesWhatIsNotAnMsvcToolset) { +UTest msvc_row_sysroot_refuses_what_is_not_an_msvc_toolset{"Manifest/MsvcRowSysrootRefusesWhatIsNotAnMsvcToolset", [] { // A C library package, the zero-libc tier, a package asked for the // machine's toolset, and a bare family name are all refused by name. for (auto value : {"xim:glibc@2.39", "", "xim:msvc@system", "msvc", "gcc@16.1.0"}) { auto m = msvc_row("x86_64-windows-msvc", value); - ASSERT_FALSE(m.has_value()) << "'" << value << "' was accepted"; - EXPECT_NE(m.error().format().find("MSVC toolset"), std::string::npos) - << m.error().format(); + expect(fatal(!m.has_value())) << "'" << value << "' was accepted"; + if (auto&& failed = expect(ne(m.error().format().find("MSVC toolset"), std::string::npos)); !failed) failed << m.error().format(); } -} +}}; -TEST(Manifest, TheMsvcSpellingIsAcceptedByTheGrammarElsewhere) { +UTest the_msvc_spelling_is_accepted_by_the_grammar_elsewhere{"Manifest/TheMsvcSpellingIsAcceptedByTheGrammarElsewhere", [] { // On every other row the spelling is a locator the grammar knows; which // row takes it is decided where the target is known // (`mcpp.toolchain.base::refusal`, tests/unit/test_target_base.cpp). auto m = msvc_row("x86_64-windows-gnu", "msvc@14.44.35207"); - ASSERT_TRUE(m.has_value()) << m.error().format(); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); +}}; // ── Dependency version requirements are checked where they are written ─────── // @@ -4691,7 +4655,7 @@ TEST(Manifest, TheMsvcSpellingIsAcceptedByTheGrammarElsewhere) { // `install path missing`. The check added below is a manifest check and has no // business ruling on the installer's path derivation, so this test pins what // the PARSER must keep accepting and says nothing about what fetches. -TEST(Manifest, DependencyVersionParserAcceptsEveryEstablishedForm) { +UTest dependency_version_parser_accepts_every_established_form{"Manifest/DependencyVersionParserAcceptsEveryEstablishedForm", [] { // This test exists to prevent the check from NARROWING anything. for (const char* v : { "0.0.1", "0.0", "^0.0.1", ">=0.0.1, <0.1.0", "*" }) { auto src = std::format(R"( @@ -4702,16 +4666,16 @@ version = "0.1.0" cmdline = "{}" )", v); auto m = mcpp::manifest::parse_string(src); - EXPECT_TRUE(m.has_value()) << v << ": " << (m ? "" : m.error().format()); + if (auto&& failed = expect(m.has_value()); !failed) failed << v << ": " << (m ? "" : m.error().format()); } -} +}}; // REPORTED, NOT REJECTED, and the distinction is load-bearing. The first // version of this check returned an error, and a project pinned to a PUBLISHED // package carrying such a string stopped loading entirely — including when the // offending entry belonged to a feature nobody activates. Published data must // not be invalidated by a new program any more than the reverse. -TEST(Manifest, DependencyVersionReportsATrailingXWithoutFailingTheLoad) { +UTest dependency_version_reports_a_trailing_x_without_failing_the_load{"Manifest/DependencyVersionReportsATrailingXWithoutFailingTheLoad", [] { constexpr auto src = R"( [package] name = "x" @@ -4720,7 +4684,7 @@ version = "0.1.0" cmdline = "0.0.x" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << (m ? "" : m.error().format()); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << (m ? "" : m.error().format()); // THE ASSERTION IS ON THE PROPERTIES THE MESSAGE MUST CARRY, not on its // prose. Keying on a phrase makes a reworded warning either a silent // no-op or a failure about wording; what a reader needs is the offending @@ -4731,13 +4695,13 @@ cmdline = "0.0.x" for (auto const& w : m->schemaWarnings) { if (w.find("0.0.x") == std::string::npos) continue; found = true; - EXPECT_NE(w.find("PACKAGE"), std::string::npos) << w; - EXPECT_NE(w.find("^1.2.3"), std::string::npos) << w; + expect(ne(w.find("PACKAGE"), std::string::npos)) << w; + expect(ne(w.find("^1.2.3"), std::string::npos)) << w; } - EXPECT_TRUE(found) << "no warning quoting the offending version string"; -} + expect(bool(found)) << "no warning quoting the offending version string"; +}}; -TEST(Manifest, DependencyVersionReportsTrailingGarbage) { +UTest dependency_version_reports_trailing_garbage{"Manifest/DependencyVersionReportsTrailingGarbage", [] { constexpr auto src = R"( [package] name = "x" @@ -4746,19 +4710,19 @@ version = "0.1.0" cmdline = "1.2.3abc" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()); + expect(fatal(m.has_value())); // "some warning was produced" is satisfied by a warning about anything at // all, including a typo elsewhere in the fixture. The warning has to quote // the string it is about, or the reader cannot find it. bool found = false; for (auto const& w : m->schemaWarnings) if (w.find("1.2.3abc") != std::string::npos) found = true; - EXPECT_TRUE(found) << "no warning quoting the offending version string"; -} + expect(bool(found)) << "no warning quoting the offending version string"; +}}; // A path or git dependency has no version, and requiring one would break every // workspace member. -TEST(Manifest, DependencyWithoutAVersionIsUnaffected) { +UTest dependency_without_a_version_is_unaffected{"Manifest/DependencyWithoutAVersionIsUnaffected", [] { constexpr auto src = R"( [package] name = "x" @@ -4767,12 +4731,12 @@ version = "0.1.0" local = { path = "../local" } )"; auto m = mcpp::manifest::parse_string(src); - EXPECT_TRUE(m.has_value()) << (m ? "" : m.error().format()); -} + if (auto&& failed = expect(m.has_value()); !failed) failed << (m ? "" : m.error().format()); +}}; // The long form reaches a different code path than the bare string, and only // one of the two used to set `spec.version`. Both are checked. -TEST(Manifest, LongFormDependencyVersionIsCheckedToo) { +UTest long_form_dependency_version_is_checked_too{"Manifest/LongFormDependencyVersionIsCheckedToo", [] { constexpr auto src = R"( [package] name = "x" @@ -4781,12 +4745,12 @@ version = "0.1.0" cmdline = { version = "0.0.x", features = ["a"] } )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << (m ? "" : m.error().format()); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << (m ? "" : m.error().format()); bool found = false; for (auto const& w : m->schemaWarnings) if (w.find("0.0.x") != std::string::npos) found = true; - EXPECT_TRUE(found) << "the long form's version was not checked"; -} + expect(bool(found)) << "the long form's version was not checked"; +}}; // ─── [hooks] — project build lifecycle commands (#496) ─────────────────── // @@ -4808,50 +4772,48 @@ parse_with_hooks(std::string_view hooksSection) { } } // namespace -TEST(ManifestHooks, AbsentSectionIsInertButNotDisabled) { +UTest absent_section_is_inert_but_not_disabled{"ManifestHooks/AbsentSectionIsInertButNotDisabled", [] { auto m = mcpp::manifest::parse_string(kHookPreamble); - ASSERT_TRUE(m.has_value()) << (m ? "" : m.error().format()); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << (m ? "" : m.error().format()); // `enabled` defaults to true and says nothing about whether there is work // to do; `active()` is the predicate the fast path is allowed to consult. - EXPECT_TRUE(m->hooks.enabled); - EXPECT_FALSE(m->hooks.active()); - EXPECT_EQ(m->hooks.timeoutSeconds, 10); + expect(bool(m->hooks.enabled)); + expect(!(m->hooks.active())); + expect(eq(m->hooks.timeoutSeconds, 10)); // Experimental: a hook may not decide whether a build succeeded, so this // defaults to false and is the only value the parser accepts. - EXPECT_FALSE(m->hooks.sideEffect); -} + expect(!(m->hooks.sideEffect)); +}}; // The experimental gate. Refused rather than downgraded: honouring it would // give an experimental feature a veto over every build, and ignoring it would // leave the project believing its build is gated on a notifier when nothing // is. Both silent options are worse than the error. -TEST(ManifestHooks, SideEffectTrueIsRefusedWhileTheFeatureIsExperimental) { +UTest side_effect_true_is_refused_while_the_feature_is_experimental{"ManifestHooks/SideEffectTrueIsRefusedWhileTheFeatureIsExperimental", [] { auto m = parse_with_hooks(R"( [hooks] build_finished = "echo done" side_effect = true )"); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().message.find("experimental"), std::string::npos) - << m.error().message; - EXPECT_NE(m.error().message.find("side_effect"), std::string::npos) - << m.error().message; -} + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().message.find("experimental"), std::string::npos)); !failed) failed << m.error().message; + if (auto&& failed = expect(ne(m.error().message.find("side_effect"), std::string::npos)); !failed) failed << m.error().message; +}}; // The key stays in the vocabulary: writing the value that IS the current // behaviour must keep working, so a manifest does not have to change when the // feature is promoted. -TEST(ManifestHooks, SideEffectFalseIsAcceptedSoTheKeyStaysReserved) { +UTest side_effect_false_is_accepted_so_the_key_stays_reserved{"ManifestHooks/SideEffectFalseIsAcceptedSoTheKeyStaysReserved", [] { auto m = parse_with_hooks(R"( [hooks] build_finished = "echo done" side_effect = false )"); - ASSERT_TRUE(m.has_value()) << (m ? "" : m.error().format()); - EXPECT_FALSE(m->hooks.sideEffect); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << (m ? "" : m.error().format()); + expect(!(m->hooks.sideEffect)); +}}; -TEST(ManifestHooks, CommandsAndPolicyAreRead) { +UTest commands_and_policy_are_read{"ManifestHooks/CommandsAndPolicyAreRead", [] { auto m = parse_with_hooks(R"( [hooks] build_start = "echo start" @@ -4860,54 +4822,54 @@ build_finished = "echo finished" timeout_seconds = 30 side_effect = false )"); - ASSERT_TRUE(m.has_value()) << (m ? "" : m.error().format()); - EXPECT_EQ(m->hooks.buildStart.cmd, "echo start"); - EXPECT_EQ(m->hooks.buildFailed.cmd, "echo failed"); - EXPECT_EQ(m->hooks.buildFinished.cmd, "echo finished"); - EXPECT_EQ(m->hooks.timeoutSeconds, 30); - EXPECT_FALSE(m->hooks.sideEffect); - EXPECT_TRUE(m->hooks.active()); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << (m ? "" : m.error().format()); + expect(eq(m->hooks.buildStart.cmd, "echo start")); + expect(eq(m->hooks.buildFailed.cmd, "echo failed")); + expect(eq(m->hooks.buildFinished.cmd, "echo finished")); + expect(eq(m->hooks.timeoutSeconds, 30)); + expect(!(m->hooks.sideEffect)); + expect(bool(m->hooks.active())); +}}; // `enabled = false` has to switch off a table that still names commands — // otherwise the only way to silence hooks is to delete them. -TEST(ManifestHooks, DisabledTableIsNotActive) { +UTest disabled_table_is_not_active{"ManifestHooks/DisabledTableIsNotActive", [] { auto m = parse_with_hooks(R"( [hooks] build_start = "echo start" enabled = false )"); - ASSERT_TRUE(m.has_value()) << (m ? "" : m.error().format()); - EXPECT_EQ(m->hooks.buildStart.cmd, "echo start"); - EXPECT_FALSE(m->hooks.active()); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << (m ? "" : m.error().format()); + expect(eq(m->hooks.buildStart.cmd, "echo start")); + expect(!(m->hooks.active())); +}}; // Policy without a command is not work: a table that only raises the timeout // must leave the fast path alone. -TEST(ManifestHooks, PolicyOnlyTableIsNotActive) { +UTest policy_only_table_is_not_active{"ManifestHooks/PolicyOnlyTableIsNotActive", [] { auto m = parse_with_hooks(R"( [hooks] timeout_seconds = 60 )"); - ASSERT_TRUE(m.has_value()) << (m ? "" : m.error().format()); - EXPECT_FALSE(m->hooks.active()); - EXPECT_EQ(m->hooks.timeoutSeconds, 60); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << (m ? "" : m.error().format()); + expect(!(m->hooks.active())); + expect(eq(m->hooks.timeoutSeconds, 60)); +}}; // Both ends of the documented range are the range: the largest accepted value // is asserted next to the smallest rejected one, so a change to either is a // change to a test. -TEST(ManifestHooks, TheDocumentedTimeoutCeilingIsAccepted) { +UTest the_documented_timeout_ceiling_is_accepted{"ManifestHooks/TheDocumentedTimeoutCeilingIsAccepted", [] { auto m = parse_with_hooks(R"( [hooks] build_start = "echo start" timeout_seconds = 86400 )"); - ASSERT_TRUE(m.has_value()) << (m ? "" : m.error().format()); - EXPECT_EQ(m->hooks.timeoutSeconds, 86400); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << (m ? "" : m.error().format()); + expect(eq(m->hooks.timeoutSeconds, 86400)); +}}; -TEST(ManifestHooks, ValueErrorsAreRejectedWithTheKeyNamed) { +UTest value_errors_are_rejected_with_the_key_named{"ManifestHooks/ValueErrorsAreRejectedWithTheKeyNamed", [] { struct Case { std::string_view section; std::string_view needle; }; for (auto const& c : { Case{R"([hooks] @@ -4928,15 +4890,14 @@ side_effect = 1)", "side_effect"}, timeout_seconds = 86401)", "timeout_seconds"}, }) { auto m = parse_with_hooks(c.section); - ASSERT_FALSE(m.has_value()) << c.section; - EXPECT_NE(m.error().message.find(c.needle), std::string::npos) - << c.section << " -> " << m.error().message; + expect(fatal(!m.has_value())) << c.section; + if (auto&& failed = expect(ne(m.error().message.find(c.needle), std::string::npos)); !failed) failed << c.section << " -> " << m.error().message; } -} +}}; // `hooks = "…"` is the shape a user reaches for when they want one command // and no table. Saying so beats parsing nothing and running nothing. -TEST(ManifestHooks, AScalarHooksKeyIsRejected) { +UTest a_scalar_hooks_key_is_rejected{"ManifestHooks/AScalarHooksKeyIsRejected", [] { auto m = mcpp::manifest::parse_string(R"( hooks = "echo hi" @@ -4944,90 +4905,87 @@ hooks = "echo hi" name = "x" version = "0.1.0" )"); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().message.find("[hooks]"), std::string::npos) - << m.error().message; -} + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().message.find("[hooks]"), std::string::npos)); !failed) failed << m.error().message; +}}; // An unknown KEY is a warning, not an error — the same split [build] uses. // A manifest written for a later mcpp (say, a `build_cancelled` event) still // loads on this one, while a typo is still reported. -TEST(ManifestHooks, UnknownKeyWarnsInsteadOfFailing) { +UTest unknown_key_warns_instead_of_failing{"ManifestHooks/UnknownKeyWarnsInsteadOfFailing", [] { auto m = parse_with_hooks(R"( [hooks] build_start = "echo start" build_stared = "echo typo" )"); - ASSERT_TRUE(m.has_value()) << (m ? "" : m.error().format()); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << (m ? "" : m.error().format()); bool found = false; for (auto const& w : m->schemaWarnings) if (w.find("build_stared") != std::string::npos) found = true; - EXPECT_TRUE(found); - EXPECT_TRUE(m->hooks.active()); -} + expect(bool(found)); + expect(bool(m->hooks.active())); +}}; // ─── The table form, and the interval each event names ─────────────────── -TEST(ManifestHooks, ABareStringIsSugarForATable) { +UTest a_bare_string_is_sugar_for_a_table{"ManifestHooks/ABareStringIsSugarForATable", [] { auto m = parse_with_hooks(R"( [hooks] build_start = "echo start" )"); - ASSERT_TRUE(m.has_value()) << (m ? "" : m.error().format()); - EXPECT_EQ(m->hooks.buildStart.cmd, "echo start"); - EXPECT_EQ(m->hooks.buildStart.timeoutSeconds, 0); // 0 = inherit - EXPECT_FALSE(m->hooks.buildStart.loop); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << (m ? "" : m.error().format()); + expect(eq(m->hooks.buildStart.cmd, "echo start")); + expect(eq(m->hooks.buildStart.timeoutSeconds, 0)); // 0 = inherit + expect(!(m->hooks.buildStart.loop)); +}}; // The per-event value wins over the table's, and 0 is what makes "not given" // expressible — without it there is no way to tell an inherited 10 from a // declared one. -TEST(ManifestHooks, APerEventTimeoutOverridesTheTableDefault) { +UTest a_per_event_timeout_overrides_the_table_default{"ManifestHooks/APerEventTimeoutOverridesTheTableDefault", [] { auto m = parse_with_hooks(R"( [hooks] timeout_seconds = 10 build_finished = { cmd = "slow-notifier", timeout_seconds = 45 } build_start = "echo start" )"); - ASSERT_TRUE(m.has_value()) << (m ? "" : m.error().format()); - EXPECT_EQ(m->hooks.timeout_for(m->hooks.buildFinished), 45); - EXPECT_EQ(m->hooks.timeout_for(m->hooks.buildStart), 10); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << (m ? "" : m.error().format()); + expect(eq(m->hooks.timeout_for(m->hooks.buildFinished), 45)); + expect(eq(m->hooks.timeout_for(m->hooks.buildStart), 10)); +}}; -TEST(ManifestHooks, DuringBuildIsReadAndMakesTheTableActive) { +UTest during_build_is_read_and_makes_the_table_active{"ManifestHooks/DuringBuildIsReadAndMakesTheTableActive", [] { auto m = parse_with_hooks(R"( [hooks] during_build = { cmd = "play bgm.mp3", loop = true } )"); - ASSERT_TRUE(m.has_value()) << (m ? "" : m.error().format()); - EXPECT_EQ(m->hooks.duringBuild.cmd, "play bgm.mp3"); - EXPECT_TRUE(m->hooks.duringBuild.loop); - EXPECT_TRUE(m->hooks.active()); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << (m ? "" : m.error().format()); + expect(eq(m->hooks.duringBuild.cmd, "play bgm.mp3")); + expect(bool(m->hooks.duringBuild.loop)); + expect(bool(m->hooks.active())); +}}; // The two keys that only exist for one interval. Accepted-and-ignored is // the failure mode this rejects: the user writes `loop` on `build_start`, // nothing repeats, and the feature looks broken rather than misused. Each // message names the event that does support the key. -TEST(ManifestHooks, AKeyOfferedToTheWrongIntervalIsRejected) { +UTest a_key_offered_to_the_wrong_interval_is_rejected{"ManifestHooks/AKeyOfferedToTheWrongIntervalIsRejected", [] { auto loopOnSelfClosing = parse_with_hooks(R"( [hooks] build_start = { cmd = "play bgm.mp3", loop = true } )"); - ASSERT_FALSE(loopOnSelfClosing.has_value()); - EXPECT_NE(loopOnSelfClosing.error().message.find("during_build"), - std::string::npos) << loopOnSelfClosing.error().message; + expect(fatal(!loopOnSelfClosing.has_value())); + if (auto&& failed = expect(ne(loopOnSelfClosing.error().message.find("during_build"), std::string::npos)); !failed) failed << loopOnSelfClosing.error().message; auto timeoutOnSpanning = parse_with_hooks(R"( [hooks] during_build = { cmd = "play bgm.mp3", timeout_seconds = 30 } )"); - ASSERT_FALSE(timeoutOnSpanning.has_value()); - EXPECT_NE(timeoutOnSpanning.error().message.find("timeout_seconds"), - std::string::npos) << timeoutOnSpanning.error().message; -} + expect(fatal(!timeoutOnSpanning.has_value())); + if (auto&& failed = expect(ne(timeoutOnSpanning.error().message.find("timeout_seconds"), std::string::npos)); !failed) failed << timeoutOnSpanning.error().message; +}}; -TEST(ManifestHooks, ATableWithoutCmdIsRejected) { +UTest a_table_without_cmd_is_rejected{"ManifestHooks/ATableWithoutCmdIsRejected", [] { for (std::string_view section : { R"([hooks] during_build = { loop = true })", @@ -5037,24 +4995,23 @@ build_start = { cmd = "" })", build_start = 42)", }) { auto m = parse_with_hooks(section); - ASSERT_FALSE(m.has_value()) << section; - EXPECT_NE(m.error().message.find("cmd"), std::string::npos) - << section << " -> " << m.error().message; + expect(fatal(!m.has_value())) << section; + if (auto&& failed = expect(ne(m.error().message.find("cmd"), std::string::npos)); !failed) failed << section << " -> " << m.error().message; } -} +}}; -TEST(ManifestHooks, AnUnknownKeyInsideAnEventTableWarns) { +UTest an_unknown_key_inside_an_event_table_warns{"ManifestHooks/AnUnknownKeyInsideAnEventTableWarns", [] { auto m = parse_with_hooks(R"( [hooks] during_build = { cmd = "play bgm.mp3", lopo = true } )"); - ASSERT_TRUE(m.has_value()) << (m ? "" : m.error().format()); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << (m ? "" : m.error().format()); bool found = false; for (auto const& w : m->schemaWarnings) if (w.find("lopo") != std::string::npos) found = true; - EXPECT_TRUE(found); - EXPECT_TRUE(m->hooks.active()); -} + expect(bool(found)); + expect(bool(m->hooks.active())); +}}; // ─── #544: array keys in the [target.] sweep; [xlings] per host ──── @@ -5063,7 +5020,7 @@ during_build = { cmd = "play bgm.mp3", lopo = true } // silence and left `runner` out of the "supported keys" list. Both halves of // the issue's third ask are pinned here; TargetRunnerParsesAndDoesNotWarn // above keeps the other direction (the real key stays unreported). -TEST(Manifest, TargetSweepReportsArrayTyposAndListsRunner) { +UTest target_sweep_reports_array_typos_and_lists_runner{"Manifest/TargetSweepReportsArrayTyposAndListsRunner", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "app" @@ -5071,23 +5028,22 @@ version = "0.1.0" [target.aarch64-linux-musl] runnerX = ["qemu-aarch64-static"] )"); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->schemaWarnings.size(), 1u); - EXPECT_NE(m->schemaWarnings[0].find("'runnerX'"), std::string::npos) << m->schemaWarnings[0]; + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->schemaWarnings.size(), 1u))); + if (auto&& failed = expect(ne(m->schemaWarnings[0].find("'runnerX'"), std::string::npos)); !failed) failed << m->schemaWarnings[0]; // #610 / #630 item 5 — `min_api_level` joined this list; see // tests/unit/test_target_scalar_keys.cpp for the test that keeps this // string (and the array beside it) from drifting from the parser again. - EXPECT_NE(m->schemaWarnings[0].find( + expect(ne(m->schemaWarnings[0].find( "Supported keys: cxx_runtime, linkage, min_api_level, platform, run_under, " - "runner, sysroot, toolchain"), - std::string::npos) << m->schemaWarnings[0]; -} + "runner, sysroot, toolchain"), std::string::npos)) << m->schemaWarnings[0]; +}}; // `[xlings]` mirrors `.xlings.json`, whose values may be an object keyed by // platform. The parser resolves the form against this host at load, so the // assertions below are written in terms of host_platform_key() rather than a // fixed platform: the same test file runs on all three CI hosts. -TEST(Manifest, XlingsDepsAcceptPerPlatformEntries) { +UTest xlings_deps_accept_per_platform_entries{"Manifest/XlingsDepsAcceptPerPlatformEntries", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "app" @@ -5095,15 +5051,15 @@ version = "0.1.0" [xlings] deps = ["xim:ninja", { linux = "qemu-user-aarch64" }, { windows = "nasm", default = "yasm" }] )"); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); const auto host = mcpp::manifest::host_platform_key(); std::vector want{"xim:ninja"}; if (host == "linux") want.push_back("qemu-user-aarch64"); want.push_back(host == "windows" ? "nasm" : "yasm"); - EXPECT_EQ(m->xlings.deps, want); -} + expect(eq(m->xlings.deps, want)); +}}; -TEST(Manifest, XlingsWorkspaceAcceptsPerPlatformValues) { +UTest xlings_workspace_accepts_per_platform_values{"Manifest/XlingsWorkspaceAcceptsPerPlatformValues", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "app" @@ -5113,18 +5069,18 @@ gcc = { linux = "15.1.0" } llvm = { macos = "20", default = "22" } xmake = "3.0.7" )"); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); const auto host = mcpp::manifest::host_platform_key(); - if (host == "linux") EXPECT_EQ(m->xlings.workspace.at("gcc"), "15.1.0"); - else EXPECT_EQ(m->xlings.workspace.count("gcc"), 0u); + if (host == "linux") expect(eq(m->xlings.workspace.at("gcc"), "15.1.0")); + else expect(eq(m->xlings.workspace.count("gcc"), 0u)); // `host_platform_key()` is xlings' spelling, `macosx`, since 2026.9.3.1. // Comparing against "macos" here made this the one assertion in the file // that could only fail on a macOS runner — and it did. - EXPECT_EQ(m->xlings.workspace.at("llvm"), host == "macosx" ? "20" : "22"); - EXPECT_EQ(m->xlings.workspace.at("xmake"), "3.0.7"); -} + expect(eq(m->xlings.workspace.at("llvm"), host == "macosx" ? "20" : "22")); + expect(eq(m->xlings.workspace.at("xmake"), "3.0.7")); +}}; -TEST(Manifest, XlingsUnknownPlatformKeyIsAnError) { +UTest xlings_unknown_platform_key_is_an_error{"Manifest/XlingsUnknownPlatformKeyIsAnError", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "app" @@ -5132,15 +5088,13 @@ version = "0.1.0" [xlings] deps = [{ linxu = "qemu-user-aarch64" }] )"); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().message.find("linxu"), std::string::npos) << m.error().message; - EXPECT_NE(m.error().message.find("linux, macosx (or macos), windows, default"), - std::string::npos) - << m.error().message; - EXPECT_NE(m.error().message.find("deps[0]"), std::string::npos) << m.error().message; -} + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().message.find("linxu"), std::string::npos)); !failed) failed << m.error().message; + if (auto&& failed = expect(ne(m.error().message.find("linux, macosx (or macos), windows, default"), std::string::npos)); !failed) failed << m.error().message; + if (auto&& failed = expect(ne(m.error().message.find("deps[0]"), std::string::npos)); !failed) failed << m.error().message; +}}; -TEST(Manifest, XlingsWorkspaceNonStringLeafIsAnError) { +UTest xlings_workspace_non_string_leaf_is_an_error{"Manifest/XlingsWorkspaceNonStringLeafIsAnError", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "app" @@ -5148,28 +5102,27 @@ version = "0.1.0" [xlings.workspace] gcc = { linux = 15 } )"); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().message.find("[xlings.workspace] gcc"), std::string::npos) - << m.error().message; -} + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().message.find("[xlings.workspace] gcc"), std::string::npos)); !failed) failed << m.error().message; +}}; // The resolution rule itself, with the platform passed explicitly so all // three hosts' answers are asserted on every host. -TEST(Manifest, ResolveHostValueTable) { +UTest resolve_host_value_table{"Manifest/ResolveHostValueTable", [] { using mcpp::manifest::resolve_host_value; auto d = mcpp::libs::toml::parse(R"(v = { linux = "a", macosx = "b", default = "c" })"); - ASSERT_TRUE(d.has_value()); + expect(fatal(d.has_value())); const auto& v = d->root().at("v"); - EXPECT_EQ(resolve_host_value(v, "linux").value().value(), "a"); - EXPECT_EQ(resolve_host_value(v, "macos").value().value(), "b"); // macosx alias - EXPECT_EQ(resolve_host_value(v, "windows").value().value(), "c"); + expect(eq(resolve_host_value(v, "linux").value().value(), "a")); + expect(eq(resolve_host_value(v, "macos").value().value(), "b")); // macosx alias + expect(eq(resolve_host_value(v, "windows").value().value(), "c")); auto e = mcpp::libs::toml::parse(R"(v = { linux = "a" })"); - ASSERT_TRUE(e.has_value()); - EXPECT_FALSE(resolve_host_value(e->root().at("v"), "windows").value().has_value()); + expect(fatal(e.has_value())); + expect(!(resolve_host_value(e->root().at("v"), "windows").value().has_value())); auto s = mcpp::libs::toml::parse(R"(v = "plain")"); - ASSERT_TRUE(s.has_value()); - EXPECT_EQ(resolve_host_value(s->root().at("v"), "windows").value().value(), "plain"); -} + expect(fatal(s.has_value())); + expect(eq(resolve_host_value(s->root().at("v"), "windows").value().value(), "plain")); +}}; // ── `[xlings.workspace]` as the one table (2026-09-03) ───────────────────── // @@ -5178,7 +5131,7 @@ TEST(Manifest, ResolveHostValueTable) { // pin — and the assertions are on both, because a test that checked only one // could not tell a dropped namespace from a dropped version. -TEST(Manifest, XlingsWorkspaceProducesAnAddressAndAPin) { +UTest xlings_workspace_produces_an_address_and_a_pin{"Manifest/XlingsWorkspaceProducesAnAddressAndAPin", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "app" @@ -5186,13 +5139,13 @@ version = "0.1.0" [xlings.workspace] cmake = "3.28" )"); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_EQ(m->xlings.workspace.at("cmake"), "3.28"); - ASSERT_EQ(m->xlings.deps.size(), 1u); - EXPECT_EQ(m->xlings.deps[0], "cmake@3.28"); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(eq(m->xlings.workspace.at("cmake"), "3.28")); + expect(fatal(eq(m->xlings.deps.size(), 1u))); + expect(eq(m->xlings.deps[0], "cmake@3.28")); +}}; -TEST(Manifest, XlingsWorkspaceNamespaceOnEitherHalfIsTheSameEntry) { +UTest xlings_workspace_namespace_on_either_half_is_the_same_entry{"Manifest/XlingsWorkspaceNamespaceOnEitherHalfIsTheSameEntry", [] { auto onValue = mcpp::manifest::parse_string(R"( [package] name = "app" @@ -5200,7 +5153,7 @@ version = "0.1.0" [xlings.workspace] picolibc-riscv = "xim:1.8.12" )"); - ASSERT_TRUE(onValue.has_value()) << onValue.error().format(); + if (auto&& failed = expect(fatal(onValue.has_value())); !failed) failed << onValue.error().format(); auto onKey = mcpp::manifest::parse_string(R"( [package] name = "app" @@ -5208,14 +5161,14 @@ version = "0.1.0" [xlings.workspace] "xim:picolibc-riscv" = "1.8.12" )"); - ASSERT_TRUE(onKey.has_value()) << onKey.error().format(); - EXPECT_EQ(onValue->xlings.deps, onKey->xlings.deps); - EXPECT_EQ(onValue->xlings.workspace, onKey->xlings.workspace); - EXPECT_EQ(onValue->xlings.deps.at(0), "xim:picolibc-riscv@1.8.12"); - EXPECT_EQ(onValue->xlings.workspace.at("picolibc-riscv"), "xim:1.8.12"); -} - -TEST(Manifest, XlingsWorkspaceDisagreeingNamespacesAreRefused) { + if (auto&& failed = expect(fatal(onKey.has_value())); !failed) failed << onKey.error().format(); + expect(eq(onValue->xlings.deps, onKey->xlings.deps)); + expect(eq(onValue->xlings.workspace, onKey->xlings.workspace)); + expect(eq(onValue->xlings.deps.at(0), "xim:picolibc-riscv@1.8.12")); + expect(eq(onValue->xlings.workspace.at("picolibc-riscv"), "xim:1.8.12")); +}}; + +UTest xlings_workspace_disagreeing_namespaces_are_refused{"Manifest/XlingsWorkspaceDisagreeingNamespacesAreRefused", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "app" @@ -5223,12 +5176,12 @@ version = "0.1.0" [xlings.workspace] "xim:foo" = "other:1.0" )"); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().message.find("xim"), std::string::npos) << m.error().message; - EXPECT_NE(m.error().message.find("other"), std::string::npos) << m.error().message; -} + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().message.find("xim"), std::string::npos)); !failed) failed << m.error().message; + if (auto&& failed = expect(ne(m.error().message.find("other"), std::string::npos)); !failed) failed << m.error().message; +}}; -TEST(Manifest, XlingsWorkspaceNamingOnePackageTwiceIsRefused) { +UTest xlings_workspace_naming_one_package_twice_is_refused{"Manifest/XlingsWorkspaceNamingOnePackageTwiceIsRefused", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "app" @@ -5237,15 +5190,15 @@ version = "0.1.0" foo = "1.0" "xim:foo" = "2.0" )"); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().message.find("twice"), std::string::npos) << m.error().message; - EXPECT_NE(m.error().message.find("xim:foo"), std::string::npos) << m.error().message; -} + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().message.find("twice"), std::string::npos)); !failed) failed << m.error().message; + if (auto&& failed = expect(ne(m.error().message.find("xim:foo"), std::string::npos)); !failed) failed << m.error().message; +}}; // `""` is the authored spelling for "present, version unconstrained"; it is // what `d2mcpp/.xlings.json` already writes. Both halves are asserted: an // address with no `@`, and no pin at all. -TEST(Manifest, XlingsWorkspaceEmptyVersionAsksOnlyForPresence) { +UTest xlings_workspace_empty_version_asks_only_for_presence{"Manifest/XlingsWorkspaceEmptyVersionAsksOnlyForPresence", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "app" @@ -5253,14 +5206,14 @@ version = "0.1.0" [xlings.workspace] code = "" )"); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_EQ(m->xlings.deps.at(0), "code"); - EXPECT_TRUE(m->xlings.workspace.at("code").empty()); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(eq(m->xlings.deps.at(0), "code")); + expect(m->xlings.workspace.at("code").empty()); +}}; // The descriptor emitter needs every platform, and the host resolution has // already discarded two of them — so the unresolved declaration is kept. -TEST(Manifest, XlingsWorkspaceKeepsEveryPlatformForTheEmitter) { +UTest xlings_workspace_keeps_every_platform_for_the_emitter{"Manifest/XlingsWorkspaceKeepsEveryPlatformForTheEmitter", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "app" @@ -5269,26 +5222,22 @@ version = "0.1.0" qemu = { linux = "9.2.4-1", windows = "9.2.4-2" } ninja = "1.12.1" )"); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); const auto& byPlat = m->xlings.workspaceByPlatform; - ASSERT_TRUE(byPlat.contains("linux")); - ASSERT_TRUE(byPlat.contains("windows")); - EXPECT_NE(std::ranges::find(byPlat.at("linux"), "qemu@9.2.4-1"), - byPlat.at("linux").end()); - EXPECT_NE(std::ranges::find(byPlat.at("windows"), "qemu@9.2.4-2"), - byPlat.at("windows").end()); + expect(fatal(byPlat.contains("linux"))); + expect(fatal(byPlat.contains("windows"))); + expect(ne(std::ranges::find(byPlat.at("linux"), "qemu@9.2.4-1"), byPlat.at("linux").end())); + expect(ne(std::ranges::find(byPlat.at("windows"), "qemu@9.2.4-2"), byPlat.at("windows").end())); // A plain string speaks for every platform, macosx included, even though // `qemu` named none there. - ASSERT_TRUE(byPlat.contains("macosx")); - EXPECT_NE(std::ranges::find(byPlat.at("macosx"), "ninja@1.12.1"), - byPlat.at("macosx").end()); - EXPECT_EQ(std::ranges::find(byPlat.at("macosx"), "qemu@9.2.4-1"), - byPlat.at("macosx").end()); -} + expect(fatal(byPlat.contains("macosx"))); + expect(ne(std::ranges::find(byPlat.at("macosx"), "ninja@1.12.1"), byPlat.at("macosx").end())); + expect(eq(std::ranges::find(byPlat.at("macosx"), "qemu@9.2.4-1"), byPlat.at("macosx").end())); +}}; // `deps` is honoured and reported. Refusing it would reach a DEPENDENCY's // manifest, which a consumer that pinned that package cannot edit. -TEST(Manifest, XlingsDepsIsHonouredAndReported) { +UTest xlings_deps_is_honoured_and_reported{"Manifest/XlingsDepsIsHonouredAndReported", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "app" @@ -5296,22 +5245,20 @@ version = "0.1.0" [xlings] deps = ["xim:qemu-riscv@9.2.4-1"] )"); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_EQ(m->xlings.deps.at(0), "xim:qemu-riscv@9.2.4-1"); - ASSERT_EQ(m->schemaWarnings.size(), 1u); - EXPECT_NE(m->schemaWarnings[0].find("[xlings.workspace]"), std::string::npos) - << m->schemaWarnings[0]; + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(eq(m->xlings.deps.at(0), "xim:qemu-riscv@9.2.4-1")); + expect(fatal(eq(m->schemaWarnings.size(), 1u))); + if (auto&& failed = expect(ne(m->schemaWarnings[0].find("[xlings.workspace]"), std::string::npos)); !failed) failed << m->schemaWarnings[0]; // The message shows the line to write, in the RECOMMENDED spelling: the // namespace on the key, which is what an author writes and what every // official package uses. - EXPECT_NE(m->schemaWarnings[0].find("\"xim:qemu-riscv\" = \"9.2.4-1\""), - std::string::npos) << m->schemaWarnings[0]; -} + expect(ne(m->schemaWarnings[0].find("\"xim:qemu-riscv\" = \"9.2.4-1\""), std::string::npos)) << m->schemaWarnings[0]; +}}; // `envs` was materialised and read by nothing, while the documentation said it // reached the tool environment. A key that does nothing is worse when // something claims it does, so this is an error rather than a warning. -TEST(Manifest, XlingsEnvsIsRefused) { +UTest xlings_envs_is_refused{"Manifest/XlingsEnvsIsRefused", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "app" @@ -5319,16 +5266,15 @@ version = "0.1.0" [xlings.envs] OPENBLAS_NUM_THREADS = "1" )"); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().message.find("[xlings.envs]"), std::string::npos) - << m.error().message; -} + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().message.find("[xlings.envs]"), std::string::npos)); !failed) failed << m.error().message; +}}; // `macos` and `macosx` are one platform under two vocabularies — mcpp's triple // says the first, a descriptor and xlings' project file say the second. The // rule lives in mcpp.toolchain.platform; this asserts the manifest reaches it, // on every host, by resolving explicitly rather than against this one. -TEST(Manifest, XlingsWorkspaceAcceptsBothMacosSpellings) { +UTest xlings_workspace_accepts_both_macos_spellings{"Manifest/XlingsWorkspaceAcceptsBothMacosSpellings", [] { auto with_macos = mcpp::manifest::parse_string(R"( [package] name = "app" @@ -5336,7 +5282,7 @@ version = "0.1.0" [xlings.workspace] llvm = { macos = "20", default = "22" } )"); - ASSERT_TRUE(with_macos.has_value()) << with_macos.error().format(); + if (auto&& failed = expect(fatal(with_macos.has_value())); !failed) failed << with_macos.error().format(); auto with_macosx = mcpp::manifest::parse_string(R"( [package] name = "app" @@ -5344,19 +5290,16 @@ version = "0.1.0" [xlings.workspace] llvm = { macosx = "20", default = "22" } )"); - ASSERT_TRUE(with_macosx.has_value()) << with_macosx.error().format(); + if (auto&& failed = expect(fatal(with_macosx.has_value())); !failed) failed << with_macosx.error().format(); // Same platform, so the same per-platform declaration and the same // host-resolved answer, whichever way it was written. - EXPECT_EQ(with_macos->xlings.workspaceByPlatform, - with_macosx->xlings.workspaceByPlatform); - EXPECT_EQ(with_macos->xlings.workspace, with_macosx->xlings.workspace); - EXPECT_NE(std::ranges::find(with_macos->xlings.workspaceByPlatform.at("macosx"), - "llvm@20"), - with_macos->xlings.workspaceByPlatform.at("macosx").end()); - EXPECT_NE(std::ranges::find(with_macos->xlings.workspaceByPlatform.at("linux"), - "llvm@22"), - with_macos->xlings.workspaceByPlatform.at("linux").end()); -} + expect(eq(with_macos->xlings.workspaceByPlatform, with_macosx->xlings.workspaceByPlatform)); + expect(eq(with_macos->xlings.workspace, with_macosx->xlings.workspace)); + expect(ne(std::ranges::find(with_macos->xlings.workspaceByPlatform.at("macosx"), + "llvm@20"), with_macos->xlings.workspaceByPlatform.at("macosx").end())); + expect(ne(std::ranges::find(with_macos->xlings.workspaceByPlatform.at("linux"), + "llvm@22"), with_macos->xlings.workspaceByPlatform.at("linux").end())); +}}; // ─── Tool tiers: `when` on an entry, and `[feature-xlings.]` ──────────── // @@ -5365,7 +5308,7 @@ llvm = { macosx = "20", default = "22" } // keyed by `version` and `when`. Neither key set contains a member of the // other, so the presence of `version`/`when` decides — and this test is what // says so, because nothing in the parser's shape forces it. -TEST(Manifest, XlingsWorkspaceEntryTiers) { +UTest xlings_workspace_entry_tiers{"Manifest/XlingsWorkspaceEntryTiers", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "app" @@ -5377,24 +5320,24 @@ ran = { version = "3.0", when = "run" } devd = { version = "4.0", when = "dev" } anyver = { version = "", when = "run" } )"); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); using W = mcpp::manifest::ToolWhen; // An entry with no `when` is on the tier every verb installs. That is the // pre-2026.9.4.2 behaviour, and it is the reason no manifest has to change. - EXPECT_EQ(m->xlings.when_of("plain@1.0"), W::Always); - EXPECT_EQ(m->xlings.when_of("built@2.0"), W::Build); - EXPECT_EQ(m->xlings.when_of("ran@3.0"), W::Run); - EXPECT_EQ(m->xlings.when_of("devd@4.0"), W::Dev); + expect(eq(m->xlings.when_of("plain@1.0"), W::Always)); + expect(eq(m->xlings.when_of("built@2.0"), W::Build)); + expect(eq(m->xlings.when_of("ran@3.0"), W::Run)); + expect(eq(m->xlings.when_of("devd@4.0"), W::Dev)); // A tier does not change the address, only when it is asked for. An entry // that names no version asks for presence, exactly as the plain spelling // does — the address carries no `@`. - EXPECT_EQ(m->xlings.when_of("anyver"), W::Run); - EXPECT_EQ(m->xlings.workspace.at("plain"), "1.0"); - EXPECT_EQ(m->xlings.workspace.at("ran"), "3.0"); - EXPECT_TRUE(m->xlings.workspace.at("anyver").empty()); -} + expect(eq(m->xlings.when_of("anyver"), W::Run)); + expect(eq(m->xlings.workspace.at("plain"), "1.0")); + expect(eq(m->xlings.workspace.at("ran"), "3.0")); + expect(m->xlings.workspace.at("anyver").empty()); +}}; -TEST(Manifest, XlingsWorkspacePlatformTableStillParses) { +UTest xlings_workspace_platform_table_still_parses{"Manifest/XlingsWorkspacePlatformTableStillParses", [] { // The regression this pair guards: adding the tier form must not make a // platform table read as a malformed tier table. auto m = mcpp::manifest::parse_string(R"( @@ -5404,11 +5347,11 @@ version = "0.1.0" [xlings.workspace] llvm = { macos = "20", default = "22" } )"); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_EQ(m->xlings.when_of(m->xlings.deps.at(0)), mcpp::manifest::ToolWhen::Always); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(eq(m->xlings.when_of(m->xlings.deps.at(0)), mcpp::manifest::ToolWhen::Always)); +}}; -TEST(Manifest, XlingsWorkspaceBadTierIsAnError) { +UTest xlings_workspace_bad_tier_is_an_error{"Manifest/XlingsWorkspaceBadTierIsAnError", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "app" @@ -5416,14 +5359,12 @@ version = "0.1.0" [xlings.workspace] tool = { version = "1.0", when = "sometimes" } )"); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().message.find("sometimes"), std::string::npos) - << m.error().message; - EXPECT_NE(m.error().message.find("'build', 'run' or 'dev'"), std::string::npos) - << m.error().message; -} + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().message.find("sometimes"), std::string::npos)); !failed) failed << m.error().message; + if (auto&& failed = expect(ne(m.error().message.find("'build', 'run' or 'dev'"), std::string::npos)); !failed) failed << m.error().message; +}}; -TEST(Manifest, XlingsWorkspaceScopedEntryNeedsVersion) { +UTest xlings_workspace_scoped_entry_needs_version{"Manifest/XlingsWorkspaceScopedEntryNeedsVersion", [] { // `{ when = "run" }` and a typo of `version` are indistinguishable, so the // key is required and `""` is how "any version" is said. auto m = mcpp::manifest::parse_string(R"( @@ -5433,12 +5374,11 @@ version = "0.1.0" [xlings.workspace] tool = { versoin = "1.0", when = "run" } )"); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().message.find("versoin"), std::string::npos) - << m.error().message; -} + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().message.find("versoin"), std::string::npos)); !failed) failed << m.error().message; +}}; -TEST(Manifest, FeatureXlingsIsGatedAndReported) { +UTest feature_xlings_is_gated_and_reported{"Manifest/FeatureXlingsIsGatedAndReported", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "app" @@ -5452,21 +5392,20 @@ hardware = {} [feature-xlings.hardwear] "xim:typo" = "1.0" )"); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->xlings.featureDeps.count("hardware"), 1u); - EXPECT_EQ(m->xlings.featureDeps.at("hardware").at(0), "xim:probe-rs@0.24.0"); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->xlings.featureDeps.count("hardware"), 1u))); + expect(eq(m->xlings.featureDeps.at("hardware").at(0), "xim:probe-rs@0.24.0")); // A feature-gated tool is NOT in the unconditional list; a consumer that // never activates the feature never asks for it. - EXPECT_EQ(std::ranges::find(m->xlings.deps, "xim:probe-rs@0.24.0"), - m->xlings.deps.end()); + expect(eq(std::ranges::find(m->xlings.deps, "xim:probe-rs@0.24.0"), m->xlings.deps.end())); // A misspelt feature installs nothing and says nothing, which is the // shape #531 was filed for. Reported as a schema warning rather than an // error, so a package may adopt a feature before its consumers upgrade. bool sawTypo = false; for (auto const& w : m->schemaWarnings) if (w.find("hardwear") != std::string::npos) sawTypo = true; - EXPECT_TRUE(sawTypo) << "no warning for [feature-xlings.hardwear]"; -} + if (auto&& failed = expect(bool(sawTypo)); !failed) failed << "no warning for [feature-xlings.hardwear]"; +}}; // ── [package] exclusive ──────────────────────────────────────────────────── // @@ -5482,7 +5421,7 @@ hardware = {} // plain list (every existing reader keeps working); the constraint is what // prepare narrows on. -TEST(ManifestSourceConstraints, ATableEntryJoinsTheListAndCarriesItsConstraint) { +UTest a_table_entry_joins_the_list_and_carries_its_constraint{"ManifestSourceConstraints/ATableEntryJoinsTheListAndCarriesItsConstraint", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "infer" @@ -5490,17 +5429,17 @@ version = "0.1.0" [build] sources = ["src/**/*.cppm", { glob = "src/kernels/**/*.cu", accel = "cuda12.9+{sm_89}" }] )"); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->buildConfig.sources.size(), 2u); - EXPECT_EQ(m->buildConfig.sources[0], "src/**/*.cppm"); - EXPECT_EQ(m->buildConfig.sources[1], "src/kernels/**/*.cu"); - EXPECT_TRUE(m->buildConfig.sourcesDeclared); - ASSERT_EQ(m->buildConfig.sourceConstraints.size(), 1u); - EXPECT_EQ(m->buildConfig.sourceConstraints[0].glob, "src/kernels/**/*.cu"); - EXPECT_EQ(m->buildConfig.sourceConstraints[0].accel, "cuda12.9+{sm_89}"); -} - -TEST(ManifestSourceConstraints, ATableEntryWithoutAccelIsJustAGlob) { + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->buildConfig.sources.size(), 2u))); + expect(eq(m->buildConfig.sources[0], "src/**/*.cppm")); + expect(eq(m->buildConfig.sources[1], "src/kernels/**/*.cu")); + expect(bool(m->buildConfig.sourcesDeclared)); + expect(fatal(eq(m->buildConfig.sourceConstraints.size(), 1u))); + expect(eq(m->buildConfig.sourceConstraints[0].glob, "src/kernels/**/*.cu")); + expect(eq(m->buildConfig.sourceConstraints[0].accel, "cuda12.9+{sm_89}")); +}}; + +UTest a_table_entry_without_accel_is_just_a_glob{"ManifestSourceConstraints/ATableEntryWithoutAccelIsJustAGlob", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "infer" @@ -5508,12 +5447,12 @@ version = "0.1.0" [build] sources = [{ glob = "src/**/*.cpp" }] )"); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->buildConfig.sources.size(), 1u); - EXPECT_TRUE(m->buildConfig.sourceConstraints.empty()); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->buildConfig.sources.size(), 1u))); + expect(m->buildConfig.sourceConstraints.empty()); +}}; -TEST(ManifestSourceConstraints, AnEntryWithoutGlobIsRefused) { +UTest an_entry_without_glob_is_refused{"ManifestSourceConstraints/AnEntryWithoutGlobIsRefused", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "infer" @@ -5521,11 +5460,11 @@ version = "0.1.0" [build] sources = [{ accel = "cuda12.9+{sm_89}" }] )"); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().format().find("glob"), std::string::npos) << m.error().format(); -} + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().format().find("glob"), std::string::npos)); !failed) failed << m.error().format(); +}}; -TEST(ManifestSourceConstraints, AnUnknownKeyIsRefusedRatherThanSkipped) { +UTest an_unknown_key_is_refused_rather_than_skipped{"ManifestSourceConstraints/AnUnknownKeyIsRefusedRatherThanSkipped", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "infer" @@ -5533,11 +5472,11 @@ version = "0.1.0" [build] sources = [{ glob = "src/k/*.cu", acel = "cuda12.9+{sm_89}" }] )"); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().format().find("acel"), std::string::npos) << m.error().format(); -} + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().format().find("acel"), std::string::npos)); !failed) failed << m.error().format(); +}}; -TEST(ManifestSourceConstraints, AnExclusionCannotBeConstrained) { +UTest an_exclusion_cannot_be_constrained{"ManifestSourceConstraints/AnExclusionCannotBeConstrained", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "infer" @@ -5545,10 +5484,10 @@ version = "0.1.0" [build] sources = [{ glob = "!src/k/*.cu", accel = "cuda12.9+{sm_89}" }] )"); - ASSERT_FALSE(m.has_value()); -} + expect(fatal(!m.has_value())); +}}; -TEST(ManifestSourceConstraints, AConstrainedEntryUnderTheConditionalAxisIsRefused) { +UTest a_constrained_entry_under_the_conditional_axis_is_refused{"ManifestSourceConstraints/AConstrainedEntryUnderTheConditionalAxisIsRefused", [] { // read_list keeps strings only; a table that vanished there would be a // device glob nobody ever narrowed, so it is refused with a pointer to // where it belongs. @@ -5559,11 +5498,11 @@ version = "0.1.0" [target.'cfg(linux)'.build] sources = [{ glob = "src/k/*.cu", accel = "cuda12.9+{sm_89}" }] )"); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().format().find("[build].sources"), std::string::npos) << m.error().format(); -} + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().format().find("[build].sources"), std::string::npos)); !failed) failed << m.error().format(); +}}; -TEST(ManifestExclusive, CarriesTheListAndWarnsAboutNothing) { +UTest carries_the_list_and_warns_about_nothing{"ManifestExclusive/CarriesTheListAndWarnsAboutNothing", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "compat.cublas" @@ -5571,15 +5510,15 @@ version = "0.1.0" provides = ["gpu-blas", "gpu-fft"] exclusive = ["gpu-blas"] )"); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->exclusive.size(), 1u); - EXPECT_EQ(m->exclusive[0], "gpu-blas"); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->exclusive.size(), 1u))); + expect(eq(m->exclusive[0], "gpu-blas")); std::string all; for (auto const& w : m->schemaWarnings) { all += w; all += '\n'; } - EXPECT_TRUE(m->schemaWarnings.empty()) << all; -} + expect(m->schemaWarnings.empty()) << all; +}}; -TEST(ManifestExclusive, ClaimingSomethingNotProvidedIsReported) { +UTest claiming_something_not_provided_is_reported{"ManifestExclusive/ClaimingSomethingNotProvidedIsReported", [] { // Not an error: the declaration is carried so the binding-time check sees // exactly what was written. What it cannot do is act on a capability this // package does not supply, and a silent no-op is how a typo survives. @@ -5590,15 +5529,15 @@ version = "0.1.0" provides = ["gpu-blas"] exclusive = ["gpu-bals"] )"); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->exclusive.size(), 1u); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->exclusive.size(), 1u))); std::string all; for (auto const& w : m->schemaWarnings) { all += w; all += '\n'; } - EXPECT_NE(all.find("gpu-bals"), std::string::npos) + expect(ne(all.find("gpu-bals"), std::string::npos)) << "the warning must name the entry that cannot be enforced:\n" << all; -} +}}; -TEST(ManifestExclusive, AFeatureProvidedCapabilityCounts) { +UTest a_feature_provided_capability_counts{"ManifestExclusive/AFeatureProvidedCapabilityCounts", [] { // `provides` is not the only way a package supplies a capability, and a // check that only looked there would warn about a correct manifest. auto m = mcpp::manifest::parse_string(R"( @@ -5610,12 +5549,12 @@ exclusive = ["gpu-blas"] [features] cuda = { provides = ["gpu-blas"] } )"); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); std::string all; for (auto const& w : m->schemaWarnings) { all += w; all += '\n'; } - EXPECT_EQ(all.find("gpu-blas"), std::string::npos) + expect(eq(all.find("gpu-blas"), std::string::npos)) << "a feature-provided capability is supplied; no warning is due:\n" << all; -} +}}; // A predicate that carries ONLY a `[target..runtime]` table is recorded. // @@ -5629,7 +5568,7 @@ cuda = { provides = ["gpu-blas"] } // `frameworks` is the third per-target runtime key (#622 A2): a table that // carries only it is recorded, it is not reported as unsupported, and a key // outside the three is still reported naming all three. -TEST(Manifest, AConditionalFrameworksTableIsRecordedAndNotWarned) { +UTest a_conditional_frameworks_table_is_recorded_and_not_warned{"Manifest/AConditionalFrameworksTableIsRecordedAndNotWarned", [] { constexpr auto only_frameworks = R"( [package] name = "x" @@ -5638,13 +5577,13 @@ version = "0.1.0" frameworks = ["UIKit", "MobileCoreServices"] )"; auto m = mcpp::manifest::parse_string(only_frameworks); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->conditionalConfigs.size(), 1u) + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->conditionalConfigs.size(), 1u))) << "a frameworks-only predicate was parsed and then discarded"; - ASSERT_EQ(m->conditionalConfigs[0].frameworks.size(), 2u); - EXPECT_EQ(m->conditionalConfigs[0].frameworks[0], "UIKit"); + expect(fatal(eq(m->conditionalConfigs[0].frameworks.size(), 2u))); + expect(eq(m->conditionalConfigs[0].frameworks[0], "UIKit")); for (auto const& w : m->schemaWarnings) - EXPECT_EQ(w.find("frameworks"), std::string::npos) << w; + expect(eq(w.find("frameworks"), std::string::npos)) << w; constexpr auto a_fourth_key = R"( [package] @@ -5654,16 +5593,16 @@ version = "0.1.0" deploy_files = ["x"] )"; auto m2 = mcpp::manifest::parse_string(a_fourth_key); - ASSERT_TRUE(m2.has_value()) << m2.error().format(); + if (auto&& failed = expect(fatal(m2.has_value())); !failed) failed << m2.error().format(); bool named = false; for (auto const& w : m2->schemaWarnings) if (w.find("unsupported key 'deploy_files'") != std::string::npos && w.find("frameworks, libraries, link_library_dirs") != std::string::npos) named = true; - EXPECT_TRUE(named) << "the unsupported-key report must list the three keys"; -} + expect(bool(named)) << "the unsupported-key report must list the three keys"; +}}; -TEST(Manifest, AConditionalRuntimeTableAloneIsRecorded) { +UTest a_conditional_runtime_table_alone_is_recorded{"Manifest/AConditionalRuntimeTableAloneIsRecorded", [] { constexpr auto only_runtime = R"( [package] name = "x" @@ -5673,15 +5612,15 @@ libraries = ["dl"] link_library_dirs = ["lib"] )"; auto m = mcpp::manifest::parse_string(only_runtime); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->conditionalConfigs.size(), 1u) + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->conditionalConfigs.size(), 1u))) << "a runtime-only predicate was parsed and then discarded"; - ASSERT_EQ(m->conditionalConfigs[0].libraries.size(), 1u); - EXPECT_EQ(m->conditionalConfigs[0].libraries[0], "dl"); - EXPECT_EQ(m->conditionalConfigs[0].linkLibraryDirs.size(), 1u); -} + expect(fatal(eq(m->conditionalConfigs[0].libraries.size(), 1u))); + expect(eq(m->conditionalConfigs[0].libraries[0], "dl")); + expect(eq(m->conditionalConfigs[0].linkLibraryDirs.size(), 1u)); +}}; -TEST(Manifest, AConditionalRuntimeTableWithAnUnrelatedSiblingIsAlsoRecorded) { +UTest a_conditional_runtime_table_with_an_unrelated_sibling_is_also_recorded{"Manifest/AConditionalRuntimeTableWithAnUnrelatedSiblingIsAlsoRecorded", [] { // The control for the test above. This shape ALWAYS worked, which is why // the defect survived: every fixture that exercised the neutral link intent // was a generated distribution package, and those carry `ldflags` too. @@ -5695,16 +5634,16 @@ libraries = ["dl"] defines = ["UNRELATED_TO_LIBRARIES"] )"; auto m = mcpp::manifest::parse_string(with_sibling); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->conditionalConfigs.size(), 1u); - ASSERT_EQ(m->conditionalConfigs[0].libraries.size(), 1u); - EXPECT_EQ(m->conditionalConfigs[0].libraries[0], "dl"); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->conditionalConfigs.size(), 1u))); + expect(fatal(eq(m->conditionalConfigs[0].libraries.size(), 1u))); + expect(eq(m->conditionalConfigs[0].libraries[0], "dl")); +}}; // An unsupported key in `[runtime]` is REPORTED, not dropped — the rule // `[build]`, `[target.]` and `[target..build]` have each followed // since #418/#249/#544, and which these two tables did not. -TEST(Manifest, AnUnknownRuntimeKeyIsReported) { +UTest an_unknown_runtime_key_is_reported{"Manifest/AnUnknownRuntimeKeyIsReported", [] { constexpr auto src = R"( [package] name = "x" @@ -5714,19 +5653,18 @@ libraries = ["m"] dlopen_lib = ["one"] )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->schemaWarnings.size(), 1u); - EXPECT_NE(m->schemaWarnings[0].find("dlopen_lib"), std::string::npos) - << m->schemaWarnings[0]; + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->schemaWarnings.size(), 1u))); + if (auto&& failed = expect(ne(m->schemaWarnings[0].find("dlopen_lib"), std::string::npos)); !failed) failed << m->schemaWarnings[0]; // The message names the list the check uses, so a reader can find the key // they meant without leaving the message. - EXPECT_NE(m->schemaWarnings[0].find("dlopen_libs"), std::string::npos) + expect(ne(m->schemaWarnings[0].find("dlopen_libs"), std::string::npos)) << m->schemaWarnings[0]; // The correctly spelled sibling still took effect. - ASSERT_EQ(m->runtimeConfig.linkIntent.libraries.size(), 1u); -} + expect(fatal(eq(m->runtimeConfig.linkIntent.libraries.size(), 1u))); +}}; -TEST(Manifest, AnUnknownConditionalRuntimeKeyIsReported) { +UTest an_unknown_conditional_runtime_key_is_reported{"Manifest/AnUnknownConditionalRuntimeKeyIsReported", [] { constexpr auto src = R"( [package] name = "x" @@ -5736,20 +5674,19 @@ libraries = ["dl"] deploy_files = ["x"] )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->schemaWarnings.size(), 1u); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->schemaWarnings.size(), 1u))); // `deploy_files` is a real `[runtime]` key and not a per-target one, so // the plausible-looking case is the one held here. (`frameworks` was the // example until #622 made it the third per-target key.) - EXPECT_NE(m->schemaWarnings[0].find("deploy_files"), std::string::npos) - << m->schemaWarnings[0]; - EXPECT_NE(m->schemaWarnings[0].find("[target.linux.runtime]"), std::string::npos) + expect(ne(m->schemaWarnings[0].find("deploy_files"), std::string::npos)) << m->schemaWarnings[0]; -} + if (auto&& failed = expect(ne(m->schemaWarnings[0].find("[target.linux.runtime]"), std::string::npos)); !failed) failed << m->schemaWarnings[0]; +}}; // The negative control for both sweeps. Without it they pass against a parser // that reports every key, which reads identically in a green suite. -TEST(Manifest, CorrectlySpelledRuntimeKeysAreSilent) { +UTest correctly_spelled_runtime_keys_are_silent{"Manifest/CorrectlySpelledRuntimeKeysAreSilent", [] { constexpr auto src = R"( [package] name = "x" @@ -5780,37 +5717,37 @@ libraries = ["dl"] link_library_dirs = ["lib"] )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); std::string all; for (auto const& w : m->schemaWarnings) { all += w; all += '\n'; } - EXPECT_TRUE(m->schemaWarnings.empty()) << all; + expect(m->schemaWarnings.empty()) << all; // A `[runtime.]` sub-table is a provider override, not a typo. - EXPECT_EQ(m->runtimeConfig.providerOverrides.at("somecapability"), "pkg"); + expect(eq(m->runtimeConfig.providerOverrides.at("somecapability"), "pkg")); // `[[runtime.artifacts]]` and `[[runtime.requirements]]` are arrays, not // tables, so they reach the sweep rather than being skipped with the // provider channel. They are exactly what `mcpp pack` emits into every // packed library, so a false positive here would warn on all of them. - EXPECT_EQ(m->runtimeConfig.artifacts.size(), 1u); - EXPECT_EQ(m->runtimeConfig.requirements.size(), 1u); -} + expect(eq(m->runtimeConfig.artifacts.size(), 1u)); + expect(eq(m->runtimeConfig.requirements.size(), 1u)); +}}; // A manifest authored on Windows commonly begins with a UTF-8 byte-order mark. // It used to reach the lexer as a bare key and produce `1:1: expected key`, a // true statement about the token that says nothing about the file. -TEST(Manifest, AByteOrderMarkOnTheManifestIsNotAnError) { +UTest a_byte_order_mark_on_the_manifest_is_not_an_error{"Manifest/AByteOrderMarkOnTheManifestIsNotAnError", [] { const std::string src = "\xEF\xBB\xBF" "[package]\n" "name = \"x\"\n" "version = \"0.1.0\"\n"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_EQ(m->package.name, "x"); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(eq(m->package.name, "x")); +}}; // ── #615: `runtime.deploy` ────────────────────────────────────────────────── -TEST(Manifest, RuntimeDeployParsesFromAndTo) { +UTest runtime_deploy_parses_from_and_to{"Manifest/RuntimeDeployParsesFromAndTo", [] { constexpr auto src = R"( [package] name = "icd" @@ -5822,19 +5759,19 @@ deploy = [ ] )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); auto const& d = m->runtimeConfig.linkIntent.deploy; - ASSERT_EQ(d.size(), 2u); - EXPECT_EQ(d[0].from, std::filesystem::path("share/vulkan/icd.d/lvp_icd.json")); - EXPECT_EQ(d[0].to, "vulkan/icd.d"); - EXPECT_EQ(d[1].from, std::filesystem::path("share/readme.txt")); - EXPECT_EQ(d[1].to, "."); + expect(fatal(eq(d.size(), 2u))); + expect(eq(d[0].from, std::filesystem::path("share/vulkan/icd.d/lvp_icd.json"))); + expect(eq(d[0].to, "vulkan/icd.d")); + expect(eq(d[1].from, std::filesystem::path("share/readme.txt"))); + expect(eq(d[1].to, ".")); // A key of its own: `deploy_files` is untouched. - EXPECT_TRUE(m->runtimeConfig.linkIntent.deployFiles.empty()); - EXPECT_TRUE(m->schemaWarnings.empty()); -} + expect(m->runtimeConfig.linkIntent.deployFiles.empty()); + expect(m->schemaWarnings.empty()); +}}; -TEST(Manifest, RuntimeDeployRefusesEachMalformedEntryNamingIt) { +UTest runtime_deploy_refuses_each_malformed_entry_naming_it{"Manifest/RuntimeDeployRefusesEachMalformedEntryNamingIt", [] { const std::pair cases[] = { {R"({ from = "a.json", to = "../outside" })", "`to` has a `.` or `..` component"}, {R"({ from = "/etc/a.json", to = "x" })", "`from` is absolute"}, @@ -5856,17 +5793,15 @@ version = "0.1.0" deploy = [ {} ] )", entry); auto m = mcpp::manifest::parse_string(src); - ASSERT_FALSE(m.has_value()) << entry; - EXPECT_NE(m.error().message.find(expected), std::string::npos) - << entry << " -> " << m.error().message; - EXPECT_NE(m.error().message.find("runtime.deploy[1]"), std::string::npos) - << m.error().message; + expect(fatal(!m.has_value())) << entry; + if (auto&& failed = expect(ne(m.error().message.find(expected), std::string::npos)); !failed) failed << entry << " -> " << m.error().message; + if (auto&& failed = expect(ne(m.error().message.find("runtime.deploy[1]"), std::string::npos)); !failed) failed << m.error().message; } -} +}}; // ── `requires_abi` (design 2026-09-12, section 5.2) ──────────────────────── -TEST(Manifest, RequiresAbiThreadsParsesOnThePackageAndOnAFeature) { +UTest requires_abi_threads_parses_on_the_package_and_on_a_feature{"Manifest/RequiresAbiThreadsParsesOnThePackageAndOnAFeature", [] { constexpr auto src = R"( [package] name = "wasmrt" @@ -5877,20 +5812,20 @@ mt = { requires_abi = { threads = true } } st = { requires_abi = { threads = false } } )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_TRUE(m->requiresAbiThreads); - ASSERT_TRUE(m->featureRequiresAbiThreads.contains("mt")); - EXPECT_TRUE(m->featureRequiresAbiThreads.at("mt")); - ASSERT_TRUE(m->featureRequiresAbiThreads.contains("st")); - EXPECT_FALSE(m->featureRequiresAbiThreads.at("st")); - EXPECT_TRUE(m->schemaWarnings.empty()); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(bool(m->requiresAbiThreads)); + expect(fatal(m->featureRequiresAbiThreads.contains("mt"))); + expect(bool(m->featureRequiresAbiThreads.at("mt"))); + expect(fatal(m->featureRequiresAbiThreads.contains("st"))); + expect(!(m->featureRequiresAbiThreads.at("st"))); + expect(m->schemaWarnings.empty()); +}}; // `exceptions` joined `threads` as the second `abi` member 2026-09-12 (the UI // framework record, section 2.1, A1); the accepted set and every refusal // below name both. See tests/unit/test_abi.cpp for the member's own coverage // (parsing, rendering, the target-axis `requires_abi` forms). -TEST(Manifest, AbiTablesRefuseAnythingButABooleanMember) { +UTest abi_tables_refuse_anything_but_a_boolean_member{"Manifest/AbiTablesRefuseAnythingButABooleanMember", [] { const std::pair cases[] = { {"[package]\nname = \"a\"\nversion = \"0.1.0\"\nrequires_abi = true\n", "[package] requires_abi must be a table such as `{ threads = true }`"}, @@ -5905,18 +5840,17 @@ TEST(Manifest, AbiTablesRefuseAnythingButABooleanMember) { }; for (auto [src, expected] : cases) { auto m = mcpp::manifest::parse_string(src); - ASSERT_FALSE(m.has_value()) << src; - EXPECT_NE(m.error().message.find(expected), std::string::npos) - << src << " -> " << m.error().message; + expect(fatal(!m.has_value())) << src; + if (auto&& failed = expect(ne(m.error().message.find(expected), std::string::npos)); !failed) failed << src << " -> " << m.error().message; } -} +}}; // ── The per-target tool declaration (design 2026-09-12, section 5.1) ─────── // // A package written directly under `[target..xlings]` is refused, and // the refusal names the table that does accept it, which is the statement the // SDK batch's record was missing when it concluded the declaration did not exist. -TEST(Manifest, TargetXlingsRefusalNamesTheWorkspaceTable) { +UTest target_xlings_refusal_names_the_workspace_table{"Manifest/TargetXlingsRefusalNamesTheWorkspaceTable", [] { constexpr auto src = R"( [package] name = "app" @@ -5925,20 +5859,18 @@ version = "0.1.0" "xim:apple-simulator-tools" = "" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().message.find( - "[target.aarch64-ios-sim.xlings] does not accept 'xim:apple-simulator-tools'"), - std::string::npos) << m.error().message; - EXPECT_NE(m.error().message.find("[target.aarch64-ios-sim.xlings.workspace]"), - std::string::npos) << m.error().message; -} + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().message.find( + "[target.aarch64-ios-sim.xlings] does not accept 'xim:apple-simulator-tools'"), std::string::npos)); !failed) failed << m.error().message; + if (auto&& failed = expect(ne(m.error().message.find("[target.aarch64-ios-sim.xlings.workspace]"), std::string::npos)); !failed) failed << m.error().message; +}}; // ─── `linkage` beside `kind`: a library's default form (#642 E1) ──────────── // // `kind = "shared"` is a constraint; `linkage` is the answer a consumer gets // when it asks for nothing. The parse accepts the default on a library, refuses // it beside the constraint and on a program, and a row states one of the two. -TEST(Manifest, TargetLinkageIsParsedAsTheLibrarysDefaultForm) { +UTest target_linkage_is_parsed_as_the_librarys_default_form{"Manifest/TargetLinkageIsParsedAsTheLibrarysDefaultForm", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "fw" @@ -5947,17 +5879,17 @@ version = "0.1.0" kind = "lib" linkage = "shared" )"); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->targets.size(), 1u); - EXPECT_EQ(m->targets[0].kind, mcpp::manifest::Target::Library); - EXPECT_EQ(m->targets[0].linkageDefault, "shared"); - EXPECT_EQ(m->targets[0].linkageDeclaredBy, "[targets.fw] linkage = \"shared\""); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->targets.size(), 1u))); + expect(eq(m->targets[0].kind, mcpp::manifest::Target::Library)); + expect(eq(m->targets[0].linkageDefault, "shared")); + expect(eq(m->targets[0].linkageDeclaredBy, "[targets.fw] linkage = \"shared\"")); // A known key: no schema warning. for (auto const& w : m->schemaWarnings) - EXPECT_EQ(w.find("'linkage'"), std::string::npos) << w; -} + expect(eq(w.find("'linkage'"), std::string::npos)) << w; +}}; -TEST(Manifest, TargetLinkageIsRefusedBesideKindSharedAndOnAProgram) { +UTest target_linkage_is_refused_beside_kind_shared_and_on_a_program{"Manifest/TargetLinkageIsRefusedBesideKindSharedAndOnAProgram", [] { auto both = mcpp::manifest::parse_string(R"( [package] name = "fw" @@ -5966,10 +5898,9 @@ version = "0.1.0" kind = "shared" linkage = "static" )"); - ASSERT_FALSE(both.has_value()); - EXPECT_NE(both.error().message.find( - "states both `kind = \"shared\"` and `linkage = \"static\"`"), - std::string::npos) << both.error().message; + expect(fatal(!both.has_value())); + if (auto&& failed = expect(ne(both.error().message.find( + "states both `kind = \"shared\"` and `linkage = \"static\"`"), std::string::npos)); !failed) failed << both.error().message; auto program = mcpp::manifest::parse_string(R"( [package] @@ -5980,9 +5911,8 @@ kind = "bin" main = "src/main.cpp" linkage = "shared" )"); - ASSERT_FALSE(program.has_value()); - EXPECT_NE(program.error().message.find("a program target has no link form"), - std::string::npos) << program.error().message; + expect(fatal(!program.has_value())); + if (auto&& failed = expect(ne(program.error().message.find("a program target has no link form"), std::string::npos)); !failed) failed << program.error().message; auto value = mcpp::manifest::parse_string(R"( [package] @@ -5992,12 +5922,11 @@ version = "0.1.0" kind = "lib" linkage = "dynamic" )"); - ASSERT_FALSE(value.has_value()); - EXPECT_NE(value.error().message.find("`static` or `shared`"), std::string::npos) - << value.error().message; -} + expect(fatal(!value.has_value())); + if (auto&& failed = expect(ne(value.error().message.find("`static` or `shared`"), std::string::npos)); !failed) failed << value.error().message; +}}; -TEST(Manifest, ARowStatesEitherKindOrLinkage) { +UTest a_row_states_either_kind_or_linkage{"Manifest/ARowStatesEitherKindOrLinkage", [] { auto ok = mcpp::manifest::parse_string(R"( [package] name = "fw" @@ -6007,15 +5936,14 @@ kind = "lib" [target.'cfg(os = "linux")'.targets.fw] linkage = "shared" )"); - ASSERT_TRUE(ok.has_value()) << ok.error().format(); - ASSERT_EQ(ok->conditionalConfigs.size(), 1u); + if (auto&& failed = expect(fatal(ok.has_value())); !failed) failed << ok.error().format(); + expect(fatal(eq(ok->conditionalConfigs.size(), 1u))); auto const& rows = ok->conditionalConfigs[0].targetKinds; - ASSERT_EQ(rows.count("fw"), 1u); - EXPECT_EQ(rows.at("fw").linkage, "shared"); - EXPECT_EQ(rows.at("fw").statement, - "[target.'cfg(os = \"linux\")'.targets.fw] linkage = \"shared\""); + expect(fatal(eq(rows.count("fw"), 1u))); + expect(eq(rows.at("fw").linkage, "shared")); + expect(eq(rows.at("fw").statement, "[target.'cfg(os = \"linux\")'.targets.fw] linkage = \"shared\"")); for (auto const& w : ok->schemaWarnings) - EXPECT_EQ(w.find("unsupported key"), std::string::npos) << w; + expect(eq(w.find("unsupported key"), std::string::npos)) << w; auto both = mcpp::manifest::parse_string(R"( [package] @@ -6027,16 +5955,15 @@ kind = "lib" kind = "shared" linkage = "shared" )"); - ASSERT_FALSE(both.has_value()); - EXPECT_NE(both.error().message.find("states both `kind` and `linkage`"), - std::string::npos) << both.error().message; -} + expect(fatal(!both.has_value())); + if (auto&& failed = expect(ne(both.error().message.find("states both `kind` and `linkage`"), std::string::npos)); !failed) failed << both.error().message; +}}; namespace { mcpp::manifest::Manifest merged_for_linux(std::string_view src) { auto m = mcpp::manifest::parse_string(src); - EXPECT_TRUE(m.has_value()) << (m.has_value() ? "" : m.error().format()); + if (auto&& failed = expect(m.has_value()); !failed) failed << (m.has_value() ? "" : m.error().format()); if (!m) return {}; mcpp::build::merge_conditional_config( *m, mcpp::build::cfgpred::context_for("x86_64-unknown-linux-gnu")); @@ -6045,7 +5972,7 @@ mcpp::manifest::Manifest merged_for_linux(std::string_view src) { } // namespace -TEST(Manifest, ARowDefaultReplacesAnUnconditionalKindShared) { +UTest a_row_default_replaces_an_unconditional_kind_shared{"Manifest/ARowDefaultReplacesAnUnconditionalKindShared", [] { auto m = merged_for_linux(R"( [package] name = "fw" @@ -6055,12 +5982,12 @@ kind = "shared" [target.'cfg(os = "linux")'.targets.fw] linkage = "static" )"); - ASSERT_EQ(m.targets.size(), 1u); - EXPECT_EQ(m.targets[0].kind, mcpp::manifest::Target::Library); - EXPECT_EQ(m.targets[0].linkageDefault, "static"); -} + expect(fatal(eq(m.targets.size(), 1u))); + expect(eq(m.targets[0].kind, mcpp::manifest::Target::Library)); + expect(eq(m.targets[0].linkageDefault, "static")); +}}; -TEST(Manifest, ARowKindClearsAnUnconditionalDefault) { +UTest a_row_kind_clears_an_unconditional_default{"Manifest/ARowKindClearsAnUnconditionalDefault", [] { auto m = merged_for_linux(R"( [package] name = "fw" @@ -6071,12 +5998,12 @@ linkage = "shared" [target.'cfg(os = "linux")'.targets.fw] kind = "lib" )"); - ASSERT_EQ(m.targets.size(), 1u); - EXPECT_EQ(m.targets[0].kind, mcpp::manifest::Target::Library); - EXPECT_TRUE(m.targets[0].linkageDefault.empty()); -} + expect(fatal(eq(m.targets.size(), 1u))); + expect(eq(m.targets[0].kind, mcpp::manifest::Target::Library)); + expect(m.targets[0].linkageDefault.empty()); +}}; -TEST(Manifest, ARowThatDoesNotMatchLeavesTheUnconditionalStatement) { +UTest a_row_that_does_not_match_leaves_the_unconditional_statement{"Manifest/ARowThatDoesNotMatchLeavesTheUnconditionalStatement", [] { auto m = merged_for_linux(R"( [package] name = "fw" @@ -6087,14 +6014,14 @@ linkage = "shared" [target.'cfg(os = "windows")'.targets.fw] kind = "shared" )"); - ASSERT_EQ(m.targets.size(), 1u); - EXPECT_EQ(m.targets[0].kind, mcpp::manifest::Target::Library); - EXPECT_EQ(m.targets[0].linkageDefault, "shared"); -} + expect(fatal(eq(m.targets.size(), 1u))); + expect(eq(m.targets[0].kind, mcpp::manifest::Target::Library)); + expect(eq(m.targets[0].linkageDefault, "shared")); +}}; // #709: `[features.] tools` names the package's own bin targets, and is // checked once inference has produced the target list. -TEST(Manifest, AFeatureToolMustNameABinTargetOfThePackage) { +UTest a_feature_tool_must_name_a_bin_target_of_the_package{"Manifest/AFeatureToolMustNameABinTargetOfThePackage", [] { auto dir = std::filesystem::temp_directory_path() / std::format("mcpp_feature_tools_{}", std::random_device{}()); std::filesystem::create_directories(dir / "src"); @@ -6107,19 +6034,19 @@ TEST(Manifest, AFeatureToolMustNameABinTargetOfThePackage) { }; write("gen-tool"); auto ok = mcpp::manifest::load(dir / "mcpp.toml"); - ASSERT_TRUE(ok) << (ok ? "" : ok.error().message); - ASSERT_EQ(ok->featureTools.at("codegen").size(), 1u); - EXPECT_EQ(ok->featureTools.at("codegen")[0], "gen-tool"); + if (auto&& failed = expect(fatal(bool(ok))); !failed) failed << (ok ? "" : ok.error().message); + expect(fatal(eq(ok->featureTools.at("codegen").size(), 1u))); + expect(eq(ok->featureTools.at("codegen")[0], "gen-tool")); write("nosuch"); auto bad = mcpp::manifest::load(dir / "mcpp.toml"); - ASSERT_FALSE(bad); - EXPECT_NE(bad.error().message.find("nosuch"), std::string::npos) << bad.error().message; - EXPECT_NE(bad.error().message.find("gen-tool"), std::string::npos) << bad.error().message; + expect(fatal(!bad)); + if (auto&& failed = expect(ne(bad.error().message.find("nosuch"), std::string::npos)); !failed) failed << bad.error().message; + if (auto&& failed = expect(ne(bad.error().message.find("gen-tool"), std::string::npos)); !failed) failed << bad.error().message; std::error_code ec; std::filesystem::remove_all(dir, ec); -} +}}; // #714: `sources = []` states that the default build compiles nothing, so a // module interface under `src/` is not a library of that build. A build-logic @@ -6127,7 +6054,7 @@ TEST(Manifest, AFeatureToolMustNameABinTargetOfThePackage) { // request; inferring a library for it made every build of the package link an // archive with no inputs. A glob that happens to match nothing is a different // statement and keeps the inferred library (and #533's empty-link refusal). -TEST(Manifest, AnExplicitlyEmptySourceListInfersNoLibrary) { +UTest an_explicitly_empty_source_list_infers_no_library{"Manifest/AnExplicitlyEmptySourceListInfersNoLibrary", [] { auto dir = std::filesystem::temp_directory_path() / std::format("mcpp_empty_sources_{}", std::random_device{}()); std::filesystem::create_directories(dir / "src"); @@ -6139,8 +6066,8 @@ TEST(Manifest, AnExplicitlyEmptySourceListInfersNoLibrary) { "[features.host]\nsources = [\"src/buildlib.cppm\"]\n"; } auto empty = mcpp::manifest::load(dir / "mcpp.toml"); - ASSERT_TRUE(empty) << (empty ? "" : empty.error().message); - EXPECT_TRUE(empty->targets.empty()); + if (auto&& failed = expect(fatal(bool(empty))); !failed) failed << (empty ? "" : empty.error().message); + expect(empty->targets.empty()); { std::ofstream(dir / "mcpp.toml") @@ -6148,19 +6075,19 @@ TEST(Manifest, AnExplicitlyEmptySourceListInfersNoLibrary) { "[build]\nsources = [\"srcs/**/*.cppm\"]\n"; } auto typo = mcpp::manifest::load(dir / "mcpp.toml"); - ASSERT_TRUE(typo) << (typo ? "" : typo.error().message); - ASSERT_EQ(typo->targets.size(), 1u); - EXPECT_EQ(typo->targets[0].kind, mcpp::manifest::Target::Library); + if (auto&& failed = expect(fatal(bool(typo))); !failed) failed << (typo ? "" : typo.error().message); + expect(fatal(eq(typo->targets.size(), 1u))); + expect(eq(typo->targets[0].kind, mcpp::manifest::Target::Library)); std::error_code ec; std::filesystem::remove_all(dir, ec); -} +}}; // #649 E6: a consumer reads "every declared target is a program" as "a tool // provider that contributes nothing to my graph". A target list the loader // INFERRED from the tree is not that statement, so the loader says which of // the two it produced. -TEST(Manifest, TargetsInferredIsDistinguishedFromDeclared) { +UTest targets_inferred_is_distinguished_from_declared{"Manifest/TargetsInferredIsDistinguishedFromDeclared", [] { auto dir = std::filesystem::temp_directory_path() / std::format("mcpp_targets_inferred_{}", std::random_device{}()); std::filesystem::create_directories(dir / "src"); @@ -6169,10 +6096,10 @@ TEST(Manifest, TargetsInferredIsDistinguishedFromDeclared) { std::ofstream(dir / "mcpp.toml") << "[package]\nname = \"inferred\"\nversion = \"0.1.0\"\n"; } auto inferred = mcpp::manifest::load(dir / "mcpp.toml"); - ASSERT_TRUE(inferred) << (inferred ? "" : inferred.error().message); - ASSERT_EQ(inferred->targets.size(), 1u); - EXPECT_EQ(inferred->targets[0].kind, mcpp::manifest::Target::Binary); - EXPECT_TRUE(inferred->targetsInferred); + if (auto&& failed = expect(fatal(bool(inferred))); !failed) failed << (inferred ? "" : inferred.error().message); + expect(fatal(eq(inferred->targets.size(), 1u))); + expect(eq(inferred->targets[0].kind, mcpp::manifest::Target::Binary)); + expect(bool(inferred->targetsInferred)); { std::ofstream(dir / "mcpp.toml") @@ -6180,20 +6107,20 @@ TEST(Manifest, TargetsInferredIsDistinguishedFromDeclared) { "[targets.declared]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n"; } auto declared = mcpp::manifest::load(dir / "mcpp.toml"); - ASSERT_TRUE(declared) << (declared ? "" : declared.error().message); - ASSERT_EQ(declared->targets.size(), 1u); - EXPECT_FALSE(declared->targetsInferred); + if (auto&& failed = expect(fatal(bool(declared))); !failed) failed << (declared ? "" : declared.error().message); + expect(fatal(eq(declared->targets.size(), 1u))); + expect(!declared->targetsInferred); std::error_code ec; std::filesystem::remove_all(dir, ec); -} +}}; // ── #647 E1 / #649 X4: `[package.metadata]` and the `[package]` key check ──── // // The metadata table is kept verbatim for the graph document, as JSON text, and // `metadata` is a known key. Any other key the parser does not read is reported // the way `[build]` reports one, so `licence` is no longer accepted in silence. -TEST(Manifest, PackageMetadataIsKeptVerbatimAsJson) { +UTest package_metadata_is_kept_verbatim_as_json{"Manifest/PackageMetadataIsKeptVerbatimAsJson", [] { constexpr auto src = R"( [package] name = "x" @@ -6206,20 +6133,18 @@ enabled = true note = "a \"quoted\" value" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_EQ(m->packageMetadataJson, - R"({"demo":{"count":3,"enabled":true,"langs":["en","zh"],"note":"a \"quoted\" value","resources":"res"}})"); - EXPECT_TRUE(m->schemaWarnings.empty()) - << (m->schemaWarnings.empty() ? "" : m->schemaWarnings[0]); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(eq(m->packageMetadataJson, R"({"demo":{"count":3,"enabled":true,"langs":["en","zh"],"note":"a \"quoted\" value","resources":"res"}})")); + if (auto&& failed = expect(m->schemaWarnings.empty()); !failed) failed << (m->schemaWarnings.empty() ? "" : m->schemaWarnings[0]); +}}; -TEST(Manifest, PackageWithoutMetadataHasNone) { +UTest package_without_metadata_has_none{"Manifest/PackageWithoutMetadataHasNone", [] { auto m = mcpp::manifest::parse_string("[package]\nname = \"x\"\nversion = \"0.1.0\"\n"); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_TRUE(m->packageMetadataJson.empty()); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(m->packageMetadataJson.empty()); +}}; -TEST(Manifest, UnknownPackageKeyIsReported) { +UTest unknown_package_key_is_reported{"Manifest/UnknownPackageKeyIsReported", [] { constexpr auto src = R"( [package] name = "x" @@ -6227,13 +6152,12 @@ version = "0.1.0" licence = "MIT" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->schemaWarnings.size(), 1u); - EXPECT_NE(m->schemaWarnings[0].find("[package] has unsupported key 'licence'"), - std::string::npos) << m->schemaWarnings[0]; -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->schemaWarnings.size(), 1u))); + if (auto&& failed = expect(ne(m->schemaWarnings[0].find("[package] has unsupported key 'licence'"), std::string::npos)); !failed) failed << m->schemaWarnings[0]; +}}; -TEST(Manifest, EveryReadPackageKeyIsKnown) { +UTest every_read_package_key_is_known{"Manifest/EveryReadPackageKeyIsKnown", [] { constexpr auto src = R"( [package] name = "x" @@ -6255,16 +6179,16 @@ std-module-flags = ["-x"] c-environment = "platform" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); for (auto const& w : m->schemaWarnings) - EXPECT_EQ(w.find("[package] has unsupported key"), std::string::npos) << w; - EXPECT_EQ(m->cEnvironment, "platform"); -} + expect(eq(w.find("[package] has unsupported key"), std::string::npos)) << w; + expect(eq(m->cEnvironment, "platform")); +}}; // ── `[c-abi]` — the C environment a `mcpp:c-abi=` provider declares // (design 2026-09-18 §3.2) ────────────────────────────────────────────────── -TEST(Manifest, CAbiBlockParsesOnAProviderOfTheLayer) { +UTest c_abi_block_parses_on_a_provider_of_the_layer{"Manifest/CAbiBlockParsesOnAProviderOfTheLayer", [] { constexpr auto src = R"( [package] name = "openkal-musl" @@ -6278,17 +6202,17 @@ wchar = 32 builtins = "iso" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_TRUE(m->cAbiDecl.has_value()); - EXPECT_TRUE(m->cAbiDecl->declared); - EXPECT_EQ(m->cAbiDecl->presents, mcpp::targetside::CAbiPresents::Posix); - EXPECT_EQ(m->cAbiDecl->dataModel, mcpp::targetside::CAbiDataModel::ArchDefault); - EXPECT_EQ(m->cAbiDecl->wcharBits, 32); - EXPECT_EQ(m->cAbiDecl->builtins, mcpp::targetside::CAbiBuiltins::Iso); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(m->cAbiDecl.has_value())); + expect(bool(m->cAbiDecl->declared)); + expect(eq(m->cAbiDecl->presents, mcpp::targetside::CAbiPresents::Posix)); + expect(eq(m->cAbiDecl->dataModel, mcpp::targetside::CAbiDataModel::ArchDefault)); + expect(eq(m->cAbiDecl->wcharBits, 32)); + expect(eq(m->cAbiDecl->builtins, mcpp::targetside::CAbiBuiltins::Iso)); +}}; // `builtins` alone defaults — the other three carry no default (§3.2). -TEST(Manifest, CAbiBuiltinsDefaultsToPlatform) { +UTest c_abi_builtins_defaults_to_platform{"Manifest/CAbiBuiltinsDefaultsToPlatform", [] { constexpr auto src = R"( [package] name = "openkal-musl" @@ -6301,15 +6225,15 @@ data-model = "arch-default" wchar = 32 )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_TRUE(m->cAbiDecl.has_value()); - EXPECT_EQ(m->cAbiDecl->builtins, mcpp::targetside::CAbiBuiltins::Platform); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(m->cAbiDecl.has_value())); + expect(eq(m->cAbiDecl->builtins, mcpp::targetside::CAbiBuiltins::Platform)); +}}; // A package that does not provide the layer has no standing to state what // environment it presents — an internal contradiction, refused rather than // silently ignored (§3.2, gap #3 of the design's own self-review). -TEST(Manifest, CAbiBlockWithoutProvidingTheLayerIsRefused) { +UTest c_abi_block_without_providing_the_layer_is_refused{"Manifest/CAbiBlockWithoutProvidingTheLayerIsRefused", [] { constexpr auto src = R"( [package] name = "not-a-c-library" @@ -6321,13 +6245,12 @@ data-model = "arch-default" wchar = 32 )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().message.find( - "[package] provides does not list `mcpp:c-abi=`"), - std::string::npos) << m.error().message; -} + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().message.find( + "[package] provides does not list `mcpp:c-abi=`"), std::string::npos)); !failed) failed << m.error().message; +}}; -TEST(Manifest, CAbiRequiresAllThreeKeysWithNoDefault) { +UTest c_abi_requires_all_three_keys_with_no_default{"Manifest/CAbiRequiresAllThreeKeysWithNoDefault", [] { const std::pair cases[] = { {R"([package] name = "a" @@ -6356,13 +6279,12 @@ data-model = "arch-default" }; for (auto [src, expected] : cases) { auto m = mcpp::manifest::parse_string(src); - ASSERT_FALSE(m.has_value()) << src; - EXPECT_NE(m.error().message.find(expected), std::string::npos) - << src << " -> " << m.error().message; + expect(fatal(!m.has_value())) << src; + if (auto&& failed = expect(ne(m.error().message.find(expected), std::string::npos)); !failed) failed << src << " -> " << m.error().message; } -} +}}; -TEST(Manifest, CAbiClosedValueSetsRejectAMisspelling) { +UTest c_abi_closed_value_sets_reject_a_misspelling{"Manifest/CAbiClosedValueSetsRejectAMisspelling", [] { const std::pair cases[] = { {R"([package] name = "a" @@ -6414,15 +6336,14 @@ extra = 1 }; for (auto [src, expected] : cases) { auto m = mcpp::manifest::parse_string(src); - ASSERT_FALSE(m.has_value()) << src; - EXPECT_NE(m.error().message.find(expected), std::string::npos) - << src << " -> " << m.error().message; + expect(fatal(!m.has_value())) << src; + if (auto&& failed = expect(ne(m.error().message.find(expected), std::string::npos)); !failed) failed << src << " -> " << m.error().message; } -} +}}; // A package with no [c-abi] block resolves `cAbiDecl` to nullopt — the // absent-block case §3.2 requires to be byte-identical to today. -TEST(Manifest, NoCAbiBlockLeavesCAbiDeclEmpty) { +UTest no_c_abi_block_leaves_c_abi_decl_empty{"Manifest/NoCAbiBlockLeavesCAbiDeclEmpty", [] { constexpr auto src = R"( [package] name = "openkal-musl" @@ -6430,12 +6351,12 @@ version = "0.15.0" provides = ["mcpp:c-abi=musl"] )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_FALSE(m->cAbiDecl.has_value()); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(!(m->cAbiDecl.has_value())); +}}; // `[package] c-environment` — design §3.4. The only value is "platform". -TEST(Manifest, CEnvironmentAcceptsOnlyPlatform) { +UTest c_environment_accepts_only_platform{"Manifest/CEnvironmentAcceptsOnlyPlatform", [] { constexpr auto ok = R"( [package] name = "openkal-windows" @@ -6444,8 +6365,8 @@ provides = ["mcpp:kernel-abi=openkal"] c-environment = "platform" )"; auto m = mcpp::manifest::parse_string(ok); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_EQ(m->cEnvironment, "platform"); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(eq(m->cEnvironment, "platform")); constexpr auto bad = R"( [package] @@ -6454,11 +6375,10 @@ version = "0.1.0" c-environment = "native" )"; auto m2 = mcpp::manifest::parse_string(bad); - ASSERT_FALSE(m2.has_value()); - EXPECT_NE(m2.error().message.find( - "c-environment = \"native\" names no known override"), - std::string::npos) << m2.error().message; -} + expect(fatal(!m2.has_value())); + if (auto&& failed = expect(ne(m2.error().message.find( + "c-environment = \"native\" names no known override"), std::string::npos)); !failed) failed << m2.error().message; +}}; // `c-environment = "platform"` is INFERRED for a `mcpp:kernel-abi=` // provider (coordinator revision, openkal-musl spike: openkal-windows called @@ -6476,7 +6396,7 @@ c-environment = "native" // that prose has no executor — it was contradicted once already (0.16.0: a // disposition accepted for every signal and installed for none). -TEST(Manifest, ACLibraryMayEnumerateWhatItDoesNotSupply) { +UTest ac_library_may_enumerate_what_it_does_not_supply{"Manifest/ACLibraryMayEnumerateWhatItDoesNotSupply", [] { constexpr auto src = R"( [package] name = "openkal-musl" @@ -6494,21 +6414,19 @@ fork = { form = "link" } mprotect = { form = "enosys", note = "openkal has no operation upon a mapping's protection" } )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_TRUE(m->cAbiDecl.has_value()); - ASSERT_EQ(m->cAbiDecl->absent.size(), 2u); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(m->cAbiDecl.has_value())); + expect(fatal(eq(m->cAbiDecl->absent.size(), 2u))); // Sorted by name, so a diagnostic built from this list reads the same way // on every run and a test may index it. - EXPECT_EQ(m->cAbiDecl->absent[0].name, "fork"); - EXPECT_EQ(m->cAbiDecl->absent[0].form, - mcpp::targetside::CAbiAbsentForm::Link); - EXPECT_EQ(m->cAbiDecl->absent[1].name, "mprotect"); - EXPECT_EQ(m->cAbiDecl->absent[1].form, - mcpp::targetside::CAbiAbsentForm::Enosys); - EXPECT_FALSE(m->cAbiDecl->absent[1].note.empty()); -} - -TEST(Manifest, AnAbsenceWithNoNamedShapeIsRefused) { + expect(eq(m->cAbiDecl->absent[0].name, "fork")); + expect(eq(m->cAbiDecl->absent[0].form, mcpp::targetside::CAbiAbsentForm::Link)); + expect(eq(m->cAbiDecl->absent[1].name, "mprotect")); + expect(eq(m->cAbiDecl->absent[1].form, mcpp::targetside::CAbiAbsentForm::Enosys)); + expect(!(m->cAbiDecl->absent[1].note.empty())); +}}; + +UTest an_absence_with_no_named_shape_is_refused{"Manifest/AnAbsenceWithNoNamedShapeIsRefused", [] { // A facility absent in an unnamed shape is one nothing can assert // against, which is the whole reason for writing it down. constexpr auto src = R"( @@ -6526,12 +6444,12 @@ wchar = 32 fork = { note = "no process image duplication" } )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_FALSE(m.has_value()); + expect(fatal(!m.has_value())); auto msg = m.error().format(); - EXPECT_NE(msg.find("form"), std::string::npos) << msg; -} + expect(ne(msg.find("form"), std::string::npos)) << msg; +}}; -TEST(Manifest, AnUnknownAbsentShapeNamesTheThreeThatExist) { +UTest an_unknown_absent_shape_names_the_three_that_exist{"Manifest/AnUnknownAbsentShapeNamesTheThreeThatExist", [] { constexpr auto src = R"( [package] name = "openkal-musl" @@ -6547,12 +6465,12 @@ wchar = 32 fork = { form = "sometimes" } )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_FALSE(m.has_value()); + expect(fatal(!m.has_value())); auto msg = m.error().format(); - EXPECT_NE(msg.find("accepted-no-effect"), std::string::npos) << msg; -} + expect(ne(msg.find("accepted-no-effect"), std::string::npos)) << msg; +}}; -TEST(Manifest, TheAcceptedNoEffectShapeHasAName) { +UTest the_accepted_no_effect_shape_has_a_name{"Manifest/TheAcceptedNoEffectShapeHasAName", [] { // It is the shape of the defect openkal-musl 0.16.0 repaired: a call that // succeeds and does not do part of what it was asked. Naming it is what // makes "how many of these are there" a question with an answer. @@ -6571,13 +6489,12 @@ wchar = 32 tcsetattr = { form = "accepted-no-effect", note = "the fields openkal does not name are not applied" } )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->cAbiDecl->absent.size(), 1u); - EXPECT_EQ(m->cAbiDecl->absent[0].form, - mcpp::targetside::CAbiAbsentForm::AcceptedNoEffect); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->cAbiDecl->absent.size(), 1u))); + expect(eq(m->cAbiDecl->absent[0].form, mcpp::targetside::CAbiAbsentForm::AcceptedNoEffect)); +}}; -TEST(Manifest, TheAbsenceTableIsTopLevelSoOlderEnginesIgnoreIt) { +UTest the_absence_table_is_top_level_so_older_engines_ignore_it{"Manifest/TheAbsenceTableIsTopLevelSoOlderEnginesIgnoreIt", [] { // WHY THIS TABLE IS NOT `[c-abi].absent`, WHICH READS BETTER. Measured // against the genuine published 2026.9.18.3 archive -- the index floor // when this shipped -- on the exact manifest openkal-musl 0.17.0 @@ -6605,13 +6522,13 @@ wchar = 32 absent = { fork = { form = "link" } } )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_FALSE(m.has_value()); + expect(fatal(!m.has_value())); auto msg = m.error().format(); - EXPECT_NE(msg.find("has no member 'absent'"), std::string::npos) << msg; - EXPECT_NE(msg.find("[c-abi-absent]"), std::string::npos) << msg; -} + expect(ne(msg.find("has no member 'absent'"), std::string::npos)) << msg; + expect(ne(msg.find("[c-abi-absent]"), std::string::npos)) << msg; +}}; -TEST(Manifest, AbsencesMayBeStatedWithoutAnEnvironmentDeclaration) { +UTest absences_may_be_stated_without_an_environment_declaration{"Manifest/AbsencesMayBeStatedWithoutAnEnvironmentDeclaration", [] { // The two tables are independent. A C library whose environment identity // is already its target's default has nothing to put in [c-abi], and is // still entitled to say what it does not supply. `declared` stays false, @@ -6626,14 +6543,14 @@ provides = ["mcpp:c-abi=musl"] fork = { form = "link" } )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_TRUE(m->cAbiDecl.has_value()); - EXPECT_FALSE(m->cAbiDecl->declared); - ASSERT_EQ(m->cAbiDecl->absent.size(), 1u); - EXPECT_EQ(m->cAbiDecl->absent[0].name, "fork"); -} - -TEST(Manifest, OnlyTheCLibraryMayStateWhatIsAbsent) { + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(m->cAbiDecl.has_value())); + expect(!m->cAbiDecl->declared); + expect(fatal(eq(m->cAbiDecl->absent.size(), 1u))); + expect(eq(m->cAbiDecl->absent[0].name, "fork")); +}}; + +UTest only_the_c_library_may_state_what_is_absent{"Manifest/OnlyTheCLibraryMayStateWhatIsAbsent", [] { // The gate [c-abi] applies, for the reason it applies it. Moving the // table to the top level moved it out from behind that gate, so the gate // is restated here rather than inherited -- and this is the test that @@ -6647,12 +6564,12 @@ version = "0.1.0" fork = { form = "link" } )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_FALSE(m.has_value()); + expect(fatal(!m.has_value())); auto msg = m.error().format(); - EXPECT_NE(msg.find("mcpp:c-abi="), std::string::npos) << msg; -} + expect(ne(msg.find("mcpp:c-abi="), std::string::npos)) << msg; +}}; -TEST(Manifest, ACLibraryThatEnumeratesNothingHasClaimedNothing) { +UTest ac_library_that_enumerates_nothing_has_claimed_nothing{"Manifest/ACLibraryThatEnumeratesNothingHasClaimedNothing", [] { constexpr auto src = R"( [package] name = "openkal-musl" @@ -6665,10 +6582,10 @@ data-model = "arch-default" wchar = 32 )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_TRUE(m->cAbiDecl.has_value()); - EXPECT_TRUE(m->cAbiDecl->absent.empty()); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(m->cAbiDecl.has_value())); + expect(m->cAbiDecl->absent.empty()); +}}; // ── [kernel-abi] provides-interfaces / requires-interfaces ───────────────── // @@ -6677,7 +6594,7 @@ wchar = 32 // real names in one direction and an invented one in the other, and neither // changes what the parser does. -TEST(Manifest, AKernelAbiProviderMayStateWhichInterfacesItProvides) { +UTest a_kernel_abi_provider_may_state_which_interfaces_it_provides{"Manifest/AKernelAbiProviderMayStateWhichInterfacesItProvides", [] { constexpr auto src = R"( [package] name = "openkal-windows" @@ -6688,13 +6605,13 @@ provides = ["mcpp:kernel-abi=openkal"] provides-interfaces = ["openkal.abort", "openkal.stream", "openkal.memory"] )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->kernelAbiProvidesInterfaces.size(), 3u); - EXPECT_EQ(m->kernelAbiProvidesInterfaces[0], "openkal.abort"); - EXPECT_TRUE(m->kernelAbiRequiresInterfaces.empty()); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->kernelAbiProvidesInterfaces.size(), 3u))); + expect(eq(m->kernelAbiProvidesInterfaces[0], "openkal.abort")); + expect(m->kernelAbiRequiresInterfaces.empty()); +}}; -TEST(Manifest, APackageThatDoesNotSupplyTheLayerMayNotStateWhatItProvides) { +UTest a_package_that_does_not_supply_the_layer_may_not_state_what_it_provides{"Manifest/APackageThatDoesNotSupplyTheLayerMayNotStateWhatItProvides", [] { // The same rule `[c-abi]` follows, and for the same reason: a package // without the layer is stating a fact about something it does not have. constexpr auto src = R"( @@ -6706,14 +6623,14 @@ version = "1.0.0" provides-interfaces = ["openkal.fs"] )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_FALSE(m.has_value()) + expect(fatal(!m.has_value())) << "a package with no `mcpp:kernel-abi=` in provides must be " "refused when it states provides-interfaces"; auto msg = m.error().format(); - EXPECT_NE(msg.find("mcpp:kernel-abi"), std::string::npos) << msg; -} + expect(ne(msg.find("mcpp:kernel-abi"), std::string::npos)) << msg; +}}; -TEST(Manifest, AnyConsumerMayStateWhichInterfacesItRequires) { +UTest any_consumer_may_state_which_interfaces_it_requires{"Manifest/AnyConsumerMayStateWhichInterfacesItRequires", [] { // `requires-interfaces` is a statement about the package making it, so it // carries no such restriction. openkal SPEC 0.14 §3.3: "a consumer states // its required set per target, in its own package, which is where a @@ -6727,12 +6644,12 @@ version = "1.0.0" requires-interfaces = ["openkal.fs", "openkal.net"] )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->kernelAbiRequiresInterfaces.size(), 2u); - EXPECT_TRUE(m->kernelAbiProvidesInterfaces.empty()); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->kernelAbiRequiresInterfaces.size(), 2u))); + expect(m->kernelAbiProvidesInterfaces.empty()); +}}; -TEST(Manifest, AnUnknownKernelAbiMemberIsAParseErrorNamingTheKey) { +UTest an_unknown_kernel_abi_member_is_a_parse_error_naming_the_key{"Manifest/AnUnknownKernelAbiMemberIsAParseErrorNamingTheKey", [] { constexpr auto src = R"( [package] name = "openkal-linux" @@ -6743,12 +6660,12 @@ provides = ["mcpp:kernel-abi=openkal"] provides-interface = ["openkal.fs"] )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_FALSE(m.has_value()) << "a misspelled member must not be ignored"; + expect(fatal(!m.has_value())) << "a misspelled member must not be ignored"; auto msg = m.error().format(); - EXPECT_NE(msg.find("provides-interface"), std::string::npos) << msg; -} + expect(ne(msg.find("provides-interface"), std::string::npos)) << msg; +}}; -TEST(Manifest, APackageWithNoKernelAbiTableStatesNothing) { +UTest a_package_with_no_kernel_abi_table_states_nothing{"Manifest/APackageWithNoKernelAbiTableStatesNothing", [] { // The guarantee every addition to this parser owes the graph that predates // it: a manifest that writes nothing here is byte-identical in effect. constexpr auto src = R"( @@ -6757,12 +6674,12 @@ name = "some-library" version = "1.0.0" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_TRUE(m->kernelAbiProvidesInterfaces.empty()); - EXPECT_TRUE(m->kernelAbiRequiresInterfaces.empty()); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(m->kernelAbiProvidesInterfaces.empty()); + expect(m->kernelAbiRequiresInterfaces.empty()); +}}; -TEST(Manifest, CEnvironmentIsInferredForAKernelAbiProvider) { +UTest c_environment_is_inferred_for_a_kernel_abi_provider{"Manifest/CEnvironmentIsInferredForAKernelAbiProvider", [] { constexpr auto kernelAbiPkg = R"( [package] name = "openkal-windows" @@ -6770,8 +6687,8 @@ version = "0.8.0" provides = ["mcpp:kernel-abi=openkal"] )"; auto m = mcpp::manifest::parse_string(kernelAbiPkg); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_EQ(m->cEnvironment, "platform") + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(eq(m->cEnvironment, "platform")) << "a kernel-abi provider must default to the platform boundary " "without having to declare c-environment itself"; @@ -6781,8 +6698,8 @@ name = "some-library" version = "1.0.0" )"; auto m2 = mcpp::manifest::parse_string(ordinaryPkg); - ASSERT_TRUE(m2.has_value()) << m2.error().format(); - EXPECT_TRUE(m2->cEnvironment.empty()) + if (auto&& failed = expect(fatal(m2.has_value())); !failed) failed << m2.error().format(); + expect(m2->cEnvironment.empty()) << "an ordinary package must NOT be inferred into the platform " "boundary -- only a kernel-abi provider is"; @@ -6795,9 +6712,9 @@ version = "0.15.0" provides = ["mcpp:c-abi=musl"] )"; auto m3 = mcpp::manifest::parse_string(cAbiPkg); - ASSERT_TRUE(m3.has_value()) << m3.error().format(); - EXPECT_TRUE(m3->cEnvironment.empty()); -} + if (auto&& failed = expect(fatal(m3.has_value())); !failed) failed << m3.error().format(); + expect(m3->cEnvironment.empty()); +}}; // The same inference, through the OTHER manifest parser: an xpkg descriptor // (index packages -- exactly how a released kernel-abi implementation like @@ -6805,7 +6722,7 @@ provides = ["mcpp:c-abi=musl"] // at all (docs/22's own documented gap), so the inference here is // unconditional rather than "if empty" -- and this is the test that pins // that it still runs. -TEST(SynthesizeFromXpkgLua, CEnvironmentIsInferredForAKernelAbiProvider) { +UTest synthesize_from_xpkg_lua_c_environment_is_inferred_for_a_kernel_abi_provider{"SynthesizeFromXpkgLua/CEnvironmentIsInferredForAKernelAbiProvider", [] { constexpr auto lua = R"( package = { spec = "1", @@ -6820,9 +6737,9 @@ package = { )"; auto m = mcpp::manifest::synthesize_from_xpkg_lua( lua, "openkal-windows", "0.8.0", mcpp::toolchain::platform::HostPlatform::current()); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_EQ(m->cEnvironment, "platform"); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(eq(m->cEnvironment, "platform")); +}}; // ── #728, D7: matching conditional tables apply in order of specificity ────── // @@ -6831,22 +6748,22 @@ package = { // breaks a tie. The triple below sorts BEFORE `linux` and `cfg(unix)`, so under // the lexical order these tables used to apply in, the family table was the // last word on an aarch64 Linux target. -TEST(ConditionalOrder, SpecificityRanksATripleOverAnOsOverAFamily) { +UTest specificity_ranks_a_triple_over_an_os_over_a_family{"ConditionalOrder/SpecificityRanksATripleOverAnOsOverAFamily", [] { using mcpp::manifest::cfg::specificity; - EXPECT_EQ(specificity("x86_64-unknown-linux-gnu"), mcpp::manifest::cfg::kTripleRank); - EXPECT_EQ(specificity("linux"), 2); - EXPECT_EQ(specificity("cfg(os = \"linux\")"), 2); - EXPECT_EQ(specificity("unix"), 1); - EXPECT_EQ(specificity("cfg(family = \"unix\")"), 1); - EXPECT_EQ(specificity("cfg(arch = \"x86_64\")"), 1); - EXPECT_EQ(specificity("cfg(all(os = \"linux\", arch = \"aarch64\"))"), 3); - EXPECT_EQ(specificity("cfg(any(linux, windows))"), 0); - EXPECT_EQ(specificity("cfg(not(windows))"), 0); - EXPECT_EQ(specificity("cfg(c-abi = \"musl\")"), 0); - EXPECT_GT(specificity("aarch64-unknown-linux-gnu"), specificity("cfg(all(os = \"linux\", arch = \"aarch64\"))")); -} - -TEST(ConditionalOrder, AMoreSpecificTableAppliesLater) { + expect(eq(specificity("x86_64-unknown-linux-gnu"), mcpp::manifest::cfg::kTripleRank)); + expect(eq(specificity("linux"), 2)); + expect(eq(specificity("cfg(os = \"linux\")"), 2)); + expect(eq(specificity("unix"), 1)); + expect(eq(specificity("cfg(family = \"unix\")"), 1)); + expect(eq(specificity("cfg(arch = \"x86_64\")"), 1)); + expect(eq(specificity("cfg(all(os = \"linux\", arch = \"aarch64\"))"), 3)); + expect(eq(specificity("cfg(any(linux, windows))"), 0)); + expect(eq(specificity("cfg(not(windows))"), 0)); + expect(eq(specificity("cfg(c-abi = \"musl\")"), 0)); + expect(gt(specificity("aarch64-unknown-linux-gnu"), specificity("cfg(all(os = \"linux\", arch = \"aarch64\"))"))); +}}; + +UTest a_more_specific_table_applies_later{"ConditionalOrder/AMoreSpecificTableAppliesLater", [] { constexpr auto src = R"( [package] name = "x" @@ -6865,55 +6782,55 @@ cxxflags = ["-DFAMILY"] threads = false )"; auto parsed = mcpp::manifest::parse_string(src); - ASSERT_TRUE(parsed.has_value()) << parsed.error().format(); + if (auto&& failed = expect(fatal(parsed.has_value())); !failed) failed << parsed.error().format(); std::vector order; for (auto const& cc : parsed->conditionalConfigs) order.push_back(cc.predicate); // The two `cfg(unix)` rows (build, abi) are one table; ties keep text order. - ASSERT_GE(order.size(), 3u); - EXPECT_EQ(order.front(), "cfg(unix)"); - EXPECT_EQ(order.back(), "aarch64-unknown-linux-gnu"); + expect(fatal(ge(order.size(), 3u))); + expect(eq(order.front(), "cfg(unix)")); + expect(eq(order.back(), "aarch64-unknown-linux-gnu")); auto m = manifest_dialect::merged_for(src, "aarch64-unknown-linux-gnu"); const auto& f = m.buildConfig.cxxflags; - ASSERT_EQ(f.size(), 4u); - EXPECT_EQ(f[0], "-DBASE"); - EXPECT_EQ(f[1], "-DFAMILY"); - EXPECT_EQ(f[2], "-DOS"); - EXPECT_EQ(f[3], "-DTRIPLE"); + expect(fatal(eq(f.size(), 4u))); + expect(eq(f[0], "-DBASE")); + expect(eq(f[1], "-DFAMILY")); + expect(eq(f[2], "-DOS")); + expect(eq(f[3], "-DTRIPLE")); // The scalar: the triple states it last, so the triple's value stands. - EXPECT_TRUE(m.buildConfig.abiThreads); -} + expect(bool(m.buildConfig.abiThreads)); +}}; // A tie is broken by the selector text, so the order is the same on every // reader and every run. -TEST(ConditionalOrder, EqualSpecificityKeepsTheSelectorTextOrder) { +UTest equal_specificity_keeps_the_selector_text_order{"ConditionalOrder/EqualSpecificityKeepsTheSelectorTextOrder", [] { std::vector tables(3); tables[0].predicate = "cfg(unix)"; tables[1].predicate = "cfg(arch = \"x86_64\")"; tables[2].predicate = "cfg(env = \"gnu\")"; mcpp::manifest::cfg::order_by_specificity(tables); - EXPECT_EQ(tables[0].predicate, "cfg(arch = \"x86_64\")"); - EXPECT_EQ(tables[1].predicate, "cfg(env = \"gnu\")"); - EXPECT_EQ(tables[2].predicate, "cfg(unix)"); -} + expect(eq(tables[0].predicate, "cfg(arch = \"x86_64\")")); + expect(eq(tables[1].predicate, "cfg(env = \"gnu\")")); + expect(eq(tables[2].predicate, "cfg(unix)")); +}}; // `[test] windows_code_page` (2026.10.5.2): the code page of every discovered // test program, in the closed vocabulary of the target key. -TEST(Manifest, TestWindowsCodePageIsAClosedSetOfTwo) { +UTest test_windows_code_page_is_a_closed_set_of_two{"Manifest/TestWindowsCodePageIsAClosedSetOfTwo", [] { auto ok = mcpp::manifest::parse_string( "[package]\nname = \"p\"\nversion = \"0.1.0\"\n[test]\nwindows_code_page = \"utf-8\"\n"); - ASSERT_TRUE(ok.has_value()) << ok.error().format(); - EXPECT_EQ(ok->testWindowsCodePage, "utf-8"); - EXPECT_TRUE(ok->schemaWarnings.empty()); + if (auto&& failed = expect(fatal(ok.has_value())); !failed) failed << ok.error().format(); + expect(eq(ok->testWindowsCodePage, "utf-8")); + expect(ok->schemaWarnings.empty()); auto bad = mcpp::manifest::parse_string( "[package]\nname = \"p\"\nversion = \"0.1.0\"\n[test]\nwindows_code_page = \"936\"\n"); - ASSERT_FALSE(bad.has_value()); - EXPECT_NE(bad.error().format().find("windows_code_page"), std::string::npos); -} + expect(fatal(!bad.has_value())); + expect(ne(bad.error().format().find("windows_code_page"), std::string::npos)); +}}; // A Form B descriptor states its interface root with `lib`, below the // archive's wrapping directory as its `sources` are (SPEC-008 v0.2, mcpp#797). -TEST(SynthesizeFromXpkgLua, LibStatesTheInterfaceRootBelowTheWrappingDirectory) { +UTest lib_states_the_interface_root_below_the_wrapping_directory{"SynthesizeFromXpkgLua/LibStatesTheInterfaceRootBelowTheWrappingDirectory", [] { constexpr auto src = R"( package = { spec = "1", @@ -6929,23 +6846,22 @@ package = { )"; auto m = mcpp::manifest::synthesize_from_xpkg_lua(src, "tomlpp", "3.4.0", mcpp::toolchain::platform::HostPlatform::current()); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_EQ(m->lib.path.generic_string(), "*/modules/tomlplusplus.cppm"); - EXPECT_TRUE(m->xpkgUnknownKeys.empty()); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(eq(m->lib.path.generic_string(), "*/modules/tomlplusplus.cppm")); + expect(m->xpkgUnknownKeys.empty()); const auto root = std::filesystem::temp_directory_path() / std::format("mcpp-lib-root-{}", std::random_device{}()); std::filesystem::create_directories(root / "tomlplusplus-3.4.0" / "modules"); std::ofstream(root / "tomlplusplus-3.4.0" / "modules" / "tomlplusplus.cppm") << "export module tomlpp;\n"; - EXPECT_EQ(mcpp::manifest::resolve_lib_root_path(*m, root).generic_string(), - "tomlplusplus-3.4.0/modules/tomlplusplus.cppm"); + expect(eq(mcpp::manifest::resolve_lib_root_path(*m, root).generic_string(), "tomlplusplus-3.4.0/modules/tomlplusplus.cppm")); std::error_code ec; std::filesystem::remove_all(root, ec); -} +}}; // Design 2026-10-26 §24.3: an entry target, a program that begins in a // dependency's entry, and the symbol an image is entered at. -TEST(Manifest, ParsesEntryTargetsAndAProgramThatBeginsInOne) { +UTest parses_entry_targets_and_a_program_that_begins_in_one{"Manifest/ParsesEntryTargetsAndAProgramThatBeginsInOne", [] { constexpr auto src = R"( [package] name = "fw" @@ -6963,29 +6879,28 @@ main = { from = "openmui/player" } start = "_start" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_EQ(m->targets.size(), 2u); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(eq(m->targets.size(), 2u))); auto const& entry = m->targets[0].name == "player" ? m->targets[0] : m->targets[1]; auto const& notes = m->targets[0].name == "notes" ? m->targets[0] : m->targets[1]; - EXPECT_EQ(entry.kind, mcpp::manifest::Target::Entry); - EXPECT_FALSE(entry.is_program()); - EXPECT_EQ(entry.main, "src/entry/player.cpp"); - EXPECT_EQ(entry.start, "_reset"); - EXPECT_EQ(entry.windowsEntry, "WinMain"); - EXPECT_EQ(notes.main, ""); - EXPECT_EQ(notes.mainFrom, "openmui/player"); - EXPECT_EQ(notes.start, "_start"); - EXPECT_EQ(notes.startDeclaredBy, "[targets.notes] start = \"_start\""); - EXPECT_TRUE(m->schemaWarnings.empty()); -} - -TEST(Manifest, RefusesWhatAnEntryOrABeginningCannotState) { + expect(eq(entry.kind, mcpp::manifest::Target::Entry)); + expect(!entry.is_program()); + expect(eq(entry.main, "src/entry/player.cpp")); + expect(eq(entry.start, "_reset")); + expect(eq(entry.windowsEntry, "WinMain")); + expect(eq(notes.main, "")); + expect(eq(notes.mainFrom, "openmui/player")); + expect(eq(notes.start, "_start")); + expect(eq(notes.startDeclaredBy, "[targets.notes] start = \"_start\"")); + expect(m->schemaWarnings.empty()); +}}; + +UTest refuses_what_an_entry_or_a_beginning_cannot_state{"Manifest/RefusesWhatAnEntryOrABeginningCannotState", [] { auto refused = [](std::string_view body, std::string_view needle) { auto m = mcpp::manifest::parse_string( std::format("[package]\nname = \"p\"\nversion = \"0.1.0\"\n{}", body)); - ASSERT_FALSE(m.has_value()) << body; - EXPECT_NE(m.error().format().find(needle), std::string::npos) - << body << "\n" << m.error().format(); + expect(fatal(!m.has_value())) << body; + if (auto&& failed = expect(ne(m.error().format().find(needle), std::string::npos)); !failed) failed << body << "\n" << m.error().format(); }; refused("[targets.e]\nkind = \"entry\"\nmain = \"e.cpp\"\nsoname = \"libe.so\"\n", "produces no artifact of its own"); @@ -7003,9 +6918,9 @@ TEST(Manifest, RefusesWhatAnEntryOrABeginningCannotState) { "applies to the target's own entry translation unit"); refused("[test]\nmain = \"t.cpp\"\n", "[test] main is `{ from"); refused("[test]\nstart = \"9\"\n", "[test] start"); -} +}}; -TEST(Manifest, ParsesTestMainFromAndStart) { +UTest parses_test_main_from_and_start{"Manifest/ParsesTestMainFromAndStart", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "p" @@ -7014,14 +6929,14 @@ version = "0.1.0" main = { from = "compat.gtest/main" } start = "_reset" )"); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_EQ(m->testMainFrom, "compat.gtest/main"); - EXPECT_EQ(m->testStart, "_reset"); -} + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(eq(m->testMainFrom, "compat.gtest/main")); + expect(eq(m->testStart, "_reset")); +}}; // A row states a program's or an entry's beginning, and only of such a target; // every file a target names as `main` on any row is recorded on it. -TEST(Manifest, ARowStatesABeginningOfAProgramOrAnEntry) { +UTest a_row_states_a_beginning_of_a_program_or_an_entry{"Manifest/ARowStatesABeginningOfAProgramOrAnEntry", [] { auto tmp = std::filesystem::temp_directory_path() / "mcpp_row_beginning"; std::filesystem::create_directories(tmp); auto path = tmp / "mcpp.toml"; @@ -7040,15 +6955,15 @@ main = "src/entry/player_android.cpp" start = "_reset" )"); auto m = mcpp::manifest::load(path); - ASSERT_TRUE(m) << (m ? "" : m.error().message); + if (auto&& failed = expect(fatal(bool(m))); !failed) failed << (m ? "" : m.error().message); auto it = std::ranges::find_if(m->targets, [](auto const& t) { return t.name == "player"; }); - ASSERT_NE(it, m->targets.end()); - EXPECT_EQ(it->otherMains, (std::vector{"src/entry/player_android.cpp"})); - ASSERT_EQ(m->conditionalConfigs.size(), 1u); + expect(fatal(ne(it, m->targets.end()))); + expect(eq(it->otherMains, (std::vector{"src/entry/player_android.cpp"}))); + expect(fatal(eq(m->conditionalConfigs.size(), 1u))); auto const& row = m->conditionalConfigs[0].targetKinds.at("player"); - EXPECT_FALSE(row.statesForm); - EXPECT_EQ(row.main, std::optional("src/entry/player_android.cpp")); - EXPECT_EQ(row.start, std::optional("_reset")); + expect(!row.statesForm); + expect(eq(row.main, std::optional("src/entry/player_android.cpp"))); + expect(eq(row.start, std::optional("_reset"))); write(R"( [targets.p] @@ -7057,8 +6972,8 @@ kind = "lib" main = "x.cpp" )"); auto lib = mcpp::manifest::load(path); - ASSERT_FALSE(lib); - EXPECT_NE(lib.error().message.find("is a library target"), std::string::npos) << lib.error().message; + expect(fatal(!lib)); + if (auto&& failed = expect(ne(lib.error().message.find("is a library target"), std::string::npos)); !failed) failed << lib.error().message; write(R"( [targets.p] @@ -7068,16 +6983,15 @@ kind = "shared" main = "x.cpp" )"); auto both = mcpp::manifest::load(path); - ASSERT_FALSE(both); - EXPECT_NE(both.error().message.find("a target is one or the other"), std::string::npos) - << both.error().message; + expect(fatal(!both)); + if (auto&& failed = expect(ne(both.error().message.find("a target is one or the other"), std::string::npos)); !failed) failed << both.error().message; std::error_code ec; std::filesystem::remove_all(tmp, ec); -} +}}; // A package that declares only entries still has the library its sources // infer: an entry is no artifact of its own package. -TEST(Manifest, OnlyEntryTargetsStillInferTheLibrary) { +UTest only_entry_targets_still_infer_the_library{"Manifest/OnlyEntryTargetsStillInferTheLibrary", [] { auto tmp = std::filesystem::temp_directory_path() / "mcpp_entry_infer"; std::filesystem::create_directories(tmp / "src" / "entry"); { std::ofstream(tmp / "src" / "fw.cppm") << "export module fw;\n"; } @@ -7087,17 +7001,17 @@ TEST(Manifest, OnlyEntryTargetsStillInferTheLibrary) { "[targets.player]\nkind = \"entry\"\nmain = \"src/entry/player.cpp\"\n"; } auto m = mcpp::manifest::load(tmp / "mcpp.toml"); - ASSERT_TRUE(m) << (m ? "" : m.error().message); - EXPECT_TRUE(std::ranges::any_of(m->targets, [](auto const& t) { + if (auto&& failed = expect(fatal(bool(m))); !failed) failed << (m ? "" : m.error().message); + expect(std::ranges::any_of(m->targets, [](auto const& t) { return t.name == "fw" && t.kind == mcpp::manifest::Target::Library; })); std::error_code ec; std::filesystem::remove_all(tmp, ec); -} +}}; // ── [pack] targets and [build] target (SPEC-015 §3.3) ──────────────────────── -TEST(Manifest, PackTargetsTakeTargetsAndSets) { +UTest pack_targets_take_targets_and_sets{"Manifest/PackTargetsTakeTargetsAndSets", [] { auto list = mcpp::manifest::parse_string(R"( [package] name = "p" @@ -7105,8 +7019,8 @@ version = "0.1.0" [pack] targets = ["openkal", "x86_64-linux-musl"] )"); - ASSERT_TRUE(list.has_value()) << list.error().format(); - EXPECT_EQ(list->packConfig.targets, (std::vector{"openkal", "x86_64-linux-musl"})); + if (auto&& failed = expect(fatal(list.has_value())); !failed) failed << list.error().format(); + expect(eq(list->packConfig.targets, (std::vector{"openkal", "x86_64-linux-musl"}))); auto one = mcpp::manifest::parse_string(R"( [package] name = "p" @@ -7114,13 +7028,13 @@ version = "0.1.0" [pack] targets = "linux-musl" )"); - ASSERT_TRUE(one.has_value()) << one.error().format(); - EXPECT_EQ(one->packConfig.targets, (std::vector{"linux-musl"})); -} + if (auto&& failed = expect(fatal(one.has_value())); !failed) failed << one.error().format(); + expect(eq(one->packConfig.targets, (std::vector{"linux-musl"}))); +}}; // `[pack] format` and `[pack] systems` are a string or a list, and // `default_mode` takes the canonical names (design 2026-10-29 §4.1, §4.6). -TEST(Manifest, PackFormatAndSystemsAreAStringOrAList) { +UTest pack_format_and_systems_are_a_string_or_a_list{"Manifest/PackFormatAndSystemsAreAStringOrAList", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "p" @@ -7130,10 +7044,10 @@ default_mode = "self-contained" format = ["tar", "okb"] systems = "linux-aarch64" )"); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_EQ(m->packConfig.defaultMode, "self-contained"); - EXPECT_EQ(m->packConfig.format, (std::vector{"tar", "okb"})); - EXPECT_EQ(m->packConfig.systems, (std::vector{"linux-aarch64"})); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(eq(m->packConfig.defaultMode, "self-contained")); + expect(eq(m->packConfig.format, (std::vector{"tar", "okb"}))); + expect(eq(m->packConfig.systems, (std::vector{"linux-aarch64"}))); auto one = mcpp::manifest::parse_string(R"( [package] name = "p" @@ -7141,11 +7055,11 @@ version = "0.1.0" [pack] format = "zip" )"); - ASSERT_TRUE(one.has_value()) << one.error().format(); - EXPECT_EQ(one->packConfig.format, (std::vector{"zip"})); -} + if (auto&& failed = expect(fatal(one.has_value())); !failed) failed << one.error().format(); + expect(eq(one->packConfig.format, (std::vector{"zip"}))); +}}; -TEST(Manifest, PackTargetsRefuseWhatIsNeitherATargetNorASet) { +UTest pack_targets_refuse_what_is_neither_a_target_nor_a_set{"Manifest/PackTargetsRefuseWhatIsNeitherATargetNorASet", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "p" @@ -7153,11 +7067,11 @@ version = "0.1.0" [pack] targets = ["not a target"] )"); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().format().find("neither a target"), std::string::npos); -} + expect(fatal(!m.has_value())); + expect(ne(m.error().format().find("neither a target"), std::string::npos)); +}}; -TEST(Manifest, BuildTargetIsOneTarget) { +UTest build_target_is_one_target{"Manifest/BuildTargetIsOneTarget", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "p" @@ -7165,8 +7079,8 @@ version = "0.1.0" [build] target = "openkal" )"); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().format().find("names a target set"), std::string::npos); + expect(fatal(!m.has_value())); + expect(ne(m.error().format().find("names a target set"), std::string::npos)); auto ok = mcpp::manifest::parse_string(R"( [package] name = "p" @@ -7174,5 +7088,32 @@ version = "0.1.0" [build] target = "x86_64-openkal" )"); - ASSERT_TRUE(ok.has_value()) << ok.error().format(); -} + if (auto&& failed = expect(fatal(ok.has_value())); !failed) failed << ok.error().format(); +}}; + +// A GIT DEPENDENCY NAMES AT MOST ONE OF rev, tag, branch (SPEC-001 §5); none is +// the remote's default branch. +UTest a_git_dependency_names_at_most_one_ref{"Manifest/AGitDependencyNamesAtMostOneRef", [] { + auto m = mcpp::manifest::parse_string(R"( +[package] +name = "x" +version = "0.1.0" + +[dependencies] +bar = { git = "https://example.com/bar.git" } +)"); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + auto const& bar = m->dependencies.at("bar"); + expect(eq(bar.gitRefKind, "branch")); + expect(bar.gitRev.empty()); + auto two = mcpp::manifest::parse_string(R"( +[package] +name = "x" +version = "0.1.0" + +[dependencies] +bar = { git = "https://example.com/bar.git", tag = "v1", rev = "abc" } +)"); + expect(fatal(!two.has_value())); + expect(ne(two.error().format().find("a git dependency names at most one"), std::string::npos)); +}}; diff --git a/tests/unit/test_member_selection.cpp b/tests/unit/test_member_selection.cpp index 0cecc1ffb..d8e3708a3 100644 --- a/tests/unit/test_member_selection.cpp +++ b/tests/unit/test_member_selection.cpp @@ -1,9 +1,11 @@ -#include import std; +import mcpplibs.mtest; import mcpp.manifest; import mcpp.cli.selection; +using namespace mcpplibs::mtest; + // Member selection design 2026-09-30, S1 to S3. `select_members` is the one // function every command that acts on workspace members reads its `-p`, // `--workspace` and `--exclude` through, so the set a command plans does not @@ -53,7 +55,7 @@ struct Workspace { } mcpp::manifest::Manifest load() { auto m = mcpp::manifest::load(root / "mcpp.toml"); - EXPECT_TRUE(m.has_value()) << (m ? "" : m.error().format()); + if (auto&& failed = expect(m.has_value()); !failed) failed << (m ? "" : m.error().format()); return m ? std::move(*m) : mcpp::manifest::Manifest{}; } }; @@ -78,56 +80,56 @@ struct Three : Workspace { // S1: a virtual root without `-p` is the whole workspace, `--workspace` is // the whole workspace anywhere, and both are the same set. -TEST(MemberSelection, AVirtualRootWithoutDashPAndWorkspaceAreEveryMember) { +UTest a_virtual_root_without_dash_p_and_workspace_are_every_member{"MemberSelection/AVirtualRootWithoutDashPAndWorkspaceAreEveryMember", [] { Three f; auto ws = f.ws(); auto implicit = mcpp::cli::select_members(ws, f.root, "", request(false)); - ASSERT_TRUE(implicit.has_value()) << implicit.error(); - EXPECT_EQ(implicit->members, (Members{"a", "b", "c"})); - EXPECT_TRUE(implicit->whole); + if (auto&& failed = expect(fatal(implicit.has_value())); !failed) failed << implicit.error(); + expect(eq(implicit->members, (Members{"a", "b", "c"}))); + expect(bool(implicit->whole)); auto all = mcpp::cli::select_members(ws, f.root, "b", request(true)); - ASSERT_TRUE(all.has_value()) << all.error(); - EXPECT_EQ(all->members, (Members{"a", "b", "c"})); - EXPECT_TRUE(all->whole); -} + if (auto&& failed = expect(fatal(all.has_value())); !failed) failed << all.error(); + expect(eq(all->members, (Members{"a", "b", "c"}))); + expect(bool(all->whole)); +}}; // A rooted workspace's own package is "." and comes first. Without a selector // it is the only member; `--workspace` adds the others. -TEST(MemberSelection, ARootedRootSelectsItsOwnPackageFirst) { +UTest a_rooted_root_selects_its_own_package_first{"MemberSelection/ARootedRootSelectsItsOwnPackageFirst", [] { Workspace f("rooted"); f.member("a", "a"); f.member("b", "b"); auto ws = f.rooted_root("\"a\", \"b\""); auto bare = mcpp::cli::select_members(ws, f.root, "", request(false)); - ASSERT_TRUE(bare.has_value()) << bare.error(); - EXPECT_EQ(bare->members, (Members{"."})); - EXPECT_FALSE(bare->whole); + if (auto&& failed = expect(fatal(bare.has_value())); !failed) failed << bare.error(); + expect(eq(bare->members, (Members{"."}))); + expect(!bare->whole); auto all = mcpp::cli::select_members(ws, f.root, "", request(true)); - ASSERT_TRUE(all.has_value()) << all.error(); - EXPECT_EQ(all->members, (Members{".", "a", "b"})); + if (auto&& failed = expect(fatal(all.has_value())); !failed) failed << all.error(); + expect(eq(all->members, (Members{".", "a", "b"}))); auto named = mcpp::cli::select_members(ws, f.root, "", request(false, {"b", "rootpkg"})); - ASSERT_TRUE(named.has_value()) << named.error(); - EXPECT_EQ(named->members, (Members{".", "b"})); -} + if (auto&& failed = expect(fatal(named.has_value())); !failed) failed << named.error(); + expect(eq(named->members, (Members{".", "b"}))); +}}; // A command run inside a member's directory selects that member. -TEST(MemberSelection, InsideAMemberSelectsThatMember) { +UTest inside_a_member_selects_that_member{"MemberSelection/InsideAMemberSelectsThatMember", [] { Three f; auto ws = f.ws(); auto in = mcpp::cli::select_members(ws, f.root, "b", request(false)); - ASSERT_TRUE(in.has_value()) << in.error(); - EXPECT_EQ(in->members, (Members{"b"})); - EXPECT_FALSE(in->whole); -} + if (auto&& failed = expect(fatal(in.has_value())); !failed) failed << in.error(); + expect(eq(in->members, (Members{"b"}))); + expect(!in->whole); +}}; // S1: `-p` names members, each resolved in the order docs/07 §5.3 states. The // selection is a set kept in manifest order: the order `-p` was written in // does not change it, and a member named twice, by two spellings, is one. -TEST(MemberSelection, SeveralDashPSelectTheirMembersOnceInManifestOrder) { +UTest several_dash_p_select_their_members_once_in_manifest_order{"MemberSelection/SeveralDashPSelectTheirMembersOnceInManifestOrder", [] { Workspace f("set"); f.member("libs/a", "alpha"); f.member("libs/b", "beta"); @@ -136,23 +138,23 @@ TEST(MemberSelection, SeveralDashPSelectTheirMembersOnceInManifestOrder) { auto ab = mcpp::cli::select_members(ws, f.root, "", request(false, {"alpha", "beta"})); auto ba = mcpp::cli::select_members(ws, f.root, "", request(false, {"beta", "alpha"})); - ASSERT_TRUE(ab.has_value()) << ab.error(); - ASSERT_TRUE(ba.has_value()) << ba.error(); - EXPECT_EQ(ab->members, (Members{"libs/a", "libs/b"})); - EXPECT_EQ(ab->members, ba->members); - EXPECT_FALSE(ab->whole); + if (auto&& failed = expect(fatal(ab.has_value())); !failed) failed << ab.error(); + if (auto&& failed = expect(fatal(ba.has_value())); !failed) failed << ba.error(); + expect(eq(ab->members, (Members{"libs/a", "libs/b"}))); + expect(eq(ab->members, ba->members)); + expect(!ab->whole); // The package name, the member path and the directory's last segment are // three spellings of one member. auto same = mcpp::cli::select_members(ws, f.root, "", request(false, {"alpha", "libs/a", "a", "./libs/a"})); - ASSERT_TRUE(same.has_value()) << same.error(); - EXPECT_EQ(same->members, (Members{"libs/a"})); -} + if (auto&& failed = expect(fatal(same.has_value())); !failed) failed << same.error(); + expect(eq(same->members, (Members{"libs/a"}))); +}}; // A `-p` that names no member is refused, and the refusal names it and lists // the members; one that several members match is refused naming every match. -TEST(MemberSelection, AnUnknownOrAmbiguousDashPIsRefusedByName) { +UTest an_unknown_or_ambiguous_dash_p_is_refused_by_name{"MemberSelection/AnUnknownOrAmbiguousDashPIsRefusedByName", [] { Workspace f("refuse"); f.member("a", "same", "ns1"); f.member("b", "same", "ns2"); @@ -160,125 +162,124 @@ TEST(MemberSelection, AnUnknownOrAmbiguousDashPIsRefusedByName) { auto ws = f.virtual_root("\"a\", \"b\", \"c\""); auto unknown = mcpp::cli::select_members(ws, f.root, "", request(false, {"c", "nosuch"})); - ASSERT_FALSE(unknown.has_value()); - EXPECT_NE(unknown.error().find("nosuch"), std::string::npos) << unknown.error(); - EXPECT_NE(unknown.error().find("'c'"), std::string::npos) << unknown.error(); + expect(fatal(!unknown.has_value())); + if (auto&& failed = expect(ne(unknown.error().find("nosuch"), std::string::npos)); !failed) failed << unknown.error(); + if (auto&& failed = expect(ne(unknown.error().find("'c'"), std::string::npos)); !failed) failed << unknown.error(); auto ambiguous = mcpp::cli::select_members(ws, f.root, "", request(false, {"same"})); - ASSERT_FALSE(ambiguous.has_value()); - EXPECT_NE(ambiguous.error().find("ns1.same"), std::string::npos) << ambiguous.error(); - EXPECT_NE(ambiguous.error().find("ns2.same"), std::string::npos) << ambiguous.error(); + expect(fatal(!ambiguous.has_value())); + if (auto&& failed = expect(ne(ambiguous.error().find("ns1.same"), std::string::npos)); !failed) failed << ambiguous.error(); + if (auto&& failed = expect(ne(ambiguous.error().find("ns2.same"), std::string::npos)); !failed) failed << ambiguous.error(); auto qualified = mcpp::cli::select_members(ws, f.root, "", request(false, {"ns2.same", "ns1.same"})); - ASSERT_TRUE(qualified.has_value()) << qualified.error(); - EXPECT_EQ(qualified->members, (Members{"a", "b"})); -} + if (auto&& failed = expect(fatal(qualified.has_value())); !failed) failed << qualified.error(); + expect(eq(qualified->members, (Members{"a", "b"}))); +}}; // S3: `--exclude` removes members from either "all" form, by the same // resolution as `-p`, and keeps manifest order. -TEST(MemberSelection, ExcludeRemovesMembersFromAnAllForm) { +UTest exclude_removes_members_from_an_all_form{"MemberSelection/ExcludeRemovesMembersFromAnAllForm", [] { Three f; auto ws = f.ws(); auto flag = mcpp::cli::select_members(ws, f.root, "", request(true, {}, {"c"})); - ASSERT_TRUE(flag.has_value()) << flag.error(); - EXPECT_EQ(flag->members, (Members{"a", "b"})); - EXPECT_TRUE(flag->whole); + if (auto&& failed = expect(fatal(flag.has_value())); !failed) failed << flag.error(); + expect(eq(flag->members, (Members{"a", "b"}))); + expect(bool(flag->whole)); // The implicit whole selection of a virtual root, and two spellings. auto implicit = mcpp::cli::select_members(ws, f.root, "", request(false, {}, {"a", "./c"})); - ASSERT_TRUE(implicit.has_value()) << implicit.error(); - EXPECT_EQ(implicit->members, (Members{"b"})); + if (auto&& failed = expect(fatal(implicit.has_value())); !failed) failed << implicit.error(); + expect(eq(implicit->members, (Members{"b"}))); Workspace r("rooted-exclude"); r.member("a", "a"); auto rws = r.rooted_root("\"a\""); auto root = mcpp::cli::select_members(rws, r.root, "", request(true, {}, {"rootpkg"})); - ASSERT_TRUE(root.has_value()) << root.error(); - EXPECT_EQ(root->members, (Members{"a"})); -} + if (auto&& failed = expect(fatal(root.has_value())); !failed) failed << root.error(); + expect(eq(root->members, (Members{"a"}))); +}}; // S1, refusals: before anything is planned, `--exclude` with `-p`, `--exclude` // that names no member, `--exclude` that leaves nothing, and `--exclude` // where no "all" form applies. -TEST(MemberSelection, ExcludeIsRefusedWhereItHasNoMeaning) { +UTest exclude_is_refused_where_it_has_no_meaning{"MemberSelection/ExcludeIsRefusedWhereItHasNoMeaning", [] { Three f; auto ws = f.ws(); auto withP = mcpp::cli::select_members(ws, f.root, "", request(false, {"a"}, {"b"})); - ASSERT_FALSE(withP.has_value()); - EXPECT_NE(withP.error().find("--exclude"), std::string::npos) << withP.error(); - EXPECT_NE(withP.error().find("-p"), std::string::npos) << withP.error(); + expect(fatal(!withP.has_value())); + if (auto&& failed = expect(ne(withP.error().find("--exclude"), std::string::npos)); !failed) failed << withP.error(); + if (auto&& failed = expect(ne(withP.error().find("-p"), std::string::npos)); !failed) failed << withP.error(); auto withPAndAll = mcpp::cli::select_members(ws, f.root, "", request(true, {"a"}, {"b"})); - EXPECT_FALSE(withPAndAll.has_value()); + expect(!withPAndAll.has_value()); auto unknown = mcpp::cli::select_members(ws, f.root, "", request(true, {}, {"nosuch"})); - ASSERT_FALSE(unknown.has_value()); - EXPECT_NE(unknown.error().find("nosuch"), std::string::npos) << unknown.error(); + expect(fatal(!unknown.has_value())); + if (auto&& failed = expect(ne(unknown.error().find("nosuch"), std::string::npos)); !failed) failed << unknown.error(); auto everything = mcpp::cli::select_members(ws, f.root, "", request(true, {}, {"a", "b", "c"})); - ASSERT_FALSE(everything.has_value()); - EXPECT_NE(everything.error().find("every member"), std::string::npos) << everything.error(); + expect(fatal(!everything.has_value())); + if (auto&& failed = expect(ne(everything.error().find("every member"), std::string::npos)); !failed) failed << everything.error(); // Inside a member, and at a rooted root, without `--workspace`: nothing is // a whole-workspace selection to remove from. auto inside = mcpp::cli::select_members(ws, f.root, "a", request(false, {}, {"b"})); - ASSERT_FALSE(inside.has_value()); - EXPECT_NE(inside.error().find("--workspace"), std::string::npos) << inside.error(); + expect(fatal(!inside.has_value())); + if (auto&& failed = expect(ne(inside.error().find("--workspace"), std::string::npos)); !failed) failed << inside.error(); Workspace r("rooted-refuse"); r.member("a", "a"); auto rws = r.rooted_root("\"a\""); - EXPECT_FALSE(mcpp::cli::select_members(rws, r.root, "", request(false, {}, {"a"})).has_value()); -} + expect(!(mcpp::cli::select_members(rws, r.root, "", request(false, {}, {"a"})).has_value())); +}}; // `--workspace` and `-p` state two selections; neither is taken over the other. -TEST(MemberSelection, WorkspaceTogetherWithDashPIsRefused) { +UTest workspace_together_with_dash_p_is_refused{"MemberSelection/WorkspaceTogetherWithDashPIsRefused", [] { Three f; auto ws = f.ws(); auto both = mcpp::cli::select_members(ws, f.root, "", request(true, {"a"}, {})); - ASSERT_FALSE(both.has_value()); - EXPECT_NE(both.error().find("--workspace"), std::string::npos) << both.error(); - EXPECT_NE(both.error().find("-p"), std::string::npos) << both.error(); -} + expect(fatal(!both.has_value())); + if (auto&& failed = expect(ne(both.error().find("--workspace"), std::string::npos)); !failed) failed << both.error(); + if (auto&& failed = expect(ne(both.error().find("-p"), std::string::npos)); !failed) failed << both.error(); +}}; // The overload that takes a directory finds the workspace the directory // belongs to: from a member's directory, `-p` and `--workspace` still select // among the members of the workspace that lists it. Outside a workspace there // is no selection, and `--exclude` has nothing to name. -TEST(MemberSelection, TheDirectoryDecidesTheWorkspaceAndTheMemberItIsIn) { +UTest the_directory_decides_the_workspace_and_the_member_it_is_in{"MemberSelection/TheDirectoryDecidesTheWorkspaceAndTheMemberItIsIn", [] { Three f; f.ws(); auto at_root = mcpp::cli::select_members(request(false), f.root); - ASSERT_TRUE(at_root.has_value()) << at_root.error(); - ASSERT_TRUE(at_root->has_value()); - EXPECT_EQ((*at_root)->members, (Members{"a", "b", "c"})); + if (auto&& failed = expect(fatal(at_root.has_value())); !failed) failed << at_root.error(); + expect(fatal(at_root->has_value())); + expect(eq((*at_root)->members, (Members{"a", "b", "c"}))); auto in_member = mcpp::cli::select_members(request(false), f.root / "b"); - ASSERT_TRUE(in_member.has_value()) << in_member.error(); - ASSERT_TRUE(in_member->has_value()); - EXPECT_EQ((*in_member)->members, (Members{"b"})); - EXPECT_EQ(std::filesystem::weakly_canonical((*in_member)->root), - std::filesystem::weakly_canonical(f.root)); + if (auto&& failed = expect(fatal(in_member.has_value())); !failed) failed << in_member.error(); + expect(fatal(in_member->has_value())); + expect(eq((*in_member)->members, (Members{"b"}))); + expect(eq(std::filesystem::weakly_canonical((*in_member)->root), std::filesystem::weakly_canonical(f.root))); auto several_from_member = mcpp::cli::select_members(request(false, {"a", "c"}), f.root / "b"); - ASSERT_TRUE(several_from_member.has_value()) << several_from_member.error(); - ASSERT_TRUE(several_from_member->has_value()); - EXPECT_EQ((*several_from_member)->members, (Members{"a", "c"})); + if (auto&& failed = expect(fatal(several_from_member.has_value())); !failed) failed << several_from_member.error(); + expect(fatal(several_from_member->has_value())); + expect(eq((*several_from_member)->members, (Members{"a", "c"}))); auto all_from_member = mcpp::cli::select_members(request(true, {}, {"a"}), f.root / "b"); - ASSERT_TRUE(all_from_member.has_value()) << all_from_member.error(); - ASSERT_TRUE(all_from_member->has_value()); - EXPECT_EQ((*all_from_member)->members, (Members{"b", "c"})); + if (auto&& failed = expect(fatal(all_from_member.has_value())); !failed) failed << all_from_member.error(); + expect(fatal(all_from_member->has_value())); + expect(eq((*all_from_member)->members, (Members{"b", "c"}))); Workspace alone("alone"); alone.member(".", "solo"); auto none = mcpp::cli::select_members(request(false), alone.root); - ASSERT_TRUE(none.has_value()) << none.error(); - EXPECT_FALSE(none->has_value()); + if (auto&& failed = expect(fatal(none.has_value())); !failed) failed << none.error(); + expect(!none->has_value()); auto excluded = mcpp::cli::select_members(request(false, {}, {"x"}), alone.root); - ASSERT_FALSE(excluded.has_value()); - EXPECT_NE(excluded.error().find("not in one"), std::string::npos) << excluded.error(); -} + expect(fatal(!excluded.has_value())); + if (auto&& failed = expect(ne(excluded.error().find("not in one"), std::string::npos)); !failed) failed << excluded.error(); +}}; diff --git a/tests/unit/test_modgraph.cpp b/tests/unit/test_modgraph.cpp index 5469e99bb..f66008feb 100644 --- a/tests/unit/test_modgraph.cpp +++ b/tests/unit/test_modgraph.cpp @@ -1,6 +1,6 @@ -#include import std; +import mcpplibs.mtest; import mcpp.modgraph.glob; import mcpp.modgraph.graph; import mcpp.modgraph.scanner; @@ -8,6 +8,8 @@ import mcpp.modgraph.validate; import mcpp.manifest; import mcpp.source_kind; +using namespace mcpplibs::mtest; + using namespace mcpp::modgraph; namespace { @@ -27,7 +29,7 @@ void write(const std::filesystem::path& p, std::string_view content) { } // namespace -TEST(Scanner, ProvidesAndRequires) { +UTest provides_and_requires{"Scanner/ProvidesAndRequires", [] { auto dir = make_tempdir("mcpp-scanner"); // NOTE: avoid raw string literal for module source — clang-scan-deps // on Windows may false-positive on `import bar;` inside R"(...)". @@ -38,15 +40,15 @@ TEST(Scanner, ProvidesAndRequires) { "export int answer();\n"); auto u = scan_file(dir / "src" / "foo.cppm", "pkg", mcpp::builtin_extension_table()); - ASSERT_TRUE(u.has_value()) << u.error().format(); - ASSERT_TRUE(u->provides.has_value()); - EXPECT_EQ(u->provides->logicalName, "foo"); - ASSERT_EQ(u->requires_.size(), 2u); - EXPECT_EQ(u->requires_[0].logicalName, "std"); - EXPECT_EQ(u->requires_[1].logicalName, "bar"); + if (auto&& failed = expect(fatal(u.has_value())); !failed) failed << u.error().format(); + expect(fatal(u->provides.has_value())); + expect(eq(u->provides->logicalName, "foo")); + expect(fatal(eq(u->requires_.size(), 2u))); + expect(eq(u->requires_[0].logicalName, "std")); + expect(eq(u->requires_[1].logicalName, "bar")); std::filesystem::remove_all(dir); -} +}}; // Regression: `import` lines that live INSIDE a multi-line raw-string literal // (e.g. a `mcpp new --template gui` skeleton embedded as R"GUI( ... )GUI") must @@ -62,7 +64,7 @@ TEST(Scanner, ProvidesAndRequires) { // also warned "imported but not provided in this build" on every platform, // which read like a note instead of the cause. -TEST(Scanner, ImplementationPartitionProvidesItsPartitionName) { +UTest implementation_partition_provides_its_partition_name{"Scanner/ImplementationPartitionProvidesItsPartitionName", [] { auto dir = make_tempdir("scan-implpart"); std::filesystem::create_directories(dir / "src"); write(dir / "src" / "secret.cppm", @@ -71,39 +73,39 @@ TEST(Scanner, ImplementationPartitionProvidesItsPartitionName) { auto u = scan_file(dir / "src" / "secret.cppm", "pkg", mcpp::builtin_extension_table()); - ASSERT_TRUE(u.has_value()); - ASSERT_TRUE(u->provides.has_value()); - EXPECT_EQ(u->provides->logicalName, "mathkit:secret"); + expect(fatal(u.has_value())); + expect(fatal(u->provides.has_value())); + expect(eq(u->provides->logicalName, "mathkit:secret")); // Not an interface: its source must not be published by `mcpp pack`. // Compared against the value, not tested for truthiness — the field is a // tri-state now (nullopt means "nobody determined this"), and on an // optional both EXPECT_FALSE and EXPECT_TRUE ask about the wrong thing. - EXPECT_EQ(u->providesInterface, std::optional{false}); + expect(eq(u->providesInterface, std::optional{false})); // And it must not require its own name, which is what it used to do. for (auto const& r : u->requires_) - EXPECT_NE(r.logicalName, "mathkit:secret"); + expect(ne(r.logicalName, "mathkit:secret")); std::filesystem::remove_all(dir); -} +}}; -TEST(Scanner, InterfacePartitionIsMarkedAsAnInterface) { +UTest interface_partition_is_marked_as_an_interface{"Scanner/InterfacePartitionIsMarkedAsAnInterface", [] { auto dir = make_tempdir("scan-ifacepart"); std::filesystem::create_directories(dir / "src"); write(dir / "src" / "api.cppm", "export module mathkit:api;\nexport int f();\n"); auto u = scan_file(dir / "src" / "api.cppm", "pkg", mcpp::builtin_extension_table()); - ASSERT_TRUE(u.has_value()); - ASSERT_TRUE(u->provides.has_value()); - EXPECT_EQ(u->provides->logicalName, "mathkit:api"); - EXPECT_EQ(u->providesInterface, std::optional{true}); + expect(fatal(u.has_value())); + expect(fatal(u->provides.has_value())); + expect(eq(u->provides->logicalName, "mathkit:api")); + expect(eq(u->providesInterface, std::optional{true})); std::filesystem::remove_all(dir); -} +}}; // ─── the declaration form (build database roles) ───────────────────────── // // `module M;` is stored as `requires M`, exactly as `import M;` is, so the form // cannot be recovered from provides/requires. The scanner records what it read. -TEST(Scanner, DeclarationFormIsRecordedAsRead) { +UTest declaration_form_is_recorded_as_read{"Scanner/DeclarationFormIsRecordedAsRead", [] { auto dir = make_tempdir("scan-declform"); std::filesystem::create_directories(dir / "src"); struct Case { const char* file; const char* text; ModuleDeclaration want; }; @@ -122,11 +124,11 @@ TEST(Scanner, DeclarationFormIsRecordedAsRead) { for (auto const& c : cases) { write(dir / "src" / c.file, c.text); auto u = scan_file(dir / "src" / c.file, "pkg", mcpp::builtin_extension_table()); - ASSERT_TRUE(u.has_value()) << c.file; - EXPECT_EQ(static_cast(u->declaration), static_cast(c.want)) << c.file; + expect(fatal(u.has_value())) << c.file; + expect(eq(static_cast(u->declaration), static_cast(c.want))) << c.file; } std::filesystem::remove_all(dir); -} +}}; // ─── an entry source no `sources` glob matched ───────────────────────────── // @@ -134,7 +136,7 @@ TEST(Scanner, DeclarationFormIsRecordedAsRead) { // Its imports were read from line-leading `import` alone, so an import inside a // comment or a raw string was recorded as one: a scanner test in lsp-mcpp was // planned as importing three modules no source provides. -TEST(Scanner, AnEntrySourceIsReadByTheScanner) { +UTest an_entry_source_is_read_by_the_scanner{"Scanner/AnEntrySourceIsReadByTheScanner", [] { auto dir = make_tempdir("scan-entry"); write(dir / "t.cpp", "import std;\n" @@ -148,14 +150,14 @@ TEST(Scanner, AnEntrySourceIsReadByTheScanner) { auto u = scan_entry_file(dir / "t.cpp", "pkg", mcpp::builtin_extension_table()); std::vector names; for (auto const& r : u.requires_) names.push_back(r.logicalName); - EXPECT_EQ(names, (std::vector{"std", "real"})); - EXPECT_EQ(static_cast(u.declaration), static_cast(ModuleDeclaration::None)); + expect(eq(names, (std::vector{"std", "real"}))); + expect(eq(static_cast(u.declaration), static_cast(ModuleDeclaration::None))); std::filesystem::remove_all(dir); -} +}}; // The scanner's refusals were never applied to such a file, and are not now: a // file it refuses keeps its line-leading imports, and its form is not decided. -TEST(Scanner, AnEntrySourceTheScannerRefusesKeepsItsImports) { +UTest an_entry_source_the_scanner_refuses_keeps_its_imports{"Scanner/AnEntrySourceTheScannerRefusesKeepsItsImports", [] { auto dir = make_tempdir("scan-entry-refused"); write(dir / "t.cpp", "#ifdef WITH_EXTRA\n" @@ -163,30 +165,30 @@ TEST(Scanner, AnEntrySourceTheScannerRefusesKeepsItsImports) { "#endif\n" "export import std;\n" "int main() {}\n"); - ASSERT_FALSE(scan_file(dir / "t.cpp", "pkg", mcpp::builtin_extension_table()).has_value()); + expect(fatal(!(scan_file(dir / "t.cpp", "pkg", mcpp::builtin_extension_table()).has_value()))); auto u = scan_entry_file(dir / "t.cpp", "pkg", mcpp::builtin_extension_table()); std::vector names; for (auto const& r : u.requires_) names.push_back(r.logicalName); - EXPECT_EQ(names, (std::vector{"extra", "std"})); - EXPECT_EQ(static_cast(u.declaration), static_cast(ModuleDeclaration::Unknown)); + expect(eq(names, (std::vector{"extra", "std"}))); + expect(eq(static_cast(u.declaration), static_cast(ModuleDeclaration::Unknown))); std::filesystem::remove_all(dir); -} +}}; -TEST(Scanner, PlainImplementationUnitStillRequiresItsInterface) { +UTest plain_implementation_unit_still_requires_its_interface{"Scanner/PlainImplementationUnitStillRequiresItsInterface", [] { auto dir = make_tempdir("scan-implunit"); std::filesystem::create_directories(dir / "src"); write(dir / "src" / "impl.cpp", "module mathkit;\nnamespace mk { int g() { return 2; } }\n"); auto u = scan_file(dir / "src" / "impl.cpp", "pkg", mcpp::builtin_extension_table()); - ASSERT_TRUE(u.has_value()); - EXPECT_FALSE(u->provides.has_value()); - ASSERT_EQ(u->requires_.size(), 1u); - EXPECT_EQ(u->requires_[0].logicalName, "mathkit"); + expect(fatal(u.has_value())); + expect(!(u->provides.has_value())); + expect(fatal(eq(u->requires_.size(), 1u))); + expect(eq(u->requires_[0].logicalName, "mathkit")); std::filesystem::remove_all(dir); -} +}}; -TEST(Scanner, PartitionImportResolvesInsideAnImplementationUnit) { +UTest partition_import_resolves_inside_an_implementation_unit{"Scanner/PartitionImportResolvesInsideAnImplementationUnit", [] { // The other half of the same bug: resolution keyed off `u.provides`, and an // implementation unit has none — so `import :secret;` stayed the literal // `:secret`, which nothing provides. @@ -197,17 +199,17 @@ TEST(Scanner, PartitionImportResolvesInsideAnImplementationUnit) { auto u = scan_file(dir / "src" / "impl.cpp", "pkg", mcpp::builtin_extension_table()); - ASSERT_TRUE(u.has_value()); + expect(fatal(u.has_value())); bool found = false; for (auto const& r : u->requires_) { - EXPECT_NE(r.logicalName, ":secret"); + expect(ne(r.logicalName, ":secret")); if (r.logicalName == "mathkit:secret") found = true; } - EXPECT_TRUE(found) << "`import :secret;` did not resolve to mathkit:secret"; + expect(bool(found)) << "`import :secret;` did not resolve to mathkit:secret"; std::filesystem::remove_all(dir); -} +}}; -TEST(Scanner, PartitionImportResolvesInsideAPartition) { +UTest partition_import_resolves_inside_a_partition{"Scanner/PartitionImportResolvesInsideAPartition", [] { // `export module foo:http;` + `import :tls;` must give `foo:tls`, not // `foo:http:tls` — the case the old code was written for, still true. auto dir = make_tempdir("scan-partpart"); @@ -216,13 +218,13 @@ TEST(Scanner, PartitionImportResolvesInsideAPartition) { auto u = scan_file(dir / "src" / "http.cppm", "pkg", mcpp::builtin_extension_table()); - ASSERT_TRUE(u.has_value()); - ASSERT_EQ(u->requires_.size(), 1u); - EXPECT_EQ(u->requires_[0].logicalName, "foo:tls"); + expect(fatal(u.has_value())); + expect(fatal(eq(u->requires_.size(), 1u))); + expect(eq(u->requires_[0].logicalName, "foo:tls")); std::filesystem::remove_all(dir); -} +}}; -TEST(Scanner, IgnoresImportsInsideRawStringLiteral) { +UTest ignores_imports_inside_raw_string_literal{"Scanner/IgnoresImportsInsideRawStringLiteral", [] { auto dir = make_tempdir("mcpp-scanner-raw"); write(dir / "src" / "gen.cppm", "export module gen;\n" @@ -236,23 +238,23 @@ TEST(Scanner, IgnoresImportsInsideRawStringLiteral) { "export void f();\n"); auto u = scan_file(dir / "src" / "gen.cppm", "pkg", mcpp::builtin_extension_table()); - ASSERT_TRUE(u.has_value()) << u.error().format(); - ASSERT_TRUE(u->provides.has_value()); - EXPECT_EQ(u->provides->logicalName, "gen"); + if (auto&& failed = expect(fatal(u.has_value())); !failed) failed << u.error().format(); + expect(fatal(u->provides.has_value())); + expect(eq(u->provides->logicalName, "gen")); // Only the two genuine top-level imports — NOT imgui.core / imgui.app. - ASSERT_EQ(u->requires_.size(), 2u); - EXPECT_EQ(u->requires_[0].logicalName, "std"); - EXPECT_EQ(u->requires_[1].logicalName, "bar"); + expect(fatal(eq(u->requires_.size(), 2u))); + expect(eq(u->requires_[0].logicalName, "std")); + expect(eq(u->requires_[1].logicalName, "bar")); for (auto& r : u->requires_) { - EXPECT_NE(r.logicalName, "imgui.core"); - EXPECT_NE(r.logicalName, "imgui.app"); + expect(ne(r.logicalName, "imgui.core")); + expect(ne(r.logicalName, "imgui.app")); } std::filesystem::remove_all(dir); -} +}}; // A single-line raw string with an embedded import-looking body stays code. -TEST(Scanner, IgnoresImportInsideSingleLineRawString) { +UTest ignores_import_inside_single_line_raw_string{"Scanner/IgnoresImportInsideSingleLineRawString", [] { auto dir = make_tempdir("mcpp-scanner-raw1"); write(dir / "src" / "one.cppm", "export module one;\n" @@ -260,17 +262,17 @@ TEST(Scanner, IgnoresImportInsideSingleLineRawString) { "import real;\n"); auto u = scan_file(dir / "src" / "one.cppm", "pkg", mcpp::builtin_extension_table()); - ASSERT_TRUE(u.has_value()) << u.error().format(); - ASSERT_EQ(u->requires_.size(), 1u); - EXPECT_EQ(u->requires_[0].logicalName, "real"); + if (auto&& failed = expect(fatal(u.has_value())); !failed) failed << u.error().format(); + expect(fatal(eq(u->requires_.size(), 1u))); + expect(eq(u->requires_[0].logicalName, "real")); std::filesystem::remove_all(dir); -} +}}; // Assembly units skip the module text scan entirely (like .c): an .asm/.S // file legally contains no `import`/`module` declarations, and asm comment // syntax would misparse. -TEST(Scanner, AssemblySourcesSkipModuleScan) { +UTest assembly_sources_skip_module_scan{"Scanner/AssemblySourcesSkipModuleScan", [] { auto dir = make_tempdir("mcpp-scanner-asm"); write(dir / "src" / "simd.asm", "; import std -- a comment, not a declaration\n" @@ -283,34 +285,34 @@ TEST(Scanner, AssemblySourcesSkipModuleScan) { for (auto name : { "simd.asm", "copy.S" }) { auto u = scan_file(dir / "src" / name, "pkg", mcpp::builtin_extension_table()); - ASSERT_TRUE(u.has_value()) << u.error().format(); - EXPECT_FALSE(u->provides.has_value()) << name; - EXPECT_TRUE(u->requires_.empty()) << name; + if (auto&& failed = expect(fatal(u.has_value())); !failed) failed << u.error().format(); + expect(!(u->provides.has_value())) << name; + expect(u->requires_.empty()) << name; } std::filesystem::remove_all(dir); -} +}}; // G8a: source globs follow directory symlinks (vendored trees are often // symlink farms) without looping on cycles. -TEST(Scanner, ExpandGlobFollowsDirectorySymlinks) { +UTest expand_glob_follows_directory_symlinks{"Scanner/ExpandGlobFollowsDirectorySymlinks", [] { auto dir = make_tempdir("mcpp-scanner-symlink"); auto real = dir / "vendor-real"; write(real / "impl.cpp", "int f() { return 1; }\n"); std::error_code ec; std::filesystem::create_directories(dir / "src"); std::filesystem::create_directory_symlink(real, dir / "src" / "vendor", ec); - if (ec) GTEST_SKIP() << "symlinks unsupported here: " << ec.message(); + if (ec) skip([&] { std::ostringstream text; text << "symlinks unsupported here: " << ec.message(); return text.str(); }()); // A cycle: the linked tree points back at its parent. std::filesystem::create_directory_symlink(dir, real / "loop", ec); auto files = expand_glob(dir, "src/**/*.cpp"); - ASSERT_EQ(files.size(), 1u); - EXPECT_EQ(files[0].filename(), "impl.cpp"); + expect(fatal(eq(files.size(), 1u))); + expect(eq(files[0].filename(), "impl.cpp")); std::filesystem::remove_all(dir); -} +}}; // mcpp#225 review finding: glob_literal_prefix's contract (the piece that // actually decides WHERE expand_glob/expand_dir_glob start walking) is the @@ -318,20 +320,20 @@ TEST(Scanner, ExpandGlobFollowsDirectorySymlinks) { // directly — rather than inferring its behavior from a filesystem walk whose // entry order is unspecified by the standard — makes this test's pass/fail // independent of directory-enumeration order on any filesystem. -TEST(Scanner, GlobLiteralPrefixDerivation) { - EXPECT_EQ(glob_literal_prefix("src/**/*.cppm"), "src"); +UTest glob_literal_prefix_derivation{"Scanner/GlobLiteralPrefixDerivation", [] { + expect(eq(glob_literal_prefix("src/**/*.cppm"), "src")); // Wildcard already in the first segment: no literal directory to bound to. - EXPECT_EQ(glob_literal_prefix("**/*.c"), ""); + expect(eq(glob_literal_prefix("**/*.c"), "")); // No wildcard at all: the full parent directory path is the prefix. - EXPECT_EQ(glob_literal_prefix("a/b/c.cpp").generic_string(), "a/b"); + expect(eq(glob_literal_prefix("a/b/c.cpp").generic_string(), "a/b")); // Truncate back to the last COMPLETE '/' before the first wildcard char — // "x*.cpp" is a partial segment, not a real directory named "x". - EXPECT_EQ(glob_literal_prefix("src/x*.cpp"), "src"); + expect(eq(glob_literal_prefix("src/x*.cpp"), "src")); // '{' is treated as a segment-boundary wildcard char (brace-expansion // globs are desugared before reaching this helper — see its comment in // scanner.cppm), so the prefix truncates at the last '/' before it. - EXPECT_EQ(glob_literal_prefix("a/{x,y}/z"), "a"); -} + expect(eq(glob_literal_prefix("a/{x,y}/z"), "a")); +}}; // MSVC's std::filesystem::path preserves the separators of the string it was // constructed from, so a raw `a/b` prefix stays generic and `root / p` turns @@ -340,64 +342,60 @@ TEST(Scanner, GlobLiteralPrefixDerivation) { // glob_literal_prefix must return NATIVE separators so the walk and everything // downstream is native too. (The generic spelling is already locked down by // Scanner.GlobLiteralPrefixDerivation above.) -TEST(Scanner, GlobLiteralPrefixUsesNativeSeparators) { +UTest glob_literal_prefix_uses_native_separators{"Scanner/GlobLiteralPrefixUsesNativeSeparators", [] { if constexpr (std::filesystem::path::preferred_separator == '\\') { - EXPECT_EQ(glob_literal_prefix("a/b/c.cpp").string(), "a\\b"); - EXPECT_EQ(glob_literal_prefix("a/b/c.cpp").string().find('/'), - std::string::npos); + expect(eq(glob_literal_prefix("a/b/c.cpp").string(), "a\\b")); + expect(eq(glob_literal_prefix("a/b/c.cpp").string().find('/'), std::string::npos)); } -} +}}; // The exported converter itself — both spelling directions. -TEST(Glob, NativePathFromGeneric) { +UTest glob_native_path_from_generic{"Glob/NativePathFromGeneric", [] { auto p = mcpp::modgraph::native_path_from_generic("a/b/c"); - EXPECT_EQ(p.generic_string(), "a/b/c"); + expect(eq(p.generic_string(), "a/b/c")); if constexpr (std::filesystem::path::preferred_separator == '\\') { - EXPECT_EQ(p.string(), "a\\b\\c"); + expect(eq(p.string(), "a\\b\\c")); // Already-native input is untouched. - EXPECT_EQ(mcpp::modgraph::native_path_from_generic("C:\\x\\y").string(), - "C:\\x\\y"); + expect(eq(mcpp::modgraph::native_path_from_generic("C:\\x\\y").string(), "C:\\x\\y")); } -} +}}; // The end-to-end shape of the reported bug: a source under a multi-segment // glob (`generated/modules/**/*.cppm`) must come out of expand_glob with // NATIVE separators on Windows — the mixed `root\generated/modules\a.cppm` // was what compile_commands.json's `file` field showed before the fix. -TEST(Scanner, ExpandGlobMultiSegmentPrefixUsesNativeSeparators) { +UTest expand_glob_multi_segment_prefix_uses_native_separators{"Scanner/ExpandGlobMultiSegmentPrefixUsesNativeSeparators", [] { auto dir = make_tempdir("mcpp-scanner-multi"); write(dir / "generated" / "modules" / "a.cppm", "export module a;\n"); auto files = expand_glob(dir, "generated/modules/**/*.cppm"); - ASSERT_EQ(files.size(), 1u); + expect(fatal(eq(files.size(), 1u))); if constexpr (std::filesystem::path::preferred_separator == '\\') { - EXPECT_EQ(files[0].string().find('/'), std::string::npos) << files[0]; + if (auto&& failed = expect(eq(files[0].string().find('/'), std::string::npos)); !failed) failed << files[0]; } - EXPECT_EQ(files[0].generic_string(), - (dir / "generated" / "modules" / "a.cppm").generic_string()); + expect(eq(files[0].generic_string(), (dir / "generated" / "modules" / "a.cppm").generic_string())); std::filesystem::remove_all(dir); -} +}}; // Same contract for the INCLUDE-DIR channel (expand_dir_glob): a multi-segment // `third_party/inc` entry must yield a native path or the CDB's -I carries the // mixed form. -TEST(Scanner, ExpandDirGlobMultiSegmentUsesNativeSeparators) { +UTest expand_dir_glob_multi_segment_uses_native_separators{"Scanner/ExpandDirGlobMultiSegmentUsesNativeSeparators", [] { auto dir = make_tempdir("mcpp-scanner-dirglob"); std::filesystem::create_directories(dir / "third_party" / "inc"); auto dirs = expand_dir_glob(dir, "third_party/inc"); - ASSERT_EQ(dirs.size(), 1u); + expect(fatal(eq(dirs.size(), 1u))); if constexpr (std::filesystem::path::preferred_separator == '\\') { - EXPECT_EQ(dirs[0].string().find('/'), std::string::npos) << dirs[0]; + if (auto&& failed = expect(eq(dirs[0].string().find('/'), std::string::npos)); !failed) failed << dirs[0]; } - EXPECT_EQ(dirs[0].generic_string(), - (dir / "third_party" / "inc").generic_string()); + expect(eq(dirs[0].generic_string(), (dir / "third_party" / "inc").generic_string())); std::filesystem::remove_all(dir); -} +}}; // mcpp#225: expand_glob must bound its walk to the glob's literal directory // prefix ("src" for "src/**/*.cppm") instead of always walking the whole @@ -408,18 +406,18 @@ TEST(Scanner, ExpandDirGlobMultiSegmentUsesNativeSeparators) { // deterministically by Scanner.GlobLiteralPrefixDerivation above, which // tests glob_literal_prefix directly and isn't subject to filesystem // enumeration order. -TEST(Scanner, ExpandGlobStartsAtLiteralPrefix) { +UTest expand_glob_starts_at_literal_prefix{"Scanner/ExpandGlobStartsAtLiteralPrefix", [] { auto dir = make_tempdir("mcpp-scanner-prefix"); write(dir / "other" / "b.cppm", "export module b;\n"); write(dir / "src" / "a.cppm", "export module a;\n"); auto files = expand_glob(dir, "src/**/*.cppm"); - ASSERT_EQ(files.size(), 1u); - EXPECT_EQ(files[0], dir / "src" / "a.cppm"); + expect(fatal(eq(files.size(), 1u))); + expect(eq(files[0], dir / "src" / "a.cppm")); std::filesystem::remove_all(dir); -} +}}; // Supplementary (not the primary regression guard — see // Scanner.GlobLiteralPrefixDerivation and Scanner.ExpandGlobStartsAtLiteralPrefix @@ -431,7 +429,7 @@ TEST(Scanner, ExpandGlobStartsAtLiteralPrefix) { // landmine didn't fire (order-dependent), the test still asserts the // positive result and reports (via trace) that the landmine was inert this // run, instead of pretending it verified the old-code-fails claim. -TEST(Scanner, ExpandGlobStartsAtLiteralPrefixLandmine) { +UTest expand_glob_starts_at_literal_prefix_landmine{"Scanner/ExpandGlobStartsAtLiteralPrefixLandmine", [] { auto dir = make_tempdir("mcpp-scanner-prefix-landmine"); auto blocked = dir / "junk" / "blocked"; std::filesystem::create_directories(blocked); @@ -448,7 +446,7 @@ TEST(Scanner, ExpandGlobStartsAtLiteralPrefixLandmine) { if (!probeEc) { std::filesystem::permissions(blocked, std::filesystem::perms::all, permEc); std::filesystem::remove_all(dir); - GTEST_SKIP() << "cannot restrict directory permissions in this environment"; + skip("cannot restrict directory permissions in this environment"); } auto files = expand_glob(dir, "src/**/*.cppm"); @@ -465,17 +463,17 @@ TEST(Scanner, ExpandGlobStartsAtLiteralPrefixLandmine) { // just isn't, by itself, an order-independent proof that old code would // have failed here (that proof now lives in // Scanner.GlobLiteralPrefixDerivation instead). - ASSERT_EQ(files.size(), 1u); - EXPECT_EQ(files[0], dir / "src" / "a.cppm"); + expect(fatal(eq(files.size(), 1u))); + expect(eq(files[0], dir / "src" / "a.cppm")); std::filesystem::remove_all(dir); -} +}}; // mcpp#225: the walk must prune VCS metadata (.git) and mcpp's own build // output (target) the same way it already prunes .mcpp (mcpp#230). Glob has // no literal prefix ("**/*.cppm") so the walk starts at root and would // otherwise reach all three excluded trees. -TEST(Scanner, ExcludesGitAndTargetAndMcpp) { +UTest excludes_git_and_target_and_mcpp{"Scanner/ExcludesGitAndTargetAndMcpp", [] { auto dir = make_tempdir("mcpp-scanner-exclude"); write(dir / ".git" / "x.cppm", "export module x;\n"); write(dir / "target" / "y.cppm", "export module y;\n"); @@ -484,13 +482,13 @@ TEST(Scanner, ExcludesGitAndTargetAndMcpp) { auto files = expand_glob(dir, "**/*.cppm"); - ASSERT_EQ(files.size(), 1u); - EXPECT_EQ(files[0], dir / "src" / "ok.cppm"); + expect(fatal(eq(files.size(), 1u))); + expect(eq(files[0], dir / "src" / "ok.cppm")); std::filesystem::remove_all(dir); -} +}}; -TEST(Scanner, RecordsPackageLocalIncludeDirs) { +UTest records_package_local_include_dirs{"Scanner/RecordsPackageLocalIncludeDirs", [] { auto dir = make_tempdir("mcpp-scanner-includes"); write(dir / "src" / "foo.cpp", "int answer() { return 42; }\n"); @@ -503,18 +501,18 @@ TEST(Scanner, RecordsPackageLocalIncludeDirs) { m.buildConfig.includeDirs = {"include", "private/*"}; auto r = scan_packages({PackageRoot{dir, m}}); - ASSERT_TRUE(r.errors.empty()); - ASSERT_EQ(r.graph.units.size(), 1u); + expect(fatal(r.errors.empty())); + expect(fatal(eq(r.graph.units.size(), 1u))); auto const& dirs = r.graph.units[0].localIncludeDirs; - ASSERT_EQ(dirs.size(), 2u); - EXPECT_EQ(dirs[0], dir / "include"); - EXPECT_EQ(dirs[1], dir / "private" / "nested"); + expect(fatal(eq(dirs.size(), 2u))); + expect(eq(dirs[0], dir / "include")); + expect(eq(dirs[1], dir / "private" / "nested")); std::filesystem::remove_all(dir); -} +}}; -TEST(Scanner, UsesResolvedPackagePrivateBuildIncludeDirs) { +UTest uses_resolved_package_private_build_include_dirs{"Scanner/UsesResolvedPackagePrivateBuildIncludeDirs", [] { auto dir = make_tempdir("mcpp-scanner-resolved-includes"); write(dir / "src" / "foo.cpp", "int answer() { return 42; }\n"); @@ -531,15 +529,15 @@ TEST(Scanner, UsesResolvedPackagePrivateBuildIncludeDirs) { p.privateBuild.includeDirs = {dir / "resolved"}; auto r = scan_packages({p}); - ASSERT_TRUE(r.errors.empty()); - ASSERT_EQ(r.graph.units.size(), 1u); + expect(fatal(r.errors.empty())); + expect(fatal(eq(r.graph.units.size(), 1u))); auto const& dirs = r.graph.units[0].localIncludeDirs; - ASSERT_EQ(dirs.size(), 1u); - EXPECT_EQ(dirs[0], dir / "resolved"); + expect(fatal(eq(dirs.size(), 1u))); + expect(eq(dirs[0], dir / "resolved")); std::filesystem::remove_all(dir); -} +}}; // The realised [c-abi] environment (design 2026-09-18) broadcasts into // `privateBuild.asmflags`, and this proves the wiring on the ONE point that @@ -551,7 +549,7 @@ TEST(Scanner, UsesResolvedPackagePrivateBuildIncludeDirs) { // is not a degraded build, it is a build that does not assemble at all, so // this is a refusal-shaped guarantee even though nothing here calls // `refusal::record`: the wrong list for the wrong assembler. -TEST(Scanner, ResolvedCEnvAsmflagsReachGasUnitsOnlyNotNasm) { +UTest resolved_c_env_asmflags_reach_gas_units_only_not_nasm{"Scanner/ResolvedCEnvAsmflagsReachGasUnitsOnlyNotNasm", [] { auto dir = make_tempdir("mcpp-scanner-cenv-asm"); write(dir / "src" / "gas.S", ".text\n.globl gas_marker\ngas_marker:\n ret\n"); @@ -567,33 +565,31 @@ TEST(Scanner, ResolvedCEnvAsmflagsReachGasUnitsOnlyNotNasm) { p.privateBuild.asmflags = {"--target=x86_64-pc-cygwin", "-fno-short-wchar"}; auto r = scan_packages({p}); - ASSERT_TRUE(r.errors.empty()); - ASSERT_EQ(r.graph.units.size(), 2u); + expect(fatal(r.errors.empty())); + expect(fatal(eq(r.graph.units.size(), 2u))); bool sawGas = false, sawNasm = false; for (auto const& u : r.graph.units) { if (u.kind == mcpp::SourceKind::GasAsm) { sawGas = true; - EXPECT_NE(std::find(u.packageAsmflags.begin(), u.packageAsmflags.end(), - "--target=x86_64-pc-cygwin"), - u.packageAsmflags.end()) + expect(ne(std::find(u.packageAsmflags.begin(), u.packageAsmflags.end(), + "--target=x86_64-pc-cygwin"), u.packageAsmflags.end())) << "the realised environment must reach a GAS unit"; } else if (u.kind == mcpp::SourceKind::NasmAsm) { sawNasm = true; - EXPECT_EQ(std::find(u.packageAsmflags.begin(), u.packageAsmflags.end(), - "--target=x86_64-pc-cygwin"), - u.packageAsmflags.end()) + expect(eq(std::find(u.packageAsmflags.begin(), u.packageAsmflags.end(), + "--target=x86_64-pc-cygwin"), u.packageAsmflags.end())) << "NASM does not understand a clang --target= flag and must " "never receive it"; } } - EXPECT_TRUE(sawGas); - EXPECT_TRUE(sawNasm); + expect(bool(sawGas)); + expect(bool(sawNasm)); std::filesystem::remove_all(dir); -} +}}; -TEST(Scanner, PartitionImportFromPrimaryInterface) { +UTest partition_import_from_primary_interface{"Scanner/PartitionImportFromPrimaryInterface", [] { // Primary module interface: `export module foo;` → logicalName = "foo". // `import :tls;` resolves to "foo:tls". auto dir = make_tempdir("mcpp-scanner"); @@ -601,13 +597,13 @@ TEST(Scanner, PartitionImportFromPrimaryInterface) { "export module foo;\n" "import :tls;\n"); auto u = scan_file(dir / "src" / "foo.cppm", "pkg", mcpp::builtin_extension_table()); - ASSERT_TRUE(u.has_value()) << u.error().format(); - ASSERT_EQ(u->requires_.size(), 1u); - EXPECT_EQ(u->requires_[0].logicalName, "foo:tls"); + if (auto&& failed = expect(fatal(u.has_value())); !failed) failed << u.error().format(); + expect(fatal(eq(u->requires_.size(), 1u))); + expect(eq(u->requires_[0].logicalName, "foo:tls")); std::filesystem::remove_all(dir); -} +}}; -TEST(Scanner, PartitionImportFromAnotherPartition) { +UTest partition_import_from_another_partition{"Scanner/PartitionImportFromAnotherPartition", [] { // Partition interface: `export module foo:http;` → logicalName = "foo:http". // `import :tls;` must resolve to "foo:tls" (the sibling partition), // NOT "foo:http:tls" (which is what a naive prepend produces). @@ -617,16 +613,16 @@ TEST(Scanner, PartitionImportFromAnotherPartition) { "import :tls;\n" "import :socket;\n"); auto u = scan_file(dir / "src" / "http.cppm", "pkg", mcpp::builtin_extension_table()); - ASSERT_TRUE(u.has_value()) << u.error().format(); - ASSERT_TRUE(u->provides.has_value()); - EXPECT_EQ(u->provides->logicalName, "foo:http"); - ASSERT_EQ(u->requires_.size(), 2u); - EXPECT_EQ(u->requires_[0].logicalName, "foo:tls"); - EXPECT_EQ(u->requires_[1].logicalName, "foo:socket"); + if (auto&& failed = expect(fatal(u.has_value())); !failed) failed << u.error().format(); + expect(fatal(u->provides.has_value())); + expect(eq(u->provides->logicalName, "foo:http")); + expect(fatal(eq(u->requires_.size(), 2u))); + expect(eq(u->requires_[0].logicalName, "foo:tls")); + expect(eq(u->requires_[1].logicalName, "foo:socket")); std::filesystem::remove_all(dir); -} +}}; -TEST(Scanner, PartitionImportWithDottedModuleName) { +UTest partition_import_with_dotted_module_name{"Scanner/PartitionImportWithDottedModuleName", [] { // Dotted module names (xpkg-style, e.g. `mcpplibs.tinyhttps:http`) // — only the colon-prefixed partition suffix is what we strip. auto dir = make_tempdir("mcpp-scanner"); @@ -634,13 +630,13 @@ TEST(Scanner, PartitionImportWithDottedModuleName) { "export module mcpplibs.tinyhttps:http;\n" "import :tls;\n"); auto u = scan_file(dir / "src" / "http.cppm", "pkg", mcpp::builtin_extension_table()); - ASSERT_TRUE(u.has_value()) << u.error().format(); - ASSERT_EQ(u->requires_.size(), 1u); - EXPECT_EQ(u->requires_[0].logicalName, "mcpplibs.tinyhttps:tls"); + if (auto&& failed = expect(fatal(u.has_value())); !failed) failed << u.error().format(); + expect(fatal(eq(u->requires_.size(), 1u))); + expect(eq(u->requires_[0].logicalName, "mcpplibs.tinyhttps:tls")); std::filesystem::remove_all(dir); -} +}}; -TEST(Scanner, RejectsConditionalImport) { +UTest rejects_conditional_import{"Scanner/RejectsConditionalImport", [] { auto dir = make_tempdir("mcpp-scanner"); write(dir / "main.cpp", "import std;\n" @@ -649,38 +645,38 @@ TEST(Scanner, RejectsConditionalImport) { "#endif\n" "int main(){}"); auto r = scan_file(dir / "main.cpp", "pkg", mcpp::builtin_extension_table()); - EXPECT_FALSE(r.has_value()); - EXPECT_NE(r.error().message.find("conditional"), std::string::npos); + expect(!r.has_value()); + expect(ne(r.error().message.find("conditional"), std::string::npos)); std::filesystem::remove_all(dir); -} +}}; -TEST(Scanner, RejectsHeaderUnit) { +UTest rejects_header_unit{"Scanner/RejectsHeaderUnit", [] { auto dir = make_tempdir("mcpp-scanner"); write(dir / "main.cpp", "import std;\n" "import \"x.h\";\n" "int main(){}"); auto r = scan_file(dir / "main.cpp", "pkg", mcpp::builtin_extension_table()); - EXPECT_FALSE(r.has_value()); - EXPECT_NE(r.error().message.find("header units"), std::string::npos); + expect(!r.has_value()); + expect(ne(r.error().message.find("header units"), std::string::npos)); std::filesystem::remove_all(dir); -} +}}; -TEST(Scanner, ObjectiveCSourceIsCLike) { +UTest objective_c_source_is_c_like{"Scanner/ObjectiveCSourceIsCLike", [] { auto dir = make_tempdir("mcpp-scanner-objc"); write(dir / "src" / "window.m", "import Cocoa;\n" "int answer(void) { return 42; }\n"); auto u = scan_file(dir / "src" / "window.m", "pkg", mcpp::builtin_extension_table()); - ASSERT_TRUE(u.has_value()) << u.error().format(); - EXPECT_FALSE(u->provides.has_value()); - EXPECT_TRUE(u->requires_.empty()); + if (auto&& failed = expect(fatal(u.has_value())); !failed) failed << u.error().format(); + expect(!(u->provides.has_value())); + expect(u->requires_.empty()); std::filesystem::remove_all(dir); -} +}}; -TEST(Validate, ModuleNameNotRequiredToMatchPackageName) { +UTest module_name_not_required_to_match_package_name{"Validate/ModuleNameNotRequiredToMatchPackageName", [] { // 0.0.10+: module name does NOT need to be prefixed by package name. // The library author decides the module naming convention. Graph g; @@ -695,10 +691,10 @@ TEST(Validate, ModuleNameNotRequiredToMatchPackageName) { m.package.name = "myorg.foo"; auto rep = validate(g, m); - EXPECT_TRUE(rep.ok()) << "module name mismatch should not be an error"; -} + expect(rep.ok()) << "module name mismatch should not be an error"; +}}; -TEST(Validate, ForbiddenTopName) { +UTest forbidden_top_name{"Validate/ForbiddenTopName", [] { Graph g; SourceUnit u; u.path = "/x/foo.cppm"; @@ -711,10 +707,10 @@ TEST(Validate, ForbiddenTopName) { m.package.name = "myorg.foo"; auto rep = validate(g, m); - EXPECT_FALSE(rep.ok()); -} + expect(!rep.ok()); +}}; -TEST(Validate, LibRootHappyPath) { +UTest lib_root_happy_path{"Validate/LibRootHappyPath", [] { // Project: lib target "tinyhttps", convention puts the lib root at // src/tinyhttps.cppm exporting `mcpplibs.tinyhttps`. Two partition // siblings sit alongside. @@ -740,12 +736,12 @@ TEST(Validate, LibRootHappyPath) { m.targets.push_back(t); auto rep = validate(g, m); // empty projectRoot → on-disk check skipped - EXPECT_TRUE(rep.ok()) << "errors:" << [&]{ + expect(rep.ok()) << "errors:" << [&]{ std::string s; for (auto& e : rep.errors) s += "\n " + e.message; return s; }(); -} +}}; -TEST(Validate, LibRootExportsPartitionIsError) { +UTest lib_root_exports_partition_is_error{"Validate/LibRootExportsPartitionIsError", [] { // Lib root file at the conventional path exports `:foo` (a partition // suffix) — must be rejected: lib root must be the primary module. Graph g; @@ -764,16 +760,16 @@ TEST(Validate, LibRootExportsPartitionIsError) { m.targets.push_back(t); auto rep = validate(g, m); - EXPECT_FALSE(rep.ok()); + expect(!rep.ok()); bool found = false; for (auto& e : rep.errors) { if (e.message.find("partition") != std::string::npos && e.message.find("primary module") != std::string::npos) { found = true; break; } } - EXPECT_TRUE(found) << "expected lib-root partition error"; -} + expect(bool(found)) << "expected lib-root partition error"; +}}; -TEST(Validate, LibRootDifferentModuleNameIsAllowed) { +UTest lib_root_different_module_name_is_allowed{"Validate/LibRootDifferentModuleNameIsAllowed", [] { // 0.0.10+: lib root module name does NOT need to match [package].name. // The library author decides the module name; the build tool auto-detects. Graph g; @@ -792,10 +788,10 @@ TEST(Validate, LibRootDifferentModuleNameIsAllowed) { m.targets.push_back(t); auto rep = validate(g, m); - EXPECT_TRUE(rep.ok()) << "module name mismatch should not be an error"; -} + expect(rep.ok()) << "module name mismatch should not be an error"; +}}; -TEST(Validate, LibRootNotEnforcedForBinaryProject) { +UTest lib_root_not_enforced_for_binary_project{"Validate/LibRootNotEnforcedForBinaryProject", [] { // Pure-binary project: no lib target → no lib-root checks. Even if a // file at src/.cppm exists exporting an unrelated module, no // error should fire. @@ -809,10 +805,10 @@ TEST(Validate, LibRootNotEnforcedForBinaryProject) { m.targets.push_back(t); auto rep = validate(g, m); - EXPECT_TRUE(rep.ok()); -} + expect(rep.ok()); +}}; -TEST(Validate, LibRootMissingFileWithExplicitPathIsError) { +UTest lib_root_missing_file_with_explicit_path_is_error{"Validate/LibRootMissingFileWithExplicitPathIsError", [] { Graph g; mcpp::manifest::Manifest m; m.package.name = "myorg.foo"; @@ -824,15 +820,15 @@ TEST(Validate, LibRootMissingFileWithExplicitPathIsError) { // Pass a non-empty projectRoot so the on-disk check is enabled. auto rep = validate(g, m, std::filesystem::current_path()); - EXPECT_FALSE(rep.ok()); + expect(!rep.ok()); bool found = false; for (auto& e : rep.errors) { if (e.message.find("does not exist") != std::string::npos) { found = true; break; } } - EXPECT_TRUE(found) << "expected explicit-path-missing error"; -} + expect(bool(found)) << "expected explicit-path-missing error"; +}}; -TEST(TopoSort, DetectsCycle) { +UTest detects_cycle{"TopoSort/DetectsCycle", [] { Graph g; g.units.resize(2); g.units[0].provides = ModuleId{"a"}; @@ -845,24 +841,24 @@ TEST(TopoSort, DetectsCycle) { g.edges.push_back({1, 0}); // b->a auto r = topo_sort(g); - EXPECT_FALSE(r.has_value()); -} + expect(!r.has_value()); +}}; -TEST(IsPublicPackage, DotMarksPublic) { - EXPECT_TRUE(is_public_package_name("myorg.foo")); - EXPECT_FALSE(is_public_package_name("foo")); -} +UTest dot_marks_public{"IsPublicPackage/DotMarksPublic", [] { + expect(is_public_package_name("myorg.foo")); + expect(!is_public_package_name("foo")); +}}; -TEST(IsForbiddenTopModule, KnownNames) { - EXPECT_TRUE(is_forbidden_top_module("core")); - EXPECT_TRUE(is_forbidden_top_module("util.x")); - EXPECT_FALSE(is_forbidden_top_module("myorg.foo")); -} +UTest known_names{"IsForbiddenTopModule/KnownNames", [] { + expect(is_forbidden_top_module("core")); + expect(is_forbidden_top_module("util.x")); + expect(!is_forbidden_top_module("myorg.foo")); +}}; // G4: per-glob flags attach to exactly the matched units, in declaration // order; a zero-hit glob warns (not errors — cfg-gated source sets may // legitimately leave a glob empty on some targets). -TEST(Scanner, PerGlobFlagsAttachToMatchedUnits) { +UTest per_glob_flags_attach_to_matched_units{"Scanner/PerGlobFlagsAttachToMatchedUnits", [] { auto dir = make_tempdir("mcpp-scanner-globflags"); write(dir / "src" / "hot.cpp", "int hot() { return 1; }\n"); write(dir / "third_party" / "noisy.cpp", "int noisy() { return 2; }\n"); @@ -878,44 +874,40 @@ TEST(Scanner, PerGlobFlagsAttachToMatchedUnits) { { .glob = "nothing/**", .cxxflags = {"-Wnever"} }); auto res = scan_package(dir, m); - ASSERT_TRUE(res.errors.empty()); - ASSERT_EQ(res.graph.units.size(), 2u); + expect(fatal(res.errors.empty())); + expect(fatal(eq(res.graph.units.size(), 2u))); for (auto& u : res.graph.units) { if (u.path.filename() == "noisy.cpp") { - EXPECT_EQ(u.packageCxxflags, (std::vector{"-w"})); + expect(eq(u.packageCxxflags, (std::vector{"-w"}))); } else { // defines land before the entry's own flag lists - EXPECT_EQ(u.packageCxxflags, - (std::vector{"-DHOT", "-mavx2"})); + expect(eq(u.packageCxxflags, (std::vector{"-DHOT", "-mavx2"}))); } } // The unmatched glob warns. bool warned = false; for (auto& w : res.warnings) if (w.message.find("nothing/**") != std::string::npos) warned = true; - EXPECT_TRUE(warned); + expect(bool(warned)); std::filesystem::remove_all(dir); -} +}}; // #228: expand_braces desugars `{a,b}` into a cartesian product of plain // globs. No braces -> passthrough; multiple/nested groups combine. -TEST(Scanner, ExpandBracesDesugarsCartesianProduct) { - EXPECT_EQ(expand_braces("a/**"), (std::vector{"a/**"})); - EXPECT_EQ(expand_braces("a/{x,y}/**"), - (std::vector{"a/x/**", "a/y/**"})); - EXPECT_EQ(expand_braces("a/{x,y}/{1,2}"), - (std::vector{"a/x/1", "a/x/2", "a/y/1", "a/y/2"})); +UTest expand_braces_desugars_cartesian_product{"Scanner/ExpandBracesDesugarsCartesianProduct", [] { + expect(eq(expand_braces("a/**"), (std::vector{"a/**"}))); + expect(eq(expand_braces("a/{x,y}/**"), (std::vector{"a/x/**", "a/y/**"}))); + expect(eq(expand_braces("a/{x,y}/{1,2}"), (std::vector{"a/x/1", "a/x/2", "a/y/1", "a/y/2"}))); // Nested group. - EXPECT_EQ(expand_braces("a/{x,{y,z}}/**"), - (std::vector{"a/x/**", "a/y/**", "a/z/**"})); -} + expect(eq(expand_braces("a/{x,{y,z}}/**"), (std::vector{"a/x/**", "a/y/**", "a/z/**"}))); +}}; // Review fix on #228: brace-nesting recursion depth is bounded (cap: 32 // levels), so a pathological manifest with deeply nested `{` cannot // stack-overflow. Beyond the cap the remainder is passed through as a // literal instead of throwing — this must simply return, not crash. -TEST(Scanner, ExpandBracesBoundsDeeplyNestedRecursion) { +UTest expand_braces_bounds_deeply_nested_recursion{"Scanner/ExpandBracesBoundsDeeplyNestedRecursion", [] { std::string glob = "a/"; constexpr int kDepth = 500; // far beyond the 32-level cap for (int i = 0; i < kDepth; ++i) glob += "{"; @@ -923,15 +915,15 @@ TEST(Scanner, ExpandBracesBoundsDeeplyNestedRecursion) { for (int i = 0; i < kDepth; ++i) glob += "}"; std::vector out; - ASSERT_NO_THROW(out = expand_braces(glob)); - EXPECT_FALSE(out.empty()); -} + expect(fatal(nothrow([&] { out = expand_braces(glob); }))); + expect(!out.empty()); +}}; // #228: expand_glob applies brace desugaring at its entry, so a glob like // "p/{aac,bsf}/**" matches files under EITHER alternative directory and // nothing else (a sibling directory outside the brace group, "opus", must // not appear). -TEST(Scanner, ExpandGlobBraceAlternation) { +UTest expand_glob_brace_alternation{"Scanner/ExpandGlobBraceAlternation", [] { auto dir = make_tempdir("mcpp-scanner-brace"); write(dir / "p" / "aac" / "x.c", "int x;\n"); write(dir / "p" / "bsf" / "y.c", "int y;\n"); @@ -939,21 +931,21 @@ TEST(Scanner, ExpandGlobBraceAlternation) { auto files = expand_glob(dir, "p/{aac,bsf}/**"); - ASSERT_EQ(files.size(), 2u); + expect(fatal(eq(files.size(), 2u))); std::set names; for (auto& f : files) names.insert(f.filename().string()); - EXPECT_TRUE(names.contains("x.c")); - EXPECT_TRUE(names.contains("y.c")); - EXPECT_FALSE(names.contains("z.c")); + expect(names.contains("x.c")); + expect(names.contains("y.c")); + expect(!names.contains("z.c")); std::filesystem::remove_all(dir); -} +}}; // #228: brace alternation in a [build].flags glob must also match — the // per-glob-flags match point (scan_one_into's apply_glob_flags) uses // path_matches_glob directly rather than expand_glob's walk, so it needs its // own desugaring wire-up. -TEST(Scanner, PerGlobFlagsMatchBraceAlternation) { +UTest per_glob_flags_match_brace_alternation{"Scanner/PerGlobFlagsMatchBraceAlternation", [] { auto dir = make_tempdir("mcpp-scanner-globflags-brace"); write(dir / "p" / "aac" / "x.cpp", "int x() { return 1; }\n"); write(dir / "p" / "bsf" / "y.cpp", "int y() { return 2; }\n"); @@ -966,24 +958,24 @@ TEST(Scanner, PerGlobFlagsMatchBraceAlternation) { { .glob = "p/{aac,bsf}/**", .defines = {"CODEC"} }); auto res = scan_package(dir, m); - ASSERT_TRUE(res.errors.empty()); - ASSERT_EQ(res.graph.units.size(), 3u); + expect(fatal(res.errors.empty())); + expect(fatal(eq(res.graph.units.size(), 3u))); for (auto& u : res.graph.units) { bool wantsDefine = u.path.filename() != "z.cpp"; bool hasDefine = std::find(u.packageCxxflags.begin(), u.packageCxxflags.end(), "-DCODEC") != u.packageCxxflags.end(); - EXPECT_EQ(hasDefine, wantsDefine) << u.path.string(); + expect(eq(hasDefine, wantsDefine)) << u.path.string(); } std::filesystem::remove_all(dir); -} +}}; // G8b: relative -I flags are root-relative in the manifest but ninja runs // with cwd = output dir — the scanner absolutizes them on every unit. // The absolute spelling is NORMALIZED to native separators (#390): MSVC's // path keeps the input `/` verbatim, and `-I/abs/path` written with forward // slashes used to survive into the CDB's arguments as a mixed path. -TEST(Scanner, RelativeIncludeFlagsAbsolutized) { +UTest relative_include_flags_absolutized{"Scanner/RelativeIncludeFlagsAbsolutized", [] { auto dir = make_tempdir("mcpp-scanner-relinc"); write(dir / "src" / "a.cpp", "int a();\n"); @@ -997,17 +989,17 @@ TEST(Scanner, RelativeIncludeFlagsAbsolutized) { mm.buildConfig.cxxflags = {"-Iinc", "-I/abs/path", "-DKEEP"}; return scan_package(dir, mm); }(); - ASSERT_EQ(res.graph.units.size(), 1u); + expect(fatal(eq(res.graph.units.size(), 1u))); auto& fl = res.graph.units[0].packageCxxflags; - EXPECT_EQ(fl[0], "-I" + (dir / "inc").string()); + expect(eq(fl[0], "-I" + (dir / "inc").string())); if constexpr (std::filesystem::path::preferred_separator == '\\') - EXPECT_EQ(fl[1], "-I\\abs\\path"); // absolute stays, but native + expect(eq(fl[1], "-I\\abs\\path")); // absolute stays, but native else - EXPECT_EQ(fl[1], "-I/abs/path"); // absolute stays - EXPECT_EQ(fl[2], "-DKEEP"); // non-include untouched + expect(eq(fl[1], "-I/abs/path")); // absolute stays + expect(eq(fl[2], "-DKEEP")); // non-include untouched std::filesystem::remove_all(dir); -} +}}; // ─── mcpp#516 / #230: names the active code page cannot spell ────────────── // @@ -1035,7 +1027,7 @@ namespace { // nearest ancestor that CAN be named — never by the offending name, which // cannot be put into a message without throwing the very exception being // reported. Platform-independent logic, so this runs everywhere. -TEST(Glob, UnnarrowablePathsDedupToTheirSpellableAncestor) { +UTest unnarrowable_paths_dedup_to_their_spellable_ancestor{"Glob/UnnarrowablePathsDedupToTheirSpellableAncestor", [] { (void)take_unnarrowable_paths(); // other tests in this binary walk too const std::filesystem::path base = "/pkg/test/www"; @@ -1044,20 +1036,18 @@ TEST(Glob, UnnarrowablePathsDedupToTheirSpellableAncestor) { note_unnarrowable_path(base / "other.txt"); auto notes = take_unnarrowable_paths(); - ASSERT_EQ(notes.size(), 2u); // ".../www/bad" and ".../www" — not three files - EXPECT_NE(std::find(notes.begin(), notes.end(), (base / "bad").generic_string()), - notes.end()); - EXPECT_NE(std::find(notes.begin(), notes.end(), base.generic_string()), - notes.end()); + expect(fatal(eq(notes.size(), 2u))); // ".../www/bad" and ".../www" — not three files + expect(ne(std::find(notes.begin(), notes.end(), (base / "bad").generic_string()), notes.end())); + expect(ne(std::find(notes.begin(), notes.end(), base.generic_string()), notes.end())); // take() clears: the next command reports afresh rather than replaying. - EXPECT_TRUE(take_unnarrowable_paths().empty()); -} + expect(take_unnarrowable_paths().empty()); +}}; // A non-ASCII directory name must not disturb the walk on ANY platform. This // one is reproducible everywhere (on Windows the UTF-8 bytes land as whatever // the ACP makes of them, which still exercises the walk). -TEST(Scanner, GlobWalkHandlesNonAsciiNames) { +UTest glob_walk_handles_non_ascii_names{"Scanner/GlobWalkHandlesNonAsciiNames", [] { auto dir = make_tempdir("mcpp-scanner-nonascii"); // U+65E5 U+672C U+8A9E in UTF-8, spelled as bytes so the test does not // depend on the source file's encoding. Split before "Dir" because a C++ @@ -1067,16 +1057,15 @@ TEST(Scanner, GlobWalkHandlesNonAsciiNames) { write(dir / "zzz_ascii" / "x.h", "#pragma once\n"); std::vector dirs; - ASSERT_NO_THROW({ dirs = expand_dir_glob(dir, "*"); }); - EXPECT_NE(std::find(dirs.begin(), dirs.end(), dir / "zzz_ascii"), dirs.end()); + expect(fatal(nothrow([&] { { dirs = expand_dir_glob(dir, "*"); }; }))); + expect(ne(std::find(dirs.begin(), dirs.end(), dir / "zzz_ascii"), dirs.end())); std::vector files; - ASSERT_NO_THROW({ files = expand_glob(dir, "**/*.h"); }); - EXPECT_NE(std::find(files.begin(), files.end(), dir / "zzz_ascii" / "x.h"), - files.end()); + expect(fatal(nothrow([&] { { files = expand_glob(dir, "**/*.h"); }; }))); + expect(ne(std::find(files.begin(), files.end(), dir / "zzz_ascii" / "x.h"), files.end())); std::filesystem::remove_all(dir); -} +}}; // mcpp#516 proper: a directory name the ACTIVE CODE PAGE cannot spell used to // throw out of `is_excluded_walk_dir`'s `.filename().string()` — the first @@ -1089,10 +1078,10 @@ TEST(Scanner, GlobWalkHandlesNonAsciiNames) { // Asserts (a) the walk does not throw, (b) it is not TRUNCATED at the bad // entry, and (c) the skip was RECORDED — silently dropping the file is the // half-fix this whole change exists to avoid. -TEST(Scanner, GlobWalkSurvivesNamesTheCodePageCannotSpell) { +UTest glob_walk_survives_names_the_code_page_cannot_spell{"Scanner/GlobWalkSurvivesNamesTheCodePageCannotSpell", [] { #ifndef _WIN32 - GTEST_SKIP() << "path::string() performs no encoding conversion off Windows; " - "mcpp#516 is not reproducible here"; + skip([&] { std::ostringstream text; text << "path::string() performs no encoding conversion off Windows; " + "mcpp#516 is not reproducible here"; return text.str(); }()); #else // Devanagari is in NO Windows ANSI code page, so this case stays live on // every non-UTF-8 ACP. The Japanese name mirrors mcpp#516's real input @@ -1110,8 +1099,8 @@ TEST(Scanner, GlobWalkSurvivesNamesTheCodePageCannotSpell) { L'D', L'i', L'r'}; if (acp_can_spell(unspellable)) { - GTEST_SKIP() << "the active code page can spell any name (UTF-8 ACP); " - "mcpp#516 is unreachable on this host"; + skip([&] { std::ostringstream text; text << "the active code page can spell any name (UTF-8 ACP); " + "mcpp#516 is unreachable on this host"; return text.str(); }()); } auto dir = make_tempdir("mcpp-scanner-acp"); @@ -1125,23 +1114,22 @@ TEST(Scanner, GlobWalkSurvivesNamesTheCodePageCannotSpell) { // The include-dir channel — mcpp#516's actual path (`include_dirs = {"*"}`, // whose literal prefix is empty, so the walk starts at the package root). std::vector dirs; - ASSERT_NO_THROW({ dirs = expand_dir_glob(dir, "*"); }); - EXPECT_NE(std::find(dirs.begin(), dirs.end(), dir / "zzz_ascii"), dirs.end()) + expect(fatal(nothrow([&] { { dirs = expand_dir_glob(dir, "*"); }; }))); + expect(ne(std::find(dirs.begin(), dirs.end(), dir / "zzz_ascii"), dirs.end())) << "the walk was truncated at the unspellable entry"; // The file channel — installedLayoutMatchesIndex's // `expand_glob(verRoot, "mcpp.toml")` walks the whole tree the same way. std::vector files; - ASSERT_NO_THROW({ files = expand_glob(dir, "**/*.h"); }); - EXPECT_NE(std::find(files.begin(), files.end(), dir / "zzz_ascii" / "x.h"), - files.end()); + expect(fatal(nothrow([&] { { files = expand_glob(dir, "**/*.h"); }; }))); + expect(ne(std::find(files.begin(), files.end(), dir / "zzz_ascii" / "x.h"), files.end())); - EXPECT_FALSE(take_unnarrowable_paths().empty()) + expect(!(take_unnarrowable_paths().empty())) << "the skipped entries were not recorded, so nothing would be reported"; std::filesystem::remove_all(dir); #endif -} +}}; // ─── #693: the UTF-8 spelling a path must have ───────────────────────────── // @@ -1151,20 +1139,20 @@ TEST(Scanner, GlobWalkSurvivesNamesTheCodePageCannotSpell) { // and recorded, exactly as the Windows test above states for a name the code // page cannot spell. -TEST(Glob, Utf8ValidityIsDecidedByteByByte) { - EXPECT_TRUE(is_valid_utf8("")); - EXPECT_TRUE(is_valid_utf8("plain/ascii")); - EXPECT_TRUE(is_valid_utf8("caf\xC3\xA9")); // U+00E9 - EXPECT_TRUE(is_valid_utf8("\xE6\xB5\x8B\xE8\xAF\x95")); // U+6D4B U+8BD5 - EXPECT_TRUE(is_valid_utf8("\xF0\x9F\x98\x80")); // U+1F600 - EXPECT_FALSE(is_valid_utf8("caf\xE9")); // Latin-1 - EXPECT_FALSE(is_valid_utf8("\xB2\xE2\xCA\xD4")); // GBK for U+6D4B U+8BD5 - EXPECT_FALSE(is_valid_utf8("\xC0\xAF")); // an overlong '/' - EXPECT_FALSE(is_valid_utf8("\xED\xA0\x80")); // a surrogate - EXPECT_FALSE(is_valid_utf8("\xF4\x90\x80\x80")); // above U+10FFFF - EXPECT_FALSE(is_valid_utf8("\xE6\xB5")); // truncated - EXPECT_FALSE(is_valid_utf8("\x80")); // a stray continuation -} +UTest utf8_validity_is_decided_byte_by_byte{"Glob/Utf8ValidityIsDecidedByteByByte", [] { + expect(is_valid_utf8("")); + expect(is_valid_utf8("plain/ascii")); + expect(is_valid_utf8("caf\xC3\xA9")); // U+00E9 + expect(is_valid_utf8("\xE6\xB5\x8B\xE8\xAF\x95")); // U+6D4B U+8BD5 + expect(is_valid_utf8("\xF0\x9F\x98\x80")); // U+1F600 + expect(!is_valid_utf8("caf\xE9")); // Latin-1 + expect(!is_valid_utf8("\xB2\xE2\xCA\xD4")); // GBK for U+6D4B U+8BD5 + expect(!is_valid_utf8("\xC0\xAF")); // an overlong '/' + expect(!is_valid_utf8("\xED\xA0\x80")); // a surrogate + expect(!is_valid_utf8("\xF4\x90\x80\x80")); // above U+10FFFF + expect(!is_valid_utf8("\xE6\xB5")); // truncated + expect(!is_valid_utf8("\x80")); // a stray continuation +}}; // A diagnostic names a path that has no UTF-8 spelling through an escaped one. // @@ -1174,47 +1162,46 @@ TEST(Glob, Utf8ValidityIsDecidedByteByByte) { // throw. The assertion that once followed the branches did exactly that, so // the test passed on an English runner and failed on a Chinese one (O6 of the // 2026-10-05 design). -TEST(Glob, EscapedSpellingIsUtf8WhateverTheName) { +UTest escaped_spelling_is_utf8_whatever_the_name{"Glob/EscapedSpellingIsUtf8WhateverTheName", [] { #ifdef _WIN32 const std::wstring lone{L'a', wchar_t(0xD800), L'b'}; - EXPECT_EQ(escaped_spelling(std::filesystem::path(lone)), "a\\u{D800}b"); - EXPECT_TRUE(is_valid_utf8(escaped_spelling(std::filesystem::path(lone)))); + expect(eq(escaped_spelling(std::filesystem::path(lone)), "a\\u{D800}b")); + expect(is_valid_utf8(escaped_spelling(std::filesystem::path(lone)))); const std::wstring cafe{L'c', L'a', L'f', wchar_t(0x00E9)}; - EXPECT_EQ(escaped_spelling(std::filesystem::path(cafe)), "caf\xC3\xA9"); + expect(eq(escaped_spelling(std::filesystem::path(cafe)), "caf\xC3\xA9")); #else - EXPECT_EQ(escaped_spelling("/x/caf\xE9"), "/x/caf\\xE9"); - EXPECT_EQ(escaped_spelling("/x/caf\xC3\xA9"), "/x/caf\xC3\xA9"); - EXPECT_TRUE(is_valid_utf8(escaped_spelling(std::filesystem::path("caf\xE9")))); + expect(eq(escaped_spelling("/x/caf\xE9"), "/x/caf\\xE9")); + expect(eq(escaped_spelling("/x/caf\xC3\xA9"), "/x/caf\xC3\xA9")); + expect(is_valid_utf8(escaped_spelling(std::filesystem::path("caf\xE9")))); #endif -} +}}; -TEST(Scanner, GlobWalkSkipsNamesThatAreNotUtf8) { +UTest glob_walk_skips_names_that_are_not_utf8{"Scanner/GlobWalkSkipsNamesThatAreNotUtf8", [] { #ifdef _WIN32 - GTEST_SKIP() << "a Windows name is UTF-16; the test above states the Windows case"; + skip("a Windows name is UTF-16; the test above states the Windows case"); #else auto dir = make_tempdir("mcpp-scanner-latin1"); std::error_code ec; std::filesystem::create_directories(dir / "caf\xE9", ec); // Latin-1 if (ec) { std::filesystem::remove_all(dir); - GTEST_SKIP() << "this file system refuses a name that is not UTF-8: " - << ec.message(); + skip([&] { std::ostringstream text; text << "this file system refuses a name that is not UTF-8: " << ec.message(); return text.str(); }()); } write(dir / "caf\xE9" / "x.h", "#pragma once\n"); write(dir / "zzz_ascii" / "x.h", "#pragma once\n"); (void)take_unnarrowable_paths(); std::vector files; - ASSERT_NO_THROW({ files = expand_glob(dir, "**/*.h"); }); - EXPECT_EQ(files, (std::vector{dir / "zzz_ascii" / "x.h"})); + expect(fatal(nothrow([&] { { files = expand_glob(dir, "**/*.h"); }; }))); + expect(eq(files, (std::vector{dir / "zzz_ascii" / "x.h"}))); auto notes = take_unnarrowable_paths(); - ASSERT_EQ(notes.size(), 1u); - EXPECT_EQ(notes[0], dir.generic_string()); + expect(fatal(eq(notes.size(), 1u))); + expect(eq(notes[0], dir.generic_string())); std::filesystem::remove_all(dir); #endif -} +}}; // `module : private;` is a THIRD production, not a spelling of the two the // scanner already knew ([module.private.frag]). It declares nothing: the unit @@ -1225,7 +1212,7 @@ TEST(Scanner, GlobWalkSkipsNamesThatAreNotUtf8) { // are checked because the scanner tokenises them differently — `module : // private;` yields the "name" `:` and `module :private;` yields `:private` — // and one of the two passing proves nothing about the other. -TEST(Scanner, PrivateModuleFragmentDeclaresNothing) { +UTest private_module_fragment_declares_nothing{"Scanner/PrivateModuleFragmentDeclaresNothing", [] { for (std::string_view spelling : {"module : private;", "module :private;"}) { auto dir = make_tempdir("mcpp-privfrag"); write(dir / "src" / "pf.cppm", @@ -1236,20 +1223,20 @@ TEST(Scanner, PrivateModuleFragmentDeclaresNothing) { auto u = scan_file(dir / "src" / "pf.cppm", "pkg", mcpp::builtin_extension_table()); - ASSERT_TRUE(u.has_value()) << spelling << ": " << u.error().format(); - ASSERT_TRUE(u->provides.has_value()) << spelling; - EXPECT_EQ(u->provides->logicalName, "pf") << spelling; - EXPECT_TRUE(u->providesInterface) << spelling; + if (auto&& failed = expect(fatal(u.has_value())); !failed) failed << spelling << ": " << u.error().format(); + expect(fatal(u->provides.has_value())) << spelling; + expect(eq(u->provides->logicalName, "pf")) << spelling; + expect(bool(u->providesInterface)) << spelling; // The fragment contributes no edge in either direction. - EXPECT_TRUE(u->requires_.empty()) << spelling; + expect(u->requires_.empty()) << spelling; } -} +}}; // A UTF-8 byte-order mark is not part of the source text. MSVC writes one by // default, so this is ordinary input; every compiler skips it, and this scanner // did not — the mark stayed attached to the first token, the declaration was // not seen, and the unit silently provided nothing. -TEST(Scanner, ByteOrderMarkDoesNotHideTheModuleDeclaration) { +UTest byte_order_mark_does_not_hide_the_module_declaration{"Scanner/ByteOrderMarkDoesNotHideTheModuleDeclaration", [] { auto dir = make_tempdir("mcpp-bom"); write(dir / "src" / "bom.cppm", "\xEF\xBB\xBF" @@ -1259,27 +1246,26 @@ TEST(Scanner, ByteOrderMarkDoesNotHideTheModuleDeclaration) { auto u = scan_file(dir / "src" / "bom.cppm", "pkg", mcpp::builtin_extension_table()); - ASSERT_TRUE(u.has_value()) << u.error().format(); - ASSERT_TRUE(u->provides.has_value()) + if (auto&& failed = expect(fatal(u.has_value())); !failed) failed << u.error().format(); + expect(fatal(u->provides.has_value())) << "the BOM hid the module declaration"; - EXPECT_EQ(u->provides->logicalName, "bom"); - ASSERT_EQ(u->requires_.size(), 1u); - EXPECT_EQ(u->requires_[0].logicalName, "std"); -} + expect(eq(u->provides->logicalName, "bom")); + expect(fatal(eq(u->requires_.size(), 1u))); + expect(eq(u->requires_[0].logicalName, "std")); +}}; // UTF-16/32 is refused by name rather than misread. Every line after the mark // would be garbage, so a named refusal is the difference between a message and // a mystery. -TEST(Scanner, Utf16ByteOrderMarkIsRefusedByName) { +UTest utf16_byte_order_mark_is_refused_by_name{"Scanner/Utf16ByteOrderMarkIsRefusedByName", [] { auto dir = make_tempdir("mcpp-bom16"); write(dir / "src" / "u16.cppm", std::string_view("\xFF\xFE" "e\0x\0p\0", 8)); auto u = scan_file(dir / "src" / "u16.cppm", "pkg", mcpp::builtin_extension_table()); - ASSERT_FALSE(u.has_value()); - EXPECT_NE(u.error().message.find("UTF-16"), std::string::npos) - << u.error().message; -} + expect(fatal(!u.has_value())); + if (auto&& failed = expect(ne(u.error().message.find("UTF-16"), std::string::npos)); !failed) failed << u.error().message; +}}; // The scanner must never RECORD an identity no source could have declared. // @@ -1289,16 +1275,15 @@ TEST(Scanner, Utf16ByteOrderMarkIsRefusedByName) { // module fragment recorded a module called `:`, and the build graph grew an // edge for a BMI named after a colon. Nothing reported it — every step after // the mis-parse worked correctly on the answer it was given. -TEST(Scanner, AModuleIdentityThatIsNotANameIsRefused) { +UTest a_module_identity_that_is_not_a_name_is_refused{"Scanner/AModuleIdentityThatIsNotANameIsRefused", [] { auto dir = make_tempdir("mcpp-badname"); write(dir / "src" / "bad.cppm", "module :not_private;\n"); auto u = scan_file(dir / "src" / "bad.cppm", "pkg", mcpp::builtin_extension_table()); - ASSERT_FALSE(u.has_value()); - EXPECT_NE(u.error().message.find("is not a module name"), std::string::npos) - << u.error().message; -} + expect(fatal(!u.has_value())); + if (auto&& failed = expect(ne(u.error().message.find("is not a module name"), std::string::npos)); !failed) failed << u.error().message; +}}; // The message quotes WHAT WAS READ, not what was recorded. // @@ -1309,24 +1294,21 @@ TEST(Scanner, AModuleIdentityThatIsNotANameIsRefused) { // every byte of a UTF-8 sequence. GCC 16.1 refuses the same declaration with // `unrecognized 'MODULE-EXPORT ...'`, so this is a clearer sentence for a // refusal that already existed, not a new restriction. -TEST(Scanner, AnUnreadableModuleNameIsQuotedAsWritten) { +UTest an_unreadable_module_name_is_quoted_as_written{"Scanner/AnUnreadableModuleNameIsQuotedAsWritten", [] { auto dir = make_tempdir("mcpp-nonascii"); write(dir / "src" / "u.cppm", "export module \u6a21\u5757;\n"); auto u = scan_file(dir / "src" / "u.cppm", "pkg", mcpp::builtin_extension_table()); - ASSERT_FALSE(u.has_value()); - EXPECT_NE(u.error().message.find("is not a module name"), std::string::npos) - << u.error().message; - EXPECT_EQ(u.error().message.find("'' is not"), std::string::npos) - << "the message quoted an empty name: " << u.error().message; - EXPECT_NE(u.error().message.find("\u6a21\u5757"), std::string::npos) - << u.error().message; -} + expect(fatal(!u.has_value())); + if (auto&& failed = expect(ne(u.error().message.find("is not a module name"), std::string::npos)); !failed) failed << u.error().message; + if (auto&& failed = expect(eq(u.error().message.find("'' is not"), std::string::npos)); !failed) failed << "the message quoted an empty name: " << u.error().message; + if (auto&& failed = expect(ne(u.error().message.find("\u6a21\u5757"), std::string::npos)); !failed) failed << u.error().message; +}}; // The guard above must not refuse what the language allows. A partition, a // dotted name and a dotted partition are all names. -TEST(Scanner, WellFormedNamesSurviveTheIdentityGuard) { +UTest well_formed_names_survive_the_identity_guard{"Scanner/WellFormedNamesSurviveTheIdentityGuard", [] { struct Case { std::string_view decl; std::string_view provides; }; for (auto [decl, provides] : { Case{"export module a.b.c;", "a.b.c"}, @@ -1337,11 +1319,11 @@ TEST(Scanner, WellFormedNamesSurviveTheIdentityGuard) { write(dir / "src" / "g.cppm", std::format("{}\n", decl)); auto u = scan_file(dir / "src" / "g.cppm", "pkg", mcpp::builtin_extension_table()); - ASSERT_TRUE(u.has_value()) << decl << ": " << u.error().format(); - ASSERT_TRUE(u->provides.has_value()) << decl; - EXPECT_EQ(u->provides->logicalName, provides) << decl; + if (auto&& failed = expect(fatal(u.has_value())); !failed) failed << decl << ": " << u.error().format(); + expect(fatal(u->provides.has_value())) << decl; + expect(eq(u->provides->logicalName, provides)) << decl; } -} +}}; // ─── A module name is resolved in the importer's closure (mcpp#732) ───────── // @@ -1351,25 +1333,25 @@ TEST(Scanner, WellFormedNamesSurviveTheIdentityGuard) { // an import is resolved in the importing package's closure, and a closure may // hold one provider of a name. -TEST(ModuleResolution, OneProviderIsTakenWhereverItIs) { +UTest one_provider_is_taken_wherever_it_is{"ModuleResolution/OneProviderIsTakenWhereverItIs", [] { const std::vector providers{"lib"}; - EXPECT_EQ(choose_provider(providers, "app", {}), std::optional{0}); -} + expect(eq(choose_provider(providers, "app", {}), std::optional{0})); +}}; -TEST(ModuleResolution, TwoProvidersResolveInTheImportersClosure) { +UTest two_providers_resolve_in_the_importers_closure{"ModuleResolution/TwoProvidersResolveInTheImportersClosure", [] { const std::vector providers{"app", "updater"}; const Closures closures{{"app", {"app"}}, {"updater", {"updater"}}}; - EXPECT_EQ(choose_provider(providers, "app", closures), std::optional{0}); - EXPECT_EQ(choose_provider(providers, "updater", closures), std::optional{1}); -} + expect(eq(choose_provider(providers, "app", closures), std::optional{0})); + expect(eq(choose_provider(providers, "updater", closures), std::optional{1})); +}}; -TEST(ModuleResolution, TwoProvidersInOneClosureOrNoneResolveToNothing) { +UTest two_providers_in_one_closure_or_none_resolve_to_nothing{"ModuleResolution/TwoProvidersInOneClosureOrNoneResolveToNothing", [] { const std::vector providers{"lib1", "lib2"}; const Closures closures{{"prog", {"prog", "lib1", "lib2"}}, {"other", {"other"}}}; - EXPECT_FALSE(choose_provider(providers, "prog", closures)); - EXPECT_FALSE(choose_provider(providers, "other", closures)); - EXPECT_FALSE(choose_provider(providers, "prog", {})); -} + expect(!choose_provider(providers, "prog", closures)); + expect(!choose_provider(providers, "other", closures)); + expect(!choose_provider(providers, "prog", {})); +}}; namespace { // Two packages under `dir`, each providing module `boost` and importing it. @@ -1392,39 +1374,39 @@ std::string all_errors(const ScanResult& r) { } } // namespace -TEST(ModuleResolution, TwoProgramsEachResolveTheirOwnProvider) { +UTest two_programs_each_resolve_their_own_provider{"ModuleResolution/TwoProgramsEachResolveTheirOwnProvider", [] { auto dir = make_tempdir("mcpp-732-two-programs"); const Closures closures{{"app", {"app"}}, {"updater", {"updater"}}}; auto r = scan_packages(two_boost_packages(dir), closures); - ASSERT_TRUE(r.errors.empty()) << all_errors(r); - ASSERT_EQ(r.graph.providersOf.at("boost").size(), 2u); - EXPECT_FALSE(r.graph.producerOf.contains("boost")); + expect(fatal(r.errors.empty())) << all_errors(r); + expect(fatal(eq(r.graph.providersOf.at("boost").size(), 2u))); + expect(!r.graph.producerOf.contains("boost")); // Every edge joins a unit to the provider of its own package. - ASSERT_EQ(r.graph.edges.size(), 2u); + expect(fatal(eq(r.graph.edges.size(), 2u))); for (auto [consumer, producer] : r.graph.edges) - EXPECT_EQ(r.graph.units[consumer].packageName, r.graph.units[producer].packageName); + expect(eq(r.graph.units[consumer].packageName, r.graph.units[producer].packageName)); std::filesystem::remove_all(dir); -} +}}; -TEST(ModuleResolution, TwoProvidersInOneClosureAreRefused) { +UTest two_providers_in_one_closure_are_refused{"ModuleResolution/TwoProvidersInOneClosureAreRefused", [] { auto dir = make_tempdir("mcpp-732-one-closure"); const Closures closures{{"app", {"app", "updater"}}, {"updater", {"updater"}}}; auto r = scan_packages(two_boost_packages(dir), closures); const auto errors = all_errors(r); - EXPECT_NE(errors.find("module 'boost' is provided by package"), std::string::npos) << errors; - EXPECT_NE(errors.find("closure of package 'app'"), std::string::npos) << errors; + expect(ne(errors.find("module 'boost' is provided by package"), std::string::npos)) << errors; + expect(ne(errors.find("closure of package 'app'"), std::string::npos)) << errors; std::filesystem::remove_all(dir); -} +}}; -TEST(ModuleResolution, WithoutClosuresANameIsUniqueInTheGraph) { +UTest without_closures_a_name_is_unique_in_the_graph{"ModuleResolution/WithoutClosuresANameIsUniqueInTheGraph", [] { auto dir = make_tempdir("mcpp-732-no-closures"); auto r = scan_packages(two_boost_packages(dir)); const auto errors = all_errors(r); - EXPECT_NE(errors.find("module 'boost' is provided by package"), std::string::npos) << errors; + expect(ne(errors.find("module 'boost' is provided by package"), std::string::npos)) << errors; std::filesystem::remove_all(dir); -} +}}; -TEST(ModuleResolution, OneFileReachedTwiceIsNamedAsSuchWhateverItsSpelling) { +UTest one_file_reached_twice_is_named_as_such_whatever_its_spelling{"ModuleResolution/OneFileReachedTwiceIsNamedAsSuchWhateverItsSpelling", [] { auto dir = make_tempdir("mcpp-732-one-file"); write(dir / "shared" / "boost.cppm", "export module boost;\n"); std::vector packages; @@ -1438,52 +1420,52 @@ TEST(ModuleResolution, OneFileReachedTwiceIsNamedAsSuchWhateverItsSpelling) { const Closures closures{{"lib3", {"lib3", "lib4"}}, {"lib4", {"lib4"}}}; auto r = scan_packages(packages, closures); const auto errors = all_errors(r); - EXPECT_NE(errors.find("one file is reached as two packages"), std::string::npos) << errors; + expect(ne(errors.find("one file is reached as two packages"), std::string::npos)) << errors; std::filesystem::remove_all(dir); -} +}}; // A kept walk matched by the literal tail of a glob: `**/` also matches no // directory, so `**/main.cpp` matches a `main.cpp` at the root as well as one // below it, as the per-pattern walk did. `domain.cpp` ends with the same tail // and is still not a `main.cpp`. -TEST(Scanner, ADoubleStarSlashTailMatchesAtTheRoot) { +UTest a_double_star_slash_tail_matches_at_the_root{"Scanner/ADoubleStarSlashTailMatchesAtTheRoot", [] { auto dir = make_tempdir("mcpp-glob-tail"); write(dir / "main.cpp", "int main() {}\n"); write(dir / "sub" / "main.cpp", "int main() {}\n"); write(dir / "sub" / "other.cpp", "int x;\n"); write(dir / "sub" / "domain.cpp", "int y;\n"); auto files = expand_glob(dir, "**/main.cpp"); - ASSERT_EQ(files.size(), 2u); - EXPECT_EQ(files[0], dir / "main.cpp"); - EXPECT_EQ(files[1], dir / "sub" / "main.cpp"); + expect(fatal(eq(files.size(), 2u))); + expect(eq(files[0], dir / "main.cpp")); + expect(eq(files[1], dir / "sub" / "main.cpp")); std::filesystem::remove_all(dir); -} +}}; // `**/` stands for zero or more whole directories: what follows it starts a // path segment. It used to resume at any character, so `**/main.cpp` named // `src/domain.cpp` and `tests/**/test_*.cpp` named `tests/mytest_a.cpp`. // A `**` not followed by `/` still crosses segments and names. -TEST(Glob, ADoubleStarSlashStandsForWholeDirectories) { - EXPECT_TRUE(relative_path_matches_glob("main.cpp", "**/main.cpp")); - EXPECT_TRUE(relative_path_matches_glob("src/main.cpp", "**/main.cpp")); - EXPECT_TRUE(relative_path_matches_glob("src/a/b/main.cpp", "**/main.cpp")); - EXPECT_FALSE(relative_path_matches_glob("domain.cpp", "**/main.cpp")); - EXPECT_FALSE(relative_path_matches_glob("src/domain.cpp", "**/main.cpp")); - EXPECT_FALSE(relative_path_matches_glob("src/a/domain.cpp", "**/main.cpp")); - - EXPECT_TRUE(relative_path_matches_glob("tests/test_a.cpp", "tests/**/test_*.cpp")); - EXPECT_TRUE(relative_path_matches_glob("tests/x/test_a.cpp", "tests/**/test_*.cpp")); - EXPECT_FALSE(relative_path_matches_glob("tests/mytest_a.cpp", "tests/**/test_*.cpp")); - EXPECT_FALSE(relative_path_matches_glob("tests/x/mytest_a.cpp", "tests/**/test_*.cpp")); +UTest a_double_star_slash_stands_for_whole_directories{"Glob/ADoubleStarSlashStandsForWholeDirectories", [] { + expect(bool(relative_path_matches_glob("main.cpp", "**/main.cpp"))); + expect(bool(relative_path_matches_glob("src/main.cpp", "**/main.cpp"))); + expect(bool(relative_path_matches_glob("src/a/b/main.cpp", "**/main.cpp"))); + expect(!relative_path_matches_glob("domain.cpp", "**/main.cpp")); + expect(!relative_path_matches_glob("src/domain.cpp", "**/main.cpp")); + expect(!relative_path_matches_glob("src/a/domain.cpp", "**/main.cpp")); + + expect(bool(relative_path_matches_glob("tests/test_a.cpp", "tests/**/test_*.cpp"))); + expect(bool(relative_path_matches_glob("tests/x/test_a.cpp", "tests/**/test_*.cpp"))); + expect(!relative_path_matches_glob("tests/mytest_a.cpp", "tests/**/test_*.cpp")); + expect(!relative_path_matches_glob("tests/x/mytest_a.cpp", "tests/**/test_*.cpp")); const std::string_view dnn = "**/modules/dnn/**/*.avx2.cpp"; - EXPECT_TRUE(relative_path_matches_glob("modules/dnn/l.avx2.cpp", dnn)); - EXPECT_TRUE(relative_path_matches_glob("a/modules/dnn/b/l.avx2.cpp", dnn)); - EXPECT_FALSE(relative_path_matches_glob("a/submodules/dnn/l.avx2.cpp", dnn)); - EXPECT_FALSE(relative_path_matches_glob("modules/xdnn/l.avx2.cpp", dnn)); - - EXPECT_TRUE(relative_path_matches_glob("src/a/b.cpp", "src/**")); - EXPECT_TRUE(relative_path_matches_glob("src/a/b.cpp", "src/**.cpp")); - EXPECT_TRUE(relative_path_matches_glob("src/a/b.cpp", "**")); - EXPECT_FALSE(relative_path_matches_glob("srcx/a.cpp", "src/**/*.cpp")); -} + expect(bool(relative_path_matches_glob("modules/dnn/l.avx2.cpp", dnn))); + expect(bool(relative_path_matches_glob("a/modules/dnn/b/l.avx2.cpp", dnn))); + expect(!relative_path_matches_glob("a/submodules/dnn/l.avx2.cpp", dnn)); + expect(!relative_path_matches_glob("modules/xdnn/l.avx2.cpp", dnn)); + + expect(bool(relative_path_matches_glob("src/a/b.cpp", "src/**"))); + expect(bool(relative_path_matches_glob("src/a/b.cpp", "src/**.cpp"))); + expect(bool(relative_path_matches_glob("src/a/b.cpp", "**"))); + expect(!relative_path_matches_glob("srcx/a.cpp", "src/**/*.cpp")); +}}; diff --git a/tests/unit/test_module_address.cpp b/tests/unit/test_module_address.cpp index cc9341970..ec81cb261 100644 --- a/tests/unit/test_module_address.cpp +++ b/tests/unit/test_module_address.cpp @@ -15,9 +15,9 @@ // plans differ in both. It is the BMI counterpart of test_object_address.cpp // (mcpp#344). -#include import std; +import mcpplibs.mtest; import mcpp.build.plan; import mcpp.build.ninja; import mcpp.manifest; @@ -26,6 +26,8 @@ import mcpp.modgraph.scanner; import mcpp.source_kind; import mcpp.toolchain.model; +using namespace mcpplibs::mtest; + using namespace mcpp::build; namespace { @@ -165,70 +167,70 @@ void expect_same_core(mcpp::toolchain::CompilerId id) { Tmp t; std::string err; auto alone = plan_core(t, id, false, &err); - ASSERT_TRUE(alone) << err; + expect(fatal(bool(alone))) << err; auto beside = plan_core(t, id, true, &err); - ASSERT_TRUE(beside) << err; - ASSERT_EQ(alone->core.size(), 2u); - ASSERT_EQ(beside->core.size(), 2u); + expect(fatal(bool(beside))) << err; + expect(fatal(eq(alone->core.size(), 2u))); + expect(fatal(eq(beside->core.size(), 2u))); for (auto const& [file, a] : alone->core) { auto const& b = beside->core.at(file); - EXPECT_EQ(a.bmiFile, b.bmiFile) << file; - EXPECT_EQ(a.flags, b.flags) << file; - EXPECT_EQ(a.mapContent, b.mapContent) << file; - EXPECT_EQ(a.arguments, b.arguments) << file; + expect(eq(a.bmiFile, b.bmiFile)) << file; + expect(eq(a.flags, b.flags)) << file; + expect(eq(a.mapContent, b.mapContent)) << file; + expect(eq(a.arguments, b.arguments)) << file; } - EXPECT_EQ(alone->needsPic, beside->needsPic); + expect(eq(alone->needsPic, beside->needsPic)); } } // namespace // The load-bearing assertion: a provider of the same module name elsewhere in // the graph does not move `core`'s BMIs or change a word of its commands. -TEST(ModuleAddress, MemberCommandsAreImmuneToAnotherProviderOfTheName) { +UTest member_commands_are_immune_to_another_provider_of_the_name{"ModuleAddress/MemberCommandsAreImmuneToAnotherProviderOfTheName", [] { expect_same_core(mcpp::toolchain::CompilerId::GCC); expect_same_core(mcpp::toolchain::CompilerId::Clang); -} +}}; // Every BMI of a package other than the root lies below that package's // directory, and a unit is told where through one map that lists its own // module and what it reaches through its imports, and nothing else. -TEST(ModuleAddress, MemberBmisAreQualifiedAndTheMapIsTheUnitsReach) { +UTest member_bmis_are_qualified_and_the_map_is_the_units_reach{"ModuleAddress/MemberBmisAreQualifiedAndTheMapIsTheUnitsReach", [] { Tmp t; std::string err; auto gcc = plan_core(t, mcpp::toolchain::CompilerId::GCC, true, &err); - ASSERT_TRUE(gcc) << err; + expect(fatal(bool(gcc))) << err; auto const& iface = gcc->core.at("core.cppm"); - EXPECT_EQ(iface.bmiFile, "core/corelib.gcm"); + expect(eq(iface.bmiFile, "core/corelib.gcm")); // GCC's map is rooted at the BMI directory, as its default mapper is, so // a BMI records its imports at the same paths whichever mapper made it. - EXPECT_TRUE(iface.mapContent.starts_with("$root gcm.cache\n")) << iface.mapContent; - EXPECT_NE(iface.mapContent.find("corelib core/corelib.gcm\n"), std::string::npos); - EXPECT_NE(iface.mapContent.find("m core/m.gcm\n"), std::string::npos); - EXPECT_NE(iface.mapContent.find("std std.gcm\n"), std::string::npos); - EXPECT_EQ(iface.mapContent.find("tool"), std::string::npos) + expect(iface.mapContent.starts_with("$root gcm.cache\n")) << iface.mapContent; + expect(ne(iface.mapContent.find("corelib core/corelib.gcm\n"), std::string::npos)); + expect(ne(iface.mapContent.find("m core/m.gcm\n"), std::string::npos)); + expect(ne(iface.mapContent.find("std std.gcm\n"), std::string::npos)); + expect(eq(iface.mapContent.find("tool"), std::string::npos)) << "the map names a module the unit does not reach:\n" << iface.mapContent; - ASSERT_FALSE(iface.flags.empty()); - EXPECT_TRUE(iface.flags.back().find("-fmodule-mapper=modmap/core-") != std::string::npos) + expect(fatal(!iface.flags.empty())); + expect(iface.flags.back().find("-fmodule-mapper=modmap/core-") != std::string::npos) << iface.flags.back(); auto clang = plan_core(t, mcpp::toolchain::CompilerId::Clang, true, &err); - ASSERT_TRUE(clang) << err; + expect(fatal(bool(clang))) << err; auto const& ci = clang->core.at("core.cppm"); - EXPECT_EQ(ci.bmiFile, "core/corelib.pcm"); + expect(eq(ci.bmiFile, "core/corelib.pcm")); // clang reads the imported module's BMI from the argument file; its own // module is written through -fmodule-output and is not listed there. - ASSERT_EQ(ci.arguments.size(), 1u); - EXPECT_TRUE(ci.arguments.front().starts_with("-fmodule-file=m=")); - EXPECT_TRUE(ci.arguments.front().ends_with("pcm.cache/core/m.pcm")) << ci.arguments.front(); - ASSERT_FALSE(ci.flags.empty()); - EXPECT_TRUE(ci.flags.back().find(".modmap") != std::string::npos) << ci.flags.back(); - EXPECT_EQ(ci.flags.back().front(), '@'); -} + expect(fatal(eq(ci.arguments.size(), 1u))); + expect(ci.arguments.front().starts_with("-fmodule-file=m=")); + expect(ci.arguments.front().ends_with("pcm.cache/core/m.pcm")) << ci.arguments.front(); + expect(fatal(!ci.flags.empty())); + expect(ci.flags.back().find(".modmap") != std::string::npos) << ci.flags.back(); + expect(eq(ci.flags.back().front(), '@')); +}}; // The root package's modules stay at their names, and a unit that reaches only // those and `std` is told nothing: a project without dependency modules is laid // out, and every command spelled, as before. -TEST(ModuleAddress, RootModulesStayFlatAndNeedNoMap) { +UTest root_modules_stay_flat_and_need_no_map{"ModuleAddress/RootModulesStayFlatAndNeedNoMap", [] { Tmp t; const auto proj = t.path / "proj"; mcpp::manifest::Manifest root; @@ -251,25 +253,25 @@ TEST(ModuleAddress, RootModulesStayFlatAndNeedNoMap) { for (auto id : {mcpp::toolchain::CompilerId::GCC, mcpp::toolchain::CompilerId::Clang}) { auto plan = make_plan(root, toolchain(id), {}, graph, topo, packages, proj, proj / "target" / "t", {}, {}, {}); - ASSERT_TRUE(plan) << plan.error(); - EXPECT_TRUE(plan->moduleScopes.empty()); + if (auto&& failed = expect(fatal(bool(plan))); !failed) failed << plan.error(); + expect(plan->moduleScopes.empty()); for (auto const& cu : plan->compileUnits) { - EXPECT_TRUE(cu.moduleScope.empty()) << cu.source; + expect(cu.moduleScope.empty()) << cu.source; if (!cu.providesModule.empty()) - EXPECT_EQ(cu.bmiFile.find('/'), std::string::npos) << cu.bmiFile; + expect(eq(cu.bmiFile.find('/'), std::string::npos)) << cu.bmiFile; } } -} +}}; // Position independence is a property of the target (B2): every unit of a // hosted ELF target is compiled position-independent whether or not the graph // links a shared library, and a freestanding target's units are not. -TEST(ModuleAddress, PositionIndependenceFollowsTheTargetNotTheGraph) { +UTest position_independence_follows_the_target_not_the_graph{"ModuleAddress/PositionIndependenceFollowsTheTargetNotTheGraph", [] { Tmp t; std::string err; auto planned = plan_core(t, mcpp::toolchain::CompilerId::GCC, false, &err); - ASSERT_TRUE(planned) << err; - EXPECT_TRUE(planned->needsPic); + expect(fatal(bool(planned))) << err; + expect(bool(planned->needsPic)); const auto proj = t.path / "bare"; mcpp::manifest::Manifest root; @@ -286,41 +288,41 @@ TEST(ModuleAddress, PositionIndependenceFollowsTheTargetNotTheGraph) { tc.targetTriple = "arm-none-eabi"; auto plan = make_plan(root, tc, {}, graph, {0}, packages, proj, proj / "target" / "t", {}, {}, {}); - ASSERT_TRUE(plan) << plan.error(); - EXPECT_FALSE(plan->needsPic); -} + if (auto&& failed = expect(fatal(bool(plan))); !failed) failed << plan.error(); + expect(!plan->needsPic); +}}; // Every argument file a unit names is the one its own map states, with the // arguments the backend writes into it. A module and a unit importing it in // one package can list the same BMIs while only the importer loads one; when // the two shared a key, the importer named a file nothing wrote. -TEST(ModuleAddress, EveryNamedArgumentFileIsOneThePlanStates) { +UTest every_named_argument_file_is_one_the_plan_states{"ModuleAddress/EveryNamedArgumentFileIsOneThePlanStates", [] { Tmp t; auto plan = plan_workspace(t, mcpp::toolchain::CompilerId::Clang, true); - ASSERT_TRUE(plan) << plan.error(); + if (auto&& failed = expect(fatal(bool(plan))); !failed) failed << plan.error(); std::size_t named = 0; for (auto const& cu : plan->compileUnits) { for (auto const& f : cu.packageCxxflags) { if (!f.starts_with("@")) continue; ++named; auto it = plan->moduleScopes.find(cu.moduleScope); - ASSERT_NE(it, plan->moduleScopes.end()) << cu.source; - EXPECT_FALSE(it->second.arguments.empty()) << cu.source; - EXPECT_FALSE(it->second.argsFile.empty()) << cu.source; - EXPECT_TRUE(f.ends_with(it->second.argsFile.filename().string())) << f; + expect(fatal(ne(it, plan->moduleScopes.end()))) << cu.source; + expect(!(it->second.arguments.empty())) << cu.source; + expect(!(it->second.argsFile.empty())) << cu.source; + expect(f.ends_with(it->second.argsFile.filename().string())) << f; } } // core.cppm, app's main.cpp and tool's main.cpp import a module of // another package or of a member. - EXPECT_EQ(named, 3u); -} + expect(eq(named, 3u)); +}}; // The argument file of a module map, as each compiler reads it. MSVC takes an // option's value only from the same line of a command file (`D8004: '/reference' // requires an argument` on windows-2025 when they were on two lines), and reads // a non-ASCII path only with a byte order mark; clang reads GNU rules, where a // word with a space is quoted. -TEST(ModuleAddress, ArgumentFilesAreWrittenAsEachCompilerReadsThem) { +UTest argument_files_are_written_as_each_compiler_reads_them{"ModuleAddress/ArgumentFilesAreWrittenAsEachCompilerReadsThem", [] { Tmp t; auto read = [](const std::filesystem::path& p) { std::ifstream in(p, std::ios::binary); @@ -341,10 +343,9 @@ TEST(ModuleAddress, ArgumentFilesAreWrittenAsEachCompilerReadsThem) { std::filesystem::create_directories(msvc.outputDir / "modmap"); std::ofstream(msvc.outputDir / "modmap/core-1.modmap") << "/reference m=D:/old/m.ifc\n"; write_module_maps(msvc); - EXPECT_EQ(read(msvc.outputDir / "modmap/core-1.modmap"), - "\xEF\xBB\xBF/reference m=C:/build/ifc.cache/core/m.ifc\n" - "/reference n=C:/build/ifc.cache/core/n.ifc\n"); - EXPECT_EQ(read(msvc.outputDir / "modmap/core-1.map"), ms.content); + expect(eq(read(msvc.outputDir / "modmap/core-1.modmap"), "\xEF\xBB\xBF/reference m=C:/build/ifc.cache/core/m.ifc\n" + "/reference n=C:/build/ifc.cache/core/n.ifc\n")); + expect(eq(read(msvc.outputDir / "modmap/core-1.map"), ms.content)); BuildPlan clang; clang.outputDir = t.path / "clang"; @@ -357,7 +358,6 @@ TEST(ModuleAddress, ArgumentFilesAreWrittenAsEachCompilerReadsThem) { "-fmodule-file=n=/build/pcm.cache/core/n.pcm"}; clang.moduleScopes.emplace("core-2", cs); write_module_maps(clang); - EXPECT_EQ(read(clang.outputDir / "modmap/core-2.modmap"), - "\"-fmodule-file=m=/build dir/pcm.cache/core/m.pcm\"\n" - "-fmodule-file=n=/build/pcm.cache/core/n.pcm\n"); -} + expect(eq(read(clang.outputDir / "modmap/core-2.modmap"), "\"-fmodule-file=m=/build dir/pcm.cache/core/m.pcm\"\n" + "-fmodule-file=n=/build/pcm.cache/core/n.pcm\n")); +}}; diff --git a/tests/unit/test_msvc_coroutines.cpp b/tests/unit/test_msvc_coroutines.cpp index 5ecf76af6..77b4acb64 100644 --- a/tests/unit/test_msvc_coroutines.cpp +++ b/tests/unit/test_msvc_coroutines.cpp @@ -1,8 +1,10 @@ -#include import std; +import mcpplibs.mtest; import mcpp.toolchain.msvc_coroutines; +using namespace mcpplibs::mtest; + // The note a failed compile gets when clang does not support coroutines on the // MSVC ABI it targets (LLVM 23.1.3 Part 3, D1). The outputs below are copied // from probe PR #788 (run 37792334189): clang 23.1.3, i686-pc-windows-msvc, @@ -61,74 +63,74 @@ std::string with_standard(std::string_view message, std::string_view from, std:: return s; } -TEST(MsvcCoroutines, StdModuleMessageWithoutDiagnosticsGetsTheNote) { +UTest std_module_message_without_diagnostics_gets_the_note{"MsvcCoroutines/StdModuleMessageWithoutDiagnosticsGetsTheNote", [] { auto note = co::std_module_advice(kStdModuleMessage, answering(false)); - ASSERT_FALSE(note.empty()); - EXPECT_NE(note.find(""), std::string::npos); - EXPECT_NE(note.find("standard = \"c++20\""), std::string::npos); - EXPECT_NE(note.find("[target.i686-windows-msvc]"), std::string::npos); -} + expect(fatal(!note.empty())); + expect(ne(note.find(""), std::string::npos)); + expect(ne(note.find("standard = \"c++20\""), std::string::npos)); + expect(ne(note.find("[target.i686-windows-msvc]"), std::string::npos)); +}}; -TEST(MsvcCoroutines, StdModuleBelowCxx23IsNotThisFailure) { +UTest std_module_below_cxx23_is_not_this_failure{"MsvcCoroutines/StdModuleBelowCxx23IsNotThisFailure", [] { int calls = 0; auto cxx20 = with_standard(kStdModuleMessage, "-std=c++23", "-std=c++20"); - EXPECT_TRUE(co::std_module_advice(cxx20, answering(false, &calls)).empty()); - EXPECT_EQ(calls, 0); + expect(co::std_module_advice(cxx20, answering(false, &calls)).empty()); + expect(eq(calls, 0)); for (auto spelling : {"-std=c++2b", "-std=c++26", "-std=gnu++23"}) { auto later = with_standard(kStdModuleMessage, "-std=c++23", spelling); - EXPECT_FALSE(co::std_module_advice(later, answering(false)).empty()) << spelling; + expect(!(co::std_module_advice(later, answering(false)).empty())) << spelling; } -} +}}; -TEST(MsvcCoroutines, StdModuleOfLibcxxIsNotThisFailure) { +UTest std_module_of_libcxx_is_not_this_failure{"MsvcCoroutines/StdModuleOfLibcxxIsNotThisFailure", [] { int calls = 0; auto libcxx = with_standard(kStdModuleMessage, "modules\\std.ixx", "share\\libc++\\v1\\std.cppm"); - EXPECT_TRUE(co::std_module_advice(libcxx, answering(false, &calls)).empty()); - EXPECT_EQ(calls, 0); -} + expect(co::std_module_advice(libcxx, answering(false, &calls)).empty()); + expect(eq(calls, 0)); +}}; -TEST(MsvcCoroutines, StdModuleOnACompilerThatPredefinesTheMacroGetsNoNote) { - EXPECT_TRUE(co::std_module_advice(kStdModuleMessage, answering(true)).empty()); -} +UTest std_module_on_a_compiler_that_predefines_the_macro_gets_no_note{"MsvcCoroutines/StdModuleOnACompilerThatPredefinesTheMacroGetsNoNote", [] { + expect(co::std_module_advice(kStdModuleMessage, answering(true)).empty()); +}}; -TEST(MsvcCoroutines, StdModuleFailureIsTheGeneratorSymptom) { - EXPECT_EQ(co::symptom_in(kStdModuleFailure), co::Symptom::StdModuleGenerator); +UTest std_module_failure_is_the_generator_symptom{"MsvcCoroutines/StdModuleFailureIsTheGeneratorSymptom", [] { + expect(eq(co::symptom_in(kStdModuleFailure), co::Symptom::StdModuleGenerator)); auto cmd = co::failed_command_in(kStdModuleFailure); - ASSERT_TRUE(cmd.has_value()); - EXPECT_TRUE(cmd->compiler.ends_with("clang++.exe")); - EXPECT_EQ(cmd->target, "i686-pc-windows-msvc"); - EXPECT_EQ(cmd->standard, "c++23"); -} + expect(fatal(cmd.has_value())); + expect(cmd->compiler.ends_with("clang++.exe")); + expect(eq(cmd->target, "i686-pc-windows-msvc")); + expect(eq(cmd->standard, "c++23")); +}}; -TEST(MsvcCoroutines, StdModuleNoteOffersCxx20FirstAndLlvm22Optionally) { +UTest std_module_note_offers_cxx20_first_and_llvm22_optionally{"MsvcCoroutines/StdModuleNoteOffersCxx20FirstAndLlvm22Optionally", [] { auto note = co::advice(kStdModuleFailure, answering(false)); - ASSERT_FALSE(note.empty()); - EXPECT_NE(note.find("clang 23.1.3 does not support C++20 coroutines"), std::string::npos); - EXPECT_NE(note.find(""), std::string::npos); + expect(fatal(!note.empty())); + expect(ne(note.find("clang 23.1.3 does not support C++20 coroutines"), std::string::npos)); + expect(ne(note.find(""), std::string::npos)); const auto cxx20 = note.find("standard = \"c++20\""); const auto llvm22 = note.find("toolchain = \"llvm@22.1.8\""); - ASSERT_NE(cxx20, std::string::npos); - ASSERT_NE(llvm22, std::string::npos); - EXPECT_LT(cxx20, llvm22); + expect(fatal(ne(cxx20, std::string::npos))); + expect(fatal(ne(llvm22, std::string::npos))); + expect(lt(cxx20, llvm22)); // The section uses mcpp's spelling of the target. - EXPECT_NE(note.find("[target.i686-windows-msvc]"), std::string::npos); - EXPECT_NE(note.find("Defining __cpp_impl_coroutine yourself is not a fix"), std::string::npos); -} + expect(ne(note.find("[target.i686-windows-msvc]"), std::string::npos)); + expect(ne(note.find("Defining __cpp_impl_coroutine yourself is not a fix"), std::string::npos)); +}}; -TEST(MsvcCoroutines, CoroutineUseNoteDoesNotOfferCxx20) { - EXPECT_EQ(co::symptom_in(kCoroutineUseFailure), co::Symptom::CoroutineUse); +UTest coroutine_use_note_does_not_offer_cxx20{"MsvcCoroutines/CoroutineUseNoteDoesNotOfferCxx20", [] { + expect(eq(co::symptom_in(kCoroutineUseFailure), co::Symptom::CoroutineUse)); auto cmd = co::failed_command_in(kCoroutineUseFailure); - ASSERT_TRUE(cmd.has_value()); - EXPECT_EQ(cmd->standard, "c++20"); + expect(fatal(cmd.has_value())); + expect(eq(cmd->standard, "c++20")); auto note = co::advice(kCoroutineUseFailure, answering(false)); - ASSERT_FALSE(note.empty()); - EXPECT_NE(note.find("Code that uses coroutines"), std::string::npos); - EXPECT_EQ(note.find("c++20\""), std::string::npos); - EXPECT_NE(note.find("llvm@22.1.8"), std::string::npos); -} + expect(fatal(!note.empty())); + expect(ne(note.find("Code that uses coroutines"), std::string::npos)); + expect(eq(note.find("c++20\""), std::string::npos)); + expect(ne(note.find("llvm@22.1.8"), std::string::npos)); +}}; // A compile run through the capped executor names its compiler after `--`. -TEST(MsvcCoroutines, CoroutineUseThroughTheCappedExecutorGetsTheNote) { +UTest coroutine_use_through_the_capped_executor_gets_the_note{"MsvcCoroutines/CoroutineUseThroughTheCappedExecutorGetsTheNote", [] { constexpr std::string_view failure = "FAILED: obj/main.o \n" "C:/mcpp/bin/mcpp.exe __compiler-cap --sem .mcpp-sched --output obj/main.o --depfile obj/main.o.d -- " @@ -137,55 +139,55 @@ TEST(MsvcCoroutines, CoroutineUseThroughTheCappedExecutorGetsTheNote) { "D:/a/_temp/src/main.cpp:2:71: error: use of undeclared identifier 'std'\n" "D:/a/_temp/src/main.cpp:3:9: error: std::coroutine_traits type was not found; include before defining a coroutine\n"; auto cmd = co::failed_command_in(failure); - ASSERT_TRUE(cmd.has_value()); - EXPECT_TRUE(cmd->compiler.ends_with("clang++.exe")); - EXPECT_EQ(cmd->target, "i686-pc-windows-msvc"); - EXPECT_FALSE(co::advice(failure, answering(false)).empty()); -} + expect(fatal(cmd.has_value())); + expect(cmd->compiler.ends_with("clang++.exe")); + expect(eq(cmd->target, "i686-pc-windows-msvc")); + expect(!(co::advice(failure, answering(false)).empty())); +}}; -TEST(MsvcCoroutines, CompilerThatPredefinesTheMacroGetsNoNote) { - EXPECT_TRUE(co::advice(kStdModuleFailure, answering(true)).empty()); -} +UTest compiler_that_predefines_the_macro_gets_no_note{"MsvcCoroutines/CompilerThatPredefinesTheMacroGetsNoNote", [] { + expect(co::advice(kStdModuleFailure, answering(true)).empty()); +}}; -TEST(MsvcCoroutines, ProbeFailureGetsNoNote) { +UTest probe_failure_gets_no_note{"MsvcCoroutines/ProbeFailureGetsNoNote", [] { co::ProbeFn none = [](const co::FailedCommand&) -> std::optional { return std::nullopt; }; - EXPECT_TRUE(co::advice(kStdModuleFailure, none).empty()); -} + expect(co::advice(kStdModuleFailure, none).empty()); +}}; -TEST(MsvcCoroutines, UnrelatedFailureDoesNotRunTheProbe) { +UTest unrelated_failure_does_not_run_the_probe{"MsvcCoroutines/UnrelatedFailureDoesNotRunTheProbe", [] { int calls = 0; std::string other = "FAILED: obj/main.o \n" "clang++.exe -std=c++23 --target=i686-pc-windows-msvc -c main.cpp\n" "main.cpp:1:10: fatal error: 'missing.h' file not found\n"; - EXPECT_TRUE(co::advice(other, answering(false, &calls)).empty()); - EXPECT_EQ(calls, 0); -} + expect(co::advice(other, answering(false, &calls)).empty()); + expect(eq(calls, 0)); +}}; -TEST(MsvcCoroutines, NonMsvcTargetDoesNotRunTheProbe) { +UTest non_msvc_target_does_not_run_the_probe{"MsvcCoroutines/NonMsvcTargetDoesNotRunTheProbe", [] { int calls = 0; std::string gnu = "FAILED: obj/main.o \n" "clang++ -std=c++20 --target=i686-pc-windows-gnu -c main.cpp\n" "main.cpp:3:9: error: std::coroutine_traits type was not found\n"; - EXPECT_TRUE(co::advice(gnu, answering(false, &calls)).empty()); - EXPECT_EQ(calls, 0); -} + expect(co::advice(gnu, answering(false, &calls)).empty()); + expect(eq(calls, 0)); +}}; -TEST(MsvcCoroutines, OutputWithoutACommandGetsNoNote) { +UTest output_without_a_command_gets_no_note{"MsvcCoroutines/OutputWithoutACommandGetsNoNote", [] { std::string bare = "main.cpp:3:9: error: std::coroutine_traits type was not found\n"; - EXPECT_TRUE(co::advice(bare, answering(false)).empty()); -} + expect(co::advice(bare, answering(false)).empty()); +}}; -TEST(MsvcCoroutines, NoteWithoutVersionNamesTheCompilerGenerically) { +UTest note_without_version_names_the_compiler_generically{"MsvcCoroutines/NoteWithoutVersionNamesTheCompilerGenerically", [] { co::ProbeFn unversioned = [](const co::FailedCommand&) -> std::optional { return co::Probe{false, ""}; }; auto note = co::advice(kStdModuleFailure, unversioned); - EXPECT_NE(note.find("this clang does not support"), std::string::npos); -} + expect(ne(note.find("this clang does not support"), std::string::npos)); +}}; } // namespace diff --git a/tests/unit/test_network.cpp b/tests/unit/test_network.cpp index e30940548..fc94ae26e 100644 --- a/tests/unit/test_network.cpp +++ b/tests/unit/test_network.cpp @@ -1,12 +1,14 @@ -#include import std; +import mcpplibs.mtest; import mcpp.provider; import mcpp.provider.xlings; import mcpplibs.os.platform; import mcpp.invocation; import mcpp.run; +using namespace mcpplibs::mtest; + namespace { struct NetworkFixture { std::filesystem::path dir = std::filesystem::temp_directory_path() @@ -28,25 +30,25 @@ fi }; } -TEST(NetworkCheck, OfflineNeverInvokesTheProvider) { - if constexpr (mcpplibs::os::is_windows) GTEST_SKIP() << "POSIX subprocess fixture"; +UTest offline_never_invokes_the_provider{"NetworkCheck/OfflineNeverInvokesTheProvider", [] { + if constexpr (mcpplibs::os::is_windows) skip("POSIX subprocess fixture"); NetworkFixture f(R"SH(touch "$XLINGS_HOME/invoked" exit 1 )SH"); { mcpp::invocation::ScopedEnv online("MCPP_OFFLINE", "0"); - EXPECT_EQ(mcpp::provider::xlings::network_check(f.env, std::chrono::seconds(1)).status, "unsupported"); - ASSERT_TRUE(std::filesystem::exists(f.dir / "invoked")); + expect(eq(mcpp::provider::xlings::network_check(f.env, std::chrono::seconds(1)).status, "unsupported")); + expect(fatal(std::filesystem::exists(f.dir / "invoked"))); std::filesystem::remove(f.dir / "invoked"); } mcpp::invocation::ScopedEnv offline("MCPP_OFFLINE", "1"); const auto result = mcpp::provider::xlings::network_check(f.env, std::chrono::milliseconds(100)); - EXPECT_EQ(result.status, "offline"); - EXPECT_FALSE(std::filesystem::exists(f.dir / "invoked")); -} + expect(eq(result.status, "offline")); + expect(!std::filesystem::exists(f.dir / "invoked")); +}}; -TEST(NetworkCheck, DiscoveryAndProbesShareOneDeadline) { - if constexpr (mcpplibs::os::is_windows) GTEST_SKIP() << "POSIX subprocess fixture"; +UTest discovery_and_probes_share_one_deadline{"NetworkCheck/DiscoveryAndProbesShareOneDeadline", [] { + if constexpr (mcpplibs::os::is_windows) skip("POSIX subprocess fixture"); NetworkFixture f(R"SH( if [ "$2" = "network_endpoints" ]; then echo '{"kind":"data","dataKind":"network_endpoints","payload":{"endpoints":[{"url":"https://one.invalid"},{"url":"https://two.invalid"},{"url":"https://three.invalid"},{"url":"https://four.invalid"},{"url":"https://five.invalid"}]}}' @@ -57,13 +59,13 @@ fi mcpp::invocation::ScopedEnv offline("MCPP_OFFLINE", "0"); const auto start = std::chrono::steady_clock::now(); const auto result = mcpp::provider::xlings::network_check(f.env, std::chrono::milliseconds(400)); - EXPECT_LT(std::chrono::steady_clock::now() - start, std::chrono::seconds(2)); - ASSERT_EQ(result.endpoints.size(), 5u); - for (const auto& e : result.endpoints) EXPECT_EQ(e.status, "timeout"); -} + expect(lt(std::chrono::steady_clock::now() - start, std::chrono::seconds(2))); + expect(fatal(eq(result.endpoints.size(), 5u))); + for (const auto& e : result.endpoints) expect(eq(e.status, "timeout")); +}}; -TEST(NetworkCheck, MalformedProviderDataDoesNotEscapeDiagnosis) { - if constexpr (mcpplibs::os::is_windows) GTEST_SKIP() << "POSIX subprocess fixture"; +UTest malformed_provider_data_does_not_escape_diagnosis{"NetworkCheck/MalformedProviderDataDoesNotEscapeDiagnosis", [] { + if constexpr (mcpplibs::os::is_windows) skip("POSIX subprocess fixture"); NetworkFixture f(R"SH( if [ "$2" = "network_endpoints" ]; then echo '{"kind":"data","dataKind":"network_endpoints","payload":{"endpoints":[{"url":"https://one.invalid"}]}}' @@ -73,11 +75,11 @@ fi )SH"); mcpp::invocation::ScopedEnv offline("MCPP_OFFLINE", "0"); const auto result = mcpp::provider::xlings::network_check(f.env, std::chrono::seconds(1)); - ASSERT_EQ(result.endpoints.size(), 1u); - EXPECT_EQ(result.endpoints.front().status, "invalid_response"); -} + expect(fatal(eq(result.endpoints.size(), 1u))); + expect(eq(result.endpoints.front().status, "invalid_response")); +}}; -TEST(NetworkCheck, HeartbeatsCannotKeepAByteStalledDownloadAlive) { +UTest heartbeats_cannot_keep_a_byte_stalled_download_alive{"NetworkCheck/HeartbeatsCannotKeepAByteStalledDownloadAlive", [] { using namespace std::chrono; mcpp::provider::xlings::DownloadWatchdog watchdog(seconds(30)); const auto start = steady_clock::now(); @@ -85,24 +87,24 @@ TEST(NetworkCheck, HeartbeatsCannotKeepAByteStalledDownloadAlive) { p.files.push_back({.name = "tool", .started = true, .sourceAttempt = 1}); watchdog.observe(p, start); watchdog.observe(p, start + seconds(29)); // repeated protocol sample - EXPECT_TRUE(watchdog.expired(start + seconds(30))); + expect(bool(watchdog.expired(start + seconds(30)))); p.files.front().downloadedBytes = 1; watchdog.observe(p, start + seconds(30)); - EXPECT_FALSE(watchdog.expired(start + seconds(59))); + expect(!(watchdog.expired(start + seconds(59)))); p.files.front().sourceAttempt = 2; p.files.front().downloadedBytes = 0; watchdog.observe(p, start + seconds(60)); - EXPECT_FALSE(watchdog.expired(start + seconds(89))); + expect(!(watchdog.expired(start + seconds(89)))); p.files.front().finished = true; watchdog.observe(p, start + seconds(90)); - EXPECT_FALSE(watchdog.expired(start + hours(3))); -} + expect(!(watchdog.expired(start + hours(3)))); +}}; // xlings leaves a source that sent no byte for kSourceIdleTimeout and reports // the next one; mcpp's own bound is longer, so the next source is reached. // With the two equal, a source that connected and then sent nothing stopped the // install at attempt 1 (CI, 2026-11-02). -TEST(NetworkCheck, ASilentSourceIsLeftBeforeTheInstallStops) { +UTest a_silent_source_is_left_before_the_install_stops{"NetworkCheck/ASilentSourceIsLeftBeforeTheInstallStops", [] { using namespace std::chrono; namespace x = mcpp::provider::xlings; static_assert(x::kSourceConnectTimeout < x::kSourceIdleTimeout); @@ -113,32 +115,30 @@ TEST(NetworkCheck, ASilentSourceIsLeftBeforeTheInstallStops) { mcpp::provider::BootstrapProgress p; p.files.push_back({.name = "pkg", .started = true, .sourceAttempt = 1}); watchdog.observe(p, start); - EXPECT_FALSE(watchdog.expired(left)); + expect(!watchdog.expired(left)); p.files.front().sourceAttempt = 2; watchdog.observe(p, left); - EXPECT_FALSE(watchdog.expired(left + x::kSourceIdleTimeout)); - EXPECT_TRUE(watchdog.expired(left + x::kDownloadStallTimeout)); -} + expect(!watchdog.expired(left + x::kSourceIdleTimeout)); + expect(bool(watchdog.expired(left + x::kDownloadStallTimeout))); +}}; -TEST(NetworkCheck, AnotherDownloadCannotRenewAStalledFilesBudget) { +UTest another_download_cannot_renew_a_stalled_files_budget{"NetworkCheck/AnotherDownloadCannotRenewAStalledFilesBudget", [] { using namespace std::chrono; namespace x = mcpp::provider::xlings; const auto start = steady_clock::now(); x::InstallDeadline deadline(seconds(120), seconds(45), start); deadline.observe(R"({"kind":"data","dataKind":"download_progress","payload":{"stream":"install:1","files":[{"name":"stalled","started":true,"finished":false,"sourceAttempt":1,"downloadedBytes":0},{"name":"healthy","started":true,"finished":false,"sourceAttempt":1,"downloadedBytes":1}]}})", start); deadline.observe(R"({"kind":"data","dataKind":"download_progress","payload":{"stream":"install:1","files":[{"name":"stalled","started":true,"finished":false,"sourceAttempt":1,"downloadedBytes":0},{"name":"healthy","started":true,"finished":false,"sourceAttempt":1,"downloadedBytes":100}]}})", start + seconds(44)); - EXPECT_FALSE(deadline.expired(start + seconds(44))); - EXPECT_EQ(deadline.failure_reason(start + seconds(45)), - "download 'stalled' made no byte progress or source change for 45s (attempt 1)"); + expect(!(deadline.expired(start + seconds(44)))); + expect(eq(deadline.failure_reason(start + seconds(45)), "download 'stalled' made no byte progress or source change for 45s (attempt 1)")); deadline.observe(R"({"kind":"data","dataKind":"download_progress","payload":{"stream":"install:1","files":[{"name":"stalled","started":true,"finished":false,"sourceAttempt":2,"downloadedBytes":0},{"name":"healthy","started":true,"finished":true,"sourceAttempt":1,"downloadedBytes":100}]}})", start + seconds(46)); - EXPECT_TRUE(deadline.failure_reason(start + seconds(90)).empty()); - EXPECT_EQ(deadline.failure_reason(start + seconds(91)), - "download 'stalled' made no byte progress or source change for 45s (attempt 2)"); + expect(deadline.failure_reason(start + seconds(90)).empty()); + expect(eq(deadline.failure_reason(start + seconds(91)), "download 'stalled' made no byte progress or source change for 45s (attempt 2)")); deadline.observe(R"({"kind":"data","dataKind":"download_progress","payload":{"stream":"install:1","files":[{"name":"stalled","started":true,"finished":true,"sourceAttempt":2,"downloadedBytes":1}]}})", start + seconds(92)); - EXPECT_TRUE(deadline.failure_reason(start + hours(1)).empty()); -} + expect(deadline.failure_reason(start + hours(1)).empty()); +}}; -TEST(NetworkCheck, AQueuedFilesBudgetStartsWhenItBecomesActive) { +UTest a_queued_files_budget_starts_when_it_becomes_active{"NetworkCheck/AQueuedFilesBudgetStartsWhenItBecomesActive", [] { using namespace std::chrono; namespace x = mcpp::provider::xlings; const auto start = steady_clock::now(); @@ -146,22 +146,21 @@ TEST(NetworkCheck, AQueuedFilesBudgetStartsWhenItBecomesActive) { mcpp::provider::BootstrapProgress progress; progress.files.push_back({.name = "queued", .started = false}); watchdog.observe(progress, start); - EXPECT_FALSE(watchdog.expired(start + seconds(96))); + expect(!(watchdog.expired(start + seconds(96)))); // Neither bytes nor sourceAttempt changes when the worker obtains a slot. progress.files.front().started = true; watchdog.observe(progress, start + seconds(96)); - EXPECT_FALSE(watchdog.expired(start + seconds(140))); - EXPECT_EQ(watchdog.failure_reason(start + seconds(141)), - "download 'queued' made no byte progress or source change for 45s (attempt 0)"); + expect(!(watchdog.expired(start + seconds(140)))); + expect(eq(watchdog.failure_reason(start + seconds(141)), "download 'queued' made no byte progress or source change for 45s (attempt 0)")); progress.files.front().sourceAttempt = 1; watchdog.observe(progress, start + seconds(142)); - EXPECT_FALSE(watchdog.expired(start + seconds(186))); + expect(!(watchdog.expired(start + seconds(186)))); progress.files.front().finished = true; watchdog.observe(progress, start + seconds(187)); - EXPECT_FALSE(watchdog.expired(start + hours(1))); -} + expect(!(watchdog.expired(start + hours(1)))); +}}; -TEST(NetworkProcess, ProgressCancellationIsPolledWhileOutputContinues) { +UTest progress_cancellation_is_polled_while_output_continues{"NetworkProcess/ProgressCancellationIsPolledWhileOutputContinues", [] { using namespace std::chrono; const auto start = steady_clock::now(); const auto cmd = mcpplibs::os::is_windows @@ -175,22 +174,22 @@ TEST(NetworkProcess, ProgressCancellationIsPolledWhileOutputContinues) { .on_line = [&](std::string_view) { ++lines; }, .should_stop = [&] { return steady_clock::now() - start >= milliseconds(200); }, .one_string = sh.one_string }).timed_out; - EXPECT_TRUE(stopped); - EXPECT_GT(lines, 0); - EXPECT_LT(steady_clock::now() - start, seconds(3)); -} + expect(bool(stopped)); + expect(gt(lines, 0)); + expect(lt(steady_clock::now() - start, seconds(3))); +}}; -TEST(NetworkCheck, IndexPreparationIsBoundedBeforeTheFirstDownloadEvent) { +UTest index_preparation_is_bounded_before_the_first_download_event{"NetworkCheck/IndexPreparationIsBoundedBeforeTheFirstDownloadEvent", [] { using namespace std::chrono; const auto start = steady_clock::now(); mcpp::provider::xlings::InstallDeadline deadline(seconds(120), seconds(30), start); deadline.observe(R"({"kind":"heartbeat","ts":100})", start + seconds(119)); - EXPECT_TRUE(deadline.expired(start + seconds(120))); + expect(bool(deadline.expired(start + seconds(120)))); deadline.observe(R"({"kind":"progress","phase":"extract","percent":0})", start + seconds(119)); - EXPECT_FALSE(deadline.expired(start + minutes(10))); -} + expect(!(deadline.expired(start + minutes(10)))); +}}; -TEST(NetworkCheck, LegacyStatusAndHandshakeDoNotWriteTheSelectedHome) { +UTest legacy_status_and_handshake_do_not_write_the_selected_home{"NetworkCheck/LegacyStatusAndHandshakeDoNotWriteTheSelectedHome", [] { const auto dir = std::filesystem::temp_directory_path() / std::format("mcpp-legacy-probe-{}", std::chrono::steady_clock::now().time_since_epoch().count()); struct Cleanup { @@ -219,20 +218,20 @@ TEST(NetworkCheck, LegacyStatusAndHandshakeDoNotWriteTheSelectedHome) { std::filesystem::current_path(project); mcpp::provider::Location env{binary, home, {}}; const auto metadata = mcpp::provider::xlings::describe(std::filesystem::relative(binary, project)); - ASSERT_TRUE(metadata.answered) << "the legacy handshake fixture never ran"; - EXPECT_EQ(metadata.version, "9999.1.1.1"); + expect(fatal(bool(metadata.answered))) << "the legacy handshake fixture never ran"; + expect(eq(metadata.version, "9999.1.1.1")); for (const auto* capability : {"system_status", "payload_status"}) { const auto result = mcpp::provider::xlings::call(env, capability, "{}"); - ASSERT_FALSE(result); - EXPECT_NE(result.error().find("read_only_queries"), std::string::npos); + expect(fatal(!result)); + expect(ne(result.error().find("read_only_queries"), std::string::npos)); } mcpp::invocation::ScopedEnv online("MCPP_OFFLINE", "0"); - EXPECT_EQ(mcpp::provider::xlings::network_check(env, std::chrono::seconds(1)).status, "unsupported"); - EXPECT_TRUE(std::filesystem::is_empty(home)) << "legacy metadata or status touched the selected home"; - EXPECT_TRUE(std::filesystem::is_empty(project)) << "legacy metadata touched the current project"; -} + expect(eq(mcpp::provider::xlings::network_check(env, std::chrono::seconds(1)).status, "unsupported")); + expect(std::filesystem::is_empty(home)) << "legacy metadata or status touched the selected home"; + expect(std::filesystem::is_empty(project)) << "legacy metadata touched the current project"; +}}; -TEST(NetworkCheck, BootstrapRequiresDownloadBudgetsAndReadOnlyQueries) { +UTest bootstrap_requires_download_budgets_and_read_only_queries{"NetworkCheck/BootstrapRequiresDownloadBudgetsAndReadOnlyQueries", [] { mcpp::provider::ProviderInfo info; info.answered = true; info.version = "9999.1.1.1"; @@ -240,15 +239,15 @@ TEST(NetworkCheck, BootstrapRequiresDownloadBudgetsAndReadOnlyQueries) { info.features = {"index_dir", "index_store", "upstreams", "payload_status", "sync_index", "payload_currency", "binding_state"}; for (const auto* feature : {"network_diagnostics", "download_socket_budgets", "read_only_queries"}) { - EXPECT_NE(mcpp::provider::xlings::unmet(info).find(feature), std::string::npos); + expect(ne(mcpp::provider::xlings::unmet(info).find(feature), std::string::npos)); info.features.push_back(feature); } - EXPECT_TRUE(mcpp::provider::xlings::unmet(info).empty()); -} + expect(mcpp::provider::xlings::unmet(info).empty()); +}}; -TEST(NetworkProcess, MetadataAndResourceDeadlinesStopDescendants) { - if constexpr (mcpplibs::os::is_windows) GTEST_SKIP() << "POSIX subprocess fixture"; +UTest metadata_and_resource_deadlines_stop_descendants{"NetworkProcess/MetadataAndResourceDeadlinesStopDescendants", [] { + if constexpr (mcpplibs::os::is_windows) skip("POSIX subprocess fixture"); const std::string writer = R"SH( (file="${0%/*}/ticks"; i=0 while [ "$i" -lt 50 ]; do @@ -266,19 +265,19 @@ wait if (metadata) std::ofstream(f.env.binary) << "#!/bin/sh\n" << writer; const auto start = std::chrono::steady_clock::now(); if (metadata) - EXPECT_FALSE(mcpp::provider::xlings::describe(f.env.binary, std::chrono::milliseconds(500)).answered); + expect(!(mcpp::provider::xlings::describe(f.env.binary, std::chrono::milliseconds(500)).answered)); else { const auto report = mcpp::provider::xlings::network_check(f.env, std::chrono::milliseconds(500)); - ASSERT_EQ(report.endpoints.size(), 1u); - EXPECT_EQ(report.endpoints.front().status, "timeout"); + expect(fatal(eq(report.endpoints.size(), 1u))); + expect(eq(report.endpoints.front().status, "timeout")); } - EXPECT_LT(std::chrono::steady_clock::now() - start, std::chrono::seconds(2)); + expect(lt(std::chrono::steady_clock::now() - start, std::chrono::seconds(2))); const auto ticks = f.dir / "ticks"; - ASSERT_TRUE(std::filesystem::exists(ticks)) << "the descendant fixture never ran"; + expect(fatal(std::filesystem::exists(ticks))) << "the descendant fixture never ran"; const auto before = std::filesystem::file_size(ticks); - ASSERT_GT(before, 0u); + expect(fatal(gt(before, 0u))); std::this_thread::sleep_for(std::chrono::milliseconds(200)); - EXPECT_EQ(std::filesystem::file_size(ticks), before) + expect(eq(std::filesystem::file_size(ticks), before)) << (metadata ? "metadata" : "resource") << " deadline left a descendant writing"; } -} +}}; diff --git a/tests/unit/test_ninja_backend.cpp b/tests/unit/test_ninja_backend.cpp index bfb793ed7..af4ea11a0 100644 --- a/tests/unit/test_ninja_backend.cpp +++ b/tests/unit/test_ninja_backend.cpp @@ -1,6 +1,6 @@ -#include import std; +import mcpplibs.mtest; import mcpp.invocation; import mcpp.source_kind; import mcpp.build.compile_commands; @@ -17,6 +17,8 @@ import mcpplibs.os.platform; import mcpp.runtime.search; import mcpp.targetside; +using namespace mcpplibs::mtest; + using namespace mcpp::build; namespace { @@ -77,10 +79,10 @@ BuildPlan minimal_plan() { } // namespace -TEST(NinjaBackend, WindowsDriverLinksRetainTheSelectedTargetForCAndCxx) { +UTest windows_driver_links_retain_the_selected_target_for_c_and_cxx{"NinjaBackend/WindowsDriverLinksRetainTheSelectedTargetForCAndCxx", [] { // This regression covers the Windows-host PE driver link branch. if constexpr (!mcpplibs::os::is_windows) - GTEST_SKIP() << "the PE driver link branch is selected on Windows hosts"; + skip("the PE driver link branch is selected on Windows hosts"); for (const auto arch : {"i686", "x86_64", "aarch64"}) { auto plan = minimal_plan(); plan.toolchain.compiler = mcpp::toolchain::CompilerId::Clang; @@ -93,12 +95,12 @@ TEST(NinjaBackend, WindowsDriverLinksRetainTheSelectedTargetForCAndCxx) { plan.targetSide.cAbi.interfaceName = "msvc"; plan.targetSide.cxx.interfaceName = "msvc"; auto flags = compute_flags(plan); - EXPECT_NE(flags.ld.find(plan.toolchain.crossTargetFlag), std::string::npos); - EXPECT_NE(flags.ldC.find(plan.toolchain.crossTargetFlag), std::string::npos); + expect(ne(flags.ld.find(plan.toolchain.crossTargetFlag), std::string::npos)); + expect(ne(flags.ldC.find(plan.toolchain.crossTargetFlag), std::string::npos)); } -} +}}; -TEST(NinjaBackend, BitcodeExportToolsComeFromTheSelectedLlvmAndAreQuotedAsWords) { +UTest bitcode_export_tools_come_from_the_selected_llvm_and_are_quoted_as_words{"NinjaBackend/BitcodeExportToolsComeFromTheSelectedLlvmAndAreQuotedAsWords", [] { auto plan = minimal_plan(); plan.toolchain.compiler = mcpp::toolchain::CompilerId::Clang; plan.toolchain.targetTriple = "x86_64-pc-windows-msvc"; @@ -114,16 +116,16 @@ TEST(NinjaBackend, BitcodeExportToolsComeFromTheSelectedLlvmAndAreQuotedAsWords) auto text = emit_ninja_string(plan); const auto nm = plan.toolchain.binaryPath.parent_path() / (mcpplibs::os::is_windows ? "llvm-nm.exe" : "llvm-nm"); - EXPECT_NE(text.find("--llvm-cxx " + ninja_command_word(plan.toolchain.binaryPath.generic_string())), std::string::npos); - EXPECT_NE(text.find("--llvm-nm " + ninja_command_word(nm.generic_string())), std::string::npos); - EXPECT_NE(text.find("--llvm-target x86_64-pc-windows-msvc"), std::string::npos); - EXPECT_NE(text.find("build bin/probe.def : coff_def obj/probe.o"), std::string::npos); + expect(ne(text.find("--llvm-cxx " + ninja_command_word(plan.toolchain.binaryPath.generic_string())), std::string::npos)); + expect(ne(text.find("--llvm-nm " + ninja_command_word(nm.generic_string())), std::string::npos)); + expect(ne(text.find("--llvm-target x86_64-pc-windows-msvc"), std::string::npos)); + expect(ne(text.find("build bin/probe.def : coff_def obj/probe.o"), std::string::npos)); plan.toolchain.compiler = mcpp::toolchain::CompilerId::MSVC; plan.toolchain.binaryPath = "/native/bin/cl.exe"; - EXPECT_EQ(emit_ninja_string(plan).find("coff_args ="), std::string::npos); -} + expect(eq(emit_ninja_string(plan).find("coff_args ="), std::string::npos)); +}}; -TEST(NinjaBackend, OrdinaryCompilersShareTheDetachedScheduleBound) { +UTest ordinary_compilers_share_the_detached_schedule_bound{"NinjaBackend/OrdinaryCompilersShareTheDetachedScheduleBound", [] { auto plan = minimal_plan(); plan.toolchain.version = "16.1.0"; plan.scheduleTag = "detach-codegen"; @@ -137,22 +139,22 @@ TEST(NinjaBackend, OrdinaryCompilersShareTheDetachedScheduleBound) { const auto text = emit_ninja_string(plan); auto rule = [&](std::string_view name) { const auto start = text.find("rule " + std::string(name) + "\n"); - EXPECT_NE(start, std::string::npos) << name; + expect(ne(start, std::string::npos)) << name; return start == std::string::npos ? std::string{} : text.substr(start, text.find("\n\n", start) - start); }; for (const auto name : {"cxx_module", "cxx_object", "c_object", "asm_object", "asm_object_raw", "nasm_object", "cxx_scan"}) - EXPECT_NE(rule(name).find("__compiler-cap --sem .mcpp-sched --output $out --depfile $out.d -- "), std::string::npos) << name; + expect(ne(rule(name).find("__compiler-cap --sem .mcpp-sched --output $out --depfile $out.d -- "), std::string::npos)) << name; // The generation carries the cap: the graph names no number of compilers, // so it is the same whatever the job count (e2e 861). - EXPECT_EQ(text.find("--cap"), std::string::npos); - EXPECT_EQ(text.find("sched_cap"), std::string::npos); - EXPECT_EQ(rule("cxx_module_bmi").find("__compiler-cap"), std::string::npos) + expect(eq(text.find("--cap"), std::string::npos)); + expect(eq(text.find("sched_cap"), std::string::npos)); + expect(eq(rule("cxx_module_bmi").find("__compiler-cap"), std::string::npos)) << "the BMI supervisor already holds a permit"; - EXPECT_EQ(rule("cxx_module_obj").find("__compiler-cap"), std::string::npos) + expect(eq(rule("cxx_module_obj").find("__compiler-cap"), std::string::npos)) << "a join must not wait for a second permit"; -} +}}; -TEST(NinjaBackend, ObjectiveCSourceUsesCObjectRuleAndCFlags) { +UTest objective_c_source_uses_c_object_rule_and_c_flags{"NinjaBackend/ObjectiveCSourceUsesCObjectRuleAndCFlags", [] { auto plan = minimal_plan(); plan.compileUnits.push_back({ .source = "src/cocoa.m", @@ -165,15 +167,15 @@ TEST(NinjaBackend, ObjectiveCSourceUsesCObjectRuleAndCFlags) { auto ninja = emit_ninja_string(plan); - EXPECT_NE(ninja.find("build obj/cocoa.o : c_object src/cocoa.m"), std::string::npos) + expect(ne(ninja.find("build obj/cocoa.o : c_object src/cocoa.m"), std::string::npos)) << ninja; - EXPECT_EQ(ninja.find("build obj/cocoa.o : cxx_object src/cocoa.m"), std::string::npos) + expect(eq(ninja.find("build obj/cocoa.o : cxx_object src/cocoa.m"), std::string::npos)) << ninja; - EXPECT_NE(ninja.find("unit_cflags = -DOBJ_C_BUILD=1"), std::string::npos) + expect(ne(ninja.find("unit_cflags = -DOBJ_C_BUILD=1"), std::string::npos)) << ninja; - EXPECT_EQ(ninja.find("unit_cxxflags = -DWRONG_CXX_FLAG=1"), std::string::npos) + expect(eq(ninja.find("unit_cxxflags = -DWRONG_CXX_FLAG=1"), std::string::npos)) << ninja; -} +}}; // mcpp#235: cxx_module/cxx_object must track header/purview/GMF includes via // a GNU-style depfile on non-MSVC toolchains (this test's plan uses GCC on a @@ -185,7 +187,7 @@ TEST(NinjaBackend, ObjectiveCSourceUsesCObjectRuleAndCFlags) { // GCC's `-fmodules` bolts non-standard "reversed" module rules onto the raw // -MMD output that ninja's depfile loader rejects — see the long comment at // the definition site for the empirically-confirmed failure mode. -TEST(NinjaBackend, CxxModuleAndCxxObjectRulesTrackHeaderDepsViaGccDepfile) { +UTest cxx_module_and_cxx_object_rules_track_header_deps_via_gcc_depfile{"NinjaBackend/CxxModuleAndCxxObjectRulesTrackHeaderDepsViaGccDepfile", [] { // Every host (the 2026-09-28 design, WS2): whether a unit emits a depfile // is the compiler's property. What differs by host is how GCC's filtered // form arrives: the awk program in the POSIX rule's shell command, and @@ -197,32 +199,31 @@ TEST(NinjaBackend, CxxModuleAndCxxObjectRulesTrackHeaderDepsViaGccDepfile) { auto module_rule_start = ninja.find("rule cxx_module"); auto object_rule_start = ninja.find("rule cxx_object"); - ASSERT_NE(module_rule_start, std::string::npos) << ninja; - ASSERT_NE(object_rule_start, std::string::npos) << ninja; - ASSERT_LT(module_rule_start, object_rule_start) << ninja; + expect(fatal(ne(module_rule_start, std::string::npos))) << ninja; + expect(fatal(ne(object_rule_start, std::string::npos))) << ninja; + expect(fatal(lt(module_rule_start, object_rule_start))) << ninja; auto module_rule = ninja.substr(module_rule_start, object_rule_start - module_rule_start); auto object_rule = ninja.substr(object_rule_start); for (auto const& rule : {module_rule, object_rule}) { - EXPECT_NE(rule.find("-MMD -MF $out.d.raw"), std::string::npos) << ninja; - EXPECT_NE(rule.find("deps = gcc"), std::string::npos) << ninja; - EXPECT_NE(rule.find("depfile = $out.d\n"), std::string::npos) << ninja; + expect(ne(rule.find("-MMD -MF $out.d.raw"), std::string::npos)) << ninja; + expect(ne(rule.find("deps = gcc"), std::string::npos)) << ninja; + expect(ne(rule.find("depfile = $out.d\n"), std::string::npos)) << ninja; // The raw compiler depfile (with GCC's module-specific reversed // rules) must never be bound directly as ninja's depfile. - EXPECT_EQ(rule.find("depfile = $out.d.raw"), std::string::npos) << ninja; + expect(eq(rule.find("depfile = $out.d.raw"), std::string::npos)) << ninja; if constexpr (mcpplibs::os::is_windows) { - EXPECT_NE(rule.find("$mcpp depfile-filter --raw $out.d.raw --out $out.d -- "), - std::string::npos) << ninja; - EXPECT_EQ(rule.find("awk"), std::string::npos) << ninja; + expect(ne(rule.find("$mcpp depfile-filter --raw $out.d.raw --out $out.d -- "), std::string::npos)) << ninja; + expect(eq(rule.find("awk"), std::string::npos)) << ninja; } else { - EXPECT_NE(rule.find("awk"), std::string::npos) << ninja; - EXPECT_EQ(rule.find("depfile-filter"), std::string::npos) << ninja; + expect(ne(rule.find("awk"), std::string::npos)) << ninja; + expect(eq(rule.find("depfile-filter"), std::string::npos)) << ninja; } } -} +}}; -TEST(NinjaBackend, UsesPackageCppStandardForCxxFlags) { +UTest uses_package_cpp_standard_for_cxx_flags{"NinjaBackend/UsesPackageCppStandardForCxxFlags", [] { auto plan = minimal_plan(); plan.manifest.package.standard = "c++26"; plan.manifest.language.standard = "c++26"; @@ -237,13 +238,13 @@ TEST(NinjaBackend, UsesPackageCppStandardForCxxFlags) { auto ninja = emit_ninja_string(plan); - EXPECT_NE(ninja.find("cxxflags = -std=c++26"), std::string::npos) + expect(ne(ninja.find("cxxflags = -std=c++26"), std::string::npos)) << ninja; - EXPECT_EQ(ninja.find("-std=c++23"), std::string::npos) + expect(eq(ninja.find("-std=c++23"), std::string::npos)) << ninja; -} +}}; -TEST(NinjaBackend, CompileCommandsUsesSameCppStandard) { +UTest compile_commands_uses_same_cpp_standard{"NinjaBackend/CompileCommandsUsesSameCppStandard", [] { auto plan = minimal_plan(); plan.manifest.package.standard = "c++26"; plan.manifest.language.standard = "c++26"; @@ -259,11 +260,11 @@ TEST(NinjaBackend, CompileCommandsUsesSameCppStandard) { auto flags = compute_flags(plan); auto cdb = emit_compile_commands(plan, flags); - EXPECT_NE(cdb.find("\"-std=c++26\""), std::string::npos) + expect(ne(cdb.find("\"-std=c++26\""), std::string::npos)) << cdb; - EXPECT_EQ(cdb.find("\"-std=c++23\""), std::string::npos) + expect(eq(cdb.find("\"-std=c++23\""), std::string::npos)) << cdb; -} +}}; // #690 (design record F7): the root's `[build] include_dirs` and // `include_dirs_after` are its PRIVATE build requirement. They reach the root's @@ -273,7 +274,7 @@ TEST(NinjaBackend, CompileCommandsUsesSameCppStandard) { // were appended to all four strings, so a root header named like a system or // dependency header shadowed it inside the dependency, and a dependency cache // entry could be compiled against another project's root headers. -TEST(NinjaBackend, FileLevelFlagsCarryNoRootIncludeDirectory) { +UTest file_level_flags_carry_no_root_include_directory{"NinjaBackend/FileLevelFlagsCarryNoRootIncludeDirectory", [] { auto plan = minimal_plan(); plan.nasmPath = "/usr/bin/nasm"; plan.manifest.buildConfig.includeDirs = {"include", "third_party/imgui"}; @@ -283,12 +284,11 @@ TEST(NinjaBackend, FileLevelFlagsCarryNoRootIncludeDirectory) { for (auto const* channel : {&flags.cxx, &flags.cc, &flags.as, &flags.nasm}) { for (auto const* dir : {"include", "imgui", "generated"}) { - EXPECT_EQ(channel->find(dir), std::string::npos) - << "root include directory '" << dir + if (auto&& failed = expect(eq(channel->find(dir), std::string::npos)); !failed) failed << "root include directory '" << dir << "' reached a file-level flag string: " << *channel; } } -} +}}; // What the file-level channel used to guarantee now holds on the per-unit // one: a relative `[build] include_dirs` entry is absolutised against the @@ -296,16 +296,16 @@ TEST(NinjaBackend, FileLevelFlagsCarryNoRootIncludeDirectory) { // directory (#390; the Cluster A review fix of #226/#234, whose failure shape // was a relative `/Iinclude` under the MSVC dialect that stopped resolving // once ninja ran from the output directory). -TEST(NinjaBackend, RootIncludeEntriesAreAbsolutisedForTheUnitChannel) { +UTest root_include_entries_are_absolutised_for_the_unit_channel{"NinjaBackend/RootIncludeEntriesAreAbsolutisedForTheUnitChannel", [] { auto plan = minimal_plan(); auto dirs = mcpp::build::expand_manifest_include_entry( plan.projectRoot, "third_party/imgui"); - ASSERT_EQ(dirs.size(), 1u); + expect(fatal(eq(dirs.size(), 1u))); auto expected = plan.projectRoot / "third_party" / "imgui"; expected.make_preferred(); - EXPECT_EQ(dirs.front(), expected); - EXPECT_TRUE(dirs.front().has_root_path()) << dirs.front(); -} + expect(eq(dirs.front(), expected)); + expect(dirs.front().has_root_path()) << dirs.front(); +}}; // #249: a compile unit's localIncludeDirsAfter emit as -idirafter into the // same $local_includes variable, APPENDED after the -I entries. -idirafter @@ -315,7 +315,7 @@ TEST(NinjaBackend, RootIncludeEntriesAreAbsolutisedForTheUnitChannel) { // while the dep's real headers stay findable. The flag's semantics — not // its position — carry the priority; the -I-then-idirafter ordering is for // readability. -TEST(NinjaBackend, LocalIncludeDirsAfterEmitIdirafterAppendedAfterDashI) { +UTest local_include_dirs_after_emit_idirafter_appended_after_dash_i{"NinjaBackend/LocalIncludeDirsAfterEmitIdirafterAppendedAfterDashI", [] { auto plan = minimal_plan(); plan.compileUnits.push_back({ .source = "src/main.cpp", @@ -329,24 +329,24 @@ TEST(NinjaBackend, LocalIncludeDirsAfterEmitIdirafterAppendedAfterDashI) { auto ninja = emit_ninja_string(plan); auto line_start = ninja.find("local_includes ="); - ASSERT_NE(line_start, std::string::npos) << ninja; + expect(fatal(ne(line_start, std::string::npos))) << ninja; auto line_end = ninja.find('\n', line_start); auto line = ninja.substr(line_start, line_end - line_start); auto i_pos = line.find("-I/dep/include"); auto after_pos = line.find("-idirafter/dep/tarball-root"); - ASSERT_NE(i_pos, std::string::npos) << line; - ASSERT_NE(after_pos, std::string::npos) << line; + expect(fatal(ne(i_pos, std::string::npos))) << line; + expect(fatal(ne(after_pos, std::string::npos))) << line; // After-dirs come after the -I entries within $local_includes. - EXPECT_LT(i_pos, after_pos) << line; + expect(lt(i_pos, after_pos)) << line; // Never upgraded to -I. - EXPECT_EQ(line.find("-I/dep/tarball-root"), std::string::npos) << line; -} + expect(eq(line.find("-I/dep/tarball-root"), std::string::npos)) << line; +}}; // #249 MSVC degradation: cl.exe has no -idirafter, so under the msvc // dialect after-dirs are emitted as regular /I at the END of the include // list (clang targeting MSVC uses the gnu dialect and gets -idirafter). -TEST(NinjaBackend, MsvcDialectEmitsIncludeDirsAfterAsTrailingSlashI) { +UTest msvc_dialect_emits_include_dirs_after_as_trailing_slash_i{"NinjaBackend/MsvcDialectEmitsIncludeDirsAfterAsTrailingSlashI", [] { auto plan = minimal_plan(); plan.toolchain.compiler = mcpp::toolchain::CompilerId::MSVC; plan.toolchain.binaryPath = "cl.exe"; @@ -363,7 +363,7 @@ TEST(NinjaBackend, MsvcDialectEmitsIncludeDirsAfterAsTrailingSlashI) { auto ninja = emit_ninja_string(plan); auto line_start = ninja.find("local_includes ="); - ASSERT_NE(line_start, std::string::npos) << ninja; + expect(fatal(ne(line_start, std::string::npos))) << ninja; auto line_end = ninja.find('\n', line_start); auto line = ninja.substr(line_start, line_end - line_start); @@ -373,19 +373,19 @@ TEST(NinjaBackend, MsvcDialectEmitsIncludeDirsAfterAsTrailingSlashI) { // the dialect on one of them. Converging both on include_token fixed it. auto i_pos = line.find("/I/dep/include"); auto after_pos = line.find("/I/dep/tarball-root"); - ASSERT_NE(i_pos, std::string::npos) << line; - ASSERT_NE(after_pos, std::string::npos) << line; - EXPECT_LT(i_pos, after_pos) << line; - EXPECT_EQ(line.find("-idirafter"), std::string::npos) << line; - EXPECT_EQ(line.find("-I/dep"), std::string::npos) << line; -} + expect(fatal(ne(i_pos, std::string::npos))) << line; + expect(fatal(ne(after_pos, std::string::npos))) << line; + expect(lt(i_pos, after_pos)) << line; + expect(eq(line.find("-idirafter"), std::string::npos)) << line; + expect(eq(line.find("-I/dep"), std::string::npos)) << line; +}}; // #331: the per-TU include channel applied only ninja's `$` escaping, while // the global channel in flags.cppm shell-quoted. Same manifest include_dirs, // two derivations — so a directory with a space in it survived one path and // split into separate shell words on the other, which is what every Windows // user hits the moment a dependency lands under `C:\Program Files`. -TEST(NinjaBackend, LocalIncludeDirsWithSpacesAreShellQuoted) { +UTest local_include_dirs_with_spaces_are_shell_quoted{"NinjaBackend/LocalIncludeDirsWithSpacesAreShellQuoted", [] { auto plan = minimal_plan(); plan.compileUnits.push_back({ .source = "src/main.cpp", @@ -398,7 +398,7 @@ TEST(NinjaBackend, LocalIncludeDirsWithSpacesAreShellQuoted) { auto ninja = emit_ninja_string(plan); auto line_start = ninja.find("local_includes ="); - ASSERT_NE(line_start, std::string::npos) << ninja; + expect(fatal(ne(line_start, std::string::npos))) << ninja; auto line = ninja.substr(line_start, ninja.find('\n', line_start) - line_start); // Two escaping layers, in order: ninja's (`$ ` for a literal space, so @@ -412,9 +412,9 @@ TEST(NinjaBackend, LocalIncludeDirsWithSpacesAreShellQuoted) { // wants single quotes, cmd.exe double. Hardcoding `'` passed on Linux // and failed on the Windows runner for a difference that is correct. const std::string q = mcpplibs::os::is_windows ? "\"" : "'"; - EXPECT_NE(line.find(q + "-I/opt/my$ dep/include" + q), std::string::npos) << line; - EXPECT_NE(line.find(q + "-idirafter/opt/my$ dep/after" + q), std::string::npos) << line; -} + expect(ne(line.find(q + "-I/opt/my$ dep/include" + q), std::string::npos)) << line; + expect(ne(line.find(q + "-idirafter/opt/my$ dep/after" + q), std::string::npos)) << line; +}}; // #261: on Windows $local_includes is copied into a RESPONSE FILE, which the // drivers tokenize GNU-style — a backslash is an escape character there, and @@ -427,19 +427,19 @@ TEST(NinjaBackend, LocalIncludeDirsWithSpacesAreShellQuoted) { // assertion cannot be made from a Linux host. Guarded rather than weakened — // a test that passes for the wrong reason everywhere is worse than one that // says where it applies. Windows CI is the enforcement point. -TEST(NinjaBackend, LocalIncludeTokensUseGenericSeparators) { +UTest local_include_tokens_use_generic_separators{"NinjaBackend/LocalIncludeTokensUseGenericSeparators", [] { const auto& gnu = mcpp::toolchain::gnu_dialect(); auto sub = std::filesystem::path("src") / "inc"; auto tok = mcpp::build::include_token(gnu, sub, {}, mcpp::build::PathForm::Generic); // True on every host: the generic form never uses the native separator. - EXPECT_NE(tok.find("src/inc"), std::string::npos) << tok; + expect(ne(tok.find("src/inc"), std::string::npos)) << tok; if constexpr (mcpplibs::os::is_windows) { auto abs = mcpp::build::include_token( gnu, std::filesystem::path("C:\\src\\inc"), {}, mcpp::build::PathForm::Generic); - EXPECT_EQ(abs.find('\\'), std::string::npos) << abs; + expect(eq(abs.find('\\'), std::string::npos)) << abs; // The command-line channel keeps native separators — a backslash is // just a character there, and rewriting those paths would be a change @@ -447,15 +447,15 @@ TEST(NinjaBackend, LocalIncludeTokensUseGenericSeparators) { auto native = mcpp::build::include_token( gnu, std::filesystem::path("C:\\src\\inc"), {}, mcpp::build::PathForm::Native); - EXPECT_NE(native.find('\\'), std::string::npos) << native; + expect(ne(native.find('\\'), std::string::npos)) << native; } -} +}}; // #249 NASM degradation: nasm_object edges share $local_includes, but NASM // would parse `-idirafter

` as its `-i` option with value `dirafter

` — // a silently wrong search dir. Only the C/C++ frontends have a system-header // chain to protect, so a nasm unit's after-dirs degrade to plain -I. -TEST(NinjaBackend, NasmUnitEmitsIncludeDirsAfterAsPlainDashI) { +UTest nasm_unit_emits_include_dirs_after_as_plain_dash_i{"NinjaBackend/NasmUnitEmitsIncludeDirsAfterAsPlainDashI", [] { auto plan = minimal_plan(); plan.nasmPath = "/usr/bin/nasm"; plan.compileUnits.push_back({ @@ -470,19 +470,19 @@ TEST(NinjaBackend, NasmUnitEmitsIncludeDirsAfterAsPlainDashI) { auto ninja = emit_ninja_string(plan); auto edge_start = ninja.find("build obj/scale.asm.o : nasm_object"); - ASSERT_NE(edge_start, std::string::npos) << ninja; + expect(fatal(ne(edge_start, std::string::npos))) << ninja; auto line_start = ninja.find("local_includes =", edge_start); - ASSERT_NE(line_start, std::string::npos) << ninja; + expect(fatal(ne(line_start, std::string::npos))) << ninja; auto line = ninja.substr(line_start, ninja.find('\n', line_start) - line_start); - EXPECT_NE(line.find("-I/dep/x86"), std::string::npos) << line; - EXPECT_NE(line.find("-I/dep/tarball-root"), std::string::npos) << line; - EXPECT_EQ(line.find("-idirafter"), std::string::npos) << line; -} + expect(ne(line.find("-I/dep/x86"), std::string::npos)) << line; + expect(ne(line.find("-I/dep/tarball-root"), std::string::npos)) << line; + expect(eq(line.find("-idirafter"), std::string::npos)) << line; +}}; // ── assembly sources (.S/.s → asm_object via $cc, .asm → nasm_object) ──────── -TEST(NinjaBackend, GasSourceUsesAsmObjectRule) { +UTest gas_source_uses_asm_object_rule{"NinjaBackend/GasSourceUsesAsmObjectRule", [] { auto plan = minimal_plan(); plan.compileUnits.push_back({ .source = "src/copy.S", @@ -497,23 +497,22 @@ TEST(NinjaBackend, GasSourceUsesAsmObjectRule) { auto ninja = emit_ninja_string(plan); - EXPECT_NE(ninja.find("rule asm_object"), std::string::npos) << ninja; - EXPECT_NE(ninja.find("build obj/copy.S.o : asm_object src/copy.S"), - std::string::npos) << ninja; - EXPECT_NE(ninja.find("cc = "), std::string::npos) << ninja; - EXPECT_NE(ninja.find("unit_asmflags = -DHAVE_ASM=1\n"), std::string::npos) + expect(ne(ninja.find("rule asm_object"), std::string::npos)) << ninja; + expect(ne(ninja.find("build obj/copy.S.o : asm_object src/copy.S"), std::string::npos)) << ninja; + expect(ne(ninja.find("cc = "), std::string::npos)) << ninja; + expect(ne(ninja.find("unit_asmflags = -DHAVE_ASM=1\n"), std::string::npos)) << ninja; - EXPECT_EQ(ninja.find("unit_asmflags = -DHAVE_ASM=1 -std=c99"), std::string::npos) + expect(eq(ninja.find("unit_asmflags = -DHAVE_ASM=1 -std=c99"), std::string::npos)) << ninja; // The global asm flag string must not carry a C standard or opt level. auto asmline_pos = ninja.find("asmflags ="); - ASSERT_NE(asmline_pos, std::string::npos) << ninja; + expect(fatal(ne(asmline_pos, std::string::npos))) << ninja; auto asmline = ninja.substr(asmline_pos, ninja.find('\n', asmline_pos) - asmline_pos); - EXPECT_EQ(asmline.find("-std="), std::string::npos) << asmline; - EXPECT_EQ(asmline.find("-O"), std::string::npos) << asmline; -} + expect(eq(asmline.find("-std="), std::string::npos)) << asmline; + expect(eq(asmline.find("-O"), std::string::npos)) << asmline; +}}; -TEST(NinjaBackend, NasmSourceUsesNasmRuleWithDerivedFormat) { +UTest nasm_source_uses_nasm_rule_with_derived_format{"NinjaBackend/NasmSourceUsesNasmRuleWithDerivedFormat", [] { auto plan = minimal_plan(); plan.nasmPath = "/opt/bin/nasm"; plan.nasmFormat = "elf64"; @@ -527,16 +526,15 @@ TEST(NinjaBackend, NasmSourceUsesNasmRuleWithDerivedFormat) { auto ninja = emit_ninja_string(plan); - EXPECT_NE(ninja.find("rule nasm_object"), std::string::npos) << ninja; - EXPECT_NE(ninja.find("nasm = /opt/bin/nasm"), std::string::npos) << ninja; - EXPECT_NE(ninja.find("nasmfmt = elf64"), std::string::npos) << ninja; - EXPECT_NE(ninja.find("build obj/simd.asm.o : nasm_object src/simd.asm"), - std::string::npos) << ninja; - EXPECT_NE(ninja.find("unit_asmflags = -DHAVE_AVX2=1\n"), std::string::npos) + expect(ne(ninja.find("rule nasm_object"), std::string::npos)) << ninja; + expect(ne(ninja.find("nasm = /opt/bin/nasm"), std::string::npos)) << ninja; + expect(ne(ninja.find("nasmfmt = elf64"), std::string::npos)) << ninja; + expect(ne(ninja.find("build obj/simd.asm.o : nasm_object src/simd.asm"), std::string::npos)) << ninja; + expect(ne(ninja.find("unit_asmflags = -DHAVE_AVX2=1\n"), std::string::npos)) << ninja; -} +}}; -TEST(NinjaBackend, NoAsmRulesWithoutAsmSources) { +UTest no_asm_rules_without_asm_sources{"NinjaBackend/NoAsmRulesWithoutAsmSources", [] { auto plan = minimal_plan(); plan.compileUnits.push_back({ .source = "src/main.cpp", @@ -547,11 +545,11 @@ TEST(NinjaBackend, NoAsmRulesWithoutAsmSources) { auto ninja = emit_ninja_string(plan); - EXPECT_EQ(ninja.find("rule asm_object"), std::string::npos) << ninja; - EXPECT_EQ(ninja.find("rule nasm_object"), std::string::npos) << ninja; -} + expect(eq(ninja.find("rule asm_object"), std::string::npos)) << ninja; + expect(eq(ninja.find("rule nasm_object"), std::string::npos)) << ninja; +}}; -TEST(NinjaBackend, CompileCommandsSkipNasmAndCoverGas) { +UTest compile_commands_skip_nasm_and_cover_gas{"NinjaBackend/CompileCommandsSkipNasmAndCoverGas", [] { auto plan = minimal_plan(); plan.nasmPath = "/opt/bin/nasm"; plan.nasmFormat = "elf64"; @@ -572,11 +570,11 @@ TEST(NinjaBackend, CompileCommandsSkipNasmAndCoverGas) { auto cdb = emit_compile_commands(plan, flags); // NASM command lines are meaningless to CDB consumers (clangd) — excluded. - EXPECT_EQ(cdb.find("simd.asm"), std::string::npos) << cdb; + expect(eq(cdb.find("simd.asm"), std::string::npos)) << cdb; // GAS units ride the C driver and stay in the CDB. - EXPECT_NE(cdb.find("copy.S"), std::string::npos) << cdb; - EXPECT_EQ(cdb.find("\"-std=c11\""), std::string::npos) << cdb; // asm-safe flags, no C std -} + expect(ne(cdb.find("copy.S"), std::string::npos)) << cdb; + expect(eq(cdb.find("\"-std=c11\""), std::string::npos)) << cdb; // asm-safe flags, no C std +}}; // mcpp#234: each packageCflags/packageCxxflags element is already one argv // token — apply_glob_flags pushes a define like `T=long long` as the single @@ -585,7 +583,7 @@ TEST(NinjaBackend, CompileCommandsSkipNasmAndCoverGas) { // it to the shell, the embedded space split `-DT=long long` into TWO words // (`-DT=long` and a bare `long`). The emitted unit_cflags line must carry the // define as a single shell-quoted token. -TEST(NinjaBackend, QuotesFlagValueWithSpace) { +UTest quotes_flag_value_with_space{"NinjaBackend/QuotesFlagValueWithSpace", [] { auto plan = minimal_plan(); plan.compileUnits.push_back({ .source = "src/main.c", @@ -602,15 +600,15 @@ TEST(NinjaBackend, QuotesFlagValueWithSpace) { const std::string quoted = mcpplibs::os::is_windows ? "unit_cflags = \"-DT=long long\"" : "unit_cflags = '-DT=long long'"; - EXPECT_NE(ninja.find(quoted), std::string::npos) << ninja; + expect(ne(ninja.find(quoted), std::string::npos)) << ninja; // Must NOT appear as two bare, unquoted words split on the space. - EXPECT_EQ(ninja.find("unit_cflags = -DT=long long"), std::string::npos) + expect(eq(ninja.find("unit_cflags = -DT=long long"), std::string::npos)) << ninja; -} +}}; // Plain framework-shaped flags with nothing shell-significant must pass // through byte-for-byte unquoted (no over-quoting regression). -TEST(NinjaBackend, PlainFlagsPassThroughUnquoted) { +UTest plain_flags_pass_through_unquoted{"NinjaBackend/PlainFlagsPassThroughUnquoted", [] { auto plan = minimal_plan(); plan.compileUnits.push_back({ .source = "src/main.cpp", @@ -622,9 +620,9 @@ TEST(NinjaBackend, PlainFlagsPassThroughUnquoted) { auto ninja = emit_ninja_string(plan); - EXPECT_NE(ninja.find("unit_cxxflags = -DFOO=1 -O2"), std::string::npos) + expect(ne(ninja.find("unit_cxxflags = -DFOO=1 -O2"), std::string::npos)) << ninja; -} +}}; // The SubOS farm must reach DT_RPATH AFTER the artifact's own directory. // @@ -637,9 +635,9 @@ TEST(NinjaBackend, PlainFlagsPassThroughUnquoted) { // The assertion is on the EFFECTIVE line (global then unit, exactly as the // rule expands it), not on either variable alone: checking one of them is what // made the original mistake invisible. -TEST(NinjaBackend, SubosFarmRpathFollowsTheArtifactsOwnDirectory) { +UTest subos_farm_rpath_follows_the_artifacts_own_directory{"NinjaBackend/SubosFarmRpathFollowsTheArtifactsOwnDirectory", [] { if constexpr (!mcpplibs::os::is_linux) - GTEST_SKIP() << "the SubOS farm rpath is emitted for ELF targets only"; + skip("the SubOS farm rpath is emitted for ELF targets only"); const std::string farm = "/tmp/mcpp-ninja-test-farm/subos/default/lib"; auto plan = minimal_plan(); @@ -675,22 +673,22 @@ TEST(NinjaBackend, SubosFarmRpathFollowsTheArtifactsOwnDirectory) { auto origin = effective.find("$$ORIGIN"); auto fallback = effective.find(farm); - ASSERT_NE(origin, std::string::npos) << effective; - ASSERT_NE(fallback, std::string::npos) << effective; - EXPECT_LT(origin, fallback) + expect(fatal(ne(origin, std::string::npos))) << effective; + expect(fatal(ne(fallback, std::string::npos))) << effective; + expect(lt(origin, fallback)) << "the SubOS farm outranks $ORIGIN, so the artifact can load a " "different build of a library than it linked against:\n" << effective; // And it must have left the global channel entirely — leaving a copy there // would restore the old order no matter what the unit tail says. - EXPECT_EQ(line_after("\nldflags =").find(farm), std::string::npos) << ninja; -} + expect(eq(line_after("\nldflags =").find(farm), std::string::npos)) << ninja; +}}; // An archive is produced by `ar`, whose rule never expands `$unit_ldflags`. // Emitting run-time search flags there would be dead bytes in every graph. -TEST(NinjaBackend, StaticLibraryCarriesNoRuntimeFallback) { +UTest static_library_carries_no_runtime_fallback{"NinjaBackend/StaticLibraryCarriesNoRuntimeFallback", [] { if constexpr (!mcpplibs::os::is_linux) - GTEST_SKIP() << "the SubOS farm rpath is emitted for ELF targets only"; + skip("the SubOS farm rpath is emitted for ELF targets only"); const std::string farm = "/tmp/mcpp-ninja-test-farm/subos/default/lib"; auto plan = minimal_plan(); @@ -711,10 +709,10 @@ TEST(NinjaBackend, StaticLibraryCarriesNoRuntimeFallback) { auto ninja = emit_ninja_string(plan); auto at = ninja.find("build lib/libgreet.a"); - ASSERT_NE(at, std::string::npos) << ninja; + expect(fatal(ne(at, std::string::npos))) << ninja; auto stanza = ninja.substr(at, ninja.find("\nbuild ", at + 1) - at); - EXPECT_EQ(stanza.find(farm), std::string::npos) << stanza; -} + expect(eq(stanza.find(farm), std::string::npos)) << stanza; +}}; // Regression: mcpp-GENERATED per-unit LINK flags are already correctly // shell-quoted + ninja-escaped at construction — e.g. the shared-dep rpath @@ -723,7 +721,7 @@ TEST(NinjaBackend, StaticLibraryCarriesNoRuntimeFallback) { // shell_quote_arg over them: doing so double-quotes the token, baking a // literal `'$ORIGIN'` (quotes included) into the binary's RUNPATH so the // dynamic linker can't find dependency .so's next to the exe (e2e 55-57/64). -TEST(NinjaBackend, LinkFlagsAreNotReQuoted) { +UTest link_flags_are_not_re_quoted{"NinjaBackend/LinkFlagsAreNotReQuoted", [] { auto plan = minimal_plan(); plan.compileUnits.push_back({ .source = "src/main.cpp", @@ -743,11 +741,11 @@ TEST(NinjaBackend, LinkFlagsAreNotReQuoted) { auto ninja = emit_ninja_string(plan); // The rpath token passes through verbatim (ninja `$$` = literal `$`). - EXPECT_NE(ninja.find("-Wl,-rpath,'$$ORIGIN'"), std::string::npos) << ninja; + expect(ne(ninja.find("-Wl,-rpath,'$$ORIGIN'"), std::string::npos)) << ninja; // Must NOT be double-quoted: shell_quote_arg escaping an embedded `'` // produces the `'\''` sequence, which only appears if it re-quoted. - EXPECT_EQ(ninja.find("'\\''"), std::string::npos) << ninja; -} + expect(eq(ninja.find("'\\''"), std::string::npos)) << ninja; +}}; // Regression (#234 follow-up): a raw descriptor flag that packs two argv // tokens into one string — e.g. compat.lua's `-include

` — must pass @@ -756,7 +754,7 @@ TEST(NinjaBackend, LinkFlagsAreNotReQuoted) { // file literally named "header" → "No such file" → aarch64/macos/windows // cross-builds failed. Only `-D`/`/D` define tokens with an intra-value space // get quoted; `-include foo.h` does not. -TEST(NinjaBackend, RawMultiTokenFlagIsNotQuoted) { +UTest raw_multi_token_flag_is_not_quoted{"NinjaBackend/RawMultiTokenFlagIsNotQuoted", [] { auto plan = minimal_plan(); plan.compileUnits.push_back({ .source = "src/main.c", @@ -769,12 +767,10 @@ TEST(NinjaBackend, RawMultiTokenFlagIsNotQuoted) { auto ninja = emit_ninja_string(plan); // Verbatim, so the shell re-splits into two args. - EXPECT_NE(ninja.find("unit_cflags = -include mcpp_lua_platform_config.h"), - std::string::npos) << ninja; + expect(ne(ninja.find("unit_cflags = -include mcpp_lua_platform_config.h"), std::string::npos)) << ninja; // Must NOT be wrapped in quotes (which would make it one malformed arg). - EXPECT_EQ(ninja.find("'-include mcpp_lua_platform_config.h'"), - std::string::npos) << ninja; -} + expect(eq(ninja.find("'-include mcpp_lua_platform_config.h'"), std::string::npos)) << ninja; +}}; // mcpp#247 + #344: link/archive/shared route the object list through a response // file on EVERY platform. The number of objects on one link edge is unbounded — @@ -792,7 +788,7 @@ TEST(NinjaBackend, RawMultiTokenFlagIsNotQuoted) { // object directories took it to 161 687 — at which point ninja dies with // `posix_spawn: Argument list too long`, naming no edge and no cause. A ceiling // nothing watches is not a ceiling anyone can stay under, so there isn't one now. -TEST(NinjaBackend, DriverStyleLinkRulesAlwaysUseRspfile) { +UTest driver_style_link_rules_always_use_rspfile{"NinjaBackend/DriverStyleLinkRulesAlwaysUseRspfile", [] { auto plan = minimal_plan(); // GCC → gnu dialect → driver-style branch auto ninja = emit_ninja_string(plan); @@ -800,20 +796,20 @@ TEST(NinjaBackend, DriverStyleLinkRulesAlwaysUseRspfile) { for (std::string_view rule : {"rule cxx_link\n", "rule cxx_archive\n", "rule cxx_shared\n"}) { auto start = ninja.find(rule); - ASSERT_NE(start, std::string::npos) << ninja; + expect(fatal(ne(start, std::string::npos))) << ninja; auto end = ninja.find("\n\n", start); - ASSERT_NE(end, std::string::npos) << ninja; + expect(fatal(ne(end, std::string::npos))) << ninja; auto body = ninja.substr(start, end - start); - EXPECT_NE(body.find("@$out.rsp"), std::string::npos) << body; - EXPECT_NE(body.find("rspfile = $out.rsp"), std::string::npos) << body; - EXPECT_NE(body.find("rspfile_content = $in"), std::string::npos) << body; + expect(ne(body.find("@$out.rsp"), std::string::npos)) << body; + expect(ne(body.find("rspfile = $out.rsp"), std::string::npos)) << body; + expect(ne(body.find("rspfile_content = $in"), std::string::npos)) << body; // And the object list must no longer be inlined into the command: that // is the whole point, so `$in` may appear only as rspfile_content. - EXPECT_EQ(count_occurrences(body, "$in"), 1u) << body; + expect(eq(count_occurrences(body, "$in"), 1u)) << body; } -} +}}; -TEST(NinjaBackend, RootPackageCxxflagsAreEmittedOncePerUnit) { +UTest root_package_cxxflags_are_emitted_once_per_unit{"NinjaBackend/RootPackageCxxflagsAreEmittedOncePerUnit", [] { auto plan = minimal_plan(); plan.manifest.buildConfig.cxxflags = {"-DROOT_FLAG=1"}; plan.compileUnits.push_back({ @@ -826,18 +822,18 @@ TEST(NinjaBackend, RootPackageCxxflagsAreEmittedOncePerUnit) { auto ninja = emit_ninja_string(plan); - EXPECT_EQ(count_occurrences(ninja, "unit_cxxflags = -DROOT_FLAG=1"), 2u) + expect(eq(count_occurrences(ninja, "unit_cxxflags = -DROOT_FLAG=1"), 2u)) << ninja; - EXPECT_EQ(ninja.find("cxxflags = -std=c++23 -O2 -DROOT_FLAG=1"), std::string::npos) + expect(eq(ninja.find("cxxflags = -std=c++23 -O2 -DROOT_FLAG=1"), std::string::npos)) << ninja; -} +}}; // mcpp#261: the clang scan rule used shell redirection (`> $out`), which on // Windows forced a `cmd /c` wrapper and with it cmd.exe's 8191-char command // line ceiling — a quarter of what ninja's CreateProcess path allows. Any // package with a large include-dir list overran it. clang-scan-deps has -o // (LLVM 17+), so the redirect and the wrapper are both unnecessary. -TEST(NinjaBackend, ClangScanRuleWritesViaDashOWithoutShellRedirection) { +UTest clang_scan_rule_writes_via_dash_o_without_shell_redirection{"NinjaBackend/ClangScanRuleWritesViaDashOWithoutShellRedirection", [] { auto plan = minimal_plan(); plan.toolchain.compiler = mcpp::toolchain::CompilerId::Clang; plan.toolchain.binaryPath = "/usr/bin/clang++"; @@ -853,19 +849,19 @@ TEST(NinjaBackend, ClangScanRuleWritesViaDashOWithoutShellRedirection) { auto ninja = emit_ninja_string(plan); auto scanRule = ninja.find("rule cxx_scan"); - ASSERT_NE(scanRule, std::string::npos) << ninja; + expect(fatal(ne(scanRule, std::string::npos))) << ninja; auto scanEnd = ninja.find("description = SCAN", scanRule); - ASSERT_NE(scanEnd, std::string::npos) << ninja; + expect(fatal(ne(scanEnd, std::string::npos))) << ninja; auto rule = ninja.substr(scanRule, scanEnd - scanRule); - EXPECT_NE(rule.find("-format=p1689 -o $out --"), std::string::npos) << rule; - EXPECT_EQ(rule.find("> $out"), std::string::npos) + expect(ne(rule.find("-format=p1689 -o $out --"), std::string::npos)) << rule; + expect(eq(rule.find("> $out"), std::string::npos)) << "scan rule must not use shell redirection: " << rule; -} +}}; // The `cmd /c` wrapper was the only place mcpp put a shell between ninja and // a compiler invocation. Keep it gone: it is what re-imposes the 8191 ceiling. -TEST(NinjaBackend, NoRuleWrapsItsCommandInCmdSlashC) { +UTest no_rule_wraps_its_command_in_cmd_slash_c{"NinjaBackend/NoRuleWrapsItsCommandInCmdSlashC", [] { for (auto compiler : {mcpp::toolchain::CompilerId::GCC, mcpp::toolchain::CompilerId::Clang}) { auto plan = minimal_plan(); @@ -883,10 +879,10 @@ TEST(NinjaBackend, NoRuleWrapsItsCommandInCmdSlashC) { }); auto ninja = emit_ninja_string(plan); - EXPECT_EQ(ninja.find("cmd /c"), std::string::npos) + expect(eq(ninja.find("cmd /c"), std::string::npos)) << "compiler=" << static_cast(compiler) << "\n" << ninja; } -} +}}; // mcpp#261: the compile and scan rules carry an UNBOUNDED flag payload — // one -I per dependency include dir — and on Windows ninja spawns through @@ -895,7 +891,7 @@ TEST(NinjaBackend, NoRuleWrapsItsCommandInCmdSlashC) { // reason. Reachable from a POSIX test host via the msvc dialect, which only // ever runs on Windows. NASM is excluded on purpose (it spells response // files `-@ file`), and POSIX keeps the inline form byte-identical. -TEST(NinjaBackend, CompileAndScanRulesRouteFlagsThroughRspfileUnderMsvcDialect) { +UTest compile_and_scan_rules_route_flags_through_rspfile_under_msvc_dialect{"NinjaBackend/CompileAndScanRulesRouteFlagsThroughRspfileUnderMsvcDialect", [] { auto plan = minimal_plan(); plan.toolchain.compiler = mcpp::toolchain::CompilerId::MSVC; plan.toolchain.binaryPath = "cl.exe"; @@ -926,13 +922,13 @@ TEST(NinjaBackend, CompileAndScanRulesRouteFlagsThroughRspfileUnderMsvcDialect) } for (std::string_view rule : rules) { auto start = ninja.find(rule); - ASSERT_NE(start, std::string::npos) << rule << "\n" << ninja; + expect(fatal(ne(start, std::string::npos))) << rule << "\n" << ninja; auto end = ninja.find("\n\n", start); - ASSERT_NE(end, std::string::npos) << ninja; + expect(fatal(ne(end, std::string::npos))) << ninja; auto body = ninja.substr(start, end - start); - EXPECT_NE(body.find("@$out.rsp"), std::string::npos) << body; - EXPECT_NE(body.find("rspfile = $out.rsp"), std::string::npos) << body; + expect(ne(body.find("@$out.rsp"), std::string::npos)) << body; + expect(ne(body.find("rspfile = $out.rsp"), std::string::npos)) << body; // ONLY $local_includes may live in the response file: its content // is tokenized GNU-style (backslash = escape), and those are the // only paths this file forward-slashes itself. $cxxflags carries @@ -940,27 +936,26 @@ TEST(NinjaBackend, CompileAndScanRulesRouteFlagsThroughRspfileUnderMsvcDialect) // routing it through the rsp ate the separators of the std.pcm path // and broke every `import std;` on Windows. The byte order mark in // front is how cl.exe learns the file is UTF-8 (#693). - EXPECT_NE(body.find("rspfile_content = \xEF\xBB\xBF $local_includes\n"), - std::string::npos) << body; + expect(ne(body.find("rspfile_content = \xEF\xBB\xBF $local_includes\n"), std::string::npos)) << body; // The payload must not ALSO remain inline, or the ceiling stands. auto cmdStart = body.find("command = "); auto cmdEnd = body.find('\n', cmdStart); auto cmd = body.substr(cmdStart, cmdEnd - cmdStart); - EXPECT_EQ(cmd.find("$local_includes"), std::string::npos) << cmd; + expect(eq(cmd.find("$local_includes"), std::string::npos)) << cmd; // ...and the flags that must NOT move off the command line stay there. if (rule != "rule c_object\n") - EXPECT_NE(cmd.find("$cxxflags"), std::string::npos) << cmd; + expect(ne(cmd.find("$cxxflags"), std::string::npos)) << cmd; else - EXPECT_NE(cmd.find("$cflags"), std::string::npos) << cmd; + expect(ne(cmd.find("$cflags"), std::string::npos)) << cmd; } -} +}}; // POSIX must keep the inline form: ARG_MAX is ample and an inline command is // far easier to re-run by hand. This is the byte-identity guard for the // #261 change on the platform where it must be a no-op. -TEST(NinjaBackend, CompileRulesStayInlineOnPosixDrivers) { +UTest compile_rules_stay_inline_on_posix_drivers{"NinjaBackend/CompileRulesStayInlineOnPosixDrivers", [] { if constexpr (mcpplibs::os::is_windows) { - GTEST_SKIP() << "inline form is Windows-exempt by design"; + skip("inline form is Windows-exempt by design"); } else { auto plan = minimal_plan(); // GCC → gnu dialect, non-msvc deps plan.compileUnits.push_back({ @@ -975,22 +970,22 @@ TEST(NinjaBackend, CompileRulesStayInlineOnPosixDrivers) { for (std::string_view rule : {"rule cxx_module\n", "rule cxx_object\n", "rule c_object\n"}) { auto start = ninja.find(rule); - ASSERT_NE(start, std::string::npos) << rule << "\n" << ninja; + expect(fatal(ne(start, std::string::npos))) << rule << "\n" << ninja; auto end = ninja.find("\n\n", start); - ASSERT_NE(end, std::string::npos) << ninja; + expect(fatal(ne(end, std::string::npos))) << ninja; auto body = ninja.substr(start, end - start); - EXPECT_EQ(body.find("rspfile"), std::string::npos) << body; - EXPECT_NE(body.find("$local_includes"), std::string::npos) << body; + expect(eq(body.find("rspfile"), std::string::npos)) << body; + expect(ne(body.find("$local_includes"), std::string::npos)) << body; } } -} +}}; // mcpp#257: "emit a depfile" and "strip GCC's reversed module rules" are two // decisions; 0.0.97 conflated them and left Clang with no include tracking // at all. Clang emits a single plain make rule (measured on 20.1.7/22.1.8), // so it takes the depfile WITHOUT the awk filter, writing -MF straight to // $out.d. -TEST(NinjaBackend, ClangGetsDepfileWithoutTheGccModuleRuleFilter) { +UTest clang_gets_depfile_without_the_gcc_module_rule_filter{"NinjaBackend/ClangGetsDepfileWithoutTheGccModuleRuleFilter", [] { // Every host: the clang++ row on Windows (the default Windows row since // #718) had no depfile until 2026.9.28.2 (WS2). auto plan = minimal_plan(); @@ -1001,23 +996,23 @@ TEST(NinjaBackend, ClangGetsDepfileWithoutTheGccModuleRuleFilter) { auto module_rule_start = ninja.find("rule cxx_module"); auto object_rule_start = ninja.find("rule cxx_object"); - ASSERT_NE(module_rule_start, std::string::npos) << ninja; - ASSERT_NE(object_rule_start, std::string::npos) << ninja; + expect(fatal(ne(module_rule_start, std::string::npos))) << ninja; + expect(fatal(ne(object_rule_start, std::string::npos))) << ninja; auto module_rule = ninja.substr(module_rule_start, object_rule_start - module_rule_start); - EXPECT_NE(module_rule.find("-MMD -MF $out.d"), std::string::npos) << ninja; - EXPECT_NE(module_rule.find("deps = gcc"), std::string::npos) << ninja; - EXPECT_NE(module_rule.find("depfile = $out.d\n"), std::string::npos) << ninja; + expect(ne(module_rule.find("-MMD -MF $out.d"), std::string::npos)) << ninja; + expect(ne(module_rule.find("deps = gcc"), std::string::npos)) << ninja; + expect(ne(module_rule.find("depfile = $out.d\n"), std::string::npos)) << ninja; // No scratch file and no filter: there is nothing to strip. - EXPECT_EQ(module_rule.find("$out.d.raw"), std::string::npos) << ninja; - EXPECT_EQ(module_rule.find("awk"), std::string::npos) << ninja; - EXPECT_EQ(module_rule.find("depfile-filter"), std::string::npos) << ninja; -} + expect(eq(module_rule.find("$out.d.raw"), std::string::npos)) << ninja; + expect(eq(module_rule.find("awk"), std::string::npos)) << ninja; + expect(eq(module_rule.find("depfile-filter"), std::string::npos)) << ninja; +}}; // The other half of the same asymmetry: C and GAS edges include headers too // and had no depfile on ANY toolchain. -TEST(NinjaBackend, CAndAsmRulesAlsoTrackHeaderDeps) { +UTest c_and_asm_rules_also_track_header_deps{"NinjaBackend/CAndAsmRulesAlsoTrackHeaderDeps", [] { auto plan = minimal_plan(); plan.compileUnits.push_back({ .source = "src/a.c", @@ -1038,28 +1033,28 @@ TEST(NinjaBackend, CAndAsmRulesAlsoTrackHeaderDeps) { // NOTE: asm_object is the `.S` rule. `.s` uses asm_object_raw and // deliberately has no depfile — see below. auto start = ninja.find(rule); - ASSERT_NE(start, std::string::npos) << rule << "\n" << ninja; + expect(fatal(ne(start, std::string::npos))) << rule << "\n" << ninja; auto end = ninja.find("\n\n", start); - ASSERT_NE(end, std::string::npos) << ninja; + expect(fatal(ne(end, std::string::npos))) << ninja; auto body = ninja.substr(start, end - start); - EXPECT_NE(body.find("-MMD -MF $out.d"), std::string::npos) << body; - EXPECT_NE(body.find("deps = gcc"), std::string::npos) << body; - EXPECT_NE(body.find("depfile = $out.d"), std::string::npos) << body; + expect(ne(body.find("-MMD -MF $out.d"), std::string::npos)) << body; + expect(ne(body.find("deps = gcc"), std::string::npos)) << body; + expect(ne(body.find("depfile = $out.d"), std::string::npos)) << body; // C/GAS units never carry module reversed-rules — no filter, and so // no scratch file to bind by mistake. - EXPECT_EQ(body.find("$out.d.raw"), std::string::npos) << body; - EXPECT_EQ(body.find("awk"), std::string::npos) << body; + expect(eq(body.find("$out.d.raw"), std::string::npos)) << body; + expect(eq(body.find("awk"), std::string::npos)) << body; } -} +}}; // The C driver preprocesses `.S` but not `.s`, so only `.S` can produce a // depfile. Asking anyway makes clang emit "argument unused during // compilation: '-MMD'" for every such file and write nothing, and ninja's // `deps = gcc` treats an absent depfile as an error — so the two cases need // separate rules. -TEST(NinjaBackend, LowercaseAsmHasNoDepfileAndItsOwnRule) { +UTest lowercase_asm_has_no_depfile_and_its_own_rule{"NinjaBackend/LowercaseAsmHasNoDepfileAndItsOwnRule", [] { if constexpr (mcpplibs::os::is_windows) - GTEST_SKIP() << "POSIX depfile shape only"; + skip("POSIX depfile shape only"); auto plan = minimal_plan(); plan.compileUnits.push_back({ @@ -1079,72 +1074,69 @@ TEST(NinjaBackend, LowercaseAsmHasNoDepfileAndItsOwnRule) { auto body_of = [&](std::string_view rule) { auto start = ninja.find(rule); - EXPECT_NE(start, std::string::npos) << rule << "\n" << ninja; + expect(ne(start, std::string::npos)) << rule << "\n" << ninja; auto end = ninja.find("\n\n", start); return ninja.substr(start, end - start); }; auto upper = body_of("rule asm_object\n"); - EXPECT_NE(upper.find("-MMD -MF $out.d"), std::string::npos) << upper; - EXPECT_NE(upper.find("depfile = $out.d"), std::string::npos) << upper; + expect(ne(upper.find("-MMD -MF $out.d"), std::string::npos)) << upper; + expect(ne(upper.find("depfile = $out.d"), std::string::npos)) << upper; auto lower = body_of("rule asm_object_raw\n"); - EXPECT_EQ(lower.find("-MMD"), std::string::npos) << lower; - EXPECT_EQ(lower.find("depfile"), std::string::npos) << lower; + expect(eq(lower.find("-MMD"), std::string::npos)) << lower; + expect(eq(lower.find("depfile"), std::string::npos)) << lower; // And the edges must be routed to the matching rule. - EXPECT_NE(ninja.find("build obj/upper.o : asm_object src/upper.S"), - std::string::npos) << ninja; - EXPECT_NE(ninja.find("build obj/lower.o : asm_object_raw src/lower.s"), - std::string::npos) << ninja; -} + expect(ne(ninja.find("build obj/upper.o : asm_object src/upper.S"), std::string::npos)) << ninja; + expect(ne(ninja.find("build obj/lower.o : asm_object_raw src/lower.s"), std::string::npos)) << ninja; +}}; // ── #311: staging goes through `mcpp stage`, never an in-place shell copy ── -TEST(NinjaBackend, StageRuleRunsThroughMcppWithRestat) { +UTest stage_rule_runs_through_mcpp_with_restat{"NinjaBackend/StageRuleRunsThroughMcppWithRestat", [] { auto plan = minimal_plan(); auto ninja = emit_ninja_string(plan); - EXPECT_NE(ninja.find("rule stage_file\n"), std::string::npos) << ninja; - EXPECT_NE(ninja.find(" command = $mcpp stage $verify --output $out $in\n"), - std::string::npos) << ninja; + expect(ne(ninja.find("rule stage_file\n"), std::string::npos)) << ninja; + expect(ne(ninja.find(" command = $mcpp stage $verify --output $out $in\n"), std::string::npos)) << ninja; // restat is what keeps a skipped (already-equivalent) stage from dirtying // every importer of the staged BMI. auto rulePos = ninja.find("rule stage_file\n"); auto restatPos = ninja.find(" restat = 1\n", rulePos); auto nextRulePos = ninja.find("\nrule ", rulePos + 1); - ASSERT_NE(restatPos, std::string::npos) << ninja; - EXPECT_LT(restatPos, nextRulePos) << ninja; + expect(fatal(ne(restatPos, std::string::npos))) << ninja; + expect(lt(restatPos, nextRulePos)) << ninja; // The old per-platform in-place copies are gone for good — including the // second one, in runtime_alias, which the Windows job caught still // shelling out to PowerShell. - EXPECT_EQ(ninja.find("Copy-Item"), std::string::npos) << ninja; - EXPECT_EQ(ninja.find("cp -f $in $out"), std::string::npos) << ninja; - EXPECT_EQ(ninja.find("cp_bmi"), std::string::npos) << ninja; -} + expect(eq(ninja.find("Copy-Item"), std::string::npos)) << ninja; + expect(eq(ninja.find("cp -f $in $out"), std::string::npos)) << ninja; + expect(eq(ninja.find("cp_bmi"), std::string::npos)) << ninja; +}}; -TEST(NinjaBackend, RuntimeAliasCopiesThroughStageOnWindowsAndSymlinksElsewhere) { +UTest runtime_alias_copies_through_stage_on_windows_and_symlinks_elsewhere{"NinjaBackend/RuntimeAliasCopiesThroughStageOnWindowsAndSymlinksElsewhere", [] { auto plan = minimal_plan(); auto ninja = emit_ninja_string(plan); auto rulePos = ninja.find("rule runtime_alias\n"); - ASSERT_NE(rulePos, std::string::npos) << ninja; + expect(fatal(ne(rulePos, std::string::npos))) << ninja; auto body = ninja.substr(rulePos, ninja.find("\nrule ", rulePos + 1) - rulePos); if constexpr (mcpplibs::os::is_windows) { // PE has no soname symlink: the alias is a copy of a freshly built DLL, // the same hazard class as BMI staging. - EXPECT_NE(body.find("$mcpp stage --output $out $in"), std::string::npos) << body; - EXPECT_NE(body.find("restat = 1"), std::string::npos) << body; + expect(ne(body.find("$mcpp stage --output $out $in"), std::string::npos)) << body; + expect(ne(body.find("restat = 1"), std::string::npos)) << body; } else { // POSIX must keep the SYMLINK — turning it into a copy would change // what gets packaged, not just how it is written. - EXPECT_NE(body.find("ln -s"), std::string::npos) << body; - EXPECT_EQ(body.find("stage --output"), std::string::npos) << body; + expect(ne(body.find("ln -s"), std::string::npos)) << body; + expect(eq(body.find("stage --output"), std::string::npos)) << body; } -} +}}; -TEST(NinjaBackend, McppBinaryIsBoundEvenWithoutDyndep) { +UTest mcpp_binary_is_bound_even_without_dyndep{"NinjaBackend/McppBinaryIsBoundEvenWithoutDyndep", [] { // dyndep off ⇒ no scan/collect rules, but stage_file still needs $mcpp. std::string ninja; { @@ -1153,22 +1145,22 @@ TEST(NinjaBackend, McppBinaryIsBoundEvenWithoutDyndep) { ninja = emit_ninja_string(plan); } - EXPECT_EQ(ninja.find("rule cxx_scan"), std::string::npos) << ninja; - EXPECT_NE(ninja.find("\nmcpp = "), std::string::npos) << ninja; -} + expect(eq(ninja.find("rule cxx_scan"), std::string::npos)) << ninja; + expect(ne(ninja.find("\nmcpp = "), std::string::npos)) << ninja; +}}; // SPEC-007 R4.2: one destination, one writer. On a PE target two spellings // that differ only in case are one file where the program runs, so they are // one destination; elsewhere they are two. -TEST(DeployDestination, FoldsCaseOnlyOnAPeTarget) { +UTest folds_case_only_on_a_pe_target{"DeployDestination/FoldsCaseOnlyOnAPeTarget", [] { const mcpp::build::BuildPlan::DeployFile d{{"/a/Foo.DLL"}, "bin/Foo.DLL"}; - EXPECT_TRUE(d.is_destination("bin/Foo.DLL", /*peTarget=*/false)); - EXPECT_FALSE(d.is_destination("bin/foo.dll", /*peTarget=*/false)); - EXPECT_TRUE(d.is_destination("bin/foo.dll", /*peTarget=*/true)); - EXPECT_FALSE(d.is_destination("bin/sub/foo.dll", /*peTarget=*/true)); -} + expect(d.is_destination("bin/Foo.DLL", /*peTarget=*/false)); + expect(!d.is_destination("bin/foo.dll", /*peTarget=*/false)); + expect(d.is_destination("bin/foo.dll", /*peTarget=*/true)); + expect(!d.is_destination("bin/sub/foo.dll", /*peTarget=*/true)); +}}; -TEST(NinjaBackend, StdArtifactsAndRuntimeDllsUseTheStageRule) { +UTest std_artifacts_and_runtime_dlls_use_the_stage_rule{"NinjaBackend/StdArtifactsAndRuntimeDllsUseTheStageRule", [] { auto plan = minimal_plan(); plan.toolchain.compiler = mcpp::toolchain::CompilerId::Clang; plan.toolchain.binaryPath = "/usr/bin/clang++"; @@ -1183,26 +1175,23 @@ TEST(NinjaBackend, StdArtifactsAndRuntimeDllsUseTheStageRule) { // The std artifacts are fingerprint-scoped, so they carry the size-only // check: it needs no OPEN of the destination, which is what lets staging // survive a holder that denies reads (Windows FileShare.None). - EXPECT_NE(ninja.find("build pcm.cache/std.pcm : stage_file " + expect(ne(ninja.find("build pcm.cache/std.pcm : stage_file " "/cache/bmi/fp/pcm.cache/std.pcm\n" - " verify = --verify size\n"), - std::string::npos) << ninja; - EXPECT_NE(ninja.find("build obj/std.o : stage_file /cache/bmi/fp/std.o\n" - " verify = --verify size\n"), - std::string::npos) << ninja; + " verify = --verify size\n"), std::string::npos)) << ninja; + expect(ne(ninja.find("build obj/std.o : stage_file /cache/bmi/fp/std.o\n" + " verify = --verify size\n"), std::string::npos)) << ninja; // std.compat keeps its order-only dependency on the staged std BMI. - EXPECT_NE(ninja.find("build pcm.cache/std.compat.pcm : stage_file " - "/cache/bmi/fp/pcm.cache/std.compat.pcm | pcm.cache/std.pcm"), - std::string::npos) << ninja; + expect(ne(ninja.find("build pcm.cache/std.compat.pcm : stage_file " + "/cache/bmi/fp/pcm.cache/std.compat.pcm | pcm.cache/std.pcm"), std::string::npos)) << ninja; // Windows DLL deployment shares the rule — and therefore the // skip-if-equivalent behaviour that keeps a loaded DLL from failing a build. // But NOT the size-only shortcut: a rebuilt DLL keeps its size (PE sections // are page-padded), so this edge must compare content. auto dllEdge = ninja.find("build bin/libfoo.dll : stage_file /pkg/lib/libfoo.dll\n"); - ASSERT_NE(dllEdge, std::string::npos) << ninja; + expect(fatal(ne(dllEdge, std::string::npos))) << ninja; auto afterDll = ninja.substr(dllEdge, ninja.find('\n', dllEdge + 1) - dllEdge + 1); - EXPECT_EQ(afterDll.find("verify"), std::string::npos) << afterDll; -} + expect(eq(afterDll.find("verify"), std::string::npos)) << afterDll; +}}; // ── SPEC-007 R4.3: a Windows program's runtime DLLs are placed after its link ── @@ -1225,53 +1214,51 @@ BuildPlan program_plan(std::string_view triple, bool withRuntimeDirs) { } // namespace -TEST(NinjaBackend, PeProgramWithRuntimeSearchDirsGetsAPlacementEdge) { +UTest pe_program_with_runtime_search_dirs_gets_a_placement_edge{"NinjaBackend/PeProgramWithRuntimeSearchDirsGetsAPlacementEdge", [] { auto ninja = emit_ninja_string(program_plan("x86_64-w64-windows-gnu", true)); // One rule, carrying the directories in search order, each one word. - EXPECT_NE(ninja.find("rule place_dlls\n"), std::string::npos) << ninja; + expect(ne(ninja.find("rule place_dlls\n"), std::string::npos)) << ninja; // The C++ runtime rule the resolver applied (mcpp.build.runtime_placement): // a GNU PE program has no MSVC runtime set, so the edge is told so and // places by search order. - EXPECT_NE(ninja.find("place-dlls --output $out --depfile $out.d --crt not-applicable $in "), - std::string::npos) << ninja; - EXPECT_NE(ninja.find("my bin"), std::string::npos) << ninja; - EXPECT_NE(ninja.find(" deps = gcc\n"), std::string::npos) << ninja; + expect(ne(ninja.find("place-dlls --output $out --depfile $out.d --crt not-applicable $in "), std::string::npos)) << ninja; + expect(ne(ninja.find("my bin"), std::string::npos)) << ninja; + expect(ne(ninja.find(" deps = gcc\n"), std::string::npos)) << ninja; // One edge per program, after the link it reads, and built by default. - EXPECT_NE(ninja.find("build bin/app.exe.dlls: place_dlls bin/app.exe\n"), - std::string::npos) << ninja; - EXPECT_NE(ninja.find("default bin/app.exe.dlls\n"), std::string::npos) << ninja; -} + expect(ne(ninja.find("build bin/app.exe.dlls: place_dlls bin/app.exe\n"), std::string::npos)) << ninja; + expect(ne(ninja.find("default bin/app.exe.dlls\n"), std::string::npos)) << ninja; +}}; -TEST(NinjaBackend, NoPlacementEdgeWithoutRuntimeSearchDirsOrOffPe) { +UTest no_placement_edge_without_runtime_search_dirs_or_off_pe{"NinjaBackend/NoPlacementEdgeWithoutRuntimeSearchDirsOrOffPe", [] { // A PE program with no runtime search directory has nothing to place. auto pe = emit_ninja_string(program_plan("x86_64-w64-windows-gnu", false)); - EXPECT_EQ(pe.find("place_dlls"), std::string::npos) << pe; + expect(eq(pe.find("place_dlls"), std::string::npos)) << pe; // ELF keeps its run path: the directories are RUNPATH entries, not copies. auto elf = emit_ninja_string(program_plan("x86_64-linux-gnu", true)); - EXPECT_EQ(elf.find("place_dlls"), std::string::npos) << elf; -} + expect(eq(elf.find("place_dlls"), std::string::npos)) << elf; +}}; -TEST(NinjaBackend, DeployedDllsAreOrderOnlyInputsOfTheLink) { +UTest deployed_dlls_are_order_only_inputs_of_the_link{"NinjaBackend/DeployedDllsAreOrderOnlyInputsOfTheLink", [] { // The linker reads the import library, never the deployed DLL, so a DLL // that changes (or a deploy entry a later plan adds) must not relink. auto plan = program_plan("x86_64-w64-windows-gnu", false); plan.runtimeDeployFiles.push_back({{"/pkg/lib/libfoo.dll"}, "bin/libfoo.dll"}); auto ninja = emit_ninja_string(plan); auto link = ninja.find("build bin/app.exe"); - ASSERT_NE(link, std::string::npos) << ninja; + expect(fatal(ne(link, std::string::npos))) << ninja; auto line = ninja.substr(link, ninja.find('\n', link) - link); auto oo = line.find(" || "); - ASSERT_NE(oo, std::string::npos) << line; - EXPECT_NE(line.find("bin/libfoo.dll", oo), std::string::npos) << line; + expect(fatal(ne(oo, std::string::npos))) << line; + expect(ne(line.find("bin/libfoo.dll", oo), std::string::npos)) << line; auto implicitBar = line.find(" | "); - EXPECT_TRUE(implicitBar == std::string::npos || implicitBar >= oo + expect(implicitBar == std::string::npos || implicitBar >= oo || line.substr(implicitBar, oo - implicitBar).find("libfoo.dll") == std::string::npos) << line; -} +}}; // ── #311: staging failures must stay readable through the output filter ── -TEST(NinjaBackend, FilterKeepsStagingDiagnosticsAndFailedTarget) { +UTest filter_keeps_staging_diagnostics_and_failed_target{"NinjaBackend/FilterKeepsStagingDiagnosticsAndFailedTarget", [] { std::vector prefixes = {"/usr/bin/g++", "/opt/mcpp/bin/mcpp"}; std::string raw = "ninja: Entering directory `target/x86_64-linux-gnu/fp'\n" @@ -1287,14 +1274,14 @@ TEST(NinjaBackend, FilterKeepsStagingDiagnosticsAndFailedTarget) { // Which output failed is now preserved (it used to be dropped entirely). // ninja pads the target list with a trailing space; the normalized line // must not inherit it. - EXPECT_NE(filtered.find("failed: pcm.cache/std.pcm\n"), std::string::npos) << filtered; - EXPECT_EQ(filtered.find("[code=1]"), std::string::npos) << filtered; + expect(ne(filtered.find("failed: pcm.cache/std.pcm\n"), std::string::npos)) << filtered; + expect(eq(filtered.find("[code=1]"), std::string::npos)) << filtered; // The diagnostic survives; the echoed command line does not. - EXPECT_NE(filtered.find("error: cannot stage file"), std::string::npos) << filtered; - EXPECT_NE(filtered.find("help:"), std::string::npos) << filtered; - EXPECT_EQ(filtered.find("stage --output"), std::string::npos) << filtered; - EXPECT_EQ(filtered.find("ninja: Entering directory"), std::string::npos) << filtered; -} + expect(ne(filtered.find("error: cannot stage file"), std::string::npos)) << filtered; + expect(ne(filtered.find("help:"), std::string::npos)) << filtered; + expect(eq(filtered.find("stage --output"), std::string::npos)) << filtered; + expect(eq(filtered.find("ninja: Entering directory"), std::string::npos)) << filtered; +}}; // ── Cache-served units: stage edges, not compile edges ────────────────────── // @@ -1305,7 +1292,7 @@ TEST(NinjaBackend, FilterKeepsStagingDiagnosticsAndFailedTarget) { // every "cached" unit was recompiled — the cache saved nothing and the CLI still // printed "Cached". Staging has to be an edge for ninja to have a record. -TEST(NinjaBackend, CachedUnitEmitsStageEdgeAndNoCompileEdge) { +UTest cached_unit_emits_stage_edge_and_no_compile_edge{"NinjaBackend/CachedUnitEmitsStageEdgeAndNoCompileEdge", [] { auto plan = minimal_plan(); plan.compileUnits.push_back({ .source = "/store/dep/src/dep.cppm", @@ -1320,26 +1307,24 @@ TEST(NinjaBackend, CachedUnitEmitsStageEdgeAndNoCompileEdge) { auto ninja = emit_ninja_string(plan); - EXPECT_NE(ninja.find("build obj/dep.m.o : stage_file " - "/bc/v1/pkg/idx/dep@1.0.0/key/obj/dep.m.o"), - std::string::npos) << ninja; + expect(ne(ninja.find("build obj/dep.m.o : stage_file " + "/bc/v1/pkg/idx/dep@1.0.0/key/obj/dep.m.o"), std::string::npos)) << ninja; // The BMI is staged too, at the path a compile edge would have produced. - EXPECT_NE(ninja.find("stage_file /bc/v1/pkg/idx/dep@1.0.0/key/bmi/dep.gcm"), - std::string::npos) << ninja; + expect(ne(ninja.find("stage_file /bc/v1/pkg/idx/dep@1.0.0/key/bmi/dep.gcm"), std::string::npos)) << ninja; // No compile edge, and no scan edge, for this unit. - EXPECT_EQ(ninja.find("cxx_module /store/dep/src/dep.cppm"), std::string::npos) + expect(eq(ninja.find("cxx_module /store/dep/src/dep.cppm"), std::string::npos)) << ninja; - EXPECT_EQ(ninja.find("cxx_object /store/dep/src/dep.cppm"), std::string::npos) + expect(eq(ninja.find("cxx_object /store/dep/src/dep.cppm"), std::string::npos)) << ninja; - EXPECT_EQ(ninja.find("dep.cppm.ddi"), std::string::npos) << ninja; -} + expect(eq(ninja.find("dep.cppm.ddi"), std::string::npos)) << ninja; +}}; // `--verify size` for the same reason the std artifacts use it: the entry // directory is named by a key covering the toolchain, dialect, profile, the // package's own config and its dependencies' keys, so under one key equal size // IS equivalence — and size comes from directory metadata, which stays readable // even when a holder denies opening the file. -TEST(NinjaBackend, CachedStageEdgesUseSizeVerification) { +UTest cached_stage_edges_use_size_verification{"NinjaBackend/CachedStageEdgesUseSizeVerification", [] { auto plan = minimal_plan(); plan.compileUnits.push_back({ .source = "/store/dep/src/a.c", @@ -1352,12 +1337,12 @@ TEST(NinjaBackend, CachedStageEdgesUseSizeVerification) { auto ninja = emit_ninja_string(plan); auto at = ninja.find("build obj/a.o : stage_file /cache/obj/a.o"); - ASSERT_NE(at, std::string::npos) << ninja; - EXPECT_NE(ninja.find("verify = --verify size", at), std::string::npos) << ninja; -} + expect(fatal(ne(at, std::string::npos))) << ninja; + expect(ne(ninja.find("verify = --verify size", at), std::string::npos)) << ninja; +}}; // A cached unit sits next to uncached ones; only the cached one changes shape. -TEST(NinjaBackend, UncachedUnitsStillCompileAlongsideCachedOnes) { +UTest uncached_units_still_compile_alongside_cached_ones{"NinjaBackend/UncachedUnitsStillCompileAlongsideCachedOnes", [] { auto plan = minimal_plan(); plan.compileUnits.push_back({ .source = "/store/dep/src/dep.c", @@ -1375,15 +1360,13 @@ TEST(NinjaBackend, UncachedUnitsStillCompileAlongsideCachedOnes) { }); auto ninja = emit_ninja_string(plan); - EXPECT_NE(ninja.find("build obj/dep.o : stage_file /cache/obj/dep.o"), - std::string::npos) << ninja; - EXPECT_NE(ninja.find("build obj/main.o : cxx_object src/main.cpp"), - std::string::npos) << ninja; -} + expect(ne(ninja.find("build obj/dep.o : stage_file /cache/obj/dep.o"), std::string::npos)) << ninja; + expect(ne(ninja.find("build obj/main.o : cxx_object src/main.cpp"), std::string::npos)) << ninja; +}}; // The default path must be byte-identical to what it was: `servedFromCache` is // false everywhere unless a cache hit set it. -TEST(NinjaBackend, NoStageEdgesWithoutCachedUnits) { +UTest no_stage_edges_without_cached_units{"NinjaBackend/NoStageEdgesWithoutCachedUnits", [] { auto plan = minimal_plan(); plan.compileUnits.push_back({ .source = "src/main.cpp", @@ -1393,12 +1376,11 @@ TEST(NinjaBackend, NoStageEdgesWithoutCachedUnits) { }); auto ninja = emit_ninja_string(plan); - EXPECT_EQ(ninja.find("build obj/main.o : stage_file"), std::string::npos) << ninja; - EXPECT_NE(ninja.find("build obj/main.o : cxx_object src/main.cpp"), - std::string::npos) << ninja; -} + expect(eq(ninja.find("build obj/main.o : stage_file"), std::string::npos)) << ninja; + expect(ne(ninja.find("build obj/main.o : cxx_object src/main.cpp"), std::string::npos)) << ninja; +}}; -TEST(NinjaBackend, ObjectOnlyPackageBuildsOwnObjectsAlongsideCacheFills) { +UTest object_only_package_builds_own_objects_alongside_cache_fills{"NinjaBackend/ObjectOnlyPackageBuildsOwnObjectsAlongsideCacheFills", [] { auto plan = minimal_plan(); plan.compileUnits.push_back({ .source = "src/provider.c", @@ -1416,16 +1398,16 @@ TEST(NinjaBackend, ObjectOnlyPackageBuildsOwnObjectsAlongsideCacheFills) { auto ninja = emit_ninja_string(plan); auto at = ninja.rfind("\ndefault "); - ASSERT_NE(at, std::string::npos) << ninja; + expect(fatal(ne(at, std::string::npos))) << ninja; auto defaults = ninja.substr(at, ninja.find('\n', at + 1) - at); - EXPECT_NE(defaults.find(" obj/provider.o"), std::string::npos) << defaults; - EXPECT_EQ(count_occurrences(defaults, " obj/dep.o"), 1u) << defaults; -} + expect(ne(defaults.find(" obj/provider.o"), std::string::npos)) << defaults; + expect(eq(count_occurrences(defaults, " obj/dep.o"), 1u)) << defaults; +}}; // A cached unit whose cachedObject was never filled in is a bug upstream, not a // reason to emit a stage edge with an empty input — it must fall back to being // compiled rather than producing an unbuildable graph. -TEST(NinjaBackend, CachedUnitWithoutCachedObjectPathIsStillCompiled) { +UTest cached_unit_without_cached_object_path_is_still_compiled{"NinjaBackend/CachedUnitWithoutCachedObjectPathIsStillCompiled", [] { auto plan = minimal_plan(); plan.compileUnits.push_back({ .source = "src/main.cpp", @@ -1436,15 +1418,15 @@ TEST(NinjaBackend, CachedUnitWithoutCachedObjectPathIsStillCompiled) { }); auto ninja = emit_ninja_string(plan); - EXPECT_EQ(ninja.find("stage_file \n"), std::string::npos) << ninja; - EXPECT_EQ(ninja.find("build obj/main.o : stage_file"), std::string::npos) << ninja; -} + expect(eq(ninja.find("stage_file \n"), std::string::npos)) << ninja; + expect(eq(ninja.find("build obj/main.o : stage_file"), std::string::npos)) << ninja; +}}; // A cache-served unit keeps its compile_commands.json entry. The units stay in // the plan (only their EDGE changes shape) precisely so clangd does not lose the // dependency's sources — a cache that silently degraded IDE navigation would be // a bad trade for build time. -TEST(NinjaBackend, CachedUnitsStillAppearInCompileCommands) { +UTest cached_units_still_appear_in_compile_commands{"NinjaBackend/CachedUnitsStillAppearInCompileCommands", [] { auto plan = minimal_plan(); plan.compileUnits.push_back({ .source = "/store/dep/src/dep.c", @@ -1478,9 +1460,9 @@ TEST(NinjaBackend, CachedUnitsStillAppearInCompileCommands) { sawDep = sawDep || f == dep.string(); sawMain = sawMain || f == main.string(); } - EXPECT_TRUE(sawDep) << cdb; - EXPECT_TRUE(sawMain) << cdb; -} + expect(bool(sawDep)) << cdb; + expect(bool(sawMain)) << cdb; +}}; // Replacing a package's compile edges with stage edges also removes the ordering // those compile edges carried. A module PARTITION is the case that breaks: a @@ -1490,7 +1472,7 @@ TEST(NinjaBackend, CachedUnitsStillAppearInCompileCommands) { // the partition is still unstaged, and Clang fails with `failed to find module // file for module 'pkg:part'`. Observed on macOS CI while Linux won the race, // which is why it is pinned here rather than left to scheduling. -TEST(NinjaBackend, NonCachedEdgesOrderAfterEveryStagedArtifact) { +UTest non_cached_edges_order_after_every_staged_artifact{"NinjaBackend/NonCachedEdgesOrderAfterEveryStagedArtifact", [] { auto plan = minimal_plan(); // A cached package with a primary interface and a partition. plan.compileUnits.push_back({ @@ -1527,29 +1509,29 @@ TEST(NinjaBackend, NonCachedEdgesOrderAfterEveryStagedArtifact) { // One phony aggregating every staged artifact — a per-edge list would repeat // it and grow ninja lines without bound (mcpp#274). auto phony = ninja.find("build _mcpp_staged_cache : phony"); - ASSERT_NE(phony, std::string::npos) << ninja; + expect(fatal(ne(phony, std::string::npos))) << ninja; auto phonyLine = ninja.substr(phony, ninja.find('\n', phony) - phony); for (auto* art : {"obj/dep.m.o", "obj/part.m.o", "gcm.cache/dep.gcm", "gcm.cache/dep-part.gcm"}) { - EXPECT_NE(phonyLine.find(art), std::string::npos) + expect(ne(phonyLine.find(art), std::string::npos)) << art << " missing from: " << phonyLine; } // The consumer's edge orders after it... auto consumer = ninja.find("build obj/main.o"); - ASSERT_NE(consumer, std::string::npos) << ninja; + expect(fatal(ne(consumer, std::string::npos))) << ninja; auto consumerLine = ninja.substr(consumer, ninja.find('\n', consumer) - consumer); - EXPECT_NE(consumerLine.find("|| _mcpp_staged_cache"), std::string::npos) + expect(ne(consumerLine.find("|| _mcpp_staged_cache"), std::string::npos)) << consumerLine; // ...as an ORDER-ONLY dependency, not an implicit one: the real content // dependency is still declared where it always was, so this must add // sequencing without making a staged BMI dirty its consumers. auto bar = consumerLine.find("|| _mcpp_staged_cache"); - EXPECT_EQ(consumerLine.find("| _mcpp_staged_cache"), bar + 1) + expect(eq(consumerLine.find("| _mcpp_staged_cache"), bar + 1)) << "staged phony must appear only after ||, never as an implicit input: " << consumerLine; -} +}}; // The other direction of the same ordering. A staged BMI can import a module // compiled HERE: a unit of the package that the cache entry does not hold, @@ -1561,7 +1543,7 @@ TEST(NinjaBackend, NonCachedEdgesOrderAfterEveryStagedArtifact) { // pass changed the schedule). The stage edge now waits for those BMIs, and it // leaves the aggregate, because every compile edge waits for the aggregate and // the compile it waits for is one of them. -TEST(NinjaBackend, AStagedBmiWaitsForTheModulesItImportsThatCompileHere) { +UTest a_staged_bmi_waits_for_the_modules_it_imports_that_compile_here{"NinjaBackend/AStagedBmiWaitsForTheModulesItImportsThatCompileHere", [] { auto plan = minimal_plan(); // The generated unit of the cached package, compiled here. plan.compileUnits.push_back({ @@ -1622,18 +1604,18 @@ TEST(NinjaBackend, AStagedBmiWaitsForTheModulesItImportsThatCompileHere) { }; auto dep = line_of("build gcm.cache/dep.gcm : stage_file"); - ASSERT_FALSE(dep.empty()) << ninja; - EXPECT_NE(dep.find("|| gcm.cache/dep.gen.gcm"), std::string::npos) << dep; + expect(fatal(!dep.empty())) << ninja; + expect(ne(dep.find("|| gcm.cache/dep.gen.gcm"), std::string::npos)) << dep; auto api = line_of("build gcm.cache/dep.api.gcm : stage_file"); - ASSERT_FALSE(api.empty()) << ninja; - EXPECT_NE(api.find("|| gcm.cache/dep.gcm"), std::string::npos) << api; + expect(fatal(!api.empty())) << ninja; + expect(ne(api.find("|| gcm.cache/dep.gcm"), std::string::npos)) << api; auto util = line_of("build gcm.cache/dep.util.gcm : stage_file"); - ASSERT_FALSE(util.empty()) << ninja; - EXPECT_EQ(util.find("||"), std::string::npos) << util; + expect(fatal(!util.empty())) << ninja; + expect(eq(util.find("||"), std::string::npos)) << util; // The aggregate holds the BMIs that wait for nothing, and every object. auto phony = line_of("build _mcpp_staged_cache : phony"); - ASSERT_FALSE(phony.empty()) << ninja; + expect(fatal(!phony.empty())) << ninja; auto words = [](const std::string& s) { std::set w; std::istringstream is(s); @@ -1641,19 +1623,19 @@ TEST(NinjaBackend, AStagedBmiWaitsForTheModulesItImportsThatCompileHere) { return w; }; auto held = words(phony); - EXPECT_FALSE(held.contains("gcm.cache/dep.gcm")) << phony; - EXPECT_FALSE(held.contains("gcm.cache/dep.api.gcm")) << phony; - EXPECT_TRUE(held.contains("gcm.cache/dep.util.gcm")) << phony; + expect(!held.contains("gcm.cache/dep.gcm")) << phony; + expect(!held.contains("gcm.cache/dep.api.gcm")) << phony; + expect(held.contains("gcm.cache/dep.util.gcm")) << phony; for (auto* o : {"obj/dep.m.o", "obj/api.m.o", "obj/util.m.o"}) - EXPECT_TRUE(held.contains(o)) << o << " missing from: " << phony; + expect(held.contains(o)) << o << " missing from: " << phony; // The generated unit still compiles after the aggregate: no cycle. auto gen = line_of("build obj/gen.m.o"); - ASSERT_FALSE(gen.empty()) << ninja; - EXPECT_NE(gen.find("|| _mcpp_staged_cache"), std::string::npos) << gen; -} + expect(fatal(!gen.empty())) << ninja; + expect(ne(gen.find("|| _mcpp_staged_cache"), std::string::npos)) << gen; +}}; -TEST(NinjaBackend, NoStagedPhonyWhenNothingIsCached) { +UTest no_staged_phony_when_nothing_is_cached{"NinjaBackend/NoStagedPhonyWhenNothingIsCached", [] { auto plan = minimal_plan(); plan.compileUnits.push_back({ .source = "src/main.cpp", @@ -1662,8 +1644,8 @@ TEST(NinjaBackend, NoStagedPhonyWhenNothingIsCached) { .packageName = "objc_rule_test", }); auto ninja = emit_ninja_string(plan); - EXPECT_EQ(ninja.find("_mcpp_staged_cache"), std::string::npos) << ninja; -} + expect(eq(ninja.find("_mcpp_staged_cache"), std::string::npos)) << ninja; +}}; // #390: the main-manifest include channel's fallback join — `root / inc` for // a directory a build step will create LATER (the `generated/inc` shape) — @@ -1673,25 +1655,25 @@ TEST(NinjaBackend, NoStagedPhonyWhenNothingIsCached) { // rule for an absolute entry written with forward slashes (`C:/SDL2/include`). // (The existing-directory path runs through expand_dir_glob, covered by // Scanner.ExpandDirGlobMultiSegmentUsesNativeSeparators.) -TEST(Plan, ExpandManifestIncludeEntryNativeSpelling) { +UTest expand_manifest_include_entry_native_spelling{"Plan/ExpandManifestIncludeEntryNativeSpelling", [] { namespace fs = std::filesystem; auto root = fs::temp_directory_path() / "mcpp-plan-inc-entry"; fs::remove_all(root); fs::create_directories(root); // generated/inc deliberately does NOT exist auto fb = expand_manifest_include_entry(root, fs::path("generated/inc")); - ASSERT_EQ(fb.size(), 1u); + expect(fatal(eq(fb.size(), 1u))); if constexpr (fs::path::preferred_separator == '\\') - EXPECT_EQ(fb[0].string().find('/'), std::string::npos) << fb[0]; - EXPECT_EQ(fb[0], root / "generated" / "inc"); + expect(eq(fb[0].string().find('/'), std::string::npos)) << fb[0]; + expect(eq(fb[0], root / "generated" / "inc")); auto abs = expand_manifest_include_entry(root, fs::path("C:/SDL2/include")); - ASSERT_EQ(abs.size(), 1u); + expect(fatal(eq(abs.size(), 1u))); if constexpr (fs::path::preferred_separator == '\\') - EXPECT_EQ(abs[0].string(), "C:\\SDL2\\include"); + expect(eq(abs[0].string(), "C:\\SDL2\\include")); fs::remove_all(root); -} +}}; // ── PE `toolchain-coupled`: the toolset's CRT travels with the artifact ───── // @@ -1755,20 +1737,20 @@ std::vector toolset_entries(const mcpp::build::CompileFla } // namespace -TEST(NinjaBackendPeRuntime, ToolchainCoupledStagesTheToolsetCrtBesideTheExe) { +UTest toolchain_coupled_stages_the_toolset_crt_beside_the_exe{"NinjaBackendPeRuntime/ToolchainCoupledStagesTheToolsetCrtBesideTheExe", [] { FakeRedistDir redist; auto plan = msvc_plan_with_redist(redist, "toolchain-coupled"); auto flags = compute_flags(plan); auto toolset = toolset_entries(flags); - ASSERT_EQ(toolset.size(), 3u) + expect(fatal(eq(toolset.size(), 3u))) << "expected the three .dll and not the .manifest beside them"; for (auto const& d : toolset) { - EXPECT_EQ(d.dest.parent_path(), std::filesystem::path("bin")) + expect(eq(d.dest.parent_path(), std::filesystem::path("bin"))) << "a DLL must land in the same directory as the .exe: " << d.dest.string(); - ASSERT_EQ(d.sources.size(), 1u); - EXPECT_EQ(d.sources.front().extension(), ".dll") << d.sources.front().string(); + expect(fatal(eq(d.sources.size(), 1u))); + expect(eq(d.sources.front().extension(), ".dll")) << d.sources.front().string(); } std::string placements; @@ -1776,39 +1758,39 @@ TEST(NinjaBackendPeRuntime, ToolchainCoupledStagesTheToolsetCrtBesideTheExe) { // #734 E4: two or more placements are one `stage_list` edge whose outputs // are every destination, and the list the edge reads names each pair. const auto listAt = ninja.find(": stage_list "); - ASSERT_NE(listAt, std::string::npos) << ninja; + expect(fatal(ne(listAt, std::string::npos))) << ninja; const auto listLine = ninja.substr(ninja.rfind("\nbuild ", listAt) + 1, ninja.find('\n', listAt) - ninja.rfind("\nbuild ", listAt) - 1); for (auto name : {"vcruntime140.dll", "msvcp140.dll", "vcruntime140_1.dll"}) { - EXPECT_NE(listLine.find(std::format("bin/{}", name)), std::string::npos) + expect(ne(listLine.find(std::format("bin/{}", name)), std::string::npos)) << name << " is not an output of the placement edge\n" << ninja; - EXPECT_NE(placements.find(std::format("\tbin/{}", name)), std::string::npos) + expect(ne(placements.find(std::format("\tbin/{}", name)), std::string::npos)) << name << " is not in the placement list\n" << placements; } - EXPECT_EQ(ninja.find("Microsoft.VC143.CRT.manifest"), std::string::npos) + expect(eq(ninja.find("Microsoft.VC143.CRT.manifest"), std::string::npos)) << "copied something that is not a runtime DLL\n" << ninja; // Reachability, twice over. A copy edge nothing asks for is never run // under explicit ninja goals, and an .exe that does not depend on its DLLs // can be reported "up to date" while they are missing. auto exeAt = ninja.find("build bin/app.exe :"); - ASSERT_NE(exeAt, std::string::npos) << ninja; + expect(fatal(ne(exeAt, std::string::npos))) << ninja; auto exeStanza = ninja.substr(exeAt, ninja.find("\nbuild ", exeAt + 1) - exeAt); - EXPECT_NE(exeStanza.find("bin/vcruntime140.dll"), std::string::npos) + expect(ne(exeStanza.find("bin/vcruntime140.dll"), std::string::npos)) << "the executable does not depend on the CRT staged beside it\n" << exeStanza; auto defaultAt = ninja.rfind("\ndefault "); - ASSERT_NE(defaultAt, std::string::npos) << ninja; - EXPECT_NE(ninja.find("bin/vcruntime140.dll", defaultAt), std::string::npos) + expect(fatal(ne(defaultAt, std::string::npos))) << ninja; + expect(ne(ninja.find("bin/vcruntime140.dll", defaultAt), std::string::npos)) << "the staged CRT is not a default goal\n" << ninja.substr(defaultAt); -} +}}; -TEST(NinjaBackendPeRuntime, HostCoupledStagesNothing) { +UTest host_coupled_stages_nothing{"NinjaBackendPeRuntime/HostCoupledStagesNothing", [] { // An EXPLICIT `host-coupled`: the machine is a promise that it provides // the DLLs, and keeping a copy beside the artifact would contradict it. FakeRedistDir redist; auto plan = msvc_plan_with_redist(redist, "host-coupled"); - EXPECT_TRUE(compute_flags(plan).runtimeDeploy.empty()); + expect(compute_flags(plan).runtimeDeploy.empty()); // The BARE default (#718): `toolchain-coupled` is now the MSVC-ABI // default for every role, so a project that never mentioned @@ -1816,11 +1798,11 @@ TEST(NinjaBackendPeRuntime, HostCoupledStagesNothing) { // explicit `toolchain-coupled` would — see // `ToolchainCoupledStagesTheToolsetCrtBesideTheExe`. auto bare = msvc_plan_with_redist(redist, ""); - EXPECT_EQ(toolset_entries(compute_flags(bare)).size(), 3u) + expect(eq(toolset_entries(compute_flags(bare)).size(), 3u)) << "the undeclared MSVC-ABI default no longer stages the redistributable"; -} +}}; -TEST(NinjaBackendPeRuntime, AProjectsOwnDeployFileOutranksTheToolsets) { +UTest a_projects_own_deploy_file_outranks_the_toolsets{"NinjaBackendPeRuntime/AProjectsOwnDeployFileOutranksTheToolsets", [] { // A vendored redist named in `[runtime] deploy_files` is a human's // statement about which build of msvcp140.dll this program ships. Silently // replacing it with the toolset's copy produces a different program than @@ -1834,24 +1816,24 @@ TEST(NinjaBackendPeRuntime, AProjectsOwnDeployFileOutranksTheToolsets) { auto flags = compute_flags(plan); for (auto const& d : toolset_entries(flags)) - EXPECT_NE(d.dest.filename(), std::filesystem::path("msvcp140.dll")) + expect(ne(d.dest.filename(), std::filesystem::path("msvcp140.dll"))) << "overwrote the project's own deploy file"; - EXPECT_EQ(toolset_entries(flags).size(), 2u); - EXPECT_TRUE(std::ranges::any_of(flags.runtimeDeploy, [](auto const& d) { + expect(eq(toolset_entries(flags).size(), 2u)); + expect(std::ranges::any_of(flags.runtimeDeploy, [](auto const& d) { return d.origin == BuildPlan::DeployFile::Origin::Declared && d.sources.front() == std::filesystem::path("/vendor/msvcp140.dll"); })) << "the declared file is not placed"; - EXPECT_TRUE(std::ranges::any_of(flags.runtimeNotes, [](auto const& d) { + expect(std::ranges::any_of(flags.runtimeNotes, [](auto const& d) { return d.find("msvcp140.dll") != std::string::npos; })) << "the declared runtime file was placed without a word about its version"; -} +}}; // The 2026-09-28 design, §2.9: a build for a Windows target has one C++ // runtime, the toolset's, for the program and for the tools that build it. // Every action runs with the toolset's runtime directory first on its PATH, // through the named wrapper, whatever its role. A target without the MSVC ABI // keeps its commands as they were. -TEST(NinjaBackendPeRuntime, EveryActionOfAWindowsTargetHasTheToolsetRuntimeOnPath) { +UTest every_action_of_a_windows_target_has_the_toolset_runtime_on_path{"NinjaBackendPeRuntime/EveryActionOfAWindowsTargetHasTheToolsetRuntimeOnPath", [] { FakeRedistDir redist; auto plan = msvc_plan_with_redist(redist, ""); mcpp::manifest::BuildAction a; @@ -1864,21 +1846,21 @@ TEST(NinjaBackendPeRuntime, EveryActionOfAWindowsTargetHasTheToolsetRuntimeOnPat auto ninja = emit_ninja_string(plan); auto ruleAt = ninja.find("rule mcpp_action_0"); - ASSERT_NE(ruleAt, std::string::npos) << ninja; + expect(fatal(ne(ruleAt, std::string::npos))) << ninja; auto rule = ninja.substr(ruleAt, ninja.find("\n\n", ruleAt) - ruleAt); - EXPECT_NE(rule.find(" __action"), std::string::npos) << rule; - EXPECT_NE(rule.find("--path-prepend"), std::string::npos) << rule; - EXPECT_NE(rule.find(redist.path.filename().string()), std::string::npos) + expect(ne(rule.find(" __action"), std::string::npos)) << rule; + expect(ne(rule.find("--path-prepend"), std::string::npos)) << rule; + expect(ne(rule.find(redist.path.filename().string()), std::string::npos)) << "the prepended directory is not the toolset's runtime directory\n" << rule; auto elf = minimal_plan(); elf.actions.push_back(a); auto elfNinja = emit_ninja_string(elf); - EXPECT_EQ(elfNinja.find("--path-prepend"), std::string::npos) + expect(eq(elfNinja.find("--path-prepend"), std::string::npos)) << "a target without the MSVC ABI received the Windows runtime on PATH\n" << elfNinja; -} +}}; -TEST(NinjaBackendPeRuntime, AnElfToolchainNeverStagesItsRuntimeDirs) { +UTest an_elf_toolchain_never_stages_its_runtime_dirs{"NinjaBackendPeRuntime/AnElfToolchainNeverStagesItsRuntimeDirs", [] { // `linkRuntimeDirs` means "the toolchain's private runtime" for every // provider — on gcc it is libstdc++'s directory. That is reached through // an rpath, and copying its contents into an output tree would be a @@ -1888,8 +1870,8 @@ TEST(NinjaBackendPeRuntime, AnElfToolchainNeverStagesItsRuntimeDirs) { plan.toolchain.compiler = mcpp::toolchain::CompilerId::GCC; plan.toolchain.binaryPath = "/usr/bin/g++"; plan.toolchain.targetTriple = "x86_64-linux-gnu"; - EXPECT_TRUE(toolset_entries(compute_flags(plan)).empty()); -} + expect(toolset_entries(compute_flags(plan)).empty()); +}}; // #718: every MSVC-ABI row x {undeclared, self-contained, toolchain-coupled, // host-coupled, linkage=static} yields exactly one CRT word, spelled for its @@ -1900,7 +1882,7 @@ TEST(NinjaBackendPeRuntime, AnElfToolchainNeverStagesItsRuntimeDirs) { // link line is `link.exe` directly (`LinkShape::MsvcLinkExe`) and carries no // compile flag at all — the CRT is baked into the objects it is given — so // only the LLVM row's link line is asked to repeat the word. -TEST(NinjaBackendPeRuntime, CrtWordIsOneWordEqualOnCompileAndLink) { +UTest crt_word_is_one_word_equal_on_compile_and_link{"NinjaBackendPeRuntime/CrtWordIsOneWordEqualOnCompileAndLink", [] { FakeRedistDir redist; struct Row { std::string_view label; @@ -1928,13 +1910,13 @@ TEST(NinjaBackendPeRuntime, CrtWordIsOneWordEqualOnCompileAndLink) { auto flags = compute_flags(plan); auto want = c.wantsStatic ? row.staticWord : row.dynamicWord; auto other = c.wantsStatic ? row.dynamicWord : row.staticWord; - SCOPED_TRACE(std::format("{} linkage='{}' cxx_runtime='{}'", - row.label, c.linkage, c.cxxRuntime)); + /* trace: std::format("{} linkage='{}' cxx_runtime='{}'", + row.label, c.linkage, c.cxxRuntime) */; - EXPECT_EQ(count_occurrences(flags.cxx, want), 1u) << flags.cxx; - EXPECT_EQ(count_occurrences(flags.cc, want), 1u) << flags.cc; - EXPECT_EQ(flags.cxx.find(other), std::string::npos); - EXPECT_EQ(flags.cc.find(other), std::string::npos); + expect(eq(count_occurrences(flags.cxx, want), 1u)) << flags.cxx; + expect(eq(count_occurrences(flags.cc, want), 1u)) << flags.cc; + expect(eq(flags.cxx.find(other), std::string::npos)); + expect(eq(flags.cc.find(other), std::string::npos)); // `link_shape` (flags.cppm) picks `LinkShape::PeLld` — the // branch that carries this word on the link line — only when @@ -1946,14 +1928,14 @@ TEST(NinjaBackendPeRuntime, CrtWordIsOneWordEqualOnCompileAndLink) { // see the Windows e2e leg (814) for that half. if constexpr (mcpplibs::os::is_windows) { if (row.compiler == mcpp::toolchain::CompilerId::Clang) { - EXPECT_EQ(count_occurrences(flags.ld, want), 1u) << flags.ld; - EXPECT_EQ(flags.ld.find(other), std::string::npos); - EXPECT_EQ(flags.ld, flags.ldC); + expect(eq(count_occurrences(flags.ld, want), 1u)) << flags.ld; + expect(eq(flags.ld.find(other), std::string::npos)); + expect(eq(flags.ld, flags.ldC)); } } } } -} +}}; // ── link_failure_advice ────────────────────────────────────────────────────── // @@ -1961,45 +1943,45 @@ TEST(NinjaBackendPeRuntime, CrtWordIsOneWordEqualOnCompileAndLink) { // symbol from a header inside the standard library. Nothing in that message // says which package supplies it, or that the answer is one manifest line. -TEST(LinkFailureAdvice, NamesTheAllocatorFeatureForLldsSpelling) { +UTest names_the_allocator_feature_for_llds_spelling{"LinkFailureAdvice/NamesTheAllocatorFeatureForLldsSpelling", [] { const std::string out = "ld.lld: error: undefined symbol: operator new(unsigned long)\n" ">>> referenced by allocate.h:58\n"; auto advice = mcpp::build::link_failure_advice(out); - EXPECT_NE(advice.find("alloc-kal"), std::string::npos); - EXPECT_NE(advice.find("std-freestanding"), std::string::npos); + expect(ne(advice.find("alloc-kal"), std::string::npos)); + expect(ne(advice.find("std-freestanding"), std::string::npos)); // The four that are most often forgotten are named, because omitting them // produces a SECOND link failure after the first is fixed. - EXPECT_NE(advice.find("align_val_t"), std::string::npos); -} + expect(ne(advice.find("align_val_t"), std::string::npos)); +}}; -TEST(LinkFailureAdvice, MatchesGnuLdsSpellingToo) { +UTest matches_gnu_lds_spelling_too{"LinkFailureAdvice/MatchesGnuLdsSpellingToo", [] { // Two linkers, two spellings of the same fact. Matching only lld's would // make the advice appear on some toolchains and not others. const std::string out = "/usr/bin/ld: main.o: undefined reference to `operator new(unsigned long)'\n"; - EXPECT_FALSE(mcpp::build::link_failure_advice(out).empty()); -} + expect(!(mcpp::build::link_failure_advice(out).empty())); +}}; -TEST(LinkFailureAdvice, StaysSilentOnUnrelatedFailures) { +UTest stays_silent_on_unrelated_failures{"LinkFailureAdvice/StaysSilentOnUnrelatedFailures", [] { // The advice is appended to EVERY failed build, so a needle that matched // loosely would attach allocator advice to a missing board symbol. const std::string out = "ld.lld: error: undefined symbol: board_uart_init\n" "error: cannot open crt0.o: No such file or directory\n"; - EXPECT_TRUE(mcpp::build::link_failure_advice(out).empty()); -} + expect(mcpp::build::link_failure_advice(out).empty()); +}}; -TEST(LinkFailureAdvice, CarriesNoVersionLiteral) { +UTest carries_no_version_literal{"LinkFailureAdvice/CarriesNoVersionLiteral", [] { // The `import std` advice needed tests/e2e/135 to keep its version // honest. This one is built so it cannot go stale the same way: it names a // package and a FEATURE, and the feature pulls the implementation. A digit // sequence that looks like a version here would be a regression in kind. auto advice = mcpp::build::link_failure_advice( "ld.lld: error: undefined symbol: operator new(unsigned long)\n"); - ASSERT_FALSE(advice.empty()); - EXPECT_EQ(advice.find("0."), std::string::npos) << advice; -} + expect(fatal(!advice.empty())); + expect(eq(advice.find("0."), std::string::npos)) << advice; +}}; // ── graph_c_library_isolation_advice (mcpp#662) ───────────────────────────── // @@ -2010,14 +1992,14 @@ TEST(LinkFailureAdvice, CarriesNoVersionLiteral) { // use to diagnose it: the isolating token on the command line, together with // the compiler's own "file not found". -TEST(GraphCLibraryIsolationAdvice, FiresOnlyWhenBothTheTokenAndTheErrorAreThere) { +UTest fires_only_when_both_the_token_and_the_error_are_there{"GraphCLibraryIsolationAdvice/FiresOnlyWhenBothTheTokenAndTheErrorAreThere", [] { // Neither alone is enough. - EXPECT_TRUE(mcpp::build::graph_c_library_isolation_advice( + expect(mcpp::build::graph_c_library_isolation_advice( "clang: error: something else entirely\n").empty()); - EXPECT_TRUE(mcpp::build::graph_c_library_isolation_advice( + expect(mcpp::build::graph_c_library_isolation_advice( "-nostdlibinc -std=c11 -c probe.c\n" "probe.c:1:1: error: unknown type name 'nonsense'\n").empty()); - EXPECT_TRUE(mcpp::build::graph_c_library_isolation_advice( + expect(mcpp::build::graph_c_library_isolation_advice( "clang -xc -std=c11 -c probe.c\n" "probe.c:1:10: fatal error: 'io.h' file not found\n").empty()); // Both together: an ordinary compile failure the fix's own token is on. @@ -2027,13 +2009,13 @@ TEST(GraphCLibraryIsolationAdvice, FiresOnlyWhenBothTheTokenAndTheErrorAreThere) "-nostdinc++ -c probe.c -o obj/probe.c.o\n" "probe.c:1:10: fatal error: 'io.h' file not found\n" " 1 | #include \n"; - EXPECT_FALSE(mcpp::build::graph_c_library_isolation_advice(out).empty()); -} + expect(!(mcpp::build::graph_c_library_isolation_advice(out).empty())); +}}; // WHETHER THE C LIBRARY IS THE GRAPH'S reaches the fast path through a file // beside build.ninja (LLVM 23.1.3 Part 3, D3): a native build on a managed C // library carries `-nostdlibinc` too, so the token alone no longer answers it. -TEST(GraphCLibrarySidecar, PresentOnlyForAGraphCLibrary) { +UTest present_only_for_a_graph_c_library{"GraphCLibrarySidecar/PresentOnlyForAGraphCLibrary", [] { const auto dir = std::filesystem::temp_directory_path() / std::format("mcpp_graph_clib_{}", std::random_device{}()); std::filesystem::create_directories(dir); @@ -2044,37 +2026,37 @@ TEST(GraphCLibrarySidecar, PresentOnlyForAGraphCLibrary) { graph.impl = "openkal-musl@0.3.5"; mcpp::build::write_graph_c_library_sidecar(dir, graph); auto read = mcpp::build::read_graph_c_library_sidecar(dir); - ASSERT_TRUE(read.has_value()); - EXPECT_EQ(read->first, "musl"); - EXPECT_EQ(read->second, "openkal-musl@0.3.5"); + expect(fatal(read.has_value())); + expect(eq(read->first, "musl")); + expect(eq(read->second, "openkal-musl@0.3.5")); // A payload C library (the native managed glibc) removes the file. mcpp::targetside::Layer payload; payload.origin = mcpp::targetside::Origin::Payload; payload.interfaceName = "glibc"; mcpp::build::write_graph_c_library_sidecar(dir, payload); - EXPECT_FALSE(mcpp::build::read_graph_c_library_sidecar(dir).has_value()); + expect(!(mcpp::build::read_graph_c_library_sidecar(dir).has_value())); std::error_code ec; std::filesystem::remove_all(dir, ec); -} +}}; -TEST(GraphCLibraryIsolationAdvice, NamesTheLibraryWhenGiven) { +UTest names_the_library_when_given{"GraphCLibraryIsolationAdvice/NamesTheLibraryWhenGiven", [] { const std::string out = "clang -nostdlibinc -c probe.c\n" "probe.c:1:10: fatal error: 'io.h' file not found\n"; auto advice = mcpp::build::graph_c_library_isolation_advice( out, "musl", "openkal-musl@0.3.5"); - ASSERT_FALSE(advice.empty()); - EXPECT_NE(advice.find("musl"), std::string::npos); - EXPECT_NE(advice.find("openkal-musl@0.3.5"), std::string::npos); + expect(fatal(!advice.empty())); + expect(ne(advice.find("musl"), std::string::npos)); + expect(ne(advice.find("openkal-musl@0.3.5"), std::string::npos)); // Both remedies from the plan: adapting to the C library, and bringing // the platform dependency into the graph privately. - EXPECT_NE(advice.find("cfg(c-abi = \"musl\")"), std::string::npos); - EXPECT_NE(advice.find("private"), std::string::npos); -} + expect(ne(advice.find("cfg(c-abi = \"musl\")"), std::string::npos)); + expect(ne(advice.find("private"), std::string::npos)); +}}; -TEST(GraphCLibraryIsolationAdvice, DegradesWithoutNamingAnyPackage) { +UTest degrades_without_naming_any_package{"GraphCLibraryIsolationAdvice/DegradesWithoutNamingAnyPackage", [] { // The fast path (execute.cppm) has no BuildPlan to read a name from. The // note still fires — it does not stay silent just because the caller // could not name the library — and it does not fabricate a name either. @@ -2082,9 +2064,9 @@ TEST(GraphCLibraryIsolationAdvice, DegradesWithoutNamingAnyPackage) { "clang -nostdlibinc -c probe.c\n" "probe.c:1:10: fatal error: 'io.h' file not found\n"; auto advice = mcpp::build::graph_c_library_isolation_advice(out); - EXPECT_FALSE(advice.empty()); - EXPECT_NE(advice.find("this target's C library"), std::string::npos); -} + expect(!advice.empty()); + expect(ne(advice.find("this target's C library"), std::string::npos)); +}}; // ── consumer_include_scope_advice (#690, design record F7) ────────────────── // @@ -2105,7 +2087,7 @@ std::filesystem::path consumer_include_fixture() { } } // namespace -TEST(ConsumerIncludeScopeAdvice, NamesTheConsumerDirectoryForEachDriverSpelling) { +UTest names_the_consumer_directory_for_each_driver_spelling{"ConsumerIncludeScopeAdvice/NamesTheConsumerDirectoryForEachDriverSpelling", [] { const auto root = consumer_include_fixture(); const std::vector dirs{root / "absent", root / "appinc"}; const std::string gcc = @@ -2118,40 +2100,40 @@ TEST(ConsumerIncludeScopeAdvice, NamesTheConsumerDirectoryForEachDriverSpelling) "No such file or directory\n"; for (auto const& out : {gcc, clang, cl}) { auto advice = mcpp::build::consumer_include_scope_advice(out, dirs); - ASSERT_FALSE(advice.empty()) << out; - EXPECT_NE(advice.find((root / "appinc").string()), std::string::npos) << advice; - EXPECT_NE(advice.find("no longer"), std::string::npos) << advice; + expect(fatal(!advice.empty())) << out; + expect(ne(advice.find((root / "appinc").string()), std::string::npos)) << advice; + expect(ne(advice.find("no longer"), std::string::npos)) << advice; } std::filesystem::remove_all(root); -} +}}; -TEST(ConsumerIncludeScopeAdvice, SilentWhenTheHeaderIsNotTheConsumersOrNoHeaderIsMissing) { +UTest silent_when_the_header_is_not_the_consumers_or_no_header_is_missing{"ConsumerIncludeScopeAdvice/SilentWhenTheHeaderIsNotTheConsumersOrNoHeaderIsMissing", [] { const auto root = consumer_include_fixture(); const std::vector dirs{root / "appinc"}; // A missing header the consumer does not have either: an ordinary error. - EXPECT_TRUE(mcpp::build::consumer_include_scope_advice( + expect(mcpp::build::consumer_include_scope_advice( "dep.c:1:10: fatal error: other.h: No such file or directory\n", dirs).empty()); // A failure that is not a missing header, although cfg7.h is the consumer's. - EXPECT_TRUE(mcpp::build::consumer_include_scope_advice( + expect(mcpp::build::consumer_include_scope_advice( "dep.c:3:1: error: unknown type name 'cfg7'\n", dirs).empty()); // No root include directories at all (the fast path before any plan wrote // the sidecar). - EXPECT_TRUE(mcpp::build::consumer_include_scope_advice( + expect(mcpp::build::consumer_include_scope_advice( "dep.c:1:10: fatal error: cfg7.h: No such file or directory\n", {}).empty()); std::filesystem::remove_all(root); -} +}}; -TEST(ConsumerIncludeScopeAdvice, SidecarRoundTrips) { +UTest sidecar_round_trips{"ConsumerIncludeScopeAdvice/SidecarRoundTrips", [] { const auto root = consumer_include_fixture(); const std::vector dirs{root / "appinc", root / "with space"}; mcpp::build::write_consumer_include_sidecar(root, dirs); - EXPECT_EQ(mcpp::build::read_consumer_include_sidecar(root), dirs); + expect(eq(mcpp::build::read_consumer_include_sidecar(root), dirs)); mcpp::build::write_consumer_include_sidecar(root, {}); - EXPECT_TRUE(mcpp::build::read_consumer_include_sidecar(root).empty()); + expect(mcpp::build::read_consumer_include_sidecar(root).empty()); std::filesystem::remove_all(root); -} +}}; -TEST(GraphCLibraryIsolationAdvice, DoesNotRewriteTheCompilersOwnLine) { +UTest does_not_rewrite_the_compilers_own_line{"GraphCLibraryIsolationAdvice/DoesNotRewriteTheCompilersOwnLine", [] { // "Reuse the channel, don't rewrite compiler output": the advice is // APPENDED text, and the original diagnostic line survives byte for byte // wherever a caller concatenates it (as both call sites do). @@ -2159,10 +2141,10 @@ TEST(GraphCLibraryIsolationAdvice, DoesNotRewriteTheCompilersOwnLine) { "probe.c:1:10: fatal error: 'io.h' file not found\n"; const std::string out = "clang -nostdlibinc -c probe.c\n" + compilerLine; auto advice = mcpp::build::graph_c_library_isolation_advice(out); - ASSERT_FALSE(advice.empty()); + expect(fatal(!advice.empty())); std::string combined = out + advice; - EXPECT_NE(combined.find(compilerLine), std::string::npos); -} + expect(ne(combined.find(compilerLine), std::string::npos)); +}}; // ═══ mcpp#533 / mcpp#534: emitter self-checks ══════════════════════════════ @@ -2179,7 +2161,7 @@ TEST(GraphCLibraryIsolationAdvice, DoesNotRewriteTheCompilersOwnLine) { // `emit_ninja_string` is supposed never to produce. A test that could only // reach it through a BuildPlan could not show the checker works at all. -TEST(NinjaEmitterGuards, ARuleWhoseCommandStartsWithAnUndefinedVariableIsRefused) { +UTest a_rule_whose_command_starts_with_an_undefined_variable_is_refused{"NinjaEmitterGuards/ARuleWhoseCommandStartsWithAnUndefinedVariableIsRefused", [] { const std::string manifest = "cxx = /usr/bin/g++\n" "\n" @@ -2187,19 +2169,19 @@ TEST(NinjaEmitterGuards, ARuleWhoseCommandStartsWithAnUndefinedVariableIsRefused " command = $cc -shared $in -o $out\n" " description = SHARED $out\n"; auto bad = check_rule_commands_name_a_program(manifest); - ASSERT_TRUE(bad.has_value()) << manifest; - EXPECT_NE(bad->find("c_shared"), std::string::npos) << *bad; - EXPECT_NE(bad->find("$cc"), std::string::npos) << *bad; -} + expect(fatal(bad.has_value())) << manifest; + if (auto&& failed = expect(ne(bad->find("c_shared"), std::string::npos)); !failed) failed << *bad; + if (auto&& failed = expect(ne(bad->find("$cc"), std::string::npos)); !failed) failed << *bad; +}}; -TEST(NinjaEmitterGuards, ARuleWhoseLeadingVariableIsDefinedButEmptyIsRefused) { +UTest a_rule_whose_leading_variable_is_defined_but_empty_is_refused{"NinjaEmitterGuards/ARuleWhoseLeadingVariableIsDefinedButEmptyIsRefused", [] { const std::string manifest = "cc =\n" "\n" "rule c_link\n" " command = $cc $in -o $out\n"; - EXPECT_TRUE(check_rule_commands_name_a_program(manifest).has_value()); -} + expect(check_rule_commands_name_a_program(manifest).has_value()); +}}; // The negatives, which are what stop this from being an always-fires check. // Note especially `$soname_flag`: variables set per-EDGE are legitimately @@ -2208,7 +2190,7 @@ TEST(NinjaEmitterGuards, ARuleWhoseLeadingVariableIsDefinedButEmptyIsRefused) { // not about undefined variables in general — the general form would have // needed an allowlist, and a hand-maintained exception list is the shape this // file has been burned by twice. -TEST(NinjaEmitterGuards, WellFormedRulesAndOptionalPerEdgeVariablesPass) { +UTest well_formed_rules_and_optional_per_edge_variables_pass{"NinjaEmitterGuards/WellFormedRulesAndOptionalPerEdgeVariablesPass", [] { const std::string manifest = "cc = /usr/bin/gcc\n" "cxx = /usr/bin/g++\n" @@ -2223,22 +2205,22 @@ TEST(NinjaEmitterGuards, WellFormedRulesAndOptionalPerEdgeVariablesPass) { " tool = /usr/bin/objcopy\n" " command = $tool -O binary $in $out\n"; auto bad = check_rule_commands_name_a_program(manifest); - EXPECT_FALSE(bad.has_value()) << (bad ? *bad : std::string{}); -} + if (auto&& failed = expect(!bad.has_value()); !failed) failed << (bad ? *bad : std::string{}); +}}; // The emitter's own output must satisfy its own guard — including the case // that produced the defect: a plan whose compile set has no C or asm unit. -TEST(NinjaEmitterGuards, EmittedManifestsAlwaysNameAProgram) { +UTest emitted_manifests_always_name_a_program{"NinjaEmitterGuards/EmittedManifestsAlwaysNameAProgram", [] { auto plan = minimal_plan(); auto ninja = emit_ninja_string(plan); auto bad = check_rule_commands_name_a_program(ninja); - EXPECT_FALSE(bad.has_value()) << (bad ? *bad : std::string{}); + if (auto&& failed = expect(!bad.has_value()); !failed) failed << (bad ? *bad : std::string{}); // `cc` unconditionally, not "when something C is being compiled". - EXPECT_NE(ninja.find("\ncc = "), std::string::npos) + expect(ne(ninja.find("\ncc = "), std::string::npos)) << "cc is not defined in a plan with no C sources — this is mcpp#533\n" << ninja; -} +}}; // ── A package's compiles wait for its generators (mcpp#534) ──────────────── @@ -2268,34 +2250,34 @@ BuildPlan plan_with_generated_header() { } // namespace -TEST(ActionOrdering, AGeneratedHeaderGetsAPhonyAndTheCompileEdgeWaitsForIt) { +UTest a_generated_header_gets_a_phony_and_the_compile_edge_waits_for_it{"ActionOrdering/AGeneratedHeaderGetsAPhonyAndTheCompileEdgeWaitsForIt", [] { auto ninja = emit_ninja_string(plan_with_generated_header()); - EXPECT_NE(ninja.find("build mcpp-actions-gen_pkg : phony"), std::string::npos) + expect(ne(ninja.find("build mcpp-actions-gen_pkg : phony"), std::string::npos)) << ninja; - EXPECT_NE(ninja.find("|| mcpp-actions-gen_pkg"), std::string::npos) + expect(ne(ninja.find("|| mcpp-actions-gen_pkg"), std::string::npos)) << "the compile edge does not wait for the generator\n" << ninja; -} +}}; // THE DENOMINATOR. "every edge that should carry the ordering does" is // vacuously true when no edge does — and that vacuum is exactly the state this // change fixes, so the count of edges is asserted separately from the count // that carries it. A bare equality would have passed against the defect. -TEST(ActionOrdering, EveryCompileEdgeOfThatPackageCarriesIt) { +UTest every_compile_edge_of_that_package_carries_it{"ActionOrdering/EveryCompileEdgeOfThatPackageCarriesIt", [] { auto ninja = emit_ninja_string(plan_with_generated_header()); const auto edges = count_occurrences(ninja, "build obj/uses_header.o :"); const auto ordered = count_occurrences(ninja, "|| mcpp-actions-gen_pkg"); - EXPECT_GT(edges, 0u) << "no compile edge was emitted at all — the " + expect(gt(edges, 0u)) << "no compile edge was emitted at all — the " "assertion below would describe nothing\n" << ninja; - EXPECT_GE(ordered, edges) << ninja; -} + expect(ge(ordered, edges)) << ninja; +}}; // A package with no gating action gets no phony, and its edges are untouched. // Without this the change would read as "always add an order-only edge", which // is not what it does and would serialise builds that have nothing to wait for. -TEST(ActionOrdering, APackageWithNoActionsIsUnchanged) { +UTest a_package_with_no_actions_is_unchanged{"ActionOrdering/APackageWithNoActionsIsUnchanged", [] { auto plan = minimal_plan(); plan.compileUnits.push_back({ .source = "src/plain.cpp", @@ -2304,14 +2286,14 @@ TEST(ActionOrdering, APackageWithNoActionsIsUnchanged) { .packageName = "plain_pkg", }); auto ninja = emit_ninja_string(plan); - EXPECT_EQ(ninja.find("mcpp-actions-plain_pkg"), std::string::npos) << ninja; -} + expect(eq(ninja.find("mcpp-actions-plain_pkg"), std::string::npos)) << ninja; +}}; // `Object` and `Artifact` really are ordered by file dependency — their // outputs are link inputs and their inputs are link outputs — so pulling them // into the phony would add an edge that expresses nothing. This is the half of // the original comment that was true, and it has to stay true. -TEST(ActionOrdering, ObjectRoleActionsDoNotJoinTheCompileGate) { +UTest object_role_actions_do_not_join_the_compile_gate{"ActionOrdering/ObjectRoleActionsDoNotJoinTheCompileGate", [] { auto plan = minimal_plan(); plan.compileUnits.push_back({ .source = "src/x.cpp", @@ -2328,14 +2310,14 @@ TEST(ActionOrdering, ObjectRoleActionsDoNotJoinTheCompileGate) { plan.actions.push_back(std::move(a)); auto ninja = emit_ninja_string(plan); - EXPECT_EQ(ninja.find("mcpp-actions-obj_pkg"), std::string::npos) << ninja; -} + expect(eq(ninja.find("mcpp-actions-obj_pkg"), std::string::npos)) << ninja; +}}; // A non-blocking check runs alongside compilation; a blocking one precedes it. // `blocking` was typed, transported, parsed, documented in two languages and // demonstrated in a shipped example — and read by nothing, which made it a // no-op with a paper trail. -TEST(ActionOrdering, BlockingDecidesWhetherACheckGatesTheCompile) { +UTest blocking_decides_whether_a_check_gates_the_compile{"ActionOrdering/BlockingDecidesWhetherACheckGatesTheCompile", [] { auto make = [](bool blocking) { auto plan = minimal_plan(); plan.compileUnits.push_back({ @@ -2355,11 +2337,11 @@ TEST(ActionOrdering, BlockingDecidesWhetherACheckGatesTheCompile) { return emit_ninja_string(plan); }; - EXPECT_NE(make(true).find("|| mcpp-actions-chk_pkg"), std::string::npos) + expect(ne(make(true).find("|| mcpp-actions-chk_pkg"), std::string::npos)) << "blocking = true did not gate the compile"; - EXPECT_EQ(make(false).find("mcpp-actions-chk_pkg"), std::string::npos) + expect(eq(make(false).find("mcpp-actions-chk_pkg"), std::string::npos)) << "blocking = false gated the compile anyway"; -} +}}; // ── depfile: ninja must track what the action's COMMAND read, not only what // the build program declared as an input ────────────────────────────────── @@ -2389,7 +2371,7 @@ std::string action_rule_block(std::string_view ninja, std::string_view rule_head } // namespace -TEST(ActionDepfile, ADeclaredDepfileEmitsDepfileAndDepsGcc) { +UTest a_declared_depfile_emits_depfile_and_deps_gcc{"ActionDepfile/ADeclaredDepfileEmitsDepfileAndDepsGcc", [] { auto plan = minimal_plan(); plan.compileUnits.push_back({ .source = "src/shader_user.cpp", @@ -2408,27 +2390,27 @@ TEST(ActionDepfile, ADeclaredDepfileEmitsDepfileAndDepsGcc) { auto ninja = emit_ninja_string(plan); auto block = action_rule_block(ninja, "rule mcpp_action_0\n"); - ASSERT_FALSE(block.empty()) << "no rule emitted for the action\n" << ninja; - EXPECT_NE(block.find("\n depfile = out/shader.spv.d\n"), std::string::npos) + expect(fatal(!block.empty())) << "no rule emitted for the action\n" << ninja; + expect(ne(block.find("\n depfile = out/shader.spv.d\n"), std::string::npos)) << block; // No trailing "\n" required: `deps = gcc` is the last line this backend // emits for the rule before the blank separator, so the block extracted // above (up to, not including, that separator) ends exactly here. - EXPECT_NE(block.find("\n deps = gcc"), std::string::npos) << block; + expect(ne(block.find("\n deps = gcc"), std::string::npos)) << block; // The depfile must never also be a declared ninja OUTPUT of the same // edge: `deps = gcc` makes ninja consume and delete it once read, and a // file simultaneously promised as an output would be one ninja expects // to still exist afterward. - EXPECT_EQ(ninja.find("shader.spv.d :"), std::string::npos) + expect(eq(ninja.find("shader.spv.d :"), std::string::npos)) << "the depfile must not appear as a build edge's own output\n" << ninja; -} +}}; // THE CONTROL. Without it, a backend that emitted `depfile =`/`deps = gcc` // unconditionally for every action would also pass the test above — exactly // the shape `blocking` took before anything read it (mcpp#534 / e2e 315): // typed, transported, parsed, and a no-op with a paper trail. -TEST(ActionDepfile, NoDepfileEmitsNeitherLine) { +UTest no_depfile_emits_neither_line{"ActionDepfile/NoDepfileEmitsNeitherLine", [] { auto plan = minimal_plan(); plan.compileUnits.push_back({ .source = "src/plain_user.cpp", @@ -2446,10 +2428,10 @@ TEST(ActionDepfile, NoDepfileEmitsNeitherLine) { auto ninja = emit_ninja_string(plan); auto block = action_rule_block(ninja, "rule mcpp_action_0\n"); - ASSERT_FALSE(block.empty()) << "no rule emitted for the action\n" << ninja; - EXPECT_EQ(block.find("depfile ="), std::string::npos) << block; - EXPECT_EQ(block.find("deps = gcc"), std::string::npos) << block; -} + expect(fatal(!block.empty())) << "no rule emitted for the action\n" << ninja; + expect(eq(block.find("depfile ="), std::string::npos)) << block; + expect(eq(block.find("deps = gcc"), std::string::npos)) << block; +}}; // A module-extension file need not provide a module: an implementation unit // (`module M;`) is a legal inhabitant of a `.cppm` and provides nothing @@ -2464,7 +2446,7 @@ TEST(ActionDepfile, NoDepfileEmitsNeitherLine) { // in module declaration`. GCC's interface spelling is the // plain language, so the identical project built on GCC // and not on Clang. -TEST(NinjaBackend, ModuleEdgeFlagsComeFromTheScanNotTheExtension) { +UTest module_edge_flags_come_from_the_scan_not_the_extension{"NinjaBackend/ModuleEdgeFlagsComeFromTheScanNotTheExtension", [] { auto plan = minimal_plan(); plan.toolchain.compiler = mcpp::toolchain::CompilerId::Clang; plan.toolchain.binaryPath = "/usr/bin/clang++"; @@ -2488,12 +2470,12 @@ TEST(NinjaBackend, ModuleEdgeFlagsComeFromTheScanNotTheExtension) { // being undone by someone restoring a rule-level constant that "works" // for every edge that happens to provide a module. auto rule_start = ninja.find("rule cxx_module\n"); - ASSERT_NE(rule_start, std::string::npos) << ninja; + expect(fatal(ne(rule_start, std::string::npos))) << ninja; auto rule = ninja.substr(rule_start, ninja.find("\n\n", rule_start) - rule_start); - EXPECT_NE(rule.find("$module_output $module_lang"), std::string::npos) << rule; - EXPECT_EQ(rule.find("-fmodule-output=$bmi_out"), std::string::npos) + expect(ne(rule.find("$module_output $module_lang"), std::string::npos)) << rule; + expect(eq(rule.find("-fmodule-output=$bmi_out"), std::string::npos)) << "the BMI flag is per-edge; a rule-level one reaches edges with no BMI"; - EXPECT_EQ(rule.find("-x c++-module"), std::string::npos) + expect(eq(rule.find("-x c++-module"), std::string::npos)) << "the interface spelling is per-edge, not a property of the rule"; // EVERY cxx_module EDGE BINDS module_lang, AND THE DENOMINATOR IS ASSERTED. @@ -2507,26 +2489,25 @@ TEST(NinjaBackend, ModuleEdgeFlagsComeFromTheScanNotTheExtension) { ++with_lang; pos = end; } - ASSERT_EQ(edges, 2u) << ninja; - EXPECT_EQ(with_lang, edges) << ninja; + expect(fatal(eq(edges, 2u))) << ninja; + expect(eq(with_lang, edges)) << ninja; // The interface: told it is an interface, and given somewhere to put the BMI. - EXPECT_NE(ninja.find(" module_lang = -x c++-module\n"), std::string::npos) << ninja; - EXPECT_NE(ninja.find(" module_output = -fmodule-output=pcm.cache/iface.pcm\n"), - std::string::npos) << ninja; + expect(ne(ninja.find(" module_lang = -x c++-module\n"), std::string::npos)) << ninja; + expect(ne(ninja.find(" module_output = -fmodule-output=pcm.cache/iface.pcm\n"), std::string::npos)) << ninja; // The implementation unit: told it is C++, and given no BMI path at all. - EXPECT_NE(ninja.find(" module_lang = -x c++\n"), std::string::npos) << ninja; - EXPECT_EQ(count_occurrences(ninja, " module_output ="), 1u) + expect(ne(ninja.find(" module_lang = -x c++\n"), std::string::npos)) << ninja; + expect(eq(count_occurrences(ninja, " module_output ="), 1u)) << "only the unit that provides a module names a BMI"; // The empty-value spelling must not appear anywhere in the graph. - EXPECT_EQ(ninja.find("-fmodule-output= "), std::string::npos) << ninja; - EXPECT_EQ(ninja.find("-fmodule-output=\n"), std::string::npos) << ninja; -} + expect(eq(ninja.find("-fmodule-output= "), std::string::npos)) << ninja; + expect(eq(ninja.find("-fmodule-output=\n"), std::string::npos)) << ninja; +}}; // GCC needs no explicit module output at all, so the same two units must emit // `module_lang` twice and `module_output` never. Without this the test above // would leave "the GCC branch still emits a flag it does not want" unmeasured. -TEST(NinjaBackend, GccModuleEdgesCarryTheLanguageAndNoBmiFlag) { +UTest gcc_module_edges_carry_the_language_and_no_bmi_flag{"NinjaBackend/GccModuleEdgesCarryTheLanguageAndNoBmiFlag", [] { auto plan = minimal_plan(); // GCC plan.compileUnits.push_back({ .source = "src/iface.cppm", @@ -2543,15 +2524,15 @@ TEST(NinjaBackend, GccModuleEdgesCarryTheLanguageAndNoBmiFlag) { }); auto ninja = emit_ninja_string(plan); - EXPECT_EQ(count_occurrences(ninja, " module_lang = -x c++\n"), 2u) << ninja; - EXPECT_EQ(count_occurrences(ninja, " module_output ="), 0u) << ninja; -} + expect(eq(count_occurrences(ninja, " module_lang = -x c++\n"), 2u)) << ninja; + expect(eq(count_occurrences(ninja, " module_output ="), 0u)) << ninja; +}}; // The third dialect, and the one where the empty value was worst. `/ifcOutput` // takes its path as a SEPARATE token, so `/ifcOutput ` followed by nothing // consumed whatever came next on the command line -- the observed failure was // `could not open output file '/interface'`, naming a flag as a filename. -TEST(NinjaBackend, MsvcModuleEdgesSplitTheInterfaceFlagFromTheLanguageFlag) { +UTest msvc_module_edges_split_the_interface_flag_from_the_language_flag{"NinjaBackend/MsvcModuleEdgesSplitTheInterfaceFlagFromTheLanguageFlag", [] { auto plan = minimal_plan(); plan.toolchain.compiler = mcpp::toolchain::CompilerId::MSVC; plan.toolchain.binaryPath = "cl.exe"; @@ -2573,14 +2554,14 @@ TEST(NinjaBackend, MsvcModuleEdgesSplitTheInterfaceFlagFromTheLanguageFlag) { // `/interface /TP` says both "this is an interface" and "this is C++"; a // unit that is not an interface keeps only the second half. - EXPECT_NE(ninja.find(" module_lang = /interface /TP\n"), std::string::npos) << ninja; - EXPECT_NE(ninja.find(" module_lang = /TP\n"), std::string::npos) << ninja; - EXPECT_EQ(count_occurrences(ninja, " module_lang = /interface /TP\n"), 1u) << ninja; + expect(ne(ninja.find(" module_lang = /interface /TP\n"), std::string::npos)) << ninja; + expect(ne(ninja.find(" module_lang = /TP\n"), std::string::npos)) << ninja; + expect(eq(count_occurrences(ninja, " module_lang = /interface /TP\n"), 1u)) << ninja; // Exactly one edge names an .ifc, and it is the one that provides. - EXPECT_EQ(count_occurrences(ninja, " module_output = /ifcOutput "), 1u) << ninja; + expect(eq(count_occurrences(ninja, " module_output = /ifcOutput "), 1u)) << ninja; // The trailing-space spelling with nothing after it must not exist. - EXPECT_EQ(ninja.find("/ifcOutput \n"), std::string::npos) << ninja; -} + expect(eq(ninja.find("/ifcOutput \n"), std::string::npos)) << ninja; +}}; // ── #618: `windows_subsystem` / `windows_entry` ───────────────────────────── // @@ -2599,26 +2580,26 @@ using Flags = std::vector; } // namespace -TEST(NinjaBackend, WindowsKeysRenderNothingOffPe) { +UTest windows_keys_render_nothing_off_pe{"NinjaBackend/WindowsKeysRenderNothingOffPe", [] { for (auto triple : {"x86_64-linux-gnu", "x86_64-linux-musl", "aarch64-macos"}) { auto plan = plan_for_triple(triple); for (bool sep : {false, true}) - EXPECT_EQ(windows_executable_link_flags(plan, sep, "windows", "wWinMain"), Flags{}) + expect(eq(windows_executable_link_flags(plan, sep, "windows", "wWinMain"), Flags{})) << triple; } -} +}}; -TEST(NinjaBackend, WindowsDefaultsRenderNothingOnEitherAbi) { +UTest windows_defaults_render_nothing_on_either_abi{"NinjaBackend/WindowsDefaultsRenderNothingOnEitherAbi", [] { for (auto triple : {"x86_64-windows-msvc", "x86_64-windows-gnu"}) { auto plan = plan_for_triple(triple); for (auto [subsystem, entry] : {std::pair{"", ""}, std::pair{"console", ""}, std::pair{"", "main"}, std::pair{"console", "main"}}) - EXPECT_EQ(windows_executable_link_flags(plan, false, subsystem, entry), Flags{}) + expect(eq(windows_executable_link_flags(plan, false, subsystem, entry), Flags{})) << triple << " " << subsystem << " " << entry; } -} +}}; -TEST(NinjaBackend, WindowsMsvcAbiStatesBothSubsystemAndCrtEntry) { +UTest windows_msvc_abi_states_both_subsystem_and_crt_entry{"NinjaBackend/WindowsMsvcAbiStatesBothSubsystemAndCrtEntry", [] { auto plan = plan_for_triple("x86_64-windows-msvc"); const std::tuple rows[] = { {"windows", "", {"/SUBSYSTEM:WINDOWS", "/ENTRY:mainCRTStartup"}}, @@ -2631,17 +2612,17 @@ TEST(NinjaBackend, WindowsMsvcAbiStatesBothSubsystemAndCrtEntry) { }; for (auto const& [subsystem, entry, expected] : rows) { // link.exe invoked directly takes the flag bare... - EXPECT_EQ(windows_executable_link_flags(plan, true, subsystem, entry), expected) + expect(eq(windows_executable_link_flags(plan, true, subsystem, entry), expected)) << subsystem << " " << entry; // ...and a GNU-style driver (clang targeting the MSVC ABI) passes it on. Flags wrapped; for (auto const& f : expected) wrapped.push_back("-Wl," + f); - EXPECT_EQ(windows_executable_link_flags(plan, false, subsystem, entry), wrapped) + expect(eq(windows_executable_link_flags(plan, false, subsystem, entry), wrapped)) << subsystem << " " << entry; } -} +}}; -TEST(NinjaBackend, WindowsGnuAbiUsesTheDriverFlags) { +UTest windows_gnu_abi_uses_the_driver_flags{"NinjaBackend/WindowsGnuAbiUsesTheDriverFlags", [] { auto plan = plan_for_triple("x86_64-windows-gnu"); const std::tuple rows[] = { {"windows", "", {"-mwindows"}}, @@ -2652,13 +2633,13 @@ TEST(NinjaBackend, WindowsGnuAbiUsesTheDriverFlags) { {"", "WinMain", {}}, }; for (auto const& [subsystem, entry, expected] : rows) - EXPECT_EQ(windows_executable_link_flags(plan, false, subsystem, entry), expected) + expect(eq(windows_executable_link_flags(plan, false, subsystem, entry), expected)) << subsystem << " " << entry; -} +}}; // Through the emitted graph: the flags reach the declaring executable's link // edge and no other edge, including a test binary of the same package. -TEST(NinjaBackend, WindowsSubsystemReachesOnlyTheDeclaringExecutable) { +UTest windows_subsystem_reaches_only_the_declaring_executable{"NinjaBackend/WindowsSubsystemReachesOnlyTheDeclaringExecutable", [] { auto plan = plan_for_triple("x86_64-windows-gnu"); plan.compileUnits.push_back({ .source = "src/gui.cpp", @@ -2700,14 +2681,14 @@ TEST(NinjaBackend, WindowsSubsystemReachesOnlyTheDeclaringExecutable) { plan.linkUnits.push_back(test); auto ninja = emit_ninja_string(plan); - EXPECT_EQ(count_occurrences(ninja, "-mwindows"), 1u) << ninja; + expect(eq(count_occurrences(ninja, "-mwindows"), 1u)) << ninja; const auto guiEdge = ninja.find("build bin/gui.exe"); - ASSERT_NE(guiEdge, std::string::npos) << ninja; + expect(fatal(ne(guiEdge, std::string::npos))) << ninja; const auto nextEdge = ninja.find("\nbuild ", guiEdge + 1); const auto flag = ninja.find("-mwindows"); - EXPECT_GT(flag, guiEdge) << ninja; - EXPECT_LT(flag, nextEdge) << ninja; -} + expect(gt(flag, guiEdge)) << ninja; + expect(lt(flag, nextEdge)) << ninja; +}}; // #694 (W1d): THE LEVEL A PROFILE DECLARES IS THE LEVEL EVERY ROW REALISES. // @@ -2718,7 +2699,7 @@ TEST(NinjaBackend, WindowsSubsystemReachesOnlyTheDeclaringExecutable) { // compilers, so a later branch that overrides a declared level fails here and // not in a published binary. `realised_opt_level` is the answer both the flags // and the `Finished` line read, so it is asserted against the same rows. -TEST(NinjaBackendOptimization, EveryTargetRowRealisesTheDeclaredLevel) { +UTest every_target_row_realises_the_declared_level{"NinjaBackendOptimization/EveryTargetRowRealisesTheDeclaredLevel", [] { using mcpp::toolchain::CompilerId; for (auto const& row : mcpp::toolchain::triple::kKnownTargets) { for (auto compiler : {CompilerId::GCC, CompilerId::Clang}) { @@ -2735,21 +2716,21 @@ TEST(NinjaBackendOptimization, EveryTargetRowRealisesTheDeclaredLevel) { const std::string where = std::format( "target={} compiler={} opt={}", row.canonical, static_cast(compiler), level); - EXPECT_NE(f.cxx.find(want), std::string::npos) << where << "\n" << f.cxx; - EXPECT_NE(f.cc.find(want), std::string::npos) << where << "\n" << f.cc; - EXPECT_EQ(f.cxx.find(" -Og"), std::string::npos) << where << "\n" << f.cxx; - EXPECT_EQ(f.cc.find(" -Og"), std::string::npos) << where << "\n" << f.cc; - EXPECT_EQ(realised_opt_level(plan.manifest.buildConfig), level) << where; - EXPECT_EQ(realises_optimization(plan.manifest.buildConfig), level != "0") + expect(ne(f.cxx.find(want), std::string::npos)) << where << "\n" << f.cxx; + expect(ne(f.cc.find(want), std::string::npos)) << where << "\n" << f.cc; + expect(eq(f.cxx.find(" -Og"), std::string::npos)) << where << "\n" << f.cxx; + expect(eq(f.cc.find(" -Og"), std::string::npos)) << where << "\n" << f.cc; + expect(eq(realised_opt_level(plan.manifest.buildConfig), level)) << where; + expect(eq(realises_optimization(plan.manifest.buildConfig), level != "0")) << where; } } } -} +}}; // The MSVC dialect spells the same levels its own way: `/Od` for zero (there is // no `/O0`) and `/O` otherwise. -TEST(NinjaBackendOptimization, MsvcDialectRealisesTheDeclaredLevel) { +UTest msvc_dialect_realises_the_declared_level{"NinjaBackendOptimization/MsvcDialectRealisesTheDeclaredLevel", [] { for (std::string level : {"0", "1", "2"}) { auto plan = minimal_plan(); plan.toolchain.compiler = mcpp::toolchain::CompilerId::MSVC; @@ -2758,23 +2739,23 @@ TEST(NinjaBackendOptimization, MsvcDialectRealisesTheDeclaredLevel) { plan.manifest.buildConfig.optLevel = level; auto f = compute_flags(plan); const std::string want = level == "0" ? " /Od" : " /O" + level; - EXPECT_NE(f.cxx.find(want), std::string::npos) << level << "\n" << f.cxx; - EXPECT_NE(f.cc.find(want), std::string::npos) << level << "\n" << f.cc; + expect(ne(f.cxx.find(want), std::string::npos)) << level << "\n" << f.cxx; + expect(ne(f.cc.find(want), std::string::npos)) << level << "\n" << f.cc; } -} +}}; // An empty level is zero, for the flags and for the `Finished` line alike. The // old spelling rendered it as a bare `-O`, which GCC reads as `-O1`, while the // line called the build unoptimized. -TEST(NinjaBackendOptimization, AnEmptyLevelIsZeroForFlagsAndDescriptor) { +UTest an_empty_level_is_zero_for_flags_and_descriptor{"NinjaBackendOptimization/AnEmptyLevelIsZeroForFlagsAndDescriptor", [] { auto plan = minimal_plan(); plan.manifest.buildConfig.optLevel.clear(); auto f = compute_flags(plan); - EXPECT_NE(f.cxx.find(" -O0"), std::string::npos) << f.cxx; - EXPECT_EQ(f.cxx.find(" -O "), std::string::npos) << f.cxx; - EXPECT_EQ(realised_opt_level(plan.manifest.buildConfig), "0"); - EXPECT_FALSE(realises_optimization(plan.manifest.buildConfig)); -} + expect(ne(f.cxx.find(" -O0"), std::string::npos)) << f.cxx; + expect(eq(f.cxx.find(" -O "), std::string::npos)) << f.cxx; + expect(eq(realised_opt_level(plan.manifest.buildConfig), "0")); + expect(!realises_optimization(plan.manifest.buildConfig)); +}}; // #695 and #690 (C2): THE FILE-LEVEL FLAGS CARRY NOTHING A PACKAGE DECLARES FOR // ITSELF. @@ -2784,7 +2765,7 @@ TEST(NinjaBackendOptimization, AnEmptyLevelIsZeroForFlagsAndDescriptor) { // that channel until #691, and the root's C standard until #695. The root here // declares every package-private key; none of those values may appear in the // file-level flags. The C standard there is the engine's constant. -TEST(NinjaBackendScope, TheFileLevelFlagsCarryNoPackagePrivateValue) { +UTest the_file_level_flags_carry_no_package_private_value{"NinjaBackendScope/TheFileLevelFlagsCarryNoPackagePrivateValue", [] { auto plan = minimal_plan(); auto& bc = plan.manifest.buildConfig; bc.cStandard = "gnu11"; @@ -2796,12 +2777,12 @@ TEST(NinjaBackendScope, TheFileLevelFlagsCarryNoPackagePrivateValue) { auto f = compute_flags(plan); for (auto const* line : {&f.cc, &f.cxx}) { - EXPECT_EQ(line->find("gnu11"), std::string::npos) << *line; - EXPECT_EQ(line->find("/root-private/include"), std::string::npos) << *line; - EXPECT_EQ(line->find("ROOT_ONLY"), std::string::npos) << *line; + if (auto&& failed = expect(eq(line->find("gnu11"), std::string::npos)); !failed) failed << *line; + if (auto&& failed = expect(eq(line->find("/root-private/include"), std::string::npos)); !failed) failed << *line; + if (auto&& failed = expect(eq(line->find("ROOT_ONLY"), std::string::npos)); !failed) failed << *line; } - EXPECT_NE(f.cc.find("-std=c11"), std::string::npos) << f.cc; -} + expect(ne(f.cc.find("-std=c11"), std::string::npos)) << f.cc; +}}; // #696 (W2b): AN ELF LINK OVER A GRAPH-SUPPLIED C LIBRARY SEARCHES NOTHING OF // THE HOST'S. With no sysroot, clang derived its library search from `/` and, @@ -2825,42 +2806,42 @@ BuildPlan graph_c_library_plan(std::string_view triple, } } // namespace -TEST(NinjaBackendGraphLink, AnElfGraphLinkByClangCarriesTheEmptySysroot) { +UTest an_elf_graph_link_by_clang_carries_the_empty_sysroot{"NinjaBackendGraphLink/AnElfGraphLinkByClangCarriesTheEmptySysroot", [] { for (auto triple : {"x86_64-linux-musl", "aarch64-linux-musl"}) { auto plan = graph_c_library_plan(triple, mcpp::toolchain::CompilerId::Clang); auto f = compute_flags(plan); - EXPECT_TRUE(f.graphLinkIsolated) << triple; - EXPECT_NE(f.ld.find("--sysroot="), std::string::npos) << triple << "\n" << f.ld; - EXPECT_NE(f.ld.find(std::string(kGraphLinkSysrootDir)), std::string::npos) + expect(bool(f.graphLinkIsolated)) << triple; + expect(ne(f.ld.find("--sysroot="), std::string::npos)) << triple << "\n" << f.ld; + expect(ne(f.ld.find(std::string(kGraphLinkSysrootDir)), std::string::npos)) << triple << "\n" << f.ld; } -} +}}; // The measured case is clang on ELF; GCC over the graph and PE links keep their // line until they are measured, and a payload C library keeps its own sysroot. -TEST(NinjaBackendGraphLink, OtherLinksDoNotCarryTheEmptySysroot) { +UTest other_links_do_not_carry_the_empty_sysroot{"NinjaBackendGraphLink/OtherLinksDoNotCarryTheEmptySysroot", [] { { auto plan = graph_c_library_plan("x86_64-linux-musl", mcpp::toolchain::CompilerId::GCC); auto f = compute_flags(plan); - EXPECT_FALSE(f.graphLinkIsolated); - EXPECT_EQ(f.ld.find(std::string(kGraphLinkSysrootDir)), std::string::npos) << f.ld; + expect(!f.graphLinkIsolated); + expect(eq(f.ld.find(std::string(kGraphLinkSysrootDir)), std::string::npos)) << f.ld; } { auto plan = graph_c_library_plan("x86_64-windows-musl", mcpp::toolchain::CompilerId::Clang); auto f = compute_flags(plan); - EXPECT_FALSE(f.graphLinkIsolated); - EXPECT_EQ(f.ld.find(std::string(kGraphLinkSysrootDir)), std::string::npos) << f.ld; + expect(!f.graphLinkIsolated); + expect(eq(f.ld.find(std::string(kGraphLinkSysrootDir)), std::string::npos)) << f.ld; } { auto plan = minimal_plan(); auto f = compute_flags(plan); - EXPECT_FALSE(f.graphLinkIsolated); - EXPECT_EQ(f.ld.find(std::string(kGraphLinkSysrootDir)), std::string::npos) << f.ld; + expect(!f.graphLinkIsolated); + expect(eq(f.ld.find(std::string(kGraphLinkSysrootDir)), std::string::npos)) << f.ld; } -} +}}; // #696: the note for an unanswered `-l` on an isolated graph link. -TEST(GraphLinkLibraryAdvice, NamesTheLibraryAndTheMuslRelease) { +UTest names_the_library_and_the_musl_release{"GraphLinkLibraryAdvice/NamesTheLibraryAndTheMuslRelease", [] { const std::string out = "FAILED: bin/app\n" "clang++ @bin/app.rsp -o bin/app --sysroot=/p/target/x/graph-sysroot -lm -lpthread\n" @@ -2868,29 +2849,29 @@ TEST(GraphLinkLibraryAdvice, NamesTheLibraryAndTheMuslRelease) { "ld.lld: error: unable to find library -lpthread\n" "ld.lld: error: unable to find library -lm\n"; auto advice = graph_link_library_advice(out, "musl", "openkal-musl@0.19.1"); - EXPECT_NE(advice.find("musl (openkal-musl@0.19.1)"), std::string::npos) << advice; - EXPECT_NE(advice.find("-lm, -lpthread"), std::string::npos) << advice; - EXPECT_NE(advice.find("openkal-musl 0.19.2"), std::string::npos) << advice; - EXPECT_NE(advice.find("mcpp#696"), std::string::npos) << advice; -} + expect(ne(advice.find("musl (openkal-musl@0.19.1)"), std::string::npos)) << advice; + expect(ne(advice.find("-lm, -lpthread"), std::string::npos)) << advice; + expect(ne(advice.find("openkal-musl 0.19.2"), std::string::npos)) << advice; + expect(ne(advice.find("mcpp#696"), std::string::npos)) << advice; +}}; -TEST(GraphLinkLibraryAdvice, AnotherLibraryGetsNoMuslParagraph) { +UTest another_library_gets_no_musl_paragraph{"GraphLinkLibraryAdvice/AnotherLibraryGetsNoMuslParagraph", [] { const std::string out = "clang++ -o bin/app --sysroot=/p/target/x/graph-sysroot -lz\n" "ld.lld: error: unable to find library -lz\n"; auto advice = graph_link_library_advice(out, "musl", "openkal-musl@0.19.2"); - EXPECT_NE(advice.find("-lz"), std::string::npos) << advice; - EXPECT_EQ(advice.find("openkal-musl 0.19.2 and later"), std::string::npos) << advice; -} + expect(ne(advice.find("-lz"), std::string::npos)) << advice; + expect(eq(advice.find("openkal-musl 0.19.2 and later"), std::string::npos)) << advice; +}}; -TEST(GraphLinkLibraryAdvice, StaysSilentWithoutTheGraphSysroot) { +UTest stays_silent_without_the_graph_sysroot{"GraphLinkLibraryAdvice/StaysSilentWithoutTheGraphSysroot", [] { const std::string out = "clang++ -o bin/app -lm\n" "ld.lld: error: unable to find library -lm\n"; - EXPECT_TRUE(graph_link_library_advice(out, "musl", "openkal-musl@0.19.1").empty()); - EXPECT_TRUE(graph_link_library_advice( + expect(graph_link_library_advice(out, "musl", "openkal-musl@0.19.1").empty()); + expect(graph_link_library_advice( "clang++ --sysroot=/p/graph-sysroot\nld.lld: error: undefined symbol: foo\n").empty()); -} +}}; // ─── #693 (W4d): the encodings the tools read ─────────────────────────────── // @@ -2915,7 +2896,7 @@ std::string rsp_line(const std::string& ninja, std::string_view rule) { } // namespace -TEST(NinjaBackendEncoding, MsvcResponseFilesBeginWithTheByteOrderMark) { +UTest msvc_response_files_begin_with_the_byte_order_mark{"NinjaBackendEncoding/MsvcResponseFilesBeginWithTheByteOrderMark", [] { auto plan = minimal_plan(); plan.toolchain.compiler = mcpp::toolchain::CompilerId::MSVC; plan.toolchain.binaryPath = "cl.exe"; @@ -2938,12 +2919,12 @@ TEST(NinjaBackendEncoding, MsvcResponseFilesBeginWithTheByteOrderMark) { "rule cxx_link\n", "rule cxx_archive\n", "rule cxx_shared\n"}) { auto line = rsp_line(ninja, rule); - ASSERT_FALSE(line.empty()) << rule << ninja; - EXPECT_NE(line.find("= \xEF\xBB\xBF"), std::string::npos) << rule << line; + expect(fatal(!line.empty())) << rule << ninja; + expect(ne(line.find("= \xEF\xBB\xBF"), std::string::npos)) << rule << line; } -} +}}; -TEST(NinjaBackendEncoding, GnuResponseFilesCarryNoByteOrderMark) { +UTest gnu_response_files_carry_no_byte_order_mark{"NinjaBackendEncoding/GnuResponseFilesCarryNoByteOrderMark", [] { auto plan = minimal_plan(); plan.compileUnits.push_back({ .source = "src/a.c", @@ -2952,40 +2933,40 @@ TEST(NinjaBackendEncoding, GnuResponseFilesCarryNoByteOrderMark) { .packageName = "objc_rule_test", }); auto ninja = emit_ninja_string(plan); - EXPECT_FALSE(rsp_line(ninja, "rule cxx_link\n").empty()) << ninja; - EXPECT_EQ(ninja.find("\xEF\xBB\xBF"), std::string::npos) << ninja; -} + expect(!(rsp_line(ninja, "rule cxx_link\n").empty())) << ninja; + expect(eq(ninja.find("\xEF\xBB\xBF"), std::string::npos)) << ninja; +}}; // Ninja reports the encoding it reads build.ninja in; mcpp writes UTF-8. -TEST(NinjaBackendEncoding, ANinjaThatReadsUtf8Passes) { - EXPECT_FALSE(ninja_encoding_mismatch("Build file encoding: UTF-8\r\n", 65001, - "ninja.exe").has_value()); - EXPECT_FALSE(ninja_encoding_mismatch("Build file encoding: UTF-8\n", 1252, - "ninja.exe").has_value()); -} - -TEST(NinjaBackendEncoding, ANinjaThatReadsAnsiIsRefusedByName) { +UTest a_ninja_that_reads_utf8_passes{"NinjaBackendEncoding/ANinjaThatReadsUtf8Passes", [] { + expect(!(ninja_encoding_mismatch("Build file encoding: UTF-8\r\n", 65001, + "ninja.exe").has_value())); + expect(!(ninja_encoding_mismatch("Build file encoding: UTF-8\n", 1252, + "ninja.exe").has_value())); +}}; + +UTest a_ninja_that_reads_ansi_is_refused_by_name{"NinjaBackendEncoding/ANinjaThatReadsAnsiIsRefusedByName", [] { auto modern = ninja_encoding_mismatch("Build file encoding: ANSI\r\n", 65001, "C:/tools/ninja.exe"); - ASSERT_TRUE(modern.has_value()); - EXPECT_NE(modern->find("C:/tools/ninja.exe"), std::string::npos) << *modern; - EXPECT_NE(modern->find("ANSI"), std::string::npos) << *modern; - EXPECT_NE(modern->find("1.11"), std::string::npos) << *modern; + expect(fatal(modern.has_value())); + if (auto&& failed = expect(ne(modern->find("C:/tools/ninja.exe"), std::string::npos)); !failed) failed << *modern; + if (auto&& failed = expect(ne(modern->find("ANSI"), std::string::npos)); !failed) failed << *modern; + if (auto&& failed = expect(ne(modern->find("1.11"), std::string::npos)); !failed) failed << *modern; // On a host that ignores the declaration the advice names the code page. auto legacy = ninja_encoding_mismatch("Build file encoding: ANSI\n", 936, "ninja.exe"); - ASSERT_TRUE(legacy.has_value()); - EXPECT_NE(legacy->find("936"), std::string::npos) << *legacy; -} + expect(fatal(legacy.has_value())); + if (auto&& failed = expect(ne(legacy->find("936"), std::string::npos)); !failed) failed << *legacy; +}}; -TEST(NinjaBackendEncoding, OutputThatNamesNoEncodingDecidesNothing) { - EXPECT_FALSE(ninja_encoding_mismatch("", 65001, "ninja").has_value()); - EXPECT_FALSE(ninja_encoding_mismatch("ninja: error: unknown tool 'wincodepage'", - 65001, "ninja").has_value()); -} +UTest output_that_names_no_encoding_decides_nothing{"NinjaBackendEncoding/OutputThatNamesNoEncodingDecidesNothing", [] { + expect(!(ninja_encoding_mismatch("", 65001, "ninja").has_value())); + expect(!(ninja_encoding_mismatch("ninja: error: unknown tool 'wincodepage'", + 65001, "ninja").has_value())); +}}; -TEST(NinjaBackend, PeExportsPatternsAndAConsumerReachTheDefEdge) { +UTest pe_exports_patterns_and_a_consumer_reach_the_def_edge{"NinjaBackend/PeExportsPatternsAndAConsumerReachTheDefEdge", [] { auto plan = minimal_plan(); plan.toolchain.compiler = mcpp::toolchain::CompilerId::MSVC; plan.toolchain.targetTriple = "x86_64-pc-windows-msvc"; @@ -3001,13 +2982,12 @@ TEST(NinjaBackend, PeExportsPatternsAndAConsumerReachTheDefEdge) { dll.objects = {"obj/probe.o"}; plan.linkUnits.push_back(dll); auto alone = emit_ninja_string(plan); - EXPECT_NE(alone.find("build bin/probe.def : coff_def obj/probe.o | obj/probe.exports.gen"), - std::string::npos) << alone; - EXPECT_NE(alone.find("--exports-file obj/probe.exports.gen"), std::string::npos); - EXPECT_EQ(alone.find("--required"), std::string::npos); + expect(ne(alone.find("build bin/probe.def : coff_def obj/probe.o | obj/probe.exports.gen"), std::string::npos)) << alone; + expect(ne(alone.find("--exports-file obj/probe.exports.gen"), std::string::npos)); + expect(eq(alone.find("--required"), std::string::npos)); std::ifstream in(plan.outputDir / "obj" / "probe.exports.gen"); std::string written((std::istreambuf_iterator(in)), std::istreambuf_iterator()); - EXPECT_EQ(written, "probe_*\n"); + expect(eq(written, "probe_*\n")); LinkUnit consumer; consumer.kind = LinkUnit::Binary; @@ -3016,16 +2996,16 @@ TEST(NinjaBackend, PeExportsPatternsAndAConsumerReachTheDefEdge) { consumer.objects = {"obj/app.o"}; consumer.implicitInputs = {"bin/probe.dll", "bin/probe.lib"}; plan.linkUnits.push_back(consumer); - EXPECT_NE(emit_ninja_string(plan).find("--required"), std::string::npos); + expect(ne(emit_ninja_string(plan).find("--required"), std::string::npos)); std::error_code ec; std::filesystem::remove_all(plan.outputDir, ec); -} +}}; // `trim_paths` PER PACKAGE (mcpp#844). The map of the whole plan sat in the // flags every unit shares, so a dependency's command line named the roots of // whichever members the plan built, and switching members recompiled it. A // unit now carries its own package's map, and the shared flags carry none. -TEST(NinjaBackend, ATrimMapIsTheUnitsPackagesAndNoOther) { +UTest a_trim_map_is_the_units_packages_and_no_other{"NinjaBackend/ATrimMapIsTheUnitsPackagesAndNoOther", [] { auto plan = minimal_plan(); plan.manifest.buildConfig.trimPaths = true; const std::filesystem::path build = "/w/target/x"; @@ -3040,28 +3020,28 @@ TEST(NinjaBackend, ATrimMapIsTheUnitsPackagesAndNoOther) { .object = "obj/main.o", .packageName = "app"}); const auto flags = compute_flags(plan); - EXPECT_EQ(flags.cxx.find("file-prefix-map"), std::string::npos) << flags.cxx; - EXPECT_EQ(flags.cc.find("file-prefix-map"), std::string::npos) << flags.cc; + expect(eq(flags.cxx.find("file-prefix-map"), std::string::npos)) << flags.cxx; + expect(eq(flags.cc.find("file-prefix-map"), std::string::npos)) << flags.cc; const auto ninja = emit_ninja_string(plan); auto edge = [&](std::string_view object) { const auto at = ninja.find(std::format("build {} :", object)); - EXPECT_NE(at, std::string::npos) << ninja; + expect(ne(at, std::string::npos)) << ninja; return ninja.substr(at, ninja.find("\nbuild ", at + 1) - at); }; const auto coreEdge = edge("obj/core.o"); - EXPECT_NE(coreEdge.find("-ffile-prefix-map=/w/core=core@0.1.0"), std::string::npos) << coreEdge; - EXPECT_NE(coreEdge.find("-ffile-prefix-map=/w/target/x=/mcpp/build"), std::string::npos) << coreEdge; - EXPECT_EQ(coreEdge.find("/w/app"), std::string::npos) << "a consumer's root reached its dependency: " << coreEdge; + expect(ne(coreEdge.find("-ffile-prefix-map=/w/core=core@0.1.0"), std::string::npos)) << coreEdge; + expect(ne(coreEdge.find("-ffile-prefix-map=/w/target/x=/mcpp/build"), std::string::npos)) << coreEdge; + expect(eq(coreEdge.find("/w/app"), std::string::npos)) << "a consumer's root reached its dependency: " << coreEdge; const auto appEdge = edge("obj/main.o"); - EXPECT_NE(appEdge.find("-ffile-prefix-map=/w/app=app@0.1.0"), std::string::npos) << appEdge; - EXPECT_NE(appEdge.find("-ffile-prefix-map=/w/core=core@0.1.0"), std::string::npos) << appEdge; -} + expect(ne(appEdge.find("-ffile-prefix-map=/w/app=app@0.1.0"), std::string::npos)) << appEdge; + expect(ne(appEdge.find("-ffile-prefix-map=/w/core=core@0.1.0"), std::string::npos)) << appEdge; +}}; // The map of a package is the fixed roots and its closure's roots, in the // order of the whole plan's map: shortest prefix first, so that a driver // trying the last mapping first takes the longest match. -TEST(NinjaBackend, ATrimMapKeepsTheFixedRootsAndTheMarkedPackages) { +UTest a_trim_map_keeps_the_fixed_roots_and_the_marked_packages{"NinjaBackend/ATrimMapKeepsTheFixedRootsAndTheMarkedPackages", [] { // Absolute on every host: `trim_map` maps absolute prefixes only, and a // path without a volume is not absolute on Windows. const auto w = std::filesystem::current_path().root_path() / "w"; @@ -3080,36 +3060,34 @@ TEST(NinjaBackend, ATrimMapKeepsTheFixedRootsAndTheMarkedPackages) { return out; }; const auto coreNames = names(core); - EXPECT_NE(std::ranges::find(coreNames, "core@0.1.0"), coreNames.end()); - EXPECT_NE(std::ranges::find(coreNames, "/mcpp/toolchain"), coreNames.end()); - EXPECT_NE(std::ranges::find(coreNames, "/mcpp/build"), coreNames.end()); - EXPECT_EQ(std::ranges::find(coreNames, "app@1.0.0"), coreNames.end()); - EXPECT_EQ(std::ranges::find(coreNames, "gui@0.2.0"), coreNames.end()); - EXPECT_EQ(names(all).size(), coreNames.size() + 2); + expect(ne(std::ranges::find(coreNames, "core@0.1.0"), coreNames.end())); + expect(ne(std::ranges::find(coreNames, "/mcpp/toolchain"), coreNames.end())); + expect(ne(std::ranges::find(coreNames, "/mcpp/build"), coreNames.end())); + expect(eq(std::ranges::find(coreNames, "app@1.0.0"), coreNames.end())); + expect(eq(std::ranges::find(coreNames, "gui@0.2.0"), coreNames.end())); + expect(eq(names(all).size(), coreNames.size() + 2)); for (std::size_t i = 1; i < core.size(); ++i) - EXPECT_LE(core[i - 1].first.native().size(), core[i].first.native().size()); -} + expect(le(core[i - 1].first.native().size(), core[i].first.native().size())); +}}; // Design 2026-10-26 §24.4 item 9: the entry symbol of a freestanding image, and // the `--defsym` that makes an undefined one a link error, through the driver // and to ld.lld itself. -TEST(NinjaBackend, StartRendersTheEntryAndItsDefinitionCheck) { +UTest start_renders_the_entry_and_its_definition_check{"NinjaBackend/StartRendersTheEntryAndItsDefinitionCheck", [] { using mcpp::build::start_link_flags; using Flags = std::vector; - EXPECT_EQ(start_link_flags("", false), Flags{}); - EXPECT_EQ(start_link_flags("_reset", false), - (Flags{"-Wl,-e,_reset", "-Wl,--defsym=__mcpp_start=_reset"})); - EXPECT_EQ(start_link_flags("_reset", true), - (Flags{"-e", "_reset", "--defsym=__mcpp_start=_reset"})); -} + expect(eq(start_link_flags("", false), Flags{})); + expect(eq(start_link_flags("_reset", false), (Flags{"-Wl,-e,_reset", "-Wl,--defsym=__mcpp_start=_reset"}))); + expect(eq(start_link_flags("_reset", true), (Flags{"-e", "_reset", "--defsym=__mcpp_start=_reset"}))); +}}; -TEST(NinjaBackend, StartFailureAdviceNamesTheStatement) { +UTest start_failure_advice_names_the_statement{"NinjaBackend/StartFailureAdviceNamesTheStatement", [] { using mcpp::build::start_failure_advice; const std::string lld = "ld.lld: error: --defsym:1: symbol not found: _missing\n"; const auto named = start_failure_advice(lld, {{"_missing", "[targets.k] start = \"_missing\""}}); - EXPECT_NE(named.find("It was named by [targets.k] start = \"_missing\""), std::string::npos) << named; + expect(ne(named.find("It was named by [targets.k] start = \"_missing\""), std::string::npos)) << named; const auto generic = start_failure_advice(lld, {}); - EXPECT_NE(generic.find("`_missing`"), std::string::npos) << generic; - EXPECT_NE(generic.find("mcpp:link-start"), std::string::npos) << generic; - EXPECT_EQ(start_failure_advice("undefined symbol: foo", {}), ""); -} + expect(ne(generic.find("`_missing`"), std::string::npos)) << generic; + expect(ne(generic.find("mcpp:link-start"), std::string::npos)) << generic; + expect(eq(start_failure_advice("undefined symbol: foo", {}), "")); +}}; diff --git a/tests/unit/test_object_address.cpp b/tests/unit/test_object_address.cpp index c60edeeca..aab2cb265 100644 --- a/tests/unit/test_object_address.cpp +++ b/tests/unit/test_object_address.cpp @@ -15,15 +15,17 @@ // a package engineered to collide with it — and demands byte-identical // addresses. It fails on the pre-#344 tree. -#include import std; +import mcpplibs.mtest; import mcpp.build.plan; import mcpp.manifest; import mcpp.modgraph.graph; import mcpp.modgraph.scanner; import mcpp.toolchain.model; +using namespace mcpplibs::mtest; + using namespace mcpp::build; namespace { @@ -137,42 +139,42 @@ std::optional planZlib(const Tmp& t, bool withBzip2, std::string* err) { // The load-bearing assertion. Adding an unrelated, deliberately colliding // package to the graph must not move zlib's object by one byte — not its build // path, and above all not its cache address. -TEST(ObjectAddress, DependencyAddressesAreImmuneToTheRestOfTheGraph) { +UTest dependency_addresses_are_immune_to_the_rest_of_the_graph{"ObjectAddress/DependencyAddressesAreImmuneToTheRestOfTheGraph", [] { Tmp t; std::string errAlone, errBeside; auto alone = planZlib(t, /*withBzip2=*/false, &errAlone); auto beside = planZlib(t, /*withBzip2=*/true, &errBeside); - ASSERT_TRUE(alone) << errAlone; - ASSERT_TRUE(beside) << errBeside; + expect(fatal(bool(alone))) << errAlone; + expect(fatal(bool(beside))) << errBeside; - EXPECT_EQ(alone->packageObjectRel, beside->packageObjectRel) + expect(eq(alone->packageObjectRel, beside->packageObjectRel)) << "the cache-entry address moved when an unrelated package joined the " "graph — that is mcpp#344"; - EXPECT_EQ(alone->object, beside->object); -} + expect(eq(alone->object, beside->object)); +}}; // The address must also be package-internal: nothing about the consumer, and // nothing that only exists on this machine. It is read back by another machine // that computed the same key. -TEST(ObjectAddress, DependencyCacheAddressIsPackageInternal) { +UTest dependency_cache_address_is_package_internal{"ObjectAddress/DependencyCacheAddressIsPackageInternal", [] { Tmp t; std::string err; auto built = planZlib(t, /*withBzip2=*/true, &err); - ASSERT_TRUE(built) << err; + expect(fatal(bool(built))) << err; auto rel = built->packageObjectRel.generic_string(); - EXPECT_FALSE(rel.empty()); - EXPECT_FALSE(built->packageObjectRel.is_absolute()) << rel; - EXPECT_FALSE(rel.starts_with("..")) << rel; + expect(!rel.empty()); + expect(!(built->packageObjectRel.is_absolute())) << rel; + expect(!rel.starts_with("..")) << rel; // Mirrors the source's path relative to its OWN package root, and carries // no package-partition component (that lives in the build path only). - EXPECT_EQ(rel, "zlib-1.3.2/compress.o") << rel; + expect(eq(rel, "zlib-1.3.2/compress.o")) << rel; // The build path does partition by package — that is what makes the // cross-package census unnecessary. auto obj = built->object.generic_string(); - EXPECT_EQ(obj, "obj/compat_zlib/zlib-1.3.2/compress.o") << obj; -} + expect(eq(obj, "obj/compat_zlib/zlib-1.3.2/compress.o")) << obj; +}}; // `path_is_under_any` decides both whether a package may be cached and where // its objects are anchored, and it must answer LEXICALLY. @@ -184,7 +186,7 @@ TEST(ObjectAddress, DependencyCacheAddressIsPackageInternal) { // which point `/registry/data/xpkgs/` no longer looks like it is in // the store — every such package silently drops out of the build cache. That // regression was caught by e2e 40 and is pinned here where it is cheap. -TEST(ObjectAddress, PathContainmentIsLexicalSoSymlinkedStoresStillCount) { +UTest path_containment_is_lexical_so_symlinked_stores_still_count{"ObjectAddress/PathContainmentIsLexicalSoSymlinkedStoresStillCount", [] { Tmp t; auto real = t.path / "real-store"; auto store = t.path / "home" / "registry" / "data" / "xpkgs"; @@ -194,23 +196,23 @@ TEST(ObjectAddress, PathContainmentIsLexicalSoSymlinkedStoresStillCount) { std::error_code ec; std::filesystem::create_directory_symlink( real / "compat.zlib@1.3.2", store / "compat.zlib@1.3.2", ec); - if (ec) GTEST_SKIP() << "symlinks unavailable: " << ec.message(); + if (ec) skip([&] { std::ostringstream text; text << "symlinks unavailable: " << ec.message(); return text.str(); }()); auto pkgRoot = store / "compat.zlib@1.3.2"; - EXPECT_TRUE(path_is_under_any(pkgRoot, {store})); - EXPECT_TRUE(path_is_under_any(pkgRoot / "src" / "compress.c", {store})); + expect(path_is_under_any(pkgRoot, {store})); + expect(path_is_under_any(pkgRoot / "src" / "compress.c", {store})); // And it still says no to something genuinely outside — the gate has to // keep rejecting `target/.mangled/**`, which is the case it exists for. - EXPECT_FALSE(path_is_under_any(t.path / "proj" / "target" / ".mangled" / "x", + expect(!path_is_under_any(t.path / "proj" / "target" / ".mangled" / "x", {store})); - EXPECT_FALSE(path_is_under_any(pkgRoot, {})); -} + expect(!path_is_under_any(pkgRoot, {})); +}}; // The root project is never cached, so it keeps the flat layout every project // has had since 0.0.1 — and a dependency shipping a same-named file can no // longer force it to disambiguate. -TEST(ObjectAddress, RootObjectsStayFlatAndUncacheable) { +UTest root_objects_stay_flat_and_uncacheable{"ObjectAddress/RootObjectsStayFlatAndUncacheable", [] { Tmp t; auto projectRoot = t.path / "proj"; auto storeRoot = t.path / "store"; @@ -240,16 +242,16 @@ TEST(ObjectAddress, RootObjectsStayFlatAndUncacheable) { auto plan = make_plan(rootManifest, gccLike(), {}, graph, topo, packages, projectRoot, projectRoot / "target" / "t", {}, {}, {storeRoot}); - ASSERT_TRUE(plan) << plan.error(); + if (auto&& failed = expect(fatal(bool(plan))); !failed) failed << plan.error(); for (auto& cu : plan->compileUnits) { if (cu.source == projectRoot / "src" / "compress.c") { - EXPECT_EQ(cu.object.generic_string(), "obj/compress.o"); - EXPECT_TRUE(cu.packageObjectRel.empty()) + expect(eq(cu.object.generic_string(), "obj/compress.o")); + expect(cu.packageObjectRel.empty()) << "the root project must never get a cache address"; } } -} +}}; // ── A source outside the package that declares it (mcpp#641, item 3) ─────── @@ -313,25 +315,25 @@ std::optional addressOfDeclaredDependencySource( // The address names the package that contains the file and the path inside it, // so it does not grow with the distance between the two package roots. -TEST(ObjectAddress, ASourceUnderAnotherPackageIsAddressedByThatPackage) { +UTest a_source_under_another_package_is_addressed_by_that_package{"ObjectAddress/ASourceUnderAnotherPackageIsAddressedByThatPackage", [] { Tmp t; const auto dep = t.path / "fw" / "dep"; std::string err; auto near = addressOfDeclaredDependencySource(t.path / "fw" / "tool", dep, &err); - ASSERT_TRUE(near) << err; - EXPECT_EQ(*near, "obj/installer/__pkg/dep/platform/impl/helper.o"); - EXPECT_EQ(near->find("__up"), std::string::npos) << *near; + expect(fatal(bool(near))) << err; + expect(eq(*near, "obj/installer/__pkg/dep/platform/impl/helper.o")); + if (auto&& failed = expect(eq(near->find("__up"), std::string::npos)); !failed) failed << *near; auto deeper = addressOfDeclaredDependencySource( t.path / "fw" / "tools" / "windows" / "installer" / "tool", dep, &err); - ASSERT_TRUE(deeper) << err; - EXPECT_EQ(*deeper, *near) + expect(fatal(bool(deeper))) << err; + expect(eq(*deeper, *near)) << "moving the declaring package deeper changed the address"; -} +}}; // A source no package contains, colliding by basename with a root source, is // filed under a hash of its directory rather than a climbing mirror of it. -TEST(ObjectAddress, AnUnownedEscapingSourceIsAddressedByAHashOfItsDirectory) { +UTest an_unowned_escaping_source_is_addressed_by_a_hash_of_its_directory{"ObjectAddress/AnUnownedEscapingSourceIsAddressedByAHashOfItsDirectory", [] { Tmp t; auto projectRoot = t.path / "proj"; auto depRoot = t.path / "store" / "dep@1.0.0"; @@ -354,23 +356,23 @@ TEST(ObjectAddress, AnUnownedEscapingSourceIsAddressedByAHashOfItsDirectory) { std::vector topo{0, 1}; auto plan = make_plan(rootManifest, gccLike(), {}, graph, topo, packages, projectRoot, projectRoot / "target" / "t", {}, {}, {}); - ASSERT_TRUE(plan) << plan.error(); + if (auto&& failed = expect(fatal(bool(plan))); !failed) failed << plan.error(); std::string rootObj, outsideObj; for (auto& cu : plan->compileUnits) { if (cu.source == projectRoot / "src" / "gen.cpp") rootObj = cu.object.generic_string(); if (cu.source == outside) outsideObj = cu.object.generic_string(); } - EXPECT_EQ(rootObj, "obj/app/src/gen.o"); - ASSERT_TRUE(outsideObj.starts_with("obj/dep/__ext/")) << outsideObj; - EXPECT_TRUE(outsideObj.ends_with("/gen.o")) << outsideObj; - EXPECT_EQ(outsideObj.find("__up"), std::string::npos) << outsideObj; + expect(eq(rootObj, "obj/app/src/gen.o")); + expect(fatal(outsideObj.starts_with("obj/dep/__ext/"))) << outsideObj; + expect(outsideObj.ends_with("/gen.o")) << outsideObj; + expect(eq(outsideObj.find("__up"), std::string::npos)) << outsideObj; const auto hash = outsideObj.substr(std::string("obj/dep/__ext/").size(), 8); - EXPECT_EQ(outsideObj.size(), std::string("obj/dep/__ext/").size() + 8 + std::string("/gen.o").size()) + expect(eq(outsideObj.size(), std::string("obj/dep/__ext/").size() + 8 + std::string("/gen.o").size())) << outsideObj; - EXPECT_TRUE(std::ranges::all_of(hash, [](char c) { + expect(std::ranges::all_of(hash, [](char c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'); })) << outsideObj; -} +}}; // A WORKSPACE PLAN'S ROOT OWNS NO SOURCE (pack drive and selection design // 2026-10-01, B1; mcpp#751). A member lists a file that lies in the workspace @@ -378,7 +380,7 @@ TEST(ObjectAddress, AnUnownedEscapingSourceIsAddressedByAHashOfItsDirectory) { // workspace's, and owning the file put it in the root's basename census, so // its object moved when a second member listed the same file: `-p` and // `--workspace` compiled it to two addresses in one build directory. -TEST(ObjectAddress, AMembersOutsideSourceIsImmuneToAnotherMemberListingIt) { +UTest a_members_outside_source_is_immune_to_another_member_listing_it{"ObjectAddress/AMembersOutsideSourceIsImmuneToAnotherMemberListingIt", [] { Tmp t; const auto ws = t.path / "ws"; auto plan_with = [&](bool withTool) -> std::filesystem::path { @@ -401,25 +403,25 @@ TEST(ObjectAddress, AMembersOutsideSourceIsImmuneToAnotherMemberListingIt) { for (std::size_t i = 0; i < graph.units.size(); ++i) topo.push_back(i); auto plan = make_plan(root, gccLike(), {}, graph, topo, packages, ws, ws / "target" / "t", {}, {}, {}); - EXPECT_TRUE(plan) << (plan ? std::string{} : plan.error()); + if (auto&& failed = expect(bool(plan)); !failed) failed << (plan ? std::string{} : plan.error()); if (!plan) return {}; for (auto const& cu : plan->compileUnits) if (cu.packageName == "core") return cu.object; - ADD_FAILURE() << "core's unit is not in the plan"; + expect(false) << "core's unit is not in the plan"; return {}; }; const auto alone = plan_with(false); const auto beside = plan_with(true); - EXPECT_FALSE(alone.empty()); - EXPECT_EQ(alone, beside); + expect(!alone.empty()); + expect(eq(alone, beside)); // Filed under its declaring member, not at the root's flat address. - EXPECT_NE(alone.generic_string().find("obj/core/"), std::string::npos) << alone; -} + expect(ne(alone.generic_string().find("obj/core/"), std::string::npos)) << alone; +}}; // The same for a file inside ANOTHER member's directory: the member that // contains it may be outside the selection, and then the virtual root contains // the file instead. The address is the file's place in the workspace. -TEST(ObjectAddress, AMembersSourceInAnotherMembersDirectoryIsImmuneToTheSelection) { +UTest a_members_source_in_another_members_directory_is_immune_to_the_selection{"ObjectAddress/AMembersSourceInAnotherMembersDirectoryIsImmuneToTheSelection", [] { Tmp t; const auto ws = t.path / "ws"; auto plan_with = [&](bool withLib) -> std::filesystem::path { @@ -442,16 +444,16 @@ TEST(ObjectAddress, AMembersSourceInAnotherMembersDirectoryIsImmuneToTheSelectio for (std::size_t i = 0; i < graph.units.size(); ++i) topo.push_back(i); auto plan = make_plan(root, gccLike(), {}, graph, topo, packages, ws, ws / "target" / "t", {}, {}, {}); - EXPECT_TRUE(plan) << (plan ? std::string{} : plan.error()); + if (auto&& failed = expect(bool(plan)); !failed) failed << (plan ? std::string{} : plan.error()); if (!plan) return {}; for (auto const& cu : plan->compileUnits) if (cu.packageName == "app") return cu.object; - ADD_FAILURE() << "app's unit is not in the plan"; + expect(false) << "app's unit is not in the plan"; return {}; }; const auto alone = plan_with(false); const auto beside = plan_with(true); - EXPECT_FALSE(alone.empty()); - EXPECT_EQ(alone, beside); - EXPECT_NE(alone.generic_string().find("obj/app/__ws/lib/src/"), std::string::npos) << alone; -} + expect(!alone.empty()); + expect(eq(alone, beside)); + expect(ne(alone.generic_string().find("obj/app/__ws/lib/src/"), std::string::npos)) << alone; +}}; diff --git a/tests/unit/test_p1689.cpp b/tests/unit/test_p1689.cpp index da49c6947..75957b71b 100644 --- a/tests/unit/test_p1689.cpp +++ b/tests/unit/test_p1689.cpp @@ -1,8 +1,10 @@ -#include import std; +import mcpplibs.mtest; import mcpp.modgraph.p1689; +using namespace mcpplibs::mtest; + using namespace mcpp::modgraph::p1689; namespace { @@ -58,36 +60,36 @@ constexpr const char* kEmptyRequires = R"({ } // namespace -TEST(P1689Parse, SimpleProvider) { +UTest simple_provider{"P1689Parse/SimpleProvider", [] { auto r = parse_ddi(kSimpleProvider); - ASSERT_TRUE(r) << r.error(); - EXPECT_EQ(r->primaryOutput, "/tmp/foo.o"); - ASSERT_EQ(r->provides.size(), 1u); - EXPECT_EQ(r->provides[0].logicalName, "foo"); + if (auto&& failed = expect(fatal(bool(r))); !failed) failed << r.error(); + expect(eq(r->primaryOutput, "/tmp/foo.o")); + expect(fatal(eq(r->provides.size(), 1u))); + expect(eq(r->provides[0].logicalName, "foo")); // Not EXPECT_TRUE: on an optional that only asserts "the key was present", // which stays green when the value parses as false. - EXPECT_EQ(r->provides[0].isInterface, std::optional{true}); - ASSERT_EQ(r->requires_.size(), 2u); - EXPECT_EQ(r->requires_[0], "std"); - EXPECT_EQ(r->requires_[1], "foo:impl"); -} + expect(eq(r->provides[0].isInterface, std::optional{true})); + expect(fatal(eq(r->requires_.size(), 2u))); + expect(eq(r->requires_[0], "std")); + expect(eq(r->requires_[1], "foo:impl")); +}}; -TEST(P1689Parse, PureConsumer) { +UTest pure_consumer{"P1689Parse/PureConsumer", [] { auto r = parse_ddi(kPureConsumer); - ASSERT_TRUE(r) << r.error(); - EXPECT_TRUE(r->provides.empty()); - ASSERT_EQ(r->requires_.size(), 2u); - EXPECT_EQ(r->requires_[0], "std"); - EXPECT_EQ(r->requires_[1], "myapp.lib"); -} - -TEST(P1689Parse, EmptyRequires) { + if (auto&& failed = expect(fatal(bool(r))); !failed) failed << r.error(); + expect(r->provides.empty()); + expect(fatal(eq(r->requires_.size(), 2u))); + expect(eq(r->requires_[0], "std")); + expect(eq(r->requires_[1], "myapp.lib")); +}}; + +UTest empty_requires{"P1689Parse/EmptyRequires", [] { auto r = parse_ddi(kEmptyRequires); - ASSERT_TRUE(r) << r.error(); - ASSERT_EQ(r->provides.size(), 1u); - EXPECT_EQ(r->provides[0].logicalName, "lone"); - EXPECT_TRUE(r->requires_.empty()); -} + if (auto&& failed = expect(fatal(bool(r))); !failed) failed << r.error(); + expect(fatal(eq(r->provides.size(), 1u))); + expect(eq(r->provides[0].logicalName, "lone")); + expect(r->requires_.empty()); +}}; // ─── is-interface decides whether a source may be published ──────────────── // @@ -116,34 +118,34 @@ constexpr const char* kNoIsInterfaceKey = R"({ ] })"; -TEST(P1689Parse, ImplementationPartitionIsNotAnInterface) { +UTest implementation_partition_is_not_an_interface{"P1689Parse/ImplementationPartitionIsNotAnInterface", [] { auto r = parse_ddi(kImplementationPartition); - ASSERT_TRUE(r) << r.error(); - ASSERT_EQ(r->provides.size(), 1u); - EXPECT_EQ(r->provides[0].logicalName, "mathkit:secret"); - EXPECT_EQ(r->provides[0].isInterface, std::optional{false}); -} + if (auto&& failed = expect(fatal(bool(r))); !failed) failed << r.error(); + expect(fatal(eq(r->provides.size(), 1u))); + expect(eq(r->provides[0].logicalName, "mathkit:secret")); + expect(eq(r->provides[0].isInterface, std::optional{false})); +}}; -TEST(P1689Parse, AnAbsentIsInterfaceKeyStaysAbsent) { +UTest an_absent_is_interface_key_stays_absent{"P1689Parse/AnAbsentIsInterfaceKeyStaysAbsent", [] { // P1689 makes the key optional, so absence has to reach the packer as // "nobody said" rather than as either answer. It used to arrive as `false` // by struct default — which is the same value as an explicit // implementation partition, i.e. the two became indistinguishable. auto r = parse_ddi(kNoIsInterfaceKey); - ASSERT_TRUE(r) << r.error(); - ASSERT_EQ(r->provides.size(), 1u); - EXPECT_EQ(r->provides[0].logicalName, "mathkit:quiet"); - EXPECT_FALSE(r->provides[0].isInterface.has_value()); -} + if (auto&& failed = expect(fatal(bool(r))); !failed) failed << r.error(); + expect(fatal(eq(r->provides.size(), 1u))); + expect(eq(r->provides[0].logicalName, "mathkit:quiet")); + expect(!(r->provides[0].isInterface.has_value())); +}}; -TEST(P1689Parse, RejectsNonObject) { +UTest rejects_non_object{"P1689Parse/RejectsNonObject", [] { auto r = parse_ddi("[]"); - EXPECT_FALSE(r); - EXPECT_NE(r.error().find("top-level"), std::string::npos); -} + expect(!r); + expect(ne(r.error().find("top-level"), std::string::npos)); +}}; -TEST(P1689Parse, RejectsMissingRules) { +UTest rejects_missing_rules{"P1689Parse/RejectsMissingRules", [] { auto r = parse_ddi(R"({"version": 0})"); - EXPECT_FALSE(r); - EXPECT_NE(r.error().find("rules"), std::string::npos); -} + expect(!r); + expect(ne(r.error().find("rules"), std::string::npos)); +}}; diff --git a/tests/unit/test_pack_abi_tag.cpp b/tests/unit/test_pack_abi_tag.cpp index 8236827ad..374583ea6 100644 --- a/tests/unit/test_pack_abi_tag.cpp +++ b/tests/unit/test_pack_abi_tag.cpp @@ -1,9 +1,11 @@ -#include import std; +import mcpplibs.mtest; import mcpp.pack.abi_tag; import mcpp.toolchain.model; +using namespace mcpplibs::mtest; + using namespace mcpp::pack; using mcpp::toolchain::CompilerId; using mcpp::toolchain::Toolchain; @@ -25,142 +27,142 @@ Toolchain tc(CompilerId cc, std::string ver, std::string stdlib, std::string std // ─── the tag is a projection, and of the CANONICAL triple ────────────────── -TEST(AbiTag, CxxSurfaceNamesEveryDimension) { +UTest cxx_surface_names_every_dimension{"AbiTag/CxxSurfaceNamesEveryDimension", [] { auto t = cxx_surface_tag(tc(CompilerId::GCC, "16.1.0", "libstdc++", "16.1.0"), "x86_64-linux-gnu", 23); - EXPECT_EQ(t.str(), "x86_64-linux-gnu-gcc16-libstdcxx16-c++23"); - EXPECT_FALSE(t.c_surface()); -} + expect(eq(t.str(), "x86_64-linux-gnu-gcc16-libstdcxx16-c++23")); + expect(!t.c_surface()); +}}; -TEST(AbiTag, UsesTheCanonicalTripleNotTheCompilerReportedOne) { +UTest uses_the_canonical_triple_not_the_compiler_reported_one{"AbiTag/UsesTheCanonicalTripleNotTheCompilerReportedOne", [] { // The toolchain above reports `x86_64-w64-mingw32`; mcpp's target // vocabulary — and every `[target.'']` key a package can be // selected by — says `x86_64-windows-gnu`. Publishing the compiler's // spelling would give one decision two spellings. auto t = cxx_surface_tag(tc(CompilerId::GCC, "16.1.0", "libstdc++", "16.1.0"), "x86_64-windows-gnu", 23); - EXPECT_EQ(t.triple, "x86_64-windows-gnu"); - EXPECT_EQ(t.str().find("mingw"), std::string::npos); -} - -TEST(AbiTag, LibcxxAndMsvcStlTokenize) { - EXPECT_EQ(stdlib_token("libstdc++"), "libstdcxx"); - EXPECT_EQ(stdlib_token("libc++"), "libcxx"); - EXPECT_EQ(stdlib_token("msvc-stl"), "msvcstl"); - EXPECT_EQ(stdlib_token(""), "unknownstl"); -} - -TEST(AbiTag, MajorIsLeadingDigitsOnly) { - EXPECT_EQ(major_of("16.1.0"), "16"); - EXPECT_EQ(major_of("19.44.35207"), "19"); - EXPECT_EQ(major_of("22"), "22"); - EXPECT_EQ(major_of(""), "0"); -} + expect(eq(t.triple, "x86_64-windows-gnu")); + expect(eq(t.str().find("mingw"), std::string::npos)); +}}; + +UTest libcxx_and_msvc_stl_tokenize{"AbiTag/LibcxxAndMsvcStlTokenize", [] { + expect(eq(stdlib_token("libstdc++"), "libstdcxx")); + expect(eq(stdlib_token("libc++"), "libcxx")); + expect(eq(stdlib_token("msvc-stl"), "msvcstl")); + expect(eq(stdlib_token(""), "unknownstl")); +}}; + +UTest major_is_leading_digits_only{"AbiTag/MajorIsLeadingDigitsOnly", [] { + expect(eq(major_of("16.1.0"), "16")); + expect(eq(major_of("19.44.35207"), "19")); + expect(eq(major_of("22"), "22")); + expect(eq(major_of(""), "0")); +}}; // ─── the SHAPE is the surface: a C library publishes a shorter tag ───────── -TEST(AbiTag, CSurfaceIsTripleOnly) { +UTest c_surface_is_triple_only{"AbiTag/CSurfaceIsTripleOnly", [] { auto t = c_surface_tag("x86_64-linux-gnu"); - EXPECT_EQ(t.str(), "x86_64-linux-gnu"); - EXPECT_TRUE(t.c_surface()); -} + expect(eq(t.str(), "x86_64-linux-gnu")); + expect(bool(t.c_surface())); +}}; -TEST(AbiTag, CSurfaceAcceptsAnyCompilerAndStdlib) { +UTest c_surface_accepts_any_compiler_and_stdlib{"AbiTag/CSurfaceAcceptsAnyCompilerAndStdlib", [] { auto published = c_surface_tag("x86_64-linux-gnu"); auto current = cxx_surface_tag(tc(CompilerId::Clang, "22.1.8", "libc++", "22.1.8"), "x86_64-linux-gnu", 26); // An extern "C" library constrains the libc ABI and nothing else, so an // unspecified dimension is don't-care — the same rule abi_check uses. - EXPECT_TRUE(tag_check(published, current).empty()); -} + expect(tag_check(published, current).empty()); +}}; -TEST(AbiTag, CSurfaceStillRefusesAForeignTriple) { +UTest c_surface_still_refuses_a_foreign_triple{"AbiTag/CSurfaceStillRefusesAForeignTriple", [] { auto published = c_surface_tag("x86_64-linux-gnu"); auto current = c_surface_tag("aarch64-linux-gnu"); auto bad = tag_check(published, current); - ASSERT_EQ(bad.size(), 1u); - EXPECT_EQ(bad[0].dimension, "triple"); -} + expect(fatal(eq(bad.size(), 1u))); + expect(eq(bad[0].dimension, "triple")); +}}; // ─── round-trip: parsing runs from the END, because triples vary in length ── -TEST(AbiTag, ParsesFullTagBackWithATwoDashTriple) { +UTest parses_full_tag_back_with_a_two_dash_triple{"AbiTag/ParsesFullTagBackWithATwoDashTriple", [] { auto t = parse_abi_tag("x86_64-linux-gnu-gcc16-libstdcxx16-c++23"); - ASSERT_TRUE(t.has_value()); - EXPECT_EQ(t->triple, "x86_64-linux-gnu"); - EXPECT_EQ(t->compiler, "gcc16"); - EXPECT_EQ(t->stdlib, "libstdcxx16"); - EXPECT_EQ(t->standard, "c++23"); -} - -TEST(AbiTag, ParsesFullTagBackWithAOneDashTriple) { + expect(fatal(t.has_value())); + expect(eq(t->triple, "x86_64-linux-gnu")); + expect(eq(t->compiler, "gcc16")); + expect(eq(t->stdlib, "libstdcxx16")); + expect(eq(t->standard, "c++23")); +}}; + +UTest parses_full_tag_back_with_a_one_dash_triple{"AbiTag/ParsesFullTagBackWithAOneDashTriple", [] { // `aarch64-macos` has one dash, `x86_64-linux-gnu` has two. Splitting from // the front cannot tell where the triple ends; splitting from the back can, // because the C++ half is exactly three segments ending in c++NN. auto t = parse_abi_tag("aarch64-macos-llvm22-libcxx22-c++23"); - ASSERT_TRUE(t.has_value()); - EXPECT_EQ(t->triple, "aarch64-macos"); - EXPECT_EQ(t->compiler, "llvm22"); - EXPECT_EQ(t->standard, "c++23"); -} + expect(fatal(t.has_value())); + expect(eq(t->triple, "aarch64-macos")); + expect(eq(t->compiler, "llvm22")); + expect(eq(t->standard, "c++23")); +}}; -TEST(AbiTag, ParsesATripleOnlyTag) { +UTest parses_a_triple_only_tag{"AbiTag/ParsesATripleOnlyTag", [] { auto t = parse_abi_tag("x86_64-linux-gnu"); - ASSERT_TRUE(t.has_value()); - EXPECT_TRUE(t->c_surface()); - EXPECT_EQ(t->triple, "x86_64-linux-gnu"); -} + expect(fatal(t.has_value())); + expect(bool(t->c_surface())); + expect(eq(t->triple, "x86_64-linux-gnu")); +}}; -TEST(AbiTag, RoundTripsEveryShape) { +UTest round_trips_every_shape{"AbiTag/RoundTripsEveryShape", [] { for (auto s : { "x86_64-linux-gnu", "x86_64-linux-musl-gcc16-libstdcxx16-c++23", "aarch64-macos-llvm22-libcxx22-c++26", "x86_64-windows-msvc-msvc19-msvcstl19-c++23" }) { auto t = parse_abi_tag(s); - ASSERT_TRUE(t.has_value()) << s; - EXPECT_EQ(t->str(), s); + expect(fatal(t.has_value())) << s; + expect(eq(t->str(), s)); } -} +}}; -TEST(AbiTag, RejectsEmptyAndNonTagInput) { - EXPECT_FALSE(parse_abi_tag("").has_value()); +UTest rejects_empty_and_non_tag_input{"AbiTag/RejectsEmptyAndNonTagInput", [] { + expect(!(parse_abi_tag("").has_value())); // Three trailing segments that do not end in c++NN are part of the triple, // not a C++ half — this must not be silently mis-split. auto t = parse_abi_tag("a-b-c-d"); - ASSERT_TRUE(t.has_value()); - EXPECT_EQ(t->triple, "a-b-c-d"); - EXPECT_TRUE(t->c_surface()); -} + expect(fatal(t.has_value())); + expect(eq(t->triple, "a-b-c-d")); + expect(bool(t->c_surface())); +}}; // ─── the gate ────────────────────────────────────────────────────────────── -TEST(AbiTag, RefusesADifferentCompilerMajor) { +UTest refuses_a_different_compiler_major{"AbiTag/RefusesADifferentCompilerMajor", [] { auto published = *parse_abi_tag("x86_64-linux-gnu-gcc15-libstdcxx15-c++23"); auto current = *parse_abi_tag("x86_64-linux-gnu-gcc16-libstdcxx16-c++23"); auto bad = tag_check(published, current); - ASSERT_EQ(bad.size(), 2u); - EXPECT_EQ(bad[0].dimension, "compiler"); - EXPECT_EQ(bad[0].need, "gcc15"); - EXPECT_EQ(bad[0].got, "gcc16"); - EXPECT_EQ(bad[1].dimension, "stdlib"); -} - -TEST(AbiTag, StandardIsAFloorNotAnEquality) { + expect(fatal(eq(bad.size(), 2u))); + expect(eq(bad[0].dimension, "compiler")); + expect(eq(bad[0].need, "gcc15")); + expect(eq(bad[0].got, "gcc16")); + expect(eq(bad[1].dimension, "stdlib")); +}}; + +UTest standard_is_a_floor_not_an_equality{"AbiTag/StandardIsAFloorNotAnEquality", [] { auto published = *parse_abi_tag("x86_64-linux-gnu-gcc16-libstdcxx16-c++23"); // Building at a HIGHER level is fine: the interface being compiled is the // artifact's own source and a newer level accepts it. auto higher = *parse_abi_tag("x86_64-linux-gnu-gcc16-libstdcxx16-c++26"); - EXPECT_TRUE(tag_check(published, higher).empty()); + expect(tag_check(published, higher).empty()); // Lower is not: the interface may use syntax this level lacks. auto lower = *parse_abi_tag("x86_64-linux-gnu-gcc16-libstdcxx16-c++20"); auto bad = tag_check(published, lower); - ASSERT_EQ(bad.size(), 1u); - EXPECT_EQ(bad[0].dimension, "standard"); -} + expect(fatal(eq(bad.size(), 1u))); + expect(eq(bad[0].dimension, "standard")); +}}; -TEST(AbiTag, ReportsEveryMismatchAtOnce) { +UTest reports_every_mismatch_at_once{"AbiTag/ReportsEveryMismatchAtOnce", [] { // One diagnostic, not one per rebuild. auto published = *parse_abi_tag("aarch64-linux-gnu-gcc15-libcxx15-c++26"); auto current = *parse_abi_tag("x86_64-linux-gnu-gcc16-libstdcxx16-c++23"); - EXPECT_EQ(tag_check(published, current).size(), 4u); -} + expect(eq(tag_check(published, current).size(), 4u)); +}}; diff --git a/tests/unit/test_pack_archive_remove.cpp b/tests/unit/test_pack_archive_remove.cpp index a3a718703..944ccbd03 100644 --- a/tests/unit/test_pack_archive_remove.cpp +++ b/tests/unit/test_pack_archive_remove.cpp @@ -1,9 +1,11 @@ -#include import std; +import mcpplibs.mtest; import mcpp.pack.library; import mcpp.toolchain.dialect; +using namespace mcpplibs::mtest; + using mcpp::pack::archive_remove_command; using Argv = std::vector; using mcpp::toolchain::gnu_dialect; @@ -34,49 +36,49 @@ const std::vector kMembers{ "mathkit.m.o", "api.m.o" }; } // namespace -TEST(ArchiveRemoveCommand, GnuTakesOneVerbThenTheArchiveThenEveryMember) { +UTest gnu_takes_one_verb_then_the_archive_then_every_member{"ArchiveRemoveCommand/GnuTakesOneVerbThenTheArchiveThenEveryMember", [] { auto cmd = archive_remove_command("/usr/bin/ar", "bin/libmathkit.a", kMembers, gnu_dialect().archiveRemoveArg, gnu_dialect().archiveRemoveTakesArchiveFirst); - EXPECT_EQ(cmd, (Argv{ "/usr/bin/ar", "d", "bin/libmathkit.a", "mathkit.m.o", "api.m.o" })); -} + expect(eq(cmd, (Argv{ "/usr/bin/ar", "d", "bin/libmathkit.a", "mathkit.m.o", "api.m.o" }))); +}}; -TEST(ArchiveRemoveCommand, MsvcTakesOneFlagPerMemberAndTheArchiveLast) { +UTest msvc_takes_one_flag_per_member_and_the_archive_last{"ArchiveRemoveCommand/MsvcTakesOneFlagPerMemberAndTheArchiveLast", [] { auto cmd = archive_remove_command("lib.exe", "bin/mathkit.lib", kMembers, msvc_dialect().archiveRemoveArg, msvc_dialect().archiveRemoveTakesArchiveFirst); - EXPECT_EQ(cmd, (Argv{ "lib.exe", "/REMOVE:mathkit.m.o", "/REMOVE:api.m.o", "bin/mathkit.lib" })); -} + expect(eq(cmd, (Argv{ "lib.exe", "/REMOVE:mathkit.m.o", "/REMOVE:api.m.o", "bin/mathkit.lib" }))); +}}; // The two rows in dialect.cppm are the input to the assembly above. Pinned here // too, so changing one without the other fails rather than producing a command // that is well-formed for the wrong archiver. -TEST(ArchiveRemoveCommand, TheDialectRowsSupplyTheseTwoSpellings) { - EXPECT_EQ(gnu_dialect().archiveRemoveArg, "d"); - EXPECT_TRUE(gnu_dialect().archiveRemoveTakesArchiveFirst); - EXPECT_EQ(msvc_dialect().archiveRemoveArg, "/REMOVE:{}"); - EXPECT_FALSE(msvc_dialect().archiveRemoveTakesArchiveFirst); -} +UTest the_dialect_rows_supply_these_two_spellings{"ArchiveRemoveCommand/TheDialectRowsSupplyTheseTwoSpellings", [] { + expect(eq(gnu_dialect().archiveRemoveArg, "d")); + expect(bool(gnu_dialect().archiveRemoveTakesArchiveFirst)); + expect(eq(msvc_dialect().archiveRemoveArg, "/REMOVE:{}")); + expect(!(msvc_dialect().archiveRemoveTakesArchiveFirst)); +}}; // The failure that motivated the seam: the packer used to emit `ar` syntax // unconditionally. Stated as what must NOT appear, because "it contains // /REMOVE:" alone would stay green for a command that also carried the `d` verb // or put the archive in the wrong place. -TEST(ArchiveRemoveCommand, TheMsvcFormCarriesNoArVerbAndNoLeadingArchive) { +UTest the_msvc_form_carries_no_ar_verb_and_no_leading_archive{"ArchiveRemoveCommand/TheMsvcFormCarriesNoArVerbAndNoLeadingArchive", [] { auto cmd = archive_remove_command("lib.exe", "bin/mathkit.lib", kMembers, msvc_dialect().archiveRemoveArg, msvc_dialect().archiveRemoveTakesArchiveFirst); - EXPECT_EQ(std::ranges::find(cmd, "d"), cmd.end()); - EXPECT_LT(std::ranges::find(cmd, "/REMOVE:mathkit.m.o"), std::ranges::find(cmd, "bin/mathkit.lib")); -} + expect(eq(std::ranges::find(cmd, "d"), cmd.end())); + expect(lt(std::ranges::find(cmd, "/REMOVE:mathkit.m.o"), std::ranges::find(cmd, "bin/mathkit.lib"))); +}}; -TEST(ArchiveRemoveCommand, ASingleMemberIsSpelledTheSameWayAsMany) { +UTest a_single_member_is_spelled_the_same_way_as_many{"ArchiveRemoveCommand/ASingleMemberIsSpelledTheSameWayAsMany", [] { // The per-member branch is a loop; a one-element list is where an // "and-then-the-rest" bug hides. auto gnu = archive_remove_command("ar", "libx.a", { "only.m.o" }, gnu_dialect().archiveRemoveArg, true); - EXPECT_EQ(gnu, (Argv{ "ar", "d", "libx.a", "only.m.o" })); + expect(eq(gnu, (Argv{ "ar", "d", "libx.a", "only.m.o" }))); auto msvc = archive_remove_command("lib.exe", "x.lib", { "only.m.o" }, msvc_dialect().archiveRemoveArg, false); - EXPECT_EQ(msvc, (Argv{ "lib.exe", "/REMOVE:only.m.o", "x.lib" })); -} + expect(eq(msvc, (Argv{ "lib.exe", "/REMOVE:only.m.o", "x.lib" }))); +}}; diff --git a/tests/unit/test_pack_binfmt.cpp b/tests/unit/test_pack_binfmt.cpp index 175035b1d..5bfd2cc99 100644 --- a/tests/unit/test_pack_binfmt.cpp +++ b/tests/unit/test_pack_binfmt.cpp @@ -1,11 +1,13 @@ -#include #include // stderr is a MACRO — `import std;` cannot export it #include // getenv import std; +import mcpplibs.mtest; import mcpp.pack.binfmt; import mcpp.pack.zip; +using namespace mcpplibs::mtest; + namespace bf = mcpp::pack::binfmt; // WHY THE FIXTURES ARE SYNTHESIZED RATHER THAN CHECKED IN. @@ -351,28 +353,28 @@ std::string macho_fat(const std::vector>& } // namespace -TEST(PackBinfmt, IdentifiesElfWithoutRunningIt) { +UTest identifies_elf_without_running_it{"PackBinfmt/IdentifiesElfWithoutRunningIt", [] { std::array needed{"libc.so.6"}; TempFile f{"elf", elf_with_needed(needed)}; auto id = bf::identify(f.path); - EXPECT_EQ(id.format, bf::Format::Elf); - EXPECT_EQ(id.arch, "x86_64"); - EXPECT_TRUE(id.is64); -} + expect(eq(id.format, bf::Format::Elf)); + expect(eq(id.arch, "x86_64")); + expect(bool(id.is64)); +}}; -TEST(PackBinfmt, ReadsElfDtNeededThroughTheSegmentTable) { +UTest reads_elf_dt_needed_through_the_segment_table{"PackBinfmt/ReadsElfDtNeededThroughTheSegmentTable", [] { // DT_STRTAB is a virtual address, so the parser has to translate it via // PT_LOAD. Two entries, in file order. std::array needed{"libstdc++.so.6", "libcustom.so.1"}; TempFile f{"elfneeded", elf_with_needed(needed)}; auto names = bf::needed_names(f.path); - ASSERT_TRUE(names.has_value()) << names.error(); - ASSERT_EQ(names->size(), 2u); - EXPECT_EQ((*names)[0], "libstdc++.so.6"); - EXPECT_EQ((*names)[1], "libcustom.so.1"); -} + if (auto&& failed = expect(fatal(names.has_value())); !failed) failed << names.error(); + expect(fatal(eq(names->size(), 2u))); + expect(eq((*names)[0], "libstdc++.so.6")); + expect(eq((*names)[1], "libcustom.so.1")); +}}; -TEST(PackBinfmt, AnElfWithNoDynamicSectionHasZeroDepsAndIsNotAnError) { +UTest an_elf_with_no_dynamic_section_has_zero_deps_and_is_not_an_error{"PackBinfmt/AnElfWithNoDynamicSectionHasZeroDepsAndIsNotAnError", [] { // A fully static binary. "No dependencies" and "could not be read" must // not look alike — one is a packageable artifact and the other is a bug. auto bytes = elf_with_needed({}); @@ -380,52 +382,52 @@ TEST(PackBinfmt, AnElfWithNoDynamicSectionHasZeroDepsAndIsNotAnError) { put(bytes, 120, 0, 4); TempFile f{"elfstatic", bytes}; auto names = bf::needed_names(f.path); - ASSERT_TRUE(names.has_value()) << names.error(); - EXPECT_TRUE(names->empty()); -} + if (auto&& failed = expect(fatal(names.has_value())); !failed) failed << names.error(); + expect(names->empty()); +}}; // SPLIT INTO THREE ON PURPOSE. The combined version died with SIGSEGV on the // macOS ARM64 runner and nowhere else, and a single test that builds a // fixture, identifies it and parses it cannot say WHICH of the three it was. // A test that cannot localise its own failure is a test that costs a CI round // per hypothesis. -TEST(PackBinfmt, ThePeFixtureItselfIsWellFormed) { +UTest the_pe_fixture_itself_is_well_formed{"PackBinfmt/ThePeFixtureItselfIsWellFormed", [] { auto bytes = pe_with_imports(pe_imports(), pe_delayed()); - ASSERT_GT(bytes.size(), 0x400u); - EXPECT_EQ(bytes.substr(0, 2), "MZ"); - EXPECT_EQ(bytes.substr(0x40, 4), std::string("PE\0\0", 4)); + expect(fatal(gt(bytes.size(), 0x400u))); + expect(eq(bytes.substr(0, 2), "MZ")); + expect(eq(bytes.substr(0x40, 4), std::string("PE\0\0", 4))); // The names have to be IN the image, or every assertion below is about // the fixture rather than about the reader. for (auto want : {"KERNEL32.dll", "vcruntime140.dll", "dbghelp.dll"}) - EXPECT_NE(bytes.find(want), std::string::npos) << want; -} + expect(ne(bytes.find(want), std::string::npos)) << want; +}}; -TEST(PackBinfmt, IdentifiesPe) { +UTest identifies_pe{"PackBinfmt/IdentifiesPe", [] { TempFile f{"peid", pe_with_imports(pe_imports(), pe_delayed())}; auto id = bf::identify(f.path); - EXPECT_EQ(id.format, bf::Format::Pe); - EXPECT_EQ(id.arch, "x86_64"); - EXPECT_TRUE(id.is64); -} + expect(eq(id.format, bf::Format::Pe)); + expect(eq(id.arch, "x86_64")); + expect(bool(id.is64)); +}}; -TEST(PackBinfmt, ReadsBothPeImportDirectories) { +UTest reads_both_pe_import_directories{"PackBinfmt/ReadsBothPeImportDirectories", [] { TempFile f{"peimp", pe_with_imports(pe_imports(), pe_delayed())}; auto names = bf::needed_names(f.path); - ASSERT_TRUE(names.has_value()) << names.error(); + if (auto&& failed = expect(fatal(names.has_value())); !failed) failed << names.error(); // The DELAY-loaded one is a dependency too, and leaving it out is worse // than leaving out an ordinary import: a missing delay-load does not fail // at startup, it fails at the first call through it. - EXPECT_EQ(names->size(), 3u); + expect(eq(names->size(), 3u)); for (auto want : {"KERNEL32.dll", "vcruntime140.dll", "dbghelp.dll"}) { bool found = false; for (auto const& n : *names) found = found || n == want; - EXPECT_TRUE(found) << want << " missing from the closure"; + expect(bool(found)) << want << " missing from the closure"; } -} +}}; -TEST(PackBinfmt, ADosStubWithoutAPeSignatureIsNotAPe) { +UTest a_dos_stub_without_a_pe_signature_is_not_a_pe{"PackBinfmt/ADosStubWithoutAPeSignatureIsNotAPe", [] { // "MZ" alone is a DOS executable. Calling it PE would send the caller // into a parser that cannot succeed, and the error would describe the // wrong thing. @@ -433,11 +435,11 @@ TEST(PackBinfmt, ADosStubWithoutAPeSignatureIsNotAPe) { b[0] = 'M'; b[1] = 'Z'; put(b, 0x3C, 0x40, 4); // points at zeros, not "PE\0\0" TempFile f{"dos", b}; - EXPECT_EQ(bf::identify(f.path).format, bf::Format::Unknown); - EXPECT_FALSE(bf::needed_names(f.path).has_value()); -} + expect(eq(bf::identify(f.path).format, bf::Format::Unknown)); + expect(!(bf::needed_names(f.path).has_value())); +}}; -TEST(PackBinfmt, AGarbageELfanewIsRejectedAndDoesNotThrow) { +UTest a_garbage_e_lfanew_is_rejected_and_does_not_throw{"PackBinfmt/AGarbageELfanewIsRejectedAndDoesNotThrow", [] { // An "MZ" file whose `e_lfanew` points past the end is ordinary malformed // input: a truncated download, a DOS stub, a text file named `.exe`. // @@ -450,15 +452,15 @@ TEST(PackBinfmt, AGarbageELfanewIsRejectedAndDoesNotThrow) { b[0] = 'M'; b[1] = 'Z'; put(b, 0x3C, lfanew, 4); TempFile f{"mzjunk", b}; - EXPECT_NO_THROW({ - EXPECT_EQ(bf::identify(f.path).format, bf::Format::Unknown) + expect(nothrow([&] { { + expect(eq(bf::identify(f.path).format, bf::Format::Unknown)) << "e_lfanew=" << lfanew; - EXPECT_FALSE(bf::needed_names(f.path).has_value()); - }) << "e_lfanew=" << lfanew; + expect(!(bf::needed_names(f.path).has_value())); + }; })) << "e_lfanew=" << lfanew; } -} +}}; -TEST(PackBinfmt, TruncatedInputIsRejectedRatherThanRead) { +UTest truncated_input_is_rejected_rather_than_read{"PackBinfmt/TruncatedInputIsRejectedRatherThanRead", [] { // Malformed input is ordinary: a half-downloaded file, a text file named // `.exe`. Every read is bounds-checked, so the parser is total over it. for (std::size_t keep : {0u, 4u, 0x40u, 0x80u}) { @@ -466,45 +468,45 @@ TEST(PackBinfmt, TruncatedInputIsRejectedRatherThanRead) { bytes.resize(keep); TempFile f{"trunc", bytes}; auto names = bf::needed_names(f.path); - if (names) EXPECT_TRUE(names->empty()) << "keep=" << keep; + if (names) expect(names->empty()) << "keep=" << keep; } -} +}}; -TEST(PackBinfmt, TheSystemPredicateKnowsWindowsFromTheToolset) { +UTest the_system_predicate_knows_windows_from_the_toolset{"PackBinfmt/TheSystemPredicateKnowsWindowsFromTheToolset", [] { // Windows' own: shipping a private copy is a broken program, not a // heavier one. for (auto n : {"KERNEL32.dll", "kernel32.dll", "ucrtbase.dll", "ntdll.dll", "api-ms-win-crt-runtime-l1-1-0.dll"}) - EXPECT_TRUE(bf::is_system_lib(bf::Format::Pe, n)) << n; + expect(bf::is_system_lib(bf::Format::Pe, n)) << n; // NOT Windows' own. These belong to the TOOLSET, and whether they travel // is `cxx_runtime`'s decision — the one call this predicate must not make // on its behalf. for (auto n : {"vcruntime140.dll", "vcruntime140_1.dll", "msvcp140.dll", "libwinpthread-1.dll", "libstdc++-6.dll"}) - EXPECT_FALSE(bf::is_system_lib(bf::Format::Pe, n)) << n; + expect(!bf::is_system_lib(bf::Format::Pe, n)) << n; // ELF keeps the manylinux allow-list it always had. - EXPECT_TRUE(bf::is_system_lib(bf::Format::Elf, "libstdc++.so.6")); - EXPECT_TRUE(bf::is_system_lib(bf::Format::Elf, "ld-linux-x86-64.so.2")); - EXPECT_FALSE(bf::is_system_lib(bf::Format::Elf, "libcurl.so.4")); -} + expect(bf::is_system_lib(bf::Format::Elf, "libstdc++.so.6")); + expect(bf::is_system_lib(bf::Format::Elf, "ld-linux-x86-64.so.2")); + expect(!bf::is_system_lib(bf::Format::Elf, "libcurl.so.4")); +}}; // ─── Mach-O: #630 §4 — the reader `needed_names` used to refuse ───────── -TEST(PackBinfmt, IdentifiesThinAndFatMachOWithoutRunningIt) { +UTest identifies_thin_and_fat_mach_o_without_running_it{"PackBinfmt/IdentifiesThinAndFatMachOWithoutRunningIt", [] { TempFile thin{"macho-thin", macho_thin(/*is64=*/true, /*bigEndian=*/false, kCpuTypeArm64, {{kLcLoadDylib, kDylibStructSize, "/usr/lib/libSystem.B.dylib"}})}; - EXPECT_EQ(bf::identify(thin.path).format, bf::Format::MachO); + expect(eq(bf::identify(thin.path).format, bf::Format::MachO)); TempFile fat{"macho-fat", macho_fat({{kCpuTypeX86_64, macho_thin(true, false, kCpuTypeX86_64, {})}, {kCpuTypeArm64, macho_thin(true, false, kCpuTypeArm64, {})}})}; - EXPECT_EQ(bf::identify(fat.path).format, bf::Format::MachO); -} + expect(eq(bf::identify(fat.path).format, bf::Format::MachO)); +}}; -TEST(PackBinfmt, ThinMachOReadsNamesAndRpathsInLoadCommandOrder) { +UTest thin_mach_o_reads_names_and_rpaths_in_load_command_order{"PackBinfmt/ThinMachOReadsNamesAndRpathsInLoadCommandOrder", [] { // Two LC_LOAD_DYLIB, one LC_LOAD_WEAK_DYLIB, two LC_RPATH — the record's // §4.3 fixture shape, in one file. TempFile f{"macho-thin-arm64", @@ -517,20 +519,20 @@ TEST(PackBinfmt, ThinMachOReadsNamesAndRpathsInLoadCommandOrder) { })}; auto r = bf::macho_needed(f.path, "aarch64"); - ASSERT_TRUE(r.has_value()) << r.error(); - EXPECT_EQ(r->names, (std::vector{ - "@rpath/libfoo.dylib", "/usr/lib/libSystem.B.dylib", "@rpath/libbar.dylib"})); - EXPECT_EQ(r->rpaths, (std::vector{ - "@executable_path/../Frameworks", "@loader_path/../lib"})); + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); + expect(eq(r->names, (std::vector{ + "@rpath/libfoo.dylib", "/usr/lib/libSystem.B.dylib", "@rpath/libbar.dylib"}))); + expect(eq(r->rpaths, (std::vector{ + "@executable_path/../Frameworks", "@loader_path/../lib"}))); // `needed_names` completes for Mach-O now: names alone, through the same // dispatcher ELF and PE already go through. auto names = bf::needed_names(f.path); - ASSERT_TRUE(names.has_value()) << names.error(); - EXPECT_EQ(*names, r->names); -} + if (auto&& failed = expect(fatal(names.has_value())); !failed) failed << names.error(); + expect(eq(*names, r->names)); +}}; -TEST(PackBinfmt, AWrongArchRequestOnAThinFileStillReadsIt) { +UTest a_wrong_arch_request_on_a_thin_file_still_reads_it{"PackBinfmt/AWrongArchRequestOnAThinFileStillReadsIt", [] { // There is only one slice in a thin file; refusing to read it because // the caller asked for a different architecture would be wrong — the // caller already knows what it built, and mcpp never builds a thin @@ -539,11 +541,11 @@ TEST(PackBinfmt, AWrongArchRequestOnAThinFileStillReadsIt) { macho_thin(true, false, kCpuTypeArm64, {{kLcLoadDylib, kDylibStructSize, "@rpath/libfoo.dylib"}})}; auto r = bf::macho_needed(f.path, "x86_64"); - ASSERT_TRUE(r.has_value()) << r.error(); - EXPECT_EQ(r->names, (std::vector{"@rpath/libfoo.dylib"})); -} + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); + expect(eq(r->names, (std::vector{"@rpath/libfoo.dylib"}))); +}}; -TEST(PackBinfmt, FatMachOReadsOnlyTheRequestedSlice) { +UTest fat_mach_o_reads_only_the_requested_slice{"PackBinfmt/FatMachOReadsOnlyTheRequestedSlice", [] { auto x64 = macho_thin(true, false, kCpuTypeX86_64, {{kLcLoadDylib, kDylibStructSize, "onlyintel.dylib"}}); auto arm = macho_thin(true, false, kCpuTypeArm64, @@ -552,19 +554,18 @@ TEST(PackBinfmt, FatMachOReadsOnlyTheRequestedSlice) { macho_fat({{kCpuTypeX86_64, x64}, {kCpuTypeArm64, arm}})}; auto arm64Read = bf::macho_needed(fat.path, "aarch64"); - ASSERT_TRUE(arm64Read.has_value()) << arm64Read.error(); - EXPECT_EQ(arm64Read->names, (std::vector{"onlyarm.dylib"})); + if (auto&& failed = expect(fatal(arm64Read.has_value())); !failed) failed << arm64Read.error(); + expect(eq(arm64Read->names, (std::vector{"onlyarm.dylib"}))); // A name present only in the OTHER slice must be absent — the whole // point of selecting by architecture rather than reading both. - EXPECT_EQ(std::ranges::find(arm64Read->names, "onlyintel.dylib"), - arm64Read->names.end()); + expect(eq(std::ranges::find(arm64Read->names, "onlyintel.dylib"), arm64Read->names.end())); auto x64Read = bf::macho_needed(fat.path, "x86_64"); - ASSERT_TRUE(x64Read.has_value()) << x64Read.error(); - EXPECT_EQ(x64Read->names, (std::vector{"onlyintel.dylib"})); -} + if (auto&& failed = expect(fatal(x64Read.has_value())); !failed) failed << x64Read.error(); + expect(eq(x64Read->names, (std::vector{"onlyintel.dylib"}))); +}}; -TEST(PackBinfmt, ABigEndianMagicThinMachOIsReadCorrectly) { +UTest a_big_endian_magic_thin_mach_o_is_read_correctly{"PackBinfmt/ABigEndianMagicThinMachOIsReadCorrectly", [] { // The CIGAM leg: every integer in the header and its load commands is // written big-endian, which `macho_thin_needed` has to detect from the // magic alone and undo. @@ -573,20 +574,20 @@ TEST(PackBinfmt, ABigEndianMagicThinMachOIsReadCorrectly) { {{kLcLoadDylib, kDylibStructSize, "/usr/lib/libSystem.B.dylib"}, {kLcRpath, kRpathStructSize, "@executable_path/../lib"}})}; auto r = bf::macho_needed(f.path, {}); - ASSERT_TRUE(r.has_value()) << r.error(); - EXPECT_EQ(r->names, (std::vector{"/usr/lib/libSystem.B.dylib"})); - EXPECT_EQ(r->rpaths, (std::vector{"@executable_path/../lib"})); -} - -TEST(PackBinfmt, TheSystemPredicateKnowsMachOsOwnRoots) { - EXPECT_TRUE(bf::is_system_lib(bf::Format::MachO, "/usr/lib/libSystem.B.dylib")); - EXPECT_TRUE(bf::is_system_lib(bf::Format::MachO, + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); + expect(eq(r->names, (std::vector{"/usr/lib/libSystem.B.dylib"}))); + expect(eq(r->rpaths, (std::vector{"@executable_path/../lib"}))); +}}; + +UTest the_system_predicate_knows_mach_os_own_roots{"PackBinfmt/TheSystemPredicateKnowsMachOsOwnRoots", [] { + expect(bf::is_system_lib(bf::Format::MachO, "/usr/lib/libSystem.B.dylib")); + expect(bf::is_system_lib(bf::Format::MachO, "/System/Library/Frameworks/Foundation.framework/Foundation")); // Not yet resolved to a file -- `@rpath` names a search, not a root. - EXPECT_FALSE(bf::is_system_lib(bf::Format::MachO, "@rpath/libc++.1.dylib")); -} + expect(!bf::is_system_lib(bf::Format::MachO, "@rpath/libc++.1.dylib")); +}}; -TEST(PackBinfmt, ResolveMachoNamesTriesRpathsInOrderAndReportsUnresolved) { +UTest resolve_macho_names_tries_rpaths_in_order_and_reports_unresolved{"PackBinfmt/ResolveMachoNamesTriesRpathsInOrderAndReportsUnresolved", [] { auto dir = std::filesystem::temp_directory_path() / std::format("mcpp-macho-resolve-{}", std::chrono::steady_clock::now().time_since_epoch().count()); @@ -613,28 +614,28 @@ TEST(PackBinfmt, ResolveMachoNamesTriesRpathsInOrderAndReportsUnresolved) { }; auto resolved = bf::resolve_macho_names(names, rpaths, bin, bin); - ASSERT_EQ(resolved.size(), 2u); + expect(fatal(eq(resolved.size(), 2u))); - EXPECT_EQ(resolved[0].name, "@rpath/dup.dylib"); - EXPECT_FALSE(resolved[0].unresolved); + expect(eq(resolved[0].name, "@rpath/dup.dylib")); + expect(!(resolved[0].unresolved)); // `lexically_normal`: the resolver joins `@executable_path/../first` onto // `bin` without collapsing the `..` itself (that is the filesystem's job, // at `exists()`), so the raw and the hand-built path differ textually // while naming the same file. - EXPECT_EQ(resolved[0].path.lexically_normal(), (first / "dup.dylib").lexically_normal()) + expect(eq(resolved[0].path.lexically_normal(), (first / "dup.dylib").lexically_normal())) << "the FIRST rpath entry must win when both would resolve"; // Reported, not skipped: the vector still carries an entry for the name // that resolved nowhere. - EXPECT_EQ(resolved[1].name, "@rpath/nowhere.dylib"); - EXPECT_TRUE(resolved[1].unresolved); - EXPECT_TRUE(resolved[1].path.empty()); + expect(eq(resolved[1].name, "@rpath/nowhere.dylib")); + expect(bool(resolved[1].unresolved)); + expect(resolved[1].path.empty()); std::error_code ec; std::filesystem::remove_all(dir, ec); -} +}}; -TEST(PackBinfmt, ResolveMachoNamesSubstitutesExecutableAndLoaderPath) { +UTest resolve_macho_names_substitutes_executable_and_loader_path{"PackBinfmt/ResolveMachoNamesSubstitutesExecutableAndLoaderPath", [] { auto dir = std::filesystem::temp_directory_path() / std::format("mcpp-macho-resolve2-{}", std::chrono::steady_clock::now().time_since_epoch().count()); @@ -650,28 +651,28 @@ TEST(PackBinfmt, ResolveMachoNamesSubstitutesExecutableAndLoaderPath) { "@loader_path/libplugin.dylib", }; auto resolved = bf::resolve_macho_names(names, {}, exeDir, loaderDir); - ASSERT_EQ(resolved.size(), 2u); - EXPECT_EQ(resolved[0].path, exeDir / "libbeside.dylib"); - EXPECT_FALSE(resolved[0].unresolved); - EXPECT_EQ(resolved[1].path, loaderDir / "libplugin.dylib"); - EXPECT_FALSE(resolved[1].unresolved); + expect(fatal(eq(resolved.size(), 2u))); + expect(eq(resolved[0].path, exeDir / "libbeside.dylib")); + expect(!(resolved[0].unresolved)); + expect(eq(resolved[1].path, loaderDir / "libplugin.dylib")); + expect(!(resolved[1].unresolved)); std::error_code ec; std::filesystem::remove_all(dir, ec); -} +}}; // ─── the zip writer ────────────────────────────────────────────────────── -TEST(PackZip, Crc32MatchesTheKnownVectors) { +UTest crc32_matches_the_known_vectors{"PackZip/Crc32MatchesTheKnownVectors", [] { // Self-consistency proves nothing here: an archive whose CRCs are wrong // extracts with a warning on some tools and silently on others. - EXPECT_EQ(mcpp::pack::zip::crc32(""), 0u); - EXPECT_EQ(mcpp::pack::zip::crc32("123456789"), 0xCBF43926u); - EXPECT_EQ(mcpp::pack::zip::crc32( - "The quick brown fox jumps over the lazy dog"), 0x414FA339u); -} + expect(eq(mcpp::pack::zip::crc32(""), 0u)); + expect(eq(mcpp::pack::zip::crc32("123456789"), 0xCBF43926u)); + expect(eq(mcpp::pack::zip::crc32( + "The quick brown fox jumps over the lazy dog"), 0x414FA339u)); +}}; -TEST(PackZip, WritesAReadableArchiveWithForwardSlashNames) { +UTest writes_a_readable_archive_with_forward_slash_names{"PackZip/WritesAReadableArchiveWithForwardSlashNames", [] { auto dir = std::filesystem::temp_directory_path() / std::format("mcpp-zip-{}", std::chrono::steady_clock::now() .time_since_epoch().count()); @@ -685,29 +686,29 @@ TEST(PackZip, WritesAReadableArchiveWithForwardSlashNames) { }; auto out = dir / "pkg.zip"; auto r = mcpp::pack::zip::write(out, entries); - ASSERT_TRUE(r.has_value()) << r.error(); + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); std::ifstream is(out, std::ios::binary); std::string bytes((std::istreambuf_iterator(is)), {}); - ASSERT_GE(bytes.size(), 22u); - EXPECT_EQ(bytes.substr(0, 4), std::string("PK\x03\x04", 4)) + expect(fatal(ge(bytes.size(), 22u))); + expect(eq(bytes.substr(0, 4), std::string("PK\x03\x04", 4))) << "no local file header signature"; - EXPECT_NE(bytes.find(std::string("PK\x01\x02", 4)), std::string::npos) + expect(ne(bytes.find(std::string("PK\x01\x02", 4)), std::string::npos)) << "no central directory"; - EXPECT_EQ(bytes.substr(bytes.size() - 22, 4), std::string("PK\x05\x06", 4)) + expect(eq(bytes.substr(bytes.size() - 22, 4), std::string("PK\x05\x06", 4))) << "end-of-central-directory is not last (a trailing comment?)"; // Names are stored verbatim; a backslash here is what makes an archive // written on Windows extract to one oddly-named file everywhere else. - EXPECT_NE(bytes.find("pkg/a.txt"), std::string::npos); - EXPECT_EQ(bytes.find("pkg\\a.txt"), std::string::npos); + expect(ne(bytes.find("pkg/a.txt"), std::string::npos)); + expect(eq(bytes.find("pkg\\a.txt"), std::string::npos)); // Stored, not deflated: the payload appears literally. - EXPECT_NE(bytes.find("hello"), std::string::npos); + expect(ne(bytes.find("hello"), std::string::npos)); std::error_code ec; std::filesystem::remove_all(dir, ec); -} +}}; -TEST(PackZip, TwoWritesOfTheSameInputAreByteIdentical) { +UTest two_writes_of_the_same_input_are_byte_identical{"PackZip/TwoWritesOfTheSameInputAreByteIdentical", [] { // A published checksum only means something if the archive is a function // of its contents. Reading mtimes would make it a function of when. auto dir = std::filesystem::temp_directory_path() @@ -721,10 +722,10 @@ TEST(PackZip, TwoWritesOfTheSameInputAreByteIdentical) { std::ifstream is(p, std::ios::binary); return std::string((std::istreambuf_iterator(is)), {}); }; - ASSERT_TRUE(mcpp::pack::zip::write(dir / "one.zip", entries).has_value()); - ASSERT_TRUE(mcpp::pack::zip::write(dir / "two.zip", entries).has_value()); - EXPECT_EQ(read(dir / "one.zip"), read(dir / "two.zip")); + expect(fatal(mcpp::pack::zip::write(dir / "one.zip", entries).has_value())); + expect(fatal(mcpp::pack::zip::write(dir / "two.zip", entries).has_value())); + expect(eq(read(dir / "one.zip"), read(dir / "two.zip"))); std::error_code ec; std::filesystem::remove_all(dir, ec); -} +}}; diff --git a/tests/unit/test_pack_closure.cpp b/tests/unit/test_pack_closure.cpp index aafd6cb67..3fdf2f2bb 100644 --- a/tests/unit/test_pack_closure.cpp +++ b/tests/unit/test_pack_closure.cpp @@ -1,8 +1,10 @@ -#include import std; +import mcpplibs.mtest; import mcpp.pack; +using namespace mcpplibs::mtest; + // #634 A3: the closure `mcpp pack` reads from the files for the PE, Android and // Mach-O rows. The rules are asserted here, over hand-built objects in a // temporary tree, because two of the three rows cannot be produced on the @@ -203,7 +205,7 @@ using Names = std::vector; // ── Android ────────────────────────────────────────────────────────────── -TEST(PackClosureAndroid, TheStubDirectoryIsTheDevicesAndEveryOtherNameTravels) { +UTest the_stub_directory_is_the_devices_and_every_other_name_travels{"PackClosureAndroid/TheStubDirectoryIsTheDevicesAndEveryOtherNameTravels", [] { Tree t; t.write("platform/libc.so", "stub"); t.write("platform/libm.so", "stub"); @@ -221,14 +223,14 @@ TEST(PackClosureAndroid, TheStubDirectoryIsTheDevicesAndEveryOtherNameTravels) { in.platformDirs = {t.root / "platform"}; auto r = mcpp::pack::read_closure(in); - EXPECT_EQ(member_dests(r), (Names{"libc++_shared.so", "libfw.so"})); - EXPECT_EQ(r.platform, (Names{"libc.so", "libm.so"})); - EXPECT_TRUE(r.unresolved.empty()); - ASSERT_EQ(r.members.size(), 2u); - EXPECT_EQ(r.members[0].source, t.root / "ndk" / "libc++_shared.so"); -} + expect(eq(member_dests(r), (Names{"libc++_shared.so", "libfw.so"}))); + expect(eq(r.platform, (Names{"libc.so", "libm.so"}))); + expect(r.unresolved.empty()); + expect(fatal(eq(r.members.size(), 2u))); + expect(eq(r.members[0].source, t.root / "ndk" / "libc++_shared.so")); +}}; -TEST(PackClosureAndroid, ANameFoundNowhereLeavesTheClosureIncomplete) { +UTest a_name_found_nowhere_leaves_the_closure_incomplete{"PackClosureAndroid/ANameFoundNowhereLeavesTheClosureIncomplete", [] { Tree t; t.write("platform/libc.so", "stub"); auto app = t.write("build/libapp.so", elf_needing({"libgone.so", "libc.so"})); @@ -240,16 +242,15 @@ TEST(PackClosureAndroid, ANameFoundNowhereLeavesTheClosureIncomplete) { in.platformDirs = {t.root / "platform"}; auto r = mcpp::pack::read_closure(in); - EXPECT_TRUE(r.members.empty()); - EXPECT_EQ(unresolved_names(r), (Names{"libgone.so"})); - ASSERT_EQ(r.unresolved.size(), 1u); - EXPECT_NE(r.unresolved[0].why.find((t.root / "build").string()), std::string::npos) - << "the reason names where it looked: " << r.unresolved[0].why; -} + expect(r.members.empty()); + expect(eq(unresolved_names(r), (Names{"libgone.so"}))); + expect(fatal(eq(r.unresolved.size(), 1u))); + if (auto&& failed = expect(ne(r.unresolved[0].why.find((t.root / "build").string()), std::string::npos)); !failed) failed << "the reason names where it looked: " << r.unresolved[0].why; +}}; // ── PE, unchanged by the generalisation ────────────────────────────────── -TEST(PackClosurePe, SystemNamesAndNamesFoundNowhereAreTheTargets) { +UTest system_names_and_names_found_nowhere_are_the_targets{"PackClosurePe/SystemNamesAndNamesFoundNowhereAreTheTargets", [] { Tree t; auto exe = t.write("bin/app.exe", pe_importing({"KERNEL32.dll", "foo.dll", "missing.dll"})); @@ -262,12 +263,12 @@ TEST(PackClosurePe, SystemNamesAndNamesFoundNowhereAreTheTargets) { in.searchDirs = {t.root / "bin", t.root / "deps"}; auto r = mcpp::pack::read_closure(in); - EXPECT_EQ(member_dests(r), (Names{"bar.dll", "foo.dll"})); - EXPECT_EQ(r.platform, (Names{"KERNEL32.dll", "missing.dll"})); - EXPECT_TRUE(r.unresolved.empty()) << "PE's rule resolves every name"; -} + expect(eq(member_dests(r), (Names{"bar.dll", "foo.dll"}))); + expect(eq(r.platform, (Names{"KERNEL32.dll", "missing.dll"}))); + expect(r.unresolved.empty()) << "PE's rule resolves every name"; +}}; -TEST(PackClosurePe, ForceBundleReachesTheSystemList) { +UTest force_bundle_reaches_the_system_list{"PackClosurePe/ForceBundleReachesTheSystemList", [] { Tree t; auto exe = t.write("bin/app.exe", pe_importing({"dbghelp.dll"})); t.write("deps/dbghelp.dll", pe_importing({})); @@ -277,13 +278,13 @@ TEST(PackClosurePe, ForceBundleReachesTheSystemList) { in.rule = mcpp::pack::ClosureRule::Pe; in.searchDirs = {t.root / "deps"}; auto without = mcpp::pack::read_closure(in); - EXPECT_EQ(without.platform, (Names{"dbghelp.dll"})); + expect(eq(without.platform, (Names{"dbghelp.dll"}))); in.forceBundle = {"dbghelp.dll"}; auto with = mcpp::pack::read_closure(in); - EXPECT_EQ(member_dests(with), (Names{"dbghelp.dll"})); - EXPECT_TRUE(with.platform.empty()); -} + expect(eq(member_dests(with), (Names{"dbghelp.dll"}))); + expect(with.platform.empty()); +}}; // ── SPEC-007 R4.2/R4.3: one destination, one writer (#723 self-review) ── // @@ -302,7 +303,7 @@ std::string read_bytes(const std::filesystem::path& p) { } // namespace -TEST(PlaceRuntimeDlls, ANameTheDeployListPlacesIsNeitherOverwrittenNorPlaced) { +UTest a_name_the_deploy_list_places_is_neither_overwritten_nor_placed{"PlaceRuntimeDlls/ANameTheDeployListPlacesIsNeitherOverwrittenNorPlaced", [] { Tree t; auto exe = t.write("bin/app.exe", pe_importing({"foo.dll"})); // The deploy list's own file, already beside the program (as it would be @@ -313,16 +314,16 @@ TEST(PlaceRuntimeDlls, ANameTheDeployListPlacesIsNeitherOverwrittenNorPlaced) { auto r = mcpp::pack::place_runtime_dlls( exe, {t.root / "deps"}, /*placedBefore=*/{}, /*placedByDeploy=*/{"foo.dll"}); - ASSERT_TRUE(r.has_value()) << r.error().message; - EXPECT_TRUE(r->names.empty()) << "the deploy list's name was placed a second time"; - EXPECT_TRUE(r->sources.empty()); - ASSERT_EQ(r->warnings.size(), 1u); - EXPECT_NE(r->warnings[0].find("foo.dll"), std::string::npos) << r->warnings[0]; + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error().message; + expect(r->names.empty()) << "the deploy list's name was placed a second time"; + expect(r->sources.empty()); + expect(fatal(eq(r->warnings.size(), 1u))); + if (auto&& failed = expect(ne(r->warnings[0].find("foo.dll"), std::string::npos)); !failed) failed << r->warnings[0]; // Not overwritten: the file beside the program is still the deploy list's. - EXPECT_EQ(read_bytes(t.root / "bin" / "foo.dll"), pe_importing({}) + "DEPLOYED"); -} + expect(eq(read_bytes(t.root / "bin" / "foo.dll"), pe_importing({}) + "DEPLOYED")); +}}; -TEST(PlaceRuntimeDlls, ANameTheDeployListPlacesWithMatchingBytesIsSilent) { +UTest a_name_the_deploy_list_places_with_matching_bytes_is_silent{"PlaceRuntimeDlls/ANameTheDeployListPlacesWithMatchingBytesIsSilent", [] { Tree t; auto exe = t.write("bin/app.exe", pe_importing({"foo.dll"})); const auto bytes = pe_importing({}) + "SAME"; @@ -331,12 +332,12 @@ TEST(PlaceRuntimeDlls, ANameTheDeployListPlacesWithMatchingBytesIsSilent) { auto r = mcpp::pack::place_runtime_dlls( exe, {t.root / "deps"}, {}, {"foo.dll"}); - ASSERT_TRUE(r.has_value()) << r.error().message; - EXPECT_TRUE(r->names.empty()); - EXPECT_TRUE(r->warnings.empty()); -} + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error().message; + expect(r->names.empty()); + expect(r->warnings.empty()); +}}; -TEST(PlaceRuntimeDlls, TheDeployListNameIsMatchedCaseInsensitively) { +UTest the_deploy_list_name_is_matched_case_insensitively{"PlaceRuntimeDlls/TheDeployListNameIsMatchedCaseInsensitively", [] { Tree t; auto exe = t.write("bin/app.exe", pe_importing({"Foo.DLL"})); // The deploy list itself may spell the name differently (it named a @@ -347,27 +348,27 @@ TEST(PlaceRuntimeDlls, TheDeployListNameIsMatchedCaseInsensitively) { auto r = mcpp::pack::place_runtime_dlls( exe, {t.root / "deps"}, {}, {"foo.dll"}); - ASSERT_TRUE(r.has_value()) << r.error().message; - EXPECT_TRUE(r->names.empty()) + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error().message; + expect(r->names.empty()) << "PE names are case-insensitive; the deploy list's entry should still match"; -} +}}; -TEST(PlaceRuntimeDlls, ANameNotInTheDeployListIsPlacedAsBefore) { +UTest a_name_not_in_the_deploy_list_is_placed_as_before{"PlaceRuntimeDlls/ANameNotInTheDeployListIsPlacedAsBefore", [] { Tree t; auto exe = t.write("bin/app.exe", pe_importing({"foo.dll"})); t.write("deps/foo.dll", pe_importing({}) + "PAYLOAD"); auto r = mcpp::pack::place_runtime_dlls(exe, {t.root / "deps"}); - ASSERT_TRUE(r.has_value()) << r.error().message; - EXPECT_EQ(r->names, (Names{"foo.dll"})); - EXPECT_EQ(r->sources, (std::vector{t.root / "deps" / "foo.dll"})); - EXPECT_TRUE(r->warnings.empty()); - EXPECT_EQ(read_bytes(t.root / "bin" / "foo.dll"), pe_importing({}) + "PAYLOAD"); -} + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error().message; + expect(eq(r->names, (Names{"foo.dll"}))); + expect(eq(r->sources, (std::vector{t.root / "deps" / "foo.dll"}))); + expect(r->warnings.empty()); + expect(eq(read_bytes(t.root / "bin" / "foo.dll"), pe_importing({}) + "PAYLOAD")); +}}; // ── Mach-O ─────────────────────────────────────────────────────────────── -TEST(PackClosureMachO, AnRpathDylibBesideTheProgramIsAMemberTransitively) { +UTest an_rpath_dylib_beside_the_program_is_a_member_transitively{"PackClosureMachO/AnRpathDylibBesideTheProgramIsAMemberTransitively", [] { Tree t; auto app = t.write("bin/app", macho_naming({"@rpath/libfw.dylib", "/usr/lib/libSystem.B.dylib"}, @@ -383,12 +384,12 @@ TEST(PackClosureMachO, AnRpathDylibBesideTheProgramIsAMemberTransitively) { in.arch = "aarch64"; auto r = mcpp::pack::read_closure(in); - EXPECT_EQ(member_dests(r), (Names{"libdep.dylib", "libfw.dylib"})); - EXPECT_EQ(r.platform, (Names{"/usr/lib/libSystem.B.dylib", "/usr/lib/libc++.1.dylib"})); - EXPECT_TRUE(r.unresolved.empty()); -} + expect(eq(member_dests(r), (Names{"libdep.dylib", "libfw.dylib"}))); + expect(eq(r.platform, (Names{"/usr/lib/libSystem.B.dylib", "/usr/lib/libc++.1.dylib"}))); + expect(r.unresolved.empty()); +}}; -TEST(PackClosureMachO, ATrailingSlashAndExecutablePathQualifyAsTheProgramsDirectory) { +UTest a_trailing_slash_and_executable_path_qualify_as_the_programs_directory{"PackClosureMachO/ATrailingSlashAndExecutablePathQualifyAsTheProgramsDirectory", [] { Tree t; auto app = t.write("bin/app", macho_naming({"@rpath/liba.dylib", "@executable_path/libb.dylib"}, @@ -400,11 +401,11 @@ TEST(PackClosureMachO, ATrailingSlashAndExecutablePathQualifyAsTheProgramsDirect in.object = app; in.rule = mcpp::pack::ClosureRule::MachO; auto r = mcpp::pack::read_closure(in); - EXPECT_EQ(member_dests(r), (Names{"liba.dylib", "libb.dylib"})); - EXPECT_TRUE(r.unresolved.empty()); -} + expect(eq(member_dests(r), (Names{"liba.dylib", "libb.dylib"}))); + expect(r.unresolved.empty()); +}}; -TEST(PackClosureMachO, AnAbsoluteInstallNameOutsideTheOsRootsIsUnresolved) { +UTest an_absolute_install_name_outside_the_os_roots_is_unresolved{"PackClosureMachO/AnAbsoluteInstallNameOutsideTheOsRootsIsUnresolved", [] { Tree t; // The file exists on this machine; the loader on another one reads that // path, not the tree, so a copy would not be what loads. An install name @@ -420,14 +421,13 @@ TEST(PackClosureMachO, AnAbsoluteInstallNameOutsideTheOsRootsIsUnresolved) { in.object = app; in.rule = mcpp::pack::ClosureRule::MachO; auto r = mcpp::pack::read_closure(in); - EXPECT_TRUE(r.members.empty()); - EXPECT_EQ(unresolved_names(r), (Names{installName})); - ASSERT_EQ(r.unresolved.size(), 1u); - EXPECT_NE(r.unresolved[0].why.find("absolute install name"), std::string::npos) - << r.unresolved[0].why; -} + expect(r.members.empty()); + expect(eq(unresolved_names(r), (Names{installName}))); + expect(fatal(eq(r.unresolved.size(), 1u))); + if (auto&& failed = expect(ne(r.unresolved[0].why.find("absolute install name"), std::string::npos)); !failed) failed << r.unresolved[0].why; +}}; -TEST(PackClosureMachO, AnRpathOnlyAnAbsoluteEntryReachesIsUnresolved) { +UTest an_rpath_only_an_absolute_entry_reaches_is_unresolved{"PackClosureMachO/AnRpathOnlyAnAbsoluteEntryReachesIsUnresolved", [] { Tree t; t.write("store/libfw.dylib", macho_naming({}, {})); auto app = t.write("bin/app", @@ -437,11 +437,10 @@ TEST(PackClosureMachO, AnRpathOnlyAnAbsoluteEntryReachesIsUnresolved) { in.object = app; in.rule = mcpp::pack::ClosureRule::MachO; auto r = mcpp::pack::read_closure(in); - EXPECT_TRUE(r.members.empty()); - EXPECT_EQ(unresolved_names(r), (Names{"@rpath/libfw.dylib"})); - ASSERT_EQ(r.unresolved.size(), 1u); - EXPECT_NE(r.unresolved[0].why.find("@loader_path"), std::string::npos) - << r.unresolved[0].why; + expect(r.members.empty()); + expect(eq(unresolved_names(r), (Names{"@rpath/libfw.dylib"}))); + expect(fatal(eq(r.unresolved.size(), 1u))); + if (auto&& failed = expect(ne(r.unresolved[0].why.find("@loader_path"), std::string::npos)); !failed) failed << r.unresolved[0].why; // The negative direction: the same dylib staged through a qualifying // rpath is a member, sourced from wherever the loader finds it here. @@ -450,12 +449,12 @@ TEST(PackClosureMachO, AnRpathOnlyAnAbsoluteEntryReachesIsUnresolved) { {(t.root / "store").string(), "@loader_path"})); in.object = app2; auto r2 = mcpp::pack::read_closure(in); - EXPECT_EQ(member_dests(r2), (Names{"libfw.dylib"})); - ASSERT_EQ(r2.members.size(), 1u); - EXPECT_EQ(r2.members[0].source, t.root / "store" / "libfw.dylib"); -} + expect(eq(member_dests(r2), (Names{"libfw.dylib"}))); + expect(fatal(eq(r2.members.size(), 1u))); + expect(eq(r2.members[0].source, t.root / "store" / "libfw.dylib")); +}}; -TEST(PackClosureMachO, ALoaderPathSubdirectoryIsStagedAtThatSubdirectory) { +UTest a_loader_path_subdirectory_is_staged_at_that_subdirectory{"PackClosureMachO/ALoaderPathSubdirectoryIsStagedAtThatSubdirectory", [] { Tree t; auto app = t.write("bin/app", macho_naming({"@loader_path/plugins/libp.dylib"}, {})); t.write("bin/plugins/libp.dylib", macho_naming({"@loader_path/libq.dylib"}, {})); @@ -465,11 +464,11 @@ TEST(PackClosureMachO, ALoaderPathSubdirectoryIsStagedAtThatSubdirectory) { in.object = app; in.rule = mcpp::pack::ClosureRule::MachO; auto r = mcpp::pack::read_closure(in); - EXPECT_EQ(member_dests(r), (Names{"plugins/libp.dylib", "plugins/libq.dylib"})); - EXPECT_TRUE(r.unresolved.empty()); -} + expect(eq(member_dests(r), (Names{"plugins/libp.dylib", "plugins/libq.dylib"}))); + expect(r.unresolved.empty()); +}}; -TEST(PackClosureMachO, ANameOutsideTheProgramsDirectoryIsUnresolved) { +UTest a_name_outside_the_programs_directory_is_unresolved{"PackClosureMachO/ANameOutsideTheProgramsDirectoryIsUnresolved", [] { Tree t; t.write("Frameworks/libz.dylib", macho_naming({}, {})); auto app = t.write("bin/app", @@ -479,11 +478,11 @@ TEST(PackClosureMachO, ANameOutsideTheProgramsDirectoryIsUnresolved) { in.object = app; in.rule = mcpp::pack::ClosureRule::MachO; auto r = mcpp::pack::read_closure(in); - EXPECT_TRUE(r.members.empty()); - EXPECT_EQ(unresolved_names(r), (Names{"@executable_path/../Frameworks/libz.dylib"})); -} + expect(r.members.empty()); + expect(eq(unresolved_names(r), (Names{"@executable_path/../Frameworks/libz.dylib"}))); +}}; -TEST(PackClosureMachO, AMissingDylibIsUnresolvedAndNamed) { +UTest a_missing_dylib_is_unresolved_and_named{"PackClosureMachO/AMissingDylibIsUnresolvedAndNamed", [] { Tree t; auto app = t.write("bin/app", macho_naming({"@rpath/libgone.dylib"}, {"@loader_path"})); @@ -491,6 +490,6 @@ TEST(PackClosureMachO, AMissingDylibIsUnresolvedAndNamed) { in.object = app; in.rule = mcpp::pack::ClosureRule::MachO; auto r = mcpp::pack::read_closure(in); - EXPECT_TRUE(r.members.empty()); - EXPECT_EQ(unresolved_names(r), (Names{"@rpath/libgone.dylib"})); -} + expect(r.members.empty()); + expect(eq(unresolved_names(r), (Names{"@rpath/libgone.dylib"}))); +}}; diff --git a/tests/unit/test_pack_distribution.cpp b/tests/unit/test_pack_distribution.cpp index 7f2112f74..833fc629c 100644 --- a/tests/unit/test_pack_distribution.cpp +++ b/tests/unit/test_pack_distribution.cpp @@ -2,13 +2,15 @@ // deflated and aligned members, SHA-256, the tar mcpp writes, what `unpack` // refuses to create, the launchers, the package metadata, `--merge` and the // single file's payload. -#include import std; +import mcpplibs.mtest; import mcpp.pack.compress; import mcpp.pack.distribution; import mcpp.pack.zip; +using namespace mcpplibs::mtest; + namespace fs = std::filesystem; namespace c = mcpp::pack::compress; namespace d = mcpp::pack::distribution; @@ -45,113 +47,112 @@ std::uint32_t le32(std::string_view b, std::size_t at) { // A zip of `entries` at `dir/name`, members stored. fs::path zip_of(const fs::path& dir, std::string_view name, const std::vector& entries) { const auto out = dir / name; - EXPECT_TRUE(mcpp::pack::zip::write(out, entries)); + expect(bool(mcpp::pack::zip::write(out, entries))); return out; } } // namespace -TEST(PackCompress, ParsesAlgorithmsAndLevels) { +UTest parses_algorithms_and_levels{"PackCompress/ParsesAlgorithmsAndLevels", [] { auto z = c::parse("zstd:19"); - ASSERT_TRUE(z); - EXPECT_EQ(z->algorithm, c::Algorithm::Zstd); - EXPECT_EQ(z->level, 19); - EXPECT_TRUE(z->stated); - EXPECT_TRUE(c::parse("none")); - EXPECT_FALSE(c::parse("none:3")); - EXPECT_FALSE(c::parse("gzip:10")); - EXPECT_FALSE(c::parse("brotli")); - EXPECT_EQ(c::tar_extension(c::Algorithm::Xz), ".tar.xz"); - EXPECT_EQ(c::tar_extension(c::Algorithm::None), ".tar"); -} + expect(fatal(bool(z))); + expect(eq(z->algorithm, c::Algorithm::Zstd)); + expect(eq(z->level, 19)); + expect(bool(z->stated)); + expect(bool(c::parse("none"))); + expect(!c::parse("none:3")); + expect(!c::parse("gzip:10")); + expect(!c::parse("brotli")); + expect(eq(c::tar_extension(c::Algorithm::Xz), ".tar.xz")); + expect(eq(c::tar_extension(c::Algorithm::None), ".tar")); +}}; // An unstated level is -1, the codec's default; every stated one is honoured, // `xz:0` and `zstd:1` too. -TEST(PackCompress, AStatedLevelIsHonoured) { - EXPECT_EQ(c::Choice{}.level, -1); - EXPECT_EQ(c::parse("xz")->level, -1); - EXPECT_EQ(c::parse("xz:0")->level, 0); - EXPECT_EQ(c::parse("zstd:1")->level, 1); +UTest a_stated_level_is_honoured{"PackCompress/AStatedLevelIsHonoured", [] { + expect(eq(c::Choice{}.level, -1)); + expect(eq(c::parse("xz")->level, -1)); + expect(eq(c::parse("xz:0")->level, 0)); + expect(eq(c::parse("zstd:1")->level, 1)); std::string text; for (int i = 0; i < 4000; ++i) text += std::format("{} ", i * 7919 % 10007); auto fastest = c::bytes(text, *c::parse("xz:0")); auto preset = c::bytes(text, *c::parse("xz")); - ASSERT_TRUE(fastest && preset); - EXPECT_NE(*fastest, *preset) << "xz:0 was written at the default preset"; + expect(fatal(fastest && preset)); + expect(ne(*fastest, *preset)) << "xz:0 was written at the default preset"; auto z1 = c::bytes(text, *c::parse("zstd:1")); auto z19 = c::bytes(text, *c::parse("zstd")); - ASSERT_TRUE(z1 && z19); - EXPECT_NE(*z1, *z19) << "zstd:1 was written at the default level"; -} + expect(fatal(z1 && z19)); + expect(ne(*z1, *z19)) << "zstd:1 was written at the default level"; +}}; // A gzip member names no file, no time and the system 255, whatever host // wrote it. -TEST(PackCompress, AGzipHeaderIsTheSameOnEveryHost) { +UTest a_gzip_header_is_the_same_on_every_host{"PackCompress/AGzipHeaderIsTheSameOnEveryHost", [] { auto gz = c::bytes("the same bytes", c::Choice{}); - ASSERT_TRUE(gz); - ASSERT_GE(gz->size(), 10u); - EXPECT_EQ(static_cast((*gz)[0]), 0x1f); - EXPECT_EQ(static_cast((*gz)[3]), 0) << "a name, a comment or an extra field"; - EXPECT_EQ(le32(*gz, 4), 0u) << "a modification time"; - EXPECT_EQ(static_cast((*gz)[9]), 255) << "the system byte"; -} - -TEST(PackCompress, EveryCodecRoundTrips) { + expect(fatal(bool(gz))); + expect(fatal(ge(gz->size(), 10u))); + expect(eq(static_cast((*gz)[0]), 0x1f)); + expect(eq(static_cast((*gz)[3]), 0)) << "a name, a comment or an extra field"; + expect(eq(le32(*gz, 4), 0u)) << "a modification time"; + expect(eq(static_cast((*gz)[9]), 255)) << "the system byte"; +}}; + +UTest every_codec_round_trips{"PackCompress/EveryCodecRoundTrips", [] { std::string text; for (int i = 0; i < 4000; ++i) text += std::format("line {} of a package\n", i % 37); for (auto a : { c::Algorithm::Gzip, c::Algorithm::Zstd, c::Algorithm::Xz, c::Algorithm::None }) { c::Choice ch; ch.algorithm = a; auto packed = c::bytes(text, ch); - ASSERT_TRUE(packed) << c::name(a); - if (a != c::Algorithm::None) EXPECT_LT(packed->size(), text.size()) << c::name(a); + expect(fatal(bool(packed))) << c::name(a); + if (a != c::Algorithm::None) expect(lt(packed->size(), text.size())) << c::name(a); auto back = c::decompress(*packed, a); - ASSERT_TRUE(back) << c::name(a); - EXPECT_EQ(*back, text) << c::name(a); + expect(fatal(bool(back))) << c::name(a); + expect(eq(*back, text)) << c::name(a); } auto raw = c::deflate_raw(text, -1); - ASSERT_TRUE(raw); + expect(fatal(bool(raw))); auto back = c::inflate_raw(*raw, text.size()); - ASSERT_TRUE(back); - EXPECT_EQ(*back, text); -} + expect(fatal(bool(back))); + expect(eq(*back, text)); +}}; // The name of a tar archive states its compression, the inverse of // tar_extension; a test archive is written and read by it. -TEST(PackCompress, ATarNameStatesItsCompression) { +UTest a_tar_name_states_its_compression{"PackCompress/ATarNameStatesItsCompression", [] { for (auto a : { c::Algorithm::Gzip, c::Algorithm::Zstd, c::Algorithm::Xz, c::Algorithm::None }) - EXPECT_EQ(c::tar_algorithm(std::format("t{}", c::tar_extension(a))), a) << c::name(a); - EXPECT_EQ(c::tar_algorithm("t.tgz"), c::Algorithm::Gzip); - EXPECT_FALSE(c::tar_algorithm("t.zip")); - EXPECT_FALSE(c::tar_algorithm("t.gz")); - EXPECT_FALSE(c::tar_algorithm("tests")); -} + expect(eq(c::tar_algorithm(std::format("t{}", c::tar_extension(a))), a)) << c::name(a); + expect(eq(c::tar_algorithm("t.tgz"), c::Algorithm::Gzip)); + expect(!c::tar_algorithm("t.zip")); + expect(!c::tar_algorithm("t.gz")); + expect(!c::tar_algorithm("tests")); +}}; // zlib counts in 32 bits; the input is handed over in pieces, so one gzip // member holds more than one piece (1 GiB) and comes back whole. A member cut // short is refused, not waited on. -TEST(PackCompress, AGzipMemberLargerThanOnePieceRoundTrips) { +UTest a_gzip_member_larger_than_one_piece_round_trips{"PackCompress/AGzipMemberLargerThanOnePieceRoundTrips", [] { std::string big((std::size_t{1} << 30) + 4096, '\0'); big.replace(big.size() - 7, 7, "the end"); auto packed = c::bytes(big, *c::parse("gzip:1")); - ASSERT_TRUE(packed) << packed.error(); + if (auto&& failed = expect(fatal(bool(packed))); !failed) failed << packed.error(); auto back = c::decompress(*packed, c::Algorithm::Gzip); - ASSERT_TRUE(back) << back.error(); - EXPECT_EQ(back->size(), big.size()); - EXPECT_TRUE(back->ends_with("the end")); + if (auto&& failed = expect(fatal(bool(back))); !failed) failed << back.error(); + expect(eq(back->size(), big.size())); + expect(back->ends_with("the end")); big = {}; back = {}; - EXPECT_FALSE(c::decompress(std::string_view(*packed).substr(0, packed->size() / 2), c::Algorithm::Gzip)); -} + expect(!(c::decompress(std::string_view(*packed).substr(0, packed->size() / 2), c::Algorithm::Gzip))); +}}; -TEST(PackDistribution, Sha256OfKnownVectors) { - EXPECT_EQ(d::sha256(""), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); - EXPECT_EQ(d::sha256("abc"), "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"); - EXPECT_EQ(d::sha256(std::string(1000, 'a')), - "41edece42d63e8d9bf515a9ba6932e1c20cbc9f5a5d134645adb5db1b9737ea3"); -} +UTest sha256_of_known_vectors{"PackDistribution/Sha256OfKnownVectors", [] { + expect(eq(d::sha256(""), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")); + expect(eq(d::sha256("abc"), "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad")); + expect(eq(d::sha256(std::string(1000, 'a')), "41edece42d63e8d9bf515a9ba6932e1c20cbc9f5a5d134645adb5db1b9737ea3")); +}}; -TEST(PackZip, AnAlignedMemberIsStoredOnItsBoundary) { +UTest an_aligned_member_is_stored_on_its_boundary{"PackZip/AnAlignedMemberIsStoredOnItsBoundary", [] { Tmp t; write(t.path / "a.oke", std::string(3000, 'x')); write(t.path / "libb.okso", std::string(70000, 'y')); @@ -161,16 +162,16 @@ TEST(PackZip, AnAlignedMemberIsStoredOnItsBoundary) { }; mcpp::pack::zip::WriteOptions o; o.align = 65536; - ASSERT_TRUE(mcpp::pack::zip::write(t.path / "out.zip", entries, o)); + expect(fatal(bool(mcpp::pack::zip::write(t.path / "out.zip", entries, o)))); std::ifstream in(t.path / "out.zip", std::ios::binary); std::string b{std::istreambuf_iterator(in), {}}; // Each member's data, found through the central directory, starts on a // 64 KiB boundary and is stored. std::size_t eocd = b.rfind(std::string("PK\x05\x06", 4)); - ASSERT_NE(eocd, std::string::npos); + expect(fatal(ne(eocd, std::string::npos))); std::size_t at = le32(b, eocd + 16); for (int n = 0; n < 2; ++n) { - ASSERT_EQ(le32(b, at), 0x02014b50u); + expect(fatal(eq(le32(b, at), 0x02014b50u))); const auto method = static_cast(b[at + 10]); const auto nameLen = static_cast(static_cast(b[at + 28])) | static_cast(static_cast(b[at + 29])) << 8; @@ -179,106 +180,105 @@ TEST(PackZip, AnAlignedMemberIsStoredOnItsBoundary) { | static_cast(static_cast(b[local + 27])) << 8; const auto lextra = static_cast(static_cast(b[local + 28])) | static_cast(static_cast(b[local + 29])) << 8; - EXPECT_EQ(method, 0); - EXPECT_EQ((local + 30 + lname + lextra) % 65536, 0u); + expect(eq(method, 0)); + expect(eq((local + 30 + lname + lextra) % 65536, 0u)); at += 46 + nameLen; } -} +}}; -TEST(PackZip, DeflateWhenItShrinks) { +UTest deflate_when_it_shrinks{"PackZip/DeflateWhenItShrinks", [] { Tmp t; write(t.path / "text", std::string(10000, 'z')); mcpp::pack::zip::WriteOptions o; o.deflate = true; - ASSERT_TRUE(mcpp::pack::zip::write(t.path / "out.zip", { { "text", t.path / "text", false, {} } }, o)); - EXPECT_LT(fs::file_size(t.path / "out.zip"), 1000u); + expect(fatal(bool(mcpp::pack::zip::write(t.path / "out.zip", { { "text", t.path / "text", false, {} } }, o)))); + expect(lt(fs::file_size(t.path / "out.zip"), 1000u)); // And it reads back. auto root = d::unpack(t.path / "out.zip", t.path / "x"); - ASSERT_FALSE(root) << "a zip without one top-level directory is not a package"; -} + expect(fatal(!root)) << "a zip without one top-level directory is not a package"; +}}; -TEST(PackDistribution, LaunchersPickTheArchitectureAndTheRunner) { +UTest launchers_pick_the_architecture_and_the_runner{"PackDistribution/LaunchersPickTheArchitectureAndTheRunner", [] { d::Launch l; l.name = "hello"; l.runner = { "okrun", "{}" }; l.entry = { { "x86_64", "hello.oke" }, { "aarch64", "hello.oke" } }; l.loader = "okrun"; const auto sh = d::posix_launcher(l); - EXPECT_TRUE(sh.starts_with("#!/bin/sh\n")); - EXPECT_NE(sh.find("x86_64|amd64|AMD64) arch=x86_64"), std::string::npos); - EXPECT_NE(sh.find("'aarch64') entry='hello.oke' ;;"), std::string::npos); - EXPECT_NE(sh.find("image=\"$top/lib/$arch/$entry\""), std::string::npos); - EXPECT_NE(sh.find("runner=\"$top/libexec/$loader/$os-$arch/$loader\""), std::string::npos); - EXPECT_NE(sh.find("exec \"$runner\" \"$image\" \"$@\""), std::string::npos); + expect(sh.starts_with("#!/bin/sh\n")); + expect(ne(sh.find("x86_64|amd64|AMD64) arch=x86_64"), std::string::npos)); + expect(ne(sh.find("'aarch64') entry='hello.oke' ;;"), std::string::npos)); + expect(ne(sh.find("image=\"$top/lib/$arch/$entry\""), std::string::npos)); + expect(ne(sh.find("runner=\"$top/libexec/$loader/$os-$arch/$loader\""), std::string::npos)); + expect(ne(sh.find("exec \"$runner\" \"$image\" \"$@\""), std::string::npos)); // The package is found through a link to the launcher, without readlink -f. - EXPECT_NE(sh.find("readlink -- \"$self\""), std::string::npos); - EXPECT_EQ(sh.find("readlink -f"), std::string::npos); + expect(ne(sh.find("readlink -- \"$self\""), std::string::npos)); + expect(eq(sh.find("readlink -f"), std::string::npos)); // A POSIX shell on Windows hands over to the program of the same name. - EXPECT_NE(sh.find("MINGW*|MSYS*|CYGWIN*) exec \"$top/bin/\"'hello'.exe \"$@\""), std::string::npos) << sh; + expect(ne(sh.find("MINGW*|MSYS*|CYGWIN*) exec \"$top/bin/\"'hello'.exe \"$@\""), std::string::npos)) << sh; // On macOS a runner carrying the quarantine mark of a browser's download is // named, with the command that removes the mark (mcpp design 2026-10-30 // part 2 §10 D6), before it is started. const auto mark = sh.find("xattr -p com.apple.quarantine \"$runner\""); - EXPECT_NE(mark, std::string::npos) << sh; - EXPECT_NE(sh.find("xattr -dr com.apple.quarantine"), std::string::npos) << sh; - EXPECT_LT(mark, sh.find("exec \"$runner\"")) << sh; -} + expect(ne(mark, std::string::npos)) << sh; + expect(ne(sh.find("xattr -dr com.apple.quarantine"), std::string::npos)) << sh; + expect(lt(mark, sh.find("exec \"$runner\""))) << sh; +}}; // The runner's words expand as `mcpp run` expands them: a word's `{}` is the // image inside the word as well as alone; with no `{}` the image is appended. -TEST(PackDistribution, LaunchersExpandTheRunnerAsMcppRunDoes) { +UTest launchers_expand_the_runner_as_mcpp_run_does{"PackDistribution/LaunchersExpandTheRunnerAsMcppRunDoes", [] { d::Launch l; l.name = "hello"; l.entry = { { "x86_64", "hello.oke" } }; l.runner = { "okrun", "--image={}", "it's" }; - EXPECT_NE(d::posix_launcher(l).find("exec \"$runner\" '--image='\"$image\" 'it'\\''s' \"$@\""), - std::string::npos) << d::posix_launcher(l); + expect(ne(d::posix_launcher(l).find("exec \"$runner\" '--image='\"$image\" 'it'\\''s' \"$@\""), std::string::npos)) << d::posix_launcher(l); l.runner = { "okrun", "--quiet" }; - EXPECT_NE(d::posix_launcher(l).find("exec \"$runner\" '--quiet' \"$image\" \"$@\""), std::string::npos) + expect(ne(d::posix_launcher(l).find("exec \"$runner\" '--quiet' \"$image\" \"$@\""), std::string::npos)) << d::posix_launcher(l); -} +}}; // The Windows launcher is a program (design 2026-10-29 §4.3): its source states // the launch as constants, finds the package from its own path, reads the // machine's architecture, and expands the runner's words as `mcpp run` does. // Any word is stated, quotes and percent signs included: a raw string holds it. -TEST(PackDistribution, TheWindowsLauncherIsAProgram) { +UTest the_windows_launcher_is_a_program{"PackDistribution/TheWindowsLauncherIsAProgram", [] { d::Launch l; l.name = "hello"; l.entry = { { "x86_64", "hello.oke" }, { "aarch64", "hello.oke" } }; l.runner = { "okrun", "--say=\"50%\"", "{}" }; l.loader = "okrun"; const auto src = d::exe_launcher_main(l); - EXPECT_NE(src.find("import mcpplibs.os;"), std::string::npos) << src; - EXPECT_NE(src.find("os::env::image()"), std::string::npos) << src; - EXPECT_NE(src.find("PROCESSOR_ARCHITEW6432"), std::string::npos) << src; - EXPECT_NE(src.find("R\"mcpp(--say=\"50%\")mcpp\""), std::string::npos) << src; + expect(ne(src.find("import mcpplibs.os;"), std::string::npos)) << src; + expect(ne(src.find("os::env::image()"), std::string::npos)) << src; + expect(ne(src.find("PROCESSOR_ARCHITEW6432"), std::string::npos)) << src; + expect(ne(src.find("R\"mcpp(--say=\"50%\")mcpp\""), std::string::npos)) << src; // One program for every set of legs: the entry's name, the architectures // the directories under `lib/`. - EXPECT_NE(src.find("kEntry = R\"mcpp(hello.oke)mcpp\""), std::string::npos) << src; - EXPECT_EQ(src.find("R\"mcpp(aarch64)mcpp\""), std::string::npos) << src; - EXPECT_NE(src.find("fs::directory_iterator(top / \"lib\""), std::string::npos) << src; - EXPECT_NE(src.find("(\"windows-\" + arch)"), std::string::npos) << src; - EXPECT_NE(src.find("os::process::exec"), std::string::npos) << src; + expect(ne(src.find("kEntry = R\"mcpp(hello.oke)mcpp\""), std::string::npos)) << src; + expect(eq(src.find("R\"mcpp(aarch64)mcpp\""), std::string::npos)) << src; + expect(ne(src.find("fs::directory_iterator(top / \"lib\""), std::string::npos)) << src; + expect(ne(src.find("(\"windows-\" + arch)"), std::string::npos)) << src; + expect(ne(src.find("os::process::exec"), std::string::npos)) << src; const auto manifest = d::exe_launcher_manifest("hello", "1.0.0"); - EXPECT_NE(manifest.find("mcpplibs.os = \"0.4.0\""), std::string::npos) << manifest; - EXPECT_NE(manifest.find("[targets.hello]"), std::string::npos) << manifest; -} + expect(ne(manifest.find("mcpplibs.os = \"0.4.1\""), std::string::npos)) << manifest; + expect(ne(manifest.find("[targets.\"hello\"]"), std::string::npos)) << manifest; +}}; // The POSIX launcher, run through a link to it from another directory, finds // its package and starts the runner with the image spliced into its word. -TEST(PackDistribution, APosixLauncherRunsThroughALink) { - if (!fs::exists("/bin/sh")) GTEST_SKIP() << "no /bin/sh"; +UTest a_posix_launcher_runs_through_a_link{"PackDistribution/APosixLauncherRunsThroughALink", [] { + if (!fs::exists("/bin/sh")) skip("no /bin/sh"); std::string arch; { Tmp probe; if (std::system(std::format("uname -m > '{}'", (probe.path / "m").string()).c_str()) != 0) - GTEST_SKIP() << "no uname"; + skip("no uname"); arch = slurp(probe.path / "m"); } arch = arch.starts_with("x86_64") || arch.starts_with("amd64") ? "x86_64" : arch.starts_with("aarch64") || arch.starts_with("arm64") ? "aarch64" : ""; - if (arch.empty()) GTEST_SKIP() << "no launcher row for this machine"; + if (arch.empty()) skip("no launcher row for this machine"); Tmp t; const auto pkg = t.path / "my pkg"; d::Launch l; @@ -296,16 +296,16 @@ TEST(PackDistribution, APosixLauncherRunsThroughALink) { const auto rc = std::system(std::format("PATH='{}':\"$PATH\" '{}' 'a b' c > '{}' 2>&1", (t.path / "path").string(), (t.path / "elsewhere/hello").string(), out.string()).c_str()); - EXPECT_EQ(rc, 0) << slurp(out); + expect(eq(rc, 0)) << slurp(out); // The physical path: the launcher follows links to the directories too // (macOS's temporary directory is under a link, /var -> /private/var). - EXPECT_EQ(slurp(out), std::format("--image={}|a b|c|", - (fs::canonical(pkg) / "lib" / arch / "hello.oke").string())); -} + expect(eq(slurp(out), std::format("--image={}|a b|c|", + (fs::canonical(pkg) / "lib" / arch / "hello.oke").string()))); +}}; // A shared object is `.so` or `.so.`; a name that merely holds `.so` // is not one. -TEST(PackDistribution, ALibraryIsNamedSoOrSoVersion) { +UTest a_library_is_named_so_or_so_version{"PackDistribution/ALibraryIsNamedSoOrSoVersion", [] { Tmp t; const auto root = t.path / "app-1.0"; for (auto f : { "lib/libz.so", "lib/libz.so.1.3", "share/notes.sol", "share/a.so.x", "share/b.socket" }) @@ -314,30 +314,30 @@ TEST(PackDistribution, ALibraryIsNamedSoOrSoVersion) { p.name = "app"; p.version = "1.0"; auto described = d::describe(root, p, {}); - ASSERT_TRUE(described) << described.error(); + if (auto&& failed = expect(fatal(bool(described))); !failed) failed << described.error(); std::map roles; for (auto const& f : described->files) roles[f.path] = f.role; - EXPECT_EQ(roles["lib/libz.so"], "library"); - EXPECT_EQ(roles["lib/libz.so.1.3"], "library"); - EXPECT_EQ(roles["share/notes.sol"], "resource"); - EXPECT_EQ(roles["share/a.so.x"], "resource"); - EXPECT_EQ(roles["share/b.socket"], "resource"); -} + expect(eq(roles["lib/libz.so"], "library")); + expect(eq(roles["lib/libz.so.1.3"], "library")); + expect(eq(roles["share/notes.sol"], "resource")); + expect(eq(roles["share/a.so.x"], "resource")); + expect(eq(roles["share/b.socket"], "resource")); +}}; // `read` refuses a document that names a path outside the package. -TEST(PackDistribution, ReadRefusesAPathOutsideThePackage) { +UTest read_refuses_a_path_outside_the_package{"PackDistribution/ReadRefusesAPathOutsideThePackage", [] { Tmp t; for (auto bad : { "../outside", "/etc/passwd", "a/../../b", "./bin/x", "bin\\\\x", "" }) { write(t.path / "mcpp-package.json", std::format(R"({{"schema": 1, "name": "x", "version": "1", "files": [{{"path": "{}", "role": "resource", "sha256": ""}}]}})", bad)); auto r = d::read(t.path); - ASSERT_FALSE(r) << bad; - EXPECT_NE(r.error().find("not a path inside the package"), std::string::npos) << r.error(); + expect(fatal(!r)) << bad; + if (auto&& failed = expect(ne(r.error().find("not a path inside the package"), std::string::npos)); !failed) failed << r.error(); } -} +}}; -TEST(PackDistribution, PackageMetadataDescribesEveryFile) { +UTest package_metadata_describes_every_file{"PackDistribution/PackageMetadataDescribesEveryFile", [] { Tmp t; const auto root = t.path / "app-1.0-openkal"; write(root / "bin/app", "#!/bin/sh\n"); @@ -353,31 +353,31 @@ TEST(PackDistribution, PackageMetadataDescribesEveryFile) { d::Stated s; s.role["bin/app"] = "launcher"; auto described = d::describe(root, p, s); - ASSERT_TRUE(described); + expect(fatal(bool(described))); auto back = d::read(root); - ASSERT_TRUE(back) << back.error(); + if (auto&& failed = expect(fatal(bool(back))); !failed) failed << back.error(); std::map roles; for (auto const& f : back->files) roles[f.path] = f.role; - EXPECT_EQ(roles["bin/app"], "launcher"); - EXPECT_EQ(roles["lib/x86_64/app.oke"], "image"); - EXPECT_EQ(roles["share/data.txt"], "resource"); - EXPECT_EQ(roles["LICENSE"], "metadata"); - EXPECT_EQ(back->entry.at("x86_64"), "lib/x86_64/app.oke"); + expect(eq(roles["bin/app"], "launcher")); + expect(eq(roles["lib/x86_64/app.oke"], "image")); + expect(eq(roles["share/data.txt"], "resource")); + expect(eq(roles["LICENSE"], "metadata")); + expect(eq(back->entry.at("x86_64"), "lib/x86_64/app.oke")); for (auto const& f : back->files) - EXPECT_EQ(f.sha256, d::sha256_file(root / f.path)) << f.path; -} + expect(eq(f.sha256, d::sha256_file(root / f.path))) << f.path; +}}; -TEST(PackDistribution, TreePayloadIsOrderedRecords) { +UTest tree_payload_is_ordered_records{"PackDistribution/TreePayloadIsOrderedRecords", [] { Tmp t; write(t.path / "b/x", "12"); write(t.path / "a", "3"); auto payload = d::tree_payload(t.path); - ASSERT_TRUE(payload); - EXPECT_TRUE(payload->starts_with("MCPPTREE1\n")); + expect(fatal(bool(payload))); + expect(payload->starts_with("MCPPTREE1\n")); // `a` before `b` before `b/x`. const auto a = payload->find("a"), b = payload->find("b/x"); - EXPECT_LT(a, b); -} + expect(lt(a, b)); +}}; // ─── the tar mcpp writes ────────────────────────────────────────────────── @@ -403,69 +403,68 @@ const c::Choice kPlain{ c::Algorithm::None, -1, false }; // Two writes of one tree are the same bytes, its times and modes changed in // between: no owner, no time, the modes normalised. -TEST(PackTar, TheSameTreeIsTheSameBytes) { +UTest the_same_tree_is_the_same_bytes{"PackTar/TheSameTreeIsTheSameBytes", [] { Tmp t; const auto root = sample_tree(t.path); for (auto ch : { kPlain, c::Choice{} }) { - ASSERT_TRUE(d::write_tar(root, t.path / "one", ch)); + expect(fatal(bool(d::write_tar(root, t.path / "one", ch)))); fs::last_write_time(root / "share/data.txt", fs::file_time_type::clock::now() - std::chrono::hours(48)); fs::permissions(root / "share/data.txt", fs::perms::group_write, fs::perm_options::add); - ASSERT_TRUE(d::write_tar(root, t.path / "two", ch)); - EXPECT_EQ(slurp(t.path / "one"), slurp(t.path / "two")) << c::name(ch.algorithm); + expect(fatal(bool(d::write_tar(root, t.path / "two", ch)))); + expect(eq(slurp(t.path / "one"), slurp(t.path / "two"))) << c::name(ch.algorithm); } - ASSERT_TRUE(d::write_tar(root, t.path / "plain.tar", kPlain)); + expect(fatal(bool(d::write_tar(root, t.path / "plain.tar", kPlain)))); const auto tar = slurp(t.path / "plain.tar"); - ASSERT_GE(tar.size(), 1024u); - EXPECT_EQ(tar.substr(0, 9), std::string("app-1.0/\0", 9)); - EXPECT_EQ(tar.substr(108, 8), std::string("0000000\0", 8)) << "uid"; - EXPECT_EQ(tar.substr(116, 8), std::string("0000000\0", 8)) << "gid"; - EXPECT_EQ(tar.substr(136, 12), std::string("00000000000\0", 12)) << "mtime"; - EXPECT_EQ(tar.substr(257, 6), std::string("ustar\0", 6)); - EXPECT_EQ(tar.substr(265, 32), std::string(32, '\0')) << "uname"; - EXPECT_EQ(tar.size() % 512, 0u); -} + expect(fatal(ge(tar.size(), 1024u))); + expect(eq(tar.substr(0, 9), std::string("app-1.0/\0", 9))); + expect(eq(tar.substr(108, 8), std::string("0000000\0", 8))) << "uid"; + expect(eq(tar.substr(116, 8), std::string("0000000\0", 8))) << "gid"; + expect(eq(tar.substr(136, 12), std::string("00000000000\0", 12))) << "mtime"; + expect(eq(tar.substr(257, 6), std::string("ustar\0", 6))); + expect(eq(tar.substr(265, 32), std::string(32, '\0'))) << "uname"; + expect(eq(tar.size() % 512, 0u)); +}}; // What mcpp writes, `unpack` reads back: files, modes, links, long names. -TEST(PackTar, UnpackReadsWhatWriteTarWrote) { +UTest unpack_reads_what_write_tar_wrote{"PackTar/UnpackReadsWhatWriteTarWrote", [] { Tmp t; const auto root = sample_tree(t.path); for (auto ch : { kPlain, c::Choice{}, c::Choice{ c::Algorithm::Zstd, -1, false }, c::Choice{ c::Algorithm::Xz, -1, false } }) { const auto archive = t.path / ("app" + std::string(c::tar_extension(ch.algorithm))); - ASSERT_TRUE(d::write_tar(root, archive, ch)); + expect(fatal(bool(d::write_tar(root, archive, ch)))); const auto into = t.path / ("x-" + std::string(c::name(ch.algorithm))); auto back = d::unpack(archive, into); - ASSERT_TRUE(back) << back.error(); - EXPECT_EQ(back->filename(), "app-1.0"); - EXPECT_EQ(slurp(*back / "share/data.txt"), "data"); - EXPECT_EQ(slurp(*back / "share" / std::string(60, 'd') / (std::string(60, 'e') + ".txt")), "deep"); - EXPECT_NE(fs::status(*back / "bin/app").permissions() & fs::perms::owner_exec, fs::perms::none); - EXPECT_TRUE(fs::is_directory(*back / "empty")); - EXPECT_TRUE(fs::is_symlink(*back / "share/link")); - EXPECT_EQ(fs::read_symlink(*back / "share/far").generic_string(), - std::string(60, 'd') + "/" + std::string(60, 'e') + ".txt"); - EXPECT_EQ(slurp(*back / "share/far"), "deep"); + if (auto&& failed = expect(fatal(bool(back))); !failed) failed << back.error(); + expect(eq(back->filename(), "app-1.0")); + expect(eq(slurp(*back / "share/data.txt"), "data")); + expect(eq(slurp(*back / "share" / std::string(60, 'd') / (std::string(60, 'e') + ".txt")), "deep")); + expect(ne(fs::status(*back / "bin/app").permissions() & fs::perms::owner_exec, fs::perms::none)); + expect(fs::is_directory(*back / "empty")); + expect(fs::is_symlink(*back / "share/link")); + expect(eq(fs::read_symlink(*back / "share/far").generic_string(), std::string(60, 'd') + "/" + std::string(60, 'e') + ".txt")); + expect(eq(slurp(*back / "share/far"), "deep")); } -} +}}; // A tar holding a link out of its tree is refused, and nothing is written. -TEST(PackTar, UnpackRefusesALinkOutOfTheTree) { +UTest unpack_refuses_a_link_out_of_the_tree{"PackTar/UnpackRefusesALinkOutOfTheTree", [] { Tmp t; const auto root = t.path / "app-1.0"; write(root / "a", "a"); fs::create_symlink("../../outside", root / "escape"); - ASSERT_TRUE(d::write_tar(root, t.path / "app.tar", kPlain)); + expect(fatal(bool(d::write_tar(root, t.path / "app.tar", kPlain)))); auto r = d::unpack(t.path / "app.tar", t.path / "x"); - ASSERT_FALSE(r); - EXPECT_NE(r.error().find("outside the package"), std::string::npos) << r.error(); - EXPECT_TRUE(fs::is_empty(t.path / "x")); -} + expect(fatal(!r)); + if (auto&& failed = expect(ne(r.error().find("outside the package"), std::string::npos)); !failed) failed << r.error(); + expect(fs::is_empty(t.path / "x")); +}}; // ─── what unpack refuses ────────────────────────────────────────────────── // A member that would land outside the extraction root, or be written through // a link member, is refused before anything is written. -TEST(PackUnpack, AZipCreatesNothingOutsideItsRoot) { +UTest a_zip_creates_nothing_outside_its_root{"PackUnpack/AZipCreatesNothingOutsideItsRoot", [] { Tmp t; write(t.path / "f", "content"); using E = mcpp::pack::zip::Entry; @@ -487,34 +486,34 @@ TEST(PackUnpack, AZipCreatesNothingOutsideItsRoot) { for (std::size_t i = 0; i < cases.size(); ++i) { const auto into = t.path / std::format("x{}", i); auto r = d::unpack(zip_of(t.path, std::format("c{}.zip", i), cases[i].entries), into); - ASSERT_FALSE(r) << cases[i].label; - EXPECT_NE(r.error().find(cases[i].why), std::string::npos) << cases[i].label << ": " << r.error(); - EXPECT_TRUE(fs::is_empty(into)) << cases[i].label << ": something was written"; + if (auto&& failed = expect(fatal(!r)); !failed) failed << cases[i].label; + if (auto&& failed = expect(ne(r.error().find(cases[i].why), std::string::npos)); !failed) failed << cases[i].label << ": " << r.error(); + if (auto&& failed = expect(fs::is_empty(into)); !failed) failed << cases[i].label << ": something was written"; } - EXPECT_FALSE(fs::exists(t.path / "evil")); + expect(!fs::exists(t.path / "evil")); // A link inside the tree is made, after the files. auto ok = d::unpack(zip_of(t.path, "ok.zip", { { "pkg/l", {}, false, "sub" }, { "pkg/sub/x", f, false, {} } }), t.path / "ok"); - ASSERT_TRUE(ok) << ok.error(); - EXPECT_TRUE(fs::is_symlink(*ok / "l")); - EXPECT_EQ(slurp(*ok / "l/x"), "content"); -} + if (auto&& failed = expect(fatal(bool(ok))); !failed) failed << ok.error(); + expect(fs::is_symlink(*ok / "l")); + expect(eq(slurp(*ok / "l/x"), "content")); +}}; // Every truncation of a zip, and every byte of it changed, is an error or a // tree, never a read past the archive. -TEST(PackUnpack, ATruncatedOrCorruptZipIsAnError) { +UTest a_truncated_or_corrupt_zip_is_an_error{"PackUnpack/ATruncatedOrCorruptZipIsAnError", [] { Tmp t; write(t.path / "f", std::string(300, 'q')); write(t.path / "g", "small"); mcpp::pack::zip::WriteOptions o; o.deflate = true; - ASSERT_TRUE(mcpp::pack::zip::write(t.path / "a.zip", { { "pkg/f", t.path / "f", false, {} }, - { "pkg/g", t.path / "g", true, {} } }, o)); + expect(fatal(bool(mcpp::pack::zip::write(t.path / "a.zip", { { "pkg/f", t.path / "f", false, {} }, + { "pkg/g", t.path / "g", true, {} } }, o)))); const auto whole = slurp(t.path / "a.zip"); for (std::size_t n = 0; n < whole.size(); ++n) { write(t.path / "cut.zip", whole.substr(0, n)); const auto into = t.path / std::format("cut{}", n); - EXPECT_FALSE(d::unpack(t.path / "cut.zip", into)) << n << " bytes"; + expect(!d::unpack(t.path / "cut.zip", into)) << n << " bytes"; fs::remove_all(into); } for (std::size_t n = 0; n < whole.size(); ++n) { @@ -525,21 +524,21 @@ TEST(PackUnpack, ATruncatedOrCorruptZipIsAnError) { (void)d::unpack(t.path / "bad.zip", into); fs::remove_all(into); } -} +}}; // A member whose bytes do not match its CRC-32 is refused. -TEST(PackUnpack, ACrcMismatchIsRefused) { +UTest a_crc_mismatch_is_refused{"PackUnpack/ACrcMismatchIsRefused", [] { Tmp t; write(t.path / "f", "the member's content"); auto zip = slurp(zip_of(t.path, "a.zip", { { "pkg/f", t.path / "f", false, {} } })); const auto at = zip.find("the member's content"); - ASSERT_NE(at, std::string::npos); + expect(fatal(ne(at, std::string::npos))); zip[at] = 'T'; write(t.path / "a.zip", zip); auto r = d::unpack(t.path / "a.zip", t.path / "x"); - ASSERT_FALSE(r); - EXPECT_NE(r.error().find("CRC-32"), std::string::npos) << r.error(); -} + expect(fatal(!r)); + if (auto&& failed = expect(ne(r.error().find("CRC-32"), std::string::npos)); !failed) failed << r.error(); +}}; // ─── --merge ────────────────────────────────────────────────────────────── @@ -556,7 +555,7 @@ fs::path leg_package(const fs::path& dir, std::string_view leg, std::string_view p.legs = { std::string(leg) }; p.runner = { "okrun", "{}" }; p.entry = { { std::string(arch), std::format("lib/{}/app.oke", arch) } }; - EXPECT_TRUE(d::describe(root, p, {})); + expect(bool(d::describe(root, p, {}))); return root; } @@ -564,7 +563,7 @@ fs::path leg_package(const fs::path& dir, std::string_view leg, std::string_view // A file changed after its package was written is refused, named with both // digests. -TEST(PackMerge, AFileThatIsNotItsDigestIsRefused) { +UTest a_file_that_is_not_its_digest_is_refused{"PackMerge/AFileThatIsNotItsDigestIsRefused", [] { Tmp t; const auto a = leg_package(t.path / "a", "x86_64-openkal", "x86_64"); const auto b = leg_package(t.path / "b", "aarch64-openkal", "aarch64"); @@ -573,18 +572,18 @@ TEST(PackMerge, AFileThatIsNotItsDigestIsRefused) { req.inputs = { a, b }; req.output = t.path / "out"; auto r = d::merge(req); - ASSERT_FALSE(r); - EXPECT_NE(r.error().find("lib/aarch64/app.oke"), std::string::npos) << r.error(); - EXPECT_NE(r.error().find(d::sha256("changed")), std::string::npos) << r.error(); - EXPECT_NE(r.error().find(d::sha256("image of aarch64")), std::string::npos) << r.error(); - EXPECT_FALSE(fs::exists(t.path / "out")); -} + expect(fatal(!r)); + if (auto&& failed = expect(ne(r.error().find("lib/aarch64/app.oke"), std::string::npos)); !failed) failed << r.error(); + if (auto&& failed = expect(ne(r.error().find(d::sha256("changed")), std::string::npos)); !failed) failed << r.error(); + if (auto&& failed = expect(ne(r.error().find(d::sha256("image of aarch64")), std::string::npos)); !failed) failed << r.error(); + expect(!fs::exists(t.path / "out")); +}}; // THE LAUNCHERS ARE DERIVED (design 2026-10-29 §4.3): two one-leg trees whose // POSIX launchers differ merge into the tree of both, its launcher written for // both legs; the Windows launcher, one program for every set of legs, is taken // once. A package written before `runs_on` existed is given its pairs. -TEST(PackMerge, TheLaunchersAreWrittenForTheMergedLegs) { +UTest the_launchers_are_written_for_the_merged_legs{"PackMerge/TheLaunchersAreWrittenForTheMergedLegs", [] { Tmp t; auto a = leg_package(t.path / "a", "x86_64-openkal", "x86_64"); auto b = leg_package(t.path / "b", "aarch64-openkal", "aarch64"); @@ -592,34 +591,34 @@ TEST(PackMerge, TheLaunchersAreWrittenForTheMergedLegs) { write(root / "bin/app", std::format("#!/bin/sh\n# the launcher of {}\n", arch)); write(root / "bin/app.exe", std::format("MZ the program of {}", arch)); auto pkg = d::read(root); - ASSERT_TRUE(pkg) << pkg.error(); + if (auto&& failed = expect(fatal(bool(pkg))); !failed) failed << pkg.error(); d::Stated stated; stated.role["bin/app"] = "launcher"; stated.role["bin/app.exe"] = "launcher"; - ASSERT_TRUE(d::describe(root, *pkg, stated)); + expect(fatal(bool(d::describe(root, *pkg, stated)))); } d::MergeRequest req; req.inputs = { a, b }; req.format = "dir"; req.output = t.path / "out"; auto r = d::merge(req); - ASSERT_TRUE(r) << r.error(); + if (auto&& failed = expect(fatal(bool(r))); !failed) failed << r.error(); const auto script = slurp(*r / "bin/app"); - EXPECT_NE(script.find("'x86_64') entry='app.oke'"), std::string::npos) << script; - EXPECT_NE(script.find("'aarch64') entry='app.oke'"), std::string::npos) << script; - EXPECT_EQ(slurp(*r / "bin/app.exe"), "MZ the program of x86_64"); + expect(ne(script.find("'x86_64') entry='app.oke'"), std::string::npos)) << script; + expect(ne(script.find("'aarch64') entry='app.oke'"), std::string::npos)) << script; + expect(eq(slurp(*r / "bin/app.exe"), "MZ the program of x86_64")); auto merged = d::read(*r); - ASSERT_TRUE(merged) << merged.error(); - ASSERT_TRUE(merged->runsOnSet); - ASSERT_TRUE(merged->runsOnWith.contains("okrun")); - EXPECT_NE(std::ranges::find(merged->runsOnWith["okrun"], "macos-aarch64"), merged->runsOnWith["okrun"].end()); + if (auto&& failed = expect(fatal(bool(merged))); !failed) failed << merged.error(); + expect(fatal(bool(merged->runsOnSet))); + expect(fatal(merged->runsOnWith.contains("okrun"))); + expect(ne(std::ranges::find(merged->runsOnWith["okrun"], "macos-aarch64"), merged->runsOnWith["okrun"].end())); for (auto const& f : merged->files) - EXPECT_EQ(d::sha256_file(*r / f.path), f.sha256) << f.path; -} + expect(eq(d::sha256_file(*r / f.path), f.sha256)) << f.path; +}}; // Merged without `-o`, a tree of a platform's legs is named after the platform // (design 2026-10-29 §4.2). -TEST(PackMerge, AMergedTreeIsNamedAfterThePlatform) { +UTest a_merged_tree_is_named_after_the_platform{"PackMerge/AMergedTreeIsNamedAfterThePlatform", [] { Tmp t; std::vector inputs; for (auto [leg, arch] : { std::pair{ "x86_64-openkal", "x86_64" }, std::pair{ "aarch64-openkal", "aarch64" } }) { @@ -632,7 +631,7 @@ TEST(PackMerge, AMergedTreeIsNamedAfterThePlatform) { p.legs = { leg }; p.runner = { "okrun", "{}" }; p.entry = { { arch, std::format("lib/{}/app.oke", arch) } }; - ASSERT_TRUE(d::describe(root, p, {})); + expect(fatal(bool(d::describe(root, p, {})))); inputs.push_back(root); } std::error_code ec; @@ -643,13 +642,13 @@ TEST(PackMerge, AMergedTreeIsNamedAfterThePlatform) { req.format = "dir"; auto r = d::merge(req); fs::current_path(cwd, ec); - ASSERT_TRUE(r) << r.error(); - EXPECT_EQ(r->filename(), "app-1.0-openkal"); -} + if (auto&& failed = expect(fatal(bool(r))); !failed) failed << r.error(); + expect(eq(r->filename(), "app-1.0-openkal")); +}}; // The legs merge into a new directory; an output that exists, or is an input, // is refused, and an input written with a trailing separator is its directory. -TEST(PackMerge, TheOutputIsNewOrAPackage) { +UTest the_output_is_new_or_a_package{"PackMerge/TheOutputIsNewOrAPackage", [] { Tmp t; const auto a = leg_package(t.path / "a", "x86_64-openkal", "x86_64"); const auto b = leg_package(t.path / "b", "aarch64-openkal", "aarch64"); @@ -657,37 +656,37 @@ TEST(PackMerge, TheOutputIsNewOrAPackage) { req.inputs = { fs::path(a.string() + "/"), b }; req.output = t.path / "out"; auto r = d::merge(req); - ASSERT_TRUE(r) << r.error(); + if (auto&& failed = expect(fatal(bool(r))); !failed) failed << r.error(); auto merged = d::read(*r); - ASSERT_TRUE(merged) << merged.error(); - EXPECT_EQ(merged->legs, (std::vector{ "aarch64-openkal", "x86_64-openkal" })); - EXPECT_EQ(slurp(*r / "lib/aarch64/app.oke"), "image of aarch64"); + if (auto&& failed = expect(fatal(bool(merged))); !failed) failed << merged.error(); + expect(eq(merged->legs, (std::vector{ "aarch64-openkal", "x86_64-openkal" }))); + expect(eq(slurp(*r / "lib/aarch64/app.oke"), "image of aarch64")); // Again into the directory it wrote: a directory is never replaced. auto again = d::merge(req); - ASSERT_FALSE(again); - EXPECT_NE(again.error().find("exists"), std::string::npos) << again.error(); + expect(fatal(!again)); + if (auto&& failed = expect(ne(again.error().find("exists"), std::string::npos)); !failed) failed << again.error(); // Onto an input. req.output = a; - ASSERT_FALSE(d::merge(req)); - EXPECT_TRUE(fs::exists(a / "mcpp-package.json")); + expect(fatal(!d::merge(req))); + expect(fs::exists(a / "mcpp-package.json")); // Over a file that is not a package. write(t.path / "notes.tar.gz", "my notes"); req.output = t.path / "notes.tar.gz"; auto over = d::merge(req); - ASSERT_FALSE(over); - EXPECT_NE(over.error().find("not a package"), std::string::npos) << over.error(); - EXPECT_EQ(slurp(t.path / "notes.tar.gz"), "my notes"); + expect(fatal(!over)); + if (auto&& failed = expect(ne(over.error().find("not a package"), std::string::npos)); !failed) failed << over.error(); + expect(eq(slurp(t.path / "notes.tar.gz"), "my notes")); // Over an archive that is one: replaced. req.output = t.path / "app.tar.gz"; - ASSERT_TRUE(d::merge(req)); + expect(fatal(bool(d::merge(req)))); auto replaced = d::merge(req); - ASSERT_TRUE(replaced) << replaced.error(); -} + if (auto&& failed = expect(fatal(bool(replaced))); !failed) failed << replaced.error(); +}}; // What the metadata states reaches the generated Lua as strings: a quote or a // line break in a name is escaped, never code. -TEST(PackDistribution, XpkgDescriptorWritesMetadataAsStrings) { +UTest xpkg_descriptor_writes_metadata_as_strings{"PackDistribution/XpkgDescriptorWritesMetadataAsStrings", [] { d::Asset a; a.file = "evil-1.0-openkal.zip"; a.top = "evil-1.0-openkal"; @@ -697,15 +696,15 @@ TEST(PackDistribution, XpkgDescriptorWritesMetadataAsStrings) { a.pkg.runner = { "okrun", "{}" }; a.pkg.entry = { { "x86_64", "lib/x86_64/evil.oke" } }; auto text = d::xpkg_descriptor({ a }, "https://example.org/v1.0"); - ASSERT_TRUE(text) << text.error(); - EXPECT_NE(text->find("description = \"ev\\\"il\\010os.exit() 1.0"), std::string::npos) << *text; - EXPECT_EQ(text->find("\nos.exit()"), std::string::npos) << *text; - EXPECT_NE(text->find("deps = { \"xim:okrun\" }"), std::string::npos) << *text; -} + if (auto&& failed = expect(fatal(bool(text))); !failed) failed << text.error(); + if (auto&& failed = expect(ne(text->find("description = \"ev\\\"il\\010os.exit() 1.0"), std::string::npos)); !failed) failed << *text; + if (auto&& failed = expect(eq(text->find("\nos.exit()"), std::string::npos)); !failed) failed << *text; + if (auto&& failed = expect(ne(text->find("deps = { \"xim:okrun\" }"), std::string::npos)); !failed) failed << *text; +}}; // The compression fits the container the merge writes and the name it is // given: zstd in a .tar.gz, or in a zip the first input decided, is refused. -TEST(PackMerge, TheCompressionFitsTheContainer) { +UTest the_compression_fits_the_container{"PackMerge/TheCompressionFitsTheContainer", [] { Tmp t; const auto a = leg_package(t.path / "a", "x86_64-openkal", "x86_64"); const auto b = leg_package(t.path / "b", "aarch64-openkal", "aarch64"); @@ -714,20 +713,20 @@ TEST(PackMerge, TheCompressionFitsTheContainer) { req.compression = *c::parse("zstd"); req.output = t.path / "out.tar.gz"; auto named = d::merge(req); - ASSERT_FALSE(named); - EXPECT_NE(named.error().find(".tar.zst"), std::string::npos) << named.error(); + expect(fatal(!named)); + if (auto&& failed = expect(ne(named.error().find(".tar.zst"), std::string::npos)); !failed) failed << named.error(); req.output = t.path / "out.zip"; - ASSERT_FALSE(d::merge(req)); + expect(fatal(!d::merge(req))); req.output = t.path / "out.tar.zst"; auto fits = d::merge(req); - ASSERT_TRUE(fits) << fits.error(); -} + if (auto&& failed = expect(fatal(bool(fits))); !failed) failed << fits.error(); +}}; // #860 (design 2026-10-29 §4.4): a self-contained tree of images runs where // it carries a runner, and its entry names exactly those pairs -- macOS aarch64 // only, Windows x86_64 only -- with no runner to install. A package that needs // the machine's runner names the pairs the platform's runner exists for. -TEST(PackDistribution, AnEntryNamesWhereThePackageRuns) { +UTest an_entry_names_where_the_package_runs{"PackDistribution/AnEntryNamesWhereThePackageRuns", [] { d::Asset a; a.file = "mcpp-ide-0.1.1-openkal.tar.gz"; a.sha256 = std::string(64, 'c'); @@ -742,43 +741,43 @@ TEST(PackDistribution, AnEntryNamesWhereThePackageRuns) { const std::vector hosts = { "linux-x86_64", "linux-aarch64", "macos-aarch64", "macos-x86_64", "windows-x86_64" }; d::compute_runs_on(a.pkg, hosts, false); - EXPECT_EQ(a.pkg.runsOnSelf, a.pkg.systems); - ASSERT_TRUE(a.pkg.runsOnWith.contains("okrun")); - EXPECT_EQ(a.pkg.runsOnWith.at("okrun"), (std::vector{"macos-x86_64"})); + expect(eq(a.pkg.runsOnSelf, a.pkg.systems)); + expect(fatal(a.pkg.runsOnWith.contains("okrun"))); + expect(eq(a.pkg.runsOnWith.at("okrun"), (std::vector{"macos-x86_64"}))); auto r = d::xpkg_descriptor({ a }, "https://example.org/v0.1.1"); - ASSERT_TRUE(r) << r.error(); + if (auto&& failed = expect(fatal(bool(r))); !failed) failed << r.error(); auto block = [&](std::string_view system) { const auto at = r->find(std::format(" {} = {{", system)); return at == std::string::npos ? std::string() : r->substr(at, r->find(" },", at) - at); }; - EXPECT_NE(block("macosx").find("[\"aarch64\"]"), std::string::npos) << *r; - EXPECT_EQ(block("macosx").find("[\"x86_64\"]"), std::string::npos) << *r; - EXPECT_NE(block("windows").find("[\"x86_64\"]"), std::string::npos) << *r; - EXPECT_EQ(block("windows").find("[\"aarch64\"]"), std::string::npos) << *r; - EXPECT_EQ(r->find("deps = {"), std::string::npos) << *r; + if (auto&& failed = expect(ne(block("macosx").find("[\"aarch64\"]"), std::string::npos)); !failed) failed << *r; + if (auto&& failed = expect(eq(block("macosx").find("[\"x86_64\"]"), std::string::npos)); !failed) failed << *r; + if (auto&& failed = expect(ne(block("windows").find("[\"x86_64\"]"), std::string::npos)); !failed) failed << *r; + if (auto&& failed = expect(eq(block("windows").find("[\"aarch64\"]"), std::string::npos)); !failed) failed << *r; + if (auto&& failed = expect(eq(r->find("deps = {"), std::string::npos)); !failed) failed << *r; a.pkg.mode = "vendored"; a.pkg.systems.clear(); d::compute_runs_on(a.pkg, hosts, false); - EXPECT_TRUE(a.pkg.runsOnSelf.empty()); - EXPECT_EQ(a.pkg.runsOnWith.at("okrun").size(), hosts.size()); + expect(a.pkg.runsOnSelf.empty()); + expect(eq(a.pkg.runsOnWith.at("okrun").size(), hosts.size())); auto v = d::xpkg_descriptor({ a }, "https://example.org/v0.1.1"); - ASSERT_TRUE(v) << v.error(); - EXPECT_NE(v->find("deps = { \"xim:okrun\" }"), std::string::npos) << *v; + if (auto&& failed = expect(fatal(bool(v))); !failed) failed << v.error(); + if (auto&& failed = expect(ne(v->find("deps = { \"xim:okrun\" }"), std::string::npos)); !failed) failed << *v; // A platform that states no hosts for its runner is read as every system // of each architecture, as before runs_on existed. d::compute_runs_on(a.pkg, {}, false); - EXPECT_EQ(a.pkg.runsOnWith.at("okrun").size(), 6u); + expect(eq(a.pkg.runsOnWith.at("okrun").size(), 6u)); // The document carries it, and a reader takes it back. auto j = d::to_json(a.pkg); - EXPECT_NE(j.find("\"runs_on\""), std::string::npos) << j; -} + expect(ne(j.find("\"runs_on\""), std::string::npos)) << j; +}}; // The entry is named after the command a person runs, not the package, and // moves the archive's own top-level directory into place. -TEST(PackDistribution, XpkgDescriptorNamesTheCommandAndTheArchiveTop) { +UTest xpkg_descriptor_names_the_command_and_the_archive_top{"PackDistribution/XpkgDescriptorNamesTheCommandAndTheArchiveTop", [] { d::Asset uni; uni.file = "mcppls-0.0.18-openkal.zip"; uni.sha256 = std::string(64, 'a'); @@ -788,13 +787,13 @@ TEST(PackDistribution, XpkgDescriptorNamesTheCommandAndTheArchiveTop) { uni.pkg.runner = { "okrun", "{}" }; uni.pkg.entry = { { "x86_64", "lib/x86_64/mcppls.oke" }, { "aarch64", "lib/aarch64/mcppls.oke" } }; auto u = d::xpkg_descriptor({ uni }, "https://example.org/v0.0.18"); - ASSERT_TRUE(u) << u.error(); - EXPECT_NE(u->find("name = \"mcppls\""), std::string::npos) << *u; - EXPECT_NE(u->find("xvm.add(\"mcppls\", opts)"), std::string::npos) << *u; - EXPECT_NE(u->find("opts.alias = \"mcppls.exe\""), std::string::npos) << *u; - EXPECT_NE(u->find("for _, ext in ipairs({ \".zip\" }) do"), std::string::npos) << *u; - EXPECT_NE(u->find("local top = pkginfo.install_file()"), std::string::npos) << *u; - EXPECT_NE(u->find("os.mv(top, pkginfo.install_dir())"), std::string::npos) << *u; + if (auto&& failed = expect(fatal(bool(u))); !failed) failed << u.error(); + if (auto&& failed = expect(ne(u->find("name = \"mcppls\""), std::string::npos)); !failed) failed << *u; + if (auto&& failed = expect(ne(u->find("xvm.add(\"mcppls\", opts)"), std::string::npos)); !failed) failed << *u; + if (auto&& failed = expect(ne(u->find("opts.alias = \"mcppls.exe\""), std::string::npos)); !failed) failed << *u; + if (auto&& failed = expect(ne(u->find("for _, ext in ipairs({ \".zip\" }) do"), std::string::npos)); !failed) failed << *u; + if (auto&& failed = expect(ne(u->find("local top = pkginfo.install_file()"), std::string::npos)); !failed) failed << *u; + if (auto&& failed = expect(ne(u->find("os.mv(top, pkginfo.install_dir())"), std::string::npos)); !failed) failed << *u; std::vector per; for (auto [triple, top] : { std::pair{ "x86_64-linux-musl", "okrun-0.1.3-linux-x86_64" }, @@ -810,16 +809,16 @@ TEST(PackDistribution, XpkgDescriptorNamesTheCommandAndTheArchiveTop) { per.push_back(a); } auto r = d::xpkg_descriptor(per, "https://example.org/0.1.3"); - ASSERT_TRUE(r) << r.error(); - EXPECT_NE(r->find("name = \"okrun\""), std::string::npos) << *r; - EXPECT_NE(r->find("for _, ext in ipairs({ \".zip\" }) do"), std::string::npos) << *r; - EXPECT_NE(r->find("path.join(pkginfo.install_dir(), \"bin\")"), std::string::npos) << *r; -} + if (auto&& failed = expect(fatal(bool(r))); !failed) failed << r.error(); + if (auto&& failed = expect(ne(r->find("name = \"okrun\""), std::string::npos)); !failed) failed << *r; + if (auto&& failed = expect(ne(r->find("for _, ext in ipairs({ \".zip\" }) do"), std::string::npos)); !failed) failed << *r; + if (auto&& failed = expect(ne(r->find("path.join(pkginfo.install_dir(), \"bin\")"), std::string::npos)); !failed) failed << *r; +}}; // The install takes the package's directory from the downloaded archive's // name, which is what `mcpp pack` names it after; an archive renamed since // is refused, and so is a file xim does not unpack. -TEST(PackDistribution, XpkgDescriptorInstallsTheDirectoryNamedAfterTheArchive) { +UTest xpkg_descriptor_installs_the_directory_named_after_the_archive{"PackDistribution/XpkgDescriptorInstallsTheDirectoryNamedAfterTheArchive", [] { d::Asset a; a.file = "tool-1.0-linux-x86_64.tar.gz"; a.top = "tool-1.0-linux-x86_64"; @@ -834,33 +833,33 @@ TEST(PackDistribution, XpkgDescriptorInstallsTheDirectoryNamedAfterTheArchive) { w.pkg.legs = { "x86_64-windows-gnu" }; w.pkg.program = "tool.exe"; auto r = d::xpkg_descriptor({ a, w }, "https://example.org/1.0"); - ASSERT_TRUE(r) << r.error(); - EXPECT_NE(r->find("for _, ext in ipairs({ \".tar.gz\", \".zip\" }) do"), std::string::npos) << *r; + if (auto&& failed = expect(fatal(bool(r))); !failed) failed << r.error(); + if (auto&& failed = expect(ne(r->find("for _, ext in ipairs({ \".tar.gz\", \".zip\" }) do"), std::string::npos)); !failed) failed << *r; d::Asset renamed = a; renamed.file = "tool.tar.gz"; auto refused = d::xpkg_descriptor({ renamed }, "https://example.org/1.0"); - ASSERT_FALSE(refused); - EXPECT_NE(refused.error().find("named after the archive"), std::string::npos) << refused.error(); + expect(fatal(!refused)); + if (auto&& failed = expect(ne(refused.error().find("named after the archive"), std::string::npos)); !failed) failed << refused.error(); d::Asset tgz = a; tgz.file = "tool-1.0-linux-x86_64.tgz"; auto t = d::xpkg_descriptor({ tgz }, "https://example.org/1.0"); - ASSERT_TRUE(t) << t.error(); - EXPECT_NE(t->find("for _, ext in ipairs({ \".tgz\" }) do"), std::string::npos) << *t; - EXPECT_EQ(d::archive_directory("app-1.0-openkal.tar.zst"), "app-1.0-openkal"); - EXPECT_EQ(d::archive_directory("app-1.0-openkal"), "app-1.0-openkal"); + if (auto&& failed = expect(fatal(bool(t))); !failed) failed << t.error(); + if (auto&& failed = expect(ne(t->find("for _, ext in ipairs({ \".tgz\" }) do"), std::string::npos)); !failed) failed << *t; + expect(eq(d::archive_directory("app-1.0-openkal.tar.zst"), "app-1.0-openkal")); + expect(eq(d::archive_directory("app-1.0-openkal"), "app-1.0-openkal")); d::Asset okb = a; okb.file = "tool-1.0-linux-x86_64.okb"; auto notArchive = d::xpkg_descriptor({ okb }, "https://example.org/1.0"); - ASSERT_FALSE(notArchive); - EXPECT_NE(notArchive.error().find("not an archive xim unpacks"), std::string::npos) << notArchive.error(); -} + expect(fatal(!notArchive)); + if (auto&& failed = expect(ne(notArchive.error().find("not an archive xim unpacks"), std::string::npos)); !failed) failed << notArchive.error(); +}}; // Packages of two systems that place their program apart -- a Windows one at // the root, a POSIX one under bin/ -- give each system its own directory. -TEST(PackDistribution, XpkgDescriptorNamesEachSystemsProgramDirectory) { +UTest xpkg_descriptor_names_each_systems_program_directory{"PackDistribution/XpkgDescriptorNamesEachSystemsProgramDirectory", [] { std::vector per; for (auto [triple, top, program] : { std::tuple{ "x86_64-linux-musl", "okrun-0.1.3-linux-x86_64", "bin/okrun" }, @@ -877,12 +876,11 @@ TEST(PackDistribution, XpkgDescriptorNamesEachSystemsProgramDirectory) { per.push_back(a); } auto r = d::xpkg_descriptor(per, "https://example.org/0.1.3"); - ASSERT_TRUE(r) << r.error(); - EXPECT_NE(r->find("local bindir = { linux = \"bin\", macosx = \"bin\", windows = \".\" }"), - std::string::npos) << *r; - EXPECT_NE(r->find("path.join(pkginfo.install_dir(), bindir[os.host()])"), std::string::npos) << *r; - EXPECT_NE(r->find("name = \"okrun\""), std::string::npos) << *r; -} + if (auto&& failed = expect(fatal(bool(r))); !failed) failed << r.error(); + if (auto&& failed = expect(ne(r->find("local bindir = { linux = \"bin\", macosx = \"bin\", windows = \".\" }"), std::string::npos)); !failed) failed << *r; + if (auto&& failed = expect(ne(r->find("path.join(pkginfo.install_dir(), bindir[os.host()])"), std::string::npos)); !failed) failed << *r; + if (auto&& failed = expect(ne(r->find("name = \"okrun\""), std::string::npos)); !failed) failed << *r; +}}; // An entry describes packages of one leg for the system their program runs on // (SPEC-014 §8): a Linux, macOS or Windows program, or the carrier of an ok @@ -891,7 +889,7 @@ TEST(PackDistribution, XpkgDescriptorNamesEachSystemsProgramDirectory) { // application, a bare-metal image, an ok image without its carrier -- is // refused, naming the package, the triple and the route that distributes it; // it was written `linux` before. -TEST(PackDistribution, XpkgDescriptorDescribesProgramsOfASystem) { +UTest xpkg_descriptor_describes_programs_of_a_system{"PackDistribution/XpkgDescriptorDescribesProgramsOfASystem", [] { auto one = [](std::string triple, std::string top, std::string program) { d::Asset a; a.file = top + ".tar.gz"; @@ -910,10 +908,10 @@ TEST(PackDistribution, XpkgDescriptorDescribesProgramsOfASystem) { Row{ "x86_64-openkal", "--target openkal" } }) { auto r = d::xpkg_descriptor({ one(triple, std::format("web-0.1.0-{}", triple), "bin/web.js") }, "https://example.org/0.1.0"); - ASSERT_FALSE(r) << triple << ": " << *r; - EXPECT_NE(r.error().find(std::format("is a package of {}, which is no program of Linux, " - "macOS or Windows", triple)), std::string::npos) << r.error(); - EXPECT_NE(r.error().find(says), std::string::npos) << r.error(); + if (auto&& failed = expect(fatal(!r)); !failed) failed << triple << ": " << *r; + if (auto&& failed = expect(ne(r.error().find(std::format("is a package of {}, which is no program of Linux, " + "macOS or Windows", triple)), std::string::npos)); !failed) failed << r.error(); + if (auto&& failed = expect(ne(r.error().find(says), std::string::npos)); !failed) failed << r.error(); } for (auto [triple, system] : { std::pair{ "x86_64-linux-gnu", "linux" }, std::pair{ "aarch64-linux-musl", "linux" }, @@ -921,8 +919,8 @@ TEST(PackDistribution, XpkgDescriptorDescribesProgramsOfASystem) { std::pair{ "x86_64-windows-msvc", "windows" } }) { auto r = d::xpkg_descriptor({ one(triple, std::format("tool-0.1.0-{}", triple), "bin/tool") }, "https://example.org/0.1.0"); - ASSERT_TRUE(r) << triple << ": " << r.error(); - EXPECT_NE(r->find(std::format(" {} = {{", system)), std::string::npos) << *r; + if (auto&& failed = expect(fatal(bool(r))); !failed) failed << triple << ": " << r.error(); + if (auto&& failed = expect(ne(r->find(std::format(" {} = {{", system)), std::string::npos)); !failed) failed << *r; } // A one-leg ok image packed with its carrier on Linux: a Linux program of // the image's architecture, as `systems` records it. @@ -930,20 +928,20 @@ TEST(PackDistribution, XpkgDescriptorDescribesProgramsOfASystem) { carried.pkg.mode = "self-contained"; carried.pkg.systems = { "linux-x86_64" }; auto c = d::xpkg_descriptor({ carried }, "https://example.org/0.1.0"); - ASSERT_TRUE(c) << c.error(); - EXPECT_NE(c->find("archs = {\"x86_64\"}"), std::string::npos) << *c; - EXPECT_NE(c->find(" linux = {"), std::string::npos) << *c; - EXPECT_EQ(c->find("macosx = {"), std::string::npos) << *c; - EXPECT_NE(c->find("app-0.1.0-${arch}-openkal-bundle-all.tar.gz"), std::string::npos) << *c; - EXPECT_NE(c->find("path.join(pkginfo.install_dir(), \"bin\")"), std::string::npos) << *c; + if (auto&& failed = expect(fatal(bool(c))); !failed) failed << c.error(); + if (auto&& failed = expect(ne(c->find("archs = {\"x86_64\"}"), std::string::npos)); !failed) failed << *c; + if (auto&& failed = expect(ne(c->find(" linux = {"), std::string::npos)); !failed) failed << *c; + if (auto&& failed = expect(eq(c->find("macosx = {"), std::string::npos)); !failed) failed << *c; + if (auto&& failed = expect(ne(c->find("app-0.1.0-${arch}-openkal-bundle-all.tar.gz"), std::string::npos)); !failed) failed << *c; + if (auto&& failed = expect(ne(c->find("path.join(pkginfo.install_dir(), \"bin\")"), std::string::npos)); !failed) failed << *c; // ...and on a macOS machine of the other architecture, a macOS program. carried.pkg.systems = { "macos-aarch64" }; carried.pkg.legs = { "aarch64-openkal" }; carried.file = "app-0.1.0-aarch64-openkal-bundle-all.tar.gz"; carried.top = "app-0.1.0-aarch64-openkal-bundle-all"; auto m = d::xpkg_descriptor({ carried }, "https://example.org/0.1.0"); - ASSERT_TRUE(m) << m.error(); - EXPECT_NE(m->find("archs = {\"aarch64\"}"), std::string::npos) << *m; - EXPECT_NE(m->find(" macosx = {"), std::string::npos) << *m; - EXPECT_EQ(m->find("linux = {"), std::string::npos) << *m; -} + if (auto&& failed = expect(fatal(bool(m))); !failed) failed << m.error(); + if (auto&& failed = expect(ne(m->find("archs = {\"aarch64\"}"), std::string::npos)); !failed) failed << *m; + if (auto&& failed = expect(ne(m->find(" macosx = {"), std::string::npos)); !failed) failed << *m; + if (auto&& failed = expect(eq(m->find("linux = {"), std::string::npos)); !failed) failed << *m; +}}; diff --git a/tests/unit/test_pack_interface.cpp b/tests/unit/test_pack_interface.cpp index a5cfadf6b..c92e1dec7 100644 --- a/tests/unit/test_pack_interface.cpp +++ b/tests/unit/test_pack_interface.cpp @@ -1,9 +1,11 @@ -#include import std; +import mcpplibs.mtest; import mcpp.pack.interface; import mcpp.modgraph.graph; +using namespace mcpplibs::mtest; + using namespace mcpp::pack; using mcpp::modgraph::Graph; using mcpp::modgraph::ModuleId; @@ -65,60 +67,58 @@ std::vector names(const std::vector& v) { // ─── what travels, and what does not ─────────────────────────────────────── -TEST(InterfaceClosure, PublishesTheRootAndItsInterfacePartitionOnly) { +UTest publishes_the_root_and_its_interface_partition_only{"InterfaceClosure/PublishesTheRootAndItsInterfacePartitionOnly", [] { auto c = interface_closure(library_graph(), "mathkit", "mathkit"); - ASSERT_TRUE(c.has_value()) << (c ? "" : c.error()); - EXPECT_EQ(names(c->published), (std::vector{"api.cppm", "mathkit.cppm"})); -} + if (auto&& failed = expect(fatal(c.has_value())); !failed) failed << (c ? "" : c.error()); + expect(eq(names(c->published), (std::vector{"api.cppm", "mathkit.cppm"}))); +}}; -TEST(InterfaceClosure, WithholdsTheImplementationPartitionSource) { +UTest withholds_the_implementation_partition_source{"InterfaceClosure/WithholdsTheImplementationPartitionSource", [] { // The whole point for a closed-source library: `secret.cppm` produces a // BMI and an object, and its SOURCE must not be published. auto c = interface_closure(library_graph(), "mathkit", "mathkit"); - ASSERT_TRUE(c.has_value()); - EXPECT_EQ(names(c->withheld), - (std::vector{"capi.c", "impl.cpp", "secret.cppm"})); -} + expect(fatal(c.has_value())); + expect(eq(names(c->withheld), (std::vector{"capi.c", "impl.cpp", "secret.cppm"}))); +}}; -TEST(InterfaceClosure, DropSetIsThePublishedObjectsNotEveryModuleObject) { +UTest drop_set_is_the_published_objects_not_every_module_object{"InterfaceClosure/DropSetIsThePublishedObjectsNotEveryModuleObject", [] { // Measured: deleting every `.m.o` also deletes `secret.m.o`, which holds // real code, and every target then fails to link with an undefined // reference that names neither the archive nor the rule that removed it. auto c = interface_closure(library_graph(), "mathkit", "mathkit"); - ASSERT_TRUE(c.has_value()); + expect(fatal(c.has_value())); auto drop = published_object_names(*c); std::ranges::sort(drop); - EXPECT_EQ(drop, (std::vector{"api.m.o", "mathkit.m.o"})); - EXPECT_EQ(std::ranges::find(drop, "secret.m.o"), drop.end()); -} + expect(eq(drop, (std::vector{"api.m.o", "mathkit.m.o"}))); + expect(eq(std::ranges::find(drop, "secret.m.o"), drop.end())); +}}; // ─── the loud half of the asymmetry ──────────────────────────────────────── -TEST(InterfaceClosure, AnInterfaceThatReachesAnImplementationPartitionPublishesItAndSaysSo) { +UTest an_interface_that_reaches_an_implementation_partition_publishes_it_and_says_so{"InterfaceClosure/AnInterfaceThatReachesAnImplementationPartitionPublishesItAndSaysSo", [] { // Legal, and the consumer cannot build the interface's BMI without that // source — so it is published rather than refused. But for a closed-source // library it is the one outcome nobody wants by accident, and `import // :secret;` in an interface reads like any other import, so it is reported. auto c = interface_closure(library_graph(/*interfaceReachesSecret=*/true), "mathkit", "mathkit"); - ASSERT_TRUE(c.has_value()); - EXPECT_EQ(names(c->published), - (std::vector{"api.cppm", "mathkit.cppm", "secret.cppm"})); - ASSERT_EQ(c->publishedImplementationPartitions.size(), 1u); - EXPECT_EQ(c->publishedImplementationPartitions[0].filename().string(), "secret.cppm"); - EXPECT_TRUE(c->unresolvedImports.empty()); -} - -TEST(InterfaceClosure, AnInterfaceOnlyClosureReportsNoPartitionLeak) { + expect(fatal(c.has_value())); + expect(eq(names(c->published), (std::vector{"api.cppm", "mathkit.cppm", "secret.cppm"}))); + expect(fatal(eq(c->publishedImplementationPartitions.size(), 1u))); + expect(eq(c->publishedImplementationPartitions[0].filename().string(), "secret.cppm")); + expect(c->unresolvedImports.empty()); +}}; + +UTest an_interface_only_closure_reports_no_partition_leak{"InterfaceClosure/AnInterfaceOnlyClosureReportsNoPartitionLeak", [] { auto c = interface_closure(library_graph(), "mathkit", "mathkit"); - ASSERT_TRUE(c.has_value()); - EXPECT_TRUE(c->publishedImplementationPartitions.empty()); - EXPECT_TRUE(c->publishedUndeterminedPartitions.empty()); -} + expect(fatal(c.has_value())); + expect(c->publishedImplementationPartitions.empty()); + expect(c->publishedUndeterminedPartitions.empty()); +}}; // ─── the state that used to be spelled "interface" ───────────────────────── -TEST(InterfaceClosure, APublishedPartitionOfUnknownKindIsReportedSeparately) { +UTest a_published_partition_of_unknown_kind_is_reported_separately{"InterfaceClosure/APublishedPartitionOfUnknownKindIsReportedSeparately", [] { // A `[scan_overrides.""]` entry says which module a file provides and // has nowhere to say whether the declaration is exported; a P1689 scanner // may omit `is-interface`. That used to arrive as `providesInterface = @@ -127,18 +127,17 @@ TEST(InterfaceClosure, APublishedPartitionOfUnknownKindIsReportedSeparately) { auto g = library_graph(/*interfaceReachesSecret=*/true); g.units[2].providesInterface.reset(); // src/secret.cppm, kind unknown auto c = interface_closure(g, "mathkit", "mathkit"); - ASSERT_TRUE(c.has_value()); + expect(fatal(c.has_value())); // Still published — the consumer cannot build the root's BMI without it. - EXPECT_EQ(names(c->published), - (std::vector{"api.cppm", "mathkit.cppm", "secret.cppm"})); + expect(eq(names(c->published), (std::vector{"api.cppm", "mathkit.cppm", "secret.cppm"}))); // But reported as undetermined, not as a known implementation partition: // the sentence to print is a different one. - EXPECT_TRUE(c->publishedImplementationPartitions.empty()); - ASSERT_EQ(c->publishedUndeterminedPartitions.size(), 1u); - EXPECT_EQ(c->publishedUndeterminedPartitions[0].filename().string(), "secret.cppm"); -} + expect(c->publishedImplementationPartitions.empty()); + expect(fatal(eq(c->publishedUndeterminedPartitions.size(), 1u))); + expect(eq(c->publishedUndeterminedPartitions[0].filename().string(), "secret.cppm")); +}}; -TEST(InterfaceClosure, AnUndeterminedPrimaryInterfaceIsNotReported) { +UTest an_undetermined_primary_interface_is_not_reported{"InterfaceClosure/AnUndeterminedPrimaryInterfaceIsNotReported", [] { // Only a PARTITION can be either kind. `module M;` provides nothing, so the // only declaration that provides a bare `M` is `export module M;` — asking // the question there would warn about every primary interface in every @@ -147,42 +146,42 @@ TEST(InterfaceClosure, AnUndeterminedPrimaryInterfaceIsNotReported) { auto g = library_graph(); g.units[0].providesInterface.reset(); // src/mathkit.cppm, the root auto c = interface_closure(g, "mathkit", "mathkit"); - ASSERT_TRUE(c.has_value()); - EXPECT_TRUE(c->publishedUndeterminedPartitions.empty()); - EXPECT_TRUE(c->publishedImplementationPartitions.empty()); -} + expect(fatal(c.has_value())); + expect(c->publishedUndeterminedPartitions.empty()); + expect(c->publishedImplementationPartitions.empty()); +}}; -TEST(InterfaceClosure, AWithheldPartitionOfUnknownKindIsNotReportedEither) { +UTest a_withheld_partition_of_unknown_kind_is_not_reported_either{"InterfaceClosure/AWithheldPartitionOfUnknownKindIsNotReportedEither", [] { // The warning is about what is PUBLISHED. `secret.cppm` is unreachable from // the interface here, so its kind never mattered. auto g = library_graph(/*interfaceReachesSecret=*/false); g.units[2].providesInterface.reset(); auto c = interface_closure(g, "mathkit", "mathkit"); - ASSERT_TRUE(c.has_value()); - EXPECT_TRUE(c->publishedUndeterminedPartitions.empty()); -} + expect(fatal(c.has_value())); + expect(c->publishedUndeterminedPartitions.empty()); +}}; -TEST(InterfaceClosure, AGenuinelyMissingPartitionIsStillAnError) { +UTest a_genuinely_missing_partition_is_still_an_error{"InterfaceClosure/AGenuinelyMissingPartitionIsStillAnError", [] { // The unresolved path is not dead: a partition nothing provides means the // published set is incomplete and the consumer's compile will fail on it. auto g = library_graph(/*interfaceReachesSecret=*/true); g.producerOf.erase("mathkit:secret"); auto c = interface_closure(g, "mathkit", "mathkit"); - ASSERT_TRUE(c.has_value()); - ASSERT_EQ(c->unresolvedImports.size(), 1u); - EXPECT_EQ(c->unresolvedImports[0], "mathkit:secret"); -} + expect(fatal(c.has_value())); + expect(fatal(eq(c->unresolvedImports.size(), 1u))); + expect(eq(c->unresolvedImports[0], "mathkit:secret")); +}}; -TEST(InterfaceClosure, ADependencysModuleIsNeitherPublishedNorUnresolved) { +UTest a_dependencys_module_is_neither_published_nor_unresolved{"InterfaceClosure/ADependencysModuleIsNeitherPublishedNorUnresolved", [] { auto g = library_graph(); g.units[0].requires_.push_back(ModuleId{"compat.zlib"}); // someone else's auto c = interface_closure(g, "mathkit", "mathkit"); - ASSERT_TRUE(c.has_value()); - EXPECT_TRUE(c->unresolvedImports.empty()); - EXPECT_EQ(c->published.size(), 2u); -} + expect(fatal(c.has_value())); + expect(c->unresolvedImports.empty()); + expect(eq(c->published.size(), 2u)); +}}; -TEST(InterfaceClosure, ForeignPackageUnitsAreNotFollowed) { +UTest foreign_package_units_are_not_followed{"InterfaceClosure/ForeignPackageUnitsAreNotFollowed", [] { auto g = library_graph(); SourceUnit other; other.path = "vendor/zlib.cppm"; @@ -193,23 +192,23 @@ TEST(InterfaceClosure, ForeignPackageUnitsAreNotFollowed) { g.units[0].requires_.push_back(ModuleId{"compat.zlib"}); auto c = interface_closure(g, "mathkit", "mathkit"); - ASSERT_TRUE(c.has_value()); - EXPECT_EQ(names(c->published), (std::vector{"api.cppm", "mathkit.cppm"})); + expect(fatal(c.has_value())); + expect(eq(names(c->published), (std::vector{"api.cppm", "mathkit.cppm"}))); // Nor does a foreign unit show up as something we withheld. auto withheld = names(c->withheld); - EXPECT_EQ(std::ranges::find(withheld, "zlib.cppm"), withheld.end()); -} + expect(eq(std::ranges::find(withheld, "zlib.cppm"), withheld.end())); +}}; -TEST(InterfaceClosure, RefusesAnUnknownRoot) { +UTest refuses_an_unknown_root{"InterfaceClosure/RefusesAnUnknownRoot", [] { auto c = interface_closure(library_graph(), "mathkit", "nosuch"); - ASSERT_FALSE(c.has_value()); - EXPECT_NE(c.error().find("nosuch"), std::string::npos); -} + expect(fatal(!c.has_value())); + expect(ne(c.error().find("nosuch"), std::string::npos)); +}}; -TEST(InterfaceClosure, RefusesARootOwnedByAnotherPackage) { +UTest refuses_a_root_owned_by_another_package{"InterfaceClosure/RefusesARootOwnedByAnotherPackage", [] { auto g = library_graph(); g.units[0].packageName = "somebody.else"; auto c = interface_closure(g, "mathkit", "mathkit"); - ASSERT_FALSE(c.has_value()); - EXPECT_NE(c.error().find("somebody.else"), std::string::npos); -} + expect(fatal(!c.has_value())); + expect(ne(c.error().find("somebody.else"), std::string::npos)); +}}; diff --git a/tests/unit/test_pack_members.cpp b/tests/unit/test_pack_members.cpp index 7e4c8e5ac..da4f02a63 100644 --- a/tests/unit/test_pack_members.cpp +++ b/tests/unit/test_pack_members.cpp @@ -4,12 +4,14 @@ // several (`with_member`), which `mcpp test` and `mcpp pack` read a member's // closure through. The e2e halves are tests/e2e/867_… to 869_…. -#include import std; +import mcpplibs.mtest; import mcpp.build.plan; import mcpp.build.prepare; +using namespace mcpplibs::mtest; + namespace { using Names = std::vector; @@ -48,81 +50,80 @@ mcpp::build::BuildContext two_members() { // A package acts for the member that is itself, the one member that reaches it, // and none when several do. -TEST(PackOwner, AMemberActsForItself) { - EXPECT_EQ(mcpp::build::pack_owner("a", Names{"a", "b"}), "a"); - EXPECT_EQ(mcpp::build::pack_owner("b", Names{"b"}), "b"); -} +UTest a_member_acts_for_itself{"PackOwner/AMemberActsForItself", [] { + expect(eq(mcpp::build::pack_owner("a", Names{"a", "b"}), "a")); + expect(eq(mcpp::build::pack_owner("b", Names{"b"}), "b")); +}}; -TEST(PackOwner, APackageOneMemberReachesActsForThatMember) { - EXPECT_EQ(mcpp::build::pack_owner("dist", Names{"a"}), "a"); -} +UTest a_package_one_member_reaches_acts_for_that_member{"PackOwner/APackageOneMemberReachesActsForThatMember", [] { + expect(eq(mcpp::build::pack_owner("dist", Names{"a"}), "a")); +}}; -TEST(PackOwner, APackageSeveralMembersReachActsForNone) { - EXPECT_EQ(mcpp::build::pack_owner("core", Names{"a", "b"}), ""); - EXPECT_EQ(mcpp::build::pack_owner("orphan", Names{}), ""); -} +UTest a_package_several_members_reach_acts_for_none{"PackOwner/APackageSeveralMembersReachActsForNone", [] { + expect(eq(mcpp::build::pack_owner("core", Names{"a", "b"}), "")); + expect(eq(mcpp::build::pack_owner("orphan", Names{}), "")); +}}; // The view of a member holds the member's closure, manifest and root, and is // undone when the call returns. -TEST(WithMember, ReadsTheMembersOwnFieldsAndRestoresTheGroups) { +UTest reads_the_members_own_fields_and_restores_the_groups{"WithMember/ReadsTheMembersOwnFieldsAndRestoresTheGroups", [] { auto ctx = two_members(); bool ran = false; mcpp::build::with_member(ctx, "b", [&] { ran = true; - EXPECT_EQ(ctx.manifest.package.name, "b"); - EXPECT_EQ(ctx.projectRoot, std::filesystem::path("/ws/b")); - EXPECT_EQ(ctx.plan.productDir, std::filesystem::path("bin/b")); - ASSERT_EQ(ctx.plan.runtimeLibraryDirs.size(), 1u); - EXPECT_EQ(ctx.plan.runtimeLibraryDirs.front(), std::filesystem::path("/closure/b")); + expect(eq(ctx.manifest.package.name, "b")); + expect(eq(ctx.projectRoot, std::filesystem::path("/ws/b"))); + expect(eq(ctx.plan.productDir, std::filesystem::path("bin/b"))); + expect(fatal(eq(ctx.plan.runtimeLibraryDirs.size(), 1u))); + expect(eq(ctx.plan.runtimeLibraryDirs.front(), std::filesystem::path("/closure/b"))); }); - EXPECT_TRUE(ran); - EXPECT_EQ(ctx.manifest.package.name, "virtual"); - EXPECT_EQ(ctx.projectRoot, std::filesystem::path("/ws")); - EXPECT_EQ(ctx.plan.productDir, std::filesystem::path("bin")); - ASSERT_EQ(ctx.plan.runtimeLibraryDirs.size(), 1u); - EXPECT_EQ(ctx.plan.runtimeLibraryDirs.front(), std::filesystem::path("/plan/lib")); + expect(bool(ran)); + expect(eq(ctx.manifest.package.name, "virtual")); + expect(eq(ctx.projectRoot, std::filesystem::path("/ws"))); + expect(eq(ctx.plan.productDir, std::filesystem::path("bin"))); + expect(fatal(eq(ctx.plan.runtimeLibraryDirs.size(), 1u))); + expect(eq(ctx.plan.runtimeLibraryDirs.front(), std::filesystem::path("/plan/lib"))); // The groups keep what they held, so the next member reads its own. - EXPECT_EQ(ctx.plan.linkGroups[0].runtimeLibraryDirs.front(), std::filesystem::path("/closure/a")); - EXPECT_EQ(ctx.plan.linkGroups[1].runtimeLibraryDirs.front(), std::filesystem::path("/closure/b")); - EXPECT_EQ(ctx.workspaceMembers[1].manifest.package.name, "b"); - EXPECT_EQ(ctx.workspaceMembers[1].root, std::filesystem::path("/ws/b")); -} + expect(eq(ctx.plan.linkGroups[0].runtimeLibraryDirs.front(), std::filesystem::path("/closure/a"))); + expect(eq(ctx.plan.linkGroups[1].runtimeLibraryDirs.front(), std::filesystem::path("/closure/b"))); + expect(eq(ctx.workspaceMembers[1].manifest.package.name, "b")); + expect(eq(ctx.workspaceMembers[1].root, std::filesystem::path("/ws/b"))); +}}; -TEST(WithMember, OneMemberAfterTheOtherEachReadsItsOwn) { +UTest one_member_after_the_other_each_reads_its_own{"WithMember/OneMemberAfterTheOtherEachReadsItsOwn", [] { auto ctx = two_members(); std::vector seen; for (std::string_view m : {"a", "b", "a"}) mcpp::build::with_member(ctx, m, [&] { seen.push_back(ctx.plan.runtimeLibraryDirs.front().generic_string()); }); - EXPECT_EQ(seen, (std::vector{"/closure/a", "/closure/b", "/closure/a"})); -} + expect(eq(seen, (std::vector{"/closure/a", "/closure/b", "/closure/a"}))); +}}; -TEST(WithMember, TheExchangeIsUndoneWhenTheCallThrows) { +UTest the_exchange_is_undone_when_the_call_throws{"WithMember/TheExchangeIsUndoneWhenTheCallThrows", [] { auto ctx = two_members(); - EXPECT_THROW(mcpp::build::with_member(ctx, "a", [&] { throw std::runtime_error("boom"); }), - std::runtime_error); - EXPECT_EQ(ctx.manifest.package.name, "virtual"); - EXPECT_EQ(ctx.projectRoot, std::filesystem::path("/ws")); - EXPECT_EQ(ctx.plan.runtimeLibraryDirs.front(), std::filesystem::path("/plan/lib")); - EXPECT_EQ(ctx.plan.linkGroups[0].runtimeLibraryDirs.front(), std::filesystem::path("/closure/a")); -} + expect(throws([&] { mcpp::build::with_member(ctx, "a", [&] { throw std::runtime_error("boom"); }); })); + expect(eq(ctx.manifest.package.name, "virtual")); + expect(eq(ctx.projectRoot, std::filesystem::path("/ws"))); + expect(eq(ctx.plan.runtimeLibraryDirs.front(), std::filesystem::path("/plan/lib"))); + expect(eq(ctx.plan.linkGroups[0].runtimeLibraryDirs.front(), std::filesystem::path("/closure/a"))); +}}; // No owner, a name the plan does not hold, and a group that places nothing all // read the plan as it is. -TEST(WithMember, APlanWithoutTheMemberIsReadAsItIs) { +UTest a_plan_without_the_member_is_read_as_it_is{"WithMember/APlanWithoutTheMemberIsReadAsItIs", [] { auto ctx = two_members(); for (std::string_view owner : {"", "nosuch"}) { bool ran = false; mcpp::build::with_member(ctx, owner, [&] { ran = true; - EXPECT_EQ(ctx.manifest.package.name, "virtual"); - EXPECT_EQ(ctx.plan.runtimeLibraryDirs.front(), std::filesystem::path("/plan/lib")); + expect(eq(ctx.manifest.package.name, "virtual")); + expect(eq(ctx.plan.runtimeLibraryDirs.front(), std::filesystem::path("/plan/lib"))); }); - EXPECT_TRUE(ran); + expect(bool(ran)); } mcpp::build::BuildContext plain; plain.manifest.package.name = "solo"; - mcpp::build::with_member(plain, "solo", [&] { EXPECT_EQ(plain.manifest.package.name, "solo"); }); - EXPECT_EQ(plain.manifest.package.name, "solo"); -} + mcpp::build::with_member(plain, "solo", [&] { expect(eq(plain.manifest.package.name, "solo")); }); + expect(eq(plain.manifest.package.name, "solo")); +}}; diff --git a/tests/unit/test_pack_modes.cpp b/tests/unit/test_pack_modes.cpp index 49b09aec7..dcc94bab2 100644 --- a/tests/unit/test_pack_modes.cpp +++ b/tests/unit/test_pack_modes.cpp @@ -1,8 +1,10 @@ -#include import std; +import mcpplibs.mtest; import mcpp.pack; +using namespace mcpplibs::mtest; + using mcpp::pack::Mode; using mcpp::pack::parse_mode; using mcpp::pack::mode_cli_name; @@ -10,28 +12,28 @@ using mcpp::pack::Format; using mcpp::pack::ClosureUnavailableOutcome; using mcpp::pack::closure_unavailable_outcome; -TEST(PackModes, CanonicalNamesParse) { - EXPECT_EQ(parse_mode("system"), Mode::None); - EXPECT_EQ(parse_mode("vendored"), Mode::BundleProject); - EXPECT_EQ(parse_mode("self-contained"), Mode::BundleAll); - EXPECT_EQ(parse_mode("static"), Mode::Static); -} +UTest canonical_names_parse{"PackModes/CanonicalNamesParse", [] { + expect(eq(parse_mode("system"), Mode::None)); + expect(eq(parse_mode("vendored"), Mode::BundleProject)); + expect(eq(parse_mode("self-contained"), Mode::BundleAll)); + expect(eq(parse_mode("static"), Mode::Static)); +}}; -TEST(PackModes, OldNamesStayAsAliases) { - EXPECT_EQ(parse_mode("bundle-project"), Mode::BundleProject); - EXPECT_EQ(parse_mode("bundle-all"), Mode::BundleAll); -} +UTest old_names_stay_as_aliases{"PackModes/OldNamesStayAsAliases", [] { + expect(eq(parse_mode("bundle-project"), Mode::BundleProject)); + expect(eq(parse_mode("bundle-all"), Mode::BundleAll)); +}}; -TEST(PackModes, UnknownIsNullopt) { - EXPECT_FALSE(parse_mode("nonsense").has_value()); -} +UTest unknown_is_nullopt{"PackModes/UnknownIsNullopt", [] { + expect(!(parse_mode("nonsense").has_value())); +}}; -TEST(PackModes, CliNamesAreCanonical) { - EXPECT_EQ(mode_cli_name(Mode::None), "system"); - EXPECT_EQ(mode_cli_name(Mode::BundleProject), "vendored"); - EXPECT_EQ(mode_cli_name(Mode::BundleAll), "self-contained"); - EXPECT_EQ(mode_cli_name(Mode::Static), "static"); -} +UTest cli_names_are_canonical{"PackModes/CliNamesAreCanonical", [] { + expect(eq(mode_cli_name(Mode::None), "system")); + expect(eq(mode_cli_name(Mode::BundleProject), "vendored")); + expect(eq(mode_cli_name(Mode::BundleAll), "self-contained")); + expect(eq(mode_cli_name(Mode::Static), "static")); +}}; // ── #630 §3: what happens when a format's dependency closure is unavailable, // as a function of the format alone ────────────────────────────────────── @@ -41,34 +43,31 @@ TEST(PackModes, CliNamesAreCanonical) { // ahead of the closure walk. A dispatched format receives the staged tree // regardless: a provider may not need a closure at all. -TEST(PackClosureUnavailableOutcome, ArchiveFormatsFailTheCommand) { - EXPECT_EQ(closure_unavailable_outcome(Format::Tar), - ClosureUnavailableOutcome::CommandFails); - EXPECT_EQ(closure_unavailable_outcome(Format::Dir), - ClosureUnavailableOutcome::CommandFails); -} +UTest archive_formats_fail_the_command{"PackClosureUnavailableOutcome/ArchiveFormatsFailTheCommand", [] { + expect(eq(closure_unavailable_outcome(Format::Tar), ClosureUnavailableOutcome::CommandFails)); + expect(eq(closure_unavailable_outcome(Format::Dir), ClosureUnavailableOutcome::CommandFails)); +}}; -TEST(PackClosureUnavailableOutcome, DispatchedFormatStagesWithoutIt) { - EXPECT_EQ(closure_unavailable_outcome(Format::Dispatched), - ClosureUnavailableOutcome::StageWithoutClosure); -} +UTest dispatched_format_stages_without_it{"PackClosureUnavailableOutcome/DispatchedFormatStagesWithoutIt", [] { + expect(eq(closure_unavailable_outcome(Format::Dispatched), ClosureUnavailableOutcome::StageWithoutClosure)); +}}; // `--systems` (design 2026-10-29 §4.1): `` for every architecture of the // legs, `-` for one, `all` for the three systems; unstated, this // machine's. -TEST(PackSystems, TheWordsNameSystemsForAnArchitecture) { +UTest the_words_name_systems_for_an_architecture{"PackSystems/TheWordsNameSystemsForAnArchitecture", [] { using V = std::vector; - EXPECT_EQ(mcpp::pack::systems_for({ "linux", "macos-aarch64" }, "x86_64"), V{ "linux" }); - EXPECT_EQ(mcpp::pack::systems_for({ "linux", "macos-aarch64" }, "aarch64"), (V{ "linux", "macos" })); - EXPECT_EQ(mcpp::pack::systems_for({ "all", "linux" }, "x86_64"), (V{ "linux", "macos", "windows" })); - EXPECT_EQ(mcpp::pack::systems_for({}, "x86_64").size(), 1u); -} + expect(eq(mcpp::pack::systems_for({ "linux", "macos-aarch64" }, "x86_64"), V{ "linux" })); + expect(eq(mcpp::pack::systems_for({ "linux", "macos-aarch64" }, "aarch64"), (V{ "linux", "macos" }))); + expect(eq(mcpp::pack::systems_for({ "all", "linux" }, "x86_64"), (V{ "linux", "macos", "windows" }))); + expect(eq(mcpp::pack::systems_for({}, "x86_64").size(), 1u)); +}}; // `-o` names a directory when it ends in a separator or is one (design // 2026-10-29 §4.2). -TEST(PackSystems, AnOutputEndingInASeparatorIsADirectory) { - EXPECT_TRUE(mcpp::pack::output_names_directory("dist/")); - EXPECT_FALSE(mcpp::pack::output_names_directory("dist/app.tar.gz")); - EXPECT_FALSE(mcpp::pack::output_names_directory("")); - EXPECT_TRUE(mcpp::pack::output_names_directory(std::filesystem::temp_directory_path())); -} +UTest an_output_ending_in_a_separator_is_a_directory{"PackSystems/AnOutputEndingInASeparatorIsADirectory", [] { + expect(bool(mcpp::pack::output_names_directory("dist/"))); + expect(!mcpp::pack::output_names_directory("dist/app.tar.gz")); + expect(!mcpp::pack::output_names_directory("")); + expect(bool(mcpp::pack::output_names_directory(std::filesystem::temp_directory_path()))); +}}; diff --git a/tests/unit/test_pack_polyglot.cpp b/tests/unit/test_pack_polyglot.cpp index 2c3fa71f3..88866317e 100644 --- a/tests/unit/test_pack_polyglot.cpp +++ b/tests/unit/test_pack_polyglot.cpp @@ -1,8 +1,10 @@ -#include import std; +import mcpplibs.mtest; import mcpp.pack.polyglot; +using namespace mcpplibs::mtest; + // A polyglot file's layout (SPEC-014 §5.3). The PE half is asserted end to // end by e2e 992, against the carriers a pack makes; what is asserted here is // the arithmetic a wrong answer to would still run on the machine that wrote @@ -28,52 +30,52 @@ std::uint64_t number_after(std::string_view text, std::string_view key) { } // namespace -TEST(PackPolyglot, WithoutWindowsTheFileIsAScriptAndEachProgramLiesWhereItsLineSays) { +UTest without_windows_the_file_is_a_script_and_each_program_lies_where_its_line_says{"PackPolyglot/WithoutWindowsTheFileIsAScriptAndEachProgramLiesWhereItsLineSays", [] { const std::string linux64(1000, 'L'), mac(777, 'M'); auto file = pg::assemble("app", { program("linux", "x86_64", linux64), program("macos", "aarch64", mac) }); - ASSERT_TRUE(file.has_value()) << file.error(); - EXPECT_TRUE(file->starts_with("#!/bin/sh\no=$0;")); - EXPECT_TRUE(pg::is_polyglot(*file)); + if (auto&& failed = expect(fatal(file.has_value())); !failed) failed << file.error(); + expect(file->starts_with("#!/bin/sh\no=$0;")); + expect(pg::is_polyglot(*file)); // The head names the script: `tail -c +` and `head -c `. const auto at = number_after(*file, "tail -c +") - 1; const auto length = number_after(*file, "|head -c "); - ASSERT_EQ(at + length, file->size()); + expect(fatal(eq(at + length, file->size()))); const std::string_view script = std::string_view(*file).substr(at); - EXPECT_TRUE(script.starts_with("# mcpp polyglot")); - EXPECT_NE(script.find("n='app'"), std::string_view::npos); - EXPECT_EQ(script.find("MINGW*"), std::string_view::npos) << "no Windows program, no MSYS line"; + expect(script.starts_with("# mcpp polyglot")); + expect(ne(script.find("n='app'"), std::string_view::npos)); + expect(eq(script.find("MINGW*"), std::string_view::npos)) << "no Windows program, no MSYS line"; for (auto [key, bytes] : { std::pair{std::string_view("k=linux-x86_64 a="), std::string_view(linux64)}, std::pair{std::string_view("k=macos-aarch64 a="), std::string_view(mac)} }) { const auto line = script.find(key); - ASSERT_NE(line, std::string_view::npos) << key; + expect(fatal(ne(line, std::string_view::npos))) << key; const auto off = number_after(script.substr(line), key); const auto len = number_after(script.substr(line), " b="); - EXPECT_EQ(len, bytes.size()) << key; - EXPECT_EQ(std::string_view(*file).substr(off, len), bytes) << key; + expect(eq(len, bytes.size())) << key; + expect(eq(std::string_view(*file).substr(off, len), bytes)) << key; } // `uname` spells an architecture two ways on some systems. - EXPECT_NE(script.find("Linux-x86_64|Linux-amd64)"), std::string_view::npos); - EXPECT_NE(script.find("Darwin-aarch64|Darwin-arm64)"), std::string_view::npos); -} + expect(ne(script.find("Linux-x86_64|Linux-amd64)"), std::string_view::npos)); + expect(ne(script.find("Darwin-aarch64|Darwin-arm64)"), std::string_view::npos)); +}}; -TEST(PackPolyglot, RefusesWhatItCannotLayOut) { - EXPECT_FALSE(pg::assemble("app", {}).has_value()); - EXPECT_FALSE(pg::assemble("a'b", { program("linux", "x86_64", "x") }).has_value()) +UTest refuses_what_it_cannot_lay_out{"PackPolyglot/RefusesWhatItCannotLayOut", [] { + expect(!(pg::assemble("app", {}).has_value())); + expect(!(pg::assemble("a'b", { program("linux", "x86_64", "x") }).has_value())) << "the name is quoted in the script"; - EXPECT_FALSE(pg::assemble("app", { program("freebsd", "x86_64", "x") }).has_value()); + expect(!(pg::assemble("app", { program("freebsd", "x86_64", "x") }).has_value())); auto notPe = pg::assemble("app", { program("windows", "x86_64", std::string(256, 'x')) }); - ASSERT_FALSE(notPe.has_value()); - EXPECT_NE(notPe.error().find("no PE"), std::string::npos) << notPe.error(); + expect(fatal(!notPe.has_value())); + if (auto&& failed = expect(ne(notPe.error().find("no PE"), std::string::npos)); !failed) failed << notPe.error(); auto twoWindows = pg::assemble("app", { program("windows", "x86_64", "MZ"), program("windows", "aarch64", "MZ") }); - ASSERT_FALSE(twoWindows.has_value()); - EXPECT_NE(twoWindows.error().find("one Windows program"), std::string::npos) << twoWindows.error(); -} + expect(fatal(!twoWindows.has_value())); + if (auto&& failed = expect(ne(twoWindows.error().find("one Windows program"), std::string::npos)); !failed) failed << twoWindows.error(); +}}; -TEST(PackPolyglot, RecognisesItsOwnHeadsOnly) { - EXPECT_TRUE(pg::is_polyglot("MZqFpD='\n ")); - EXPECT_FALSE(pg::is_polyglot("MZ\x90\x00")); - EXPECT_FALSE(pg::is_polyglot("#!/bin/sh\necho hi\n")); - EXPECT_FALSE(pg::is_polyglot("")); -} +UTest recognises_its_own_heads_only{"PackPolyglot/RecognisesItsOwnHeadsOnly", [] { + expect(pg::is_polyglot("MZqFpD='\n ")); + expect(!pg::is_polyglot("MZ\x90\x00")); + expect(!pg::is_polyglot("#!/bin/sh\necho hi\n")); + expect(!pg::is_polyglot("")); +}}; diff --git a/tests/unit/test_pack_relocate.cpp b/tests/unit/test_pack_relocate.cpp index c1146ea70..d8bb1d1cc 100644 --- a/tests/unit/test_pack_relocate.cpp +++ b/tests/unit/test_pack_relocate.cpp @@ -1,9 +1,11 @@ -#include import std; +import mcpplibs.mtest; import mcpp.pack.relocate; import mcpp.pack.strip; +using namespace mcpplibs::mtest; + // mcpp.pack.relocate / mcpp.pack.strip — the two halves of "the build machine // does not travel", tested where CI cannot otherwise reach them. // @@ -157,7 +159,7 @@ constexpr std::uint64_t kNull = 0, kNeeded = 1, kStrtab = 5, kSoname = 14, // ── the ELF editor ────────────────────────────────────────────────────── -TEST(PackRelocate, RemovesRunpathFromElf64LittleEndian) { +UTest removes_runpath_from_elf64_little_endian{"PackRelocate/RemovesRunpathFromElf64LittleEndian", [] { auto e = make_elf(/*wide=*/true, /*little=*/true, { {kNeeded, 0}, {kStrtab, 0}, {kRunpath, 0}, {kSoname, 0}, {kNull, 0} }, { "libc.so.6", "/home/builder/.mcpp/store/lib64", "libfoo.so.1" }, @@ -166,29 +168,29 @@ TEST(PackRelocate, RemovesRunpathFromElf64LittleEndian) { auto path = write_temp(e.bytes, "elf64le.so"); auto r = mcpp::pack::relocate::strip_search_paths(path); - ASSERT_TRUE(r.has_value()) << (r ? "" : r.error()); - EXPECT_EQ(r->outcome, mcpp::pack::relocate::Outcome::Removed); - ASSERT_EQ(r->paths.size(), 1u); + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << (r ? "" : r.error()); + expect(eq(r->outcome, mcpp::pack::relocate::Outcome::Removed)); + expect(fatal(eq(r->paths.size(), 1u))); // The report names what was dropped — a log line that says only "removed" // cannot tell a stale store path from `$ORIGIN`. - EXPECT_EQ(r->paths[0], "/home/builder/.mcpp/store/lib64"); + expect(eq(r->paths[0], "/home/builder/.mcpp/store/lib64")); std::ifstream in(path, std::ios::binary); std::string after{ std::istreambuf_iterator(in), {} }; // SAME LENGTH: the freed slot becomes DT_NULL padding, so nothing after // PT_DYNAMIC moves and no offset in the file needs fixing up. - EXPECT_EQ(after.size(), before); + expect(eq(after.size(), before)); auto tags = read_tags(after, e.dynOff, true, true); - EXPECT_EQ(std::ranges::count(tags, kRunpath), 0); + expect(eq(std::ranges::count(tags, kRunpath), 0)); // The other entries survive, in order. Losing DT_SONAME here would make the // library unfindable by the name its consumers link against. - EXPECT_EQ(std::ranges::count(tags, kNeeded), 1); - EXPECT_EQ(std::ranges::count(tags, kSoname), 1); - EXPECT_EQ(std::ranges::count(tags, kStrtab), 1); -} + expect(eq(std::ranges::count(tags, kNeeded), 1)); + expect(eq(std::ranges::count(tags, kSoname), 1)); + expect(eq(std::ranges::count(tags, kStrtab), 1)); +}}; -TEST(PackRelocate, RemovesRpathFromElf32BigEndian) { +UTest removes_rpath_from_elf32_big_endian{"PackRelocate/RemovesRpathFromElf32BigEndian", [] { // No CI job produces this shape, and `--target` can. A width or byte-order // mistake would corrupt the image rather than fail. auto e = make_elf(/*wide=*/false, /*little=*/false, @@ -199,20 +201,20 @@ TEST(PackRelocate, RemovesRpathFromElf32BigEndian) { auto path = write_temp(e.bytes, "elf32be.so"); auto r = mcpp::pack::relocate::strip_search_paths(path); - ASSERT_TRUE(r.has_value()) << (r ? "" : r.error()); - EXPECT_EQ(r->outcome, mcpp::pack::relocate::Outcome::Removed); - ASSERT_EQ(r->paths.size(), 1u); - EXPECT_EQ(r->paths[0], "/home/builder/store/lib"); + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << (r ? "" : r.error()); + expect(eq(r->outcome, mcpp::pack::relocate::Outcome::Removed)); + expect(fatal(eq(r->paths.size(), 1u))); + expect(eq(r->paths[0], "/home/builder/store/lib")); std::ifstream in(path, std::ios::binary); std::string after{ std::istreambuf_iterator(in), {} }; - EXPECT_EQ(after.size(), before); + expect(eq(after.size(), before)); auto tags = read_tags(after, e.dynOff, false, false); - EXPECT_EQ(std::ranges::count(tags, kRpath), 0); - EXPECT_EQ(std::ranges::count(tags, kNeeded), 1); -} + expect(eq(std::ranges::count(tags, kRpath), 0)); + expect(eq(std::ranges::count(tags, kNeeded), 1)); +}}; -TEST(PackRelocate, RemovesBothTagsWhenAnImageCarriesBoth) { +UTest removes_both_tags_when_an_image_carries_both{"PackRelocate/RemovesBothTagsWhenAnImageCarriesBoth", [] { // `Both` is what a linker driven with `--disable-new-dtags` after an // explicit -rpath can produce, and the loader honours DT_RUNPATH there. // Leaving either one behind leaves the defect behind. @@ -223,78 +225,78 @@ TEST(PackRelocate, RemovesBothTagsWhenAnImageCarriesBoth) { auto path = write_temp(e.bytes, "elfboth.so"); auto r = mcpp::pack::relocate::strip_search_paths(path); - ASSERT_TRUE(r.has_value()); - EXPECT_EQ(r->outcome, mcpp::pack::relocate::Outcome::Removed); - EXPECT_EQ(r->paths.size(), 2u); + expect(fatal(r.has_value())); + expect(eq(r->outcome, mcpp::pack::relocate::Outcome::Removed)); + expect(eq(r->paths.size(), 2u)); std::ifstream in(path, std::ios::binary); std::string after{ std::istreambuf_iterator(in), {} }; auto tags = read_tags(after, e.dynOff, true, true); - EXPECT_EQ(std::ranges::count(tags, kRpath), 0); - EXPECT_EQ(std::ranges::count(tags, kRunpath), 0); -} + expect(eq(std::ranges::count(tags, kRpath), 0)); + expect(eq(std::ranges::count(tags, kRunpath), 0)); +}}; -TEST(PackRelocate, AlreadyCleanIsNotTheSameAsNotChecked) { +UTest already_clean_is_not_the_same_as_not_checked{"PackRelocate/AlreadyCleanIsNotTheSameAsNotChecked", [] { auto e = make_elf(true, true, { {kNeeded, 0}, {kStrtab, 0}, {kNull, 0} }, { "libc.so.6" }, { 0 }); auto path = write_temp(e.bytes, "elfclean.so"); auto r = mcpp::pack::relocate::strip_search_paths(path); - ASSERT_TRUE(r.has_value()); - EXPECT_EQ(r->outcome, mcpp::pack::relocate::Outcome::NothingToDo); - EXPECT_TRUE(r->paths.empty()); -} + expect(fatal(r.has_value())); + expect(eq(r->outcome, mcpp::pack::relocate::Outcome::NothingToDo)); + expect(r->paths.empty()); +}}; -TEST(PackRelocate, AStaticArchiveIsNotApplicableRatherThanAnError) { +UTest a_static_archive_is_not_applicable_rather_than_an_error{"PackRelocate/AStaticArchiveIsNotApplicableRatherThanAnError", [] { // Every leg goes through this step, and a `.a` has no dynamic section to // begin with. Reporting that as a failure would make the caller branch on // the format before calling — which is the coupling this module removes. auto path = write_temp("!\n/ 0 0 0 0 4 `\n", "libfoo.a"); auto r = mcpp::pack::relocate::strip_search_paths(path); - ASSERT_TRUE(r.has_value()); - EXPECT_EQ(r->outcome, mcpp::pack::relocate::Outcome::NotApplicable); -} + expect(fatal(r.has_value())); + expect(eq(r->outcome, mcpp::pack::relocate::Outcome::NotApplicable)); +}}; -TEST(PackRelocate, AMissingFileIsAnError) { +UTest a_missing_file_is_an_error{"PackRelocate/AMissingFileIsAnError", [] { auto r = mcpp::pack::relocate::strip_search_paths( std::filesystem::temp_directory_path() / "mcpp-relocate-test" / "nope.so"); - EXPECT_FALSE(r.has_value()); -} + expect(!r.has_value()); +}}; // ── the strip table ───────────────────────────────────────────────────── -TEST(PackStrip, StaticArchiveKeepsItsSymbolIndex) { +UTest static_archive_keeps_its_symbol_index{"PackStrip/StaticArchiveKeepsItsSymbolIndex", [] { // THE MEASUREMENT BEHIND THIS TEST. `strip --strip-all` on a `.a` // removes the archive symbol index, and the consumer's link then fails // with `archive has no index; run ranlib to add one` — a message that // names neither strip nor the publisher. `--strip-debug` keeps it // (measured: 2988 -> 1244 bytes, and the consumer links and runs). auto args = mcpp::pack::strip_args(mcpp::pack::ArtifactShape::StaticArchive); - EXPECT_NE(std::ranges::find(args, "--strip-debug"), args.end()); - EXPECT_EQ(std::ranges::find(args, "--strip-all"), args.end()); - EXPECT_EQ(std::ranges::find(args, "--strip-unneeded"), args.end()); + expect(ne(std::ranges::find(args, "--strip-debug"), args.end())); + expect(eq(std::ranges::find(args, "--strip-all"), args.end())); + expect(eq(std::ranges::find(args, "--strip-unneeded"), args.end())); // Two identical packs should produce identical bytes; this is the one // place the packer rewrites an archive. - EXPECT_NE(std::ranges::find(args, "--enable-deterministic-archives"), args.end()); -} + expect(ne(std::ranges::find(args, "--enable-deterministic-archives"), args.end())); +}}; -TEST(PackStrip, SharedLibraryKeepsItsDynamicSymbols) { +UTest shared_library_keeps_its_dynamic_symbols{"PackStrip/SharedLibraryKeepsItsDynamicSymbols", [] { // `--strip-unneeded` removes `.symtab` and keeps `.dynsym`, which IS the // export list. `--strip-all` would also keep `.dynsym`, but dh_strip uses // the narrower flag and there is no reason to be broader than the // reference implementation of this exact decision. auto args = mcpp::pack::strip_args(mcpp::pack::ArtifactShape::SharedLibrary); - EXPECT_NE(std::ranges::find(args, "--strip-unneeded"), args.end()); - EXPECT_EQ(std::ranges::find(args, "--strip-all"), args.end()); - EXPECT_EQ(std::ranges::find(args, "--strip-debug"), args.end()); -} + expect(ne(std::ranges::find(args, "--strip-unneeded"), args.end())); + expect(eq(std::ranges::find(args, "--strip-all"), args.end())); + expect(eq(std::ranges::find(args, "--strip-debug"), args.end())); +}}; -TEST(PackStrip, ExecutableIsStrippedWhole) { +UTest executable_is_stripped_whole{"PackStrip/ExecutableIsStrippedWhole", [] { auto args = mcpp::pack::strip_args(mcpp::pack::ArtifactShape::Executable); - EXPECT_NE(std::ranges::find(args, "--strip-all"), args.end()); -} + expect(ne(std::ranges::find(args, "--strip-all"), args.end())); +}}; -TEST(PackStrip, EveryShapeDropsCommentAndNote) { +UTest every_shape_drops_comment_and_note{"PackStrip/EveryShapeDropsCommentAndNote", [] { // dh_strip drops both from all three. `.note` is matched by exact name, so // `.note.gnu.build-id` survives — which is what `--add-gnu-debuglink` and // debuginfod pair with. @@ -302,12 +304,12 @@ TEST(PackStrip, EveryShapeDropsCommentAndNote) { mcpp::pack::ArtifactShape::SharedLibrary, mcpp::pack::ArtifactShape::StaticArchive }) { auto args = mcpp::pack::strip_args(shape); - EXPECT_NE(std::ranges::find(args, "--remove-section=.comment"), args.end()); - EXPECT_NE(std::ranges::find(args, "--remove-section=.note"), args.end()); + expect(ne(std::ranges::find(args, "--remove-section=.comment"), args.end())); + expect(ne(std::ranges::find(args, "--remove-section=.note"), args.end())); } -} +}}; -TEST(PackStrip, WhetherStrippingAppliesIsAskedOfTheTargetNotTheCompiler) { +UTest whether_stripping_applies_is_asked_of_the_target_not_the_compiler{"PackStrip/WhetherStrippingAppliesIsAskedOfTheTargetNotTheCompiler", [] { // Keying this on the compiler is wrong in BOTH directions, and each // direction is a real configuration mcpp ships: // @@ -319,20 +321,20 @@ TEST(PackStrip, WhetherStrippingAppliesIsAskedOfTheTargetNotTheCompiler) { // `mcpp pack` on a Mac — for a format // whose image carries a debug MAP and // leaves the DWARF in the .o files. - EXPECT_TRUE (mcpp::pack::debug_info_is_in_band("x86_64-linux-gnu")); - EXPECT_TRUE (mcpp::pack::debug_info_is_in_band("aarch64-linux-musl")); - EXPECT_TRUE (mcpp::pack::debug_info_is_in_band("x86_64-windows-gnu")); - EXPECT_FALSE(mcpp::pack::debug_info_is_in_band("x86_64-windows-msvc")); - EXPECT_FALSE(mcpp::pack::debug_info_is_in_band("aarch64-macos")); - EXPECT_FALSE(mcpp::pack::debug_info_is_in_band("x86_64-macos")); + expect(mcpp::pack::debug_info_is_in_band("x86_64-linux-gnu")); + expect(mcpp::pack::debug_info_is_in_band("aarch64-linux-musl")); + expect(mcpp::pack::debug_info_is_in_band("x86_64-windows-gnu")); + expect(!mcpp::pack::debug_info_is_in_band("x86_64-windows-msvc")); + expect(!mcpp::pack::debug_info_is_in_band("aarch64-macos")); + expect(!mcpp::pack::debug_info_is_in_band("x86_64-macos")); // iOS carries the same Mach-O debug map + out-of-band .dSYM as macOS. - EXPECT_FALSE(mcpp::pack::debug_info_is_in_band("aarch64-ios")); + expect(!mcpp::pack::debug_info_is_in_band("aarch64-ios")); // Segment-wise, not substring: mcpp has been bitten by a triple predicate // that answered on a substring before. - EXPECT_TRUE(mcpp::pack::debug_info_is_in_band("macos64-linux-gnu")); -} + expect(mcpp::pack::debug_info_is_in_band("macos64-linux-gnu")); +}}; -TEST(PackStrip, MsvcHasNothingInBandToRemove) { +UTest msvc_has_nothing_in_band_to_remove{"PackStrip/MsvcHasNothingInBandToRemove", [] { // PE/MSVC keeps debug information in a separate `.pdb`. An empty `strip` // tool there is the right answer, not a missing dependency — and the two // must not be conflated, or every other platform gets a silent no-op. @@ -340,15 +342,15 @@ TEST(PackStrip, MsvcHasNothingInBandToRemove) { mcpp::pack::StripTools tools{ {}, {}, /*inBandDebugInfo=*/false }; auto r = mcpp::pack::strip_artifact(path, mcpp::pack::ArtifactShape::SharedLibrary, tools, {}); - ASSERT_TRUE(r.has_value()); - EXPECT_EQ(r->outcome, mcpp::pack::StripOutcome::NotApplicable); -} + expect(fatal(r.has_value())); + expect(eq(r->outcome, mcpp::pack::StripOutcome::NotApplicable)); +}}; -TEST(PackStrip, AMissingStripToolIsRefusedWhereDebugInfoIsInBand) { +UTest a_missing_strip_tool_is_refused_where_debug_info_is_in_band{"PackStrip/AMissingStripToolIsRefusedWhereDebugInfoIsInBand", [] { auto path = write_temp("not an image", "notanimage2.bin"); mcpp::pack::StripTools tools{ {}, {}, /*inBandDebugInfo=*/true }; auto r = mcpp::pack::strip_artifact(path, mcpp::pack::ArtifactShape::SharedLibrary, tools, {}); - ASSERT_FALSE(r.has_value()); - EXPECT_NE(r.error().find("no `strip`"), std::string::npos); -} + expect(fatal(!r.has_value())); + expect(ne(r.error().find("no `strip`"), std::string::npos)); +}}; diff --git a/tests/unit/test_pack_route.cpp b/tests/unit/test_pack_route.cpp index 92dcf8485..8d57b5a2f 100644 --- a/tests/unit/test_pack_route.cpp +++ b/tests/unit/test_pack_route.cpp @@ -1,8 +1,10 @@ -#include import std; +import mcpplibs.mtest; import mcpp.pack.route; +using namespace mcpplibs::mtest; + using namespace mcpp::pack; // #630 A9: the route is chosen by the artifact's FORM, not by the target's @@ -20,45 +22,45 @@ PackRoute library_route() { return PackRoute{ "mylib", /*library=*/true, /*isAp } // namespace -TEST(AcceptsSeveralTargets, AnAppOnSeveralAndroidRowsIsAccepted) { +UTest an_app_on_several_android_rows_is_accepted{"AcceptsSeveralTargets/AnAppOnSeveralAndroidRowsIsAccepted", [] { std::vector triples{"aarch64-linux-android", "x86_64-linux-android"}; - EXPECT_TRUE(accepts_several_targets(app_route(), triples)); -} + expect(bool(accepts_several_targets(app_route(), triples))); +}}; -TEST(AcceptsSeveralTargets, AnAppMixedWithAnExecutableRowIsRefused) { +UTest an_app_mixed_with_an_executable_row_is_refused{"AcceptsSeveralTargets/AnAppMixedWithAnExecutableRowIsRefused", [] { // The negative direction inside "an app": one Android row and one row // whose form is an executable must not slip through as "every row is a // shared object" — a mixed request is not a request this route can // satisfy either. std::vector triples{"aarch64-linux-android", "x86_64-linux-gnu"}; - EXPECT_FALSE(accepts_several_targets(app_route(), triples)); -} + expect(!(accepts_several_targets(app_route(), triples))); +}}; -TEST(AcceptsSeveralTargets, AnAppOnExecutableRowsOnlyIsRefused) { +UTest an_app_on_executable_rows_only_is_refused{"AcceptsSeveralTargets/AnAppOnExecutableRowsOnlyIsRefused", [] { std::vector triples{"x86_64-linux-gnu", "aarch64-macos"}; - EXPECT_FALSE(accepts_several_targets(app_route(), triples)); -} + expect(!(accepts_several_targets(app_route(), triples))); +}}; -TEST(AcceptsSeveralTargets, ABinTargetIsAlwaysRefusedEvenOnAndroidRows) { +UTest a_bin_target_is_always_refused_even_on_android_rows{"AcceptsSeveralTargets/ABinTargetIsAlwaysRefusedEvenOnAndroidRows", [] { // `kind = "bin"` never resolves to a shared-object form on any row // (`toolchain::triple::application_form` only answers `SharedObject` for // `kind = "app"`), so the several-triple route is not this target's, // whatever triples are named. std::vector triples{"aarch64-linux-android", "x86_64-linux-android"}; - EXPECT_FALSE(accepts_several_targets(bin_route(), triples)); -} + expect(!(accepts_several_targets(bin_route(), triples))); +}}; -TEST(AcceptsSeveralTargets, AnUnparseableTripleIsRefusedNotIgnored) { +UTest an_unparseable_triple_is_refused_not_ignored{"AcceptsSeveralTargets/AnUnparseableTripleIsRefusedNotIgnored", [] { std::vector triples{"aarch64-linux-android", "not a triple"}; - EXPECT_FALSE(accepts_several_targets(app_route(), triples)); -} + expect(!(accepts_several_targets(app_route(), triples))); +}}; -TEST(AcceptsSeveralTargets, ALibraryRouteIsUnaffected) { +UTest a_library_route_is_unaffected{"AcceptsSeveralTargets/ALibraryRouteIsUnaffected", [] { // `accepts_several_targets` is never consulted for a library route in // `cmd_pack` (libraries already take several triples unconditionally); // asked anyway, it answers false because `isApplication` is false for a // library `PackRoute`, which keeps the predicate honest about what it // actually decides. std::vector triples{"aarch64-linux-android", "x86_64-linux-android"}; - EXPECT_FALSE(accepts_several_targets(library_route(), triples)); -} + expect(!(accepts_several_targets(library_route(), triples))); +}}; diff --git a/tests/unit/test_pack_stage_tree.cpp b/tests/unit/test_pack_stage_tree.cpp index 07cf81124..4d8cb6140 100644 --- a/tests/unit/test_pack_stage_tree.cpp +++ b/tests/unit/test_pack_stage_tree.cpp @@ -1,8 +1,10 @@ -#include import std; +import mcpplibs.mtest; import mcpp.pack.stage_tree; +using namespace mcpplibs::mtest; + // The staged tree is what `mcpp pack` computes and, until `${mcpp.stage_dir}`, // then threw away. Two things about it are the engine's contract with a // distribution member, and both are asserted here rather than end to end, @@ -37,57 +39,57 @@ std::string read_file(const std::filesystem::path& p) { } // namespace -TEST(PackStageTree, TheManifestIsASiblingAndNeverAMember) { +UTest the_manifest_is_a_sibling_and_never_a_member{"PackStageTree/TheManifestIsASiblingAndNeverAMember", [] { Tmp t; auto stage = t.path / "app-1.0.0-x86_64-linux-gnu"; std::filesystem::create_directories(stage); auto manifest = mcpp::pack::stage_manifest_path(stage); - EXPECT_EQ(manifest.parent_path(), stage.parent_path()); + expect(eq(manifest.parent_path(), stage.parent_path())); // A file INSIDE the tree would be collected by every format that packages // the directory wholesale, and would then ship inside the user's // installer. Asserted as a path relationship because that is the property, // and a spelling change that broke it would otherwise only show up as an // extra file in a released package. - EXPECT_FALSE(manifest.string().starts_with(stage.string() + "/")); - EXPECT_FALSE(manifest.string().starts_with(stage.string() + "\\")); + expect(!(manifest.string().starts_with(stage.string() + "/"))); + expect(!(manifest.string().starts_with(stage.string() + "\\"))); - ASSERT_TRUE(mcpp::pack::write_stage_manifest(stage)); + expect(fatal(bool(mcpp::pack::write_stage_manifest(stage)))); std::error_code ec; for (auto const& e : std::filesystem::recursive_directory_iterator(stage, ec)) - FAIL() << "the manifest landed inside the tree: " << e.path().string(); -} + fail([&] { std::ostringstream text; text << "the manifest landed inside the tree: " << e.path().string(); return text.str(); }()); +}}; -TEST(PackStageTree, TheManifestChangesWhenTheStagedSetDoes) { +UTest the_manifest_changes_when_the_staged_set_does{"PackStageTree/TheManifestChangesWhenTheStagedSetDoes", [] { Tmp t; auto stage = t.path / "app"; write_file(stage / "bin" / "app", "0123456789"); - ASSERT_TRUE(mcpp::pack::write_stage_manifest(stage)); + expect(fatal(bool(mcpp::pack::write_stage_manifest(stage)))); const auto first = read_file(mcpp::pack::stage_manifest_path(stage)); - EXPECT_NE(first.find("bin/app"), std::string::npos); - EXPECT_NE(first.find("10 "), std::string::npos); + expect(ne(first.find("bin/app"), std::string::npos)); + expect(ne(first.find("10 "), std::string::npos)); // A DEPENDENCY'S SHARED LIBRARY JOINING THE CLOSURE. This is the case the // manifest exists for: the program's own bytes need not have changed, so an // edge that depended only on the link output would report the previous // distributable as up to date. write_file(stage / "lib" / "libdep.so.1", "xx"); - ASSERT_TRUE(mcpp::pack::write_stage_manifest(stage)); + expect(fatal(bool(mcpp::pack::write_stage_manifest(stage)))); const auto second = read_file(mcpp::pack::stage_manifest_path(stage)); - EXPECT_NE(first, second); - EXPECT_NE(second.find("lib/libdep.so.1"), std::string::npos); + expect(ne(first, second)); + expect(ne(second.find("lib/libdep.so.1"), std::string::npos)); // A staged file whose LENGTH changed, with no entry added or removed. write_file(stage / "bin" / "app", "0123456789abcdef"); - ASSERT_TRUE(mcpp::pack::write_stage_manifest(stage)); - EXPECT_NE(read_file(mcpp::pack::stage_manifest_path(stage)), second); -} + expect(fatal(bool(mcpp::pack::write_stage_manifest(stage)))); + expect(ne(read_file(mcpp::pack::stage_manifest_path(stage)), second)); +}}; -TEST(PackStageTree, StagingTheSameTreeTwiceLeavesTheManifestUntouched) { +UTest staging_the_same_tree_twice_leaves_the_manifest_untouched{"PackStageTree/StagingTheSameTreeTwiceLeavesTheManifestUntouched", [] { Tmp t; auto stage = t.path / "app"; write_file(stage / "bin" / "app", "same"); - ASSERT_TRUE(mcpp::pack::write_stage_manifest(stage)); + expect(fatal(bool(mcpp::pack::write_stage_manifest(stage)))); auto manifest = mcpp::pack::stage_manifest_path(stage); const auto before = std::filesystem::last_write_time(manifest); @@ -95,11 +97,11 @@ TEST(PackStageTree, StagingTheSameTreeTwiceLeavesTheManifestUntouched) { // input is indistinguishable from a changed input: a pack that staged the // same tree twice would rebuild the distributable both times. Same rule // `mcpp.build.stage` states at length for a staged BMI. - ASSERT_TRUE(mcpp::pack::write_stage_manifest(stage)); - EXPECT_EQ(std::filesystem::last_write_time(manifest), before); -} + expect(fatal(bool(mcpp::pack::write_stage_manifest(stage)))); + expect(eq(std::filesystem::last_write_time(manifest), before)); +}}; -TEST(PackStageTree, TheOrderOfADirectoryWalkIsNotAPromise) { +UTest the_order_of_a_directory_walk_is_not_a_promise{"PackStageTree/TheOrderOfADirectoryWalkIsNotAPromise", [] { // Two trees with the same contents produce the same bytes. Without the // sort, an iteration order that differed between runs would make the dist // edge dirty on every pack for no reason -- which reads as "packaging is @@ -109,13 +111,12 @@ TEST(PackStageTree, TheOrderOfADirectoryWalkIsNotAPromise) { write_file(root / "s" / "bin" / "app", "aa"); write_file(root / "s" / "lib" / "z.so", "bbb"); write_file(root / "s" / "share" / "doc" / "readme", "c"); - ASSERT_TRUE(mcpp::pack::write_stage_manifest(root / "s")); + expect(fatal(bool(mcpp::pack::write_stage_manifest(root / "s")))); } - EXPECT_EQ(read_file(mcpp::pack::stage_manifest_path(a.path / "s")), - read_file(mcpp::pack::stage_manifest_path(b.path / "s"))); -} + expect(eq(read_file(mcpp::pack::stage_manifest_path(a.path / "s")), read_file(mcpp::pack::stage_manifest_path(b.path / "s")))); +}}; -TEST(PackStageTree, TheEngineOwnsFiveFormatNames) { +UTest the_engine_owns_five_format_names{"PackStageTree/TheEngineOwnsFiveFormatNames", [] { // `tar`, `zip`, `dir`, `single-file` and `polyglot` are the shapes `mcpp pack` owns // (SPEC-014 §1.3); every other value of `--pkg-format` is a name a // package provides. The list lives beside the staged tree because two @@ -123,13 +124,13 @@ TEST(PackStageTree, TheEngineOwnsFiveFormatNames) { // unknown name, and the declaration check, which warns of a package that // claims one of them and would therefore be silently unreachable. for (auto name : { "tar", "zip", "dir", "single-file", "polyglot" }) - EXPECT_TRUE(mcpp::pack::is_builtin_pack_format(name)) << name; - EXPECT_FALSE(mcpp::pack::is_builtin_pack_format("appimage")); - EXPECT_FALSE(mcpp::pack::is_builtin_pack_format("okb")); - EXPECT_FALSE(mcpp::pack::is_builtin_pack_format("msi")); - EXPECT_FALSE(mcpp::pack::is_builtin_pack_format("")); - EXPECT_EQ(mcpp::pack::kBuiltinPackFormats.size(), 5u); -} + expect(mcpp::pack::is_builtin_pack_format(name)) << name; + expect(!mcpp::pack::is_builtin_pack_format("appimage")); + expect(!mcpp::pack::is_builtin_pack_format("okb")); + expect(!mcpp::pack::is_builtin_pack_format("msi")); + expect(!mcpp::pack::is_builtin_pack_format("")); + expect(eq(mcpp::pack::kBuiltinPackFormats.size(), 5u)); +}}; // ── #630 §3: the `closure` field ───────────────────────────────────────── // @@ -139,17 +140,17 @@ TEST(PackStageTree, TheEngineOwnsFiveFormatNames) { // comment). What CAN be tested here, on every host, is the writer/reader // contract itself: both values round-trip through the manifest file. -TEST(PackStageTree, DefaultClosureIsWalkedAndSaysSo) { +UTest default_closure_is_walked_and_says_so{"PackStageTree/DefaultClosureIsWalkedAndSaysSo", [] { Tmp t; auto stage = t.path / "app"; write_file(stage / "bin" / "app", "x"); - ASSERT_TRUE(mcpp::pack::write_stage_manifest(stage)); + expect(fatal(bool(mcpp::pack::write_stage_manifest(stage)))); auto text = read_file(mcpp::pack::stage_manifest_path(stage)); - EXPECT_EQ(text.substr(0, text.find('\n')), "closure = walked"); - EXPECT_EQ(text.find("reason ="), std::string::npos); -} + expect(eq(text.substr(0, text.find('\n')), "closure = walked")); + expect(eq(text.find("reason ="), std::string::npos)); +}}; -TEST(PackStageTree, NotWalkedRecordsTheReasonOnOneLine) { +UTest not_walked_records_the_reason_on_one_line{"PackStageTree/NotWalkedRecordsTheReasonOnOneLine", [] { Tmp t; auto stage = t.path / "app"; write_file(stage / "bin" / "app", "x"); @@ -160,36 +161,36 @@ TEST(PackStageTree, NotWalkedRecordsTheReasonOnOneLine) { "by running the artifact under\n" " the target's own dynamic linker.", }; - ASSERT_TRUE(mcpp::pack::write_stage_manifest(stage, closure)); + expect(fatal(bool(mcpp::pack::write_stage_manifest(stage, closure)))); auto text = read_file(mcpp::pack::stage_manifest_path(stage)); auto firstLine = text.substr(0, text.find('\n')); - EXPECT_EQ(firstLine, "closure = not-walked"); + expect(eq(firstLine, "closure = not-walked")); // Folded to ONE line -- the manifest is one entry per line, and the // reason text carries its own embedded newlines because it doubles as a // CLI diagnostic. auto secondLineEnd = text.find('\n', text.find('\n') + 1); auto secondLine = text.substr(text.find('\n') + 1, secondLineEnd - text.find('\n') - 1); - EXPECT_TRUE(secondLine.starts_with("reason = cannot package the Mach-O program")); - EXPECT_EQ(secondLine.find('\n'), std::string::npos); + expect(secondLine.starts_with("reason = cannot package the Mach-O program")); + expect(eq(secondLine.find('\n'), std::string::npos)); // The rest of the manifest -- the staged file listing -- is unaffected. - EXPECT_NE(text.find("bin/app"), std::string::npos); -} + expect(ne(text.find("bin/app"), std::string::npos)); +}}; -TEST(PackStageTree, WalkedAndNotWalkedProduceDifferentManifests) { +UTest walked_and_not_walked_produce_different_manifests{"PackStageTree/WalkedAndNotWalkedProduceDifferentManifests", [] { Tmp t; auto stage = t.path / "app"; write_file(stage / "bin" / "app", "x"); - ASSERT_TRUE(mcpp::pack::write_stage_manifest(stage, mcpp::pack::ClosureStatus{true, ""})); + expect(fatal(bool(mcpp::pack::write_stage_manifest(stage, mcpp::pack::ClosureStatus{true, ""})))); auto walked = read_file(mcpp::pack::stage_manifest_path(stage)); - ASSERT_TRUE(mcpp::pack::write_stage_manifest( - stage, mcpp::pack::ClosureStatus{false, "no such linker to ask"})); + expect(fatal(bool(mcpp::pack::write_stage_manifest( + stage, mcpp::pack::ClosureStatus{false, "no such linker to ask"})))); auto notWalked = read_file(mcpp::pack::stage_manifest_path(stage)); - EXPECT_NE(walked, notWalked); -} + expect(ne(walked, notWalked)); +}}; // ── #634 A3: the `needs` lines ────────────────────────────────────────── -TEST(PackStageTree, NeedsLinesFollowTheHeaderSortedAndOnceEach) { +UTest needs_lines_follow_the_header_sorted_and_once_each{"PackStageTree/NeedsLinesFollowTheHeaderSortedAndOnceEach", [] { Tmp t; auto stage = t.path / "app"; write_file(stage / "lib" / "libfw.so", "fw"); @@ -202,16 +203,15 @@ TEST(PackStageTree, NeedsLinesFollowTheHeaderSortedAndOnceEach) { {"libc.so", K::Platform, {}}, {"libc.so", K::Platform, {}}, }; - ASSERT_TRUE(mcpp::pack::write_stage_manifest(stage, closure)); + expect(fatal(bool(mcpp::pack::write_stage_manifest(stage, closure)))); auto text = read_file(mcpp::pack::stage_manifest_path(stage)); - EXPECT_EQ(text, - "closure = walked\n" + expect(eq(text, "closure = walked\n" "needs\tlibc.so\tplatform\n" "needs\tlibfw.so\tlib/libfw.so\n" - "2 lib/libfw.so\n"); -} + "2 lib/libfw.so\n")); +}}; -TEST(PackStageTree, AnIncompleteClosureListsItsUnresolvedNamesAfterTheReason) { +UTest an_incomplete_closure_lists_its_unresolved_names_after_the_reason{"PackStageTree/AnIncompleteClosureListsItsUnresolvedNamesAfterTheReason", [] { Tmp t; auto stage = t.path / "app"; write_file(stage / "bin" / "app", "x"); @@ -220,61 +220,59 @@ TEST(PackStageTree, AnIncompleteClosureListsItsUnresolvedNamesAfterTheReason) { closure.walked = false; closure.reason = "the dependency closure of 'app' is incomplete"; closure.needs = {{"@rpath/libgone.dylib", K::Unresolved, {}}}; - ASSERT_TRUE(mcpp::pack::write_stage_manifest(stage, closure)); + expect(fatal(bool(mcpp::pack::write_stage_manifest(stage, closure)))); auto text = read_file(mcpp::pack::stage_manifest_path(stage)); - EXPECT_EQ(text, - "closure = not-walked\n" + expect(eq(text, "closure = not-walked\n" "reason = the dependency closure of 'app' is incomplete\n" "needs\t@rpath/libgone.dylib\tunresolved\n" - "1 bin/app\n"); -} + "1 bin/app\n")); +}}; -TEST(PackStageTree, ANameWithASpaceStaysOneField) { +UTest a_name_with_a_space_stays_one_field{"PackStageTree/ANameWithASpaceStaysOneField", [] { using K = mcpp::pack::ClosureNeed::Kind; - EXPECT_EQ(mcpp::pack::render_closure_need( - {"@rpath/My Kit.framework/My Kit", K::Staged, "bin/My Kit"}), - "needs\t@rpath/My Kit.framework/My Kit\tbin/My Kit"); -} + expect(eq(mcpp::pack::render_closure_need( + {"@rpath/My Kit.framework/My Kit", K::Staged, "bin/My Kit"}), "needs\t@rpath/My Kit.framework/My Kit\tbin/My Kit")); +}}; -TEST(PackStageTree, AMissingTreeIsRefusedRatherThanDescribedAsEmpty) { +UTest a_missing_tree_is_refused_rather_than_described_as_empty{"PackStageTree/AMissingTreeIsRefusedRatherThanDescribedAsEmpty", [] { Tmp t; // An empty manifest for a directory that does not exist would say "nothing // is staged", which is what a correct pack of an empty bundle also says. // The two must not be spelled alike. - EXPECT_FALSE(mcpp::pack::write_stage_manifest(t.path / "never-staged")); - EXPECT_FALSE(std::filesystem::exists( - mcpp::pack::stage_manifest_path(t.path / "never-staged"))); -} + expect(!mcpp::pack::write_stage_manifest(t.path / "never-staged")); + expect(!(std::filesystem::exists( + mcpp::pack::stage_manifest_path(t.path / "never-staged")))); +}}; // ── #622 A5: the wasm32-emscripten stem family ─────────────────────────── // "The launcher is the executable; the family is what the link also wrote" — // asked of the directory, never of a fixed extension list. -TEST(PackStageTree, TheRequiredWasmModuleIsFound) { +UTest the_required_wasm_module_is_found{"PackStageTree/TheRequiredWasmModuleIsFound", [] { Tmp t; write_file(t.path / "app.js", "launcher"); write_file(t.path / "app.wasm", "\0asm"); auto fam = mcpp::pack::emscripten_stem_family(t.path, "app.js"); - EXPECT_TRUE(fam.hasWasm); - EXPECT_EQ(fam.siblings, (std::vector{"app.wasm"})); -} + expect(bool(fam.hasWasm)); + expect(eq(fam.siblings, (std::vector{"app.wasm"}))); +}}; // The negative direction: a launcher with no `.wasm` beside it (a build // directory that does not match the graph) must report the absence, not a // family that happens to be empty of the required member. -TEST(PackStageTree, AMissingWasmModuleIsReported) { +UTest a_missing_wasm_module_is_reported{"PackStageTree/AMissingWasmModuleIsReported", [] { Tmp t; write_file(t.path / "app.js", "launcher"); auto fam = mcpp::pack::emscripten_stem_family(t.path, "app.js"); - EXPECT_FALSE(fam.hasWasm); - EXPECT_TRUE(fam.siblings.empty()); -} + expect(!fam.hasWasm); + expect(fam.siblings.empty()); +}}; // Optional siblings travel when the link wrote them, and only files that // actually share the stem are picked up — a neighbouring, unrelated launcher // in the same directory (a second target) must not bleed into this one's // family. -TEST(PackStageTree, OptionalSiblingsTravelAndUnrelatedFilesDoNot) { +UTest optional_siblings_travel_and_unrelated_files_do_not{"PackStageTree/OptionalSiblingsTravelAndUnrelatedFilesDoNot", [] { Tmp t; write_file(t.path / "app.js", "launcher"); write_file(t.path / "app.wasm", "\0asm"); @@ -283,7 +281,7 @@ TEST(PackStageTree, OptionalSiblingsTravelAndUnrelatedFilesDoNot) { write_file(t.path / "other.js", "a different target's launcher"); write_file(t.path / "other.wasm", "\0asm"); auto fam = mcpp::pack::emscripten_stem_family(t.path, "app.js"); - EXPECT_TRUE(fam.hasWasm); - EXPECT_EQ(fam.siblings, (std::vector{ - "app.data", "app.wasm", "app.worker.js"})); -} + expect(bool(fam.hasWasm)); + expect(eq(fam.siblings, (std::vector{ + "app.data", "app.wasm", "app.worker.js"}))); +}}; diff --git a/tests/unit/test_patch.cpp b/tests/unit/test_patch.cpp index 7e08ccb20..dd965475e 100644 --- a/tests/unit/test_patch.cpp +++ b/tests/unit/test_patch.cpp @@ -1,8 +1,10 @@ -#include import std; +import mcpplibs.mtest; import mcpp.pm.patch; +using namespace mcpplibs::mtest; + // A dependency's patches (design 2026-11-02 part 3 §14.1). The applier is // exact: a hunk applies where its header says, every line of context and every // line it removes byte for byte, or the patch is refused naming the hunk. @@ -31,7 +33,7 @@ std::string get(const fs::path& p) { return ss.str(); } -TEST(Patch, AHunkAppliesWhereItsHeaderSays) { +UTest a_hunk_applies_where_its_header_says{"Patch/AHunkAppliesWhereItsHeaderSays", [] { auto root = fresh("apply"); put(root / "src/a.c", "one\ntwo\nthree\nfour\n"); const std::string diff = @@ -42,11 +44,11 @@ TEST(Patch, AHunkAppliesWhereItsHeaderSays) { "-three\n" "+THREE\n" "+three and a half\n"; - ASSERT_TRUE(pt::apply(root, diff, "p.patch")); - EXPECT_EQ(get(root / "src/a.c"), "one\ntwo\nTHREE\nthree and a half\nfour\n"); -} + expect(fatal(bool(pt::apply(root, diff, "p.patch")))); + expect(eq(get(root / "src/a.c"), "one\ntwo\nTHREE\nthree and a half\nfour\n")); +}}; -TEST(Patch, AHunkThatDoesNotMatchIsRefusedNamingIt) { +UTest a_hunk_that_does_not_match_is_refused_naming_it{"Patch/AHunkThatDoesNotMatchIsRefusedNamingIt", [] { auto root = fresh("refuse"); put(root / "src/a.c", "one\ntwo\nthree\n"); const std::string diff = @@ -56,32 +58,32 @@ TEST(Patch, AHunkThatDoesNotMatchIsRefusedNamingIt) { "-TWO\n" "+2\n"; auto r = pt::apply(root, diff, "p.patch"); - ASSERT_FALSE(r); - EXPECT_NE(r.error().find("hunk 1"), std::string::npos) << r.error(); - EXPECT_NE(r.error().find("line 2"), std::string::npos) << r.error(); - EXPECT_EQ(get(root / "src/a.c"), "one\ntwo\nthree\n"); // untouched -} + expect(fatal(!r)); + if (auto&& failed = expect(ne(r.error().find("hunk 1"), std::string::npos)); !failed) failed << r.error(); + if (auto&& failed = expect(ne(r.error().find("line 2"), std::string::npos)); !failed) failed << r.error(); + expect(eq(get(root / "src/a.c"), "one\ntwo\nthree\n")); // untouched +}}; -TEST(Patch, NothingIsSearchedForElsewhere) { +UTest nothing_is_searched_for_elsewhere{"Patch/NothingIsSearchedForElsewhere", [] { // The text the hunk expects is in the file, one line later than its // header says: an exact applier refuses rather than finding it. auto root = fresh("nofuzz"); put(root / "a.c", "zero\none\ntwo\n"); const std::string diff = "--- a/a.c\n+++ b/a.c\n@@ -1,1 +1,1 @@\n-one\n+ONE\n"; - EXPECT_FALSE(pt::apply(root, diff, "p.patch")); -} + expect(!pt::apply(root, diff, "p.patch")); +}}; -TEST(Patch, BytesAreKeptAsTheyAre) { +UTest bytes_are_kept_as_they_are{"Patch/BytesAreKeptAsTheyAre", [] { // CRLF lines stay CRLF, and a last line without a newline stays without. auto root = fresh("bytes"); put(root / "w.txt", "a\r\nb\r\nc"); const std::string diff = "--- a/w.txt\n+++ b/w.txt\n@@ -2,2 +2,2 @@\n b\r\n-c\n\\ No newline at end of file\n+C\n\\ No newline at end of file\n"; - ASSERT_TRUE(pt::apply(root, diff, "p.patch")); - EXPECT_EQ(get(root / "w.txt"), "a\r\nb\r\nC"); -} + expect(fatal(bool(pt::apply(root, diff, "p.patch")))); + expect(eq(get(root / "w.txt"), "a\r\nb\r\nC")); +}}; -TEST(Patch, FilesAreCreatedAndRemoved) { +UTest files_are_created_and_removed{"Patch/FilesAreCreatedAndRemoved", [] { auto root = fresh("create"); put(root / "old.c", "x\n"); const std::string diff = @@ -91,12 +93,12 @@ TEST(Patch, FilesAreCreatedAndRemoved) { "diff --git a/old.c b/old.c\n" "deleted file mode 100644\n" "--- a/old.c\n+++ /dev/null\n@@ -1,1 +0,0 @@\n-x\n"; - ASSERT_TRUE(pt::apply(root, diff, "p.patch")); - EXPECT_EQ(get(root / "new.c"), "int n;\nint m;\n"); - EXPECT_FALSE(fs::exists(root / "old.c")); -} + expect(fatal(bool(pt::apply(root, diff, "p.patch")))); + expect(eq(get(root / "new.c"), "int n;\nint m;\n")); + expect(!fs::exists(root / "old.c")); +}}; -TEST(Patch, TheSetDigestIsTheContentsInOrder) { +UTest the_set_digest_is_the_contents_in_order{"Patch/TheSetDigestIsTheContentsInOrder", [] { auto d = fresh("digest"); put(d / "one.patch", "A"); put(d / "two.patch", "B"); @@ -104,36 +106,36 @@ TEST(Patch, TheSetDigestIsTheContentsInOrder) { auto ab = pt::set_digest({(d / "one.patch").string(), (d / "two.patch").string()}); auto ba = pt::set_digest({(d / "two.patch").string(), (d / "one.patch").string()}); auto ab2 = pt::set_digest({(d / "renamed.patch").string(), (d / "two.patch").string()}); - ASSERT_TRUE(ab && ba && ab2); - EXPECT_NE(*ab, *ba); - EXPECT_EQ(*ab, *ab2); - EXPECT_EQ(ab->size(), 64u); - EXPECT_EQ(pt::variant_dir_name("3.6.1", *ab), std::format("3.6.1+p{}", ab->substr(0, 12))); -} - -TEST(Patch, AVariantIsMadeOnceAndWhole) { + expect(fatal(ab && ba && ab2)); + expect(ne(*ab, *ba)); + expect(eq(*ab, *ab2)); + expect(eq(ab->size(), 64u)); + expect(eq(pt::variant_dir_name("3.6.1", *ab), std::format("3.6.1+p{}", ab->substr(0, 12)))); +}}; + +UTest a_variant_is_made_once_and_whole{"Patch/AVariantIsMadeOnceAndWhole", [] { auto d = fresh("variant"); put(d / "src/inc/m.h", "#define M 1\n"); put(d / "fix.patch", "--- a/inc/m.h\n+++ b/inc/m.h\n@@ -1 +1 @@\n-#define M 1\n+#define M 2\n"); auto digest = pt::set_digest({(d / "fix.patch").string()}); - ASSERT_TRUE(digest); + expect(fatal(bool(digest))); const auto variant = d / "variants" / pt::variant_dir_name("1.0", *digest); - ASSERT_TRUE(pt::materialize(d / "src", {(d / "fix.patch").string()}, *digest, variant)); - EXPECT_EQ(get(variant / "inc/m.h"), "#define M 2\n"); - EXPECT_EQ(get(d / "src/inc/m.h"), "#define M 1\n"); // the source is untouched + expect(fatal(bool(pt::materialize(d / "src", {(d / "fix.patch").string()}, *digest, variant)))); + expect(eq(get(variant / "inc/m.h"), "#define M 2\n")); + expect(eq(get(d / "src/inc/m.h"), "#define M 1\n")); // the source is untouched // A second call finds it made. put(variant / "inc/m.h", "#define M 3\n"); - ASSERT_TRUE(pt::materialize(d / "src", {(d / "fix.patch").string()}, *digest, variant)); - EXPECT_EQ(get(variant / "inc/m.h"), "#define M 3\n"); + expect(fatal(bool(pt::materialize(d / "src", {(d / "fix.patch").string()}, *digest, variant)))); + expect(eq(get(variant / "inc/m.h"), "#define M 3\n")); // A patch that does not apply leaves no variant. put(d / "bad.patch", "--- a/inc/m.h\n+++ b/inc/m.h\n@@ -1 +1 @@\n-#define M 9\n+#define M 2\n"); auto bad = pt::set_digest({(d / "bad.patch").string()}); const auto badVariant = d / "variants" / pt::variant_dir_name("1.0", *bad); - EXPECT_FALSE(pt::materialize(d / "src", {(d / "bad.patch").string()}, *bad, badVariant)); - EXPECT_FALSE(fs::exists(badVariant)); -} + expect(!(pt::materialize(d / "src", {(d / "bad.patch").string()}, *bad, badVariant))); + expect(!fs::exists(badVariant)); +}}; -TEST(Patch, AVariantNeverWritesThroughALink) { +UTest a_variant_never_writes_through_a_link{"Patch/AVariantNeverWritesThroughALink", [] { // A store may hold a version directory, or a file in it, as a link into // another store (a test's home linking the developer's payloads, measured: // the first version of this patched the developer's own zlib.h through @@ -143,19 +145,41 @@ TEST(Patch, AVariantNeverWritesThroughALink) { put(d / "real/inc/other.h", "#define O 1\n"); std::error_code ec; fs::create_directory_symlink(d / "real", d / "linked", ec); - if (ec) GTEST_SKIP() << "no symbolic links here: " << ec.message(); + if (ec) skip([&] { std::ostringstream text; text << "no symbolic links here: " << ec.message(); return text.str(); }()); fs::create_symlink(d / "real/inc/other.h", d / "real/inc/o-link.h", ec); put(d / "fix.patch", "--- a/inc/m.h\n+++ b/inc/m.h\n@@ -1 +1 @@\n-#define M 1\n+#define M 2\n" "--- a/inc/o-link.h\n+++ b/inc/o-link.h\n@@ -1 +1 @@\n-#define O 1\n+#define O 2\n"); auto digest = pt::set_digest({(d / "fix.patch").string()}); - ASSERT_TRUE(digest); + expect(fatal(bool(digest))); const auto variant = d / "variants" / pt::variant_dir_name("1.0", *digest); - ASSERT_TRUE(pt::materialize(d / "linked", {(d / "fix.patch").string()}, *digest, variant)); - EXPECT_EQ(get(variant / "inc/m.h"), "#define M 2\n"); - EXPECT_EQ(get(variant / "inc/o-link.h"), "#define O 2\n"); - EXPECT_FALSE(fs::is_symlink(variant)); - EXPECT_EQ(get(d / "real/inc/m.h"), "#define M 1\n"); - EXPECT_EQ(get(d / "real/inc/other.h"), "#define O 1\n"); -} + expect(fatal(bool(pt::materialize(d / "linked", {(d / "fix.patch").string()}, *digest, variant)))); + expect(eq(get(variant / "inc/m.h"), "#define M 2\n")); + expect(eq(get(variant / "inc/o-link.h"), "#define O 2\n")); + expect(!fs::is_symlink(variant)); + expect(eq(get(d / "real/inc/m.h"), "#define M 1\n")); + expect(eq(get(d / "real/inc/other.h"), "#define O 1\n")); +}}; } // namespace + +// THE PATCH ROOT IS THE PACKAGE'S (mcpp#911 G8): a patch names files from the +// directory of the package's manifest inside its archive; one written from the +// archive's top, as before, applies as written and is said. +UTest a_patch_names_files_from_the_package_root{"Patch/APatchNamesFilesFromThePackageRoot", [] { + auto base = fresh("root"); + put(base / "src/pkg-1.0/run/mcpp.toml", "[package]\nname = \"run\"\n"); + const auto rooted = base / "rooted.patch", topped = base / "topped.patch"; + put(rooted, "--- a/mcpp.toml\n+++ b/mcpp.toml\n@@ -1,2 +1,3 @@\n [package]\n name = \"run\"\n+version = \"1.0\"\n"); + put(topped, "--- a/pkg-1.0/run/mcpp.toml\n+++ b/pkg-1.0/run/mcpp.toml\n@@ -1,2 +1,3 @@\n [package]\n name = \"run\"\n+version = \"1.0\"\n"); + std::vector notes; + auto r = pt::materialize(base / "src", {rooted.string()}, "d1", base / "v1", "pkg-1.0/run", ¬es); + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); + expect(eq(get(base / "v1/pkg-1.0/run/mcpp.toml"), "[package]\nname = \"run\"\nversion = \"1.0\"\n")); + expect(notes.empty()); + auto t = pt::materialize(base / "src", {topped.string()}, "d2", base / "v2", "pkg-1.0/run", ¬es); + if (auto&& failed = expect(fatal(t.has_value())); !failed) failed << t.error(); + expect(eq(get(base / "v2/pkg-1.0/run/mcpp.toml"), "[package]\nname = \"run\"\nversion = \"1.0\"\n")); + expect(fatal(eq(notes.size(), 1u))); + expect(ne(notes[0].find("from the archive's top"), std::string::npos)) << notes[0]; + fs::remove_all(base); +}}; diff --git a/tests/unit/test_pe_exports.cpp b/tests/unit/test_pe_exports.cpp index e453eb8ca..2ba90d224 100644 --- a/tests/unit/test_pe_exports.cpp +++ b/tests/unit/test_pe_exports.cpp @@ -1,6 +1,6 @@ -#include import std; +import mcpplibs.mtest; import mcpp.build.pe_exports; import mcpp.build.coff_exports; import mcpp.build.plan; @@ -9,28 +9,30 @@ import mcpp.modgraph.graph; import mcpp.modgraph.scanner; import mcpp.toolchain.model; +using namespace mcpplibs::mtest; + using mcpp::build::pe::ir_declares_exports; using mcpp::build::pe::read_nm_exports; -TEST(PeExports, RecognizesBothBitcodeContainersByContents) { +UTest recognizes_both_bitcode_containers_by_contents{"PeExports/RecognizesBothBitcodeContainersByContents", [] { const std::array raw{std::byte{0x42}, std::byte{0x43}, std::byte{0xc0}, std::byte{0xde}}; const std::array wrapped{std::byte{0xde}, std::byte{0xc0}, std::byte{0x17}, std::byte{0x0b}}; const std::array coff{std::byte{0x64}, std::byte{0x86}, std::byte{1}, std::byte{0}}; - EXPECT_TRUE(mcpp::build::pe::is_bitcode(raw)); - EXPECT_TRUE(mcpp::build::pe::is_bitcode(wrapped)); - EXPECT_FALSE(mcpp::build::pe::is_bitcode(coff)); - EXPECT_FALSE(mcpp::build::pe::is_bitcode(std::span(raw).first(3))); -} + expect(mcpp::build::pe::is_bitcode(raw)); + expect(mcpp::build::pe::is_bitcode(wrapped)); + expect(!mcpp::build::pe::is_bitcode(coff)); + expect(!(mcpp::build::pe::is_bitcode(std::span(raw).first(3)))); +}}; -TEST(PeExports, DllStorageIntentCoversFunctionsDataAndAliases) { - EXPECT_TRUE(ir_declares_exports("define dso_local dllexport i32 @api() {\nret i32 7\n}")); - EXPECT_TRUE(ir_declares_exports("@api = dllexport global i32 7")); - EXPECT_TRUE(ir_declares_exports("@api = dllexport alias i32, ptr @other")); - EXPECT_TRUE(ir_declares_exports("@\"name with spaces\" = dso_local dllexport constant i32 7")); -} +UTest dll_storage_intent_covers_functions_data_and_aliases{"PeExports/DllStorageIntentCoversFunctionsDataAndAliases", [] { + expect(bool(ir_declares_exports("define dso_local dllexport i32 @api() {\nret i32 7\n}"))); + expect(bool(ir_declares_exports("@api = dllexport global i32 7"))); + expect(bool(ir_declares_exports("@api = dllexport alias i32, ptr @other"))); + expect(bool(ir_declares_exports("@\"name with spaces\" = dso_local dllexport constant i32 7"))); +}}; -TEST(PeExports, NamesCommentsAttributesAndDataDoNotDeclareExports) { - EXPECT_FALSE(ir_declares_exports(R"( +UTest names_comments_attributes_and_data_do_not_declare_exports{"PeExports/NamesCommentsAttributesAndDataDoNotDeclareExports", [] { + expect(!(ir_declares_exports(R"( ; define dllexport i32 @comment() @dllexport = global i32 1 @"dllexport" = global i32 2 @@ -42,22 +44,22 @@ define i32 @dllexport() { attributes #0 = { "dllexport"="/EXPORT:pretend" } !llvm.ident = !{!1} !1 = !{!"/EXPORT:not_a_linker_option"} -)")); -} +)"))); +}}; -TEST(PeExports, LinkerMetadataIntentFollowsOnlyItsNamedRoot) { - EXPECT_TRUE(ir_declares_exports(R"( +UTest linker_metadata_intent_follows_only_its_named_root{"PeExports/LinkerMetadataIntentFollowsOnlyItsNamedRoot", [] { + expect(bool(ir_declares_exports(R"( !llvm.linker.options = !{!7, !8} !7 = !{!"/DEFAULTLIB:libcmt"} !8 = !{!"\2DEXPORT:chosen"} -)")); - EXPECT_TRUE(ir_declares_exports("!llvm.linker.options = !{!\"/ExPoRt:chosen,DATA\"}")); - EXPECT_TRUE(ir_declares_exports("module asm \".section .drectve\\0A.ascii \\22 /EXPORT:chosen\\22\"")); - EXPECT_FALSE(ir_declares_exports("!llvm.linker.options = !{!1}\n!1 = !{!\"/DEFAULTLIB:x\"}\n!2 = !{!\"/EXPORT:unused\"}")); - EXPECT_FALSE(ir_declares_exports("!llvm.linker.options = !{!1}\n!1 = !{!1}")); -} +)"))); + expect(bool(ir_declares_exports("!llvm.linker.options = !{!\"/ExPoRt:chosen,DATA\"}"))); + expect(bool(ir_declares_exports("module asm \".section .drectve\\0A.ascii \\22 /EXPORT:chosen\\22\""))); + expect(!ir_declares_exports("!llvm.linker.options = !{!1}\n!1 = !{!\"/DEFAULTLIB:x\"}\n!2 = !{!\"/EXPORT:unused\"}")); + expect(!ir_declares_exports("!llvm.linker.options = !{!1}\n!1 = !{!1}")); +}}; -TEST(PeExports, NmCandidatesKeepDataAndComdatButExcludeNonDefinitions) { +UTest nm_candidates_keep_data_and_comdat_but_exclude_non_definitions{"PeExports/NmCandidatesKeepDataAndComdatButExcludeNonDefinitions", [] { auto symbols = read_nm_exports(R"( function T ---------------- 0 data D ---------------- 0 @@ -71,30 +73,30 @@ absolute A ---------------- 0 ??_Gwidget@@ T ---------------- 0 managed.name T ---------------- 0 )", false); - ASSERT_TRUE(symbols.has_value()) << symbols.error(); + if (auto&& failed = expect(fatal(symbols.has_value())); !failed) failed << symbols.error(); const std::vector expected{ {"function", false}, {"data", true}, {"zero", true}, {"constant", true}, {"comdat", false}, {"vtable", true}}; - EXPECT_EQ(*symbols, expected); -} + expect(eq(*symbols, expected)); +}}; -TEST(PeExports, I386KeepsStdcallDecorationButNormalizesCdecl) { +UTest i386_keeps_stdcall_decoration_but_normalizes_cdecl{"PeExports/I386KeepsStdcallDecorationButNormalizesCdecl", [] { auto symbols = read_nm_exports("_cdecl T ---------------- 0\n_stdcall@4 T ---------------- 0\n_variable D ---------------- 0\n", true); - ASSERT_TRUE(symbols.has_value()); + expect(fatal(symbols.has_value())); const std::vector expected{ {"cdecl", false}, {"_stdcall@4", false}, {"variable", true}}; - EXPECT_EQ(*symbols, expected); + expect(eq(*symbols, expected)); auto amd64 = read_nm_exports("_cdecl T ---------------- 0", false); - ASSERT_TRUE(amd64.has_value()); - EXPECT_EQ(amd64->front().name, "_cdecl"); -} + expect(fatal(amd64.has_value())); + expect(eq(amd64->front().name, "_cdecl")); +}}; -TEST(PeExports, UnexpectedNmOutputIsAnErrorRatherThanAnEmptySurface) { - EXPECT_FALSE(read_nm_exports("tool emitted an unexpected diagnostic", false)); - EXPECT_FALSE(read_nm_exports("candidate T", false)); -} +UTest unexpected_nm_output_is_an_error_rather_than_an_empty_surface{"PeExports/UnexpectedNmOutputIsAnErrorRatherThanAnEmptySurface", [] { + expect(!read_nm_exports("tool emitted an unexpected diagnostic", false)); + expect(!read_nm_exports("candidate T", false)); +}}; -TEST(PeExports, OptOutAffectsOnlyTheMsvcAbiSharedLinkForm) { +UTest opt_out_affects_only_the_msvc_abi_shared_link_form{"PeExports/OptOutAffectsOnlyTheMsvcAbiSharedLinkForm", [] { for (auto triple : {"x86_64-pc-windows-msvc", "i686-pc-windows-msvc", "x86_64-w64-mingw32", "x86_64-linux-gnu", "aarch64-apple-darwin"}) { for (bool enabled : {false, true}) { @@ -112,43 +114,42 @@ TEST(PeExports, OptOutAffectsOnlyTheMsvcAbiSharedLinkForm) { root.manifest = manifest; const auto plan = mcpp::build::make_plan(manifest, tc, {}, {}, {}, {root}, root.root, root.root / "target", {}, {}); - ASSERT_TRUE(plan.has_value()) << plan.error(); - ASSERT_EQ(plan->linkUnits.size(), 1u); - EXPECT_EQ(!plan->linkUnits.front().defFile.empty(), - enabled && std::string_view(triple).ends_with("windows-msvc")) << triple; + if (auto&& failed = expect(fatal(plan.has_value())); !failed) failed << plan.error(); + expect(fatal(eq(plan->linkUnits.size(), 1u))); + expect(eq(!plan->linkUnits.front().defFile.empty(), enabled && std::string_view(triple).ends_with("windows-msvc"))) << triple; } } -} +}}; -TEST(PeExports, SymbolPatternsFollowVersionScriptGlobs) { +UTest symbol_patterns_follow_version_script_globs{"PeExports/SymbolPatternsFollowVersionScriptGlobs", [] { using mcpp::build::pe::symbol_matches; - EXPECT_TRUE(symbol_matches("vk_icd*", "vk_icdGetInstanceProcAddr")); - EXPECT_TRUE(symbol_matches("vk_icd*", "vk_icd")); - EXPECT_FALSE(symbol_matches("vk_icd*", "vkGetInstanceProcAddr")); - EXPECT_TRUE(symbol_matches("api_?", "api_x")); - EXPECT_FALSE(symbol_matches("api_?", "api_xy")); - EXPECT_TRUE(symbol_matches("api_[a-c]*", "api_b_open")); - EXPECT_FALSE(symbol_matches("api_[!a-c]*", "api_b_open")); - EXPECT_TRUE(symbol_matches("*Plugin*", "?createPlugin@@YAPEAXXZ")); - EXPECT_TRUE(symbol_matches("exact", "exact")); - EXPECT_FALSE(symbol_matches("exact", "exactly")); - EXPECT_TRUE(symbol_matches("*", "")); - EXPECT_TRUE(symbol_matches("a[b", "a[b")); // an unterminated class is a character -} + expect(symbol_matches("vk_icd*", "vk_icdGetInstanceProcAddr")); + expect(symbol_matches("vk_icd*", "vk_icd")); + expect(!symbol_matches("vk_icd*", "vkGetInstanceProcAddr")); + expect(symbol_matches("api_?", "api_x")); + expect(!symbol_matches("api_?", "api_xy")); + expect(symbol_matches("api_[a-c]*", "api_b_open")); + expect(!symbol_matches("api_[!a-c]*", "api_b_open")); + expect(symbol_matches("*Plugin*", "?createPlugin@@YAPEAXXZ")); + expect(symbol_matches("exact", "exact")); + expect(!symbol_matches("exact", "exactly")); + expect(symbol_matches("*", "")); + expect(symbol_matches("a[b", "a[b")); // an unterminated class is a character +}}; -TEST(PeExports, NarrowKeepsTheMatchingCandidatesAndTheirDataFlag) { +UTest narrow_keeps_the_matching_candidates_and_their_data_flag{"PeExports/NarrowKeepsTheMatchingCandidatesAndTheirDataFlag", [] { std::vector all{ {"plugin_open", false}, {"plugin_table", true}, {"helper", false}}; const std::vector patterns{"plugin_*"}; auto kept = mcpp::build::pe::narrow(all, patterns); - ASSERT_EQ(kept.size(), 2u); - EXPECT_EQ(kept[0].name, "plugin_open"); - EXPECT_FALSE(kept[0].data); - EXPECT_EQ(kept[1].name, "plugin_table"); - EXPECT_TRUE(kept[1].data); -} + expect(fatal(eq(kept.size(), 2u))); + expect(eq(kept[0].name, "plugin_open")); + expect(!(kept[0].data)); + expect(eq(kept[1].name, "plugin_table")); + expect(bool(kept[1].data)); +}}; -TEST(PeExports, ExportsWithoutDiscoveryIsRefusedOnlyOnMsvcAbiRows) { +UTest exports_without_discovery_is_refused_only_on_msvc_abi_rows{"PeExports/ExportsWithoutDiscoveryIsRefusedOnlyOnMsvcAbiRows", [] { for (auto triple : {"x86_64-pc-windows-msvc", "x86_64-w64-mingw32", "x86_64-linux-gnu"}) { mcpp::manifest::Manifest manifest; manifest.package.name = "probe"; @@ -166,10 +167,10 @@ TEST(PeExports, ExportsWithoutDiscoveryIsRefusedOnlyOnMsvcAbiRows) { const auto plan = mcpp::build::make_plan(manifest, tc, {}, {}, {}, {root}, root.root, root.root / "target", {}, {}); if (std::string_view(triple).ends_with("windows-msvc")) { - ASSERT_FALSE(plan.has_value()) << triple; - EXPECT_NE(plan.error().find("windows_auto_export = false"), std::string::npos); + expect(fatal(!plan.has_value())) << triple; + expect(ne(plan.error().find("windows_auto_export = false"), std::string::npos)); } else { - EXPECT_TRUE(plan.has_value()) << triple << ": " << (plan ? "" : plan.error()); + if (auto&& failed = expect(plan.has_value()); !failed) failed << triple << ": " << (plan ? "" : plan.error()); } } -} +}}; diff --git a/tests/unit/test_platform.cpp b/tests/unit/test_platform.cpp index 4bf6dcd0b..a1cefa43f 100644 --- a/tests/unit/test_platform.cpp +++ b/tests/unit/test_platform.cpp @@ -1,11 +1,13 @@ -#include import std; +import mcpplibs.mtest; import mcpp.manifest; import mcpp.platform; import mcpp.build.prepare_inputs; import mcpp.toolchain.triple; +using namespace mcpplibs::mtest; + // SPEC-013 (design 2026-10-28): what a platform states, how it is read in its // three places, which platform a graph selects, how the tiers merge, and the // two templates of the link (artifact names, seal steps). Tables, so each rule @@ -20,7 +22,7 @@ namespace { mm::Manifest parse(std::string_view text) { auto m = mm::parse_string(text); - if (!m) ADD_FAILURE() << m.error().format(); + if (!m) expect(false) << m.error().format(); return m ? *m : mm::Manifest{}; } @@ -81,41 +83,41 @@ openkal-musl = "0.22.3" // ── The reader ────────────────────────────────────────────────────────────── -TEST(PlatformManifest, EveryGroupIsRead) { +UTest every_group_is_read{"PlatformManifest/EveryGroupIsRead", [] { auto m = parse(kPlatform); - ASSERT_TRUE(m.platformDeclared); + expect(fatal(bool(m.platformDeclared))); auto const& p = m.platform; - EXPECT_EQ(p.targets.value, (std::vector{"x86_64-openkal", "aarch64-openkal"})); - EXPECT_EQ(p.toolchain.value, "llvm@23.1.3"); - EXPECT_EQ(p.runner.cmd, (std::vector{"okrun", "{}"})); - EXPECT_EQ(p.runner.host, "same-arch"); - EXPECT_EQ(p.carriers.at(0).package.spec.shortName, "openkal-run"); - EXPECT_EQ(p.carriers.at(0).section, ".oke"); - EXPECT_TRUE(p.carriers.at(0).main.ends_with("carrier/main.cpp")); - ASSERT_TRUE(p.layers.contains("c++-abi")); - EXPECT_EQ(p.layers.at("c++-abi").ref.spec.version, "0.16.3"); - EXPECT_EQ(p.layers.at("c-abi").ref.spec.shortName, "openkal-musl"); - EXPECT_FALSE(p.layers.at("c-abi").ref.spec.defaultFeatures); - EXPECT_EQ(p.layers.at("c-abi").ref.spec.features, (std::vector{"posix"})); - EXPECT_TRUE(p.layers.at("kernel-abi").ref.off); - EXPECT_EQ(p.tools.at("okld").ref.spec.shortName, "openkal-tools"); - EXPECT_EQ(p.tools.at("probe").ref.program, "probe-rs"); - EXPECT_EQ(p.build.cxxflags, (std::vector{"-fPIC"})); - ASSERT_EQ(p.buildCfg.size(), 1u); - EXPECT_EQ(p.link.driver.value, "lld"); - EXPECT_EQ(p.link.start.value, "ok_main"); - ASSERT_EQ(p.link.inputs.size(), 2u); - EXPECT_EQ(p.link.inputs[0].as, "ok-notes.S"); - EXPECT_TRUE(p.link.inputs[1].file.ends_with("crt/x.o")); - EXPECT_EQ(p.link.seal.size(), 2u); - EXPECT_EQ(p.hosted.toolchain.value, "llvm@23.1.3"); - EXPECT_TRUE(p.hosted.layers.contains("c-abi")); - EXPECT_EQ(p.versions.at("openkal-musl"), "0.22.3"); - EXPECT_EQ(p.runner.origin.tier, mp::Tier::Platform); - EXPECT_GT(p.runner.origin.line, 0); -} - -TEST(PlatformManifest, RefusalsNameTheKey) { + expect(eq(p.targets.value, (std::vector{"x86_64-openkal", "aarch64-openkal"}))); + expect(eq(p.toolchain.value, "llvm@23.1.3")); + expect(eq(p.runner.cmd, (std::vector{"okrun", "{}"}))); + expect(eq(p.runner.host, "same-arch")); + expect(eq(p.carriers.at(0).package.spec.shortName, "openkal-run")); + expect(eq(p.carriers.at(0).section, ".oke")); + expect(p.carriers.at(0).main.ends_with("carrier/main.cpp")); + expect(fatal(p.layers.contains("c++-abi"))); + expect(eq(p.layers.at("c++-abi").ref.spec.version, "0.16.3")); + expect(eq(p.layers.at("c-abi").ref.spec.shortName, "openkal-musl")); + expect(!(p.layers.at("c-abi").ref.spec.defaultFeatures)); + expect(eq(p.layers.at("c-abi").ref.spec.features, (std::vector{"posix"}))); + expect(bool(p.layers.at("kernel-abi").ref.off)); + expect(eq(p.tools.at("okld").ref.spec.shortName, "openkal-tools")); + expect(eq(p.tools.at("probe").ref.program, "probe-rs")); + expect(eq(p.build.cxxflags, (std::vector{"-fPIC"}))); + expect(fatal(eq(p.buildCfg.size(), 1u))); + expect(eq(p.link.driver.value, "lld")); + expect(eq(p.link.start.value, "ok_main")); + expect(fatal(eq(p.link.inputs.size(), 2u))); + expect(eq(p.link.inputs[0].as, "ok-notes.S")); + expect(p.link.inputs[1].file.ends_with("crt/x.o")); + expect(eq(p.link.seal.size(), 2u)); + expect(eq(p.hosted.toolchain.value, "llvm@23.1.3")); + expect(p.hosted.layers.contains("c-abi")); + expect(eq(p.versions.at("openkal-musl"), "0.22.3")); + expect(eq(p.runner.origin.tier, mp::Tier::Platform)); + expect(gt(p.runner.origin.line, 0)); +}}; + +UTest refusals_name_the_key{"PlatformManifest/RefusalsNameTheKey", [] { const std::string head = "[package]\nname = \"p\"\nversion = \"1.0.0\"\n"; struct Row { std::string body; std::string_view says; }; const Row rows[] = { @@ -150,13 +152,13 @@ TEST(PlatformManifest, RefusalsNameTheKey) { }; for (auto const& r : rows) { auto e = parse_error(head + r.body); - EXPECT_NE(e.find(r.says), std::string::npos) << r.body << "\n=> " << e; + expect(ne(e.find(r.says), std::string::npos)) << r.body << "\n=> " << e; } -} +}}; // An unknown key, in `[platform]`, its tables or `[host]`, is a warning: a // manifest written for a later mcpp still reads (SPEC-013 §18.1). -TEST(PlatformManifest, UnknownKeysWarn) { +UTest unknown_keys_warn{"PlatformManifest/UnknownKeysWarn", [] { auto m = parse(R"( [package] name = "p" @@ -182,11 +184,11 @@ runner = ["x"] "[platform.link] has unsupported key 'later'", "[platform.build] has unsupported key 'rustflags'", "[host] has unsupported key 'runner'" }) - EXPECT_NE(all.find(key), std::string::npos) << key << "\n=> " << all; - EXPECT_EQ(m.platform.link.exe.value, "{name}.oke"); -} + expect(ne(all.find(key), std::string::npos)) << key << "\n=> " << all; + expect(eq(m.platform.link.exe.value, "{name}.oke")); +}}; -TEST(PlatformManifest, OverridesAndHostAreRead) { +UTest overrides_and_host_are_read{"PlatformManifest/OverridesAndHostAreRead", [] { auto m = parse(R"( [package] name = "app" @@ -207,23 +209,23 @@ flags = ["--build-id"] [target.x86_64-openkal] platform = "none" )"); - for (auto const& w : m.schemaWarnings) ADD_FAILURE() << "unexpected warning: " << w; - EXPECT_TRUE(m.hostPlatform.platformSet); - EXPECT_EQ(m.hostPlatform.platform.spec.shortName, "openkal-platform"); - ASSERT_EQ(m.platformOverrides.size(), 2u); + for (auto const& w : m.schemaWarnings) expect(false) << "unexpected warning: " << w; + expect(bool(m.hostPlatform.platformSet)); + expect(eq(m.hostPlatform.platform.spec.shortName, "openkal-platform")); + expect(fatal(eq(m.platformOverrides.size(), 2u))); auto const& o = m.platformOverrides[0]; - EXPECT_EQ(o.fields.runner.cmd.front(), "my-okrun"); - EXPECT_TRUE(o.fields.runner.exclusive); - EXPECT_TRUE(o.fields.layers.at("c-abi").ref.spec.path.ends_with("openkal-musl")); - EXPECT_TRUE(o.fields.tools.at("okld").ref.spec.path.ends_with("openkal-image/tools")); - EXPECT_EQ(o.fields.link.flags.value, (std::vector{"--build-id"})); - EXPECT_EQ(o.fields.runner.origin.tier, mp::Tier::Manifest); - EXPECT_TRUE(m.platformOverrides[1].platformNone); + expect(eq(o.fields.runner.cmd.front(), "my-okrun")); + expect(bool(o.fields.runner.exclusive)); + expect(o.fields.layers.at("c-abi").ref.spec.path.ends_with("openkal-musl")); + expect(o.fields.tools.at("okld").ref.spec.path.ends_with("openkal-image/tools")); + expect(eq(o.fields.link.flags.value, (std::vector{"--build-id"}))); + expect(eq(o.fields.runner.origin.tier, mp::Tier::Manifest)); + expect(bool(m.platformOverrides[1].platformNone)); // The runner's command is the row's template as well. - EXPECT_EQ(m.targetOverrides.at("cfg(platform = \"openkal\")").runner.front(), "my-okrun"); -} + expect(eq(m.targetOverrides.at("cfg(platform = \"openkal\")").runner.front(), "my-okrun")); +}}; -TEST(PlatformManifest, ReferencesInEveryForm) { +UTest references_in_every_form{"PlatformManifest/ReferencesInEveryForm", [] { struct Row { std::string_view text; std::string_view ns, name, version; }; const Row rows[] = { { "openkal-platform@0.17.1", "mcpplibs", "openkal-platform", "0.17.1" }, @@ -232,20 +234,20 @@ TEST(PlatformManifest, ReferencesInEveryForm) { }; for (auto const& r : rows) { auto ref = mp::parse_ref_text(r.text); - ASSERT_TRUE(ref) << r.text; - EXPECT_EQ(ref->spec.namespace_, r.ns); - EXPECT_EQ(ref->spec.shortName, r.name); - EXPECT_EQ(ref->spec.version, r.version); + expect(fatal(bool(ref))) << r.text; + expect(eq(ref->spec.namespace_, r.ns)); + expect(eq(ref->spec.shortName, r.name)); + expect(eq(ref->spec.version, r.version)); } - EXPECT_FALSE(mp::parse_ref_text("openkal-platform")); - EXPECT_FALSE(mp::parse_ref_text("@1.0")); - EXPECT_EQ(mp::platform_name("openkal-platform"), "openkal"); - EXPECT_EQ(mp::platform_name("stm32-bsp"), "stm32-bsp"); -} + expect(!mp::parse_ref_text("openkal-platform")); + expect(!mp::parse_ref_text("@1.0")); + expect(eq(mp::platform_name("openkal-platform"), "openkal")); + expect(eq(mp::platform_name("stm32-bsp"), "stm32-bsp")); +}}; // ── Selection: the first tier that answers decides ───────────────────────── -TEST(PlatformSelect, TheTiersInOrder) { +UTest the_tiers_in_order{"PlatformSelect/TheTiersInOrder", [] { auto m = parse(R"( [package] name = "app" @@ -270,22 +272,21 @@ platform = "machine-platform@1.0.0" : std::span{}, column }; auto s = pf::select_target(in, t); - EXPECT_TRUE(s) << (s ? "" : s.error()); + if (auto&& failed = expect(bool(s)); !failed) failed << (s ? "" : s.error()); return s ? (s->none ? std::string("none") : s->ref.spec.shortName) + "|" + std::string(mp::tier_name(s->origin.tier)) : std::string("error"); }; - EXPECT_EQ(pick("flag-platform@1.0.0", "env-platform@1.0.0", true, true, "row-platform@1.0.0"), - "flag-platform|flag"); - EXPECT_EQ(pick("", "env-platform@1.0.0", true, true, "row-platform@1.0.0"), "env-platform|env"); - EXPECT_EQ(pick("", "", true, true, "row-platform@1.0.0"), "project-platform|mcpp.toml"); - EXPECT_EQ(pick("", "", false, true, "row-platform@1.0.0"), "machine-platform|mcpp.toml"); - EXPECT_EQ(pick("", "", false, false, "row-platform@1.0.0"), "row-platform|built-in"); - EXPECT_EQ(pick("", "", false, false, ""), "none|built-in"); - EXPECT_EQ(pick("none", "", true, true, "row-platform@1.0.0"), "none|flag"); -} - -TEST(PlatformSelect, TheMostSpecificSelectorDecides) { + expect(eq(pick("flag-platform@1.0.0", "env-platform@1.0.0", true, true, "row-platform@1.0.0"), "flag-platform|flag")); + expect(eq(pick("", "env-platform@1.0.0", true, true, "row-platform@1.0.0"), "env-platform|env")); + expect(eq(pick("", "", true, true, "row-platform@1.0.0"), "project-platform|mcpp.toml")); + expect(eq(pick("", "", false, true, "row-platform@1.0.0"), "machine-platform|mcpp.toml")); + expect(eq(pick("", "", false, false, "row-platform@1.0.0"), "row-platform|built-in")); + expect(eq(pick("", "", false, false, ""), "none|built-in")); + expect(eq(pick("none", "", true, true, "row-platform@1.0.0"), "none|flag")); +}}; + +UTest the_most_specific_selector_decides{"PlatformSelect/TheMostSpecificSelectorDecides", [] { auto m = parse(R"( [package] name = "app" @@ -297,45 +298,45 @@ platform = "arch@1.0.0" )"); pf::ChoiceInputs in{ "", "", "", m.platformOverrides, {}, "" }; auto s = pf::select_target(in, matcher("x86_64-openkal")); - ASSERT_TRUE(s); - EXPECT_EQ(s->ref.spec.shortName, "triple"); + expect(fatal(bool(s))); + expect(eq(s->ref.spec.shortName, "triple")); auto a = pf::select_target(in, matcher("aarch64-openkal")); - ASSERT_TRUE(a); - EXPECT_TRUE(a->none); -} + expect(fatal(bool(a))); + expect(bool(a->none)); +}}; -TEST(PlatformSelect, TheHostGraph) { +UTest the_host_graph{"PlatformSelect/TheHostGraph", [] { mp::Override project, config; config.platformSet = true; config.platform = *mp::parse_ref_text("machine@1.0.0"); auto s = pf::select_host("", false, project, config); - ASSERT_TRUE(s); - EXPECT_EQ(s->ref.spec.shortName, "machine"); + expect(fatal(bool(s))); + expect(eq(s->ref.spec.shortName, "machine")); project.platformSet = true; project.platformNone = true; s = pf::select_host("", false, project, config); - ASSERT_TRUE(s); - EXPECT_TRUE(s->none); + expect(fatal(bool(s))); + expect(bool(s->none)); s = pf::select_host("flag@2.0.0", true, project, config); - ASSERT_TRUE(s); - EXPECT_EQ(s->ref.spec.shortName, "flag"); - EXPECT_EQ(s->origin.tier, mp::Tier::Flag); -} + expect(fatal(bool(s))); + expect(eq(s->ref.spec.shortName, "flag")); + expect(eq(s->origin.tier, mp::Tier::Flag)); +}}; -TEST(PlatformSelect, ADirectoryIsAPath) { +UTest a_directory_is_a_path{"PlatformSelect/ADirectoryIsAPath", [] { auto s = pf::parse_choice("path:/opt/board", {}); - ASSERT_TRUE(s); + expect(fatal(bool(s))); // Absolute on this machine: `D:/opt/board` on a Windows one. - EXPECT_TRUE(s->ref.spec.path.ends_with("/opt/board")) << s->ref.spec.path; + expect(s->ref.spec.path.ends_with("/opt/board")) << s->ref.spec.path; s = pf::parse_choice("../board", {}); - ASSERT_TRUE(s); - EXPECT_FALSE(s->ref.spec.path.empty()); - EXPECT_FALSE(pf::parse_choice("not-a-reference", {})); -} + expect(fatal(bool(s))); + expect(!(s->ref.spec.path.empty())); + expect(!pf::parse_choice("not-a-reference", {})); +}}; // ── Merge: per field, flags appended, sequences replaced ─────────────────── -TEST(PlatformMerge, TheRulesOfSection45) { +UTest the_rules_of_section45{"PlatformMerge/TheRulesOfSection45", [] { auto base = parse(kPlatform).platform; mp::Statement over; over.runner.cmd = {"my-okrun", "{}"}; @@ -346,85 +347,81 @@ TEST(PlatformMerge, TheRulesOfSection45) { over.link.sealSet = true; over.layers["c-abi"] = { mp::PackageRef{ .off = true }, {} }; pf::merge(base, over); - EXPECT_EQ(base.runner.cmd.front(), "my-okrun"); - EXPECT_EQ(base.runner.host, "same-arch"); - EXPECT_EQ(base.build.cxxflags, (std::vector{"-fPIC", "-DUSER"})); - EXPECT_EQ(base.link.flags.value, (std::vector{"-shared", "--build-id"})); - ASSERT_EQ(base.link.seal.size(), 1u); - EXPECT_EQ(base.link.seal.front().front(), "sign"); - EXPECT_EQ(base.link.inputs.size(), 2u); // not stated: kept - EXPECT_TRUE(base.layers.at("c-abi").ref.off); - EXPECT_FALSE(base.layers.at("c++-abi").ref.off); - EXPECT_EQ(base.link.start.value, "ok_main"); -} - -TEST(PlatformMerge, AHostTripleTakesTheHostedPartOnly) { + expect(eq(base.runner.cmd.front(), "my-okrun")); + expect(eq(base.runner.host, "same-arch")); + expect(eq(base.build.cxxflags, (std::vector{"-fPIC", "-DUSER"}))); + expect(eq(base.link.flags.value, (std::vector{"-shared", "--build-id"}))); + expect(fatal(eq(base.link.seal.size(), 1u))); + expect(eq(base.link.seal.front().front(), "sign")); + expect(eq(base.link.inputs.size(), 2u)); // not stated: kept + expect(bool(base.layers.at("c-abi").ref.off)); + expect(!(base.layers.at("c++-abi").ref.off)); + expect(eq(base.link.start.value, "ok_main")); +}}; + +UTest a_host_triple_takes_the_hosted_part_only{"PlatformMerge/AHostTripleTakesTheHostedPartOnly", [] { auto p = parse(kPlatform).platform; auto h = pf::hosted_view(p); - EXPECT_EQ(h.toolchain.value, "llvm@23.1.3"); - EXPECT_TRUE(h.layers.contains("c-abi")); - EXPECT_FALSE(h.layers.contains("c++-abi")); - EXPECT_FALSE(h.runner.set); - EXPECT_FALSE(h.link.driver.set); - EXPECT_TRUE(h.build.empty()); + expect(eq(h.toolchain.value, "llvm@23.1.3")); + expect(h.layers.contains("c-abi")); + expect(!h.layers.contains("c++-abi")); + expect(!h.runner.set); + expect(!h.link.driver.set); + expect(h.build.empty()); auto served = pf::serves(p, "board-platform", *mcpp::toolchain::triple::parse("x86_64-linux-gnu")); - ASSERT_TRUE(served); - EXPECT_TRUE(*served); + expect(fatal(bool(served))); + expect(bool(*served)); auto target = pf::serves(p, "board-platform", *mcpp::toolchain::triple::parse("x86_64-openkal")); - ASSERT_TRUE(target); - EXPECT_FALSE(*target); + expect(fatal(bool(target))); + expect(!(*target)); p.hosted = {}; auto refused = pf::serves(p, "board-platform", *mcpp::toolchain::triple::parse("x86_64-linux-gnu")); - ASSERT_FALSE(refused); - EXPECT_NE(refused.error().find("[platform.hosted]"), std::string::npos); -} + expect(fatal(!refused)); + expect(ne(refused.error().find("[platform.hosted]"), std::string::npos)); +}}; -TEST(PlatformMerge, CfgTablesFollowTheTarget) { +UTest cfg_tables_follow_the_target{"PlatformMerge/CfgTablesFollowTheTarget", [] { auto p = parse(kPlatform).platform; auto x = pf::evaluate_flags(p, matcher("x86_64-openkal")); auto a = pf::evaluate_flags(p, matcher("aarch64-openkal")); - EXPECT_EQ(x.cxxflags, (std::vector{"-fPIC"})); - EXPECT_EQ(a.cxxflags, (std::vector{"-fPIC", "-ffixed-x18"})); -} + expect(eq(x.cxxflags, (std::vector{"-fPIC"}))); + expect(eq(a.cxxflags, (std::vector{"-fPIC", "-ffixed-x18"}))); +}}; -TEST(PlatformMerge, CfgPlatformMatchesTheNameOrThePackage) { - EXPECT_TRUE(cfgpred::matches("cfg(platform = \"openkal\")", +UTest cfg_platform_matches_the_name_or_the_package{"PlatformMerge/CfgPlatformMatchesTheNameOrThePackage", [] { + expect(cfgpred::matches("cfg(platform = \"openkal\")", cfgpred::context_for("x86_64-openkal", "openkal-platform"))); - EXPECT_TRUE(cfgpred::matches("cfg(platform = \"openkal-platform\")", + expect(cfgpred::matches("cfg(platform = \"openkal-platform\")", cfgpred::context_for("x86_64-openkal", "openkal-platform"))); - EXPECT_FALSE(cfgpred::matches("cfg(platform = \"openkal\")", - cfgpred::context_for("x86_64-openkal"))); - EXPECT_GT(mcpp::manifest::cfg::specificity("cfg(os = \"openkal\")"), - mcpp::manifest::cfg::specificity("cfg(platform = \"openkal\")")); -} + expect(!(cfgpred::matches("cfg(platform = \"openkal\")", + cfgpred::context_for("x86_64-openkal")))); + expect(gt(mcpp::manifest::cfg::specificity("cfg(os = \"openkal\")"), mcpp::manifest::cfg::specificity("cfg(platform = \"openkal\")"))); +}}; // ── The link's templates ──────────────────────────────────────────────────── -TEST(PlatformLink, NamePatterns) { +UTest name_patterns{"PlatformLink/NamePatterns", [] { auto e = pf::split_name_pattern("{name}.oke"); - ASSERT_TRUE(e); - EXPECT_EQ(e->first, ""); - EXPECT_EQ(e->second, ".oke"); + expect(fatal(bool(e))); + expect(eq(e->first, "")); + expect(eq(e->second, ".oke")); auto s = pf::split_name_pattern("lib{name}.okso"); - ASSERT_TRUE(s); - EXPECT_EQ(s->first, "lib"); - EXPECT_EQ(s->second, ".okso"); - EXPECT_FALSE(pf::split_name_pattern("name.oke")); - EXPECT_FALSE(pf::split_name_pattern("{name}{name}")); - EXPECT_FALSE(pf::split_name_pattern("bin/{name}")); -} - -TEST(PlatformLink, SealSteps) { + expect(fatal(bool(s))); + expect(eq(s->first, "lib")); + expect(eq(s->second, ".okso")); + expect(!pf::split_name_pattern("name.oke")); + expect(!pf::split_name_pattern("{name}{name}")); + expect(!pf::split_name_pattern("bin/{name}")); +}}; + +UTest seal_steps{"PlatformLink/SealSteps", [] { const std::vector check{"okld", "check", "{}", "--with", "{needed}"}; const std::vector two{"a.okso", "b.okso"}; - EXPECT_EQ(pf::expand_step(check, "x.oke.tmp", two), - (std::vector{"okld", "check", "x.oke.tmp", "--with", "a.okso", - "--with", "b.okso"})); - EXPECT_EQ(pf::expand_step(check, "x.oke.tmp", {}), - (std::vector{"okld", "check", "x.oke.tmp"})); - EXPECT_EQ(pf::expand_step({"sign", "--image={}", "{needed}"}, "x", two), - (std::vector{"sign", "--image=x", "a.okso", "b.okso"})); -} + expect(eq(pf::expand_step(check, "x.oke.tmp", two), (std::vector{"okld", "check", "x.oke.tmp", "--with", "a.okso", + "--with", "b.okso"}))); + expect(eq(pf::expand_step(check, "x.oke.tmp", {}), (std::vector{"okld", "check", "x.oke.tmp"}))); + expect(eq(pf::expand_step({"sign", "--image={}", "{needed}"}, "x", two), (std::vector{"sign", "--image=x", "a.okso", "b.okso"}))); +}}; // ── The run (§10) ─────────────────────────────────────────────────────────── @@ -432,7 +429,7 @@ TEST(PlatformLink, SealSteps) { // this one when they agree; the execution host, with that architecture's // program, when one is named; refused otherwise. `--no-runner` and every // other runner are this machine's. -TEST(PlatformRun, TheRunnerReachesTheArtifactsArchitecture) { +UTest the_runner_reaches_the_artifacts_architecture{"PlatformRun/TheRunnerReachesTheArtifactsArchitecture", [] { using R = pf::RunnerReach; mp::Runner same; same.set = true; @@ -457,14 +454,14 @@ TEST(PlatformRun, TheRunnerReachesTheArtifactsArchitecture) { { &none, "aarch64", true, false, R::Here }, }; for (auto const& r : rows) - EXPECT_EQ(pf::runner_reach(*r.runner, r.target, "x86_64", r.executionHost, r.noRunner), r.want) + expect(eq(pf::runner_reach(*r.runner, r.target, "x86_64", r.executionHost, r.noRunner), r.want)) << r.runner->host << " " << r.target << " executionHost=" << r.executionHost << " noRunner=" << r.noRunner; -} +}}; // ── The report ────────────────────────────────────────────────────────────── -TEST(PlatformReport, SourcesAndJson) { +UTest sources_and_json{"PlatformReport/SourcesAndJson", [] { pf::Resolved r; r.active = true; r.package = "mcpplibs.openkal-platform"; @@ -473,24 +470,24 @@ TEST(PlatformReport, SourcesAndJson) { r.selection.origin = { mp::Tier::BuiltIn, {}, 0, "the target table" }; r.layers["c-abi"] = { "openkal-musl@0.22.3", mp::Tier::Platform }; r.layers["c++-abi"] = { "acme-runtime@1.0.0", mp::Tier::Graph }; - EXPECT_EQ(r.display(), "openkal-platform 0.17.1"); - EXPECT_EQ(r.identity(), "mcpplibs.openkal-platform@0.17.1"); + expect(eq(r.display(), "openkal-platform 0.17.1")); + expect(eq(r.identity(), "mcpplibs.openkal-platform@0.17.1")); auto rows = pf::report_rows(r); - ASSERT_FALSE(rows.empty()); + expect(fatal(!rows.empty())); bool graph = false; for (auto const& [k, v] : rows) if (k == "c++-abi") graph = v.find("(graph)") != std::string::npos; - EXPECT_TRUE(graph); + expect(bool(graph)); auto json = pf::to_json(r); - EXPECT_NE(json.find("\"name\":\"mcpplibs.openkal-platform\""), std::string::npos); - EXPECT_NE(json.find("\"source\":\"built-in\""), std::string::npos); - EXPECT_NE(json.find("\"layers.c++-abi\""), std::string::npos); - EXPECT_EQ(pf::to_json(pf::Resolved{}), "null"); -} + expect(ne(json.find("\"name\":\"mcpplibs.openkal-platform\""), std::string::npos)); + expect(ne(json.find("\"source\":\"built-in\""), std::string::npos)); + expect(ne(json.find("\"layers.c++-abi\""), std::string::npos)); + expect(eq(pf::to_json(pf::Resolved{}), "null")); +}}; // ── Runner hosts, carrier entries, descriptor keys (design 2026-10-29) ────── -TEST(PlatformManifest, RunnerHostsCarrierEntriesAndDescriptorKeysAreRead) { +UTest runner_hosts_carrier_entries_and_descriptor_keys_are_read{"PlatformManifest/RunnerHostsCarrierEntriesAndDescriptorKeysAreRead", [] { auto m = parse(R"( [package] name = "p" @@ -507,26 +504,28 @@ carrier = [ descriptor_keys = ["openkal", "linux"] )"); auto const& p = m.platform; - EXPECT_TRUE(p.runner.hostsSet); - EXPECT_EQ(p.runner.hosts, (std::vector{"linux-x86_64", "macos-aarch64"})); + expect(bool(p.runner.hostsSet)); + expect(eq(p.runner.hosts, (std::vector{"linux-x86_64", "macos-aarch64"}))); // The entry of a model this mcpp does not build is skipped, not refused. - ASSERT_EQ(p.carriers.size(), 2u); - EXPECT_EQ(p.carriers[0].hosts, (std::vector{"linux-x86_64"})); - EXPECT_EQ(p.carriers[0].build, "row"); - EXPECT_EQ(p.descriptorKeys.value, (std::vector{"openkal", "linux"})); + expect(fatal(eq(p.carriers.size(), 2u))); + expect(eq(p.carriers[0].hosts, (std::vector{"linux-x86_64"}))); + expect(eq(p.carriers[0].build, "row")); + expect(eq(p.descriptorKeys.value, (std::vector{"openkal", "linux"}))); // The entry naming the pair serves it; another pair takes the open entry. - EXPECT_EQ(mp::carrier_for(p, "linux-x86_64"), &p.carriers[0]); - EXPECT_EQ(mp::carrier_for(p, "windows-x86_64"), &p.carriers[1]); -} + expect(eq(mp::carrier_for(p, "linux-x86_64"), &p.carriers[0])); + expect(eq(mp::carrier_for(p, "windows-x86_64"), &p.carriers[1])); +}}; -TEST(PlatformManifest, ACarrierWithoutHostsServesEveryPairAndNoneServesNothing) { +UTest a_carrier_without_hosts_serves_every_pair_and_none_serves_nothing{"PlatformManifest/ACarrierWithoutHostsServesEveryPairAndNoneServesNothing", [] { auto m = parse(kPlatform); - EXPECT_EQ(mp::carrier_for(m.platform, "macos-aarch64"), &m.platform.carriers.at(0)); + expect(eq(mp::carrier_for(m.platform, "macos-aarch64"), &m.platform.carriers.at(0))); mp::Statement none; - EXPECT_EQ(mp::carrier_for(none, "linux-x86_64"), nullptr); -} + expect(eq(mp::carrier_for(none, "linux-x86_64"), nullptr)); +}}; -TEST(PlatformManifest, HostPairsAndDescriptorKeysAreAClosedVocabulary) { +// A host pair and a descriptor key are words: a platform's rows may name a +// system and read descriptor blocks of their own (SPEC-016 §7). +UTest host_pairs_and_descriptor_keys_are_words{"PlatformManifest/HostPairsAndDescriptorKeysAreWords", [] { const auto base = std::string(R"( [package] name = "p" @@ -534,14 +533,17 @@ version = "1.0.0" [platform] targets = ["x86_64-openkal"] )"); - EXPECT_NE(parse_error(base + R"(runner = { cmd = ["okrun"], hosts = ["linux"] })" "\n"), ""); - EXPECT_NE(parse_error(base + R"(runner = { cmd = ["okrun"], hosts = ["freebsd-x86_64"] })" "\n"), ""); - EXPECT_NE(parse_error(base + R"(descriptor_keys = ["macos"])" "\n").find("macosx"), std::string::npos); - EXPECT_NE(parse_error(base + R"(descriptor_keys = [])" "\n"), ""); - EXPECT_EQ(parse_error(base + R"(descriptor_keys = ["openkal", "linux"])" "\n"), ""); -} - -TEST(PlatformMerge, AnOverrideReplacesHostsCarriersAndDescriptorKeys) { + expect(ne(parse_error(base + R"(runner = { cmd = ["okrun"], hosts = ["linux"] })" "\n"), "")); + // A system a platform defines is a host's system as well (mcpp#911 G10). + expect(eq(parse_error(base + R"(runner = { cmd = ["okrun"], hosts = ["freebsd-x86_64"] })" "\n"), "")); + expect(ne(parse_error(base + R"(runner = { cmd = ["okrun"], hosts = ["Linux-x86_64"] })" "\n"), "")); + expect(ne(parse_error(base + R"(descriptor_keys = ["mac os"])" "\n").find("macosx"), std::string::npos)); + expect(eq(parse_error(base + R"(descriptor_keys = ["freebsd", "linux"])" "\n"), "")); + expect(ne(parse_error(base + R"(descriptor_keys = [])" "\n"), "")); + expect(eq(parse_error(base + R"(descriptor_keys = ["openkal", "linux"])" "\n"), "")); +}}; + +UTest an_override_replaces_hosts_carriers_and_descriptor_keys{"PlatformMerge/AnOverrideReplacesHostsCarriersAndDescriptorKeys", [] { auto m = parse(kPlatform); mp::Statement over; over.runner.hosts = {"windows-x86_64"}; @@ -553,27 +555,105 @@ TEST(PlatformMerge, AnOverrideReplacesHostsCarriersAndDescriptorKeys) { over.carriers = {c}; auto merged = m.platform; pf::merge(merged, over); - EXPECT_EQ(merged.runner.hosts, (std::vector{"windows-x86_64"})); - EXPECT_EQ(merged.runner.cmd, m.platform.runner.cmd); - ASSERT_EQ(merged.carriers.size(), 1u); - EXPECT_EQ(merged.carriers[0].section, ".x"); - EXPECT_EQ(merged.descriptorKeys.value, (std::vector{"openkal"})); -} + expect(eq(merged.runner.hosts, (std::vector{"windows-x86_64"}))); + expect(eq(merged.runner.cmd, m.platform.runner.cmd)); + expect(fatal(eq(merged.carriers.size(), 1u))); + expect(eq(merged.carriers[0].section, ".x")); + expect(eq(merged.descriptorKeys.value, (std::vector{"openkal"}))); +}}; // D20 (design 2026-11-07 §4.10): the target table's default is one release; a // failed build names a newer one in the index with the line to write, and says // nothing when the project, the configuration or the command line chose. -TEST(PlatformDefault, ANewerReleaseIsNamedOnlyForTheDefault) { +UTest a_newer_release_is_named_only_for_the_default{"PlatformDefault/ANewerReleaseIsNamedOnlyForTheDefault", [] { namespace mp = mcpp::manifest::platform; mp::Origin builtIn{}; builtIn.tier = mp::Tier::BuiltIn; auto tip = mcpp::platform::newer_release_tip(builtIn, "openkal-platform", "0.24.3", "0.24.4", "2026.11.7.2"); - ASSERT_TRUE(tip.has_value()); - EXPECT_NE(tip->find("openkal-platform 0.24.3, mcpp 2026.11.7.2's default"), std::string::npos) << *tip; - EXPECT_NE(tip->find("platform = \"openkal-platform@0.24.4\""), std::string::npos) << *tip; + expect(fatal(tip.has_value())); + if (auto&& failed = expect(ne(tip->find("openkal-platform 0.24.3, mcpp 2026.11.7.2's default"), std::string::npos)); !failed) failed << *tip; + if (auto&& failed = expect(ne(tip->find("platform = \"openkal-platform@0.24.4\""), std::string::npos)); !failed) failed << *tip; for (auto tier : {mp::Tier::Manifest, mp::Tier::Config, mp::Tier::Env, mp::Tier::Flag}) { mp::Origin chosen{}; chosen.tier = tier; - EXPECT_FALSE(mcpp::platform::newer_release_tip(chosen, "openkal-platform", "0.24.3", "0.24.4", "x")); + expect(!mcpp::platform::newer_release_tip(chosen, "openkal-platform", "0.24.3", "0.24.4", "x")); } - EXPECT_FALSE(mcpp::platform::newer_release_tip(builtIn, "openkal-platform", "0.24.3", "0.24.3", "x")); - EXPECT_FALSE(mcpp::platform::newer_release_tip(builtIn, "openkal-platform", "0.24.3", "", "x")); -} + expect(!mcpp::platform::newer_release_tip(builtIn, "openkal-platform", "0.24.3", "0.24.3", "x")); + expect(!mcpp::platform::newer_release_tip(builtIn, "openkal-platform", "0.24.3", "", "x")); +}}; + +// THE STRING FORM OF A REPOSITORY (SPEC-016 §8): `git+` with at most one of +// tag, rev, branch after `#`; none is the remote's default branch. +UTest a_repository_reference_has_a_string_form{"PlatformManifest/ARepositoryReferenceHasAStringForm", [] { + auto tagged = mp::parse_ref_text("git+https://example.org/acme/toy-platform.git#tag=v1"); + expect(fatal(tagged.has_value())); + expect(eq(tagged->spec.git, "https://example.org/acme/toy-platform.git")); + expect(eq(tagged->spec.gitRefKind, "tag")); + expect(eq(tagged->spec.gitRev, "v1")); + auto deflt = mp::parse_ref_text("git+git@example.org:acme/toy.git"); + expect(fatal(deflt.has_value())); + expect(eq(deflt->spec.gitRefKind, "branch")); + expect(deflt->spec.gitRev.empty()); + expect(!mp::parse_ref_text("git+https://example.org/x.git#sha=1").has_value()); + expect(!mp::parse_ref_text("git+").has_value()); +}}; + +// A PLATFORM'S STATEMENT IS CHECKED AGAINST THE ROWS AS IT IS READ (SPEC-016 §5): +// a definition of a built-in row and a row defined twice are errors; a provider +// that serves no row and a tie of priorities are warnings; the served +// identities are ordered by identity, then priority, then provider. +UTest a_platform_statement_is_checked_against_the_rows{"PlatformManifest/AStatementIsCheckedAgainstTheRows", [] { + constexpr std::string_view kHead = R"( +[package] +name = "toy-platform" +version = "0.1.0" + +[platform] +toolchain = "llvm@23.1.3" +)"; + auto redefined = parse_error(std::string(kHead) + R"( +[[platform.define]] +triple = "x86_64-linux-gnu" + +[[platform.provider]] +serves = ["x86_64-linux-gnu"] +)"); + if (auto&& failed = expect(ne(redefined.find("'x86_64-linux-gnu' is a built-in row"), std::string::npos)); !failed) + failed << redefined; + auto twice = parse_error(std::string(kHead) + R"( +[[platform.define]] +triple = "x86_64-toy" + +[[platform.define]] +triple = "x86_64-toy" + +[[platform.provider]] +serves = ["x86_64-toy"] +)"); + if (auto&& failed = expect(ne(twice.find("'x86_64-toy' is defined twice"), std::string::npos)); !failed) + failed << twice; + + auto m = parse(std::string(kHead) + R"( +[[platform.define]] +triple = "x86_64-toy" + +[[platform.provider]] +serves = ["x86_64-toy"] + +[[platform.provider]] +name = "second" +serves = ["*-toy"] + +[[platform.provider]] +name = "nothing" +serves = ["riscv64-nosuch"] +)"); + auto has = [&](std::string_view s) { + return std::ranges::any_of(m.schemaWarnings, [&](auto const& w) { return w.find(s) != std::string::npos; }); + }; + expect(has("'nothing' serves no row")); + expect(has("'x86_64-toy' is served by 'second' and 'toy' at priority 100")); + auto served = mp::served_identities(m.platform, "toy"); + expect(fatal(eq(served.size(), std::size_t{2}))); + expect(eq(served[0].identity, "x86_64-toy")); + expect(eq(served[0].provider, "second")); + expect(eq(served[1].provider, "toy")); +}}; diff --git a/tests/unit/test_platform_apple_sdk.cpp b/tests/unit/test_platform_apple_sdk.cpp index 7933dbb9c..246a60763 100644 --- a/tests/unit/test_platform_apple_sdk.cpp +++ b/tests/unit/test_platform_apple_sdk.cpp @@ -1,11 +1,13 @@ -#include #include import std; +import mcpplibs.mtest; import mcpp.invocation; import mcpplibs.os.platform; import mcpp.toolchain.apple_sdk; +using namespace mcpplibs::mtest; + namespace mac = mcpp::toolchain::apple; namespace { @@ -33,7 +35,7 @@ class ScopedDeploymentTargetEnv { // it, and they are pure, which is why a Linux runner can check them: the // probing itself needs the machine. -TEST(AppleSdk, EveryRequestedSdkNamesItsOwnDirectories) { +UTest every_requested_sdk_names_its_own_directories{"AppleSdk/EveryRequestedSdkNamesItsOwnDirectories", [] { struct Case { std::string_view sdk, platformDir, sdkDir; bool clt; }; const Case cases[] = { { mac::sdk_macos, "MacOSX.platform", "MacOSX.sdk", true }, @@ -42,28 +44,28 @@ TEST(AppleSdk, EveryRequestedSdkNamesItsOwnDirectories) { }; for (auto const& c : cases) { auto l = mac::sdk_layout(c.sdk); - ASSERT_TRUE(l.has_value()) << c.sdk; - EXPECT_EQ(l->platformDir, c.platformDir) << c.sdk; - EXPECT_EQ(l->sdkDir, c.sdkDir) << c.sdk; + expect(fatal(l.has_value())) << c.sdk; + expect(eq(l->platformDir, c.platformDir)) << c.sdk; + expect(eq(l->sdkDir, c.sdkDir)) << c.sdk; // A CLT-ONLY INSTALL SHIPS NO iOS SDK. Probing its SDKs directory for // one would be a path that cannot exist, and the refusal that names // the SDK is the better answer -- so the table says so. - EXPECT_EQ(l->inCommandLineTools, c.clt) << c.sdk; + expect(eq(l->inCommandLineTools, c.clt)) << c.sdk; } // A name no Apple SDK answers to resolves to nothing, and `sdk_path` // returns nullopt before it runs a single command. for (auto bogus : {"", "ios", "iphone", "macos", "watchos"}) { - EXPECT_FALSE(mac::sdk_layout(bogus).has_value()) << bogus; - EXPECT_FALSE(mac::sdk_path(bogus).has_value()) << bogus; + expect(!(mac::sdk_layout(bogus).has_value())) << bogus; + expect(!(mac::sdk_path(bogus).has_value())) << bogus; } -} +}}; // SDKROOT NAMES ONE SDK AND THERE ARE THREE QUESTIONS. Honouring the override // for every request would answer `sdk_path("iphoneos")` with a macOS SDK on // any machine whose shell has SDKROOT set -- a wrong sysroot, which surfaces // much later as missing headers rather than as a bad override. So the override // is matched against what the path IS, and this is that comparison. -TEST(AppleSdk, AnOverrideAnswersOnlyForTheSdkItIs) { +UTest an_override_answers_only_for_the_sdk_it_is{"AppleSdk/AnOverrideAnswersOnlyForTheSdkItIs", [] { struct Case { std::string_view path, name; }; const Case cases[] = { { "/x/MacOSX.sdk", mac::sdk_macos }, @@ -74,15 +76,14 @@ TEST(AppleSdk, AnOverrideAnswersOnlyForTheSdkItIs) { { "/x/iPhoneSimulator18.4.sdk", mac::sdk_iphonesim }, }; for (auto const& c : cases) - EXPECT_EQ(mac::sdk_name_of_root(std::filesystem::path(c.path)), c.name) + expect(eq(mac::sdk_name_of_root(std::filesystem::path(c.path)), c.name)) << c.path; // AND THE TWO iOS NAMES DO NOT COLLAPSE INTO EACH OTHER. Both begin // `iPhone`, and a prefix test would make the simulator SDK satisfy a // device request -- an SDK that compiles and produces an artefact for the // wrong platform, which no later step refuses. - EXPECT_NE(mac::sdk_name_of_root("/x/iPhoneSimulator18.4.sdk"), - mac::sdk_name_of_root("/x/iPhoneOS18.4.sdk")); + expect(ne(mac::sdk_name_of_root("/x/iPhoneSimulator18.4.sdk"), mac::sdk_name_of_root("/x/iPhoneOS18.4.sdk"))); // AND `.sdk` IS REQUIRED. This is positive identification, and the suffix // is the marker Apple's own naming gives an SDK; `iPhoneOS.platform` is @@ -90,33 +91,33 @@ TEST(AppleSdk, AnOverrideAnswersOnlyForTheSdkItIs) { for (auto notAnSdk : {"/x/Developer", "/x/MacOSX", "/x/iPhoneOS", "/x/iPhoneOS.platform", "/x/.sdk", "/x/Frameworks.sdk"}) - EXPECT_TRUE(mac::sdk_name_of_root(notAnSdk).empty()) << notAnSdk; -} + expect(mac::sdk_name_of_root(notAnSdk).empty()) << notAnSdk; +}}; // The override arm itself, which on macOS sits behind a filesystem probe and // is therefore reachable by no other check on a Linux runner. -TEST(AppleSdk, AnUnnamedOverrideAnswersTheDefaultQuestionOnly) { +UTest an_unnamed_override_answers_the_default_question_only{"AppleSdk/AnUnnamedOverrideAnswersTheDefaultQuestionOnly", [] { // Positive identification: an SDK answers for itself and for nothing else. - EXPECT_TRUE (mac::sdkroot_answers("/x/iPhoneOS18.4.sdk", mac::sdk_iphoneos)); - EXPECT_FALSE(mac::sdkroot_answers("/x/iPhoneOS18.4.sdk", mac::sdk_iphonesim)); - EXPECT_FALSE(mac::sdkroot_answers("/x/iPhoneOS18.4.sdk", mac::sdk_macos)); - EXPECT_TRUE (mac::sdkroot_answers("/x/MacOSX15.4.sdk", mac::sdk_macos)); - EXPECT_FALSE(mac::sdkroot_answers("/x/MacOSX15.4.sdk", mac::sdk_iphoneos)); + expect(bool(mac::sdkroot_answers("/x/iPhoneOS18.4.sdk", mac::sdk_iphoneos))); + expect(!mac::sdkroot_answers("/x/iPhoneOS18.4.sdk", mac::sdk_iphonesim)); + expect(!mac::sdkroot_answers("/x/iPhoneOS18.4.sdk", mac::sdk_macos)); + expect(bool(mac::sdkroot_answers("/x/MacOSX15.4.sdk", mac::sdk_macos))); + expect(!mac::sdkroot_answers("/x/MacOSX15.4.sdk", mac::sdk_iphoneos)); // NO FLAG DAY. `SDKROOT=/opt/my-sysroot` is a spelling clang accepts and // a spelling `sdk_path()` honoured before it took a parameter, so it // still answers the default question -- and still does not answer an iOS // one, because a hand-rolled macOS sysroot is not an iOS SDK. for (auto unnamed : {"/opt/my-sysroot", "/x/Developer", "/x/MacOSX"}) { - EXPECT_TRUE (mac::sdkroot_answers(unnamed, mac::sdk_macos)) << unnamed; - EXPECT_FALSE(mac::sdkroot_answers(unnamed, mac::sdk_iphoneos)) << unnamed; - EXPECT_FALSE(mac::sdkroot_answers(unnamed, mac::sdk_iphonesim)) << unnamed; + expect(bool(mac::sdkroot_answers(unnamed, mac::sdk_macos))) << unnamed; + expect(!mac::sdkroot_answers(unnamed, mac::sdk_iphoneos)) << unnamed; + expect(!mac::sdkroot_answers(unnamed, mac::sdk_iphonesim)) << unnamed; } // A name no Apple SDK answers to is not a question, so no override // answers it. - EXPECT_FALSE(mac::sdkroot_answers("/x/MacOSX15.4.sdk", "watchos")); -} + expect(!mac::sdkroot_answers("/x/MacOSX15.4.sdk", "watchos")); +}}; // NO FLAG DAY FOR THE THREE CALLERS. The parameter has a default, and the // default request behaves exactly as the unparameterised function did: on a @@ -124,15 +125,15 @@ TEST(AppleSdk, AnUnnamedOverrideAnswersTheDefaultQuestionOnly) { // `xcrun --show-sdk-path` probe stays first for it and is not used for any // other SDK, because it returns the ACTIVE default rather than the one asked // for. -TEST(AppleSdk, TheDefaultRequestIsTheMacosSdk) { - EXPECT_EQ(mac::sdk_path(), mac::sdk_path(mac::sdk_macos)); +UTest the_default_request_is_the_macos_sdk{"AppleSdk/TheDefaultRequestIsTheMacosSdk", [] { + expect(eq(mac::sdk_path(), mac::sdk_path(mac::sdk_macos))); if constexpr (!mcpplibs::os::is_macos) { - EXPECT_FALSE(mac::sdk_path().has_value()) + expect(!(mac::sdk_path().has_value())) << "an Apple SDK on a host that has none is a located path that " "does not exist"; - EXPECT_FALSE(mac::sdk_path(mac::sdk_iphoneos).has_value()); + expect(!(mac::sdk_path(mac::sdk_iphoneos).has_value())); } -} +}}; // ─── deployment_target: a TARGET decision, not a HOST one (mcpp#685) ──────── // @@ -146,43 +147,43 @@ TEST(AppleSdk, TheDefaultRequestIsTheMacosSdk) { // guard would make `TargetIsMacosEnvBeatsManifestBeatsDefault` fail on any // non-Apple host. -TEST(AppleSdk, DeploymentTargetIsEmptyForANonMacosTarget) { +UTest deployment_target_is_empty_for_a_non_macos_target{"AppleSdk/DeploymentTargetIsEmptyForANonMacosTarget", [] { // No env, no manifest value, and no env with a manifest value: a // non-macOS target gets no floor regardless of what either input says, // because applying one would be an answer to a question this target // never asked (see min_platform_version's Apple-only branch). ScopedDeploymentTargetEnv noEnv(nullptr); - EXPECT_EQ(mac::deployment_target(false, ""), ""); - EXPECT_EQ(mac::deployment_target(false, "11.0"), ""); + expect(eq(mac::deployment_target(false, ""), "")); + expect(eq(mac::deployment_target(false, "11.0"), "")); ScopedDeploymentTargetEnv env("12.3"); - EXPECT_EQ(mac::deployment_target(false, "11.0"), ""); -} + expect(eq(mac::deployment_target(false, "11.0"), "")); +}}; -TEST(AppleSdk, TargetIsMacosEnvBeatsManifestBeatsDefault) { +UTest target_is_macos_env_beats_manifest_beats_default{"AppleSdk/TargetIsMacosEnvBeatsManifestBeatsDefault", [] { { // Neither input set: the built-in floor. ScopedDeploymentTargetEnv noEnv(nullptr); - EXPECT_EQ(mac::deployment_target(true, ""), mac::default_deployment_target); + expect(eq(mac::deployment_target(true, ""), mac::default_deployment_target)); } { // Manifest only: the project's stated default. ScopedDeploymentTargetEnv noEnv(nullptr); - EXPECT_EQ(mac::deployment_target(true, "11.0"), "11.0"); + expect(eq(mac::deployment_target(true, "11.0"), "11.0")); } { // Env only: the per-invocation override, same convention cargo/cc use. ScopedDeploymentTargetEnv env("12.3"); - EXPECT_EQ(mac::deployment_target(true, ""), "12.3"); + expect(eq(mac::deployment_target(true, ""), "12.3")); } { // Both set: env wins. ScopedDeploymentTargetEnv env("12.3"); - EXPECT_EQ(mac::deployment_target(true, "11.0"), "12.3"); + expect(eq(mac::deployment_target(true, "11.0"), "12.3")); } { // An env var present but empty is the same as absent (the convention // every other MACOSX_DEPLOYMENT_TARGET reader in this tree follows). ScopedDeploymentTargetEnv env(""); - EXPECT_EQ(mac::deployment_target(true, "11.0"), "11.0"); + expect(eq(mac::deployment_target(true, "11.0"), "11.0")); } -} +}}; diff --git a/tests/unit/test_platform_env.cpp b/tests/unit/test_platform_env.cpp index 7822fc7e0..575a3180d 100644 --- a/tests/unit/test_platform_env.cpp +++ b/tests/unit/test_platform_env.cpp @@ -1,10 +1,12 @@ -#include #include import std; +import mcpplibs.mtest; import mcpp.invocation; import mcpp.runtime.launch_env; +using namespace mcpplibs::mtest; + namespace { class ScopedVar { @@ -35,18 +37,17 @@ std::string host(std::string_view p) { } // namespace -TEST(PlatformEnv, StripPrivateGlibcDropsOnlyPayloadEntries) { +UTest strip_private_glibc_drops_only_payload_entries{"PlatformEnv/StripPrivateGlibcDropsOnlyPayloadEntries", [] { auto in = join({"/usr/lib", "/home/u/.mcpp/registry/data/xpkgs/xim-x-glibc/2.39/lib64", "/opt/mine"}); - EXPECT_EQ(mcpp::runtime::launch_env::strip_private_glibc(in), - join({host("/usr/lib"), host("/opt/mine")})); -} + expect(eq(mcpp::runtime::launch_env::strip_private_glibc(in), join({host("/usr/lib"), host("/opt/mine")}))); +}}; -TEST(PlatformEnv, StripPrivateGlibcCanEmptyTheList) { - EXPECT_EQ(mcpp::runtime::launch_env::strip_private_glibc( - "/x/xpkgs/xim-x-glibc/2.42/lib64"), ""); - EXPECT_EQ(mcpp::runtime::launch_env::strip_private_glibc(""), ""); -} +UTest strip_private_glibc_can_empty_the_list{"PlatformEnv/StripPrivateGlibcCanEmptyTheList", [] { + expect(eq(mcpp::runtime::launch_env::strip_private_glibc( + "/x/xpkgs/xim-x-glibc/2.42/lib64"), "")); + expect(eq(mcpp::runtime::launch_env::strip_private_glibc(""), "")); +}}; // The regression this guards (mcpp#311 investigation): the composed value // becomes an EXPLICIT override, and process.cppm's merged_environ takes explicit @@ -55,42 +56,38 @@ TEST(PlatformEnv, StripPrivateGlibcCanEmptyTheList) { // glibc, it reached the child anyway and a payload tool patched against a // DIFFERENT glibc segfaulted in the dynamic linker. Sanitizing here is what // makes process.cppm's guarantee actually hold one hop down. -TEST(PlatformEnv, PrependPathListSanitizesTheInheritedLoaderTail) { +UTest prepend_path_list_sanitizes_the_inherited_loader_tail{"PlatformEnv/PrependPathListSanitizesTheInheritedLoaderTail", [] { auto inherited = join({"/first", "/home/u/.mcpp/registry/data/xpkgs/xim-x-glibc/2.39/lib64", "/last"}); ScopedVar ld("LD_LIBRARY_PATH", inherited.c_str()); std::vector dirs{"/payload/lib"}; - EXPECT_EQ(mcpp::runtime::launch_env::prepend_path_list("LD_LIBRARY_PATH", dirs), - join({host("/payload/lib"), host("/first"), host("/last")})); -} + expect(eq(mcpp::runtime::launch_env::prepend_path_list("LD_LIBRARY_PATH", dirs), join({host("/payload/lib"), host("/first"), host("/last")}))); +}}; // A payload dir the CALLER passed is the entry the sandbox binary needs — it // must survive even though it matches the same pattern. -TEST(PlatformEnv, PrependPathListKeepsExplicitPayloadDirs) { +UTest prepend_path_list_keeps_explicit_payload_dirs{"PlatformEnv/PrependPathListKeepsExplicitPayloadDirs", [] { ScopedVar ld("LD_LIBRARY_PATH", "/home/u/.mcpp/registry/data/xpkgs/xim-x-glibc/2.39/lib64"); std::vector dirs{ "/home/u/.mcpp/registry/data/xpkgs/xim-x-glibc/2.42/lib64"}; // Explicit dir kept, inherited (older, mismatched) entry dropped. - EXPECT_EQ(mcpp::runtime::launch_env::prepend_path_list("LD_LIBRARY_PATH", dirs), - host("/home/u/.mcpp/registry/data/xpkgs/xim-x-glibc/2.42/lib64")); -} + expect(eq(mcpp::runtime::launch_env::prepend_path_list("LD_LIBRARY_PATH", dirs), host("/home/u/.mcpp/registry/data/xpkgs/xim-x-glibc/2.42/lib64"))); +}}; // PATH is not a loader search path: leave its inherited value alone. -TEST(PlatformEnv, PrependPathListLeavesPathUntouched) { +UTest prepend_path_list_leaves_path_untouched{"PlatformEnv/PrependPathListLeavesPathUntouched", [] { auto inherited = join({"/bin", "/x/xpkgs/xim-x-glibc/2.39/lib64"}); ScopedVar path("PATH", inherited.c_str()); std::vector dirs{"/tools/bin"}; - EXPECT_EQ(mcpp::runtime::launch_env::prepend_path_list("PATH", dirs), - join({host("/tools/bin"), "/bin", "/x/xpkgs/xim-x-glibc/2.39/lib64"})); -} + expect(eq(mcpp::runtime::launch_env::prepend_path_list("PATH", dirs), join({host("/tools/bin"), "/bin", "/x/xpkgs/xim-x-glibc/2.39/lib64"}))); +}}; -TEST(PlatformEnv, PrependPathListWithNoInheritedValueIsJustTheDirs) { +UTest prepend_path_list_with_no_inherited_value_is_just_the_dirs{"PlatformEnv/PrependPathListWithNoInheritedValueIsJustTheDirs", [] { ScopedVar ld("LD_LIBRARY_PATH", nullptr); std::vector dirs{"/a", "/b"}; - EXPECT_EQ(mcpp::runtime::launch_env::prepend_path_list("LD_LIBRARY_PATH", dirs), - join({host("/a"), host("/b")})); -} + expect(eq(mcpp::runtime::launch_env::prepend_path_list("LD_LIBRARY_PATH", dirs), join({host("/a"), host("/b")}))); +}}; diff --git a/tests/unit/test_platform_fs.cpp b/tests/unit/test_platform_fs.cpp index 14386994a..b02916c22 100644 --- a/tests/unit/test_platform_fs.cpp +++ b/tests/unit/test_platform_fs.cpp @@ -1,11 +1,13 @@ -#include import std; +import mcpplibs.mtest; import mcpplibs.os.platform; import mcpplibs.os.fs; import mcpp.invocation; import mcpp.home.files; +using namespace mcpplibs::mtest; + namespace { struct TempDir { @@ -31,7 +33,7 @@ std::string read_file(const std::filesystem::path& path) { return {std::istreambuf_iterator(in), {}}; } -TEST(PlatformFs, ReplaceFileAtomicallyReplacesExistingFile) { +UTest replace_file_atomically_replaces_existing_file{"PlatformFs/ReplaceFileAtomicallyReplacesExistingFile", [] { TempDir temp; auto source = temp.path / "new.tmp"; auto destination = temp.path / "compile_commands.json"; @@ -39,23 +41,23 @@ TEST(PlatformFs, ReplaceFileAtomicallyReplacesExistingFile) { write_file(destination, "old"); std::error_code ec; - EXPECT_TRUE(mcpp::home::replace_file(source, destination, ec)) + expect(bool(mcpp::home::replace_file(source, destination, ec))) << ec.message(); - EXPECT_FALSE(std::filesystem::exists(source)); - EXPECT_EQ(read_file(destination), "new"); -} + expect(!std::filesystem::exists(source)); + expect(eq(read_file(destination), "new")); +}}; -TEST(PlatformFs, ReplaceFailureKeepsExistingDestination) { +UTest replace_failure_keeps_existing_destination{"PlatformFs/ReplaceFailureKeepsExistingDestination", [] { TempDir temp; auto destination = temp.path / "compile_commands.json"; write_file(destination, "last-known-good"); std::error_code ec; - EXPECT_FALSE(mcpp::home::replace_file( + expect(!mcpp::home::replace_file( temp.path / "missing.tmp", destination, ec)); - EXPECT_TRUE(ec); - EXPECT_EQ(read_file(destination), "last-known-good"); -} + expect(bool(ec)); + expect(eq(read_file(destination), "last-known-good")); +}}; } // namespace @@ -63,69 +65,65 @@ TEST(PlatformFs, ReplaceFailureKeepsExistingDestination) { // Outside Windows there is no path limit of this kind, so the helper is the // identity: a relative path stays relative and nothing is spelled differently. -TEST(PlatformFs, ExtendedLengthIsTheIdentityOutsideWindows) { +UTest extended_length_is_the_identity_outside_windows{"PlatformFs/ExtendedLengthIsTheIdentityOutsideWindows", [] { if constexpr (mcpplibs::os::is_windows) { - GTEST_SKIP() << "the Windows branch is covered by the spelling tests"; + skip("the Windows branch is covered by the spelling tests"); } else { const std::filesystem::path rel{"obj/pkg/__pkg/dep/a.cpp.ddi"}; - EXPECT_EQ(mcpplibs::os::fs::extended(rel), rel); + expect(eq(mcpplibs::os::fs::extended(rel), rel)); const std::filesystem::path abs{"/tmp/x/../y"}; - EXPECT_EQ(mcpplibs::os::fs::extended(abs), abs); - EXPECT_TRUE(mcpplibs::os::fs::extended({}).empty()); + expect(eq(mcpplibs::os::fs::extended(abs), abs)); + expect(mcpplibs::os::fs::extended({}).empty()); } -} +}}; // The Windows rule, as a pure function, so every host tests it. -TEST(PlatformFs, WindowsExtendedLengthSpellsADrivePath) { +UTest windows_extended_length_spells_a_drive_path{"PlatformFs/WindowsExtendedLengthSpellsADrivePath", [] { using mcpplibs::os::fs::windows_extended; - EXPECT_EQ(mcpplibs::os::fs::windows_extended("C:/Users/runner/.mcpp/obj/a.ddi"), - "\\\\?\\C:\\Users\\runner\\.mcpp\\obj\\a.ddi"); + expect(eq(mcpplibs::os::fs::windows_extended("C:/Users/runner/.mcpp/obj/a.ddi"), "\\\\?\\C:\\Users\\runner\\.mcpp\\obj\\a.ddi")); // `.` and `..` are resolved lexically, because the prefix turns their // processing off; `..` does not climb above the drive. - EXPECT_EQ(mcpplibs::os::fs::windows_extended("C:\\a\\.\\b\\..\\c\\"), - "\\\\?\\C:\\a\\c"); - EXPECT_EQ(mcpplibs::os::fs::windows_extended("C:/../../x"), "\\\\?\\C:\\x"); - EXPECT_EQ(mcpplibs::os::fs::windows_extended("D:/"), "\\\\?\\D:\\"); -} + expect(eq(mcpplibs::os::fs::windows_extended("C:\\a\\.\\b\\..\\c\\"), "\\\\?\\C:\\a\\c")); + expect(eq(mcpplibs::os::fs::windows_extended("C:/../../x"), "\\\\?\\C:\\x")); + expect(eq(mcpplibs::os::fs::windows_extended("D:/"), "\\\\?\\D:\\")); +}}; -TEST(PlatformFs, WindowsExtendedLengthSpellsAUncPath) { +UTest windows_extended_length_spells_a_unc_path{"PlatformFs/WindowsExtendedLengthSpellsAUncPath", [] { using mcpplibs::os::fs::windows_extended; - EXPECT_EQ(mcpplibs::os::fs::windows_extended("//server/share/dir/../f.o"), - "\\\\?\\UNC\\server\\share\\f.o"); + expect(eq(mcpplibs::os::fs::windows_extended("//server/share/dir/../f.o"), "\\\\?\\UNC\\server\\share\\f.o")); // `..` does not climb above the share. - EXPECT_EQ(mcpplibs::os::fs::windows_extended("\\\\server\\share\\..\\..\\f"), - "\\\\?\\UNC\\server\\share\\f"); -} + expect(eq(mcpplibs::os::fs::windows_extended("\\\\server\\share\\..\\..\\f"), "\\\\?\\UNC\\server\\share\\f")); +}}; -TEST(PlatformFs, WindowsExtendedLengthLeavesPrefixedAndRelativePathsAlone) { +UTest windows_extended_length_leaves_prefixed_and_relative_paths_alone{"PlatformFs/WindowsExtendedLengthLeavesPrefixedAndRelativePathsAlone", [] { using mcpplibs::os::fs::windows_extended; // Idempotent: a path handed on to a child process is converted again there. - EXPECT_EQ(mcpplibs::os::fs::windows_extended("\\\\?\\C:\\a\\b"), "\\\\?\\C:\\a\\b"); - EXPECT_EQ(mcpplibs::os::fs::windows_extended("\\\\.\\pipe\\x"), "\\\\.\\pipe\\x"); - EXPECT_EQ(mcpplibs::os::fs::windows_extended("obj/a.ddi"), "obj/a.ddi"); - EXPECT_EQ(mcpplibs::os::fs::windows_extended(""), ""); -} + expect(eq(mcpplibs::os::fs::windows_extended("\\\\?\\C:\\a\\b"), "\\\\?\\C:\\a\\b")); + expect(eq(mcpplibs::os::fs::windows_extended("\\\\.\\pipe\\x"), "\\\\.\\pipe\\x")); + expect(eq(mcpplibs::os::fs::windows_extended("obj/a.ddi"), "obj/a.ddi")); + expect(eq(mcpplibs::os::fs::windows_extended(""), "")); +}}; // A NAME THE SHELL ANSWERS FOR ITSELF still resolves to the program on PATH. // `command -v true` prints `true`, because a shell runs its own builtin, and // the path is then found by walking PATH (mcpp#755: a bare-name payload // override of such a name was refused as "not found on PATH" on a machine // carrying /usr/bin/true). -TEST(PlatformFs, WhichResolvesANameThatIsAlsoAShellBuiltin) { +UTest which_resolves_a_name_that_is_also_a_shell_builtin{"PlatformFs/WhichResolvesANameThatIsAlsoAShellBuiltin", [] { #if defined(_WIN32) - GTEST_SKIP() << "`where` reports programs only; the shell answers nothing"; + skip("`where` reports programs only; the shell answers nothing"); #else std::error_code ec; if (!std::filesystem::is_regular_file("/usr/bin/true", ec) && !std::filesystem::is_regular_file("/bin/true", ec)) - GTEST_SKIP() << "this host has no `true` program to find"; + skip("this host has no `true` program to find"); const auto found = mcpp::invocation::which("true"); - ASSERT_TRUE(found.has_value()); - EXPECT_EQ(found->filename(), "true"); - EXPECT_TRUE(std::filesystem::is_regular_file(*found, ec)) << found->string(); + expect(fatal(found.has_value())); + expect(eq(found->filename(), "true")); + if (auto&& failed = expect(std::filesystem::is_regular_file(*found, ec)); !failed) failed << found->string(); #endif -} +}}; -TEST(PlatformFs, WhichStillAnswersNothingForANameNoHostHas) { - EXPECT_FALSE(mcpp::invocation::which("mcpp-no-such-program-7c25d9f6").has_value()); -} +UTest which_still_answers_nothing_for_a_name_no_host_has{"PlatformFs/WhichStillAnswersNothingForANameNoHostHas", [] { + expect(!(mcpp::invocation::which("mcpp-no-such-program-7c25d9f6").has_value())); +}}; diff --git a/tests/unit/test_platform_terminal.cpp b/tests/unit/test_platform_terminal.cpp index 9dc788b01..d4fe131ed 100644 --- a/tests/unit/test_platform_terminal.cpp +++ b/tests/unit/test_platform_terminal.cpp @@ -1,8 +1,10 @@ -#include import std; +import mcpplibs.mtest; import mcpp.ui.terminal; +using namespace mcpplibs::mtest; + // The keys a game reads from a POSIX terminal // (.agents/docs/2026-09-30-build-output-refinement-design.md, §5.14): the // decoder turns the bytes the terminal sent into keys, skips a sequence for @@ -11,35 +13,35 @@ import mcpp.ui.terminal; using mcpp::ui::terminal::Key; using mcpp::ui::terminal::decode_keys; -TEST(TerminalKeys, ArrowsInBothModesAndTheSecondSet) { +UTest arrows_in_both_modes_and_the_second_set{"TerminalKeys/ArrowsInBothModesAndTheSecondSet", [] { std::string in = "\x1b[A\x1b[B\x1bOC\x1bOD wasd"; const auto keys = decode_keys(in); const std::vector want = {Key::Up, Key::Down, Key::Right, Key::Left, Key::Space, Key::Up, Key::Left, Key::Down, Key::Right}; - EXPECT_EQ(keys, want); - EXPECT_TRUE(in.empty()); -} + expect(eq(keys, want)); + expect(in.empty()); +}}; -TEST(TerminalKeys, AModifiedArrowIsOneKeyAndOtherSequencesAreSkippedWhole) { +UTest a_modified_arrow_is_one_key_and_other_sequences_are_skipped_whole{"TerminalKeys/AModifiedArrowIsOneKeyAndOtherSequencesAreSkippedWhole", [] { // Ctrl-Up, then F5 (`ESC [ 1 5 ~`), then Home in application mode. std::string in = "\x1b[1;5A\x1b[15~\x1bOH"; const auto keys = decode_keys(in); const std::vector want = {Key::Up}; - EXPECT_EQ(keys, want) << "a parameter byte or a final byte was read as a key"; - EXPECT_TRUE(in.empty()); -} + expect(eq(keys, want)) << "a parameter byte or a final byte was read as a key"; + expect(in.empty()); +}}; -TEST(TerminalKeys, AnIncompleteSequenceWaitsForTheNextRead) { +UTest an_incomplete_sequence_waits_for_the_next_read{"TerminalKeys/AnIncompleteSequenceWaitsForTheNextRead", [] { std::string in = "d\x1b[1;"; - EXPECT_EQ(decode_keys(in), std::vector{Key::Right}); - EXPECT_EQ(in, "\x1b[1;"); + expect(eq(decode_keys(in), std::vector{Key::Right})); + expect(eq(in, "\x1b[1;")); in += "2B"; - EXPECT_EQ(decode_keys(in), std::vector{Key::Down}); - EXPECT_TRUE(in.empty()); + expect(eq(decode_keys(in), std::vector{Key::Down})); + expect(in.empty()); std::string esc = "\x1b"; - EXPECT_TRUE(decode_keys(esc).empty()); - EXPECT_EQ(esc, "\x1b"); + expect(decode_keys(esc).empty()); + expect(eq(esc, "\x1b")); esc += "w"; // escape on its own, then a key - EXPECT_EQ(decode_keys(esc), std::vector{Key::Up}); -} + expect(eq(decode_keys(esc), std::vector{Key::Up})); +}}; diff --git a/tests/unit/test_pm_bare_name_window.cpp b/tests/unit/test_pm_bare_name_window.cpp index b9b48841a..a640f4b26 100644 --- a/tests/unit/test_pm_bare_name_window.cpp +++ b/tests/unit/test_pm_bare_name_window.cpp @@ -1,37 +1,39 @@ -#include import std; +import mcpplibs.mtest; import mcpp.pm.dep_spec; import mcpp.pm.dependency_selector; +using namespace mcpplibs::mtest; + // The one-release exit ramp for namespace-omitted selectors. Kept in its // own file so removing the window in 2026.9 is one deletion, not an // archaeology exercise across the resolver tests. -TEST(PmBareNameWindow, LegacyRungsExistOnlyForAnOmittedNamespace) { +UTest legacy_rungs_exist_only_for_an_omitted_namespace{"PmBareNameWindow/LegacyRungsExistOnlyForAnOmittedNamespace", [] { // Eligible: the author omitted the namespace, so `mcpplibs` was supplied. auto omitted = mcpp::pm::legacy_bare_candidates( mcpp::pm::DependencyCoordinate{ .namespace_ = std::string(mcpp::pm::kDefaultNamespace), .shortName = "gtest", }); - ASSERT_EQ(omitted.size(), 2u); - EXPECT_EQ(omitted[0].namespace_, mcpp::pm::kCompatNamespace); - EXPECT_EQ(omitted[0].shortName, "gtest"); + expect(fatal(eq(omitted.size(), 2u))); + expect(eq(omitted[0].namespace_, mcpp::pm::kCompatNamespace)); + expect(eq(omitted[0].shortName, "gtest")); // The namespace-less discovery rung, for upstream descriptors that declare // no namespace at all. Callers must still reject a descriptor that DOES // declare one, or this is the cross-namespace wildcard #278 removed. - EXPECT_TRUE(omitted[1].namespace_.empty()); - EXPECT_EQ(omitted[1].shortName, "gtest"); + expect(omitted[1].namespace_.empty()); + expect(eq(omitted[1].shortName, "gtest")); // Not eligible: every one of these STATES an identity. A stated identity // that misses is a miss — the window exists for spellings that predate the // rule, not for ones that break it. for (std::string_view ns : { "compat", "acme", "mcpplibs.capi" }) { - EXPECT_TRUE(mcpp::pm::legacy_bare_candidates( + expect(mcpp::pm::legacy_bare_candidates( mcpp::pm::DependencyCoordinate{ .namespace_ = std::string(ns), .shortName = "gtest", }).empty()) << ns; } -} +}}; diff --git a/tests/unit/test_pm_compat.cpp b/tests/unit/test_pm_compat.cpp index 7e6b172b2..ee1db9923 100644 --- a/tests/unit/test_pm_compat.cpp +++ b/tests/unit/test_pm_compat.cpp @@ -1,117 +1,115 @@ -#include import std; +import mcpplibs.mtest; import mcpp.pm.compat; import mcpp.pm.dependency_selector; -TEST(PmCompat, InstallDirCandidatesIncludeNestedNamespaceFallback) { +using namespace mcpplibs::mtest; + +UTest install_dir_candidates_include_nested_namespace_fallback{"PmCompat/InstallDirCandidatesIncludeNestedNamespaceFallback", [] { auto candidates = mcpp::pm::compat::install_dir_candidates( "mcpplibs", "capi.lua", "mcpplibs"); - EXPECT_NE( - std::find(candidates.begin(), candidates.end(), - "mcpplibs.capi-x-mcpplibs.capi.lua"), - candidates.end()); -} + expect(ne(std::find(candidates.begin(), candidates.end(), + "mcpplibs.capi-x-mcpplibs.capi.lua"), candidates.end())); +}}; -TEST(PmCompat, NormalizeNestedNamespacePreservesQualifiedName) { +UTest normalize_nested_namespace_preserves_qualified_name{"PmCompat/NormalizeNestedNamespacePreservesQualifiedName", [] { std::string ns = "mcpplibs"; std::string shortName = "capi.lua"; mcpp::pm::compat::normalize_nested_namespace(ns, shortName, /*legacyDottedKey=*/true); - EXPECT_EQ(ns, "mcpplibs.capi"); - EXPECT_EQ(shortName, "lua"); - EXPECT_EQ(mcpp::pm::compat::qualified_name(ns, shortName), - "mcpplibs.capi.lua"); -} + expect(eq(ns, "mcpplibs.capi")); + expect(eq(shortName, "lua")); + expect(eq(mcpp::pm::compat::qualified_name(ns, shortName), "mcpplibs.capi.lua")); +}}; -TEST(PmCompat, SplitLegacyDependencyKeyMarksDottedKeyAsCompat) { +UTest split_legacy_dependency_key_marks_dotted_key_as_compat{"PmCompat/SplitLegacyDependencyKeyMarksDottedKeyAsCompat", [] { auto key = mcpp::pm::compat::split_legacy_dependency_key( "mcpplibs.capi.lua"); - EXPECT_EQ(key.namespace_, "mcpplibs"); - EXPECT_EQ(key.shortName, "capi.lua"); - EXPECT_TRUE(key.legacyDottedKey); -} + expect(eq(key.namespace_, "mcpplibs")); + expect(eq(key.shortName, "capi.lua")); + expect(bool(key.legacyDottedKey)); +}}; -TEST(PmCompat, NormalizeNestedNamespaceSkipsCanonicalNamespacedDeps) { +UTest normalize_nested_namespace_skips_canonical_namespaced_deps{"PmCompat/NormalizeNestedNamespaceSkipsCanonicalNamespacedDeps", [] { std::string ns = "mcpplibs.capi"; std::string shortName = "lua.extra"; mcpp::pm::compat::normalize_nested_namespace(ns, shortName, /*legacyDottedKey=*/false); - EXPECT_EQ(ns, "mcpplibs.capi"); - EXPECT_EQ(shortName, "lua.extra"); -} + expect(eq(ns, "mcpplibs.capi")); + expect(eq(shortName, "lua.extra")); +}}; -TEST(DependencySelector, DottedSelectorIsOneExactNamespace) { +UTest dotted_selector_is_one_exact_namespace{"DependencySelector/DottedSelectorIsOneExactNamespace", [] { auto selector = mcpp::pm::resolve_dependency_selector( "imgui.backend.glfw_opengl3"); - EXPECT_EQ(selector.stableMapKey, "imgui.backend.glfw_opengl3"); - ASSERT_EQ(selector.candidates.size(), 1u); - EXPECT_EQ(selector.candidates[0].namespace_, "imgui.backend"); - EXPECT_EQ(selector.candidates[0].shortName, "glfw_opengl3"); -} + expect(eq(selector.stableMapKey, "imgui.backend.glfw_opengl3")); + expect(fatal(eq(selector.candidates.size(), 1u))); + expect(eq(selector.candidates[0].namespace_, "imgui.backend")); + expect(eq(selector.candidates[0].shortName, "glfw_opengl3")); +}}; -TEST(DependencySelector, BareSelectorUsesOnlyDefaultNamespace) { +UTest bare_selector_uses_only_default_namespace{"DependencySelector/BareSelectorUsesOnlyDefaultNamespace", [] { auto selector = mcpp::pm::resolve_dependency_selector("imgui"); - EXPECT_EQ(selector.stableMapKey, "imgui"); - ASSERT_EQ(selector.candidates.size(), 1u); - EXPECT_EQ(selector.candidates[0].namespace_, "mcpplibs"); - EXPECT_EQ(selector.candidates[0].shortName, "imgui"); -} + expect(eq(selector.stableMapKey, "imgui")); + expect(fatal(eq(selector.candidates.size(), 1u))); + expect(eq(selector.candidates[0].namespace_, "mcpplibs")); + expect(eq(selector.candidates[0].shortName, "imgui")); +}}; -TEST(DependencySelector, ExplicitMcpplibsPrefixDoesNotAddPeerFallback) { +UTest explicit_mcpplibs_prefix_does_not_add_peer_fallback{"DependencySelector/ExplicitMcpplibsPrefixDoesNotAddPeerFallback", [] { auto selector = mcpp::pm::resolve_dependency_selector( "mcpplibs.capi.lua"); - EXPECT_EQ(selector.stableMapKey, "mcpplibs.capi.lua"); - ASSERT_EQ(selector.candidates.size(), 1u); - EXPECT_EQ(selector.candidates[0].namespace_, "mcpplibs.capi"); - EXPECT_EQ(selector.candidates[0].shortName, "lua"); -} + expect(eq(selector.stableMapKey, "mcpplibs.capi.lua")); + expect(fatal(eq(selector.candidates.size(), 1u))); + expect(eq(selector.candidates[0].namespace_, "mcpplibs.capi")); + expect(eq(selector.candidates[0].shortName, "lua")); +}}; -TEST(DependencySelector, ExplicitRootSelectorHasOnlyThatRoot) { +UTest explicit_root_selector_has_only_that_root{"DependencySelector/ExplicitRootSelectorHasOnlyThatRoot", [] { auto selector = mcpp::pm::make_direct_dependency_selector( "compat", "gtest", "compat.gtest"); - EXPECT_EQ(selector.stableMapKey, "compat.gtest"); - ASSERT_EQ(selector.candidates.size(), 1u); - EXPECT_EQ(selector.candidates[0].namespace_, "compat"); - EXPECT_EQ(selector.candidates[0].shortName, "gtest"); -} + expect(eq(selector.stableMapKey, "compat.gtest")); + expect(fatal(eq(selector.candidates.size(), 1u))); + expect(eq(selector.candidates[0].namespace_, "compat")); + expect(eq(selector.candidates[0].shortName, "gtest")); +}}; -TEST(DependencySelector, SharedParserNormalizesBareAndDottedSelectors) { +UTest shared_parser_normalizes_bare_and_dotted_selectors{"DependencySelector/SharedParserNormalizesBareAndDottedSelectors", [] { auto bare = mcpp::pm::parse_package_selector("lua"); - ASSERT_TRUE(bare.has_value()); - EXPECT_FALSE(bare->namespace_.has_value()); - EXPECT_EQ(bare->name, "lua"); - EXPECT_EQ(bare->spelling, "lua"); + expect(fatal(bare.has_value())); + expect(!(bare->namespace_.has_value())); + expect(eq(bare->name, "lua")); + expect(eq(bare->spelling, "lua")); auto bareCoord = mcpp::pm::normalize_package_selector(*bare); - EXPECT_EQ(bareCoord.namespace_, "mcpplibs"); - EXPECT_EQ(bareCoord.shortName, "lua"); - EXPECT_EQ(mcpp::pm::format_package_selector(bareCoord), "lua"); + expect(eq(bareCoord.namespace_, "mcpplibs")); + expect(eq(bareCoord.shortName, "lua")); + expect(eq(mcpp::pm::format_package_selector(bareCoord), "lua")); auto nested = mcpp::pm::parse_package_selector("mcpplibs.capi.lua"); - ASSERT_TRUE(nested.has_value()); - ASSERT_TRUE(nested->namespace_.has_value()); - EXPECT_EQ(*nested->namespace_, "mcpplibs.capi"); - EXPECT_EQ(nested->name, "lua"); + expect(fatal(nested.has_value())); + expect(fatal(nested->namespace_.has_value())); + expect(eq(*nested->namespace_, "mcpplibs.capi")); + expect(eq(nested->name, "lua")); auto nestedCoord = mcpp::pm::normalize_package_selector(*nested); - EXPECT_EQ(nestedCoord.namespace_, "mcpplibs.capi"); - EXPECT_EQ(nestedCoord.shortName, "lua"); - EXPECT_EQ(mcpp::pm::format_package_selector(nestedCoord), - "mcpplibs.capi.lua"); -} + expect(eq(nestedCoord.namespace_, "mcpplibs.capi")); + expect(eq(nestedCoord.shortName, "lua")); + expect(eq(mcpp::pm::format_package_selector(nestedCoord), "mcpplibs.capi.lua")); +}}; -TEST(DependencySelector, SharedParserRejectsAmbiguousOrUnsafeSegments) { +UTest shared_parser_rejects_ambiguous_or_unsafe_segments{"DependencySelector/SharedParserRejectsAmbiguousOrUnsafeSegments", [] { const std::vector invalid{ "", ".imgui", @@ -130,84 +128,84 @@ TEST(DependencySelector, SharedParserRejectsAmbiguousOrUnsafeSegments) { }; for (auto const& spelling : invalid) { auto parsed = mcpp::pm::parse_package_selector(spelling); - EXPECT_FALSE(parsed.has_value()) << spelling; + expect(!parsed.has_value()) << spelling; if (!parsed) - EXPECT_FALSE(parsed.error().message.empty()) << spelling; + expect(!(parsed.error().message.empty())) << spelling; } -} +}}; -TEST(DependencySelector, LegacyDottedPrimaryIsExplicitAndNeverImplicitlyUsed) { +UTest legacy_dotted_primary_is_explicit_and_never_implicitly_used{"DependencySelector/LegacyDottedPrimaryIsExplicitAndNeverImplicitlyUsed", [] { auto capi = mcpp::pm::parse_package_selector("capi.lua"); - ASSERT_TRUE(capi.has_value()); + expect(fatal(capi.has_value())); auto legacy = mcpp::pm::legacy_prefixed_coordinate( mcpp::pm::normalize_package_selector(*capi)); - ASSERT_TRUE(legacy.has_value()); - EXPECT_EQ(legacy->namespace_, "mcpplibs.capi"); - EXPECT_EQ(legacy->shortName, "lua"); + expect(fatal(legacy.has_value())); + expect(eq(legacy->namespace_, "mcpplibs.capi")); + expect(eq(legacy->shortName, "lua")); auto explicitDefault = mcpp::pm::parse_package_selector( "mcpplibs.capi.lua"); - ASSERT_TRUE(explicitDefault.has_value()); - EXPECT_FALSE(mcpp::pm::legacy_prefixed_coordinate( - mcpp::pm::normalize_package_selector(*explicitDefault)).has_value()); + expect(fatal(explicitDefault.has_value())); + expect(!(mcpp::pm::legacy_prefixed_coordinate( + mcpp::pm::normalize_package_selector(*explicitDefault)).has_value())); auto bare = mcpp::pm::parse_package_selector("lua"); - ASSERT_TRUE(bare.has_value()); - EXPECT_FALSE(mcpp::pm::legacy_prefixed_coordinate( - mcpp::pm::normalize_package_selector(*bare)).has_value()); -} + expect(fatal(bare.has_value())); + expect(!(mcpp::pm::legacy_prefixed_coordinate( + mcpp::pm::normalize_package_selector(*bare)).has_value())); +}}; // ─── descriptor_coordinates (package-template fetch) ──────────────── -TEST(PmCompat, DescriptorCoordinatesLegacyEmbeddedNamespace) { +UTest descriptor_coordinates_legacy_embedded_namespace{"PmCompat/DescriptorCoordinatesLegacyEmbeddedNamespace", [] { // pkgs/l/llmapi.lua: namespace = "mcpplibs", name = "mcpplibs.llmapi" auto r = mcpp::pm::compat::descriptor_coordinates( "llmapi", "mcpplibs", "mcpplibs.llmapi"); - EXPECT_EQ(r.namespace_, "mcpplibs"); - EXPECT_EQ(r.shortName, "llmapi"); - EXPECT_TRUE(r.usedLegacySplit); -} + expect(eq(r.namespace_, "mcpplibs")); + expect(eq(r.shortName, "llmapi")); + expect(bool(r.usedLegacySplit)); +}}; -TEST(PmCompat, DescriptorCoordinatesCanonicalNamespaceField) { +UTest descriptor_coordinates_canonical_namespace_field{"PmCompat/DescriptorCoordinatesCanonicalNamespaceField", [] { auto r = mcpp::pm::compat::descriptor_coordinates( "llmapi", "mcpplibs", "llmapi"); - EXPECT_EQ(r.namespace_, "mcpplibs"); - EXPECT_EQ(r.shortName, "llmapi"); - EXPECT_FALSE(r.usedLegacySplit); -} + expect(eq(r.namespace_, "mcpplibs")); + expect(eq(r.shortName, "llmapi")); + expect(!r.usedLegacySplit); +}}; -TEST(PmCompat, DescriptorCoordinatesRootPackageStaysInRoot) { +UTest descriptor_coordinates_root_package_stays_in_root{"PmCompat/DescriptorCoordinatesRootPackageStaysInRoot", [] { // pkgs/i/imgui.lua: namespace = "", name = "imgui" — must NOT be // promoted to the default namespace (it installs by its bare name). auto r = mcpp::pm::compat::descriptor_coordinates("imgui", "", "imgui"); - EXPECT_EQ(r.namespace_, ""); - EXPECT_EQ(r.shortName, "imgui"); - EXPECT_FALSE(r.usedLegacySplit); -} + expect(eq(r.namespace_, "")); + expect(eq(r.shortName, "imgui")); + expect(!r.usedLegacySplit); +}}; -TEST(PmCompat, DescriptorCoordinatesLegacyDottedNameWithoutNamespace) { +UTest descriptor_coordinates_legacy_dotted_name_without_namespace{"PmCompat/DescriptorCoordinatesLegacyDottedNameWithoutNamespace", [] { auto r = mcpp::pm::compat::descriptor_coordinates( "tinyhttps", "", "mcpplibs.tinyhttps"); - EXPECT_EQ(r.namespace_, "mcpplibs"); - EXPECT_EQ(r.shortName, "tinyhttps"); - EXPECT_TRUE(r.usedLegacySplit); -} + expect(eq(r.namespace_, "mcpplibs")); + expect(eq(r.shortName, "tinyhttps")); + expect(bool(r.usedLegacySplit)); +}}; -TEST(PmCompat, DescriptorCoordinatesFallsBackToSpecWhenNameMissing) { +UTest descriptor_coordinates_falls_back_to_spec_when_name_missing{"PmCompat/DescriptorCoordinatesFallsBackToSpecWhenNameMissing", [] { auto r = mcpp::pm::compat::descriptor_coordinates("imgui", "", ""); - EXPECT_EQ(r.namespace_, ""); - EXPECT_EQ(r.shortName, "imgui"); -} + expect(eq(r.namespace_, "")); + expect(eq(r.shortName, "imgui")); +}}; -TEST(PmCompat, DescriptorCoordinatesCompatNamespace) { +UTest descriptor_coordinates_compat_namespace{"PmCompat/DescriptorCoordinatesCompatNamespace", [] { auto r = mcpp::pm::compat::descriptor_coordinates( "mbedtls", "compat", "compat.mbedtls"); - EXPECT_EQ(r.namespace_, "compat"); - EXPECT_EQ(r.shortName, "mbedtls"); -} + expect(eq(r.namespace_, "compat")); + expect(eq(r.shortName, "mbedtls")); +}}; diff --git a/tests/unit/test_pm_index_refresh.cpp b/tests/unit/test_pm_index_refresh.cpp index 69668bec4..122976fec 100644 --- a/tests/unit/test_pm_index_refresh.cpp +++ b/tests/unit/test_pm_index_refresh.cpp @@ -12,9 +12,9 @@ // host publishes no 1.2.3" is not "1.2.3 does not exist". // ColdStartIgnoresDebounce — an absent index has nothing to debounce against. -#include import std; +import mcpplibs.mtest; import mcpp.config; import mcpp.pm.dep_spec; import mcpp.pm.index_refresh; @@ -23,6 +23,8 @@ import mcpp.pm.index_spec; import mcpp.toolchain.platform; import mcpp.provider.xlings; +using namespace mcpplibs::mtest; + namespace { using mcpp::pm::RefreshReason; @@ -139,7 +141,7 @@ constexpr std::string_view kOneVersion = R"(["1.2.0"] = { url = "u", sha256 = "s // ─── Rows that never reach the filesystem ──────────────────────────────── -TEST(PmIndexRefresh, PathAndGitDependenciesNeverRefresh) { +UTest path_and_git_dependencies_never_refresh{"PmIndexRefresh/PathAndGitDependenciesNeverRefresh", [] { FakeRegistry reg("srcdeps"); mcpp::pm::IndexMap indices; auto cfg = reg.config(); @@ -147,14 +149,14 @@ TEST(PmIndexRefresh, PathAndGitDependenciesNeverRefresh) { mcpp::pm::DependencySpec pathDep; pathDep.path = "../sibling"; - EXPECT_EQ(decide(route, pathDep, reg.env()), RefreshReason::None); + expect(eq(decide(route, pathDep, reg.env()), RefreshReason::None)); mcpp::pm::DependencySpec gitDep; gitDep.git = "https://example.invalid/x.git"; - EXPECT_EQ(decide(route, gitDep, reg.env()), RefreshReason::None); -} + expect(eq(decide(route, gitDep, reg.env()), RefreshReason::None)); +}}; -TEST(PmIndexRefresh, OfflineSuppressesEvenAGenuineMiss) { +UTest offline_suppresses_even_a_genuine_miss{"PmIndexRefresh/OfflineSuppressesEvenAGenuineMiss", [] { FakeRegistry reg("offline"); reg.mark_refreshed(std::chrono::hours{99}); mcpp::pm::IndexMap indices; @@ -164,16 +166,16 @@ TEST(PmIndexRefresh, OfflineSuppressesEvenAGenuineMiss) { auto spec = version_dep("mcpplibs", "absent", "1.0.0"); mcpp::pm::RefreshPolicy offline; offline.offline = true; - EXPECT_EQ(decide(route, spec, reg.env(), offline), RefreshReason::SuppressedOffline); - EXPECT_FALSE(refreshes(route, spec, reg.env(), offline)); -} + expect(eq(decide(route, spec, reg.env(), offline), RefreshReason::SuppressedOffline)); + expect(!(refreshes(route, spec, reg.env(), offline))); +}}; // mcpp-community/mcpp#648. `ftxui = "6.1.9"` omits the namespace, so the // exact coordinate is (mcpplibs, ftxui), which the index does not carry; the // resolver then reaches compat.ftxui through the deprecated bare-name rung and // the build succeeds from disk. The decision must walk the same rung, or every // build of such a manifest refreshes the index once the debounce has passed. -TEST(PmIndexRefresh, BareNameResolvedThroughTheLegacyRungIsNotAMiss) { +UTest bare_name_resolved_through_the_legacy_rung_is_not_a_miss{"PmIndexRefresh/BareNameResolvedThroughTheLegacyRungIsNotAMiss", [] { FakeRegistry reg("legacyrung"); reg.publish_namespaced("compat", "ftxui", R"(["6.1.9"] = { url = "u", sha256 = "s" },)"); reg.mark_refreshed(std::chrono::hours{99}); @@ -183,21 +185,21 @@ TEST(PmIndexRefresh, BareNameResolvedThroughTheLegacyRungIsNotAMiss) { auto bare = version_dep("mcpplibs", "ftxui", "6.1.9"); bare.namespaceOmitted = true; - EXPECT_EQ(decide(route, bare, reg.env()), RefreshReason::None); - EXPECT_FALSE(refreshes(route, bare, reg.env())); + expect(eq(decide(route, bare, reg.env()), RefreshReason::None)); + expect(!(refreshes(route, bare, reg.env()))); // A constraint is checked against the descriptor the rung found. auto bareRange = version_dep("mcpplibs", "ftxui", "^7"); bareRange.namespaceOmitted = true; - EXPECT_EQ(decide(route, bareRange, reg.env()), RefreshReason::VersionMiss); + expect(eq(decide(route, bareRange, reg.env()), RefreshReason::VersionMiss)); // The namespace written out states an identity: the rung does not apply, // exactly as in the resolver, and the miss stands. auto stated = version_dep("mcpplibs", "ftxui", "6.1.9"); - EXPECT_EQ(decide(route, stated, reg.env()), RefreshReason::DescriptorMiss); -} + expect(eq(decide(route, stated, reg.env()), RefreshReason::DescriptorMiss)); +}}; -TEST(PmIndexRefresh, OfflineDoesNotMisreportAResolvableDependency) { +UTest offline_does_not_misreport_a_resolvable_dependency{"PmIndexRefresh/OfflineDoesNotMisreportAResolvableDependency", [] { // The opt-outs are applied AFTER the local analysis, so a dependency that // resolved fine is reported as such even offline. Short-circuiting on the // flag first would answer "offline mode" for every dependency — hiding @@ -210,11 +212,10 @@ TEST(PmIndexRefresh, OfflineDoesNotMisreportAResolvableDependency) { mcpp::pm::IndexRoute route{ &indices, "/nowhere", &cfg }; mcpp::pm::RefreshPolicy offline; offline.offline = true; - EXPECT_EQ(decide(route, version_dep("mcpplibs", "widget", "1.2.0"), reg.env(), offline), - RefreshReason::None); -} + expect(eq(decide(route, version_dep("mcpplibs", "widget", "1.2.0"), reg.env(), offline), RefreshReason::None)); +}}; -TEST(PmIndexRefresh, AutoRefreshOptOutSuppressesAGenuineMiss) { +UTest auto_refresh_opt_out_suppresses_a_genuine_miss{"PmIndexRefresh/AutoRefreshOptOutSuppressesAGenuineMiss", [] { FakeRegistry reg("optout"); reg.mark_refreshed(std::chrono::hours{99}); mcpp::pm::IndexMap indices; @@ -224,12 +225,12 @@ TEST(PmIndexRefresh, AutoRefreshOptOutSuppressesAGenuineMiss) { auto spec = version_dep("mcpplibs", "absent", "1.0.0"); mcpp::pm::RefreshPolicy off; off.autoRefresh = false; - EXPECT_EQ(decide(route, spec, reg.env(), off), RefreshReason::SuppressedDisabled); -} + expect(eq(decide(route, spec, reg.env(), off), RefreshReason::SuppressedDisabled)); +}}; // ─── INV-3: a miss must be refutable to mean anything ──────────────────── -TEST(PmIndexRefresh, InconclusiveNamespaceNeverRefreshes) { +UTest inconclusive_namespace_never_refreshes{"PmIndexRefresh/InconclusiveNamespaceNeverRefreshes", [] { FakeRegistry reg("inconclusive"); reg.mark_refreshed(std::chrono::hours{99}); mcpp::pm::IndexMap indices; @@ -241,13 +242,13 @@ TEST(PmIndexRefresh, InconclusiveNamespaceNeverRefreshes) { // so "absent" here is not evidence of anything. for (auto ns : { "xim", "somevendor" }) { auto spec = version_dep(ns, "thing", "1.0.0"); - EXPECT_EQ(decide(route, spec, reg.env()), RefreshReason::SuppressedInconclusive) + expect(eq(decide(route, spec, reg.env()), RefreshReason::SuppressedInconclusive)) << "namespace: " << ns; - EXPECT_FALSE(refreshes(route, spec, reg.env())) << "namespace: " << ns; + expect(!(refreshes(route, spec, reg.env()))) << "namespace: " << ns; } -} +}}; -TEST(PmIndexRefresh, LazyGitIndexNeverRefreshesTheGlobalOne) { +UTest lazy_git_index_never_refreshes_the_global_one{"PmIndexRefresh/LazyGitIndexNeverRefreshesTheGlobalOne", [] { FakeRegistry reg("lazygit"); reg.mark_refreshed(std::chrono::hours{99}); mcpp::pm::IndexMap indices; @@ -257,12 +258,12 @@ TEST(PmIndexRefresh, LazyGitIndexNeverRefreshesTheGlobalOne) { // Served by a custom index; syncing the shared registry would not help. auto spec = version_dep("acme", "widget", "1.0.0"); - EXPECT_EQ(decide(route, spec, reg.env()), RefreshReason::None); -} + expect(eq(decide(route, spec, reg.env()), RefreshReason::None)); +}}; // ─── The three ways local state fails to answer ────────────────────────── -TEST(PmIndexRefresh, PresentDescriptorIsTheSteadyStateAndCostsNothing) { +UTest present_descriptor_is_the_steady_state_and_costs_nothing{"PmIndexRefresh/PresentDescriptorIsTheSteadyStateAndCostsNothing", [] { FakeRegistry reg("steady"); reg.publish("widget", kOneVersion); reg.mark_refreshed(std::chrono::hours{99}); // old, and irrelevant @@ -270,13 +271,11 @@ TEST(PmIndexRefresh, PresentDescriptorIsTheSteadyStateAndCostsNothing) { auto cfg = reg.config(); mcpp::pm::IndexRoute route{ &indices, "/nowhere", &cfg }; - EXPECT_EQ(decide(route, version_dep("mcpplibs", "widget", "1.2.0"), reg.env()), - RefreshReason::None); - EXPECT_EQ(decide(route, version_dep("mcpplibs", "widget", "^1.2"), reg.env()), - RefreshReason::None); -} + expect(eq(decide(route, version_dep("mcpplibs", "widget", "1.2.0"), reg.env()), RefreshReason::None)); + expect(eq(decide(route, version_dep("mcpplibs", "widget", "^1.2"), reg.env()), RefreshReason::None)); +}}; -TEST(PmIndexRefresh, MissingDescriptorTriggersARefresh) { +UTest missing_descriptor_triggers_a_refresh{"PmIndexRefresh/MissingDescriptorTriggersARefresh", [] { FakeRegistry reg("descmiss"); reg.publish("widget", kOneVersion); reg.mark_refreshed(std::chrono::hours{99}); @@ -285,11 +284,11 @@ TEST(PmIndexRefresh, MissingDescriptorTriggersARefresh) { mcpp::pm::IndexRoute route{ &indices, "/nowhere", &cfg }; auto spec = version_dep("mcpplibs", "absent", "1.0.0"); - EXPECT_EQ(decide(route, spec, reg.env()), RefreshReason::DescriptorMiss); - EXPECT_TRUE(refreshes(route, spec, reg.env())); -} + expect(eq(decide(route, spec, reg.env()), RefreshReason::DescriptorMiss)); + expect(bool(refreshes(route, spec, reg.env()))); +}}; -TEST(PmIndexRefresh, UnsatisfiableConstraintTriggersARefresh) { +UTest unsatisfiable_constraint_triggers_a_refresh{"PmIndexRefresh/UnsatisfiableConstraintTriggersARefresh", [] { FakeRegistry reg("vermiss"); reg.publish("widget", kOneVersion); // only 1.2.0 exists reg.mark_refreshed(std::chrono::hours{99}); @@ -298,10 +297,10 @@ TEST(PmIndexRefresh, UnsatisfiableConstraintTriggersARefresh) { mcpp::pm::IndexRoute route{ &indices, "/nowhere", &cfg }; auto spec = version_dep("mcpplibs", "widget", "^9.9"); - EXPECT_EQ(decide(route, spec, reg.env()), RefreshReason::VersionMiss); -} + expect(eq(decide(route, spec, reg.env()), RefreshReason::VersionMiss)); +}}; -TEST(PmIndexRefresh, ExactVersionAbsentIsNotAVersionMiss) { +UTest exact_version_absent_is_not_a_version_miss{"PmIndexRefresh/ExactVersionAbsentIsNotAVersionMiss", [] { FakeRegistry reg("exactver"); reg.publish("widget", kOneVersion); // only 1.2.0 exists reg.mark_refreshed(std::chrono::hours{99}); @@ -313,12 +312,12 @@ TEST(PmIndexRefresh, ExactVersionAbsentIsNotAVersionMiss) { // simply not be built for this host. Refreshing on it would fire on EVERY // build for such a dependency. The install layer covers the real case. auto spec = version_dep("mcpplibs", "widget", "9.9.9"); - EXPECT_EQ(decide(route, spec, reg.env()), RefreshReason::None); -} + expect(eq(decide(route, spec, reg.env()), RefreshReason::None)); +}}; // ─── Debounce ──────────────────────────────────────────────────────────── -TEST(PmIndexRefresh, DebounceSuppressesTheSecondMissInAWindow) { +UTest debounce_suppresses_the_second_miss_in_a_window{"PmIndexRefresh/DebounceSuppressesTheSecondMissInAWindow", [] { FakeRegistry reg("debounce"); reg.publish("widget", kOneVersion); reg.mark_refreshed(std::chrono::seconds{5}); // just synced @@ -327,11 +326,11 @@ TEST(PmIndexRefresh, DebounceSuppressesTheSecondMissInAWindow) { mcpp::pm::IndexRoute route{ &indices, "/nowhere", &cfg }; auto spec = version_dep("mcpplibs", "absent", "1.0.0"); - EXPECT_EQ(decide(route, spec, reg.env()), RefreshReason::SuppressedDebounce); - EXPECT_FALSE(refreshes(route, spec, reg.env())); -} + expect(eq(decide(route, spec, reg.env()), RefreshReason::SuppressedDebounce)); + expect(!(refreshes(route, spec, reg.env()))); +}}; -TEST(PmIndexRefresh, ColdStartIgnoresDebounce) { +UTest cold_start_ignores_debounce{"PmIndexRefresh/ColdStartIgnoresDebounce", [] { FakeRegistry reg("coldstart"); reg.mark_refreshed(std::chrono::seconds{5}); reg.remove_index(); // marker gone with it; no local index at all @@ -340,35 +339,32 @@ TEST(PmIndexRefresh, ColdStartIgnoresDebounce) { mcpp::pm::IndexRoute route{ &indices, "/nowhere", &cfg }; auto spec = version_dep("mcpplibs", "widget", "1.0.0"); - EXPECT_EQ(decide(route, spec, reg.env()), RefreshReason::IndexAbsent); - EXPECT_TRUE(refreshes(route, spec, reg.env())); -} + expect(eq(decide(route, spec, reg.env()), RefreshReason::IndexAbsent)); + expect(bool(refreshes(route, spec, reg.env()))); +}}; // ─── decide_for_miss (the `mcpp add` half) ─────────────────────────────── -TEST(PmIndexRefresh, DecideForMissHonoursTheSameOptOutsAndDebounce) { +UTest decide_for_miss_honours_the_same_opt_outs_and_debounce{"PmIndexRefresh/DecideForMissHonoursTheSameOptOutsAndDebounce", [] { FakeRegistry reg("formiss"); reg.mark_refreshed(std::chrono::hours{99}); auto env = reg.env(); mcpp::pm::RefreshPolicy allowed; - EXPECT_TRUE(mcpp::pm::decide_for_miss(allowed, env, "pkg").shouldRefresh); + expect(bool(mcpp::pm::decide_for_miss(allowed, env, "pkg").shouldRefresh)); mcpp::pm::RefreshPolicy offline; offline.offline = true; - EXPECT_EQ(mcpp::pm::decide_for_miss(offline, env, "pkg").reason, - RefreshReason::SuppressedOffline); + expect(eq(mcpp::pm::decide_for_miss(offline, env, "pkg").reason, RefreshReason::SuppressedOffline)); mcpp::pm::RefreshPolicy disabled; disabled.autoRefresh = false; - EXPECT_EQ(mcpp::pm::decide_for_miss(disabled, env, "pkg").reason, - RefreshReason::SuppressedDisabled); + expect(eq(mcpp::pm::decide_for_miss(disabled, env, "pkg").reason, RefreshReason::SuppressedDisabled)); // Two `mcpp add` typos in a row must not buy two multi-repo syncs. reg.mark_refreshed(std::chrono::seconds{5}); - EXPECT_EQ(mcpp::pm::decide_for_miss(allowed, env, "pkg").reason, - RefreshReason::SuppressedDebounce); -} + expect(eq(mcpp::pm::decide_for_miss(allowed, env, "pkg").reason, RefreshReason::SuppressedDebounce)); +}}; -TEST(PmIndexRefresh, SubjectEchoesWhatTheUserWrote) { +UTest subject_echoes_what_the_user_wrote{"PmIndexRefresh/SubjectEchoesWhatTheUserWrote", [] { FakeRegistry reg("subject"); reg.mark_refreshed(std::chrono::hours{99}); mcpp::pm::IndexMap indices; @@ -382,5 +378,5 @@ TEST(PmIndexRefresh, SubjectEchoesWhatTheUserWrote) { auto d = mcpp::pm::decide_for_dependency( route, "xim.nasm", spec, reg.env(), mcpp::toolchain::platform::TargetPlatform::for_os("linux"), {}); - EXPECT_EQ(d.subject, "xim.nasm@2.16.03"); -} + expect(eq(d.subject, "xim.nasm@2.16.03")); +}}; diff --git a/tests/unit/test_pm_index_route.cpp b/tests/unit/test_pm_index_route.cpp index a7cf67258..f3326b93c 100644 --- a/tests/unit/test_pm_index_route.cpp +++ b/tests/unit/test_pm_index_route.cpp @@ -1,11 +1,13 @@ -#include import std; +import mcpplibs.mtest; import mcpp.pm.dep_spec; import mcpp.pm.dependency_selector; import mcpp.pm.index_route; import mcpp.pm.index_spec; +using namespace mcpplibs::mtest; + namespace { struct LocalIndex { @@ -36,73 +38,72 @@ mcpp::pm::IndexMap local_map(const LocalIndex& idx) { } // namespace -TEST(PmIndexRoute, FindForNsResolvesNestedNamespaceToItsRootIndex) { +UTest find_for_ns_resolves_nested_namespace_to_its_root_index{"PmIndexRoute/FindForNsResolvesNestedNamespaceToItsRootIndex", [] { mcpp::pm::IndexMap indices; indices["acme"] = mcpp::pm::IndexSpec{ .name = "acme", .url = "https://x/y" }; mcpp::pm::IndexRoute route{ &indices, "/nowhere", nullptr }; - EXPECT_NE(route.find_for_ns("acme"), nullptr); - EXPECT_NE(route.find_for_ns("acme.nested"), nullptr); - EXPECT_EQ(route.find_for_ns("other"), nullptr); -} + expect(ne(route.find_for_ns("acme"), nullptr)); + expect(ne(route.find_for_ns("acme.nested"), nullptr)); + expect(eq(route.find_for_ns("other"), nullptr)); +}}; -TEST(PmIndexRoute, LazyGitIndexCannotRefuteAMiss) { +UTest lazy_git_index_cannot_refute_a_miss{"PmIndexRoute/LazyGitIndexCannotRefuteAMiss", [] { mcpp::pm::IndexMap indices; indices["acme"] = mcpp::pm::IndexSpec{ .name = "acme", .url = "https://x/y" }; mcpp::pm::IndexRoute route{ &indices, "/nowhere", nullptr }; - EXPECT_TRUE(route.lazy_git("acme")); - EXPECT_FALSE(route.authoritative_for("acme")); -} + expect(bool(route.lazy_git("acme"))); + expect(!route.authoritative_for("acme")); +}}; -TEST(PmIndexRoute, LocalPathIndexIsAuthoritative) { +UTest local_path_index_is_authoritative{"PmIndexRoute/LocalPathIndexIsAuthoritative", [] { LocalIndex idx("authoritative"); auto indices = local_map(idx); mcpp::pm::IndexRoute route{ &indices, "/nowhere", nullptr }; - EXPECT_FALSE(route.lazy_git("acme")); - EXPECT_TRUE(route.authoritative_for("acme")); -} + expect(!route.lazy_git("acme")); + expect(bool(route.authoritative_for("acme"))); +}}; -TEST(PmIndexRoute, RouteDescriptionDoesNotExposeItsLocalPath) { +UTest route_description_does_not_expose_its_local_path{"PmIndexRoute/RouteDescriptionDoesNotExposeItsLocalPath", [] { LocalIndex idx("diagnostic-privacy"); auto indices = local_map(idx); mcpp::pm::IndexRoute route{ &indices, "/nowhere", nullptr }; const auto description = route.describe("acme"); - EXPECT_EQ(description, "local index 'acme': root present, pkgs present"); - EXPECT_EQ(description.find(idx.root.string()), std::string::npos); -} + expect(eq(description, "local index 'acme': root present, pkgs present")); + expect(eq(description.find(idx.root.string()), std::string::npos)); +}}; -TEST(PmIndexRoute, RouteDescriptionDistinguishesAMissingLocalRoot) { +UTest route_description_distinguishes_a_missing_local_root{"PmIndexRoute/RouteDescriptionDistinguishesAMissingLocalRoot", [] { mcpp::pm::IndexMap indices; indices["acme"] = mcpp::pm::IndexSpec{ .name = "acme", .path = "missing-local-index" }; mcpp::pm::IndexRoute route{ &indices, "/nowhere", nullptr }; - EXPECT_EQ(route.describe("acme"), - "local index 'acme': root absent, pkgs absent"); -} + expect(eq(route.describe("acme"), "local index 'acme': root absent, pkgs absent")); +}}; // The regression behind #305/#307: a dotted selector is one exact NAMESPACE // PATH, so it resolves through the coordinate the manifest parser derives. Probing the // literal short name can never match — `package.name` is a single atomic // segment, so nothing in any index is named "acme.util". -TEST(PmIndexRoute, DottedSelectorResolvesThroughItsExactCoordinate) { +UTest dotted_selector_resolves_through_its_exact_coordinate{"PmIndexRoute/DottedSelectorResolvesThroughItsExactCoordinate", [] { LocalIndex idx("dotted"); auto indices = local_map(idx); mcpp::pm::IndexRoute route{ &indices, "/nowhere", nullptr }; auto selector = mcpp::pm::resolve_dependency_selector("acme.util"); - ASSERT_EQ(selector.candidates.size(), 1u); + expect(fatal(eq(selector.candidates.size(), 1u))); auto found = mcpp::pm::lookup_descriptor(route, selector.candidates); - ASSERT_TRUE(found.hit.has_value()); - EXPECT_EQ(found.hit->coord.namespace_, "acme"); - EXPECT_EQ(found.hit->coord.shortName, "util"); -} + expect(fatal(found.hit.has_value())); + expect(eq(found.hit->coord.namespace_, "acme")); + expect(eq(found.hit->coord.shortName, "util")); +}}; -TEST(PmIndexRoute, NameOnlyDescriptorInheritsOwningLocalIndexNamespace) { +UTest name_only_descriptor_inherits_owning_local_index_namespace{"PmIndexRoute/NameOnlyDescriptorInheritsOwningLocalIndexNamespace", [] { LocalIndex idx("name-only"); std::ofstream(idx.root / "pkgs" / "a" / "acme.util.lua") << R"( package = { @@ -117,12 +118,12 @@ package = { auto selector = mcpp::pm::resolve_dependency_selector("acme.util"); auto found = mcpp::pm::lookup_descriptor(route, selector.candidates); - ASSERT_TRUE(found.hit.has_value()); - EXPECT_EQ(found.hit->coord.namespace_, "acme"); - EXPECT_EQ(found.hit->coord.shortName, "util"); -} + expect(fatal(found.hit.has_value())); + expect(eq(found.hit->coord.namespace_, "acme")); + expect(eq(found.hit->coord.shortName, "util")); +}}; -TEST(PmIndexRoute, MalformedCandidateIsAnErrorNotANotFoundMiss) { +UTest malformed_candidate_is_an_error_not_a_not_found_miss{"PmIndexRoute/MalformedCandidateIsAnErrorNotANotFoundMiss", [] { LocalIndex idx("malformed"); std::ofstream(idx.root / "pkgs" / "a" / "acme.util.lua") << R"( package = { @@ -140,11 +141,11 @@ package = { auto selector = mcpp::pm::resolve_dependency_selector("acme.nested.util"); auto found = mcpp::pm::lookup_descriptor(route, selector.candidates); - EXPECT_FALSE(found.hit.has_value()); - EXPECT_NE(found.error.find("single atomic segment"), std::string::npos); -} + expect(!found.hit.has_value()); + expect(ne(found.error.find("single atomic segment"), std::string::npos)); +}}; -TEST(PmIndexRoute, LiteralDottedShortNameNeverMatches) { +UTest literal_dotted_short_name_never_matches{"PmIndexRoute/LiteralDottedShortNameNeverMatches", [] { LocalIndex idx("literal"); auto indices = local_map(idx); mcpp::pm::IndexRoute route{ &indices, "/nowhere", nullptr }; @@ -152,10 +153,10 @@ TEST(PmIndexRoute, LiteralDottedShortNameNeverMatches) { std::vector literal{ { .namespace_ = "mcpplibs", .shortName = "acme.util" }, }; - EXPECT_FALSE(mcpp::pm::lookup_descriptor(route, literal).hit.has_value()); -} + expect(!(mcpp::pm::lookup_descriptor(route, literal).hit.has_value())); +}}; -TEST(PmIndexRoute, MissInAReadableIndexIsConclusive) { +UTest miss_in_a_readable_index_is_conclusive{"PmIndexRoute/MissInAReadableIndexIsConclusive", [] { LocalIndex idx("conclusive"); auto indices = local_map(idx); mcpp::pm::IndexRoute route{ &indices, "/nowhere", nullptr }; @@ -164,11 +165,11 @@ TEST(PmIndexRoute, MissInAReadableIndexIsConclusive) { { .namespace_ = "acme", .shortName = "nope" }, }; auto found = mcpp::pm::lookup_descriptor(route, missing); - EXPECT_FALSE(found.hit.has_value()); - EXPECT_TRUE(found.conclusive); -} + expect(!found.hit.has_value()); + expect(bool(found.conclusive)); +}}; -TEST(PmIndexRoute, MissBehindAnUnreadableIndexIsNotConclusive) { +UTest miss_behind_an_unreadable_index_is_not_conclusive{"PmIndexRoute/MissBehindAnUnreadableIndexIsNotConclusive", [] { mcpp::pm::IndexMap indices; indices["acme"] = mcpp::pm::IndexSpec{ .name = "acme", .url = "https://x/y" }; mcpp::pm::IndexRoute route{ &indices, "/nowhere", nullptr }; @@ -177,11 +178,11 @@ TEST(PmIndexRoute, MissBehindAnUnreadableIndexIsNotConclusive) { { .namespace_ = "acme", .shortName = "nope" }, }; auto found = mcpp::pm::lookup_descriptor(route, missing); - EXPECT_FALSE(found.hit.has_value()); - EXPECT_FALSE(found.conclusive); -} + expect(!found.hit.has_value()); + expect(!found.conclusive); +}}; -TEST(PmIndexRoute, WorkspaceMemberReadsRootAnchoredRelativeIndex) { +UTest workspace_member_reads_root_anchored_relative_index{"PmIndexRoute/WorkspaceMemberReadsRootAnchoredRelativeIndex", [] { auto root = std::filesystem::temp_directory_path() / "mcpp-index-route-workspace-relative"; std::filesystem::remove_all(root); @@ -217,7 +218,7 @@ version = "0.1.0" )"; auto indices = mcpp::pm::effective_indices(root / "member"); - ASSERT_TRUE(indices.contains("acme")); + expect(fatal(indices.contains("acme"))); // Assert the CAPABILITY (the member can read the root's index), not the // path SPELLING. The first version of this test compared the stored path @@ -226,14 +227,13 @@ version = "0.1.0" // about whether the index is reachable — which is why it stayed green // while the Windows suite was red. mcpp::pm::IndexRoute route{ &indices, root / "member", nullptr }; - EXPECT_EQ(route.describe("acme"), - "local index 'acme': root present, pkgs present"); + expect(eq(route.describe("acme"), "local index 'acme': root present, pkgs present")); auto selector = mcpp::pm::resolve_dependency_selector("acme.util"); auto found = mcpp::pm::lookup_descriptor(route, selector.candidates); - ASSERT_TRUE(found.hit.has_value()); - EXPECT_EQ(found.hit->coord.namespace_, "acme"); - EXPECT_EQ(found.hit->coord.shortName, "util"); -} + expect(fatal(found.hit.has_value())); + expect(eq(found.hit->coord.namespace_, "acme")); + expect(eq(found.hit->coord.shortName, "util")); +}}; // Why `inherit_workspace_indices` anchors with `lexically_normal` and not // `weakly_canonical`. @@ -251,9 +251,9 @@ version = "0.1.0" // short-name alias — turned out NOT to be the cause of the red Windows suite; // that was a fixture writing an MSYS path into mcpp.toml. The change survives // on this invariant, which is why the invariant is now a test.) -TEST(PmIndexRoute, InheritedIndexStaysInsideTheDeclaredWorkspaceTree) { +UTest inherited_index_stays_inside_the_declared_workspace_tree{"PmIndexRoute/InheritedIndexStaysInsideTheDeclaredWorkspaceTree", [] { #ifdef _WIN32 - GTEST_SKIP() << "directory symlinks need elevation on Windows"; + skip("directory symlinks need elevation on Windows"); #else auto base = std::filesystem::temp_directory_path() / "mcpp-index-route-workspace-symlink"; @@ -292,26 +292,25 @@ version = "0.1.0" )"; std::error_code ec; std::filesystem::create_directory_symlink(real, link, ec); - if (ec) GTEST_SKIP() << "symlinks unavailable here: " << ec.message(); + if (ec) skip([&] { std::ostringstream text; text << "symlinks unavailable here: " << ec.message(); return text.str(); }()); auto indices = mcpp::pm::effective_indices(link / "member"); - ASSERT_TRUE(indices.contains("acme")); + expect(fatal(indices.contains("acme"))); // The declared tree is preserved: the anchored path is still addressed // through `link`, not rewritten to `real`. const auto anchored = indices.at("acme").path.string(); - EXPECT_TRUE(anchored.starts_with(link.string())) + expect(anchored.starts_with(link.string())) << "anchoring relocated the index out of the declared tree: " << anchored; // …and it is still readable through that spelling. Preserving identity is // only correct if the index remains reachable. mcpp::pm::IndexRoute route{ &indices, link / "member", nullptr }; - EXPECT_EQ(route.describe("acme"), - "local index 'acme': root present, pkgs present"); + expect(eq(route.describe("acme"), "local index 'acme': root present, pkgs present")); auto selector = mcpp::pm::resolve_dependency_selector("acme.util"); auto found = mcpp::pm::lookup_descriptor(route, selector.candidates); - ASSERT_TRUE(found.hit.has_value()); - EXPECT_EQ(found.hit->coord.namespace_, "acme"); + expect(fatal(found.hit.has_value())); + expect(eq(found.hit->coord.namespace_, "acme")); #endif -} +}}; diff --git a/tests/unit/test_pm_lock_io.cpp b/tests/unit/test_pm_lock_io.cpp index d0d3c146d..ad3c1db59 100644 --- a/tests/unit/test_pm_lock_io.cpp +++ b/tests/unit/test_pm_lock_io.cpp @@ -1,110 +1,107 @@ -#include import std; +import mcpplibs.mtest; import mcpp.pm.lock_io; -TEST(PmLockIo, ParseGitBranchWithCommit) { +using namespace mcpplibs::mtest; + +UTest parse_git_branch_with_commit{"PmLockIo/ParseGitBranchWithCommit", [] { auto parsed = mcpp::pm::parse_git_source( "git+https://github.com/user/repo#branch=develop@584894315b7a4fe4d7957d3c29dc4052b8012860"); - ASSERT_TRUE(parsed.has_value()); - EXPECT_EQ(parsed->url, "https://github.com/user/repo"); - EXPECT_EQ(parsed->refKind, "branch"); - EXPECT_EQ(parsed->ref, "develop"); - ASSERT_TRUE(parsed->resolvedCommit.has_value()); - EXPECT_EQ(parsed->resolvedCommit.value(), - "584894315b7a4fe4d7957d3c29dc4052b8012860"); -} + expect(fatal(parsed.has_value())); + expect(eq(parsed->url, "https://github.com/user/repo")); + expect(eq(parsed->refKind, "branch")); + expect(eq(parsed->ref, "develop")); + expect(fatal(parsed->resolvedCommit.has_value())); + expect(eq(parsed->resolvedCommit.value(), "584894315b7a4fe4d7957d3c29dc4052b8012860")); +}}; -TEST(PmLockIo, ParseGitBranchWithoutCommit) { +UTest parse_git_branch_without_commit{"PmLockIo/ParseGitBranchWithoutCommit", [] { auto parsed = mcpp::pm::parse_git_source( "git+https://github.com/user/repo#branch=develop"); - ASSERT_TRUE(parsed.has_value()); - EXPECT_EQ(parsed->url, "https://github.com/user/repo"); - EXPECT_EQ(parsed->refKind, "branch"); - EXPECT_EQ(parsed->ref, "develop"); - EXPECT_FALSE(parsed->resolvedCommit.has_value()); -} + expect(fatal(parsed.has_value())); + expect(eq(parsed->url, "https://github.com/user/repo")); + expect(eq(parsed->refKind, "branch")); + expect(eq(parsed->ref, "develop")); + expect(!(parsed->resolvedCommit.has_value())); +}}; -TEST(PmLockIo, ParseGitTag) { +UTest parse_git_tag{"PmLockIo/ParseGitTag", [] { auto parsed = mcpp::pm::parse_git_source( "git+https://github.com/user/repo#tag=v1.0.0"); - ASSERT_TRUE(parsed.has_value()); - EXPECT_EQ(parsed->url, "https://github.com/user/repo"); - EXPECT_EQ(parsed->refKind, "tag"); - EXPECT_EQ(parsed->ref, "v1.0.0"); - EXPECT_FALSE(parsed->resolvedCommit.has_value()); -} + expect(fatal(parsed.has_value())); + expect(eq(parsed->url, "https://github.com/user/repo")); + expect(eq(parsed->refKind, "tag")); + expect(eq(parsed->ref, "v1.0.0")); + expect(!(parsed->resolvedCommit.has_value())); +}}; -TEST(PmLockIo, ParseGitRev) { +UTest parse_git_rev{"PmLockIo/ParseGitRev", [] { auto parsed = mcpp::pm::parse_git_source( "git+https://github.com/user/repo#rev=584894315b7a4fe4d7957d3c29dc4052b8012860"); - ASSERT_TRUE(parsed.has_value()); - EXPECT_EQ(parsed->url, "https://github.com/user/repo"); - EXPECT_EQ(parsed->refKind, "rev"); - EXPECT_EQ(parsed->ref, "584894315b7a4fe4d7957d3c29dc4052b8012860"); - EXPECT_FALSE(parsed->resolvedCommit.has_value()); -} + expect(fatal(parsed.has_value())); + expect(eq(parsed->url, "https://github.com/user/repo")); + expect(eq(parsed->refKind, "rev")); + expect(eq(parsed->ref, "584894315b7a4fe4d7957d3c29dc4052b8012860")); + expect(!(parsed->resolvedCommit.has_value())); +}}; // A branch name may legitimately contain '@' — the commit is whatever follows // the LAST one, matching how prepare.cppm writes `ref + "@" + commit`. -TEST(PmLockIo, ParseGitBranchNameContainingAt) { +UTest parse_git_branch_name_containing_at{"PmLockIo/ParseGitBranchNameContainingAt", [] { auto parsed = mcpp::pm::parse_git_source( "git+https://github.com/user/repo#branch=feat@v2@584894315b7a4fe4d7957d3c29dc4052b8012860"); - ASSERT_TRUE(parsed.has_value()); - EXPECT_EQ(parsed->ref, "feat@v2"); - ASSERT_TRUE(parsed->resolvedCommit.has_value()); - EXPECT_EQ(parsed->resolvedCommit.value(), - "584894315b7a4fe4d7957d3c29dc4052b8012860"); -} + expect(fatal(parsed.has_value())); + expect(eq(parsed->ref, "feat@v2")); + expect(fatal(parsed->resolvedCommit.has_value())); + expect(eq(parsed->resolvedCommit.value(), "584894315b7a4fe4d7957d3c29dc4052b8012860")); +}}; // `branch=foo@` records no commit; an empty string would make the anchor claim // a pin it does not have. -TEST(PmLockIo, ParseGitBranchWithEmptyCommit) { +UTest parse_git_branch_with_empty_commit{"PmLockIo/ParseGitBranchWithEmptyCommit", [] { auto parsed = mcpp::pm::parse_git_source( "git+https://github.com/user/repo#branch=develop@"); - ASSERT_TRUE(parsed.has_value()); - EXPECT_EQ(parsed->ref, "develop"); - EXPECT_FALSE(parsed->resolvedCommit.has_value()); -} + expect(fatal(parsed.has_value())); + expect(eq(parsed->ref, "develop")); + expect(!(parsed->resolvedCommit.has_value())); +}}; // Only branch entries carry `@`, so a tag or rev whose name contains // '@' must survive verbatim rather than be split into ref + commit. -TEST(PmLockIo, ParseGitTagNameContainingAtIsNotSplit) { +UTest parse_git_tag_name_containing_at_is_not_split{"PmLockIo/ParseGitTagNameContainingAtIsNotSplit", [] { auto tag = mcpp::pm::parse_git_source( "git+https://github.com/user/repo#tag=v1.0@rc1"); - ASSERT_TRUE(tag.has_value()); - EXPECT_EQ(tag->ref, "v1.0@rc1"); - EXPECT_FALSE(tag->resolvedCommit.has_value()); + expect(fatal(tag.has_value())); + expect(eq(tag->ref, "v1.0@rc1")); + expect(!(tag->resolvedCommit.has_value())); auto rev = mcpp::pm::parse_git_source( "git+https://github.com/user/repo#rev=abc123@def"); - ASSERT_TRUE(rev.has_value()); - EXPECT_EQ(rev->ref, "abc123@def"); - EXPECT_FALSE(rev->resolvedCommit.has_value()); -} + expect(fatal(rev.has_value())); + expect(eq(rev->ref, "abc123@def")); + expect(!(rev->resolvedCommit.has_value())); +}}; // An scp-like URL puts '@' before the '#', so the URL half must not be // disturbed by the ref-side split. -TEST(PmLockIo, ParseGitScpLikeUrl) { +UTest parse_git_scp_like_url{"PmLockIo/ParseGitScpLikeUrl", [] { auto parsed = mcpp::pm::parse_git_source( "git+git@github.com:user/repo.git#branch=develop@584894315b7a4fe4d7957d3c29dc4052b8012860"); - ASSERT_TRUE(parsed.has_value()); - EXPECT_EQ(parsed->url, "git@github.com:user/repo.git"); - EXPECT_EQ(parsed->ref, "develop"); - ASSERT_TRUE(parsed->resolvedCommit.has_value()); - EXPECT_EQ(parsed->resolvedCommit.value(), - "584894315b7a4fe4d7957d3c29dc4052b8012860"); -} + expect(fatal(parsed.has_value())); + expect(eq(parsed->url, "git@github.com:user/repo.git")); + expect(eq(parsed->ref, "develop")); + expect(fatal(parsed->resolvedCommit.has_value())); + expect(eq(parsed->resolvedCommit.value(), "584894315b7a4fe4d7957d3c29dc4052b8012860")); +}}; -TEST(PmLockIo, ParseNonGitSourceReturnsNullopt) { - EXPECT_FALSE(mcpp::pm::parse_git_source("index+mcpplibs@1.0.0").has_value()); - EXPECT_FALSE(mcpp::pm::parse_git_source("").has_value()); - EXPECT_FALSE( - mcpp::pm::parse_git_source("https://github.com/user/repo").has_value()); - EXPECT_FALSE(mcpp::pm::parse_git_source("git+https://host/repo").has_value()); - EXPECT_FALSE( - mcpp::pm::parse_git_source("git+https://host/repo#bad").has_value()); -} +UTest parse_non_git_source_returns_nullopt{"PmLockIo/ParseNonGitSourceReturnsNullopt", [] { + expect(!(mcpp::pm::parse_git_source("index+mcpplibs@1.0.0").has_value())); + expect(!(mcpp::pm::parse_git_source("").has_value())); + expect(!(mcpp::pm::parse_git_source("https://github.com/user/repo").has_value())); + expect(!(mcpp::pm::parse_git_source("git+https://host/repo").has_value())); + expect(!(mcpp::pm::parse_git_source("git+https://host/repo#bad").has_value())); +}}; // The lock digest MUST be the same on every host. It used `std::hash`, // whose output is implementation-defined — MSVC implements it as FNV-1a, while @@ -113,12 +110,10 @@ TEST(PmLockIo, ParseNonGitSourceReturnsNullopt) { // clean checkout showed a lock diff after every command, and the `fnv1a:` prefix // was only true on MSVC. These vectors are the cross-platform contract: a // host-dependent hash turns them red on one of the two platforms. -TEST(PmLockIo, IndexPackageDigestIsFnv1aOnEveryHost) { - EXPECT_EQ(mcpp::pm::index_package_digest("compat", "compat.catch2", "2.13.10"), - "fnv1a:50719400df192025"); - EXPECT_EQ(mcpp::pm::index_package_digest("acme", "acme.gadget", "2.1.0"), - "fnv1a:2adea846f70078bc"); -} +UTest index_package_digest_is_fnv1a_on_every_host{"PmLockIo/IndexPackageDigestIsFnv1aOnEveryHost", [] { + expect(eq(mcpp::pm::index_package_digest("compat", "compat.catch2", "2.13.10"), "fnv1a:50719400df192025")); + expect(eq(mcpp::pm::index_package_digest("acme", "acme.gadget", "2.1.0"), "fnv1a:2adea846f70078bc")); +}}; namespace { @@ -139,7 +134,7 @@ std::string lock_bytes(const std::filesystem::path& file) { } } -TEST(PmLockIo, UnchangedLfAndCrlfLocksKeepTheirBytesAndTimestamp) { +UTest unchanged_lf_and_crlf_locks_keep_their_bytes_and_timestamp{"PmLockIo/UnchangedLfAndCrlfLocksKeepTheirBytesAndTimestamp", [] { LockDirectory t; const auto file = t.root / "mcpp.lock"; mcpp::pm::Lockfile lock; @@ -155,68 +150,68 @@ TEST(PmLockIo, UnchangedLfAndCrlfLocksKeepTheirBytesAndTimestamp) { const auto old = std::filesystem::file_time_type::clock::now() - std::chrono::hours(24); std::filesystem::last_write_time(file, old); const auto recorded = std::filesystem::last_write_time(file); - ASSERT_TRUE(mcpp::pm::write(lock, file)); - EXPECT_EQ(lock_bytes(file), bytes); - EXPECT_EQ(std::filesystem::last_write_time(file), recorded); + expect(fatal(bool(mcpp::pm::write(lock, file)))); + expect(eq(lock_bytes(file), bytes)); + expect(eq(std::filesystem::last_write_time(file), recorded)); } -} +}}; -TEST(PmLockIo, CreatesMissingParentsAndPublishesChangedResolution) { +UTest creates_missing_parents_and_publishes_changed_resolution{"PmLockIo/CreatesMissingParentsAndPublishesChangedResolution", [] { LockDirectory t; const auto file = t.root / "nested" / "mcpp.lock"; mcpp::pm::Lockfile lock; lock.packages.push_back({"fixture", "example", "1.0.0", "path", "digest"}); - ASSERT_TRUE(mcpp::pm::write(lock, file)); + expect(fatal(bool(mcpp::pm::write(lock, file)))); lock.packages[0].version = "2.0.0"; - ASSERT_TRUE(mcpp::pm::write(lock, file)); + expect(fatal(bool(mcpp::pm::write(lock, file)))); auto loaded = mcpp::pm::load(file); - ASSERT_TRUE(loaded); - ASSERT_EQ(loaded->packages.size(), 1u); - EXPECT_EQ(loaded->packages[0].version, "2.0.0"); -} + expect(fatal(bool(loaded))); + expect(fatal(eq(loaded->packages.size(), 1u))); + expect(eq(loaded->packages[0].version, "2.0.0")); +}}; -TEST(PmLockIo, UnchangedReadOnlyLockDoesNotNeedWriteAccess) { +UTest unchanged_read_only_lock_does_not_need_write_access{"PmLockIo/UnchangedReadOnlyLockDoesNotNeedWriteAccess", [] { LockDirectory t; const auto file = t.root / "mcpp.lock"; mcpp::pm::Lockfile lock; - ASSERT_TRUE(mcpp::pm::write(lock, file)); + expect(fatal(bool(mcpp::pm::write(lock, file)))); std::filesystem::permissions(file, std::filesystem::perms::owner_read); const auto recorded = std::filesystem::last_write_time(file); - EXPECT_TRUE(mcpp::pm::write(lock, file)); - EXPECT_EQ(std::filesystem::last_write_time(file), recorded); -} + expect(bool(mcpp::pm::write(lock, file))); + expect(eq(std::filesystem::last_write_time(file), recorded)); +}}; -TEST(PmLockIo, AnUnreadableFileOrUncreatableParentReturnsAnError) { +UTest an_unreadable_file_or_uncreatable_parent_returns_an_error{"PmLockIo/AnUnreadableFileOrUncreatableParentReturnsAnError", [] { LockDirectory t; const auto blocker = t.root / "file"; { std::ofstream out(blocker); out << "not a directory"; } mcpp::pm::Lockfile lock; - EXPECT_FALSE(mcpp::pm::write(lock, blocker / "mcpp.lock")); - EXPECT_FALSE(mcpp::pm::write(lock, t.root)); -} + expect(!mcpp::pm::write(lock, blocker / "mcpp.lock")); + expect(!mcpp::pm::write(lock, t.root)); +}}; -TEST(PmLockIo, UpdatingALinkedLockKeepsTheLinkAndUpdatesItsTarget) { +UTest updating_a_linked_lock_keeps_the_link_and_updates_its_target{"PmLockIo/UpdatingALinkedLockKeepsTheLinkAndUpdatesItsTarget", [] { LockDirectory t; const auto target = t.root / "target.lock"; const auto link = t.root / "mcpp.lock"; mcpp::pm::Lockfile lock; - ASSERT_TRUE(mcpp::pm::write(lock, target)); + expect(fatal(bool(mcpp::pm::write(lock, target)))); std::error_code ec; std::filesystem::create_symlink(target, link, ec); - if (ec) GTEST_SKIP() << "this host does not permit file symlinks: " << ec.message(); + if (ec) skip([&] { std::ostringstream text; text << "this host does not permit file symlinks: " << ec.message(); return text.str(); }()); lock.packages.push_back({"fixture", "example", "2.0.0", "path", "digest"}); - ASSERT_TRUE(mcpp::pm::write(lock, link)); - EXPECT_TRUE(std::filesystem::is_symlink(link)); + expect(fatal(bool(mcpp::pm::write(lock, link)))); + expect(std::filesystem::is_symlink(link)); auto loaded = mcpp::pm::load(target); - ASSERT_TRUE(loaded); - ASSERT_EQ(loaded->packages.size(), 1u); - EXPECT_EQ(loaded->packages[0].version, "2.0.0"); -} + expect(fatal(bool(loaded))); + expect(fatal(eq(loaded->packages.size(), 1u))); + expect(eq(loaded->packages[0].version, "2.0.0")); +}}; // An entry names the configurations that resolve it, and reads back as written // (SPEC-013 §15.1); a lock written before the field reads with none. -TEST(PmLockIo, TargetsRoundTrip) { +UTest targets_round_trip{"PmLockIo/TargetsRoundTrip", [] { const auto dir = std::filesystem::temp_directory_path() / std::format("mcpp-lock-{}", std::chrono::steady_clock::now().time_since_epoch().count()); std::filesystem::create_directories(dir); @@ -231,16 +226,49 @@ TEST(PmLockIo, TargetsRoundTrip) { p.origin = "platform"; p.targets = { "aarch64-openkal", "x86_64-openkal" }; lock.packages.push_back(p); - ASSERT_TRUE(mcpp::pm::write(lock, dir / "mcpp.lock")); + expect(fatal(bool(mcpp::pm::write(lock, dir / "mcpp.lock")))); auto back = mcpp::pm::load(dir / "mcpp.lock"); - ASSERT_TRUE(back); - ASSERT_EQ(back->packages.size(), 1u); - EXPECT_EQ(back->packages[0].targets, p.targets); - EXPECT_EQ(back->packages[0].origin, "platform"); + expect(fatal(bool(back))); + expect(fatal(eq(back->packages.size(), 1u))); + expect(eq(back->packages[0].targets, p.targets)); + expect(eq(back->packages[0].origin, "platform")); std::ofstream(dir / "old.lock") << "version = 2\n\n[package.\"x\"]\nnamespace = \"mcpplibs\"\nversion = \"1\"\n"; auto old = mcpp::pm::load(dir / "old.lock"); - ASSERT_TRUE(old); - EXPECT_TRUE(old->packages.at(0).targets.empty()); + expect(fatal(bool(old))); + expect(old->packages.at(0).targets.empty()); std::error_code ec; std::filesystem::remove_all(dir, ec); -} +}}; + +// THE IDENTITY A REQUEST WAS GIVEN IS RECORDED (SPEC-016 §4.5): written as +// `[target.""]` and read back as it was written. +UTest a_requests_identity_round_trips{"PmLockIo/ARequestsIdentityRoundTrips", [] { + mcpp::pm::Lockfile lock; + lock.targets.push_back({ "x86_64-windows", "x86_64-windows-musl", "openkal" }); + lock.targets.push_back({ "x86_64-linux", "x86_64-linux-gnu", "glibc" }); + mcpp::pm::LockedPackage p; + p.name = "mcpplibs.cmdline"; + p.namespace_ = "mcpplibs"; + p.version = "0.0.1"; + p.source = "index+mcpplibs@0.0.1"; + p.hash = "fnv1a:0"; + lock.packages.push_back(p); + const auto text = mcpp::pm::serialize(lock); + expect(ne(text.find("[target.\"x86_64-windows\"]\nidentity = \"x86_64-windows-musl\"\nprovider = \"openkal\""), + std::string::npos)) << text; + const auto dir = std::filesystem::temp_directory_path() + / std::format("mcpp-lock-target-{}", std::random_device{}()); + std::filesystem::create_directories(dir); + { + std::ofstream(dir / "mcpp.lock", std::ios::binary) << text; + } + auto back = mcpp::pm::load(dir / "mcpp.lock"); + std::filesystem::remove_all(dir); + expect(fatal(back.has_value())); + expect(fatal(eq(back->targets.size(), 2u))); + auto w = std::ranges::find(back->targets, std::string("x86_64-windows"), &mcpp::pm::LockedTarget::request); + expect(fatal(w != back->targets.end())); + expect(eq(w->identity, "x86_64-windows-musl")); + expect(eq(w->provider, "openkal")); + expect(eq(back->packages.size(), 1u)); +}}; diff --git a/tests/unit/test_pm_package_fetcher.cpp b/tests/unit/test_pm_package_fetcher.cpp index dc0b32dc8..d3795c17e 100644 --- a/tests/unit/test_pm_package_fetcher.cpp +++ b/tests/unit/test_pm_package_fetcher.cpp @@ -1,9 +1,12 @@ -#include import std; +import mcpplibs.mtest; +import mcpplibs.os.stdio; import mcpp.pm.package_fetcher; import mcpp.pm.index_contract; +using namespace mcpplibs::mtest; + // Regression for the compat.zlib vs upstream bare zlib.lua collision. // // read_xpkg_lua_from_project_data scans every index dir under the project's @@ -49,7 +52,7 @@ package = { } // namespace -TEST(PmPackageFetcher, ResolvesCompatZlibNotForeignBareZlib) { +UTest resolves_compat_zlib_not_foreign_bare_zlib{"PmPackageFetcher/ResolvesCompatZlibNotForeignBareZlib", [] { auto project = make_tempdir("mcpp-fetcher-collision"); auto dataRoot = project / ".mcpp" / "data"; @@ -63,16 +66,16 @@ TEST(PmPackageFetcher, ResolvesCompatZlibNotForeignBareZlib) { auto lua = mcpp::pm::Fetcher::read_xpkg_lua_from_project_data( project, "compat", "zlib"); - ASSERT_TRUE(lua.has_value()); - EXPECT_NE(lua->find("compat.zlib"), std::string::npos) + expect(fatal(lua.has_value())); + expect(ne(lua->find("compat.zlib"), std::string::npos)) << "should return the compat-namespaced descriptor"; - EXPECT_NE(lua->find("mcpp"), std::string::npos) + expect(ne(lua->find("mcpp"), std::string::npos)) << "the resolved descriptor must carry the mcpp block"; std::filesystem::remove_all(project); -} +}}; -TEST(PmPackageFetcher, ForeignBareZlibAloneDoesNotSatisfyCompatRequest) { +UTest foreign_bare_zlib_alone_does_not_satisfy_compat_request{"PmPackageFetcher/ForeignBareZlibAloneDoesNotSatisfyCompatRequest", [] { auto project = make_tempdir("mcpp-fetcher-nomatch"); auto dataRoot = project / ".mcpp" / "data"; @@ -84,12 +87,12 @@ TEST(PmPackageFetcher, ForeignBareZlibAloneDoesNotSatisfyCompatRequest) { auto lua = mcpp::pm::Fetcher::read_xpkg_lua_from_project_data( project, "compat", "zlib"); - EXPECT_FALSE(lua.has_value()); + expect(!lua.has_value()); std::filesystem::remove_all(project); -} +}}; -TEST(PmPackageFetcher, DefaultNamespaceCannotResolveCompatDescriptor) { +UTest default_namespace_cannot_resolve_compat_descriptor{"PmPackageFetcher/DefaultNamespaceCannotResolveCompatDescriptor", [] { auto project = make_tempdir("mcpp-fetcher-compat-alias"); auto dataRoot = project / ".mcpp" / "data"; @@ -106,17 +109,17 @@ package = { auto defaultLua = mcpp::pm::Fetcher::read_xpkg_lua_from_project_data( project, "mcpplibs", "gtest"); - EXPECT_FALSE(defaultLua.has_value()); + expect(!defaultLua.has_value()); auto explicitLua = mcpp::pm::Fetcher::read_xpkg_lua_from_project_data( project, "compat", "gtest"); - ASSERT_TRUE(explicitLua.has_value()); - EXPECT_NE(explicitLua->find("compat.gtest"), std::string::npos); + expect(fatal(explicitLua.has_value())); + expect(ne(explicitLua->find("compat.gtest"), std::string::npos)); std::filesystem::remove_all(project); -} +}}; -TEST(PmPackageFetcher, LocalPathIndexAttributesOwnNamespaceToNoNsDescriptor) { +UTest local_path_index_attributes_own_namespace_to_no_ns_descriptor{"PmPackageFetcher/LocalPathIndexAttributesOwnNamespaceToNoNsDescriptor", [] { // e2e 49/51: a `[indices]` path index `local-dev` whose descriptor declares // only name="tinycfg" (no namespace field). The namespace is owned by the // index, so a `(local-dev, tinycfg)` request must match via index-owned @@ -126,15 +129,15 @@ TEST(PmPackageFetcher, LocalPathIndexAttributesOwnNamespaceToNoNsDescriptor) { R"(package = { name = "tinycfg", version = "1.0.0", mcpp = { sources = {"*.c"} } })"); auto lua = mcpp::pm::Fetcher::read_xpkg_lua_from_path(index, "local-dev", "tinycfg"); - ASSERT_TRUE(lua.has_value()) << "no-namespace descriptor must inherit the index namespace"; - EXPECT_NE(lua->find("tinycfg"), std::string::npos); + expect(fatal(lua.has_value())) << "no-namespace descriptor must inherit the index namespace"; + expect(ne(lua->find("tinycfg"), std::string::npos)); // A short name the index does not contain still resolves to nothing. auto miss = mcpp::pm::Fetcher::read_xpkg_lua_from_path(index, "local-dev", "absent"); - EXPECT_FALSE(miss.has_value()); + expect(!miss.has_value()); std::filesystem::remove_all(index); -} +}}; // Coverage gap closed: a custom-namespace descriptor filed under a NON-canonical // filename must still resolve for a qualified request, because identity is the @@ -148,7 +151,7 @@ TEST(PmPackageFetcher, LocalPathIndexAttributesOwnNamespaceToNoNsDescriptor) { // `read_xpkg_lua*` already keys on declared identity, so this asserts the READ // layer is correct and the production failure is isolated to candidate SELECTION // (`selectDependencyCandidate`'s canonical-filename-only strict reader). -TEST(PmPackageFetcher, ResolvesCustomNamespaceDescriptorUnderNonCanonicalFilename) { +UTest resolves_custom_namespace_descriptor_under_non_canonical_filename{"PmPackageFetcher/ResolvesCustomNamespaceDescriptorUnderNonCanonicalFilename", [] { auto project = make_tempdir("mcpp-noncanonical-filename"); auto dataRoot = project / ".mcpp" / "data"; @@ -165,18 +168,18 @@ TEST(PmPackageFetcher, ResolvesCustomNamespaceDescriptorUnderNonCanonicalFilenam // Qualified request for the custom namespace must resolve, filename be damned. auto hit = mcpp::pm::Fetcher::read_xpkg_lua_from_project_data( project, "aimol", "tensorvia-cpu"); - ASSERT_TRUE(hit.has_value()) + expect(fatal(hit.has_value())) << "declared (aimol, tensorvia-cpu) must resolve regardless of filename"; - EXPECT_NE(hit->find("tensorvia-cpu"), std::string::npos); + expect(ne(hit->find("tensorvia-cpu"), std::string::npos)); // A foreign namespace for the same short name must NOT match it. auto wrongNs = mcpp::pm::Fetcher::read_xpkg_lua_from_project_data( project, "mcpplibs", "tensorvia-cpu"); - EXPECT_FALSE(wrongNs.has_value()) + expect(!wrongNs.has_value()) << "the descriptor is (aimol, …), so a (mcpplibs, …) request must miss"; std::filesystem::remove_all(project); -} +}}; // ─── #238: install_packages failure diagnostics ───────────────────── // @@ -186,7 +189,7 @@ TEST(PmPackageFetcher, ResolvesCustomNamespaceDescriptorUnderNonCanonicalFilenam // .xlings.json index_repos reader so the enriched, actionable error shape is // locked without needing a live (failing) multi-repo xlings. -TEST(PmPackageFetcher, InstallFailureDiagnosticFlagsMultiRepoGap) { +UTest install_failure_diagnostic_flags_multi_repo_gap{"PmPackageFetcher/InstallFailureDiagnosticFlagsMultiRepoGap", [] { std::vector repos = { "mcpplibs -> https://example/mcpplibs", "myindex -> https://example/myindex", @@ -195,44 +198,44 @@ TEST(PmPackageFetcher, InstallFailureDiagnosticFlagsMultiRepoGap) { "acme.widget@1.2.3", 1, repos, /*childError=*/""); // Names the target + exit code. - EXPECT_NE(msg.find("acme.widget@1.2.3"), std::string::npos); - EXPECT_NE(msg.find("exit 1"), std::string::npos); + expect(ne(msg.find("acme.widget@1.2.3"), std::string::npos)); + expect(ne(msg.find("exit 1"), std::string::npos)); // Enumerates the configured repos (count + identities). - EXPECT_NE(msg.find("2 index repos"), std::string::npos); - EXPECT_NE(msg.find("mcpplibs"), std::string::npos); - EXPECT_NE(msg.find("myindex"), std::string::npos); + expect(ne(msg.find("2 index repos"), std::string::npos)); + expect(ne(msg.find("mcpplibs"), std::string::npos)); + expect(ne(msg.find("myindex"), std::string::npos)); // Flags the known ≥2-repo xlings gap and links the tracking issue. - EXPECT_NE(msg.find("#238"), std::string::npos); - EXPECT_NE(msg.find("openxlings/xlings"), std::string::npos); + expect(ne(msg.find("#238"), std::string::npos)); + expect(ne(msg.find("openxlings/xlings"), std::string::npos)); // No child error → points at MCPP_VERBOSE for the raw output. - EXPECT_NE(msg.find("MCPP_VERBOSE"), std::string::npos); -} + expect(ne(msg.find("MCPP_VERBOSE"), std::string::npos)); +}}; -TEST(PmPackageFetcher, InstallFailureDiagnosticSingleRepoNoGapFlag) { +UTest install_failure_diagnostic_single_repo_no_gap_flag{"PmPackageFetcher/InstallFailureDiagnosticSingleRepoNoGapFlag", [] { std::vector repos = { "mcpplibs" }; auto msg = mcpp::pm::format_install_failure_diagnostic( "acme.widget@1.2.3", 1, repos, /*childError=*/""); - EXPECT_NE(msg.find("1 index repo"), std::string::npos); + expect(ne(msg.find("1 index repo"), std::string::npos)); // A single repo is NOT the #238 failure mode; do not misattribute it. - EXPECT_EQ(msg.find("known xlings resolution gap"), std::string::npos); - EXPECT_EQ(msg.find("#238"), std::string::npos); -} + expect(eq(msg.find("known xlings resolution gap"), std::string::npos)); + expect(eq(msg.find("#238"), std::string::npos)); +}}; -TEST(PmPackageFetcher, InstallFailureDiagnosticPreservesChildError) { +UTest install_failure_diagnostic_preserves_child_error{"PmPackageFetcher/InstallFailureDiagnosticPreservesChildError", [] { std::vector repos = { "a", "b" }; auto msg = mcpp::pm::format_install_failure_diagnostic( "acme.widget@1.2.3", 1, repos, /*childError=*/"[error] descriptor not found in index"); // Child-emitted text must be surfaced, not swallowed. - EXPECT_NE(msg.find("descriptor not found in index"), std::string::npos); - EXPECT_NE(msg.find("xlings reported"), std::string::npos); + expect(ne(msg.find("descriptor not found in index"), std::string::npos)); + expect(ne(msg.find("xlings reported"), std::string::npos)); // With a real child error we don't fall back to the MCPP_VERBOSE nudge. - EXPECT_EQ(msg.find("MCPP_VERBOSE"), std::string::npos); -} + expect(eq(msg.find("MCPP_VERBOSE"), std::string::npos)); +}}; -TEST(PmPackageFetcher, ReadSeededIndexReposParsesXlingsJson) { +UTest read_seeded_index_repos_parses_xlings_json{"PmPackageFetcher/ReadSeededIndexReposParsesXlingsJson", [] { auto project = make_tempdir("mcpp-238-xlings-json"); auto xjson = project / ".xlings.json"; write_file(xjson, R"({ @@ -244,19 +247,19 @@ TEST(PmPackageFetcher, ReadSeededIndexReposParsesXlingsJson) { })"); auto repos = mcpp::pm::read_seeded_index_repos(xjson); - ASSERT_EQ(repos.size(), 2u); - EXPECT_NE(repos[0].find("mcpplibs"), std::string::npos); - EXPECT_NE(repos[0].find("https://example/mcpplibs"), std::string::npos); - EXPECT_NE(repos[1].find("myindex"), std::string::npos); + expect(fatal(eq(repos.size(), 2u))); + expect(ne(repos[0].find("mcpplibs"), std::string::npos)); + expect(ne(repos[0].find("https://example/mcpplibs"), std::string::npos)); + expect(ne(repos[1].find("myindex"), std::string::npos)); // Missing file → empty (caller degrades to "unknown configuration"). auto none = mcpp::pm::read_seeded_index_repos(project / "does-not-exist.json"); - EXPECT_TRUE(none.empty()); + expect(none.empty()); std::filesystem::remove_all(project); -} +}}; -TEST(PmPackageFetcher, ReadSeededIndexReposToleratesArtifactFields) { +UTest read_seeded_index_repos_tolerates_artifact_fields{"PmPackageFetcher/ReadSeededIndexReposToleratesArtifactFields", [] { // #269: entries seeded with per-repo artifact/source keys (xlings >= // 0.4.68) must still yield name+url — regression lock for the hand- // rolled index_repos reader. @@ -270,43 +273,42 @@ TEST(PmPackageFetcher, ReadSeededIndexReposToleratesArtifactFields) { })"); auto repos = mcpp::pm::read_seeded_index_repos(xjson); - ASSERT_EQ(repos.size(), 1u); - EXPECT_NE(repos[0].find("mcpplibs"), std::string::npos); - EXPECT_NE(repos[0].find("https://github.com/mcpplibs/mcpp-index.git"), - std::string::npos); + expect(fatal(eq(repos.size(), 1u))); + expect(ne(repos[0].find("mcpplibs"), std::string::npos)); + expect(ne(repos[0].find("https://github.com/mcpplibs/mcpp-index.git"), std::string::npos)); std::filesystem::remove_all(project); -} +}}; // A tree whose index.toml requires a newer mcpp answers no lookup, and the // read prints nothing. The fact is recorded for the message of a run that then // fails (unusable_index_hint), which carries the E0006 text itself; a run that // resolves every package elsewhere has no error to report. Before this, the // read site printed the E0006 text as `error:` at the start of such runs. -TEST(PackageFetcher, AnIndexThatRequiresANewerMcppIsRecordedNotPrinted) { +UTest an_index_that_requires_a_newer_mcpp_is_recorded_not_printed{"PackageFetcher/AnIndexThatRequiresANewerMcppIsRecordedNotPrinted", [] { auto root = make_tempdir("mcpp-floor-read"); write_file(root / "index.toml", "[index]\nspec = \"1\"\nmin_mcpp = \"9999.9.9.9\"\n"); write_file(root / "pkgs" / "f" / "floorpkg.lua", "package = { spec = \"1\", name = \"floorpkg\", type = \"package\" }\n"); - testing::internal::CaptureStderr(); + auto stderrCapture = mcpplibs::os::stdio::capture::begin(mcpplibs::os::stdio::stream::error).value(); auto first = mcpp::pm::Fetcher::read_xpkg_lua_from_path(root, "floorns", "floorpkg"); auto second = mcpp::pm::Fetcher::read_xpkg_lua_from_path(root, "floorns", "floorpkg"); - auto err = testing::internal::GetCapturedStderr(); + auto err = stderrCapture.text(); - EXPECT_FALSE(first.has_value()); - EXPECT_FALSE(second.has_value()) << "a tree recorded as unusable answers no later lookup"; - EXPECT_TRUE(err.empty()) << err; - EXPECT_TRUE(mcpp::pm::index_marked_unusable(root)); + expect(!first.has_value()); + expect(!second.has_value()) << "a tree recorded as unusable answers no later lookup"; + expect(err.empty()) << err; + expect(bool(mcpp::pm::index_marked_unusable(root))); auto hint = mcpp::pm::unusable_index_hint(); - EXPECT_NE(hint.find("E0006"), std::string::npos) << hint; - EXPECT_NE(hint.find("9999.9.9.9"), std::string::npos) << hint; - EXPECT_EQ(hint.find("error above"), std::string::npos) << hint; + expect(ne(hint.find("E0006"), std::string::npos)) << hint; + expect(ne(hint.find("9999.9.9.9"), std::string::npos)) << hint; + expect(eq(hint.find("error above"), std::string::npos)) << hint; mcpp::pm::reset_unusable_indexes_for_test(); std::filesystem::remove_all(root); -} +}}; // ─── What a caller's intent does with a payload (design 2026-10-17 D3) ─── // @@ -314,36 +316,36 @@ TEST(PackageFetcher, AnIndexThatRequiresANewerMcppIsRecordedNotPrinted) { // revision 1 with no floor -- complete, usable, not current. A build USES it; // only `mcpp toolchain install` replaces it. 2026.10.16.x replaced it inside // the build (and, with one dangling record in the home, failed every run). -TEST(PmPayloadIntent, AnOlderPackagingIsUsedByABuildAndReplacedOnlyWhenProvisioned) { +UTest an_older_packaging_is_used_by_a_build_and_replaced_only_when_provisioned{"PmPayloadIntent/AnOlderPackagingIsUsedByABuildAndReplacedOnlyWhenProvisioned", [] { using mcpp::pm::PayloadAction; using mcpp::pm::PayloadIntent; using mcpp::pm::payload_action; // present, usable, current - EXPECT_EQ(payload_action(PayloadIntent::Use, true, true, false), PayloadAction::Take); - EXPECT_EQ(payload_action(PayloadIntent::Locate, true, true, false), PayloadAction::Take); - EXPECT_EQ(payload_action(PayloadIntent::Provision, true, true, false), PayloadAction::Replace); - EXPECT_EQ(payload_action(PayloadIntent::Provision, true, true, true), PayloadAction::Take); -} + expect(eq(payload_action(PayloadIntent::Use, true, true, false), PayloadAction::Take)); + expect(eq(payload_action(PayloadIntent::Locate, true, true, false), PayloadAction::Take)); + expect(eq(payload_action(PayloadIntent::Provision, true, true, false), PayloadAction::Replace)); + expect(eq(payload_action(PayloadIntent::Provision, true, true, true), PayloadAction::Take)); +}}; // Below the recipe's floor, or incomplete: not usable, so even a build // replaces it -- the author said the old packaging must go. -TEST(PmPayloadIntent, AnUnusablePayloadIsReplacedByEveryIntentThatMayInstall) { +UTest an_unusable_payload_is_replaced_by_every_intent_that_may_install{"PmPayloadIntent/AnUnusablePayloadIsReplacedByEveryIntentThatMayInstall", [] { using mcpp::pm::PayloadAction; using mcpp::pm::PayloadIntent; using mcpp::pm::payload_action; - EXPECT_EQ(payload_action(PayloadIntent::Use, true, false, false), PayloadAction::Replace); - EXPECT_EQ(payload_action(PayloadIntent::Provision, true, false, false), PayloadAction::Replace); - EXPECT_EQ(payload_action(PayloadIntent::Locate, true, false, false), PayloadAction::Refuse); -} + expect(eq(payload_action(PayloadIntent::Use, true, false, false), PayloadAction::Replace)); + expect(eq(payload_action(PayloadIntent::Provision, true, false, false), PayloadAction::Replace)); + expect(eq(payload_action(PayloadIntent::Locate, true, false, false), PayloadAction::Refuse)); +}}; -TEST(PmPayloadIntent, NothingThereIsInstalledUnlessOnlyLocated) { +UTest nothing_there_is_installed_unless_only_located{"PmPayloadIntent/NothingThereIsInstalledUnlessOnlyLocated", [] { using mcpp::pm::PayloadAction; using mcpp::pm::PayloadIntent; using mcpp::pm::payload_action; - EXPECT_EQ(payload_action(PayloadIntent::Use, false, false, false), PayloadAction::Install); - EXPECT_EQ(payload_action(PayloadIntent::Provision, false, false, false), PayloadAction::Install); - EXPECT_EQ(payload_action(PayloadIntent::Locate, false, false, false), PayloadAction::Refuse); -} + expect(eq(payload_action(PayloadIntent::Use, false, false, false), PayloadAction::Install)); + expect(eq(payload_action(PayloadIntent::Provision, false, false, false), PayloadAction::Install)); + expect(eq(payload_action(PayloadIntent::Locate, false, false, false), PayloadAction::Refuse)); +}}; // ─── What a failed install calls for (D5) ─── // @@ -352,21 +354,21 @@ TEST(PmPayloadIntent, NothingThereIsInstalledUnlessOnlyLocated) { // descriptor that was local, retried the same doomed replacement through the // command line, and told the user to retry. A deterministic refusal is said // once; only a package the provider could not find refreshes the index. -TEST(PmInstallFailurePolicy, ADeterministicRefusalIsNeitherRefreshedNorRetried) { +UTest a_deterministic_refusal_is_neither_refreshed_nor_retried{"PmInstallFailurePolicy/ADeterministicRefusalIsNeitherRefreshedNorRetried", [] { for (std::string_view code : {"E_INTERNAL", "E_PERMISSION", "E_DISK_FULL", "E_CANCELLED"}) { const auto p = mcpp::pm::install_failure_policy(code); - EXPECT_FALSE(p.refreshIndex) << code; - EXPECT_FALSE(p.retry) << code; + expect(!p.refreshIndex) << code; + expect(!p.retry) << code; } -} - -TEST(PmInstallFailurePolicy, OnlyAMissingPackageRefreshesTheIndex) { - EXPECT_TRUE(mcpp::pm::install_failure_policy("E_NOT_FOUND").refreshIndex); - EXPECT_TRUE(mcpp::pm::install_failure_policy("E_NOT_FOUND").retry); - EXPECT_FALSE(mcpp::pm::install_failure_policy("E_NETWORK").refreshIndex); - EXPECT_FALSE(mcpp::pm::install_failure_policy("E_NETWORK").retry); - EXPECT_TRUE(mcpp::pm::install_failure_policy("E_INVALID_INPUT").retry); +}}; + +UTest only_a_missing_package_refreshes_the_index{"PmInstallFailurePolicy/OnlyAMissingPackageRefreshesTheIndex", [] { + expect(bool(mcpp::pm::install_failure_policy("E_NOT_FOUND").refreshIndex)); + expect(bool(mcpp::pm::install_failure_policy("E_NOT_FOUND").retry)); + expect(!(mcpp::pm::install_failure_policy("E_NETWORK").refreshIndex)); + expect(!(mcpp::pm::install_failure_policy("E_NETWORK").retry)); + expect(bool(mcpp::pm::install_failure_policy("E_INVALID_INPUT").retry)); // A provider that said nothing (#238): the old conservative path. - EXPECT_TRUE(mcpp::pm::install_failure_policy("").refreshIndex); - EXPECT_TRUE(mcpp::pm::install_failure_policy("").retry); -} + expect(bool(mcpp::pm::install_failure_policy("").refreshIndex)); + expect(bool(mcpp::pm::install_failure_policy("").retry)); +}}; diff --git a/tests/unit/test_post_install.cpp b/tests/unit/test_post_install.cpp index 62e74a1fe..6e9f4ea3c 100644 --- a/tests/unit/test_post_install.cpp +++ b/tests/unit/test_post_install.cpp @@ -1,12 +1,14 @@ -#include #include import std; +import mcpplibs.mtest; import mcpp.toolchain.post_install; import mcpp.provider.xlings; import mcpp.config; import mcpp.toolchain.registry; +using namespace mcpplibs::mtest; + // detect_baked_loader parses gcc SPECS GRAMMAR, not plain text. The baked // loader path is embedded inside %-spec conditionals, e.g. // %{mmusl:/lib/ld-musl-x86_64.so.1;:/baked/dir/ld-linux-x86-64.so.2} @@ -30,36 +32,34 @@ const std::string kBakedSpecs = "%{mmusl:/lib/ld-musl-x86_64.so.1;:/opt/other-home/.xlings/data/xpkgs/xim-x-glibc/2.39/lib64/ld-linux-x86-64.so.2}}}}} " "%{static:-static}}\n"; -TEST(DetectBakedLoader, ExtractsExactPathWithoutSpecBraces) { +UTest extracts_exact_path_without_spec_braces{"DetectBakedLoader/ExtractsExactPathWithoutSpecBraces", [] { auto got = detect_baked_loader(kBakedSpecs); - EXPECT_EQ(got, - "/opt/other-home/.xlings/data/xpkgs/xim-x-glibc/2.39/lib64/ld-linux-x86-64.so.2"); + expect(eq(got, "/opt/other-home/.xlings/data/xpkgs/xim-x-glibc/2.39/lib64/ld-linux-x86-64.so.2")); // The regression: any brace in the result corrupts the specs on rewrite. - EXPECT_EQ(got.find('}'), std::string::npos); - EXPECT_EQ(got.find('{'), std::string::npos); -} + expect(eq(got.find('}'), std::string::npos)); + expect(eq(got.find('{'), std::string::npos)); +}}; -TEST(DetectBakedLoader, IgnoresPristineMultilibDefaults) { +UTest ignores_pristine_multilib_defaults{"DetectBakedLoader/IgnoresPristineMultilibDefaults", [] { // An unbaked spec (all-standard /lib*/ paths) must not be rewritten. const std::string pristine = "%{mmusl:/lib/ld-musl-x86_64.so.1;:/lib64/ld-linux-x86-64.so.2} " "%{m16|m32:-dynamic-linker /lib/ld-linux.so.2} " "%{mx32:-dynamic-linker /libx32/ld-linux-x32.so.2}"; - EXPECT_EQ(detect_baked_loader(pristine), ""); -} + expect(eq(detect_baked_loader(pristine), "")); +}}; -TEST(DetectBakedLoader, Aarch64LoaderNameDetected) { +UTest aarch64_loader_name_detected{"DetectBakedLoader/Aarch64LoaderNameDetected", [] { const std::string specs = "-dynamic-linker %{mmusl:/lib/ld-musl-aarch64.so.1;:" "/srv/build/.xlings/xpkgs/xim-x-glibc/2.39/lib/ld-linux-aarch64.so.1}"; - EXPECT_EQ(detect_baked_loader(specs), - "/srv/build/.xlings/xpkgs/xim-x-glibc/2.39/lib/ld-linux-aarch64.so.1"); -} + expect(eq(detect_baked_loader(specs), "/srv/build/.xlings/xpkgs/xim-x-glibc/2.39/lib/ld-linux-aarch64.so.1")); +}}; -TEST(DetectBakedLoader, EmptyWhenNoGnuLoaderPresent) { - EXPECT_EQ(detect_baked_loader("no loaders here"), ""); - EXPECT_EQ(detect_baked_loader("%{mmusl:/lib/ld-musl-x86_64.so.1}"), ""); -} +UTest empty_when_no_gnu_loader_present{"DetectBakedLoader/EmptyWhenNoGnuLoaderPresent", [] { + expect(eq(detect_baked_loader("no loaders here"), "")); + expect(eq(detect_baked_loader("%{mmusl:/lib/ld-musl-x86_64.so.1}"), "")); +}}; } // namespace @@ -112,45 +112,45 @@ struct GlibcRootFixture { } // namespace -TEST(GlibcPayload, TheExactVersionIsSelected) { +UTest the_exact_version_is_selected{"GlibcPayload/TheExactVersionIsSelected", [] { GlibcRootFixture fx{"mcpp_glibc_exact"}; auto want = make_glibc_payload(fx.root, "2.44.3"); make_glibc_payload(fx.root, "2.44.2"); auto got = mcpp::toolchain::select_glibc_payload_lib(fx.root, "glibc@2.44.3"); - ASSERT_TRUE(got.has_value()) << got.error(); - EXPECT_EQ(*got, want); -} + if (auto&& failed = expect(fatal(got.has_value())); !failed) failed << got.error(); + expect(eq(*got, want)); +}}; // THE CASE OF mcpp#660. Two revisions of the line are present and the declared // version is neither; the answer is "not installed", naming the coordinate // that provides it, and never one of the two. -TEST(GlibcPayload, ANeighbouringRevisionIsNotASubstitute) { +UTest a_neighbouring_revision_is_not_a_substitute{"GlibcPayload/ANeighbouringRevisionIsNotASubstitute", [] { GlibcRootFixture fx{"mcpp_glibc_neighbour"}; make_glibc_payload(fx.root, "2.44.2"); make_glibc_payload(fx.root, "2.44.3"); auto got = mcpp::toolchain::select_glibc_payload_lib(fx.root, "glibc@2.44"); - ASSERT_FALSE(got.has_value()); - EXPECT_NE(got.error().find("xim:glibc@2.44"), std::string::npos) << got.error(); - EXPECT_NE(got.error().find("not installed"), std::string::npos) << got.error(); + expect(fatal(!got.has_value())); + if (auto&& failed = expect(ne(got.error().find("xim:glibc@2.44"), std::string::npos)); !failed) failed << got.error(); + if (auto&& failed = expect(ne(got.error().find("not installed"), std::string::npos)); !failed) failed << got.error(); // One neighbour is not a substitute either: the old tolerance accepted it. GlibcRootFixture one{"mcpp_glibc_one_neighbour"}; make_glibc_payload(one.root, "2.44.2"); - EXPECT_FALSE(mcpp::toolchain::select_glibc_payload_lib(one.root, "glibc@2.44")); -} + expect(!mcpp::toolchain::select_glibc_payload_lib(one.root, "glibc@2.44")); +}}; -TEST(GlibcPayload, NothingInstalledIsStillRefused) { +UTest nothing_installed_is_still_refused{"GlibcPayload/NothingInstalledIsStillRefused", [] { GlibcRootFixture fx{"mcpp_glibc_empty"}; auto got = mcpp::toolchain::select_glibc_payload_lib(fx.root, "glibc@2.44"); - EXPECT_FALSE(got.has_value()); -} + expect(!got.has_value()); +}}; // A DIRECTORY WITHOUT A LOADER IS REPORTED AS INCOMPLETE, NOT AS ABSENT. The // two call for different repairs, so the messages must not coincide. -TEST(GlibcPayload, APayloadWithoutALoaderIsIncomplete) { +UTest a_payload_without_a_loader_is_incomplete{"GlibcPayload/APayloadWithoutALoaderIsIncomplete", [] { GlibcRootFixture fx{"mcpp_glibc_husk"}; std::filesystem::create_directories(fx.root / "2.44.3"); auto got = mcpp::toolchain::select_glibc_payload_lib(fx.root, "glibc@2.44.3"); - ASSERT_FALSE(got.has_value()); - EXPECT_NE(got.error().find("stale/incomplete"), std::string::npos) << got.error(); -} + expect(fatal(!got.has_value())); + if (auto&& failed = expect(ne(got.error().find("stale/incomplete"), std::string::npos)); !failed) failed << got.error(); +}}; diff --git a/tests/unit/test_predefines.cpp b/tests/unit/test_predefines.cpp index 32d7c7dc6..5d883ba26 100644 --- a/tests/unit/test_predefines.cpp +++ b/tests/unit/test_predefines.cpp @@ -9,7 +9,6 @@ // because a set compared by reading is a set compared by sampling --- this // project has already shipped two tables that were "the ones it was missing" // and were not. -#include #include #include @@ -17,9 +16,13 @@ #include #include +import mcpplibs.mtest; + import mcpp.toolchain.predefines; import mcpp.toolchain.triple; +using namespace mcpplibs::mtest; + namespace pd = mcpp::toolchain::predefines; namespace triple = mcpp::toolchain::triple; @@ -48,16 +51,16 @@ bool listed(std::string_view name, std::string_view os) { } // namespace -TEST(Predefines, EveryEmittedMacroIsInTheContract) { +UTest every_emitted_macro_is_in_the_contract{"Predefines/EveryEmittedMacroIsInTheContract", [] { for (auto const& os : {"linux", "windows", "macos", "ios", "android", "none"}) for (bool openkal : {false, true}) for (auto const& m : emitted(os, openkal)) - EXPECT_TRUE(listed(m, os)) + expect(bool(listed(m, os))) << m << " is emitted for os=" << os << " openkal=" << openkal << " and kContract does not list it"; -} +}}; -TEST(Predefines, EveryContractRowThisModuleEmitsIsReachable) { +UTest every_contract_row_this_module_emits_is_reachable{"Predefines/EveryContractRowThisModuleEmitsIsReachable", [] { // `Realisation` rows are emitted by `mcpp.toolchain.cenv`, deliberately, // and are listed here because the contract is about what a READER may // encounter rather than about which module wrote it. @@ -70,21 +73,19 @@ TEST(Predefines, EveryContractRowThisModuleEmitsIsReachable) { if (std::ranges::find(ms, pd::spelling_for(e, os)) != ms.end()) reached = true; } - EXPECT_TRUE(reached) + expect(bool(reached)) << e.spelling << " is in kContract and no configuration emits it"; } -} +}}; -TEST(Predefines, TheTargetMacroIsSpeltFromTheTripleField) { +UTest the_target_macro_is_spelt_from_the_triple_field{"Predefines/TheTargetMacroIsSpeltFromTheTripleField", [] { // The engine learns no operating-system name: a target added to the // triple parser gets its macro without an edit to this module. - EXPECT_EQ(pd::define_tokens("freebsd", false).front(), - "-D__MCPP_TARGET_FREEBSD__=1"); - EXPECT_EQ(pd::define_tokens("none", false).front(), - "-D__MCPP_TARGET_NONE__=1"); -} + expect(eq(pd::define_tokens("freebsd", false).front(), "-D__MCPP_TARGET_FREEBSD__=1")); + expect(eq(pd::define_tokens("none", false).front(), "-D__MCPP_TARGET_NONE__=1")); +}}; -TEST(Predefines, OneMacroPerTargetAndItIsAlwaysPresent) { +UTest one_macro_per_target_and_it_is_always_present{"Predefines/OneMacroPerTargetAndItIsAlwaysPresent", [] { // Conditional emission would make absence ambiguous: "not Windows" and // "Windows, but nothing suppressed its macros" would read the same. for (auto const& os : {"linux", "windows", "macos", "ios", "android", "none"}) { @@ -92,36 +93,36 @@ TEST(Predefines, OneMacroPerTargetAndItIsAlwaysPresent) { const auto n = std::ranges::count_if(ms, [](std::string const& m) { return m.starts_with("__MCPP_TARGET_"); }); - EXPECT_EQ(n, 1) << "os=" << os; + expect(eq(n, 1)) << "os=" << os; } -} +}}; -TEST(Predefines, OpenkalIsTiedToTheResolvedLayerAndNotToAnyTarget) { +UTest openkal_is_tied_to_the_resolved_layer_and_not_to_any_target{"Predefines/OpenkalIsTiedToTheResolvedLayerAndNotToAnyTarget", [] { for (auto const& os : {"linux", "windows", "macos"}) { auto without = emitted(os, false); auto with = emitted(os, true); - EXPECT_EQ(std::ranges::find(without, "__OPENKAL__"), without.end()); - EXPECT_NE(std::ranges::find(with, "__OPENKAL__"), with.end()); + expect(eq(std::ranges::find(without, "__OPENKAL__"), without.end())); + expect(ne(std::ranges::find(with, "__OPENKAL__"), with.end())); } -} +}}; -TEST(Predefines, AnEmptyTargetOsEmitsNoTargetMacro) { +UTest an_empty_target_os_emits_no_target_macro{"Predefines/AnEmptyTargetOsEmitsNoTargetMacro", [] { // A triple that failed to parse must not produce `__MCPP_TARGET___`. auto ms = emitted("", false); - EXPECT_TRUE(ms.empty()); -} + expect(ms.empty()); +}}; -TEST(Predefines, EveryContractRowStatesBothWhatIsAllowedAndWhatIsNot) { +UTest every_contract_row_states_both_what_is_allowed_and_what_is_not{"Predefines/EveryContractRowStatesBothWhatIsAllowedAndWhatIsNot", [] { // A rule with only a permission invites every use its author did not // think of; the `__OPENKAL__` entry has had a forbidden list since it was // written, and the rest follow it. for (auto const& e : pd::kContract) { - EXPECT_FALSE(e.allowed.empty()) << e.spelling; - EXPECT_FALSE(e.forbidden.empty()) << e.spelling; + expect(!e.allowed.empty()) << e.spelling; + expect(!e.forbidden.empty()) << e.spelling; } -} +}}; -TEST(Predefines, EveryOwnedNameIsSpeltInThisProjectsConvention) { +UTest every_owned_name_is_spelt_in_this_projects_convention{"Predefines/EveryOwnedNameIsSpeltInThisProjectsConvention", [] { // Upper case, `__`-wrapped. The convention splits by what a name IS: a // vendor or product name is upper (`__APPLE__`, `_WIN32`), a // kind-of-system name is lower (`__linux__`). Every owned row names a @@ -135,20 +136,20 @@ TEST(Predefines, EveryOwnedNameIsSpeltInThisProjectsConvention) { // and must never be pulled into this project's namespace. for (auto const& e : pd::kContract) { if (!e.owned) { - EXPECT_FALSE(e.spelling.starts_with("__MCPP_")) + expect(!e.spelling.starts_with("__MCPP_")) << e.spelling << " is supplied rather than owned, so it keeps " "the standard spelling"; continue; } - EXPECT_TRUE(e.spelling.starts_with("__") && e.spelling.ends_with("__")) + expect(e.spelling.starts_with("__") && e.spelling.ends_with("__")) << e.spelling << " is owned and is not `__`-wrapped"; for (char c : e.spelling) - EXPECT_FALSE(c >= 'a' && c <= 'z') + expect(!(c >= 'a' && c <= 'z')) << e.spelling << " is owned and carries a lower-case letter"; } -} +}}; -TEST(Predefines, EveryTargetInTheRegistryYieldsAValidIdentifier) { +UTest every_target_in_the_registry_yields_a_valid_identifier{"Predefines/EveryTargetInTheRegistryYieldsAValidIdentifier", [] { // The spelling is derived from the triple's own `os` field, which is why // a target added to the parser needs no edit to `predefines.cppm`. The // price of that is that an `os` carrying a character no identifier may @@ -162,20 +163,19 @@ TEST(Predefines, EveryTargetInTheRegistryYieldsAValidIdentifier) { std::size_t rows = 0; for (auto const& row : triple::known_targets()) { const auto t = triple::parse(row.canonical); - ASSERT_TRUE(t.has_value()) << row.canonical << " does not parse"; + expect(fatal(t.has_value())) << row.canonical << " does not parse"; const auto toks = pd::define_tokens(t->os, false); - ASSERT_EQ(toks.size(), 1u) << row.canonical << " (os=" << t->os << ")"; + if (auto&& failed = expect(fatal(eq(toks.size(), 1u))); !failed) failed << row.canonical << " (os=" << t->os << ")"; const std::string m = macro_of(toks.front()); - EXPECT_FALSE(m.empty()); - EXPECT_TRUE(std::isalpha(static_cast(m.front())) - || m.front() == '_') - << row.canonical << " -> " << m; + expect(!m.empty()); + if (auto&& failed = expect(std::isalpha(static_cast(m.front())) + || m.front() == '_'); !failed) failed << row.canonical << " -> " << m; for (char c : m) - EXPECT_TRUE(std::isalnum(static_cast(c)) || c == '_') + expect(std::isalnum(static_cast(c)) || c == '_') << row.canonical << " -> " << m << " is not a valid identifier"; ++rows; } // A registry that enumerated nothing would pass every assertion above. - EXPECT_GT(rows, 10u) << "the target registry produced " << rows << " rows"; -} + expect(gt(rows, 10u)) << "the target registry produced " << rows << " rows"; +}}; diff --git a/tests/unit/test_prepare_helpers.cpp b/tests/unit/test_prepare_helpers.cpp index a8d52ed2e..cfcaae315 100644 --- a/tests/unit/test_prepare_helpers.cpp +++ b/tests/unit/test_prepare_helpers.cpp @@ -5,61 +5,62 @@ // decomposition of prepare.cppm these were internal to one 16,000-line unit // and reached only through whole builds; they are exported for this file. -#include import std; +import mcpplibs.mtest; import mcpp.build.prepare; +using namespace mcpplibs::mtest; + using Words = std::vector; -TEST(FeatureRequest, TokensSplitOnCommasAndSpaces) { - EXPECT_EQ(mcpp::build::feature_request_tokens("a,b c,, d"), (Words{"a", "b", "c", "d"})); - EXPECT_TRUE(mcpp::build::feature_request_tokens("").empty()); - EXPECT_TRUE(mcpp::build::feature_request_tokens(" , ").empty()); -} +UTest tokens_split_on_commas_and_spaces{"FeatureRequest/TokensSplitOnCommasAndSpaces", [] { + expect(eq(mcpp::build::feature_request_tokens("a,b c,, d"), (Words{"a", "b", "c", "d"}))); + expect(mcpp::build::feature_request_tokens("").empty()); + expect(mcpp::build::feature_request_tokens(" , ").empty()); +}}; // #649 E8: a token with `/` opens a dependency's feature, so it is a forward // of the root and never one of the root's own features. A token with an // empty half stays whole, for the caller to name. -TEST(FeatureRequest, ASlashTokenIsAForwardAndNotARootFeature) { +UTest a_slash_token_is_a_forward_and_not_a_root_feature{"FeatureRequest/ASlashTokenIsAForwardAndNotARootFeature", [] { constexpr std::string_view request = "simd, dep/fast, gpu,/x,y/"; - EXPECT_EQ(mcpp::build::parse_feature_request(request), (Words{"simd", "gpu"})); - EXPECT_EQ(mcpp::build::feature_forward_request_tokens(request), - (Words{"dep/fast", "/x", "y/"})); -} + expect(eq(mcpp::build::parse_feature_request(request), (Words{"simd", "gpu"}))); + expect(eq(mcpp::build::feature_forward_request_tokens(request), (Words{"dep/fast", "/x", "y/"}))); +}}; -TEST(DefineName, IsTheTextBeforeTheFirstEquals) { - EXPECT_EQ(mcpp::build::define_name("NDEBUG"), "NDEBUG"); - EXPECT_EQ(mcpp::build::define_name("VERSION=1"), "VERSION"); - EXPECT_EQ(mcpp::build::define_name("EXPR=a=b"), "EXPR"); - EXPECT_EQ(mcpp::build::define_name("=x"), ""); -} +UTest is_the_text_before_the_first_equals{"DefineName/IsTheTextBeforeTheFirstEquals", [] { + expect(eq(mcpp::build::define_name("NDEBUG"), "NDEBUG")); + expect(eq(mcpp::build::define_name("VERSION=1"), "VERSION")); + expect(eq(mcpp::build::define_name("EXPR=a=b"), "EXPR")); + expect(eq(mcpp::build::define_name("=x"), "")); +}}; // The words an element reached the compiler as before flag_words: `$$` was a // literal dollar, and `${name}` or `$name` a ninja variable, empty on a // compile edge. A `defines` entry was read as `-D`. -TEST(PreviousReleaseWords, ReadNinjaEscapesAndVariablesAsTheOldEdgeDid) { - EXPECT_EQ(mcpp::build::previous_release_words("-DA=x$$y", false), (Words{"-DA=x$y"})); - EXPECT_EQ(mcpp::build::previous_release_words("-DA=${v}z", false), (Words{"-DA=z"})); - EXPECT_EQ(mcpp::build::previous_release_words("-DA=$v", false), (Words{"-DA="})); - EXPECT_EQ(mcpp::build::previous_release_words("B=1", true), (Words{"-DB=1"})); -} +UTest read_ninja_escapes_and_variables_as_the_old_edge_did{"PreviousReleaseWords/ReadNinjaEscapesAndVariablesAsTheOldEdgeDid", [] { + expect(eq(mcpp::build::previous_release_words("-DA=x$$y", false), (Words{"-DA=x$y"}))); + expect(eq(mcpp::build::previous_release_words("-DA=${v}z", false), (Words{"-DA=z"}))); + expect(eq(mcpp::build::previous_release_words("-DA=$v", false), (Words{"-DA="}))); + expect(eq(mcpp::build::previous_release_words("B=1", true), (Words{"-DB=1"}))); +}}; // A remote served by plain filesystem reads is local, so `--offline` keeps // building it; a scheme or scp-like syntax is remote. A Windows drive letter // has a colon but no `@`, so an existing `C:\repo` stays local. -TEST(GitRemote, LocalMeansAFileUrlOrAnExistingPath) { - EXPECT_TRUE(mcpp::build::is_local_git_remote("file:///srv/repo.git")); - EXPECT_FALSE(mcpp::build::is_local_git_remote("https://github.com/x/y.git")); - EXPECT_FALSE(mcpp::build::is_local_git_remote("ssh://git@host/x.git")); - EXPECT_FALSE(mcpp::build::is_local_git_remote("git@github.com:x/y.git")); +UTest local_means_a_file_url_or_an_existing_path{"GitRemote/LocalMeansAFileUrlOrAnExistingPath", [] { + expect(mcpp::build::is_local_git_remote("file:///srv/repo.git")); + expect(!mcpp::build::is_local_git_remote("https://github.com/x/y.git")); + expect(!mcpp::build::is_local_git_remote("ssh://git@host/x.git")); + expect(!mcpp::build::is_local_git_remote("git@github.com:x/y.git")); namespace fs = std::filesystem; const auto dir = fs::temp_directory_path() / std::format("mcpp-prepare-helpers-{:x}", std::random_device{}()); fs::create_directories(dir); - EXPECT_TRUE(mcpp::build::is_local_git_remote(dir.string())); - EXPECT_FALSE(mcpp::build::is_local_git_remote((dir / "missing").string())); + expect(mcpp::build::is_local_git_remote(dir.string())); + expect(!(mcpp::build::is_local_git_remote((dir / "missing").string()))); std::error_code ec; fs::remove_all(dir, ec); -} +}}; diff --git a/tests/unit/test_process_run_exec.cpp b/tests/unit/test_process_run_exec.cpp index 024b2df86..03cc99c10 100644 --- a/tests/unit/test_process_run_exec.cpp +++ b/tests/unit/test_process_run_exec.cpp @@ -1,12 +1,15 @@ -#include #include #include #include import std; +import mcpplibs.mtest; +import mcpplibs.os.stdio; import mcpp.run; import mcpp.invocation; +using namespace mcpplibs::mtest; + namespace process = mcpp::run; // These exercise the POSIX launch path with POSIX program paths (/bin/true, @@ -20,9 +23,9 @@ namespace process = mcpp::run; // change mcpp's own environment — that mutation is exactly what leaked into // /bin/sh and crashed it on newer-glibc hosts. The addition is the started // program's alone; this invocation's statement is what it was. -TEST(RunExec, OutputProducer) { +UTest output_producer{"RunExec/OutputProducer", [] { if (mcpp::invocation::get("MCPP_TEST_OUTPUT_PRODUCER") != "1") - GTEST_SKIP() << "subprocess producer"; + skip("subprocess producer"); std::fputs("mcpp-stderr-ready\n", stdout); std::fflush(stdout); const bool error = mcpp::invocation::get("MCPP_TEST_OUTPUT_STREAM") != "stdout"; @@ -39,13 +42,13 @@ TEST(RunExec, OutputProducer) { std::this_thread::sleep_until(next); } std::_Exit(7); -} +}}; -TEST(RunExec, QueuedOutputRenewsIdleAfterALineHandler) { +UTest queued_output_renews_idle_after_a_line_handler{"RunExec/QueuedOutputRenewsIdleAfterALineHandler", [] { std::vector ticks; const auto result = process::execute({mcpp::invocation::self_exe_path().string(), - "--gtest_filter=RunExec.OutputProducer"}, { - .env = {{"MCPP_TEST_OUTPUT_PRODUCER", "1"}, {"MCPP_TEST_OUTPUT_STREAM", "stdout"}, {"MCPP_TEST_OUTPUT_TICKS", "48"}}, + "RunExec/OutputProducer"}, { + .env = {{"MTEST_PROTOCOL", ""}, {"MCPP_TEST_OUTPUT_PRODUCER", "1"}, {"MCPP_TEST_OUTPUT_STREAM", "stdout"}, {"MCPP_TEST_OUTPUT_TICKS", "48"}}, .streams = process::Streams::merged, .total = std::chrono::seconds(3), .idle = std::chrono::milliseconds(500), .on_line = [&](std::string_view line) { @@ -55,30 +58,30 @@ TEST(RunExec, QueuedOutputRenewsIdleAfterALineHandler) { if (std::from_chars(line.data(), line.data() + line.size(), tick).ec == std::errc{}) ticks.push_back(tick); }}); - EXPECT_EQ(result.exit_code, 7); - EXPECT_FALSE(result.timed_out); - EXPECT_EQ(ticks.size(), 48u); -} + expect(eq(result.exit_code, 7)); + expect(!result.timed_out); + expect(eq(ticks.size(), 48u)); +}}; -TEST(RunExec, QueuedErrorOutputRenewsIdleAfterALineHandler) { +UTest queued_error_output_renews_idle_after_a_line_handler{"RunExec/QueuedErrorOutputRenewsIdleAfterALineHandler", [] { const auto result = process::execute({mcpp::invocation::self_exe_path().string(), - "--gtest_filter=RunExec.OutputProducer"}, { - .env = {{"MCPP_TEST_OUTPUT_PRODUCER", "1"}, {"MCPP_TEST_OUTPUT_TICKS", "48"}}, + "RunExec/OutputProducer"}, { + .env = {{"MTEST_PROTOCOL", ""}, {"MCPP_TEST_OUTPUT_PRODUCER", "1"}, {"MCPP_TEST_OUTPUT_TICKS", "48"}}, .streams = process::Streams::output_error_kept, .total = std::chrono::seconds(3), .idle = std::chrono::milliseconds(500), .on_line = [](std::string_view line) { if (line == "mcpp-stderr-ready") std::this_thread::sleep_for(std::chrono::milliseconds(750)); }}); - EXPECT_EQ(result.exit_code, 7) << result.error_output; - EXPECT_FALSE(result.timed_out); + expect(eq(result.exit_code, 7)) << result.error_output; + expect(!result.timed_out); std::istringstream input(result.error_output); int ticks = 0; for (long long tick; input >> tick;) ++ticks; - EXPECT_EQ(ticks, 48); -} + expect(eq(ticks, 48)); +}}; -TEST(RunExec, StreamCompletionAndChildWaitKeepTheirBounds) { +UTest stream_completion_and_child_wait_keep_their_bounds{"RunExec/StreamCompletionAndChildWaitKeepTheirBounds", [] { struct Case { const char* name; const char* command; @@ -102,7 +105,7 @@ TEST(RunExec, StreamCompletionAndChildWaitKeepTheirBounds) { {"stdout-EOF-total", "exec 1>&-; sleep 2", 50}, }; auto exercise = [](process::Streams streams, const Case& test) { - SCOPED_TRACE(std::format("stream={} case={}", static_cast(streams), test.name)); + /* trace: std::format("stream={} case={}", static_cast(streams), test.name) */; const auto started = std::chrono::steady_clock::now(); process::Options options; options.streams = streams; @@ -113,33 +116,34 @@ TEST(RunExec, StreamCompletionAndChildWaitKeepTheirBounds) { }; const bool producer = std::string_view(test.command) == "@stderr-producer"; if (producer) options.env.emplace_back("MCPP_TEST_OUTPUT_PRODUCER", "1"); + if (producer) options.env.emplace_back("MTEST_PROTOCOL", ""); const std::vector argv = producer ? std::vector{mcpp::invocation::self_exe_path().string(), - "--gtest_filter=RunExec.OutputProducer"} + "RunExec/OutputProducer"} : std::vector{"/bin/sh", "-c", test.command}; const auto result = process::execute(argv, options); if (test.exit >= 0) { - EXPECT_EQ(result.exit_code, test.exit); - EXPECT_FALSE(result.timed_out); + expect(eq(result.exit_code, test.exit)); + expect(!result.timed_out); } else { - EXPECT_NE(result.exit_code, 0); - EXPECT_TRUE(result.timed_out); + expect(ne(result.exit_code, 0)); + expect(bool(result.timed_out)); } const auto elapsed = std::chrono::steady_clock::now() - started; if (producer) { // One process emits across the idle window. Forking an external // sleep for every tick would add platform-dependent launch gaps. - SCOPED_TRACE(result.error_output); + /* trace: result.error_output */; std::istringstream input(result.error_output); std::vector ticks; for (long long tick; input >> tick;) ticks.push_back(tick); - ASSERT_EQ(ticks.size(), 24u); + expect(fatal(eq(ticks.size(), 24u))); for (std::size_t i = 1; i < ticks.size(); ++i) - EXPECT_LT(std::chrono::nanoseconds(ticks[i] - ticks[i - 1]), options.idle); - EXPECT_GT(elapsed, options.idle); - EXPECT_LT(elapsed, std::chrono::seconds(3)); + expect(lt(std::chrono::nanoseconds(ticks[i] - ticks[i - 1]), options.idle)); + expect(gt(elapsed, options.idle)); + expect(lt(elapsed, std::chrono::seconds(3))); } else { - EXPECT_LT(elapsed, std::chrono::seconds(1)); + expect(lt(elapsed, std::chrono::seconds(1))); } }; for (const auto streams : {process::Streams::inherit, process::Streams::discard, @@ -151,21 +155,21 @@ TEST(RunExec, StreamCompletionAndChildWaitKeepTheirBounds) { } exercise(process::Streams::output_error_kept, {"stderr-activity", "@stderr-producer", 3000, 500, false, 7}); -} +}}; -TEST(RunExec, StopPredicateAppliesAfterCapturedStreamsClose) { +UTest stop_predicate_applies_after_captured_streams_close{"RunExec/StopPredicateAppliesAfterCapturedStreamsClose", [] { const auto started = std::chrono::steady_clock::now(); const auto result = process::execute({"/bin/sh", "-c", "exec 1>&- 2>&-; sleep 2"}, { .streams = process::Streams::merged, .should_stop = [&] { return std::chrono::steady_clock::now() - started > std::chrono::milliseconds(50); }}); - EXPECT_TRUE(result.timed_out); - EXPECT_NE(result.exit_code, 0); - EXPECT_LT(std::chrono::steady_clock::now() - started, std::chrono::seconds(1)); -} + expect(bool(result.timed_out)); + expect(ne(result.exit_code, 0)); + expect(lt(std::chrono::steady_clock::now() - started, std::chrono::seconds(1))); +}}; -TEST(RunExec, StopPredicateAppliesWithoutCapturedStreams) { +UTest stop_predicate_applies_without_captured_streams{"RunExec/StopPredicateAppliesWithoutCapturedStreams", [] { for (const auto streams : {process::Streams::inherit, process::Streams::discard}) { const auto started = std::chrono::steady_clock::now(); int checks = 0; @@ -175,60 +179,60 @@ TEST(RunExec, StopPredicateAppliesWithoutCapturedStreams) { ++checks; return std::chrono::steady_clock::now() - started > std::chrono::milliseconds(50); }}); - EXPECT_GT(checks, 0); - EXPECT_TRUE(result.timed_out); - EXPECT_NE(result.exit_code, 0); - EXPECT_LT(std::chrono::steady_clock::now() - started, std::chrono::seconds(1)); + expect(gt(checks, 0)); + expect(bool(result.timed_out)); + expect(ne(result.exit_code, 0)); + expect(lt(std::chrono::steady_clock::now() - started, std::chrono::seconds(1))); } -} +}}; -TEST(RunExec, DoesNotMutateParentEnvironment) { +UTest does_not_mutate_parent_environment{"RunExec/DoesNotMutateParentEnvironment", [] { mcpp::invocation::ScopedEnv leak("MCPP_TEST_LEAK", "sentinel"); // Use /bin/sh (present on Linux AND macOS) — /bin/true lives at // /usr/bin/true on macOS, so it is not a portable launch target. int rc = process::run_exec({"/bin/sh", "-c", "[ \"$MCPP_TEST_LEAK\" = injected ]"}, {{"MCPP_TEST_LEAK", "injected"}}); - EXPECT_EQ(rc, 0); - EXPECT_EQ(mcpp::invocation::get("MCPP_TEST_LEAK"), "sentinel"); // unchanged → no leak -} + expect(eq(rc, 0)); + expect(eq(mcpp::invocation::get("MCPP_TEST_LEAK"), "sentinel")); // unchanged → no leak +}}; -TEST(RunExec, ChildSeesInjectedEnv) { +UTest child_sees_injected_env{"RunExec/ChildSeesInjectedEnv", [] { // `sh -c '[ "$X" = injected ]'` exits 0 only if the child received X. int rc = process::run_exec( {"/bin/sh", "-c", "[ \"$MCPP_TEST_INJECT\" = injected ]"}, {{"MCPP_TEST_INJECT", "injected"}}); - EXPECT_EQ(rc, 0); -} + expect(eq(rc, 0)); +}}; -TEST(RunExec, PropagatesChildExitCode) { - EXPECT_EQ(process::run_exec({"/bin/sh", "-c", "exit 7"}), 7); -} +UTest propagates_child_exit_code{"RunExec/PropagatesChildExitCode", [] { + expect(eq(process::run_exec({"/bin/sh", "-c", "exit 7"}), 7)); +}}; -TEST(RunExec, ReturnsErrorWhenProgramMissing) { - EXPECT_NE(process::run_exec({"/no/such/program/mcpp-xyz"}), 0); -} +UTest returns_error_when_program_missing{"RunExec/ReturnsErrorWhenProgramMissing", [] { + expect(ne(process::run_exec({"/no/such/program/mcpp-xyz"}), 0)); +}}; -TEST(CaptureExec, MissingProgramReportsSpawnFailure) { +UTest missing_program_reports_spawn_failure{"CaptureExec/MissingProgramReportsSpawnFailure", [] { constexpr std::string_view missing = "/no/such/program/mcpp-capture-xyz"; auto r = process::capture_exec({std::string(missing)}); - EXPECT_EQ(r.exit_code, 127); - EXPECT_NE(r.output.find(missing), std::string::npos) << r.output; - EXPECT_NE(r.output.find("error 2"), std::string::npos) << r.output; -} + expect(eq(r.exit_code, 127)); + expect(ne(r.output.find(missing), std::string::npos)) << r.output; + expect(ne(r.output.find("error 2"), std::string::npos)) << r.output; +}}; -TEST(CaptureExec, CapturesStdoutWithoutShell) { +UTest captures_stdout_without_shell{"CaptureExec/CapturesStdoutWithoutShell", [] { auto r = process::capture_exec({"/bin/echo", "hello world"}); - EXPECT_EQ(r.exit_code, 0); - EXPECT_EQ(r.output, "hello world\n"); -} + expect(eq(r.exit_code, 0)); + expect(eq(r.output, "hello world\n")); +}}; -TEST(CaptureExec, CapturesStderrCombined) { +UTest captures_stderr_combined{"CaptureExec/CapturesStderrCombined", [] { // stderr must be merged into output (replaces the old `2>&1`), otherwise // ninja's error text would be lost from the fast-path stale detector. auto r = process::capture_exec({"/bin/sh", "-c", "echo oops 1>&2"}); - EXPECT_EQ(r.exit_code, 0); - EXPECT_EQ(r.output, "oops\n"); -} + expect(eq(r.exit_code, 0)); + expect(eq(r.output, "oops\n")); +}}; // #248 regression: extraEnv AND cwd BOTH non-empty — the previously untested // combination (every prior call site passed at most one of the two; the only @@ -237,29 +241,29 @@ TEST(CaptureExec, CapturesStderrCombined) { // first command (`cd`) across `&&` — so the real program received NO extra env // and build.mcpp lost the whole MCPP_* contract. Linux and macOS now share the // posix_spawn path, so this locks the contract on both. -TEST(CaptureExec, EnvAndCwdCombinedBothReachChild) { +UTest env_and_cwd_combined_both_reach_child{"CaptureExec/EnvAndCwdCombinedBothReachChild", [] { auto dir = std::filesystem::temp_directory_path() / "mcpp_248_env_cwd"; std::filesystem::create_directories(dir); auto r = process::capture_exec( {"/bin/sh", "-c", "printf '%s|%s' \"$MCPP_TEST_CONTRACT\" \"$PWD\""}, {{"MCPP_TEST_CONTRACT", "hello"}}, dir.string()); - EXPECT_EQ(r.exit_code, 0); + expect(eq(r.exit_code, 0)); // Env value must reach the child (the old prefix bound it to `cd` only). - EXPECT_NE(r.output.find("hello|"), std::string::npos) << r.output; + expect(ne(r.output.find("hello|"), std::string::npos)) << r.output; // Child must actually run in `cwd`. Compare the leaf name, not the full // path: macOS resolves /var → /private/var in $PWD. - EXPECT_NE(r.output.find("mcpp_248_env_cwd"), std::string::npos) << r.output; -} + expect(ne(r.output.find("mcpp_248_env_cwd"), std::string::npos)) << r.output; +}}; // Companion check for run_exec (no cwd parameter there, but extraEnv must // still land in the child via the same merged_environ posix_spawn path). -TEST(RunExec, InjectedEnvSurvivesShellChildChain) { +UTest injected_env_survives_shell_child_chain{"RunExec/InjectedEnvSurvivesShellChildChain", [] { int rc = process::run_exec( {"/bin/sh", "-c", "[ \"$MCPP_TEST_CONTRACT\" = hello ]"}, {{"MCPP_TEST_CONTRACT", "hello"}}); - EXPECT_EQ(rc, 0); -} + expect(eq(rc, 0)); +}}; // ── spawn failures are typed, reported once, and never retried (#544) ────── // @@ -271,24 +275,24 @@ TEST(RunExec, InjectedEnvSurvivesShellChildChain) { // aarch64-linux-musl` on a host without an emulator printed "Running …", a // blank line, and exited 1. -TEST(RunExec, MissingProgramReportsOnStderrWhenCallerDoesNotAsk) { - testing::internal::CaptureStderr(); +UTest missing_program_reports_on_stderr_when_caller_does_not_ask{"RunExec/MissingProgramReportsOnStderrWhenCallerDoesNotAsk", [] { + auto stderrCapture = mcpplibs::os::stdio::capture::begin(mcpplibs::os::stdio::stream::error).value(); int rc = process::run_exec({"/no/such/program/mcpp-run-xyz"}); - auto err = testing::internal::GetCapturedStderr(); - EXPECT_EQ(rc, 127); - EXPECT_NE(err.find("/no/such/program/mcpp-run-xyz"), std::string::npos) << err; - EXPECT_NE(err.find("error 2"), std::string::npos) << err; -} + auto err = stderrCapture.text(); + expect(eq(rc, 127)); + expect(ne(err.find("/no/such/program/mcpp-run-xyz"), std::string::npos)) << err; + expect(ne(err.find("error 2"), std::string::npos)) << err; +}}; -TEST(RunExec, MissingProgramTypesErrnoWhenCallerAsks) { +UTest missing_program_types_errno_when_caller_asks{"RunExec/MissingProgramTypesErrnoWhenCallerAsks", [] { int spawnErr = 0; - testing::internal::CaptureStderr(); + auto stderrCapture = mcpplibs::os::stdio::capture::begin(mcpplibs::os::stdio::stream::error).value(); int rc = process::run_exec({"/no/such/program/mcpp-run-xyz"}, {}, &spawnErr); - auto err = testing::internal::GetCapturedStderr(); - EXPECT_EQ(rc, 127); - EXPECT_EQ(spawnErr, ENOENT); - EXPECT_TRUE(err.empty()) << err; // the caller owns the report -} + auto err = stderrCapture.text(); + expect(eq(rc, 127)); + expect(eq(spawnErr, ENOENT)); + expect(err.empty()) << err; // the caller owns the report +}}; #if defined(__linux__) // Linux only, and that is a MEASUREMENT (CI, macOS 14 ARM64, 2026-09-02): on @@ -299,7 +303,7 @@ TEST(RunExec, MissingProgramTypesErrnoWhenCallerAsks) { // synthesises. `mcpp run`'s "this host cannot execute" typing therefore fires // on Linux for any unloadable file and on macOS only for a real foreign // binary; see the design's open question 1. -TEST(RunExec, UnloadableArtifactIsENOEXEC) { +UTest unloadable_artifact_is_enoexec{"RunExec/UnloadableArtifactIsENOEXEC", [] { // An executable file with no loader magic: the kernel refuses it with // ENOEXEC. glibc's posix_spawnp does not retry through /bin/sh the way // execvp does, so the errno reaches the caller untouched. This is the @@ -311,120 +315,120 @@ TEST(RunExec, UnloadableArtifactIsENOEXEC) { std::filesystem::permissions(f, std::filesystem::perms::owner_all); int spawnErr = 0; int rc = process::run_exec({f.string()}, {}, &spawnErr); - EXPECT_EQ(rc, 127); - EXPECT_EQ(spawnErr, ENOEXEC); -} + expect(eq(rc, 127)); + expect(eq(spawnErr, ENOEXEC)); +}}; #endif -TEST(RunExec, SpawnedChildLeavesSpawnErrorZero) { +UTest spawned_child_leaves_spawn_error_zero{"RunExec/SpawnedChildLeavesSpawnErrorZero", [] { int spawnErr = -1; - EXPECT_EQ(process::run_exec({"/bin/sh", "-c", "exit 7"}, {}, &spawnErr), 7); - EXPECT_EQ(spawnErr, 0); -} + expect(eq(process::run_exec({"/bin/sh", "-c", "exit 7"}, {}, &spawnErr), 7)); + expect(eq(spawnErr, 0)); +}}; -TEST(CaptureExec, MissingProgramTypesErrnoWhenCallerAsks) { +UTest capture_exec_missing_program_types_errno_when_caller_asks{"CaptureExec/MissingProgramTypesErrnoWhenCallerAsks", [] { int spawnErr = 0; auto r = process::capture_exec({"/no/such/program/mcpp-capture-typed"}, {}, {}, &spawnErr); - EXPECT_EQ(r.exit_code, 127); - EXPECT_EQ(spawnErr, ENOENT); - EXPECT_TRUE(r.output.empty()) << r.output; -} + expect(eq(r.exit_code, 127)); + expect(eq(spawnErr, ENOENT)); + expect(r.output.empty()) << r.output; +}}; -TEST(RunExecDeadline, SpawnFailureIsTypedAndNotRetried) { +UTest spawn_failure_is_typed_and_not_retried{"RunExecDeadline/SpawnFailureIsTypedAndNotRetried", [] { int spawnErr = 0; bool timedOut = true; - testing::internal::CaptureStderr(); + auto stderrCapture = mcpplibs::os::stdio::capture::begin(mcpplibs::os::stdio::stream::error).value(); int rc = process::run_exec_deadline({"/no/such/program/mcpp-dl-xyz"}, {}, std::chrono::milliseconds(5000), &timedOut, &spawnErr); - auto err = testing::internal::GetCapturedStderr(); - EXPECT_EQ(rc, 127); - EXPECT_EQ(spawnErr, ENOENT); - EXPECT_FALSE(timedOut); - EXPECT_TRUE(err.empty()) << err; -} - -TEST(RunExecDeadline, SpawnFailureIsReportedWhenCallerDoesNotAsk) { + auto err = stderrCapture.text(); + expect(eq(rc, 127)); + expect(eq(spawnErr, ENOENT)); + expect(!timedOut); + expect(err.empty()) << err; +}}; + +UTest spawn_failure_is_reported_when_caller_does_not_ask{"RunExecDeadline/SpawnFailureIsReportedWhenCallerDoesNotAsk", [] { bool timedOut = true; - testing::internal::CaptureStderr(); + auto stderrCapture = mcpplibs::os::stdio::capture::begin(mcpplibs::os::stdio::stream::error).value(); int rc = process::run_exec_deadline({"/no/such/program/mcpp-dl-untyped"}, {}, std::chrono::milliseconds(5000), &timedOut); - auto err = testing::internal::GetCapturedStderr(); - EXPECT_EQ(rc, 127); - EXPECT_FALSE(timedOut); - EXPECT_NE(err.find("mcpp-dl-untyped"), std::string::npos) << err; - EXPECT_NE(err.find("error 2"), std::string::npos) << err; -} - -TEST(CaptureExecDeadline, SpawnFailureIsTypedInOutcome) { + auto err = stderrCapture.text(); + expect(eq(rc, 127)); + expect(!timedOut); + expect(ne(err.find("mcpp-dl-untyped"), std::string::npos)) << err; + expect(ne(err.find("error 2"), std::string::npos)) << err; +}}; + +UTest spawn_failure_is_typed_in_outcome{"CaptureExecDeadline/SpawnFailureIsTypedInOutcome", [] { int spawnErr = 0; bool timedOut = true; auto r = process::capture_exec_deadline({"/no/such/program/mcpp-cdl-xyz"}, {}, std::chrono::milliseconds(5000), &timedOut, {}, &spawnErr); - EXPECT_EQ(r.exit_code, 127); - EXPECT_EQ(spawnErr, ENOENT); - EXPECT_FALSE(timedOut); - EXPECT_TRUE(r.output.empty()) << r.output; -} + expect(eq(r.exit_code, 127)); + expect(eq(spawnErr, ENOENT)); + expect(!timedOut); + expect(r.output.empty()) << r.output; +}}; -TEST(CaptureExecDeadline, SpawnFailureIsInOutputWhenCallerDoesNotAsk) { +UTest spawn_failure_is_in_output_when_caller_does_not_ask{"CaptureExecDeadline/SpawnFailureIsInOutputWhenCallerDoesNotAsk", [] { bool timedOut = true; auto r = process::capture_exec_deadline({"/no/such/program/mcpp-cdl-untyped"}, {}, std::chrono::milliseconds(5000), &timedOut); - EXPECT_EQ(r.exit_code, 127); - EXPECT_NE(r.output.find("mcpp-cdl-untyped"), std::string::npos) << r.output; - EXPECT_NE(r.output.find("error 2"), std::string::npos) << r.output; -} + expect(eq(r.exit_code, 127)); + expect(ne(r.output.find("mcpp-cdl-untyped"), std::string::npos)) << r.output; + expect(ne(r.output.find("error 2"), std::string::npos)) << r.output; +}}; // ── capture_stdout: the argument-vector form of ` 2>/dev/null` ── -TEST(CaptureStdout, CapturesStandardOutputOnly) { +UTest captures_standard_output_only{"CaptureStdout/CapturesStandardOutputOnly", [] { auto r = process::capture_stdout( {"/bin/sh", "-c", "echo out; echo 9.9.9-on-stderr 1>&2"}); - EXPECT_EQ(r.exit_code, 0); - EXPECT_EQ(r.output, "out\n"); -} + expect(eq(r.exit_code, 0)); + expect(eq(r.output, "out\n")); +}}; -TEST(CaptureStdout, PropagatesExitCode) { +UTest propagates_exit_code{"CaptureStdout/PropagatesExitCode", [] { auto r = process::capture_stdout({"/bin/sh", "-c", "echo partial; exit 3"}); - EXPECT_EQ(r.exit_code, 3); - EXPECT_EQ(r.output, "partial\n"); -} + expect(eq(r.exit_code, 3)); + expect(eq(r.output, "partial\n")); +}}; // A probe parses what it captured, so a launcher message must never be read // as the program's answer: a missing program leaves the output empty. -TEST(CaptureStdout, MissingProgramLeavesOutputEmpty) { +UTest missing_program_leaves_output_empty{"CaptureStdout/MissingProgramLeavesOutputEmpty", [] { int spawnErr = 0; auto r = process::capture_stdout({"/no/such/program/mcpp-probe-2026.1.2.3"}, {}, &spawnErr); - EXPECT_EQ(r.exit_code, 127); - EXPECT_EQ(spawnErr, ENOENT); - EXPECT_TRUE(r.output.empty()) << r.output; -} + expect(eq(r.exit_code, 127)); + expect(eq(spawnErr, ENOENT)); + expect(r.output.empty()) << r.output; +}}; -TEST(CaptureStdout, StandardInputIsEmpty) { +UTest standard_input_is_empty{"CaptureStdout/StandardInputIsEmpty", [] { // `cat` returns at once with nothing when stdin is /dev/null; with an // inherited terminal or pipe it would block or echo the parent's input. auto r = process::capture_stdout({"/bin/sh", "-c", "cat; echo done"}); - EXPECT_EQ(r.exit_code, 0); - EXPECT_EQ(r.output, "done\n"); -} + expect(eq(r.exit_code, 0)); + expect(eq(r.output, "done\n")); +}}; -TEST(CaptureStdout, ExtraEnvironmentReachesTheChild) { +UTest extra_environment_reaches_the_child{"CaptureStdout/ExtraEnvironmentReachesTheChild", [] { auto r = process::capture_stdout({"/bin/sh", "-c", "printf %s \"$MCPP_PROBE_ENV\""}, {{"MCPP_PROBE_ENV", "reached"}}); - EXPECT_EQ(r.exit_code, 0); - EXPECT_EQ(r.output, "reached"); -} + expect(eq(r.exit_code, 0)); + expect(eq(r.output, "reached")); +}}; #else // _WIN32 -TEST(RunExec, WindowsCoveredByIntegration) { +UTest windows_covered_by_integration{"RunExec/WindowsCoveredByIntegration", [] { // The Windows path uses _spawnvpe (no /bin/* programs to point at here); // it is exercised by the integration build that launches ninja. - SUCCEED(); -} + /* passes */; +}}; #endif diff --git a/tests/unit/test_process_seal_stdin.cpp b/tests/unit/test_process_seal_stdin.cpp index 62097b77a..463a13252 100644 --- a/tests/unit/test_process_seal_stdin.cpp +++ b/tests/unit/test_process_seal_stdin.cpp @@ -12,14 +12,16 @@ // wait for as long as the pipe is open, and the copy with it; the outer test // bounds that wait. -#include import std; +import mcpplibs.mtest; import mcpp.run; import mcpp.invocation; import mcpplibs.os.platform; import mcpplibs.os.process; +using namespace mcpplibs::mtest; + namespace { namespace os = mcpplibs::os; @@ -40,9 +42,9 @@ std::optional child_mode() { return mcpp::invocation::get("MCPP_TES // The copy started by the test below: starts the reader through the launcher // it is named, with this process's input the pipe nobody writes. -TEST(ProcessSealStdin, Child) { +UTest process_seal_stdin_child{"ProcessSealStdin/Child", [] { const auto mode = child_mode(); - if (!mode) GTEST_SKIP() << "run by AProgramReadsAnEmptyInput"; + if (!mode) skip("run by AProgramReadsAnEmptyInput"); if (*mode == "execute") { (void)mcpp::run::execute(reader(), { .streams = mcpp::run::Streams::discard }); } else if (*mode == "stream") { @@ -53,26 +55,26 @@ TEST(ProcessSealStdin, Child) { } else if (*mode == "shell") { (void)mcpp::run::capture_shell(os::is_windows ? "more >nul 2>&1" : "cat >/dev/null"); } -} +}}; -TEST(ProcessSealStdin, AProgramReadsAnEmptyInput) { - if (child_mode()) GTEST_SKIP(); +UTest a_program_reads_an_empty_input{"ProcessSealStdin/AProgramReadsAnEmptyInput", [] { + if (child_mode()) skip(""); for (std::string mode : { "execute", "stream", "probe", "shell" }) { os::process::spawn_options o; o.program = mcpp::invocation::self_exe_path(); - o.args = { "--gtest_filter=ProcessSealStdin.Child" }; - o.env = { { "MCPP_TEST_STDIN_CHILD", mode } }; + o.args = { "ProcessSealStdin/Child" }; + o.env = { { "MCPP_TEST_STDIN_CHILD", mode }, { "MTEST_PROTOCOL", "" } }; o.in = os::process::stdio::pipe; // held open, never written o.out = o.err = os::process::stdio::null; o.own_unit = true; auto p = os::process::process::spawn(o); - ASSERT_TRUE(p) << mode; + expect(fatal(bool(p))) << mode; auto w = p->wait_for(kMaxSealed); if (!w || !w->has_value()) { p->end_unit(); p->kill(); - FAIL() << mode << ": the started program waited on mcpp's input"; + fail([&] { std::ostringstream text; text << mode << ": the started program waited on mcpp's input"; return text.str(); }()); } - EXPECT_TRUE((*w)->success()) << mode; + expect(bool((*w)->success())) << mode; } -} +}}; diff --git a/tests/unit/test_progress_render.cpp b/tests/unit/test_progress_render.cpp index aaf6d09b5..7048d944a 100644 --- a/tests/unit/test_progress_render.cpp +++ b/tests/unit/test_progress_render.cpp @@ -1,37 +1,40 @@ -#include import std; +import mcpplibs.mtest; +import mcpplibs.os.stdio; import mcpp.ui; import mcpp.fetcher.progress; import mcpp.run; import mcpplibs.os.platform; +using namespace mcpplibs::mtest; + // W11: one renderer for every acquisition, with a terminal mode (redrawn in // place) and a plain mode; either prints one line per item (#734). The renderer // narrates, and narration is on standard error (output streams plan // 2026-10-01, R3), which is what these tests capture. -TEST(GitProgress, ParsesThePhaseAndThePercentage) { +UTest parses_the_phase_and_the_percentage{"GitProgress/ParsesThePhaseAndThePercentage", [] { auto r = mcpp::fetcher::parse_git_progress( "Receiving objects: 45% (450/1000), 1.20 MiB | 800.00 KiB/s"); - ASSERT_TRUE(r.has_value()); - EXPECT_EQ(r->phase, "Receiving objects"); - EXPECT_EQ(r->percent, 45u); + expect(fatal(r.has_value())); + expect(eq(r->phase, "Receiving objects")); + expect(eq(r->percent, 45u)); auto remote = mcpp::fetcher::parse_git_progress("remote: Counting objects: 100% (5/5), done."); - ASSERT_TRUE(remote.has_value()); - EXPECT_EQ(remote->phase, "Counting objects"); - EXPECT_EQ(remote->percent, 100u); + expect(fatal(remote.has_value())); + expect(eq(remote->phase, "Counting objects")); + expect(eq(remote->percent, 100u)); - EXPECT_FALSE(mcpp::fetcher::parse_git_progress("Cloning into 'x'...").has_value()); - EXPECT_FALSE(mcpp::fetcher::parse_git_progress("fatal: repository not found").has_value()); - EXPECT_FALSE(mcpp::fetcher::parse_git_progress("").has_value()); -} + expect(!(mcpp::fetcher::parse_git_progress("Cloning into 'x'...").has_value())); + expect(!(mcpp::fetcher::parse_git_progress("fatal: repository not found").has_value())); + expect(!(mcpp::fetcher::parse_git_progress("").has_value())); +}}; -TEST(ProgressBarPlain, OneCompletionLineWithoutRepaints) { +UTest one_completion_line_without_repaints{"ProgressBarPlain/OneCompletionLineWithoutRepaints", [] { mcpp::ui::disable_color(); mcpp::ui::set_live_progress(false); - testing::internal::CaptureStderr(); + auto stderrCapture = mcpplibs::os::stdio::capture::begin(mcpplibs::os::stdio::stream::error).value(); { mcpp::ui::ProgressBar bar("Fetching", "example"); bar.update_bytes(10, 100, 0.1); @@ -39,65 +42,65 @@ TEST(ProgressBarPlain, OneCompletionLineWithoutRepaints) { bar.update_bytes(100, 100, 1.0); bar.finish(); } - auto out = testing::internal::GetCapturedStderr(); - EXPECT_EQ(out.find('\r'), std::string::npos) << out; - EXPECT_EQ(out.find('\x1b'), std::string::npos) << out; + auto out = stderrCapture.text(); + expect(eq(out.find('\r'), std::string::npos)) << out; + expect(eq(out.find('\x1b'), std::string::npos)) << out; // #734: one line per item, stating the size and the time. - EXPECT_EQ(std::ranges::count(out, '\n'), 1) << out; - EXPECT_NE(out.find("Fetching example done, 100 B in"), std::string::npos) << out; -} + expect(eq(std::ranges::count(out, '\n'), 1)) << out; + expect(ne(out.find("Fetching example done, 100 B in"), std::string::npos)) << out; +}}; -TEST(ProgressBarPlain, AFailedItemIsNotReportedDone) { +UTest a_failed_item_is_not_reported_done{"ProgressBarPlain/AFailedItemIsNotReportedDone", [] { mcpp::ui::disable_color(); mcpp::ui::set_live_progress(false); - testing::internal::CaptureStderr(); + auto stderrCapture = mcpplibs::os::stdio::capture::begin(mcpplibs::os::stdio::stream::error).value(); { mcpp::ui::ProgressBar bar("Installing", "xim:example"); bar.update_indeterminate(0, 0.2); bar.finish_failed("xim:example"); } - auto out = testing::internal::GetCapturedStderr(); - EXPECT_NE(out.find("did not complete"), std::string::npos) << out; - EXPECT_EQ(out.find(" done"), std::string::npos) << out; -} + auto out = stderrCapture.text(); + expect(ne(out.find("did not complete"), std::string::npos)) << out; + expect(eq(out.find(" done"), std::string::npos)) << out; +}}; -TEST(DownloadProgressPlain, AnInterruptedDownloadIsNotReportedDone) { +UTest an_interrupted_download_is_not_reported_done{"DownloadProgressPlain/AnInterruptedDownloadIsNotReportedDone", [] { mcpp::ui::disable_color(); mcpp::ui::set_live_progress(false); - testing::internal::CaptureStderr(); + auto stderrCapture = mcpplibs::os::stdio::capture::begin(mcpplibs::os::stdio::stream::error).value(); { mcpp::ui::DownloadProgress dl; const mcpp::ui::DownloadFile f{"xim-index.tar.gz", 10, 100, true, false}; dl.update(std::span{&f, 1}, 0.2); dl.finish_failed(); } - auto out = testing::internal::GetCapturedStderr(); - EXPECT_NE(out.find("xim-index.tar.gz did not complete"), std::string::npos) << out; - EXPECT_EQ(out.find(" done"), std::string::npos) << out; -} + auto out = stderrCapture.text(); + expect(ne(out.find("xim-index.tar.gz did not complete"), std::string::npos)) << out; + expect(eq(out.find(" done"), std::string::npos)) << out; +}}; -TEST(ProgressBarLive, RedrawsInPlace) { +UTest redraws_in_place{"ProgressBarLive/RedrawsInPlace", [] { mcpp::ui::disable_color(); mcpp::ui::set_live_progress(true); - testing::internal::CaptureStderr(); + auto stderrCapture = mcpplibs::os::stdio::capture::begin(mcpplibs::os::stdio::stream::error).value(); { mcpp::ui::ProgressBar bar("Fetching", "example"); bar.update(100); bar.finish(); } - auto out = testing::internal::GetCapturedStderr(); + auto out = stderrCapture.text(); mcpp::ui::set_live_progress(false); - EXPECT_NE(out.find('\r'), std::string::npos) << out; -} + expect(ne(out.find('\r'), std::string::npos)) << out; +}}; -TEST(StreamingBounded, ACarriageReturnEndsALineOnlyWhenAsked) { - if constexpr (mcpplibs::os::is_windows) GTEST_SKIP() << "printf is a POSIX program"; +UTest a_carriage_return_ends_a_line_only_when_asked{"StreamingBounded/ACarriageReturnEndsALineOnlyWhenAsked", [] { + if constexpr (mcpplibs::os::is_windows) skip("printf is a POSIX program"); const std::vector argv{ "printf", "a\\rb\\r\\nc\\n" }; std::vector plain, split; (void)mcpp::run::execute(argv, { .idle = std::chrono::seconds{30}, .on_line = [&](std::string_view l) { plain.emplace_back(l); } }); (void)mcpp::run::execute(argv, { .idle = std::chrono::seconds{30}, .on_line = [&](std::string_view l) { split.emplace_back(l); }, .split_on_cr = true }); - EXPECT_EQ(plain, (std::vector{"a\rb", "c"})); - EXPECT_EQ(split, (std::vector{"a", "b", "c"})); -} + expect(eq(plain, (std::vector{"a\rb", "c"}))); + expect(eq(split, (std::vector{"a", "b", "c"}))); +}}; diff --git a/tests/unit/test_provisions.cpp b/tests/unit/test_provisions.cpp index 9955f4a39..31e982be7 100644 --- a/tests/unit/test_provisions.cpp +++ b/tests/unit/test_provisions.cpp @@ -1,8 +1,10 @@ -#include import std; +import mcpplibs.mtest; import mcpp.build.provisions; +using namespace mcpplibs::mtest; + // A build-time provision — a host tool, a host build rule, a dependency's // directory — is something a dependency hands to its consumer's build PROGRAM. // Before #359 each kind invented its own reach, and none could be re-exported, @@ -46,61 +48,61 @@ bool sees_dir(const prov::Propagation& p, std::size_t consumer, // ── Table integrity ──────────────────────────────────────────────────────── -TEST(Provisions, EveryKindAnswersBothQuestions) { +UTest every_kind_answers_both_questions{"Provisions/EveryKindAnswersBothQuestions", [] { // The table exists so a new provision kind cannot be added without someone // stating how far it travels. A row that answered neither would compile // fine and silently pick a default, which is the exact failure #359 fixed. std::set seen; for (auto const& d : prov::kTable) { - EXPECT_FALSE(d.name.empty()); - EXPECT_TRUE(d.needsReexport) << d.name; + expect(!d.name.empty()); + expect(bool(d.needsReexport)) << d.name; seen.insert(d.kind); } - EXPECT_EQ(seen.size(), 3u); - EXPECT_TRUE(prov::def_of(prov::Kind::Tool).bareAddressable); - EXPECT_TRUE(prov::def_of(prov::Kind::DepDir).bareAddressable); + expect(eq(seen.size(), 3u)); + expect(bool(prov::def_of(prov::Kind::Tool).bareAddressable)); + expect(bool(prov::def_of(prov::Kind::DepDir).bareAddressable)); // A host module is addressed by `import ;` — the compiler resolves // it, so there is no bare-name channel for mcpp to bind. - EXPECT_FALSE(prov::def_of(prov::Kind::HostModule).bareAddressable); -} + expect(!(prov::def_of(prov::Kind::HostModule).bareAddressable)); +}}; // ── Propagation ──────────────────────────────────────────────────────────── -TEST(Provisions, ToolIsVisibleToTheRequesterWithoutAnyReexport) { +UTest tool_is_visible_to_the_requester_without_any_reexport{"Provisions/ToolIsVisibleToTheRequesterWithoutAnyReexport", [] { // 0=root, 1=protobuf. root --tools=[protoc]--> protobuf std::vector edges{ { 0, 1, { "protoc" }, false, false, false } }; auto p = prov::propagate(edges, 2); - EXPECT_TRUE(sees_tool(p, 0, 1, "protoc")); + expect(bool(sees_tool(p, 0, 1, "protoc"))); // Nothing to export: the root has no consumers, and it never said it // re-exported anything anyway. - EXPECT_TRUE(p.exported[0].empty()); -} + expect(p.exported[0].empty()); +}}; -TEST(Provisions, WithoutReexportALibrarysToolStaysWithTheLibrary) { +UTest without_reexport_a_librarys_tool_stays_with_the_library{"Provisions/WithoutReexportALibrarysToolStaysWithTheLibrary", [] { // 0=app, 1=lib, 2=protobuf. app -> lib --tools=[protoc]--> protobuf std::vector edges{ { 0, 1, {}, false, false, false }, { 1, 2, { "protoc" }, false, false, /*reexport=*/false }, }; auto p = prov::propagate(edges, 3); - EXPECT_TRUE(sees_tool(p, 1, 2, "protoc")); + expect(bool(sees_tool(p, 1, 2, "protoc"))); // THE test: an ordinary dependency must not push entries into its // consumer's tool namespace. That is a supply-chain rule, not a // convenience — which is why `reexport` defaults to false. - EXPECT_FALSE(sees_tool(p, 0, 2, "protoc")); -} + expect(!sees_tool(p, 0, 2, "protoc")); +}}; -TEST(Provisions, ReexportHandsTheToolToTheLibrarysConsumer) { +UTest reexport_hands_the_tool_to_the_librarys_consumer{"Provisions/ReexportHandsTheToolToTheLibrarysConsumer", [] { std::vector edges{ { 0, 1, {}, false, false, false }, { 1, 2, { "protoc" }, false, false, /*reexport=*/true }, }; auto p = prov::propagate(edges, 3); - EXPECT_TRUE(sees_tool(p, 0, 2, "protoc")); - EXPECT_TRUE(sees_tool(p, 1, 2, "protoc")); -} + expect(bool(sees_tool(p, 0, 2, "protoc"))); + expect(bool(sees_tool(p, 1, 2, "protoc"))); +}}; -TEST(Provisions, ReexportTravelsFurtherOnlyWhenEachHopSaysSo) { +UTest reexport_travels_further_only_when_each_hop_says_so{"Provisions/ReexportTravelsFurtherOnlyWhenEachHopSaysSo", [] { // 0=app, 1=mid, 2=lib, 3=protobuf. // lib re-exports protoc to mid; mid does NOT re-export to app. std::vector edges{ @@ -109,30 +111,30 @@ TEST(Provisions, ReexportTravelsFurtherOnlyWhenEachHopSaysSo) { { 2, 3, { "protoc" }, false, false, /*reexport=*/true }, }; auto p = prov::propagate(edges, 4); - EXPECT_TRUE(sees_tool(p, 2, 3, "protoc")); - EXPECT_TRUE(sees_tool(p, 1, 3, "protoc")); - EXPECT_FALSE(sees_tool(p, 0, 3, "protoc")); + expect(bool(sees_tool(p, 2, 3, "protoc"))); + expect(bool(sees_tool(p, 1, 3, "protoc"))); + expect(!sees_tool(p, 0, 3, "protoc")); // Flip the middle hop and it reaches all the way. Each package decides // what IT hands to ITS consumers; nothing decides on someone else's // behalf. edges[1].reexport = true; auto q = prov::propagate(edges, 4); - EXPECT_TRUE(sees_tool(q, 0, 3, "protoc")); -} + expect(bool(sees_tool(q, 0, 3, "protoc"))); +}}; -TEST(Provisions, HostModuleFollowsTheSameRule) { +UTest host_module_follows_the_same_rule{"Provisions/HostModuleFollowsTheSameRule", [] { std::vector edges{ { 0, 1, {}, false, false, false }, { 1, 2, {}, /*buildTable=*/true, /*wantsLibrary=*/true, /*reexport=*/true }, }; auto p = prov::propagate(edges, 3); const prov::Provision rule{ prov::Kind::HostModule, 2, {} }; - EXPECT_TRUE(p.visible[0].contains(rule)); - EXPECT_TRUE(p.visible[1].contains(rule)); -} + expect(p.visible[0].contains(rule)); + expect(p.visible[1].contains(rule)); +}}; -TEST(Provisions, EveryEdgeExposesTheDependencyDirectory) { +UTest every_edge_exposes_the_dependency_directory{"Provisions/EveryEdgeExposesTheDependencyDirectory", [] { // dep_dir() has always worked for a DIRECT dependency without anyone // declaring anything; that stays true. Re-export is what extends it. std::vector edges{ @@ -141,13 +143,13 @@ TEST(Provisions, EveryEdgeExposesTheDependencyDirectory) { { 1, 3, {}, false, false, /*reexport=*/false }, }; auto p = prov::propagate(edges, 4); - EXPECT_TRUE(sees_dir(p, 0, 1)); - EXPECT_TRUE(sees_dir(p, 0, 2)); - EXPECT_FALSE(sees_dir(p, 0, 3)); - EXPECT_TRUE(sees_dir(p, 1, 3)); -} + expect(bool(sees_dir(p, 0, 1))); + expect(bool(sees_dir(p, 0, 2))); + expect(!sees_dir(p, 0, 3)); + expect(bool(sees_dir(p, 1, 3))); +}}; -TEST(Provisions, ACycleTerminates) { +UTest a_cycle_terminates{"Provisions/ACycleTerminates", [] { // Resolution should not produce one, but the fixpoint must not depend on // that: sets only grow and are bounded, so a cycle simply stops changing. std::vector edges{ @@ -155,58 +157,58 @@ TEST(Provisions, ACycleTerminates) { { 1, 0, { "b" }, false, false, true }, }; auto p = prov::propagate(edges, 2); - EXPECT_TRUE(sees_tool(p, 0, 1, "a")); - EXPECT_TRUE(sees_tool(p, 1, 0, "b")); -} + expect(bool(sees_tool(p, 0, 1, "a"))); + expect(bool(sees_tool(p, 1, 0, "b"))); +}}; // ── Bare-name binding ────────────────────────────────────────────────────── -TEST(Provisions, ALoneCandidateOwnsItsBareName) { +UTest a_lone_candidate_owns_its_bare_name{"Provisions/ALoneCandidateOwnsItsBareName", [] { auto b = prov::bind_bare_names({ "compat.protobuf" }); - ASSERT_TRUE(b.contains("protobuf")); - EXPECT_EQ(b["protobuf"].owner, "compat.protobuf"); - EXPECT_FALSE(b["protobuf"].contested); - EXPECT_TRUE(prov::contest_note("protobuf", b["protobuf"]).empty()); -} + expect(fatal(b.contains("protobuf"))); + expect(eq(b["protobuf"].owner, "compat.protobuf")); + expect(!(b["protobuf"].contested)); + expect(prov::contest_note("protobuf", b["protobuf"]).empty()); +}}; -TEST(Provisions, ANonDefaultNamespaceStillGetsItsBareName) { +UTest a_non_default_namespace_still_gets_its_bare_name{"Provisions/ANonDefaultNamespaceStillGetsItsBareName", [] { // grpc-m's rule calls dep_bin("grpc-plugin", …); (grpc, grpc-plugin) is on // no rung of the package-identity ladder, so without the // unique-candidate rung the spelling already in the wild would break. auto b = prov::bind_bare_names({ "grpc.grpc-plugin" }); - EXPECT_EQ(b["grpc-plugin"].owner, "grpc.grpc-plugin"); -} + expect(eq(b["grpc-plugin"].owner, "grpc.grpc-plugin")); +}}; -TEST(Provisions, TheLadderPicksTheDefaultNamespaceFirst) { +UTest the_ladder_picks_the_default_namespace_first{"Provisions/TheLadderPicksTheDefaultNamespaceFirst", [] { auto b = prov::bind_bare_names({ "compat.zlib", "mcpplibs.zlib", "acme.zlib" }); - EXPECT_EQ(b["zlib"].owner, "mcpplibs.zlib"); - EXPECT_TRUE(b["zlib"].contested); + expect(eq(b["zlib"].owner, "mcpplibs.zlib")); + expect(bool(b["zlib"].contested)); // Contested but bound: the user is told which one won and how to name the // other, rather than getting whichever the loop appended last. auto note = prov::contest_note("zlib", b["zlib"]); - EXPECT_NE(note.find("mcpplibs.zlib"), std::string::npos); - EXPECT_NE(note.find("acme.zlib"), std::string::npos); -} + expect(ne(note.find("mcpplibs.zlib"), std::string::npos)); + expect(ne(note.find("acme.zlib"), std::string::npos)); +}}; -TEST(Provisions, CompatIsTheSecondRung) { +UTest compat_is_the_second_rung{"Provisions/CompatIsTheSecondRung", [] { auto b = prov::bind_bare_names({ "compat.zlib", "acme.zlib" }); - EXPECT_EQ(b["zlib"].owner, "compat.zlib"); -} + expect(eq(b["zlib"].owner, "compat.zlib")); +}}; -TEST(Provisions, TwoUnreachableNamespacesLeaveTheBareNameUnbound) { +UTest two_unreachable_namespaces_leave_the_bare_name_unbound{"Provisions/TwoUnreachableNamespacesLeaveTheBareNameUnbound", [] { // Neither is on the ladder and neither is unique. Binding either one would // be an arbitrary choice that decides which binary runs. auto b = prov::bind_bare_names({ "acme.protoc-ish", "other.protoc-ish" }); - EXPECT_TRUE(b["protoc-ish"].owner.empty()); - EXPECT_TRUE(b["protoc-ish"].contested); + expect(b["protoc-ish"].owner.empty()); + expect(bool(b["protoc-ish"].contested)); auto note = prov::contest_note("protoc-ish", b["protoc-ish"]); - EXPECT_NE(note.find("NOT"), std::string::npos); -} + expect(ne(note.find("NOT"), std::string::npos)); +}}; -TEST(Provisions, AnUnnamespacedPackageIsTheThirdRung) { +UTest an_unnamespaced_package_is_the_third_rung{"Provisions/AnUnnamespacedPackageIsTheThirdRung", [] { auto b = prov::bind_bare_names({ "grpcgen", "acme.grpcgen" }); - EXPECT_EQ(b["grpcgen"].owner, "grpcgen"); -} + expect(eq(b["grpcgen"].owner, "grpcgen")); +}}; // ── Host module identity ─────────────────────────────────────────────────── // @@ -229,41 +231,40 @@ std::filesystem::path write_iface(std::string_view stem, std::string_view body) } // namespace -TEST(HostModuleIdentity, TheDeclaredNameWinsOverThePackageName) { +UTest the_declared_name_wins_over_the_package_name{"HostModuleIdentity/TheDeclaredNameWinsOverThePackageName", [] { auto p = write_iface("declared-wins", "export module mcpp.rules.protobuf;\nimport std;\n"); - EXPECT_EQ(prov::host_module_name(p, "protobufgen"), "mcpp.rules.protobuf"); -} + expect(eq(prov::host_module_name(p, "protobufgen"), "mcpp.rules.protobuf")); +}}; -TEST(HostModuleIdentity, ADottedNameSurvivesWhole) { +UTest a_dotted_name_survives_whole{"HostModuleIdentity/ADottedNameSurvivesWhole", [] { // `declared_module_roots` is named for module ROOTS as opposed to // partitions; it must not truncate `a.b.c` to `a`, or every namespaced // rule would register under one colliding name. auto p = write_iface("dotted", "export module a.b.c;\n"); - EXPECT_EQ(prov::host_module_name(p, "fallback"), "a.b.c"); -} + expect(eq(prov::host_module_name(p, "fallback"), "a.b.c")); +}}; -TEST(HostModuleIdentity, AgreementIsTheCommonCaseAndIsUnchanged) { +UTest agreement_is_the_common_case_and_is_unchanged{"HostModuleIdentity/AgreementIsTheCommonCaseAndIsUnchanged", [] { // The zero-break claim: every rule package that works today declares a // name equal to its package name, so the scan returns what the old code // returned. auto p = write_iface("grpcgen", "export module grpcgen;\nimport std;\n"); - EXPECT_EQ(prov::host_module_name(p, "grpcgen"), "grpcgen"); -} + expect(eq(prov::host_module_name(p, "grpcgen"), "grpcgen")); +}}; -TEST(HostModuleIdentity, AnUnreadableInterfaceFallsBackToThePackageName) { +UTest an_unreadable_interface_falls_back_to_the_package_name{"HostModuleIdentity/AnUnreadableInterfaceFallsBackToThePackageName", [] { // build_host_module reports a missing interface unit by path, and that // diagnostic needs a name to report the package with. - EXPECT_EQ(prov::host_module_name("/nonexistent/nowhere.cppm", "rulepkg"), - "rulepkg"); -} + expect(eq(prov::host_module_name("/nonexistent/nowhere.cppm", "rulepkg"), "rulepkg")); +}}; -TEST(HostModuleIdentity, AnInterfaceDeclaringNoModuleFallsBack) { +UTest an_interface_declaring_no_module_falls_back{"HostModuleIdentity/AnInterfaceDeclaringNoModuleFallsBack", [] { auto p = write_iface("nomodule", "int main() { return 0; }\n"); - EXPECT_EQ(prov::host_module_name(p, "rulepkg"), "rulepkg"); -} + expect(eq(prov::host_module_name(p, "rulepkg"), "rulepkg")); +}}; -TEST(HostModuleIdentity, TwoRulesProvidingOneModuleNameAreRefused) { +UTest two_rules_providing_one_module_name_are_refused{"HostModuleIdentity/TwoRulesProvidingOneModuleNameAreRefused", [] { // Designated initialisers on purpose. Positional aggregate init put the // interface path into a field added later, and the test then asserted that // a path it had never passed was absent — a green that meant nothing until @@ -275,16 +276,16 @@ TEST(HostModuleIdentity, TwoRulesProvidingOneModuleNameAreRefused) { .interface = "/b/src/rules.cppm"}, }; auto clash = prov::host_module_collision(mods); - ASSERT_TRUE(clash.has_value()); + expect(fatal(clash.has_value())); // Both identities and both paths are the CONTENT of this diagnostic: with // one module name shared, they are the only way to tell the two apart. - EXPECT_NE(clash->find("acme.tidy"), std::string::npos); - EXPECT_NE(clash->find("other.tidy"), std::string::npos); - EXPECT_NE(clash->find("/a/src/tidy.cppm"), std::string::npos); - EXPECT_NE(clash->find("/b/src/rules.cppm"), std::string::npos); -} + expect(ne(clash->find("acme.tidy"), std::string::npos)); + expect(ne(clash->find("other.tidy"), std::string::npos)); + expect(ne(clash->find("/a/src/tidy.cppm"), std::string::npos)); + expect(ne(clash->find("/b/src/rules.cppm"), std::string::npos)); +}}; -TEST(HostModuleIdentity, DistinctModuleNamesFromOnePackageNameAreFine) { +UTest distinct_module_names_from_one_package_name_are_fine{"HostModuleIdentity/DistinctModuleNamesFromOnePackageNameAreFine", [] { // The mirror of the previous test, and the reason I1 and I3 ship together: // decoupling the names is what makes two packages able to share a package // name while differing in what they declare. @@ -294,58 +295,58 @@ TEST(HostModuleIdentity, DistinctModuleNamesFromOnePackageNameAreFine) { {.module = "lints", .package = "other.rules", .interface = "/b/src/rules.cppm"}, }; - EXPECT_FALSE(prov::host_module_collision(mods).has_value()); -} + expect(!(prov::host_module_collision(mods).has_value())); +}}; -TEST(ReservedPrefix, AnOutsidePackageClaimingMcppIsWarnedAbout) { +UTest an_outside_package_claiming_mcpp_is_warned_about{"ReservedPrefix/AnOutsidePackageClaimingMcppIsWarnedAbout", [] { auto w = prov::reserved_prefix_warning("mcpp.rules.protobuf", "acme", "acme.protobufgen"); - ASSERT_TRUE(w.has_value()); - EXPECT_NE(w->find("mcpp.rules.protobuf"), std::string::npos); - EXPECT_NE(w->find("acme.protobufgen"), std::string::npos); -} - -TEST(ReservedPrefix, TheOfficialNamespaceIsSilent) { - EXPECT_FALSE(prov::reserved_prefix_warning("mcpp.rules.protobuf", "mcpp", - "mcpp.protobuf").has_value()); -} - -TEST(ReservedPrefix, AnOrdinaryNameIsSilent) { - EXPECT_FALSE(prov::reserved_prefix_warning("grpcgen", "mcpplibs", - "mcpplibs.grpcgen").has_value()); + expect(fatal(w.has_value())); + expect(ne(w->find("mcpp.rules.protobuf"), std::string::npos)); + expect(ne(w->find("acme.protobufgen"), std::string::npos)); +}}; + +UTest the_official_namespace_is_silent{"ReservedPrefix/TheOfficialNamespaceIsSilent", [] { + expect(!(prov::reserved_prefix_warning("mcpp.rules.protobuf", "mcpp", + "mcpp.protobuf").has_value())); +}}; + +UTest an_ordinary_name_is_silent{"ReservedPrefix/AnOrdinaryNameIsSilent", [] { + expect(!(prov::reserved_prefix_warning("grpcgen", "mcpplibs", + "mcpplibs.grpcgen").has_value())); // `mcppish` is not under the `mcpp.` prefix: the dot is what makes the // claim, and a substring test would have flagged this. - EXPECT_FALSE(prov::reserved_prefix_warning("mcppish", "acme", - "acme.mcppish").has_value()); -} + expect(!(prov::reserved_prefix_warning("mcppish", "acme", + "acme.mcppish").has_value())); +}}; // #734 E10: a plugin may say it is an mcpp plugin under its own namespace; // the official second segments stay the mcpp project's. -TEST(ReservedPrefix, AnOwnNamespaceUnderMcppIsSilent) { - EXPECT_FALSE(prov::reserved_prefix_warning("mcpp.acme.protobuf", "acme", - "acme.protobufgen").has_value()); - EXPECT_FALSE(prov::reserved_prefix_warning("mcpp.acme", "acme", - "acme.gen").has_value()); - EXPECT_FALSE(prov::reserved_prefix_warning("mcpp.mcpplibs.capi.lua", "mcpplibs.capi", - "mcpplibs.capi.lua").has_value()); -} - -TEST(ReservedPrefix, EveryReservedSecondSegmentIsWarnedAbout) { +UTest an_own_namespace_under_mcpp_is_silent{"ReservedPrefix/AnOwnNamespaceUnderMcppIsSilent", [] { + expect(!(prov::reserved_prefix_warning("mcpp.acme.protobuf", "acme", + "acme.protobufgen").has_value())); + expect(!(prov::reserved_prefix_warning("mcpp.acme", "acme", + "acme.gen").has_value())); + expect(!(prov::reserved_prefix_warning("mcpp.mcpplibs.capi.lua", "mcpplibs.capi", + "mcpplibs.capi.lua").has_value())); +}}; + +UTest every_reserved_second_segment_is_warned_about{"ReservedPrefix/EveryReservedSecondSegmentIsWarnedAbout", [] { for (auto seg : prov::kReservedSecondSegments) { auto name = std::string("mcpp.") + std::string(seg) + ".x"; auto w = prov::reserved_prefix_warning(name, "acme", "acme.x"); - ASSERT_TRUE(w.has_value()) << name; - EXPECT_NE(w->find("mcpp.acme.*"), std::string::npos) << *w; + expect(fatal(w.has_value())) << name; + if (auto&& failed = expect(ne(w->find("mcpp.acme.*"), std::string::npos)); !failed) failed << *w; } -} +}}; -TEST(ReservedPrefix, AnotherNamespaceUnderMcppIsWarnedAbout) { +UTest another_namespace_under_mcpp_is_warned_about{"ReservedPrefix/AnotherNamespaceUnderMcppIsWarnedAbout", [] { auto w = prov::reserved_prefix_warning("mcpp.other.x", "acme", "acme.x"); - ASSERT_TRUE(w.has_value()); - EXPECT_NE(w->find("mcpp.acme.*"), std::string::npos) << *w; + expect(fatal(w.has_value())); + if (auto&& failed = expect(ne(w->find("mcpp.acme.*"), std::string::npos)); !failed) failed << *w; // `mcpp.acmeish` is not under `mcpp.acme.`: the segment boundary decides. - EXPECT_TRUE(prov::reserved_prefix_warning("mcpp.acmeish.x", "acme", "acme.x").has_value()); -} + expect(prov::reserved_prefix_warning("mcpp.acmeish.x", "acme", "acme.x").has_value()); +}}; // A package that offers several rules through features (mcpp 2026.9.5.3+) // contributes every module INTERFACE unit among its resolved sources. The @@ -353,30 +354,26 @@ TEST(ReservedPrefix, AnotherNamespaceUnderMcppIsWarnedAbout) { // wrong answer would turn into a compile of something that cannot be compiled // alone -- an implementation unit, a partition -- or into a phantom module // named by a comment. -TEST(InterfaceUnit, ThePrimaryInterfaceDeclarationIsRecognised) { - EXPECT_EQ(prov::declared_interface_name("export module mcpp.rules.cuda;\n"), - "mcpp.rules.cuda"); - EXPECT_EQ(prov::declared_interface_name( - "module;\n#include \nexport module a.b;\nimport std;\n"), - "a.b"); - EXPECT_EQ(prov::declared_interface_name("\texport module\tx ; // trailing\n"), - "x"); -} - -TEST(InterfaceUnit, WhatIsNotAPrimaryInterfaceIsNotNamed) { - EXPECT_EQ(prov::declared_interface_name("module a.b;\n"), ""); // implementation - EXPECT_EQ(prov::declared_interface_name("export module a.b:part;\n"), ""); // partition - EXPECT_EQ(prov::declared_interface_name("module;\n"), ""); // global fragment - EXPECT_EQ(prov::declared_interface_name("export module;\n"), ""); - EXPECT_EQ(prov::declared_interface_name("// export module a.b;\n"), ""); // documentation - EXPECT_EQ(prov::declared_interface_name("exportmodule a.b;\n"), ""); - EXPECT_EQ(prov::declared_interface_name("int x; // not a module at all\n"), ""); -} - -TEST(InterfaceUnit, TheFirstDeclarationWins) { +UTest the_primary_interface_declaration_is_recognised{"InterfaceUnit/ThePrimaryInterfaceDeclarationIsRecognised", [] { + expect(eq(prov::declared_interface_name("export module mcpp.rules.cuda;\n"), "mcpp.rules.cuda")); + expect(eq(prov::declared_interface_name( + "module;\n#include \nexport module a.b;\nimport std;\n"), "a.b")); + expect(eq(prov::declared_interface_name("\texport module\tx ; // trailing\n"), "x")); +}}; + +UTest what_is_not_a_primary_interface_is_not_named{"InterfaceUnit/WhatIsNotAPrimaryInterfaceIsNotNamed", [] { + expect(eq(prov::declared_interface_name("module a.b;\n"), "")); // implementation + expect(eq(prov::declared_interface_name("export module a.b:part;\n"), "")); // partition + expect(eq(prov::declared_interface_name("module;\n"), "")); // global fragment + expect(eq(prov::declared_interface_name("export module;\n"), "")); + expect(eq(prov::declared_interface_name("// export module a.b;\n"), "")); // documentation + expect(eq(prov::declared_interface_name("exportmodule a.b;\n"), "")); + expect(eq(prov::declared_interface_name("int x; // not a module at all\n"), "")); +}}; + +UTest the_first_declaration_wins{"InterfaceUnit/TheFirstDeclarationWins", [] { // A file names one module; anything after the first declaration is that // module's body, and a quoted declaration inside it is text. - EXPECT_EQ(prov::declared_interface_name( - "export module first;\n/* export module second; */\n"), - "first"); -} + expect(eq(prov::declared_interface_name( + "export module first;\n/* export module second; */\n"), "first")); +}}; diff --git a/tests/unit/test_run_concurrent_children.cpp b/tests/unit/test_run_concurrent_children.cpp index 8dafbed17..02c4015b8 100644 --- a/tests/unit/test_run_concurrent_children.cpp +++ b/tests/unit/test_run_concurrent_children.cpp @@ -1,9 +1,11 @@ -#include import std; +import mcpplibs.mtest; import mcpplibs.os; import mcpp.run; +using namespace mcpplibs::mtest; + // SUBSYSTEM-LEVEL, and the contract is B2-0 of the member-selection and // build-program-cost plan (#748): the launcher is safe for concurrent callers. // @@ -57,7 +59,7 @@ Outcome run_when_open(const std::atomic& gate, const std::vector gate{false}; Outcome quick, slow; @@ -67,18 +69,18 @@ TEST(ConcurrentChildren, AReaderReturnsWhenItsOwnChildExits) { tQuick.join(); tSlow.join(); - ASSERT_EQ(quick.exit_code, 0) << "round " << round << ": " << quick.output; - ASSERT_EQ(slow.exit_code, 0) << "round " << round << ": " << slow.output; + expect(fatal(eq(quick.exit_code, 0))) << "round " << round << ": " << quick.output; + expect(fatal(eq(slow.exit_code, 0))) << "round " << round << ": " << slow.output; // Each reader is handed its own child's output and nothing else. - EXPECT_NE(quick.output.find("quick"), std::string::npos) << quick.output; + expect(ne(quick.output.find("quick"), std::string::npos)) << quick.output; // The quick child was gone long before the slow one. Half the slow // child's life is the margin: it does not depend on how fast the // machine starts a process, only on the two readers not sharing a pipe. const auto gap = std::chrono::duration_cast( slow.end - quick.end); - EXPECT_GT(gap, kSlowFor / 2) + expect(gt(gap, kSlowFor / 2)) << "round " << round << ": the reader of the child that exited at once " << "returned only " << gap.count() << " ms before the slow child did; " << "it waited for a child another thread started"; } -} +}}; diff --git a/tests/unit/test_runner_lookup.cpp b/tests/unit/test_runner_lookup.cpp index 0f5a5f169..144aca228 100644 --- a/tests/unit/test_runner_lookup.cpp +++ b/tests/unit/test_runner_lookup.cpp @@ -1,10 +1,12 @@ -#include #include #include import std; +import mcpplibs.mtest; import mcpp.build.runner_lookup; +using namespace mcpplibs::mtest; + using namespace mcpp::build::runner_lookup; // The lookup order is the whole point (#544 §4.4): a declared payload's bin/ @@ -29,125 +31,125 @@ std::filesystem::path make_exe(const std::filesystem::path& dir, std::string_vie } } // namespace -TEST(RunnerLookup, PayloadBinBeatsPath) { +UTest payload_bin_beats_path{"RunnerLookup/PayloadBinBeatsPath", [] { auto root = fresh_root("mcpp-runner-lookup-1"); auto inPayload = make_exe(root / "payload" / "bin", "qemu-x"); make_exe(root / "path", "qemu-x"); std::vector bins{root / "payload" / "bin"}; auto l = locate("qemu-x", bins, (root / "path").string()); - ASSERT_TRUE(l.program.has_value()); - EXPECT_EQ(*l.program, inPayload); -} + expect(fatal(l.program.has_value())); + expect(eq(*l.program, inPayload)); +}}; -TEST(RunnerLookup, PathIsSearchedAfterPayloads) { +UTest path_is_searched_after_payloads{"RunnerLookup/PathIsSearchedAfterPayloads", [] { auto root = fresh_root("mcpp-runner-lookup-2"); auto onPath = make_exe(root / "path", "qemu-y"); std::vector bins{root / "payload" / "bin"}; // absent dir auto l = locate("qemu-y", bins, (root / "path").string()); - ASSERT_TRUE(l.program.has_value()); - EXPECT_EQ(*l.program, onPath); - ASSERT_EQ(l.searched.size(), 2u); - EXPECT_EQ(l.searched[0], root / "payload" / "bin"); - EXPECT_EQ(l.searched[1], root / "path"); -} - -TEST(RunnerLookup, NonExecutableFileIsSkipped) { + expect(fatal(l.program.has_value())); + expect(eq(*l.program, onPath)); + expect(fatal(eq(l.searched.size(), 2u))); + expect(eq(l.searched[0], root / "payload" / "bin")); + expect(eq(l.searched[1], root / "path")); +}}; + +UTest non_executable_file_is_skipped{"RunnerLookup/NonExecutableFileIsSkipped", [] { auto root = fresh_root("mcpp-runner-lookup-3"); std::filesystem::create_directories(root / "p1"); { std::ofstream o(root / "p1" / "tool"); o << "data"; } // no exec bit auto real = make_exe(root / "p2", "tool"); auto l = locate("tool", {}, (root / "p1").string() + ":" + (root / "p2").string()); - ASSERT_TRUE(l.program.has_value()); - EXPECT_EQ(*l.program, real); -} + expect(fatal(l.program.has_value())); + expect(eq(*l.program, real)); +}}; -TEST(RunnerLookup, NotFoundListsEveryDirectorySearched) { +UTest not_found_lists_every_directory_searched{"RunnerLookup/NotFoundListsEveryDirectorySearched", [] { auto root = fresh_root("mcpp-runner-lookup-4"); std::vector bins{root / "a" / "bin"}; auto l = locate("nope", bins, (root / "p1").string() + ":" + (root / "p2").string()); - EXPECT_FALSE(l.program.has_value()); - ASSERT_EQ(l.searched.size(), 3u); + expect(!l.program.has_value()); + expect(fatal(eq(l.searched.size(), 3u))); auto msg = not_found_message("aarch64-linux-musl", "nope", l.searched); - EXPECT_NE(msg.find("runner 'nope'"), std::string::npos) << msg; - EXPECT_NE(msg.find("aarch64-linux-musl"), std::string::npos) << msg; - EXPECT_NE(msg.find((root / "a" / "bin").string()), std::string::npos) << msg; - EXPECT_NE(msg.find((root / "p2").string()), std::string::npos) << msg; - EXPECT_NE(msg.find("[xlings.workspace]"), std::string::npos) << msg; - EXPECT_NE(msg.find("--no-runner"), std::string::npos) << msg; -} - -TEST(RunnerLookup, AbsoluteArgv0IsTakenAsIs) { + expect(ne(msg.find("runner 'nope'"), std::string::npos)) << msg; + expect(ne(msg.find("aarch64-linux-musl"), std::string::npos)) << msg; + expect(ne(msg.find((root / "a" / "bin").string()), std::string::npos)) << msg; + expect(ne(msg.find((root / "p2").string()), std::string::npos)) << msg; + expect(ne(msg.find("[xlings.workspace]"), std::string::npos)) << msg; + expect(ne(msg.find("--no-runner"), std::string::npos)) << msg; +}}; + +UTest absolute_argv0_is_taken_as_is{"RunnerLookup/AbsoluteArgv0IsTakenAsIs", [] { auto root = fresh_root("mcpp-runner-lookup-5"); auto abs = make_exe(root, "runner.sh"); auto l = locate(abs.string(), {}, ""); - ASSERT_TRUE(l.program.has_value()); - EXPECT_EQ(*l.program, abs); + expect(fatal(l.program.has_value())); + expect(eq(*l.program, abs)); // ...and an absolute path that is not there is not searched for elsewhere. auto missing = locate((root / "absent.sh").string(), {}, root.string()); - EXPECT_FALSE(missing.program.has_value()); -} + expect(!missing.program.has_value()); +}}; -TEST(RunnerLookup, EmptyPathEntriesAreIgnored) { +UTest empty_path_entries_are_ignored{"RunnerLookup/EmptyPathEntriesAreIgnored", [] { auto root = fresh_root("mcpp-runner-lookup-6"); auto onPath = make_exe(root / "p", "tool"); auto l = locate("tool", {}, ":" + (root / "p").string() + "::"); - ASSERT_TRUE(l.program.has_value()); - EXPECT_EQ(*l.program, onPath); - EXPECT_EQ(l.searched.size(), 1u); -} - -TEST(RunnerLookup, ClassifiesENOEXECAsUnloadable) { - EXPECT_EQ(classify(ENOEXEC), SpawnClass::Unloadable); - EXPECT_EQ(classify(EACCES), SpawnClass::Other); - EXPECT_EQ(classify(ENOENT), SpawnClass::Other); - EXPECT_EQ(classify(0), SpawnClass::Other); -} - -TEST(RunnerLookup, UnrunnableMessageNamesKernelAnswerTripleAndKey) { + expect(fatal(l.program.has_value())); + expect(eq(*l.program, onPath)); + expect(eq(l.searched.size(), 1u)); +}}; + +UTest classifies_enoexec_as_unloadable{"RunnerLookup/ClassifiesENOEXECAsUnloadable", [] { + expect(eq(classify(ENOEXEC), SpawnClass::Unloadable)); + expect(eq(classify(EACCES), SpawnClass::Other)); + expect(eq(classify(ENOENT), SpawnClass::Other)); + expect(eq(classify(0), SpawnClass::Other)); +}}; + +UTest unrunnable_message_names_kernel_answer_triple_and_key{"RunnerLookup/UnrunnableMessageNamesKernelAnswerTripleAndKey", [] { auto msg = unrunnable_message("aarch64-linux-musl", "/x/bin/app", ENOEXEC); - EXPECT_NE(msg.find("Exec format error"), std::string::npos) << msg; - EXPECT_NE(msg.find("/x/bin/app"), std::string::npos) << msg; - EXPECT_NE(msg.find("built for 'aarch64-linux-musl'"), std::string::npos) << msg; - EXPECT_NE(msg.find("[target.aarch64-linux-musl]"), std::string::npos) << msg; - EXPECT_NE(msg.find("runner = [\"qemu-aarch64-static\"]"), std::string::npos) << msg; - EXPECT_NE(msg.find("--no-runner"), std::string::npos) << msg; -} + expect(ne(msg.find("Exec format error"), std::string::npos)) << msg; + expect(ne(msg.find("/x/bin/app"), std::string::npos)) << msg; + expect(ne(msg.find("built for 'aarch64-linux-musl'"), std::string::npos)) << msg; + expect(ne(msg.find("[target.aarch64-linux-musl]"), std::string::npos)) << msg; + expect(ne(msg.find("runner = [\"qemu-aarch64-static\"]"), std::string::npos)) << msg; + expect(ne(msg.find("--no-runner"), std::string::npos)) << msg; +}}; // A PAYLOAD CONTRIBUTES TWO DIRECTORIES, AND THE ORDER IS PART OF THE RULE. // The flat case is the one measured missing on a macOS runner: a program at // the payload root with no `bin/`, reached through exactly the directories // prepare collects for each installed payload. -TEST(RunnerLookup, AFlatPayloadIsFoundAtItsRoot) { +UTest a_flat_payload_is_found_at_its_root{"RunnerLookup/AFlatPayloadIsFoundAtItsRoot", [] { auto root = fresh_root("mcpp-runner-lookup-flat"); auto flat = make_exe(root / "payload", "simctl-run"); auto l = locate("simctl-run", payload_search_dirs(root / "payload"), ""); - ASSERT_TRUE(l.program.has_value()); - EXPECT_EQ(*l.program, flat); - ASSERT_EQ(l.searched.size(), 2u); - EXPECT_EQ(l.searched[0], root / "payload" / "bin"); - EXPECT_EQ(l.searched[1], root / "payload"); -} + expect(fatal(l.program.has_value())); + expect(eq(*l.program, flat)); + expect(fatal(eq(l.searched.size(), 2u))); + expect(eq(l.searched[0], root / "payload" / "bin")); + expect(eq(l.searched[1], root / "payload")); +}}; // And a recipe that follows the convention is unaffected by the second entry. -TEST(RunnerLookup, APayloadsBinIsTriedBeforeItsRoot) { +UTest a_payloads_bin_is_tried_before_its_root{"RunnerLookup/APayloadsBinIsTriedBeforeItsRoot", [] { auto root = fresh_root("mcpp-runner-lookup-binfirst"); auto inBin = make_exe(root / "payload" / "bin", "tool"); make_exe(root / "payload", "tool"); auto l = locate("tool", payload_search_dirs(root / "payload"), ""); - ASSERT_TRUE(l.program.has_value()); - EXPECT_EQ(*l.program, inBin); -} + expect(fatal(l.program.has_value())); + expect(eq(*l.program, inBin)); +}}; -TEST(RunnerLookup, SpawnFailedMessageIsVerbatim) { +UTest spawn_failed_message_is_verbatim{"RunnerLookup/SpawnFailedMessageIsVerbatim", [] { auto msg = spawn_failed_message("/x/bin/qemu", EACCES); - EXPECT_NE(msg.find("'/x/bin/qemu' could not be started"), std::string::npos) << msg; - EXPECT_NE(msg.find("Permission denied"), std::string::npos) << msg; - EXPECT_NE(msg.find("(error 13)"), std::string::npos) << msg; - EXPECT_EQ(msg.find("runner = ["), std::string::npos) << msg; // no runner advice -} + expect(ne(msg.find("'/x/bin/qemu' could not be started"), std::string::npos)) << msg; + expect(ne(msg.find("Permission denied"), std::string::npos)) << msg; + expect(ne(msg.find("(error 13)"), std::string::npos)) << msg; + expect(eq(msg.find("runner = ["), std::string::npos)) << msg; // no runner advice +}}; #else -TEST(RunnerLookup, WindowsCoveredByIntegration) { SUCCEED(); } +UTest runner_lookup_windows_covered_by_integration{"RunnerLookup/WindowsCoveredByIntegration", [] { /* passes */; }}; #endif diff --git a/tests/unit/test_runtime_binding_fallback.cpp b/tests/unit/test_runtime_binding_fallback.cpp index b61e0d958..2bd3cc959 100644 --- a/tests/unit/test_runtime_binding_fallback.cpp +++ b/tests/unit/test_runtime_binding_fallback.cpp @@ -14,11 +14,13 @@ // the compiler's PT_INTERP) are all things some earlier mechanism wrote, and a // machine can legitimately have none of them. -#include import std; +import mcpplibs.mtest; import mcpp.toolchain.post_install; +using namespace mcpplibs::mtest; + namespace tc = mcpp::toolchain; namespace { @@ -39,32 +41,32 @@ struct Home { ~Home() { std::error_code ec; std::filesystem::remove_all(root, ec); } }; -TEST(RuntimeBindingFallback, OnePayloadIsAnAnswer) { +UTest one_payload_is_an_answer{"RuntimeBindingFallback/OnePayloadIsAnAnswer", [] { Home h{"2.39"}; - EXPECT_EQ(tc::baked_runtime_binding(h.compiler), "glibc@2.39"); -} + expect(eq(tc::baked_runtime_binding(h.compiler), "glibc@2.39")); +}}; // The incident, in one assertion. Two payloads and nothing declaring which: // silence, not a coin flip. -TEST(RuntimeBindingFallback, TwoPayloadsIsSilence) { +UTest two_payloads_is_silence{"RuntimeBindingFallback/TwoPayloadsIsSilence", [] { Home h{"2.39", "2.44"}; - EXPECT_EQ(tc::baked_runtime_binding(h.compiler), ""); -} + expect(eq(tc::baked_runtime_binding(h.compiler), "")); +}}; -TEST(RuntimeBindingFallback, NoPayloadIsSilence) { +UTest no_payload_is_silence{"RuntimeBindingFallback/NoPayloadIsSilence", [] { Home h{}; - EXPECT_EQ(tc::baked_runtime_binding(h.compiler), ""); -} + expect(eq(tc::baked_runtime_binding(h.compiler), "")); +}}; // A compiler outside any xpkgs tree (a system gcc) has no payload set to read, // and must not acquire one from somewhere else. -TEST(RuntimeBindingFallback, NonPayloadCompilerGetsNothing) { - EXPECT_EQ(tc::baked_runtime_binding("/usr/bin/g++"), ""); -} +UTest non_payload_compiler_gets_nothing{"RuntimeBindingFallback/NonPayloadCompilerGetsNothing", [] { + expect(eq(tc::baked_runtime_binding("/usr/bin/g++"), "")); +}}; -TEST(RuntimeBindingFallback, EmptyPathIsSilence) { - EXPECT_EQ(tc::baked_runtime_binding({}), ""); -} +UTest empty_path_is_silence{"RuntimeBindingFallback/EmptyPathIsSilence", [] { + expect(eq(tc::baked_runtime_binding({}), "")); +}}; // A record that names a payload which is no longer installed. // @@ -97,11 +99,11 @@ struct HomeWithSpecs { ~HomeWithSpecs() { std::error_code ec; std::filesystem::remove_all(root, ec); } }; -TEST(RuntimeBindingFallback, RecordNamingAnAbsentPayloadIsSkipped) { +UTest record_naming_an_absent_payload_is_skipped{"RuntimeBindingFallback/RecordNamingAnAbsentPayloadIsSkipped", [] { HomeWithSpecs h{{"2.44"}, "2.39"}; // Not "glibc@2.39": that payload is gone. The singleton rule answers. - EXPECT_EQ(tc::baked_runtime_binding(h.compiler), "glibc@2.44"); -} + expect(eq(tc::baked_runtime_binding(h.compiler), "glibc@2.44")); +}}; // Linux only, and not as an exemption -- the mechanism does not exist // elsewhere. detect_baked_loader parses GCC specs, whose loader paths are @@ -114,19 +116,19 @@ TEST(RuntimeBindingFallback, RecordNamingAnAbsentPayloadIsSkipped) { // // The other cases in this file are platform-neutral (they exercise the // payload set, not the parser) and keep running everywhere. -TEST(RuntimeBindingFallback, RecordNamingAPresentPayloadWins) { +UTest record_naming_a_present_payload_wins{"RuntimeBindingFallback/RecordNamingAPresentPayloadWins", [] { if constexpr (!std::filesystem::path::preferred_separator || std::filesystem::path::preferred_separator != '/') - GTEST_SKIP() << "GCC specs loader paths are POSIX; no such record here"; + skip("GCC specs loader paths are POSIX; no such record here"); HomeWithSpecs h{{"2.39", "2.44"}, "2.39"}; // Two installed, so the singleton rule cannot answer -- but the record // names one that IS there, and that is what the artifact would load. - EXPECT_EQ(tc::baked_runtime_binding(h.compiler), "glibc@2.39"); -} + expect(eq(tc::baked_runtime_binding(h.compiler), "glibc@2.39")); +}}; -TEST(RuntimeBindingFallback, StaleRecordWithNothingToFallBackOnIsSilence) { +UTest stale_record_with_nothing_to_fall_back_on_is_silence{"RuntimeBindingFallback/StaleRecordWithNothingToFallBackOnIsSilence", [] { HomeWithSpecs h{{"2.39", "2.44"}, "2.28"}; - EXPECT_EQ(tc::baked_runtime_binding(h.compiler), ""); -} + expect(eq(tc::baked_runtime_binding(h.compiler), "")); +}}; } // namespace diff --git a/tests/unit/test_runtime_contract.cpp b/tests/unit/test_runtime_contract.cpp index 5b89c3ea3..f1b8b7f2f 100644 --- a/tests/unit/test_runtime_contract.cpp +++ b/tests/unit/test_runtime_contract.cpp @@ -1,6 +1,6 @@ -#include import std; +import mcpplibs.mtest; import mcpp.build.plan; import mcpp.manifest.toml; import mcpp.manifest.types; @@ -12,6 +12,8 @@ import mcpp.runtime.search; import mcpp.runtime.binding; import mcpp.provider.xlings.subos_info; +using namespace mcpplibs::mtest; + namespace build = mcpp::build; namespace mf = mcpp::manifest; @@ -63,7 +65,7 @@ mcpp::modgraph::PackageRoot package( return out; } -TEST(RuntimeContract, TomlReadsStructuredValuesAndKeepsLegacyFields) { +UTest toml_reads_structured_values_and_keeps_legacy_fields{"RuntimeContract/TomlReadsStructuredValuesAndKeepsLegacyFields", [] { auto parsed = mf::parse_string(R"( [package] name = "consumer" @@ -88,25 +90,23 @@ artifacts = [ { role = "library", path = "run/libwidget.so.1", provenance = "payload", abi = "elf-x86_64", digest = "sha256:abc", host_fingerprint = "host-1" }, ] )"); - ASSERT_TRUE(parsed) << parsed.error().format(); - ASSERT_EQ(parsed->runtimeConfig.requirements.size(), 2u); - EXPECT_EQ(parsed->runtimeConfig.requirements[0].kind, "capability"); - EXPECT_EQ(parsed->runtimeConfig.requirements[0].value, "display.present"); - EXPECT_TRUE(parsed->runtimeConfig.requirements[0].required); - EXPECT_FALSE(parsed->runtimeConfig.requirements[1].required); - ASSERT_EQ(parsed->runtimeConfig.artifacts.size(), 1u); - EXPECT_EQ(parsed->runtimeConfig.artifacts[0].role, "library"); - EXPECT_EQ(parsed->runtimeConfig.artifacts[0].hostFingerprint, "host-1"); - EXPECT_EQ(parsed->runtimeConfig.linkIntent.libraries, - std::vector{"widget"}); - EXPECT_EQ(parsed->runtimeConfig.linkIntent.runtimeSearchDirs, - std::vector{"run"}); - EXPECT_EQ(parsed->runtimeConfig.libraryDirs[0], "legacy/run"); - EXPECT_EQ(parsed->runtimeConfig.capabilities[0], "legacy.display"); - EXPECT_EQ(parsed->runtimeConfig.provides[0], "legacy.provider"); -} - -TEST(RuntimeContract, XpkgReadsTheSameStructuredContract) { + if (auto&& failed = expect(fatal(bool(parsed))); !failed) failed << parsed.error().format(); + expect(fatal(eq(parsed->runtimeConfig.requirements.size(), 2u))); + expect(eq(parsed->runtimeConfig.requirements[0].kind, "capability")); + expect(eq(parsed->runtimeConfig.requirements[0].value, "display.present")); + expect(bool(parsed->runtimeConfig.requirements[0].required)); + expect(!(parsed->runtimeConfig.requirements[1].required)); + expect(fatal(eq(parsed->runtimeConfig.artifacts.size(), 1u))); + expect(eq(parsed->runtimeConfig.artifacts[0].role, "library")); + expect(eq(parsed->runtimeConfig.artifacts[0].hostFingerprint, "host-1")); + expect(eq(parsed->runtimeConfig.linkIntent.libraries, std::vector{"widget"})); + expect(eq(parsed->runtimeConfig.linkIntent.runtimeSearchDirs, std::vector{"run"})); + expect(eq(parsed->runtimeConfig.libraryDirs[0], "legacy/run")); + expect(eq(parsed->runtimeConfig.capabilities[0], "legacy.display")); + expect(eq(parsed->runtimeConfig.provides[0], "legacy.provider")); +}}; + +UTest xpkg_reads_the_same_structured_contract{"RuntimeContract/XpkgReadsTheSameStructuredContract", [] { constexpr auto lua = R"( package = { spec = "1", @@ -134,16 +134,15 @@ package = { )"; auto parsed = mf::synthesize_from_xpkg_lua( lua, "backend", "2.0.0", mcpp::toolchain::platform::HostPlatform::current()); - ASSERT_TRUE(parsed) << parsed.error().format(); - ASSERT_EQ(parsed->runtimeConfig.requirements.size(), 1u); - EXPECT_EQ(parsed->runtimeConfig.requirements[0].value, "display.present"); - ASSERT_EQ(parsed->runtimeConfig.artifacts.size(), 1u); - EXPECT_EQ(parsed->runtimeConfig.artifacts[0].path, "lib/libbackend.so"); - EXPECT_EQ(parsed->runtimeConfig.linkIntent.transitiveNeededDirs, - std::vector{"closure"}); -} - -TEST(RuntimeContract, RequirementsNeverSelfProvideAndCanonicalIdsDoNotCollide) { + if (auto&& failed = expect(fatal(bool(parsed))); !failed) failed << parsed.error().format(); + expect(fatal(eq(parsed->runtimeConfig.requirements.size(), 1u))); + expect(eq(parsed->runtimeConfig.requirements[0].value, "display.present")); + expect(fatal(eq(parsed->runtimeConfig.artifacts.size(), 1u))); + expect(eq(parsed->runtimeConfig.artifacts[0].path, "lib/libbackend.so")); + expect(eq(parsed->runtimeConfig.linkIntent.transitiveNeededDirs, std::vector{"closure"})); +}}; + +UTest requirements_never_self_provide_and_canonical_ids_do_not_collide{"RuntimeContract/RequirementsNeverSelfProvideAndCanonicalIdsDoNotCollide", [] { auto base = std::filesystem::temp_directory_path() / "mcpp-runtime-contract"; auto consumer = package(base / "consumer", "mcpplibs", "consumer", "1.0.0", "path+consumer"); @@ -173,35 +172,31 @@ TEST(RuntimeContract, RequirementsNeverSelfProvideAndCanonicalIdsDoNotCollide) { }); auto contract = build::resolve_runtime_contract({consumer, alpha, beta}); - ASSERT_EQ(contract.requirements.size(), 2u); - EXPECT_EQ(contract.requirements[0].requester.canonical(), - "mcpplibs.consumer@1.0.0"); - EXPECT_EQ(contract.requirements[0].requester.sourceProvenance, - "path+consumer"); - - ASSERT_EQ(contract.providers.size(), 2u); - EXPECT_EQ(contract.providers[0].provider.canonical(), "alpha.backend@2.0.0"); - EXPECT_EQ(contract.providers[1].provider.canonical(), "beta.backend@3.0.0"); - EXPECT_NE(contract.providers[0].provider, contract.providers[1].provider); - EXPECT_TRUE(std::ranges::none_of(contract.providers, [](auto const& provider) { + expect(fatal(eq(contract.requirements.size(), 2u))); + expect(eq(contract.requirements[0].requester.canonical(), "mcpplibs.consumer@1.0.0")); + expect(eq(contract.requirements[0].requester.sourceProvenance, "path+consumer")); + + expect(fatal(eq(contract.providers.size(), 2u))); + expect(eq(contract.providers[0].provider.canonical(), "alpha.backend@2.0.0")); + expect(eq(contract.providers[1].provider.canonical(), "beta.backend@3.0.0")); + expect(ne(contract.providers[0].provider, contract.providers[1].provider)); + expect(std::ranges::none_of(contract.providers, [](auto const& provider) { return provider.provider.canonical() == "mcpplibs.consumer@1.0.0"; })); - ASSERT_EQ(contract.artifacts.size(), 2u); - EXPECT_EQ(contract.artifacts[0].provider.canonical(), "alpha.backend@2.0.0"); - EXPECT_EQ(contract.artifacts[1].provider.canonical(), "beta.backend@3.0.0"); - EXPECT_EQ(contract.artifacts[0].path, - (base / "alpha/lib/libbackend.so").lexically_normal()); - ASSERT_EQ(contract.linkIntent.libraries.size(), 2u); - EXPECT_EQ(contract.linkIntent.libraries[0], - (base / "alpha/lib/libbackend.a").lexically_normal().string()); - EXPECT_EQ(contract.linkIntent.libraries[1], "backend"); -} + expect(fatal(eq(contract.artifacts.size(), 2u))); + expect(eq(contract.artifacts[0].provider.canonical(), "alpha.backend@2.0.0")); + expect(eq(contract.artifacts[1].provider.canonical(), "beta.backend@3.0.0")); + expect(eq(contract.artifacts[0].path, (base / "alpha/lib/libbackend.so").lexically_normal())); + expect(fatal(eq(contract.linkIntent.libraries.size(), 2u))); + expect(eq(contract.linkIntent.libraries[0], (base / "alpha/lib/libbackend.a").lexically_normal().string())); + expect(eq(contract.linkIntent.libraries[1], "backend")); +}}; -TEST(RuntimeContract, SourceOwnsNoProviderSpecificSelectionOrProbeBranch) { +UTest source_owns_no_provider_specific_selection_or_probe_branch{"RuntimeContract/SourceOwnsNoProviderSpecificSelectionOrProbeBranch", [] { auto repo = std::filesystem::path(__FILE__).parent_path().parent_path().parent_path(); auto src = repo / "src"; - ASSERT_TRUE(std::filesystem::is_directory(src)) << src; + expect(fatal(std::filesystem::is_directory(src))) << src; // Intentionally assembled so the ownership gate does not flag its own // vocabulary if this test is ever moved under src/. const std::vector providerWords = { @@ -233,8 +228,7 @@ TEST(RuntimeContract, SourceOwnsNoProviderSpecificSelectionOrProbeBranch) { || lineText.find("case ") != std::string::npos; if (!branch) continue; for (auto const& word : providerWords) { - EXPECT_EQ(lineText.find(word), std::string::npos) - << it->path() << ':' << line + if (auto&& failed = expect(eq(lineText.find(word), std::string::npos)); !failed) failed << it->path() << ':' << line << " contains provider-specific selection logic"; } } @@ -244,16 +238,15 @@ TEST(RuntimeContract, SourceOwnsNoProviderSpecificSelectionOrProbeBranch) { const auto begin = pos > 240 ? pos - 240 : 0; const auto window = code.substr(begin, 480); for (auto const& launch : launchWords) { - EXPECT_EQ(window.find(launch), std::string::npos) - << it->path() + if (auto&& failed = expect(eq(window.find(launch), std::string::npos)); !failed) failed << it->path() << " couples a provider-specific word to a launched probe"; } } } } -} +}}; -TEST(RuntimeContract, XlingsSelectedFactsPrecedeDescriptorFallbacks) { +UTest xlings_selected_facts_precede_descriptor_fallbacks{"RuntimeContract/XlingsSelectedFactsPrecedeDescriptorFallbacks", [] { build::BuildPlan plan; plan.runtimeProviders.push_back({ .capability = "render.demo", @@ -278,16 +271,14 @@ TEST(RuntimeContract, XlingsSelectedFactsPrecedeDescriptorFallbacks) { }); build::merge_runtime_binding_contract(plan, binding); - ASSERT_EQ(plan.runtimeSearch.size(), 0u) + expect(fatal(eq(plan.runtimeSearch.size(), 0u))) << "a binding with no SubOS library view contributes no farm entry"; - ASSERT_EQ(plan.runtimeProviders.size(), 2u); - EXPECT_EQ(plan.runtimeProviders.front().provider.canonical(), - "xim.selected@4.0.0"); - ASSERT_EQ(plan.runtimeArtifacts.size(), 1u); - EXPECT_EQ(plan.runtimeArtifacts[0].provider.canonical(), - "xim.selected@4.0.0"); - EXPECT_EQ(plan.runtimeArtifacts[0].hostFingerprint, "host-2"); -} + expect(fatal(eq(plan.runtimeProviders.size(), 2u))); + expect(eq(plan.runtimeProviders.front().provider.canonical(), "xim.selected@4.0.0")); + expect(fatal(eq(plan.runtimeArtifacts.size(), 1u))); + expect(eq(plan.runtimeArtifacts[0].provider.canonical(), "xim.selected@4.0.0")); + expect(eq(plan.runtimeArtifacts[0].hostFingerprint, "host-2")); +}}; } // namespace @@ -324,52 +315,52 @@ std::size_t farm_entries(const std::vector& dirs) { } // namespace -TEST(RuntimeSearchClosure, HostTargetGetsTheFarm) { +UTest host_target_gets_the_farm{"RuntimeSearchClosure/HostTargetGetsTheFarm", [] { // An EXPLICIT Linux target is a farm target from anywhere, including a // macOS or Windows runner: the guards read the target, not the runner. - EXPECT_EQ(farm_entries(build::runtime_search_closure( - plan_for("x86_64-linux-gnu"), host_binding_with_farm())), 1u); + expect(eq(farm_entries(build::runtime_search_closure( + plan_for("x86_64-linux-gnu"), host_binding_with_farm())), 1u)); // An EMPTY triple means "this host", so the answer is the host's format — // and that is the point of the ELF guard, not an exception to it. Asserting // 1 unconditionally is what failed on the macOS and Windows runners. const std::size_t expectedForHost = mcpplibs::os::is_linux ? 1u : 0u; - EXPECT_EQ(farm_entries(build::runtime_search_closure( - plan_for(""), host_binding_with_farm())), expectedForHost) + expect(eq(farm_entries(build::runtime_search_closure( + plan_for(""), host_binding_with_farm())), expectedForHost)) << "an empty triple must resolve to this host's format"; -} +}}; -TEST(RuntimeSearchClosure, CrossTargetGetsNoFarm) { +UTest cross_target_gets_no_farm{"RuntimeSearchClosure/CrossTargetGetsNoFarm", [] { // Another architecture: the farm holds x86_64 objects. - EXPECT_EQ(farm_entries(build::runtime_search_closure( - plan_for("aarch64-linux-gnu"), host_binding_with_farm())), 0u); + expect(eq(farm_entries(build::runtime_search_closure( + plan_for("aarch64-linux-gnu"), host_binding_with_farm())), 0u)); // Another C library: a glibc farm on a musl program's search path is the // payload mixing rule B exists to prevent. - EXPECT_EQ(farm_entries(build::runtime_search_closure( - plan_for("x86_64-linux-musl"), host_binding_with_farm())), 0u); + expect(eq(farm_entries(build::runtime_search_closure( + plan_for("x86_64-linux-musl"), host_binding_with_farm())), 0u)); // Another format: DT_RPATH does not exist there at all. - EXPECT_EQ(farm_entries(build::runtime_search_closure( - plan_for("x86_64-windows-gnu"), host_binding_with_farm())), 0u); - EXPECT_EQ(farm_entries(build::runtime_search_closure( - plan_for("aarch64-macos"), host_binding_with_farm())), 0u); -} + expect(eq(farm_entries(build::runtime_search_closure( + plan_for("x86_64-windows-gnu"), host_binding_with_farm())), 0u)); + expect(eq(farm_entries(build::runtime_search_closure( + plan_for("aarch64-macos"), host_binding_with_farm())), 0u)); +}}; // A MISMATCH must be proven, not assumed. An undeclared SubOS has no runtime // identity, and refusing its own library view on that basis would be exactly // the absence-read-as-contradiction this release removes elsewhere. Caught by // e2e 220, which builds against a SubOS it creates and which therefore has no // `subos_info` to declare anything. -TEST(RuntimeSearchClosure, UndeclaredBindingStillGetsItsOwnFarm) { +UTest undeclared_binding_still_gets_its_own_farm{"RuntimeSearchClosure/UndeclaredBindingStillGetsItsOwnFarm", [] { auto b = host_binding_with_farm(); b.declared = false; b.runtimeId.clear(); - EXPECT_EQ(farm_entries(build::runtime_search_closure( - plan_for("x86_64-linux-gnu"), b)), 1u); + expect(eq(farm_entries(build::runtime_search_closure( + plan_for("x86_64-linux-gnu"), b)), 1u)); // …but the architecture guard still applies: unknown libc is not a licence // to ignore everything else. - EXPECT_EQ(farm_entries(build::runtime_search_closure( - plan_for("aarch64-linux-gnu"), b)), 0u); -} + expect(eq(farm_entries(build::runtime_search_closure( + plan_for("aarch64-linux-gnu"), b)), 0u)); +}}; // ─── the Windows runtime identity ──────────────────────────────────────── // @@ -379,19 +370,19 @@ TEST(RuntimeSearchClosure, UndeclaredBindingStillGetsItsOwnFarm) { // Windows SDKs produced one cache key — the version axis simply stopped // existing one layer down. -TEST(RuntimeIdentity, ProviderIsDispatchedOnNotPatternMatched) { +UTest provider_is_dispatched_on_not_pattern_matched{"RuntimeIdentity/ProviderIsDispatchedOnNotPatternMatched", [] { namespace rt = mcpp::runtime; - EXPECT_EQ(rt::runtime_provider("glibc@2.39"), "glibc"); - EXPECT_EQ(rt::runtime_provider("ucrt@10.0.26100.0"), "ucrt"); - EXPECT_EQ(rt::runtime_provider("macos_sdk@14.0"), "macos_sdk"); + expect(eq(rt::runtime_provider("glibc@2.39"), "glibc")); + expect(eq(rt::runtime_provider("ucrt@10.0.26100.0"), "ucrt")); + expect(eq(rt::runtime_provider("macos_sdk@14.0"), "macos_sdk")); // No identity at all is a DIFFERENT answer from "an identity belonging to // some other provider", and every consumer that spelled // `starts_with("glibc@")` collapsed the two. - EXPECT_TRUE(rt::runtime_provider("").empty()); - EXPECT_TRUE(rt::runtime_provider("nonsense").empty()); -} + expect(rt::runtime_provider("").empty()); + expect(rt::runtime_provider("nonsense").empty()); +}}; -TEST(RuntimeIdentity, TheSdkVersionReachesTheContractHash) { +UTest the_sdk_version_reaches_the_contract_hash{"RuntimeIdentity/TheSdkVersionReachesTheContractHash", [] { namespace rt = mcpp::runtime; rt::RuntimeBinding a; a.platform = "windows"; @@ -401,33 +392,33 @@ TEST(RuntimeIdentity, TheSdkVersionReachesTheContractHash) { rt::bind_windows_ucrt(a, "10.0.22621.0"); rt::bind_windows_ucrt(b, "10.0.26100.0"); - EXPECT_EQ(a.runtimeId, "ucrt@10.0.22621.0"); - EXPECT_EQ(b.runtimeId, "ucrt@10.0.26100.0"); - EXPECT_NE(a.contractHash, b.contractHash) + expect(eq(a.runtimeId, "ucrt@10.0.22621.0")); + expect(eq(b.runtimeId, "ucrt@10.0.26100.0")); + expect(ne(a.contractHash, b.contractHash)) << "two SDKs produced one contract hash, so they produce one build " "cache — which is the defect this identity closes"; - EXPECT_NE(a.contractHash, "unset") << "the hash was not re-derived"; -} + expect(ne(a.contractHash, "unset")) << "the hash was not re-derived"; +}}; -TEST(RuntimeIdentity, BindingUcrtIsIdempotentAndSkipsAnEmptyVersion) { +UTest binding_ucrt_is_idempotent_and_skips_an_empty_version{"RuntimeIdentity/BindingUcrtIsIdempotentAndSkipsAnEmptyVersion", [] { namespace rt = mcpp::runtime; rt::RuntimeBinding b; b.platform = "windows"; rt::bind_windows_ucrt(b, "10.0.26100.0"); auto once = b.contractHash; rt::bind_windows_ucrt(b, "10.0.26100.0"); - EXPECT_EQ(b.contractHash, once); + expect(eq(b.contractHash, once)); // A Windows box with no SDK still builds — toolchain SELECTION has to // work there — so there is simply nothing to declare. rt::RuntimeBinding empty; empty.contractHash = "keep"; rt::bind_windows_ucrt(empty, ""); - EXPECT_TRUE(empty.runtimeId.empty()); - EXPECT_EQ(empty.contractHash, "keep"); -} + expect(empty.runtimeId.empty()); + expect(eq(empty.contractHash, "keep")); +}}; -TEST(RuntimeIdentity, UcrtIsAFloorDeclarationNotAPrivatePayload) { +UTest ucrt_is_a_floor_declaration_not_a_private_payload{"RuntimeIdentity/UcrtIsAFloorDeclarationNotAPrivatePayload", [] { // The asymmetry with glibc, pinned so it cannot be "tidied up" later. // // `glibc@2.39` binds a payload: the headers and the .so are both in it and @@ -442,15 +433,15 @@ TEST(RuntimeIdentity, UcrtIsAFloorDeclarationNotAPrivatePayload) { rt::RuntimeBinding b; b.platform = "windows"; rt::bind_windows_ucrt(b, "10.0.26100.0"); - EXPECT_TRUE(b.libc.empty()) + expect(b.libc.empty()) << "ucrt was projected into the private-libc field"; - EXPECT_FALSE(b.loader.has_value()); - EXPECT_TRUE(b.libraryDirs.empty()); -} + expect(!b.loader.has_value()); + expect(b.libraryDirs.empty()); +}}; // #615: `runtime.deploy` in a descriptor -- the same `{ from, to }` entries as // mcpp.toml, and the same path rule. -TEST(RuntimeContract, XpkgReadsRuntimeDeployEntries) { +UTest xpkg_reads_runtime_deploy_entries{"RuntimeContract/XpkgReadsRuntimeDeployEntries", [] { constexpr auto lua = R"( package = { spec = "1", @@ -471,19 +462,18 @@ package = { )"; auto parsed = mf::synthesize_from_xpkg_lua( lua, "icd", "1.0.0", mcpp::toolchain::platform::HostPlatform::current()); - ASSERT_TRUE(parsed) << parsed.error().format(); + if (auto&& failed = expect(fatal(bool(parsed))); !failed) failed << parsed.error().format(); auto const& d = parsed->runtimeConfig.linkIntent.deploy; - ASSERT_EQ(d.size(), 2u); - EXPECT_EQ(d[0].from, std::filesystem::path("share/vulkan/icd.d/lvp_icd.json")); - EXPECT_EQ(d[0].to, "vulkan/icd.d"); - EXPECT_EQ(d[1].from, std::filesystem::path("share/readme.txt")); - EXPECT_EQ(d[1].to, "."); + expect(fatal(eq(d.size(), 2u))); + expect(eq(d[0].from, std::filesystem::path("share/vulkan/icd.d/lvp_icd.json"))); + expect(eq(d[0].to, "vulkan/icd.d")); + expect(eq(d[1].from, std::filesystem::path("share/readme.txt"))); + expect(eq(d[1].to, ".")); // The key after it is still read, so the entry loop consumed exactly its table. - EXPECT_EQ(parsed->runtimeConfig.linkIntent.deployFiles, - std::vector{"bin/backend.dll"}); -} + expect(eq(parsed->runtimeConfig.linkIntent.deployFiles, std::vector{"bin/backend.dll"})); +}}; -TEST(RuntimeContract, XpkgRuntimeDeployRefusesAnEscapingDestination) { +UTest xpkg_runtime_deploy_refuses_an_escaping_destination{"RuntimeContract/XpkgRuntimeDeployRefusesAnEscapingDestination", [] { constexpr auto lua = R"( package = { spec = "1", namespace = "acme", name = "icd", @@ -496,7 +486,6 @@ package = { )"; auto parsed = mf::synthesize_from_xpkg_lua( lua, "icd", "1.0.0", mcpp::toolchain::platform::HostPlatform::current()); - ASSERT_FALSE(parsed); - EXPECT_NE(parsed.error().message.find("runtime.deploy[1]: `to` has a `.` or `..` component"), - std::string::npos) << parsed.error().message; -} + expect(fatal(!parsed)); + if (auto&& failed = expect(ne(parsed.error().message.find("runtime.deploy[1]: `to` has a `.` or `..` component"), std::string::npos)); !failed) failed << parsed.error().message; +}}; diff --git a/tests/unit/test_runtime_placement.cpp b/tests/unit/test_runtime_placement.cpp index f179694c1..5f182eecf 100644 --- a/tests/unit/test_runtime_placement.cpp +++ b/tests/unit/test_runtime_placement.cpp @@ -4,12 +4,14 @@ // the resolver reads versions through an injected function, so every // combination of kinds, versions and contracts is a unit test rather than a // Windows runner's. -#include import std; +import mcpplibs.mtest; import mcpp.pack.binfmt; import mcpp.build.runtime_placement; +using namespace mcpplibs::mtest; + namespace fs = std::filesystem; namespace rp = mcpp::build::runtime_placement; using mcpp::pack::binfmt::PeVersion; @@ -95,36 +97,36 @@ std::string make_pe(std::optional fileVersion, std::uint8_t linkerMaj } // namespace -TEST(PeVersionReader, ReadsTheFixedFileVersionAndTheLinkerVersion) { +UTest reads_the_fixed_file_version_and_the_linker_version{"PeVersionReader/ReadsTheFixedFileVersionAndTheLinkerVersion", [] { const auto image = make_pe(PeVersion{14, 44, 35112, 1}, 14, 44); auto v = mcpp::pack::binfmt::pe_file_version(std::string_view{image}); - ASSERT_TRUE(v.has_value()); - EXPECT_EQ(*v, (PeVersion{14, 44, 35112, 1})); - EXPECT_EQ(v->str(), "14.44.35112.1"); + expect(fatal(v.has_value())); + expect(eq(*v, (PeVersion{14, 44, 35112, 1}))); + expect(eq(v->str(), "14.44.35112.1")); auto l = mcpp::pack::binfmt::pe_linker_version(std::string_view{image}); - ASSERT_TRUE(l.has_value()); - EXPECT_EQ(l->major, 14); - EXPECT_EQ(l->minor, 44); -} + expect(fatal(l.has_value())); + expect(eq(l->major, 14)); + expect(eq(l->minor, 44)); +}}; // A reader that cannot answer does not decide: no resource section, a // truncated image and a file that is not PE all read as "unknown". -TEST(PeVersionReader, AnImageWithoutAVersionReadsAsUnknown) { +UTest an_image_without_a_version_reads_as_unknown{"PeVersionReader/AnImageWithoutAVersionReadsAsUnknown", [] { const auto noResource = make_pe(std::nullopt, 14, 0); - EXPECT_FALSE(mcpp::pack::binfmt::pe_file_version(std::string_view{noResource})); + expect(!mcpp::pack::binfmt::pe_file_version(std::string_view{noResource})); // The linker field is still there: lld-link writes 14.0. auto l = mcpp::pack::binfmt::pe_linker_version(std::string_view{noResource}); - ASSERT_TRUE(l.has_value()); - EXPECT_EQ(l->minor, 0); + expect(fatal(l.has_value())); + expect(eq(l->minor, 0)); auto truncated = make_pe(PeVersion{14, 44, 1, 0}, 14, 44); truncated.resize(0x420); - EXPECT_FALSE(mcpp::pack::binfmt::pe_file_version(std::string_view{truncated})); - EXPECT_FALSE(mcpp::pack::binfmt::pe_file_version(std::string_view{"\x7f" "ELF not a PE"})); - EXPECT_FALSE(mcpp::pack::binfmt::pe_linker_version(std::string_view{"MZ"})); -} + expect(!mcpp::pack::binfmt::pe_file_version(std::string_view{truncated})); + expect(!mcpp::pack::binfmt::pe_file_version(std::string_view{"\x7f" "ELF not a PE"})); + expect(!mcpp::pack::binfmt::pe_linker_version(std::string_view{"MZ"})); +}}; -TEST(PeVersionReader, ReadsAVersionFromAFile) { +UTest reads_a_version_from_a_file{"PeVersionReader/ReadsAVersionFromAFile", [] { const auto dir = fs::temp_directory_path() / std::format("mcpp-pever-{}", std::chrono::steady_clock::now().time_since_epoch().count()); fs::create_directories(dir); @@ -134,12 +136,12 @@ TEST(PeVersionReader, ReadsAVersionFromAFile) { out.write(image.data(), static_cast(image.size())); } auto v = mcpp::pack::binfmt::pe_file_version(dir / "vcruntime140.dll"); - ASSERT_TRUE(v.has_value()); - EXPECT_EQ(v->str(), "14.51.36231.0"); - EXPECT_FALSE(mcpp::pack::binfmt::pe_file_version(dir / "missing.dll")); + expect(fatal(v.has_value())); + expect(eq(v->str(), "14.51.36231.0")); + expect(!mcpp::pack::binfmt::pe_file_version(dir / "missing.dll")); std::error_code ec; fs::remove_all(dir, ec); -} +}}; // ── the resolver ─────────────────────────────────────────────────────────── @@ -225,7 +227,7 @@ rp::Input input_for(const Case& c, std::map>& // Every combination of the contract's rule, a declared runtime file, the // toolset's set and a dependency's set, with every version relation. The // properties are the rule of mcpp.build.runtime_placement, stated once each. -TEST(RuntimePlacement, EveryCombinationOfKindsVersionsAndContracts) { +UTest every_combination_of_kinds_versions_and_contracts{"RuntimePlacement/EveryCombinationOfKindsVersionsAndContracts", [] { int cases = 0; for (auto policy : {rp::CrtPolicy::Carry, rp::CrtPolicy::System, rp::CrtPolicy::Static, rp::CrtPolicy::NotApplicable}) @@ -248,16 +250,16 @@ TEST(RuntimePlacement, EveryCombinationOfKindsVersionsAndContracts) { const auto in = input_for(c, versions); const auto d = rp::resolve(in); ++cases; - SCOPED_TRACE(describe(c)); + /* trace: describe(c) */; // P1. One file per destination, compared without case. std::set dests; for (auto const& p : d.placed) - EXPECT_TRUE(dests.insert(rp::fold(p.dest.generic_string())).second) + expect(bool(dests.insert(rp::fold(p.dest.generic_string())).second)) << "two placements for " << p.dest.string(); // P2. An ordinary name is never touched by the runtime rule. - EXPECT_TRUE(dests.contains("bin/qt6core.dll")); + expect(dests.contains("bin/qt6core.dll")); std::vector crt; for (auto const& p : d.placed) @@ -266,20 +268,20 @@ TEST(RuntimePlacement, EveryCombinationOfKindsVersionsAndContracts) { if (policy == rp::CrtPolicy::System) { // P3. Host-coupled: no copy of the runtime from any source; a // declared one is refused; a dependency's is stated. - EXPECT_TRUE(crt.empty()); - EXPECT_EQ(!d.errors.empty(), declared.has_value()); - if (derived) EXPECT_FALSE(d.notes.empty()); + expect(crt.empty()); + expect(eq(!d.errors.empty(), declared.has_value())); + if (derived) expect(!d.notes.empty()); continue; } - EXPECT_TRUE(d.errors.empty()); + expect(d.errors.empty()); if (policy == rp::CrtPolicy::NotApplicable) { // P4. Off the MSVC ABI the names take the kind order: a // declaration, else the derived file. if (declared) { auto it = std::ranges::find_if(d.placed, [](auto const& p) { return rp::fold(p.dest.filename().string()) == "vcruntime140.dll"; }); - ASSERT_NE(it, d.placed.end()); - EXPECT_EQ(it->kind, rp::Kind::Declared); + expect(fatal(ne(it, d.placed.end()))); + expect(eq(it->kind, rp::Kind::Declared)); } continue; } @@ -288,14 +290,14 @@ TEST(RuntimePlacement, EveryCombinationOfKindsVersionsAndContracts) { if (declared) { auto it = std::ranges::find_if(crt, [](auto const* p) { return rp::fold(p->dest.filename().string()) == "vcruntime140.dll"; }); - ASSERT_NE(it, crt.end()); - EXPECT_EQ((*it)->kind, rp::Kind::Declared); + expect(fatal(ne(it, crt.end()))); + expect(eq((*it)->kind, rp::Kind::Declared)); // P6. D2: older than the toolset's runtime is a warning; an // unreadable version is a note; neither is said otherwise. const bool older = toolset && *declared == Ver::Old; - EXPECT_EQ(!d.warnings.empty(), older); + expect(eq(!d.warnings.empty(), older)); } else { - EXPECT_TRUE(d.warnings.empty()); + expect(d.warnings.empty()); } // P7. The rest of the runtime is ONE set: every non-declared runtime @@ -305,7 +307,7 @@ TEST(RuntimePlacement, EveryCombinationOfKindsVersionsAndContracts) { if (p->kind != rp::Kind::Declared) origins.insert(std::format("{}:{}", rp::to_string(p->kind), p->sources.front().parent_path().string())); - EXPECT_LE(origins.size(), 1u) << "the runtime set was mixed"; + expect(le(origins.size(), 1u)) << "the runtime set was mixed"; // P8. Which set: the toolset's, unless the dependency's is complete, // readable and strictly newer (D1); without the toolset's, the @@ -314,15 +316,15 @@ TEST(RuntimePlacement, EveryCombinationOfKindsVersionsAndContracts) { && (!toolset || (complete && *derived == Ver::New)); const bool anyPlaced = !origins.empty(); if (policy == rp::CrtPolicy::Carry) - EXPECT_TRUE(anyPlaced || (declared && !toolset)); + expect(anyPlaced || (declared && !toolset)); if (anyPlaced) { const bool fromDerived = origins.begin()->starts_with("derived:"); - EXPECT_EQ(fromDerived, derivedWins); + expect(eq(fromDerived, derivedWins)); } // P9. Under self-contained nothing is placed unless something brings // a runtime name. if (policy == rp::CrtPolicy::Static && !declared && !derived) - EXPECT_TRUE(crt.empty()); + expect(crt.empty()); // P10. A dependency's set that is not placed is stated once, as a // packaging fault; one that is placed over the toolset's is stated @@ -330,22 +332,22 @@ TEST(RuntimePlacement, EveryCombinationOfKindsVersionsAndContracts) { if (derived && toolset) { const auto fault = std::ranges::count_if(d.notes, [](auto const& n) { return n.find("does not carry the compiler's runtime") != std::string::npos; }); - EXPECT_EQ(fault, derivedWins ? 0 : 1); + expect(eq(fault, derivedWins ? 0 : 1)); if (derivedWins) - EXPECT_TRUE(std::ranges::any_of(d.notes, [](auto const& n) { + expect(std::ranges::any_of(d.notes, [](auto const& n) { return n.find("newer than the toolset's") != std::string::npos; })); } // P11. An unreadable version is said, and decides nothing. if (derived == Ver::Unreadable && toolset && complete) - EXPECT_TRUE(std::ranges::any_of(d.notes, [](auto const& n) { + expect(std::ranges::any_of(d.notes, [](auto const& n) { return n.find("could not be read") != std::string::npos; })); } - EXPECT_GT(cases, 100); -} + expect(gt(cases, 100)); +}}; // Two search directories offering one ordinary name stay two sources of one // destination (SPEC-007 R4.2): `mcpp stage` compares their bytes. -TEST(RuntimePlacement, TwoDerivedCopiesOfAnOrdinaryNameAreOneDestination) { +UTest two_derived_copies_of_an_ordinary_name_are_one_destination{"RuntimePlacement/TwoDerivedCopiesOfAnOrdinaryNameAreOneDestination", [] { rp::Input in; in.crt = rp::CrtPolicy::Carry; in.candidates.push_back({{"/a/libfoo.dll"}, "bin/libfoo.dll", rp::Kind::Derived}); @@ -353,24 +355,24 @@ TEST(RuntimePlacement, TwoDerivedCopiesOfAnOrdinaryNameAreOneDestination) { in.candidates.push_back({{"/c/libbar.dll"}, "bin/libbar.dll", rp::Kind::Derived}); in.candidates.push_back({{"/p/libbar.dll"}, "bin/libbar.dll", rp::Kind::Declared}); const auto d = rp::resolve(in); - ASSERT_EQ(d.placed.size(), 2u); + expect(fatal(eq(d.placed.size(), 2u))); auto foo = std::ranges::find_if(d.placed, [](auto const& p) { return p.dest == "bin/libfoo.dll"; }); - ASSERT_NE(foo, d.placed.end()); - EXPECT_EQ(foo->sources.size(), 2u); + expect(fatal(ne(foo, d.placed.end()))); + expect(eq(foo->sources.size(), 2u)); auto bar = std::ranges::find_if(d.placed, [](auto const& p) { return p.dest == "bin/libbar.dll"; }); - ASSERT_NE(bar, d.placed.end()); - EXPECT_EQ(bar->kind, rp::Kind::Declared); - EXPECT_EQ(bar->sources, std::vector{"/p/libbar.dll"}); -} + expect(fatal(ne(bar, d.placed.end()))); + expect(eq(bar->kind, rp::Kind::Declared)); + expect(eq(bar->sources, std::vector{"/p/libbar.dll"})); +}}; // A runtime name a declaration places under a subdirectory is not the // program's runtime and is not governed by the rule. -TEST(RuntimePlacement, ARuntimeNameInASubdirectoryIsAnOrdinaryFile) { +UTest a_runtime_name_in_a_subdirectory_is_an_ordinary_file{"RuntimePlacement/ARuntimeNameInASubdirectoryIsAnOrdinaryFile", [] { rp::Input in; in.crt = rp::CrtPolicy::System; in.candidates.push_back({{"/p/vcruntime140.dll"}, "bin/plugins/vcruntime140.dll", rp::Kind::Declared}); const auto d = rp::resolve(in); - EXPECT_TRUE(d.errors.empty()); - ASSERT_EQ(d.placed.size(), 1u); - EXPECT_EQ(d.placed.front().dest, "bin/plugins/vcruntime140.dll"); -} + expect(d.errors.empty()); + expect(fatal(eq(d.placed.size(), 1u))); + expect(eq(d.placed.front().dest, "bin/plugins/vcruntime140.dll")); +}}; diff --git a/tests/unit/test_runtime_search.cpp b/tests/unit/test_runtime_search.cpp index e978aef92..7a0d986af 100644 --- a/tests/unit/test_runtime_search.cpp +++ b/tests/unit/test_runtime_search.cpp @@ -6,11 +6,13 @@ // Everything downstream (the DT_RPATH mcpp emits, what `mcpp pack` strips, // what the closure resolver walks) is a consequence of these four answers. -#include import std; +import mcpplibs.mtest; import mcpp.runtime.search; +using namespace mcpplibs::mtest; + namespace search = mcpp::runtime::search; using search::Origin; @@ -20,114 +22,114 @@ namespace { // symlink view rewritten by every `xlings install`, a payload directory is // written once, and an already-linked artifact must not have its libc changed // out from under it by a later install. -TEST(RuntimeSearch, PayloadOutranksFarm) { - EXPECT_LT(search::rank(Origin::Payload), search::rank(Origin::SubosFarm)); - EXPECT_LT(search::rank(Origin::Package), search::rank(Origin::SubosFarm)); - EXPECT_LT(search::rank(Origin::SubosFarm), search::rank(Origin::HostDefault)); +UTest payload_outranks_farm{"RuntimeSearch/PayloadOutranksFarm", [] { + expect(lt(search::rank(Origin::Payload), search::rank(Origin::SubosFarm))); + expect(lt(search::rank(Origin::Package), search::rank(Origin::SubosFarm))); + expect(lt(search::rank(Origin::SubosFarm), search::rank(Origin::HostDefault))); // #415 — the artifact's own directory (`$ORIGIN`) sits between the packages // and the farm. This is the ordering #414 was about: with the farm ahead of // `$ORIGIN` an artifact linked one libX11 and loaded another. A pinned // payload still wins, because that is the one thing nothing re-points. - EXPECT_LT(search::rank(Origin::Package), search::rank(Origin::Artifact)); - EXPECT_LT(search::rank(Origin::Artifact), search::rank(Origin::SubosFarm)); - EXPECT_LT(search::rank(Origin::Payload), search::rank(Origin::Artifact)); -} + expect(lt(search::rank(Origin::Package), search::rank(Origin::Artifact))); + expect(lt(search::rank(Origin::Artifact), search::rank(Origin::SubosFarm))); + expect(lt(search::rank(Origin::Payload), search::rank(Origin::Artifact))); +}}; // `mcpp pack` asks this to decide what may not be baked into a distributable. // The host's own /usr/lib is the only one that is NOT this machine's private // state — relying on it is an ordinary host requirement, not a dependency on // the build box. -TEST(RuntimeSearch, MachineLocalIsEverythingButTheHostDefaults) { - EXPECT_TRUE(search::is_machine_local(Origin::Payload)); - EXPECT_TRUE(search::is_machine_local(Origin::Package)); - EXPECT_TRUE(search::is_machine_local(Origin::SubosFarm)); - EXPECT_FALSE(search::is_machine_local(Origin::HostDefault)); +UTest machine_local_is_everything_but_the_host_defaults{"RuntimeSearch/MachineLocalIsEverythingButTheHostDefaults", [] { + expect(search::is_machine_local(Origin::Payload)); + expect(search::is_machine_local(Origin::Package)); + expect(search::is_machine_local(Origin::SubosFarm)); + expect(!search::is_machine_local(Origin::HostDefault)); // `$ORIGIN` is NOT machine-local, and getting this backwards would be // worse than the gap it closes: it is resolved by the loader relative to // the artifact, so it means the same thing wherever that artifact is // copied — `pack` REWRITES everything else into this form. Marking it // local would make pack reject the one entry it is trying to produce. - EXPECT_FALSE(search::is_machine_local(Origin::Artifact)); -} + expect(!search::is_machine_local(Origin::Artifact)); +}}; // These strings are PUBLISHED — they are the `origin` field of every entry in // `resolution.json`'s `runtime.search.closure`, which CI, `mcpp why runtime` and // e2e 219 all read. Renaming one is a wire-format change, so it is pinned here // rather than left to whatever `to_string` happens to say. -TEST(RuntimeSearch, OriginNamesArePublishedAndStable) { - EXPECT_EQ(search::to_string(Origin::Payload), "payload"); - EXPECT_EQ(search::to_string(Origin::Package), "package"); - EXPECT_EQ(search::to_string(Origin::Artifact), "artifact"); - EXPECT_EQ(search::to_string(Origin::SubosFarm), "subos_farm"); - EXPECT_EQ(search::to_string(Origin::HostDefault), "host_default"); -} - -TEST(RuntimeSearch, OrderedSortsByRankNotByInsertion) { +UTest origin_names_are_published_and_stable{"RuntimeSearch/OriginNamesArePublishedAndStable", [] { + expect(eq(search::to_string(Origin::Payload), "payload")); + expect(eq(search::to_string(Origin::Package), "package")); + expect(eq(search::to_string(Origin::Artifact), "artifact")); + expect(eq(search::to_string(Origin::SubosFarm), "subos_farm")); + expect(eq(search::to_string(Origin::HostDefault), "host_default")); +}}; + +UTest ordered_sorts_by_rank_not_by_insertion{"RuntimeSearch/OrderedSortsByRankNotByInsertion", [] { std::vector input{ {"/farm", Origin::SubosFarm}, {"/pkg", Origin::Package}, {"/pay", Origin::Payload}, }; auto out = search::ordered(input); - ASSERT_EQ(out.size(), 3u); - EXPECT_EQ(out[0].path, std::filesystem::path("/pay")); - EXPECT_EQ(out[1].path, std::filesystem::path("/pkg")); - EXPECT_EQ(out[2].path, std::filesystem::path("/farm")); -} + expect(fatal(eq(out.size(), 3u))); + expect(eq(out[0].path, std::filesystem::path("/pay"))); + expect(eq(out[1].path, std::filesystem::path("/pkg"))); + expect(eq(out[2].path, std::filesystem::path("/farm"))); +}}; // Stable within a rank, because insertion order still decides among peers — // libglvnd resolves GL vendors by exactly that order, so reordering equals // would change which driver a program gets. -TEST(RuntimeSearch, OrderedIsStableWithinARank) { +UTest ordered_is_stable_within_a_rank{"RuntimeSearch/OrderedIsStableWithinARank", [] { std::vector input{ {"/pay/b", Origin::Payload}, {"/pay/a", Origin::Payload}, {"/pay/c", Origin::Payload}, }; auto out = search::ordered(input); - ASSERT_EQ(out.size(), 3u); - EXPECT_EQ(out[0].path, std::filesystem::path("/pay/b")); - EXPECT_EQ(out[1].path, std::filesystem::path("/pay/a")); - EXPECT_EQ(out[2].path, std::filesystem::path("/pay/c")); -} + expect(fatal(eq(out.size(), 3u))); + expect(eq(out[0].path, std::filesystem::path("/pay/b"))); + expect(eq(out[1].path, std::filesystem::path("/pay/a"))); + expect(eq(out[2].path, std::filesystem::path("/pay/c"))); +}}; // The same directory reachable two ways is ONE entry, and it takes the // stronger origin. A payload that is also visible through the farm view is a // payload; ranking it as farm would push the real libc behind everything else // the farm holds — the exact failure the ordering exists to prevent. -TEST(RuntimeSearch, DuplicatePathKeepsTheStrongestOrigin) { +UTest duplicate_path_keeps_the_strongest_origin{"RuntimeSearch/DuplicatePathKeepsTheStrongestOrigin", [] { std::vector input{ {"/shared", Origin::SubosFarm}, {"/shared", Origin::Payload}, {"/other", Origin::SubosFarm}, }; auto out = search::ordered(input); - ASSERT_EQ(out.size(), 2u); - EXPECT_EQ(out[0].path, std::filesystem::path("/shared")); - EXPECT_EQ(out[0].origin, Origin::Payload); - EXPECT_EQ(out[1].origin, Origin::SubosFarm); -} + expect(fatal(eq(out.size(), 2u))); + expect(eq(out[0].path, std::filesystem::path("/shared"))); + expect(eq(out[0].origin, Origin::Payload)); + expect(eq(out[1].origin, Origin::SubosFarm)); +}}; -TEST(RuntimeSearch, OrderedNormalizesAndDropsEmpty) { +UTest ordered_normalizes_and_drops_empty{"RuntimeSearch/OrderedNormalizesAndDropsEmpty", [] { std::vector input{ {"", Origin::Payload}, {"/a/./b/", Origin::Payload}, {"/a/b", Origin::Payload}, }; auto out = search::ordered(input); - ASSERT_EQ(out.size(), 1u); - EXPECT_EQ(out[0].path, std::filesystem::path("/a/b")); -} + expect(fatal(eq(out.size(), 1u))); + expect(eq(out[0].path, std::filesystem::path("/a/b"))); +}}; // The opt-out is a CROSS-REPO name (openxlings/xlings#540). Pinning the // spelling here means a rename shows up as a failed test rather than as a // silently ineffective declaration — the failure mode of an env var nobody // reads is that everything looks fine. -TEST(RuntimeSearch, LinkerOptOutIsSpelledOnce) { - EXPECT_EQ(search::kLinkerPathInjectionOptOut, "XLINGS_SUBOS_LD_PATHS"); - EXPECT_EQ(search::kLinkerPathInjectionOptOutValue, "0"); -} +UTest linker_opt_out_is_spelled_once{"RuntimeSearch/LinkerOptOutIsSpelledOnce", [] { + expect(eq(search::kLinkerPathInjectionOptOut, "XLINGS_SUBOS_LD_PATHS")); + expect(eq(search::kLinkerPathInjectionOptOutValue, "0")); +}}; } // namespace diff --git a/tests/unit/test_runtime_selection.cpp b/tests/unit/test_runtime_selection.cpp index 0b845520b..ec4830038 100644 --- a/tests/unit/test_runtime_selection.cpp +++ b/tests/unit/test_runtime_selection.cpp @@ -1,12 +1,14 @@ -#include import std; +import mcpplibs.mtest; import mcpp.config; import mcpp.manifest; import mcpplibs.os.platform; import mcpp.runtime.binding; import mcpp.provider.runtime_selection; +using namespace mcpplibs::mtest; + namespace runtime = mcpp::provider::runtime; namespace { @@ -88,105 +90,104 @@ struct RuntimeHome { } }; -TEST(RuntimeSelection, AbsenceMeansMcppDefault) { +UTest absence_means_mcpp_default{"RuntimeSelection/AbsenceMeansMcppDefault", [] { mcpp::manifest::Manifest root; auto selected = runtime::select_runtime(root, std::nullopt, "/repo"); - ASSERT_TRUE(selected.has_value()) << selected.error(); - EXPECT_EQ(selected->mode, runtime::RuntimeSelection::Mode::McppDefault); - EXPECT_EQ(selected->source, runtime::RuntimeSelection::Source::DefaultPolicy); - EXPECT_EQ(selected->subosName, "default"); - EXPECT_EQ(selected->ownerRoot, std::filesystem::path("/repo")); -} - -TEST(RuntimeSelection, ExplicitDefaultRemainsNamed) { + if (auto&& failed = expect(fatal(selected.has_value())); !failed) failed << selected.error(); + expect(eq(selected->mode, runtime::RuntimeSelection::Mode::McppDefault)); + expect(eq(selected->source, runtime::RuntimeSelection::Source::DefaultPolicy)); + expect(eq(selected->subosName, "default")); + expect(eq(selected->ownerRoot, std::filesystem::path("/repo"))); +}}; + +UTest explicit_default_remains_named{"RuntimeSelection/ExplicitDefaultRemainsNamed", [] { auto root = named("default"); auto selected = runtime::select_runtime(root, std::nullopt, "/repo"); - ASSERT_TRUE(selected.has_value()) << selected.error(); - EXPECT_EQ(selected->mode, runtime::RuntimeSelection::Mode::NamedSubos); - EXPECT_EQ(selected->source, runtime::RuntimeSelection::Source::Manifest); - EXPECT_EQ(selected->subosName, "default"); -} + if (auto&& failed = expect(fatal(selected.has_value())); !failed) failed << selected.error(); + expect(eq(selected->mode, runtime::RuntimeSelection::Mode::NamedSubos)); + expect(eq(selected->source, runtime::RuntimeSelection::Source::Manifest)); + expect(eq(selected->subosName, "default")); +}}; -TEST(RuntimeSelection, WorkspaceRootOverridesMember) { +UTest workspace_root_overrides_member{"RuntimeSelection/WorkspaceRootOverridesMember", [] { auto member = named("member-dev"); auto workspace = named("workspace-el8"); auto selected = runtime::select_runtime( member, std::cref(workspace), "/repo/member", "/repo"); - ASSERT_TRUE(selected.has_value()) << selected.error(); - EXPECT_EQ(selected->subosName, "workspace-el8"); - EXPECT_EQ(selected->ownerRoot, std::filesystem::path("/repo")); -} + if (auto&& failed = expect(fatal(selected.has_value())); !failed) failed << selected.error(); + expect(eq(selected->subosName, "workspace-el8")); + expect(eq(selected->ownerRoot, std::filesystem::path("/repo"))); +}}; -TEST(RuntimeSelection, MemberAppliesWhenItIsTheIndependentRoot) { +UTest member_applies_when_it_is_the_independent_root{"RuntimeSelection/MemberAppliesWhenItIsTheIndependentRoot", [] { auto member = named("member-dev"); auto selected = runtime::select_runtime(member, std::nullopt, "/repo/member"); - ASSERT_TRUE(selected.has_value()) << selected.error(); - EXPECT_EQ(selected->subosName, "member-dev"); - EXPECT_EQ(selected->ownerRoot, std::filesystem::path("/repo/member")); -} + if (auto&& failed = expect(fatal(selected.has_value())); !failed) failed << selected.error(); + expect(eq(selected->subosName, "member-dev")); + expect(eq(selected->ownerRoot, std::filesystem::path("/repo/member"))); +}}; -TEST(RuntimeSelection, EmptyAndPathLikeNamesAreRejected) { +UTest empty_and_path_like_names_are_rejected{"RuntimeSelection/EmptyAndPathLikeNamesAreRejected", [] { for (auto bad : {"", ".", "..", "a/b", "a\\b", "a:b"}) { auto root = named(bad); auto selected = runtime::select_runtime(root, std::nullopt, "/repo"); - EXPECT_FALSE(selected.has_value()) << bad; + expect(!selected.has_value()) << bad; } -} +}}; -TEST(RuntimeSelection, DependencyDeclarationCannotAffectConsumerSelection) { +UTest dependency_declaration_cannot_affect_consumer_selection{"RuntimeSelection/DependencyDeclarationCannotAffectConsumerSelection", [] { mcpp::manifest::Manifest consumer; auto dependency = named("dependency-only"); (void)dependency; auto selected = runtime::select_runtime(consumer, std::nullopt, "/consumer"); - ASSERT_TRUE(selected.has_value()) << selected.error(); - EXPECT_EQ(selected->mode, runtime::RuntimeSelection::Mode::McppDefault); - EXPECT_EQ(selected->subosName, "default"); -} + if (auto&& failed = expect(fatal(selected.has_value())); !failed) failed << selected.error(); + expect(eq(selected->mode, runtime::RuntimeSelection::Mode::McppDefault)); + expect(eq(selected->subosName, "default")); +}}; -TEST(RuntimeBinding, DefaultAlwaysUsesConfiguredMcppHomeDefault) { +UTest default_always_uses_configured_mcpp_home_default{"RuntimeBinding/DefaultAlwaysUsesConfiguredMcppHomeDefault", [] { RuntimeHome h; const auto expected = h.cfg.xlingsHome() / "subos" / "default"; h.write(expected); mcpp::manifest::Manifest root; auto selection = runtime::select_runtime(root, std::nullopt, h.dir / "repo"); - ASSERT_TRUE(selection); + expect(fatal(bool(selection))); auto binding = mcpp::runtime::resolve_runtime_binding( *selection, {}, h.cfg.xlingsHome()); - ASSERT_TRUE(binding.has_value()) << binding.error(); - EXPECT_EQ(binding->subosDir, expected); - EXPECT_EQ(binding->runtimeId, "glibc@2.39"); + if (auto&& failed = expect(fatal(binding.has_value())); !failed) failed << binding.error(); + expect(eq(binding->subosDir, expected)); + expect(eq(binding->runtimeId, "glibc@2.39")); if constexpr (mcpplibs::os::is_linux) - EXPECT_EQ(binding->libc, "glibc@2.39"); + expect(eq(binding->libc, "glibc@2.39")); else - EXPECT_TRUE(binding->libc.empty()); - EXPECT_EQ(binding->provenance, "mcpp_default"); - EXPECT_FALSE(binding->contractHash.empty()); - ASSERT_EQ(binding->runtimeProviders.size(), 1u); - EXPECT_EQ(binding->runtimeProviders[0].provider.name, "renderer"); - ASSERT_EQ(binding->runtimeArtifacts.size(), 1u); - EXPECT_EQ(binding->runtimeArtifacts[0].path, - (expected / "runtime/renderer.bin").lexically_normal()); -} - -TEST(RuntimeBinding, ExplicitDefaultUsesGlobalDefaultButKeepsNamedIdentity) { + expect(binding->libc.empty()); + expect(eq(binding->provenance, "mcpp_default")); + expect(!(binding->contractHash.empty())); + expect(fatal(eq(binding->runtimeProviders.size(), 1u))); + expect(eq(binding->runtimeProviders[0].provider.name, "renderer")); + expect(fatal(eq(binding->runtimeArtifacts.size(), 1u))); + expect(eq(binding->runtimeArtifacts[0].path, (expected / "runtime/renderer.bin").lexically_normal())); +}}; + +UTest explicit_default_uses_global_default_but_keeps_named_identity{"RuntimeBinding/ExplicitDefaultUsesGlobalDefaultButKeepsNamedIdentity", [] { RuntimeHome h; const auto expected = h.cfg.xlingsHome() / "subos" / "default"; h.write(expected); auto manifest = named("default"); auto selection = runtime::select_runtime(manifest, std::nullopt, h.dir / "repo"); - ASSERT_TRUE(selection); + expect(fatal(bool(selection))); auto binding = mcpp::runtime::resolve_runtime_binding( *selection, {}, h.cfg.xlingsHome()); - ASSERT_TRUE(binding.has_value()) << binding.error(); - EXPECT_EQ(binding->subosDir, expected); - EXPECT_EQ(binding->provenance, "named_subos"); - EXPECT_EQ(binding->selection.mode, runtime::RuntimeSelection::Mode::NamedSubos); -} + if (auto&& failed = expect(fatal(binding.has_value())); !failed) failed << binding.error(); + expect(eq(binding->subosDir, expected)); + expect(eq(binding->provenance, "named_subos")); + expect(eq(binding->selection.mode, runtime::RuntimeSelection::Mode::NamedSubos)); +}}; -TEST(RuntimeBinding, PhysicalSubosViewReconcilesAStaleDeclaredGlibcIdentity) { +UTest physical_subos_view_reconciles_a_stale_declared_glibc_identity{"RuntimeBinding/PhysicalSubosViewReconcilesAStaleDeclaredGlibcIdentity", [] { if constexpr (!mcpplibs::os::is_linux) - GTEST_SKIP() << "glibc SubOS views only exist on Linux"; + skip("glibc SubOS views only exist on Linux"); RuntimeHome h; const auto subos = h.cfg.xlingsHome() / "subos" / "default"; @@ -195,22 +196,21 @@ TEST(RuntimeBinding, PhysicalSubosViewReconcilesAStaleDeclaredGlibcIdentity) { mcpp::manifest::Manifest root; auto selection = runtime::select_runtime(root, std::nullopt, h.dir / "repo"); - ASSERT_TRUE(selection); + expect(fatal(bool(selection))); auto binding = mcpp::runtime::resolve_runtime_binding( *selection, {}, h.cfg.xlingsHome()); - ASSERT_TRUE(binding.has_value()) << binding.error(); - - EXPECT_EQ(binding->runtimeId, "glibc@2.44"); - EXPECT_EQ(binding->libc, "glibc@2.44"); - ASSERT_EQ(binding->libraryDirs.size(), 1u); - EXPECT_EQ(binding->libraryDirs.front(), - h.cfg.xlingsHome() / "data" / "xpkgs" - / "xim-x-glibc" / "2.44" / "lib"); -} + if (auto&& failed = expect(fatal(binding.has_value())); !failed) failed << binding.error(); + + expect(eq(binding->runtimeId, "glibc@2.44")); + expect(eq(binding->libc, "glibc@2.44")); + expect(fatal(eq(binding->libraryDirs.size(), 1u))); + expect(eq(binding->libraryDirs.front(), h.cfg.xlingsHome() / "data" / "xpkgs" + / "xim-x-glibc" / "2.44" / "lib")); +}}; -TEST(RuntimeBinding, BrokenSubosViewFallsBackOnlyToTheExactDeclaredPayload) { +UTest broken_subos_view_falls_back_only_to_the_exact_declared_payload{"RuntimeBinding/BrokenSubosViewFallsBackOnlyToTheExactDeclaredPayload", [] { if constexpr (!mcpplibs::os::is_linux) - GTEST_SKIP() << "glibc SubOS views only exist on Linux"; + skip("glibc SubOS views only exist on Linux"); RuntimeHome h; const auto subos = h.cfg.xlingsHome() / "subos" / "default"; @@ -224,39 +224,38 @@ TEST(RuntimeBinding, BrokenSubosViewFallsBackOnlyToTheExactDeclaredPayload) { mcpp::manifest::Manifest root; auto selection = runtime::select_runtime(root, std::nullopt, h.dir / "repo"); - ASSERT_TRUE(selection); + expect(fatal(bool(selection))); auto binding = mcpp::runtime::resolve_runtime_binding( *selection, {}, h.cfg.xlingsHome()); - ASSERT_TRUE(binding.has_value()) << binding.error(); - - EXPECT_EQ(binding->runtimeId, "glibc@2.44"); - ASSERT_EQ(binding->libraryDirs.size(), 1u); - EXPECT_EQ(binding->libraryDirs.front(), payload); - EXPECT_EQ(binding->loader, - std::optional( - payload / "ld-linux-x86-64.so.2")); -} + if (auto&& failed = expect(fatal(binding.has_value())); !failed) failed << binding.error(); -TEST(RuntimeBinding, NamedSubosIsOwnedByTheRootAndMissingIsHardError) { + expect(eq(binding->runtimeId, "glibc@2.44")); + expect(fatal(eq(binding->libraryDirs.size(), 1u))); + expect(eq(binding->libraryDirs.front(), payload)); + expect(eq(binding->loader, std::optional( + payload / "ld-linux-x86-64.so.2"))); +}}; + +UTest named_subos_is_owned_by_the_root_and_missing_is_hard_error{"RuntimeBinding/NamedSubosIsOwnedByTheRootAndMissingIsHardError", [] { RuntimeHome h; auto manifest = named("el8"); auto selection = runtime::select_runtime(manifest, std::nullopt, h.dir / "repo"); - ASSERT_TRUE(selection); + expect(fatal(bool(selection))); auto missing = mcpp::runtime::resolve_runtime_binding( *selection, {}, h.cfg.xlingsHome()); - ASSERT_FALSE(missing.has_value()); - EXPECT_NE(missing.error().find("el8"), std::string::npos); + expect(fatal(!missing.has_value())); + expect(ne(missing.error().find("el8"), std::string::npos)); const auto expected = h.dir / "repo" / ".mcpp" / ".xlings" / "subos" / "el8"; h.write(expected); auto binding = mcpp::runtime::resolve_runtime_binding( *selection, {}, h.cfg.xlingsHome()); - ASSERT_TRUE(binding.has_value()) << binding.error(); - EXPECT_EQ(binding->subosDir, expected); -} + if (auto&& failed = expect(fatal(binding.has_value())); !failed) failed << binding.error(); + expect(eq(binding->subosDir, expected)); +}}; -TEST(RuntimeBinding, NamedEnvironmentsHaveDistinctContractsAndRoundTrip) { +UTest named_environments_have_distinct_contracts_and_round_trip{"RuntimeBinding/NamedEnvironmentsHaveDistinctContractsAndRoundTrip", [] { RuntimeHome h; auto el8Manifest = named("el8"); auto devManifest = named("dev"); @@ -264,8 +263,8 @@ TEST(RuntimeBinding, NamedEnvironmentsHaveDistinctContractsAndRoundTrip) { el8Manifest, std::nullopt, h.dir / "repo"); auto devSelection = runtime::select_runtime( devManifest, std::nullopt, h.dir / "repo"); - ASSERT_TRUE(el8Selection); - ASSERT_TRUE(devSelection); + expect(fatal(bool(el8Selection))); + expect(fatal(bool(devSelection))); h.write(h.dir / "repo" / ".mcpp" / ".xlings" / "subos" / "el8"); h.write(h.dir / "repo" / ".mcpp" / ".xlings" / "subos" / "dev"); @@ -273,29 +272,29 @@ TEST(RuntimeBinding, NamedEnvironmentsHaveDistinctContractsAndRoundTrip) { *el8Selection, {}, h.cfg.xlingsHome()); auto dev = mcpp::runtime::resolve_runtime_binding( *devSelection, {}, h.cfg.xlingsHome()); - ASSERT_TRUE(el8); - ASSERT_TRUE(dev); - EXPECT_NE(el8->contractHash, dev->contractHash); + expect(fatal(bool(el8))); + expect(fatal(bool(dev))); + expect(ne(el8->contractHash, dev->contractHash)); auto encoded = mcpp::runtime::serialize_runtime_binding(*el8); auto decoded = mcpp::runtime::deserialize_runtime_binding(encoded); - ASSERT_TRUE(decoded.has_value()) << decoded.error(); - EXPECT_EQ(decoded->contractHash, el8->contractHash); + if (auto&& failed = expect(fatal(decoded.has_value())); !failed) failed << decoded.error(); + expect(eq(decoded->contractHash, el8->contractHash)); if constexpr (mcpplibs::os::is_linux) { - ASSERT_FALSE(decoded->hostLibc.empty()); - EXPECT_EQ(decoded->hostLibc, "2.43"); + expect(fatal(!(decoded->hostLibc.empty()))); + expect(eq(decoded->hostLibc, "2.43")); } else { - EXPECT_TRUE(decoded->hostLibc.empty()); + expect(decoded->hostLibc.empty()); } - EXPECT_EQ(decoded->subosDir, el8->subosDir); - ASSERT_EQ(decoded->environment.size(), 1u); - EXPECT_EQ(decoded->environment[0].var, "DRIVERS"); - ASSERT_EQ(decoded->runtimeProviders.size(), 1u); - EXPECT_EQ(decoded->runtimeProviders[0].provider.namespace_, "xim"); - ASSERT_EQ(decoded->runtimeArtifacts.size(), 1u); - EXPECT_EQ(decoded->runtimeArtifacts[0].hostFingerprint, "host-2"); - EXPECT_TRUE(decoded->declared); -} + expect(eq(decoded->subosDir, el8->subosDir)); + expect(fatal(eq(decoded->environment.size(), 1u))); + expect(eq(decoded->environment[0].var, "DRIVERS")); + expect(fatal(eq(decoded->runtimeProviders.size(), 1u))); + expect(eq(decoded->runtimeProviders[0].provider.namespace_, "xim")); + expect(fatal(eq(decoded->runtimeArtifacts.size(), 1u))); + expect(eq(decoded->runtimeArtifacts[0].hostFingerprint, "host-2")); + expect(bool(decoded->declared)); +}}; // ── absence degrades, contradiction fails (openxlings/xlings#543) ────────── // @@ -306,12 +305,12 @@ TEST(RuntimeBinding, NamedEnvironmentsHaveDistinctContractsAndRoundTrip) { // already paid for applies here too: data that is missing or newer must not // invalidate the program that reads it. -TEST(RuntimeBindingDegradation, UndeclaredSubosStillResolves) { +UTest undeclared_subos_still_resolves{"RuntimeBindingDegradation/UndeclaredSubosStillResolves", [] { RuntimeHome h; auto manifest = named("bare"); auto selection = runtime::select_runtime(manifest, std::nullopt, h.dir / "repo"); - ASSERT_TRUE(selection); + expect(fatal(bool(selection))); // Exists, describes nothing. The common state of a freshly created SubOS, // and the permanent state on a platform whose xlings does not write the // block at all. @@ -320,49 +319,48 @@ TEST(RuntimeBindingDegradation, UndeclaredSubosStillResolves) { auto binding = mcpp::runtime::resolve_runtime_binding( *selection, {}, h.cfg.xlingsHome()); - ASSERT_TRUE(binding.has_value()) - << "absence must degrade, not fail: " << binding.error(); - EXPECT_FALSE(binding->declared); - EXPECT_FALSE(binding->note.empty()) + if (auto&& failed = expect(fatal(binding.has_value())); !failed) failed << "absence must degrade, not fail: " << binding.error(); + expect(!binding->declared); + expect(!(binding->note.empty())) << "a degradation nobody reports is indistinguishable from none"; - EXPECT_TRUE(binding->runtimeId.empty()); - EXPECT_FALSE(binding->hermetic()); -} + expect(binding->runtimeId.empty()); + expect(!binding->hermetic()); +}}; // The cache must survive it too. A degraded binding legitimately has schema 0 // and no runtime identity, and a completeness check that demands those fields // would make every cached degraded binding undecodable — sending the build // back down the slow path on every single invocation, forever. -TEST(RuntimeBindingDegradation, UndeclaredBindingRoundTrips) { +UTest undeclared_binding_round_trips{"RuntimeBindingDegradation/UndeclaredBindingRoundTrips", [] { RuntimeHome h; auto manifest = named("bare"); auto selection = runtime::select_runtime(manifest, std::nullopt, h.dir / "repo"); - ASSERT_TRUE(selection); + expect(fatal(bool(selection))); std::filesystem::create_directories( h.dir / "repo" / ".mcpp" / ".xlings" / "subos" / "bare"); auto binding = mcpp::runtime::resolve_runtime_binding( *selection, {}, h.cfg.xlingsHome()); - ASSERT_TRUE(binding.has_value()); + expect(fatal(binding.has_value())); auto decoded = mcpp::runtime::deserialize_runtime_binding( mcpp::runtime::serialize_runtime_binding(*binding)); - ASSERT_TRUE(decoded.has_value()) << decoded.error(); - EXPECT_EQ(decoded->contractHash, binding->contractHash); - EXPECT_FALSE(decoded->declared); - EXPECT_EQ(decoded->note, binding->note); -} + if (auto&& failed = expect(fatal(decoded.has_value())); !failed) failed << decoded.error(); + expect(eq(decoded->contractHash, binding->contractHash)); + expect(!decoded->declared); + expect(eq(decoded->note, binding->note)); +}}; // A newer schema is read, not refused. `subos_info::read` already documents // this ("a HIGHER one on disk is still read"); the consumer used to demand // equality, so the day xlings writes schema 2 every build on every platform // would have stopped at once. -TEST(RuntimeBindingDegradation, NewerSchemaIsReadNotRefused) { +UTest newer_schema_is_read_not_refused{"RuntimeBindingDegradation/NewerSchemaIsReadNotRefused", [] { RuntimeHome h; auto manifest = named("future"); auto selection = runtime::select_runtime(manifest, std::nullopt, h.dir / "repo"); - ASSERT_TRUE(selection); + expect(fatal(bool(selection))); auto subos = h.dir / "repo" / ".mcpp" / ".xlings" / "subos" / "future"; std::filesystem::create_directories(subos); std::ofstream(subos / ".xlings.json") << R"({ @@ -376,29 +374,28 @@ TEST(RuntimeBindingDegradation, NewerSchemaIsReadNotRefused) { auto binding = mcpp::runtime::resolve_runtime_binding( *selection, {}, h.cfg.xlingsHome()); - ASSERT_TRUE(binding.has_value()) - << "a newer contract must not invalidate an older mcpp: " + if (auto&& failed = expect(fatal(binding.has_value())); !failed) failed << "a newer contract must not invalidate an older mcpp: " << binding.error(); - EXPECT_TRUE(binding->declared); - EXPECT_EQ(binding->runtimeId, "glibc@2.39"); - EXPECT_NE(binding->note.find("newer"), std::string::npos) + expect(bool(binding->declared)); + expect(eq(binding->runtimeId, "glibc@2.39")); + expect(ne(binding->note.find("newer"), std::string::npos)) << "reading a newer schema silently is the other half of the defect"; -} +}}; // The boundary. A SubOS the user NAMED and that is not there cannot be // satisfied under any interpretation, and picking a different one would make // one mcpp.toml mean different ABIs on different machines. -TEST(RuntimeBindingDegradation, MissingSubosStillFails) { +UTest missing_subos_still_fails{"RuntimeBindingDegradation/MissingSubosStillFails", [] { RuntimeHome h; auto manifest = named("never-created"); auto selection = runtime::select_runtime(manifest, std::nullopt, h.dir / "repo"); - ASSERT_TRUE(selection); + expect(fatal(bool(selection))); auto binding = mcpp::runtime::resolve_runtime_binding( *selection, {}, h.cfg.xlingsHome()); - ASSERT_FALSE(binding.has_value()) + expect(fatal(!binding.has_value())) << "a contradiction must be reported, not degraded"; - EXPECT_NE(binding.error().find("does not exist"), std::string::npos); -} + expect(ne(binding.error().find("does not exist"), std::string::npos)); +}}; } // namespace diff --git a/tests/unit/test_scaffold.cpp b/tests/unit/test_scaffold.cpp index 9d346b472..8b55d6c1f 100644 --- a/tests/unit/test_scaffold.cpp +++ b/tests/unit/test_scaffold.cpp @@ -1,14 +1,16 @@ -#include import std; +import mcpplibs.mtest; import mcpp.scaffold; import mcpp.scaffold.project_name; import mcpp.pm.dependency_selector; import mcpplibs.os.fs; +using namespace mcpplibs::mtest; + namespace { -TEST(TemplateSpec, ParsesSharedExactPackageGrammar) { +UTest parses_shared_exact_package_grammar{"TemplateSpec/ParsesSharedExactPackageGrammar", [] { struct Row { std::string input; std::optional ns; @@ -32,32 +34,32 @@ TEST(TemplateSpec, ParsesSharedExactPackageGrammar) { for (auto const& row : rows) { auto parsed = mcpp::scaffold::parse_template_spec(row.input); - ASSERT_TRUE(parsed.has_value()) << row.input; - EXPECT_EQ(parsed->package.namespace_, row.ns) << row.input; - EXPECT_EQ(parsed->package.name, row.name) << row.input; - EXPECT_EQ(parsed->version, row.version) << row.input; - EXPECT_EQ(parsed->templateName, row.tmpl) << row.input; - EXPECT_FALSE(parsed->legacyList) << row.input; + expect(fatal(parsed.has_value())) << row.input; + expect(eq(parsed->package.namespace_, row.ns)) << row.input; + expect(eq(parsed->package.name, row.name)) << row.input; + expect(eq(parsed->version, row.version)) << row.input; + expect(eq(parsed->templateName, row.tmpl)) << row.input; + expect(!parsed->legacyList) << row.input; } -} +}}; -TEST(TemplateSpec, BarePackageNormalizesToMcpplibs) { +UTest bare_package_normalizes_to_mcpplibs{"TemplateSpec/BarePackageNormalizesToMcpplibs", [] { auto parsed = mcpp::scaffold::parse_template_spec("imgui"); - ASSERT_TRUE(parsed.has_value()); + expect(fatal(parsed.has_value())); auto coord = mcpp::pm::normalize_package_selector(parsed->package); - EXPECT_EQ(coord.namespace_, "mcpplibs"); - EXPECT_EQ(coord.shortName, "imgui"); -} + expect(eq(coord.namespace_, "mcpplibs")); + expect(eq(coord.shortName, "imgui")); +}}; -TEST(TemplateSpec, RecognizesTrailingColonOnlyAsLegacyListAlias) { +UTest recognizes_trailing_colon_only_as_legacy_list_alias{"TemplateSpec/RecognizesTrailingColonOnlyAsLegacyListAlias", [] { auto parsed = mcpp::scaffold::parse_template_spec("ocornut.imgui@1.92.8:"); - ASSERT_TRUE(parsed.has_value()); - EXPECT_TRUE(parsed->legacyList); - EXPECT_EQ(parsed->version, "1.92.8"); - EXPECT_FALSE(parsed->templateName.has_value()); -} + expect(fatal(parsed.has_value())); + expect(bool(parsed->legacyList)); + expect(eq(parsed->version, "1.92.8")); + expect(!(parsed->templateName.has_value())); +}}; -TEST(TemplateSpec, RejectsAmbiguousEmptyOrUnsafeComponents) { +UTest rejects_ambiguous_empty_or_unsafe_components{"TemplateSpec/RejectsAmbiguousEmptyOrUnsafeComponents", [] { const std::vector invalid{ "", "@1.0.0", ":starter", ".imgui@1.0", "ocornut..imgui", "ocornut.imgui@", "ocornut.imgui:bad:extra", @@ -68,90 +70,89 @@ TEST(TemplateSpec, RejectsAmbiguousEmptyOrUnsafeComponents) { }; for (auto const& input : invalid) { auto parsed = mcpp::scaffold::parse_template_spec(input); - EXPECT_FALSE(parsed.has_value()) << input; - if (!parsed) EXPECT_FALSE(parsed.error().message.empty()) << input; + expect(!parsed.has_value()) << input; + if (!parsed) expect(!(parsed.error().message.empty())) << input; } -} +}}; -TEST(TemplateSelection, SoleTemplateWithoutExplicitDefaultAutoWins) { +UTest sole_template_without_explicit_default_auto_wins{"TemplateSelection/SoleTemplateWithoutExplicitDefaultAutoWins", [] { std::vector entries{ {"solo", {.description = "only choice", .isDefault = false}}, }; auto chosen = mcpp::scaffold::select_template(entries, std::nullopt); - ASSERT_TRUE(chosen.has_value()); - EXPECT_EQ((*chosen)->name, "solo"); -} + expect(fatal(chosen.has_value())); + expect(eq((*chosen)->name, "solo")); +}}; -TEST(TemplateSelection, OneExplicitDefaultWinsAmongMany) { +UTest one_explicit_default_wins_among_many{"TemplateSelection/OneExplicitDefaultWinsAmongMany", [] { std::vector entries{ {"a", {.isDefault = false}}, {"b", {.isDefault = true}}, }; auto chosen = mcpp::scaffold::select_template(entries, std::nullopt); - ASSERT_TRUE(chosen.has_value()); - EXPECT_EQ((*chosen)->name, "b"); -} + expect(fatal(chosen.has_value())); + expect(eq((*chosen)->name, "b")); +}}; -TEST(TemplateSelection, MultipleWithoutDefaultAreAmbiguous) { +UTest multiple_without_default_are_ambiguous{"TemplateSelection/MultipleWithoutDefaultAreAmbiguous", [] { std::vector entries{ {"a", {.isDefault = false}}, {"b", {.isDefault = false}}, }; auto chosen = mcpp::scaffold::select_template(entries, std::nullopt); - ASSERT_FALSE(chosen.has_value()); - EXPECT_NE(chosen.error().find("a"), std::string::npos); - EXPECT_NE(chosen.error().find("b"), std::string::npos); -} + expect(fatal(!chosen.has_value())); + expect(ne(chosen.error().find("a"), std::string::npos)); + expect(ne(chosen.error().find("b"), std::string::npos)); +}}; -TEST(TemplateSelection, ExplicitNameWinsAndUnknownListsChoices) { +UTest explicit_name_wins_and_unknown_lists_choices{"TemplateSelection/ExplicitNameWinsAndUnknownListsChoices", [] { std::vector entries{ {"a", {.isDefault = false}}, {"b", {.isDefault = false}}, }; auto chosen = mcpp::scaffold::select_template(entries, std::string_view{"b"}); - ASSERT_TRUE(chosen.has_value()); - EXPECT_EQ((*chosen)->name, "b"); + expect(fatal(chosen.has_value())); + expect(eq((*chosen)->name, "b")); auto missing = mcpp::scaffold::select_template( entries, std::string_view{"missing"}); - ASSERT_FALSE(missing.has_value()); - EXPECT_NE(missing.error().find("a"), std::string::npos); - EXPECT_NE(missing.error().find("b"), std::string::npos); -} + expect(fatal(!missing.has_value())); + expect(ne(missing.error().find("a"), std::string::npos)); + expect(ne(missing.error().find("b"), std::string::npos)); +}}; -TEST(TemplateSelection, EmptyOrMultipleDefaultsAreProviderErrors) { +UTest empty_or_multiple_defaults_are_provider_errors{"TemplateSelection/EmptyOrMultipleDefaultsAreProviderErrors", [] { std::vector empty; auto noTemplates = mcpp::scaffold::select_template(empty, std::nullopt); - ASSERT_FALSE(noTemplates.has_value()); - EXPECT_NE(noTemplates.error().find("no templates"), std::string::npos); + expect(fatal(!noTemplates.has_value())); + expect(ne(noTemplates.error().find("no templates"), std::string::npos)); std::vector entries{ {"a", {.isDefault = true}}, {"b", {.isDefault = true}}, }; auto duplicate = mcpp::scaffold::select_template(entries, std::nullopt); - ASSERT_FALSE(duplicate.has_value()); - EXPECT_NE(duplicate.error().find("more than one default"), - std::string::npos); -} + expect(fatal(!duplicate.has_value())); + expect(ne(duplicate.error().find("more than one default"), std::string::npos)); +}}; -TEST(ProjectName, AcceptsPortableBareAndQualifiedNames) { +UTest accepts_portable_bare_and_qualified_names{"ProjectName/AcceptsPortableBareAndQualifiedNames", [] { auto bare = mcpp::scaffold::validate_project_name("hello-world"); - ASSERT_TRUE(bare.has_value()); - EXPECT_EQ(bare->directoryName, "hello-world"); - EXPECT_EQ(bare->name, "hello-world"); - EXPECT_TRUE(bare->namespace_.empty()); - EXPECT_EQ(bare->qualifiedName, "hello-world"); + expect(fatal(bare.has_value())); + expect(eq(bare->directoryName, "hello-world")); + expect(eq(bare->name, "hello-world")); + expect(bare->namespace_.empty()); + expect(eq(bare->qualifiedName, "hello-world")); auto qualified = mcpp::scaffold::validate_project_name("acme.tools.app"); - ASSERT_TRUE(qualified.has_value()); - EXPECT_EQ(qualified->directoryName, "acme.tools.app"); - EXPECT_EQ(qualified->namespace_, "acme.tools"); - EXPECT_EQ(qualified->name, "app"); - EXPECT_EQ(qualified->qualifiedName, "acme.tools.app"); -} - -TEST(ProjectName, RejectsPathEscapeControlsReservedNamesAndLegacyMarker) { + expect(fatal(qualified.has_value())); + expect(eq(qualified->directoryName, "acme.tools.app")); + expect(eq(qualified->namespace_, "acme.tools")); + expect(eq(qualified->name, "app")); + expect(eq(qualified->qualifiedName, "acme.tools.app")); +}}; + +UTest rejects_path_escape_controls_reserved_names_and_legacy_marker{"ProjectName/RejectsPathEscapeControlsReservedNamesAndLegacyMarker", [] { const std::vector invalid{ "", ".", "..", "/absolute", "a/b", "a\\b", "a:b", "a\"b", "a?b", "a*b", "a|b", "ab", "trail.", "trail ", @@ -161,12 +162,12 @@ TEST(ProjectName, RejectsPathEscapeControlsReservedNamesAndLegacyMarker) { }; for (auto const& name : invalid) { auto result = mcpp::scaffold::validate_project_name(name); - EXPECT_FALSE(result.has_value()) << name; - if (!result) EXPECT_FALSE(result.error().message.empty()) << name; + expect(!result.has_value()) << name; + if (!result) expect(!(result.error().message.empty())) << name; } -} +}}; -TEST(RenderTokens, ExpandsCompleteVocabularyWithoutRescanningValues) { +UTest expands_complete_vocabulary_without_rescanning_values{"RenderTokens/ExpandsCompleteVocabularyWithoutRescanningValues", [] { mcpp::scaffold::RenderVars vars{ .projectName = "app-{{template.name}}", .projectNamespace = "acme", @@ -183,25 +184,23 @@ TEST(RenderTokens, ExpandsCompleteVocabularyWithoutRescanningValues) { "{{template.package.selector}}|{{template.package.version}}|" "{{template.name}}|{{self.name}}|{{self.version}}", vars); - ASSERT_TRUE(rendered.has_value()) << rendered.error().message; - EXPECT_EQ(*rendered, - "app-{{template.name}}|acme|acme.app|widgets.ui|starter|" - "widgets.ui.starter|2.0.0|window|widgets.ui.starter|2.0.0"); -} + if (auto&& failed = expect(fatal(rendered.has_value())); !failed) failed << rendered.error().message; + expect(eq(*rendered, "app-{{template.name}}|acme|acme.app|widgets.ui|starter|" + "widgets.ui.starter|2.0.0|window|widgets.ui.starter|2.0.0")); +}}; -TEST(RenderTokens, RejectsUnknownOrUnterminatedTokens) { +UTest rejects_unknown_or_unterminated_tokens{"RenderTokens/RejectsUnknownOrUnterminatedTokens", [] { mcpp::scaffold::RenderVars vars; auto unknown = mcpp::scaffold::render_tokens("{{project.typo}}", vars); - ASSERT_FALSE(unknown.has_value()); - EXPECT_NE(unknown.error().message.find("project.typo"), std::string::npos); + expect(fatal(!unknown.has_value())); + expect(ne(unknown.error().message.find("project.typo"), std::string::npos)); auto unterminated = mcpp::scaffold::render_tokens("{{project.name", vars); - ASSERT_FALSE(unterminated.has_value()); - EXPECT_NE(unterminated.error().message.find("unterminated"), - std::string::npos); -} + expect(fatal(!unterminated.has_value())); + expect(ne(unterminated.error().message.find("unterminated"), std::string::npos)); +}}; -TEST(ScaffoldTransaction, RollbackRemovesSiblingStagingTree) { +UTest rollback_removes_sibling_staging_tree{"ScaffoldTransaction/RollbackRemovesSiblingStagingTree", [] { auto parent = std::filesystem::temp_directory_path() / std::format("mcpp-scaffold-rollback-{}", std::chrono::steady_clock::now().time_since_epoch().count()); @@ -209,47 +208,47 @@ TEST(ScaffoldTransaction, RollbackRemovesSiblingStagingTree) { std::filesystem::path stage; { auto tx = mcpp::scaffold::ScaffoldTransaction::begin(parent, "app"); - ASSERT_TRUE(tx.has_value()) << tx.error(); + if (auto&& failed = expect(fatal(tx.has_value())); !failed) failed << tx.error(); stage = tx->staging_path(); std::ofstream(stage / "partial.txt") << "partial"; - EXPECT_TRUE(std::filesystem::exists(stage)); + expect(std::filesystem::exists(stage)); } - EXPECT_FALSE(std::filesystem::exists(stage)); - EXPECT_FALSE(std::filesystem::exists(parent / "app")); + expect(!std::filesystem::exists(stage)); + expect(!std::filesystem::exists(parent / "app")); std::filesystem::remove_all(parent); -} +}}; -TEST(ScaffoldTransaction, CommitIsAtomicAndRenameFailureRollsBack) { +UTest commit_is_atomic_and_rename_failure_rolls_back{"ScaffoldTransaction/CommitIsAtomicAndRenameFailureRollsBack", [] { auto parent = std::filesystem::temp_directory_path() / std::format("mcpp-scaffold-commit-{}", std::chrono::steady_clock::now().time_since_epoch().count()); std::filesystem::create_directories(parent); { auto tx = mcpp::scaffold::ScaffoldTransaction::begin(parent, "ok"); - ASSERT_TRUE(tx.has_value()) << tx.error(); + if (auto&& failed = expect(fatal(tx.has_value())); !failed) failed << tx.error(); std::ofstream(tx->staging_path() / "done.txt") << "done"; auto committed = tx->commit(); - ASSERT_TRUE(committed.has_value()) << committed.error(); - EXPECT_TRUE(std::filesystem::exists(parent / "ok" / "done.txt")); - EXPECT_FALSE(std::filesystem::exists(tx->staging_path())); + if (auto&& failed = expect(fatal(committed.has_value())); !failed) failed << committed.error(); + expect(std::filesystem::exists(parent / "ok" / "done.txt")); + expect(!(std::filesystem::exists(tx->staging_path()))); } { auto tx = mcpp::scaffold::ScaffoldTransaction::begin(parent, "collision"); - ASSERT_TRUE(tx.has_value()) << tx.error(); + if (auto&& failed = expect(fatal(tx.has_value())); !failed) failed << tx.error(); auto stage = tx->staging_path(); std::ofstream(stage / "partial.txt") << "partial"; std::filesystem::create_directories(parent / "collision"); auto committed = tx->commit(); - ASSERT_FALSE(committed.has_value()); - EXPECT_TRUE(std::filesystem::exists(stage)); + expect(fatal(!committed.has_value())); + expect(std::filesystem::exists(stage)); } for (auto const& entry : std::filesystem::directory_iterator(parent)) { - EXPECT_FALSE(entry.path().filename().string().starts_with(".mcpp-new-")); + expect(!(entry.path().filename().string().starts_with(".mcpp-new-"))); } std::filesystem::remove_all(parent); -} +}}; -TEST(ScaffoldFilesystem, AtomicRenameNeverReplacesExistingTarget) { +UTest atomic_rename_never_replaces_existing_target{"ScaffoldFilesystem/AtomicRenameNeverReplacesExistingTarget", [] { auto parent = std::filesystem::temp_directory_path() / std::format("mcpp-scaffold-rename-{}", std::chrono::steady_clock::now().time_since_epoch().count()); @@ -262,21 +261,21 @@ TEST(ScaffoldFilesystem, AtomicRenameNeverReplacesExistingTarget) { auto collision = mcpplibs::os::fs::place( source, target); - ASSERT_FALSE(collision.has_value()); - EXPECT_TRUE(std::filesystem::exists(source / "payload.txt")); - EXPECT_TRUE(std::filesystem::exists(target / "owner.txt")); + expect(fatal(!collision.has_value())); + expect(std::filesystem::exists(source / "payload.txt")); + expect(std::filesystem::exists(target / "owner.txt")); std::filesystem::remove_all(target); auto committed = mcpplibs::os::fs::place( source, target); - ASSERT_TRUE(committed.has_value()) << committed.error().message(); - EXPECT_FALSE(std::filesystem::exists(source)); - EXPECT_TRUE(std::filesystem::exists(target / "payload.txt")); + if (auto&& failed = expect(fatal(committed.has_value())); !failed) failed << committed.error().message(); + expect(!std::filesystem::exists(source)); + expect(std::filesystem::exists(target / "payload.txt")); std::filesystem::remove_all(parent); -} +}}; #if !defined(_WIN32) -TEST(ScaffoldTransaction, ReadWriteAndCopyFailuresRollbackCompletely) { +UTest read_write_and_copy_failures_rollback_completely{"ScaffoldTransaction/ReadWriteAndCopyFailuresRollbackCompletely", [] { namespace fs = std::filesystem; auto parent = fs::temp_directory_path() / std::format("mcpp-scaffold-iofail-{}", @@ -295,15 +294,15 @@ TEST(ScaffoldTransaction, ReadWriteAndCopyFailuresRollbackCompletely) { fs::path readStage; { auto tx = mcpp::scaffold::ScaffoldTransaction::begin(parent, "read-app"); - ASSERT_TRUE(tx.has_value()) << tx.error(); + if (auto&& failed = expect(fatal(tx.has_value())); !failed) failed << tx.error(); readStage = tx->staging_path(); auto result = mcpp::scaffold::instantiate( readProvider, readStage, vars); - EXPECT_FALSE(result.has_value()); + expect(!result.has_value()); fs::permissions(unreadable, fs::perms::owner_read); } - EXPECT_FALSE(fs::exists(readStage)); - EXPECT_FALSE(fs::exists(parent / "read-app")); + expect(!fs::exists(readStage)); + expect(!fs::exists(parent / "read-app")); auto exercise_locked_destination = [&](std::string_view caseName, std::string_view sourceName) { @@ -314,31 +313,31 @@ TEST(ScaffoldTransaction, ReadWriteAndCopyFailuresRollbackCompletely) { { auto tx = mcpp::scaffold::ScaffoldTransaction::begin( parent, std::format("{}-app", caseName)); - EXPECT_TRUE(tx.has_value()); + expect(tx.has_value()); if (!tx) return; stage = tx->staging_path(); fs::create_directories(stage / "locked"); fs::permissions(stage / "locked", fs::perms::owner_read | fs::perms::owner_exec); auto result = mcpp::scaffold::instantiate(provider, stage, vars); - EXPECT_FALSE(result.has_value()) << caseName; + expect(!result.has_value()) << caseName; fs::permissions(stage / "locked", fs::perms::owner_all); } - EXPECT_FALSE(fs::exists(stage)) << caseName; - EXPECT_FALSE(fs::exists(parent / std::format("{}-app", caseName))) + expect(!fs::exists(stage)) << caseName; + expect(!(fs::exists(parent / std::format("{}-app", caseName)))) << caseName; }; exercise_locked_destination("write", "mcpp.toml.in"); exercise_locked_destination("copy", "data.bin"); for (auto const& entry : fs::directory_iterator(parent)) { - EXPECT_FALSE(entry.path().filename().string().starts_with(".mcpp-new-")); + expect(!(entry.path().filename().string().starts_with(".mcpp-new-"))); } fs::remove_all(parent); -} +}}; #endif -TEST(SelfDependencyInjection, ComparesCanonicalPackageIdentity) { +UTest compares_canonical_package_identity{"SelfDependencyInjection/ComparesCanonicalPackageIdentity", [] { auto root = std::filesystem::temp_directory_path() / std::format("mcpp-inject-{}", std::chrono::steady_clock::now().time_since_epoch().count()); @@ -362,7 +361,7 @@ widget = "1.0.0" }; auto injected = mcpp::scaffold::inject_self_dependency( manifest, vars, {"gui"}); - ASSERT_TRUE(injected.has_value()) << injected.error(); + if (auto&& failed = expect(fatal(injected.has_value())); !failed) failed << injected.error(); std::string text; { std::ifstream is(manifest); @@ -370,22 +369,20 @@ widget = "1.0.0" ss << is.rdbuf(); text = ss.str(); } - EXPECT_NE(text.find("[dependencies.acme]\nwidget = { version = \"2.0.0\", features = [\"gui\"] }"), - std::string::npos); - EXPECT_NE(text.find("[dependencies.compat]\nwidget = \"1.0.0\""), - std::string::npos); + expect(ne(text.find("[dependencies.acme]\nwidget = { version = \"2.0.0\", features = [\"gui\"] }"), std::string::npos)); + expect(ne(text.find("[dependencies.compat]\nwidget = \"1.0.0\""), std::string::npos)); auto again = mcpp::scaffold::inject_self_dependency( manifest, vars, {"gui"}); - ASSERT_TRUE(again.has_value()) << again.error(); + if (auto&& failed = expect(fatal(again.has_value())); !failed) failed << again.error(); { std::ifstream is(manifest); std::stringstream ss; ss << is.rdbuf(); - EXPECT_EQ(ss.str(), text); + expect(eq(ss.str(), text)); } std::filesystem::remove_all(root); -} +}}; } // namespace @@ -400,7 +397,7 @@ widget = "1.0.0" // includes . Measured: the injected dependency failed to compile // before the generated project was reached, in a project whose manifest // declares no dependencies at all. -TEST(ScaffoldTemplate, InjectSelfCanBeDeclined) { +UTest inject_self_can_be_declined{"ScaffoldTemplate/InjectSelfCanBeDeclined", [] { auto dir = std::filesystem::temp_directory_path() / "mcpp_tmpl_noself"; std::filesystem::remove_all(dir); std::filesystem::create_directories(dir); @@ -409,15 +406,15 @@ TEST(ScaffoldTemplate, InjectSelfCanBeDeclined) { f << "[template]\ndescription = \"d\"\n[template.inject]\nself = false\n"; } auto meta = mcpp::scaffold::load_meta(dir); - ASSERT_TRUE(meta.has_value()) << (meta ? "" : meta.error()); - EXPECT_FALSE(meta->injectSelf); + if (auto&& failed = expect(fatal(meta.has_value())); !failed) failed << (meta ? "" : meta.error()); + expect(!meta->injectSelf); std::filesystem::remove_all(dir); -} +}}; // The default must stay ON: every template that does not say otherwise relies // on the injection, and a silent flip would leave them all without their own // package. -TEST(ScaffoldTemplate, InjectSelfDefaultsToOn) { +UTest inject_self_defaults_to_on{"ScaffoldTemplate/InjectSelfDefaultsToOn", [] { auto dir = std::filesystem::temp_directory_path() / "mcpp_tmpl_self_default"; std::filesystem::remove_all(dir); std::filesystem::create_directories(dir); @@ -426,13 +423,13 @@ TEST(ScaffoldTemplate, InjectSelfDefaultsToOn) { f << "[template]\ndescription = \"d\"\n"; } auto meta = mcpp::scaffold::load_meta(dir); - ASSERT_TRUE(meta.has_value()) << (meta ? "" : meta.error()); - EXPECT_TRUE(meta->injectSelf); + if (auto&& failed = expect(fatal(meta.has_value())); !failed) failed << (meta ? "" : meta.error()); + expect(bool(meta->injectSelf)); std::filesystem::remove_all(dir); -} +}}; // The table form still carries features, and must not be read as "declined". -TEST(ScaffoldTemplate, InjectSelfTableFormStillInjects) { +UTest inject_self_table_form_still_injects{"ScaffoldTemplate/InjectSelfTableFormStillInjects", [] { auto dir = std::filesystem::temp_directory_path() / "mcpp_tmpl_self_table"; std::filesystem::remove_all(dir); std::filesystem::create_directories(dir); @@ -442,9 +439,9 @@ TEST(ScaffoldTemplate, InjectSelfTableFormStillInjects) { "[template.inject]\nself = { features = [\"x\"] }\n"; } auto meta = mcpp::scaffold::load_meta(dir); - ASSERT_TRUE(meta.has_value()) << (meta ? "" : meta.error()); - EXPECT_TRUE(meta->injectSelf); - ASSERT_EQ(meta->injectSelfFeatures.size(), 1u); - EXPECT_EQ(meta->injectSelfFeatures[0], "x"); + if (auto&& failed = expect(fatal(meta.has_value())); !failed) failed << (meta ? "" : meta.error()); + expect(bool(meta->injectSelf)); + expect(fatal(eq(meta->injectSelfFeatures.size(), 1u))); + expect(eq(meta->injectSelfFeatures[0], "x")); std::filesystem::remove_all(dir); -} +}}; diff --git a/tests/unit/test_schedule_policy.cpp b/tests/unit/test_schedule_policy.cpp index 375d44d6d..4053f6dac 100644 --- a/tests/unit/test_schedule_policy.cpp +++ b/tests/unit/test_schedule_policy.cpp @@ -4,15 +4,17 @@ // and no compiler — the table can be wrong in a way that only shows up as a // slower build, which is the kind of wrong that never gets noticed. -#include #include import std; +import mcpplibs.mtest; import mcpp.invocation; import mcpp.build.schedule.policy; import mcpp.toolchain.model; import mcpp.manifest; +using namespace mcpplibs::mtest; + using mcpp::build::schedule::Strategy; using mcpp::build::schedule::decide; using mcpp::build::schedule::decide_shape; @@ -48,74 +50,74 @@ mcpp::manifest::Manifest with_schedule(std::string v) { // backwards is silent: clang writes its BMI to the final path with O_TRUNC, so // detach-codegen would hand importers a half-written file; gcc has no cheap // BMI-only mode, so two-phase would just compile everything twice. -TEST(SchedulePolicy, EachCompilerGetsItsOwnMechanism) { - EXPECT_EQ(decide(with(CompilerId::Clang), "on", 8).strategy, Strategy::TwoPhase); - EXPECT_EQ(decide(with(CompilerId::GCC), "on", 8).strategy, Strategy::DetachCodegen); -} +UTest each_compiler_gets_its_own_mechanism{"SchedulePolicy/EachCompilerGetsItsOwnMechanism", [] { + expect(eq(decide(with(CompilerId::Clang), "on", 8).strategy, Strategy::TwoPhase)); + expect(eq(decide(with(CompilerId::GCC), "on", 8).strategy, Strategy::DetachCodegen)); +}}; // Unmeasured means None. A guess here is not a slow build, it is a miscompile: // a BMI read while it is still being written is not a diagnostic. // `auto` is OFF for now — pinned, because it is a decision rather than a gap. -TEST(SchedulePolicy, AutoIsOptInForNow) { - EXPECT_EQ(decide(with(CompilerId::GCC), "auto", 8).strategy, Strategy::None); - EXPECT_NE(decide(with(CompilerId::GCC), "on", 8).strategy, Strategy::None); -} +UTest auto_is_opt_in_for_now{"SchedulePolicy/AutoIsOptInForNow", [] { + expect(eq(decide(with(CompilerId::GCC), "auto", 8).strategy, Strategy::None)); + expect(ne(decide(with(CompilerId::GCC), "on", 8).strategy, Strategy::None)); +}}; -TEST(SchedulePolicy, UnmeasuredCompilersStayConservative) { - EXPECT_EQ(decide(with(CompilerId::MSVC), "on", 8).strategy, Strategy::None); - EXPECT_EQ(decide(with(CompilerId::Unknown), "on", 8).strategy, Strategy::None); -} +UTest unmeasured_compilers_stay_conservative{"SchedulePolicy/UnmeasuredCompilersStayConservative", [] { + expect(eq(decide(with(CompilerId::MSVC), "on", 8).strategy, Strategy::None)); + expect(eq(decide(with(CompilerId::Unknown), "on", 8).strategy, Strategy::None)); +}}; // Asserted from BOTH sides: that "off" disables, and that the same input with // "auto" does NOT. Checking only the first would pass an implementation that // never enables anything at all. -TEST(SchedulePolicy, OffDisablesAndAutoDoesNot) { - EXPECT_EQ(decide(with(CompilerId::GCC), "off", 8).strategy, Strategy::None); - EXPECT_NE(decide(with(CompilerId::GCC), "on", 8).strategy, Strategy::None); -} +UTest off_disables_and_auto_does_not{"SchedulePolicy/OffDisablesAndAutoDoesNot", [] { + expect(eq(decide(with(CompilerId::GCC), "off", 8).strategy, Strategy::None)); + expect(ne(decide(with(CompilerId::GCC), "on", 8).strategy, Strategy::None)); +}}; // HAZARD 2, encoded. Under detach-codegen a compiler stops holding a ninja slot // the moment it publishes its BMI, so ninja's -j is no longer a bound on how // many compilers run. With the two equal, ninja's slots fill with edges that // are merely sleeping, the ready frontier starves, and the schedule degenerates // to the baseline — which is exactly what the first prototype measured. -TEST(SchedulePolicy, DetachCodegenGivesNinjaMoreSlotsThanCompilers) { +UTest detach_codegen_gives_ninja_more_slots_than_compilers{"SchedulePolicy/DetachCodegenGivesNinjaMoreSlotsThanCompilers", [] { const auto d = decide(with(CompilerId::GCC), "on", 32); - EXPECT_EQ(d.compilerCap, 32); - EXPECT_GT(d.ninjaJobs, d.compilerCap); -} + expect(eq(d.compilerCap, 32)); + expect(gt(d.ninjaJobs, d.compilerCap)); +}}; // Two-phase runs ordinary compilers that hold their slot for the whole compile, // so inflating -j there would only oversubscribe the machine. -TEST(SchedulePolicy, TwoPhaseLeavesTheJobCountAlone) { +UTest two_phase_leaves_the_job_count_alone{"SchedulePolicy/TwoPhaseLeavesTheJobCountAlone", [] { const auto d = decide(with(CompilerId::Clang), "on", 32); - EXPECT_EQ(d.ninjaJobs, d.compilerCap); -} + expect(eq(d.ninjaJobs, d.compilerCap)); +}}; // A scheduler that silently declines to optimise cannot be debugged: "why is my // build not using the fast shape?" has to have an answer that ships with the // build. Every branch, including the ones that choose None. -TEST(SchedulePolicy, EveryDecisionCarriesAReason) { +UTest every_decision_carries_a_reason{"SchedulePolicy/EveryDecisionCarriesAReason", [] { for (auto id : {CompilerId::GCC, CompilerId::Clang, CompilerId::MSVC, CompilerId::Unknown}) { - EXPECT_FALSE(decide(with(id), "auto", 8).reason.empty()) + expect(!(decide(with(id), "auto", 8).reason.empty())) << "no reason for compiler id " << static_cast(id); - EXPECT_FALSE(decide(with(id), "off", 8).reason.empty()) + expect(!(decide(with(id), "off", 8).reason.empty())) << "no reason when disabled, compiler id " << static_cast(id); } -} +}}; // A host that reports nothing must not turn into "-j0" or a negative cap. -TEST(SchedulePolicy, ZeroJobsStaysZeroRatherThanBecomingNonsense) { +UTest zero_jobs_stays_zero_rather_than_becoming_nonsense{"SchedulePolicy/ZeroJobsStaysZeroRatherThanBecomingNonsense", [] { const auto d = decide(with(CompilerId::GCC), "on", 0); - EXPECT_EQ(d.compilerCap, 0); - EXPECT_EQ(d.ninjaJobs, 0); + expect(eq(d.compilerCap, 0)); + expect(eq(d.ninjaJobs, 0)); // ...and it must SAY so. Under detach-codegen a cap of 0 disables the // semaphore, which is the only bound on how many compilers run at once, so // this state is not a neutral default — it is unbounded concurrency. - EXPECT_NE(d.reason.find("bounded by nothing"), std::string::npos) + expect(ne(d.reason.find("bounded by nothing"), std::string::npos)) << "a zero cap silently means no bound at all: " << d.reason; -} +}}; // THE DEFAULT CONFIGURATION MUST STILL BE BOUNDED. // @@ -130,21 +132,21 @@ TEST(SchedulePolicy, ZeroJobsStaysZeroRatherThanBecomingNonsense) { // `sched_cap = 0`, i.e. ninja starting compiles as fast as BMIs appeared with // nothing limiting them, on a workload whose single compile peaks near a // gigabyte. The caller now resolves what the host would pick and hands it in. -TEST(SchedulePolicy, DetachCodegenFallsBackToTheHostWhenNoJobCountWasGiven) { +UTest detach_codegen_falls_back_to_the_host_when_no_job_count_was_given{"SchedulePolicy/DetachCodegenFallsBackToTheHostWhenNoJobCountWasGiven", [] { const auto d = decide(with(CompilerId::GCC), "on", /*hostJobs=*/0, /*autoJobs=*/24); - EXPECT_EQ(d.strategy, Strategy::DetachCodegen); - EXPECT_EQ(d.compilerCap, 24) << "the semaphore would be disabled"; - EXPECT_GT(d.ninjaJobs, d.compilerCap) << "hazard 2: -j must exceed the cap"; + expect(eq(d.strategy, Strategy::DetachCodegen)); + expect(eq(d.compilerCap, 24)) << "the semaphore would be disabled"; + expect(gt(d.ninjaJobs, d.compilerCap)) << "hazard 2: -j must exceed the cap"; // An explicit job count still wins over the fallback. const auto e = decide(with(CompilerId::GCC), "on", /*hostJobs=*/8, /*autoJobs=*/24); - EXPECT_EQ(e.compilerCap, 8); + expect(eq(e.compilerCap, 8)); // The fallback is only for the strategy that needs it; two-phase uses // ordinary edges, where ninja's -j is already the bound. const auto c = decide(with(CompilerId::Clang), "on", /*hostJobs=*/0, /*autoJobs=*/24); - EXPECT_EQ(c.ninjaJobs, 0) << "clang must still defer to the backend default"; -} + expect(eq(c.ninjaJobs, 0)) << "clang must still defer to the backend default"; +}}; // `auto` is bounded by recommended_jobs' ceiling of 64, but `--jobs N` is only // checked for `> 0`, so an absurd N reaches decide() intact and `cap * 6` was @@ -152,11 +154,11 @@ TEST(SchedulePolicy, DetachCodegenFallsBackToTheHostWhenNoJobCountWasGiven) { // one of the friendlier outcomes. Asserted as "still positive and still greater // than the cap" rather than against the clamp constant, so tuning the clamp does // not require editing the test that exists to stop it going negative. -TEST(SchedulePolicy, AnAbsurdJobCountDoesNotOverflowIntoANegativeOne) { +UTest an_absurd_job_count_does_not_overflow_into_a_negative_one{"SchedulePolicy/AnAbsurdJobCountDoesNotOverflowIntoANegativeOne", [] { const auto d = decide(with(CompilerId::GCC), "on", 2000000000); - EXPECT_GT(d.ninjaJobs, 0) << "ninja -j went non-positive"; - EXPECT_GT(d.ninjaJobs, 1) << "hazard 2: ninja must still outnumber the compilers"; -} + expect(gt(d.ninjaJobs, 0)) << "ninja -j went non-positive"; + expect(gt(d.ninjaJobs, 1)) << "hazard 2: ninja must still outnumber the compilers"; +}}; // ─── resolve_jobs precedence (#564) ──────────────────────────────────────── // @@ -173,7 +175,7 @@ TEST(SchedulePolicy, AnAbsurdJobCountDoesNotOverflowIntoANegativeOne) { // opposite of correct, because the global value is a property of the machine // and the environment variable is this invocation. Each of the three levels is // therefore checked against the level that must beat it. -TEST(SchedulePolicy, ResolveJobsPutsTheMachineBelowTheProjectAndTheInvocation) { +UTest resolve_jobs_puts_the_machine_below_the_project_and_the_invocation{"SchedulePolicy/ResolveJobsPutsTheMachineBelowTheProjectAndTheInvocation", [] { mcpp::manifest::Manifest bare; // no [build] jobs auto with_jobs = [](std::string v) { mcpp::manifest::Manifest m; @@ -185,24 +187,24 @@ TEST(SchedulePolicy, ResolveJobsPutsTheMachineBelowTheProjectAndTheInvocation) { { // The machine's value is used when nothing else says anything, and 0 // still means "say nothing" for everyone who has not written the key. ScopedVar clear("MCPP_JOBS", nullptr); - EXPECT_EQ(resolve_jobs(bare, {}, 4), 4); - EXPECT_EQ(resolve_jobs(bare, {}, 0), 0); + expect(eq(resolve_jobs(bare, {}, 4), 4)); + expect(eq(resolve_jobs(bare, {}, 0), 0)); // A non-positive value reads as absent rather than as a bound: that is // what the generated template's `0` means, and a negative -j would be // handed straight to the backend. - EXPECT_EQ(resolve_jobs(bare, {}, -1), 0); + expect(eq(resolve_jobs(bare, {}, -1), 0)); // The project beats the machine. A project that states a number has a // reason the machine cannot know. - EXPECT_EQ(resolve_jobs(with_jobs("6"), {}, 4), 6); + expect(eq(resolve_jobs(with_jobs("6"), {}, 4), 6)); } { // The invocation beats both. Without this leg the parameter could be // wired in above MCPP_JOBS and every other assertion here would pass. ScopedVar jobs("MCPP_JOBS", "2"); - EXPECT_EQ(resolve_jobs(bare, {}, 4), 2); - EXPECT_EQ(resolve_jobs(with_jobs("6"), {}, 4), 2); + expect(eq(resolve_jobs(bare, {}, 4), 2)); + expect(eq(resolve_jobs(with_jobs("6"), {}, 4), 2)); } -} +}}; // ─── requested_switch: a typo is a diagnostic, never a silent "auto" ─────── // @@ -210,63 +212,63 @@ TEST(SchedulePolicy, ResolveJobsPutsTheMachineBelowTheProjectAndTheInvocation) { // `bmi_schedule = "ON"` was accepted, meant OFF, and explained itself with // "the split schedule is opt-in until verified" — which reads as "you did not // ask for it" to someone who just did. -TEST(SchedulePolicy, RequestedSwitchPassesTheThreeSpellingsThrough) { +UTest requested_switch_passes_the_three_spellings_through{"SchedulePolicy/RequestedSwitchPassesTheThreeSpellingsThrough", [] { ScopedVar clear("MCPP_BMI_SCHEDULE", nullptr); - EXPECT_EQ(requested_switch(with_schedule("on")), "on"); - EXPECT_EQ(requested_switch(with_schedule("off")), "off"); - EXPECT_EQ(requested_switch(with_schedule("auto")), "auto"); - EXPECT_EQ(requested_switch(with_schedule("")), "auto"); // unset -} + expect(eq(requested_switch(with_schedule("on")), "on")); + expect(eq(requested_switch(with_schedule("off")), "off")); + expect(eq(requested_switch(with_schedule("auto")), "auto")); + expect(eq(requested_switch(with_schedule("")), "auto")); // unset +}}; -TEST(SchedulePolicy, RequestedSwitchReportsATypoInsteadOfSwallowingIt) { +UTest requested_switch_reports_a_typo_instead_of_swallowing_it{"SchedulePolicy/RequestedSwitchReportsATypoInsteadOfSwallowingIt", [] { ScopedVar clear("MCPP_BMI_SCHEDULE", nullptr); for (const char* typo : {"ON", "On", "true", "yes", "1", "enabled"}) { std::string seen; const auto v = requested_switch(with_schedule(typo), [&](std::string_view bad) { seen = bad; }); - EXPECT_EQ(v, "auto") << typo << " must fall back to the default"; - EXPECT_EQ(seen, typo) << typo << " was accepted silently"; + expect(eq(v, "auto")) << typo << " must fall back to the default"; + expect(eq(seen, typo)) << typo << " was accepted silently"; } -} +}}; // Both directions. Checking only that a typo warns would pass an implementation // that warns about everything, including the spellings that are correct. -TEST(SchedulePolicy, RequestedSwitchStaysQuietForValidValues) { +UTest requested_switch_stays_quiet_for_valid_values{"SchedulePolicy/RequestedSwitchStaysQuietForValidValues", [] { ScopedVar clear("MCPP_BMI_SCHEDULE", nullptr); for (const char* ok : {"on", "off", "auto"}) { bool warned = false; requested_switch(with_schedule(ok), [&](std::string_view) { warned = true; }); - EXPECT_FALSE(warned) << ok << " is valid but was reported as invalid"; + expect(!warned) << ok << " is valid but was reported as invalid"; } -} +}}; // The environment outranks the manifest — and is validated on the same terms. // A typo'd MCPP_BMI_SCHEDULE must not fall through to the manifest either: // silently honouring `[build] bmi_schedule = "on"` when the environment asked // for something unparseable would make the warning a lie. -TEST(SchedulePolicy, EnvironmentBeatsManifestAndIsValidatedToo) { +UTest environment_beats_manifest_and_is_validated_too{"SchedulePolicy/EnvironmentBeatsManifestAndIsValidatedToo", [] { { ScopedVar on("MCPP_BMI_SCHEDULE", "off"); - EXPECT_EQ(requested_switch(with_schedule("on")), "off"); + expect(eq(requested_switch(with_schedule("on")), "off")); } { ScopedVar bad("MCPP_BMI_SCHEDULE", "ON"); std::string seen; - EXPECT_EQ(requested_switch(with_schedule("on"), - [&](std::string_view b) { seen = b; }), "auto"); - EXPECT_EQ(seen, "ON"); + expect(eq(requested_switch(with_schedule("on"), + [&](std::string_view b) { seen = b; }), "auto")); + expect(eq(seen, "ON")); } -} +}}; // EVERY STRATEGY CAPS ITS COMPILERS (2026.11.7.1): the capped executor is what // registers a compiler in its build's generation, so a strategy without a split // still has a cap -- the job count, else what this machine would choose. -TEST(SchedulePolicy, EveryStrategyCarriesACompilerCap) { +UTest every_strategy_carries_a_compiler_cap{"SchedulePolicy/EveryStrategyCarriesACompilerCap", [] { for (auto id : {CompilerId::GCC, CompilerId::Clang, CompilerId::MSVC, CompilerId::Unknown}) { - EXPECT_EQ(decide(with(id), "auto", 8).compilerCap, 8) << static_cast(id); - EXPECT_EQ(decide(with(id), "off", 0, 6).compilerCap, 6) << static_cast(id); + expect(eq(decide(with(id), "auto", 8).compilerCap, 8)) << static_cast(id); + expect(eq(decide(with(id), "off", 0, 6).compilerCap, 6)) << static_cast(id); } // The shape alone is unchanged: only the split strategies carry a cap there. - EXPECT_EQ(decide_shape(with(CompilerId::MSVC), "auto", 8).compilerCap, 0); -} + expect(eq(decide_shape(with(CompilerId::MSVC), "auto", 8).compilerCap, 0)); +}}; diff --git a/tests/unit/test_search_versions.cpp b/tests/unit/test_search_versions.cpp index 9852e5aa8..236bec4cf 100644 --- a/tests/unit/test_search_versions.cpp +++ b/tests/unit/test_search_versions.cpp @@ -3,12 +3,14 @@ // semver-descending version union; anything unreadable degrades to empty // without failing the search. -#include import std; +import mcpplibs.mtest; import mcpp.config; import mcpp.pm.package_fetcher; +using namespace mcpplibs::mtest; + namespace { struct TempDir { @@ -29,7 +31,7 @@ mcpp::pm::Fetcher::SearchHit hit(std::string name) { return {"mcpplibs", std::move(name), "description"}; } -TEST(SearchVersions, MergesPerOsTablesOfTheHitsDescriptor) { +UTest merges_per_os_tables_of_the_hits_descriptor{"SearchVersions/MergesPerOsTablesOfTheHitsDescriptor", [] { TempDir temp; // Layout rule: the reader derives the letter dir from the candidate // FILENAME's first char — "compat.gadget.lua" lives under pkgs/c/. @@ -50,26 +52,26 @@ TEST(SearchVersions, MergesPerOsTablesOfTheHitsDescriptor) { mcpp::pm::Fetcher f(cfg); auto versions = f.versions_for_hit(hit("compat:gadget")); - ASSERT_EQ(versions.size(), 2u); - EXPECT_EQ(versions[0], "2.0.0"); - EXPECT_EQ(versions[1], "1.0.0"); -} + expect(fatal(eq(versions.size(), 2u))); + expect(eq(versions[0], "2.0.0")); + expect(eq(versions[1], "1.0.0")); +}}; -TEST(SearchVersions, BareNameWithoutNamespaceCarriesNoVersions) { +UTest bare_name_without_namespace_carries_no_versions{"SearchVersions/BareNameWithoutNamespaceCarriesNoVersions", [] { mcpp::config::GlobalConfig cfg{}; cfg.registryDir = std::filesystem::temp_directory_path() / "mcpp-search-versions-no-registry"; mcpp::pm::Fetcher f(cfg); - EXPECT_TRUE(f.versions_for_hit(hit("gadget")).empty()); -} + expect(f.versions_for_hit(hit("gadget")).empty()); +}}; -TEST(SearchVersions, UnreadableDescriptorDegradesToEmpty) { +UTest unreadable_descriptor_degrades_to_empty{"SearchVersions/UnreadableDescriptorDegradesToEmpty", [] { TempDir temp; mcpp::config::GlobalConfig cfg{}; cfg.registryDir = temp.path; // data/ exists nowhere: nothing readable mcpp::pm::Fetcher f(cfg); - EXPECT_TRUE(f.versions_for_hit(hit("compat:ghost")).empty()); -} + expect(f.versions_for_hit(hit("compat:ghost")).empty()); +}}; } // namespace diff --git a/tests/unit/test_select_sources.cpp b/tests/unit/test_select_sources.cpp index b39a90c17..c44da4491 100644 --- a/tests/unit/test_select_sources.cpp +++ b/tests/unit/test_select_sources.cpp @@ -1,8 +1,10 @@ -#include import std; +import mcpplibs.mtest; import mcpp.modgraph.scanner; +using namespace mcpplibs::mtest; + // A `sources` list selects every pattern's files less the files any `!` // pattern matches, whichever pattern selected them. The rows of // `[target..build] sources` are appended to the list, so a system's row @@ -30,25 +32,25 @@ struct Tree { }; } // namespace -TEST(SelectSources, AnExclusionRemovesWhatThePatternsSelected) { +UTest an_exclusion_removes_what_the_patterns_selected{"SelectSources/AnExclusionRemovesWhatThePatternsSelected", [] { Tree t; auto got = t.names(mcpp::modgraph::select_sources(t.root, { "src/**/*.cpp", "!src/**/*_test.cpp", "!src/native/**" })); - EXPECT_EQ(got, (std::vector{ "src/a.cpp" })); -} + expect(eq(got, (std::vector{ "src/a.cpp" }))); +}}; -TEST(SelectSources, ASystemsRowLeavesOutTheOtherSystemsDirectory) { +UTest a_systems_row_leaves_out_the_other_systems_directory{"SelectSources/ASystemsRowLeavesOutTheOtherSystemsDirectory", [] { Tree t; // The unconditional list, then the linux row. auto got = t.names(mcpp::modgraph::select_sources(t.root, { "src/**/*.cpp", "!src/native/windows/**" })); - EXPECT_EQ(got, (std::vector{ "src/a.cpp", "src/b_test.cpp", "src/native/posix/lock.cpp" })); -} + expect(eq(got, (std::vector{ "src/a.cpp", "src/b_test.cpp", "src/native/posix/lock.cpp" }))); +}}; -TEST(SelectSources, AnExclusionRemovesWhicheverPatternSelected) { +UTest an_exclusion_removes_whichever_pattern_selected{"SelectSources/AnExclusionRemovesWhicheverPatternSelected", [] { Tree t; // An exclusion written before the pattern that selects the file still // removes it, as published manifests rely on. auto got = t.names(mcpp::modgraph::select_sources(t.root, { "!src/native/**", "src/**/*.cpp", "src/native/posix/*.cpp" })); - EXPECT_EQ(got, (std::vector{ "src/a.cpp", "src/b_test.cpp" })); -} + expect(eq(got, (std::vector{ "src/a.cpp", "src/b_test.cpp" }))); +}}; diff --git a/tests/unit/test_short_name_scan.cpp b/tests/unit/test_short_name_scan.cpp index 11f73d2be..96efa24ec 100644 --- a/tests/unit/test_short_name_scan.cpp +++ b/tests/unit/test_short_name_scan.cpp @@ -3,11 +3,13 @@ // the suggested packages actually offer. Still DIAGNOSTIC ONLY: nothing here // feeds resolution, the lockfile, or the install layer. -#include import std; +import mcpplibs.mtest; import mcpp.pm.package_fetcher; +using namespace mcpplibs::mtest; + namespace { struct TempDir { @@ -45,7 +47,7 @@ std::string descriptor_with_versions(std::string_view ns, ns, name, xpmTable); } -TEST(ShortNameScan, CarriesMergedDescendingVersionsPerMatch) { +UTest carries_merged_descending_versions_per_match{"ShortNameScan/CarriesMergedDescendingVersionsPerMatch", [] { TempDir temp; write_descriptor(temp.path, "a.acme.widget.lua", descriptor_with_versions( "acme", "acme.widget", @@ -60,17 +62,17 @@ TEST(ShortNameScan, CarriesMergedDescendingVersionsPerMatch) { {temp.path}, "widget"); // Sorted by fqn; each record keeps its fqn AND its version union. - ASSERT_EQ(matches.size(), 1u); - EXPECT_EQ(matches[0].fqn, "acme.widget"); - ASSERT_EQ(matches[0].versions.size(), 2u); - EXPECT_EQ(matches[0].versions[0], "1.0.0"); - EXPECT_EQ(matches[0].versions[1], "0.9.0"); + expect(fatal(eq(matches.size(), 1u))); + expect(eq(matches[0].fqn, "acme.widget")); + expect(fatal(eq(matches[0].versions.size(), 2u))); + expect(eq(matches[0].versions[0], "1.0.0")); + expect(eq(matches[0].versions[1], "0.9.0")); // The short name is matched on the SHORT component only: compat/gadget // must not appear for "widget". -} +}}; -TEST(ShortNameScan, DescriptorWithoutXpmYieldsEmptyVersions) { +UTest descriptor_without_xpm_yields_empty_versions{"ShortNameScan/DescriptorWithoutXpmYieldsEmptyVersions", [] { TempDir temp; write_descriptor(temp.path, "a.acme.bare.lua", descriptor_with_versions( "acme", "acme.bare", "mcpp = { schema = \"0.1\" }")); @@ -78,12 +80,12 @@ TEST(ShortNameScan, DescriptorWithoutXpmYieldsEmptyVersions) { auto matches = mcpp::pm::Fetcher::scan_short_name_matches( {temp.path}, "bare"); - ASSERT_EQ(matches.size(), 1u); - EXPECT_EQ(matches[0].fqn, "acme.bare"); - EXPECT_TRUE(matches[0].versions.empty()); -} + expect(fatal(eq(matches.size(), 1u))); + expect(eq(matches[0].fqn, "acme.bare")); + expect(matches[0].versions.empty()); +}}; -TEST(ShortNameScan, EmptyResultOnNoMatchIsNotAnError) { +UTest empty_result_on_no_match_is_not_an_error{"ShortNameScan/EmptyResultOnNoMatchIsNotAnError", [] { TempDir temp; write_descriptor(temp.path, "a.acme.widget.lua", descriptor_with_versions( "acme", "acme.widget", @@ -92,7 +94,7 @@ TEST(ShortNameScan, EmptyResultOnNoMatchIsNotAnError) { auto matches = mcpp::pm::Fetcher::scan_short_name_matches( {temp.path}, "nosuchthing"); - EXPECT_TRUE(matches.empty()); -} + expect(matches.empty()); +}}; } // namespace diff --git a/tests/unit/test_source_digest.cpp b/tests/unit/test_source_digest.cpp index fdc3c050e..2e178076e 100644 --- a/tests/unit/test_source_digest.cpp +++ b/tests/unit/test_source_digest.cpp @@ -1,8 +1,10 @@ -#include import std; +import mcpplibs.mtest; import mcpp.build.source_digest; +using namespace mcpplibs::mtest; + // THE CONTENT DIGEST OF A SOURCE TREE, RE-READ ONLY WHERE A STAT MOVED // (mcpp#797, R6). A build program depends on every source of the packages in // its graph, an included header among them. @@ -28,26 +30,26 @@ struct Tree { }; } // namespace -TEST(SourceDigest, AnEditedHeaderChangesIt) { +UTest an_edited_header_changes_it{"SourceDigest/AnEditedHeaderChangesIt", [] { Tree t; t.write("src/lib.cppm", "module;\n#include \"v.h\"\nexport module lib;\n"); t.write("src/v.h", "#define V 1\n"); const auto a = t.digest(); - EXPECT_EQ(t.digest(), a) << "a tree that did not change keeps its digest"; + expect(eq(t.digest(), a)) << "a tree that did not change keeps its digest"; t.write("src/v.h", "#define V 2\n"); - EXPECT_NE(t.digest(), a); -} + expect(ne(t.digest(), a)); +}}; -TEST(SourceDigest, ATouchWithoutAnEditKeepsIt) { +UTest a_touch_without_an_edit_keeps_it{"SourceDigest/ATouchWithoutAnEditKeepsIt", [] { Tree t; t.write("src/v.h", "#define V 1\n"); const auto a = t.digest(); std::filesystem::last_write_time(t.root / "src/v.h", std::filesystem::last_write_time(t.root / "src/v.h") + std::chrono::seconds(5)); - EXPECT_EQ(t.digest(), a); -} + expect(eq(t.digest(), a)); +}}; -TEST(SourceDigest, BuildOutputAndNestedPackagesAreNotPartOfIt) { +UTest build_output_and_nested_packages_are_not_part_of_it{"SourceDigest/BuildOutputAndNestedPackagesAreNotPartOfIt", [] { Tree t; t.write("src/v.h", "#define V 1\n"); const auto a = t.digest(); @@ -55,16 +57,16 @@ TEST(SourceDigest, BuildOutputAndNestedPackagesAreNotPartOfIt) { t.write(".git/HEAD", "ref"); t.write("member/mcpp.toml", "[package]\n"); t.write("member/src/m.cppm", "export module m;\n"); - EXPECT_EQ(t.digest(), a); -} + expect(eq(t.digest(), a)); +}}; -TEST(SourceDigest, AnAddedOrRemovedFileChangesIt) { +UTest an_added_or_removed_file_changes_it{"SourceDigest/AnAddedOrRemovedFileChangesIt", [] { Tree t; t.write("src/a.cppm", "export module a;\n"); const auto a = t.digest(); t.write("src/b.cppm", "export module b;\n"); const auto b = t.digest(); - EXPECT_NE(b, a); + expect(ne(b, a)); std::filesystem::remove(t.root / "src/b.cppm"); - EXPECT_EQ(t.digest(), a); -} + expect(eq(t.digest(), a)); +}}; diff --git a/tests/unit/test_source_kind.cpp b/tests/unit/test_source_kind.cpp index 0ce2ccdae..82620e43f 100644 --- a/tests/unit/test_source_kind.cpp +++ b/tests/unit/test_source_kind.cpp @@ -1,28 +1,30 @@ -#include import std; +import mcpplibs.mtest; import mcpp.source_kind; import mcpp.toolchain.model; import mcpp.toolchain.detect; +using namespace mcpplibs::mtest; + using mcpp::SourceKind; // ─── T-1. classify() ─────────────────────────────────────────────────────── -TEST(SourceKind, BuiltInTableIsCppmOnly) { +UTest built_in_table_is_cppm_only{"SourceKind/BuiltInTableIsCppmOnly", [] { // Load-bearing, not conservatism. Widening the built-in set widens the // DEFAULT source glob with it, so a published package with a vendored // MSVC-only `.ixx` under src/ would start compiling it on the next mcpp // upgrade — a break its author cannot fix, because that version's tarball // has already shipped. auto t = mcpp::builtin_extension_table(); - EXPECT_EQ(t.moduleInterface, (std::vector{".cppm"})); - EXPECT_EQ(mcpp::classify("src/a.ixx", t), SourceKind::Other); - EXPECT_EQ(mcpp::classify("src/a.ccm", t), SourceKind::Other); - EXPECT_EQ(mcpp::classify("src/a.cxxm", t), SourceKind::Other); -} + expect(eq(t.moduleInterface, (std::vector{".cppm"}))); + expect(eq(mcpp::classify("src/a.ixx", t), SourceKind::Other)); + expect(eq(mcpp::classify("src/a.ccm", t), SourceKind::Other)); + expect(eq(mcpp::classify("src/a.cxxm", t), SourceKind::Other)); +}}; -TEST(SourceKind, ClassifiesEveryBuiltInRole) { +UTest classifies_every_built_in_role{"SourceKind/ClassifiesEveryBuiltInRole", [] { auto t = mcpp::builtin_extension_table(); struct Row { const char* path; SourceKind want; }; const Row rows[] = { @@ -44,128 +46,126 @@ TEST(SourceKind, ClassifiesEveryBuiltInRole) { {"src/README", SourceKind::Other}, }; for (auto const& r : rows) - EXPECT_EQ(mcpp::classify(r.path, t), r.want) << r.path; -} + expect(eq(mcpp::classify(r.path, t), r.want)) << r.path; +}}; -TEST(SourceKind, CaseIsNeverFolded) { +UTest case_is_never_folded{"SourceKind/CaseIsNeverFolded", [] { // `.S` and `.s` are DIFFERENT LANGUAGES here — `.S` goes through the C // preprocessor, `.s` does not. Any normalization that lowercases would // silently merge them, so extensions are compared literally. auto t = mcpp::builtin_extension_table(); - EXPECT_EQ(mcpp::classify("a.S", t), SourceKind::GasAsm); - EXPECT_EQ(mcpp::classify("a.s", t), SourceKind::GasAsm); - EXPECT_EQ(mcpp::normalize_extension(".IXX"), ".IXX"); - EXPECT_EQ(mcpp::classify("a.CPPM", t), SourceKind::Other); -} + expect(eq(mcpp::classify("a.S", t), SourceKind::GasAsm)); + expect(eq(mcpp::classify("a.s", t), SourceKind::GasAsm)); + expect(eq(mcpp::normalize_extension(".IXX"), ".IXX")); + expect(eq(mcpp::classify("a.CPPM", t), SourceKind::Other)); +}}; -TEST(SourceKind, ConfiguredExtensionsAreAdditiveAndNormalized) { +UTest configured_extensions_are_additive_and_normalized{"SourceKind/ConfiguredExtensionsAreAdditiveAndNormalized", [] { // `ixx` without a dot, a duplicate, and one already built in. auto t = mcpp::extension_table_for( std::vector{"ixx", ".ccm", ".ccm", ".cppm", " .cxxm "}); - EXPECT_EQ(t.moduleInterface, - (std::vector{".cppm", ".ixx", ".ccm", ".cxxm"})); - EXPECT_EQ(mcpp::classify("src/a.ixx", t), SourceKind::ModuleInterface); - EXPECT_EQ(mcpp::classify("src/a.ccm", t), SourceKind::ModuleInterface); - EXPECT_EQ(mcpp::classify("src/a.cxxm", t), SourceKind::ModuleInterface); + expect(eq(t.moduleInterface, (std::vector{".cppm", ".ixx", ".ccm", ".cxxm"}))); + expect(eq(mcpp::classify("src/a.ixx", t), SourceKind::ModuleInterface)); + expect(eq(mcpp::classify("src/a.ccm", t), SourceKind::ModuleInterface)); + expect(eq(mcpp::classify("src/a.cxxm", t), SourceKind::ModuleInterface)); // Built-in roles are untouched by the addition. - EXPECT_EQ(mcpp::classify("src/a.cpp", t), SourceKind::Cxx); - EXPECT_EQ(mcpp::classify("src/a.c", t), SourceKind::C); -} + expect(eq(mcpp::classify("src/a.cpp", t), SourceKind::Cxx)); + expect(eq(mcpp::classify("src/a.c", t), SourceKind::C)); +}}; -TEST(SourceKind, ReservedExtensionsAreRejectedNotSilentlyAccepted) { +UTest reserved_extensions_are_rejected_not_silently_accepted{"SourceKind/ReservedExtensionsAreRejectedNotSilentlyAccepted", [] { // Declaring `.c` a module interface has no legitimate use and would route // C files to the C++ module rule, failing somewhere that names neither the // file nor the key. A hard error, not a warning. for (auto const* bad : {".cpp", ".cc", ".cxx", ".c", ".m", ".mm", ".h", ".hpp", ".hh", ".hxx", ".S", ".s", ".asm"}) { auto err = mcpp::validate_module_extensions(std::vector{bad}); - ASSERT_TRUE(err.has_value()) << bad; - EXPECT_NE(err->find(bad), std::string::npos) << *err; + expect(fatal(err.has_value())) << bad; + if (auto&& failed = expect(ne(err->find(bad), std::string::npos)); !failed) failed << *err; } // An unusual-but-free extension is fine: mcpp tells the compiler what the // unit is rather than relying on the driver to recognize the suffix. - EXPECT_FALSE(mcpp::validate_module_extensions( - std::vector{".mpp", ".cppmi"}).has_value()); -} + expect(!(mcpp::validate_module_extensions( + std::vector{".mpp", ".cppmi"}).has_value())); +}}; -TEST(SourceKind, ValidationRejectsNonExtensionShapes) { +UTest validation_rejects_non_extension_shapes{"SourceKind/ValidationRejectsNonExtensionShapes", [] { for (auto const* bad : {"", " ", ".", "src/*.ixx", "a/b.ixx", "foo.bar.ixx"}) { - EXPECT_TRUE(mcpp::validate_module_extensions( + expect(mcpp::validate_module_extensions( std::vector{bad}).has_value()) << "accepted: " << bad; } -} +}}; -TEST(SourceKind, PredicatesAgreeWithTheKind) { - EXPECT_TRUE(mcpp::produces_bmi(SourceKind::ModuleInterface)); - EXPECT_TRUE(mcpp::links_unconditionally(SourceKind::ModuleInterface)); +UTest predicates_agree_with_the_kind{"SourceKind/PredicatesAgreeWithTheKind", [] { + expect(bool(mcpp::produces_bmi(SourceKind::ModuleInterface))); + expect(bool(mcpp::links_unconditionally(SourceKind::ModuleInterface))); for (auto k : {SourceKind::Cxx, SourceKind::C, SourceKind::GasAsm, SourceKind::NasmAsm, SourceKind::Header, SourceKind::Other}) { - EXPECT_FALSE(mcpp::produces_bmi(k)); - EXPECT_FALSE(mcpp::links_unconditionally(k)); + expect(!mcpp::produces_bmi(k)); + expect(!mcpp::links_unconditionally(k)); } // Scan-exempt: cannot contain import/module, so no P1689 scan. - EXPECT_TRUE(mcpp::is_scan_exempt(SourceKind::C)); - EXPECT_TRUE(mcpp::is_scan_exempt(SourceKind::GasAsm)); - EXPECT_TRUE(mcpp::is_scan_exempt(SourceKind::NasmAsm)); - EXPECT_FALSE(mcpp::is_scan_exempt(SourceKind::ModuleInterface)); - EXPECT_FALSE(mcpp::is_scan_exempt(SourceKind::Cxx)); + expect(mcpp::is_scan_exempt(SourceKind::C)); + expect(mcpp::is_scan_exempt(SourceKind::GasAsm)); + expect(mcpp::is_scan_exempt(SourceKind::NasmAsm)); + expect(!mcpp::is_scan_exempt(SourceKind::ModuleInterface)); + expect(!mcpp::is_scan_exempt(SourceKind::Cxx)); // The fast path's question: could editing this change the graph's SHAPE? // Assembly is absent on purpose — it has no import and no scanned include // graph, so editing one changes its object (ninja tracks that) and nothing // else. A NEW assembly file is a different question, answered by // program_inputs_stale. - EXPECT_TRUE(mcpp::affects_graph_shape(SourceKind::ModuleInterface)); - EXPECT_TRUE(mcpp::affects_graph_shape(SourceKind::Cxx)); - EXPECT_TRUE(mcpp::affects_graph_shape(SourceKind::C)); - EXPECT_TRUE(mcpp::affects_graph_shape(SourceKind::Header)); - EXPECT_FALSE(mcpp::affects_graph_shape(SourceKind::GasAsm)); - EXPECT_FALSE(mcpp::affects_graph_shape(SourceKind::Other)); -} - -TEST(SourceKind, DefaultGlobsAreDerivedFromTheTable) { + expect(bool(mcpp::affects_graph_shape(SourceKind::ModuleInterface))); + expect(bool(mcpp::affects_graph_shape(SourceKind::Cxx))); + expect(bool(mcpp::affects_graph_shape(SourceKind::C))); + expect(bool(mcpp::affects_graph_shape(SourceKind::Header))); + expect(!mcpp::affects_graph_shape(SourceKind::GasAsm)); + expect(!mcpp::affects_graph_shape(SourceKind::Other)); +}}; + +UTest default_globs_are_derived_from_the_table{"SourceKind/DefaultGlobsAreDerivedFromTheTable", [] { // Two hand-maintained copies of this list used to exist and had already // drifted apart (the staging fallback was missing all three assembly // extensions). Deriving them is what keeps a declared extension from being // classified but never FOUND. auto builtin = mcpp::default_source_globs(mcpp::builtin_extension_table()); - EXPECT_EQ(builtin, (std::vector{ + expect(eq(builtin, (std::vector{ "src/**/*.cppm", "src/**/*.cpp", "src/**/*.cc", "src/**/*.c", - "src/**/*.S", "src/**/*.s", "src/**/*.asm"})); + "src/**/*.S", "src/**/*.s", "src/**/*.asm"}))); auto wide = mcpp::default_source_globs( mcpp::extension_table_for(std::vector{".ixx"})); - EXPECT_EQ(wide.front(), "src/**/*.cppm"); - EXPECT_NE(std::ranges::find(wide, "src/**/*.ixx"), wide.end()); + expect(eq(wide.front(), "src/**/*.cppm")); + expect(ne(std::ranges::find(wide, "src/**/*.ixx"), wide.end())); - EXPECT_EQ(mcpp::default_source_globs_note(mcpp::builtin_extension_table()), - "sources [src/**/*.{cppm,cpp,cc,c,S,s,asm}]"); -} + expect(eq(mcpp::default_source_globs_note(mcpp::builtin_extension_table()), "sources [src/**/*.{cppm,cpp,cc,c,S,s,asm}]")); +}}; // ─── T-2. Object naming ──────────────────────────────────────────────────── -TEST(SourceKind, ObjectNamingIsMonotoneAndCollisionFree) { +UTest object_naming_is_monotone_and_collision_free{"SourceKind/ObjectNamingIsMonotoneAndCollisionFree", [] { // Historical names are FROZEN: an object's name is part of the internal // layout of a global cache entry, so renaming one without changing the // cache key produces a HIT on an entry that lacks the object the link then // asks for — a missing `.o` at link time, not a cache miss. - EXPECT_EQ(mcpp::object_naming_for("foo.cpp"), mcpp::ObjectNaming::Stem); - EXPECT_EQ(mcpp::object_naming_for("foo.cc"), mcpp::ObjectNaming::Stem); - EXPECT_EQ(mcpp::object_naming_for("foo.c"), mcpp::ObjectNaming::Stem); - EXPECT_EQ(mcpp::object_naming_for("foo.cppm"), mcpp::ObjectNaming::StemDotM); - EXPECT_EQ(mcpp::object_naming_for("foo.S"), mcpp::ObjectNaming::FullFilename); - EXPECT_EQ(mcpp::object_naming_for("foo.asm"), mcpp::ObjectNaming::FullFilename); + expect(eq(mcpp::object_naming_for("foo.cpp"), mcpp::ObjectNaming::Stem)); + expect(eq(mcpp::object_naming_for("foo.cc"), mcpp::ObjectNaming::Stem)); + expect(eq(mcpp::object_naming_for("foo.c"), mcpp::ObjectNaming::Stem)); + expect(eq(mcpp::object_naming_for("foo.cppm"), mcpp::ObjectNaming::StemDotM)); + expect(eq(mcpp::object_naming_for("foo.S"), mcpp::ObjectNaming::FullFilename)); + expect(eq(mcpp::object_naming_for("foo.asm"), mcpp::ObjectNaming::FullFilename)); // Everything a project can ADD gets the collision-proof form, so a new // extension can never change an existing object's name. for (auto const* ext : {"foo.ixx", "foo.ccm", "foo.cxxm", "foo.mpp"}) - EXPECT_EQ(mcpp::object_naming_for(ext), mcpp::ObjectNaming::FullFilename) + expect(eq(mcpp::object_naming_for(ext), mcpp::ObjectNaming::FullFilename)) << ext; -} +}}; -TEST(SourceKind, SameStemAcrossModuleExtensionsNeverCollides) { +UTest same_stem_across_module_extensions_never_collides{"SourceKind/SameStemAcrossModuleExtensionsNeverCollides", [] { // mcpp#272 proposed giving all four module extensions the `.m` prefix, // which makes `foo.cppm` and `foo.ccm` BOTH `foo.m.o`. The per-package // collision prefix cannot help: it mirrors the source DIRECTORY, and these @@ -185,7 +185,7 @@ TEST(SourceKind, SameStemAcrossModuleExtensionsNeverCollides) { for (auto const* f : {"foo.cppm", "foo.ccm", "foo.cxxm", "foo.ixx", "foo.cpp", "foo.S", "foo.s", "foo.asm"}) { auto n = name(f); - EXPECT_TRUE(seen.insert(n).second) << "collision: " << f << " -> " << n; + if (auto&& failed = expect(bool(seen.insert(n).second)); !failed) failed << "collision: " << f << " -> " << n; } // KNOWN GAP, deliberately pinned rather than asserted away: `foo.c` and @@ -193,13 +193,13 @@ TEST(SourceKind, SameStemAcrossModuleExtensionsNeverCollides) { // every C object, which is exactly the cache-layout change described // above and needs a cache-key revision to be safe. Tracked separately — // this assertion documents the gap so nobody "fixes" the test instead. - EXPECT_EQ(name("foo.c"), name("foo.cpp")) + expect(eq(name("foo.c"), name("foo.cpp"))) << "known gap closed? update this test and bump the cache key"; -} +}}; // ─── T-8. The module-interface language flag ─────────────────────────────── -TEST(SourceKind, ModuleInterfaceLangFlagIsPerCompilerAndNotInterchangeable) { +UTest module_interface_lang_flag_is_per_compiler_and_not_interchangeable{"SourceKind/ModuleInterfaceLangFlagIsPerCompilerAndNotInterchangeable", [] { // Measured 2026-08-11: `-x c++-module` makes GCC exit with "language // c++-module not recognized", and `-x c++` makes Clang emit a 174-byte // stub instead of a module BMI. The spelling is a property of the compiler @@ -209,12 +209,9 @@ TEST(SourceKind, ModuleInterfaceLangFlagIsPerCompilerAndNotInterchangeable) { tc.compiler = id; return mcpp::toolchain::bmi_traits(tc); }; - EXPECT_EQ(traits_for(mcpp::toolchain::CompilerId::GCC).moduleInterfaceLangFlag, - " -x c++"); - EXPECT_EQ(traits_for(mcpp::toolchain::CompilerId::Clang).moduleInterfaceLangFlag, - " -x c++-module"); - EXPECT_EQ(traits_for(mcpp::toolchain::CompilerId::MSVC).moduleInterfaceLangFlag, - " /interface /TP"); + expect(eq(traits_for(mcpp::toolchain::CompilerId::GCC).moduleInterfaceLangFlag, " -x c++")); + expect(eq(traits_for(mcpp::toolchain::CompilerId::Clang).moduleInterfaceLangFlag, " -x c++-module")); + expect(eq(traits_for(mcpp::toolchain::CompilerId::MSVC).moduleInterfaceLangFlag, " /interface /TP")); // Never empty: mcpp tells the driver EVERY time rather than tracking which // suffix each driver version happens to know. That table would expire with @@ -223,8 +220,8 @@ TEST(SourceKind, ModuleInterfaceLangFlagIsPerCompilerAndNotInterchangeable) { for (auto id : {mcpp::toolchain::CompilerId::GCC, mcpp::toolchain::CompilerId::Clang, mcpp::toolchain::CompilerId::MSVC}) - EXPECT_FALSE(traits_for(id).moduleInterfaceLangFlag.empty()); -} + expect(!(traits_for(id).moduleInterfaceLangFlag.empty())); +}}; // ─── T-6. Device translation units ───────────────────────────────────────── // @@ -234,27 +231,27 @@ TEST(SourceKind, ModuleInterfaceLangFlagIsPerCompilerAndNotInterchangeable) { // about the language, which is what lets one kind cover CUDA C++, HIP, and // dialects that are not C++ at all. -TEST(SourceKind, ClassifiesDeviceTranslationUnits) { +UTest classifies_device_translation_units{"SourceKind/ClassifiesDeviceTranslationUnits", [] { auto t = mcpp::builtin_extension_table(); - EXPECT_EQ(mcpp::classify("src/k.cu", t), SourceKind::Device); - EXPECT_EQ(mcpp::classify("src/k.hip", t), SourceKind::Device); -} + expect(eq(mcpp::classify("src/k.cu", t), SourceKind::Device)); + expect(eq(mcpp::classify("src/k.hip", t), SourceKind::Device)); +}}; -TEST(SourceKind, ClassifiesSyclTranslationUnitsByCompilerNotDialect) { +UTest classifies_sycl_translation_units_by_compiler_not_dialect{"SourceKind/ClassifiesSyclTranslationUnitsByCompilerNotDialect", [] { // A `.sycl` unit is ordinary C++ and is still a device TU, because the // criterion is which compiler consumes it: a SYCL unit goes to icpx or a // clang with the SYCL front end, neither of which mcpp drives and neither // of which accepts C++20 modules. Naming it `.cpp` and routing it by glob // would make one extension mean two things depending on match order. auto t = mcpp::builtin_extension_table(); - EXPECT_EQ(mcpp::classify("src/kernels/saxpy.sycl", t), SourceKind::Device); - EXPECT_TRUE(mcpp::is_scan_exempt(SourceKind::Device)); + expect(eq(mcpp::classify("src/kernels/saxpy.sycl", t), SourceKind::Device)); + expect(mcpp::is_scan_exempt(SourceKind::Device)); // And the C++ spelling of the same content stays C++: adding the row must // not reclassify anything that is not literally named `.sycl`. - EXPECT_EQ(mcpp::classify("src/kernels/saxpy.cpp", t), SourceKind::Cxx); -} + expect(eq(mcpp::classify("src/kernels/saxpy.cpp", t), SourceKind::Cxx)); +}}; -TEST(SourceKind, ClassifiesShaderAndKernelLanguages) { +UTest classifies_shader_and_kernel_languages{"SourceKind/ClassifiesShaderAndKernelLanguages", [] { // The criterion is "a separate compiler consumes it", not "NVIDIA ships // it". Before this, the list was `.cu` and `.hip` alone, and a shader in a // constrained glob was refused with "no role for the extension '.comp'" — @@ -267,10 +264,10 @@ TEST(SourceKind, ClassifiesShaderAndKernelLanguages) { "shaders/s.rint", "shaders/s.rahit", "shaders/s.rchit", "shaders/s.rmiss", "shaders/s.rcall", "shaders/s.glsl", "shaders/s.hlsl", "kernels/k.cl", "kernels/k.metal"}) - EXPECT_EQ(mcpp::classify(p, t), SourceKind::Device) << p; -} + expect(eq(mcpp::classify(p, t), SourceKind::Device)) << p; +}}; -TEST(SourceKind, ShaderExtensionsDoNotWidenTheDefaultGlob) { +UTest shader_extensions_do_not_widen_the_default_glob{"SourceKind/ShaderExtensionsDoNotWidenTheDefaultGlob", [] { // The whole safety argument for widening the table rests on this: the // default globs are unchanged, so no published package starts compiling a // vendored shader on the next upgrade. Asserted over the WHOLE list rather @@ -281,72 +278,72 @@ TEST(SourceKind, ShaderExtensionsDoNotWidenTheDefaultGlob) { "src/**/*.cppm", "src/**/*.cpp", "src/**/*.cc", "src/**/*.c", "src/**/*.S", "src/**/*.s", "src/**/*.asm", }; - EXPECT_EQ(globs, expected); -} + expect(eq(globs, expected)); +}}; -TEST(SourceKind, ShaderExtensionsCannotBeClaimedAsModuleInterfaces) { +UTest shader_extensions_cannot_be_claimed_as_module_interfaces{"SourceKind/ShaderExtensionsCannotBeClaimedAsModuleInterfaces", [] { // Reserved for the same reason `.cu` is: routing a shader to the C++ // module rule fails somewhere that names neither the file nor the key. for (const char* ext : {".comp", ".hlsl", ".cl", ".metal", ".sycl"}) { auto err = mcpp::validate_module_extensions(std::vector{ext}); - ASSERT_TRUE(err.has_value()) << ext; - EXPECT_NE(err->find(ext), std::string::npos) << ext; + expect(fatal(err.has_value())) << ext; + expect(ne(err->find(ext), std::string::npos)) << ext; } -} +}}; -TEST(SourceKind, ShaderObjectsUseTheCollisionProofName) { +UTest shader_objects_use_the_collision_proof_name{"SourceKind/ShaderObjectsUseTheCollisionProofName", [] { // `scale.comp` and `scale.vert` are one rename apart in a shader // directory, and both would be `scale.o` under the stem-named form. - EXPECT_EQ(mcpp::object_filename_for("shaders/scale.comp", ".o"), "scale.comp.o"); - EXPECT_EQ(mcpp::object_filename_for("shaders/scale.vert", ".o"), "scale.vert.o"); -} + expect(eq(mcpp::object_filename_for("shaders/scale.comp", ".o"), "scale.comp.o")); + expect(eq(mcpp::object_filename_for("shaders/scale.vert", ".o"), "scale.vert.o")); +}}; -TEST(SourceKind, DeviceHeadersAffectGraphShape) { +UTest device_headers_affect_graph_shape{"SourceKind/DeviceHeadersAffectGraphShape", [] { // `.cuh` reaches a device TU through the preprocessor, so editing one can // change what the graph should be. Leaving it in `Other` is why a project // would see "edited the kernel header, nothing rebuilt". auto t = mcpp::builtin_extension_table(); - EXPECT_EQ(mcpp::classify("src/k.cuh", t), SourceKind::Header); - EXPECT_EQ(mcpp::classify("src/k.hiph", t), SourceKind::Header); - EXPECT_TRUE(mcpp::affects_graph_shape(SourceKind::Header)); -} + expect(eq(mcpp::classify("src/k.cuh", t), SourceKind::Header)); + expect(eq(mcpp::classify("src/k.hiph", t), SourceKind::Header)); + expect(bool(mcpp::affects_graph_shape(SourceKind::Header))); +}}; -TEST(SourceKind, DevicePredicates) { +UTest device_predicates{"SourceKind/DevicePredicates", [] { // Scan-exempt for the same reason assembly is: a device TU carries no // `import`, so there is nothing for P1689 to answer. - EXPECT_TRUE (mcpp::is_scan_exempt(SourceKind::Device)); - EXPECT_FALSE(mcpp::produces_bmi(SourceKind::Device)); - EXPECT_FALSE(mcpp::is_cxx_like(SourceKind::Device)); - EXPECT_FALSE(mcpp::links_unconditionally(SourceKind::Device)); + expect(mcpp::is_scan_exempt(SourceKind::Device)); + expect(!mcpp::produces_bmi(SourceKind::Device)); + expect(!mcpp::is_cxx_like(SourceKind::Device)); + expect(!mcpp::links_unconditionally(SourceKind::Device)); // Absent for the same reason assembly is absent: the content change is // tracked by ninja, and a NEW file is `program_inputs_stale`'s question. - EXPECT_FALSE(mcpp::affects_graph_shape(SourceKind::Device)); - EXPECT_EQ(mcpp::to_string(SourceKind::Device), "device"); -} + expect(!mcpp::affects_graph_shape(SourceKind::Device)); + expect(eq(mcpp::to_string(SourceKind::Device), "device")); +}}; -TEST(SourceKind, DeviceExtensionsAreNotInTheDefaultGlob) { +UTest device_extensions_are_not_in_the_default_glob{"SourceKind/DeviceExtensionsAreNotInTheDefaultGlob", [] { // Same compatibility argument as the built-in module-extension table: a // published package that vendors a `.cu` it builds elsewhere must not // start compiling it on the next mcpp upgrade. Device sources are opted // into by naming them in a `kind = "device"` target. auto globs = mcpp::default_source_globs(mcpp::builtin_extension_table()); for (auto const& g : globs) { - EXPECT_EQ(g.find(".cu"), std::string::npos) << g; - EXPECT_EQ(g.find(".hip"), std::string::npos) << g; + expect(eq(g.find(".cu"), std::string::npos)) << g; + expect(eq(g.find(".hip"), std::string::npos)) << g; } - EXPECT_EQ(mcpp::default_source_globs_note(mcpp::builtin_extension_table()) - .find("cu"), std::string::npos); -} + expect(eq(mcpp::default_source_globs_note(mcpp::builtin_extension_table()) + .find("cu"), std::string::npos)); +}}; -TEST(SourceKind, DeviceObjectsUseTheCollisionProofName) { +UTest device_objects_use_the_collision_proof_name{"SourceKind/DeviceObjectsUseTheCollisionProofName", [] { // `k.cu` and `k.cpp` in one directory are common in a mixed project; the // stem-named form would give both `k.o`. - EXPECT_EQ(mcpp::object_filename_for("src/k.cu", ".o"), "k.cu.o"); - EXPECT_EQ(mcpp::object_filename_for("src/k.hip", ".o"), "k.hip.o"); -} + expect(eq(mcpp::object_filename_for("src/k.cu", ".o"), "k.cu.o")); + expect(eq(mcpp::object_filename_for("src/k.hip", ".o"), "k.hip.o")); +}}; -TEST(SourceKind, DeviceExtensionsCannotBeClaimedAsModuleInterfaces) { +UTest device_extensions_cannot_be_claimed_as_module_interfaces{"SourceKind/DeviceExtensionsCannotBeClaimedAsModuleInterfaces", [] { auto err = mcpp::validate_module_extensions(std::vector{".cu"}); - ASSERT_TRUE(err.has_value()); - EXPECT_NE(err->find(".cu"), std::string::npos); -} + expect(fatal(err.has_value())); + expect(ne(err->find(".cu"), std::string::npos)); +}}; diff --git a/tests/unit/test_sources.cpp b/tests/unit/test_sources.cpp index b13fb7943..c5e6c17e2 100644 --- a/tests/unit/test_sources.cpp +++ b/tests/unit/test_sources.cpp @@ -2,9 +2,9 @@ // toolchain comes from, the protocol-15 directives a build program states // sources with, and the override checks of the payload unification. -#include import std; +import mcpplibs.mtest; import mcpp.manifest; import mcpp.config; import mcpp.build.build_program; @@ -14,6 +14,8 @@ import mcpp.toolchain.dialect; import mcpp.toolchain.registry; import mcpp.provider.address_set; +using namespace mcpplibs::mtest; + namespace dirs = mcpp::build::directives; namespace addrset = mcpp::provider::addrset; @@ -39,69 +41,69 @@ version = "0.1.0" // ── [xlings.overrides] ────────────────────────────────────────────────────── -TEST(Sources, OverridePathAndTableFormsParse) { +UTest override_path_and_table_forms_parse{"Sources/OverridePathAndTableFormsParse", [] { auto m = manifest(R"( [xlings.overrides] "xim:cmake" = "/usr/bin/cmake" vcpkg = { root = "/opt/vcpkg" } "xim:slang" = { program = "slangc", version = "2026.14.1" } )"); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); using K = mcpp::manifest::XlingsOverride::Kind; const auto& o = m->xlings.overrides; - ASSERT_EQ(o.size(), 3u); - EXPECT_EQ(o.at("xim:cmake").kind, K::Path); - EXPECT_EQ(o.at("xim:cmake").value, "/usr/bin/cmake"); + expect(fatal(eq(o.size(), 3u))); + expect(eq(o.at("xim:cmake").kind, K::Path)); + expect(eq(o.at("xim:cmake").value, "/usr/bin/cmake")); // A key without a namespace names the `xim` package, as everywhere else. - EXPECT_EQ(o.at("xim:vcpkg").kind, K::Root); - EXPECT_EQ(o.at("xim:slang").kind, K::Program); - EXPECT_EQ(o.at("xim:slang").version, "2026.14.1"); - EXPECT_GT(o.at("xim:cmake").line, 0); -} + expect(eq(o.at("xim:vcpkg").kind, K::Root)); + expect(eq(o.at("xim:slang").kind, K::Program)); + expect(eq(o.at("xim:slang").version, "2026.14.1")); + expect(gt(o.at("xim:cmake").line, 0)); +}}; -TEST(Sources, OverrideKeyWithAVersionIsRefused) { +UTest override_key_with_a_version_is_refused{"Sources/OverrideKeyWithAVersionIsRefused", [] { auto m = manifest(R"( [xlings.overrides] "xim:cmake@3.31" = "/usr/bin/cmake" )"); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().message.find("names a version"), std::string::npos) << m.error().message; -} + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().message.find("names a version"), std::string::npos)); !failed) failed << m.error().message; +}}; -TEST(Sources, OverrideNamingBothProgramAndRootIsRefused) { +UTest override_naming_both_program_and_root_is_refused{"Sources/OverrideNamingBothProgramAndRootIsRefused", [] { auto m = manifest(R"( [xlings.overrides] cmake = { program = "/usr/bin/cmake", root = "/usr" } )"); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().message.find("exactly one of"), std::string::npos) << m.error().message; -} + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().message.find("exactly one of"), std::string::npos)); !failed) failed << m.error().message; +}}; -TEST(Sources, OverrideUnknownKeyIsRefused) { +UTest override_unknown_key_is_refused{"Sources/OverrideUnknownKeyIsRefused", [] { auto m = manifest(R"( [xlings.overrides] cmake = { programme = "/usr/bin/cmake" } )"); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().message.find("programme"), std::string::npos) << m.error().message; -} + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().message.find("programme"), std::string::npos)); !failed) failed << m.error().message; +}}; -TEST(Sources, OverrideUnderATargetSelectorParses) { +UTest override_under_a_target_selector_parses{"Sources/OverrideUnderATargetSelectorParses", [] { auto m = manifest(R"( [target.'cfg(os = "linux")'.xlings.overrides] "xim:cmake" = "/usr/bin/cmake" )"); - ASSERT_TRUE(m.has_value()) << m.error().format(); - ASSERT_FALSE(m->conditionalConfigs.empty()); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(fatal(!(m->conditionalConfigs.empty()))); bool found = false; for (auto const& cc : m->conditionalConfigs) if (cc.xlings.overrides.contains("xim:cmake")) found = true; - EXPECT_TRUE(found); -} + expect(bool(found)); +}}; // ── provision = "on-request" ──────────────────────────────────────────────── -TEST(Sources, ProvisionOnRequestIsRecordedOnBothTables) { +UTest provision_on_request_is_recorded_on_both_tables{"Sources/ProvisionOnRequestIsRecordedOnBothTables", [] { auto m = manifest(R"( [features] tools = {} @@ -111,131 +113,131 @@ tools = {} [feature-xlings.tools] "xim:slang" = { version = "", provision = "on-request", when = "build" } )"); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_TRUE(m->xlings.on_request("xim:cmake@>=3.31")); - EXPECT_FALSE(m->xlings.on_request("xim:ninja@1.12")); - EXPECT_TRUE(m->xlings.on_request("xim:slang")); - EXPECT_EQ(m->xlings.when_of("xim:slang"), mcpp::manifest::ToolWhen::Build); -} - -TEST(Sources, ProvisionUnknownModeIsRefused) { + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(bool(m->xlings.on_request("xim:cmake@>=3.31"))); + expect(!(m->xlings.on_request("xim:ninja@1.12"))); + expect(bool(m->xlings.on_request("xim:slang"))); + expect(eq(m->xlings.when_of("xim:slang"), mcpp::manifest::ToolWhen::Build)); +}}; + +UTest provision_unknown_mode_is_refused{"Sources/ProvisionUnknownModeIsRefused", [] { auto m = manifest(R"( [xlings.workspace] "xim:cmake" = { version = "", provision = "lazy" } )"); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().message.find("on-request"), std::string::npos) << m.error().message; -} + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().message.find("on-request"), std::string::npos)); !failed) failed << m.error().message; +}}; // ── [toolchain] tables ────────────────────────────────────────────────────── -TEST(Sources, ToolchainTableNamesAPath) { +UTest toolchain_table_names_a_path{"Sources/ToolchainTableNamesAPath", [] { auto m = manifest(R"( [toolchain] default = { path = "/opt/llvm", launcher = "ccache", tools = { ld = "/opt/lld/bin/ld.lld" } } bootstrap = "llvm@22.1.8" )"); - ASSERT_TRUE(m.has_value()) << m.error().format(); - EXPECT_EQ(m->toolchain.for_platform("linux").value_or(""), "path:/opt/llvm"); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); + expect(eq(m->toolchain.for_platform("linux").value_or(""), "path:/opt/llvm")); auto* lt = m->toolchain.local_for("linux"); - ASSERT_NE(lt, nullptr); - EXPECT_EQ(lt->launcher, "ccache"); - ASSERT_EQ(lt->tools.size(), 1u); - EXPECT_EQ(lt->tools[0].first, "ld"); - EXPECT_EQ(m->toolchain.bootstrap, "llvm@22.1.8"); + expect(fatal(ne(lt, nullptr))); + expect(eq(lt->launcher, "ccache")); + expect(fatal(eq(lt->tools.size(), 1u))); + expect(eq(lt->tools[0].first, "ld")); + expect(eq(m->toolchain.bootstrap, "llvm@22.1.8")); // `bootstrap` is not a platform. - EXPECT_FALSE(m->toolchain.byPlatform.contains("bootstrap")); -} + expect(!(m->toolchain.byPlatform.contains("bootstrap"))); +}}; -TEST(Sources, ToolchainConfigureIsAloneInItsTable) { +UTest toolchain_configure_is_alone_in_its_table{"Sources/ToolchainConfigureIsAloneInItsTable", [] { auto ok = manifest(R"( [toolchain] default = { configure = "build.mcpp" } )"); - ASSERT_TRUE(ok.has_value()) << ok.error().format(); - EXPECT_EQ(ok->toolchain.for_platform("macos").value_or(""), "configure:build.mcpp"); + if (auto&& failed = expect(fatal(ok.has_value())); !failed) failed << ok.error().format(); + expect(eq(ok->toolchain.for_platform("macos").value_or(""), "configure:build.mcpp")); auto both = manifest(R"( [toolchain] default = { configure = "build.mcpp", path = "/opt/llvm" } )"); - ASSERT_FALSE(both.has_value()); + expect(fatal(!both.has_value())); auto other = manifest(R"( [toolchain] default = { configure = "cmake" } )"); - ASSERT_FALSE(other.has_value()); - EXPECT_NE(other.error().message.find("build.mcpp"), std::string::npos) << other.error().message; -} + expect(fatal(!other.has_value())); + if (auto&& failed = expect(ne(other.error().message.find("build.mcpp"), std::string::npos)); !failed) failed << other.error().message; +}}; -TEST(Sources, ToolchainTableWithoutPathIsRefused) { +UTest toolchain_table_without_path_is_refused{"Sources/ToolchainTableWithoutPathIsRefused", [] { auto m = manifest(R"( [toolchain] linux = { prefix = "aarch64-none-linux-gnu-" } )"); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().message.find("path"), std::string::npos) << m.error().message; -} + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().message.find("path"), std::string::npos)); !failed) failed << m.error().message; +}}; -TEST(Sources, ToolchainUnknownToolRoleIsRefused) { +UTest toolchain_unknown_tool_role_is_refused{"Sources/ToolchainUnknownToolRoleIsRefused", [] { auto m = manifest(R"( [toolchain] default = { path = "/opt/llvm", tools = { linker = "/x" } } )"); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().message.find("linker"), std::string::npos) << m.error().message; -} + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().message.find("linker"), std::string::npos)); !failed) failed << m.error().message; +}}; // ── Protocol-15 directives ────────────────────────────────────────────────── -TEST(Sources, SourceDirectivesFoldIntoTheBuildConfig) { +UTest source_directives_fold_into_the_build_config{"Sources/SourceDirectivesFoldIntoTheBuildConfig", [] { auto d = parse_directives( "mcpp:protocol=15\n" "mcpp:decision=tool:mcpp.deps.cmake:cmake\tchoice\t/usr/bin/cmake\t/p/build.mcpp\t9\txim:cmake\n" "mcpp:xpkg-request=xim:cmake\n" "mcpp:toolchain=path=/opt/llvm\n" "mcpp:toolchain=origin=/p/build.mcpp:4\n"); - EXPECT_FALSE(dirs::protocol_error(d).has_value()); + expect(!(dirs::protocol_error(d).has_value())); mcpp::manifest::Manifest m; dirs::apply(m, d); - ASSERT_EQ(m.buildConfig.toolDecisions.size(), 1u); - EXPECT_TRUE(m.buildConfig.toolDecisions[0].starts_with("tool:mcpp.deps.cmake:cmake\tchoice")); - ASSERT_EQ(m.buildConfig.xpkgRequests.size(), 1u); - EXPECT_EQ(m.buildConfig.xpkgRequests[0], "xim:cmake"); - ASSERT_EQ(m.buildConfig.toolchainStatement.size(), 2u); - EXPECT_EQ(m.buildConfig.toolchainStatement[0], "path=/opt/llvm"); -} - -TEST(Sources, SourceDirectivesAreProtocolFifteen) { + expect(fatal(eq(m.buildConfig.toolDecisions.size(), 1u))); + expect(m.buildConfig.toolDecisions[0].starts_with("tool:mcpp.deps.cmake:cmake\tchoice")); + expect(fatal(eq(m.buildConfig.xpkgRequests.size(), 1u))); + expect(eq(m.buildConfig.xpkgRequests[0], "xim:cmake")); + expect(fatal(eq(m.buildConfig.toolchainStatement.size(), 2u))); + expect(eq(m.buildConfig.toolchainStatement[0], "path=/opt/llvm")); +}}; + +UTest source_directives_are_protocol_fifteen{"Sources/SourceDirectivesAreProtocolFifteen", [] { for (auto wire : {"decision", "xpkg-request", "toolchain"}) { auto def = dirs::find_by_wire(wire); - ASSERT_NE(def, nullptr) << wire; - EXPECT_EQ(def->sinceProtocol, 15) << wire; - EXPECT_FALSE(def->tag.empty()) << wire; + expect(fatal(ne(def, nullptr))) << wire; + expect(eq(def->sinceProtocol, 15)) << wire; + expect(!(def->tag.empty())) << wire; } -} +}}; // ── Override checks in the unification ───────────────────────────────────── -TEST(Sources, OverrideVersionIsCheckedAgainstRequirements) { +UTest override_version_is_checked_against_requirements{"Sources/OverrideVersionIsCheckedAgainstRequirements", [] { std::vector claims{ {"xim:cmake@>=3.31", "mcpp:plugins", 1}, {"xim:cmake", "this project", 0}, }; - EXPECT_FALSE(addrset::override_violation(claims, "xim:cmake", "3.31.6", "[xlings.overrides]")); + expect(!addrset::override_violation(claims, "xim:cmake", "3.31.6", "[xlings.overrides]")); auto bad = addrset::override_violation(claims, "xim:cmake", "3.28.3", "[xlings.overrides]"); - ASSERT_TRUE(bad.has_value()); - EXPECT_NE(bad->find(">=3.31"), std::string::npos) << *bad; - EXPECT_NE(bad->find("mcpp:plugins"), std::string::npos) << *bad; + expect(fatal(bad.has_value())); + if (auto&& failed = expect(ne(bad->find(">=3.31"), std::string::npos)); !failed) failed << *bad; + if (auto&& failed = expect(ne(bad->find("mcpp:plugins"), std::string::npos)); !failed) failed << *bad; // No stated version: nothing to compare, and the requirement is listed. - EXPECT_FALSE(addrset::override_violation(claims, "xim:cmake", "", "[xlings.overrides]")); + expect(!addrset::override_violation(claims, "xim:cmake", "", "[xlings.overrides]")); auto reqs = addrset::requirements_for(claims, "xim:cmake"); - ASSERT_EQ(reqs.size(), 1u); - EXPECT_EQ(reqs[0], ">=3.31 by mcpp:plugins"); -} + expect(fatal(eq(reqs.size(), 1u))); + expect(eq(reqs[0], ">=3.31 by mcpp:plugins")); +}}; // ── The response file a long compile command goes through ─────────────────── -TEST(Sources, ResponseFileQuotesForTheWindowsTokenizer) { +UTest response_file_quotes_for_the_windows_tokenizer{"Sources/ResponseFileQuotesForTheWindowsTokenizer", [] { const std::vector args{ "-std=c++23", "/Tp C:/Program Files/x/build.mcpp", @@ -243,16 +245,16 @@ TEST(Sources, ResponseFileQuotesForTheWindowsTokenizer) { "C:\\with space\\dir\\", }; const auto body = mcpp::build::response_file_body(args, /*gnuQuoting=*/false); - EXPECT_EQ(std::ranges::count(body, '\n'), 4); - EXPECT_NE(body.find("-std=c++23\n"), std::string::npos) << body; - EXPECT_NE(body.find("\"/Tp C:/Program Files/x/build.mcpp\"\n"), std::string::npos) << body; - EXPECT_NE(body.find("\"-DNAME=\\\"v\\\"\"\n"), std::string::npos) << body; + expect(eq(std::ranges::count(body, '\n'), 4)); + expect(ne(body.find("-std=c++23\n"), std::string::npos)) << body; + expect(ne(body.find("\"/Tp C:/Program Files/x/build.mcpp\"\n"), std::string::npos)) << body; + expect(ne(body.find("\"-DNAME=\\\"v\\\"\"\n"), std::string::npos)) << body; // The run of backslashes that ends the argument is doubled, so it does not // escape the closing quote. - EXPECT_NE(body.find("\"C:\\with space\\dir\\\\\"\n"), std::string::npos) << body; -} + expect(ne(body.find("\"C:\\with space\\dir\\\\\"\n"), std::string::npos)) << body; +}}; -TEST(Sources, ResponseFileKeepsBackslashesLiteralForTheGnuTokenizer) { +UTest response_file_keeps_backslashes_literal_for_the_gnu_tokenizer{"Sources/ResponseFileKeepsBackslashesLiteralForTheGnuTokenizer", [] { // clang and GCC read a backslash as an escape, inside quotes as well as // outside, so a Windows path survives only when every backslash is doubled. // Measured with clang 22.1.8: `'-DX=a\b'` in a response file yields `X=ab`, @@ -264,60 +266,60 @@ TEST(Sources, ResponseFileKeepsBackslashesLiteralForTheGnuTokenizer) { R"(-DNAME="v")", }; const auto body = mcpp::build::response_file_body(args, /*gnuQuoting=*/true); - EXPECT_EQ(std::ranges::count(body, '\n'), 4); - EXPECT_NE(body.find("-fmodule-file=mcpp=D:\\\\a\\\\p\\\\mcpp.pcm\n"), std::string::npos) << body; - EXPECT_NE(body.find("D:\\\\a\\\\obj\\\\x.o\n"), std::string::npos) << body; + expect(eq(std::ranges::count(body, '\n'), 4)); + expect(ne(body.find("-fmodule-file=mcpp=D:\\\\a\\\\p\\\\mcpp.pcm\n"), std::string::npos)) << body; + expect(ne(body.find("D:\\\\a\\\\obj\\\\x.o\n"), std::string::npos)) << body; // Whitespace is handled by quoting the whole argument; the doubling holds // inside the quotes too. - EXPECT_NE(body.find("\"/Tp D:\\\\a\\\\b c\\\\build.mcpp\"\n"), std::string::npos) << body; + expect(ne(body.find("\"/Tp D:\\\\a\\\\b c\\\\build.mcpp\"\n"), std::string::npos)) << body; // A quote of the argument's own is escaped. - EXPECT_NE(body.find("-DNAME=\\\"v\\\"\n"), std::string::npos) << body; -} + expect(ne(body.find("-DNAME=\\\"v\\\"\n"), std::string::npos)) << body; +}}; // ── [xlings.overrides] in config.toml ─────────────────────────────────────── -TEST(Sources, ConfigOverridesParseWithTheManifestsTwoShapes) { +UTest config_overrides_parse_with_the_manifests_two_shapes{"Sources/ConfigOverridesParseWithTheManifestsTwoShapes", [] { auto doc = mcpp::libs::toml::parse(R"( [xlings.overrides] "xim:cmake" = "/usr/bin/cmake" vcpkg = { root = "/opt/vcpkg" } "xim:slang" = { program = "slangc", version = "2026.14.1" } )"); - ASSERT_TRUE(doc.has_value()); + expect(fatal(doc.has_value())); auto o = mcpp::config::parse_payload_overrides(*doc); - ASSERT_TRUE(o.has_value()) << o.error(); - ASSERT_EQ(o->size(), 3u); - EXPECT_EQ(o->at("xim:cmake").kind, "path"); - EXPECT_EQ(o->at("xim:cmake").value, "/usr/bin/cmake"); + if (auto&& failed = expect(fatal(o.has_value())); !failed) failed << o.error(); + expect(fatal(eq(o->size(), 3u))); + expect(eq(o->at("xim:cmake").kind, "path")); + expect(eq(o->at("xim:cmake").value, "/usr/bin/cmake")); // A key without a namespace names the `xim` package, as everywhere else. - EXPECT_EQ(o->at("xim:vcpkg").kind, "root"); - EXPECT_EQ(o->at("xim:slang").kind, "program"); - EXPECT_EQ(o->at("xim:slang").version, "2026.14.1"); - EXPECT_GT(o->at("xim:cmake").line, 0); -} + expect(eq(o->at("xim:vcpkg").kind, "root")); + expect(eq(o->at("xim:slang").kind, "program")); + expect(eq(o->at("xim:slang").version, "2026.14.1")); + expect(gt(o->at("xim:cmake").line, 0)); +}}; -TEST(Sources, ConfigOverrideWithAnUnknownKeyIsRefused) { +UTest config_override_with_an_unknown_key_is_refused{"Sources/ConfigOverrideWithAnUnknownKeyIsRefused", [] { auto doc = mcpp::libs::toml::parse(R"( [xlings.overrides] cmake = { programme = "/usr/bin/cmake" } )"); - ASSERT_TRUE(doc.has_value()); + expect(fatal(doc.has_value())); auto o = mcpp::config::parse_payload_overrides(*doc); - ASSERT_FALSE(o.has_value()); - EXPECT_NE(o.error().find("programme"), std::string::npos) << o.error(); -} + expect(fatal(!o.has_value())); + if (auto&& failed = expect(ne(o.error().find("programme"), std::string::npos)); !failed) failed << o.error(); +}}; -TEST(Sources, ConfigWithoutTheTableHasNoOverrides) { +UTest config_without_the_table_has_no_overrides{"Sources/ConfigWithoutTheTableHasNoOverrides", [] { auto doc = mcpp::libs::toml::parse("[toolchain]\ndefault = \"gcc@16.1.0\"\n"); - ASSERT_TRUE(doc.has_value()); + expect(fatal(doc.has_value())); auto o = mcpp::config::parse_payload_overrides(*doc); - ASSERT_TRUE(o.has_value()) << o.error(); - EXPECT_TRUE(o->empty()); -} + if (auto&& failed = expect(fatal(o.has_value())); !failed) failed << o.error(); + expect(o->empty()); +}}; // ── A toolchain named by path ─────────────────────────────────────────────── -TEST(Sources, PathSpecTakesTheFamilyFromTheDrivers) { +UTest path_spec_takes_the_family_from_the_drivers{"Sources/PathSpecTakesTheFamilyFromTheDrivers", [] { const auto base = std::filesystem::temp_directory_path() / "mcpp-test-sources-tc"; std::filesystem::remove_all(base); std::filesystem::create_directories(base / "llvm" / "bin"); @@ -325,32 +327,30 @@ TEST(Sources, PathSpecTakesTheFamilyFromTheDrivers) { std::ofstream(base / "llvm" / "bin" / "clang++") << ""; std::ofstream(base / "gcc" / "bin" / "aarch64-none-linux-gnu-g++") << ""; auto llvm = mcpp::toolchain::parse_toolchain_spec("path:" + (base / "llvm").string()); - ASSERT_TRUE(llvm.has_value()) << llvm.error(); - EXPECT_EQ(llvm->family, mcpp::toolchain::Family::Llvm); - EXPECT_EQ(llvm->spec_str(), "path:" + (base / "llvm").lexically_normal().generic_string()); + if (auto&& failed = expect(fatal(llvm.has_value())); !failed) failed << llvm.error(); + expect(eq(llvm->family, mcpp::toolchain::Family::Llvm)); + expect(eq(llvm->spec_str(), "path:" + (base / "llvm").lexically_normal().generic_string())); auto gcc = mcpp::toolchain::parse_toolchain_spec("path:" + (base / "gcc").string()); - ASSERT_TRUE(gcc.has_value()) << gcc.error(); - EXPECT_EQ(gcc->family, mcpp::toolchain::Family::Gcc); + if (auto&& failed = expect(fatal(gcc.has_value())); !failed) failed << gcc.error(); + expect(eq(gcc->family, mcpp::toolchain::Family::Gcc)); auto none = mcpp::toolchain::parse_toolchain_spec("path:" + (base / "missing").string()); - EXPECT_FALSE(none.has_value()); + expect(!none.has_value()); std::filesystem::remove_all(base); -} +}}; // ── The engine floor read from a manifest that does not parse ─────────────── -TEST(Sources, StatedMcppFloorIsReadFromPackageOnly) { +UTest stated_mcpp_floor_is_read_from_package_only{"Sources/StatedMcppFloorIsReadFromPackageOnly", [] { using mcpp::manifest::stated_mcpp_floor; - EXPECT_EQ(stated_mcpp_floor("[package]\nname = \"p\"\nmcpp = \">=2026.10.1.3\"\n"), - "2026.10.1.3"); + expect(eq(stated_mcpp_floor("[package]\nname = \"p\"\nmcpp = \">=2026.10.1.3\"\n"), "2026.10.1.3")); // A bare version is the same statement. - EXPECT_EQ(stated_mcpp_floor("[package]\nmcpp = \"2026.9.28.3\"\n"), "2026.9.28.3"); + expect(eq(stated_mcpp_floor("[package]\nmcpp = \"2026.9.28.3\"\n"), "2026.9.28.3")); // Only `[package]`: a dependency named mcpp states a dependency, not a floor. - EXPECT_EQ(stated_mcpp_floor("[package]\nname = \"p\"\n\n[dependencies]\nmcpp = \"1.0\"\n"), - ""); + expect(eq(stated_mcpp_floor("[package]\nname = \"p\"\n\n[dependencies]\nmcpp = \"1.0\"\n"), "")); // `[package.metadata]` is another table. - EXPECT_EQ(stated_mcpp_floor("[package.metadata]\nmcpp = \"9.9.9.9\"\n"), ""); + expect(eq(stated_mcpp_floor("[package.metadata]\nmcpp = \"9.9.9.9\"\n"), "")); // A key that merely starts with the name is not the key. - EXPECT_EQ(stated_mcpp_floor("[package]\nmcpp_home = \"/x\"\n"), ""); - EXPECT_EQ(stated_mcpp_floor("[package]\n# mcpp = \"9.9.9.9\"\n"), ""); - EXPECT_EQ(stated_mcpp_floor(""), ""); -} + expect(eq(stated_mcpp_floor("[package]\nmcpp_home = \"/x\"\n"), "")); + expect(eq(stated_mcpp_floor("[package]\n# mcpp = \"9.9.9.9\"\n"), "")); + expect(eq(stated_mcpp_floor(""), "")); +}}; diff --git a/tests/unit/test_static_placement.cpp b/tests/unit/test_static_placement.cpp index a457f8b1a..69920c709 100644 --- a/tests/unit/test_static_placement.cpp +++ b/tests/unit/test_static_placement.cpp @@ -10,11 +10,13 @@ // // Package 0 is the root. `shared` maps each shared package to its image count. -#include import std; +import mcpplibs.mtest; import mcpp.build.plan; +using namespace mcpplibs::mtest; + using mcpp::build::place_static_packages; namespace { @@ -24,87 +26,87 @@ using Edges = std::map>; } // namespace // root -> fw (shared) -> x: x belongs in fw. -TEST(StaticPlacement, AStaticPackageUnderOneSharedLibraryGoesIntoIt) { +UTest a_static_package_under_one_shared_library_goes_into_it{"StaticPlacement/AStaticPackageUnderOneSharedLibraryGoesIntoIt", [] { Edges edges{{0, {1}}, {1, {2}}}; auto p = place_static_packages(edges, {{1, 1}}, {}); - ASSERT_EQ(p.intoImage.size(), 1u); - EXPECT_EQ(p.intoImage.at(2), 1u); - EXPECT_TRUE(p.conflicts.empty()); -} + expect(fatal(eq(p.intoImage.size(), 1u))); + expect(eq(p.intoImage.at(2), 1u)); + expect(p.conflicts.empty()); +}}; // root -> fw -> a -> x, fw -> x: the whole static closure goes into fw. -TEST(StaticPlacement, ADiamondInsideOneImageStaysInThatImage) { +UTest a_diamond_inside_one_image_stays_in_that_image{"StaticPlacement/ADiamondInsideOneImageStaysInThatImage", [] { Edges edges{{0, {1}}, {1, {2, 3}}, {2, {3}}}; auto p = place_static_packages(edges, {{1, 1}}, {}); - EXPECT_EQ(p.intoImage.size(), 2u); - EXPECT_EQ(p.intoImage.at(2), 1u); - EXPECT_EQ(p.intoImage.at(3), 1u); - EXPECT_TRUE(p.conflicts.empty()); -} + expect(eq(p.intoImage.size(), 2u)); + expect(eq(p.intoImage.at(2), 1u)); + expect(eq(p.intoImage.at(3), 1u)); + expect(p.conflicts.empty()); +}}; // root -> fw1 -> x, root -> fw2 -> x: x has no single image. -TEST(StaticPlacement, TwoImagesReachingOnePackageIsAConflict) { +UTest two_images_reaching_one_package_is_a_conflict{"StaticPlacement/TwoImagesReachingOnePackageIsAConflict", [] { Edges edges{{0, {1, 2}}, {1, {3}}, {2, {3}}}; auto p = place_static_packages(edges, {{1, 1}, {2, 1}}, {}); - EXPECT_TRUE(p.intoImage.empty()); - ASSERT_EQ(p.conflicts.size(), 1u); - EXPECT_EQ(p.conflicts[0].package, 3u); - EXPECT_EQ(p.conflicts[0].images, (std::vector{1, 2})); - EXPECT_FALSE(p.conflicts[0].root); -} + expect(p.intoImage.empty()); + expect(fatal(eq(p.conflicts.size(), 1u))); + expect(eq(p.conflicts[0].package, 3u)); + expect(eq(p.conflicts[0].images, (std::vector{1, 2}))); + expect(!(p.conflicts[0].root)); +}}; // root -> fw -> x and root -> x: the program and the library both need it. -TEST(StaticPlacement, ThePackageTheRootAlsoLinksIsAConflictNamingTheRoot) { +UTest the_package_the_root_also_links_is_a_conflict_naming_the_root{"StaticPlacement/ThePackageTheRootAlsoLinksIsAConflictNamingTheRoot", [] { Edges edges{{0, {1, 2}}, {1, {2}}}; auto p = place_static_packages(edges, {{1, 1}}, {}); - EXPECT_TRUE(p.intoImage.empty()); - ASSERT_EQ(p.conflicts.size(), 1u); - EXPECT_EQ(p.conflicts[0].package, 2u); - EXPECT_TRUE(p.conflicts[0].root); -} + expect(p.intoImage.empty()); + expect(fatal(eq(p.conflicts.size(), 1u))); + expect(eq(p.conflicts[0].package, 2u)); + expect(bool(p.conflicts[0].root)); +}}; // root (itself the shared image) -> x: nothing moves; the root's images // already take every static package they reach. -TEST(StaticPlacement, ARootOwnedImageKeepsItsStaticPackages) { +UTest a_root_owned_image_keeps_its_static_packages{"StaticPlacement/ARootOwnedImageKeepsItsStaticPackages", [] { Edges edges{{0, {1}}}; auto p = place_static_packages(edges, {}, {}); - EXPECT_TRUE(p.intoImage.empty()); - EXPECT_TRUE(p.conflicts.empty()); -} + expect(p.intoImage.empty()); + expect(p.conflicts.empty()); +}}; // root -> fw1 -> fw2 -> x: a closure stops at the next shared package, so x // belongs to fw2 and not to fw1. -TEST(StaticPlacement, AClosureStopsAtTheNextSharedLibrary) { +UTest a_closure_stops_at_the_next_shared_library{"StaticPlacement/AClosureStopsAtTheNextSharedLibrary", [] { Edges edges{{0, {1}}, {1, {2}}, {2, {3}}}; auto p = place_static_packages(edges, {{1, 1}, {2, 1}}, {}); - ASSERT_EQ(p.intoImage.size(), 1u); - EXPECT_EQ(p.intoImage.at(3), 2u); - EXPECT_TRUE(p.conflicts.empty()); -} + expect(fatal(eq(p.intoImage.size(), 1u))); + expect(eq(p.intoImage.at(3), 2u)); + expect(p.conflicts.empty()); +}}; // A package with two shared targets is two images: its static closure has no // single home. -TEST(StaticPlacement, OnePackageWithTwoSharedTargetsIsTwoImages) { +UTest one_package_with_two_shared_targets_is_two_images{"StaticPlacement/OnePackageWithTwoSharedTargetsIsTwoImages", [] { Edges edges{{0, {1}}, {1, {2}}}; auto p = place_static_packages(edges, {{1, 2}}, {}); - EXPECT_TRUE(p.intoImage.empty()); - ASSERT_EQ(p.conflicts.size(), 1u); - EXPECT_EQ(p.conflicts[0].package, 2u); -} + expect(p.intoImage.empty()); + expect(fatal(eq(p.conflicts.size(), 1u))); + expect(eq(p.conflicts[0].package, 2u)); +}}; // A boundary (a distribution package, or a package providing a target layer) // is neither placed nor walked through. -TEST(StaticPlacement, ABoundaryIsNeitherPlacedNorTraversed) { +UTest a_boundary_is_neither_placed_nor_traversed{"StaticPlacement/ABoundaryIsNeitherPlacedNorTraversed", [] { Edges edges{{0, {1}}, {1, {2}}, {2, {3}}}; auto p = place_static_packages(edges, {{1, 1}}, {2}); - EXPECT_TRUE(p.intoImage.empty()); - EXPECT_TRUE(p.conflicts.empty()); -} + expect(p.intoImage.empty()); + expect(p.conflicts.empty()); +}}; // A header-only package placed into an image contributes no objects, and the // placement is still well defined. -TEST(StaticPlacement, APackageWithNoObjectsIsPlacedLikeAnyOther) { +UTest a_package_with_no_objects_is_placed_like_any_other{"StaticPlacement/APackageWithNoObjectsIsPlacedLikeAnyOther", [] { Edges edges{{0, {1}}, {1, {2}}}; auto p = place_static_packages(edges, {{1, 1}}, {}); - EXPECT_EQ(p.intoImage.at(2), 1u); -} + expect(eq(p.intoImage.at(2), 1u)); +}}; diff --git a/tests/unit/test_subos_info.cpp b/tests/unit/test_subos_info.cpp index 66daef297..b4d06babf 100644 --- a/tests/unit/test_subos_info.cpp +++ b/tests/unit/test_subos_info.cpp @@ -7,12 +7,14 @@ // that has been around a while, and silence there is what made mcpp#352 // expensive to find in the first place. -#include import std; +import mcpplibs.mtest; import mcpp.runtime.launch_env; import mcpp.provider.xlings.subos_info; +using namespace mcpplibs::mtest; + namespace su = mcpp::provider::xlings::subos; namespace { @@ -30,7 +32,7 @@ struct Tmp { } }; -TEST(SubosInfo, ReadsRuntimeAndEnvDeclarations) { +UTest reads_runtime_and_env_declarations{"SubosInfo/ReadsRuntimeAndEnvDeclarations", [] { Tmp t; t.write(R"({ "workspace": {}, @@ -64,26 +66,25 @@ TEST(SubosInfo, ReadsRuntimeAndEnvDeclarations) { } })"); auto info = su::read(t.dir); - EXPECT_TRUE(info.present); - EXPECT_EQ(info.schema, 1); - EXPECT_EQ(info.runtime, "glibc@2.39"); - EXPECT_EQ(info.hostGlibc, "2.43"); - ASSERT_EQ(info.providers.size(), 1u); - EXPECT_EQ(info.providers[0].binding, "mesa@25.0.7.1"); - ASSERT_EQ(info.providers[0].decls.size(), 2u); - EXPECT_EQ(info.providers[0].decls[0].var, "LIBGL_DRIVERS_PATH"); - ASSERT_EQ(info.runtimeProviders.size(), 1u); - EXPECT_EQ(info.runtimeProviders[0].capability, "render.demo"); - EXPECT_EQ(info.runtimeProviders[0].provider.namespace_, "xim"); - EXPECT_EQ(info.runtimeProviders[0].provider.name, "renderer"); - ASSERT_EQ(info.runtimeArtifacts.size(), 1u); - EXPECT_EQ(info.runtimeArtifacts[0].path, - (t.dir / "runtime/renderer.bin").lexically_normal()); - EXPECT_EQ(info.runtimeArtifacts[0].hostFingerprint, "host-2"); - EXPECT_TRUE(info.note.empty()); -} - -TEST(SubosInfo, OrdersGenericContractByTheCompleteFactIdentity) { + expect(bool(info.present)); + expect(eq(info.schema, 1)); + expect(eq(info.runtime, "glibc@2.39")); + expect(eq(info.hostGlibc, "2.43")); + expect(fatal(eq(info.providers.size(), 1u))); + expect(eq(info.providers[0].binding, "mesa@25.0.7.1")); + expect(fatal(eq(info.providers[0].decls.size(), 2u))); + expect(eq(info.providers[0].decls[0].var, "LIBGL_DRIVERS_PATH")); + expect(fatal(eq(info.runtimeProviders.size(), 1u))); + expect(eq(info.runtimeProviders[0].capability, "render.demo")); + expect(eq(info.runtimeProviders[0].provider.namespace_, "xim")); + expect(eq(info.runtimeProviders[0].provider.name, "renderer")); + expect(fatal(eq(info.runtimeArtifacts.size(), 1u))); + expect(eq(info.runtimeArtifacts[0].path, (t.dir / "runtime/renderer.bin").lexically_normal())); + expect(eq(info.runtimeArtifacts[0].hostFingerprint, "host-2")); + expect(info.note.empty()); +}}; + +UTest orders_generic_contract_by_the_complete_fact_identity{"SubosInfo/OrdersGenericContractByTheCompleteFactIdentity", [] { Tmp t; t.write(R"({"subos_info":{"schema_version":1,"runtime":"glibc@2.39", "runtime_contract":{ @@ -103,79 +104,77 @@ TEST(SubosInfo, OrdersGenericContractByTheCompleteFactIdentity) { ] }}})"); auto info = su::read(t.dir); - ASSERT_EQ(info.runtimeProviders.size(), 2u); - EXPECT_EQ(info.runtimeProviders[0].provider.source, "a-index"); - EXPECT_EQ(info.runtimeProviders[1].provider.source, "z-index"); - ASSERT_EQ(info.runtimeArtifacts.size(), 2u); - EXPECT_EQ(info.runtimeArtifacts[0].provenance, "a-view"); - EXPECT_EQ(info.runtimeArtifacts[1].provenance, "z-view"); -} + expect(fatal(eq(info.runtimeProviders.size(), 2u))); + expect(eq(info.runtimeProviders[0].provider.source, "a-index")); + expect(eq(info.runtimeProviders[1].provider.source, "z-index")); + expect(fatal(eq(info.runtimeArtifacts.size(), 2u))); + expect(eq(info.runtimeArtifacts[0].provenance, "a-view")); + expect(eq(info.runtimeArtifacts[1].provenance, "z-view")); +}}; // `${subosdir}` expands against the subos the block was read FROM, not // against any global notion of "the" subos. That is what lets a program // built under one subos run correctly under another. -TEST(SubosInfo, ResolvesSubosdirPlaceholder) { +UTest resolves_subosdir_placeholder{"SubosInfo/ResolvesSubosdirPlaceholder", [] { Tmp t; t.write(R"({"subos_info":{"schema_version":1,"runtime":"glibc@2.39", "envs":{"mesa@1":[ {"var":"LIBGL_DRIVERS_PATH","op":"prepend","value":"${subosdir}/usr/lib/dri"}]}}})"); auto env = su::resolve_env(su::read(t.dir), t.dir); - ASSERT_EQ(env.size(), 1u); - EXPECT_EQ(env[0].first, "LIBGL_DRIVERS_PATH"); + expect(fatal(eq(env.size(), 1u))); + expect(eq(env[0].first, "LIBGL_DRIVERS_PATH")); // Literal concatenation, NOT a path join. The separator in the declaration // belongs to the subos manifest and is substituted verbatim; turning it // into the host's would rewrite a value we do not own. On Windows the two // spellings differ and the path-join form is the wrong expectation. - EXPECT_EQ(env[0].second, t.dir.string() + "/usr/lib/dri"); -} + expect(eq(env[0].second, t.dir.string() + "/usr/lib/dri")); +}}; // Several providers may contribute to one variable — that is the normal // shape for the graphics stack, where mesa and a vendor bridge both add an // EGL vendor directory. `prepend` joins them; it must not drop either. -TEST(SubosInfo, PrependJoinsProvidersInOrder) { +UTest prepend_joins_providers_in_order{"SubosInfo/PrependJoinsProvidersInOrder", [] { Tmp t; t.write(R"({"subos_info":{"schema_version":1,"runtime":"glibc@2.39","envs":{ "a-mesa@1":[{"var":"V","op":"prepend","value":"${subosdir}/one"}], "b-vendor@1":[{"var":"V","op":"prepend","value":"${subosdir}/two"}]}}})"); auto env = su::resolve_env(su::read(t.dir), t.dir); - ASSERT_EQ(env.size(), 1u); + expect(fatal(eq(env.size(), 1u))); const auto sep = mcpp::runtime::launch_env::path_list_separator(); - EXPECT_EQ(env[0].second, - t.dir.string() + "/two" + sep + t.dir.string() + "/one") + expect(eq(env[0].second, t.dir.string() + "/two" + sep + t.dir.string() + "/one")) << "both providers must survive, later binding front-most"; -} +}}; // The same value arriving twice must not accumulate: nested invocations // would otherwise grow the variable without bound. -TEST(SubosInfo, PrependDeduplicates) { +UTest prepend_deduplicates{"SubosInfo/PrependDeduplicates", [] { Tmp t; t.write(R"({"subos_info":{"schema_version":1,"runtime":"glibc@2.39","envs":{ "a@1":[{"var":"V","op":"prepend","value":"${subosdir}/x"}], "b@1":[{"var":"V","op":"prepend","value":"${subosdir}/x"}]}}})"); auto env = su::resolve_env(su::read(t.dir), t.dir); - ASSERT_EQ(env.size(), 1u); + expect(fatal(eq(env.size(), 1u))); // One entry, not two. The de-duplication has to split on the PLATFORM's // list separator: keyed on ':' it would cut "C:\\x" apart on Windows, // match nothing, and grow the list on every nested invocation. - EXPECT_EQ(env[0].second, t.dir.string() + "/x"); - EXPECT_EQ(env[0].second.find(mcpp::runtime::launch_env::path_list_separator()), - std::string::npos); -} + expect(eq(env[0].second, t.dir.string() + "/x")); + expect(eq(env[0].second.find(mcpp::runtime::launch_env::path_list_separator()), std::string::npos)); +}}; // `set` supplies a value only when the caller did not define the variable. // Presence — not non-emptiness — is the boundary, matching xlings: an // explicitly empty export is still an intentional caller value. -TEST(SubosInfo, SetAppliesWhenAmbientIsAbsent) { +UTest set_applies_when_ambient_is_absent{"SubosInfo/SetAppliesWhenAmbientIsAbsent", [] { Tmp t; t.write(R"({"subos_info":{"schema_version":1,"runtime":"glibc@2.39","envs":{ "a@1":[{"var":"V","op":"prepend","value":"/one"}], "b@1":[{"var":"V","op":"set","value":"/two"}]}}})"); auto env = su::resolve_env(su::read(t.dir), t.dir); - ASSERT_EQ(env.size(), 1u); - EXPECT_EQ(env[0].second, "/two"); -} + expect(fatal(eq(env.size(), 1u))); + expect(eq(env[0].second, "/two")); +}}; -TEST(SubosInfo, SetPreservesPresentAmbientValue) { +UTest set_preserves_present_ambient_value{"SubosInfo/SetPreservesPresentAmbientValue", [] { Tmp t; t.write(R"({"subos_info":{"schema_version":1,"runtime":"glibc@2.39","envs":{ "a@1":[{"var":"V","op":"set","value":"/subos"}]}}})"); @@ -185,11 +184,11 @@ TEST(SubosInfo, SetPreservesPresentAmbientValue) { if (var == "V") return "/caller"; return std::nullopt; }); - ASSERT_EQ(env.size(), 1u); - EXPECT_EQ(env[0].second, "/caller"); -} + expect(fatal(eq(env.size(), 1u))); + expect(eq(env[0].second, "/caller")); +}}; -TEST(SubosInfo, SetPreservesExplicitlyEmptyAmbientValue) { +UTest set_preserves_explicitly_empty_ambient_value{"SubosInfo/SetPreservesExplicitlyEmptyAmbientValue", [] { Tmp t; t.write(R"({"subos_info":{"schema_version":1,"runtime":"glibc@2.39","envs":{ "a@1":[{"var":"V","op":"set","value":"/subos"}]}}})"); @@ -199,64 +198,64 @@ TEST(SubosInfo, SetPreservesExplicitlyEmptyAmbientValue) { if (var == "V") return std::string{}; return std::nullopt; }); - ASSERT_EQ(env.size(), 1u); - EXPECT_TRUE(env[0].second.empty()); -} + expect(fatal(eq(env.size(), 1u))); + expect(env[0].second.empty()); +}}; // A subos made before xlings grew the block. Degrade, and SAY SO — this is // the common case on an existing machine, and mcpp's own sandbox subos was // measured in exactly this state. -TEST(SubosInfo, MissingBlockDegradesWithANote) { +UTest missing_block_degrades_with_a_note{"SubosInfo/MissingBlockDegradesWithANote", [] { Tmp t; t.write(R"({"workspace":{}})"); auto info = su::read(t.dir); - EXPECT_FALSE(info.present); - EXPECT_TRUE(info.runtime.empty()); - EXPECT_FALSE(info.note.empty()); - EXPECT_TRUE(su::resolve_env(info, t.dir).empty()); -} + expect(!info.present); + expect(info.runtime.empty()); + expect(!info.note.empty()); + expect(su::resolve_env(info, t.dir).empty()); +}}; // A schema newer than we understand: read what we can, and say we are behind. // Refusing outright would make a newer xlings break an older mcpp, which is // the failure mode the index-floor incident already paid for once. -TEST(SubosInfo, NewerSchemaIsUsedAndAnnounced) { +UTest newer_schema_is_used_and_announced{"SubosInfo/NewerSchemaIsUsedAndAnnounced", [] { Tmp t; t.write(R"({"subos_info":{"schema_version":99,"runtime":"glibc@2.44","envs":[]}})"); auto info = su::read(t.dir); - EXPECT_TRUE(info.present); - EXPECT_EQ(info.runtime, "glibc@2.44"); - EXPECT_FALSE(info.note.empty()); -} + expect(bool(info.present)); + expect(eq(info.runtime, "glibc@2.44")); + expect(!info.note.empty()); +}}; -TEST(SubosInfo, NoFileAtAllIsNotACrash) { +UTest no_file_at_all_is_not_a_crash{"SubosInfo/NoFileAtAllIsNotACrash", [] { Tmp t; // nothing written auto info = su::read(t.dir); - EXPECT_FALSE(info.present); - EXPECT_FALSE(info.note.empty()); -} + expect(!info.present); + expect(!info.note.empty()); +}}; -TEST(SubosInfo, MalformedJsonDegrades) { +UTest malformed_json_degrades{"SubosInfo/MalformedJsonDegrades", [] { Tmp t; t.write("{ this is not json"); auto info = su::read(t.dir); - EXPECT_FALSE(info.present); - EXPECT_FALSE(info.note.empty()); -} + expect(!info.present); + expect(!info.note.empty()); +}}; // The family mapping is part of the cross-repo contract: xlings derives it // from the runtime string too, and the two must agree or a subos and a build // will disagree about what ABI they are talking about. -TEST(SubosInfo, FamilyOfMirrorsXlings) { - EXPECT_EQ(su::family_of("glibc@2.39"), "linux-x86_64-glibc"); - EXPECT_EQ(su::family_of("musl@1.2.5"), "linux-x86_64-musl"); - EXPECT_EQ(su::family_of("glibc@2.39", "aarch64"), "linux-aarch64-glibc"); - EXPECT_EQ(su::family_of("wasi-libc@1"), "wasm32-wasi"); - EXPECT_EQ(su::family_of("macos_sdk@14.0", "arm64"), "darwin-arm64"); - EXPECT_EQ(su::family_of("ucrt@10"), "windows-x86_64-ucrt"); - EXPECT_EQ(su::family_of("nonsense@1"), "unknown"); +UTest family_of_mirrors_xlings{"SubosInfo/FamilyOfMirrorsXlings", [] { + expect(eq(su::family_of("glibc@2.39"), "linux-x86_64-glibc")); + expect(eq(su::family_of("musl@1.2.5"), "linux-x86_64-musl")); + expect(eq(su::family_of("glibc@2.39", "aarch64"), "linux-aarch64-glibc")); + expect(eq(su::family_of("wasi-libc@1"), "wasm32-wasi")); + expect(eq(su::family_of("macos_sdk@14.0", "arm64"), "darwin-arm64")); + expect(eq(su::family_of("ucrt@10"), "windows-x86_64-ucrt")); + expect(eq(su::family_of("nonsense@1"), "unknown")); // No '@' at all is not a binding; it must not be read as one. - EXPECT_EQ(su::family_of("glibc"), "linux-x86_64-glibc"); -} + expect(eq(su::family_of("glibc"), "linux-x86_64-glibc")); +}}; // A VERBATIM capture of what a real xlings wrote, after `xlings install @@ -272,7 +271,7 @@ TEST(SubosInfo, FamilyOfMirrorsXlings) { // // A fixture composed from the same understanding as the parser cannot catch // that. Only one taken from the writer can. -TEST(SubosInfo, RealXlingsCapture) { +UTest real_xlings_capture{"SubosInfo/RealXlingsCapture", [] { Tmp t; t.write(R"({ "subos_info": { @@ -295,42 +294,41 @@ TEST(SubosInfo, RealXlingsCapture) { })"); auto info = su::read(t.dir); - ASSERT_TRUE(info.present); - EXPECT_EQ(info.runtime, "glibc@2.39"); - ASSERT_EQ(info.providers.size(), 2u); - EXPECT_EQ(info.providers[0].binding, "mesa@25.0.7.1"); - EXPECT_EQ(info.providers[1].binding, "nvidia-gl-host-link@0.1.1"); + expect(fatal(bool(info.present))); + expect(eq(info.runtime, "glibc@2.39")); + expect(fatal(eq(info.providers.size(), 2u))); + expect(eq(info.providers[0].binding, "mesa@25.0.7.1")); + expect(eq(info.providers[1].binding, "nvidia-gl-host-link@0.1.1")); auto env = su::resolve_env(info, t.dir); - ASSERT_EQ(env.size(), 3u) << "all three graphics variables must be produced"; + expect(fatal(eq(env.size(), 3u))) << "all three graphics variables must be produced"; std::map byVar; for (auto& [k, v] : env) byVar[k] = v; const auto sep = mcpp::runtime::launch_env::path_list_separator(); - EXPECT_EQ(byVar["LIBGL_DRIVERS_PATH"], t.dir.string() + "/usr/lib/dri"); - EXPECT_EQ(byVar["XDG_DATA_DIRS"], t.dir.string() + "/share"); + expect(eq(byVar["LIBGL_DRIVERS_PATH"], t.dir.string() + "/usr/lib/dri")); + expect(eq(byVar["XDG_DATA_DIRS"], t.dir.string() + "/share")); // Both providers name the same vendor directory; de-duplication must // leave exactly one, or libglvnd sees it twice and enumerates the device // twice -- which is a defect xlings hit on its own side. - EXPECT_EQ(byVar["__EGL_VENDOR_LIBRARY_DIRS"], - t.dir.string() + "/share/glvnd/egl_vendor.d"); - EXPECT_EQ(byVar["__EGL_VENDOR_LIBRARY_DIRS"].find(sep), std::string::npos); -} + expect(eq(byVar["__EGL_VENDOR_LIBRARY_DIRS"], t.dir.string() + "/share/glvnd/egl_vendor.d")); + expect(eq(byVar["__EGL_VENDOR_LIBRARY_DIRS"].find(sep), std::string::npos)); +}}; // xlings drops a declaration whose op it does not recognise. A reader more // permissive than its writer eventually applies something the writer meant to // reject, so this asserts the same refusal rather than a tolerant guess. -TEST(SubosInfo, UnknownOpIsDroppedLikeXlingsDrops) { +UTest unknown_op_is_dropped_like_xlings_drops{"SubosInfo/UnknownOpIsDroppedLikeXlingsDrops", [] { Tmp t; t.write(R"({"subos_info":{"schema_version":1,"runtime":"glibc@2.39","envs":{ "a@1":[{"var":"V","op":"append","value":"/nope"}, {"var":"W","op":"prepend","value":"/yes"}, {"var":"","op":"prepend","value":"/no-name"}]}}})"); auto env = su::resolve_env(su::read(t.dir), t.dir); - ASSERT_EQ(env.size(), 1u); - EXPECT_EQ(env[0].first, "W"); - EXPECT_EQ(env[0].second, "/yes"); -} + expect(fatal(eq(env.size(), 1u))); + expect(eq(env[0].first, "W")); + expect(eq(env[0].second, "/yes")); +}}; @@ -338,7 +336,7 @@ TEST(SubosInfo, UnknownOpIsDroppedLikeXlingsDrops) { // `prepend` prepends TO the caller's value rather than replacing it. Same // reason: the pair replaces the variable, so emitting the declared value alone // discards whatever search path the user had. -TEST(SubosResolveEnv, PrependKeepsTheExportedValue) { +UTest prepend_keeps_the_exported_value{"SubosResolveEnv/PrependKeepsTheExportedValue", [] { Tmp t; t.write(R"({"workspace":{},"subos_info":{"schema_version":1,"runtime":"glibc@2.39", "envs":{"glibc@2.39":[{"var":"LD_LIBRARY_PATH","op":"prepend","value":"/sub/lib"}]}}})"); @@ -348,13 +346,13 @@ TEST(SubosResolveEnv, PrependKeepsTheExportedValue) { if (v == "LD_LIBRARY_PATH") return std::string("/user/lib"); return std::nullopt; }); - ASSERT_EQ(out.size(), 1u); + expect(fatal(eq(out.size(), 1u))); const auto sep = mcpp::runtime::launch_env::path_list_separator(); - EXPECT_EQ(out[0].second, std::string("/sub/lib") + sep + "/user/lib"); -} + expect(eq(out[0].second, std::string("/sub/lib") + sep + "/user/lib")); +}}; // Already there: prepending again would grow the list on every nested run. -TEST(SubosResolveEnv, PrependIsIdempotentAgainstTheExportedValue) { +UTest prepend_is_idempotent_against_the_exported_value{"SubosResolveEnv/PrependIsIdempotentAgainstTheExportedValue", [] { Tmp t; t.write(R"({"workspace":{},"subos_info":{"schema_version":1,"runtime":"glibc@2.39", "envs":{"glibc@2.39":[{"var":"LD_LIBRARY_PATH","op":"prepend","value":"/sub/lib"}]}}})"); @@ -366,14 +364,14 @@ TEST(SubosResolveEnv, PrependIsIdempotentAgainstTheExportedValue) { if (v == "LD_LIBRARY_PATH") return existing; return std::nullopt; }); - ASSERT_EQ(out.size(), 1u); - EXPECT_EQ(out[0].second, existing); -} + expect(fatal(eq(out.size(), 1u))); + expect(eq(out[0].second, existing)); +}}; // xlings defines `set` as set-if-absent. mcpp must use the same presence // boundary or the exact same SubOS changes behaviour depending on which tool // launches the program. -TEST(SubosResolveEnv, SetYieldsToAnExportedValue) { +UTest set_yields_to_an_exported_value{"SubosResolveEnv/SetYieldsToAnExportedValue", [] { Tmp t; t.write(R"({"workspace":{},"subos_info":{"schema_version":1, "runtime":"glibc@2.39", @@ -384,8 +382,8 @@ TEST(SubosResolveEnv, SetYieldsToAnExportedValue) { if (v == "GALLIUM_DRIVER") return std::string("llvmpipe"); return std::nullopt; }); - ASSERT_EQ(out.size(), 1u); - EXPECT_EQ(out[0].second, "llvmpipe"); -} + expect(fatal(eq(out.size(), 1u))); + expect(eq(out[0].second, "llvmpipe")); +}}; } // namespace diff --git a/tests/unit/test_subsystem_contracts.cpp b/tests/unit/test_subsystem_contracts.cpp index bf7b208e6..950cd7bb4 100644 --- a/tests/unit/test_subsystem_contracts.cpp +++ b/tests/unit/test_subsystem_contracts.cpp @@ -1,14 +1,16 @@ -#include #include #include #include import std; +import mcpplibs.mtest; import mcpp.version; import mcpp.build.program_protocol; import mcpp.build.hostprogram; +using namespace mcpplibs::mtest; + // GLOBAL CROSS-CHECK. // // The packages under `modules/` carry their own tests, which see only the @@ -30,7 +32,7 @@ import mcpp.build.hostprogram; namespace pp = mcpp::build::program_protocol; -TEST(SubsystemContracts, TheAnnouncedProtocolIsSubstitutedAndNotHardcoded) { +UTest the_announced_protocol_is_substituted_and_not_hardcoded{"SubsystemContracts/TheAnnouncedProtocolIsSubstitutedAndNotHardcoded", [] { // The bundled `mcpp` module announces `mcpp:protocol=N` so the engine can // reject a program written for a newer protocol. N is injected by // substituting `@PROTOCOL@` at compile time, PRECISELY so the announced @@ -40,28 +42,28 @@ TEST(SubsystemContracts, TheAnnouncedProtocolIsSubstitutedAndNotHardcoded) { // subsystem's own tests stay green while a bumped protocol silently keeps // announcing the old one. const std::string src{mcpp::build::kMcppModuleSource}; - EXPECT_NE(src.find("@PROTOCOL@"), std::string::npos) + expect(ne(src.find("@PROTOCOL@"), std::string::npos)) << "the bundled module no longer carries the substitution point"; const std::string hardcoded = "mcpp:protocol=" + std::to_string(pp::kProtocolVersion); - EXPECT_EQ(src.find(hardcoded), std::string::npos) + expect(eq(src.find(hardcoded), std::string::npos)) << "the protocol number is spelled into the bundled module; " "substitute @PROTOCOL@ instead so the two cannot diverge"; -} +}}; -TEST(SubsystemContracts, TheModuleDeclarationIsAlsoSubstituted) { +UTest the_module_declaration_is_also_substituted{"SubsystemContracts/TheModuleDeclarationIsAlsoSubstituted", [] { // `export module mcpp;` inside a string literal would be read by mcpp's // own regex scanner as this file exporting a second module. The engine // writes `@MODULE@` and substitutes it when it emits the source. const std::string src{mcpp::build::kMcppModuleSource}; - EXPECT_NE(src.find("@MODULE@"), std::string::npos); - EXPECT_EQ(src.find("export module mcpp;"), std::string::npos) + expect(ne(src.find("@MODULE@"), std::string::npos)); + expect(eq(src.find("export module mcpp;"), std::string::npos)) << "a literal module declaration here makes the scanner believe this " "translation unit exports it"; -} +}}; -TEST(SubsystemContracts, TheBinaryVersionMatchesTheRootManifest) { +UTest the_binary_version_matches_the_root_manifest{"SubsystemContracts/TheBinaryVersionMatchesTheRootManifest", [] { // Two files, one fact. `.github/tools/check_version_pins.sh` enforces this // as text before anything is built; this asserts it on the value the // COMPILED binary carries, which is what `--version`, the BMI fingerprint @@ -73,11 +75,11 @@ TEST(SubsystemContracts, TheBinaryVersionMatchesTheRootManifest) { fs::path root = fs::current_path(); for (int i = 0; i < 6 && !fs::exists(root / "mcpp.toml"); ++i) root = root.parent_path(); - ASSERT_TRUE(fs::exists(root / "mcpp.toml")) + expect(fatal(fs::exists(root / "mcpp.toml"))) << "could not find the root manifest from " << fs::current_path(); std::ifstream in(root / "mcpp.toml"); - ASSERT_TRUE(in.is_open()); + expect(fatal(in.is_open())); std::string line, fromToml; while (std::getline(in, line)) { if (!line.starts_with("version")) continue; @@ -87,6 +89,6 @@ TEST(SubsystemContracts, TheBinaryVersionMatchesTheRootManifest) { fromToml = line.substr(a + 1, b - a - 1); break; } - ASSERT_FALSE(fromToml.empty()) << "no [package] version in the root manifest"; - EXPECT_EQ(fromToml, std::string(mcpp::MCPP_VERSION)); -} + if (auto&& failed = expect(fatal(!fromToml.empty())); !failed) failed << "no [package] version in the root manifest"; + expect(eq(fromToml, std::string(mcpp::MCPP_VERSION))); +}}; diff --git a/tests/unit/test_symbol_provision.cpp b/tests/unit/test_symbol_provision.cpp index f31f44832..e0d09ead7 100644 --- a/tests/unit/test_symbol_provision.cpp +++ b/tests/unit/test_symbol_provision.cpp @@ -11,12 +11,14 @@ // arrangement, where a shared dependency's static dependency legitimately // lands in the consumer's executable with exactly one copy in the process. -#include import std; +import mcpplibs.mtest; import mcpp.build.symbol_provision; import mcpp.runtime.elf; +using namespace mcpplibs::mtest; + namespace sp = mcpp::build::symbol_provision; namespace elf = mcpp::runtime::elf; @@ -47,27 +49,27 @@ std::vector names(const std::vector& exports) { // ── stage one ────────────────────────────────────────────────────────────── -TEST(SymbolProvision, ADefinedFunctionIsAlwaysAnExport) { +UTest a_defined_function_is_always_an_export{"SymbolProvision/ADefinedFunctionIsAlwaysAnExport", [] { auto s = image(); s.defined.push_back(func("inflate")); // Even at an address that carries a copy relocation: a copy relocation // moves DATA, so a function sharing that address is not one. s.copyRelocations.insert(0x1000); auto exports = sp::exported_definitions(s); - ASSERT_TRUE(exports.has_value()); - EXPECT_EQ(names(*exports), std::vector{"inflate"}); -} + expect(fatal(exports.has_value())); + expect(eq(names(*exports), std::vector{"inflate"})); +}}; -TEST(SymbolProvision, CopyRelocatedDataIsNotAnExport) { +UTest copy_relocated_data_is_not_an_export{"SymbolProvision/CopyRelocatedDataIsNotAnExport", [] { auto s = image(); s.defined.push_back(object("stdout", 0xb1d888)); s.copyRelocations.insert(0xb1d888); auto exports = sp::exported_definitions(s); - ASSERT_TRUE(exports.has_value()); - EXPECT_TRUE(exports->empty()); -} + expect(fatal(exports.has_value())); + expect(exports->empty()); +}}; -TEST(SymbolProvision, CopyRelocationsMatchByAddressNotByName) { +UTest copy_relocations_match_by_address_not_by_name{"SymbolProvision/CopyRelocationsMatchByAddressNotByName", [] { // MEASURED, on every mcpp binary: glibc's `environ` is a WEAK alias of // `__environ` at one address, and only `__environ` appears in `.rela.dyn`. // A name-keyed filter reports `environ` as a hijacked symbol in every @@ -77,35 +79,34 @@ TEST(SymbolProvision, CopyRelocationsMatchByAddressNotByName) { s.defined.push_back(object("environ", 0xb1d840)); // same address s.copyRelocations.insert(0xb1d840); // only one entry auto exports = sp::exported_definitions(s); - ASSERT_TRUE(exports.has_value()); - EXPECT_TRUE(exports->empty()) << "environ must not be reported"; -} + expect(fatal(exports.has_value())); + expect(exports->empty()) << "environ must not be reported"; +}}; -TEST(SymbolProvision, AnUninitialisedDataSymbolWithNoCopyRelocationIsAnExport) { +UTest an_uninitialised_data_symbol_with_no_copy_relocation_is_an_export{"SymbolProvision/AnUninitialisedDataSymbolWithNoCopyRelocationIsAnExport", [] { // /usr/bin/ls's `obstack_alloc_failed_handler` — a function-pointer // variable that gnulib defines and glibc also defines. It lives in .bss // like a copy relocation would, and it is NOT one. auto s = image(); s.defined.push_back(object("obstack_alloc_failed_handler", 0x2000)); auto exports = sp::exported_definitions(s); - ASSERT_TRUE(exports.has_value()); - EXPECT_EQ(names(*exports), - std::vector{"obstack_alloc_failed_handler"}); -} + expect(fatal(exports.has_value())); + expect(eq(names(*exports), std::vector{"obstack_alloc_failed_handler"})); +}}; -TEST(SymbolProvision, AnUnknownMachineDeclinesRatherThanGuessing) { +UTest an_unknown_machine_declines_rather_than_guessing{"SymbolProvision/AnUnknownMachineDeclinesRatherThanGuessing", [] { // Without the machine's COPY relocation type every data symbol looks like // an export. "Could not evaluate" must not be spelled the same way as // "evaluated and clean". auto s = image(); s.copyRelocationsKnown = false; s.defined.push_back(object("stdout", 0x10)); - EXPECT_FALSE(sp::exported_definitions(s).has_value()); -} + expect(!(sp::exported_definitions(s).has_value())); +}}; // ── stage two ────────────────────────────────────────────────────────────── -TEST(SymbolProvision, AnExportWithNoSecondProviderIsNotAConflict) { +UTest an_export_with_no_second_provider_is_not_a_conflict{"SymbolProvision/AnExportWithNoSecondProviderIsNotAConflict", [] { // mcpp's OWN arrangement. A `kind = "shared"` dependency's link unit // takes only its own objects, so its static dependency lands in the // consumer's executable and the shared library binds back to it. One copy @@ -115,10 +116,10 @@ TEST(SymbolProvision, AnExportWithNoSecondProviderIsNotAConflict) { std::vector closure{ sp::Provider{"/lib/libc.so.6", {"printf", "malloc"}}, }; - EXPECT_TRUE(sp::conflicting_exports(exports, closure).empty()); -} + expect(sp::conflicting_exports(exports, closure).empty()); +}}; -TEST(SymbolProvision, AnExportWithASecondProviderIsAConflictAndNamesIt) { +UTest an_export_with_a_second_provider_is_a_conflict_and_names_it{"SymbolProvision/AnExportWithASecondProviderIsAConflictAndNamesIt", [] { std::vector exports{ sp::Export{"inflate", true}, sp::Export{"deflate", true} }; std::vector closure{ @@ -126,13 +127,13 @@ TEST(SymbolProvision, AnExportWithASecondProviderIsAConflictAndNamesIt) { sp::Provider{"/lib/libc.so.6", {"printf"}}, }; auto conflicts = sp::conflicting_exports(exports, closure); - ASSERT_EQ(conflicts.size(), 2u); - EXPECT_EQ(conflicts[0].name, "inflate"); - ASSERT_EQ(conflicts[0].alsoProvidedBy.size(), 1u); - EXPECT_EQ(conflicts[0].alsoProvidedBy[0], "/pkg/lib/libz.so.1"); -} + expect(fatal(eq(conflicts.size(), 2u))); + expect(eq(conflicts[0].name, "inflate")); + expect(fatal(eq(conflicts[0].alsoProvidedBy.size(), 1u))); + expect(eq(conflicts[0].alsoProvidedBy[0], "/pkg/lib/libz.so.1")); +}}; -TEST(SymbolProvision, TheReportNamesEveryProviderAndCapsTheSymbolList) { +UTest the_report_names_every_provider_and_caps_the_symbol_list{"SymbolProvision/TheReportNamesEveryProviderAndCapsTheSymbolList", [] { sp::Report report; report.status = sp::Status::Conflict; report.total = 217; @@ -148,63 +149,62 @@ TEST(SymbolProvision, TheReportNamesEveryProviderAndCapsTheSymbolList) { report.exported = report.conflicts.size(); auto text = report.explain("consumer"); - EXPECT_NE(text.find("20 symbols"), std::string::npos); - EXPECT_NE(text.find("and 14 more"), std::string::npos); - EXPECT_NE(text.find("/pkg/lib/libz.so.1"), std::string::npos); + expect(ne(text.find("20 symbols"), std::string::npos)); + expect(ne(text.find("and 14 more"), std::string::npos)); + expect(ne(text.find("/pkg/lib/libz.so.1"), std::string::npos)); // The three ways out are the point of the message, and their ORDER is // load-bearing: switching the form removes this finding while leaving two // copies loaded unless the SONAMEs also match, so it must not be first. auto stop = text.find("stop one side"); auto soname = text.find("SONAME"); auto form = text.find("dependency_linkage"); - ASSERT_NE(stop, std::string::npos); - ASSERT_NE(soname, std::string::npos); - ASSERT_NE(form, std::string::npos); - EXPECT_LT(stop, soname); - EXPECT_LT(soname, form); -} + expect(fatal(ne(stop, std::string::npos))); + expect(fatal(ne(soname, std::string::npos))); + expect(fatal(ne(form, std::string::npos))); + expect(lt(stop, soname)); + expect(lt(soname, form)); +}}; -TEST(SymbolProvision, OnlyAConflictIsActionable) { - EXPECT_FALSE(sp::not_applicable("static").actionable()); - EXPECT_FALSE(sp::not_evaluated("unknown machine").actionable()); +UTest only_a_conflict_is_actionable{"SymbolProvision/OnlyAConflictIsActionable", [] { + expect(!(sp::not_applicable("static").actionable())); + expect(!(sp::not_evaluated("unknown machine").actionable())); sp::Report clean; clean.status = sp::Status::Clean; - EXPECT_FALSE(clean.actionable()); - EXPECT_TRUE(clean.explain("x").empty()); -} + expect(!clean.actionable()); + expect(clean.explain("x").empty()); +}}; -TEST(SymbolProvision, ANonAnswerCarriesItsReason) { +UTest a_non_answer_carries_its_reason{"SymbolProvision/ANonAnswerCarriesItsReason", [] { // "Not checked" and "checked and clean" must never render the same. auto na = sp::not_applicable("statically linked"); - EXPECT_EQ(na.status, sp::Status::NotApplicable); - EXPECT_EQ(na.reason, "statically linked"); - EXPECT_EQ(sp::to_string(sp::Status::NotApplicable), "not-applicable"); - EXPECT_EQ(sp::to_string(sp::Status::NotEvaluated), "not-evaluated"); - EXPECT_NE(sp::to_string(sp::Status::Clean), - sp::to_string(sp::Status::NotApplicable)); -} + expect(eq(na.status, sp::Status::NotApplicable)); + expect(eq(na.reason, "statically linked")); + expect(eq(sp::to_string(sp::Status::NotApplicable), "not-applicable")); + expect(eq(sp::to_string(sp::Status::NotEvaluated), "not-evaluated")); + expect(ne(sp::to_string(sp::Status::Clean), sp::to_string(sp::Status::NotApplicable))); +}}; // ── the precondition ─────────────────────────────────────────────────────── -TEST(SymbolProvision, AnAuthorRequestedExportSurfaceVoidsThePredicate) { +UTest an_author_requested_export_surface_voids_the_predicate{"SymbolProvision/AnAuthorRequestedExportSurfaceVoidsThePredicate", [] { // /usr/bin/bash exports 2339 symbols on purpose, for loadable builtins. // Detecting exactly the thing the author asked for is not a finding. - EXPECT_TRUE(sp::export_dynamic_requested( - std::vector{"-O2", "-rdynamic"})); - EXPECT_TRUE(sp::export_dynamic_requested( - std::vector{"-Wl,--export-dynamic"})); - EXPECT_TRUE(sp::export_dynamic_requested(std::vector{"-Wl,-E"})); - EXPECT_TRUE(sp::export_dynamic_requested( - std::vector{"-Wl,--dynamic-list=syms.txt"})); - EXPECT_TRUE(sp::export_dynamic_requested( - std::vector{"-Wl,--export-dynamic-symbol=foo"})); -} + expect(bool(sp::export_dynamic_requested( + std::vector{"-O2", "-rdynamic"}))); + expect(bool(sp::export_dynamic_requested( + std::vector{"-Wl,--export-dynamic"}))); + expect(bool(sp::export_dynamic_requested(std::vector{"-Wl,-E"}))); + expect(bool(sp::export_dynamic_requested( + std::vector{"-Wl,--dynamic-list=syms.txt"}))); + expect(bool(sp::export_dynamic_requested( + std::vector{"-Wl,--export-dynamic-symbol=foo"}))); +}}; -TEST(SymbolProvision, OrdinaryLinkFlagsDoNotVoidThePredicate) { +UTest ordinary_link_flags_do_not_void_the_predicate{"SymbolProvision/OrdinaryLinkFlagsDoNotVoidThePredicate", [] { // A substring sweep would fire on any of these. - EXPECT_FALSE(sp::export_dynamic_requested(std::vector{ + expect(!sp::export_dynamic_requested(std::vector{ "-O2", "-Wl,-rpath,$ORIGIN", "-lz", "-Wl,--as-needed", "-Wl,--enable-new-dtags", "-static-libstdc++", "-shared"})); -} +}}; // ── vague linkage is not a second provider ───────────────────────────────── // @@ -219,21 +219,21 @@ TEST(SymbolProvision, OrdinaryLinkFlagsDoNotVoidThePredicate) { // island's own `extern "C"` entry points, which libsycl does not define. A // check that could not tell binding from name reported all of them. -TEST(SymbolProvision, AWeakDefinitionIsCarriedThroughAsWeak) { +UTest a_weak_definition_is_carried_through_as_weak{"SymbolProvision/AWeakDefinitionIsCarriedThroughAsWeak", [] { auto s = image(); auto weak = func("_ZN4sycl3_V15queueD2Ev"); weak.isWeak = true; s.defined.push_back(weak); s.defined.push_back(func("saxpy_device")); auto exports = sp::exported_definitions(s); - ASSERT_TRUE(exports.has_value()); - ASSERT_EQ(exports->size(), 2u); + expect(fatal(exports.has_value())); + expect(fatal(eq(exports->size(), 2u))); // Sorted by name: the mangled one first. - EXPECT_TRUE((*exports)[0].isWeak); - EXPECT_FALSE((*exports)[1].isWeak); -} + expect(bool((*exports)[0].isWeak)); + expect(!((*exports)[1].isWeak)); +}}; -TEST(SymbolProvision, AConflictRemembersWhetherItsDefinitionIsWeak) { +UTest a_conflict_remembers_whether_its_definition_is_weak{"SymbolProvision/AConflictRemembersWhetherItsDefinitionIsWeak", [] { std::vector exports{ { .name = "_ZN4sycl3_V15queueD2Ev", .isFunc = true, .isWeak = true }, { .name = "inflate", .isFunc = true, .isWeak = false }, @@ -243,29 +243,29 @@ TEST(SymbolProvision, AConflictRemembersWhetherItsDefinitionIsWeak) { .defines = {"_ZN4sycl3_V15queueD2Ev", "inflate"} }, }; auto conflicts = sp::conflicting_exports(exports, closure); - ASSERT_EQ(conflicts.size(), 2u); + expect(fatal(eq(conflicts.size(), 2u))); // Both are shared; only the binding separates them, and the caller is what // decides which one is a finding. Asserted here rather than in the caller // so the DATA carries the distinction even if a future caller forgets it. - EXPECT_TRUE(conflicts[0].isWeak); - EXPECT_FALSE(conflicts[1].isWeak); -} + expect(bool(conflicts[0].isWeak)); + expect(!(conflicts[1].isWeak)); +}}; // #646 F3. GCC emits the static data of an inline entity with STB_GNU_UNIQUE // (10) so that the loader keeps one copy across RTLD_LOCAL. It is vague linkage // exactly as STB_WEAK is; reading only the latter reported seven libstdc++ // objects as a program displacing the library. -TEST(SymbolProvision, TheUniqueBindingIsVagueLinkageAsTheWeakOneIs) { - EXPECT_TRUE(elf::is_vague_linkage_binding(2)); // STB_WEAK - EXPECT_TRUE(elf::is_vague_linkage_binding(10)); // STB_GNU_UNIQUE - EXPECT_FALSE(elf::is_vague_linkage_binding(1)); // STB_GLOBAL - EXPECT_FALSE(elf::is_vague_linkage_binding(0)); // STB_LOCAL -} +UTest the_unique_binding_is_vague_linkage_as_the_weak_one_is{"SymbolProvision/TheUniqueBindingIsVagueLinkageAsTheWeakOneIs", [] { + expect(elf::is_vague_linkage_binding(2)); // STB_WEAK + expect(elf::is_vague_linkage_binding(10)); // STB_GNU_UNIQUE + expect(!elf::is_vague_linkage_binding(1)); // STB_GLOBAL + expect(!elf::is_vague_linkage_binding(0)); // STB_LOCAL +}}; // #646 F3. The std module's initialiser is linked into every C++ image that // imports `std`, from one object. A provider that defines a name only through // an object this build linked into both images is removed from that conflict. -TEST(SymbolProvision, ADefinitionBothImagesTakeFromOneObjectIsNotAConflict) { +UTest a_definition_both_images_take_from_one_object_is_not_a_conflict{"SymbolProvision/ADefinitionBothImagesTakeFromOneObjectIsNotAConflict", [] { std::vector conflicts{ { .name = "_ZGIW3std", .isFunc = true, .alsoProvidedBy = {"bin/liblib.so"} }, @@ -275,21 +275,20 @@ TEST(SymbolProvision, ADefinitionBothImagesTakeFromOneObjectIsNotAConflict) { const std::map> shared{ { "bin/liblib.so", {"_ZGIW3std"} }, }; - EXPECT_EQ(sp::drop_shared_plan_definitions(conflicts, shared), 1u); - ASSERT_EQ(conflicts.size(), 1u); - EXPECT_EQ(conflicts[0].name, "inflate"); + expect(eq(sp::drop_shared_plan_definitions(conflicts, shared), 1u)); + expect(fatal(eq(conflicts.size(), 1u))); + expect(eq(conflicts[0].name, "inflate")); // The library still defines `inflate` from its OWN object, so it stays a // provider: attribution is by object, never by the library as a whole. - EXPECT_EQ(conflicts[0].alsoProvidedBy, - (std::vector{"bin/liblib.so", "/usr/lib/libz.so.1"})); -} + expect(eq(conflicts[0].alsoProvidedBy, (std::vector{"bin/liblib.so", "/usr/lib/libz.so.1"}))); +}}; // The rule is provenance, not a name pattern: an initialiser-shaped name that // no shared object defines is still a finding. -TEST(SymbolProvision, AnInitialiserShapedNameWithoutSharedProvenanceIsStillReported) { +UTest an_initialiser_shaped_name_without_shared_provenance_is_still_reported{"SymbolProvision/AnInitialiserShapedNameWithoutSharedProvenanceIsStillReported", [] { std::vector conflicts{ { .name = "_ZGIW5other", .isFunc = true, .alsoProvidedBy = {"bin/libother.so"} }, }; - EXPECT_EQ(sp::drop_shared_plan_definitions(conflicts, {}), 0u); - EXPECT_EQ(conflicts.size(), 1u); -} + expect(eq(sp::drop_shared_plan_definitions(conflicts, {}), 0u)); + expect(eq(conflicts.size(), 1u)); +}}; diff --git a/tests/unit/test_sysroot_ownership.cpp b/tests/unit/test_sysroot_ownership.cpp index 56e1f6be3..dd1a40cb0 100644 --- a/tests/unit/test_sysroot_ownership.cpp +++ b/tests/unit/test_sysroot_ownership.cpp @@ -10,11 +10,13 @@ // These assertions pin the axis that replaced it. Nothing here touches the // filesystem, deliberately: the point is that existence is not consulted. -#include import std; +import mcpplibs.mtest; import mcpp.fallback.probe_sysroot; +using namespace mcpplibs::mtest; + namespace fb = mcpp::fallback; namespace { @@ -22,66 +24,66 @@ namespace { const std::filesystem::path kRegistry = "/home/u/.mcpp/registry"; const std::filesystem::path kProject = "/home/u/work/thisrepo"; -TEST(SysrootOwnership, PayloadSysrootIsOwned) { - EXPECT_FALSE(fb::sysroot_is_foreign( +UTest payload_sysroot_is_owned{"SysrootOwnership/PayloadSysrootIsOwned", [] { + expect(!fb::sysroot_is_foreign( kRegistry / "subos" / "default", kRegistry, kProject)); -} +}}; -TEST(SysrootOwnership, ProjectLocalSysrootIsOwned) { - EXPECT_FALSE(fb::sysroot_is_foreign( +UTest project_local_sysroot_is_owned{"SysrootOwnership/ProjectLocalSysrootIsOwned", [] { + expect(!fb::sysroot_is_foreign( kProject / ".xlings" / "subos" / "default", kRegistry, kProject)); -} +}}; // The case that motivated the predicate: a path that exists, is spelled // exactly like a legitimate one, and belongs to someone else. -TEST(SysrootOwnership, AnotherCheckoutIsForeign) { - EXPECT_TRUE(fb::sysroot_is_foreign( +UTest another_checkout_is_foreign{"SysrootOwnership/AnotherCheckoutIsForeign", [] { + expect(fb::sysroot_is_foreign( "/home/u/work/otherrepo/.xlings/subos/default", kRegistry, kProject)); -} +}}; -TEST(SysrootOwnership, AnotherHomeIsForeign) { - EXPECT_TRUE(fb::sysroot_is_foreign( +UTest another_home_is_foreign{"SysrootOwnership/AnotherHomeIsForeign", [] { + expect(fb::sysroot_is_foreign( "/home/other/.mcpp/registry/subos/default", kRegistry, kProject)); -} +}}; // Both anchors are load-bearing. Registry alone would condemn every // project-local tree; project alone would condemn every payload sysroot. -TEST(SysrootOwnership, EitherAnchorSuffices) { - EXPECT_FALSE(fb::sysroot_is_foreign( +UTest either_anchor_suffices{"SysrootOwnership/EitherAnchorSuffices", [] { + expect(!fb::sysroot_is_foreign( kRegistry / "subos" / "default", kRegistry, {})); - EXPECT_FALSE(fb::sysroot_is_foreign( + expect(!fb::sysroot_is_foreign( kProject / ".xlings" / "subos" / "default", {}, kProject)); -} +}}; // No sysroot is not a foreign sysroot. gcc payloads on this machine report an // empty `-print-sysroot`, so this is the ordinary case, not an edge one. -TEST(SysrootOwnership, EmptyIsNotForeign) { - EXPECT_FALSE(fb::sysroot_is_foreign({}, kRegistry, kProject)); -} +UTest empty_is_not_foreign{"SysrootOwnership/EmptyIsNotForeign", [] { + expect(!fb::sysroot_is_foreign({}, kRegistry, kProject)); +}}; // A prefix that merely shares characters is not containment. -TEST(SysrootOwnership, SiblingPrefixIsNotContainment) { - EXPECT_TRUE(fb::sysroot_is_foreign( +UTest sibling_prefix_is_not_containment{"SysrootOwnership/SiblingPrefixIsNotContainment", [] { + expect(fb::sysroot_is_foreign( "/home/u/work/thisrepo-backup/.xlings/subos/default", kRegistry, kProject)); -} +}}; // A directory whose NAME begins with two dots is not an escape. The first // version compared the relative path's text (`rfind("..", 0)`), which called // `/home/u/.mcpp/registry/..cache` an escape from the registry -- and did not // compile at all on Windows, where `native()` is a wstring. Containment is a // question about path components, so it is asked of components. -TEST(SysrootOwnership, DotDotPrefixedNameIsNotAnEscape) { - EXPECT_FALSE(fb::sysroot_is_foreign( +UTest dot_dot_prefixed_name_is_not_an_escape{"SysrootOwnership/DotDotPrefixedNameIsNotAnEscape", [] { + expect(!fb::sysroot_is_foreign( kRegistry / "..cache" / "subos" / "default", kRegistry, kProject)); -} - -TEST(SysrootOwnership, PathIsUnderIsDirectlyTestable) { - EXPECT_TRUE(fb::path_is_under(kRegistry / "a" / "b", kRegistry)); - EXPECT_TRUE(fb::path_is_under(kRegistry, kRegistry)); - EXPECT_FALSE(fb::path_is_under(kRegistry.parent_path(), kRegistry)); - EXPECT_FALSE(fb::path_is_under(kRegistry, {})); - EXPECT_FALSE(fb::path_is_under({}, kRegistry)); -} +}}; + +UTest path_is_under_is_directly_testable{"SysrootOwnership/PathIsUnderIsDirectlyTestable", [] { + expect(fb::path_is_under(kRegistry / "a" / "b", kRegistry)); + expect(fb::path_is_under(kRegistry, kRegistry)); + expect(!(fb::path_is_under(kRegistry.parent_path(), kRegistry))); + expect(!fb::path_is_under(kRegistry, {})); + expect(!fb::path_is_under({}, kRegistry)); +}}; } // namespace diff --git a/tests/unit/test_target_base.cpp b/tests/unit/test_target_base.cpp index c2ed0bc1b..5c82a336f 100644 --- a/tests/unit/test_target_base.cpp +++ b/tests/unit/test_target_base.cpp @@ -1,6 +1,6 @@ -#include import std; +import mcpplibs.mtest; import mcpp.toolchain.base; import mcpp.toolchain.base_probe; import mcpp.toolchain.linkmodel; @@ -11,6 +11,8 @@ import mcpp.toolchain.apple_sdk; import mcpp.invocation; import mcpp.build.command_line; +using namespace mcpplibs::mtest; + // The target base (design 2026-10-23, "target base and execution host"): the // string grammar of `sysroot`, which spellings each row takes, the // measurement tables, and what the link model renders for a stated base. @@ -49,148 +51,147 @@ bool has(const std::vector& v, std::string_view s) { } // namespace -TEST(TargetBaseGrammar, EveryForm) { +UTest every_form{"TargetBaseGrammar/EveryForm", [] { auto e = tb::parse_text(""); - ASSERT_TRUE(e); - EXPECT_EQ(e->form, tb::Form::Empty); + expect(fatal(bool(e))); + expect(eq(e->form, tb::Form::Empty)); auto p = tb::parse_text("path:/opt/acme//sysroot"); - ASSERT_TRUE(p); - EXPECT_EQ(p->form, tb::Form::Path); - EXPECT_EQ(p->text, "path:/opt/acme/sysroot"); + expect(fatal(bool(p))); + expect(eq(p->form, tb::Form::Path)); + expect(eq(p->text, "path:/opt/acme/sysroot")); auto k = tb::parse_text("acme:sysroot-rhel8@1.0"); - ASSERT_TRUE(k); - EXPECT_EQ(k->form, tb::Form::Package); + expect(fatal(bool(k))); + expect(eq(k->form, tb::Form::Package)); auto l = tb::parse_text("glibc@system"); - ASSERT_TRUE(l); - EXPECT_EQ(l->form, tb::Form::Locator); - EXPECT_EQ(l->family, "glibc"); - EXPECT_EQ(l->version, "system"); + expect(fatal(bool(l))); + expect(eq(l->form, tb::Form::Locator)); + expect(eq(l->family, "glibc")); + expect(eq(l->version, "system")); // A versioned C library without a namespace is the ecosystem's package // (SPEC-006 §2.3); a versioned SDK is a locator with a version. auto g = tb::parse_text("glibc@2.39"); - ASSERT_TRUE(g); - EXPECT_EQ(g->form, tb::Form::Package); - EXPECT_EQ(g->text, "xim:glibc@2.39"); + expect(fatal(bool(g))); + expect(eq(g->form, tb::Form::Package)); + expect(eq(g->text, "xim:glibc@2.39")); auto i = tb::parse_text("iphoneos@18.5"); - ASSERT_TRUE(i); - EXPECT_EQ(i->form, tb::Form::Locator); - EXPECT_EQ(i->version, "18.5"); -} - -TEST(TargetBaseGrammar, RefusesWhatNamesNothing) { - EXPECT_FALSE(tb::parse_text("sysroot")); - EXPECT_FALSE(tb::parse_text("path:")); - EXPECT_FALSE(tb::parse_text("bogus@1.0")); + expect(fatal(bool(i))); + expect(eq(i->form, tb::Form::Locator)); + expect(eq(i->version, "18.5")); +}}; + +UTest refuses_what_names_nothing{"TargetBaseGrammar/RefusesWhatNamesNothing", [] { + expect(!tb::parse_text("sysroot")); + expect(!tb::parse_text("path:")); + expect(!tb::parse_text("bogus@1.0")); // An Apple SDK has no package; `@system` takes no namespace. - EXPECT_FALSE(tb::parse_text("xim:iphoneos@18.5")); - EXPECT_FALSE(tb::parse_text("xim:glibc@system")); -} + expect(!tb::parse_text("xim:iphoneos@18.5")); + expect(!tb::parse_text("xim:glibc@system")); +}}; -TEST(TargetBaseRows, EachRowTakesItsOwnForms) { +UTest each_row_takes_its_own_forms{"TargetBaseRows/EachRowTakesItsOwnForms", [] { auto linux_gnu = *parse("x86_64-linux-gnu"); - EXPECT_FALSE(tb::refusal(linux_gnu, *tb::parse_text("glibc@system"))); - EXPECT_FALSE(tb::refusal(linux_gnu, *tb::parse_text("path:/x"))); - EXPECT_FALSE(tb::refusal(linux_gnu, *tb::parse_text("xim:glibc@2.39"))); + expect(!(tb::refusal(linux_gnu, *tb::parse_text("glibc@system")))); + expect(!(tb::refusal(linux_gnu, *tb::parse_text("path:/x")))); + expect(!(tb::refusal(linux_gnu, *tb::parse_text("xim:glibc@2.39")))); // A family names a platform, so it must agree with the row. - EXPECT_TRUE(tb::refusal(linux_gnu, *tb::parse_text("musl@system"))); - EXPECT_TRUE(tb::refusal(linux_gnu, *tb::parse_text("iphoneos@system"))); + expect(bool(tb::refusal(linux_gnu, *tb::parse_text("musl@system")))); + expect(bool(tb::refusal(linux_gnu, *tb::parse_text("iphoneos@system")))); auto sim = *parse("aarch64-ios-sim"); - EXPECT_FALSE(tb::refusal(sim, *tb::parse_text("iphonesimulator@system"))); - EXPECT_FALSE(tb::refusal(sim, *tb::parse_text("path:/sdk"))); - EXPECT_TRUE(tb::refusal(sim, *tb::parse_text("iphoneos@system"))); + expect(!(tb::refusal(sim, *tb::parse_text("iphonesimulator@system")))); + expect(!(tb::refusal(sim, *tb::parse_text("path:/sdk")))); + expect(bool(tb::refusal(sim, *tb::parse_text("iphoneos@system")))); // The spellings the row took before it took a locator keep their meaning. - EXPECT_FALSE(tb::refusal(sim, *tb::parse_text(""))); + expect(!(tb::refusal(sim, *tb::parse_text("")))); auto msvc = *parse("x86_64-windows-msvc"); - EXPECT_FALSE(tb::refusal(msvc, *tb::parse_text("msvc@system"))); - EXPECT_FALSE(tb::refusal(msvc, *tb::parse_text("xim:msvc@14.44"))); - EXPECT_TRUE(tb::refusal(msvc, *tb::parse_text("acme:vs@1"))); + expect(!(tb::refusal(msvc, *tb::parse_text("msvc@system")))); + expect(!(tb::refusal(msvc, *tb::parse_text("xim:msvc@14.44")))); + expect(bool(tb::refusal(msvc, *tb::parse_text("acme:vs@1")))); // The MSVC spelling on a MinGW row names another platform's toolset. - EXPECT_TRUE(tb::refusal(*parse("x86_64-windows-gnu"), *tb::parse_text("msvc@14.44.35207"))); + expect(bool(tb::refusal(*parse("x86_64-windows-gnu"), *tb::parse_text("msvc@14.44.35207")))); // An SDK toolchain's row carries its own system: no new form. auto android = *parse("aarch64-linux-android"); - EXPECT_TRUE(tb::refusal(android, *tb::parse_text("path:/ndk"))); - EXPECT_EQ(tb::row_base(android).address, tb::Address::DriverInternal); + expect(bool(tb::refusal(android, *tb::parse_text("path:/ndk")))); + expect(eq(tb::row_base(android).address, tb::Address::DriverInternal)); - EXPECT_EQ(tb::row_base(*parse("riscv64-none-elf")).address, tb::Address::Multilib); - EXPECT_EQ(tb::row_base(*parse("aarch64-macos")).address, tb::Address::AppleIsysroot); -} + expect(eq(tb::row_base(*parse("riscv64-none-elf")).address, tb::Address::Multilib)); + expect(eq(tb::row_base(*parse("aarch64-macos")).address, tb::Address::AppleIsysroot)); +}}; -TEST(TargetBaseRows, PathSlashIsTheHostLocator) { +UTest path_slash_is_the_host_locator{"TargetBaseRows/PathSlashIsTheHostLocator", [] { auto s = tb::normalise(*parse("x86_64-linux-gnu"), *tb::parse_text("path:/")); - EXPECT_EQ(s.form, tb::Form::Locator); - EXPECT_EQ(s.text, "glibc@system"); + expect(eq(s.form, tb::Form::Locator)); + expect(eq(s.text, "glibc@system")); auto m = tb::normalise(*parse("x86_64-linux-musl"), *tb::parse_text("path:/")); - EXPECT_EQ(m.text, "musl@system"); -} + expect(eq(m.text, "musl@system")); +}}; -TEST(TargetBaseMeasure, ImplementationFromThePreprocessor) { +UTest implementation_from_the_preprocessor{"TargetBaseMeasure/ImplementationFromThePreprocessor", [] { auto glibc = tb::classify_macros(tb::parse_defines( "#define __GLIBC__ 2\n#define __GLIBC_MINOR__ 31\n#define FOO(x) x\n")); - ASSERT_TRUE(glibc); - EXPECT_EQ(glibc->impl, "glibc"); - EXPECT_EQ(glibc->version, "2.31"); + expect(fatal(bool(glibc))); + expect(eq(glibc->impl, "glibc")); + expect(eq(glibc->version, "2.31")); auto pico = tb::classify_macros(tb::parse_defines( "#define __PICOLIBC__ 1\n#define __PICOLIBC_VERSION__ \"1.8.12\"\n")); - ASSERT_TRUE(pico); - EXPECT_EQ(pico->version, "1.8.12"); + expect(fatal(bool(pico))); + expect(eq(pico->version, "1.8.12")); auto bionic = tb::classify_macros(tb::parse_defines( "#define __BIONIC__ 1\n#define __ANDROID_API__ 24\n")); - ASSERT_TRUE(bionic); - EXPECT_EQ(bionic->factName, "android.api"); - - EXPECT_FALSE(tb::classify_macros(tb::parse_defines("#define __linux__ 1\n"))); -} - -TEST(TargetBaseMeasure, VersionsCompareNumerically) { - EXPECT_LT(tb::compare_versions("2.9", "2.31"), 0); - EXPECT_GT(tb::compare_versions("2.38", "2.31"), 0); - EXPECT_EQ(tb::compare_versions("2.31", "2.31.0"), 0); - EXPECT_EQ(tb::highest_glibc({"GLIBC_2.2.5", "GLIBC_2.38", "GLIBC_PRIVATE", "GLIBC_2.34"}), - "2.38"); -} - -TEST(TargetBaseMeasure, MultiarchTuplesAreDebiansTable) { - EXPECT_EQ(tb::multiarch_tuple(*parse("x86_64-linux-gnu")), "x86_64-linux-gnu"); - EXPECT_EQ(tb::multiarch_tuple(*parse("aarch64-linux-gnu")), "aarch64-linux-gnu"); - EXPECT_EQ(tb::multiarch_tuple(*parse("x86_64-linux-musl")), "x86_64-linux-musl"); -} - -TEST(TargetBaseMeasure, PayloadLayoutIsNotASysroot) { + expect(fatal(bool(bionic))); + expect(eq(bionic->factName, "android.api")); + + expect(!(tb::classify_macros(tb::parse_defines("#define __linux__ 1\n")))); +}}; + +UTest versions_compare_numerically{"TargetBaseMeasure/VersionsCompareNumerically", [] { + expect(lt(tb::compare_versions("2.9", "2.31"), 0)); + expect(gt(tb::compare_versions("2.38", "2.31"), 0)); + expect(eq(tb::compare_versions("2.31", "2.31.0"), 0)); + expect(eq(tb::highest_glibc({"GLIBC_2.2.5", "GLIBC_2.38", "GLIBC_PRIVATE", "GLIBC_2.34"}), "2.38")); +}}; + +UTest multiarch_tuples_are_debians_table{"TargetBaseMeasure/MultiarchTuplesAreDebiansTable", [] { + expect(eq(tb::multiarch_tuple(*parse("x86_64-linux-gnu")), "x86_64-linux-gnu")); + expect(eq(tb::multiarch_tuple(*parse("aarch64-linux-gnu")), "aarch64-linux-gnu")); + expect(eq(tb::multiarch_tuple(*parse("x86_64-linux-musl")), "x86_64-linux-musl")); +}}; + +UTest payload_layout_is_not_a_sysroot{"TargetBaseMeasure/PayloadLayoutIsNotASysroot", [] { Tmp t; touch(t.path / "pkg" / "include" / "features.h"); touch(t.path / "pkg" / "lib64" / "libc.so.6"); - EXPECT_TRUE(tb::is_payload_layout(t.path / "pkg")); + expect(tb::is_payload_layout(t.path / "pkg")); touch(t.path / "root" / "usr" / "include" / "features.h"); touch(t.path / "root" / "lib64" / "libc.so.6"); - EXPECT_FALSE(tb::is_payload_layout(t.path / "root")); -} + expect(!tb::is_payload_layout(t.path / "root")); +}}; // The identity moves when a file the measurement rested on is rewritten in // place, and only then. -TEST(TargetBaseIdentity, FollowsTheEvidence) { +UTest follows_the_evidence{"TargetBaseIdentity/FollowsTheEvidence", [] { Tmp t; const auto lib = t.path / "lib64" / "libc.so.6"; touch(lib, "one"); const auto a = tb::identity_of("path:/x", t.path, {lib}, "glibc 2.31"); - EXPECT_EQ(a, tb::identity_of("path:/x", t.path, {lib}, "glibc 2.31")); + expect(eq(a, tb::identity_of("path:/x", t.path, {lib}, "glibc 2.31"))); touch(lib, "a longer file"); - EXPECT_NE(a, tb::identity_of("path:/x", t.path, {lib}, "glibc 2.31")); - EXPECT_NE(a, tb::identity_of("path:/y", t.path, {lib}, "glibc 2.31")); -} + expect(ne(a, tb::identity_of("path:/x", t.path, {lib}, "glibc 2.31"))); + expect(ne(a, tb::identity_of("path:/y", t.path, {lib}, "glibc 2.31"))); +}}; // Apple's own convention, on every host: `SDKROOT` names the SDK, its // `SDKSettings.json` its version, its `libSystem.tbd` its slices. -TEST(TargetBaseApple, SdkrootOnEveryHost) { +UTest sdkroot_on_every_host{"TargetBaseApple/SdkrootOnEveryHost", [] { Tmp t; const auto sdk = t.path / "iPhoneSimulator18.5.sdk"; touch(sdk / "SDKSettings.json", R"({"CanonicalName": "iphonesimulator18.5", "Version": "18.5"})"); @@ -199,30 +200,30 @@ TEST(TargetBaseApple, SdkrootOnEveryHost) { "--- !tapi-tbd\ntbd-version: 4\ntargets: [ x86_64-ios-simulator, arm64-ios-simulator ]\n"); mcpp::invocation::ScopedEnv env("SDKROOT", sdk.string()); auto found = tb::locate_apple(*tb::parse_text("iphonesimulator@system"), "iphonesimulator"); - ASSERT_TRUE(found) << found.error(); - EXPECT_EQ(found->root, sdk); - EXPECT_EQ(found->version, "18.5"); - EXPECT_FALSE(tb::locate_apple(*tb::parse_text("iphonesimulator@17.0"), "iphonesimulator")); + if (auto&& failed = expect(fatal(bool(found))); !failed) failed << found.error(); + expect(eq(found->root, sdk)); + expect(eq(found->version, "18.5")); + expect(!(tb::locate_apple(*tb::parse_text("iphonesimulator@17.0"), "iphonesimulator"))); // SDKROOT names one SDK and does not answer for another. (On an Apple // host `xcrun` answers next, and Xcode's iPhoneOS SDK is found there.) if (!mcpplibs::os::is_macos) - EXPECT_FALSE(tb::locate_apple(*tb::parse_text("iphoneos@system"), "iphoneos")); + expect(!(tb::locate_apple(*tb::parse_text("iphoneos@system"), "iphoneos"))); using mcpp::toolchain::apple::Slice; - EXPECT_EQ(mcpp::toolchain::apple::sdk_has_target(sdk, "x86_64-ios-simulator"), Slice::carried); - EXPECT_EQ(mcpp::toolchain::apple::sdk_has_target(sdk, "x86_64-ios"), Slice::absent); -} + expect(eq(mcpp::toolchain::apple::sdk_has_target(sdk, "x86_64-ios-simulator"), Slice::carried)); + expect(eq(mcpp::toolchain::apple::sdk_has_target(sdk, "x86_64-ios"), Slice::absent)); +}}; -TEST(TargetBaseApple, PathMustBeAnSdk) { +UTest path_must_be_an_sdk{"TargetBaseApple/PathMustBeAnSdk", [] { Tmp t; auto none = tb::locate_apple(*tb::parse_text("path:" + (t.path / "nothing").string()), "macosx"); - EXPECT_FALSE(none); + expect(!none); touch(t.path / "empty" / "readme"); - EXPECT_FALSE(tb::locate_apple(*tb::parse_text("path:" + (t.path / "empty").string()), "macosx")); -} + expect(!(tb::locate_apple(*tb::parse_text("path:" + (t.path / "empty").string()), "macosx"))); +}}; // THE LINKER IS GIVEN THE SYSROOT, AND A BINDING ARTIFACT LOADS THE BASE: the // two defects #755's sysroot had (scenarios C and D). -TEST(TargetBaseLinkModel, StatedBindingBaseNamesItsLoaderAndDirs) { +UTest stated_binding_base_names_its_loader_and_dirs{"TargetBaseLinkModel/StatedBindingBaseNamesItsLoaderAndDirs", [] { tc::Toolchain t; t.compiler = tc::CompilerId::GCC; t.targetTriple = "x86_64-linux-gnu"; @@ -234,22 +235,22 @@ TEST(TargetBaseLinkModel, StatedBindingBaseNamesItsLoaderAndDirs) { t.base.loader = "/sr/lib64/ld-linux-x86-64.so.2"; t.base.runtimeDirs = {"/sr/lib64"}; auto lm = tc::resolve_link_model(t); - EXPECT_EQ(lm.mode, tc::CLibMode::Sysroot); + expect(eq(lm.mode, tc::CLibMode::Sysroot)); auto link = lm.link_tokens(ident); - EXPECT_TRUE(has(link, "--sysroot=/sr")); - EXPECT_TRUE(has(link, "-Wl,--sysroot=/sr")); - EXPECT_TRUE(has(link, "-Wl,--dynamic-linker=/sr/lib64/ld-linux-x86-64.so.2")); - EXPECT_TRUE(has(link, "-Wl,-rpath,/sr/lib64")); + expect(bool(has(link, "--sysroot=/sr"))); + expect(bool(has(link, "-Wl,--sysroot=/sr"))); + expect(bool(has(link, "-Wl,--dynamic-linker=/sr/lib64/ld-linux-x86-64.so.2"))); + expect(bool(has(link, "-Wl,-rpath,/sr/lib64"))); // A floor (a cross target) names no loader: the pristine spec's standard // path is the one the target machine has. t.base.runtime = tb::Runtime::Floor; auto floor = tc::resolve_link_model(t).link_tokens(ident); - EXPECT_FALSE(std::ranges::any_of(floor, [](auto const& s) { - return s.starts_with("-Wl,--dynamic-linker"); })); -} + expect(!(std::ranges::any_of(floor, [](auto const& s) { + return s.starts_with("-Wl,--dynamic-linker"); }))); +}}; -TEST(TargetBaseLinkModel, MultiarchForADriverThatDoesNotKnowIt) { +UTest multiarch_for_a_driver_that_does_not_know_it{"TargetBaseLinkModel/MultiarchForADriverThatDoesNotKnowIt", [] { tc::Toolchain t; t.compiler = tc::CompilerId::GCC; t.targetTriple = "x86_64-linux-gnu"; @@ -265,15 +266,15 @@ TEST(TargetBaseLinkModel, MultiarchForADriverThatDoesNotKnowIt) { // operators, so a Windows host writes its own separator. namespace fs = std::filesystem; const auto root = fs::path("/"); - EXPECT_TRUE(has(lm.compile_tokens(ident), - "-idirafter" + (root / "usr" / "include" / "x86_64-linux-gnu").string())); + expect(bool(has(lm.compile_tokens(ident), + "-idirafter" + (root / "usr" / "include" / "x86_64-linux-gnu").string()))); auto link = lm.link_tokens(ident); - EXPECT_TRUE(has(link, "-B" + (root / "usr" / "lib" / "x86_64-linux-gnu").string())); - EXPECT_TRUE(has(link, "-Wl,-rpath-link," + (root / "lib" / "x86_64-linux-gnu").string())); -} - -TEST(PasteArg, QuotesOnlyWhatTheShellWouldSplit) { - EXPECT_EQ(mcpp::build::command_line::paste_arg("/tmp/home"), "/tmp/home"); - EXPECT_NE(mcpp::build::command_line::paste_arg("/tmp/mc probe/home"), "/tmp/mc probe/home"); - EXPECT_NE(mcpp::build::command_line::paste_arg("/tmp/a'b"), "/tmp/a'b"); -} + expect(bool(has(link, "-B" + (root / "usr" / "lib" / "x86_64-linux-gnu").string()))); + expect(bool(has(link, "-Wl,-rpath-link," + (root / "lib" / "x86_64-linux-gnu").string()))); +}}; + +UTest quotes_only_what_the_shell_would_split{"PasteArg/QuotesOnlyWhatTheShellWouldSplit", [] { + expect(eq(mcpp::build::command_line::paste_arg("/tmp/home"), "/tmp/home")); + expect(ne(mcpp::build::command_line::paste_arg("/tmp/mc probe/home"), "/tmp/mc probe/home")); + expect(ne(mcpp::build::command_line::paste_arg("/tmp/a'b"), "/tmp/a'b")); +}}; diff --git a/tests/unit/test_target_row_spelling.cpp b/tests/unit/test_target_row_spelling.cpp index 1df8135a3..ec7b6f49c 100644 --- a/tests/unit/test_target_row_spelling.cpp +++ b/tests/unit/test_target_row_spelling.cpp @@ -1,10 +1,12 @@ -#include import std; +import mcpplibs.mtest; import mcpp.manifest; import mcpp.toolchain.triple; import mcpp.build.prepare_inputs; +using namespace mcpplibs::mtest; + // A `[target.]` row is one row whichever spelling names it. Every // reader of a row asks `find_target_entry`; before 2026.10.5.3 the readers of // `runner` and `min_api_level` compared the key exactly, so a section written @@ -13,7 +15,7 @@ import mcpp.build.prepare_inputs; namespace tr = mcpp::toolchain::triple; -TEST(TargetRowSpelling, EverySpellingOfATripleFindsItsRow) { +UTest every_spelling_of_a_triple_finds_its_row{"TargetRowSpelling/EverySpellingOfATripleFindsItsRow", [] { auto m = mcpp::manifest::parse_string(R"( [package] name = "p" @@ -28,18 +30,18 @@ runner = ["wine64"] [target.i386-windows-msvc] runner = ["i386"] )"); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); auto runner_of = [&](std::string_view triple) -> std::string { auto t = tr::parse(triple); if (!t) return ""; auto* row = mcpp::build::find_target_entry(*m, *t); return row && !row->runner.empty() ? row->runner.front() : ""; }; - EXPECT_EQ(runner_of("i686-windows-msvc"), "wine"); - EXPECT_EQ(runner_of("i686-pc-windows-msvc"), "wine"); - EXPECT_EQ(runner_of("x86-windows-msvc"), "wine"); - EXPECT_EQ(runner_of("x86_64-windows-gnu"), "wine64"); + expect(eq(runner_of("i686-windows-msvc"), "wine")); + expect(eq(runner_of("i686-pc-windows-msvc"), "wine")); + expect(eq(runner_of("x86-windows-msvc"), "wine")); + expect(eq(runner_of("x86_64-windows-gnu"), "wine64")); // A different baseline CPU is a different row. - EXPECT_EQ(runner_of("i386-windows-msvc"), "i386"); - EXPECT_EQ(runner_of("i586-windows-msvc"), ""); -} + expect(eq(runner_of("i386-windows-msvc"), "i386")); + expect(eq(runner_of("i586-windows-msvc"), "")); +}}; diff --git a/tests/unit/test_target_rows_snapshot.cpp b/tests/unit/test_target_rows_snapshot.cpp new file mode 100644 index 000000000..82c40e7ba --- /dev/null +++ b/tests/unit/test_target_rows_snapshot.cpp @@ -0,0 +1,172 @@ + +import std; +import mcpplibs.mtest; +import mcpp.toolchain.triple; +import mcpp.toolchain.target_schema; +import mcpp.toolchain.target_select; +import mcpp.toolchain.platform; +import mcpp.freestanding.target; +import mcpp.freestanding.linkline; + +using namespace mcpplibs::mtest; + +// THE EQUIVALENCE GATE OF THE TARGET MODEL (design 2026-11-09 §9, §14). +// +// The target rows moved from code to data. Every fact a row used to answer in +// code is rendered here, one line per fact, and compared with +// tests/fixtures/target-rows.snapshot, which was written by the code before the +// move. A difference is a change of behaviour of a built-in target; it is either +// a defect of the data or a deliberate change, and a deliberate change rewrites +// the snapshot (MCPP_SNAPSHOT_UPDATE=1) in the same commit that explains it. + +namespace tr = mcpp::toolchain::triple; +namespace fs = mcpp::freestanding; + +namespace { + +std::string join(const std::vector& v) { + std::string out; + for (auto const& s : v) { if (!out.empty()) out += ' '; out += s; } + return out; +} + +std::string yes(bool b) { return b ? "1" : "0"; } + +std::string facts_of(const tr::Triple& t) { + std::string o; + auto line = [&](std::string_view k, std::string v) { o += std::format(" {} {}\n", k, v); }; + line("str", t.str()); + line("llvm", t.llvm_triple()); + line("llvm@15.0", t.llvm_triple("15.0")); + line("format", std::string(tr::to_string(t.object_format()))); + line("family", t.family()); + line("pe/macho/wasm", yes(t.is_pe()) + yes(t.is_mach_o()) + yes(t.is_wasm())); + line("apple/ios/sim/android", yes(t.is_apple()) + yes(t.is_ios()) + yes(t.is_ios_simulator()) + + yes(t.is_android())); + line("freestanding/openkal/own-sysroot/capability", + yes(t.is_freestanding()) + yes(t.is_openkal()) + yes(t.has_own_sysroot()) + + yes(t.pin_is_capability())); + line("musl/msvc/windows-gnu/x86_32", yes(t.is_musl()) + yes(t.is_msvc_env()) + + yes(t.is_windows_gnu()) + yes(t.is_x86_32())); + line("msvc-arch", std::string(t.msvc_arch())); + line("nasm", t.nasm_format().value_or("-")); + const tr::ArtifactNaming host{".host", "hostlib", ".hosta", ".hostso", false}; + auto n = tr::artifact_naming(t, host); + line("naming", std::format("exe={} lib={} static={} shared={} import={} sharedPrefix={}", + n.exeSuffix, n.libPrefix, n.staticLibExt, n.sharedLibExt, + yes(n.sharedNeedsImportLib), + n.sharedLibPrefixSet ? std::string(n.sharedLibPrefix) : "-")); + line("application", tr::application_form(t) == tr::ApplicationForm::Executable ? "exe" : "shared"); + line("android-abi", tr::android_abi(t)); + line("platform-name", tr::platform_name(t)); + line("descriptor-keys", join(mcpp::toolchain::platform::descriptor_keys_for_os(t.os))); + line("sysroot", tr::effective_sysroot(t, nullptr)); + if (auto s = fs::resolve(t)) { + line("fs.spec", std::format("march={} mabi={} mcmodel={} libdir={} lld={}", s->march, s->mabi, + s->mcmodel, s->libdir, s->lldEmulation)); + line("fs.compile", join(fs::compile_flags(*s, false))); + line("fs.compile+rt", join(fs::compile_flags(*s, true))); + auto esc = [](const std::filesystem::path& p) { return p.generic_string(); }; + fs::LinkInputs in{ "/lld", "/script.ld", "/sysroot/lib", {}, {} }; + line("fs.link", fs::link_flags(*s, in, esc)); + if (!s->lldEmulation.empty()) line("fs.link-direct", fs::link_flags_direct(*s, in, esc)); + } + return o; +} + +std::string render() { + std::string o; + for (auto const& k : tr::known_targets()) { + o += std::format("row {}\n", k.canonical); + o += std::format(" table tier={} note={} pin={} sysroot={} static={} platform={}\n", k.tier, + k.note, k.pin, k.sysroot, yes(k.defaultStatic), k.platform); + if (auto t = tr::parse(k.canonical)) o += facts_of(*t); + else o += " unparseable\n"; + } + // The spellings a request may take, and what each resolves to before any + // provider is consulted. + static constexpr std::string_view kRequests[] = { + "x86_64-linux", "aarch64-linux", "riscv64-linux", "x86_64-windows", "i686-windows", + "aarch64-windows", "x86_64-macos", "aarch64-macos", "aarch64-ios", "x86_64-openkal", + "x86_64-w64-mingw32", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc", + "arm64-apple-darwin24.1.0", "arm64-apple-macosx15.0", "aarch64-unknown-linux-musl", + "aarch64-linux-android21", "wasm32-unknown-emscripten", "x86-windows-msvc", + "i686-none-elf", "x86_64-freebsd", "x86_64-linux-gnux32", + }; + for (auto r : kRequests) { + o += std::format("request {}\n", r); + auto t = tr::parse(r); + if (!t) { o += " unparseable\n"; continue; } + o += std::format(" parsed {} envExplicit={}\n", t->str(), yes(t->envExplicit)); + // The selection (SPEC-016 §4) on three hosts, none with MSVC. + for (auto host : { "linux-x86_64", "macos-aarch64", "windows-x86_64" }) { + mcpp::toolchain::target::Context ctx; + ctx.host = host; + auto sel = mcpp::toolchain::target::select(r, t->arch, t->os, t->env, t->envExplicit, ctx); + std::vector cand; + for (auto const& c : sel.candidates) + cand.push_back(std::format("{}/{}/{}{}", c.row->triple, + c.provider ? c.provider->name : std::string_view("-"), + c.priority, c.available ? "" : "/x")); + o += std::format(" on {} -> {} {} fallback={} | {}\n", host, + sel.row ? sel.identity() : std::string("-"), + sel.provider ? sel.provider->name : std::string_view("-"), + yes(sel.fallback), join(cand)); + } + } + static constexpr std::string_view kTypos[] = { + "x86_64-linux-mus", "x86-64-linux-gnu", "aarch64-macso", "riscv64-none-eld", "thumbv7m-none", + }; + for (auto r : kTypos) o += std::format("did-you-mean {} {}\n", r, tr::did_you_mean(r).value_or("-")); + namespace ts = mcpp::toolchain::target_set; + auto platformOf = [](const tr::Triple& t) -> std::expected { + if (auto* k = tr::find_known_target(t)) return std::string(k->platform); + return std::string{}; + }; + static constexpr std::string_view kSets[] = { + "linux", "linux-musl", "windows", "windows-msvc", "macos", "openkal", "none-elf", "none-eabihf", + "android", "ios", "ios-sim", "emscripten", "none", + }; + for (auto s : kSets) { + auto e = ts::expand_set(s, platformOf); + if (!e) { o += std::format("set {} refused\n", s); continue; } + o += std::format("set {} -> {} | {}\n", s, join(e->members), e->platform); + } + return o; +} + +std::optional snapshot_path() { + for (auto const* base : { "tests/fixtures/target-rows.snapshot", "../tests/fixtures/target-rows.snapshot", + "../../tests/fixtures/target-rows.snapshot" }) { + std::error_code ec; + if (std::filesystem::exists(std::filesystem::path(base).parent_path(), ec)) + return std::filesystem::path(base); + } + return std::nullopt; +} + +} // namespace + +UTest every_built_in_row_answers_as_the_snapshot_records{"TargetRows/EveryBuiltInRowAnswersAsTheSnapshotRecords", [] { + auto path = snapshot_path(); + expect(fatal(bool(path))) << "tests/fixtures not found from " << std::filesystem::current_path(); + const auto now = render(); + if (const char* u = std::getenv("MCPP_SNAPSHOT_UPDATE"); u && std::string_view(u) == "1") { + std::ofstream(*path, std::ios::binary) << now; + skip([&] { std::ostringstream text; text << "snapshot written to " << path->generic_string(); return text.str(); }()); + } + std::ifstream in(*path, std::ios::binary); + if (auto&& failed = expect(fatal(bool(in))); !failed) failed << "no snapshot at " << path->generic_string(); + const std::string was{std::istreambuf_iterator(in), {}}; + if (now == was) return; + // The first differing line, with its row, so the failure names the fact. + std::istringstream a(was), b(now); + std::string la, lb, row; + for (int n = 1;; ++n) { + const bool ga = bool(std::getline(a, la)), gb = bool(std::getline(b, lb)); + if (!ga && !gb) break; + if (gb && (lb.starts_with("row ") || lb.starts_with("request "))) row = lb; + if (la != lb || ga != gb) + fail([&] { std::ostringstream text; text << "line " << n << " (" << row << ")\n snapshot: " << (ga ? la : "") << "\n now: " << (gb ? lb : ""); return text.str(); }()); + } +}}; diff --git a/tests/unit/test_target_runtime_frameworks.cpp b/tests/unit/test_target_runtime_frameworks.cpp index 09da61d93..e076e50ee 100644 --- a/tests/unit/test_target_runtime_frameworks.cpp +++ b/tests/unit/test_target_runtime_frameworks.cpp @@ -1,9 +1,11 @@ -#include import std; +import mcpplibs.mtest; import mcpp.manifest; import mcpp.build.prepare; +using namespace mcpplibs::mtest; + namespace cfgpred = mcpp::build::cfgpred; // `[target..runtime] frameworks` (#622 A2). The one Mach-O key that @@ -31,7 +33,7 @@ frameworks = ["UIKit"] std::vector frameworks_for(std::string_view triple) { auto m = mcpp::manifest::parse_string(kSrc); - EXPECT_TRUE(m.has_value()); + expect(m.has_value()); if (!m) return {}; mcpp::build::merge_conditional_config(*m, cfgpred::context_for(triple)); return m->runtimeConfig.linkIntent.frameworks; @@ -39,11 +41,11 @@ std::vector frameworks_for(std::string_view triple) { } // namespace -TEST(TargetRuntimeFrameworks, TheSelectorDecidesAndTheTopLevelStays) { +UTest the_selector_decides_and_the_top_level_stays{"TargetRuntimeFrameworks/TheSelectorDecidesAndTheTopLevelStays", [] { const std::vector mac{"Foundation", "AppKit"}; const std::vector ios{"Foundation", "UIKit"}; const std::vector lin{"Foundation"}; - EXPECT_EQ(frameworks_for("aarch64-macos"), mac); - EXPECT_EQ(frameworks_for("aarch64-ios-sim"), ios); - EXPECT_EQ(frameworks_for("x86_64-linux-gnu"), lin); -} + expect(eq(frameworks_for("aarch64-macos"), mac)); + expect(eq(frameworks_for("aarch64-ios-sim"), ios)); + expect(eq(frameworks_for("x86_64-linux-gnu"), lin)); +}}; diff --git a/tests/unit/test_target_scalar_keys.cpp b/tests/unit/test_target_scalar_keys.cpp index 3f97f35f5..197aefd6a 100644 --- a/tests/unit/test_target_scalar_keys.cpp +++ b/tests/unit/test_target_scalar_keys.cpp @@ -1,6 +1,8 @@ -#include import std; +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; // #610 / #630 item 5 — `[target.]` has a scalar/array parser (a // sequence of `body.find("")` blocks) and, further down in the same @@ -127,28 +129,28 @@ std::vector known_list(const std::string& region, std::string_view } // namespace -TEST(TargetScalarKeys, EveryParsedNonTableKeyIsKnownToTheSweep) { +UTest every_parsed_non_table_key_is_known_to_the_sweep{"TargetScalarKeys/EveryParsedNonTableKeyIsKnownToTheSweep", [] { auto source = read_file(repo_root() / "modules" / "manifest" / "src" / "toml.cppm"); - ASSERT_FALSE(source.empty()) << "could not read toml.cppm"; + expect(fatal(!source.empty())) << "could not read toml.cppm"; auto region = target_entry_region(source); - ASSERT_FALSE(region.empty()) << "could not locate the [target.] entry parser"; + if (auto&& failed = expect(fatal(!region.empty())); !failed) failed << "could not locate the [target.] entry parser"; auto parsed = parsed_target_keys(region); auto knownScalars = known_list(region, "kKnownTargetScalars"); auto knownArrays = known_list(region, "kKnownTargetArrays"); - ASSERT_FALSE(knownScalars.empty()); - ASSERT_FALSE(knownArrays.empty()); + expect(fatal(!knownScalars.empty())); + expect(fatal(!knownArrays.empty())); // Direction 1 (the positive claim, and the failure mode #610 produced): // every key the parser reads as a scalar is in the sweep's scalar list, // and likewise for arrays. `min_api_level` is the case that used to fail // this. for (auto const& key : parsed.scalars) - EXPECT_NE(std::ranges::find(knownScalars, key), knownScalars.end()) + expect(ne(std::ranges::find(knownScalars, key), knownScalars.end())) << "'" << key << "' is parsed as a scalar but missing from " << "kKnownTargetScalars, so the sweep would report it as unsupported"; for (auto const& key : parsed.arrays) - EXPECT_NE(std::ranges::find(knownArrays, key), knownArrays.end()) + expect(ne(std::ranges::find(knownArrays, key), knownArrays.end())) << "'" << key << "' is parsed as an array but missing from " << "kKnownTargetArrays"; @@ -156,14 +158,14 @@ TEST(TargetScalarKeys, EveryParsedNonTableKeyIsKnownToTheSweep) { // names something the parser reads. A stale list entry would pass every // manifest silently and give no test failure otherwise. for (auto const& key : knownScalars) - EXPECT_NE(std::ranges::find(parsed.scalars, key), parsed.scalars.end()) + expect(ne(std::ranges::find(parsed.scalars, key), parsed.scalars.end())) << "kKnownTargetScalars names '" << key << "', which has no " << "body.find(...) parse site in the [target.] entry parser"; for (auto const& key : knownArrays) - EXPECT_NE(std::ranges::find(parsed.arrays, key), parsed.arrays.end()) + expect(ne(std::ranges::find(parsed.arrays, key), parsed.arrays.end())) << "kKnownTargetArrays names '" << key << "', which has no " << "body.find(...) parse site"; -} +}}; // The whole `[target.]` loop body: the scalar parser, the sweep, and // the conditional channel that follows it, up to the point the block is @@ -204,43 +206,43 @@ std::vector parsed_target_tables(const std::string& region) { return out; } -TEST(TargetScalarKeys, EveryParsedSubTableIsKnownToTheSweep) { +UTest every_parsed_sub_table_is_known_to_the_sweep{"TargetScalarKeys/EveryParsedSubTableIsKnownToTheSweep", [] { // #634: the sweep used to skip every table-valued key, so a misspelled // section, and a section an older engine does not read, did nothing // without a word. It now checks tables against `kKnownTargetTables`, and // this test holds that list to the parse sites in both directions, which // is the drift the skip was introduced to avoid. auto source = read_file(repo_root() / "modules" / "manifest" / "src" / "toml.cppm"); - ASSERT_FALSE(source.empty()) << "could not read toml.cppm"; + expect(fatal(!source.empty())) << "could not read toml.cppm"; auto region = target_loop_region(source); - ASSERT_FALSE(region.empty()) << "could not locate the [target.] loop body"; + if (auto&& failed = expect(fatal(!region.empty())); !failed) failed << "could not locate the [target.] loop body"; auto parsed = parsed_target_tables(region); auto known = known_list(region, "kKnownTargetTables[]"); - ASSERT_FALSE(parsed.empty()); - ASSERT_FALSE(known.empty()); + expect(fatal(!parsed.empty())); + expect(fatal(!known.empty())); for (auto const& key : parsed) - EXPECT_NE(std::ranges::find(known, key), known.end()) + expect(ne(std::ranges::find(known, key), known.end())) << "'" << key << "' is parsed as a sub-table of [target.] " << "but missing from kKnownTargetTables, so the sweep would report it"; for (auto const& key : known) - EXPECT_NE(std::ranges::find(parsed, key), parsed.end()) + expect(ne(std::ranges::find(parsed, key), parsed.end())) << "kKnownTargetTables names '" << key << "', which has no parse site " << "in the [target.] loop body"; // The table this list was introduced beside. - EXPECT_NE(std::ranges::find(parsed, std::string("targets")), parsed.end()); -} + expect(ne(std::ranges::find(parsed, std::string("targets")), parsed.end())); +}}; -TEST(TargetScalarKeys, MinApiLevelIsParsedAndKnown) { +UTest min_api_level_is_parsed_and_known{"TargetScalarKeys/MinApiLevelIsParsedAndKnown", [] { // A direct, non-derived check on the specific regression: `min_api_level` // must be both a parse site and a known scalar key. If this test passes // while the source-scan test above also passes, the fix is coherent; if // this one passes and the scan test fails, a *different* key drifted. auto source = read_file(repo_root() / "modules" / "manifest" / "src" / "toml.cppm"); auto region = target_entry_region(source); - ASSERT_FALSE(region.empty()); - EXPECT_NE(region.find("body.find(\"min_api_level\")"), std::string::npos); + expect(fatal(!region.empty())); + expect(ne(region.find("body.find(\"min_api_level\")"), std::string::npos)); auto knownScalars = known_list(region, "kKnownTargetScalars"); - EXPECT_NE(std::ranges::find(knownScalars, std::string("min_api_level")), knownScalars.end()); -} + expect(ne(std::ranges::find(knownScalars, std::string("min_api_level")), knownScalars.end())); +}}; diff --git a/tests/unit/test_target_set.cpp b/tests/unit/test_target_set.cpp index dfe543eda..516aa1b53 100644 --- a/tests/unit/test_target_set.cpp +++ b/tests/unit/test_target_set.cpp @@ -1,8 +1,10 @@ -#include import std; +import mcpplibs.mtest; import mcpp.toolchain.triple; +using namespace mcpplibs::mtest; + namespace ts = mcpp::toolchain::target_set; namespace tt = mcpp::toolchain::triple; @@ -18,7 +20,7 @@ std::expected column_platform(const tt::Triple& t) { std::vector legs_of(std::initializer_list requests) { std::vector req(requests); auto r = ts::resolve(req, column_platform); - EXPECT_TRUE(r.has_value()) << (r ? "" : r.error()); + if (auto&& failed = expect(r.has_value()); !failed) failed << (r ? "" : r.error()); std::vector out; if (r) for (auto& l : *r) out.push_back(l.triple); return out; @@ -28,143 +30,143 @@ std::vector legs_of(std::initializer_list requests) { // ── recognition: the first segment, not a failed parse ────────────────────── -TEST(TargetSet, RecognisesSystemWords) { - EXPECT_TRUE(ts::is_set_spelling("openkal")); - EXPECT_TRUE(ts::is_set_spelling("linux-musl")); - EXPECT_TRUE(ts::is_set_spelling("none-eabihf")); - EXPECT_TRUE(ts::is_set_spelling("android")); - EXPECT_FALSE(ts::is_set_spelling("x86_64-openkal")); - EXPECT_FALSE(ts::is_set_spelling("aarch64-linux-musl")); -} +UTest recognises_system_words{"TargetSet/RecognisesSystemWords", [] { + expect(ts::is_set_spelling("openkal")); + expect(ts::is_set_spelling("linux-musl")); + expect(ts::is_set_spelling("none-eabihf")); + expect(ts::is_set_spelling("android")); + expect(!ts::is_set_spelling("x86_64-openkal")); + expect(!ts::is_set_spelling("aarch64-linux-musl")); +}}; -TEST(TargetSet, VendorFirstSpellingIsATriple) { +UTest vendor_first_spelling_is_a_triple{"TargetSet/VendorFirstSpellingIsATriple", [] { // `unknown-linux-gnu` parses as a triple (architecture `unknown`); it is // not a set, because `unknown` is not a system word. - EXPECT_FALSE(ts::is_set_spelling("unknown-linux-gnu")); - EXPECT_FALSE(ts::is_set_spelling("apple-macos")); + expect(!ts::is_set_spelling("unknown-linux-gnu")); + expect(!ts::is_set_spelling("apple-macos")); auto legs = legs_of({"unknown-linux-gnu"}); - ASSERT_EQ(legs.size(), 1u); - EXPECT_EQ(legs.front(), "unknown-linux-gnu"); -} + expect(fatal(eq(legs.size(), 1u))); + expect(eq(legs.front(), "unknown-linux-gnu")); +}}; // ── expansion ──────────────────────────────────────────────────────────────── -TEST(TargetSet, OpenkalNamesBothArchitectures) { +UTest openkal_names_both_architectures{"TargetSet/OpenkalNamesBothArchitectures", [] { auto e = ts::expand_set("openkal", column_platform); - ASSERT_TRUE(e.has_value()) << e.error(); - EXPECT_EQ(e->members, (std::vector{"x86_64-openkal", "aarch64-openkal"})); - EXPECT_TRUE(e->platform.starts_with("mcpplibs.openkal-platform@")); -} + if (auto&& failed = expect(fatal(e.has_value())); !failed) failed << e.error(); + expect(eq(e->members, (std::vector{"x86_64-openkal", "aarch64-openkal"}))); + expect(e->platform.starts_with("mcpplibs.openkal-platform@")); +}}; -TEST(TargetSet, LinuxMuslLeavesPlannedRowsOut) { +UTest linux_musl_leaves_planned_rows_out{"TargetSet/LinuxMuslLeavesPlannedRowsOut", [] { auto e = ts::expand_set("linux-musl", column_platform); - ASSERT_TRUE(e.has_value()) << e.error(); - EXPECT_EQ(e->members, (std::vector{"x86_64-linux-musl", "aarch64-linux-musl"})); - for (auto& m : e->members) EXPECT_NE(m, "riscv64-linux-musl"); -} + if (auto&& failed = expect(fatal(e.has_value())); !failed) failed << e.error(); + expect(eq(e->members, (std::vector{"x86_64-linux-musl", "aarch64-linux-musl"}))); + for (auto& m : e->members) expect(ne(m, "riscv64-linux-musl")); +}}; -TEST(TargetSet, LinuxFillsGnuAndLeavesAndroidOut) { +UTest linux_fills_gnu_and_leaves_android_out{"TargetSet/LinuxFillsGnuAndLeavesAndroidOut", [] { auto e = ts::expand_set("linux", column_platform); - ASSERT_TRUE(e.has_value()) << e.error(); - EXPECT_EQ(e->members, (std::vector{"x86_64-linux-gnu", "aarch64-linux-gnu"})); -} + if (auto&& failed = expect(fatal(e.has_value())); !failed) failed << e.error(); + expect(eq(e->members, (std::vector{"x86_64-linux-gnu", "aarch64-linux-gnu"}))); +}}; -TEST(TargetSet, WindowsFillsGnu) { +UTest windows_fills_gnu{"TargetSet/WindowsFillsGnu", [] { auto e = ts::expand_set("windows", column_platform); - ASSERT_TRUE(e.has_value()) << e.error(); - EXPECT_EQ(e->members, (std::vector{"x86_64-windows-gnu"})); -} + if (auto&& failed = expect(fatal(e.has_value())); !failed) failed << e.error(); + expect(eq(e->members, (std::vector{"x86_64-windows-gnu"}))); +}}; -TEST(TargetSet, AndroidIsTheLinuxAndroidRows) { +UTest android_is_the_linux_android_rows{"TargetSet/AndroidIsTheLinuxAndroidRows", [] { auto e = ts::expand_set("android", column_platform); - ASSERT_TRUE(e.has_value()) << e.error(); - EXPECT_EQ(e->members, - (std::vector{"aarch64-linux-android", "x86_64-linux-android"})); -} + if (auto&& failed = expect(fatal(e.has_value())); !failed) failed << e.error(); + expect(eq(e->members, (std::vector{"aarch64-linux-android", "x86_64-linux-android"}))); +}}; -TEST(TargetSet, IosAndItsSimulator) { +UTest ios_and_its_simulator{"TargetSet/IosAndItsSimulator", [] { auto device = ts::expand_set("ios", column_platform); - ASSERT_TRUE(device.has_value()) << device.error(); - EXPECT_EQ(device->members, (std::vector{"aarch64-ios"})); + if (auto&& failed = expect(fatal(device.has_value())); !failed) failed << device.error(); + expect(eq(device->members, (std::vector{"aarch64-ios"}))); auto sim = ts::expand_set("ios-sim", column_platform); - ASSERT_TRUE(sim.has_value()) << sim.error(); - EXPECT_EQ(sim->members, (std::vector{"aarch64-ios-sim", "x86_64-ios-sim"})); -} + if (auto&& failed = expect(fatal(sim.has_value())); !failed) failed << sim.error(); + expect(eq(sim->members, (std::vector{"aarch64-ios-sim", "x86_64-ios-sim"}))); +}}; -TEST(TargetSet, BareMetalNeedsItsEnvironment) { +UTest bare_metal_needs_its_environment{"TargetSet/BareMetalNeedsItsEnvironment", [] { auto bare = ts::expand_set("none", column_platform); - ASSERT_FALSE(bare.has_value()); - EXPECT_NE(bare.error().find("none-elf"), std::string::npos); + expect(fatal(!bare.has_value())); + expect(ne(bare.error().find("none-elf"), std::string::npos)); auto elf = ts::expand_set("none-elf", column_platform); - ASSERT_TRUE(elf.has_value()) << elf.error(); - EXPECT_EQ(elf->members, (std::vector{ - "riscv64-none-elf", "riscv32-none-elf", "aarch64-none-elf", "x86_64-none-elf"})); -} + if (auto&& failed = expect(fatal(elf.has_value())); !failed) failed << elf.error(); + expect(eq(elf->members, (std::vector{ + "riscv64-none-elf", "riscv32-none-elf", "aarch64-none-elf", "x86_64-none-elf", + "i686-none-elf"}))); +}}; -TEST(TargetSet, EmptySetIsRefusedNamingThePlannedRows) { +UTest empty_set_is_refused_naming_the_planned_rows{"TargetSet/EmptySetIsRefusedNamingThePlannedRows", [] { auto e = ts::expand_set("macos-gnu", column_platform); - ASSERT_FALSE(e.has_value()); + expect(fatal(!e.has_value())); auto none = ts::expand_set("linux-msvc", column_platform); - ASSERT_FALSE(none.has_value()); - EXPECT_NE(none.error().find("names no target"), std::string::npos); -} - -TEST(TargetSet, MalformedSetsAreRefused) { - EXPECT_FALSE(ts::expand_set("linux-musl-extra", column_platform).has_value()); - EXPECT_FALSE(ts::expand_set("linux-", column_platform).has_value()); - EXPECT_FALSE(ts::expand_set("openkal-gnu", column_platform).has_value()); - EXPECT_FALSE(ts::expand_set("android-x", column_platform).has_value()); -} - -TEST(TargetSet, MembersSelectingDifferentPlatformsAreRefused) { + expect(fatal(!none.has_value())); + expect(ne(none.error().find("names no target"), std::string::npos)); +}}; + +UTest malformed_sets_are_refused{"TargetSet/MalformedSetsAreRefused", [] { + expect(!(ts::expand_set("linux-musl-extra", column_platform).has_value())); + expect(!(ts::expand_set("linux-", column_platform).has_value())); + expect(!(ts::expand_set("openkal-gnu", column_platform).has_value())); + expect(!(ts::expand_set("android-x", column_platform).has_value())); +}}; + +UTest members_selecting_different_platforms_are_refused{"TargetSet/MembersSelectingDifferentPlatformsAreRefused", [] { auto split = [](const tt::Triple& t) -> std::expected { return t.arch == "x86_64" ? std::string("a@1") : std::string("b@1"); }; auto e = ts::expand_set("openkal", split); - ASSERT_FALSE(e.has_value()); - EXPECT_NE(e.error().find("select different platforms"), std::string::npos); - EXPECT_NE(e.error().find("x86_64-openkal"), std::string::npos); - EXPECT_NE(e.error().find("aarch64-openkal"), std::string::npos); -} + expect(fatal(!e.has_value())); + expect(ne(e.error().find("select different platforms"), std::string::npos)); + expect(ne(e.error().find("x86_64-openkal"), std::string::npos)); + expect(ne(e.error().find("aarch64-openkal"), std::string::npos)); +}}; -TEST(TargetSet, ASelectionErrorNamesTheMember) { +UTest a_selection_error_names_the_member{"TargetSet/ASelectionErrorNamesTheMember", [] { auto broken = [](const tt::Triple&) -> std::expected { return std::unexpected(std::string("no such platform")); }; auto e = ts::expand_set("openkal", broken); - ASSERT_FALSE(e.has_value()); - EXPECT_NE(e.error().find("x86_64-openkal: no such platform"), std::string::npos); -} + expect(fatal(!e.has_value())); + expect(ne(e.error().find("x86_64-openkal: no such platform"), std::string::npos)); +}}; // ── several --target ───────────────────────────────────────────────────────── -TEST(TargetSet, UnionInTableOrderWithoutDuplicates) { +UTest union_in_table_order_without_duplicates{"TargetSet/UnionInTableOrderWithoutDuplicates", [] { auto legs = legs_of({"aarch64-openkal", "openkal", "x86_64-linux-musl"}); - EXPECT_EQ(legs, (std::vector{ - "x86_64-linux-musl", "x86_64-openkal", "aarch64-openkal"})); -} + expect(eq(legs, (std::vector{ + "x86_64-linux-musl", "x86_64-openkal", "aarch64-openkal"}))); +}}; -TEST(TargetSet, ATargetKeepsItsSpelling) { +UTest a_target_keeps_its_spelling{"TargetSet/ATargetKeepsItsSpelling", [] { // Planned as the command would plan it alone: `x86_64-linux` is completed // and reported by the build, not here. auto legs = legs_of({"x86_64-linux"}); - ASSERT_EQ(legs.size(), 1u); - EXPECT_EQ(legs.front(), "x86_64-linux"); + expect(fatal(eq(legs.size(), 1u))); + expect(eq(legs.front(), "x86_64-linux")); auto twice = legs_of({"x86_64-linux", "x86_64-linux-gnu"}); - EXPECT_EQ(twice.size(), 1u); -} + expect(eq(twice.size(), 1u)); +}}; -TEST(TargetSet, UnknownTargetsFollowTheTableInWrittenOrder) { +UTest unknown_targets_follow_the_table_in_written_order{"TargetSet/UnknownTargetsFollowTheTableInWrittenOrder", [] { auto legs = legs_of({"zz-custom-target", "x86_64-openkal", "yy-other"}); - EXPECT_EQ(legs, (std::vector{"x86_64-openkal", "zz-custom-target", "yy-other"})); -} + expect(eq(legs, (std::vector{"x86_64-openkal", "zz-custom-target", "yy-other"}))); +}}; -TEST(TargetSet, LegsRecordTheirSet) { +UTest legs_record_their_set{"TargetSet/LegsRecordTheirSet", [] { std::vector req{"openkal", "x86_64-linux-musl"}; auto r = ts::resolve(req, column_platform); - ASSERT_TRUE(r.has_value()); + expect(fatal(r.has_value())); for (auto& l : *r) { - if (l.triple.ends_with("openkal")) EXPECT_EQ(l.fromSet, "openkal"); - else EXPECT_TRUE(l.fromSet.empty()); + if (l.triple.ends_with("openkal")) expect(eq(l.fromSet, "openkal")); + else expect(l.fromSet.empty()); } -} +}}; diff --git a/tests/unit/test_target_static.cpp b/tests/unit/test_target_static.cpp index 921c710ce..21c91976c 100644 --- a/tests/unit/test_target_static.cpp +++ b/tests/unit/test_target_static.cpp @@ -1,9 +1,11 @@ -#include import std; +import mcpplibs.mtest; import mcpp.toolchain.model; import mcpp.toolchain.platform; +using namespace mcpplibs::mtest; + // Whether the produced artifact can be fully statically linked (`-static`) is // a property of the TARGET, never of the machine doing the build. // @@ -24,54 +26,54 @@ namespace tc = mcpp::toolchain; // ── The regression itself ─────────────────────────────────────────────────── // Every one of these runs a NON-Linux host (hostCapability=false) against a // Linux target. Before the fix all four returned false. -TEST(TargetSupportsFullStatic, LinuxTargetFromNonLinuxHost) { - EXPECT_TRUE(tc::target_supports_full_static("x86_64-linux-musl", false)); - EXPECT_TRUE(tc::target_supports_full_static("aarch64-linux-musl", false)); - EXPECT_TRUE(tc::target_supports_full_static("x86_64-linux-gnu", false)); +UTest linux_target_from_non_linux_host{"TargetSupportsFullStatic/LinuxTargetFromNonLinuxHost", [] { + expect(bool(tc::target_supports_full_static("x86_64-linux-musl", false))); + expect(bool(tc::target_supports_full_static("aarch64-linux-musl", false))); + expect(bool(tc::target_supports_full_static("x86_64-linux-gnu", false))); // Legacy/alternate spellings must resolve identically — the triple parser // is the single source of truth, not a substring match. - EXPECT_TRUE(tc::target_supports_full_static("x86_64-unknown-linux-musl", false)); -} + expect(bool(tc::target_supports_full_static("x86_64-unknown-linux-musl", false))); +}}; // A Linux host keeps answering exactly as before: this fix must be a no-op // on every path that works today. -TEST(TargetSupportsFullStatic, LinuxTargetFromLinuxHostUnchanged) { - EXPECT_TRUE(tc::target_supports_full_static("x86_64-linux-musl", true)); - EXPECT_TRUE(tc::target_supports_full_static("aarch64-linux-musl", true)); - EXPECT_TRUE(tc::target_supports_full_static("x86_64-linux-gnu", true)); -} +UTest linux_target_from_linux_host_unchanged{"TargetSupportsFullStatic/LinuxTargetFromLinuxHostUnchanged", [] { + expect(bool(tc::target_supports_full_static("x86_64-linux-musl", true))); + expect(bool(tc::target_supports_full_static("aarch64-linux-musl", true))); + expect(bool(tc::target_supports_full_static("x86_64-linux-gnu", true))); +}}; // ── Targets that genuinely cannot be fully static ─────────────────────────── // macOS: libSystem must stay dynamic, no matter who builds it. -TEST(TargetSupportsFullStatic, MacosTargetNeverStatic) { - EXPECT_FALSE(tc::target_supports_full_static("aarch64-macos", true)); - EXPECT_FALSE(tc::target_supports_full_static("aarch64-macos", false)); - EXPECT_FALSE(tc::target_supports_full_static("x86_64-macos", true)); -} +UTest macos_target_never_static{"TargetSupportsFullStatic/MacosTargetNeverStatic", [] { + expect(!tc::target_supports_full_static("aarch64-macos", true)); + expect(!tc::target_supports_full_static("aarch64-macos", false)); + expect(!tc::target_supports_full_static("x86_64-macos", true)); +}}; // PE targets take their `-static` from the C++ runtime distribution contract // (dist::Format::Pe in flags.cppm), not from this predicate. Answering false // here is what keeps the two mechanisms from both emitting the flag. -TEST(TargetSupportsFullStatic, PeTargetsDeferToContractTable) { - EXPECT_FALSE(tc::target_supports_full_static("x86_64-windows-gnu", true)); - EXPECT_FALSE(tc::target_supports_full_static("x86_64-windows-msvc", true)); - EXPECT_FALSE(tc::target_supports_full_static("x86_64-w64-mingw32", true)); -} +UTest pe_targets_defer_to_contract_table{"TargetSupportsFullStatic/PeTargetsDeferToContractTable", [] { + expect(!tc::target_supports_full_static("x86_64-windows-gnu", true)); + expect(!tc::target_supports_full_static("x86_64-windows-msvc", true)); + expect(!tc::target_supports_full_static("x86_64-w64-mingw32", true)); +}}; // ── Host target (empty triple) ────────────────────────────────────────────── // Empty means "build for this machine", so the host capability IS the answer. // This is the one case where the host constant remains correct. -TEST(TargetSupportsFullStatic, EmptyTripleFallsBackToHost) { - EXPECT_TRUE(tc::target_supports_full_static("", true)); - EXPECT_FALSE(tc::target_supports_full_static("", false)); -} +UTest static_empty_triple_falls_back_to_host{"TargetSupportsFullStatic/EmptyTripleFallsBackToHost", [] { + expect(bool(tc::target_supports_full_static("", true))); + expect(!tc::target_supports_full_static("", false)); +}}; // An unparseable triple must not silently become "static" — fall back to the // host answer rather than guessing from a substring. -TEST(TargetSupportsFullStatic, UnknownTripleFallsBackToHost) { - EXPECT_TRUE(tc::target_supports_full_static("wasm32-unknown-unknown", true)); - EXPECT_FALSE(tc::target_supports_full_static("wasm32-unknown-unknown", false)); - EXPECT_FALSE(tc::target_supports_full_static("not-a-triple", false)); -} +UTest unknown_triple_falls_back_to_host{"TargetSupportsFullStatic/UnknownTripleFallsBackToHost", [] { + expect(bool(tc::target_supports_full_static("wasm32-unknown-unknown", true))); + expect(!tc::target_supports_full_static("wasm32-unknown-unknown", false)); + expect(!tc::target_supports_full_static("not-a-triple", false)); +}}; } // namespace diff --git a/tests/unit/test_target_xlings_axis.cpp b/tests/unit/test_target_xlings_axis.cpp index bcfffe701..a795980a7 100644 --- a/tests/unit/test_target_xlings_axis.cpp +++ b/tests/unit/test_target_xlings_axis.cpp @@ -1,9 +1,11 @@ -#include import std; +import mcpplibs.mtest; import mcpp.manifest; import mcpp.build.prepare; +using namespace mcpplibs::mtest; + // SPEC-004 §4 — the tool plane has TWO resolution axes, and this file is the // measurement that tells them apart. // @@ -50,7 +52,7 @@ bool has(const std::vector& v, std::string_view a) { // leg had already merged would report the union and pass either way. mcpp::manifest::Manifest merged_for(std::string_view triple) { auto m = mcpp::manifest::parse_string(kSrc); - EXPECT_TRUE(m.has_value()); + expect(m.has_value()); if (!m) return {}; mcpp::build::merge_conditional_config(*m, cfgpred::context_for(triple)); return *m; @@ -58,33 +60,33 @@ mcpp::manifest::Manifest merged_for(std::string_view triple) { } // namespace -TEST(TargetXlingsAxis, SelectorDecidesWhichEntriesApply) { +UTest selector_decides_which_entries_apply{"TargetXlingsAxis/SelectorDecidesWhichEntriesApply", [] { auto lin = merged_for("x86_64-unknown-linux-gnu"); auto win = merged_for("x86_64-pc-windows-gnu"); // The host axis is the SAME on both legs. It is not conditional on the // target and must not become so: `xim:dpcpp` is a compiler that runs here // no matter what it is asked to emit. - EXPECT_TRUE(has(lin.xlings.deps, "xim:hosttool@1.0.0")); - EXPECT_TRUE(has(win.xlings.deps, "xim:hosttool@1.0.0")); + expect(bool(has(lin.xlings.deps, "xim:hosttool@1.0.0"))); + expect(bool(has(win.xlings.deps, "xim:hosttool@1.0.0"))); // The target axis follows the target, and nothing else in this test does. - EXPECT_TRUE (has(lin.xlings.deps, "xim:linuxonly@9.9.9")); - EXPECT_FALSE(has(lin.xlings.deps, "xim:winonly@8.8.8")); - EXPECT_TRUE (has(win.xlings.deps, "xim:winonly@8.8.8")); - EXPECT_FALSE(has(win.xlings.deps, "xim:linuxonly@9.9.9")); + expect(bool(has(lin.xlings.deps, "xim:linuxonly@9.9.9"))); + expect(!has(lin.xlings.deps, "xim:winonly@8.8.8")); + expect(bool(has(win.xlings.deps, "xim:winonly@8.8.8"))); + expect(!has(win.xlings.deps, "xim:linuxonly@9.9.9")); // The pin reaches the materialised resolution layer, keyed the way the // host axis keys it. An entry installed without its pin resolves to // whatever the machine already has, which is a different package on a // different day. auto pin = win.xlings.workspace.find("winonly"); - ASSERT_NE(pin, win.xlings.workspace.end()); - EXPECT_EQ(pin->second, "xim:8.8.8"); - EXPECT_FALSE(win.xlings.workspace.contains("linuxonly")); -} + expect(fatal(ne(pin, win.xlings.workspace.end()))); + expect(eq(pin->second, "xim:8.8.8")); + expect(!win.xlings.workspace.contains("linuxonly")); +}}; -TEST(TargetXlingsAxis, GateAndSelectorCompose) { +UTest gate_and_selector_compose{"TargetXlingsAxis/GateAndSelectorCompose", [] { // `[target..feature-xlings.]` is a gate INSIDE a condition, the // same composition `[target..feature-deps.]` already had (#359). // The feature is registered unconditionally — whether the target matches @@ -94,19 +96,19 @@ TEST(TargetXlingsAxis, GateAndSelectorCompose) { auto lin = merged_for("x86_64-unknown-linux-gnu"); auto win = merged_for("x86_64-pc-windows-gnu"); - EXPECT_TRUE(lin.featuresMap.contains("gate")); - EXPECT_TRUE(win.featuresMap.contains("gate")); + expect(lin.featuresMap.contains("gate")); + expect(win.featuresMap.contains("gate")); auto lg = lin.xlings.featureDeps.find("gate"); - EXPECT_TRUE(lg == lin.xlings.featureDeps.end() || lg->second.empty()); + expect(lg == lin.xlings.featureDeps.end() || lg->second.empty()); auto wg = win.xlings.featureDeps.find("gate"); - ASSERT_NE(wg, win.xlings.featureDeps.end()); - EXPECT_TRUE(has(wg->second, "xim:gatedwin@7.7.7")); - EXPECT_EQ(win.xlings.featurePins.at("xim:gatedwin@7.7.7"), "xim:7.7.7"); -} + expect(fatal(ne(wg, win.xlings.featureDeps.end()))); + expect(bool(has(wg->second, "xim:gatedwin@7.7.7"))); + expect(eq(win.xlings.featurePins.at("xim:gatedwin@7.7.7"), "xim:7.7.7")); +}}; -TEST(TargetXlingsAxis, ConditionWrittenTwiceIsRefused) { +UTest condition_written_twice_is_refused{"TargetXlingsAxis/ConditionWrittenTwiceIsRefused", [] { // Platform keys inside a value under a selector say a second time what the // selector already said. Refused rather than resolved: two statements of // one fact can disagree, and whichever one loses does so in silence. @@ -118,16 +120,16 @@ version = "0.1.0" [target.'cfg(os = "linux")'.xlings.workspace] "xim:tool" = { linux = "1.0.0", macosx = "2.0.0" } )"); - ASSERT_FALSE(m.has_value()); + expect(fatal(!m.has_value())); auto msg = m.error().format(); - EXPECT_NE(msg.find("xim:tool"), std::string::npos) << msg; - EXPECT_NE(msg.find("platform keys"), std::string::npos) << msg; + expect(ne(msg.find("xim:tool"), std::string::npos)) << msg; + expect(ne(msg.find("platform keys"), std::string::npos)) << msg; // The message must name the OUTER selector, because that is the half the // author has to delete, and it is not the half they are looking at. - EXPECT_NE(msg.find("cfg(os = \"linux\")"), std::string::npos) << msg; -} + expect(ne(msg.find("cfg(os = \"linux\")"), std::string::npos)) << msg; +}}; -TEST(TargetXlingsAxis, SubosIsNotConditionalOnATarget) { +UTest subos_is_not_conditional_on_a_target{"TargetXlingsAxis/SubosIsNotConditionalOnATarget", [] { // A project has one environment, not one per target. Refused rather than // ignored: a silently dropped environment is the failure #531 was filed // for. @@ -139,12 +141,12 @@ version = "0.1.0" [target.'cfg(os = "linux")'.xlings] subos = "3" )"); - ASSERT_FALSE(m.has_value()); + expect(fatal(!m.has_value())); auto msg = m.error().format(); - EXPECT_NE(msg.find("subos"), std::string::npos) << msg; -} + expect(ne(msg.find("subos"), std::string::npos)) << msg; +}}; -TEST(TargetXlingsAxis, HostAxisKeepsItsPlatformKeys) { +UTest host_axis_keeps_its_platform_keys{"TargetXlingsAxis/HostAxisKeepsItsPlatformKeys", [] { // V4, at unit scale: the existing spelling is untouched. The value object // under a TOP-LEVEL `[xlings]` entry is not legacy — it is how the host // axis is written, and it stays resolved against the host. @@ -156,27 +158,27 @@ version = "0.1.0" [xlings.workspace] "xim:tool" = { linux = "1.0.0", macosx = "2.0.0", windows = "3.0.0", default = "4.0.0" } )"); - ASSERT_TRUE(m.has_value()); + expect(fatal(m.has_value())); const auto& pin = m->xlings.workspace.at("tool"); #if defined(__linux__) - EXPECT_EQ(pin, "xim:1.0.0"); - EXPECT_TRUE(has(m->xlings.deps, "xim:tool@1.0.0")); + expect(eq(pin, "xim:1.0.0")); + expect(bool(has(m->xlings.deps, "xim:tool@1.0.0"))); #elif defined(__APPLE__) - EXPECT_EQ(pin, "xim:2.0.0"); - EXPECT_TRUE(has(m->xlings.deps, "xim:tool@2.0.0")); + expect(eq(pin, "xim:2.0.0")); + expect(bool(has(m->xlings.deps, "xim:tool@2.0.0"))); #elif defined(_WIN32) - EXPECT_EQ(pin, "xim:3.0.0"); - EXPECT_TRUE(has(m->xlings.deps, "xim:tool@3.0.0")); + expect(eq(pin, "xim:3.0.0")); + expect(bool(has(m->xlings.deps, "xim:tool@3.0.0"))); #else - EXPECT_EQ(pin, "xim:4.0.0"); - EXPECT_TRUE(has(m->xlings.deps, "xim:tool@4.0.0")); + expect(eq(pin, "xim:4.0.0")); + expect(bool(has(m->xlings.deps, "xim:tool@4.0.0"))); #endif // And it is NOT reported as a conditional section: the host axis resolves // at parse time and leaves nothing for the merge to do. - EXPECT_TRUE(m->conditionalConfigs.empty()); -} + expect(m->conditionalConfigs.empty()); +}}; -TEST(TargetXlingsAxis, BothAxesNamingOnePackageResolveToOneInstall) { +UTest both_axes_naming_one_package_resolve_to_one_install{"TargetXlingsAxis/BothAxesNamingOnePackageResolveToOneInstall", [] { // `xim:glibc` and `xim:glibc@2.40` are two addresses for ONE install. // Keeping both would ask xlings for the same package at two versions, and // that is not a build that fails — it is a build whose environment depends @@ -192,19 +194,19 @@ version = "0.1.0" [target.'cfg(os = "linux")'.xlings.workspace] "xim:glibc" = "2.40" )"); - ASSERT_TRUE(m.has_value()); + expect(fatal(m.has_value())); mcpp::build::merge_conditional_config( *m, cfgpred::context_for("x86_64-unknown-linux-gnu")); auto named = std::ranges::count_if(m->xlings.deps, [](const std::string& a) { return mcpp::manifest::parse_address(a).target == "glibc"; }); - EXPECT_EQ(named, 1); - EXPECT_TRUE(has(m->xlings.deps, "xim:glibc@2.40")); - EXPECT_EQ(m->xlings.workspace.at("glibc"), "xim:2.40"); -} + expect(eq(named, 1)); + expect(bool(has(m->xlings.deps, "xim:glibc@2.40"))); + expect(eq(m->xlings.workspace.at("glibc"), "xim:2.40")); +}}; -TEST(TargetXlingsAxis, ABareTripleSelectorIsASelectorToo) { +UTest a_bare_triple_selector_is_a_selector_too{"TargetXlingsAxis/ABareTripleSelectorIsASelectorToo", [] { // `[target..xlings.workspace]` is the other spelling of a selector, // and it was reached by the same dispatcher rather than by the // `[target.]` handler that owns `toolchain` and `runner`. Asserted @@ -219,14 +221,14 @@ version = "0.1.0" "xim:tool" = "1.0.0" )"; auto lin = mcpp::manifest::parse_string(src); - ASSERT_TRUE(lin.has_value()); + expect(fatal(lin.has_value())); mcpp::build::merge_conditional_config( *lin, cfgpred::context_for("x86_64-unknown-linux-gnu")); - EXPECT_TRUE(has(lin->xlings.deps, "xim:tool@1.0.0")); + expect(bool(has(lin->xlings.deps, "xim:tool@1.0.0"))); auto other = mcpp::manifest::parse_string(src); - ASSERT_TRUE(other.has_value()); + expect(fatal(other.has_value())); mcpp::build::merge_conditional_config( *other, cfgpred::context_for("aarch64-unknown-linux-gnu")); - EXPECT_FALSE(has(other->xlings.deps, "xim:tool@1.0.0")); -} + expect(!has(other->xlings.deps, "xim:tool@1.0.0")); +}}; diff --git a/tests/unit/test_targetside.cpp b/tests/unit/test_targetside.cpp index 36a699f4e..6f1007db9 100644 --- a/tests/unit/test_targetside.cpp +++ b/tests/unit/test_targetside.cpp @@ -12,11 +12,13 @@ // from inside a 7000-line translation unit and had zero test coverage, because // asserting it required running a whole build. Nothing here runs a build. -#include import std; +import mcpplibs.mtest; import mcpp.targetside; +using namespace mcpplibs::mtest; + namespace ts = mcpp::targetside; namespace { @@ -48,70 +50,70 @@ ts::Provider provider(std::string name, std::string version, // ── The capability grammar ─────────────────────────────────────────────────── -TEST(TargetSideCapability, ThreeLayerNamesAreAccepted) { +UTest three_layer_names_are_accepted{"TargetSideCapability/ThreeLayerNamesAreAccepted", [] { for (auto [entry, layer] : std::initializer_list< std::pair>{ {"mcpp:kernel-abi", ts::CapLayer::KernelAbi}, {"mcpp:c-abi", ts::CapLayer::CAbi}, {"mcpp:c++-abi", ts::CapLayer::CxxAbi}}) { auto r = ts::parse_capability(entry); - ASSERT_TRUE(r.has_value()) << entry; - ASSERT_TRUE(r->has_value()) << entry; - EXPECT_EQ((*r)->layer, layer) << entry; - EXPECT_TRUE((*r)->interfaceName.empty()) << entry; + expect(fatal(r.has_value())) << entry; + expect(fatal(r->has_value())) << entry; + expect(eq((*r)->layer, layer)) << entry; + expect((*r)->interfaceName.empty()) << entry; } -} +}}; -TEST(TargetSideCapability, InterfaceNameIsCarriedByTheDeclaration) { +UTest interface_name_is_carried_by_the_declaration{"TargetSideCapability/InterfaceNameIsCarriedByTheDeclaration", [] { auto r = ts::parse_capability("mcpp:kernel-abi=openkal"); - ASSERT_TRUE(r.has_value()); - ASSERT_TRUE(r->has_value()); - EXPECT_EQ((*r)->layer, ts::CapLayer::KernelAbi); - EXPECT_EQ((*r)->interfaceName, "openkal"); -} + expect(fatal(r.has_value())); + expect(fatal(r->has_value())); + expect(eq((*r)->layer, ts::CapLayer::KernelAbi)); + expect(eq((*r)->interfaceName, "openkal")); +}}; // The engine must not reject names it does not own. `provides` serves the // feature system too, and a closed set over the whole array would break the // allocator selection that already ships. -TEST(TargetSideCapability, NamesOutsideTheReservedPrefixPassThrough) { +UTest names_outside_the_reserved_prefix_pass_through{"TargetSideCapability/NamesOutsideTheReservedPrefixPassThrough", [] { for (std::string_view entry : {"freestanding-allocator", "hosted-standard-library", "anything-a-package-invents"}) { auto r = ts::parse_capability(entry); - ASSERT_TRUE(r.has_value()) << entry; - EXPECT_FALSE(r->has_value()) << entry; + expect(fatal(r.has_value())) << entry; + expect(!r->has_value()) << entry; } -} +}}; // And the point of having a prefix at all: inside it, a typo is an error rather // than a silently disabled behaviour. -TEST(TargetSideCapability, UnknownNameInsideTheReservedPrefixIsAnError) { +UTest unknown_name_inside_the_reserved_prefix_is_an_error{"TargetSideCapability/UnknownNameInsideTheReservedPrefixIsAnError", [] { for (std::string_view entry : {"mcpp:kernel_abi", // underscore, not hyphen "mcpp:cabi", "mcpp:c++abi", "mcpp:target-system"}) { auto r = ts::parse_capability(entry); - EXPECT_FALSE(r.has_value()) << entry; + expect(!r.has_value()) << entry; if (!r.has_value()) - EXPECT_NE(r.error().find("mcpp:kernel-abi"), std::string::npos) + expect(ne(r.error().find("mcpp:kernel-abi"), std::string::npos)) << "the diagnostic must list the layers that do exist"; } -} +}}; // ── The resolution table ───────────────────────────────────────────────────── -TEST(TargetSideResolve, TraditionalStackTakesEveryLayerFromThePayload) { +UTest traditional_stack_takes_every_layer_from_the_payload{"TargetSideResolve/TraditionalStackTakesEveryLayerFromThePayload", [] { auto r = ts::resolve(payload_linux()); - EXPECT_EQ(r.kernelAbi.origin, ts::Origin::Payload); - EXPECT_EQ(r.cAbi.origin, ts::Origin::Payload); - EXPECT_EQ(r.cxx.origin, ts::Origin::Payload); - EXPECT_EQ(r.kernelAbi.interfaceName, "linux"); - EXPECT_EQ(r.cAbi.interfaceName, "glibc"); - EXPECT_EQ(r.cxx.interfaceName, "libc++"); - EXPECT_FALSE(r.system_from_graph()); -} - -TEST(TargetSideResolve, OpenkalCxxTakesEveryLayerFromTheGraph) { + expect(eq(r.kernelAbi.origin, ts::Origin::Payload)); + expect(eq(r.cAbi.origin, ts::Origin::Payload)); + expect(eq(r.cxx.origin, ts::Origin::Payload)); + expect(eq(r.kernelAbi.interfaceName, "linux")); + expect(eq(r.cAbi.interfaceName, "glibc")); + expect(eq(r.cxx.interfaceName, "libc++")); + expect(!r.system_from_graph()); +}}; + +UTest openkal_cxx_takes_every_layer_from_the_graph{"TargetSideResolve/OpenkalCxxTakesEveryLayerFromTheGraph", [] { auto in = payload_linux(); in.llvmTriple = "arm64-apple-macos14.0"; in.targetOs = "macos"; @@ -120,14 +122,14 @@ TEST(TargetSideResolve, OpenkalCxxTakesEveryLayerFromTheGraph) { in.cxxAbi = provider("openkal-llvm-runtime", "0.1.0", "libc++", /*stdModule=*/true); auto r = ts::resolve(in); - EXPECT_EQ(r.kernelAbi.origin, ts::Origin::Graph); - EXPECT_EQ(r.cAbi.origin, ts::Origin::Graph); - EXPECT_EQ(r.cxx.origin, ts::Origin::Graph); - EXPECT_EQ(r.kernelAbi.impl, "openkal-macos@0.3.1"); - EXPECT_EQ(r.cAbi.impl, "openkal-musl@0.3.1"); - EXPECT_FALSE(r.cxx.subset) << "a package that declares a std module supplies the whole library"; - EXPECT_TRUE(r.system_from_graph()); -} + expect(eq(r.kernelAbi.origin, ts::Origin::Graph)); + expect(eq(r.cAbi.origin, ts::Origin::Graph)); + expect(eq(r.cxx.origin, ts::Origin::Graph)); + expect(eq(r.kernelAbi.impl, "openkal-macos@0.3.1")); + expect(eq(r.cAbi.impl, "openkal-musl@0.3.1")); + expect(!r.cxx.subset) << "a package that declares a std module supplies the whole library"; + expect(bool(r.system_from_graph())); +}}; // THE CASE THAT WAS UNREPRESENTABLE, AND THE MEASURED DEFECT. // @@ -136,7 +138,7 @@ TEST(TargetSideResolve, OpenkalCxxTakesEveryLayerFromTheGraph) { // read as "the target side is not from the graph", the payload's own libc++ // stayed on the link line, and a macOS cross ended in // `libc++.so: unhandled file type`. -TEST(TargetSideResolve, PureCOverOpenkalStillHasItsSystemFromTheGraph) { +UTest pure_c_over_openkal_still_has_its_system_from_the_graph{"TargetSideResolve/PureCOverOpenkalStillHasItsSystemFromTheGraph", [] { auto in = payload_linux(); in.llvmTriple = "arm64-apple-macos14.0"; in.targetOs = "macos"; @@ -145,47 +147,47 @@ TEST(TargetSideResolve, PureCOverOpenkalStillHasItsSystemFromTheGraph) { // no cxxAbi provider auto r = ts::resolve(in); - EXPECT_EQ(r.cxx.origin, ts::Origin::None) + expect(eq(r.cxx.origin, ts::Origin::None)) << "a C program needs no C++ runtime, and that is not a failure to find one"; - EXPECT_TRUE(r.system_from_graph()) + expect(bool(r.system_from_graph())) << "the system still comes from the graph, which is the whole defect"; - EXPECT_NE(r.cxx.origin, ts::Origin::Payload) + expect(ne(r.cxx.origin, ts::Origin::Payload)) << "the payload's C++ runtime must not be selected over a graph C library"; -} +}}; // Only the platform implementation, and nothing above it. -TEST(TargetSideResolve, RawOpenkalHasOnlyAKernelAbi) { +UTest raw_openkal_has_only_a_kernel_abi{"TargetSideResolve/RawOpenkalHasOnlyAKernelAbi", [] { auto in = payload_linux(); in.kernelAbi = provider("openkal-linux", "0.5.1", "openkal"); in.sysrootDeclaredEmpty = true; auto r = ts::resolve(in); - EXPECT_EQ(r.kernelAbi.origin, ts::Origin::Graph); - EXPECT_TRUE(r.cAbi.absent()); - EXPECT_TRUE(r.cxx.absent()); -} + expect(eq(r.kernelAbi.origin, ts::Origin::Graph)); + expect(bool(r.cAbi.absent())); + expect(bool(r.cxx.absent())); +}}; -TEST(TargetSideResolve, BareMetalWithPicolibcHasNoKernelAbi) { +UTest bare_metal_with_picolibc_has_no_kernel_abi{"TargetSideResolve/BareMetalWithPicolibcHasNoKernelAbi", [] { ts::Inputs in; in.llvmTriple = "riscv64-none-elf"; in.freestandingTarget = true; in.sysrootXpkg = "xim:picolibc-riscv@1.8.12"; auto r = ts::resolve(in); - EXPECT_TRUE(r.kernelAbi.absent()) + expect(bool(r.kernelAbi.absent())) << "a bare machine has no kernel, and saying so is the information"; - EXPECT_EQ(r.cAbi.origin, ts::Origin::Xpkg); + expect(eq(r.cAbi.origin, ts::Origin::Xpkg)); // The package name, not a prettier form of it. Stripping the `-riscv` // suffix would mean the engine knows how this ecosystem names its // packages, which is exactly the knowledge this design keeps out of it. - EXPECT_EQ(r.cAbi.interfaceName, "picolibc-riscv"); - EXPECT_EQ(r.cAbi.impl, "xim:picolibc-riscv@1.8.12"); - EXPECT_TRUE(r.cxx.absent()); - EXPECT_FALSE(r.system_from_graph()); -} + expect(eq(r.cAbi.interfaceName, "picolibc-riscv")); + expect(eq(r.cAbi.impl, "xim:picolibc-riscv@1.8.12")); + expect(bool(r.cxx.absent())); + expect(!r.system_from_graph()); +}}; // THE MIXED CASE. It already ships, and no single boolean can express it. -TEST(TargetSideResolve, PrebuiltCLibraryUnderAGraphSuppliedCxxSubset) { +UTest prebuilt_c_library_under_a_graph_supplied_cxx_subset{"TargetSideResolve/PrebuiltCLibraryUnderAGraphSuppliedCxxSubset", [] { ts::Inputs in; in.llvmTriple = "riscv64-none-elf"; in.freestandingTarget = true; @@ -193,33 +195,33 @@ TEST(TargetSideResolve, PrebuiltCLibraryUnderAGraphSuppliedCxxSubset) { in.cxxAbi = provider("std-freestanding", "0.5.0", "freestanding subset"); auto r = ts::resolve(in); - EXPECT_EQ(r.cAbi.origin, ts::Origin::Xpkg) << "prebuilt"; - EXPECT_EQ(r.cxx.origin, ts::Origin::Graph) << "composed at build time"; - EXPECT_TRUE(r.cxx.subset) << "no std module declared, so the library is a subset"; -} + expect(eq(r.cAbi.origin, ts::Origin::Xpkg)) << "prebuilt"; + expect(eq(r.cxx.origin, ts::Origin::Graph)) << "composed at build time"; + expect(bool(r.cxx.subset)) << "no std module declared, so the library is a subset"; +}}; // A hosted target whose C library is the payload's (a located SDK, or glibc) // while a package supplies the whole standard library: the iOS rows over // `llvm.libcxx` (mcpp#630). The C++ layer is the graph's and is not a subset, // and the C library stays the payload's; nothing about one decides the other. -TEST(TargetSideResolve, PrebuiltCLibraryUnderAGraphSuppliedLibcxx) { +UTest prebuilt_c_library_under_a_graph_supplied_libcxx{"TargetSideResolve/PrebuiltCLibraryUnderAGraphSuppliedLibcxx", [] { auto in = payload_linux(); in.cxxAbi = provider("libcxx", "22.1.8.1", "libc++", /*stdModule=*/true); auto r = ts::resolve(in); - EXPECT_EQ(r.cAbi.origin, ts::Origin::Payload) << "the SDK's or glibc's, prebuilt"; - EXPECT_TRUE(r.cAbi.prebuilt()); - EXPECT_EQ(r.cxx.origin, ts::Origin::Graph); - EXPECT_TRUE(r.cxx.fromGraph()); - EXPECT_FALSE(r.cxx.subset) << "a std module is declared, so this is the whole library"; - EXPECT_FALSE(r.system_from_graph()) << "the system is still the payload's"; -} + expect(eq(r.cAbi.origin, ts::Origin::Payload)) << "the SDK's or glibc's, prebuilt"; + expect(bool(r.cAbi.prebuilt())); + expect(eq(r.cxx.origin, ts::Origin::Graph)); + expect(bool(r.cxx.fromGraph())); + expect(!r.cxx.subset) << "a std module is declared, so this is the whole library"; + expect(!r.system_from_graph()) << "the system is still the payload's"; +}}; // The payload has no compiler runtime for this platform and the graph // declares none: the layer is absent, and the report says so rather than // naming the compiler's family for an archive that does not exist. With a // graph provider the same input resolves to the graph. -TEST(TargetSideResolve, APayloadWithoutACompilerRuntimeForThePlatform) { +UTest a_payload_without_a_compiler_runtime_for_the_platform{"TargetSideResolve/APayloadWithoutACompilerRuntimeForThePlatform", [] { auto in = payload_linux(); in.llvmTriple = "arm64-apple-ios18.0-simulator"; in.targetOs = "ios"; @@ -228,72 +230,72 @@ TEST(TargetSideResolve, APayloadWithoutACompilerRuntimeForThePlatform) { in.payloadCompilerRuntimeAbsent = true; auto absent = ts::resolve(in); - EXPECT_TRUE(absent.compilerRuntime.absent()); + expect(bool(absent.compilerRuntime.absent())); in.compilerRuntime = provider("compiler-rt-builtins", "22.1.8.3", "compiler-rt"); auto supplied = ts::resolve(in); - EXPECT_EQ(supplied.compilerRuntime.origin, ts::Origin::Graph); - EXPECT_FALSE(supplied.compilerRuntime.absent()); + expect(eq(supplied.compilerRuntime.origin, ts::Origin::Graph)); + expect(!supplied.compilerRuntime.absent()); // Negative direction: a payload that has the archive reports its own. in.compilerRuntime.reset(); in.payloadCompilerRuntimeAbsent = false; auto own = ts::resolve(in); - EXPECT_EQ(own.compilerRuntime.origin, ts::Origin::Payload); -} + expect(eq(own.compilerRuntime.origin, ts::Origin::Payload)); +}}; -TEST(TargetSideResolve, ZeroLibcTierHasNothingAtAll) { +UTest zero_libc_tier_has_nothing_at_all{"TargetSideResolve/ZeroLibcTierHasNothingAtAll", [] { ts::Inputs in; in.llvmTriple = "x86_64-none-elf"; in.freestandingTarget = true; in.sysrootDeclaredEmpty = true; auto r = ts::resolve(in); - EXPECT_TRUE(r.kernelAbi.absent()); - EXPECT_TRUE(r.cAbi.absent()); - EXPECT_TRUE(r.cxx.absent()); -} + expect(bool(r.kernelAbi.absent())); + expect(bool(r.cAbi.absent())); + expect(bool(r.cxx.absent())); +}}; // ── The layering rule ──────────────────────────────────────────────────────── // // An implementation must have been configured for the layer beneath it. The // resolver enforces it by construction; this asserts that the construction // really does, rather than that a later check catches it. -TEST(TargetSideResolve, PayloadCxxIsNotSelectedOverANonPayloadCLibrary) { +UTest payload_cxx_is_not_selected_over_a_non_payload_c_library{"TargetSideResolve/PayloadCxxIsNotSelectedOverANonPayloadCLibrary", [] { for (auto make : std::initializer_list>{ [] { auto in = payload_linux(); in.sysrootXpkg = "xim:picolibc-riscv@1.8.12"; return in; }, [] { auto in = payload_linux(); in.cAbi = provider("openkal-musl", "0.3.1", "musl"); return in; }}) { auto r = ts::resolve(make()); - EXPECT_NE(r.cxx.origin, ts::Origin::Payload); - EXPECT_EQ(ts::check_layering(r), std::nullopt) + expect(ne(r.cxx.origin, ts::Origin::Payload)); + expect(eq(ts::check_layering(r), std::nullopt)) << "the default path must not be able to construct the violation"; } -} +}}; -TEST(TargetSideResolve, TheLayeringRuleIsStatedForTheOverridePath) { +UTest the_layering_rule_is_stated_for_the_override_path{"TargetSideResolve/TheLayeringRuleIsStatedForTheOverridePath", [] { ts::TargetSide bad; bad.cAbi = { ts::Origin::Graph, "musl", "openkal-musl@0.3.1", false }; bad.cxx = { ts::Origin::Payload, "libc++", "xim-x-llvm@22.1.8", false }; auto why = ts::check_layering(bad); - ASSERT_TRUE(why.has_value()); - EXPECT_NE(why->find("never configured for this one"), std::string::npos) + expect(fatal(why.has_value())); + expect(ne(why->find("never configured for this one"), std::string::npos)) << "the diagnostic must state the reason, not merely the combination"; -} +}}; // ── The report ─────────────────────────────────────────────────────────────── -TEST(TargetSideReport, ShowsTheTranslatedTripleWhenItDiffers) { +UTest shows_the_translated_triple_when_it_differs{"TargetSideReport/ShowsTheTranslatedTripleWhenItDiffers", [] { auto in = payload_linux(); in.llvmTriple = "arm64-apple-macos14.0"; auto text = ts::format_report(ts::resolve(in), "aarch64-macos"); - EXPECT_NE(text.find("aarch64-macos → arm64-apple-macos14.0"), std::string::npos) + expect(ne(text.find("aarch64-macos → arm64-apple-macos14.0"), std::string::npos)) << "the translation is load bearing: the untranslated form emits a Mach-O " "whose MinVersion carries no platform"; -} +}}; -TEST(TargetSideReport, AbsentLayersReadAsADashRatherThanBeingOmitted) { +UTest absent_layers_read_as_a_dash_rather_than_being_omitted{"TargetSideReport/AbsentLayersReadAsADashRatherThanBeingOmitted", [] { ts::Inputs in; in.llvmTriple = "x86_64-none-elf"; in.freestandingTarget = true; @@ -301,38 +303,38 @@ TEST(TargetSideReport, AbsentLayersReadAsADashRatherThanBeingOmitted) { in.compilerFamily = "llvm"; auto text = ts::format_report(ts::resolve(in), "x86_64-none-elf", /*verbose=*/true); - EXPECT_NE(text.find("kernel-abi"), std::string::npos); - EXPECT_NE(text.find("c-abi"), std::string::npos); - EXPECT_NE(text.find("c++-abi"), std::string::npos); - EXPECT_NE(text.find("—"), std::string::npos); -} + expect(ne(text.find("kernel-abi"), std::string::npos)); + expect(ne(text.find("c-abi"), std::string::npos)); + expect(ne(text.find("c++-abi"), std::string::npos)); + expect(ne(text.find("—"), std::string::npos)); +}}; -TEST(TargetSideReport, NamesInterfaceAndImplementationSeparately) { +UTest names_interface_and_implementation_separately{"TargetSideReport/NamesInterfaceAndImplementationSeparately", [] { auto in = payload_linux(); in.kernelAbi = provider("openkal-opensbi", "0.1.0", "openkal"); auto text = ts::format_report(ts::resolve(in), "riscv64-none-elf"); - EXPECT_NE(text.find("openkal"), std::string::npos); - EXPECT_NE(text.find("openkal-opensbi@0.1.0"), std::string::npos) + expect(ne(text.find("openkal"), std::string::npos)); + expect(ne(text.find("openkal-opensbi@0.1.0"), std::string::npos)) << "four packages answer to one interface name; collapsing them would " "hide why one source reaches four machines"; - EXPECT_NE(text.find("graph"), std::string::npos); -} + expect(ne(text.find("graph"), std::string::npos)); +}}; // ── What earns a line ──────────────────────────────────────────────────────── // // A zero-configuration build resolves every layer from one compiler payload, // and five lines reading `(payload)` answer a question nobody asked. What earns // a line is a layer that came from somewhere else. -TEST(TargetSideReport, AZeroConfigurationBuildPrintsOnlyTheTarget) { +UTest a_zero_configuration_build_prints_only_the_target{"TargetSideReport/AZeroConfigurationBuildPrintsOnlyTheTarget", [] { auto in = payload_linux(); in.compilerFamily = "llvm"; in.compilerVersion = "22.1.8"; auto text = ts::format_report(ts::resolve(in), "x86_64-linux-gnu"); - EXPECT_EQ(text.find('\n'), std::string::npos) + expect(eq(text.find('\n'), std::string::npos)) << "every layer came from the payload, so none of them is news:\n" << text; -} +}}; -TEST(TargetSideReport, VerbosePrintsAllFiveIncludingThePayloadOnes) { +UTest verbose_prints_all_five_including_the_payload_ones{"TargetSideReport/VerbosePrintsAllFiveIncludingThePayloadOnes", [] { auto in = payload_linux(); in.compilerFamily = "llvm"; in.compilerVersion = "22.1.8"; @@ -340,34 +342,34 @@ TEST(TargetSideReport, VerbosePrintsAllFiveIncludingThePayloadOnes) { /*verbose=*/true); for (auto* label : {"compiler", "compiler-runtime", "kernel-abi", "c-abi", "c++-abi"}) - EXPECT_NE(text.find(label), std::string::npos) << label; -} + expect(ne(text.find(label), std::string::npos)) << label; +}}; // An absent layer is a statement, and whether the stack is shown at all must not // depend on where that statement sits among the rows. A bare-metal build has an // absent kernel interface ABOVE a prebuilt C library; deciding per row swallowed // the first and printed the second. -TEST(TargetSideReport, AnAbsentLayerAboveAPrintedOneIsStillPrinted) { +UTest an_absent_layer_above_a_printed_one_is_still_printed{"TargetSideReport/AnAbsentLayerAboveAPrintedOneIsStillPrinted", [] { ts::Inputs in; in.llvmTriple = "riscv64-none-elf"; in.freestandingTarget = true; in.compilerFamily = "llvm"; in.sysrootXpkg = "xim:picolibc-riscv@1.8.12"; auto text = ts::format_report(ts::resolve(in), "riscv64-none-elf"); - EXPECT_NE(text.find("c-abi"), std::string::npos) << text; - EXPECT_NE(text.find("kernel-abi"), std::string::npos) + expect(ne(text.find("c-abi"), std::string::npos)) << text; + expect(ne(text.find("kernel-abi"), std::string::npos)) << "the kernel interface is absent, which is the information:\n" << text; -} +}}; -TEST(TargetSideReport, OnlyTheLayersThatCameFromElsewhereEarnALine) { +UTest only_the_layers_that_came_from_elsewhere_earn_a_line{"TargetSideReport/OnlyTheLayersThatCameFromElsewhereEarnALine", [] { auto in = payload_linux(); in.compilerFamily = "llvm"; in.cxxAbi = provider("openkal-llvm-runtime", "0.1.1", "libc++"); auto text = ts::format_report(ts::resolve(in), "x86_64-linux-gnu"); - EXPECT_NE(text.find("c++-abi"), std::string::npos); - EXPECT_EQ(text.find("kernel-abi"), std::string::npos) + expect(ne(text.find("c++-abi"), std::string::npos)); + expect(eq(text.find("kernel-abi"), std::string::npos)) << "the kernel interface is the payload's, which is not news"; -} +}}; // ── The five layers ────────────────────────────────────────────────────────── @@ -375,81 +377,81 @@ TEST(TargetSideReport, OnlyTheLayersThatCameFromElsewhereEarnALine) { // one. macOS has none, and falling back to `glibc` named a library that does not // exist on the platform — invisible while the report printed only the layers a // build had something to say about. -TEST(TargetSideResolve, ThePayloadCLibraryIsNamedForItsPlatform) { +UTest the_payload_c_library_is_named_for_its_platform{"TargetSideResolve/ThePayloadCLibraryIsNamedForItsPlatform", [] { auto mac = payload_linux(); mac.targetOs = "macos"; mac.targetEnv = ""; - EXPECT_EQ(ts::resolve(mac).cAbi.interfaceName, "libSystem"); + expect(eq(ts::resolve(mac).cAbi.interfaceName, "libSystem")); auto win = payload_linux(); win.targetOs = "windows"; win.targetEnv = ""; - EXPECT_EQ(ts::resolve(win).cAbi.interfaceName, "ucrt"); + expect(eq(ts::resolve(win).cAbi.interfaceName, "ucrt")); // #540: the layer names the LIBRARY, never the triple's env segment. `musl` // is both spellings at once and hid the difference; `gnu` is not the name of // any C library, and on Windows it does not even mean the same one. - EXPECT_EQ(ts::resolve(payload_linux()).cAbi.interfaceName, "glibc") + expect(eq(ts::resolve(payload_linux()).cAbi.interfaceName, "glibc")) << "the env segment `gnu` asks for a C library; the layer answers glibc"; auto winGnu = payload_linux(); winGnu.targetOs = "windows"; winGnu.targetEnv = "gnu"; - EXPECT_EQ(ts::resolve(winGnu).cAbi.interfaceName, "ucrt") + expect(eq(ts::resolve(winGnu).cAbi.interfaceName, "ucrt")) << "on Windows `-gnu` names the MinGW toolchain flavour, not glibc"; auto musl = payload_linux(); musl.targetEnv = "musl"; - EXPECT_EQ(ts::resolve(musl).cAbi.interfaceName, "musl") + expect(eq(ts::resolve(musl).cAbi.interfaceName, "musl")) << "the one env segment that IS a C library name is unchanged"; -} +}}; // The rename above must not turn every ordinary `-gnu` build into a reported // request mismatch: the request keeps the triple's spelling by definition // (docs/specs/target-side.md §3.4) and the answer now names the library. -TEST(TargetSideResolve, TheEnvRequestIsSatisfiedByTheLibraryItNames) { - EXPECT_TRUE(ts::c_abi_request_satisfied("gnu", "glibc")); - EXPECT_TRUE(ts::c_abi_request_satisfied("gnu", "ucrt")); - EXPECT_TRUE(ts::c_abi_request_satisfied("musl", "musl")); - EXPECT_TRUE(ts::c_abi_request_satisfied("glibc", "gnu")); - EXPECT_FALSE(ts::c_abi_request_satisfied("gnu", "musl")); - EXPECT_FALSE(ts::c_abi_request_satisfied("musl", "glibc")); -} - -TEST(TargetSideResolve, TheCompilerIsALayerAndItIsAlwaysThePayloads) { +UTest the_env_request_is_satisfied_by_the_library_it_names{"TargetSideResolve/TheEnvRequestIsSatisfiedByTheLibraryItNames", [] { + expect(bool(ts::c_abi_request_satisfied("gnu", "glibc"))); + expect(bool(ts::c_abi_request_satisfied("gnu", "ucrt"))); + expect(bool(ts::c_abi_request_satisfied("musl", "musl"))); + expect(bool(ts::c_abi_request_satisfied("glibc", "gnu"))); + expect(!ts::c_abi_request_satisfied("gnu", "musl")); + expect(!ts::c_abi_request_satisfied("musl", "glibc")); +}}; + +UTest the_compiler_is_a_layer_and_it_is_always_the_payloads{"TargetSideResolve/TheCompilerIsALayerAndItIsAlwaysThePayloads", [] { auto in = payload_linux(); in.compilerFamily = "gcc"; in.compilerVersion = "16.1.0"; auto r = ts::resolve(in); - EXPECT_EQ(r.compiler.origin, ts::Origin::Payload); - EXPECT_EQ(r.compiler.interfaceName, "gcc"); - EXPECT_EQ(r.compiler.impl, "16.1.0"); -} + expect(eq(r.compiler.origin, ts::Origin::Payload)); + expect(eq(r.compiler.interfaceName, "gcc")); + expect(eq(r.compiler.impl, "16.1.0")); +}}; // The builtins are what a PURE C PROGRAM needs. A package supplies them only // when the payload's own are wrong for the target; absent from the graph means // the payload's, not absent. -TEST(TargetSideResolve, TheCompilerRuntimeDefaultsToTheCompilersOwn) { +UTest the_compiler_runtime_defaults_to_the_compilers_own{"TargetSideResolve/TheCompilerRuntimeDefaultsToTheCompilersOwn", [] { auto in = payload_linux(); in.compilerFamily = "llvm"; auto r = ts::resolve(in); - EXPECT_EQ(r.compilerRuntime.origin, ts::Origin::Payload); - EXPECT_EQ(r.compilerRuntime.interfaceName, "llvm"); + expect(eq(r.compilerRuntime.origin, ts::Origin::Payload)); + expect(eq(r.compilerRuntime.interfaceName, "llvm")); in.compilerRuntime = provider("openkal-llvm-runtime", "0.1.1", "compiler-rt"); auto g = ts::resolve(in); - EXPECT_EQ(g.compilerRuntime.origin, ts::Origin::Graph); - EXPECT_EQ(g.compilerRuntime.interfaceName, "compiler-rt"); -} + expect(eq(g.compilerRuntime.origin, ts::Origin::Graph)); + expect(eq(g.compilerRuntime.interfaceName, "compiler-rt")); +}}; -TEST(TargetSideCapability, TheCompilerIsALayerNoPackageMaySupply) { - EXPECT_FALSE(ts::layer_is_suppliable_by_package(ts::CapLayer::Compiler)); +UTest the_compiler_is_a_layer_no_package_may_supply{"TargetSideCapability/TheCompilerIsALayerNoPackageMaySupply", [] { + expect(!ts::layer_is_suppliable_by_package(ts::CapLayer::Compiler)); for (auto l : {ts::CapLayer::CompilerRuntime, ts::CapLayer::KernelAbi, ts::CapLayer::CAbi, ts::CapLayer::CxxAbi}) - EXPECT_TRUE(ts::layer_is_suppliable_by_package(l)); -} + expect(ts::layer_is_suppliable_by_package(l)); +}}; -TEST(TargetSideCapability, TheGrammarKnowsAllFiveLayers) { +UTest the_grammar_knows_all_five_layers{"TargetSideCapability/TheGrammarKnowsAllFiveLayers", [] { struct Row { const char* entry; ts::CapLayer layer; const char* iface; }; for (auto const& row : std::initializer_list{ {"mcpp:compiler=llvm", ts::CapLayer::Compiler, "llvm"}, @@ -458,22 +460,22 @@ TEST(TargetSideCapability, TheGrammarKnowsAllFiveLayers) { {"mcpp:c-abi=musl", ts::CapLayer::CAbi, "musl"}, {"mcpp:c++-abi=libc++", ts::CapLayer::CxxAbi, "libc++"}}) { auto parsed = ts::parse_capability(row.entry); - ASSERT_TRUE(parsed.has_value()) << row.entry; - ASSERT_TRUE(parsed->has_value()) << row.entry; - EXPECT_EQ((*parsed)->layer, row.layer) << row.entry; - EXPECT_EQ((*parsed)->interfaceName, row.iface) << row.entry; + expect(fatal(parsed.has_value())) << row.entry; + expect(fatal(parsed->has_value())) << row.entry; + expect(eq((*parsed)->layer, row.layer)) << row.entry; + expect(eq((*parsed)->interfaceName, row.iface)) << row.entry; } // Outside the reserved prefix the feature system owns the name. auto own = ts::parse_capability("freestanding-allocator"); - ASSERT_TRUE(own.has_value()); - EXPECT_FALSE(own->has_value()); + expect(fatal(own.has_value())); + expect(!own->has_value()); // Inside it, a misspelling is an error rather than a silently dead check. - EXPECT_FALSE(ts::parse_capability("mcpp:c_abi=musl").has_value()); -} + expect(!(ts::parse_capability("mcpp:c_abi=musl").has_value())); +}}; // ── The triple is a request; the target side is the fact ──────────────────── -TEST(TargetSideRequest, AFilledEnvSegmentStatesNothing) { +UTest a_filled_env_segment_states_nothing{"TargetSideRequest/AFilledEnvSegmentStatesNothing", [] { auto in = payload_linux(); in.compilerFamily = "llvm"; in.cAbi = provider("openkal-musl", "0.3.3", "musl"); @@ -481,14 +483,14 @@ TEST(TargetSideRequest, AFilledEnvSegmentStatesNothing) { // `--target x86_64-linux` — the project declined to name a C library, so // the parser's fill must not become a claim it can be held to. in.requestedCAbi.clear(); - EXPECT_EQ(ts::check_request(ts::resolve(in)), std::nullopt); -} + expect(eq(ts::check_request(ts::resolve(in)), std::nullopt)); +}}; // REPORTED, NOT REFUSED. The graph supplies the C library either way, so the // segment is ignored rather than violated and the artifact is identical with or // without it. Refusing was tried and broke every project spelling the host // target `x86_64-linux-gnu` — which is what `mcpp toolchain list` prints. -TEST(TargetSideRequest, AWrittenEnvSegmentThatDisagreesIsReported) { +UTest a_written_env_segment_that_disagrees_is_reported{"TargetSideRequest/AWrittenEnvSegmentThatDisagreesIsReported", [] { auto in = payload_linux(); in.compilerFamily = "llvm"; in.cAbi = provider("openkal-musl", "0.3.3", "musl"); @@ -496,23 +498,23 @@ TEST(TargetSideRequest, AWrittenEnvSegmentThatDisagreesIsReported) { in.requestFreeTarget = "x86_64-linux"; in.envAxis = ts::EnvAxis::CLibrary; auto why = ts::check_request(ts::resolve(in)); - ASSERT_TRUE(why.has_value()); - EXPECT_NE(why->find("`gnu`"), std::string::npos); - EXPECT_NE(why->find("musl"), std::string::npos); - EXPECT_NE(why->find("--target x86_64-linux"), std::string::npos) + expect(fatal(why.has_value())); + expect(ne(why->find("`gnu`"), std::string::npos)); + expect(ne(why->find("musl"), std::string::npos)); + expect(ne(why->find("--target x86_64-linux"), std::string::npos)) << "telling someone their target name is wrong is only useful once " "there is a right one to give them"; -} +}}; -TEST(TargetSideRequest, APrebuiltCLibraryIsWhatTheRequestSelected) { +UTest a_prebuilt_c_library_is_what_the_request_selected{"TargetSideRequest/APrebuiltCLibraryIsWhatTheRequestSelected", [] { auto in = payload_linux(); in.compilerFamily = "llvm"; in.requestedCAbi = "musl"; in.envAxis = ts::EnvAxis::CLibrary; // No graph supplier: the payload's C library IS the request's answer, so // there is nothing to contradict even when the names differ. - EXPECT_EQ(ts::check_request(ts::resolve(in)), std::nullopt); -} + expect(eq(ts::check_request(ts::resolve(in)), std::nullopt)); +}}; // On Windows the same segment names the OBJECT ABI — `gnu` is PE with the // GNU ABI, `msvc` is PE with Microsoft's — and both are compatible with more @@ -527,7 +529,7 @@ TEST(TargetSideRequest, APrebuiltCLibraryIsWhatTheRequestSelected) { // C library replaces. The second half is a name/fact disagreement of exactly // the shape this function reports, and it went unreported — while the identical // shape on Linux warned. -TEST(TargetSideRequest, TheObjectAbiAxisReportsTheCLibraryHalfToo) { +UTest the_object_abi_axis_reports_the_c_library_half_too{"TargetSideRequest/TheObjectAbiAxisReportsTheCLibraryHalfToo", [] { auto in = payload_linux(); in.targetOs = "windows"; in.compilerFamily = "llvm"; @@ -537,18 +539,18 @@ TEST(TargetSideRequest, TheObjectAbiAxisReportsTheCLibraryHalfToo) { in.envAxis = ts::EnvAxis::ObjectAbi; auto why = ts::check_request(ts::resolve(in)); - ASSERT_NE(why, std::nullopt); - EXPECT_NE(why->find("musl"), std::string::npos) << *why; + expect(fatal(ne(why, std::nullopt))); + if (auto&& failed = expect(ne(why->find("musl"), std::string::npos)); !failed) failed << *why; // And it says the ABI half was honoured, so the reader does not conclude // the object ABI changed as well. - EXPECT_NE(why->find("object ABI"), std::string::npos) << *why; - EXPECT_NE(why->find("--target x86_64-windows"), std::string::npos) << *why; -} + expect(ne(why->find("object ABI"), std::string::npos)) << *why; + if (auto&& failed = expect(ne(why->find("--target x86_64-windows"), std::string::npos)); !failed) failed << *why; +}}; // The object-FORMAT axis stays exempt, and not for symmetry: `elf` names no C // library on any platform, so "asks for the `elf` C ABI" would be nonsense // rather than merely noisy. -TEST(TargetSideRequest, TheObjectFormatAxisStaysExempt) { +UTest the_object_format_axis_stays_exempt{"TargetSideRequest/TheObjectFormatAxisStaysExempt", [] { auto in = payload_linux(); in.targetOs = "none"; in.compilerFamily = "llvm"; @@ -556,8 +558,8 @@ TEST(TargetSideRequest, TheObjectFormatAxisStaysExempt) { in.requestedCAbi = "elf"; in.requestFreeTarget = "riscv64-none"; in.envAxis = ts::EnvAxis::ObjectFormat; - EXPECT_EQ(ts::check_request(ts::resolve(in)), std::nullopt); -} + expect(eq(ts::check_request(ts::resolve(in)), std::nullopt)); +}}; // AND WHEN IT DOES NOT NAME ONE, THE REPORT SAYS WHAT IT DOES NAME. // @@ -568,7 +570,7 @@ TEST(TargetSideRequest, TheObjectFormatAxisStaysExempt) { // THE GLOSS IS NOW THE OBJECT-FORMAT AXIS ALONE. The object-ABI axis warns // instead (see `TheObjectAbiAxisReportsTheCLibraryHalfToo`), and leaving both // in place would state one finding twice — once as an aside, once as a warning. -TEST(TargetSideReport, TheObjectAbiAxisIsNoLongerGlossed) { +UTest the_object_abi_axis_is_no_longer_glossed{"TargetSideReport/TheObjectAbiAxisIsNoLongerGlossed", [] { auto in = payload_linux(); in.targetOs = "windows"; in.compilerFamily = "llvm"; @@ -578,12 +580,12 @@ TEST(TargetSideReport, TheObjectAbiAxisIsNoLongerGlossed) { in.envAxis = ts::EnvAxis::ObjectAbi; auto r = ts::format_report(ts::resolve(in), "x86_64-windows-gnu"); - EXPECT_EQ(r.find("not a C library"), std::string::npos) << r; -} + expect(eq(r.find("not a C library"), std::string::npos)) << r; +}}; // A bare-metal target's segment names the object FORMAT, and the same gloss // applies with a different noun. One enum, three platforms, no `#if`. -TEST(TargetSideReport, OnBareMetalTheSegmentNamesTheObjectFormat) { +UTest on_bare_metal_the_segment_names_the_object_format{"TargetSideReport/OnBareMetalTheSegmentNamesTheObjectFormat", [] { auto in = payload_linux(); in.targetOs = "none"; in.compilerFamily = "llvm"; @@ -593,16 +595,15 @@ TEST(TargetSideReport, OnBareMetalTheSegmentNamesTheObjectFormat) { in.envAxis = ts::EnvAxis::ObjectFormat; auto r = ts::format_report(ts::resolve(in), "riscv64-none-elf"); - EXPECT_NE(r.find("elf selects the object format, not a C library"), - std::string::npos) << r; -} + expect(ne(r.find("elf selects the object format, not a C library"), std::string::npos)) << r; +}}; // AND IT DOES NOT FIRE WHEN THE C LIBRARY CAME FROM A PAYLOAD. // // A payload C library IS what the triple selected — the triple is how it was // selected — so `gnu → ucrt` follows visibly and a gloss would be noise on // every ordinary Windows build. -TEST(TargetSideReport, NoGlossWhenTheTripleItselfChoseTheCLibrary) { +UTest no_gloss_when_the_triple_itself_chose_the_c_library{"TargetSideReport/NoGlossWhenTheTripleItselfChoseTheCLibrary", [] { auto in = payload_linux(); in.targetOs = "windows"; in.compilerFamily = "llvm"; @@ -612,13 +613,13 @@ TEST(TargetSideReport, NoGlossWhenTheTripleItselfChoseTheCLibrary) { // cAbi left as the payload's — not from the graph. auto r = ts::format_report(ts::resolve(in), "x86_64-windows-gnu"); - EXPECT_EQ(r.find("not a C library"), std::string::npos) << r; -} + expect(eq(r.find("not a C library"), std::string::npos)) << r; +}}; // On Linux the segment DOES name a C library, so there is nothing to gloss — // the disagreement is reported as a warning instead, which is a different // mechanism and must not double up. -TEST(TargetSideReport, NoGlossWhereTheSegmentNamesACLibrary) { +UTest no_gloss_where_the_segment_names_ac_library{"TargetSideReport/NoGlossWhereTheSegmentNamesACLibrary", [] { auto in = payload_linux(); in.compilerFamily = "llvm"; in.cAbi = provider("openkal-musl", "0.3.3", "musl"); @@ -627,13 +628,13 @@ TEST(TargetSideReport, NoGlossWhereTheSegmentNamesACLibrary) { in.envAxis = ts::EnvAxis::CLibrary; auto r = ts::format_report(ts::resolve(in), "x86_64-linux-gnu"); - EXPECT_EQ(r.find("not a C library"), std::string::npos) << r; - EXPECT_NE(ts::check_request(ts::resolve(in)), std::nullopt); -} + expect(eq(r.find("not a C library"), std::string::npos)) << r; + expect(ne(ts::check_request(ts::resolve(in)), std::nullopt)); +}}; // ── Rule two: declared requirements ────────────────────────────────────────── -TEST(TargetSideRequirements, ARequirementIsCheckedAgainstWhatResolved) { +UTest a_requirement_is_checked_against_what_resolved{"TargetSideRequirements/ARequirementIsCheckedAgainstWhatResolved", [] { auto in = payload_linux(); in.compilerFamily = "gcc"; in.cxxAbi = provider("acme-runtime", "0.1.0", "libc++"); @@ -642,9 +643,9 @@ TEST(TargetSideRequirements, ARequirementIsCheckedAgainstWhatResolved) { std::vector reqs{ {"acme-runtime@0.1.0", ts::CapLayer::Compiler, "llvm"}}; auto why = ts::check_requirements(r, reqs); - ASSERT_TRUE(why.has_value()); - EXPECT_NE(why->find("requires the compiler to be `llvm`"), std::string::npos); - EXPECT_NE(why->find("default = \"llvm\""), std::string::npos) + expect(fatal(why.has_value())); + expect(ne(why->find("requires the compiler to be `llvm`"), std::string::npos)); + expect(ne(why->find("default = \"llvm\""), std::string::npos)) << "a diagnostic that names no next step is a diagnostic the reader " "must still go and research"; // AND THE STEP IT NAMES MUST NOT BE A GLOBAL ONE. Until 2026.8.26.2 the @@ -653,22 +654,22 @@ TEST(TargetSideRequirements, ARequirementIsCheckedAgainstWhatResolved) { // asked. mcpp now applies the graph's requirement itself wherever its own // answer was revisable, so a global change is both unnecessary and, in the // only case that still reaches here, ineffective. - EXPECT_EQ(why->find("mcpp toolchain default"), std::string::npos) + expect(eq(why->find("mcpp toolchain default"), std::string::npos)) << "the remedy must stay inside the project that has the problem"; // With the caller naming where the compiler was stated, the advice points // at that statement instead of at the generic pair of spellings. auto stated = ts::check_requirements(r, reqs, "[toolchain] in mcpp.toml"); - ASSERT_TRUE(stated.has_value()); - EXPECT_NE(stated->find("[toolchain] in mcpp.toml"), std::string::npos); - EXPECT_NE(stated->find("remove it"), std::string::npos) + expect(fatal(stated.has_value())); + expect(ne(stated->find("[toolchain] in mcpp.toml"), std::string::npos)); + expect(ne(stated->find("remove it"), std::string::npos)) << "removing the statement is what lets mcpp take the graph's compiler"; in.compilerFamily = "llvm"; - EXPECT_EQ(ts::check_requirements(ts::resolve(in), reqs), std::nullopt); -} + expect(eq(ts::check_requirements(ts::resolve(in), reqs), std::nullopt)); +}}; -TEST(TargetSideRequirements, AnUnsuppliedLayerIsNamedAsSuch) { +UTest an_unsupplied_layer_is_named_as_such{"TargetSideRequirements/AnUnsuppliedLayerIsNamedAsSuch", [] { auto in = payload_linux(); in.compilerFamily = "llvm"; in.freestandingTarget = true; @@ -676,28 +677,28 @@ TEST(TargetSideRequirements, AnUnsuppliedLayerIsNamedAsSuch) { std::vector reqs{ {"acme-board@0.1.0", ts::CapLayer::CAbi, "picolibc"}}; auto why = ts::check_requirements(ts::resolve(in), reqs); - ASSERT_TRUE(why.has_value()); - EXPECT_NE(why->find("nothing supplies that layer"), std::string::npos); - EXPECT_NE(why->find("mcpp:c-abi=picolibc"), std::string::npos); -} + expect(fatal(why.has_value())); + expect(ne(why->find("nothing supplies that layer"), std::string::npos)); + expect(ne(why->find("mcpp:c-abi=picolibc"), std::string::npos)); +}}; // ── Rule one: one supplier per layer ───────────────────────────────────────── -TEST(TargetSideConflict, TwoSuppliersAreNamedTogetherWithHowEachArrived) { +UTest two_suppliers_are_named_together_with_how_each_arrived{"TargetSideConflict/TwoSuppliersAreNamedTogetherWithHowEachArrived", [] { ts::Conflict c; c.layer = ts::CapLayer::CAbi; c.first = "mcpplibs/openkal-musl@0.3.3"; c.firstVia = "a transitive dependency"; c.second = "acme/tinylibc@0.2.0"; auto text = ts::format_conflict(c); - EXPECT_NE(text.find("openkal-musl@0.3.3"), std::string::npos); - EXPECT_NE(text.find("tinylibc@0.2.0"), std::string::npos); - EXPECT_NE(text.find("a transitive dependency"), std::string::npos); - EXPECT_NE(text.find("a direct dependency"), std::string::npos); - EXPECT_NE(text.find("choice rather than a contribution"), std::string::npos) + expect(ne(text.find("openkal-musl@0.3.3"), std::string::npos)); + expect(ne(text.find("tinylibc@0.2.0"), std::string::npos)); + expect(ne(text.find("a transitive dependency"), std::string::npos)); + expect(ne(text.find("a direct dependency"), std::string::npos)); + expect(ne(text.find("choice rather than a contribution"), std::string::npos)) << "the reason matters: choosing wrong does not fail the link, it " "produces a program that runs and occasionally does not"; -} +}}; // ── Which layer decides the payload's C-library flags ─────────────────────── // @@ -733,38 +734,38 @@ mcpp::targetside::TargetSide side_with(mcpp::targetside::Origin cAbiOrigin, } } -TEST(TargetSideCLibrary, ThePayloadsCLibraryIsPrebuilt) { +UTest the_payloads_c_library_is_prebuilt{"TargetSideCLibrary/ThePayloadsCLibraryIsPrebuilt", [] { // The shape that failed: the graph supplies the kernel interface and the C // library is still the payload's, so the payload's flags must survive. - EXPECT_TRUE(side_with(mcpp::targetside::Origin::Payload).cAbi.prebuilt()); -} + expect(bool(side_with(mcpp::targetside::Origin::Payload).cAbi.prebuilt())); +}}; -TEST(TargetSideCLibrary, AnXpkgSysrootIsPrebuiltToo) { +UTest an_xpkg_sysroot_is_prebuilt_too{"TargetSideCLibrary/AnXpkgSysrootIsPrebuiltToo", [] { // The value a hand-written `fromGraph() || absent()` was silent about. A // sysroot from an xpkg is a directory that existed before resolution, like // a payload and unlike a package. - EXPECT_TRUE(side_with(mcpp::targetside::Origin::Xpkg).cAbi.prebuilt()); -} + expect(bool(side_with(mcpp::targetside::Origin::Xpkg).cAbi.prebuilt())); +}}; -TEST(TargetSideCLibrary, AGraphBuiltCLibraryIsNotPrebuilt) { - EXPECT_FALSE(side_with(mcpp::targetside::Origin::Graph).cAbi.prebuilt()); -} +UTest a_graph_built_c_library_is_not_prebuilt{"TargetSideCLibrary/AGraphBuiltCLibraryIsNotPrebuilt", [] { + expect(!(side_with(mcpp::targetside::Origin::Graph).cAbi.prebuilt())); +}}; -TEST(TargetSideCLibrary, NoCLibraryIsNotPrebuilt) { +UTest no_c_library_is_not_prebuilt{"TargetSideCLibrary/NoCLibraryIsNotPrebuilt", [] { // Nothing to reach, and the link line adds `-nostdlib -static` for it. - EXPECT_FALSE(side_with(mcpp::targetside::Origin::None).cAbi.prebuilt()); - EXPECT_TRUE(side_with(mcpp::targetside::Origin::None).cAbi.absent()); -} + expect(!(side_with(mcpp::targetside::Origin::None).cAbi.prebuilt())); + expect(bool(side_with(mcpp::targetside::Origin::None).cAbi.absent())); +}}; -TEST(TargetSideCLibrary, TheKernelInterfaceDoesNotDecideIt) { +UTest the_kernel_interface_does_not_decide_it{"TargetSideCLibrary/TheKernelInterfaceDoesNotDecideIt", [] { // The defect itself, as an assertion: every kernel-interface origin leaves // the answer to the C-ABI layer alone. using namespace mcpp::targetside; for (auto k : { Origin::Payload, Origin::Xpkg, Origin::Graph, Origin::None }) { - EXPECT_TRUE(side_with(Origin::Payload, k).cAbi.prebuilt()); - EXPECT_FALSE(side_with(Origin::Graph, k).cAbi.prebuilt()); + expect(bool(side_with(Origin::Payload, k).cAbi.prebuilt())); + expect(!(side_with(Origin::Graph, k).cAbi.prebuilt())); } -} +}}; // ── interfaces_not_provided — the whole of the engine's participation ─────── // @@ -774,23 +775,23 @@ TEST(TargetSideCLibrary, TheKernelInterfaceDoesNotDecideIt) { // member of either set; these tests use openkal's real names in one place and // an invented one in another, and the function behaves identically. -TEST(TargetSideInterfaces, EveryRequiredInterfaceProvidedYieldsNoGap) { +UTest every_required_interface_provided_yields_no_gap{"TargetSideInterfaces/EveryRequiredInterfaceProvidedYieldsNoGap", [] { std::vector required{"openkal.fs", "openkal.stream"}; std::vector provided{"openkal.abort", "openkal.stream", "openkal.memory", "openkal.fs"}; - EXPECT_TRUE(mcpp::targetside::interfaces_not_provided(required, provided) + expect(mcpp::targetside::interfaces_not_provided(required, provided) .empty()); -} +}}; -TEST(TargetSideInterfaces, AMissingInterfaceIsReportedByName) { +UTest a_missing_interface_is_reported_by_name{"TargetSideInterfaces/AMissingInterfaceIsReportedByName", [] { std::vector required{"openkal.fs", "openkal.net"}; std::vector provided{"openkal.fs"}; auto gap = mcpp::targetside::interfaces_not_provided(required, provided); - ASSERT_EQ(gap.size(), 1u); - EXPECT_EQ(gap[0], "openkal.net"); -} + expect(fatal(eq(gap.size(), 1u))); + expect(eq(gap[0], "openkal.net")); +}}; -TEST(TargetSideInterfaces, ANameThisEngineHasNeverHeardOfBehavesTheSame) { +UTest a_name_this_engine_has_never_heard_of_behaves_the_same{"TargetSideInterfaces/ANameThisEngineHasNeverHeardOfBehavesTheSame", [] { // The point of the opaque strings: a specification may add an interface // without a release of this engine, and a name that does not exist is // missing for the same reason a name that does exist and is not provided @@ -798,18 +799,17 @@ TEST(TargetSideInterfaces, ANameThisEngineHasNeverHeardOfBehavesTheSame) { std::vector required{"openkal.event", "some.other.spec/iface"}; std::vector provided{"openkal.fs"}; auto gap = mcpp::targetside::interfaces_not_provided(required, provided); - ASSERT_EQ(gap.size(), 2u); - EXPECT_EQ(gap[0], "openkal.event"); - EXPECT_EQ(gap[1], "some.other.spec/iface"); -} + expect(fatal(eq(gap.size(), 2u))); + expect(eq(gap[0], "openkal.event")); + expect(eq(gap[1], "some.other.spec/iface")); +}}; -TEST(TargetSideInterfaces, RequiringNothingIsNeverAGap) { - EXPECT_TRUE(mcpp::targetside::interfaces_not_provided({}, {}).empty()); - EXPECT_TRUE( - mcpp::targetside::interfaces_not_provided({}, {"openkal.fs"}).empty()); -} +UTest requiring_nothing_is_never_a_gap{"TargetSideInterfaces/RequiringNothingIsNeverAGap", [] { + expect(mcpp::targetside::interfaces_not_provided({}, {}).empty()); + expect(mcpp::targetside::interfaces_not_provided({}, {"openkal.fs"}).empty()); +}}; -TEST(TargetSideInterfaces, ProvidingNothingMakesEveryRequirementAGap) { +UTest providing_nothing_makes_every_requirement_a_gap{"TargetSideInterfaces/ProvidingNothingMakesEveryRequirementAGap", [] { // The shape a graph takes when the provider predates the key. The caller // (mcpp.build.prepare) does not reach this function in that case — a // provider that states nothing is not a provider that provides nothing — @@ -817,5 +817,5 @@ TEST(TargetSideInterfaces, ProvidingNothingMakesEveryRequirementAGap) { // the caller's and does not migrate here by accident. std::vector required{"openkal.fs"}; auto gap = mcpp::targetside::interfaces_not_provided(required, {}); - ASSERT_EQ(gap.size(), 1u); -} + expect(fatal(eq(gap.size(), 1u))); +}}; diff --git a/tests/unit/test_test_options.cpp b/tests/unit/test_test_options.cpp index 266a6a7d2..607aae7e5 100644 --- a/tests/unit/test_test_options.cpp +++ b/tests/unit/test_test_options.cpp @@ -1,9 +1,11 @@ -#include import std; +import mcpplibs.mtest; import mcpp.build.backend; import mcpp.build.execute; +using namespace mcpplibs::mtest; + using namespace mcpp::build; // `mcpp test` has to be BOUNDABLE, and its run half bounded by default. It was @@ -12,64 +14,64 @@ using namespace mcpp::build; // consume an entire unattended CI job with nothing to show for it. These // defaults are the contract; asserting them here means neither half can drift // back by an unnoticed edit. -TEST(TestOptions, RunIsBoundedByDefault) { +UTest run_is_bounded_by_default{"TestOptions/RunIsBoundedByDefault", [] { TestOptions to; - EXPECT_GT(to.timeoutSecs, 0) << "per-test run deadline must default to a limit"; - EXPECT_EQ(to.timeoutSecs, 300); -} + expect(gt(to.timeoutSecs, 0)) << "per-test run deadline must default to a limit"; + expect(eq(to.timeoutSecs, 300)); +}}; // The build deadline deliberately does NOT default on, and the asymmetry is // measured: a test binary running over five minutes is unusual, a cold // dependency build running over fifteen is ordinary (one mcpp-index member // builds OpenCV from source in 1019s on Linux, 1289s on Windows). A default // ceiling would turn slow-but-correct builds red and blame mcpp for it. -TEST(TestOptions, BuildDeadlineIsOptIn) { +UTest build_deadline_is_opt_in{"TestOptions/BuildDeadlineIsOptIn", [] { TestOptions to; - EXPECT_EQ(to.buildTimeoutSecs, 0) << "a default build ceiling would fail legitimate cold builds"; -} + expect(eq(to.buildTimeoutSecs, 0)) << "a default build ceiling would fail legitimate cold builds"; +}}; // 0 is still reachable — "no limit" did not disappear, it just has to be asked // for rather than being what you get by not thinking about it. -TEST(TestOptions, ZeroStillMeansUnlimited) { +UTest zero_still_means_unlimited{"TestOptions/ZeroStillMeansUnlimited", [] { TestOptions to; to.timeoutSecs = 0; to.buildTimeoutSecs = 0; - EXPECT_EQ(to.timeoutSecs, 0); - EXPECT_EQ(to.buildTimeoutSecs, 0); -} + expect(eq(to.timeoutSecs, 0)); + expect(eq(to.buildTimeoutSecs, 0)); +}}; // A build drive with no explicit ceiling must stay unbounded: BuildOptions is // shared with `mcpp build`, which is interactive and must not acquire a // surprise deadline just because `mcpp test` grew one. -TEST(BuildOptions, BuildTimeoutDefaultsToUnbounded) { +UTest build_timeout_defaults_to_unbounded{"BuildOptions/BuildTimeoutDefaultsToUnbounded", [] { BuildOptions bo; - EXPECT_EQ(bo.buildTimeoutSecs, 0u); -} + expect(eq(bo.buildTimeoutSecs, 0u)); +}}; -TEST(BuildOptions, CompileDatabaseIsOptionalByDefault) { +UTest compile_database_is_optional_by_default{"BuildOptions/CompileDatabaseIsOptionalByDefault", [] { BuildOptions options; - EXPECT_FALSE(options.requireCompileDatabase); -} + expect(!options.requireCompileDatabase); +}}; // The timeout verdict travels as a flag, not as a message prefix: run_tests // reports a timed-out compile differently from a broken one, and matching on // prose is exactly how that distinction rots silently. -TEST(BuildError, TimedOutIsAFlagAndDefaultsFalse) { +UTest timed_out_is_a_flag_and_defaults_false{"BuildError/TimedOutIsAFlagAndDefaultsFalse", [] { BuildError e{"build failed", std::nullopt, "", false}; - EXPECT_FALSE(e.timedOut); + expect(!e.timedOut); BuildError t{"build timed out after 900s", std::nullopt, "", true}; - EXPECT_TRUE(t.timedOut); -} + expect(bool(t.timedOut)); +}}; // The fan-out needs more than an exit code to report per-member progress: how // many tests ran, and where the time went. A zero-initialised summary must be // safe to print (the "no tests found" early return leaves it untouched). -TEST(TestRunSummary, DefaultsAreZeroAndPrintable) { +UTest defaults_are_zero_and_printable{"TestRunSummary/DefaultsAreZeroAndPrintable", [] { TestRunSummary s; - EXPECT_EQ(s.passed, 0); - EXPECT_EQ(s.failed, 0); - EXPECT_EQ(s.buildMs, 0); - EXPECT_EQ(s.runMs, 0); - EXPECT_EQ(s.elapsedMs, 0); - EXPECT_FALSE(s.packageError); -} + expect(eq(s.passed, 0)); + expect(eq(s.failed, 0)); + expect(eq(s.buildMs, 0)); + expect(eq(s.runMs, 0)); + expect(eq(s.elapsedMs, 0)); + expect(!s.packageError); +}}; diff --git a/tests/unit/test_test_protocol.cpp b/tests/unit/test_test_protocol.cpp index 68b9b7ac1..b78adf865 100644 --- a/tests/unit/test_test_protocol.cpp +++ b/tests/unit/test_test_protocol.cpp @@ -1,8 +1,10 @@ -#include import std; +import mcpplibs.mtest; import mcpp.build.test_protocol; +using namespace mcpplibs::mtest; + // The runner's side of the mtest test protocol, version 1 (docs/08 "Cases"): // the marker a program's file carries (P2.3), the report it writes (P5), a // crash's open cases (P5.4) and the patterns of `--case`. @@ -19,22 +21,22 @@ std::filesystem::path write_file(std::string_view name, std::string_view content return path; } -TEST(TestProtocol, TheMarkerStatesTheVersion) { - EXPECT_EQ(tp::marker_version(write_file("a", std::string("\x7f" "ELF....mtest-protocol=1\0rest", 30))), 1); - EXPECT_EQ(tp::marker_version(write_file("b", "no marker here")), 0); +UTest the_marker_states_the_version{"TestProtocol/TheMarkerStatesTheVersion", [] { + expect(eq(tp::marker_version(write_file("a", std::string("\x7f" "ELF....mtest-protocol=1\0rest", 30))), 1)); + expect(eq(tp::marker_version(write_file("b", "no marker here")), 0)); // The version is followed by a byte that is not a digit; a marker with no // version is none. - EXPECT_EQ(tp::marker_version(write_file("c", "mtest-protocol=x mtest-protocol=12;")), 12); - EXPECT_EQ(tp::marker_version("/nonexistent/program"), 0); -} + expect(eq(tp::marker_version(write_file("c", "mtest-protocol=x mtest-protocol=12;")), 12)); + expect(eq(tp::marker_version("/nonexistent/program"), 0)); +}}; -TEST(TestProtocol, TheMarkerAcrossABlockBoundary) { +UTest the_marker_across_a_block_boundary{"TestProtocol/TheMarkerAcrossABlockBoundary", [] { std::string bytes((1 << 20) - 5, 'a'); bytes += "mtest-protocol=1;"; - EXPECT_EQ(tp::marker_version(write_file("d", bytes)), 1); -} + expect(eq(tp::marker_version(write_file("d", bytes)), 1)); +}}; -TEST(TestProtocol, AReportGivesEachCaseAndItsFailures) { +UTest a_report_gives_each_case_and_its_failures{"TestProtocol/AReportGivesEachCaseAndItsFailures", [] { auto r = tp::read_report(write_file("r.ndjson", "{\"e\":\"program\",\"protocol\":1,\"level\":3,\"framework\":\"f\"}\n" "not json\n" @@ -46,31 +48,31 @@ TEST(TestProtocol, AReportGivesEachCaseAndItsFailures) { "{\"e\":\"future\",\"id\":\"b\"}\n" "{\"e\":\"missing\",\"id\":\"c\"}\n" "{\"e\":\"done\",\"status\":\"fail\",\"counts\":{}}\n"), 1); - EXPECT_EQ(r.level, 3); - EXPECT_EQ(r.framework, "f"); - EXPECT_TRUE(r.done); - ASSERT_EQ(r.cases.size(), 2u); - EXPECT_EQ(r.cases[0].status, "pass"); - EXPECT_DOUBLE_EQ(r.cases[0].ms, 1.5); - ASSERT_EQ(r.cases[1].failures.size(), 1u); - EXPECT_EQ(r.cases[1].failures[0].line, 9); - EXPECT_EQ(r.cases[1].failures[0].actual, "2"); - EXPECT_EQ(r.missing, std::vector{"c"}); - EXPECT_TRUE(r.failed()); - EXPECT_EQ(r.counts().at("fail"), 1); -} + expect(eq(r.level, 3)); + expect(eq(r.framework, "f")); + expect(bool(r.done)); + expect(fatal(eq(r.cases.size(), 2u))); + expect(eq(r.cases[0].status, "pass")); + expect(near(r.cases[0].ms, 1.5, 1e-9)); + expect(fatal(eq(r.cases[1].failures.size(), 1u))); + expect(eq(r.cases[1].failures[0].line, 9)); + expect(eq(r.cases[1].failures[0].actual, "2")); + expect(eq(r.missing, std::vector{"c"})); + expect(bool(r.failed())); + expect(eq(r.counts().at("fail"), 1)); +}}; -TEST(TestProtocol, ACaseACrashLeftOpenFailed) { +UTest a_case_a_crash_left_open_failed{"TestProtocol/ACaseACrashLeftOpenFailed", [] { auto r = tp::read_report(write_file("crash.ndjson", "{\"e\":\"program\",\"protocol\":1,\"level\":1}\n" "{\"e\":\"begin\",\"id\":\"x\",\"t\":0}\n"), 139); - EXPECT_FALSE(r.done); - ASSERT_EQ(r.cases.size(), 1u); - EXPECT_EQ(r.cases[0].status, "fail"); - EXPECT_NE(r.cases[0].reason.find("status 139"), std::string::npos); -} + expect(!r.done); + expect(fatal(eq(r.cases.size(), 1u))); + expect(eq(r.cases[0].status, "fail")); + expect(ne(r.cases[0].reason.find("status 139"), std::string::npos)); +}}; -TEST(TestProtocol, APatternMatchesIdentifiers) { +UTest a_pattern_matches_identifiers{"TestProtocol/APatternMatchesIdentifiers", [] { struct Row { std::string_view id, pattern; bool matches; }; constexpr Row rows[] = { {"fs/read", "fs/read", true}, @@ -84,29 +86,29 @@ TEST(TestProtocol, APatternMatchesIdentifiers) { {"a b/c", "a b/*", true}, }; for (const auto& row : rows) - EXPECT_EQ(tp::matches(row.id, row.pattern), row.matches) << row.id << " ~ " << row.pattern; -} + expect(eq(tp::matches(row.id, row.pattern), row.matches)) << row.id << " ~ " << row.pattern; +}}; -TEST(TestProtocol, ASelectionReachesTheCasesAListedCaseDeclares) { +UTest a_selection_reaches_the_cases_a_listed_case_declares{"TestProtocol/ASelectionReachesTheCasesAListedCaseDeclares", [] { // P3.4: `fields` is listed; the cases its body declares are reported when it runs. const std::vector listed = {{"fields"}, {"empty"}}; using V = std::vector; - EXPECT_EQ(tp::selection(listed, {"fields"}), V{"fields"}); - EXPECT_EQ(tp::selection(listed, {"fields/one"}), V{"fields/one"}); // passed as it is - EXPECT_EQ(tp::selection(listed, {"fields/o*"}), V{"fields"}); // the listed case, all of it - EXPECT_EQ(tp::selection(listed, {"e*", "fields/one", "fields"}), (V{"empty", "fields/one", "fields"})); - EXPECT_EQ(tp::selection(listed, {"other/one"}), V{}); -} + expect(eq(tp::selection(listed, {"fields"}), V{"fields"})); + expect(eq(tp::selection(listed, {"fields/one"}), V{"fields/one"})); // passed as it is + expect(eq(tp::selection(listed, {"fields/o*"}), V{"fields"})); // the listed case, all of it + expect(eq(tp::selection(listed, {"e*", "fields/one", "fields"}), (V{"empty", "fields/one", "fields"}))); + expect(eq(tp::selection(listed, {"other/one"}), V{})); +}}; -TEST(TestProtocol, TheVariablesOfARun) { +UTest the_variables_of_a_run{"TestProtocol/TheVariablesOfARun", [] { auto env = tp::variables("run", "/r", std::filesystem::path("/s"), "/t", 7); std::map m(env.begin(), env.end()); - EXPECT_EQ(m.at("MTEST_PROTOCOL"), "1"); - EXPECT_EQ(m.at("MTEST_MODE"), "run"); - EXPECT_EQ(m.at("MTEST_SELECT"), std::filesystem::path("/s").string()); - EXPECT_EQ(m.at("MTEST_SEED"), "7"); + expect(eq(m.at("MTEST_PROTOCOL"), "1")); + expect(eq(m.at("MTEST_MODE"), "run")); + expect(eq(m.at("MTEST_SELECT"), std::filesystem::path("/s").string())); + expect(eq(m.at("MTEST_SEED"), "7")); auto list = tp::variables("list", "/r", std::nullopt, "/t", std::nullopt); - EXPECT_TRUE(std::ranges::none_of(list, [](auto& kv) { return kv.first == "MTEST_SELECT" || kv.first == "MTEST_SEED"; })); -} + expect(std::ranges::none_of(list, [](auto& kv) { return kv.first == "MTEST_SELECT" || kv.first == "MTEST_SEED"; })); +}}; } // namespace diff --git a/tests/unit/test_test_targets.cpp b/tests/unit/test_test_targets.cpp index d03eff962..28d0a2b27 100644 --- a/tests/unit/test_test_targets.cpp +++ b/tests/unit/test_test_targets.cpp @@ -1,8 +1,10 @@ -#include import std; +import mcpplibs.mtest; import mcpp.build.test_targets; +using namespace mcpplibs::mtest; + namespace { struct Tmp { @@ -30,7 +32,7 @@ void write_manifest(const std::filesystem::path& root, std::string_view body) { write_file(root / "mcpp.toml", body); } -TEST(TestTargets, NestedNamesUseTestsRelativePath) { +UTest nested_names_use_tests_relative_path{"TestTargets/NestedNamesUseTestsRelativePath", [] { Tmp tmp; write_manifest(tmp.path, R"([package] name = "demo" @@ -39,14 +41,13 @@ version = "0.1.0" write_file(tmp.path / "tests/tagged/nested/smoke.cpp", "int main() {}\n"); auto result = mcpp::build::discover_test_targets(tmp.path, {}); - ASSERT_TRUE(result.has_value()) << result.error(); - ASSERT_EQ(result->targets.size(), 1u); - EXPECT_EQ(result->targets[0].name, "tagged/nested/smoke"); - EXPECT_EQ(std::filesystem::path(result->targets[0].main), - std::filesystem::path("tests") / "tagged" / "nested" / "smoke.cpp"); -} + if (auto&& failed = expect(fatal(result.has_value())); !failed) failed << result.error(); + expect(fatal(eq(result->targets.size(), 1u))); + expect(eq(result->targets[0].name, "tagged/nested/smoke")); + expect(eq(std::filesystem::path(result->targets[0].main), std::filesystem::path("tests") / "tagged" / "nested" / "smoke.cpp")); +}}; -TEST(TestTargets, GlobFlagsBecomePerTargetFlags) { +UTest glob_flags_become_per_target_flags{"TestTargets/GlobFlagsBecomePerTargetFlags", [] { Tmp tmp; write_manifest(tmp.path, R"([package] name = "demo" @@ -58,14 +59,14 @@ flags = [{ glob = "tests/**/*.cpp", defines = ["TEST_FEATURE"], cflags = ["-Wall write_file(tmp.path / "tests/main.cpp", "int main() {}\n"); auto result = mcpp::build::discover_test_targets(tmp.path, {}); - ASSERT_TRUE(result.has_value()) << result.error(); - ASSERT_EQ(result->targets.size(), 1u); - EXPECT_EQ(result->targets[0].defines, std::vector{"TEST_FEATURE"}); - EXPECT_EQ(result->targets[0].cflags, std::vector{"-Wall"}); - EXPECT_EQ(result->targets[0].cxxflags, std::vector{"-Wextra"}); -} - -TEST(TestTargets, PackageFilterScopesWorkspaceMember) { + if (auto&& failed = expect(fatal(result.has_value())); !failed) failed << result.error(); + expect(fatal(eq(result->targets.size(), 1u))); + expect(eq(result->targets[0].defines, std::vector{"TEST_FEATURE"})); + expect(eq(result->targets[0].cflags, std::vector{"-Wall"})); + expect(eq(result->targets[0].cxxflags, std::vector{"-Wextra"})); +}}; + +UTest package_filter_scopes_workspace_member{"TestTargets/PackageFilterScopesWorkspaceMember", [] { Tmp tmp; write_manifest(tmp.path, R"([workspace] members = ["a", "b"] @@ -82,14 +83,13 @@ version = "0.1.0" write_file(tmp.path / "b/tests/main.cpp", "int main() {}\n"); auto result = mcpp::build::discover_test_targets(tmp.path, "a"); - ASSERT_TRUE(result.has_value()) << result.error(); - ASSERT_EQ(result->packageRoot, tmp.path / "a"); - ASSERT_EQ(result->targets.size(), 1u); - EXPECT_EQ(std::filesystem::path(result->targets[0].main), - std::filesystem::path("tests") / "main.cpp"); -} + if (auto&& failed = expect(fatal(result.has_value())); !failed) failed << result.error(); + expect(fatal(eq(result->packageRoot, tmp.path / "a"))); + expect(fatal(eq(result->targets.size(), 1u))); + expect(eq(std::filesystem::path(result->targets[0].main), std::filesystem::path("tests") / "main.cpp")); +}}; -TEST(TestTargets, DirectorySymlinkKeepsLexicalPackageBoundary) { +UTest directory_symlink_keeps_lexical_package_boundary{"TestTargets/DirectorySymlinkKeepsLexicalPackageBoundary", [] { Tmp tmp; write_manifest(tmp.path, R"([package] name = "demo" @@ -102,19 +102,18 @@ version = "0.1.0" std::error_code ec; std::filesystem::create_directory_symlink( outside.path, tmp.path / "tests/vendor", ec); - if (ec) GTEST_SKIP() << "directory symlink unavailable: " << ec.message(); + if (ec) skip([&] { std::ostringstream text; text << "directory symlink unavailable: " << ec.message(); return text.str(); }()); auto result = mcpp::build::discover_test_targets(tmp.path, {}); - ASSERT_TRUE(result.has_value()) << result.error(); - ASSERT_EQ(result->targets.size(), 1u); - EXPECT_EQ(result->targets[0].name, "vendor/external"); - EXPECT_EQ(std::filesystem::path(result->targets[0].main), - std::filesystem::path("tests") / "vendor" / "external.cpp"); -} + if (auto&& failed = expect(fatal(result.has_value())); !failed) failed << result.error(); + expect(fatal(eq(result->targets.size(), 1u))); + expect(eq(result->targets[0].name, "vendor/external")); + expect(eq(std::filesystem::path(result->targets[0].main), std::filesystem::path("tests") / "vendor" / "external.cpp")); +}}; // `[test] discover` (#634 A5): names are relative to the fixed directory of // the glob that found the file, `main` stays relative to the package root. -TEST(TestTargets, DiscoverNamesAreRelativeToTheGlobsDirectory) { +UTest discover_names_are_relative_to_the_globs_directory{"TestTargets/DiscoverNamesAreRelativeToTheGlobsDirectory", [] { Tmp tmp; write_manifest(tmp.path, R"([package] name = "demo" @@ -127,17 +126,16 @@ discover = ["checks/**/*.cpp"] write_file(tmp.path / "tests/ignored.cpp", "int main() {}\n"); auto result = mcpp::build::discover_test_targets(tmp.path, {}); - ASSERT_TRUE(result.has_value()) << result.error(); - EXPECT_TRUE(result->discoverDeclared); - ASSERT_EQ(result->targets.size(), 1u); - EXPECT_EQ(result->targets[0].name, "deep/a"); - EXPECT_EQ(std::filesystem::path(result->targets[0].main), - std::filesystem::path("checks") / "deep" / "a.cpp"); -} + if (auto&& failed = expect(fatal(result.has_value())); !failed) failed << result.error(); + expect(bool(result->discoverDeclared)); + expect(fatal(eq(result->targets.size(), 1u))); + expect(eq(result->targets[0].name, "deep/a")); + expect(eq(std::filesystem::path(result->targets[0].main), std::filesystem::path("checks") / "deep" / "a.cpp")); +}}; // An exclusion removes a file whichever positive glob found it, and a glob // with no fixed directory names files relative to the package root. -TEST(TestTargets, DiscoverExclusionsApplyAcrossGlobs) { +UTest discover_exclusions_apply_across_globs{"TestTargets/DiscoverExclusionsApplyAcrossGlobs", [] { Tmp tmp; write_manifest(tmp.path, R"([package] name = "demo" @@ -151,13 +149,13 @@ discover = ["tests/**/*.cpp", "*_check.cpp", "!tests/fixtures/**"] write_file(tmp.path / "root_check.cpp", "int main() {}\n"); auto result = mcpp::build::discover_test_targets(tmp.path, {}); - ASSERT_TRUE(result.has_value()) << result.error(); + if (auto&& failed = expect(fatal(result.has_value())); !failed) failed << result.error(); std::set names; for (auto const& t : result->targets) names.insert(t.name); - EXPECT_EQ(names, (std::set{"kept", "root_check"})); -} + expect(eq(names, (std::set{"kept", "root_check"}))); +}}; -TEST(TestTargets, AnEmptyDiscoverFindsNothing) { +UTest an_empty_discover_finds_nothing{"TestTargets/AnEmptyDiscoverFindsNothing", [] { Tmp tmp; write_manifest(tmp.path, R"([package] name = "demo" @@ -169,13 +167,13 @@ discover = [] write_file(tmp.path / "tests/main.cpp", "int main() {}\n"); auto result = mcpp::build::discover_test_targets(tmp.path, {}); - ASSERT_TRUE(result.has_value()) << result.error(); - EXPECT_TRUE(result->discoverDeclared); - EXPECT_TRUE(result->discover.empty()); - EXPECT_TRUE(result->targets.empty()); -} + if (auto&& failed = expect(fatal(result.has_value())); !failed) failed << result.error(); + expect(bool(result->discoverDeclared)); + expect(result->discover.empty()); + expect(result->targets.empty()); +}}; -TEST(TestTargets, TwoFilesWithOneNameAreRefusedNamingBoth) { +UTest two_files_with_one_name_are_refused_naming_both{"TestTargets/TwoFilesWithOneNameAreRefusedNamingBoth", [] { Tmp tmp; write_manifest(tmp.path, R"([package] name = "demo" @@ -188,14 +186,14 @@ discover = ["tests/*.cpp", "checks/*.cpp"] write_file(tmp.path / "checks/same.cpp", "int main() {}\n"); auto result = mcpp::build::discover_test_targets(tmp.path, {}); - ASSERT_FALSE(result.has_value()); - EXPECT_NE(result.error().find("duplicate test name 'same'"), std::string::npos) << result.error(); - EXPECT_NE(result.error().find("tests/same.cpp"), std::string::npos) << result.error(); - EXPECT_NE(result.error().find("checks/same.cpp"), std::string::npos) << result.error(); -} + expect(fatal(!result.has_value())); + if (auto&& failed = expect(ne(result.error().find("duplicate test name 'same'"), std::string::npos)); !failed) failed << result.error(); + if (auto&& failed = expect(ne(result.error().find("tests/same.cpp"), std::string::npos)); !failed) failed << result.error(); + if (auto&& failed = expect(ne(result.error().find("checks/same.cpp"), std::string::npos)); !failed) failed << result.error(); +}}; // Without the key the set, and every name, is the one earlier releases gave. -TEST(TestTargets, WithoutDiscoverTheDefaultIsTestsGlob) { +UTest without_discover_the_default_is_tests_glob{"TestTargets/WithoutDiscoverTheDefaultIsTestsGlob", [] { Tmp tmp; write_manifest(tmp.path, R"([package] name = "demo" @@ -205,23 +203,52 @@ version = "0.1.0" write_file(tmp.path / "checks/b.cpp", "int main() {}\n"); auto result = mcpp::build::discover_test_targets(tmp.path, {}); - ASSERT_TRUE(result.has_value()) << result.error(); - EXPECT_FALSE(result->discoverDeclared); - EXPECT_EQ(result->discover, std::vector{"tests/**/*.cpp"}); - ASSERT_EQ(result->targets.size(), 1u); - EXPECT_EQ(result->targets[0].name, "unit/a"); -} - -TEST(TestTargets, BrokenManifestStillReturnsInventory) { + if (auto&& failed = expect(fatal(result.has_value())); !failed) failed << result.error(); + expect(!result->discoverDeclared); + expect(eq(result->discover, std::vector{"tests/**/*.cpp"})); + expect(fatal(eq(result->targets.size(), 1u))); + expect(eq(result->targets[0].name, "unit/a")); +}}; + +UTest broken_manifest_still_returns_inventory{"TestTargets/BrokenManifestStillReturnsInventory", [] { Tmp tmp; write_file(tmp.path / "mcpp.toml", "this is not valid TOML\n"); write_file(tmp.path / "tests/main.cpp", "int main() {}\n"); auto result = mcpp::build::discover_test_targets(tmp.path, {}); - ASSERT_TRUE(result.has_value()) << result.error(); - ASSERT_EQ(result->packageRoot, tmp.path); - ASSERT_EQ(result->targets.size(), 1u); - EXPECT_EQ(result->targets[0].name, "main"); -} + if (auto&& failed = expect(fatal(result.has_value())); !failed) failed << result.error(); + expect(fatal(eq(result->packageRoot, tmp.path))); + expect(fatal(eq(result->targets.size(), 1u))); + expect(eq(result->targets[0].name, "main")); +}}; } // namespace + +// A test's name is the path of its program: a directory program `unit` and a +// program `unit/own` below it would be a file and a directory of one path. +UTest a_name_that_is_another_s_directory_is_refused{"TestTargets/ANameThatIsAnothersDirectoryIsRefused", [] { + Tmp tmp; + write_manifest(tmp.path, R"([package] +name = "demo" +version = "0.1.0" + +[test] +programs = "directory" +)"); + write_file(tmp.path / "tests/unit/a.cpp", "int main() {}\n"); + write_file(tmp.path / "tests/unit/b.cpp", "int b() { return 0; }\n"); + write_file(tmp.path / "tests/unit/own/c.cpp", "int main() {}\n"); + write_file(tmp.path / "tests/unit-x/d.cpp", "int main() {}\n"); + + auto result = mcpp::build::discover_test_targets(tmp.path, {}); + expect(fatal(!result.has_value())); + if (auto&& failed = expect(result.error().find("test 'unit'") != std::string::npos + && result.error().find("test 'unit/own'") != std::string::npos); + !failed) + failed << result.error(); + + std::filesystem::rename(tmp.path / "tests/unit/own", tmp.path / "tests/own"); + auto moved = mcpp::build::discover_test_targets(tmp.path, {}); + if (auto&& failed = expect(fatal(moved.has_value())); !failed) failed << moved.error(); + expect(eq(moved->targets.size(), 3u)); +}}; diff --git a/tests/unit/test_toml.cpp b/tests/unit/test_toml.cpp index e6d5ae13b..5ab1ce0c9 100644 --- a/tests/unit/test_toml.cpp +++ b/tests/unit/test_toml.cpp @@ -1,165 +1,166 @@ -#include import std; +import mcpplibs.mtest; import mcpp.libs.toml; +using namespace mcpplibs::mtest; + namespace t = mcpp::libs::toml; -TEST(Toml, EmptyDocumentParses) { +UTest empty_document_parses{"Toml/EmptyDocumentParses", [] { auto d = t::parse(""); - ASSERT_TRUE(d.has_value()); - EXPECT_TRUE(d->root().empty()); -} + expect(fatal(d.has_value())); + expect(d->root().empty()); +}}; -TEST(Toml, SimpleKeyValue) { +UTest simple_key_value{"Toml/SimpleKeyValue", [] { auto d = t::parse("key = \"value\"\nnum = 42\nflag = true"); - ASSERT_TRUE(d.has_value()) << d.error().message; - EXPECT_EQ(d->get_string("key").value_or(""), "value"); - EXPECT_EQ(d->get_int("num").value_or(-1), 42); - EXPECT_EQ(d->get_bool("flag").value_or(false), true); -} + if (auto&& failed = expect(fatal(d.has_value())); !failed) failed << d.error().message; + expect(eq(d->get_string("key").value_or(""), "value")); + expect(eq(d->get_int("num").value_or(-1), 42)); + expect(eq(d->get_bool("flag").value_or(false), true)); +}}; -TEST(Toml, NestedTables) { +UTest nested_tables{"Toml/NestedTables", [] { auto d = t::parse(R"( [a.b] x = "deep" [a] y = "shallow" )"); - ASSERT_TRUE(d.has_value()) << d.error().message; - EXPECT_EQ(d->get_string("a.b.x").value_or(""), "deep"); - EXPECT_EQ(d->get_string("a.y").value_or(""), "shallow"); -} + if (auto&& failed = expect(fatal(d.has_value())); !failed) failed << d.error().message; + expect(eq(d->get_string("a.b.x").value_or(""), "deep")); + expect(eq(d->get_string("a.y").value_or(""), "shallow")); +}}; -TEST(Toml, ArrayOfStrings) { +UTest array_of_strings{"Toml/ArrayOfStrings", [] { auto d = t::parse(R"(items = ["a", "b", "c"])"); - ASSERT_TRUE(d.has_value()); + expect(fatal(d.has_value())); auto arr = d->get_string_array("items"); - ASSERT_TRUE(arr.has_value()); - EXPECT_EQ(arr->size(), 3u); - EXPECT_EQ((*arr)[0], "a"); - EXPECT_EQ((*arr)[2], "c"); -} + expect(fatal(arr.has_value())); + expect(eq(arr->size(), 3u)); + expect(eq((*arr)[0], "a")); + expect(eq((*arr)[2], "c")); +}}; -TEST(Toml, ArrayAllowsTrailingComma) { +UTest array_allows_trailing_comma{"Toml/ArrayAllowsTrailingComma", [] { auto d = t::parse(R"( items = [ "a", "b", ] )"); - ASSERT_TRUE(d.has_value()) << d.error().message; + if (auto&& failed = expect(fatal(d.has_value())); !failed) failed << d.error().message; auto arr = d->get_string_array("items"); - ASSERT_TRUE(arr.has_value()); - EXPECT_EQ(*arr, std::vector({"a", "b"})); -} + expect(fatal(arr.has_value())); + expect(eq(*arr, std::vector({"a", "b"}))); +}}; -TEST(Toml, EscapedString) { +UTest escaped_string{"Toml/EscapedString", [] { auto d = t::parse(R"(s = "a\tb\nc")"); - ASSERT_TRUE(d.has_value()); - EXPECT_EQ(d->get_string("s").value_or(""), std::string("a\tb\nc")); -} + expect(fatal(d.has_value())); + expect(eq(d->get_string("s").value_or(""), std::string("a\tb\nc"))); +}}; // TOML 1.0's escapes: \b, \f and the Unicode forms were refused as "invalid // escape", so a valid manifest written with them could not be read. -TEST(Toml, EveryTomlEscapeIsRead) { +UTest every_toml_escape_is_read{"Toml/EveryTomlEscapeIsRead", [] { auto d = t::parse(R"(s = "a\bb\fc")"); - ASSERT_TRUE(d.has_value()) << d.error().message; - EXPECT_EQ(d->get_string("s").value_or(""), std::string("a\bb\fc")); + if (auto&& failed = expect(fatal(d.has_value())); !failed) failed << d.error().message; + expect(eq(d->get_string("s").value_or(""), std::string("a\bb\fc"))); // One, two, three and four UTF-8 bytes, upper- and lower-case hex. d = t::parse(R"(s = "\u0041 caf\u00e9 \u4E2D \U0001F600")"); - ASSERT_TRUE(d.has_value()) << d.error().message; - EXPECT_EQ(d->get_string("s").value_or(""), - std::string("A caf\xC3\xA9 \xE4\xB8\xAD \xF0\x9F\x98\x80")); + if (auto&& failed = expect(fatal(d.has_value())); !failed) failed << d.error().message; + expect(eq(d->get_string("s").value_or(""), std::string("A caf\xC3\xA9 \xE4\xB8\xAD \xF0\x9F\x98\x80"))); d = t::parse("s = \"\"\"\nJos\\u00e9\\U0000004A\"\"\""); - ASSERT_TRUE(d.has_value()) << d.error().message; - EXPECT_EQ(d->get_string("s").value_or(""), std::string("Jos\xC3\xA9J")); + if (auto&& failed = expect(fatal(d.has_value())); !failed) failed << d.error().message; + expect(eq(d->get_string("s").value_or(""), std::string("Jos\xC3\xA9J"))); // A quoted key is a basic string too. d = t::parse(R"("caf\u00e9" = 1)"); - ASSERT_TRUE(d.has_value()) << d.error().message; - EXPECT_EQ(d->root().count("caf\xC3\xA9"), 1u); + if (auto&& failed = expect(fatal(d.has_value())); !failed) failed << d.error().message; + expect(eq(d->root().count("caf\xC3\xA9"), 1u)); // A literal string keeps its backslashes. d = t::parse(R"(s = 'C:\u0041')"); - ASSERT_TRUE(d.has_value()) << d.error().message; - EXPECT_EQ(d->get_string("s").value_or(""), R"(C:\u0041)"); -} - -TEST(Toml, AUnicodeEscapeNamesAScalarValue) { - EXPECT_FALSE(t::parse(R"(s = "\u12")").has_value()); // too few digits - EXPECT_FALSE(t::parse(R"(s = "\u12G4")").has_value()); // not hex - EXPECT_FALSE(t::parse(R"(s = "\U0001F60")").has_value()); // \U takes eight - EXPECT_FALSE(t::parse(R"(s = "\uD800")").has_value()); // a surrogate - EXPECT_FALSE(t::parse(R"(s = "\U00110000")").has_value()); // past U+10FFFF - EXPECT_FALSE(t::parse(R"(s = "\x41")").has_value()); // not TOML 1.0 + if (auto&& failed = expect(fatal(d.has_value())); !failed) failed << d.error().message; + expect(eq(d->get_string("s").value_or(""), R"(C:\u0041)")); +}}; + +UTest a_unicode_escape_names_a_scalar_value{"Toml/AUnicodeEscapeNamesAScalarValue", [] { + expect(!(t::parse(R"(s = "\u12")").has_value())); // too few digits + expect(!(t::parse(R"(s = "\u12G4")").has_value())); // not hex + expect(!(t::parse(R"(s = "\U0001F60")").has_value())); // \U takes eight + expect(!(t::parse(R"(s = "\uD800")").has_value())); // a surrogate + expect(!(t::parse(R"(s = "\U00110000")").has_value())); // past U+10FFFF + expect(!(t::parse(R"(s = "\x41")").has_value())); // not TOML 1.0 auto bad = t::parse("s = \"ok\\uDFFF\""); - ASSERT_FALSE(bad.has_value()); - EXPECT_NE(bad.error().message.find("\\uDFFF"), std::string::npos) << bad.error().message; -} + expect(fatal(!bad.has_value())); + if (auto&& failed = expect(ne(bad.error().message.find("\\uDFFF"), std::string::npos)); !failed) failed << bad.error().message; +}}; -TEST(Toml, RejectUnterminatedString) { +UTest reject_unterminated_string{"Toml/RejectUnterminatedString", [] { auto d = t::parse(R"(x = "no end)"); - EXPECT_FALSE(d.has_value()); -} + expect(!d.has_value()); +}}; -TEST(Toml, CommentsIgnored) { +UTest comments_ignored{"Toml/CommentsIgnored", [] { auto d = t::parse(R"( # top comment x = 1 # trailing isn't supported but full-line is y = 2 )"); - ASSERT_TRUE(d.has_value()) << d.error().message; - EXPECT_EQ(d->get_int("x").value_or(-1), 1); - EXPECT_EQ(d->get_int("y").value_or(-1), 2); -} - -TEST(Toml, EscapeStringHelper) { - EXPECT_EQ(t::escape_string("hello"), "\"hello\""); - EXPECT_EQ(t::escape_string("a\"b"), "\"a\\\"b\""); - EXPECT_EQ(t::escape_string("a\\b"), "\"a\\\\b\""); -} - -TEST(Toml, MultilineBasicString) { + if (auto&& failed = expect(fatal(d.has_value())); !failed) failed << d.error().message; + expect(eq(d->get_int("x").value_or(-1), 1)); + expect(eq(d->get_int("y").value_or(-1), 2)); +}}; + +UTest escape_string_helper{"Toml/EscapeStringHelper", [] { + expect(eq(t::escape_string("hello"), "\"hello\"")); + expect(eq(t::escape_string("a\"b"), "\"a\\\"b\"")); + expect(eq(t::escape_string("a\\b"), "\"a\\\\b\"")); +}}; + +UTest multiline_basic_string{"Toml/MultilineBasicString", [] { auto d = t::parse("body = \"\"\"\nline1\nline2 \"quoted\"\n\"\"\"\nafter = 1"); - ASSERT_TRUE(d.has_value()) << d.error().message; + if (auto&& failed = expect(fatal(d.has_value())); !failed) failed << d.error().message; // First newline after the opening delimiter is trimmed (TOML 1.0). - EXPECT_EQ(d->get_string("body").value_or(""), "line1\nline2 \"quoted\"\n"); - EXPECT_EQ(d->get_int("after").value_or(-1), 1); -} + expect(eq(d->get_string("body").value_or(""), "line1\nline2 \"quoted\"\n")); + expect(eq(d->get_int("after").value_or(-1), 1)); +}}; -TEST(Toml, MultilineLiteralString) { +UTest multiline_literal_string{"Toml/MultilineLiteralString", [] { auto d = t::parse("raw = '''\nno \\escapes here\n'''"); - ASSERT_TRUE(d.has_value()) << d.error().message; - EXPECT_EQ(d->get_string("raw").value_or(""), "no \\escapes here\n"); -} + if (auto&& failed = expect(fatal(d.has_value())); !failed) failed << d.error().message; + expect(eq(d->get_string("raw").value_or(""), "no \\escapes here\n")); +}}; -TEST(Toml, MultilineLineEndingBackslash) { +UTest multiline_line_ending_backslash{"Toml/MultilineLineEndingBackslash", [] { auto d = t::parse("s = \"\"\"a\\\n b\"\"\""); - ASSERT_TRUE(d.has_value()) << d.error().message; - EXPECT_EQ(d->get_string("s").value_or(""), "ab"); -} + if (auto&& failed = expect(fatal(d.has_value())); !failed) failed << d.error().message; + expect(eq(d->get_string("s").value_or(""), "ab")); +}}; -TEST(Toml, MultilineUnterminatedIsError) { +UTest multiline_unterminated_is_error{"Toml/MultilineUnterminatedIsError", [] { auto d = t::parse("s = \"\"\"never closed"); - EXPECT_FALSE(d.has_value()); -} + expect(!d.has_value()); +}}; -TEST(Toml, EmptyStringStillParses) { +UTest empty_string_still_parses{"Toml/EmptyStringStillParses", [] { auto d = t::parse("e = \"\"\nx = 2"); - ASSERT_TRUE(d.has_value()) << d.error().message; - EXPECT_EQ(d->get_string("e").value_or("?"), ""); - EXPECT_EQ(d->get_int("x").value_or(-1), 2); -} + if (auto&& failed = expect(fatal(d.has_value())); !failed) failed << d.error().message; + expect(eq(d->get_string("e").value_or("?"), "")); + expect(eq(d->get_int("x").value_or(-1), 2)); +}}; // #227: `[[dotted.path]]` array-of-tables — each occurrence appends a fresh // table to an array at that dotted path; keys after it fill THAT table until // the next `[...]`/`[[...]]` header. Declaration order must be preserved // (it's the "later entries win" override order for [build].flags). -TEST(Toml, ParsesArrayOfTables) { +UTest parses_array_of_tables{"Toml/ParsesArrayOfTables", [] { auto d = t::parse(R"( [[build.flags]] glob = "a/**" @@ -167,27 +168,27 @@ cxxflags = ["-DX=1"] [[build.flags]] glob = "b/**" )"); - ASSERT_TRUE(d.has_value()) << d.error().message; + if (auto&& failed = expect(fatal(d.has_value())); !failed) failed << d.error().message; auto* v = d->get("build.flags"); - ASSERT_NE(v, nullptr); - ASSERT_TRUE(v->is_array()); + expect(fatal(ne(v, nullptr))); + expect(fatal(v->is_array())); auto& arr = v->as_array(); - ASSERT_EQ(arr.size(), 2u); - ASSERT_TRUE(arr[0].is_table()); - EXPECT_EQ(arr[0].as_table().at("glob").as_string(), "a/**"); + expect(fatal(eq(arr.size(), 2u))); + expect(fatal(arr[0].is_table())); + expect(eq(arr[0].as_table().at("glob").as_string(), "a/**")); auto cx = arr[0].as_table().at("cxxflags"); - ASSERT_TRUE(cx.is_array()); - ASSERT_EQ(cx.as_array().size(), 1u); - EXPECT_EQ(cx.as_array()[0].as_string(), "-DX=1"); - ASSERT_TRUE(arr[1].is_table()); - EXPECT_EQ(arr[1].as_table().at("glob").as_string(), "b/**"); - EXPECT_EQ(arr[1].as_table().count("cxxflags"), 0u); -} + expect(fatal(cx.is_array())); + expect(fatal(eq(cx.as_array().size(), 1u))); + expect(eq(cx.as_array()[0].as_string(), "-DX=1")); + expect(fatal(arr[1].is_table())); + expect(eq(arr[1].as_table().at("glob").as_string(), "b/**")); + expect(eq(arr[1].as_table().count("cxxflags"), 0u)); +}}; // Regular `[table]` and inline-array-of-tables parsing must be unaffected by // AOT support — array-of-tables is an ADDITIONAL grammar form, not a // replacement. -TEST(Toml, RegularTablesStillWorkAlongsideArrayOfTables) { +UTest regular_tables_still_work_alongside_array_of_tables{"Toml/RegularTablesStillWorkAlongsideArrayOfTables", [] { auto d = t::parse(R"( [package] name = "x" @@ -196,11 +197,11 @@ glob = "a/**" [dependencies] foo = "1.0" )"); - ASSERT_TRUE(d.has_value()) << d.error().message; - EXPECT_EQ(d->get_string("package.name").value_or(""), "x"); - EXPECT_EQ(d->get_string("dependencies.foo").value_or(""), "1.0"); + if (auto&& failed = expect(fatal(d.has_value())); !failed) failed << d.error().message; + expect(eq(d->get_string("package.name").value_or(""), "x")); + expect(eq(d->get_string("dependencies.foo").value_or(""), "1.0")); auto* v = d->get("build.flags"); - ASSERT_NE(v, nullptr); - ASSERT_TRUE(v->is_array()); - EXPECT_EQ(v->as_array().size(), 1u); -} + expect(fatal(ne(v, nullptr))); + expect(fatal(v->is_array())); + expect(eq(v->as_array().size(), 1u)); +}}; diff --git a/tests/unit/test_toml_serialize.cpp b/tests/unit/test_toml_serialize.cpp index ca9217cf6..8e42aedc4 100644 --- a/tests/unit/test_toml_serialize.cpp +++ b/tests/unit/test_toml_serialize.cpp @@ -1,9 +1,11 @@ -#include import std; +import mcpplibs.mtest; import mcpp.libs.toml; import mcpp.manifest; +using namespace mcpplibs::mtest; + // `serialize` is what `mcpp publish` writes a normalised manifest with (#690). // The property it owes is a round trip: the text it produces parses into the // same tree AND the same set of explicit tables, because the manifest reader @@ -44,21 +46,21 @@ bool same(const t::Value& a, const t::Value& b) { void expect_round_trip(std::string_view source, std::string_view label) { auto first = t::parse(source); - ASSERT_TRUE(first.has_value()) << label << ": " << first.error().message; + if (auto&& failed = expect(fatal(first.has_value())); !failed) failed << label << ": " << first.error().message; auto text = t::serialize(*first); auto second = t::parse(text); - ASSERT_TRUE(second.has_value()) << label << ": the serialised text does not parse: " + if (auto&& failed = expect(fatal(second.has_value())); !failed) failed << label << ": the serialised text does not parse: " << second.error().message << "\n" << text; - EXPECT_TRUE(same(t::Value{first->root()}, t::Value{second->root()})) + expect(bool(same(t::Value{first->root()}, t::Value{second->root()}))) << label << ": the tree changed\n" << text; - EXPECT_EQ(first->explicit_tables(), second->explicit_tables()) + expect(eq(first->explicit_tables(), second->explicit_tables())) << label << ": the explicit tables changed\n" << text; // The manifest reader sees the same dependencies. A namespace table that // came back as a dotted selector, or the reverse, changes the keys here. auto m1 = mcpp::manifest::parse_string(source); auto m2 = mcpp::manifest::parse_string(text); - ASSERT_EQ(m1.has_value(), m2.has_value()) << label; + expect(fatal(eq(m1.has_value(), m2.has_value()))) << label; if (!m1) return; auto keys = [](auto const& map) { std::vector out; @@ -66,16 +68,16 @@ void expect_round_trip(std::string_view source, std::string_view label) { out.push_back(std::format("{}|{}|{}|{}", k, v.version, v.path, v.namespace_)); return out; }; - EXPECT_EQ(keys(m1->dependencies), keys(m2->dependencies)) << label << "\n" << text; - EXPECT_EQ(keys(m1->devDependencies), keys(m2->devDependencies)) << label; - EXPECT_EQ(keys(m1->buildDependencies), keys(m2->buildDependencies)) << label; - EXPECT_EQ(m1->package.name, m2->package.name) << label; - EXPECT_EQ(m1->package.version, m2->package.version) << label; + expect(eq(keys(m1->dependencies), keys(m2->dependencies))) << label << "\n" << text; + expect(eq(keys(m1->devDependencies), keys(m2->devDependencies))) << label; + expect(eq(keys(m1->buildDependencies), keys(m2->buildDependencies))) << label; + expect(eq(m1->package.name, m2->package.name)) << label; + expect(eq(m1->package.version, m2->package.version)) << label; } } // namespace -TEST(TomlSerialize, HeadersInlineTablesAndDottedKeys) { +UTest headers_inline_tables_and_dotted_keys{"TomlSerialize/HeadersInlineTablesAndDottedKeys", [] { expect_round_trip(R"( [package] name = "demo" @@ -108,9 +110,9 @@ extra = { version = "2.0", features = ["a"] } kind = "bin" main = "src/main.cpp" )", "inline fixture"); -} +}}; -TEST(TomlSerialize, ImplicitContainerWithExplicitDescendant) { +UTest implicit_container_with_explicit_descendant{"TomlSerialize/ImplicitContainerWithExplicitDescendant", [] { // `dependencies` is created by a dotted key at the root and also has an // explicit descendant: its own entries must stay dotted keys. expect_round_trip(R"( @@ -123,13 +125,13 @@ bar = "2.0" name = "p" version = "0.1.0" )", "implicit container"); -} +}}; // The reader accepts every TOML 1.0 escape (#867), so a value may hold any // control character; the writer escapes each one, so the text it produces is // TOML and reads back as the same value. `\0` is this reader's extension and // not TOML 1.0, so it is written as `\u0000`. -TEST(TomlSerialize, ControlCharactersAreEscaped) { +UTest control_characters_are_escaped{"TomlSerialize/ControlCharactersAreEscaped", [] { expect_round_trip(R"( [package] name = "p" @@ -143,31 +145,31 @@ description = "b\b f\f t\t n\n r\r e\u001B d\u007F z\u0000 nul\0 ué" const auto text = t::escape_string(all); for (char c : text) { const auto u = static_cast(c); - EXPECT_FALSE(u < 0x20 || u == 0x7F) << "an unescaped control character in " << text; + expect(!(u < 0x20 || u == 0x7F)) << "an unescaped control character in " << text; } - EXPECT_EQ(text.find("\\0"), std::string::npos) << "\\0 is not TOML 1.0: " << text; + expect(eq(text.find("\\0"), std::string::npos)) << "\\0 is not TOML 1.0: " << text; auto back = t::parse("v = " + text + "\n"); - ASSERT_TRUE(back.has_value()) << back.error().message << "\n" << text; - EXPECT_EQ(back->root().at("v").as_string(), all); -} + if (auto&& failed = expect(fatal(back.has_value())); !failed) failed << back.error().message << "\n" << text; + expect(eq(back->root().at("v").as_string(), all)); +}}; -TEST(TomlSerialize, RepositoryManifestsRoundTrip) { +UTest repository_manifests_round_trip{"TomlSerialize/RepositoryManifestsRoundTrip", [] { // Every manifest in the repository's examples, and the project's own. The // repository is located from this file's own path rather than from the // working directory, and the count is asserted, so a run that found no // file cannot pass as one that round-tripped them all. auto here = std::filesystem::path(std::source_location::current().file_name()); auto repo = here.parent_path().parent_path().parent_path(); - ASSERT_TRUE(std::filesystem::exists(repo / "mcpp.toml")) << repo.string(); + expect(fatal(std::filesystem::exists(repo / "mcpp.toml"))) << repo.string(); std::vector files{ repo / "mcpp.toml" }; for (auto const& e : std::filesystem::recursive_directory_iterator(repo / "examples")) if (e.is_regular_file() && e.path().filename() == "mcpp.toml") files.push_back(e.path()); - EXPECT_GE(files.size(), 10u); + expect(ge(files.size(), 10u)); for (auto const& f : files) { std::ifstream is(f, std::ios::binary); std::stringstream ss; ss << is.rdbuf(); expect_round_trip(ss.str(), f.string()); } -} +}}; diff --git a/tests/unit/test_tool_store.cpp b/tests/unit/test_tool_store.cpp index 55c9f2564..85c796746 100644 --- a/tests/unit/test_tool_store.cpp +++ b/tests/unit/test_tool_store.cpp @@ -8,12 +8,14 @@ // stamp, a change to a source moves it, and a change under the build products // does not. -#include #include import std; +import mcpplibs.mtest; import mcpp.build.tool_store; +using namespace mcpplibs::mtest; + namespace fs = std::filesystem; namespace { @@ -39,29 +41,29 @@ struct Tree { // masked by that, so every edit below also changes the length. } // namespace -TEST(ToolStoreStamp, TheSameTreeGivesTheSameStamp) { +UTest the_same_tree_gives_the_same_stamp{"ToolStoreStamp/TheSameTreeGivesTheSameStamp", [] { Tree t("mcpp_tool_store_stamp_same"); const auto a = mcpp::build::tool_store::tree_stamp(t.root); const auto b = mcpp::build::tool_store::tree_stamp(t.root); - EXPECT_EQ(a, b); - EXPECT_EQ(a.size(), 16u) << "sixteen hex digits, the fingerprint module's shape"; -} + expect(eq(a, b)); + expect(eq(a.size(), 16u)) << "sixteen hex digits, the fingerprint module's shape"; +}}; -TEST(ToolStoreStamp, AnEditToASourceMovesTheStampInBothDirections) { +UTest an_edit_to_a_source_moves_the_stamp_in_both_directions{"ToolStoreStamp/AnEditToASourceMovesTheStampInBothDirections", [] { Tree t("mcpp_tool_store_stamp_edit"); const auto before = mcpp::build::tool_store::tree_stamp(t.root); t.write("src/gen.cpp", "int main() { return 42; } // edited\n"); const auto edited = mcpp::build::tool_store::tree_stamp(t.root); - EXPECT_NE(before, edited); + expect(ne(before, edited)); // The reversal is a new state too: the file's length is back, its // modification time is not, and a key that only ever grew would miss // exactly this step. t.write("src/gen.cpp", "int main() { return 41; }\n"); const auto reverted = mcpp::build::tool_store::tree_stamp(t.root); - EXPECT_NE(edited, reverted); -} + expect(ne(edited, reverted)); +}}; -TEST(ToolStoreStamp, BuildProductsAndTheVersionControlDirectoryDoNotCount) { +UTest build_products_and_the_version_control_directory_do_not_count{"ToolStoreStamp/BuildProductsAndTheVersionControlDirectoryDoNotCount", [] { Tree t("mcpp_tool_store_stamp_target"); const auto before = mcpp::build::tool_store::tree_stamp(t.root); t.write("target/x/gen.o", "a different object of another length"); @@ -71,33 +73,33 @@ TEST(ToolStoreStamp, BuildProductsAndTheVersionControlDirectoryDoNotCount) { t.write(".mcpp/stamp", "x"); t.write("compile_commands.json", "[]"); const auto after = mcpp::build::tool_store::tree_stamp(t.root); - EXPECT_EQ(before, after) << "build products, .git, .mcpp and the compile database are excluded"; -} + expect(eq(before, after)) << "build products, .git, .mcpp and the compile database are excluded"; +}}; // #705: a consumer nested in the tool's tree -- a fixture, an example -- wrote // its own sources and outputs under the tool's root, so every build of the // consumer moved the tool's key and rebuilt the tool. A directory with its own // mcpp.toml is another package and is not this tool's input. A directory // without one is still an ordinary part of the tree. -TEST(ToolStoreStamp, ANestedPackageIsNotPartOfTheTree) { +UTest a_nested_package_is_not_part_of_the_tree{"ToolStoreStamp/ANestedPackageIsNotPartOfTheTree", [] { Tree t("mcpp_tool_store_stamp_nested"); fs::create_directories(t.root / "tests" / "consumer" / "src"); t.write("tests/consumer/mcpp.toml", "[package]\nname = \"consumer\"\n"); const auto before = mcpp::build::tool_store::tree_stamp(t.root); t.write("tests/consumer/src/main.cpp", "int main() { return 0; }\n"); t.write("tests/consumer/build.mcpp", "// generated by the consumer's prepare\n"); - EXPECT_EQ(before, mcpp::build::tool_store::tree_stamp(t.root)); + expect(eq(before, mcpp::build::tool_store::tree_stamp(t.root))); fs::create_directories(t.root / "tests" / "data"); t.write("tests/data/input.txt", "an ordinary input of the tool"); - EXPECT_NE(before, mcpp::build::tool_store::tree_stamp(t.root)); -} + expect(ne(before, mcpp::build::tool_store::tree_stamp(t.root))); +}}; // The sub-build scratch (mcpp#641, item 3). Every object path the sub-build // writes is appended to it, so it spends no length on names the entry already // records; it still separates two consumers of one entry, and a re-run by the // same consumer finds the same directory. -TEST(ToolStoreScratch, IsShortPerConsumerAndStable) { +UTest is_short_per_consumer_and_stable{"ToolStoreScratch/IsShortPerConsumerAndStable", [] { const fs::path cache{"/home/u/.mcpp/build-cache/v1"}; const fs::path entry = cache / "tool" / "mcpplibs" / "installer@0.1.0+path.e285b2f02cb1c429" / "7005eaf9bd8c80ef"; @@ -107,11 +109,11 @@ TEST(ToolStoreScratch, IsShortPerConsumerAndStable) { const auto other = mcpp::build::tool_store::scratch_dir( cache, entry.parent_path() / "0000000000000000", "/work/app"); - EXPECT_EQ(a1, a2); - EXPECT_NE(a1, b); - EXPECT_NE(a1, other); - EXPECT_EQ(a1.parent_path(), cache / "tool" / ".build"); - EXPECT_EQ(a1.filename().string().size(), 16u) << a1; + expect(eq(a1, a2)); + expect(ne(a1, b)); + expect(ne(a1, other)); + expect(eq(a1.parent_path(), cache / "tool" / ".build")); + expect(eq(a1.filename().string().size(), 16u)) << a1; // The old scratch lived inside the entry, as `/build-<16 hex>`. - EXPECT_LT(a1.generic_string().size(), (entry / "build-0123456789abcdef").generic_string().size()); -} + expect(lt(a1.generic_string().size(), (entry / "build-0123456789abcdef").generic_string().size())); +}}; diff --git a/tests/unit/test_toolchain_compat.cpp b/tests/unit/test_toolchain_compat.cpp index 9ea01d539..7ab1b9843 100644 --- a/tests/unit/test_toolchain_compat.cpp +++ b/tests/unit/test_toolchain_compat.cpp @@ -1,9 +1,11 @@ -#include import std; +import mcpplibs.mtest; import mcpp.toolchain.platform; import mcpp.toolchain.compat; +using namespace mcpplibs::mtest; + using namespace mcpp::toolchain::compat; static std::string host_musl() { @@ -12,97 +14,97 @@ static std::string host_musl() { // ── canonical spellings pass through unchanged ─────────────────────────────── -TEST(Compat, CanonicalFamiliesPassThrough) { +UTest canonical_families_pass_through{"Compat/CanonicalFamiliesPassThrough", [] { auto g = normalize_spec("gcc", "16.1.0"); - ASSERT_TRUE(g.has_value()); - EXPECT_EQ(g->family, "gcc"); - EXPECT_EQ(g->version, "16.1.0"); - EXPECT_TRUE(g->target.empty()); - EXPECT_FALSE(g->changed); + expect(fatal(g.has_value())); + expect(eq(g->family, "gcc")); + expect(eq(g->version, "16.1.0")); + expect(g->target.empty()); + expect(!g->changed); auto l = normalize_spec("llvm", "20.1.7"); - ASSERT_TRUE(l.has_value()); - EXPECT_EQ(l->family, "llvm"); - EXPECT_FALSE(l->changed); + expect(fatal(l.has_value())); + expect(eq(l->family, "llvm")); + expect(!l->changed); auto m = normalize_spec("msvc", "system"); - ASSERT_TRUE(m.has_value()); - EXPECT_EQ(m->family, "msvc"); - EXPECT_EQ(m->version, "system"); - EXPECT_FALSE(m->changed); -} + expect(fatal(m.has_value())); + expect(eq(m->family, "msvc")); + expect(eq(m->version, "system")); + expect(!m->changed); +}}; // ── §6.2 alias table: every legacy row round-trips ─────────────────────────── -TEST(Compat, MuslVersionSuffixBecomesTarget) { +UTest musl_version_suffix_becomes_target{"Compat/MuslVersionSuffixBecomesTarget", [] { auto s = normalize_spec("gcc", "15.1.0-musl"); - ASSERT_TRUE(s.has_value()); - EXPECT_EQ(s->family, "gcc"); - EXPECT_EQ(s->version, "15.1.0"); - EXPECT_EQ(s->target.str(), host_musl()); - EXPECT_TRUE(s->changed); - EXPECT_FALSE(s->hint.empty()); -} - -TEST(Compat, MuslGccCompilerNameBecomesTarget) { + expect(fatal(s.has_value())); + expect(eq(s->family, "gcc")); + expect(eq(s->version, "15.1.0")); + expect(eq(s->target.str(), host_musl())); + expect(bool(s->changed)); + expect(!(s->hint.empty())); +}}; + +UTest musl_gcc_compiler_name_becomes_target{"Compat/MuslGccCompilerNameBecomesTarget", [] { auto s = normalize_spec("musl-gcc", "15.1.0"); - ASSERT_TRUE(s.has_value()); - EXPECT_EQ(s->family, "gcc"); - EXPECT_EQ(s->version, "15.1.0"); - EXPECT_EQ(s->target.str(), host_musl()); - EXPECT_TRUE(s->changed); -} - -TEST(Compat, TripleNamedGccBecomesTarget) { + expect(fatal(s.has_value())); + expect(eq(s->family, "gcc")); + expect(eq(s->version, "15.1.0")); + expect(eq(s->target.str(), host_musl())); + expect(bool(s->changed)); +}}; + +UTest triple_named_gcc_becomes_target{"Compat/TripleNamedGccBecomesTarget", [] { auto s = normalize_spec("aarch64-linux-musl-gcc", "16.1.0"); - ASSERT_TRUE(s.has_value()); - EXPECT_EQ(s->family, "gcc"); - EXPECT_EQ(s->version, "16.1.0"); - EXPECT_EQ(s->target.str(), "aarch64-linux-musl"); - EXPECT_TRUE(s->changed); -} - -TEST(Compat, MingwAndMingwCrossCollapseToWindowsGnu) { + expect(fatal(s.has_value())); + expect(eq(s->family, "gcc")); + expect(eq(s->version, "16.1.0")); + expect(eq(s->target.str(), "aarch64-linux-musl")); + expect(bool(s->changed)); +}}; + +UTest mingw_and_mingw_cross_collapse_to_windows_gnu{"Compat/MingwAndMingwCrossCollapseToWindowsGnu", [] { // One concept, two host-split legacy names → the SAME normalized identity. auto native = normalize_spec("mingw", "16.1.0"); auto cross = normalize_spec("mingw-cross", "16.1.0"); - ASSERT_TRUE(native.has_value()); - ASSERT_TRUE(cross.has_value()); - EXPECT_EQ(native->family, "gcc"); - EXPECT_EQ(cross->family, "gcc"); - EXPECT_EQ(native->target.str(), "x86_64-windows-gnu"); - EXPECT_EQ(cross->target.str(), "x86_64-windows-gnu"); - EXPECT_TRUE(native->changed); - EXPECT_TRUE(cross->changed); -} - -TEST(Compat, ClangAliasBecomesLlvm) { + expect(fatal(native.has_value())); + expect(fatal(cross.has_value())); + expect(eq(native->family, "gcc")); + expect(eq(cross->family, "gcc")); + expect(eq(native->target.str(), "x86_64-windows-gnu")); + expect(eq(cross->target.str(), "x86_64-windows-gnu")); + expect(bool(native->changed)); + expect(bool(cross->changed)); +}}; + +UTest clang_alias_becomes_llvm{"Compat/ClangAliasBecomesLlvm", [] { auto s = normalize_spec("clang", "20.1.7"); - ASSERT_TRUE(s.has_value()); - EXPECT_EQ(s->family, "llvm"); - EXPECT_EQ(s->version, "20.1.7"); - EXPECT_TRUE(s->changed); -} + expect(fatal(s.has_value())); + expect(eq(s->family, "llvm")); + expect(eq(s->version, "20.1.7")); + expect(bool(s->changed)); +}}; -TEST(Compat, PartialMuslVersionSuffix) { +UTest partial_musl_version_suffix{"Compat/PartialMuslVersionSuffix", [] { // "gcc 15-musl" — partial version with the legacy suffix. auto s = normalize_spec("gcc", "15-musl"); - ASSERT_TRUE(s.has_value()); - EXPECT_EQ(s->version, "15"); - EXPECT_EQ(s->target.str(), host_musl()); -} + expect(fatal(s.has_value())); + expect(eq(s->version, "15")); + expect(eq(s->target.str(), host_musl())); +}}; -TEST(Compat, XimNamespacePrefixStripped) { +UTest xim_namespace_prefix_stripped{"Compat/XimNamespacePrefixStripped", [] { auto s = normalize_spec("xim:gcc", "16.1.0"); - ASSERT_TRUE(s.has_value()); - EXPECT_EQ(s->family, "gcc"); - EXPECT_FALSE(s->changed); -} + expect(fatal(s.has_value())); + expect(eq(s->family, "gcc")); + expect(!s->changed); +}}; // ── rejects ───────────────────────────────────────────────────────────────── -TEST(Compat, RejectsUnknownFamilies) { - EXPECT_FALSE(normalize_spec("tcc", "1.0").has_value()); - EXPECT_FALSE(normalize_spec("llvm", "20.1.7-musl").has_value()); // llvm has no musl flavor - EXPECT_FALSE(normalize_spec("not-a-triple-gcc", "1.0").has_value()); -} +UTest rejects_unknown_families{"Compat/RejectsUnknownFamilies", [] { + expect(!(normalize_spec("tcc", "1.0").has_value())); + expect(!(normalize_spec("llvm", "20.1.7-musl").has_value())); // llvm has no musl flavor + expect(!(normalize_spec("not-a-triple-gcc", "1.0").has_value())); +}}; diff --git a/tests/unit/test_toolchain_descriptor.cpp b/tests/unit/test_toolchain_descriptor.cpp index 8e62d4fba..86e086ea3 100644 --- a/tests/unit/test_toolchain_descriptor.cpp +++ b/tests/unit/test_toolchain_descriptor.cpp @@ -1,11 +1,13 @@ -#include import std; +import mcpplibs.mtest; import mcpplibs.os.platform; import mcpp.toolchain.model; import mcpp.toolchain.registry; import mcpp.toolchain.triple; +using namespace mcpplibs::mtest; + using namespace mcpp::toolchain; namespace { @@ -64,20 +66,20 @@ std::string host_frontend_name() { // feature in which that stops working. The criterion has a denominator: the // SAME package object resolves the SAME frontend as before the descriptor // existed, which is what the assertion below measures. -TEST(PayloadDescriptor, NoDescriptorResolvesExactlyAsBefore) { +UTest no_descriptor_resolves_exactly_as_before{"PayloadDescriptor/NoDescriptorResolvesExactlyAsBefore", [] { FakePayload fp{"none"}; auto clang = fp.add_compiler("bin/" + host_frontend_name()); auto pkg = bin_shaped_pkg(); auto d = read_payload_descriptor(fp.root); - ASSERT_TRUE(d.has_value()) << d.error(); - EXPECT_FALSE(d->has_value()) << "no file means no answer, not an error"; + if (auto&& failed = expect(fatal(d.has_value())); !failed) failed << d.error(); + expect(!d->has_value()) << "no file means no answer, not an error"; auto found = payload_frontend(fp.root, pkg); - ASSERT_TRUE(found.has_value()) << found.error(); - EXPECT_EQ(*found, clang); - EXPECT_EQ(payload_frontend_dir(fp.root, pkg), fp.root / "bin"); -} + if (auto&& failed = expect(fatal(found.has_value())); !failed) failed << found.error(); + expect(eq(*found, clang)); + expect(eq(payload_frontend_dir(fp.root, pkg), fp.root / "bin")); +}}; // ─── The engine stops knowing the layout (C) ──────────────────────────────── // @@ -86,7 +88,7 @@ TEST(PayloadDescriptor, NoDescriptorResolvesExactlyAsBefore) { // guess would look. Here `bin/` is empty and the compiler is five levels down // in the NDK's own shape -- so a resolution that succeeds could only have come // from the descriptor. -TEST(PayloadDescriptor, TheDescriptorAnswersWhereTheGuessCannot) { +UTest the_descriptor_answers_where_the_guess_cannot{"PayloadDescriptor/TheDescriptorAnswersWhereTheGuessCannot", [] { FakePayload fp{"frontend"}; // NOT the host-specific name, deliberately: the descriptor says exactly // where the compiler is, so the payload is free to spell it any way it @@ -103,38 +105,38 @@ TEST(PayloadDescriptor, TheDescriptorAnswersWhereTheGuessCannot) { // `some-host-tag` is not a host tag this engine can compute, which is the // point: the path is the payload's, not a derivation. auto found = payload_frontend(fp.root, pkg); - ASSERT_TRUE(found.has_value()) << found.error(); - EXPECT_EQ(*found, deep); + if (auto&& failed = expect(fatal(found.has_value())); !failed) failed << found.error(); + expect(eq(*found, deep)); // AND THE MESSAGE NAMES WHERE THE SEARCH HAPPENED. A refusal naming // `/bin` while the lookup read the descriptor's path is the failure // this repository records most often: the lookup is repaired and the // message is not. - EXPECT_EQ(payload_frontend_dir(fp.root, pkg), deep.parent_path()); -} + expect(eq(payload_frontend_dir(fp.root, pkg), deep.parent_path())); +}}; // A descriptor naming a frontend that is not there is "nothing found where we // looked" -- empty, the caller's long-standing cue -- and NOT an error. The // two outcomes have different repairs: install the payload again, versus fix // the recipe. -TEST(PayloadDescriptor, ANamedFrontendThatIsAbsentIsEmptyNotAnError) { +UTest a_named_frontend_that_is_absent_is_empty_not_an_error{"PayloadDescriptor/ANamedFrontendThatIsAbsentIsEmptyNotAnError", [] { FakePayload fp{"absent"}; fp.add_compiler("bin/" + host_frontend_name()); // the guess WOULD find this fp.write_descriptor(R"({"schema": 1, "frontend": "opt/bin/clang++"})"); auto found = payload_frontend(fp.root, bin_shaped_pkg()); - ASSERT_TRUE(found.has_value()) << found.error(); - EXPECT_TRUE(found->empty()) + if (auto&& failed = expect(fatal(found.has_value())); !failed) failed << found.error(); + expect(found->empty()) << "the payload said where its compiler is; the guess is not a " "second opinion"; -} +}}; // ─── Present and malformed is refused, naming the file (C) ────────────────── // // This is the property that makes the descriptor safe to add. Without it a typo // reads as "an older payload": the engine falls back to a hardcoded path for a // layout that has moved, and the build fails somewhere else entirely. -TEST(PayloadDescriptor, EveryMalformedShapeIsRefusedByName) { +UTest every_malformed_shape_is_refused_by_name{"PayloadDescriptor/EveryMalformedShapeIsRefusedByName", [] { struct Case { std::string_view why, json, mustMention; }; const Case cases[] = { { "not JSON", "{ not json", "JSON" }, @@ -179,25 +181,24 @@ TEST(PayloadDescriptor, EveryMalformedShapeIsRefusedByName) { fp.write_descriptor(c.json); auto d = read_payload_descriptor(fp.root); - ASSERT_FALSE(d.has_value()) << c.why << " was accepted"; + expect(fatal(!d.has_value())) << c.why << " was accepted"; // THE FILE IS NAMED IN EVERY ONE. A message that says only "invalid // descriptor" leaves the reader to find which payload wrote it. - EXPECT_NE(d.error().find(".mcpp-toolchain.json"), std::string::npos) - << c.why << ": " << d.error(); - EXPECT_NE(d.error().find(c.mustMention), std::string::npos) + expect(ne(d.error().find(".mcpp-toolchain.json"), std::string::npos)) << c.why << ": " << d.error(); + if (auto&& failed = expect(ne(d.error().find(c.mustMention), std::string::npos)); !failed) failed << c.why << ": " << d.error(); // And the refusal travels: the frontend lookup cannot express it as // "not found", so it propagates rather than falling back to the guess // -- even though the guess would have succeeded here. auto found = payload_frontend(fp.root, bin_shaped_pkg()); - ASSERT_FALSE(found.has_value()) << c.why << " fell back to the guess"; - EXPECT_EQ(found.error(), d.error()) << c.why; + expect(fatal(!found.has_value())) << c.why << " fell back to the guess"; + expect(eq(found.error(), d.error())) << c.why; } -} +}}; // ─── The three answers, read as written ───────────────────────────────────── -TEST(PayloadDescriptor, TheThreeKeysAreCarriedThrough) { +UTest the_three_keys_are_carried_through{"PayloadDescriptor/TheThreeKeysAreCarriedThrough", [] { FakePayload fp{"full"}; fp.add_compiler("toolchains/llvm/prebuilt/linux-x86_64/bin/clang++"); fp.write_descriptor(R"({ @@ -207,27 +208,26 @@ TEST(PayloadDescriptor, TheThreeKeysAreCarriedThrough) { "std_module_defines": ["__BIONIC_CTYPE_INLINE=", "_FORTIFY_SOURCE=0"] })"); auto d = read_payload_descriptor(fp.root); - ASSERT_TRUE(d.has_value()) << d.error(); - ASSERT_TRUE(d->has_value()); - EXPECT_EQ((*d)->schema, 1); - EXPECT_EQ((*d)->frontend, - "toolchains/llvm/prebuilt/linux-x86_64/bin/clang++"); - EXPECT_EQ((*d)->platformFloor, "21"); - ASSERT_EQ((*d)->stdModuleDefines.size(), 2u); - EXPECT_EQ((*d)->stdModuleDefines[0], "__BIONIC_CTYPE_INLINE="); - EXPECT_EQ((*d)->stdModuleDefines[1], "_FORTIFY_SOURCE=0"); + if (auto&& failed = expect(fatal(d.has_value())); !failed) failed << d.error(); + expect(fatal(d->has_value())); + expect(eq((*d)->schema, 1)); + expect(eq((*d)->frontend, "toolchains/llvm/prebuilt/linux-x86_64/bin/clang++")); + expect(eq((*d)->platformFloor, "21")); + expect(fatal(eq((*d)->stdModuleDefines.size(), 2u))); + expect(eq((*d)->stdModuleDefines[0], "__BIONIC_CTYPE_INLINE=")); + expect(eq((*d)->stdModuleDefines[1], "_FORTIFY_SOURCE=0")); // ALL THREE KEYS ARE OPTIONAL. A payload that answers one question and // not the others is a payload, not a malformed file -- otherwise a second // SDK would have to fabricate answers it does not have. fp.write_descriptor(R"({"schema": 1})"); auto bare = read_payload_descriptor(fp.root); - ASSERT_TRUE(bare.has_value()) << bare.error(); - ASSERT_TRUE(bare->has_value()); - EXPECT_TRUE((*bare)->frontend.empty()); - EXPECT_TRUE((*bare)->platformFloor.empty()); - EXPECT_TRUE((*bare)->stdModuleDefines.empty()); -} + if (auto&& failed = expect(fatal(bare.has_value())); !failed) failed << bare.error(); + expect(fatal(bare->has_value())); + expect((*bare)->frontend.empty()); + expect((*bare)->platformFloor.empty()); + expect((*bare)->stdModuleDefines.empty()); +}}; // ─── Found from the compiler, by walking rather than counting ─────────────── // @@ -235,33 +235,32 @@ TEST(PayloadDescriptor, TheThreeKeysAreCarriedThrough) { // hand. Counting components is the fact that changes silently when a layout // does, so the search walks up -- and is bounded, so a compiler that belongs // to no payload cannot adopt a descriptor from some ancestor directory. -TEST(PayloadDescriptor, FoundFromTheCompilerAndTheWalkIsBounded) { +UTest found_from_the_compiler_and_the_walk_is_bounded{"PayloadDescriptor/FoundFromTheCompilerAndTheWalkIsBounded", [] { FakePayload fp{"walk"}; auto deep = fp.add_compiler( "toolchains/llvm/prebuilt/linux-x86_64/bin/clang++"); fp.write_descriptor(R"({"schema": 1, "platform_floor": "21"})"); auto d = payload_descriptor_for_compiler(deep); - ASSERT_TRUE(d.has_value()) << d.error(); - ASSERT_TRUE(d->has_value()) << "five levels up was not reached"; - EXPECT_EQ((*d)->platformFloor, "21"); + if (auto&& failed = expect(fatal(d.has_value())); !failed) failed << d.error(); + expect(fatal(d->has_value())) << "five levels up was not reached"; + expect(eq((*d)->platformFloor, "21")); // A refusal still travels through the walk, because a compiler whose // payload describes itself wrongly is the same defect wherever it is read. fp.write_descriptor(R"({"schema": 1, "platform_floor": 21})"); auto bad = payload_descriptor_for_compiler(deep); - ASSERT_FALSE(bad.has_value()); - EXPECT_NE(bad.error().find(".mcpp-toolchain.json"), std::string::npos) - << bad.error(); + expect(fatal(!bad.has_value())); + if (auto&& failed = expect(ne(bad.error().find(".mcpp-toolchain.json"), std::string::npos)); !failed) failed << bad.error(); // Beyond the bound there is no answer. Nine levels is one past it. auto far = fp.add_compiler("a/b/c/d/e/f/g/h/i/clang++"); auto none = payload_descriptor_for_compiler(far); - ASSERT_TRUE(none.has_value()) << none.error(); - EXPECT_FALSE(none->has_value()) + if (auto&& failed = expect(fatal(none.has_value())); !failed) failed << none.error(); + expect(!none->has_value()) << "an unbounded walk adopts a descriptor from a directory that is " "not this payload"; -} +}}; // ─── The fourth key: the program that runs what the payload produces ──────── // @@ -300,27 +299,27 @@ struct FakeStore { }; } // namespace -TEST(PayloadDescriptor, TheRunnerIsCarriedThroughWithTheRootItWasReadFrom) { +UTest the_runner_is_carried_through_with_the_root_it_was_read_from{"PayloadDescriptor/TheRunnerIsCarriedThroughWithTheRootItWasReadFrom", [] { FakePayload fp{"runner"}; fp.write_descriptor(R"({"schema": 1, "runner": "bin/node"})"); auto d = read_payload_descriptor(fp.root); - ASSERT_TRUE(d.has_value()) << d.error(); - ASSERT_TRUE(d->has_value()); - EXPECT_EQ((*d)->runner, "bin/node"); - EXPECT_EQ((*d)->root, fp.root); + if (auto&& failed = expect(fatal(d.has_value())); !failed) failed << d.error(); + expect(fatal(d->has_value())); + expect(eq((*d)->runner, "bin/node")); + expect(eq((*d)->root, fp.root)); // Optional like the other three: a payload whose artefacts run natively // has no runner to name. fp.write_descriptor(R"({"schema": 1})"); auto bare = read_payload_descriptor(fp.root); - ASSERT_TRUE(bare.has_value()) << bare.error(); - ASSERT_TRUE(bare->has_value()); - EXPECT_TRUE((*bare)->runner.empty()); -} + if (auto&& failed = expect(fatal(bare.has_value())); !failed) failed << bare.error(); + expect(fatal(bare->has_value())); + expect((*bare)->runner.empty()); +}}; // THE SHAPE emsdk HAS: the compiler two levels below the root, and the runner // a DEPENDENCY's program named by its absolute path in the same store. -TEST(PayloadDescriptor, ARunnerInTheSameStoreIsTheDefaultForItsCompiler) { +UTest a_runner_in_the_same_store_is_the_default_for_its_compiler{"PayloadDescriptor/ARunnerInTheSameStoreIsTheDefaultForItsCompiler", [] { FakeStore fs{"same"}; auto emsdk = fs.payload("xim-x-emsdk", "6.0.9"); auto compiler = FakeStore::file(emsdk / "emscripten" / "em++"); @@ -330,44 +329,44 @@ TEST(PayloadDescriptor, ARunnerInTheSameStoreIsTheDefaultForItsCompiler) { node.generic_string())); auto argv = payload_default_runner(compiler); - ASSERT_EQ(argv.size(), 1u); - EXPECT_EQ(std::filesystem::path(argv[0]), std::filesystem::weakly_canonical(node)); + expect(fatal(eq(argv.size(), 1u))); + expect(eq(std::filesystem::path(argv[0]), std::filesystem::weakly_canonical(node))); // The relative form resolves against the root the descriptor came from. FakeStore::describe(emsdk, R"({"schema": 1, "runner": "bin/node"})"); auto rel = payload_default_runner(compiler); - ASSERT_EQ(rel.size(), 1u); - EXPECT_EQ(std::filesystem::path(rel[0]), emsdk / "bin/node"); -} + expect(fatal(eq(rel.size(), 1u))); + expect(eq(std::filesystem::path(rel[0]), emsdk / "bin/node")); +}}; // WHAT THE STORE RULE REMOVES: a payload choosing a host interpreter. Ignored // rather than refused, so a machine whose store is elsewhere does not lose its // build over a run-time default. -TEST(PayloadDescriptor, ARunnerOutsideTheStoreIsNotHonoured) { +UTest a_runner_outside_the_store_is_not_honoured{"PayloadDescriptor/ARunnerOutsideTheStoreIsNotHonoured", [] { FakeStore fs{"outside"}; auto emsdk = fs.payload("xim-x-emsdk", "6.0.9"); auto compiler = FakeStore::file(emsdk / "emscripten" / "em++"); FakeStore::describe(emsdk, R"({"schema": 1, "runner": "/usr/bin/node"})"); auto d = read_payload_descriptor(emsdk); - ASSERT_TRUE(d.has_value()) << "an absolute runner is well formed: " << d.error(); - EXPECT_TRUE(payload_default_runner(compiler).empty()) + if (auto&& failed = expect(fatal(d.has_value())); !failed) failed << "an absolute runner is well formed: " << d.error(); + expect(payload_default_runner(compiler).empty()) << "a runner outside the payload's store was honoured"; -} +}}; -TEST(PayloadDescriptor, NoDescriptorOrNoRunnerMeansNoDefault) { +UTest no_descriptor_or_no_runner_means_no_default{"PayloadDescriptor/NoDescriptorOrNoRunnerMeansNoDefault", [] { FakeStore fs{"none"}; auto llvm = fs.payload("xim-x-llvm", "22.1.8"); auto compiler = FakeStore::file(llvm / "bin" / "clang++"); - EXPECT_TRUE(payload_default_runner(compiler).empty()); + expect(payload_default_runner(compiler).empty()); FakeStore::describe(llvm, R"({"schema": 1, "platform_floor": "21"})"); - EXPECT_TRUE(payload_default_runner(compiler).empty()); + expect(payload_default_runner(compiler).empty()); // And a malformed descriptor is absence here; the build refused it first. FakeStore::describe(llvm, R"({"schema": 1, "runner": 7})"); - EXPECT_TRUE(payload_default_runner(compiler).empty()); -} + expect(payload_default_runner(compiler).empty()); +}}; -TEST(PayloadDescriptor, AMalformedRunnerIsRefusedByNameAndByKey) { +UTest a_malformed_runner_is_refused_by_name_and_by_key{"PayloadDescriptor/AMalformedRunnerIsRefusedByNameAndByKey", [] { FakePayload fp{"badrunner"}; for (std::string_view json : { std::string_view(R"({"schema": 1, "runner": 7})"), @@ -380,8 +379,8 @@ TEST(PayloadDescriptor, AMalformedRunnerIsRefusedByNameAndByKey) { }) { fp.write_descriptor(json); auto d = read_payload_descriptor(fp.root); - ASSERT_FALSE(d.has_value()) << "accepted: " << json; - EXPECT_NE(d.error().find("\"runner\""), std::string::npos) << d.error(); - EXPECT_NE(d.error().find(".mcpp-toolchain.json"), std::string::npos) << d.error(); + expect(fatal(!d.has_value())) << "accepted: " << json; + if (auto&& failed = expect(ne(d.error().find("\"runner\""), std::string::npos)); !failed) failed << d.error(); + if (auto&& failed = expect(ne(d.error().find(".mcpp-toolchain.json"), std::string::npos)); !failed) failed << d.error(); } -} +}}; diff --git a/tests/unit/test_toolchain_detect.cpp b/tests/unit/test_toolchain_detect.cpp index 9cedb79cd..fea9f9a8e 100644 --- a/tests/unit/test_toolchain_detect.cpp +++ b/tests/unit/test_toolchain_detect.cpp @@ -1,10 +1,12 @@ -#include import std; +import mcpplibs.mtest; import mcpp.invocation; import mcpp.toolchain.detect; import mcpp.toolchain.probe; +using namespace mcpplibs::mtest; + using namespace mcpp::toolchain; namespace { @@ -65,22 +67,22 @@ std::filesystem::path make_hostile_ld_dir() { #if !defined(_WIN32) // Uses a fake shell script as a compiler — POSIX only. -TEST(ToolchainDetect, ClangVersionOutputIsNotMisclassifiedByGccPaths) { +UTest clang_version_output_is_not_misclassified_by_gcc_paths{"ToolchainDetect/ClangVersionOutputIsNotMisclassifiedByGccPaths", [] { auto clang = make_fake_clang(); TempDirGuard cleanup{clang.parent_path()}; auto tc = detect(clang); - ASSERT_TRUE(tc.has_value()) << tc.error().message; - EXPECT_EQ(tc->compiler, CompilerId::Clang); - EXPECT_EQ(tc->version, "20.1.7"); - EXPECT_EQ(tc->targetTriple, "x86_64-unknown-linux-gnu"); - EXPECT_EQ(tc->stdlibId, "libc++"); - EXPECT_FALSE(tc->hasImportStd); -} + if (auto&& failed = expect(fatal(tc.has_value())); !failed) failed << tc.error().message; + expect(eq(tc->compiler, CompilerId::Clang)); + expect(eq(tc->version, "20.1.7")); + expect(eq(tc->targetTriple, "x86_64-unknown-linux-gnu")); + expect(eq(tc->stdlibId, "libc++")); + expect(!tc->hasImportStd); +}}; #endif // !defined(_WIN32) #if defined(__linux__) -TEST(ToolchainDetect, IgnoresTargetRuntimeLibraryPathDuringProbe) { +UTest ignores_target_runtime_library_path_during_probe{"ToolchainDetect/IgnoresTargetRuntimeLibraryPathDuringProbe", [] { auto clang = make_fake_clang(); TempDirGuard cleanup_clang{clang.parent_path()}; auto hostile = make_hostile_ld_dir(); @@ -89,10 +91,10 @@ TEST(ToolchainDetect, IgnoresTargetRuntimeLibraryPathDuringProbe) { mcpp::invocation::ScopedEnv ld("LD_LIBRARY_PATH", hostile.string()); auto tc = detect(clang); - ASSERT_TRUE(tc.has_value()) << tc.error().message; - EXPECT_EQ(tc->compiler, CompilerId::Clang); - EXPECT_EQ(tc->targetTriple, "x86_64-unknown-linux-gnu"); -} + if (auto&& failed = expect(fatal(tc.has_value())); !failed) failed << tc.error().message; + expect(eq(tc->compiler, CompilerId::Clang)); + expect(eq(tc->targetTriple, "x86_64-unknown-linux-gnu")); +}}; #endif // defined(__linux__) // ─── normalize_driver_output: path-free semantic identity ───────────── @@ -104,7 +106,7 @@ TEST(ToolchainDetect, IgnoresTargetRuntimeLibraryPathDuringProbe) { // etc.) yet behave identically. We now use a normalized `--version` // string as the path-free identity instead. -TEST(NormalizeDriverOutput, TrimsWhitespaceAndCollapsesBlankLines) { +UTest trims_whitespace_and_collapses_blank_lines{"NormalizeDriverOutput/TrimsWhitespaceAndCollapsesBlankLines", [] { std::string raw = " g++ (xim-x-gcc 16.1.0) 16.1.0\n" "\n" @@ -112,13 +114,12 @@ TEST(NormalizeDriverOutput, TrimsWhitespaceAndCollapsesBlankLines) { "\n\n" "This is free software; ...\n"; auto out = normalize_driver_output(raw); - EXPECT_EQ(out, - "g++ (xim-x-gcc 16.1.0) 16.1.0\n" + expect(eq(out, "g++ (xim-x-gcc 16.1.0) 16.1.0\n" "Copyright (C) 2023 Free Software Foundation, Inc.\n" - "This is free software; ..."); -} + "This is free software; ...")); +}}; -TEST(NormalizeDriverOutput, IsStableAcrossInstallPrefixes) { +UTest is_stable_across_install_prefixes{"NormalizeDriverOutput/IsStableAcrossInstallPrefixes", [] { // Same gcc package, two different install locations on disk. // --version output is identical regardless of where the binary lives, // so normalized identity must be identical too. @@ -128,10 +129,10 @@ TEST(NormalizeDriverOutput, IsStableAcrossInstallPrefixes) { std::string from_b = "g++ (xim-x-gcc 16.1.0) 16.1.0\n" "Copyright (C) 2023 Free Software Foundation, Inc.\n"; - EXPECT_EQ(normalize_driver_output(from_a), normalize_driver_output(from_b)); -} + expect(eq(normalize_driver_output(from_a), normalize_driver_output(from_b))); +}}; -TEST(NormalizeDriverOutput, ReplacesLocalInstallPaths) { +UTest replaces_local_install_paths{"NormalizeDriverOutput/ReplacesLocalInstallPaths", [] { std::string a = "clang version 20.1.7\n" "Configuration file: /home/speak/.mcpp/registry/data/xpkgs/llvm/bin/clang.cfg\n"; @@ -139,32 +140,31 @@ TEST(NormalizeDriverOutput, ReplacesLocalInstallPaths) { "clang version 20.1.7\n" "Configuration file: /home/speak/.xlings/data/xpkgs/llvm/bin/clang.cfg\n"; - EXPECT_EQ(normalize_driver_output(a), normalize_driver_output(b)); - EXPECT_EQ(normalize_driver_output(a).find("/home/"), std::string::npos); -} + expect(eq(normalize_driver_output(a), normalize_driver_output(b))); + expect(eq(normalize_driver_output(a).find("/home/"), std::string::npos)); +}}; -TEST(NormalizeDriverOutput, DistinguishesDifferentVersions) { +UTest distinguishes_different_versions{"NormalizeDriverOutput/DistinguishesDifferentVersions", [] { std::string a = "g++ (xim-x-gcc 16.1.0) 16.1.0\n"; std::string b = "g++ (xim-x-gcc 15.1.0) 15.1.0\n"; - EXPECT_NE(normalize_driver_output(a), normalize_driver_output(b)); -} + expect(ne(normalize_driver_output(a), normalize_driver_output(b))); +}}; -TEST(NormalizeDriverOutput, EmptyInputProducesEmpty) { - EXPECT_EQ(normalize_driver_output(""), ""); - EXPECT_EQ(normalize_driver_output("\n\n\n"), ""); -} +UTest empty_input_produces_empty{"NormalizeDriverOutput/EmptyInputProducesEmpty", [] { + expect(eq(normalize_driver_output(""), "")); + expect(eq(normalize_driver_output("\n\n\n"), "")); +}}; #if !defined(_WIN32) // ─── detect() populates driverIdent ───────────────────────────────── -TEST(ToolchainDetect, PopulatesDriverIdentFromVersionOutput) { +UTest populates_driver_ident_from_version_output{"ToolchainDetect/PopulatesDriverIdentFromVersionOutput", [] { auto clang = make_fake_clang(); TempDirGuard cleanup{clang.parent_path()}; auto tc = detect(clang); - ASSERT_TRUE(tc.has_value()) << tc.error().message; - EXPECT_FALSE(tc->driverIdent.empty()) + if (auto&& failed = expect(fatal(tc.has_value())); !failed) failed << tc.error().message; + expect(!(tc->driverIdent.empty())) << "detect() should populate Toolchain::driverIdent from --version output"; - EXPECT_NE(tc->driverIdent.find("clang version 20.1.7"), std::string::npos) - << "driverIdent should contain the --version header: " << tc->driverIdent; -} + if (auto&& failed = expect(ne(tc->driverIdent.find("clang version 20.1.7"), std::string::npos)); !failed) failed << "driverIdent should contain the --version header: " << tc->driverIdent; +}}; #endif // !defined(_WIN32) diff --git a/tests/unit/test_toolchain_dialect.cpp b/tests/unit/test_toolchain_dialect.cpp index 77200150c..94f50f789 100644 --- a/tests/unit/test_toolchain_dialect.cpp +++ b/tests/unit/test_toolchain_dialect.cpp @@ -1,11 +1,13 @@ -#include import std; +import mcpplibs.mtest; import mcpp.manifest; import mcpp.toolchain.dialect; import mcpp.toolchain.model; import mcpp.toolchain.registry; +using namespace mcpplibs::mtest; + using namespace mcpp::toolchain; namespace { @@ -17,170 +19,168 @@ Toolchain make_tc(CompilerId id, std::string triple = {}) { } } // namespace -TEST(CommandDialect, GnuSpellings) { +UTest gnu_spellings{"CommandDialect/GnuSpellings", [] { auto tc = make_tc(CompilerId::GCC); const auto& d = dialect_for(tc); - EXPECT_EQ(d.id, "gnu"); - EXPECT_EQ(d.includePrefix, "-I"); - EXPECT_EQ(d.stdPrefix, "-std="); - EXPECT_EQ(d.outputObjPrefix, "-o "); - EXPECT_EQ(d.objExt, ".o"); - EXPECT_EQ(d.archiveCmd, "$ar rcs $out $in"); - EXPECT_FALSE(d.rspfileLink); - EXPECT_EQ(d.linkStyle, CommandDialect::LinkStyle::Driver); + expect(eq(d.id, "gnu")); + expect(eq(d.includePrefix, "-I")); + expect(eq(d.stdPrefix, "-std=")); + expect(eq(d.outputObjPrefix, "-o ")); + expect(eq(d.objExt, ".o")); + expect(eq(d.archiveCmd, "$ar rcs $out $in")); + expect(!d.rspfileLink); + expect(eq(d.linkStyle, CommandDialect::LinkStyle::Driver)); // Clang and MinGW share the gnu row. - EXPECT_EQ(dialect_for(make_tc(CompilerId::Clang)).id, "gnu"); - EXPECT_EQ(dialect_for(make_tc(CompilerId::GCC, "x86_64-w64-mingw32")).id, "gnu"); -} + expect(eq(dialect_for(make_tc(CompilerId::Clang)).id, "gnu")); + expect(eq(dialect_for(make_tc(CompilerId::GCC, "x86_64-w64-mingw32")).id, "gnu")); +}}; -TEST(CommandDialect, MsvcSpellings) { +UTest msvc_spellings{"CommandDialect/MsvcSpellings", [] { const auto& d = dialect_for(make_tc(CompilerId::MSVC)); - EXPECT_EQ(d.id, "msvc"); - EXPECT_EQ(d.includePrefix, "/I"); - EXPECT_EQ(d.stdPrefix, "/std:"); - EXPECT_EQ(d.outputObjPrefix, "/Fo:"); - EXPECT_EQ(d.objExt, ".obj"); + expect(eq(d.id, "msvc")); + expect(eq(d.includePrefix, "/I")); + expect(eq(d.stdPrefix, "/std:")); + expect(eq(d.outputObjPrefix, "/Fo:")); + expect(eq(d.objExt, ".obj")); // Each object carries its debug information: no compile writes through // the shared PDB server another compile's unit holds. - EXPECT_EQ(d.debugFlags, "/Z7"); - EXPECT_TRUE(d.rspfileLink); - EXPECT_EQ(d.linkStyle, CommandDialect::LinkStyle::SeparateLinker); -} + expect(eq(d.debugFlags, "/Z7")); + expect(bool(d.rspfileLink)); + expect(eq(d.linkStyle, CommandDialect::LinkStyle::SeparateLinker)); +}}; -TEST(BmiTraitsSpellings, ModuleFlagRowsAreConsistent) { +UTest module_flag_rows_are_consistent{"BmiTraitsSpellings/ModuleFlagRowsAreConsistent", [] { auto gcc = bmi_traits(make_tc(CompilerId::GCC)); - EXPECT_EQ(gcc.compileModulesFlag, " -fmodules"); - EXPECT_TRUE(gcc.stdBmiUsePrefix.empty()); - EXPECT_TRUE(gcc.moduleOutputPrefix.empty()); + expect(eq(gcc.compileModulesFlag, " -fmodules")); + expect(gcc.stdBmiUsePrefix.empty()); + expect(gcc.moduleOutputPrefix.empty()); auto clang = bmi_traits(make_tc(CompilerId::Clang)); - EXPECT_TRUE(clang.compileModulesFlag.empty()); - EXPECT_EQ(clang.stdBmiUsePrefix, " -fmodule-file=std="); - EXPECT_EQ(clang.moduleOutputPrefix, " -fmodule-output="); - EXPECT_EQ(clang.bmiSearchPrefix, " -fprebuilt-module-path="); + expect(clang.compileModulesFlag.empty()); + expect(eq(clang.stdBmiUsePrefix, " -fmodule-file=std=")); + expect(eq(clang.moduleOutputPrefix, " -fmodule-output=")); + expect(eq(clang.bmiSearchPrefix, " -fprebuilt-module-path=")); auto msvc = bmi_traits(make_tc(CompilerId::MSVC)); - EXPECT_EQ(msvc.stdBmiUsePrefix, " /reference std="); - EXPECT_EQ(msvc.moduleOutputPrefix, " /ifcOutput "); - EXPECT_EQ(msvc.bmiSearchPrefix, " /ifcSearchDir "); -} + expect(eq(msvc.stdBmiUsePrefix, " /reference std=")); + expect(eq(msvc.moduleOutputPrefix, " /ifcOutput ")); + expect(eq(msvc.bmiSearchPrefix, " /ifcSearchDir ")); +}}; -TEST(MingwSpec, LegacyMingwSpellingIsGccTargetingWindowsGnu) { +UTest legacy_mingw_spelling_is_gcc_targeting_windows_gnu{"MingwSpec/LegacyMingwSpellingIsGccTargetingWindowsGnu", [] { // `mingw` is no longer a family: it normalizes to gcc targeting // x86_64-windows-gnu. Which payload serves it is host-split at the // distribution layer (winlibs mingw-gcc on Windows, mingw-cross-gcc // elsewhere) — the user-facing identity is the same either way. auto spec = parse_toolchain_spec("mingw@16.1.0"); - ASSERT_TRUE(spec.has_value()); - EXPECT_FALSE(is_system_toolchain(*spec)); - EXPECT_EQ(spec->family, Family::Gcc); - EXPECT_EQ(spec->target.str(), "x86_64-windows-gnu"); - EXPECT_EQ(spec->display(), "gcc@16.1.0 → x86_64-windows-gnu"); + expect(fatal(spec.has_value())); + expect(!is_system_toolchain(*spec)); + expect(eq(spec->family, Family::Gcc)); + expect(eq(spec->target.str(), "x86_64-windows-gnu")); + expect(eq(spec->display(), "gcc@16.1.0 → x86_64-windows-gnu")); auto pkg = to_xim_package(*spec); - EXPECT_EQ(pkg.ximVersion, "16.1.0"); - ASSERT_FALSE(pkg.frontendCandidates.empty()); + expect(eq(pkg.ximVersion, "16.1.0")); + expect(fatal(!pkg.frontendCandidates.empty())); #if defined(_WIN32) - EXPECT_EQ(pkg.ximName, "mingw-gcc"); - EXPECT_EQ(pkg.frontendCandidates.front(), "g++.exe"); + expect(eq(pkg.ximName, "mingw-gcc")); + expect(eq(pkg.frontendCandidates.front(), "g++.exe")); #else - EXPECT_EQ(pkg.ximName, "mingw-cross-gcc"); - EXPECT_EQ(pkg.frontendCandidates.front(), "x86_64-w64-mingw32-g++"); + expect(eq(pkg.ximName, "mingw-cross-gcc")); + expect(eq(pkg.frontendCandidates.front(), "x86_64-w64-mingw32-g++")); #endif - EXPECT_FALSE(pkg.needsGccPostInstallFixup); -} + expect(!pkg.needsGccPostInstallFixup); +}}; -TEST(MingwSpec, PayloadIdentityAndDefaultMatching) { +UTest payload_identity_and_default_matching{"MingwSpec/PayloadIdentityAndDefaultMatching", [] { // Both host-split payload dirs identify as the SAME (gcc, windows-gnu). for (auto dir : {"mingw-gcc", "mingw-cross-gcc"}) { auto id = identify_xim_payload(dir); - ASSERT_TRUE(id.has_value()) << dir; - EXPECT_EQ(id->family, Family::Gcc) << dir; - EXPECT_EQ(id->target.str(), "x86_64-windows-gnu") << dir; + expect(fatal(id.has_value())) << dir; + expect(eq(id->family, Family::Gcc)) << dir; + expect(eq(id->target.str(), "x86_64-windows-gnu")) << dir; } auto id = *identify_xim_payload("mingw-cross-gcc"); auto def = parse_toolchain_spec("mingw-cross@16.1.0"); // legacy default string - ASSERT_TRUE(def.has_value()); - EXPECT_TRUE(spec_matches_payload(*def, id, "16.1.0")); - EXPECT_FALSE(spec_matches_payload(*def, id, "15.1.0")); + expect(fatal(def.has_value())); + expect(bool(spec_matches_payload(*def, id, "16.1.0"))); + expect(!spec_matches_payload(*def, id, "15.1.0")); auto llvmDef = parse_toolchain_spec("llvm@16.1.0"); - ASSERT_TRUE(llvmDef.has_value()); - EXPECT_FALSE(spec_matches_payload(*llvmDef, id, "16.1.0")); -} + expect(fatal(llvmDef.has_value())); + expect(!spec_matches_payload(*llvmDef, id, "16.1.0")); +}}; -TEST(StdFlagFor, PerDialectSpelling) { +UTest per_dialect_spelling{"StdFlagFor/PerDialectSpelling", [] { const auto& gnu = dialect_for(make_tc(CompilerId::GCC)); const auto& msvc = dialect_for(make_tc(CompilerId::MSVC)); - EXPECT_EQ(std_flag_for(gnu, "c++26", 26), "-std=c++26"); - EXPECT_EQ(std_flag_for(gnu, "gnu++23", 23), "-std=gnu++23"); - EXPECT_EQ(std_flag_for(gnu, "c++20", 20), "-std=c++20"); - EXPECT_EQ(std_flag_for(gnu, "gnu++20", 20), "-std=gnu++20"); + expect(eq(std_flag_for(gnu, "c++26", 26), "-std=c++26")); + expect(eq(std_flag_for(gnu, "gnu++23", 23), "-std=gnu++23")); + expect(eq(std_flag_for(gnu, "c++20", 20), "-std=c++20")); + expect(eq(std_flag_for(gnu, "gnu++20", 20), "-std=gnu++20")); // The `level <= 20` row: unreachable until c++20 entered the allow-list, // because 23 was the lowest level a manifest could produce. - EXPECT_EQ(std_flag_for(msvc, "c++20", 20), "/std:c++20"); - EXPECT_EQ(std_flag_for(msvc, "gnu++20", 20), "/std:c++20"); // no MSVC gnu dialect - EXPECT_EQ(std_flag_for(msvc, "c++23", 23), "/std:c++latest"); - EXPECT_EQ(std_flag_for(msvc, "c++26", 26), "/std:c++latest"); -} + expect(eq(std_flag_for(msvc, "c++20", 20), "/std:c++20")); + expect(eq(std_flag_for(msvc, "gnu++20", 20), "/std:c++20")); // no MSVC gnu dialect + expect(eq(std_flag_for(msvc, "c++23", 23), "/std:c++latest")); + expect(eq(std_flag_for(msvc, "c++26", 26), "/std:c++latest")); +}}; // ─── issue #210: dialect-class flag extraction ─────────────────────────── -TEST(DialectFlags, KnownListAndEscapeHatch) { +UTest known_list_and_escape_hatch{"DialectFlags/KnownListAndEscapeHatch", [] { mcpp::manifest::BuildConfig bc; bc.cxxflags = {"-freflection", "-O3", "-Wall", "-D_GLIBCXX_USE_CXX11_ABI=0"}; bc.dialectCxxflags = {"-fcustom-std-thing", "-freflection"}; // dup dedups auto flags = mcpp::manifest::dialect_flags(bc); - ASSERT_EQ(flags.size(), 3u); - EXPECT_EQ(flags[0], "-fcustom-std-thing"); // explicit first, order kept - EXPECT_EQ(flags[1], "-freflection"); // deduped against cxxflags copy - EXPECT_EQ(flags[2], "-D_GLIBCXX_USE_CXX11_ABI=0"); // auto-promoted -} + expect(fatal(eq(flags.size(), 3u))); + expect(eq(flags[0], "-fcustom-std-thing")); // explicit first, order kept + expect(eq(flags[1], "-freflection")); // deduped against cxxflags copy + expect(eq(flags[2], "-D_GLIBCXX_USE_CXX11_ABI=0")); // auto-promoted +}}; -TEST(DialectFlags, ExtractionDetails) { +UTest extraction_details{"DialectFlags/ExtractionDetails", [] { using mcpp::manifest::is_dialect_flag; - EXPECT_TRUE(is_dialect_flag("-freflection")); - EXPECT_TRUE(is_dialect_flag("-fcontracts")); - EXPECT_TRUE(is_dialect_flag("-fno-char8_t")); - EXPECT_TRUE(is_dialect_flag("-D_GLIBCXX_USE_CXX11_ABI=1")); + expect(is_dialect_flag("-freflection")); + expect(is_dialect_flag("-fcontracts")); + expect(is_dialect_flag("-fno-char8_t")); + expect(is_dialect_flag("-D_GLIBCXX_USE_CXX11_ABI=1")); // Conservative first list: these stay per-unit. - EXPECT_FALSE(is_dialect_flag("-fno-exceptions")); - EXPECT_FALSE(is_dialect_flag("-fno-rtti")); - EXPECT_FALSE(is_dialect_flag("-O2")); - EXPECT_FALSE(is_dialect_flag("-Wall")); - EXPECT_FALSE(is_dialect_flag("-fPIC")); -} + expect(!is_dialect_flag("-fno-exceptions")); + expect(!is_dialect_flag("-fno-rtti")); + expect(!is_dialect_flag("-O2")); + expect(!is_dialect_flag("-Wall")); + expect(!is_dialect_flag("-fPIC")); +}}; -TEST(MingwModel, TargetPredicate) { - EXPECT_TRUE(is_mingw_target(make_tc(CompilerId::GCC, "x86_64-w64-mingw32"))); - EXPECT_FALSE(is_mingw_target(make_tc(CompilerId::GCC, "x86_64-linux-gnu"))); - EXPECT_FALSE(is_mingw_target(make_tc(CompilerId::Clang, "x86_64-pc-windows-msvc"))); -} +UTest target_predicate{"MingwModel/TargetPredicate", [] { + expect(is_mingw_target(make_tc(CompilerId::GCC, "x86_64-w64-mingw32"))); + expect(!(is_mingw_target(make_tc(CompilerId::GCC, "x86_64-linux-gnu")))); + expect(!(is_mingw_target(make_tc(CompilerId::Clang, "x86_64-pc-windows-msvc")))); +}}; // ─── #718: the CRT model reaches every MSVC-ABI row, cl and clang++ alike ── // The property the whole design rests on: one helper, `msvc_abi_crt_word`, // spells the SAME two-state switch (static/dynamic CRT) for whichever driver // `tc` is, and says nothing for a row that is not the MSVC ABI at all. -TEST(MsvcAbiCrtWord, SpelledForEachDriverAndEmptyOffTheAbi) { +UTest spelled_for_each_driver_and_empty_off_the_abi{"MsvcAbiCrtWord/SpelledForEachDriverAndEmptyOffTheAbi", [] { auto cl = make_tc(CompilerId::MSVC, "x86_64-pc-windows-msvc"); - EXPECT_EQ(msvc_abi_crt_word(cl, /*staticCrt=*/true), "/MT"); - EXPECT_EQ(msvc_abi_crt_word(cl, /*staticCrt=*/false), "/MD"); + expect(eq(msvc_abi_crt_word(cl, /*staticCrt=*/true), "/MT")); + expect(eq(msvc_abi_crt_word(cl, /*staticCrt=*/false), "/MD")); auto llvmRow = make_tc(CompilerId::Clang, "x86_64-pc-windows-msvc"); - EXPECT_EQ(msvc_abi_crt_word(llvmRow, /*staticCrt=*/true), - "-fms-runtime-lib=static"); - EXPECT_EQ(msvc_abi_crt_word(llvmRow, /*staticCrt=*/false), - "-fms-runtime-lib=dll"); + expect(eq(msvc_abi_crt_word(llvmRow, /*staticCrt=*/true), "-fms-runtime-lib=static")); + expect(eq(msvc_abi_crt_word(llvmRow, /*staticCrt=*/false), "-fms-runtime-lib=dll")); // Neither the MSVC ABI: MinGW links the MSVC CRT to no row at all, and a // plain Linux row has no CRT axis of this kind either. - EXPECT_TRUE(msvc_abi_crt_word( + expect(msvc_abi_crt_word( make_tc(CompilerId::GCC, "x86_64-w64-mingw32"), true).empty()); - EXPECT_TRUE(msvc_abi_crt_word( + expect(msvc_abi_crt_word( make_tc(CompilerId::Clang, "x86_64-linux-gnu"), false).empty()); - EXPECT_TRUE(msvc_abi_crt_word( + expect(msvc_abi_crt_word( make_tc(CompilerId::GCC, "x86_64-linux-gnu"), true).empty()); -} +}}; // Every MSVC-ABI row x {undeclared, self-contained, toolchain-coupled, // host-coupled, linkage=static} yields exactly one CRT word, spelled for its @@ -191,7 +191,7 @@ TEST(MsvcAbiCrtWord, SpelledForEachDriverAndEmptyOffTheAbi) { // against the dynamic CRT; they differ only in whether the redistributable // is staged, which is a distribution.cppm/flags.cppm concern this helper does // not carry. -TEST(MsvcAbiCrtWord, EveryContractResolvesToExactlyOneWordPerRow) { +UTest every_contract_resolves_to_exactly_one_word_per_row{"MsvcAbiCrtWord/EveryContractResolvesToExactlyOneWordPerRow", [] { struct Row { Toolchain tc; std::string_view label; }; Row rows[] = { {make_tc(CompilerId::MSVC, "x86_64-pc-windows-msvc"), "cl"}, @@ -211,64 +211,63 @@ TEST(MsvcAbiCrtWord, EveryContractResolvesToExactlyOneWordPerRow) { for (auto& row : rows) { for (auto& c : cases) { const bool staticCrt = msvc_wants_static_crt(c.linkage, c.cxxRuntime); - EXPECT_EQ(staticCrt, c.wantsStatic) + expect(eq(staticCrt, c.wantsStatic)) << row.label << " linkage='" << c.linkage << "' cxx_runtime='" << c.cxxRuntime << "'"; auto word = msvc_abi_crt_word(row.tc, staticCrt); - EXPECT_FALSE(word.empty()) << row.label; + expect(!word.empty()) << row.label; // Exactly one of the two spellings for this driver, and it is // the SAME word `flags.cppm`'s compile line and `scan.cpp`'s std // BMI mirror both call this helper for — one derivation, so a TU // and its std BMI cannot disagree by construction. if (row.tc.compiler == CompilerId::MSVC) { - EXPECT_EQ(word, staticCrt ? "/MT" : "/MD") << row.label; + expect(eq(word, staticCrt ? "/MT" : "/MD")) << row.label; } else { - EXPECT_EQ(word, staticCrt ? "-fms-runtime-lib=static" - : "-fms-runtime-lib=dll") << row.label; + expect(eq(word, staticCrt ? "-fms-runtime-lib=static" + : "-fms-runtime-lib=dll")) << row.label; } } } -} +}}; // A shared-PDB word is refused on cl.exe, either dash; other words pass. -TEST(CheckSharedPdbWord, ZiZIAndFSAreRefusedOtherWordsPass) { +UTest zi_zi_and_fs_are_refused_other_words_pass{"CheckSharedPdbWord/ZiZIAndFSAreRefusedOtherWordsPass", [] { for (auto* word : {"/Zi", "-Zi", "/ZI", "-ZI", "/FS", "-FS"}) { auto refusal = check_shared_pdb_word(word, "[build] cxxflags"); - ASSERT_TRUE(refusal.has_value()) << word; - EXPECT_NE(refusal->find(word), std::string::npos) << *refusal; - EXPECT_NE(refusal->find("[build] cxxflags"), std::string::npos) << *refusal; - EXPECT_NE(refusal->find("/Z7"), std::string::npos) << *refusal; + expect(fatal(refusal.has_value())) << word; + if (auto&& failed = expect(ne(refusal->find(word), std::string::npos)); !failed) failed << *refusal; + if (auto&& failed = expect(ne(refusal->find("[build] cxxflags"), std::string::npos)); !failed) failed << *refusal; + if (auto&& failed = expect(ne(refusal->find("/Z7"), std::string::npos)); !failed) failed << *refusal; } for (auto* word : {"/Z7", "-Z7", "/Zc:__cplusplus", "/Zp8", "/FSx", "/MT", "-g"}) - EXPECT_FALSE(check_shared_pdb_word(word, "[build] cxxflags").has_value()) << word; -} + expect(!(check_shared_pdb_word(word, "[build] cxxflags").has_value())) << word; +}}; // D3: a free-form CRT word is always a second statement once the model is // resolved for every MSVC-ABI row. -TEST(CheckCrtWord, AgreeingWordIsRedundantContradictingWordIsRefused) { +UTest agreeing_word_is_redundant_contradicting_word_is_refused{"CheckCrtWord/AgreeingWordIsRedundantContradictingWordIsRefused", [] { // Recognised spellings, both dash conventions and both drivers. for (auto* word : {"/MT", "-MT", "-fms-runtime-lib=static"}) { auto agree = check_crt_word(word, /*staticCrt=*/true, "[build] cxxflags"); - ASSERT_TRUE(agree.has_value()) << word; - EXPECT_FALSE(agree->contradicts) << word; + expect(fatal(agree.has_value())) << word; + expect(!agree->contradicts) << word; // The key it names states the same model. - EXPECT_NE(agree->message.find("self-contained"), std::string::npos) << agree->message; + expect(ne(agree->message.find("self-contained"), std::string::npos)) << agree->message; auto disagree = check_crt_word(word, /*staticCrt=*/false, "[build] cxxflags"); - ASSERT_TRUE(disagree.has_value()) << word; - EXPECT_TRUE(disagree->contradicts) << word; - EXPECT_NE(disagree->message.find(word), std::string::npos) << disagree->message; - EXPECT_NE(disagree->message.find("[build] cxxflags"), std::string::npos) - << disagree->message; + expect(fatal(disagree.has_value())) << word; + expect(bool(disagree->contradicts)) << word; + if (auto&& failed = expect(ne(disagree->message.find(word), std::string::npos)); !failed) failed << disagree->message; + if (auto&& failed = expect(ne(disagree->message.find("[build] cxxflags"), std::string::npos)); !failed) failed << disagree->message; } for (auto* word : {"/MD", "-MD", "-fms-runtime-lib=dll"}) { auto agree = check_crt_word(word, /*staticCrt=*/false, "dialect_cxxflags"); - ASSERT_TRUE(agree.has_value()) << word; - EXPECT_FALSE(agree->contradicts) << word; - EXPECT_NE(agree->message.find("toolchain-coupled"), std::string::npos) << agree->message; - EXPECT_EQ(agree->message.find("self-contained"), std::string::npos) << agree->message; + expect(fatal(agree.has_value())) << word; + expect(!agree->contradicts) << word; + if (auto&& failed = expect(ne(agree->message.find("toolchain-coupled"), std::string::npos)); !failed) failed << agree->message; + if (auto&& failed = expect(eq(agree->message.find("self-contained"), std::string::npos)); !failed) failed << agree->message; auto disagree = check_crt_word(word, /*staticCrt=*/true, "dialect_cxxflags"); - ASSERT_TRUE(disagree.has_value()) << word; - EXPECT_TRUE(disagree->contradicts) << word; + expect(fatal(disagree.has_value())) << word; + expect(bool(disagree->contradicts)) << word; } // A debug CRT word is refused under either model: the model has no debug // axis, and the std module and the link use the release CRT. @@ -276,12 +275,12 @@ TEST(CheckCrtWord, AgreeingWordIsRedundantContradictingWordIsRefused) { "-fms-runtime-lib=static_dbg", "-fms-runtime-lib=dll_dbg"}) { for (bool staticCrt : {true, false}) { auto v = check_crt_word(word, staticCrt, "[build] cxxflags"); - ASSERT_TRUE(v.has_value()) << word; - EXPECT_TRUE(v->contradicts) << word; - EXPECT_NE(v->message.find("debug CRT"), std::string::npos) << v->message; + expect(fatal(v.has_value())) << word; + expect(bool(v->contradicts)) << word; + if (auto&& failed = expect(ne(v->message.find("debug CRT"), std::string::npos)); !failed) failed << v->message; } } // A word this axis does not recognise says nothing about it. - EXPECT_FALSE(check_crt_word("-O2", true, "[build] cxxflags").has_value()); - EXPECT_FALSE(check_crt_word("/EHsc", false, "[build] cxxflags").has_value()); -} + expect(!(check_crt_word("-O2", true, "[build] cxxflags").has_value())); + expect(!(check_crt_word("/EHsc", false, "[build] cxxflags").has_value())); +}}; diff --git a/tests/unit/test_toolchain_lifecycle.cpp b/tests/unit/test_toolchain_lifecycle.cpp index 297feb579..d5d58c709 100644 --- a/tests/unit/test_toolchain_lifecycle.cpp +++ b/tests/unit/test_toolchain_lifecycle.cpp @@ -1,6 +1,6 @@ -#include import std; +import mcpplibs.mtest; import mcpp.config; import mcpplibs.os.platform; import mcpp.runtime.binding; @@ -8,6 +8,8 @@ import mcpp.toolchain.lifecycle; import mcpp.toolchain.post_install; import mcpp.toolchain.registry; +using namespace mcpplibs::mtest; + using namespace mcpp::toolchain; // WHAT IS AND IS NOT TESTED HERE, because the difference is not a gap. @@ -31,7 +33,7 @@ using namespace mcpp::toolchain; // takes the ordinary route, and the sweep is precise about what it eats. // Nothing here asserts something this platform cannot express. -TEST(ToolchainRemove, AnOrdinaryPayloadIsJustDeleted) { +UTest an_ordinary_payload_is_just_deleted{"ToolchainRemove/AnOrdinaryPayloadIsJustDeleted", [] { // The common path must not grow a `.trash-` directory: moving files aside // is the fallback, and a fallback that fires always is not a fallback. auto dir = std::filesystem::temp_directory_path() @@ -41,18 +43,18 @@ TEST(ToolchainRemove, AnOrdinaryPayloadIsJustDeleted) { std::ofstream{dir / "14.44.35207" / "bin" / "cl.exe"} << "not a compiler"; std::error_code ec; - EXPECT_TRUE(remove_payload_tree(dir / "14.44.35207", ec)); - EXPECT_FALSE(std::filesystem::exists(dir / "14.44.35207")); + expect(bool(remove_payload_tree(dir / "14.44.35207", ec))); + expect(!std::filesystem::exists(dir / "14.44.35207")); bool anyTrash = false; for (auto& e : std::filesystem::directory_iterator(dir, ec)) anyTrash |= e.path().filename().string().starts_with(".trash-"); - EXPECT_FALSE(anyTrash) << "a deletable payload was parked instead of deleted"; + expect(!anyTrash) << "a deletable payload was parked instead of deleted"; std::filesystem::remove_all(dir, ec); -} +}}; -TEST(ToolchainRemove, TheSweepDoesNotTouchAnotherVersionThatHasNoFilesYet) { +UTest the_sweep_does_not_touch_another_version_that_has_no_files_yet{"ToolchainRemove/TheSweepDoesNotTouchAnotherVersionThatHasNoFilesYet", [] { // An install populates a version directory over time, so a DIFFERENT // version being extracted right now is briefly indistinguishable from a // leftover skeleton. Two mcpp processes against one MCPP_HOME is ordinary @@ -70,16 +72,16 @@ TEST(ToolchainRemove, TheSweepDoesNotTouchAnotherVersionThatHasNoFilesYet) { sweep_parked_payloads(pkgRoot, mine); - EXPECT_FALSE(std::filesystem::exists(mine)) << "the named skeleton survived"; - EXPECT_TRUE(std::filesystem::exists(theirs)) + expect(!std::filesystem::exists(mine)) << "the named skeleton survived"; + expect(std::filesystem::exists(theirs)) << "swept a version this command was not about — that is someone " "else's install being extracted"; std::error_code ec; std::filesystem::remove_all(pkgRoot, ec); -} +}}; -TEST(ToolchainRemove, TheSweepDeletesParkedPayloadsAndNothingElse) { +UTest the_sweep_deletes_parked_payloads_and_nothing_else{"ToolchainRemove/TheSweepDeletesParkedPayloadsAndNothingElse", [] { // The sweep runs before every install and remove, so it must be precise: // `.trash-*` goes, an installed version directory beside it stays. auto pkgRoot = std::filesystem::temp_directory_path() @@ -92,16 +94,16 @@ TEST(ToolchainRemove, TheSweepDeletesParkedPayloadsAndNothingElse) { sweep_parked_payloads(pkgRoot, {}); - EXPECT_FALSE(std::filesystem::exists(pkgRoot / ".trash-14.44.35207-1")) + expect(!std::filesystem::exists(pkgRoot / ".trash-14.44.35207-1")) << "parked payload was not swept"; - EXPECT_TRUE(std::filesystem::exists(pkgRoot / "14.44.35207" / "bin" / "cl.exe")) + expect(std::filesystem::exists(pkgRoot / "14.44.35207" / "bin" / "cl.exe")) << "the sweep deleted an installed toolset"; std::error_code ec; std::filesystem::remove_all(pkgRoot, ec); -} +}}; -TEST(ToolchainRemove, ADirectorySkeletonWithNoFilesCountsAsRemoved) { +UTest a_directory_skeleton_with_no_files_counts_as_removed{"ToolchainRemove/ADirectorySkeletonWithNoFilesCountsAsRemoved", [] { // Windows can refuse to delete an empty DIRECTORY when a process has it // as its current directory — mspdbsrv.exe is launched inside the payload, // so this is the ordinary tail of a /Zi build with the toolset being @@ -116,13 +118,13 @@ TEST(ToolchainRemove, ADirectorySkeletonWithNoFilesCountsAsRemoved) { std::filesystem::create_directories(dir / "14.44.35207" / "bin" / "Hostx64" / "x64"); std::error_code ec; - EXPECT_TRUE(remove_payload_tree(dir / "14.44.35207", ec)) + expect(bool(remove_payload_tree(dir / "14.44.35207", ec))) << "a payload with no files left was reported as still installed"; std::filesystem::remove_all(dir, ec); -} +}}; -TEST(ToolchainRemove, TheSweepAlsoClearsAFileLessSkeleton) { +UTest the_sweep_also_clears_a_file_less_skeleton{"ToolchainRemove/TheSweepAlsoClearsAFileLessSkeleton", [] { // The deferral half: whatever the removal could not delete goes on the // next lifecycle command, once the process holding it has exited. auto pkgRoot = std::filesystem::temp_directory_path() @@ -134,14 +136,14 @@ TEST(ToolchainRemove, TheSweepAlsoClearsAFileLessSkeleton) { sweep_parked_payloads(pkgRoot, pkgRoot / "14.44.35207"); - EXPECT_FALSE(std::filesystem::exists(pkgRoot / "14.44.35207")) + expect(!std::filesystem::exists(pkgRoot / "14.44.35207")) << "the empty skeleton survived the sweep"; - EXPECT_TRUE(std::filesystem::exists(pkgRoot / "14.52.36629" / "bin" / "cl.exe")) + expect(std::filesystem::exists(pkgRoot / "14.52.36629" / "bin" / "cl.exe")) << "the sweep ate an installed toolset"; std::error_code ec; std::filesystem::remove_all(pkgRoot, ec); -} +}}; // ─── The declared C runtime payload (mcpp#660) ───────────────────────────── // @@ -162,53 +164,53 @@ mcpp::config::GlobalConfig unreachable_cfg() { } // namespace -TEST(DeclaredRuntime, ABindingThatLocatesItsPayloadIsLeftAlone) { +UTest a_binding_that_locates_its_payload_is_left_alone{"DeclaredRuntime/ABindingThatLocatesItsPayloadIsLeftAlone", [] { auto cfg = unreachable_cfg(); mcpp::runtime::RuntimeBinding binding; binding.runtimeId = "glibc@2.44.3"; binding.libraryDirs = {"/store/xim-x-glibc/2.44.3/lib64"}; const auto before = binding.libraryDirs; - EXPECT_EQ(ensure_declared_runtime(cfg, binding).value_or(true), false); - EXPECT_EQ(binding.libraryDirs, before); - EXPECT_EQ(binding.runtimeId, "glibc@2.44.3"); -} + expect(eq(ensure_declared_runtime(cfg, binding).value_or(true), false)); + expect(eq(binding.libraryDirs, before)); + expect(eq(binding.runtimeId, "glibc@2.44.3")); +}}; // A provider that binds no payload (`ucrt` is an OS component) and an absent // identity are not requests to install anything. -TEST(DeclaredRuntime, OnlyAGlibcPayloadIsInstalled) { +UTest only_a_glibc_payload_is_installed{"DeclaredRuntime/OnlyAGlibcPayloadIsInstalled", [] { auto cfg = unreachable_cfg(); for (auto id : {"", "ucrt@10.0.26100.0", "macos_sdk@15.0", "glibc"}) { mcpp::runtime::RuntimeBinding binding; binding.runtimeId = id; - EXPECT_EQ(ensure_declared_runtime(cfg, binding).value_or(true), false) << id; - EXPECT_TRUE(binding.libraryDirs.empty()) << id; + expect(eq(ensure_declared_runtime(cfg, binding).value_or(true), false)) << id; + expect(binding.libraryDirs.empty()) << id; } -} +}}; // Off Linux there is no glibc payload to provide, whatever the identity says. -TEST(DeclaredRuntime, NothingIsInstalledOffLinux) { +UTest nothing_is_installed_off_linux{"DeclaredRuntime/NothingIsInstalledOffLinux", [] { if constexpr (mcpplibs::os::is_linux) - GTEST_SKIP() << "the Linux path is exercised by e2e 700"; + skip("the Linux path is exercised by e2e 700"); auto cfg = unreachable_cfg(); mcpp::runtime::RuntimeBinding binding; binding.runtimeId = "glibc@2.44.3"; - EXPECT_EQ(ensure_declared_runtime(cfg, binding).value_or(true), false); -} + expect(eq(ensure_declared_runtime(cfg, binding).value_or(true), false)); +}}; // Which toolchains run on a C runtime payload: a GNU gcc and an llvm on // Linux. Any other toolchain must not cause a runtime download. -TEST(DeclaredRuntime, OnlyToolchainsOnARuntimePayloadNeedIt) { +UTest only_toolchains_on_a_runtime_payload_need_it{"DeclaredRuntime/OnlyToolchainsOnARuntimePayloadNeedIt", [] { XimToolchainPackage gcc; gcc.ximName = "gcc"; gcc.needsGccPostInstallFixup = true; XimToolchainPackage llvm; llvm.ximName = "llvm"; XimToolchainPackage musl; musl.ximName = "musl-gcc"; XimToolchainPackage msvc; msvc.ximName = "msvc"; if constexpr (mcpplibs::os::is_windows) { for (auto const* p : {&gcc, &llvm, &musl, &msvc}) - EXPECT_TRUE(runtime_payload_kind(*p).empty()) << p->ximName; + expect(runtime_payload_kind(*p).empty()) << p->ximName; } else { - EXPECT_EQ(runtime_payload_kind(gcc), "gcc"); - EXPECT_EQ(runtime_payload_kind(llvm), "llvm"); - EXPECT_TRUE(runtime_payload_kind(musl).empty()); - EXPECT_TRUE(runtime_payload_kind(msvc).empty()); + expect(eq(runtime_payload_kind(gcc), "gcc")); + expect(eq(runtime_payload_kind(llvm), "llvm")); + expect(runtime_payload_kind(musl).empty()); + expect(runtime_payload_kind(msvc).empty()); } -} +}}; diff --git a/tests/unit/test_toolchain_msvc.cpp b/tests/unit/test_toolchain_msvc.cpp index 0f2953d94..885b2e48f 100644 --- a/tests/unit/test_toolchain_msvc.cpp +++ b/tests/unit/test_toolchain_msvc.cpp @@ -1,59 +1,61 @@ -#include import std; +import mcpplibs.mtest; import mcpp.invocation; import mcpp.toolchain.model; import mcpp.toolchain.msvc; import mcpp.toolchain.registry; import mcpp.toolchain.dialect; +using namespace mcpplibs::mtest; + using namespace mcpp::toolchain; // ─── parse_cl_banner ───────────────────────────────────────────────────── -TEST(MsvcBanner, ParsesEnglishBanner) { +UTest parses_english_banner{"MsvcBanner/ParsesEnglishBanner", [] { auto r = msvc::parse_cl_banner( "Microsoft (R) C/C++ Optimizing Compiler Version 19.44.35211 for x64\n" "Copyright (C) Microsoft Corporation. All rights reserved.\n" "\n" "usage: cl [ option... ] filename... [ /link linkoption... ]\n"); - ASSERT_TRUE(r.has_value()); - EXPECT_EQ(r->first, "19.44.35211"); - EXPECT_EQ(r->second, "x64"); -} + expect(fatal(r.has_value())); + expect(eq(r->first, "19.44.35211")); + expect(eq(r->second, "x64")); +}}; -TEST(MsvcBanner, ParsesLocalizedBannerByTokens) { +UTest parses_localized_banner_by_tokens{"MsvcBanner/ParsesLocalizedBannerByTokens", [] { // Chinese VS reorders the sentence; only the tokens are stable. auto r = msvc::parse_cl_banner( "用于 x64 的 Microsoft (R) C/C++ 优化编译器 19.44.35211 版\n" "版权所有(C) Microsoft Corporation。保留所有权利。\n"); - ASSERT_TRUE(r.has_value()); - EXPECT_EQ(r->first, "19.44.35211"); - EXPECT_EQ(r->second, "x64"); -} + expect(fatal(r.has_value())); + expect(eq(r->first, "19.44.35211")); + expect(eq(r->second, "x64")); +}}; -TEST(MsvcBanner, ParsesArm64AndFourComponentVersions) { +UTest parses_arm64_and_four_component_versions{"MsvcBanner/ParsesArm64AndFourComponentVersions", [] { auto r = msvc::parse_cl_banner( "Microsoft (R) C/C++ Optimizing Compiler Version 19.29.30133.0 for ARM64\n"); - ASSERT_TRUE(r.has_value()); - EXPECT_EQ(r->first, "19.29.30133.0"); - EXPECT_EQ(r->second, "arm64"); -} - -TEST(MsvcBanner, RejectsGarbage) { - EXPECT_FALSE(msvc::parse_cl_banner("").has_value()); - EXPECT_FALSE(msvc::parse_cl_banner("bash: cl: command not found").has_value()); + expect(fatal(r.has_value())); + expect(eq(r->first, "19.29.30133.0")); + expect(eq(r->second, "arm64")); +}}; + +UTest msvc_rejects_garbage{"MsvcBanner/RejectsGarbage", [] { + expect(!(msvc::parse_cl_banner("").has_value())); + expect(!(msvc::parse_cl_banner("bash: cl: command not found").has_value())); // A bare two-component number is not a cl version. - EXPECT_FALSE(msvc::parse_cl_banner("something 1.2 else").has_value()); -} + expect(!(msvc::parse_cl_banner("something 1.2 else").has_value())); +}}; -TEST(MsvcBanner, TripleForArch) { - EXPECT_EQ(msvc::triple_for_arch("x64"), "x86_64-pc-windows-msvc"); - EXPECT_EQ(msvc::triple_for_arch("x86"), "i686-pc-windows-msvc"); - EXPECT_EQ(msvc::triple_for_arch("arm64"), "aarch64-pc-windows-msvc"); +UTest msvc_banner_triple_for_arch{"MsvcBanner/TripleForArch", [] { + expect(eq(msvc::triple_for_arch("x64"), "x86_64-pc-windows-msvc")); + expect(eq(msvc::triple_for_arch("x86"), "i686-pc-windows-msvc")); + expect(eq(msvc::triple_for_arch("arm64"), "aarch64-pc-windows-msvc")); // Unknown/empty arch falls back to the x64 triple. - EXPECT_EQ(msvc::triple_for_arch(""), "x86_64-pc-windows-msvc"); -} + expect(eq(msvc::triple_for_arch(""), "x86_64-pc-windows-msvc")); +}}; // ─── install guidance ──────────────────────────────────────────────────── @@ -75,26 +77,26 @@ std::string fixed_file_info(std::uint32_t ms, std::uint32_t ls, } } // namespace -TEST(MsvcCompilerVersion, ReadsTheFixedFileInfoAsClangDoes) { +UTest reads_the_fixed_file_info_as_clang_does{"MsvcCompilerVersion/ReadsTheFixedFileInfoAsClangDoes", [] { // 19.51.36257.0: MS = 19 << 16 | 51, LS = 36257 << 16 | 0. std::string image(300, 'x'); image += fixed_file_info((19u << 16) | 51u, (36257u << 16) | 0u); image += std::string(40, 'y'); auto v = mcpp::toolchain::msvc::pe_file_version(image); - ASSERT_TRUE(v.has_value()); - EXPECT_EQ(*v, "19.51.36257"); -} + expect(fatal(v.has_value())); + expect(eq(*v, "19.51.36257")); +}}; -TEST(MsvcCompilerVersion, SkipsASignatureThatIsNotTheStructure) { +UTest skips_a_signature_that_is_not_the_structure{"MsvcCompilerVersion/SkipsASignatureThatIsNotTheStructure", [] { // The four signature bytes can occur in code; only the structure, whose // next word is 0x00010000, is read. std::string image = fixed_file_info((1u << 16) | 2u, (3u << 16), 0xDEADBEEFu); image += fixed_file_info((19u << 16) | 44u, (35211u << 16) | 7u); - EXPECT_EQ(mcpp::toolchain::msvc::pe_file_version(image).value_or(""), "19.44.35211"); - EXPECT_FALSE(mcpp::toolchain::msvc::pe_file_version("no version here").has_value()); -} + expect(eq(mcpp::toolchain::msvc::pe_file_version(image).value_or(""), "19.44.35211")); + expect(!(mcpp::toolchain::msvc::pe_file_version("no version here").has_value())); +}}; -TEST(MsvcCompilerVersion, IsReadFromTheCompilerClangReadsForTheTarget) { +UTest is_read_from_the_compiler_clang_reads_for_the_target{"MsvcCompilerVersion/IsReadFromTheCompilerClangReadsForTheTarget", [] { // clang reads bin/Host{x64|x86}//cl.exe; each target's compiler // here carries its own build, under both host directories, so the answer // depends on the target and not on which host runs the test. @@ -111,94 +113,94 @@ TEST(MsvcCompilerVersion, IsReadFromTheCompilerClangReadsForTheTarget) { os << std::string(64, 'M') << fixed_file_info((19u << 16) | 51u, (build << 16)); } namespace msvc = mcpp::toolchain::msvc; - EXPECT_EQ(msvc::compiler_version_in_tools_dir(tools, "x86_64"), "19.51.36260"); - EXPECT_EQ(msvc::compiler_version_in_tools_dir(tools, "aarch64"), "19.51.36261"); - EXPECT_EQ(msvc::compiler_version_in_tools_dir(tools, "i686"), "19.51.36262"); + expect(eq(msvc::compiler_version_in_tools_dir(tools, "x86_64"), "19.51.36260")); + expect(eq(msvc::compiler_version_in_tools_dir(tools, "aarch64"), "19.51.36261")); + expect(eq(msvc::compiler_version_in_tools_dir(tools, "i686"), "19.51.36262")); // Every 32-bit x86 spelling is the x86 directory (2026.10.5.3; i386-i586 // used to read x64's compiler). - EXPECT_EQ(msvc::compiler_version_in_tools_dir(tools, "i386"), "19.51.36262"); - EXPECT_EQ(msvc::compiler_version_in_tools_dir(tools, "i586"), "19.51.36262"); + expect(eq(msvc::compiler_version_in_tools_dir(tools, "i386"), "19.51.36262")); + expect(eq(msvc::compiler_version_in_tools_dir(tools, "i586"), "19.51.36262")); // No compiler where clang looks: no version, and clang falls back itself. - EXPECT_EQ(msvc::compiler_version_in_tools_dir(tools / "absent", "x86_64"), ""); + expect(eq(msvc::compiler_version_in_tools_dir(tools / "absent", "x86_64"), "")); std::error_code ec; fs::remove_all(tools.parent_path().parent_path().parent_path().parent_path(), ec); -} +}}; -TEST(MsvcGuidance, OffersBothOrigins) { +UTest offers_both_origins{"MsvcGuidance/OffersBothOrigins", [] { auto g = msvc::install_guidance(); - ASSERT_FALSE(g.empty()); + expect(fatal(!g.empty())); // The managed origin has to be reachable from the message a user sees // when nothing is installed — otherwise "mcpp can install a toolset" is // true and undiscoverable at the same time. - EXPECT_NE(g.find("mcpp toolchain install msvc"), std::string::npos) << g; - EXPECT_NE(g.find("[toolchain]"), std::string::npos) << g; + expect(ne(g.find("mcpp toolchain install msvc"), std::string::npos)) << g; + expect(ne(g.find("[toolchain]"), std::string::npos)) << g; // …and the system origin stays offered, with its own route. - EXPECT_NE(g.find("winget"), std::string::npos) << g; - EXPECT_NE(g.find("msvc@system"), std::string::npos) << g; - EXPECT_NE(g.find("mcpp toolchain default msvc"), std::string::npos) << g; -} + expect(ne(g.find("winget"), std::string::npos)) << g; + expect(ne(g.find("msvc@system"), std::string::npos)) << g; + expect(ne(g.find("mcpp toolchain default msvc"), std::string::npos)) << g; +}}; // ─── spec layer: the VERSION axis decides the origin ───────────────────── -TEST(MsvcSpec, SystemOriginIsTheUnversionedSpec) { +UTest system_origin_is_the_unversioned_spec{"MsvcSpec/SystemOriginIsTheUnversionedSpec", [] { for (auto s : {"msvc", "msvc@system"}) { auto spec = parse_toolchain_spec(s); - ASSERT_TRUE(spec.has_value()) << s; - EXPECT_TRUE(is_system_toolchain(*spec)) << s; + expect(fatal(spec.has_value())) << s; + expect(is_system_toolchain(*spec)) << s; } auto gcc = parse_toolchain_spec("gcc@16.1.0"); - ASSERT_TRUE(gcc.has_value()); - EXPECT_FALSE(is_system_toolchain(*gcc)); -} + expect(fatal(gcc.has_value())); + expect(!is_system_toolchain(*gcc)); +}}; -TEST(MsvcSpec, ToolsetVersionIsAManagedPayloadNotASystemSpec) { +UTest toolset_version_is_a_managed_payload_not_a_system_spec{"MsvcSpec/ToolsetVersionIsAManagedPayloadNotASystemSpec", [] { // The defect this closes: EVERY msvc spec used to be a system spec, so a // manifest could name a toolset and silently get whatever the machine // had. A version means a payload, exactly like gcc@16.1.0. for (auto s : {"msvc@14.44.35207", "msvc@14.52.36629"}) { auto spec = parse_toolchain_spec(s); - ASSERT_TRUE(spec.has_value()) << s; - EXPECT_FALSE(is_system_toolchain(*spec)) << s; + expect(fatal(spec.has_value())) << s; + expect(!is_system_toolchain(*spec)) << s; auto pkg = to_xim_package(*spec); - EXPECT_EQ(pkg.ximName, "msvc") << s; - EXPECT_EQ(pkg.ximVersion, std::string(s).substr(5)) << s; + expect(eq(pkg.ximName, "msvc")) << s; + expect(eq(pkg.ximVersion, std::string(s).substr(5))) << s; } -} +}}; -TEST(MsvcSpec, StableDefaultMatchesAnyDetectedVersion) { +UTest stable_default_matches_any_detected_version{"MsvcSpec/StableDefaultMatchesAnyDetectedVersion", [] { // The persisted default is always the stable "msvc@system" — it matches // whatever concrete version detection reports (family-level match). auto def = parse_toolchain_spec("msvc@system"); - ASSERT_TRUE(def.has_value()); + expect(fatal(def.has_value())); PayloadIdentity msvcId{ Family::Msvc, {} }; - EXPECT_TRUE(spec_matches_payload(*def, msvcId, "19.44.35211")); - EXPECT_TRUE(spec_matches_payload(*def, msvcId, "19.29.30133")); + expect(bool(spec_matches_payload(*def, msvcId, "19.44.35211"))); + expect(bool(spec_matches_payload(*def, msvcId, "19.29.30133"))); PayloadIdentity gccId{ Family::Gcc, {} }; - EXPECT_FALSE(spec_matches_payload(*def, gccId, "16.1.0")); + expect(!spec_matches_payload(*def, gccId, "16.1.0")); auto gccDef = parse_toolchain_spec("gcc@16.1.0"); - ASSERT_TRUE(gccDef.has_value()); - EXPECT_FALSE(spec_matches_payload(*gccDef, msvcId, "19.44.35211")); -} + expect(fatal(gccDef.has_value())); + expect(!spec_matches_payload(*gccDef, msvcId, "19.44.35211")); +}}; -TEST(MsvcSpec, PinnedToolsetMatchesOnlyItsOwnPayload) { +UTest pinned_toolset_matches_only_its_own_payload{"MsvcSpec/PinnedToolsetMatchesOnlyItsOwnPayload", [] { // The other half of the family-level match above: once a version is // named, it has to be compared. A pin that matched any payload would be // the same silent divergence wearing a version number. auto pinned = parse_toolchain_spec("msvc@14.52.36629"); - ASSERT_TRUE(pinned.has_value()); + expect(fatal(pinned.has_value())); PayloadIdentity msvcId{ Family::Msvc, {} }; - EXPECT_TRUE(spec_matches_payload(*pinned, msvcId, "14.52.36629")); - EXPECT_FALSE(spec_matches_payload(*pinned, msvcId, "14.44.35207")); -} + expect(bool(spec_matches_payload(*pinned, msvcId, "14.52.36629"))); + expect(!spec_matches_payload(*pinned, msvcId, "14.44.35207")); +}}; -TEST(MsvcSpec, PayloadDirectoryIsIdentifiedAsMsvc) { +UTest payload_directory_is_identified_as_msvc{"MsvcSpec/PayloadDirectoryIsIdentifiedAsMsvc", [] { // Installed payloads are listed by walking `xim-x-` directories. // Without this row an install would succeed and then be invisible. auto id = identify_xim_payload("msvc"); - ASSERT_TRUE(id.has_value()); - EXPECT_EQ(id->family, Family::Msvc); - EXPECT_TRUE(id->target.empty()); // host-target, like gcc and llvm -} + expect(fatal(id.has_value())); + expect(eq(id->family, Family::Msvc)); + expect(id->target.empty()); // host-target, like gcc and llvm +}}; // ─── managed origin: resolution from a payload ─────────────────────────── // @@ -274,7 +276,7 @@ struct NoSdkEnv { } // namespace -TEST(MsvcManaged, ResolvesTheDeclaredToolsetAndNotItsNeighbour) { +UTest resolves_the_declared_toolset_and_not_its_neighbour{"MsvcManaged/ResolvesTheDeclaredToolsetAndNotItsNeighbour", [] { FakeToolset t{"pick"}; t.add_toolset("14.44.35207"); t.add_toolset("14.52.36629"); @@ -283,18 +285,18 @@ TEST(MsvcManaged, ResolvesTheDeclaredToolsetAndNotItsNeighbour) { // answer 14.52 — the exact substitution the managed origin exists to // prevent. auto older = msvc::installation_at(t.root, "14.44.35207"); - ASSERT_TRUE(older.has_value()); - EXPECT_EQ(older->toolsVersion, "14.44.35207"); - EXPECT_TRUE(older->hasStdModules); - EXPECT_EQ(older->clPath.filename(), "cl.exe"); - EXPECT_NE(older->clPath.string().find("14.44.35207"), std::string::npos); + expect(fatal(older.has_value())); + expect(eq(older->toolsVersion, "14.44.35207")); + expect(bool(older->hasStdModules)); + expect(eq(older->clPath.filename(), "cl.exe")); + expect(ne(older->clPath.string().find("14.44.35207"), std::string::npos)); auto newer = msvc::installation_at(t.root, "14.52.36629"); - ASSERT_TRUE(newer.has_value()); - EXPECT_EQ(newer->toolsVersion, "14.52.36629"); -} + expect(fatal(newer.has_value())); + expect(eq(newer->toolsVersion, "14.52.36629")); +}}; -TEST(MsvcManaged, TheVersionDirIsTheRootNotItsLoneSubdirectory) { +UTest the_version_dir_is_the_root_not_its_lone_subdirectory{"MsvcManaged/TheVersionDirIsTheRootNotItsLoneSubdirectory", [] { // A real installed payload has exactly ONE entry: `VC/`. The fetcher's // `XpkgPayload::root` treats the version directory as the root only when // it directly contains bin/ include/ lib/, and otherwise descends into a @@ -309,35 +311,35 @@ TEST(MsvcManaged, TheVersionDirIsTheRootNotItsLoneSubdirectory) { t.add_toolset("14.44.35207"); auto ok = msvc::installation_at(t.root, "14.44.35207"); - ASSERT_TRUE(ok.has_value()) << "the version directory must be the root"; + expect(fatal(ok.has_value())) << "the version directory must be the root"; - EXPECT_FALSE(msvc::installation_at(t.root / "VC", "14.44.35207").has_value()) + expect(!(msvc::installation_at(t.root / "VC", "14.44.35207").has_value())) << "a caller handing in the VC subdir is passing the wrong thing, and " "must be told so rather than quietly rescued"; -} +}}; -TEST(MsvcManaged, AbsentToolsetIsNulloptNotASubstitute) { +UTest absent_toolset_is_nullopt_not_a_substitute{"MsvcManaged/AbsentToolsetIsNulloptNotASubstitute", [] { FakeToolset t{"absent"}; t.add_toolset("14.44.35207"); // Nothing is "close enough": a pin that silently fell back would report // success while building with a compiler nobody asked for. - EXPECT_FALSE(msvc::installation_at(t.root, "14.52.36629").has_value()); - EXPECT_FALSE(msvc::installation_at(t.root, "14.44").has_value()); - EXPECT_FALSE(msvc::installation_at(t.root, "").has_value()); -} + expect(!(msvc::installation_at(t.root, "14.52.36629").has_value())); + expect(!(msvc::installation_at(t.root, "14.44").has_value())); + expect(!(msvc::installation_at(t.root, "").has_value())); +}}; -TEST(MsvcManaged, VersionFallsBackToTheDeclaredOneWhenTheBannerCannotBeRead) { +UTest version_falls_back_to_the_declared_one_when_the_banner_cannot_be_read{"MsvcManaged/VersionFallsBackToTheDeclaredOneWhenTheBannerCannotBeRead", [] { // A fixture cl.exe produces no banner (and on Windows a real one would). // display_version() must still name the toolset rather than an empty // string — the declared version is a fact even when the probe fails. FakeToolset t{"banner"}; t.add_toolset("14.52.36629"); auto inst = msvc::installation_at(t.root, "14.52.36629"); - ASSERT_TRUE(inst.has_value()); - EXPECT_EQ(inst->display_version(), "14.52.36629"); -} + expect(fatal(inst.has_value())); + expect(eq(inst->display_version(), "14.52.36629")); +}}; -TEST(MsvcManaged, PayloadFrontendFindsClWhereMsvcActuallyKeepsIt) { +UTest payload_frontend_finds_cl_where_msvc_actually_keeps_it{"MsvcManaged/PayloadFrontendFindsClWhereMsvcActuallyKeepsIt", [] { // The defect this pins: `toolchain list` asked `toolchain_frontend(root / // "bin", …)`, got nothing, and skipped the row -- so an msvc toolset // installed correctly and then did not appear anywhere. Three places need @@ -346,28 +348,28 @@ TEST(MsvcManaged, PayloadFrontendFindsClWhereMsvcActuallyKeepsIt) { t.add_toolset("14.44.35207"); auto spec = parse_toolchain_spec("msvc@14.44.35207"); - ASSERT_TRUE(spec.has_value()); + expect(fatal(spec.has_value())); auto pkg = to_xim_package(*spec); // The family comes from the PACKAGE now, not from a second argument every // caller had to source from a differently-named local -- five of them // composed `payload->binDir` themselves instead and so could not see // `frontendSubdir` at all. - EXPECT_EQ(pkg.family, Family::Msvc); + expect(eq(pkg.family, Family::Msvc)); auto found = payload_frontend(t.root, pkg); - ASSERT_TRUE(found.has_value()) << found.error(); - ASSERT_FALSE(found->empty()) + if (auto&& failed = expect(fatal(found.has_value())); !failed) failed << found.error(); + expect(fatal(!found->empty())) << "payload_frontend found no cl.exe under " << t.root; - EXPECT_EQ(found->filename(), "cl.exe"); + expect(eq(found->filename(), "cl.exe")); // The `bin/`-shaped question is the one that used to be asked, and it // still answers nothing here — which is exactly why it was the wrong // question rather than a broken implementation. - EXPECT_TRUE(toolchain_frontend(t.root / "bin", pkg).empty()); + expect(toolchain_frontend(t.root / "bin", pkg).empty()); // A root with no toolset at that version stays empty rather than // returning a path that does not exist. - EXPECT_TRUE(payload_frontend(t.root, + expect(payload_frontend(t.root, to_xim_package(*parse_toolchain_spec("msvc@14.52.36629"))) .value().empty()); @@ -376,24 +378,24 @@ TEST(MsvcManaged, PayloadFrontendFindsClWhereMsvcActuallyKeepsIt) { // carries the toolset version, so a refusal naming `bin` would name a // directory nothing looked in. auto dir = payload_frontend_dir(t.root, pkg); - EXPECT_FALSE(dir.empty()); - EXPECT_NE(dir.string().find("14.44.35207"), std::string::npos) << dir; -} + expect(!dir.empty()); + expect(ne(dir.string().find("14.44.35207"), std::string::npos)) << dir; +}}; // ─── Windows SDK discovery ─────────────────────────────────────────────── -TEST(MsvcSdk, WindowsSdkDirBeatsTheHardcodedPaths) { +UTest windows_sdk_dir_beats_the_hardcoded_paths{"MsvcSdk/WindowsSdkDirBeatsTheHardcodedPaths", [] { NoSdkEnv clean; FakeToolset t{"sdkenv"}; t.add_sdk("10.0.26100.0"); ScopedEnv dir{"WindowsSdkDir", t.root.string()}; auto sdk = msvc::find_windows_sdk(); - ASSERT_TRUE(sdk.has_value()); - EXPECT_EQ(sdk->root, t.root); - EXPECT_EQ(sdk->version, "10.0.26100.0"); -} + expect(fatal(sdk.has_value())); + expect(eq(sdk->root, t.root)); + expect(eq(sdk->version, "10.0.26100.0")); +}}; -TEST(MsvcSdk, WindowsSdkVersionSelectsAmongInstalledOnes) { +UTest windows_sdk_version_selects_among_installed_ones{"MsvcSdk/WindowsSdkVersionSelectsAmongInstalledOnes", [] { NoSdkEnv clean; FakeToolset t{"sdkver"}; t.add_sdk("10.0.22621.0"); @@ -403,16 +405,16 @@ TEST(MsvcSdk, WindowsSdkVersionSelectsAmongInstalledOnes) { // the directory name, and comparing it raw finds nothing. ScopedEnv ver{"WindowsSdkVersion", "10.0.22621.0\\"}; auto sdk = msvc::find_windows_sdk(); - ASSERT_TRUE(sdk.has_value()); - EXPECT_EQ(sdk->version, "10.0.22621.0"); + expect(fatal(sdk.has_value())); + expect(eq(sdk->version, "10.0.22621.0")); } // Unset again: highest usable wins. auto sdk = msvc::find_windows_sdk(); - ASSERT_TRUE(sdk.has_value()); - EXPECT_EQ(sdk->version, "10.0.26100.0"); -} + expect(fatal(sdk.has_value())); + expect(eq(sdk->version, "10.0.26100.0")); +}}; -TEST(MsvcSdk, ExtraRootsCoverTheManagedPayload) { +UTest extra_roots_cover_the_managed_payload{"MsvcSdk/ExtraRootsCoverTheManagedPayload", [] { NoSdkEnv clean; FakeToolset t{"sdkextra"}; t.add_sdk("10.0.26100.0"); @@ -420,11 +422,11 @@ TEST(MsvcSdk, ExtraRootsCoverTheManagedPayload) { // found, which is exactly the managed toolset's situation. std::array roots{t.root}; auto sdk = msvc::find_windows_sdk(roots); - ASSERT_TRUE(sdk.has_value()); - EXPECT_EQ(sdk->root, t.root); -} + expect(fatal(sdk.has_value())); + expect(eq(sdk->root, t.root)); +}}; -TEST(MsvcSdk, IncompleteSdkRootIsNotAnAnswer) { +UTest incomplete_sdk_root_is_not_an_answer{"MsvcSdk/IncompleteSdkRootIsNotAnAnswer", [] { // Include// exists but carries no ucrt/corecrt.h: a half-installed // SDK must read as absent, not as a usable one that fails later inside // the compiler. Checked through extraRoots so the assertion cannot be @@ -434,8 +436,8 @@ TEST(MsvcSdk, IncompleteSdkRootIsNotAnAnswer) { std::filesystem::create_directories(t.root / "Include" / "10.0.26100.0" / "um"); std::array roots{t.root}; auto sdk = msvc::find_windows_sdk(roots); - if (sdk) EXPECT_NE(sdk->root, t.root) << "an SDK-less root was accepted"; -} + if (sdk) expect(ne(sdk->root, t.root)) << "an SDK-less root was accepted"; +}}; // ─── the toolset's own redistributable CRT ─────────────────────────────── @@ -477,89 +479,88 @@ struct FakeRedist { } // namespace -TEST(MsvcRedist, FoundFromTheCompilerPathAlone) { +UTest found_from_the_compiler_path_alone{"MsvcRedist/FoundFromTheCompilerPathAlone", [] { // Nothing configured, no version derived: the compiler's own location // says which toolset this is, and the toolset carries its runtime. FakeRedist t{"14.44.35207", "14.44.35112"}; auto d = msvc::vc_redist_dir(t.clPath, "x64"); - ASSERT_FALSE(d.empty()) << "the toolset's redistributable CRT was not found"; - EXPECT_TRUE(std::filesystem::exists(d / "vcruntime140.dll")); -} + expect(fatal(!d.empty())) << "the toolset's redistributable CRT was not found"; + expect(std::filesystem::exists(d / "vcruntime140.dll")); +}}; -TEST(MsvcRedist, TheRedistVersionIsNotTheToolsVersion) { +UTest the_redist_version_is_not_the_tools_version{"MsvcRedist/TheRedistVersionIsNotTheToolsVersion", [] { // 14.44.35207 (tools) vs 14.44.35112 (redist) is what MSVC actually // ships. Deriving one from the other finds nothing — this is why the // directory is searched rather than composed. FakeRedist t{"14.44.35207", "14.44.35112"}; auto d = msvc::vc_redist_dir(t.clPath, "x64"); - ASSERT_FALSE(d.empty()); - EXPECT_NE(d.string().find("14.44.35112"), std::string::npos) + expect(fatal(!d.empty())); + expect(ne(d.string().find("14.44.35112"), std::string::npos)) << "did not land in the redist version directory: " << d.string(); -} +}}; -TEST(MsvcRedist, NewestRedistWins) { +UTest newest_redist_wins{"MsvcRedist/NewestRedistWins", [] { FakeRedist t{"14.44.35207", "14.40.00000"}; t.add("14.44.35112", "x64", "Microsoft.VC143.CRT", "vcruntime140.dll"); auto d = msvc::vc_redist_dir(t.clPath, "x64"); - ASSERT_FALSE(d.empty()); - EXPECT_NE(d.string().find("14.44.35112"), std::string::npos) + expect(fatal(!d.empty())); + expect(ne(d.string().find("14.44.35112"), std::string::npos)) << "older redist won: " << d.string(); -} +}}; -TEST(MsvcRedist, TheDebugCrtIsNeverReturned) { +UTest the_debug_crt_is_never_returned{"MsvcRedist/TheDebugCrtIsNeverReturned", [] { // debug_nonredist may NOT be redistributed. Returning it would put // vcruntime140d.dll on PATH and, later, into a shipped artifact. FakeRedist t{"14.44.35207", "14.44.35112"}; t.add_debug("14.99.99999"); // newer, and must still lose auto d = msvc::vc_redist_dir(t.clPath, "x64"); - ASSERT_FALSE(d.empty()); - EXPECT_EQ(d.string().find("debug_nonredist"), std::string::npos) + expect(fatal(!d.empty())); + expect(eq(d.string().find("debug_nonredist"), std::string::npos)) << "returned the non-redistributable debug CRT: " << d.string(); -} +}}; -TEST(MsvcRedist, AToolsetWithoutARedistIsNotAnError) { +UTest a_toolset_without_a_redist_is_not_an_error{"MsvcRedist/AToolsetWithoutARedistIsNotAnError", [] { // msvc@system on a machine whose VS install omits the redist component, // for instance. Empty means "nothing to add to PATH", not a failure. FakeRedist t{"14.44.35207", "14.44.35112"}; std::error_code ec; std::filesystem::remove_all(t.root / "VC" / "Redist", ec); - EXPECT_TRUE(msvc::vc_redist_dir(t.clPath, "x64").empty()); -} + expect(msvc::vc_redist_dir(t.clPath, "x64").empty()); +}}; // #718: the LLVM row reaches the SAME redistributable from its sysroot's // tools directory, since it runs no cl.exe of its own to derive one from. -TEST(MsvcRedist, TheSameDirectoryIsReachableFromTheToolsDirAlone) { +UTest the_same_directory_is_reachable_from_the_tools_dir_alone{"MsvcRedist/TheSameDirectoryIsReachableFromTheToolsDirAlone", [] { FakeRedist t{"14.44.35207", "14.44.35112"}; // /Tools/MSVC//bin/Hostx64/x64/cl.exe -> up 4 reaches the tools // dir `Toolchain::msvcToolsDir` carries (/Tools/MSVC/). auto toolsDir = t.clPath.parent_path().parent_path().parent_path().parent_path(); auto fromCl = msvc::vc_redist_dir(t.clPath, "x64"); auto fromTools = msvc::vc_redist_dir_for_tools_dir(toolsDir, "x86_64"); - ASSERT_FALSE(fromCl.empty()); - EXPECT_EQ(fromCl, fromTools); -} + expect(fatal(!fromCl.empty())); + expect(eq(fromCl, fromTools)); +}}; -TEST(MsvcRedist, ToolsDirArchMapping) { +UTest tools_dir_arch_mapping{"MsvcRedist/ToolsDirArchMapping", [] { // The GNU-spelled triple architecture (what a Toolchain carries) maps to // the msvc spelling `vc_redist_dir` takes. FakeRedist t{"14.44.35207", "14.44.35112"}; t.add("14.44.35112", "arm64", "Microsoft.VC143.CRT", "vcruntime140.dll"); t.add("14.44.35112", "x86", "Microsoft.VC143.CRT", "vcruntime140.dll"); auto toolsDir = t.clPath.parent_path().parent_path().parent_path().parent_path(); - EXPECT_FALSE(msvc::vc_redist_dir_for_tools_dir(toolsDir, "aarch64").empty()); - EXPECT_FALSE(msvc::vc_redist_dir_for_tools_dir(toolsDir, "i686").empty()); - EXPECT_EQ(msvc::vc_redist_dir_for_tools_dir(toolsDir, "i386"), - msvc::vc_redist_dir_for_tools_dir(toolsDir, "i686")); - EXPECT_FALSE(msvc::vc_redist_dir_for_tools_dir(toolsDir, "x86_64").empty()); + expect(!(msvc::vc_redist_dir_for_tools_dir(toolsDir, "aarch64").empty())); + expect(!(msvc::vc_redist_dir_for_tools_dir(toolsDir, "i686").empty())); + expect(eq(msvc::vc_redist_dir_for_tools_dir(toolsDir, "i386"), msvc::vc_redist_dir_for_tools_dir(toolsDir, "i686"))); + expect(!(msvc::vc_redist_dir_for_tools_dir(toolsDir, "x86_64").empty())); // An architecture this toolset was never given a redist for finds none — // it does not fall back to a different one. std::error_code ec; auto vc = toolsDir.parent_path().parent_path().parent_path(); // std::filesystem::remove_all(vc / "Redist" / "MSVC" / "14.44.35112" / "arm64", ec); - EXPECT_TRUE(msvc::vc_redist_dir_for_tools_dir(toolsDir, "aarch64").empty()); -} + expect(msvc::vc_redist_dir_for_tools_dir(toolsDir, "aarch64").empty()); +}}; -TEST(MsvcSdk, HeadersWithoutImportLibsIsNotAnAnswer) { +UTest headers_without_import_libs_is_not_an_answer{"MsvcSdk/HeadersWithoutImportLibsIsNotAnAnswer", [] { // The half that used to pass. `Include//ucrt/corecrt.h` is there and // `Lib/` is not, which is exactly what a managed windows-sdk payload // looked like when its um-libs MSI had not unpacked: every TU compiled @@ -573,11 +574,11 @@ TEST(MsvcSdk, HeadersWithoutImportLibsIsNotAnAnswer) { t.add_sdk_headers_only("10.0.26100.0"); std::array roots{t.root}; auto sdk = msvc::find_windows_sdk(roots); - if (sdk) EXPECT_NE(sdk->root, t.root) + if (sdk) expect(ne(sdk->root, t.root)) << "a headers-only SDK was accepted; the link would fail on kernel32.lib"; -} +}}; -TEST(MsvcSdk, ACompleteRootIsStillAccepted) { +UTest a_complete_root_is_still_accepted{"MsvcSdk/ACompleteRootIsStillAccepted", [] { // The other direction, so the check above cannot be satisfied by // rejecting everything. NoSdkEnv clean; @@ -585,12 +586,12 @@ TEST(MsvcSdk, ACompleteRootIsStillAccepted) { t.add_sdk("10.0.26100.0"); std::array roots{t.root}; auto sdk = msvc::find_windows_sdk(roots); - ASSERT_TRUE(sdk) << "a complete SDK root was rejected"; - EXPECT_EQ(sdk->root, t.root); - EXPECT_EQ(sdk->version, "10.0.26100.0"); -} + expect(fatal(bool(sdk))) << "a complete SDK root was rejected"; + expect(eq(sdk->root, t.root)); + expect(eq(sdk->version, "10.0.26100.0")); +}}; -TEST(MsvcSdk, APartialRootYieldsToACompleteOne) { +UTest a_partial_root_yields_to_a_complete_one{"MsvcSdk/APartialRootYieldsToACompleteOne", [] { // Ordering, not just acceptance: given both, the usable one must win even // though the partial one carries the HIGHER version — which is how the // managed payload outranked the system SDK in the first place. @@ -601,12 +602,12 @@ TEST(MsvcSdk, APartialRootYieldsToACompleteOne) { complete.add_sdk("10.0.26100.0"); std::array roots{partial.root, complete.root}; auto sdk = msvc::find_windows_sdk(roots); - ASSERT_TRUE(sdk); - EXPECT_EQ(sdk->root, complete.root) + expect(fatal(bool(sdk))); + expect(eq(sdk->root, complete.root)) << "the partial root won on version; it cannot link"; -} +}}; -TEST(MsvcSdk, DeclaredRootOutranksTheManagedOne) { +UTest declared_root_outranks_the_managed_one{"MsvcSdk/DeclaredRootOutranksTheManagedOne", [] { // Both available. WindowsSdkDir is someone saying which one to use, and // it has to win — the same precedence VSINSTALLDIR has over vswhere. NoSdkEnv clean; @@ -617,30 +618,30 @@ TEST(MsvcSdk, DeclaredRootOutranksTheManagedOne) { ScopedEnv dir{"WindowsSdkDir", declared.root.string()}; std::array roots{managed.root}; auto sdk = msvc::find_windows_sdk(roots); - ASSERT_TRUE(sdk.has_value()); - EXPECT_EQ(sdk->root, declared.root); - EXPECT_EQ(sdk->version, "10.0.22621.0"); -} + expect(fatal(sdk.has_value())); + expect(eq(sdk->root, declared.root)); + expect(eq(sdk->version, "10.0.22621.0")); +}}; -TEST(MsvcSdk, SiblingSdkRootsAreEmptyForACompilerOutsideAnyStore) { +UTest sibling_sdk_roots_are_empty_for_a_compiler_outside_any_store{"MsvcSdk/SiblingSdkRootsAreEmptyForACompilerOutsideAnyStore", [] { // A system cl.exe is not in an xlings store, so there is nothing to // offer — and offering the wrong thing would be worse than nothing. - EXPECT_TRUE(msvc::sibling_sdk_roots( + expect(msvc::sibling_sdk_roots( "C:/Program Files/Microsoft Visual Studio/18/Enterprise/VC/Tools/" "MSVC/14.51.36231/bin/Hostx64/x64/cl.exe").empty()); -} +}}; // ─── model traits ──────────────────────────────────────────────────────── -TEST(MsvcModel, BmiTraitsUseIfc) { +UTest bmi_traits_use_ifc{"MsvcModel/BmiTraitsUseIfc", [] { Toolchain tc; tc.compiler = CompilerId::MSVC; auto t = bmi_traits(tc); - EXPECT_EQ(t.bmiDir, "ifc.cache"); - EXPECT_EQ(t.bmiExt, ".ifc"); - EXPECT_TRUE(t.needsExplicitModuleOutput); - EXPECT_FALSE(t.scanNeedsFModules); -} + expect(eq(t.bmiDir, "ifc.cache")); + expect(eq(t.bmiExt, ".ifc")); + expect(bool(t.needsExplicitModuleOutput)); + expect(!t.scanNeedsFModules); +}}; // ─── std module minimum standard level ─────────────────────────────────── // @@ -650,7 +651,7 @@ TEST(MsvcModel, BmiTraitsUseIfc) { // refuse with an actionable message instead. // See .agents/docs/2026-07-31-cpp20-standard-support-design.md §2.3. -TEST(MsvcStdModule, MinLevelFollowsStlUnblockVersion) { +UTest min_level_follows_stl_unblock_version{"MsvcStdModule/MinLevelFollowsStlUnblockVersion", [] { auto tc_of = [](std::string ver) { Toolchain tc; tc.compiler = CompilerId::MSVC; @@ -658,16 +659,16 @@ TEST(MsvcStdModule, MinLevelFollowsStlUnblockVersion) { return tc; }; // VS 2022 17.8 and newer: C++20 is allowed. - EXPECT_EQ(msvc::std_module_min_level(tc_of("19.38.33130")), 20); - EXPECT_EQ(msvc::std_module_min_level(tc_of("19.44.35211")), 20); - EXPECT_EQ(msvc::std_module_min_level(tc_of("20.0")), 20); + expect(eq(msvc::std_module_min_level(tc_of("19.38.33130")), 20)); + expect(eq(msvc::std_module_min_level(tc_of("19.44.35211")), 20)); + expect(eq(msvc::std_module_min_level(tc_of("20.0")), 20)); // Older toolsets keep the C++23 floor. - EXPECT_EQ(msvc::std_module_min_level(tc_of("19.37.32825")), 23); - EXPECT_EQ(msvc::std_module_min_level(tc_of("19.29.30153")), 23); + expect(eq(msvc::std_module_min_level(tc_of("19.37.32825")), 23)); + expect(eq(msvc::std_module_min_level(tc_of("19.29.30153")), 23)); // Unparseable banner version: stay strict rather than guess. - EXPECT_EQ(msvc::std_module_min_level(tc_of("")), 23); - EXPECT_EQ(msvc::std_module_min_level(tc_of("unknown")), 23); -} + expect(eq(msvc::std_module_min_level(tc_of("")), 23)); + expect(eq(msvc::std_module_min_level(tc_of("unknown")), 23)); +}}; // #603 — THE QUESTION IS ASKED OF THE STL, NOT OF WHATEVER COMPILER REACHES IT. // @@ -680,28 +681,28 @@ TEST(MsvcStdModule, MinLevelFollowsStlUnblockVersion) { // because it would compare a CLANG version number against an MSVC threshold and // be right by accident: clang 20.x passes `>= 19.38` and clang 19.x fails it. // The toolset version is in the path of the module source that was selected. -TEST(MsvcStdModule, MinLevelForStlReadsTheToolsetOutOfTheModuleSourcePath) { +UTest min_level_for_stl_reads_the_toolset_out_of_the_module_source_path{"MsvcStdModule/MinLevelForStlReadsTheToolsetOutOfTheModuleSourcePath", [] { auto lvl = [](std::string p) { return msvc::std_module_min_level_for_stl(std::filesystem::path(p)); }; // VS 2022 17.8 (toolset 14.38) is where microsoft/STL#3977 first ships. - EXPECT_EQ(lvl("C:/VS/VC/Tools/MSVC/14.38.33130/modules/std.ixx"), 20); - EXPECT_EQ(lvl("C:/VS/VC/Tools/MSVC/14.44.35207/modules/std.ixx"), 20); + expect(eq(lvl("C:/VS/VC/Tools/MSVC/14.38.33130/modules/std.ixx"), 20)); + expect(eq(lvl("C:/VS/VC/Tools/MSVC/14.44.35207/modules/std.ixx"), 20)); // Older toolsets keep the C++23 floor and get an actionable diagnostic. - EXPECT_EQ(lvl("C:/VS/VC/Tools/MSVC/14.37.32822/modules/std.ixx"), 23); - EXPECT_EQ(lvl("C:/VS/VC/Tools/MSVC/14.29.30133/modules/std.ixx"), 23); + expect(eq(lvl("C:/VS/VC/Tools/MSVC/14.37.32822/modules/std.ixx"), 23)); + expect(eq(lvl("C:/VS/VC/Tools/MSVC/14.29.30133/modules/std.ixx"), 23)); // A layout this mapping is not defined for answers 23 rather than guessing. // The safety the hardcode was after is kept for exactly these cases. - EXPECT_EQ(lvl(""), 23); - EXPECT_EQ(lvl("C:/VS/VC/Tools/MSVC/modules/std.ixx"), 23); - EXPECT_EQ(lvl("/opt/llvm/share/libc++/v1/std.cppm"), 23); - EXPECT_EQ(lvl("C:/VS/VC/Tools/MSVC/15.0.0/modules/std.ixx"), 23); -} + expect(eq(lvl(""), 23)); + expect(eq(lvl("C:/VS/VC/Tools/MSVC/modules/std.ixx"), 23)); + expect(eq(lvl("/opt/llvm/share/libc++/v1/std.cppm"), 23)); + expect(eq(lvl("C:/VS/VC/Tools/MSVC/15.0.0/modules/std.ixx"), 23)); +}}; // The two forms must agree for a well-formed installation, because that is the // one path the banner form was verified on. If they could disagree there, this // change would be a silent behaviour change on the toolchain that worked. -TEST(MsvcStdModule, TheStlAndBannerFormsAgreeForAWellFormedInstallation) { +UTest the_stl_and_banner_forms_agree_for_a_well_formed_installation{"MsvcStdModule/TheStlAndBannerFormsAgreeForAWellFormedInstallation", [] { auto tc_of = [](std::string ver) { Toolchain tc; tc.compiler = CompilerId::MSVC; @@ -714,13 +715,12 @@ TEST(MsvcStdModule, TheStlAndBannerFormsAgreeForAWellFormedInstallation) { std::pair{"19.44.35211", "14.44.35207"}, std::pair{"19.37.32825", "14.37.32822"}, std::pair{"19.29.30153", "14.29.30133"} }) { - EXPECT_EQ(msvc::std_module_min_level(tc_of(banner)), - msvc::std_module_min_level_for_stl(std::filesystem::path( + expect(eq(msvc::std_module_min_level(tc_of(banner)), msvc::std_module_min_level_for_stl(std::filesystem::path( std::string("C:/VS/VC/Tools/MSVC/") + toolset - + "/modules/std.ixx"))) + + "/modules/std.ixx")))) << "banner " << banner << " vs toolset " << toolset; } -} +}}; // #422 — the std module must be built with the SAME CRT model as the TUs that // import it. @@ -737,7 +737,7 @@ TEST(MsvcStdModule, TheStlAndBannerFormsAgreeForAWellFormedInstallation) { // and it must be in the command STRING (so it enters `std_build_commands`, // which is part of the std cache identity — two CRT models then cannot share a // cache directory and silently serve each other's module). -TEST(ToolchainMsvc, StdModuleCarriesTheProjectCrtModel) { +UTest std_module_carries_the_project_crt_model{"ToolchainMsvc/StdModuleCarriesTheProjectCrtModel", [] { Toolchain tc; tc.compiler = CompilerId::MSVC; tc.binaryPath = "C:/vc/bin/cl.exe"; @@ -748,63 +748,60 @@ TEST(ToolchainMsvc, StdModuleCarriesTheProjectCrtModel) { for (std::string_view crt : {std::string_view("/MT"), std::string_view("/MD")}) { auto cmds = msvc::std_module_build_commands(tc, cache, "/std:c++23", crt); - ASSERT_EQ(cmds.size(), 1u); - EXPECT_NE(cmds[0].find(crt), std::string::npos) - << crt << " missing from the std build command: " << cmds[0]; + expect(fatal(eq(cmds.size(), 1u))); + if (auto&& failed = expect(ne(cmds[0].find(crt), std::string::npos)); !failed) failed << crt << " missing from the std build command: " << cmds[0]; auto compat = msvc::std_compat_build_commands(tc, cache, "/std:c++23", crt); - ASSERT_EQ(compat.size(), 1u); - EXPECT_NE(compat[0].find(crt), std::string::npos) - << crt << " missing from the std.compat build command: " << compat[0]; + expect(fatal(eq(compat.size(), 1u))); + if (auto&& failed = expect(ne(compat[0].find(crt), std::string::npos)); !failed) failed << crt << " missing from the std.compat build command: " << compat[0]; } // The two models must produce DIFFERENT command strings. Equal strings would // mean equal cache keys, i.e. the exact silent divergence this fixes. - EXPECT_NE(msvc::std_module_build_commands(tc, cache, "/std:c++23", "/MT")[0], - msvc::std_module_build_commands(tc, cache, "/std:c++23", "/MD")[0]); + expect(ne(msvc::std_module_build_commands(tc, cache, "/std:c++23", "/MT")[0], msvc::std_module_build_commands(tc, cache, "/std:c++23", "/MD")[0])); // Empty keeps cl's own default, so non-MSVC callers are unaffected. auto bare = msvc::std_module_build_commands(tc, cache, "/std:c++23"); - EXPECT_EQ(bare[0].find("/MT"), std::string::npos); - EXPECT_EQ(bare[0].find("/MD"), std::string::npos); -} + expect(eq(bare[0].find("/MT"), std::string::npos)); + expect(eq(bare[0].find("/MD"), std::string::npos)); +}}; // The mapping linkage -> CRT model lives in ONE place. It was derived twice — // flags.cppm for the project's TUs, and cl's default for the std module — which // is how they disagreed. -TEST(ToolchainMsvc, CrtFlagHasASingleDerivation) { +UTest crt_flag_has_a_single_derivation{"ToolchainMsvc/CrtFlagHasASingleDerivation", [] { Toolchain cl; cl.compiler = CompilerId::MSVC; const auto& msvcDialect = dialect_for(cl); - EXPECT_EQ(msvc_crt_flag(msvcDialect, /*staticCrt=*/true), "/MT"); - EXPECT_EQ(msvc_crt_flag(msvcDialect, /*staticCrt=*/false), "/MD"); + expect(eq(msvc_crt_flag(msvcDialect, /*staticCrt=*/true), "/MT")); + expect(eq(msvc_crt_flag(msvcDialect, /*staticCrt=*/false), "/MD")); // GNU has no counterpart; the helper must yield nothing rather than invent // a flag that would be passed to gcc. Toolchain gcc; gcc.compiler = CompilerId::GCC; const auto& gnu = dialect_for(gcc); - EXPECT_TRUE(msvc_crt_flag(gnu, false).empty()); -} + expect(msvc_crt_flag(gnu, false).empty()); +}}; // Both manifest keys reach the SAME physical switch, because on the MSVC ABI // they are the same switch. `cxx_runtime = "self-contained"` used to report // "not implemented" while `linkage = "static"` quietly did the very thing it // said was unimplemented. -TEST(ToolchainMsvc, BothKeysSelectTheStaticCrt) { - EXPECT_TRUE(msvc_wants_static_crt("static", "")); - EXPECT_TRUE(msvc_wants_static_crt("", "self-contained")); - EXPECT_TRUE(msvc_wants_static_crt("static", "self-contained")); +UTest both_keys_select_the_static_crt{"ToolchainMsvc/BothKeysSelectTheStaticCrt", [] { + expect(bool(msvc_wants_static_crt("static", ""))); + expect(bool(msvc_wants_static_crt("", "self-contained"))); + expect(bool(msvc_wants_static_crt("static", "self-contained"))); // Default: neither written down. This has to stay /MD — most roles // DEFAULT to the self-contained contract, so keying off the resolved // contract instead of the written key would flip every Windows build to // /MT and split the CRT across a project's dependencies. - EXPECT_FALSE(msvc_wants_static_crt("", "")); - EXPECT_FALSE(msvc_wants_static_crt("dynamic", "")); - EXPECT_FALSE(msvc_wants_static_crt("", "host-coupled")); - EXPECT_FALSE(msvc_wants_static_crt("", "toolchain-coupled")); -} + expect(!msvc_wants_static_crt("", "")); + expect(!msvc_wants_static_crt("dynamic", "")); + expect(!msvc_wants_static_crt("", "host-coupled")); + expect(!msvc_wants_static_crt("", "toolchain-coupled")); +}}; // ─── the SDK axis: bound for a managed toolset, searched for a system one ── // @@ -865,28 +862,28 @@ struct FakeStore { } // namespace -TEST(MsvcSdkOrigin, ACompilerInAStoreIsManagedAndOneOutsideIsNot) { +UTest a_compiler_in_a_store_is_managed_and_one_outside_is_not{"MsvcSdkOrigin/ACompilerInAStoreIsManagedAndOneOutsideIsNot", [] { FakeStore store; auto cl = store.add_toolset("14.44.35207"); - EXPECT_EQ(msvc::origin_of(cl), Origin::Managed); - EXPECT_EQ(msvc::origin_of( + expect(eq(msvc::origin_of(cl), Origin::Managed)); + expect(eq(msvc::origin_of( "C:/Program Files/Microsoft Visual Studio/18/Enterprise/VC/Tools/" - "MSVC/14.51.36231/bin/Hostx64/x64/cl.exe"), Origin::SystemMsvc); -} + "MSVC/14.51.36231/bin/Hostx64/x64/cl.exe"), Origin::SystemMsvc)); +}}; -TEST(MsvcSdkOrigin, AManagedToolsetTakesTheSdkFromItsOwnStore) { +UTest a_managed_toolset_takes_the_sdk_from_its_own_store{"MsvcSdkOrigin/AManagedToolsetTakesTheSdkFromItsOwnStore", [] { NoSdkEnv clean; FakeStore store; auto cl = store.add_toolset("14.44.35207"); store.add_sdk("10.0.26100.0"); auto choice = msvc::resolve_sdk_for(cl); - ASSERT_TRUE(choice.sdk.has_value()) + expect(fatal(choice.sdk.has_value())) << "the toolset's own SDK payload was not found"; - EXPECT_EQ(choice.origin, Origin::Managed); - EXPECT_EQ(choice.sdk->version, "10.0.26100.0"); - EXPECT_TRUE(choice.note.empty()) << choice.note; -} + expect(eq(choice.origin, Origin::Managed)); + expect(eq(choice.sdk->version, "10.0.26100.0")); + expect(choice.note.empty()) << choice.note; +}}; // THE CRITERION THIS AXIS EXISTS FOR. An environment that can overwrite the // choice means the manifest did not pin anything — it only expressed a @@ -895,7 +892,7 @@ TEST(MsvcSdkOrigin, AManagedToolsetTakesTheSdkFromItsOwnStore) { // This is the design doc's §6 acceptance test, as a unit test: point // WindowsSdkDir somewhere else entirely and the build must still use the // payload's SDK, and must SAY that the variable was ignored. -TEST(MsvcSdkOrigin, WindowsSdkDirCannotOverrideAPinnedToolsetsSdk) { +UTest windows_sdk_dir_cannot_override_a_pinned_toolsets_sdk{"MsvcSdkOrigin/WindowsSdkDirCannotOverrideAPinnedToolsetsSdk", [] { NoSdkEnv clean; FakeStore store; auto cl = store.add_toolset("14.44.35207"); @@ -906,16 +903,16 @@ TEST(MsvcSdkOrigin, WindowsSdkDirCannotOverrideAPinnedToolsetsSdk) { ScopedEnv dir{"WindowsSdkDir", elsewhere.root.string()}; auto choice = msvc::resolve_sdk_for(cl); - ASSERT_TRUE(choice.sdk.has_value()); - EXPECT_EQ(choice.sdk->version, "10.0.26100.0") + expect(fatal(choice.sdk.has_value())); + expect(eq(choice.sdk->version, "10.0.26100.0")) << "the environment overrode a pinned toolset's SDK; the pin is not one"; - EXPECT_NE(choice.sdk->root, elsewhere.root); - EXPECT_NE(choice.note.find("WindowsSdkDir"), std::string::npos) + expect(ne(choice.sdk->root, elsewhere.root)); + expect(ne(choice.note.find("WindowsSdkDir"), std::string::npos)) << "an ignored override that says nothing is indistinguishable from " "one that was never set: " << choice.note; -} +}}; -TEST(MsvcSdkOrigin, WindowsSdkVersionDoesNotPickAmongPayloadsEither) { +UTest windows_sdk_version_does_not_pick_among_payloads_either{"MsvcSdkOrigin/WindowsSdkVersionDoesNotPickAmongPayloadsEither", [] { // Same override wearing a smaller hat: naming a version is still the // environment choosing which headers a pinned build compiles against. NoSdkEnv clean; @@ -926,13 +923,13 @@ TEST(MsvcSdkOrigin, WindowsSdkVersionDoesNotPickAmongPayloadsEither) { ScopedEnv ver{"WindowsSdkVersion", "10.0.22621.0\\"}; auto choice = msvc::resolve_sdk_for(cl); - ASSERT_TRUE(choice.sdk.has_value()); - EXPECT_EQ(choice.sdk->version, "10.0.26100.0") + expect(fatal(choice.sdk.has_value())); + expect(eq(choice.sdk->version, "10.0.26100.0")) << "WindowsSdkVersion selected among the store's payloads"; - EXPECT_FALSE(choice.note.empty()); -} + expect(!choice.note.empty()); +}}; -TEST(MsvcSdkOrigin, AManagedToolsetWithNoSdkPayloadFallsBackAndSaysSo) { +UTest a_managed_toolset_with_no_sdk_payload_falls_back_and_says_so{"MsvcSdkOrigin/AManagedToolsetWithNoSdkPayloadFallsBackAndSaysSo", [] { // Working beats failing here — the machine may well have a complete SDK, // and refusing to build would be a regression for anyone whose toolset // predates the SDK dependency. What must not happen is the fallback being @@ -947,12 +944,12 @@ TEST(MsvcSdkOrigin, AManagedToolsetWithNoSdkPayloadFallsBackAndSaysSo) { ScopedEnv dir{"WindowsSdkDir", machine.root.string()}; auto choice = msvc::resolve_sdk_for(cl); - ASSERT_TRUE(choice.sdk.has_value()); - EXPECT_EQ(choice.sdk->root, machine.root); - EXPECT_NE(choice.note.find("machine"), std::string::npos) << choice.note; -} + expect(fatal(choice.sdk.has_value())); + expect(eq(choice.sdk->root, machine.root)); + expect(ne(choice.note.find("machine"), std::string::npos)) << choice.note; +}}; -TEST(MsvcSdkOrigin, ASystemToolsetKeepsTheDeclaredSearchChain) { +UTest a_system_toolset_keeps_the_declared_search_chain{"MsvcSdkOrigin/ASystemToolsetKeepsTheDeclaredSearchChain", [] { // The other origin is unchanged, and must be: a machine's things can only // be found by looking, and WindowsSdkDir is the most specific answer // available there — the same precedence VSINSTALLDIR has over vswhere. @@ -966,12 +963,12 @@ TEST(MsvcSdkOrigin, ASystemToolsetKeepsTheDeclaredSearchChain) { auto cl = vs.root / "VC" / "Tools" / "MSVC" / "14.51.36231" / "bin" / "Hostx64" / "x64" / "cl.exe"; auto choice = msvc::resolve_sdk_for(cl); - EXPECT_EQ(choice.origin, Origin::SystemMsvc); - ASSERT_TRUE(choice.sdk.has_value()); - EXPECT_EQ(choice.sdk->root, declared.root); - EXPECT_TRUE(choice.note.empty()) + expect(eq(choice.origin, Origin::SystemMsvc)); + expect(fatal(choice.sdk.has_value())); + expect(eq(choice.sdk->root, declared.root)); + expect(choice.note.empty()) << "nothing was ignored, so there is nothing to report: " << choice.note; -} +}}; // ─── Toolset selection: one rule for the cl.exe row and the clang row ───── // @@ -1032,53 +1029,53 @@ struct TwoInstances { } // namespace -TEST(MsvcToolsetVersion, ComparedAsNumbersNotStrings) { - EXPECT_LT(msvc::compare_toolset_versions("14.9", "14.10"), 0); - EXPECT_GT(msvc::compare_toolset_versions("14.44.35207", "14.38.33130"), 0); - EXPECT_EQ(msvc::compare_toolset_versions("14.44", "14.44.0"), 0); - EXPECT_EQ(msvc::compare_toolset_versions("17.14.36301.6", "17.14.36301.6"), 0); -} - -TEST(MsvcToolsetVersion, APrefixMatchesWholeComponents) { - EXPECT_TRUE(msvc::toolset_version_matches("14.44", "14.44.35207")); - EXPECT_TRUE(msvc::toolset_version_matches("14.44.35207", "14.44.35207")); - EXPECT_TRUE(msvc::toolset_version_matches("14", "14.51.36014")); +UTest compared_as_numbers_not_strings{"MsvcToolsetVersion/ComparedAsNumbersNotStrings", [] { + expect(lt(msvc::compare_toolset_versions("14.9", "14.10"), 0)); + expect(gt(msvc::compare_toolset_versions("14.44.35207", "14.38.33130"), 0)); + expect(eq(msvc::compare_toolset_versions("14.44", "14.44.0"), 0)); + expect(eq(msvc::compare_toolset_versions("17.14.36301.6", "17.14.36301.6"), 0)); +}}; + +UTest a_prefix_matches_whole_components{"MsvcToolsetVersion/APrefixMatchesWholeComponents", [] { + expect(msvc::toolset_version_matches("14.44", "14.44.35207")); + expect(msvc::toolset_version_matches("14.44.35207", "14.44.35207")); + expect(msvc::toolset_version_matches("14", "14.51.36014")); // `14.4` is the 14.4 line, not a text prefix of 14.44. - EXPECT_FALSE(msvc::toolset_version_matches("14.4", "14.44.35207")); - EXPECT_FALSE(msvc::toolset_version_matches("14.44.35207.1", "14.44.35207")); - EXPECT_FALSE(msvc::toolset_version_matches("", "14.44.35207")); -} + expect(!msvc::toolset_version_matches("14.4", "14.44.35207")); + expect(!msvc::toolset_version_matches("14.44.35207.1", "14.44.35207")); + expect(!msvc::toolset_version_matches("", "14.44.35207")); +}}; -TEST(MsvcToolsetSelect, APinnedVersionIsFoundInWhicheverInstanceHasIt) { +UTest a_pinned_version_is_found_in_whichever_instance_has_it{"MsvcToolsetSelect/APinnedVersionIsFoundInWhicheverInstanceHasIt", [] { TwoInstances m; msvc::ToolsetNeeds needs; auto a = msvc::select_system_toolset(m.all(), {}, "14.38", needs); - ASSERT_TRUE(a.has_value()); - EXPECT_EQ(a->version, "14.38.33130"); - EXPECT_EQ(a->toolsDir, m.vs2022.tools("14.38.33130")); - EXPECT_EQ(a->via, "pinned"); + expect(fatal(a.has_value())); + expect(eq(a->version, "14.38.33130")); + expect(eq(a->toolsDir, m.vs2022.tools("14.38.33130"))); + expect(eq(a->via, "pinned")); auto b = msvc::select_system_toolset(m.all(), {}, "14.51.36014", needs); - ASSERT_TRUE(b.has_value()); - EXPECT_EQ(b->vsRoot, m.vs18.dir.root); - EXPECT_EQ(b->product, "Visual Studio Community 18 Insiders"); + expect(fatal(b.has_value())); + expect(eq(b->vsRoot, m.vs18.dir.root)); + expect(eq(b->product, "Visual Studio Community 18 Insiders")); // The highest match across every instance, by number. auto c = msvc::select_system_toolset(m.all(), {}, "14", needs); - ASSERT_TRUE(c.has_value()); - EXPECT_EQ(c->version, "14.51.36014"); -} + expect(fatal(c.has_value())); + expect(eq(c->version, "14.51.36014")); +}}; -TEST(MsvcToolsetSelect, APinWithNoInstalledMatchIsNulloptSoThePackageIsUsed) { +UTest a_pin_with_no_installed_match_is_nullopt_so_the_package_is_used{"MsvcToolsetSelect/APinWithNoInstalledMatchIsNulloptSoThePackageIsUsed", [] { TwoInstances m; - EXPECT_FALSE(msvc::select_system_toolset(m.all(), {}, "14.29", - msvc::ToolsetNeeds{}).has_value()); + expect(!(msvc::select_system_toolset(m.all(), {}, "14.29", + msvc::ToolsetNeeds{}).has_value())); // `14.4` does not reach 14.44 by text. - EXPECT_FALSE(msvc::select_system_toolset(m.all(), {}, "14.4", - msvc::ToolsetNeeds{}).has_value()); -} + expect(!(msvc::select_system_toolset(m.all(), {}, "14.4", + msvc::ToolsetNeeds{}).has_value())); +}}; -TEST(MsvcToolsetSelect, APinIgnoresTheEnvironmentAndSaysSo) { +UTest a_pin_ignores_the_environment_and_says_so{"MsvcToolsetSelect/APinIgnoresTheEnvironmentAndSaysSo", [] { // A developer prompt opened with `-vcvars_ver=14.38` exports this, and a // pinned manifest must not follow it. TwoInstances m; @@ -1087,57 +1084,57 @@ TEST(MsvcToolsetSelect, APinIgnoresTheEnvironmentAndSaysSo) { env.vsInstallDir = m.vs2022.dir.root; auto c = msvc::select_system_toolset(m.all(), env, "14.44.35207", msvc::ToolsetNeeds{}); - ASSERT_TRUE(c.has_value()); - EXPECT_EQ(c->version, "14.44.35207"); + expect(fatal(c.has_value())); + expect(eq(c->version, "14.44.35207")); bool noted = false; for (auto const& n : c->notes) noted = noted || (n.find("VCToolsInstallDir") != std::string::npos && n.find("14.38.33130") != std::string::npos); - EXPECT_TRUE(noted) << "an ignored declaration has to be reported"; -} + expect(bool(noted)) << "an ignored declaration has to be reported"; +}}; -TEST(MsvcToolsetSelect, AnIncompleteMatchIsSkippedAndReported) { +UTest an_incomplete_match_is_skipped_and_reported{"MsvcToolsetSelect/AnIncompleteMatchIsSkippedAndReported", [] { TwoInstances m; m.vs18.toolset("14.44.35210", /*complete=*/false); // newer, but no lib/x64 auto c = msvc::select_system_toolset(m.all(), {}, "14.44", msvc::ToolsetNeeds{}); - ASSERT_TRUE(c.has_value()); - EXPECT_EQ(c->version, "14.44.35207"); - ASSERT_FALSE(c->notes.empty()); - EXPECT_NE(c->notes.front().find("14.44.35210"), std::string::npos); -} + expect(fatal(c.has_value())); + expect(eq(c->version, "14.44.35207")); + expect(fatal(!(c->notes.empty()))); + expect(ne(c->notes.front().find("14.44.35210"), std::string::npos)); +}}; -TEST(MsvcToolsetSelect, SystemFollowsTheDeclarationsInOrder) { +UTest system_follows_the_declarations_in_order{"MsvcToolsetSelect/SystemFollowsTheDeclarationsInOrder", [] { TwoInstances m; msvc::ToolsetNeeds needs; msvc::MsvcEnvSnapshot byTools; byTools.vcToolsInstallDir = m.vs2022.tools("14.38.33130"); auto a = msvc::select_system_toolset(m.all(), byTools, "system", needs); - ASSERT_TRUE(a.has_value()); - EXPECT_EQ(a->version, "14.38.33130"); - EXPECT_EQ(a->via, "VCToolsInstallDir"); + expect(fatal(a.has_value())); + expect(eq(a->version, "14.38.33130")); + expect(eq(a->via, "VCToolsInstallDir")); msvc::MsvcEnvSnapshot byInstance; byInstance.vsInstallDir = m.vs2022.dir.root; auto b = msvc::select_system_toolset(m.all(), byInstance, "system", needs); - ASSERT_TRUE(b.has_value()); - EXPECT_EQ(b->version, "14.44.35207") << "the instance's default, not its highest"; - EXPECT_EQ(b->via, "VSINSTALLDIR"); + expect(fatal(b.has_value())); + expect(eq(b->version, "14.44.35207")) << "the instance's default, not its highest"; + expect(eq(b->via, "VSINSTALLDIR")); msvc::MsvcEnvSnapshot byPath; byPath.clOnPath = m.vs2022.tools("14.38.33130") / "bin" / "Hostx64" / "x64" / "cl.exe"; auto c = msvc::select_system_toolset(m.all(), byPath, "system", needs); - ASSERT_TRUE(c.has_value()); - EXPECT_EQ(c->version, "14.38.33130"); - EXPECT_EQ(c->via, "PATH"); + expect(fatal(c.has_value())); + expect(eq(c->version, "14.38.33130")); + expect(eq(c->via, "PATH")); auto d = msvc::select_system_toolset(m.all(), {}, "system", needs); - ASSERT_TRUE(d.has_value()); - EXPECT_EQ(d->version, "14.51.36014") << "the newest installation's default"; - EXPECT_EQ(d->via, "newest instance"); -} + expect(fatal(d.has_value())); + expect(eq(d->version, "14.51.36014")) << "the newest installation's default"; + expect(eq(d->via, "newest instance")); +}}; -TEST(MsvcToolsetSelect, TheDefaultFileOutranksTheHighestDirectory) { +UTest the_default_file_outranks_the_highest_directory{"MsvcToolsetSelect/TheDefaultFileOutranksTheHighestDirectory", [] { // A side-by-side toolset newer than the instance's default is what VS's // own vcvars would NOT pick; neither does this. FakeInstance vs{"sel-default", "Visual Studio Community 2022", "17.14.1"}; @@ -1145,11 +1142,11 @@ TEST(MsvcToolsetSelect, TheDefaultFileOutranksTheHighestDirectory) { vs.toolset("14.50.35717"); vs.default_is("14.44.35207"); auto c = msvc::select_system_toolset({vs.inst}, {}, "system", msvc::ToolsetNeeds{}); - ASSERT_TRUE(c.has_value()); - EXPECT_EQ(c->version, "14.44.35207"); -} + expect(fatal(c.has_value())); + expect(eq(c->version, "14.44.35207")); +}}; -TEST(MsvcToolsetSelect, TheNewestInstanceWithoutAToolsetIsPassedOver) { +UTest the_newest_instance_without_a_toolset_is_passed_over{"MsvcToolsetSelect/TheNewestInstanceWithoutAToolsetIsPassedOver", [] { // Visual Studio installed without the C++ workload: newest by installation // version, and nothing a build can use. TwoInstances m; @@ -1157,81 +1154,81 @@ TEST(MsvcToolsetSelect, TheNewestInstanceWithoutAToolsetIsPassedOver) { auto all = m.all(); all.insert(all.begin(), bare.inst); auto c = msvc::select_system_toolset(all, {}, "system", msvc::ToolsetNeeds{}); - ASSERT_TRUE(c.has_value()); - EXPECT_EQ(c->version, "14.51.36014"); -} + expect(fatal(c.has_value())); + expect(eq(c->version, "14.51.36014")); +}}; -TEST(MsvcToolsetSelect, TheClangRowDoesNotNeedClExe) { +UTest the_clang_row_does_not_need_cl_exe{"MsvcToolsetSelect/TheClangRowDoesNotNeedClExe", [] { FakeInstance vs{"sel-nocl", "Visual Studio Build Tools 2022", "17.14.1"}; vs.toolset("14.44.35207", /*complete=*/true, /*withCl=*/false); msvc::ToolsetNeeds clRow; msvc::ToolsetNeeds clangRow; clangRow.cl = false; - EXPECT_FALSE(msvc::select_system_toolset({vs.inst}, {}, "system", clRow).has_value()); + expect(!(msvc::select_system_toolset({vs.inst}, {}, "system", clRow).has_value())); auto c = msvc::select_system_toolset({vs.inst}, {}, "system", clangRow); - ASSERT_TRUE(c.has_value()); - EXPECT_EQ(c->version, "14.44.35207"); -} + expect(fatal(c.has_value())); + expect(eq(c->version, "14.44.35207")); +}}; -TEST(MsvcToolsetSelect, TheRefusalListsWhatIsInstalled) { +UTest the_refusal_lists_what_is_installed{"MsvcToolsetSelect/TheRefusalListsWhatIsInstalled", [] { TwoInstances m; auto lines = msvc::describe_system_toolsets(m.all(), msvc::ToolsetNeeds{}); - ASSERT_EQ(lines.size(), 3u); + expect(fatal(eq(lines.size(), 3u))); bool defaultMarked = false; for (auto const& l : lines) defaultMarked = defaultMarked || (l.find("14.44.35207") != std::string::npos && l.find("(default)") != std::string::npos); - EXPECT_TRUE(defaultMarked); -} + expect(bool(defaultMarked)); +}}; // ─── `xim:` names the ecosystem package ────────────────────────────────── -TEST(ToolchainSpelling, XimMsvcIsThePackageOnly) { +UTest xim_msvc_is_the_package_only{"ToolchainSpelling/XimMsvcIsThePackageOnly", [] { auto spec = parse_toolchain_spec("xim:msvc@14.44.35207"); - ASSERT_TRUE(spec.has_value()) << spec.error(); - EXPECT_EQ(spec->family, Family::Msvc); - EXPECT_EQ(spec->version, "14.44.35207"); - EXPECT_TRUE(spec->ecosystemOnly); - EXPECT_FALSE(is_system_toolchain(*spec)); + if (auto&& failed = expect(fatal(spec.has_value())); !failed) failed << spec.error(); + expect(eq(spec->family, Family::Msvc)); + expect(eq(spec->version, "14.44.35207")); + expect(bool(spec->ecosystemOnly)); + expect(!is_system_toolchain(*spec)); // The prefix changes the meaning for msvc, so it survives persistence. - EXPECT_EQ(spec->spec_str(), "xim:msvc@14.44.35207"); - EXPECT_EQ(to_xim_package(*spec).target(), "xim:msvc@14.44.35207"); + expect(eq(spec->spec_str(), "xim:msvc@14.44.35207")); + expect(eq(to_xim_package(*spec).target(), "xim:msvc@14.44.35207")); auto bare = parse_toolchain_spec("msvc@14.44.35207"); - ASSERT_TRUE(bare.has_value()); - EXPECT_FALSE(bare->ecosystemOnly); - EXPECT_EQ(bare->spec_str(), "msvc@14.44.35207"); -} + expect(fatal(bare.has_value())); + expect(!bare->ecosystemOnly); + expect(eq(bare->spec_str(), "msvc@14.44.35207")); +}}; -TEST(ToolchainSpelling, XimIsASynonymWhereThereIsNoSystemOrigin) { +UTest xim_is_a_synonym_where_there_is_no_system_origin{"ToolchainSpelling/XimIsASynonymWhereThereIsNoSystemOrigin", [] { for (auto [prefixed, bare] : {std::pair{"xim:gcc@16.1.0", "gcc@16.1.0"}, std::pair{"xim:llvm@22.1.8", "llvm@22.1.8"}}) { auto a = parse_toolchain_spec(prefixed); auto b = parse_toolchain_spec(bare); - ASSERT_TRUE(a.has_value()) << prefixed; - ASSERT_TRUE(b.has_value()) << bare; - EXPECT_EQ(a->spec_str(), b->spec_str()) << "one canonical spelling"; - EXPECT_EQ(to_xim_package(*a).target(), to_xim_package(*b).target()); + expect(fatal(a.has_value())) << prefixed; + expect(fatal(b.has_value())) << bare; + expect(eq(a->spec_str(), b->spec_str())) << "one canonical spelling"; + expect(eq(to_xim_package(*a).target(), to_xim_package(*b).target())); } -} +}}; -TEST(ToolchainSpelling, XimWithSystemIsRefusedNamingBothSpellings) { +UTest xim_with_system_is_refused_naming_both_spellings{"ToolchainSpelling/XimWithSystemIsRefusedNamingBothSpellings", [] { auto spec = parse_toolchain_spec("xim:msvc@system"); - ASSERT_FALSE(spec.has_value()); - EXPECT_NE(spec.error().find("msvc@system"), std::string::npos) << spec.error(); - EXPECT_NE(spec.error().find("xim:msvc@"), std::string::npos) << spec.error(); -} + expect(fatal(!spec.has_value())); + if (auto&& failed = expect(ne(spec.error().find("msvc@system"), std::string::npos)); !failed) failed << spec.error(); + if (auto&& failed = expect(ne(spec.error().find("xim:msvc@"), std::string::npos)); !failed) failed << spec.error(); +}}; -TEST(ToolchainSpelling, AnotherNamespaceIsRefused) { +UTest another_namespace_is_refused{"ToolchainSpelling/AnotherNamespaceIsRefused", [] { auto spec = parse_toolchain_spec("foo:gcc@16.1.0"); - ASSERT_FALSE(spec.has_value()); - EXPECT_NE(spec.error().find("xim:"), std::string::npos) << spec.error(); -} + expect(fatal(!spec.has_value())); + if (auto&& failed = expect(ne(spec.error().find("xim:"), std::string::npos)); !failed) failed << spec.error(); +}}; // vswhere `-format text`: one instance per `instanceId:` line, CRLF endings, // `displayName` and not `catalog_productDisplayVersion`, and an instance with // no installation path dropped. -TEST(MsvcVswhereText, OneInstancePerInstanceId) { +UTest one_instance_per_instance_id{"MsvcVswhereText/OneInstancePerInstanceId", [] { const std::string text = "instanceId: 1a2b\r\n" "installationName: VisualStudio/17.14.8+36301.6\r\n" @@ -1248,39 +1245,39 @@ TEST(MsvcVswhereText, OneInstancePerInstanceId) { "instanceId: 5e6f\r\n" "displayName: incomplete\r\n"; auto v = mcpp::toolchain::msvc::parse_vswhere_text(text); - ASSERT_EQ(v.size(), 2u); - EXPECT_EQ(v[0].root.string(), "C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise"); - EXPECT_EQ(v[0].installVersion, "17.14.36301.6"); - EXPECT_EQ(v[0].product, "Visual Studio Enterprise 2022"); - EXPECT_EQ(v[1].root.string(), "D:\\VS\\Preview"); - EXPECT_EQ(v[1].product, "Visual Studio Community 2026 Insiders"); - EXPECT_TRUE(mcpp::toolchain::msvc::parse_vswhere_text("").empty()); -} + expect(fatal(eq(v.size(), 2u))); + expect(eq(v[0].root.string(), "C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise")); + expect(eq(v[0].installVersion, "17.14.36301.6")); + expect(eq(v[0].product, "Visual Studio Enterprise 2022")); + expect(eq(v[1].root.string(), "D:\\VS\\Preview")); + expect(eq(v[1].product, "Visual Studio Community 2026 Insiders")); + expect(mcpp::toolchain::msvc::parse_vswhere_text("").empty()); +}}; // D16: the refusal and the first-run notice say which half of the MSVC ABI is // missing and where the SDK was looked for. The sentence they replaced said // "no Visual Studio found" on a machine whose Visual Studio was found and // whose SDK sat on another drive (design 2026-10-10 §12.1, run 37974353683). -TEST(MsvcProbe, TheDescriptionNamesTheMissingHalfAndWhereItLooked) { +UTest the_description_names_the_missing_half_and_where_it_looked{"MsvcProbe/TheDescriptionNamesTheMissingHalfAndWhereItLooked", [] { namespace msvc = mcpp::toolchain::msvc; msvc::MsvcProbe vsOnly; vsOnly.visualStudio = {"D:\\VS\\2026\\Enterprise"}; vsOnly.stl = true; vsOnly.sdkSearched = {"D:\\Kits\\10", "C:\\Program Files (x86)\\Windows Kits\\10"}; const auto a = msvc::describe_msvc_probe(vsOnly); - EXPECT_NE(a.find("Visual Studio was found at D:\\VS\\2026\\Enterprise"), std::string::npos) << a; - EXPECT_NE(a.find("no Windows SDK"), std::string::npos) << a; - EXPECT_NE(a.find("D:\\Kits\\10"), std::string::npos) << a; - EXPECT_EQ(a.find("no Visual Studio"), std::string::npos) << a; + expect(ne(a.find("Visual Studio was found at D:\\VS\\2026\\Enterprise"), std::string::npos)) << a; + expect(ne(a.find("no Windows SDK"), std::string::npos)) << a; + expect(ne(a.find("D:\\Kits\\10"), std::string::npos)) << a; + expect(eq(a.find("no Visual Studio"), std::string::npos)) << a; msvc::MsvcProbe sdkOnly; sdkOnly.sdk = msvc::WindowsSdk{"D:\\Kits\\10", "10.0.26100.0"}; const auto b = msvc::describe_msvc_probe(sdkOnly); - EXPECT_NE(b.find("10.0.26100.0"), std::string::npos) << b; - EXPECT_NE(b.find("no Visual Studio"), std::string::npos) << b; + expect(ne(b.find("10.0.26100.0"), std::string::npos)) << b; + expect(ne(b.find("no Visual Studio"), std::string::npos)) << b; msvc::MsvcProbe both = vsOnly; both.sdk = sdkOnly.sdk; - EXPECT_TRUE(both.usable()); - EXPECT_TRUE(msvc::describe_msvc_probe(both).empty()); -} + expect(bool(both.usable())); + expect(msvc::describe_msvc_probe(both).empty()); +}}; diff --git a/tests/unit/test_toolchain_probe.cpp b/tests/unit/test_toolchain_probe.cpp index 8eff0b4ba..0a2be0d91 100644 --- a/tests/unit/test_toolchain_probe.cpp +++ b/tests/unit/test_toolchain_probe.cpp @@ -16,11 +16,13 @@ // // Design: .agents/docs/2026-08-08-payload-version-and-contract-drift-design.md §3.2 -#include import std; +import mcpplibs.mtest; import mcpp.toolchain.probe; +using namespace mcpplibs::mtest; + namespace { namespace tc = mcpp::toolchain; @@ -61,50 +63,50 @@ struct TwoGlibcHome { } }; -TEST(PayloadProbe, ResolvesTheBindingExactly) { +UTest resolves_the_binding_exactly{"PayloadProbe/ResolvesTheBindingExactly", [] { TwoGlibcHome h; auto pp = tc::probe_payload_paths(h.compilerBin, "glibc@2.39"); - ASSERT_TRUE(pp.has_value()); - EXPECT_EQ(pp->glibcLib, h.libOf("2.39")) + expect(fatal(pp.has_value())); + expect(eq(pp->glibcLib, h.libOf("2.39"))) << "the authority said 2.39 and 2.44 is also installed; a resolver " "that scans would be free to return either"; -} +}}; -TEST(PayloadProbe, ResolvesTheOtherBindingExactly) { +UTest resolves_the_other_binding_exactly{"PayloadProbe/ResolvesTheOtherBindingExactly", [] { TwoGlibcHome h; auto pp = tc::probe_payload_paths(h.compilerBin, "glibc@2.44"); - ASSERT_TRUE(pp.has_value()); - EXPECT_EQ(pp->glibcLib, h.libOf("2.44")); -} + expect(fatal(pp.has_value())); + expect(eq(pp->glibcLib, h.libOf("2.44"))); +}}; // No authority is not "pick something sensible" -- it is a refusal. Guessing // here is what produced a build whose compile and run halves disagreed, and // the guess is invisible: every artifact looks normal until one links a // library built against the other version. -TEST(PayloadProbe, NoBindingIsARefusalNotAGuess) { +UTest no_binding_is_a_refusal_not_a_guess{"PayloadProbe/NoBindingIsARefusalNotAGuess", [] { TwoGlibcHome h; - EXPECT_FALSE(tc::probe_payload_paths(h.compilerBin, "").has_value()); -} + expect(!(tc::probe_payload_paths(h.compilerBin, "").has_value())); +}}; // A binding naming a version that is not installed must fail rather than fall // back to one that is. Falling back would reintroduce the split silently. -TEST(PayloadProbe, MissingVersionDoesNotFallBack) { +UTest missing_version_does_not_fall_back{"PayloadProbe/MissingVersionDoesNotFallBack", [] { TwoGlibcHome h; - EXPECT_FALSE(tc::probe_payload_paths(h.compilerBin, "glibc@2.28").has_value()); -} + expect(!(tc::probe_payload_paths(h.compilerBin, "glibc@2.28").has_value())); +}}; // The binding's NAME matters too: `musl@1.2.5` must not resolve a glibc // payload just because the version string happens to be parseable. -TEST(PayloadProbe, WrongRuntimeFamilyDoesNotResolveGlibc) { +UTest wrong_runtime_family_does_not_resolve_glibc{"PayloadProbe/WrongRuntimeFamilyDoesNotResolveGlibc", [] { TwoGlibcHome h; - EXPECT_FALSE(tc::probe_payload_paths(h.compilerBin, "musl@1.2.5").has_value()); -} + expect(!(tc::probe_payload_paths(h.compilerBin, "musl@1.2.5").has_value())); +}}; // Layout: lib64 preferred, lib accepted. Kept from the original probe -- a // payload that ships only `lib` is a real shape, and this is the one piece of // convention that stays, because it is about WHERE inside a chosen payload, // not about WHICH payload. -TEST(PayloadProbe, AcceptsLibWhenThereIsNoLib64) { +UTest accepts_lib_when_there_is_no_lib64{"PayloadProbe/AcceptsLibWhenThereIsNoLib64", [] { Tmp dir; auto xpkgs = dir.path / "data" / "xpkgs"; auto bin = xpkgs / "xim-x-gcc" / "16.1.0" / "bin" / "g++"; @@ -114,8 +116,8 @@ TEST(PayloadProbe, AcceptsLibWhenThereIsNoLib64) { touch(root / "lib" / "ld-linux-x86-64.so.2"); auto pp = tc::probe_payload_paths(bin, "glibc@2.39"); - ASSERT_TRUE(pp.has_value()); - EXPECT_EQ(pp->glibcLib, root / "lib"); -} + expect(fatal(pp.has_value())); + expect(eq(pp->glibcLib, root / "lib")); +}}; } // namespace diff --git a/tests/unit/test_toolchain_registry.cpp b/tests/unit/test_toolchain_registry.cpp index 6bfcb9dfb..07353b2ef 100644 --- a/tests/unit/test_toolchain_registry.cpp +++ b/tests/unit/test_toolchain_registry.cpp @@ -1,6 +1,7 @@ -#include import std; +import mcpplibs.mtest; +import mcpplibs.os.process; import mcpplibs.os.platform; import mcpp.toolchain.platform; import mcpp.toolchain.lifecycle; @@ -8,6 +9,8 @@ import mcpp.toolchain.model; import mcpp.toolchain.registry; import mcpp.toolchain.triple; +using namespace mcpplibs::mtest; + using namespace mcpp::toolchain; static std::string host_musl() { @@ -33,57 +36,57 @@ static std::string expected_musl_frontend(const std::string& triple) { // ── canonical two-axis identity ────────────────────────────────────────────── -TEST(ToolchainRegistry, MapsGccSpecToGccPackage) { +UTest maps_gcc_spec_to_gcc_package{"ToolchainRegistry/MapsGccSpecToGccPackage", [] { auto spec = parse_toolchain_spec("gcc@16.1.0"); - ASSERT_TRUE(spec.has_value()) << spec.error(); - EXPECT_EQ(spec->family, Family::Gcc); - EXPECT_TRUE(spec->is_host_target()); - EXPECT_EQ(spec->spec_str(), "gcc@16.1.0"); - EXPECT_EQ(spec->display(), "gcc@16.1.0"); - EXPECT_TRUE(spec->compatHint.empty()); + if (auto&& failed = expect(fatal(spec.has_value())); !failed) failed << spec.error(); + expect(eq(spec->family, Family::Gcc)); + expect(spec->is_host_target()); + expect(eq(spec->spec_str(), "gcc@16.1.0")); + expect(eq(spec->display(), "gcc@16.1.0")); + expect(spec->compatHint.empty()); auto pkg = to_xim_package(*spec); #if defined(_WIN32) // gcc family on a Windows host = MinGW-w64 (the GNU-env host toolchain). - EXPECT_EQ(pkg.ximName, "mingw-gcc"); + expect(eq(pkg.ximName, "mingw-gcc")); #else - ASSERT_FALSE(pkg.frontendCandidates.empty()); + expect(fatal(!pkg.frontendCandidates.empty())); if constexpr (mcpplibs::os::is_linux && mcpp::toolchain::platform::host_arch != std::string_view("x86_64")) { // Non-x86 Linux native GCC payloads are musl-backed even when the host // process uses glibc. Its driver needs no glibc specs fixup. - EXPECT_EQ(pkg.ximName, "musl-gcc"); - EXPECT_FALSE(pkg.needsGccPostInstallFixup); - EXPECT_EQ(pkg.frontendCandidates.front(), host_musl() + "-g++"); + expect(eq(pkg.ximName, "musl-gcc")); + expect(!pkg.needsGccPostInstallFixup); + expect(eq(pkg.frontendCandidates.front(), host_musl() + "-g++")); } else { - EXPECT_EQ(pkg.ximName, "gcc"); - EXPECT_TRUE(pkg.needsGccPostInstallFixup); - EXPECT_EQ(pkg.frontendCandidates.front(), "g++"); + expect(eq(pkg.ximName, "gcc")); + expect(bool(pkg.needsGccPostInstallFixup)); + expect(eq(pkg.frontendCandidates.front(), "g++")); } #endif - EXPECT_EQ(pkg.ximVersion, "16.1.0"); - EXPECT_EQ(pkg.display_spec(), "gcc@16.1.0"); -} + expect(eq(pkg.ximVersion, "16.1.0")); + expect(eq(pkg.display_spec(), "gcc@16.1.0")); +}}; -TEST(ToolchainRegistry, LegacyMuslSuffixNormalizesToMuslTarget) { +UTest legacy_musl_suffix_normalizes_to_musl_target{"ToolchainRegistry/LegacyMuslSuffixNormalizesToMuslTarget", [] { // "gcc@15.1.0-musl" — the variant moves out of the version and into the // target axis: (gcc, 15.1.0, -linux-musl). auto spec = parse_toolchain_spec("gcc@15.1.0-musl"); - ASSERT_TRUE(spec.has_value()) << spec.error(); - EXPECT_EQ(spec->family, Family::Gcc); - EXPECT_EQ(spec->version, "15.1.0"); - EXPECT_EQ(spec->target.str(), host_musl()); - EXPECT_FALSE(spec->compatHint.empty()); // legacy spelling → hint + if (auto&& failed = expect(fatal(spec.has_value())); !failed) failed << spec.error(); + expect(eq(spec->family, Family::Gcc)); + expect(eq(spec->version, "15.1.0")); + expect(eq(spec->target.str(), host_musl())); + expect(!(spec->compatHint.empty())); // legacy spelling → hint auto pkg = to_xim_package(*spec); - EXPECT_EQ(pkg.ximName, expected_musl_xim()); - EXPECT_EQ(pkg.ximVersion, "15.1.0"); - ASSERT_FALSE(pkg.frontendCandidates.empty()); - EXPECT_EQ(pkg.frontendCandidates.front(), expected_musl_frontend(host_musl())); - EXPECT_FALSE(pkg.needsGccPostInstallFixup); -} - -TEST(ToolchainRegistry, CrossArchMuslTargetPicksTripleNamedPackage) { + expect(eq(pkg.ximName, expected_musl_xim())); + expect(eq(pkg.ximVersion, "15.1.0")); + expect(fatal(!pkg.frontendCandidates.empty())); + expect(eq(pkg.frontendCandidates.front(), expected_musl_frontend(host_musl()))); + expect(!pkg.needsGccPostInstallFixup); +}}; + +UTest cross_arch_musl_target_picks_triple_named_package{"ToolchainRegistry/CrossArchMuslTargetPicksTripleNamedPackage", [] { // Target arch ≠ host arch → the triple-named cross package. ToolchainSpec spec; spec.family = Family::Gcc; @@ -93,113 +96,112 @@ TEST(ToolchainRegistry, CrossArchMuslTargetPicksTripleNamedPackage) { spec.target.arch = "riscv64"; // stay cross on any host auto pkg = to_xim_package(spec); - EXPECT_EQ(pkg.ximName, spec.target.str() + "-gcc"); - ASSERT_FALSE(pkg.frontendCandidates.empty()); - EXPECT_EQ(pkg.frontendCandidates.front(), expected_musl_frontend(spec.target.str())); - EXPECT_FALSE(pkg.needsGccPostInstallFixup); -} + expect(eq(pkg.ximName, spec.target.str() + "-gcc")); + expect(fatal(!pkg.frontendCandidates.empty())); + expect(eq(pkg.frontendCandidates.front(), expected_musl_frontend(spec.target.str()))); + expect(!pkg.needsGccPostInstallFixup); +}}; -TEST(ToolchainRegistry, WindowsGnuTargetIsHostSplitAtDistributionLayer) { +UTest windows_gnu_target_is_host_split_at_distribution_layer{"ToolchainRegistry/WindowsGnuTargetIsHostSplitAtDistributionLayer", [] { // ONE identity (gcc → x86_64-windows-gnu); the payload is host-split: // winlibs mingw-gcc on Windows hosts, the Linux-hosted MSVCRT cross // elsewhere. "cross" appears only in the xim package name — never in // the user-facing spec. auto spec = parse_toolchain_spec("gcc@16.1.0"); - ASSERT_TRUE(spec.has_value()); + expect(fatal(spec.has_value())); spec->target = { "x86_64", "windows", "gnu" }; auto pkg = to_xim_package(*spec); - EXPECT_EQ(pkg.ximVersion, "16.1.0"); - ASSERT_FALSE(pkg.frontendCandidates.empty()); + expect(eq(pkg.ximVersion, "16.1.0")); + expect(fatal(!pkg.frontendCandidates.empty())); #if defined(_WIN32) - EXPECT_EQ(pkg.ximName, "mingw-gcc"); - EXPECT_EQ(pkg.frontendCandidates.front(), "g++.exe"); + expect(eq(pkg.ximName, "mingw-gcc")); + expect(eq(pkg.frontendCandidates.front(), "g++.exe")); #else - EXPECT_EQ(pkg.ximName, "mingw-cross-gcc"); - EXPECT_EQ(pkg.frontendCandidates.front(), "x86_64-w64-mingw32-g++"); + expect(eq(pkg.ximName, "mingw-cross-gcc")); + expect(eq(pkg.frontendCandidates.front(), "x86_64-w64-mingw32-g++")); #endif - EXPECT_FALSE(pkg.needsGccPostInstallFixup); - EXPECT_EQ(pkg.display_spec(), "gcc@16.1.0 → x86_64-windows-gnu"); -} + expect(!pkg.needsGccPostInstallFixup); + expect(eq(pkg.display_spec(), "gcc@16.1.0 → x86_64-windows-gnu")); +}}; -TEST(ToolchainRegistry, LegacyMingwCrossSpellingCollapses) { +UTest legacy_mingw_cross_spelling_collapses{"ToolchainRegistry/LegacyMingwCrossSpellingCollapses", [] { auto spec = parse_toolchain_spec("mingw-cross@16.1.0"); - ASSERT_TRUE(spec.has_value()) << spec.error(); - EXPECT_EQ(spec->family, Family::Gcc); - EXPECT_EQ(spec->target.str(), "x86_64-windows-gnu"); - EXPECT_FALSE(spec->compatHint.empty()); -} + if (auto&& failed = expect(fatal(spec.has_value())); !failed) failed << spec.error(); + expect(eq(spec->family, Family::Gcc)); + expect(eq(spec->target.str(), "x86_64-windows-gnu")); + expect(!(spec->compatHint.empty())); +}}; -TEST(ToolchainRegistry, MapsLlvmAndClangAliasesToLlvmPackage) { +UTest maps_llvm_and_clang_aliases_to_llvm_package{"ToolchainRegistry/MapsLlvmAndClangAliasesToLlvmPackage", [] { auto llvmSpec = parse_toolchain_spec("llvm", "20.1.7"); auto clangSpec = parse_toolchain_spec("clang@20.1.7"); - ASSERT_TRUE(llvmSpec.has_value()) << llvmSpec.error(); - ASSERT_TRUE(clangSpec.has_value()) << clangSpec.error(); + if (auto&& failed = expect(fatal(llvmSpec.has_value())); !failed) failed << llvmSpec.error(); + if (auto&& failed = expect(fatal(clangSpec.has_value())); !failed) failed << clangSpec.error(); - EXPECT_EQ(llvmSpec->family, Family::Llvm); - EXPECT_EQ(clangSpec->family, Family::Llvm); // alias family → llvm - EXPECT_TRUE(llvmSpec->compatHint.empty()); - EXPECT_FALSE(clangSpec->compatHint.empty()); + expect(eq(llvmSpec->family, Family::Llvm)); + expect(eq(clangSpec->family, Family::Llvm)); // alias family → llvm + expect(llvmSpec->compatHint.empty()); + expect(!(clangSpec->compatHint.empty())); auto llvmPkg = to_xim_package(*llvmSpec); auto clangPkg = to_xim_package(*clangSpec); - EXPECT_EQ(llvmPkg.ximName, "llvm"); - EXPECT_EQ(clangPkg.ximName, "llvm"); - EXPECT_EQ(clangPkg.display_spec(), "llvm@20.1.7"); - ASSERT_FALSE(clangPkg.frontendCandidates.empty()); + expect(eq(llvmPkg.ximName, "llvm")); + expect(eq(clangPkg.ximName, "llvm")); + expect(eq(clangPkg.display_spec(), "llvm@20.1.7")); + expect(fatal(!clangPkg.frontendCandidates.empty())); #if defined(_WIN32) - EXPECT_EQ(clangPkg.frontendCandidates.front(), "clang++.exe"); + expect(eq(clangPkg.frontendCandidates.front(), "clang++.exe")); #else - EXPECT_EQ(clangPkg.frontendCandidates.front(), "clang++"); + expect(eq(clangPkg.frontendCandidates.front(), "clang++")); #endif -} +}}; -TEST(ToolchainRegistry, ResolvesPartialMuslVersion) { +UTest resolves_partial_musl_version{"ToolchainRegistry/ResolvesPartialMuslVersion", [] { auto spec = parse_toolchain_spec("gcc", "15-musl"); - ASSERT_TRUE(spec.has_value()) << spec.error(); - EXPECT_EQ(spec->version, "15"); - EXPECT_EQ(spec->target.str(), host_musl()); + if (auto&& failed = expect(fatal(spec.has_value())); !failed) failed << spec.error(); + expect(eq(spec->version, "15")); + expect(eq(spec->target.str(), host_musl())); auto resolved = with_resolved_xim_version(*spec, "15.1.0"); auto pkg = to_xim_package(resolved); - EXPECT_EQ(resolved.version, "15.1.0"); - EXPECT_EQ(pkg.ximName, expected_musl_xim()); - EXPECT_EQ(pkg.ximVersion, "15.1.0"); - EXPECT_EQ(pkg.display_spec(), - std::format("gcc@15.1.0 → {}", host_musl())); -} + expect(eq(resolved.version, "15.1.0")); + expect(eq(pkg.ximName, expected_musl_xim())); + expect(eq(pkg.ximVersion, "15.1.0")); + expect(eq(pkg.display_spec(), std::format("gcc@15.1.0 → {}", host_musl()))); +}}; -TEST(ToolchainRegistry, RejectsUnknownFamily) { +UTest rejects_unknown_family{"ToolchainRegistry/RejectsUnknownFamily", [] { auto spec = parse_toolchain_spec("tcc@1.0"); - EXPECT_FALSE(spec.has_value()); -} + expect(!spec.has_value()); +}}; // ── payload reverse mapping ────────────────────────────────────────────────── -TEST(ToolchainRegistry, IdentifiesToolchainPayloadsAndSkipsOthers) { +UTest identifies_toolchain_payloads_and_skips_others{"ToolchainRegistry/IdentifiesToolchainPayloadsAndSkipsOthers", [] { auto gcc = identify_xim_payload("gcc"); - ASSERT_TRUE(gcc.has_value()); - EXPECT_EQ(gcc->family, Family::Gcc); - EXPECT_TRUE(gcc->target.empty()); + expect(fatal(gcc.has_value())); + expect(eq(gcc->family, Family::Gcc)); + expect(gcc->target.empty()); auto musl = identify_xim_payload("musl-gcc"); - ASSERT_TRUE(musl.has_value()); - EXPECT_EQ(musl->target.str(), host_musl()); + expect(fatal(musl.has_value())); + expect(eq(musl->target.str(), host_musl())); auto crossMusl = identify_xim_payload("aarch64-linux-musl-gcc"); - ASSERT_TRUE(crossMusl.has_value()); - EXPECT_EQ(crossMusl->target.str(), "aarch64-linux-musl"); + expect(fatal(crossMusl.has_value())); + expect(eq(crossMusl->target.str(), "aarch64-linux-musl")); auto llvm = identify_xim_payload("llvm"); - ASSERT_TRUE(llvm.has_value()); - EXPECT_EQ(llvm->family, Family::Llvm); + expect(fatal(llvm.has_value())); + expect(eq(llvm->family, Family::Llvm)); // Non-toolchain xpkgs must not be identified (list/doctor filter on this). - EXPECT_FALSE(identify_xim_payload("ninja").has_value()); - EXPECT_FALSE(identify_xim_payload("glibc").has_value()); - EXPECT_FALSE(identify_xim_payload("python").has_value()); - EXPECT_FALSE(identify_xim_payload("linux-headers").has_value()); -} + expect(!(identify_xim_payload("ninja").has_value())); + expect(!(identify_xim_payload("glibc").has_value())); + expect(!(identify_xim_payload("python").has_value())); + expect(!(identify_xim_payload("linux-headers").has_value())); +}}; // #367: which GCC payload backs a NATIVE Linux build. // @@ -214,32 +216,32 @@ TEST(ToolchainRegistry, IdentifiesToolchainPayloadsAndSkipsOthers) { // Tested through a free function taking the host arch rather than the // compile-time constant, precisely so the aarch64 answer is checkable from an // x86_64 machine. -TEST(ToolchainRegistry, NativeGccPayloadFollowsWhatTheArchActuallyPublishes) { +UTest native_gcc_payload_follows_what_the_arch_actually_publishes{"ToolchainRegistry/NativeGccPayloadFollowsWhatTheArchActuallyPublishes", [] { using mcpp::toolchain::gcc_native_payload_is_musl; const mcpp::toolchain::triple::Triple none{}; // x86_64: unchanged — the glibc package is the one that exists. - EXPECT_FALSE(gcc_native_payload_is_musl("x86_64", true, none)); - EXPECT_FALSE(gcc_native_payload_is_musl( + expect(!gcc_native_payload_is_musl("x86_64", true, none)); + expect(!gcc_native_payload_is_musl( "x86_64", true, {"x86_64", "linux", "gnu"})); // aarch64 with no target: musl-gcc, the payload every version publishes. - EXPECT_TRUE(gcc_native_payload_is_musl("aarch64", true, none)); - EXPECT_TRUE(gcc_native_payload_is_musl( + expect(gcc_native_payload_is_musl("aarch64", true, none)); + expect(gcc_native_payload_is_musl( "aarch64", true, {"aarch64", "linux", "musl"})); // A target that names glibc asks for the glibc payload (xim:gcc publishes // aarch64 since 16.1.0; a Luban system's own gcc is that one). - EXPECT_FALSE(gcc_native_payload_is_musl( + expect(!gcc_native_payload_is_musl( "aarch64", true, {"aarch64", "linux", "gnu"})); // A CROSS target keeps its own payload rule — this is about the native // one, and `-gcc` packages answer for the rest. - EXPECT_FALSE(gcc_native_payload_is_musl( + expect(!gcc_native_payload_is_musl( "aarch64", true, {"x86_64", "linux", "gnu"})); // Non-Linux hosts are out of scope: macOS uses llvm, Windows mingw/msvc. - EXPECT_FALSE(gcc_native_payload_is_musl("aarch64", false, none)); -} + expect(!gcc_native_payload_is_musl("aarch64", false, none)); +}}; // ─── the origin axis ───────────────────────────────────────────────────── // @@ -261,10 +263,10 @@ TEST(ToolchainRegistry, NativeGccPayloadFollowsWhatTheArchActuallyPublishes) { // A CONSTANT HERE WOULD BE THE DEFECT THIS AVOIDS: this repository has // recorded more than once that a version written into a comment becomes a // version in a diagnostic and then in somebody's install command. -TEST(ToolchainRegistry, TheNdkApiLevelFloorIsReadFromThePayloadsOwnMetadata) { +UTest the_ndk_api_level_floor_is_read_from_the_payloads_own_metadata{"ToolchainRegistry/TheNdkApiLevelFloorIsReadFromThePayloadsOwnMetadata", [] { namespace fs = std::filesystem; auto root = fs::temp_directory_path() - / ("mcpp-ndk-meta-" + std::to_string(::getpid())); + / ("mcpp-ndk-meta-" + std::to_string(mcpplibs::os::process::self()->id)); fs::remove_all(root); // The real layout: the compiler sits four directories below the NDK root, // and `meta/` is a sibling of `toolchains/`. @@ -279,7 +281,7 @@ TEST(ToolchainRegistry, TheNdkApiLevelFloorIsReadFromThePayloadsOwnMetadata) { std::ofstream o(root / "meta" / "platforms.json"); o << R"({"min": 21, "max": 37, "aliases": {"N": 24}})"; } - EXPECT_EQ(mcpp::toolchain::ndk_min_api_level(clangxx), 21); + expect(eq(mcpp::toolchain::ndk_min_api_level(clangxx), 21)); // A DIFFERENT PAYLOAD ANSWERS DIFFERENTLY, which is the whole point of // reading it: the same code must not return 21 for an NDK that says 24. @@ -287,68 +289,66 @@ TEST(ToolchainRegistry, TheNdkApiLevelFloorIsReadFromThePayloadsOwnMetadata) { std::ofstream o(root / "meta" / "platforms.json"); o << R"({"min": 24, "max": 40})"; } - EXPECT_EQ(mcpp::toolchain::ndk_min_api_level(clangxx), 24); + expect(eq(mcpp::toolchain::ndk_min_api_level(clangxx), 24)); // 0 WHEN IT CANNOT BE READ, and the caller turns that into a refusal // naming `min_api_level`. A guessed level would be worse than the refusal: // it selects which bionic symbols exist, so guessing produces an artefact // that links here and fails to load on a device. fs::remove(root / "meta" / "platforms.json"); - EXPECT_EQ(mcpp::toolchain::ndk_min_api_level(clangxx), 0); + expect(eq(mcpp::toolchain::ndk_min_api_level(clangxx), 0)); // Malformed rather than absent -- same answer, and no exception escapes. { std::ofstream o(root / "meta" / "platforms.json"); o << "{not json"; } - EXPECT_EQ(mcpp::toolchain::ndk_min_api_level(clangxx), 0); + expect(eq(mcpp::toolchain::ndk_min_api_level(clangxx), 0)); // Present but not a number: still 0, never a silent 1 from a cast. { std::ofstream o(root / "meta" / "platforms.json"); o << R"({"min": "21"})"; } - EXPECT_EQ(mcpp::toolchain::ndk_min_api_level(clangxx), 0); + expect(eq(mcpp::toolchain::ndk_min_api_level(clangxx), 0)); // A path that is not inside an NDK at all walks to the filesystem root and // stops; it must not loop. - EXPECT_EQ(mcpp::toolchain::ndk_min_api_level( - fs::temp_directory_path() / "definitely-not-an-ndk" / "clang++"), 0); + expect(eq(mcpp::toolchain::ndk_min_api_level( + fs::temp_directory_path() / "definitely-not-an-ndk" / "clang++"), 0)); fs::remove_all(root); -} +}}; -TEST(ToolchainOrigin, MsvcIsTheOnlyFamilyWithASystemSpelling) { +UTest msvc_is_the_only_family_with_a_system_spelling{"ToolchainOrigin/MsvcIsTheOnlyFamilyWithASystemSpelling", [] { auto msvcSystem = parse_toolchain_spec("msvc@system"); - ASSERT_TRUE(msvcSystem.has_value()) << msvcSystem.error(); - EXPECT_TRUE(is_system_toolchain(*msvcSystem)); - EXPECT_EQ(origin_of(*msvcSystem), Origin::SystemMsvc); + if (auto&& failed = expect(fatal(msvcSystem.has_value())); !failed) failed << msvcSystem.error(); + expect(is_system_toolchain(*msvcSystem)); + expect(eq(origin_of(*msvcSystem), Origin::SystemMsvc)); // A VERSIONED msvc spec is the other origin — that split is the whole // point of the version axis. auto pinned = parse_toolchain_spec("msvc@14.44.35207"); - ASSERT_TRUE(pinned.has_value()); - EXPECT_EQ(origin_of(*pinned), Origin::Managed); + expect(fatal(pinned.has_value())); + expect(eq(origin_of(*pinned), Origin::Managed)); auto gcc = parse_toolchain_spec("gcc@16.1.0"); - ASSERT_TRUE(gcc.has_value()); - EXPECT_EQ(origin_of(*gcc), Origin::Managed); -} + expect(fatal(gcc.has_value())); + expect(eq(origin_of(*gcc), Origin::Managed)); +}}; -TEST(ToolchainOrigin, NonMsvcSystemIsRejectedWhereItIsReadAndOffersTheAlternatives) { +UTest non_msvc_system_is_rejected_where_it_is_read_and_offers_the_alternatives{"ToolchainOrigin/NonMsvcSystemIsRejectedWhereItIsReadAndOffersTheAlternatives", [] { // It used to parse, and then fail somewhere else entirely as // `xim:gcc@system` → "no such package" — which sends the reader looking // for a version that was never going to exist. The error has to name both // things the user might have meant. for (auto spec : {"gcc@system", "llvm@system"}) { auto r = parse_toolchain_spec(spec); - ASSERT_FALSE(r.has_value()) << spec << " was accepted"; - EXPECT_NE(r.error().find("only msvc"), std::string::npos) << r.error(); - EXPECT_NE(r.error().find("@"), std::string::npos) - << "the pin alternative is not offered: " << r.error(); + expect(fatal(!r.has_value())) << spec << " was accepted"; + if (auto&& failed = expect(ne(r.error().find("only msvc"), std::string::npos)); !failed) failed << r.error(); + if (auto&& failed = expect(ne(r.error().find("@"), std::string::npos)); !failed) failed << "the pin alternative is not offered: " << r.error(); // The family-less PATH escape hatch is the OTHER thing they might // have wanted, and it is a different mechanism. - EXPECT_NE(r.error().find("PATH compiler"), std::string::npos) + expect(ne(r.error().find("PATH compiler"), std::string::npos)) << "the escape hatch is not offered: " << r.error(); - EXPECT_EQ(r.error().find("xim:"), std::string::npos) - << "still leaking the package spelling that cannot exist: " + if (auto&& failed = expect(eq(r.error().find("xim:"), std::string::npos)); !failed) failed << "still leaking the package spelling that cannot exist: " << r.error(); } -} +}}; // ─── An SDK payload is chosen by the TARGET, and knows its own layout ────── // @@ -357,7 +357,7 @@ TEST(ToolchainOrigin, NonMsvcSystemIsRejectedWhereItIsReadAndOffersTheAlternativ // that arrive with their own clang. `em++` and the NDK's `clang++` ARE clang -- // same family, same flag vocabulary -- so no fourth `Family` value exists; // what changes is which package answers and where its driver lives. -TEST(SdkPayloads, TheTargetChoosesThePackageAndThePackageKnowsItsLayout) { +UTest the_target_chooses_the_package_and_the_package_knows_its_layout{"SdkPayloads/TheTargetChoosesThePackageAndThePackageKnowsItsLayout", [] { auto pkg_for = [](std::string_view target) { // A SPELLING THAT NAMES NO PAYLOAD, WHICH IS WHAT THIS TEST IS ABOUT. // @@ -382,34 +382,32 @@ TEST(SdkPayloads, TheTargetChoosesThePackageAndThePackageKnowsItsLayout) { // compiles for wasm and then links like an ordinary clang, producing a // module with none of Emscripten's JavaScript glue. auto pkg = pkg_for("wasm32-emscripten"); - EXPECT_EQ(pkg.ximName, "emsdk"); - EXPECT_EQ(pkg.frontendSubdir, "emscripten"); - ASSERT_FALSE(pkg.frontendCandidates.empty()); - EXPECT_EQ(pkg.frontendCandidates.front(), "em++"); + expect(eq(pkg.ximName, "emsdk")); + expect(eq(pkg.frontendSubdir, "emscripten")); + expect(fatal(!pkg.frontendCandidates.empty())); + expect(eq(pkg.frontendCandidates.front(), "em++")); } { // Android: ONE payload for both arches -- the arch arrives as // `--target=-linux-android`, not as a different package -- // and the host tuple in the path is the HOST's, not the target's. for (auto target : {"aarch64-linux-android", "x86_64-linux-android"}) { auto pkg = pkg_for(target); - EXPECT_EQ(pkg.ximName, "android-ndk") << target; - EXPECT_NE(pkg.frontendSubdir.find("toolchains/llvm/prebuilt/"), - std::string::npos) << target; + expect(eq(pkg.ximName, "android-ndk")) << target; + expect(ne(pkg.frontendSubdir.find("toolchains/llvm/prebuilt/"), std::string::npos)) << target; // The HOST, so an aarch64 Linux machine cross-compiling still // reads `linux-x86_64`. Asserted as "not the target's arch" rather // than against a literal, so this test says the same thing on // every runner. - EXPECT_EQ(pkg.frontendSubdir.find("aarch64-linux-android"), - std::string::npos) << target; + expect(eq(pkg.frontendSubdir.find("aarch64-linux-android"), std::string::npos)) << target; } } { // And every other target still gets the generic llvm payload in bin/. auto pkg = pkg_for("x86_64-linux-gnu"); - EXPECT_NE(pkg.ximName, "emsdk"); - EXPECT_NE(pkg.ximName, "android-ndk"); - EXPECT_EQ(pkg.frontendSubdir, "bin"); + expect(ne(pkg.ximName, "emsdk")); + expect(ne(pkg.ximName, "android-ndk")); + expect(eq(pkg.frontendSubdir, "bin")); } -} +}}; // THE MESSAGE MUST NAME THE DIRECTORY THAT WAS SEARCHED. // @@ -417,19 +415,19 @@ TEST(SdkPayloads, TheTargetChoosesThePackageAndThePackageKnowsItsLayout) { // themselves. Once the package decides where its frontend lives, `bin` is a // directory nothing looked in -- and this codebase's most frequent defect is a // fixed lookup with an unfixed message. -TEST(SdkPayloads, TheSearchedDirectoryIsAvailableForTheDiagnostic) { +UTest the_searched_directory_is_available_for_the_diagnostic{"SdkPayloads/TheSearchedDirectoryIsAvailableForTheDiagnostic", [] { auto spec = mcpp::toolchain::parse_toolchain_spec("emsdk@6.0.9"); - ASSERT_TRUE(spec.has_value()); + expect(fatal(spec.has_value())); auto pkg = mcpp::toolchain::to_xim_package(*spec); auto dir = mcpp::toolchain::payload_frontend_dir("/p/xim-x-emsdk/6.0.9", pkg); - EXPECT_EQ(dir, std::filesystem::path("/p/xim-x-emsdk/6.0.9/emscripten")); -} + expect(eq(dir, std::filesystem::path("/p/xim-x-emsdk/6.0.9/emscripten"))); +}}; // AN SDK IS SERVED WHERE THE SDK IS PUBLISHED, and the first version of this // said "wherever" -- the same over-broad shape as the branches it sits above. // The target matrix caught it: declaring the row servable on macOS and Windows // would have claimed a payload that does not exist there. -TEST(SdkPayloads, ServedOnEveryHostTheSdkIsPublishedFor) { +UTest served_on_every_host_the_sdk_is_published_for{"SdkPayloads/ServedOnEveryHostTheSdkIsPublishedFor", [] { // THIS ASSERTION USED TO BE TRUE BY ARITHMETIC ON ONE HOST. // // It read `EXPECT_EQ(host_can_serve(*wasm), mcpplibs::os::is_linux)`, @@ -444,8 +442,8 @@ TEST(SdkPayloads, ServedOnEveryHostTheSdkIsPublishedFor) { for (auto name : {"wasm32-emscripten", "aarch64-linux-android", "x86_64-linux-android"}) { auto t = mcpp::toolchain::triple::parse(name); - ASSERT_TRUE(t.has_value()) << name; - EXPECT_TRUE(mcpp::toolchain::host_can_serve(*t)) << name; + expect(fatal(t.has_value())) << name; + expect(mcpp::toolchain::host_can_serve(*t)) << name; } // AND THE PREDICATE IS STILL ABLE TO SAY NO, which is what keeps the @@ -454,8 +452,8 @@ TEST(SdkPayloads, ServedOnEveryHostTheSdkIsPublishedFor) { // serve them -- the exclusion this function exists to make. if constexpr (mcpplibs::os::is_linux) { auto mac = mcpp::toolchain::triple::parse("aarch64-macos"); - ASSERT_TRUE(mac.has_value()); - EXPECT_FALSE(mcpp::toolchain::host_can_serve(*mac)); + expect(fatal(mac.has_value())); + expect(!mcpp::toolchain::host_can_serve(*mac)); } // AND APPLE IS ONE ANSWER FOR BOTH OF ITS PLATFORMS, which this function @@ -476,12 +474,11 @@ TEST(SdkPayloads, ServedOnEveryHostTheSdkIsPublishedFor) { for (auto name : {"aarch64-macos", "aarch64-ios", "aarch64-ios-sim", "x86_64-ios-sim"}) { auto t = mcpp::toolchain::triple::parse(name); - ASSERT_TRUE(t.has_value()) << name; - EXPECT_TRUE(t->is_apple()) << name; - EXPECT_EQ(mcpp::toolchain::host_can_serve(*t), - bool(mcpplibs::os::is_macos)) << name; + expect(fatal(t.has_value())) << name; + expect(t->is_apple()) << name; + expect(eq(mcpp::toolchain::host_can_serve(*t), bool(mcpplibs::os::is_macos))) << name; } -} +}}; // ─── The payload is SAID, not only resolved (R3) ─────────────────────────── // @@ -491,33 +488,31 @@ TEST(SdkPayloads, ServedOnEveryHostTheSdkIsPublishedFor) { // `xim:llvm`, and not what the user typed. The family and the payload are two // questions, and `to_xim_package` already answered the second; this is the // field that lets it be printed. -TEST(SdkPayloads, TheDisplayNamesThePayloadAndNotOnlyTheFamily) { +UTest the_display_names_the_payload_and_not_only_the_family{"SdkPayloads/TheDisplayNamesThePayloadAndNotOnlyTheFamily", [] { auto em = mcpp::toolchain::parse_toolchain_spec("emsdk@6.0.9"); - ASSERT_TRUE(em.has_value()); - EXPECT_EQ(em->family, mcpp::toolchain::Family::Llvm) + expect(fatal(em.has_value())); + expect(eq(em->family, mcpp::toolchain::Family::Llvm)) << "em++ is clang; a fourth family would be a false claim"; - EXPECT_EQ(em->payloadName, "emsdk"); - EXPECT_NE(em->display().find("emsdk@6.0.9"), std::string::npos) - << em->display(); - EXPECT_EQ(em->display().find("llvm@"), std::string::npos) << em->display(); + expect(eq(em->payloadName, "emsdk")); + if (auto&& failed = expect(ne(em->display().find("emsdk@6.0.9"), std::string::npos)); !failed) failed << em->display(); + if (auto&& failed = expect(eq(em->display().find("llvm@"), std::string::npos)); !failed) failed << em->display(); auto ndk = mcpp::toolchain::parse_toolchain_spec("android-ndk@30.0.16248370"); - ASSERT_TRUE(ndk.has_value()); - EXPECT_EQ(ndk->family, mcpp::toolchain::Family::Llvm); - EXPECT_EQ(ndk->payloadName, "android-ndk"); - EXPECT_NE(ndk->display().find("android-ndk@"), std::string::npos) - << ndk->display(); + expect(fatal(ndk.has_value())); + expect(eq(ndk->family, mcpp::toolchain::Family::Llvm)); + expect(eq(ndk->payloadName, "android-ndk")); + if (auto&& failed = expect(ne(ndk->display().find("android-ndk@"), std::string::npos)); !failed) failed << ndk->display(); // AND NOTHING ELSE MOVES. Empty `payloadName` means the family's own // payload, which is every row but these two -- so no existing output // changes, which is what makes this additive. for (auto spelled : {"llvm@22.1.8", "gcc@16.1.0", "msvc@14.44.35207"}) { auto sp = mcpp::toolchain::parse_toolchain_spec(spelled); - ASSERT_TRUE(sp.has_value()) << spelled; - EXPECT_TRUE(sp->payloadName.empty()) << spelled; - EXPECT_NE(sp->display().find(spelled), std::string::npos) << sp->display(); + expect(fatal(sp.has_value())) << spelled; + expect(sp->payloadName.empty()) << spelled; + if (auto&& failed = expect(ne(sp->display().find(spelled), std::string::npos)); !failed) failed << sp->display(); } -} +}}; // ─── A spec that names a payload has answered which payload ─────────────── // @@ -538,7 +533,7 @@ TEST(SdkPayloads, TheDisplayNamesThePayloadAndNotOnlyTheFamily) { // THE DENOMINATOR IS BOTH PAYLOADS AND BOTH SPELLINGS, because the defect is // in the AXIS and not in one row: a payload named with a target, and the same // payload named without one, must answer identically. -TEST(SdkPayloads, ThePayloadTheSpecNamedDecidesWithoutATarget) { +UTest the_payload_the_spec_named_decides_without_a_target{"SdkPayloads/ThePayloadTheSpecNamedDecidesWithoutATarget", [] { struct Case { std::string_view spelling, ximName, subdir, frontend, target; }; @@ -552,14 +547,14 @@ TEST(SdkPayloads, ThePayloadTheSpecNamedDecidesWithoutATarget) { }; for (auto const& c : cases) { auto spec = mcpp::toolchain::parse_toolchain_spec(std::string(c.spelling)); - ASSERT_TRUE(spec.has_value()) << c.spelling; + expect(fatal(spec.has_value())) << c.spelling; if (!c.target.empty()) if (auto t = mcpp::toolchain::triple::parse(c.target)) spec->target = *t; auto pkg = mcpp::toolchain::to_xim_package(*spec); const std::string where = std::string(c.spelling) + " target='" + std::string(c.target) + "'"; - EXPECT_EQ(pkg.ximName, c.ximName) << where; - EXPECT_NE(pkg.frontendSubdir.find(c.subdir), std::string::npos) + expect(eq(pkg.ximName, c.ximName)) << where; + expect(ne(pkg.frontendSubdir.find(c.subdir), std::string::npos)) << where << " subdir=" << pkg.frontendSubdir; // The candidate list names the payload's own driver rather than the // family's, which is the half that sent the install looking for @@ -567,7 +562,7 @@ TEST(SdkPayloads, ThePayloadTheSpecNamedDecidesWithoutATarget) { bool named = false; for (auto const& cand : pkg.frontendCandidates) if (cand.find(c.frontend) != std::string::npos) named = true; - EXPECT_TRUE(named) << where << " candidates do not name " << c.frontend; + expect(bool(named)) << where << " candidates do not name " << c.frontend; } // AND A SPEC THAT NAMES NO PAYLOAD STILL READS THE TARGET. This is the @@ -576,11 +571,11 @@ TEST(SdkPayloads, ThePayloadTheSpecNamedDecidesWithoutATarget) { // rather than the NDK -- the capability gate is what refuses it, and a // gate cannot refuse what was silently rewritten. auto plain = mcpp::toolchain::parse_toolchain_spec("llvm@22.1.8"); - ASSERT_TRUE(plain.has_value()); - EXPECT_TRUE(plain->payloadName.empty()); + expect(fatal(plain.has_value())); + expect(plain->payloadName.empty()); if (auto t = mcpp::toolchain::triple::parse("aarch64-macos")) plain->target = *t; - EXPECT_EQ(mcpp::toolchain::to_xim_package(*plain).ximName, "llvm"); -} + expect(eq(mcpp::toolchain::to_xim_package(*plain).ximName, "llvm")); +}}; // ─── The C compiler beside a C++ one ─────────────────────────────────────── // @@ -599,7 +594,7 @@ TEST(SdkPayloads, ThePayloadTheSpecNamedDecidesWithoutATarget) { // clang's rule. `g` was already a special case for exactly this reason, which // is what makes a table the right shape: the property is "this driver names // its C compiler with a different word". -TEST(SdkPayloads, EveryFrontendNamesTheCCompilerBesideIt) { +UTest every_frontend_names_the_c_compiler_beside_it{"SdkPayloads/EveryFrontendNamesTheCCompilerBesideIt", [] { struct Case { std::string_view cxx, c; }; const Case cases[] = { // clang and its triple-prefixed forms: dropping `++` is correct. @@ -618,24 +613,23 @@ TEST(SdkPayloads, EveryFrontendNamesTheCCompilerBesideIt) { for (auto const& c : cases) { const std::filesystem::path cxx = std::filesystem::path("/p/bin") / std::string(c.cxx); - EXPECT_EQ(mcpp::toolchain::derive_c_compiler_path(cxx).filename().string(), - std::string(c.c)) << c.cxx; + expect(eq(mcpp::toolchain::derive_c_compiler_path(cxx).filename().string(), std::string(c.c))) << c.cxx; } // AND THE EXECUTABLE SUFFIX SURVIVES, which is the part a stem-based // rewrite loses: on Windows the payload ships `g++.exe`, and a C compile // spawning `gcc` with no suffix finds nothing. - EXPECT_EQ(mcpp::toolchain::derive_c_compiler_path("/p/bin/g++.exe") - .filename().string(), "gcc.exe"); - EXPECT_EQ(mcpp::toolchain::derive_c_compiler_path("/p/bin/clang++.exe") - .filename().string(), "clang.exe"); + expect(eq(mcpp::toolchain::derive_c_compiler_path("/p/bin/g++.exe") + .filename().string(), "gcc.exe")); + expect(eq(mcpp::toolchain::derive_c_compiler_path("/p/bin/clang++.exe") + .filename().string(), "clang.exe")); // `clang++` MUST NOT MATCH THE `g++` ROW. It ends in `g++`'s last two // characters plus nothing, and a suffix test without the separator would // turn it into `clanccc` or worse -- a name that is nothing. - EXPECT_EQ(mcpp::toolchain::derive_c_compiler_path("/p/bin/clang++") - .filename().string(), "clang"); -} + expect(eq(mcpp::toolchain::derive_c_compiler_path("/p/bin/clang++") + .filename().string(), "clang")); +}}; // ─── One payload, one spelling (B1) ──────────────────────────────────────── // @@ -652,25 +646,23 @@ TEST(SdkPayloads, EveryFrontendNamesTheCCompilerBesideIt) { // the one the index uses, so a single string names this payload ecosystem-wide. // // The criterion is WHERE the refusal happens, not that one happens. -TEST(SdkPayloads, TheWithdrawnAliasIsRefusedAtParseAndNotByTheCapabilityGate) { +UTest the_withdrawn_alias_is_refused_at_parse_and_not_by_the_capability_gate{"SdkPayloads/TheWithdrawnAliasIsRefusedAtParseAndNotByTheCapabilityGate", [] { auto aliased = mcpp::toolchain::parse_toolchain_spec("ndk@30.0.16248370"); - ASSERT_FALSE(aliased.has_value()) + expect(fatal(!aliased.has_value())) << "an alias that parses is an alias the capability gate must refuse " "later, by comparing spellings"; - EXPECT_NE(aliased.error().find("unknown toolchain"), std::string::npos) - << aliased.error(); - EXPECT_NE(aliased.error().find("'ndk'"), std::string::npos) - << "the refusal quotes what was typed: " << aliased.error(); + if (auto&& failed = expect(ne(aliased.error().find("unknown toolchain"), std::string::npos)); !failed) failed << aliased.error(); + if (auto&& failed = expect(ne(aliased.error().find("'ndk'"), std::string::npos)); !failed) failed << "the refusal quotes what was typed: " << aliased.error(); // AND IT SAYS WHICH SPELLING TO USE. A refusal that names the accepted // payload is one edit away from a build; one that does not sends the // reader to the documentation. - EXPECT_NE(aliased.error().find("android-ndk"), std::string::npos) + expect(ne(aliased.error().find("android-ndk"), std::string::npos)) << aliased.error(); // The kept spelling is unaffected, and it is the one the row pins. auto kept = mcpp::toolchain::parse_toolchain_spec("android-ndk@30.0.16248370"); - ASSERT_TRUE(kept.has_value()) << kept.error(); - EXPECT_EQ(kept->payloadName, "android-ndk"); + if (auto&& failed = expect(fatal(kept.has_value())); !failed) failed << kept.error(); + expect(eq(kept->payloadName, "android-ndk")); // THE MECHANISM THAT NO LONGER HAS TO BE TAUGHT. Both Android rows pin the // kept spelling, and the gate's test is `pin's name is a substring of what @@ -678,17 +670,16 @@ TEST(SdkPayloads, TheWithdrawnAliasIsRefusedAtParseAndNotByTheCapabilityGate) { // the alias gone, the gate never sees the case it would have to special. for (auto triple : {"aarch64-linux-android", "x86_64-linux-android"}) { auto parsed = mcpp::toolchain::triple::parse(triple); - ASSERT_TRUE(parsed.has_value()) << triple; + expect(fatal(parsed.has_value())) << triple; auto info = mcpp::toolchain::triple::find_known_target(*parsed); - ASSERT_NE(info, nullptr) << triple; + expect(fatal(ne(info, nullptr))) << triple; const std::string pin(info->pin); const std::string pinName = pin.substr(0, pin.find('@')); - EXPECT_EQ(pinName, "android-ndk") << triple << " pins " << pin; - EXPECT_EQ(std::string("ndk@30.0.16248370").find(pinName), - std::string::npos) + expect(eq(pinName, "android-ndk")) << triple << " pins " << pin; + expect(eq(std::string("ndk@30.0.16248370").find(pinName), std::string::npos)) << "the alias could only pass the gate by a second mechanism"; } -} +}}; // ── A required family's version comes from pins of the same payload (#641) ── // @@ -718,21 +709,21 @@ std::string pinned_row_version(std::string_view payload) { } } // namespace -TEST(RequiredFamilyPins, EachFamilyTakesThePinsOfItsOwnPayload) { +UTest each_family_takes_the_pins_of_its_own_payload{"RequiredFamilyPins/EachFamilyTakesThePinsOfItsOwnPayload", [] { for (auto payload : {"llvm", "emsdk", "android-ndk", "gcc"}) { const auto expected = pinned_row_version(payload); - ASSERT_FALSE(expected.empty()) << "no row pins " << payload; - EXPECT_EQ(highest_pin(payload), expected) << payload; + expect(fatal(!expected.empty())) << "no row pins " << payload; + expect(eq(highest_pin(payload), expected)) << payload; } -} +}}; -TEST(RequiredFamilyPins, NoLlvmPinComesFromAnotherPayload) { +UTest no_llvm_pin_comes_from_another_payload{"RequiredFamilyPins/NoLlvmPinComesFromAnotherPayload", [] { auto spec = parse_toolchain_spec("llvm"); - ASSERT_TRUE(spec.has_value()) << spec.error(); + if (auto&& failed = expect(fatal(spec.has_value())); !failed) failed << spec.error(); const auto versions = pinned_versions_for(*spec); - EXPECT_FALSE(versions.empty()); + expect(!versions.empty()); for (auto const& v : versions) { - EXPECT_NE(v, pinned_row_version("android-ndk")); - EXPECT_NE(v, pinned_row_version("emsdk")); + expect(ne(v, pinned_row_version("android-ndk"))); + expect(ne(v, pinned_row_version("emsdk"))); } -} +}}; diff --git a/tests/unit/test_toolchain_stdmod.cpp b/tests/unit/test_toolchain_stdmod.cpp index f25b7edcb..cee77ac43 100644 --- a/tests/unit/test_toolchain_stdmod.cpp +++ b/tests/unit/test_toolchain_stdmod.cpp @@ -1,10 +1,12 @@ -#include import std; +import mcpplibs.mtest; import mcpp.toolchain.clang; import mcpp.toolchain.gcc; import mcpp.toolchain.model; +using namespace mcpplibs::mtest; + using namespace mcpp::toolchain; namespace { @@ -36,26 +38,26 @@ Toolchain clang_toolchain() { } // namespace -TEST(ToolchainStdmod, GccStdModuleCommandUsesRequestedStandard) { +UTest gcc_std_module_command_uses_requested_standard{"ToolchainStdmod/GccStdModuleCommandUsesRequestedStandard", [] { auto cmd = gcc::std_module_build_command( gcc_toolchain(), "cache", "", "-std=c++26"); - EXPECT_NE(cmd.find("-std=c++26"), std::string::npos) << cmd; - EXPECT_EQ(cmd.find("-std=c++23"), std::string::npos) << cmd; -} + expect(ne(cmd.find("-std=c++26"), std::string::npos)) << cmd; + expect(eq(cmd.find("-std=c++23"), std::string::npos)) << cmd; +}}; -TEST(ToolchainStdmod, ClangStdModuleCommandsUseRequestedStandard) { +UTest clang_std_module_commands_use_requested_standard{"ToolchainStdmod/ClangStdModuleCommandsUseRequestedStandard", [] { auto cmds = clang::std_module_build_commands( clang_toolchain(), "cache", "cache/pcm.cache/std.pcm", "", "-std=c++26"); - ASSERT_EQ(cmds.size(), 2u); + expect(fatal(eq(cmds.size(), 2u))); for (auto const& cmd : cmds) { - EXPECT_NE(cmd.find("-std=c++26"), std::string::npos) << cmd; - EXPECT_EQ(cmd.find("-std=c++23"), std::string::npos) << cmd; + expect(ne(cmd.find("-std=c++26"), std::string::npos)) << cmd; + expect(eq(cmd.find("-std=c++23"), std::string::npos)) << cmd; } -} +}}; -TEST(ToolchainStdmod, ClangStdCompatCommandsUseRequestedStandard) { +UTest clang_std_compat_commands_use_requested_standard{"ToolchainStdmod/ClangStdCompatCommandsUseRequestedStandard", [] { auto cmds = clang::std_compat_build_commands( clang_toolchain(), "cache", @@ -64,33 +66,33 @@ TEST(ToolchainStdmod, ClangStdCompatCommandsUseRequestedStandard) { "", "-std=c++26"); - ASSERT_EQ(cmds.size(), 2u); + expect(fatal(eq(cmds.size(), 2u))); for (auto const& cmd : cmds) { - EXPECT_NE(cmd.find("-std=c++26"), std::string::npos) << cmd; - EXPECT_EQ(cmd.find("-std=c++23"), std::string::npos) << cmd; + expect(ne(cmd.find("-std=c++26"), std::string::npos)) << cmd; + expect(eq(cmd.find("-std=c++23"), std::string::npos)) << cmd; } -} +}}; -TEST(ToolchainStdmod, MsvcCompatSourceIsParsedAsAModuleButItsBmiIsNot) { +UTest msvc_compat_source_is_parsed_as_a_module_but_its_bmi_is_not{"ToolchainStdmod/MsvcCompatSourceIsParsedAsAModuleButItsBmiIsNot", [] { auto tc = clang_toolchain(); tc.stdModuleSource = "std.ixx"; tc.stdCompatSource = "std.compat.ixx"; auto cmds = clang::std_compat_build_commands( tc, "cache", "cache/pcm.cache/std.compat.pcm", "cache/pcm.cache/std.pcm", "", "-std=c++23", "-fms-runtime-lib=dll"); - ASSERT_EQ(cmds.size(), 2u); - EXPECT_NE(cmds[0].find("-x c++-module"), std::string::npos); - EXPECT_EQ(cmds[1].find("-x c++-module"), std::string::npos); + expect(fatal(eq(cmds.size(), 2u))); + expect(ne(cmds[0].find("-x c++-module"), std::string::npos)); + expect(eq(cmds[1].find("-x c++-module"), std::string::npos)); for (auto const& cmd : cmds) { - EXPECT_NE(cmd.find("-fmodule-file=std="), std::string::npos); - EXPECT_NE(cmd.find("-fms-runtime-lib=dll"), std::string::npos); + expect(ne(cmd.find("-fmodule-file=std="), std::string::npos)); + expect(ne(cmd.find("-fms-runtime-lib=dll"), std::string::npos)); } tc.stdCompatSource = "std.compat.cppm"; cmds = clang::std_compat_build_commands( tc, "cache", "cache/pcm.cache/std.compat.pcm", "cache/pcm.cache/std.pcm", "", "-std=c++23"); - EXPECT_EQ(cmds[0].find("-x c++-module"), std::string::npos); -} + expect(eq(cmds[0].find("-x c++-module"), std::string::npos)); +}}; // THE PRECOMPILE HAS TO KNOW WHICH MACHINE, AND ONLY ONE OF TWO SOURCES EVER // CARRIES IT. @@ -106,36 +108,35 @@ TEST(ToolchainStdmod, MsvcCompatSourceIsParsedAsAModuleButItsBmiIsNot) { // carries the target inside `stdModuleFlags`. A payload whose compiler serves // SEVERAL targets is a third kind and has neither -- one NDK clang++ compiles // for both Android ABIs and is told which by `--target` alone. -TEST(ToolchainStdmod, ThePrecompileCarriesTheMachineWhenOnlyTargetFlagsHaveIt) { +UTest the_precompile_carries_the_machine_when_only_target_flags_have_it{"ToolchainStdmod/ThePrecompileCarriesTheMachineWhenOnlyTargetFlagsHaveIt", [] { auto tc = clang_toolchain(); tc.targetTriple = "aarch64-linux-android"; // What prepare_build sets for such a row: the machine, and nothing about // include paths, because the SDK's own driver finds those. tc.stdModuleTargetFlags = " --target=aarch64-unknown-linux-android21 -D__BIONIC_CTYPE_INLINE="; - ASSERT_TRUE(tc.stdModuleFlags.empty()); + expect(fatal(tc.stdModuleFlags.empty())); auto cmds = clang::std_module_build_commands( tc, "cache", "cache/pcm.cache/std.pcm", "", "-std=c++23"); - ASSERT_EQ(cmds.size(), 2u); + expect(fatal(eq(cmds.size(), 2u))); // BOTH commands, not just the second. The precompile is the one that was // missing it and the one whose failure names a header. for (auto const& cmd : cmds) { - EXPECT_NE(cmd.find("--target=aarch64-unknown-linux-android21"), - std::string::npos) << cmd; + expect(ne(cmd.find("--target=aarch64-unknown-linux-android21"), std::string::npos)) << cmd; } // And the bionic workaround reaches the step that parses the headers. - EXPECT_NE(cmds[0].find("-D__BIONIC_CTYPE_INLINE="), std::string::npos) + expect(ne(cmds[0].find("-D__BIONIC_CTYPE_INLINE="), std::string::npos)) << cmds[0]; -} +}}; // AND IT IS NOT ADDED TWICE. `stdModuleFlags` is a SUPERSET of // `stdModuleTargetFlags` when it is set at all -- its producer builds the // machine part first and appends the include part -- so a precompile that // concatenated both would put `--target=` on the command line twice. Taking // the superset in preference is what keeps that from happening. -TEST(ToolchainStdmod, APackageProvidedModuleStillStatesTheMachineExactlyOnce) { +UTest a_package_provided_module_still_states_the_machine_exactly_once{"ToolchainStdmod/APackageProvidedModuleStillStatesTheMachineExactlyOnce", [] { auto tc = clang_toolchain(); tc.targetTriple = "aarch64-macos"; tc.stdModuleTargetFlags = " --target=arm64-apple-macos14.0"; @@ -144,7 +145,7 @@ TEST(ToolchainStdmod, APackageProvidedModuleStillStatesTheMachineExactlyOnce) { auto cmds = clang::std_module_build_commands( tc, "cache", "cache/pcm.cache/std.pcm", "", "-std=c++23"); - ASSERT_EQ(cmds.size(), 2u); + expect(fatal(eq(cmds.size(), 2u))); const auto count = [](std::string_view hay, std::string_view needle) { std::size_t n = 0, at = 0; @@ -153,10 +154,9 @@ TEST(ToolchainStdmod, APackageProvidedModuleStillStatesTheMachineExactlyOnce) { } return n; }; - EXPECT_EQ(count(cmds[0], "--target="), 1u) << cmds[0]; + if (auto&& failed = expect(eq(count(cmds[0], "--target="), 1u)); !failed) failed << cmds[0]; // The package's include path reaches the step that needs it, and only it. - EXPECT_NE(cmds[0].find("-isystem /pkg/include"), std::string::npos) + expect(ne(cmds[0].find("-isystem /pkg/include"), std::string::npos)) << cmds[0]; - EXPECT_EQ(cmds[1].find("-isystem /pkg/include"), std::string::npos) - << cmds[1]; -} + if (auto&& failed = expect(eq(cmds[1].find("-isystem /pkg/include"), std::string::npos)); !failed) failed << cmds[1]; +}}; diff --git a/tests/unit/test_toolchain_triple.cpp b/tests/unit/test_toolchain_triple.cpp index b0adb5524..aa3085868 100644 --- a/tests/unit/test_toolchain_triple.cpp +++ b/tests/unit/test_toolchain_triple.cpp @@ -1,275 +1,175 @@ -#include import std; +import mcpplibs.mtest; import mcpp.toolchain.triple; +using namespace mcpplibs::mtest; + using namespace mcpp::toolchain::triple; namespace triple = mcpp::toolchain::triple; // ── parse: canonical spellings round-trip ──────────────────────────────────── -TEST(Triple, ParsesCanonicalThreeSegment) { +UTest parses_canonical_three_segment{"Triple/ParsesCanonicalThreeSegment", [] { auto t = parse("x86_64-linux-musl"); - ASSERT_TRUE(t.has_value()); - EXPECT_EQ(t->arch, "x86_64"); - EXPECT_EQ(t->os, "linux"); - EXPECT_EQ(t->env, "musl"); - EXPECT_EQ(t->str(), "x86_64-linux-musl"); - EXPECT_TRUE(t->is_musl()); - EXPECT_TRUE(is_known_target(*t)); -} - -TEST(Triple, ParsesCanonicalWindowsGnu) { + expect(fatal(t.has_value())); + expect(eq(t->arch, "x86_64")); + expect(eq(t->os, "linux")); + expect(eq(t->env, "musl")); + expect(eq(t->str(), "x86_64-linux-musl")); + expect(t->is_musl()); + expect(is_known_target(*t)); +}}; + +UTest parses_canonical_windows_gnu{"Triple/ParsesCanonicalWindowsGnu", [] { auto t = parse("x86_64-windows-gnu"); - ASSERT_TRUE(t.has_value()); - EXPECT_EQ(t->str(), "x86_64-windows-gnu"); - EXPECT_TRUE(t->is_windows_gnu()); - EXPECT_TRUE(t->is_pe()); - EXPECT_EQ(t->family(), "windows"); - EXPECT_TRUE(is_known_target(*t)); -} - -TEST(Triple, ParsesCanonicalMacos) { + expect(fatal(t.has_value())); + expect(eq(t->str(), "x86_64-windows-gnu")); + expect(t->is_windows_gnu()); + expect(t->is_pe()); + expect(eq(t->family(), "windows")); + expect(is_known_target(*t)); +}}; + +UTest parses_canonical_macos{"Triple/ParsesCanonicalMacos", [] { auto t = parse("aarch64-macos"); - ASSERT_TRUE(t.has_value()); - EXPECT_EQ(t->arch, "aarch64"); - EXPECT_EQ(t->os, "macos"); - EXPECT_EQ(t->env, ""); - EXPECT_EQ(t->str(), "aarch64-macos"); - EXPECT_EQ(t->family(), "unix"); -} + expect(fatal(t.has_value())); + expect(eq(t->arch, "aarch64")); + expect(eq(t->os, "macos")); + expect(eq(t->env, "")); + expect(eq(t->str(), "aarch64-macos")); + expect(eq(t->family(), "unix")); +}}; // ── parse: alias spellings normalize to canonical ──────────────────────────── -TEST(Triple, NormalizesGnuMingwSpelling) { +UTest normalizes_gnu_mingw_spelling{"Triple/NormalizesGnuMingwSpelling", [] { // GNU vendor triple: "w64" = vendor, "mingw32" = os segment (historical — // 64-bit targets still say mingw32). Canonicalizes to windows-gnu. auto t = parse("x86_64-w64-mingw32"); - ASSERT_TRUE(t.has_value()); - EXPECT_EQ(t->str(), "x86_64-windows-gnu"); - EXPECT_TRUE(t->is_windows_gnu()); -} - -TEST(Triple, NormalizesFourSegmentRustSpellings) { - EXPECT_EQ(parse("x86_64-unknown-linux-musl")->str(), "x86_64-linux-musl"); - EXPECT_EQ(parse("x86_64-unknown-linux-gnu")->str(), "x86_64-linux-gnu"); - EXPECT_EQ(parse("x86_64-pc-windows-msvc")->str(), "x86_64-windows-msvc"); - EXPECT_EQ(parse("x86_64-pc-windows-gnu")->str(), "x86_64-windows-gnu"); -} - -TEST(Triple, NormalizesAppleSpellings) { + expect(fatal(t.has_value())); + expect(eq(t->str(), "x86_64-windows-gnu")); + expect(t->is_windows_gnu()); +}}; + +UTest normalizes_four_segment_rust_spellings{"Triple/NormalizesFourSegmentRustSpellings", [] { + expect(eq(parse("x86_64-unknown-linux-musl")->str(), "x86_64-linux-musl")); + expect(eq(parse("x86_64-unknown-linux-gnu")->str(), "x86_64-linux-gnu")); + expect(eq(parse("x86_64-pc-windows-msvc")->str(), "x86_64-windows-msvc")); + expect(eq(parse("x86_64-pc-windows-gnu")->str(), "x86_64-windows-gnu")); +}}; + +UTest normalizes_apple_spellings{"Triple/NormalizesAppleSpellings", [] { // arm64 → aarch64 (GNU arch spelling); darwin/macosx version suffixes drop. - EXPECT_EQ(parse("arm64-apple-darwin24.1.0")->str(), "aarch64-macos"); - EXPECT_EQ(parse("arm64-apple-macosx15.0")->str(), "aarch64-macos"); - EXPECT_EQ(parse("aarch64-apple-darwin")->str(), "aarch64-macos"); -} - -TEST(Triple, NormalizesBareLinuxToGnuEnv) { - EXPECT_EQ(parse("x86_64-linux")->str(), "x86_64-linux-gnu"); -} - -// ── resolve_request: the REQUEST is completed from the vocabulary ──────────── -// -// `parse` fills lexically so the IDENTITY stays total and host-independent (the -// test just above pins that). `resolve_request` answers the other half — which -// row a request that named no C library should resolve to — and it is the one -// that consults `kKnownTargets`. - -TEST(TripleRequest, ASupportedLexicalDefaultIsKept) { - // x86_64-linux-gnu is `verified`, so nothing moves. This is the control for - // the case below: a fix that sent every bare `-linux` to musl would pass the - // aarch64 test and break every project on the planet. - auto r = triple::resolve_request(*parse("x86_64-linux")); - EXPECT_EQ(r.triple.str(), "x86_64-linux-gnu"); - EXPECT_FALSE(r.completedFromVocabulary); - EXPECT_FALSE(r.ambiguous); -} - -TEST(TripleRequest, NativeArm64UsesTheSupportedGnuRow) { - // The supported GNU lexical default outranks the musl sibling. - auto r = triple::resolve_request(*parse("aarch64-linux")); - EXPECT_EQ(r.triple.str(), "aarch64-linux-gnu"); - EXPECT_FALSE(r.completedFromVocabulary); - // mcpp CHOOSING A ROW IS NOT THE PROJECT NAMING A C LIBRARY. `envExplicit` - // feeds the request/fact comparison and the report's display name; setting - // it here would make mcpp compare its own answer against itself. - EXPECT_FALSE(r.triple.envExplicit); -} - -TEST(TripleRequest, ABareLinuxTripleIsNeverCompletedToAndroid) { - // `aarch64-linux` asks for a C LIBRARY to be filled in. `android` shares - // the `arch-os` prefix because its kernel is Linux -- the modelling - // decision that makes every Linux-shaped answer in the tree right about it - // -- and it is not an alternative C library for the same platform. - // - // This became reachable when the Android rows stopped being `planned`: - // `aarch64-linux` then had two supported siblings and resolved as - // ambiguous, where it had completed to `aarch64-linux-musl` before. Both - // outcomes of that ambiguity are wrong -- refusing a request with an - // obvious answer, or answering it with bionic. - auto r = triple::resolve_request(*parse("aarch64-linux")); - EXPECT_EQ(r.triple.str(), "aarch64-linux-gnu"); - EXPECT_FALSE(r.completedFromVocabulary); - EXPECT_FALSE(r.ambiguous); - // Not offered as a suggestion either: `siblings` is what the diagnostic - // prints, and naming it there would suggest building for another platform. - for (auto& s : r.siblings) - EXPECT_EQ(s.find("android"), std::string::npos) << s; - - // x86_64 has the same shape and a different supported set, so it exercises - // the exclusion independently rather than re-testing one row. - auto x = triple::resolve_request(*parse("x86_64-linux")); - for (auto& s : x.siblings) - EXPECT_EQ(s.find("android"), std::string::npos) << s; - - // AND THE WRITTEN SPELLING IS STILL HONOURED. The exclusion is about - // filling a gap, not about refusing a request -- an explicit env returns - // before the candidate loop runs. - auto explicitAndroid = triple::resolve_request(*parse("aarch64-linux-android")); - EXPECT_EQ(explicitAndroid.triple.str(), "aarch64-linux-android"); - EXPECT_FALSE(explicitAndroid.completedFromVocabulary); -} - -TEST(TripleRequest, AWrittenSegmentIsARequestAndIsNotRevised) { - // An explicit environment segment is preserved independently of tier. - auto r = triple::resolve_request(*parse("aarch64-linux-gnu")); - EXPECT_EQ(r.triple.str(), "aarch64-linux-gnu"); - EXPECT_FALSE(r.completedFromVocabulary); -} - -TEST(TripleRequest, AFamilyWithNoSupportedRowKeepsTheLexicalFillAndReportsItsRows) { - // riscv64-linux-musl is `planned` and riscv64-linux-gnu does not exist at - // all, so the fill named a row outside the vocabulary and the refusal came - // out as `unknown target 'riscv64-linux'` — false, the family is registered. - auto r = triple::resolve_request(*parse("riscv64-linux")); - EXPECT_EQ(r.triple.str(), "riscv64-linux-gnu"); - EXPECT_FALSE(r.completedFromVocabulary); - ASSERT_EQ(r.siblings.size(), 1u); - EXPECT_EQ(r.siblings[0], "riscv64-linux-musl"); - EXPECT_TRUE(r.supported.empty()); -} - -TEST(TripleRequest, MacosCarriesNoEnvAndIsLeftAlone) { - EXPECT_EQ(triple::resolve_request(*parse("aarch64-macos")).triple.str(), - "aarch64-macos"); - // x86_64-macos is `planned` with no sibling: the tier gate still speaks. - auto r = triple::resolve_request(*parse("x86_64-macos")); - EXPECT_EQ(r.triple.str(), "x86_64-macos"); - EXPECT_TRUE(r.supported.empty()); -} - -TEST(TripleRequest, WindowsAndBareMetalDefaultsAreSupportedRows) { - // Both lexical fills (`gnu` on Windows, `elf` freestanding) name supported - // rows, so completion is a no-op — recorded so that a future row change - // which breaks that shows up here rather than in a user's build. - EXPECT_EQ(triple::resolve_request(*parse("x86_64-windows")).triple.str(), - "x86_64-windows-gnu"); - EXPECT_EQ(triple::resolve_request(*parse("riscv64-none")).triple.str(), - "riscv64-none-elf"); -} - -TEST(TripleRequest, EverySupportedRowIsReachableFromItsOwnSpelling) { - // The completion must never turn a written, supported triple into a - // different one. Checked across the whole vocabulary rather than by example, - // because the failure mode is one row nobody thought to test. + expect(eq(parse("arm64-apple-darwin24.1.0")->str(), "aarch64-macos")); + expect(eq(parse("arm64-apple-macosx15.0")->str(), "aarch64-macos")); + expect(eq(parse("aarch64-apple-darwin")->str(), "aarch64-macos")); +}}; + +UTest normalizes_bare_linux_to_gnu_env{"Triple/NormalizesBareLinuxToGnuEnv", [] { + expect(eq(parse("x86_64-linux")->str(), "x86_64-linux-gnu")); +}}; + +UTest every_supported_row_is_its_own_spelling{"Triple/EverySupportedRowIsItsOwnSpelling", [] { + // A written, supported triple parses to itself: checked across the whole + // vocabulary, because the failure mode is one row nobody thought to test. for (auto const& row : triple::known_targets()) { if (row.tier == "planned") continue; auto t = parse(row.canonical); - ASSERT_TRUE(t.has_value()) << row.canonical; - EXPECT_EQ(triple::resolve_request(*t).triple.str(), - std::string(row.canonical)); + expect(fatal(t.has_value())) << row.canonical; + expect(eq(t->str(), std::string(row.canonical))); } -} +}}; -TEST(Triple, NormalizesDumpmachineSpellings) { +UTest normalizes_dumpmachine_spellings{"Triple/NormalizesDumpmachineSpellings", [] { // What real toolchains report via -dumpmachine. - EXPECT_EQ(parse("x86_64-pc-linux-gnu")->str(), "x86_64-linux-gnu"); - EXPECT_EQ(parse("x86_64-linux-gnu")->str(), "x86_64-linux-gnu"); - EXPECT_EQ(parse("aarch64-linux-musl")->str(), "aarch64-linux-musl"); -} + expect(eq(parse("x86_64-pc-linux-gnu")->str(), "x86_64-linux-gnu")); + expect(eq(parse("x86_64-linux-gnu")->str(), "x86_64-linux-gnu")); + expect(eq(parse("aarch64-linux-musl")->str(), "aarch64-linux-musl")); +}}; // ── parse: rejects non-triples ─────────────────────────────────────────────── -TEST(Triple, RejectsNonTriples) { - EXPECT_FALSE(parse("").has_value()); - EXPECT_FALSE(parse("gcc").has_value()); - EXPECT_FALSE(parse("x86_64").has_value()); - EXPECT_FALSE(parse("x86_64-linux-mus").has_value()); // typo'd env segment - EXPECT_FALSE(parse("wasm32-wasi").has_value()); // outside the language -} +UTest rejects_non_triples{"Triple/RejectsNonTriples", [] { + expect(!(parse("").has_value())); + expect(!(parse("gcc").has_value())); + expect(!(parse("x86_64").has_value())); + expect(!(parse("x86_64-linux-mus").has_value())); // typo'd env segment + expect(!(parse("wasm32-wasi").has_value())); // outside the language +}}; // ── known-target vocabulary ────────────────────────────────────────────────── -TEST(Triple, KnownTargetTableExposesTierAndPins) { +UTest known_target_table_exposes_tier_and_pins{"Triple/KnownTargetTableExposesTierAndPins", [] { auto [name, tier] = std::pair{"x86_64-linux-musl", "verified"}; auto t = parse(name); - ASSERT_TRUE(t.has_value()); + expect(fatal(t.has_value())); auto* info = find_known_target(*t); - ASSERT_NE(info, nullptr); - EXPECT_EQ(info->tier, tier); - EXPECT_EQ(info->pin, "gcc@16.1.0"); - EXPECT_TRUE(info->defaultStatic); + expect(fatal(ne(info, nullptr))); + expect(eq(info->tier, tier)); + expect(eq(info->pin, "gcc@16.1.0")); + expect(bool(info->defaultStatic)); auto w = parse("x86_64-w64-mingw32"); // alias resolves to the same row - ASSERT_TRUE(w.has_value()); + expect(fatal(w.has_value())); auto* winfo = find_known_target(*w); - ASSERT_NE(winfo, nullptr); - EXPECT_EQ(winfo->canonical, "x86_64-windows-gnu"); - EXPECT_EQ(winfo->pin, "gcc@16.1.0"); - EXPECT_TRUE(winfo->defaultStatic); -} + expect(fatal(ne(winfo, nullptr))); + expect(eq(winfo->canonical, "x86_64-windows-gnu")); + expect(eq(winfo->pin, "gcc@16.1.0")); + expect(bool(winfo->defaultStatic)); +}}; -TEST(Triple, UnknownButParseableTripleIsNotKnown) { +UTest unknown_but_parseable_triple_is_not_known{"Triple/UnknownButParseableTripleIsNotKnown", [] { auto t = parse("riscv64-linux-gnu"); - ASSERT_TRUE(t.has_value()); - EXPECT_FALSE(is_known_target(*t)); -} + expect(fatal(t.has_value())); + expect(!is_known_target(*t)); +}}; // ── did-you-mean ───────────────────────────────────────────────────────────── -TEST(Triple, DidYouMeanCatchesTypos) { +UTest did_you_mean_catches_typos{"Triple/DidYouMeanCatchesTypos", [] { auto s = did_you_mean("x86_64-linux-mus"); - ASSERT_TRUE(s.has_value()); - EXPECT_EQ(*s, "x86_64-linux-musl"); + expect(fatal(s.has_value())); + expect(eq(*s, "x86_64-linux-musl")); auto w = did_you_mean("x86_64-w64-mingw"); - ASSERT_TRUE(w.has_value()); - EXPECT_EQ(*w, "x86_64-windows-gnu"); -} + expect(fatal(w.has_value())); + expect(eq(*w, "x86_64-windows-gnu")); +}}; -TEST(Triple, DidYouMeanStaysQuietOnGarbage) { - EXPECT_FALSE(did_you_mean("totally-unrelated-string-xyz").has_value()); -} +UTest did_you_mean_stays_quiet_on_garbage{"Triple/DidYouMeanStaysQuietOnGarbage", [] { + expect(!(did_you_mean("totally-unrelated-string-xyz").has_value())); +}}; // ── nasm output format ─────────────────────────────────────────────────────── -TEST(Triple, NasmFormatCoversX86Targets) { - EXPECT_EQ(parse("x86_64-linux-gnu")->nasm_format(), "elf64"); - EXPECT_EQ(parse("x86_64-linux-musl")->nasm_format(), "elf64"); - EXPECT_EQ(parse("x86_64-windows-gnu")->nasm_format(), "win64"); - EXPECT_EQ(parse("x86_64-windows-msvc")->nasm_format(), "win64"); - EXPECT_EQ(parse("x86_64-macos")->nasm_format(), "macho64"); - EXPECT_EQ(parse("i686-linux-gnu")->nasm_format(), "elf32"); - EXPECT_EQ(parse("i686-windows-gnu")->nasm_format(), "win32"); -} - -TEST(Triple, NasmFormatIsNulloptOffX86) { +UTest nasm_format_covers_x86_targets{"Triple/NasmFormatCoversX86Targets", [] { + expect(eq(parse("x86_64-linux-gnu")->nasm_format(), "elf64")); + expect(eq(parse("x86_64-linux-musl")->nasm_format(), "elf64")); + expect(eq(parse("x86_64-windows-gnu")->nasm_format(), "win64")); + expect(eq(parse("x86_64-windows-msvc")->nasm_format(), "win64")); + expect(eq(parse("x86_64-macos")->nasm_format(), "macho64")); + expect(eq(parse("i686-linux-gnu")->nasm_format(), "elf32")); + expect(eq(parse("i686-windows-gnu")->nasm_format(), "win32")); +}}; + +UTest nasm_format_is_nullopt_off_x86{"Triple/NasmFormatIsNulloptOffX86", [] { // NASM is an x86-family assembler; non-x86 targets have no format and the // caller must hard-error (with a cfg-gating hint) instead of guessing. - EXPECT_FALSE(parse("aarch64-linux-musl")->nasm_format().has_value()); - EXPECT_FALSE(parse("aarch64-macos")->nasm_format().has_value()); - EXPECT_FALSE(parse("riscv64-linux-musl")->nasm_format().has_value()); -} + expect(!(parse("aarch64-linux-musl")->nasm_format().has_value())); + expect(!(parse("aarch64-macos")->nasm_format().has_value())); + expect(!(parse("riscv64-linux-musl")->nasm_format().has_value())); +}}; // ── host ───────────────────────────────────────────────────────────────────── -TEST(Triple, HostTripleIsCanonicalAndNonEmpty) { +UTest host_triple_is_canonical_and_non_empty{"Triple/HostTripleIsCanonicalAndNonEmpty", [] { auto h = host_triple(); - EXPECT_FALSE(h.empty()); + expect(!h.empty()); auto reparsed = parse(h.str()); - ASSERT_TRUE(reparsed.has_value()); - EXPECT_EQ(reparsed->str(), h.str()); -} + expect(fatal(reparsed.has_value())); + expect(eq(reparsed->str(), h.str())); +}}; // ── bare metal: `none` is a vendor segment AND an OS segment ───────────────── // @@ -279,56 +179,56 @@ TEST(Triple, HostTripleIsCanonicalAndNonEmpty) { // would fall through to the host and the build would report success while // producing an x86-64 binary. -TEST(Triple, NoneIsTheOsWhenNoOtherOsToken) { +UTest none_is_the_os_when_no_other_os_token{"Triple/NoneIsTheOsWhenNoOtherOsToken", [] { auto t = parse("riscv64-none-elf"); - ASSERT_TRUE(t.has_value()); - EXPECT_EQ(t->arch, "riscv64"); - EXPECT_EQ(t->os, "none"); - EXPECT_EQ(t->env, "elf"); - EXPECT_EQ(t->str(), "riscv64-none-elf"); - EXPECT_TRUE(t->is_freestanding()); - EXPECT_TRUE(is_known_target(*t)); + expect(fatal(t.has_value())); + expect(eq(t->arch, "riscv64")); + expect(eq(t->os, "none")); + expect(eq(t->env, "elf")); + expect(eq(t->str(), "riscv64-none-elf")); + expect(t->is_freestanding()); + expect(is_known_target(*t)); // No OS means no cfg family — a freestanding target is neither unix nor // windows, and claiming either would enable the wrong cfg branches. - EXPECT_EQ(t->family(), ""); -} + expect(eq(t->family(), "")); +}}; -TEST(Triple, NoneStaysAVendorWhenARealOsFollows) { +UTest none_stays_a_vendor_when_a_real_os_follows{"Triple/NoneStaysAVendorWhenARealOsFollows", [] { auto t = parse("x86_64-none-linux-gnu"); - ASSERT_TRUE(t.has_value()); - EXPECT_EQ(t->os, "linux"); - EXPECT_EQ(t->env, "gnu"); - EXPECT_FALSE(t->is_freestanding()); - EXPECT_EQ(t->family(), "unix"); -} - -TEST(Triple, Riscv32BareMetalParses) { + expect(fatal(t.has_value())); + expect(eq(t->os, "linux")); + expect(eq(t->env, "gnu")); + expect(!t->is_freestanding()); + expect(eq(t->family(), "unix")); +}}; + +UTest riscv32_bare_metal_parses{"Triple/Riscv32BareMetalParses", [] { auto t = parse("riscv32-none-elf"); - ASSERT_TRUE(t.has_value()); - EXPECT_EQ(t->str(), "riscv32-none-elf"); - EXPECT_TRUE(t->is_freestanding()); - EXPECT_TRUE(is_known_target(*t)); -} + expect(fatal(t.has_value())); + expect(eq(t->str(), "riscv32-none-elf")); + expect(t->is_freestanding()); + expect(is_known_target(*t)); +}}; -TEST(Triple, HostedTargetsAreNotFreestanding) { +UTest hosted_targets_are_not_freestanding{"Triple/HostedTargetsAreNotFreestanding", [] { for (const char* s : { "x86_64-linux-gnu", "x86_64-linux-musl", "x86_64-windows-gnu", "aarch64-macos" }) { auto t = parse(s); - ASSERT_TRUE(t.has_value()) << s; - EXPECT_FALSE(t->is_freestanding()) << s; + expect(fatal(t.has_value())) << s; + expect(!t->is_freestanding()) << s; } -} +}}; -TEST(Triple, BareMetalEabiSpellings) { +UTest bare_metal_eabi_spellings{"Triple/BareMetalEabiSpellings", [] { // Accepted only under os=none, so a hosted triple cannot pick them up. auto hf = parse("arm-none-eabihf"); - ASSERT_TRUE(hf.has_value()); - EXPECT_EQ(hf->os, "none"); - EXPECT_EQ(hf->env, "eabihf"); + expect(fatal(hf.has_value())); + expect(eq(hf->os, "none")); + expect(eq(hf->env, "eabihf")); // Parsing is not the same as being supported: arm is not in the // vocabulary table yet, and the target gate is what says so. - EXPECT_FALSE(is_known_target(*hf)); -} + expect(!is_known_target(*hf)); +}}; // ── effective_sysroot: the project's override, the target row otherwise ────── // @@ -344,33 +244,32 @@ const std::string kEmpty = ""; // asked for no C library" the same value, and a kernel project would silently // get picolibc back. -TEST(Triple, EffectiveSysrootFallsBackToTheTargetRow) { +UTest effective_sysroot_falls_back_to_the_target_row{"Triple/EffectiveSysrootFallsBackToTheTargetRow", [] { auto t = parse("riscv64-none-elf"); - ASSERT_TRUE(t.has_value()); - EXPECT_EQ(effective_sysroot(*t, nullptr), "xim:picolibc-riscv@1.8.12"); -} + expect(fatal(t.has_value())); + expect(eq(effective_sysroot(*t, nullptr), "xim:picolibc-riscv@1.8.12")); +}}; -TEST(Triple, EffectiveSysrootHonoursAnOverride) { +UTest effective_sysroot_honours_an_override{"Triple/EffectiveSysrootHonoursAnOverride", [] { auto t = parse("riscv64-none-elf"); - ASSERT_TRUE(t.has_value()); - EXPECT_EQ(effective_sysroot(*t, &kNewlib), - "xim:newlib-riscv@4.4"); -} + expect(fatal(t.has_value())); + expect(eq(effective_sysroot(*t, &kNewlib), "xim:newlib-riscv@4.4")); +}}; -TEST(Triple, EffectiveSysrootEmptyStringIsTheZeroLibcTier) { +UTest effective_sysroot_empty_string_is_the_zero_libc_tier{"Triple/EffectiveSysrootEmptyStringIsTheZeroLibcTier", [] { // Present-and-empty must NOT fall through to the target row. Measured // end-to-end alongside this: with `sysroot = ""`, `#include ` // stops resolving and a self-contained image links at 108 bytes. auto t = parse("riscv64-none-elf"); - ASSERT_TRUE(t.has_value()); - EXPECT_EQ(effective_sysroot(*t, &kEmpty), ""); -} + expect(fatal(t.has_value())); + expect(eq(effective_sysroot(*t, &kEmpty), "")); +}}; -TEST(Triple, EffectiveSysrootIsEmptyForHostedTargets) { +UTest effective_sysroot_is_empty_for_hosted_targets{"Triple/EffectiveSysrootIsEmptyForHostedTargets", [] { auto t = parse("x86_64-linux-musl"); - ASSERT_TRUE(t.has_value()); - EXPECT_EQ(effective_sysroot(*t, nullptr), ""); -} + expect(fatal(t.has_value())); + expect(eq(effective_sysroot(*t, nullptr), "")); +}}; // ── llvm_triple: the spelling a compiler takes, which is not the one mcpp uses @@ -379,49 +278,49 @@ TEST(Triple, EffectiveSysrootIsEmptyForHostedTargets) { // operating system carries a version. Every one of these was a defect the // three-host matrix found rather than a test. -TEST(Triple, LlvmTripleLinuxGnu) { +UTest llvm_triple_linux_gnu{"Triple/LlvmTripleLinuxGnu", [] { auto t = parse("x86_64-linux-gnu"); - ASSERT_TRUE(t.has_value()); - EXPECT_EQ(t->llvm_triple(""), "x86_64-unknown-linux-gnu"); -} + expect(fatal(t.has_value())); + expect(eq(t->llvm_triple(""), "x86_64-unknown-linux-gnu")); +}}; -TEST(Triple, LlvmTripleLinuxMusl) { +UTest llvm_triple_linux_musl{"Triple/LlvmTripleLinuxMusl", [] { auto t = parse("aarch64-linux-musl"); - ASSERT_TRUE(t.has_value()); - EXPECT_EQ(t->llvm_triple(""), "aarch64-unknown-linux-musl"); -} + expect(fatal(t.has_value())); + expect(eq(t->llvm_triple(""), "aarch64-unknown-linux-musl")); +}}; -TEST(Triple, LlvmTripleWindowsGnu) { +UTest llvm_triple_windows_gnu{"Triple/LlvmTripleWindowsGnu", [] { auto t = parse("x86_64-windows-gnu"); - ASSERT_TRUE(t.has_value()); - EXPECT_EQ(t->llvm_triple(""), "x86_64-w64-windows-gnu"); -} + expect(fatal(t.has_value())); + expect(eq(t->llvm_triple(""), "x86_64-w64-windows-gnu")); +}}; // `aarch64` becomes `arm64` and the version is appended. A build that // emitted mcpp's own spelling produced `--target=aarch64-macos`, which clang // accepts as a triple it has never heard of and then treats as bare-metal // aarch64 — the module and its importers then agree with each other and with // nothing else. -TEST(Triple, LlvmTripleMacosRenamesArchAndCarriesVersion) { +UTest llvm_triple_macos_renames_arch_and_carries_version{"Triple/LlvmTripleMacosRenamesArchAndCarriesVersion", [] { auto t = parse("aarch64-macos"); - ASSERT_TRUE(t.has_value()); - EXPECT_EQ(t->llvm_triple("14.0"), "arm64-apple-macos14.0"); - EXPECT_EQ(t->llvm_triple("15.2"), "arm64-apple-macos15.2"); -} + expect(fatal(t.has_value())); + expect(eq(t->llvm_triple("14.0"), "arm64-apple-macos14.0")); + expect(eq(t->llvm_triple("15.2"), "arm64-apple-macos15.2")); +}}; -TEST(Triple, LlvmTripleMacosX86KeepsArchName) { +UTest llvm_triple_macos_x86_keeps_arch_name{"Triple/LlvmTripleMacosX86KeepsArchName", [] { auto t = parse("x86_64-macos"); - ASSERT_TRUE(t.has_value()); - EXPECT_EQ(t->llvm_triple("14.0"), "x86_64-apple-macos14.0"); -} + expect(fatal(t.has_value())); + expect(eq(t->llvm_triple("14.0"), "x86_64-apple-macos14.0")); +}}; // A freestanding triple is already LLVM's own form, so it is returned as // written rather than expanded into four fields. -TEST(Triple, LlvmTripleFreestandingIsUnchanged) { +UTest llvm_triple_freestanding_is_unchanged{"Triple/LlvmTripleFreestandingIsUnchanged", [] { auto t = parse("riscv64-none-elf"); - ASSERT_TRUE(t.has_value()); - EXPECT_EQ(t->llvm_triple(""), "riscv64-none-elf"); -} + expect(fatal(t.has_value())); + expect(eq(t->llvm_triple(""), "riscv64-none-elf")); +}}; // ── The object format each target has, asked of the triple rather than of the // machine running the build. @@ -432,30 +331,30 @@ TEST(Triple, LlvmTripleFreestandingIsUnchanged) { // question about the HOST, and produced opposite errors on opposite hosts: an // ELF contract for a Mach-O when built on Linux, and a Mach-O contract for an // ELF when built on macOS. -TEST(Triple, OsFieldIdentifiesMacosWithoutTheWordApple) { +UTest os_field_identifies_macos_without_the_word_apple{"Triple/OsFieldIdentifiesMacosWithoutTheWordApple", [] { auto t = parse("aarch64-macos"); - ASSERT_TRUE(t.has_value()); - EXPECT_EQ(t->os, "macos"); - EXPECT_FALSE(t->is_pe()); - EXPECT_FALSE(t->is_freestanding()); - EXPECT_EQ(t->str().find("apple"), std::string::npos); - EXPECT_EQ(t->str().find("darwin"), std::string::npos); -} - -TEST(Triple, OsFieldIdentifiesPe) { + expect(fatal(t.has_value())); + expect(eq(t->os, "macos")); + expect(!t->is_pe()); + expect(!t->is_freestanding()); + expect(eq(t->str().find("apple"), std::string::npos)); + expect(eq(t->str().find("darwin"), std::string::npos)); +}}; + +UTest os_field_identifies_pe{"Triple/OsFieldIdentifiesPe", [] { auto t = parse("x86_64-windows-gnu"); - ASSERT_TRUE(t.has_value()); - EXPECT_TRUE(t->is_pe()); - EXPECT_EQ(t->os, "windows"); -} + expect(fatal(t.has_value())); + expect(t->is_pe()); + expect(eq(t->os, "windows")); +}}; -TEST(Triple, OsFieldIdentifiesFreestanding) { +UTest os_field_identifies_freestanding{"Triple/OsFieldIdentifiesFreestanding", [] { auto t = parse("riscv64-none-elf"); - ASSERT_TRUE(t.has_value()); - EXPECT_TRUE(t->is_freestanding()); - EXPECT_EQ(t->os, "none"); - EXPECT_FALSE(t->is_pe()); -} + expect(fatal(t.has_value())); + expect(t->is_freestanding()); + expect(eq(t->os, "none")); + expect(!t->is_pe()); +}}; // ── The env segment may be declined on every platform ──────────────────────── // @@ -468,33 +367,33 @@ TEST(Triple, OsFieldIdentifiesFreestanding) { // The identity stays total: `x86_64-windows` IS `x86_64-windows-gnu`, one output // directory and one cache key. What differs is `envExplicit`, which records that // nothing was asked for. -TEST(Triple, TheEnvSegmentMayBeDeclinedOnWindows) { +UTest the_env_segment_may_be_declined_on_windows{"Triple/TheEnvSegmentMayBeDeclinedOnWindows", [] { auto t = parse("x86_64-windows"); - ASSERT_TRUE(t.has_value()); - EXPECT_EQ(t->str(), "x86_64-windows-gnu"); - EXPECT_EQ(t->env, "gnu"); - EXPECT_FALSE(t->envExplicit); -} + expect(fatal(t.has_value())); + expect(eq(t->str(), "x86_64-windows-gnu")); + expect(eq(t->env, "gnu")); + expect(!t->envExplicit); +}}; -TEST(Triple, TheEnvSegmentMayBeDeclinedOnBareMetal) { +UTest the_env_segment_may_be_declined_on_bare_metal{"Triple/TheEnvSegmentMayBeDeclinedOnBareMetal", [] { auto t = parse("riscv64-none"); - ASSERT_TRUE(t.has_value()); - EXPECT_EQ(t->str(), "riscv64-none-elf"); - EXPECT_TRUE(t->is_freestanding()); - EXPECT_FALSE(t->envExplicit); -} + expect(fatal(t.has_value())); + expect(eq(t->str(), "riscv64-none-elf")); + expect(t->is_freestanding()); + expect(!t->envExplicit); +}}; // Writing it out is still a request, and still recorded as one. -TEST(Triple, WritingTheSegmentOutIsStillARequest) { +UTest writing_the_segment_out_is_still_a_request{"Triple/WritingTheSegmentOutIsStillARequest", [] { auto g = parse("x86_64-windows-gnu"); - ASSERT_TRUE(g.has_value()); - EXPECT_TRUE(g->envExplicit); + expect(fatal(g.has_value())); + expect(bool(g->envExplicit)); auto m = parse("x86_64-windows-msvc"); - ASSERT_TRUE(m.has_value()); - EXPECT_EQ(m->env, "msvc"); - EXPECT_TRUE(m->envExplicit); -} + expect(fatal(m.has_value())); + expect(eq(m->env, "msvc")); + expect(bool(m->envExplicit)); +}}; // THE FILL IS `gnu` AND NOT THE HOST'S OWN ENV. // @@ -502,19 +401,19 @@ TEST(Triple, WritingTheSegmentOutIsStillARequest) { // give one command a different identity — a different output directory and // cache key — on each host, and a target's identity may not depend on where it // was built. The two spellings must therefore agree on every machine. -TEST(Triple, TheFillDoesNotDependOnTheHost) { - EXPECT_EQ(parse("x86_64-windows")->str(), parse("x86_64-windows-gnu")->str()); - EXPECT_EQ(parse("riscv64-none")->str(), parse("riscv64-none-elf")->str()); -} +UTest the_fill_does_not_depend_on_the_host{"Triple/TheFillDoesNotDependOnTheHost", [] { + expect(eq(parse("x86_64-windows")->str(), parse("x86_64-windows-gnu")->str())); + expect(eq(parse("riscv64-none")->str(), parse("riscv64-none-elf")->str())); +}}; // macOS declines the segment by having none at all, which was already true and // is asserted here so the four platforms are covered in one place. -TEST(Triple, MacosCarriesNoSegmentToDecline) { +UTest macos_carries_no_segment_to_decline{"Triple/MacosCarriesNoSegmentToDecline", [] { auto t = parse("aarch64-macos"); - ASSERT_TRUE(t.has_value()); - EXPECT_TRUE(t->env.empty()); - EXPECT_FALSE(t->envExplicit); -} + expect(fatal(t.has_value())); + expect(t->env.empty()); + expect(!t->envExplicit); +}}; // NO TWO ROWS MAY SHARE A CANONICAL NAME, AND THIS WAS NOT A HYPOTHETICAL. // @@ -527,26 +426,26 @@ TEST(Triple, MacosCarriesNoSegmentToDecline) { // Caught by reading the diff, which is the wrong mechanism: a duplicate is a // property of the table and a machine can see it. The cost of the check is four // lines. -TEST(Triple, TheTargetTableHasNoDuplicateNames) { +UTest the_target_table_has_no_duplicate_names{"Triple/TheTargetTableHasNoDuplicateNames", [] { std::vector seen; for (const auto& info : known_targets()) { for (auto s : seen) - EXPECT_NE(s, info.canonical) + expect(ne(s, info.canonical)) << "duplicate row in kKnownTargets: " << info.canonical; seen.push_back(info.canonical); } -} +}}; // And every row must parse to itself: a canonical name that does not survive a // round trip through `parse`/`str` is a row no `--target` can reach. -TEST(Triple, EveryTableRowIsItsOwnCanonicalForm) { +UTest every_table_row_is_its_own_canonical_form{"Triple/EveryTableRowIsItsOwnCanonicalForm", [] { for (const auto& info : known_targets()) { auto t = parse(info.canonical); - ASSERT_TRUE(t.has_value()) << info.canonical; - EXPECT_EQ(t->str(), info.canonical) + expect(fatal(t.has_value())) << info.canonical; + expect(eq(t->str(), info.canonical)) << info.canonical << " does not round-trip"; } -} +}}; // ── The object-format axis, and the three platforms it exists for ─────────── // @@ -556,111 +455,111 @@ TEST(Triple, EveryTableRowIsItsOwnCanonicalForm) { // it silently answers ELF, which is what every `else` branch in the tree // assumes. -TEST(Triple, TheObjectFormatIsOneAnswerAndNotADerivation) { - EXPECT_EQ(parse("x86_64-linux-gnu")->object_format(), ObjectFormat::Elf); - EXPECT_EQ(parse("x86_64-linux-musl")->object_format(), ObjectFormat::Elf); - EXPECT_EQ(parse("aarch64-linux-android")->object_format(), ObjectFormat::Elf); - EXPECT_EQ(parse("riscv64-none-elf")->object_format(), ObjectFormat::Elf); - EXPECT_EQ(parse("x86_64-windows-gnu")->object_format(), ObjectFormat::Pe); - EXPECT_EQ(parse("x86_64-windows-msvc")->object_format(), ObjectFormat::Pe); - EXPECT_EQ(parse("aarch64-macos")->object_format(), ObjectFormat::MachO); - EXPECT_EQ(parse("aarch64-ios")->object_format(), ObjectFormat::MachO); - EXPECT_EQ(parse("wasm32-emscripten")->object_format(), ObjectFormat::Wasm); -} - -TEST(Triple, TheFormatQuestionIsNotTheOperatingSystemQuestion) { +UTest the_object_format_is_one_answer_and_not_a_derivation{"Triple/TheObjectFormatIsOneAnswerAndNotADerivation", [] { + expect(eq(parse("x86_64-linux-gnu")->object_format(), ObjectFormat::Elf)); + expect(eq(parse("x86_64-linux-musl")->object_format(), ObjectFormat::Elf)); + expect(eq(parse("aarch64-linux-android")->object_format(), ObjectFormat::Elf)); + expect(eq(parse("riscv64-none-elf")->object_format(), ObjectFormat::Elf)); + expect(eq(parse("x86_64-windows-gnu")->object_format(), ObjectFormat::Pe)); + expect(eq(parse("x86_64-windows-msvc")->object_format(), ObjectFormat::Pe)); + expect(eq(parse("aarch64-macos")->object_format(), ObjectFormat::MachO)); + expect(eq(parse("aarch64-ios")->object_format(), ObjectFormat::MachO)); + expect(eq(parse("wasm32-emscripten")->object_format(), ObjectFormat::Wasm)); +}}; + +UTest the_format_question_is_not_the_operating_system_question{"Triple/TheFormatQuestionIsNotTheOperatingSystemQuestion", [] { // The two axes were conflated before `ObjectFormat` existed, and merging // them is the mistake it replaces: a bare-metal image is ELF with no OS, // and a wasm module has an OS-like layer and is not ELF. auto bare = parse("riscv64-none-elf"); - EXPECT_TRUE(bare->is_freestanding()); - EXPECT_EQ(bare->object_format(), ObjectFormat::Elf); + expect(bare->is_freestanding()); + expect(eq(bare->object_format(), ObjectFormat::Elf)); auto web = parse("wasm32-emscripten"); - EXPECT_FALSE(web->is_freestanding()); - EXPECT_TRUE(web->is_wasm()); -} - -TEST(Triple, IsPeAndIsMachOReadTheSingleAnswer) { - EXPECT_TRUE (parse("x86_64-windows-gnu")->is_pe()); - EXPECT_FALSE(parse("x86_64-windows-gnu")->is_mach_o()); - EXPECT_TRUE (parse("aarch64-ios")->is_mach_o()); - EXPECT_FALSE(parse("aarch64-ios")->is_pe()); + expect(!web->is_freestanding()); + expect(web->is_wasm()); +}}; + +UTest is_pe_and_is_mach_o_read_the_single_answer{"Triple/IsPeAndIsMachOReadTheSingleAnswer", [] { + expect(parse("x86_64-windows-gnu")->is_pe()); + expect(!(parse("x86_64-windows-gnu")->is_mach_o())); + expect(parse("aarch64-ios")->is_mach_o()); + expect(!(parse("aarch64-ios")->is_pe())); // The row that used to answer this by `os == "windows"` and would have // answered ELF for wasm. - EXPECT_FALSE(parse("wasm32-emscripten")->is_pe()); - EXPECT_FALSE(parse("wasm32-emscripten")->is_mach_o()); -} + expect(!(parse("wasm32-emscripten")->is_pe())); + expect(!(parse("wasm32-emscripten")->is_mach_o())); +}}; -TEST(Triple, AndroidIsAnEnvOnALinuxOs) { +UTest android_is_an_env_on_a_linux_os{"Triple/AndroidIsAnEnvOnALinuxOs", [] { auto t = parse("aarch64-linux-android"); - ASSERT_TRUE(t.has_value()); - EXPECT_EQ(t->arch, "aarch64"); + expect(fatal(t.has_value())); + expect(eq(t->arch, "aarch64")); // THE PLACEMENT IS THE MODELLING DECISION. The kernel is Linux, so ELF, // the `unix` family and `nasm -f elf64` are all already right; an // `os = "android"` would have made every one of them wrong by default. - EXPECT_EQ(t->os, "linux"); - EXPECT_EQ(t->env, "android"); - EXPECT_TRUE(t->is_android()); - EXPECT_EQ(t->family(), "unix"); - EXPECT_EQ(t->str(), "aarch64-linux-android"); - EXPECT_EQ(t->llvm_triple(), "aarch64-unknown-linux-android"); + expect(eq(t->os, "linux")); + expect(eq(t->env, "android")); + expect(t->is_android()); + expect(eq(t->family(), "unix")); + expect(eq(t->str(), "aarch64-linux-android")); + expect(eq(t->llvm_triple(), "aarch64-unknown-linux-android")); // `androideabi` is the 32-bit ARM spelling of the same env: the EABI half // is the calling convention, which the arch segment already carries. auto eabi = parse("armv7a-linux-androideabi"); - ASSERT_TRUE(eabi.has_value()); - EXPECT_EQ(eabi->env, "android"); + expect(fatal(eabi.has_value())); + expect(eq(eabi->env, "android")); // The env fill must not reach an Android request. `x86_64-linux` is still // `gnu`; `x86_64-linux-android` is not. - EXPECT_EQ(parse("x86_64-linux")->env, "gnu"); - EXPECT_EQ(parse("x86_64-linux-android")->env, "android"); -} + expect(eq(parse("x86_64-linux")->env, "gnu")); + expect(eq(parse("x86_64-linux-android")->env, "android")); +}}; -TEST(Triple, IosIsAppleWithoutBeingMacos) { +UTest ios_is_apple_without_being_macos{"Triple/IosIsAppleWithoutBeingMacos", [] { auto t = parse("aarch64-ios"); - ASSERT_TRUE(t.has_value()); - EXPECT_EQ(t->os, "ios"); - EXPECT_TRUE(t->env.empty()); - EXPECT_EQ(t->family(), "unix"); - EXPECT_EQ(t->str(), "aarch64-ios"); + expect(fatal(t.has_value())); + expect(eq(t->os, "ios")); + expect(t->env.empty()); + expect(eq(t->family(), "unix")); + expect(eq(t->str(), "aarch64-ios")); // Apple's own spelling of the architecture, as the macOS branch already // produces. No deployment target is baked in: that flag belongs to the // layer that owns the SDK, and a default here would be a second answer. - EXPECT_EQ(t->llvm_triple(), "arm64-apple-ios"); + expect(eq(t->llvm_triple(), "arm64-apple-ios")); // A SITE THAT MEANS "APPLE" AND ASKS "macOS" GETS iOS WRONG IN THE // DIRECTION THAT STILL LINKS, which is why the predicate exists. - EXPECT_TRUE(parse("aarch64-macos")->is_apple()); - EXPECT_TRUE(parse("aarch64-ios")->is_apple()); - EXPECT_FALSE(parse("aarch64-linux-musl")->is_apple()); + expect(parse("aarch64-macos")->is_apple()); + expect(parse("aarch64-ios")->is_apple()); + expect(!(parse("aarch64-linux-musl")->is_apple())); // An effective triple carries the deployment target on this segment. auto eff = parse("arm64-apple-ios17.0"); - ASSERT_TRUE(eff.has_value()); - EXPECT_EQ(eff->os, "ios"); -} + expect(fatal(eff.has_value())); + expect(eq(eff->os, "ios")); +}}; -TEST(Triple, EmscriptenIsAnOsSegmentAndNotAnEnv) { +UTest emscripten_is_an_os_segment_and_not_an_env{"Triple/EmscriptenIsAnOsSegmentAndNotAnEnv", [] { auto t = parse("wasm32-emscripten"); - ASSERT_TRUE(t.has_value()); - EXPECT_EQ(t->arch, "wasm32"); + expect(fatal(t.has_value())); + expect(eq(t->arch, "wasm32")); // It names the platform layer a module is compiled against -- the POSIX // emulation, the filesystem shim, the main loop -- which is the kind of // thing `linux` names and not the kind of thing `musl` names. - EXPECT_EQ(t->os, "emscripten"); - EXPECT_TRUE(t->env.empty()); - EXPECT_EQ(t->str(), "wasm32-emscripten"); - EXPECT_EQ(t->llvm_triple(), "wasm32-unknown-emscripten"); + expect(eq(t->os, "emscripten")); + expect(t->env.empty()); + expect(eq(t->str(), "wasm32-emscripten")); + expect(eq(t->llvm_triple(), "wasm32-unknown-emscripten")); // `unix` on the test the predicate actually applies -- what API surface a // source may assume -- rather than on a claim about wasm. - EXPECT_EQ(t->family(), "unix"); + expect(eq(t->family(), "unix")); // NASM is x86-family by construction and must decline rather than choose. - EXPECT_FALSE(t->nasm_format().has_value()); -} + expect(!(t->nasm_format().has_value())); +}}; -TEST(Triple, EachRowsTierMatchesTheEvidenceThatExistsForIt) { +UTest each_rows_tier_matches_the_evidence_that_exists_for_it{"Triple/EachRowsTierMatchesTheEvidenceThatExistsForIt", [] { // WHAT A TIER ASSERTS, AND THE THREE ANSWERS ARE DIFFERENT CLAIMS. // // verified an artefact was built AND RUN @@ -676,18 +575,18 @@ TEST(Triple, EachRowsTierMatchesTheEvidenceThatExistsForIt) { // printed `1-2-3`. Built and run, so `verified`. auto [name, tier] = std::pair{"wasm32-emscripten", "verified"}; auto t = parse(name); - ASSERT_TRUE(t.has_value()); + expect(fatal(t.has_value())); auto* info = find_known_target(*t); - ASSERT_NE(info, nullptr); - EXPECT_EQ(info->tier, tier); + expect(fatal(ne(info, nullptr))); + expect(eq(info->tier, tier)); // A ROW THAT IS WIRED NAMES ITS PAYLOAD. Without the pin the row's // tier was reachable only through an explicit // `[target.wasm32-emscripten] toolchain = "..."` override, which is // the escape hatch and not the support claim. - EXPECT_EQ(info->pin, "emsdk@6.0.9"); + expect(eq(info->pin, "emsdk@6.0.9")); // No `sysroot` column, and that is a statement: the SDK ships one, so // there is no separate C library for the row to name. - EXPECT_TRUE(info->sysroot.empty()); + expect(info->sysroot.empty()); } // BOTH ANDROID ROWS ARE NOW WIRED, AND `preview` IS THE HONEST TIER. @@ -723,14 +622,14 @@ TEST(Triple, EachRowsTierMatchesTheEvidenceThatExistsForIt) { for (auto [name, tier] : {std::pair{"aarch64-linux-android", "verified"}, std::pair{"x86_64-linux-android", "verified"}}) { auto t = parse(name); - ASSERT_TRUE(t.has_value()) << name; - EXPECT_EQ(t->str(), name); + expect(fatal(t.has_value())) << name; + expect(eq(t->str(), name)); auto* info = find_known_target(*t); - ASSERT_NE(info, nullptr) << name; - EXPECT_EQ(info->tier, tier) << name; - EXPECT_EQ(info->pin, "android-ndk@30.0.16248370") << name; + expect(fatal(ne(info, nullptr))) << name; + expect(eq(info->tier, tier)) << name; + expect(eq(info->pin, "android-ndk@30.0.16248370")) << name; // Same statement the wasm row makes: the SDK ships the sysroot. - EXPECT_TRUE(info->sysroot.empty()) << name; + expect(info->sysroot.empty()) << name; } { @@ -741,14 +640,14 @@ TEST(Triple, EachRowsTierMatchesTheEvidenceThatExistsForIt) { // ARM64 host (SPEC-009 §10.7). auto [name, tier] = std::pair{"aarch64-linux-gnu", "preview"}; auto t = parse(name); - ASSERT_TRUE(t.has_value()); + expect(fatal(t.has_value())); auto* info = find_known_target(*t); - ASSERT_NE(info, nullptr); - EXPECT_EQ(info->tier, tier); - EXPECT_EQ(info->pin, "llvm@23.1.3"); - EXPECT_TRUE(info->sysroot.empty()); - EXPECT_FALSE(info->defaultStatic); - EXPECT_FALSE(t->pin_is_capability()); + expect(fatal(ne(info, nullptr))); + expect(eq(info->tier, tier)); + expect(eq(info->pin, "llvm@23.1.3")); + expect(info->sysroot.empty()); + expect(!info->defaultStatic); + expect(!t->pin_is_capability()); } // THE THREE APPLE ROWS, AND WHAT THE SDK'S LICENCE DOES AND DOES NOT @@ -770,16 +669,16 @@ TEST(Triple, EachRowsTierMatchesTheEvidenceThatExistsForIt) { std::pair{"aarch64-ios-sim", "verified"}, std::pair{"x86_64-ios-sim", "verified"}}) { auto t = parse(name); - ASSERT_TRUE(t.has_value()) << name; - EXPECT_EQ(t->str(), name); + expect(fatal(t.has_value())) << name; + expect(eq(t->str(), name)); auto* info = find_known_target(*t); - ASSERT_NE(info, nullptr) << name; - EXPECT_EQ(info->tier, tier) << name; - EXPECT_EQ(info->pin, "llvm@23.1.3") << name; - EXPECT_TRUE(info->sysroot.empty()) << name; - EXPECT_FALSE(t->pin_is_capability()) << name; + expect(fatal(ne(info, nullptr))) << name; + expect(eq(info->tier, tier)) << name; + expect(eq(info->pin, "llvm@23.1.3")) << name; + expect(info->sysroot.empty()) << name; + expect(!t->pin_is_capability()) << name; } -} +}}; // ─── The iOS deployment target is said in exactly one place ──────────────── // @@ -796,7 +695,7 @@ TEST(Triple, EachRowsTierMatchesTheEvidenceThatExistsForIt) { // this tree would notice the version silently leaving the triple. It cannot be // an end-to-end criterion either -- the SDK gate refuses before the toolchain // is resolved, so a host without Xcode never prints the effective triple. -TEST(Triple, TheIosDeploymentTargetIsCarriedByTheEffectiveTripleAlone) { +UTest the_ios_deployment_target_is_carried_by_the_effective_triple_alone{"Triple/TheIosDeploymentTargetIsCarriedByTheEffectiveTripleAlone", [] { struct Case { std::string_view canonical, unversioned, versioned; }; const Case cases[] = { { "aarch64-ios", "arm64-apple-ios", @@ -808,22 +707,21 @@ TEST(Triple, TheIosDeploymentTargetIsCarriedByTheEffectiveTripleAlone) { }; for (auto const& c : cases) { auto t = parse(c.canonical); - ASSERT_TRUE(t.has_value()) << c.canonical; - EXPECT_EQ(t->llvm_triple("18.0"), c.versioned) << c.canonical; + expect(fatal(t.has_value())) << c.canonical; + expect(eq(t->llvm_triple("18.0"), c.versioned)) << c.canonical; // AND AN UNSTATED VERSION LEAVES THE SEGMENT ALONE rather than // inventing one. macOS bakes in a floor because its static libc++ // archives have one; iOS takes libc++ from the located SDK, so an // unstated version means the SDK's own default -- which clang supplies // for an Apple target and bionic famously does not. - EXPECT_EQ(t->llvm_triple(), c.unversioned) << c.canonical; + expect(eq(t->llvm_triple(), c.unversioned)) << c.canonical; // THE `-simulator` SUFFIX COMES AFTER THE VERSION, which is Apple's // order and not the other one. `arm64-apple-ios-simulator18.0` is not // a triple clang accepts, and the two spellings differ only in where // four characters sit. - EXPECT_TRUE(t->is_ios()) << c.canonical; - EXPECT_EQ(t->is_ios_simulator(), - c.canonical.find("-sim") != std::string_view::npos) + expect(t->is_ios()) << c.canonical; + expect(eq(t->is_ios_simulator(), c.canonical.find("-sim") != std::string_view::npos)) << c.canonical; } @@ -833,9 +731,9 @@ TEST(Triple, TheIosDeploymentTargetIsCarriedByTheEffectiveTripleAlone) { // later step refuses. auto dev = parse("aarch64-ios"); auto sim = parse("aarch64-ios-sim"); - ASSERT_TRUE(dev.has_value() && sim.has_value()); - EXPECT_NE(dev->llvm_triple("18.0"), sim->llvm_triple("18.0")); -} + expect(fatal(dev.has_value() && sim.has_value())); + expect(ne(dev->llvm_triple("18.0"), sim->llvm_triple("18.0"))); +}}; // A CAPABILITY PIN CANNOT BE OVERRIDDEN; A CONVENTION PIN CAN. // @@ -845,11 +743,11 @@ TEST(Triple, TheIosDeploymentTargetIsCarriedByTheEffectiveTripleAlone) { // and each time the refusal explained a different row: "No gcc payload emits a // PE with a musl C library", printed for a wasm target and then for an Android // one, because a fourth case fell into an `else` written as the third's answer. -TEST(Triple, ExactlyTheseRowsHaveACapabilityPin) { +UTest exactly_these_rows_have_a_capability_pin{"Triple/ExactlyTheseRowsHaveACapabilityPin", [] { std::set capability; for (auto& row : known_targets()) { auto t = parse(row.canonical); - ASSERT_TRUE(t.has_value()) << row.canonical; + expect(fatal(t.has_value())) << row.canonical; if (t->pin_is_capability()) capability.insert(std::string(row.canonical)); } // Every freestanding row, the PE+musl row, wasm, and both Android rows. @@ -858,14 +756,16 @@ TEST(Triple, ExactlyTheseRowsHaveACapabilityPin) { "riscv32-none-elf", "riscv64-none-elf", "thumbv6m-none-eabi", "thumbv7em-none-eabi", "thumbv7em-none-eabihf", "thumbv7m-none-eabi", "thumbv8m.base-none-eabi", "thumbv8m.main-none-eabi", - "thumbv8m.main-none-eabihf", "x86_64-none-elf", + "thumbv8m.main-none-eabihf", "x86_64-none-elf", "i686-none-elf", "x86_64-windows-musl", + // musl on macOS: openkal-platform's hosted part, which pairs with clang only. + "aarch64-macos-musl", "x86_64-macos-musl", "wasm32-emscripten", "aarch64-linux-android", "x86_64-linux-android", // Ok images: clang and lld emit them on every host, as for bare metal. "x86_64-openkal", "aarch64-openkal", }; - EXPECT_EQ(capability, expected); + expect(eq(capability, expected)); // ANDROID IS THE ONE THAT DOES NOT FIT THE OTHERS' REASON, and that is why // it was left out. The other entries are refused because the toolchain @@ -873,85 +773,85 @@ TEST(Triple, ExactlyTheseRowsHaveACapabilityPin) { // What it cannot supply is bionic -- headers, per-API-level stubs, loader // path -- and no package adds those to another compiler. auto android = parse("aarch64-linux-android"); - ASSERT_TRUE(android.has_value()); - EXPECT_TRUE(android->pin_is_capability()); - EXPECT_FALSE(android->is_freestanding()); - EXPECT_FALSE(android->is_wasm()); - EXPECT_FALSE(android->is_pe() && android->is_musl()); + expect(fatal(android.has_value())); + expect(android->pin_is_capability()); + expect(!android->is_freestanding()); + expect(!android->is_wasm()); + expect(!(android->is_pe() && android->is_musl())); // And a hosted row's pin stays a convention: an author who supplies the // system may name any compiler. auto musl = parse("x86_64-linux-musl"); - ASSERT_TRUE(musl.has_value()); - EXPECT_FALSE(musl->pin_is_capability()); -} + expect(fatal(musl.has_value())); + expect(!musl->pin_is_capability()); +}}; // THE SIMULATOR IS A TARGET, NOT A RUNNER, AND IT NEEDS A SPELLING. -TEST(Triple, TheSimulatorRowsCarryEnvSimAndTheirOwnEffectiveTriple) { +UTest the_simulator_rows_carry_env_sim_and_their_own_effective_triple{"Triple/TheSimulatorRowsCarryEnvSimAndTheirOwnEffectiveTriple", [] { // mcpp's three-field form. Rust spells this `aarch64-apple-ios-sim`; the // difference is the vendor segment this table elides everywhere. auto sim = parse("aarch64-ios-sim"); - ASSERT_TRUE(sim.has_value()); - EXPECT_EQ(sim->arch, "aarch64"); - EXPECT_EQ(sim->os, "ios"); - EXPECT_EQ(sim->env, "sim"); - EXPECT_EQ(sim->str(), "aarch64-ios-sim"); - EXPECT_TRUE(sim->is_apple()); - EXPECT_EQ(sim->object_format(), ObjectFormat::MachO); - EXPECT_EQ(sim->family(), "unix"); + expect(fatal(sim.has_value())); + expect(eq(sim->arch, "aarch64")); + expect(eq(sim->os, "ios")); + expect(eq(sim->env, "sim")); + expect(eq(sim->str(), "aarch64-ios-sim")); + expect(sim->is_apple()); + expect(eq(sim->object_format(), ObjectFormat::MachO)); + expect(eq(sim->family(), "unix")); // APPLE'S OWN SPELLING PARSES TO THE SAME ROW. An effective triple clang // prints carries `-simulator`, and a reader who pastes one must not be // told mcpp has never heard of it. auto apple = parse("arm64-apple-ios-simulator"); - ASSERT_TRUE(apple.has_value()); - EXPECT_EQ(apple->str(), "aarch64-ios-sim"); + expect(fatal(apple.has_value())); + expect(eq(apple->str(), "aarch64-ios-sim")); // TWO ROWS THAT MUST NOT SHARE AN IDENTITY. The device and the simulator // have different SDKs and different objects; one identity would put two // targets in one build directory. auto device = parse("aarch64-ios"); - ASSERT_TRUE(device.has_value()); - EXPECT_NE(device->str(), sim->str()); - EXPECT_TRUE(device->env.empty()); + expect(fatal(device.has_value())); + expect(ne(device->str(), sim->str())); + expect(device->env.empty()); // The effective triple differs too, and that is what the SDK selection // downstream keys off. - EXPECT_EQ(sim->llvm_triple({}), "arm64-apple-ios-simulator"); - EXPECT_EQ(device->llvm_triple({}), "arm64-apple-ios"); + expect(eq(sim->llvm_triple({}), "arm64-apple-ios-simulator")); + expect(eq(device->llvm_triple({}), "arm64-apple-ios")); // Both host arches, because the simulator runs the HOST's architecture: a // single row would describe a simulator half the machines cannot run. auto x86sim = parse("x86_64-ios-sim"); - ASSERT_TRUE(x86sim.has_value()); - EXPECT_EQ(x86sim->str(), "x86_64-ios-sim"); - EXPECT_EQ(x86sim->llvm_triple({}), "x86_64-apple-ios-simulator"); -} + expect(fatal(x86sim.has_value())); + expect(eq(x86sim->str(), "x86_64-ios-sim")); + expect(eq(x86sim->llvm_triple({}), "x86_64-apple-ios-simulator")); +}}; // THE API LEVEL IS FUSED ONTO THE ENV SEGMENT, AND IT IS NOT OPTIONAL. -TEST(Triple, AndroidFusesTheApiLevelAndBionicRequiresOne) { +UTest android_fuses_the_api_level_and_bionic_requires_one{"Triple/AndroidFusesTheApiLevelAndBionicRequiresOne", [] { auto t = parse("aarch64-linux-android"); - ASSERT_TRUE(t.has_value()); + expect(fatal(t.has_value())); // Canonical identity carries no level: one row serves every level, which // is why the level is a project decision in `[target.]` and not a // multiplication of the table. - EXPECT_EQ(t->str(), "aarch64-linux-android"); + expect(eq(t->str(), "aarch64-linux-android")); // The effective triple is where it lands. Measured: // `clang -target aarch64-linux-android21 -print-effective-triple` // answers `aarch64-unknown-linux-android21`. - EXPECT_EQ(t->llvm_triple("21"), "aarch64-unknown-linux-android21"); - EXPECT_EQ(t->llvm_triple("24"), "aarch64-unknown-linux-android24"); + expect(eq(t->llvm_triple("21"), "aarch64-unknown-linux-android21")); + expect(eq(t->llvm_triple("24"), "aarch64-unknown-linux-android24")); // WITH NO LEVEL THE FORM IS STILL PRODUCED, and that is deliberate: this // function composes, it does not decide. The refusal lives where the level // is chosen, because bionic's own stops the build -- // "Unversioned target triples are not supported!" -- and the default comes // from the NDK's `meta/platforms.json` rather than from here. - EXPECT_EQ(t->llvm_triple({}), "aarch64-unknown-linux-android"); + expect(eq(t->llvm_triple({}), "aarch64-unknown-linux-android")); // An effective triple with a level parses back to the canonical row. auto back = parse("aarch64-unknown-linux-android21"); - ASSERT_TRUE(back.has_value()); - EXPECT_EQ(back->str(), "aarch64-linux-android"); -} + expect(fatal(back.has_value())); + expect(eq(back->str(), "aarch64-linux-android")); +}}; // DOES THIS TARGET'S TOOLCHAIN ARRIVE WITH ITS OWN COMPLETE SYSTEM? // @@ -967,15 +867,15 @@ TEST(Triple, AndroidFusesTheApiLevelAndBionicRequiresOne) { // // Asserted as an exhaustive statement over the table rather than on examples, // so a new row cannot join the set by accident or be left out of it. -TEST(Triple, OnlyTheSdkTargetsShipTheirOwnSysroot) { +UTest only_the_sdk_targets_ship_their_own_sysroot{"Triple/OnlyTheSdkTargetsShipTheirOwnSysroot", [] { std::set shipsOwn; for (auto& row : known_targets()) { auto t = parse(row.canonical); - ASSERT_TRUE(t.has_value()) << row.canonical; + expect(fatal(t.has_value())) << row.canonical; if (t->has_own_sysroot()) shipsOwn.insert(std::string(row.canonical)); } - EXPECT_EQ(shipsOwn, (std::set{ - "aarch64-linux-android", "x86_64-linux-android", "wasm32-emscripten"})); + expect(eq(shipsOwn, (std::set{ + "aarch64-linux-android", "x86_64-linux-android", "wasm32-emscripten"}))); // `aarch64-ios` is NOT in the set, and that is the interesting exclusion. // The iPhoneOS SDK does ship a sysroot -- but mcpp reaches it with @@ -983,9 +883,9 @@ TEST(Triple, OnlyTheSdkTargetsShipTheirOwnSysroot) { // whether the DRIVER resolves the system without being told, and an // ordinary clang pointed at an SDK does not. auto ios = parse("aarch64-ios"); - ASSERT_TRUE(ios.has_value()); - EXPECT_FALSE(ios->has_own_sysroot()); -} + expect(fatal(ios.has_value())); + expect(!ios->has_own_sysroot()); +}}; // A CAPABILITY PIN CANNOT BE OVERRIDDEN, BECAUSE NOTHING ELSE CAN EMIT THE // TARGET. A convention pin is a preference; this is a fact about the world. @@ -1001,34 +901,34 @@ TEST(Triple, OnlyTheSdkTargetsShipTheirOwnSysroot) { // Asserted as the PAIR, because the whole design is that the two differ: if // `str()` ever carried the version, the output directory and `cfg()` would // multiply per level and the table would need a row for each. -TEST(Triple, TheMinimumPlatformVersionReachesTheEffectiveTripleAndNotTheCanonicalOne) { +UTest the_minimum_platform_version_reaches_the_effective_triple_and_not_the_canonical_one{"Triple/TheMinimumPlatformVersionReachesTheEffectiveTripleAndNotTheCanonicalOne", [] { auto droid = parse("aarch64-linux-android"); - ASSERT_TRUE(droid.has_value()); - EXPECT_EQ(droid->str(), "aarch64-linux-android"); - EXPECT_EQ(droid->llvm_triple("24"), "aarch64-unknown-linux-android24"); - EXPECT_EQ(droid->llvm_triple("21"), "aarch64-unknown-linux-android21"); + expect(fatal(droid.has_value())); + expect(eq(droid->str(), "aarch64-linux-android")); + expect(eq(droid->llvm_triple("24"), "aarch64-unknown-linux-android24")); + expect(eq(droid->llvm_triple("21"), "aarch64-unknown-linux-android21")); // Unset is legal and means the NDK's own default -- what clang normalises // when no level is given. - EXPECT_EQ(droid->llvm_triple(""), "aarch64-unknown-linux-android"); + expect(eq(droid->llvm_triple(""), "aarch64-unknown-linux-android")); // And the canonical form is unmoved by any of it. - EXPECT_EQ(droid->str(), "aarch64-linux-android"); + expect(eq(droid->str(), "aarch64-linux-android")); // macOS, the platform this parameter already served, is unchanged. auto mac = parse("aarch64-macos"); - ASSERT_TRUE(mac.has_value()); - EXPECT_EQ(mac->llvm_triple("15.2"), "arm64-apple-macos15.2"); - EXPECT_EQ(mac->str(), "aarch64-macos"); + expect(fatal(mac.has_value())); + expect(eq(mac->llvm_triple("15.2"), "arm64-apple-macos15.2")); + expect(eq(mac->str(), "aarch64-macos")); // AND NO OTHER ROW TAKES IT. One parameter serves both platforms, so the // risk is a caller handing one platform's answer to another's row -- an // ordinary Linux target must ignore it rather than fuse it. auto lin = parse("x86_64-linux-gnu"); - ASSERT_TRUE(lin.has_value()); - EXPECT_EQ(lin->llvm_triple("24"), "x86_64-unknown-linux-gnu"); + expect(fatal(lin.has_value())); + expect(eq(lin->llvm_triple("24"), "x86_64-unknown-linux-gnu")); auto musl = parse("aarch64-linux-musl"); - ASSERT_TRUE(musl.has_value()); - EXPECT_EQ(musl->llvm_triple("24"), "aarch64-unknown-linux-musl"); -} + expect(fatal(musl.has_value())); + expect(eq(musl->llvm_triple("24"), "aarch64-unknown-linux-musl")); +}}; // THE FOUR-FIELD SPELLING IS WHAT EVERY OTHER TOOLCHAIN PRINTS, so refusing it // is a cost with no design benefit. `em++ -v` passes @@ -1039,7 +939,7 @@ TEST(Triple, TheMinimumPlatformVersionReachesTheEffectiveTripleAndNotTheCanonica // information for any row in the table -- so this is a normalisation and not a // second vocabulary: `str()` returns the three-field form either way, which is // what keeps the output directory, `cfg()` and the ABI tag single-valued. -TEST(Triple, TheFourFieldSpellingParsesToTheSameCanonicalTriple) { +UTest the_four_field_spelling_parses_to_the_same_canonical_triple{"Triple/TheFourFieldSpellingParsesToTheSameCanonicalTriple", [] { struct Case { const char* spelled; const char* canonical; }; for (auto [spelled, canonical] : { Case{"wasm32-unknown-emscripten", "wasm32-emscripten"}, @@ -1050,29 +950,29 @@ TEST(Triple, TheFourFieldSpellingParsesToTheSameCanonicalTriple) { Case{"aarch64-unknown-linux-musl","aarch64-linux-musl"}, }) { auto t = parse(spelled); - ASSERT_TRUE(t.has_value()) << spelled; - EXPECT_EQ(t->str(), canonical) << spelled; + expect(fatal(t.has_value())) << spelled; + expect(eq(t->str(), canonical)) << spelled; // And the three-field form still parses to itself, so accepting the // longer spelling did not make the canonical one a second dialect. auto c = parse(canonical); - ASSERT_TRUE(c.has_value()) << canonical; - EXPECT_EQ(c->str(), canonical) << canonical; + expect(fatal(c.has_value())) << canonical; + expect(eq(c->str(), canonical)) << canonical; } -} +}}; -TEST(Triple, WasmJoinsTheCapabilityPinsBecauseNothingElseEmitsIt) { +UTest wasm_joins_the_capability_pins_because_nothing_else_emits_it{"Triple/WasmJoinsTheCapabilityPinsBecauseNothingElseEmitsIt", [] { auto wasm = parse("wasm32-emscripten"); - ASSERT_TRUE(wasm.has_value()); - EXPECT_TRUE(wasm->pin_is_capability()) + expect(fatal(wasm.has_value())); + expect(wasm->pin_is_capability()) << "a declared gcc@16.1.0 would otherwise override emsdk@6.0.9 and " "fail inside a compiler that cannot emit WebAssembly"; // The two that were there before, unchanged. - EXPECT_TRUE(parse("riscv64-none-elf")->pin_is_capability()); - EXPECT_TRUE(parse("x86_64-windows-musl")->pin_is_capability()); + expect(parse("riscv64-none-elf")->pin_is_capability()); + expect(parse("x86_64-windows-musl")->pin_is_capability()); // And an ordinary hosted row is still a convention: a project may name // whichever compiler it likes for its own Linux. - EXPECT_FALSE(parse("x86_64-linux-musl")->pin_is_capability()); + expect(!(parse("x86_64-linux-musl")->pin_is_capability())); // ANDROID IS ONE NOW, AND THIS ASSERTION PREDICTED ITS OWN EXPIRY. It // read EXPECT_FALSE, with the note "the row carries no pin yet [...] it // moves when the row does" -- the row moved, so it did. @@ -1081,16 +981,16 @@ TEST(Triple, WasmJoinsTheCapabilityPinsBecauseNothingElseEmitsIt) { // toolchain cannot emit the FORMAT; a stock clang emits aarch64 ELF // perfectly well. What it cannot supply is bionic, which lives inside the // NDK and is not packaged onto another compiler. - EXPECT_TRUE(parse("aarch64-linux-android")->pin_is_capability()); - EXPECT_TRUE(parse("x86_64-linux-android")->pin_is_capability()); + expect(parse("aarch64-linux-android")->pin_is_capability()); + expect(parse("x86_64-linux-android")->pin_is_capability()); // The iOS rows are NOT capability pins, and the distinction is worth an // assertion: they carry no pin at all, so there is nothing to override and // nothing to refuse. Their tier is what stops a build, not their pin. - EXPECT_FALSE(parse("aarch64-ios")->pin_is_capability()); - EXPECT_FALSE(parse("aarch64-ios-sim")->pin_is_capability()); -} + expect(!(parse("aarch64-ios")->pin_is_capability())); + expect(!(parse("aarch64-ios-sim")->pin_is_capability())); +}}; -TEST(Triple, TheCanonicalSpellingIsNotSEARCHABLEForAVENDORNAME) { +UTest the_canonical_spelling_is_not_searchable_for_avendorname{"Triple/TheCanonicalSpellingIsNotSEARCHABLEForAVENDORNAME", [] { // WHY A SUBSTRING TEST ON THE CANONICAL TRIPLE IS WRONG, stated as a fact // about the vocabulary rather than as a comment somewhere else. // @@ -1109,23 +1009,22 @@ TEST(Triple, TheCanonicalSpellingIsNotSEARCHABLEForAVENDORNAME) { // ^ the identity ^ what clang is given for (auto name : {"aarch64-macos", "x86_64-macos", "aarch64-ios"}) { auto t = parse(name); - ASSERT_TRUE(t.has_value()) << name; + expect(fatal(t.has_value())) << name; const std::string canonical = t->str(); - EXPECT_EQ(canonical.find("apple"), std::string::npos) << canonical; - EXPECT_EQ(canonical.find("darwin"), std::string::npos) << canonical; + expect(eq(canonical.find("apple"), std::string::npos)) << canonical; + expect(eq(canonical.find("darwin"), std::string::npos)) << canonical; // And the format is right anyway, because it is asked of the fields. - EXPECT_EQ(t->object_format(), ObjectFormat::MachO) << canonical; + expect(eq(t->object_format(), ObjectFormat::MachO)) << canonical; // The LLVM spelling is where the vendor name lives. - EXPECT_NE(t->llvm_triple().find("apple"), std::string::npos) + expect(ne(t->llvm_triple().find("apple"), std::string::npos)) << t->llvm_triple(); } // The one family the substring test got right, and only by luck: the // canonical spelling happens to carry the OS name. - EXPECT_NE(std::string(parse("x86_64-windows-gnu")->str()).find("windows"), - std::string::npos); -} + expect(ne(std::string(parse("x86_64-windows-gnu")->str()).find("windows"), std::string::npos)); +}}; -TEST(Triple, EveryKnownRowHasAnObjectFormatAndNoneFallsThrough) { +UTest every_known_row_has_an_object_format_and_none_falls_through{"Triple/EveryKnownRowHasAnObjectFormatAndNoneFallsThrough", [] { // THE DENOMINATOR IS THE TABLE. A row added without an answer here would // otherwise be covered by a test whose name says every row is -- and the // answer it would get is ELF, because ELF is what every `else` branch in @@ -1133,8 +1032,8 @@ TEST(Triple, EveryKnownRowHasAnObjectFormatAndNoneFallsThrough) { std::size_t elf = 0, macho = 0, pe = 0, wasm = 0; for (auto const& row : known_targets()) { auto t = parse(row.canonical); - ASSERT_TRUE(t.has_value()) << row.canonical; - EXPECT_EQ(t->str(), row.canonical) << "a row that is not its own canonical form"; + expect(fatal(t.has_value())) << row.canonical; + expect(eq(t->str(), row.canonical)) << "a row that is not its own canonical form"; switch (t->object_format()) { case ObjectFormat::Elf: ++elf; break; case ObjectFormat::MachO: ++macho; break; @@ -1144,38 +1043,38 @@ TEST(Triple, EveryKnownRowHasAnObjectFormatAndNoneFallsThrough) { } // Each format has at least one row, which is what makes the axis worth // having: a fourth value with no row would be an enum nothing produces. - EXPECT_GT(elf, 0u); - EXPECT_GT(macho, 0u); - EXPECT_GT(pe, 0u); - EXPECT_EQ(wasm, 1u) << "wasm32-emscripten is the only wasm row today"; - EXPECT_EQ(elf + macho + pe + wasm, known_targets().size()); -} + expect(gt(elf, 0u)); + expect(gt(macho, 0u)); + expect(gt(pe, 0u)); + expect(eq(wasm, 1u)) << "wasm32-emscripten is the only wasm row today"; + expect(eq(elf + macho + pe + wasm, known_targets().size())); +}}; // `-openkal` (design 2026-10-26 part 2, §8.5): an ok image, compiled for // no operating system, named after the convention, and neither freestanding // nor any system's. -TEST(Triple, OpenkalIsAnImageForNoOperatingSystem) { +UTest openkal_is_an_image_for_no_operating_system{"Triple/OpenkalIsAnImageForNoOperatingSystem", [] { using namespace mcpp::toolchain::triple; for (auto spelling : { "x86_64-openkal", "x86_64-unknown-openkal" }) { auto t = parse(spelling); - ASSERT_TRUE(t.has_value()) << spelling; - EXPECT_EQ(t->str(), "x86_64-openkal"); - EXPECT_TRUE(t->is_openkal()); - EXPECT_FALSE(t->is_freestanding()); - EXPECT_EQ(t->family(), ""); - EXPECT_EQ(t->llvm_triple(), "x86_64-unknown-none-elf"); - EXPECT_EQ(t->object_format(), ObjectFormat::Elf); + expect(fatal(t.has_value())) << spelling; + expect(eq(t->str(), "x86_64-openkal")); + expect(t->is_openkal()); + expect(!t->is_freestanding()); + expect(eq(t->family(), "")); + expect(eq(t->llvm_triple(), "x86_64-unknown-none-elf")); + expect(eq(t->object_format(), ObjectFormat::Elf)); } auto a = parse("aarch64-openkal"); - ASSERT_TRUE(a.has_value()); - EXPECT_EQ(a->llvm_triple(), "aarch64-unknown-none-elf"); + expect(fatal(a.has_value())); + expect(eq(a->llvm_triple(), "aarch64-unknown-none-elf")); const ArtifactNaming host{ "", "lib", ".a", ".so", false }; const auto n = artifact_naming(*a, host); - EXPECT_EQ(n.exeSuffix, ".oke"); - EXPECT_EQ(n.sharedLibExt, ".okso"); - EXPECT_EQ(n.staticLibExt, ".a"); + expect(eq(n.exeSuffix, ".oke")); + expect(eq(n.sharedLibExt, ".okso")); + expect(eq(n.staticLibExt, ".a")); const auto* row = find_known_target(*a); - ASSERT_NE(row, nullptr); - EXPECT_EQ(row->pin, "llvm@23.1.3"); - EXPECT_EQ(platform_name(*a), "openkal"); -} + expect(fatal(ne(row, nullptr))); + expect(eq(row->pin, "llvm@23.1.3")); + expect(eq(platform_name(*a), "openkal")); +}}; diff --git a/tests/unit/test_ui_closing_notices.cpp b/tests/unit/test_ui_closing_notices.cpp index 2783d2e5d..5b4720ba7 100644 --- a/tests/unit/test_ui_closing_notices.cpp +++ b/tests/unit/test_ui_closing_notices.cpp @@ -1,43 +1,46 @@ -#include import std; +import mcpplibs.mtest; +import mcpplibs.os.stdio; import mcpp.ui; +using namespace mcpplibs::mtest; + // Closing notices are advisories about the run as a whole. They are printed // once, after the command's own output, as `tip:` lines on stderr; a command // that writes an envelope takes them first and reports them as notes. -TEST(ClosingNotices, AreDeduplicatedAndTakenOnce) { +UTest are_deduplicated_and_taken_once{"ClosingNotices/AreDeduplicatedAndTakenOnce", [] { (void)mcpp::ui::take_closing_notices(); mcpp::ui::add_closing_notice("CODE_A", "the index needs a newer mcpp"); mcpp::ui::add_closing_notice("CODE_A", "the index needs a newer mcpp"); mcpp::ui::add_closing_notice("CODE_B", "a second notice"); auto taken = mcpp::ui::take_closing_notices(); - ASSERT_EQ(taken.size(), 2u); - EXPECT_EQ(taken[0].code, "CODE_A"); - EXPECT_EQ(taken[1].message, "a second notice"); - EXPECT_TRUE(mcpp::ui::take_closing_notices().empty()); -} + expect(fatal(eq(taken.size(), 2u))); + expect(eq(taken[0].code, "CODE_A")); + expect(eq(taken[1].message, "a second notice")); + expect(mcpp::ui::take_closing_notices().empty()); +}}; -TEST(ClosingNotices, PrintAsTipLinesOnStderrAndThenAreGone) { +UTest print_as_tip_lines_on_stderr_and_then_are_gone{"ClosingNotices/PrintAsTipLinesOnStderrAndThenAreGone", [] { (void)mcpp::ui::take_closing_notices(); mcpp::ui::disable_color(); mcpp::ui::add_closing_notice("CODE", "upgrade to see newer packages"); - testing::internal::CaptureStderr(); + auto stderrCapture = mcpplibs::os::stdio::capture::begin(mcpplibs::os::stdio::stream::error).value(); mcpp::ui::print_closing_notices(); mcpp::ui::print_closing_notices(); - auto err = testing::internal::GetCapturedStderr(); - EXPECT_EQ(err, "tip: upgrade to see newer packages\n"); -} + auto err = stderrCapture.text(); + expect(eq(err, "tip: upgrade to see newer packages\n")); +}}; -TEST(ClosingNotices, QuietPrintsNothingButStillConsumes) { +UTest quiet_prints_nothing_but_still_consumes{"ClosingNotices/QuietPrintsNothingButStillConsumes", [] { (void)mcpp::ui::take_closing_notices(); mcpp::ui::add_closing_notice("CODE", "not shown under --quiet"); mcpp::ui::set_quiet(true); - testing::internal::CaptureStderr(); + auto stderrCapture = mcpplibs::os::stdio::capture::begin(mcpplibs::os::stdio::stream::error).value(); mcpp::ui::print_closing_notices(); - auto err = testing::internal::GetCapturedStderr(); + auto err = stderrCapture.text(); mcpp::ui::set_quiet(false); - EXPECT_TRUE(err.empty()) << err; - EXPECT_TRUE(mcpp::ui::take_closing_notices().empty()); -} + expect(err.empty()) << err; + expect(mcpp::ui::take_closing_notices().empty()); +}}; diff --git a/tests/unit/test_ui_streams.cpp b/tests/unit/test_ui_streams.cpp index aa5f9ccd0..db9f05e98 100644 --- a/tests/unit/test_ui_streams.cpp +++ b/tests/unit/test_ui_streams.cpp @@ -1,9 +1,12 @@ -#include import std; +import mcpplibs.mtest; +import mcpplibs.os.stdio; import mcpp.ui; import mcpp.ui.terminal; +using namespace mcpplibs::mtest; + // Two streams, one rule (output streams plan 2026-10-01, R3): a line that // narrates what the command is doing is written to standard error, and a line // that states its result stays on standard output. `mcpp.ui` makes the choice @@ -19,12 +22,12 @@ struct Captured { // What `body` wrote to each of the two streams. template Captured capture(F&& body) { - testing::internal::CaptureStdout(); - testing::internal::CaptureStderr(); + auto stdoutCapture = mcpplibs::os::stdio::capture::begin(mcpplibs::os::stdio::stream::output).value(); + auto stderrCapture = mcpplibs::os::stdio::capture::begin(mcpplibs::os::stdio::stream::error).value(); body(); Captured c; - c.err = testing::internal::GetCapturedStderr(); - c.out = testing::internal::GetCapturedStdout(); + c.err = stderrCapture.text(); + c.out = stdoutCapture.text(); return c; } @@ -36,61 +39,58 @@ void plain_mode() { } // namespace -TEST(UiStreams, NarrationIsStandardError) { - EXPECT_EQ(mcpp::ui::narration_stream(), mcpp::ui::terminal::Stream::Err); -} +UTest narration_is_standard_error{"UiStreams/NarrationIsStandardError", [] { + expect(eq(mcpp::ui::narration_stream(), mcpp::ui::terminal::Stream::Err)); +}}; -TEST(UiStreams, AStepIsNarration) { +UTest a_step_is_narration{"UiStreams/AStepIsNarration", [] { plain_mode(); auto c = capture([] { mcpp::ui::status("Compiling", "app v0.1.0 (.)"); mcpp::ui::info("Downloading", "xim:gcc"); mcpp::ui::line(" a line of a step"); }); - EXPECT_EQ(c.out, "") << c.out; - EXPECT_EQ(c.err, - " Compiling app v0.1.0 (.)\n" + expect(eq(c.out, "")) << c.out; + expect(eq(c.err, " Compiling app v0.1.0 (.)\n" " Downloading xim:gcc\n" - " a line of a step\n"); -} + " a line of a step\n")); +}}; -TEST(UiStreams, FinishedIsNarrationAndFollowsTheLinesBeforeItWithABlankLine) { +UTest finished_is_narration_and_follows_the_lines_before_it_with_a_blank_line{"UiStreams/FinishedIsNarrationAndFollowsTheLinesBeforeItWithABlankLine", [] { plain_mode(); auto c = capture([] { mcpp::ui::status("Compiling", "app v0.1.0 (.)"); mcpp::ui::finished("dev", std::chrono::milliseconds(840), "unoptimized + debuginfo"); }); - EXPECT_EQ(c.out, "") << c.out; - EXPECT_NE(c.err.find(" Finished dev [unoptimized + debuginfo] in 0.84s\n"), - std::string::npos) << c.err; + expect(eq(c.out, "")) << c.out; + expect(ne(c.err.find(" Finished dev [unoptimized + debuginfo] in 0.84s\n"), std::string::npos)) << c.err; // The blank line answers to a narrated line, and the line before it is one. - EXPECT_NE(c.err.find("(.)\n\n Finished"), std::string::npos) << c.err; -} + expect(ne(c.err.find("(.)\n\n Finished"), std::string::npos)) << c.err; +}}; -TEST(UiStreams, AResultStaysOnStandardOutput) { +UTest a_result_stays_on_standard_output{"UiStreams/AResultStaysOnStandardOutput", [] { plain_mode(); auto c = capture([] { mcpp::ui::plain("tests/smoke ... ok (0.01s)"); mcpp::ui::result("test result", "ok. 1 passed; 0 failed; finished in 0.02s"); mcpp::ui::result("workspace result", "ok. 2 member(s); 2 passed"); }); - EXPECT_EQ(c.err, "") << c.err; - EXPECT_EQ(c.out, - "tests/smoke ... ok (0.01s)\n" + expect(eq(c.err, "")) << c.err; + expect(eq(c.out, "tests/smoke ... ok (0.01s)\n" " test result ok. 1 passed; 0 failed; finished in 0.02s\n" - "workspace result ok. 2 member(s); 2 passed\n"); -} + "workspace result ok. 2 member(s); 2 passed\n")); +}}; // The stream is the caller's statement, never the verb's spelling: `status` // narrates whatever its verb reads. -TEST(UiStreams, TheStreamIsNotInferredFromTheVerb) { +UTest the_stream_is_not_inferred_from_the_verb{"UiStreams/TheStreamIsNotInferredFromTheVerb", [] { plain_mode(); auto c = capture([] { mcpp::ui::status("test result", "ok. 1 passed"); }); - EXPECT_EQ(c.out, "") << c.out; - EXPECT_NE(c.err.find("test result ok. 1 passed"), std::string::npos) << c.err; -} + expect(eq(c.out, "")) << c.out; + expect(ne(c.err.find("test result ok. 1 passed"), std::string::npos)) << c.err; +}}; -TEST(UiStreams, WarningsAndErrorsAreStandardError) { +UTest warnings_and_errors_are_standard_error{"UiStreams/WarningsAndErrorsAreStandardError", [] { plain_mode(); auto c = capture([] { mcpp::ui::warning("a warning"); @@ -98,11 +98,11 @@ TEST(UiStreams, WarningsAndErrorsAreStandardError) { mcpp::ui::note("a note"); mcpp::ui::block("diagnostic text"); }); - EXPECT_EQ(c.out, "") << c.out; - EXPECT_EQ(c.err, "warning: a warning\nerror: an error\nnote: a note\ndiagnostic text\n"); -} + expect(eq(c.out, "")) << c.out; + expect(eq(c.err, "warning: a warning\nerror: an error\nnote: a note\ndiagnostic text\n")); +}}; -TEST(UiStreams, QuietSuppressesTheNarrationAndTheBlankLineThatSeparatesIt) { +UTest quiet_suppresses_the_narration_and_the_blank_line_that_separates_it{"UiStreams/QuietSuppressesTheNarrationAndTheBlankLineThatSeparatesIt", [] { plain_mode(); mcpp::ui::set_quiet(true); auto c = capture([] { @@ -112,27 +112,27 @@ TEST(UiStreams, QuietSuppressesTheNarrationAndTheBlankLineThatSeparatesIt) { mcpp::ui::finished("dev", std::chrono::milliseconds(1)); }); mcpp::ui::set_quiet(false); - EXPECT_EQ(c.out, "") << c.out; - EXPECT_EQ(c.err, "") << c.err; -} + expect(eq(c.out, "")) << c.out; + expect(eq(c.err, "")) << c.err; +}}; -TEST(UiStreams, TheBlankLineAfterRunningIsOnTheStreamOfTheRunningLine) { +UTest the_blank_line_after_running_is_on_the_stream_of_the_running_line{"UiStreams/TheBlankLineAfterRunningIsOnTheStreamOfTheRunningLine", [] { plain_mode(); auto c = capture([] { mcpp::ui::status("Running", "`target/bin/app`"); mcpp::ui::line(""); }); - EXPECT_EQ(c.out, "") << c.out; - EXPECT_EQ(c.err, " Running `target/bin/app`\n\n"); -} + expect(eq(c.out, "")) << c.out; + expect(eq(c.err, " Running `target/bin/app`\n\n")); +}}; -TEST(UiStreams, AProgressBarCompletionLineIsNarration) { +UTest a_progress_bar_completion_line_is_narration{"UiStreams/AProgressBarCompletionLineIsNarration", [] { plain_mode(); auto c = capture([] { mcpp::ui::ProgressBar bar("Fetching", "example"); bar.update_bytes(100, 100, 1.0); bar.finish(); }); - EXPECT_EQ(c.out, "") << c.out; - EXPECT_NE(c.err.find("Fetching example done"), std::string::npos) << c.err; -} + expect(eq(c.out, "")) << c.out; + expect(ne(c.err.find("Fetching example done"), std::string::npos)) << c.err; +}}; diff --git a/tests/unit/test_version_floor.cpp b/tests/unit/test_version_floor.cpp index f409d2abd..ce2a61342 100644 --- a/tests/unit/test_version_floor.cpp +++ b/tests/unit/test_version_floor.cpp @@ -1,73 +1,75 @@ -#include import std; +import mcpplibs.mtest; import mcpp.build.version_floor; +using namespace mcpplibs::mtest; + using mcpp::build::parse_version_floor; using mcpp::build::parse_version_fact; using mcpp::build::version_at_least; // ── The two spellings ────────────────────────────────────────────────────── -TEST(VersionFloor, ReadsAFloor) { +UTest reads_a_floor{"VersionFloor/ReadsAFloor", [] { auto f = parse_version_floor("cuda.driver >= 12.0"); - ASSERT_TRUE(f.valid()); - EXPECT_EQ(f.name, "cuda.driver"); - EXPECT_EQ(f.version, "12.0"); -} + expect(fatal(bool(f.valid()))); + expect(eq(f.name, "cuda.driver")); + expect(eq(f.version, "12.0")); +}}; -TEST(VersionFloor, ReadsAFact) { +UTest reads_a_fact{"VersionFloor/ReadsAFact", [] { auto f = parse_version_fact("cuda.driver=12.4"); - ASSERT_TRUE(f.valid()); - EXPECT_EQ(f.name, "cuda.driver"); - EXPECT_EQ(f.version, "12.4"); -} + expect(fatal(bool(f.valid()))); + expect(eq(f.name, "cuda.driver")); + expect(eq(f.version, "12.4")); +}}; -TEST(VersionFloor, AFloorIsNotReadAsAFact) { +UTest a_floor_is_not_read_as_a_fact{"VersionFloor/AFloorIsNotReadAsAFact", [] { // Both spellings live in string lists, and `>=` contains `=`. Reading one // as the other would turn "needs at least 12.0" into "this machine has // 12.0" — a requirement silently becoming its own satisfaction. - EXPECT_FALSE(parse_version_fact("cuda.driver >= 12.0").valid()); -} + expect(!(parse_version_fact("cuda.driver >= 12.0").valid())); +}}; -TEST(VersionFloor, AnythingElseIsNotAFloor) { - EXPECT_FALSE(parse_version_floor("cuda.driver").valid()); - EXPECT_FALSE(parse_version_floor("cuda.driver >= ").valid()); - EXPECT_FALSE(parse_version_floor(" >= 12.0").valid()); +UTest anything_else_is_not_a_floor{"VersionFloor/AnythingElseIsNotAFloor", [] { + expect(!(parse_version_floor("cuda.driver").valid())); + expect(!(parse_version_floor("cuda.driver >= ").valid())); + expect(!(parse_version_floor(" >= 12.0").valid())); // Not a version: refused rather than read as `12`. - EXPECT_FALSE(parse_version_floor("cuda.driver >= 12.").valid()); - EXPECT_FALSE(parse_version_floor("cuda.driver >= twelve").valid()); -} + expect(!(parse_version_floor("cuda.driver >= 12.").valid())); + expect(!(parse_version_floor("cuda.driver >= twelve").valid())); +}}; // ── Comparison ───────────────────────────────────────────────────────────── -TEST(VersionFloor, ComparesComponentwise) { - EXPECT_EQ(version_at_least("12.4", "12.0"), std::optional{true}); - EXPECT_EQ(version_at_least("12.0", "12.4"), std::optional{false}); - EXPECT_EQ(version_at_least("13.0", "12.9"), std::optional{true}); +UTest compares_componentwise{"VersionFloor/ComparesComponentwise", [] { + expect(eq(version_at_least("12.4", "12.0"), std::optional{true})); + expect(eq(version_at_least("12.0", "12.4"), std::optional{false})); + expect(eq(version_at_least("13.0", "12.9"), std::optional{true})); // Not lexicographic: 12.10 is above 12.9. - EXPECT_EQ(version_at_least("12.10", "12.9"), std::optional{true}); -} + expect(eq(version_at_least("12.10", "12.9"), std::optional{true})); +}}; -TEST(VersionFloor, AMissingComponentIsZero) { - EXPECT_EQ(version_at_least("12", "12.0"), std::optional{true}); - EXPECT_EQ(version_at_least("12.0", "12"), std::optional{true}); - EXPECT_EQ(version_at_least("12", "12.1"), std::optional{false}); -} +UTest a_missing_component_is_zero{"VersionFloor/AMissingComponentIsZero", [] { + expect(eq(version_at_least("12", "12.0"), std::optional{true})); + expect(eq(version_at_least("12.0", "12"), std::optional{true})); + expect(eq(version_at_least("12", "12.1"), std::optional{false})); +}}; -TEST(VersionFloor, EitherSideUnreadableIsNoClaim) { +UTest either_side_unreadable_is_no_claim{"VersionFloor/EitherSideUnreadableIsNoClaim", [] { // The whole reason this mechanism exists is that a wrong answer is worse // than no answer. A version that cannot be read yields neither. - EXPECT_FALSE(version_at_least("unknown", "12.0").has_value()); - EXPECT_FALSE(version_at_least("12.0", "").has_value()); -} + expect(!(version_at_least("unknown", "12.0").has_value())); + expect(!(version_at_least("12.0", "").has_value())); +}}; -TEST(VersionFloor, NoVendorVocabularyIsRequired) { +UTest no_vendor_vocabulary_is_required{"VersionFloor/NoVendorVocabularyIsRequired", [] { // The name is data. A backend this engine has never heard of compares the // same way, which is what keeps the comparison out of the vendors' reach. auto f = parse_version_floor("some.future.thing >= 4.2.1"); - ASSERT_TRUE(f.valid()); - EXPECT_EQ(f.name, "some.future.thing"); - EXPECT_EQ(version_at_least("4.3", f.version), std::optional{true}); - EXPECT_EQ(version_at_least("4.2", f.version), std::optional{false}); -} + expect(fatal(bool(f.valid()))); + expect(eq(f.name, "some.future.thing")); + expect(eq(version_at_least("4.3", f.version), std::optional{true})); + expect(eq(version_at_least("4.2", f.version), std::optional{false})); +}}; diff --git a/tests/unit/test_version_req.cpp b/tests/unit/test_version_req.cpp index c6108ff46..aac38ceaf 100644 --- a/tests/unit/test_version_req.cpp +++ b/tests/unit/test_version_req.cpp @@ -1,168 +1,170 @@ -#include import std; +import mcpplibs.mtest; import mcpp.version_req; +using namespace mcpplibs::mtest; + using namespace mcpp::version_req; -TEST(VersionReq, ParseVersion) { +UTest version_req_parse_version{"VersionReq/ParseVersion", [] { auto v = parse_version("1.2.3"); - ASSERT_TRUE(v); - EXPECT_EQ(v->major(), 1); EXPECT_EQ(v->minor(), 2); EXPECT_EQ(v->patch(), 3); + expect(fatal(bool(v))); + expect(eq(v->major(), 1)); expect(eq(v->minor(), 2)); expect(eq(v->patch(), 3)); v = parse_version("0.5"); - ASSERT_TRUE(v); - EXPECT_EQ(v->major(), 0); EXPECT_EQ(v->minor(), 5); EXPECT_EQ(v->patch(), 0); + expect(fatal(bool(v))); + expect(eq(v->major(), 0)); expect(eq(v->minor(), 5)); expect(eq(v->patch(), 0)); v = parse_version("7"); - ASSERT_TRUE(v); - EXPECT_EQ(v->major(), 7); EXPECT_EQ(v->minor(), 0); EXPECT_EQ(v->patch(), 0); + expect(fatal(bool(v))); + expect(eq(v->major(), 7)); expect(eq(v->minor(), 0)); expect(eq(v->patch(), 0)); // #363: pre-release is PARSED, not stripped. Build metadata is dropped from // the order (SemVer §10) but the key's identity lives in the literal, which // pm/resolver.cppm carries separately. v = parse_version("1.2.3-beta.1+build42"); - ASSERT_TRUE(v); - EXPECT_EQ(v->str(), "1.2.3-beta.1"); - EXPECT_TRUE(v->isPrerelease()); -} - -TEST(VersionReq, ParseAny) { - EXPECT_TRUE(parse_req("")->any); - EXPECT_TRUE(parse_req("*")->any); - EXPECT_TRUE(parse_req(" * ")->any); -} - -TEST(VersionReq, MatchExact) { - auto r = parse_req("=1.2.3"); ASSERT_TRUE(r); - EXPECT_TRUE (matches(*r, *parse_version("1.2.3"))); - EXPECT_FALSE(matches(*r, *parse_version("1.2.4"))); - EXPECT_FALSE(matches(*r, *parse_version("1.2.2"))); -} - -TEST(VersionReq, MatchCaretBare) { - auto r = parse_req("1.2.3"); ASSERT_TRUE(r); - EXPECT_TRUE (matches(*r, *parse_version("1.2.3"))); - EXPECT_TRUE (matches(*r, *parse_version("1.5.0"))); - EXPECT_TRUE (matches(*r, *parse_version("1.99.99"))); - EXPECT_FALSE(matches(*r, *parse_version("2.0.0"))); - EXPECT_FALSE(matches(*r, *parse_version("1.2.2"))); -} - -TEST(VersionReq, MatchCaretZeroMajor) { - auto r = parse_req("0.5.3"); ASSERT_TRUE(r); - EXPECT_TRUE (matches(*r, *parse_version("0.5.3"))); - EXPECT_TRUE (matches(*r, *parse_version("0.5.99"))); - EXPECT_FALSE(matches(*r, *parse_version("0.6.0"))) << "minor bump kills caret when major==0"; -} - -TEST(VersionReq, MatchTilde) { - auto r = parse_req("~1.2.3"); ASSERT_TRUE(r); - EXPECT_TRUE (matches(*r, *parse_version("1.2.3"))); - EXPECT_TRUE (matches(*r, *parse_version("1.2.99"))); - EXPECT_FALSE(matches(*r, *parse_version("1.3.0"))); - EXPECT_FALSE(matches(*r, *parse_version("1.2.2"))); -} + expect(fatal(bool(v))); + expect(eq(v->str(), "1.2.3-beta.1")); + expect(bool(v->isPrerelease())); +}}; + +UTest parse_any{"VersionReq/ParseAny", [] { + expect(bool(parse_req("")->any)); + expect(bool(parse_req("*")->any)); + expect(bool(parse_req(" * ")->any)); +}}; + +UTest match_exact{"VersionReq/MatchExact", [] { + auto r = parse_req("=1.2.3"); expect(fatal(bool(r))); + expect(matches(*r, *parse_version("1.2.3"))); + expect(!(matches(*r, *parse_version("1.2.4")))); + expect(!(matches(*r, *parse_version("1.2.2")))); +}}; + +UTest match_caret_bare{"VersionReq/MatchCaretBare", [] { + auto r = parse_req("1.2.3"); expect(fatal(bool(r))); + expect(matches(*r, *parse_version("1.2.3"))); + expect(matches(*r, *parse_version("1.5.0"))); + expect(matches(*r, *parse_version("1.99.99"))); + expect(!(matches(*r, *parse_version("2.0.0")))); + expect(!(matches(*r, *parse_version("1.2.2")))); +}}; + +UTest match_caret_zero_major{"VersionReq/MatchCaretZeroMajor", [] { + auto r = parse_req("0.5.3"); expect(fatal(bool(r))); + expect(matches(*r, *parse_version("0.5.3"))); + expect(matches(*r, *parse_version("0.5.99"))); + expect(!(matches(*r, *parse_version("0.6.0")))) << "minor bump kills caret when major==0"; +}}; + +UTest match_tilde{"VersionReq/MatchTilde", [] { + auto r = parse_req("~1.2.3"); expect(fatal(bool(r))); + expect(matches(*r, *parse_version("1.2.3"))); + expect(matches(*r, *parse_version("1.2.99"))); + expect(!(matches(*r, *parse_version("1.3.0")))); + expect(!(matches(*r, *parse_version("1.2.2")))); +}}; // A segment that is not written is a wildcard, not a zero (Cargo's partial // requirements). Reading `^0` as `^0.0.0` made it admit 0.0.0 alone. -TEST(VersionReq, PartialCaretBoundsFollowTheWrittenSegments) { - auto major = parse_req("^0"); ASSERT_TRUE(major); - EXPECT_TRUE (matches(*major, *parse_version("0.0.0"))); - EXPECT_TRUE (matches(*major, *parse_version("0.0.5"))); - EXPECT_TRUE (matches(*major, *parse_version("0.9.3"))); - EXPECT_FALSE(matches(*major, *parse_version("1.0.0"))); - - auto minor = parse_req("^0.0"); ASSERT_TRUE(minor); - EXPECT_TRUE (matches(*minor, *parse_version("0.0.0"))); - EXPECT_TRUE (matches(*minor, *parse_version("0.0.7"))); - EXPECT_FALSE(matches(*minor, *parse_version("0.1.0"))); +UTest partial_caret_bounds_follow_the_written_segments{"VersionReq/PartialCaretBoundsFollowTheWrittenSegments", [] { + auto major = parse_req("^0"); expect(fatal(bool(major))); + expect(matches(*major, *parse_version("0.0.0"))); + expect(matches(*major, *parse_version("0.0.5"))); + expect(matches(*major, *parse_version("0.9.3"))); + expect(!(matches(*major, *parse_version("1.0.0")))); + + auto minor = parse_req("^0.0"); expect(fatal(bool(minor))); + expect(matches(*minor, *parse_version("0.0.0"))); + expect(matches(*minor, *parse_version("0.0.7"))); + expect(!(matches(*minor, *parse_version("0.1.0")))); // Every segment written: the leftmost-nonzero rule is unchanged. - auto patch = parse_req("^0.0.0"); ASSERT_TRUE(patch); - EXPECT_TRUE (matches(*patch, *parse_version("0.0.0"))); - EXPECT_FALSE(matches(*patch, *parse_version("0.0.1"))); + auto patch = parse_req("^0.0.0"); expect(fatal(bool(patch))); + expect(matches(*patch, *parse_version("0.0.0"))); + expect(!(matches(*patch, *parse_version("0.0.1")))); - auto exact = parse_req("^0.0.3"); ASSERT_TRUE(exact); - EXPECT_TRUE (matches(*exact, *parse_version("0.0.3"))); - EXPECT_FALSE(matches(*exact, *parse_version("0.0.4"))); + auto exact = parse_req("^0.0.3"); expect(fatal(bool(exact))); + expect(matches(*exact, *parse_version("0.0.3"))); + expect(!(matches(*exact, *parse_version("0.0.4")))); - auto one = parse_req("^1"); ASSERT_TRUE(one); - EXPECT_TRUE (matches(*one, *parse_version("1.9.9"))); - EXPECT_FALSE(matches(*one, *parse_version("2.0.0"))); + auto one = parse_req("^1"); expect(fatal(bool(one))); + expect(matches(*one, *parse_version("1.9.9"))); + expect(!(matches(*one, *parse_version("2.0.0")))); - auto zeroFive = parse_req("^0.5"); ASSERT_TRUE(zeroFive); - EXPECT_TRUE (matches(*zeroFive, *parse_version("0.5.9"))); - EXPECT_FALSE(matches(*zeroFive, *parse_version("0.6.0"))); -} + auto zeroFive = parse_req("^0.5"); expect(fatal(bool(zeroFive))); + expect(matches(*zeroFive, *parse_version("0.5.9"))); + expect(!(matches(*zeroFive, *parse_version("0.6.0")))); +}}; // A bare requirement is a caret (#866's follow-up), so a bare partial spelling // takes the same wildcard bounds: "0" is <1.0.0 and "0.0" is <0.1.0, where // before #866 both matched 0.0.0 alone. -TEST(VersionReq, BarePartialRequirementsAreCarets) { +UTest bare_partial_requirements_are_carets{"VersionReq/BarePartialRequirementsAreCarets", [] { for (auto [bare, caret] : {std::pair{"0", "^0"}, std::pair{"0.0", "^0.0"}, std::pair{"1", "^1"}, std::pair{"1.2", "^1.2"}}) { - auto b = parse_req(bare); ASSERT_TRUE(b) << bare; - auto c = parse_req(caret); ASSERT_TRUE(c) << caret; + auto b = parse_req(bare); expect(fatal(bool(b))) << bare; + auto c = parse_req(caret); expect(fatal(bool(c))) << caret; for (auto v : {"0.0.0", "0.0.7", "0.1.0", "0.9.3", "1.0.0", "1.2.0", "1.2.9", "1.3.0", "2.0.0"}) - EXPECT_EQ(matches(*b, *parse_version(v)), matches(*c, *parse_version(v))) + expect(eq(matches(*b, *parse_version(v)), matches(*c, *parse_version(v)))) << bare << " against " << v; } - EXPECT_TRUE (matches(*parse_req("0"), *parse_version("0.9.3"))); - EXPECT_FALSE(matches(*parse_req("0"), *parse_version("1.0.0"))); - EXPECT_TRUE (matches(*parse_req("0.0"), *parse_version("0.0.7"))); - EXPECT_FALSE(matches(*parse_req("0.0"), *parse_version("0.1.0"))); -} - -TEST(VersionReq, PartialTildeWithOnlyAMajorHoldsTheMajor) { - auto r = parse_req("~1"); ASSERT_TRUE(r); - EXPECT_TRUE (matches(*r, *parse_version("1.0.0"))); - EXPECT_TRUE (matches(*r, *parse_version("1.1.0"))); - EXPECT_TRUE (matches(*r, *parse_version("1.9.9"))); - EXPECT_FALSE(matches(*r, *parse_version("2.0.0"))); - EXPECT_FALSE(matches(*r, *parse_version("0.9.9"))); - - auto zero = parse_req("~0"); ASSERT_TRUE(zero); - EXPECT_TRUE (matches(*zero, *parse_version("0.4.0"))); - EXPECT_FALSE(matches(*zero, *parse_version("1.0.0"))); + expect(matches(*parse_req("0"), *parse_version("0.9.3"))); + expect(!(matches(*parse_req("0"), *parse_version("1.0.0")))); + expect(matches(*parse_req("0.0"), *parse_version("0.0.7"))); + expect(!(matches(*parse_req("0.0"), *parse_version("0.1.0")))); +}}; + +UTest partial_tilde_with_only_a_major_holds_the_major{"VersionReq/PartialTildeWithOnlyAMajorHoldsTheMajor", [] { + auto r = parse_req("~1"); expect(fatal(bool(r))); + expect(matches(*r, *parse_version("1.0.0"))); + expect(matches(*r, *parse_version("1.1.0"))); + expect(matches(*r, *parse_version("1.9.9"))); + expect(!(matches(*r, *parse_version("2.0.0")))); + expect(!(matches(*r, *parse_version("0.9.9")))); + + auto zero = parse_req("~0"); expect(fatal(bool(zero))); + expect(matches(*zero, *parse_version("0.4.0"))); + expect(!(matches(*zero, *parse_version("1.0.0")))); // With the minor written, tilde holds it, as before. - auto minor = parse_req("~1.2"); ASSERT_TRUE(minor); - EXPECT_TRUE (matches(*minor, *parse_version("1.2.9"))); - EXPECT_FALSE(matches(*minor, *parse_version("1.3.0"))); -} - -TEST(VersionReq, MatchRangeAnd) { - auto r = parse_req(">=1.2, <2.0"); ASSERT_TRUE(r); - EXPECT_TRUE (matches(*r, *parse_version("1.2.0"))); - EXPECT_TRUE (matches(*r, *parse_version("1.99.99"))); - EXPECT_FALSE(matches(*r, *parse_version("1.1.99"))); - EXPECT_FALSE(matches(*r, *parse_version("2.0.0"))); -} - -TEST(VersionReq, ChooseBest) { + auto minor = parse_req("~1.2"); expect(fatal(bool(minor))); + expect(matches(*minor, *parse_version("1.2.9"))); + expect(!(matches(*minor, *parse_version("1.3.0")))); +}}; + +UTest match_range_and{"VersionReq/MatchRangeAnd", [] { + auto r = parse_req(">=1.2, <2.0"); expect(fatal(bool(r))); + expect(matches(*r, *parse_version("1.2.0"))); + expect(matches(*r, *parse_version("1.99.99"))); + expect(!(matches(*r, *parse_version("1.1.99")))); + expect(!(matches(*r, *parse_version("2.0.0")))); +}}; + +UTest choose_best{"VersionReq/ChooseBest", [] { std::vector avail = { *parse_version("1.0.0"), *parse_version("1.2.0"), *parse_version("1.2.5"), *parse_version("2.0.0"), }; - auto r = parse_req("^1.2"); ASSERT_TRUE(r); + auto r = parse_req("^1.2"); expect(fatal(bool(r))); auto pick = choose(*r, avail); - ASSERT_TRUE(pick); - EXPECT_EQ(avail[*pick].str(), "1.2.5"); -} + expect(fatal(bool(pick))); + expect(eq(avail[*pick].str(), "1.2.5")); +}}; -TEST(VersionReq, ChooseUnsatisfiable) { +UTest choose_unsatisfiable{"VersionReq/ChooseUnsatisfiable", [] { std::vector avail = { *parse_version("1.0.0"), *parse_version("2.0.0") }; - auto r = parse_req("^3"); ASSERT_TRUE(r); - EXPECT_FALSE(choose(*r, avail).has_value()); -} + auto r = parse_req("^3"); expect(fatal(bool(r))); + expect(!(choose(*r, avail).has_value())); +}}; -TEST(VersionReq, RejectsGarbage) { - EXPECT_FALSE(parse_req(">=foo").has_value()); - EXPECT_FALSE(parse_version("not-a-version").has_value()); -} +UTest rejects_garbage{"VersionReq/RejectsGarbage", [] { + expect(!(parse_req(">=foo").has_value())); + expect(!(parse_version("not-a-version").has_value())); +}}; // ─── Date-based versions (YYYY.M.D.N), mcpp's own scheme ──────────────── // @@ -170,82 +172,82 @@ TEST(VersionReq, RejectsGarbage) { // every release of a given day compared EQUAL — which silently disabled the // E0006 index-floor check (index_contract.cppm compares mcpp's own version). -TEST(VersionReq, DateVersionParsesFourSegments) { +UTest date_version_parses_four_segments{"VersionReq/DateVersionParsesFourSegments", [] { auto v = parse_version("2026.7.27.1"); - ASSERT_TRUE(v); - EXPECT_EQ(v->major(), 2026); - EXPECT_EQ(v->minor(), 7); - EXPECT_EQ(v->patch(), 27); - EXPECT_EQ(v->revision(), 1); -} - -TEST(VersionReq, DateVersionOrdersWithinTheSameDay) { - EXPECT_LT(*parse_version("2026.7.27.1"), *parse_version("2026.7.27.2")); + expect(fatal(bool(v))); + expect(eq(v->major(), 2026)); + expect(eq(v->minor(), 7)); + expect(eq(v->patch(), 27)); + expect(eq(v->revision(), 1)); +}}; + +UTest date_version_orders_within_the_same_day{"VersionReq/DateVersionOrdersWithinTheSameDay", [] { + expect(lt(*parse_version("2026.7.27.1"), *parse_version("2026.7.27.2"))); // Numeric, not lexicographic: the 10th release of a day must outrank the 9th. - EXPECT_LT(*parse_version("2026.7.27.9"), *parse_version("2026.7.27.10")); - EXPECT_EQ(*parse_version("2026.7.27.3"), *parse_version("2026.7.27.3")); -} + expect(lt(*parse_version("2026.7.27.9"), *parse_version("2026.7.27.10"))); + expect(eq(*parse_version("2026.7.27.3"), *parse_version("2026.7.27.3"))); +}}; -TEST(VersionReq, DateVersionOutranksLegacySemver) { - EXPECT_LT(*parse_version("0.0.109"), *parse_version("2026.7.27.1")); -} +UTest date_version_outranks_legacy_semver{"VersionReq/DateVersionOutranksLegacySemver", [] { + expect(lt(*parse_version("0.0.109"), *parse_version("2026.7.27.1"))); +}}; -TEST(VersionReq, ThreeSegmentVersionsStillCompareEqualAcrossWrittenLength) { +UTest three_segment_versions_still_compare_equal_across_written_length{"VersionReq/ThreeSegmentVersionsStillCompareEqualAcrossWrittenLength", [] { // `components` must not leak into ordering. - EXPECT_EQ(*parse_version("1.2"), *parse_version("1.2.0")); - EXPECT_EQ(*parse_version("1"), *parse_version("1.0.0")); - EXPECT_EQ(*parse_version("1.2.3"), *parse_version("1.2.3")); -} + expect(eq(*parse_version("1.2"), *parse_version("1.2.0"))); + expect(eq(*parse_version("1"), *parse_version("1.0.0"))); + expect(eq(*parse_version("1.2.3"), *parse_version("1.2.3"))); +}}; // str() feeds pm/resolver.cppm's resolved-version return value, which becomes // the lock entry and the xlings wire address — it has to reproduce the // index's literal version key. -TEST(VersionReq, StrKeepsThreeSegmentNormalizationUnchanged) { - EXPECT_EQ(parse_version("1.15.2")->str(), "1.15.2"); - EXPECT_EQ(parse_version("0.5")->str(), "0.5.0"); - EXPECT_EQ(parse_version("7")->str(), "7.0.0"); -} - -TEST(VersionReq, StrRendersTheFourthSegmentWhenWritten) { - EXPECT_EQ(parse_version("2026.7.27.1")->str(), "2026.7.27.1"); +UTest str_keeps_three_segment_normalization_unchanged{"VersionReq/StrKeepsThreeSegmentNormalizationUnchanged", [] { + expect(eq(parse_version("1.15.2")->str(), "1.15.2")); + expect(eq(parse_version("0.5")->str(), "0.5.0")); + expect(eq(parse_version("7")->str(), "7.0.0")); +}}; + +UTest str_renders_the_fourth_segment_when_written{"VersionReq/StrRendersTheFourthSegmentWhenWritten", [] { + expect(eq(parse_version("2026.7.27.1")->str(), "2026.7.27.1")); // A trailing ".0" is mcpp's formal-release marker and must NOT collapse // to three segments — "2026.8.1" is a key that does not exist. - EXPECT_EQ(parse_version("2026.8.1.0")->str(), "2026.8.1.0"); -} + expect(eq(parse_version("2026.8.1.0")->str(), "2026.8.1.0")); +}}; -TEST(VersionReq, CaretUpperBoundZeroesTheFourthSegment) { - auto r = parse_req("^2026.7.27.3"); ASSERT_TRUE(r); - EXPECT_TRUE (matches(*r, *parse_version("2026.7.27.3"))); - EXPECT_TRUE (matches(*r, *parse_version("2026.9.1.0"))); +UTest caret_upper_bound_zeroes_the_fourth_segment{"VersionReq/CaretUpperBoundZeroesTheFourthSegment", [] { + auto r = parse_req("^2026.7.27.3"); expect(fatal(bool(r))); + expect(matches(*r, *parse_version("2026.7.27.3"))); + expect(matches(*r, *parse_version("2026.9.1.0"))); // Upper bound is 2027.0.0.0 — it must not inherit the constraint's ".3". - EXPECT_FALSE(matches(*r, *parse_version("2027.0.0.0"))); - EXPECT_FALSE(matches(*r, *parse_version("2027.0.0.2"))); -} - -TEST(VersionReq, TildeUpperBoundZeroesTheFourthSegment) { - auto r = parse_req("~2026.7.27.3"); ASSERT_TRUE(r); - EXPECT_TRUE (matches(*r, *parse_version("2026.7.99.0"))); - EXPECT_FALSE(matches(*r, *parse_version("2026.8.0.0"))); - EXPECT_FALSE(matches(*r, *parse_version("2026.8.0.5"))); -} - -TEST(VersionReq, ExactMatchDistinguishesSameDayReleases) { - auto r = parse_req("=2026.7.27.1"); ASSERT_TRUE(r); - EXPECT_TRUE (matches(*r, *parse_version("2026.7.27.1"))); - EXPECT_FALSE(matches(*r, *parse_version("2026.7.27.2"))); -} - -TEST(VersionReq, ChooseBestAmongDateVersions) { + expect(!(matches(*r, *parse_version("2027.0.0.0")))); + expect(!(matches(*r, *parse_version("2027.0.0.2")))); +}}; + +UTest tilde_upper_bound_zeroes_the_fourth_segment{"VersionReq/TildeUpperBoundZeroesTheFourthSegment", [] { + auto r = parse_req("~2026.7.27.3"); expect(fatal(bool(r))); + expect(matches(*r, *parse_version("2026.7.99.0"))); + expect(!(matches(*r, *parse_version("2026.8.0.0")))); + expect(!(matches(*r, *parse_version("2026.8.0.5")))); +}}; + +UTest exact_match_distinguishes_same_day_releases{"VersionReq/ExactMatchDistinguishesSameDayReleases", [] { + auto r = parse_req("=2026.7.27.1"); expect(fatal(bool(r))); + expect(matches(*r, *parse_version("2026.7.27.1"))); + expect(!(matches(*r, *parse_version("2026.7.27.2")))); +}}; + +UTest choose_best_among_date_versions{"VersionReq/ChooseBestAmongDateVersions", [] { std::vector avail = { *parse_version("2026.7.27.1"), *parse_version("2026.7.27.10"), *parse_version("2026.7.27.9"), }; - auto r = parse_req("*"); ASSERT_TRUE(r); + auto r = parse_req("*"); expect(fatal(bool(r))); auto pick = choose(*r, avail); - ASSERT_TRUE(pick); - EXPECT_EQ(avail[*pick].str(), "2026.7.27.10"); -} + expect(fatal(bool(pick))); + expect(eq(avail[*pick].str(), "2026.7.27.10")); +}}; // ─── #363: a version is an ORDER; the literal key is the identity ──────── // @@ -255,88 +257,88 @@ TEST(VersionReq, ChooseBestAmongDateVersions) { // 25.0.4+7, `jdk-corretto` ships the five-segment 25.0.4.7.1, and `khistory` // publishes only `pre-v0.0.5`. -TEST(VersionReq, PrereleaseSortsBelowItsRelease) { - EXPECT_LT(*parse_version("1.92.8-docking"), *parse_version("1.92.8")); - EXPECT_LT(*parse_version("1.3.3-beta.1"), *parse_version("1.3.3")); +UTest prerelease_sorts_below_its_release{"VersionReq/PrereleaseSortsBelowItsRelease", [] { + expect(lt(*parse_version("1.92.8-docking"), *parse_version("1.92.8"))); + expect(lt(*parse_version("1.3.3-beta.1"), *parse_version("1.3.3"))); // ...and above the previous release. - EXPECT_LT(*parse_version("1.3.2"), *parse_version("1.3.3-beta.1")); -} + expect(lt(*parse_version("1.3.2"), *parse_version("1.3.3-beta.1"))); +}}; -TEST(VersionReq, PrereleaseIdentifiersCompareBySemVerRules) { +UTest prerelease_identifiers_compare_by_sem_ver_rules{"VersionReq/PrereleaseIdentifiersCompareBySemVerRules", [] { // Numeric identifiers compare numerically, not lexicographically. - EXPECT_LT(*parse_version("1.0.0-rc.9"), *parse_version("1.0.0-rc.10")); + expect(lt(*parse_version("1.0.0-rc.9"), *parse_version("1.0.0-rc.10"))); // Numeric identifiers rank BELOW alphanumeric ones (SemVer §11.4.3). - EXPECT_LT(*parse_version("1.0.0-1"), *parse_version("1.0.0-alpha")); + expect(lt(*parse_version("1.0.0-1"), *parse_version("1.0.0-alpha"))); // A longer identifier list outranks its own prefix (§11.4.4). - EXPECT_LT(*parse_version("1.0.0-alpha"), *parse_version("1.0.0-alpha.1")); + expect(lt(*parse_version("1.0.0-alpha"), *parse_version("1.0.0-alpha.1"))); // Distinct pre-releases are never equal — the property that keeps // 1.92.8-docking and 1.92.8-nodocking addressable as two things. - EXPECT_NE(*parse_version("1.92.8-docking"), *parse_version("1.92.8-legacy")); -} + expect(ne(*parse_version("1.92.8-docking"), *parse_version("1.92.8-legacy"))); +}}; -TEST(VersionReq, RangesDoNotSeePrereleasesUnlessAskedFor) { +UTest ranges_do_not_see_prereleases_unless_asked_for{"VersionReq/RangesDoNotSeePrereleasesUnlessAskedFor", [] { // The #363 headline: ^1.92.8 must not silently pick up the docking branch. - auto caret = parse_req("^1.92.8"); ASSERT_TRUE(caret); - EXPECT_TRUE (matches(*caret, *parse_version("1.92.8"))); - EXPECT_FALSE(matches(*caret, *parse_version("1.92.8-docking"))); + auto caret = parse_req("^1.92.8"); expect(fatal(bool(caret))); + expect(matches(*caret, *parse_version("1.92.8"))); + expect(!(matches(*caret, *parse_version("1.92.8-docking")))); // `*` never reaches a pre-release either. - auto any = parse_req("*"); ASSERT_TRUE(any); - EXPECT_FALSE(matches(*any, *parse_version("1.92.8-docking"))); + auto any = parse_req("*"); expect(fatal(bool(any))); + expect(!(matches(*any, *parse_version("1.92.8-docking")))); // Naming a pre-release at the same numeric tuple opts in. - auto optIn = parse_req("^1.92.8-a"); ASSERT_TRUE(optIn); - EXPECT_TRUE (matches(*optIn, *parse_version("1.92.8-docking"))); + auto optIn = parse_req("^1.92.8-a"); expect(fatal(bool(optIn))); + expect(matches(*optIn, *parse_version("1.92.8-docking"))); // Exact still addresses it (this is the path `imgui = "1.92.8-docking"` // takes after try_merge_semver canonicalises the literal to "=..."). - auto exact = parse_req("=1.92.8-docking"); ASSERT_TRUE(exact); - EXPECT_TRUE (matches(*exact, *parse_version("1.92.8-docking"))); - EXPECT_FALSE(matches(*exact, *parse_version("1.92.8"))); -} + auto exact = parse_req("=1.92.8-docking"); expect(fatal(bool(exact))); + expect(matches(*exact, *parse_version("1.92.8-docking"))); + expect(!(matches(*exact, *parse_version("1.92.8")))); +}}; -TEST(VersionReq, CaretUpperBoundDoesNotLeakIntoTheNextMajorsPrerelease) { +UTest caret_upper_bound_does_not_leak_into_the_next_majors_prerelease{"VersionReq/CaretUpperBoundDoesNotLeakIntoTheNextMajorsPrerelease", [] { // 2.0.0-alpha sorts BELOW 2.0.0, so a plain `v < upper` bound admitted it. - auto r = parse_req("^1.2.3"); ASSERT_TRUE(r); - EXPECT_FALSE(matches(*r, *parse_version("2.0.0-alpha"))); - EXPECT_FALSE(matches(*r, *parse_version("2.0.0"))); - EXPECT_TRUE (matches(*r, *parse_version("1.9.9"))); -} + auto r = parse_req("^1.2.3"); expect(fatal(bool(r))); + expect(!(matches(*r, *parse_version("2.0.0-alpha")))); + expect(!(matches(*r, *parse_version("2.0.0")))); + expect(matches(*r, *parse_version("1.9.9"))); +}}; -TEST(VersionReq, FiveSegmentKeysAreNotTruncated) { +UTest five_segment_keys_are_not_truncated{"VersionReq/FiveSegmentKeysAreNotTruncated", [] { // jdk-corretto's scheme: ..... // Truncating at four made 25.0.4.7.1 and 25.0.4.7.2 compare EQUAL. - EXPECT_LT(*parse_version("25.0.4.7.1"), *parse_version("25.0.4.7.2")); - EXPECT_LT(*parse_version("21.0.12.8.1"), *parse_version("25.0.4.7.1")); - EXPECT_EQ(parse_version("25.0.4.7.1")->str(), "25.0.4.7.1"); + expect(lt(*parse_version("25.0.4.7.1"), *parse_version("25.0.4.7.2"))); + expect(lt(*parse_version("21.0.12.8.1"), *parse_version("25.0.4.7.1"))); + expect(eq(parse_version("25.0.4.7.1")->str(), "25.0.4.7.1")); // Insignificant trailing zeros still compare equal, at any length. - EXPECT_EQ(*parse_version("1.2"), *parse_version("1.2.0.0.0")); -} + expect(eq(*parse_version("1.2"), *parse_version("1.2.0.0.0"))); +}}; -TEST(VersionReq, BuildMetadataIsExcludedFromPrecedenceButParses) { +UTest build_metadata_is_excluded_from_precedence_but_parses{"VersionReq/BuildMetadataIsExcludedFromPrecedenceButParses", [] { auto v = parse_version("25.0.4+7"); - ASSERT_TRUE(v); - EXPECT_EQ(*v, *parse_version("25.0.4")); // SemVer §10 - EXPECT_EQ(v->str(), "25.0.4"); -} + expect(fatal(bool(v))); + expect(eq(*v, *parse_version("25.0.4"))); // SemVer §10 + expect(eq(v->str(), "25.0.4")); +}}; -TEST(VersionReq, UnorderableKeysAreRejectedRatherThanTruncated) { +UTest unorderable_keys_are_rejected_rather_than_truncated{"VersionReq/UnorderableKeysAreRejectedRatherThanTruncated", [] { // llama.cpp-style build numbers, and the two non-numeric keys the real // index publishes. Each must be an ERROR here (→ exact-match-only in the // resolver), never a silent parse that collapses onto something else. - EXPECT_FALSE(parse_version("b10069").has_value()); - EXPECT_FALSE(parse_version("latest").has_value()); - EXPECT_FALSE(parse_version("nightly").has_value()); - EXPECT_FALSE(parse_version("pre-v0.0.5").has_value()); + expect(!(parse_version("b10069").has_value())); + expect(!(parse_version("latest").has_value())); + expect(!(parse_version("nightly").has_value())); + expect(!(parse_version("pre-v0.0.5").has_value())); // Trailing garbage used to parse as 1.2.3 — and therefore compare EQUAL // to it, the same silent merge the extra segments exist to prevent. - EXPECT_FALSE(parse_version("1.2.3abc").has_value()); - EXPECT_FALSE(parse_version("1.2.").has_value()); - EXPECT_FALSE(parse_version("1.2.3-").has_value()); - EXPECT_FALSE(parse_version("1.2.3+").has_value()); -} + expect(!(parse_version("1.2.3abc").has_value())); + expect(!(parse_version("1.2.").has_value())); + expect(!(parse_version("1.2.3-").has_value())); + expect(!(parse_version("1.2.3+").has_value())); +}}; -TEST(VersionReq, ChooseAllReportsPrecedenceTies) { +UTest choose_all_reports_precedence_ties{"VersionReq/ChooseAllReportsPrecedenceTies", [] { // 25.0.4 and 25.0.4+7 are two addresses at one precedence. choose_all must // hand BOTH back so the resolver can refuse instead of guessing. std::vector avail = { @@ -344,23 +346,23 @@ TEST(VersionReq, ChooseAllReportsPrecedenceTies) { *parse_version("25.0.4+7"), *parse_version("21.0.12"), }; - auto r = parse_req("*"); ASSERT_TRUE(r); + auto r = parse_req("*"); expect(fatal(bool(r))); auto best = choose_all(*r, avail); - ASSERT_EQ(best.size(), 2u); - EXPECT_EQ(best[0], 0u); - EXPECT_EQ(best[1], 1u); + expect(fatal(eq(best.size(), 2u))); + expect(eq(best[0], 0u)); + expect(eq(best[1], 1u)); // The ordinary case stays a single answer. std::vector plain = { *parse_version("1.0.0"), *parse_version("1.2.0") }; - EXPECT_EQ(choose_all(*r, plain).size(), 1u); -} + expect(eq(choose_all(*r, plain).size(), 1u)); +}}; -TEST(VersionReq, McppOwnDateVersionsAreUnaffectedByPrereleaseSupport) { +UTest mcpp_own_date_versions_are_unaffected_by_prerelease_support{"VersionReq/McppOwnDateVersionsAreUnaffectedByPrereleaseSupport", [] { // E0006 (index floor) compares mcpp's own version through this parser. // Adding pre-release/metadata handling must not move any of it. - EXPECT_LT(*parse_version("2026.8.3.3"), *parse_version("2026.8.6.3")); - EXPECT_LT(*parse_version("2026.8.1"), *parse_version("2026.8.1.1")); - EXPECT_EQ(*parse_version("2026.8.1.0"), *parse_version("2026.8.1")); - EXPECT_FALSE(parse_version("2026.8.6.3")->isPrerelease()); - EXPECT_EQ(parse_version("2026.8.6.3")->str(), "2026.8.6.3"); -} + expect(lt(*parse_version("2026.8.3.3"), *parse_version("2026.8.6.3"))); + expect(lt(*parse_version("2026.8.1"), *parse_version("2026.8.1.1"))); + expect(eq(*parse_version("2026.8.1.0"), *parse_version("2026.8.1"))); + expect(!(parse_version("2026.8.6.3")->isPrerelease())); + expect(eq(parse_version("2026.8.6.3")->str(), "2026.8.6.3")); +}}; diff --git a/tests/unit/test_windows_command_line.cpp b/tests/unit/test_windows_command_line.cpp index 7a004df1b..a1ec746bd 100644 --- a/tests/unit/test_windows_command_line.cpp +++ b/tests/unit/test_windows_command_line.cpp @@ -1,8 +1,10 @@ -#include import std; +import mcpplibs.mtest; import mcpp.build.command_line; +using namespace mcpplibs::mtest; + // The cmd.exe quoting rules are the easiest thing in mcpp to get wrong and // the hardest to notice: on Linux and macOS the Windows branch is not even // compiled, so nothing here is exercised by an ordinary local run. These @@ -14,10 +16,10 @@ static std::string wrapped_once(std::string_view command) { return "\"" + std::string(command) + "\""; } -TEST(WindowsCommandLine, EmbeddedQuotesAreEscaped) { +UTest embedded_quotes_are_escaped{"WindowsCommandLine/EmbeddedQuotesAreEscaped", [] { auto q = mcpp::build::command_line::quote_windows("a\"b"); - EXPECT_EQ(q, "\"a\\\"b\""); -} + expect(eq(q, "\"a\\\"b\"")); +}}; // ─── A user-authored shell command (project `[hooks]`, #496) ───────────── // @@ -27,7 +29,7 @@ TEST(WindowsCommandLine, EmbeddedQuotesAreEscaped) { // not "the string looks plausible". static std::string cmd_c_tail_under_slash_s(std::string_view line) { constexpr std::string_view kPrefix = "cmd.exe /d /s /c "; - EXPECT_TRUE(line.starts_with(kPrefix)) << line; + expect(line.starts_with(kPrefix)) << line; std::string tail(line.substr(kPrefix.size())); if (tail.empty() || tail.front() != '"') return tail; // rule does not fire tail.erase(0, 1); @@ -39,38 +41,35 @@ static std::string cmd_c_tail_under_slash_s(std::string_view line) { // which is what quoting each argument by the CreateProcess rules gives) they // are no longer switches, and the command tail keeps a quote pair cmd never // consumes — the CI failure this shape exists to prevent. -TEST(WindowsCommandLine, ShellCommandKeepsCmdSwitchesBare) { +UTest shell_command_keeps_cmd_switches_bare{"WindowsCommandLine/ShellCommandKeepsCmdSwitchesBare", [] { auto line = mcpp::build::command_line::windows_shell_command_line("echo hi"); - EXPECT_TRUE(line.starts_with("cmd.exe /d /s /c ")) << line; - EXPECT_EQ(line.find("\"/c\""), std::string::npos) << line; -} + expect(line.starts_with("cmd.exe /d /s /c ")) << line; + expect(eq(line.find("\"/c\""), std::string::npos)) << line; +}}; // A redirect is the ordinary case for a hook that appends to a log, and it is // also the case argv quoting destroys: `>` inside a quoted argument is a // literal, not a redirect. -TEST(WindowsCommandLine, ShellCommandDeliversARedirectVerbatim) { +UTest shell_command_delivers_a_redirect_verbatim{"WindowsCommandLine/ShellCommandDeliversARedirectVerbatim", [] { constexpr std::string_view command = "echo start>>hooks.log"; - EXPECT_EQ(cmd_c_tail_under_slash_s(mcpp::build::command_line::windows_shell_command_line(command)), - command); -} + expect(eq(cmd_c_tail_under_slash_s(mcpp::build::command_line::windows_shell_command_line(command)), command)); +}}; // More than one interior quote pair is exactly where the /C rule bites, and // where the "wrap once" shape earns its keep: whatever the author wrote comes // back byte for byte. -TEST(WindowsCommandLine, ShellCommandDeliversQuotedPathsVerbatim) { +UTest shell_command_delivers_quoted_paths_verbatim{"WindowsCommandLine/ShellCommandDeliversQuotedPathsVerbatim", [] { constexpr std::string_view command = R"("C:\Program Files\notify\notify.exe" --title "build done")"; - EXPECT_EQ(cmd_c_tail_under_slash_s(mcpp::build::command_line::windows_shell_command_line(command)), - command); -} + expect(eq(cmd_c_tail_under_slash_s(mcpp::build::command_line::windows_shell_command_line(command)), command)); +}}; // The POSIX half must keep its own convention — a shared helper that // silently applied Windows quoting on Linux would break every sh command. -TEST(WindowsCommandLine, PosixQuotingIsUnaffected) { - EXPECT_EQ(mcpp::build::command_line::quote_posix("/home/my dir"), - "'/home/my dir'"); - EXPECT_EQ(mcpp::build::command_line::quote_posix("it's"), "'it'\\''s'"); -} +UTest posix_quoting_is_unaffected{"WindowsCommandLine/PosixQuotingIsUnaffected", [] { + expect(eq(mcpp::build::command_line::quote_posix("/home/my dir"), "'/home/my dir'")); + expect(eq(mcpp::build::command_line::quote_posix("it's"), "'it'\\''s'")); +}}; // ── An argument that must survive cmd.exe AND the child's argv parser ─────── @@ -167,34 +166,34 @@ constexpr std::string_view kJsonWithAFloor = } // namespace -TEST(WindowsCommandLine, PlainQuotingLetsCmdSeeARedirection) { +UTest plain_quoting_lets_cmd_see_a_redirection{"WindowsCommandLine/PlainQuotingLetsCmdSeeARedirection", [] { // The state before the fix, stated so the fix below is not asserting // against nothing. This is `shell::quote_windows`, which is correct for // the child and incomplete for cmd. auto line = "xlings.exe interface install_packages --args " + mcpp::build::command_line::quote_windows(kJsonWithAFloor); auto parsed = simulate_cmd_c(wrapped_once(line)); - EXPECT_TRUE(parsed.sawActiveMetacharacter) + expect(bool(parsed.sawActiveMetacharacter)) << "if this ever becomes false the simulator stopped modelling cmd, " "and the test below proves nothing"; -} +}}; -TEST(WindowsCommandLine, MetacharacterQuotingSurvivesBothParsers) { +UTest metacharacter_quoting_survives_both_parsers{"WindowsCommandLine/MetacharacterQuotingSurvivesBothParsers", [] { auto line = "xlings.exe interface install_packages --args " + mcpp::build::command_line::quote_windows_through_cmd(kJsonWithAFloor); auto parsed = simulate_cmd_c(wrapped_once(line)); - EXPECT_FALSE(parsed.sawActiveMetacharacter) + expect(!parsed.sawActiveMetacharacter) << "cmd would still read the `>` in the version floor as a redirection"; // xlings.exe / interface / install_packages / --args / auto argv = msvcrt_argv(parsed.passedOn); - ASSERT_EQ(argv.size(), 5u) << parsed.passedOn; - EXPECT_EQ(argv[4], kJsonWithAFloor) + expect(fatal(eq(argv.size(), 5u))) << parsed.passedOn; + expect(eq(argv[4], kJsonWithAFloor)) << "the child received something other than the JSON that was meant"; -} +}}; -TEST(WindowsCommandLine, NothingToEscapeMeansByteIdenticalToPlainQuoting) { +UTest nothing_to_escape_means_byte_identical_to_plain_quoting{"WindowsCommandLine/NothingToEscapeMeansByteIdenticalToPlainQuoting", [] { // The conservative half of the rule, and it is the half that was measured // the hard way: an earlier version escaped every metacharacter INCLUDING // the quotes, which is defensible on paper and broke every package fetch on @@ -204,22 +203,21 @@ TEST(WindowsCommandLine, NothingToEscapeMeansByteIdenticalToPlainQuoting) { R"({"targets":["mcpplibs:tpl-demo@1.0.0"],"yes":true})", R"({"targets":["compat:widget@1.38.1"],"yes":true})", R"(a plain path C:\Program Files\x)" }) { - EXPECT_EQ(mcpp::build::command_line::quote_windows_through_cmd(plain), - mcpp::build::command_line::quote_windows(plain)) + expect(eq(mcpp::build::command_line::quote_windows_through_cmd(plain), mcpp::build::command_line::quote_windows(plain))) << "a payload with no metacharacter acquired an escape: " << plain; } -} +}}; -TEST(WindowsCommandLine, MetacharacterQuotingIsUnchangedForPlainText) { +UTest metacharacter_quoting_is_unchanged_for_plain_text{"WindowsCommandLine/MetacharacterQuotingIsUnchangedForPlainText", [] { // A payload with nothing to escape must not acquire carets, so the common // case stays legible in a log. constexpr std::string_view plain = R"({"targets":["xim:shaderc@2026.3"]})"; auto quoted = mcpp::build::command_line::quote_windows_through_cmd(plain); auto parsed = simulate_cmd_c(wrapped_once("prog.exe --args " + quoted)); auto argv = msvcrt_argv(parsed.passedOn); - ASSERT_EQ(argv.size(), 3u) << parsed.passedOn; - EXPECT_EQ(argv[2], plain); -} + expect(fatal(eq(argv.size(), 3u))) << parsed.passedOn; + expect(eq(argv[2], plain)); +}}; // ─── The argv rules of CommandLineToArgvW (2026.10.5.2) ────────────────── // @@ -252,7 +250,7 @@ static std::vector argv_of(std::string_view line) { return out; } -TEST(WindowsCommandLine, EveryArgumentRoundTripsThroughTheArgvRules) { +UTest every_argument_round_trips_through_the_argv_rules{"WindowsCommandLine/EveryArgumentRoundTripsThroughTheArgvRules", [] { const std::vector args = { "plain", "C:\\Program Files (x86)\\Windows Kits\\10\\", // vcvars' WindowsSdkDir @@ -265,11 +263,11 @@ TEST(WindowsCommandLine, EveryArgumentRoundTripsThroughTheArgvRules) { }; std::string line; for (auto const& a : args) line += mcpp::build::command_line::quote_windows(a) + " "; - EXPECT_EQ(argv_of(line), args) << line; -} + expect(eq(argv_of(line), args)) << line; +}}; -TEST(WindowsCommandLine, AnArgumentEndingInABackslashDoesNotSwallowTheNext) { +UTest an_argument_ending_in_a_backslash_does_not_swallow_the_next{"WindowsCommandLine/AnArgumentEndingInABackslashDoesNotSwallowTheNext", [] { const auto line = mcpp::build::command_line::quote_windows("C:\\Kits\\10\\") + " " + mcpp::build::command_line::quote_windows("-o"); - EXPECT_EQ(argv_of(line), (std::vector{"C:\\Kits\\10\\", "-o"})) << line; -} + expect(eq(argv_of(line), (std::vector{"C:\\Kits\\10\\", "-o"}))) << line; +}}; diff --git a/tests/unit/test_windows_defaults.cpp b/tests/unit/test_windows_defaults.cpp index aa6d18905..843501563 100644 --- a/tests/unit/test_windows_defaults.cpp +++ b/tests/unit/test_windows_defaults.cpp @@ -1,12 +1,14 @@ -#include import std; +import mcpplibs.mtest; import mcpplibs.os.platform; import mcpp.toolchain.msvc; import mcpp.toolchain.triple; import mcpp.toolchain.registry; import mcpp.build.prepare; +using namespace mcpplibs::mtest; + // ── has_usable_msvc() ─────────────────────────────────────────────────────── // // The predicate that decides whether the MSVC ABI is a viable default on this @@ -14,22 +16,22 @@ import mcpp.build.prepare; // STL (Visual Studio's "Desktop development with C++" workload) nor the // Windows SDK, so a default that targets the MSVC ABI there can never build. -TEST(WindowsDefaults, HasUsableMsvcIsFalseOffWindows) { +UTest has_usable_msvc_is_false_off_windows{"WindowsDefaults/HasUsableMsvcIsFalseOffWindows", [] { if constexpr (mcpplibs::os::is_windows) { - GTEST_SKIP() << "windows behavior is covered by e2e 182"; + skip("windows behavior is covered by e2e 182"); } else { - EXPECT_FALSE(mcpp::toolchain::msvc::has_usable_msvc()); + expect(!mcpp::toolchain::msvc::has_usable_msvc()); } -} +}}; // The predicate must never disagree with the two probes it is defined as. // A drift here (e.g. someone "optimizing" it down to find_vs_install_path()) // silently reintroduces the half-installed-VS trap it exists to close. -TEST(WindowsDefaults, HasUsableMsvcAgreesWithItsParts) { +UTest has_usable_msvc_agrees_with_its_parts{"WindowsDefaults/HasUsableMsvcAgreesWithItsParts", [] { const bool both = mcpp::toolchain::msvc::find_std_module_source().has_value() && mcpp::toolchain::msvc::find_windows_sdk().has_value(); - EXPECT_EQ(mcpp::toolchain::msvc::has_usable_msvc(), both); -} + expect(eq(mcpp::toolchain::msvc::has_usable_msvc(), both)); +}}; // ── First-run pins ────────────────────────────────────────────────────────── // @@ -37,29 +39,29 @@ TEST(WindowsDefaults, HasUsableMsvcAgreesWithItsParts) { // ships no GCC, while on Windows clang targets the MSVC ABI and therefore // needs a Visual Studio that is not preinstalled. -TEST(WindowsDefaults, FirstRunPinsParse) { +UTest first_run_pins_parse{"WindowsDefaults/FirstRunPinsParse", [] { namespace pins = mcpp::toolchain::triple::pins; for (auto spec : { pins::kFirstRunMac, pins::kFirstRunWinMsvc, pins::kFirstRunWinGnu, pins::kFirstRunLinuxX86_64, pins::kFirstRunLinuxAarch64, pins::kFirstRunLinuxOther }) { auto parsed = mcpp::toolchain::parse_toolchain_spec(std::string(spec)); - ASSERT_TRUE(parsed.has_value()) << spec; - EXPECT_FALSE(parsed->version.empty()) << spec; + expect(fatal(parsed.has_value())) << spec; + expect(!(parsed->version.empty())) << spec; } -} +}}; // The GNU fallback must land on a target mcpp is willing to build for, and // the toolchain it names must equal that target's vocabulary pin — otherwise // the same decision is derived in two places and they will drift. -TEST(WindowsDefaults, GnuFallbackTargetIsVerifiedAndPinAgrees) { +UTest gnu_fallback_target_is_verified_and_pin_agrees{"WindowsDefaults/GnuFallbackTargetIsVerifiedAndPinAgrees", [] { namespace triple = mcpp::toolchain::triple; auto t = triple::parse(std::string(triple::pins::kFirstRunWinGnuTarget)); - ASSERT_TRUE(t.has_value()); + expect(fatal(t.has_value())); const auto* known = triple::find_known_target(*t); - ASSERT_NE(known, nullptr) << triple::pins::kFirstRunWinGnuTarget; - EXPECT_EQ(known->tier, "verified"); - EXPECT_EQ(known->pin, triple::pins::kFirstRunWinGnu); -} + expect(fatal(ne(known, nullptr))) << triple::pins::kFirstRunWinGnuTarget; + expect(eq(known->tier, "verified")); + expect(eq(known->pin, triple::pins::kFirstRunWinGnu)); +}}; // ── Intent classification ─────────────────────────────────────────────────── // @@ -67,19 +69,19 @@ TEST(WindowsDefaults, GnuFallbackTargetIsVerifiedAndPinAgrees) { // directions: call an explicit choice "mcpp's own" and a project that needs // the MSVC ABI silently gets a different one; call mcpp's own default // "explicit" and every user carrying a stale persisted default stays stuck. -TEST(WindowsDefaults, OriginClassification) { +UTest origin_classification{"WindowsDefaults/OriginClassification", [] { using mcpp::build::TcOrigin; using mcpp::build::tc_origin_is_user_explicit; - EXPECT_TRUE (tc_origin_is_user_explicit(TcOrigin::ManifestToolchain)); - EXPECT_TRUE (tc_origin_is_user_explicit(TcOrigin::TargetSection)); - EXPECT_FALSE(tc_origin_is_user_explicit(TcOrigin::GlobalDefault)); - EXPECT_FALSE(tc_origin_is_user_explicit(TcOrigin::TargetPin)); - EXPECT_FALSE(tc_origin_is_user_explicit(TcOrigin::FirstRun)); - EXPECT_FALSE(tc_origin_is_user_explicit(TcOrigin::None)); + expect(tc_origin_is_user_explicit(TcOrigin::ManifestToolchain)); + expect(tc_origin_is_user_explicit(TcOrigin::TargetSection)); + expect(!tc_origin_is_user_explicit(TcOrigin::GlobalDefault)); + expect(!tc_origin_is_user_explicit(TcOrigin::TargetPin)); + expect(!tc_origin_is_user_explicit(TcOrigin::FirstRun)); + expect(!tc_origin_is_user_explicit(TcOrigin::None)); // A compiler the dependency graph required is not the user's word either — // mcpp derived it, so the two paths that revise mcpp's own answers may. - EXPECT_FALSE(tc_origin_is_user_explicit(TcOrigin::GraphRequirement)); -} + expect(!tc_origin_is_user_explicit(TcOrigin::GraphRequirement)); +}}; // AND IT IS THE ONE ORIGIN THAT MAY NEVER BECOME THE MACHINE'S DEFAULT. // @@ -93,55 +95,55 @@ TEST(WindowsDefaults, OriginClassification) { // WINDOWS MACHINE. The e2e that checks `config.toml`'s sha256 runs where a // toolchain is already configured, so it never reaches either branch; here the // rule itself is the subject. -TEST(WindowsDefaults, OnlyMcppsOwnAnswersMayBePersisted) { +UTest only_mcpps_own_answers_may_be_persisted{"WindowsDefaults/OnlyMcppsOwnAnswersMayBePersisted", [] { using mcpp::build::TcOrigin; using mcpp::build::tc_origin_may_persist; - EXPECT_FALSE(tc_origin_may_persist(TcOrigin::GraphRequirement)); - EXPECT_TRUE (tc_origin_may_persist(TcOrigin::GlobalDefault)); - EXPECT_TRUE (tc_origin_may_persist(TcOrigin::TargetPin)); - EXPECT_TRUE (tc_origin_may_persist(TcOrigin::FirstRun)); - EXPECT_TRUE (tc_origin_may_persist(TcOrigin::None)); -} + expect(!tc_origin_may_persist(TcOrigin::GraphRequirement)); + expect(bool(tc_origin_may_persist(TcOrigin::GlobalDefault))); + expect(bool(tc_origin_may_persist(TcOrigin::TargetPin))); + expect(bool(tc_origin_may_persist(TcOrigin::FirstRun))); + expect(bool(tc_origin_may_persist(TcOrigin::None))); +}}; // The fallback target must be PE/GNU, not the host's MSVC-ABI triple — the // whole point is to leave the ABI that needs Visual Studio behind. -TEST(WindowsDefaults, GnuFallbackTargetIsWindowsGnu) { +UTest gnu_fallback_target_is_windows_gnu{"WindowsDefaults/GnuFallbackTargetIsWindowsGnu", [] { namespace triple = mcpp::toolchain::triple; auto t = triple::parse(std::string(triple::pins::kFirstRunWinGnuTarget)); - ASSERT_TRUE(t.has_value()); - EXPECT_EQ(t->os, "windows"); - EXPECT_EQ(t->env, "gnu"); - EXPECT_TRUE(t->is_windows_gnu()); -} + expect(fatal(t.has_value())); + expect(eq(t->os, "windows")); + expect(eq(t->env, "gnu")); + expect(t->is_windows_gnu()); +}}; -TEST(LinuxDefaults, NativeArm64UsesLlvmAndOtherDefaultsRemainStable) { +UTest native_arm64_uses_llvm_and_other_defaults_remain_stable{"LinuxDefaults/NativeArm64UsesLlvmAndOtherDefaultsRemainStable", [] { namespace pins = mcpp::toolchain::triple::pins; - EXPECT_EQ(pins::linux_default_toolchain("aarch64"), "llvm@23.1.3"); - EXPECT_EQ(pins::linux_default_toolchain("x86_64"), "gcc@16.1.0"); - EXPECT_EQ(pins::linux_default_toolchain("riscv64"), "gcc@15.1.0-musl"); + expect(eq(pins::linux_default_toolchain("aarch64"), "llvm@23.1.3")); + expect(eq(pins::linux_default_toolchain("x86_64"), "gcc@16.1.0")); + expect(eq(pins::linux_default_toolchain("riscv64"), "gcc@15.1.0-musl")); auto target = mcpp::toolchain::triple::parse("aarch64-linux-gnu"); - ASSERT_TRUE(target); + expect(fatal(bool(target))); auto row = mcpp::toolchain::triple::find_known_target(*target); - ASSERT_TRUE(row); - EXPECT_EQ(row->pin, pins::kFirstRunLinuxAarch64); - EXPECT_FALSE(target->pin_is_capability()); -} + expect(fatal(bool(row))); + expect(eq(row->pin, pins::kFirstRunLinuxAarch64)); + expect(!target->pin_is_capability()); +}}; // A Luban system's /etc/mcpp/config.toml names its own gcc on the host's // triple (gcc@16.1.0 + aarch64-linux-gnu); the aarch64-linux-gnu row's llvm // convention replaced it with a download (xim-pkgindex#986, editions CI on // arm). The configured pair for the host's own target is kept; a cross target // and a remembered default without a target are not this case. -TEST(LinuxDefaults, AConfiguredPairForTheHostsOwnTargetKeepsItsToolchain) { +UTest a_configured_pair_for_the_hosts_own_target_keeps_its_toolchain{"LinuxDefaults/AConfiguredPairForTheHostsOwnTargetKeepsItsToolchain", [] { namespace tr = mcpp::toolchain::triple; using mcpp::build::TcOrigin; using mcpp::build::configured_native_pair; auto arm = tr::parse("aarch64-linux-gnu"); auto x86 = tr::parse("x86_64-linux-gnu"); auto winGnu = tr::parse("x86_64-windows-gnu"); - ASSERT_TRUE(arm && x86 && winGnu); - EXPECT_TRUE(configured_native_pair(true, TcOrigin::GlobalDefault, *arm, *arm)); - EXPECT_FALSE(configured_native_pair(false, TcOrigin::GlobalDefault, *arm, *arm)) << "--target names it"; - EXPECT_FALSE(configured_native_pair(true, TcOrigin::FirstRun, *arm, *arm)) << "mcpp's own guess"; - EXPECT_FALSE(configured_native_pair(true, TcOrigin::GlobalDefault, *winGnu, *x86)) << "a cross target"; -} + expect(fatal(arm && x86 && winGnu)); + expect(bool(configured_native_pair(true, TcOrigin::GlobalDefault, *arm, *arm))); + expect(!configured_native_pair(false, TcOrigin::GlobalDefault, *arm, *arm)) << "--target names it"; + expect(!configured_native_pair(true, TcOrigin::FirstRun, *arm, *arm)) << "mcpp's own guess"; + expect(!configured_native_pair(true, TcOrigin::GlobalDefault, *winGnu, *x86)) << "a cross target"; +}}; diff --git a/tests/unit/test_wire.cpp b/tests/unit/test_wire.cpp index 87d53dab6..b8995daa8 100644 --- a/tests/unit/test_wire.cpp +++ b/tests/unit/test_wire.cpp @@ -9,12 +9,14 @@ // // Design: .agents/docs/2026-08-08-machine-readable-output-protocol-design.md -#include import std; +import mcpplibs.mtest; import mcpp.wire; import nlohmann.json; +using namespace mcpplibs::mtest; + namespace w = mcpp::wire; namespace { @@ -24,48 +26,48 @@ namespace { // the entry point: on older versions it is itself an unknown option. So the // envelope must be self-identifying, and these two keys are what identifies // it. -TEST(WireEnvelope, IsSelfIdentifying) { +UTest is_self_identifying{"WireEnvelope/IsSelfIdentifying", [] { auto j = w::to_json(w::Envelope{.kind = "mcpp.env"}); - ASSERT_TRUE(j.contains("schemaVersion")) << "a client detects by parsing"; - ASSERT_TRUE(j.contains("kind")); - EXPECT_EQ(j["schemaVersion"], w::kEnvelopeVersion); - EXPECT_EQ(j["kind"], "mcpp.env"); -} + expect(fatal(j.contains("schemaVersion"))) << "a client detects by parsing"; + expect(fatal(j.contains("kind"))); + expect(eq(j["schemaVersion"], w::kEnvelopeVersion)); + expect(eq(j["kind"], "mcpp.env")); +}}; -TEST(WireEnvelope, CarriesTheKindsOwnVersionSeparately) { +UTest carries_the_kinds_own_version_separately{"WireEnvelope/CarriesTheKindsOwnVersionSeparately", [] { auto j = w::to_json(w::Envelope{.kind = "mcpp.xpkg"}); // Separate from schemaVersion on purpose: one global number would make a // field added to mcpp.env move the version a client reads for mcpp.xpkg, // with no way to tell which actually changed. - ASSERT_TRUE(j.contains("kindVersion")); - EXPECT_EQ(j["kindVersion"], 1); -} + expect(fatal(j.contains("kindVersion"))); + expect(eq(j["kindVersion"], 1)); +}}; -TEST(WireEnvelope, AnUnknownKindHasNoVersion) { +UTest an_unknown_kind_has_no_version{"WireEnvelope/AnUnknownKindHasNoVersion", [] { auto j = w::to_json(w::Envelope{.kind = "mcpp.not-a-kind"}); - EXPECT_EQ(j["kindVersion"], 0) << "silently claiming version 1 would be a lie"; -} + expect(eq(j["kindVersion"], 0)) << "silently claiming version 1 would be a lie"; +}}; // Effects are named, not boolean. `init-mcpp-home` is something a client may // ignore; `exec-build-script` runs code out of the workspace and is the whole // reason an untrusted-workspace gate exists. One bool cannot separate them. -TEST(WireEnvelope, EffectsAreNamed) { +UTest effects_are_named{"WireEnvelope/EffectsAreNamed", [] { auto j = w::to_json(w::Envelope{ .kind = "mcpp.env", .effects = {w::Effect::InitMcppHome, w::Effect::ExecBuildScript}}); - ASSERT_EQ(j["effects"].size(), 2u); - EXPECT_EQ(j["effects"][0], "init-mcpp-home"); - EXPECT_EQ(j["effects"][1], "exec-build-script"); -} + expect(fatal(eq(j["effects"].size(), 2u))); + expect(eq(j["effects"][0], "init-mcpp-home")); + expect(eq(j["effects"][1], "exec-build-script")); +}}; -TEST(WireEnvelope, NoEffectsIsAnEmptyArrayNotAbsent) { +UTest no_effects_is_an_empty_array_not_absent{"WireEnvelope/NoEffectsIsAnEmptyArrayNotAbsent", [] { auto j = w::to_json(w::Envelope{.kind = "mcpp.xpkg"}); - ASSERT_TRUE(j.contains("effects")); - EXPECT_TRUE(j["effects"].is_array()); - EXPECT_TRUE(j["effects"].empty()) << "absent would mean 'unknown', not 'none'"; -} + expect(fatal(j.contains("effects"))); + expect(j["effects"].is_array()); + expect(j["effects"].empty()) << "absent would mean 'unknown', not 'none'"; +}}; -TEST(WireEnvelope, DiagnosticsCarryCodeAndPosition) { +UTest diagnostics_carry_code_and_position{"WireEnvelope/DiagnosticsCarryCodeAndPosition", [] { w::Diagnostic d{ .code = "MCPP_MANIFEST_UNKNOWN_KEY", .severity = w::Severity::Warning, @@ -74,112 +76,112 @@ TEST(WireEnvelope, DiagnosticsCarryCodeAndPosition) { .range = w::Range{{3, 1}, {3, 9}}, }; auto j = w::to_json(w::Envelope{.kind = "mcpp.env", .diagnostics = {d}}); - ASSERT_EQ(j["diagnostics"].size(), 1u); + expect(fatal(eq(j["diagnostics"].size(), 1u))); auto const& g = j["diagnostics"][0]; - EXPECT_EQ(g["code"], "MCPP_MANIFEST_UNKNOWN_KEY"); - EXPECT_EQ(g["severity"], "warning"); - EXPECT_EQ(g["source"], "mcpp"); - EXPECT_EQ(g["path"], "mcpp.toml"); - EXPECT_EQ(g["range"]["start"]["line"], 3); - EXPECT_EQ(g["range"]["end"]["column"], 9); -} + expect(eq(g["code"], "MCPP_MANIFEST_UNKNOWN_KEY")); + expect(eq(g["severity"], "warning")); + expect(eq(g["source"], "mcpp")); + expect(eq(g["path"], "mcpp.toml")); + expect(eq(g["range"]["start"]["line"], 3)); + expect(eq(g["range"]["end"]["column"], 9)); +}}; // A diagnostic without a location must not invent one: absent keys, not // zeros. `line: 0` would send a client to a position that does not exist. -TEST(WireEnvelope, ADiagnosticWithoutALocationOmitsIt) { +UTest a_diagnostic_without_a_location_omits_it{"WireEnvelope/ADiagnosticWithoutALocationOmitsIt", [] { auto j = w::to_json(w::Envelope{ .kind = "mcpp.env", .diagnostics = {{.code = "MCPP_X", .message = "no place"}}}); auto const& g = j["diagnostics"][0]; - EXPECT_FALSE(g.contains("path")); - EXPECT_FALSE(g.contains("range")); -} + expect(!g.contains("path")); + expect(!g.contains("range")); +}}; -TEST(WireEnvelope, ReportsMcppVersionAndProtocolWindow) { +UTest reports_mcpp_version_and_protocol_window{"WireEnvelope/ReportsMcppVersionAndProtocolWindow", [] { auto j = w::to_json(w::Envelope{.kind = "mcpp.env"}); - EXPECT_FALSE(j["mcpp"]["version"].get().empty()); - EXPECT_EQ(j["mcpp"]["protocol"]["min"], w::kEnvelopeVersion); - EXPECT_EQ(j["mcpp"]["protocol"]["max"], w::kEnvelopeVersion); -} + expect(!(j["mcpp"]["version"].get().empty())); + expect(eq(j["mcpp"]["protocol"]["min"], w::kEnvelopeVersion)); + expect(eq(j["mcpp"]["protocol"]["max"], w::kEnvelopeVersion)); +}}; // Every envelope carries the features beside the version, so a client's one // `self env` (or any job's envelope) says what the project's mcpp can do // (design 2026-10-30 part 2 §4.3); the list is the protocol document's. -TEST(WireEnvelope, CarriesTheFeaturesOfTheProtocolDocument) { +UTest carries_the_features_of_the_protocol_document{"WireEnvelope/CarriesTheFeaturesOfTheProtocolDocument", [] { auto j = w::to_json(w::Envelope{.kind = "mcpp.env"}); - ASSERT_TRUE(j["mcpp"].contains("features") && j["mcpp"]["features"].is_array()); - EXPECT_EQ(j["mcpp"]["features"], w::protocol_document({})["features"]); -} + expect(fatal(j["mcpp"].contains("features") && j["mcpp"]["features"].is_array())); + expect(eq(j["mcpp"]["features"], w::protocol_document({})["features"])); +}}; // ── Negotiation ──────────────────────────────────────────────────────────── -TEST(WireProtocol, ListsKindsAndTheirVersions) { +UTest lists_kinds_and_their_versions{"WireProtocol/ListsKindsAndTheirVersions", [] { auto j = w::protocol_document({ {"self env", {w::Effect::InitMcppHome}}, {"xpkg parse", {}}, }); - ASSERT_TRUE(j.contains("kinds")); + expect(fatal(j.contains("kinds"))); // `{min,max}` alone cannot answer "do you speak mcpp.env, at what // version" — which is the question a client actually has. - EXPECT_EQ(j["kinds"]["mcpp.env"], 1); - EXPECT_EQ(j["kinds"]["mcpp.xpkg"], 1); -} + expect(eq(j["kinds"]["mcpp.env"], 1)); + expect(eq(j["kinds"]["mcpp.xpkg"], 1)); +}}; // The gate has to decide BEFORE running. By the time an envelope arrives, // whatever it describes has already happened — so the effects have to be // available statically too. -TEST(WireProtocol, DeclaresEffectsPerCommandStatically) { +UTest declares_effects_per_command_statically{"WireProtocol/DeclaresEffectsPerCommandStatically", [] { auto j = w::protocol_document({ {"self env", {w::Effect::InitMcppHome}}, {"xpkg parse", {}}, }); - ASSERT_TRUE(j["commands"].contains("self env")); - EXPECT_EQ(j["commands"]["self env"]["effects"][0], "init-mcpp-home"); - EXPECT_TRUE(j["commands"]["xpkg parse"]["effects"].empty()) + expect(fatal(j["commands"].contains("self env"))); + expect(eq(j["commands"]["self env"]["effects"][0], "init-mcpp-home")); + expect(j["commands"]["xpkg parse"]["effects"].empty()) << "measured on a fresh MCPP_HOME: it creates nothing"; -} +}}; // A client gates on a feature, not on a version number (design 2026-10-30 // part 2 §4.3): the list is present, and the names are the stable ones. -TEST(WireProtocol, NamesTheFeaturesAClientGatesOn) { +UTest names_the_features_a_client_gates_on{"WireProtocol/NamesTheFeaturesAClientGatesOn", [] { auto j = w::protocol_document({}); - ASSERT_TRUE(j.contains("features") && j["features"].is_array()); + expect(fatal(j.contains("features") && j["features"].is_array())); std::vector names; for (auto const& f : j["features"]) names.push_back(f.get()); - EXPECT_NE(std::ranges::find(names, "target-sets"), names.end()); - EXPECT_NE(std::ranges::find(names, "pkg-format-list"), names.end()); - EXPECT_NE(std::ranges::find(names, "test-launch"), names.end()); - EXPECT_NE(std::ranges::find(names, "launch-stream"), names.end()); - EXPECT_NE(std::ranges::find(names, "test-protocol"), names.end()); -} - -TEST(WireProtocol, IsItselfSelfIdentifying) { + expect(ne(std::ranges::find(names, "target-sets"), names.end())); + expect(ne(std::ranges::find(names, "pkg-format-list"), names.end())); + expect(ne(std::ranges::find(names, "test-launch"), names.end())); + expect(ne(std::ranges::find(names, "launch-stream"), names.end())); + expect(ne(std::ranges::find(names, "test-protocol"), names.end())); +}}; + +UTest is_itself_self_identifying{"WireProtocol/IsItselfSelfIdentifying", [] { auto j = w::protocol_document({}); - EXPECT_EQ(j["kind"], "mcpp.protocol"); - EXPECT_TRUE(j.contains("schemaVersion")); -} + expect(eq(j["kind"], "mcpp.protocol")); + expect(j.contains("schemaVersion")); +}}; // ── Format selection ─────────────────────────────────────────────────────── -TEST(WireFormat, KnowsJsonAndRefusesTheRest) { - EXPECT_EQ(w::parse_format("json"), w::Format::Json); - EXPECT_FALSE(w::parse_format("yaml").has_value()); - EXPECT_FALSE(w::parse_format("ndjson").has_value()) << "reserved, not supported"; - EXPECT_FALSE(w::parse_format("").has_value()); -} +UTest knows_json_and_refuses_the_rest{"WireFormat/KnowsJsonAndRefusesTheRest", [] { + expect(eq(w::parse_format("json"), w::Format::Json)); + expect(!(w::parse_format("yaml").has_value())); + expect(!(w::parse_format("ndjson").has_value())) << "reserved, not supported"; + expect(!(w::parse_format("").has_value())); +}}; -TEST(WireFormat, TheRefusalNamesWhatIsSupported) { +UTest the_refusal_names_what_is_supported{"WireFormat/TheRefusalNamesWhatIsSupported", [] { auto m = w::unsupported_format("yaml"); - EXPECT_NE(m.find("yaml"), std::string::npos); - EXPECT_NE(m.find("json"), std::string::npos) << "tell the client what to ask for"; -} + expect(ne(m.find("yaml"), std::string::npos)); + expect(ne(m.find("json"), std::string::npos)) << "tell the client what to ask for"; +}}; // LegacyJson is a distinct format, not a synonym. `--json` has shipped with a // bare payload (`cache list --json` is `{root, entries}` at the top level, and // this repo's e2e asserts those keys); enveloping it would break consumers. -TEST(WireFormat, LegacyJsonIsNotTheSameFormatAsJson) { - EXPECT_NE(w::Format::LegacyJson, w::Format::Json); -} +UTest legacy_json_is_not_the_same_format_as_json{"WireFormat/LegacyJsonIsNotTheSameFormatAsJson", [] { + expect(ne(w::Format::LegacyJson, w::Format::Json)); +}}; // ── Golden shapes, one per kind ──────────────────────────────────────────── // @@ -198,7 +200,7 @@ void expect_has_keys(const nlohmann::json& obj, std::initializer_list keys, std::string_view what) { for (auto k : keys) - EXPECT_TRUE(obj.contains(k)) + expect(obj.contains(k)) << what << " lost published key '" << k << "'. Removing or renaming one is a breaking change: bump the " "kind's version in mcpp.wire and say so in " @@ -207,15 +209,15 @@ void expect_has_keys(const nlohmann::json& obj, } // namespace -TEST(WireGolden, EnvelopeKeySet) { +UTest envelope_key_set{"WireGolden/EnvelopeKeySet", [] { auto j = w::to_json(w::Envelope{.kind = "mcpp.env"}); expect_has_keys(j, {"schemaVersion", "kind", "kindVersion", "effects", "mcpp", "data", "diagnostics"}, "the envelope"); expect_has_keys(j["mcpp"], {"version", "protocol", "features"}, "envelope.mcpp"); expect_has_keys(j["mcpp"]["protocol"], {"min", "max"}, "envelope.mcpp.protocol"); -} +}}; -TEST(WireGolden, DiagnosticKeySet) { +UTest diagnostic_key_set{"WireGolden/DiagnosticKeySet", [] { auto j = w::to_json(w::Envelope{ .kind = "mcpp.env", .diagnostics = {{.code = "C", .message = "m", .path = "p", @@ -226,19 +228,19 @@ TEST(WireGolden, DiagnosticKeySet) { expect_has_keys(j["diagnostics"][0]["range"], {"start", "end"}, "a range"); expect_has_keys(j["diagnostics"][0]["range"]["start"], {"line", "column"}, "a position"); -} +}}; -TEST(WireGolden, ProtocolDocumentKeySet) { +UTest protocol_document_key_set{"WireGolden/ProtocolDocumentKeySet", [] { auto j = w::protocol_document({{"self env", {w::Effect::InitMcppHome}}}); expect_has_keys(j, {"schemaVersion", "kind", "mcpp", "envelope", "kinds", "commands", "features"}, "the protocol document"); expect_has_keys(j["envelope"], {"min", "max"}, "protocol.envelope"); expect_has_keys(j["commands"]["self env"], {"effects"}, "a command entry"); -} +}}; // Every effect name is part of the contract: a client matches on these // strings, so renaming one silently changes what a gate lets through. -TEST(WireGolden, EffectNamesAreStable) { +UTest effect_names_are_stable{"WireGolden/EffectNamesAreStable", [] { struct { w::Effect e; const char* name; } const expected[]{ {w::Effect::InitMcppHome, "init-mcpp-home"}, {w::Effect::ReadProject, "read-project"}, @@ -248,41 +250,41 @@ TEST(WireGolden, EffectNamesAreStable) { {w::Effect::ExecBuildScript, "exec-build-script"}, }; for (auto const& x : expected) - EXPECT_EQ(w::effect_name(x.e), x.name) + expect(eq(w::effect_name(x.e), x.name)) << "effect names are matched by clients; renaming one changes " "what an untrusted-workspace gate admits"; -} +}}; -TEST(WireGolden, SeverityNamesAreStable) { - EXPECT_EQ(w::severity_name(w::Severity::Error), "error"); - EXPECT_EQ(w::severity_name(w::Severity::Warning), "warning"); - EXPECT_EQ(w::severity_name(w::Severity::Note), "note"); -} +UTest severity_names_are_stable{"WireGolden/SeverityNamesAreStable", [] { + expect(eq(w::severity_name(w::Severity::Error), "error")); + expect(eq(w::severity_name(w::Severity::Warning), "warning")); + expect(eq(w::severity_name(w::Severity::Note), "note")); +}}; // The kinds this build claims to speak. A client reads this list to decide // whether to bother calling; dropping one silently is a breaking change. -TEST(WireGolden, DeclaredKinds) { +UTest declared_kinds{"WireGolden/DeclaredKinds", [] { auto j = w::protocol_document({}); expect_has_keys(j["kinds"], {"mcpp.env", "mcpp.xpkg", "mcpp.cache", "mcpp.build-database"}, "the kind list"); - EXPECT_EQ(j["kinds"]["mcpp.build-database"], 1); -} + expect(eq(j["kinds"]["mcpp.build-database"], 1)); +}}; // A failure that leaves nothing to describe omits `data`: a consumer of the // build database reads "no data" as "the command failed" (S2 0.2.0 section 3.4), // so an empty object in its place would read as a success with nothing in it. -TEST(WireGolden, NullDataIsOmitted) { +UTest null_data_is_omitted{"WireGolden/NullDataIsOmitted", [] { auto failed = w::to_json(w::Envelope{ .kind = "mcpp.build-database", .effects = {w::Effect::ReadProject}, .data = nullptr, .diagnostics = {{"MCPP_BUILD_DATABASE_PLAN_FAILED", w::Severity::Error, "x"}}, }); - EXPECT_FALSE(failed.contains("data")) << failed.dump(); - EXPECT_TRUE(failed.contains("diagnostics")); + expect(!failed.contains("data")) << failed.dump(); + expect(failed.contains("diagnostics")); auto ok = w::to_json(w::Envelope{.kind = "mcpp.env"}); - EXPECT_TRUE(ok.contains("data")) << ok.dump(); -} + expect(ok.contains("data")) << ok.dump(); +}}; } // namespace diff --git a/tests/unit/test_workspace_inheritance.cpp b/tests/unit/test_workspace_inheritance.cpp index a22ae5249..fd1e52124 100644 --- a/tests/unit/test_workspace_inheritance.cpp +++ b/tests/unit/test_workspace_inheritance.cpp @@ -1,10 +1,13 @@ -#include import std; +import mcpplibs.mtest; +import mcpplibs.os.stdio; import mcpp.manifest; import mcpp.project; import mcpp.build.prepare; +using namespace mcpplibs::mtest; + // #690. The unit-level halves of the workspace-inheritance repair: the // `[workspace.build]` key table (one statement of the inheritable subset), the // keyed `defines` fold, and the snapshot's post-condition. The end-to-end @@ -53,133 +56,131 @@ std::vector as_strings(const std::vector& v) // F3: a key the parser assigns and inherits must also be a key it accepts. // `ios_deployment_target` was the counterexample. -TEST(WorkspaceInheritance, EveryTableRowIsParsedAndInherited) { +UTest every_table_row_is_parsed_and_inherited{"WorkspaceInheritance/EveryTableRowIsParsedAndInherited", [] { auto ws = mcpp::manifest::parse_string(workspace_declaring_every_key()); - ASSERT_TRUE(ws.has_value()) << ws.error().format(); - ASSERT_TRUE(ws->workspace.inherited.buildPresent); + if (auto&& failed = expect(fatal(ws.has_value())); !failed) failed << ws.error().format(); + expect(fatal(bool(ws->workspace.inherited.buildPresent))); auto member = mcpp::manifest::parse_string( "[package]\nname = \"m\"\n", "m/mcpp.toml", {.insideWorkspace = true}); - ASSERT_TRUE(member.has_value()) << member.error().format(); + if (auto&& failed = expect(fatal(member.has_value())); !failed) failed << member.error().format(); const std::filesystem::path wsRoot = "/ws"; mcpp::project::inherit_workspace_build(*member, *ws, wsRoot); std::size_t scalars = 0; for (auto const& row : mcpp::manifest::kWorkspaceBuildKeys) { - SCOPED_TRACE(std::string(row.key)); + /* trace: std::string(row.key) */; auto const& parsed = ws->workspace.inherited.build; auto const& inherited = member->buildConfig; if (auto const* f = std::get_if mcpp::manifest::BuildConfig::*>(&row.field)) { const std::vector want{std::format("ws_{}", row.key)}; - EXPECT_EQ(parsed.**f, want); - EXPECT_EQ(inherited.**f, want); + expect(eq(parsed.**f, want)); + expect(eq(inherited.**f, want)); } else if (auto const* f = std::get_if mcpp::manifest::BuildConfig::*>(&row.field)) { - EXPECT_EQ(as_strings(parsed.**f), - std::vector{std::format("ws_{}", row.key)}); + expect(eq(as_strings(parsed.**f), std::vector{std::format("ws_{}", row.key)})); // Relative include directories are anchored at the workspace root. - EXPECT_EQ(as_strings(inherited.**f), - std::vector{ - (wsRoot / std::format("ws_{}", row.key)).generic_string()}); + expect(eq(as_strings(inherited.**f), std::vector{ + (wsRoot / std::format("ws_{}", row.key)).generic_string()})); } else if (auto const* f = std::get_if(&row.field)) { ++scalars; auto it = kSampleScalar.find(std::string(row.key)); - ASSERT_NE(it, kSampleScalar.end()) << "no sample value for a new scalar row"; - EXPECT_EQ(parsed.**f, it->second); - EXPECT_EQ(inherited.**f, it->second); + expect(fatal(ne(it, kSampleScalar.end()))) << "no sample value for a new scalar row"; + expect(eq(parsed.**f, it->second)); + expect(eq(inherited.**f, it->second)); } } - EXPECT_EQ(scalars, kSampleScalar.size()); -} + expect(eq(scalars, kSampleScalar.size())); +}}; -TEST(WorkspaceInheritance, UnknownKeyErrorListsTheTable) { +UTest unknown_key_error_lists_the_table{"WorkspaceInheritance/UnknownKeyErrorListsTheTable", [] { auto ws = mcpp::manifest::parse_string( "[workspace]\nmembers = [\"m\"]\n\n[workspace.build]\nno_such_key = []\n"); - ASSERT_FALSE(ws.has_value()); + expect(fatal(!ws.has_value())); const auto text = ws.error().format(); for (auto const& row : mcpp::manifest::kWorkspaceBuildKeys) - EXPECT_NE(text.find(row.key), std::string::npos) << row.key; -} + expect(ne(text.find(row.key), std::string::npos)) << row.key; +}}; // W7: `defines` is a set keyed by macro name. -TEST(DefinesFold, LaterEntryReplacesEarlierInPlace) { +UTest later_entry_replaces_earlier_in_place{"DefinesFold/LaterEntryReplacesEarlierInPlace", [] { mcpp::manifest::BuildConfig bc; bc.defines = {"X=1", "Y", "X=2"}; mcpp::build::fold_build_defines_into_flags(bc); - EXPECT_EQ(bc.cxxflags, (std::vector{"-DX=2", "-DY"})); - EXPECT_EQ(bc.cflags, (std::vector{"-DX=2", "-DY"})); - EXPECT_TRUE(bc.defines.empty()); -} + expect(eq(bc.cxxflags, (std::vector{"-DX=2", "-DY"}))); + expect(eq(bc.cflags, (std::vector{"-DX=2", "-DY"}))); + expect(bc.defines.empty()); +}}; -TEST(DefinesFold, BangNameRemovesTheName) { +UTest bang_name_removes_the_name{"DefinesFold/BangNameRemovesTheName", [] { mcpp::manifest::BuildConfig bc; bc.defines = {"X=1", "!X", "Z"}; mcpp::build::fold_build_defines_into_flags(bc); - EXPECT_EQ(bc.cxxflags, (std::vector{"-DZ"})); -} + expect(eq(bc.cxxflags, (std::vector{"-DZ"}))); +}}; -TEST(DefinesFold, EntrySupersedesADashDWordInTheFlagLists) { +UTest entry_supersedes_a_dash_d_word_in_the_flag_lists{"DefinesFold/EntrySupersedesADashDWordInTheFlagLists", [] { mcpp::manifest::BuildConfig bc; bc.cxxflags = {"-DX=1", "-O2", "-DXY=1"}; bc.cflags = {"-DX"}; bc.defines = {"X=3"}; mcpp::build::fold_build_defines_into_flags(bc); // `-DXY` is another name and stays; `-DX=1` and `-DX` are superseded. - EXPECT_EQ(bc.cxxflags, (std::vector{"-O2", "-DXY=1", "-DX=3"})); - EXPECT_EQ(bc.cflags, (std::vector{"-DX=3"})); -} + expect(eq(bc.cxxflags, (std::vector{"-O2", "-DXY=1", "-DX=3"}))); + expect(eq(bc.cflags, (std::vector{"-DX=3"}))); +}}; // The layer-conditional pass folds a second time with only its own entries. -TEST(DefinesFold, SecondPassRemovesAWordTheFirstPassFolded) { +UTest second_pass_removes_a_word_the_first_pass_folded{"DefinesFold/SecondPassRemovesAWordTheFirstPassFolded", [] { mcpp::manifest::BuildConfig bc; bc.defines = {"X=1", "W"}; mcpp::build::fold_build_defines_into_flags(bc); bc.defines = {"!X"}; mcpp::build::fold_build_defines_into_flags(bc); - EXPECT_EQ(bc.cxxflags, (std::vector{"-DW"})); -} + expect(eq(bc.cxxflags, (std::vector{"-DW"}))); +}}; -TEST(DefinesFold, ValueWithSpaceStaysOneWordAndKeyed) { +UTest value_with_space_stays_one_word_and_keyed{"DefinesFold/ValueWithSpaceStaysOneWordAndKeyed", [] { mcpp::manifest::BuildConfig bc; bc.defines = {"N=\"a b\""}; mcpp::build::fold_build_defines_into_flags(bc); - ASSERT_EQ(bc.cxxflags.size(), 1u); + expect(fatal(eq(bc.cxxflags.size(), 1u))); bc.defines = {"N=2"}; mcpp::build::fold_build_defines_into_flags(bc); - EXPECT_EQ(bc.cxxflags, (std::vector{"-DN=2"})); -} + expect(eq(bc.cxxflags, (std::vector{"-DN=2"}))); +}}; // W1: the snapshot refuses a manifest whose `defines` were not folded. -TEST(SnapshotPostcondition, UnfoldedDefinesAreAnInternalError) { +UTest unfolded_defines_are_an_internal_error{"SnapshotPostcondition/UnfoldedDefinesAreAnInternalError", [] { mcpp::manifest::Manifest m; m.package.name = "lib"; - EXPECT_FALSE(mcpp::build::unfolded_defines_error(m).has_value()); + expect(!(mcpp::build::unfolded_defines_error(m).has_value())); m.buildConfig.defines = {"WORKSPACE_DEFINE=1"}; auto err = mcpp::build::unfolded_defines_error(m); - ASSERT_TRUE(err.has_value()); - EXPECT_NE(err->find("internal error"), std::string::npos); - EXPECT_NE(err->find("'lib'"), std::string::npos); - EXPECT_NE(err->find("WORKSPACE_DEFINE=1"), std::string::npos); + expect(fatal(err.has_value())); + expect(ne(err->find("internal error"), std::string::npos)); + expect(ne(err->find("'lib'"), std::string::npos)); + expect(ne(err->find("WORKSPACE_DEFINE=1"), std::string::npos)); mcpp::build::fold_build_defines_into_flags(m.buildConfig); - EXPECT_FALSE(mcpp::build::unfolded_defines_error(m).has_value()); -} + expect(!(mcpp::build::unfolded_defines_error(m).has_value())); +}}; // #713. A member inherits the workspace root's `[xlings.workspace]` entries, // conditional rows included; a package the member declares itself keeps the // member's address, because the nearer declaration wins (SPEC-004 §4.5). -TEST(WorkspaceXlings, AMemberInheritsTheRootsEntriesAndItsOwnWins) { +UTest a_member_inherits_the_roots_entries_and_its_own_wins{"WorkspaceXlings/AMemberInheritsTheRootsEntriesAndItsOwnWins", [] { auto ws = mcpp::manifest::parse_string( "[workspace]\nmembers = [\"m\"]\n\n" "[xlings.workspace]\nninja = \"1.12.1\"\ncmake = \"3.30.0\"\n\n" "[target.'cfg(os = \"linux\")'.xlings.workspace]\npatchelf = \"0.18.0\"\n"); - ASSERT_TRUE(ws.has_value()) << ws.error().format(); + if (auto&& failed = expect(fatal(ws.has_value())); !failed) failed << ws.error().format(); auto member = mcpp::manifest::parse_string( "[package]\nname = \"m\"\nversion = \"0.1.0\"\n\n" "[xlings.workspace]\ncmake = \"3.31.0\"\n", "m/mcpp.toml", {.insideWorkspace = true}); - ASSERT_TRUE(member.has_value()) << member.error().format(); + if (auto&& failed = expect(fatal(member.has_value())); !failed) failed << member.error().format(); mcpp::project::inherit_workspace_xlings(*member, *ws); @@ -188,41 +189,41 @@ TEST(WorkspaceXlings, AMemberInheritsTheRootsEntriesAndItsOwnWins) { return a.find(needle) != std::string::npos; }); }; - EXPECT_TRUE(has("ninja@1.12.1")); - EXPECT_TRUE(has("cmake@3.31.0")); - EXPECT_FALSE(has("cmake@3.30.0")); + expect(bool(has("ninja@1.12.1"))); + expect(bool(has("cmake@3.31.0"))); + expect(!has("cmake@3.30.0")); // The conditional row travels as a row, decided by its selector at merge time. bool rowCarried = false; for (auto const& cc : member->conditionalConfigs) for (auto const& a : cc.xlings.deps) rowCarried = rowCarried || a.find("patchelf@0.18.0") != std::string::npos; - EXPECT_TRUE(rowCarried); -} + expect(bool(rowCarried)); +}}; // #714. An entry that says `workspace = true` and that no workspace resolved is // refused by name, in every dependency table. -TEST(WorkspaceDependency, AnUnresolvedWorkspaceEntryIsNamed) { +UTest an_unresolved_workspace_entry_is_named{"WorkspaceDependency/AnUnresolvedWorkspaceEntryIsNamed", [] { for (std::string_view table : {"dependencies", "dev-dependencies", "build-dependencies"}) { - SCOPED_TRACE(std::string(table)); + /* trace: std::string(table) */; auto m = mcpp::manifest::parse_string(std::format( "[package]\nname = \"m\"\nversion = \"0.1.0\"\n\n[{}]\nfmt = {{ workspace = true }}\n", table), "m/mcpp.toml", {.insideWorkspace = true}); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); auto err = mcpp::project::unresolved_workspace_dependency_error(*m, "/p/m"); - ASSERT_TRUE(err.has_value()); - EXPECT_NE(err->find(std::format("[{}] fmt", table)), std::string::npos) << *err; - EXPECT_NE(err->find("members"), std::string::npos) << *err; + expect(fatal(err.has_value())); + if (auto&& failed = expect(ne(err->find(std::format("[{}] fmt", table)), std::string::npos)); !failed) failed << *err; + if (auto&& failed = expect(ne(err->find("members"), std::string::npos)); !failed) failed << *err; } auto resolved = mcpp::manifest::parse_string( "[package]\nname = \"m\"\nversion = \"0.1.0\"\n\n[dependencies]\nfmt = \"11.0.0\"\n"); - ASSERT_TRUE(resolved.has_value()); - EXPECT_FALSE(mcpp::project::unresolved_workspace_dependency_error(*resolved, "/p/m")); -} + expect(fatal(resolved.has_value())); + expect(!mcpp::project::unresolved_workspace_dependency_error(*resolved, "/p/m")); +}}; // #710. A host tool's toolchain is the one its own build would use: its host // row, then `[toolchain]`, each after the root-position keys of the workspace // that lists it. A member tool that declares nothing takes the workspace's. -TEST(HostToolToolchain, AMemberToolReadsItsWorkspaceToolchain) { +UTest a_member_tool_reads_its_workspace_toolchain{"HostToolToolchain/AMemberToolReadsItsWorkspaceToolchain", [] { namespace fs = std::filesystem; const auto root = fs::temp_directory_path() / std::format("mcpp-710-{:x}", std::random_device{}()); @@ -235,27 +236,27 @@ TEST(HostToolToolchain, AMemberToolReadsItsWorkspaceToolchain) { write(root / "tool" / "mcpp.toml", "[package]\nname = \"tool\"\nversion = \"0.1.0\"\n"); auto tool = mcpp::manifest::load(root / "tool" / "mcpp.toml"); - ASSERT_TRUE(tool.has_value()); + expect(fatal(tool.has_value())); // Compared through the value: with clang and the MSVC STL, constructing // std::optional from a literal fails to instantiate in this // translation unit, which includes gtest's headers and imports std. auto tc15 = mcpp::build::host_tool_declared_toolchain(*tool, root / "tool", "linux"); - ASSERT_TRUE(tc15.has_value()); - EXPECT_EQ(*tc15, "gcc@15.1.0"); + expect(fatal(tc15.has_value())); + expect(eq(*tc15, "gcc@15.1.0")); // The tool's own declaration wins over the workspace's. write(root / "tool" / "mcpp.toml", "[package]\nname = \"tool\"\nversion = \"0.1.0\"\n\n" "[toolchain]\ndefault = \"gcc@16.1.0\"\n"); tool = mcpp::manifest::load(root / "tool" / "mcpp.toml"); - ASSERT_TRUE(tool.has_value()); + expect(fatal(tool.has_value())); auto tc16 = mcpp::build::host_tool_declared_toolchain(*tool, root / "tool", "linux"); - ASSERT_TRUE(tc16.has_value()); - EXPECT_EQ(*tc16, "gcc@16.1.0"); + expect(fatal(tc16.has_value())); + expect(eq(*tc16, "gcc@16.1.0")); std::error_code ec; fs::remove_all(root, ec); -} +}}; // #725. `-p, --package ` resolves the package identity first, and the // directory spellings docs/07 §5.3 has always documented second @@ -297,7 +298,7 @@ struct Fixture { write("mcpp.toml", std::format( "[package]\nname = \"root\"\nversion = \"0.1.0\"\n\n{}", membersToml)); auto m = mcpp::manifest::load(root / "mcpp.toml"); - EXPECT_TRUE(m.has_value()) << (m ? "" : m.error().format()); + if (auto&& failed = expect(m.has_value()); !failed) failed << (m ? "" : m.error().format()); return m ? std::move(*m) : mcpp::manifest::Manifest{}; } }; @@ -305,7 +306,7 @@ struct Fixture { // A member's own package name selects it, its full path does, and so does // its directory's last segment (docs/07 §5.3's historical spellings, kept as // a fallback). -TEST(ResolveMember, PackageNamePathAndBasenameAllSelectTheOneMember) { +UTest package_name_path_and_basename_all_select_the_one_member{"ResolveMember/PackageNamePathAndBasenameAllSelectTheOneMember", [] { Fixture f("single"); f.member("modules/base", "ws-base"); auto ws = f.root_manifest("[workspace]\nmembers = [\"modules/base\"]\n"); @@ -316,152 +317,152 @@ TEST(ResolveMember, PackageNamePathAndBasenameAllSelectTheOneMember) { filters.push_back("modules\\base"); #endif for (std::string_view filter : filters) { - SCOPED_TRACE(std::string(filter)); - testing::internal::CaptureStderr(); + /* trace: std::string(filter) */; + auto stderrCapture = mcpplibs::os::stdio::capture::begin(mcpplibs::os::stdio::stream::error).value(); auto m = mcpp::project::resolve_member_dir(ws, f.root, filter); - auto warned = testing::internal::GetCapturedStderr(); - ASSERT_TRUE(m.has_value()) << (m ? "" : m.error()); - EXPECT_EQ(*m, f.root / "modules/base"); - EXPECT_TRUE(warned.empty()) << warned; + auto warned = stderrCapture.text(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << (m ? "" : m.error()); + expect(eq(*m, f.root / "modules/base")); + expect(warned.empty()) << warned; } -} +}}; // Two members declare the same package name under different namespaces: // the bare name is refused (naming both qualified names), and each // qualified name selects its own member without any ambiguity. -TEST(ResolveMember, SameNameUnderTwoNamespacesRefusesTheBareNameOnly) { +UTest same_name_under_two_namespaces_refuses_the_bare_name_only{"ResolveMember/SameNameUnderTwoNamespacesRefusesTheBareNameOnly", [] { Fixture f("dup-ns"); f.member("a", "ws-common", "ns1"); f.member("b", "ws-common", "ns2"); auto ws = f.root_manifest("[workspace]\nmembers = [\"a\", \"b\"]\n"); auto bare = mcpp::project::resolve_member_dir(ws, f.root, "ws-common"); - ASSERT_FALSE(bare.has_value()); - EXPECT_NE(bare.error().find("ns1.ws-common"), std::string::npos) << bare.error(); - EXPECT_NE(bare.error().find("ns2.ws-common"), std::string::npos) << bare.error(); + expect(fatal(!bare.has_value())); + if (auto&& failed = expect(ne(bare.error().find("ns1.ws-common"), std::string::npos)); !failed) failed << bare.error(); + if (auto&& failed = expect(ne(bare.error().find("ns2.ws-common"), std::string::npos)); !failed) failed << bare.error(); auto qa = mcpp::project::resolve_member_dir(ws, f.root, "ns1.ws-common"); - ASSERT_TRUE(qa.has_value()) << qa.error(); - EXPECT_EQ(*qa, f.root / "a"); + if (auto&& failed = expect(fatal(qa.has_value())); !failed) failed << qa.error(); + expect(eq(*qa, f.root / "a")); auto qb = mcpp::project::resolve_member_dir(ws, f.root, "ns2.ws-common"); - ASSERT_TRUE(qb.has_value()) << qb.error(); - EXPECT_EQ(*qb, f.root / "b"); -} + if (auto&& failed = expect(fatal(qb.has_value())); !failed) failed << qb.error(); + expect(eq(*qb, f.root / "b")); +}}; // A value that is one member's package name and a different member's // directory selects the package -- the option names a package -- and warns, // naming the other member and its path. -TEST(ResolveMember, PackageNameOutranksAnotherMembersDirectoryAndWarns) { +UTest package_name_outranks_another_members_directory_and_warns{"ResolveMember/PackageNameOutranksAnotherMembersDirectoryAndWarns", [] { Fixture f("name-vs-dir"); f.member("modules/base", "ws-base"); f.member("ws-base", "other"); auto ws = f.root_manifest( "[workspace]\nmembers = [\"modules/base\", \"ws-base\"]\n"); - testing::internal::CaptureStderr(); + auto stderrCapture = mcpplibs::os::stdio::capture::begin(mcpplibs::os::stdio::stream::error).value(); auto m = mcpp::project::resolve_member_dir(ws, f.root, "ws-base"); - auto warned = testing::internal::GetCapturedStderr(); - ASSERT_TRUE(m.has_value()) << (m ? "" : m.error()); - EXPECT_EQ(*m, f.root / "modules/base"); - EXPECT_NE(warned.find("modules/base"), std::string::npos) << warned; - EXPECT_NE(warned.find("ws-base"), std::string::npos) << warned; -} + auto warned = stderrCapture.text(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << (m ? "" : m.error()); + expect(eq(*m, f.root / "modules/base")); + expect(ne(warned.find("modules/base"), std::string::npos)) << warned; + expect(ne(warned.find("ws-base"), std::string::npos)) << warned; +}}; // Two members share a directory basename (no package-name collision): the // first in `[workspace] members` is still selected (a script written against // it keeps working), and a warning now names the other and its path. -TEST(ResolveMember, DuplicateBasenameKeepsFirstMatchAndWarns) { +UTest duplicate_basename_keeps_first_match_and_warns{"ResolveMember/DuplicateBasenameKeepsFirstMatchAndWarns", [] { Fixture f("dup-basename"); f.member("apps/core", "coreapp"); f.member("libs/core", "corelib"); auto ws = f.root_manifest("[workspace]\nmembers = [\"apps/core\", \"libs/core\"]\n"); - testing::internal::CaptureStderr(); + auto stderrCapture = mcpplibs::os::stdio::capture::begin(mcpplibs::os::stdio::stream::error).value(); auto m = mcpp::project::resolve_member_dir(ws, f.root, "core"); - auto warned = testing::internal::GetCapturedStderr(); - ASSERT_TRUE(m.has_value()) << (m ? "" : m.error()); - EXPECT_EQ(*m, f.root / "apps/core"); - EXPECT_NE(warned.find("libs/core"), std::string::npos) << warned; -} + auto warned = stderrCapture.text(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << (m ? "" : m.error()); + expect(eq(*m, f.root / "apps/core")); + expect(ne(warned.find("libs/core"), std::string::npos)) << warned; +}}; // A filter matching nothing lists every member with its package name and its // path. -TEST(ResolveMember, NotFoundListsEveryMemberByNameAndPath) { +UTest not_found_lists_every_member_by_name_and_path{"ResolveMember/NotFoundListsEveryMemberByNameAndPath", [] { Fixture f("not-found"); f.member("a", "widget", "acme"); f.member("b", "gadget"); auto ws = f.root_manifest("[workspace]\nmembers = [\"a\", \"b\"]\n"); auto m = mcpp::project::resolve_member_dir(ws, f.root, "no-such-member"); - ASSERT_FALSE(m.has_value()); - EXPECT_NE(m.error().find("acme.widget"), std::string::npos) << m.error(); - EXPECT_NE(m.error().find("'a'"), std::string::npos) << m.error(); - EXPECT_NE(m.error().find("gadget"), std::string::npos) << m.error(); - EXPECT_NE(m.error().find("'b'"), std::string::npos) << m.error(); -} + expect(fatal(!m.has_value())); + if (auto&& failed = expect(ne(m.error().find("acme.widget"), std::string::npos)); !failed) failed << m.error(); + if (auto&& failed = expect(ne(m.error().find("'a'"), std::string::npos)); !failed) failed << m.error(); + if (auto&& failed = expect(ne(m.error().find("gadget"), std::string::npos)); !failed) failed << m.error(); + if (auto&& failed = expect(ne(m.error().find("'b'"), std::string::npos)); !failed) failed << m.error(); +}}; // No filter on a rooted workspace acts on the root package; no filter on a // virtual one is refused. Unaffected by #725, kept here as the resolver's // baseline. -TEST(ResolveMember, NoFilterActsOnRootedRootAndRefusesVirtual) { +UTest no_filter_acts_on_rooted_root_and_refuses_virtual{"ResolveMember/NoFilterActsOnRootedRootAndRefusesVirtual", [] { Fixture f("no-filter"); f.member("a", "a"); auto rooted = f.root_manifest("[workspace]\nmembers = [\"a\"]\n"); auto onRoot = mcpp::project::resolve_member_dir(rooted, f.root, ""); - ASSERT_TRUE(onRoot.has_value()) << onRoot.error(); - EXPECT_TRUE(onRoot->empty()); + if (auto&& failed = expect(fatal(onRoot.has_value())); !failed) failed << onRoot.error(); + expect(onRoot->empty()); auto virt = mcpp::manifest::parse_string("[workspace]\nmembers = [\"a\"]\n"); - ASSERT_TRUE(virt.has_value()) << virt.error().format(); + if (auto&& failed = expect(fatal(virt.has_value())); !failed) failed << virt.error().format(); auto onVirtual = mcpp::project::resolve_member_dir(*virt, f.root, ""); - ASSERT_FALSE(onVirtual.has_value()); - EXPECT_NE(onVirtual.error().find("--workspace"), std::string::npos); -} + expect(fatal(!onVirtual.has_value())); + expect(ne(onVirtual.error().find("--workspace"), std::string::npos)); +}}; } // namespace resolve_member // A `workspace = true` entry under a target selector, or behind a feature, // receives the workspace's source as one in [dependencies] does: a git // dependency inherited there resolved from the index before 2026.10.26.2. -TEST(WorkspaceInheritance, ConditionalAndFeatureTablesInheritTheSource) { +UTest conditional_and_feature_tables_inherit_the_source{"WorkspaceInheritance/ConditionalAndFeatureTablesInheritTheSource", [] { auto ws = mcpp::manifest::parse_string( "[workspace]\nmembers = [\"m\"]\n\n" "[workspace.dependencies]\n" "lib = { git = \"https://example.invalid/lib.git\", branch = \"main\" }\n" "tool = { path = \"tools/tool\" }\n"); - ASSERT_TRUE(ws.has_value()) << ws.error().format(); + if (auto&& failed = expect(fatal(ws.has_value())); !failed) failed << ws.error().format(); auto m = mcpp::manifest::parse_string( "[package]\nname = \"m\"\nversion = \"0.1.0\"\n\n" "[features]\nextra = []\n\n" "[feature-deps.extra]\ntool.workspace = true\n\n" "[target.'cfg(os = \"linux\")'.dependencies]\nlib.workspace = true\n"); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); mcpp::project::merge_workspace_deps(*m, *ws, "/ws"); - ASSERT_FALSE(m->conditionalConfigs.empty()); + expect(fatal(!(m->conditionalConfigs.empty()))); bool found = false; for (auto const& c : m->conditionalConfigs) { auto it = c.dependencies.find("lib"); if (it == c.dependencies.end()) continue; found = true; - EXPECT_FALSE(it->second.inheritWorkspace); - EXPECT_EQ(it->second.git, "https://example.invalid/lib.git"); - EXPECT_EQ(it->second.gitRev, "main"); + expect(!(it->second.inheritWorkspace)); + expect(eq(it->second.git, "https://example.invalid/lib.git")); + expect(eq(it->second.gitRev, "main")); } - EXPECT_TRUE(found); - ASSERT_TRUE(m->featureDeps.contains("extra")); + expect(bool(found)); + expect(fatal(m->featureDeps.contains("extra"))); auto const& tool = m->featureDeps.at("extra").at("tool"); - EXPECT_FALSE(tool.inheritWorkspace); - EXPECT_FALSE(tool.path.empty()); - EXPECT_FALSE(mcpp::project::unresolved_workspace_dependency_error(*m, "/ws/m").has_value()); -} + expect(!tool.inheritWorkspace); + expect(!tool.path.empty()); + expect(!(mcpp::project::unresolved_workspace_dependency_error(*m, "/ws/m").has_value())); +}}; -TEST(WorkspaceInheritance, AnUnresolvedConditionalEntryIsNamed) { +UTest an_unresolved_conditional_entry_is_named{"WorkspaceInheritance/AnUnresolvedConditionalEntryIsNamed", [] { auto m = mcpp::manifest::parse_string( "[package]\nname = \"m\"\nversion = \"0.1.0\"\n\n" "[target.'cfg(os = \"linux\")'.dependencies]\nlib.workspace = true\n"); - ASSERT_TRUE(m.has_value()) << m.error().format(); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << m.error().format(); auto err = mcpp::project::unresolved_workspace_dependency_error(*m, "/m"); - ASSERT_TRUE(err.has_value()); - EXPECT_NE(err->find("dependencies] lib"), std::string::npos) << *err; -} + expect(fatal(err.has_value())); + if (auto&& failed = expect(ne(err->find("dependencies] lib"), std::string::npos)); !failed) failed << *err; +}}; diff --git a/tests/unit/test_workspace_plan.cpp b/tests/unit/test_workspace_plan.cpp index 297e93698..fbab2db02 100644 --- a/tests/unit/test_workspace_plan.cpp +++ b/tests/unit/test_workspace_plan.cpp @@ -1,9 +1,11 @@ -#include import std; +import mcpplibs.mtest; import mcpp.manifest; import mcpp.project; +using namespace mcpplibs::mtest; + // The unit-level statements of a workspace plan (workspace design 2026-09-29 // §15): which member values separate two plans, what the virtual root holds, // and where a member's products are placed. The end-to-end halves are @@ -28,7 +30,7 @@ mcpp::project::WorkspaceMember listed(std::string path, std::string ns, std::str // A member's own flags, sources and dependencies are attributes of its node: // they never separate it from another member. -TEST(WorkspacePlan, PackageAttributesDoNotSeparateMembers) { +UTest package_attributes_do_not_separate_members{"WorkspacePlan/PackageAttributesDoNotSeparateMembers", [] { auto a = member("a"); auto b = member("b"); b.buildConfig.cxxflags = {"-DONLY_B=1"}; @@ -36,33 +38,33 @@ TEST(WorkspacePlan, PackageAttributesDoNotSeparateMembers) { b.buildConfig.sources = {"src/**/*.cpp"}; b.buildConfig.cStandard = "c17"; b.dependencies["fmt"] = mcpp::manifest::DependencySpec{.version = "11.0.0"}; - EXPECT_EQ(mcpp::project::root_position_key(a), mcpp::project::root_position_key(b)); -} + expect(eq(mcpp::project::root_position_key(a), mcpp::project::root_position_key(b))); +}}; // What every node of one graph shares does separate them: the toolchain, the // standard, the dialect flags, the target and the profile. -TEST(WorkspacePlan, RootPositionValuesSeparateMembers) { +UTest root_position_values_separate_members{"WorkspacePlan/RootPositionValuesSeparateMembers", [] { const auto ref = mcpp::project::root_position_key(member("a")); { auto m = member("a"); m.package.standard = "c++26"; - EXPECT_NE(mcpp::project::root_position_key(m), ref); } + expect(ne(mcpp::project::root_position_key(m), ref)); } { auto m = member("a"); m.toolchain.byPlatform["default"] = "gcc@16.1.0"; - EXPECT_NE(mcpp::project::root_position_key(m), ref); } + expect(ne(mcpp::project::root_position_key(m), ref)); } { auto m = member("a"); m.buildConfig.dialectCxxflags = {"-freflection"}; - EXPECT_NE(mcpp::project::root_position_key(m), ref); } + expect(ne(mcpp::project::root_position_key(m), ref)); } { auto m = member("a"); m.buildConfig.target = "x86_64-linux-musl"; - EXPECT_NE(mcpp::project::root_position_key(m), ref); } + expect(ne(mcpp::project::root_position_key(m), ref)); } { auto m = member("a"); m.buildConfig.defaultProfile = "release"; - EXPECT_NE(mcpp::project::root_position_key(m), ref); } + expect(ne(mcpp::project::root_position_key(m), ref)); } { auto m = member("a"); m.buildConfig.cxxRuntime = "static"; - EXPECT_NE(mcpp::project::root_position_key(m), ref); } + expect(ne(mcpp::project::root_position_key(m), ref)); } // `linkage` chooses the C runtime every object of the plan is compiled // against, so it is the plan's and not the member's. { auto m = member("a"); m.buildConfig.linkage = "static"; - EXPECT_NE(mcpp::project::root_position_key(m), ref); } -} + expect(ne(mcpp::project::root_position_key(m), ref)); } +}}; // The virtual root carries the plan's values and nothing a package owns. -TEST(WorkspacePlan, TheVirtualRootHoldsNoPackageContent) { +UTest the_virtual_root_holds_no_package_content{"WorkspacePlan/TheVirtualRootHoldsNoPackageContent", [] { mcpp::manifest::Manifest ws; ws.workspace.present = true; ws.workspace.members = {"a"}; @@ -77,40 +79,40 @@ TEST(WorkspacePlan, TheVirtualRootHoldsNoPackageContent) { first.dependencies["fmt"] = mcpp::manifest::DependencySpec{.version = "11.0.0"}; const auto v = mcpp::project::virtual_workspace_root(ws, first, "/ws"); - EXPECT_TRUE(v.package.virtualRoot); - EXPECT_EQ(v.package.standard, "c++26"); - EXPECT_EQ(v.toolchain.byPlatform.at("default"), "gcc@16.1.0"); - EXPECT_EQ(v.buildConfig.dialectCxxflags, first.buildConfig.dialectCxxflags); - EXPECT_TRUE(v.buildConfig.cxxflags.empty()); - EXPECT_TRUE(v.buildConfig.ldflags.empty()); - EXPECT_TRUE(v.buildConfig.sources.empty()); - EXPECT_TRUE(v.buildConfig.sourcesDeclared); - EXPECT_TRUE(v.targets.empty()); - EXPECT_TRUE(v.dependencies.empty()); + expect(bool(v.package.virtualRoot)); + expect(eq(v.package.standard, "c++26")); + expect(eq(v.toolchain.byPlatform.at("default"), "gcc@16.1.0")); + expect(eq(v.buildConfig.dialectCxxflags, first.buildConfig.dialectCxxflags)); + expect(v.buildConfig.cxxflags.empty()); + expect(v.buildConfig.ldflags.empty()); + expect(v.buildConfig.sources.empty()); + expect(bool(v.buildConfig.sourcesDeclared)); + expect(v.targets.empty()); + expect(v.dependencies.empty()); // The root's values and the group's are one statement. - EXPECT_EQ(mcpp::project::root_position_key(v), mcpp::project::root_position_key(first)); -} + expect(eq(mcpp::project::root_position_key(v), mcpp::project::root_position_key(first))); +}}; // A member's products are in `bin//`, qualified when another // member of the workspace has the same name; the workspace's own package // keeps `bin/`. -TEST(WorkspacePlan, ProductDirectoriesAreNamedByPackage) { +UTest product_directories_are_named_by_package{"WorkspacePlan/ProductDirectoriesAreNamedByPackage", [] { const std::vector all = { listed("apps/cli", "", "cli"), listed("ns1/common", "ns1", "common"), listed("ns2/common", "ns2", "common"), }; - EXPECT_EQ(mcpp::project::product_directory_name(all, "apps/cli"), "cli"); - EXPECT_EQ(mcpp::project::product_directory_name(all, "ns1/common"), "ns1.common"); - EXPECT_EQ(mcpp::project::product_directory_name(all, "ns2/common"), "ns2.common"); - EXPECT_EQ(mcpp::project::product_directory_name(all, "."), ""); -} + expect(eq(mcpp::project::product_directory_name(all, "apps/cli"), "cli")); + expect(eq(mcpp::project::product_directory_name(all, "ns1/common"), "ns1.common")); + expect(eq(mcpp::project::product_directory_name(all, "ns2/common"), "ns2.common")); + expect(eq(mcpp::project::product_directory_name(all, "."), "")); +}}; // A relative `[indices].path` a member declares was written in the member's // directory, and one the workspace declares in the workspace's: both are // anchored where they were written, so two members naming one tree share a // configuration and a member's path never resolves against the plan's root. -TEST(WorkspacePlan, IndexPathsAreAnchoredWhereTheyWereWritten) { +UTest index_paths_are_anchored_where_they_were_written{"WorkspacePlan/IndexPathsAreAnchoredWhereTheyWereWritten", [] { namespace fs = std::filesystem; const auto ws = fs::temp_directory_path() / std::format("mcpp-ws-index-{}", std::random_device{}()); @@ -125,23 +127,23 @@ TEST(WorkspacePlan, IndexPathsAreAnchoredWhereTheyWereWritten) { write(ws / "b" / "mcpp.toml", "[indices]\nx = { path = \"../idx\" }\n\n[package]\nname = \"b\"\nversion = \"0.1.0\"\n"); auto root = mcpp::manifest::load(ws / "mcpp.toml"); - ASSERT_TRUE(root.has_value()); + expect(fatal(root.has_value())); auto a = mcpp::project::load_member_manifest(*root, ws, "a"); auto b = mcpp::project::load_member_manifest(*root, ws, "b"); - ASSERT_TRUE(a.has_value()) << a.error(); - ASSERT_TRUE(b.has_value()) << b.error(); - EXPECT_EQ(a->indices.at("x").path, (ws / "idx").lexically_normal()); - EXPECT_EQ(b->indices.at("x").path, (ws / "idx").lexically_normal()); - EXPECT_EQ(mcpp::project::root_position_key(*a), mcpp::project::root_position_key(*b)); + if (auto&& failed = expect(fatal(a.has_value())); !failed) failed << a.error(); + if (auto&& failed = expect(fatal(b.has_value())); !failed) failed << b.error(); + expect(eq(a->indices.at("x").path, (ws / "idx").lexically_normal())); + expect(eq(b->indices.at("x").path, (ws / "idx").lexically_normal())); + expect(eq(mcpp::project::root_position_key(*a), mcpp::project::root_position_key(*b))); std::error_code ec; fs::remove_all(ws, ec); -} +}}; // THE WORKSPACE'S PROFILES ARE ROOT-POSITION VALUES (2026.10.5.2). A member // inherits each `[profile.]` it does not declare, and its own table of a // name replaces the workspace's whole, so members without a profile of their // own share one configuration with the workspace's profile in it. -TEST(WorkspacePlan, MembersInheritTheWorkspacesProfilesByName) { +UTest members_inherit_the_workspaces_profiles_by_name{"WorkspacePlan/MembersInheritTheWorkspacesProfilesByName", [] { namespace fs = std::filesystem; const auto ws = fs::temp_directory_path() / std::format("mcpp-ws-profile-{}", std::random_device{}()); @@ -156,23 +158,23 @@ TEST(WorkspacePlan, MembersInheritTheWorkspacesProfilesByName) { write(ws / "b" / "mcpp.toml", "[package]\nname = \"b\"\nversion = \"0.1.0\"\n\n[profile.release]\nopt = 1\n"); auto root = mcpp::manifest::load(ws / "mcpp.toml"); - ASSERT_TRUE(root.has_value()); + expect(fatal(root.has_value())); auto a = mcpp::project::load_member_manifest(*root, ws, "a"); auto b = mcpp::project::load_member_manifest(*root, ws, "b"); - ASSERT_TRUE(a.has_value()) << a.error(); - ASSERT_TRUE(b.has_value()) << b.error(); - EXPECT_EQ(a->profiles.at("release").optLevel, "3"); - EXPECT_EQ(a->profiles.at("release").ldflags, (std::vector{"-Wl,-z,now"})); - EXPECT_EQ(b->profiles.at("release").optLevel, "1"); - EXPECT_TRUE(b->profiles.at("release").ldflags.empty()); - EXPECT_EQ(b->profiles.at("bench").optLevel, "2"); + if (auto&& failed = expect(fatal(a.has_value())); !failed) failed << a.error(); + if (auto&& failed = expect(fatal(b.has_value())); !failed) failed << b.error(); + expect(eq(a->profiles.at("release").optLevel, "3")); + expect(eq(a->profiles.at("release").ldflags, (std::vector{"-Wl,-z,now"}))); + expect(eq(b->profiles.at("release").optLevel, "1")); + expect(b->profiles.at("release").ldflags.empty()); + expect(eq(b->profiles.at("bench").optLevel, "2")); std::error_code ec; fs::remove_all(ws, ec); -} +}}; // THE ROOT PACKAGE OF A WORKSPACE IS A MEMBER (2026.10.5.2): it receives // `[workspace.build]` once, before its own `[build]`, on every path that reads it. -TEST(WorkspacePlan, TheRootPackageInheritsTheWorkspaceBuildOnce) { +UTest the_root_package_inherits_the_workspace_build_once{"WorkspacePlan/TheRootPackageInheritsTheWorkspaceBuildOnce", [] { namespace fs = std::filesystem; const auto ws = fs::temp_directory_path() / std::format("mcpp-ws-rooted-{}", std::random_device{}()); @@ -185,18 +187,18 @@ TEST(WorkspacePlan, TheRootPackageInheritsTheWorkspaceBuildOnce) { std::ofstream(ws / "lib" / "mcpp.toml") << "[package]\nname = \"lib\"\nversion = \"0.1.0\"\n"; const std::vector expected{"-DSHARED", "-DOWN"}; auto effective = mcpp::project::load_effective_manifest(ws); - ASSERT_TRUE(effective.has_value()) << effective.error(); - EXPECT_EQ(effective->manifest.buildConfig.cxxflags, expected); + if (auto&& failed = expect(fatal(effective.has_value())); !failed) failed << effective.error(); + expect(eq(effective->manifest.buildConfig.cxxflags, expected)); // The manifest a selection reads is the one already inherited; the member // "." loaded from it is not inherited a second time. auto dot = mcpp::project::load_member_manifest(effective->manifest, ws, "."); - ASSERT_TRUE(dot.has_value()) << dot.error(); - EXPECT_EQ(dot->buildConfig.cxxflags, expected); + if (auto&& failed = expect(fatal(dot.has_value())); !failed) failed << dot.error(); + expect(eq(dot->buildConfig.cxxflags, expected)); auto raw = mcpp::manifest::load(ws / "mcpp.toml"); - ASSERT_TRUE(raw.has_value()); + expect(fatal(raw.has_value())); auto fromRaw = mcpp::project::load_member_manifest(*raw, ws, "."); - ASSERT_TRUE(fromRaw.has_value()) << fromRaw.error(); - EXPECT_EQ(fromRaw->buildConfig.cxxflags, expected); + if (auto&& failed = expect(fatal(fromRaw.has_value())); !failed) failed << fromRaw.error(); + expect(eq(fromRaw->buildConfig.cxxflags, expected)); std::error_code ec; fs::remove_all(ws, ec); -} +}}; diff --git a/tests/unit/test_xlings.cpp b/tests/unit/test_xlings.cpp index 0101c25ca..1f7a7cacc 100644 --- a/tests/unit/test_xlings.cpp +++ b/tests/unit/test_xlings.cpp @@ -1,10 +1,12 @@ -#include import std; +import mcpplibs.mtest; import mcpplibs.os.platform; import mcpp.provider.xlings; import mcpp.invocation; +using namespace mcpplibs::mtest; + namespace { std::filesystem::path make_tempdir(std::string_view name) { @@ -86,7 +88,7 @@ std::string path_without_real_nasm() { } // namespace -TEST(FindUsableNasm, FindsOnPathWhenVersionIsAdequate) { +UTest finds_on_path_when_version_is_adequate{"FindUsableNasm/FindsOnPathWhenVersionIsAdequate", [] { auto pathDir = make_tempdir("mcpp-nasm-path"); auto expected = make_fake_nasm(pathDir, "NASM version 2.16.03 compiled on Jan 1 2026"); @@ -100,14 +102,14 @@ TEST(FindUsableNasm, FindsOnPathWhenVersionIsAdequate) { mcpp::provider::xlings::Env env{.home = home}; auto found = mcpp::provider::xlings::find_usable_nasm(env); - ASSERT_TRUE(found.has_value()); - EXPECT_EQ(*found, expected); + expect(fatal(found.has_value())); + expect(eq(*found, expected)); std::filesystem::remove_all(pathDir); std::filesystem::remove_all(home); -} +}}; -TEST(FindUsableNasm, RejectsPathNasmBelowMinVersionAndFindsNoSandboxFallback) { +UTest rejects_path_nasm_below_min_version_and_finds_no_sandbox_fallback{"FindUsableNasm/RejectsPathNasmBelowMinVersionAndFindsNoSandboxFallback", [] { auto pathDir = make_tempdir("mcpp-nasm-path-old"); make_fake_nasm(pathDir, "NASM version 2.10.00 compiled on Jan 1 2020"); @@ -117,13 +119,13 @@ TEST(FindUsableNasm, RejectsPathNasmBelowMinVersionAndFindsNoSandboxFallback) { auto home = make_tempdir("mcpp-nasm-home-empty"); // no sandbox nasm mcpp::provider::xlings::Env env{.home = home}; - EXPECT_FALSE(mcpp::provider::xlings::find_usable_nasm(env).has_value()); + expect(!(mcpp::provider::xlings::find_usable_nasm(env).has_value())); std::filesystem::remove_all(pathDir); std::filesystem::remove_all(home); -} +}}; -TEST(FindUsableNasm, FallsBackToSandboxWhenPathHasNoNasm) { +UTest falls_back_to_sandbox_when_path_has_no_nasm{"FindUsableNasm/FallsBackToSandboxWhenPathHasNoNasm", [] { // PATH has the usual system directories (so the sandbox script's own // shebang still resolves) but none of them contain a real `nasm`. mcpp::invocation::ScopedEnv path("PATH", path_without_real_nasm()); @@ -135,13 +137,13 @@ TEST(FindUsableNasm, FallsBackToSandboxWhenPathHasNoNasm) { mcpp::provider::xlings::Env env{.home = home}; auto found = mcpp::provider::xlings::find_usable_nasm(env); - ASSERT_TRUE(found.has_value()); - EXPECT_EQ(*found, expected); + expect(fatal(found.has_value())); + expect(eq(*found, expected)); std::filesystem::remove_all(home); -} +}}; -TEST(FindSandboxNasm, FindsUnderVersionedBinSubdir) { +UTest finds_under_versioned_bin_subdir{"FindSandboxNasm/FindsUnderVersionedBinSubdir", [] { auto home = make_tempdir("mcpp-nasm-home-bin-subdir"); auto sandboxDir = home / "data" / "xpkgs" / "xim-x-nasm" / "3.02" / "bin"; auto expected = make_fake_nasm(sandboxDir, "NASM version 3.02 compiled on Jan 1 2026"); @@ -149,70 +151,70 @@ TEST(FindSandboxNasm, FindsUnderVersionedBinSubdir) { mcpp::provider::xlings::Env env{.home = home}; auto found = mcpp::provider::xlings::find_sandbox_nasm(env); - ASSERT_TRUE(found.has_value()); - EXPECT_EQ(*found, expected); + expect(fatal(found.has_value())); + expect(eq(*found, expected)); std::filesystem::remove_all(home); -} +}}; -TEST(FindSandboxNasm, RejectsInstalledNasmBelowMinVersion) { +UTest rejects_installed_nasm_below_min_version{"FindSandboxNasm/RejectsInstalledNasmBelowMinVersion", [] { auto home = make_tempdir("mcpp-nasm-home-old-sandboxed"); auto sandboxDir = home / "data" / "xpkgs" / "xim-x-nasm" / "2.10"; make_fake_nasm(sandboxDir, "NASM version 2.10.00 compiled on Jan 1 2020"); mcpp::provider::xlings::Env env{.home = home}; - EXPECT_FALSE(mcpp::provider::xlings::find_sandbox_nasm(env).has_value()); + expect(!(mcpp::provider::xlings::find_sandbox_nasm(env).has_value())); std::filesystem::remove_all(home); -} +}}; -TEST(FindSandboxNasm, NoSandboxDirectoryYieldsNullopt) { +UTest no_sandbox_directory_yields_nullopt{"FindSandboxNasm/NoSandboxDirectoryYieldsNullopt", [] { auto home = make_tempdir("mcpp-nasm-home-absent"); mcpp::provider::xlings::Env env{.home = home}; - EXPECT_FALSE(mcpp::provider::xlings::find_sandbox_nasm(env).has_value()); + expect(!(mcpp::provider::xlings::find_sandbox_nasm(env).has_value())); std::filesystem::remove_all(home); -} +}}; #endif // !defined(_WIN32) -TEST(XlingsIndexFreshness, RequiresDefaultMcpplibsIndex) { +UTest requires_default_mcpplibs_index{"XlingsIndexFreshness/RequiresDefaultMcpplibsIndex", [] { auto home = make_tempdir("mcpp-xlings-index-freshness"); std::filesystem::create_directories(home / "data" / "xim-pkgindex" / "pkgs"); mcpp::provider::xlings::Env env{.home = home}; - EXPECT_FALSE(mcpp::provider::xlings::is_index_fresh(env, 3600)); + expect(!mcpp::provider::xlings::is_index_fresh(env, 3600)); std::filesystem::remove_all(home); -} +}}; -TEST(XlingsIndexFreshness, AcceptsFreshDefaultMcpplibsIndex) { +UTest accepts_fresh_default_mcpplibs_index{"XlingsIndexFreshness/AcceptsFreshDefaultMcpplibsIndex", [] { auto home = make_tempdir("mcpp-xlings-index-freshness"); std::filesystem::create_directories(home / "data" / "mcpplibs" / "pkgs"); std::ofstream(home / "data" / "mcpplibs" / ".mcpp-index-updated") << "ok\n"; mcpp::provider::xlings::Env env{.home = home}; - EXPECT_TRUE(mcpp::provider::xlings::is_index_fresh(env, 3600)); + expect(mcpp::provider::xlings::is_index_fresh(env, 3600)); std::filesystem::remove_all(home); -} +}}; -TEST(XlingsIndexFreshness, RequiresRefreshMarkerForDefaultMcpplibsIndex) { +UTest requires_refresh_marker_for_default_mcpplibs_index{"XlingsIndexFreshness/RequiresRefreshMarkerForDefaultMcpplibsIndex", [] { auto home = make_tempdir("mcpp-xlings-index-freshness"); std::filesystem::create_directories(home / "data" / "mcpplibs" / "pkgs"); mcpp::provider::xlings::Env env{.home = home}; - EXPECT_FALSE(mcpp::provider::xlings::is_index_fresh(env, 3600)); + expect(!mcpp::provider::xlings::is_index_fresh(env, 3600)); std::filesystem::remove_all(home); -} +}}; -TEST(XlingsIndexFreshness, RejectsStaleRefreshMarker) { +UTest rejects_stale_refresh_marker{"XlingsIndexFreshness/RejectsStaleRefreshMarker", [] { auto home = make_tempdir("mcpp-xlings-index-freshness"); std::filesystem::create_directories(home / "data" / "mcpplibs" / "pkgs"); auto marker = home / "data" / "mcpplibs" / ".mcpp-index-updated"; @@ -222,48 +224,48 @@ TEST(XlingsIndexFreshness, RejectsStaleRefreshMarker) { mcpp::provider::xlings::Env env{.home = home}; - EXPECT_FALSE(mcpp::provider::xlings::is_index_fresh(env, 3600)); + expect(!mcpp::provider::xlings::is_index_fresh(env, 3600)); std::filesystem::remove_all(home); -} +}}; -TEST(XlingsIndexFreshness, RequiresOfficialXimIndexEvenWhenDefaultIndexIsFresh) { +UTest requires_official_xim_index_even_when_default_index_is_fresh{"XlingsIndexFreshness/RequiresOfficialXimIndexEvenWhenDefaultIndexIsFresh", [] { auto home = make_tempdir("mcpp-xlings-index-freshness"); std::filesystem::create_directories(home / "data" / "mcpplibs" / "pkgs"); std::ofstream(home / "data" / "mcpplibs" / ".mcpp-index-updated") << "ok\n"; mcpp::provider::xlings::Env env{.home = home}; - EXPECT_FALSE(mcpp::provider::xlings::is_official_index_fresh(env, 3600)); + expect(!mcpp::provider::xlings::is_official_index_fresh(env, 3600)); std::filesystem::remove_all(home); -} +}}; -TEST(XlingsIndexFreshness, AcceptsFreshOfficialXimIndex) { +UTest accepts_fresh_official_xim_index{"XlingsIndexFreshness/AcceptsFreshOfficialXimIndex", [] { auto home = make_tempdir("mcpp-xlings-index-freshness"); std::filesystem::create_directories(home / "data" / "xim-pkgindex" / "pkgs"); std::ofstream(home / "data" / "xim-pkgindex" / ".mcpp-index-updated") << "ok\n"; mcpp::provider::xlings::Env env{.home = home}; - EXPECT_TRUE(mcpp::provider::xlings::is_official_index_fresh(env, 3600)); + expect(mcpp::provider::xlings::is_official_index_fresh(env, 3600)); std::filesystem::remove_all(home); -} +}}; -TEST(XlingsIndexFreshness, RequiresOfficialPackageFileEvenWhenOfficialIndexIsFresh) { +UTest requires_official_package_file_even_when_official_index_is_fresh{"XlingsIndexFreshness/RequiresOfficialPackageFileEvenWhenOfficialIndexIsFresh", [] { auto home = make_tempdir("mcpp-xlings-index-freshness"); std::filesystem::create_directories(home / "data" / "xim-pkgindex" / "pkgs"); std::ofstream(home / "data" / "xim-pkgindex" / ".mcpp-index-updated") << "ok\n"; mcpp::provider::xlings::Env env{.home = home}; - EXPECT_FALSE(mcpp::provider::xlings::is_official_package_index_fresh(env, "musl-gcc", 3600)); + expect(!mcpp::provider::xlings::is_official_package_index_fresh(env, "musl-gcc", 3600)); std::filesystem::remove_all(home); -} +}}; -TEST(XlingsIndexFreshness, AcceptsFreshOfficialPackageFile) { +UTest accepts_fresh_official_package_file{"XlingsIndexFreshness/AcceptsFreshOfficialPackageFile", [] { auto home = make_tempdir("mcpp-xlings-index-freshness"); std::filesystem::create_directories(home / "data" / "xim-pkgindex" / "pkgs" / "m"); std::ofstream(home / "data" / "xim-pkgindex" / ".mcpp-index-updated") << "ok\n"; @@ -271,12 +273,12 @@ TEST(XlingsIndexFreshness, AcceptsFreshOfficialPackageFile) { mcpp::provider::xlings::Env env{.home = home}; - EXPECT_TRUE(mcpp::provider::xlings::is_official_package_index_fresh(env, "musl-gcc", 3600)); + expect(mcpp::provider::xlings::is_official_package_index_fresh(env, "musl-gcc", 3600)); std::filesystem::remove_all(home); -} +}}; -TEST(XlingsIndexFreshness, RejectsOfficialPackageCacheWithForeignPath) { +UTest rejects_official_package_cache_with_foreign_path{"XlingsIndexFreshness/RejectsOfficialPackageCacheWithForeignPath", [] { auto home = make_tempdir("mcpp-xlings-index-freshness"); auto pkg = home / "data" / "xim-pkgindex" / "pkgs" / "m" / "musl-gcc.lua"; std::filesystem::create_directories(pkg.parent_path()); @@ -287,12 +289,12 @@ TEST(XlingsIndexFreshness, RejectsOfficialPackageCacheWithForeignPath) { mcpp::provider::xlings::Env env{.home = home}; - EXPECT_FALSE(mcpp::provider::xlings::is_official_package_index_fresh(env, "musl-gcc", 3600)); + expect(!mcpp::provider::xlings::is_official_package_index_fresh(env, "musl-gcc", 3600)); std::filesystem::remove_all(home); -} +}}; -TEST(XlingsIndexFreshness, AcceptsOfficialPackageCacheWithCurrentPath) { +UTest accepts_official_package_cache_with_current_path{"XlingsIndexFreshness/AcceptsOfficialPackageCacheWithCurrentPath", [] { auto home = make_tempdir("mcpp-xlings-index-freshness"); auto pkg = home / "data" / "xim-pkgindex" / "pkgs" / "m" / "musl-gcc.lua"; std::filesystem::create_directories(pkg.parent_path()); @@ -303,10 +305,10 @@ TEST(XlingsIndexFreshness, AcceptsOfficialPackageCacheWithCurrentPath) { mcpp::provider::xlings::Env env{.home = home}; - EXPECT_TRUE(mcpp::provider::xlings::is_official_package_index_fresh(env, "musl-gcc", 3600)); + expect(mcpp::provider::xlings::is_official_package_index_fresh(env, "musl-gcc", 3600)); std::filesystem::remove_all(home); -} +}}; // ─── Sibling/home payload discovery (issue #120) ───────────────────── // @@ -324,7 +326,7 @@ void touch(const std::filesystem::path& p, std::string_view content = "x") { } // namespace -TEST(XlingsSiblingPackage, MetadataOnlyHuskIsNotContent) { +UTest metadata_only_husk_is_not_content{"XlingsSiblingPackage/MetadataOnlyHuskIsNotContent", [] { auto tmp = make_tempdir("mcpp-husk"); auto xpkgs = tmp / "xpkgs"; auto gccBin = xpkgs / "xim-x-gcc" / "16.1.0" / "bin" / "g++"; @@ -342,12 +344,12 @@ TEST(XlingsSiblingPackage, MetadataOnlyHuskIsNotContent) { found = mcpp::provider::xlings::paths::find_sibling_package(gccBin, "linux-headers"); } - EXPECT_FALSE(found.has_value()); + expect(!found.has_value()); std::filesystem::remove_all(tmp); -} +}}; -TEST(XlingsSiblingPackage, SkipsHuskAndFindsPayloadUnderOtherPrefix) { +UTest skips_husk_and_finds_payload_under_other_prefix{"XlingsSiblingPackage/SkipsHuskAndFindsPayloadUnderOtherPrefix", [] { auto tmp = make_tempdir("mcpp-husk"); auto xpkgs = tmp / "xpkgs"; auto gccBin = xpkgs / "xim-x-gcc" / "16.1.0" / "bin" / "g++"; @@ -361,13 +363,13 @@ TEST(XlingsSiblingPackage, SkipsHuskAndFindsPayloadUnderOtherPrefix) { touch(real / "include" / "linux" / "limits.h"); auto found = mcpp::provider::xlings::paths::find_sibling_package(gccBin, "linux-headers"); - ASSERT_TRUE(found.has_value()); - EXPECT_EQ(*found, real); + expect(fatal(found.has_value())); + expect(eq(*found, real)); std::filesystem::remove_all(tmp); -} +}}; -TEST(XlingsSiblingPackage, RequiredRelPathRejectsContentfulButWrongCandidate) { +UTest required_rel_path_rejects_contentful_but_wrong_candidate{"XlingsSiblingPackage/RequiredRelPathRejectsContentfulButWrongCandidate", [] { auto tmp = make_tempdir("mcpp-husk"); auto xpkgs = tmp / "xpkgs"; auto gccBin = xpkgs / "xim-x-gcc" / "16.1.0" / "bin" / "g++"; @@ -382,13 +384,13 @@ TEST(XlingsSiblingPackage, RequiredRelPathRejectsContentfulButWrongCandidate) { auto found = mcpp::provider::xlings::paths::find_sibling_package( gccBin, "linux-headers", "include/linux/limits.h"); - ASSERT_TRUE(found.has_value()); - EXPECT_EQ(*found, real); + expect(fatal(found.has_value())); + expect(eq(*found, real)); std::filesystem::remove_all(tmp); -} +}}; -TEST(XlingsHomeTool, FindsPayloadUnderNonXimPrefix) { +UTest finds_payload_under_non_xim_prefix{"XlingsHomeTool/FindsPayloadUnderNonXimPrefix", [] { auto tmp = make_tempdir("mcpp-husk-home"); auto xpkgs = tmp / "registry" / "data" / "xpkgs"; @@ -406,12 +408,12 @@ TEST(XlingsHomeTool, FindsPayloadUnderNonXimPrefix) { "linux-headers", "include/linux/limits.h"); } - ASSERT_TRUE(found.has_value()); + expect(fatal(found.has_value())); // The original home is normalized; macOS spells /var's target /private/var. - EXPECT_TRUE(std::filesystem::equivalent(*found, real)); + expect(bool(std::filesystem::equivalent(*found, real))); std::filesystem::remove_all(tmp); -} +}}; // ─── seed_xlings_json — per-repo artifact/source emission (#269) ────── @@ -425,7 +427,7 @@ std::string read_seeded(const std::filesystem::path& home) { } // namespace -TEST(XlingsSeed, PlainRepoEmissionIsByteStable) { +UTest plain_repo_emission_is_byte_stable{"XlingsSeed/PlainRepoEmissionIsByteStable", [] { // Zero-diff gate: repos without artifact/source must serialize exactly // as before the SeedRepo extension. auto dir = make_tempdir("mcpp-seed-plain"); @@ -435,15 +437,14 @@ TEST(XlingsSeed, PlainRepoEmissionIsByteStable) { { "mcpplibs", "https://x.git", "", "" } }; mcpp::provider::xlings::seed_xlings_json(env, repos); auto text = read_seeded(dir); - EXPECT_NE(text.find( - " { \"name\": \"mcpplibs\", \"url\": \"https://x.git\" }\n"), - std::string::npos) << text; - EXPECT_EQ(text.find("artifact"), std::string::npos); - EXPECT_EQ(text.find("source"), std::string::npos); + expect(ne(text.find( + " { \"name\": \"mcpplibs\", \"url\": \"https://x.git\" }\n"), std::string::npos)) << text; + expect(eq(text.find("artifact"), std::string::npos)); + expect(eq(text.find("source"), std::string::npos)); std::filesystem::remove_all(dir); -} +}}; -TEST(XlingsSeed, ArtifactAndSourceEmittedWhenSet) { +UTest artifact_and_source_emitted_when_set{"XlingsSeed/ArtifactAndSourceEmittedWhenSet", [] { auto dir = make_tempdir("mcpp-seed-art"); mcpp::provider::xlings::Env env; env.home = dir; @@ -452,16 +453,15 @@ TEST(XlingsSeed, ArtifactAndSourceEmittedWhenSet) { "https://github.com/xlings-res/mcpp-index", "auto" } }; mcpp::provider::xlings::seed_xlings_json(env, repos); auto text = read_seeded(dir); - EXPECT_NE(text.find( + expect(ne(text.find( "\"url\": \"https://x.git\", \"artifact\": " - "\"https://github.com/xlings-res/mcpp-index\", \"source\": \"auto\""), - std::string::npos) << text; + "\"https://github.com/xlings-res/mcpp-index\", \"source\": \"auto\""), std::string::npos)) << text; std::filesystem::remove_all(dir); -} +}}; // ─── Indices mcpp owns: their places (design 2026-10-16 §5) ────────────── -TEST(XlingsOwnedIndex, SeedNamesItsDataAndStore) { +UTest seed_names_its_data_and_store{"XlingsOwnedIndex/SeedNamesItsDataAndStore", [] { auto dir = make_tempdir("mcpp-seed-owned"); mcpp::provider::xlings::Env env; env.home = dir / "registry"; @@ -471,29 +471,29 @@ TEST(XlingsOwnedIndex, SeedNamesItsDataAndStore) { std::vector repos{ repo }; mcpp::provider::xlings::seed_xlings_json(env, repos); const auto declared = mcpp::provider::xlings::paths::declared_indices(env); - ASSERT_EQ(declared.size(), 1u); - EXPECT_TRUE(declared[0].owned); - EXPECT_EQ(declared[0].dir, (dir / "indices" / "mcpplibs").lexically_normal()); - EXPECT_EQ(declared[0].store, (dir / "xpkgs").lexically_normal()); + expect(fatal(eq(declared.size(), 1u))); + expect(bool(declared[0].owned)); + expect(eq(declared[0].dir, (dir / "indices" / "mcpplibs").lexically_normal())); + expect(eq(declared[0].store, (dir / "xpkgs").lexically_normal())); // Its store first, the home's after it (payloads from before, §5.3). const auto stores = mcpp::provider::xlings::paths::stores(env); - ASSERT_EQ(stores.size(), 2u); - EXPECT_EQ(stores[0], (dir / "xpkgs").lexically_normal()); - EXPECT_EQ(stores[1], env.home / "data" / "xpkgs"); + expect(fatal(eq(stores.size(), 2u))); + expect(eq(stores[0], (dir / "xpkgs").lexically_normal())); + expect(eq(stores[1], env.home / "data" / "xpkgs")); std::filesystem::remove_all(dir); -} +}}; -TEST(XlingsOwnedIndex, AHomeServesItsDefaultAndItsConfiguredIndexes) { +UTest a_home_serves_its_default_and_its_configured_indexes{"XlingsOwnedIndex/AHomeServesItsDefaultAndItsConfiguredIndexes", [] { auto dir = make_tempdir("mcpp-indices-of"); - EXPECT_TRUE(mcpp::provider::xlings::paths::indices_of(dir).empty()); // not a provider home + expect(mcpp::provider::xlings::paths::indices_of(dir).empty()); // not a provider home std::ofstream(dir / ".xlings.json") << "{}"; - EXPECT_EQ(mcpp::provider::xlings::paths::indices_of(dir), std::vector{"xim"}); + expect(eq(mcpp::provider::xlings::paths::indices_of(dir), std::vector{"xim"})); std::ofstream(dir / ".xlings.json") << R"({"index_repos":[{"name":"mcpp-index","url":"u"},{"name":"xim","url":"v"}]})"; - EXPECT_EQ(mcpp::provider::xlings::paths::indices_of(dir), (std::vector{"xim", "mcpp-index"})); + expect(eq(mcpp::provider::xlings::paths::indices_of(dir), (std::vector{"xim", "mcpp-index"}))); std::filesystem::remove_all(dir); -} +}}; -TEST(XlingsOwnedIndex, AHomeFromBeforeHasItsIndexMovedOnce) { +UTest a_home_from_before_has_its_index_moved_once{"XlingsOwnedIndex/AHomeFromBeforeHasItsIndexMovedOnce", [] { auto dir = make_tempdir("mcpp-own-indices"); mcpp::provider::xlings::Env env; env.home = dir / "registry"; @@ -509,28 +509,28 @@ TEST(XlingsOwnedIndex, AHomeFromBeforeHasItsIndexMovedOnce) { owned.store = dir / "xpkgs"; std::vector repos{ owned }; mcpp::provider::xlings::paths::own_indices(env, repos); - EXPECT_FALSE(std::filesystem::exists(old)); - EXPECT_TRUE(std::filesystem::exists(owned.dir / "pkgs" / "f" / "foo.lua")); + expect(!std::filesystem::exists(old)); + expect(std::filesystem::exists(owned.dir / "pkgs" / "f" / "foo.lua")); const auto declared = mcpp::provider::xlings::paths::declared_indices(env); - ASSERT_EQ(declared.size(), 1u); - EXPECT_TRUE(declared[0].owned); + expect(fatal(eq(declared.size(), 1u))); + expect(bool(declared[0].owned)); // Read from both places: the provider's data dir and the owned one. const auto roots = mcpp::provider::xlings::paths::index_roots(env); - EXPECT_NE(std::ranges::find(roots, owned.dir.lexically_normal()), roots.end()); - EXPECT_NE(std::ranges::find(roots, env.home / "data" / "xim-pkgindex"), roots.end()); + expect(ne(std::ranges::find(roots, owned.dir.lexically_normal()), roots.end())); + expect(ne(std::ranges::find(roots, env.home / "data" / "xim-pkgindex"), roots.end())); // Once: data that reappears at the old place is not moved over the new. std::filesystem::create_directories(old / "pkgs"); mcpp::provider::xlings::paths::own_indices(env, repos); - EXPECT_TRUE(std::filesystem::exists(old)); - EXPECT_TRUE(std::filesystem::exists(owned.dir / "pkgs" / "f" / "foo.lua")); + expect(std::filesystem::exists(old)); + expect(std::filesystem::exists(owned.dir / "pkgs" / "f" / "foo.lua")); // And it is not read as a second index. const auto again = mcpp::provider::xlings::paths::index_roots(env); - EXPECT_EQ(std::ranges::find(again, old), again.end()); + expect(eq(std::ranges::find(again, old), again.end())); std::filesystem::remove_all(dir); -} +}}; -TEST(XlingsOwnedIndex, APayloadIsFoundInItsIndexStoreThenInTheHomes) { +UTest a_payload_is_found_in_its_index_store_then_in_the_homes{"XlingsOwnedIndex/APayloadIsFoundInItsIndexStoreThenInTheHomes", [] { auto dir = make_tempdir("mcpp-owned-payload"); mcpp::provider::xlings::Env env; env.home = dir / "registry"; @@ -544,17 +544,17 @@ TEST(XlingsOwnedIndex, APayloadIsFoundInItsIndexStoreThenInTheHomes) { std::filesystem::create_directories(dir / "xpkgs" / "mcpplibs-x-cmdline" / "0.0.1" / "src"); using mcpp::provider::xlings::paths::xpkg_payload; auto zlib = xpkg_payload(env, { "compat", "zlib", "1.3.1" }); - ASSERT_TRUE(zlib.has_value()); - EXPECT_EQ(*zlib, env.home / "data" / "xpkgs" / "compat-x-zlib" / "1.3.1"); + expect(fatal(zlib.has_value())); + expect(eq(*zlib, env.home / "data" / "xpkgs" / "compat-x-zlib" / "1.3.1")); auto cmdline = xpkg_payload(env, { "mcpplibs", "cmdline", "" }); - ASSERT_TRUE(cmdline.has_value()); - EXPECT_EQ(*cmdline, (dir / "xpkgs").lexically_normal() / "mcpplibs-x-cmdline" / "0.0.1"); + expect(fatal(cmdline.has_value())); + expect(eq(*cmdline, (dir / "xpkgs").lexically_normal() / "mcpplibs-x-cmdline" / "0.0.1")); std::filesystem::remove_all(dir); -} +}}; // ─── Index identity + clock-skew hardening (#315) ──────────────────────── -TEST(XlingsIndexFreshness, FutureMarkerIsStaleNotEternallyFresh) { +UTest future_marker_is_stale_not_eternally_fresh{"XlingsIndexFreshness/FutureMarkerIsStaleNotEternallyFresh", [] { auto home = make_tempdir("mcpp-xlings-index-future"); auto dir = home / "data" / "mcpplibs"; std::filesystem::create_directories(dir / "pkgs"); @@ -570,40 +570,40 @@ TEST(XlingsIndexFreshness, FutureMarkerIsStaleNotEternallyFresh) { mcpp::provider::xlings::Env env{.home = home}; auto st = mcpp::provider::xlings::default_index_status(env, 3600); - EXPECT_FALSE(st.fresh); - EXPECT_EQ(st.ageSeconds, -1); // unusable timestamp reports as unknown + expect(!st.fresh); + expect(eq(st.ageSeconds, -1)); // unusable timestamp reports as unknown std::filesystem::remove_all(home); -} +}}; -TEST(XlingsIndexRevision, ReadsTrimmedValueAndDegradesToNullopt) { +UTest reads_trimmed_value_and_degrades_to_nullopt{"XlingsIndexRevision/ReadsTrimmedValueAndDegradesToNullopt", [] { auto home = make_tempdir("mcpp-xlings-index-rev"); auto dir = home / "data" / "mcpplibs"; std::filesystem::create_directories(dir / "pkgs"); // Absent: a local `path` index has no such file — must not be an error. - EXPECT_FALSE(mcpp::provider::xlings::index_revision(dir).has_value()); + expect(!(mcpp::provider::xlings::index_revision(dir).has_value())); // Present. The observed files carry no trailing newline, but a writer that // added one (or a \r from Windows) must not change the value, or every run // would look like the index had changed. std::ofstream(dir / ".xlings-index-version") << " 8d67478\r\n"; auto rev = mcpp::provider::xlings::index_revision(dir); - ASSERT_TRUE(rev.has_value()); - EXPECT_EQ(*rev, "8d67478"); + expect(fatal(rev.has_value())); + expect(eq(*rev, "8d67478")); // Blank is not an identity. std::ofstream(dir / ".xlings-index-version", std::ios::trunc) << "\n"; - EXPECT_FALSE(mcpp::provider::xlings::index_revision(dir).has_value()); + expect(!(mcpp::provider::xlings::index_revision(dir).has_value())); // The value is OPAQUE: sub-indexes carry a date version, not a sha. std::ofstream(dir / ".xlings-index-version", std::ios::trunc) << "2026.7.30.1"; - EXPECT_EQ(mcpp::provider::xlings::index_revision(dir).value(), "2026.7.30.1"); + expect(eq(mcpp::provider::xlings::index_revision(dir).value(), "2026.7.30.1")); std::filesystem::remove_all(home); -} +}}; -TEST(XlingsIndexRevision, StatusCarriesTheRevision) { +UTest status_carries_the_revision{"XlingsIndexRevision/StatusCarriesTheRevision", [] { auto home = make_tempdir("mcpp-xlings-index-rev-status"); auto dir = home / "data" / "mcpplibs"; std::filesystem::create_directories(dir / "pkgs"); @@ -613,11 +613,11 @@ TEST(XlingsIndexRevision, StatusCarriesTheRevision) { mcpp::provider::xlings::Env env{.home = home}; auto st = mcpp::provider::xlings::default_index_status(env, 3600); - ASSERT_TRUE(st.rev.has_value()); - EXPECT_EQ(*st.rev, "abc1234"); + expect(fatal(st.rev.has_value())); + expect(eq(*st.rev, "abc1234")); std::filesystem::remove_all(home); -} +}}; // ─── stderr_error_tail (#614) ───────────────────────────────────────── // @@ -625,32 +625,30 @@ TEST(XlingsIndexRevision, StatusCarriesTheRevision) { // selection keeps the lines that name a rejection and bounds them, so a verbose // child cannot bury the diagnostic they are attached to. -TEST(XlingsStderrTail, KeepsOnlyErrorLevelLines) { +UTest keeps_only_error_level_lines{"XlingsStderrTail/KeepsOnlyErrorLevelLines", [] { auto tail = mcpp::provider::xlings::stderr_error_tail( "[xim] resolving acme.widget\r\n" "downloading 42%\n" "Error: checksum mismatch\n" "hint: E_NETWORK, retry later\n" "done\n"); - EXPECT_EQ(tail, (std::vector{ + expect(eq(tail, (std::vector{ "[xim] resolving acme.widget", "Error: checksum mismatch", - "hint: E_NETWORK, retry later"})); -} + "hint: E_NETWORK, retry later"}))); +}}; -TEST(XlingsStderrTail, KeepsTheLastLinesUpToTheLimit) { +UTest keeps_the_last_lines_up_to_the_limit{"XlingsStderrTail/KeepsTheLastLinesUpToTheLimit", [] { std::string text; for (int i = 0; i < 30; ++i) text += std::format("error {}\n", i); auto tail = mcpp::provider::xlings::stderr_error_tail(text); - ASSERT_EQ(tail.size(), 20u); - EXPECT_EQ(tail.front(), "error 10"); - EXPECT_EQ(tail.back(), "error 29"); - EXPECT_EQ(mcpp::provider::xlings::stderr_error_tail(text, 2), - (std::vector{"error 28", "error 29"})); - EXPECT_TRUE(mcpp::provider::xlings::stderr_error_tail("").empty()); - EXPECT_EQ(mcpp::provider::xlings::stderr_error_tail("error without a newline"), - (std::vector{"error without a newline"})); -} + expect(fatal(eq(tail.size(), 20u))); + expect(eq(tail.front(), "error 10")); + expect(eq(tail.back(), "error 29")); + expect(eq(mcpp::provider::xlings::stderr_error_tail(text, 2), (std::vector{"error 28", "error 29"}))); + expect(mcpp::provider::xlings::stderr_error_tail("").empty()); + expect(eq(mcpp::provider::xlings::stderr_error_tail("error without a newline"), (std::vector{"error without a newline"}))); +}}; // ─── invocation_env and the started xlings (#614) ───────────────────── // @@ -678,21 +676,21 @@ mcpp::provider::xlings::InvocationVar decided(const mcpp::provider::xlings::Env& } // namespace -TEST(XlingsInvocationEnv, GlobalModeIsAnAbsentProjectDirectory) { +UTest global_mode_is_an_absent_project_directory{"XlingsInvocationEnv/GlobalModeIsAnAbsentProjectDirectory", [] { auto global = xlings_env(""); auto scope = decided(global, "XLINGS_PROJECT_DIR"); - ASSERT_EQ(scope.name, "XLINGS_PROJECT_DIR"); - EXPECT_FALSE(scope.present); + expect(fatal(eq(scope.name, "XLINGS_PROJECT_DIR"))); + expect(!scope.present); auto home = decided(global, "XLINGS_HOME"); - ASSERT_EQ(home.name, "XLINGS_HOME"); - EXPECT_TRUE(home.present); - EXPECT_EQ(home.value, global.home.string()); + expect(fatal(eq(home.name, "XLINGS_HOME"))); + expect(bool(home.present)); + expect(eq(home.value, global.home.string())); auto project = xlings_env("proj-dir"); auto projectScope = decided(project, "XLINGS_PROJECT_DIR"); - EXPECT_TRUE(projectScope.present); - EXPECT_EQ(projectScope.value, project.projectDir.string()); -} + expect(bool(projectScope.present)); + expect(eq(projectScope.value, project.projectDir.string())); +}}; // What a started xlings receives, recorded by a stub in place of the binary: // its working directory, then its environment. @@ -721,8 +719,8 @@ Seen started_with(const mcpp::provider::xlings::Env& e) { // front of PATH used to stay in the process on Windows, so every action of a // build that had installed a payload found `xim:llvm`'s `cl`, `link` and `lib` // shims in front of MSVC's tools, while a build that installed nothing did not. -TEST(XlingsInvocationEnv, TheDecisionReachesTheStartedXlingsOnly) { - if constexpr (mcpplibs::os::is_windows) GTEST_SKIP() << "a POSIX stub records what xlings received"; +UTest the_decision_reaches_the_started_xlings_only{"XlingsInvocationEnv/TheDecisionReachesTheStartedXlingsOnly", [] { + if constexpr (mcpplibs::os::is_windows) skip("a POSIX stub records what xlings received"); namespace env = mcpp::invocation; env::ScopedEnv home("XLINGS_HOME", std::string("shell-home")); env::ScopedEnv subos("XLINGS_ACTIVE_SUBOS", std::string("shell-subos")); @@ -731,59 +729,57 @@ TEST(XlingsInvocationEnv, TheDecisionReachesTheStartedXlingsOnly) { auto global = xlings_env(""); auto g = started_with(global); - EXPECT_EQ(std::filesystem::path(g.cwd), std::filesystem::weakly_canonical(global.home)); - EXPECT_EQ(g.env["XLINGS_HOME"], global.home.string()); - EXPECT_FALSE(g.env.contains("XLINGS_PROJECT_DIR")); - EXPECT_FALSE(g.env.contains("XLINGS_ACTIVE_SUBOS")); + expect(eq(std::filesystem::path(g.cwd), std::filesystem::weakly_canonical(global.home))); + expect(eq(g.env["XLINGS_HOME"], global.home.string())); + expect(!g.env.contains("XLINGS_PROJECT_DIR")); + expect(!g.env.contains("XLINGS_ACTIVE_SUBOS")); const auto bin = (global.home / "subos" / "default" / "bin").string(); - EXPECT_EQ(g.env["PATH"].substr(0, bin.size()), bin) << g.env["PATH"]; + if (auto&& failed = expect(eq(g.env["PATH"].substr(0, bin.size()), bin)); !failed) failed << g.env["PATH"]; auto inProject = xlings_env("proj-dir"); auto p = started_with(inProject); - EXPECT_EQ(p.env["XLINGS_PROJECT_DIR"], inProject.projectDir.string()); - EXPECT_FALSE(p.env.contains("XLINGS_ACTIVE_SUBOS")); + expect(eq(p.env["XLINGS_PROJECT_DIR"], inProject.projectDir.string())); + expect(!p.env.contains("XLINGS_ACTIVE_SUBOS")); - EXPECT_EQ(env::get("PATH"), pathBefore); - EXPECT_EQ(env::get("XLINGS_HOME"), std::optional("shell-home")); - EXPECT_EQ(env::get("XLINGS_ACTIVE_SUBOS"), std::optional("shell-subos")); - EXPECT_EQ(env::get("XLINGS_PROJECT_DIR"), std::optional("prior-project")); -} + expect(eq(env::get("PATH"), pathBefore)); + expect(eq(env::get("XLINGS_HOME"), std::optional("shell-home"))); + expect(eq(env::get("XLINGS_ACTIVE_SUBOS"), std::optional("shell-subos"))); + expect(eq(env::get("XLINGS_PROJECT_DIR"), std::optional("prior-project"))); +}}; // #726: on Windows too the registry's xlings starts in its home, never in the // directory mcpp was started from, so a project's `.xlings.json` cannot put it // in that project's mode. -TEST(XlingsInvocationEnv, TheWindowsPrefixStartsInTheHome) { +UTest the_windows_prefix_starts_in_the_home{"XlingsInvocationEnv/TheWindowsPrefixStartsInTheHome", [] { mcpp::provider::xlings::Env e; e.home = "C:\\Users\\a b\\.mcpp\\registry"; e.binary = "C:\\Users\\a b\\.mcpp\\registry\\bin\\xlings.exe"; - EXPECT_EQ(mcpp::provider::xlings::windows_command_prefix(e), - "cd /d \"C:\\Users\\a b\\.mcpp\\registry\" && " - "\"C:\\Users\\a b\\.mcpp\\registry\\bin\\xlings.exe\""); -} + expect(eq(mcpp::provider::xlings::windows_command_prefix(e), "cd /d \"C:\\Users\\a b\\.mcpp\\registry\" && " + "\"C:\\Users\\a b\\.mcpp\\registry\\bin\\xlings.exe\"")); +}}; -TEST(XlingsInvocationEnv, ThePosixPrefixRendersTheDecision) { - if constexpr (mcpplibs::os::is_windows) GTEST_SKIP() << "the POSIX spelling"; +UTest the_posix_prefix_renders_the_decision{"XlingsInvocationEnv/ThePosixPrefixRendersTheDecision", [] { + if constexpr (mcpplibs::os::is_windows) skip("the POSIX spelling"); auto global = mcpp::provider::xlings::build_command_prefix(xlings_env("")); - EXPECT_NE(global.find("env -u XLINGS_PROJECT_DIR -u XLINGS_ACTIVE_SUBOS PATH="), - std::string::npos) << global; - EXPECT_EQ(global.find("XLINGS_PROJECT_DIR="), std::string::npos) << global; + expect(ne(global.find("env -u XLINGS_PROJECT_DIR -u XLINGS_ACTIVE_SUBOS PATH="), std::string::npos)) << global; + expect(eq(global.find("XLINGS_PROJECT_DIR="), std::string::npos)) << global; auto project = mcpp::provider::xlings::build_command_prefix(xlings_env("/work/proj")); - EXPECT_EQ(project.find("-u XLINGS_PROJECT_DIR"), std::string::npos) << project; - EXPECT_NE(project.find("env -u XLINGS_ACTIVE_SUBOS PATH="), std::string::npos) << project; - EXPECT_NE(project.find("XLINGS_PROJECT_DIR="), std::string::npos) << project; - EXPECT_NE(project.find("/work/proj"), std::string::npos) << project; -} + expect(eq(project.find("-u XLINGS_PROJECT_DIR"), std::string::npos)) << project; + expect(ne(project.find("env -u XLINGS_ACTIVE_SUBOS PATH="), std::string::npos)) << project; + expect(ne(project.find("XLINGS_PROJECT_DIR="), std::string::npos)) << project; + expect(ne(project.find("/work/proj"), std::string::npos)) << project; +}}; // A shell that ran `xlings subos use ` exports XLINGS_ACTIVE_SUBOS, which // names a SubOS of the shell's xlings home and not of mcpp's registry; no // invocation carries it, in either mode. -TEST(XlingsInvocationEnv, TheShellsActiveSubosIsNeverInherited) { +UTest the_shells_active_subos_is_never_inherited{"XlingsInvocationEnv/TheShellsActiveSubosIsNeverInherited", [] { auto global = decided(xlings_env(""), "XLINGS_ACTIVE_SUBOS"); - ASSERT_EQ(global.name, "XLINGS_ACTIVE_SUBOS"); - EXPECT_FALSE(global.present); + expect(fatal(eq(global.name, "XLINGS_ACTIVE_SUBOS"))); + expect(!global.present); auto project = decided(xlings_env("proj-dir"), "XLINGS_ACTIVE_SUBOS"); - ASSERT_EQ(project.name, "XLINGS_ACTIVE_SUBOS"); - EXPECT_FALSE(project.present); + expect(fatal(eq(project.name, "XLINGS_ACTIVE_SUBOS"))); + expect(!project.present); -} +}}; diff --git a/tests/unit/test_xlings_address_set.cpp b/tests/unit/test_xlings_address_set.cpp index 182af0422..942457220 100644 --- a/tests/unit/test_xlings_address_set.cpp +++ b/tests/unit/test_xlings_address_set.cpp @@ -1,8 +1,10 @@ -#include import std; +import mcpplibs.mtest; import mcpp.provider.address_set; +using namespace mcpplibs::mtest; + namespace addrset = mcpp::provider::addrset; namespace { @@ -27,19 +29,19 @@ std::vector addresses(const addrset::Resolution& r) { // The version is a constraint on a package, never part of its name. Two // definitions of this used to coexist: the conditional merge compared the bare // name, the graph split compared the whole address string. -TEST(XlingsIdentity, IsTheNamespaceAndNameAndNeverTheVersion) { - EXPECT_EQ(addrset::package_key("xim:glibc@2.40"), "xim:glibc"); - EXPECT_EQ(addrset::package_key("xim:glibc"), "xim:glibc"); - EXPECT_EQ(addrset::package_key("xim:glibc@>=2.38"), "xim:glibc"); +UTest is_the_namespace_and_name_and_never_the_version{"XlingsIdentity/IsTheNamespaceAndNameAndNeverTheVersion", [] { + expect(eq(addrset::package_key("xim:glibc@2.40"), "xim:glibc")); + expect(eq(addrset::package_key("xim:glibc"), "xim:glibc")); + expect(eq(addrset::package_key("xim:glibc@>=2.38"), "xim:glibc")); // The manifest's default namespace, spelled the same way `parse_xpkg_ref` // spells it: `ninja` and `xim:ninja` name one package. - EXPECT_EQ(addrset::package_key("ninja"), "xim:ninja"); + expect(eq(addrset::package_key("ninja"), "xim:ninja")); // …and a different index is a different package, which the bare-name // definition could not express. - EXPECT_NE(addrset::package_key("scode:cuda"), addrset::package_key("xim:cuda")); -} + expect(ne(addrset::package_key("scode:cuda"), addrset::package_key("xim:cuda"))); +}}; // ─── Adjudication ────────────────────────────────────────────────────────── @@ -47,162 +49,160 @@ TEST(XlingsIdentity, IsTheNamespaceAndNameAndNeverTheVersion) { // the project owns the exact version. Two statements, one install, and no // warning -- a note here would fire on every build of every project that pins a // tool its rule also requires, which is the intended arrangement. -TEST(XlingsUnify, OnePackageYieldsOneAddress) { +UTest one_package_yields_one_address{"XlingsUnify/OnePackageYieldsOneAddress", [] { std::vector claims{ project("xim:cuda-nvcc@13.3.33"), from("mcpp:plugins", "xim:cuda-nvcc@>=12.9.86"), }; auto r = addrset::unify(claims); - ASSERT_TRUE(r.has_value()) << r.error(); - EXPECT_EQ(addresses(*r), (std::vector{"xim:cuda-nvcc@13.3.33"})); - EXPECT_TRUE(r->overrides.empty()); -} + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); + expect(eq(addresses(*r), (std::vector{"xim:cuda-nvcc@13.3.33"}))); + expect(r->overrides.empty()); +}}; // …and when the two really do disagree, the note names both sides and says // which was used. Reported, not inferred from an install log. -TEST(XlingsUnify, AResolvedDisagreementNamesBothSidesAndTheWinner) { +UTest a_resolved_disagreement_names_both_sides_and_the_winner{"XlingsUnify/AResolvedDisagreementNamesBothSidesAndTheWinner", [] { std::vector claims{ project("xim:cuda-nvcc@13.3.33"), from("mcpp:plugins", "xim:cuda-nvcc@12.9.86"), }; auto r = addrset::unify(claims); - ASSERT_TRUE(r.has_value()) << r.error(); - ASSERT_EQ(r->overrides.size(), 1u); - EXPECT_NE(r->overrides[0].find("mcpp:plugins"), std::string::npos); - EXPECT_NE(r->overrides[0].find("this project"), std::string::npos); - EXPECT_NE(r->overrides[0].find("12.9.86"), std::string::npos); - EXPECT_NE(r->overrides[0].find("13.3.33"), std::string::npos); -} + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); + expect(fatal(eq(r->overrides.size(), 1u))); + expect(ne(r->overrides[0].find("mcpp:plugins"), std::string::npos)); + expect(ne(r->overrides[0].find("this project"), std::string::npos)); + expect(ne(r->overrides[0].find("12.9.86"), std::string::npos)); + expect(ne(r->overrides[0].find("13.3.33"), std::string::npos)); +}}; // A claim naming no version states that the package is wanted and nothing about // which version, so it must not out-rank a floor merely by being nearer. -TEST(XlingsUnify, AnUnversionedClaimAbstainsFromTheVersionQuestion) { +UTest an_unversioned_claim_abstains_from_the_version_question{"XlingsUnify/AnUnversionedClaimAbstainsFromTheVersionQuestion", [] { std::vector claims{ project("xim:cann-toolkit"), from("mcpp:plugins", "xim:cann-toolkit@>=8.5.0"), }; auto r = addrset::unify(claims); - ASSERT_TRUE(r.has_value()) << r.error(); - EXPECT_EQ(addresses(*r), - (std::vector{"xim:cann-toolkit@>=8.5.0"})); - EXPECT_TRUE(r->overrides.empty()); -} + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); + expect(eq(addresses(*r), (std::vector{"xim:cann-toolkit@>=8.5.0"}))); + expect(r->overrides.empty()); +}}; -TEST(XlingsUnify, WithNoVersionAnywhereTheBareAddressSurvives) { +UTest with_no_version_anywhere_the_bare_address_survives{"XlingsUnify/WithNoVersionAnywhereTheBareAddressSurvives", [] { std::vector claims{ project("xim:ninja"), from("board:virt", "ninja"), }; auto r = addrset::unify(claims); - ASSERT_TRUE(r.has_value()) << r.error(); - EXPECT_EQ(addresses(*r), (std::vector{"xim:ninja"})); -} + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); + expect(eq(addresses(*r), (std::vector{"xim:ninja"}))); +}}; // Order is the order packages were first claimed, so a build's install list // does not permute when an unrelated dependency is added. -TEST(XlingsUnify, KeepsFirstClaimedOrder) { +UTest keeps_first_claimed_order{"XlingsUnify/KeepsFirstClaimedOrder", [] { std::vector claims{ project("xim:b@1.0"), project("xim:a@1.0"), from("dep", "xim:a@>=1.0"), from("dep", "xim:c@1.0"), }; auto r = addrset::unify(claims); - ASSERT_TRUE(r.has_value()) << r.error(); - EXPECT_EQ(addresses(*r), - (std::vector{"xim:b@1.0", "xim:a@1.0", "xim:c@1.0"})); -} + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); + expect(eq(addresses(*r), (std::vector{"xim:b@1.0", "xim:a@1.0", "xim:c@1.0"}))); +}}; // ─── Validation ──────────────────────────────────────────────────────────── // The one refusal this module adds. It is a comparison against a chosen // version, not a search for one: nothing here consults which versions exist. -TEST(XlingsUnify, RefusesAPinBelowAStatedFloorAndNamesBothSides) { +UTest refuses_a_pin_below_a_stated_floor_and_names_both_sides{"XlingsUnify/RefusesAPinBelowAStatedFloorAndNamesBothSides", [] { std::vector claims{ project("xim:cann-toolkit@8.0.0"), from("mcpp:plugins", "xim:cann-toolkit@>=8.5.0"), }; auto r = addrset::unify(claims); - ASSERT_FALSE(r.has_value()); - EXPECT_NE(r.error().find("xim:cann-toolkit"), std::string::npos); - EXPECT_NE(r.error().find("8.0.0"), std::string::npos); - EXPECT_NE(r.error().find(">=8.5.0"), std::string::npos); - EXPECT_NE(r.error().find("mcpp:plugins"), std::string::npos); + expect(fatal(!r.has_value())); + expect(ne(r.error().find("xim:cann-toolkit"), std::string::npos)); + expect(ne(r.error().find("8.0.0"), std::string::npos)); + expect(ne(r.error().find(">=8.5.0"), std::string::npos)); + expect(ne(r.error().find("mcpp:plugins"), std::string::npos)); // The way out is in the message, which is what makes the shortcut of not // intersecting constraints an acceptable one. - EXPECT_NE(r.error().find("drop the pin"), std::string::npos); -} + expect(ne(r.error().find("drop the pin"), std::string::npos)); +}}; -TEST(XlingsUnify, AcceptsAPinThatSatisfiesTheFloor) { +UTest accepts_a_pin_that_satisfies_the_floor{"XlingsUnify/AcceptsAPinThatSatisfiesTheFloor", [] { std::vector claims{ project("xim:cann-toolkit@8.5.0"), from("mcpp:plugins", "xim:cann-toolkit@>=8.5.0"), }; auto r = addrset::unify(claims); - ASSERT_TRUE(r.has_value()) << r.error(); - EXPECT_EQ(addresses(*r), (std::vector{"xim:cann-toolkit@8.5.0"})); + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); + expect(eq(addresses(*r), (std::vector{"xim:cann-toolkit@8.5.0"}))); // Satisfied is silent: an override note here would fire on every build of // every project that pins a tool its rule also requires, which is the // intended arrangement rather than a problem. - EXPECT_TRUE(r->overrides.empty()); -} + expect(r->overrides.empty()); +}}; // TWO EXACT PINS ARE TWO CHOICES, NOT A VIOLATED REQUIREMENT. Refusing them // would turn every dependency that pinned a tool the project also pins into a // hard failure on upgrade, for a disagreement adjudication already settles. -TEST(XlingsUnify, TwoExactPinsAreAdjudicatedRatherThanRefused) { +UTest two_exact_pins_are_adjudicated_rather_than_refused{"XlingsUnify/TwoExactPinsAreAdjudicatedRatherThanRefused", [] { std::vector claims{ project("xim:cuda-nvcc@13.3.33"), from("mcpp:plugins", "xim:cuda-nvcc@12.9.86"), }; auto r = addrset::unify(claims); - ASSERT_TRUE(r.has_value()) << r.error(); - EXPECT_EQ(addresses(*r), (std::vector{"xim:cuda-nvcc@13.3.33"})); - EXPECT_EQ(r->overrides.size(), 1u); -} + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); + expect(eq(addresses(*r), (std::vector{"xim:cuda-nvcc@13.3.33"}))); + expect(eq(r->overrides.size(), 1u)); +}}; // Two dependencies, no project statement: there is no "nearer", so the tie is // broken by first appearance -- and the validation still runs, which is what // keeps an arbitrary tie-break from silently lowering a floor. -TEST(XlingsUnify, TwoDependenciesInConflictAreStillValidated) { +UTest two_dependencies_in_conflict_are_still_validated{"XlingsUnify/TwoDependenciesInConflictAreStillValidated", [] { std::vector ok{ from("dep-a", "xim:tool@2.0.0"), from("dep-b", "xim:tool@>=1.0.0"), }; - ASSERT_TRUE(addrset::unify(ok).has_value()); + expect(fatal(addrset::unify(ok).has_value())); std::vector bad{ from("dep-a", "xim:tool@1.0.0"), from("dep-b", "xim:tool@>=2.0.0"), }; auto r = addrset::unify(bad); - ASSERT_FALSE(r.has_value()); - EXPECT_NE(r.error().find("dep-a"), std::string::npos); - EXPECT_NE(r.error().find("dep-b"), std::string::npos); -} + expect(fatal(!r.has_value())); + expect(ne(r.error().find("dep-a"), std::string::npos)); + expect(ne(r.error().find("dep-b"), std::string::npos)); +}}; // A version this grammar cannot parse is not evidence of a conflict. Refusing // on a spelling nobody can evaluate would be a refusal manufactured from // ignorance -- `8.0.RC1` is a real published version. -TEST(XlingsUnify, AnUnevaluableSpellingIsReportedNotRefused) { +UTest an_unevaluable_spelling_is_reported_not_refused{"XlingsUnify/AnUnevaluableSpellingIsReportedNotRefused", [] { std::vector claims{ project("xim:cann-toolkit@8.0.RC1"), from("mcpp:plugins", "xim:cann-toolkit@>=8.5.0"), }; auto r = addrset::unify(claims); - ASSERT_TRUE(r.has_value()) << r.error(); - EXPECT_EQ(addresses(*r), (std::vector{"xim:cann-toolkit@8.0.RC1"})); - EXPECT_EQ(r->overrides.size(), 1u); -} + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); + expect(eq(addresses(*r), (std::vector{"xim:cann-toolkit@8.0.RC1"}))); + expect(eq(r->overrides.size(), 1u)); +}}; // The same spelling twice is one statement, not a disagreement. Without this a // project that is its own runtime owner would warn about itself on every build. -TEST(XlingsUnify, RepeatingOneSpellingIsSilent) { +UTest repeating_one_spelling_is_silent{"XlingsUnify/RepeatingOneSpellingIsSilent", [] { std::vector claims{ project("xim:shaderc@2026.3"), project("xim:shaderc@2026.3"), from("dep", "xim:shaderc@2026.3"), }; auto r = addrset::unify(claims); - ASSERT_TRUE(r.has_value()) << r.error(); - EXPECT_EQ(addresses(*r), (std::vector{"xim:shaderc@2026.3"})); - EXPECT_TRUE(r->overrides.empty()); -} + if (auto&& failed = expect(fatal(r.has_value())); !failed) failed << r.error(); + expect(eq(addresses(*r), (std::vector{"xim:shaderc@2026.3"}))); + expect(r->overrides.empty()); +}}; diff --git a/tests/unit/test_xlings_version_pin.cpp b/tests/unit/test_xlings_version_pin.cpp index 6a39f0b4e..59efa3b6e 100644 --- a/tests/unit/test_xlings_version_pin.cpp +++ b/tests/unit/test_xlings_version_pin.cpp @@ -12,45 +12,48 @@ // put a newer xlings there must not be downgraded by an mcpp pinning an older // one, and an unparseable version is not evidence of being behind. -#include #include #include import std; +import mcpplibs.mtest; +import mcpplibs.os.stdio; import mcpp.invocation; import mcpp.fallback.xlings_binary; +using namespace mcpplibs::mtest; + namespace fb = mcpp::fallback; namespace { -TEST(XlingsVersionPin, OlderIsBehind) { - EXPECT_TRUE(fb::version_is_older("2026.8.2.1", "2026.8.6.3")); - EXPECT_TRUE(fb::version_is_older("2026.7.31.3", "2026.8.1.1")); - EXPECT_TRUE(fb::version_is_older("2026.8.6", "2026.8.6.1")); -} +UTest older_is_behind{"XlingsVersionPin/OlderIsBehind", [] { + expect(fb::version_is_older("2026.8.2.1", "2026.8.6.3")); + expect(fb::version_is_older("2026.7.31.3", "2026.8.1.1")); + expect(fb::version_is_older("2026.8.6", "2026.8.6.1")); +}}; -TEST(XlingsVersionPin, EqualIsNotBehind) { - EXPECT_FALSE(fb::version_is_older("2026.8.6.3", "2026.8.6.3")); +UTest equal_is_not_behind{"XlingsVersionPin/EqualIsNotBehind", [] { + expect(!fb::version_is_older("2026.8.6.3", "2026.8.6.3")); // Trailing zeros compare equal, not older. - EXPECT_FALSE(fb::version_is_older("2026.8.6.0", "2026.8.6")); -} + expect(!fb::version_is_older("2026.8.6.0", "2026.8.6")); +}}; // The case that decides whether this is safe to run automatically: a user who // put a NEWER xlings in place keeps it. -TEST(XlingsVersionPin, NewerIsNotDowngraded) { - EXPECT_FALSE(fb::version_is_older("2026.9.1.1", "2026.8.6.3")); - EXPECT_FALSE(fb::version_is_older("2027.1.1.1", "2026.12.31.9")); -} +UTest newer_is_not_downgraded{"XlingsVersionPin/NewerIsNotDowngraded", [] { + expect(!fb::version_is_older("2026.9.1.1", "2026.8.6.3")); + expect(!fb::version_is_older("2027.1.1.1", "2026.12.31.9")); +}}; // A version this code cannot parse says nothing, and "says nothing" must not // be read as "is behind" -- that would delete a working binary on a guess. -TEST(XlingsVersionPin, UnparseableIsNotBehind) { - EXPECT_FALSE(fb::version_is_older("dev", "2026.8.6.3")); - EXPECT_FALSE(fb::version_is_older("2026.8.6.3", "")); - EXPECT_FALSE(fb::version_is_older("", "2026.8.6.3")); - EXPECT_FALSE(fb::version_is_older("2026.8.x", "2026.8.6.3")); -} +UTest unparseable_is_not_behind{"XlingsVersionPin/UnparseableIsNotBehind", [] { + expect(!fb::version_is_older("dev", "2026.8.6.3")); + expect(!fb::version_is_older("2026.8.6.3", "")); + expect(!fb::version_is_older("", "2026.8.6.3")); + expect(!fb::version_is_older("2026.8.x", "2026.8.6.3")); +}}; // The version scheme changed epochs: xlings went 0.4.x -> YYYY.M.D.N. Both // live on the same disk, and a home's system xlings may still be a 0.4.x while @@ -60,11 +63,11 @@ TEST(XlingsVersionPin, UnparseableIsNotBehind) { // 2026.8.2.1 into 0.4.51: older still, and equally missing the very feature // the replacement existed to restore. Being behind the pin justifies looking // for a replacement; it does not justify accepting whatever turns up. -TEST(XlingsVersionPin, DatedSchemeIsNewerThanTheOldOne) { - EXPECT_FALSE(fb::version_is_older("2026.8.2.1", "0.4.51")); - EXPECT_TRUE(fb::version_is_older("0.4.51", "2026.8.2.1")); - EXPECT_TRUE(fb::version_is_older("0.4.51", "0.4.54")); -} +UTest dated_scheme_is_newer_than_the_old_one{"XlingsVersionPin/DatedSchemeIsNewerThanTheOldOne", [] { + expect(!fb::version_is_older("2026.8.2.1", "0.4.51")); + expect(fb::version_is_older("0.4.51", "2026.8.2.1")); + expect(fb::version_is_older("0.4.51", "0.4.54")); +}}; // THE PROBE READS STANDARD OUTPUT, AND ON WINDOWS IT RUNS AT ALL. // @@ -76,7 +79,7 @@ TEST(XlingsVersionPin, DatedSchemeIsNewerThanTheOldOne) { // this test runs a .bat through the real launcher, which is the path that // failed; elsewhere a shell script. Both print a dotted number on stderr first, // which must not be taken for the version. -TEST(XlingsVersionPin, ProbeReadsStandardOutputThroughTheLauncher) { +UTest probe_reads_standard_output_through_the_launcher{"XlingsVersionPin/ProbeReadsStandardOutputThroughTheLauncher", [] { auto dir = std::filesystem::temp_directory_path() / std::format("mcpp probe {}", std::chrono::steady_clock::now().time_since_epoch().count()); std::filesystem::create_directories(dir); @@ -95,10 +98,10 @@ TEST(XlingsVersionPin, ProbeReadsStandardOutputThroughTheLauncher) { std::filesystem::permissions(fake, std::filesystem::perms::owner_all, std::filesystem::perm_options::replace); #endif - EXPECT_EQ(fb::vendored_xlings_version(fake), "2026.1.2.3"); + expect(eq(fb::vendored_xlings_version(fake), "2026.1.2.3")); std::error_code ec; std::filesystem::remove_all(dir, ec); -} +}}; } // namespace @@ -112,45 +115,45 @@ fb::XlingsSource src(std::string v, std::string origin) { } } // namespace -TEST(XlingsSource, TheOverrideIsTakenAsAnExplicitChoice) { +UTest the_override_is_taken_as_an_explicit_choice{"XlingsSource/TheOverrideIsTakenAsAnExplicitChoice", [] { auto c = fb::choose_xlings_source(src("2026.1.1.1", "override"), src("2026.9.1.1", "released"), src("2026.9.9.1", "path")); - ASSERT_TRUE(c); - EXPECT_EQ(c->origin, "override"); -} + expect(fatal(bool(c))); + expect(eq(c->origin, "override")); +}}; -TEST(XlingsSource, TheNewerOfTheReleasedAndThePathCopyIsTaken) { +UTest the_newer_of_the_released_and_the_path_copy_is_taken{"XlingsSource/TheNewerOfTheReleasedAndThePathCopyIsTaken", [] { auto a = fb::choose_xlings_source(std::nullopt, src("2026.9.29.1", "released"), src("2026.9.30.1", "path")); - ASSERT_TRUE(a); - EXPECT_EQ(a->origin, "path"); + expect(fatal(bool(a))); + expect(eq(a->origin, "path")); auto b = fb::choose_xlings_source(std::nullopt, src("2026.9.30.1", "released"), src("2026.9.29.1", "path")); - ASSERT_TRUE(b); - EXPECT_EQ(b->origin, "released"); -} + expect(fatal(bool(b))); + expect(eq(b->origin, "released")); +}}; -TEST(XlingsSource, TheReleasedCopyWinsATieAndAnUnreadablePathCopy) { +UTest the_released_copy_wins_a_tie_and_an_unreadable_path_copy{"XlingsSource/TheReleasedCopyWinsATieAndAnUnreadablePathCopy", [] { auto tie = fb::choose_xlings_source(std::nullopt, src("2026.9.30.1", "released"), src("2026.9.30.1", "path")); - ASSERT_TRUE(tie); - EXPECT_EQ(tie->origin, "released"); + expect(fatal(bool(tie))); + expect(eq(tie->origin, "released")); auto unreadable = fb::choose_xlings_source(std::nullopt, src("2026.9.30.1", "released"), src("", "path")); - ASSERT_TRUE(unreadable); - EXPECT_EQ(unreadable->origin, "released"); + expect(fatal(bool(unreadable))); + expect(eq(unreadable->origin, "released")); auto releasedUnreadable = fb::choose_xlings_source(std::nullopt, src("", "released"), src("2026.9.30.1", "path")); - ASSERT_TRUE(releasedUnreadable); - EXPECT_EQ(releasedUnreadable->origin, "path"); -} + expect(fatal(bool(releasedUnreadable))); + expect(eq(releasedUnreadable->origin, "path")); +}}; -TEST(XlingsSource, AMissingSourceLeavesTheOther) { +UTest a_missing_source_leaves_the_other{"XlingsSource/AMissingSourceLeavesTheOther", [] { auto onlyPath = fb::choose_xlings_source(std::nullopt, std::nullopt, src("1.0", "path")); - ASSERT_TRUE(onlyPath); - EXPECT_EQ(onlyPath->origin, "path"); - EXPECT_FALSE(fb::choose_xlings_source(std::nullopt, std::nullopt, std::nullopt)); -} + expect(fatal(bool(onlyPath))); + expect(eq(onlyPath->origin, "path")); + expect(!fb::choose_xlings_source(std::nullopt, std::nullopt, std::nullopt)); +}}; // THE VERSION IS ASKED ONCE. Asking costs a process of xlings (0.35 s // measured), and every command that loads the configuration asked. Within a @@ -202,43 +205,43 @@ std::string memo_key(const std::filesystem::path& bin) { } } // namespace -TEST(XlingsVersionMemo, AskedOncePerProcessAndStoredForTheNext) { +UTest asked_once_per_process_and_stored_for_the_next{"XlingsVersionMemo/AskedOncePerProcessAndStoredForTheNext", [] { CountingStub stub("2026.1.2.3"); const auto memo = stub.dir / "memo"; - EXPECT_EQ(fb::known_xlings_version(stub.bin, memo), "2026.1.2.3"); - EXPECT_EQ(fb::known_xlings_version(stub.bin, memo), "2026.1.2.3"); - EXPECT_EQ(stub.runs(), 1); + expect(eq(fb::known_xlings_version(stub.bin, memo), "2026.1.2.3")); + expect(eq(fb::known_xlings_version(stub.bin, memo), "2026.1.2.3")); + expect(eq(stub.runs(), 1)); std::ifstream is(memo); std::string line; std::getline(is, line); - EXPECT_EQ(line, memo_key(stub.bin) + "\t2026.1.2.3"); -} + expect(eq(line, memo_key(stub.bin) + "\t2026.1.2.3")); +}}; -TEST(XlingsVersionMemo, AStoredAnswerIsReadWithoutRunningTheBinary) { +UTest a_stored_answer_is_read_without_running_the_binary{"XlingsVersionMemo/AStoredAnswerIsReadWithoutRunningTheBinary", [] { CountingStub stub("2026.1.2.3"); const auto memo = stub.dir / "memo"; { std::ofstream os(memo, std::ios::binary); os << "/elsewhere/xlings\t1\t1\t2020.1.1.1\n" << memo_key(stub.bin) << "\t2026.7.7.7\n"; } - EXPECT_EQ(fb::known_xlings_version(stub.bin, memo), "2026.7.7.7"); - EXPECT_EQ(stub.runs(), 0); -} + expect(eq(fb::known_xlings_version(stub.bin, memo), "2026.7.7.7")); + expect(eq(stub.runs(), 0)); +}}; -TEST(XlingsVersionMemo, ANewFileIsAskedAgain) { +UTest a_new_file_is_asked_again{"XlingsVersionMemo/ANewFileIsAskedAgain", [] { CountingStub stub("2026.1.2.3"); const auto memo = stub.dir / "memo"; - EXPECT_EQ(fb::known_xlings_version(stub.bin, memo), "2026.1.2.3"); + expect(eq(fb::known_xlings_version(stub.bin, memo), "2026.1.2.3")); // An update writes a new file: another size, and another modification time. std::this_thread::sleep_for(std::chrono::milliseconds(20)); stub.write("2026.10.20.30"); - EXPECT_EQ(fb::known_xlings_version(stub.bin, memo), "2026.10.20.30"); - EXPECT_EQ(stub.runs(), 2); + expect(eq(fb::known_xlings_version(stub.bin, memo), "2026.10.20.30")); + expect(eq(stub.runs(), 2)); std::ifstream is(memo); int lines = 0; for (std::string l; std::getline(is, l);) ++lines; - EXPECT_EQ(lines, 1) << "the binary's earlier line is replaced, not kept"; -} + expect(eq(lines, 1)) << "the binary's earlier line is replaced, not kept"; +}}; // ONCE PER PROCESS (mcpp#744). The configuration is loaded from about ten call // sites, and one `mcpp pack` over a workspace printed its note three times per @@ -254,7 +257,7 @@ class ScopedEnv { }; } // namespace -TEST(XlingsAcquire, TheNoteIsStatedOncePerProcess) { +UTest the_note_is_stated_once_per_process{"XlingsAcquire/TheNoteIsStatedOncePerProcess", [] { CountingStub stub("2026.1.1.1"); const auto empty = stub.dir / "empty-path"; std::filesystem::create_directories(empty); @@ -262,17 +265,17 @@ TEST(XlingsAcquire, TheNoteIsStatedOncePerProcess) { // not run from a release layout (`/bin/`). ScopedEnv override_("MCPP_VENDORED_XLINGS", nullptr); ScopedEnv path("PATH", empty.string().c_str()); - testing::internal::CaptureStderr(); + auto stderrCapture = mcpplibs::os::stdio::capture::begin(mcpplibs::os::stdio::stream::error).value(); auto first = fb::acquire_xlings_binary(stub.bin, /*quiet=*/false, "2026.9.30.1"); auto second = fb::acquire_xlings_binary(stub.bin, /*quiet=*/false, "2026.9.30.1"); - const auto err = testing::internal::GetCapturedStderr(); - ASSERT_TRUE(first); - ASSERT_TRUE(second); + const auto err = stderrCapture.text(); + expect(fatal(bool(first))); + expect(fatal(bool(second))); std::size_t notes = 0; for (auto at = err.find("no newer source is available"); at != std::string::npos; at = err.find("no newer source is available", at + 1)) ++notes; - EXPECT_EQ(notes, 1u) << err; - EXPECT_EQ(stub.runs(), 1) << "the second load asked the version again"; -} + expect(eq(notes, 1u)) << err; + expect(eq(stub.runs(), 1)) << "the second load asked the version again"; +}}; diff --git a/tests/unit/test_xpkg_emit.cpp b/tests/unit/test_xpkg_emit.cpp index d335724c7..d42bdd7e1 100644 --- a/tests/unit/test_xpkg_emit.cpp +++ b/tests/unit/test_xpkg_emit.cpp @@ -1,12 +1,14 @@ -#include import std; +import mcpplibs.mtest; import mcpp.manifest; import mcpp.modgraph.graph; import mcpp.invocation; import mcpp.publish.xpkg_emit; import mcpp.diag; +using namespace mcpplibs::mtest; + using namespace mcpp::publish; namespace { @@ -35,32 +37,32 @@ mcpp::modgraph::Graph minimal_graph() { } // namespace -TEST(XpkgEmit, ContainsRequiredFields) { +UTest contains_required_fields{"XpkgEmit/ContainsRequiredFields", [] { auto m = minimal_manifest(); auto g = minimal_graph(); auto rel = placeholder_release("0.1.0"); auto out = emit_xpkg(m, g, rel); - EXPECT_NE(out.find("AUTO-GENERATED"), std::string::npos); - EXPECT_NE(out.find("name = \"mcpplibs.hello\""), std::string::npos); - EXPECT_NE(out.find("import_std = true"), std::string::npos); - EXPECT_NE(out.find("\"mcpplibs.hello\""), std::string::npos); // module - EXPECT_NE(out.find("manifest = \"mcpp.toml\""), std::string::npos); - EXPECT_NE(out.find("[\"mcpplibs.primitives\"]"), std::string::npos); // dep -} + expect(ne(out.find("AUTO-GENERATED"), std::string::npos)); + expect(ne(out.find("name = \"mcpplibs.hello\""), std::string::npos)); + expect(ne(out.find("import_std = true"), std::string::npos)); + expect(ne(out.find("\"mcpplibs.hello\""), std::string::npos)); // module + expect(ne(out.find("manifest = \"mcpp.toml\""), std::string::npos)); + expect(ne(out.find("[\"mcpplibs.primitives\"]"), std::string::npos)); // dep +}}; -TEST(XpkgEmit, RejectsLuaInjection) { +UTest rejects_lua_injection{"XpkgEmit/RejectsLuaInjection", [] { auto m = minimal_manifest(); m.package.description = "evil\"injection"; auto g = minimal_graph(); auto out = emit_xpkg(m, g, placeholder_release("0.1.0")); // The escaped form must be present, NOT the raw quote. - EXPECT_NE(out.find("evil\\\"injection"), std::string::npos); -} + expect(ne(out.find("evil\\\"injection"), std::string::npos)); +}}; -TEST(XpkgEmit, EscapesControlCharacters) { +UTest escapes_control_characters{"XpkgEmit/EscapesControlCharacters", [] { auto m = minimal_manifest(); auto g = minimal_graph(); @@ -70,60 +72,56 @@ TEST(XpkgEmit, EscapesControlCharacters) { auto out = emit_xpkg(m, g, placeholder_release("0.1.0")); // Backslash → doubled - EXPECT_NE(out.find("a\\\\b"), std::string::npos); + expect(ne(out.find("a\\\\b"), std::string::npos)); // \r → \r escape - EXPECT_NE(out.find("\\rc"), std::string::npos); + expect(ne(out.find("\\rc"), std::string::npos)); // NUL → \0 - EXPECT_NE(out.find("\\0"), std::string::npos); + expect(ne(out.find("\\0"), std::string::npos)); // 0x07 (BEL) → \x07 - EXPECT_NE(out.find("\\x07"), std::string::npos); + expect(ne(out.find("\\x07"), std::string::npos)); // 0x7f (DEL) → \x7f - EXPECT_NE(out.find("\\x7f"), std::string::npos); + expect(ne(out.find("\\x7f"), std::string::npos)); // No literal control bytes leak through - EXPECT_EQ(out.find('\r'), std::string::npos); - EXPECT_EQ(out.find('\0'), std::string::npos); - EXPECT_EQ(out.find('\x07'), std::string::npos); -} + expect(eq(out.find('\r'), std::string::npos)); + expect(eq(out.find('\0'), std::string::npos)); + expect(eq(out.find('\x07'), std::string::npos)); +}}; -TEST(XpkgEmit, ReleaseTarballUrl) { +UTest xpkg_emit_release_tarball_url{"XpkgEmit/ReleaseTarballUrl", [] { using namespace mcpp::publish; - EXPECT_EQ(release_tarball_url("https://github.com/foo/bar", "bar", "0.1.0"), - "https://github.com/foo/bar/releases/download/v0.1.0/bar-0.1.0.tar.gz"); + expect(eq(release_tarball_url("https://github.com/foo/bar", "bar", "0.1.0"), "https://github.com/foo/bar/releases/download/v0.1.0/bar-0.1.0.tar.gz")); // .git suffix gets stripped - EXPECT_EQ(release_tarball_url("https://github.com/foo/bar.git", "bar", "0.1.0"), - "https://github.com/foo/bar/releases/download/v0.1.0/bar-0.1.0.tar.gz"); + expect(eq(release_tarball_url("https://github.com/foo/bar.git", "bar", "0.1.0"), "https://github.com/foo/bar/releases/download/v0.1.0/bar-0.1.0.tar.gz")); // empty repo → empty URL (caller errors out) - EXPECT_EQ(release_tarball_url("", "bar", "0.1.0"), ""); + expect(eq(release_tarball_url("", "bar", "0.1.0"), "")); // non-http(s) repo URL → empty (don't trust ssh:// / git@ for downloads) - EXPECT_EQ(release_tarball_url("git@github.com:foo/bar.git", "bar", "0.1.0"), ""); -} + expect(eq(release_tarball_url("git@github.com:foo/bar.git", "bar", "0.1.0"), "")); +}}; #if !defined(_WIN32) // sha256_of_file uses sha256sum which is not available on Windows -TEST(XpkgEmit, Sha256OfFile) { +UTest xpkg_emit_sha256_of_file{"XpkgEmit/Sha256OfFile", [] { using namespace mcpp::publish; // Empty file → known SHA-256 auto p = std::filesystem::temp_directory_path() / "mcpp_unit_sha_empty"; { std::ofstream(p).close(); } - EXPECT_EQ(sha256_of_file(p), - "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); + expect(eq(sha256_of_file(p), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")); std::filesystem::remove(p); // Known content auto p2 = std::filesystem::temp_directory_path() / "mcpp_unit_sha_abc"; { std::ofstream os(p2); os << "abc"; } - EXPECT_EQ(sha256_of_file(p2), - "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"); + expect(eq(sha256_of_file(p2), "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad")); std::filesystem::remove(p2); // Non-existent file → empty string - EXPECT_EQ(sha256_of_file("/no/such/path/zzz"), ""); -} + expect(eq(sha256_of_file("/no/such/path/zzz"), "")); +}}; #endif // !defined(_WIN32) #if defined(__linux__) -TEST(XpkgEmit, Sha256OfFileIgnoresTargetRuntimeLibraryPath) { +UTest sha256_of_file_ignores_target_runtime_library_path{"XpkgEmit/Sha256OfFileIgnoresTargetRuntimeLibraryPath", [] { using namespace mcpp::publish; auto hostile = std::filesystem::temp_directory_path() @@ -137,17 +135,16 @@ TEST(XpkgEmit, Sha256OfFileIgnoresTargetRuntimeLibraryPath) { { mcpp::invocation::ScopedEnv ld("LD_LIBRARY_PATH", hostile.string()); - EXPECT_EQ(sha256_of_file(p), - "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); + expect(eq(sha256_of_file(p), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")); } std::error_code ec; std::filesystem::remove(p, ec); std::filesystem::remove_all(hostile, ec); -} +}}; #endif // defined(__linux__) -TEST(XpkgEmit, LongBracketSequenceInValueIsHarmless) { +UTest long_bracket_sequence_in_value_is_harmless{"XpkgEmit/LongBracketSequenceInValueIsHarmless", [] { // We emit `"..."` strings, not `[[...]]`, so a literal `]=]` in // user data can't terminate the literal early. Just make sure it // round-trips as escaped content (the `]`/`=` chars are printable @@ -157,8 +154,8 @@ TEST(XpkgEmit, LongBracketSequenceInValueIsHarmless) { auto g = minimal_graph(); m.package.description = "trick: ]==] more stuff"; auto out = emit_xpkg(m, g, placeholder_release("0.1.0")); - EXPECT_NE(out.find("\"trick: ]==] more stuff\""), std::string::npos); -} + expect(ne(out.find("\"trick: ]==] more stuff\""), std::string::npos)); +}}; // ── #630 item 7: an OS-only selector is a platform ────────────────────────── // @@ -184,7 +181,7 @@ std::string block(const std::string& out, std::string_view header) { } // namespace -TEST(XpkgEmit, OsOnlySelectorNamesItsPlatformBlockAndSuppressesTheWarning) { +UTest os_only_selector_names_its_platform_block_and_suppresses_the_warning{"XpkgEmit/OsOnlySelectorNamesItsPlatformBlockAndSuppressesTheWarning", [] { constexpr const char* src = R"( [package] name = "gtkapp" @@ -194,24 +191,24 @@ version = "0.1.0" "xim:gtk4" = "" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << (m ? std::string{} : m.error().format()); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << (m ? std::string{} : m.error().format()); auto g = minimal_graph(); mcpp::diag::reset(); auto out = emit_xpkg(*m, g, placeholder_release("0.1.0")); - EXPECT_NE(block(out, "linux = {").find("xim:gtk4"), std::string::npos) + expect(ne(block(out, "linux = {").find("xim:gtk4"), std::string::npos)) << out; - EXPECT_EQ(block(out, "macosx = {").find("xim:gtk4"), std::string::npos); - EXPECT_EQ(block(out, "windows = {").find("xim:gtk4"), std::string::npos); + expect(eq(block(out, "macosx = {").find("xim:gtk4"), std::string::npos)); + expect(eq(block(out, "windows = {").find("xim:gtk4"), std::string::npos)); // No `publish/target-axis-tools` advisory for THIS section: an OS-only // selector is emitted, so there is nothing left to warn about. - EXPECT_EQ(mcpp::diag::count(mcpp::diag::Severity::Warning), 0u); - EXPECT_TRUE(mcpp::diag::records().empty()); -} + expect(eq(mcpp::diag::count(mcpp::diag::Severity::Warning), 0u)); + expect(mcpp::diag::records().empty()); +}}; -TEST(XpkgEmit, NonOsSelectorNamesNoBlockAndStillWarns) { +UTest non_os_selector_names_no_block_and_still_warns{"XpkgEmit/NonOsSelectorNamesNoBlockAndStillWarns", [] { constexpr const char* src = R"( [package] name = "gtkapp" @@ -221,23 +218,23 @@ version = "0.1.0" "xim:gtk4" = "" )"; auto m = mcpp::manifest::parse_string(src); - ASSERT_TRUE(m.has_value()) << (m ? std::string{} : m.error().format()); + if (auto&& failed = expect(fatal(m.has_value())); !failed) failed << (m ? std::string{} : m.error().format()); auto g = minimal_graph(); mcpp::diag::reset(); auto out = emit_xpkg(*m, g, placeholder_release("0.1.0")); - EXPECT_EQ(block(out, "linux = {").find("xim:gtk4"), std::string::npos); - EXPECT_EQ(block(out, "macosx = {").find("xim:gtk4"), std::string::npos); - EXPECT_EQ(block(out, "windows = {").find("xim:gtk4"), std::string::npos); + expect(eq(block(out, "linux = {").find("xim:gtk4"), std::string::npos)); + expect(eq(block(out, "macosx = {").find("xim:gtk4"), std::string::npos)); + expect(eq(block(out, "windows = {").find("xim:gtk4"), std::string::npos)); // The residual case: the advisory still fires, and its text now also // states that an OS-only selector WOULD have been emitted. - ASSERT_EQ(mcpp::diag::count(mcpp::diag::Severity::Warning), 1u); + expect(fatal(eq(mcpp::diag::count(mcpp::diag::Severity::Warning), 1u))); auto recs = mcpp::diag::records(); - ASSERT_EQ(recs.size(), 1u); - EXPECT_EQ(recs.front().domain, "publish/target-axis-tools"); - EXPECT_NE(recs.front().what.find("xim:gtk4"), std::string::npos); - EXPECT_NE(recs.front().what.find("OS-only selector"), std::string::npos) + expect(fatal(eq(recs.size(), 1u))); + expect(eq(recs.front().domain, "publish/target-axis-tools")); + expect(ne(recs.front().what.find("xim:gtk4"), std::string::npos)); + expect(ne(recs.front().what.find("OS-only selector"), std::string::npos)) << recs.front().what; -} +}}; diff --git a/tests/unit/test_xpkg_host_tools.cpp b/tests/unit/test_xpkg_host_tools.cpp index 8524b69a4..774be9294 100644 --- a/tests/unit/test_xpkg_host_tools.cpp +++ b/tests/unit/test_xpkg_host_tools.cpp @@ -1,10 +1,12 @@ -#include import std; +import mcpplibs.mtest; import mcpp.manifest; import mcpp.manifest.xpkg; import mcpp.toolchain.platform; +using namespace mcpplibs::mtest; + // #355 needed two things from the Form B (xpkg .lua) descriptor parser that it // simply did not read, and neither failed loudly: // @@ -24,7 +26,7 @@ namespace { mcpp::manifest::Manifest parse_or_fail(std::string_view lua) { auto m = mcpp::manifest::synthesize_from_xpkg_lua( lua, "compat.demo", "1.0.0", mcpp::toolchain::platform::HostPlatform::current()); - EXPECT_TRUE(m.has_value()) << (m ? "" : m.error().message); + if (auto&& failed = expect(m.has_value()); !failed) failed << (m ? "" : m.error().message); return m.value_or(mcpp::manifest::Manifest{}); } @@ -37,7 +39,7 @@ const mcpp::manifest::Target* find_target(const mcpp::manifest::Manifest& m, } // namespace -TEST(XpkgHostTools, TargetsCarryRequiredFeatures) { +UTest targets_carry_required_features{"XpkgHostTools/TargetsCarryRequiredFeatures", [] { auto m = parse_or_fail(R"LUA( package = { spec = "1", name = "demo", namespace = "compat", type = "package", @@ -53,19 +55,18 @@ package = { )LUA"); auto const* lib = find_target(m, "demo"); - ASSERT_NE(lib, nullptr); - EXPECT_EQ(lib->kind, mcpp::manifest::Target::Library); - EXPECT_TRUE(lib->requiredFeatures.empty()); + expect(fatal(ne(lib, nullptr))); + expect(eq(lib->kind, mcpp::manifest::Target::Library)); + expect(lib->requiredFeatures.empty()); auto const* tool = find_target(m, "protoc"); - ASSERT_NE(tool, nullptr); - EXPECT_EQ(tool->kind, mcpp::manifest::Target::Binary); - EXPECT_EQ(tool->main, "src/compiler/main.cc"); - EXPECT_EQ(tool->requiredFeatures, - (std::vector{"protoc", "upb"})); -} + expect(fatal(ne(tool, nullptr))); + expect(eq(tool->kind, mcpp::manifest::Target::Binary)); + expect(eq(tool->main, "src/compiler/main.cc")); + expect(eq(tool->requiredFeatures, (std::vector{"protoc", "upb"}))); +}}; -TEST(XpkgHostTools, DepsAcceptBothAStringAndATable) { +UTest deps_accept_both_a_string_and_a_table{"XpkgHostTools/DepsAcceptBothAStringAndATable", [] { // The string form is the long-standing one and must keep working // unchanged; the table form is what lets a descriptor request a tool. auto m = parse_or_fail(R"LUA( @@ -88,16 +89,16 @@ package = { if (k.find("protobuf") != std::string::npos) pb = &spec; } - ASSERT_NE(zlib, nullptr); - EXPECT_EQ(zlib->version, "1.3.2"); - EXPECT_TRUE(zlib->tools.empty()); + expect(fatal(ne(zlib, nullptr))); + expect(eq(zlib->version, "1.3.2")); + expect(zlib->tools.empty()); - ASSERT_NE(pb, nullptr); - EXPECT_EQ(pb->version, "35.1"); - EXPECT_EQ(pb->tools, (std::vector{"protoc"})); -} + expect(fatal(ne(pb, nullptr))); + expect(eq(pb->version, "35.1")); + expect(eq(pb->tools, (std::vector{"protoc"}))); +}}; -TEST(XpkgHostTools, UnknownDepKeyIsRecordedRatherThanSwallowed) { +UTest unknown_dep_key_is_recorded_rather_than_swallowed{"XpkgHostTools/UnknownDepKeyIsRecordedRatherThanSwallowed", [] { // A descriptor author writing an unsupported key must be told. Silently // ignoring it leaves the dependency half-configured with no signal — the // exact failure the per-feature key recording already exists to prevent. @@ -116,17 +117,17 @@ package = { bool recorded = false; for (auto const& k : m.xpkgUnknownKeys) if (k.find("no_such_key") != std::string::npos) recorded = true; - EXPECT_TRUE(recorded) << "unknown dep key was swallowed"; + expect(bool(recorded)) << "unknown dep key was swallowed"; // ...and the keys it DOES understand still take effect. for (auto const& [k, spec] : m.dependencies) if (k.find("protobuf") != std::string::npos) - EXPECT_EQ(spec.version, "35.1"); -} + expect(eq(spec.version, "35.1")); +}}; // #642 E1: a descriptor's target states its default link form exactly as // `mcpp.toml` does, and the same statements are refused. -TEST(XpkgHostTools, TargetsCarryTheirDefaultLinkForm) { +UTest targets_carry_their_default_link_form{"XpkgHostTools/TargetsCarryTheirDefaultLinkForm", [] { auto m = parse_or_fail(R"LUA( package = { spec = "1", name = "demo", namespace = "compat", type = "package", @@ -139,9 +140,9 @@ package = { } )LUA"); auto const* lib = find_target(m, "demo"); - ASSERT_NE(lib, nullptr); - EXPECT_EQ(lib->kind, mcpp::manifest::Target::Library); - EXPECT_EQ(lib->linkageDefault, "shared"); + expect(fatal(ne(lib, nullptr))); + expect(eq(lib->kind, mcpp::manifest::Target::Library)); + expect(eq(lib->linkageDefault, "shared")); auto refused = mcpp::manifest::synthesize_from_xpkg_lua(R"LUA( package = { @@ -154,7 +155,6 @@ package = { }, } )LUA", "compat.demo", "1.0.0", mcpp::toolchain::platform::HostPlatform::current()); - ASSERT_FALSE(refused.has_value()); - EXPECT_NE(refused.error().message.find("states both `kind = \"shared\"`"), - std::string::npos) << refused.error().message; -} + expect(fatal(!refused.has_value())); + if (auto&& failed = expect(ne(refused.error().message.find("states both `kind = \"shared\"`"), std::string::npos)); !failed) failed << refused.error().message; +}}; diff --git a/tests/unit/test_xpkg_versions_merge.cpp b/tests/unit/test_xpkg_versions_merge.cpp index df62e111d..fc9d6372a 100644 --- a/tests/unit/test_xpkg_versions_merge.cpp +++ b/tests/unit/test_xpkg_versions_merge.cpp @@ -3,69 +3,71 @@ // display keeps the ORIGINAL key byte-for-byte (#363's lesson: an arbitrary // index key cannot be reproduced from its parsed form). -#include import std; +import mcpplibs.mtest; import mcpp.manifest; +using namespace mcpplibs::mtest; + namespace { -TEST(XpkgVersionsMerge, OrdersSemverDescendingAcrossPlatforms) { +UTest orders_semver_descending_across_platforms{"XpkgVersionsMerge/OrdersSemverDescendingAcrossPlatforms", [] { std::vector> perPlatform = { {"1.0.0"}, {"0.9.0", "1.2.0"}, }; auto merged = mcpp::manifest::merge_xpkg_versions_desc(perPlatform); - ASSERT_EQ(merged.size(), 3u); - EXPECT_EQ(merged[0], "1.2.0"); - EXPECT_EQ(merged[1], "1.0.0"); - EXPECT_EQ(merged[2], "0.9.0"); -} + expect(fatal(eq(merged.size(), 3u))); + expect(eq(merged[0], "1.2.0")); + expect(eq(merged[1], "1.0.0")); + expect(eq(merged[2], "0.9.0")); +}}; -TEST(XpkgVersionsMerge, DeduplicatesKeysSharedAcrossPlatforms) { +UTest deduplicates_keys_shared_across_platforms{"XpkgVersionsMerge/DeduplicatesKeysSharedAcrossPlatforms", [] { std::vector> perPlatform = { {"1.0.0", "0.9.0"}, {"1.0.0", "0.8.0"}, {"1.0.0"}, }; auto merged = mcpp::manifest::merge_xpkg_versions_desc(perPlatform); - ASSERT_EQ(merged.size(), 3u); - EXPECT_EQ(merged[0], "1.0.0"); - EXPECT_EQ(merged[1], "0.9.0"); - EXPECT_EQ(merged[2], "0.8.0"); -} + expect(fatal(eq(merged.size(), 3u))); + expect(eq(merged[0], "1.0.0")); + expect(eq(merged[1], "0.9.0")); + expect(eq(merged[2], "0.8.0")); +}}; -TEST(XpkgVersionsMerge, ReleaseOutranksPrereleaseOfSameNumbers) { +UTest release_outranks_prerelease_of_same_numbers{"XpkgVersionsMerge/ReleaseOutranksPrereleaseOfSameNumbers", [] { std::vector> perPlatform = { {"1.0.0-rc.1", "1.0.0"}, }; auto merged = mcpp::manifest::merge_xpkg_versions_desc(perPlatform); - ASSERT_EQ(merged.size(), 2u); - EXPECT_EQ(merged[0], "1.0.0"); - EXPECT_EQ(merged[1], "1.0.0-rc.1"); -} + expect(fatal(eq(merged.size(), 2u))); + expect(eq(merged[0], "1.0.0")); + expect(eq(merged[1], "1.0.0-rc.1")); +}}; -TEST(XpkgVersionsMerge, PrereleaseNumericIdentifiersCompareNumerically) { +UTest prerelease_numeric_identifiers_compare_numerically{"XpkgVersionsMerge/PrereleaseNumericIdentifiersCompareNumerically", [] { std::vector> perPlatform = { {"1.0.0-rc.2", "1.0.0-rc.10"}, }; auto merged = mcpp::manifest::merge_xpkg_versions_desc(perPlatform); - ASSERT_EQ(merged.size(), 2u); - EXPECT_EQ(merged[0], "1.0.0-rc.10"); - EXPECT_EQ(merged[1], "1.0.0-rc.2"); -} + expect(fatal(eq(merged.size(), 2u))); + expect(eq(merged[0], "1.0.0-rc.10")); + expect(eq(merged[1], "1.0.0-rc.2")); +}}; -TEST(XpkgVersionsMerge, FourthSegmentComparesNumerically) { +UTest fourth_segment_compares_numerically{"XpkgVersionsMerge/FourthSegmentComparesNumerically", [] { std::vector> perPlatform = { {"1.2.3.10", "1.2.3.9"}, }; auto merged = mcpp::manifest::merge_xpkg_versions_desc(perPlatform); - ASSERT_EQ(merged.size(), 2u); - EXPECT_EQ(merged[0], "1.2.3.10"); - EXPECT_EQ(merged[1], "1.2.3.9"); -} + expect(fatal(eq(merged.size(), 2u))); + expect(eq(merged[0], "1.2.3.10")); + expect(eq(merged[1], "1.2.3.9")); +}}; -TEST(XpkgVersionsMerge, KeepsOriginalKeyForSuffixedBuilds) { +UTest keeps_original_key_for_suffixed_builds{"XpkgVersionsMerge/KeepsOriginalKeyForSuffixedBuilds", [] { // compat.glad publishes keys like "0.0.0-651a425": the suffix rides along // as pre-release identifiers for ordering, but what reaches the screen is // the key as written. @@ -73,47 +75,44 @@ TEST(XpkgVersionsMerge, KeepsOriginalKeyForSuffixedBuilds) { {"0.0.0-651a425", "0.0.1"}, }; auto merged = mcpp::manifest::merge_xpkg_versions_desc(perPlatform); - ASSERT_EQ(merged.size(), 2u); - EXPECT_EQ(merged[0], "0.0.1"); - EXPECT_EQ(merged[1], "0.0.0-651a425"); -} + expect(fatal(eq(merged.size(), 2u))); + expect(eq(merged[0], "0.0.1")); + expect(eq(merged[1], "0.0.0-651a425")); +}}; -TEST(XpkgVersionsMerge, UnparsableKeysSortLastByNameAndKeepTheirText) { +UTest unparsable_keys_sort_last_by_name_and_keep_their_text{"XpkgVersionsMerge/UnparsableKeysSortLastByNameAndKeepTheirText", [] { std::vector> perPlatform = { {"unknown", "1.0.0", "beta-thing"}, }; auto merged = mcpp::manifest::merge_xpkg_versions_desc(perPlatform); - ASSERT_EQ(merged.size(), 3u); - EXPECT_EQ(merged[0], "1.0.0"); - EXPECT_EQ(merged[1], "beta-thing"); - EXPECT_EQ(merged[2], "unknown"); -} - -TEST(XpkgVersionsMerge, EmptyInputsYieldEmptyOutput) { - EXPECT_TRUE(mcpp::manifest::merge_xpkg_versions_desc({}).empty()); - EXPECT_TRUE( - mcpp::manifest::merge_xpkg_versions_desc({{}, {}}).empty()); -} + expect(fatal(eq(merged.size(), 3u))); + expect(eq(merged[0], "1.0.0")); + expect(eq(merged[1], "beta-thing")); + expect(eq(merged[2], "unknown")); +}}; + +UTest empty_inputs_yield_empty_output{"XpkgVersionsMerge/EmptyInputsYieldEmptyOutput", [] { + expect(mcpp::manifest::merge_xpkg_versions_desc({}).empty()); + expect(mcpp::manifest::merge_xpkg_versions_desc({{}, {}}).empty()); +}}; // A truncated line must say it is truncated. Without the marker the default // three-version view reads as the complete set, which is the one wrong // conclusion the display can produce -- and both `mcpp search` and the // `mcpp add` suggestion render through this function, so one assertion // covers the contract for both. -TEST(JoinVersionsDesc, TruncationIsMarkedAndAnExactFitIsNot) { +UTest truncation_is_marked_and_an_exact_fit_is_not{"JoinVersionsDesc/TruncationIsMarkedAndAnExactFitIsNot", [] { const std::vector six = {"0.0.6", "0.0.5", "0.0.4", "0.0.3", "0.0.2", "0.0.1"}; - EXPECT_EQ(mcpp::manifest::join_versions_desc(six), - "0.0.6, 0.0.5, 0.0.4, ..."); - EXPECT_EQ(mcpp::manifest::join_versions_desc(six, six.size()), - "0.0.6, 0.0.5, 0.0.4, 0.0.3, 0.0.2, 0.0.1"); + expect(eq(mcpp::manifest::join_versions_desc(six), "0.0.6, 0.0.5, 0.0.4, ...")); + expect(eq(mcpp::manifest::join_versions_desc(six, six.size()), "0.0.6, 0.0.5, 0.0.4, 0.0.3, 0.0.2, 0.0.1")); // Exactly `keep` entries is not a truncation. const std::vector three = {"2.0.0", "1.0.0", "0.9.0"}; - EXPECT_EQ(mcpp::manifest::join_versions_desc(three), "2.0.0, 1.0.0, 0.9.0"); + expect(eq(mcpp::manifest::join_versions_desc(three), "2.0.0, 1.0.0, 0.9.0")); - EXPECT_EQ(mcpp::manifest::join_versions_desc({}), ""); - EXPECT_EQ(mcpp::manifest::join_versions_desc({"1.0.0"}), "1.0.0"); -} + expect(eq(mcpp::manifest::join_versions_desc({}), "")); + expect(eq(mcpp::manifest::join_versions_desc({"1.0.0"}), "1.0.0")); +}}; } // namespace