|
1 | 1 | #!/usr/bin/python3
|
2 |
| - |
| 2 | +import argparse |
| 3 | +import logging |
3 | 4 | import sys
|
4 | 5 | import os
|
5 | 6 | import glob
|
6 | 7 | import subprocess
|
| 8 | +import time |
| 9 | +from enum import Enum |
7 | 10 | from pathlib import Path
|
8 |
| - |
9 |
| - |
10 |
| -def print_e(*text, end='\n'): |
11 |
| - print(*text, end=end, file=sys.stderr) |
12 |
| - |
13 |
| - |
14 |
| -FAIL = '' |
15 |
| -OKGREEN = '' |
16 |
| -OKBLUE = '' |
17 |
| -ENDC = '' |
| 11 | +from typing import List, Tuple |
18 | 12 |
|
19 | 13 |
|
20 | 14 | class NoExecutableFileError(Exception):
|
21 | 15 | pass
|
22 | 16 |
|
23 | 17 |
|
24 |
| -class IrregularInOutFileError(Exception): |
| 18 | +class IrregularSampleFileError(Exception): |
25 | 19 | pass
|
26 | 20 |
|
27 | 21 |
|
28 |
| -class NoCppFileError(Exception): |
29 |
| - pass |
| 22 | +class ExecStatus(Enum): |
| 23 | + NORMAL = "NORMAL" |
| 24 | + TLE = "TLE" |
| 25 | + RE = "RE" |
30 | 26 |
|
31 | 27 |
|
32 |
| -class MultipleCppFilesError(Exception): |
33 |
| - pass |
| 28 | +class ExecResult: |
| 29 | + def __init__(self, status: ExecStatus, output: str = None, elapsed_sec: float = None): |
| 30 | + self.status = status |
| 31 | + self.output = output |
| 32 | + if elapsed_sec is not None: |
| 33 | + self.elapsed_ms = int(elapsed_sec * 1000 + 0.5) |
| 34 | + else: |
| 35 | + self.elapsed_ms = None |
| 36 | + |
| 37 | + def is_correct_output(self, answer_text): |
| 38 | + return answer_text == self.output |
34 | 39 |
|
35 | 40 |
|
36 | 41 | def is_executable_file(file_name):
|
37 | 42 | return os.access(file_name, os.X_OK) and Path(file_name).is_file() \
|
38 | 43 | and file_name.find(".cpp") == -1 and not file_name.endswith(".txt") # cppやtxtを省くのは一応の Cygwin 対策
|
39 | 44 |
|
40 | 45 |
|
41 |
| -def do_test(exec_file=None): |
42 |
| - exec_files = [ |
43 |
| - fname for fname in glob.glob( |
44 |
| - './*') if is_executable_file( |
45 |
| - fname)] |
46 |
| - if exec_file is None: |
47 |
| - if len(exec_files) == 0: |
48 |
| - raise NoExecutableFileError |
49 |
| - exec_file = exec_files[0] |
50 |
| - if len(exec_files) >= 2: |
51 |
| - print_e("WARNING: There're multiple executable files. This time, '%s' is selected." % |
52 |
| - exec_file, "candidates =", exec_files) |
53 |
| - |
54 |
| - infiles = sorted(glob.glob('./in_*.txt')) |
55 |
| - outfiles = sorted(glob.glob('./out_*.txt')) |
56 |
| - |
57 |
| - success = 0 |
58 |
| - total = 0 |
59 |
| - |
60 |
| - for infile, outfile in zip(infiles, outfiles): |
61 |
| - if os.path.basename(infile)[2:] != os.path.basename(outfile)[3:]: |
62 |
| - print_e("The output for '%s' is not '%s'!!!" % (infile, outfile)) |
63 |
| - raise IrregularInOutFileError |
64 |
| - with open(infile, "r") as inf, open(outfile, "r") as ouf: |
65 |
| - ans_data = ouf.read() |
66 |
| - out_data = "" |
67 |
| - status = "WA" |
68 |
| - try: |
69 |
| - out_data = subprocess.check_output( |
70 |
| - [exec_file, ""], stdin=inf, universal_newlines=True, timeout=1) |
71 |
| - except subprocess.TimeoutExpired: |
72 |
| - status = "TLE(1s)" |
73 |
| - except subprocess.CalledProcessError: |
74 |
| - status = "RE" |
75 |
| - |
76 |
| - if out_data == ans_data: |
77 |
| - status = "PASSED" |
78 |
| - print_e("# %s ... %s" % (os.path.basename(infile), |
79 |
| - "%s%s%s" % (OKGREEN, status, ENDC))) |
80 |
| - success += 1 |
| 46 | +def infer_exec_file(filenames): |
| 47 | + exec_files = [name for name in sorted( |
| 48 | + filenames) if is_executable_file(name)] |
| 49 | + |
| 50 | + if len(exec_files) == 0: |
| 51 | + raise NoExecutableFileError |
| 52 | + |
| 53 | + exec_file = exec_files[0] |
| 54 | + if len(exec_files) >= 2: |
| 55 | + logging.warning("{0} {1}".format( |
| 56 | + "There're multiple executable files. '{exec_file}' is selected.".format( |
| 57 | + exec_file=exec_file), |
| 58 | + "The candidates were {exec_files}.".format(exec_files=exec_files))) |
| 59 | + return exec_file |
| 60 | + |
| 61 | + |
| 62 | +def infer_case_num(sample_filename: str): |
| 63 | + result = "" |
| 64 | + for c in sample_filename: |
| 65 | + if c.isdigit(): |
| 66 | + result += c |
| 67 | + return int(result) |
| 68 | + |
| 69 | + |
| 70 | +def run_program(exec_file: str, input_file: str, timeout_sec: int) -> ExecResult: |
| 71 | + try: |
| 72 | + elapsed_sec = -time.time() |
| 73 | + out_data = subprocess.check_output( |
| 74 | + [exec_file, ""], stdin=open(input_file, 'r'), universal_newlines=True, timeout=timeout_sec) |
| 75 | + elapsed_sec += time.time() |
| 76 | + return ExecResult(ExecStatus.NORMAL, out_data, elapsed_sec) |
| 77 | + except subprocess.TimeoutExpired: |
| 78 | + return ExecResult(ExecStatus.TLE) |
| 79 | + except subprocess.CalledProcessError: |
| 80 | + return ExecResult(ExecStatus.RE) |
| 81 | + |
| 82 | + |
| 83 | +def build_details_str(exec_res: ExecResult, input_file: str, output_file: str) -> str: |
| 84 | + res = "" |
| 85 | + |
| 86 | + def append(text: str, end='\n'): |
| 87 | + nonlocal res |
| 88 | + res += text + end |
| 89 | + |
| 90 | + append("[Input]") |
| 91 | + with open(input_file, "r") as f: |
| 92 | + append(f.read(), end='') |
| 93 | + |
| 94 | + append("[Expected]") |
| 95 | + with open(output_file, "r") as f: |
| 96 | + append(f.read(), end='') |
| 97 | + |
| 98 | + if exec_res.status == ExecStatus.NORMAL: |
| 99 | + append("[Received]") |
| 100 | + if exec_res.status == ExecStatus.NORMAL: |
| 101 | + append(exec_res.output, end='') |
| 102 | + else: |
| 103 | + append("[Log]") |
| 104 | + append(exec_res.status.name) |
| 105 | + return res |
| 106 | + |
| 107 | + |
| 108 | +def run_for_samples(exec_file: str, sample_pair_list: List[Tuple[str, str]], timeout_sec: int, knock_out: bool = False): |
| 109 | + success_count = 0 |
| 110 | + for in_sample_file, out_sample_file in sample_pair_list: |
| 111 | + # Run program |
| 112 | + exec_res = run_program(exec_file, in_sample_file, |
| 113 | + timeout_sec=timeout_sec) |
| 114 | + |
| 115 | + # Output header |
| 116 | + with open(out_sample_file, 'r') as f: |
| 117 | + answer_text = f.read() |
| 118 | + |
| 119 | + is_correct = exec_res.is_correct_output(answer_text) |
| 120 | + if is_correct: |
| 121 | + message = "PASSED {elapsed} ms".format(elapsed=exec_res.elapsed_ms) |
| 122 | + success_count += 1 |
| 123 | + else: |
| 124 | + if exec_res.status == ExecStatus.NORMAL: |
| 125 | + message = "WA" |
81 | 126 | else:
|
82 |
| - print_e("# %s ... %s" % (os.path.basename(infile), |
83 |
| - "%s%s%s" % (FAIL, status, ENDC))) |
84 |
| - print_e("[Input]") |
85 |
| - with open(infile, "r") as inf2: |
86 |
| - print_e(inf2.read(), end='') |
87 |
| - print_e("[Expected]") |
88 |
| - print_e("%s%s%s" % |
89 |
| - (OKBLUE, ans_data, ENDC), end='') |
90 |
| - print_e("[Received]") |
91 |
| - print_e("%s%s%s" % |
92 |
| - (FAIL, out_data, ENDC), end='') |
93 |
| - print_e() |
94 |
| - total += 1 |
95 |
| - |
96 |
| - success_flag = False |
97 |
| - if total == 0: |
98 |
| - print_e("No test cases") |
99 |
| - elif success != total: |
100 |
| - print_e("Some cases FAILED (passed %s of %s)" % (success, total)) |
| 127 | + message = exec_res.status.name |
| 128 | + |
| 129 | + print("# {case_name} ... {message}".format( |
| 130 | + case_name=os.path.basename(in_sample_file), |
| 131 | + message=message, |
| 132 | + )) |
| 133 | + |
| 134 | + # Output details for incorrect results. |
| 135 | + if not is_correct: |
| 136 | + print('{}\n'.format(build_details_str( |
| 137 | + exec_res, in_sample_file, out_sample_file))) |
| 138 | + if knock_out: |
| 139 | + print('Stop testing ...') |
| 140 | + break |
| 141 | + return success_count |
| 142 | + |
| 143 | + |
| 144 | +def validate_sample_pair(in_sample_file, out_sample_file): |
| 145 | + if os.path.basename(in_sample_file).split("_")[-1] != os.path.basename(out_sample_file).split("_")[-1]: |
| 146 | + logging.error( |
| 147 | + 'The file combination of {} and {} is wrong.'.format( |
| 148 | + in_sample_file, |
| 149 | + out_sample_file |
| 150 | + )) |
| 151 | + raise IrregularSampleFileError |
| 152 | + |
| 153 | + |
| 154 | +def run_single_test(exec_file, in_sample_file_list, out_sample_file_list, timeout_sec: int, case_num: int) -> bool: |
| 155 | + def single_or_none(lst: List): |
| 156 | + if len(lst) == 1: |
| 157 | + return lst[0] |
| 158 | + if len(lst) == 0: |
| 159 | + return None |
| 160 | + raise IrregularSampleFileError( |
| 161 | + "Multiple samples are detected for given case num: {}".format(lst)) |
| 162 | + |
| 163 | + in_sample_file = single_or_none( |
| 164 | + [name for name in in_sample_file_list if infer_case_num(name) == case_num]) |
| 165 | + out_sample_file = single_or_none( |
| 166 | + [name for name in out_sample_file_list if infer_case_num(name) == case_num]) |
| 167 | + |
| 168 | + if in_sample_file is None or out_sample_file is None: |
| 169 | + print("Invalid test case number: {}".format(case_num)) |
| 170 | + return False |
| 171 | + |
| 172 | + validate_sample_pair(in_sample_file, out_sample_file) |
| 173 | + |
| 174 | + success_count = run_for_samples( |
| 175 | + exec_file, [(in_sample_file, out_sample_file)], timeout_sec) |
| 176 | + |
| 177 | + return success_count == 1 |
| 178 | + |
| 179 | + |
| 180 | +def run_all_tests(exec_file, in_sample_file_list, out_sample_file_list, timeout_sec: int, knock_out: bool) -> bool: |
| 181 | + if len(in_sample_file_list) != len(out_sample_file_list): |
| 182 | + logging.error("{0}{1}{2}".format( |
| 183 | + "The number of the sample inputs and outputs are different.\n", |
| 184 | + "# of sample inputs: {}\n".format(len(in_sample_file_list)), |
| 185 | + "# of sample outputs: {}\n".format(len(out_sample_file_list)))) |
| 186 | + raise IrregularSampleFileError |
| 187 | + samples = [] |
| 188 | + for in_sample_file, out_sample_file in zip(in_sample_file_list, out_sample_file_list): |
| 189 | + validate_sample_pair(in_sample_file, out_sample_file) |
| 190 | + samples.append((in_sample_file, out_sample_file)) |
| 191 | + |
| 192 | + success_count = run_for_samples(exec_file, samples, timeout_sec, knock_out) |
| 193 | + |
| 194 | + if len(samples) == 0: |
| 195 | + print("No test cases") |
| 196 | + return False |
| 197 | + elif success_count != len(samples): |
| 198 | + print("Some cases FAILED (passed {success_count} of {total})".format( |
| 199 | + success_count=success_count, |
| 200 | + total=len(samples), |
| 201 | + )) |
| 202 | + return False |
101 | 203 | else:
|
102 |
| - print_e("Passed all testcases!!!") |
103 |
| - success_flag = True |
104 |
| - return success_flag |
| 204 | + print("Passed all test cases!!!") |
| 205 | + return True |
| 206 | + |
| 207 | + |
| 208 | +def main(prog, args) -> bool: |
| 209 | + parser = argparse.ArgumentParser( |
| 210 | + prog=prog, |
| 211 | + formatter_class=argparse.RawTextHelpFormatter) |
| 212 | + |
| 213 | + parser.add_argument("--exec", '-e', |
| 214 | + help="file path to the execution target. Automatically detects an exec file if not specified.", |
| 215 | + default=None) |
| 216 | + |
| 217 | + parser.add_argument("--num", '-n', |
| 218 | + help="the case number to test (1-origin). All cases are tested if not specified.", |
| 219 | + type=int, |
| 220 | + default=None) |
| 221 | + |
| 222 | + parser.add_argument("--dir", '-d', |
| 223 | + help="target directory to test. [Default] Current directory", |
| 224 | + default=".") |
| 225 | + |
| 226 | + parser.add_argument("--timeout", '-t', |
| 227 | + help="Timeout for each test cases (sec) [Default] 1", |
| 228 | + type=int, |
| 229 | + default=1) |
| 230 | + |
| 231 | + parser.add_argument("--knock-out", '-k', |
| 232 | + help="Stop execution immediately after any example's failure [Default] False", |
| 233 | + action="store_true", |
| 234 | + default=False) |
| 235 | + |
| 236 | + args = parser.parse_args(args) |
| 237 | + exec_file = args.exec or infer_exec_file( |
| 238 | + glob.glob(os.path.join(args.dir, '*'))) |
| 239 | + in_sample_file_list = sorted(glob.glob(os.path.join(args.dir, 'in_*.txt'))) |
| 240 | + out_sample_file_list = sorted( |
| 241 | + glob.glob(os.path.join(args.dir, 'out_*.txt'))) |
| 242 | + |
| 243 | + if args.num is None: |
| 244 | + return run_all_tests(exec_file, in_sample_file_list, out_sample_file_list, args.timeout, args.knock_out) |
| 245 | + else: |
| 246 | + return run_single_test(exec_file, in_sample_file_list, out_sample_file_list, args.timeout, args.num) |
105 | 247 |
|
106 | 248 |
|
107 | 249 | if __name__ == "__main__":
|
108 |
| - if do_test(): |
109 |
| - sys.exit(0) |
110 |
| - else: |
111 |
| - sys.exit(-1) |
| 250 | + main(sys.argv[0], sys.argv[1:]) |
0 commit comments