patchground 0.1.0__py3-none-any.whl
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.
- patchground/__init__.py +10 -0
- patchground/__main__.py +3 -0
- patchground/authoring/__init__.py +5 -0
- patchground/authoring/create.py +179 -0
- patchground/authoring/templates/python-debug/TICKET.md.tmpl +11 -0
- patchground/authoring/templates/python-debug/challenge.yaml.tmpl +21 -0
- patchground/authoring/templates/python-debug/checks/hidden/tests/test_starter_hidden.py.tmpl +13 -0
- patchground/authoring/templates/python-debug/checks/visible/tests/test_starter.py.tmpl +9 -0
- patchground/authoring/templates/python-debug/environment/Dockerfile.tmpl +3 -0
- patchground/authoring/templates/python-debug/solution/reference.patch.tmpl +8 -0
- patchground/authoring/templates/python-debug/workspace/pyproject.toml.tmpl +8 -0
- patchground/authoring/templates/python-debug/workspace/src/starter.py.tmpl +3 -0
- patchground/authoring/templates/python-feature/TICKET.md.tmpl +13 -0
- patchground/authoring/templates/python-feature/challenge.yaml.tmpl +22 -0
- patchground/authoring/templates/python-feature/checks/hidden/tests/test_starter_hidden.py.tmpl +13 -0
- patchground/authoring/templates/python-feature/checks/visible/tests/test_starter.py.tmpl +9 -0
- patchground/authoring/templates/python-feature/environment/Dockerfile.tmpl +3 -0
- patchground/authoring/templates/python-feature/solution/reference.patch.tmpl +9 -0
- patchground/authoring/templates/python-feature/workspace/pyproject.toml.tmpl +8 -0
- patchground/authoring/templates/python-feature/workspace/src/starter.py.tmpl +8 -0
- patchground/authoring/templates/typescript-debug/TICKET.md.tmpl +11 -0
- patchground/authoring/templates/typescript-debug/challenge.yaml.tmpl +21 -0
- patchground/authoring/templates/typescript-debug/checks/hidden/tests/starter-hidden.test.ts.tmpl +14 -0
- patchground/authoring/templates/typescript-debug/checks/visible/tests/starter.test.ts.tmpl +10 -0
- patchground/authoring/templates/typescript-debug/solution/reference.patch.tmpl +9 -0
- patchground/authoring/templates/typescript-debug/workspace/package.json.tmpl +6 -0
- patchground/authoring/templates/typescript-debug/workspace/src/starter.ts.tmpl +4 -0
- patchground/authoring/templates/typescript-feature/TICKET.md.tmpl +13 -0
- patchground/authoring/templates/typescript-feature/challenge.yaml.tmpl +22 -0
- patchground/authoring/templates/typescript-feature/checks/hidden/tests/starter-hidden.test.ts.tmpl +14 -0
- patchground/authoring/templates/typescript-feature/checks/visible/tests/starter.test.ts.tmpl +10 -0
- patchground/authoring/templates/typescript-feature/solution/reference.patch.tmpl +10 -0
- patchground/authoring/templates/typescript-feature/workspace/package.json.tmpl +6 -0
- patchground/authoring/templates/typescript-feature/workspace/src/starter.ts.tmpl +9 -0
- patchground/automation/__init__.py +5 -0
- patchground/automation/__main__.py +131 -0
- patchground/automation/workflow.py +214 -0
- patchground/catalog/README.md +10 -0
- patchground/catalog/cache-expiry/TICKET.md +49 -0
- patchground/catalog/cache-expiry/challenge.yaml +32 -0
- patchground/catalog/cache-expiry/checks/hidden/tests/test_cache_hidden.py +164 -0
- patchground/catalog/cache-expiry/checks/visible/tests/test_cache.py +56 -0
- patchground/catalog/cache-expiry/debrief/questions.md +4 -0
- patchground/catalog/cache-expiry/environment/Dockerfile +3 -0
- patchground/catalog/cache-expiry/solution/reference.patch +41 -0
- patchground/catalog/cache-expiry/workspace/pyproject.toml +8 -0
- patchground/catalog/cache-expiry/workspace/src/profilecache/__init__.py +6 -0
- patchground/catalog/cache-expiry/workspace/src/profilecache/cache.py +67 -0
- patchground/catalog/cache-expiry/workspace/src/profilecache/client.py +38 -0
- patchground/catalog/csv-import-row-errors/TICKET.md +97 -0
- patchground/catalog/csv-import-row-errors/challenge.yaml +33 -0
- patchground/catalog/csv-import-row-errors/checks/hidden/tests/test_import_hidden.py +359 -0
- patchground/catalog/csv-import-row-errors/checks/visible/tests/test_import.py +34 -0
- patchground/catalog/csv-import-row-errors/debrief/questions.md +5 -0
- patchground/catalog/csv-import-row-errors/environment/Dockerfile +3 -0
- patchground/catalog/csv-import-row-errors/solution/reference.patch +74 -0
- patchground/catalog/csv-import-row-errors/workspace/pyproject.toml +8 -0
- patchground/catalog/csv-import-row-errors/workspace/src/stockimport/__init__.py +4 -0
- patchground/catalog/csv-import-row-errors/workspace/src/stockimport/importer.py +69 -0
- patchground/catalog/csv-import-row-errors/workspace/src/stockimport/models.py +32 -0
- patchground/catalog/duplicate-charges-on-retry/TICKET.md +28 -0
- patchground/catalog/duplicate-charges-on-retry/challenge.yaml +30 -0
- patchground/catalog/duplicate-charges-on-retry/checks/hidden/tests/test_payment_regressions.py +87 -0
- patchground/catalog/duplicate-charges-on-retry/checks/visible/tests/test_checkout.py +34 -0
- patchground/catalog/duplicate-charges-on-retry/debrief/questions.md +6 -0
- patchground/catalog/duplicate-charges-on-retry/environment/Dockerfile +3 -0
- patchground/catalog/duplicate-charges-on-retry/solution/reference.patch +11 -0
- patchground/catalog/duplicate-charges-on-retry/workspace/pyproject.toml +8 -0
- patchground/catalog/duplicate-charges-on-retry/workspace/src/payments/__init__.py +1 -0
- patchground/catalog/duplicate-charges-on-retry/workspace/src/payments/gateway.py +38 -0
- patchground/catalog/duplicate-charges-on-retry/workspace/src/payments/models.py +19 -0
- patchground/catalog/duplicate-charges-on-retry/workspace/src/payments/service.py +23 -0
- patchground/catalog/event-bus-subscriptions/TICKET.md +108 -0
- patchground/catalog/event-bus-subscriptions/challenge.yaml +33 -0
- patchground/catalog/event-bus-subscriptions/checks/hidden/tests/event-bus-contract.test.ts +566 -0
- patchground/catalog/event-bus-subscriptions/checks/visible/tests/event-bus.test.ts +70 -0
- patchground/catalog/event-bus-subscriptions/debrief/questions.md +5 -0
- patchground/catalog/event-bus-subscriptions/environment/Dockerfile +7 -0
- patchground/catalog/event-bus-subscriptions/environment/package-lock.json +1054 -0
- patchground/catalog/event-bus-subscriptions/environment/package.json +13 -0
- patchground/catalog/event-bus-subscriptions/environment/vitest-runner.mjs +124 -0
- patchground/catalog/event-bus-subscriptions/solution/reference.patch +82 -0
- patchground/catalog/event-bus-subscriptions/workspace/package.json +6 -0
- patchground/catalog/event-bus-subscriptions/workspace/src/bus.ts +32 -0
- patchground/catalog/order-notifications-boundary/TICKET.md +51 -0
- patchground/catalog/order-notifications-boundary/challenge.yaml +32 -0
- patchground/catalog/order-notifications-boundary/checks/hidden/tests/test_boundary_hidden.py +87 -0
- patchground/catalog/order-notifications-boundary/checks/visible/tests/test_confirmation.py +41 -0
- patchground/catalog/order-notifications-boundary/debrief/questions.md +5 -0
- patchground/catalog/order-notifications-boundary/environment/Dockerfile +3 -0
- patchground/catalog/order-notifications-boundary/solution/reference.patch +77 -0
- patchground/catalog/order-notifications-boundary/workspace/pyproject.toml +8 -0
- patchground/catalog/order-notifications-boundary/workspace/src/orders/__init__.py +6 -0
- patchground/catalog/order-notifications-boundary/workspace/src/orders/app.py +9 -0
- patchground/catalog/order-notifications-boundary/workspace/src/orders/models.py +25 -0
- patchground/catalog/order-notifications-boundary/workspace/src/orders/service.py +31 -0
- patchground/catalog/order-notifications-boundary/workspace/src/orders/smtp_gateway.py +19 -0
- patchground/catalog/order-notifications-boundary/workspace/src/orders/sqlite_store.py +44 -0
- patchground/catalog/pagination-last-page/TICKET.md +44 -0
- patchground/catalog/pagination-last-page/challenge.yaml +31 -0
- patchground/catalog/pagination-last-page/checks/hidden/tests/test_pagination_hidden.py +32 -0
- patchground/catalog/pagination-last-page/checks/visible/tests/test_pagination.py +37 -0
- patchground/catalog/pagination-last-page/debrief/questions.md +9 -0
- patchground/catalog/pagination-last-page/environment/Dockerfile +3 -0
- patchground/catalog/pagination-last-page/solution/reference.patch +22 -0
- patchground/catalog/pagination-last-page/workspace/pyproject.toml +8 -0
- patchground/catalog/pagination-last-page/workspace/src/catalog/__init__.py +1 -0
- patchground/catalog/pagination-last-page/workspace/src/catalog/pagination.py +52 -0
- patchground/catalog/release-version-ordering/TICKET.md +72 -0
- patchground/catalog/release-version-ordering/challenge.yaml +31 -0
- patchground/catalog/release-version-ordering/checks/hidden/tests/releasekit_contract.test.ts +92 -0
- patchground/catalog/release-version-ordering/checks/visible/tests/releasekit.test.ts +31 -0
- patchground/catalog/release-version-ordering/debrief/questions.md +9 -0
- patchground/catalog/release-version-ordering/environment/Dockerfile +7 -0
- patchground/catalog/release-version-ordering/environment/package-lock.json +1054 -0
- patchground/catalog/release-version-ordering/environment/package.json +13 -0
- patchground/catalog/release-version-ordering/environment/vitest-runner.mjs +124 -0
- patchground/catalog/release-version-ordering/solution/reference.patch +36 -0
- patchground/catalog/release-version-ordering/workspace/README.md +7 -0
- patchground/catalog/release-version-ordering/workspace/src/releasekit/index.ts +2 -0
- patchground/catalog/release-version-ordering/workspace/src/releasekit/ordering.ts +41 -0
- patchground/catalog/release-version-ordering/workspace/src/releasekit/parse.ts +24 -0
- patchground/cli/__init__.py +1 -0
- patchground/cli/app.py +59 -0
- patchground/cli/authoring.py +41 -0
- patchground/cli/consent.py +77 -0
- patchground/cli/discovery.py +189 -0
- patchground/cli/doctor.py +249 -0
- patchground/cli/packs.py +142 -0
- patchground/cli/practice.py +141 -0
- patchground/cli/render.py +58 -0
- patchground/cli/reports.py +53 -0
- patchground/cli/sessions.py +195 -0
- patchground/cli/validate.py +117 -0
- patchground/packs/__init__.py +23 -0
- patchground/packs/builtin.py +53 -0
- patchground/packs/discovery.py +285 -0
- patchground/packs/execution.py +420 -0
- patchground/packs/models.py +98 -0
- patchground/packs/registry.py +318 -0
- patchground/packs/source.py +230 -0
- patchground/packs/store.py +420 -0
- patchground/py.typed +0 -0
- patchground/report/__init__.py +6 -0
- patchground/report/analysis.py +188 -0
- patchground/report/models.py +59 -0
- patchground/report/regression.py +597 -0
- patchground/report/render.py +92 -0
- patchground/report/workflow.py +84 -0
- patchground/runner_assets/Dockerfile +7 -0
- patchground/runner_assets/__init__.py +16 -0
- patchground/runner_assets/package-lock.json +1054 -0
- patchground/runner_assets/package.json +13 -0
- patchground/runner_assets/vitest-runner.mjs +124 -0
- patchground/safeio/__init__.py +115 -0
- patchground/sandbox/__init__.py +39 -0
- patchground/sandbox/_engine.py +23 -0
- patchground/sandbox/build.py +202 -0
- patchground/sandbox/errors.py +23 -0
- patchground/sandbox/junit.py +76 -0
- patchground/sandbox/probe.py +118 -0
- patchground/sandbox/run.py +535 -0
- patchground/sessions/__init__.py +44 -0
- patchground/sessions/diagnostics.py +145 -0
- patchground/sessions/files.py +113 -0
- patchground/sessions/git.py +70 -0
- patchground/sessions/history.py +386 -0
- patchground/sessions/history_ignore.py +279 -0
- patchground/sessions/models.py +126 -0
- patchground/sessions/practice.py +202 -0
- patchground/sessions/reset.py +121 -0
- patchground/sessions/store.py +203 -0
- patchground/sessions/submission.py +344 -0
- patchground/sessions/workflow.py +297 -0
- patchground/spec/__init__.py +35 -0
- patchground/spec/_schema.py +53 -0
- patchground/spec/loader.py +713 -0
- patchground/spec/models.py +394 -0
- patchground/spec/schema.py +50 -0
- patchground/verify/__init__.py +29 -0
- patchground/verify/errors.py +10 -0
- patchground/verify/pytest_adapter.py +95 -0
- patchground/verify/record.py +94 -0
- patchground/verify/reference.py +158 -0
- patchground/verify/result.py +199 -0
- patchground/verify/runners.py +47 -0
- patchground/verify/tampering.py +106 -0
- patchground/verify/validate.py +686 -0
- patchground/verify/vitest_adapter.py +252 -0
- patchground-0.1.0.dist-info/METADATA +258 -0
- patchground-0.1.0.dist-info/RECORD +194 -0
- patchground-0.1.0.dist-info/WHEEL +4 -0
- patchground-0.1.0.dist-info/entry_points.txt +3 -0
- patchground-0.1.0.dist-info/licenses/LICENSE +202 -0
patchground/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Patchground: real code, real problems, your tools."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("patchground")
|
|
7
|
+
except PackageNotFoundError: # pragma: no cover - running from a source tree without install
|
|
8
|
+
__version__ = "0.0.0"
|
|
9
|
+
|
|
10
|
+
__all__ = ["__version__"]
|
patchground/__main__.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Create a runnable challenge from bundled, non-executable template resources."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
from contextlib import ExitStack, suppress
|
|
8
|
+
from importlib.resources import files
|
|
9
|
+
from pathlib import Path, PurePosixPath
|
|
10
|
+
from typing import Literal
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, ConfigDict
|
|
13
|
+
|
|
14
|
+
from patchground import runner_assets
|
|
15
|
+
from patchground.spec import ManifestError, load_challenge
|
|
16
|
+
from patchground.spec.models import SLUG_PATTERN
|
|
17
|
+
|
|
18
|
+
TEMPLATE_FILES = (
|
|
19
|
+
"challenge.yaml",
|
|
20
|
+
"TICKET.md",
|
|
21
|
+
"workspace/pyproject.toml",
|
|
22
|
+
"workspace/src/starter.py",
|
|
23
|
+
"checks/visible/tests/test_starter.py",
|
|
24
|
+
"checks/hidden/tests/test_starter_hidden.py",
|
|
25
|
+
"environment/Dockerfile",
|
|
26
|
+
"solution/reference.patch",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_TYPESCRIPT_FILES = (
|
|
31
|
+
"challenge.yaml",
|
|
32
|
+
"TICKET.md",
|
|
33
|
+
"workspace/package.json",
|
|
34
|
+
"workspace/src/starter.ts",
|
|
35
|
+
"checks/visible/tests/starter.test.ts",
|
|
36
|
+
"checks/hidden/tests/starter-hidden.test.ts",
|
|
37
|
+
"solution/reference.patch",
|
|
38
|
+
)
|
|
39
|
+
# (type, language) -> bundled template folder and its template-backed relative files. TypeScript
|
|
40
|
+
# starters additionally receive the canonical runner environment from `runner_assets`.
|
|
41
|
+
_STARTERS = {
|
|
42
|
+
("debug", "python"): ("python-debug", TEMPLATE_FILES),
|
|
43
|
+
("feature", "python"): ("python-feature", TEMPLATE_FILES),
|
|
44
|
+
("debug", "typescript"): ("typescript-debug", _TYPESCRIPT_FILES),
|
|
45
|
+
("feature", "typescript"): ("typescript-feature", _TYPESCRIPT_FILES),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class AuthoringError(Exception):
|
|
50
|
+
"""Invalid scaffold input, an occupied destination or unavailable local storage."""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class CreatedChallenge(BaseModel):
|
|
54
|
+
model_config = ConfigDict(frozen=True)
|
|
55
|
+
|
|
56
|
+
id: str
|
|
57
|
+
path: Path
|
|
58
|
+
type: Literal["debug", "feature"] = "debug"
|
|
59
|
+
language: Literal["python", "typescript"] = "python"
|
|
60
|
+
files: tuple[str, ...] = TEMPLATE_FILES
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _identity(parent: int, name: str) -> tuple[int, int]:
|
|
64
|
+
info = os.stat(name, dir_fd=parent, follow_symlinks=False)
|
|
65
|
+
return info.st_dev, info.st_ino
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _runner_environment() -> dict[str, bytes]:
|
|
69
|
+
"""Canonical TypeScript environment files, verbatim and keyed by path under environment/."""
|
|
70
|
+
try:
|
|
71
|
+
assets = runner_assets.vitest_assets()
|
|
72
|
+
except Exception as exc:
|
|
73
|
+
raise AuthoringError("TypeScript runner assets are unavailable") from exc
|
|
74
|
+
if "Dockerfile" not in assets:
|
|
75
|
+
raise AuthoringError("TypeScript runner assets are incomplete")
|
|
76
|
+
names = set(assets)
|
|
77
|
+
for name, content in assets.items():
|
|
78
|
+
path = PurePosixPath(name)
|
|
79
|
+
if (
|
|
80
|
+
not isinstance(content, bytes)
|
|
81
|
+
or path.is_absolute()
|
|
82
|
+
or str(path) != name
|
|
83
|
+
or ".." in path.parts
|
|
84
|
+
or "\\" in name
|
|
85
|
+
or any(str(parent) in names for parent in path.parents)
|
|
86
|
+
):
|
|
87
|
+
raise AuthoringError("TypeScript runner assets are invalid")
|
|
88
|
+
return {f"environment/{name}": content for name, content in assets.items()}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def create_challenge(
|
|
92
|
+
challenge_id: str, *, type_: str = "debug", language: str = "python"
|
|
93
|
+
) -> CreatedChallenge:
|
|
94
|
+
"""Reserve cwd/id exclusively; never overwrite, run a challenge or initialize Git."""
|
|
95
|
+
if re.fullmatch(SLUG_PATTERN, challenge_id) is None:
|
|
96
|
+
raise AuthoringError("id must use lowercase letters, digits and single hyphens")
|
|
97
|
+
if (type_, language) not in _STARTERS:
|
|
98
|
+
raise AuthoringError("supported templates: --type debug|feature --lang python|typescript")
|
|
99
|
+
folder, names = _STARTERS[(type_, language)]
|
|
100
|
+
template = files("patchground.authoring").joinpath("templates", folder)
|
|
101
|
+
contents = {
|
|
102
|
+
name: template.joinpath(name + ".tmpl")
|
|
103
|
+
.read_bytes()
|
|
104
|
+
.replace(b"__CHALLENGE_ID__", challenge_id.encode("ascii"))
|
|
105
|
+
for name in names
|
|
106
|
+
}
|
|
107
|
+
if language == "typescript":
|
|
108
|
+
contents.update(_runner_environment())
|
|
109
|
+
created: list[tuple[int, str, tuple[int, int], bool]] = []
|
|
110
|
+
with ExitStack() as handles:
|
|
111
|
+
try:
|
|
112
|
+
parent = os.open(".", os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
|
|
113
|
+
handles.callback(os.close, parent)
|
|
114
|
+
try:
|
|
115
|
+
os.mkdir(challenge_id, mode=0o700, dir_fd=parent)
|
|
116
|
+
except FileExistsError as exc:
|
|
117
|
+
raise AuthoringError(
|
|
118
|
+
"destination already exists; choose a new challenge id"
|
|
119
|
+
) from exc
|
|
120
|
+
root_identity = _identity(parent, challenge_id)
|
|
121
|
+
created.append((parent, challenge_id, root_identity, True))
|
|
122
|
+
root = os.open(
|
|
123
|
+
challenge_id, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=parent
|
|
124
|
+
)
|
|
125
|
+
handles.callback(os.close, root)
|
|
126
|
+
directories: dict[tuple[str, ...], int] = {(): root}
|
|
127
|
+
for relative, content in contents.items():
|
|
128
|
+
parts = Path(relative).parts
|
|
129
|
+
for length in range(1, len(parts)):
|
|
130
|
+
key = parts[:length]
|
|
131
|
+
if key in directories:
|
|
132
|
+
continue
|
|
133
|
+
directory = directories[key[:-1]]
|
|
134
|
+
os.mkdir(key[-1], mode=0o700, dir_fd=directory)
|
|
135
|
+
created.append((directory, key[-1], _identity(directory, key[-1]), True))
|
|
136
|
+
descriptor = os.open(
|
|
137
|
+
key[-1], os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=directory
|
|
138
|
+
)
|
|
139
|
+
handles.callback(os.close, descriptor)
|
|
140
|
+
directories[key] = descriptor
|
|
141
|
+
directory = directories[parts[:-1]]
|
|
142
|
+
descriptor = os.open(
|
|
143
|
+
parts[-1],
|
|
144
|
+
os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,
|
|
145
|
+
0o600,
|
|
146
|
+
dir_fd=directory,
|
|
147
|
+
)
|
|
148
|
+
with os.fdopen(descriptor, "wb") as stream:
|
|
149
|
+
info = os.fstat(stream.fileno())
|
|
150
|
+
created.append((directory, parts[-1], (info.st_dev, info.st_ino), False))
|
|
151
|
+
stream.write(content)
|
|
152
|
+
stream.flush()
|
|
153
|
+
os.fsync(stream.fileno())
|
|
154
|
+
for directory, name, identity, _is_directory in created:
|
|
155
|
+
if _identity(directory, name) != identity:
|
|
156
|
+
raise AuthoringError("scaffold destination changed during creation")
|
|
157
|
+
load_challenge(Path(challenge_id))
|
|
158
|
+
except BaseException as exc:
|
|
159
|
+
for directory, name, identity, is_directory in reversed(created):
|
|
160
|
+
with suppress(OSError):
|
|
161
|
+
if _identity(directory, name) == identity:
|
|
162
|
+
if is_directory:
|
|
163
|
+
os.rmdir(name, dir_fd=directory)
|
|
164
|
+
else:
|
|
165
|
+
os.unlink(name, dir_fd=directory)
|
|
166
|
+
if isinstance(exc, OSError | ManifestError):
|
|
167
|
+
raise AuthoringError(
|
|
168
|
+
"could not create the scaffold; check disk space and directory access"
|
|
169
|
+
) from exc
|
|
170
|
+
raise
|
|
171
|
+
return CreatedChallenge.model_validate(
|
|
172
|
+
{
|
|
173
|
+
"id": challenge_id,
|
|
174
|
+
"path": Path(challenge_id),
|
|
175
|
+
"type": type_,
|
|
176
|
+
"language": language,
|
|
177
|
+
"files": tuple(contents),
|
|
178
|
+
}
|
|
179
|
+
)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Line-item totals are sometimes too small
|
|
2
|
+
|
|
3
|
+
The total displayed for a list of integer amounts is sometimes smaller than expected.
|
|
4
|
+
An empty list should total zero, and every supplied amount should contribute exactly once.
|
|
5
|
+
|
|
6
|
+
Investigate `total` in `src/starter.py`. Keep its public API and add a regression test.
|
|
7
|
+
Run `pgnd check` from a session workspace to verify the fix in Docker.
|
|
8
|
+
|
|
9
|
+
This is a tiny teaching starter for challenge authors, not a curated realistic exercise.
|
|
10
|
+
Authors should replace its story, application, tests and reference together, then run
|
|
11
|
+
`pgnd validate` before sharing it.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
spec_version: "0.1"
|
|
2
|
+
id: __CHALLENGE_ID__
|
|
3
|
+
version: 0.1.0
|
|
4
|
+
title: The last line item is missing from the total
|
|
5
|
+
type: debug
|
|
6
|
+
summary: Investigate incorrect totals without changing the public API.
|
|
7
|
+
languages: [python]
|
|
8
|
+
tags: [starter]
|
|
9
|
+
difficulty: beginner
|
|
10
|
+
timebox_minutes: 15
|
|
11
|
+
ai_policy: allowed
|
|
12
|
+
environment:
|
|
13
|
+
dockerfile: environment/Dockerfile
|
|
14
|
+
services: []
|
|
15
|
+
test_command: pytest -q -p no:cacheprovider --junitxml=$PG_RESULTS/junit.xml
|
|
16
|
+
checks:
|
|
17
|
+
visible: checks/visible
|
|
18
|
+
hidden: checks/hidden
|
|
19
|
+
solution: solution/reference.patch
|
|
20
|
+
author: {name: Challenge author}
|
|
21
|
+
content_license: CC-BY-4.0
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Add an average of the line-item amounts
|
|
2
|
+
|
|
3
|
+
Reports need the mean of a list of integer amounts. Implement `average` in `src/starter.py`:
|
|
4
|
+
|
|
5
|
+
- `average(items)` returns the arithmetic mean of the amounts as a `float`.
|
|
6
|
+
- An empty list averages `0.0` instead of raising an error.
|
|
7
|
+
- `total` and the public signatures stay as they are.
|
|
8
|
+
|
|
9
|
+
Add tests for your implementation. Run `pgnd check` from a session workspace to verify it in Docker.
|
|
10
|
+
|
|
11
|
+
This is a tiny teaching starter for challenge authors, not a curated realistic exercise.
|
|
12
|
+
Authors should replace its story, application, tests and reference together, then run
|
|
13
|
+
`pgnd validate` before sharing it.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
spec_version: "0.1"
|
|
2
|
+
id: __CHALLENGE_ID__
|
|
3
|
+
version: 0.1.0
|
|
4
|
+
title: Add an average to the line-item helpers
|
|
5
|
+
type: feature
|
|
6
|
+
summary: Add the missing average helper without changing the existing total.
|
|
7
|
+
languages: [python]
|
|
8
|
+
tags: [starter]
|
|
9
|
+
difficulty: beginner
|
|
10
|
+
timebox_minutes: 15
|
|
11
|
+
ai_policy: allowed
|
|
12
|
+
environment:
|
|
13
|
+
dockerfile: environment/Dockerfile
|
|
14
|
+
services: []
|
|
15
|
+
test_command: pytest -q -p no:cacheprovider --junitxml=$PG_RESULTS/junit.xml
|
|
16
|
+
checks:
|
|
17
|
+
visible: checks/visible
|
|
18
|
+
hidden: checks/hidden
|
|
19
|
+
solution: solution/reference.patch
|
|
20
|
+
expected_paths: [src/, tests/]
|
|
21
|
+
author: {name: Challenge author}
|
|
22
|
+
content_license: CC-BY-4.0
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
diff --git a/src/starter.py b/src/starter.py
|
|
2
|
+
--- a/src/starter.py
|
|
3
|
+
+++ b/src/starter.py
|
|
4
|
+
@@ -4,4 +4,4 @@
|
|
5
|
+
|
|
6
|
+
def average(items: list[int]) -> float:
|
|
7
|
+
"""Return the mean of the amounts, or 0.0 for an empty list."""
|
|
8
|
+
- raise NotImplementedError
|
|
9
|
+
+ return sum(items) / len(items) if items else 0.0
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Line-item totals are sometimes too small
|
|
2
|
+
|
|
3
|
+
The total displayed for a list of integer amounts is sometimes smaller than expected.
|
|
4
|
+
An empty list should total zero, and every supplied amount should contribute exactly once.
|
|
5
|
+
|
|
6
|
+
Investigate `total` in `src/starter.ts`. Keep its public API and add a regression test.
|
|
7
|
+
Run `pgnd check` from a session workspace to verify the fix in Docker.
|
|
8
|
+
|
|
9
|
+
This is a tiny teaching starter for challenge authors, not a curated realistic exercise.
|
|
10
|
+
Authors should replace its story, application, tests and reference together, then run
|
|
11
|
+
`pgnd validate` before sharing it.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
spec_version: "0.1"
|
|
2
|
+
id: __CHALLENGE_ID__
|
|
3
|
+
version: 0.1.0
|
|
4
|
+
title: The last line item is missing from the total
|
|
5
|
+
type: debug
|
|
6
|
+
summary: Investigate incorrect totals without changing the public API.
|
|
7
|
+
languages: [typescript]
|
|
8
|
+
tags: [starter]
|
|
9
|
+
difficulty: beginner
|
|
10
|
+
timebox_minutes: 15
|
|
11
|
+
ai_policy: allowed
|
|
12
|
+
environment:
|
|
13
|
+
dockerfile: environment/Dockerfile
|
|
14
|
+
services: []
|
|
15
|
+
test_command: node /opt/patchground/vitest-runner.mjs
|
|
16
|
+
checks:
|
|
17
|
+
visible: checks/visible
|
|
18
|
+
hidden: checks/hidden
|
|
19
|
+
solution: solution/reference.patch
|
|
20
|
+
author: {name: Challenge author}
|
|
21
|
+
content_license: CC-BY-4.0
|
patchground/authoring/templates/typescript-debug/checks/hidden/tests/starter-hidden.test.ts.tmpl
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { expect, test } from "vitest";
|
|
2
|
+
import { total } from "../src/starter";
|
|
3
|
+
|
|
4
|
+
test("total includes the final amount", () => {
|
|
5
|
+
expect(total([3, 2])).toBe(5);
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
test("total of a single amount", () => {
|
|
9
|
+
expect(total([7])).toBe(7);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("total handles negative amounts", () => {
|
|
13
|
+
expect(total([4, -2])).toBe(2);
|
|
14
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { expect, test } from "vitest";
|
|
2
|
+
import { total } from "../src/starter";
|
|
3
|
+
|
|
4
|
+
test("total of an empty list is zero", () => {
|
|
5
|
+
expect(total([])).toBe(0);
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
test("total ignores a zero final amount", () => {
|
|
9
|
+
expect(total([4, 5, 0])).toBe(9);
|
|
10
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
diff --git a/src/starter.ts b/src/starter.ts
|
|
2
|
+
--- a/src/starter.ts
|
|
3
|
+
+++ b/src/starter.ts
|
|
4
|
+
@@ -1,4 +1,4 @@
|
|
5
|
+
/** Return the sum of the supplied integer amounts. */
|
|
6
|
+
export function total(items: number[]): number {
|
|
7
|
+
- return items.slice(0, -1).reduce((sum, amount) => sum + amount, 0);
|
|
8
|
+
+ return items.reduce((sum, amount) => sum + amount, 0);
|
|
9
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Add an average of the line-item amounts
|
|
2
|
+
|
|
3
|
+
Reports need the mean of a list of integer amounts. Implement `average` in `src/starter.ts`:
|
|
4
|
+
|
|
5
|
+
- `average(items)` returns the arithmetic mean of the amounts as a `float`.
|
|
6
|
+
- An empty list averages `0.0` instead of raising an error.
|
|
7
|
+
- `total` and the public signatures stay as they are.
|
|
8
|
+
|
|
9
|
+
Add tests for your implementation. Run `pgnd check` from a session workspace to verify it in Docker.
|
|
10
|
+
|
|
11
|
+
This is a tiny teaching starter for challenge authors, not a curated realistic exercise.
|
|
12
|
+
Authors should replace its story, application, tests and reference together, then run
|
|
13
|
+
`pgnd validate` before sharing it.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
spec_version: "0.1"
|
|
2
|
+
id: __CHALLENGE_ID__
|
|
3
|
+
version: 0.1.0
|
|
4
|
+
title: Add an average to the line-item helpers
|
|
5
|
+
type: feature
|
|
6
|
+
summary: Add the missing average helper without changing the existing total.
|
|
7
|
+
languages: [typescript]
|
|
8
|
+
tags: [starter]
|
|
9
|
+
difficulty: beginner
|
|
10
|
+
timebox_minutes: 15
|
|
11
|
+
ai_policy: allowed
|
|
12
|
+
environment:
|
|
13
|
+
dockerfile: environment/Dockerfile
|
|
14
|
+
services: []
|
|
15
|
+
test_command: node /opt/patchground/vitest-runner.mjs
|
|
16
|
+
checks:
|
|
17
|
+
visible: checks/visible
|
|
18
|
+
hidden: checks/hidden
|
|
19
|
+
solution: solution/reference.patch
|
|
20
|
+
expected_paths: [src/, tests/]
|
|
21
|
+
author: {name: Challenge author}
|
|
22
|
+
content_license: CC-BY-4.0
|
patchground/authoring/templates/typescript-feature/checks/hidden/tests/starter-hidden.test.ts.tmpl
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { expect, test } from "vitest";
|
|
2
|
+
import { average } from "../src/starter";
|
|
3
|
+
|
|
4
|
+
test("average of the amounts", () => {
|
|
5
|
+
expect(average([2, 4])).toBe(3);
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
test("average of one amount", () => {
|
|
9
|
+
expect(average([7])).toBe(7);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("average of an empty list is zero", () => {
|
|
13
|
+
expect(average([])).toBe(0);
|
|
14
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { expect, test } from "vitest";
|
|
2
|
+
import { average, total } from "../src/starter";
|
|
3
|
+
|
|
4
|
+
test("total of the amounts", () => {
|
|
5
|
+
expect(total([4, 5, 0])).toBe(9);
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
test("average is exported", () => {
|
|
9
|
+
expect(typeof average).toBe("function");
|
|
10
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
diff --git a/src/starter.ts b/src/starter.ts
|
|
2
|
+
--- a/src/starter.ts
|
|
3
|
+
+++ b/src/starter.ts
|
|
4
|
+
@@ -5,5 +5,5 @@
|
|
5
|
+
|
|
6
|
+
/** Return the mean of the amounts, or 0 for an empty list. */
|
|
7
|
+
export function average(items: number[]): number {
|
|
8
|
+
- throw new Error("not implemented");
|
|
9
|
+
+ return items.length === 0 ? 0 : total(items) / items.length;
|
|
10
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Return the sum of the supplied integer amounts. */
|
|
2
|
+
export function total(items: number[]): number {
|
|
3
|
+
return items.reduce((sum, amount) => sum + amount, 0);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/** Return the mean of the amounts, or 0 for an empty list. */
|
|
7
|
+
export function average(items: number[]): number {
|
|
8
|
+
throw new Error("not implemented");
|
|
9
|
+
}
|