lograder 0.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. lograder/__init__.py +2 -0
  2. lograder/_core_exceptions.py +7 -0
  3. lograder/common/__init__.py +0 -0
  4. lograder/common/types.py +5 -0
  5. lograder/common/utils.py +6 -0
  6. lograder/dispatch/__init__.py +9 -0
  7. lograder/dispatch/_core_exceptions.py +10 -0
  8. lograder/dispatch/common/__init__.py +30 -0
  9. lograder/dispatch/common/assignment.py +99 -0
  10. lograder/dispatch/common/exceptions.py +42 -0
  11. lograder/dispatch/common/file_operations.py +118 -0
  12. lograder/dispatch/common/interface.py +227 -0
  13. lograder/dispatch/common/templates/__init__.py +10 -0
  14. lograder/dispatch/common/templates/cli_builder.py +59 -0
  15. lograder/dispatch/common/templates/executable_runner.py +23 -0
  16. lograder/dispatch/common/templates/trivial.py +34 -0
  17. lograder/dispatch/common/types.py +55 -0
  18. lograder/dispatch/cpp/__init__.py +7 -0
  19. lograder/dispatch/cpp/cmake.py +172 -0
  20. lograder/dispatch/cpp/cpp_source.py +101 -0
  21. lograder/dispatch/exceptions.py +19 -0
  22. lograder/dispatch/misc/__init__.py +4 -0
  23. lograder/dispatch/misc/dispatcher.py +89 -0
  24. lograder/dispatch/misc/makefile.py +89 -0
  25. lograder/exceptions.py +15 -0
  26. lograder/output/__init__.py +0 -0
  27. lograder/output/common/__init__.py +3 -0
  28. lograder/output/common/types.py +6 -0
  29. lograder/output/formatters/__init__.py +0 -0
  30. lograder/output/formatters/default.py +359 -0
  31. lograder/output/formatters/format_templates.py +53 -0
  32. lograder/output/formatters/interfaces.py +67 -0
  33. lograder/output/raw_json/__init__.py +0 -0
  34. lograder/output/raw_json/assignment.py +44 -0
  35. lograder/output/raw_json/leaderboard.py +19 -0
  36. lograder/output/raw_json/test_case.py +26 -0
  37. lograder/static/__init__.py +7 -0
  38. lograder/static/basicconfig.py +18 -0
  39. lograder/static/messageconfig.py +15 -0
  40. lograder/tests/__init__.py +37 -0
  41. lograder/tests/_core_exceptions.py +20 -0
  42. lograder/tests/common/__init__.py +3 -0
  43. lograder/tests/common/exceptions.py +73 -0
  44. lograder/tests/common/validation.py +17 -0
  45. lograder/tests/exceptions.py +10 -0
  46. lograder/tests/file/__init__.py +3 -0
  47. lograder/tests/file/test_maker.py +48 -0
  48. lograder/tests/generator/__init__.py +17 -0
  49. lograder/tests/generator/test_maker.py +78 -0
  50. lograder/tests/generator/types.py +47 -0
  51. lograder/tests/registry/__init__.py +3 -0
  52. lograder/tests/registry/registry.py +44 -0
  53. lograder/tests/simple/__init__.py +3 -0
  54. lograder/tests/simple/test_maker.py +46 -0
  55. lograder/tests/template/__init__.py +9 -0
  56. lograder/tests/template/test_maker.py +110 -0
  57. lograder/tests/template/types.py +15 -0
  58. lograder/tests/test/__init__.py +8 -0
  59. lograder/tests/test/analytics.py +337 -0
  60. lograder/tests/test/comparison_test.py +176 -0
  61. lograder/tests/test/interface.py +87 -0
  62. lograder-0.0.2.dist-info/METADATA +342 -0
  63. lograder-0.0.2.dist-info/RECORD +66 -0
  64. lograder-0.0.2.dist-info/WHEEL +5 -0
  65. lograder-0.0.2.dist-info/licenses/LICENSE +7 -0
  66. lograder-0.0.2.dist-info/top_level.txt +1 -0
lograder/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ def hello_world() -> str:
2
+ return "Hello world! ~from `lograder`!"
@@ -0,0 +1,7 @@
1
+ class LograderError(Exception):
2
+ """
3
+ This is the base exception class for all exceptions raised
4
+ by the `lograder` module, for easy error handling.
5
+ """
6
+
7
+ pass
File without changes
@@ -0,0 +1,5 @@
1
+ from os import PathLike
2
+ from pathlib import Path
3
+ from typing import Union
4
+
5
+ FilePath = Union[str, PathLike[str], Path]
@@ -0,0 +1,6 @@
1
+ import random
2
+ import string
3
+
4
+
5
+ def random_name(length=25) -> str:
6
+ return "".join(random.choices(string.ascii_uppercase + string.digits, k=length))
@@ -0,0 +1,9 @@
1
+ from . import common, cpp, misc
2
+ from .common import * # noqa
3
+ from .cpp import * # noqa
4
+ from .misc import * # noqa
5
+
6
+ __all__ = []
7
+ for _mod in (common, cpp, misc):
8
+ if hasattr(_mod, "__all__"):
9
+ __all__.extend(_mod.__all__)
@@ -0,0 +1,10 @@
1
+ from .._core_exceptions import LograderError
2
+
3
+
4
+ class LograderStudentBuildError(LograderError):
5
+ """
6
+ This is the base exception class for all exceptions raised
7
+ by the `lograder.dispatch` module, for easy error handling.
8
+ """
9
+
10
+ pass
@@ -0,0 +1,30 @@
1
+ from .assignment import AssignmentSummary, BuilderOutput, PreprocessorOutput
2
+ from .interface import (
3
+ BuilderInterface,
4
+ DispatcherInterface,
5
+ ExecutableBuildResults,
6
+ PreprocessorInterface,
7
+ PreprocessorResults,
8
+ RunnerInterface,
9
+ RuntimePrepResults,
10
+ RuntimeResults,
11
+ )
12
+ from .templates import CLIBuilder, ExecutableRunner, TrivialBuilder, TrivialPreprocessor
13
+
14
+ __all__ = [
15
+ "TrivialBuilder",
16
+ "TrivialPreprocessor",
17
+ "AssignmentSummary",
18
+ "CLIBuilder",
19
+ "ExecutableRunner",
20
+ "BuilderInterface",
21
+ "PreprocessorInterface",
22
+ "RunnerInterface",
23
+ "DispatcherInterface",
24
+ "BuilderOutput",
25
+ "ExecutableBuildResults",
26
+ "PreprocessorResults",
27
+ "PreprocessorOutput",
28
+ "RuntimePrepResults",
29
+ "RuntimeResults",
30
+ ]
@@ -0,0 +1,99 @@
1
+ from typing import List, Optional
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from ...output.formatters.default import (
6
+ DefaultBuildOutputFormatter,
7
+ DefaultExecutableTestCaseFormatter,
8
+ DefaultMetadataFormatter,
9
+ DefaultPreprocessorOutputFormatter,
10
+ DefaultRuntimeSummaryFormatter,
11
+ )
12
+ from ...output.formatters.interfaces import (
13
+ BuildOutputFormatterInterface,
14
+ ExecutableTestFormatterInterface,
15
+ MetadataFormatterInterface,
16
+ PreprocessorOutputFormatterInterface,
17
+ RuntimeSummaryFormatterInterface,
18
+ )
19
+ from ...output.raw_json.assignment import AssignmentJSON
20
+ from ...output.raw_json.test_case import TestCaseJSON
21
+ from ...tests.test import ExecutableTestInterface
22
+ from .types import AssignmentMetadata, BuilderOutput, PreprocessorOutput
23
+
24
+
25
+ class AssignmentSummary(BaseModel):
26
+ model_config = {"arbitrary_types_allowed": True}
27
+
28
+ metadata: AssignmentMetadata
29
+ preprocessor_output: PreprocessorOutput
30
+ build_output: BuilderOutput
31
+ test_cases: List[ExecutableTestInterface]
32
+
33
+ metadata_fmt: MetadataFormatterInterface = Field(
34
+ default_factory=DefaultMetadataFormatter, exclude=True
35
+ )
36
+ preprocessor_output_fmt: PreprocessorOutputFormatterInterface = Field(
37
+ default_factory=DefaultPreprocessorOutputFormatter, exclude=True
38
+ )
39
+ build_output_fmt: BuildOutputFormatterInterface = Field(
40
+ default_factory=DefaultBuildOutputFormatter, exclude=True
41
+ )
42
+ runtime_summary_fmt: RuntimeSummaryFormatterInterface = Field(
43
+ default_factory=DefaultRuntimeSummaryFormatter, exclude=True
44
+ )
45
+ test_case_fmt: ExecutableTestFormatterInterface = Field(
46
+ default_factory=DefaultExecutableTestCaseFormatter, exclude=True
47
+ )
48
+
49
+ @classmethod
50
+ def set_formatters(
51
+ cls,
52
+ *,
53
+ metadata: Optional[MetadataFormatterInterface] = None,
54
+ preprocessor_output: Optional[PreprocessorOutputFormatterInterface] = None,
55
+ build_output: Optional[BuildOutputFormatterInterface] = None,
56
+ runtime_summary: Optional[RuntimeSummaryFormatterInterface] = None,
57
+ test_case: Optional[ExecutableTestFormatterInterface] = None,
58
+ ):
59
+ if metadata is not None:
60
+ cls.metadata_fmt = metadata
61
+ if preprocessor_output is not None:
62
+ cls.preprocessor_output_fmt = preprocessor_output
63
+ if build_output is not None:
64
+ cls.build_output_fmt = build_output
65
+ if runtime_summary is not None:
66
+ cls.runtime_summary_fmt = runtime_summary
67
+ if test_case is not None:
68
+ cls.test_case_fmt = test_case
69
+
70
+ def get_assignment_text(self):
71
+ return (
72
+ f"\n{self.metadata_fmt.format(self.metadata)}"
73
+ f"{self.preprocessor_output_fmt.format(self.preprocessor_output)}\n\n"
74
+ f"{self.build_output_fmt.format(self.build_output)}\n\n"
75
+ f"{self.runtime_summary_fmt.format(self.test_cases)}\n\n"
76
+ )
77
+
78
+ def get_score_multiplier(self):
79
+ total_score = sum([test_case.get_weight() for test_case in self.test_cases])
80
+ return 100.0 / total_score if total_score else 0.0
81
+
82
+ def get_raw(self) -> AssignmentJSON:
83
+ return AssignmentJSON(
84
+ output=self.get_assignment_text(),
85
+ visibility="visible",
86
+ tests=[
87
+ TestCaseJSON(
88
+ name=test_case.get_name(),
89
+ output=self.test_case_fmt.format(test_case),
90
+ score=self.get_score_multiplier()
91
+ * test_case.get_weight()
92
+ * test_case.get_successful()
93
+ * test_case.get_penalty(),
94
+ max_score=self.get_score_multiplier() * test_case.get_weight(),
95
+ )
96
+ for test_case in self.test_cases
97
+ ],
98
+ leaderboard=None, # TODO: Add leaderboard support.
99
+ )
@@ -0,0 +1,42 @@
1
+ from pathlib import Path
2
+ from typing import List
3
+
4
+ from .._core_exceptions import LograderStudentBuildError
5
+
6
+
7
+ class CxxSourceBuildError(LograderStudentBuildError):
8
+ def __init__(self, sources: List[Path]):
9
+ super().__init__(
10
+ f"Was unable to compile C++ sources; found source files: {', '.join([str(path.resolve()) for path in sources])}."
11
+ )
12
+
13
+
14
+ class CMakeListsNotFoundError(LograderStudentBuildError):
15
+ def __init__(self):
16
+ super().__init__("Could not find a `CMakeLists.txt` anywhere in the project.")
17
+
18
+
19
+ class CMakeTargetNotFoundError(LograderStudentBuildError):
20
+ def __init__(self, targets: List[str], cmake_path: Path):
21
+ super().__init__(
22
+ f"Could not find a valid cmake target anywhere in `{cmake_path.resolve()}`. The targets found were: [{', '.join(targets)}]."
23
+ )
24
+
25
+
26
+ class CMakeExecutableNotFoundError(LograderStudentBuildError):
27
+ def __init__(self, cmake_path: Path):
28
+ super().__init__(
29
+ f"Could not find a valid cmake executable output path anywhere in `{cmake_path.resolve()}`."
30
+ )
31
+
32
+
33
+ class MakefileNotFoundError(LograderStudentBuildError):
34
+ def __init__(self):
35
+ super().__init__("Could not find a `Makefile` anywhere in the project.")
36
+
37
+
38
+ class MakefileRunNotFoundError(LograderStudentBuildError):
39
+ def __init__(self, makefile_path: Path):
40
+ super().__init__(
41
+ f"Could not find an `run` entrypoint in `{makefile_path.resolve()}` (i.e. the command `make run ARGS=<test-args>` is used to run the project)."
42
+ )
@@ -0,0 +1,118 @@
1
+ import subprocess
2
+ import sys
3
+ from collections import deque
4
+ from pathlib import Path
5
+ from typing import List, Optional
6
+
7
+ from ...static.basicconfig import LograderBasicConfig
8
+
9
+
10
+ def bfs_walk(root: Path): # pathlib defaults to dfs; must implement bfs ourselves.
11
+ queue = deque([root])
12
+ while queue:
13
+ current = queue.popleft()
14
+ if current.is_dir():
15
+ for child in current.iterdir():
16
+ queue.append(child)
17
+ else:
18
+ yield current
19
+
20
+
21
+ def is_cxx_source_file(path: Path) -> bool:
22
+ return path.exists() and path.suffix in (
23
+ ".cc",
24
+ ".cp",
25
+ ".cxx",
26
+ ".cpp",
27
+ ".CPP",
28
+ ".c++",
29
+ ".C",
30
+ ".c",
31
+ )
32
+
33
+
34
+ def is_cmake_file(path: Path) -> bool:
35
+ return path.exists() and path.name.startswith("CMakeLists.txt")
36
+
37
+
38
+ def is_makefile_file(path: Path) -> bool:
39
+ return path.exists() and path.name == "Makefile"
40
+
41
+
42
+ def is_makefile_target(makefile: Path, target: str) -> bool:
43
+ if not is_makefile_file(makefile):
44
+ return False
45
+ proc = subprocess.run(
46
+ ["make", "-qp"], cwd=makefile.parent, capture_output=True, text=True
47
+ )
48
+ for line in proc.stdout.splitlines():
49
+ if line.strip().startswith(f"{target}:"):
50
+ return True
51
+ return False
52
+
53
+
54
+ def is_valid_target(target: str) -> bool:
55
+ if target in (
56
+ "all",
57
+ "install",
58
+ "depend",
59
+ "test",
60
+ "package",
61
+ "package_source",
62
+ "edit_cache",
63
+ "rebuild_cache",
64
+ "clean",
65
+ "help",
66
+ "ALL_BUILD",
67
+ "ZERO_CHECK",
68
+ "INSTALL",
69
+ "RUN_TESTS",
70
+ "PACKAGE",
71
+ ):
72
+ return False
73
+ if target.endswith(".obj") or target.endswith(".i") or target.endswith(".s"):
74
+ return False
75
+ return True
76
+
77
+
78
+ def do_process(args: List[str | Path], **kwargs) -> subprocess.CompletedProcess:
79
+ win_prefix: List[str | Path] = ["cmd", "/c"]
80
+ cmd: List[str | Path] = args
81
+ if sys.platform.startswith("win"):
82
+ cmd = win_prefix + cmd
83
+ return subprocess.run(cmd, **kwargs)
84
+
85
+
86
+ def run_cmd(
87
+ cmd: List[str | Path],
88
+ commands: Optional[List[List[str | Path]]] = None,
89
+ stdout: Optional[List[str]] = None,
90
+ stderr: Optional[List[str]] = None,
91
+ working_directory: Optional[Path] = None,
92
+ ):
93
+
94
+ if working_directory is None:
95
+ result = do_process(
96
+ cmd,
97
+ stdout=subprocess.PIPE,
98
+ stderr=subprocess.PIPE,
99
+ text=True,
100
+ timeout=LograderBasicConfig.DEFAULT_EXECUTABLE_TIMEOUT,
101
+ )
102
+ else:
103
+ result = do_process(
104
+ cmd,
105
+ stdout=subprocess.PIPE,
106
+ stderr=subprocess.PIPE,
107
+ text=True,
108
+ timeout=LograderBasicConfig.DEFAULT_EXECUTABLE_TIMEOUT,
109
+ cwd=working_directory,
110
+ )
111
+
112
+ if commands is not None:
113
+ commands.append(cmd)
114
+ if stdout is not None:
115
+ stdout.append(result.stdout)
116
+ if stderr is not None:
117
+ stderr.append(result.stderr)
118
+ return result
@@ -0,0 +1,227 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from abc import ABC, abstractmethod
5
+ from pathlib import Path
6
+ from typing import List, Optional, Sequence
7
+
8
+ from ...common.types import FilePath
9
+ from ...static import LograderBasicConfig, LograderMessageConfig
10
+ from ...tests.common.exceptions import TestNotRunError
11
+ from ...tests.test import ExecutableTestInterface
12
+ from ..common.assignment import AssignmentSummary, BuilderOutput, PreprocessorOutput
13
+ from .types import AssignmentMetadata
14
+
15
+
16
+ class PreprocessorResults:
17
+ def __init__(self, output: PreprocessorOutput):
18
+ # The reason why this is a class is in case we want to "expand" later.
19
+ self._output = output
20
+
21
+ def get_output(self) -> PreprocessorOutput:
22
+ return self._output
23
+
24
+
25
+ class BuildResults:
26
+ def __init__(self, output: BuilderOutput):
27
+ self._output = output
28
+
29
+ def get_output(self) -> BuilderOutput:
30
+ return self._output
31
+
32
+
33
+ class ExecutableBuildResults(BuildResults):
34
+ def __init__(self, executable: FilePath, output: BuilderOutput):
35
+ super().__init__(output)
36
+ self._executable = Path(executable)
37
+
38
+ def get_executable(self) -> Path:
39
+ return self._executable
40
+
41
+
42
+ class RuntimePrepResults: # dummy class to allow distinguishing between pre- and post-run.
43
+ def __init__(
44
+ self,
45
+ results: (
46
+ RuntimePrepResults | RuntimeResults | Sequence[ExecutableTestInterface]
47
+ ),
48
+ ):
49
+ if isinstance(results, RuntimePrepResults):
50
+ self._results = results.get_test_cases()
51
+ else:
52
+ self._results = list(results)
53
+
54
+ def get_test_cases(self) -> List[ExecutableTestInterface]:
55
+ return self._results
56
+
57
+
58
+ class RuntimeResults(RuntimePrepResults):
59
+ def get_test_cases(self) -> List[ExecutableTestInterface]:
60
+ for result in self._results:
61
+ if not result.is_executed():
62
+ raise TestNotRunError(result.get_name())
63
+ return self._results
64
+
65
+
66
+ class ProcessInterface(ABC):
67
+ _linked_preprocessors: set[PreprocessorInterface] = set()
68
+ _linked_builders: set[BuilderInterface] = set()
69
+ _linked_runners: set[RunnerInterface] = set()
70
+
71
+ @classmethod
72
+ def register_builder(cls, builder: BuilderInterface):
73
+ cls._linked_builders.add(builder)
74
+
75
+ @classmethod
76
+ def register_preprocessor(cls, preprocessor: PreprocessorInterface):
77
+ cls._linked_preprocessors.add(preprocessor)
78
+
79
+ @classmethod
80
+ def register_runner(cls, runner: RunnerInterface):
81
+ cls._linked_runners.add(runner)
82
+
83
+ @classmethod
84
+ def is_build_successful(cls) -> bool:
85
+ for builder in cls._linked_builders:
86
+ if builder.is_build_error():
87
+ return False
88
+ return True
89
+
90
+ @classmethod
91
+ def is_preprocessor_successful(cls) -> bool:
92
+ return cls.get_validation_multiplier() == 1.0
93
+
94
+ @classmethod
95
+ def get_validation_multiplier(cls) -> float:
96
+ scores = [prep.get_validation_penalty() for prep in cls._linked_preprocessors]
97
+ score = 1.0
98
+ for score in scores:
99
+ score *= score
100
+ return score
101
+
102
+ def __eq__(self, other):
103
+ return id(self) == id(
104
+ other
105
+ ) # default "is" comparison, but it's here in case it changes.
106
+
107
+ def __hash__(self):
108
+ return hash(id(self))
109
+
110
+
111
+ class PreprocessorInterface(ProcessInterface, ABC):
112
+ def __init__(self):
113
+ super().__init__()
114
+ ProcessInterface.register_preprocessor(self)
115
+ self._validation_penalty: float = 0.0
116
+
117
+ def set_validation_penalty(self, penalty: float):
118
+ self._validation_penalty = penalty
119
+
120
+ def get_validation_penalty(self) -> float:
121
+ return self._validation_penalty
122
+
123
+ def get_individual_validation_multiplier(self) -> float:
124
+ return 1.0 - min(max(self.get_validation_penalty(), 0.0), 1.0)
125
+
126
+ def check(self):
127
+ if self.validate():
128
+ return
129
+ self.set_validation_penalty(1.0)
130
+
131
+ @abstractmethod
132
+ def validate(self) -> bool:
133
+ pass
134
+
135
+ @abstractmethod
136
+ def preprocess(self) -> PreprocessorResults:
137
+ pass
138
+
139
+
140
+ class BuilderInterface(ProcessInterface, ABC):
141
+ def __init__(self):
142
+ super().__init__()
143
+ ProcessInterface.register_builder(self)
144
+ self._build_error: bool = False
145
+
146
+ def set_build_error(self, build_error: bool):
147
+ self._build_error = build_error
148
+
149
+ def is_build_error(self) -> bool:
150
+ return self._build_error
151
+
152
+ @abstractmethod
153
+ def build(self) -> BuildResults:
154
+ pass
155
+
156
+
157
+ class ExecutableBuilderInterface(BuilderInterface, ABC):
158
+ @abstractmethod
159
+ def build(self) -> ExecutableBuildResults:
160
+ pass
161
+
162
+
163
+ class RunnerInterface(ProcessInterface, ABC):
164
+ def __init__(self):
165
+ super().__init__()
166
+ ProcessInterface.register_runner(self)
167
+ self._wrap_args: bool = False
168
+ self._cwd: Optional[Path] = None
169
+
170
+ def set_cwd(self, cwd: Path):
171
+ self._cwd = cwd
172
+
173
+ def set_wrap_args(self, wrap_args: bool = True):
174
+ self._wrap_args = wrap_args
175
+
176
+ def run_tests_auto(self) -> RuntimeResults:
177
+ results: RuntimePrepResults = self.prep_tests()
178
+ for test_case in results.get_test_cases():
179
+ if not self.is_build_successful():
180
+ test_case.force_unsuccessful()
181
+ test_case.override_output(
182
+ LograderMessageConfig.DEFAULT_BUILD_ERROR_OVERRIDE_MESSAGE,
183
+ LograderMessageConfig.DEFAULT_BUILD_ERROR_OVERRIDE_MESSAGE,
184
+ )
185
+ else:
186
+ test_case.run(wrap_args=self._wrap_args, working_directory=self._cwd)
187
+ return RuntimeResults(results)
188
+
189
+ @abstractmethod
190
+ def prep_tests(self) -> RuntimePrepResults:
191
+ pass
192
+
193
+
194
+ class DispatcherInterface(ABC):
195
+ def run(
196
+ self, out_path: Path = LograderBasicConfig.DEFAULT_RESULT_PATH
197
+ ) -> AssignmentSummary:
198
+ metadata = self.metadata()
199
+ prep = self.preprocess()
200
+ build = self.build()
201
+ runtime_results = self.run_tests()
202
+
203
+ summary = AssignmentSummary(
204
+ metadata=metadata,
205
+ preprocessor_output=prep.get_output(),
206
+ build_output=build.get_output(),
207
+ test_cases=runtime_results.get_test_cases(),
208
+ )
209
+ out_path.parent.mkdir(parents=True, exist_ok=True)
210
+ out_path.write_text(json.dumps(summary.model_dump()))
211
+ return summary
212
+
213
+ @abstractmethod
214
+ def metadata(self) -> AssignmentMetadata:
215
+ pass
216
+
217
+ @abstractmethod
218
+ def preprocess(self) -> PreprocessorResults:
219
+ pass
220
+
221
+ @abstractmethod
222
+ def build(self) -> ExecutableBuildResults:
223
+ pass
224
+
225
+ @abstractmethod
226
+ def run_tests(self) -> RuntimeResults:
227
+ pass
@@ -0,0 +1,10 @@
1
+ from .cli_builder import CLIBuilder
2
+ from .executable_runner import ExecutableRunner
3
+ from .trivial import TrivialBuilder, TrivialPreprocessor
4
+
5
+ __all__ = [
6
+ "ExecutableRunner",
7
+ "CLIBuilder",
8
+ "TrivialPreprocessor",
9
+ "TrivialBuilder",
10
+ ]
@@ -0,0 +1,59 @@
1
+ from abc import ABC
2
+ from pathlib import Path
3
+ from typing import List, Optional
4
+
5
+ from ....static import LograderMessageConfig
6
+ from ..file_operations import run_cmd
7
+ from ..interface import BuilderInterface, BuilderOutput, ExecutableBuildResults
8
+ from ..types import ProjectType
9
+
10
+
11
+ class CLIBuilder(BuilderInterface, ABC):
12
+ def __init__(self, build_type: ProjectType):
13
+ super().__init__()
14
+ self._commands: List[List[str | Path]] = []
15
+ self._stdout: List[str] = []
16
+ self._stderr: List[str] = []
17
+ self._build_type = build_type
18
+
19
+ def get_commands(self) -> List[List[str | Path]]:
20
+ return self._commands
21
+
22
+ def get_build_type(self) -> ProjectType:
23
+ return self._build_type
24
+
25
+ def get_stdout(self) -> List[str]:
26
+ return self._stdout
27
+
28
+ def get_stderr(self) -> List[str]:
29
+ return self._stderr
30
+
31
+ def get_build_error_output(self) -> ExecutableBuildResults:
32
+ return ExecutableBuildResults(
33
+ executable=LograderMessageConfig.DEFAULT_BUILD_ERROR_EXECUTABLE_NAME,
34
+ output=BuilderOutput(
35
+ commands=self.get_commands(),
36
+ stdout=self.get_stdout(),
37
+ stderr=self.get_stderr(),
38
+ project_type=self.get_build_type(),
39
+ ),
40
+ )
41
+
42
+ def run_cmd(
43
+ self, cmd: List[str | Path], working_directory: Optional[Path] = None
44
+ ) -> BuilderOutput:
45
+ result = run_cmd(
46
+ cmd,
47
+ commands=self._commands,
48
+ stdout=self._stdout,
49
+ stderr=self._stderr,
50
+ working_directory=working_directory,
51
+ )
52
+ if result.returncode != 0:
53
+ self.set_build_error(True)
54
+ return BuilderOutput(
55
+ commands=self._commands,
56
+ stdout=self._stdout,
57
+ stderr=self._stderr,
58
+ project_type=self._build_type,
59
+ )
@@ -0,0 +1,23 @@
1
+ from abc import ABC, abstractmethod
2
+ from pathlib import Path
3
+ from typing import List
4
+
5
+ from ....tests.registry import TestRegistry
6
+ from ....tests.test.interface import ExecutableTestInterface
7
+ from ..interface import RunnerInterface, RuntimePrepResults
8
+
9
+
10
+ class ExecutableRunner(RunnerInterface, ABC):
11
+ @abstractmethod
12
+ def get_executable(self) -> List[str | Path]:
13
+ pass
14
+
15
+ def prep_tests(self) -> RuntimePrepResults:
16
+ tests: List[ExecutableTestInterface] = []
17
+ for test in TestRegistry.iterate():
18
+ if isinstance(test, ExecutableTestInterface):
19
+ test.set_target(self.get_executable())
20
+ tests.append(test)
21
+ else:
22
+ raise TypeError
23
+ return RuntimePrepResults(tests)