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 212 213 214 215 216 217 218 219 220 221 222
|
import json
# Parameters.
ALL_ERRORS = False
REPLACEMENTS = {}
def _print_path(path):
'''Format a JSON path for output.'''
return '/'.join(path)
def _report_error(msg):
'''Report an error.'''
full_msg = 'ERROR: ' + msg
if ALL_ERRORS:
print(full_msg)
else:
raise RuntimeError(full_msg)
def _error_type_mismatch(path, actual, expect):
'''Report that there is a type mismatch.'''
_report_error('type mismatch at %s: actual: "%s" expect: "%s"' % (_print_path(path), actual, expect))
def _error_unknown_type(path, typ):
'''Report that there is an unknown type in the JSON object.'''
_report_error('unknown type at %s: "%s"' % (_print_path(path), typ))
def _error_length_mismatch(path, actual, expect):
'''Report a length mismatch in an object or array.'''
_report_error('length mismatch at %s: actual: "%s" expect: "%s"' % (_print_path(path), actual, expect))
def _error_unexpect_value(path, actual, expect):
'''Report a value mismatch.'''
_report_error('value mismatch at %s: actual: "%s" expect: "%s"' % (_print_path(path), actual, expect))
def _error_extra_key(path, key):
'''Report on a key that is unexpected.'''
_report_error('extra key at %s: "%s"' % (_print_path(path), key))
def _error_missing_key(path, key):
'''Report on a key that is missing.'''
_report_error('extra key at %s: %s' % (_print_path(path), key))
def _compare_object(path, actual, expect):
'''Compare a JSON object.'''
is_ok = True
if not len(actual) == len(expect):
_error_length_mismatch(path, len(actual), len(expect))
is_ok = False
for key in actual:
if key not in expect:
_error_extra_key(path, key)
is_ok = False
else:
sub_error = compare_json(path + [key], actual[key], expect[key])
if sub_error:
is_ok = False
for key in expect:
if key not in actual:
_error_missing_key(path, key)
is_ok = False
return is_ok
def _compare_array(path, actual, expect):
'''Compare a JSON array.'''
is_ok = True
if not len(actual) == len(expect):
_error_length_mismatch(path, len(actual), len(expect))
is_ok = False
for (idx, (a, e)) in enumerate(zip(actual, expect)):
sub_error = compare_json(path + [str(idx)], a, e)
if sub_error:
is_ok = False
return is_ok
def _make_replacements(value):
for (old, new) in REPLACEMENTS.values():
value = value.replace(old, new)
return value
def _compare_string(path, actual, expect):
'''Compare a JSON string supporting replacements in the expected output.'''
expect = _make_replacements(expect)
if not actual == expect:
_error_unexpect_value(path, actual, expect)
return False
else:
print('%s is ok: %s' % (_print_path(path), actual))
return True
def _compare_number(path, actual, expect):
'''Compare a JSON integer.'''
if not actual == expect:
_error_unexpect_value(path, actual, expect)
return False
else:
print('%s is ok: %s' % (_print_path(path), actual))
return True
def _inspect_ordering(arr):
req_ordering = True
if not arr:
return arr, req_ordering
if arr[0] == '__P1689_unordered__':
arr.pop(0)
req_ordering = False
return arr, req_ordering
def compare_json(path, actual, expect):
actual_type = type(actual)
expect_type = type(expect)
is_ok = True
if not actual_type == expect_type:
_error_type_mismatch(path, actual_type, expect_type)
is_ok = False
elif actual_type == dict:
is_ok = _compare_object(path, actual, expect)
elif actual_type == list:
expect, req_ordering = _inspect_ordering(expect)
if not req_ordering:
actual = set(actual)
expect = set(expect)
is_ok = _compare_array(path, actual, expect)
elif actual_type == str:
is_ok = _compare_string(path, actual, expect)
elif actual_type == float:
is_ok = _compare_number(path, actual, expect)
elif actual_type == int:
is_ok = _compare_number(path, actual, expect)
elif actual_type == bool:
is_ok = _compare_number(path, actual, expect)
elif actual_type == type(None):
pass
else:
_error_unknown_type(path, actual_type)
is_ok = False
return is_ok
def validate_p1689(actual, expect):
'''Validate a P1689 file against an expected output file.
Returns `False` if it fails, `True` if they are the same.
'''
with open(actual, 'r') as fin:
actual_content = fin.read()
with open(expect, 'r') as fin:
expect_content = fin.read()
actual_json = json.loads(actual_content)
expect_json = json.loads(expect_content)
return compare_json([], actual_json, expect_json)
if __name__ == '__main__':
import sys
actual = None
expect = None
# Parse arguments.
args = sys.argv[1:]
while args:
# Take an argument.
arg = args.pop(0)
# Parse out replacement expressions.
if arg == '-r' or arg == '--replace':
replacement = args.pop(0)
(key, value) = replacement.split('=', maxsplit=1)
REPLACEMENTS[key] = value
# Flag to change how errors are reported.
elif arg == '-A' or arg == '--all':
ALL_ERRORS = True
# Required arguments.
elif arg == '-a' or arg == '--actual':
actual = args.pop(0)
elif arg == '-e' or arg == '--expect':
expect = args.pop(0)
# Validate that we have the required arguments.
if actual is None:
raise RuntimeError('missing "actual" file')
if expect is None:
raise RuntimeError('missing "expect" file')
# Do the actual work.
is_ok = validate_p1689(actual, expect)
# Fail if errors are found.
if not is_ok:
sys.exit(1)
|