From 3ecc247baae53928481918d1103f132b4d57c5d2 Mon Sep 17 00:00:00 2001 From: Svein Seldal Date: Tue, 3 Dec 2024 19:11:33 +0100 Subject: [PATCH] Fix the ogd diff function (#56) * Improve odg diff mechanism * Add text diff by default, with options for data and internal variants * Fixup bugs and add testing --- setup.cfg | 2 +- src/objdictgen/__main__.py | 66 ++++++--------------- src/objdictgen/jsonod.py | 107 +++++++++++++++++---------------- src/objdictgen/printing.py | 118 ++++++++++++++++++++++++++++++++++++- src/objdictgen/utils.py | 87 ++++++++++++++++++++++++++- tests/od/diff-a.json | 89 ++++++++++++++++++++++++++++ tests/od/diff-b.json | 82 ++++++++++++++++++++++++++ tests/test_jsonod.py | 22 ++++++- tests/test_printing.py | 35 ++++++++++- tests/test_utils.py | 59 +++++++++++++++++++ 10 files changed, 561 insertions(+), 106 deletions(-) create mode 100644 tests/od/diff-a.json create mode 100644 tests/od/diff-b.json diff --git a/setup.cfg b/setup.cfg index 5e77936..5c3e844 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,7 +31,7 @@ python_requires = >=3.10, <4 install_requires = jsonschema colorama - deepdiff<8.0.0 + deepdiff [options.packages.find] where = src diff --git a/src/objdictgen/__main__.py b/src/objdictgen/__main__.py index 259cad6..e041122 100644 --- a/src/objdictgen/__main__.py +++ b/src/objdictgen/__main__.py @@ -23,16 +23,15 @@ import logging import sys from dataclasses import dataclass, field -from pprint import pformat -from typing import TYPE_CHECKING, Callable, Generator, Sequence, TypeVar +from typing import Callable, Sequence, TypeVar from colorama import Fore, Style, init import objdictgen from objdictgen import jsonod from objdictgen.node import Node -from objdictgen.printing import format_node -from objdictgen.typing import TDiffEntries, TDiffNodes, TPath +from objdictgen.printing import format_diff_nodes, format_node +from objdictgen.typing import TPath T = TypeVar('T') @@ -88,38 +87,6 @@ def open_od(fname: TPath|str, validate=True, fix=False) -> "Node": raise -def print_diffs(diffs: TDiffNodes, show=False): - """ Print the differences between two object dictionaries""" - - def _pprint(text: str): - for line in pformat(text).splitlines(): - print(" ", line) - - def _printlines(entries: TDiffEntries): - for chtype, change, path in entries: - if 'removed' in chtype: - print(f"<<< {path} only in LEFT") - if show: - _pprint(change.t1) - elif 'added' in chtype: - print(f" >>> {path} only in RIGHT") - if show: - _pprint(change.t2) - elif 'changed' in chtype: - print(f"<< - >> {path} value changed from '{change.t1}' to '{change.t2}'") - else: - print(f"{Fore.RED}{chtype} {path} {change}{Style.RESET_ALL}") - - rest = diffs.pop('', None) - if rest: - print(f"{Fore.GREEN}Changes:{Style.RESET_ALL}") - _printlines(rest) - - for index in sorted(diffs): - print(f"{Fore.GREEN}Index 0x{index:04x} ({index}){Style.RESET_ALL}") - _printlines(diffs[index]) - - @debug_wrapper() def main(debugopts: DebugOpts, args: Sequence[str]|None = None): """ Main command dispatcher """ @@ -144,6 +111,7 @@ def main(debugopts: DebugOpts, args: Sequence[str]|None = None): opt_debug = dict(action='store_true', help="Debug: enable tracebacks on errors") opt_od = dict(metavar='od', default=None, help="Object dictionary") opt_novalidate = dict(action='store_true', help="Don't validate input files") + opt_nocolor = dict(action='store_true', help="Disable colored output") parser.add_argument('--version', action='version', version='%(prog)s ' + objdictgen.__version__) parser.add_argument('--no-color', action='store_true', help="Disable colored output") @@ -183,11 +151,13 @@ def main(debugopts: DebugOpts, args: Sequence[str]|None = None): """, aliases=['compare']) subp.add_argument('od1', **opt_od) # type: ignore[arg-type] subp.add_argument('od2', **opt_od) # type: ignore[arg-type] + subp.add_argument('--show', action="store_true", help="Show difference data") subp.add_argument('--internal', action="store_true", help="Diff internal object") + subp.add_argument('--data', action="store_true", help="Show difference as data") + subp.add_argument('--raw', action="store_true", help="Show raw difference") + subp.add_argument('--no-color', **opt_nocolor) # type: ignore[arg-type] subp.add_argument('--novalidate', **opt_novalidate) # type: ignore[arg-type] - subp.add_argument('--show', action="store_true", help="Show difference data") subp.add_argument('-D', '--debug', **opt_debug) # type: ignore[arg-type] - subp.add_argument('--no-color', action='store_true', help="Disable colored output") # -- EDIT -- subp = subparser.add_parser('edit', help=""" @@ -210,9 +180,9 @@ def main(debugopts: DebugOpts, args: Sequence[str]|None = None): subp.add_argument('--short', action="store_true", help="Do not list sub-index") subp.add_argument('--unused', action="store_true", help="Include unused profile parameters") subp.add_argument('--internal', action="store_true", help="Show internal data") - subp.add_argument('-D', '--debug', **opt_debug) # type: ignore[arg-type] + subp.add_argument('--no-color', **opt_nocolor) # type: ignore[arg-type] subp.add_argument('--novalidate', **opt_novalidate) # type: ignore[arg-type] - subp.add_argument('--no-color', action='store_true', help="Disable colored output") + subp.add_argument('-D', '--debug', **opt_debug) # type: ignore[arg-type] # -- NETWORK -- subp = subparser.add_parser('network', help=""" @@ -306,22 +276,22 @@ def main(debugopts: DebugOpts, args: Sequence[str]|None = None): # -- DIFF command -- elif opts.command in ("diff", "compare"): + od1 = open_od(opts.od1, validate=not opts.novalidate) od2 = open_od(opts.od2, validate=not opts.novalidate) - diffs = jsonod.diff_nodes( - od1, od2, asdict=not opts.internal, - validate=not opts.novalidate, - ) + lines = list(format_diff_nodes(od1, od2, data=opts.data, raw=opts.raw, + internal=opts.internal, show=opts.show)) + + for line in lines: + print(line) - if diffs: - errcode = 1 + errcode = 1 if lines else 0 + if errcode: print(f"{objdictgen.ODG_PROGRAM}: '{opts.od1}' and '{opts.od2}' differ") else: - errcode = 0 print(f"{objdictgen.ODG_PROGRAM}: '{opts.od1}' and '{opts.od2}' are equal") - print_diffs(diffs, show=opts.show) if errcode: parser.exit(errcode) diff --git a/src/objdictgen/jsonod.py b/src/objdictgen/jsonod.py index aa874a3..07b3dea 100644 --- a/src/objdictgen/jsonod.py +++ b/src/objdictgen/jsonod.py @@ -17,6 +17,8 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA +from __future__ import annotations + import copy import json import logging @@ -36,7 +38,7 @@ from objdictgen.typing import (TDiffNodes, TIndexEntry, TODJson, TODObjJson, TODObj, TODSubObj, TODSubObjJson, TODValue, TParamEntry, TPath, TProfileMenu) from objdictgen.utils import (copy_in_order, exc_amend, maybe_number, - str_to_int) + str_to_int, strip_brackets) T = TypeVar('T') M = TypeVar('M', bound=Mapping) @@ -173,6 +175,10 @@ class ValidationError(Exception): # Copied from https://github.com/NickolaiBeloguzov/jsonc-parser/blob/master/jsonc_parser/parser.py#L11-L39 RE_JSONC = re.compile(r"(\".*?(? str: """ Remove jsonc annotations """ @@ -1394,67 +1400,64 @@ def _validate_dictionary(index, obj): raise -def diff_nodes(node1: "Node", node2: "Node", asdict=True, validate=True) -> TDiffNodes: +def diff(node1: Node, node2: Node, internal=False) -> TDiffNodes: """Compare two nodes and return the differences.""" diffs: dict[int|str, list] = {} - if asdict: - jd1 = node_todict(node1, sort=True, validate=validate) - jd2 = node_todict(node2, sort=True, validate=validate) + if internal: - dt = datetime.isoformat(datetime.now()) - jd1['$date'] = jd2['$date'] = dt + # Simply diff the python data structure for the nodes + diff = deepdiff.DeepDiff(node1.__dict__, node2.__dict__, exclude_paths=[ + "IndexOrder" + ], view='tree') - # DeepDiff does not have typing, but the diff object is a dict-like object - # DeepDiff[str, deepdiff.model.PrettyOrderedSet]. - # PrettyOrderedSet is a list-like object - # PrettyOrderedSet[deepdiff.model.DiffLevel] + else: - diff = deepdiff.DeepDiff(jd1, jd2, exclude_paths=[ - "root['dictionary']" - ], view='tree') + # Don't use rich format for diffing, as it will contain comments which confuse the output + jd1 = node_todict(node1, sort=True, rich=False, internal=True) + jd2 = node_todict(node2, sort=True, rich=False, internal=True) - chtype: str - for chtype, changes in diff.items(): - change: deepdiff.model.DiffLevel - for change in changes: - path: str = change.path(force='fake') # pyright: ignore[reportAssignmentType] - entries = diffs.setdefault('', []) - entries.append((chtype, change, path.replace('root', ''))) - - diff = deepdiff.DeepDiff(jd1['dictionary'], jd2['dictionary'], view='tree', group_by='index') - - res = re.compile(r"root\[('0x[0-9a-fA-F]+'|\d+)\]") - - for chtype, changes in diff.items(): - for change in changes: - path = change.path(force='fake') # pyright: ignore[reportAssignmentType] - m = res.search(path) - if m: - num = str_to_int(m.group(1).strip("'")) - entries = diffs.setdefault(num, []) - entries.append((chtype, change, path.replace(m.group(0), ''))) - else: - entries = diffs.setdefault('', []) - entries.append((chtype, change, path.replace('root', ''))) + # Convert the dictionary list to a dict to ensure the order of the objects + jd1["dictionary"] = {obj["index"]: obj for obj in jd1["dictionary"]} + jd2["dictionary"] = {obj["index"]: obj for obj in jd2["dictionary"]} - else: - diff = deepdiff.DeepDiff(node1.__dict__, node2.__dict__, exclude_paths=[ - "root.IndexOrder" - ], view='tree') + # Diff the two nodes in json object format + diff = deepdiff.DeepDiff(jd1, jd2, view='tree') + + # Iterate over the changes + for chtype, changes in diff.items(): + for change in changes: + path = change.path() + + # Match the root[]... part of the path + m = RE_DIFF_ROOT.match(path) + if not m: + raise ValueError(f"Unexpected path '{path}' in compare") - res = re.compile(r"root\.(Profile|Dictionary|ParamsDictionary|UserMapping|DS302)\[(\d+)\]") + # Path is the display path, root the categorization + path = m[2] + m[3] + root = m[2] + + if not internal: + if m[1] == "root['dictionary']": + # Extract the index from the path + m = RE_DIFF_INDEX.match(path) + root = f"Index {m[1]}" + path = m[2] - for chtype, changes in diff.items(): - for change in changes: - path = change.path(force='fake') # pyright: ignore[reportAssignmentType] - m = res.search(path) - if m: - entries = diffs.setdefault(int(m.group(2)), []) - entries.append((chtype, change, path.replace(m.group(0), m.group(1)))) else: - entries = diffs.setdefault('', []) - entries.append((chtype, change, path.replace('root.', ''))) + root = "Header fields" + + # Append the change to the list of changes + entries = diffs.setdefault(strip_brackets(root), []) + entries.append((chtype, change, strip_brackets(path))) + + # Ensure the Index entries are sorted correctly + def _sort(text): + if text.startswith("Index "): + return f"zz 0x{int(text[6:]):04x}" + return text - return diffs + # Sort the entries + return {k: diffs[k] for k in sorted(diffs, key=_sort)} diff --git a/src/objdictgen/printing.py b/src/objdictgen/printing.py index d24e9b6..10b6a26 100644 --- a/src/objdictgen/printing.py +++ b/src/objdictgen/printing.py @@ -10,8 +10,9 @@ from objdictgen import jsonod, maps from objdictgen.maps import OD from objdictgen.node import Node -from objdictgen.typing import TIndexEntry -from objdictgen.utils import TERM_COLS, str_to_int +from objdictgen.typing import TDiffNodes, TIndexEntry +from objdictgen.utils import (TERM_COLS, diff_colored_lines, highlight_changes, + remove_color, str_to_int) @dataclass @@ -313,3 +314,116 @@ def format_od_object( if not compact and infos: yield "" + +def format_diff_nodes( + od1: Node, od2: Node, *, data=False, raw=False, + internal=False, show=False +) -> Generator[str, None, None]: + """ Compare two object dictionaries and return the formatted differences. """ + + if internal or data: + diffs = jsonod.diff(od1, od2, internal=internal) + else: + diffs = text_diff(od1, od2, data_mode=raw) + + rst = Style.RESET_ALL + + def _pprint(text: str, prefix: str = ' '): + for line in pformat(text, width=TERM_COLS).splitlines(): + yield prefix + line + + for index, entries in diffs.items(): + if data or raw or internal: + yield f"{Fore.LIGHTYELLOW_EX}{index}{rst}" + for chtype, change, path in entries: + + # Prepare the path for printing + ppath = path + if ppath: + if ppath[0] != "'": + ppath = "'" + ppath + "'" + ppath = ppath + ' ' + ppath = f"{Fore.CYAN}{ppath}{rst}" + + if 'removed' in chtype: + yield f"<< {ppath}only in {Fore.MAGENTA}LEFT{rst}" + if show: + yield from _pprint(change.t1, " < ") + + elif 'added' in chtype: + yield f" >> {ppath}only in {Fore.BLUE}RIGHT{rst}" + if show: + yield from _pprint(change.t2, " > ") + + elif 'changed' in chtype: + yield f"<< - >> {ppath}changed value from '{Fore.GREEN}{change.t1}{rst}' to '{Fore.GREEN}{change.t2}{rst}'" + if show: + yield from _pprint(change.t1, " < ") + yield from _pprint(change.t2, " > ") + + elif 'type_changes' in chtype: + yield f"<< - >> {ppath}changed type and value from '{Fore.GREEN}{change.t1}{rst}' to '{Fore.GREEN}{change.t2}{rst}'" + if show: + yield from _pprint(change.t1, " < ") + yield from _pprint(change.t2, " > ") + + elif 'diff' in chtype: + start = path[0:2] + if start == ' ': + ppath = ' ' + path + elif start == '+ ': + ppath = path.replace('+ ', ' >> ') + if ppath == ' >> ': + ppath = '' + elif start == '- ': + ppath = path.replace('- ', '<< ') + if ppath == '<< ': + ppath = '' + elif start == '? ': + ppath = path.replace('? ', ' ') + ppath = f"{Fore.RED}{ppath}{rst}" + else: + ppath = f"{Fore.RED}{chtype} {ppath} {change}{rst}" + yield ppath + else: + yield f"{Fore.RED}{chtype} {ppath} {change}{rst}" + + +def text_diff(od1: Node, od2: Node, data_mode: bool=False) -> TDiffNodes: + """ Compare two object dictionaries as text and return the differences. """ + + # Get all indices for the nodes + keys1 = set(od1.GetAllIndices()) + keys2 = set(od2.GetAllIndices()) + + diffs: dict[int|str, list] = {} + + for index in sorted(keys1 | keys2): + changes = [] + + # Get the object print entries + text1 = text2 = [] + entry1: TIndexEntry = {} + entry2: TIndexEntry = {} + if index in keys1: + text1 = list(format_od_object(od1, index, unused=True)) + entry1 = od1.GetIndexEntry(index) + if index in keys2: + text2 = list(format_od_object(od2, index, unused=True)) + entry2 = od2.GetIndexEntry(index) + + if data_mode: + text1 = text2 = [] + if entry1: + text1 = pformat(entry1, width=TERM_COLS-10, indent=2).splitlines() + if entry2: + text2 = pformat(entry2, width=TERM_COLS-10, indent=2).splitlines() + + if entry1 == entry2: + continue + + for line in highlight_changes(diff_colored_lines(text1, text2)): + changes.append(('diff', '', line)) + diffs[f"Index {index}"] = changes + + return diffs diff --git a/src/objdictgen/utils.py b/src/objdictgen/utils.py index 00b7691..4c42a23 100644 --- a/src/objdictgen/utils.py +++ b/src/objdictgen/utils.py @@ -1,12 +1,16 @@ """ Utility functions for objdictgen """ +import difflib import os -from typing import Mapping, Sequence, TypeVar, cast +import re +from typing import Generator, Iterable, Mapping, Sequence, TypeVar, cast from colorama import Fore, Style T = TypeVar('T') M = TypeVar('M', bound=Mapping) +RE_BRACKETS_STRIP = re.compile(r"^\['(.*?)'\](.*)") + try: TERMINAL = os.get_terminal_size() TERM_COLS = TERMINAL.columns @@ -14,6 +18,23 @@ TERM_COLS = 80 +def remove_color(text: str) -> str: + """ Remove color codes from text """ + for color in Fore.__dict__.values(): + text = text.replace(color, '') + text = text.replace(Style.RESET_ALL, '') + return text + + +def strip_brackets(s: str) -> str: + """ Strip surrounding brackets and quotes from a string """ + # Convert any [''] to + m = RE_BRACKETS_STRIP.match(s) + if m: + return m[1] + m[2] + return s + + def exc_amend(exc: Exception, text: str) -> Exception: """ Helper to prefix text to an exception """ args = list(exc.args) @@ -59,3 +80,67 @@ def copy_in_order(d: M, order: Sequence[T]) -> M: }) return cast(M, out) # FIXME: For mypy + +def diff_colored_lines(lines1: list[str], lines2: list[str]) -> Generator[str, None, None]: + """Diff two lists of lines and return the differences.""" + + nocolor1 = [remove_color(line).rstrip() for line in lines1] + nocolor2 = [remove_color(line).rstrip() for line in lines2] + + for line in difflib.ndiff(nocolor1, nocolor2): + if line.startswith('? '): + continue + try: + # Find the index of the line in the uncolored list + # and replace the line with the colored line + index = nocolor1.index(line[2:]) + line = line[0:2] + lines1[index] + except ValueError: + pass + try: + # Find the index of the line in the uncolored list + # and replace the line with the colored line + index = nocolor2.index(line[2:]) + line = line[0:2] + lines2[index] + except ValueError: + pass + yield line + + +def highlight_changes(lines: Iterable[str]) -> Generator[str, None, None]: + """Highlight changes in a list of lines.""" + + lines = list(lines) + linecount = len(lines) + + delay = None + for i, line in enumerate(lines, start=1): + + yield line + + if delay: + yield delay + delay = None + continue + + if line.startswith('- '): + if i < linecount and lines[i].startswith('+ '): + nextline = lines[i] + + l1 = remove_color(line[2:]).rstrip() + l2 = remove_color(nextline[2:]).rstrip() + + s = difflib.SequenceMatcher(None, l1, l2, autojunk=False) + s1 = ['^'] * len(l1) + s2 = ['^'] * len(l2) + for a, b, n in s.get_matching_blocks(): + for c in range(a, a+n, 1): + s1[c] = ' ' + for c in range(b, b+n, 1): + s2[c] = ' ' + d1 = ''.join(s1).rstrip() + d2 = ''.join(s2).rstrip() + if d1: + yield '? ' + d1 + if d2: + delay = '? ' + d2 diff --git a/tests/od/diff-a.json b/tests/od/diff-a.json new file mode 100644 index 0000000..d8b1a11 --- /dev/null +++ b/tests/od/diff-a.json @@ -0,0 +1,89 @@ +{ + "$id": "od data", + "$version": "1", + "$description": "Canfestival object dictionary data", + "$tool": "odg 3.2", + "$date": "2022-08-08T21:01:45.713961", + "name": "Diff test A", + "description": "", + "type": "master", + "id": 0, + "profile": "None", + "dictionary": [ + { + "index": "0x2000", // 4096 + "name": "Device Type", + "struct": "var", + "group": "user", + "mandatory": true, + "sub": [ + { + "name": "Device Type", + "type": "UNSIGNED32", // 7 + "access": "ro", + "pdo": false, + "value": 0 + } + ] + }, + { + "index": "0x2018", // 4120 + "name": "Identity", + "struct": "record", + "group": "user", + "mandatory": true, + "sub": [ + { + "name": "Number of Entries", + "type": "UNSIGNED8", // 5 + "access": "ro", + "pdo": false + }, + { + "name": "Vendor ID", + "type": "UNSIGNED32", // 7 + "access": "ro", + "pdo": false, + "value": 0 + }, + { + "name": "Product Code", + "type": "UNSIGNED32", // 7 + "access": "ro", + "pdo": false, + "value": 0 + }, + { + "name": "Revision Number", + "type": "UNSIGNED32", // 7 + "access": "ro", + "pdo": false, + "value": 0 + }, + { + "name": "Serial Number", + "type": "UNSIGNED32", // 7 + "access": "ro", + "pdo": false, + "value": 0 + } + ] + }, + { + "index": "0x3000", // 4096 + "name": "Device Type", + "struct": "var", + "group": "user", + "mandatory": true, + "sub": [ + { + "name": "Device Type", + "type": "UNSIGNED32", // 7 + "access": "ro", + "pdo": false, + "value": 0 + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/od/diff-b.json b/tests/od/diff-b.json new file mode 100644 index 0000000..7f86c8a --- /dev/null +++ b/tests/od/diff-b.json @@ -0,0 +1,82 @@ +{ + "$id": "od data", + "$version": "1", + "$description": "Canfestival object dictionary data", + "$tool": "odg 3.2", + "$date": "2022-08-08T21:01:45.713961", + "name": "Diff test B", + "description": "", + "type": "master", + "id": 0, + "profile": "None", + "dictionary": [ + { + "index": "0x2000", // 4096 + "name": "Device Type", + "struct": "var", + "group": "user", + "mandatory": true, + "sub": [ + { + "name": "Device Type", + "type": "UNSIGNED32", // 7 + "access": "ro", + "pdo": false, + "value": 42 + } + ] + }, + { + "index": "0x2018", // 4120 + "name": "Identity", + "struct": "record", + "group": "user", + "mandatory": true, + "sub": [ + { + "name": "Number of Entries", + "type": "UNSIGNED8", // 5 + "access": "ro", + "pdo": false + }, + { + "name": "Vendor ID", + "type": "UNSIGNED32", // 7 + "access": "ro", + "pdo": false, + "value": 0 + }, + { + "name": "Product Code", + "type": "UNSIGNED32", // 7 + "access": "ro", + "pdo": false, + "value": 0 + }, + { + "name": "Revision Number", + "type": "UNSIGNED32", // 7 + "access": "ro", + "pdo": false, + "value": 0 + } + ] + }, + { + "index": "0x3001", // 4096 + "name": "Device Type", + "struct": "var", + "group": "user", + "mandatory": true, + "sub": [ + { + "name": "Device Type", + "type": "UNSIGNED32", // 7 + "access": "ro", + "pdo": false, + "value": 0 + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/test_jsonod.py b/tests/test_jsonod.py index 7c25438..1cffbfc 100644 --- a/tests/test_jsonod.py +++ b/tests/test_jsonod.py @@ -1,9 +1,11 @@ +""" Test the jsonod module. """ import re from pprint import pprint import datetime from freezegun import freeze_time +import pytest from objdictgen import Node -from objdictgen.jsonod import generate_jsonc, generate_node, remove_jsonc +from objdictgen.jsonod import generate_jsonc, generate_node, remove_jsonc, diff from .test_odcompare import shave_equal @@ -160,3 +162,21 @@ def test_jsonod_comments(odpath): if '"$date"' in a or '"$tool"' in a: continue assert a == b + + +@pytest.mark.parametrize("filepair", [ + ("slave-emcy.json", "slave-heartbeat.json"), +]) +def test_jsonod_diff(odpath, filepair): + """ Test the diff function in the jsonod module. """ + + m1 = Node.LoadFile(odpath / filepair[0]) + m2 = Node.LoadFile(odpath / filepair[1]) + + diffs = diff(m1, m2) + + assert set(diffs.keys()) == {"Index 4116", "Index 4119", "Header fields"} + + diffs = diff(m1, m2, internal=True) + + assert set(diffs.keys()) == {"Dictionary", "Description"} diff --git a/tests/test_printing.py b/tests/test_printing.py index 549c893..3ff63a4 100644 --- a/tests/test_printing.py +++ b/tests/test_printing.py @@ -3,7 +3,8 @@ import pytest from objdictgen import __main__ -from objdictgen.printing import FormatNodeOpts, format_node, format_od_header, format_od_object +from objdictgen.printing import (FormatNodeOpts, format_node, format_od_header, + format_od_object, format_diff_nodes) from objdictgen.node import Node @@ -69,3 +70,35 @@ def test_printing_format_od_object(odpath, file): assert isinstance(lines, types.GeneratorType) for line in lines: assert isinstance(line, str) + + +@pytest.mark.parametrize("filepair", [ + ("slave-emcy.json", "slave-heartbeat.json"), + ("diff-a.json", "diff-b.json"), +]) +def test_printing_diff_nodes(odpath, filepair): + + print(filepair) + + m1 = Node.LoadFile(odpath / filepair[0]) + m2 = Node.LoadFile(odpath / filepair[1]) + + lines = format_diff_nodes(m1, m2) + assert isinstance(lines, types.GeneratorType) + for line in lines: + assert isinstance(line, str) + + lines = format_diff_nodes(m1, m2, data=True, show=True) + assert isinstance(lines, types.GeneratorType) + for line in lines: + assert isinstance(line, str) + + lines = format_diff_nodes(m1, m2, raw=True) + assert isinstance(lines, types.GeneratorType) + for line in lines: + assert isinstance(line, str) + + lines = format_diff_nodes(m1, m2, internal=True) + assert isinstance(lines, types.GeneratorType) + for line in lines: + assert isinstance(line, str) diff --git a/tests/test_utils.py b/tests/test_utils.py index 7ef4702..775b0ef 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,7 +1,29 @@ """Test utils module.""" + +from colorama import Fore, Style from objdictgen import utils +def test_utils_remove_color(): + """Test remove_color function.""" + assert utils.remove_color("Hello, World!") == "Hello, World!" + + assert utils.remove_color(Fore.RED + "Hello, World!") == "Hello, World!" + + assert utils.remove_color(Fore.RED + "Hello, World!" + Style.RESET_ALL) == "Hello, World!" + + +def test_utils_strip_brackets(): + """Test strip_brackets function.""" + assert utils.strip_brackets("['Hello']") == "Hello" + + assert utils.strip_brackets("['Hello'] World") == "Hello World" + + assert utils.strip_brackets("['Hello'] World") == "Hello World" + + assert utils.strip_brackets("Hello") == "Hello" + + def test_utils_exc_amend(): """Test exc_amend function.""" @@ -55,3 +77,40 @@ def test_utils_copy_in_order(): assert utils.copy_in_order(d, ["b", "d"]) == {"b": 2, "a": 1, "c": 3} assert utils.copy_in_order(d, []) == d + + +def test_utils_diff_colored_lines(): + """Test diff_colored_lines function.""" + + lines1 = ["Hello", "World"] + lines2 = ["Hello", "World!"] + + out = list(utils.diff_colored_lines(lines1, lines2)) + + assert out == [" Hello", "- World", "+ World!" ] + + + lines1 = [f"{Fore.RED}Hello", f"{Fore.GREEN}World"] + lines2 = ["Hello", "World!"] + + out = list(utils.diff_colored_lines(lines1, lines2)) + + assert out == [f" {Fore.RED}Hello", f"- {Fore.GREEN}World", "+ World!" ] + + + lines1 = ["Hello", "World!"] + lines2 = [f"{Fore.RED}Hello", f"{Fore.GREEN}World"] + + out = list(utils.diff_colored_lines(lines1, lines2)) + + assert out == [f" {Fore.RED}Hello", "- World!", f"+ {Fore.GREEN}World" ] + + +def test_utils_diff_highlight_changes(): + """Test highlight_changes function.""" + + lines = [" Hello", "- World", "+ Friend"] + + out = list(utils.highlight_changes(lines)) + + assert out == [" Hello", "- World", "? ^^ ^", "+ Friend", "? ^ ^^^"]