pl-e2e-test 0.1.6__tar.gz → 0.1.7__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 (56) hide show
  1. {pl_e2e_test-0.1.6/src/pl_e2e_test.egg-info → pl_e2e_test-0.1.7}/PKG-INFO +9 -5
  2. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/README.md +8 -4
  3. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/pyproject.toml +1 -1
  4. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7/src/pl_e2e_test.egg-info}/PKG-INFO +9 -5
  5. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/harness.py +6 -6
  6. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/test_harness.py +63 -0
  7. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/test_publish_version.py +35 -0
  8. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/LICENSE +0 -0
  9. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/MANIFEST.in +0 -0
  10. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/setup.cfg +0 -0
  11. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/pl_e2e_test.egg-info/SOURCES.txt +0 -0
  12. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/pl_e2e_test.egg-info/dependency_links.txt +0 -0
  13. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/pl_e2e_test.egg-info/entry_points.txt +0 -0
  14. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/pl_e2e_test.egg-info/requires.txt +0 -0
  15. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/pl_e2e_test.egg-info/top_level.txt +0 -0
  16. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/__init__.py +0 -0
  17. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/__main__.py +0 -0
  18. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/_git.py +0 -0
  19. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/backend.py +0 -0
  20. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/cli.py +0 -0
  21. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/config.py +0 -0
  22. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/contract.py +0 -0
  23. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/discovery.py +0 -0
  24. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/errors.py +0 -0
  25. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/expected_errors.py +0 -0
  26. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/fuzz.py +0 -0
  27. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/html_checks.py +0 -0
  28. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/metadata.py +0 -0
  29. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/parallel.py +0 -0
  30. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/pytest_plugin.py +0 -0
  31. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/question_tests.py +0 -0
  32. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/regression.py +0 -0
  33. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/reporting.py +0 -0
  34. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/repository.py +0 -0
  35. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/runner.py +0 -0
  36. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/server.py +0 -0
  37. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/sharding.py +0 -0
  38. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/__init__.py +0 -0
  39. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/conftest.py +0 -0
  40. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/stub_git.py +0 -0
  41. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/test_cli.py +0 -0
  42. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/test_config.py +0 -0
  43. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/test_contract.py +0 -0
  44. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/test_discovery.py +0 -0
  45. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/test_fuzz.py +0 -0
  46. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/test_git.py +0 -0
  47. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/test_html_checks.py +0 -0
  48. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/test_metadata.py +0 -0
  49. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/test_parallel.py +0 -0
  50. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/test_pytest_plugin.py +0 -0
  51. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/test_question_tests.py +0 -0
  52. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/test_regression.py +0 -0
  53. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/test_reporting.py +0 -0
  54. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/test_repository.py +0 -0
  55. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/test_server.py +0 -0
  56. {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/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.6
3
+ Version: 0.1.7
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
@@ -335,9 +335,12 @@ Compound elements keep the declared answer name as their grading identity while
335
335
  namespaced child fields for browser inputs. For example, an element with
336
336
  `answers-name="match"` may test and parse `match-dropdown-0` and `match-dropdown-1`,
337
337
  then store its score in `partial_scores["match"]`; it does not need to invent a
338
- `submitted_answers["match"]` value. After parsing, every raw field must either remain
339
- non-null under its child key or produce a non-null value under its declared parent
340
- answer. This also supports elements that normalize `answer-input` into `answer`.
338
+ `submitted_answers["match"]` value. After parsing, every raw field associated with a
339
+ declared gradable answer must either remain non-null under its child key or produce a
340
+ non-null value under its declared parent answer. This also supports elements that
341
+ normalize `answer-input` into `answer`. Unassociated transport fields from file-producing
342
+ elements may instead be consumed into `submitted_answers["_files"]`; they are not answer
343
+ parse-coverage obligations.
341
344
 
342
345
  This contract keeps element-specific synthesis with the element or question that owns
343
346
  the data. PrairieLearn's tuple-based `pl-matching` state normalizes to arrays at the JSON
@@ -480,7 +483,8 @@ environment. From a clean branch, run:
480
483
  make publish-version VERSION=0.2.0
481
484
  ```
482
485
 
483
- This runs the full checks, updates `pyproject.toml` and `uv.lock`, builds the
486
+ This installs Python 3.13, performs the same locked dependency sync used in CI, runs
487
+ the full checks, updates `pyproject.toml` and `uv.lock`, builds and smoke-tests the
484
488
  distributions, creates a release commit and annotated `v0.2.0` tag, and atomically
485
489
  pushes the branch and tag. The tag starts the workflow on `ubuntu-latest`; the workflow
486
490
  rechecks the tag/version match, tests, builds, generates attestations, and publishes
@@ -311,9 +311,12 @@ Compound elements keep the declared answer name as their grading identity while
311
311
  namespaced child fields for browser inputs. For example, an element with
312
312
  `answers-name="match"` may test and parse `match-dropdown-0` and `match-dropdown-1`,
313
313
  then store its score in `partial_scores["match"]`; it does not need to invent a
314
- `submitted_answers["match"]` value. After parsing, every raw field must either remain
315
- non-null under its child key or produce a non-null value under its declared parent
316
- answer. This also supports elements that normalize `answer-input` into `answer`.
314
+ `submitted_answers["match"]` value. After parsing, every raw field associated with a
315
+ declared gradable answer must either remain non-null under its child key or produce a
316
+ non-null value under its declared parent answer. This also supports elements that
317
+ normalize `answer-input` into `answer`. Unassociated transport fields from file-producing
318
+ elements may instead be consumed into `submitted_answers["_files"]`; they are not answer
319
+ parse-coverage obligations.
317
320
 
318
321
  This contract keeps element-specific synthesis with the element or question that owns
319
322
  the data. PrairieLearn's tuple-based `pl-matching` state normalizes to arrays at the JSON
@@ -456,7 +459,8 @@ environment. From a clean branch, run:
456
459
  make publish-version VERSION=0.2.0
457
460
  ```
458
461
 
459
- This runs the full checks, updates `pyproject.toml` and `uv.lock`, builds the
462
+ This installs Python 3.13, performs the same locked dependency sync used in CI, runs
463
+ the full checks, updates `pyproject.toml` and `uv.lock`, builds and smoke-tests the
460
464
  distributions, creates a release commit and annotated `v0.2.0` tag, and atomically
461
465
  pushes the branch and tag. The tag starts the workflow on `ubuntu-latest`; the workflow
462
466
  rechecks the tag/version match, tests, builds, generates attestations, and publishes
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pl-e2e-test"
7
- version = "0.1.6"
7
+ version = "0.1.7"
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.6
3
+ Version: 0.1.7
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
@@ -335,9 +335,12 @@ Compound elements keep the declared answer name as their grading identity while
335
335
  namespaced child fields for browser inputs. For example, an element with
336
336
  `answers-name="match"` may test and parse `match-dropdown-0` and `match-dropdown-1`,
337
337
  then store its score in `partial_scores["match"]`; it does not need to invent a
338
- `submitted_answers["match"]` value. After parsing, every raw field must either remain
339
- non-null under its child key or produce a non-null value under its declared parent
340
- answer. This also supports elements that normalize `answer-input` into `answer`.
338
+ `submitted_answers["match"]` value. After parsing, every raw field associated with a
339
+ declared gradable answer must either remain non-null under its child key or produce a
340
+ non-null value under its declared parent answer. This also supports elements that
341
+ normalize `answer-input` into `answer`. Unassociated transport fields from file-producing
342
+ elements may instead be consumed into `submitted_answers["_files"]`; they are not answer
343
+ parse-coverage obligations.
341
344
 
342
345
  This contract keeps element-specific synthesis with the element or question that owns
343
346
  the data. PrairieLearn's tuple-based `pl-matching` state normalizes to arrays at the JSON
@@ -480,7 +483,8 @@ environment. From a clean branch, run:
480
483
  make publish-version VERSION=0.2.0
481
484
  ```
482
485
 
483
- This runs the full checks, updates `pyproject.toml` and `uv.lock`, builds the
486
+ This installs Python 3.13, performs the same locked dependency sync used in CI, runs
487
+ the full checks, updates `pyproject.toml` and `uv.lock`, builds and smoke-tests the
484
488
  distributions, creates a release commit and annotated `v0.2.0` tag, and atomically
485
489
  pushes the branch and tag. The tag starts the workflow on `ubuntu-latest`; the workflow
486
490
  rechecks the tag/version match, tests, builds, generates attestations, and publishes
@@ -128,13 +128,13 @@ def _assert_full_credit(data: dict[str, Any], answer_names: tuple[str, ...]) ->
128
128
  submitted_answers = data["submitted_answers"]
129
129
  unparsed: list[str] = []
130
130
  for raw_name in data["raw_submitted_answers"]:
131
- if submitted_answers.get(raw_name) is not None:
132
- continue
133
131
  parent_name = _associated_answer_name(raw_name, answer_names)
134
- parent_parsed = (
135
- parent_name is not None and submitted_answers.get(parent_name) is not None
136
- )
137
- if not parent_parsed:
132
+ if parent_name is None:
133
+ continue
134
+ if (
135
+ submitted_answers.get(raw_name) is None
136
+ and submitted_answers.get(parent_name) is None
137
+ ):
138
138
  unparsed.append(raw_name)
139
139
  if unparsed:
140
140
  raise HarnessError(
@@ -25,6 +25,10 @@ class RecordingBackend:
25
25
 
26
26
 
27
27
  class ElementOwnedAnswerBackend(RecordingBackend):
28
+ def __init__(self, *, null_after_parse: bool = False) -> None:
29
+ super().__init__()
30
+ self.null_after_parse = null_after_parse
31
+
28
32
  def process(
29
33
  self, phase: Phase, html: str, data: dict[str, Any]
30
34
  ) -> tuple[str | None, set[str]]:
@@ -32,6 +36,27 @@ class ElementOwnedAnswerBackend(RecordingBackend):
32
36
  if phase == "test":
33
37
  data["raw_submitted_answers"]["diagram"] = '{"nodes":[1,2]}'
34
38
  data["partial_scores"]["diagram"] = {"score": 1}
39
+ elif phase == "parse" and self.null_after_parse:
40
+ data["submitted_answers"]["diagram"] = None
41
+ return result
42
+
43
+
44
+ class UnassociatedFileFieldBackend(RecordingBackend):
45
+ def process(
46
+ self, phase: Phase, html: str, data: dict[str, Any]
47
+ ) -> tuple[str | None, set[str]]:
48
+ result = super().process(phase, html, data)
49
+ if phase == "test":
50
+ data["raw_submitted_answers"].update(
51
+ {
52
+ "diagram": '{"nodes":[1,2]}',
53
+ "_file_transport_token": '[{"name":"notes.txt"}]',
54
+ }
55
+ )
56
+ data["partial_scores"]["diagram"] = {"score": 1}
57
+ elif phase == "parse":
58
+ data["submitted_answers"].pop("_file_transport_token")
59
+ data["submitted_answers"]["_files"] = [{"name": "notes.txt"}]
35
60
  return result
36
61
 
37
62
 
@@ -183,6 +208,44 @@ def test_element_owned_answer_supplies_submission_without_correct_answer(
183
208
  assert result.partial_scores == {"diagram": {"score": 1}}
184
209
 
185
210
 
211
+ def test_unassociated_raw_file_field_does_not_require_submitted_answer(
212
+ tmp_path: Path,
213
+ ) -> None:
214
+ harness = _answer_harness(
215
+ tmp_path,
216
+ UnassociatedFileFieldBackend(),
217
+ html='<custom-answer answers-name="diagram"></custom-answer>',
218
+ selector="custom-answer",
219
+ )
220
+
221
+ result = harness.run_grade(0)
222
+
223
+ assert result.raw_submitted_answers == {
224
+ "diagram": '{"nodes":[1,2]}',
225
+ "_file_transport_token": '[{"name":"notes.txt"}]',
226
+ }
227
+ assert result.submitted_answers == {
228
+ "diagram": '{"nodes":[1,2]}',
229
+ "_files": [{"name": "notes.txt"}],
230
+ }
231
+ assert result.partial_scores == {"diagram": {"score": 1}}
232
+
233
+
234
+ def test_direct_answer_rejects_unparsed_value(tmp_path: Path) -> None:
235
+ harness = _answer_harness(
236
+ tmp_path,
237
+ ElementOwnedAnswerBackend(null_after_parse=True),
238
+ html='<custom-answer answers-name="diagram"></custom-answer>',
239
+ selector="custom-answer",
240
+ )
241
+
242
+ with pytest.raises(
243
+ HarnessError,
244
+ match="raw submission fields did not parse successfully: diagram",
245
+ ):
246
+ harness.run_grade(0)
247
+
248
+
186
249
  def test_question_test_hook_round_trips_structured_submission(tmp_path: Path) -> None:
187
250
  harness = _answer_harness(
188
251
  tmp_path,
@@ -62,6 +62,9 @@ def prepare_release_repository(
62
62
  """#!/usr/bin/env bash
63
63
  set -euo pipefail
64
64
  printf '%s\\n' "$*" >> "${FAKE_UV_LOG}"
65
+ if [[ -n "${FAKE_UV_FAIL_ON:-}" && "$*" == "${FAKE_UV_FAIL_ON}" ]]; then
66
+ exit 1
67
+ fi
65
68
  if [[ "${1:-}" == version && "${2:-}" == --short ]]; then
66
69
  printf '%s\\n' "${FAKE_CURRENT_VERSION}"
67
70
  fi
@@ -109,6 +112,13 @@ def test_publish_version_can_tag_an_unpushed_prebumped_commit(tmp_path: Path):
109
112
  == release_commit
110
113
  )
111
114
  assert "version 0.4.0" not in uv_log.read_text().splitlines()
115
+ assert uv_log.read_text().splitlines()[1:3] == [
116
+ "python install 3.13",
117
+ "sync --locked --python 3.13",
118
+ ]
119
+ assert "run --python 3.13 python scripts/smoke_dist.py" in (
120
+ uv_log.read_text().splitlines()
121
+ )
112
122
  assert "Published v0.4.0" in result.stdout
113
123
 
114
124
 
@@ -132,3 +142,28 @@ def test_publish_version_rejects_pushed_prebumped_commit(tmp_path: Path):
132
142
 
133
143
  assert result.returncode == 1
134
144
  assert "prebumped-version bypass requires an unpushed commit" in result.stderr
145
+
146
+
147
+ def test_publish_version_does_not_tag_when_locked_sync_fails(tmp_path: Path):
148
+ repository, remote, environment, uv_log = prepare_release_repository(tmp_path)
149
+ release_commit = git("rev-parse", "HEAD", cwd=repository)
150
+ environment["FAKE_UV_FAIL_ON"] = "sync --locked --python 3.13"
151
+
152
+ result = publish(repository, environment, allow_prebumped=True)
153
+
154
+ assert result.returncode == 1
155
+ assert git("rev-parse", "HEAD", cwd=repository) == release_commit
156
+ assert (
157
+ run(
158
+ "git",
159
+ "--git-dir",
160
+ str(remote),
161
+ "rev-parse",
162
+ "--verify",
163
+ "refs/tags/v0.4.0",
164
+ cwd=tmp_path,
165
+ check=False,
166
+ ).returncode
167
+ != 0
168
+ )
169
+ assert uv_log.read_text().splitlines()[-1] == "sync --locked --python 3.13"
File without changes
File without changes
File without changes