Browse Source

G-083 increment 2: punk::args optionconflict parse-time enforcement (0.20.0)

The -conflicts and -parsekeymode error vocabulary declared at define time
(0.19.0) is now enforced at parse. A new optionconflict failure class joins the
PUNKARGS VALIDATION errorcode vocabulary, mirroring optionmissing's shape:
{optionconflict <arg_a> <arg_b> received <receivednames>} - it names both
offending received arguments, for both per-arg -conflicts violations (any
pair, cross-group) and -parsekeymode error group co-occurrence (distinct
members of a shared-parsekey group).

The check runs in a single post-resolution site in get_dict_form (after the
optionmissing/valuemissing block), so the ordinary option path and the mash
(short-flag bundling) path share it - both raise identically. Checked against
RECEIVED arguments only (defaults never conflict), after prefix/abbreviation
resolution, on optset identity (a new optsets_received tracker for options,
since flagsreceived collapses shared-parsekey members onto one api_opt). Runs
unconditionally - a received conflict is a hard contradiction in any mode, not
end-of-input exhaustion, so parse_status_classify maps optionconflict to invalid
(not incomplete) and the candidacy/viability probe reports it.

Define-time check added: a group marked -parsekeymode error must also declare a
non-empty -parsekey (the strict mode only applies to a shared-parsekey group).

relations.test extended with 7 enforcement pins (raise cases for -conflicts
and -parsekeymode error, defaults-never-conflict, cross-group conflicts,
one-received-ok, override-legacy last-wins, parse_status invalid). buildversion
0.19.0 -> 0.20.0.

Bug found and fixed during the increment: the group-co-occurrence dedup check
initially used `ni` (not-in) where `in` was meant - `ni` returns true on an
empty list, so it skipped every member and the check never fired. Corrected
to `in`.

Legacy untouched by default: -parsekeymode override is the default and absent
-conflicts means no check runs, so the full existing suite (including the
pinned parsekey_repeat_ordering last-wins / prepend-defaults idiom) passes
unchanged.

Verification: full punk/args suite via canonical tclsh90s - 363 total / 360
passed / 3 skipped (punkargsKnownBug) / 0 failed; punk/ns suite 125/125 clean.
goals_lint clean. No punkproject.toml bump (module API addition, not shell-
level user-visible behaviour yet - the lsearch moduledoc adoption that ships
user-visible behaviour lands in increment 3).

Assisted-by: harness=pi; primary-model=huggingface/zai-org/GLM-5.2; api-location=huggingface.co
master
Julian Noble 2 days ago
parent
commit
43f009ed80
  1. 27
      goals/G-083-punkargs-argument-relations.md
  2. 97
      src/modules/punk/args-999999.0a1.0.tm
  3. 3
      src/modules/punk/args-buildversion.txt
  4. 2
      src/tests/modules/AGENTS.md
  5. 107
      src/tests/modules/punk/args/testsuites/args/relations.test

27
goals/G-083-punkargs-argument-relations.md

@ -108,7 +108,26 @@ Design leans (settle and record here during the work):
clicks uses `-parsekeymode error` on its single named group (full pairwise
exclusivity). Both adoption cases use named groups. Unnamed shared-parsekey
groups default to override (a user wanting strict adds `-group`).
- Remains for acceptance: increment 2 (parse-time optionconflict raise in the
shared post-resolution block, mash+ordinary parity, parse_status_classify);
increment 3 (synopsis/usage hints for conflicts/parsekeymode; lsearch + clock
clicks moduledoc adoption and dropping the caveat).
- Remains for acceptance: increment 3 (synopsis/usage hints for conflicts/parsekeymode;
lsearch + clock clicks moduledoc adoption and dropping the caveat).
- 2026-08-07 increment 2 (parse-time enforcement, punk::args 0.20.0): the optionconflict
failure class joins PUNKARGS VALIDATION, mirroring optionmissing's shape
{optionconflict <arg_a> <arg_b> received <receivednames>}. Enforced in a single
post-resolution site in get_dict_form (after the optionmissing/valuemissing block)
so the ordinary option path and the mash (short-flag bundling) path share it - both
raise identically (verified: -ab mash with -conflicts and with -parsekeymode error
both raise). Checked against RECEIVED args only (defaults never conflict, pinned),
after prefix/abbreviation resolution, on optset identity (a new optsets_received
tracker for options, since flagsreceived collapses shared-parsekey members onto
one api_opt). Runs unconditionally - a received conflict is a hard contradiction,
not end-of-input exhaustion, so parse_status_classify maps optionconflict to invalid
(not incomplete) and the candidacy/viability probe reports it. Define-time check
added: a -parsekeymode error group must also declare a non-empty -parsekey.
relations.test extended with 7 enforcement pins (raise cases for -conflicts and
-parsekeymode error, defaults-never-conflict, cross-group conflicts, one-received-ok,
override-legacy last-wins, parse_status invalid). Legacy untouched by default
confirmed: full punk/args suite 363 total / 360 passed / 3 skipped / 0 failed via
tclsh90s; punk/ns suite 125/125 clean.
- Bug found and fixed during increment 2: the group-co-occurrence dedup check
initially used `ni` (not-in) where `in` was meant - `ni` returns true on an empty
list, so it skipped every member and the check never fired. Corrected to `in`.

97
src/modules/punk/args-999999.0a1.0.tm

@ -3814,6 +3814,20 @@ tcl::namespace::eval punk::args {
}
unset -nocomplain _g83_argnames _g83_resolve_targets _g83_an _g83_pk _g83_other _g83_other_pk _g83_t _g83_tpk _g83_conflicts _g83_target
#(c) a group marked -parsekeymode error must have a non-empty -parsekey -
#the strict mode only has meaning for a shared-parsekey group (distinct
#members collide on the shared result key); an unnamed or keyless group
#has no shared key to conflict on.
dict for {_g83_g _g83_ginfo} [dict get $FDICT OPT_GROUPS] {
if {[punk::args::system::Dict_getdef $_g83_ginfo -parsekeymode override] ne "error"} continue
if {$_g83_g eq ""} continue ;#-parsekeymode required -group at the @opts line already
set _g83_gpk [punk::args::system::Dict_getdef $_g83_ginfo -parsekey ""]
if {$_g83_gpk eq ""} {
error "punk::args::resolve - -parsekeymode error for group '$_g83_g' requires the group to also declare a non-empty -parsekey (the strict mode only applies to a shared-parsekey group). @id:$DEF_definition_id"
}
}
unset -nocomplain _g83_g _g83_ginfo _g83_gpk
#set mashargs [dict get $F $fid OPT_MASHES]
set mashargs [dict get $FDICT OPT_MASHES]
if {[llength $mashargs]} {
@ -7167,6 +7181,12 @@ tcl::namespace::eval punk::args {
missingrequiredleader - missingrequiredvalue - leadermissing - optionmissing - valuemissing - missingoptionvalue {
return incomplete
}
optionconflict {
#G-083: two received arguments marked as conflicting (per-arg -conflicts, or
#distinct members of a -parsekeymode error group) - a hard contradiction,
#not end-of-input exhaustion: appending words cannot resolve it.
return invalid
}
leadingvaluecount - trailingvaluecount {
#payload: <num> min <min> max <max>
set num [lindex $payload 0]
@ -10455,6 +10475,7 @@ tcl::namespace::eval punk::args {
}
#puts "-arg_info->$arg_info"
set flagsreceived [list] ;#for checking if required flags satisfied
set optsets_received [list] ;#G-083: per-optset received tracker (options only) for -conflicts/-parsekeymode checks - flagsreceived collapses shared-parsekey members onto one api_opt, so the optset is tracked separately here
set solosreceived [list]
set multisreceived [list]
#secondary purpose:
@ -10503,7 +10524,7 @@ tcl::namespace::eval punk::args {
set OPT_ALL_MASH_LETTERS [dict get $formdict OPT_ALL_MASH_LETTERS]
set OPTSPEC_DEFAULTS [dict get $formdict OPTSPEC_DEFAULTS]
set OPT_CHECKS_DEFAULTS [dict get $formdict OPT_CHECKS_DEFAULTS]
#set OPT_GROUPS [dict get $formdict OPT_GROUPS]
set OPT_GROUPS [dict get $formdict OPT_GROUPS] ;#G-083: -parsekeymode per-group strict mode
set VAL_DEFAULTS [dict get $formdict VAL_DEFAULTS]
set VAL_REQUIRED [dict get $formdict VAL_REQUIRED]
@ -11178,6 +11199,7 @@ tcl::namespace::eval punk::args {
}
}
lappend flagsreceived $api_opt
lappend optsets_received $mashflagoptionset
incr posn
}
#update vals_remaining_possible by one or 2 if the last flag took a value.
@ -11418,6 +11440,7 @@ tcl::namespace::eval punk::args {
lappend solosreceived $api_opt ;#dups ok
}
lappend flagsreceived $api_opt ;#dups ok
lappend optsets_received $optionset ;#G-083
} else {
#starts with - but unmatched option flag
#comparison to valmin already done above
@ -12287,6 +12310,78 @@ tcl::namespace::eval punk::args {
}
}
#---------------------------------------------------------------------------------------------
#G-083: optionconflict - per-arg -conflicts and @opts -parsekeymode error group exclusivity.
#Checked against RECEIVED arguments only (defaults never conflict), after prefix/
#abbreviation resolution, on the OPTSET identity (not the received-name form - flagsreceived
#collapses distinct shared-parsekey members onto one api_opt, so the matched optset is
#tracked in optsets_received for options). Runs unconditionally: a received conflict is a
#hard contradiction in any mode (candidacy probe or live parse), not end-of-input
#exhaustion, so it is not gated by the skip_* flags above. Mirrors the optionmissing
#errorcode shape: PUNKARGS VALIDATION {optionconflict <opt_a> <opt_b> received <receivednames>}.
set _g83_received_optsets $optsets_received
foreach _g83_n $leadernames_received {if {[dict exists $ARG_INFO $_g83_n]} {lappend _g83_received_optsets $_g83_n}}
foreach _g83_n $valnames_received {if {[dict exists $ARG_INFO $_g83_n]} {lappend _g83_received_optsets $_g83_n}}
#effective parsekey of an optset: its own -parsekey, else the group's -parsekey if it
#is in a named group that declares one, else "" (the member then keys on its api_opt).
set _g83_effpk [dict create]
set _g83_pk_to_optsets [dict create]
foreach _g83_optset $OPT_NAMES {
set _g83_p [punk::args::system::Dict_getdef [dict get $ARG_INFO $_g83_optset] -parsekey ""]
if {$_g83_p eq ""} {
set _g83_grp [punk::args::system::Dict_getdef [dict get $ARG_INFO $_g83_optset] -group ""]
if {$_g83_grp ne "" && [dict exists $OPT_GROUPS $_g83_grp]} {
set _g83_p [punk::args::system::Dict_getdef [dict get $OPT_GROUPS $_g83_grp] -parsekey ""]
}
}
dict set _g83_effpk $_g83_optset $_g83_p
if {$_g83_p ne ""} {tcl::dict::lappend _g83_pk_to_optsets $_g83_p $_g83_optset}
}
#G-083 conflict check: per-arg -conflicts (iterate received optsets in definition order
#for deterministic output; report the first conflicting pair).
foreach _g83_optset $OPT_NAMES {
if {$_g83_optset ni $_g83_received_optsets} continue
set _g83_conflicts [punk::args::system::Dict_getdef [dict get $ARG_INFO $_g83_optset] -conflicts {}]
if {![llength $_g83_conflicts]} continue
foreach _g83_t $_g83_conflicts {
#resolve target to optset(s): flagname spelling, then argname, then parsekey
if {[dict exists $lookup_optset $_g83_t]} {
set _g83_t_optsets [list [dict get $lookup_optset $_g83_t]]
} elseif {[dict exists $ARG_INFO $_g83_t]} {
set _g83_t_optsets [list $_g83_t]
} elseif {[dict exists $_g83_pk_to_optsets $_g83_t]} {
set _g83_t_optsets [dict get $_g83_pk_to_optsets $_g83_t]
} else {
set _g83_t_optsets [list] ;#define-time should have rejected it - defensive skip
}
foreach _g83_t_optset $_g83_t_optsets {
if {$_g83_t_optset eq $_g83_optset} continue ;#not a conflict with itself
if {$_g83_t_optset ni $_g83_received_optsets} continue ;#target not received
set msg "Arguments '$_g83_optset' and '$_g83_t_optset' for %caller% are marked as conflicting (-conflicts) and may not be supplied together."
return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list optionconflict $_g83_optset $_g83_t_optset received [list {*}$leadernames_received {*}$flagsreceived {*}$valnames_received]] -argspecs $argspecs]] $msg
}
}
}
#G-083 group exclusivity: @opts -parsekeymode error - distinct members of a shared-
#parsekey group received together is an error (iterate groups for deterministic output).
dict for {_g83_g _g83_ginfo} $OPT_GROUPS {
if {[punk::args::system::Dict_getdef $_g83_ginfo -parsekeymode override] ne "error"} continue
set _g83_g_received [list]
foreach _g83_optset $OPT_NAMES {
if {$_g83_optset ni $_g83_received_optsets} continue
set _g83_grp [punk::args::system::Dict_getdef [dict get $ARG_INFO $_g83_optset] -group ""]
if {$_g83_grp ne $_g83_g} continue
if {$_g83_optset in $_g83_g_received} continue
lappend _g83_g_received $_g83_optset
}
if {[llength $_g83_g_received] > 1} {
set _g83_a [lindex $_g83_g_received 0]
set _g83_b [lindex $_g83_g_received 1]
set msg "Arguments '$_g83_a' and '$_g83_b' for %caller% belong to mutually exclusive group '$_g83_g' (-parsekeymode error) and may not be supplied together."
return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list optionconflict $_g83_a $_g83_b received [list {*}$leadernames_received {*}$flagsreceived {*}$valnames_received]] -argspecs $argspecs]] $msg
}
}
unset -nocomplain _g83_received_optsets _g83_n _g83_effpk _g83_pk_to_optsets _g83_optset _g83_p _g83_grp _g83_conflicts _g83_t _g83_t_optsets _g83_t_optset _g83_g _g83_ginfo _g83_g_received _g83_a _g83_b
#---------------------------------------------------------------------------------------------
#maintain order of opts $opts values $values as caller may use lassign.
set receivednames [list {*}$leadernames_received {*}$flagsreceived {*}$valnames_received]

3
src/modules/punk/args-buildversion.txt

@ -1,6 +1,7 @@
0.19.0
0.20.0
#First line must be a semantic version number
#all other lines are ignored.
#0.20.0 - G-083 increment 2 (argument-relations parse-time enforcement): the -conflicts and -parsekeymode error vocabulary declared at define time (0.19.0) is now enforced at parse. A new optionconflict failure class joins the PUNKARGS VALIDATION errorcode vocabulary, mirroring optionmissing's shape: {optionconflict <arg_a> <arg_b> received <receivednames>} - it names both offending received arguments, for both per-arg -conflicts violations (any pair, cross-group) and -parsekeymode error group co-occurrence (distinct members of a shared-parsekey group). The check runs in a single post-resolution site in get_dict_form (after the optionmissing/valuemissing block), so the ordinary option path and the mash (short-flag bundling) path share it - both raise identically. Checked against RECEIVED arguments only (defaults never conflict), after prefix/abbreviation resolution, on optset identity (a new optsets_received tracker for options, since flagsreceived collapses shared-parsekey members onto one api_opt). Runs unconditionally - a received conflict is a hard contradiction in any mode, not end-of-input exhaustion, so the candidacy/viability probe reports it as status invalid (parse_status_classify maps optionconflict to invalid, not incomplete). Define-time check added: a group marked -parsekeymode error must also declare a non-empty -parsekey (the strict mode only applies to a shared-parsekey group). Legacy untouched by default: -parsekeymode override is the default and absent -conflicts means no check runs, so the full existing suite (including the pinned parsekey_repeat_ordering last-wins / prepend-defaults idiom) passes unchanged. relations.test extended with 7 parse-time enforcement pins (raise cases, defaults-never-conflict, cross-group, parsekeymode error raise + one-received-ok + override-legacy, parse_status invalid); full punk/args suite green.
#0.19.0 - G-083 increment 1 (argument-relations define-time vocabulary): new per-argument key -conflicts <list> (parsekeys or flag names that must not be RECEIVED together with this argument; checked against received args only at parse time, defaults never conflict - parse-time optionconflict enforcement lands in a later G-083 increment) and new @opts-level -parsekeymode override|error (per named OPT_GROUPS group; error = distinct-member co-occurrence within a shared-parsekey group raises optionconflict at parse; override = legacy last-wins, the default). Both keys are cross-validated at resolve: -conflicts targets must name a defined argname or declared -parsekey; -parsekeymode requires -group and a value of override|error. Define-time integrity hole closed: a -parsekey value colliding with a distinct defined argument's name (one that does not share that -parsekey) is now a resolve error instead of silently forming an implicit shared-key group (parsekey_collides_with_defined_optname_GAP flipped to a define-time error pin in parsekey.test). Legacy behaviour untouched by default (no -conflicts, default -parsekeymode override, no colliding parsekey). New testsuite relations.test pins the define-time vocabulary; full punk/args suite green.
#0.18.0 - G-151 annotated success render: new punk::args::parse_report - landing report for a SUCCESSFUL parse, one row per value-holding argument (received, or filled from -default) in declaration-section order (leaders, opts, values) with columns Argument | Source | Value - the success-side sibling of the arg_error usage table (a flag-like word consumed as a VALUE by position shows attributed to its consuming argument - the make.tcl dry-run 'kitname = punk91 -confirm 0' class). Input: a punk::args::parse result dict (its id key must resolve to a registered definition), or words + 'withid <id>' (the parse is performed first; a rejected line raises the same validation error parse would - only successful parses have a landing report). Source column: received / received xN (-multiple aggregation count) / default - the plain-mode-safe twin of the colour marking (received CLR(check), default CLR(parsekey_hint)); absent optionals with no default-in-effect get no row. Returns: table (arg_error-styled bordered block via textblock; the default), tableobject, string (plain lines 'name (source) = value'; also the degraded no-textblock path), dict (machine form: id/form/rows with class/source/received/positions/multiple/hasvalue/value - never elided). Display elision is type-aware, deterministic and always marked (display-hint honesty): strings single-line-ized (ansistring VIEW -lf 1 -vt 1 -sp 0 - embedded controls/ANSI render visibly so a value cannot disturb the table; real spaces stay spaces) then grapheme-capped via the new ansistring TRUNCATE (punk::ansi 0.2.0) with trailing '...' plus ' (len N)' citing the raw value's string length; -multiple aggregations and -type list values element-capped with '(+N more)' (whole list-quoted elements kept while they fit, first element char-capped if it alone overflows); -type dict values pair-capped with '(+N pairs)'; markers stay intact so cells can exceed very small caps by the marker width; plain-string fallbacks when punk::ansi is unavailable. Width cascade per the G-149 plumbing precedent: caller -valuewidth > new @cmd directive key -reportvaluewidth > built-in 48; 0 means no cap; deliberately never console-derived (deterministic and pipe-safe - the G-007 emit-then-query lesson). Value-in-effect lookup bridges storage-key folds at display level (-parsekey renames, aliased optionsets' last-|-member finalopts fold) so received aliased/parsekey'd arguments keep their row - G-084 owns the storage-key model itself. Failure-side rendering (arg_error/usage) unchanged. make.tcl retiring its interim one-line 'dry-run: line accepted ...' report in favour of this render is consumer follow-through after a bootsupport promotion (not part of this change). New testsuite parsereport.test; define -help documents @cmd -reportvaluewidth beside the other directive-options.
#0.17.0 - G-149 configurable multi-form synopsis hint labels: the per-form hint labels rendered beside a multi-form definition's synopsis lines in the usage/arg_error table (hard-coded ' i -form N ...' previously) are now a template substituted per form - %formindex% replaced with the form's 0-based ordinal, %formname% with its name; an empty template suppresses the labels entirely (the cell keeps just the Synopsis: label, as single-form renders do). Precedence: caller option > definition > built-in default. New arg_error/usage option -formhint <template> (supplied-empty means suppress; usage pulls it from arg_error's definition via resolved_def); new @cmd directive key -formhint <template> for definition-supplied hints (rides the existing arbitrary-key cmd_info merge, so resolved_def round-trips it; not display-deferred). Unconfigured definitions render byte-identical to 0.16.0 (default template reproduces the exact prior bytes, verified by capture diff). Motivation (G-143 origin): script-level (script) ids - e.g make.tcl's - are unreachable from any repl, so the repl-oriented ' i -form N ...' default is not actionable there; consumers can now name their own per-form navigation. The string (non-table) renderer never carried hint labels and is unchanged. make.tcl adoption is consumer follow-through after a bootsupport promotion (not part of this change). New testsuite formhint.test; define -help documents the @cmd key beside the other directive-options.

2
src/tests/modules/AGENTS.md

File diff suppressed because one or more lines are too long

107
src/tests/modules/punk/args/testsuites/args/relations.test

@ -41,9 +41,9 @@ namespace eval ::testspace {
}\
-result [list 1]
test parsekeymode_stored {@opts -parsekeymode error with -group is accepted and stored on the OPT_GROUPS entry}\
test parsekeymode_stored {@opts -parsekeymode error with -group and -parsekey is accepted and stored on the OPT_GROUPS entry}\
-setup $common -body {
set argd [punk::args::parse {} withdef {@opts -group G -parsekeymode error} {-x -type none}]
set argd [punk::args::parse {} withdef {@opts -group G -parsekey -pk -parsekeymode error} {-x -type none}]
lappend docids [dict get $argd id]
set spec [punk::args::get_spec [dict get $argd id]]
dict for {fid fd} [dict get $spec FORMS] {
@ -87,10 +87,10 @@ namespace eval ::testspace {
}\
-result [list 1]
test conflicts_valid_flagname_target {per-arg -conflicts naming a defined flag is accepted at define time}\
test conflicts_valid_flagname_target {per-arg -conflicts naming a defined flag is accepted at define time (parse with only one member received)}\
-setup $common -body {
#accepted at define time (parse-time enforcement lands in a later G-083 increment)
set argd [punk::args::parse {-x -y} withdef @opts {-x -type none -conflicts -y} {-y -type none}]
#accepted at define time; supplying only -x (not -y) does not trigger the conflict
set argd [punk::args::parse {-x} withdef @opts {-x -type none -conflicts -y} {-y -type none}]
lappend docids [dict get $argd id]
lappend result ok
set result
@ -100,9 +100,9 @@ namespace eval ::testspace {
}\
-result [list ok]
test conflicts_valid_parsekey_target {per-arg -conflicts naming a declared -parsekey is accepted at define time}\
test conflicts_valid_parsekey_target {per-arg -conflicts naming a declared -parsekey is accepted at define time (parse with only one member received)}\
-setup $common -body {
set argd [punk::args::parse {-x -y} withdef @opts {-x -type none -parsekey -pk -conflicts -y} {-y -type none -parsekey -pk}]
set argd [punk::args::parse {-x} withdef @opts {-x -type none -parsekey -pk -conflicts -y} {-y -type none -parsekey -pk}]
lappend docids [dict get $argd id]
lappend result ok
set result
@ -112,5 +112,98 @@ namespace eval ::testspace {
}\
-result [list ok]
#added 2026-08-07 (agent) - G-083 increment 2 parse-time enforcement
test conflicts_raises {per-arg -conflicts raises optionconflict naming both offending received arguments when both are supplied}\
-setup $common -body {
try {
punk::args::parse {-x -y} withdef @opts {-x -type none -conflicts -y} {-y -type none}
lappend result "UNEXPECTED-accepted"
} on error {emsg eopts} {
lappend result [lrange [dict get $eopts -errorcode] 0 2]
}
set result
}\
-result [list {PUNKARGS VALIDATION {optionconflict -x -y received {-x -y}}}]
test conflicts_defaults_never_conflict {a -conflicts target supplied only via -default does not raise (defaults never conflict)}\
-setup $common -body {
#-y has a default; supplying only -x (the -conflicts source) must not raise
#because -y was not RECEIVED (only defaulted)
set argd [punk::args::parse {-x} withdef @opts {-x -type none -conflicts -y} {-y -type none -default 1}]
lappend docids [dict get $argd id]
lappend result ok
set result
}\
-cleanup {
foreach id $docids {punk::args::undefine $id 1}
}\
-result [list ok]
test conflicts_cross_group {per-arg -conflicts works across distinct groups (no shared parsekey needed)}\
-setup $common -body {
try {
punk::args::parse {-bisect -all} withdef @opts {-bisect -type none -conflicts {-all -not}} {-all -type none} {-not -type none}
lappend result "UNEXPECTED-accepted"
} on error {emsg eopts} {
lappend result [lrange [dict get $eopts -errorcode] 0 2]
}
set result
}\
-result [list {PUNKARGS VALIDATION {optionconflict -bisect -all received {-bisect -all}}}]
test parsekeymode_error_raises {@opts -parsekeymode error raises optionconflict when distinct members of the shared-parsekey group are both received}\
-setup $common -body {
try {
punk::args::parse {-a -b} withdef {@opts -group G -parsekey -pk -parsekeymode error} {-a -type none} {-b -type none}
lappend result "UNEXPECTED-accepted"
} on error {emsg eopts} {
lappend result [lrange [dict get $eopts -errorcode] 0 2]
}
set result
}\
-result [list {PUNKARGS VALIDATION {optionconflict -a -b received {-pk -pk}}}]
test parsekeymode_error_one_received_ok {@opts -parsekeymode error with only one member received does not raise}\
-setup $common -body {
set argd [punk::args::parse {-a} withdef {@opts -group G -parsekey -pk -parsekeymode error} {-a -type none} {-b -type none}]
lappend docids [dict get $argd id]
lappend result ok
set result
}\
-cleanup {
foreach id $docids {punk::args::undefine $id 1}
}\
-result [list ok]
test parsekeymode_override_legacy {@opts -parsekeymode override (the default) keeps last-wins for distinct members - no raise}\
-setup $common -body {
#explicit override + the default (no -parsekeymode) both last-wins
set argd [punk::args::parse {-a -b} withdef {@opts -group G -parsekey -pk -parsekeymode override} {-a -type none} {-b -type none}]
lappend docids [dict get $argd id]
lappend result [dict get $argd opts]
punk::args::undefine [dict get $argd id] 1
set argd [punk::args::parse {-a -b} withdef {@opts -group G -parsekey -pk} {-a -type none} {-b -type none}]
lappend docids [dict get $argd id]
lappend result [dict get $argd opts]
set result
}\
-cleanup {
foreach id $docids {punk::args::undefine $id 1}
}\
-result [list {-pk 1} {-pk 1}]
test optionconflict_parse_status_invalid {parse_status reports an optionconflict failure as status invalid (not incomplete)}\
-setup $common -body {
set ps [punk::args::parse_status {-x -y} withdef @opts {-x -type none -conflicts -y} {-y -type none}]
lappend docids [dict get $ps id]
lappend result [dict get $ps status]
lappend result [dict get $ps failureclass]
set result
}\
-cleanup {
foreach id $docids {punk::args::undefine $id 1}
}\
-result [list invalid optionconflict]
}
tcltest::cleanupTests ;#needed to produce test summary line.
Loading…
Cancel
Save