|
| 1 | +# Exercism Python Track Test Generator |
| 2 | + |
| 3 | +The Python track uses a generator script and [Jinja2] templates for |
| 4 | +creating test files from the canonical data. |
| 5 | + |
| 6 | +## Table of Contents |
| 7 | + |
| 8 | +- [Exercism Python Track Test Generator](#exercism-python-track-test-generator) |
| 9 | + * [Script Usage](#script-usage) |
| 10 | + * [Test Templates](#test-templates) |
| 11 | + + [Conventions](#conventions) |
| 12 | + + [Layout](#layout) |
| 13 | + + [Overriding Imports](#overriding-imports) |
| 14 | + + [Ignoring Properties](#ignoring-properties) |
| 15 | + * [Learning Jinja](#learning-jinja) |
| 16 | + * [Creating a templates](#creating-a-templates) |
| 17 | + |
| 18 | +## Script Usage |
| 19 | + |
| 20 | +Test generation requires a local copy of the [problem-specifications] repository. |
| 21 | + |
| 22 | +Run `bin/generate_tests.py --help` for usage information. |
| 23 | + |
| 24 | +## Test Templates |
| 25 | + |
| 26 | +Test templates support [Jinja2] syntax, and have the following context |
| 27 | +variables available from the canonical data: |
| 28 | +- `exercise`: The hyphenated name of the exercise (ex: `two-fer`) |
| 29 | +- `version`: The canonical data version (ex. `1.2.0`) |
| 30 | +- `cases`: A list of case objects or a list of `cases` lists. For exact |
| 31 | +structure for the exercise you're working on, please consult |
| 32 | +`canonical-data.json` |
| 33 | +- `has_error_case`: Indicates if any test case expects an |
| 34 | +error to be thrown (ex: `False`) |
| 35 | +- `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`) |
| 36 | + |
| 37 | +### Conventions |
| 38 | + |
| 39 | +- General-use macros for highly repeated template structures are defined in `config/generator_macros.j2`. |
| 40 | + - These may be imported with the following: |
| 41 | + `{%- import "generator_macros.j2" as macros with context -%}` |
| 42 | +- All test templates should end with `{{ macros.footer() }}`. |
| 43 | +- All Python class names should be in CamelCase (ex: `TwoFer`). |
| 44 | + - Convert using `{{ "two-fer" | camel_case }}` |
| 45 | +- All Python module and function names should be in snake_case |
| 46 | +(ex: `high_scores`, `personal_best`). |
| 47 | + - Convert using `{{ "personalBest" | to_snake }}` |
| 48 | +- Track-specific tests are defined in the option file `.meta/additional_tests.json`. The JSON object defined in this file is to |
| 49 | +have a single key, `cases`, which has the same structure as `cases` in |
| 50 | +`canonical-data.json`. |
| 51 | +- Track-specific tests should be placed after canonical tests in test |
| 52 | +files. |
| 53 | +- Track-specific tests should be marked in the test file with the following comment: |
| 54 | +``` |
| 55 | +# Additional tests for this track |
| 56 | +``` |
| 57 | + |
| 58 | +### Layout |
| 59 | + |
| 60 | +Most templates will look something like this: |
| 61 | + |
| 62 | +```Jinja2 |
| 63 | +{%- import "generator_macros.j2" as macros with context -%} |
| 64 | +{{ macros.header() }} |
| 65 | +
|
| 66 | +class {{ exercise | camel_case }}Test(unittest.TestCase): |
| 67 | + {% for case in cases -%} |
| 68 | + def test_{{ case["description"] | to_snake }}(self): |
| 69 | + value = {{ case["input"]["value"] }} |
| 70 | + expected = {{ case["expected"] }} |
| 71 | + self.assertEqual({{ case["property"] }}(value), expected) |
| 72 | +
|
| 73 | + {% endfor %} |
| 74 | +
|
| 75 | +{{ macros.footer() }} |
| 76 | +``` |
| 77 | + |
| 78 | +### Overriding Imports |
| 79 | + |
| 80 | +The names imported in `macros.header()` may be overridden by adding |
| 81 | +a list of alternate names to import (ex:`clock`): |
| 82 | + |
| 83 | +```Jinja2 |
| 84 | +{{ macros.header(["Clock"])}} |
| 85 | +``` |
| 86 | + |
| 87 | +### Ignoring Properties |
| 88 | + |
| 89 | +On rare occasion, it may be necessary to filter out properties that |
| 90 | +are not tested in this track. The `header` macro also accepts an |
| 91 | +`ignore` argument (ex: `high-scores`): |
| 92 | + |
| 93 | +```Jinja2 |
| 94 | +{{ macros.header(ignore=["scores"]) }} |
| 95 | +``` |
| 96 | + |
| 97 | +## Learning Jinja |
| 98 | + |
| 99 | +Starting with the [Jinja Documentation] is highly recommended, but a complete reading is not strictly necessary. |
| 100 | + |
| 101 | +Additional Resources: |
| 102 | +- [Primer on Jinja Templating] |
| 103 | +- [Python Jinja tutorial] |
| 104 | + |
| 105 | + |
| 106 | +## Creating a templates |
| 107 | + |
| 108 | +1. Create `.meta/template.j2` for the exercise you are implementing, |
| 109 | + and open it in your editor. |
| 110 | +2. Copy and paste the [example layout](#Layout) in the template file |
| 111 | + and save. |
| 112 | +3. Make the appropriate changes to the template file until it produces |
| 113 | + valid test code, referencing the exercise's `canonical-data.json` for |
| 114 | + input names and case structure. |
| 115 | + - Use the [available macros](config/generator_macros.j2) to avoid re-writing standardized sections. |
| 116 | + - If you are implementing a template for an existing exercise, |
| 117 | + matching the exact structure of the existing test file is not a |
| 118 | + requirement, but minimizing differences will make PR review a much smoother process for everyone involved. |
| 119 | + |
| 120 | + |
| 121 | + |
| 122 | +[Jinja2]: https://jinja.pocoo.org/ |
| 123 | +[Jinja Documentation]: https://jinja.palletsprojects.com/en/2.10.x/ |
| 124 | +[Primer on Jinja Templating]: https://realpython.com/primer-on-jinja-templating/ |
| 125 | +[Python Jinja tutorial]: http://zetcode.com/python/jinja/ |
| 126 | +[problem-specifications]: https://github.com/exercism/problem-specifications |
0 commit comments