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.
- {pl_e2e_test-0.1.6/src/pl_e2e_test.egg-info → pl_e2e_test-0.1.7}/PKG-INFO +9 -5
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/README.md +8 -4
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/pyproject.toml +1 -1
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7/src/pl_e2e_test.egg-info}/PKG-INFO +9 -5
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/harness.py +6 -6
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/test_harness.py +63 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/test_publish_version.py +35 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/LICENSE +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/MANIFEST.in +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/setup.cfg +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/pl_e2e_test.egg-info/SOURCES.txt +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/pl_e2e_test.egg-info/dependency_links.txt +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/pl_e2e_test.egg-info/entry_points.txt +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/pl_e2e_test.egg-info/requires.txt +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/pl_e2e_test.egg-info/top_level.txt +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/__init__.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/__main__.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/_git.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/backend.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/cli.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/config.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/contract.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/discovery.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/errors.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/expected_errors.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/fuzz.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/html_checks.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/metadata.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/parallel.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/pytest_plugin.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/question_tests.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/regression.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/reporting.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/repository.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/runner.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/server.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/src/prairielearn_e2e/sharding.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/__init__.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/conftest.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/stub_git.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/test_cli.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/test_config.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/test_contract.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/test_discovery.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/test_fuzz.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/test_git.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/test_html_checks.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/test_metadata.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/test_parallel.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/test_pytest_plugin.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/test_question_tests.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/test_regression.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/test_reporting.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/test_repository.py +0 -0
- {pl_e2e_test-0.1.6 → pl_e2e_test-0.1.7}/tests/test_server.py +0 -0
- {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.
|
|
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
|
|
339
|
-
non-null under its child key or produce a
|
|
340
|
-
answer. This also supports elements that
|
|
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
|
|
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
|
|
315
|
-
non-null under its child key or produce a
|
|
316
|
-
answer. This also supports elements that
|
|
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
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pl-e2e-test
|
|
3
|
-
Version: 0.1.
|
|
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
|
|
339
|
-
non-null under its child key or produce a
|
|
340
|
-
answer. This also supports elements that
|
|
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
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|