From e6c8a3a528c07b29c7705e07c7f668f4e63049cd Mon Sep 17 00:00:00 2001 From: DYHARDx Date: Fri, 27 Mar 2026 11:09:08 +0530 Subject: [PATCH] feat: use difflib SequenceMatcher to handle frame offsets in test diffs --- mod_test/nicediff/diff.py | 132 ++++++++++++++++++++------------------ 1 file changed, 68 insertions(+), 64 deletions(-) diff --git a/mod_test/nicediff/diff.py b/mod_test/nicediff/diff.py index b359d499..0b3cfa1c 100644 --- a/mod_test/nicediff/diff.py +++ b/mod_test/nicediff/diff.py @@ -1,3 +1,4 @@ +import difflib import html import re from typing import Any, List, Optional, Tuple, Union @@ -122,11 +123,11 @@ def create_diff_entries(suffix_id: str, id_name: str, events: List[List[object]] def get_html_diff(test_correct_lines: List[str], test_res_lines: List[str], to_view: bool = True) -> str: - """Return generated difference in HTML formatted table.""" + """Return generated difference in HTML formatted table using smart sequence matching.""" # variable to keep count of diff lines noted number_of_noted_diff_lines = 0 - html = """ + html_out = """ @@ -138,81 +139,84 @@ def get_html_diff(test_correct_lines: List[str], test_res_lines: List[str], to_v
""" - res_len = len(test_res_lines) - correct_len = len(test_correct_lines) - - if res_len <= correct_len: - use = res_len - till = correct_len - - else: - use = correct_len - till = res_len - - for line in range(use): + sm = difflib.SequenceMatcher(None, test_res_lines, test_correct_lines) + + for tag, i1, i2, j1, j2 in sm.get_opcodes(): if to_view and number_of_noted_diff_lines >= MAX_NUMBER_OF_LINES_TO_VIEW: break - - if test_correct_lines[line] == test_res_lines[line]: + + if tag == 'equal': continue - html += """ + + if tag == 'replace': + max_len = max(i2 - i1, j2 - j1) + for k in range(max_len): + if to_view and number_of_noted_diff_lines >= MAX_NUMBER_OF_LINES_TO_VIEW: + break + + html_out += """ """ - actual, expected = _process(test_res_lines[line], test_correct_lines[line], suffix_id=str(line)) - - html += f""" + + res_idx = i1 + k + corr_idx = j1 + k + + res_line = test_res_lines[res_idx] if res_idx < i2 else " " + corr_line = test_correct_lines[corr_idx] if corr_idx < j2 else " " + line_display = str(res_idx + 1) if res_idx < i2 else "" + + actual, expected = _process(res_line, corr_line, suffix_id=str(res_idx if res_idx < i2 else corr_idx)) + + html_out += f""" - + """ - html += """ + html_out += """
{line + 1}{line_display} {actual}
{expected}
""" + number_of_noted_diff_lines += 1 - # increase noted diff line by one - number_of_noted_diff_lines += 1 - - # processing remaining lines - for line in range(use, till): - - # stop at 50 lines if test-data for viewing - if to_view and number_of_noted_diff_lines >= MAX_NUMBER_OF_LINES_TO_VIEW: - break - - html += """ + elif tag == 'delete': + for k in range(i1, i2): + if to_view and number_of_noted_diff_lines >= MAX_NUMBER_OF_LINES_TO_VIEW: + break + html_out += """ """ - - if till == res_len: - output, _ = _process(test_res_lines[line], " ", suffix_id=str(line)) - html += f""" - - - - - - - - - """ - else: - _, output = _process(" ", test_correct_lines[line], suffix_id=str(line)) - html += f""" - - - - - - - - - """ - - html += """ + output, _ = _process(test_res_lines[k], " ", suffix_id=str(k)) + html_out += f""" + + + + + + + + """ + html_out += """ +
{line + 1}{output}
{line + 1}
{output}
{k + 1}{output}
""" + number_of_noted_diff_lines += 1 + + elif tag == 'insert': + for k in range(j1, j2): + if to_view and number_of_noted_diff_lines >= MAX_NUMBER_OF_LINES_TO_VIEW: + break + html_out += """ """ + _, output = _process(" ", test_correct_lines[k], suffix_id=str(k)) + html_out += f""" + + + + + + + + """ + html_out += """ +
{output}
""" + number_of_noted_diff_lines += 1 - # increase noted diff line by one - number_of_noted_diff_lines += 1 - - return html + return html_out