|
| 1 | +#! /usr/bin/env python |
| 2 | +from __future__ import print_function |
| 3 | +import sys |
| 4 | +import textwrap |
| 5 | + |
| 6 | +try: |
| 7 | + import argparse |
| 8 | +except ImportError: |
| 9 | + print(textwrap.dedent(""" |
| 10 | + The argparse library could not be imported. jsonschema_suite requires |
| 11 | + either Python 2.7 or for you to install argparse. You can do so by |
| 12 | + running `pip install argparse`, `easy_install argparse` or by |
| 13 | + downloading argparse and running `python2.6 setup.py install`. |
| 14 | +
|
| 15 | + See https://pypi.python.org/pypi/argparse for details. |
| 16 | + """.strip("\n"))) |
| 17 | + sys.exit(1) |
| 18 | + |
| 19 | +import errno |
| 20 | +import fnmatch |
| 21 | +import json |
| 22 | +import os |
| 23 | +import random |
| 24 | +import shutil |
| 25 | +import unittest |
| 26 | +import warnings |
| 27 | + |
| 28 | +if getattr(unittest, "skipIf", None) is None: |
| 29 | + unittest.skipIf = lambda cond, msg : lambda fn : fn |
| 30 | + |
| 31 | +try: |
| 32 | + import jsonschema |
| 33 | +except ImportError: |
| 34 | + jsonschema = None |
| 35 | +else: |
| 36 | + validators = getattr( |
| 37 | + jsonschema.validators, "validators", jsonschema.validators |
| 38 | + ) |
| 39 | + |
| 40 | + |
| 41 | +ROOT_DIR = os.path.join( |
| 42 | + os.path.dirname(__file__), os.pardir).rstrip("__pycache__") |
| 43 | +SUITE_ROOT_DIR = os.path.join(ROOT_DIR, "tests") |
| 44 | + |
| 45 | +REMOTES = { |
| 46 | + "integer.json": {"type": "integer"}, |
| 47 | + "subSchemas.json": { |
| 48 | + "integer": {"type": "integer"}, |
| 49 | + "refToInteger": {"$ref": "#/integer"}, |
| 50 | + }, |
| 51 | + "folder/folderInteger.json": {"type": "integer"} |
| 52 | +} |
| 53 | +REMOTES_DIR = os.path.join(ROOT_DIR, "remotes") |
| 54 | + |
| 55 | +TESTSUITE_SCHEMA = { |
| 56 | + "$schema": "http://json-schema.org/draft-03/schema#", |
| 57 | + "type": "array", |
| 58 | + "items": { |
| 59 | + "type": "object", |
| 60 | + "properties": { |
| 61 | + "description": {"type": "string", "required": True}, |
| 62 | + "schema": {"required": True}, |
| 63 | + "tests": { |
| 64 | + "type": "array", |
| 65 | + "items": { |
| 66 | + "type": "object", |
| 67 | + "properties": { |
| 68 | + "description": {"type": "string", "required": True}, |
| 69 | + "data": {"required": True}, |
| 70 | + "valid": {"type": "boolean", "required": True} |
| 71 | + }, |
| 72 | + "additionalProperties": False |
| 73 | + }, |
| 74 | + "minItems": 1 |
| 75 | + } |
| 76 | + }, |
| 77 | + "additionalProperties": False, |
| 78 | + "minItems": 1 |
| 79 | + } |
| 80 | +} |
| 81 | + |
| 82 | + |
| 83 | +def files(paths): |
| 84 | + for path in paths: |
| 85 | + with open(path) as test_file: |
| 86 | + yield json.load(test_file) |
| 87 | + |
| 88 | + |
| 89 | +def groups(paths): |
| 90 | + for test_file in files(paths): |
| 91 | + for group in test_file: |
| 92 | + yield group |
| 93 | + |
| 94 | + |
| 95 | +def cases(paths): |
| 96 | + for test_group in groups(paths): |
| 97 | + for test in test_group["tests"]: |
| 98 | + test["schema"] = test_group["schema"] |
| 99 | + yield test |
| 100 | + |
| 101 | + |
| 102 | +def collect(root_dir): |
| 103 | + for root, dirs, files in os.walk(root_dir): |
| 104 | + for filename in fnmatch.filter(files, "*.json"): |
| 105 | + yield os.path.join(root, filename) |
| 106 | + |
| 107 | + |
| 108 | +class SanityTests(unittest.TestCase): |
| 109 | + @classmethod |
| 110 | + def setUpClass(cls): |
| 111 | + print("Looking for tests in %s" % SUITE_ROOT_DIR) |
| 112 | + cls.test_files = list(collect(SUITE_ROOT_DIR)) |
| 113 | + print("Found %s test files" % len(cls.test_files)) |
| 114 | + assert cls.test_files, "Didn't find the test files!" |
| 115 | + |
| 116 | + def test_all_files_are_valid_json(self): |
| 117 | + for path in self.test_files: |
| 118 | + with open(path) as test_file: |
| 119 | + try: |
| 120 | + json.load(test_file) |
| 121 | + except ValueError as error: |
| 122 | + self.fail("%s contains invalid JSON (%s)" % (path, error)) |
| 123 | + |
| 124 | + def test_all_descriptions_have_reasonable_length(self): |
| 125 | + for case in cases(self.test_files): |
| 126 | + descript = case["description"] |
| 127 | + self.assertLess( |
| 128 | + len(descript), |
| 129 | + 60, |
| 130 | + "%r is too long! (keep it to less than 60 chars)" % (descript,) |
| 131 | + ) |
| 132 | + |
| 133 | + def test_all_descriptions_are_unique(self): |
| 134 | + for group in groups(self.test_files): |
| 135 | + descriptions = set(test["description"] for test in group["tests"]) |
| 136 | + self.assertEqual( |
| 137 | + len(descriptions), |
| 138 | + len(group["tests"]), |
| 139 | + "%r contains a duplicate description" % (group,) |
| 140 | + ) |
| 141 | + |
| 142 | + @unittest.skipIf(jsonschema is None, "Validation library not present!") |
| 143 | + def test_all_schemas_are_valid(self): |
| 144 | + for schema in os.listdir(SUITE_ROOT_DIR): |
| 145 | + schema_validator = validators.get(schema) |
| 146 | + if schema_validator is not None: |
| 147 | + test_files = collect(os.path.join(SUITE_ROOT_DIR, schema)) |
| 148 | + for case in cases(test_files): |
| 149 | + try: |
| 150 | + schema_validator.check_schema(case["schema"]) |
| 151 | + except jsonschema.SchemaError as error: |
| 152 | + self.fail("%s contains an invalid schema (%s)" % |
| 153 | + (case, error)) |
| 154 | + else: |
| 155 | + warnings.warn("No schema validator for %s" % schema) |
| 156 | + |
| 157 | + @unittest.skipIf(jsonschema is None, "Validation library not present!") |
| 158 | + def test_suites_are_valid(self): |
| 159 | + validator = jsonschema.Draft3Validator(TESTSUITE_SCHEMA) |
| 160 | + for tests in files(self.test_files): |
| 161 | + try: |
| 162 | + validator.validate(tests) |
| 163 | + except jsonschema.ValidationError as error: |
| 164 | + self.fail(str(error)) |
| 165 | + |
| 166 | + def test_remote_schemas_are_updated(self): |
| 167 | + for url, schema in REMOTES.items(): |
| 168 | + filepath = os.path.join(REMOTES_DIR, url) |
| 169 | + with open(filepath) as schema_file: |
| 170 | + self.assertEqual(json.load(schema_file), schema) |
| 171 | + |
| 172 | + |
| 173 | +def main(arguments): |
| 174 | + if arguments.command == "check": |
| 175 | + suite = unittest.TestLoader().loadTestsFromTestCase(SanityTests) |
| 176 | + result = unittest.TextTestRunner(verbosity=2).run(suite) |
| 177 | + sys.exit(not result.wasSuccessful()) |
| 178 | + elif arguments.command == "flatten": |
| 179 | + selected_cases = [case for case in cases(collect(arguments.version))] |
| 180 | + |
| 181 | + if arguments.randomize: |
| 182 | + random.shuffle(selected_cases) |
| 183 | + |
| 184 | + json.dump(selected_cases, sys.stdout, indent=4, sort_keys=True) |
| 185 | + elif arguments.command == "remotes": |
| 186 | + json.dump(REMOTES, sys.stdout, indent=4, sort_keys=True) |
| 187 | + elif arguments.command == "dump_remotes": |
| 188 | + if arguments.update: |
| 189 | + shutil.rmtree(arguments.out_dir, ignore_errors=True) |
| 190 | + |
| 191 | + try: |
| 192 | + os.makedirs(arguments.out_dir) |
| 193 | + except OSError as e: |
| 194 | + if e.errno == errno.EEXIST: |
| 195 | + print("%s already exists. Aborting." % arguments.out_dir) |
| 196 | + sys.exit(1) |
| 197 | + raise |
| 198 | + |
| 199 | + for url, schema in REMOTES.items(): |
| 200 | + filepath = os.path.join(arguments.out_dir, url) |
| 201 | + |
| 202 | + try: |
| 203 | + os.makedirs(os.path.dirname(filepath)) |
| 204 | + except OSError as e: |
| 205 | + if e.errno != errno.EEXIST: |
| 206 | + raise |
| 207 | + |
| 208 | + with open(filepath, "wb") as out_file: |
| 209 | + json.dump(schema, out_file, indent=4, sort_keys=True) |
| 210 | + elif arguments.command == "serve": |
| 211 | + try: |
| 212 | + from flask import Flask, jsonify |
| 213 | + except ImportError: |
| 214 | + print(textwrap.dedent(""" |
| 215 | + The Flask library is required to serve the remote schemas. |
| 216 | +
|
| 217 | + You can install it by running `pip install Flask`. |
| 218 | +
|
| 219 | + Alternatively, see the `jsonschema_suite remotes` or |
| 220 | + `jsonschema_suite dump_remotes` commands to create static files |
| 221 | + that can be served with your own web server. |
| 222 | + """.strip("\n"))) |
| 223 | + sys.exit(1) |
| 224 | + |
| 225 | + app = Flask(__name__) |
| 226 | + |
| 227 | + @app.route("/<path:path>") |
| 228 | + def serve_path(path): |
| 229 | + if path in REMOTES: |
| 230 | + return jsonify(REMOTES[path]) |
| 231 | + return "Document does not exist.", 404 |
| 232 | + |
| 233 | + app.run(port=1234) |
| 234 | + |
| 235 | + |
| 236 | +parser = argparse.ArgumentParser( |
| 237 | + description="JSON Schema Test Suite utilities", |
| 238 | +) |
| 239 | +subparsers = parser.add_subparsers(help="utility commands", dest="command") |
| 240 | + |
| 241 | +check = subparsers.add_parser("check", help="Sanity check the test suite.") |
| 242 | + |
| 243 | +flatten = subparsers.add_parser( |
| 244 | + "flatten", |
| 245 | + help="Output a flattened file containing a selected version's test cases." |
| 246 | +) |
| 247 | +flatten.add_argument( |
| 248 | + "--randomize", |
| 249 | + action="store_true", |
| 250 | + help="Randomize the order of the outputted cases.", |
| 251 | +) |
| 252 | +flatten.add_argument( |
| 253 | + "version", help="The directory containing the version to output", |
| 254 | +) |
| 255 | + |
| 256 | +remotes = subparsers.add_parser( |
| 257 | + "remotes", |
| 258 | + help="Output the expected URLs and their associated schemas for remote " |
| 259 | + "ref tests as a JSON object." |
| 260 | +) |
| 261 | + |
| 262 | +dump_remotes = subparsers.add_parser( |
| 263 | + "dump_remotes", help="Dump the remote ref schemas into a file tree", |
| 264 | +) |
| 265 | +dump_remotes.add_argument( |
| 266 | + "--update", |
| 267 | + action="store_true", |
| 268 | + help="Update the remotes in an existing directory.", |
| 269 | +) |
| 270 | +dump_remotes.add_argument( |
| 271 | + "--out-dir", |
| 272 | + default=REMOTES_DIR, |
| 273 | + type=os.path.abspath, |
| 274 | + help="The output directory to create as the root of the file tree", |
| 275 | +) |
| 276 | + |
| 277 | +serve = subparsers.add_parser( |
| 278 | + "serve", |
| 279 | + help="Start a webserver to serve schemas used by remote ref tests." |
| 280 | +) |
| 281 | + |
| 282 | +if __name__ == "__main__": |
| 283 | + main(parser.parse_args()) |
0 commit comments