pl-e2e-test 0.2.1__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.
Files changed (65) hide show
  1. {pl_e2e_test-0.2.1/src/pl_e2e_test.egg-info → pl_e2e_test-0.2.2}/PKG-INFO +2 -2
  2. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/README.md +1 -1
  3. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/pyproject.toml +1 -1
  4. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/scripts/demo_cli.py +3 -3
  5. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2/src/pl_e2e_test.egg-info}/PKG-INFO +2 -2
  6. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/parallel.py +35 -18
  7. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/tests/test_demo_cli.py +17 -4
  8. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/tests/test_parallel.py +92 -4
  9. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/LICENSE +0 -0
  10. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/MANIFEST.in +0 -0
  11. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/setup.cfg +0 -0
  12. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/src/pl_e2e_test.egg-info/SOURCES.txt +0 -0
  13. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/src/pl_e2e_test.egg-info/dependency_links.txt +0 -0
  14. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/src/pl_e2e_test.egg-info/entry_points.txt +0 -0
  15. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/src/pl_e2e_test.egg-info/requires.txt +0 -0
  16. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/src/pl_e2e_test.egg-info/top_level.txt +0 -0
  17. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/__init__.py +0 -0
  18. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/__main__.py +0 -0
  19. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/_git.py +0 -0
  20. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/_options.py +0 -0
  21. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/backend.py +0 -0
  22. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/cli.py +0 -0
  23. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/config.py +0 -0
  24. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/contract.py +0 -0
  25. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/discovery.py +0 -0
  26. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/errors.py +0 -0
  27. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/expected_errors.py +0 -0
  28. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/fuzz.py +0 -0
  29. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/harness.py +0 -0
  30. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/html_checks.py +0 -0
  31. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/instances.py +0 -0
  32. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/metadata.py +0 -0
  33. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/pytest_plugin.py +0 -0
  34. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/question_tests.py +0 -0
  35. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/regression.py +0 -0
  36. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/reporting.py +0 -0
  37. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/repository.py +0 -0
  38. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/runner.py +0 -0
  39. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/runtime.py +0 -0
  40. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/server.py +0 -0
  41. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/sharding.py +0 -0
  42. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/src/prairielearn_e2e/ui.py +0 -0
  43. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/tests/__init__.py +0 -0
  44. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/tests/conftest.py +0 -0
  45. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/tests/stub_git.py +0 -0
  46. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/tests/test_cli.py +0 -0
  47. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/tests/test_config.py +0 -0
  48. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/tests/test_contract.py +0 -0
  49. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/tests/test_discovery.py +0 -0
  50. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/tests/test_fuzz.py +0 -0
  51. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/tests/test_git.py +0 -0
  52. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/tests/test_harness.py +0 -0
  53. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/tests/test_html_checks.py +0 -0
  54. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/tests/test_instances.py +0 -0
  55. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/tests/test_metadata.py +0 -0
  56. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/tests/test_options.py +0 -0
  57. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/tests/test_publish_version.py +0 -0
  58. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/tests/test_pytest_plugin.py +0 -0
  59. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/tests/test_question_tests.py +0 -0
  60. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/tests/test_regression.py +0 -0
  61. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/tests/test_reporting.py +0 -0
  62. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/tests/test_repository.py +0 -0
  63. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/tests/test_runtime.py +0 -0
  64. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.2}/tests/test_server.py +0 -0
  65. {pl_e2e_test-0.2.1 → pl_e2e_test-0.2.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.2.1
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
@@ -115,7 +115,7 @@ uv run python scripts/demo_cli.py --list
115
115
  ```
116
116
 
117
117
  The demo covers help and CLI errors plus passing, failing, skipped, xfailed/xpassed,
118
- warning, collection-error, empty-selection, quiet, verbose, and per-shard progress
118
+ warning, collection-error, empty-selection, quiet, verbose, and aggregate shard progress
119
119
  output.
120
120
 
121
121
  `--fuzz-seeds` creates and reports a random 64-bit master seed. Reproduce the same
@@ -89,7 +89,7 @@ uv run python scripts/demo_cli.py --list
89
89
  ```
90
90
 
91
91
  The demo covers help and CLI errors plus passing, failing, skipped, xfailed/xpassed,
92
- warning, collection-error, empty-selection, quiet, verbose, and per-shard progress
92
+ warning, collection-error, empty-selection, quiet, verbose, and aggregate shard progress
93
93
  output.
94
94
 
95
95
  `--fuzz-seeds` creates and reports a random 64-bit master seed. Reproduce the same
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pl-e2e-test"
7
- version = "0.2.1"
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"
@@ -144,10 +144,10 @@ def _verbose(color: ColorChoice) -> int:
144
144
 
145
145
  def _shards(color: ColorChoice) -> int:
146
146
  console = make_console(color=color)
147
- outcomes = ("passed", "passed", "failed (1)", "passed")
147
+ outcomes = (True, True, False, True)
148
148
  with _shard_progress(console, len(outcomes), disable=False) as complete:
149
- for index, outcome in enumerate(outcomes, start=1):
150
- complete(index, outcome)
149
+ for index, succeeded in enumerate(outcomes, start=1):
150
+ complete(index, succeeded)
151
151
  return 1
152
152
 
153
153
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pl-e2e-test
3
- Version: 0.2.1
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
@@ -115,7 +115,7 @@ uv run python scripts/demo_cli.py --list
115
115
  ```
116
116
 
117
117
  The demo covers help and CLI errors plus passing, failing, skipped, xfailed/xpassed,
118
- warning, collection-error, empty-selection, quiet, verbose, and per-shard progress
118
+ warning, collection-error, empty-selection, quiet, verbose, and aggregate shard progress
119
119
  output.
120
120
 
121
121
  `--fuzz-seeds` creates and reports a random 64-bit master seed. Reproduce the same
@@ -16,7 +16,7 @@ from tempfile import TemporaryDirectory
16
16
  from typing import IO
17
17
 
18
18
  from rich.console import Console
19
- from rich.progress import BarColumn, Progress, TaskID, TaskProgressColumn, TextColumn
19
+ from rich.progress import BarColumn, MofNCompleteColumn, Progress, TextColumn
20
20
  from rich.text import Text
21
21
 
22
22
  from ._options import scan_options
@@ -55,26 +55,45 @@ class _Child:
55
55
  @contextmanager
56
56
  def _shard_progress(
57
57
  console: Console, shard_count: int, *, disable: bool
58
- ) -> Generator[Callable[[int, str], None]]:
59
- """Render one parent-owned completion bar for every child shard."""
58
+ ) -> Generator[Callable[[int, bool], None]]:
59
+ """Render one parent-owned aggregate completion bar for child shards."""
60
60
 
61
+ bar = BarColumn(complete_style="green", finished_style="green")
61
62
  progress = Progress(
62
- TextColumn("Shard {task.fields[index]}/{task.fields[count]}"),
63
- BarColumn(),
64
- TaskProgressColumn(),
63
+ TextColumn("Shards"),
64
+ bar,
65
+ MofNCompleteColumn(),
65
66
  TextColumn("{task.description}"),
66
67
  console=console,
67
68
  disable=disable,
68
69
  )
69
- tasks: dict[int, TaskID] = {}
70
+ completed: set[int] = set()
71
+ passed = 0
72
+ failed = 0
70
73
  with progress:
71
- for index in range(1, shard_count + 1):
72
- tasks[index] = progress.add_task(
73
- "running", total=1, index=index, count=shard_count
74
- )
74
+ task = progress.add_task("running", total=shard_count)
75
+
76
+ def complete(index: int, succeeded: bool) -> None:
77
+ nonlocal failed, passed
75
78
 
76
- def complete(index: int, status: str) -> None:
77
- progress.update(tasks[index], description=status, completed=1)
79
+ if index in completed:
80
+ return
81
+ completed.add(index)
82
+ if succeeded:
83
+ passed += 1
84
+ else:
85
+ failed += 1
86
+ bar.complete_style = "red"
87
+ bar.finished_style = "red"
88
+ statuses = [f"{passed} passed"] if passed else []
89
+ if failed:
90
+ statuses.append(f"{failed} failed")
91
+ progress.update(
92
+ task,
93
+ completed=len(completed),
94
+ description=", ".join(statuses),
95
+ refresh=True,
96
+ )
78
97
 
79
98
  yield complete
80
99
 
@@ -291,12 +310,10 @@ def run_parallel(
291
310
  child.returncode = child.process.poll()
292
311
  if child.returncode is None:
293
312
  continue
294
- status = (
295
- "passed"
296
- if child.returncode == 0
297
- else f"failed ({child.returncode})"
313
+ complete_shard(
314
+ child.index,
315
+ child.returncode == 0 and child.report.is_file(),
298
316
  )
299
- complete_shard(child.index, status)
300
317
  pending.remove(child)
301
318
  if pending:
302
319
  time.sleep(0.05)
@@ -37,7 +37,7 @@ def test_mixed_demo_uses_native_reporter() -> None:
37
37
  assert result.stderr == ""
38
38
 
39
39
 
40
- def test_shard_demo_has_one_progress_bar_per_shard() -> None:
40
+ def test_shard_demo_has_one_aggregate_progress_bar() -> None:
41
41
  result = subprocess.run(
42
42
  [sys.executable, SCRIPT, "--color", "never", "shards"],
43
43
  check=True,
@@ -45,7 +45,20 @@ def test_shard_demo_has_one_progress_bar_per_shard() -> None:
45
45
  text=True,
46
46
  )
47
47
 
48
- for index in range(1, 5):
49
- assert f"Shard {index}/4" in result.stdout
50
- assert "failed (1)" in result.stdout
48
+ assert "Shards" in result.stdout
49
+ assert "4/4" in result.stdout
50
+ assert "3 passed, 1 failed" in result.stdout
51
+ assert all(f"Shard {index}/4" not in result.stdout for index in range(1, 5))
51
52
  assert "exit status: 1" in result.stdout
53
+
54
+
55
+ def test_failed_shard_demo_uses_a_red_aggregate_bar() -> None:
56
+ result = subprocess.run(
57
+ [sys.executable, SCRIPT, "--color", "always", "shards"],
58
+ check=True,
59
+ capture_output=True,
60
+ text=True,
61
+ )
62
+
63
+ assert "\x1b[31m" in result.stdout
64
+ assert "3 passed, 1 failed" in result.stdout
@@ -9,7 +9,7 @@ import sys
9
9
  import xml.etree.ElementTree as ET
10
10
  from collections.abc import Sequence
11
11
  from pathlib import Path
12
- from typing import Any, cast
12
+ from typing import Any, Self, cast
13
13
 
14
14
  import pytest
15
15
  from rich.console import Console
@@ -87,6 +87,87 @@ def test_invalid_count(count: int) -> None:
87
87
  run_parallel([], shard_count=count)
88
88
 
89
89
 
90
+ @pytest.mark.parametrize(
91
+ ("completions", "descriptions", "styles"),
92
+ [
93
+ (
94
+ [(2, True), (1, True)],
95
+ ["1 passed", "2 passed"],
96
+ ["green", "green"],
97
+ ),
98
+ (
99
+ [(3, True), (1, False), (2, True)],
100
+ ["1 passed", "1 passed, 1 failed", "2 passed, 1 failed"],
101
+ ["green", "red", "red"],
102
+ ),
103
+ ],
104
+ )
105
+ def test_shard_progress_aggregates_and_refreshes_each_completion(
106
+ monkeypatch: pytest.MonkeyPatch,
107
+ completions: list[tuple[int, bool]],
108
+ descriptions: list[str],
109
+ styles: list[str],
110
+ ) -> None:
111
+ created: list[FakeProgress] = []
112
+
113
+ class FakeProgress:
114
+ def __init__(self, *columns: object, **kwargs: object) -> None:
115
+ self.columns = columns
116
+ self.kwargs = kwargs
117
+ self.added: list[tuple[str, dict[str, object]]] = []
118
+ self.updates: list[tuple[dict[str, object], object, object]] = []
119
+ created.append(self)
120
+
121
+ def __enter__(self) -> Self:
122
+ return self
123
+
124
+ def __exit__(self, *args: object) -> None:
125
+ pass
126
+
127
+ def add_task(self, description: str, **kwargs: object) -> int:
128
+ self.added.append((description, kwargs))
129
+ return 42
130
+
131
+ def update(self, task: int, **kwargs: object) -> None:
132
+ assert task == 42
133
+ bar = next(
134
+ column
135
+ for column in self.columns
136
+ if isinstance(column, parallel.BarColumn)
137
+ )
138
+ self.updates.append((kwargs, bar.complete_style, bar.finished_style))
139
+
140
+ monkeypatch.setattr(parallel, "Progress", FakeProgress)
141
+
142
+ with parallel._shard_progress(
143
+ Console(file=io.StringIO()), len(completions), disable=False
144
+ ) as complete:
145
+ for index, succeeded in completions:
146
+ complete(index, succeeded)
147
+
148
+ assert len(created) == 1
149
+ progress = created[0]
150
+ assert progress.added == [("running", {"total": len(completions)})]
151
+ assert [update[0]["completed"] for update in progress.updates] == list(
152
+ range(1, len(completions) + 1)
153
+ )
154
+ assert [update[0]["description"] for update in progress.updates] == descriptions
155
+ assert all(update[0]["refresh"] is True for update in progress.updates)
156
+ assert [update[1] for update in progress.updates] == styles
157
+ assert [update[2] for update in progress.updates] == styles
158
+
159
+
160
+ def test_shard_progress_can_be_disabled() -> None:
161
+ output = io.StringIO()
162
+ console = Console(file=output, force_terminal=True)
163
+
164
+ with parallel._shard_progress(console, 2, disable=True) as complete:
165
+ complete(1, True)
166
+ complete(2, False)
167
+
168
+ assert output.getvalue() == ""
169
+
170
+
90
171
  class FinishedProcess:
91
172
  def __init__(self, status: int):
92
173
  self.status = status
@@ -167,10 +248,17 @@ def test_orchestration_and_diagnostics(
167
248
  assert all(not path.exists() for path in reports)
168
249
  assert "a warning" in output.getvalue()
169
250
  assert "hidden success output" not in output.getvalue()
251
+ rendered = output.getvalue()
170
252
  if not terminal:
171
- assert "Running 3 shards in parallel" in output.getvalue()
172
- for index in range(1, 4):
173
- assert (f"Shard {index}/3" in output.getvalue()) is terminal
253
+ assert "Running 3 shards in parallel" in rendered
254
+ assert ("Shards" in rendered) is terminal
255
+ if terminal:
256
+ assert "3/3" in rendered
257
+ if failure in {"missing", "launch", "exit"}:
258
+ assert "2 passed, 1 failed" in rendered
259
+ else:
260
+ assert "3 passed" in rendered
261
+ assert all(f"Shard {index}/3" not in rendered for index in range(1, 4))
174
262
  if failure in {"none", "exit"}:
175
263
  assert (tmp_path / "combined.xml").is_file()
176
264
  if failure == "launch":
File without changes
File without changes
File without changes