Browse Source

punk::args: characterise longopt style distinctions + mash edges (new longopts.test)

Gap-fill from a coverage review of long-option and mashopt parsing: the module's
comment blocks specify style distinctions the suite never pinned. All behaviour
was live-probed before pinning (characterisation, not aspiration).

New testsuites/args/longopts.test (6 tests):
- mixed optset -f|--file|--file= accepts inline/spaced/short-alias forms incl
  unique-prefix abbreviation of the longopt itself (--fi=abc / --fi abc)
- gnu-only --flag= splits at the first = only (--filename=a=b -> a=b), accepts
  an empty inline value, rejects spaced and solo usage (badoptionformat)
- plain --flag accepts spaced only; inline =value is invalidoption
- the two =-member resolve-time definition errors (-type none with an = member;
  optional ?type? member without one) surface at first parse
- single-dash -flag=value is never split at the = (invalidoption)
- @opts -any 1 adhoc lane: undefined --flag=value splits to --flag + value, but
  single-dash -zz=5 stays a WHOLE adhoc flagname expecting a following value

mashopts.test additions (2 tests): a mash containing an undefined letter is
rejected whole (invalidoption naming the mashable set); under -any 1 -mash 1
defined flags still mash while undefined mash-lookalike tokens (-xy, -az) are
adhoc flags taking the next word as value - never mash attempts.

Error-shape pins use the first three -errorcode elements (PUNKARGS VALIDATION
+ failclass) rather than the ANSI-rendered messages; the two resolve errors are
pinned by distinctive message substrings (plain text, no ANSI).

Verified: runtests -include-paths modules/punk/args/testsuites/args
longopts.test mashopts.test via tclsh90s - 13/13 pass, no warnings.
src/tests/modules/AGENTS.md punk/args index entry updated (DOX pass).

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 3 days ago
parent
commit
6b4aeac95f
  1. 2
      src/tests/modules/AGENTS.md
  2. 163
      src/tests/modules/punk/args/testsuites/args/longopts.test
  3. 46
      src/tests/modules/punk/args/testsuites/args/mashopts.test

2
src/tests/modules/AGENTS.md

File diff suppressed because one or more lines are too long

163
src/tests/modules/punk/args/testsuites/args/longopts.test

@ -0,0 +1,163 @@
package require tcltest
package require punk::args
#Longopt (--flag / --flag=value) parsing characterisation - added 2026-08-07 as part of
#the longopt/mash coverage gap review. The gnu-style inline-value lane (--flag=value with
#choices, prefixes and optional ?type? members) is covered in opts.test and synopsis.test;
#this file pins the style DISTINCTIONS the definition grammar draws:
# --flag= gnu style - inline =value only; spaced and solo usage rejected
# --flag plain style - spaced value only; inline =value rejected
# -f|--file|--file= mixed - both forms accepted (the fossil-style optset)
#plus the resolve-time definition errors for = members, the rule that single-dash
#-flag=value tokens are never split at the =, and adhoc (@opts -any 1) passthrough of
#=value tokens.
#Error-shape pins use the first three -errorcode elements: PUNKARGS VALIDATION followed
#by the failclass element, e.g. (badoptionformat --filename= index -1).
namespace eval ::testspace {
namespace import ::tcltest::*
variable common {
set result ""
}
#added 2026-08-07 (agent) - longopt/mash characterisation gap fill
test longopts_mixed_optset_forms {a mixed optset -f|--file|--file= accepts inline, spaced and short-alias forms - storing under the last alternative stripped of =}\
-setup $common -body {
#gnu-style inline value
lappend result [dict get [punk::args::parse {--file=abc} withdef @opts {-f|--file|--file= -type string}] opts]
#spaced value via the longopt itself - allowed because the plain --file member is present
lappend result [dict get [punk::args::parse {--file abc} withdef @opts {-f|--file|--file= -type string}] opts]
#short alias spaced value
lappend result [dict get [punk::args::parse {-f abc} withdef @opts {-f|--file|--file= -type string}] opts]
#a unique prefix of the longopt works for both inline and spaced forms
lappend result [dict get [punk::args::parse {--fi=abc} withdef @opts {-f|--file|--file= -type string}] opts]
lappend result [dict get [punk::args::parse {--fi abc} withdef @opts {-f|--file|--file= -type string}] opts]
}\
-cleanup {
}\
-result [list\
{--file abc}\
{--file abc}\
{--file abc}\
{--file abc}\
{--file abc}\
]
test longopts_gnu_only_forms {a gnu-only --flag= optset takes inline =value split at the first = only; spaced and solo usage are badoptionformat errors}\
-setup $common -body {
#a value containing = - the split happens at the first = only
lappend result [dict get [punk::args::parse {--filename=a=b} withdef @opts {--filename= -type string}] opts]
#an empty inline value is accepted for -type string
lappend result [dict get [punk::args::parse {--filename=} withdef @opts {--filename= -type string}] opts]
#a spaced value is not allowed when only the = member exists
try {
punk::args::parse {--filename abc} withdef @opts {--filename= -type string}
lappend result "UNEXPECTED-accepted-spaced-value"
} on error {emsg eopts} {
lappend result [lrange [dict get $eopts -errorcode] 0 2]
}
#solo usage is not allowed either - the type is not an optional ?type? member
try {
punk::args::parse {--filename} withdef @opts {--filename= -type string}
lappend result "UNEXPECTED-accepted-solo"
} on error {emsg eopts} {
lappend result [lrange [dict get $eopts -errorcode] 0 2]
}
set result
}\
-cleanup {
}\
-result [list\
{--filename a=b}\
{--filename {}}\
{PUNKARGS VALIDATION {badoptionformat --filename= index -1}}\
{PUNKARGS VALIDATION {badoptionformat --filename= index -1}}\
]
test longopts_plain_doubledash_forms {a plain --flag optset accepts a spaced value but rejects inline =value as invalidoption}\
-setup $common -body {
lappend result [dict get [punk::args::parse {--filename abc} withdef @opts {--filename -type string}] opts]
try {
punk::args::parse {--filename=abc} withdef @opts {--filename -type string}
lappend result "UNEXPECTED-accepted-inline-value"
} on error {emsg eopts} {
lappend result [lrange [dict get $eopts -errorcode] 0 2]
}
set result
}\
-cleanup {
}\
-result [list\
{--filename abc}\
{PUNKARGS VALIDATION {invalidoption --filename=abc options --filename}}\
]
test longopts_resolve_definition_errors {-type none with an = member and an optional ?type? without an = member are both resolve-time definition errors}\
-setup $common -body {
#definitions are lazy - the resolve error surfaces at first parse
try {
punk::args::parse {--flag} withdef @opts {--flag= -type none}
lappend result "UNEXPECTED-resolved-none-with-eq"
} on error {emsg eopts} {
lappend result [string match "*flag type 'none'*not supported when any flag member ends with =*" $emsg]
}
try {
punk::args::parse {-f 3} withdef @opts {-f -type ?int?}
lappend result "UNEXPECTED-resolved-optional-without-eq"
} on error {emsg eopts} {
lappend result [string match "*Optional flag parameter*not supported when no flag member ends with =*" $emsg]
}
set result
}\
-cleanup {
}\
-result [list\
1\
1\
]
test longopts_singledash_no_inline_value {inline =value splitting applies only to double-dash longopts - a single-dash -flag=value token is not split}\
-setup $common -body {
#-f=abc is not recognised as -f with value abc - the whole token fails as an unknown option
try {
punk::args::parse {-f=abc} withdef @opts {-f|--file= -type string}
lappend result "UNEXPECTED-accepted-singledash-eq"
} on error {emsg eopts} {
lappend result [lrange [dict get $eopts -errorcode] 0 2]
}
set result
}\
-cleanup {
}\
-result [list\
{PUNKARGS VALIDATION {invalidoption -f=abc options -f|--file=}}\
]
test longopts_adhoc_any_passthrough {@opts -any 1 splits an undefined --flag=value at the = but a single-dash -flag=value is a whole adhoc flagname expecting a value}\
-setup $common -body {
#undefined --zz=5 splits to adhoc option --zz with value 5 - and is recorded as received
set argd [punk::args::parse {--zz=5} withdef {@opts -any 1} {-x -type int} {@values -min 0 -max 0}]
lappend result [dict get $argd opts]
lappend result [dict get $argd received]
#single-dash -zz=5 is NOT split - the whole token is the adhoc flagname and requires a following value
try {
punk::args::parse {-zz=5} withdef {@opts -any 1} {-x -type int} {@values -min 0 -max 0}
lappend result "UNEXPECTED-accepted-bare-singledash-eq"
} on error {emsg eopts} {
lappend result [lrange [dict get $eopts -errorcode] 0 2]
}
set argd [punk::args::parse {-zz=5 6} withdef {@opts -any 1} {-x -type int} {@values -min 0 -max 0}]
lappend result [dict get $argd opts]
}\
-cleanup {
}\
-result [list\
{--zz 5}\
{--zz 0}\
{PUNKARGS VALIDATION {missingoptionvalue -zz=5 index 0}}\
{-zz=5 6}\
]
}
tcltest::cleanupTests ;#needed to produce test summary line.

46
src/tests/modules/punk/args/testsuites/args/mashopts.test

@ -223,5 +223,51 @@ namespace eval ::testspace {
expected-error\
]
#added 2026-08-07 (agent) - longopt/mash characterisation gap fill
test mashopts_unknown_letter {a mash containing an undefined letter is rejected whole as invalidoption naming the mashable set}\
-setup $common -body {
try {
punk::args::parse {-abz} withdef {@opts -mash 1} {-a -type none} {-b -type none} {@values -min 0 -max 0}
lappend result "UNEXPECTED-accepted-unknown-letter"
} on error {emsg eopts} {
lappend result [lrange [dict get $eopts -errorcode] 0 2]
}
set result
}\
-cleanup {
}\
-result [list\
{PUNKARGS VALIDATION {invalidoption -abz options {-a -b}}}\
]
test mashopts_with_arbitrary_opts {with @opts -any 1 defined flags still mash but undefined dash tokens become whole adhoc flagnames expecting values - never mash attempts}\
-setup $common -body {
#defined flags still mash under -any
set argd [punk::args::parse {-ab} withdef {@opts -any 1 -mash 1} {-a -type none} {-b -type none} {@values -min 0 -max 0}]
lappend result [dict get $argd opts]
#a fully undefined token that looks like a mash is an adhoc flag taking the next word as its value
set argd [punk::args::parse {-xy foo} withdef {@opts -any 1 -mash 1} {-a -type none} {@values -min 0 -max 0}]
lappend result [dict get $argd opts]
#even when it starts with a defined mashable letter
set argd [punk::args::parse {-az foo} withdef {@opts -any 1 -mash 1} {-a -type none} {@values -min 0 -max 0}]
lappend result [dict get $argd opts]
#with no following word the adhoc flag is missing its value
try {
punk::args::parse {-az} withdef {@opts -any 1 -mash 1} {-a -type none} {@values -min 0 -max 0}
lappend result "UNEXPECTED-accepted-bare-adhoc"
} on error {emsg eopts} {
lappend result [lrange [dict get $eopts -errorcode] 0 2]
}
set result
}\
-cleanup {
}\
-result [list\
{-a 1 -b 1}\
{-xy foo}\
{-az foo}\
{PUNKARGS VALIDATION {missingoptionvalue -az index 0}}\
]
}
tcltest::cleanupTests ;#needed to produce test summary line.

Loading…
Cancel
Save