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

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 

7 

8 

9class TestCaseRule(abc.ABC): 

10 

11 def __init__(self, problem: 'Problem'): 

12 self.problem = problem 

13 

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 

21 

22 

23class IncludeDirectory(TestCaseRule): 

24 

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) 

34 

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) 

39 

40 if not path.exists(): 

41 if self.optional: 

42 return True 

43 raise BadTestCase(f'directory {self.path} does not exist') 

44 

45 if path.is_file(): 

46 raise BadTestCase(f'{self.path} is not a directory') 

47 

48 return True 

49 

50 

51class SimpleIO(TestCaseRule): 

52 ''' 

53 Test cases that only contains single input and output file. 

54 ''' 

55 

56 def __init__(self, problem: 'Problem', excludes: List[str] = []): 

57 self.excludes = excludes 

58 super().__init__(problem) 

59 

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 

76 

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 

84 

85 

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 ''' 

91 

92 def validate(self, test_case_fp: BinaryIO) -> bool: 

93 if test_case_fp is None: 

94 raise BadTestCase('test case is None') 

95 

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') 

101 

102 expected_dirs = self.expected_test_case_dirs() 

103 

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) 

111 

112 if len(expected_dirs): 

113 raise BadTestCase( 

114 f'missing test case directory: {", ".join(expected_dirs)}') 

115 

116 def validate_test_case_dir(self, test_case_dir: zipfile.Path): 

117 requireds = { 

118 'STDIN', 

119 'STDOUT', 

120 } 

121 

122 for r in requireds: 

123 if not (test_case_dir / r).exists(): 

124 raise BadTestCase(f'required file/dir not found: {r}') 

125 

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}') 

138 

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