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.
+
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)
|