implemented runner classes

This commit is contained in:
dogeystamp 2023-11-01 15:52:43 -04:00
parent 4b443a5201
commit 7367c3c2b2
Signed by: dogeystamp
GPG Key ID: 7225FE3592EFFA38
4 changed files with 113 additions and 18 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
__pycache__/

0
testr/__init__.py Normal file
View File

79
testr/file_data.py Normal file
View 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__()

View File

@ -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