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 6e41895..322be2e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,10 @@ env: # fail. MCPP_VERSION: 2026.11.5.1 XLINGS_VERSION: v2026.10.26.1 + # The mtest engine the conformance suite reports to (tools/run-conformance.sh + # with MTEST set): an xlings package, else the release bundle with this hash. + MTEST_VERSION: 0.5.2 + MTEST_SHA256: be443f8c546b3f4628dcd3f1eb6f45b4d698a7f805232f966349faf8250863c6 XLINGS_NON_INTERACTIVE: '1' jobs: @@ -865,9 +869,31 @@ jobs: # verdict: 0 when every observation held, 1 when one did not, and 2 when # nothing was observed --- the last being the outcome a run that selected # no interface would otherwise pass silently. + - uses: ./.github/actions/mtest + + # THE SUITE REPORTS BY THE TEST PROTOCOL HERE (MTEST is set): each + # interface a case of kind `cts', each observation a case within it, kept + # in the engine's store under conformance/.mtest. A failure is explained + # in the log, since the engine prints a summary and not the suite's text. - name: Every interface, every kind of examination run: | - bash tools/run-conformance.sh '${{ matrix.implementation }}' .impl '${{ matrix.features }}' + bash tools/run-conformance.sh '${{ matrix.implementation }}' .impl '${{ matrix.features }}' \ + || { ( cd conformance && "$MTEST" explain ) || true; exit 1; } + + - name: The run, reported + if: always() + run: | + [ -f conformance/.mtest/index.ndjson ] || { echo "the suite kept no run"; exit 0; } + ( cd conformance && "$MTEST" report --format md --group "$GITHUB_RUN_ID" ) >> "$GITHUB_STEP_SUMMARY" + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: mtest-cts-${{ matrix.implementation }}-${{ strategy.job-index }} + path: conformance/.mtest + include-hidden-files: true + if-no-files-found: ignore + retention-days: 7 # The suite is composable because openkal is: an implementation provides # an interface in whole or not at all, and a suite that examined all eight @@ -992,7 +1018,8 @@ jobs: - name: A selection of three interfaces is examined, not refused run: | rm -rf conformance/target - bash tools/run-conformance.sh '${{ matrix.implementation }}' .impl core,fs,task \ + # The suite's own text is what this step reads, so no runner is named. + MTEST= bash tools/run-conformance.sh '${{ matrix.implementation }}' .impl core,fs,task \ | tee selected.log # A report was produced, and nothing in it failed. @@ -1006,3 +1033,33 @@ jobs: # And an interface that was selected was examined. grep -qE 'held +\[behaviour\].*' selected.log + + # --------------------------------------------------------------------------- + # EVERY IMPLEMENTATION'S RUN AS ONE REPORT: each conformance job's store, + # taken into one, and reported together as the workflow run's group + # (`mtest ci report`): the cases of each interface on each implementation, + # the failures, and the measurements of cost. + conformance-report: + name: conformance (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-cts-* + path: ${{ runner.temp }}/stores + - name: Every implementation's run, reported together + 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 conformance job kept a run" >> "$GITHUB_STEP_SUMMARY"; exit 0; fi + "$MTEST" ci report --out "$RUNNER_TEMP/conformance.md" diff --git a/README.md b/README.md index 6eaf852..e29df8f 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,15 @@ admission and permits a helper to finish its bookkeeping after the unit's external members have ended. The caller requirements are stated in [SPEC clause 7.18](SPEC.md#718-observable-process-units) and `openkal/process.h`. +## Version 0.25 + +A file may be given a second name with `kal_fs_hard_link`, answered per volume by +`KAL_FS_PROP_HARD_LINKS` (clause 11 entry 59). The word `kal_process_stop_requested` +returns counts the requests to end that reach the program, so that each one is +observed (entry 60). The conformance suite reports to a test runner by the mtest +protocol, and checks that an implementation's manifest, `kal_interfaces()` and its +exported names state the same interfaces. + ## How a program uses openkal A program declares two dependencies. The first fixes the version of the contract @@ -87,16 +96,16 @@ conditional on the target. ```toml [dependencies] -openkal = "0.24.0" +openkal = "0.25.0" [target.'cfg(os = "linux")'.dependencies] -openkal-linux = "0.25.0" +openkal-linux = "0.26.0" [target.'cfg(os = "macos")'.dependencies] -openkal-macos = "0.22.0" +openkal-macos = "0.23.0" [target.'cfg(windows)'.dependencies] -openkal-windows = "0.20.0" +openkal-windows = "0.21.0" ``` The program imports the interface and names no implementation. diff --git a/SPEC.md b/SPEC.md index 548bcb9..db0dc28 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,4 +1,4 @@ -# openkal Specification, version 0.24 +# openkal Specification, version 0.25 ## 1. Scope @@ -2110,6 +2110,30 @@ The following are recorded so that they are not mistaken for oversights. continuation names the surviving program, not a remembered numeric group. The existing spawn form and immutable structure layouts remain unchanged. +59. **A second name for a file.** **Specified in 0.25**, as `kal_fs_hard_link` + and `KAL_FS_PROP_HARD_LINKS` in `openkal.fs`. A C library's `link` and + `linkat`, and a C++ library's `std::filesystem::create_hard_link`, could not + be written above this interface: no operation gave an existing file a + second name, and copying the file is a different file with a different + identity. Every environment this specification targets has the operation + natively where the volume's format records several names for one node + (`linkat`, `CreateHardLinkW`), so it is answered per volume by + `kal_fs_props`, as the operations upon links are (entry 7), and a volume + that does not claim it refuses the request rather than copying. The name + being given another is not resolved, for the reason `kal_fs_remove` and + `kal_fs_rename` act upon a name: a program that names a link means the + link. + +60. **The requests to end, counted.** **Specified in 0.25**, in + `kal_process_stop_requested`. The word was set once and never cleared, so a + program that took the requests over and went on after the first could not + learn of a second: an interactive program to which each interrupt keystroke + is a request to abandon the current command observed the first keystroke + and none after it. The word now counts the requests, skipping zero when the + count wraps, so the earlier statement --- zero until a request, non-zero + afterwards, never cleared --- remains true of every implementation of 0.25, + and a program written against 0.24 reads it as before. + ## 12. Images A program written against this specification is built for an environment: its diff --git a/SURFACE.txt b/SURFACE.txt index 2d0c488..15ac957 100644 --- a/SURFACE.txt +++ b/SURFACE.txt @@ -1,4 +1,4 @@ -# The C surface of openkal 0.24.0, one name per line. +# The C surface of openkal 0.25.0, one name per line. # # This file is normative and is the single source consulted by clause 9. A # conforming implementation exports the names of the interfaces it provides @@ -60,6 +60,7 @@ kal_fs_close_dir kal_fs_close_file kal_fs_dir_attenuate kal_fs_file_info +kal_fs_hard_link kal_fs_info kal_fs_link_create kal_fs_link_read diff --git a/conformance/README.md b/conformance/README.md index 92825c6..5fc1ec5 100644 --- a/conformance/README.md +++ b/conformance/README.md @@ -26,6 +26,41 @@ would be a surprising thing for `[dependencies]` to name — and it is reached b checking out the specification, which is the package it is the conformance procedure for. +## A runner's view: the mtest test protocol + +The suite speaks the [mtest test protocol](https://github.com/mcpp-community/mtest/blob/main/spec/protocol.md), +version 1, at level 2, written without a library as the rest of the suite is. Each +interface is a case of kind `cts` that names the interface as the clause it covers; +each observation is a case within it, reported as it runs (protocol P3.4): `pass`, +`fail`, or `skip` with the reason an observation was not made; each measurement of +cost is a metric in nanoseconds. A selection may name an interface or an +observation within one; the second runs its interface. Every case holds the +resource `serial:openkal-conformance`, because the sections create fixed names in +the working directory. + +The protocol needs `openkal.env` and `openkal.fs`; without them, or without a +runner, the suite is judged by its exit status alone, as before. With an engine +named in `MTEST`, `tools/run-conformance.sh` runs the suite through it in one job +and the run is kept in `.mtest`: + +```bash +MTEST=mtest bash .spec/tools/run-conformance.sh openkal-linux . full,optional +(cd .spec/conformance && mtest report --format md) +``` + +The protocol's variables are not passed to the copies the suite starts: a copy is +started with the environment the suite gives it, which is empty (P2.2). + +## The three statements of an interface set + +An implementation states the interfaces it provides three times: its manifest's +`provides-interfaces` (what a build system checks a program's requirements +against), `kal_interfaces()` (what a loader checks an image's against), and the +names it exports. `tools/run-conformance.sh` reads the first and names it to the +suite as `OPENKAL_CONFORMANCE_PROVIDES`, which compares the second with it; the +script compares the third. A manifest that states no list is reported and not +compared. + ## It is composable, because openkal is An interface is the unit of provision. An implementation provides one in whole diff --git a/conformance/mcpp.toml b/conformance/mcpp.toml index c1fd5d9..7c4c258 100644 --- a/conformance/mcpp.toml +++ b/conformance/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-conformance" -version = "0.8.0" +version = "0.9.0" description = "The behavioural half of clause 9: a suite an implementation of openkal runs against itself, selectable to the interfaces it provides." license = "Apache-2.0" authors = ["mcpplibs"] @@ -18,7 +18,7 @@ repo = "https://github.com/mcpplibs/openkal" # What it depends upon is therefore openkal and the language. The formatting in # okc.report is the price of that, and it is sixty lines. [dependencies] -openkal = "0.24.0" +openkal = "0.25.0" # The implementation under examination is not named here. # diff --git a/conformance/src/declarations.c b/conformance/src/declarations.c index 8cf95cf..8e4d190 100644 --- a/conformance/src/declarations.c +++ b/conformance/src/declarations.c @@ -62,6 +62,7 @@ void okc_declarations_c(void) (void)sizeof(&kal_fs_close_file); (void)sizeof(&kal_fs_dir_attenuate); (void)sizeof(&kal_fs_file_info); + (void)sizeof(&kal_fs_hard_link); (void)sizeof(&kal_fs_info); (void)sizeof(&kal_fs_link_create); (void)sizeof(&kal_fs_link_read); diff --git a/conformance/src/protocol.cpp b/conformance/src/protocol.cpp new file mode 100644 index 0000000..b465669 --- /dev/null +++ b/conformance/src/protocol.cpp @@ -0,0 +1,346 @@ +module okc.protocol; + +import openkal.types; +import openkal.env; +import openkal.fs; +import openkal.stream; +import openkal.time; +import okc.spec; + +namespace okc::protocol { +namespace { + +kal_uintptr length(const char* s) { kal_uintptr n = 0; while (s && s[n]) ++n; return n; } + +bool same(const char* a, kal_uintptr alen, const char* b, kal_uintptr blen) { + if (alen != blen) return false; + for (kal_uintptr i = 0; i < alen; ++i) if (a[i] != b[i]) return false; + return true; +} + +// THE MARKER (protocol P2.3): the bytes a runner finds in the program's file, +// followed by a byte that is not a digit. Read once below, so that no link +// that keeps only what it reaches removes it. +extern "C" const char okc_mtest_marker[] = "mtest-protocol=1;"; + +bool g_active = false; +bool g_listing = false; +kal_stream g_report{}; +kal_u64 g_start = 0; + +// The selection: identifiers, one per line, and whether each named a case. +char g_select[262144]; +kal_uintptr g_select_len = 0; +bool g_selecting = false; +constexpr int kMaxSelected = 4096; +bool g_matched[kMaxSelected]; + +// The case running, and the cases within it whose identifiers were given, so +// that two observations of one text are two identifiers (P3.2). +char g_case[64]; +bool g_case_open = false; +kal_u64 g_case_began = 0; +bool g_case_failed = false; +int g_case_parts = 0; +constexpr int kMaxParts = 1024; +kal_u64 g_part_hash[kMaxParts]; +int g_part_seen[kMaxParts]; + +// ---- the bytes of one event -------------------------------------------------- +struct line { + char buf[4096]; + kal_uintptr n = 0; + bool overflow = false; + void raw(const char* s, kal_uintptr len) { + for (kal_uintptr i = 0; i < len; ++i) { + if (n + 1 >= sizeof buf) { overflow = true; return; } + buf[n++] = s[i]; + } + } + void raw(const char* s) { raw(s, length(s)); } + void str(const char* s, kal_uintptr len) { + raw("\""); + for (kal_uintptr i = 0; i < len; ++i) { + const unsigned char c = static_cast(s[i]); + if (c == '"') raw("\\\""); + else if (c == '\\') raw("\\\\"); + else if (c == '\n') raw("\\n"); + else if (c == '\r') raw("\\r"); + else if (c == '\t') raw("\\t"); + else if (c < 0x20) { + const char hex[] = "0123456789abcdef"; + const char e[6] = { '\\', 'u', '0', '0', hex[c >> 4], hex[c & 15] }; + raw(e, 6); + } else { + const char b = static_cast(c); + raw(&b, 1); + } + } + raw("\""); + } + void str(const char* s) { str(s, length(s)); } + void num(kal_u64 v) { + char d[24]; int i = 24; + if (v == 0) d[--i] = '0'; + while (v) { d[--i] = static_cast('0' + v % 10); v /= 10; } + raw(d + i, static_cast(24 - i)); + } + void send() { + if (!g_active || overflow) return; + raw("\n"); + kal_stream_write(g_report, buf, n); // one write per event (P5.1) + } +}; + +kal_u64 elapsed_ms() { +#ifdef MCPP_FEATURE_TIME + return (kal_time_monotonic() - g_start) / 1000000u; +#else + return 0; +#endif +} + +kal_u64 hash(const char* s) { + kal_u64 h = 1469598103934665603ull; + for (kal_uintptr i = 0; s[i]; ++i) { h ^= static_cast(s[i]); h *= 1099511628211ull; } + return h; +} + +// The case identifier a heading denotes: its first word, and the heading of +// the implementation's self-description is `openkal.version'. +void id_of(const char* heading_text, char* out, kal_uintptr cap) { + kal_uintptr n = 0; + while (heading_text[n] && heading_text[n] != ' ' && n + 1 < cap) { out[n] = heading_text[n]; ++n; } + out[n] = 0; + if (same(out, n, "openkal", 7)) { + const char v[] = "openkal.version"; + for (kal_uintptr i = 0; i < sizeof v && i < cap; ++i) out[i] = v[i]; + } +} + +void begin_event(const char* id) { + line e; e.raw("{\"e\":\"begin\",\"id\":"); e.str(id); e.raw(",\"t\":"); e.num(elapsed_ms()); + e.raw(",\"kind\":\"cts\",\"clauses\":["); e.str(id); e.raw("],\"resources\":[\"serial:openkal-conformance\"]}"); e.send(); +} + +void end_event(const char* id, const char* status, const char* reason, kal_u64 ms = 0) { + line e; e.raw("{\"e\":\"end\",\"id\":"); e.str(id); e.raw(",\"status\":"); e.str(status); + e.raw(",\"ms\":"); e.num(ms); + if (reason) { e.raw(",\"reason\":"); e.str(reason); } + e.raw("}"); e.send(); +} + +void close_case() { + if (!g_case_open) return; + end_event(g_case, g_case_failed ? "fail" : "pass", nullptr, elapsed_ms() - g_case_began); + g_case_open = false; +} + +#if defined(MCPP_FEATURE_ENV) +kal_intptr variable(const char* name, char* out, kal_uintptr cap) { + return kal_env_var(name, length(name), out, cap); +} +#endif + +#if defined(MCPP_FEATURE_ENV) && defined(MCPP_FEATURE_FS) +// A name the environment gave, opened through the grant whose name is the +// longest prefix of it: openkal has no ambient root, and the grants are how a +// program reaches a name it was told. +bool open_by_name(const char* name, kal_uintptr len, kal_uintptr flags, kal_file* out) { + char path[1024]; + if (len >= sizeof path) return false; + for (kal_uintptr i = 0; i < len; ++i) path[i] = name[i] == '\\' ? '/' : name[i]; + kal_dir best{}; kal_uintptr best_len = 0; bool found = false; + const kal_uintptr grants = kal_fs_preopen_count(); + for (kal_uintptr g = 0; g < grants; ++g) { + char gname[1024]; kal_uintptr glen = 0; kal_dir d{}; + if (kal_fs_preopen(g, &d, gname, sizeof gname, &glen) != kal_ok || glen == 0 || glen >= sizeof gname) + continue; + for (kal_uintptr i = 0; i < glen; ++i) if (gname[i] == '\\') gname[i] = '/'; + while (glen > 1 && gname[glen - 1] == '/') --glen; + if (glen > len) continue; + bool prefix = true; + for (kal_uintptr i = 0; i < glen && prefix; ++i) prefix = gname[i] == path[i]; + if (!prefix) continue; + if (glen < len && path[glen] != '/' && !(glen == 1 && gname[0] == '/')) continue; + if (!found || glen > best_len) { best = d; best_len = glen; found = true; } + } + if (!found) return false; + kal_uintptr at = best_len; + while (at < len && path[at] == '/') ++at; + return kal_fs_open(best, path + at, len - at, flags, out) == kal_ok; +} +#endif + +} // namespace + +bool begin() { + if (*static_cast(okc_mtest_marker) != 'm') return false; +#if defined(MCPP_FEATURE_ENV) && defined(MCPP_FEATURE_FS) + char value[1024]; + const kal_intptr p = variable("MTEST_PROTOCOL", value, sizeof value); + if (p != 1 || value[0] != '1') return false; + const kal_intptr r = variable("MTEST_REPORT", value, sizeof value); + if (r <= 0 || static_cast(r) >= sizeof value) return false; + kal_file f{}; + if (!open_by_name(value, static_cast(r), + (kal::fs::open::write | kal::fs::open::create | kal::fs::open::append).bits, &f)) + return false; + g_report = kal_fs_stream(f); + g_active = true; +#ifdef MCPP_FEATURE_TIME + g_start = kal_time_monotonic(); +#endif + const kal_intptr m = variable("MTEST_MODE", value, sizeof value); + g_listing = m == 4 && same(value, 4, "list", 4); + + const kal_intptr s = variable("MTEST_SELECT", value, sizeof value); + if (s > 0 && static_cast(s) < sizeof value) { + kal_file sf{}; + if (open_by_name(value, static_cast(s), kal::fs::open::read.bits, &sf)) { + g_selecting = true; + const kal_stream in = kal_fs_stream(sf); + for (;;) { + const kal_intptr got = kal_stream_read(in, g_select + g_select_len, + sizeof g_select - 1 - g_select_len); + if (got <= 0) break; + g_select_len += static_cast(got); + if (g_select_len + 1 >= sizeof g_select) break; + } + kal_fs_close_file(sf); + } + } + + line e; + e.raw("{\"e\":\"program\",\"protocol\":1,\"level\":2,\"framework\":\"openkal-conformance\"," + "\"version\":\"0.9.0\",\"isolation\":[\"process\"]}"); + e.send(); + return true; +#else + return false; +#endif +} + +bool active() { return g_active; } +bool listing() { return g_listing; } + +bool wanted(const char* id) { + if (!g_selecting) return true; + // A line names the section's case, or a part within it (`/'): + // selecting a part runs the case that declares it (protocol P3.4). + const kal_uintptr idlen = length(id); + bool any = false; + kal_uintptr at = 0; int index = 0; + while (at < g_select_len) { + kal_uintptr end = at; + while (end < g_select_len && g_select[end] != '\n') ++end; + kal_uintptr len = end - at; + if (len > 0 && g_select[at + len - 1] == '\r') --len; + if (len > 0) { + const bool whole = same(g_select + at, len, id, idlen); + const bool part = len > idlen && g_select[at + idlen] == '/' + && same(g_select + at, idlen, id, idlen); + if (whole || part) { + if (index < kMaxSelected) g_matched[index] = true; + any = true; + } + ++index; + } + at = end + 1; + } + return any; +} + +// EVERY CASE HOLDS ONE SERIAL RESOURCE (protocol P7.1). The sections create +// and remove fixed names in the working directory they were given, so two +// copies of this program running at once would observe each other's files; a +// runner that divides the cases among workers runs them one at a time. +void list_case(const char* id) { + line e; e.raw("{\"e\":\"case\",\"id\":"); e.str(id); e.raw(",\"name\":"); e.str(id); + e.raw(",\"kind\":\"cts\",\"clauses\":["); e.str(id); e.raw("],\"resources\":[\"serial:openkal-conformance\"]}"); e.send(); +} + +void section(const char* heading_text) { + if (!g_active) return; + close_case(); + id_of(heading_text, g_case, sizeof g_case); + g_case_open = true; + g_case_failed = false; + g_case_parts = 0; + g_case_began = elapsed_ms(); + begin_event(g_case); +} + +void observation(kind k, const char* what, bool held, const char* because) { + if (!g_active || !g_case_open) return; + // The part's identifier: the case's, the observation's text, and its + // ordinal where the text recurs in the case. + char id[1024]; kal_uintptr n = 0; + for (kal_uintptr i = 0; g_case[i] && n + 1 < sizeof id; ++i) id[n++] = g_case[i]; + if (n + 1 < sizeof id) id[n++] = '/'; + for (kal_uintptr i = 0; what && what[i] && n + 8 < sizeof id; ++i) id[n++] = what[i]; + id[n] = 0; + const kal_u64 h = hash(id); + int seen = 0; + for (int i = 0; i < g_case_parts; ++i) if (g_part_hash[i] == h) { seen = ++g_part_seen[i]; break; } + if (seen == 0 && g_case_parts < kMaxParts) { g_part_hash[g_case_parts] = h; g_part_seen[g_case_parts] = 1; ++g_case_parts; } + if (seen > 1 && n + 8 < sizeof id) { + id[n++] = ' '; id[n++] = '#'; + char d[8]; int i = 8; int v = seen; + while (v) { d[--i] = static_cast('0' + v % 10); v /= 10; } + for (; i < 8; ++i) id[n++] = d[i]; + id[n] = 0; + } + + line b; b.raw("{\"e\":\"begin\",\"id\":"); b.str(id); b.raw(",\"t\":"); b.num(elapsed_ms()); + b.raw(",\"kind\":\"cts\",\"tags\":["); b.str(name_of(k)); b.raw("],\"clauses\":["); b.str(g_case); + b.raw("],\"resources\":[\"serial:openkal-conformance\"]}"); b.send(); + if (because != nullptr) { + end_event(id, "skip", because); + return; + } + if (!held) { + g_case_failed = true; + line f; f.raw("{\"e\":\"failure\",\"id\":"); f.str(id); + f.raw(",\"file\":\"\",\"line\":0,\"message\":"); f.str(what); f.raw("}"); f.send(); + } + end_event(id, held ? "pass" : "fail", nullptr); +} + +void metric(const char* what, kal_u64 ns) { + if (!g_active || !g_case_open) return; + line e; e.raw("{\"e\":\"metric\",\"id\":"); e.str(g_case); e.raw(",\"name\":"); e.str(what); + e.raw(",\"value\":"); e.num(ns); e.raw(",\"unit\":\"ns\",\"better\":\"lower\"}"); e.send(); +} + +int finish(int held, int failed, int unobserved) { + if (!g_active) return -1; + close_case(); + int missing = 0; + if (g_selecting) { + kal_uintptr at = 0; int index = 0; + while (at < g_select_len) { + kal_uintptr end = at; + while (end < g_select_len && g_select[end] != '\n') ++end; + kal_uintptr len = end - at; + if (len > 0 && g_select[at + len - 1] == '\r') --len; + if (len > 0) { + if (index >= kMaxSelected || !g_matched[index]) { + line e; e.raw("{\"e\":\"missing\",\"id\":"); e.str(g_select + at, len); e.raw("}"); e.send(); + ++missing; + } + ++index; + } + at = end + 1; + } + } + const bool ok = failed == 0 && missing == 0; + line e; e.raw("{\"e\":\"done\",\"status\":"); e.str(ok ? "pass" : "fail"); + e.raw(",\"counts\":{\"pass\":"); e.num(static_cast(held)); + e.raw(",\"fail\":"); e.num(static_cast(failed)); + e.raw(",\"skip\":"); e.num(static_cast(unobserved)); e.raw("}}"); + e.send(); + return ok ? 0 : 1; +} + +} // namespace okc::protocol diff --git a/conformance/src/protocol.cppm b/conformance/src/protocol.cppm new file mode 100644 index 0000000..d578ac0 --- /dev/null +++ b/conformance/src/protocol.cppm @@ -0,0 +1,42 @@ +// The mtest test protocol, version 1, as this suite speaks it. +// +// A runner (mtest, a host's own test runner, a continuous-integration step) +// learns what was examined from the protocol's events rather than from the +// suite's text: one case per interface, of kind `cts', naming the interface as +// the clause it covers; each observation a case within it (protocol P3.4) that +// passed, failed or was not observed; each measurement of cost a metric. The +// interfaces are cases because they are what a selection names and what an +// implementation provides in whole or not at all; the observations are found +// only by running, so they are reported as they run and listed by none. +// +// WRITTEN WITHOUT A LIBRARY, AS THE SUITE IS. The suite runs beneath a C +// library and on a machine with none, so the events are composed here and +// written through openkal.fs; where that interface or openkal.env was not +// selected, no event is written and a runner judges the program by its exit +// status alone (protocol P1.1), which is what the suite reported before. +export module okc.protocol; + +import openkal.types; +import okc.spec; + +export namespace okc::protocol { + +// Reads the handshake (protocol P2). True when a runner speaks the protocol and +// the report could be opened. +bool begin(); +bool active(); +bool listing(); // MTEST_MODE=list: list and run nothing + +// Whether an interface's case is selected (MTEST_SELECT, P4); every case when +// no selection was given or the protocol is not spoken. +bool wanted(const char* id); + +void list_case(const char* id); +void section(const char* heading_text); // ends the previous case, begins the next +void observation(kind k, const char* what, bool held, const char* because); +void metric(const char* what, kal_u64 ns); +// Ends the last case, reports the selections that named no case (P4.1) and +// writes `done'. Returns the exit status the protocol requires. +int finish(int held, int failed, int unobserved); + +} // namespace okc::protocol diff --git a/conformance/src/report.cpp b/conformance/src/report.cpp index a00d7a0..986733a 100644 --- a/conformance/src/report.cpp +++ b/conformance/src/report.cpp @@ -3,6 +3,7 @@ module okc.report; import openkal.types; import openkal.stream; import okc.spec; +import okc.protocol; namespace okc { namespace { @@ -49,6 +50,7 @@ void put_hex(kal_uintptr v) { } void observe(kind k, bool held, const char* what) { + protocol::observation(k, what, held, nullptr); if (held) { ++g_held; write(" held "); } else { ++g_failed; write(" DID NOT HOLD "); } write("["); write(name_of(k)); write("] "); @@ -56,12 +58,14 @@ void observe(kind k, bool held, const char* what) { } void unobserved(kind k, const char* what, const char* because) { + protocol::observation(k, what, false, because ? because : "not observed"); ++g_unobserved; write(" not observed ["); write(name_of(k)); write("] "); write(what); write(" --- "); write(because); write("\n"); } void measure(const char* what, kal_u64 total_ns, int iterations) { + protocol::metric(what, iterations ? total_ns / static_cast(iterations) : 0); write(" measured [cost] "); write(what); write(": "); @@ -75,7 +79,7 @@ void claim(const char* what, kal_uintptr word) { write(" claimed "); write(what); write(" = "); put_hex(word); write("\n"); } -void heading(const char* text) { write("\n"); write(text); write("\n"); } +void heading(const char* text) { protocol::section(text); write("\n"); write(text); write("\n"); } void line(const char* text) { write(text); write("\n"); } int held_count() { return g_held; } @@ -111,6 +115,7 @@ void write_inventory() { } int summarise() { + const int verdict = protocol::finish(g_held, g_failed, g_unobserved); write("\nobservations: "); put_signed(g_held); write(" held, "); put_signed(g_failed); write(" did not hold, "); @@ -126,6 +131,7 @@ int summarise() { } line(g_failed == 0 ? "the implementation conforms in every observation made" : "the implementation does not conform"); + if (verdict >= 0) return verdict; return g_failed == 0 ? 0 : 1; } diff --git a/conformance/src/sections/child.cpp b/conformance/src/sections/child.cpp index e76137a..8c81bbc 100644 --- a/conformance/src/sections/child.cpp +++ b/conformance/src/sections/child.cpp @@ -38,6 +38,7 @@ const char* argument_for(errand e) { case errand::report_grants: return "--child-grants"; case errand::report_no_preopens: return "--child-no-preopens"; case errand::end_as_requested: return "--child-end-as-requested"; + case errand::count_requests: return "--child-count-requests"; case errand::map_beyond_shortened: return "--child-map-beyond-shortened"; case errand::hold_memory: return "--child-hold-memory"; case errand::grow_memory: return "--child-grow-memory"; @@ -65,6 +66,7 @@ errand child_errand() { if (same(a, len, argument_for(errand::report_grants))) return errand::report_grants; if (same(a, len, argument_for(errand::report_no_preopens))) return errand::report_no_preopens; if (same(a, len, argument_for(errand::end_as_requested))) return errand::end_as_requested; + if (same(a, len, argument_for(errand::count_requests))) return errand::count_requests; if (same(a, len, argument_for(errand::map_beyond_shortened))) return errand::map_beyond_shortened; if (same(a, len, argument_for(errand::hold_memory))) return errand::hold_memory; if (same(a, len, argument_for(errand::grow_memory))) return errand::grow_memory; @@ -163,6 +165,23 @@ after g_after; kal_process_end_as_requested(); kal_exit(99); } +#endif + kal_exit(98); + case errand::count_requests: + // The word is taken over and two requests are awaited, five seconds + // in all. 40: each request changed the word and it is non-zero. 41: + // the second request left the word as the first had (0.24's word). + // 98: no request arrived as one. +#if defined(MCPP_FEATURE_PROCESS) && defined(MCPP_FEATURE_TIME) + if (const kal_u32* word = kal_process_stop_requested()) { + kal_stream_write(kal_stdout(), "ARMED\n", 6); + for (int i = 0; i < 1000 && *word == 0; ++i) kal_time_sleep(5u * 1000u * 1000u); + const kal_u32 first = *word; + if (first == 0) kal_exit(98); + for (int i = 0; i < 400 && *word == first; ++i) kal_time_sleep(5u * 1000u * 1000u); + const kal_u32 second = *word; + kal_exit(second != first && second != 0 ? 40 : 41); + } #endif kal_exit(98); case errand::wait_to_be_terminated: diff --git a/conformance/src/sections/child.cppm b/conformance/src/sections/child.cppm index 4a6ad44..7fe37ca 100644 --- a/conformance/src/sections/child.cppm +++ b/conformance/src/sections/child.cppm @@ -26,6 +26,7 @@ enum class errand { report_grants, // the directories a starter granted are its preopens report_no_preopens, // ... and a count of zero leaves it none (clause 7.13) end_as_requested, // takes the request over, then ends as it would have (0.17) + count_requests, // takes the request over and observes two requests (0.25) map_beyond_shortened, // reads a mapped page its file no longer holds (0.18) hold_memory, // holds 32 MiB written through, then waits to be ended (0.20) grow_memory, // grows by 8 MiB at a time to 512 MiB, then waits; ends diff --git a/conformance/src/sections/fs.cpp b/conformance/src/sections/fs.cpp index f84d5e1..4768071 100644 --- a/conformance/src/sections/fs.cpp +++ b/conformance/src/sections/fs.cpp @@ -887,6 +887,68 @@ void run() { } } + // --- a second name for a file, version 0.25 --------------------------- + // + // ASKED FIRST, AS LINKS ARE: whether a node may have several names is a + // property of the volume's format. Where it is claimed, both names reach one + // file --- one identity, one content, and a file that remains while either + // name does; where it is not, the request is refused and copies nothing. + { + const char* kSecond = "okc-conformance-second-name.tmp"; + kal_fs_remove(here(), kSecond, length(kSecond)); + put_file(kName, "0123456789"); + if ((kal_fs_props(here()) & kal::fs::hard_links.bits) != 0) { + const int made = kal_fs_hard_link(here(), kName, length(kName), + here(), kSecond, length(kSecond)); + observe(kind::behaviour, made == kal_ok, + "a file is given a second name where the volume claims it"); + kal_node_info first = fresh(), second = fresh(); + const int a = kal_fs_info(here(), kName, length(kName), 0, kal::fs::field::all, &first); + const int b = kal_fs_info(here(), kSecond, length(kSecond), 0, kal::fs::field::all, &second); + if (a == kal_ok && b == kal_ok + && (first.present & second.present & kal::fs::field::identity) != 0) { + observe(kind::behaviour, + first.identity[0] == second.identity[0] && first.identity[1] == second.identity[1], + "both names reach one node, of one identity"); + } else { + unobserved(kind::behaviour, "both names reach one node, of one identity", + "the implementation does not report an identity for this resource"); + } + observe(kind::behaviour, b == kal_ok && second.kind == kal_node_file && second.size == 10, + "the second name reaches the file's content"); + observe(kind::behaviour, + kal_fs_hard_link(here(), kName, length(kName), here(), kSecond, length(kSecond)) + == kal_err_exists, + "a second name that already names a node is refused"); + const int removed = kal_fs_remove(here(), kName, length(kName)); + kal_node_info left = fresh(); + const int l = kal_fs_info(here(), kSecond, length(kSecond), 0, kal::fs::field::all, &left); + observe(kind::behaviour, removed == kal_ok && l == kal_ok && left.kind == kal_node_file && left.size == 10, + "removing one name leaves the file reachable by the other"); + const char* kNoSuch = "okc-conformance-no-such-file.tmp"; + observe(kind::behaviour, + kal_fs_hard_link(here(), kNoSuch, length(kNoSuch), here(), kName, length(kName)) + == kal_err_not_found, + "a name that names nothing is not given a second"); + const char* kDir = "okc-conformance-unnamed-dir.tmp"; + kal_fs_mkdir(here(), kDir, length(kDir)); + const int d = kal_fs_hard_link(here(), kDir, length(kDir), here(), kName, length(kName)); + observe(kind::behaviour, d == kal_err_is_directory, + "a directory is not given a second name"); + kal_fs_remove(here(), kDir, length(kDir)); + kal_fs_remove(here(), kSecond, length(kSecond)); + kal_fs_remove(here(), kName, length(kName)); + } else { + unobserved(kind::behaviour, "a file is given a second name", + "this volume does not claim it, which the enquiry reports"); + observe(kind::behaviour, + kal_fs_hard_link(here(), kName, length(kName), here(), kSecond, length(kSecond)) != kal_ok, + "a second name the enquiry did not claim is refused"); + kal_fs_remove(here(), kSecond, length(kSecond)); + kal_fs_remove(here(), kName, length(kName)); + } + } + // --- exclusion upon a range of a file, version 0.10 --------------------- // // THE OBSERVATION THAT TELLS THE TWO FORMS OF THIS APART NEEDS NO SECOND @@ -1071,7 +1133,7 @@ void run() { | kal::fs::local_endpoint | kal::fs::remove_open | kal::fs::resolved_name | kal::fs::attenuate | kal::fs::read_only | kal::fs::unchanging - | kal::fs::list_info).bits; + | kal::fs::list_info | kal::fs::hard_links).bits; observe(kind::abi, (kal_fs_props(here()) & ~assigned) == 0, "the capability word contains no position the specification has not assigned"); diff --git a/conformance/src/sections/process.cpp b/conformance/src/sections/process.cpp index 83e321b..d914569 100644 --- a/conformance/src/sections/process.cpp +++ b/conformance/src/sections/process.cpp @@ -115,6 +115,32 @@ void run() { } #endif + // EACH REQUEST IS COUNTED (version 0.25). A copy takes the requests over and + // is asked to end twice; the word changes at each, and is never zero again. +#ifdef MCPP_FEATURE_TIME + if (kal::process::has(kal::process::stop_requested) && kal::process::has(kal::process::terminate)) { + kal_process child{}; + if (start_copy_running("openkal-conformance-child", + argument_for(errand::count_requests), child)) { + kal_time_sleep(300u * 1000u * 1000u); + kal_process_terminate(child); + kal_time_sleep(300u * 1000u * 1000u); + kal_process_terminate(child); + int status = 0, terminated = 0; + kal_process_wait(child, &status, &terminated); + if (terminated != 0 || status == 98) + unobserved(kind::behaviour, "each request to end changes the word", + "a request to terminate did not arrive as a request on this environment"); + else + observe(kind::behaviour, status == 40, "each request to end changes the word"); + kal_process_close(child); + } + } else { + unobserved(kind::behaviour, "each request to end changes the word", + "the implementation does not claim prop_stop_requested and prop_terminate"); + } +#endif + // WHAT IS KNOWN OF A RUNNING PROGRAM (version 0.17). { kal_process self{}; diff --git a/conformance/src/sections/version.cpp b/conformance/src/sections/version.cpp index 265f406..66db0c2 100644 --- a/conformance/src/sections/version.cpp +++ b/conformance/src/sections/version.cpp @@ -2,6 +2,7 @@ module okc.version; import openkal.types; import openkal.version; +import openkal.env; import okc.report; import okc.spec; @@ -83,6 +84,60 @@ void run() { observe(kind::behaviour, agrees, "every interface this suite linked against is reported as present"); + // THE IMPLEMENTATION'S MANIFEST, kal_interfaces() AND ITS EXPORTED NAMES + // STATE ONE SET (0.25). A build system checks a program's requirements + // against the manifest's `provides-interfaces', and a loader checks an + // image's against kal_interfaces(); the two must therefore agree, or a + // program accepted by one is refused by the other. The runner reads the + // manifest and names its list here (tools/run-conformance.sh); the third + // statement, the exported names, is compared with the manifest by the same + // script, which can read the program's symbols and this program cannot. +#ifdef MCPP_FEATURE_ENV + { + static const char kVar[] = "OPENKAL_CONFORMANCE_PROVIDES"; + char list[1024]; + const kal_intptr n = kal_env_var(kVar, sizeof kVar - 1, list, sizeof list); + if (n > 0 && static_cast(n) < sizeof list) { + struct named { const char* name; kal_u64 bit; }; + static const named kNames[] = { + {"openkal.abort", kal::iface::abort_.bits}, {"openkal.stream", kal::iface::stream.bits}, + {"openkal.memory", kal::iface::memory.bits}, {"openkal.env", kal::iface::env.bits}, + {"openkal.time", kal::iface::time.bits}, {"openkal.random", kal::iface::random.bits}, + {"openkal.fs", kal::iface::fs.bits}, {"openkal.process", kal::iface::process.bits}, + {"openkal.task", kal::iface::task.bits}, {"openkal.exec", kal::iface::exec.bits}, + {"openkal.terminal", kal::iface::terminal.bits}, {"openkal.net", kal::iface::net.bits}, + {"openkal.datagram", kal::iface::datagram.bits}, {"openkal.space", kal::iface::space.bits}, + {"openkal.timeout", kal::iface::timeout.bits}, {"openkal.map", kal::iface::map.bits}, + {"openkal.region", kal::iface::region.bits}, {"openkal.prof", kal::iface::prof.bits}, + {"openkal.debug", kal::iface::debug.bits}, + }; + kal_u64 stated = 0; + bool known = true; + kal_uintptr i = 0; + while (i < static_cast(n)) { + while (i < static_cast(n) && (list[i] == ';' || list[i] == ' ')) ++i; + kal_uintptr b = i; + while (i < static_cast(n) && list[i] != ';' && list[i] != ' ') ++i; + if (i == b) break; + bool found = false; + for (const named& e : kNames) { + kal_uintptr k = 0; + while (e.name[k] && b + k < i && e.name[k] == list[b + k]) ++k; + if (e.name[k] == 0 && b + k == i) { stated |= e.bit; found = true; break; } + } + if (!found) known = false; + } + observe(kind::behaviour, known, + "every interface the manifest states is one the specification defines"); + observe(kind::behaviour, stated == present, + "kal_interfaces() states the interfaces the manifest states"); + } else { + unobserved(kind::behaviour, "kal_interfaces() states the interfaces the manifest states", + "the runner named no manifest list (OPENKAL_CONFORMANCE_PROVIDES)"); + } + } +#endif + if (performs(kind::abi)) { observe(kind::abi, kal_version() == v && kal_interfaces() == present, "the self-description does not change between calls"); diff --git a/conformance/src/suite.cpp b/conformance/src/suite.cpp index 9132601..072bcea 100644 --- a/conformance/src/suite.cpp +++ b/conformance/src/suite.cpp @@ -2,6 +2,7 @@ module okc.suite; import okc.spec; import okc.report; +import okc.protocol; import okc.version; import okc.abort; import okc.stream; @@ -25,36 +26,46 @@ import okc.debug; namespace okc { +namespace { + +// The cases a runner lists and selects (the mtest protocol): one per section, +// in the order the sections run. A section's case begins at its heading. +struct section_entry { const char* id; void (*run)(); }; +constexpr section_entry sections[] = { + { "openkal.version", version::run }, + { "openkal.stream", stream::run }, + { "openkal.memory", memory::run }, + { "openkal.env", env::run }, + { "openkal.time", time::run }, + { "openkal.random", random::run }, + { "openkal.fs", fs::run }, + { "openkal.process", process::run }, + { "openkal.task", task::run }, + { "openkal.exec", exec::run }, + { "openkal.terminal", terminal::run }, + { "openkal.net", net::run }, + { "openkal.datagram", datagram::run }, + { "openkal.space", space::run }, + { "openkal.timeout", timeout::run }, + { "openkal.map", map::run }, + { "openkal.region", region::run }, + { "openkal.prof", prof::run }, + { "openkal.debug", debug::run }, + { "openkal.abort", termination::run }, +}; + +} // namespace + int run_all() { - write_inventory(); + protocol::begin(); + if (protocol::listing()) { + for (const section_entry& e : sections) protocol::list_case(e.id); + return protocol::finish(0, 0, 0); + } - // The order is clause 3's, so that two runs against different - // implementations can be read side by side without either being sorted. - // openkal.abort is last among the core interfaces rather than first, - // because observing it requires starting a copy and the copy's own report - // would otherwise appear before this one's heading. - // First, because it is what a consumer asks before it uses anything, and - // because it is the one section no arrangement skips. - version::run(); - stream::run(); - memory::run(); - env::run(); - time::run(); - random::run(); - fs::run(); - process::run(); - task::run(); - exec::run(); - terminal::run(); - net::run(); - datagram::run(); - space::run(); - timeout::run(); - map::run(); - region::run(); - prof::run(); - debug::run(); - termination::run(); + write_inventory(); + for (const section_entry& e : sections) + if (protocol::wanted(e.id)) e.run(); return summarise(); } diff --git a/examples/portable/mcpp.toml b/examples/portable/mcpp.toml index e20c44c..689d300 100644 --- a/examples/portable/mcpp.toml +++ b/examples/portable/mcpp.toml @@ -9,7 +9,7 @@ name = "portable" version = "0.1.0" [dependencies] -openkal = "0.24.0" +openkal = "0.25.0" # AND THE VERSIONS HAVE TO FOLLOW THE SOURCE, which is the reason this example # is a criterion rather than an assumption. diff --git a/include/openkal/fs.h b/include/openkal/fs.h index 1ca2aac..89f7892 100644 --- a/include/openkal/fs.h +++ b/include/openkal/fs.h @@ -190,6 +190,8 @@ struct kal_node_info { * entry's size, time and * identity without a * further enquiry. 0.23 */ +#define KAL_FS_PROP_HARD_LINKS ((kal_uintptr)1u << 16) /* kal_fs_hard_link is + * answered here. 0.25 */ /* Positions in the flags word of kal_fs_open. */ #define KAL_OPEN_READ ((kal_uintptr)1u << 0) @@ -583,6 +585,34 @@ int kal_fs_link_create(struct kal_dir base, const char* name, kal_uintptr kal_intptr kal_fs_link_read (struct kal_dir base, const char* name, kal_uintptr len, char* out, kal_uintptr cap); +/* A second name for a file. Version 0.25. + * + * `kal_fs_hard_link' makes `b' in `to' name the node that `a' in `from' names, + * so that both names reach one file: the same content, the same identity + * (KAL_INFO_IDENTITY), and a file that remains while either name does. It is the + * operation `link' and `linkat' perform, and the one `CreateHardLinkW' performs; + * a C library's `link' and a C++ library's `create_hard_link' are written above + * it. Clause 11 entry 59. + * + * IT IS AN OPERATION OF THIS INTERFACE FOR THE REASON THE OPERATIONS ON LINKS + * ARE: whether a node may have several names is a property of the volume's + * format, so kal_fs_props of `to' answers it with KAL_FS_PROP_HARD_LINKS, and a + * request upon a volume that does not claim it is kal_err_not_supported and makes + * nothing. + * + * `a' IS NOT RESOLVED: a name that is a link (kal_node_link) receives a second + * name itself, as kal_fs_remove and kal_fs_rename act upon the name and not upon + * what it leads to. The components before the last are resolved as everywhere. + * + * Refusals, each of which makes nothing: kal_err_not_found where `a' names + * nothing; kal_err_is_directory where it names a directory, which no environment + * this specification targets gives a second name; kal_err_exists where `b' + * already names a node; kal_err_not_supported where `from' and `to' are on two + * volumes, since a name is an entry of one volume's directory and cannot reach a + * node of another. */ +int kal_fs_hard_link(struct kal_dir from, const char* a, kal_uintptr alen, + struct kal_dir to, const char* b, kal_uintptr blen); + /* The name `name' refers to with every link in it followed: absolute, in the * environment's spelling, as kal_env_image states a name. Writes min(cap, * length) bytes at `out' and returns the length, or the negated error: diff --git a/include/openkal/process.h b/include/openkal/process.h index ff190f4..ddd71f3 100644 --- a/include/openkal/process.h +++ b/include/openkal/process.h @@ -402,6 +402,16 @@ void kal_process_close(struct kal_process); * Zero until then, non-zero afterwards, and never cleared. Null where an * implementation does not claim KAL_PROCESS_PROP_STOP_REQUESTED. * + * IT COUNTS THE REQUESTS. Version 0.25. Each request that reaches the program + * adds one to the word, and an addition that would make it zero makes it one, + * so the word is never zero again once a request has arrived. A program that + * survives a request and goes on --- an interactive program to which every + * interrupt keystroke is a request to abandon the current command --- compares + * the word with the value it last saw and learns of each request, and one that + * waits with `kal_task_wait' upon the value it saw is woken by the next. Before + * 0.25 the word was set to a non-zero value once, and a second request could + * not be told from none. Clause 11 entry 60. + * * THIS IS NOT A SIGNAL INTERFACE, AND THE DIFFERENCE IS WHY IT CAN EXIST AT * ALL. Signals are one environment's mechanism: a numbered set, a disposition per * program, a handler that interrupts whatever was running. This system has them; diff --git a/include/openkal/version.h b/include/openkal/version.h index 369f489..0801ff6 100644 --- a/include/openkal/version.h +++ b/include/openkal/version.h @@ -42,7 +42,7 @@ * for the reason tools/check-readme-versions.sh exists --- the one fact a * consumer actually asks for was checked by nobody. */ #define KAL_VERSION_MAJOR 0u -#define KAL_VERSION_MINOR 24u +#define KAL_VERSION_MINOR 25u #define KAL_VERSION_PATCH 0u #define KAL_VERSION_MAKE(major, minor, patch) \ diff --git a/kit/mcpp.toml b/kit/mcpp.toml index 4ef64db..0412d1a 100644 --- a/kit/mcpp.toml +++ b/kit/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-kit" -version = "0.2.1" +version = "0.3.0" description = "Facilities composed from openkal's interfaces. Not part of the specification, and structurally incapable of being mistaken for it." license = "Apache-2.0" authors = ["mcpplibs"] @@ -67,7 +67,7 @@ kind = "lib" # published packages, which is the only place this could be observed: no # continuous integration in this ecosystem resolves a published package, # because they all substitute working trees by design. -openkal = "0.24.0" +openkal = "0.25.0" # NO `include_dirs` HERE EITHER. It named `../include`, which reaches out of # this package into whatever happens to sit above it. With the specification diff --git a/mcpp.toml b/mcpp.toml index d80a62b..72da29f 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal" -version = "0.24.0" +version = "0.25.0" description = "openkal: a portable kernel ABI specification. This package carries the normative declarations; implementations are separate packages." license = "Apache-2.0" authors = ["mcpplibs"] diff --git a/src/fs.cppm b/src/fs.cppm index 625d9b2..3c6589d 100644 --- a/src/fs.cppm +++ b/src/fs.cppm @@ -63,6 +63,7 @@ export using ::kal_fs_props; export using ::kal_fs_max_name; export using ::kal_fs_link_create; export using ::kal_fs_link_read; +export using ::kal_fs_hard_link; export using ::kal_fs_resolved_name; export using ::kal_fs_list_next_info; export using ::kal_fs_dir_attenuate; @@ -141,6 +142,8 @@ inline constexpr props attenuate{KAL_FS_PROP_ATTENUATE}; inline constexpr props read_only{KAL_FS_PROP_READ_ONLY}; inline constexpr props unchanging{KAL_FS_PROP_UNCHANGING}; inline constexpr props list_info{KAL_FS_PROP_LIST_INFO}; +// A file may be given a second name (kal_fs_hard_link). Version 0.25. +inline constexpr props hard_links{KAL_FS_PROP_HARD_LINKS}; // What kal_fs_dir_attenuate takes away or states (0.23). struct dir_right_tag; diff --git a/src/macros.cppm b/src/macros.cppm index 041e3ab..7d46c2a 100644 --- a/src/macros.cppm +++ b/src/macros.cppm @@ -59,6 +59,7 @@ inline constexpr kal_uintptr KAL_FS_PROP_ATTENUATE_M = (kal_uintptr)( inline constexpr kal_uintptr KAL_FS_PROP_CAPACITY_M = (kal_uintptr)(KAL_FS_PROP_CAPACITY); inline constexpr kal_uintptr KAL_FS_PROP_CASE_SENSITIVE_M = (kal_uintptr)(KAL_FS_PROP_CASE_SENSITIVE); inline constexpr kal_uintptr KAL_FS_PROP_EXECUTABLE_M = (kal_uintptr)(KAL_FS_PROP_EXECUTABLE); +inline constexpr kal_uintptr KAL_FS_PROP_HARD_LINKS_M = (kal_uintptr)(KAL_FS_PROP_HARD_LINKS); inline constexpr kal_uintptr KAL_FS_PROP_LINKS_M = (kal_uintptr)(KAL_FS_PROP_LINKS); inline constexpr kal_uintptr KAL_FS_PROP_LIST_INFO_M = (kal_uintptr)(KAL_FS_PROP_LIST_INFO); inline constexpr kal_uintptr KAL_FS_PROP_LOCAL_ENDPOINT_M = (kal_uintptr)(KAL_FS_PROP_LOCAL_ENDPOINT); diff --git a/tools/run-conformance.sh b/tools/run-conformance.sh index e300fd7..457796a 100755 --- a/tools/run-conformance.sh +++ b/tools/run-conformance.sh @@ -259,7 +259,54 @@ export OPENKAL_CONFORMANCE_EMPTY="" # so the copy is started by the same mechanism as the original. runner="${OPENKAL_CONFORMANCE_RUNNER:-}" +# THE INTERFACES THE IMPLEMENTATION'S MANIFEST STATES (0.25). +# +# Three statements of one set exist: the manifest's `provides-interfaces', which +# a build system checks a program's requirements against; kal_interfaces(), +# which a loader checks an image's against; and the names the implementation +# exports. The suite compares the second with the first (it is told the list +# here, because it cannot read a manifest); this script compares the third with +# the first, because it can read the program's symbols and the suite cannot. +# A manifest that states no list is reported and not compared: a provider that +# states nothing is not one that provides nothing. +manifest_list="$(awk ' + /^[[:space:]]*provides-interfaces[[:space:]]*=/ { on = 1 } + on { line = line $0; if ($0 ~ /\]/) { on = 0 } } + END { gsub(/.*\[/, "", line); gsub(/\].*/, "", line); gsub(/[" \t]/, "", line); gsub(/,/, ";", line); print line } +' "$implementation/mcpp.toml.orig")" +if [ -n "$manifest_list" ]; then + export OPENKAL_CONFORMANCE_PROVIDES="$manifest_list" + exported="$(bash "$here/tools/check-surface.sh" --interfaces "$here/SURFACE.txt" "$binary" 2> /dev/null \ + | sort -u | tr '\n' ';' | sed 's/;$//' || true)" + stated="$(printf '%s' "$manifest_list" | tr ';' '\n' | sort -u | tr '\n' ';' | sed 's/;$//')" + if [ -z "$exported" ]; then + echo "--- the exported names could not be read here; the manifest's list is not compared with them ---" + elif [ "$exported" = "$stated" ]; then + echo " ok the exported names state the interfaces the manifest states" + else + echo " no the exported names state $exported and the manifest states $stated" >&2 + exit 1 + fi +else + echo "--- $package states no provides-interfaces; the three statements are not compared ---" +fi + # The suite's own exit status is the verdict: 0 when every observation held, 1 # when one did not, 2 when nothing was observed. The last is the one a run that # selected no interface would otherwise pass silently. -$runner "./$binary" +# BY THE TEST PROTOCOL WHERE A RUNNER IS NAMED (MTEST, an mtest engine). The +# suite reports each interface as a case of kind `cts' and each observation as +# a case within it, and the engine keeps the run in its store (`.mtest' here), +# from which a report is written. One job: the suite is one sequential program +# whose sections share the working directory's names, so its cases are not +# divided among processes. The verdict is the engine's: 0 when every case +# passed. Without MTEST the suite runs as it always has and its exit status is +# the verdict. +if [ -n "${MTEST:-}" ]; then + # bash 3.2 (macOS) treats an empty array as unset under `set -u', hence the +form below. + under=() + [ -n "$runner" ] && under=(--run-under "$runner") + "$MTEST" run --jobs 1 --group "${MTEST_GROUP:-${GITHUB_RUN_ID:-local}}" ${under[@]+"${under[@]}"} -- "./$binary" +else + $runner "./$binary" +fi