From 6b4aeac95f3af62a9f13fe88a093b011e9c53db9 Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Fri, 7 Aug 2026 14:18:22 +1000 Subject: [PATCH] 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 --- src/tests/modules/AGENTS.md | 2 +- .../punk/args/testsuites/args/longopts.test | 163 ++++++++++++++++++ .../punk/args/testsuites/args/mashopts.test | 46 +++++ 3 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 src/tests/modules/punk/args/testsuites/args/longopts.test diff --git a/src/tests/modules/AGENTS.md b/src/tests/modules/AGENTS.md index e11bfdca..c3ce2956 100644 --- a/src/tests/modules/AGENTS.md +++ b/src/tests/modules/AGENTS.md @@ -43,7 +43,7 @@ Unit tests for editable source modules under `src/modules/`, `src/modules_tcl8/` - `commandstack/` — commandstack (cooperative command renaming) tests (`testsuites/commandstack/commandstack.test`, 2026-08-03 - characterisation suite + the G-160 hygiene-pass contract at commandstack 0.6.0 + the 0.7.0 convenience removal forms + the 0.7.1 reload-contract state guards (G-160 follow-ons, 2026-08-04); usage-driven from punk::packagepreference/packagetrace/packagesuppress/punk-auto_execok/punk::nav::fs-cd): record shape as a contract (token first/renamer second dict key order for the lsearch -index 1/-index 3 convention, trailing `did_rename` 0|1, `{implementation {} did_rename 0}` no-rename signal), COMMANDSTACKNEXT/COMMANDSTACKNEXT_ORIGINAL delegation + the `commandstack::next` helper (caller-context parity with the manual uplevel convention pinned), unique+monotonic per-(renamer,command) tokenids (same-renamer re-renames chain and are removable by exact token, third rename succeeds), multi-renamer stacking with removal in any order (bottom-removal re-linking), builtin renames (next_implementor `original`), remove_rename's three argument forms + unknown-renamer errors, the 0.7.0 convenience removal forms (pop_rename - command form pops topmost-for-renamer and returns the removed record, bare form searches live stacks with a multi-command ambiguity error; remove_renamer - all of a renamer's entries across live stacks with correct re-linking, Rename_stack-parked stacks skipped, removed records returned keyed by command; restore_original - whole-stack unwind to the original returning the record count, deliberately registering stack-evidenced renamers so it survives a known_renamers reset while the renamer-explicit forms keep the gate; all silent with debug off), the token->implementation map get_next_command resolves through (map/stack consistency pinned across rename/remove; parked stacks keep dispatching), channel discipline (silent full cycle with debug off; warnings only under debug), debug argument validation, -renamer misplacement errors, get_stack raw-key-first retrieval of Rename_stack-parked records + Rename_stack 1/0 returns, Delete_stack live-record guard (errors; empty/missing return 1), get_IMPLEMENTOR classification incl builtin-where-cmdtype-exists (dynamic expectation - undetermined on 8.6), lib::split_body round-trip, lib::splitx, show_stack fallback render, the reload contract (a module re-source preserves ALL state - stacks/token counters/token map/known_renamers/debug each info-exists guarded; delegation and exact-token removal keep working after reload), the help overview, and lazy punk::args registration of the PUNKARGS docs. Behavioural tests run in fresh child interps per test (module sourced by path relative to the test file; a ::puts shim captures module output for silence/warning assertions and keeps runner output clean); descriptions are single-line per the tcltestrun banner-parsing style guidance in src/tests/AGENTS.md (a hard contract until G-161 made the parser multi-line tolerant). Green on tclsh90 (9.0.3) and punk86 (8.6) - `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; extended 2026-08-05 by the G-151 pre-modification coverage survey - VIEW -lf 0/1/2 single-line-ization modes + always-on NUL + silent unknown-option tolerance (load-bearing for overtype's '-nul 1' sites) + C1/zero-width visuals, RANGE end-relative/clamping plus the merged-code-stack re-emission and trailing-code-drop semantics a truncate primitive must respect, COUNT-vs-length combining-accent divergence, NEW object basics, and the KNOWN-DEFICIENCY pin that VIEWCODES/VIEWSTYLE raise invalid-command in a plain punk::ansi interp (bare 'a+'/'a' interactive-alias dependency; pinned in a fresh child interp); plus the TRUNCATE primitive pins landed with G-151 (punk::ansi 0.2.0) - fits-unchanged byte identity incl styled fixtures, grapheme-exact capping with marker budget participation and marker-alone truncation, SGR-reset-before-marker on styled prefixes, CJK/combining-cluster grapheme safety, and strict option errors (deliberately unlike VIEW's pinned silent tolerance)), 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 (incl the 2026-08-05 candidacy fencing/fall-through primitive pins ahead of G-168: -regexprefail on optional value slots is form-fatal with no re-landing, unknown-option/dangling-option/option-value-fence failures fall through to sibling forms as successful parses, and the documented per-word limit - a complete option+value+file line double-matches a fenced scriptfile form and a dash-tolerant stdin form), registered-namespace lazy definition loading (`docpackages.test`, G-169 pre-work: inert registration/scan-vs-load split, id_exists never triggers loading, real_id/usage lazy resolution incl tag-prefixed script-level id families, duplicate-definer last-loaded-wins characterization), the G-168 launch-definition model parity (`punkexemodel.test`, punkexe moduledoc 0.7.0, id loading updated for the G-169 handover - the script id resolves via app-punkscript-docs from src/lib (auto_path derived from the test file location) while the moduledoc carries the core-owned ids: script/tclsh selection matrices against the real (script)::punkexe ids with the real-side dispatch/app oracle recorded in goals/archive/G-168, the bare-'-e' viable-incomplete verdict, the ./-e fence message, -encoding fall-through statuses, and the sanctioned complete-'-encoding' multipleformmatches LIMIT pins; the formcheck.test punkexe GAPs flipped to discriminated/sanctioned/selection-sound pins in the same arc), the G-151 landing report (`parsereport.test`, punk::args 0.18.0 parse_report: the canonical flag-like-word-consumed-as-VALUE attribution row, parsed-result vs words+withid entry parity, machine dict shape with declaration-section row order and absent-optional row omission, received xN multiplicity for solos/-multiple opts, type-aware always-marked elision at the default width plus the caller > @cmd -reportvaluewidth > built-in width cascade and never-elided dict returns, VIEW-style single-line-ization of control-bearing values, aliased-optionset storage-key row bridging, bordered-table/tableobject renders, words-form failure errorcode parity with parse, and the registered two-form definition's own render), rendering/indentation characterization, synopsis display characterization (`synopsis.test`: basic italic argname/`` 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 appear when the class is live OR is a declared forward unavailable class - dict labels on every runtime since G-073, unicode keeps the live-only rule; the G-073 forward-class adoption invariants and the USER-SANCTIONED 'di' prefix-strictness exemption (the model is deliberately stricter than real 8.6, full words parity-true); and the G-166 pin that an unavailable class's per-class virtual id LEADS its help with the unavailability statement on runtimes lacking the class while modern runtimes generate it unchanged. All expectations are derived from the running interpreter, green on 8.6/8.7/9.0 through runtests.tcl on each - native Tcl 8.6 has been a supported runner interpreter since 2026-07-21) +- `punk/args/` — punk::args tests (`testsuites/args/`): parsing, choices/choicegroups, forms (incl the 2026-08-05 candidacy fencing/fall-through primitive pins ahead of G-168: -regexprefail on optional value slots is form-fatal with no re-landing, unknown-option/dangling-option/option-value-fence failures fall through to sibling forms as successful parses, and the documented per-word limit - a complete option+value+file line double-matches a fenced scriptfile form and a dash-tolerant stdin form), registered-namespace lazy definition loading (`docpackages.test`, G-169 pre-work: inert registration/scan-vs-load split, id_exists never triggers loading, real_id/usage lazy resolution incl tag-prefixed script-level id families, duplicate-definer last-loaded-wins characterization), the G-168 launch-definition model parity (`punkexemodel.test`, punkexe moduledoc 0.7.0, id loading updated for the G-169 handover - the script id resolves via app-punkscript-docs from src/lib (auto_path derived from the test file location) while the moduledoc carries the core-owned ids: script/tclsh selection matrices against the real (script)::punkexe ids with the real-side dispatch/app oracle recorded in goals/archive/G-168, the bare-'-e' viable-incomplete verdict, the ./-e fence message, -encoding fall-through statuses, and the sanctioned complete-'-encoding' multipleformmatches LIMIT pins; the formcheck.test punkexe GAPs flipped to discriminated/sanctioned/selection-sound pins in the same arc), the G-151 landing report (`parsereport.test`, punk::args 0.18.0 parse_report: the canonical flag-like-word-consumed-as-VALUE attribution row, parsed-result vs words+withid entry parity, machine dict shape with declaration-section row order and absent-optional row omission, received xN multiplicity for solos/-multiple opts, type-aware always-marked elision at the default width plus the caller > @cmd -reportvaluewidth > built-in width cascade and never-elided dict returns, VIEW-style single-line-ization of control-bearing values, aliased-optionset storage-key row bridging, bordered-table/tableobject renders, words-form failure errorcode parity with parse, and the registered two-form definition's own render), rendering/indentation characterization, synopsis display characterization (`synopsis.test`: basic italic argname/`` 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`), longopt style-distinction and mash-edge characterisation (`longopts.test`, 2026-08-07 gap-fill: the gnu (`--flag=` inline-only; spaced/solo usage = badoptionformat) vs plain (`--flag` spaced-only; inline `=` = invalidoption) vs mixed (`-f|--file|--file=` both forms incl longopt prefix abbreviation) definition-grammar distinctions, first-`=`-only value splitting and empty inline values, the two `=`-member resolve-time definition errors, single-dash `-flag=value` never split at the `=`, and `@opts -any 1` adhoc passthrough where `--flag=value` splits but `-flag=value` stays a whole adhoc flagname expecting a value; the same gap-fill added to `mashopts.test`: unknown-letter-in-mash invalidoption rejection and the `-any` interplay - defined flags still mash while undefined mash-lookalike tokens are adhoc flags, never mash attempts; error-shape pins use the first three -errorcode elements), 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 appear when the class is live OR is a declared forward unavailable class - dict labels on every runtime since G-073, unicode keeps the live-only rule; the G-073 forward-class adoption invariants and the USER-SANCTIONED 'di' prefix-strictness exemption (the model is deliberately stricter than real 8.6, full words parity-true); and the G-166 pin that an unavailable class's per-class virtual id LEADS its help with the unavailability statement on runtimes lacking the class while modern runtimes generate it unchanged. All expectations are derived from the running interpreter, green on 8.6/8.7/9.0 through runtests.tcl on each - native Tcl 8.6 has been a supported runner interpreter since 2026-07-21) - `punk/nav/ns/` — punk::nav::ns tests (`testsuites/nav/navns.test`): the n/ n// n/// navigation state machine (ns/ transitions absolute/relative/glob-no-nav, failed-nav state preservation, quad-colon normalization, v-form content selection, ensemble annotation) and the ::punk::nav::ns::ns_current variable contract the repl/codethread/subshell seeding all consume; display content is covered in punk/ns nslist.test - `punk/repl/` — punk::repl tests (`testsuites/repl/`): opunk console backend integration (`consolebackends.test`) and repl current-namespace retention (`nscurrent.test`: real codethread via repl::init driven by synchronous runscript sends - inscope evaluation of ns_current, retention across submissions, n/-navigation retained, auto-create-with-notice for missing namespaces, the 2026-07-14 stray-namespace seeding fix pinned behaviourally plus a source-text guard on repl::start's inline template; the end-to-end piped subshell session is covered at shell level by shell/testsuites/punkexe/shellnavns.test - which found the first-subshell shared-code-interp asymmetry and the piped-inscope gap recorded there) - `punk/ns/` — punk::ns tests (`testsuites/ns/`): cmdwhich/cmdinfo/cmd_traverse doc-lookup flow (`cmdflow.test`, G-040 parity; plus the G-166 availability axis on the flowunavail/flowunavail_nodoc fixtures - the `unavailable` cmdinfo key present on every result, exact and unique-prefix landings on a `-choiceunavailable` name resolving that name's virtual docid ATTRIBUTED rather than resolving nothing, the choice traverse attributing without addressing when no virtual docid exists, and cmdtype staying unchanged throughout since availability is a second axis, never a cmdtype value), n/ display machinery characterization (`nslist.test`: tier A get_ns_dicts classification buckets as the machine contract for display reworks - incl package tail/prefix derivation, alias edge cases, usageinfo scan-dependence; tier B per-element layout-agnostic marking - underline/underdouble/underdotted namespace package styles, command type tag colours, exported/imported markers, the punkargs doc icon; tier C REWORK-flagged pins of the current hardcoded 2-col/4-col layout and nspath subtables, to flip deliberately with the planned punk-tables/width-responsive rework; plus the flipped nslist_types_default pin - bare nslist without -types displays all member types since punk::ns 0.7.1 fixed the braced-literal -types default), 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), cmdtrace characterization (`cmdtrace.test`: -pause 0 non-interactive runs, linedict line-mark keys for flat and 2-word-form nested switches as correct-mark guards, and GAP pins for the upstream nested-switch mismark - core.tcl-lang.org tktview 5d5b1052280c976ea3d4, arm bodies whose split-list index lands on a literal switch-command word report container-relative lines; mark tests gated on have_tclcoredocs because cmdtrace's arm-offset correction parses against the ::switch argdoc; plus the fixed-canary asserting punk::lib::check::has_tclbug_nestedswitch_tracelines still reports the bug - a live behavioural probe, so a fixed Tcl release fails the canary first and triggers the documented flip workflow), 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); the flipped G-051 pins for pseudo-command cmdtype (`doconly`) + space-form docid prefixes (real `string is` pins behind the have_tclcoredocs constraint); the G-166 unavailability marking on the helpstrua fixture and on the real `string is dict` subject - the `-return dict` key, the table/string marking with the info scheme suppressed so a valid argument tail no longer renders as a cleanly usable command line, and the `-return text` leading `UNAVAILABLE:` line (the two deliberate key-list flips - `cmdhelp_cmdinfo_result_shape`, `cmdhelp_return_dict_valid` - landed with it); remaining GAP pins for TclOO undocumented-method fallback (G-052) and synopsis marking absence (G-050); plus the G-150 flag-led form-narrowing + selection-soundness pins against punk::auto_exec::hash - bare-parse selection contract (incl the dash-led-name noformmatch cost), no-word/unknown-flag/ambiguity whole-render fallbacks, and hash runtime-behaviour-unchanged), and name/path primitive characterization (`nsprimitives.test`: string pins for nsparts/nsprefix/nstail/nsjoin/nsjoinall incl weird colon-run (`:::`) splitting, the trailing-colon parse ambiguity (`::x:` + `y` joins to the same string as `::x` + `:y` and reparses leading-colon-greedy), and prefix/tail/join round-trip and its absolutizing exceptions (the original twin-divergence pins for nsparts1/nsprefix1/nsprefix_orig/nstail1/nstail_orig served as safe-deletion evidence and were removed with the twins in the punk::ns 0.7.0 hygiene pass - divergence record in this file's git history, commit 0c7168a1); plus nseval fq-requirement/create-on-eval/evaluator-proc caching, the native-vs-punk `p:::x` resolution divergence (native namespace eval reaches child `x`, nseval creates/reaches literal `:x`), nseval_ifexists no-create + error propagation on plain and genuinely weird namespaces, nsexists/nschildren/nstree_raw weird-ns and relative-resolution pins, globmatchns `*`/`**`/`?` semantics (incl `*` matching a single inner colon - a formerly stale 'should be fixed' comment above nsglob_as_re was corrected in the 0.7.0 hygiene pass), and nspath_to_absolute/nspath_here_absolute caller-resolution pins; the nsjoinall error-message wart pin ('nsjoin:' prefix) flipped when punk::ns 0.7.1 fixed it) diff --git a/src/tests/modules/punk/args/testsuites/args/longopts.test b/src/tests/modules/punk/args/testsuites/args/longopts.test new file mode 100644 index 00000000..e9c5919e --- /dev/null +++ b/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. diff --git a/src/tests/modules/punk/args/testsuites/args/mashopts.test b/src/tests/modules/punk/args/testsuites/args/mashopts.test index 26f772b5..f65b0ecb 100644 --- a/src/tests/modules/punk/args/testsuites/args/mashopts.test +++ b/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.