diff --git a/CHANGELIST.md b/CHANGELIST.md index 57ceddb962..3b778e1544 100644 --- a/CHANGELIST.md +++ b/CHANGELIST.md @@ -44,7 +44,7 @@ The headline of this release: `modules/dasLLAMA`, a complete LLM inference engin #### dasLLAMA: Text-to-Speech (#3929, #3985, #3992, #3933) -daslang speaks: KittenTTS nano/mini and Kokoro-82M from a das-native front end (a G2P rail with a packed lexicon, a POS tagger, and the JIT's aarch64 vector-math rail underneath) (#3929); Pocket TTS as the third family - voice cloning, six languages, published Q8_0 files, the server and a tutorial (#3985); its small form - the K-quant lane, a 75 MB English file, storywish on one voice, the parrot example (#3992); the control page speaks, the front end speaks British, and a pack the worker cannot load degrades the boot instead of killing it (#3933). +daslang speaks: KittenTTS nano/mini and Kokoro-82M from a das-native front end (a G2P rail with a packed lexicon, a POS tagger, and the JIT's aarch64 vector-math rail underneath) (#3929); Pocket TTS as the third family - voice cloning, six languages, published Q8_0 files, the server and a tutorial (#3985); its small form - the K-quant lane, a 75 MB English file, storywish on one voice, the parrot example (#3992); the control page speaks, the front end speaks British, and a pack the worker cannot load degrades the boot instead of killing it (#3933). Portable Pocket voices (#4055) - a reusable prompt format with preparation, validation, registration and a serialization tutorial, so an encoder-free Pocket model speaks a compatible prepared voice; the same change stages a wasm release's declared external files into its bundle and fixes touch-only browsers (Pointer Lock guarded, tap coordinates queued before button events) - browser applications need a rebuild for the input fixes. #### dasMetal: Native Metal Compute + the dasLLAMA Metal Tier (#3443, #3459, #3474, #3484, #3496, #3509, #3515, #3519, #3521, #3526, #3527, #3530, #3533, #3535, #3546, #3554, #3558, #3562, #3653, #3663, #3754, #3763) @@ -201,6 +201,7 @@ A native glTF 2.0 loader with an OpenGL PBR renderer, viewer, and capstone tutor - **Pure-das Dear ImGui renderer** (#3283) and its compile-time WebGL2 draw path + `get_running_platform_name` (#3294); strudel audio in the playground (#3272); embedded fonts (#3292); canvas-CSS-size/DPR + `glfw_toggle_fullscreen` externs for responsive, fullscreen-capable cards (#3323, #3326) - **Static-link gates** (#3351, #3358) - `examples/fatman` links every external C++ module into one host with a CI lane (the ABI canary for daslang-vs-externals drift), and its web sibling builds the imgui family from source into the threaded wasm playground - **The browser tier's K-quant dot, FMA, spinning workers, and a mixer off the host clock** (#4045) - `dot_k4q8` rides the `idot` lattice op (both nibbles into a signed `byte16`, one body, no target branch; 1.1x -> 1.7x real time for Pocket TTS on the parrot page, 1.90x on x64, bit-exact), `+relaxed-simd` joins the wasm feature string so a contracted multiply-add becomes `f32x4.relaxed_madd` instead of splitting (8728 contractions across the module), the job queue's spin window reads the clock once per stride and pauses with no JS in it (pool clock time 25 s -> 5 s per 15 s window), and a browser's workers take the same 30 ms spin window as every other target (1.7x -> 2.3x); dasAudio's browser mixer parks on a drain counter the worklet bumps instead of a sleep, primes a 20 ms ring before the device starts, and counts underruns (`sound_playback_underrun_frames`) +- **A native WebAssembly SIMD128 vecmath backend** (#4052) - `include/vecmath/dag_vecMath_wasm.h` replaces the SSE2 arm run through emscripten's compat layer, where a float-to-int conversion was a per-lane `lrint` loop (86 wasm instructions for `v_cvti_vec4i`, 170 for `v_cvt_roundi`, 91 for `v_floor`) and min, max, select, abs and mul+add each cost two to four ops; the new arm answers with the ISA's own instructions (saturating truncation, floor/nearest, pmin/pmax with the SSE NaN and signed-zero rule, bitselect), the web build and the dasImgui wasm archives drop `-msse2`, relaxed SIMD stays off by default (WebKit implements none of the opcodes), and the backend battery builds as node-runnable pairs the wasm lane runs. On the web `int(x)` of a float outside the 32-bit range now saturates (NaN gives 0) instead of INT_MIN - the NEON contract. Interpreter under node: the float-to-int/floor/round loop 26.4 -> 11.9 ms - **std140 on WebGL2** (#3273, #3277, #3278) - a UBO pack binder off a shared layout walk (mat3 and composites), buffer release, and the glsl emitter indexing a runtime matrix/vector/array column as `base[index]` - **The browser lifecycle** (#3900, #4000, #4001, #4010) - both browser drivers collect the heap between frames; the wasm job pool floors at four workers so a browser reporting two cores runs the examples on more than one; arcanoid on the web with the worker heap, the cross-target layouts, CRT scanlines by pixel pitch, audio surviving live reloads, and dasbind proxies for cache-served registrars @@ -256,7 +257,7 @@ An MCP bridge for DAP debugging (#3937) - launch, breakpoints, stepping, scopes, #### The Document System: `[arch]` Citations and `REVIEW.das` Gates (#3882, #3887, #3766, #3914, #3896) -`REVIEW.md` carries bans and duties that bind a diff, `ARCHITECTURE.md` carries present-tense statements, one grammar under `REVIEW_COMMON.md` / `ARCHITECTURE_COMMON.md` (#3882). `[arch(at="doc.md#anchor")]` makes code cite the section it implements and LINT026 keeps every anchor cited and every citation resolving (#3887; mandatory across the Vulkan tier in #3896; the C++ pointer form in #4047). A checklist's mechanical half becomes an executable `REVIEW.das` gate (#3766). SDK bundles ship generated excerpts of the cited sections so LINT026 stays armed in an installed tree (#3914). +`REVIEW.md` carries bans and duties that bind a diff, `ARCHITECTURE.md` carries present-tense statements, one grammar under `REVIEW_COMMON.md` / `ARCHITECTURE_COMMON.md` (#3882). `[arch(at="doc.md#anchor")]` makes code cite the section it implements and LINT026 keeps every anchor cited and every citation resolving (#3887; mandatory across the Vulkan tier in #3896; the C++ pointer form in #4047). A checklist's mechanical half becomes an executable `REVIEW.das` gate (#3766). SDK bundles ship generated excerpts of the cited sections so LINT026 stays armed in an installed tree (#3914). The mechanical auditors (tdd, review-md, style-hygiene, placement, dupe, harvester, janitor, spartan, analysis-bot) pin high effort, skip CLAUDE.md, and gather what they read in dependency order before judging - a 33-run replay showed 43% of steps reading one file at a time; grouping cut steps 39%, cost 25%, time 38% (#4056). #### Telegram Dictation Bot + Cadmus (#3408, #3411, #3465, #3482, #3483, #3491, #3517, #3531) @@ -323,6 +324,7 @@ An MCP bridge for DAP debugging (#3937) - launch, breakpoints, stepping, scopes, - **The JIT closes on the AOT** (#3732, #3868, #3880, #3917, #3981, #3999) - `each()`, generator ranges, array push and indirect dispatch match; binds are NTTP and the bind-flavor gate reaches the module; a handled type iterates and indexes through its annotation; hot-path wins, `[jit]` in a C++ AOT host, unresolved AOT-object address globals; `--jit-debug` describes das locals and a das name is what a debugger breaks on; the runtime entry points get their own file - **Sealed lookup tables** (#3973) - `Context` finds functions and globals by name and by mangled hash through perfect-hash tables; standalone exes and AOT contexts adopt emitter-sealed constant data - **Embedded hosts enable the JIT through the C API** (#3889) - and the macOS in-memory JIT arm is fixed; **a statement after a terminator parks in its own dead block** (#3964) +- **The LLVM-AOT rail links** (#4046) - a `no_jit` dependency stops folding a zero into the semantic hash (`linkCppAot` skipped the hash pass the emitter took), an AOT-consuming run is stamp-free whether or not the JIT is on (the object emitter baked its own tune sidecar into the artifact), every function of an in-memory module carries the host cpu and feature string so the subtarget it is selected under matches the gates that emitted it (`test_lattice_idot` aborted on `AArch64ISD::SDOT`), and the emitter disposes the target triple LLVM hands it - 12836 green under `test_llvm_aot -jit --run` #### Runtime, Tooling, and Hosting @@ -377,6 +379,7 @@ An MCP bridge for DAP debugging (#3937) - launch, breakpoints, stepping, scopes, ### Bug Fixes +- **The driver forwards `-h` / `--help` placed after `--`** (#4057) - it answered them with its own banner wherever they sat, so no interpreted clargs tool could print its help; the release validation catches up with the tools since RC2 in the same change (the bundle smoke compiles and launches the DAP bridge, the static watchdog's `--help` and `--lsp` fronts, the LLVM-AOT driver and the LSP subtools; the release audit's rows carry an expected exit code, the telegram rows become the crash example's local native package, the compile cap is 180 s) - **Side-effect inference misses that DCE'd real writes** - writes through forwarded `const?` args (#3313, fixes #3311) and through pointer-offset args (#3324, fixes #3321) are now recorded - **`E(a=1)` on an enum silently folded to the zero enumerant** (#3427, fixes #3426) - **Block variables hoisted correctly in blocks with `finally`** (#3367) @@ -436,6 +439,7 @@ An MCP bridge for DAP debugging (#3937) - launch, breakpoints, stepping, scopes, - **A hung sweep names the program that hung** (#3949) - dastest prints a timestamped `run N/M: ` before every program of a sequential sweep and `worker K starts: ` before every isolated batch, on stderr, flushed at once and printed under `--failures-only` too; the `run_tests_*` cmake targets carry `USES_TERMINAL`, so Ninja streams the sweep's output instead of holding it until the command ends - a lane cancelled mid-hang keeps its last line instead of losing everything the sweep printed. The first hang those lines named was `tests/debug_agent/test_callback_threadlock.das`: its hook waited on a thread that had yet to create its context, while the tick holds the agent registry every context creation needs - `DapiDebugAgent` carries the contract, and the test now proves the callback/pinvoke serialization from one thread with the new `try_invoke_in_context`, the non-blocking `invoke_in_context` that returns false, running nothing, while another thread holds the context lock - **Bounded apt on ubuntu runners** (#3785) - every workflow apt site goes through `ci/apt_install.sh`: the degrading Azure mirror swapped for the public archive (mirrorlist included), capped fetches, retried update - the worst case becomes a loud failure in minutes instead of a silent hours-long hang - **Gate rosters catch up with the tree** (#4047) - `MESHOPTIMIZER.LICENSE` joins the bundle smoke's license roster (the module is default-on and its MIT notice installed, but the gate that exists to notice a silently-skipped install rule did not list it); preflight's compile sweep stops trying `examples/dasLLAMA/library/main.das`, whose `dasllama_lib_c` only the opt-in `dasllama_lib_jit` target writes (#4045); `tests-cpp`'s `standalone_ctx` fixture pins its output directory with `$<1:...>` so `standalone_capi_dasbind` passes under Visual Studio and Xcode generators (#4045) +- **The nightly goes green, and a PR that closes a nightly-failure issue runs the nightly** (#4046) - the LLVM-AOT rail's five causes fixed (above), `run_tests_llvm_aot` walks the curated corpus through `dastest --test-list` instead of all of `tests/`, the standalone sweeps hand their ~900-path file list to cmake in a file (one 70 KB `-DFILES=` argument overran cmd.exe's line limit), the RelWithDebInfo lane's `test_worker_heap` ceiling widens for a free-list-off allocator (10248 KB against a 10240 KB cap), the tsan lane's two plain-global writes in `Module::Initialize` (the fusion hooks, the deferred loader) become a `once_flag` and an atomic; `pre_job` resolves a PR's closing issues and labels into one flag that arms the nightly-only jobs and steps on that PR, while the issue-filing job still gates on `schedule` alone - **The nightly benchmark stand** (#3976) - one box runs every `[benchmark]` in the tree and plots each arm over commits; `benchctl`'s `run` and `report` verbs, a static viewer with no build step - **The SDK payload** (#3563, #3571, #3857, #3914, #3963) - the 0.6.4 version bump; the install ships what the docs promise and the smoke test stops hiding a hole; every third-party license ships; generated `[arch]` excerpts; the generated binding tables `aot_builtin_ast.h` includes diff --git a/ci/smoke_test_bundle.sh b/ci/smoke_test_bundle.sh index 5fd1593ffb..8c974de094 100644 --- a/ci/smoke_test_bundle.sh +++ b/ci/smoke_test_bundle.sh @@ -68,20 +68,26 @@ cd "$BUNDLE" # running it: nothing ever compiled it, so nothing noticed. COMPILE_TESTS=( "aot|utils/aot/main.das" + "aot-llvm|utils/aot/main_llvm_aot.das" "benchctl|utils/benchctl/main.das" + "dap|utils/dap/main.das" "das-fmt|utils/das-fmt/dasfmt.das" "dascov|utils/dascov/main.das" "dasllama-convert|utils/dasllama-convert/main.das" "dasllama-server|utils/dasllama-server/main.das" + "dasllama-server-bench|utils/dasllama-server/server_bench.das" "daspkg|utils/daspkg/main.das" "detect-dupe|utils/detect-dupe/main.das" "fix-lint-errors|utils/fix-lint-errors/main.das" "gen1-to-gen2|utils/gen1-to-gen2/main.das" "jobque-timeline|utils/jobque-timeline/main.das" "lint|utils/lint/main.das" + "lsp-nav|utils/lsp/subtools/nav.das" + "lsp-validate|utils/lsp/subtools/validate.das" "mcp|utils/mcp/main.das" "mcp-cpp|utils/mcp/cpp_main.das" "mcp-setup|utils/mcp/setup.das" + "watchdog-das|utils/watchdog/main.das" # Not an entry point, but the library an adopting repo's REVIEW.das requires # (REVIEW_COMMON.md contract) — a bundle where it does not compile breaks # every external gate. @@ -108,6 +114,7 @@ COMPILE_TESTS=( # user's box. SHIPPED_EXE_TESTS=( "daslang-live|cpp" + "watchdog|cpp" "benchctl|dasexe" "dascov|dasexe" "das-fmt|dasexe" @@ -117,11 +124,6 @@ SHIPPED_EXE_TESTS=( "lint|dasexe" ) -# Stdio launch test for the mcp JSON-RPC server (run from source via daslang — -# the exe is no longer bundled): the only safe "did it actually start" probe is -# to feed empty stdin and check for a clean exit. It prints "Starting daslang -# MCP server" then "stdin closed, shutting down" within ~1s. - PASS=0 FAIL=0 LOG="$(mktemp)" @@ -217,8 +219,17 @@ done echo echo "Runtime launch:" +# A stdio JSON-RPC server (the MCP server and the DAP bridge run from source, the watchdog's +# LSP front in the static exe) is started on empty stdin: a clean exit is the only safe +# did-it-start probe. The watchdog's own help is captured, not piped, so its exit code counts. run_check "mcp.das (empty stdin)" bash -c \ "'$DASLANG' utils/mcp/main.das < /dev/null" +run_check "dap.das (empty stdin)" bash -c \ + "'$DASLANG' utils/dap/main.das < /dev/null" +run_check "watchdog --help" bash -c \ + "out=\"\$('$BUNDLE/bin/watchdog${CPP_SUFFIX}' --help)\" && printf '%s' \"\$out\" | grep -q -- '--stable-seconds'" +run_check "watchdog --lsp (empty stdin)" bash -c \ + "'$BUNDLE/bin/watchdog${CPP_SUFFIX}' --lsp < /dev/null" # Two prebuilt tools past --help. dastest.exe must compile and run a shipped suite # (isolated mode also spawns its own workers); lint.exe over daslib must resolve every diff --git a/tests/fio/_fixture_echo_args.das b/tests/fio/_fixture_echo_args.das new file mode 100644 index 0000000000..4db3e52552 --- /dev/null +++ b/tests/fio/_fixture_echo_args.das @@ -0,0 +1,11 @@ +options gen2 + +require daslib/clargs + +//! one argument per line - tests/fio/popen_argv.das matches on these lines +[export] +def main() { + for (a in get_cli_arguments()) { + print("{a}\n") + } +} diff --git a/tests/fio/popen_argv.das b/tests/fio/popen_argv.das index 97f7b79791..7edf3425d3 100644 --- a/tests/fio/popen_argv.das +++ b/tests/fio/popen_argv.das @@ -55,6 +55,20 @@ def test_popen_argv_unknown_flag_nonzero_exit(t : T?) { } } +[test] +def test_help_after_separator_reaches_the_script(t : T?) { + t |> run("--help and -h after -- are script arguments, not the driver's") @(t : T?) { + let fixture = path_join(get_das_root(), "tests/fio/_fixture_echo_args.das") + var output : string + let rc = run_argv([das_exe(), fixture, "--", "--help", "-h"], output) + t |> equal(rc, 0) + t |> success(find(output, "--help\n") >= 0, "the script should see --help: {output}") + t |> success(find(output, "-h\n") >= 0, "the script should see -h: {output}") + t |> success(find(output, "daslang version") < 0, + "the driver must not answer a help flag placed after --: {output}") + } +} + [test] def test_popen_argv_unix_dollar_paren_not_expanded(t : T?) { t |> run("argv with $(...) is passed literally, not expanded by a shell") @(t : T?) { diff --git a/utils/REVIEW.md b/utils/REVIEW.md index a40032dfac..821b989a88 100644 --- a/utils/REVIEW.md +++ b/utils/REVIEW.md @@ -5,12 +5,17 @@ doc: `CLAUDE.md` (repo root). A tool is a directory holding a program and the files only that program uses. A directory is a tool when `utils/CMakeLists.txt` (beside this file) or the repo root's `CMakeLists.txt` -builds or ships a program in it, wherever that directory sits; a directory under `utils/` is +builds or ships a program in it, or when `utils/CMakeLists.txt` runs a `DAS_UTILS_TO_TEST` +suite against a program in it, wherever that directory sits; a directory under `utils/` is also a tool when its `.das_package` declares a program with `release_main` (its own) or -`release_program` (a companion). A change under `common/` (beside this file) is a change to -every tool that requires it. An arm is one test case of a tool's suite - a `t |> run(...)` case -of a `[test]` function. An arm's load-bearing assertions are the ones that prove the change, -never a skip-path assertion. +`release_program` (a companion). A tool is shipped when `cmake --install` puts it in the +bundle: a `DAS_UTILS_SHIPPED_EXES` entry or a C++ executable target in `utils/CMakeLists.txt` +(beside this file), a C++ executable target in the repo root's `CMakeLists.txt`, or an entry +point an `install(FILES ...)` rule in the repo root's `CMakeLists.txt` copies as source. A +change under `common/` (beside this file) is a change to every tool that requires it. An arm +is one test case `dastest` runs - a `[test]` function, or one `t |> run(...)` case inside one. +An arm's load-bearing assertions are the ones that prove the change, never a skip-path +assertion. A CI row is a workflow step whose command runs the arm, directly or through a process it spawns. An assertion no CI row can run is one where either no CI row runs the arm, or the arm returns or skips before the assertion. An arm that skips unless a host tool is present has @@ -58,3 +63,8 @@ A run against an already-deployed artifact proves nothing about the diff under r **A diff that adds or renames a key in a `watchdog.json` that belongs to a tool, wherever the tool sits, names a `WatchdogConfig` field in `watchdog/watchdog.das`, in the same change** - the supervisor refuses to start on an unknown key. + +**A diff that adds a front to a shipped tool adds a `run_check` row starting that front to +`ci/smoke_test_bundle.sh` (repo root), in the same change.** A front is a command-line mode +that makes a program serve a different role or protocol. The bundle gate proves only the +fronts it starts. diff --git a/utils/daslang/main.cpp b/utils/daslang/main.cpp index ab020798c0..ce63351fcb 100644 --- a/utils/daslang/main.cpp +++ b/utils/daslang/main.cpp @@ -1120,7 +1120,7 @@ int MAIN_FUNC_NAME ( int argc, char * argv[] ) { dumpLeaks = true; } else if ( cmd=="-no-dump-leaks" ) { dumpLeaks = false; - } else if ( cmd=="h" || cmd=="-help" ) { + } else if ( !scriptArgs && (cmd=="h" || cmd=="-help") ) { print_help(); return 0; } else if ( !scriptArgs) { diff --git a/utils/internal/test-release/README.md b/utils/internal/test-release/README.md index 8d40518c33..521adf7fd3 100644 --- a/utils/internal/test-release/README.md +++ b/utils/internal/test-release/README.md @@ -28,7 +28,9 @@ passes. `tutorials/`, and `modules/*/{examples,tutorials}` compiles with `/bin/daslang -compile-only`, per-file timeout, parallel self-spawned workers (`utils/common/parallel_workers`). No execution, so windows/devices/models never engage; - the timeout covers the one thing that can still spin - compile-time macro work. + the timeout covers the one thing that can still spin - compile-time macro work. Its + default is 180 s: the bare `-compile-only` never reads the module cache, a dasLLAMA + example compiles cold in about 40 s alone, and a full worker set slows every file. - **utils** (implemented, `utils_phase.das`) - every shipped tool exercised in its capacity, from the bundle alone: the shipped suites (`utils/{common,lint,dascov,find-dupe, jobque-timeline}`) through the shipped runner, then `bin/dastest.exe` on one of them @@ -36,16 +38,27 @@ passes. the shipped `daslib` (the tree is at zero and formatter-clean, and stays so); the analysis tools on their shipped fixtures (`detect-dupe` on `utils/detect-dupe/fixture`, `dascov` on its test script, `benchctl` reset/query, `aot` emitting `hello_world`); the LSP - subtools on the MCP fixtures; `daslang-live` on `examples/daslive/test_api`. Every row + subtools on the MCP fixtures; the DAP bridge's argument gate; both watchdog forms' + `--help`; `daslang-live` on `examples/daslive/test_api`. Every row is exit code plus substrings only a working run prints - `0 issue(s), 0 error(s)`, `N tests, N passed, 0 failed`, `Verified!` - never rc alone. With `--network`, `daspkg` installs the packages the gated examples need (sequence -> das-cards, the daspkg tutorial - projects, telegram, the local C/C++ build example), proves the unlock by compiling or + projects, the crash example, the local C/C++ build example), proves the unlock by compiling or running the example, and `daspkg cleanup` returns each dir to shipped state. Rows are the verified command lines from the RC1 utils audit; `--only ` runs a subset. - **run** (planned) - console-pure allowlist executed with timeout, rc==0 required. - **headless** (planned) - dasImgui apps through the harness headless arm. +## Row shapes {#utils-row-shapes} + +A tool with no help flag has no front to launch, so its row proves the tool through a refusal: +the DAP bridge is a stdio server whose only reachable surface is its argument parser, and its +row passes an unknown flag with `expect_rc` naming the code that parser exits with. + +`network = true` marks a row that mutates the bundle's example dirs, which is not the same as +one that downloads: the crash example's `dascrash` package resolves from a local path and +builds through cmake, so that row proves the shipped C++ build kit with nothing fetched. + ## expected_compile.txt / expected_utils.txt ` `, one per line, `#` comments. Discipline: an entry is a diff --git a/utils/internal/test-release/expected_compile.txt b/utils/internal/test-release/expected_compile.txt index 9250981a8c..66aa2cb312 100644 --- a/utils/internal/test-release/expected_compile.txt +++ b/utils/internal/test-release/expected_compile.txt @@ -1,7 +1,5 @@ # test-release expected compile failures — a RULING per line, not a mute button. -# --- ship defects awaiting repo fixes (RC2) --- - # --- daspkg-package tier: compile only after `daspkg install` (planned daspkg phase) --- examples/benchmarks/sql/_common.das needs duckdb/postgres provider packages examples/benchmarks/sql/_sql_families.das needs duckdb/postgres provider packages @@ -19,7 +17,8 @@ examples/daspkg/daspkg-version-2/main.das daspkg tutorial project — resolves a examples/daspkg/packages/daspkg-example-mixed/daslib/smooth.das daspkg tutorial package internals examples/games/sequence/main.das needs the cards package -# --- integration-scaffold tier: the tutorial has the user BUILD the native module --- +# --- integration-scaffold tier: the file requires a native module its own example or tutorial build writes --- +examples/dasLLAMA/library/main.das requires dasllama_lib_c, written by the opt-in dasllama_lib_jit target the example's README builds tutorials/integration/c/03_binding_types.das requires the native scaffold the tutorial builds tutorials/integration/c/04_callbacks.das requires the native scaffold the tutorial builds tutorials/integration/c/05_unaligned_advanced.das requires the native scaffold the tutorial builds diff --git a/utils/internal/test-release/main.das b/utils/internal/test-release/main.das index 49c0345ad1..90d8494f40 100644 --- a/utils/internal/test-release/main.das +++ b/utils/internal/test-release/main.das @@ -22,7 +22,7 @@ struct Config { bundle : string @clarg_doc = "Seconds allowed per file" - timeout : float = 60.0 + timeout : float = 180.0 @clarg_doc = "Worker processes (0 = auto)" jobs : int = 0 diff --git a/utils/internal/test-release/test_utils_phase.das b/utils/internal/test-release/test_utils_phase.das index 85d3160625..1e0e029b8e 100644 --- a/utils/internal/test-release/test_utils_phase.das +++ b/utils/internal/test-release/test_utils_phase.das @@ -51,6 +51,13 @@ def test_judged_rc_and_head(t : T?) { t |> equal("expected `Verified!` in output", judged_head(row, 0, "Complete! 3 files")) t |> equal("Complete! 3 files", judged_head(row, 2, "Complete! 3 files"), "a real failure shows the child's output") t |> equal("Verified! 3 files", judged_head(row, 0, "Verified! 3 files")) + let refusal <- ToolCheck(name = "r", want <- ["unknown argument"], expect_rc = 2) + t |> equal(0, judged_rc(refusal, 2, "tool: unknown argument --x"), "the expected refusal code with the oracle met is a pass") + t |> equal(1, judged_rc(refusal, 2, "tool: started"), "the expected code with the oracle unmet becomes 1") + t |> equal(1, judged_rc(refusal, 0, "tool: started"), "exit 0 where a refusal was expected is a failure") + t |> equal(3, judged_rc(refusal, 3, "unknown argument"), "another non-zero exit keeps its code") + t |> equal("expected exit 2, got 0", judged_head(refusal, 0, "tool: started")) + t |> equal("expected `unknown argument` in output", judged_head(refusal, 2, "tool: started")) } [test] diff --git a/utils/internal/test-release/utils_phase.das b/utils/internal/test-release/utils_phase.das index 2c2bb4f3fd..4c519b2b97 100644 --- a/utils/internal/test-release/utils_phase.das +++ b/utils/internal/test-release/utils_phase.das @@ -6,7 +6,7 @@ require daslib/fio require classify // Phase `utils`: every shipped tool exercised from the bundle alone. -// Row = argv + substring oracle; --network rows fetch packages and mutate example dirs. +// Row = argv + oracle (substrings, an expected exit code); --network rows mutate the bundle's example dirs. struct ToolCheck { name : string //! verdict key, e.g. "lint.exe/daslib" @@ -14,8 +14,9 @@ struct ToolCheck { want : array //! every substring must appear in the merged output forbid : array //! none may appear (checked against "\n" + output, so "\n0 tests" anchors a line) timeout : float = 120.0 - network : bool = false //! only with --network: fetches packages, mutates the bundle's example dirs + network : bool = false //! only with --network: mutates the bundle's example dirs (most such rows also fetch packages) copy_from : string //! bundle-relative file copied into @OUT@ before the run (formatter round-trips) + expect_rc : int = 0 //! the exit code that passes } let private DASTEST_OK <- ["0 failed, 0 errors, 0 skipped"] @@ -28,6 +29,7 @@ def private suite_row(name, dir : string; extra_argv : array) : ToolChec return <- ToolCheck(name = name, argv <- argv, want := DASTEST_OK, forbid := DASTEST_EMPTY, timeout = 300.0) } +[arch(at="README.md#utils-row-shapes")] def public utils_checks() : array { var rows <- [ // --- shipped suites through the shipped runner --------------------------------- @@ -68,6 +70,10 @@ def public utils_checks() : array { ToolCheck(name = "lsp/nav", argv <- ["@DASLANG@", "utils/lsp/subtools/nav.das", "--", "documentSymbol", "utils/mcp/tests/_fixture_valid.das"], want <- ["\"ok\":true", "\"name\":\"Point\""]), ToolCheck(name = "mcp/setup-help", argv <- ["@DASLANG@", "utils/mcp/setup.das", "--", "--show-help"], want <- ["--no-build"]), + ToolCheck(name = "dap/refuses-unknown-flag", argv <- ["@DASLANG@", "utils/dap/main.das", "--", "--no-such-flag", "x"], + want <- ["the options are --repo-root, --executable, --timeout"], expect_rc = 2), + ToolCheck(name = "watchdog.exe/help", argv <- ["@EXE:watchdog@", "--help"], want <- ["--stable-seconds"]), + ToolCheck(name = "watchdog/help", argv <- ["@DASLANG@", "utils/watchdog/main.das", "--", "--help"], want <- ["--stable-seconds"]), ToolCheck(name = "daslang-live/test_api", argv <- ["@EXE:daslang-live@", "examples/daslive/test_api/main.das", "--live-port", "19099"], want <- ["PASS: 2 commands registered"], timeout = 60.0), // --- daspkg: launch offline; the installs need the network and mutate example dirs -- @@ -79,9 +85,9 @@ def public utils_checks() : array { want <- ["Installed 'daspkg-test-pure'", "Installed 'daspkg-test-deps'"], network = true), ToolCheck(name = "daspkg.exe/run-daspkg-example", argv <- ["@DASLANG@", "examples/daspkg/daspkg-example/main.das"], want <- ["Hello, daspkg!", "2 + 3 = 5"], network = true), - ToolCheck(name = "daspkg.exe/install-telegram", argv <- ["@EXE:daspkg@", "install", "--root", "examples/telegram/echo-bot", "--no-color"], - want <- ["Installed 'dasTelegram'"], network = true), - ToolCheck(name = "daspkg.exe/unlock-telegram", argv <- ["@DASLANG@", "-compile-only", "examples/telegram/echo-bot/echo_bot.das"], network = true), + ToolCheck(name = "daspkg.exe/install-crash", argv <- ["@EXE:daspkg@", "install", "--root", "examples/crash", "--no-color"], + want <- ["Installed 'dascrash' (local)"], network = true, timeout = 600.0), + ToolCheck(name = "daspkg.exe/unlock-crash", argv <- ["@DASLANG@", "-compile-only", "examples/crash/main.das"], network = true), ToolCheck(name = "daspkg.exe/install-build-example", argv <- ["@EXE:daspkg@", "install", "--root", "examples/daspkg/daspkg-build-example", "--no-color"], want <- ["Installed 'daspkg-example-c' (local)", "Installed 'daspkg-example-cpp' (local)"], network = true, timeout = 600.0), ToolCheck(name = "daspkg.exe/run-build-example", argv <- ["@DASLANG@", "examples/daspkg/daspkg-build-example/main.das"], @@ -89,7 +95,7 @@ def public utils_checks() : array { ] // rows run in order - the daspkg cleanup rows must stay last rows |> reserve(length(rows) + 4) - for (root in ["examples/games/sequence", "examples/daspkg/daspkg-example", "examples/telegram/echo-bot", "examples/daspkg/daspkg-build-example"]) { + for (root in ["examples/games/sequence", "examples/daspkg/daspkg-example", "examples/crash", "examples/daspkg/daspkg-build-example"]) { rows |> emplace(ToolCheck(name = "daspkg.exe/cleanup:{root}", argv <- ["@EXE:daspkg@", "cleanup", "--force", "--root", root, "--no-color"], network = true)) } return <- rows @@ -122,18 +128,23 @@ def public bundle_exe(bundle, name : string) : string { return get_full_file_name(path_join(bundle, "bin/{name}")) // missing: the launch fails loudly } -//! An exit code of 0 is only a pass when the oracle holds: 1 with the unmet expectation -//! as the FAIL head otherwise. Non-zero exits keep their code and the child's output. +//! The row's verdict: rc 0 only when the child's rc equals expect_rc and the oracle holds, with the child's +//! output as the head; an unmet oracle - or a 0 where another code was expected - is rc 1 with the unmet +//! expectation as the head; any other unexpected rc passes through with the child's output. +def public judged(check : ToolCheck; rc : int; out : string) : tuple { + if (rc != check.expect_rc) { + return rc == 0 ? (rc = 1, head = "expected exit {check.expect_rc}, got 0") : (rc = rc, head = out) + } + let miss = unmet(check, out) + return empty(miss) ? (rc = 0, head = out) : (rc = 1, head = miss) +} + def public judged_rc(check : ToolCheck; rc : int; out : string) : int { - return rc == 0 && !empty(unmet(check, out)) ? 1 : rc + return judged(check, rc, out).rc } def public judged_head(check : ToolCheck; rc : int; out : string) : string { - if (rc != 0) { - return out - } - let miss = unmet(check, out) - return empty(miss) ? out : miss + return judged(check, rc, out).head } //! "" when the output satisfies the row, else the first unmet expectation - the FAIL head. @@ -173,7 +184,8 @@ def public run_utils_checks(bundle, daslang : string; network : bool; name_filte var out : string let rc = run_and_capture(argv, out, check.timeout) let sec = float(get_time_usec(t0)) / 1000000.0 - verdicts |> push(verdict_line(check.name, judged_rc(check, rc, out), sec, check.timeout, judged_head(check, rc, out))) + let v = judged(check, rc, out) + verdicts |> push(verdict_line(check.name, v.rc, sec, check.timeout, v.head)) } rmdir_rec(out_dir) return <- verdicts