/
test_target.py
211 lines (183 loc) · 6.28 KB
/
test_target.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
#!/usr/bin/env python3
import argparse
import os
import re
import sys
from typing import List, Tuple
from unittest import TestCase
import unittest
from target import IGNORED, KNOWN, VIRTUAL, Target, parse_bool
class Line(str):
"""A Line is a string that remembers which file and line number it came from"""
file: str
idx: int
nr: int
def __new__(cls, text: str, file: str, idx: int):
line = super().__new__(cls, text)
line.file = file
line.idx = idx
line.nr = idx + 1
return line
@property
def location(self):
return self.file + ":" + str(self.nr)
def read_lines(f, filename: str, start_line=0) -> List[Line]:
"""Read from 'f' and turn the lines into Lines"""
n = start_line
lines = []
for s in f:
s = s.rstrip()
line = Line(s, filename, n)
lines.append(line)
n += 1
return lines
def split_tests(lines: List[Line]) -> List[Tuple[str, List[Line]]]:
tests = []
cur = None
header = None
count = 0
location = None
for line in lines:
if cur is None:
if line.startswith("```test"):
location = line.location
cur = []
elif line.startswith('#'):
header = line.lstrip('#').strip()
header = re.sub(r'\W+', '_', header).lower()
count = 0
else:
if line.startswith("```"):
count += 1
name = f"{header}_{count}"
assert len(cur) > 0
tests.append((name, cur))
cur = None
else:
cur.append(line)
if cur is not None:
raise Exception(f"Unclosed block at {location}")
return tests
# Note: we will programmatically add test methods to this file
# based on the contents of tests.md.
class TargetTests(TestCase):
def test_dummy_tests(self):
"""Convenience method. Run the tests from t.md if that exists."""
filename = 't.md'
if not os.path.exists(filename):
raise unittest.SkipTest(f"{filename} does not exist")
lines = read_lines(open(filename), filename)
tests = split_tests(lines)
for name, test in tests:
self.run_test(test)
def xtest_generic_tests(self):
filename = "tests.md"
self.run_test_script("tests.md")
def run_test_script(self, filename):
lines = read_lines(open(filename), filename)
tests = split_tests(lines)
for name, test in tests:
start = test[0].location
print(name, start, file=sys.stderr)
with self.subTest("joeri", name=name, location=test[0].location):
self.run_test(test)
def run_test(self, test):
target = Target()
for line in test:
try:
self.apply_line(target, line)
continue
except AssertionError as e:
if hasattr(e, 'add_note'):
e.add_note(f"At {line.location}")
raise
else:
raise AssertionError(f"At {line.location}: {e}")
except unittest.SkipTest:
break
except Exception as e:
if hasattr(e, 'add_note'):
e.add_note(f"At {line.location}")
raise
def apply_line(self, target: Target, line: Line):
if not line:
return
command, rest = line.split(None, 1)
command = command.upper()
if command == "PARSE":
self.apply_parse(target, rest)
elif command == "ACCEPT":
self.apply_accept(target, rest)
elif command == "REJECT":
self.apply_reject(target, rest)
elif command == "EXPECT":
key, value = rest.split('=', 1)
self.apply_expect(target, key, value)
elif command == "SET":
key, value = rest.split('=', 1)
self.apply_set(target, key, value)
elif command == "ONLY":
impl = rest
if impl != 'pymonetdb':
raise unittest.SkipTest(f"only for {impl}")
elif command == "NOT":
impl = rest
if impl == 'pymonetdb':
raise unittest.SkipTest(f"not for {impl}")
else:
self.fail(f"Unknown command: {command}")
def apply_parse(self, target: Target, url):
target.parse(url)
def apply_accept(self, target: Target, url):
target.parse(url)
target.validate()
def apply_reject(self, target: Target, url):
try:
target.parse(url)
except ValueError:
return
# last hope
try:
target.validate()
except ValueError:
return
raise ValueError("Expected URL to be rejected")
def apply_set(self, target: Target, key, value):
target.set(key, value)
def apply_expect(self, target: Target, key, expected_value):
if key == 'valid':
should_succeed = parse_bool(expected_value)
try:
target.validate()
if not should_succeed:
self.fail("Expected valid=false")
except ValueError as e:
if should_succeed:
self.fail(f"Expected valid=true, got error {e}")
return
if key in VIRTUAL:
target.validate()
actual_value = target.get(key)
if isinstance(actual_value, bool):
expected_value = parse_bool(expected_value)
elif isinstance(actual_value, int):
try:
expected_value = int(expected_value)
except ValueError:
# will show up in the comparison below
pass
if actual_value != expected_value:
self.fail(f"Expected {key}={expected_value!r}, found {actual_value!r}")
# Magic alert!
# Read tests.md and generate test cases programmatically!
filename = 'tests.md'
lines = read_lines(open(filename), filename)
tests = split_tests(lines)
for name, test in tests:
if test:
line_nr = f"line_{test[0].nr}_"
else:
line_nr = ""
def newlexicalscope(test):
return lambda this: this.run_test(test)
setattr(TargetTests, f"test_{line_nr}{name}", newlexicalscope(test))