Skip to content

Commit 9cdaed0

Browse files
committed
Add tera filter to_hex for use in test templates
and improve documentation about creating test templates
1 parent 4696194 commit 9cdaed0

4 files changed

Lines changed: 96 additions & 2 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ tmp
77
exercises/*/*/Cargo.lock
88
exercises/*/*/clippy.log
99
.vscode
10+
.prob-spec
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/usr/bin/env bash
2+
set -eo pipefail
3+
4+
cd "$(git rev-parse --show-toplevel)"
5+
6+
for exercise in exercises/practice/*; do
7+
name="$(basename "$exercise")"
8+
if [ -d "problem-specifications/exercises/$name" ]; then
9+
[ -e "$exercise/.prob-spec" ] && rm "$exercise/.prob-spec"
10+
ln -s "../../../problem-specifications/exercises/$name" "$exercise/.prob-spec"
11+
fi
12+
done

docs/CONTRIBUTING.md

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Run `just add-practice-exercise` and you'll be prompted for the minimal
3333
information required to generate the exercise stub for you.
3434
After that, jump in the generated exercise and fill in any todos you find.
3535
This includes most notably:
36+
3637
- adding an example solution in `.meta/example.rs`
3738
- Adjusting `.meta/test_template.tera`
3839

@@ -41,12 +42,14 @@ The input of the template is the canonical data from [`problem-specifications`].
4142
if you want to exclude certain tests from being generated,
4243
you have to set `include = false` in `.meta/tests.toml`.
4344

45+
Find some tips about writing tera templates [here](#tera-templates).
46+
4447
[Tera]: https://keats.github.io/tera/docs/
4548
[`problem-specifications`]: https://github.com/exercism/problem-specifications/
4649

4750
Many aspects of a correctly implemented exercises are checked in CI.
4851
I recommend that instead of spending lots of time studying and writing
49-
documentation about the process, *just do it*.
52+
documentation about the process, _just do it_.
5053
If something breaks, fix it and add a test / automation
5154
so it won't happen anymore.
5255

@@ -87,6 +90,70 @@ Run `just update-practice-exercise` to update an exercise.
8790
This outsources most work to `configlet sync --update`
8891
and runs the test generator again.
8992

93+
When updaing an exercise that doesn't have a tera template yet,
94+
a new one will be generated for you.
95+
You will likely have to adjust it to some extent.
96+
97+
Find some tips about writing tera templates [in the next section](#tera-templates).
98+
99+
## Tera templates
100+
101+
The full documentation for tera templates is [here][tera-docs].
102+
Following are some approaches that have worked for our specific needs.
103+
104+
You will likely want to look at the exercise's `canonical-data.json`
105+
to see what structure your input data has.
106+
You can use `bin/symlink_problem-specifications.sh` to have this data
107+
symlinked into the actual exercise directory. Handy!
108+
109+
The name of the input property is different for each exercise.
110+
The default template will be something like this:
111+
112+
```
113+
let input = {{ test.input | json_encode() }};
114+
```
115+
116+
You will have to add the specific field of input for this exercise, e.g.
117+
118+
```
119+
let input = {{ test.input.integers | json_encode() }};
120+
```
121+
122+
Some exercises may have error return values.
123+
You can use an if-else to render something different,
124+
depending on the structure of the data:
125+
126+
```
127+
let expected = {% if test.expected is object -%}
128+
None
129+
{%- else -%}
130+
Some({{ test.expected }})
131+
{%- endif %};
132+
```
133+
134+
If every test case needs to do some crunching of the inputs,
135+
you can add utils functions at the top of the tera template.
136+
See [`word-count`'s template][word-count-tmpl] for an example.
137+
138+
Some exercises have multiple functions that need to be implemented
139+
by the student and therefore tested.
140+
The canonical data contains a field `property` in that case.
141+
The template also has access to a value `fn_names`,
142+
which is an array of functions found in `lib.rs`.
143+
So, you can construct if-else-chains based on `test.property`
144+
and render a different element of `fn_names` based on that.
145+
See [`variable-length-quantity`'s template][var-len-q-tmpl] for an example.
146+
147+
There is a custom tera fiter `to_hex`, which formats ints in hexadecimal.
148+
Feel free to add your own in the crate `rust-tooling`.
149+
Custom filters added there will be available to all templates.
150+
How to create such custom filters is documented int he [tera docs][tera-docs-filters].
151+
152+
[tera-docs]: https://keats.github.io/tera/docs/#templates
153+
[word-count-tmpl]: /exercises/practice/word-count/.meta/test_template.tera
154+
[var-len-q-tmpl]: /exercises/practice/variable-length-quantity/.meta/test_template.tera
155+
[tera-docs-filters]: https://keats.github.io/tera/docs/#filters
156+
90157
## Syllabus
91158

92159
The syllabus is currently deactivated due to low quality.

rust-tooling/src/exercise_generation.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::collections::HashMap;
2+
13
use tera::Context;
24

35
use crate::{
@@ -81,6 +83,13 @@ fn extend_single_cases(single_cases: &mut Vec<SingleTestCase>, cases: Vec<TestCa
8183
}
8284
}
8385

86+
fn to_hex(value: &tera::Value, _args: &HashMap<String, tera::Value>) -> tera::Result<tera::Value> {
87+
Ok(serde_json::Value::String(format!(
88+
"{:x}",
89+
value.as_u64().unwrap()
90+
)))
91+
}
92+
8493
fn generate_tests(slug: &str, fn_names: Vec<String>) -> String {
8594
let cases = get_canonical_data(slug).cases;
8695
let excluded_tests = get_excluded_tests(slug);
@@ -90,6 +99,7 @@ fn generate_tests(slug: &str, fn_names: Vec<String>) -> String {
9099
.add_raw_template("test_template.tera", TEST_TEMPLATE)
91100
.unwrap();
92101
}
102+
template.register_filter("to_hex", to_hex);
93103

94104
let mut single_cases = Vec::new();
95105
extend_single_cases(&mut single_cases, cases);
@@ -100,5 +110,9 @@ fn generate_tests(slug: &str, fn_names: Vec<String>) -> String {
100110
context.insert("fn_names", &fn_names);
101111
context.insert("cases", &single_cases);
102112

103-
template.render("test_template.tera", &context).unwrap().trim_start().into()
113+
template
114+
.render("test_template.tera", &context)
115+
.unwrap()
116+
.trim_start()
117+
.into()
104118
}

0 commit comments

Comments
 (0)