Skip to content

Commit 76f74a7

Browse files
Smaug123claude
andcommitted
Automate the discriminated-union generation-rejection tests
Cross-case name collisions (including case-insensitive ones, the soundness hole for case selection), ambiguous empty cases, positionals inside or alongside a union, and malformed case payloads are now asserted by driving the generator over in-memory source. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 7ea3e25 commit 76f74a7

1 file changed

Lines changed: 214 additions & 0 deletions

File tree

WoofWare.Myriad.Plugins/Test/TestArgParserRejection.fs

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,3 +312,217 @@ type NoConflict =
312312

313313
// One namespace for the embedded runtime module, one for the generated parser module.
314314
List.length modules |> shouldEqual 2
315+
316+
// ------------------------------------------------------------------------------------------
317+
// Discriminated unions of alternative argument sets. Global name uniqueness (under the
318+
// scanner's case-insensitive equality) is the axiom which makes case selection sound: an
319+
// argument name shared between two cases would be routed to whichever case is declared
320+
// first, silently selecting it.
321+
322+
[<Test>]
323+
let ``Argument names differing only by case collide across union cases`` () =
324+
// The empirical counterexample from review: with a case-sensitive check, `--FOO=3` parsed
325+
// successfully and constructed FooCase, and BarCase's argument was unreachable.
326+
"""namespace TestMe
327+
328+
open WoofWare.Myriad.Plugins
329+
330+
type FooArgs =
331+
{
332+
Foo : int
333+
}
334+
335+
type BarArgs =
336+
{
337+
[<ArgumentLongForm "FOO">]
338+
Bar : int
339+
}
340+
341+
[<ArgParser>]
342+
type DuArgs =
343+
| FooCase of FooArgs
344+
| BarCase of BarArgs
345+
"""
346+
|> shouldRejectWith
347+
"Conflicting argument names detected (names are matched case-insensitively):\nThe argument name '--foo' is claimed by: '--foo' (field 'Foo'); '--FOO' (field 'Bar')"
348+
349+
[<Test>]
350+
let ``Identical argument names collide across union cases`` () =
351+
"""namespace TestMe
352+
353+
open WoofWare.Myriad.Plugins
354+
355+
type FooArgs =
356+
{
357+
Foo : int
358+
}
359+
360+
type BarArgs =
361+
{
362+
[<ArgumentLongForm "foo">]
363+
Bar : int
364+
}
365+
366+
[<ArgParser>]
367+
type DuArgs =
368+
| FooCase of FooArgs
369+
| BarCase of BarArgs
370+
"""
371+
|> shouldRejectWith
372+
"Conflicting argument names detected (names are matched case-insensitively):\nThe argument name '--foo' is claimed by: '--foo' (field 'Foo'); '--foo' (field 'Bar')"
373+
374+
[<Test>]
375+
let ``Two union cases which are both satisfiable with no arguments are rejected`` () =
376+
// An empty command line could not choose between them.
377+
"""namespace TestMe
378+
379+
open WoofWare.Myriad.Plugins
380+
381+
type AllOptionalA =
382+
{
383+
A : int option
384+
}
385+
386+
type AllOptionalB =
387+
{
388+
B : int option
389+
}
390+
391+
[<ArgParser>]
392+
type AmbiguousEmptyCases =
393+
| CaseA of AllOptionalA
394+
| CaseB of AllOptionalB
395+
"""
396+
|> shouldRejectWith
397+
"Cases CaseA, CaseB can all be satisfied without supplying any arguments, so an empty command line cannot choose between them. Make an argument in all but one of them mandatory."
398+
399+
[<Test>]
400+
let ``Positional args are rejected inside a union case`` () =
401+
"""namespace TestMe
402+
403+
open WoofWare.Myriad.Plugins
404+
405+
type SomePositionals =
406+
{
407+
[<PositionalArgs>]
408+
Rest : string list
409+
}
410+
411+
type NotPositional =
412+
{
413+
C : int
414+
}
415+
416+
type PositionalOrNot =
417+
| Pos of SomePositionals
418+
| NotPos of NotPositional
419+
420+
[<ArgParser>]
421+
type PositionalInsideUnion =
422+
{
423+
Choice : PositionalOrNot
424+
}
425+
"""
426+
|> shouldRejectWith
427+
"Positional args are not permitted inside cases of the [<ArgParser>] union PositionalOrNot: the parser could not tell which alternative a positional arg belongs to."
428+
429+
[<Test>]
430+
let ``Positional args are rejected alongside a union`` () =
431+
// Conservative in v1: a Reject-mode sink beside a union is in principle sound (bare
432+
// tokens cannot influence case selection), but a Collect-mode sink silently swallows
433+
// typo'd case-selecting keys, so for now the combination is banned wholesale.
434+
"""namespace TestMe
435+
436+
open WoofWare.Myriad.Plugins
437+
438+
type AutoMode =
439+
{
440+
Quiet : bool option
441+
}
442+
443+
type ManualMode =
444+
{
445+
Level : int
446+
}
447+
448+
type Mode =
449+
| Auto of AutoMode
450+
| Manual of ManualMode
451+
452+
[<ArgParser>]
453+
type WithModeAndPositionals =
454+
{
455+
Mode : Mode
456+
457+
[<PositionalArgs>]
458+
Rest : string list
459+
}
460+
"""
461+
|> shouldRejectWith
462+
"Positional args cannot be combined with a discriminated-union arg: the parser could not tell which alternative an unrecognised arg belongs to."
463+
464+
[<Test>]
465+
let ``A union case must carry a record defined alongside the union`` () =
466+
"""namespace TestMe
467+
468+
open WoofWare.Myriad.Plugins
469+
470+
type FooArgs =
471+
{
472+
Foo : int
473+
}
474+
475+
[<ArgParser>]
476+
type BadDu =
477+
| FooCase of int
478+
| BarCase of FooArgs
479+
"""
480+
|> shouldRejectWith
481+
"Case FooCase of [<ArgParser>] union BadDu must have a payload which is a record defined alongside the union."
482+
483+
[<Test>]
484+
let ``A union case must carry exactly one field`` () =
485+
"""namespace TestMe
486+
487+
open WoofWare.Myriad.Plugins
488+
489+
type FooArgs =
490+
{
491+
Foo : int
492+
}
493+
494+
[<ArgParser>]
495+
type BadDu =
496+
| FooCase of FooArgs * int
497+
| BarCase of FooArgs
498+
"""
499+
|> shouldRejectWith
500+
"Case FooCase of [<ArgParser>] union BadDu must have exactly one field: a record holding that case's arguments."
501+
502+
[<Test>]
503+
let ``The motivating union of alternative argument sets generates successfully`` () =
504+
let modules =
505+
generateFromSource
506+
"""namespace TestMe
507+
508+
open WoofWare.Myriad.Plugins
509+
510+
type FooArgs =
511+
{
512+
Foo : int
513+
}
514+
515+
type BarArgs =
516+
{
517+
Bar : int
518+
Baz : int
519+
}
520+
521+
[<ArgParser>]
522+
type DuArgs =
523+
| FooCase of FooArgs
524+
| BarCase of BarArgs
525+
"""
526+
527+
// One namespace for the embedded runtime module, one for the generated parser module.
528+
List.length modules |> shouldEqual 2

0 commit comments

Comments
 (0)