From 3a5a68ec9b5380fc45421340c992183febc56db1 Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Sat, 11 Jul 2026 01:03:14 +1000 Subject: [PATCH] G-059 achieved: WSL capability probing + native-filesystem staging for unix-side tests from windows - new src/tests/testsupport/wslprobe.tcl (::punktest::wsl): memoized capability probe for the wsllinux constraint - default distro launches and unames as Linux, tools present (bash/mktemp/wslpath + sha1 tool), native tempdir file round trip; any failure -> skip with reason, so wsl.exe-present-but-unusable installs cannot false-positive. Staging helpers (staging_create/copy_in/run_in/cleanup) enforce the design: execution on the distro NATIVE filesystem only, shared /mnt used for one-way copy-in/out, the windows checkout never operated on from inside WSL (DrvFs slowness + cross-boundary stat differences that make git re-hash its index and fossil see phantom changes). Probe uses wsl -e invocations only (wsl --status/-l emit UTF-16) - multishell.test: scriptwrap_multishell_exec_wsl_sh - the polyglot sh payload produces its marker under the distro bash from native staging (the direct isunix twin still skips on windows) - new shell/testsuites/binscripts/runtimebash_wsl.test (8 tests): runtime.bash first-ever execution on real unix - multi-candidate run errors listing candidates, use/list/run round trip with argument passing, PUNK_ACTIVE_RUNTIME override, unknown/unselectable use errors, stale-active guidance, single-candidate fallback, offline list -remote (crafted cached sha1sums.txt + PUNKBIN_URL forced fetch failure), and the checkout-untouched guard (git status --porcelain identical before/after per acceptance) - green on tcl 9.0.3 + 8.7 against Ubuntu-24.04/WSL2; suites skip cleanly when the probe fails; enablement/limitations in src/tests/AGENTS.md (known limitation: a present-but-HANGING wsl can stall the probe) - GOALS.md G-059 flipped to achieved 2026-07-11; detail file records the outcome incl the UTF-16 discovery and the 512-byte label regression this work surfaced (caught by the guardrail suite as designed) Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com --- GOALS.md | 2 +- goals/G-059-wsl-test-driving.md | 30 ++- src/tests/AGENTS.md | 1 + src/tests/modules/AGENTS.md | 2 +- .../mix/testsuites/scriptwrap/multishell.test | 28 +++ src/tests/shell/AGENTS.md | 1 + .../binscripts/runtimebash_wsl.test | 228 ++++++++++++++++++ src/tests/testsupport/wslprobe.tcl | 133 ++++++++++ 8 files changed, 422 insertions(+), 3 deletions(-) create mode 100644 src/tests/shell/testsuites/binscripts/runtimebash_wsl.test create mode 100644 src/tests/testsupport/wslprobe.tcl diff --git a/GOALS.md b/GOALS.md index 56c9f551..252f4dc2 100644 --- a/GOALS.md +++ b/GOALS.md @@ -398,7 +398,7 @@ Detail: goals/G-058-static-runtime-packages.md Goal: punkshell running on a runtime with statically-linked/builtin packages (tclsfe-x64: Thread/twapi/sqlite3/tdbc; punkbin runtimes' builtin Thread/tcllibc/vfs/vlerq; the expected shape of future zig-built static runtimes, G-005) keeps those packages resolvable in every interp and thread punkshell fabricates: boot captures the static baseline (info loaded entries with empty filename, plus their provided versions) before replacing package search paths, code interps and punkshell-created threads are seeded with ifneeded scripts mapping each static package to 'load {} ', and punk::packagepreference resolves static-vs-bundled by a documented version-aware policy instead of blindly loading a bundled dll over an already-provided static package. Acceptance: a punk91-style kit (tclsfe-x64 + punk9win.vfs, no thread dll in the vfs) boots to a working repl with no "can't find package Thread" - punk::console loads in the code interp, and package require Thread succeeds there and in a punkshell-created worker thread, resolving to the static version; in the same kit, twapi resolves per the documented policy (no repeat of the observed static-Twapi-masked-by-older-vfs-twapi-5.0.2 double load; a genuinely newer bundled copy remains reachable by that policy); dll-based kits (punk902z) boot and pass their existing shell test baseline unchanged, as does a plain tclsh dev launch; the seeding mechanism is generic - driven by the captured baseline, no runtime-specific package naming - and the boot-time static baseline is introspectable at the repl; the seeding/preference logic is covered by un-gated unit tests against simulated baselines (runnable under plain tclsh), while kit-boot integration tests are gated behind a capability-probed tcltest constraint (a built kit whose baseline shows static entries including Thread) that skips cleanly when no such kit is present; the runtimes used for verification (tclsfe-x64.exe at minimum) are added to the punkbin artifact repository under win32-x86_64 with sha1sums.txt updated, so the constraint is satisfiable on other machines via the existing runtime-retrieval path; the punk91 code-interp vfs/vfs::zip load failure is re-diagnosed after the fix and either resolved or recorded as a distinct issue/candidate goal. -### G-059 [proposed] WSL detection and suitability probing for driving unix-side tests from Windows +### G-059 [achieved 2026-07-11] WSL detection and suitability probing for driving unix-side tests from Windows Scope: src/tests/ (capability probe helpers + constraint-gated cases in existing suites, e.g the unix sh-payload execution test in modules/punk/mix/testsuites/scriptwrap/multishell.test and a runtime.bash behaviour test), src/tests/AGENTS.md (enablement notes) Detail: goals/G-059-wsl-test-driving.md Goal: test runs on a Windows dev machine can exercise selected unix-side behaviour through WSL when it is present AND suitable - a capability-probed tcltest constraint (not mere wsl.exe existence) verifies a launchable distro, required tools (bash, coreutils/sha1 tooling), and working one-way staging into the guest's NATIVE filesystem - with all guest-side execution happening on that native filesystem: per-test artifacts are staged into a WSL-native tempdir and results collected back, the Windows checkout is never operated on via the shared /mnt path (DrvFs is slow and cross-boundary stat differences make git re-hash its index and fossil see phantom changes), and any future full-suite mode uses a separate native clone rather than the shared working tree. diff --git a/goals/G-059-wsl-test-driving.md b/goals/G-059-wsl-test-driving.md index 661338ca..963aa7cb 100644 --- a/goals/G-059-wsl-test-driving.md +++ b/goals/G-059-wsl-test-driving.md @@ -1,6 +1,6 @@ # G-059 WSL detection and suitability probing for driving unix-side tests from Windows -Status: proposed +Status: achieved 2026-07-11 Scope: src/tests/ (capability probe helpers + constraint-gated cases in existing suites, e.g the unix sh-payload execution test in modules/punk/mix/testsuites/scriptwrap/multishell.test and a runtime.bash behaviour test), src/tests/AGENTS.md (enablement notes) Acceptance: see GOALS.md index entry (canonical). @@ -62,3 +62,31 @@ a staged file. Any probe failure -> skip cleanly (never fail the suite). cheapest provider of that guest-driving contract. - WSL itself is a Windows feature (no licensing entanglement); distro choice is the user's - the probe should not assume a specific distro, only capabilities. + +## Outcome (achieved 2026-07-11) + +- `src/tests/testsupport/wslprobe.tcl` (`::punktest::wsl`): memoized capability probe + (wsl.exe present -> default distro launches and unames as Linux -> tools present: + bash/mktemp/wslpath + a sha1 tool -> native tempdir file round trip; any failure + yields available=0 with a reason and consumers skip) plus staging helpers + (staging_create/staging_copy_in/run_in/staging_cleanup - cleanup refuses paths it + didn't create). Probe uses `wsl -e ` invocations only: management commands + (`wsl --status`, `-l`) emit UTF-16 - discovered during implementation. +- Consumers: `scriptwrap_multishell_exec_wsl_sh` in multishell.test (the polyglot's sh + payload produces its marker under the distro's bash from native staging - the direct + isunix twin still skips on windows) and the new + `src/tests/shell/testsuites/binscripts/runtimebash_wsl.test` (8 tests): runtime.bash's + first-ever execution on real unix - multi-candidate run errors with candidates, + use/list/run round trip with argument passing, PUNK_ACTIVE_RUNTIME override, + unknown/unselectable use errors, stale-active guidance, single-candidate fallback, + offline `list -remote` (crafted cached sha1sums.txt + PUNKBIN_URL forced-failure), and + the checkout-untouched guard (git status --porcelain identical before/after the + WSL-gated run - the native-staging design made verifiable). +- All green on Tcl 9.0.3 and 8.7 against Ubuntu-24.04/WSL2; suites skip cleanly when the + probe fails (constraint gating; helper absence also degrades to skip). +- Enablement/limitations recorded in src/tests/AGENTS.md (incl the known limitation: a + present-but-hanging WSL can stall the probe - erroring installs are handled). +- Bonus finding: the WSL list -remote test work triggered the polyglot's first real + 512-byte label regression (payload growth pushed the template's :exit_multishell + across a boundary); fixed via a documented spacer comment in runtime.bash - the G-05x + guardrail suite caught it exactly as designed. diff --git a/src/tests/AGENTS.md b/src/tests/AGENTS.md index d6afc1d0..c255537a 100644 --- a/src/tests/AGENTS.md +++ b/src/tests/AGENTS.md @@ -27,6 +27,7 @@ Top-level test harness and source-tree tests for ShellSpy/Punk. Tests here exerc - `-report json` emits a machine-readable final summary, but package-load warnings may still precede it on stdout/stderr. - ERROR-status failures now surface `errorInfo` (full Tcl error message/stack trace) in markdown, compact, and json reports. Compact mode truncates to a single line. - FAILED-status failures (result mismatch, not error) now surface `result_was` (actual) and `result_expected` (expected) in markdown, compact, and json reports. Compact mode truncates each to a single line. +- `testsupport/` holds helper `.tcl` files sourced by `.test` files (not discovered as suites). `testsupport/wslprobe.tcl` (G-059) provides `::punktest::wsl`: a memoized capability probe yielding the `wsllinux` constraint (default distro launches, answers uname/tool probes, and round-trips a file through a native tempdir - NOT mere wsl.exe existence) plus native-filesystem staging helpers (`staging_create`/`staging_copy_in`/`run_in`/`staging_cleanup`). WSL-gated tests must execute from a staging dir on the distro's native filesystem with the shared `/mnt` path used only for one-way copy-in/out - never operate on the Windows checkout from inside WSL (DrvFs is slow and cross-boundary stat differences make git re-hash its index and fossil see phantom changes). Probe invocations use `wsl -e ` only (`wsl --status`/`-l` emit UTF-16). Known limitation: a broken-but-present WSL that hangs (rather than errors) on `wsl -e` can stall the probe. ## Work Guidance diff --git a/src/tests/modules/AGENTS.md b/src/tests/modules/AGENTS.md index 5572b45e..a1584b37 100644 --- a/src/tests/modules/AGENTS.md +++ b/src/tests/modules/AGENTS.md @@ -41,7 +41,7 @@ Unit tests for editable source modules under `src/modules/`, `src/modules_tcl8/` - `punk/ansi/` — punk::ansi tests (`testsuites/ansi/`): ansistrip/ansimerge, plus characterization of the ANSI-at-position mechanisms (`ansistring.test`: INDEX/INDEXCODE/INDEXCHAR/RANGE/INSERT grapheme indexing with SGR-prefix merging, INDEXCOLUMNS/COLUMNINDEX double-wide column mapping, trim/VIEW), code splitting invariants (`ta.test`: detect/detectcode distinction, split_codes/split_codes_single/split_at_codes shapes and round-trip) and single-code/effective-state semantics (`codetype.test`: is_sgr_reset/has_sgr_leadingreset, has_any/all_effective, sgr_merge, sequence_type classify). ANSI codes in these tests are literal escape strings so results are colour-state independent - `punk/args/` — punk::args tests (`testsuites/args/`): parsing, choices/choicegroups, forms, rendering/indentation characterization, usage-marking characterization (`usagemarking.test`: -parsedargs/-badarg/-parsestatus/-scheme marking primitives plus goodchoice highlighting of selected/default-in-effect choice words, asserted by SGR-parameter subset against the live colour arrays; the G-049 nocolour/colour-leak GAP pins flipped 2026-07-10 to scheme-statelessness assertions), and the G-049 parse-status structure (`parsestatus.test`: punk::args::parse_status overall/per-argument statuses, badarg for type/allocation failures, -caller attribution, errorcode -argspecs stripping) - `punk/ns/` — punk::ns tests (`testsuites/ns/`): cmdwhich/cmdinfo/cmd_traverse doc-lookup flow (`cmdflow.test`, G-040 parity) and cmdhelp usage-rendering integration (`cmdhelp.test`: scheme selection, goodarg/badarg marking incl type/allocation failures, goodchoice highlighting of supplied/default choice words, alias path, cmdinfo result shape, queried-command failure attribution, and `-return dict` parse-status returns (G-049 - its GAP pins flipped 2026-07-10); remaining GAP pins for pseudo-command cmdtype + space-form docid prefixes (G-051, real `string is` pins behind the have_tclcoredocs constraint), TclOO undocumented-method fallback (G-052), and synopsis marking absence (G-050)) -- `punk/mix/` — punk::mix::cli tests (prune helpers, punkcheck virtual sources), punk::mix::commandset::repo fossil move/rename characterization tests (`testsuites/repo/`, FOSSIL_HOME-isolated; GAP-marked tests pin behaviour G-022 will change), and the MULTISHELL polyglot build machinery (`testsuites/scriptwrap/multishell.test`: scriptset wrap via the punk.multishell.cmd template - structure/LF-only/determinism, checkfile 512-byte label validation of fresh wraps AND the committed bin/runtime.cmd, the runtime scriptset round-trip byte-identity pin, and platform-gated execution smoke: cmd.exe→powershell payload on windows, sh payload on unix) +- `punk/mix/` — punk::mix::cli tests (prune helpers, punkcheck virtual sources), punk::mix::commandset::repo fossil move/rename characterization tests (`testsuites/repo/`, FOSSIL_HOME-isolated; GAP-marked tests pin behaviour G-022 will change), and the MULTISHELL polyglot build machinery (`testsuites/scriptwrap/multishell.test`: scriptset wrap via the punk.multishell.cmd template - structure/LF-only/determinism, checkfile 512-byte label validation of fresh wraps AND the committed bin/runtime.cmd, the runtime scriptset round-trip byte-identity pin, and platform-gated execution smoke: cmd.exe→powershell payload on windows, sh payload on unix or via the `wsllinux` capability constraint from `src/tests/testsupport/wslprobe.tcl` - staged to the WSL distro's native filesystem, G-059) - `punk/lib/` — punk::lib tests (`testsuites/lib/`): range/index/parse/compat/interp_sync utilities, and G-058 static-baseline seeding (`staticseed.test`: interp_sync_package_paths/snapshot_package_paths propagate a simulated ::punkboot static baseline and seed `load {} ` ifneeded mappings; no-op without a baseline) - `punk/packagepreference/` — punk::packagepreference tests (`testsuites/packagepreference/`): G-058 static-vs-bundled policy (`staticpolicy.test`: require of a baseline package triggers the index scan before resolution so a newer bundled copy wins, static beats older bundled, exact requires of bundled versions stay reachable, missing static mappings get seeded) - `punk/libunknown/` — .tm same-version shadowing pin-tests (`testsuites/shadowing/`): tcl::tm::add prepend rule, head-of-tm-list wins exact-version ties, version beats order, punk::libunknown parity — shipped behaviour depends on these (runtests tm ordering, punk_main package-mode precedence, G-033); mixed .tm/pkgIndex.tcl characterization is goal G-035 diff --git a/src/tests/modules/punk/mix/testsuites/scriptwrap/multishell.test b/src/tests/modules/punk/mix/testsuites/scriptwrap/multishell.test index f6f6f3f3..ca781f93 100644 --- a/src/tests/modules/punk/mix/testsuites/scriptwrap/multishell.test +++ b/src/tests/modules/punk/mix/testsuites/scriptwrap/multishell.test @@ -232,5 +232,33 @@ namespace eval ::testspace { -cleanup { }\ -result [list 1] + + #--- G-059: drive the sh payload under WSL from windows ---------------------------------- + #Staged into the distro's NATIVE filesystem (never executed from the shared /mnt path) + #per the G-059 design - see src/tests/testsupport/wslprobe.tcl. + if {[file exists [file join $testdir .. .. .. .. .. testsupport wslprobe.tcl]]} { + source [file join $testdir .. .. .. .. .. testsupport wslprobe.tcl] + testConstraint wsllinux [expr {$::tcl_platform(platform) eq "windows" && [dict get [punktest::wsl::probe] available]}] + } else { + testConstraint wsllinux 0 + } + + test scriptwrap_multishell_exec_wsl_sh {a suitable WSL distro runs the polyglot's shell payload from a native-filesystem staging dir}\ + -constraints {wrapok wsllinux}\ + -setup { + set result "" + variable outfile + set stagingdir [punktest::wsl::staging_create] + punktest::wsl::staging_copy_in $stagingdir $outfile + }\ + -body { + set r [punktest::wsl::run_in $stagingdir "bash ./[file tail $outfile] ` scriptlib-resolution mechanism, and the tclsh-matching Tk main-loop behaviour. The `lib:` and Tk cases resolve committed fixtures under `scriptlib/_punktest/` (test-owned; a `lib:`-resolvable fixture cannot live in a tcltest temp dir since resolution only searches scriptlib locations relative to the exe). Tk cases are gated by the `punktk` constraint, auto-detected by probing whether the kit can load Tk. - punkexe tests run against the built binary: after changing punk::repl or app-punkshell source, rebuild via `make.tcl packages`, `make.tcl vfscommonupdate` (requires interactive `y`), then `make.tcl project` before trusting results; `make.tcl project` alone does not refresh `_vfscommon.vfs`. +- `testsuites/binscripts/runtimebash_wsl.test` (G-059) exercises the runtime.cmd unix payload (`src/scriptapps/bin/runtime.bash`) through WSL: active/use/run resolution, env override, stale-active guidance, single-candidate fallback, and `list -remote` local-vs-server comparison (offline via a crafted cached sha1sums.txt + `PUNKBIN_URL` pointed at an unreachable address). Gated by the `wsllinux` capability constraint from `src/tests/testsupport/wslprobe.tcl`; all execution happens in a WSL-native staging dir, and a final test asserts the Windows checkout's git status is unchanged by the run. - `testsuites/punkexe/staticruntime.test` (G-058) targets a kit built on a runtime with statically-linked packages: resolved from `env(PUNK_STATICKIT_TEST_EXE)`, else `/bin/punk91.exe`, gated by the capability-probed `statickitavailable` constraint (a probe run must show a captured `::punkboot::static_packages` baseline including Thread - not mere file existence). It asserts repl boot without the codethread Thread failure, Thread resolution in the code interp / fabricated interps / snapshot-seeded threads, and twapi resolution per the static-vs-bundled policy. The verification runtime (tclsfe-x64.exe) is pinned in the punkbin artifact repo so the constraint is satisfiable off the original dev machine. Kit script-exec paths must be passed with forward slashes. - GOAL tests (constraint `punkgoals`, enabled by env `PUNK_TEST_GOALS=1`) cover the piped-stdin-then-interactive-shell drop-in (`shellpipe_eof_then_interactive_shell`, `shellpipe_script_and_eof_then_interactive_shell`); they are excluded from normal runs because they require an openable console in the test environment and briefly attach a live shell to it - they pass in console-attached environments against a current build. - The eof-policy discriminator is `env(PUNK_PIPE_EOF)` (`exit`|`interactive`|unset=console heuristic) implemented in app-punkshell; termination-guard tests spawn children with `exit`, goal tests spawn with it unset. diff --git a/src/tests/shell/testsuites/binscripts/runtimebash_wsl.test b/src/tests/shell/testsuites/binscripts/runtimebash_wsl.test new file mode 100644 index 00000000..ef1f4689 --- /dev/null +++ b/src/tests/shell/testsuites/binscripts/runtimebash_wsl.test @@ -0,0 +1,228 @@ +package require tcltest + +#G-059: behaviour tests for the runtime.cmd unix payload (src/scriptapps/bin/runtime.bash) +#driven through WSL from a windows dev machine. The payload had never been executed on +#real unix before this suite - only syntax-checked (bash -n / zsh -n). +# +#Staging design (goals/G-059-wsl-test-driving.md): runtime.bash plus a fixture runtime +#folder (fake executable "runtimes" that just echo markers) are staged ONE-WAY into a +#tempdir on the distro's NATIVE filesystem and all execution happens there. The windows +#checkout is never operated on from inside WSL, so git/fossil state cannot be disturbed +#(asserted by the final test via a before/after git status comparison). +# +#Covered (offline - no network/fetch cases; those are future env-gated work): +# - run with multiple runtimes and no active selection errors listing candidates +# - use records active.toml; list marks the active entry; run launches it +# - PUNK_ACTIVE_RUNTIME env override wins over active.toml +# - use with an unknown/unselectable name errors +# - stale active.toml (file removed) errors with reselect guidance +# - single-candidate fallback when no active is recorded + +namespace eval ::testspace { + namespace import ::tcltest::* + variable common { + set result "" + } + + variable testdir [file dirname [file normalize [info script]]] + #/src/tests/shell/testsuites/binscripts -> src/tests is 3 levels up + variable testsroot [file normalize [file join $testdir .. .. ..]] + variable projectroot [file normalize [file join $testsroot .. ..]] + + if {[file exists [file join $testsroot testsupport wslprobe.tcl]]} { + source [file join $testsroot testsupport wslprobe.tcl] + testConstraint wsllinux [expr {$::tcl_platform(platform) eq "windows" && [dict get [punktest::wsl::probe] available]}] + } else { + testConstraint wsllinux 0 + } + variable runtimebash [file join $projectroot src scriptapps bin runtime.bash] + testConstraint havesource [file exists $runtimebash] + + #vcs-state guard (G-059 acceptance): snapshot before any WSL activity in this file + variable git_status_before "" + catch { + variable git_status_before [exec {*}[auto_execok git] -C $projectroot status --porcelain] + } + + #one staging area for the whole file: runtime.bash + runtime/linux-x86_64 fixture + #with two fake "runtimes" (echo scripts). Rebuilt fresh via reset_fixture per test. + variable stagingdir "" + if {[testConstraint wsllinux] && [testConstraint havesource]} { + set stagingdir [punktest::wsl::staging_create] + punktest::wsl::staging_copy_in $stagingdir $runtimebash + punktest::wsl::run_in $stagingdir { + mkdir -p runtime/linux-x86_64 && + printf '#!/bin/sh\necho FAKERT_ALPHA_RAN "$@"\n' > runtime/linux-x86_64/fakert_alpha && + printf '#!/bin/sh\necho FAKERT_BETA_RAN "$@"\n' > runtime/linux-x86_64/fakert_beta && + chmod +x runtime/linux-x86_64/fakert_alpha runtime/linux-x86_64/fakert_beta + } + } + #run runtime.bash inside the staging dir with args; returns dict ok/output + proc rtbash {args} { + variable stagingdir + return [punktest::wsl::run_in $stagingdir "bash ./runtime.bash $args"] + } + proc reset_fixture {} { + variable stagingdir + punktest::wsl::run_in $stagingdir {rm -f runtime/linux-x86_64/active.toml runtime/linux-x86_64/fakert_gamma} + } + + test runtimebash_run_multi_no_active_errors {run with multiple runtimes and no active selection errors and lists candidates}\ + -constraints {wsllinux havesource}\ + -setup { + set result "" + reset_fixture + }\ + -body { + set r [rtbash run] + lappend result [dict get $r ok] + lappend result [string match "*Multiple runtimes installed and no active runtime selected*" [dict get $r output]] + lappend result [string match "*fakert_alpha*" [dict get $r output]] + lappend result [string match "*fakert_beta*" [dict get $r output]] + }\ + -cleanup { + }\ + -result [list 0 1 1 1] + + test runtimebash_use_records_and_run_launches {use records active.toml, list marks it, run launches the selected runtime with arguments}\ + -constraints {wsllinux havesource}\ + -setup { + set result "" + reset_fixture + }\ + -body { + set r [rtbash use fakert_beta] + lappend result [dict get $r ok] + set r [punktest::wsl::run_in $::testspace::stagingdir {cat runtime/linux-x86_64/active.toml}] + lappend result [string trim [dict get $r output]] + set r [rtbash list] + lappend result [string match {*\* fakert_beta (active)*} [dict get $r output]] + set r [rtbash run hello world] + lappend result [dict get $r ok] + lappend result [string match "*FAKERT_BETA_RAN hello world*" [dict get $r output]] + }\ + -cleanup { + }\ + -result [list 1 {active = "fakert_beta"} 1 1 1] + + test runtimebash_env_override_wins {PUNK_ACTIVE_RUNTIME env override wins over active.toml}\ + -constraints {wsllinux havesource}\ + -setup { + set result "" + reset_fixture + }\ + -body { + rtbash use fakert_beta + variable stagingdir + set r [punktest::wsl::run_in $stagingdir "PUNK_ACTIVE_RUNTIME=fakert_alpha bash ./runtime.bash run"] + lappend result [dict get $r ok] + lappend result [string match "*FAKERT_ALPHA_RAN*" [dict get $r output]] + }\ + -cleanup { + }\ + -result [list 1 1] + + test runtimebash_use_unknown_errors {use with an unknown name errors and lists candidates; non-runtime extensions are unselectable}\ + -constraints {wsllinux havesource}\ + -setup { + set result "" + reset_fixture + }\ + -body { + set r [rtbash use no_such_runtime] + lappend result [dict get $r ok] + lappend result [string match "*No runtime named 'no_such_runtime'*" [dict get $r output]] + set r [rtbash use sha1sums.txt] + lappend result [dict get $r ok] + lappend result [string match "*not a selectable runtime*" [dict get $r output]] + }\ + -cleanup { + }\ + -result [list 0 1 0 1] + + test runtimebash_stale_active_errors {a recorded active runtime that is no longer present errors with reselect guidance}\ + -constraints {wsllinux havesource}\ + -setup { + set result "" + reset_fixture + }\ + -body { + variable stagingdir + punktest::wsl::run_in $stagingdir {printf 'active = "gone_runtime"\n' > runtime/linux-x86_64/active.toml} + set r [rtbash run] + lappend result [dict get $r ok] + lappend result [string match "*active runtime 'gone_runtime'*is not present*" [dict get $r output]] + lappend result [string match "*use *" [dict get $r output]] + }\ + -cleanup { + }\ + -result [list 0 1 1] + + test runtimebash_single_candidate_fallback {with exactly one installed runtime and no active recorded, run uses it}\ + -constraints {wsllinux havesource}\ + -setup { + set result "" + reset_fixture + }\ + -body { + variable stagingdir + punktest::wsl::run_in $stagingdir {mkdir -p solo/runtime/linux-x86_64 && cp runtime.bash solo/ && cp runtime/linux-x86_64/fakert_alpha solo/runtime/linux-x86_64/} + set r [punktest::wsl::run_in $stagingdir "cd solo && bash ./runtime.bash run"] + lappend result [dict get $r ok] + lappend result [string match "*FAKERT_ALPHA_RAN*" [dict get $r output]] + }\ + -cleanup { + }\ + -result [list 1 1] + + test runtimebash_list_remote_cached_compare {list -remote compares local runtimes against sha1sums.txt (offline: forced fetch failure falls back to the cached copy)}\ + -constraints {wsllinux havesource}\ + -setup { + set result "" + reset_fixture + }\ + -body { + variable stagingdir + #craft a cached sha1sums.txt: correct hash for alpha, wrong hash for beta, + #plus a remote-only entry; PUNKBIN_URL points nowhere so curl fails fast + punktest::wsl::run_in $stagingdir { + a=$(sha1sum runtime/linux-x86_64/fakert_alpha | cut -d' ' -f1) && + printf '%s *fakert_alpha\n0000000000000000000000000000000000000000 *fakert_beta\n1111111111111111111111111111111111111111 *fakert_remote_only\n' "$a" > runtime/linux-x86_64/sha1sums.txt + } + set r [punktest::wsl::run_in $stagingdir "PUNKBIN_URL=http://127.0.0.1:9/nowhere bash ./runtime.bash list -remote"] + lappend result [dict get $r ok] + set out [dict get $r output] + lappend result [string match "*using cached copy*" $out] + lappend result [regexp {fakert_alpha\s+Same version} $out] + lappend result [regexp {fakert_beta\s+UPDATE AVAILABLE} $out] + lappend result [regexp -- {-\s+fakert_remote_only} $out] + }\ + -cleanup { + punktest::wsl::run_in $::testspace::stagingdir {rm -f runtime/linux-x86_64/sha1sums.txt} + }\ + -result [list 1 1 1 1 1] + + test runtimebash_wsl_leaves_checkout_untouched {WSL-gated runs do not disturb the windows checkout's git state (G-059 native-staging design)}\ + -constraints {wsllinux havesource}\ + -setup { + set result "" + }\ + -body { + variable git_status_before + variable projectroot + set git_status_after "" + catch { + set git_status_after [exec {*}[auto_execok git] -C $projectroot status --porcelain] + } + lappend result [expr {$git_status_after eq $git_status_before}] + }\ + -cleanup { + }\ + -result [list 1] + + #whole-file staging cleanup + if {[testConstraint wsllinux] && [testConstraint havesource] && $stagingdir ne ""} { + punktest::wsl::staging_cleanup $stagingdir + } +} +tcltest::cleanupTests ;#needed to produce test summary line. diff --git a/src/tests/testsupport/wslprobe.tcl b/src/tests/testsupport/wslprobe.tcl new file mode 100644 index 00000000..a3fc02b3 --- /dev/null +++ b/src/tests/testsupport/wslprobe.tcl @@ -0,0 +1,133 @@ +#wslprobe.tcl - G-059 test support: WSL capability probing + native-filesystem staging +# +#Sourced by .test files (runtests.tcl only discovers *.test - helper .tcl files are not +#run as suites). Provides ::punktest::wsl with: +# probe - memoized capability probe; dict: available 0|1, reason, uname +# winpath_to_wsl - translate a windows path for use inside the distro (wslpath -u) +# staging_create - mktemp -d on the distro's NATIVE filesystem +# staging_copy_in - one-way copy of windows files into a staging dir +# run_in - run a bash command with cwd set to a staging dir; returns +# dict: ok 0|1 output +# staging_cleanup - rm -rf a staging dir (guarded: only paths staging_create made) +# +#DESIGN (see goals/G-059-wsl-test-driving.md): all guest-side execution happens on the +#distro's native filesystem. The shared /mnt path is used only for one-way copy-in of +#artifacts - the windows checkout is never operated on from inside WSL (DrvFs is slow +#and cross-boundary stat differences make git re-hash its index and fossil see phantom +#changes; two VCS clients must never share a working tree). +# +#ENCODING: wsl.exe management commands (--status, -l) emit UTF-16 - the probe only ever +#uses 'wsl -e ' invocations, whose output is the command's own (UTF-8). +# +#CAVEAT: a broken-but-present WSL install that HANGS (rather than erroring) on first +#'wsl -e' can stall the probe - known limitation, documented in src/tests/AGENTS.md. + +namespace eval ::punktest::wsl { + variable probe_result "" + variable created_stagingdirs [list] + + proc wslexe {} { + return [auto_execok wsl] + } + + #memoized capability probe - capability-based, not wsl.exe-existence-based: + #the distro must launch, answer uname, have the required tools, and round-trip a + #file through a native tempdir. Any failure -> available 0 with a reason (skip, not fail). + proc probe {} { + variable probe_result + if {$probe_result ne ""} { + return $probe_result + } + set result [dict create available 0 reason "" uname ""] + set wsl [wslexe] + if {![llength $wsl]} { + dict set result reason "wsl.exe not found" + set probe_result $result + return $result + } + #distro launches and is linux + if {[catch {exec {*}$wsl -e uname -sm} unamesm]} { + dict set result reason "wsl -e uname failed (no usable default distro?): [lindex [split $unamesm \n] 0]" + set probe_result $result + return $result + } + set unamesm [string trim $unamesm] + dict set result uname $unamesm + if {![string match "Linux*" $unamesm]} { + dict set result reason "default distro uname '$unamesm' is not Linux" + set probe_result $result + return $result + } + #required tools + if {[catch {exec {*}$wsl -e bash -c {command -v bash mktemp wslpath >/dev/null && (command -v sha1sum >/dev/null || command -v shasum >/dev/null) && echo TOOLS_OK}} toolsout] + || ![string match "*TOOLS_OK*" $toolsout]} { + dict set result reason "required tools missing in distro (bash/mktemp/wslpath + sha1 tool)" + set probe_result $result + return $result + } + #native staging round trip + if {[catch { + set tmpd [string trim [exec {*}$wsl -e mktemp -d]] + if {![string match "/*" $tmpd]} { + error "unexpected mktemp result '$tmpd'" + } + set echoed [string trim [exec {*}$wsl -e bash -c "echo punkprobe > $tmpd/probe.txt && cat $tmpd/probe.txt && rm -rf $tmpd"]] + if {$echoed ne "punkprobe"} { + error "staging round trip mismatch '$echoed'" + } + } errM]} { + dict set result reason "native tempdir staging round trip failed: [lindex [split $errM \n] 0]" + set probe_result $result + return $result + } + dict set result available 1 + dict set result reason "ok" + set probe_result $result + return $result + } + + proc winpath_to_wsl {winpath} { + set wsl [wslexe] + return [string trim [exec {*}$wsl -e wslpath -u [file normalize $winpath]]] + } + + proc staging_create {} { + variable created_stagingdirs + set wsl [wslexe] + set tmpd [string trim [exec {*}$wsl -e mktemp -d -t punktest.XXXXXX]] + if {![string match "/tmp/*" $tmpd]} { + error "wsl staging_create: unexpected tempdir '$tmpd'" + } + lappend created_stagingdirs $tmpd + return $tmpd + } + + #one-way copy-in: read each windows file over the shared path ONCE, into the native dir + proc staging_copy_in {stagingdir args} { + set wsl [wslexe] + foreach winfile $args { + set src [winpath_to_wsl $winfile] + exec {*}$wsl -e cp $src $stagingdir/ + } + } + + #run a bash command line with cwd at the staging dir. Returns dict: ok, output. + proc run_in {stagingdir cmdline} { + set wsl [wslexe] + set script "cd $stagingdir && $cmdline" + if {[catch {exec {*}$wsl -e bash -c $script 2>@1} output]} { + return [dict create ok 0 output $output] + } + return [dict create ok 1 output $output] + } + + proc staging_cleanup {stagingdir} { + variable created_stagingdirs + if {$stagingdir ni $created_stagingdirs} { + error "wsl staging_cleanup: refusing to remove '$stagingdir' (not created by staging_create)" + } + set wsl [wslexe] + catch {exec {*}$wsl -e rm -rf $stagingdir} + set created_stagingdirs [lsearch -all -inline -not -exact $created_stagingdirs $stagingdir] + } +}