diff --git a/changelogs/fragments/Fre_fact_diff.yaml b/changelogs/fragments/Fre_fact_diff.yaml new file mode 100644 index 0000000..dfa49f7 --- /dev/null +++ b/changelogs/fragments/Fre_fact_diff.yaml @@ -0,0 +1,3 @@ +--- +minor_changes: + - Add support in fact_diff filter plugin to show common lines.(https://github.com/ansible-collections/ansible.utils/issues/311) diff --git a/docs/ansible.utils.fact_diff_filter.rst b/docs/ansible.utils.fact_diff_filter.rst index f3460d6..2241c4f 100644 --- a/docs/ansible.utils.fact_diff_filter.rst +++ b/docs/ansible.utils.fact_diff_filter.rst @@ -70,6 +70,27 @@ Parameters
The first fact to be used in the comparison.
+ + +
+ common + +
+ boolean +
+ + + + + + + +
Show all common lines.
+ +
diff --git a/plugins/filter/fact_diff.py b/plugins/filter/fact_diff.py index 1eb13bb..a690124 100644 --- a/plugins/filter/fact_diff.py +++ b/plugins/filter/fact_diff.py @@ -59,6 +59,11 @@ DOCUMENTATION = """ comparison type: list elements: str + common: + description: + - Show all common lines. + type: bool + default: false """ EXAMPLES = """ @@ -185,7 +190,7 @@ except ImportError: def _fact_diff(*args, **kwargs): """Find the difference between currently set facts""" - keys = ["before", "after", "plugin"] + keys = ["before", "after", "plugin", "common"] data = dict(zip(keys, args[1:])) data.update(kwargs) aav = AnsibleArgSpecValidator(data=data, schema=DOCUMENTATION, name="fact_diff") diff --git a/plugins/plugin_utils/fact_diff.py b/plugins/plugin_utils/fact_diff.py index 831ba3b..e2d0f82 100644 --- a/plugins/plugin_utils/fact_diff.py +++ b/plugins/plugin_utils/fact_diff.py @@ -10,8 +10,11 @@ The fact_diff plugin code """ from __future__ import absolute_import, division, print_function +import difflib import re +from collections.abc import MutableMapping + from ansible.plugins.callback import CallbackBase @@ -31,7 +34,7 @@ def _raise_error(msg): raise AnsibleFilterError(error) -def fact_diff(before, after, plugin): +def fact_diff(before, after, plugin, common): """Compare two facts or variables and get a diff. :param before: The first fact to be used in the comparison. :type before: raw @@ -40,46 +43,103 @@ def fact_diff(before, after, plugin): :param plugin: The name of the plugin in collection format :type plugin: string """ - result = run_diff(before, after, plugin) + if plugin.get("name") == "ansible.utils.native": + result = fact_diff_native().run_diff(before, after, plugin, common) return result -def _check_valid_regexes(skip_lines): - if skip_lines: - for idx, regex in enumerate(skip_lines): - try: - skip_lines[idx] = re.compile(regex) - except re.error as exc: - msg = "The regex '{regex}', is not valid. The error was {err}.".format( - regex=regex, - err=str(exc), +class fact_diff_native(CallbackBase): + def _check_valid_regexes(self, skip_lines): + if skip_lines: + for idx, regex in enumerate(skip_lines): + try: + skip_lines[idx] = re.compile(regex) + except re.error as exc: + msg = "The regex '{regex}', is not valid. The error was {err}.".format( + regex=regex, + err=str(exc), + ) + _raise_error(msg) + + def _xform(self, before, after, skip_lines): + if skip_lines: + if isinstance(before, str): + before = before.splitlines() + if isinstance(after, str): + after = after.splitlines() + before = [ + line for line in before if not any(regex.match(str(line)) for regex in skip_lines) + ] + after = [ + line for line in after if not any(regex.match(str(line)) for regex in skip_lines) + ] + if isinstance(before, list): + before = "\n".join(map(str, before)) + "\n" + if isinstance(after, list): + after = "\n".join(map(str, after)) + "\n" + return before, after, skip_lines + + def get_fact_diff(self, difflist): + if not isinstance(difflist, list): + difflist = [difflist] + ret = [] + for diff in difflist: + if "before" in diff and "after" in diff: + # format complex structures into 'files' + for x in ["before", "after"]: + if isinstance(diff[x], MutableMapping): + diff[x] = self._serialize_diff(diff[x]) + elif diff[x] is None: + diff[x] = "" + if "before_header" in diff: + before_header = "before: %s" % diff["before_header"] + else: + before_header = "before" + if "after_header" in diff: + after_header = "after: %s" % diff["after_header"] + else: + after_header = "after" + before_lines = diff["before"].splitlines(True) + after_lines = diff["after"].splitlines(True) + if before_lines and not before_lines[-1].endswith("\n"): + before_lines[-1] += "\n\\ No newline at end of file\n" + if after_lines and not after_lines[-1].endswith("\n"): + after_lines[-1] += "\n\\ No newline at end of file\n" + diff_context = ( + len(before_lines) if len(before_lines) > len(after_lines) else len(after_lines) ) - _raise_error(msg) + differ = difflib.unified_diff( + before_lines, + after_lines, + fromfile=before_header, + tofile=after_header, + fromfiledate="", + tofiledate="", + n=diff_context, + ) + difflines = list(differ) + has_diff = False + for line in difflines: + has_diff = True + if diff["common"]: + if line.startswith("+") or line.startswith("-"): + pass + else: + ret.append(line) + else: + ret.append(line) + if has_diff: + ret.append("\n") + if "prepared" in diff: + ret.append(diff["prepared"]) + return "".join(ret) - -def _xform(before, after, skip_lines): - if skip_lines: - if isinstance(before, str): - before = before.splitlines() - if isinstance(after, str): - after = after.splitlines() - before = [ - line for line in before if not any(regex.match(str(line)) for regex in skip_lines) - ] - after = [line for line in after if not any(regex.match(str(line)) for regex in skip_lines)] - if isinstance(before, list): - before = "\n".join(map(str, before)) + "\n" - if isinstance(after, list): - after = "\n".join(map(str, after)) + "\n" - return before, after, skip_lines - - -def run_diff(before, after, plugin): - skip_lines = plugin["vars"].get("skip_lines") - _check_valid_regexes(skip_lines=skip_lines) - before, after, skip_lines = _xform(before, after, skip_lines=skip_lines) - diff = CallbackBase()._get_diff({"before": before, "after": after}) - ansi_escape = re.compile(r"\x1B[@-_][0-?]*[ -/]*[@-~]") - diff_text = ansi_escape.sub("", diff) - result = list(diff_text.splitlines()) - return result + def run_diff(self, before, after, plugin, common): + skip_lines = plugin["vars"].get("skip_lines") + self._check_valid_regexes(skip_lines=skip_lines) + before, after, skip_lines = self._xform(before, after, skip_lines=skip_lines) + diff = self.get_fact_diff({"before": before, "after": after, "common": common}) + ansi_escape = re.compile(r"\x1B[@-_][0-?]*[ -/]*[@-~]") + diff_text = ansi_escape.sub("", diff) + result = list(diff_text.splitlines()) + return result diff --git a/tests/integration/targets/utils_fact_diff/tasks/filter.yaml b/tests/integration/targets/utils_fact_diff/tasks/filter.yaml index 2ff2f1a..bd6e02c 100644 --- a/tests/integration/targets/utils_fact_diff/tasks/filter.yaml +++ b/tests/integration/targets/utils_fact_diff/tasks/filter.yaml @@ -20,28 +20,16 @@ msg: "The regex '+', is not valid" - name: Check argspec validation - ansible.builtin.set_fact: "{{ ansible.utils.fact_diff([1, 2, 3]) }}" + ansible.builtin.set_fact: + result: "{{ [1, 2, 3] | ansible.utils.fact_diff() }}" + register: error ignore_errors: true - register: result - name: Assert ansible.builtin.assert: - that: "{{ msg in result.msg }}" + that: "{{ msg in error.msg }}" vars: msg: "missing required arguments: after" - when: "result.msg | type_debug == 'list'" - -- name: Check argspec validation - ansible.builtin.set_fact: "{{ ansible.utils.fact_diff([1, 2, 3]) }}" - ignore_errors: true - register: result - -- name: Assert - ansible.builtin.assert: - that: "{{ msg in result.msg }}" - vars: - msg: "missing required arguments: before" - when: "result.msg | type_debug == 'list'" - name: Set fact ansible.builtin.set_fact: @@ -69,7 +57,7 @@ before: "{{ before | ansible.utils.to_paths }}" after: "{{ after | ansible.utils.to_paths }}" -- name: Show the difference in json format +- name: Show the difference in path format ansible.builtin.set_fact: result: "{{ before | ansible.utils.fact_diff(after) }}" @@ -78,6 +66,27 @@ before: "{{ before | to_nice_yaml }}" after: "{{ after | to_nice_yaml }}" -- name: Show the difference in json format +- name: Show the difference in yaml format ansible.builtin.set_fact: result: "{{ before | ansible.utils.fact_diff(after) }}" + +- name: Set fact + ansible.builtin.set_fact: + before: + a: + b: + c: + d: + - 0 + - 1 + after: + a: + b: + c: + d: + - 2 + - 3 + +- name: Show the common lines in json format + ansible.builtin.set_fact: + result: "{{ before | ansible.utils.fact_diff(after, common=true) }}" diff --git a/tests/unit/plugins/filter/test_fact_diff.py b/tests/unit/plugins/filter/test_fact_diff.py index f112d12..c4bdff0 100644 --- a/tests/unit/plugins/filter/test_fact_diff.py +++ b/tests/unit/plugins/filter/test_fact_diff.py @@ -106,3 +106,13 @@ class TestUpdate_Fact(unittest.TestCase): "missing required arguments: after", str(error.exception), ) + + def test_diff_dict_common(self): + """Compare two dicts that with common option""" + self.maxDiff = None + before = {"a": {"b": {"c": {"d": [0, 1, 2, 3]}}}} + after = {"a": {"b": {"c": {"d": [0, 1, 2, 4]}}}} + result = _fact_diff("", before, after, common=True) + self.assertIn(" 0", result) + self.assertIn(" 1", result) + self.assertIn(" 2", result)