Browse Source
- 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.commaster
8 changed files with 422 additions and 3 deletions
@ -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 <name> 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]]] |
||||
#<projectroot>/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 <name>*" [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. |
||||
@ -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 <combined stdout+stderr> |
||||
# 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 <cmd>' 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] |
||||
} |
||||
} |
||||
Loading…
Reference in new issue