Add four harder benchmark tasks
All seven newer local models passed the original four tasks 20 of 20, so the benchmark no longer told them apart. The new tasks keep plans explicit, as the planner is told to write them, and make the work itself harder: - trace-bug: a wrong multi-currency total whose cause is a shared rate helper; discounts and tax go through it too, so a fix at the call site fails the check. - refactor-rename: rename load_config across a ten-module package, leaving load_config_defaults and reload_config alone. - spec-parser: implement parse_duration from an eight-rule SPEC.md given three examples, and write tests for every rule. - long-milestone: eight dependent steps adding due dates to a task tracker, from the model through storage versions to the CLI. Self-tests prove that a call-site fix, a blind text replace and a partial rename all fail their checks. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
f2c48d8b16
commit
25fd57351d
67 files changed
+2115
-2
No files matched your search
@@ -0,0 +1,89 @@
|
||||
import pytest
|
||||
from durations import parse_duration
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("text", "seconds"),
|
||||
[
|
||||
("2h", 7200), # rule 1
|
||||
("1h30m", 5400),
|
||||
("1d2h3m4s", 93784),
|
||||
("45s", 45),
|
||||
("1d", 86400),
|
||||
("1H30M", 5400), # rule 2
|
||||
("2D", 172800),
|
||||
(" 1h 30m ", 5400), # rule 4
|
||||
("1d 2h 3m 4s", 93784),
|
||||
("90", 90), # rule 5
|
||||
(" 7 ", 7),
|
||||
("1.5h", 5400), # rule 6
|
||||
("1.5m", 90),
|
||||
("0.25h", 900),
|
||||
("1:30", 90), # rule 7
|
||||
("1:02:03", 3723),
|
||||
("90:00", 5400),
|
||||
("0:05", 5),
|
||||
(" 1:30 ", 90),
|
||||
("0", 0), # rule 8
|
||||
("0s", 0),
|
||||
("0:00", 0),
|
||||
],
|
||||
)
|
||||
def test_valid(text: str, seconds: int) -> None:
|
||||
assert parse_duration(text) == seconds
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"text",
|
||||
[
|
||||
"1x", # rule 1: not a unit
|
||||
"h",
|
||||
".5h", # not a number
|
||||
"1.h",
|
||||
"+2h",
|
||||
"30m1h", # rule 3
|
||||
"1h2h",
|
||||
"1s1m",
|
||||
"1 h", # rule 4
|
||||
"1h 30", # rule 5
|
||||
"30 1h",
|
||||
"1h1.5m", # rule 6
|
||||
"0.5s",
|
||||
"0.01m",
|
||||
"1.5",
|
||||
"1:60", # rule 7
|
||||
"1:2",
|
||||
"1:02:60",
|
||||
"1:02:03:04",
|
||||
"1.5:30",
|
||||
"1: 30",
|
||||
"", # rule 8
|
||||
" ",
|
||||
"-5",
|
||||
"-1h",
|
||||
],
|
||||
)
|
||||
def test_invalid(text: str) -> None:
|
||||
with pytest.raises(ValueError) as caught:
|
||||
parse_duration(text)
|
||||
assert repr(text) in str(caught.value)
|
||||
|
||||
|
||||
def test_the_result_is_an_int() -> None:
|
||||
assert type(parse_duration("1.5h")) is int
|
||||
|
||||
|
||||
def test_the_plan_asked_for_tests_and_they_pass() -> None:
|
||||
# At least one valid and one invalid input for each of the eight rules.
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
run = subprocess.run(
|
||||
[sys.executable, "-m", "pytest", "-q", "test_durations.py"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
assert run.returncode == 0, run.stdout[-1500:]
|
||||
summary = run.stdout.strip().splitlines()[-1]
|
||||
assert int(summary.split()[0]) >= 16, summary
|
||||
@@ -0,0 +1,35 @@
|
||||
# parse_duration
|
||||
|
||||
`parse_duration(text: str) -> int` turns a human-written duration into a whole
|
||||
number of seconds.
|
||||
|
||||
A *number* below is one or more digits, optionally followed by a dot and one
|
||||
or more digits: `2`, `1.5`, `0.25`. Nothing else is a number: not `.5`, `1.`,
|
||||
`+2` or `-2`.
|
||||
|
||||
## Rules
|
||||
|
||||
1. **Unit form.** One or more components, each a number immediately followed
|
||||
by a unit: `d` (86400 seconds), `h` (3600), `m` (60), `s` (1).
|
||||
`"2h"` is 7200, `"1h30m"` is 5400, `"1d2h3m4s"` is 93784.
|
||||
2. **Case.** Units are case-insensitive: `"1H30M"` is 5400.
|
||||
3. **Order.** Components appear in the order d, h, m, s, and each unit at
|
||||
most once. `"30m1h"` and `"1h2h"` are invalid.
|
||||
4. **Spaces.** Spaces are allowed between components and around the whole
|
||||
text: `" 1h 30m "` is 5400. A space between a number and its unit is not
|
||||
allowed: `"1 h"` is invalid.
|
||||
5. **Bare number.** A number with no unit, and nothing else, is seconds:
|
||||
`"90"` is 90. A bare number mixed with components is invalid: `"1h 30"`.
|
||||
6. **Decimals.** Only the first component may have a decimal part:
|
||||
`"1.5h"` is 5400, but `"1h1.5m"` is invalid. The result must be a whole
|
||||
number of seconds: `"1.5m"` is 90, but `"0.5s"` and `"0.01m"` are invalid.
|
||||
7. **Clock form.** `M:SS` or `H:MM:SS`: whole numbers separated by colons.
|
||||
`"1:30"` is 90, `"1:02:03"` is 3723, `"90:00"` is 5400. Every field after
|
||||
the first is exactly two digits and below 60: `"1:60"`, `"1:2"` and
|
||||
`"1:02:60"` are invalid. More than three fields, decimals, and spaces inside
|
||||
the clock (`"1: 30"`) are invalid; spaces around it are allowed.
|
||||
8. **Zero and errors.** Zero is allowed: `"0"`, `"0s"` and `"0:00"` are all 0.
|
||||
An empty string, only spaces, or a negative duration is invalid.
|
||||
|
||||
Every invalid input raises `ValueError`, and its message includes the input
|
||||
exactly as given, in `repr` form: `invalid duration: '1h2h'`.
|
||||
@@ -0,0 +1,6 @@
|
||||
"""Human-written durations. See SPEC.md."""
|
||||
|
||||
|
||||
def parse_duration(text: str) -> int:
|
||||
"""A duration in whole seconds, as SPEC.md specifies."""
|
||||
raise NotImplementedError
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Human-written durations. See SPEC.md."""
|
||||
|
||||
import re
|
||||
from decimal import Decimal
|
||||
|
||||
_NUMBER = r"\d+(?:\.\d+)?"
|
||||
_UNIT_FORM = re.compile(
|
||||
rf"(?:(?P<d>{_NUMBER})d)?\s*(?:(?P<h>{_NUMBER})h)?\s*"
|
||||
rf"(?:(?P<m>{_NUMBER})m)?\s*(?:(?P<s>{_NUMBER})s)?",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
_CLOCK = re.compile(r"\d+(?::[0-5]\d){1,2}")
|
||||
_SECONDS = {"d": 86400, "h": 3600, "m": 60, "s": 1}
|
||||
|
||||
|
||||
def parse_duration(text: str) -> int:
|
||||
"""A duration in whole seconds, as SPEC.md specifies."""
|
||||
|
||||
def invalid() -> ValueError:
|
||||
return ValueError(f"invalid duration: {text!r}")
|
||||
|
||||
stripped = text.strip()
|
||||
if not stripped:
|
||||
raise invalid()
|
||||
if _CLOCK.fullmatch(stripped):
|
||||
total = 0
|
||||
for field in stripped.split(":"):
|
||||
total = total * 60 + int(field)
|
||||
return total
|
||||
if re.fullmatch(_NUMBER, stripped):
|
||||
seconds = Decimal(stripped)
|
||||
else:
|
||||
match = _UNIT_FORM.fullmatch(stripped)
|
||||
if not match or not any(match.groupdict().values()):
|
||||
raise invalid()
|
||||
present = [(unit, value) for unit, value in match.groupdict().items() if value]
|
||||
if any("." in value for _unit, value in present[1:]):
|
||||
raise invalid()
|
||||
seconds = sum(Decimal(value) * _SECONDS[unit] for unit, value in present)
|
||||
if seconds != seconds.to_integral_value():
|
||||
raise invalid()
|
||||
return int(seconds)
|
||||
@@ -0,0 +1,26 @@
|
||||
import pytest
|
||||
from durations import parse_duration
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("text", "seconds"),
|
||||
[
|
||||
("1h30m", 5400),
|
||||
("1H30M", 5400),
|
||||
(" 1h 30m ", 5400),
|
||||
("90", 90),
|
||||
("1.5h", 5400),
|
||||
("1:02:03", 3723),
|
||||
("0s", 0),
|
||||
],
|
||||
)
|
||||
def test_valid(text: str, seconds: int) -> None:
|
||||
assert parse_duration(text) == seconds
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"text", ["1x", "30m1h", "1 h", "1h 30", "1h1.5m", "0.5s", "1:60", "", "-5"]
|
||||
)
|
||||
def test_invalid(text: str) -> None:
|
||||
with pytest.raises(ValueError, match="invalid duration"):
|
||||
parse_duration(text)
|
||||
@@ -0,0 +1,15 @@
|
||||
title: Implement a parser from a written specification
|
||||
check: python -m pytest -q .bench_check
|
||||
plan: |
|
||||
Milestone 1: implement `parse_duration` as `SPEC.md` specifies.
|
||||
|
||||
1. Read all of `SPEC.md`. It has eight numbered rules; every one applies.
|
||||
2. In `durations.py`, replace the body of `parse_duration(text: str) -> int`
|
||||
so that it follows every rule in `SPEC.md`, including the inputs each
|
||||
rule says are invalid and the error message for them. Examples:
|
||||
"1h30m" -> 5400; "90" -> 90; "1:02:03" -> 3723.
|
||||
3. Create `test_durations.py` with pytest tests: at least one valid and one
|
||||
invalid input for each of the eight rules.
|
||||
4. Run `python -m pytest -q test_durations.py`, and fix `durations.py`
|
||||
until every test passes.
|
||||
5. Call finish.
|
||||
Reference in new issue
Block a user