lograder 0.0.2__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.
Files changed (74) hide show
  1. lograder-0.0.2/LICENSE +7 -0
  2. lograder-0.0.2/PKG-INFO +342 -0
  3. lograder-0.0.2/README.md +307 -0
  4. lograder-0.0.2/pyproject.toml +63 -0
  5. lograder-0.0.2/setup.cfg +4 -0
  6. lograder-0.0.2/src/lograder/__init__.py +2 -0
  7. lograder-0.0.2/src/lograder/_core_exceptions.py +7 -0
  8. lograder-0.0.2/src/lograder/common/__init__.py +0 -0
  9. lograder-0.0.2/src/lograder/common/types.py +5 -0
  10. lograder-0.0.2/src/lograder/common/utils.py +6 -0
  11. lograder-0.0.2/src/lograder/dispatch/__init__.py +9 -0
  12. lograder-0.0.2/src/lograder/dispatch/_core_exceptions.py +10 -0
  13. lograder-0.0.2/src/lograder/dispatch/common/__init__.py +30 -0
  14. lograder-0.0.2/src/lograder/dispatch/common/assignment.py +99 -0
  15. lograder-0.0.2/src/lograder/dispatch/common/exceptions.py +42 -0
  16. lograder-0.0.2/src/lograder/dispatch/common/file_operations.py +118 -0
  17. lograder-0.0.2/src/lograder/dispatch/common/interface.py +227 -0
  18. lograder-0.0.2/src/lograder/dispatch/common/templates/__init__.py +10 -0
  19. lograder-0.0.2/src/lograder/dispatch/common/templates/cli_builder.py +59 -0
  20. lograder-0.0.2/src/lograder/dispatch/common/templates/executable_runner.py +23 -0
  21. lograder-0.0.2/src/lograder/dispatch/common/templates/trivial.py +34 -0
  22. lograder-0.0.2/src/lograder/dispatch/common/types.py +55 -0
  23. lograder-0.0.2/src/lograder/dispatch/cpp/__init__.py +7 -0
  24. lograder-0.0.2/src/lograder/dispatch/cpp/cmake.py +172 -0
  25. lograder-0.0.2/src/lograder/dispatch/cpp/cpp_source.py +101 -0
  26. lograder-0.0.2/src/lograder/dispatch/exceptions.py +19 -0
  27. lograder-0.0.2/src/lograder/dispatch/misc/__init__.py +4 -0
  28. lograder-0.0.2/src/lograder/dispatch/misc/dispatcher.py +89 -0
  29. lograder-0.0.2/src/lograder/dispatch/misc/makefile.py +89 -0
  30. lograder-0.0.2/src/lograder/exceptions.py +15 -0
  31. lograder-0.0.2/src/lograder/output/__init__.py +0 -0
  32. lograder-0.0.2/src/lograder/output/common/__init__.py +3 -0
  33. lograder-0.0.2/src/lograder/output/common/types.py +6 -0
  34. lograder-0.0.2/src/lograder/output/formatters/__init__.py +0 -0
  35. lograder-0.0.2/src/lograder/output/formatters/default.py +359 -0
  36. lograder-0.0.2/src/lograder/output/formatters/format_templates.py +53 -0
  37. lograder-0.0.2/src/lograder/output/formatters/interfaces.py +67 -0
  38. lograder-0.0.2/src/lograder/output/raw_json/__init__.py +0 -0
  39. lograder-0.0.2/src/lograder/output/raw_json/assignment.py +44 -0
  40. lograder-0.0.2/src/lograder/output/raw_json/leaderboard.py +19 -0
  41. lograder-0.0.2/src/lograder/output/raw_json/test_case.py +26 -0
  42. lograder-0.0.2/src/lograder/static/__init__.py +7 -0
  43. lograder-0.0.2/src/lograder/static/basicconfig.py +18 -0
  44. lograder-0.0.2/src/lograder/static/messageconfig.py +15 -0
  45. lograder-0.0.2/src/lograder/tests/__init__.py +37 -0
  46. lograder-0.0.2/src/lograder/tests/_core_exceptions.py +20 -0
  47. lograder-0.0.2/src/lograder/tests/common/__init__.py +3 -0
  48. lograder-0.0.2/src/lograder/tests/common/exceptions.py +73 -0
  49. lograder-0.0.2/src/lograder/tests/common/validation.py +17 -0
  50. lograder-0.0.2/src/lograder/tests/exceptions.py +10 -0
  51. lograder-0.0.2/src/lograder/tests/file/__init__.py +3 -0
  52. lograder-0.0.2/src/lograder/tests/file/test_maker.py +48 -0
  53. lograder-0.0.2/src/lograder/tests/generator/__init__.py +17 -0
  54. lograder-0.0.2/src/lograder/tests/generator/test_maker.py +78 -0
  55. lograder-0.0.2/src/lograder/tests/generator/types.py +47 -0
  56. lograder-0.0.2/src/lograder/tests/registry/__init__.py +3 -0
  57. lograder-0.0.2/src/lograder/tests/registry/registry.py +44 -0
  58. lograder-0.0.2/src/lograder/tests/simple/__init__.py +3 -0
  59. lograder-0.0.2/src/lograder/tests/simple/test_maker.py +46 -0
  60. lograder-0.0.2/src/lograder/tests/template/__init__.py +9 -0
  61. lograder-0.0.2/src/lograder/tests/template/test_maker.py +110 -0
  62. lograder-0.0.2/src/lograder/tests/template/types.py +15 -0
  63. lograder-0.0.2/src/lograder/tests/test/__init__.py +8 -0
  64. lograder-0.0.2/src/lograder/tests/test/analytics.py +337 -0
  65. lograder-0.0.2/src/lograder/tests/test/comparison_test.py +176 -0
  66. lograder-0.0.2/src/lograder/tests/test/interface.py +87 -0
  67. lograder-0.0.2/src/lograder.egg-info/PKG-INFO +342 -0
  68. lograder-0.0.2/src/lograder.egg-info/SOURCES.txt +72 -0
  69. lograder-0.0.2/src/lograder.egg-info/dependency_links.txt +1 -0
  70. lograder-0.0.2/src/lograder.egg-info/requires.txt +11 -0
  71. lograder-0.0.2/src/lograder.egg-info/top_level.txt +1 -0
  72. lograder-0.0.2/tests/test_build.py +8 -0
  73. lograder-0.0.2/tests/test_builder.py +251 -0
  74. lograder-0.0.2/tests/test_tests.py +217 -0
lograder-0.0.2/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2025 Logan Dapp
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,342 @@
1
+ Metadata-Version: 2.4
2
+ Name: lograder
3
+ Version: 0.0.2
4
+ Summary: An API for easy Gradescope Autograder assignment creation.
5
+ Author-email: lognd <logan@logand.app>
6
+ License: Copyright 2025 Logan Dapp
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
11
+
12
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
13
+ Project-URL: Homepage, https://github.com/lognd/lograder
14
+ Project-URL: Documentation, https://github.com/lognd/lograder
15
+ Project-URL: Source, https://github.com/lognd/lograder
16
+ Project-URL: Issues, https://github.com/lognd/lograder/issues
17
+ Keywords: gradescope,autograder
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: License :: OSI Approved :: MIT License
20
+ Classifier: Operating System :: OS Independent
21
+ Requires-Python: >=3.12
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: pydantic>=2.11.7
25
+ Requires-Dist: colorama>=0.4.6
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=7.0; extra == "dev"
28
+ Requires-Dist: black>=24.0; extra == "dev"
29
+ Requires-Dist: ruff>=0.2; extra == "dev"
30
+ Requires-Dist: isort>=5.13; extra == "dev"
31
+ Requires-Dist: mypy>=1.0; extra == "dev"
32
+ Requires-Dist: pytest>=8.4.1; extra == "dev"
33
+ Requires-Dist: types-colorama>=0.4.15.20250801; extra == "dev"
34
+ Dynamic: license-file
35
+
36
+ # `lograder`: A Gradescope Autograder API
37
+
38
+ ----
39
+ This project just serves to standard different kinds of tests
40
+ that can be run on student code for the Gradescope autograder.
41
+ Additionally, this project was developed for the **University
42
+ of Florida's Fall 2025 COP3504C** (*Advanced Programming
43
+ Fundamentals*), taught by Michael Link. However, you are
44
+ completely free to use, remix, refactor, and abuse this code
45
+ as much as you like.
46
+
47
+ ----
48
+ # Project Builders
49
+
50
+ ----
51
+
52
+ ### C++ Complete Project with [I/O Comparison](#output-comparison)
53
+
54
+ #### Build from C++ Source (*WIP*)
55
+ To build from source, you will need to import the C++
56
+ `CxxSourceBuilder`. The executable will be randomly
57
+ named and put in either a build directory, if the student
58
+ has one (`./build`) or the project root directory (`./`).
59
+
60
+ ```py
61
+ from lograder.dispatch import CxxSourceDispatcher
62
+ from lograder.output import AssignmentSummary
63
+
64
+ # Note that when you make a test, it's automatically
65
+ # registered with the `lograder.tests.registry.TestRegistry`
66
+
67
+ assignment = CxxSourceDispatcher(project_root="/autograder/submission")
68
+ preprocessor_results = assignment.preprocess()
69
+ build_results = assignment.build()
70
+ runtime_results = assignment.run_tests()
71
+
72
+ summary = AssignmentSummary(
73
+ preprocessor_output=preprocessor_results.get_output(),
74
+ build_output=build_results.get_output(),
75
+ runtime_summary=runtime_results.get_summary(),
76
+ test_cases=runtime_results.get_test_cases()
77
+ )
78
+ ```
79
+
80
+ #### Build using CMake (*WIP*)
81
+ To build from a `CMakeLists.txt`, you will need to import the C++
82
+ `CMakeBuilder`. This method will automatically run a breadth-first
83
+ search starting in the project root directory (`./`) and "lock on"
84
+ the first (i.e. the file in the highest-level) `CMakeLists.txt` that
85
+ it finds. If it can't find a `CMakeLists.txt`, it will raise an error.
86
+
87
+ Additionally, the program will look for the following targets first:
88
+ `main`, `build`, and `demo`. Afterward, it will search for any target
89
+ that doesn't match: `all`, `install`, `test`, `package`, `package_source`,
90
+ `edit_cache`, `rebuild_cache`, `clean`, `help`, `ALL_BUILD`, `ZERO_CHECK`,
91
+ `INSTALL`, `RUN_TESTS`, and `PACKAGE`, and run the first target that it
92
+ finds. If it can't find a valid target, it will raise an error.
93
+
94
+ ```py
95
+ from lograder.dispatch import CMakeDispatcher
96
+ from lograder.output import AssignmentSummary
97
+
98
+ # Note that when you make a test, it's automatically
99
+ # registered with the `lograder.tests.registry.TestRegistry`
100
+
101
+ assignment = CMakeDispatcher(project_root="/autograder/submission")
102
+ preprocessor_results = assignment.preprocess()
103
+ build_results = assignment.build()
104
+ runtime_results = assignment.run_tests()
105
+
106
+ summary = AssignmentSummary(
107
+ preprocessor_output=preprocessor_results.get_output(),
108
+ build_output=build_results.get_output(),
109
+ runtime_summary=runtime_results.get_summary(),
110
+ test_cases=runtime_results.get_test_cases()
111
+ )
112
+ ```
113
+ ----
114
+
115
+ ### C++ Catch2 Unit Testing (*WIP*)
116
+
117
+ ----
118
+
119
+ ### Python Complete Project with [I/O Comparison](#output-comparison)
120
+
121
+ #### Run project from `main.py` (*WIP*)
122
+
123
+ #### Run project from `pyproject.toml` (*WIP*)
124
+
125
+ ----
126
+
127
+ ### Python pytest Unit Testing (*WIP*)
128
+
129
+ ----
130
+
131
+ ### Makefile Complete Project with [I/O Comparison](#output-comparison) (*WIP*)
132
+
133
+ To build from a `Makefile`, you will need a `MakefileBuilder`. It follows
134
+ the same general idea as the `CMakeBuilder` except that it searches for
135
+ `Makefile` instead of `CMakeLists.txt`. Additionally, `MakefileBuilder`
136
+ will just run the default `make`.
137
+
138
+ ```py
139
+ from lograder.dispatch import MakefileDispatcher
140
+ from lograder.output import AssignmentSummary
141
+
142
+ # Note that when you make a test, it's automatically
143
+ # registered with the `lograder.tests.registry.TestRegistry`
144
+
145
+ assignment = MakefileDispatcher(project_root="/autograder/submission")
146
+ preprocessor_results = assignment.preprocess()
147
+ build_results = assignment.build()
148
+ runtime_results = assignment.run_tests()
149
+
150
+ summary = AssignmentSummary(
151
+ preprocessor_output=preprocessor_results.get_output(),
152
+ build_output=build_results.get_output(),
153
+ runtime_summary=runtime_results.get_summary(),
154
+ test_cases=runtime_results.get_test_cases()
155
+ )
156
+ ```
157
+
158
+ ----
159
+ # Test Generation
160
+
161
+ ----
162
+
163
+ ## Output Comparison
164
+
165
+ ### Compare Simple Strings
166
+
167
+ For the smallest number of tiny test cases, there's no reason
168
+ to have an over-bloated mess. You can just use:
169
+
170
+ ```py
171
+ from typing import Sequence, Optional, List
172
+ from pathlib import Path
173
+ from lograder.tests import make_tests_from_strs, ExecutableOutputComparisonTest
174
+
175
+
176
+ def make_test_from_strs(
177
+ *, # kwargs-only; to avoid confusion with argument sequence.
178
+ names: Sequence[str],
179
+ inputs: Sequence[str],
180
+ expected_outputs: Sequence[str],
181
+ flag_sets: Optional[Sequence[List[str | Path]]] = None,
182
+ # Pass flags like ["--option-1", "--option-2"] to student programs
183
+ weights: Optional[Sequence[float]] = None, # Defaults to equal-weight.
184
+ ) -> List[ExecutableOutputComparisonTest]: ...
185
+
186
+
187
+ # Here's an example of how you'd use the above method:
188
+ make_tests_from_strs(
189
+ names=["Test Case 1", "Test Case 2"],
190
+ inputs=["stdin-1", "stdin-2"],
191
+ expected_outputs=["stdout-1", "stdout-2"]
192
+ )
193
+ ```
194
+
195
+ ### Compare from Files
196
+
197
+ If you have a larger test, it would be very convenient to
198
+ read files for input and output. Luckily, there's just the
199
+ method to do so:
200
+
201
+ ```py
202
+ from typing import Sequence, Optional, List
203
+ from pathlib import Path
204
+ from lograder.tests import make_tests_from_files, FilePath, ExecutableOutputComparisonTest
205
+
206
+
207
+ # `make_tests_from_files` has the following signature.
208
+ def make_tests_from_files(
209
+ *, # kwargs-only; to avoid confusion with argument sequence.
210
+ names: Sequence[str],
211
+ input_files: Optional[Sequence[FilePath]] = None, # `input_files` and `input_strs` mutually exclusive.
212
+ input_strs: Optional[Sequence[str]] = None,
213
+ expected_output_files: Optional[Sequence[FilePath]] = None,
214
+ # same with `expected_output_files` and `expected_output_strs`
215
+ expected_output_strs: Optional[Sequence[str]] = None,
216
+ flag_sets: Optional[Sequence[List[str | Path]]] = None,
217
+ # Pass flags like ["--option-1", "--option-2"] to student programs
218
+ weights: Optional[Sequence[float]] = None, # Defaults to equal-weight.
219
+ ) -> List[ExecutableOutputComparisonTest]: ...
220
+
221
+
222
+ # Here's an example of how you'd use the above method:
223
+ make_tests_from_files(
224
+ names=["Test Case 1", "Test Case 2"],
225
+ input_files=["test/inputs/input1.txt", "test/inputs/input2.txt"],
226
+ expected_output_files=["test/inputs/output1.txt", "test/inputs/output2.txt"]
227
+ )
228
+ ```
229
+
230
+ ### Compare from Template
231
+
232
+ Finally, sometimes the test-cases might be very long but
233
+ very repetitive. You can use `make_tests_from_template`
234
+ and pass a `TestCaseTemplate` object and ...
235
+
236
+ ```py
237
+ from typing import Sequence, Optional, List
238
+ from pathlib import Path
239
+ from lograder.tests import make_tests_from_template, TestCaseTemplate, FilePath
240
+
241
+
242
+ # Here's the signature of a `TemplateSubstitution`
243
+ class TemplateSubstitution:
244
+ def __init__(self, *args, **kwargs):
245
+ # Stores args and kwargs to pass to str.format(...) later.
246
+ ...
247
+
248
+
249
+ TSub = TemplateSubstitution # Here's an alias that's quicker to type.
250
+
251
+
252
+ # Here's the signature of a `TestCaseTemplate`
253
+ class TestCaseTemplate:
254
+ def __init__(self, *,
255
+ inputs: Optional[Sequence[str]] = None,
256
+ input_template_file: Optional[FilePath] = None,
257
+ input_template_str: Optional[str] = None,
258
+ input_substitutions: Optional[Sequence[TemplateSubstitution]] = None,
259
+ expected_outputs: Optional[Sequence[str]] = None,
260
+ expected_output_template_file: Optional[FilePath] = None,
261
+ expected_output_template_str: Optional[str] = None,
262
+ expected_output_substitutions: Optional[Sequence[TemplateSubstitution]] = None,
263
+ flag_sets: Optional[Sequence[List[str | Path]]] = None, # Pass flags like ["--option-1", "--option-2"] to student programs
264
+ ):
265
+ # +=====================================================================================+
266
+ # | Validation Rules |
267
+ # +=====================================================================================+
268
+ # * If `inputs` is specified, all other `input_*` parameters must be left unspecified.
269
+ # * Same thing with `expected_outputs`.
270
+ # * If `inputs` is not specified, you must specify either (mutually exclusive)
271
+ # `input_template_file` or `input_template_str` that follows a typical python
272
+ # format string, and you must specify `input_substitutions`.
273
+ # * Same thing with `expected_output_template_file`, `expected_output_template_str`,
274
+ # and `expected_output_substitutions`
275
+ ...
276
+
277
+
278
+ # Here's an example of how you would use TestCaseTemplate
279
+ test_suite_1 = TestCaseTemplate(
280
+ inputs=["A", "B", "C"], # Three (3) Total Cases
281
+ expected_output_template_str="{}, {kwarged}, {}",
282
+ expected_output_substitutions=[
283
+ TSub(1.0, 2.0, kwarged="middle-arg-1"), # Case 1 Substitutions
284
+ TSub(2.0, 5.0, kwarged="middle-arg-2"), # Case 2 Substitutions
285
+ TSub(7.0, 6.0, kwarged="middle-arg-3"), # Case 3 Substitutions
286
+ ]
287
+ )
288
+ make_tests_from_template(
289
+ ["Test 1", "Test 2", "Test 3"],
290
+ test_suite_1
291
+ ) # remember to construct the tests!
292
+
293
+ ```
294
+
295
+ ### Compare from Python Generator/Iterable
296
+
297
+ Sometimes, you want to generate a ton of test-cases (especially
298
+ small test-cases), and it would be incredibly waste to have thousands
299
+ of single-line files. You can create a python generator function that
300
+ follows either the following `Protocol` or `TypedDict`.
301
+
302
+ ```py
303
+ from typing import Protocol, TypedDict, Generator, NotRequired, List
304
+ from pathlib import Path
305
+ from lograder.tests import make_tests_from_generator
306
+
307
+
308
+ # Your generator may return objects following the protocol...
309
+ class TestCaseProtocol(Protocol):
310
+ def get_name(self): ...
311
+
312
+ def get_input(self): ...
313
+
314
+ def get_expected_output(self): ...
315
+
316
+ class FlaggedTestCaseProtocol(TestCaseProtocol, Protocol):
317
+ def get_flags(self) -> List[str | Path]: ...
318
+
319
+ # Notice that TestCaseProtocol defaults to equal-weights
320
+ class WeightedTestCaseProtocol(TestCaseProtocol, Protocol):
321
+ def get_weight(self): ...
322
+
323
+ # ... or you can directly return a dict with the following keys.
324
+ class TestCaseDict(TypedDict):
325
+ name: str
326
+ input: str
327
+ expected_output: str
328
+ weight: NotRequired[float] # Defaults to 1.0, a.k.a. equal-weight.
329
+ flags: NotRequired[List[str | Path]]
330
+
331
+
332
+ # Here's an example of the syntax as well as the required
333
+ # signature of such a method:
334
+ @make_tests_from_generator
335
+ def test_suite_1() -> Generator[TestCaseProtocol | WeightedTestCaseProtocol | TestCaseDict, None, None]:
336
+ pass
337
+
338
+ # You'll have to query the `TestRegistry` from `lograder.tests` to access these tests directly, though.
339
+ ```
340
+
341
+
342
+
@@ -0,0 +1,307 @@
1
+ # `lograder`: A Gradescope Autograder API
2
+
3
+ ----
4
+ This project just serves to standard different kinds of tests
5
+ that can be run on student code for the Gradescope autograder.
6
+ Additionally, this project was developed for the **University
7
+ of Florida's Fall 2025 COP3504C** (*Advanced Programming
8
+ Fundamentals*), taught by Michael Link. However, you are
9
+ completely free to use, remix, refactor, and abuse this code
10
+ as much as you like.
11
+
12
+ ----
13
+ # Project Builders
14
+
15
+ ----
16
+
17
+ ### C++ Complete Project with [I/O Comparison](#output-comparison)
18
+
19
+ #### Build from C++ Source (*WIP*)
20
+ To build from source, you will need to import the C++
21
+ `CxxSourceBuilder`. The executable will be randomly
22
+ named and put in either a build directory, if the student
23
+ has one (`./build`) or the project root directory (`./`).
24
+
25
+ ```py
26
+ from lograder.dispatch import CxxSourceDispatcher
27
+ from lograder.output import AssignmentSummary
28
+
29
+ # Note that when you make a test, it's automatically
30
+ # registered with the `lograder.tests.registry.TestRegistry`
31
+
32
+ assignment = CxxSourceDispatcher(project_root="/autograder/submission")
33
+ preprocessor_results = assignment.preprocess()
34
+ build_results = assignment.build()
35
+ runtime_results = assignment.run_tests()
36
+
37
+ summary = AssignmentSummary(
38
+ preprocessor_output=preprocessor_results.get_output(),
39
+ build_output=build_results.get_output(),
40
+ runtime_summary=runtime_results.get_summary(),
41
+ test_cases=runtime_results.get_test_cases()
42
+ )
43
+ ```
44
+
45
+ #### Build using CMake (*WIP*)
46
+ To build from a `CMakeLists.txt`, you will need to import the C++
47
+ `CMakeBuilder`. This method will automatically run a breadth-first
48
+ search starting in the project root directory (`./`) and "lock on"
49
+ the first (i.e. the file in the highest-level) `CMakeLists.txt` that
50
+ it finds. If it can't find a `CMakeLists.txt`, it will raise an error.
51
+
52
+ Additionally, the program will look for the following targets first:
53
+ `main`, `build`, and `demo`. Afterward, it will search for any target
54
+ that doesn't match: `all`, `install`, `test`, `package`, `package_source`,
55
+ `edit_cache`, `rebuild_cache`, `clean`, `help`, `ALL_BUILD`, `ZERO_CHECK`,
56
+ `INSTALL`, `RUN_TESTS`, and `PACKAGE`, and run the first target that it
57
+ finds. If it can't find a valid target, it will raise an error.
58
+
59
+ ```py
60
+ from lograder.dispatch import CMakeDispatcher
61
+ from lograder.output import AssignmentSummary
62
+
63
+ # Note that when you make a test, it's automatically
64
+ # registered with the `lograder.tests.registry.TestRegistry`
65
+
66
+ assignment = CMakeDispatcher(project_root="/autograder/submission")
67
+ preprocessor_results = assignment.preprocess()
68
+ build_results = assignment.build()
69
+ runtime_results = assignment.run_tests()
70
+
71
+ summary = AssignmentSummary(
72
+ preprocessor_output=preprocessor_results.get_output(),
73
+ build_output=build_results.get_output(),
74
+ runtime_summary=runtime_results.get_summary(),
75
+ test_cases=runtime_results.get_test_cases()
76
+ )
77
+ ```
78
+ ----
79
+
80
+ ### C++ Catch2 Unit Testing (*WIP*)
81
+
82
+ ----
83
+
84
+ ### Python Complete Project with [I/O Comparison](#output-comparison)
85
+
86
+ #### Run project from `main.py` (*WIP*)
87
+
88
+ #### Run project from `pyproject.toml` (*WIP*)
89
+
90
+ ----
91
+
92
+ ### Python pytest Unit Testing (*WIP*)
93
+
94
+ ----
95
+
96
+ ### Makefile Complete Project with [I/O Comparison](#output-comparison) (*WIP*)
97
+
98
+ To build from a `Makefile`, you will need a `MakefileBuilder`. It follows
99
+ the same general idea as the `CMakeBuilder` except that it searches for
100
+ `Makefile` instead of `CMakeLists.txt`. Additionally, `MakefileBuilder`
101
+ will just run the default `make`.
102
+
103
+ ```py
104
+ from lograder.dispatch import MakefileDispatcher
105
+ from lograder.output import AssignmentSummary
106
+
107
+ # Note that when you make a test, it's automatically
108
+ # registered with the `lograder.tests.registry.TestRegistry`
109
+
110
+ assignment = MakefileDispatcher(project_root="/autograder/submission")
111
+ preprocessor_results = assignment.preprocess()
112
+ build_results = assignment.build()
113
+ runtime_results = assignment.run_tests()
114
+
115
+ summary = AssignmentSummary(
116
+ preprocessor_output=preprocessor_results.get_output(),
117
+ build_output=build_results.get_output(),
118
+ runtime_summary=runtime_results.get_summary(),
119
+ test_cases=runtime_results.get_test_cases()
120
+ )
121
+ ```
122
+
123
+ ----
124
+ # Test Generation
125
+
126
+ ----
127
+
128
+ ## Output Comparison
129
+
130
+ ### Compare Simple Strings
131
+
132
+ For the smallest number of tiny test cases, there's no reason
133
+ to have an over-bloated mess. You can just use:
134
+
135
+ ```py
136
+ from typing import Sequence, Optional, List
137
+ from pathlib import Path
138
+ from lograder.tests import make_tests_from_strs, ExecutableOutputComparisonTest
139
+
140
+
141
+ def make_test_from_strs(
142
+ *, # kwargs-only; to avoid confusion with argument sequence.
143
+ names: Sequence[str],
144
+ inputs: Sequence[str],
145
+ expected_outputs: Sequence[str],
146
+ flag_sets: Optional[Sequence[List[str | Path]]] = None,
147
+ # Pass flags like ["--option-1", "--option-2"] to student programs
148
+ weights: Optional[Sequence[float]] = None, # Defaults to equal-weight.
149
+ ) -> List[ExecutableOutputComparisonTest]: ...
150
+
151
+
152
+ # Here's an example of how you'd use the above method:
153
+ make_tests_from_strs(
154
+ names=["Test Case 1", "Test Case 2"],
155
+ inputs=["stdin-1", "stdin-2"],
156
+ expected_outputs=["stdout-1", "stdout-2"]
157
+ )
158
+ ```
159
+
160
+ ### Compare from Files
161
+
162
+ If you have a larger test, it would be very convenient to
163
+ read files for input and output. Luckily, there's just the
164
+ method to do so:
165
+
166
+ ```py
167
+ from typing import Sequence, Optional, List
168
+ from pathlib import Path
169
+ from lograder.tests import make_tests_from_files, FilePath, ExecutableOutputComparisonTest
170
+
171
+
172
+ # `make_tests_from_files` has the following signature.
173
+ def make_tests_from_files(
174
+ *, # kwargs-only; to avoid confusion with argument sequence.
175
+ names: Sequence[str],
176
+ input_files: Optional[Sequence[FilePath]] = None, # `input_files` and `input_strs` mutually exclusive.
177
+ input_strs: Optional[Sequence[str]] = None,
178
+ expected_output_files: Optional[Sequence[FilePath]] = None,
179
+ # same with `expected_output_files` and `expected_output_strs`
180
+ expected_output_strs: Optional[Sequence[str]] = None,
181
+ flag_sets: Optional[Sequence[List[str | Path]]] = None,
182
+ # Pass flags like ["--option-1", "--option-2"] to student programs
183
+ weights: Optional[Sequence[float]] = None, # Defaults to equal-weight.
184
+ ) -> List[ExecutableOutputComparisonTest]: ...
185
+
186
+
187
+ # Here's an example of how you'd use the above method:
188
+ make_tests_from_files(
189
+ names=["Test Case 1", "Test Case 2"],
190
+ input_files=["test/inputs/input1.txt", "test/inputs/input2.txt"],
191
+ expected_output_files=["test/inputs/output1.txt", "test/inputs/output2.txt"]
192
+ )
193
+ ```
194
+
195
+ ### Compare from Template
196
+
197
+ Finally, sometimes the test-cases might be very long but
198
+ very repetitive. You can use `make_tests_from_template`
199
+ and pass a `TestCaseTemplate` object and ...
200
+
201
+ ```py
202
+ from typing import Sequence, Optional, List
203
+ from pathlib import Path
204
+ from lograder.tests import make_tests_from_template, TestCaseTemplate, FilePath
205
+
206
+
207
+ # Here's the signature of a `TemplateSubstitution`
208
+ class TemplateSubstitution:
209
+ def __init__(self, *args, **kwargs):
210
+ # Stores args and kwargs to pass to str.format(...) later.
211
+ ...
212
+
213
+
214
+ TSub = TemplateSubstitution # Here's an alias that's quicker to type.
215
+
216
+
217
+ # Here's the signature of a `TestCaseTemplate`
218
+ class TestCaseTemplate:
219
+ def __init__(self, *,
220
+ inputs: Optional[Sequence[str]] = None,
221
+ input_template_file: Optional[FilePath] = None,
222
+ input_template_str: Optional[str] = None,
223
+ input_substitutions: Optional[Sequence[TemplateSubstitution]] = None,
224
+ expected_outputs: Optional[Sequence[str]] = None,
225
+ expected_output_template_file: Optional[FilePath] = None,
226
+ expected_output_template_str: Optional[str] = None,
227
+ expected_output_substitutions: Optional[Sequence[TemplateSubstitution]] = None,
228
+ flag_sets: Optional[Sequence[List[str | Path]]] = None, # Pass flags like ["--option-1", "--option-2"] to student programs
229
+ ):
230
+ # +=====================================================================================+
231
+ # | Validation Rules |
232
+ # +=====================================================================================+
233
+ # * If `inputs` is specified, all other `input_*` parameters must be left unspecified.
234
+ # * Same thing with `expected_outputs`.
235
+ # * If `inputs` is not specified, you must specify either (mutually exclusive)
236
+ # `input_template_file` or `input_template_str` that follows a typical python
237
+ # format string, and you must specify `input_substitutions`.
238
+ # * Same thing with `expected_output_template_file`, `expected_output_template_str`,
239
+ # and `expected_output_substitutions`
240
+ ...
241
+
242
+
243
+ # Here's an example of how you would use TestCaseTemplate
244
+ test_suite_1 = TestCaseTemplate(
245
+ inputs=["A", "B", "C"], # Three (3) Total Cases
246
+ expected_output_template_str="{}, {kwarged}, {}",
247
+ expected_output_substitutions=[
248
+ TSub(1.0, 2.0, kwarged="middle-arg-1"), # Case 1 Substitutions
249
+ TSub(2.0, 5.0, kwarged="middle-arg-2"), # Case 2 Substitutions
250
+ TSub(7.0, 6.0, kwarged="middle-arg-3"), # Case 3 Substitutions
251
+ ]
252
+ )
253
+ make_tests_from_template(
254
+ ["Test 1", "Test 2", "Test 3"],
255
+ test_suite_1
256
+ ) # remember to construct the tests!
257
+
258
+ ```
259
+
260
+ ### Compare from Python Generator/Iterable
261
+
262
+ Sometimes, you want to generate a ton of test-cases (especially
263
+ small test-cases), and it would be incredibly waste to have thousands
264
+ of single-line files. You can create a python generator function that
265
+ follows either the following `Protocol` or `TypedDict`.
266
+
267
+ ```py
268
+ from typing import Protocol, TypedDict, Generator, NotRequired, List
269
+ from pathlib import Path
270
+ from lograder.tests import make_tests_from_generator
271
+
272
+
273
+ # Your generator may return objects following the protocol...
274
+ class TestCaseProtocol(Protocol):
275
+ def get_name(self): ...
276
+
277
+ def get_input(self): ...
278
+
279
+ def get_expected_output(self): ...
280
+
281
+ class FlaggedTestCaseProtocol(TestCaseProtocol, Protocol):
282
+ def get_flags(self) -> List[str | Path]: ...
283
+
284
+ # Notice that TestCaseProtocol defaults to equal-weights
285
+ class WeightedTestCaseProtocol(TestCaseProtocol, Protocol):
286
+ def get_weight(self): ...
287
+
288
+ # ... or you can directly return a dict with the following keys.
289
+ class TestCaseDict(TypedDict):
290
+ name: str
291
+ input: str
292
+ expected_output: str
293
+ weight: NotRequired[float] # Defaults to 1.0, a.k.a. equal-weight.
294
+ flags: NotRequired[List[str | Path]]
295
+
296
+
297
+ # Here's an example of the syntax as well as the required
298
+ # signature of such a method:
299
+ @make_tests_from_generator
300
+ def test_suite_1() -> Generator[TestCaseProtocol | WeightedTestCaseProtocol | TestCaseDict, None, None]:
301
+ pass
302
+
303
+ # You'll have to query the `TestRegistry` from `lograder.tests` to access these tests directly, though.
304
+ ```
305
+
306
+
307
+