Skip to content

Commit 89e0674

Browse files
committed
Refactor test file configuration handling
1 parent 91c12f8 commit 89e0674

File tree

1 file changed

+91
-71
lines changed
  • graalpython/com.oracle.graal.python.test/src

1 file changed

+91
-71
lines changed

graalpython/com.oracle.graal.python.test/src/runner.py

Lines changed: 91 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import argparse
4040
import concurrent.futures
4141
import enum
42+
import fnmatch
4243
import json
4344
import math
4445
import multiprocessing
@@ -59,7 +60,7 @@
5960
import unittest.loader
6061
from abc import abstractmethod
6162
from collections import defaultdict
62-
from dataclasses import dataclass, field
63+
from dataclasses import dataclass
6364
from functools import lru_cache
6465
from pathlib import Path
6566

@@ -294,7 +295,8 @@ def emit(self, **data):
294295
self.data.append(data)
295296

296297

297-
def test_path_to_module(path: Path):
298+
def test_path_to_module(test_file: 'TestFileConfig'):
299+
path = test_file.path.resolve().relative_to(test_file.config.rootdir)
298300
return str(path).removesuffix('.py').replace(os.sep, '.')
299301

300302

@@ -415,14 +417,14 @@ def generate_tags(self, append=False):
415417
for result in self.results:
416418
by_file[result.test_id.test_file].append(result)
417419
for test_file, results in by_file.items():
418-
config = config_for_file(test_file)
419-
tag_file = config.get_tag_file(test_file)
420+
test_file = configure_test_file(test_file)
421+
tag_file = test_file.get_tag_file()
420422
if not tag_file:
421423
log(f"WARNNING: no tag directory for test file {test_file}")
422424
continue
423425
tags = {result.test_id.test_name for result in results if result.status == TestStatus.SUCCESS}
424426
if append:
425-
tags |= {test.test_name for test in read_tags(test_file, config)}
427+
tags |= {test.test_name for test in read_tags(test_file)}
426428
with open(tag_file, 'w') as f:
427429
for test_name in sorted(tags):
428430
f.write(f'{test_name}\n')
@@ -670,42 +672,63 @@ def filter_tree(test_file: Path, test_suite: unittest.TestSuite, specifiers: lis
670672
return collected_tests, untagged_tests
671673

672674

675+
class TestFileNameMatcher:
676+
def __init__(self, matcher: typing.Iterable[str] = ()):
677+
matcher = [name.removesuffix('.py') for name in matcher]
678+
globs, exact_matches = partition_list(matcher, lambda x: '*' in x)
679+
self.exact_matches = frozenset(exact_matches)
680+
self.globs = [re.compile(fnmatch.translate(glob)) for glob in globs]
681+
682+
def matches(self, name):
683+
return name in self.exact_matches or any(glob.match(name) for glob in self.globs)
684+
685+
673686
@dataclass
674687
class Config:
675688
configdir: Path = Path('.').resolve()
676689
rootdir: Path = Path('.').resolve()
677690
tags_dir: Path | None = None
678691
run_top_level_functions: bool = False
679692
new_worker_per_file: bool = False
680-
serial_tests: frozenset[str] = frozenset()
681-
partial_splits_individual_tests: frozenset[str] = frozenset()
682-
excludes: dict[str, frozenset[str]] = field(default_factory=dict)
693+
serial_tests: TestFileNameMatcher = TestFileNameMatcher()
694+
partial_splits_individual_tests: TestFileNameMatcher = TestFileNameMatcher()
695+
excludes: TestFileNameMatcher = TestFileNameMatcher()
683696

684-
def test_file_name(self, test_file):
685-
resolved = test_file.resolve().relative_to(self.configdir)
686-
return str(resolved).removesuffix('.py')
687697

688-
def is_serial_test(self, test_file: Path):
689-
return self.test_file_name(test_file) in self.serial_tests
698+
@dataclass
699+
class TestFileConfig:
700+
path: Path
701+
name: str
702+
config: Config
690703

691-
def is_partial_splits_individual_tests(self, test_file: Path):
692-
return self.test_file_name(test_file) in self.partial_splits_individual_tests
704+
excluded: bool
705+
serial: bool
706+
partial_splits_individual_tests: bool
693707

694-
def get_tag_file(self, test_file: Path):
695-
if self.tags_dir:
696-
return self.tags_dir / (test_file.name.removesuffix('.py') + '.txt')
708+
def __str__(self):
709+
return str(self.path)
697710

698-
def is_excluded(self, test_file: Path):
699-
exclude_keys = [sys.platform]
700-
if IS_GRAALPY:
701-
# noinspection PyUnresolvedReferences
702-
exclude_keys.append('native_image' if __graalpython__.is_native else 'jvm')
703-
test_file_name = self.test_file_name(test_file)
704-
for key in exclude_keys:
705-
if excludes := self.excludes.get(key):
706-
if test_file_name in excludes:
707-
return True
708-
return False
711+
def __eq__(self, other):
712+
return self.path == other.path
713+
714+
def get_tag_file(self):
715+
if self.config.tags_dir:
716+
return self.config.tags_dir / (self.name.removesuffix('.py') + '.txt')
717+
718+
719+
def configure_test_file(path: Path) -> TestFileConfig:
720+
config = config_for_file(path)
721+
resolved = path.resolve().relative_to(config.configdir)
722+
name = str(resolved).removesuffix('.py')
723+
724+
return TestFileConfig(
725+
path=path,
726+
name=name,
727+
config=config,
728+
excluded=config.excludes.matches(name),
729+
serial=config.serial_tests.matches(name),
730+
partial_splits_individual_tests=config.partial_splits_individual_tests.matches(name),
731+
)
709732

710733

711734
@lru_cache
@@ -725,28 +748,30 @@ def config_for_file(test_file: Path) -> Config:
725748
return config_for_dir(path)
726749

727750

728-
def test_file_name_set(test_files: list[str]):
729-
return frozenset({test_file.removesuffix('.py') for test_file in test_files})
730-
731-
732751
@lru_cache
733752
def parse_config(config_path, path):
734753
with open(config_path, 'rb') as f:
735754
config_dict = tomllib.load(f)['tests']
736755
tags_dir = None
737756
if config_tags_dir := config_dict.get('tags_dir'):
738757
tags_dir = (path / config_tags_dir).resolve()
758+
exclude_keys = [sys.platform]
759+
if IS_GRAALPY:
760+
# noinspection PyUnresolvedReferences
761+
exclude_keys.append('native_image' if __graalpython__.is_native else 'jvm')
762+
excludes = []
763+
if excludes_dict := config_dict.get('excludes'):
764+
for key in exclude_keys:
765+
excludes += excludes_dict.get(key, ())
739766
return Config(
740767
configdir=config_path.parent.resolve(),
741768
rootdir=config_path.parent.parent.resolve(),
742769
tags_dir=tags_dir,
743770
run_top_level_functions=config_dict.get('run_top_level_functions', Config.run_top_level_functions),
744771
new_worker_per_file=config_dict.get('new_worker_per_file', Config.new_worker_per_file),
745-
serial_tests=test_file_name_set(config_dict.get('serial_tests', Config.serial_tests)),
746-
partial_splits_individual_tests=test_file_name_set(
747-
config_dict.get('partial_splits_individual_tests', Config.partial_splits_individual_tests)
748-
),
749-
excludes={key: test_file_name_set(excludes) for key, excludes in config_dict.get('excludes', {}).items()},
772+
serial_tests=TestFileNameMatcher(config_dict.get('serial_tests', ())),
773+
partial_splits_individual_tests=TestFileNameMatcher(config_dict.get('partial_splits_individual_tests', ())),
774+
excludes=TestFileNameMatcher(excludes),
750775
)
751776

752777

@@ -768,13 +793,6 @@ def run(self, result):
768793
sys.path[:] = saved_path
769794

770795

771-
def group_specifiers_by_file(specifiers: list[TestSpecifier]) -> dict[Path, list[TestSpecifier]]:
772-
by_file = defaultdict(list)
773-
for specifier in specifiers:
774-
by_file[specifier.test_file].append(specifier)
775-
return by_file
776-
777-
778796
def expand_specifier_paths(specifiers: list[TestSpecifier]) -> list[TestSpecifier]:
779797
expanded_specifiers = []
780798
for specifier in specifiers:
@@ -812,29 +830,30 @@ def expand_specifier_paths(specifiers: list[TestSpecifier]) -> list[TestSpecifie
812830
return expanded_specifiers
813831

814832

815-
def collect_module(test_file: Path, specifiers: list[TestSpecifier], use_tags=False, partial=None) -> TestSuite | None:
816-
config = config_for_file(test_file)
833+
def collect_module(test_file: TestFileConfig, specifiers: list[TestSpecifier], use_tags=False,
834+
partial=None) -> TestSuite | None:
835+
config = test_file.config
817836
saved_path = sys.path[:]
818837
sys.path.insert(0, str(config.rootdir))
819838
try:
820839
loader = TopLevelFunctionLoader() if config.run_top_level_functions else unittest.TestLoader()
821840
tags = None
822841
if use_tags and config.tags_dir:
823-
tags = read_tags(test_file, config)
842+
tags = read_tags(test_file)
824843
if not tags:
825844
return None
845+
test_module = test_path_to_module(test_file)
826846
try:
827-
test_module = test_path_to_module(test_file.resolve().relative_to(config.rootdir))
828847
test_suite = loader.loadTestsFromName(test_module)
829848
except unittest.SkipTest as e:
830849
log(f"Test file {test_file} skipped: {e}")
831850
return
832-
collected_tests, untagged_tests = filter_tree(test_file, test_suite, specifiers, tags)
833-
if partial and config.is_partial_splits_individual_tests(test_file):
851+
collected_tests, untagged_tests = filter_tree(test_file.path, test_suite, specifiers, tags)
852+
if partial and test_file.partial_splits_individual_tests:
834853
selected, total = partial
835854
collected_tests = collected_tests[selected::total]
836855
if collected_tests:
837-
return TestSuite(config, test_file, sys.path[:], test_suite, collected_tests, untagged_tests)
856+
return TestSuite(config, test_file.path, sys.path[:], test_suite, collected_tests, untagged_tests)
838857
finally:
839858
sys.path[:] = saved_path
840859

@@ -848,34 +867,35 @@ def collect(all_specifiers: list[TestSpecifier], *, use_tags=False, ignore=None,
848867
continue_on_errors=False, no_excludes=False) -> list[TestSuite]:
849868
to_run = []
850869
all_specifiers = expand_specifier_paths(all_specifiers)
851-
specifiers_by_file = group_specifiers_by_file(all_specifiers)
870+
test_files = []
871+
for specifier in all_specifiers:
872+
if not specifier.test_file.exists():
873+
sys.exit(f"File does not exist: {specifier.test_file}")
874+
test_files.append(configure_test_file(specifier.test_file))
852875
if ignore:
853876
ignore = [path_for_comparison(i) for i in ignore]
854-
for test_file in set(specifiers_by_file):
855-
if any(path_for_comparison(test_file).is_relative_to(i) for i in ignore):
856-
del specifiers_by_file[test_file]
877+
test_files = [
878+
test_file for test_file in test_files
879+
if any(path_for_comparison(test_file.path).is_relative_to(i) for i in ignore)
880+
]
857881
if not no_excludes:
858-
for test_file in set(specifiers_by_file):
859-
config = config_for_file(test_file)
860-
if config.is_excluded(test_file):
861-
log(f"Test file {test_file} is excluded on this platform/configuration, use --no-excludes to overrride")
862-
del specifiers_by_file[test_file]
882+
excluded, test_files = partition_list(test_files, lambda f: f.excluded)
883+
for file in excluded:
884+
log(f"Test file {file} is excluded on this platform/configuration, use --no-excludes to overrride")
863885
if partial:
864886
selected, total = partial
865887
to_split = []
866888
partial_files = set()
867889
# Always keep files that are split per-test
868-
for test_file in specifiers_by_file:
869-
config = config_for_file(test_file)
870-
if config.is_partial_splits_individual_tests(test_file):
890+
for test_file in test_files:
891+
if test_file.partial_splits_individual_tests:
871892
partial_files.add(test_file)
872893
else:
873894
to_split.append(test_file)
874895
partial_files |= set(to_split[selected::total])
875-
specifiers_by_file = {f: s for f, s in specifiers_by_file.items() if f in partial_files}
876-
for test_file, specifiers in specifiers_by_file.items():
877-
if not test_file.exists():
878-
sys.exit(f"File does not exist: {test_file}")
896+
test_files = [f for f in test_files if f in partial_files]
897+
for test_file in test_files:
898+
specifiers = [s for s in all_specifiers if s.test_file == test_file.path]
879899
try:
880900
collected = collect_module(test_file, specifiers, use_tags=use_tags, partial=partial)
881901
except Exception as e:
@@ -888,14 +908,14 @@ def collect(all_specifiers: list[TestSpecifier], *, use_tags=False, ignore=None,
888908
return to_run
889909

890910

891-
def read_tags(test_file: Path, config: Config) -> list[TestId]:
892-
tag_file = config.get_tag_file(test_file)
911+
def read_tags(test_file: TestFileConfig) -> list[TestId]:
912+
tag_file = test_file.get_tag_file()
893913
tags = []
894914
if tag_file.exists():
895915
with open(tag_file) as f:
896916
for line in f:
897917
test = line.strip()
898-
tags.append(TestId(test_file, test))
918+
tags.append(TestId(test_file.path, test))
899919
return tags
900920
return tags
901921

0 commit comments

Comments
 (0)