pl-e2e-test 0.2.0__tar.gz → 0.2.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.
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/MANIFEST.in +1 -0
- {pl_e2e_test-0.2.0/src/pl_e2e_test.egg-info → pl_e2e_test-0.2.2}/PKG-INFO +76 -26
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/README.md +73 -25
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/pyproject.toml +3 -1
- pl_e2e_test-0.2.2/scripts/demo_cli.py +219 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2/src/pl_e2e_test.egg-info}/PKG-INFO +76 -26
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/src/pl_e2e_test.egg-info/SOURCES.txt +5 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/src/pl_e2e_test.egg-info/requires.txt +2 -0
- pl_e2e_test-0.2.2/src/prairielearn_e2e/_options.py +29 -0
- pl_e2e_test-0.2.2/src/prairielearn_e2e/cli.py +721 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/config.py +77 -5
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/fuzz.py +116 -163
- pl_e2e_test-0.2.2/src/prairielearn_e2e/instances.py +199 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/parallel.py +114 -84
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/pytest_plugin.py +1 -1
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/regression.py +22 -31
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/reporting.py +353 -32
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/repository.py +72 -6
- pl_e2e_test-0.2.2/src/prairielearn_e2e/runner.py +82 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/runtime.py +41 -18
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/sharding.py +4 -5
- pl_e2e_test-0.2.2/src/prairielearn_e2e/ui.py +158 -0
- pl_e2e_test-0.2.2/tests/test_cli.py +294 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/tests/test_config.py +41 -1
- pl_e2e_test-0.2.2/tests/test_demo_cli.py +64 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/tests/test_fuzz.py +54 -37
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/tests/test_instances.py +134 -53
- pl_e2e_test-0.2.2/tests/test_options.py +17 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/tests/test_parallel.py +135 -26
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/tests/test_pytest_plugin.py +232 -1
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/tests/test_regression.py +1 -2
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/tests/test_reporting.py +25 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/tests/test_repository.py +106 -1
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/tests/test_runtime.py +25 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/tests/test_sharding.py +1 -2
- pl_e2e_test-0.2.0/src/prairielearn_e2e/cli.py +0 -89
- pl_e2e_test-0.2.0/src/prairielearn_e2e/instances.py +0 -161
- pl_e2e_test-0.2.0/src/prairielearn_e2e/runner.py +0 -48
- pl_e2e_test-0.2.0/tests/test_cli.py +0 -91
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/LICENSE +0 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/setup.cfg +0 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/src/pl_e2e_test.egg-info/dependency_links.txt +0 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/src/pl_e2e_test.egg-info/entry_points.txt +0 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/src/pl_e2e_test.egg-info/top_level.txt +0 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/__init__.py +0 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/__main__.py +0 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/_git.py +0 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/backend.py +0 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/contract.py +0 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/discovery.py +0 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/errors.py +0 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/expected_errors.py +0 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/harness.py +0 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/html_checks.py +0 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/metadata.py +0 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/question_tests.py +0 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/server.py +0 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/tests/__init__.py +0 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/tests/conftest.py +0 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/tests/stub_git.py +0 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/tests/test_contract.py +0 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/tests/test_discovery.py +0 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/tests/test_git.py +0 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/tests/test_harness.py +0 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/tests/test_html_checks.py +0 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/tests/test_metadata.py +0 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/tests/test_publish_version.py +0 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/tests/test_question_tests.py +0 -0
- {pl_e2e_test-0.2.0 → pl_e2e_test-0.2.2}/tests/test_server.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pl-e2e-test
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
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
|
|
@@ -10,6 +10,7 @@ Requires-Python: >=3.13
|
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
11
|
License-File: LICENSE
|
|
12
12
|
Requires-Dist: chevron==0.14.0
|
|
13
|
+
Requires-Dist: cyclopts<6,>=5.3
|
|
13
14
|
Requires-Dist: filelock>=3.20
|
|
14
15
|
Requires-Dist: jsonschema>=4.26
|
|
15
16
|
Requires-Dist: lxml>=6.1
|
|
@@ -20,6 +21,7 @@ Requires-Dist: pyright>=1.1.414
|
|
|
20
21
|
Requires-Dist: pytest>=9.1
|
|
21
22
|
Requires-Dist: rich>=14
|
|
22
23
|
Requires-Dist: ruff>=0.16
|
|
24
|
+
Requires-Dist: tomlkit>=0.15.2
|
|
23
25
|
Dynamic: license-file
|
|
24
26
|
|
|
25
27
|
# pl-e2e-test
|
|
@@ -32,16 +34,18 @@ instead of copying the harness and its support scripts.
|
|
|
32
34
|
The package installs the reusable `prairielearn_e2e` Python API and a single
|
|
33
35
|
`pl-e2e-test` command:
|
|
34
36
|
|
|
35
|
-
- `pl-e2e-test`
|
|
36
|
-
lifecycle pytest suite.
|
|
37
|
+
- `pl-e2e-test run` runs the installed lifecycle pytest suite.
|
|
37
38
|
- `pl-e2e-test diff` fuzzes questions selected from a Git diff.
|
|
38
|
-
- `pl-e2e-test parallel`
|
|
39
|
+
- `pl-e2e-test parallel run` and `parallel diff` run local shards and merge them.
|
|
39
40
|
- `pl-e2e-test add-seed` records a failing concrete variant seed.
|
|
40
41
|
- `pl-e2e-test reports` merges parallel shard results into terminal and JUnit reports.
|
|
41
42
|
- `pl-e2e-test instances` shows configured, installed, cached, and local PrairieLearn
|
|
42
43
|
revisions.
|
|
43
|
-
- `pl-e2e-test version`
|
|
44
|
-
- `pl-e2e-test
|
|
44
|
+
- `pl-e2e-test version [REVISION]` reads or updates the configured PrairieLearn revision.
|
|
45
|
+
- `pl-e2e-test cache clean` clears the managed PrairieLearn instance cache.
|
|
46
|
+
- `pl-e2e-test --version` prints the installed CLI version.
|
|
47
|
+
- `pl-e2e-test help [COMMAND...]` prints top-level or nested command help. A bare
|
|
48
|
+
invocation also prints top-level help.
|
|
45
49
|
|
|
46
50
|
## Requirements and installation
|
|
47
51
|
|
|
@@ -83,20 +87,37 @@ Pass a question, a directory of questions, or an `info.json` file. Repeat
|
|
|
83
87
|
`--question-path` to select multiple roots:
|
|
84
88
|
|
|
85
89
|
```sh
|
|
86
|
-
uv run pl-e2e-test \
|
|
90
|
+
uv run pl-e2e-test run \
|
|
87
91
|
--question-path questions \
|
|
88
92
|
--seed-count 3
|
|
89
93
|
```
|
|
90
94
|
|
|
91
95
|
The installed suite exposes separate `generate`, `prepare`, `render`, and `grade`
|
|
92
|
-
pytest markers.
|
|
96
|
+
pytest markers. Put pytest selection and execution arguments after `--`:
|
|
93
97
|
|
|
94
98
|
```sh
|
|
95
|
-
uv run pl-e2e-test \
|
|
99
|
+
uv run pl-e2e-test -vv run \
|
|
96
100
|
--question-path questions/chapter/example \
|
|
97
|
-
--fuzz-seeds -m render -x
|
|
101
|
+
--fuzz-seeds -- -m render -x
|
|
98
102
|
```
|
|
99
103
|
|
|
104
|
+
The native reporter owns progress, capture, tracebacks, summaries, and color. Use
|
|
105
|
+
repeatable global `-q` or `-v` flags and `--color auto|always|never`; pytest
|
|
106
|
+
presentation flags such as `-q`, `-v`, `--tb`, `-r`, `--color`, and `-s` are rejected
|
|
107
|
+
after `--`. `NO_COLOR`, `FORCE_COLOR`, and `CLICOLOR_FORCE` are also honored.
|
|
108
|
+
|
|
109
|
+
To preview the terminal UX without a PrairieLearn course or network access, run:
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
uv run python scripts/demo_cli.py
|
|
113
|
+
uv run python scripts/demo_cli.py --color always mixed verbose cli-error
|
|
114
|
+
uv run python scripts/demo_cli.py --list
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The demo covers help and CLI errors plus passing, failing, skipped, xfailed/xpassed,
|
|
118
|
+
warning, collection-error, empty-selection, quiet, verbose, and aggregate shard progress
|
|
119
|
+
output.
|
|
120
|
+
|
|
100
121
|
`--fuzz-seeds` creates and reports a random 64-bit master seed. Reproduce the same
|
|
101
122
|
variant set with `--fuzz-seed N`. Without either option, deterministic seeds start at
|
|
102
123
|
zero. Question metadata with `singleVariant: true` always uses seed zero.
|
|
@@ -106,22 +127,23 @@ zero. Question metadata with `singleVariant: true` always uses seed zero.
|
|
|
106
127
|
Run multiple shards in one machine or CI job, with six concurrent processes by default:
|
|
107
128
|
|
|
108
129
|
```sh
|
|
109
|
-
uv run pl-e2e-test parallel --shard-count 6
|
|
130
|
+
uv run pl-e2e-test parallel run --shard-count 6 \
|
|
110
131
|
--question-path questions --fuzz-seeds
|
|
111
132
|
|
|
112
|
-
uv run pl-e2e-test parallel --shard-count 6 --junitxml results.xml
|
|
133
|
+
uv run pl-e2e-test parallel diff --shard-count 6 --junitxml results.xml \
|
|
113
134
|
--base main --fuzz-seed 42
|
|
114
135
|
```
|
|
115
136
|
|
|
116
|
-
|
|
117
|
-
|
|
137
|
+
Native options go after `parallel run` or `parallel diff` and before an optional `--`;
|
|
138
|
+
only pytest arguments follow that boundary. Normal runs remain deterministic. For
|
|
118
139
|
`--fuzz-seeds` and diff runs, the launcher generates one shared master seed unless
|
|
119
140
|
`--fuzz-seed` is supplied, and prints a command for replaying the run without sharding.
|
|
120
141
|
|
|
121
142
|
Each shard runs in a separate Python process using the current environment and
|
|
122
|
-
working directory. Terminals show
|
|
123
|
-
lines. All shards finish before reports are merged, even when a test
|
|
124
|
-
|
|
143
|
+
working directory. Terminals show aggregate progress; redirected output uses stable
|
|
144
|
+
start and summary lines. All shards finish before reports are merged, even when a test
|
|
145
|
+
fails. Raw child output is shown only when a launch or internal failure cannot produce
|
|
146
|
+
a usable report.
|
|
125
147
|
Interrupting the launcher stops and reaps its children.
|
|
126
148
|
|
|
127
149
|
Intermediate reports are temporary. Pass the launcher's `--junitxml` option to retain
|
|
@@ -148,19 +170,19 @@ Relative JUnit paths are resolved against the course root. With `junitxml=None`,
|
|
|
148
170
|
merged output is temporary. Invalid launcher settings raise `ValueError`.
|
|
149
171
|
|
|
150
172
|
Courses using `scripts/run_e2e_shards.py` can replace its invocation with
|
|
151
|
-
`pl-e2e-test parallel`, move `--fuzz-seed` into the
|
|
173
|
+
`pl-e2e-test parallel run`, move `--fuzz-seed` into the native arguments, and explicitly
|
|
152
174
|
pass `--fuzz-seeds` for full-suite runs to preserve the script's fuzzing behavior.
|
|
153
175
|
For example:
|
|
154
176
|
|
|
155
177
|
```make
|
|
156
178
|
test-e2e-sharded:
|
|
157
|
-
uv run --active pl-e2e-test parallel --shard-count "$(E2E_SHARD_COUNT)"
|
|
158
|
-
$(E2E_TEST_ARGS) $(E2E_PRAIRIELEARN_ARG) --fuzz-seeds $(E2E_FUZZ_SEED_ARG) $(PYTEST_ARGS)
|
|
179
|
+
uv run --active pl-e2e-test parallel run --shard-count "$(E2E_SHARD_COUNT)" \
|
|
180
|
+
$(E2E_TEST_ARGS) $(E2E_PRAIRIELEARN_ARG) --fuzz-seeds $(E2E_FUZZ_SEED_ARG) -- $(PYTEST_ARGS)
|
|
159
181
|
|
|
160
182
|
test-e2e-fuzz-sharded:
|
|
161
|
-
uv run --active pl-e2e-test parallel --shard-count "$(E2E_SHARD_COUNT)"
|
|
183
|
+
uv run --active pl-e2e-test parallel diff --shard-count "$(E2E_SHARD_COUNT)" \
|
|
162
184
|
--base "$(E2E_DIFF_BASE)" --head "$(E2E_DIFF_HEAD)" \
|
|
163
|
-
--seed-count "$(E2E_FUZZ_SEED_COUNT)" $(E2E_FUZZ_SEED_ARG) $(E2E_PRAIRIELEARN_ARG) $(PYTEST_ARGS)
|
|
185
|
+
--seed-count "$(E2E_FUZZ_SEED_COUNT)" $(E2E_FUZZ_SEED_ARG) $(E2E_PRAIRIELEARN_ARG) -- $(PYTEST_ARGS)
|
|
164
186
|
```
|
|
165
187
|
|
|
166
188
|
### CI sharding
|
|
@@ -202,7 +224,7 @@ jobs:
|
|
|
202
224
|
- run: uv python install && uv sync --locked
|
|
203
225
|
- name: Run shard
|
|
204
226
|
run: |
|
|
205
|
-
uv run pl-e2e-test \
|
|
227
|
+
uv run pl-e2e-test run \
|
|
206
228
|
--question-path questions \
|
|
207
229
|
--seed-count 10 \
|
|
208
230
|
--fuzz-seed "${{ github.run_id }}" \
|
|
@@ -272,6 +294,34 @@ Add `--refresh` to resolve the configured moving selector and verify remote acce
|
|
|
272
294
|
The table distinguishes fully hydrated runtime revisions from legacy element-only cache
|
|
273
295
|
entries. Standard cache environment settings such as `XDG_CACHE_HOME` can relocate the
|
|
274
296
|
managed cache; CI systems can persist that directory between runs if desired.
|
|
297
|
+
Cache use refreshes each repository or hydrated runtime's last-use timestamp. Whenever
|
|
298
|
+
the managed cache is used, unlocked items that have been idle for more than 30 days are
|
|
299
|
+
removed automatically across all configured PrairieLearn sources.
|
|
300
|
+
|
|
301
|
+
Read the effective configured selector without resolving it or contacting the network:
|
|
302
|
+
|
|
303
|
+
```sh
|
|
304
|
+
uv run pl-e2e-test version
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Set `tool.pl-e2e-test.prairielearnVersion` in an existing `pyproject.toml` to a full
|
|
308
|
+
commit or verified PrairieLearn release tag with:
|
|
309
|
+
|
|
310
|
+
```sh
|
|
311
|
+
uv run pl-e2e-test version 3.2.0
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Release tags are verified against the configured PrairieLearn Git source before the
|
|
315
|
+
file is changed. Use `--config PATH` to update another existing TOML file. Comments and
|
|
316
|
+
unrelated formatting are preserved, and a missing file is not created.
|
|
317
|
+
|
|
318
|
+
Remove all managed bare repositories and hydrated runtime revisions with:
|
|
319
|
+
|
|
320
|
+
```sh
|
|
321
|
+
uv run pl-e2e-test cache clean
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
The next test run fetches and hydrates its selected PrairieLearn revision again.
|
|
275
325
|
|
|
276
326
|
### Harness configuration
|
|
277
327
|
|
|
@@ -296,8 +346,8 @@ answers from grading checks. Each supplied list replaces its default; omitted se
|
|
|
296
346
|
keep their defaults, and `[]` disables a list.
|
|
297
347
|
|
|
298
348
|
Use `--config path/to/settings.toml` to select another TOML file with the same
|
|
299
|
-
`[tool.pl-e2e-test]` table. This works with
|
|
300
|
-
`parallel
|
|
349
|
+
`[tool.pl-e2e-test]` table. This works with `run`, `diff`, `parallel run`, and
|
|
350
|
+
`parallel diff`. Explicit paths are relative to the working directory.
|
|
301
351
|
|
|
302
352
|
### Lifecycle JSON boundary
|
|
303
353
|
|
|
@@ -451,7 +501,7 @@ diffignore = [
|
|
|
451
501
|
|
|
452
502
|
Rules use Git's native ignore syntax and are evaluated in order, so later rules may
|
|
453
503
|
negate earlier ones with `!`. A missing `pyproject.toml`, tool table, or `diffignore`
|
|
454
|
-
entry means that no paths are ignored. Additional arguments
|
|
504
|
+
entry means that no paths are ignored. Additional pytest arguments must follow `--`.
|
|
455
505
|
|
|
456
506
|
To migrate from `.question-e2e.diffignore`, copy its non-comment rules into the
|
|
457
507
|
`diffignore` array. The former file and `--e2ediffignore` option are no longer used.
|
|
@@ -8,16 +8,18 @@ instead of copying the harness and its support scripts.
|
|
|
8
8
|
The package installs the reusable `prairielearn_e2e` Python API and a single
|
|
9
9
|
`pl-e2e-test` command:
|
|
10
10
|
|
|
11
|
-
- `pl-e2e-test`
|
|
12
|
-
lifecycle pytest suite.
|
|
11
|
+
- `pl-e2e-test run` runs the installed lifecycle pytest suite.
|
|
13
12
|
- `pl-e2e-test diff` fuzzes questions selected from a Git diff.
|
|
14
|
-
- `pl-e2e-test parallel`
|
|
13
|
+
- `pl-e2e-test parallel run` and `parallel diff` run local shards and merge them.
|
|
15
14
|
- `pl-e2e-test add-seed` records a failing concrete variant seed.
|
|
16
15
|
- `pl-e2e-test reports` merges parallel shard results into terminal and JUnit reports.
|
|
17
16
|
- `pl-e2e-test instances` shows configured, installed, cached, and local PrairieLearn
|
|
18
17
|
revisions.
|
|
19
|
-
- `pl-e2e-test version`
|
|
20
|
-
- `pl-e2e-test
|
|
18
|
+
- `pl-e2e-test version [REVISION]` reads or updates the configured PrairieLearn revision.
|
|
19
|
+
- `pl-e2e-test cache clean` clears the managed PrairieLearn instance cache.
|
|
20
|
+
- `pl-e2e-test --version` prints the installed CLI version.
|
|
21
|
+
- `pl-e2e-test help [COMMAND...]` prints top-level or nested command help. A bare
|
|
22
|
+
invocation also prints top-level help.
|
|
21
23
|
|
|
22
24
|
## Requirements and installation
|
|
23
25
|
|
|
@@ -59,20 +61,37 @@ Pass a question, a directory of questions, or an `info.json` file. Repeat
|
|
|
59
61
|
`--question-path` to select multiple roots:
|
|
60
62
|
|
|
61
63
|
```sh
|
|
62
|
-
uv run pl-e2e-test \
|
|
64
|
+
uv run pl-e2e-test run \
|
|
63
65
|
--question-path questions \
|
|
64
66
|
--seed-count 3
|
|
65
67
|
```
|
|
66
68
|
|
|
67
69
|
The installed suite exposes separate `generate`, `prepare`, `render`, and `grade`
|
|
68
|
-
pytest markers.
|
|
70
|
+
pytest markers. Put pytest selection and execution arguments after `--`:
|
|
69
71
|
|
|
70
72
|
```sh
|
|
71
|
-
uv run pl-e2e-test \
|
|
73
|
+
uv run pl-e2e-test -vv run \
|
|
72
74
|
--question-path questions/chapter/example \
|
|
73
|
-
--fuzz-seeds -m render -x
|
|
75
|
+
--fuzz-seeds -- -m render -x
|
|
74
76
|
```
|
|
75
77
|
|
|
78
|
+
The native reporter owns progress, capture, tracebacks, summaries, and color. Use
|
|
79
|
+
repeatable global `-q` or `-v` flags and `--color auto|always|never`; pytest
|
|
80
|
+
presentation flags such as `-q`, `-v`, `--tb`, `-r`, `--color`, and `-s` are rejected
|
|
81
|
+
after `--`. `NO_COLOR`, `FORCE_COLOR`, and `CLICOLOR_FORCE` are also honored.
|
|
82
|
+
|
|
83
|
+
To preview the terminal UX without a PrairieLearn course or network access, run:
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
uv run python scripts/demo_cli.py
|
|
87
|
+
uv run python scripts/demo_cli.py --color always mixed verbose cli-error
|
|
88
|
+
uv run python scripts/demo_cli.py --list
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The demo covers help and CLI errors plus passing, failing, skipped, xfailed/xpassed,
|
|
92
|
+
warning, collection-error, empty-selection, quiet, verbose, and aggregate shard progress
|
|
93
|
+
output.
|
|
94
|
+
|
|
76
95
|
`--fuzz-seeds` creates and reports a random 64-bit master seed. Reproduce the same
|
|
77
96
|
variant set with `--fuzz-seed N`. Without either option, deterministic seeds start at
|
|
78
97
|
zero. Question metadata with `singleVariant: true` always uses seed zero.
|
|
@@ -82,22 +101,23 @@ zero. Question metadata with `singleVariant: true` always uses seed zero.
|
|
|
82
101
|
Run multiple shards in one machine or CI job, with six concurrent processes by default:
|
|
83
102
|
|
|
84
103
|
```sh
|
|
85
|
-
uv run pl-e2e-test parallel --shard-count 6
|
|
104
|
+
uv run pl-e2e-test parallel run --shard-count 6 \
|
|
86
105
|
--question-path questions --fuzz-seeds
|
|
87
106
|
|
|
88
|
-
uv run pl-e2e-test parallel --shard-count 6 --junitxml results.xml
|
|
107
|
+
uv run pl-e2e-test parallel diff --shard-count 6 --junitxml results.xml \
|
|
89
108
|
--base main --fuzz-seed 42
|
|
90
109
|
```
|
|
91
110
|
|
|
92
|
-
|
|
93
|
-
|
|
111
|
+
Native options go after `parallel run` or `parallel diff` and before an optional `--`;
|
|
112
|
+
only pytest arguments follow that boundary. Normal runs remain deterministic. For
|
|
94
113
|
`--fuzz-seeds` and diff runs, the launcher generates one shared master seed unless
|
|
95
114
|
`--fuzz-seed` is supplied, and prints a command for replaying the run without sharding.
|
|
96
115
|
|
|
97
116
|
Each shard runs in a separate Python process using the current environment and
|
|
98
|
-
working directory. Terminals show
|
|
99
|
-
lines. All shards finish before reports are merged, even when a test
|
|
100
|
-
|
|
117
|
+
working directory. Terminals show aggregate progress; redirected output uses stable
|
|
118
|
+
start and summary lines. All shards finish before reports are merged, even when a test
|
|
119
|
+
fails. Raw child output is shown only when a launch or internal failure cannot produce
|
|
120
|
+
a usable report.
|
|
101
121
|
Interrupting the launcher stops and reaps its children.
|
|
102
122
|
|
|
103
123
|
Intermediate reports are temporary. Pass the launcher's `--junitxml` option to retain
|
|
@@ -124,19 +144,19 @@ Relative JUnit paths are resolved against the course root. With `junitxml=None`,
|
|
|
124
144
|
merged output is temporary. Invalid launcher settings raise `ValueError`.
|
|
125
145
|
|
|
126
146
|
Courses using `scripts/run_e2e_shards.py` can replace its invocation with
|
|
127
|
-
`pl-e2e-test parallel`, move `--fuzz-seed` into the
|
|
147
|
+
`pl-e2e-test parallel run`, move `--fuzz-seed` into the native arguments, and explicitly
|
|
128
148
|
pass `--fuzz-seeds` for full-suite runs to preserve the script's fuzzing behavior.
|
|
129
149
|
For example:
|
|
130
150
|
|
|
131
151
|
```make
|
|
132
152
|
test-e2e-sharded:
|
|
133
|
-
uv run --active pl-e2e-test parallel --shard-count "$(E2E_SHARD_COUNT)"
|
|
134
|
-
$(E2E_TEST_ARGS) $(E2E_PRAIRIELEARN_ARG) --fuzz-seeds $(E2E_FUZZ_SEED_ARG) $(PYTEST_ARGS)
|
|
153
|
+
uv run --active pl-e2e-test parallel run --shard-count "$(E2E_SHARD_COUNT)" \
|
|
154
|
+
$(E2E_TEST_ARGS) $(E2E_PRAIRIELEARN_ARG) --fuzz-seeds $(E2E_FUZZ_SEED_ARG) -- $(PYTEST_ARGS)
|
|
135
155
|
|
|
136
156
|
test-e2e-fuzz-sharded:
|
|
137
|
-
uv run --active pl-e2e-test parallel --shard-count "$(E2E_SHARD_COUNT)"
|
|
157
|
+
uv run --active pl-e2e-test parallel diff --shard-count "$(E2E_SHARD_COUNT)" \
|
|
138
158
|
--base "$(E2E_DIFF_BASE)" --head "$(E2E_DIFF_HEAD)" \
|
|
139
|
-
--seed-count "$(E2E_FUZZ_SEED_COUNT)" $(E2E_FUZZ_SEED_ARG) $(E2E_PRAIRIELEARN_ARG) $(PYTEST_ARGS)
|
|
159
|
+
--seed-count "$(E2E_FUZZ_SEED_COUNT)" $(E2E_FUZZ_SEED_ARG) $(E2E_PRAIRIELEARN_ARG) -- $(PYTEST_ARGS)
|
|
140
160
|
```
|
|
141
161
|
|
|
142
162
|
### CI sharding
|
|
@@ -178,7 +198,7 @@ jobs:
|
|
|
178
198
|
- run: uv python install && uv sync --locked
|
|
179
199
|
- name: Run shard
|
|
180
200
|
run: |
|
|
181
|
-
uv run pl-e2e-test \
|
|
201
|
+
uv run pl-e2e-test run \
|
|
182
202
|
--question-path questions \
|
|
183
203
|
--seed-count 10 \
|
|
184
204
|
--fuzz-seed "${{ github.run_id }}" \
|
|
@@ -248,6 +268,34 @@ Add `--refresh` to resolve the configured moving selector and verify remote acce
|
|
|
248
268
|
The table distinguishes fully hydrated runtime revisions from legacy element-only cache
|
|
249
269
|
entries. Standard cache environment settings such as `XDG_CACHE_HOME` can relocate the
|
|
250
270
|
managed cache; CI systems can persist that directory between runs if desired.
|
|
271
|
+
Cache use refreshes each repository or hydrated runtime's last-use timestamp. Whenever
|
|
272
|
+
the managed cache is used, unlocked items that have been idle for more than 30 days are
|
|
273
|
+
removed automatically across all configured PrairieLearn sources.
|
|
274
|
+
|
|
275
|
+
Read the effective configured selector without resolving it or contacting the network:
|
|
276
|
+
|
|
277
|
+
```sh
|
|
278
|
+
uv run pl-e2e-test version
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Set `tool.pl-e2e-test.prairielearnVersion` in an existing `pyproject.toml` to a full
|
|
282
|
+
commit or verified PrairieLearn release tag with:
|
|
283
|
+
|
|
284
|
+
```sh
|
|
285
|
+
uv run pl-e2e-test version 3.2.0
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Release tags are verified against the configured PrairieLearn Git source before the
|
|
289
|
+
file is changed. Use `--config PATH` to update another existing TOML file. Comments and
|
|
290
|
+
unrelated formatting are preserved, and a missing file is not created.
|
|
291
|
+
|
|
292
|
+
Remove all managed bare repositories and hydrated runtime revisions with:
|
|
293
|
+
|
|
294
|
+
```sh
|
|
295
|
+
uv run pl-e2e-test cache clean
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
The next test run fetches and hydrates its selected PrairieLearn revision again.
|
|
251
299
|
|
|
252
300
|
### Harness configuration
|
|
253
301
|
|
|
@@ -272,8 +320,8 @@ answers from grading checks. Each supplied list replaces its default; omitted se
|
|
|
272
320
|
keep their defaults, and `[]` disables a list.
|
|
273
321
|
|
|
274
322
|
Use `--config path/to/settings.toml` to select another TOML file with the same
|
|
275
|
-
`[tool.pl-e2e-test]` table. This works with
|
|
276
|
-
`parallel
|
|
323
|
+
`[tool.pl-e2e-test]` table. This works with `run`, `diff`, `parallel run`, and
|
|
324
|
+
`parallel diff`. Explicit paths are relative to the working directory.
|
|
277
325
|
|
|
278
326
|
### Lifecycle JSON boundary
|
|
279
327
|
|
|
@@ -427,7 +475,7 @@ diffignore = [
|
|
|
427
475
|
|
|
428
476
|
Rules use Git's native ignore syntax and are evaluated in order, so later rules may
|
|
429
477
|
negate earlier ones with `!`. A missing `pyproject.toml`, tool table, or `diffignore`
|
|
430
|
-
entry means that no paths are ignored. Additional arguments
|
|
478
|
+
entry means that no paths are ignored. Additional pytest arguments must follow `--`.
|
|
431
479
|
|
|
432
480
|
To migrate from `.question-e2e.diffignore`, copy its non-comment rules into the
|
|
433
481
|
`diffignore` array. The former file and `--e2ediffignore` option are no longer used.
|
|
@@ -4,13 +4,14 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pl-e2e-test"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Fuzz PrairieLearn question lifecycles against the real Python element runtime"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.13"
|
|
11
11
|
license = "MIT"
|
|
12
12
|
dependencies = [
|
|
13
13
|
"chevron==0.14.0",
|
|
14
|
+
"cyclopts>=5.3,<6",
|
|
14
15
|
"filelock>=3.20",
|
|
15
16
|
"jsonschema>=4.26",
|
|
16
17
|
"lxml>=6.1",
|
|
@@ -21,6 +22,7 @@ dependencies = [
|
|
|
21
22
|
"pytest>=9.1",
|
|
22
23
|
"rich>=14",
|
|
23
24
|
"ruff>=0.16",
|
|
25
|
+
"tomlkit>=0.15.2",
|
|
24
26
|
]
|
|
25
27
|
|
|
26
28
|
[dependency-groups]
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""Show the pl-e2e-test terminal UX without a course or network access."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
import textwrap
|
|
9
|
+
from collections.abc import Callable, Sequence
|
|
10
|
+
from itertools import count
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import cast
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
from prairielearn_e2e import cli
|
|
17
|
+
from prairielearn_e2e.parallel import _shard_progress
|
|
18
|
+
from prairielearn_e2e.reporting import NativeReporter
|
|
19
|
+
from prairielearn_e2e.ui import CliContext, ColorChoice, make_console
|
|
20
|
+
|
|
21
|
+
type Demo = Callable[[ColorChoice], int]
|
|
22
|
+
_CASE_IDS = count()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _pytest_demo(
|
|
26
|
+
color: ColorChoice,
|
|
27
|
+
source: str,
|
|
28
|
+
*,
|
|
29
|
+
quiet: int = 0,
|
|
30
|
+
verbose: int = 0,
|
|
31
|
+
pytest_args: Sequence[str] = (),
|
|
32
|
+
) -> int:
|
|
33
|
+
with tempfile.TemporaryDirectory(prefix="pl-e2e-terminal-demo-") as temporary:
|
|
34
|
+
root = Path(temporary)
|
|
35
|
+
test_file = root / f"test_demo_{next(_CASE_IDS)}.py"
|
|
36
|
+
test_file.write_text(textwrap.dedent(source), encoding="utf-8")
|
|
37
|
+
context = CliContext(color=color, quiet=quiet, verbose=verbose)
|
|
38
|
+
reporter = NativeReporter(context)
|
|
39
|
+
return int(
|
|
40
|
+
pytest.main(
|
|
41
|
+
[
|
|
42
|
+
os.fspath(test_file),
|
|
43
|
+
"--rootdir",
|
|
44
|
+
os.fspath(root),
|
|
45
|
+
*pytest_args,
|
|
46
|
+
],
|
|
47
|
+
plugins=[reporter],
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _help(color: ColorChoice) -> int:
|
|
53
|
+
return cli.main(["--color", color, "--help"])
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _cli_error(color: ColorChoice) -> int:
|
|
57
|
+
return cli.main(["--color", color, "rn"])
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _passing(color: ColorChoice) -> int:
|
|
61
|
+
return _pytest_demo(
|
|
62
|
+
color,
|
|
63
|
+
"""
|
|
64
|
+
def test_passes():
|
|
65
|
+
print("captured output from a passing test")
|
|
66
|
+
""",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _mixed(color: ColorChoice) -> int:
|
|
71
|
+
return _pytest_demo(
|
|
72
|
+
color,
|
|
73
|
+
"""
|
|
74
|
+
import warnings
|
|
75
|
+
import pytest
|
|
76
|
+
|
|
77
|
+
def test_passes():
|
|
78
|
+
print("passing capture is hidden at default verbosity")
|
|
79
|
+
|
|
80
|
+
def test_fails():
|
|
81
|
+
print("captured stdout from the failing test")
|
|
82
|
+
assert {"actual": 1} == {"expected": 2}
|
|
83
|
+
|
|
84
|
+
@pytest.mark.skip(reason="optional service is unavailable")
|
|
85
|
+
def test_skips():
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
@pytest.mark.xfail(reason="known upstream issue")
|
|
89
|
+
def test_expected_failure():
|
|
90
|
+
assert False
|
|
91
|
+
|
|
92
|
+
@pytest.mark.xfail(reason="the upstream issue may be fixed")
|
|
93
|
+
def test_unexpected_pass():
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
def test_warns():
|
|
97
|
+
warnings.warn("example lifecycle warning", UserWarning)
|
|
98
|
+
""",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _collection_error(color: ColorChoice) -> int:
|
|
103
|
+
return _pytest_demo(
|
|
104
|
+
color,
|
|
105
|
+
"""
|
|
106
|
+
def test_broken(:
|
|
107
|
+
pass
|
|
108
|
+
""",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _empty(color: ColorChoice) -> int:
|
|
113
|
+
return _pytest_demo(
|
|
114
|
+
color,
|
|
115
|
+
"""
|
|
116
|
+
def test_passes():
|
|
117
|
+
pass
|
|
118
|
+
""",
|
|
119
|
+
pytest_args=("-k", "does-not-exist"),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _quiet(color: ColorChoice) -> int:
|
|
124
|
+
return _pytest_demo(
|
|
125
|
+
color,
|
|
126
|
+
"""
|
|
127
|
+
def test_passes_silently():
|
|
128
|
+
pass
|
|
129
|
+
""",
|
|
130
|
+
quiet=2,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _verbose(color: ColorChoice) -> int:
|
|
135
|
+
return _pytest_demo(
|
|
136
|
+
color,
|
|
137
|
+
"""
|
|
138
|
+
def test_verbose_capture():
|
|
139
|
+
print("captured output shown by -vv")
|
|
140
|
+
""",
|
|
141
|
+
verbose=2,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _shards(color: ColorChoice) -> int:
|
|
146
|
+
console = make_console(color=color)
|
|
147
|
+
outcomes = (True, True, False, True)
|
|
148
|
+
with _shard_progress(console, len(outcomes), disable=False) as complete:
|
|
149
|
+
for index, succeeded in enumerate(outcomes, start=1):
|
|
150
|
+
complete(index, succeeded)
|
|
151
|
+
return 1
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
DEMOS: dict[str, Demo] = {
|
|
155
|
+
"help": _help,
|
|
156
|
+
"cli-error": _cli_error,
|
|
157
|
+
"passing": _passing,
|
|
158
|
+
"mixed": _mixed,
|
|
159
|
+
"collection-error": _collection_error,
|
|
160
|
+
"empty": _empty,
|
|
161
|
+
"quiet": _quiet,
|
|
162
|
+
"verbose": _verbose,
|
|
163
|
+
"shards": _shards,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _arguments(argv: Sequence[str]) -> tuple[ColorChoice, list[str], bool]:
|
|
168
|
+
color: ColorChoice = "auto"
|
|
169
|
+
selected: list[str] = []
|
|
170
|
+
list_only = False
|
|
171
|
+
index = 0
|
|
172
|
+
while index < len(argv):
|
|
173
|
+
token = argv[index]
|
|
174
|
+
if token == "--list":
|
|
175
|
+
list_only = True
|
|
176
|
+
elif token.startswith("--color="):
|
|
177
|
+
value = token.split("=", 1)[1]
|
|
178
|
+
if value not in {"auto", "always", "never"}:
|
|
179
|
+
raise ValueError("--color must be auto, always, or never")
|
|
180
|
+
color = cast(ColorChoice, value)
|
|
181
|
+
elif token == "--color":
|
|
182
|
+
index += 1
|
|
183
|
+
if index >= len(argv) or argv[index] not in {"auto", "always", "never"}:
|
|
184
|
+
raise ValueError("--color must be followed by auto, always, or never")
|
|
185
|
+
color = cast(ColorChoice, argv[index])
|
|
186
|
+
elif token.startswith("-"):
|
|
187
|
+
raise ValueError(f"unknown option: {token}")
|
|
188
|
+
else:
|
|
189
|
+
selected.append(token)
|
|
190
|
+
index += 1
|
|
191
|
+
return color, selected, list_only
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
195
|
+
try:
|
|
196
|
+
color, selected, list_only = _arguments(sys.argv[1:] if argv is None else argv)
|
|
197
|
+
except ValueError as exc:
|
|
198
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
199
|
+
return 2
|
|
200
|
+
if list_only:
|
|
201
|
+
print("\n".join(DEMOS))
|
|
202
|
+
return 0
|
|
203
|
+
unknown = [name for name in selected if name not in DEMOS]
|
|
204
|
+
if unknown:
|
|
205
|
+
print(f"error: unknown demo {unknown[0]!r}", file=sys.stderr)
|
|
206
|
+
print(f"available demos: {', '.join(DEMOS)}", file=sys.stderr)
|
|
207
|
+
return 2
|
|
208
|
+
|
|
209
|
+
console = make_console(color=color)
|
|
210
|
+
for name in selected or list(DEMOS):
|
|
211
|
+
console.rule(f"demo: {name}", style="bold cyan")
|
|
212
|
+
status = DEMOS[name](color)
|
|
213
|
+
console.print(f"[dim]exit status: {status}[/dim]")
|
|
214
|
+
console.print()
|
|
215
|
+
return 0
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
if __name__ == "__main__":
|
|
219
|
+
raise SystemExit(main())
|