128Autograder 5.0.1__tar.gz
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.
- 128autograder-5.0.1/PKG-INFO +15 -0
- 128autograder-5.0.1/pyproject.toml +97 -0
- 128autograder-5.0.1/setup.cfg +4 -0
- 128autograder-5.0.1/source/128Autograder.egg-info/PKG-INFO +15 -0
- 128autograder-5.0.1/source/128Autograder.egg-info/SOURCES.txt +47 -0
- 128autograder-5.0.1/source/128Autograder.egg-info/dependency_links.txt +1 -0
- 128autograder-5.0.1/source/128Autograder.egg-info/entry_points.txt +7 -0
- 128autograder-5.0.1/source/128Autograder.egg-info/requires.txt +10 -0
- 128autograder-5.0.1/source/128Autograder.egg-info/top_level.txt +2 -0
- 128autograder-5.0.1/source/autograder_cli/__init__.py +0 -0
- 128autograder-5.0.1/source/autograder_cli/build_autograder.py +314 -0
- 128autograder-5.0.1/source/autograder_cli/create_upload.py +103 -0
- 128autograder-5.0.1/source/autograder_cli/run_gradescope.py +108 -0
- 128autograder-5.0.1/source/autograder_cli/run_local.py +299 -0
- 128autograder-5.0.1/source/autograder_cli/run_prairielearn.py +59 -0
- 128autograder-5.0.1/source/autograder_platform/Executors/Environment.py +273 -0
- 128autograder-5.0.1/source/autograder_platform/Executors/Executor.py +64 -0
- 128autograder-5.0.1/source/autograder_platform/Executors/__init__.py +0 -0
- 128autograder-5.0.1/source/autograder_platform/Executors/common.py +55 -0
- 128autograder-5.0.1/source/autograder_platform/StudentSubmission/AbstractStudentSubmission.py +178 -0
- 128autograder-5.0.1/source/autograder_platform/StudentSubmission/AbstractValidator.py +28 -0
- 128autograder-5.0.1/source/autograder_platform/StudentSubmission/GenericValidators.py +43 -0
- 128autograder-5.0.1/source/autograder_platform/StudentSubmission/ISubmissionProcess.py +27 -0
- 128autograder-5.0.1/source/autograder_platform/StudentSubmission/SubmissionProcessFactory.py +66 -0
- 128autograder-5.0.1/source/autograder_platform/StudentSubmission/__init__.py +0 -0
- 128autograder-5.0.1/source/autograder_platform/StudentSubmission/common.py +60 -0
- 128autograder-5.0.1/source/autograder_platform/StudentSubmissionImpl/Python/AbstractPythonImportFactory.py +11 -0
- 128autograder-5.0.1/source/autograder_platform/StudentSubmissionImpl/Python/PythonEnvironment.py +122 -0
- 128autograder-5.0.1/source/autograder_platform/StudentSubmissionImpl/Python/PythonFileImportFactory.py +62 -0
- 128autograder-5.0.1/source/autograder_platform/StudentSubmissionImpl/Python/PythonModuleMockImportFactory.py +37 -0
- 128autograder-5.0.1/source/autograder_platform/StudentSubmissionImpl/Python/PythonSubmission.py +204 -0
- 128autograder-5.0.1/source/autograder_platform/StudentSubmissionImpl/Python/PythonSubmissionProcess.py +358 -0
- 128autograder-5.0.1/source/autograder_platform/StudentSubmissionImpl/Python/PythonValidators.py +130 -0
- 128autograder-5.0.1/source/autograder_platform/StudentSubmissionImpl/Python/Runners.py +278 -0
- 128autograder-5.0.1/source/autograder_platform/StudentSubmissionImpl/Python/__init__.py +7 -0
- 128autograder-5.0.1/source/autograder_platform/StudentSubmissionImpl/Python/common.py +46 -0
- 128autograder-5.0.1/source/autograder_platform/StudentSubmissionImpl/__init__.py +0 -0
- 128autograder-5.0.1/source/autograder_platform/Tasks/Task.py +51 -0
- 128autograder-5.0.1/source/autograder_platform/Tasks/TaskRunner.py +75 -0
- 128autograder-5.0.1/source/autograder_platform/Tasks/__init__.py +0 -0
- 128autograder-5.0.1/source/autograder_platform/Tasks/common.py +26 -0
- 128autograder-5.0.1/source/autograder_platform/TestingFramework/Assertions.py +177 -0
- 128autograder-5.0.1/source/autograder_platform/TestingFramework/SingleFunctionMock.py +69 -0
- 128autograder-5.0.1/source/autograder_platform/TestingFramework/__init__.py +0 -0
- 128autograder-5.0.1/source/autograder_platform/__init__.py +1 -0
- 128autograder-5.0.1/source/autograder_platform/cli.py +85 -0
- 128autograder-5.0.1/source/autograder_platform/config/Config.py +365 -0
- 128autograder-5.0.1/source/autograder_platform/config/__init__.py +0 -0
- 128autograder-5.0.1/source/autograder_platform/config/common.py +22 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: 128Autograder
|
|
3
|
+
Version: 5.0.1
|
|
4
|
+
Author: Gregory Bell
|
|
5
|
+
Maintainer: Gregory Bell
|
|
6
|
+
Requires-Python: >=3.11.0
|
|
7
|
+
Requires-Dist: HybridJSONTestRunner==0.8.1
|
|
8
|
+
Requires-Dist: dill==0.3.6
|
|
9
|
+
Requires-Dist: Better-PyUnit-Format==0.2.3
|
|
10
|
+
Requires-Dist: schema==0.7.5
|
|
11
|
+
Requires-Dist: requests==2.31.0
|
|
12
|
+
Requires-Dist: tomli==2.0.1
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: coverage[toml]; extra == "dev"
|
|
15
|
+
Requires-Dist: build; extra == "dev"
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools >= 61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[tool.setuptools.packages.find]
|
|
6
|
+
where=["source"]
|
|
7
|
+
exclude =["tests"]
|
|
8
|
+
|
|
9
|
+
[tool.setuptools.dynamic]
|
|
10
|
+
version = {attr = "autograder_platform.__version__"}
|
|
11
|
+
|
|
12
|
+
[project]
|
|
13
|
+
name = "128Autograder"
|
|
14
|
+
authors = [
|
|
15
|
+
{ name = "Gregory Bell" }
|
|
16
|
+
]
|
|
17
|
+
maintainers = [
|
|
18
|
+
{ name = "Gregory Bell" }
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
dynamic = ["version"]
|
|
22
|
+
|
|
23
|
+
requires-python = ">=3.11.0"
|
|
24
|
+
|
|
25
|
+
dependencies = [
|
|
26
|
+
"HybridJSONTestRunner==0.8.1",
|
|
27
|
+
"dill==0.3.6",
|
|
28
|
+
"Better-PyUnit-Format==0.2.3",
|
|
29
|
+
"schema==0.7.5",
|
|
30
|
+
"requests==2.31.0",
|
|
31
|
+
"tomli==2.0.1",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
dev = [
|
|
36
|
+
"coverage[toml]",
|
|
37
|
+
"build",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.scripts]
|
|
41
|
+
run_gradescope = "autograder_cli.run_gradescope:tool"
|
|
42
|
+
run_prairielearn = "autograder_cli.run_prairielearn:tool"
|
|
43
|
+
test_my_work = "autograder_cli.run_local:tool"
|
|
44
|
+
run_autograder = "autograder_cli.run_local:tool"
|
|
45
|
+
create_gradescope_upload = "autograder_cli.create_upload:tool"
|
|
46
|
+
build_autograder = "autograder_cli.build_autograder:tool"
|
|
47
|
+
|
|
48
|
+
[tool.pyright]
|
|
49
|
+
include = ["source"]
|
|
50
|
+
|
|
51
|
+
exclude = ["**__pycache__**"]
|
|
52
|
+
scrict = ["."]
|
|
53
|
+
|
|
54
|
+
executionEnvironments = [
|
|
55
|
+
{ root = "tests", extraPaths = ["source", "source/utils/student", "source/utils"] }
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
[tool.coverage.run]
|
|
59
|
+
command_line = "-m unittest"
|
|
60
|
+
concurrency = ["multiprocessing"]
|
|
61
|
+
# Omit testing files, omit dynamic files, omit __init__.py
|
|
62
|
+
# Doing it at the run level as it will that means they wont be run in the first place
|
|
63
|
+
# Ignore build.py as its it is primarly covered by integration tests
|
|
64
|
+
omit = [
|
|
65
|
+
# TODO write unit tests for the CLI
|
|
66
|
+
# shouldnt be ignoring them, but also this needs to get out the door
|
|
67
|
+
# Technically the CLIs are covered via the e2e tests, and we dont seem to have a hole there anymore
|
|
68
|
+
"source/autograder_cli/*",
|
|
69
|
+
"sandbox/*",
|
|
70
|
+
"testPrograms/*",
|
|
71
|
+
"test_code",
|
|
72
|
+
"test_code.py",
|
|
73
|
+
"setup_code",
|
|
74
|
+
"tests/*",
|
|
75
|
+
"*/student_submission",
|
|
76
|
+
"__init__.py",
|
|
77
|
+
"Build.py",
|
|
78
|
+
"INJECTED_*",
|
|
79
|
+
"__getstate__*",
|
|
80
|
+
"__setstate__*",
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
[tool.coverage.report]
|
|
84
|
+
fail_under = 90
|
|
85
|
+
skip_empty = true
|
|
86
|
+
exclude_also = [
|
|
87
|
+
# Dont flag error conditions that cant be reached or that are just defensive
|
|
88
|
+
"raise AssertionError",
|
|
89
|
+
"raise NotImplementedError",
|
|
90
|
+
"raise EnvironmentError",
|
|
91
|
+
"raise AttributeError",
|
|
92
|
+
"raise InvalidRunner",
|
|
93
|
+
"if __name__ == .__main__.:",
|
|
94
|
+
# Don't complain about abstract methods, they aren't run:
|
|
95
|
+
"@(abc\\.)?abstractmethod",
|
|
96
|
+
]
|
|
97
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: 128Autograder
|
|
3
|
+
Version: 5.0.1
|
|
4
|
+
Author: Gregory Bell
|
|
5
|
+
Maintainer: Gregory Bell
|
|
6
|
+
Requires-Python: >=3.11.0
|
|
7
|
+
Requires-Dist: HybridJSONTestRunner==0.8.1
|
|
8
|
+
Requires-Dist: dill==0.3.6
|
|
9
|
+
Requires-Dist: Better-PyUnit-Format==0.2.3
|
|
10
|
+
Requires-Dist: schema==0.7.5
|
|
11
|
+
Requires-Dist: requests==2.31.0
|
|
12
|
+
Requires-Dist: tomli==2.0.1
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: coverage[toml]; extra == "dev"
|
|
15
|
+
Requires-Dist: build; extra == "dev"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
source/128Autograder.egg-info/PKG-INFO
|
|
3
|
+
source/128Autograder.egg-info/SOURCES.txt
|
|
4
|
+
source/128Autograder.egg-info/dependency_links.txt
|
|
5
|
+
source/128Autograder.egg-info/entry_points.txt
|
|
6
|
+
source/128Autograder.egg-info/requires.txt
|
|
7
|
+
source/128Autograder.egg-info/top_level.txt
|
|
8
|
+
source/autograder_cli/__init__.py
|
|
9
|
+
source/autograder_cli/build_autograder.py
|
|
10
|
+
source/autograder_cli/create_upload.py
|
|
11
|
+
source/autograder_cli/run_gradescope.py
|
|
12
|
+
source/autograder_cli/run_local.py
|
|
13
|
+
source/autograder_cli/run_prairielearn.py
|
|
14
|
+
source/autograder_platform/__init__.py
|
|
15
|
+
source/autograder_platform/cli.py
|
|
16
|
+
source/autograder_platform/Executors/Environment.py
|
|
17
|
+
source/autograder_platform/Executors/Executor.py
|
|
18
|
+
source/autograder_platform/Executors/__init__.py
|
|
19
|
+
source/autograder_platform/Executors/common.py
|
|
20
|
+
source/autograder_platform/StudentSubmission/AbstractStudentSubmission.py
|
|
21
|
+
source/autograder_platform/StudentSubmission/AbstractValidator.py
|
|
22
|
+
source/autograder_platform/StudentSubmission/GenericValidators.py
|
|
23
|
+
source/autograder_platform/StudentSubmission/ISubmissionProcess.py
|
|
24
|
+
source/autograder_platform/StudentSubmission/SubmissionProcessFactory.py
|
|
25
|
+
source/autograder_platform/StudentSubmission/__init__.py
|
|
26
|
+
source/autograder_platform/StudentSubmission/common.py
|
|
27
|
+
source/autograder_platform/StudentSubmissionImpl/__init__.py
|
|
28
|
+
source/autograder_platform/StudentSubmissionImpl/Python/AbstractPythonImportFactory.py
|
|
29
|
+
source/autograder_platform/StudentSubmissionImpl/Python/PythonEnvironment.py
|
|
30
|
+
source/autograder_platform/StudentSubmissionImpl/Python/PythonFileImportFactory.py
|
|
31
|
+
source/autograder_platform/StudentSubmissionImpl/Python/PythonModuleMockImportFactory.py
|
|
32
|
+
source/autograder_platform/StudentSubmissionImpl/Python/PythonSubmission.py
|
|
33
|
+
source/autograder_platform/StudentSubmissionImpl/Python/PythonSubmissionProcess.py
|
|
34
|
+
source/autograder_platform/StudentSubmissionImpl/Python/PythonValidators.py
|
|
35
|
+
source/autograder_platform/StudentSubmissionImpl/Python/Runners.py
|
|
36
|
+
source/autograder_platform/StudentSubmissionImpl/Python/__init__.py
|
|
37
|
+
source/autograder_platform/StudentSubmissionImpl/Python/common.py
|
|
38
|
+
source/autograder_platform/Tasks/Task.py
|
|
39
|
+
source/autograder_platform/Tasks/TaskRunner.py
|
|
40
|
+
source/autograder_platform/Tasks/__init__.py
|
|
41
|
+
source/autograder_platform/Tasks/common.py
|
|
42
|
+
source/autograder_platform/TestingFramework/Assertions.py
|
|
43
|
+
source/autograder_platform/TestingFramework/SingleFunctionMock.py
|
|
44
|
+
source/autograder_platform/TestingFramework/__init__.py
|
|
45
|
+
source/autograder_platform/config/Config.py
|
|
46
|
+
source/autograder_platform/config/__init__.py
|
|
47
|
+
source/autograder_platform/config/common.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
[console_scripts]
|
|
2
|
+
build_autograder = autograder_cli.build_autograder:tool
|
|
3
|
+
create_gradescope_upload = autograder_cli.create_upload:tool
|
|
4
|
+
run_autograder = autograder_cli.run_local:tool
|
|
5
|
+
run_gradescope = autograder_cli.run_gradescope:tool
|
|
6
|
+
run_prairielearn = autograder_cli.run_prairielearn:tool
|
|
7
|
+
test_my_work = autograder_cli.run_local:tool
|
|
File without changes
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import shutil
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import List, Dict, Callable
|
|
6
|
+
|
|
7
|
+
from autograder_platform.cli import AutograderCLITool
|
|
8
|
+
from autograder_platform.config.Config import AutograderConfigurationBuilder, AutograderConfiguration
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FilesEnum(Enum):
|
|
12
|
+
PUBLIC_TEST = 0
|
|
13
|
+
PRIVATE_TEST = 1
|
|
14
|
+
PUBLIC_DATA = 2
|
|
15
|
+
PRIVATE_DATA = 3
|
|
16
|
+
STARTER_CODE = 4
|
|
17
|
+
CONFIG_FILE = 5
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Build:
|
|
21
|
+
IGNORE = ["__pycache__"]
|
|
22
|
+
|
|
23
|
+
def __init__(self, config: AutograderConfiguration, sourceRoot, binRoot, version) -> None:
|
|
24
|
+
self.config = config
|
|
25
|
+
self.generationDirectory = os.path.join(binRoot, "generation")
|
|
26
|
+
self.distDirectory = os.path.join(binRoot, "dist")
|
|
27
|
+
self.binRoot = binRoot
|
|
28
|
+
self.sourceDir = sourceRoot
|
|
29
|
+
self.version = version
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def _discoverTestFiles(allowPrivate: bool,
|
|
33
|
+
privateTestFileRegex: re.Pattern, publicTestFileRegex: re.Pattern,
|
|
34
|
+
testDirectory: str,
|
|
35
|
+
discoveredPrivateFiles: List[str], discoveredPublicFiles: List[str]):
|
|
36
|
+
"""
|
|
37
|
+
Description
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
This function discovers the test files in the testDirectory and puts them in the correct list
|
|
41
|
+
based on the regex matches.
|
|
42
|
+
|
|
43
|
+
Note: If allowPrivate is false, then all files that match the private regex will be added to the public list
|
|
44
|
+
|
|
45
|
+
:param allowPrivate: If private files should be treated as private
|
|
46
|
+
:param privateTestFileRegex: The regex pattern used to match private test files
|
|
47
|
+
:param publicTestFileRegex: The regex pattern used to match public test files
|
|
48
|
+
:param discoveredPrivateFiles: The list that contains the private files to be copied
|
|
49
|
+
:param discoveredPublicFiles: The list that contains the public files to be copied
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
# test discovery is non recursive for now
|
|
53
|
+
test_files = [file for file in os.listdir(testDirectory) if os.path.isfile(os.path.join(testDirectory, file))]
|
|
54
|
+
|
|
55
|
+
for file in test_files:
|
|
56
|
+
path = os.path.join(testDirectory, file)
|
|
57
|
+
# Dont need to worry about double checking, the private test will only be run once in both of these cases
|
|
58
|
+
if allowPrivate and privateTestFileRegex.match(file):
|
|
59
|
+
discoveredPrivateFiles.append(path)
|
|
60
|
+
elif publicTestFileRegex.match(file) or privateTestFileRegex.match(file):
|
|
61
|
+
discoveredPublicFiles.append(path)
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def _discoverDataFiles(allowPrivate: bool, dataFilesSource: str,
|
|
65
|
+
discoveredPrivateFiles: List[str], discoveredPublicFiles: List[str]):
|
|
66
|
+
"""
|
|
67
|
+
Description
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
This function recursively discovers the data files in the dataFilesSource.
|
|
71
|
+
As opposed to the test file function, this will mark files as private if they contain 'private' anywhere in the path.
|
|
72
|
+
|
|
73
|
+
Note: if allowPrivate is false, then all files that would otherwise be private will be added to the public list
|
|
74
|
+
|
|
75
|
+
In the godforsaken event that some how we have a directory structure that exceeds 1000 folders, this will fail
|
|
76
|
+
due to a recursion error
|
|
77
|
+
|
|
78
|
+
:param allowPrivate: If private files should be treated as private
|
|
79
|
+
:param dataFilesSource: The current search directory
|
|
80
|
+
:param discoveredPrivateFiles: The list that contains the private files to be copied
|
|
81
|
+
:param discoveredPublicFiles: the list that contains the public files to be copied
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
for file in os.listdir(dataFilesSource):
|
|
85
|
+
# ignore hidden files + directories
|
|
86
|
+
if file[0] == ".":
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
path = os.path.join(dataFilesSource, file)
|
|
90
|
+
|
|
91
|
+
# ignore any and all test files
|
|
92
|
+
if os.path.isfile(path) and "test" in file.lower():
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
if os.path.isdir(path):
|
|
96
|
+
Build._discoverDataFiles(allowPrivate, path, discoveredPrivateFiles, discoveredPublicFiles)
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
if allowPrivate and "private" in path.lower():
|
|
100
|
+
discoveredPrivateFiles.append(path)
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
discoveredPublicFiles.append(path)
|
|
104
|
+
|
|
105
|
+
def discoverFiles(self) -> Dict[FilesEnum, List[str]]:
|
|
106
|
+
"""
|
|
107
|
+
Description
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
This function discovers all of the user defined files to copy.
|
|
111
|
+
See :ref:`_discoverTestFiles` and :ref:`_discoverDataFiles` for more information on how this process works
|
|
112
|
+
|
|
113
|
+
:returns: A map containing all the user defined files to copy
|
|
114
|
+
"""
|
|
115
|
+
config = self.config.build
|
|
116
|
+
|
|
117
|
+
files: Dict[FilesEnum, List[str]] = {
|
|
118
|
+
FilesEnum.PUBLIC_TEST: [],
|
|
119
|
+
FilesEnum.PRIVATE_TEST: [],
|
|
120
|
+
FilesEnum.PUBLIC_DATA: [],
|
|
121
|
+
FilesEnum.PRIVATE_DATA: [],
|
|
122
|
+
FilesEnum.STARTER_CODE: [],
|
|
123
|
+
FilesEnum.CONFIG_FILE: [],
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
self._discoverTestFiles(config.allow_private,
|
|
127
|
+
re.compile(config.private_tests_regex), re.compile(config.public_tests_regex),
|
|
128
|
+
self.config.config.test_directory,
|
|
129
|
+
files[FilesEnum.PRIVATE_TEST], files[FilesEnum.PUBLIC_TEST])
|
|
130
|
+
|
|
131
|
+
# imo, this is not worth moving into its function atm
|
|
132
|
+
if config.use_starter_code:
|
|
133
|
+
# we can assume that the file exists if the config has it
|
|
134
|
+
files[FilesEnum.STARTER_CODE].append(config.starter_code_source)
|
|
135
|
+
|
|
136
|
+
if config.use_data_files:
|
|
137
|
+
self._discoverDataFiles(config.allow_private, config.data_files_source,
|
|
138
|
+
files[FilesEnum.PRIVATE_DATA], files[FilesEnum.PUBLIC_DATA])
|
|
139
|
+
|
|
140
|
+
files[FilesEnum.CONFIG_FILE] = [os.path.join(self.sourceDir, "config.toml")]
|
|
141
|
+
|
|
142
|
+
return files
|
|
143
|
+
|
|
144
|
+
@staticmethod
|
|
145
|
+
def copy(src, dest):
|
|
146
|
+
if os.path.isdir(src):
|
|
147
|
+
shutil.copytree(src, dest)
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
shutil.copy(src, dest)
|
|
151
|
+
|
|
152
|
+
def createFolders(self):
|
|
153
|
+
# clean build if it exists
|
|
154
|
+
if os.path.exists(self.binRoot):
|
|
155
|
+
try:
|
|
156
|
+
shutil.rmtree(self.binRoot, ignore_errors=True)
|
|
157
|
+
except OSError:
|
|
158
|
+
print("WARN: Failed to clean bin directory")
|
|
159
|
+
|
|
160
|
+
# create directories
|
|
161
|
+
os.makedirs(self.generationDirectory, exist_ok=True)
|
|
162
|
+
os.makedirs(self.distDirectory, exist_ok=True)
|
|
163
|
+
|
|
164
|
+
@staticmethod
|
|
165
|
+
def createSetupForGradescope(path: str, version: str):
|
|
166
|
+
with open(os.path.join(path, "setup.sh"), "w") as w:
|
|
167
|
+
w.write(
|
|
168
|
+
"apt-get install python3.11 -y\n"
|
|
169
|
+
"apt-get install python3-pip -y\n"
|
|
170
|
+
# "apt-get install -y libgbm-dev xvfb\n"
|
|
171
|
+
"pip3 install --upgrade pip\n"
|
|
172
|
+
f"pip3 install 128Autograder=={version}\n"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
@staticmethod
|
|
176
|
+
def createRunFileForGradescope(path: str):
|
|
177
|
+
with open(os.path.join(path, "run_autograder"), "w") as w:
|
|
178
|
+
w.write(
|
|
179
|
+
"#!/bin/bash\n"
|
|
180
|
+
"pushd source > /dev/null || echo 'Autograder failed to open source'\n"
|
|
181
|
+
"run_gradescope\n"
|
|
182
|
+
"popd > /dev/null || true\n"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
@staticmethod
|
|
186
|
+
def createRunForPrairieLearn(path: str):
|
|
187
|
+
with open(os.path.join(path, "run_autograder"), "w") as w:
|
|
188
|
+
w.write(
|
|
189
|
+
"#!/bin/bash\n"
|
|
190
|
+
"pushd source > /dev/null || echo 'Autograder failed to open source'\n"
|
|
191
|
+
"run_prairielearn\n"
|
|
192
|
+
"popd > /dev/null || true\n"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
@staticmethod
|
|
196
|
+
def generateDocker(generationPath: str, platform, files: Dict[FilesEnum, List[str]], version: str,
|
|
197
|
+
setupFileGenerator: Callable[[str, str], None], runFileGenerator: Callable[[str], None]):
|
|
198
|
+
generationPath = os.path.join(generationPath, "docker", platform)
|
|
199
|
+
os.makedirs(generationPath, exist_ok=True)
|
|
200
|
+
|
|
201
|
+
setupFileGenerator(generationPath, version)
|
|
202
|
+
runFileGenerator(generationPath)
|
|
203
|
+
|
|
204
|
+
for key, listOfFiles in files.items():
|
|
205
|
+
if key is FilesEnum.STARTER_CODE:
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
for file in listOfFiles:
|
|
209
|
+
destPath = os.path.join(generationPath, file)
|
|
210
|
+
os.makedirs(os.path.dirname(destPath), exist_ok=True)
|
|
211
|
+
Build.copy(file, destPath)
|
|
212
|
+
|
|
213
|
+
@staticmethod
|
|
214
|
+
def generateStudent(generationPath: str, files: Dict[FilesEnum, List[str]], studentWorkFolder: str):
|
|
215
|
+
generationPath = os.path.join(generationPath, "student")
|
|
216
|
+
os.makedirs(generationPath, exist_ok=True)
|
|
217
|
+
|
|
218
|
+
# create student_work folder
|
|
219
|
+
studentWorkFolder = os.path.join(generationPath, studentWorkFolder)
|
|
220
|
+
os.makedirs(studentWorkFolder, exist_ok=True)
|
|
221
|
+
|
|
222
|
+
for file in files[FilesEnum.PUBLIC_TEST]:
|
|
223
|
+
destPath = os.path.join(generationPath, file)
|
|
224
|
+
os.makedirs(os.path.dirname(destPath), exist_ok=True)
|
|
225
|
+
Build.copy(file, destPath)
|
|
226
|
+
|
|
227
|
+
for file in files[FilesEnum.PUBLIC_DATA]:
|
|
228
|
+
destPath = os.path.join(generationPath, file)
|
|
229
|
+
os.makedirs(os.path.dirname(destPath), exist_ok=True)
|
|
230
|
+
Build.copy(file, destPath)
|
|
231
|
+
# also add to student work folder
|
|
232
|
+
destPath = os.path.join(studentWorkFolder, os.path.basename(file))
|
|
233
|
+
Build.copy(file, destPath)
|
|
234
|
+
|
|
235
|
+
for file in files[FilesEnum.STARTER_CODE]:
|
|
236
|
+
destPath = os.path.join(studentWorkFolder, os.path.basename(file))
|
|
237
|
+
Build.copy(file, destPath)
|
|
238
|
+
|
|
239
|
+
for file in files[FilesEnum.CONFIG_FILE]:
|
|
240
|
+
destPath = os.path.join(generationPath, os.path.basename(file))
|
|
241
|
+
Build.copy(file, destPath)
|
|
242
|
+
|
|
243
|
+
# create .keep so that we dont loose the file
|
|
244
|
+
with open(os.path.join(studentWorkFolder, ".keep"), "w") as w:
|
|
245
|
+
w.write("DO NOT WRITE YOUR CODE HERE!\nCreate a *new* file in this directory!!!")
|
|
246
|
+
|
|
247
|
+
@staticmethod
|
|
248
|
+
def createDist(distType: str, generationPath: str, distPath: str, assignmentName: str):
|
|
249
|
+
generationPath = os.path.join(generationPath, distType)
|
|
250
|
+
if not os.path.exists(generationPath) or not os.path.isdir(generationPath):
|
|
251
|
+
raise AttributeError(f"Invalid generation path: {generationPath}")
|
|
252
|
+
|
|
253
|
+
os.makedirs(distPath, exist_ok=True)
|
|
254
|
+
|
|
255
|
+
assignmentName += f"-{'-'.join(distType.split('/'))}"
|
|
256
|
+
distPath = os.path.join(distPath, assignmentName)
|
|
257
|
+
|
|
258
|
+
shutil.make_archive(distPath, "zip", root_dir=generationPath)
|
|
259
|
+
|
|
260
|
+
def build(self):
|
|
261
|
+
files = self.discoverFiles()
|
|
262
|
+
|
|
263
|
+
self.createFolders()
|
|
264
|
+
|
|
265
|
+
if self.config.build.build_gradescope:
|
|
266
|
+
self.generateDocker(self.generationDirectory, "gradescope", files, self.version,
|
|
267
|
+
self.createSetupForGradescope, self.createRunFileForGradescope)
|
|
268
|
+
self.createDist("docker/gradescope", self.generationDirectory, self.distDirectory,
|
|
269
|
+
f"{self.config.semester}_{self.config.assignment_name}")
|
|
270
|
+
if self.config.build.build_prairie_learn:
|
|
271
|
+
# this build is a touch bigger than it needs to be, but for now I'm not super concerned about it
|
|
272
|
+
self.generateDocker(self.generationDirectory, "prairielearn", files, self.version,
|
|
273
|
+
lambda _, __: None, self.createRunForPrairieLearn)
|
|
274
|
+
self.createDist("docker/prairielearn", self.generationDirectory, self.distDirectory,
|
|
275
|
+
f"{self.config.semester}_{self.config.assignment_name}")
|
|
276
|
+
|
|
277
|
+
if self.config.build.build_student:
|
|
278
|
+
self.generateStudent(self.generationDirectory, files,
|
|
279
|
+
self.config.build.student_work_folder)
|
|
280
|
+
self.createDist("student", self.generationDirectory, self.distDirectory,
|
|
281
|
+
f"{self.config.semester}_{self.config.assignment_name}")
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class BuildAutograderCLI(AutograderCLITool):
|
|
285
|
+
def __init__(self):
|
|
286
|
+
super().__init__(f"Builder v{AutograderCLITool.get_version()}")
|
|
287
|
+
|
|
288
|
+
def configure_options(self):
|
|
289
|
+
self.parser.add_argument("--source", default=".", help="Autograder source root")
|
|
290
|
+
self.parser.add_argument("-o", default="./bin", help="Output folder")
|
|
291
|
+
|
|
292
|
+
def run(self) -> bool:
|
|
293
|
+
self.configure_options()
|
|
294
|
+
self.load_config()
|
|
295
|
+
|
|
296
|
+
build = Build(self.config, self.arguments.source, self.arguments.o, self.get_version())
|
|
297
|
+
|
|
298
|
+
build.build()
|
|
299
|
+
|
|
300
|
+
return True
|
|
301
|
+
|
|
302
|
+
def set_config_arguments(self, configBuilder: AutograderConfigurationBuilder[AutograderConfiguration]):
|
|
303
|
+
pass
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
tool = BuildAutograderCLI().run
|
|
307
|
+
|
|
308
|
+
if __name__ == "__main__":
|
|
309
|
+
res = tool()
|
|
310
|
+
|
|
311
|
+
if res:
|
|
312
|
+
exit(0)
|
|
313
|
+
|
|
314
|
+
exit(1)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import getpass
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from zipfile import ZipFile
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def buildZipName() -> str:
|
|
8
|
+
"""
|
|
9
|
+
This function generates the zip file name and sets a fallback if the user's name can't be determined
|
|
10
|
+
:return: the generated zip file name
|
|
11
|
+
"""
|
|
12
|
+
DEFAULT_NAME: str = "csci_128_student"
|
|
13
|
+
zipName: str = ""
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
# Get user can throw an exception if it fails to look up the username in the password database
|
|
17
|
+
userName = getpass.getuser().lower()
|
|
18
|
+
# replace spaces with underscore bc spaces make me sad
|
|
19
|
+
userName = userName.replace(" ", "_")
|
|
20
|
+
|
|
21
|
+
zipName += userName
|
|
22
|
+
|
|
23
|
+
except KeyError:
|
|
24
|
+
print(f"Can't automatically determine user name. Defaulting to {DEFAULT_NAME}")
|
|
25
|
+
zipName += DEFAULT_NAME
|
|
26
|
+
|
|
27
|
+
zipName += "-submission"
|
|
28
|
+
zipName += ".zip"
|
|
29
|
+
|
|
30
|
+
return zipName
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def addFolderToZip(_currentZipBuffer: ZipFile, _directoryToAdd: str) -> None:
|
|
34
|
+
"""
|
|
35
|
+
This function allows us to recursively descend through the directory structure to
|
|
36
|
+
collect all of a student's submitted files
|
|
37
|
+
:param _currentZipBuffer: the zip file that is currently being written.
|
|
38
|
+
:param _directoryToAdd: the directory that needs to be added
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
# Ignore hidden directories
|
|
42
|
+
if _directoryToAdd[0] == ".":
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
# Ignore directories python directories
|
|
46
|
+
if "__" in _directoryToAdd:
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
print(f"\tEntering {_directoryToAdd}...")
|
|
50
|
+
|
|
51
|
+
for file in os.listdir(_directoryToAdd):
|
|
52
|
+
if os.path.isfile(_directoryToAdd + file) and file[-3:] == ".py":
|
|
53
|
+
print(f"\tAdding {_directoryToAdd + file}...")
|
|
54
|
+
_currentZipBuffer.write(_directoryToAdd + file)
|
|
55
|
+
elif os.path.isfile(_directoryToAdd + file):
|
|
56
|
+
print(f"\tIgnoring {_directoryToAdd + file}...")
|
|
57
|
+
else:
|
|
58
|
+
addFolderToZip(_currentZipBuffer, _directoryToAdd + file + "/")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def generateZipFile(_submissionDirectory: str) -> None:
|
|
62
|
+
"""
|
|
63
|
+
This function generates the zip file needed for gradescope.
|
|
64
|
+
:param _submissionDirectory: the directory that the students work is in
|
|
65
|
+
"""
|
|
66
|
+
GREEN_COLOR: str = u"\u001b[32m"
|
|
67
|
+
RESET_COLOR: str = u"\u001b[0m"
|
|
68
|
+
|
|
69
|
+
print("Generating gradescope upload...")
|
|
70
|
+
|
|
71
|
+
zipName: str = buildZipName()
|
|
72
|
+
|
|
73
|
+
with ZipFile(zipName, 'w') as submissionZip:
|
|
74
|
+
os.chdir(_submissionDirectory)
|
|
75
|
+
for file in os.listdir("."):
|
|
76
|
+
if os.path.isfile(file) and file[-3:] == ".py":
|
|
77
|
+
print(f"\tAdding {_submissionDirectory + file} to zip...")
|
|
78
|
+
submissionZip.write(file)
|
|
79
|
+
elif os.path.isfile(file):
|
|
80
|
+
print(f"\tIgnoring {_submissionDirectory + file}...")
|
|
81
|
+
else:
|
|
82
|
+
addFolderToZip(submissionZip, file + "/")
|
|
83
|
+
|
|
84
|
+
print("...Done.")
|
|
85
|
+
print(f"\n\n{GREEN_COLOR}Submit {zipName} to Gradescope under the corresponding assignment.{RESET_COLOR}")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def tool(): # pragma: no cover
|
|
90
|
+
submissionDirectory = "student_work"
|
|
91
|
+
|
|
92
|
+
if len(sys.argv) == 2:
|
|
93
|
+
submissionDirectory = sys.argv[1]
|
|
94
|
+
|
|
95
|
+
# need to make sure to that we have a / at the end of the path
|
|
96
|
+
if submissionDirectory[-1:] != '/':
|
|
97
|
+
submissionDirectory += "/"
|
|
98
|
+
|
|
99
|
+
generateZipFile(submissionDirectory)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
if __name__ == "__main__":
|
|
103
|
+
tool()
|