Browse Source

tests: corp/grepstr/untabify characterization - punk::ns hygiene-pass precursor coverage

New punk/ansi suites and extended punk/ns corp.test, characterizing the
corp -> grepstr -> untabify tab-handling interaction and the standalone
behaviours. All console-query paths are mocked (the overtype renderline.test
get_tabstops pattern, extended to get_size + punk::console::tabwidth): the
-untabify none / -stops terminal / -stops <int> paths consult
punk::console::get_tabstops / get_size which emit live terminal queries that
block or error in headless/piped environments - mocking is the answer to
probing that behaviour deterministically in the test harness.

- ansi/untabify.test (10): -stops 8/int/list/terminal(mocked), -with
  spaces/unicode(U+21E5 + U+00A0)/custom pair, multiline, usage errors, and
  the EXPERIMENTAL -plastic elastic-tabstop mode: basic expansion, the
  cross-line column-alignment property, -with composition - pinned as
  interim behaviour (hardcoded 8-multiple stops marked temp in source) and
  deliberately retained for possible future repl editbuf use.
- ansi/grepstr.test (9): return modes incl summarydict (linemap pinned
  always-present; -help claims -n-only - reconciliation deferred to the
  hygiene pass), exact highlight SGR wrapping, -n numbering with * match
  counts, invert + empty-highlight strip, -C context with break indicators,
  capture groups, and tab handling: warns once per call on stderr, a simple
  single-pass tab line survives with correct highlights.
- ns/corp.test (+6): basic-highlight ansistrip equivalence with -syntax none,
  -n numbering, -untabify spaces exact tab-free expansion + proper 4-element
  -syntax none list, -untabify unicode markers, the KNOWN DEFICIENCY pin for
  default -untabify none on a tabbed body (5 grepstr warnings; brace overlays
  mangle tabbed lines - leaked/shifted close-braces pinned exactly under
  mocked tabstops; flip deliberately if tab handling is fixed or corp moves
  off grepstr), and a ::tcl::CopyDirectory -untabify spaces smoke test
  (tab-free, warning-free, constraint-guarded).

src/tests/modules/AGENTS.md punk/ansi + punk/ns index entries updated.

Verified: punk/ansi 63/63 and punk/ns 63/63 under Tcl 9.0.3; the three
new/extended files green under Tcl 8.7 (10/10, 9/9, 13/13).

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 17 hours ago
parent
commit
765d24b302
  1. 4
      src/tests/modules/AGENTS.md
  2. 178
      src/tests/modules/punk/ansi/testsuites/ansi/grepstr.test
  3. 159
      src/tests/modules/punk/ansi/testsuites/ansi/untabify.test
  4. 207
      src/tests/modules/punk/ns/testsuites/ns/corp.test

4
src/tests/modules/AGENTS.md

@ -39,9 +39,9 @@ Unit tests for editable source modules under `src/modules/`, `src/modules_tcl8/`
- `opunk/console/` — ::opunk::Console backend subclass tests (`testsuites/console/backends.test`, G-001): virtual dispatch of subclass overrides through base-class calls and punk::console::console_spec_resolve (both unchanged), TestConsole determinism + probe-free at_eof, SshConsole capability/eof + the flagship size-via-ANSI-query-over-socket case (a scripted remote terminal answers CSI 6n), TkConsole widget size/eof (gated behind env PUNK_TEST_TK=1 - Tk in the shared testinterp has side effects; also verifiable standalone under a tk-capable kit e.g `punk91 src <script>`) - `opunk/console/` — ::opunk::Console backend subclass tests (`testsuites/console/backends.test`, G-001): virtual dispatch of subclass overrides through base-class calls and punk::console::console_spec_resolve (both unchanged), TestConsole determinism + probe-free at_eof, SshConsole capability/eof + the flagship size-via-ANSI-query-over-socket case (a scripted remote terminal answers CSI 6n), TkConsole widget size/eof (gated behind env PUNK_TEST_TK=1 - Tk in the shared testinterp has side effects; also verifiable standalone under a tk-capable kit e.g `punk91 src <script>`)
- `punkcheck/` — punkcheck module tests (install, summarize_install_resultdict, installtrack) - `punkcheck/` — punkcheck module tests (install, summarize_install_resultdict, installtrack)
- `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/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), grepstr characterization (`grepstr.test`: return modes incl summarydict (linemap pinned as always-present - the -help says -n-only, reconciliation deferred to the planned hygiene pass), exact highlight SGR wrapping, -n line numbering, invert + empty-highlight strip, -C context/breaks, capture groups, and the tab deficiency: warns once per call on stderr, single-pass tab line survives - the multi-pass mangling is pinned at consumer level in punk/ns corp.test), and untabify characterization (`untabify.test`: -stops int/list/terminal, -with spaces/unicode/custom-pair, multiline, errors, plus the EXPERIMENTAL -plastic elastic-tabstop mode deliberately pinned-as-interim and retained for possible repl editbuf use). Console queries (get_tabstops/get_size + punk::console::tabwidth) are mocked per the overtype renderline.test pattern - they emit live terminal queries that block/error headless. 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, synopsis display characterization (`synopsis.test`: basic italic argname/`<type>` styling, longopt `--x=` alias forms, literal/literalprefix/stringstartswith/stringendswith type-alternates rendering unitalicised, option alternate parenthesization, multi-element clause display incl `?type?` members and argname tail-word hints, `-typesynopsis` value-element lists and option passthrough incl documenter ANSI, and the small-restricted-choice-set literal rule: 1-3 restricted choices render as unitalicised `|`-joined literals in leader/option/value positions with choicegroups counted, >3 or `-choicerestricted 0` falling back to italics, `-typesynopsis` taking precedence), 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), 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), -parsekey characterization (`parsekey.test`: result/received/solos/multis keying, shared-key required satisfaction and defaults, mash-path and prefix-abbreviation keying, plus GAP pins for last-defined-member default precedence, cross-member -multiple value loss, parsekey/optname collision conflation, and values/leaders parsekey breakage - desired-behaviour pins disabled behind punkargsKnownBug in `testsuites/dev/parsekey-knownbugs.test`), and tclcore doc/interpreter behavioural parity (`tclcoreparity.test`, G-054, gated on have_tclcoredocs: 'string is' class choices equal the live-harvested set, per-class docids exist, error-vs-ok agreement across the probe matrix, version-note labels conditional on class presence - expectations derived from the running interpreter, green on 8.6/8.7/9.0; under 8.6 run the file directly via a plain tclkit + tcltest driver since runtests' harness needs newer infrastructure) - `punk/args/` — punk::args tests (`testsuites/args/`): parsing, choices/choicegroups, forms, rendering/indentation characterization, synopsis display characterization (`synopsis.test`: basic italic argname/`<type>` styling, longopt `--x=` alias forms, literal/literalprefix/stringstartswith/stringendswith type-alternates rendering unitalicised, option alternate parenthesization, multi-element clause display incl `?type?` members and argname tail-word hints, `-typesynopsis` value-element lists and option passthrough incl documenter ANSI, and the small-restricted-choice-set literal rule: 1-3 restricted choices render as unitalicised `|`-joined literals in leader/option/value positions with choicegroups counted, >3 or `-choicerestricted 0` falling back to italics, `-typesynopsis` taking precedence), 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), 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), -parsekey characterization (`parsekey.test`: result/received/solos/multis keying, shared-key required satisfaction and defaults, mash-path and prefix-abbreviation keying, plus GAP pins for last-defined-member default precedence, cross-member -multiple value loss, parsekey/optname collision conflation, and values/leaders parsekey breakage - desired-behaviour pins disabled behind punkargsKnownBug in `testsuites/dev/parsekey-knownbugs.test`), and tclcore doc/interpreter behavioural parity (`tclcoreparity.test`, G-054, gated on have_tclcoredocs: 'string is' class choices equal the live-harvested set, per-class docids exist, error-vs-ok agreement across the probe matrix, version-note labels conditional on class presence - expectations derived from the running interpreter, green on 8.6/8.7/9.0; under 8.6 run the file directly via a plain tclkit + tcltest driver since runtests' harness needs newer infrastructure)
- `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/ns/` — punk::ns tests (`testsuites/ns/`): cmdwhich/cmdinfo/cmd_traverse doc-lookup flow (`cmdflow.test`, G-040 parity), corp proc-retrieval and syntax/untabify interplay (`corp.test`: name edge cases, -ranges/-n line handling, basic-highlight ansistrip equivalence, -untabify spaces/unicode tab-free output, the KNOWN-DEFICIENCY pin for default -untabify none on tabbed bodies - grepstr warns per pass and brace overlays mangle tabbed lines, deterministic under mocked console tabstops - and a ::tcl::CopyDirectory -untabify spaces smoke test; precursor coverage for the planned punk::ns hygiene pass), 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), punk::mix::commandset::loadedlib tests (`testsuites/loadedlib/libsearch.test`: 'dev lib.search' match semantics via -return list — wrap-glob default, =exact prefix, case rules, explicit globs, version aggregation — plus the loadedlib 0.2.0 contract: deep discovery by default (deep .tm modules found without -refresh, registration persists), -refresh = genuine re-scan (epoch incr + rediscovery picks up .tm files added to already-scanned dirs), and highlight working without the shell-global a+ alias; shared provisioned child interp sourcing the source-tree libunknown directly — see the file's ORDERING NOTE), 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/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), punk::mix::commandset::loadedlib tests (`testsuites/loadedlib/libsearch.test`: 'dev lib.search' match semantics via -return list — wrap-glob default, =exact prefix, case rules, explicit globs, version aggregation — plus the loadedlib 0.2.0 contract: deep discovery by default (deep .tm modules found without -refresh, registration persists), -refresh = genuine re-scan (epoch incr + rediscovery picks up .tm files added to already-scanned dirs), and highlight working without the shell-global a+ alias; shared provisioned child interp sourcing the source-tree libunknown directly — see the file's ORDERING NOTE), 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, G-058 static-baseline seeding (`staticseed.test`: interp_sync_package_paths/snapshot_package_paths propagate a simulated ::punkboot static baseline and seed `load {} <prefix>` ifneeded mappings; no-op without a baseline), and the repl command-completeness engine (`commandcomplete.test`: punk::lib::system::incomplete pending-opener stacks - the info-complete quoting quirk progression (`set x "{*}{"` standalone vs in-proc-body), single openers, tabs, escapes, incomplete<->info-complete parity property; pre-repl-refactor characterization, see goals/G-044 detail preserve-list) - `punk/lib/` — punk::lib tests (`testsuites/lib/`): range/index/parse/compat/interp_sync utilities, G-058 static-baseline seeding (`staticseed.test`: interp_sync_package_paths/snapshot_package_paths propagate a simulated ::punkboot static baseline and seed `load {} <prefix>` ifneeded mappings; no-op without a baseline), and the repl command-completeness engine (`commandcomplete.test`: punk::lib::system::incomplete pending-opener stacks - the info-complete quoting quirk progression (`set x "{*}{"` standalone vs in-proc-body), single openers, tabs, escapes, incomplete<->info-complete parity property; pre-repl-refactor characterization, see goals/G-044 detail preserve-list)
- `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/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)

178
src/tests/modules/punk/ansi/testsuites/ansi/grepstr.test

@ -0,0 +1,178 @@
package require tcltest
package require punk::ansi
package require punk::console
#added 2026-07-14 (agent) - punk::ansi::grepstr characterization, precursor to the
#planned punk::ns hygiene pass (punk::ns::corp's basic syntax highlighter is built
#on repeated grepstr passes - see ns corp.test for the corp-level interplay).
#ANSI expectations are literal escape strings (suite convention - results are
#colour-state independent). term-teal resolves to SGR 38;5;6.
#
#Console queries are mocked for every test (renderline.test pattern) so no test
#can emit a live terminal query in headless/piped environments - relevant for
#tab-containing input, where highlight overlay machinery may consult tabstops.
#
#Tab handling: grepstr does NOT currently handle tab characters - it warns once
#per call on stderr and highlight columns are computed as if tabs were absent.
#A single simple tab line survives (pinned below), but multi-pass highlighting
#over tabbed lines mangles output - that interaction is pinned at the consumer
#level in punk/ns corp.test (corp -untabify none).
namespace eval ::testspace {
namespace import ::tcltest::*
proc mock_console {} {
if {[llength [info commands ::punk::console::get_tabstops]]} {
rename ::punk::console::get_tabstops ::testspace::__orig_get_tabstops
}
proc ::punk::console::get_tabstops {{inoutchannels {stdin stdout}}} {
set stops {}
for {set c 9} {$c <= 201} {incr c 8} {lappend stops $c}
return $stops
}
if {[llength [info commands ::punk::console::get_size]]} {
rename ::punk::console::get_size ::testspace::__orig_get_size
}
proc ::punk::console::get_size {args} {
return [dict create columns 80 rows 24]
}
}
proc restore_console {} {
catch {rename ::punk::console::get_tabstops {}}
if {[llength [info commands ::testspace::__orig_get_tabstops]]} {
rename ::testspace::__orig_get_tabstops ::punk::console::get_tabstops
}
catch {rename ::punk::console::get_size {}}
if {[llength [info commands ::testspace::__orig_get_size]]} {
rename ::testspace::__orig_get_size ::punk::console::get_size
}
}
variable csetup {
set result ""
::testspace::mock_console
}
variable ccleanup {
::testspace::restore_console
}
#capture stderr written during script (chan push transform; popped in all paths)
proc capture_stderr {script} {
variable caught ""
chan push stderr {apply {{cmd chan args} {
switch -- $cmd {
initialize {return {initialize finalize write}}
finalize {return}
write {append ::testspace::caught [lindex $args 0]; return ""}
}
}}}
set code [catch {uplevel 1 $script} r ropts]
chan pop stderr
if {$code} {
return -options $ropts $r
}
return $caught
}
variable haystack "line one\nhas needle here\nline three\nneedle again\nlast line"
test grepstr_return_all_highlight_exact {-return all: every line returned, matches wrapped in highlight SGR + reset}\
-setup $csetup -body {
lappend result [punk::ansi::grepstr -return all -highlight term-teal {\{|\}} "abc {def} ghi"]
}\
-cleanup $ccleanup\
-result [list\
"abc \x1b\[38;5;6m{\x1b\[mdef\x1b\[38;5;6m}\x1b\[m ghi"
]
test grepstr_default_breaksandmatches {default -return breaksandmatches: matched lines with break indicators for skipped runs}\
-setup $csetup -body {
variable haystack
lappend result [punk::ansi::ansistrip [punk::ansi::grepstr {needle} $haystack]]
}\
-cleanup $ccleanup\
-result [list\
"-- 1≠\nhas needle here\n-- 1≠\nneedle again\n-- 1≠"
]
test grepstr_return_matched {-r m: only matching lines, no break indicators}\
-setup $csetup -body {
variable haystack
lappend result [punk::ansi::ansistrip [punk::ansi::grepstr -r m {needle} $haystack]]
}\
-cleanup $ccleanup\
-result [list\
"has needle here\nneedle again"
]
test grepstr_summarydict {-return summarydict: counts plus linemap}\
-setup $csetup -body {
variable haystack
#NOTE characterization: the documented behaviour says linemap appears only
#when -n is also supplied - current behaviour includes linemap always.
#Pinned as-is; doc/behaviour reconciliation left to the punk::ansi/ns
#hygiene pass (either fix the -help or gate the key).
lappend result [punk::ansi::grepstr -return summarydict {needle} $haystack]
lappend result [punk::ansi::grepstr -return summarydict -n {needle} $haystack]
}\
-cleanup $ccleanup\
-result [list\
{matches 2 matchlines 2 totallines 5 linemap {2 1 4 1}}\
{matches 2 matchlines 2 totallines 5 linemap {2 1 4 1}}
]
test grepstr_linenumbers {-n -return all: relative line numbers with * match-count indicator}\
-setup $csetup -body {
lappend result [punk::ansi::ansistrip [punk::ansi::grepstr -n -return all {needle} "aaa\nbbb needle\nccc"]]
}\
-cleanup $ccleanup\
-result [list\
"1 *000 aaa\n2 *001 bbb needle\n3 *000 ccc"
]
test grepstr_invert_empty_highlight {-v with -highlight "": non-matching lines returned, ANSI-stripped by the empty highlight}\
-setup $csetup -body {
lappend result [punk::ansi::ansistrip [punk::ansi::grepstr -r m -highlight "" -v {^##} "##one\ntwo\n##three\nfour"]]
}\
-cleanup $ccleanup\
-result [list\
"two\nfour"
]
test grepstr_context {-C 1: one line of context either side of each match, breaks for skipped runs}\
-setup $csetup -body {
lappend result [punk::ansi::ansistrip [punk::ansi::grepstr -C 1 {needle} "l1\nl2\nl3 needle\nl4\nl5\nl6\nl7 needle\nl8"]]
}\
-cleanup $ccleanup\
-result [list\
"-- 1≠\nl2\nl3 needle\nl4\n-- 1≠\nl6\nl7 needle\nl8"
]
test grepstr_capture_group {capturing group: matched line returned with group content intact}\
-setup $csetup -body {
lappend result [punk::ansi::ansistrip [punk::ansi::grepstr -r m {;\s*(#.*)} "set x 1 ;# tail comment"]]
}\
-cleanup $ccleanup\
-result [list\
"set x 1 ;# tail comment"
]
test grepstr_tab_warning_and_passthrough {tab input: warns once per call on stderr; a simple single-pass tab line survives with correct highlights}\
-setup $csetup -body {
set errout [capture_stderr {
set ::testspace::tabres [punk::ansi::grepstr -return all -highlight term-teal {\{|\}} "ab\tcd {ef} gh"]
}]
lappend result [regexp -all {Warning: grepstr does not currently handle tab} $errout]
lappend result $::testspace::tabres
lappend result [punk::ansi::ansistrip $::testspace::tabres]
unset ::testspace::tabres
set result
}\
-cleanup $ccleanup\
-result [list\
1\
"ab\tcd \x1b\[38;5;6m{\x1b\[mef\x1b\[38;5;6m}\x1b\[m gh"\
"ab\tcd {ef} gh"
]
}
tcltest::cleanupTests ;#needed to produce test summary line.

159
src/tests/modules/punk/ansi/testsuites/ansi/untabify.test

@ -0,0 +1,159 @@
package require tcltest
package require punk::ansi
package require punk::console
#added 2026-07-14 (agent) - punk::ansi::untabify characterization, precursor to the
#planned punk::ns hygiene pass (punk::ns::corp -untabify delegates here, and
#punk::ansi::grepstr requires tab-free input for correct highlight placement).
#
#Console queries are mocked (same pattern as the overtype renderline.test suite):
#-stops <int> consults punk::console::get_size and -stops terminal consults
#punk::console::get_tabstops - both emit live terminal queries that block or error
#in headless/piped environments, so every test that can reach them runs mocked.
#Mock geometry: 80x24, tabstops at columns 9 17 25 ... 201 (8-wide).
#
#The -plastic (elastic tab stops) feature is EXPERIMENTAL but deliberately
#characterized and retained - the user wants it available for possible future use
#in repl editbuf scenarios (2026-07-14). Its current implementation is interim:
#expand_elastic_tabs hardcodes 8-multiple stops to 200 (marked 'temp' in source),
#so these pins encode interim behaviour and are expected to be deliberately
#updated if/when -plastic graduates (e.g to honour -stops).
namespace eval ::testspace {
namespace import ::tcltest::*
proc mock_console {} {
if {[llength [info commands ::punk::console::get_tabstops]]} {
rename ::punk::console::get_tabstops ::testspace::__orig_get_tabstops
}
proc ::punk::console::get_tabstops {{inoutchannels {stdin stdout}}} {
set stops {}
for {set c 9} {$c <= 201} {incr c 8} {lappend stops $c}
return $stops
}
if {[llength [info commands ::punk::console::get_size]]} {
rename ::punk::console::get_size ::testspace::__orig_get_size
}
proc ::punk::console::get_size {args} {
return [dict create columns 80 rows 24]
}
}
proc restore_console {} {
catch {rename ::punk::console::get_tabstops {}}
if {[llength [info commands ::testspace::__orig_get_tabstops]]} {
rename ::testspace::__orig_get_tabstops ::punk::console::get_tabstops
}
catch {rename ::punk::console::get_size {}}
if {[llength [info commands ::testspace::__orig_get_size]]} {
rename ::testspace::__orig_get_size ::punk::console::get_size
}
}
variable csetup {
set result ""
::testspace::mock_console
}
variable ccleanup {
::testspace::restore_console
}
#sp n - n plain spaces (keeps expected values reviewable - no embedded space runs)
proc sp {n} {return [string repeat { } $n]}
#nbsp n - n non-breaking spaces (U+00A0) as used by -with unicode
proc nbsp {n} {return [string repeat   $n]}
test untabify_default_width8 {default -stops 8: tabs advance to next multiple-of-8 column}\
-setup $csetup -body {
lappend result [punk::ansi::untabify "a\tbb\tccc"]
}\
-cleanup $ccleanup\
-result [list\
"a[sp 7]bb[sp 6]ccc"
]
test untabify_stops_int_and_list {-stops <int> and an equivalent explicit stop list expand identically}\
-setup $csetup -body {
lappend result [punk::ansi::untabify -stops 4 "a\tbb\tccc"]
lappend result [punk::ansi::untabify -stops {1 5 9 13} "a\tbb\tccc"]
}\
-cleanup $ccleanup\
-result [list\
"a[sp 3]bb[sp 2]ccc"\
"a[sp 3]bb[sp 2]ccc"
]
test untabify_stops_terminal_mocked {-stops terminal uses punk::console::get_tabstops (mocked to 8-wide stops)}\
-setup $csetup -body {
lappend result [punk::ansi::untabify -stops terminal "a\tbb\tccc"]
}\
-cleanup $ccleanup\
-result [list\
"a[sp 7]bb[sp 6]ccc"
]
test untabify_with_unicode {-with unicode: tab becomes U+21E5 followed by U+00A0 non-breaking spaces}\
-setup $csetup -body {
lappend result [punk::ansi::untabify -with unicode "a\tbb"]
}\
-cleanup $ccleanup\
-result [list\
"a⇥[nbsp 6]bb"
]
test untabify_with_custom_pair {-with {tabchar spacechar}: custom single-column replacement characters}\
-setup $csetup -body {
lappend result [punk::ansi::untabify -with [list > .] "a\tbb"]
}\
-cleanup $ccleanup\
-result [list\
"a>......bb"
]
test untabify_multiline_leading_tab {multiline input: each line expands independently incl leading tabs}\
-setup $csetup -body {
lappend result [punk::ansi::untabify -stops 4 "a\tb\n\tindented"]
}\
-cleanup $ccleanup\
-result [list\
"a[sp 3]b\n[sp 4]indented"
]
test untabify_plastic_basic {-plastic (experimental, retained for future editbuf use): interim 8-multiple stops expansion}\
-setup $csetup -body {
lappend result [punk::ansi::untabify -plastic "a\tbb\tccc\ndddd\te\tf"]
}\
-cleanup $ccleanup\
-result [list\
"a[sp 7]bb[sp 6]ccc\ndddd[sp 4]e[sp 7]f"
]
test untabify_plastic_column_alignment {-plastic aligns cell columns across lines (elastic behaviour, quantized to interim 8-multiple stops)}\
-setup $csetup -body {
#the longest first-column cell (longertext, 10 wide) pushes BOTH lines'
#second column to the next interim stop (col 17) - the elastic property
lappend result [punk::ansi::untabify -plastic "short\tx\nlongertext\ty"]
}\
-cleanup $ccleanup\
-result [list\
"short[sp 11]x\nlongertext[sp 6]y"
]
test untabify_plastic_with_unicode {-plastic composes with -with unicode replacement characters}\
-setup $csetup -body {
lappend result [punk::ansi::untabify -plastic -with unicode "a\tbb\tccc"]
}\
-cleanup $ccleanup\
-result [list\
"a⇥[nbsp 6]bb⇥[nbsp 5]ccc"
]
test untabify_errors {no text argument and non int/list/terminal -stops raise usage errors}\
-setup $csetup -body {
lappend result [catch {punk::ansi::untabify}]
lappend result [catch {punk::ansi::untabify -stops bogus "a\tb"}]
}\
-cleanup $ccleanup\
-result [list\
1 1
]
}
tcltest::cleanupTests ;#needed to produce test summary line.

207
src/tests/modules/punk/ns/testsuites/ns/corp.test

@ -132,5 +132,212 @@ namespace eval ::testspace {
7 7
3 3
}] }]
#added 2026-07-14 (agent) - corp syntax/untabify/tab interplay characterization
#(precursor to the planned punk::ns hygiene pass). corp's basic highlighter is
#multiple punk::ansi::grepstr passes; grepstr does not handle tab characters
#(it warns on stderr and computes highlight columns as if tabs were absent), so
#proc bodies containing literal tabs need -untabify spaces|unicode for correct
#output. The default -untabify none interaction is pinned below as a known
#deficiency - deterministic only under mocked console tabstops.
#Console queries are mocked (overtype renderline.test pattern): the tab paths
#consult punk::console::get_tabstops / get_size, which emit live terminal
#queries that block or error in headless/piped environments.
proc mock_console {} {
if {[llength [info commands ::punk::console::get_tabstops]]} {
rename ::punk::console::get_tabstops ::testspace::__orig_get_tabstops
}
proc ::punk::console::get_tabstops {{inoutchannels {stdin stdout}}} {
set stops {}
for {set c 9} {$c <= 201} {incr c 8} {lappend stops $c}
return $stops
}
if {[llength [info commands ::punk::console::get_size]]} {
rename ::punk::console::get_size ::testspace::__orig_get_size
}
proc ::punk::console::get_size {args} {
return [dict create columns 80 rows 24]
}
#corp reads punk::console::tabwidth for its untabify stops - pin to 8
variable __had_tabwidth [info exists ::punk::console::tabwidth]
if {$__had_tabwidth} {
variable __orig_tabwidth $::punk::console::tabwidth
}
set ::punk::console::tabwidth 8
}
proc restore_console {} {
catch {rename ::punk::console::get_tabstops {}}
if {[llength [info commands ::testspace::__orig_get_tabstops]]} {
rename ::testspace::__orig_get_tabstops ::punk::console::get_tabstops
}
catch {rename ::punk::console::get_size {}}
if {[llength [info commands ::testspace::__orig_get_size]]} {
rename ::testspace::__orig_get_size ::punk::console::get_size
}
variable __had_tabwidth
if {$__had_tabwidth} {
variable __orig_tabwidth
set ::punk::console::tabwidth $__orig_tabwidth
} else {
unset -nocomplain ::punk::console::tabwidth
}
}
#capture stderr written during script (chan push transform; popped in all paths)
proc capture_stderr {script} {
variable caught ""
chan push stderr {apply {{cmd chan args} {
switch -- $cmd {
initialize {return {initialize finalize write}}
finalize {return}
write {append ::testspace::caught [lindex $args 0]; return ""}
}
}}}
set code [catch {uplevel 1 $script} r ropts]
chan pop stderr
if {$code} {
return -options $ropts $r
}
return $caught
}
#sp n - n plain spaces (keeps expected values reviewable)
proc sp {n} {return [string repeat { } $n]}
#fixture: proc with literal tab indentation and tabs inside a quoted string
proc make_tabby {} {
eval [string map [list <T> \t] {proc ::tabby {a {b bee}} {
<T>set x "col1<T>col2<T>col3"
<T>if {$a} {
<T><T>return $x
<T>}
<T>return $b
}}]
}
variable tabsetup {
set result ""
::testspace::mock_console
::testspace::make_tabby
}
variable tabcleanup {
rename ::tabby ""
::testspace::restore_console
}
test corp_syntax_basic_strip_equivalence {for a tab-free proc, ansistripped basic-syntax output equals -syntax none output}\
-setup $common -body {
proc spud5 {x {y why}} {
#a comment line
set d {a A}
if {$x} {
return [dict get $d a] ;#tail comment
}
return $y
}
set plain [punk::ns::corp -syntax none spud5]
set fancy [punk::ns::corp spud5]
lappend result [expr {[punk::ansi::ansistrip $fancy] eq $plain}]
#basic output contains ANSI highlighting (comment green = SGR 32)
lappend result [punk::ansi::ta::detect $fancy]
}\
-cleanup {
rename spud5 ""
}\
-result [list\
1 1
]
test corp_linenumber_syntax_none {-n with -syntax none: unstarred 1-based line numbers, body untouched (tabs intact)}\
-setup $tabsetup -body {
lappend result [punk::ns::corp -n -syntax none ::tabby]
}\
-cleanup $tabcleanup\
-result [list\
"1 proc ::tabby {a {b bee}} {\n2 \tset x \"col1\tcol2\tcol3\"\n3 \tif {\$a} {\n4 \t\treturn \$x\n5 \t}\n6 \treturn \$b\n7 }"
]
test corp_untabify_spaces {-untabify spaces: tab-free output, tabs expanded to mocked 8-wide stops}\
-setup $tabsetup -body {
set stripped [punk::ansi::ansistrip [punk::ns::corp -untabify spaces ::tabby]]
lappend result [expr {[string first \t $stripped] < 0}]
lappend result $stripped
#-syntax none -untabify spaces returns a proper 4 element proc statement
set plainlist [punk::ns::corp -syntax none -untabify spaces ::tabby]
lappend result [llength $plainlist] [lindex $plainlist 0]
}\
-cleanup $tabcleanup\
-result [list\
1\
"proc ::tabby {a {b bee}} {\n[sp 8]set x \"col1[sp 5]col2[sp 4]col3\"\n[sp 8]if {\$a} {\n[sp 16]return \$x\n[sp 8]}\n[sp 8]return \$b\n}"\
4 proc
]
test corp_untabify_unicode {-untabify unicode: no tabs remain, U+21E5 tab markers present}\
-setup $tabsetup -body {
set stripped [punk::ansi::ansistrip [punk::ns::corp -untabify unicode ::tabby]]
lappend result [expr {[string first \t $stripped] < 0}]
lappend result [expr {[string first ⇥ $stripped] >= 0}]
}\
-cleanup $tabcleanup\
-result [list\
1 1
]
test corp_untabify_none_tab_interaction {KNOWN DEFICIENCY: default -untabify none on a tabbed body - grepstr warns per pass and highlight overlays mangle tabbed lines}\
-setup $tabsetup -body {
#grepstr computes highlight columns as if tabs were absent - with the
#basic highlighter's multiple passes, brace overlays land at wrong
#columns on tab-containing lines (lines 3 and 5 below: a leaked
#close-brace and a shifted one). Deterministic only under the mocked
#tabstops. The
#stderr warning fires once per grepstr pass that sees tabs (5 passes).
#If tab handling is ever fixed (or corp moves off grepstr), flip these
#pins deliberately.
set errout [capture_stderr {
set ::testspace::corptabres [punk::ns::corp ::tabby]
}]
lappend result [regexp -all {Warning: grepstr does not currently handle tab} $errout]
lappend result {*}[split [punk::ansi::ansistrip $::testspace::corptabres] \n]
unset ::testspace::corptabres
set result
}\
-cleanup $tabcleanup\
-result [list\
5\
"proc ::tabby \{a \{b bee\}\} \{"\
"\tset x \"col1\tcol2\tcol3\""\
"[sp 7]\}if \{\$a\} \{"\
"\t\treturn \$x"\
" \}[sp 6]\}"\
"\treturn \$b"\
"\}"
]
if {[catch {info body ::tcl::CopyDirectory}]} {
tcltest::testConstraint have_tclCopyDirectory 0
} else {
tcltest::testConstraint have_tclCopyDirectory 1
}
test corp_copydirectory_untabify_spaces_smoke {::tcl::CopyDirectory (a real tab-containing proc): -untabify spaces output is tab-free with no grepstr warnings}\
-constraints have_tclCopyDirectory\
-setup {
set result ""
::testspace::mock_console
}\
-body {
set errout [capture_stderr {
set ::testspace::cdres [punk::ns::corp -untabify spaces ::tcl::CopyDirectory]
}]
set stripped [punk::ansi::ansistrip $::testspace::cdres]
unset ::testspace::cdres
lappend result [regexp -all {Warning: grepstr does not currently handle tab} $errout]
lappend result [expr {[string first \t $stripped] < 0}]
lappend result [string match "proc ::tcl::CopyDirectory *" $stripped]
}\
-cleanup {
::testspace::restore_console
}\
-result [list\
0 1 1
]
} }
tcltest::cleanupTests ;#needed to produce test summary. tcltest::cleanupTests ;#needed to produce test summary.

Loading…
Cancel
Save