Browse Source
'dev doc.validate' invokes a bare dtplite command which the repl unknown-handler resolves via auto_execok on PATH - no dtplite executable existed (only the legacy misnamed bin/dtplite_run.bat), giving "invalid command name dtplite". - src/scriptapps/dtplite.tcl: LF-converted (polyglot payloads are embedded verbatim into the LF-only multishell output); fall back to the project-vendored tcllib (src/vendorlib_tcl9/<arch>/tcllib*) when the invoking tclsh lacks the dtplite package - src/scriptapps/dtplite_wrap.toml: new scriptset config (tclsh nextshell on all platforms) - bin/dtplite.cmd: generated polyglot via punk::mix::commandset::scriptwrap::multishell (checkfile-clean, LF-only) - src/tests/shell/testsuites/binscripts/dtplite.test: artifact pins (LF-only, checkfile, byte-identical round-trip re-wrap) + execution usecases (usage error, validate good/bad file, validate directory tree, html generation) - 8/8 pass under runtests - AGENTS.md (bin, src/scriptapps): record the dtplite scriptset, its test pin and the LF-only payload requirement - punkproject.toml: 0.12.22 -> 0.12.23 Verified: 'dev doc.validate' in punk902z src validates all 64 src/doc .man files clean. The doctools pipeline remains interim - punk::args is the intended doc source of truth. Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.commaster
7 changed files with 1964 additions and 30 deletions
@ -1,4 +1,4 @@ |
|||||||
[project] |
[project] |
||||||
name = "punkshell" |
name = "punkshell" |
||||||
version = "0.12.22" |
version = "0.12.23" |
||||||
license = "BSD-2-Clause" |
license = "BSD-2-Clause" |
||||||
|
|||||||
@ -0,0 +1,15 @@ |
|||||||
|
[application] |
||||||
|
template="punk.multishell.cmd" |
||||||
|
as_admin=false |
||||||
|
|
||||||
|
scripts=[ |
||||||
|
"dtplite.tcl", |
||||||
|
] |
||||||
|
|
||||||
|
default_outputfile="dtplite.cmd" |
||||||
|
default_nextshellpath="/usr/bin/env tclsh" |
||||||
|
default_nextshelltype="tcl" |
||||||
|
|
||||||
|
win32.nextshellpath="tclsh" |
||||||
|
win32.nextshelltype="tcl" |
||||||
|
win32.outputfile="dtplite.cmd" |
||||||
@ -0,0 +1,231 @@ |
|||||||
|
package require tcltest |
||||||
|
|
||||||
|
#Behaviour + artifact tests for bin/dtplite.cmd - the scriptwrap-multishell wrapped |
||||||
|
#tcllib dtplite application (payload: src/scriptapps/dtplite.tcl + dtplite_wrap.toml). |
||||||
|
# |
||||||
|
#Context: 'dev doc.validate' (punk::mix::commandset::doc::validate) invokes a bare |
||||||
|
#`dtplite validate <path>` which the punk repl unknown-handler resolves via auto_execok, |
||||||
|
#so a dtplite executable must be findable on PATH. bin/dtplite.cmd provides that |
||||||
|
#cross-platform (cmd.exe on windows dispatching to tclsh; sh/tclsh on unix). |
||||||
|
# |
||||||
|
#Covered: |
||||||
|
# - artifact contract: bin/dtplite.cmd is LF-only and passes scriptwrap checkfile |
||||||
|
# (no 512-byte label location errors) |
||||||
|
# - source sync: re-wrapping the dtplite scriptset from src/scriptapps reproduces the |
||||||
|
# committed bin/dtplite.cmd byte for byte (fix payloads and re-wrap - never the output) |
||||||
|
# - execution usecases (windows via cmd.exe; unix via sh - both need a tclsh with the |
||||||
|
# dtplite package or the project-vendored tcllib fallback): |
||||||
|
# * no args -> usage error, nonzero exit |
||||||
|
# * validate <good .man file> -> exit 0 |
||||||
|
# * validate <bad .man file> -> nonzero exit + doctools error on stderr |
||||||
|
# * validate <directory tree> -> exit 0 (the 'dev doc.validate' usecase) |
||||||
|
# * html generation to an output file |
||||||
|
# |
||||||
|
#NOTE for agents: bin/dtplite.cmd is GENERATED - edit src/scriptapps/dtplite.tcl / |
||||||
|
#dtplite_wrap.toml and re-wrap with punk::mix::commandset::scriptwrap::multishell |
||||||
|
#(see src/scriptapps/AGENTS.md). |
||||||
|
|
||||||
|
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 .. ..]] |
||||||
|
|
||||||
|
variable target [file join $projectroot bin dtplite.cmd] |
||||||
|
testConstraint havedtplitecmd [file exists $target] |
||||||
|
testConstraint iswindows [expr {$::tcl_platform(platform) eq "windows"}] |
||||||
|
testConstraint isunix [expr {$::tcl_platform(platform) eq "unix"}] |
||||||
|
testConstraint havescriptwrap [expr {![catch { |
||||||
|
package require Thread ;#punk::fileline textinfo (used by checkfile) calls thread::id |
||||||
|
package require punk::mix::commandset::scriptwrap |
||||||
|
}]}] |
||||||
|
|
||||||
|
proc readbytes {path} { |
||||||
|
set fd [open $path r] |
||||||
|
fconfigure $fd -translation binary |
||||||
|
set data [read $fd] |
||||||
|
close $fd |
||||||
|
return $data |
||||||
|
} |
||||||
|
|
||||||
|
#write with explicit lf translation |
||||||
|
proc writefile_lf {path content} { |
||||||
|
set fd [open $path w] |
||||||
|
fconfigure $fd -translation lf |
||||||
|
puts $fd $content |
||||||
|
close $fd |
||||||
|
} |
||||||
|
|
||||||
|
#run bin/dtplite.cmd with args; returns dict {exit <code> output <stdout+stderr>} |
||||||
|
#on windows via cmd.exe (the way the repl unknown-handler launches it), on unix via sh |
||||||
|
proc run_dtplite {args} { |
||||||
|
variable target |
||||||
|
set output "" |
||||||
|
set code 0 |
||||||
|
if {$::tcl_platform(platform) eq "windows"} { |
||||||
|
set comspec [expr {[info exists ::env(ComSpec)] ? $::env(ComSpec) : "cmd.exe"}] |
||||||
|
set cmdline [list $comspec /c [file nativename $target] {*}$args] |
||||||
|
} else { |
||||||
|
set cmdline [list sh $target {*}$args] |
||||||
|
} |
||||||
|
if {[catch {exec -- {*}$cmdline << "" 2>@1} output opts]} { |
||||||
|
set ec [dict get $opts -errorcode] |
||||||
|
if {[lindex $ec 0] eq "CHILDSTATUS"} { |
||||||
|
set code [lindex $ec 2] |
||||||
|
} else { |
||||||
|
set code -1 |
||||||
|
} |
||||||
|
} |
||||||
|
return [dict create exit $code output $output] |
||||||
|
} |
||||||
|
|
||||||
|
#--- fixtures: minimal good/bad doctools manpages + a small doc tree ------------- |
||||||
|
variable good_man {[manpage_begin sample_good n 1.0] |
||||||
|
[copyright {2026}] |
||||||
|
[moddesc {Sample}] |
||||||
|
[titledesc {A minimal valid doctools manpage}] |
||||||
|
[description] |
||||||
|
[para] This is a minimal valid doctools document used for smoke testing dtplite. |
||||||
|
[section Usage] |
||||||
|
[para] Nothing to see here. |
||||||
|
[manpage_end]} |
||||||
|
variable bad_man {[manpage_begin sample_bad n 1.0] |
||||||
|
[copyright {2026}] |
||||||
|
[moddesc {Sample}] |
||||||
|
[titledesc {An invalid doctools manpage - manpage_end missing}] |
||||||
|
[description] |
||||||
|
[list_begin itemized] |
||||||
|
[item] an item, but the list is never closed and manpage_end is missing} |
||||||
|
|
||||||
|
variable fixdir [makeDirectory dtplite_fixture] |
||||||
|
writefile_lf $fixdir/sample_good.man $good_man |
||||||
|
writefile_lf $fixdir/sample_bad.man $bad_man |
||||||
|
variable treedir [makeDirectory dtplite_fixture_tree] |
||||||
|
file mkdir $treedir/sub |
||||||
|
writefile_lf $treedir/one.man [string map {sample_good tree_one} $good_man] |
||||||
|
writefile_lf $treedir/sub/two.man [string map {sample_good tree_two} $good_man] |
||||||
|
|
||||||
|
#--- artifact contract ------------------------------------------------------------ |
||||||
|
|
||||||
|
test dtplite_cmd_lf_only {the committed bin/dtplite.cmd uses LF line endings exclusively (cmd label scanning depends on it)}\ |
||||||
|
-constraints havedtplitecmd\ |
||||||
|
-setup $common -body { |
||||||
|
variable target |
||||||
|
set data [readbytes $target] |
||||||
|
lappend result [expr {[string first \r $data] == -1}] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list 1] |
||||||
|
|
||||||
|
test dtplite_cmd_checkfile_no_label_errors {the committed bin/dtplite.cmd passes scriptwrap checkfile - no 512-byte label location errors}\ |
||||||
|
-constraints {havedtplitecmd havescriptwrap}\ |
||||||
|
-setup $common -body { |
||||||
|
variable target |
||||||
|
set summary [punk::mix::commandset::scriptwrap::checkfile $target] |
||||||
|
lappend result [string match "*ERROR: label location errors*" $summary] |
||||||
|
#sanity: the analysis actually ran (labels enumerated) |
||||||
|
lappend result [string match "*call-labels-found:*" $summary] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list 0 1] |
||||||
|
|
||||||
|
test dtplite_cmd_roundtrip_no_drift {re-wrapping the dtplite scriptset from src/scriptapps reproduces the committed bin/dtplite.cmd byte for byte}\ |
||||||
|
-constraints {havedtplitecmd havescriptwrap}\ |
||||||
|
-setup $common -body { |
||||||
|
variable projectroot |
||||||
|
variable target |
||||||
|
set scriptapps [file join $projectroot src scriptapps] |
||||||
|
if {![file exists $scriptapps/dtplite_wrap.toml]} { |
||||||
|
lappend result no_dtplite_sources |
||||||
|
} else { |
||||||
|
set outdir [makeDirectory dtplite_roundtrip] |
||||||
|
set startdir [pwd] |
||||||
|
cd $scriptapps |
||||||
|
set res [punk::mix::commandset::scriptwrap::multishell dtplite -outputfolder $outdir -askme 0 -force 1] |
||||||
|
cd $startdir |
||||||
|
set fresh [dict get $res filename] |
||||||
|
lappend result [expr {[readbytes $fresh] eq [readbytes $target]}] |
||||||
|
#a failure here means either bin/dtplite.cmd was edited directly (fix |
||||||
|
#src/scriptapps/dtplite.tcl or dtplite_wrap.toml instead and re-wrap), |
||||||
|
#or the payload/template changed without regenerating bin/dtplite.cmd |
||||||
|
} |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list 1] |
||||||
|
|
||||||
|
#--- execution usecases ----------------------------------------------------------- |
||||||
|
|
||||||
|
test dtplite_exec_no_args_usage_error {running dtplite.cmd without arguments exits nonzero with the dtplite usage message}\ |
||||||
|
-constraints havedtplitecmd\ |
||||||
|
-setup $common -body { |
||||||
|
set r [run_dtplite] |
||||||
|
lappend result [expr {[dict get $r exit] != 0}] |
||||||
|
lappend result [string match "*wrong#args*" [dict get $r output]] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list 1 1] |
||||||
|
|
||||||
|
test dtplite_exec_validate_good {validate of a well-formed .man file exits 0}\ |
||||||
|
-constraints havedtplitecmd\ |
||||||
|
-setup $common -body { |
||||||
|
variable fixdir |
||||||
|
set r [run_dtplite validate [file nativename $fixdir/sample_good.man]] |
||||||
|
lappend result [dict get $r exit] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list 0] |
||||||
|
|
||||||
|
test dtplite_exec_validate_bad {validate of a malformed .man file exits nonzero and reports a doctools format error}\ |
||||||
|
-constraints havedtplitecmd\ |
||||||
|
-setup $common -body { |
||||||
|
variable fixdir |
||||||
|
set r [run_dtplite validate [file nativename $fixdir/sample_bad.man]] |
||||||
|
lappend result [expr {[dict get $r exit] != 0}] |
||||||
|
lappend result [string match "*FmtError*" [dict get $r output]] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list 1 1] |
||||||
|
|
||||||
|
test dtplite_exec_validate_tree {validate of a directory tree of .man files exits 0 - the 'dev doc.validate' usecase}\ |
||||||
|
-constraints havedtplitecmd\ |
||||||
|
-setup $common -body { |
||||||
|
variable treedir |
||||||
|
set r [run_dtplite validate [file nativename $treedir]] |
||||||
|
lappend result [dict get $r exit] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list 0] |
||||||
|
|
||||||
|
test dtplite_exec_html_generation {html generation from a .man file produces an html output file mentioning the manpage name}\ |
||||||
|
-constraints havedtplitecmd\ |
||||||
|
-setup $common -body { |
||||||
|
variable fixdir |
||||||
|
set outfile $fixdir/sample_good.html |
||||||
|
set r [run_dtplite -o [file nativename $outfile] html [file nativename $fixdir/sample_good.man]] |
||||||
|
lappend result [dict get $r exit] |
||||||
|
lappend result [file exists $outfile] |
||||||
|
if {[file exists $outfile]} { |
||||||
|
set html [readbytes $outfile] |
||||||
|
lappend result [string match "*sample_good*" $html] |
||||||
|
} else { |
||||||
|
lappend result no_output_file |
||||||
|
} |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
file delete -force $fixdir/sample_good.html |
||||||
|
}\ |
||||||
|
-result [list 0 1 1] |
||||||
|
} |
||||||
|
tcltest::cleanupTests ;#needed to produce test summary line. |
||||||
Loading…
Reference in new issue