Skip to content

Commit 60cb5a4

Browse files
beatorizucmccandless
authored andcommitted
grep: add test template (#2106)
* grep: add test template * queen-attack: create macro to apply to canonical and additional test cases * grep: extract variables * grep: set variable name using filename Remove hard-code files variables name and change filesnames dict to list to avoid duplicated work in case of add files to test. * grep: read filename from canonical data comments * grep: create content variables Set builtin zip as jinja filter to iterate names and contents lists. * grep: add mock-file * create plugins file and move appropriate content there
1 parent 3f0caf8 commit 60cb5a4

File tree

4 files changed

+277
-116
lines changed

4 files changed

+277
-116
lines changed

bin/generate_tests.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ def parse_datetime(string, strip_module=False):
9191
9292
import datetime
9393
94-
However if strip_module is True then the template will need to
94+
However if strip_module is True then the template will need to
9595
import the datetime _class_ instead.
9696
9797
from datetime import datetime
@@ -137,6 +137,14 @@ def regex_replace(s, find, repl):
137137
return re.sub(find, repl, s)
138138

139139

140+
def regex_find(s, find):
141+
return re.findall(find, s)
142+
143+
144+
def regex_split(s, find):
145+
return re.split(find, s)
146+
147+
140148
def load_canonical(exercise, spec_path):
141149
"""
142150
Loads the canonical data for an exercise as a nested dictionary
@@ -279,6 +287,9 @@ def generate(
279287
env.filters["camel_case"] = camel_case
280288
env.filters["wrap_overlong"] = wrap_overlong
281289
env.filters["regex_replace"] = regex_replace
290+
env.filters["regex_find"] = regex_find
291+
env.filters["regex_split"] = regex_split
292+
env.filters["zip"] = zip
282293
env.filters["parse_datetime"] = parse_datetime
283294
env.tests["error_case"] = error_case
284295
result = True

exercises/grep/.meta/plugins.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import re
2+
3+
RGX_ILLEGAL_CHARS = re.compile(r"\||-")
4+
RGX_LINEBREAK = re.compile(r"\s*\n\s*")
5+
6+
7+
def strip_illegal(s):
8+
return RGX_ILLEGAL_CHARS.sub("", s)
9+
10+
11+
def clean_filetext(text):
12+
text = strip_illegal(text)
13+
text = RGX_ILLEGAL_CHARS.sub("", text)
14+
text = RGX_LINEBREAK.sub("\n", text)
15+
return text.strip()

exercises/grep/.meta/template.j2

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
{%- import "generator_macros.j2" as macros with context -%}
2+
{{ macros.header() }}
3+
import builtins
4+
5+
{% set filenames = comments | join("\n") | regex_find("[a-z-]*\.txt") -%}
6+
{% set contents = (comments | join("\n") | regex_split("[a-z-]*\.txt"))[1:] -%}
7+
8+
FILE_TEXT = {
9+
{% for filename, content in filenames | zip(contents) -%}
10+
"{{ filename }}": """{{ plugins.clean_filetext(content) }}""",
11+
{% endfor %}
12+
}
13+
FILES = {}
14+
15+
16+
class File:
17+
def __init__(self, name=''):
18+
self.name = name
19+
self.contents = ''
20+
21+
def read(self):
22+
return self.contents
23+
24+
def readlines(self):
25+
return [line + '\n' for line in self.read().split('\n') if line]
26+
27+
def write(self, data):
28+
self.contents += data
29+
30+
def __enter__(self):
31+
return self
32+
33+
def __exit__(self, *args):
34+
pass
35+
36+
37+
# Store builtin definition of open()
38+
builtins.oldopen = builtins.open
39+
40+
41+
def open(name, mode='r', *args, **kwargs):
42+
# if name is a mocked file name, lookup corresponding mocked file
43+
if name in FILE_TEXT:
44+
if mode == 'w' or name not in FILES:
45+
FILES[name] = File(name)
46+
return FILES[name]
47+
# else call builtin open()
48+
else:
49+
return builtins.oldopen(name, mode, *args, **kwargs)
50+
51+
52+
# remove mocked file contents
53+
def remove_file(file_name):
54+
del FILES[file_name]
55+
56+
57+
def create_file(name, contents):
58+
with open(name, 'w') as f:
59+
f.write(contents)
60+
61+
class {{ exercise | camel_case }}Test(unittest.TestCase):
62+
@classmethod
63+
def setUpClass(cls):
64+
# Override builtin open() with mock-file-enabled one
65+
builtins.open = open
66+
for name, text in FILE_TEXT.items():
67+
create_file(name, text)
68+
cls.maxDiff = None
69+
70+
@classmethod
71+
def tearDownClass(cls):
72+
for name in FILE_TEXT:
73+
remove_file(name)
74+
# Restore builtin open()
75+
builtins.open = builtins.oldopen
76+
77+
{% set suite_tests = cases -%}
78+
79+
{%- macro test_case(case) -%}
80+
{% set expected = case['expected'] -%}
81+
{% set pattern = case['input']['pattern'] -%}
82+
{% set flags = case['input']['flags'] -%}
83+
{% set files = case['input']['files'] -%}
84+
def test_{{ case["description"] | to_snake }}(self):
85+
self.assertMultiLineEqual(
86+
grep("{{ pattern }}", "{{ flags|join(" ") }}", {{ files }}),
87+
{% if expected == [] -%}
88+
""
89+
{% else %}
90+
{% for e in expected -%}
91+
"{{ e }}\n"
92+
{% endfor %}
93+
{% endif %}
94+
)
95+
{%- endmacro -%}
96+
97+
{% for cases in suite_tests -%}
98+
# {{ cases["description"] }}
99+
{% for case in cases["cases"] -%}
100+
{{ test_case(case) }}
101+
{% endfor %}
102+
103+
{% endfor %}
104+
105+
{{ macros.footer() }}

0 commit comments

Comments
 (0)