Skip to content

Commit 46b3043

Browse files
authored
Add test generator to track tooling (#495)
* Write test generator article * Link article to track tooling docs * Link article to new track docs * Add article to config.json
1 parent 7640d7f commit 46b3043

File tree

4 files changed

+167
-3
lines changed

4 files changed

+167
-3
lines changed

building/config.json

+7
Original file line numberDiff line numberDiff line change
@@ -820,6 +820,13 @@
820820
"title": "Snippet Extractor",
821821
"blurb": ""
822822
},
823+
{
824+
"uuid": "ba393f61-0f7d-4178-922a-c16cda052338",
825+
"slug": "tooling/test-generators",
826+
"path": "building/tooling/test-generators.md",
827+
"title": "Test Generators",
828+
"blurb": ""
829+
},
823830
{
824831
"uuid": "3423667a-3b30-4590-9a88-5a30f712f382",
825832
"slug": "product",

building/tooling/README.md

+6
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,9 @@ There is also general tooling that can be configured for your track:
1919

2020
- **[Lines of Code Counter](/docs/building/tooling/lines-of-code-counter)**
2121
- **[Snippet Extractor](/docs/building/tooling/snippet-extractor)**
22+
23+
## Tooling for contribution
24+
25+
Tracks may also provide tooling for contribution:
26+
27+
- **[Test Generators](/docs/building/tooling/test-generators)**

building/tooling/test-generators.md

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# Test Generators
2+
3+
A Test Generator is a piece of software that creates a practice exercise's tests from the common [problem specifications](https://github.com/exercism/problem-specifications).
4+
Some tracks also create tests for concept exercises from a similar track-owned data source.
5+
6+
A Test Generator give us these advantages:
7+
8+
1. They allow adding exercises more quickly without writing much boilerplate code.
9+
2. Contributors can focus on the **design** of an exercise immediately.
10+
3. Along the track life, automatic updates of existing tests can lower maintainer workload.
11+
12+
## Contributing to Test Generators
13+
14+
Each language may have its own Test Generator, written in that language.
15+
It adds code and sometimes files to what [`configlet`](/docs/building/configlet) created / updated.
16+
The code usually is rendered from template files, written for the tracks preferred templating engine.
17+
You should find all the details in the tracks contribution docs or a `README` near the test generator.
18+
19+
You should also know:
20+
21+
- what [`configlet create`](/docs/building/configlet/create) or [`configlet sync`](/docs/building/configlet/sync) do.
22+
- what [`canonical-data.json` in problem specifications](https://github.com/exercism/problem-specifications?tab=readme-ov-file#test-data-canonical-datajson) may provide.
23+
- why ["creating from scratch" is different from "reproducing for updates"](#from-scratch-vs-updating).
24+
25+
## Creating a Test Generator from scratch
26+
27+
There are various test generators in Exercism's tracks.
28+
These guidelines are based on the experiences of these tracks.
29+
30+
Even so test generators work very similar, they are very track specific.
31+
It starts with the choice of the templating engine and ends with additional things they do for each track.
32+
So a common test generator was not and will not be written.
33+
34+
There were helpful discussions [around the Rust](https://forum.exercism.org/t/advice-for-writing-a-test-generator/7178) and the [JavaScript](https://forum.exercism.org/t/test-generators-for-tracks/10615) test generators.
35+
The [forum](https://forum.exercism.org/c/exercism/building-exercism/125) also is the best place for seeking additional advice.
36+
37+
### Things to know
38+
39+
- `configlet` cache with a local copy of the problem specifications is stored in a [location depending on the users system](https://nim-lang.org/docs/osappdirs.html#getCacheDir).
40+
Use `configlet info -o -v d | head -1 | cut -d " " -f 5` to get the location.
41+
Or fetch data from the problem specifications repository directly (`https://raw.githubusercontent.com/exercism/problem-specifications/main/exercises/{{exercise-slug}}/canonical-data.json`)
42+
- [`canonical-data.json` data structure](https://github.com/exercism/problem-specifications?tab=readme-ov-file#test-data-canonical-datajson) is well documented. There is optional nesting of `cases` arrays in `cases` mixed with actual test cases.
43+
- The contents of `input` and `expected` test case keys of `canonical-data.json` vary largely. These can include simple scalar values, lambdas in pseudo code, lists of operations to perform on the students code and any other kind of input or result one can imagine.
44+
45+
### From Scratch vs. Updating
46+
47+
There are 2 common tasks a test generator may do, that require separate approaches:
48+
49+
- [Creating tests from scratch](#creating-tests-from-scratch)
50+
- [Reproducing tests for updates](#reproducing-tests-for-updates)
51+
52+
The reason for this distinction is "designing the exercise" vs. "production-ready code".
53+
54+
When creating tests from scratch the test generator should provide all the information contained in `canonical-data.json` in the resulting files.
55+
This enables contributors to simply open up the generated test file(s) and find all relevant information interwoven with the tracks boilerplate code.
56+
They then design the exercise's tests and student facing code based on these files rather than on the original `canonical-data.json`.
57+
As there is no knowledge of exercise specific things, yet, a one-fits-all template targeting the boilerplate code can be used.
58+
59+
When the exercise is already in production, changes in `canonical-data.json` are rarely a reason to change the design of the exercise.
60+
So reproducing tests for updates is based on the existing design and should result in production-ready code.
61+
Much of the additional data presented when creating the exercise from scratch is no longer part of the result.
62+
63+
Instead, very often additional conversion of test case data is required, which is specific to this exercise.
64+
Most tracks opt for having at least one template per exercise for this.
65+
This way they can represent all the design choices in that template without complicating things too much for further contribution.
66+
67+
### Creating tests from scratch
68+
69+
This is more productive in the beginning of a tracks life.
70+
It is way more easy to implement than the "updating" part.
71+
72+
Doing only the bare minimum required for a first usable test generator may already help contributors a lot:
73+
74+
- Read the `canonical-data.json` of the exercise from `configlet` cache or retrieve it from GitHub directly
75+
- Preserve all data (including `comments`, `description` and `scenarios`)
76+
- If the tracks testing framework supports no nested test case groups, flatten the nested data structure into a list of test cases
77+
- Dump the test cases into the one-fits-all boilerplate template(s)
78+
- Preserve the test case grouping for nested test case groups, e.g.
79+
- using the test frameworks grouping capability
80+
- using comments and code folding markers (`{{{`, `}}}`)
81+
- concatenating group `description` and test case `description`
82+
- Show all data (including `comments`, `description` and `scenarios`)
83+
84+
```exercism/note
85+
Don't try to produce perfect production-ready code!
86+
Dump all data and let the contributor design the exercise from that.
87+
There is way too much variation in the exercises to handle all in one template.
88+
```
89+
90+
There are optional things a test generator might do:
91+
92+
- Provide code for a simple test case (e.g. call a function with `input`, compare result to `expected`)
93+
- Provide boilerplate code for student code file(s) or additional files required by the track
94+
- Respect `scenarios` for grouping / test case selection
95+
- Skip over "reimplemented" test cases (those referred to in a `reimplements` key of another test case)
96+
- Update `tests.toml` with `include=false` to reflect tests skipped by `scenarios` / `reimplements`
97+
98+
### Reproducing tests for updates
99+
100+
This may become more relevant over track life time.
101+
It is much harder to implement than the "from scratch" part.
102+
If you need to invest much effort here, maybe manual maintenance is more efficient.
103+
Also keep in mind: maintaining the test generator adds to the maintainers workload, too.
104+
105+
```exercism/note
106+
Choose a flexible and extensible templating engine!
107+
The test cases vary largely between exercises.
108+
They include simple scalar values, lambdas in pseudo code, lists of operations to perform on the students code and any other kind of input or result one can imagine.
109+
```
110+
111+
Doing the bare minimum required for a usable updating test generator includes:
112+
113+
- Read the `canonical-data.json` of the exercise from `configlet` cache or retrieve it from GitHub directly
114+
- If the tracks testing framework supports no nested test case groups, flatten the nested data structure into a list of test cases
115+
- Render the test cases into the exercise specific template(s) located in an exercise's `.meta/` folder
116+
- Render production-ready code that matches the manually designed exercise
117+
- Skip over "reimplemented" test cases (those referred to in a `reimplements` key of another test case)
118+
- Render only test cases selected by `tests.toml` (or another track-specific data source)
119+
120+
There are different strategies for respecting test case changes like "replace always", "replace when forced to", "use `tests.toml` to ignore replaced test cases" (works like a baseline for known test issues).
121+
None of them is perfect.
122+
123+
```exercism/note
124+
Don't try to have a versatile one-fits-all template!
125+
There is way too much variation in the exercises to handle all in one template.
126+
```
127+
128+
There are optional things a test generator might do:
129+
130+
- Provide a library of templates and / or extensions to the template engine
131+
- Maintain or respect another track-specific data source than `tests.toml`
132+
- Maintain student code file(s) or additional files required by the track
133+
- Handle `scenarios` for grouping / test case selection
134+
- Have a check functionality (e.g. to run after `configlet sync`) to detect when updating is required

building/tracks/new/implement-tooling.md

+20-3
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,23 @@ After launching the track with the first 20+ exercises, the focus should shift t
44
Each track has various pieces of tooling that run in production.
55
Each provides a key function to the learning experience of that language.
66

7-
There are (currently) three pieces of tooling:
7+
There also can be track tooling to run for contribution or maintainance.
8+
Such tools provide help to create new exercises or keeping them up-to-date.
9+
Each lowers the barriers for new contributors and speeds up the growth of the track.
10+
11+
There are (currently) three pieces of tooling for production:
812

913
- **[Test Runners](/docs/building/tooling/test-runners)**: runs an exercise's tests against a student's code. (required)
1014
- **[Representers](/docs/building/tooling/representers)**: create a normalized representation of a solution (optional)
1115
- **[Analyzers](/docs/building/tooling/analyzers)**: automatically assess student submissions and provide mentor-style commentary. (optional)
1216

17+
Some tracks have (currently) implemented these pieces of tooling for contribution:
18+
19+
- **[Test Generators](/docs/building/tooling/test-generators)**: create or update an exercise's tests and student's code interface. (optional)
20+
1321
## Which tool to implement?
1422

15-
Of these three tools, the test runner should be implemented first as it enables:
23+
Of the three production tools, the test runner should be implemented first as it enables:
1624

1725
- Students to solve exercises using the [in-browser editor](/docs/using/solving-exercises/using-the-online-editor) ([no CLI needed](/docs/using/solving-exercises/working-locally)).
1826
- The website to automatically verify if an iteration passes all the tests.
@@ -31,9 +39,18 @@ To get started building a Representer, check the [Creating a Representer from sc
3139
Finally, after having implemented a Representer, the last tool to build is the Analyzer.
3240
To get started building an Analyzer, check the [Creating an Analyzer from scratch](/docs/building/tooling/analyzers/creating-from-scratch) document.
3341

42+
To speed up adding new exercises, a Test Generator is very handy.
43+
The first thing to implement is creating tests for new exercises from scratch.
44+
This takes away writing boilerplate code and leaves the focus on designing the exercises.
45+
Later in track development, the test generator may become capable of reproducing production-ready tests for updates.
46+
There are many hints and guidelines collected in the [Test Generators](/docs/building/tooling/test-generators) document.
47+
3448
## Implementation
3549

3650
The tooling is (generally) written in the track's language, but you're completely free to use whatever language (or combination of languages) you prefer.
3751

38-
Each tool is packaged and run as a [Docker container](/docs/building/tooling/docker).
52+
Each production tool is packaged and run as a [Docker container](/docs/building/tooling/docker).
3953
Tooling images are deployed automatically using a [Docker workflow](https://github.com/exercism/generic-test-runner/blob/main/.github/workflows/docker.yml).
54+
55+
Tools for contribution should fit into a workflow common for the language of the track.
56+
When using external packages, make sure these do not get packaged into the production Docker images or loaded in CI.

0 commit comments

Comments
 (0)