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
« 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
8from pydantic import Field, field_validator
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
20from .verifier import ChangeDetection, SplitState, Verifier
22logger = getLogger(__name__)
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
39 prev_result: pathlib.Path | None = None
40 change_detection: ChangeDetection = "timestamp"
42 download: bool = True
44 output: pathlib.Path | None = None
46 split: int | None = None
47 split_index: int | None = None
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
62 def write_result(self, result: VerifyCommandResult):
63 super().write_result(result)
65 result_json = result.model_dump_json(exclude_none=True)
66 print(result_json)
68 if self.output:
69 self.output.parent.mkdir(parents=True, exist_ok=True)
70 self.output.write_text(result_json, encoding="utf-8")
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
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
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 )
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 )
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()
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)
185 is_success = result.is_success()
187 if is_success:
188 logger.info("success!")
189 else:
190 logger.warning("not success!")
192 return is_success or self.ignore_error