Skip to content

Add test generator - #380

Merged
keiravillekode merged 14 commits into
exercism:mainfrom
keiravillekode:generators-generate
Sep 28, 2026
Merged

keiravillekode merged 14 commits into
exercism:mainfrom
keiravillekode:generators-generate

Conversation

@keiravillekode

Copy link
Copy Markdown
Contributor

The generator is implemented in Python and modelled on the x86-64-assembly track's generator.

It keeps the stub in source/*.d and replaces everything from the unittest line onwards.

It uses .meta/additional-test-cases.json if the file exists.

Generators are added for

  • all-your-base
  • game-of-life
  • isbn-verifier
  • largest-series-product
  • matrix
  • phone-number

The generated tests are identical to the existing tests.

Python style is enforced by ruff, run by pre-commit and a new pre-commit workflow.

Upgrade workflows to ubuntu-26.04.

Sync .gitignore from generic-track.

I used Claude for this PR.

The generator is implemented in Python and modelled on the
x86-64-assembly track's generator.

It keeps the stub in source/*.d and replaces everything from the
`unittest` line onwards.

It uses `.meta/additional-test-cases.json` if the file exists.

Generators are added for

- all-your-base
- game-of-life
- isbn-verifier
- largest-series-product
- matrix
- phone-number

The generated tests are identical to the existing tests.

Python style is enforced by ruff, run by pre-commit and a new
pre-commit workflow.

Upgrade workflows to ubuntu-26.04.

Sync .gitignore from generic-track.

I used Claude for this PR.
@github-actions

Copy link
Copy Markdown
Contributor

Hello 👋 Thanks for your PR.

This repo does not currently have dedicated maintainers. Our cross-track maintainers team will attempt to review and merge your PR, but it will likely take longer for your PR to be reviewed.

If you enjoy contributing to Exercism and have a track-record of doing so successfully, you might like to become an Exercism maintainer for this track.

Please feel free to ask any questions, or chat to us about anything to do with this PR or the reviewing process on the Exercism forum.

(cc @exercism/cross-track-maintainers)

Comment thread .github/workflows/pre-commit.yml Outdated
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
with:
python-version: "3.12"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the default on Ubuntu 26? It seems old.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first draft of the file had Ubuntu 24. Yes it is the default there (Python 3.12.3).

(My desktop is also Ubuntu 24 with Python 3.12.3.)

Comment thread .github/workflows/pre-commit.yml Outdated
with:
python-version: "3.12"
- name: Set PY
run: echo "PY=$(python -VV | sha256sum | cut -d' ' -f1)" >> "$GITHUB_ENV"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the purpose of the hash here? Is the Python version set by hah and not version string?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was in ahans' exercism/arm64-assembly#158 and I had never looked into it. Turns out it is a key for the cache, but we can remove these lines.

Comment thread bin/add-practice-exercise Outdated
- Create the test suite in $(jq -r '.test' <<< "${files}")
- The tests should be based on the canonical data at 'https://github.com/exercism/problem-specifications/blob/main/exercises/${slug}/canonical-data.json'
- Any test cases you don't implement, mark them in 'exercises/practice/${slug}/.meta/tests.toml' with "include = false"
- The tests can be generated by adding 'generators/exercises/${slug//-/_}.py' and running 'bin/generate ${slug}'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make more sense to move this into .meta?

Comment thread bin/generate Outdated
# Example generating every exercise that has a generator:
# bin/generate --all

set -euo pipefail

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this wrapper add any value?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

Lean's wrapper does add value. I never gained muscle memory without it.

Yesterday it would have been nice to have

bin/configlet sync -u -e list-ops
bin/generate list-ops
bin/verify-exercises-in-docker list-ops

in every track.

Comment thread docs/ABOUT.md
Comment thread generators/generate.py Outdated
def generated_slugs() -> list[str]:
return sorted(
path.stem.replace("_", "-")
for path in GENERATORS.glob("*.py")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't the filename match the exercise name?

Comment thread generators/generate Outdated
for slug in slugs:
try:
generate(slug, specifications)
except GeneratorError as error:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the generator continue on error?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We now stop.

If a file was missing or the exercise stub was empty, we print an error message.

If an exception occurs in an exercise .meta/slug.py, it presumably won't be any of FileNotFoundError, EOFError, subprocess.CalledProcessError. We don't catch the error, and Python prints a stack trace.

Comment thread generators/lib.py Outdated
for char in value:
if char in _ESCAPES:
chars.append(_ESCAPES[char])
elif ord(char) < 0x20 or ord(char) == 0x7F:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this use the printable test?

Comment thread generators/lib.py Outdated

def d_int(value: int) -> str:
"""Render an integer as a D integer literal."""
if isinstance(value, bool) or not isinstance(value, int):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this test needed? The string function doesn't do type validation

Comment thread generators/lib.py Outdated
Comment thread .pre-commit-config.yaml
Comment thread .github/workflows/pre-commit.yml Outdated
Comment thread .pre-commit-config.yaml
rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of these checks (especially end-of-file) would be good to use widely. These might be nice to add to the generic-track* repos :D

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My impression was that the generic-* repos essentially never change, but now I notice you have merged to generic-test-runner every few months. Should I have created a PR when I noticed https://forum.exercism.org/t/dependabot-frequency/53725 ?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes please. We clone those for each new track. If we keep those up to date and shiny, new tracks start off with the latest and greatest tooling we have!

@@ -0,0 +1,14 @@
from lib import assert_eq, d_array, d_inline_array

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Out of curiosity, is there a reason you didn't want to use templates?
  2. If these files live in .meta, would it make sense for them to use a standard filename, eg test_generator.py, similar to the other meta files?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. I have never been a big user of templates, for example my first web development was with Java servlets and I never bothered with JSP. I can read and write without templates more easily than with templates

  2. Now using test_generator.py

Comment thread generators/generate Outdated
Comment on lines +44 to +51
def flatten(cases: list[Case], parents: tuple[str, ...] = ()) -> list[Case]:
"""Flatten nested groups, recording the descriptions of enclosing groups."""
flattened = []
for case in cases:
if "cases" in case:
description = case.get("description")
nested = (*parents, description) if description else parents
flattened.extend(flatten(case["cases"], nested))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def flatten(cases: list[Case], parents: tuple[str, ...] = ()) -> list[Case]:
"""Flatten nested groups, recording the descriptions of enclosing groups."""
flattened = []
for case in cases:
if "cases" in case:
description = case.get("description")
nested = (*parents, description) if description else parents
flattened.extend(flatten(case["cases"], nested))
def flatten(cases: list[Case], parents: list[str] | None = None) -> list[Case]:
"""Flatten nested groups, recording the descriptions of enclosing groups."""
flattened = []
if parents is None:
parents = []
for case in cases:
if "cases" in case:
description = case.get("description")
nested = parents + [description] if description else parents
flattened.extend(flatten(case["cases"], nested))

Comment thread generators/generate Outdated
Comment on lines +101 to +102
# Avoid leaving __pycache__ in the exercise directory.
sys.dont_write_bytecode = True

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably either go at the top of the file or, if you want to localize the change, be reset.

Comment thread generators/generate
Comment thread generators/generate
raise EOFError(f"{path.relative_to(ROOT)} is empty; create the stub first")
match = re.search(r"^unittest\b", content, flags=re.MULTILINE)
if match:
return content[: match.start()]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return content[: match.start()]
return content[:match.start()]

Make spacing consistent with describe()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ruff format demands that space and puts it back.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it's fine with description[:1].upper() on line 114? That's odd. But probably not worth fighting ruff over.

Comment thread generators/generate
Comment thread generators/lib.py
"\n": "\\n",
"\r": "\\r",
"\t": "\\t",
"\0": "\\0",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect there's no NULL or CR chars in any of the exercises, but I suppose it doesn't hurt to have these.

Comment thread .pre-commit-config.yaml
rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes please. We clone those for each new track. If we keep those up to date and shiny, new tracks start off with the latest and greatest tooling we have!

Comment thread .pre-commit-config.yaml Outdated
rev: v0.16.9
hooks:
- id: ruff-check
args: [--fix, --target-version, py314]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit surprised ruff doesn't pick up the system version 🤷‍♂️ We could also drop a .ruff.toml config file to move the version from the workflow file to the repo "main" content.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a .ruff.toml config file

@keiravillekode keiravillekode added the ready-to-translate The English in this PR is final; queue its translations label Sep 28, 2026
exercism-i18n Bot added a commit to exercism/i18n that referenced this pull request Sep 28, 2026
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@exercism-i18n

exercism-i18n Bot commented Sep 28, 2026

Copy link
Copy Markdown

This PR has been translated 🚀

@keiravillekode
keiravillekode merged commit 3d6eeb5 into exercism:main Sep 28, 2026
13 of 14 checks passed
@keiravillekode
keiravillekode deleted the generators-generate branch September 28, 2026 23:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-translate The English in this PR is final; queue its translations

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants