Skip to content

Commit 934ce06

Browse files
authored
実行時間制限を問題文から取得、ローカルテストに反映する機能を追加 (#254)
* timeoutを問題文から取得するのを追加, testerのMetadata完全一致を修正 * テストのmetadataをtimeoutに対応 * metadataから制限時間を読むテストを追加 * timeout->timeout_ms * READMEのtimeout_adjustmentの記述を修正。timeout_adjustmentのテストを追加
1 parent 21d16aa commit 934ce06

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+248
-94
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ optional arguments:
215215

216216
- `compile_before_testing` テスト前にコンパイルを実行するか否かをTrue/Falseで指定。何も指定しないとFalseとなります。
217217
- `compile_only_when_diff_detected` テスト前のコンパイルの際、元のソースに変更があった場合のみ実行するかをTrue/Falseで指定。何も指定しないとFalseとなります。
218+
- `timeout_adjustment=1.2` 問題文に記載された実行時間制限にこの値をかけた秒数がローカルテストの際の実行時間制限になります。例えばatcoderで制限時間2秒の問題は2x1.2=2.4秒となります。atcoderとローカルの実行環境が異なる場合の調整に使用してください。
218219

219220
なお、`compile_before_testing`, `compile_only_when_diff_detected`はいずれもtesterの引数で指定することも可能で、指定した場合はそちらが優先されます。
220221

@@ -243,6 +244,7 @@ compile_only_when_diff_detected=true
243244
[tester]
244245
compile_before_testing=true
245246
compile_only_when_diff_detected=true
247+
timeout_adjustment=1.2
246248
[etc]
247249
download_without_login=false
248250
parallel_download=false

atcodertools/config/tester_config.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ def __init__(self,
44
compile_before_testing: bool = False,
55
compile_only_when_diff_detected: bool = False,
66
compile_command: str = None,
7-
run_command: str = None
7+
run_command: str = None,
8+
timeout_adjustment: float = 1.0
89
):
910
self.compile_before_testing = compile_before_testing
1011
self.compile_only_when_diff_detected = compile_only_when_diff_detected
1112
self.compile_command = compile_command
1213
self.run_command = run_command
14+
self.timeout_adjustment = timeout_adjustment

atcodertools/constprediction/constants_prediction.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from atcodertools.common.judgetype import ErrorType, NormalJudge, DecimalJudge, Judge
88
from atcodertools.common.logging import logger
99
from atcodertools.constprediction.models.problem_constant_set import ProblemConstantSet
10+
import math
1011

1112

1213
class YesNoPredictionFailedError(Exception):
@@ -19,6 +20,12 @@ def __init__(self, cands):
1920
self.cands = cands
2021

2122

23+
class MultipleLimitCandidatesError(Exception):
24+
25+
def __init__(self, cands):
26+
self.cands = cands
27+
28+
2229
class NoDecimalCandidatesError(Exception):
2330
pass
2431

@@ -31,6 +38,7 @@ def __init__(self, cands):
3138

3239
MOD_ANCHORS = ["余り", "あまり", "mod", "割っ", "modulo"]
3340
DECIMAL_ANCHORS = ["誤差", " error "]
41+
LIMIT_ANCHORS = ["時間制限", "Time Limit"]
3442

3543
MOD_STRATEGY_RE_LIST = [
3644
re.compile("([0-9]+).?.?.?で割った"),
@@ -47,6 +55,10 @@ def __init__(self, cands):
4755
re.compile("1e(-[0-9]+)")
4856
]
4957

58+
LIMIT_STRATEGY_RE_LIST = [
59+
re.compile("([0-9.]+)\s*sec")
60+
]
61+
5062

5163
def is_mod_context(sentence):
5264
for kw in MOD_ANCHORS:
@@ -62,6 +74,13 @@ def is_decimal_context(sentence):
6274
return False
6375

6476

77+
def is_limit_context(sentence):
78+
for kw in LIMIT_ANCHORS:
79+
if kw in sentence:
80+
return True
81+
return False
82+
83+
6584
def predict_modulo(html: str) -> Optional[int]:
6685
def normalize(sentence):
6786
return sentence.replace('\\', '').replace("{", "").replace("}", "").replace(",", "").replace(" ", "").replace(
@@ -161,6 +180,32 @@ def normalize(sentence):
161180
return NormalJudge()
162181

163182

183+
def predict_limit(html: str) -> Optional[int]:
184+
def normalize(sentence):
185+
return sentence.replace('\\', '').replace("{", "").replace("}", "").replace(",", "").replace(" ", "").lower().strip()
186+
187+
soup = BeautifulSoup(html, "html.parser")
188+
sentences = soup.get_text().split("\n")
189+
sentences = [normalize(s) for s in sentences if is_limit_context(s)]
190+
191+
limit_cands = set()
192+
193+
for s in sentences:
194+
for regexp in LIMIT_STRATEGY_RE_LIST:
195+
m = regexp.search(s)
196+
if m is not None:
197+
extracted_val = float(m.group(1))
198+
limit_cands.add(extracted_val)
199+
200+
if len(limit_cands) == 0:
201+
return None
202+
203+
if len(limit_cands) == 1:
204+
return math.floor(list(limit_cands)[0] * 1000.0 + 0.5)
205+
206+
raise MultipleModCandidatesError(limit_cands)
207+
208+
164209
def predict_constants(html: str) -> ProblemConstantSet:
165210
try:
166211
yes_str, no_str = predict_yes_no(html)
@@ -181,4 +226,11 @@ def predict_constants(html: str) -> ProblemConstantSet:
181226
"two or more candidates {} are detected as decimal values".format(e.cands))
182227
judge = NormalJudge()
183228

184-
return ProblemConstantSet(mod=mod, yes_str=yes_str, no_str=no_str, judge_method=judge)
229+
try:
230+
timeout = predict_limit(html)
231+
except MultipleLimitCandidatesError as e:
232+
logger.warning("limit prediction failed -- "
233+
"two or more candidates {} are detected as limit".format(e.cands))
234+
timeout = None
235+
236+
return ProblemConstantSet(mod=mod, yes_str=yes_str, no_str=no_str, judge_method=judge, timeout=timeout)

atcodertools/constprediction/models/problem_constant_set.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ def __init__(self,
88
yes_str: str = None,
99
no_str: str = None,
1010
judge_method: Judge = None,
11+
timeout: float = None
1112
):
1213
self.mod = mod
1314
self.yes_str = yes_str
1415
self.no_str = no_str
1516
self.judge_method = judge_method
17+
self.timeout = timeout

atcodertools/executils/run_program.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def has_stderr(self):
3030
return len(self.stderr) > 0
3131

3232

33-
def run_program(exec_file: str, input_file: str, timeout_sec: int, args=None, current_working_dir: str = None) -> ExecResult:
33+
def run_program(exec_file: str, input_file: str, timeout_sec: float, args=None, current_working_dir: str = None) -> ExecResult:
3434
if args is None:
3535
args = []
3636
try:

atcodertools/tools/envgen.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ def emit_info(text):
152152
config.etc_config.out_example_format.replace("{}", "*"),
153153
lang,
154154
constants.judge_method,
155+
constants.timeout
155156
).save_to(metadata_path)
156157
emit_info("Saved metadata to {}".format(metadata_path))
157158

atcodertools/tools/models/metadata.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@
88
class Metadata:
99

1010
def __init__(self, problem: Problem, code_filename: str, sample_in_pattern: str, sample_out_pattern: str,
11-
lang: Language, judge_method: Judge = NormalJudge()):
11+
lang: Language, judge_method: Judge = NormalJudge(), timeout_ms: int = None):
1212
self.problem = problem
1313
self.code_filename = code_filename
1414
self.sample_in_pattern = sample_in_pattern
1515
self.sample_out_pattern = sample_out_pattern
1616
self.lang = lang
1717
self.judge_method = judge_method
18+
self.timeout_ms = timeout_ms
1819

1920
def to_dict(self):
2021
return {
@@ -24,6 +25,7 @@ def to_dict(self):
2425
"sample_out_pattern": self.sample_out_pattern,
2526
"lang": self.lang.name,
2627
"judge": self.judge_method.to_dict(),
28+
"timeout_ms": self.timeout_ms,
2729
}
2830

2931
@classmethod
@@ -39,13 +41,19 @@ def from_dict(cls, dic):
3941
else:
4042
judge_method = NormalJudge()
4143

44+
if "timeout_ms" in dic:
45+
timeout_ms = dic["timeout_ms"]
46+
else:
47+
timeout_ms = None
48+
4249
return Metadata(
4350
problem=Problem.from_dict(dic["problem"]),
4451
code_filename=dic["code_filename"],
4552
sample_in_pattern=dic["sample_in_pattern"],
4653
sample_out_pattern=dic["sample_out_pattern"],
4754
lang=Language.from_name(dic["lang"]),
48-
judge_method=judge_method
55+
judge_method=judge_method,
56+
timeout_ms=timeout_ms
4957
)
5058

5159
@classmethod

atcodertools/tools/tester.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ def append(text: str, end='\n'):
111111
return res
112112

113113

114-
def run_for_samples(exec_file: str, sample_pair_list: List[Tuple[str, str]], timeout_sec: int,
114+
def run_for_samples(exec_file: str, sample_pair_list: List[Tuple[str, str]], timeout_sec: float,
115115
judge_method: Judge = NormalJudge(), knock_out: bool = False,
116116
skip_io_on_success: bool = False, cwd="./") -> TestSummary:
117117
success_count = 0
@@ -171,7 +171,7 @@ def validate_sample_pair(in_sample_file, out_sample_file):
171171
raise IrregularSampleFileError
172172

173173

174-
def run_single_test(exec_file, in_sample_file_list, out_sample_file_list, timeout_sec: int, case_num: int,
174+
def run_single_test(exec_file, in_sample_file_list, out_sample_file_list, timeout_sec: float, case_num: int,
175175
judge_method: Judge, cwd: str, judge_program_language: Language) -> bool:
176176
def single_or_none(lst: List):
177177
if len(lst) == 1:
@@ -197,7 +197,7 @@ def single_or_none(lst: List):
197197
return test_summary.success_count == 1 and not test_summary.has_error_output
198198

199199

200-
def run_all_tests(exec_file, in_sample_file_list, out_sample_file_list, timeout_sec: int, knock_out: bool,
200+
def run_all_tests(exec_file, in_sample_file_list, out_sample_file_list, timeout_sec: float, knock_out: bool,
201201
skip_stderr_on_success: bool, judge_method: Judge, cwd: str,
202202
judge_program_language: Language) -> bool:
203203
if len(in_sample_file_list) != len(out_sample_file_list):
@@ -303,9 +303,9 @@ def main(prog, args) -> bool:
303303
default=".")
304304

305305
parser.add_argument("--timeout", '-t',
306-
help="Timeout for each test cases (sec) [Default] 1",
307-
type=int,
308-
default=1)
306+
help="Timeout for each test cases (sec) [Default] auto",
307+
type=float,
308+
default=None)
309309

310310
parser.add_argument("--knock-out", '-k',
311311
help="Stop execution immediately after any example's failure [Default] False",
@@ -374,6 +374,15 @@ def main(prog, args) -> bool:
374374
with open(args.config, "r") as f:
375375
config = Config.load(f, {ConfigType.TESTER}, args, lang.name)
376376

377+
if args.timeout is None:
378+
if metadata.timeout_ms is None:
379+
logger.info(
380+
"timeout_ms is not found in metadata. Default timeout (2.0 sec) is set. ")
381+
args.timeout = 2.0
382+
else:
383+
args.timeout = float(metadata.timeout_ms) / 1000.0
384+
args.timeout *= config.tester_config.timeout_adjustment
385+
377386
in_sample_file_list = sorted(
378387
glob.glob(os.path.join(args.dir, metadata.sample_in_pattern)))
379388
out_sample_file_list = sorted(

tests/resources/test_atc_env/test_backup/agc029/A/metadata.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@
1212
"problem_id": "agc029_a"
1313
},
1414
"sample_in_pattern": "input_*.txt",
15-
"sample_out_pattern": "output_*.txt"
15+
"sample_out_pattern": "output_*.txt",
16+
"timeout_ms": 2000
1617
}

tests/resources/test_atc_env/test_backup/agc029/B/metadata.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@
1212
"problem_id": "agc029_b"
1313
},
1414
"sample_in_pattern": "input_*.txt",
15-
"sample_out_pattern": "output_*.txt"
15+
"sample_out_pattern": "output_*.txt",
16+
"timeout_ms": 2000
1617
}

0 commit comments

Comments
 (0)