pl-e2e-test 0.1.0__tar.gz → 0.1.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 (53) hide show
  1. {pl_e2e_test-0.1.0/src/pl_e2e_test.egg-info → pl_e2e_test-0.1.2}/PKG-INFO +32 -15
  2. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/README.md +31 -14
  3. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/pyproject.toml +2 -5
  4. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2/src/pl_e2e_test.egg-info}/PKG-INFO +32 -15
  5. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/src/pl_e2e_test.egg-info/SOURCES.txt +2 -0
  6. pl_e2e_test-0.1.2/src/pl_e2e_test.egg-info/entry_points.txt +2 -0
  7. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/src/prairielearn_e2e/__main__.py +1 -1
  8. pl_e2e_test-0.1.2/src/prairielearn_e2e/cli.py +56 -0
  9. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/src/prairielearn_e2e/fuzz.py +37 -21
  10. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/src/prairielearn_e2e/regression.py +1 -1
  11. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/src/prairielearn_e2e/reporting.py +3 -2
  12. pl_e2e_test-0.1.2/tests/test_cli.py +81 -0
  13. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/tests/test_fuzz.py +104 -8
  14. pl_e2e_test-0.1.0/src/pl_e2e_test.egg-info/entry_points.txt +0 -5
  15. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/LICENSE +0 -0
  16. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/MANIFEST.in +0 -0
  17. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/setup.cfg +0 -0
  18. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/src/pl_e2e_test.egg-info/dependency_links.txt +0 -0
  19. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/src/pl_e2e_test.egg-info/requires.txt +0 -0
  20. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/src/pl_e2e_test.egg-info/top_level.txt +0 -0
  21. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/src/prairielearn_e2e/__init__.py +0 -0
  22. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/src/prairielearn_e2e/_git.py +0 -0
  23. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/src/prairielearn_e2e/backend.py +0 -0
  24. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/src/prairielearn_e2e/config.json +0 -0
  25. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/src/prairielearn_e2e/config.py +0 -0
  26. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/src/prairielearn_e2e/contract.py +0 -0
  27. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/src/prairielearn_e2e/discovery.py +0 -0
  28. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/src/prairielearn_e2e/errors.py +0 -0
  29. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/src/prairielearn_e2e/harness.py +0 -0
  30. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/src/prairielearn_e2e/html_checks.py +0 -0
  31. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/src/prairielearn_e2e/metadata.py +0 -0
  32. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/src/prairielearn_e2e/pytest_plugin.py +0 -0
  33. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/src/prairielearn_e2e/question_tests.py +0 -0
  34. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/src/prairielearn_e2e/repository.py +0 -0
  35. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/src/prairielearn_e2e/runner.py +0 -0
  36. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/src/prairielearn_e2e/server.py +0 -0
  37. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/src/prairielearn_e2e/sharding.py +0 -0
  38. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/tests/__init__.py +0 -0
  39. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/tests/conftest.py +0 -0
  40. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/tests/stub_git.py +0 -0
  41. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/tests/test_contract.py +0 -0
  42. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/tests/test_discovery.py +0 -0
  43. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/tests/test_git.py +0 -0
  44. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/tests/test_harness.py +0 -0
  45. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/tests/test_html_checks.py +0 -0
  46. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/tests/test_metadata.py +0 -0
  47. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/tests/test_publish_version.py +0 -0
  48. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/tests/test_pytest_plugin.py +0 -0
  49. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/tests/test_regression.py +0 -0
  50. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/tests/test_reporting.py +0 -0
  51. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/tests/test_repository.py +0 -0
  52. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/tests/test_server.py +0 -0
  53. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.2}/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.0
3
+ Version: 0.1.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
@@ -28,12 +28,16 @@ generate, prepare, render, parse, grade, test, and file phases. It was extracted
28
28
  from the `pl-oer-lv102` course so that courses can install one uv-managed tool
29
29
  instead of copying the harness and its support scripts.
30
30
 
31
- The package installs the reusable `prairielearn_e2e` Python API and these commands:
31
+ The package installs the reusable `prairielearn_e2e` Python API and a single
32
+ `pl-e2e-test` command:
32
33
 
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.
34
+ - `pl-e2e-test` (no command, or any pytest/harness options) runs the installed
35
+ lifecycle pytest suite.
36
+ - `pl-e2e-test diff` fuzzes questions selected from a Git diff.
37
+ - `pl-e2e-test add-seed` records a failing concrete variant seed.
38
+ - `pl-e2e-test reports` merges parallel shard results into terminal and JUnit reports.
39
+ - `pl-e2e-test version` prints the installed version.
40
+ - `pl-e2e-test help [COMMAND]` prints top-level help, or one command's help.
37
41
 
38
42
  ## Requirements and installation
39
43
 
@@ -105,7 +109,7 @@ same checkout and arguments. Sharded fuzz runs must also use one explicit shared
105
109
  merge them into a combined terminal summary and JUnit report:
106
110
 
107
111
  ```sh
108
- uv run pl-e2e-merge-reports e2e-reports \
112
+ uv run pl-e2e-test reports e2e-reports \
109
113
  --junitxml pl-e2e-results.xml
110
114
  ```
111
115
 
@@ -161,7 +165,7 @@ jobs:
161
165
  merge-multiple: true
162
166
  - name: Merge reports
163
167
  run: |
164
- uv run pl-e2e-merge-reports e2e-reports \
168
+ uv run pl-e2e-test reports e2e-reports \
165
169
  --junitxml pl-e2e-results.xml
166
170
  - name: Upload merged JUnit report
167
171
  if: ${{ always() }}
@@ -208,7 +212,7 @@ questions automatically skip only the grade check.
208
212
  Record the concrete seed shown in a failing pytest case with:
209
213
 
210
214
  ```sh
211
- uv run pl-e2e-add-regression-seed \
215
+ uv run pl-e2e-test add-seed \
212
216
  questions/chapter/example 1843927501
213
217
  ```
214
218
 
@@ -221,20 +225,33 @@ The diff runner tests changed question directories. A non-ignored changed file o
221
225
  `questions/` selects every question:
222
226
 
223
227
  ```sh
224
- uv run pl-e2e-test-diff \
228
+ uv run pl-e2e-test diff \
225
229
  --base origin/main \
226
230
  --head HEAD \
227
231
  --seed-count 10
228
232
  ```
229
233
 
230
- Optional repository-relative ignore rules live in `.question-e2e.diffignore` and use
231
- Git's native ignore syntax. Missing default configuration means that no paths are
232
- ignored. Pass `--e2ediffignore PATH` to require and use another file. Additional
233
- arguments are forwarded to pytest.
234
+ Configure optional repository-relative ignore rules in the course's `pyproject.toml`:
235
+
236
+ ```toml
237
+ [tool.pl-e2e-test]
238
+ diffignore = [
239
+ ".github",
240
+ "README.md",
241
+ "*lock.yaml",
242
+ ]
243
+ ```
244
+
245
+ Rules use Git's native ignore syntax and are evaluated in order, so later rules may
246
+ negate earlier ones with `!`. A missing `pyproject.toml`, tool table, or `diffignore`
247
+ entry means that no paths are ignored. Additional arguments are forwarded to pytest.
248
+
249
+ To migrate from `.question-e2e.diffignore`, copy its non-comment rules into the
250
+ `diffignore` array. The former file and `--e2ediffignore` option are no longer used.
234
251
 
235
252
  In pull-request CI, fetch the base commit before running the command and pass the
236
253
  appropriate base and head SHAs. Set `--fuzz-seed` to replay a reported run.
237
- `pl-e2e-test-diff` accepts the same `--shard` and `--shard-report` options. If a diff
254
+ `pl-e2e-test diff` accepts the same `--shard` and `--shard-report` options. If a diff
238
255
  selects no questions, it writes a successful empty shard report so the merge job can
239
256
  still verify that every matrix job completed.
240
257
 
@@ -5,12 +5,16 @@ 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:
8
+ The package installs the reusable `prairielearn_e2e` Python API and a single
9
+ `pl-e2e-test` command:
9
10
 
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.
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 add-seed` records a failing concrete variant seed.
15
+ - `pl-e2e-test reports` merges parallel shard results into terminal and JUnit reports.
16
+ - `pl-e2e-test version` prints the installed version.
17
+ - `pl-e2e-test help [COMMAND]` prints top-level help, or one command's help.
14
18
 
15
19
  ## Requirements and installation
16
20
 
@@ -82,7 +86,7 @@ same checkout and arguments. Sharded fuzz runs must also use one explicit shared
82
86
  merge them into a combined terminal summary and JUnit report:
83
87
 
84
88
  ```sh
85
- uv run pl-e2e-merge-reports e2e-reports \
89
+ uv run pl-e2e-test reports e2e-reports \
86
90
  --junitxml pl-e2e-results.xml
87
91
  ```
88
92
 
@@ -138,7 +142,7 @@ jobs:
138
142
  merge-multiple: true
139
143
  - name: Merge reports
140
144
  run: |
141
- uv run pl-e2e-merge-reports e2e-reports \
145
+ uv run pl-e2e-test reports e2e-reports \
142
146
  --junitxml pl-e2e-results.xml
143
147
  - name: Upload merged JUnit report
144
148
  if: ${{ always() }}
@@ -185,7 +189,7 @@ questions automatically skip only the grade check.
185
189
  Record the concrete seed shown in a failing pytest case with:
186
190
 
187
191
  ```sh
188
- uv run pl-e2e-add-regression-seed \
192
+ uv run pl-e2e-test add-seed \
189
193
  questions/chapter/example 1843927501
190
194
  ```
191
195
 
@@ -198,20 +202,33 @@ The diff runner tests changed question directories. A non-ignored changed file o
198
202
  `questions/` selects every question:
199
203
 
200
204
  ```sh
201
- uv run pl-e2e-test-diff \
205
+ uv run pl-e2e-test diff \
202
206
  --base origin/main \
203
207
  --head HEAD \
204
208
  --seed-count 10
205
209
  ```
206
210
 
207
- Optional repository-relative ignore rules live in `.question-e2e.diffignore` and use
208
- Git's native ignore syntax. Missing default configuration means that no paths are
209
- ignored. Pass `--e2ediffignore PATH` to require and use another file. Additional
210
- arguments are forwarded to pytest.
211
+ Configure optional repository-relative ignore rules in the course's `pyproject.toml`:
212
+
213
+ ```toml
214
+ [tool.pl-e2e-test]
215
+ diffignore = [
216
+ ".github",
217
+ "README.md",
218
+ "*lock.yaml",
219
+ ]
220
+ ```
221
+
222
+ Rules use Git's native ignore syntax and are evaluated in order, so later rules may
223
+ negate earlier ones with `!`. A missing `pyproject.toml`, tool table, or `diffignore`
224
+ entry means that no paths are ignored. Additional arguments are forwarded to pytest.
225
+
226
+ To migrate from `.question-e2e.diffignore`, copy its non-comment rules into the
227
+ `diffignore` array. The former file and `--e2ediffignore` option are no longer used.
211
228
 
212
229
  In pull-request CI, fetch the base commit before running the command and pass the
213
230
  appropriate base and head SHAs. Set `--fuzz-seed` to replay a reported run.
214
- `pl-e2e-test-diff` accepts the same `--shard` and `--shard-report` options. If a diff
231
+ `pl-e2e-test diff` accepts the same `--shard` and `--shard-report` options. If a diff
215
232
  selects no questions, it writes a successful empty shard report so the merge job can
216
233
  still verify that every matrix job completed.
217
234
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pl-e2e-test"
7
- version = "0.1.0"
7
+ version = "0.1.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"
@@ -31,10 +31,7 @@ Issues = "https://github.com/PrairieLearn/pl-e2e-test/issues"
31
31
  Repository = "https://github.com/PrairieLearn/pl-e2e-test"
32
32
 
33
33
  [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"
34
+ pl-e2e-test = "prairielearn_e2e.cli:main"
38
35
 
39
36
  [tool.setuptools.packages.find]
40
37
  where = ["src"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pl-e2e-test
3
- Version: 0.1.0
3
+ Version: 0.1.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
@@ -28,12 +28,16 @@ generate, prepare, render, parse, grade, test, and file phases. It was extracted
28
28
  from the `pl-oer-lv102` course so that courses can install one uv-managed tool
29
29
  instead of copying the harness and its support scripts.
30
30
 
31
- The package installs the reusable `prairielearn_e2e` Python API and these commands:
31
+ The package installs the reusable `prairielearn_e2e` Python API and a single
32
+ `pl-e2e-test` command:
32
33
 
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.
34
+ - `pl-e2e-test` (no command, or any pytest/harness options) runs the installed
35
+ lifecycle pytest suite.
36
+ - `pl-e2e-test diff` fuzzes questions selected from a Git diff.
37
+ - `pl-e2e-test add-seed` records a failing concrete variant seed.
38
+ - `pl-e2e-test reports` merges parallel shard results into terminal and JUnit reports.
39
+ - `pl-e2e-test version` prints the installed version.
40
+ - `pl-e2e-test help [COMMAND]` prints top-level help, or one command's help.
37
41
 
38
42
  ## Requirements and installation
39
43
 
@@ -105,7 +109,7 @@ same checkout and arguments. Sharded fuzz runs must also use one explicit shared
105
109
  merge them into a combined terminal summary and JUnit report:
106
110
 
107
111
  ```sh
108
- uv run pl-e2e-merge-reports e2e-reports \
112
+ uv run pl-e2e-test reports e2e-reports \
109
113
  --junitxml pl-e2e-results.xml
110
114
  ```
111
115
 
@@ -161,7 +165,7 @@ jobs:
161
165
  merge-multiple: true
162
166
  - name: Merge reports
163
167
  run: |
164
- uv run pl-e2e-merge-reports e2e-reports \
168
+ uv run pl-e2e-test reports e2e-reports \
165
169
  --junitxml pl-e2e-results.xml
166
170
  - name: Upload merged JUnit report
167
171
  if: ${{ always() }}
@@ -208,7 +212,7 @@ questions automatically skip only the grade check.
208
212
  Record the concrete seed shown in a failing pytest case with:
209
213
 
210
214
  ```sh
211
- uv run pl-e2e-add-regression-seed \
215
+ uv run pl-e2e-test add-seed \
212
216
  questions/chapter/example 1843927501
213
217
  ```
214
218
 
@@ -221,20 +225,33 @@ The diff runner tests changed question directories. A non-ignored changed file o
221
225
  `questions/` selects every question:
222
226
 
223
227
  ```sh
224
- uv run pl-e2e-test-diff \
228
+ uv run pl-e2e-test diff \
225
229
  --base origin/main \
226
230
  --head HEAD \
227
231
  --seed-count 10
228
232
  ```
229
233
 
230
- Optional repository-relative ignore rules live in `.question-e2e.diffignore` and use
231
- Git's native ignore syntax. Missing default configuration means that no paths are
232
- ignored. Pass `--e2ediffignore PATH` to require and use another file. Additional
233
- arguments are forwarded to pytest.
234
+ Configure optional repository-relative ignore rules in the course's `pyproject.toml`:
235
+
236
+ ```toml
237
+ [tool.pl-e2e-test]
238
+ diffignore = [
239
+ ".github",
240
+ "README.md",
241
+ "*lock.yaml",
242
+ ]
243
+ ```
244
+
245
+ Rules use Git's native ignore syntax and are evaluated in order, so later rules may
246
+ negate earlier ones with `!`. A missing `pyproject.toml`, tool table, or `diffignore`
247
+ entry means that no paths are ignored. Additional arguments are forwarded to pytest.
248
+
249
+ To migrate from `.question-e2e.diffignore`, copy its non-comment rules into the
250
+ `diffignore` array. The former file and `--e2ediffignore` option are no longer used.
234
251
 
235
252
  In pull-request CI, fetch the base commit before running the command and pass the
236
253
  appropriate base and head SHAs. Set `--fuzz-seed` to replay a reported run.
237
- `pl-e2e-test-diff` accepts the same `--shard` and `--shard-report` options. If a diff
254
+ `pl-e2e-test diff` accepts the same `--shard` and `--shard-report` options. If a diff
238
255
  selects no questions, it writes a successful empty shard report so the merge job can
239
256
  still verify that every matrix job completed.
240
257
 
@@ -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
@@ -32,6 +33,7 @@ src/prairielearn_e2e/sharding.py
32
33
  tests/__init__.py
33
34
  tests/conftest.py
34
35
  tests/stub_git.py
36
+ tests/test_cli.py
35
37
  tests/test_contract.py
36
38
  tests/test_discovery.py
37
39
  tests/test_fuzz.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pl-e2e-test = prairielearn_e2e.cli:main
@@ -1,3 +1,3 @@
1
- from .runner import main
1
+ from .cli import main
2
2
 
3
3
  raise SystemExit(main())
@@ -0,0 +1,56 @@
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, regression, reporting, runner
10
+
11
+ _SUBCOMMANDS = {
12
+ "diff": fuzz.main,
13
+ "add-seed": regression.main,
14
+ "reports": reporting.main,
15
+ }
16
+
17
+ _TOP_LEVEL_HELP = """\
18
+ usage: pl-e2e-test [COMMAND] [ARGS...]
19
+
20
+ With no COMMAND, runs the installed PrairieLearn question lifecycle test
21
+ suite; any arguments (pytest options, --question-path, --seed-count, ...)
22
+ are passed straight through to pytest.
23
+
24
+ Commands:
25
+ diff fuzz questions changed between two Git refs
26
+ add-seed record a concrete regression seed for a question
27
+ reports merge parallel shard reports into one terminal/JUnit report
28
+ version show the installed pl-e2e-test version
29
+ help show this message, or `pl-e2e-test help COMMAND` for one command
30
+ """
31
+
32
+
33
+ def _version_command(_argv: Sequence[str]) -> int:
34
+ print(f"pl-e2e-test {version('pl-e2e-test')}")
35
+ return 0
36
+
37
+
38
+ def _help_command(argv: Sequence[str]) -> int:
39
+ if argv and argv[0] in _SUBCOMMANDS:
40
+ return _SUBCOMMANDS[argv[0]](["--help"])
41
+ print(_TOP_LEVEL_HELP, end="")
42
+ return 0
43
+
44
+
45
+ _COMMANDS = {**_SUBCOMMANDS, "version": _version_command, "help": _help_command}
46
+
47
+
48
+ def main(argv: Sequence[str] | None = None) -> int:
49
+ arguments = list(sys.argv[1:] if argv is None else argv)
50
+ if arguments and arguments[0] in _COMMANDS:
51
+ return _COMMANDS[arguments[0]](arguments[1:])
52
+ return runner.main(arguments)
53
+
54
+
55
+ if __name__ == "__main__":
56
+ raise SystemExit(main())
@@ -7,6 +7,7 @@ import os
7
7
  import secrets
8
8
  import shlex
9
9
  import subprocess
10
+ import tomllib
10
11
  from collections.abc import Sequence
11
12
  from pathlib import Path
12
13
  from tempfile import TemporaryDirectory
@@ -17,7 +18,7 @@ from .runner import run_tests
17
18
  from .sharding import Shard, parse_shard
18
19
 
19
20
  QUESTIONS_ROOT = Path("questions")
20
- DEFAULT_E2E_DIFF_IGNORE = Path(".question-e2e.diffignore")
21
+ PYPROJECT_CONFIG = Path("pyproject.toml")
21
22
 
22
23
 
23
24
  def git_repository_root(start: Path) -> Path:
@@ -32,18 +33,37 @@ def git_repository_root(start: Path) -> Path:
32
33
  return Path(result.stdout.strip()).resolve()
33
34
 
34
35
 
35
- def load_e2e_diff_ignore(config_path: Path, *, missing_ok: bool = False) -> list[str]:
36
- """Load Git-style ignore rules used by E2E change detection."""
36
+ def load_e2e_diff_ignore(config_path: Path) -> list[str]:
37
+ """Load Git-style ignore rules from a course's pyproject.toml."""
37
38
 
38
39
  try:
39
- return config_path.read_text(encoding="utf-8").splitlines()
40
+ with config_path.open("rb") as config_file:
41
+ config = tomllib.load(config_file)
40
42
  except FileNotFoundError:
41
- if missing_ok:
42
- return []
43
- raise ValueError(f"Unable to read {config_path}: file does not exist") from None
43
+ return []
44
+ except tomllib.TOMLDecodeError as exc:
45
+ raise ValueError(f"Unable to read {config_path}: invalid TOML: {exc}") from exc
44
46
  except OSError as exc:
45
47
  raise ValueError(f"Unable to read {config_path}: {exc}") from exc
46
48
 
49
+ tool_config = config.get("tool", {})
50
+ if not isinstance(tool_config, dict):
51
+ raise TypeError(f"Invalid configuration in {config_path}: tool must be a table")
52
+ e2e_config = tool_config.get("pl-e2e-test", {})
53
+ if not isinstance(e2e_config, dict):
54
+ raise TypeError(
55
+ f"Invalid configuration in {config_path}: tool.pl-e2e-test must be a table"
56
+ )
57
+ ignore_rules = e2e_config.get("diffignore", [])
58
+ if not isinstance(ignore_rules, list) or not all(
59
+ isinstance(rule, str) for rule in ignore_rules
60
+ ):
61
+ raise TypeError(
62
+ f"Invalid configuration in {config_path}: "
63
+ "tool.pl-e2e-test.diffignore must be an array of strings"
64
+ )
65
+ return ignore_rules
66
+
47
67
 
48
68
  def e2e_diff_ignored_files(
49
69
  changed_files: Sequence[Path], ignore_rules: Sequence[str]
@@ -175,7 +195,7 @@ def positive_integer(value: str) -> int:
175
195
 
176
196
 
177
197
  def main(argv: Sequence[str] | None = None) -> int:
178
- parser = argparse.ArgumentParser(description=__doc__)
198
+ parser = argparse.ArgumentParser(prog="pl-e2e-test diff", description=__doc__)
179
199
  parser.add_argument("--course-root", type=Path)
180
200
  parser.add_argument("--base", default="main", help="base Git ref (default: main)")
181
201
  parser.add_argument("--head", default="HEAD", help="head Git ref (default: HEAD)")
@@ -195,8 +215,7 @@ def main(argv: Sequence[str] | None = None) -> int:
195
215
  )
196
216
  parser.add_argument(
197
217
  "--e2ediffignore",
198
- type=Path,
199
- help="E2E change-detection ignore file (default: COURSE_ROOT/.question-e2e.diffignore)",
218
+ help=argparse.SUPPRESS,
200
219
  )
201
220
  parser.add_argument(
202
221
  "--shard",
@@ -211,6 +230,11 @@ def main(argv: Sequence[str] | None = None) -> int:
211
230
  help="write a mergeable JSON result blob for this shard",
212
231
  )
213
232
  args, pytest_args = parser.parse_known_args(argv)
233
+ if args.e2ediffignore is not None:
234
+ parser.error(
235
+ "--e2ediffignore has been removed; configure "
236
+ "tool.pl-e2e-test.diffignore in COURSE_ROOT/pyproject.toml"
237
+ )
214
238
  shard: Shard | None = args.shard
215
239
  if args.shard_report is not None and shard is None:
216
240
  parser.error("--shard-report requires --shard")
@@ -226,17 +250,9 @@ def main(argv: Sequence[str] | None = None) -> int:
226
250
  except (OSError, subprocess.CalledProcessError) as exc:
227
251
  parser.error(f"could not find the course Git repository: {exc}")
228
252
 
229
- ignore_path = args.e2ediffignore
230
- if ignore_path is None:
231
- ignore_path = repository_root / DEFAULT_E2E_DIFF_IGNORE
232
- missing_ok = True
233
- else:
234
- if not ignore_path.is_absolute():
235
- ignore_path = repository_root / ignore_path
236
- missing_ok = False
237
253
  try:
238
- ignored_patterns = load_e2e_diff_ignore(ignore_path, missing_ok=missing_ok)
239
- except ValueError as exc:
254
+ ignored_patterns = load_e2e_diff_ignore(repository_root / PYPROJECT_CONFIG)
255
+ except (TypeError, ValueError) as exc:
240
256
  parser.error(str(exc))
241
257
 
242
258
  changed_files = changed_files_between(args.base, args.head, repository_root)
@@ -258,7 +274,7 @@ def main(argv: Sequence[str] | None = None) -> int:
258
274
  seeds = variant_seeds({}, args.seed_count, fuzz_seed)
259
275
  print(f"E2E fuzz seed: {fuzz_seed}")
260
276
  print(f"Variant seeds: {', '.join(str(seed) for seed in seeds)}")
261
- replay_arguments = ["pl-e2e-test-diff", "--fuzz-seed", str(fuzz_seed)]
277
+ replay_arguments = ["pl-e2e-test", "diff", "--fuzz-seed", str(fuzz_seed)]
262
278
  test_arguments: list[str] = []
263
279
  if args.prairielearn_path is not None:
264
280
  replay_arguments.extend(
@@ -96,7 +96,7 @@ def add_regression_seed(course_root: Path, question_path: Path, seed: int) -> bo
96
96
 
97
97
 
98
98
  def main(argv: Sequence[str] | None = None) -> int:
99
- parser = argparse.ArgumentParser(description=__doc__)
99
+ parser = argparse.ArgumentParser(prog="pl-e2e-test add-seed", description=__doc__)
100
100
  parser.add_argument("question_path", type=Path)
101
101
  parser.add_argument("variant_seed", type=_variant_seed)
102
102
  parser.add_argument("--course-root", type=Path)
@@ -659,7 +659,8 @@ def merge_reports(inputs: Sequence[Path], junitxml: Path) -> int:
659
659
 
660
660
  def main(argv: Sequence[str] | None = None) -> int:
661
661
  parser = argparse.ArgumentParser(
662
- description="Merge pl-e2e-test shard reports into one terminal and JUnit report."
662
+ prog="pl-e2e-test reports",
663
+ description="Merge pl-e2e-test shard reports into one terminal and JUnit report.",
663
664
  )
664
665
  parser.add_argument("reports", nargs="+", type=Path)
665
666
  parser.add_argument("--junitxml", type=Path, default=Path("pl-e2e-results.xml"))
@@ -667,7 +668,7 @@ def main(argv: Sequence[str] | None = None) -> int:
667
668
  try:
668
669
  return merge_reports(args.reports, args.junitxml)
669
670
  except (OSError, ReportError) as exc:
670
- print(f"pl-e2e-merge-reports: error: {exc}", file=sys.stderr)
671
+ print(f"pl-e2e-test reports: error: {exc}", file=sys.stderr)
671
672
  return 2
672
673
 
673
674
 
@@ -0,0 +1,81 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+ from importlib.metadata import version
5
+
6
+ import pytest
7
+
8
+ from prairielearn_e2e import cli
9
+
10
+
11
+ def _stub(calls: list[Sequence[str]], result: int = 0):
12
+ def _main(argv: Sequence[str]) -> int:
13
+ calls.append(list(argv))
14
+ return result
15
+
16
+ return _main
17
+
18
+
19
+ @pytest.mark.parametrize("command", ["diff", "add-seed", "reports"])
20
+ def test_dispatches_known_commands_to_their_module(
21
+ monkeypatch: pytest.MonkeyPatch, command: str
22
+ ) -> None:
23
+ calls: list[Sequence[str]] = []
24
+ monkeypatch.setitem(cli._COMMANDS, command, _stub(calls, result=3))
25
+
26
+ assert cli.main([command, "--example", "value"]) == 3
27
+ assert calls == [["--example", "value"]]
28
+
29
+
30
+ def test_no_command_falls_through_to_the_test_runner(
31
+ monkeypatch: pytest.MonkeyPatch,
32
+ ) -> None:
33
+ calls: list[Sequence[str]] = []
34
+ monkeypatch.setattr(cli, "runner", type("_Runner", (), {"main": _stub(calls)}))
35
+
36
+ assert cli.main(["--question-path", "questions/example", "-k", "sample"]) == 0
37
+ assert calls == [["--question-path", "questions/example", "-k", "sample"]]
38
+
39
+
40
+ def test_bare_invocation_falls_through_to_the_test_runner(
41
+ monkeypatch: pytest.MonkeyPatch,
42
+ ) -> None:
43
+ calls: list[Sequence[str]] = []
44
+ monkeypatch.setattr(cli, "runner", type("_Runner", (), {"main": _stub(calls)}))
45
+
46
+ assert cli.main([]) == 0
47
+ assert calls == [[]]
48
+
49
+
50
+ def test_version_command_prints_the_installed_version(
51
+ capsys: pytest.CaptureFixture[str],
52
+ ) -> None:
53
+ assert cli.main(["version"]) == 0
54
+ assert capsys.readouterr().out == f"pl-e2e-test {version('pl-e2e-test')}\n"
55
+
56
+
57
+ def test_help_command_with_no_argument_prints_top_level_help(
58
+ capsys: pytest.CaptureFixture[str],
59
+ ) -> None:
60
+ assert cli.main(["help"]) == 0
61
+ out = capsys.readouterr().out
62
+ assert "Commands:" in out
63
+ for command in ("diff", "add-seed", "reports", "version", "help"):
64
+ assert command in out
65
+
66
+
67
+ def test_help_command_delegates_to_a_known_subcommand(
68
+ monkeypatch: pytest.MonkeyPatch,
69
+ ) -> None:
70
+ calls: list[Sequence[str]] = []
71
+ monkeypatch.setitem(cli._SUBCOMMANDS, "diff", _stub(calls))
72
+
73
+ assert cli.main(["help", "diff"]) == 0
74
+ assert calls == [["--help"]]
75
+
76
+
77
+ def test_help_command_with_unknown_argument_prints_top_level_help(
78
+ capsys: pytest.CaptureFixture[str],
79
+ ) -> None:
80
+ assert cli.main(["help", "not-a-command"]) == 0
81
+ assert "Commands:" in capsys.readouterr().out
@@ -65,14 +65,83 @@ def test_root_anchored_gitignore_pattern() -> None:
65
65
  ) == {Path("README.md")}
66
66
 
67
67
 
68
- def test_missing_default_ignore_file_is_empty(tmp_path: Path) -> None:
69
- assert (
70
- fuzz.load_e2e_diff_ignore(
71
- tmp_path / ".question-e2e.diffignore", missing_ok=True
72
- )
73
- == []
68
+ def test_later_gitignore_pattern_can_negate_earlier_pattern() -> None:
69
+ assert fuzz.e2e_diff_ignored_files(
70
+ [Path("uv.lock"), Path("required.lock")], ["*.lock", "!required.lock"]
71
+ ) == {Path("uv.lock")}
72
+
73
+
74
+ def test_loads_diff_ignore_from_pyproject(tmp_path: Path) -> None:
75
+ config_path = tmp_path / "pyproject.toml"
76
+ config_path.write_text(
77
+ """
78
+ [tool.pl-e2e-test]
79
+ diffignore = ["/README.md", "*.lock", "!required.lock"]
80
+ """,
81
+ encoding="utf-8",
74
82
  )
75
83
 
84
+ assert fuzz.load_e2e_diff_ignore(config_path) == [
85
+ "/README.md",
86
+ "*.lock",
87
+ "!required.lock",
88
+ ]
89
+
90
+
91
+ @pytest.mark.parametrize(
92
+ "contents",
93
+ [
94
+ "",
95
+ "[tool.pytest.ini_options]\n",
96
+ "[tool.pl-e2e-test]\n",
97
+ "[tool.pl-e2e-test]\ndiffignore = []\n",
98
+ ],
99
+ )
100
+ def test_missing_or_empty_diff_ignore_configuration_is_empty(
101
+ tmp_path: Path, contents: str
102
+ ) -> None:
103
+ config_path = tmp_path / "pyproject.toml"
104
+ config_path.write_text(contents, encoding="utf-8")
105
+
106
+ assert fuzz.load_e2e_diff_ignore(config_path) == []
107
+
108
+
109
+ def test_missing_pyproject_is_empty(tmp_path: Path) -> None:
110
+ assert fuzz.load_e2e_diff_ignore(tmp_path / "pyproject.toml") == []
111
+
112
+
113
+ def test_invalid_pyproject_is_rejected(tmp_path: Path) -> None:
114
+ config_path = tmp_path / "pyproject.toml"
115
+ config_path.write_text("[tool.pl-e2e-test\n", encoding="utf-8")
116
+
117
+ with pytest.raises(ValueError, match="invalid TOML"):
118
+ fuzz.load_e2e_diff_ignore(config_path)
119
+
120
+
121
+ def test_unreadable_pyproject_is_rejected(tmp_path: Path) -> None:
122
+ config_path = tmp_path / "pyproject.toml"
123
+ config_path.mkdir()
124
+
125
+ with pytest.raises(ValueError, match="Unable to read"):
126
+ fuzz.load_e2e_diff_ignore(config_path)
127
+
128
+
129
+ @pytest.mark.parametrize(
130
+ "configured_value",
131
+ ['"README.md"', '["README.md", 1]'],
132
+ )
133
+ def test_diff_ignore_must_be_an_array_of_strings(
134
+ tmp_path: Path, configured_value: str
135
+ ) -> None:
136
+ config_path = tmp_path / "pyproject.toml"
137
+ config_path.write_text(
138
+ f"[tool.pl-e2e-test]\ndiffignore = {configured_value}\n",
139
+ encoding="utf-8",
140
+ )
141
+
142
+ with pytest.raises(TypeError, match="must be an array of strings"):
143
+ fuzz.load_e2e_diff_ignore(config_path)
144
+
76
145
 
77
146
  def test_main_forwards_selected_questions_and_seed(
78
147
  monkeypatch: pytest.MonkeyPatch,
@@ -82,11 +151,16 @@ def test_main_forwards_selected_questions_and_seed(
82
151
  question = tmp_path / "questions" / "chapter" / "example"
83
152
  question.mkdir(parents=True)
84
153
  (question / "info.json").write_text("{}", encoding="utf-8")
154
+ (tmp_path / "pyproject.toml").write_text(
155
+ '[tool.pl-e2e-test]\ndiffignore = ["README.md"]\n',
156
+ encoding="utf-8",
157
+ )
85
158
  monkeypatch.setattr(
86
159
  fuzz,
87
160
  "changed_files_between",
88
161
  lambda base, head, repository_root: [
89
- Path("questions/chapter/example/server.py")
162
+ Path("README.md"),
163
+ Path("questions/chapter/example/server.py"),
90
164
  ],
91
165
  )
92
166
  calls: list[tuple[list[str], Path]] = []
@@ -131,6 +205,28 @@ def test_main_forwards_selected_questions_and_seed(
131
205
  assert "E2E fuzz seed: 12345" in capsys.readouterr().out
132
206
 
133
207
 
208
+ def test_main_reports_invalid_pyproject(
209
+ tmp_path: Path, capsys: pytest.CaptureFixture[str]
210
+ ) -> None:
211
+ (tmp_path / "pyproject.toml").write_text("[tool.pl-e2e-test\n", encoding="utf-8")
212
+
213
+ with pytest.raises(SystemExit, match="2"):
214
+ fuzz.main(["--course-root", str(tmp_path)])
215
+
216
+ assert "invalid TOML" in capsys.readouterr().err
217
+
218
+
219
+ def test_removed_diff_ignore_option_reports_migration(
220
+ capsys: pytest.CaptureFixture[str],
221
+ ) -> None:
222
+ with pytest.raises(SystemExit, match="2"):
223
+ fuzz.main(["--e2ediffignore", ".question-e2e.diffignore"])
224
+
225
+ error = capsys.readouterr().err
226
+ assert "--e2ediffignore has been removed" in error
227
+ assert "tool.pl-e2e-test.diffignore" in error
228
+
229
+
134
230
  def test_main_uses_managed_cache_when_prairielearn_path_is_omitted(
135
231
  monkeypatch: pytest.MonkeyPatch,
136
232
  tmp_path: Path,
@@ -182,7 +278,7 @@ def test_main_uses_managed_cache_when_prairielearn_path_is_omitted(
182
278
  for line in capsys.readouterr().out.splitlines()
183
279
  if line.startswith("Replay")
184
280
  )
185
- assert replay == "Replay with: pl-e2e-test-diff --fuzz-seed 9876"
281
+ assert replay == "Replay with: pl-e2e-test diff --fuzz-seed 9876"
186
282
 
187
283
 
188
284
  def test_empty_diff_does_not_generate_a_seed(
@@ -1,5 +0,0 @@
1
- [console_scripts]
2
- pl-e2e-add-regression-seed = prairielearn_e2e.regression:main
3
- pl-e2e-merge-reports = prairielearn_e2e.reporting:main
4
- pl-e2e-test = prairielearn_e2e.runner:main
5
- pl-e2e-test-diff = prairielearn_e2e.fuzz:main
File without changes
File without changes
File without changes