Skip to content

Commit a390e32

Browse files
authored
Merge pull request #619 from json-schema-org/gregsdennis/output-tests
initial run at creating output tests
2 parents 78c8882 + b538fe7 commit a390e32

18 files changed

+889
-37
lines changed

.editorconfig

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
insert_final_newline = true

bin/jsonschema_suite

+74-36
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,15 @@ else:
3030

3131
ROOT_DIR = Path(__file__).parent.parent
3232
SUITE_ROOT_DIR = ROOT_DIR / "tests"
33+
OUTPUT_ROOT_DIR = ROOT_DIR / "output-tests"
3334

3435
REMOTES_DIR = ROOT_DIR / "remotes"
3536
REMOTES_BASE_URL = "http://localhost:1234/"
3637

37-
TESTSUITE_SCHEMA = json.loads((ROOT_DIR / "test-schema.json").read_text())
38+
TEST_SCHEMA = json.loads(ROOT_DIR.joinpath("test-schema.json").read_text())
39+
OUTPUT_TEST_SCHEMA = json.loads(
40+
ROOT_DIR.joinpath("output-test-schema.json").read_text(),
41+
)
3842

3943

4044
def files(paths):
@@ -67,7 +71,7 @@ def collect(root_dir):
6771
"""
6872
All of the test file paths within the given root directory, recursively.
6973
"""
70-
return root_dir.glob("**/*.json")
74+
return root_dir.rglob("*.json")
7175

7276

7377
def url_for_path(path):
@@ -80,20 +84,29 @@ def url_for_path(path):
8084

8185
return urljoin(
8286
REMOTES_BASE_URL,
83-
str(path.relative_to(REMOTES_DIR)).replace("\\", "/") # Windows...
87+
str(path.relative_to(REMOTES_DIR)).replace("\\", "/"), # Windows...
8488
)
8589

8690

8791
class SanityTests(unittest.TestCase):
8892
@classmethod
8993
def setUpClass(cls):
9094
print(f"Looking for tests in {SUITE_ROOT_DIR}")
95+
print(f"Looking for output tests in {OUTPUT_ROOT_DIR}")
9196
print(f"Looking for remotes in {REMOTES_DIR}")
9297

9398
cls.test_files = list(collect(SUITE_ROOT_DIR))
9499
assert cls.test_files, "Didn't find the test files!"
95100
print(f"Found {len(cls.test_files)} test files")
96101

102+
cls.output_test_files = [
103+
each
104+
for each in collect(OUTPUT_ROOT_DIR)
105+
if each.name != "output-schema.json"
106+
]
107+
assert cls.output_test_files, "Didn't find the output test files!"
108+
print(f"Found {len(cls.output_test_files)} output test files")
109+
97110
cls.remote_files = list(collect(REMOTES_DIR))
98111
assert cls.remote_files, "Didn't find the remote files!"
99112
print(f"Found {len(cls.remote_files)} remote files")
@@ -131,22 +144,11 @@ class SanityTests(unittest.TestCase):
131144
self.assertNotRegex(description, r"\bshould\b", message)
132145
self.assertNotRegex(description, r"(?i)\btest(s)? that\b", message)
133146

134-
def test_all_test_files_are_valid_json(self):
135-
"""
136-
All test files contain valid JSON.
137-
"""
138-
for path in self.test_files:
139-
with self.subTest(path=path):
140-
try:
141-
json.loads(path.read_text())
142-
except ValueError as error:
143-
self.fail(f"{path} contains invalid JSON ({error})")
144-
145-
def test_all_remote_files_are_valid_json(self):
147+
def test_all_json_files_are_valid(self):
146148
"""
147-
All remote files contain valid JSON.
149+
All files (tests, output tests, remotes, etc.) contain valid JSON.
148150
"""
149-
for path in self.remote_files:
151+
for path in collect(ROOT_DIR):
150152
with self.subTest(path=path):
151153
try:
152154
json.loads(path.read_text())
@@ -157,53 +159,57 @@ class SanityTests(unittest.TestCase):
157159
"""
158160
All cases have reasonably long descriptions.
159161
"""
160-
for case in cases(self.test_files):
162+
for case in cases(self.test_files + self.output_test_files):
161163
with self.subTest(description=case["description"]):
162164
self.assertLess(
163165
len(case["description"]),
164166
150,
165-
"Description is too long (keep it to less than 150 chars)."
167+
"Description is too long (keep it to less than 150 chars).",
166168
)
167169

168170
def test_all_test_descriptions_have_reasonable_length(self):
169171
"""
170172
All tests have reasonably long descriptions.
171173
"""
172-
for count, test in enumerate(tests(self.test_files)):
174+
for count, test in enumerate(
175+
tests(self.test_files + self.output_test_files)
176+
):
173177
with self.subTest(description=test["description"]):
174178
self.assertLess(
175179
len(test["description"]),
176180
70,
177-
"Description is too long (keep it to less than 70 chars)."
181+
"Description is too long (keep it to less than 70 chars).",
178182
)
179183
print(f"Found {count} tests.")
180184

181185
def test_all_case_descriptions_are_unique(self):
182186
"""
183187
All cases have unique descriptions in their files.
184188
"""
185-
for path, cases in files(self.test_files):
189+
for path, cases in files(self.test_files + self.output_test_files):
186190
with self.subTest(path=path):
187191
self.assertUnique(case["description"] for case in cases)
188192

189193
def test_all_test_descriptions_are_unique(self):
190194
"""
191195
All test cases have unique test descriptions in their tests.
192196
"""
193-
for count, case in enumerate(cases(self.test_files)):
197+
for count, case in enumerate(
198+
cases(self.test_files + self.output_test_files)
199+
):
194200
with self.subTest(description=case["description"]):
195201
self.assertUnique(
196202
test["description"] for test in case["tests"]
197203
)
198204
print(f"Found {count} test cases.")
199205

200206
def test_case_descriptions_do_not_use_modal_verbs(self):
201-
for case in cases(self.test_files):
207+
for case in cases(self.test_files + self.output_test_files):
202208
with self.subTest(description=case["description"]):
203209
self.assertFollowsDescriptionStyle(case["description"])
204210

205211
def test_test_descriptions_do_not_use_modal_verbs(self):
206-
for test in tests(self.test_files):
212+
for test in tests(self.test_files + self.output_test_files):
207213
with self.subTest(description=test["description"]):
208214
self.assertFollowsDescriptionStyle(test["description"])
209215

@@ -218,14 +224,21 @@ class SanityTests(unittest.TestCase):
218224

219225
Validator = VALIDATORS.get(version.name)
220226
if Validator is not None:
227+
# Valid (optional test) schemas contain regexes which
228+
# aren't valid Python regexes, so skip checking it
229+
Validator.FORMAT_CHECKER.checkers.pop("regex", None)
230+
221231
test_files = collect(version)
222232
for case in cases(test_files):
223233
with self.subTest(case=case):
224234
try:
225-
Validator.check_schema(case["schema"])
235+
Validator.check_schema(
236+
case["schema"],
237+
format_checker=Validator.FORMAT_CHECKER,
238+
)
226239
except jsonschema.SchemaError:
227240
self.fail(
228-
"Found an invalid schema."
241+
"Found an invalid schema. "
229242
"See the traceback for details on why."
230243
)
231244
else:
@@ -236,15 +249,32 @@ class SanityTests(unittest.TestCase):
236249
"""
237250
All test files are valid under test-schema.json.
238251
"""
239-
Validator = jsonschema.validators.validator_for(TESTSUITE_SCHEMA)
240-
validator = Validator(TESTSUITE_SCHEMA)
252+
Validator = jsonschema.validators.validator_for(TEST_SCHEMA)
253+
validator = Validator(TEST_SCHEMA)
241254
for path, cases in files(self.test_files):
242255
with self.subTest(path=path):
243256
try:
244257
validator.validate(cases)
245258
except jsonschema.ValidationError as error:
246259
self.fail(str(error))
247260

261+
@unittest.skipIf(jsonschema is None, "Validation library not present!")
262+
def test_output_suites_are_valid(self):
263+
"""
264+
All output test files are valid under output-test-schema.json.
265+
"""
266+
Validator = jsonschema.validators.validator_for(OUTPUT_TEST_SCHEMA)
267+
validator = Validator(OUTPUT_TEST_SCHEMA)
268+
for path, cases in files(self.output_test_files):
269+
with self.subTest(path=path):
270+
try:
271+
validator.validate(cases)
272+
except jsonschema.exceptions.RefResolutionError as error:
273+
# python-jsonschema/jsonschema#884
274+
pass
275+
except jsonschema.ValidationError as error:
276+
self.fail(str(error))
277+
248278

249279
def main(arguments):
250280
if arguments.command == "check":
@@ -277,15 +307,21 @@ def main(arguments):
277307
try:
278308
import flask
279309
except ImportError:
280-
print(textwrap.dedent("""
310+
print(
311+
textwrap.dedent(
312+
"""
281313
The Flask library is required to serve the remote schemas.
282314
283315
You can install it by running `pip install Flask`.
284316
285317
Alternatively, see the `jsonschema_suite remotes` or
286318
`jsonschema_suite dump_remotes` commands to create static files
287319
that can be served with your own web server.
288-
""".strip("\n")))
320+
""".strip(
321+
"\n"
322+
)
323+
)
324+
)
289325
sys.exit(1)
290326

291327
app = flask.Flask(__name__)
@@ -309,25 +345,27 @@ check = subparsers.add_parser("check", help="Sanity check the test suite.")
309345

310346
flatten = subparsers.add_parser(
311347
"flatten",
312-
help="Output a flattened file containing a selected version's test cases."
348+
help="Output a flattened file containing a selected version's test cases.",
313349
)
314350
flatten.add_argument(
315351
"--randomize",
316352
action="store_true",
317353
help="Randomize the order of the outputted cases.",
318354
)
319355
flatten.add_argument(
320-
"version", help="The directory containing the version to output",
356+
"version",
357+
help="The directory containing the version to output",
321358
)
322359

323360
remotes = subparsers.add_parser(
324361
"remotes",
325362
help="Output the expected URLs and their associated schemas for remote "
326-
"ref tests as a JSON object."
363+
"ref tests as a JSON object.",
327364
)
328365

329366
dump_remotes = subparsers.add_parser(
330-
"dump_remotes", help="Dump the remote ref schemas into a file tree",
367+
"dump_remotes",
368+
help="Dump the remote ref schemas into a file tree",
331369
)
332370
dump_remotes.add_argument(
333371
"--update",
@@ -343,7 +381,7 @@ dump_remotes.add_argument(
343381

344382
serve = subparsers.add_parser(
345383
"serve",
346-
help="Start a webserver to serve schemas used by remote ref tests."
384+
help="Start a webserver to serve schemas used by remote ref tests.",
347385
)
348386

349387
if __name__ == "__main__":

output-test-schema.json

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"$id": "https://json-schema.org/tests/output-test-schema",
4+
"description": "A schema for files contained within this suite",
5+
6+
"type": "array",
7+
"minItems": 1,
8+
"items": {
9+
"description": "An individual test case, containing multiple tests of a single schema's behavior",
10+
11+
"type": "object",
12+
"required": [ "description", "schema", "tests" ],
13+
"properties": {
14+
"description": {
15+
"description": "The test case description",
16+
"type": "string"
17+
},
18+
"comment": {
19+
"description": "Any additional comments about the test case",
20+
"type": "string"
21+
},
22+
"schema": {
23+
"description": "A valid JSON Schema (one written for the corresponding version directory that the file sits within)."
24+
},
25+
"tests": {
26+
"description": "A set of related tests all using the same schema",
27+
"type": "array",
28+
"items": { "$ref": "#/$defs/test" },
29+
"minItems": 1
30+
}
31+
},
32+
"additionalProperties": false
33+
},
34+
35+
"$defs": {
36+
"test": {
37+
"description": "A single output test",
38+
39+
"type": "object",
40+
"required": [ "description", "data", "output" ],
41+
"properties": {
42+
"description": {
43+
"description": "The test description, briefly explaining which behavior it exercises",
44+
"type": "string"
45+
},
46+
"comment": {
47+
"description": "Any additional comments about the test",
48+
"type": "string"
49+
},
50+
"data": {
51+
"description": "The instance which should be validated against the schema in \"schema\"."
52+
},
53+
"output": {
54+
"description": "schemas that are used to verify output",
55+
"type": "object",
56+
"properties": {
57+
"flag": { "$ref": "https://json-schema.org/draft/2020-12/schema" },
58+
"basic": { "$ref": "https://json-schema.org/draft/2020-12/schema" },
59+
"detailed": { "$ref": "https://json-schema.org/draft/2020-12/schema" },
60+
"verbose": { "$ref": "https://json-schema.org/draft/2020-12/schema" },
61+
"list": { "$ref": "https://json-schema.org/draft/2020-12/schema" },
62+
"hierarchy": { "$ref": "https://json-schema.org/draft/2020-12/schema" }
63+
},
64+
"minProperties": 1,
65+
"additionalProperties": false
66+
}
67+
}
68+
}
69+
}
70+
}

0 commit comments

Comments
 (0)