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 e513301..b9b06da 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,8 +26,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: @@ -245,6 +249,8 @@ jobs: esac mcpp toolchain list + - uses: ./.github/actions/mtest + - name: Every interface, every kind of examination run: | extra='' @@ -289,6 +295,9 @@ jobs: run: | extra='' [ -n '${{ matrix.target }}' ] && extra='--target ${{ matrix.target }}' + # The gcc/gnu-CRT leg tests under `gnu-odr' (mcpp.toml says why): the + # dev profile, with the link option a GCC defect on PE needs. + case '${{ matrix.toolchain }}' in gcc@*) extra="$extra --profile gnu-odr" ;; esac mcpp test $extra 2>&1 | tee tests.log missing=0 for f in tests/*.cpp; do @@ -298,6 +307,11 @@ jobs: test "$missing" -eq 0 git checkout -- mcpp.toml + - 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 + # --------------------------------------------------------------------------- # From a system that is not this one. # @@ -307,6 +321,39 @@ jobs: # that read this environment's object format and are not on it, and a package # that only builds on the system it targets is a package a contributor on any # other system cannot work on. + + # 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 cross: name: cross build, run under wine, and examine the objects runs-on: ubuntu-24.04 @@ -534,3 +581,31 @@ jobs: fi rm -f src/probe.cpp echo "a dependence upon a C runtime is visible to the check" + + # --------------------------------------------------------------------------- + # 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 de96f34..75e0ca7 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@ An implementation of [openkal](https://github.com/mcpplibs/openkal) for Windows. ```toml [dependencies] -openkal = "0.24.0" +openkal = "0.25.0" [target.'cfg(windows)'.dependencies] -openkal-windows = "0.20.0" +openkal-windows = "0.21.0" ``` Its purpose is as much to test the specification as to be used. openkal was @@ -15,6 +15,20 @@ drawn against systems that share an ancestry; a third implementation on a system that shares none of it is what turns "this interface is not Unix's shape" from a claim into an observation. +## What 0.21 adds (openkal 0.25) + +**A second name for a file** (`kal_fs_hard_link`) is the link record of +`NtSetInformationFile`, which has the rename record's layout; the file is opened by +its name without following a final link, as `CreateHardLinkW` opens it. +`KAL_FS_PROP_HARD_LINKS` is claimed where the volume reports +`FILE_SUPPORTS_HARD_LINKS`. A directory is refused with `kal_err_is_directory` +before the call, and a name on another volume (`STATUS_NOT_SAME_DEVICE`) with +`kal_err_not_supported`. + +**The requests to end are counted.** The console control routine adds one to the +stop word for each event, atomically because events may overlap, and skips zero +when the count wraps. + ## Observable process units The process interface provides the OpenKal 0.24 declared-unit operations. diff --git a/mcpp.toml b/mcpp.toml index 300dc63..d92a959 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-windows" -version = "0.20.0" +version = "0.21.0" description = "An implementation of openkal for Windows, written on the Win32 interfaces and the object manager beneath them, using no C runtime symbol." license = "Apache-2.0" @@ -66,7 +66,7 @@ provides-interfaces = [ ] [dependencies] -openkal = "0.24.0" +openkal = "0.25.0" # The package contributes definitions and no modules. The interface it # implements is declared by the specification package, which this package @@ -125,7 +125,14 @@ ldflags = ["-lntdll", "-lsynchronization", "-lshell32", "-lkernel32", "-lbcrypt" # built with `--release` came out shortened by a different amount. The dev # profile does not optimize and never showed it, which is also why the check # below now builds both profiles. -cxxflags = ["-fno-exceptions", "-fno-rtti", "-fno-builtin"] +# +# The flags are attached to this package's own sources. The tests are +# consumers: they import the test library, which imports `std`, and the +# prebuilt `std` interface is compiled with exceptions and run-time type +# information, which a unit compiled without them cannot import. +flags = [ + { glob = "src/**", cxxflags = ["-fno-exceptions", "-fno-rtti", "-fno-builtin"] }, +] # The other ABI names them in the sources instead --- src/win.cpp for the four # above and src/random.cpp for bcrypt --- where its compilers record @@ -146,3 +153,22 @@ default = [] # the loader, and this implementation adds nothing. Where it does not, the entry # point is this implementation's, and the link line names it. standalone = { defines = ["OKW_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" + +# THE TESTS ON THE GNU ABI WITH GCC, which CI runs under this profile. GCC on PE +# emits the function-local static `std::_Sp_make_shared_tag::_S_ti()::__tag' of +# an imported std as a definition that is not COMDAT, once in every object that +# imports std --- the test library's and mcpplibs.os's --- and once in +# libstdc++.a, so the link of every test is refused as a multiple definition. +# The definitions are one entity, and this profile's link accepts the repeats. +# A profile is the root's own, so the option never reaches a consumer's link. +# Otherwise it is the dev profile, so the leg tests what dev tests. +[profile.gnu-odr] +opt = 0 +debug = true +ldflags = ["-Wl,--allow-multiple-definition"] diff --git a/src/fs.cpp b/src/fs.cpp index 6e6b722..06cdeb7 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -1267,6 +1267,62 @@ int kal_fs_rename(kal_dir from, const char* a, kal_uintptr alen, return rename_with(from, a, alen, to, b, blen, true); } +// A SECOND NAME FOR A FILE (0.25) is the link record, which has the rename +// record's layout: the file is opened by its name without following a final +// link, as `CreateHardLinkW' opens it, and given `b' beside `to'. A directory is +// answered before the call, because the object manager's refusal names a +// different condition; a name on another volume is STATUS_NOT_SAME_DEVICE, +// which this interface states as 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) { + void* source_root = dir_handle(from); + void* target_root = dir_handle(to); + if (!source_root || !target_root) return kal_err_invalid; + if (!okw::acceptable(a, alen) || !okw::acceptable(b, blen)) return kal_err_invalid; + if ((kal_fs_props(to) & KAL_FS_PROP_HARD_LINKS) == 0) return kal_err_not_supported; + if (refused_write(target_root)) return kal_err_permission; + + void* h = nullptr; + const long r = open_relative(source_root, a, alen, FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES | SYNCHRONIZE, + okw::file_open, okw::file_open_reparse_point | okw::file_open_for_backup_intent, &h); + if (!okw::ok(r)) return okw::translate_nt(r); + { + okw::file_basic_information basic{}; + okw::io_status_block s{}; + if (okw::ok(okw::NtQueryInformationFile(h, &s, &basic, sizeof basic, okw::file_basic_information_class)) + && (basic.attributes & FILE_ATTRIBUTE_DIRECTORY) != 0 + && (basic.attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) { + okw::NtClose(h); + return kal_err_is_directory; + } + } + + okw::wide_name w(b, blen); + if (!w.ok) { okw::NtClose(h); return kal_err_invalid; } + const kal_uintptr storage_bytes = sizeof(okw::file_rename_information) + + okw::kMaxName * sizeof(wchar_t); + auto* info = static_cast( + kal_alloc(storage_bytes, alignof(okw::file_rename_information))); + if (!info) { okw::NtClose(h); return kal_err_no_memory; } + info->replace_if_exists = 0; + info->root_directory = target_root; + info->file_name_length = w.string.length; + for (unsigned i = 0; i < w.string.length / 2; ++i) info->file_name[i] = w.buffer[i]; + + okw::io_status_block s{}; + const long n = okw::NtSetInformationFile( + h, &s, info, static_cast(sizeof(okw::file_rename_information) + + w.string.length), + okw::file_link_information_class); + okw::NtClose(h); + kal_free(info, storage_bytes, alignof(okw::file_rename_information)); + if (n == static_cast(0xC0000035) /* STATUS_OBJECT_NAME_COLLISION */) return kal_err_exists; + if (n == static_cast(0xC00000D4) /* STATUS_NOT_SAME_DEVICE */) return kal_err_not_supported; + if (!okw::ok(n)) return okw::translate_nt(n); + wrote(target_root, b, blen); + return kal_ok; +} + int kal_fs_rename_flags(kal_dir from, const char* a, kal_uintptr alen, kal_dir to, const char* b, kal_uintptr blen, kal_uintptr flags) { if ((flags & ~static_cast(KAL_RENAME_NO_REPLACE)) != 0) return kal_err_invalid; @@ -1545,6 +1601,10 @@ kal_uintptr kal_fs_props(kal_dir d) { // own control code. if (a.info.attributes & okw::fs_supports_reparse_points) p |= KAL_FS_PROP_LINKS | KAL_FS_PROP_MAKE_LINKS; + // A second name for a file (0.25) where the volume reports that it stores + // several names for one file. + if (a.info.attributes & okw::fs_supports_hard_links) + p |= KAL_FS_PROP_HARD_LINKS; // A rename that does not replace is a field of the rename record (0.17). p |= KAL_FS_PROP_RENAME_NO_REPLACE; // A name held open is removed at once (0.17, clause 7.14) where the volume diff --git a/src/process.cpp b/src/process.cpp index 46e9035..c8535d5 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -999,7 +999,12 @@ int g_stop_armed = 0; // installing a routine does not clear, so a request its starter declined // stays declined. BOOL OKW_API stop_routine(DWORD) { - g_stop_word = 1; + // 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. The + // routine runs on a context of the system's for each event, and events may + // overlap, so the addition is atomic. + volatile long* word = reinterpret_cast(&g_stop_word); + if (okw::atomic::increment(word) == 0) (void)okw::atomic::increment(word); WakeByAddressAll(&g_stop_word); return TRUE; } diff --git a/src/win.h b/src/win.h index f6ec23e..f78385a 100644 --- a/src/win.h +++ b/src/win.h @@ -78,8 +78,10 @@ using okw_u32 = unsigned int; extern "C" long long _InterlockedExchange64(volatile long long*, long long); extern "C" long long _InterlockedExchangeAdd64(volatile long long*, long long); extern "C" long long _InterlockedOr64(volatile long long*, long long); -#pragma intrinsic(_InterlockedExchange64, _InterlockedExchangeAdd64, _InterlockedOr64) +extern "C" long _InterlockedIncrement(volatile long*); +#pragma intrinsic(_InterlockedExchange64, _InterlockedExchangeAdd64, _InterlockedOr64, _InterlockedIncrement) namespace okw::atomic { +inline long increment(volatile long* p) { return _InterlockedIncrement(p); } inline long long exchange(volatile long long* p, long long v) { return _InterlockedExchange64(p, v); } inline long long fetch_add(volatile long long* p, long long v) { return _InterlockedExchangeAdd64(p, v); } inline long long load(volatile long long* p) { return _InterlockedOr64(p, 0); } @@ -88,6 +90,7 @@ inline long long fetch_or(volatile long long* p, long long v) { return _Interloc } // namespace okw::atomic #else namespace okw::atomic { +inline long increment(volatile long* p) { return __atomic_add_fetch(p, 1, __ATOMIC_ACQ_REL); } inline long long exchange(volatile long long* p, long long v) { return __atomic_exchange_n(p, v, __ATOMIC_ACQ_REL); } inline long long fetch_add(volatile long long* p, long long v) { return __atomic_fetch_add(p, v, __ATOMIC_ACQ_REL); } inline long long load(volatile long long* p) { return __atomic_load_n(p, __ATOMIC_ACQUIRE); } @@ -216,6 +219,7 @@ enum : unsigned long { fs_case_sensitive_search = 0x00000001u, fs_supports_reparse_points = 0x00000080u, fs_supports_posix_unlink_rename = 0x00000400u, + fs_supports_hard_links = 0x00400000u, // FILE_SUPPORTS_HARD_LINKS (0.25) fs_supports_open_by_file_id = 0x01000000u, }; @@ -230,6 +234,7 @@ enum : int { file_disposition_information_ex_class = 64, file_end_of_file_information_class = 20, file_rename_information_class = 10, + file_link_information_class = 11, // the rename record's layout (0.25) file_id_full_directory_information_class = 38, }; diff --git a/tests/bounded_read_of_an_idle_pipe.cpp b/tests/bounded_read_of_an_idle_pipe.cpp index d4e8de8..ac85114 100644 --- a/tests/bounded_read_of_an_idle_pipe.cpp +++ b/tests/bounded_read_of_an_idle_pipe.cpp @@ -11,12 +11,15 @@ #include #include #include +import mcpplibs.mtest; -int main() { +using namespace mcpplibs::mtest; + +UTest bounded_read_of_an_idle_pipe_test{"bounded_read_of_an_idle_pipe", [] { kal_stream mine{}, theirs{}; if (kal_process_channel(&mine, &theirs) != kal_ok) { - std::printf("FAIL: a channel is made\n"); - return 1; + fail("a channel is made"); + return; } char byte = 0; const kal_duration started = kal_time_monotonic(); @@ -24,16 +27,7 @@ int main() { const kal_duration took = kal_time_monotonic() - started; kal_process_channel_close(theirs); kal_process_channel_close(mine); - int failures = 0; - if (n != -kal_err_again) { - std::printf("FAIL: a bounded read of an idle pipe expires (it answered %lld)\n", static_cast(n)); - ++failures; - } - if (took > 400000000ull) { - std::printf("FAIL: it expires near its bound (100 ms asked, %llu ms taken)\n", - static_cast(took / 1000000ull)); - ++failures; - } - if (failures == 0) std::printf("openkal-windows: a bounded read of an idle pipe expires near its bound\n"); - return failures == 0 ? 0 : 1; -} + expect(n == -kal_err_again) << "a bounded read of an idle pipe expires (it answered " << static_cast(n) << ")"; + expect(took <= 400000000ull) << "it expires near its bound (100 ms asked, " + << static_cast(took / 1000000ull) << " ms taken)"; +}}; diff --git a/tests/bounded_wait_of_a_bound_program.cpp b/tests/bounded_wait_of_a_bound_program.cpp index 214a0c7..a78cb41 100644 --- a/tests/bounded_wait_of_a_bound_program.cpp +++ b/tests/bounded_wait_of_a_bound_program.cpp @@ -11,16 +11,13 @@ #include #include #include +import mcpplibs.mtest; -namespace { +using namespace mcpplibs::mtest; -int failures = 0; +namespace { -void check(bool held, const char* what) { - if (held) { std::printf("ok: %s\n", what); return; } - std::printf("FAIL: %s\n", what); - ++failures; -} +void check(bool held, const char* what) { expect(held) << what; } bool system_volume(kal_dir* volume) { const kal_uintptr count = kal_fs_preopen_count(); @@ -47,10 +44,9 @@ int start(const char* command, kal_process* p) { } // namespace -int main() { +UTest bounded_wait_of_a_bound_program_test{"bounded_wait_of_a_bound_program", [] { if ((kal_process_props() & KAL_PROCESS_PROP_BOUND_LIFETIME) == 0) { - std::printf("openkal-windows: no bound lifetime here; nothing to observe\n"); - return 0; + skip("no bound lifetime here; nothing to observe"); } kal_process slow{}; check(start("ping -n 3 127.0.0.1 >NUL", &slow) == kal_ok, "a bound program starts"); @@ -65,7 +61,4 @@ int main() { check(start("exit 3", &quick) == kal_ok, "a second bound program starts"); check(kal_timeout_wait_process(quick, 20000000000ull, &status, &terminated) == kal_ok && status == 3, "a bounded wait reports the status the program returned"); - - if (failures == 0) std::printf("openkal-windows: a bound program is waited for with a bound\n"); - return failures == 0 ? 0 : 1; -} +}}; diff --git a/tests/console_input.cpp b/tests/console_input.cpp index 0926d3a..06f06c2 100644 --- a/tests/console_input.cpp +++ b/tests/console_input.cpp @@ -18,31 +18,56 @@ // absent or redirected. Every measurement ends with a key that always produces // a byte, so a key that produces none is measured rather than waited for; a // watchdog ends the test if even that does not arrive. -#include #include +#include "../src/win32.h" #include #include #include +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; + +// The console's own functions and its key record, declared here beside +// src/win32.h rather than taken from : GCC on PE refuses the system +// header together with the imported std ("conflicting language linkage"). +struct KEY_EVENT_RECORD { + BOOL bKeyDown; + WORD wRepeatCount; + WORD wVirtualKeyCode; + WORD wVirtualScanCode; + union { wchar_t UnicodeChar; char AsciiChar; } uChar; + DWORD dwControlKeyState; +}; +struct INPUT_RECORD { + WORD EventType; + union { KEY_EVENT_RECORD KeyEvent; DWORD space[4]; } Event; +}; +static_assert(sizeof(INPUT_RECORD) == 20, "the system's layout"); +constexpr WORD KEY_EVENT = 0x0001; +constexpr DWORD LEFT_CTRL_PRESSED = 0x0008; +constexpr WORD VK_RETURN = 0x0D; +#define WINAPI OKW_API +extern "C" { +OKW_IMPORT BOOL OKW_API AllocConsole(void); +OKW_IMPORT BOOL OKW_API FreeConsole(void); +OKW_IMPORT BOOL OKW_API FlushConsoleInputBuffer(HANDLE); +OKW_IMPORT BOOL OKW_API WriteConsoleInputW(HANDLE, const INPUT_RECORD*, DWORD, DWORD*); +} namespace { -int failures = 0; - -void check(bool held, const char* what) { - std::printf(" %s %s\n", held ? "held" : "FAIL", what); - std::fflush(stdout); - if (!held) ++failures; -} +void check(bool held, const char* what) { expect(held) << what; } DWORD WINAPI watchdog(void*) { Sleep(20000); std::printf(" FAIL a read waited for a key that never arrived\n"); std::fflush(stdout); ExitProcess(3); + return 3; } void key(INPUT_RECORD* r, WORD vk, WORD scan, wchar_t ch, DWORD ctrl, BOOL down) { - ZeroMemory(r, sizeof *r); + *r = INPUT_RECORD{}; r->EventType = KEY_EVENT; r->Event.KeyEvent.bKeyDown = down; r->Event.KeyEvent.wRepeatCount = 1; @@ -112,17 +137,15 @@ void show(const char* mode, const unsigned char* b, int n) { } // namespace -int main() { +UTest console_input_test{"console_input", [] { FreeConsole(); if (!AllocConsole()) { - std::printf("openkal-windows: no console could be allocated; not observed\n"); - return 0; + skip("no console could be allocated; not observed"); } HANDLE in = CreateFileW(L"CONIN$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr); if (in == INVALID_HANDLE_VALUE) { - std::printf("openkal-windows: the console's input could not be opened; not observed\n"); - return 0; + skip("the console's input could not be opened; not observed"); } CreateThread(nullptr, 0, watchdog, nullptr, 0, nullptr); const kal_stream s{ reinterpret_cast(in) }; @@ -190,8 +213,5 @@ int main() { } show("line on: a Enter", b, got); check(got >= 2 && b[0] == 'a', "a line is delivered"); - - std::printf("openkal-windows: what a console delivers, %s\n", failures ? "FAILED" : "held"); std::fflush(stdout); - return failures == 0 ? 0 : 1; -} +}}; diff --git a/tests/enumeration_ends_at_close.cpp b/tests/enumeration_ends_at_close.cpp index 96aaa4e..4563e39 100644 --- a/tests/enumeration_ends_at_close.cpp +++ b/tests/enumeration_ends_at_close.cpp @@ -11,18 +11,16 @@ #include #include #include +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; extern "C" OKW_IMPORT BOOL OKW_API GetProcessHandleCount(HANDLE, DWORD*); extern "C" OKW_IMPORT HANDLE OKW_API GetCurrentProcess(void); namespace { -int failures = 0; - -void check(bool held, const char* what) { - std::printf(" %s %s\n", held ? "held" : "FAIL", what); - if (!held) ++failures; -} +void check(bool held, const char* what) { expect(held) << what; } DWORD handles() { DWORD n = 0; return GetProcessHandleCount(GetCurrentProcess(), &n) ? n : 0; } @@ -41,11 +39,11 @@ void reader(void* p) { } // namespace -int main() { +UTest enumeration_ends_at_close_test{"enumeration_ends_at_close", [] { kal_dir cwd{}; if (kal_fs_preopen_count() == 0 || kal_fs_preopen(0, &cwd, nullptr, 0, nullptr) != kal_ok) { - std::printf("openkal-windows: no preopened directory to test in\n"); - return 1; + fail("a preopened directory to test in"); + return; } kal_fs_mkdir(cwd, "okw-enum.d", 10); kal_file f{}; @@ -101,6 +99,4 @@ int main() { kal_fs_remove(cwd, "okw-enum.d/inner", 16); kal_fs_remove(cwd, "okw-enum.d", 10); - std::printf("openkal-windows: what closing a directory ends, %s\n", failures ? "FAILED" : "held"); - return failures == 0 ? 0 : 1; -} +}}; diff --git a/tests/environment_read_from_two_threads.cpp b/tests/environment_read_from_two_threads.cpp index 5b37504..6d689d2 100644 --- a/tests/environment_read_from_two_threads.cpp +++ b/tests/environment_read_from_two_threads.cpp @@ -6,13 +6,27 @@ // --- PATH --- was reported absent. The first enquiry is made here from two // contexts released together, in a fresh copy of this program each round, and // both are told the variable. +// +// THE COPY BEGINS BEFORE THE TEST LIBRARY DOES. mtest's state reads its own +// variables through openkal when the first case or child of this file is +// declared, which would fill the tables before the two contexts ask. The copy +// is therefore recognised from the system's own command line, by the +// arguments child_command("copy") gives it, during this file's initialisation +// and ahead of every declaration, and it ends there with its status. #include #include +#include +#include +#include "../src/win32.h" #include #include #include #include #include +#include +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; namespace { @@ -37,14 +51,29 @@ int copy() { return g_found[0] && g_found[1] ? 0 : 1; } +// Whether the system's command line ends with the arguments of the copy. +bool started_as_copy() { + const wchar_t* line = GetCommandLineW(); + const wchar_t tail[] = L" --mtest-child copy"; + kal_uintptr n = 0, t = sizeof tail / sizeof tail[0] - 1; + while (line[n]) ++n; + while (n > 0 && line[n - 1] == L' ') --n; + if (n < t) return false; + for (kal_uintptr i = 0; i < t; ++i) if (line[n - t + i] != tail[i]) return false; + return true; +} + +[[maybe_unused]] const int g_copy = started_as_copy() ? (kal_exit(copy()), 0) : 0; + } // namespace -int main(int argc, char** argv) { - if (argc > 1 && std::strcmp(argv[1], "--copy") == 0) return copy(); +// Declared so that the copy's arguments are mtest's; it is entered above. +child copy_of_this{"copy", [](std::vector) { return copy(); }}; +UTest environment_read_from_two_threads_test{"environment_read_from_two_threads", [] { char self[1024]; const kal_intptr n = kal_env_image(self, sizeof self - 1); - if (n <= 0) { std::printf("FAIL: this program's image is named\n"); return 1; } + if (n <= 0) { fail("this program's image is named"); return; } self[n] = 0; // `C:\...\x.exe' named relative to the volume the image is on. kal_dir volume{}; @@ -53,26 +82,23 @@ int main(int argc, char** argv) { if (kal_fs_preopen(i, &d, name, sizeof name, &len) != kal_ok) continue; if (len == 2 && (name[0] | 0x20) == (self[0] | 0x20) && name[1] == ':') { volume = d; break; } } - if (!volume.h) { std::printf("openkal-windows: the image's volume is not granted; nothing to observe\n"); return 0; } + if (!volume.h) skip("the image's volume is not granted; nothing to observe"); const char* path = self + 3; // past `C:\' int failures = 0; for (int round = 0; round < 20; ++round) { - const char* args[2] = { self, "--copy" }; - const kal_uintptr lens[2] = { std::strlen(self), 6 }; + const std::vector command = child_command("copy"); + 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{ volume, volume, nullptr, nullptr, 0, 0 }; kal_process p{}; - if (kal_process_spawn(&how, path, std::strlen(path), args, lens, 2, nullptr, nullptr, 0, nullptr, &p) != kal_ok) { - std::printf("FAIL: a copy starts\n"); - return 1; + if (kal_process_spawn(&how, path, std::strlen(path), args.data(), lens.data(), args.size(), nullptr, nullptr, 0, nullptr, &p) != kal_ok) { + fail("a copy starts"); + return; } int status = -1, terminated = 0; kal_process_wait(p, &status, &terminated); if (status != 0) ++failures; } - if (failures) { - std::printf("FAIL: in %d of 20 copies a context read the environment before it was filled\n", failures); - return 1; - } - std::printf("openkal-windows: the environment read first from two contexts at once\n"); - return 0; -} + expect(failures == 0) << "in " << failures << " of 20 copies a context read the environment before it was filled"; +}}; diff --git a/tests/handle_inheritance.cpp b/tests/handle_inheritance.cpp index 85a6c06..cf9b8a6 100644 --- a/tests/handle_inheritance.cpp +++ b/tests/handle_inheritance.cpp @@ -18,6 +18,7 @@ // the child's own duplicate keeps the pipe open and the bounded read does not // complete within the bound -- which is what turns a hang into a report. #include +#include #include #include "../src/win32.h" #include @@ -25,16 +26,13 @@ #include #include #include +import mcpplibs.mtest; -namespace { +using namespace mcpplibs::mtest; -int failures = 0; +namespace { -void check(bool held, const char* what) { - if (held) { std::printf("ok: %s\n", what); return; } - std::printf("FAIL: %s\n", what); - ++failures; -} +void check(bool held, const char* what) { expect(held) << what; } // A volume, found among the preopens by its own name: exactly a letter and a // colon, which is how src/fs.cpp reports one and how it distinguishes a @@ -55,11 +53,10 @@ bool find_a_volume(kal_dir* out) { } // namespace -int main() { +UTest handle_inheritance_test{"handle_inheritance", [] { kal_dir sys{}; if (!find_a_volume(&sys)) { - std::printf("SKIP: no volume was found among the preopens\n"); - return 0; + skip("no volume was found among the preopens"); } // The handle this test makes inheritable and never places. A real caller @@ -70,18 +67,16 @@ int main() { sa.bInheritHandle = TRUE; HANDLE leak_read = nullptr, leak_write = nullptr; if (!CreatePipe(&leak_read, &leak_write, &sa, 0)) { - std::printf("SKIP: the pipe for the leak this test looks for could not be made\n"); - return 0; + skip("the pipe for the leak this test looks for could not be made"); } // The one stream this spawn legitimately places: the child's standard // input, which keeps a shell reading from it alive until told to stop. kal_stream mine{}, child_stdin{}; if (kal_process_channel(&mine, &child_stdin) != kal_ok) { - std::printf("SKIP: a channel for the child's standard input could not be made\n"); CloseHandle(leak_read); CloseHandle(leak_write); - return 0; + skip("a channel for the child's standard input could not be made"); } const char* path = "Windows/System32/cmd.exe"; @@ -92,12 +87,11 @@ int main() { const int spawned = kal_process_spawn(&how, path, path_len, &path, &path_len, 1, nullptr, nullptr, 0, &streams, &child); if (spawned != kal_ok) { - std::printf("SKIP: cmd.exe could not be started (kal error %d)\n", spawned); kal_process_channel_close(mine); kal_process_channel_close(child_stdin); CloseHandle(leak_read); CloseHandle(leak_write); - return 0; + skip("cmd.exe could not be started (kal error " + std::to_string(spawned) + ")"); } // The child's own copy of its standard input. This end belongs to the // child now; this process keeps `mine', which is what lets it end the @@ -128,5 +122,4 @@ int main() { CloseHandle(leak_read); std::printf("openkal-windows: a spawn inherits only the handles it placed\n"); - return failures == 0 ? 0 : 1; -} +}}; diff --git a/tests/links_unc_and_bound.cpp b/tests/links_unc_and_bound.cpp index b669644..5a497ac 100644 --- a/tests/links_unc_and_bound.cpp +++ b/tests/links_unc_and_bound.cpp @@ -12,13 +12,12 @@ #include #include #include +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; namespace { -int failures = 0; -void check(bool held, const char* what) { - std::printf(" %s %s\n", held ? "held" : "FAIL", what); - if (!held) ++failures; -} +void check(bool held, const char* what) { expect(held) << what; } kal_dir preopen_named(const char* want) { for (kal_uintptr i = 0; i < kal_fs_preopen_count(); ++i) { kal_dir d{}; char n[64]; kal_uintptr l = 0; @@ -29,7 +28,7 @@ kal_dir preopen_named(const char* want) { } } -int main() { +UTest links_unc_and_bound_test{"links_unc_and_bound", [] { kal_dir here{}; char n[512]; kal_uintptr l = 0; kal_fs_preopen(0, &here, n, sizeof n, &l); @@ -76,6 +75,4 @@ int main() { if (rc == kal_ok) { kal_process_wait(p, &status, &terminated); kal_process_close(p); } check(rc == kal_ok && status == 5, "a program bound to this one is started and awaited"); } - std::printf(failures == 0 ? "openkal-windows: links, the network's names, a bound lifetime\n" : ""); - return failures == 0 ? 0 : 1; -} +}}; diff --git a/tests/null_device_at_a_position.cpp b/tests/null_device_at_a_position.cpp index ff21e5b..14f0a0f 100644 --- a/tests/null_device_at_a_position.cpp +++ b/tests/null_device_at_a_position.cpp @@ -5,25 +5,24 @@ // was; and a flag beside a placed stream at one position is refused. #include #include +#include +#include #include "../src/win32.h" #include #include #include #include #include +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; extern "C" OKW_IMPORT unsigned long OKW_API GetModuleFileNameW(void*, wchar_t*, unsigned long); extern "C" OKW_IMPORT BOOL OKW_API GetProcessHandleCount(HANDLE, DWORD*); namespace { -int failures = 0; - -void check(bool held, const char* what) { - if (held) { std::printf("ok: %s\n", what); return; } - std::printf("FAIL: %s\n", what); - ++failures; -} +void check(bool held, const char* what) { expect(held) << what; } constexpr kal_u64 kBound = 10000ull * 1000ull * 1000ull; // ten seconds @@ -65,34 +64,37 @@ bool locate_self(kal_dir* volume, char* relative, kal_uintptr cap) { } // namespace -int main(int argc, char** argv) { - // The started side. 41-43: a position is not a character device. 44: a - // read did not report the end of input. 45: a write was not accepted. - if (argc == 2 && std::strcmp(argv[1], "--null") == 0) { - if (GetFileType(GetStdHandle(STD_INPUT_HANDLE)) != FILE_TYPE_CHAR) return 41; - if (GetFileType(GetStdHandle(STD_OUTPUT_HANDLE)) != FILE_TYPE_CHAR) return 42; - if (GetFileType(GetStdHandle(STD_ERROR_HANDLE)) != FILE_TYPE_CHAR) return 43; - char b[8]; - if (kal_stream_read(kal_stdin(), b, sizeof b) != 0) return 44; - if (kal_stream_write(kal_stdout(), "discarded", 9) != 9) return 45; - return 0; - } +// The started side. 41-43: a position is not a character device. 44: a read +// did not report the end of input. 45: a write was not accepted. +child null_device{"null", [](std::vector args) { + if (!args.empty()) return 46; + if (GetFileType(GetStdHandle(STD_INPUT_HANDLE)) != FILE_TYPE_CHAR) return 41; + if (GetFileType(GetStdHandle(STD_OUTPUT_HANDLE)) != FILE_TYPE_CHAR) return 42; + if (GetFileType(GetStdHandle(STD_ERROR_HANDLE)) != FILE_TYPE_CHAR) return 43; + char b[8]; + if (kal_stream_read(kal_stdin(), b, sizeof b) != 0) return 44; + if (kal_stream_write(kal_stdout(), "discarded", 9) != 9) return 45; + return 0; +}}; + +UTest null_device_at_a_position_test{"null_device_at_a_position", [] { kal_dir volume{}; char self[1024]; if (!locate_self(&volume, self, sizeof self)) { - std::printf("SKIP: this program's own name could not be expressed against a volume\n"); - return 0; + skip("this program's own name could not be expressed against a volume"); } check((kal_process_props() & KAL_PROCESS_PROP_NULL_STREAMS) != 0, "the null device is claimed"); - const char* args[2] = { "null_device_at_a_position", "--null" }; - const kal_uintptr lens[2] = { std::strlen(args[0]), std::strlen(args[1]) }; + 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{ volume, volume, nullptr, nullptr, 0, KAL_SPAWN_NULL_IN | KAL_SPAWN_NULL_OUT | KAL_SPAWN_NULL_ERR }; auto run = [&](int& status, int& terminated) { kal_process child{}; - const int spawned = kal_process_spawn(&how, self, std::strlen(self), args, lens, 2, + const int spawned = kal_process_spawn(&how, self, std::strlen(self), args.data(), lens.data(), args.size(), nullptr, nullptr, 0, nullptr, &child); if (spawned == kal_ok) { if (kal_timeout_wait_process(child, kBound, &status, &terminated) != kal_ok) @@ -103,9 +105,8 @@ int main(int argc, char** argv) { }; int status = -1, terminated = -1; check(run(status, terminated) == kal_ok, "a copy is started with the null device at its three positions"); - check(terminated == 0 && status == 0, - "the copy finds a character device there, reads its end and writes to it"); - if (status > 0) std::printf(" the copy reported %d\n", status); + expect(terminated == 0 && status == 0) + << "the copy finds a character device there, reads its end and writes to it (reported " << status << ")"; // Counted across a second start: the first start of a process opens // handles of the system's own that it keeps (measured on the runner, not // under Wine), and those are not this operation's. @@ -118,7 +119,7 @@ int main(int argc, char** argv) { const kal_spawn_streams placed{ kal_stream{0}, theirs, kal_stream{0} }; const kal_spawn out_null{ volume, volume, nullptr, nullptr, 0, KAL_SPAWN_NULL_OUT }; kal_process p{}; - check(kal_process_spawn(&out_null, self, std::strlen(self), args, lens, 2, + check(kal_process_spawn(&out_null, self, std::strlen(self), args.data(), lens.data(), args.size(), nullptr, nullptr, 0, &placed, &p) == kal_err_invalid, "the null device beside a placed stream at one position is refused"); kal_process_channel_close(theirs); @@ -126,5 +127,4 @@ int main(int argc, char** argv) { } std::printf("openkal-windows: the null device at a position\n"); - return failures == 0 ? 0 : 1; -} +}}; diff --git a/tests/one_string_command_line.cpp b/tests/one_string_command_line.cpp index be4acc6..b99ef9d 100644 --- a/tests/one_string_command_line.cpp +++ b/tests/one_string_command_line.cpp @@ -13,16 +13,13 @@ #include #include #include +import mcpplibs.mtest; -namespace { +using namespace mcpplibs::mtest; -int failures = 0; +namespace { -void check(bool held, const char* what) { - if (held) { std::printf("ok: %s\n", what); return; } - std::printf("FAIL: %s\n", what); - ++failures; -} +void check(bool held, const char* what) { expect(held) << what; } bool system_volume(kal_dir* volume) { const kal_uintptr count = kal_fs_preopen_count(); @@ -68,15 +65,14 @@ int run(kal_uintptr flags, const char* command, char* out, kal_uintptr cap, kal_ } // namespace -int main() { +UTest one_string_command_line_test{"one_string_command_line", [] { check((kal_process_props() & KAL_PROCESS_PROP_ONE_STRING) != 0, "the position is claimed"); char out[4096]; kal_uintptr got = 0; const char* command = "\"echo \"a b\" && echo c\""; const int rc = run(KAL_SPAWN_ONE_STRING, command, out, sizeof out, &got); - if (rc == -1) { std::printf("skip: no C: volume among the preopens\n"); return 0; } + if (rc == -1) skip("no C: volume among the preopens"); check(rc == kal_ok, "the interpreter starts with the command line joined as it is"); check(std::strstr(out, "\"a b\"") != nullptr && std::strstr(out, "c") != nullptr, "and runs the command as written, quotation marks included"); - return failures; -} +}}; diff --git a/tests/openkal_0_23.cpp b/tests/openkal_0_23.cpp index e837c8f..325cafd 100644 --- a/tests/openkal_0_23.cpp +++ b/tests/openkal_0_23.cpp @@ -14,6 +14,8 @@ // own file. #include #include +#include +#include #include "../src/win32.h" #include "../src/handle.h" #include @@ -23,15 +25,13 @@ #include #include #include +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; namespace { -int failures = 0; -void check(bool held, const char* what) { - if (held) { std::printf("ok: %s\n", what); return; } - std::printf("FAIL: %s\n", what); - ++failures; -} +void check(bool held, const char* what) { expect(held) << what; } kal_uintptr length(const char* s) { return std::strlen(s); } @@ -76,11 +76,14 @@ kal_dir by_name(const char* want) { // the caller's preopens, and returns its status, or -1. int start(kal_dir volume, const char* errand, const kal_preopen* grants, kal_uintptr n, kal_uintptr flags, const kal_spawn_streams* streams = nullptr, bool null_grants = false) { - const char* argv[2] = { "openkal_0_23", errand }; - const kal_uintptr lens[2] = { length(argv[0]), length(errand) }; + const std::vector command = child_command(errand); + 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{ volume, volume, nullptr, null_grants ? nullptr : grants, n, flags }; kal_process p{}; - if (kal_process_spawn(&how, g_self, length(g_self), argv, lens, 2, nullptr, nullptr, 0, streams, &p) != kal_ok) + if (kal_process_spawn(&how, g_self, length(g_self), argv.data(), lens.data(), argv.size(), nullptr, nullptr, 0, + streams, &p) != kal_ok) return -1; int status = -1, terminated = 0; kal_process_wait(p, &status, &terminated); @@ -109,50 +112,50 @@ int memory_priority() { } // namespace -int main(int argc, char** argv) { - const char* errand = argc > 1 ? argv[1] : ""; +// The recorded grant is the first preopen: a file read, a name absent, and the +// end through kal_exit, which ends the record. +child read_granted{"read", [](std::vector) -> int { + kal_dir d = by_name("/granted"); + kal_file f{}; + if (kal_fs_open(d, "present.txt", 11, KAL_OPEN_READ, &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_INFO_KIND, &i); + kal_exit(0); +}}; - if (std::strcmp(errand, "--read") == 0) { - // The recorded grant is the first preopen: a file read, a name absent, - // and the end through kal_exit, which ends the record. - kal_dir d = by_name("/granted"); - kal_file f{}; - if (kal_fs_open(d, "present.txt", 11, KAL_OPEN_READ, &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_INFO_KIND, &i); - kal_exit(0); - } - if (std::strcmp(errand, "--grants") == 0) { - if (kal_fs_preopen_count() != 2) return 41; - kal_dir own_volume{}; - if (!locate_self(&own_volume)) return 47; - kal_dir granted = by_name("/granted"); - if (!granted.h) return 42; - DWORD flags = 1; - if (!GetHandleInformation(okw::unpack(granted.h), &flags) || (flags & HANDLE_FLAG_INHERIT)) return 43; - // A program this one starts without grants, the variable inherited. - kal_dir volume{}; - for (kal_uintptr i = 0; i < kal_fs_preopen_count(); ++i) { - char name[16]{}; kal_uintptr len = 0; kal_dir d{}; - if (kal_fs_preopen(i, &d, name, sizeof name, &len) == kal_ok && len == 2 && name[1] == ':') volume = d; - } - if (!volume.h) return 44; - return start(volume, "--defaults", nullptr, 0, 0, nullptr, true) == 0 ? 0 : 45; - } - if (std::strcmp(errand, "--defaults") == 0) - return kal_fs_preopen_count() > 2 && by_name("/granted").h == 0 ? 0 : 46; - if (std::strcmp(errand, "--priority") == 0) { - const int p = memory_priority(); - return p < 0 ? 69 : 70 + p; +child holds_its_grants{"grants", [](std::vector) { + if (kal_fs_preopen_count() != 2) return 41; + kal_dir own_volume{}; + if (!locate_self(&own_volume)) return 47; + kal_dir granted = by_name("/granted"); + if (!granted.h) return 42; + DWORD flags = 1; + if (!GetHandleInformation(okw::unpack(granted.h), &flags) || (flags & HANDLE_FLAG_INHERIT)) return 43; + // A program this one starts without grants, the variable inherited. + kal_dir volume{}; + for (kal_uintptr i = 0; i < kal_fs_preopen_count(); ++i) { + char name[16]{}; kal_uintptr len = 0; kal_dir d{}; + if (kal_fs_preopen(i, &d, name, sizeof name, &len) == kal_ok && len == 2 && name[1] == ':') volume = d; } - if (std::strcmp(errand, "--fault") == 0) { fault_here(); return 0; } + if (!volume.h) return 44; + return start(volume, "defaults", nullptr, 0, 0, nullptr, true) == 0 ? 0 : 45; +}}; + +child has_the_defaults{"defaults", [](std::vector) { + return kal_fs_preopen_count() > 2 && by_name("/granted").h == 0 ? 0 : 46; +}}; + +child reports_priority{"priority", [](std::vector) { + const int p = memory_priority(); + return p < 0 ? 69 : 70 + p; +}}; +child faults{"fault", [](std::vector) { fault_here(); return 0; }}; + +UTest openkal_0_23_test{"openkal_0_23", [] { kal_dir volume{}; - if (!locate_self(&volume)) { - std::printf("SKIP: this program's own name could not be expressed against a volume\n"); - return 0; - } + if (!locate_self(&volume)) skip("this program's own name could not be expressed against a volume"); char tmp[1100]; std::snprintf(tmp, sizeof tmp, "%s/okw-0.23.tmp", g_dir); kal_fs_mkdir(volume, tmp, length(tmp)); @@ -170,7 +173,7 @@ int main(int argc, char** argv) { check((kal_process_props() & KAL_PROCESS_PROP_GRANT_DIR) != 0, "the directory grant is claimed"); { const kal_preopen grants[2] = { { dir, "/granted", 8 }, { volume, g_volume_name, 2 } }; - check(start(volume, "--grants", grants, 2, 0) == 0, + check(start(volume, "grants", grants, 2, 0) == 0, "the started program has its grants, uninheritable, and one it starts without grants has the defaults"); } @@ -186,7 +189,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 grants[2] = { { rd, "/granted", 8 }, { volume, g_volume_name, 2 } }; - check(start(volume, "--read", grants, 2, 0) == 0, "a program granted the recorded handle reads through it"); + check(start(volume, "read", grants, 2, 0) == 0, "a program granted the recorded handle reads through it"); kal_fs_close_dir(rd); kal_fs_close_file(rec); char buf[4096] = {}; @@ -239,11 +242,11 @@ int main(int argc, char** argv) { // ECONOMY. { const int mine = memory_priority(); - const int low = start(volume, "--priority", nullptr, 0, KAL_SPAWN_RESOURCES_ECONOMY, nullptr, true); + const int low = start(volume, "priority", nullptr, 0, KAL_SPAWN_RESOURCES_ECONOMY, nullptr, true); if (low == 69 || mine < 0) std::printf(" this system does not report a program's memory priority\n"); else { check(low == 72, "a program started for economy has a low memory priority"); - check(start(volume, "--priority", nullptr, 0, KAL_SPAWN_RESOURCES_PERFORMANCE, nullptr, true) == 70 + mine, + check(start(volume, "priority", nullptr, 0, KAL_SPAWN_RESOURCES_PERFORMANCE, nullptr, true) == 70 + mine, "and one started for performance has the system's, its starter's"); } } @@ -253,7 +256,7 @@ int main(int argc, char** argv) { kal_stream mine{}, theirs{}; if (kal_process_channel(&mine, &theirs) == kal_ok) { const kal_spawn_streams streams{ {0}, {0}, theirs }; - const int s = start(volume, "--fault", nullptr, 0, 0, &streams, true); + const int s = start(volume, "fault", nullptr, 0, 0, &streams, true); kal_process_channel_close(theirs); static char report[16384]; kal_uintptr have = 0; @@ -281,5 +284,4 @@ int main(int argc, char** argv) { kal_fs_close_dir(dir); kal_fs_remove(volume, tmp, length(tmp)); std::printf("openkal-windows: openkal 0.23\n"); - return failures == 0 ? 0 : 1; -} +}}; diff --git a/tests/stack_size.cpp b/tests/stack_size.cpp index 392b04a..9115a07 100644 --- a/tests/stack_size.cpp +++ b/tests/stack_size.cpp @@ -6,15 +6,13 @@ #include #include #include +import mcpplibs.mtest; -namespace { +using namespace mcpplibs::mtest; -int failures = 0; +namespace { -void check(bool held, const char* what) { - std::printf(" %s %s\n", held ? "held" : "FAIL", what); - if (!held) ++failures; -} +void check(bool held, const char* what) { expect(held) << what; } long recurse(long n); long (*volatile g_recurse)(long) = recurse; @@ -37,7 +35,7 @@ void room(void* p) { } // namespace -int main() { +UTest stack_size_test{"stack_size", [] { const kal_uintptr asks[] = { 1u << 20, 16u << 20, 64u << 20 }; for (kal_uintptr ask : asks) { probe q{ ask, 0, 0 }; @@ -58,6 +56,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"); - std::printf("openkal-windows: a least stack size, %s\n", failures ? "FAILED" : "held"); - return failures == 0 ? 0 : 1; -} +}}; diff --git a/tests/what_a_started_program_receives.cpp b/tests/what_a_started_program_receives.cpp index 289d3f0..3f7d4b7 100644 --- a/tests/what_a_started_program_receives.cpp +++ b/tests/what_a_started_program_receives.cpp @@ -21,12 +21,18 @@ #include #include #include +#include +#include #include "../src/win32.h" +#include #include #include #include #include #include +import mcpplibs.mtest; + +using namespace mcpplibs::mtest; // The one declaration src/win32.h does not carry, because the implementation // never needs to know where its own program is. @@ -34,13 +40,7 @@ extern "C" OKW_IMPORT unsigned long OKW_API GetModuleFileNameW(void*, wchar_t*, namespace { -int failures = 0; - -void check(bool held, const char* what) { - if (held) { std::printf("ok: %s\n", what); return; } - std::printf("FAIL: %s\n", what); - ++failures; -} +void check(bool held, const char* what) { expect(held) << what; } constexpr kal_u64 kBound = 10000ull * 1000ull * 1000ull; // ten seconds @@ -74,14 +74,33 @@ bool locate_self(kal_dir* volume, char* relative, kal_uintptr cap) { return false; } +// This program's environment without the test runner's report. A copy that +// inherited MTEST_REPORT would hold the report open, and the number it writes +// through could name that file and not nothing. +std::vector environment_without_report() { + std::vector out; + for (kal_uintptr i = 0; i < kal_env_var_count(); ++i) { + char name[512]; + const kal_intptr n = kal_env_var_at(i, name, sizeof name); + if (n <= 0 || static_cast(n) >= sizeof name) continue; + const std::string key(name, static_cast(n)); + if (key == "MTEST_REPORT" || key == "MTEST_PROTOCOL") continue; + const kal_intptr v = kal_env_var(key.data(), key.size(), nullptr, 0); + if (v < 0) continue; + std::string value(static_cast(v), '\0'); + if (v > 0) kal_env_var(key.data(), key.size(), value.data(), value.size()); + out.push_back(key + "=" + value); + } + return out; +} + // Starts a copy that writes one byte through `value', awaits it, and reports // whether the byte reached the pipe the caller holds the other end of. // `place' selects whether the start places one stream of its own. void observe(kal_dir volume, const char* self, bool place, const char* what) { HANDLE pipe_read = nullptr, pipe_write = nullptr; if (!CreatePipe(&pipe_read, &pipe_write, nullptr, 0)) { - std::printf("SKIP: %s (the pipe could not be made)\n", what); - return; + skip(std::string(what) + " (the pipe could not be made)"); } // Inheritable for a reason of the caller's own, and never placed. SetHandleInformation(pipe_write, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT); @@ -89,8 +108,14 @@ void observe(kal_dir volume, const char* self, bool place, const char* what) { char value[32]; std::snprintf(value, sizeof value, "%llu", static_cast(reinterpret_cast(pipe_write))); - const char* argv[3] = { "what_a_started_program_receives", "--write-through", value }; - const kal_uintptr lens[3] = { std::strlen(argv[0]), std::strlen(argv[1]), std::strlen(argv[2]) }; + const std::vector command = child_command("write-through", std::vector{ value }); + std::vector argv; + std::vector lens; + for (const auto& a : command) { argv.push_back(a.c_str()); lens.push_back(a.size()); } + const std::vector environment = environment_without_report(); + std::vector envp; + std::vector elens; + for (const auto& e : environment) { envp.push_back(e.c_str()); elens.push_back(e.size()); } kal_stream mine{}, theirs{}; const bool channel = place && kal_process_channel(&mine, &theirs) == kal_ok; @@ -98,8 +123,8 @@ void observe(kal_dir volume, const char* self, bool place, const char* what) { const kal_spawn how{ volume, volume, nullptr, nullptr, 0, 0 }; kal_process child{}; - const int spawned = kal_process_spawn(&how, self, std::strlen(self), argv, lens, 3, - nullptr, nullptr, 0, channel ? &streams : nullptr, + const int spawned = kal_process_spawn(&how, self, std::strlen(self), argv.data(), lens.data(), argv.size(), + envp.data(), elens.data(), envp.size(), channel ? &streams : nullptr, &child); check(spawned == kal_ok, "a copy of this program is started"); if (channel) kal_process_channel_close(theirs); @@ -124,23 +149,24 @@ void observe(kal_dir volume, const char* self, bool place, const char* what) { } // namespace -int main(int argc, char** argv) { - // The started side: write one byte through the number it was given. Where - // the handle was not conveyed the number names nothing, or names some - // object of this program's own that refuses the write. - if (argc == 3 && std::strcmp(argv[1], "--write-through") == 0) { - const HANDLE h = reinterpret_cast( - static_cast(std::strtoull(argv[2], nullptr, 10))); - DWORD wrote = 0; - WriteFile(h, "x", 1, &wrote, nullptr); - return 0; - } +// The started side: write one byte through the number it was given. Where the +// handle was not conveyed the number names nothing, or names some object of +// this program's own that refuses the write. +child write_through{"write-through", [](std::vector args) { + if (args.size() != 1) return 2; + const HANDLE h = reinterpret_cast( + static_cast(std::strtoull(args[0].c_str(), nullptr, 10))); + DWORD wrote = 0; + WriteFile(h, "x", 1, &wrote, nullptr); + return 0; +}}; + +UTest what_a_started_program_receives_test{"what_a_started_program_receives", [] { kal_dir volume{}; char self[1024]; if (!locate_self(&volume, self, sizeof self)) { - std::printf("SKIP: this program's own name could not be expressed against a volume\n"); - return 0; + skip("this program's own name could not be expressed against a volume"); } observe(volume, self, false, @@ -149,5 +175,4 @@ int main(int argc, char** argv) { "an unplaced inheritable handle does not reach a program started with a placed stream"); std::printf("openkal-windows: what a started program receives\n"); - return failures == 0 ? 0 : 1; -} +}}; diff --git a/tests/write_handle_answers_info.cpp b/tests/write_handle_answers_info.cpp index b0d418b..5e38aa6 100644 --- a/tests/write_handle_answers_info.cpp +++ b/tests/write_handle_answers_info.cpp @@ -25,23 +25,20 @@ // environment that cannot produce the failing condition is not a second // opinion about it. #include +#include #include #include +import mcpplibs.mtest; -namespace { +using namespace mcpplibs::mtest; -int failures = 0; +namespace { -void check(bool ok, const char* what) { - if (!ok) { - std::printf(" FAILED %s\n", what); - ++failures; - } -} +void check(bool ok, const char* what) { expect(ok) << what; } } // namespace -int main() { +UTest write_handle_answers_info_test{"write_handle_answers_info", [] { // The first preopened directory, the same way handle_inheritance.cpp finds // one: a test writes where the program was given somewhere to write. kal_dir cwd{}; @@ -58,8 +55,8 @@ int main() { } } if (!haveDir) { - std::printf("openkal-windows: no preopened directory to test in\n"); - return 1; + fail("a preopened directory to test in"); + return; } static const char name[] = "okw-write-handle-info.tmp"; @@ -71,8 +68,8 @@ int main() { &f); check(opened == kal_ok, "a file opens for writing"); if (opened != kal_ok) { - std::printf("openkal-windows: kal_fs_open returned %d\n", opened); - return 1; + fail("kal_fs_open returned " + std::to_string(opened)); + return; } // THE ASSERTION. The handle was granted a line ago and the caller asks it @@ -94,5 +91,4 @@ int main() { kal_fs_remove(cwd, name, len); std::printf("openkal-windows: a write-only handle answers for itself\n"); - return failures == 0 ? 0 : 1; -} +}};