Coverage for src / competitive_verifier / verify / main.py: 100%

87 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-10-04 07:40 +0900

1import math 

2import pathlib 

3from argparse import ArgumentParser 

4from functools import cached_property 

5from logging import getLogger 

6from typing import Literal 

7 

8from pydantic import Field, field_validator 

9 

10from competitive_verifier import github 

11from competitive_verifier.arg import ( 

12 IgnoreErrorArguments, 

13 VerboseArguments, 

14 VerifyFilesJsonArguments, 

15 WriteSummaryArguments, 

16) 

17from competitive_verifier.log import GitHubMessageParams 

18from competitive_verifier.models import VerificationInput, VerifyCommandResult 

19 

20from .verifier import ChangeDetection, SplitState, Verifier 

21 

22logger = getLogger(__name__) 

23 

24 

25class Verify( 

26 WriteSummaryArguments, 

27 IgnoreErrorArguments, 

28 VerifyFilesJsonArguments, 

29 VerboseArguments, 

30): 

31 subcommand: Literal["verify"] = Field( 

32 default="verify", 

33 description="Verify library", 

34 ) 

35 timeout: float = math.inf 

36 default_tle: float | None = None 

37 default_mle: float | None = None 

38 

39 prev_result: pathlib.Path | None = None 

40 change_detection: ChangeDetection = "timestamp" 

41 

42 download: bool = True 

43 

44 output: pathlib.Path | None = None 

45 

46 split: int | None = None 

47 split_index: int | None = None 

48 

49 def read_prev_result(self) -> VerifyCommandResult | None: 

50 if not self.prev_result: 

51 return None 

52 try: 

53 return VerifyCommandResult.parse_file_relative(self.prev_result) 

54 except Exception: 

55 logger.warning( 

56 "Failed to parse prev_result: %s", 

57 self.prev_result, 

58 extra={"github": GitHubMessageParams(file=self.prev_result)}, 

59 ) 

60 

61 def write_result(self, result: VerifyCommandResult): 

62 super().write_result(result) 

63 

64 result_json = result.model_dump_json(exclude_none=True) 

65 print(result_json) 

66 

67 if self.output: 

68 self.output.parent.mkdir(parents=True, exist_ok=True) 

69 self.output.write_text(result_json, encoding="utf-8") 

70 

71 @field_validator("timeout", mode="after") 

72 @classmethod 

73 def timeout_zero_equals_inf(cls, value: float) -> float: 

74 if value == 0: 

75 return math.inf 

76 return value 

77 

78 @cached_property 

79 def split_state(self) -> SplitState | None: 

80 split = self.split 

81 split_index = self.split_index 

82 match (split_index, split): 

83 case (int(), int()): 

84 if split <= 0: 

85 raise ValueError("--split must be greater than 0.") 

86 if not (0 <= split_index < split): 

87 raise ValueError( 

88 "--split-index must be greater than 0 and less than --split." 

89 ) 

90 return SplitState(size=split, index=split_index) 

91 case (None, int()): 

92 raise ValueError("--split argument requires --split-index argument.") 

93 case (int(), None): 

94 raise ValueError("--split-index argument requires --split argument.") 

95 case _: 

96 return None 

97 

98 @classmethod 

99 def add_parser(cls, parser: ArgumentParser): 

100 super().add_parser(parser) 

101 parser.add_argument( 

102 "--timeout", 

103 type=float, 

104 default=math.inf, 

105 help="Timeout seconds. if value is zero, it is same to math.inf.", 

106 ) 

107 parser.add_argument( 

108 "--tle", 

109 dest="default_tle", 

110 type=float, 

111 default=None, 

112 help="Threshold seconds to be TLE", 

113 ) 

114 parser.add_argument( 

115 "--mle", 

116 dest="default_mle", 

117 type=float, 

118 default=None, 

119 help="Threshold memory usage (MB) to be MLE", 

120 ) 

121 parser.add_argument( 

122 "--prev-result", 

123 type=pathlib.Path, 

124 required=False, 

125 help="Previous result json file", 

126 ) 

127 parser.add_argument( 

128 "--change-detection", 

129 choices=("timestamp", "hash"), 

130 default="timestamp", 

131 help="How to detect whether a file has changed since the prev-result:" 

132 " 'timestamp' (default) compares its modification time with the" 

133 " previous verification time; 'hash' compares a content hash of the" 

134 " file and its transitive dependencies, and the hash of the test" 

135 " data present in the cache.", 

136 ) 

137 

138 parser.add_argument( 

139 "--no-download", 

140 action="store_false", 

141 dest="download", 

142 help="Suppress `oj download`." 

143 " Verifications whose test data isn't downloaded yet fail.", 

144 ) 

145 parser.add_argument( 

146 "--output", 

147 "-o", 

148 type=pathlib.Path, 

149 required=False, 

150 help="The output file for which verifier saves the result json.", 

151 ) 

152 parallel_group = parser.add_argument_group("parallel") 

153 parallel_group.add_argument( 

154 "--split", 

155 type=int, 

156 help="Parallel job size", 

157 required=False, 

158 ) 

159 parallel_group.add_argument( 

160 "--split-index", 

161 type=int, 

162 help="Parallel job index", 

163 required=False, 

164 ) 

165 

166 def _run(self) -> bool: 

167 logger.debug("arguments:%s", self) 

168 logger.info("verify_files_json=%s", self.verify_files_json) 

169 verifications = VerificationInput.parse_file_relative(self.verify_files_json) 

170 prev_result = self.read_prev_result() 

171 

172 verifier = Verifier( 

173 verifications, 

174 use_git_timestamp=github.env.is_in_github_actions(), 

175 timeout=self.timeout, 

176 default_tle=self.default_tle, 

177 default_mle=self.default_mle, 

178 prev_result=prev_result, 

179 change_detection=self.change_detection, 

180 split_state=self.split_state, 

181 ) 

182 result = verifier.verify(download=self.download) 

183 self.write_result(result) 

184 

185 is_success = result.is_success() 

186 

187 if is_success: 

188 logger.info("success!") 

189 else: 

190 logger.warning("not success!") 

191 

192 return is_success or self.ignore_error