Skip to content

Commit

Permalink
Add tera filter to_hex for use in test templates
Browse files Browse the repository at this point in the history
and improve documentation about creating test templates
  • Loading branch information
senekor committed Sep 13, 2023
1 parent 4696194 commit 4a18b45
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 2 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ tmp
exercises/*/*/Cargo.lock
exercises/*/*/clippy.log
.vscode
.prob-spec
12 changes: 12 additions & 0 deletions bin/symlink_problem_specifications.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -eo pipefail

cd "$(git rev-parse --show-toplevel)"

for exercise in exercises/practice/*; do
name="$(basename "$exercise")"
if [ -d "problem-specifications/exercises/$name" ]; then
[ -e "$exercise/.prob-spec" ] && rm "$exercise/.prob-spec"
ln -s "../../../problem-specifications/exercises/$name" "$exercise/.prob-spec"
fi
done
69 changes: 68 additions & 1 deletion docs/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Run `just add-practice-exercise` and you'll be prompted for the minimal
information required to generate the exercise stub for you.
After that, jump in the generated exercise and fill in any todos you find.
This includes most notably:

- adding an example solution in `.meta/example.rs`
- Adjusting `.meta/test_template.tera`

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

Find some tips about writing tera templates [here](#tera-templates).

[Tera]: https://keats.github.io/tera/docs/
[`problem-specifications`]: https://github.com/exercism/problem-specifications/

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

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

When updaing an exercise that doesn't have a tera template yet,
a new one will be generated for you.
You will likely have to adjust it to some extent.

Find some tips about writing tera templates [in the next section](#tera-templates).

## Tera templates

The full documentation for tera templates is [here][tera-docs].
Following are some approaches that have worked for our specific needs.

You will likely want to look at the exercise's `canonical-data.json`
to see what structure your input data has.
You can use `bin/symlink_problem_specifications.sh` to have this data
symlinked into the actual exercise directory. Handy!

The name of the input property is different for each exercise.
The default template will be something like this:

```
let input = {{ test.input | json_encode() }};
```

You will have to add the specific field of input for this exercise, e.g.

```
let input = {{ test.input.integers | json_encode() }};
```

Some exercises may have error return values.
You can use an if-else to render something different,
depending on the structure of the data:

```
let expected = {% if test.expected is object -%}
None
{%- else -%}
Some({{ test.expected }})
{%- endif %};
```

If every test case needs to do some crunching of the inputs,
you can add utils functions at the top of the tera template.
See [`word-count`'s template][word-count-tmpl] for an example.

Some exercises have multiple functions that need to be implemented
by the student and therefore tested.
The canonical data contains a field `property` in that case.
The template also has access to a value `fn_names`,
which is an array of functions found in `lib.rs`.
So, you can construct if-else-chains based on `test.property`
and render a different element of `fn_names` based on that.
See [`variable-length-quantity`'s template][var-len-q-tmpl] for an example.

There is a custom tera fiter `to_hex`, which formats ints in hexadecimal.
Feel free to add your own in the crate `rust-tooling`.
Custom filters added there will be available to all templates.
How to create such custom filters is documented int he [tera docs][tera-docs-filters].

[tera-docs]: https://keats.github.io/tera/docs/#templates
[word-count-tmpl]: /exercises/practice/word-count/.meta/test_template.tera
[var-len-q-tmpl]: /exercises/practice/variable-length-quantity/.meta/test_template.tera
[tera-docs-filters]: https://keats.github.io/tera/docs/#filters

## Syllabus

The syllabus is currently deactivated due to low quality.
Expand Down
16 changes: 15 additions & 1 deletion rust-tooling/src/exercise_generation.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::collections::HashMap;

use tera::Context;

use crate::{
Expand Down Expand Up @@ -81,6 +83,13 @@ fn extend_single_cases(single_cases: &mut Vec<SingleTestCase>, cases: Vec<TestCa
}
}

fn to_hex(value: &tera::Value, _args: &HashMap<String, tera::Value>) -> tera::Result<tera::Value> {
Ok(serde_json::Value::String(format!(
"{:x}",
value.as_u64().unwrap()
)))
}

fn generate_tests(slug: &str, fn_names: Vec<String>) -> String {
let cases = get_canonical_data(slug).cases;
let excluded_tests = get_excluded_tests(slug);
Expand All @@ -90,6 +99,7 @@ fn generate_tests(slug: &str, fn_names: Vec<String>) -> String {
.add_raw_template("test_template.tera", TEST_TEMPLATE)
.unwrap();
}
template.register_filter("to_hex", to_hex);

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

template.render("test_template.tera", &context).unwrap().trim_start().into()
template
.render("test_template.tera", &context)
.unwrap()
.trim_start()
.into()
}

0 comments on commit 4a18b45

Please sign in to comment.