basic formatting of results
This commit is contained in:
parent
91bb8a8e45
commit
bbc55a3099
@ -1,3 +1,9 @@
|
|||||||
# test runner
|
# test runner
|
||||||
|
|
||||||
testr is a script for running competitive programming test cases (for example for CodeForces).
|
testr is a script for running competitive programming test cases (for example for CodeForces).
|
||||||
|
|
||||||
|
## dependencies
|
||||||
|
|
||||||
|
You can install all of these via Arch Linux's repositories:
|
||||||
|
|
||||||
|
- colorama
|
||||||
|
18
testr.py
18
testr.py
@ -1,7 +1,10 @@
|
|||||||
|
from colorama import just_fix_windows_console, Fore, Style
|
||||||
|
just_fix_windows_console()
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from testr.file_data import DirectorySuite, ExecutableRunner
|
from testr.file_data import DirectorySuite, ExecutableRunner
|
||||||
from testr.runner import TestOptions
|
from testr.runner import StatusCode, TestOptions
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
@ -17,6 +20,14 @@ parser.add_argument("--timelim", help="Time limit in seconds", type=float, defau
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
code_styles = {
|
||||||
|
StatusCode.AC: Fore.GREEN,
|
||||||
|
StatusCode.WA: Fore.RED,
|
||||||
|
StatusCode.IR: Fore.YELLOW,
|
||||||
|
StatusCode.TLE: Fore.YELLOW,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
test_suite = DirectorySuite(Path(args.testdir))
|
test_suite = DirectorySuite(Path(args.testdir))
|
||||||
test_runner = ExecutableRunner(Path(args.exec))
|
test_runner = ExecutableRunner(Path(args.exec))
|
||||||
@ -27,7 +38,10 @@ async def main():
|
|||||||
time_limit=args.timelim
|
time_limit=args.timelim
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
print(test_case.code)
|
print(
|
||||||
|
f"{test_case.test_data.name : <20} "
|
||||||
|
f"{code_styles.get(test_case.code, '')}{test_case.code.name : >4}{Style.RESET_ALL}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
@ -6,13 +6,14 @@ from pathlib import Path
|
|||||||
class FileData(TestData):
|
class FileData(TestData):
|
||||||
"""Backend to parse test data from files."""
|
"""Backend to parse test data from files."""
|
||||||
|
|
||||||
def __init__(self, input_file: Path, output_file: Path):
|
def __init__(self, input_file: Path, output_file: Path, name: str):
|
||||||
if not input_file.is_file():
|
if not input_file.is_file():
|
||||||
raise ValueError(f"input_file must be a file, got '{input_file}'")
|
raise ValueError(f"input_file must be a file, got '{input_file}'")
|
||||||
if not output_file.is_file():
|
if not output_file.is_file():
|
||||||
raise ValueError(f"output_file must be a file, got '{output_file}'")
|
raise ValueError(f"output_file must be a file, got '{output_file}'")
|
||||||
self.input_file = input_file
|
self.input_file = input_file
|
||||||
self.output_file = output_file
|
self.output_file = output_file
|
||||||
|
self.name = name
|
||||||
|
|
||||||
async def get_input(self) -> str:
|
async def get_input(self) -> str:
|
||||||
with open(self.input_file, "r") as f:
|
with open(self.input_file, "r") as f:
|
||||||
@ -45,17 +46,17 @@ class ExecutableRunner(TestRunner):
|
|||||||
proc.communicate(input=input_data.encode()), timeout=opts.time_limit)
|
proc.communicate(input=input_data.encode()), timeout=opts.time_limit)
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
proc.kill()
|
proc.kill()
|
||||||
return TestStatus(code=StatusCode.TLE, stderr="", stdout="", stdin=input_data)
|
return TestStatus(code=StatusCode.TLE, stderr="", stdout="", test_data = data)
|
||||||
|
|
||||||
stdout: str = out_stream.decode()
|
stdout: str = out_stream.decode()
|
||||||
stderr: str = err_stream.decode()
|
stderr: str = err_stream.decode()
|
||||||
|
|
||||||
if proc.returncode != 0:
|
if proc.returncode != 0:
|
||||||
return TestStatus(code=StatusCode.IR, stdout=stdout, stderr=stderr, stdin=input_data)
|
return TestStatus(code=StatusCode.IR, stdout=stdout, stderr=stderr, test_data=data)
|
||||||
|
|
||||||
correct: bool = await data.validate_output(stdout)
|
correct: bool = await data.validate_output(stdout)
|
||||||
ret_code = StatusCode.AC if correct else StatusCode.WA
|
ret_code = StatusCode.AC if correct else StatusCode.WA
|
||||||
return TestStatus(code=ret_code, stdout=stdout, stderr=stderr, stdin=input_data)
|
return TestStatus(code=ret_code, stdout=stdout, stderr=stderr, test_data=data)
|
||||||
|
|
||||||
|
|
||||||
class DirectorySuite(TestSuite):
|
class DirectorySuite(TestSuite):
|
||||||
@ -66,13 +67,13 @@ class DirectorySuite(TestSuite):
|
|||||||
|
|
||||||
if not test_dir.is_dir():
|
if not test_dir.is_dir():
|
||||||
raise ValueError(f"test_dir must be a directory, got '{test_dir}'")
|
raise ValueError(f"test_dir must be a directory, got '{test_dir}'")
|
||||||
for inp_file in test_dir.glob("*.in"):
|
for inp_file in sorted(test_dir.glob("*.in")):
|
||||||
if not inp_file.is_file:
|
if not inp_file.is_file:
|
||||||
continue
|
continue
|
||||||
outp_file = inp_file.with_suffix(".out")
|
outp_file = inp_file.with_suffix(".out")
|
||||||
if not outp_file.is_file():
|
if not outp_file.is_file():
|
||||||
raise ValueError(f"output file '{outp_file}' is not a valid file")
|
raise ValueError(f"output file '{outp_file}' is not a valid file")
|
||||||
self.test_cases.append(FileData(inp_file, outp_file))
|
self.test_cases.append(FileData(inp_file, outp_file, name=inp_file.name))
|
||||||
|
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
|
@ -27,18 +27,6 @@ class StatusCode(Enum):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class TestStatus:
|
|
||||||
"""
|
|
||||||
Status of an individual test case.
|
|
||||||
"""
|
|
||||||
|
|
||||||
code: StatusCode
|
|
||||||
stdin: str
|
|
||||||
stderr: str
|
|
||||||
stdout: str
|
|
||||||
|
|
||||||
|
|
||||||
class TestInput(ABC):
|
class TestInput(ABC):
|
||||||
"""Input provider for single test case."""
|
"""Input provider for single test case."""
|
||||||
|
|
||||||
@ -69,9 +57,14 @@ class TestOptions:
|
|||||||
|
|
||||||
|
|
||||||
class TestData(TestInput, TestValidator):
|
class TestData(TestInput, TestValidator):
|
||||||
"""Combined input/output for single test case"""
|
"""
|
||||||
|
All information for a single test case.
|
||||||
|
|
||||||
pass
|
name
|
||||||
|
Short name for the test case. Can be a file name.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
class TestSuite(ABC):
|
class TestSuite(ABC):
|
||||||
@ -81,6 +74,20 @@ class TestSuite(ABC):
|
|||||||
def __iter__(self) -> Iterator[TestData]: pass
|
def __iter__(self) -> Iterator[TestData]: pass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TestStatus:
|
||||||
|
"""
|
||||||
|
Status of an individual test case.
|
||||||
|
"""
|
||||||
|
|
||||||
|
code: StatusCode
|
||||||
|
|
||||||
|
test_data: TestData
|
||||||
|
|
||||||
|
stderr: str
|
||||||
|
stdout: str
|
||||||
|
|
||||||
|
|
||||||
class TestRunner(ABC):
|
class TestRunner(ABC):
|
||||||
"""Runner for test cases."""
|
"""Runner for test cases."""
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user