| 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) |