# Exercism Python Track Test Generator

The Python track uses a generator script and [Jinja2][Jinja2] templates for
creating test files from Exercism's  [canonical data][canonical data].


## Table of Contents

- [Exercism Python Track Test Generator](#exercism-python-track-test-generator)
  * [Script Usage](#script-usage)
  * [Test Templates](#test-templates)
    + [Conventions](#conventions)
    + [Layout](#layout)
    + [Overriding Imports](#overriding-imports)
    + [Ignoring Properties](#ignoring-properties)
  * [Learning Jinja](#learning-jinja)
  * [Creating a templates](#creating-a-templates)


## Script Usage

Test generation requires a local copy of the [problem-specifications][problem-specifications] repository.
The script should be run from the root `python` directory, in order to correctly
access the exercises.

Run `bin/generate_tests.py --help` for usage information.


## Test Templates

Test templates support [Jinja2] syntax, and have the following context
variables available from the canonical data:
- `exercise`: The hyphenated name of the exercise (ex: `two-fer`)
- `version`: The canonical data version (ex. `1.2.0`)
- `cases`: A list of case objects or a list of `cases` lists. For exact
structure for the exercise you're working on, please consult
`canonical-data.json`
- `has_error_case`: Indicates if any test case expects an
error to be thrown (ex: `False`)
- `additional_cases`: similar structure to `cases`, but is populated from the exercise's `.meta/additional_tests.json` file if one exists (for an example, see `exercises/word-count/.meta/additional_tests.json`)

Additionally, the following template filters are added for convenience:
- `to_snake`: Converts a string to snake_case (ex: `{{ "CamelCaseString" | to_snake }}` -> `camel_case_string`)
- `camel_case`: Converts a string to CamelCase (ex: `{{ "snake_case_string" | camel_case }}` -> `SnakeCaseString` )
- `error_case`: Checks if a test case expects an error to be thrown (ex: `{% for case in cases%}{% if case is error_case}`)
- `regex_replace`: Regex string replacement (ex: `{{ "abc123" | regex_replace("\\d", "D") }}` -> `abcDDD`)


### Conventions

- General-use macros for highly repeated template structures are defined in `config/generator_macros.j2`.
  - These may be imported with the following:
  `{%- import "generator_macros.j2" as macros with context -%}`
- All test templates should end with `{{ macros.footer() }}`.
- All Python class names should be in CamelCase (ex: `TwoFer`).
  - Convert using `{{ "two-fer" | camel_case }}`
- All Python module and function names should be in snake_case
(ex: `high_scores`, `personal_best`).
  - Convert using `{{ "personalBest" | to_snake }}`
- Track-specific tests are defined in the option file `.meta/additional_tests.json`. The JSON object defined in this file is to
have a single key, `cases`, which has the same structure as `cases` in
`canonical-data.json`.
- Track-specific tests should be placed after canonical tests in test
files.
- Track-specific tests should be marked in the test file with the following comment:
```
# Additional tests for this track
```


### Layout

Most templates will look something like this:

```Jinja2
{%- import "generator_macros.j2" as macros with context -%}
{{ macros.header() }}

class {{ exercise | camel_case }}Test(unittest.TestCase):
    {% for case in cases -%}
    def test_{{ case["description"] | to_snake }}(self):
        value = {{ case["input"]["value"] }}
        expected = {{ case["expected"] }}
        self.assertEqual({{ case["property"] }}(value), expected)

    {% endfor %}

{{ macros.footer() }}
```


### Overriding Imports

The names imported in `macros.header()` may be overridden by adding
a list of alternate names to import (ex:`clock`):

```Jinja2
{{ macros.header(["Clock"])}}
```


### Ignoring Properties

On rare occasion, it may be necessary to filter out properties that
are not tested in this track. The `header` macro also accepts an
`ignore` argument (ex: `high-scores`):

```Jinja2
{{ macros.header(ignore=["scores"]) }}
```


## Learning Jinja

Starting with the [Jinja Documentation] is highly recommended, but a complete reading is not strictly necessary.

Additional Resources:
- [Primer on Jinja Templating]
- [Python Jinja tutorial]


## Creating a templates

1. Create `.meta/template.j2` for the exercise you are implementing,
  and open it in your editor.
2. Copy and paste the [example layout](#Layout) in the template file
  and save.
3. Make the appropriate changes to the template file until it produces
  valid test code, referencing the exercise's `canonical-data.json` for
  input names and case structure.
    - Use the [available macros](../config/generator_macros.j2) to avoid re-writing standardized sections.
    - If you are implementing a template for an existing exercise,
    matching the exact structure of the existing test file is not a
    requirement, but minimizing differences will make PR review a much smoother process for everyone involved.



[Jinja Documentation]: https://jinja.palletsprojects.com/en/3.1.x/
[Jinja2]: https://palletsprojects.com/p/jinja/
[Primer on Jinja Templating]: https://realpython.com/primer-on-jinja-templating/
[Python Jinja tutorial]: http://zetcode.com/python/jinja/
[canonical data]: https://github.com/exercism/problem-specifications/tree/main/exercises
[problem-specifications]: https://github.com/exercism/problem-specifications
