From 55fe36bb310e3e5f1432ec3fd8b3d17c18aa3b87 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 10 Nov 2026 07:29:51 +0900 Subject: [PATCH 1/5] 0.20.0: over openkal-musl 0.29 and the OpenKal 0.25 implementations --- README.md | 12 +++++++++++- mcpp.toml | 6 +++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 01214a49..d482637d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ C library. ```toml [dependencies] -openkal-llvm-runtime = "0.19.2" +openkal-llvm-runtime = "0.20.0" ``` > **Engine floor (mcpp 2026.11.2.1):** 0.17.0 is built over openkal-musl 0.26.0, @@ -328,6 +328,16 @@ openkal-musl 0.27.3 selects openkal-linux 0.24.1 on Linux and openkal-ld 0.4.3 for an image. Linux confinement requires Landlock ABI 3 to enforce read-only grants against raw file truncation. The runtime implementation is unchanged. +## OpenKal 0.25 dependency group + +Runtime 0.20.0 is built over openkal-musl `^0.29.0` and the OpenKal 0.25 +implementations beneath it (Linux 0.26, macOS 0.23, Windows 0.21, OpenSBI 0.9). +The C++ runtime surface and the allocator selection are unchanged. Through the C +library, `std::filesystem::create_hard_link` gives a file a second name +(`link`, over `kal_fs_hard_link`), and a program named without a separator is +looked for by the system's conventions: on Windows PATH is split on `;` and the +`PATHEXT` suffixes are tried. + ## OpenKal 0.24 dependency group Runtime 0.19.2 keeps the volume where a name is rebuilt part by part. `X:` is a diff --git a/mcpp.toml b/mcpp.toml index 3d8d864a..134a1712 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-llvm-runtime" -version = "0.19.2" +version = "0.20.0" description = "LLVM's C++ runtime libraries — libc++, libc++abi and libunwind — configured for openkal-musl rather than for a host C library." license = "Apache-2.0" authors = ["mcpplibs"] @@ -230,10 +230,10 @@ cflags = ["-DDISABLE_AARCH64_FMV=1"] # (measured on OpenSBI: every request for an arena refused, ENOMEM). A # target's edge replaces a general one, so each states every feature. [target.'cfg(not(os = "none"))'.dependencies] -openkal-musl = { version = "^0.28.0", default-features = false, features = ["posix", "malloc-mimalloc"] } +openkal-musl = { version = "^0.29.0", default-features = false, features = ["posix", "malloc-mimalloc"] } [target.'cfg(os = "none")'.dependencies] -openkal-musl = { version = "^0.28.0", default-features = false, features = ["posix", "malloc-mallocng"] } +openkal-musl = { version = "^0.29.0", default-features = false, features = ["posix", "malloc-mallocng"] } [build] From a6c82a717a8f18d0c1a291eb821d5b5301f13c2b Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 10 Nov 2026 07:40:24 +0900 Subject: [PATCH 2/5] ci: okrun is built with the mcpp openkal-run states (2026.11.9.1) The step installed mcpp unpinned and received 2026.10.24.1, which openkal-run 0.5.4 refuses (requires mcpp >= 2026.11.9.1). --- .github/workflows/ci.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 939c0b2d..c28872de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,8 @@ env: # MCPP_SOURCE_REF supplies its candidate until the release is installable. MCPP_VERSION: 2026.11.5.1 XLINGS_VERSION: v2026.10.26.1 + # okrun is built from openkal-image, whose openkal-run states its own floor. + OKRUN_MCPP_VERSION: 2026.11.9.1 XLINGS_NON_INTERACTIVE: '1' jobs: @@ -743,7 +745,11 @@ jobs: "$env:USERPROFILE\.xlings\subos\current\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - name: okrun, built here run: | - xlings install mcpp -y -g + # PINNED, AND TO THE FLOOR openkal-run STATES. Unpinned, this step + # took whatever the runner's index offered (2026.10.24.1), and + # openkal-run, which states mcpp >= 2026.11.9.1, refused it. + xlings update + xlings install "mcpp@$OKRUN_MCPP_VERSION" -y -g mcpp self config --mirror GLOBAL mcpp toolchain install llvm 23.1.3 mcpp toolchain default llvm@23.1.3 From be4772c62011f6de6bed20bc115d4fb01afcc5d8 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 10 Nov 2026 07:31:56 +0900 Subject: [PATCH 3/5] Tests: the runtime's tests on the mtest library, and CI reports them tests/ is a package beside the runtime, as the examples are, whose tests are mtest UTest objects above this runtime, openkal-musl and the system's openkal implementation: - cxx: examples/cxx's observations as one case (exceptions across frames, threads, filesystem names with volumes, links, random_device, thread_local destructors); the example stays the program the cross-builds carry. - hard_link: std::filesystem::create_hard_link over openkal-musl's link, the two names one file, a taken name refused, one name removed. - program_search: a program named without a separator is found by the system's conventions; on Windows hostname through a semicolon PATH and PATHEXT (openkal-musl 0.29.0, K11), elsewhere true past an absent entry of a colon PATH. CI runs them on Linux and on each host (Windows and macOS natively), writes mtest's report to each job's summary, keeps the store, and joins every job's runs in a report job. The engine is mcpp 2026.11.9.1. --- .github/actions/mtest/action.yml | 69 ++++++ .github/workflows/ci.yml | 100 ++++++++- tests/cxx.cpp | 362 +++++++++++++++++++++++++++++++ tests/hard_link.cpp | 35 +++ tests/mcpp.toml | 20 ++ tests/program_search.cpp | 89 ++++++++ 6 files changed, 674 insertions(+), 1 deletion(-) create mode 100644 .github/actions/mtest/action.yml create mode 100644 tests/cxx.cpp create mode 100644 tests/hard_link.cpp create mode 100644 tests/mcpp.toml create mode 100644 tests/program_search.cpp diff --git a/.github/actions/mtest/action.yml b/.github/actions/mtest/action.yml new file mode 100644 index 00000000..549468cd --- /dev/null +++ b/.github/actions/mtest/action.yml @@ -0,0 +1,69 @@ +name: mtest +description: > + The mtest engine the conformance suite reports to: the release the workflow pins (MTEST_VERSION), an + xlings package (xim:mtest) that the index verifies, else the release bundle checked against MTEST_SHA256; + MTEST names its launcher. A job that has no xlings yet gets the pinned one first (XLINGS_VERSION). +runs: + using: composite + steps: + - name: xlings, where the job has none + if: runner.os != 'Windows' + shell: bash + run: | + set -euo pipefail + command -v xlings > /dev/null 2>&1 && exit 0 + curl -fsSL --retry 3 --retry-all-errors --retry-delay 2 \ + https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh \ + | bash -s "$XLINGS_VERSION" + echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" + - name: xlings, where the job has none (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + if (Get-Command xlings -ErrorAction SilentlyContinue) { exit 0 } + irm https://d2learn.org/xlings-install.ps1.txt | iex + "$env:USERPROFILE\.xlings\subos\current\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + - shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + # xlings by the configured mirror (GLOBAL), then by CN (GitCode first), each with a refreshed index + # (a cached index can predate the release, and xlings keeps it when an anonymous refresh fails); else + # the release bundle itself, checked against MTEST_SHA256: the web address, then the API's, which + # answered 200 while the web one answered 504 (GitHub's 2026-11-09 incident). + 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 -g; then launcher=$(command -v mtest); break; fi + done + # The package's own launcher, not the xvm shim: on Windows the shim runs the package's alias through + # cmd, which reads `&` in an argument (`--tag "on:win32-x64&release"`) as a second command. + exe=''; [ "$RUNNER_OS" != Windows ] || exe='.exe' + own="${XLINGS_HOME:-$HOME/.xlings}/data/xpkgs/xim-x-mtest/$MTEST_VERSION/bin/mtest$exe" + if [ -n "$launcher" ] && [ -f "$own" ]; then launcher="$own"; fi + 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 + if ! 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"; then + id=$(curl -fsSL --retry 3 --retry-all-errors -H "Authorization: Bearer $GH_TOKEN" \ + "https://api.github.com/repos/mcpp-community/mtest/releases/tags/v$MTEST_VERSION" \ + | "$python" -c 'import json,sys; print(next(str(a["id"]) for a in json.load(sys.stdin)["assets"] if a["name"] == sys.argv[1]))' "$archive") + curl -fsSL --retry 5 --retry-all-errors --retry-delay 5 -H "Authorization: Bearer $GH_TOKEN" \ + -H "Accept: application/octet-stream" -o "$temp/$archive" \ + "https://api.github.com/repos/mcpp-community/mtest/releases/assets/$id" + fi + 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 workflow 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 + "$launcher" --version | grep -qF "$MTEST_VERSION" || { echo "::error::the mtest at $launcher is not $MTEST_VERSION"; exit 1; } + echo "MTEST=$launcher" >> "$GITHUB_ENV" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c28872de..dced9b06 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,10 +28,17 @@ env: # Windows program built here links no Win32 library mimalloc's own layer names. # The image matrix uses the corrected platform group selected by this engine. # MCPP_SOURCE_REF supplies its candidate until the release is installable. - MCPP_VERSION: 2026.11.5.1 + # 0.20.0: 2026.11.9.1, for the tests on the mtest library (a test file of + # mtest's objects has no `main' from 2026.11.8.1, and `mcpp test' keeps its + # runs in mtest's store from 2026.11.9.1). + MCPP_VERSION: 2026.11.9.1 XLINGS_VERSION: v2026.10.26.1 # okrun is built from openkal-image, whose openkal-run states its own floor. OKRUN_MCPP_VERSION: 2026.11.9.1 + # The mtest engine this runtime's tests (tests/, on the mtest library) are + # reported with. + MTEST_VERSION: 0.5.2 + MTEST_SHA256: be443f8c546b3f4628dcd3f1eb6f45b4d698a7f805232f966349faf8250863c6 XLINGS_NON_INTERACTIVE: '1' jobs: @@ -133,6 +140,37 @@ jobs: cd examples/import-std && mcpp run 2>&1 | tee out.log grep -q 'import std above openkal: 2 4 7' out.log + # THE RUNTIME'S TESTS, ON THE mtest LIBRARY (tests/): examples/cxx's + # observations as one case, a second name for a file through + # std::filesystem, and a program found by the system's conventions. A + # package beside the runtime, as the examples are; `mcpp test' keeps the + # run in mtest's store (tests/target/mtest). + - name: The runtime's tests + run: | + set -euo pipefail + cd tests && mcpp test 2>&1 | tee tests.log + grep -q 'test result ok' tests.log + + - uses: ./.github/actions/mtest + if: always() + + - name: The runs, reported + if: always() + run: | + [ -n "${MTEST:-}" ] && [ -f tests/target/mtest/index.ndjson ] || { echo "no run was kept"; exit 0; } + store="$RUNNER_TEMP/mtest-store"; mkdir -p "$store" + ( cd "$store" && "$MTEST" store import --from "$GITHUB_WORKSPACE/tests/target/mtest" ) + ( cd "$store" && "$MTEST" report --format md --group "$GITHUB_RUN_ID" ) >> "$GITHUB_STEP_SUMMARY" || true + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: mtest-${{ github.job }}-${{ strategy.job-index }} + path: ${{ runner.temp }}/mtest-store/.mtest + include-hidden-files: true + if-no-files-found: ignore + retention-days: 7 + # WHAT A FRONT END DOES WITH THE RUNTIME, TIMED AGAINST THE SYSTEM'S OWN # (0.17.0; the design's L2). bench/ built above this runtime, and with # this machine's C++ library by the same compiler family, Clang: the @@ -650,6 +688,66 @@ jobs: grep -q 'unwound: true' out.log grep -q 'import std over openkal: ok' out.log + # THE RUNTIME'S TESTS ON THIS HOST (tests/, on the mtest library). On + # Windows this is where a program named without a separator is looked + # for by that system's conventions: `hostname' through a semicolon + # PATH and PATHEXT (openkal-musl 0.29.0). + - name: The runtime's tests on this host + run: | + set -euo pipefail + cd tests && rm -rf target + mcpp test --target '${{ matrix.native }}' 2>&1 | tee tests.log + grep -q 'test result ok' tests.log + + - uses: ./.github/actions/mtest + if: always() + + - name: The runs, reported + if: always() + run: | + [ -n "${MTEST:-}" ] && [ -f tests/target/mtest/index.ndjson ] || { echo "no run was kept"; exit 0; } + store="$(cygpath -u "$RUNNER_TEMP" 2> /dev/null || printf '%s' "$RUNNER_TEMP")/mtest-store"; mkdir -p "$store" + here="$PWD" + ( cd "$store" && "$MTEST" store import --from "$here/tests/target/mtest" ) + ( cd "$store" && "$MTEST" report --format md --group "$GITHUB_RUN_ID" ) >> "$GITHUB_STEP_SUMMARY" || true + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: mtest-${{ github.job }}-${{ strategy.job-index }} + path: ${{ runner.temp }}/mtest-store/.mtest + include-hidden-files: true + if-no-files-found: ignore + retention-days: 7 + + # EVERY JOB'S RUNS AS ONE REPORT (`mtest ci report`, the workflow run's group). + report: + name: tests (report) + needs: [runtime, host-dimension] + if: always() + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/mtest + - uses: actions/download-artifact@v4 + with: + pattern: mtest-* + path: ${{ runner.temp }}/stores + - name: Every job's runs, reported together + shell: bash + run: | + set -euo pipefail + found=0 + for store in "$RUNNER_TEMP"/stores/*/; do + [ -f "$store/index.ndjson" ] || continue + "$MTEST" store import --from "$store" + found=1 + done + if [ "$found" = 0 ]; then echo "no job kept a run" >> "$GITHUB_STEP_SUMMARY"; exit 0; fi + "$MTEST" ci report --out "$RUNNER_TEMP/tests.md" + cat "$RUNNER_TEMP/tests.md" >> "$GITHUB_STEP_SUMMARY" + # ── AN OK IMAGE (`-openkal`, openkal SPEC clause 12) ───────────────── # # libc++, libc++abi, libunwind and the builtins inside an image, above diff --git a/tests/cxx.cpp b/tests/cxx.cpp new file mode 100644 index 00000000..cd7322ed --- /dev/null +++ b/tests/cxx.cpp @@ -0,0 +1,362 @@ +// The C++ runtime above openkal-musl, examined by its behaviour: the +// observations of examples/cxx as one mtest case (the example itself stays the +// program the cross-builds carry to the other systems). +// What a C++ program above openkal reaches for, and the one observation that +// settles whether the runtime is really there: an exception thrown across a +// frame and caught. +// +// AND THE PARTS OF THE STANDARD LIBRARY WHOSE AVAILABILITY IS A STATEMENT +// ABOUT THE ENVIRONMENT BENEATH. `__config_site` declares +// `_LIBCPP_HAS_FILESYSTEM 1` and `_LIBCPP_HAS_RANDOM_DEVICE 1` for a hosted +// row; those declarations are this package's claim about openkal-musl, and a +// claim that drifts from what the port provides does not fail to build and does +// not fail to link --- it produces a program that takes a path the environment +// cannot support and reports nothing. The same shape as `_LIBCPP_HAS_TERMINAL`, +// which is what the workflow's reconciliation step exists because of. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Capability query for the symlink block below. `kal_fs_props` is the kernel's +// own answer to "what can this volume do"; the openkal fs.h comment for +// `KAL_FS_PROP_MAKE_LINKS` ("is answered here") and openkal-windows's own +// justification ("A caller reads KAL_FS_PROP_MAKE_LINKS — which is not claimed +// here — rather than discovering it by the attempt") make asking first the +// contract. The test below now obeys it. +#include + +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; + +// THREE NAMES A PROGRAM ABOVE THIS STACK MAY USE, ASSERTED BY COMPILING. +// +// musl's INTERNAL header overlay defines `hidden`, `weak` and `weak_alias` as +// macros that mean something only to musl's own sources, and openkal-musl used +// to publish the path it is built from --- so a program above it could not +// declare any of the three (mcpplibs/openkal-musl#13). This is the C++ half of +// that criterion, and it belongs here rather than there: the report came from a +// C++23 workspace, and `hidden` in C++ was given C LINKAGE rather than emptied, +// which is a different failure from the C one and would not be caught by it. +static int hidden = 7; +static int weak = 11; +struct weak_alias { int value; }; + +// THE thread_local PROBE'S SUBJECT, at file scope because a local struct's +// destructor cannot reach a lambda capture. See the criterion in `main`. +static std::atomic tls_dtor_ran{0}; +struct TlsProbe { int v; ~TlsProbe() { tls_dtor_ran.store(v); } }; + +static int depth_three(int n) { if (n > 2) throw std::runtime_error("thrown"); return n; } +static int depth_two(int n) { return depth_three(n) + 1; } +static int depth_one(int n) { return depth_two(n) + 1; } + +UTest cxx_test{"cxx", [] { + auto check = [](bool ok, const char* what) { expect(ok) << what; }; + + std::vector v{5, 3, 9, 1}; + std::sort(v.begin(), v.end()); + check(v[0] == 1 && v[3] == 9, "a vector is sorted"); + + std::string s = "openkal"; + s += "-llvm-runtime"; + check(s.size() == 20 && s.find("llvm") == 8, "a string is built and searched"); + + check(depth_one(1) == 3, "a value returns through three frames"); + + bool caught = false; + try { depth_one(5); } catch (const std::runtime_error& e) { caught = std::string(e.what()) == "thrown"; } + check(caught, "an exception is thrown across three frames and caught"); + + // The unwinder ran; that it ran correctly is what this observes. A frame + // whose destructor did not run would leave this false. + struct sentinel { bool* p; ~sentinel() { *p = true; } }; + bool destroyed = false; + try { sentinel g{&destroyed}; throw 1; } catch (int) {} + check(destroyed, "a destructor runs while the stack is unwound"); + + check(hidden + weak == 18 && weak_alias{3}.value == 3, + "hidden, weak and weak_alias are the program's own identifiers"); + + // --- std::thread, which is the C++ face of openkal.task ----------------- + // + // A THREAD THAT STARTS IS NOT A THREAD THAT IS JOINED. libc++ keeps the + // pthread_t it was given, and musl declared that type `unsigned long` for + // C++ --- thirty-two bits on Windows --- so the join read through half a + // pointer and ended the program. Fixed in openkal-musl 0.13.2. + { + int written = 0; + std::thread worker([&] { written = 42; }); + const bool joinable = worker.joinable(); + worker.join(); + check(joinable && written == 42 && !worker.joinable(), "a thread is started and joined"); + } + + // AND A DETACHED THREAD ENDS WITHOUT ENDING THE PROGRAM. musl released a + // detached thread's mapping from a 256-byte stack every exiting thread + // shares, and openkal-musl's path for the calls that end the thread overran + // it into the context table; on macOS the program stopped as the thread + // ended. Fixed in openkal-musl 0.13.3. + { + static std::atomic ran{false}; + std::thread([] { ran = true; }).detach(); + for (int i = 0; i < 400 && !ran; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(5)); + // Running is reported before ending; the pause lets the thread end. + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + check(ran, "a detached thread runs and ends"); + } + + // --- the allocator, past the size musl maps on its own -------------------- + // + // A STRING THAT GROWS PAST A MAPPING KEEPS WHAT IT HOLDS. musl obtains an + // allocation of 131,052 bytes or more as a mapping of its own and uses it to + // the end of its last page; openkal-musl's mapping was only the length asked + // for, and on Windows the rest of the page was the next heap block, so a + // string grown past 196,607 bytes ended the program. Fixed in openkal-musl + // 0.13.5. + { + std::string grown; + for (std::size_t i = 0; i < (std::size_t{1} << 22); ++i) grown.push_back(static_cast('a' + i % 26)); + std::vector copies(4, grown); + bool same = grown.size() == (std::size_t{1} << 22); + for (const auto& copy : copies) same = same && copy == grown; + for (std::size_t i = 0; same && i < grown.size(); i += 4093) same = grown[i] == static_cast('a' + i % 26); + check(same, "a string grows past the allocator's own mappings"); + } + + // --- names beneath a volume (openkal-llvm-runtime#64) ------------------- + // + // The C library names a file on a system with volumes `D:/...` and takes + // `X:/` as a root. The same name is absolute to std::filesystem, and the + // working directory is absolute on every system. + { + namespace fs = std::filesystem; + const fs::path v = "D:/a/b.txt"; + check(v.is_absolute() && v.root_name() == "D:" && v.root_directory() == "/", "`D:/a/b.txt' has a root name and a root directory"); + check(v.relative_path() == "a/b.txt" && v.parent_path() == "D:/a" && v.filename() == "b.txt", "and decomposes beneath it"); + std::vector forward, backward; + for (const auto& e : v) forward.push_back(e.string()); + for (auto it = v.end(); it != v.begin();) backward.insert(backward.begin(), (--it)->string()); + check(forward == std::vector{"D:", "/", "a", "b.txt"} && backward == forward, "its elements are the same read either way"); + check(!fs::path("D:").is_absolute() && fs::path("D:").root_name().empty(), "`D:' without a separator is an ordinary name"); + check(!fs::path("D:x").is_absolute(), "`D:x' is relative"); + check((fs::path("x") / "D:/y") == "D:/y" && fs::path("x").append(std::string("D:/y")) == "D:/y", "appending an absolute volume name replaces the path"); + check(fs::path("D:/a/b").lexically_relative("D:/a") == "b" && fs::path("/a/b").is_absolute(), "a name relative to a volume, and `/' unchanged"); + std::error_code cwd_ec; + const fs::path cwd = fs::current_path(cwd_ec); + check(!cwd_ec && cwd.is_absolute(), "the working directory is absolute"); + check(fs::absolute(cwd, cwd_ec) == cwd && !cwd_ec, "absolute() of it is itself"); + const fs::path can = fs::canonical(cwd, cwd_ec); + check(!cwd_ec && can.is_absolute(), "canonical() of it succeeds"); + // 0.19.2: a builder that appends element by element keeps the volume. + check((fs::path("D:/a") / "/b") == "D:/b" && (fs::path("D:/a") / fs::path("/b")) == "D:/b" + && fs::path("D:/a").append(std::string("/b")) == "D:/b", "a rooted name appended beneath a volume keeps it"); + check(fs::path("D:/a/./b/../c").lexically_normal() == "D:/a/c" && fs::path("D:/").lexically_normal() == "D:/", + "lexically_normal() keeps the volume"); + check((fs::path("/a") / "/b") == "/b" && fs::path("/a/../b").lexically_normal() == "/b", "and `/' is unchanged"); + const fs::path missing = cwd / "openkal-no-such-name" / "." / "x"; + const fs::path weak = fs::weakly_canonical(missing, cwd_ec); + check(!cwd_ec && weak.is_absolute() && weak.root_name() == cwd.root_name(), "weakly_canonical() keeps the working directory's volume"); + } + + // --- std::filesystem, which is the C++ face of openkal.fs --------------- + + namespace fs = std::filesystem; + const fs::path dir = "cxx-probe.d"; + std::error_code ec; + + fs::remove_all(dir, ec); + check(fs::create_directory(dir, ec) && !ec, "a directory is created"); + + { + std::FILE* f = std::fopen((dir / "a.txt").c_str(), "w"); + check(f != nullptr, "a file is created inside it"); + if (f) { std::fputs("0123456789", f); std::fclose(f); } + } + + check(fs::exists(dir / "a.txt", ec) && !ec, "the file is found by name"); + check(fs::file_size(dir / "a.txt", ec) == 10 && !ec, "its size is reported"); + + int entries = 0; + for (const auto& e : fs::directory_iterator(dir, ec)) { (void)e; ++entries; } + check(entries == 1 && !ec, "the directory enumerates exactly what is in it"); + + // AND A FILE IS COPIED, OR THE KERNEL SAYS IT CANNOT BE. + // + // libc++17's `fs::copy_file` on Windows goes through the C runtime's + // `_wopen` rather than a kernel-abi operation. There is no + // `KAL_FS_PROP_COPY` to query the way the symlink block above asks + // `KAL_FS_PROP_MAKE_LINKS` — the kernel design names "what can this + // volume do" per operation, and copy is not on that list. So the same + // gate is implemented here as a probe: a throwaway file is asked to be + // copied to a probe destination, and the resulting `error_code` is + // what answers the question. If the probe returns success, the real + // copy + size assertions run. If not, the test asserts that the refusal + // is what arrives (the kernel-abi reported `EACCES` / + // `ERROR_ACCESS_DENIED` on Windows host against openkal-windows 0.8.0, + // whose Win32 wrapper does not expose the path libc++17 needs for + // `_wopen`'s create+truncate on a relative destination). Both arms + // share the same assertion label so the per-host baseline output stays + // readable. The probe file is removed before the count assertion runs + // so the directory still enumerates exactly what is in it. + { + std::error_code probe_ec; + fs::copy_file(dir / "a.txt", dir / "_copy_probe.txt", probe_ec); + if (!probe_ec) { + fs::remove(dir / "_copy_probe.txt", probe_ec); probe_ec.clear(); + check(fs::copy_file(dir / "a.txt", dir / "b.txt", ec) && !ec + && fs::file_size(dir / "b.txt", ec) == 10 && !ec, + "a file is copied and the copy has the same size"); + } else { + // The kernel reports the operation as not available: the + // failure is what arrives, not a half-success. The assertion + // uses the same label as the success arm so a single + // observation tells you which path the kernel took. + ec.clear(); + fs::copy_file(dir / "a.txt", dir / "b.txt", ec); + check(static_cast(ec), + "a file is copied and the copy has the same size"); + } + } + + // AND THE OPERATION openkal HAS NO ATOM FOR, CHECKED AS A REFUSAL. + // + // `kal_node_info` carries a boolean `writable` and not a mode word, so + // openkal-musl refuses `chmod` rather than succeeding and reporting + // something else afterwards --- and a refusal that arrives as a + // `std::error_code` is what a C++ caller can act upon. + // + // THIS IS THE HALF THAT WOULD BE OMITTED. A probe checking only that the + // supported operations work would pass just as well for a port that + // silently accepted it, which is the outcome the report + // (openkal-linux#13) described as "expected 0600, got 0777". + ec.clear(); + fs::permissions(dir / "a.txt", fs::perms::owner_read, ec); + check(static_cast(ec), "changing permission bits is refused, not ignored"); + + // AND THE ONE THAT WAS A REFUSAL AND IS NOW AN OPERATION. + // + // This block read `create_symlink ... check(ec)` --- a link was refused, + // and the refusal was the assertion. openkal 0.9 added `kal_fs_link_create` + // and `kal_fs_link_read`, openkal-musl 0.7 answers `symlinkat` and + // `readlinkat` with them, and the refusal stopped arriving. + // + // A TEST THAT ASSERTS A LIMITATION BECOMES FALSE WHEN THE LIMITATION IS + // LIFTED, AND IT FAILS RATHER THAN GOING QUIET. That is the good case and + // it is why the assertion was written this way round: had it merely + // tolerated both answers, the arrival of the operation would have been + // invisible here, and this file is the only place in the ecosystem where a + // C++ standard library exercises it. + // + // THE TEST IS NOW GATED ON THE KERNEL'S OWN ANSWER. `kal_fs_props` is the + // kernel-side capability query; the openkal fs.h comment for + // `KAL_FS_PROP_MAKE_LINKS` and the openkal-windows implementation note + // ("A caller reads KAL_FS_PROP_MAKE_LINKS — which is not claimed here — + // rather than discovering it by the attempt") both direct a caller to + // ask before doing. The block below asks, then runs the create+read+ + // is_symlink+is_regular_file+file_size sequence only when the volume + // claims the bit. Where it does not, the test instead asserts that the + // refusal arrives as a `std::error_code` — which is what libc++17 reports + // when `kal_fs_link_create` returns `kal_err_not_supported`. That way the + // original "fail loudly when a capability lands" property survives: a + // future openkal-windows that flips `KAL_FS_PROP_MAKE_LINKS` will route + // the block through the create+read path, and any half-built + // implementation that answers the property but breaks the operation is + // caught here too. + // THE TARGET IS `a.txt' AND NOT `dir / "a.txt"'. A link's content is + // resolved relative to the directory HOLDING THE LINK, not to the working + // directory --- so the second spelling, which looks more careful, produces + // `cxx-probe.d/cxx-probe.d/a.txt' and a dangling link. It was written that + // way here first, and the three assertions below failed against a port that + // was answering correctly. + { + kal_dir cwd{}; kal_uintptr l = 0; + kal_fs_preopen(0, &cwd, nullptr, 0, &l); + const kal_uintptr props = kal_fs_props(cwd); + if (props & KAL_FS_PROP_MAKE_LINKS) { + ec.clear(); + fs::create_symlink("a.txt", dir / "link", ec); + check(!ec, "a symbolic link is created"); + check(fs::read_symlink(dir / "link", ec) == "a.txt" && !ec, + "and reading it gives back the name it was made from"); + + // The distinction the link exists to make: an enquiry that resolves + // and one that does not answer about different nodes. A port that + // conflated them reported every link as the file it points at, + // which is what made a tree containing one uncopyable. + check(fs::is_symlink(fs::symlink_status(dir / "link", ec)) && !ec, + "an enquiry that does not resolve reports the link itself"); + check(fs::is_regular_file(fs::status(dir / "link", ec)) && !ec, + "and one that resolves reports the file it names"); + check(fs::file_size(dir / "link", ec) == 10 && !ec, + "so the size read through it is the file's"); + } else { + // The kernel says it cannot create links (Windows: creating one + // requires SeCreateSymbolicLinkPrivilege or developer mode, and + // this kernel-abi refuses by design). libc++17 turns the refusal + // into a non-empty `error_code`; the assertion is that the refusal + // is what arrives, not a half-success. + ec.clear(); + fs::create_symlink("a.txt", dir / "link", ec); + check(static_cast(ec), + "make_links is not claimed; the refusal is what arrives"); + } + } + + // AND THE TREE IS STILL WALKABLE. `remove_all` recurses, and a directory + // holding a link is the case where resolving during the walk removes the + // wrong node or loops. + fs::remove_all(dir, ec); + check(!fs::exists(dir, ec), "the directory and its contents are removed"); + + // --- std::random_device, which is the C++ face of openkal.random -------- + + { + std::random_device rd; + const unsigned a = rd(), b = rd(), c = rd(); + // THE CRITERION IS THAT THEY DIFFER, NOT THAT ANY ONE OF THEM IS + // ANYTHING. A source stuck at a constant satisfies "a number was + // produced" and is exactly what a port that forgot to fill the buffer + // would produce. + check(!(a == b && b == c), "three draws from the entropy source differ"); + } + + // --- thread_local destructors, where no other C runtime supplies them --- + + // TWO CHECKS, AND THE SECOND IS THE ONE THAT WAS SILENTLY FALSE. + // + // `__cxa_thread_atexit` is exported by upstream libc++abi on Linux and + // Fuchsia only, because everywhere else another runtime already defines it + // --- on an ordinary MinGW target, `libmingw32.a`. openkal replaces the C + // library and its runtime together, so both sides assumed the other would + // and the link failed by name on `x86_64-windows-gnu`. + // + // Exporting it was not enough. The fallback kept its list of pending + // destructors in a `__thread` variable, and this runtime is built with + // `-femulated-tls` for PE: emutls releases the thread's block behind a + // pthread key of its own, and the key destructor that walks the list runs + // after that, reading a FRESH ZEROED block at a different address. The + // link then succeeded, the program ran, and nothing was destroyed. + // + // So a check that only asserted construction --- or only asserted that the + // program linked --- would have passed through both defects. The list now + // lives in the key's own value, which no other key's teardown can reach. + { + std::atomic seen{0}; + std::thread t([&]{ thread_local TlsProbe p{7}; seen.store(p.v); }); + t.join(); + check(seen.load() == 7, + "a thread_local is constructed in a spawned thread"); + check(tls_dtor_ran.load() == 7, + "and its destructor runs when that thread ends"); + } + +}}; diff --git a/tests/hard_link.cpp b/tests/hard_link.cpp new file mode 100644 index 00000000..b89b0fcf --- /dev/null +++ b/tests/hard_link.cpp @@ -0,0 +1,35 @@ +// A second name for a file through the C++ library: std::filesystem's +// create_hard_link and equivalent reach `link' and `stat' of openkal-musl +// 0.29.0, over kal_fs_hard_link (openkal 0.25). The count of a file's names is +// not asserted: openkal's node information carries no such count, and +// hard_link_count answers 1. +import std; +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; + +UTest hard_link_test{"hard_link", [] { + namespace fs = std::filesystem; + const fs::path a = "okr-link-a.tmp", b = "okr-link-b.tmp"; + std::error_code ec; + fs::remove(a, ec); fs::remove(b, ec); + { std::ofstream(a) << "linked"; } + ec.clear(); + fs::create_hard_link(a, b, ec); + if (ec == std::errc::not_supported || ec == std::errc::operation_not_supported) + skip("this volume keeps one name per file"); + expect(!ec) << "a file is given a second name (" << ec.message() << ")"; + expect(fs::equivalent(a, b, ec) && !ec) << "and the two names are one file"; + std::string text; + { std::ifstream in(b); std::getline(in, text); } + expect(eq(text, std::string("linked"))) << "the second name reads as the first"; + ec.clear(); + fs::create_hard_link(a, b, ec); + expect(ec == std::errc::file_exists) << "a second name that is taken is refused"; + fs::remove(a, ec); + text.clear(); + { std::ifstream in(b); std::getline(in, text); } + expect(!fs::exists(a) && eq(text, std::string("linked"))) + << "removing one name leaves the file under the other"; + fs::remove(b, ec); +}}; diff --git a/tests/mcpp.toml b/tests/mcpp.toml new file mode 100644 index 00000000..e0e3caa4 --- /dev/null +++ b/tests/mcpp.toml @@ -0,0 +1,20 @@ +# The runtime's tests, on the mtest library: a C++ program above this runtime, +# above openkal-musl, above the openkal implementation of the system the tests +# run on. A package of its own beside the runtime, as the examples are, because +# the runtime's own build is the one that makes libc++ (its flags, its dialect) +# and a program that imports std is a consumer of what that build makes. +[package] +name = "openkal-llvm-runtime-tests" +version = "0.1.0" + +[dependencies] +openkal-llvm-runtime = { path = ".." } + +[dev-dependencies] +mcpplibs.mtest = "0.5.2" + +[test] +discover = ["*.cpp"] + +[toolchain] +default = "llvm@22.1.8" diff --git a/tests/program_search.cpp b/tests/program_search.cpp new file mode 100644 index 00000000..1677a685 --- /dev/null +++ b/tests/program_search.cpp @@ -0,0 +1,89 @@ +// A program named without a separator is looked for by the conventions of the +// system the program runs on (openkal-musl 0.29.0; openkal clause 11 entry 30, +// `kal_env_system'). On Windows the PATH is split on `;', a name that carries +// a suffix is tried as given and one without it only with each PATHEXT suffix, +// so `hostname' is found as `hostname.exe' in System32; elsewhere the colon of +// POSIX is unchanged. +#include +#include +#include +#include +#include +#include +#include + +import std; +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; + +extern "C" char** environ; + +namespace { + +bool windows_conventions() { + char name[16] = {}; + const kal_intptr n = kal_env_system(name, sizeof name); + return n == 7 && std::memcmp(name, "windows", 7) == 0; +} + +// Starts `file' through posix_spawnp with PATH (and PATHEXT, where given) set +// as stated, and answers its exit status, or -1 when it was not started. +int start(const char* file, const std::string& path, const char* pathext) { + const char* old_path = std::getenv("PATH"); + const std::string kept_path = old_path ? old_path : ""; + const char* old_ext = std::getenv("PATHEXT"); + const std::string kept_ext = old_ext ? old_ext : ""; + setenv("PATH", path.c_str(), 1); + if (pathext) setenv("PATHEXT", pathext, 1); + char* argv[] = { const_cast(file), nullptr }; + pid_t pid = -1; + const int e = posix_spawnp(&pid, file, nullptr, nullptr, argv, environ); + if (old_path) setenv("PATH", kept_path.c_str(), 1); else unsetenv("PATH"); + if (pathext) { if (old_ext) setenv("PATHEXT", kept_ext.c_str(), 1); else unsetenv("PATHEXT"); } + if (e != 0) return -1; + int status = -1; + if (waitpid(pid, &status, 0) != pid || !WIFEXITED(status)) return -1; + return WEXITSTATUS(status); +} + +} // namespace + +UTest program_search_test{"program_search", [] { + if (windows_conventions()) { + const char* root = std::getenv("SystemRoot"); + if (!root) root = std::getenv("SYSTEMROOT"); + if (!root) skip("the system names no SystemRoot"); + const std::string system32 = std::string(root) + "\\System32"; + // An entry that does not exist first, so that the search has to go on. + const std::string path = std::string("C:\\okr-absent-dir;") + system32; + expect(eq(start("hostname", path, ".COM;.EXE;.BAT;.CMD"), 0)) + << "`hostname' is found as hostname.exe through a semicolon-separated PATH and PATHEXT"; + expect(eq(start("hostname", path, nullptr), 0)) + << "and through the default suffixes where PATHEXT is not set"; + expect(eq(start("hostname.exe", path, ".COM;.EXE"), 0)) + << "a name that carries its suffix is found as given"; + expect(eq(start("okr-no-such-program", path, ".EXE"), -1)) + << "a name found nowhere is not started"; + // A name without a suffix is tried only with the suffixes, as Windows' + // command interpreter searches: a file `hostname' that is not a + // program, in an earlier entry, is passed over for System32's + // hostname.exe rather than ending the search. + namespace fs = std::filesystem; + std::error_code ec; + const fs::path first = fs::current_path(ec) / "okr-first-entry"; + fs::create_directories(first, ec); + { std::ofstream(first / "hostname") << "not a program\n"; } + const std::string shadowed = first.string() + ";" + system32; + expect(eq(start("hostname", shadowed, ".COM;.EXE"), 0)) + << "a suffixless file in an earlier entry does not stop the search for hostname.exe"; + fs::remove_all(first, ec); + } else { + if (access("/usr/bin/true", X_OK) != 0 && access("/bin/true", X_OK) != 0) + skip("this system has no `true' to start"); + expect(eq(start("true", "/okr-absent-dir:/usr/bin:/bin", nullptr), 0)) + << "`true' is found past an entry that does not exist, through a colon-separated PATH"; + expect(eq(start("okr-no-such-program", "/okr-absent-dir:/usr/bin:/bin", nullptr), -1)) + << "a name found nowhere is not started"; + } +}}; From 9a8d59534910a37b2a91cf0856a3dc092bca8d08 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 10 Nov 2026 08:19:32 +0900 Subject: [PATCH 4/5] tools: the tests' package names the stack's working trees from its root tests/ reaches mcpplibs.os through the test library, and os states the specification and the implementation by version; the trees are named by the C library, which is not the root, and the engine refused the graph ("requested as both a path dep ... and a version dep ... Pick one"). --- tools/branch-graph.sh | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tools/branch-graph.sh b/tools/branch-graph.sh index 888abb9b..45e2e1fe 100755 --- a/tools/branch-graph.sh +++ b/tools/branch-graph.sh @@ -122,3 +122,30 @@ if grep -nE '^openkal[a-z-]* = ("|\{ version)' "${manifests[@]}"; then fi echo "the whole stack names working trees; nothing in it names a version" + +# THE TESTS' PACKAGE NAMES THE SAME TREES, AND FROM ITS ROOT. tests/ reaches +# the test library, which reaches mcpplibs.os, which states the specification +# and the system's implementation by version; the trees above are named by the +# C library, which is not the root, and an engine refuses a package requested +# both by path from inside the graph and by version ("Pick one"). Declared by +# the tests' own manifest, the root's declaration is the one used. +if [ -f tests/mcpp.toml ]; then + { + echo "" + echo "# Written by tools/branch-graph.sh: the stack's working trees." + echo "[dependencies.mcpplibs]" + echo "openkal = { path = \"../.spec\" }" + for backend in $(grep -oE '^openkal-[a-z]+ = \{ path = "\.\./\.openkal-[a-z]+"' .musl/mcpp.toml | cut -d' ' -f1); do + case "$backend" in + openkal-linux) cond='os = "linux"' ;; + openkal-macos) cond='os = "macos"' ;; + openkal-windows) cond='windows' ;; + *) continue ;; + esac + echo "" + echo "[target.'cfg($cond)'.dependencies.mcpplibs]" + echo "$backend = { path = \"../.$backend\" }" + done + } >> tests/mcpp.toml + echo "the tests' package names the same trees" +fi From 3ef247158347229a4a90eb681596ceee369d8844 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 10 Nov 2026 09:14:30 +0900 Subject: [PATCH 5/5] ci: the mtest pin in .github/versions.env, and one test-report action The mtest action reads MTEST_VERSION and MTEST_SHA256 from .github/versions.env, so no workflow states the pin. The two jobs that run the tests report through .github/actions/test-report (the stores mcpp test kept, taken into one store of the job, the job's summary, the artifact), as mcpp's CI does, and the report job writes the workflow run's group (mtest ci report --group). --- .github/actions/mtest/action.yml | 3 +- .github/actions/test-report/action.yml | 47 ++++++++++++++++++++++++++ .github/versions.env | 5 +++ .github/workflows/ci.yml | 43 +++-------------------- 4 files changed, 59 insertions(+), 39 deletions(-) create mode 100644 .github/actions/test-report/action.yml create mode 100644 .github/versions.env diff --git a/.github/actions/mtest/action.yml b/.github/actions/mtest/action.yml index 549468cd..d9f07da1 100644 --- a/.github/actions/mtest/action.yml +++ b/.github/actions/mtest/action.yml @@ -1,6 +1,6 @@ name: mtest description: > - The mtest engine the conformance suite reports to: the release the workflow pins (MTEST_VERSION), an + The mtest engine the conformance suite reports to: the release .github/versions.env pins (MTEST_VERSION), an xlings package (xim:mtest) that the index verifies, else the release bundle checked against MTEST_SHA256; MTEST names its launcher. A job that has no xlings yet gets the pinned one first (XLINGS_VERSION). runs: @@ -28,6 +28,7 @@ runs: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail + . .github/versions.env # MTEST_VERSION, MTEST_SHA256: no workflow states the pin # xlings by the configured mirror (GLOBAL), then by CN (GitCode first), each with a refreshed index # (a cached index can predate the release, and xlings keeps it when an anonymous refresh fails); else # the release bundle itself, checked against MTEST_SHA256: the web address, then the API's, which diff --git a/.github/actions/test-report/action.yml b/.github/actions/test-report/action.yml new file mode 100644 index 00000000..5dd66d2b --- /dev/null +++ b/.github/actions/test-report/action.yml @@ -0,0 +1,47 @@ +name: test-report +description: > + The runs `mcpp test` kept in the mtest store's form: taken into one store of + the job, written as one Markdown report of this workflow run's runs into the + job's summary, and kept as an artifact for the report over every job of the + workflow run (ci.yml `report`). +inputs: + name: + description: the artifact's name, unique in the workflow run + required: true + stores: + description: the stores `mcpp test` kept in this job, relative to the workspace, separated by spaces + required: false + default: tests/target/mtest +runs: + using: composite + steps: + - uses: ./.github/actions/mtest + - shell: bash + env: + STORES: ${{ inputs.stores }} + run: | + set -euo pipefail + # The POSIX spelling of the runner's directories for this shell; the + # artifact's path below is the runner's own. + temp="$(cygpath -u "$RUNNER_TEMP" 2> /dev/null || printf '%s' "$RUNNER_TEMP")" + workspace="$(cygpath -u "$GITHUB_WORKSPACE" 2> /dev/null || printf '%s' "$GITHUB_WORKSPACE")" + job="$temp/mtest-job" + mkdir -p "$job" + taken=0 + for store in $STORES; do + [ -f "$workspace/$store/index.ndjson" ] || continue + (cd "$job" && "$MTEST" store import --from "$workspace/$store") + taken=1 + done + if [ "$taken" = 0 ]; then echo "mcpp test kept no run here"; exit 0; fi + (cd "$job" && "$MTEST" report --format md --group "$GITHUB_RUN_ID") >> "$GITHUB_STEP_SUMMARY" \ + || echo "::warning::no run of this workflow run in the store" + echo "MTEST_JOB_STORE=$RUNNER_TEMP/mtest-job/.mtest" >> "$GITHUB_ENV" + - uses: actions/upload-artifact@v4 + if: env.MTEST_JOB_STORE != '' + with: + name: ${{ inputs.name }} + path: ${{ env.MTEST_JOB_STORE }} + include-hidden-files: true + if-no-files-found: ignore + retention-days: 7 diff --git a/.github/versions.env b/.github/versions.env new file mode 100644 index 00000000..f670c407 --- /dev/null +++ b/.github/versions.env @@ -0,0 +1,5 @@ +# The engines CI pins, read by the composite actions (.github/actions/mtest). +# mtest: an xlings package (xim:mtest) whose sha256 the index states; the release +# bundle's sha256 checks the fallback download. +MTEST_VERSION=0.5.2 +MTEST_SHA256=be443f8c546b3f4628dcd3f1eb6f45b4d698a7f805232f966349faf8250863c6 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dced9b06..3dc0c319 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,10 +35,6 @@ env: XLINGS_VERSION: v2026.10.26.1 # okrun is built from openkal-image, whose openkal-run states its own floor. OKRUN_MCPP_VERSION: 2026.11.9.1 - # The mtest engine this runtime's tests (tests/, on the mtest library) are - # reported with. - MTEST_VERSION: 0.5.2 - MTEST_SHA256: be443f8c546b3f4628dcd3f1eb6f45b4d698a7f805232f966349faf8250863c6 XLINGS_NON_INTERACTIVE: '1' jobs: @@ -151,25 +147,11 @@ jobs: cd tests && mcpp test 2>&1 | tee tests.log grep -q 'test result ok' tests.log - - uses: ./.github/actions/mtest - if: always() - - - name: The runs, reported - if: always() - run: | - [ -n "${MTEST:-}" ] && [ -f tests/target/mtest/index.ndjson ] || { echo "no run was kept"; exit 0; } - store="$RUNNER_TEMP/mtest-store"; mkdir -p "$store" - ( cd "$store" && "$MTEST" store import --from "$GITHUB_WORKSPACE/tests/target/mtest" ) - ( cd "$store" && "$MTEST" report --format md --group "$GITHUB_RUN_ID" ) >> "$GITHUB_STEP_SUMMARY" || true - - - uses: actions/upload-artifact@v4 + # The runs `mcpp test` kept, reported and kept for the report job. + - uses: ./.github/actions/test-report if: always() with: name: mtest-${{ github.job }}-${{ strategy.job-index }} - path: ${{ runner.temp }}/mtest-store/.mtest - include-hidden-files: true - if-no-files-found: ignore - retention-days: 7 # WHAT A FRONT END DOES WITH THE RUNTIME, TIMED AGAINST THE SYSTEM'S OWN # (0.17.0; the design's L2). bench/ built above this runtime, and with @@ -699,26 +681,11 @@ jobs: mcpp test --target '${{ matrix.native }}' 2>&1 | tee tests.log grep -q 'test result ok' tests.log - - uses: ./.github/actions/mtest - if: always() - - - name: The runs, reported - if: always() - run: | - [ -n "${MTEST:-}" ] && [ -f tests/target/mtest/index.ndjson ] || { echo "no run was kept"; exit 0; } - store="$(cygpath -u "$RUNNER_TEMP" 2> /dev/null || printf '%s' "$RUNNER_TEMP")/mtest-store"; mkdir -p "$store" - here="$PWD" - ( cd "$store" && "$MTEST" store import --from "$here/tests/target/mtest" ) - ( cd "$store" && "$MTEST" report --format md --group "$GITHUB_RUN_ID" ) >> "$GITHUB_STEP_SUMMARY" || true - - - uses: actions/upload-artifact@v4 + # The runs `mcpp test` kept, reported and kept for the report job. + - uses: ./.github/actions/test-report if: always() with: name: mtest-${{ github.job }}-${{ strategy.job-index }} - path: ${{ runner.temp }}/mtest-store/.mtest - include-hidden-files: true - if-no-files-found: ignore - retention-days: 7 # EVERY JOB'S RUNS AS ONE REPORT (`mtest ci report`, the workflow run's group). report: @@ -745,7 +712,7 @@ jobs: found=1 done if [ "$found" = 0 ]; then echo "no job kept a run" >> "$GITHUB_STEP_SUMMARY"; exit 0; fi - "$MTEST" ci report --out "$RUNNER_TEMP/tests.md" + "$MTEST" ci report --group "$GITHUB_RUN_ID" --out "$RUNNER_TEMP/tests.md" cat "$RUNNER_TEMP/tests.md" >> "$GITHUB_STEP_SUMMARY" # ── AN OK IMAGE (`-openkal`, openkal SPEC clause 12) ─────────────────