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

88 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-10-03 23: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 return None 

61 

62 def write_result(self, result: VerifyCommandResult): 

63 super().write_result(result) 

64 

65 result_json = result.model_dump_json(exclude_none=True) 

66 print(result_json) 

67 

68 if self.output: 

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

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

71 

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

73 @classmethod 

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

75 if value == 0: 

76 return math.inf 

77 return value 

78 

79 @cached_property 

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

81 split = self.split 

82 split_index = self.split_index 

83 match (split_index, split): 

84 case (int(), int()): 

85 if split <= 0: 

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

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

88 raise ValueError( 

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

90 ) 

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

92 case (None, int()): 

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

94 case (int(), None): 

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

96 case _: 

97 return None 

98 

99 @classmethod 

100 def add_parser(cls, parser: ArgumentParser): 

101 super().add_parser(parser) 

102 parser.add_argument( 

103 "--timeout", 

104 type=float, 

105 default=math.inf, 

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

107 ) 

108 parser.add_argument( 

109 "--tle", 

110 dest="default_tle", 

111 type=float, 

112 default=None, 

113 help="Threshold seconds to be TLE", 

114 ) 

115 parser.add_argument( 

116 "--mle", 

117 dest="default_mle", 

118 type=float, 

119 default=None, 

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

121 ) 

122 parser.add_argument( 

123 "--prev-result", 

124 type=pathlib.Path, 

125 required=False, 

126 help="Previous result json file", 

127 ) 

128 parser.add_argument( 

129 "--change-detection", 

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

131 default="timestamp", 

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

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

134 " previous verification time; 'hash' compares a hash of the file," 

135 " its transitive dependencies, and verification settings.", 

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