Coverage for mongo/problem/test_case.py: 100%
86 statements
« prev ^ index » next coverage.py v7.3.2, created at 2024-11-05 04:22 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2024-11-05 04:22 +0000
1import abc
2import zipfile
3from typing import BinaryIO, Set, TYPE_CHECKING, List
4from .exception import BadTestCase
5if TYPE_CHECKING:
6 from .. import Problem # pragma: no cover
9class TestCaseRule(abc.ABC):
11 def __init__(self, problem: 'Problem'):
12 self.problem = problem
14 # TODO: define generic validation error
15 @abc.abstractmethod
16 def validate(self, test_case: BinaryIO) -> bool:
17 '''
18 Validate test case
19 '''
20 raise NotImplementedError # pragma: no cover
23class IncludeDirectory(TestCaseRule):
25 def __init__(
26 self,
27 problem: 'Problem',
28 path: str,
29 optional: bool = True,
30 ):
31 self.path = path
32 self.optional = optional
33 super().__init__(problem)
35 def validate(self, test_case: BinaryIO) -> bool:
36 if test_case is None:
37 raise BadTestCase('test case is None')
38 path = zipfile.Path(test_case, at=self.path)
40 if not path.exists():
41 if self.optional:
42 return True
43 raise BadTestCase(f'directory {self.path} does not exist')
45 if path.is_file():
46 raise BadTestCase(f'{self.path} is not a directory')
48 return True
51class SimpleIO(TestCaseRule):
52 '''
53 Test cases that only contains single input and output file.
54 '''
56 def __init__(self, problem: 'Problem', excludes: List[str] = []):
57 self.excludes = excludes
58 super().__init__(problem)
60 def validate(self, test_case: BinaryIO) -> bool:
61 # test case must not be None
62 if test_case is None:
63 raise BadTestCase('test case is None')
64 got = {*zipfile.ZipFile(test_case).namelist()}
65 for ex in self.excludes:
66 if ex.endswith('/'):
67 got = {g for g in got if not g.startswith(ex)}
68 else:
69 got.discard(ex)
70 expected = self.expected_test_case_filenames()
71 if got != expected:
72 raise BadTestCase('I/O data not equal to meta provided')
73 # reset
74 test_case.seek(0)
75 return True
77 def expected_test_case_filenames(self) -> Set[str]:
78 excepted = set()
79 for i, task in enumerate(self.problem.test_case.tasks):
80 for j in range(task.case_count):
81 excepted.add(f'{i:02d}{j:02d}.in')
82 excepted.add(f'{i:02d}{j:02d}.out')
83 return excepted
86class ContextIO(TestCaseRule):
87 '''
88 Test cases that contains multiple file for input/output.
89 e.g. given a image, rotate and save it on disk.
90 '''
92 def validate(self, test_case_fp: BinaryIO) -> bool:
93 if test_case_fp is None:
94 raise BadTestCase('test case is None')
96 test_case_root = zipfile.Path(test_case_fp, at='test-case/')
97 if not test_case_root.exists():
98 raise BadTestCase('test-case not found')
99 if not test_case_root.is_dir():
100 raise BadTestCase('test-case is not a directory')
102 expected_dirs = self.expected_test_case_dirs()
104 for test_case in test_case_root.iterdir():
105 try:
106 expected_dirs.remove(test_case.name)
107 except KeyError:
108 raise BadTestCase(
109 f'extra test case directory found: {test_case.name}')
110 self.validate_test_case_dir(test_case)
112 if len(expected_dirs):
113 raise BadTestCase(
114 f'missing test case directory: {", ".join(expected_dirs)}')
116 def validate_test_case_dir(self, test_case_dir: zipfile.Path):
117 requireds = {
118 'STDIN',
119 'STDOUT',
120 }
122 for r in requireds:
123 if not (test_case_dir / r).exists():
124 raise BadTestCase(f'required file/dir not found: {r}')
126 dirs = {
127 'in',
128 'out',
129 }
130 for fp in test_case_dir.iterdir():
131 # files under in/out are allowed
132 if fp.is_dir() and fp.name in dirs:
133 continue
134 # STDIN/STDOUT are allowed
135 if fp.name in requireds:
136 continue
137 raise BadTestCase(f'files in unallowed path: {fp.name}')
139 def expected_test_case_dirs(self) -> Set[str]:
140 excepted = set()
141 for i, task in enumerate(self.problem.test_case.tasks):
142 for j in range(task.case_count):
143 excepted.add(f'{i:02d}{j:02d}')
144 return excepted