pl-e2e-test 0.1.1__tar.gz → 0.1.3__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 (55) hide show
  1. {pl_e2e_test-0.1.1/src/pl_e2e_test.egg-info → pl_e2e_test-0.1.3}/PKG-INFO +79 -11
  2. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/README.md +78 -11
  3. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/pyproject.toml +3 -5
  4. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3/src/pl_e2e_test.egg-info}/PKG-INFO +79 -11
  5. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/src/pl_e2e_test.egg-info/SOURCES.txt +4 -0
  6. pl_e2e_test-0.1.3/src/pl_e2e_test.egg-info/entry_points.txt +2 -0
  7. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/src/pl_e2e_test.egg-info/requires.txt +1 -0
  8. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/src/prairielearn_e2e/__init__.py +2 -0
  9. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/src/prairielearn_e2e/__main__.py +1 -1
  10. pl_e2e_test-0.1.3/src/prairielearn_e2e/cli.py +58 -0
  11. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/src/prairielearn_e2e/fuzz.py +2 -2
  12. pl_e2e_test-0.1.3/src/prairielearn_e2e/parallel.py +250 -0
  13. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/src/prairielearn_e2e/regression.py +1 -1
  14. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/src/prairielearn_e2e/reporting.py +3 -2
  15. pl_e2e_test-0.1.3/tests/test_cli.py +81 -0
  16. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/tests/test_fuzz.py +1 -1
  17. pl_e2e_test-0.1.3/tests/test_parallel.py +400 -0
  18. pl_e2e_test-0.1.1/src/pl_e2e_test.egg-info/entry_points.txt +0 -5
  19. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/LICENSE +0 -0
  20. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/MANIFEST.in +0 -0
  21. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/setup.cfg +0 -0
  22. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/src/pl_e2e_test.egg-info/dependency_links.txt +0 -0
  23. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/src/pl_e2e_test.egg-info/top_level.txt +0 -0
  24. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/src/prairielearn_e2e/_git.py +0 -0
  25. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/src/prairielearn_e2e/backend.py +0 -0
  26. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/src/prairielearn_e2e/config.json +0 -0
  27. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/src/prairielearn_e2e/config.py +0 -0
  28. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/src/prairielearn_e2e/contract.py +0 -0
  29. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/src/prairielearn_e2e/discovery.py +0 -0
  30. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/src/prairielearn_e2e/errors.py +0 -0
  31. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/src/prairielearn_e2e/harness.py +0 -0
  32. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/src/prairielearn_e2e/html_checks.py +0 -0
  33. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/src/prairielearn_e2e/metadata.py +0 -0
  34. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/src/prairielearn_e2e/pytest_plugin.py +0 -0
  35. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/src/prairielearn_e2e/question_tests.py +0 -0
  36. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/src/prairielearn_e2e/repository.py +0 -0
  37. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/src/prairielearn_e2e/runner.py +0 -0
  38. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/src/prairielearn_e2e/server.py +0 -0
  39. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/src/prairielearn_e2e/sharding.py +0 -0
  40. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/tests/__init__.py +0 -0
  41. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/tests/conftest.py +0 -0
  42. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/tests/stub_git.py +0 -0
  43. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/tests/test_contract.py +0 -0
  44. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/tests/test_discovery.py +0 -0
  45. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/tests/test_git.py +0 -0
  46. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/tests/test_harness.py +0 -0
  47. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/tests/test_html_checks.py +0 -0
  48. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/tests/test_metadata.py +0 -0
  49. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/tests/test_publish_version.py +0 -0
  50. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/tests/test_pytest_plugin.py +0 -0
  51. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/tests/test_regression.py +0 -0
  52. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/tests/test_reporting.py +0 -0
  53. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/tests/test_repository.py +0 -0
  54. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/tests/test_server.py +0 -0
  55. {pl_e2e_test-0.1.1 → pl_e2e_test-0.1.3}/tests/test_sharding.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pl-e2e-test
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: Fuzz PrairieLearn question lifecycles against the real Python element runtime
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/PrairieLearn/pl-e2e-test
@@ -18,6 +18,7 @@ Requires-Dist: platformdirs>=4.5
18
18
  Requires-Dist: prairielearn
19
19
  Requires-Dist: pyright>=1.1.414
20
20
  Requires-Dist: pytest>=9.1
21
+ Requires-Dist: rich>=14
21
22
  Requires-Dist: ruff>=0.16
22
23
  Dynamic: license-file
23
24
 
@@ -28,12 +29,17 @@ generate, prepare, render, parse, grade, test, and file phases. It was extracted
28
29
  from the `pl-oer-lv102` course so that courses can install one uv-managed tool
29
30
  instead of copying the harness and its support scripts.
30
31
 
31
- The package installs the reusable `prairielearn_e2e` Python API and these commands:
32
+ The package installs the reusable `prairielearn_e2e` Python API and a single
33
+ `pl-e2e-test` command:
32
34
 
33
- - `pl-e2e-test` runs the installed lifecycle pytest suite.
34
- - `pl-e2e-test-diff` fuzzes questions selected from a Git diff.
35
- - `pl-e2e-add-regression-seed` records a failing concrete variant seed.
36
- - `pl-e2e-merge-reports` merges parallel shard results into terminal and JUnit reports.
35
+ - `pl-e2e-test` (no command, or any pytest/harness options) runs the installed
36
+ lifecycle pytest suite.
37
+ - `pl-e2e-test diff` fuzzes questions selected from a Git diff.
38
+ - `pl-e2e-test parallel` runs local shards and merges their results.
39
+ - `pl-e2e-test add-seed` records a failing concrete variant seed.
40
+ - `pl-e2e-test reports` merges parallel shard results into terminal and JUnit reports.
41
+ - `pl-e2e-test version` prints the installed version.
42
+ - `pl-e2e-test help [COMMAND]` prints top-level help, or one command's help.
37
43
 
38
44
  ## Requirements and installation
39
45
 
@@ -92,6 +98,68 @@ uv run pl-e2e-test \
92
98
  variant set with `--fuzz-seed N`. Without either option, deterministic seeds start at
93
99
  zero. Question metadata with `singleVariant: true` always uses seed zero.
94
100
 
101
+ ### Local parallel runs
102
+
103
+ Run multiple shards in one machine or CI job, with six concurrent processes by default:
104
+
105
+ ```sh
106
+ uv run pl-e2e-test parallel --shard-count 6 run -- \
107
+ --question-path questions --fuzz-seeds
108
+
109
+ uv run pl-e2e-test parallel --shard-count 6 --junitxml results.xml diff -- \
110
+ --base main --fuzz-seed 42
111
+ ```
112
+
113
+ Launcher options go before the required `run` or `diff` selector; arguments after
114
+ `--` are forwarded to that command. Normal runs remain deterministic. For
115
+ `--fuzz-seeds` and diff runs, the launcher generates one shared master seed unless
116
+ `--fuzz-seed` is supplied, and prints a command for replaying the run without sharding.
117
+
118
+ Each shard runs in a separate Python process using the current environment and
119
+ working directory. Terminals show Rich progress; redirected output prints completion
120
+ lines. All shards finish before reports are merged, even when a test fails. Captured
121
+ failure output and successful stderr are shown before the combined summary.
122
+ Interrupting the launcher stops and reaps its children.
123
+
124
+ Intermediate reports are temporary. Pass the launcher's `--junitxml` option to retain
125
+ merged JUnit output. Forwarded `--shard`, `--shard-report`, `--junitxml`, and
126
+ `--junit-xml` options are rejected because the launcher manages those outputs.
127
+ Choose the course with the working directory, or `course_root` in the Python API.
128
+
129
+ The same orchestration is available to Python callers:
130
+
131
+ ```python
132
+ from prairielearn_e2e import run_parallel
133
+
134
+ status = run_parallel(
135
+ ["--question-path", "questions", "--fuzz-seeds"],
136
+ command="run",
137
+ shard_count=6,
138
+ course_root="/path/to/course",
139
+ junitxml="results.xml",
140
+ )
141
+ ```
142
+
143
+ `run_parallel` returns an integer exit status, including 130 on interruption.
144
+ Relative JUnit paths are resolved against the course root. With `junitxml=None`,
145
+ merged output is temporary. Invalid launcher settings raise `ValueError`.
146
+
147
+ Courses using `scripts/run_e2e_shards.py` can replace its invocation with
148
+ `pl-e2e-test parallel`, move `--fuzz-seed` into the forwarded arguments, and explicitly
149
+ pass `--fuzz-seeds` for full-suite runs to preserve the script's fuzzing behavior.
150
+ For example:
151
+
152
+ ```make
153
+ test-e2e-sharded:
154
+ uv run --active pl-e2e-test parallel --shard-count "$(E2E_SHARD_COUNT)" run -- \
155
+ $(E2E_TEST_ARGS) $(E2E_PRAIRIELEARN_ARG) --fuzz-seeds $(E2E_FUZZ_SEED_ARG) $(PYTEST_ARGS)
156
+
157
+ test-e2e-fuzz-sharded:
158
+ uv run --active pl-e2e-test parallel --shard-count "$(E2E_SHARD_COUNT)" diff -- \
159
+ --base "$(E2E_DIFF_BASE)" --head "$(E2E_DIFF_HEAD)" \
160
+ --seed-count "$(E2E_FUZZ_SEED_COUNT)" $(E2E_FUZZ_SEED_ARG) $(E2E_PRAIRIELEARN_ARG) $(PYTEST_ARGS)
161
+ ```
162
+
95
163
  ### CI sharding
96
164
 
97
165
  Use `--shard INDEX/COUNT` to divide the selected question variants between parallel
@@ -105,7 +173,7 @@ same checkout and arguments. Sharded fuzz runs must also use one explicit shared
105
173
  merge them into a combined terminal summary and JUnit report:
106
174
 
107
175
  ```sh
108
- uv run pl-e2e-merge-reports e2e-reports \
176
+ uv run pl-e2e-test reports e2e-reports \
109
177
  --junitxml pl-e2e-results.xml
110
178
  ```
111
179
 
@@ -161,7 +229,7 @@ jobs:
161
229
  merge-multiple: true
162
230
  - name: Merge reports
163
231
  run: |
164
- uv run pl-e2e-merge-reports e2e-reports \
232
+ uv run pl-e2e-test reports e2e-reports \
165
233
  --junitxml pl-e2e-results.xml
166
234
  - name: Upload merged JUnit report
167
235
  if: ${{ always() }}
@@ -208,7 +276,7 @@ questions automatically skip only the grade check.
208
276
  Record the concrete seed shown in a failing pytest case with:
209
277
 
210
278
  ```sh
211
- uv run pl-e2e-add-regression-seed \
279
+ uv run pl-e2e-test add-seed \
212
280
  questions/chapter/example 1843927501
213
281
  ```
214
282
 
@@ -221,7 +289,7 @@ The diff runner tests changed question directories. A non-ignored changed file o
221
289
  `questions/` selects every question:
222
290
 
223
291
  ```sh
224
- uv run pl-e2e-test-diff \
292
+ uv run pl-e2e-test diff \
225
293
  --base origin/main \
226
294
  --head HEAD \
227
295
  --seed-count 10
@@ -247,7 +315,7 @@ To migrate from `.question-e2e.diffignore`, copy its non-comment rules into the
247
315
 
248
316
  In pull-request CI, fetch the base commit before running the command and pass the
249
317
  appropriate base and head SHAs. Set `--fuzz-seed` to replay a reported run.
250
- `pl-e2e-test-diff` accepts the same `--shard` and `--shard-report` options. If a diff
318
+ `pl-e2e-test diff` accepts the same `--shard` and `--shard-report` options. If a diff
251
319
  selects no questions, it writes a successful empty shard report so the merge job can
252
320
  still verify that every matrix job completed.
253
321
 
@@ -5,12 +5,17 @@ generate, prepare, render, parse, grade, test, and file phases. It was extracted
5
5
  from the `pl-oer-lv102` course so that courses can install one uv-managed tool
6
6
  instead of copying the harness and its support scripts.
7
7
 
8
- The package installs the reusable `prairielearn_e2e` Python API and these commands:
9
-
10
- - `pl-e2e-test` runs the installed lifecycle pytest suite.
11
- - `pl-e2e-test-diff` fuzzes questions selected from a Git diff.
12
- - `pl-e2e-add-regression-seed` records a failing concrete variant seed.
13
- - `pl-e2e-merge-reports` merges parallel shard results into terminal and JUnit reports.
8
+ The package installs the reusable `prairielearn_e2e` Python API and a single
9
+ `pl-e2e-test` command:
10
+
11
+ - `pl-e2e-test` (no command, or any pytest/harness options) runs the installed
12
+ lifecycle pytest suite.
13
+ - `pl-e2e-test diff` fuzzes questions selected from a Git diff.
14
+ - `pl-e2e-test parallel` runs local shards and merges their results.
15
+ - `pl-e2e-test add-seed` records a failing concrete variant seed.
16
+ - `pl-e2e-test reports` merges parallel shard results into terminal and JUnit reports.
17
+ - `pl-e2e-test version` prints the installed version.
18
+ - `pl-e2e-test help [COMMAND]` prints top-level help, or one command's help.
14
19
 
15
20
  ## Requirements and installation
16
21
 
@@ -69,6 +74,68 @@ uv run pl-e2e-test \
69
74
  variant set with `--fuzz-seed N`. Without either option, deterministic seeds start at
70
75
  zero. Question metadata with `singleVariant: true` always uses seed zero.
71
76
 
77
+ ### Local parallel runs
78
+
79
+ Run multiple shards in one machine or CI job, with six concurrent processes by default:
80
+
81
+ ```sh
82
+ uv run pl-e2e-test parallel --shard-count 6 run -- \
83
+ --question-path questions --fuzz-seeds
84
+
85
+ uv run pl-e2e-test parallel --shard-count 6 --junitxml results.xml diff -- \
86
+ --base main --fuzz-seed 42
87
+ ```
88
+
89
+ Launcher options go before the required `run` or `diff` selector; arguments after
90
+ `--` are forwarded to that command. Normal runs remain deterministic. For
91
+ `--fuzz-seeds` and diff runs, the launcher generates one shared master seed unless
92
+ `--fuzz-seed` is supplied, and prints a command for replaying the run without sharding.
93
+
94
+ Each shard runs in a separate Python process using the current environment and
95
+ working directory. Terminals show Rich progress; redirected output prints completion
96
+ lines. All shards finish before reports are merged, even when a test fails. Captured
97
+ failure output and successful stderr are shown before the combined summary.
98
+ Interrupting the launcher stops and reaps its children.
99
+
100
+ Intermediate reports are temporary. Pass the launcher's `--junitxml` option to retain
101
+ merged JUnit output. Forwarded `--shard`, `--shard-report`, `--junitxml`, and
102
+ `--junit-xml` options are rejected because the launcher manages those outputs.
103
+ Choose the course with the working directory, or `course_root` in the Python API.
104
+
105
+ The same orchestration is available to Python callers:
106
+
107
+ ```python
108
+ from prairielearn_e2e import run_parallel
109
+
110
+ status = run_parallel(
111
+ ["--question-path", "questions", "--fuzz-seeds"],
112
+ command="run",
113
+ shard_count=6,
114
+ course_root="/path/to/course",
115
+ junitxml="results.xml",
116
+ )
117
+ ```
118
+
119
+ `run_parallel` returns an integer exit status, including 130 on interruption.
120
+ Relative JUnit paths are resolved against the course root. With `junitxml=None`,
121
+ merged output is temporary. Invalid launcher settings raise `ValueError`.
122
+
123
+ Courses using `scripts/run_e2e_shards.py` can replace its invocation with
124
+ `pl-e2e-test parallel`, move `--fuzz-seed` into the forwarded arguments, and explicitly
125
+ pass `--fuzz-seeds` for full-suite runs to preserve the script's fuzzing behavior.
126
+ For example:
127
+
128
+ ```make
129
+ test-e2e-sharded:
130
+ uv run --active pl-e2e-test parallel --shard-count "$(E2E_SHARD_COUNT)" run -- \
131
+ $(E2E_TEST_ARGS) $(E2E_PRAIRIELEARN_ARG) --fuzz-seeds $(E2E_FUZZ_SEED_ARG) $(PYTEST_ARGS)
132
+
133
+ test-e2e-fuzz-sharded:
134
+ uv run --active pl-e2e-test parallel --shard-count "$(E2E_SHARD_COUNT)" diff -- \
135
+ --base "$(E2E_DIFF_BASE)" --head "$(E2E_DIFF_HEAD)" \
136
+ --seed-count "$(E2E_FUZZ_SEED_COUNT)" $(E2E_FUZZ_SEED_ARG) $(E2E_PRAIRIELEARN_ARG) $(PYTEST_ARGS)
137
+ ```
138
+
72
139
  ### CI sharding
73
140
 
74
141
  Use `--shard INDEX/COUNT` to divide the selected question variants between parallel
@@ -82,7 +149,7 @@ same checkout and arguments. Sharded fuzz runs must also use one explicit shared
82
149
  merge them into a combined terminal summary and JUnit report:
83
150
 
84
151
  ```sh
85
- uv run pl-e2e-merge-reports e2e-reports \
152
+ uv run pl-e2e-test reports e2e-reports \
86
153
  --junitxml pl-e2e-results.xml
87
154
  ```
88
155
 
@@ -138,7 +205,7 @@ jobs:
138
205
  merge-multiple: true
139
206
  - name: Merge reports
140
207
  run: |
141
- uv run pl-e2e-merge-reports e2e-reports \
208
+ uv run pl-e2e-test reports e2e-reports \
142
209
  --junitxml pl-e2e-results.xml
143
210
  - name: Upload merged JUnit report
144
211
  if: ${{ always() }}
@@ -185,7 +252,7 @@ questions automatically skip only the grade check.
185
252
  Record the concrete seed shown in a failing pytest case with:
186
253
 
187
254
  ```sh
188
- uv run pl-e2e-add-regression-seed \
255
+ uv run pl-e2e-test add-seed \
189
256
  questions/chapter/example 1843927501
190
257
  ```
191
258
 
@@ -198,7 +265,7 @@ The diff runner tests changed question directories. A non-ignored changed file o
198
265
  `questions/` selects every question:
199
266
 
200
267
  ```sh
201
- uv run pl-e2e-test-diff \
268
+ uv run pl-e2e-test diff \
202
269
  --base origin/main \
203
270
  --head HEAD \
204
271
  --seed-count 10
@@ -224,7 +291,7 @@ To migrate from `.question-e2e.diffignore`, copy its non-comment rules into the
224
291
 
225
292
  In pull-request CI, fetch the base commit before running the command and pass the
226
293
  appropriate base and head SHAs. Set `--fuzz-seed` to replay a reported run.
227
- `pl-e2e-test-diff` accepts the same `--shard` and `--shard-report` options. If a diff
294
+ `pl-e2e-test diff` accepts the same `--shard` and `--shard-report` options. If a diff
228
295
  selects no questions, it writes a successful empty shard report so the merge job can
229
296
  still verify that every matrix job completed.
230
297
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pl-e2e-test"
7
- version = "0.1.1"
7
+ version = "0.1.3"
8
8
  description = "Fuzz PrairieLearn question lifecycles against the real Python element runtime"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -19,6 +19,7 @@ dependencies = [
19
19
  "prairielearn",
20
20
  "pyright>=1.1.414",
21
21
  "pytest>=9.1",
22
+ "rich>=14",
22
23
  "ruff>=0.16",
23
24
  ]
24
25
 
@@ -31,10 +32,7 @@ Issues = "https://github.com/PrairieLearn/pl-e2e-test/issues"
31
32
  Repository = "https://github.com/PrairieLearn/pl-e2e-test"
32
33
 
33
34
  [project.scripts]
34
- pl-e2e-test = "prairielearn_e2e.runner:main"
35
- pl-e2e-test-diff = "prairielearn_e2e.fuzz:main"
36
- pl-e2e-add-regression-seed = "prairielearn_e2e.regression:main"
37
- pl-e2e-merge-reports = "prairielearn_e2e.reporting:main"
35
+ pl-e2e-test = "prairielearn_e2e.cli:main"
38
36
 
39
37
  [tool.setuptools.packages.find]
40
38
  where = ["src"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pl-e2e-test
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: Fuzz PrairieLearn question lifecycles against the real Python element runtime
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/PrairieLearn/pl-e2e-test
@@ -18,6 +18,7 @@ Requires-Dist: platformdirs>=4.5
18
18
  Requires-Dist: prairielearn
19
19
  Requires-Dist: pyright>=1.1.414
20
20
  Requires-Dist: pytest>=9.1
21
+ Requires-Dist: rich>=14
21
22
  Requires-Dist: ruff>=0.16
22
23
  Dynamic: license-file
23
24
 
@@ -28,12 +29,17 @@ generate, prepare, render, parse, grade, test, and file phases. It was extracted
28
29
  from the `pl-oer-lv102` course so that courses can install one uv-managed tool
29
30
  instead of copying the harness and its support scripts.
30
31
 
31
- The package installs the reusable `prairielearn_e2e` Python API and these commands:
32
+ The package installs the reusable `prairielearn_e2e` Python API and a single
33
+ `pl-e2e-test` command:
32
34
 
33
- - `pl-e2e-test` runs the installed lifecycle pytest suite.
34
- - `pl-e2e-test-diff` fuzzes questions selected from a Git diff.
35
- - `pl-e2e-add-regression-seed` records a failing concrete variant seed.
36
- - `pl-e2e-merge-reports` merges parallel shard results into terminal and JUnit reports.
35
+ - `pl-e2e-test` (no command, or any pytest/harness options) runs the installed
36
+ lifecycle pytest suite.
37
+ - `pl-e2e-test diff` fuzzes questions selected from a Git diff.
38
+ - `pl-e2e-test parallel` runs local shards and merges their results.
39
+ - `pl-e2e-test add-seed` records a failing concrete variant seed.
40
+ - `pl-e2e-test reports` merges parallel shard results into terminal and JUnit reports.
41
+ - `pl-e2e-test version` prints the installed version.
42
+ - `pl-e2e-test help [COMMAND]` prints top-level help, or one command's help.
37
43
 
38
44
  ## Requirements and installation
39
45
 
@@ -92,6 +98,68 @@ uv run pl-e2e-test \
92
98
  variant set with `--fuzz-seed N`. Without either option, deterministic seeds start at
93
99
  zero. Question metadata with `singleVariant: true` always uses seed zero.
94
100
 
101
+ ### Local parallel runs
102
+
103
+ Run multiple shards in one machine or CI job, with six concurrent processes by default:
104
+
105
+ ```sh
106
+ uv run pl-e2e-test parallel --shard-count 6 run -- \
107
+ --question-path questions --fuzz-seeds
108
+
109
+ uv run pl-e2e-test parallel --shard-count 6 --junitxml results.xml diff -- \
110
+ --base main --fuzz-seed 42
111
+ ```
112
+
113
+ Launcher options go before the required `run` or `diff` selector; arguments after
114
+ `--` are forwarded to that command. Normal runs remain deterministic. For
115
+ `--fuzz-seeds` and diff runs, the launcher generates one shared master seed unless
116
+ `--fuzz-seed` is supplied, and prints a command for replaying the run without sharding.
117
+
118
+ Each shard runs in a separate Python process using the current environment and
119
+ working directory. Terminals show Rich progress; redirected output prints completion
120
+ lines. All shards finish before reports are merged, even when a test fails. Captured
121
+ failure output and successful stderr are shown before the combined summary.
122
+ Interrupting the launcher stops and reaps its children.
123
+
124
+ Intermediate reports are temporary. Pass the launcher's `--junitxml` option to retain
125
+ merged JUnit output. Forwarded `--shard`, `--shard-report`, `--junitxml`, and
126
+ `--junit-xml` options are rejected because the launcher manages those outputs.
127
+ Choose the course with the working directory, or `course_root` in the Python API.
128
+
129
+ The same orchestration is available to Python callers:
130
+
131
+ ```python
132
+ from prairielearn_e2e import run_parallel
133
+
134
+ status = run_parallel(
135
+ ["--question-path", "questions", "--fuzz-seeds"],
136
+ command="run",
137
+ shard_count=6,
138
+ course_root="/path/to/course",
139
+ junitxml="results.xml",
140
+ )
141
+ ```
142
+
143
+ `run_parallel` returns an integer exit status, including 130 on interruption.
144
+ Relative JUnit paths are resolved against the course root. With `junitxml=None`,
145
+ merged output is temporary. Invalid launcher settings raise `ValueError`.
146
+
147
+ Courses using `scripts/run_e2e_shards.py` can replace its invocation with
148
+ `pl-e2e-test parallel`, move `--fuzz-seed` into the forwarded arguments, and explicitly
149
+ pass `--fuzz-seeds` for full-suite runs to preserve the script's fuzzing behavior.
150
+ For example:
151
+
152
+ ```make
153
+ test-e2e-sharded:
154
+ uv run --active pl-e2e-test parallel --shard-count "$(E2E_SHARD_COUNT)" run -- \
155
+ $(E2E_TEST_ARGS) $(E2E_PRAIRIELEARN_ARG) --fuzz-seeds $(E2E_FUZZ_SEED_ARG) $(PYTEST_ARGS)
156
+
157
+ test-e2e-fuzz-sharded:
158
+ uv run --active pl-e2e-test parallel --shard-count "$(E2E_SHARD_COUNT)" diff -- \
159
+ --base "$(E2E_DIFF_BASE)" --head "$(E2E_DIFF_HEAD)" \
160
+ --seed-count "$(E2E_FUZZ_SEED_COUNT)" $(E2E_FUZZ_SEED_ARG) $(E2E_PRAIRIELEARN_ARG) $(PYTEST_ARGS)
161
+ ```
162
+
95
163
  ### CI sharding
96
164
 
97
165
  Use `--shard INDEX/COUNT` to divide the selected question variants between parallel
@@ -105,7 +173,7 @@ same checkout and arguments. Sharded fuzz runs must also use one explicit shared
105
173
  merge them into a combined terminal summary and JUnit report:
106
174
 
107
175
  ```sh
108
- uv run pl-e2e-merge-reports e2e-reports \
176
+ uv run pl-e2e-test reports e2e-reports \
109
177
  --junitxml pl-e2e-results.xml
110
178
  ```
111
179
 
@@ -161,7 +229,7 @@ jobs:
161
229
  merge-multiple: true
162
230
  - name: Merge reports
163
231
  run: |
164
- uv run pl-e2e-merge-reports e2e-reports \
232
+ uv run pl-e2e-test reports e2e-reports \
165
233
  --junitxml pl-e2e-results.xml
166
234
  - name: Upload merged JUnit report
167
235
  if: ${{ always() }}
@@ -208,7 +276,7 @@ questions automatically skip only the grade check.
208
276
  Record the concrete seed shown in a failing pytest case with:
209
277
 
210
278
  ```sh
211
- uv run pl-e2e-add-regression-seed \
279
+ uv run pl-e2e-test add-seed \
212
280
  questions/chapter/example 1843927501
213
281
  ```
214
282
 
@@ -221,7 +289,7 @@ The diff runner tests changed question directories. A non-ignored changed file o
221
289
  `questions/` selects every question:
222
290
 
223
291
  ```sh
224
- uv run pl-e2e-test-diff \
292
+ uv run pl-e2e-test diff \
225
293
  --base origin/main \
226
294
  --head HEAD \
227
295
  --seed-count 10
@@ -247,7 +315,7 @@ To migrate from `.question-e2e.diffignore`, copy its non-comment rules into the
247
315
 
248
316
  In pull-request CI, fetch the base commit before running the command and pass the
249
317
  appropriate base and head SHAs. Set `--fuzz-seed` to replay a reported run.
250
- `pl-e2e-test-diff` accepts the same `--shard` and `--shard-report` options. If a diff
318
+ `pl-e2e-test diff` accepts the same `--shard` and `--shard-report` options. If a diff
251
319
  selects no questions, it writes a successful empty shard report so the merge job can
252
320
  still verify that every matrix job completed.
253
321
 
@@ -12,6 +12,7 @@ src/prairielearn_e2e/__init__.py
12
12
  src/prairielearn_e2e/__main__.py
13
13
  src/prairielearn_e2e/_git.py
14
14
  src/prairielearn_e2e/backend.py
15
+ src/prairielearn_e2e/cli.py
15
16
  src/prairielearn_e2e/config.json
16
17
  src/prairielearn_e2e/config.py
17
18
  src/prairielearn_e2e/contract.py
@@ -21,6 +22,7 @@ src/prairielearn_e2e/fuzz.py
21
22
  src/prairielearn_e2e/harness.py
22
23
  src/prairielearn_e2e/html_checks.py
23
24
  src/prairielearn_e2e/metadata.py
25
+ src/prairielearn_e2e/parallel.py
24
26
  src/prairielearn_e2e/pytest_plugin.py
25
27
  src/prairielearn_e2e/question_tests.py
26
28
  src/prairielearn_e2e/regression.py
@@ -32,6 +34,7 @@ src/prairielearn_e2e/sharding.py
32
34
  tests/__init__.py
33
35
  tests/conftest.py
34
36
  tests/stub_git.py
37
+ tests/test_cli.py
35
38
  tests/test_contract.py
36
39
  tests/test_discovery.py
37
40
  tests/test_fuzz.py
@@ -39,6 +42,7 @@ tests/test_git.py
39
42
  tests/test_harness.py
40
43
  tests/test_html_checks.py
41
44
  tests/test_metadata.py
45
+ tests/test_parallel.py
42
46
  tests/test_publish_version.py
43
47
  tests/test_pytest_plugin.py
44
48
  tests/test_regression.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pl-e2e-test = prairielearn_e2e.cli:main
@@ -7,4 +7,5 @@ platformdirs>=4.5
7
7
  prairielearn
8
8
  pyright>=1.1.414
9
9
  pytest>=9.1
10
+ rich>=14
10
11
  ruff>=0.16
@@ -5,6 +5,7 @@ from importlib.metadata import version
5
5
  from .backend import PrairieLearnBackend
6
6
  from .discovery import QuestionCase, discover_questions
7
7
  from .harness import LifecycleResult, QuestionHarness
8
+ from .parallel import run_parallel
8
9
 
9
10
  __version__ = version("pl-e2e-test")
10
11
 
@@ -15,4 +16,5 @@ __all__ = (
15
16
  "QuestionHarness",
16
17
  "__version__",
17
18
  "discover_questions",
19
+ "run_parallel",
18
20
  )
@@ -1,3 +1,3 @@
1
- from .runner import main
1
+ from .cli import main
2
2
 
3
3
  raise SystemExit(main())
@@ -0,0 +1,58 @@
1
+ """Top-level command dispatch for the ``pl-e2e-test`` console script."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from collections.abc import Sequence
7
+ from importlib.metadata import version
8
+
9
+ from . import fuzz, parallel, regression, reporting, runner
10
+
11
+ _SUBCOMMANDS = {
12
+ "diff": fuzz.main,
13
+ "parallel": parallel.main,
14
+ "add-seed": regression.main,
15
+ "reports": reporting.main,
16
+ }
17
+
18
+ _TOP_LEVEL_HELP = """\
19
+ usage: pl-e2e-test [COMMAND] [ARGS...]
20
+
21
+ With no COMMAND, runs the installed PrairieLearn question lifecycle test
22
+ suite; any arguments (pytest options, --question-path, --seed-count, ...)
23
+ are passed straight through to pytest.
24
+
25
+ Commands:
26
+ parallel run local shards and merge their results
27
+ diff fuzz questions changed between two Git refs
28
+ add-seed record a concrete regression seed for a question
29
+ reports merge parallel shard reports into one terminal/JUnit report
30
+ version show the installed pl-e2e-test version
31
+ help show this message, or `pl-e2e-test help COMMAND` for one command
32
+ """
33
+
34
+
35
+ def _version_command(_argv: Sequence[str]) -> int:
36
+ print(f"pl-e2e-test {version('pl-e2e-test')}")
37
+ return 0
38
+
39
+
40
+ def _help_command(argv: Sequence[str]) -> int:
41
+ if argv and argv[0] in _SUBCOMMANDS:
42
+ return _SUBCOMMANDS[argv[0]](["--help"])
43
+ print(_TOP_LEVEL_HELP, end="")
44
+ return 0
45
+
46
+
47
+ _COMMANDS = {**_SUBCOMMANDS, "version": _version_command, "help": _help_command}
48
+
49
+
50
+ def main(argv: Sequence[str] | None = None) -> int:
51
+ arguments = list(sys.argv[1:] if argv is None else argv)
52
+ if arguments and arguments[0] in _COMMANDS:
53
+ return _COMMANDS[arguments[0]](arguments[1:])
54
+ return runner.main(arguments)
55
+
56
+
57
+ if __name__ == "__main__":
58
+ raise SystemExit(main())
@@ -195,7 +195,7 @@ def positive_integer(value: str) -> int:
195
195
 
196
196
 
197
197
  def main(argv: Sequence[str] | None = None) -> int:
198
- parser = argparse.ArgumentParser(description=__doc__)
198
+ parser = argparse.ArgumentParser(prog="pl-e2e-test diff", description=__doc__)
199
199
  parser.add_argument("--course-root", type=Path)
200
200
  parser.add_argument("--base", default="main", help="base Git ref (default: main)")
201
201
  parser.add_argument("--head", default="HEAD", help="head Git ref (default: HEAD)")
@@ -274,7 +274,7 @@ def main(argv: Sequence[str] | None = None) -> int:
274
274
  seeds = variant_seeds({}, args.seed_count, fuzz_seed)
275
275
  print(f"E2E fuzz seed: {fuzz_seed}")
276
276
  print(f"Variant seeds: {', '.join(str(seed) for seed in seeds)}")
277
- replay_arguments = ["pl-e2e-test-diff", "--fuzz-seed", str(fuzz_seed)]
277
+ replay_arguments = ["pl-e2e-test", "diff", "--fuzz-seed", str(fuzz_seed)]
278
278
  test_arguments: list[str] = []
279
279
  if args.prairielearn_path is not None:
280
280
  replay_arguments.extend(