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>
|
||||
</td>
|
||||
</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>
|
||||
<td colspan="3">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) }}"
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue