diff --git a/.gitignore b/.gitignore index 6f97f3570a..f4b6c1a53d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ bin/configlet.exe .cache .pytest_cache __pycache__ + +# virtual environments +venv diff --git a/exercises/alphametics/.meta/generate.yml b/exercises/alphametics/.meta/generate.yml new file mode 100644 index 0000000000..07359c97f7 --- /dev/null +++ b/exercises/alphametics/.meta/generate.yml @@ -0,0 +1,4 @@ +skip_tests: + - description: "puzzle with ten letters and 199 addends" + property: "solve" + message: "extra-credit" diff --git a/exercises/armstrong-numbers/.meta/generate.yml b/exercises/armstrong-numbers/.meta/generate.yml new file mode 100644 index 0000000000..3398efcfc8 --- /dev/null +++ b/exercises/armstrong-numbers/.meta/generate.yml @@ -0,0 +1,2 @@ +properties_map: + isArmstrongNumber: is_armstrong diff --git a/exercises/atbash-cipher/.meta/generate.yml b/exercises/atbash-cipher/.meta/generate.yml new file mode 100644 index 0000000000..74a9f415c5 --- /dev/null +++ b/exercises/atbash-cipher/.meta/generate.yml @@ -0,0 +1,5 @@ +cases: + - description: "encode decode" + property: "decode" + input: 'encode("Testing, 1 2 3, testing.")' + expected: "testing123testing" diff --git a/exercises/binary-search/.meta/generate.yml b/exercises/binary-search/.meta/generate.yml new file mode 100644 index 0000000000..e2301881fd --- /dev/null +++ b/exercises/binary-search/.meta/generate.yml @@ -0,0 +1,2 @@ +properties_map: + find: binary_search diff --git a/exercises/bob/.meta/generate.yml b/exercises/bob/.meta/generate.yml new file mode 100644 index 0000000000..7c23231519 --- /dev/null +++ b/exercises/bob/.meta/generate.yml @@ -0,0 +1,2 @@ +properties_map: + response: hey diff --git a/exercises/book-store/.meta/generate.yml b/exercises/book-store/.meta/generate.yml new file mode 100644 index 0000000000..3682ce07de --- /dev/null +++ b/exercises/book-store/.meta/generate.yml @@ -0,0 +1,2 @@ +properties_map: + total: calculate_total diff --git a/exercises/change/.meta/generate.yml b/exercises/change/.meta/generate.yml new file mode 100644 index 0000000000..c143ed4705 --- /dev/null +++ b/exercises/change/.meta/generate.yml @@ -0,0 +1,2 @@ +properties_map: + findFewestCoins: find_minimum_coins diff --git a/exercises/collatz-conjecture/.meta/generate.yml b/exercises/collatz-conjecture/.meta/generate.yml new file mode 100644 index 0000000000..cf2ba9ff01 --- /dev/null +++ b/exercises/collatz-conjecture/.meta/generate.yml @@ -0,0 +1,2 @@ +properties_map: + steps: collatz_steps diff --git a/exercises/crypto-square/.meta/generate.yml b/exercises/crypto-square/.meta/generate.yml new file mode 100644 index 0000000000..ea570e0bed --- /dev/null +++ b/exercises/crypto-square/.meta/generate.yml @@ -0,0 +1,2 @@ +properties_map: + ciphertext: encode diff --git a/exercises/diamond/.meta/generate.yml b/exercises/diamond/.meta/generate.yml new file mode 100644 index 0000000000..eabecdf072 --- /dev/null +++ b/exercises/diamond/.meta/generate.yml @@ -0,0 +1,2 @@ +properties_map: + rows: make_diamond diff --git a/exercises/difference-of-squares/.meta/generate.yml b/exercises/difference-of-squares/.meta/generate.yml new file mode 100644 index 0000000000..a1face7271 --- /dev/null +++ b/exercises/difference-of-squares/.meta/generate.yml @@ -0,0 +1,2 @@ +properties_map: + differenceOfSquares: difference diff --git a/exercises/flatten-array/.meta/generate.yml b/exercises/flatten-array/.meta/generate.yml new file mode 100644 index 0000000000..df3fde0a90 --- /dev/null +++ b/exercises/flatten-array/.meta/generate.yml @@ -0,0 +1,10 @@ +cases: + - description: "empty nested lists" + property: "flatten" + input: '[[()]]' + expected: [] + - description: "strings" + property: "flatten" + input: + array: ['0', ['1', '2']] + expected: ['0', '1', '2'] diff --git a/exercises/grains/.meta/generate.yml b/exercises/grains/.meta/generate.yml new file mode 100644 index 0000000000..d5e8d9ff41 --- /dev/null +++ b/exercises/grains/.meta/generate.yml @@ -0,0 +1,22 @@ +properties_map: + square: on_square + total: total_after +cases: + - description: "total of square 0 raises an exception" + property: "total" + input: + square: 0 + expected: + error: "square must be between 1 and 64" + - description: "total of negative square raises an exception" + property: "square" + input: + square: -1 + expected: + error: "square must be between 1 and 64" + - description: "square greater than 64 raises an exception" + property: "square" + input: + square: 65 + expected: + error: "square must be between 1 and 64" diff --git a/exercises/hamming/.meta/generate.yml b/exercises/hamming/.meta/generate.yml new file mode 100644 index 0000000000..c891a37e78 --- /dev/null +++ b/exercises/hamming/.meta/generate.yml @@ -0,0 +1,63 @@ +cases: + - description: "complete distance in single nucleotide strands" + property: "distance" + input: + strand1: "A" + strand2: "G" + expected: 1 + + - description: "complete distance in small strands" + property: "distance" + input: + strand1: "AG" + strand2: "CT" + expected: 2 + + - description: "small distance in small strands" + property: "distance" + input: + strand1: "AT" + strand2: "CT" + expected: 1 + + - description: "small distance" + property: "distance" + input: + strand1: "GGACG" + strand2: "GGTCG" + expected: 1 + + - description: "small distance in long strands" + property: "distance" + input: + strand1: "ACCAGGG" + strand2: "ACTATGG" + expected: 2 + + - description: "non unique character in first strand" + property: "distance" + input: + strand1: "AAG" + strand2: "AAA" + expected: 1 + + - description: "non unique character in second strand" + property: "distance" + input: + strand1: "AAA" + strand2: "AAG" + expected: 1 + + - description: "same nucleotides in different positions" + property: "distance" + input: + strand1: "TAG" + strand2: "GAT" + expected: 2 + + - description: "large distance" + property: "distance" + input: + strand1: "GATACA" + strand2: "GCATAA" + expected: 4 diff --git a/exercises/isbn-verifier/.meta/generate.yml b/exercises/isbn-verifier/.meta/generate.yml new file mode 100644 index 0000000000..e7bec34491 --- /dev/null +++ b/exercises/isbn-verifier/.meta/generate.yml @@ -0,0 +1,2 @@ +properties_map: + isValid: verify diff --git a/exercises/isogram/.meta/generate.yml b/exercises/isogram/.meta/generate.yml new file mode 100644 index 0000000000..04762452e5 --- /dev/null +++ b/exercises/isogram/.meta/generate.yml @@ -0,0 +1,6 @@ +cases: + - description: "duplicated letter and nonletter character" + property: "isIsogram" + input: + phrase: "Aleph Bot Chap" + expected: false diff --git a/exercises/largest-series-product/.meta/generate.yml b/exercises/largest-series-product/.meta/generate.yml new file mode 100644 index 0000000000..6c54735538 --- /dev/null +++ b/exercises/largest-series-product/.meta/generate.yml @@ -0,0 +1,27 @@ +skip_tests: + - description: "project euler big number" + property: "largestProduct" + message: "extra-credit" +cases: + - description: "project euler big number" + property: "largestProduct" + input: + digits: + "73167176531330624919225119674426574742355349194934969835203127745\ + 06326239578318016984801869478851843858615607891129494954595017379\ + 58331952853208805511125406987471585238630507156932909632952274430\ + 43557668966489504452445231617318564030987111217223831136222989342\ + 33803081353362766142828064444866452387493035890729629049156044077\ + 23907138105158593079608667017242712188399879790879227492190169972\ + 08880937766572733300105336788122023542180975125454059475224352584\ + 90771167055601360483958644670632441572215539753697817977846174064\ + 95514929086256932197846862248283972241375657056057490261407972968\ + 65241453510047482166370484403199890008895243450658541227588666881\ + 16427171479924442928230863465674813919123162824586178664583591245\ + 66529476545682848912883142607690042242190226710556263211111093705\ + 44217506941658960408071984038509624554443629812309878799272442849\ + 09188845801561660979191338754992005240636899125607176060588611646\ + 71094050775410022569831552000559357297257163626956188267042825248\ + 3600823257530420752963450" + span: 13 + expected: 23514624000 diff --git a/exercises/leap/.meta/generate.yml b/exercises/leap/.meta/generate.yml new file mode 100644 index 0000000000..3edf968fc6 --- /dev/null +++ b/exercises/leap/.meta/generate.yml @@ -0,0 +1,2 @@ +properties_map: + leapYear: is_leap_year diff --git a/exercises/markdown/.meta/generate.yml b/exercises/markdown/.meta/generate.yml new file mode 100644 index 0000000000..57f63937fd --- /dev/null +++ b/exercises/markdown/.meta/generate.yml @@ -0,0 +1,2 @@ +properties_map: + parse: parse_markdown diff --git a/exercises/minesweeper/.meta/generate.yml b/exercises/minesweeper/.meta/generate.yml new file mode 100644 index 0000000000..11c7a2ae94 --- /dev/null +++ b/exercises/minesweeper/.meta/generate.yml @@ -0,0 +1,20 @@ +properties_map: + annotate: board +cases: + - description: "board9" + property: "annotate" + input: + minefield: [" ", " * ", " ", " ", " * "] + expected: [" 111", " 1*1", " 111", "111 ", "1*1 "] + - description: "different length" + property: "annotate" + input: + minefield: [" ", "* ", " "] + expected: + error: "Board rows must be of the same length" + - description: "invalid character" + property: "annotate" + input: + minefield: ["X * "] + expected: + error: "Illegal characters not allowed." diff --git a/exercises/nth-prime/.meta/generate.yml b/exercises/nth-prime/.meta/generate.yml new file mode 100644 index 0000000000..cf4897df57 --- /dev/null +++ b/exercises/nth-prime/.meta/generate.yml @@ -0,0 +1,2 @@ +properties_map: + prime: nth-prime diff --git a/exercises/palindrome-products/.meta/generate.yml b/exercises/palindrome-products/.meta/generate.yml new file mode 100644 index 0000000000..df751ee752 --- /dev/null +++ b/exercises/palindrome-products/.meta/generate.yml @@ -0,0 +1,3 @@ +properties_map: + largest: largest_palindrome + smallest: smallest_palindrome diff --git a/exercises/prime-factors/.meta/generate.yml b/exercises/prime-factors/.meta/generate.yml new file mode 100644 index 0000000000..d38ad5a2b9 --- /dev/null +++ b/exercises/prime-factors/.meta/generate.yml @@ -0,0 +1,2 @@ +properties_map: + factors: prime_factors diff --git a/exercises/rectangles/.meta/generate.yml b/exercises/rectangles/.meta/generate.yml new file mode 100644 index 0000000000..b83a7e5cbe --- /dev/null +++ b/exercises/rectangles/.meta/generate.yml @@ -0,0 +1,2 @@ +properties_map: + rectangles: count diff --git a/exercises/roman-numerals/.meta/generate.yml b/exercises/roman-numerals/.meta/generate.yml new file mode 100644 index 0000000000..fb213df4f6 --- /dev/null +++ b/exercises/roman-numerals/.meta/generate.yml @@ -0,0 +1,2 @@ +properties_map: + roman: numeral diff --git a/exercises/secret-handshake/.meta/generate.yml b/exercises/secret-handshake/.meta/generate.yml new file mode 100644 index 0000000000..3f46d02284 --- /dev/null +++ b/exercises/secret-handshake/.meta/generate.yml @@ -0,0 +1,2 @@ +properties_map: + commands: handshake diff --git a/exercises/sieve/.meta/generate.yml b/exercises/sieve/.meta/generate.yml new file mode 100644 index 0000000000..5ae42232b4 --- /dev/null +++ b/exercises/sieve/.meta/generate.yml @@ -0,0 +1,2 @@ +properties_map: + primes: sieve diff --git a/exercises/spiral-matrix/.meta/generate.yml b/exercises/spiral-matrix/.meta/generate.yml new file mode 100644 index 0000000000..01be28827a --- /dev/null +++ b/exercises/spiral-matrix/.meta/generate.yml @@ -0,0 +1,2 @@ +properties_map: + spiralMatrix: spiral diff --git a/exercises/sum-of-multiples/.meta/generate.yml b/exercises/sum-of-multiples/.meta/generate.yml new file mode 100644 index 0000000000..aada32a06d --- /dev/null +++ b/exercises/sum-of-multiples/.meta/generate.yml @@ -0,0 +1,2 @@ +properties_map: + sum: sum_of_multiples diff --git a/exercises/triangle/.meta/generate.yml b/exercises/triangle/.meta/generate.yml new file mode 100644 index 0000000000..fce66b9f0a --- /dev/null +++ b/exercises/triangle/.meta/generate.yml @@ -0,0 +1,4 @@ +properties_map: + equilateral: is_equilateral + isosceles: is_isosceles + scalene: is_scalene diff --git a/exercises/word-count/.meta/generate.yml b/exercises/word-count/.meta/generate.yml new file mode 100644 index 0000000000..619f347221 --- /dev/null +++ b/exercises/word-count/.meta/generate.yml @@ -0,0 +1,29 @@ +properties_map: + countWords: word_count +cases: + - description: "tabs" + property: "countWords" + input: + sentence: "rah rah ah ah ah\troma roma ma\tga ga oh la la\twant your bad romance" + expected: + rah: 2 + ah: 3 + roma: 2 + ma: 1 + ga: 2 + oh: 1 + la: 2 + want: 1 + your: 1 + bad: 1 + romance: 1 + - description: "non alphanumeric" + property: "countWords" + input: + sentence: "hey,my_spacebar_is_broken." + expected: + hey: 1 + my: 1 + spacebar: 1 + is: 1 + broken: 1 diff --git a/exercises/wordy/.meta/generate.yml b/exercises/wordy/.meta/generate.yml new file mode 100644 index 0000000000..4fb8a41538 --- /dev/null +++ b/exercises/wordy/.meta/generate.yml @@ -0,0 +1,15 @@ +properties_map: + answer: calculate +cases: + - description: "missing operation" + property: "answer" + input: + question: "What is 2 2 minus 3?" + expected: + error: "An operation is missingh" + - description: "missing number" + property: "answer" + input: + question: "What is 7 plus multiplied by -2?" + expected: + error: "A number is missing." diff --git a/exercises/yacht/.meta/generate.yml b/exercises/yacht/.meta/generate.yml new file mode 100644 index 0000000000..0ce2100846 --- /dev/null +++ b/exercises/yacht/.meta/generate.yml @@ -0,0 +1,15 @@ +imports: + - YACHT + - ONES + - TWOS + - THREES + - FOURS + - FIVES + - SIXES + - FULL_HOUSE + - FOUR_OF_A_KIND + - LITTLE_STRAIGHT + - BIG_STRAIGHT + - CHOICE +inputs_map: + category: "lambda c: c.upper().replace(' ', '_')" \ No newline at end of file diff --git a/generators/generate.py b/generators/generate.py new file mode 100644 index 0000000000..c2a67a8bce --- /dev/null +++ b/generators/generate.py @@ -0,0 +1,331 @@ +#!/usr/bin/python +""" +Generates exercise_test.py files for tests with canonical data. +""" +import json +import re +import logging +from argparse import ArgumentParser +from collections import OrderedDict +from functools import partial +from itertools import repeat +from pathlib import Path +from string import punctuation, whitespace + +# 3rd party imports: +from jinja2 import Environment, PackageLoader, select_autoescape, TemplateError +from yapf.yapflib.yapf_api import FormatFile, style +from yaml import load, dump +try: + # attempt to load libyaml C-extension if available + from yaml import CLoader as Loader, CDumper as Dumper +except ImportError: + # otherwise fall back on slower pure-Python version + from yaml import Loader, Dumper + +logging.basicConfig(format="%(levelname)s: %(message)s") +LOGGER = logging.getLogger(__name__) + + +def replace_all(string, chars, rep): + """ + Replace any char in chars with rep, reduce runs and strip terminal ends. + """ + trans = str.maketrans(dict(zip(chars, repeat(rep)))) + return re.sub("{0}+".format(re.escape(rep)), rep, + string.translate(trans)).strip(rep) + + +def to_snake(string): + """ + Convert pretty much anything to snake_case. + """ + clean = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", string) + clean = re.sub("([a-z0-9])([A-Z])", r"\1_\2", clean).lower() + return replace_all(clean, whitespace + punctuation, "_") + + +def to_camel(string): + """ + Convert pretty much anything to CamelCase. + """ + return "".join(w.title() for w in to_snake(string).split("_")) + + +def should_skip(case, tests=None): + """ + Determins if the given case should be skipped. + """ + for test in tests or []: + prop = case.get("property") + description = case.get("description") + if prop and prop == test["property"]: + if description and re.match(test["description"], description): + return test.get("message", "extra-credit") + return False + + +def is_error(case): + """ + Determines if the given case is an error. + """ + expect = case.get("expected") + return expect and isinstance(expect, dict) and "error" in expect + + +def has_error(data): + """ + Check if any case in the data is an error. + """ + if "cases" in data: + for case in data["cases"]: + if has_error(case): + return True + return False + return is_error(data) + + +def get_properties(data): + """ + Get the list of the unique property values accessed in all tests. + """ + + def _get_properties(obj): + if "cases" in obj: + for case in obj["cases"]: + for prop in get_properties(case): + yield prop + return + yield obj["property"] + + return sorted(set(_get_properties(data))) + + +def remap_property(string, properties_map=None): + """ + Builds a function that maps a property name to a new name, if required. + """ + return (properties_map or {}).get(string, string) + + +def prefer_double_quotes(string): + """ + Prefer double quotes on a string that does not embed them. + """ + if string.startswith("'") and string.endswith("'"): + if '"' not in string: + return '"' + string[1:-1] + '"' + return string + + +def format_input(case, n_args=None): + format_input_with_mappings({}) + + +def format_input_with_mappings(input_mappings, case, n_args=None): + """ + Format the case's input values for use in a function call. + + By default converts all arguments to positional form. + + If n_args is provided, convert only the first n argss to positional form. + """ + if isinstance(case["input"], str): + return case["input"] + raw = list(case["input"].items()) + n_args = len(raw) if n_args is None else n_args + result = [] + for _ in range(n_args): + name, arg = raw.pop(0) + if name in input_mappings: + result.append(input_mappings[name](arg)) + elif isinstance(arg, dict): + result.extend(map(prefer_double_quotes, map(repr, arg.values()))) + elif isinstance(arg, list): + result.append("[" + ", ".join( + map(prefer_double_quotes, map(repr, arg))) + "]") + else: + result.append(prefer_double_quotes(repr(arg))) + while raw: + result.append("{0}={1!s}".format(raw.pop(0))) + return ", ".join(result) + + +def format_expect(case): + """ + Format the case's expected values for use in a function call. + """ + if is_error(case): + return prefer_double_quotes(repr(case["expected"]["error"])) + expected = case["expected"] + if isinstance(expected, dict): + pairs = [] + for k, v in expected.items(): + pairs.append("{0}: {1}".format( + prefer_double_quotes(repr(k)), prefer_double_quotes(repr(v)))) + return "{" + ", ".join(pairs) + "}" + if isinstance(expected, list): + return "[" + ", ".join(map(prefer_double_quotes, map(repr, + expected))) + "]" + return prefer_double_quotes(repr(case["expected"])) + + +def main(args=None): + """ + Provide the CLI. + """ + parser = ArgumentParser() + parser.add_argument("-v", "--verbose", action="count") + parser.add_argument( + "-p", + "--spec-path", + type=Path, + default="../problem-specifications", + help="path to the problem specifications directory: (%(default)s)", + ) + parser.add_argument( + "-c", + "--config", + type=Path, + default="./config.json", + help="path to the Python track config.json file: (%(default)s)") + parser.add_argument( + "-o", + "--only", + help="generate tests for just the exercise specified") + parser.add_argument( + "-d", + "--output-dir", + type=Path, + default="./exercises", + help="path to the output directory: (%(default)s)") + parser.add_argument( + "-n", + "--no-autoformat", + action="store_true", + help="disable yapf autoreformat") + parser.add_argument( + "-s", + "--style", + default=style.DEFAULT_STYLE, + help="yapf autoreformat style to use: (%(default)s)") + parser.add_argument("exercises", nargs="*") + + args = parser.parse_args(args) + + # validate the config file + if not args.config.is_file(): + parser.error( + "config must be an extant file, got: {0.config}".format(args)) + elif args.config.suffix != ".json": + parser.error( + "config must be a *.json file, got: {0.config}".format(args)) + + # validate the spec path + if not args.spec_path.is_dir(): + parser.error( + "spec-path must be a directory, got: {0.spec_path}".format(args)) + + # set verbosity + if args.verbose: + LOGGER.setLevel(logging.DEBUG if args.verbose > 1 else logging.INFO) + + # get the (possibly limited) list of exercises to generate + config = json.loads(args.config.read_text()) + exercises = { + e["slug"] + for e in config["exercises"] if not e.get("deprecated") + } + if args.only is not None: + if args.only not in exercises: + LOGGER.error(f'"{args.only}" is not a valid exercise') + exit(1) + exercises = {args.only} + if args.exercises: + exercises = set(args.exercises).intersection(exercises) + + for exercise in sorted(exercises): + canonical = args.spec_path.joinpath("exercises", exercise, + "canonical-data.json") + if not canonical.is_file(): + LOGGER.warning("No canonical data found for %s: skipping", + exercise) + continue + + exercise_overrides = {} + exercise_override_file = args.output_dir.joinpath(exercise, '.meta', + 'generate.yml') + if exercise_override_file.is_file(): + LOGGER.info("Using override file for %s", exercise) + exercise_overrides = load(exercise_override_file.read_text()) + + # build the Jinja2 template environment + env = Environment( + loader=PackageLoader(__name__, "templates"), + autoescape=select_autoescape([]), + trim_blocks=True, lstrip_blocks=True, keep_trailing_newline=True) + env.filters["to_snake"] = to_snake + env.filters["to_camel"] = to_camel + env.filters["is_error"] = is_error + env.filters["format_input"] = format_input + env.filters["format_expect"] = format_expect + env.filters["should_skip"] = lambda c: False + env.filters["remap_property"] = lambda p: p + + exercise_properties_map = exercise_overrides.get("properties_map", {}) + if "skip_tests" in exercise_overrides: + env.filters["should_skip"] = partial( + should_skip, tests=exercise_overrides["skip_tests"]) + + if "properties_map" in exercise_overrides: + env.filters["remap_property"] = partial( + remap_property, + properties_map=exercise_overrides["properties_map"]) + + if "inputs_map" in exercise_overrides: + env.filters["format_input"] = partial( + format_input_with_mappings, + { + k: eval(v) + for k, v in exercise_overrides["inputs_map"].items() + } + ) + + data = json.loads(canonical.read_text(), object_pairs_hook=OrderedDict) + data["has_error"] = has_error(data) + data["properties"] = get_properties(data) + data["track_cases"] = [] + data["imports"] = exercise_overrides.get("imports", []) + + if "cases" in exercise_overrides: + exercise_cases = exercise_overrides["cases"] + data["has_error"] = data["has_error"] or has_error({"cases": exercise_cases}) + data["track_cases"] = exercise_cases + + template = env.select_template( + ["{0}.j2".format(exercise), "_default.j2"]) + try: + code = template.render(data=data) + out_dir = args.output_dir.joinpath(exercise) + if not out_dir.exists(): + out_dir.mkdir(parents=True) + out_file = out_dir.joinpath("{0}_test.py".format( + to_snake(exercise))) + out_file.write_text(code) + if not args.no_autoformat: + try: + FormatFile( + str(out_file), + style_config=args.style, + in_place=True, + verify=True) + except SyntaxError: + LOGGER.exception("Unable to reformat %s: skipping", + exercise) + except TemplateError: + LOGGER.exception("Unable to render %s: skipping", exercise) + continue + + +if __name__ == "__main__": + main() diff --git a/generators/templates/__macros.j2 b/generators/templates/__macros.j2 new file mode 100644 index 0000000000..f52bde1f1c --- /dev/null +++ b/generators/templates/__macros.j2 @@ -0,0 +1,83 @@ +{% macro make_import(data) %} +{% if data.properties | length > 0 %} +from {{ data.exercise | to_snake }} import {{ data.properties | map("remap_property") | map("to_snake") | join(", ") }} +{% else %} +import {{ data.exercise | to_snake }} +{% endif %} +{%- if data.imports | length > 0 %} +from {{ data.exercise | to_snake }} import ({{ data.imports | join(", ") }}) +{% endif %} +{% endmacro %} + +{% macro make_tests(data) %} +class {{ data.exercise | to_camel }}Test(unittest.TestCase): +{{ add_cases(data.cases) }} + {% if data.track_cases %} + # Track-specific test cases +{{ add_cases(data.track_cases) }} + {% endif %} + {% if data.has_error %} +{{ add_setup(error=data.has_error) }} + {% endif %} +{% endmacro %} + +{% macro add_setup(error=False) %} + # Utility functions + def setUp(self): +{% if error %} + try: + self.assertRaisesRegex + except AttributeError: + self.assertRaisesRegex = self.assertRaisesRegexp +{% endif %} +{% if error %} + def assertRaisesWithMessage(self, exception): + return self.assertRaisesRegex(exception, r".+") +{% endif %} +{% endmacro %} + +{% macro add_cases(cases) %} +{% for case in cases recursive %} +{% if case.cases %} +{#% if loop.nextitem is defined %#} +{#{ "#" * loop.depth }} {{ case.description }#} +{#% endif %#} +{{ loop(case.cases) }} +{% else %} +{% if case | should_skip %} + @unittest.skip("{{ case | should_skip }}") +{% endif %} + def test_{{ case.description | to_snake }}(self): +{% if case | is_error %} +{{ add_assert_raises(case) }} +{% elif case | format_expect in ['True', 'False'] %} + {{ add_assert_is(case) }} +{% else %} +{{ add_assert_equal(case) }} +{% endif %} +{% endif %} +{% endfor %} +{% endmacro %} + +{% macro add_assert_is(case) %} +{% if not case.input %} + self.assertIs({{ case.property | remap_property | to_snake }}(), {{ case | format_expect }}) +{% else %} + self.assertIs({{ case.property | remap_property | to_snake }}({{ case | format_input }}), {{ case | format_expect }}) +{% endif %} +{% endmacro %} + +{% macro add_assert_equal(case) %} + expected = {{ case | format_expect }} +{% if not case.input %} + self.assertEqual({{ case.property | remap_property | to_snake }}(), expected) +{% else %} + self.assertEqual({{ case.property | remap_property | to_snake }}({{ case | format_input }}), expected) +{% endif %} +{% endmacro %} + +{% macro add_assert_raises(case) %} + with self.assertRaisesWithMessage(ValueError): + {{ case.property | remap_property | to_snake }}({{ case | format_input }}) +{% endmacro %} + diff --git a/generators/templates/_default.j2 b/generators/templates/_default.j2 new file mode 100644 index 0000000000..75eaf625de --- /dev/null +++ b/generators/templates/_default.j2 @@ -0,0 +1,11 @@ +{%- from '__macros.j2' import make_import, make_tests -%} +import unittest + +{{ make_import(data) }} +# Tests adapted from `problem-specifications//canonical-data.json` @ v{{ data.version }} + + +{{ make_tests(data) }} + +if __name__ == '__main__': + unittest.main() diff --git a/requirements-generate.txt b/requirements-generate.txt new file mode 100644 index 0000000000..91b98b4877 --- /dev/null +++ b/requirements-generate.txt @@ -0,0 +1,4 @@ +Jinja2==2.10.1 +yapf==0.24.0 +MarkupSafe==1.1.0 +PyYAML==3.13