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.
- lograder/__init__.py +2 -0
- lograder/_core_exceptions.py +7 -0
- lograder/common/__init__.py +0 -0
- lograder/common/types.py +5 -0
- lograder/common/utils.py +6 -0
- lograder/dispatch/__init__.py +9 -0
- lograder/dispatch/_core_exceptions.py +10 -0
- lograder/dispatch/common/__init__.py +30 -0
- lograder/dispatch/common/assignment.py +99 -0
- lograder/dispatch/common/exceptions.py +42 -0
- lograder/dispatch/common/file_operations.py +118 -0
- lograder/dispatch/common/interface.py +227 -0
- lograder/dispatch/common/templates/__init__.py +10 -0
- lograder/dispatch/common/templates/cli_builder.py +59 -0
- lograder/dispatch/common/templates/executable_runner.py +23 -0
- lograder/dispatch/common/templates/trivial.py +34 -0
- lograder/dispatch/common/types.py +55 -0
- lograder/dispatch/cpp/__init__.py +7 -0
- lograder/dispatch/cpp/cmake.py +172 -0
- lograder/dispatch/cpp/cpp_source.py +101 -0
- lograder/dispatch/exceptions.py +19 -0
- lograder/dispatch/misc/__init__.py +4 -0
- lograder/dispatch/misc/dispatcher.py +89 -0
- lograder/dispatch/misc/makefile.py +89 -0
- lograder/exceptions.py +15 -0
- lograder/output/__init__.py +0 -0
- lograder/output/common/__init__.py +3 -0
- lograder/output/common/types.py +6 -0
- lograder/output/formatters/__init__.py +0 -0
- lograder/output/formatters/default.py +359 -0
- lograder/output/formatters/format_templates.py +53 -0
- lograder/output/formatters/interfaces.py +67 -0
- lograder/output/raw_json/__init__.py +0 -0
- lograder/output/raw_json/assignment.py +44 -0
- lograder/output/raw_json/leaderboard.py +19 -0
- lograder/output/raw_json/test_case.py +26 -0
- lograder/static/__init__.py +7 -0
- lograder/static/basicconfig.py +18 -0
- lograder/static/messageconfig.py +15 -0
- lograder/tests/__init__.py +37 -0
- lograder/tests/_core_exceptions.py +20 -0
- lograder/tests/common/__init__.py +3 -0
- lograder/tests/common/exceptions.py +73 -0
- lograder/tests/common/validation.py +17 -0
- lograder/tests/exceptions.py +10 -0
- lograder/tests/file/__init__.py +3 -0
- lograder/tests/file/test_maker.py +48 -0
- lograder/tests/generator/__init__.py +17 -0
- lograder/tests/generator/test_maker.py +78 -0
- lograder/tests/generator/types.py +47 -0
- lograder/tests/registry/__init__.py +3 -0
- lograder/tests/registry/registry.py +44 -0
- lograder/tests/simple/__init__.py +3 -0
- lograder/tests/simple/test_maker.py +46 -0
- lograder/tests/template/__init__.py +9 -0
- lograder/tests/template/test_maker.py +110 -0
- lograder/tests/template/types.py +15 -0
- lograder/tests/test/__init__.py +8 -0
- lograder/tests/test/analytics.py +337 -0
- lograder/tests/test/comparison_test.py +176 -0
- lograder/tests/test/interface.py +87 -0
- lograder-0.0.2.dist-info/METADATA +342 -0
- lograder-0.0.2.dist-info/RECORD +66 -0
- lograder-0.0.2.dist-info/WHEEL +5 -0
- lograder-0.0.2.dist-info/licenses/LICENSE +7 -0
- lograder-0.0.2.dist-info/top_level.txt +1 -0
lograder/__init__.py
ADDED
|
File without changes
|
lograder/common/types.py
ADDED
lograder/common/utils.py
ADDED
|
@@ -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,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)
|