Compare commits

..

No commits in common. "f247fb6019d2fa2b07e786cee84e16b58c6a2423" and "4f324d58a4101e7d8a6b48dbe4a6d98b67e1d1ee" have entirely different histories.

5 changed files with 35 additions and 60 deletions

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "gusty-testr" name = "gusty-testr"
version = "0.0.2" version = "0.0.1"
authors = [ authors = [
{ name="dogeystamp", email="dogeystamp@disroot.org" } { name="dogeystamp", email="dogeystamp@disroot.org" }
] ]

View File

@ -1,3 +0,0 @@
from testr.cli import main
main()

View File

@ -3,8 +3,8 @@ from colorama import just_fix_windows_console, Fore, Style
import asyncio import asyncio
from pathlib import Path from pathlib import Path
from .file_data import DirectorySuite, ExecutableRunner from testr.file_data import DirectorySuite, ExecutableRunner
from .runner import StatusCode, TestOptions from testr.runner import StatusCode, TestOptions
import argparse import argparse
@ -18,16 +18,13 @@ async def run_cli():
test_data_group = parser.add_mutually_exclusive_group(required=True) test_data_group = parser.add_mutually_exclusive_group(required=True)
test_data_group.add_argument("--testdir", help="Directory for test cases") test_data_group.add_argument("--testdir", help="Directory for test cases")
test_runner_group.add_argument( test_runner_group.add_argument("--exec", help="Executable to run test cases against")
"--exec", help="Executable to run test cases against"
)
parser.add_argument( parser.add_argument("--timelim", help="Time limit in seconds", type=float, default=5.0)
"--timelim", help="Time limit in seconds", type=float, default=5.0
)
args = parser.parse_args() args = parser.parse_args()
code_styles = { code_styles = {
StatusCode.AC: Fore.GREEN, StatusCode.AC: Fore.GREEN,
StatusCode.WA: Fore.RED, StatusCode.WA: Fore.RED,
@ -38,22 +35,21 @@ async def run_cli():
test_suite = DirectorySuite(Path(args.testdir)) test_suite = DirectorySuite(Path(args.testdir))
test_runner = ExecutableRunner(Path(args.exec)) test_runner = ExecutableRunner(Path(args.exec))
status_counts = {k: 0 for k in StatusCode} status_counts = { k: 0 for k in StatusCode }
test_case_count = 0 test_case_count = 0
async for test_case in test_runner.run_test_suite( async for test_case in test_runner.run_test_suite(
test_suite, TestOptions(time_limit=args.timelim) test_suite,
): TestOptions(
time_limit=args.timelim
)
):
test_case_count += 1 test_case_count += 1
status_counts[test_case.code] += 1 status_counts[test_case.code] += 1
print( print(
f"{test_case.test_data.name : <15} " f"{test_case.test_data.name : <15} "
f"[ {code_styles.get(test_case.code, '')}{test_case.code.name}{Style.RESET_ALL} ]" f"[ {code_styles.get(test_case.code, '')}{test_case.code.name}{Style.RESET_ALL} ]"
) )
if test_case.code == StatusCode.IR:
print("---\n" + Style.BRIGHT + "Program stderr:" + Style.RESET_ALL)
print("\n".join([f" {line}" for line in test_case.stderr.split("\n")]))
if test_case_count > 15: if test_case_count > 15:
print("\n Summary:\n") print("\n Summary:\n")
@ -61,9 +57,9 @@ async def run_cli():
for code in StatusCode: for code in StatusCode:
if status_counts[code] > 0: if status_counts[code] > 0:
print( print(
f"{code_styles.get(code, '')}{code.name : >7}{Style.RESET_ALL}: " f"{code_styles.get(code, '')}{code.name : >7}{Style.RESET_ALL}: "
f"x{status_counts[code]}" f"x{status_counts[code]}"
) )
print(f"\n Finished in{time.perf_counter() - start_time : .2f} seconds.\n") print(f"\n Finished in{time.perf_counter() - start_time : .2f} seconds.\n")
@ -71,6 +67,5 @@ async def run_cli():
def main(): def main():
asyncio.run(run_cli()) asyncio.run(run_cli())
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@ -1,12 +1,5 @@
import asyncio import asyncio
from testr.runner import ( from testr.runner import TestData, TestOptions, TestRunner, TestStatus, TestSuite, StatusCode
TestData,
TestOptions,
TestRunner,
TestStatus,
TestSuite,
StatusCode,
)
from pathlib import Path from pathlib import Path
@ -40,29 +33,26 @@ class ExecutableRunner(TestRunner):
async def run_test(self, data: TestData, opts: TestOptions) -> TestStatus: async def run_test(self, data: TestData, opts: TestOptions) -> TestStatus:
proc = await asyncio.create_subprocess_shell( proc = await asyncio.create_subprocess_shell(
str(self.executable), str(self.executable),
stdout=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
stdin=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE,
) )
input_data = await data.get_input() input_data = await data.get_input()
try: try:
out_stream, err_stream = await asyncio.wait_for( out_stream, err_stream = await asyncio.wait_for(
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="", test_data=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( return TestStatus(code=StatusCode.IR, stdout=stdout, stderr=stderr, test_data=data)
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
@ -83,9 +73,8 @@ class DirectorySuite(TestSuite):
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( self.test_cases.append(FileData(inp_file, outp_file, name=inp_file.with_suffix("").name))
FileData(inp_file, outp_file, name=inp_file.with_suffix("").name)
)
def __iter__(self): def __iter__(self):
return self.test_cases.__iter__() return self.test_cases.__iter__()

View File

@ -31,16 +31,14 @@ class TestInput(ABC):
"""Input provider for single test case.""" """Input provider for single test case."""
@abstractmethod @abstractmethod
async def get_input(self) -> str: async def get_input(self) -> str: pass
pass
class TestValidator(ABC): class TestValidator(ABC):
"""Output validator for single test case.""" """Output validator for single test case."""
@abstractmethod @abstractmethod
async def validate_output(self, output: str) -> bool: async def validate_output(self, output: str) -> bool: pass
pass
@dataclass @dataclass
@ -73,8 +71,7 @@ class TestSuite(ABC):
"""Loader for multiple test cases.""" """Loader for multiple test cases."""
@abstractmethod @abstractmethod
def __iter__(self) -> Iterator[TestData]: def __iter__(self) -> Iterator[TestData]: pass
pass
@dataclass @dataclass
@ -95,11 +92,8 @@ class TestRunner(ABC):
"""Runner for test cases.""" """Runner for test cases."""
@abstractmethod @abstractmethod
async def run_test(self, data: TestData, opts: TestOptions) -> TestStatus: async def run_test(self, data: TestData, opts: TestOptions) -> TestStatus: pass
pass
async def run_test_suite( async def run_test_suite(self, data: TestSuite, opts: TestOptions) -> AsyncIterator[TestStatus]:
self, data: TestSuite, opts: TestOptions
) -> AsyncIterator[TestStatus]:
for test_case in data: for test_case in data:
yield await self.run_test(test_case, opts) yield await self.run_test(test_case, opts)