aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
m---------hinnerup0
-rwxr-xr-x[-rw-r--r--]json_diff.py216
-rw-r--r--test/diff-testing-data.json24
-rw-r--r--test/diff.json28
-rw-r--r--test/nested_html_output.html43
-rw-r--r--test/new.json5
-rw-r--r--test/old.json4
-rw-r--r--test_json_diff.py62
8 files changed, 292 insertions, 90 deletions
diff --git a/hinnerup b/hinnerup
deleted file mode 160000
-Subproject 5dd7de36d0b636c25aebf72d97c1ac2e618e8a1
diff --git a/json_diff.py b/json_diff.py
index 1dadbf5..8a2cc5c 100644..100755
--- a/json_diff.py
+++ b/json_diff.py
@@ -2,72 +2,195 @@
# -*- coding: utf-8 -*-
"""
Script for comparing two objects
+
+Copyright (c) 2011, Red Hat Corp.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+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.
"""
import json
from optparse import OptionParser
import logging
+__author__ = "Matěj Cepl"
+__version__ = "0.1.0"
+
logging.basicConfig(format='%(levelname)s:%(funcName)s:%(message)s', level=logging.INFO)
+STYLE_MAP = {
+ u"_append": "append_class",
+ u"_remove": "remove_class",
+ u"_update": "update_class"
+}
+
+
+LEVEL_INDENT = " "
+
+out_str_template = u"""
+<!DOCTYPE html>
+<html lang='en'>
+<meta charset="utf-8" />
+<title>%s</title>
+<style>
+td {
+ text-align: center;
+}
+.append_class {
+ color: green;
+}
+.remove_class {
+ color: red;
+}
+.update_class {
+ color: navy;
+}
+</style>
+<body>
+ <h1>%s</h1>
+ <table>
+ %s
+"""
+
+class HTMLFormatter(object):
+
+ def __init__(self, diff_object):
+ self.diff = diff_object
+
+ def _generate_page(self, in_dict, title="json_diff result"):
+ out_str = out_str_template % (title, title,
+ self._format_dict(in_dict))
+ out_str += """
+ </table>
+ </body>
+ </html>
+ """
+ return out_str
+
+ @staticmethod
+ def _is_scalar(value):
+ return not isinstance(value, (list, tuple, dict))
+
+ def _is_leafnode(self, node):
+ # anything else than dict shouldn't happen here, so that would be
+ # pure error
+ assert(isinstance(node, dict))
+ # the following lines mean that it is an expression
+ out = True
+ for key in node:
+ if not self._is_scalar(node[key]):
+ out = False
+ return out
+
+ # doesn't have level and neither concept of it, much
+ def _format_dict(self, diff_dict, typch="unknown_change", level=0):
+ internal_keys = set(STYLE_MAP.keys())
+ level_str = ("<td>" + LEVEL_INDENT + "</td>") * level
+ 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 typechange in set(diff_dict.keys()) & internal_keys:
+ logging.debug("---- internal typechange in diff_dict.keys() = %s", typechange)
+ logging.debug("---- diff_dict[typechange] = %s", unicode(diff_dict[typechange]))
+ logging.debug("---- self._is_leafnode(diff_dict[typechange]) = %s",
+ self._is_leafnode(diff_dict[typechange]))
+ out_str += self._format_dict(diff_dict[typechange], typechange, level)
+
+ for variable in set(diff_dict.keys()) - internal_keys:
+ logging.debug("**** external variable in diff_dict.keys() = %s", variable)
+ logging.debug("**** diff_dict[variable] = %s", unicode(diff_dict[variable]))
+ logging.debug("**** self._is_scalar(diff_dict[variable]) = %s",
+ self._is_scalar(diff_dict[variable]))
+ if self._is_scalar(diff_dict[variable]):
+ out_str += ("<tr>\n %s<td class='%s'>%s = %s</td>\n </tr>\n" %
+ (level_str, STYLE_MAP[typch], variable, unicode(diff_dict[variable])))
+ logging.debug("out_str = %s", out_str)
+ else:
+ out_str += self._format_dict(diff_dict[variable], None, level+1)
+
+ return out_str
+
+
+ def __str__(self):
+ return self._generate_page(self.diff).encode("utf-8")
+
+class BadJSONError(ValueError):
+ pass
+
class Comparator(object):
"""
Main workhorse, the object itself
"""
def __init__(self, fn1=None, fn2=None, excluded_attrs=()):
if fn1:
- self.obj1 = json.load(fn1)
+ try:
+ self.obj1 = json.load(fn1)
+ except (TypeError, OverflowError, ValueError) as exc:
+ raise BadJSONError("Cannot decode object from JSON.\n%s" % unicode(exc))
if fn2:
- self.obj2 = json.load(fn2)
+ try:
+ self.obj2 = json.load(fn2)
+ except (TypeError, OverflowError, ValueError) as exc:
+ raise BadJSONError("Cannot decode object from JSON\n%s" % unicode(exc))
self.excluded_attributes = excluded_attrs
- if (fn1 and fn2):
- logging.debug("self.obj1 = %s\nself.obj2 = %s\nself.excluded_attrs = %s", \
- (self.obj1, self.obj2, self.excluded_attributes))
@staticmethod
- def _get_keys(obj):
- """
- Getter for the current object's keys.
- """
- out = set()
- for key in obj.keys():
- out.add(key)
- return out
-
- @staticmethod
- def _is_scalar(value):
+ def is_scalar(value):
"""
Primitive version, relying on the fact that JSON cannot
contain any more complicated data structures.
"""
return not isinstance(value, (list, tuple, dict))
-
+
def _compare_arrays(self, old_arr, new_arr):
+ """
+ simpler version of compare_dicts; just an internal method, becase
+ it could never be called from outside.
+ """
inters = min(old_arr, new_arr)
result = {
- u"append": {},
- u"remove": {},
- u"update": {}
+ "_append": {},
+ "_remove": {},
+ "_update": {}
}
for idx in range(len(inters)):
# changed objects, new value is new_arr
if (type(old_arr[idx]) != type(new_arr[idx])):
- result['update'][idx] = new_arr[idx]
+ result[u'_update'][idx] = new_arr[idx]
# another simple variant ... scalars
- elif (self._is_scalar(old_arr)):
+ elif (self.is_scalar(old_arr)):
if old_arr[idx] != new_arr[idx]:
- result['update'][idx] = new_arr[idx]
+ result[u'_update'][idx] = new_arr[idx]
# recursive arrays
elif (isinstance(old_arr[idx], list)):
- res_arr = self._compare_arrays(old_arr[idx], \
+ res_arr = self._compare_arrays(old_arr[idx],
new_arr[idx])
if (len(res_arr) > 0):
- result['update'][idx] = res_arr
+ result[u'_update'][idx] = res_arr
# and now nested dicts
elif isinstance(old_arr[idx], dict):
res_dict = self.compare_dicts(old_arr[idx], new_arr[idx])
if (len(res_dict) > 0):
- result['update'][idx] = res_dict
+ result[u'_update'][idx] = res_dict
# Clear out unused inters in result
out_result = {}
@@ -89,16 +212,16 @@ class Comparator(object):
old_keys = set()
new_keys = set()
if old_obj and len(old_obj) > 0:
- old_keys = self._get_keys(old_obj)
+ old_keys = set(old_obj.keys())
if new_obj and len(new_obj) > 0:
- new_keys = self._get_keys(new_obj)
+ new_keys = set(new_obj.keys())
keys = old_keys | new_keys
result = {
- u"append": {},
- u"remove": {},
- u"update": {}
+ "_append": {},
+ "_remove": {},
+ "_update": {}
}
for name in keys:
# Explicitly excluded arguments
@@ -106,35 +229,35 @@ class Comparator(object):
continue
# old_obj is missing
if name not in old_obj:
- result['append'][name] = new_obj[name]
+ result[u'_append'][name] = new_obj[name]
# new_obj is missing
elif name not in new_obj:
- result['remove'][name] = old_obj[name]
+ result[u'_remove'][name] = old_obj[name]
# changed objects, new value is new_obj
elif (type(old_obj[name]) != type(new_obj[name])):
- result['update'][name] = new_obj[name]
+ result[u'_update'][name] = new_obj[name]
# last simple variant ... scalars
- elif (self._is_scalar(old_obj[name])):
+ elif (self.is_scalar(old_obj[name])):
if old_obj[name] != new_obj[name]:
- result['update'][name] = new_obj[name]
+ result[u'_update'][name] = new_obj[name]
# now arrays
elif (isinstance(old_obj[name], list)):
- res_arr = self._compare_arrays(old_obj[name], \
+ res_arr = self._compare_arrays(old_obj[name],
new_obj[name])
if (len(res_arr) > 0):
- result['update'][name] = res_arr
+ result[u'_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
+ result[u'_update'][name] = res_dict
# Clear out unused keys in result
out_result = {}
for key in result:
if len(result[key]) > 0:
out_result[key] = result[key]
-
+
return out_result
@@ -144,11 +267,18 @@ if __name__ == "__main__":
parser.add_option("-x", "--exclude",
action="append", dest="exclude", metavar="ATTR", default=[],
help="attributes which should be ignored when comparing")
+ parser.add_option("-H", "--HTML",
+ action="store_true", dest="HTMLoutput", metavar="BOOL", default=False,
+ help="program should output to HTML report")
(options, args) = parser.parse_args()
- logging.debug("options = %s", str(options))
- logging.debug("args = %s", str(args))
+
if len(args) != 2:
parser.error("Script requires two positional arguments, names for old and new JSON file.")
diff = Comparator(file(args[0]), file(args[1]), options.exclude)
- print json.dumps(diff.compare_dicts(), indent=4, ensure_ascii=False) \ No newline at end of file
+ if options.HTMLoutput:
+ diff_res = diff.compare_dicts()
+ logging.debug("diff_res:\n%s", json.dumps(diff_res, indent=True))
+ print HTMLFormatter(diff_res)
+ else:
+ print json.dumps(diff.compare_dicts(), indent=4, ensure_ascii=False).encode("utf-8")
diff --git a/test/diff-testing-data.json b/test/diff-testing-data.json
index 884b7e1..0fe28b1 100644
--- a/test/diff-testing-data.json
+++ b/test/diff-testing-data.json
@@ -1,54 +1,54 @@
{
- "update": {
+ "_update": {
"tests": {
- "update": {
+ "_update": {
"spec/ARB_shader_texture_lod/execution/arb_shader_texture_lod-texgrad": {
- "update": {
+ "_update": {
"info": "Returncode: 1\n\nErrors:\n\n\nOutput:\nLeft: texture2D, Right: texture2DGradARB\nProbe at (4,2)\n Left: 1.000000 0.000000 0.000000 1.000000\n Right: 0.945098 0.890196 0.945098 1.000000\n"
}
},
"glean/fbo": {
- "update": {
+ "_update": {
"info": "Returncode: 0\n\nErrors:\n\n\nOutput:\n----------------------------------------------------------------------\nfbo test: Test OpenGL Extension GL_EXT_framebuffer_object\n\nGL_EXT_framebuffer_object is supported\nGL_ARB_framebuffer_object is supported\n (FBOTest::testRender2SingleTexture:648)GL_FRAMEBUFFER_UNSUPPORTED_EXT\n (FBOTest::testRender2SingleTexture:648)GL_FRAMEBUFFER_UNSUPPORTED_EXT\n (FBOTest::testRender2SingleTexture:648)GL_FRAMEBUFFER_UNSUPPORTED_EXT\n (FBOTest::testRender2SingleTexture:648)GL_FRAMEBUFFER_UNSUPPORTED_EXT\nfbo: NOTE perf[0] = 182.54 MB/s, which is using glCopyTexImage2D\nfbo: NOTE perf[1] = 165.462 MB/s, which is using FBO\nfbo: PASS rgba8, db, z24, s8, win+pmap, id 33\n\t8 tests passed, 0 tests failed.\n\n"
}
},
"glx/glx-pixmap-crosscheck": {
- "remove": {
+ "_remove": {
"returncode": 0
}
},
"glean/pbo": {
- "update": {
+ "_update": {
"info": "Returncode: 0\n\nErrors:\n\n\nOutput:\n----------------------------------------------------------------------\npbo test: Test OpenGL Extension GL_ARB_pixel_buffer_object\n\npbo: PASS rgba8, db, z24, s8, win+pmap, id 33\n\t10 tests passed, 0 tests failed.\n\n"
}
},
"glx/glx-swap-singlebuffer": {
- "update": {
+ "_update": {
"info": "Returncode: 1\n\nErrors:\nX Error of failed request: BadDrawable (invalid Pixmap or Window parameter)\n Major opcode of failed request: 137 (DRI2)\n Minor opcode of failed request: 8 ()\n Resource id in failed request: 0x9e00002\n Serial number of failed request: 35\n Current serial number in output stream: 35\n\n\nOutput:\n"
}
},
"glean/teapot": {
- "update": {
+ "_update": {
"info": "Returncode: 0\n\nErrors:\n\n\nOutput:\n----------------------------------------------------------------------\nThis test simply displays a teapot, rotates it, and attempts to\ndetermine the frame/sec the pipeline can generate\n\nteapot: PASS Teapots/Sec: 59.4814 rgba8, db, z24, s8, win+pmap, id 33\n\n"
}
},
"spec/!OpenGL 2.0/vertex-program-two-side enabled back2": {
- "update": {
+ "_update": {
"info": "Returncode: 1\n\nErrors:\n\n\nOutput:\nWindow quadrants show:\n+-------------------------+------------------------+\n| front gl_Color | back gl_Color |\n+-------------------------+------------------------+\n| front gl_SecondaryColor | back gl_SecondaryColor |\n+-------------------------+------------------------+\nProbe at (50,0)\n Expected: 0.000000 0.000000 0.250000 0.000000\n Observed: 0.007843 0.007843 0.007843 0.007843\n"
}
},
"spec/glsl-1.10/execution/samplers/in-parameter-struct": {
- "update": {
+ "_update": {
"info": "Returncode: -6\n\nErrors:\n\n\nOutput:\nir_dereference_variable @ 0x1247460 specifies undeclared variable `channel_expressions' @ 0x11d00e0\n"
}
},
"fbo/fbo-sys-blit": {
- "update": {
+ "_update": {
"info": "Returncode: 1\n\nErrors:\n\n\nOutput:\nProbe at (64,64)\n Expected: 0.000000 1.000000 0.000000\n Observed: 0.325490 0.529412 0.823529\n"
}
},
"spec/glsl-1.10/execution/samplers/normal-parameter-struct": {
- "update": {
+ "_update": {
"info": "Returncode: -6\n\nErrors:\n\n\nOutput:\nir_dereference_variable @ 0x1de33f0 specifies undeclared variable `channel_expressions' @ 0x1d6c230\n"
}
}
diff --git a/test/diff.json b/test/diff.json
index df7a5f5..e8a2330 100644
--- a/test/diff.json
+++ b/test/diff.json
@@ -1,17 +1,19 @@
{
- "append": {
- "c": 3,
- "daughter": {
- "name": "Maruška"
+ "_remove": {
+ "b": 2
+ },
+ "_update": {
+ "a": 2,
+ "children": {
+ "_update": {
+ "son": "Ivánek"
+ },
+ "_append": {
+ "daughter": "Maruška"
+ }
}
- },
- "remove": {
- "b": 2,
- "son": {
- "name": "Janošek"
- }
- },
- "update": {
- "a": 2
+ },
+ "_append": {
+ "c": 3
}
}
diff --git a/test/nested_html_output.html b/test/nested_html_output.html
new file mode 100644
index 0000000..6c5b83c
--- /dev/null
+++ b/test/nested_html_output.html
@@ -0,0 +1,43 @@
+
+<!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'>b = 2</td>
+ </tr>
+<tr>
+ <td class='update_class'>a = 2</td>
+ </tr>
+<tr>
+ <td>&nbsp;</td><td class='update_class'>son = Ivánek</td>
+ </tr>
+<tr>
+ <td>&nbsp;</td><td class='append_class'>daughter = Maruška</td>
+ </tr>
+<tr>
+ <td class='append_class'>c = 3</td>
+ </tr>
+
+
+ </table>
+ </body>
+ </html>
+
diff --git a/test/new.json b/test/new.json
index 86da691..235c843 100644
--- a/test/new.json
+++ b/test/new.json
@@ -1,7 +1,8 @@
{
"a": 2,
"c": 3,
- "daughter": {
- "name": "Maruška"
+ "children": {
+ "son": "Ivánek",
+ "daughter": "Maruška"
}
}
diff --git a/test/old.json b/test/old.json
index 17d6738..f887343 100644
--- a/test/old.json
+++ b/test/old.json
@@ -1,7 +1,7 @@
{
"a": 1,
"b": 2,
- "son": {
- "name": "Janošek"
+ "children": {
+ "son": "Janošek"
}
}
diff --git a/test_json_diff.py b/test_json_diff.py
index 3a514eb..21faf70 100644
--- a/test_json_diff.py
+++ b/test_json_diff.py
@@ -25,13 +25,13 @@ SIMPLE_NEW = u"""
SIMPLE_DIFF = u"""
{
- "append": {
+ "_append": {
"d": "přidáno"
},
- "remove": {
+ "_remove": {
"a": 1
},
- "update": {
+ "_update": {
"c": "Maruška",
"b": false
}
@@ -60,63 +60,89 @@ NESTED_NEW = u"""
NESTED_DIFF = u"""
{
- "append": {
+ "_append": {
"c": 3,
"daughter": {
"name": "Maruška"
}
},
- "remove": {
+ "_remove": {
"b": 2,
"son": {
"name": "Janošek"
}
},
- "update": {
+ "_update": {
"a": 2
}
}
"""
-class TestXorgAnalyze(unittest.TestCase):
+class TestHappyPath(unittest.TestCase):
def test_empty(self):
diffator = json_diff.Comparator({}, {})
diff = diffator.compare_dicts()
- self.assertEqual(json.dumps(diff).strip(), "{}", \
- "Empty objects diff.\n\nexpected = %s\n\nobserved = %s" % \
+ self.assertEqual(json.dumps(diff).strip(), "{}",
+ "Empty objects diff.\n\nexpected = %s\n\nobserved = %s" %
(str({}), str(diff)))
def test_simple(self):
diffator = json_diff.Comparator(StringIO(SIMPLE_OLD), StringIO(SIMPLE_NEW))
diff = diffator.compare_dicts()
expected = json.loads(SIMPLE_DIFF)
- self.assertEqual(diff, expected, "All-scalar objects diff." + \
- "\n\nexpected = %s\n\nobserved = %s" % \
+ self.assertEqual(diff, expected, "All-scalar objects diff." +
+ "\n\nexpected = %s\n\nobserved = %s" %
(str(expected), str(diff)))
def test_realFile(self):
diffator = json_diff.Comparator(open("test/old.json"), open("test/new.json"))
diff = diffator.compare_dicts()
expected = json.load(open("test/diff.json"))
- self.assertEqual(diff, expected, "Simply nested objects (from file) diff." + \
- "\n\nexpected = %s\n\nobserved = %s" % \
+ self.assertEqual(diff, expected, "Simply nested objects (from file) diff." +
+ "\n\nexpected = %s\n\nobserved = %s" %
(str(expected), str(diff)))
def test_nested(self):
diffator = json_diff.Comparator(StringIO(NESTED_OLD), StringIO(NESTED_NEW))
diff = diffator.compare_dicts()
expected = json.loads(NESTED_DIFF)
- self.assertEqual(diff, expected, "Nested objects diff. " + \
- "\n\nexpected = %s\n\nobserved = %s" % \
+ self.assertEqual(diff, expected, "Nested objects diff. " +
+ "\n\nexpected = %s\n\nobserved = %s" %
(str(expected), str(diff)))
+
+ def test_nested_formatted(self):
+ diffator = json_diff.Comparator(open("test/old.json"), open("test/new.json"))
+ diff = "\n".join([line.strip() \
+ for line in str(json_diff.HTMLFormatter(diffator.compare_dicts())).split("\n")])
+ expected = "\n".join([line.strip() for line in open("test/nested_html_output.html").readlines()])
+ self.assertEqual(diff, expected, "Simply nested objects (from file) diff formatted as HTML." +
+ "\n\nexpected = %s\n\nobserved = %s" %
+ (expected, diff))
+
def test_large_with_exclusions(self):
- diffator = json_diff.Comparator(open("test/old-testing-data.json"), \
+ diffator = json_diff.Comparator(open("test/old-testing-data.json"),
open("test/new-testing-data.json"), ('command', 'time'))
diff = diffator.compare_dicts()
expected = json.load(open("test/diff-testing-data.json"))
- self.assertEqual(diff, expected, "Large objects with exclusions diff." + \
- "\n\nexpected = %s\n\nobserved = %s" % \
+ self.assertEqual(diff, expected, "Large objects with exclusions diff." +
+ "\n\nexpected = %s\n\nobserved = %s" %
(str(expected), str(diff)))
+
+NO_JSON_OLD = u"""
+THIS IS NOT A JSON STRING
+"""
+
+NO_JSON_NEW = u"""
+AND THIS NEITHER
+"""
+
+
+class TestSadPath(unittest.TestCase):
+ def test_no_JSON(self):
+ self.assertRaises(json_diff.BadJSONError,
+ json_diff.Comparator, StringIO(NO_JSON_OLD), StringIO(NO_JSON_NEW))
+
+
if __name__ == "__main__":
unittest.main() \ No newline at end of file