diff options
-rwxr-xr-x | json_diff.py | 51 | ||||
-rw-r--r-- | test_json_diff.py | 278 | ||||
-rw-r--r-- | test_strings.py | 213 |
3 files changed, 310 insertions, 232 deletions
diff --git a/json_diff.py b/json_diff.py index d636562..b9d5470 100755 --- a/json_diff.py +++ b/json_diff.py @@ -23,14 +23,15 @@ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ from __future__ import division, absolute_import, print_function, unicode_literals -import json +import json, sys +# import pdb import logging from optparse import OptionParser __author__ = "Matěj Cepl" __version__ = "0.1.0" -logging.basicConfig(format='%(levelname)s:%(funcName)s:%(message)s', level=logging.INFO) +logging.basicConfig(format='%(levelname)s:%(funcName)s:%(message)s', level=logging.DEBUG) STYLE_MAP = { "_append": "append_class", @@ -66,12 +67,14 @@ td { %s """ +# I would love to have better solution for this ... +if sys.version_info[0] == 3: unicode = str + def is_scalar(value): """ Primitive version, relying on the fact that JSON cannot contain any more complicated data structures. """ - logging.debug("? = %s", not isinstance(value, (list, tuple, dict))) return not isinstance(value, (list, tuple, dict)) class HTMLFormatter(object): @@ -108,13 +111,6 @@ class HTMLFormatter(object): # doesn't have level and neither concept of it, much def _format_dict(self, diff_dict, typch="unknown_change", level=0): out_str = "" - logging.debug("out_str = %s", out_str) - - logging.debug("----------------------------------------------------------------") - logging.debug("diff_dict = %s", unicode(diff_dict)) - logging.debug("level = %s", unicode(level)) - logging.debug("diff_dict.keys() = %s", unicode(diff_dict.keys())) - # For all STYLE_MAP keys which are present in diff_dict for typechange in set(diff_dict.keys()) & INTERNAL_KEYS: out_str += self._format_dict(diff_dict[typechange], typechange, level) @@ -135,7 +131,7 @@ class Comparator(object): """ Main workhorse, the object itself """ - def __init__(self, fn1=None, fn2=None, excluded_attrs=(), included_attrs=()): + def __init__(self, fn1=None, fn2=None, included_attrs=(), excluded_attrs=()): self.obj1 = None self.obj2 = None if fn1: @@ -159,9 +155,9 @@ class Comparator(object): """ Be careful with the result of this function. Negative answer from this function is really None, not False, so deciding based on the return value like in - + if self._compare_scalars(...): - + leads to wrong answer (it should be if self._compare_scalars(...) is not None:) """ # Explicitly excluded arguments @@ -170,8 +166,10 @@ class Comparator(object): (name in self.excluded_attributes)): return None elif old != new: + logging.debug("Comparing result (name=%s) is %s", name, new) return new else: + logging.debug("Comparing result (name=%s) is None", name) return None def _compare_arrays(self, old_arr, new_arr): @@ -189,8 +187,6 @@ class Comparator(object): "_update": {} } for idx in range(inters): - logging.debug("idx = %s, old_arr[idx] = %s, new_arr[idx] = %s", - idx, old_arr[idx], new_arr[idx]) # changed objects, new value is new_arr if (type(old_arr[idx]) != type(new_arr[idx])): result['_update'][idx] = new_arr[idx] @@ -259,15 +255,15 @@ class Comparator(object): # new_obj is missing elif name not in new_obj: result['_remove'][name] = old_obj[name] + # We want to go through the tree post-order + elif isinstance(old_obj[name], dict): + res_dict = self.compare_dicts(old_obj[name], new_obj[name]) + if (len(res_dict) > 0): + result['_update'][name] = res_dict + # Now we are on the same level # changed objects, new value is new_obj elif (type(old_obj[name]) != type(new_obj[name])): result['_update'][name] = new_obj[name] - # last simple variant ... scalars - elif (is_scalar(old_obj[name])): - # Explicitly excluded arguments - res_scal = self._compare_scalars(old_obj[name], new_obj[name], name) - if res_scal is not None: - result['_update'][name] = res_scal # now arrays (we can be sure, that both old_obj and # new_obj are of the same type) elif (isinstance(old_obj[name], list)): @@ -275,11 +271,12 @@ class Comparator(object): new_obj[name]) if (len(res_arr) > 0): result['_update'][name] = res_arr - # and now nested dicts - elif isinstance(old_obj[name], dict): - res_dict = self.compare_dicts(old_obj[name], new_obj[name]) - if (len(res_dict) > 0): - result['_update'][name] = res_dict + # the only thing remaining are scalars + else: + # Explicitly excluded arguments + res_scal = self._compare_scalars(old_obj[name], new_obj[name], name) + if res_scal is not None: + result['_update'][name] = res_scal # Clear out unused keys in result out_result = {} @@ -311,7 +308,7 @@ if __name__ == "__main__": diff = Comparator(file(args[0]), file(args[1]), options.exclude, options.include) if options.HTMLoutput: diff_res = diff.compare_dicts() - logging.debug("diff_res:\n%s", json.dumps(diff_res, indent=True)) + # logging.debug("diff_res:\n%s", json.dumps(diff_res, indent=True)) print(HTMLFormatter(diff_res)) else: outs = json.dumps(diff.compare_dicts(), indent=4, ensure_ascii=False).encode("utf-8") diff --git a/test_json_diff.py b/test_json_diff.py index 453eba4..2d073ea 100644 --- a/test_json_diff.py +++ b/test_json_diff.py @@ -3,195 +3,16 @@ PyUnit unit tests """ from __future__ import division, absolute_import, unicode_literals -import unittest +import unittest, sys +if sys.version_info[0] == 3: unicode = str import json import json_diff from io import StringIO import codecs -SIMPLE_OLD = """ -{ - "a": 1, - "b": true, - "c": "Janošek" -} -""" - -SIMPLE_NEW = """ -{ - "b": false, - "c": "Maruška", - "d": "přidáno" -} -""" - -SIMPLE_DIFF = """ -{ - "_append": { - "d": "přidáno" - }, - "_remove": { - "a": 1 - }, - "_update": { - "c": "Maruška", - "b": false - } -} -""" - -SIMPLE_DIFF_HTML=""" -<!DOCTYPE html> -<html lang='en'> -<meta charset="utf-8" /> -<title>json_diff result</title> -<style> -td { -text-align: center; -} -.append_class { -color: green; -} -.remove_class { -color: red; -} -.update_class { -color: navy; -} -</style> -<body> -<h1>json_diff result</h1> -<table> -<tr> -<td class='remove_class'>a = 1</td> -</tr><tr> -<td class='update_class'>c = Maruška</td> -</tr><tr> -<td class='update_class'>b = False</td> -</tr><tr> -<td class='append_class'>d = přidáno</td> -</tr> -</table> -</body> -</html> -""" - -SIMPLE_ARRAY_OLD = """ -{ - "a": [ 1 ] -} -""" - -SIMPLE_ARRAY_NEW = """ -{ - "a": [ 1, 2 ] -} -""" - -SIMPLE_ARRAY_DIFF = """ -{ - "_update": { - "a": { - "_append": { - "1": 2 - } - } - } -} -""" +from test_strings import * #@UnusedWildImport -NESTED_OLD = """ -{ - "a": 1, - "b": 2, - "son": { - "name": "Janošek" - } -} -""" - -NESTED_NEW = """ -{ - "a": 2, - "c": 3, - "daughter": { - "name": "Maruška" - } -} -""" - -NESTED_DIFF = """ -{ - "_append": { - "c": 3, - "daughter": { - "name": "Maruška" - } - }, - "_remove": { - "b": 2, - "son": { - "name": "Janošek" - } - }, - "_update": { - "a": 2 - } -} -""" -NESTED_DIFF_EXCL = """ -{ - "_append": { - "c": 3 - }, - "_remove": { - "b": 2 - }, - "_update": { - "a": 2 - } -} -""" - -ARRAY_OLD = """ -{ - "a": 1, - "b": 2, - "children": [ - "Pepíček", "Anička", "Maruška" - ] -} -""" - -ARRAY_NEW = """ -{ - "a": 1, - "children": [ - "Pepíček", "Tonička", "Maruška" - ], - "c": 3 -} -""" - -ARRAY_DIFF = """ -{ - "_remove": { - "b": 2 - }, - "_append": { - "c": 3 - }, - "_update": { - "children": [ - "Pepíček", - "Tonička", - "Maruška" - ] - } -} -""" - -class TestHappyPath(unittest.TestCase): +class OurTestCase(unittest.TestCase): def _run_test(self, oldf, newf, difff, msg="", inc=(), exc=()): diffator = json_diff.Comparator(oldf, newf, inc, exc) diff = diffator.compare_dicts() @@ -201,15 +22,32 @@ class TestHappyPath(unittest.TestCase): (json.dumps(expected, sort_keys=True, indent=4, ensure_ascii=False), json.dumps(diff, sort_keys=True, indent=4, ensure_ascii=False))) + def _run_test_strings(self, olds, news, diffs, msg="", inc=(), exc=()): + self._run_test(StringIO(olds), StringIO(news), StringIO(diffs), msg, inc, exc) + def _run_test_formatted(self, oldf, newf, difff, msg=""): diffator = json_diff.Comparator(oldf, newf) diff = ("\n".join([line.strip() \ - for line in unicode(json_diff.HTMLFormatter(diffator.compare_dicts())).split("\n")])).strip() + for line in unicode( \ + json_diff.HTMLFormatter(diffator.compare_dicts())).split("\n")])).strip() expected = ("\n".join([line.strip() for line in difff if line])).strip() self.assertEqual(diff, expected, msg + "\n\nexpected = %s\n\nobserved = %s" % (expected, diff)) +#class TestUtilities(unittest.TestCase): +# def test_is_dict_interesting(self): +# diffator = json_diff.Comparator(StringIO(NESTED_OLD), StringIO(NESTED_NEW), +# included_attrs=('nome',)) +# old_res = diffator.dict_no_key_included(diffator.obj1) +# self.assertFalse(old_res, +# "check whether the old dict should be excluded or not") # or True? FIXME +# new_res = diffator.dict_no_key_included(diffator.obj2) +# self.assertFalse(new_res, +# "check whether the new dict should be excluded or not") # or True? FIXME + + +class TestBasicJSONHappyPath(OurTestCase): def test_empty(self): diffator = json_diff.Comparator({}, {}) diff = diffator.compare_dicts() @@ -217,8 +55,32 @@ class TestHappyPath(unittest.TestCase): "Empty objects diff.\n\nexpected = %s\n\nobserved = %s" % ({}, diff)) + def test_null(self): + self._run_test_strings('{"a": null}', '{"a": null}', + '{}', "Nulls") + + def test_null_to_string(self): + self._run_test_strings('{"a": null}', '{"a": "something"}', + '{"_update": {"a": "something"}}', "Null changed to string") + + def test_boolean(self): + self._run_test_strings('{"a": true}', '{"a": false}', + '{"_update": {"a": false}}', "Booleans") + + def test_integer(self): + self._run_test_strings('{"a": 1}', '{"a": 2}', + '{"_update": {"a": 2}}', "Integers") + + def test_float(self): + self._run_test_strings('{"a": 1.0}', '{"a": 1.1}', + '{"_update": {"a": 1.1}}', "Floats") + + def test_int_to_float(self): + self._run_test_strings('{"a": 1}', '{"a": 1.0}', + '{"_update": {"a": 1.0}}', "Integer changed to float") + def test_simple(self): - self._run_test(StringIO(SIMPLE_OLD), StringIO(SIMPLE_NEW), StringIO(SIMPLE_DIFF), + self._run_test_strings(SIMPLE_OLD, SIMPLE_NEW, SIMPLE_DIFF, "All-scalar objects diff.") def test_simple_formatted(self): @@ -227,44 +89,50 @@ class TestHappyPath(unittest.TestCase): "All-scalar objects diff (formatted).") def test_simple_array(self): - self._run_test(StringIO(SIMPLE_ARRAY_OLD), StringIO(SIMPLE_ARRAY_NEW), - StringIO(SIMPLE_ARRAY_DIFF), "Simple array objects diff.") + self._run_test_strings(SIMPLE_ARRAY_OLD, SIMPLE_ARRAY_NEW, + SIMPLE_ARRAY_DIFF, "Simple array objects diff.") def test_realFile(self): self._run_test(open("test/old.json"), open("test/new.json"), open("test/diff.json"), "Simply nested objects (from file) diff.") def test_nested(self): - self._run_test(StringIO(NESTED_OLD), StringIO(NESTED_NEW), - StringIO(NESTED_DIFF), "Nested objects diff.") - - # def test_nested_excluded(self): - # self._run_test(StringIO(NESTED_OLD), StringIO(NESTED_NEW), - # StringIO(NESTED_DIFF_EXCL), "Nested objects diff.", exc=("name")) - -# def test_piglit_results(self): -# self._run_test(open("test/old-testing-data.json"), open("test/new-testing-data.json"), -# open("test/diff-testing-data.json"), "Large piglit results diff.") + self._run_test_strings(NESTED_OLD, NESTED_NEW, NESTED_DIFF, "Nested objects diff.") def test_nested_formatted(self): self._run_test_formatted(open("test/old.json"), open("test/new.json"), codecs.open("test/nested_html_output.html", "r", "utf-8"), "Simply nested objects (from file) diff formatted as HTML.") -NO_JSON_OLD = """ -THIS IS NOT A JSON STRING -""" - -NO_JSON_NEW = """ -AND THIS NEITHER -""" + def test_nested_excluded(self): + self._run_test_strings(NESTED_OLD, NESTED_NEW, NESTED_DIFF_EXCL, + "Nested objects diff.", exc=("nome",)) +# def test_nested_included(self): +# self._run_test_strings(NESTED_OLD, NESTED_NEW, NESTED_DIFF_INCL, +# "Nested objects diff.", inc=("nome",)) -class TestSadPath(unittest.TestCase): +class TestBasicJSONSadPath(OurTestCase): def test_no_JSON(self): self.assertRaises(json_diff.BadJSONError, json_diff.Comparator, StringIO(NO_JSON_OLD), StringIO(NO_JSON_NEW)) + def test_bad_JSON_no_hex(self): + self.assertRaises(json_diff.BadJSONError, self._run_test_strings, + '{"a": 0x1}', '{"a": 2}', '{"_update": {"a": 2}}', + "Hex numbers not supported") + + def test_bad_JSON_no_octal(self): + self.assertRaises(json_diff.BadJSONError, self._run_test_strings, + '{"a": 01}', '{"a": 2}', '{"_update": {"a": 2}}', + "Octal numbers not supported") + + +#class TestPiglitData(OurTestCase): +# pass +# def test_piglit_results(self): +# self._run_test(open("test/old-testing-data.json"), open("test/new-testing-data.json"), +# open("test/diff-testing-data.json"), "Large piglit results diff.") if __name__ == "__main__": unittest.main() diff --git a/test_strings.py b/test_strings.py new file mode 100644 index 0000000..c96db7a --- /dev/null +++ b/test_strings.py @@ -0,0 +1,213 @@ +# -*- coding: utf-8 -*- +from __future__ import division, absolute_import, unicode_literals + +NO_JSON_OLD = """ +THIS IS NOT A JSON STRING +""" + +NO_JSON_NEW = """ +AND THIS NEITHER +""" + +SIMPLE_OLD = """ +{ + "a": 1, + "b": true, + "c": "Janošek" +} +""" + +SIMPLE_NEW = """ +{ + "b": false, + "c": "Maruška", + "d": "přidáno" +} +""" + +SIMPLE_DIFF = """ +{ + "_append": { + "d": "přidáno" + }, + "_remove": { + "a": 1 + }, + "_update": { + "c": "Maruška", + "b": false + } +} +""" + +SIMPLE_DIFF_HTML=""" +<!DOCTYPE html> +<html lang='en'> +<meta charset="utf-8" /> +<title>json_diff result</title> +<style> +td { +text-align: center; +} +.append_class { +color: green; +} +.remove_class { +color: red; +} +.update_class { +color: navy; +} +</style> +<body> +<h1>json_diff result</h1> +<table> +<tr> +<td class='remove_class'>a = 1</td> +</tr><tr> +<td class='update_class'>c = Maruška</td> +</tr><tr> +<td class='update_class'>b = False</td> +</tr><tr> +<td class='append_class'>d = přidáno</td> +</tr> +</table> +</body> +</html> +""" + +SIMPLE_ARRAY_OLD = """ +{ + "a": [ 1 ] +} +""" + +SIMPLE_ARRAY_NEW = """ +{ + "a": [ 1, 2 ] +} +""" + +SIMPLE_ARRAY_DIFF = """ +{ + "_update": { + "a": { + "_append": { + "1": 2 + } + } + } +} +""" + +NESTED_OLD = """ +{ + "a": 1, + "b": 2, + "ignore": { + "else": true + }, + "child": { + "nome": "Janošek" + } +} +""" + +NESTED_NEW = """ +{ + "a": 2, + "c": 3, + "child": { + "nome": "Maruška" + } +} +""" + +NESTED_DIFF = """ +{ + "_append": { + "c": 3 + }, + "_remove": { + "b": 2, + "ignore": { + "else": true + } + }, + "_update": { + "a": 2, + "child": { + "_update": { + "nome": "Maruška" + } + } + } +} +""" + +NESTED_DIFF_EXCL = """ +{ + "_append": { + "c": 3 + }, + "_remove": { + "b": 2, + "ignore": { + "else": true + } + }, + "_update": { + "a": 2 + } +} +""" + +NESTED_DIFF_INCL = """ +{ + "_update": { + "child": { + "_update": { + "nome": "Maruška" + } + } + } +} +""" + +ARRAY_OLD = """ +{ + "a": 1, + "b": 2, + "children": [ + "Pepíček", "Anička", "Maruška" + ] +} +""" + +ARRAY_NEW = """ +{ + "a": 1, + "children": [ + "Pepíček", "Tonička", "Maruška" + ], + "c": 3 +} +""" + +ARRAY_DIFF = """ +{ + "_remove": { + "b": 2 + }, + "_append": { + "c": 3 + }, + "_update": { + "children": [ + "Pepíček", + "Tonička", + "Maruška" + ] + } +} +""" |