diff --git a/.agents/docs/2026-07-26-add-catch2-plan.md b/.agents/docs/2026-07-26-add-catch2-plan.md new file mode 100644 index 0000000..f085937 --- /dev/null +++ b/.agents/docs/2026-07-26-add-catch2-plan.md @@ -0,0 +1,177 @@ +# Design doc: add Catch2 to mcpp-index + +Date: 2026-07-26 (revised 2026-07-27 — single package, see §"Revision") + +## Source + +- Repo: [catchorg/Catch2](https://github.com/catchorg/Catch2) +- License: BSL-1.0 (Boost Software License 1.0) + +Catch2 has two major versions with incompatible APIs and source layouts: + +| Version | Source layout | Resulting shape | +|---------|--------------|-----------------| +| 2.13.10 | `single_include/catch2/catch.hpp` (single amalgamated header) | **header-only** | +| 3.15.2 | `src/catch2/**/*.{hpp,cpp}` (individual source files) | **static library** | + +## Revision: one package, not two + +The first draft of this design shipped **two** packages, `compat.catch2` (v3) +and `compat.catch2-v2` (v2), on the reasoning that "the v2→v3 jump changed the +repo layout completely, so the two versions cannot share a single `mcpp` +block." + +The premise is right; the conclusion was not. The two layouts' globs are +**disjoint**, so a single `mcpp` block can carry the **union** of both and let +each version light up exactly one half: + +| glob | 2.13.10 | 3.15.2 | +|---|---|---| +| `*/single_include` | exists | absent | +| `*/src/catch2/**/*.cpp` | **0 matches** — v2's only `src/` file is `src/catch_with_main.cpp`, *not* under `src/catch2/` | 107 | +| `catch2/catch_all.hpp` | absent | exists | + +An `include_dirs` entry whose glob matches nothing is not an error, and a +`sources` glob that matches nothing simply contributes no TUs — which is what +makes the union safe. At 2.13.10 the package degenerates to header-only +(`single_include` + an anchor TU); at 3.15.2 it is a 106-TU static library. + +The last row doubles as a compile-time major discriminator: +`__has_include()`. + +### Why one package is the better shape regardless + +- **A version does not belong in `name`.** `catch2-v2` smuggles a major into + the atomic name segment that SPEC-001 identity reserves for the name alone — + the same anti-pattern the `compat.openssl` → `openssl` rename removed. +- **Semver stops working across the split.** With two packages a consumer + cannot write `catch2 = "^3"`, and version resolution cannot see v2 and v3 as + the same library. +- **Discovery.** One library should not answer to two package names. +- **It is a one-way door.** Once `compat.catch2-v2` ships in a published index + artifact, withdrawing it is a breaking change. The decision had to be made + before merge, not after. + +### Relationship to mcpp#290 + +[mcpp-community/mcpp#290](https://github.com/mcpp-community/mcpp/issues/290) +asks for per-version `mcpp` build blocks. It is **not** a prerequisite here — +the merge works on mcpp 0.0.109 today. + +Nor is Catch2 the case #290 argues from. That issue's motivating example +(llama.cpp b10069 vs b10107) shares ~95% of its build rules and differs in a +few source files. Catch2 v2/v3 share essentially *nothing* but boilerplate +(`language`, `import_std`, `c_standard`, `targets`, `deps`); they merge only +because their globs happen not to overlap. + +That "happen not to" is the weak point, and it is exactly what #290 would fix: +the disjointness is a property of two upstream trees, not something this +descriptor can enforce. When #290 lands, this collapses into explicit +`["2.x"]` / `["3.x"]` blocks and the implicit assumption disappears. Until +then the premise is documented in the descriptor header and must be re-checked +whenever a version is added. + +## Package shape + +- **Sources**: anchor TU + `*/src/catch2/**/*.cpp` minus + `!*/src/catch2/internal/catch_main.cpp` (107 files, 106 compiled). +- **include_dirs**: `{ "*/single_include", "*/src", "mcpp_generated" }`. +- **Anchor** (`mcpp_generated/catch2_anchor.c`): required at v2, where the + sources glob matches nothing and a lib target still needs a TU. Same shape + as compat.eigen / compat.khrplatform. Inert at v3. +- **catch_user_config.hpp**: materialised via `generated_files` (upstream ships + only `catch_user_config.hpp.in` and lets CMake fill it). Only the two VALUE + defines are mandatory — `CATCH_CONFIG_DEFAULT_REPORTER` and + `CATCH_CONFIG_CONSOLE_WIDTH`; every other entry in the `.in` is a + `#cmakedefine`, i.e. absent means "use the compiler-detected default". Both + are `#ifndef`-guarded so a consumer can override via `-D`. v2 never includes + this file. **Re-read the upstream `.in` when bumping v3** — a newly added + mandatory value-define would break silently here. + +## The `main` feature + +`features = ["main"]` compiles a **generated** TU that supplies a default entry +point, branching on `__has_include()` to pick the v3 +(`Catch::Session`) or v2 (`CATCH_CONFIG_MAIN`) spelling. + +It deliberately does **not** point at upstream's +`src/catch2/internal/catch_main.cpp`. That was the first draft's approach and +it does not work. Measured on mcpp 0.0.109: + +| descriptor form | default path | `features = ["main"]` | +|---|---|---| +| glob + `!` negation, feature → the negated path | ok | **FAIL** `undefined reference to 'main'` | +| glob, no negation, feature → the globbed path | **FAIL** `multiple definition of 'main'` | ok | +| glob + `!` negation + explicit re-add, feature → that path | ok | **FAIL** `undefined reference to 'main'` | +| glob + `!` negation, feature → **generated TU** | ok | ok | + +Two mechanics behind that table: + +1. A `!` negation in `sources` is an **absolute** exclusion applied to the + final source set. A feature cannot add the path back — not even if the path + is also listed explicitly afterwards. +2. Feature gating matches **literal** `sources` entries. A file pulled in by a + glob is not gated at all, so it lands in the default build. + +Together these mean the `compat.gtest` pattern does **not** transfer: +`gtest_main.cc` is a literal `sources` entry, so listing it under a feature +gates it. Under a glob the same idea silently breaks in one direction or the +other. A generated TU sidesteps the conflict. + +Cost of not reusing upstream's file: no `LeakDetector` registration (a Windows +CRT-debug nicety) and no `wmain` variant (reachable only under `_UNICODE` on +Windows). Consumers needing either write their own `main()`. + +## Why this needs four workspace members + +Features are resolved **per consuming project**, so one project cannot hold +`catch2` both with and without `main` (same constraint that forces `asio-ssl` +to be separate from `asio-module`). Two majors × {default, `main` feature} = 4: + +| member | version | requests | what it would catch | +|---|---|---|---| +| `catch2` | 3.15.2 | — (own `main()`) | negation stops working → `multiple definition of 'main'` | +| `catch2-main` | 3.15.2 | `features = ["main"]` | feature stops gating the TU in → `undefined reference to 'main'` | +| `catch2-v2` | 2.13.10 | — (own `CATCH_CONFIG_MAIN`) | union bleeds across versions; `single_include` unresolved | +| `catch2-v2-main` | 2.13.10 | `features = ["main"]` | the `__has_include` ELSE branch (v3 members only take THEN) | + +The first draft declared a `main` feature on both packages and covered +**neither** — which is precisely why the broken feature passed CI. Every test +body keeps a real `REQUIRE`, and Catch2 prints its assertion count on exit, so +a TU that compiles to nothing cannot pass quietly. + +## Download URLs and mirrors + +GitHub tag archive tarballs (not the individual amalgamated release assets): +xpm takes one URL per version, and the tag archive is the only form that +carries both layouts. Neither archive contains symlinks, so the Windows +extraction path is safe. + +| Version | GLOBAL | CN | SHA256 | +|---|---|---|---| +| 2.13.10 | `github.com/catchorg/Catch2/archive/refs/tags/v2.13.10.tar.gz` | `gitcode.com/mcpp-res/catch2/releases/download/2.13.10/catch2-2.13.10.tar.gz` | `d54a712b…9943` | +| 3.15.2 | `github.com/catchorg/Catch2/archive/refs/tags/v3.15.2.tar.gz` | `gitcode.com/mcpp-res/catch2/releases/download/3.15.2/catch2-3.15.2.tar.gz` | `acfae120…0420` | + +CN assets were uploaded to the `mcpp-res` gitcode org and re-downloaded: both +are byte-identical to the upstream archives (same sha256, same size), so the +single recorded `sha256` covers either source. + +## Verification + +Local, mcpp 0.0.109 (matching CI `MCPP_VERSION`), gcc@16.1.0, linux-x86_64: + +- `mcpp xpkg parse` on the descriptor: OK +- `tests/check_mirror_urls.lua`: OK +- All four members via `mcpp test -p`: ok +- **Negative controls** (not committed): dropping `features = ["main"]` from + each `-main` member fails with `undefined reference to 'main'`, confirming + the feature is what supplies the entry point rather than something else. +- Both tarballs re-downloaded from GLOBAL and CN and sha256-checked. + +CI covers linux / macOS / windows. + +## Notes + +- Catch2 v3 requires C++14, v2 requires C++11; both build under + `language = "c++23"`. +- `c_standard = "c11"` is for the anchor TU. diff --git a/README.md b/README.md index 9344d42..cb7d7e1 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ mcpp self config --mirror CN # 切换至国内镜像,默认使用 GLOBAL 上 | 原生模块库(Form A) | [`mcpplibs.xpkg`](pkgs/x/xpkg.lua) · [`mcpplibs.tinyhttps`](pkgs/t/tinyhttps.lua) · [`tensorvia-cpu`](pkgs/t/tensorvia-cpu.lua) · [`ffmpeg`](pkgs/f/ffmpeg.lua)(模块层,源码经 `compat.ffmpeg` 直编) · [`opencv`](pkgs/o/opencv.lua)(单仓库:模块层与 OpenCV 5 全源码构建同在包内,索引侧只留本描述符) | | C 源码 compat(含 `features`) | [`compat.cjson`](pkgs/c/compat.cjson.lua) · [`compat.zlib`](pkgs/c/compat.zlib.lua) | | header-only(含 `features`) | [`compat.eigen`](pkgs/c/compat.eigen.lua) | +| 单包多 major(形态随版本切换) | [`compat.catch2`](pkgs/c/compat.catch2.lua)(3.x 编 `src/catch2/` 出静态库;2.x 走 `single_include/` header-only) | | 外部构建系统(`install()` 从源码构建) | [`compat.openblas`](pkgs/c/compat.openblas.lua)(Make) · [`compat.openssl`](pkgs/c/compat.openssl.lua)(Perl Configure + Make,静态 libssl/libcrypto) | | 全源码直编(config 快照 + 源列表,零外部构建系统) | [`compat.ffmpeg`](pkgs/c/compat.ffmpeg.lua)(2281 TU 含 NASM 汇编,28 个目录 glob 声明) | | C++23 module wrapper | [`nlohmann.json`](pkgs/n/nlohmann.json.lua) · [`marzer.tomlplusplus`](pkgs/m/marzer.tomlplusplus.lua) | diff --git a/mcpp.toml b/mcpp.toml index 90fd673..d5a3ad0 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -11,6 +11,10 @@ members = [ "tests/examples/asio-module", "tests/examples/asio-ssl", "tests/examples/build-mcpp", + "tests/examples/catch2", + "tests/examples/catch2-main", + "tests/examples/catch2-v2", + "tests/examples/catch2-v2-main", "tests/examples/cjson", "tests/examples/core", "tests/examples/eigen", diff --git a/pkgs/c/compat.catch2.lua b/pkgs/c/compat.catch2.lua new file mode 100644 index 0000000..d17ff65 --- /dev/null +++ b/pkgs/c/compat.catch2.lua @@ -0,0 +1,180 @@ +-- Form B inline descriptor for Catch2 — a modern C++-native test framework. +-- +-- ONE package, TWO majors. v2 and v3 have completely different source layouts +-- (`single_include/catch2/catch.hpp` vs `src/catch2/**`), and this descriptor +-- covers both by taking the UNION of the two layouts' globs. That works only +-- because the two sets are DISJOINT: +-- +-- | glob | 2.13.10 | 3.15.2 | +-- |-------------------------|---------------------------------|--------| +-- | */single_include | exists | absent | +-- | */src/catch2/**/*.cpp | 0 matches — v2's only src/ file | 107 | +-- | | is src/catch_with_main.cpp, | | +-- | | NOT under src/catch2/ | | +-- +-- So each version lights up exactly one half and the other degenerates to +-- empty: v2 resolves to header-only (single_include + the anchor TU below), +-- v3 to a 106-TU static library. An include_dir whose glob matches nothing is +-- not an error, which is what makes the union safe. +-- +-- THIS DISJOINTNESS IS THE LOAD-BEARING PREMISE, and it is a property of the +-- two upstream trees rather than something this descriptor can enforce. Re- +-- check the table above when adding a version. The right long-term shape is +-- per-version build blocks — mcpp-community/mcpp#290 — after which this +-- collapses into explicit ["2.x"] / ["3.x"] tables and the assumption goes +-- away entirely. Splitting into two packages was rejected: it encodes a +-- version into the `name` segment, which SPEC-001 identity reserves for an +-- atomic name, and it breaks semver across the major (a consumer could not +-- write `catch2 = "^3"`). +-- +-- Features (sources-only gate): +-- `main` — compiles a GENERATED TU supplying a default entry point. It +-- branches on __has_include(), which exists only in +-- v3, to pick the v3 (Catch::Session) or v2 (CATCH_CONFIG_MAIN) spelling. +-- Excluded by default; request `features = ["main"]`. +-- +-- It deliberately does NOT point at upstream's +-- src/catch2/internal/catch_main.cpp. That file is matched by the sources +-- glob and must be `!`-negated, else a consumer with its own main() gets +-- `multiple definition of 'main'`; and a `!` negation is an ABSOLUTE +-- exclusion that a feature cannot add back — pointing the feature at the +-- negated path yields a feature that silently does nothing and an +-- `undefined reference to 'main'` at link time. Both directions were +-- measured on mcpp 0.0.109. Note this is NOT `compat.gtest`'s shape: +-- gtest lists gtest_main.cc as a LITERAL `sources` entry, and feature +-- gating matches literal entries — a file pulled in by a glob is not gated +-- at all. A generated TU sidesteps the whole conflict. +-- +-- Cost of not reusing upstream's file: no LeakDetector registration (a +-- Windows CRT-debug nicety) and no wmain variant (reachable only under +-- _UNICODE on Windows). Consumers needing either write their own main(). +-- +-- All `mcpp` paths are GLOBS relative to the verdir; the leading `*` absorbs +-- the GitHub tarball's `Catch2-/` wrap layer. +package = { + spec = "1", + namespace = "compat", + name = "catch2", + description = "A modern, C++-native test framework for unit-tests, TDD and BDD", + licenses = {"BSL-1.0"}, + repo = "https://github.com/catchorg/Catch2", + type = "package", + + xpm = { + linux = { + ["2.13.10"] = { + url = { + GLOBAL = "https://github.com/catchorg/Catch2/archive/refs/tags/v2.13.10.tar.gz", + CN = "https://gitcode.com/mcpp-res/catch2/releases/download/2.13.10/catch2-2.13.10.tar.gz", + }, + sha256 = "d54a712b7b1d7708bc7a819a8e6e47b2fde9536f487b89ccbca295072a7d9943", + }, + ["3.15.2"] = { + url = { + GLOBAL = "https://github.com/catchorg/Catch2/archive/refs/tags/v3.15.2.tar.gz", + CN = "https://gitcode.com/mcpp-res/catch2/releases/download/3.15.2/catch2-3.15.2.tar.gz", + }, + sha256 = "acfae120892c2b67a74142d36d060c0caa96f1c3aaa8aabd96e19961163d0420", + }, + }, + macosx = { + ["2.13.10"] = { + url = { + GLOBAL = "https://github.com/catchorg/Catch2/archive/refs/tags/v2.13.10.tar.gz", + CN = "https://gitcode.com/mcpp-res/catch2/releases/download/2.13.10/catch2-2.13.10.tar.gz", + }, + sha256 = "d54a712b7b1d7708bc7a819a8e6e47b2fde9536f487b89ccbca295072a7d9943", + }, + ["3.15.2"] = { + url = { + GLOBAL = "https://github.com/catchorg/Catch2/archive/refs/tags/v3.15.2.tar.gz", + CN = "https://gitcode.com/mcpp-res/catch2/releases/download/3.15.2/catch2-3.15.2.tar.gz", + }, + sha256 = "acfae120892c2b67a74142d36d060c0caa96f1c3aaa8aabd96e19961163d0420", + }, + }, + windows = { + ["2.13.10"] = { + url = { + GLOBAL = "https://github.com/catchorg/Catch2/archive/refs/tags/v2.13.10.tar.gz", + CN = "https://gitcode.com/mcpp-res/catch2/releases/download/2.13.10/catch2-2.13.10.tar.gz", + }, + sha256 = "d54a712b7b1d7708bc7a819a8e6e47b2fde9536f487b89ccbca295072a7d9943", + }, + ["3.15.2"] = { + url = { + GLOBAL = "https://github.com/catchorg/Catch2/archive/refs/tags/v3.15.2.tar.gz", + CN = "https://gitcode.com/mcpp-res/catch2/releases/download/3.15.2/catch2-3.15.2.tar.gz", + }, + sha256 = "acfae120892c2b67a74142d36d060c0caa96f1c3aaa8aabd96e19961163d0420", + }, + }, + }, + + mcpp = { + language = "c++23", + import_std = false, + c_standard = "c11", -- for the anchor TU below + + -- Union of both layouts. v2 resolves */single_include, v3 resolves + -- */src; the other one matches nothing and is skipped. mcpp_generated + -- carries catch2/catch_user_config.hpp for v3. + include_dirs = { "*/single_include", "*/src", "mcpp_generated" }, + + generated_files = { + -- v3 only. Upstream ships catch_user_config.hpp.in and lets CMake + -- materialise it. Only the two VALUE defines are mandatory — + -- everything else in the .in is a #cmakedefine, i.e. absent means + -- "use the compiler-detected default", which is what we want. + -- #ifndef-guarded so a consumer can override via -D. + -- Re-read the upstream .in when bumping v3: a newly added + -- mandatory value-define would otherwise break silently here. + -- v2 never includes this file. + ["mcpp_generated/catch2/catch_user_config.hpp"] = [==[ +#ifndef CATCH_USER_CONFIG_HPP_INCLUDED +#define CATCH_USER_CONFIG_HPP_INCLUDED + +#ifndef CATCH_CONFIG_DEFAULT_REPORTER +#define CATCH_CONFIG_DEFAULT_REPORTER "console" +#endif +#ifndef CATCH_CONFIG_CONSOLE_WIDTH +#define CATCH_CONFIG_CONSOLE_WIDTH 80 +#endif + +#endif // CATCH_USER_CONFIG_HPP_INCLUDED +]==], + -- Required for v2, where the sources glob below matches nothing + -- and a lib target still needs at least one TU (same shape as + -- compat.eigen / compat.khrplatform). Inert extra object on v3. + ["mcpp_generated/catch2_anchor.c"] = [==[ +int mcpp_compat_catch2_anchor(void) { return 0; } +]==], + -- The `main` feature's TU. See the header comment for why this is + -- generated rather than upstream's catch_main.cpp. + ["mcpp_generated/catch2_main.cpp"] = [==[ +#if __has_include() +// Catch2 v3: the library is compiled in; just drive a session. +# include +int main(int argc, char* argv[]) { return Catch::Session().run(argc, argv); } +#else +// Catch2 v2 is header-only: this TU is also where the implementation lands. +# define CATCH_CONFIG_MAIN +# include +#endif +]==], + }, + + sources = { + "mcpp_generated/catch2_anchor.c", + "*/src/catch2/**/*.cpp", + -- Upstream's default main(); would collide with a consumer's own. + "!*/src/catch2/internal/catch_main.cpp", + }, + + targets = { ["catch2"] = { kind = "lib" } }, + features = { + ["main"] = { sources = { "mcpp_generated/catch2_main.cpp" } }, + }, + deps = { }, + }, +} diff --git a/tests/examples/catch2-main/mcpp.toml b/tests/examples/catch2-main/mcpp.toml new file mode 100644 index 0000000..6a2459d --- /dev/null +++ b/tests/examples/catch2-main/mcpp.toml @@ -0,0 +1,16 @@ +# compat.catch2 at v3 — `main` FEATURE path: the package supplies the entry +# point and this project deliberately defines no main(). If the feature stops +# gating the generated TU in, this member fails with +# `undefined reference to 'main'`. +# +# This cannot share a project with tests/examples/catch2: features are resolved +# per consuming project, so one project cannot hold catch2 both with and +# without `main` (same constraint as asio-ssl vs asio-module). +# +# The `compat` index redirect is inherited from the workspace root. +[package] +name = "catch2-main-tests" +version = "0.1.0" + +[dependencies.compat] +catch2 = { version = "3.15.2", features = ["main"] } diff --git a/tests/examples/catch2-main/tests/main.cpp b/tests/examples/catch2-main/tests/main.cpp new file mode 100644 index 0000000..99af269 --- /dev/null +++ b/tests/examples/catch2-main/tests/main.cpp @@ -0,0 +1,16 @@ +// compat.catch2 v3, `main` FEATURE: no main() here on purpose — the entry +// point comes from the package's generated TU. Catch2 prints the assertion +// count on exit, so an empty binary cannot pass quietly. +#include + +static unsigned int factorial(unsigned int n) { + return n <= 1 ? 1 : n * factorial(n - 1); +} + +TEST_CASE("factorial of 5 is 120", "[math]") { + REQUIRE(factorial(5) == 120); +} + +TEST_CASE("factorial of 0 is 1", "[math]") { + REQUIRE(factorial(0) == 1); +} diff --git a/tests/examples/catch2-v2-main/mcpp.toml b/tests/examples/catch2-v2-main/mcpp.toml new file mode 100644 index 0000000..75fe50e --- /dev/null +++ b/tests/examples/catch2-v2-main/mcpp.toml @@ -0,0 +1,13 @@ +# compat.catch2 at v2 — `main` FEATURE path: no main() and no +# CATCH_CONFIG_MAIN here; both come from the package's generated TU. This is +# also what exercises the ELSE branch of that TU's +# __has_include() discriminator — the v3 members only +# ever take the THEN branch. +# +# The `compat` index redirect is inherited from the workspace root. +[package] +name = "catch2-v2-main-tests" +version = "0.1.0" + +[dependencies.compat] +catch2 = { version = "2.13.10", features = ["main"] } diff --git a/tests/examples/catch2-v2-main/tests/main.cpp b/tests/examples/catch2-v2-main/tests/main.cpp new file mode 100644 index 0000000..babfb5c --- /dev/null +++ b/tests/examples/catch2-v2-main/tests/main.cpp @@ -0,0 +1,16 @@ +// compat.catch2 v2, `main` FEATURE: neither main() nor CATCH_CONFIG_MAIN here +// — both come from the package's generated TU, which reaches this version +// through the ELSE branch of its __has_include() test. +#include + +static unsigned int factorial(unsigned int n) { + return n <= 1 ? 1 : n * factorial(n - 1); +} + +TEST_CASE("factorial of 5 is 120", "[math]") { + REQUIRE(factorial(5) == 120); +} + +TEST_CASE("factorial of 0 is 1", "[math]") { + REQUIRE(factorial(0) == 1); +} diff --git a/tests/examples/catch2-v2/mcpp.toml b/tests/examples/catch2-v2/mcpp.toml new file mode 100644 index 0000000..f01d4da --- /dev/null +++ b/tests/examples/catch2-v2/mcpp.toml @@ -0,0 +1,13 @@ +# compat.catch2 at v2 — DEFAULT path: header-only, this project defines +# CATCH_CONFIG_MAIN itself. This is the member that proves the single-package +# union holds: at 2.13.10 the `*/src/catch2/**/*.cpp` glob must match nothing +# and `*/single_include` must resolve, or the build picks up the wrong half. +# +# The `compat` index redirect is inherited from the workspace root. +# Part of the mcpp-index self-referential workspace. +[package] +name = "catch2-v2-tests" +version = "0.1.0" + +[dependencies.compat] +catch2 = "2.13.10" diff --git a/tests/examples/catch2-v2/tests/main.cpp b/tests/examples/catch2-v2/tests/main.cpp new file mode 100644 index 0000000..00290c1 --- /dev/null +++ b/tests/examples/catch2-v2/tests/main.cpp @@ -0,0 +1,14 @@ +#define CATCH_CONFIG_MAIN +#include + +static unsigned int factorial(unsigned int n) { + return n <= 1 ? 1 : n * factorial(n - 1); +} + +TEST_CASE("factorial of 5 is 120", "[math]") { + REQUIRE(factorial(5) == 120); +} + +TEST_CASE("factorial of 0 is 1", "[math]") { + REQUIRE(factorial(0) == 1); +} diff --git a/tests/examples/catch2/mcpp.toml b/tests/examples/catch2/mcpp.toml new file mode 100644 index 0000000..8f0643c --- /dev/null +++ b/tests/examples/catch2/mcpp.toml @@ -0,0 +1,13 @@ +# compat.catch2 at v3 — DEFAULT path: the package is a static library and this +# project supplies its own main() via Catch::Session. Also pins down that +# upstream's catch_main.cpp really is excluded — if the `!` negation stopped +# working, this member would fail with `multiple definition of 'main'`. +# +# The `compat` index redirect is inherited from the workspace root. +# Part of the mcpp-index self-referential workspace. +[package] +name = "catch2-tests" +version = "0.1.0" + +[dependencies.compat] +catch2 = "3.15.2" diff --git a/tests/examples/catch2/tests/main.cpp b/tests/examples/catch2/tests/main.cpp new file mode 100644 index 0000000..9798b12 --- /dev/null +++ b/tests/examples/catch2/tests/main.cpp @@ -0,0 +1,17 @@ +#include + +static unsigned int factorial(unsigned int n) { + return n <= 1 ? 1 : n * factorial(n - 1); +} + +TEST_CASE("factorial of 5 is 120", "[math]") { + REQUIRE(factorial(5) == 120); +} + +TEST_CASE("factorial of 0 is 1", "[math]") { + REQUIRE(factorial(0) == 1); +} + +int main(int argc, char* argv[]) { + return Catch::Session().run(argc, argv); +}