Subversion Repositories Kolibri OS

Rev

Go to most recent revision | Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
4358 Serge 1
#!/usr/bin/env python
2
##########################################################################
3
#
4
# Copyright 2011 Jose Fonseca
5
# All Rights Reserved.
6
#
7
# Permission is hereby granted, free of charge, to any person obtaining a copy
8
# of this software and associated documentation files (the "Software"), to deal
9
# in the Software without restriction, including without limitation the rights
10
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
# copies of the Software, and to permit persons to whom the Software is
12
# furnished to do so, subject to the following conditions:
13
#
14
# The above copyright notice and this permission notice shall be included in
15
# all copies or substantial portions of the Software.
16
#
17
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23
# THE SOFTWARE.
24
#
25
##########################################################################/
26
 
27
 
28
import json
29
import optparse
30
import re
31
import difflib
32
import sys
33
 
34
 
35
def strip_object_hook(obj):
36
    if '__class__' in obj:
37
        return None
38
    for name in obj.keys():
39
        if name.startswith('__') and name.endswith('__'):
40
            del obj[name]
41
    return obj
42
 
43
 
44
class Visitor:
45
 
46
    def visit(self, node, *args, **kwargs):
47
        if isinstance(node, dict):
48
            return self.visitObject(node, *args, **kwargs)
49
        elif isinstance(node, list):
50
            return self.visitArray(node, *args, **kwargs)
51
        else:
52
            return self.visitValue(node, *args, **kwargs)
53
 
54
    def visitObject(self, node, *args, **kwargs):
55
        pass
56
 
57
    def visitArray(self, node, *args, **kwargs):
58
        pass
59
 
60
    def visitValue(self, node, *args, **kwargs):
61
        pass
62
 
63
 
64
class Dumper(Visitor):
65
 
66
    def __init__(self, stream = sys.stdout):
67
        self.stream = stream
68
        self.level = 0
69
 
70
    def _write(self, s):
71
        self.stream.write(s)
72
 
73
    def _indent(self):
74
        self._write('  '*self.level)
75
 
76
    def _newline(self):
77
        self._write('\n')
78
 
79
    def visitObject(self, node):
80
        self.enter_object()
81
 
82
        members = node.keys()
83
        members.sort()
84
        for i in range(len(members)):
85
            name = members[i]
86
            value = node[name]
87
            self.enter_member(name)
88
            self.visit(value)
89
            self.leave_member(i == len(members) - 1)
90
        self.leave_object()
91
 
92
    def enter_object(self):
93
        self._write('{')
94
        self._newline()
95
        self.level += 1
96
 
97
    def enter_member(self, name):
98
        self._indent()
99
        self._write('%s: ' % name)
100
 
101
    def leave_member(self, last):
102
        if not last:
103
            self._write(',')
104
        self._newline()
105
 
106
    def leave_object(self):
107
        self.level -= 1
108
        self._indent()
109
        self._write('}')
110
        if self.level <= 0:
111
            self._newline()
112
 
113
    def visitArray(self, node):
114
        self.enter_array()
115
        for i in range(len(node)):
116
            value = node[i]
117
            self._indent()
118
            self.visit(value)
119
            if i != len(node) - 1:
120
                self._write(',')
121
            self._newline()
122
        self.leave_array()
123
 
124
    def enter_array(self):
125
        self._write('[')
126
        self._newline()
127
        self.level += 1
128
 
129
    def leave_array(self):
130
        self.level -= 1
131
        self._indent()
132
        self._write(']')
133
 
134
    def visitValue(self, node):
135
        self._write(json.dumps(node, allow_nan=True))
136
 
137
 
138
 
139
class Comparer(Visitor):
140
 
141
    def __init__(self, ignore_added = False, tolerance = 2.0 ** -24):
142
        self.ignore_added = ignore_added
143
        self.tolerance = tolerance
144
 
145
    def visitObject(self, a, b):
146
        if not isinstance(b, dict):
147
            return False
148
        if len(a) != len(b) and not self.ignore_added:
149
            return False
150
        ak = a.keys()
151
        bk = b.keys()
152
        ak.sort()
153
        bk.sort()
154
        if ak != bk and not self.ignore_added:
155
            return False
156
        for k in ak:
157
            ae = a[k]
158
            try:
159
                be = b[k]
160
            except KeyError:
161
                return False
162
            if not self.visit(ae, be):
163
                return False
164
        return True
165
 
166
    def visitArray(self, a, b):
167
        if not isinstance(b, list):
168
            return False
169
        if len(a) != len(b):
170
            return False
171
        for ae, be in zip(a, b):
172
            if not self.visit(ae, be):
173
                return False
174
        return True
175
 
176
    def visitValue(self, a, b):
177
        if isinstance(a, float) or isinstance(b, float):
178
            if a == 0:
179
                return abs(b) < self.tolerance
180
            else:
181
                return abs((b - a)/a) < self.tolerance
182
        else:
183
            return a == b
184
 
185
 
186
class Differ(Visitor):
187
 
188
    def __init__(self, stream = sys.stdout, ignore_added = False):
189
        self.dumper = Dumper(stream)
190
        self.comparer = Comparer(ignore_added = ignore_added)
191
 
192
    def visit(self, a, b):
193
        if self.comparer.visit(a, b):
194
            return
195
        Visitor.visit(self, a, b)
196
 
197
    def visitObject(self, a, b):
198
        if not isinstance(b, dict):
199
            self.replace(a, b)
200
        else:
201
            self.dumper.enter_object()
202
            names = set(a.keys())
203
            if not self.comparer.ignore_added:
204
                names.update(b.keys())
205
            names = list(names)
206
            names.sort()
207
 
208
            for i in range(len(names)):
209
                name = names[i]
210
                ae = a.get(name, None)
211
                be = b.get(name, None)
212
                if not self.comparer.visit(ae, be):
213
                    self.dumper.enter_member(name)
214
                    self.visit(ae, be)
215
                    self.dumper.leave_member(i == len(names) - 1)
216
 
217
            self.dumper.leave_object()
218
 
219
    def visitArray(self, a, b):
220
        if not isinstance(b, list):
221
            self.replace(a, b)
222
        else:
223
            self.dumper.enter_array()
224
            max_len = max(len(a), len(b))
225
            for i in range(max_len):
226
                try:
227
                    ae = a[i]
228
                except IndexError:
229
                    ae = None
230
                try:
231
                    be = b[i]
232
                except IndexError:
233
                    be = None
234
                self.dumper._indent()
235
                if self.comparer.visit(ae, be):
236
                    self.dumper.visit(ae)
237
                else:
238
                    self.visit(ae, be)
239
                if i != max_len - 1:
240
                    self.dumper._write(',')
241
                self.dumper._newline()
242
 
243
            self.dumper.leave_array()
244
 
245
    def visitValue(self, a, b):
246
        if a != b:
247
            self.replace(a, b)
248
 
249
    def replace(self, a, b):
250
        if isinstance(a, basestring) and isinstance(b, basestring):
251
            if '\n' in a or '\n' in b:
252
                a = a.splitlines()
253
                b = b.splitlines()
254
                differ = difflib.Differ()
255
                result = differ.compare(a, b)
256
                self.dumper.level += 1
257
                for entry in result:
258
                    self.dumper._newline()
259
                    self.dumper._indent()
260
                    tag = entry[:2]
261
                    text = entry[2:]
262
                    if tag == '? ':
263
                        tag = '  '
264
                        prefix = ' '
265
                        text = text.rstrip()
266
                        suffix = ''
267
                    else:
268
                        prefix = '"'
269
                        suffix = '\\n"'
270
                    line = tag + prefix + text + suffix
271
                    self.dumper._write(line)
272
                self.dumper.level -= 1
273
                return
274
        self.dumper.visit(a)
275
        self.dumper._write(' -> ')
276
        self.dumper.visit(b)
277
 
278
    def isMultilineString(self, value):
279
        return isinstance(value, basestring) and '\n' in value
280
 
281
    def replaceMultilineString(self, a, b):
282
        self.dumper.visit(a)
283
        self.dumper._write(' -> ')
284
        self.dumper.visit(b)
285
 
286
 
287
#
288
# Unfortunately JSON standard does not include comments, but this is a quite
289
# useful feature to have on regressions tests
290
#
291
 
292
_token_res = [
293
    r'//[^\r\n]*', # comment
294
    r'"[^"\\]*(\\.[^"\\]*)*"', # string
295
]
296
 
297
_tokens_re = re.compile(r'|'.join(['(' + token_re + ')' for token_re in _token_res]), re.DOTALL)
298
 
299
 
300
def _strip_comment(mo):
301
    if mo.group(1):
302
        return ''
303
    else:
304
        return mo.group(0)
305
 
306
 
307
def _strip_comments(data):
308
    '''Strip (non-standard) JSON comments.'''
309
    return _tokens_re.sub(_strip_comment, data)
310
 
311
 
312
assert _strip_comments('''// a comment
313
"// a comment in a string
314
"''') == '''
315
"// a comment in a string
316
"'''
317
 
318
 
319
def load(stream, strip_images = True, strip_comments = True):
320
    if strip_images:
321
        object_hook = strip_object_hook
322
    else:
323
        object_hook = None
324
    if strip_comments:
325
        data = stream.read()
326
        data = _strip_comments(data)
327
        return json.loads(data, strict=False, object_hook = object_hook)
328
    else:
329
        return json.load(stream, strict=False, object_hook = object_hook)
330
 
331
 
332
def main():
333
    optparser = optparse.OptionParser(
334
        usage="\n\t%prog [options]  ")
335
    optparser.add_option(
336
        '--keep-images',
337
        action="store_false", dest="strip_images", default=True,
338
        help="compare images")
339
 
340
    (options, args) = optparser.parse_args(sys.argv[1:])
341
 
342
    if len(args) != 2:
343
        optparser.error('incorrect number of arguments')
344
 
345
    a = load(open(sys.argv[1], 'rt'), options.strip_images)
346
    b = load(open(sys.argv[2], 'rt'), options.strip_images)
347
 
348
    if False:
349
        dumper = Dumper()
350
        dumper.visit(a)
351
 
352
    differ = Differ()
353
    differ.visit(a, b)
354
 
355
 
356
if __name__ == '__main__':
357
    main()