Enhance fact_diff to show common lines (#323)
* Enhance fact_diff to show common lines * Add integration and unit tests * Fix sanity --------- Co-authored-by: Ashwini Mhatre <amhatre@amhatre-thinkpadt14sgen2i.pnq.csb>pull/326/head^2
parent
259cb8b9e4
commit
92903a3a58
|
@ -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)
|
|
@ -70,6 +70,27 @@ Parameters
|
||||||
<div>The first fact to be used in the comparison.</div>
|
<div>The first fact to be used in the comparison.</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="3">
|
||||||
|
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||||
|
<b>common</b>
|
||||||
|
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||||
|
<div style="font-size: small">
|
||||||
|
<span style="color: purple">boolean</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<ul style="margin: 0; padding: 0"><b>Choices:</b>
|
||||||
|
<li><div style="color: blue"><b>no</b> ←</div></li>
|
||||||
|
<li>yes</li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div>Show all common lines.</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="3">
|
<td colspan="3">
|
||||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||||
|
|
|
@ -59,6 +59,11 @@ DOCUMENTATION = """
|
||||||
comparison
|
comparison
|
||||||
type: list
|
type: list
|
||||||
elements: str
|
elements: str
|
||||||
|
common:
|
||||||
|
description:
|
||||||
|
- Show all common lines.
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
"""
|
"""
|
||||||
|
|
||||||
EXAMPLES = """
|
EXAMPLES = """
|
||||||
|
@ -185,7 +190,7 @@ except ImportError:
|
||||||
def _fact_diff(*args, **kwargs):
|
def _fact_diff(*args, **kwargs):
|
||||||
"""Find the difference between currently set facts"""
|
"""Find the difference between currently set facts"""
|
||||||
|
|
||||||
keys = ["before", "after", "plugin"]
|
keys = ["before", "after", "plugin", "common"]
|
||||||
data = dict(zip(keys, args[1:]))
|
data = dict(zip(keys, args[1:]))
|
||||||
data.update(kwargs)
|
data.update(kwargs)
|
||||||
aav = AnsibleArgSpecValidator(data=data, schema=DOCUMENTATION, name="fact_diff")
|
aav = AnsibleArgSpecValidator(data=data, schema=DOCUMENTATION, name="fact_diff")
|
||||||
|
|
|
@ -10,8 +10,11 @@ The fact_diff plugin code
|
||||||
"""
|
"""
|
||||||
from __future__ import absolute_import, division, print_function
|
from __future__ import absolute_import, division, print_function
|
||||||
|
|
||||||
|
import difflib
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
from collections.abc import MutableMapping
|
||||||
|
|
||||||
from ansible.plugins.callback import CallbackBase
|
from ansible.plugins.callback import CallbackBase
|
||||||
|
|
||||||
|
|
||||||
|
@ -31,7 +34,7 @@ def _raise_error(msg):
|
||||||
raise AnsibleFilterError(error)
|
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.
|
"""Compare two facts or variables and get a diff.
|
||||||
:param before: The first fact to be used in the comparison.
|
:param before: The first fact to be used in the comparison.
|
||||||
:type before: raw
|
:type before: raw
|
||||||
|
@ -40,46 +43,103 @@ def fact_diff(before, after, plugin):
|
||||||
:param plugin: The name of the plugin in collection format
|
:param plugin: The name of the plugin in collection format
|
||||||
:type plugin: string
|
: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
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _check_valid_regexes(skip_lines):
|
class fact_diff_native(CallbackBase):
|
||||||
if skip_lines:
|
def _check_valid_regexes(self, skip_lines):
|
||||||
for idx, regex in enumerate(skip_lines):
|
if skip_lines:
|
||||||
try:
|
for idx, regex in enumerate(skip_lines):
|
||||||
skip_lines[idx] = re.compile(regex)
|
try:
|
||||||
except re.error as exc:
|
skip_lines[idx] = re.compile(regex)
|
||||||
msg = "The regex '{regex}', is not valid. The error was {err}.".format(
|
except re.error as exc:
|
||||||
regex=regex,
|
msg = "The regex '{regex}', is not valid. The error was {err}.".format(
|
||||||
err=str(exc),
|
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 run_diff(self, before, after, plugin, common):
|
||||||
def _xform(before, after, skip_lines):
|
skip_lines = plugin["vars"].get("skip_lines")
|
||||||
if skip_lines:
|
self._check_valid_regexes(skip_lines=skip_lines)
|
||||||
if isinstance(before, str):
|
before, after, skip_lines = self._xform(before, after, skip_lines=skip_lines)
|
||||||
before = before.splitlines()
|
diff = self.get_fact_diff({"before": before, "after": after, "common": common})
|
||||||
if isinstance(after, str):
|
ansi_escape = re.compile(r"\x1B[@-_][0-?]*[ -/]*[@-~]")
|
||||||
after = after.splitlines()
|
diff_text = ansi_escape.sub("", diff)
|
||||||
before = [
|
result = list(diff_text.splitlines())
|
||||||
line for line in before if not any(regex.match(str(line)) for regex in skip_lines)
|
return result
|
||||||
]
|
|
||||||
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
|
|
||||||
|
|
|
@ -20,28 +20,16 @@
|
||||||
msg: "The regex '+', is not valid"
|
msg: "The regex '+', is not valid"
|
||||||
|
|
||||||
- name: Check argspec validation
|
- 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
|
ignore_errors: true
|
||||||
register: result
|
|
||||||
|
|
||||||
- name: Assert
|
- name: Assert
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
that: "{{ msg in result.msg }}"
|
that: "{{ msg in error.msg }}"
|
||||||
vars:
|
vars:
|
||||||
msg: "missing required arguments: after"
|
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
|
- name: Set fact
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
|
@ -69,7 +57,7 @@
|
||||||
before: "{{ before | ansible.utils.to_paths }}"
|
before: "{{ before | ansible.utils.to_paths }}"
|
||||||
after: "{{ after | 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:
|
ansible.builtin.set_fact:
|
||||||
result: "{{ before | ansible.utils.fact_diff(after) }}"
|
result: "{{ before | ansible.utils.fact_diff(after) }}"
|
||||||
|
|
||||||
|
@ -78,6 +66,27 @@
|
||||||
before: "{{ before | to_nice_yaml }}"
|
before: "{{ before | to_nice_yaml }}"
|
||||||
after: "{{ after | 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:
|
ansible.builtin.set_fact:
|
||||||
result: "{{ before | ansible.utils.fact_diff(after) }}"
|
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) }}"
|
||||||
|
|
|
@ -106,3 +106,13 @@ class TestUpdate_Fact(unittest.TestCase):
|
||||||
"missing required arguments: after",
|
"missing required arguments: after",
|
||||||
str(error.exception),
|
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)
|
||||||
|
|
Loading…
Reference in New Issue