pl-e2e-test 0.1.0__tar.gz → 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. {pl_e2e_test-0.1.0/src/pl_e2e_test.egg-info → pl_e2e_test-0.1.1}/PKG-INFO +18 -5
  2. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/README.md +17 -4
  3. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/pyproject.toml +1 -1
  4. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1/src/pl_e2e_test.egg-info}/PKG-INFO +18 -5
  5. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/src/prairielearn_e2e/fuzz.py +35 -19
  6. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/tests/test_fuzz.py +103 -7
  7. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/LICENSE +0 -0
  8. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/MANIFEST.in +0 -0
  9. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/setup.cfg +0 -0
  10. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/src/pl_e2e_test.egg-info/SOURCES.txt +0 -0
  11. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/src/pl_e2e_test.egg-info/dependency_links.txt +0 -0
  12. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/src/pl_e2e_test.egg-info/entry_points.txt +0 -0
  13. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/src/pl_e2e_test.egg-info/requires.txt +0 -0
  14. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/src/pl_e2e_test.egg-info/top_level.txt +0 -0
  15. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/src/prairielearn_e2e/__init__.py +0 -0
  16. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/src/prairielearn_e2e/__main__.py +0 -0
  17. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/src/prairielearn_e2e/_git.py +0 -0
  18. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/src/prairielearn_e2e/backend.py +0 -0
  19. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/src/prairielearn_e2e/config.json +0 -0
  20. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/src/prairielearn_e2e/config.py +0 -0
  21. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/src/prairielearn_e2e/contract.py +0 -0
  22. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/src/prairielearn_e2e/discovery.py +0 -0
  23. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/src/prairielearn_e2e/errors.py +0 -0
  24. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/src/prairielearn_e2e/harness.py +0 -0
  25. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/src/prairielearn_e2e/html_checks.py +0 -0
  26. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/src/prairielearn_e2e/metadata.py +0 -0
  27. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/src/prairielearn_e2e/pytest_plugin.py +0 -0
  28. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/src/prairielearn_e2e/question_tests.py +0 -0
  29. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/src/prairielearn_e2e/regression.py +0 -0
  30. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/src/prairielearn_e2e/reporting.py +0 -0
  31. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/src/prairielearn_e2e/repository.py +0 -0
  32. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/src/prairielearn_e2e/runner.py +0 -0
  33. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/src/prairielearn_e2e/server.py +0 -0
  34. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/src/prairielearn_e2e/sharding.py +0 -0
  35. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/tests/__init__.py +0 -0
  36. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/tests/conftest.py +0 -0
  37. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/tests/stub_git.py +0 -0
  38. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/tests/test_contract.py +0 -0
  39. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/tests/test_discovery.py +0 -0
  40. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/tests/test_git.py +0 -0
  41. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/tests/test_harness.py +0 -0
  42. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/tests/test_html_checks.py +0 -0
  43. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/tests/test_metadata.py +0 -0
  44. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/tests/test_publish_version.py +0 -0
  45. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/tests/test_pytest_plugin.py +0 -0
  46. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/tests/test_regression.py +0 -0
  47. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/tests/test_reporting.py +0 -0
  48. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/tests/test_repository.py +0 -0
  49. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/tests/test_server.py +0 -0
  50. {pl_e2e_test-0.1.0 → pl_e2e_test-0.1.1}/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.1
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
@@ -227,10 +227,23 @@ uv run pl-e2e-test-diff \
227
227
  --seed-count 10
228
228
  ```
229
229
 
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.
230
+ Configure optional repository-relative ignore rules in the course's `pyproject.toml`:
231
+
232
+ ```toml
233
+ [tool.pl-e2e-test]
234
+ diffignore = [
235
+ ".github",
236
+ "README.md",
237
+ "*lock.yaml",
238
+ ]
239
+ ```
240
+
241
+ Rules use Git's native ignore syntax and are evaluated in order, so later rules may
242
+ negate earlier ones with `!`. A missing `pyproject.toml`, tool table, or `diffignore`
243
+ entry means that no paths are ignored. Additional arguments are forwarded to pytest.
244
+
245
+ To migrate from `.question-e2e.diffignore`, copy its non-comment rules into the
246
+ `diffignore` array. The former file and `--e2ediffignore` option are no longer used.
234
247
 
235
248
  In pull-request CI, fetch the base commit before running the command and pass the
236
249
  appropriate base and head SHAs. Set `--fuzz-seed` to replay a reported run.
@@ -204,10 +204,23 @@ uv run pl-e2e-test-diff \
204
204
  --seed-count 10
205
205
  ```
206
206
 
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.
207
+ Configure optional repository-relative ignore rules in the course's `pyproject.toml`:
208
+
209
+ ```toml
210
+ [tool.pl-e2e-test]
211
+ diffignore = [
212
+ ".github",
213
+ "README.md",
214
+ "*lock.yaml",
215
+ ]
216
+ ```
217
+
218
+ Rules use Git's native ignore syntax and are evaluated in order, so later rules may
219
+ negate earlier ones with `!`. A missing `pyproject.toml`, tool table, or `diffignore`
220
+ entry means that no paths are ignored. Additional arguments are forwarded to pytest.
221
+
222
+ To migrate from `.question-e2e.diffignore`, copy its non-comment rules into the
223
+ `diffignore` array. The former file and `--e2ediffignore` option are no longer used.
211
224
 
212
225
  In pull-request CI, fetch the base commit before running the command and pass the
213
226
  appropriate base and head SHAs. Set `--fuzz-seed` to replay a reported run.
@@ -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.1"
8
8
  description = "Fuzz PrairieLearn question lifecycles against the real Python element runtime"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -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.1
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
@@ -227,10 +227,23 @@ uv run pl-e2e-test-diff \
227
227
  --seed-count 10
228
228
  ```
229
229
 
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.
230
+ Configure optional repository-relative ignore rules in the course's `pyproject.toml`:
231
+
232
+ ```toml
233
+ [tool.pl-e2e-test]
234
+ diffignore = [
235
+ ".github",
236
+ "README.md",
237
+ "*lock.yaml",
238
+ ]
239
+ ```
240
+
241
+ Rules use Git's native ignore syntax and are evaluated in order, so later rules may
242
+ negate earlier ones with `!`. A missing `pyproject.toml`, tool table, or `diffignore`
243
+ entry means that no paths are ignored. Additional arguments are forwarded to pytest.
244
+
245
+ To migrate from `.question-e2e.diffignore`, copy its non-comment rules into the
246
+ `diffignore` array. The former file and `--e2ediffignore` option are no longer used.
234
247
 
235
248
  In pull-request CI, fetch the base commit before running the command and pass the
236
249
  appropriate base and head SHAs. Set `--fuzz-seed` to replay a reported run.
@@ -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]
@@ -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)
@@ -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,
File without changes
File without changes
File without changes