Backup-Restore support library + tests
[integration/test.git] / csit / libraries / backuprestore / JsonDiffTool.py
diff --git a/csit/libraries/backuprestore/JsonDiffTool.py b/csit/libraries/backuprestore/JsonDiffTool.py
new file mode 100644 (file)
index 0000000..0d24764
--- /dev/null
@@ -0,0 +1,193 @@
+import argparse
+import logging
+import jsonpatch
+import json
+from jsonpathl import jsonpath
+import types
+import sys
+
+
+"""
+Library for checking differences between json files,
+allowing pre-filtering those files using a number
+of jsonpath expressions
+This library supports the automated verification
+of backup & restore use cases for applications
+Updated: 2017-04-10
+"""
+
+__author__ = "Diego Granados"
+__copyright__ = "Copyright(c) 2017, Ericsson."
+__license__ = "New-style BSD"
+__email__ = "diego.jesus.granados.lopez@ericsson.com"
+
+
+def from_path_to_jsonpatch(matchedpath):
+    """ Given a json path (using jsonpath notation), to a json patch (RFC 6902)
+    which can be used to remove the document fragment pointed by the input path
+    Note that such conversion is not formally specified anywhere, so the conversion
+    rules are experimentation-based
+
+    :param matchedpath: the input path (using jsonpath notation, see http://goessner.net/articles/JsonPath)
+    :return: the corresponding json patch for removing the fragment
+    """
+
+    logging.info('starting. filter path: %s', matchedpath)
+
+    # First step: path format change
+    # typical input: $['ietf-yang-library:modules-state']['module'][57]
+    # desired output: /ietf-yang-library:modules-state/module/57
+
+    matchedpath = matchedpath.replace('$.', '/')
+    matchedpath = matchedpath.replace('$[\'', '/')
+    matchedpath = matchedpath.replace('\'][\'', '/')
+    matchedpath = matchedpath.replace('\']', '/')
+
+    # this one is for the $[2] pattern
+    if '$[' in matchedpath and ']' in matchedpath:
+        matchedpath = matchedpath.replace('$[', '/')
+        matchedpath = matchedpath.replace(']', '')
+
+    matchedpath = matchedpath.replace('[', '')
+    matchedpath = matchedpath.replace(']', '')
+    matchedpath = matchedpath.rstrip('/')
+
+    # Now, for input: /ietf-yang-library:modules-state/module/57
+    # desired output: [{"op":"remove","path":"/ietf-yang-library:modules-state/module/57"}]
+
+    logging.info('final filter path: %s', matchedpath)
+    as_patch = '[{{"op\":\"remove\",\"path\":\"{0}\"}}]'.format(matchedpath)
+    logging.info('generated patch line: %s', as_patch)
+    return as_patch
+
+
+def apply_filter(json_arg, filtering_line):
+    """ Filters a json document by removing the elements identified by a filtering pattern
+
+    :param json_arg: the document to filter
+    :param filtering_line: The filtering pattern. This is specified using jsonpath notation grammar
+            (see http://goessner.net/articles/JsonPath/)
+    :return: the filtered document
+    """
+
+    logging.info('apply_filter:starting. jsonPath filter=[%s]', filtering_line)
+
+    res = jsonpath(json_arg, filtering_line, result_type='PATH')
+    if isinstance(res, types.BooleanType) or len(res) == 0:
+        logging.info('apply_filter: The prefilter [%s] matched nothing', filtering_line)
+        return json_arg
+    if len(res) > 1:
+        raise AssertionError('Bad pre-filter [%s] (returned [%d] entries, should return one at most',
+                             filtering_line, len(res))
+    as_json_patch = from_path_to_jsonpatch(res[0])
+    logging.info('apply_filter: applying patch! resolved patch =%s', as_json_patch)
+    patched_json = jsonpatch.apply_patch(json_arg, as_json_patch)
+
+    logging.info('apply_filter: json after patching: %s', patched_json)
+    return patched_json
+
+
+def prefilter(json_arg, initial_prefilter):
+    """ Performs the prefiltering of a json file
+    :param json_arg: the json document to filter (as string)
+        :type json_arg: str
+    :param initial_prefilter: a file containing a number of filtering patterns (using jsonpath notation)
+    :return: the original document, python-deserialized and having the fragments
+        matched by the filtering patterns removed
+    """
+
+    if not initial_prefilter:
+        logging.info('prefilter not found!')
+        # whether it is filtered or not, return as json so it can be handled uniformly from now on
+        return json.loads(json_arg)
+
+    with open(initial_prefilter) as f:
+        lines = f.read().splitlines()
+    logging.info('prefilter:lines in prefilter file: %d ', len(lines))
+    lines = filter(lambda k: not k.startswith('#'), lines)
+    logging.info('prefilter:lines after removing comments: %d ', len(lines))
+    json_args_as_json = json.loads(json_arg)
+    for filtering_line in lines:
+        json_args_as_json = apply_filter(json_args_as_json, filtering_line)
+
+    return json_args_as_json
+
+
+def prefilter_json_files_then_compare(args):
+    """ Main function. Prefilters the input files using provided prefiltering patterns,
+        then returns number of differences (and the differences themselves, when requested)
+
+    :param args: Input arguments, already parsed
+    :return: the number of differences (from a jsonpatch standpoint) between the input
+             json files (those input files can be prefiltered using a number of patterns when
+             requested)
+    """
+
+    logging.info('prefilter_json_files_then_compare: starting!')
+    with open(args.initialFile) as f:
+        json_initial = file.read(f)
+    with open(args.finalFile) as f2:
+        json_final = file.read(f2)
+
+    patch = jsonpatch.JsonPatch.from_diff(json_initial, json_final)
+    logging.info('prefilter_json_files_then_compare:differences before patching: %d', len(list(patch)))
+
+    json_initial_filtered = prefilter(json_initial, args.initial_prefilter)
+    json_final_filtered = prefilter(json_final, args.finalPreFilter)
+
+    patch_after_filtering = jsonpatch.JsonPatch.from_diff(json_initial_filtered, json_final_filtered)
+    differences_after_patching = list(patch_after_filtering)
+    logging.info('prefilter_json_files_then_compare: differences after patching: %d', len(differences_after_patching))
+
+    if args.printDifferences:
+        for patchline in differences_after_patching:
+            print json.dumps(patchline)
+
+    print len(differences_after_patching)
+    return len(differences_after_patching)
+
+
+def Json_Diff_Check_Keyword(json_before, json_after, filter_before, filter_after):
+    input_argv = ['-i', json_before, '-f', json_after, '-ipf', filter_before, '-fpf', filter_after, '-pd']
+    sys.argv[1:] = input_argv
+    logging.info('starting. constructed command line: %s', sys.argv)
+    return Json_Diff_Check()
+
+
+def parse_args(args):
+    parser = argparse.ArgumentParser(description='both initial and final json files are compared for differences. '
+                                                 'The program returns 0 when the json contents are the same, or the '
+                                                 'number of'
+                                                 ' differences otherwise. Both json files can be prefiltered for '
+                                                 'certain patterns'
+                                                 ' before checking the differences')
+
+    parser.add_argument('-i', '--initialFile', required='true', dest='initialFile', action='store',
+                        help='initial json file')
+    parser.add_argument('-f', '--finalFile', required='true', dest='finalFile', action='store', help='final json file')
+    parser.add_argument('-ipf', '--initial_prefilter', dest='initial_prefilter',
+                        help='File with pre-filtering patterns to apply to the initial json file before comparing')
+    parser.add_argument('-fpf', '--finalPreFilter', dest='finalPreFilter',
+                        help='File with pre-filtering patterns to apply to the final json file before comparing')
+    parser.add_argument('-pd', '--printDifferences', action='store_true',
+                        help='on differences found, prints the list of paths for the found differences before exitting')
+    parser.add_argument('-v', '--verbose', dest='verbose', action='store_true', help='generate log information')
+    return parser.parse_args(args)
+
+
+def Json_Diff_Check():
+    args = parse_args(sys.argv[1:])
+
+    if hasattr(args, 'verbose'):
+        if args.verbose:
+            logging.basicConfig(level=logging.DEBUG)
+
+    if args.printDifferences:
+        logging.info('(will print differences)')
+
+    result = prefilter_json_files_then_compare(args)
+    return result
+
+
+if __name__ == '__main__':
+    Json_Diff_Check()