You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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>
// Discriminated unions of alternative argument sets. Global name uniqueness (under the
261
+
// scanner's case-insensitive equality) is the axiom which makes case selection sound: an
262
+
// argument name shared between two cases would be routed to whichever case is declared
263
+
// first, silently selecting it.
264
+
265
+
[<Test>]
266
+
let``Argument names differing only by case collide across union cases`` ()=
267
+
// The empirical counterexample from review: with a case-sensitive check, `--FOO=3` parsed
268
+
// successfully and constructed FooCase, and BarCase's argument was unreachable.
269
+
"""namespace TestMe
270
+
271
+
open WoofWare.Myriad.Plugins
272
+
273
+
type FooArgs =
274
+
{
275
+
Foo : int
276
+
}
277
+
278
+
type BarArgs =
279
+
{
280
+
[<ArgumentLongForm "FOO">]
281
+
Bar : int
282
+
}
283
+
284
+
[<ArgParser>]
285
+
type DuArgs =
286
+
| FooCase of FooArgs
287
+
| BarCase of BarArgs
288
+
"""
289
+
|> shouldRejectWith
290
+
"Conflicting argument names detected (names are matched case-insensitively):\nThe argument name '--foo' is claimed by: '--foo' (field 'Foo'); '--FOO' (field 'Bar')"
291
+
292
+
[<Test>]
293
+
let``Identical argument names collide across union cases`` ()=
294
+
"""namespace TestMe
295
+
296
+
open WoofWare.Myriad.Plugins
297
+
298
+
type FooArgs =
299
+
{
300
+
Foo : int
301
+
}
302
+
303
+
type BarArgs =
304
+
{
305
+
[<ArgumentLongForm "foo">]
306
+
Bar : int
307
+
}
308
+
309
+
[<ArgParser>]
310
+
type DuArgs =
311
+
| FooCase of FooArgs
312
+
| BarCase of BarArgs
313
+
"""
314
+
|> shouldRejectWith
315
+
"Conflicting argument names detected (names are matched case-insensitively):\nThe argument name '--foo' is claimed by: '--foo' (field 'Foo'); '--foo' (field 'Bar')"
316
+
317
+
[<Test>]
318
+
let``Two union cases which are both satisfiable with no arguments are rejected`` ()=
319
+
// An empty command line could not choose between them.
320
+
"""namespace TestMe
321
+
322
+
open WoofWare.Myriad.Plugins
323
+
324
+
type AllOptionalA =
325
+
{
326
+
A : int option
327
+
}
328
+
329
+
type AllOptionalB =
330
+
{
331
+
B : int option
332
+
}
333
+
334
+
[<ArgParser>]
335
+
type AmbiguousEmptyCases =
336
+
| CaseA of AllOptionalA
337
+
| CaseB of AllOptionalB
338
+
"""
339
+
|> shouldRejectWith
340
+
"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."
341
+
342
+
[<Test>]
343
+
let``Positional args are rejected inside a union case`` ()=
344
+
"""namespace TestMe
345
+
346
+
open WoofWare.Myriad.Plugins
347
+
348
+
type SomePositionals =
349
+
{
350
+
[<PositionalArgs>]
351
+
Rest : string list
352
+
}
353
+
354
+
type NotPositional =
355
+
{
356
+
C : int
357
+
}
358
+
359
+
type PositionalOrNot =
360
+
| Pos of SomePositionals
361
+
| NotPos of NotPositional
362
+
363
+
[<ArgParser>]
364
+
type PositionalInsideUnion =
365
+
{
366
+
Choice : PositionalOrNot
367
+
}
368
+
"""
369
+
|> shouldRejectWith
370
+
"Positional args are not permitted inside cases of the [<ArgParser>] union PositionalOrNot: the parser could not tell which alternative a positional arg belongs to."
371
+
372
+
[<Test>]
373
+
let``Positional args are rejected alongside a union`` ()=
374
+
// Conservative in v1: a Reject-mode sink beside a union is in principle sound (bare
375
+
// tokens cannot influence case selection), but a Collect-mode sink silently swallows
376
+
// typo'd case-selecting keys, so for now the combination is banned wholesale.
377
+
"""namespace TestMe
378
+
379
+
open WoofWare.Myriad.Plugins
380
+
381
+
type AutoMode =
382
+
{
383
+
Quiet : bool option
384
+
}
385
+
386
+
type ManualMode =
387
+
{
388
+
Level : int
389
+
}
390
+
391
+
type Mode =
392
+
| Auto of AutoMode
393
+
| Manual of ManualMode
394
+
395
+
[<ArgParser>]
396
+
type WithModeAndPositionals =
397
+
{
398
+
Mode : Mode
399
+
400
+
[<PositionalArgs>]
401
+
Rest : string list
402
+
}
403
+
"""
404
+
|> shouldRejectWith
405
+
"Positional args cannot be combined with a discriminated-union arg: the parser could not tell which alternative an unrecognised arg belongs to."
406
+
407
+
[<Test>]
408
+
let``A union case must carry a record defined alongside the union`` ()=
409
+
"""namespace TestMe
410
+
411
+
open WoofWare.Myriad.Plugins
412
+
413
+
type FooArgs =
414
+
{
415
+
Foo : int
416
+
}
417
+
418
+
[<ArgParser>]
419
+
type BadDu =
420
+
| FooCase of int
421
+
| BarCase of FooArgs
422
+
"""
423
+
|> shouldRejectWith
424
+
"Case FooCase of [<ArgParser>] union BadDu must have a payload which is a record defined alongside the union."
425
+
426
+
[<Test>]
427
+
let``A union case must carry exactly one field`` ()=
428
+
"""namespace TestMe
429
+
430
+
open WoofWare.Myriad.Plugins
431
+
432
+
type FooArgs =
433
+
{
434
+
Foo : int
435
+
}
436
+
437
+
[<ArgParser>]
438
+
type BadDu =
439
+
| FooCase of FooArgs * int
440
+
| BarCase of FooArgs
441
+
"""
442
+
|> shouldRejectWith
443
+
"Case FooCase of [<ArgParser>] union BadDu must have exactly one field: a record holding that case's arguments."
444
+
445
+
[<Test>]
446
+
let``The motivating union of alternative argument sets generates successfully`` ()=
447
+
letmodules=
448
+
generateFromSource
449
+
"""namespace TestMe
450
+
451
+
open WoofWare.Myriad.Plugins
452
+
453
+
type FooArgs =
454
+
{
455
+
Foo : int
456
+
}
457
+
458
+
type BarArgs =
459
+
{
460
+
Bar : int
461
+
Baz : int
462
+
}
463
+
464
+
[<ArgParser>]
465
+
type DuArgs =
466
+
| FooCase of FooArgs
467
+
| BarCase of BarArgs
468
+
"""
469
+
470
+
// One namespace for the embedded runtime module, one for the generated parser module.
0 commit comments