implemented runner classes
This commit is contained in:
parent
4b443a5201
commit
7367c3c2b2
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
__pycache__/
|
0
testr/__init__.py
Normal file
0
testr/__init__.py
Normal file
79
testr/file_data.py
Normal file
79
testr/file_data.py
Normal file
@ -0,0 +1,79 @@
|
||||
import asyncio
|
||||
from testr.runner import TestData, TestRunner, TestStatus, TestSuite, StatusCode
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class FileData(TestData):
|
||||
"""Backend to parse test data from files."""
|
||||
|
||||
def __init__(self, input_file: Path, output_file: Path):
|
||||
if not input_file.is_file():
|
||||
raise ValueError(f"input_file must be a file, got '{input_file}'")
|
||||
if not output_file.is_file():
|
||||
raise ValueError(f"output_file must be a file, got '{output_file}'")
|
||||
self.input_file = input_file
|
||||
self.output_file = output_file
|
||||
|
||||
async def get_input(self) -> str:
|
||||
with open(self.input_file, "r") as f:
|
||||
return f.read()
|
||||
|
||||
async def validate_output(self, output: str) -> bool:
|
||||
with open(self.output_file, "r") as f:
|
||||
correct = f.read()
|
||||
return correct == output
|
||||
|
||||
|
||||
class ExecutableRunner(TestRunner):
|
||||
def __init__(self, executable: Path):
|
||||
if not executable.is_file():
|
||||
raise ValueError(f"executable must be a file, got '{executable}'")
|
||||
self.executable = executable
|
||||
|
||||
async def run_test(self, data: TestData) -> TestStatus:
|
||||
proc = await asyncio.create_subprocess_shell(
|
||||
str(self.executable),
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
)
|
||||
|
||||
input_data = (await data.get_input()).encode()
|
||||
|
||||
try:
|
||||
out_stream, err_stream = await asyncio.wait_for(
|
||||
proc.communicate(input=input_data), timeout=5.0)
|
||||
except TimeoutError:
|
||||
proc.kill()
|
||||
return TestStatus(code=StatusCode.TLE, stderr="", stdout="")
|
||||
|
||||
stdout: str = out_stream.decode()
|
||||
stderr: str = err_stream.decode()
|
||||
|
||||
if proc.returncode != 0:
|
||||
return TestStatus(code=StatusCode.IR, stdout=stdout, stderr=stderr)
|
||||
|
||||
correct: bool = await data.validate_output(stdout)
|
||||
ret_code = StatusCode.AC if correct else StatusCode.WA
|
||||
return TestStatus(code=ret_code, stdout=stdout, stderr=stderr)
|
||||
|
||||
|
||||
class DirectorySuite(TestSuite):
|
||||
"""Loader for .in, .out files in a directory."""
|
||||
|
||||
def __init__(self, test_dir: Path):
|
||||
self.test_cases = []
|
||||
|
||||
if not test_dir.is_dir():
|
||||
raise ValueError(f"test_dir must be a directory, got '{test_dir}'")
|
||||
for inp_file in test_dir.glob("*.in"):
|
||||
if not inp_file.is_file:
|
||||
continue
|
||||
outp_file = inp_file.with_suffix("out")
|
||||
if not outp_file.is_file():
|
||||
raise ValueError(f"output file '{outp_file}' is not a valid file")
|
||||
self.test_cases.append(FileData(inp_file, outp_file))
|
||||
|
||||
|
||||
def __iter__(self):
|
||||
return self.test_cases.__iter__()
|
@ -1,10 +1,12 @@
|
||||
from dataclasses import dataclass
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Iterable
|
||||
from enum import Enum, auto
|
||||
|
||||
|
||||
class TestStatus(Enum):
|
||||
class StatusCode(Enum):
|
||||
"""
|
||||
Status of an individual test case.
|
||||
Status codes for an individual test case.
|
||||
"""
|
||||
|
||||
AC = auto()
|
||||
@ -19,39 +21,52 @@ class TestStatus(Enum):
|
||||
"""
|
||||
Time Limit Exceeded: program took too long to execute this case.
|
||||
"""
|
||||
WJ = auto()
|
||||
IR = auto()
|
||||
"""
|
||||
Waiting for Judgement: this test case has not been finished yet.
|
||||
Invalid Return: program did not return 0
|
||||
"""
|
||||
|
||||
|
||||
class TestData(ABC):
|
||||
"""Input and output for single test case."""
|
||||
@dataclass
|
||||
class TestStatus:
|
||||
"""
|
||||
Status of an individual test case.
|
||||
"""
|
||||
|
||||
code: StatusCode
|
||||
stderr: str
|
||||
stdout: str
|
||||
|
||||
|
||||
class TestInput(ABC):
|
||||
"""Input provider for single test case."""
|
||||
|
||||
@abstractmethod
|
||||
def get_input(self) -> str: pass
|
||||
async def get_input(self) -> str: pass
|
||||
|
||||
|
||||
class TestValidator(ABC):
|
||||
"""Output validator for single test case."""
|
||||
|
||||
@abstractmethod
|
||||
async def validate_output(self) -> bool: pass
|
||||
async def validate_output(self, output: str) -> bool: pass
|
||||
|
||||
|
||||
class TestCase(ABC):
|
||||
"""Runner for a single test case."""
|
||||
class TestData(TestInput, TestValidator):
|
||||
"""Combined input/output for single test case"""
|
||||
|
||||
def __init__(self, data: TestData):
|
||||
self.test_data = data
|
||||
super().__init__()
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def status(self) -> TestStatus: pass
|
||||
|
||||
class TestRunner(ABC):
|
||||
"""Runner for test cases."""
|
||||
|
||||
@abstractmethod
|
||||
async def run_test(self) -> None: pass
|
||||
async def run_test(self, data: TestData) -> StatusCode: pass
|
||||
|
||||
|
||||
class TestSuite(ABC):
|
||||
"""Loader for multiple test cases."""
|
||||
|
||||
@abstractmethod
|
||||
def __next__(self) -> TestCase: pass
|
||||
def __iter__(self) -> Iterable[TestData]: pass
|
||||
|
Loading…
Reference in New Issue
Block a user