From 1035b3c4de8b6f6d7d5ccf9cfae11d69e012922e Mon Sep 17 00:00:00 2001 From: Isaac Good Date: Fri, 14 Aug 2026 13:50:15 -0700 Subject: [PATCH] Add test generators for `a* = acronym all-your-base allergies armstrong-numbers atbash-cipher` --- bin/generate_tests | 50 +++++---- .../.meta/template.create_test_table.j2 | 32 ++++++ .../practice/acronym/.meta/template.data.j2 | 3 + .../.meta/template.create_test_table.j2 | 47 ++++++++ .../all-your-base/.meta/template.data.j2 | 3 + .../practice/allergies/.meta/template.data.j2 | 7 ++ exercises/practice/allergies/data.csv | 100 +++++++++--------- .../armstrong-numbers/.meta/template.data.j2 | 3 + .../.meta/template.create_test_table.j2 | 34 ++++++ .../atbash-cipher/.meta/template.data.j2 | 3 + 10 files changed, 209 insertions(+), 73 deletions(-) create mode 100644 exercises/practice/acronym/.meta/template.create_test_table.j2 create mode 100644 exercises/practice/acronym/.meta/template.data.j2 create mode 100644 exercises/practice/all-your-base/.meta/template.create_test_table.j2 create mode 100644 exercises/practice/all-your-base/.meta/template.data.j2 create mode 100644 exercises/practice/allergies/.meta/template.data.j2 create mode 100644 exercises/practice/armstrong-numbers/.meta/template.data.j2 create mode 100644 exercises/practice/atbash-cipher/.meta/template.create_test_table.j2 create mode 100644 exercises/practice/atbash-cipher/.meta/template.data.j2 diff --git a/bin/generate_tests b/bin/generate_tests index 3dcda67f..cf6ce046 100755 --- a/bin/generate_tests +++ b/bin/generate_tests @@ -3,9 +3,7 @@ """Test generator v1.""" import argparse -import csv import datetime -import io import json import os import pathlib @@ -76,29 +74,33 @@ def filter_tojson(data, separators=(',', ':'), indent=None) -> str: return json.dumps(data, separators=separators, indent=indent) +def item_tocsv(data) -> str: + """Format one item to CSV.""" + if isinstance(data, (int, float)): + return str(data) + elif isinstance(data, str): + return f'"{data.replace('"', '""')}"' + elif isinstance(data, list): + entities = ",".join([item_tocsv(i) for i in data]) + return f'"[{entities.replace('"', '""')}]"' + raise ValueError(f"Unsuported data type {type(data)} in filter_tocsv()") + + def filter_tocsv(data: list) -> str: """Filter `tocsv` that CSV encodes an object.""" - formatted_data = [] - for datum in data: - if isinstance(datum, (int, float, str)): - formatted_data.append(datum) - elif isinstance(datum, list): - formatted_data.append("[" + filter_tocsv(datum) + "]") - else: - raise ValueError(f"Unsuported data type {type(datum)} in filter_tocsv()") - buff = io.StringIO() - writer = csv.writer(buff, dialect="unix") - writer.writerow(formatted_data) - buff.seek(0) - got = buff.read().removesuffix("\n") - return got + return ",".join(item_tocsv(i) for i in data).replace("\r", "") + + +def sql_quote(data: str) -> str: + """Filter `quote` that SQL encodes a string.""" + return data.replace('"', '""').replace("'", "''") def jinja_env(exercise: pathlib.Path) -> jinja2.Environment: """Return a configured Jinja env with filters added.""" env = jinja2.Environment(loader=jinja2.FileSystemLoader(exercise / ".meta")) - # Shell quoting - env.filters["quote"] = shlex.quote + # SQL quoting + env.filters["quote"] = sql_quote # JSON formatting, default to compact form (`jq -c`). env.filters["tojson"] = filter_tojson env.filters["tocsv"] = filter_tocsv @@ -153,7 +155,7 @@ def generate(specs: pathlib.Path, exercise: pathlib.Path) -> None: try: template = jinja_env(exercise).get_template(f"template.{base}.j2") out = template.render(data).strip("\n") - except jinja2.exceptions.TemplateAssertionError as e: + except Exception as e: e.add_note(f"Error rendering template.{base}.j2 for {exercise.name}") raise @@ -186,10 +188,12 @@ def main(): exercises = args.exercises # Generate all exercises with templates if none are specified as args. if not exercises: - exercises = [ - i.parent.parent - for i in pathlib.Path("exercises/practice").glob("*/.meta/template.j2") - ] + exercises = sorted( + { + i.parent.parent + for i in pathlib.Path("exercises/practice").glob("*/.meta/template.*.j2") + } + ) else: # Turn strings to paths and make them relative to the practice exercises. out = [] diff --git a/exercises/practice/acronym/.meta/template.create_test_table.j2 b/exercises/practice/acronym/.meta/template.create_test_table.j2 new file mode 100644 index 00000000..1d26aa7b --- /dev/null +++ b/exercises/practice/acronym/.meta/template.create_test_table.j2 @@ -0,0 +1,32 @@ +DROP TABLE IF EXISTS tests; + +CREATE TABLE IF NOT EXISTS tests ( + -- uuid and description are taken from the test.toml file + uuid TEXT PRIMARY KEY, + description TEXT NOT NULL, + -- The following section is needed by the online test-runner + status TEXT DEFAULT 'fail', + message TEXT, + output TEXT, + test_code TEXT, + task_id INTEGER DEFAULT NULL, + -- Here are columns for the actual tests + phrase TEXT NOT NULL, + expected TEXT NOT NULL +); + +INSERT INTO + tests (uuid, description, phrase, expected) +VALUES +{%- for case in cases %} + ( + '{{ case["uuid"] }}', + '{{ case["description"] }}', + '{{ case["input"]["phrase"] | quote }}', + '{{ case["expected"] | quote }}' +{%- if loop.last %} + ); +{%- else %} + ), +{%- endif %} +{%- endfor %} diff --git a/exercises/practice/acronym/.meta/template.data.j2 b/exercises/practice/acronym/.meta/template.data.j2 new file mode 100644 index 00000000..4abdb0cb --- /dev/null +++ b/exercises/practice/acronym/.meta/template.data.j2 @@ -0,0 +1,3 @@ +{%- for case in cases -%} +{{ [case["input"]["phrase"]] | tocsv }},"" +{% endfor %} diff --git a/exercises/practice/all-your-base/.meta/template.create_test_table.j2 b/exercises/practice/all-your-base/.meta/template.create_test_table.j2 new file mode 100644 index 00000000..90dace8d --- /dev/null +++ b/exercises/practice/all-your-base/.meta/template.create_test_table.j2 @@ -0,0 +1,47 @@ +DROP TABLE IF EXISTS tests; + +CREATE TABLE IF NOT EXISTS tests ( + -- uuid and description are taken from the test.toml file + uuid TEXT PRIMARY KEY, + description TEXT NOT NULL, + -- The following section is needed by the online test-runner + status TEXT DEFAULT 'fail', + message TEXT, + output TEXT, + test_code TEXT, + task_id INTEGER DEFAULT NULL, + -- Here are columns for the actual tests + input_base INTEGER NOT NULL, + digits TEXT NOT NULL, -- json array + output_base INTEGER NOT NULL, + expected TEXT -- json object +); + +INSERT INTO + tests ( + uuid, + description, + input_base, + digits, + output_base, + expected + ) +VALUES +{%- for case in cases %} + ( + '{{ case["uuid"] }}', + '{{ case["description"] }}', + {{ case["input"]["inputBase"] }}, + '[{{ case["input"]["digits"] | tocsv }}]', + {{ case["input"]["outputBase"] }}, +{%- if case["expect_error"] %} + '{{ case["expected"] | tojson }}' +{%- else %} + '{{ {"digits": case["expected"]} | tojson }}' +{%- endif %} +{%- if loop.last %} + ); +{%- else %} + ), +{%- endif %} +{%- endfor %} diff --git a/exercises/practice/all-your-base/.meta/template.data.j2 b/exercises/practice/all-your-base/.meta/template.data.j2 new file mode 100644 index 00000000..7f2112e3 --- /dev/null +++ b/exercises/practice/all-your-base/.meta/template.data.j2 @@ -0,0 +1,3 @@ +{%- for case in cases -%} +{{ [case["input"]["inputBase"], case["input"]["digits"], case["input"]["outputBase"]] | tocsv }},"" +{% endfor %} diff --git a/exercises/practice/allergies/.meta/template.data.j2 b/exercises/practice/allergies/.meta/template.data.j2 new file mode 100644 index 00000000..c74f01c8 --- /dev/null +++ b/exercises/practice/allergies/.meta/template.data.j2 @@ -0,0 +1,7 @@ +{%- for case in cases -%} +{%- if case["property"] == "allergicTo" -%} +{{ [case["property"], case["input"]["item"], case["input"]["score"]] | tocsv }},"" +{% else -%} +{{ [case["property"], "", case["input"]["score"]] | tocsv }},"" +{% endif -%} +{% endfor %} diff --git a/exercises/practice/allergies/data.csv b/exercises/practice/allergies/data.csv index caf2ad5c..5212d1cd 100644 --- a/exercises/practice/allergies/data.csv +++ b/exercises/practice/allergies/data.csv @@ -1,50 +1,50 @@ -allergicTo,eggs,0, -allergicTo,eggs,1, -allergicTo,eggs,3, -allergicTo,eggs,2, -allergicTo,eggs,255, -allergicTo,peanuts,0, -allergicTo,peanuts,2, -allergicTo,peanuts,7, -allergicTo,peanuts,5, -allergicTo,peanuts,255, -allergicTo,shellfish,0, -allergicTo,shellfish,4, -allergicTo,shellfish,14, -allergicTo,shellfish,10, -allergicTo,shellfish,255, -allergicTo,strawberries,0, -allergicTo,strawberries,8, -allergicTo,strawberries,28, -allergicTo,strawberries,20, -allergicTo,strawberries,255, -allergicTo,tomatoes,0, -allergicTo,tomatoes,16, -allergicTo,tomatoes,56, -allergicTo,tomatoes,40, -allergicTo,tomatoes,255, -allergicTo,chocolate,0, -allergicTo,chocolate,32, -allergicTo,chocolate,112, -allergicTo,chocolate,80, -allergicTo,chocolate,255, -allergicTo,pollen,0, -allergicTo,pollen,64, -allergicTo,pollen,224, -allergicTo,pollen,160, -allergicTo,pollen,255, -allergicTo,cats,0, -allergicTo,cats,128, -allergicTo,cats,192, -allergicTo,cats,64, -allergicTo,cats,255, -list,,0, -list,,1, -list,,2, -list,,8, -list,,3, -list,,5, -list,,248, -list,,255, -list,,509, -list,,257, +"allergicTo","eggs",0,"" +"allergicTo","eggs",1,"" +"allergicTo","eggs",3,"" +"allergicTo","eggs",2,"" +"allergicTo","eggs",255,"" +"allergicTo","peanuts",0,"" +"allergicTo","peanuts",2,"" +"allergicTo","peanuts",7,"" +"allergicTo","peanuts",5,"" +"allergicTo","peanuts",255,"" +"allergicTo","shellfish",0,"" +"allergicTo","shellfish",4,"" +"allergicTo","shellfish",14,"" +"allergicTo","shellfish",10,"" +"allergicTo","shellfish",255,"" +"allergicTo","strawberries",0,"" +"allergicTo","strawberries",8,"" +"allergicTo","strawberries",28,"" +"allergicTo","strawberries",20,"" +"allergicTo","strawberries",255,"" +"allergicTo","tomatoes",0,"" +"allergicTo","tomatoes",16,"" +"allergicTo","tomatoes",56,"" +"allergicTo","tomatoes",40,"" +"allergicTo","tomatoes",255,"" +"allergicTo","chocolate",0,"" +"allergicTo","chocolate",32,"" +"allergicTo","chocolate",112,"" +"allergicTo","chocolate",80,"" +"allergicTo","chocolate",255,"" +"allergicTo","pollen",0,"" +"allergicTo","pollen",64,"" +"allergicTo","pollen",224,"" +"allergicTo","pollen",160,"" +"allergicTo","pollen",255,"" +"allergicTo","cats",0,"" +"allergicTo","cats",128,"" +"allergicTo","cats",192,"" +"allergicTo","cats",64,"" +"allergicTo","cats",255,"" +"list","",0,"" +"list","",1,"" +"list","",2,"" +"list","",8,"" +"list","",3,"" +"list","",5,"" +"list","",248,"" +"list","",255,"" +"list","",509,"" +"list","",257,"" diff --git a/exercises/practice/armstrong-numbers/.meta/template.data.j2 b/exercises/practice/armstrong-numbers/.meta/template.data.j2 new file mode 100644 index 00000000..64546678 --- /dev/null +++ b/exercises/practice/armstrong-numbers/.meta/template.data.j2 @@ -0,0 +1,3 @@ +{%- for case in cases -%} +{{ [case["input"]["number"]] | tocsv }},"" +{% endfor %} diff --git a/exercises/practice/atbash-cipher/.meta/template.create_test_table.j2 b/exercises/practice/atbash-cipher/.meta/template.create_test_table.j2 new file mode 100644 index 00000000..f5a6d5fb --- /dev/null +++ b/exercises/practice/atbash-cipher/.meta/template.create_test_table.j2 @@ -0,0 +1,34 @@ +DROP TABLE IF EXISTS tests; + +CREATE TABLE IF NOT EXISTS tests ( + -- uuid and description are taken from the test.toml file + uuid TEXT PRIMARY KEY, + description TEXT NOT NULL, + -- The following section is needed by the online test-runner + status TEXT DEFAULT 'fail', + message TEXT, + output TEXT, + test_code TEXT, + task_id INTEGER DEFAULT NULL, + -- Here are columns for the actual tests + property TEXT NOT NULL, + phrase TEXT NOT NULL, + expected TEXT NOT NULL +); + +INSERT INTO + tests (uuid, description, property, phrase, expected) +VALUES +{%- for case in cases %} + ( + '{{ case["uuid"] }}', + '{{ case["description"] }}', + '{{ case["property"] }}', + '{{ case["input"]["phrase"] }}', + '{{ case["expected"] }}' +{%- if loop.last %} + ); +{%- else %} + ), +{%- endif %} +{%- endfor %} diff --git a/exercises/practice/atbash-cipher/.meta/template.data.j2 b/exercises/practice/atbash-cipher/.meta/template.data.j2 new file mode 100644 index 00000000..dded78a8 --- /dev/null +++ b/exercises/practice/atbash-cipher/.meta/template.data.j2 @@ -0,0 +1,3 @@ +{%- for case in cases -%} +{{ [case["property"], case["input"]["phrase"]] | tocsv }},"" +{% endfor %}