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 abc import ABC, abstractmethod
|
||||||
|
from collections.abc import Iterable
|
||||||
from enum import Enum, auto
|
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()
|
AC = auto()
|
||||||
@ -19,39 +21,52 @@ class TestStatus(Enum):
|
|||||||
"""
|
"""
|
||||||
Time Limit Exceeded: program took too long to execute this case.
|
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):
|
@dataclass
|
||||||
"""Input and output for single test case."""
|
class TestStatus:
|
||||||
|
"""
|
||||||
|
Status of an individual test case.
|
||||||
|
"""
|
||||||
|
|
||||||
|
code: StatusCode
|
||||||
|
stderr: str
|
||||||
|
stdout: str
|
||||||
|
|
||||||
|
|
||||||
|
class TestInput(ABC):
|
||||||
|
"""Input provider for single test case."""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_input(self) -> str: pass
|
async def get_input(self) -> str: pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestValidator(ABC):
|
||||||
|
"""Output validator for single test case."""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def validate_output(self) -> bool: pass
|
async def validate_output(self, output: str) -> bool: pass
|
||||||
|
|
||||||
|
|
||||||
class TestCase(ABC):
|
class TestData(TestInput, TestValidator):
|
||||||
"""Runner for a single test case."""
|
"""Combined input/output for single test case"""
|
||||||
|
|
||||||
def __init__(self, data: TestData):
|
pass
|
||||||
self.test_data = data
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
@property
|
|
||||||
@abstractmethod
|
class TestRunner(ABC):
|
||||||
def status(self) -> TestStatus: pass
|
"""Runner for test cases."""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def run_test(self) -> None: pass
|
async def run_test(self, data: TestData) -> StatusCode: pass
|
||||||
|
|
||||||
|
|
||||||
class TestSuite(ABC):
|
class TestSuite(ABC):
|
||||||
"""Loader for multiple test cases."""
|
"""Loader for multiple test cases."""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def __next__(self) -> TestCase: pass
|
def __iter__(self) -> Iterable[TestData]: pass
|
||||||
|
Loading…
Reference in New Issue
Block a user