Browse Source

punk::args: characterize stringstartswith tail-clause reservation bug (pre-fix pins)

allocation.test gains coverage for the get_dict_can_assign_value multi-member
tail-clause reservation walk (a later optional clause mixing literal(...) and
stringstartswith(...) members, guarded from a greedy -multiple argument):

- guards (must hold pre and post fix): literal-only tail clause reserved
  correctly; non-matching trailing words correctly left to the -multiple arg
- allocation_tailclause_ssw_reservation_GAP pins the current bug: the
  stringstartswith(*) arm matches against $tp (the type string) instead of
  $rv (the value), so matching trailing words are swallowed by the -multiple
  argument (optional tail clause lost), and a prefix text that happens to
  prefix the type string (str vs stringstartswith(str)) wrongly reserves
  non-matching words, ending in a spurious toomanyarguments overflow

Verified 10/10 against the unfixed module under Tcl 9.0.3.

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 18 hours ago
parent
commit
0f649053ee
  1. 91
      src/tests/modules/punk/args/testsuites/args/allocation.test

91
src/tests/modules/punk/args/testsuites/args/allocation.test

@ -157,6 +157,97 @@ namespace eval ::testspace {
-cleanup $cleanup\
-result [list {1 valid} _default 1]
#added 2026-07-14 (agent) - tail-clause reservation walk in
#private::get_dict_can_assign_value: a LATER optional clause mixing literal(...)
#and stringstartswith(...) members should have its trailing words reserved from a
#greedy -multiple argument when (and only when) they match. The literal-only
#control and the no-match guard hold today and must keep holding.
test allocation_tailclause_literal_reservation_guard {tail-clause walk reserves trailing words for a literal-only optional clause}\
-setup {
set result ""
punk::args::define {
@id -id ::testspace::alloc_tail_lit
@values -min 0 -max -1
items -type string -multiple 1 -optional 1
ender -type {literal(with) literal(now)} -optional 1
}
}\
-body {
set argd [punk::args::parse {a b with now} withid ::testspace::alloc_tail_lit]
lappend result [dict get $argd values]
}\
-cleanup {
punk::args::undefine ::testspace::alloc_tail_lit 1
}\
-result [list\
{items {a b} ender {with now}}
]
test allocation_tailclause_ssw_nomatch_guard {tail-clause walk does not reserve trailing words that do not match the stringstartswith member}\
-setup {
set result ""
punk::args::define {
@id -id ::testspace::alloc_tail_ssw
@values -min 0 -max -1
items -type string -multiple 1 -optional 1
ender -type {literal(with) stringstartswith(v)} -optional 1
}
}\
-body {
#q99 does not start with v - items correctly consumes everything
set argd [punk::args::parse {a b with q99} withid ::testspace::alloc_tail_ssw]
lappend result [dict get $argd values]
}\
-cleanup {
punk::args::undefine ::testspace::alloc_tail_ssw 1
}\
-result [list\
{items {a b with q99}}
]
#GAP: the stringstartswith(*) arm of the multi-member tail-clause walk matches
#against $tp (the type string) instead of $rv (the candidate value), so:
# - genuinely matching trailing words are NOT reserved (the -multiple argument
# swallows them and the optional tail clause is lost), and
# - a prefix text that happens to prefix the literal type string (e.g 'str'
# prefixing 'stringstartswith(str)') reserves NON-matching words, ending in a
# spurious overflow error.
#Expected post-fix: {a b with v99} -> items {a b} ender {with v99};
# {a b with zzz} (vs stringstartswith(str)) -> items {a b with zzz}
test allocation_tailclause_ssw_reservation_GAP {GAP: stringstartswith tail-clause member compares the type string, not the value}\
-setup {
set result ""
punk::args::define {
@id -id ::testspace::alloc_tail_ssw_v
@values -min 0 -max -1
items -type string -multiple 1 -optional 1
ender -type {literal(with) stringstartswith(v)} -optional 1
}
punk::args::define {
@id -id ::testspace::alloc_tail_ssw_str
@values -min 0 -max -1
items -type string -multiple 1 -optional 1
ender -type {literal(with) stringstartswith(str)} -optional 1
}
}\
-body {
#matching trailing words lost to items
set argd [punk::args::parse {a b with v99} withid ::testspace::alloc_tail_ssw_v]
lappend result [dict get $argd values]
#non-matching trailing words wrongly reserved -> spurious overflow error
set err [catch {punk::args::parse {a b with zzz} withid ::testspace::alloc_tail_ssw_str} msg opts]
lappend result $err [lrange [dict get $opts -errorcode] 0 2]
}\
-cleanup {
punk::args::undefine ::testspace::alloc_tail_ssw_v 1
punk::args::undefine ::testspace::alloc_tail_ssw_str 1
}\
-result [list\
{items {a b with v99}}\
1 {PUNKARGS VALIDATION {toomanyarguments 4 index 2}}
]
cleanupTests
}
namespace delete ::testspace

Loading…
Cancel
Save