diff --git a/.gitignore b/.gitignore index 960ff6e..0eb3e7a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ **/__pycache__/** venv/ +build/ dist/ dragon_runner.egg-info/ scratch/ diff --git a/dragon_runner/config.py b/dragon_runner/config.py index 3ec77ad..b5dae7f 100644 --- a/dragon_runner/config.py +++ b/dragon_runner/config.py @@ -11,9 +11,9 @@ from dragon_runner.log import log from dragon_runner.cli import CLIArgs -class SubPackage(): +class SubPackage(Verifiable): """ - Represents a set of tests in a directory + Represents a set of tests in a directory. """ def __init__(self, path: str): self.path: str = path @@ -23,6 +23,12 @@ def __init__(self, path: str): self.tests: List[TestFile] = self.gather_tests() else: self.tests: List[TestFile] = [TestFile(path)] + + def verify(self) -> ErrorCollection: + """ + Verify the tests in our config have no errors. + """ + return ErrorCollection(ec for test in self.tests if (ec := test.verify())) @staticmethod def is_test(test_path: str): @@ -44,7 +50,7 @@ def gather_tests(self) -> List[TestFile]: tests.append(TestFile(test_path)) return tests -class Package(): +class Package(Verifiable): """ Represents a single test package. Shoud have a corresponding CCID if submitted. """ @@ -59,6 +65,12 @@ def __init__(self, path: str): else: self.subpackages.append(SubPackage(path)) + def verify(self) -> ErrorCollection: + """ + Propogate up all errors in subpackages. + """ + return ErrorCollection(ec for spkg in self.subpackages if (ec := spkg.verify())) + def add_subpackage(self, spkg: SubPackage): """ Add a subpackage while keeping total test count up to date @@ -90,13 +102,23 @@ def __init__(self, id: str, exe_path: str, runtime: str): self.exe_path = exe_path self.runtime = runtime self.errors = self.verify() - + def verify(self) -> ErrorCollection: - errors = ErrorCollection() + """ + Check if the binary path exists and runtime path exists (if present) + """ + errors = [] if not os.path.exists(self.exe_path): - errors.add(ConfigError( - f"Cannot find binary file: {self.exe_path} in Executable: {self.id}")) - return errors + errors.append(ConfigError( + f"Cannot find binary file: {self.exe_path} " + f"in Executable: {self.id}") + ) + if self.runtime and not os.path.exists(self.runtime): + errors.append(ConfigError( + f"Cannot find runtime file: {self.runtime} " + f"in Executable: {self.id}") + ) + return ErrorCollection(errors) def source_env(self): """ @@ -148,8 +170,8 @@ def __init__(self, config_path: str, config_data: Dict, debug_package: Optional[ config_data.get('runtimes', "")) self.solution_exe = config_data.get('solutionExecutable', None) self.toolchains = self.parse_toolchains(config_data['toolchains']) - self.error_collection = self.verify() self.packages = self.gather_packages() + self.error_collection = self.verify() def parse_executables(self, executables_data: Dict[str, str], runtimes_data: Dict[str, str]) -> List[Executable]: @@ -200,7 +222,7 @@ def log_test_info(self): def verify(self) -> ErrorCollection: """ - Assert valid paths and that all compositetoolchains and executables also verify. + Pass up all errrors by value in downstream objects like Toolchain, Testfile and Executable """ ec = ErrorCollection() if not os.path.exists(self.test_dir): @@ -209,6 +231,8 @@ def verify(self) -> ErrorCollection: ec.extend(exe.verify().errors) for tc in self.toolchains: ec.extend(tc.verify().errors) + for pkg in self.packages: + ec.extend(pkg.verify().errors) return ec def to_dict(self) -> Dict: diff --git a/dragon_runner/errors.py b/dragon_runner/errors.py index e6d5e09..8225495 100644 --- a/dragon_runner/errors.py +++ b/dragon_runner/errors.py @@ -1,21 +1,46 @@ -from typing import List +from typing import List, Union, Iterable -class ConfigError: +class Error: + def __str__(self): raise NotImplementedError("Must implement __str__") + +class ConfigError(Error): def __init__(self, message: str): self.message = message def __str__(self): - return f"CONFIG_ERROR: {self.message}" + return f"Config Error: {self.message}" -class ErrorCollection: - def __init__(self): - self.errors: List[ConfigError] = [] +class TestFileError(Error): + def __init__(self, message: str): + self.message = message - def add(self, error: ConfigError): + def __str__(self): + return f"Testfile Error: {self.message}" + +class ErrorCollection: + def __init__(self, errors: Union[None, 'ErrorCollection', Iterable[Error]] = None): + self.errors: List[Error] = [] + if errors is not None: + if isinstance(errors, ErrorCollection): + self.errors = errors.errors.copy() + elif isinstance(errors, Iterable): + self.errors = list(errors) + else: + raise TypeError("Must construct ErrorCollection with self or List of Error") + + def has_errors(self) -> bool: + return self.__bool__() + + def add(self, error: Error): self.errors.append(error) - def extend(self, errors: List[ConfigError]): - self.errors.extend(errors) + def extend(self, errors: Union['ErrorCollection', Iterable[Error]]): + if isinstance(errors, ErrorCollection): + self.errors.extend(errors.errors) + elif isinstance(errors, Iterable): + self.errors.extend(errors) + else: + raise TypeError("Must extend ErrorCollection with self or List of Error") def __bool__(self): return len(self.errors) > 0 diff --git a/dragon_runner/testfile.py b/dragon_runner/testfile.py index b8e5bab..901cb6d 100644 --- a/dragon_runner/testfile.py +++ b/dragon_runner/testfile.py @@ -1,31 +1,55 @@ import os from io import BytesIO -from typing import Optional +from typing import Optional, Union from dragon_runner.utils import str_to_bytes, bytes_to_str +from dragon_runner.errors import Verifiable, ErrorCollection, TestFileError -class TestFile: +class TestFile(Verifiable): __test__ = False - def __init__(self, test_path, input_dir="input", - input_stream_dir="input-stream", - output_dir="output", - comment_syntax="//"): + def __init__(self, test_path, input_dir="input", input_stream_dir="input-stream", + output_dir="output", + comment_syntax="//"): self.path = test_path self.stem, self.extension = os.path.splitext(os.path.basename(test_path)) self.file = self.stem + self.extension self.input_dir = input_dir self.input_stream_dir = input_stream_dir self.output_dir = output_dir - self.comment_syntax = comment_syntax # default C99 // - self.expected_out = self.get_expected_out() # fill expected output - self.input_stream = self.get_input_stream() # fill std input stream - self.expected_out_bytes = len(self.expected_out) - self.input_stream_bytes = len(self.input_stream) - - def get_file_bytes(self, file_path: str) -> bytes: - with open(file_path, "rb") as f: - file_bytes = f.read() - assert isinstance(file_bytes, bytes), "expected bytes" - return file_bytes + self.comment_syntax = comment_syntax # default C99 // + self.verify() + + def verify(self) -> ErrorCollection: + """ + Ensure the paths supplied in CHECK_FILE and INPUT_FILE exist + """ + collection = ErrorCollection() + self.expected_out = self.get_expected_out() + self.input_stream = self.get_input_stream() + + # If a parse and read of a tests input or output fails, propogate here + if isinstance(self.expected_out, TestFileError): + collection.add(self.expected_out) + if isinstance(self.input_stream, TestFileError): + collection.add(self.input_stream) + if collection.has_errors(): + return collection + + self.expected_out_bytes = len(self.expected_out) + self.input_stream_bytes = len(self.input_stream) + + def get_file_bytes(self, file_path: str) -> Optional[bytes]: + """ + Get file contents in bytes + """ + try: + with open(file_path, "rb") as f: + file_bytes = f.read() + assert isinstance(file_bytes, bytes), "expected bytes" + return file_bytes + except FileNotFoundError: + return None + except: + return None def get_directive_contents(self, directive_prefix: str) -> Optional[bytes]: """ @@ -55,7 +79,6 @@ def get_directive_contents(self, directive_prefix: str) -> Optional[bytes]: contents.seek(0) if contents: content_bytes = contents.getvalue() - assert isinstance(content_bytes, bytes), "directive content not of type bytes" return content_bytes return None @@ -71,11 +94,10 @@ def get_file_contents(self, file_suffix, symmetric_dir) -> Optional[bytes]: same_dir_path = self.path.replace(self.extension, file_suffix) if os.path.exists(same_dir_path): - return self.get_file_bytes(same_dir_path) - + return self.get_file_bytes(same_dir_path) return None - def get_expected_out(self) -> bytes: + def get_expected_out(self) -> Union[bytes, TestFileError]: """ Load the expected output for a test into a byte stream """ @@ -91,12 +113,13 @@ def get_expected_out(self) -> bytes: if check_file: test_dir = os.path.dirname(self.path) check_file_path = os.path.join(test_dir, bytes_to_str(check_file)) - return self.get_file_bytes(check_file_path) + if not os.path.exists(check_file_path): + return TestFileError( + f"Failed to locate path supplied to CHECK_FILE: {check_file_path}") + return self.get_file_bytes(check_file_path) + return b''# default expect empty output - # default expect empty output - return b'' - - def get_input_stream(self) -> bytes: + def get_input_stream(self) -> Union[bytes, TestFileError]: """ Load the input stream for a test into a byte stream """ @@ -111,11 +134,12 @@ def get_input_stream(self) -> bytes: input_file = self.get_directive_contents("INPUT_FILE:") if input_file: test_dir = os.path.dirname(self.path) - check_file_path = os.path.join(test_dir, bytes_to_str(input_file)) - return self.get_file_bytes(check_file_path) - - # default expect empty output - return b'' + input_file_path = os.path.join(test_dir, bytes_to_str(input_file)) + if not os.path.exists(input_file_path): + return TestFileError( + f"Failed to locate path supplied to INPUT_FILE: {input_file_path}") + return self.get_file_bytes(input_file_path) + return b''# default no input stream def __repr__(self): max_test_name_length = 30 diff --git a/dragon_runner/toolchain.py b/dragon_runner/toolchain.py index 3957b8d..4a27761 100644 --- a/dragon_runner/toolchain.py +++ b/dragon_runner/toolchain.py @@ -62,4 +62,3 @@ def __len__(self) -> int: def __getitem__(self, index: int) -> Step: return self.steps[index] - diff --git a/tests/configs/gccMixConfig.json b/tests/configs/gccMixConfig.json new file mode 100644 index 0000000..9911087 --- /dev/null +++ b/tests/configs/gccMixConfig.json @@ -0,0 +1,24 @@ +{ + "testDir": "../packages/CMixedPackage", + "testedExecutablePaths": { + "gcc": "/usr/bin/gcc" + }, + "toolchains": { + "GCC-toolchain": [ + { + "stepName": "compile", + "executablePath": "$EXE", + "arguments": ["$INPUT", "-o", "$OUTPUT"], + "output": "/tmp/test.o", + "allowError": true + }, + { + "stepName": "run", + "executablePath": "$INPUT", + "arguments": [], + "usesInStr": true, + "allowError": true + } + ] + } +} diff --git a/tests/configs/invalidRutnime.json b/tests/configs/invalidRutnime.json new file mode 100644 index 0000000..613324d --- /dev/null +++ b/tests/configs/invalidRutnime.json @@ -0,0 +1,27 @@ +{ + "testDir": "../packages/CPackage", + "testedExecutablePaths": { + "gcc": "/usr/bin/gcc" + }, + "runtimes": { + "gcc": "../lib/lib-this-runtime-dne.so" + }, + "toolchains": { + "GCC-toolchain": [ + { + "stepName": "compile", + "executablePath": "$EXE", + "arguments": ["$INPUT", "-o", "$OUTPUT"], + "output": "/tmp/test.o", + "allowError": true + }, + { + "stepName": "run", + "executablePath": "$INPUT", + "arguments": [], + "usesInStr": true, + "allowError": true + } + ] + } +} diff --git a/tests/packages/CMixedPackage/mixed/000_nl.c b/tests/packages/CMixedPackage/mixed/000_nl.c new file mode 100644 index 0000000..ce01525 --- /dev/null +++ b/tests/packages/CMixedPackage/mixed/000_nl.c @@ -0,0 +1,15 @@ +#include + +/// This will fail since there is an extra newline emitted. +/// The expected file will have 5 bytes, the generated will have 6 + +int main() { + + printf("a\nb\nc\n"); + + return 0; +} + +//CHECK:a +//CHECK:b +//CHECK:c \ No newline at end of file diff --git a/tests/packages/CMixedPackage/mixed/001_basic.c b/tests/packages/CMixedPackage/mixed/001_basic.c new file mode 100644 index 0000000..3b6972f --- /dev/null +++ b/tests/packages/CMixedPackage/mixed/001_basic.c @@ -0,0 +1,11 @@ +// INPUT:a + +#include + +int main() { + char c; + scanf("%c", &c); + printf("%c", c); +} + +// CHECK:a diff --git a/tests/packages/CMixedPackage/mixed/001_space.c b/tests/packages/CMixedPackage/mixed/001_space.c new file mode 100644 index 0000000..6469d92 --- /dev/null +++ b/tests/packages/CMixedPackage/mixed/001_space.c @@ -0,0 +1,10 @@ +#include + +int main() { + + printf("print\n "); + return 0; +} + +// CHECK:print +// CHECK: \ No newline at end of file diff --git a/tests/packages/CMixedPackage/mixed/002_comment_mixed.c b/tests/packages/CMixedPackage/mixed/002_comment_mixed.c new file mode 100644 index 0000000..555d756 --- /dev/null +++ b/tests/packages/CMixedPackage/mixed/002_comment_mixed.c @@ -0,0 +1,24 @@ + +// below is a line comment +// INPUT:a +// INPUT:bc + +#include + +int main() { + + char carr[1024]; + char c; + + scanf("%c", &c); + printf("%c\n", c); + scanf("%c", &c); // consume newline + + fgets(carr, 1024, stdin); + printf("%s", carr); + + return 0; +} + +// CHECK:a +// CHECK:bc diff --git a/tests/packages/CMixedPackage/mixed/002_error.c b/tests/packages/CMixedPackage/mixed/002_error.c new file mode 100644 index 0000000..10c657c --- /dev/null +++ b/tests/packages/CMixedPackage/mixed/002_error.c @@ -0,0 +1,16 @@ +#include +#include + +/// A check for the wrong line the error is thrown on. + +#define CUSTOM_COMPILE_TIME_ERROR 1 + +int main() { + #if CUSTOM_COMPILE_TIME_ERROR + printf("CompileTimeError on line 10: this is some text after."); + exit(1); + #endif + return 0; +} + +//CHECK:CompileTimeError on line 11: \ No newline at end of file diff --git a/tests/packages/CMixedPackage/mixed/003_error.c b/tests/packages/CMixedPackage/mixed/003_error.c new file mode 100644 index 0000000..472c37c --- /dev/null +++ b/tests/packages/CMixedPackage/mixed/003_error.c @@ -0,0 +1,11 @@ +#include +#include + +// An error test that does not provide a check for the expected output. + +int main() { + + fprintf(stderr, "TypeError on line 5: This is an error!"); + exit(1); + return 0; +} diff --git a/tests/packages/CMixedPackage/mixed/003_input_file.c b/tests/packages/CMixedPackage/mixed/003_input_file.c new file mode 100644 index 0000000..41284e5 --- /dev/null +++ b/tests/packages/CMixedPackage/mixed/003_input_file.c @@ -0,0 +1,25 @@ +// INPUT_FILE:./in-stream/003_input_file.ins + +#include + +int main() { + char line[1024]; + + while (fgets(line, sizeof(line), stdin)) { + // Process each line + int value1, value2; + float value3; + + if (sscanf(line, "%d %d %f", &value1, &value2, &value3) == 3) { + printf("%d, %d, %.2f\n", value1, value2, value3); + } + } + + return 0; +} + +// CHECK:10, 20, 3.14 +// CHECK:15, 30, 2.71 +// CHECK:22, 11, 1.41 +// CHECK:8, 5, 0.99 +// CHECK: \ No newline at end of file diff --git a/tests/packages/CMixedPackage/mixed/004_check_multi.c b/tests/packages/CMixedPackage/mixed/004_check_multi.c new file mode 100644 index 0000000..608f3d0 --- /dev/null +++ b/tests/packages/CMixedPackage/mixed/004_check_multi.c @@ -0,0 +1,20 @@ +#include + +#define MAX_ITER 5 +int main() { + + for (int i = 0; i < MAX_ITER; i++) { + printf("%d", i); + if (i != MAX_ITER-1) { + printf("\n"); + } + } + + return 0; +} + +//CHECK:0 +//CHECK:1 +//CHECK:2 +//CHECK:3 +//CHECK:4 diff --git a/tests/packages/CMixedPackage/mixed/004_error.c b/tests/packages/CMixedPackage/mixed/004_error.c new file mode 100644 index 0000000..581c641 --- /dev/null +++ b/tests/packages/CMixedPackage/mixed/004_error.c @@ -0,0 +1,13 @@ +#include +#include + +// An error test that provides a check but for a different line + +int main() { + + fprintf(stderr, "TypeError on line 5: This is an error!"); + exit(1); + return 0; +} + +//CHECK:WrongError on line 6 \ No newline at end of file