Skip to content

Commit 066f6f0

Browse files
committed
Expand the tests
1 parent bfd4ee0 commit 066f6f0

1 file changed

Lines changed: 113 additions & 24 deletions

File tree

WoofWare.Myriad.Plugins.Test/TestArgParser/TestArgParserPositionalReference.fs

Lines changed: 113 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,14 @@ module TestArgParserPositionalReference =
7171

7272
/// A structural input: which named leaves were observed, and the positional events in
7373
/// order. (Order does not affect acceptance; it is the candidate sets that matter.)
74+
///
75+
/// This is *post-scanning* input, and the split into named observations and positional
76+
/// events is well defined only under the existing restriction that a positional sink may
77+
/// sit beside a union only in Reject mode: a Collect-mode sink turns an unrecognised
78+
/// `--key` token into a positional event, so the split would itself depend on which case
79+
/// the scan is trying to select. Deliberately unmodelled, therefore: unrecognised
80+
/// flag-like tokens, `--help`, scanner errors, and repeat occurrences of a single named
81+
/// leaf (this is a set, so the runtime's duplicate-argument error is out of scope).
7482
type RefInput =
7583
{
7684
ObservedNamed : Set<int>
@@ -174,6 +182,14 @@ module TestArgParserPositionalReference =
174182
|> List.map (fun p -> p.Id)
175183
|> Set.ofList
176184

185+
/// The named half of acceptance: every observed named leaf belongs to the interpretation,
186+
/// and every required named leaf of the interpretation was observed. Named separately
187+
/// because on a well-formed tree this half already selects: see the confirm-or-veto
188+
/// property below.
189+
let acceptsNamed (interp : Interpretation) (input : RefInput) : bool =
190+
Set.isSubset input.ObservedNamed interp.Named
191+
&& Set.isSubset interp.RequiredNamed input.ObservedNamed
192+
177193
/// The acceptance predicate: does this interpretation structurally accept this input?
178194
///
179195
/// - every observed named leaf belongs to the interpretation;
@@ -183,8 +199,7 @@ module TestArgParserPositionalReference =
183199
///
184200
/// Conversion is deliberately absent: whether values parse never features.
185201
let accepts (sinks : RefPositionalLeaf list) (interp : Interpretation) (input : RefInput) : bool =
186-
Set.isSubset input.ObservedNamed interp.Named
187-
&& Set.isSubset interp.RequiredNamed input.ObservedNamed
202+
acceptsNamed interp input
188203
&& (List.isEmpty input.PositionalEvents
189204
|| (
190205
match Set.toList interp.Positionals with
@@ -197,7 +212,18 @@ module TestArgParserPositionalReference =
197212

198213
/// The reference semantics: the parse is structurally successful exactly when one
199214
/// interpretation accepts.
215+
///
216+
/// The tree must satisfy linearity (capacity at most one positional leaf). That is a
217+
/// property of the schema, not of the input, so it is checked up front: the assertion
218+
/// inside `accepts` fires only once an event needs routing, which would silently accept an
219+
/// over-capacity tree on the inputs that happen to carry no positional events at all.
200220
let exhaustiveSelect (tree : RefTree) (input : RefInput) : RefOutcome =
221+
match maxPositionals tree with
222+
| 0
223+
| 1 -> ()
224+
| capacity ->
225+
failwithf "linearity violated: this tree admits an interpretation with %i positional leaves" capacity
226+
201227
let sinks = positionalLeaves tree
202228

203229
match interpretations tree |> List.filter (fun interp -> accepts sinks interp input) with
@@ -647,28 +673,36 @@ module TestArgParserPositionalReference =
647673
acceptCount |> shouldBeGreaterThan 200
648674

649675
[<Test>]
650-
let ``Bare-only inputs are never ambiguous when every sum passes the empty-ambiguity check`` () =
651-
// The lemma which makes the generation-time check sufficient: bare tokens name every
652-
// sink, so they cannot discriminate between cases; if two interpretations both
653-
// accepted a bare-only input, both their cases (at the outermost sum where they
654-
// differ) would have to be satisfiable with no named observations — exactly what the
655-
// per-sum check forbids. Keyed events are excluded: a keyed form genuinely can
656-
// discriminate, which is by design.
676+
let ``Positional events can only confirm or veto the interpretation the named half selects`` () =
677+
// The lemma which makes the generation-time check sufficient, in its strongest form:
678+
// the *named* half of acceptance already picks out at most one interpretation. Leaf
679+
// ids are distinct across cases, so an observed leaf belongs to exactly one of them;
680+
// and if two interpretations were both named-accepted, then at the outermost sum where
681+
// they differ both their cases would have to be satisfiable with no named observations
682+
// — exactly what the per-sum check forbids.
683+
//
684+
// Acceptance is a conjunction, so the positional half can only filter that singleton.
685+
// Hence no input is ever ambiguous, and no positional event — bare *or* keyed — can
686+
// change which case is chosen. A bare token names every sink and so is always neutral;
687+
// a keyed token naming no sink of the selected interpretation vetoes it outright
688+
// rather than selecting some other case.
689+
//
690+
// This is what licenses the production resolver to select structurally first and
691+
// validate the events against the active sink afterwards. It rests on ids being
692+
// distinct across cases, which the generator enforces by rejecting an argument name
693+
// shared between two union cases; were that relaxed, a keyed form genuinely could
694+
// select, and this property would fail.
657695
let mutable checkedTrees = 0
658696
let mutable inputsWithEvents = 0
697+
let mutable inputsWithKeyedEvents = 0
698+
let mutable vetoed = 0
659699

660700
let cases =
661701
gen {
662702
let! sumBias = Gen.elements [ 30 ; 60 ; 90 ]
663703
let! tree = genTree sumBias
664-
let! rawInput = genInput 30 40 20 tree
665-
let bareOnly = rawInput.PositionalEvents |> List.map (fun _ -> RefSpelling.Bare)
666-
667-
return
668-
tree,
669-
{ rawInput with
670-
PositionalEvents = bareOnly
671-
}
704+
let! input = genInput 30 40 20 tree
705+
return tree, input
672706
}
673707

674708
let property (tree : RefTree, input : RefInput) : unit =
@@ -678,16 +712,42 @@ module TestArgParserPositionalReference =
678712
if not (List.isEmpty input.PositionalEvents) then
679713
inputsWithEvents <- inputsWithEvents + 1
680714

715+
let isKeyed (spelling : RefSpelling) : bool =
716+
match spelling with
717+
| RefSpelling.Keyed _ -> true
718+
| RefSpelling.Bare -> false
719+
720+
if input.PositionalEvents |> List.exists isKeyed then
721+
inputsWithKeyedEvents <- inputsWithKeyedEvents + 1
722+
723+
let namedOnly =
724+
interpretations tree |> List.filter (fun interp -> acceptsNamed interp input)
725+
726+
// The named half alone already selects.
727+
List.length namedOnly |> shouldBeSmallerThan 2
728+
681729
match exhaustiveSelect tree input with
682730
| RefOutcome.Ambiguous _ -> failwithf "ambiguous outcome for %A on %A" tree input
683-
| RefOutcome.Unique _
684-
| RefOutcome.NoInterpretation -> ()
731+
| RefOutcome.Unique interp ->
732+
// Confirmation: the events agreed, and the case chosen is the one the
733+
// named half had already picked.
734+
match namedOnly with
735+
| [ only ] -> interp.Choices |> shouldEqual only.Choices
736+
| _ -> failwithf "accepted %A, which the named half did not select, on %A" interp input
737+
| RefOutcome.NoInterpretation ->
738+
// Veto (or nothing was named-accepted in the first place).
739+
if not (List.isEmpty namedOnly) then
740+
vetoed <- vetoed + 1
685741

686742
let config = Config.QuickThrowOnFailure.WithMaxTest 2000
687743
Check.One (config, Prop.forAll (Arb.fromGen cases) property)
688744

689745
checkedTrees |> shouldBeGreaterThan 500
690746
inputsWithEvents |> shouldBeGreaterThan 200
747+
// The keyed half of the law is the interesting one, and vetoes must actually happen:
748+
// otherwise "confirm or veto" is only being tested on inputs which confirm.
749+
inputsWithKeyedEvents |> shouldBeGreaterThan 100
750+
vetoed |> shouldBeGreaterThan 20
691751

692752
[<Test>]
693753
let ``The generator explores every outcome regime`` () =
@@ -815,7 +875,7 @@ module TestArgParserPositionalReference =
815875
|> shouldEqual RefOutcome.NoInterpretation
816876

817877
[<Test>]
818-
let ``Per-case sinks: a distinctive keyed form is itself a structural discriminator`` () =
878+
let ``Per-case sinks: a distinctive keyed form confirms or vetoes, but never selects`` () =
819879
let tree =
820880
RefTree.Sum (
821881
0,
@@ -825,14 +885,32 @@ module TestArgParserPositionalReference =
825885
]
826886
)
827887

828-
// Nothing but --files=x: only Foo's interpretation can consume the event.
888+
// Foo is what the named half selects on no observations at all: it is empty-satisfiable
889+
// and Bar, whose named leaf is required, is not.
890+
match exhaustiveSelect tree (input [] []) with
891+
| RefOutcome.Unique interp -> interp.Choices |> shouldEqual (Map.ofList [ 0, 0 ])
892+
| other -> failwithf "unexpected outcome: %A" other
893+
894+
// --files=x therefore *confirms* that choice rather than making it: the sink Foo
895+
// already carries is the one which claims the form, so the event routes and the
896+
// outcome is unchanged.
829897
match exhaustiveSelect tree (input [] [ RefSpelling.Keyed "files" ]) with
830898
| RefOutcome.Unique interp ->
831899
interp.Choices |> shouldEqual (Map.ofList [ 0, 0 ])
832900
interp.Positionals |> shouldEqual (Set.singleton 1000)
833901
| other -> failwithf "unexpected outcome: %A" other
834902

835-
// A keyed form belonging to the case the named observations exclude: no acceptance.
903+
// --nums=x names no sink of Foo, so it vetoes: the outcome is failure, and emphatically
904+
// not a switch to Bar, whose required named leaf was never observed.
905+
exhaustiveSelect tree (input [] [ RefSpelling.Keyed "nums" ])
906+
|> shouldEqual RefOutcome.NoInterpretation
907+
908+
// Likewise where the named observations select Bar: --files belongs to the case they
909+
// exclude, so it vetoes rather than dragging the choice back to Foo.
910+
match exhaustiveSelect tree (input [ 1 ] []) with
911+
| RefOutcome.Unique interp -> interp.Choices |> shouldEqual (Map.ofList [ 0, 1 ])
912+
| other -> failwithf "unexpected outcome: %A" other
913+
836914
exhaustiveSelect tree (input [ 1 ] [ RefSpelling.Keyed "files" ])
837915
|> shouldEqual RefOutcome.NoInterpretation
838916

@@ -855,7 +933,18 @@ module TestArgParserPositionalReference =
855933
// The linearity rule: argv has one positional stream, so P × Q is rejected by
856934
// capacity while (A × P) + (B × Q) is fine. This is what schema validation will
857935
// enforce; the model just states the arithmetic.
858-
maxPositionals (RefTree.Product [ sink 1000 [ "rest" ] ; sink 1001 [ "files" ] ])
859-
|> shouldEqual 2
936+
let overCapacity = RefTree.Product [ sink 1000 [ "rest" ] ; sink 1001 [ "files" ] ]
937+
938+
maxPositionals overCapacity |> shouldEqual 2
860939

861940
maxPositionals perCaseSinks |> shouldEqual 1
941+
942+
// The model refuses to interpret such a tree at all. In particular it refuses on an
943+
// input with no positional events, which routes nothing and so would otherwise sail
944+
// past the routing-time check and report a unique interpretation holding both sinks.
945+
for events in [ [] ; [ RefSpelling.Bare ] ; [ RefSpelling.Keyed "rest" ] ] do
946+
let exc =
947+
Assert.Throws<exn> (fun () -> exhaustiveSelect overCapacity (input [] events) |> ignore<RefOutcome>)
948+
949+
exc.Message
950+
|> shouldEqual "linearity violated: this tree admits an interpretation with 2 positional leaves"

0 commit comments

Comments
 (0)