diff --git a/.github/actions/mtest/action.yml b/.github/actions/mtest/action.yml new file mode 100644 index 0000000..549468c --- /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 ae54213..eecade1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,8 +27,12 @@ on: default: "" env: MCPP_SOURCE_REF: ${{ github.event.inputs.mcpp_ref || vars.MCPP_SOURCE_REF }} - MCPP_VERSION: 2026.11.5.1 + MCPP_VERSION: 2026.11.9.1 XLINGS_VERSION: v2026.10.26.1 + # The mtest engine the runs are reported with: the conformance suite's cases + # (tools/run-conformance.sh with MTEST set) and `mcpp test`'s store (target/mtest). + MTEST_VERSION: 0.5.2 + MTEST_SHA256: be443f8c546b3f4628dcd3f1eb6f45b4d698a7f805232f966349faf8250863c6 XLINGS_NON_INTERACTIVE: '1' jobs: @@ -178,6 +182,8 @@ jobs: esac mcpp toolchain list + - uses: ./.github/actions/mtest + - name: Every interface, every kind of examination run: | bash .spec/tools/run-conformance.sh openkal-macos . full,optional @@ -249,6 +255,11 @@ jobs: test "$missing" -eq 0 done + - name: This package's test runs, kept for the report + if: always() + run: | + if [ -d target/mtest ]; then cp -R target/mtest "$RUNNER_TEMP/mcpp-test-runs"; fi + # WHAT THIS SYSTEM'S SANDBOX REFUSED, AND WHAT IT ENDED, when a test # failed: a confined program's refusals are the system's log's, and a # program it ended leaves a report; neither reaches the test's output. @@ -443,3 +454,64 @@ jobs: fi rm -f src/probe.cpp echo "a dependence upon a C library is visible to the check" + + # A FAILED RUN, EXPLAINED: through the engine the suite's own text is not in + # the log, so the failing cases are named here with what each observed. + - name: The failing cases, explained + if: failure() + run: | + [ -n "${MTEST:-}" ] && [ -d .spec/conformance/.mtest ] || exit 0 + ( cd .spec/conformance && "$MTEST" explain ) || true + + # THE RUNS OF THIS JOB, REPORTED: the conformance suite's (one case per + # interface, its observations within it) and `mcpp test`'s (one case per + # test program), taken into one store, written as the job's summary and kept + # for the report over every job. + - name: The runs, reported + if: always() + run: | + [ -n "${MTEST:-}" ] || { echo "no mtest engine in this job"; exit 0; } + store="$(cygpath -u "$RUNNER_TEMP" 2> /dev/null || printf '%s' "$RUNNER_TEMP")/mtest-store" + mkdir -p "$store"; here="$PWD" + for from in "$here/.spec/conformance/.mtest" "$RUNNER_TEMP/mcpp-test-runs"; do + if [ -f "$from/index.ndjson" ]; then ( cd "$store" && "$MTEST" store import --from "$from" ); fi + done + [ -f "$store/.mtest/index.ndjson" ] || { echo "no run was kept"; exit 0; } + ( cd "$store" && "$MTEST" report --format md --group "$GITHUB_RUN_ID" ) >> "$GITHUB_STEP_SUMMARY" + + - 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: conformance + 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" diff --git a/README.md b/README.md index ab40f71..0a68801 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,10 @@ written on the kernel's own calls. ```toml [dependencies] -openkal = "0.24.0" +openkal = "0.25.0" [target.'cfg(os = "macos")'.dependencies] -openkal-macos = "0.22.1" +openkal-macos = "0.23.0" ``` Its purpose is as much to test the specification as to be used. A specification @@ -183,6 +183,20 @@ same image parses as fast on Linux arm64. Since 0.17.1 it is the same kernel ide this system's library keeps for each thread (`pthread_threadid_np`, a name no C library in this ecosystem defines). +## What 0.23 adds (openkal 0.25) + +**A second name for a file** (`kal_fs_hard_link`) is `linkat` with no flags, so a +final link receives the name itself. `KAL_FS_PROP_HARD_LINKS` is claimed on APFS +and HFS+, which store several names for one file, and not on a network volume, +whose answer depends on its server. A directory is refused with +`kal_err_is_directory` before the call, because the kernel's own answer (`EPERM`) +names a permission; two volumes are refused by the kernel with `EXDEV`, which is +reported as `kal_err_not_supported`. + +**The requests to end are counted.** The handler adds one to the stop word for each +request, and one where the count would wrap to zero, in a single compare-and-exchange, +so two handlers that interrupt one another each count. + ## What 0.21 adds (openkal 0.23) **A region at an address.** `kal_exec_alloc_at` passes the address as a hint, diff --git a/mcpp.toml b/mcpp.toml index 4c74e1a..8c7e35a 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-macos" -version = "0.22.1" +version = "0.23.0" description = "An implementation of openkal for macOS, written on the kernel's own calls. Its purpose is as much to test the specification as to be used." license = "Apache-2.0" @@ -61,7 +61,7 @@ provides-interfaces = [ ] [dependencies] -openkal = "^0.24.0" +openkal = "0.25.0" [build] # The flags are attached to this package's own sources rather than to the whole @@ -113,3 +113,9 @@ default = [] # library defines --- which is the only name reachable from a program that # defines every ordinary one. standalone = { defines = ["OKM_STANDALONE"] } + +# The tests are written with the mtest library (mcpp-community/mtest): each file +# declares its cases, and a test that starts a copy of itself declares what the +# copy does as a `child'. +[dev-dependencies] +mcpplibs.mtest = "0.5.2" diff --git a/src/fs.cpp b/src/fs.cpp index 830f97c..f5bf561 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -229,7 +229,7 @@ kal_uintptr volume_props(int fd) { // Both implement `renameatx_np' with RENAME_EXCL (0.17), and both store a // socket bound to a name. if (named("apfs") || named("hfs")) - return local | KAL_FS_PROP_LINKS | KAL_FS_PROP_MAKE_LINKS + return local | KAL_FS_PROP_LINKS | KAL_FS_PROP_MAKE_LINKS | KAL_FS_PROP_HARD_LINKS | KAL_FS_PROP_EXECUTABLE | KAL_FS_PROP_RENAME_NO_REPLACE | KAL_FS_PROP_LOCAL_ENDPOINT; @@ -1060,6 +1060,35 @@ int kal_fs_link_create(kal_dir base, const char* name, kal_uintptr len, return kal_ok; } +// A SECOND NAME FOR A FILE (0.25) is `linkat' with no flags, which gives the +// node the name denotes another name and does not follow a final link. Claimed +// on the system's own formats (APFS, HFS+), which store several names for one +// file; a network volume's answer depends on its server and is not claimed. A +// directory is refused before the call, because the kernel refuses its second +// name with EPERM, which reads as a permission. Two volumes are refused by the +// kernel with EXDEV, which is reported as kal_err_not_supported: an operation +// the pair cannot perform. +int kal_fs_hard_link(kal_dir from, const char* a, kal_uintptr alen, + kal_dir to, const char* b, kal_uintptr blen) { + const int f = okm::unpack(from.h), t2 = okm::unpack(to.h); + if (f < 0 || t2 < 0 || !okm::acceptable(a, alen) || !okm::acceptable(b, blen)) return kal_err_invalid; + if ((volume_props(t2) & KAL_FS_PROP_HARD_LINKS) == 0) return kal_err_not_supported; + okm::terminated ta(a, alen), tb(b, blen); + if (!ta.ok || !tb.ok) return kal_err_invalid; + if (refused_write(t2)) return kal_err_permission; + okm::kstat64 st{}; + const okm_long s = okm::sys(okm::nr_fstatat64, f, reinterpret_cast(ta.buf), + reinterpret_cast(&st), okm::at_symlink_nofollow); + if (okm::failed(s)) return okm::translate(s); + if ((okm::stat_mode(st) & okm::s_ifmt) == okm::s_ifdir) return kal_err_is_directory; + const okm_long r = okm::sys(okm::nr_linkat, f, reinterpret_cast(ta.buf), + t2, reinterpret_cast(tb.buf), 0); + if (r == -okm::e_xdev) return kal_err_not_supported; + if (okm::failed(r)) return okm::translate(r); + wrote(t2, b, blen); + return kal_ok; +} + // THE NAME WITH EVERY LINK FOLLOWED (0.23): the node opened for events only // (O_EVTONLY, which needs no permission to read it and follows every link), and // the kernel's name for that descriptor taken (F_GETPATH), as this system's own diff --git a/src/process.cpp b/src/process.cpp index c437bc1..e6086f1 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -1230,7 +1230,17 @@ void stop_handler(int signo) { int none = 0; __atomic_compare_exchange_n(&g_stop_signal, &none, signo, false, __ATOMIC_RELAXED, __ATOMIC_RELAXED); - __atomic_store_n(&g_stop_word, 1u, __ATOMIC_RELEASE); + // EACH REQUEST IS COUNTED (openkal 0.25), and the count skips zero when it + // wraps, so the word is never zero again once a request has arrived. One + // exchange both counts and skips zero, so handlers of two signals that + // interrupt one another each count, at the wrap as everywhere else; an add + // followed by a fix-up could lose the second count between the two. + kal_u32 seen = __atomic_load_n(&g_stop_word, __ATOMIC_RELAXED), next; + do { + next = seen + 1u; + if (next == 0) next = 1u; + } while (!__atomic_compare_exchange_n(&g_stop_word, &seen, next, false, + __ATOMIC_RELEASE, __ATOMIC_RELAXED)); // Woken through the same operation `kal_task_wake' performs, issued as the // raw call because a handler may not enter code that takes a lock. Waking // ALL of them: any number of contexts may be waiting upon this one word, and diff --git a/src/sys.h b/src/sys.h index 8b79f12..e639e03 100644 --- a/src/sys.h +++ b/src/sys.h @@ -186,7 +186,7 @@ enum : okm_long { nr_getdirentries64 = 344, nr_bsdthread_terminate = 361, nr_thread_selfid = 372, nr_openat = 463, nr_renameat = 465, nr_faccessat = 466, - nr_fstatat64 = 470, nr_unlinkat = 472, nr_readlinkat = 473, + nr_fstatat64 = 470, nr_linkat = 471, nr_unlinkat = 472, nr_readlinkat = 473, nr_symlinkat = 474, nr_fstatfs64 = 346, nr_sysctl = 202, nr_mkdirat = 475, // The descriptor limit, read when the descriptors a process holds cannot be diff --git a/tests/conformance_env_time.cpp b/tests/conformance_env_time.cpp index 1aebe90..3b36e5d 100644 --- a/tests/conformance_env_time.cpp +++ b/tests/conformance_env_time.cpp @@ -3,17 +3,21 @@ // `_NSGetExecutablePath' and `realpath' are the system's own, declared without // its headers as tests/conformance_spawn.cpp declares them: this test starts // itself again by its name. +#include +#include import openkal.env; import openkal.time; import openkal.stream; import openkal.process; import openkal.fs; +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; extern "C" int _NSGetExecutablePath(char* buf, unsigned* size); extern "C" char* realpath(const char* path, char* resolved); namespace { -int failures = 0; // ASKED BEFORE THIS IMPLEMENTATION'S INITIALISER RAN (0.21.3). This format runs // initialisers in link order, and this test's objects precede the package's: a @@ -38,20 +42,7 @@ kal_u64 processor_ns() { return ok ? info.cpu_ns : ~0ull; } -void check(bool ok, const char* what) { - if (ok) return; - ++failures; - const char pre[] = "FAIL: "; - kal::write(kal::err(), pre, sizeof(pre) - 1); - kal_uintptr n = 0; while (what[n]) ++n; - kal::write(kal::err(), what, n); - kal::write(kal::err(), "\n", 1); -} - -bool is(const char* a, const char* b) { - while (*a && *a == *b) { ++a; ++b; } - return *a == *b; -} +void check(bool ok, const char* what) { expect(ok) << what; } // The granularity another start of this program states, or -1. kal_duration granularity_elsewhere() { @@ -62,12 +53,14 @@ kal_duration granularity_elsewhere() { kal_dir d{}; char n[8]; kal_uintptr l = 0; if (kal_fs_preopen(i, &d, n, sizeof n, &l) == kal_ok && l == 1 && n[0] == '/') root = d; } - const char* argv[2] = { "conformance_env_time", "--granularity" }; - const kal_uintptr lens[2] = { 20, 13 }; + const std::vector command = child_command("granularity"); + std::vector argv; + std::vector lens; + for (const auto& a : command) { argv.push_back(a.c_str()); lens.push_back(a.size()); } const kal_spawn how{ root, kal::fs::working(), nullptr, nullptr, 0, 0 }; kal_process p{}; kal_uintptr n = 0; while (self[1 + n]) ++n; - if (kal_process_spawn(&how, self + 1, n, argv, lens, 2, nullptr, nullptr, 0, nullptr, &p) != kal_ok) + if (kal_process_spawn(&how, self + 1, n, argv.data(), lens.data(), argv.size(), nullptr, nullptr, 0, nullptr, &p) != kal_ok) return ~0ull; int status = -1, terminated = -1; kal_process_wait(p, &status, &terminated); @@ -76,12 +69,13 @@ kal_duration granularity_elsewhere() { } } -int main(int argc, char** argv) { - // The other start of granularity_elsewhere: its granularity as its status. - if (argc > 1 && is(argv[1], "--granularity")) { - const kal_duration g = kal::time::granularity(); - return g < 250 ? static_cast(g) : 250; - } +// The other start of granularity_elsewhere: its granularity as its status. +child granularity{"granularity", [](std::vector) { + const kal_duration g = kal::time::granularity(); + return g < 250 ? static_cast(g) : 250; +}}; + +UTest conformance_env_time_test{"conformance_env_time", [] { // A program always receives the name it was started with, even where the // environment has none, in which case it is empty rather than absent. check(kal::env::arg_count() >= 1, "at least the program name is present"); @@ -134,5 +128,4 @@ int main(int argc, char** argv) { const char ok[] = "openkal-macos: env and time conformance\n"; kal::write(kal::out(), ok, sizeof(ok) - 1); - return failures == 0 ? 0 : 1; -} +}}; diff --git a/tests/conformance_fs.cpp b/tests/conformance_fs.cpp index f3f2b12..9ef3e47 100644 --- a/tests/conformance_fs.cpp +++ b/tests/conformance_fs.cpp @@ -8,21 +8,15 @@ import openkal.fs; import openkal.stream; import openkal.task; +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; namespace { -int failures = 0; -void check(bool ok, const char* what) { - if (ok) return; - ++failures; - const char pre[] = "FAIL: "; - kal::write(kal::err(), pre, sizeof(pre) - 1); - kal_uintptr n = 0; while (what[n]) ++n; - kal::write(kal::err(), what, n); - kal::write(kal::err(), "\n", 1); -} +void check(bool ok, const char* what) { expect(ok) << what; } } -int main() { +UTest conformance_fs_test{"conformance_fs", [] { check(kal::fs::preopen_count() >= 1, "the environment supplies at least one directory"); auto root = kal::fs::working(); check(root.h != 0, "the working directory is the first entry"); @@ -182,5 +176,4 @@ int main() { const char ok[] = "openkal-macos: file system conformance\n"; kal::write(kal::out(), ok, sizeof(ok) - 1); - return failures == 0 ? 0 : 1; -} +}}; diff --git a/tests/conformance_memory.cpp b/tests/conformance_memory.cpp index 82396a5..f9c6301 100644 --- a/tests/conformance_memory.cpp +++ b/tests/conformance_memory.cpp @@ -1,22 +1,22 @@ // Conformance: openkal.memory. import openkal.memory; import openkal.stream; +import mcpplibs.mtest; -int main() { - void* p = kal::alloc(128, 16); - if (p == nullptr) return 1; - if ((reinterpret_cast(p) % 16) != 0) return 1; +using namespace mcpplibs::mtest; +UTest conformance_memory_test{"conformance_memory", [] { + void* p = kal::alloc(128, 16); + if (p == nullptr) { fail("p == nullptr"); return; } + if ((reinterpret_cast(p) % 16) != 0) { fail("(reinterpret_cast(p) % 16) != 0"); return; } auto* b = static_cast(p); for (int i = 0; i < 128; ++i) b[i] = static_cast(i); - for (int i = 0; i < 128; ++i) if (b[i] != static_cast(i)) return 1; + for (int i = 0; i < 128; ++i) if (b[i] != static_cast(i)) { fail("each byte written is read back"); return; } kal::free(p, 128, 16); // A zero-sized request yields a null pointer rather than a region that // cannot be used. - if (kal::alloc(0, 8) != nullptr) return 1; - + if (kal::alloc(0, 8) != nullptr) { fail("kal::alloc(0, 8) != nullptr"); return; } const char ok[] = "openkal-macos: memory conformance\n"; kal::write(kal::out(), ok, sizeof(ok) - 1); - return 0; -} +}}; diff --git a/tests/conformance_process_task.cpp b/tests/conformance_process_task.cpp index cc4615c..9114f5b 100644 --- a/tests/conformance_process_task.cpp +++ b/tests/conformance_process_task.cpp @@ -4,9 +4,11 @@ import openkal.macros; import openkal.task; import openkal.fs; import openkal.stream; +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; namespace { -int failures = 0; void say(const char* s) { kal_uintptr n = 0; while (s[n]) ++n; @@ -21,15 +23,7 @@ void say_num(int v) { kal::write(kal::err(), b + i, static_cast(12 - i)); } -void check(bool ok, const char* what) { - if (ok) return; - ++failures; - const char pre[] = "FAIL: "; - kal::write(kal::err(), pre, sizeof(pre) - 1); - kal_uintptr n = 0; while (what[n]) ++n; - kal::write(kal::err(), what, n); - kal::write(kal::err(), "\n", 1); -} +void check(bool ok, const char* what) { expect(ok) << what; } // Shared between contexts. The word is what openkal.task suspends upon, and it // is ordinary memory: the interface adds no object of its own. @@ -46,7 +40,7 @@ void worker(void* arg) { } } -int main() { +UTest conformance_process_task_test{"conformance_process_task", [] { // --- openkal.process ----------------------------------------------------- // // The program to start is reached through a directory the environment @@ -378,5 +372,4 @@ int main() { const char ok[] = "openkal-macos: process and task conformance\n"; kal::write(kal::out(), ok, sizeof(ok) - 1); - return failures == 0 ? 0 : 1; -} +}}; diff --git a/tests/conformance_space_task.cpp b/tests/conformance_space_task.cpp index 1859df0..77b4099 100644 --- a/tests/conformance_space_task.cpp +++ b/tests/conformance_space_task.cpp @@ -14,18 +14,12 @@ import openkal.space; import openkal.process; import openkal.stream; import openkal.abort; +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; namespace { -int failures = 0; -void check(bool ok, const char* what) { - if (ok) return; - ++failures; - const char pre[] = "FAIL: "; - kal::write(kal::err(), pre, sizeof(pre) - 1); - kal_uintptr n = 0; while (what[n]) ++n; - kal::write(kal::err(), what, n); - kal::write(kal::err(), "\n", 1); -} +void check(bool ok, const char* what) { expect(ok) << what; } volatile int g_ran = 0; void ran(void*) { g_ran = 1; } @@ -43,7 +37,7 @@ void in_the_copy(void*) { alignas(16) unsigned char g_stack[64 * 1024]; } -int main() { +UTest conformance_space_task_test{"conformance_space_task", [] { for (int round = 0; round < 3; ++round) { kal_process p{}; const int rc = kal_space_start(in_the_copy, nullptr, g_stack + sizeof g_stack, &p); @@ -59,6 +53,4 @@ int main() { kal::write(kal::err(), line, sizeof line - 1); } } - if (failures == 0) kal::write(kal::out(), "openkal-macos: a copy of the space starts contexts\n", 51); - return failures == 0 ? 0 : 1; -} +}}; diff --git a/tests/conformance_spawn.cpp b/tests/conformance_spawn.cpp index 034a6d1..2d6e2bc 100644 --- a/tests/conformance_spawn.cpp +++ b/tests/conformance_spawn.cpp @@ -5,7 +5,7 @@ // the claim is about the started image and not about the caller. Two programs // are used. `/bin/sh' needs nothing to survive the start, so any descriptor // above 2 it holds was conveyed by the start. This test itself, started again -// with `--grant-child', reads its preopens through `kal_fs_preopen', which is +// as the child `grant', reads its preopens through `kal_fs_preopen', which is // how a granted directory is defined to arrive. // // The three names declared below are the system's own and are reached without @@ -13,10 +13,15 @@ // program on this system learns where it is, and `realpath' makes that a name // the root preopen can resolve. `dup2' places a descriptor WITHOUT the // close-on-exec flag, which is the descriptor a caller inherits and did not grant. +#include +#include import openkal.types; import openkal.fs; import openkal.stream; import openkal.process; +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; extern "C" int _NSGetExecutablePath(char* buf, unsigned* size); extern "C" char* realpath(const char* path, char* resolved); @@ -24,8 +29,6 @@ extern "C" int dup2(int from, int to); namespace { -int failures = 0; - kal_uintptr len(const char* s) { kal_uintptr n = 0; while (s[n]) ++n; return n; } bool same(const char* a, kal_uintptr n, const char* b) { @@ -36,11 +39,7 @@ bool same(const char* a, kal_uintptr n, const char* b) { void say(const char* s) { kal::write(kal::err(), s, len(s)); } -void check(bool ok, const char* what) { - if (ok) return; - ++failures; - say("FAIL: "); say(what); say("\n"); -} +void check(bool ok, const char* what) { expect(ok) << what; } const char kMarker[] = "okm-grant-marker"; @@ -65,24 +64,26 @@ bool write_marker(kal_dir d, const char* text) { // --- the started side -------------------------------------------------------- // -// argv: --grant-child { } +// arguments: { } // is the content of the marker file in that directory, or `@' // for a directory entry that must exist there. The status names the first // disagreement, so that a failure in the caller says which one it was. -int grant_child(char** argv) { +int grant_child(const std::vector& argv) { const kal_uintptr n = kal_fs_preopen_count(); - if (same(argv[2], len(argv[2]), "ambient")) { + if (argv.empty()) return 90; + if (argv[0] == "ambient") { kal_dir d{}; char nm[8]; kal_uintptr l = 0; if (n != 2) return 10; if (kal_fs_preopen(1, &d, nm, sizeof nm, &l) != kal_ok || l != 1 || nm[0] != '/') return 11; return 0; } kal_uintptr want = 0; - for (const char* c = argv[2]; *c; ++c) want = want * 10 + static_cast(*c - '0'); + for (const char* c = argv[0].c_str(); *c; ++c) want = want * 10 + static_cast(*c - '0'); + if (argv.size() != 1 + 2 * want) return 91; if (n != want) return 20; for (kal_uintptr i = 0; i < want; ++i) { - const char* name = argv[3 + 2 * i]; - const char* expect = argv[4 + 2 * i]; + const char* name = argv[1 + 2 * i].c_str(); + const char* expect = argv[2 + 2 * i].c_str(); kal_dir d{}; char nm[256]; kal_uintptr l = 0; if (kal_fs_preopen(i, &d, nm, sizeof nm, &l) != kal_ok) return 30 + static_cast(i); if (!same(nm, l, name)) return 40 + static_cast(i); @@ -104,6 +105,8 @@ int grant_child(char** argv) { return 0; } +child grant{"grant", [](std::vector args) { return grant_child(args); }}; + // --- the starting side ------------------------------------------------------- char g_self[1024]; @@ -113,15 +116,16 @@ char g_self[1024]; int run_child(const kal_preopen* grants, kal_uintptr count, const char* const* args, kal_uintptr nargs, const char** envp = nullptr, kal_uintptr envc = 0) { - const char* argv[16] = { "conformance_spawn", "--grant-child" }; - kal_uintptr lens[16] = { 17, 13 }; - for (kal_uintptr i = 0; i < nargs; ++i) { argv[2 + i] = args[i]; lens[2 + i] = len(args[i]); } + const std::vector command = child_command("grant", std::vector(args, args + nargs)); + std::vector argv; + std::vector lens; + for (const auto& x : command) { argv.push_back(x.c_str()); lens.push_back(x.size()); } kal_uintptr elens[4] = {}; for (kal_uintptr i = 0; i < envc; ++i) elens[i] = len(envp[i]); const kal_spawn how{ root(), working(), nullptr, grants, count, 0 }; kal_process p{}; const char* rel = g_self + 1; - if (kal_process_spawn(&how, rel, len(rel), argv, lens, 2 + nargs, + if (kal_process_spawn(&how, rel, len(rel), argv.data(), lens.data(), argv.size(), envp, elens, envc, nullptr, &p) != kal_ok) return -1; int status = -1, terminated = -1; @@ -132,9 +136,7 @@ int run_child(const kal_preopen* grants, kal_uintptr count, } // namespace -int main(int argc, char** argv) { - if (argc > 2 && same(argv[1], len(argv[1]), "--grant-child")) return grant_child(argv); - +UTest conformance_spawn_test{"conformance_spawn", [] { char raw[1024]; unsigned size = sizeof raw; const bool named = _NSGetExecutablePath(raw, &size) == 0 && realpath(raw, g_self) != nullptr; check(named && g_self[0] == '/' && g_self[1] != '\0', "the test can name its own program"); @@ -252,5 +254,4 @@ int main(int argc, char** argv) { kal_fs_remove(working(), db, len(db)); say("openkal-macos: what a started program receives\n"); - return failures == 0 ? 0 : 1; -} +}}; diff --git a/tests/conformance_stack_size.cpp b/tests/conformance_stack_size.cpp index ac6140f..0f7d924 100644 --- a/tests/conformance_stack_size.cpp +++ b/tests/conformance_stack_size.cpp @@ -21,6 +21,9 @@ import openkal.types; import openkal.task; import openkal.stream; +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; namespace okm { struct thread_attr { unsigned long long word[8]; }; @@ -28,16 +31,7 @@ void compose_thread_attr(thread_attr* a, __UINTPTR_TYPE__ stack, __UINTPTR_TYPE_ } namespace { -int failures = 0; -void check(bool ok, const char* what) { - if (ok) return; - ++failures; - const char pre[] = "FAIL: "; - kal::write(kal::err(), pre, sizeof(pre) - 1); - kal_uintptr n = 0; while (what[n]) ++n; - kal::write(kal::err(), what, n); - kal::write(kal::err(), "\n", 1); -} +void check(bool ok, const char* what) { expect(ok) << what; } long recurse(long n); long (*volatile g_recurse)(long) = recurse; @@ -79,7 +73,7 @@ void dump(const char* what, const void* p) { } } -int main() { +UTest conformance_stack_size_test{"conformance_stack_size", [] { static_assert(sizeof(pthread_attr_t) == sizeof(okm::thread_attr), "the structure is the system's size"); const unsigned long sizes[] = { 8ul << 20, 64ul << 20 }; for (unsigned long size : sizes) { @@ -127,6 +121,4 @@ int main() { kal_task never{}; check(kal_task_start_sized(room, nullptr, ~kal_uintptr{0} - 4095, &never) == kal_err_no_memory, "a stack larger than the address space is reported as exhaustion"); - if (failures == 0) kal::write(kal::out(), "openkal-macos: a least stack size is room\n", 42); - return failures == 0 ? 0 : 1; -} +}}; diff --git a/tests/conformance_stream.cpp b/tests/conformance_stream.cpp index 5bfe5d7..1022376 100644 --- a/tests/conformance_stream.cpp +++ b/tests/conformance_stream.cpp @@ -6,25 +6,17 @@ // omitted, and it is the half that keeps a capability claim from diverging from // the code behind it. import openkal.stream; +import mcpplibs.mtest; -namespace { +using namespace mcpplibs::mtest; -int failures = 0; +namespace { -void check(bool ok, const char* what) { - if (!ok) { - ++failures; - const char pre[] = "FAIL: "; - kal::write(kal::err(), pre, sizeof(pre) - 1); - kal_uintptr n = 0; while (what[n] != '\0') ++n; - kal::write(kal::err(), what, n); - kal::write(kal::err(), "\n", 1); - } -} +void check(bool ok, const char* what) { expect(ok) << what; } } // namespace -int main() { +UTest conformance_stream_test{"conformance_stream", [] { // The standard streams are distinct and usable. check(kal::in().h != kal::out().h, "stdin and stdout are distinct"); check(kal::out().h != kal::err().h, "stdout and stderr are distinct"); @@ -51,6 +43,4 @@ int main() { // program, by comparing the exported names of the implementation against // the specification's list. Version 0.2 defines no optional operation, so // there is nothing here for a compile-time assertion to check. - - return failures == 0 ? 0 : 1; -} +}}; diff --git a/tests/conformance_terminal.cpp b/tests/conformance_terminal.cpp index 454ccee..6af98e8 100644 --- a/tests/conformance_terminal.cpp +++ b/tests/conformance_terminal.cpp @@ -14,18 +14,12 @@ import openkal.types; import openkal.stream; import openkal.terminal; +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; namespace { -int failures = 0; -void check(bool ok, const char* what) { - if (ok) return; - ++failures; - const char pre[] = "FAIL: "; - kal::write(kal::err(), pre, sizeof(pre) - 1); - kal_uintptr n = 0; while (what[n]) ++n; - kal::write(kal::err(), what, n); - kal::write(kal::err(), "\n", 1); -} +void check(bool ok, const char* what) { expect(ok) << what; } constexpr kal_uintptr kRaw = kal::terminal::pass_control.bits; // line assembly and echo off constexpr tcflag_t kAlters = ISTRIP | INLCR | IGNCR | ICRNL; @@ -43,11 +37,10 @@ int deliver(int master, kal_stream s, const char* in, int n, unsigned char* out) } } -int main() { +UTest conformance_terminal_test{"conformance_terminal", [] { const int master = posix_openpt(O_RDWR | O_NOCTTY); if (master < 0 || grantpt(master) != 0 || unlockpt(master) != 0) { - kal::write(kal::err(), "no pseudo terminal; not observed\n", 33); - return 0; + skip("no pseudo terminal; not observed"); } const int slave = open(ptsname(master), O_RDWR | O_NOCTTY); check(slave >= 0, "the pseudo terminal opens"); @@ -101,6 +94,4 @@ int main() { "with line assembly on, the carriage return is part of the line"); close(slave); close(master); - if (failures == 0) kal::write(kal::out(), "openkal-macos: the terminal's alteration of input\n", 51); - return failures == 0 ? 0 : 1; -} +}}; diff --git a/tests/conformance_timeout.cpp b/tests/conformance_timeout.cpp index 0982784..df38393 100644 --- a/tests/conformance_timeout.cpp +++ b/tests/conformance_timeout.cpp @@ -12,10 +12,11 @@ import openkal.types; import openkal.stream; import openkal.process; import openkal.timeout; +import mcpplibs.mtest; -namespace { +using namespace mcpplibs::mtest; -int failures = 0; +namespace { void say(const char* s) { kal_uintptr n = 0; while (s[n]) ++n; @@ -29,11 +30,7 @@ void say_num(int v) { kal::write(kal::err(), b + i, static_cast(12 - i)); } -void check(bool ok, const char* what) { - if (ok) return; - ++failures; - say("FAIL: "); say(what); say("\n"); -} +void check(bool ok, const char* what) { expect(ok) << what; } // WHY SIXTEEN CHANNELS AND NOT ONE. Under the defect the wait was performed upon // descriptor N-1, and whether that expires depends on what occupies N-1. For a @@ -116,10 +113,7 @@ void the_rest_of_the_interface() { } // namespace -int main() { +UTest conformance_timeout_test{"conformance_timeout", [] { bounded_transfer_waits_on_the_stream(); the_rest_of_the_interface(); - - if (failures == 0) say("openkal-macos: timeout conformance\n"); - return failures == 0 ? 0 : 1; -} +}}; diff --git a/tests/foreign_image_not_program.cpp b/tests/foreign_image_not_program.cpp index 196832a..56fe98e 100644 --- a/tests/foreign_image_not_program.cpp +++ b/tests/foreign_image_not_program.cpp @@ -15,12 +15,12 @@ import openkal.types; import openkal.fs; import openkal.process; +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; namespace { -int failures = 0; -void check(bool held, const char* what) { - if (!held) { std::printf("FAIL: %s\n", what); ++failures; } -} +void check(bool held, const char* what) { expect(held) << what; } kal_dir root() { for (kal_uintptr i = 0; i < kal_fs_preopen_count(); ++i) { @@ -57,7 +57,7 @@ bool place(char* path, const unsigned char* bytes, std::size_t n) { } } // namespace -int main() { +UTest foreign_image_not_program_test{"foreign_image_not_program", [] { // mach_header_64: magic, a processor type no machine has, a subtype, MH_EXECUTE. const unsigned char foreign[32] = { 0xcf, 0xfa, 0xed, 0xfe, 0x45, 0x23, 0x01, 0x00, @@ -75,7 +75,4 @@ int main() { // A file with no `#!' and no image header: the kernel's ENOEXEC. check(e == kal_err_not_program, "a file that is no image is not a program"); ::unlink(plain); - - if (failures == 0) std::printf("openkal-macos: a file this machine cannot start is not a program\n"); - return failures == 0 ? 0 : 1; -} +}}; diff --git a/tests/openkal_0_22.cpp b/tests/openkal_0_22.cpp index ef53aac..0db58c5 100644 --- a/tests/openkal_0_22.cpp +++ b/tests/openkal_0_22.cpp @@ -7,6 +7,8 @@ // descriptor for it afterwards. #include #include +#include +#include #include #include #include @@ -14,12 +16,12 @@ import openkal.types; import openkal.env; import openkal.fs; import openkal.process; +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; namespace { -int failures = 0; -void check(bool held, const char* what) { - if (!held) { std::printf("FAIL: %s\n", what); ++failures; } -} +void check(bool held, const char* what) { expect(held) << what; } kal_dir root() { for (kal_uintptr i = 0; i < kal_fs_preopen_count(); ++i) { @@ -38,28 +40,33 @@ int descriptors() { char g_self[4096]; } // namespace -int main(int argc, char** argv) { - if (argc > 1 && std::strcmp(argv[1], "--null") == 0) { - struct stat in{}, err{}; - if (::fstat(0, &in) != 0 || !S_ISCHR(in.st_mode)) return 41; - if (::fstat(2, &err) != 0 || !S_ISCHR(err.st_mode)) return 42; - if (in.st_rdev != err.st_rdev) return 43; - char b[8]; - if (::read(0, b, sizeof b) != 0) return 44; - if (::write(2, "discarded", 9) != 9) return 45; - return 0; - } +// THE STARTED COPY (`null'): it finds one character device at standard input +// and standard error, reads the end of input there and writes to it. +child null_device{"null", [](std::vector) { + struct stat in{}, err{}; + if (::fstat(0, &in) != 0 || !S_ISCHR(in.st_mode)) return 41; + if (::fstat(2, &err) != 0 || !S_ISCHR(err.st_mode)) return 42; + if (in.st_rdev != err.st_rdev) return 43; + char b[8]; + if (::read(0, b, sizeof b) != 0) return 44; + if (::write(2, "discarded", 9) != 9) return 45; + return 0; +}}; + +UTest openkal_0_22_test{"openkal_0_22", [] { const kal_intptr n = kal_env_image(g_self, sizeof g_self - 1); - if (n <= 0 || g_self[0] != '/') { std::printf("FAIL: the program's own image is named\n"); return 1; } + if (n <= 0 || g_self[0] != '/') { fail("the program's own image is named"); return; } g_self[n] = 0; const kal_dir r = root(); - const char* args[] = { g_self, "--null" }; - const kal_uintptr lens[] = { std::strlen(g_self), 6 }; + const std::vector command = child_command("null"); + std::vector args; + std::vector lens; + for (const auto& a : command) { args.push_back(a.c_str()); lens.push_back(a.size()); } const kal_spawn how{ r, r, nullptr, nullptr, 0, (kal::process::null_in_flag | kal::process::null_err_flag).bits }; const int before = descriptors(); kal_process p{}; - const int e = kal_process_spawn(&how, g_self + 1, std::strlen(g_self + 1), args, lens, 2, + const int e = kal_process_spawn(&how, g_self + 1, std::strlen(g_self + 1), args.data(), lens.data(), args.size(), nullptr, nullptr, 0, nullptr, &p); check(e == kal_ok, "a program is started with the null device at two positions"); check(descriptors() == before, "and the caller holds no descriptor for the device"); @@ -68,9 +75,7 @@ int main(int argc, char** argv) { kal_process_wait(p, &status, &terminated); kal_process_close(p); } - check(terminated == 0 && status == 0, - "the started program finds one character device there, reads its end and writes to it"); - if (status > 0) std::printf(" the started program reported %d\n", status); - if (failures == 0) std::printf("openkal_0_22: ok\n"); - return failures == 0 ? 0 : 1; -} + expect(terminated == 0 && status == 0) + << "the started program finds one character device there, reads its end and writes to it (reported " + << status << ")"; +}}; diff --git a/tests/openkal_0_23.cpp b/tests/openkal_0_23.cpp index 3c6894b..11e6f80 100644 --- a/tests/openkal_0_23.cpp +++ b/tests/openkal_0_23.cpp @@ -7,6 +7,8 @@ // reaches the started program. #include #include +#include +#include #include #include #include @@ -17,12 +19,12 @@ import openkal.env; import openkal.fs; import openkal.process; import openkal.abort; +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; namespace { -int failures = 0; -void check(bool held, const char* what) { - if (!held) { std::printf("FAIL: %s\n", what); ++failures; } -} +void check(bool held, const char* what) { expect(held) << what; } kal_dir root() { for (kal_uintptr i = 0; i < kal_fs_preopen_count(); ++i) { @@ -38,12 +40,14 @@ int g_ended_by = 0; int start(const char* errand, const kal_preopen* grants, kal_uintptr n, kal_uintptr flags) { const kal_dir r = root(); - const char* args[] = { g_self, errand }; - const kal_uintptr lens[] = { std::strlen(g_self), std::strlen(errand) }; + const std::vector command = child_command(errand); + std::vector args; + std::vector lens; + for (const auto& a : command) { args.push_back(a.c_str()); lens.push_back(a.size()); } const kal_spawn how{ r, r, nullptr, grants, n, flags }; kal_process p{}; - if (kal_process_spawn(&how, g_self + 1, std::strlen(g_self + 1), args, lens, 2, nullptr, nullptr, 0, - nullptr, &p) != kal_ok) + if (kal_process_spawn(&how, g_self + 1, std::strlen(g_self + 1), args.data(), lens.data(), args.size(), nullptr, + nullptr, 0, nullptr, &p) != kal_ok) return -1; int status = -1, terminated = 0; kal_process_wait(p, &status, &terminated); @@ -53,34 +57,36 @@ int start(const char* errand, const kal_preopen* grants, kal_uintptr n, kal_uint } } // namespace -int main(int argc, char** argv) { - if (argc > 1 && std::strcmp(argv[1], "--read") == 0) { - // The first preopen is the recorded grant: read a file, ask for an - // absent one, and end through kal_exit so the record is ended. - kal_dir d{}; kal_uintptr l = 0; - if (kal_fs_preopen(0, &d, nullptr, 0, &l) != kal_ok) kal_exit(51); - kal_file f{}; - if (kal_fs_open(d, "present.txt", 11, kal::fs::open::read.bits, &f) != kal_ok) kal_exit(52); - kal_fs_close_file(f); - kal_node_info i{}; i.self_size = sizeof i; - kal_fs_info(d, "absent.txt", 10, 0, kal::fs::field::kind, &i); - kal_exit(0); - } - if (argc > 1 && std::strcmp(argv[1], "--write-raw") == 0) { - // Past openkal: this system's own library is asked to create a file in - // the read-only grant, and to read a file not granted at all. - const int a = ::open("/tmp/okm-0.21-raw.tmp", O_CREAT | O_WRONLY, 0600); - if (a >= 0) { ::unlink("/tmp/okm-0.21-raw.tmp"); return 61; } - const int b = ::open("/etc/hosts", O_RDONLY); - if (b >= 0) return 62; - return 60; - } - if (argc > 1 && std::strcmp(argv[1], "--resources") == 0) return 70 + kal_env_resources(); +// The first preopen is the recorded grant: read a file, ask for an absent one, +// and end through kal_exit so the record is ended. +child read_granted{"read", [](std::vector) -> int { + kal_dir d{}; kal_uintptr l = 0; + if (kal_fs_preopen(0, &d, nullptr, 0, &l) != kal_ok) kal_exit(51); + kal_file f{}; + if (kal_fs_open(d, "present.txt", 11, kal::fs::open::read.bits, &f) != kal_ok) kal_exit(52); + kal_fs_close_file(f); + kal_node_info i{}; i.self_size = sizeof i; + kal_fs_info(d, "absent.txt", 10, 0, kal::fs::field::kind, &i); + kal_exit(0); +}}; +// Past openkal: this system's own library is asked to create a file in the +// read-only grant, and to read a file not granted at all. +child write_raw{"write-raw", [](std::vector) { + const int a = ::open("/tmp/okm-0.21-raw.tmp", O_CREAT | O_WRONLY, 0600); + if (a >= 0) { ::unlink("/tmp/okm-0.21-raw.tmp"); return 61; } + const int b = ::open("/etc/hosts", O_RDONLY); + if (b >= 0) return 62; + return 60; +}}; + +child resources{"resources", [](std::vector) { return 70 + kal_env_resources(); }}; + +UTest openkal_0_23_test{"openkal_0_23", [] { const kal_intptr n = kal_env_image(g_self, sizeof g_self - 1); - if (n <= 0 || g_self[0] != '/') { std::printf("FAIL: the program's own image is named\n"); return 1; } + if (n <= 0 || g_self[0] != '/') { fail("the program's own image is named"); return; } g_self[n] = 0; - if (!mkdtemp(g_dir)) { std::printf("FAIL: a directory for the test\n"); return 1; } + if (!mkdtemp(g_dir)) { fail("a directory for the test"); return; } char path[256]; std::snprintf(path, sizeof path, "%s/present.txt", g_dir); { const int fd = ::open(path, O_CREAT | O_WRONLY, 0600); (void)!::write(fd, "x", 1); ::close(fd); } @@ -98,7 +104,7 @@ int main(int argc, char** argv) { kal_dir rd{}; check(kal_fs_dir_attenuate(dir, &rights, &rd) == kal_ok, "the directory is attenuated, recorded"); const kal_preopen grant{ rd, "/granted", 8 }; - check(start("--read", &grant, 1, 0) == 0, "a program granted the recorded handle reads through it"); + check(start("read", &grant, 1, 0) == 0, "a program granted the recorded handle reads through it"); kal_fs_close_dir(rd); kal_fs_close_file(rec); char buf[4096] = {}; @@ -157,7 +163,7 @@ int main(int argc, char** argv) { grants[count] = kal_preopen{ a, names[count], std::strlen(names[count]) }; ++count; } - const int s = start("--write-raw", grants, count, kal::process::confine_flag.bits); + const int s = start("write-raw", grants, count, kal::process::confine_flag.bits); check(s == 60, "a confined program is refused by the kernel what its grants do not allow, without openkal"); if (s != 60) { std::printf(" the confined program reported %d (ended by %d), granted:\n", s, g_ended_by); @@ -192,7 +198,7 @@ int main(int argc, char** argv) { ++full; } check(full == 16 && std::strlen(names[15]) > 950, "sixteen grants, the last named by more than 950 bytes"); - const int r = start("--resources", grants, full, + const int r = start("resources", grants, full, (kal::process::confine_flag | kal::process::economy_flag).bits); check(r == 72, "a confined start with sixteen long grants runs"); if (r != 72) std::printf(" the start reported %d (ended by %d)\n", r, g_ended_by); @@ -201,14 +207,12 @@ int main(int argc, char** argv) { } // A preference reaches the started program. - check(start("--resources", nullptr, 0, kal::process::economy_flag.bits) == 72, + check(start("resources", nullptr, 0, kal::process::economy_flag.bits) == 72, "a program started for economy reads economy"); - check(start("--resources", nullptr, 0, kal::process::performance_flag.bits) == 71, + check(start("resources", nullptr, 0, kal::process::performance_flag.bits) == 71, "and one started for performance reads performance"); std::snprintf(path, sizeof path, "%s/present.txt", g_dir); ::unlink(path); std::snprintf(path, sizeof path, "%s/record", g_dir); ::unlink(path); ::rmdir(g_dir); - if (failures == 0) std::printf("openkal_0_23: ok\n"); - return failures == 0 ? 0 : 1; -} +}}; diff --git a/tests/sigpipe_not_passed_on.cpp b/tests/sigpipe_not_passed_on.cpp index 8b83a82..b81eece 100644 --- a/tests/sigpipe_not_passed_on.cpp +++ b/tests/sigpipe_not_passed_on.cpp @@ -12,22 +12,22 @@ #include #include #include +#include +#include #include #include import openkal.types; import openkal.env; import openkal.fs; import openkal.process; +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; namespace { struct sigaction g_started{}; [[gnu::constructor(100)]] void observe() { ::sigaction(SIGPIPE, nullptr, &g_started); } -int failures = 0; -void check(bool held, const char* what) { - if (!held) { std::printf("FAIL: %s\n", what); ++failures; } -} - kal_dir root() { for (kal_uintptr i = 0; i < kal_fs_preopen_count(); ++i) { kal_dir d{}; char n[8]; kal_uintptr l = 0; @@ -36,58 +36,74 @@ kal_dir root() { return kal_dir{}; } -char g_self[4096]; +// This program's own image, as the environment named it. +std::string self_image() { + char self[4096]; + const kal_intptr n = kal_env_image(self, sizeof self - 1); + if (n <= 0 || self[0] != '/') return {}; + return std::string(self, static_cast(n)); +} -// This program again, through openkal, asked how it was started: 7 ignored, -// 0 default. +// This program again, as the child `report', through openkal, asked how it was +// started: 7 ignored, 0 default, -1 not started. int started_child_reports() { + const std::string self = self_image(); + if (self.empty()) return -1; + const std::vector command = child_command("report"); + std::vector argv; + std::vector lens; + for (const auto& a : command) { argv.push_back(a.c_str()); lens.push_back(a.size()); } const kal_dir r = root(); - const char* argv[] = { g_self, "--report" }; - const kal_uintptr lens[] = { std::strlen(g_self), 8 }; const kal_spawn how{ r, r, nullptr, nullptr, 0, 0 }; kal_process p{}; - const int e = kal_process_spawn(&how, g_self + 1, std::strlen(g_self + 1), argv, lens, 2, - nullptr, nullptr, 0, nullptr, &p); + const int e = kal_process_spawn(&how, self.c_str() + 1, self.size() - 1, argv.data(), lens.data(), + argv.size(), nullptr, nullptr, 0, nullptr, &p); int status = -1, terminated = 0; if (e == kal_ok) kal_process_wait(p, &status, &terminated); return e == kal_ok && terminated == 0 ? status : -1; } -} // namespace -int main(int argc, char** argv) { - if (argc > 1 && std::strcmp(argv[1], "--report") == 0) - return g_started.sa_handler == SIG_IGN ? 7 : 0; - const kal_intptr n = kal_env_image(g_self, sizeof g_self - 1); - if (n <= 0 || g_self[0] != '/') { std::printf("FAIL: the program's own image is named\n"); return 1; } - g_self[n] = 0; - if (argc > 1 && std::strcmp(argv[1], "--started-default") == 0) { - struct sigaction now{}; - ::sigaction(SIGPIPE, nullptr, &now); - if (now.sa_handler != SIG_IGN) return 2; // this program's own writes are not ended by it - return started_child_reports() == 0 ? 0 : 1; +// This program again, as the child `mode', started with SIGPIPE as +// `disposition' says: 0 when the program it then starts through openkal +// reported what was expected. +int started_with(void (*disposition)(int), const char* mode) { + const std::string self = self_image(); + const std::vector command = child_command(mode); + std::vector argv; + for (const auto& a : command) argv.push_back(const_cast(a.c_str())); + argv.push_back(nullptr); + const pid_t child = ::fork(); + if (child == 0) { + ::signal(SIGPIPE, disposition); + ::execv(self.c_str(), argv.data()); + ::_exit(93); } - if (argc > 1 && std::strcmp(argv[1], "--started-ignoring") == 0) - return started_child_reports() == 7 ? 0 : 1; + int st = 0; + ::waitpid(child, &st, 0); + return WIFEXITED(st) ? WEXITSTATUS(st) : 128 + WTERMSIG(st); +} +} // namespace - // This program again, started with SIGPIPE as `disposition' says: 0 when - // the program it then starts through openkal reported what was expected. - auto started_with = [](void (*disposition)(int), const char* mode) { - const pid_t child = ::fork(); - if (child == 0) { - ::signal(SIGPIPE, disposition); - ::execl(g_self, g_self, mode, nullptr); - ::_exit(93); - } - int st = 0; - ::waitpid(child, &st, 0); - return WIFEXITED(st) ? WEXITSTATUS(st) : 128 + WTERMSIG(st); - }; - const int d = started_with(SIG_DFL, "--started-default"); - check(d != 2, "this program's own writes are not ended by SIGPIPE"); - check(d == 0, "a program started with SIGPIPE at its default starts others with it at its default"); - check(started_with(SIG_IGN, "--started-ignoring") == 0, - "a program started with SIGPIPE ignored passes the ignore on, as it was given"); +child report{"report", [](std::vector) { + return g_started.sa_handler == SIG_IGN ? 7 : 0; +}}; - if (failures == 0) std::printf("openkal-macos: SIGPIPE ignored here, not passed on\n"); - return failures == 0 ? 0 : 1; -} +child started_default{"started-default", [](std::vector) { + struct sigaction now{}; + ::sigaction(SIGPIPE, nullptr, &now); + if (now.sa_handler != SIG_IGN) return 2; // this program's own writes are not ended by it + return started_child_reports() == 0 ? 0 : 1; +}}; + +child started_ignoring{"started-ignoring", [](std::vector) { + return started_child_reports() == 7 ? 0 : 1; +}}; + +UTest sigpipe_not_passed_on_test{"sigpipe_not_passed_on", [] { + if (self_image().empty()) { fail("the program's own image is named"); return; } + const int d = started_with(SIG_DFL, "started-default"); + expect(ne(d, 2)) << "this program's own writes are not ended by SIGPIPE"; + expect(eq(d, 0)) << "a program started with SIGPIPE at its default starts others with it at its default"; + expect(eq(started_with(SIG_IGN, "started-ignoring"), 0)) + << "a program started with SIGPIPE ignored passes the ignore on, as it was given"; +}}; diff --git a/tests/stop_word_and_sigpipe.cpp b/tests/stop_word_and_sigpipe.cpp index c26151f..51d6a46 100644 --- a/tests/stop_word_and_sigpipe.cpp +++ b/tests/stop_word_and_sigpipe.cpp @@ -8,39 +8,62 @@ #include #include #include +#include +#include #include #include import openkal.types; import openkal.env; import openkal.stream; import openkal.process; +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; namespace { -int failures = 0; -void check(bool held, const char* what) { - if (!held) { std::printf("FAIL: %s\n", what); ++failures; } +void check(bool held, const char* what) { expect(held) << what; } + +#if defined(__aarch64__) +// Starts this program again, the image `self', as the child `name' and answers +// its wait status. +int run_copy(const char* self, const char* name) { + const std::vector command = child_command(name); + std::vector argv; + for (const auto& a : command) argv.push_back(const_cast(a.c_str())); + argv.push_back(nullptr); + const pid_t copy = ::fork(); + if (copy == 0) { + ::execv(self, argv.data()); + ::_exit(93); + } + int st = 0; + ::waitpid(copy, &st, 0); + return st; } +#endif } -int main(int argc, char** argv) { - if (argc > 1 && std::strcmp(argv[1], "--hang-up") == 0) { - const kal_u32* word = kal_process_stop_requested(); - if (word == nullptr) return 90; - ::kill(::getpid(), SIGHUP); - for (int i = 0; i < 1000 && *word == 0; ++i) ::usleep(1000); - if (*word == 0) return 91; - kal_process_end_as_requested(); - return 92; - } - if (argc > 1 && std::strcmp(argv[1], "--end-as-requested") == 0) { - const kal_u32* word = kal_process_stop_requested(); - if (word == nullptr) return 90; - ::kill(::getpid(), SIGTERM); - for (int i = 0; i < 1000 && *word == 0; ++i) ::usleep(1000); - if (*word == 0) return 91; - kal_process_end_as_requested(); - return 92; - } +child hang_up{"hang-up", [](std::vector) { + const kal_u32* word = kal_process_stop_requested(); + if (word == nullptr) return 90; + ::kill(::getpid(), SIGHUP); + for (int i = 0; i < 1000 && *word == 0; ++i) ::usleep(1000); + if (*word == 0) return 91; + kal_process_end_as_requested(); + return 92; +}}; + +child end_as_requested{"end-as-requested", [](std::vector) { + const kal_u32* word = kal_process_stop_requested(); + if (word == nullptr) return 90; + ::kill(::getpid(), SIGTERM); + for (int i = 0; i < 1000 && *word == 0; ++i) ::usleep(1000); + if (*word == 0) return 91; + kal_process_end_as_requested(); + return 92; +}}; + +UTest stop_word_and_sigpipe_test{"stop_word_and_sigpipe", [] { { kal_stream mine{}, theirs{}; @@ -57,27 +80,14 @@ int main(int argc, char** argv) { char self[4096] = {}; const kal_intptr n = kal_env_image(self, sizeof self - 1); check(n > 0, "the program's own image is named"); - const pid_t child = ::fork(); - if (child == 0) { - ::execl(self, argv[0], "--end-as-requested", nullptr); - ::_exit(93); - } - int st = 0; - ::waitpid(child, &st, 0); + int st = run_copy(self, "end-as-requested"); check(WIFSIGNALED(st) && WTERMSIG(st) == SIGTERM, "a program that ends as requested is reported to its starter as ended by the request"); - const pid_t hung = ::fork(); - if (hung == 0) { - ::execl(self, argv[0], "--hang-up", nullptr); - ::_exit(93); - } - st = 0; - ::waitpid(hung, &st, 0); + st = run_copy(self, "hang-up"); check(WIFSIGNALED(st) && WTERMSIG(st) == SIGHUP, "a hang-up is taken over as a request, and the program ends as it would have (0.15.1)"); #else - (void)word; (void)argv; + (void)word; #endif - if (failures == 0) std::printf("openkal-macos: the stop word given back, and a closed stream reported\n"); - return failures == 0 ? 0 : 1; -} + std::printf("openkal-macos: the stop word given back, and a closed stream reported\n"); +}}; diff --git a/tests/stop_word_ignored.cpp b/tests/stop_word_ignored.cpp index 9b00af3..fa763d4 100644 --- a/tests/stop_word_ignored.cpp +++ b/tests/stop_word_ignored.cpp @@ -5,19 +5,21 @@ #include import openkal.types; import openkal.process; +import mcpplibs.mtest; -int main() { +using namespace mcpplibs::mtest; + +UTest stop_word_ignored_test{"stop_word_ignored", [] { #if defined(__aarch64__) std::signal(SIGINT, SIG_IGN); const kal_u32* word = kal_process_stop_requested(); - if (word == nullptr) { std::printf("FAIL: the stop word is armed\n"); return 1; } + if (word == nullptr) { fail("the stop word is armed"); return; } ::kill(::getpid(), SIGINT); ::usleep(20000); - if (*word != 0) { std::printf("FAIL: an ignored request was delivered\n"); return 1; } + if (*word != 0) { fail("an ignored request was delivered"); return; } ::kill(::getpid(), SIGTERM); for (int i = 0; i < 1000 && *word == 0; ++i) ::usleep(1000); - if (*word == 0) { std::printf("FAIL: a request after arming sets the word\n"); return 1; } + if (*word == 0) { fail("a request after arming sets the word"); return; } #endif std::printf("openkal-macos: an ignored request stays ignored\n"); - return 0; -} +}};