@@ -283,7 +283,9 @@ def __init__(
283
283
self .limits = ProblemLimits (parse_setting (yaml_data , "limits" , {}), problem , self )
284
284
285
285
parse_deprecated_setting (
286
- yaml_data , "validator_flags" , f"{ validate .OutputValidator .args_key } ' in 'testdata.yaml"
286
+ yaml_data ,
287
+ "validator_flags" ,
288
+ f"{ validate .OutputValidator .args_key } ' in 'test_group.yaml" ,
287
289
)
288
290
289
291
self .keywords : list [str ] = parse_optional_list_setting (yaml_data , "keywords" , str )
@@ -362,9 +364,9 @@ def __init__(self, path: Path, tmpdir: Path, label: Optional[str] = None):
362
364
self ._programs = dict [Path , "Program" ]()
363
365
self ._program_callbacks = dict [Path , list [Callable [["Program" ], None ]]]()
364
366
# Dictionary from path to parsed file contents.
365
- # TODO #102: Add type for testdata .yaml (typed Namespace?)
366
- self ._testdata_yamls = dict [Path , dict [str , Any ]]()
367
- self ._testdata_lock = threading .Lock ()
367
+ # TODO #102: Add type for test_group .yaml (typed Namespace?)
368
+ self ._test_case_yamls = dict [Path , dict [str , Any ]]()
369
+ self ._test_group_lock = threading .Lock ()
368
370
369
371
# The label for the problem: A, B, A1, A2, X, ...
370
372
self .label = label
@@ -457,105 +459,102 @@ def _read_settings(self):
457
459
self .multi_pass : bool = self .settings .multi_pass
458
460
self .custom_output : bool = self .settings .custom_output
459
461
460
- # TODO #102 move to TestData class
461
- def _parse_testdata_yaml (p , path , bar ):
462
+ # TODO #102 move to a new TestGroup class
463
+ def _parse_test_case_and_groups_yaml (p , path : Path , bar : BAR_TYPE ):
462
464
assert path .is_relative_to (p .path / "data" )
463
- for dir in [path ] + list (path .parents ):
465
+ for f in [path ] + list (path .parents ):
464
466
# Do not go above the data directory.
465
- if dir == p .path :
467
+ if f == p .path :
466
468
return
467
469
468
- f = dir / "testdata.yaml"
469
- if not f . is_file () or f in p . _testdata_yamls :
470
- continue
471
- with p . _testdata_lock :
472
- if f not in p . _testdata_yamls :
473
- raw = substitute (
474
- f .read_text (),
475
- p .settings .constants ,
476
- pattern = config .CONSTANT_SUBSTITUTE_REGEX ,
477
- )
478
- p . _testdata_yamls [f ] = flags = parse_yaml (raw , path = f , plain = True )
470
+ if f . is_dir ():
471
+ f = f / "test_group.yaml"
472
+ with p . _test_group_lock :
473
+ if not f . is_file () or f in p . _test_case_yamls :
474
+ continue
475
+ raw = substitute (
476
+ f .read_text (),
477
+ p .settings .constants ,
478
+ pattern = config .CONSTANT_SUBSTITUTE_REGEX ,
479
+ )
480
+ p . _test_case_yamls [f ] = flags = parse_yaml (raw , path = f , plain = True )
479
481
480
- parse_deprecated_setting (
481
- flags , "output_validator_flags" , validate .OutputValidator .args_key
482
- )
483
- parse_deprecated_setting (
484
- flags , "input_validator_flags" , validate .InputValidator .args_key
485
- )
482
+ parse_deprecated_setting (
483
+ flags , "output_validator_flags" , validate .OutputValidator .args_key
484
+ )
485
+ parse_deprecated_setting (
486
+ flags , "input_validator_flags" , validate .InputValidator .args_key
487
+ )
486
488
487
- # Verify testdata.yaml
488
- for k in flags :
489
- match k :
490
- case (
491
- validate .OutputValidator .args_key
492
- | validate .AnswerValidator .args_key
493
- | visualize .TestCaseVisualizer .args_key
494
- | visualize .OutputVisualizer .args_key
495
- ):
496
- if not isinstance (flags [k ], list ):
497
- bar .error (
498
- f"{ k } must be a list of strings" ,
499
- resume = True ,
500
- print_item = False ,
501
- )
502
- case validate .InputValidator .args_key :
503
- if not isinstance (flags [k ], (list , dict )):
504
- bar .error (
505
- f"{ k } must be list or map" ,
506
- resume = True ,
507
- print_item = False ,
508
- )
509
- if isinstance (flags [k ], dict ):
510
- input_validator_names = set (
511
- val .name for val in p .validators (validate .InputValidator )
512
- )
513
- for name in set (flags [k ]) - input_validator_names :
514
- bar .warn (
515
- f"Unknown input validator { name } ; expected { input_validator_names } " ,
516
- print_item = False ,
517
- )
518
- case (
519
- "args"
520
- | "description"
521
- | "full_feedback"
522
- | "hint"
523
- | "scoring"
524
- | "static_validation"
525
- ):
526
- bar .warn (
527
- f"{ k } in testdata.yaml not implemented in BAPCtools" ,
528
- print_item = False ,
489
+ # Use variable kwargs so the type checker does not complain when passing them to a PrintBar (nothing happens in that case anyway)
490
+ bar_kwargs = {"resume" : True , "print_item" : False }
491
+
492
+ # Verify test_group.yaml
493
+ for k in flags :
494
+ match k :
495
+ case (
496
+ validate .OutputValidator .args_key
497
+ | validate .AnswerValidator .args_key
498
+ | visualize .TestCaseVisualizer .args_key
499
+ | visualize .OutputVisualizer .args_key
500
+ ):
501
+ if not isinstance (flags [k ], list ):
502
+ bar .error (
503
+ f"{ k } must be a list of strings" ,
504
+ None ,
505
+ ** bar_kwargs ,
529
506
)
530
- case _:
531
- path = f .relative_to (p .path / "data" )
532
- bar .warn (f'Unknown key "{ k } " in { path } ' , print_item = False )
533
- # Do not go above the data directory.
534
- if dir == p .path / "data" :
535
- break
536
-
537
- def get_testdata_yaml (
507
+ case validate .InputValidator .args_key :
508
+ if not isinstance (flags [k ], (list , dict )):
509
+ bar .error (
510
+ f"{ k } must be list or map" ,
511
+ None ,
512
+ ** bar_kwargs ,
513
+ )
514
+ if isinstance (flags [k ], dict ):
515
+ input_validator_names = set (
516
+ val .name for val in p .validators (validate .InputValidator )
517
+ )
518
+ for name in set (flags [k ]) - input_validator_names :
519
+ bar .warn (
520
+ f"Unknown input validator { name } ; expected { input_validator_names } " ,
521
+ None ,
522
+ ** bar_kwargs ,
523
+ )
524
+ case "description" | "hint" :
525
+ pass # We don't do anything with hint or description in BAPCtools, but no need to warn about this
526
+ case "args" | "full_feedback" | "scoring" | "static_validation" :
527
+ bar .warn (
528
+ f"{ k } in test_group.yaml not implemented in BAPCtools" ,
529
+ None ,
530
+ ** bar_kwargs ,
531
+ )
532
+ case _:
533
+ path = f .relative_to (p .path / "data" )
534
+ bar .warn (f'Unknown key "{ k } " in { path } ' , None , ** bar_kwargs )
535
+
536
+ def get_test_case_yaml (
538
537
p ,
539
538
path : Path ,
540
539
key : str ,
541
540
bar : BAR_TYPE ,
542
541
name : Optional [str ] = None ,
543
542
) -> list [str ]:
544
543
"""
545
- Find the testdata flags applying at the given path for the given key .
546
- If necessary, walk up from `path` looking for the first testdata .yaml file that applies,
544
+ Find the value of the given test_group.yaml key applying at the given path .
545
+ If necessary, walk up from `path` looking for the first test_group .yaml file that applies.
547
546
548
547
Side effects: parses and caches the file.
549
548
550
549
Arguments
551
550
---------
552
551
path: absolute path (a file or a directory)
553
- key: The testdata .yaml key to look for (TODO: 'grading' is not yet implemented)
552
+ key: The test_group .yaml key to look for (TODO: 'grading' is not yet implemented)
554
553
name: If key == 'input_validator_args', optionally the name of the input validator.
555
554
556
555
Returns:
557
556
--------
558
- A list of string arguments, which is empty if no testdata .yaml is found.
557
+ A list of string arguments, which is empty if no test_group .yaml is found.
559
558
TODO: when 'grading' is supported, it also can return dict
560
559
"""
561
560
known_args_keys = [
@@ -572,19 +571,21 @@ def get_testdata_yaml(
572
571
f"Only input validators support flags by validator name, got { key } and { name } "
573
572
)
574
573
575
- # parse and cache testdata.yaml
576
- p ._parse_testdata_yaml (path , bar )
574
+ # parse and cache <test_case>.yaml and test_group.yaml
575
+ path = path .with_suffix (".yaml" )
576
+ p ._parse_test_case_and_groups_yaml (path , bar )
577
577
578
578
# extract the flags
579
- for dir in [path ] + list (path .parents ):
579
+ for f in [path ] + list (path .parents ):
580
580
# Do not go above the data directory.
581
- if dir == p .path :
581
+ if f == p .path :
582
582
return []
583
583
584
- f = dir / "testdata.yaml"
585
- if f not in p ._testdata_yamls :
584
+ if f .suffix != ".yaml" :
585
+ f = f / "test_group.yaml"
586
+ if f not in p ._test_case_yamls :
586
587
continue
587
- flags = p ._testdata_yamls [f ]
588
+ flags = p ._test_case_yamls [f ]
588
589
if key in flags :
589
590
args = flags [key ]
590
591
if key == validate .InputValidator .args_key :
@@ -611,6 +612,15 @@ def get_testdata_yaml(
611
612
612
613
return []
613
614
615
+ # Because Problem.testcases() may be called multiple times (e.g. validating multiple modes, or with `bt all`),
616
+ # this cache makes sure that some warnings (like malformed test case names) only appear once.
617
+ _warned_for_test_case = set [str ]()
618
+
619
+ def _warn_once (p , test_name , msg ):
620
+ if test_name not in p ._warned_for_test_case :
621
+ p ._warned_for_test_case .add (test_name )
622
+ warn (msg )
623
+
614
624
def testcases (
615
625
p ,
616
626
* ,
@@ -659,6 +669,15 @@ def testcases(
659
669
testcases = []
660
670
for f in in_paths :
661
671
t = testcase .Testcase (p , f )
672
+ if not config .COMPILED_FILE_NAME_REGEX .fullmatch (f .name ):
673
+ p ._warn_once (t .name , f"Test case name { t .name } is not valid. Skipping." )
674
+ continue
675
+ if f .with_suffix ("" ).name == "test_group" :
676
+ p ._warn_once (
677
+ t .name ,
678
+ "Test case must not be named 'test_group', this clashes with the group-level 'test_group.yaml'. Skipping." ,
679
+ )
680
+ continue
662
681
if (
663
682
(p .interactive or p .multi_pass )
664
683
and mode in [validate .Mode .INVALID , validate .Mode .VALID_OUTPUT ]
@@ -670,7 +689,7 @@ def testcases(
670
689
continue
671
690
if needans and not t .ans_path .is_file ():
672
691
if t .root != "invalid_input" :
673
- warn ( f"Found input file { f } without a .ans file. Skipping." )
692
+ p . _warn_once ( t . name , f"Found input file { f } without a .ans file. Skipping." )
674
693
continue
675
694
if mode == validate .Mode .VALID_OUTPUT :
676
695
if t .out_path is None :
@@ -1331,7 +1350,7 @@ def validate_valid_extra_data(p) -> bool:
1331
1350
if not p .validators (validate .OutputValidator , strict = True , print_warn = False ):
1332
1351
return True
1333
1352
1334
- args = p .get_testdata_yaml (
1353
+ args = p .get_test_case_yaml (
1335
1354
p .path / "data" / "valid_output" ,
1336
1355
"output_validator_args" ,
1337
1356
PrintBar ("Generic Output Validation" ),
@@ -1492,7 +1511,7 @@ def run_all(select_verdict, select):
1492
1511
return None , None , None
1493
1512
1494
1513
def get_slowest (result ):
1495
- slowest_pair = result .slowest_testcase ()
1514
+ slowest_pair = result .slowest_test_case ()
1496
1515
assert slowest_pair is not None
1497
1516
return slowest_pair
1498
1517
0 commit comments