dotfiles/bin/kat

367 lines
12 KiB
Plaintext
Raw Normal View History

2023-12-23 20:13:47 -07:00
#!/usr/bin/env python3
# Compiler and tester for kattis problems
#
# kat expects a ./sample directory with files named /input_[0-9]+/ and a
# corresponding /output_[0-9]+/ file. These will be run in sequence from lowest
# to highest number
#
# Supported languages:
# - Rust: Expects a ./src/main.rs file, as well as a ./Cargo.toml
# - Python: Expects with a single .py file or a main.py or ./src/main.py
# - C: Expects a ./main.c file or ./src/main.c
import argparse, sys, os, pathlib, time, subprocess, re
from abc import ABC, abstractmethod
from typing import Tuple
from pathlib import Path
class ProgrammingLang(ABC):
@abstractmethod
def __init__(self, root: Path, main_file: Path):
pass
@abstractmethod
def compile_debug(self):
pass
@abstractmethod
def compile_release(self):
pass
@abstractmethod
def run_debug(self, input_file, output_file) -> int:
pass
@abstractmethod
def run_release(self, input_file, output_file) -> int:
pass
class Rust(ProgrammingLang):
def __init__(self, root: Path, main_file: Path):
self.main_file = main_file
self.root = root
self.bin_name = root.name
self.release_bin = os.path.join(root, 'target', 'release', self.bin_name)
self.debug_bin = os.path.join(root, 'target', 'debug', self.bin_name)
def compile_debug(self):
cmd = subprocess.run(['cargo', 'build'], cwd=self.root)
if cmd.returncode != 0:
raise Exception(f'Compile time error: code {cmd.returncode}')
def compile_release(self):
cmd = subprocess.run(['cargo', 'build', '--release'], cwd=self.root)
if cmd.returncode != 0:
raise Exception(f'Compile time error: code {cmd.returncode}')
def __run(self, input_file, output_file, sub_dir: Path) -> int:
start = time.time()
cmd = subprocess.run(
os.path.join(self.root, 'target', sub_dir, self.bin_name),
stdin=input_file, stdout=output_file)
end = time.time()
if cmd.returncode != 0:
raise Exception(f'Runtime error: code {cmd.returncode}')
return end - start
def run_debug(self, input_file, output_file) -> int:
return self.__run(input_file, output_file, 'debug')
def run_release(self, input_file, output_file) -> int:
return self.__run(input_file, output_file, 'release')
class Clang(ProgrammingLang):
def __init__(self, root: Path, main_file: Path):
self.main_file = main_file
self.root = root
self.bin_name = 'clang_out'
self.release_bin = self.debug_bin = os.path.join(root, self.bin_name)
def compile_debug(self):
cmd = subprocess.run([
'gcc', '-Wall', '-Wextra', '-Werror',
'-g', '-O2', '-std=gnu11', '-static',
self.main_file,
'-o', self.debug_bin, '-lm'
])
if cmd.returncode != 0:
raise Exception(f'Compile time error: code {cmd.returncode}')
def compile_release(self):
cmd = subprocess.run([
'gcc', '-g', '-O2', '-std=gnu11', '-static',
self.main_file,
'-o', self.release_bin, '-lm'
])
if cmd.returncode != 0:
raise Exception(f'Compile time error: code {cmd.returncode}')
def run_debug(self, input_file, output_file) -> int:
return self.run_release(input_file, output_file)
def run_release(self, input_file, output_file) -> int:
start = time.time()
cmd = subprocess.run(self.release_bin,
stdin=input_file, stdout=output_file)
end = time.time()
return end - start
class CPlusPlus(ProgrammingLang):
def __init__(self, root: Path, main_file: Path):
self.main_file = main_file
self.root = root
self.bin_name = 'clang_out'
self.release_bin = self.debug_bin = os.path.join(root, self.bin_name)
def compile_debug(self):
cmd = subprocess.run([
'g++', '-g', '-O2', '-std=gnu++17', '-static',
'-lrt', '-Wl,--whole-archive', '-lpthread',
'-Wl,--no-whole-archive',
'-Wall', '-Wextra', '-Werror',
self.main_file,
'-o', self.release_bin
])
if cmd.returncode != 0:
raise Exception(f'Compile time error: code {cmd.returncode}')
def compile_release(self):
cmd = subprocess.run([
'g++', '-g', '-O2', '-std=gnu++17', '-static',
'-lrt', '-Wl,--whole-archive', '-lpthread',
'-Wl,--no-whole-archive',
self.main_file,
'-o', self.release_bin
])
if cmd.returncode != 0:
raise Exception(f'Compile time error: code {cmd.returncode}')
def run_debug(self, input_file, output_file) -> int:
return self.run_release(input_file, output_file)
def run_release(self, input_file, output_file) -> int:
start = time.time()
cmd = subprocess.run(self.release_bin,
stdin=input_file, stdout=output_file)
end = time.time()
return end - start
class Python(ProgrammingLang):
def __init__(self, root: Path, main_file: Path):
self.main_file = main_file
self.root = root
self.bin_name = main_file.name
self.release_bin = self.debug_bin = main_file
def compile_debug(self):
pass
def compile_release(self):
pass
def __run(self, input_file, output_file, interpreter) -> int:
start = time.time()
cmd = subprocess.run([interpreter, self.release_bin],
stdin=input_file, stdout=output_file)
end = time.time()
if cmd.returncode != 0:
raise Exception(f'Runtime error: {cmd.returncode}')
return end - start
def run_debug(self, input_file, output_file) -> int:
return self.__run(input_file, output_file, '/usr/bin/python3')
def run_release(self, input_file, output_file) -> int:
return self.__run(input_file, output_file, '/usr/bin/pypy3')
# Returns the language object
def find_language(root: Path):
ls = os.listdir(root)
if 'src' in ls:
src = os.listdir(os.path.join(root, 'src'))
if 'main.rs' in src:
return Rust(root, Path('src', 'main.rs'))
elif 'main.c' in src:
return Clang(root, Path('src', 'main.c'))
elif 'main.cc' in src:
return CPlusPlus(root, Path('src', 'main.cc'))
elif 'main.cpp' in src:
return CPlusPlus(root, Path('src', 'main.cpp'))
elif 'main.py' in src:
return Python(root, Path('src', 'main.py'))
elif '.py' in '\t'.join(ls):
return Python(root, Path('src', next(p for p in ls if '.py' in p)))
if 'main.rs' in ls:
return Rust(root, Path('main.rs'))
elif 'main.c' in ls:
return Clang(root, Path('main.c'))
elif 'main.cc' in ls:
return CPlusPlus(root, Path('main.cc'))
elif 'main.cpp' in ls:
return CPlusPlus(root, Path('main.cpp'))
elif 'main.py' in ls:
return Python(root, Path('main.py'))
elif '.py' in '\t'.join(ls):
return Python(root,
Path(os.path.join(root, next(p for p in ls if '.py' in p))))
raise Exception('Language not found or not supported')
def project_root() -> Path:
cwd = Path('.').cwd()
if cwd.name == 'sample' or cwd.name == 'src':
return cwd.parent
return cwd
def test_files(root: Path, nb: int) -> Tuple[Path, Path, Path]:
t_input = os.path.join(root, f'sample/input_{nb}')
t_output = os.path.join(root, f'sample/output_{nb}')
real_output = os.path.join(root, f'sample/run_output_{nb}')
return t_input, t_output, real_output
def is_test_exists(n: str, root: Path) -> bool:
samples = os.listdir(os.path.join(root, 'sample'))
if f'input_{n}' not in samples:
print(f'sample/input_{n} not found')
return False
if f'output_{n}' not in samples:
print(f'sample/output_{n} not found')
return False
return True
def run_test(lang, n: int, root: Path, is_time: bool, is_debug: bool) -> bool:
t_in_name, t_out_name, r_out_name = test_files(root, n)
r_out = open(r_out_name, 'w')
t_out = open(t_out_name, 'r')
t_in = open(t_in_name, 'r')
print(f"==== Testcase {n} ====")
if is_debug:
run_time = lang.run_debug(t_in, r_out)
else:
run_time = lang.run_release(t_in, r_out)
t_in.close()
r_out.close()
if is_time:
print("^^^^^^^^^^^^^^^^^^^^^^")
print(f"Time: {run_time : 4.3f}s")
r_out = open(r_out_name, 'r')
lines_real = r_out.read().splitlines()
lines_out = t_out.read().splitlines()
t_out.close()
r_out.close()
if len(lines_real) != len(lines_out):
if not is_time:
print("^^^^^^^^^^^^^^^^^^^^^^")
print(f"!!! Mismatched output\n"
f"Real line count: {len(lines_real)}\n"
f"Expected line count: {len(lines_out)}")
print(f'!!! Failed on test {n}. See ./sample/run_output_{n}')
return False
for i in range(len(lines_real)):
if lines_real[i] != lines_out[i]:
if not is_time:
print("^^^^^^^^^^^^^^^^^^^^^^")
print(f"!!! Mismatched output\n"
f"Real Output: `{lines_real[i]}`\n"
f"Expected Out: `{lines_out[i]}`")
print(f'!!! Failed on test {n}. See ./sample/run_output_{n}')
return False
return True
# Returns True if all cases passed. Otherwise returns False and the number of
# test cases
def run_tests(lang, root: Path, is_time: bool, is_debug: bool) -> Tuple[bool, int]:
try:
sample_dir = os.listdir(os.path.join(root, 'sample'))
except FileNotFoundError:
print("No ./sample directory found. Cannot test script")
return [False, None]
cases = list(filter(lambda x: x is not None,
[re.match(r'^input_([0-9]+)$', f) for f in sample_dir]))
for case in cases:
if not is_test_exists(case.group(1), root):
raise Exception()
N = len(cases)
cases.sort(key=lambda x: x.group(0))
for case in cases:
try:
if not run_test(lang, case.group(1), root, is_time, is_debug):
return False, N
except Exception as e:
print(e)
return False, N
return True, N
def main():
parser = argparse.ArgumentParser(
description='Compiler and tester for kattis problems')
parser.add_argument("-t", "--time", action="store_true",
help="Time the runtime of each test case")
parser.add_argument("-d", "--debug", action="store_true",
help="Use the debugging compiler")
parser.add_argument("test_case", type=str, metavar='T', nargs='?',
help="Time one specific test case in debug")
args = parser.parse_args()
root = project_root()
try:
lang = find_language(root)
except Exception as e:
print(f'Failed to detect language: {e}')
sys.exit(1)
try:
if args.debug or args.test_case is not None:
lang.compile_debug()
else:
lang.compile_release()
except Exception as e:
print(e)
sys.exit(1)
if args.test_case is not None:
if is_test_exists(args.test_case, root):
if run_test(lang, args.test_case, root, True, True):
print(f'Passed test case ({args.test_case})')
else:
is_passed, n = run_tests(lang, root,
args.time or args.test_case,
args.debug or args.test_case)
if is_passed:
print(f'All test ({n}) cases passed!')
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
print("\nKeyboard interrupt. Program killed");
sys.exit(1)
else:
sys.exit(0)