Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project are documented here. Versions follow

## [Unreleased]

## [1.14.1] — 2026-09-13

### Changed

- Improve report readability.

## [1.14.0] — 2026-09-11

### Added
Expand Down
2 changes: 1 addition & 1 deletion compare_tool/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""CodeGen Compare Tool - AUTOSAR MATLAB codegen diff with noise filtering."""

__version__ = "1.14.0"
__version__ = "1.14.1"
30 changes: 26 additions & 4 deletions compare_tool/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,11 @@
caption on the diff, not as a diff row. */
.fnhdr { font-family: Consolas, monospace; font-size: 11px; color: var(--fg-muted);
padding: 3px 8px 1px; }
/* groups inside one scope share the first group's caption (see _groups_html).
Which caption comes first depends on the Reviewed badge folding signed-off
groups away: .dup repeats the one above while every group shows, .dup-unrev
while reviewed groups are hidden. */
body:not(.hide-rev) .fnhdr.dup, body.hide-rev .fnhdr.dup-unrev { display: none; }
tr.mvnote td { text-align: center; color: var(--mv-fg); background: var(--panel-2);
font-size: 11px; }
/* Unimportant rows hide per ROW, not per group: a group used to be wrapped
Expand Down Expand Up @@ -661,6 +666,9 @@ def _groups_html(old_lines, new_lines, hunks, notes=None, language=None,
funcname.enclosing(new_lines, language))
out = []
rows_so_far = 0
# the scope named by the nearest code group above: once with every group
# on screen, once with signed-off groups folded away by the Reviewed badge
above = above_unrev = None
runs = _focus_runs(hunks)
for idx, (i, j, lean) in enumerate(runs):
# --max-diff-lines caps the diff embedded per file: a regenerate that
Expand All @@ -682,10 +690,24 @@ def _groups_html(old_lines, new_lines, hunks, notes=None, language=None,
cls = ' grp-rev' if done else ''
block = ['<div class="grp{}{}">'.format(' lean' if lean else '', cls)]
# a lean group is a collapsed noise placeholder with no code window, so
# a caption over it would point at rows that are not shown
label = None if lean else funcname.hunk_label(old_labels, new_labels, g[0])
if label:
block.append('<div class="fnhdr">ƒ {}</div>'.format(_esc(label)))
# a caption over it would point at rows that are not shown; naming no
# scope, it does not break a run of groups inside one either
if not lean:
label = funcname.hunk_label(old_labels, new_labels, g[0])
# a caption only where the scope changes, not over every table of
# one function. The Reviewed badge can fold the group carrying it
# away, so a caption that repeats in only one badge state is still
# emitted, marked for the CSS to hide in that state. A signed-off
# group is itself hidden once reviewed groups are, so only the
# every-group-shown state decides its own caption.
dup = label == above
dup_unrev = dup if done else label == above_unrev
if label and not (dup and dup_unrev):
mark = ' dup' if dup else ' dup-unrev' if dup_unrev else ''
block.append('<div class="fnhdr{}">ƒ {}</div>'.format(mark, _esc(label)))
above = label
if not done:
above_unrev = label
block.append(notes_html)
budget = (max_rows - rows_so_far) if max_rows else 0
block.append(_group_table(old_lines, new_lines, g, language, old_states,
Expand Down
9 changes: 4 additions & 5 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,11 +360,10 @@ until someone adds a new kind to one of them.
detach them on the next scan.
- **`funcname.enclosing`** — the scope name for each line (C/C++ function,
Python class/method, AUTOSAR SHORT-NAME, A2L block), one list the report and
the viewer both read. The
report captions each hunk group and lists a file's `Affected` functions from
it; the viewer tracks a "current function" as the pane scrolls. It never
decides a verdict — a wrong name costs a caption, so the heuristics say
`None` rather than guess.
the viewer both read. The report shows it above each change group (once,
when consecutive groups share it); the viewer tracks a "current function" as
the pane scrolls. It never decides a verdict — a wrong name costs a caption,
so the heuristics say `None` rather than guess.
- **`langspec.SPECS`** — one table of each language's comment/string grammar
(what opens a comment, how a string escapes). Two very different surfaces read
it: `syntax.py` colours a comment, and the diff shadow (`langspec.shadow` for
Expand Down
2 changes: 2 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,8 @@ Each real change shows three lines of context on either side of it — not the w

The lines themselves are always in the file — only the screen stays quiet about them. A file whose differences are purely comments doesn't even get a detail section; it just keeps its `≈` mark and `Comment` count in the tree. (If you're curious why the window is kept this tight rather than wider, that's covered in [architecture.md](architecture.md#decisions-worth-knowing-before-you-change-something).)

Each change is labelled with the function it sits in (for ARXML the SHORT-NAME, for A2L the block). When several changes sit in the same function, the name appears once, above the first.

`Focus on changes`, next to the folder tree, narrows the tree down to files that actually changed — identical, comment-only and Unimportant rows drop out, and any folder left holding none of them goes with them. Like the viewer's `Hide identical`, this is purely a view: verdicts and counts underneath are untouched. A `☀ Light` / `☾ Dark` button sits in the top right; both palettes are embedded in the file itself, so switching between them fetches nothing and works fine on a machine with no internet connection at all.

## CI integration
Expand Down
8 changes: 4 additions & 4 deletions docs/vi/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,10 +287,10 @@ khớp nhau hoàn hảo cho tới lúc ai đó thêm một kind mới vào một
làm chúng rớt ra ở lần scan sau.
- **`funcname.enclosing`** — tên scope của mỗi dòng (hàm C/C++, class/method
Python, SHORT-NAME AUTOSAR, block A2L), một list mà cả report và viewer cùng
đọc. Report chú thích mỗi nhóm
hunk và liệt kê danh sách `Affected` của file từ đó; viewer bám theo "hàm hiện
tại" khi pane cuộn. Nó không bao giờ quyết định verdict — tên sai chỉ tốn một
caption — nên heuristic thà trả `None` còn hơn đoán.
đọc. Report hiện tên này phía trên mỗi nhóm thay đổi (chỉ một lần nếu các nhóm
liền nhau cùng scope); viewer bám theo "hàm hiện tại" khi pane cuộn. Nó không
bao giờ quyết định verdict — tên sai chỉ tốn một caption — nên heuristic thà
trả `None` còn hơn đoán.
- **`langspec.SPECS`** — một bảng duy nhất mô tả ngữ pháp comment/string của mỗi
ngôn ngữ (cái gì mở một comment, string escape ra sao). Hai surface rất khác
nhau cùng đọc nó: `syntax.py` tô màu comment, còn shadow của diff
Expand Down
4 changes: 4 additions & 0 deletions docs/vi/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,10 @@ khác biệt *chỉ* là comment thì không có mục chi tiết riêng; nó ch
không rộng hơn:
[architecture.md](architecture.md#những-quyết-định-nên-biết-trước-khi-sửa).)

Mỗi change được ghi tên hàm chứa nó (với ARXML là SHORT-NAME, với A2L là
block). Nhiều change cùng nằm trong một hàm thì tên hàm chỉ hiện một lần, ở trên
change đầu tiên.

`Focus on changes`, cạnh cây thư mục, thu cây lại còn đúng các file thật sự có
thay đổi — dòng identical, comment-only và Unimportant biến mất, thư mục nào
chỉ còn lại toàn những loại đó thì biến theo luôn. Giống `Hide identical` bên
Expand Down
83 changes: 83 additions & 0 deletions tests/test_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,89 @@ def test_report_shows_minor_hunks_in_modified_files(self):
self.assertIn('delm', page) # NoiseDemo.c has comment hunks too


_CAPTION_RE = re.compile(r'<div class="(fnhdr[^"]*)">ƒ ([^<]*)</div>')


def _scoped_c(scopes):
"""C source with one function per ``(name, x, y)``, the ``x`` and ``y``
statements padded far enough apart -- and from the next function -- that
a change to either renders as its own group."""
lines = []
for name, x, y in scopes:
lines += ['void {}(void)'.format(name), '{']
for stmt in ('x = {};'.format(x), 'y = {};'.format(y), None):
# unique filler, so the line matcher has nothing to mis-anchor on
lines += [' pad{} = 0;'.format(len(lines) + k) for k in range(8)]
if stmt:
lines.append(' ' + stmt)
lines += ['}', '']
return '\n'.join(lines)


class TestFunctionCaption(unittest.TestCase):
"""The ``ƒ scope`` caption is printed where the scope changes, not over
every group: consecutive groups inside one function share the first one's.
The Reviewed badge can fold that first group away, so a repeat is marked
per badge state instead of being dropped."""

OLD = _scoped_c([('step_a', 1, 2), ('step_b', 3, 4)])

@staticmethod
def _render(old, new, rel, language, signed_off=()):
r = compare_pair(old, new, rel)
real = [h for h in r['hunks'] if h['kind'] == 'real']
# signed_off: indices into the real hunks
notes = {tuple(real[k]['old_range']) + tuple(real[k]['new_range']):
('ok', True, '') for k in signed_off}
out = _groups_html(old.split('\n'), new.split('\n'), r['hunks'], notes,
language)
return real, out, _CAPTION_RE.findall(out)

def test_groups_inside_one_function_name_it_once(self):
new = _scoped_c([('step_a', 10, 20), ('step_b', 3, 4)])
real, out, caps = self._render(self.OLD, new, 'f.c', 'c')
self.assertEqual(len(real), 2)
self.assertEqual(out.count('<div class="grp'), 2)
self.assertEqual(caps, [('fnhdr', 'step_a')])

def test_the_next_function_gets_its_own_caption(self):
new = _scoped_c([('step_a', 10, 20), ('step_b', 30, 4)])
_real, out, caps = self._render(self.OLD, new, 'f.c', 'c')
self.assertEqual(out.count('<div class="grp'), 3)
self.assertEqual(caps, [('fnhdr', 'step_a'), ('fnhdr', 'step_b')])

def test_a_signed_off_first_group_leaves_the_name_to_the_next(self):
# hiding reviewed groups takes the first caption with them, so the
# second keeps its own -- hidden by the CSS only while every group shows
new = _scoped_c([('step_a', 10, 20), ('step_b', 3, 4)])
_real, out, caps = self._render(self.OLD, new, 'f.c', 'c', signed_off=(0,))
self.assertIn('<div class="grp grp-rev">', out)
self.assertEqual(caps, [('fnhdr', 'step_a'), ('fnhdr dup', 'step_a')])
self.assertIn('body:not(.hide-rev) .fnhdr.dup, '
'body.hide-rev .fnhdr.dup-unrev { display: none; }', _CSS)

def test_a_scope_resumed_below_a_signed_off_one_is_marked_for_that_state(self):
# Pkg / Comp, its nested Comp / Port (signed off), then Comp again.
# With every group shown the third caption follows Port, a different
# scope; with Port folded away it would repeat the first
def pad(tag):
return ['<{}{}/>'.format(tag, k) for k in range(8)]

def arxml(v):
return '\n'.join(
['<AR-PACKAGE>', '<SHORT-NAME>Pkg</SHORT-NAME>', '<COMP>',
'<SHORT-NAME>Comp</SHORT-NAME>', '<A v="{}"/>'.format(v)]
+ pad('P') + ['<PORT>', '<SHORT-NAME>Port</SHORT-NAME>'] + pad('Q')
+ ['<B v="{}"/>'.format(v)] + pad('R') + ['</PORT>'] + pad('S')
+ ['<C v="{}"/>'.format(v), '</COMP>', '</AR-PACKAGE>', ''])

real, _out, caps = self._render(arxml(1), arxml(2), 'f.arxml', 'arxml',
signed_off=(1,))
self.assertEqual(len(real), 3)
self.assertEqual(caps, [('fnhdr', 'Pkg / Comp'), ('fnhdr', 'Comp / Port'),
('fnhdr dup-unrev', 'Pkg / Comp')])


class TestUnimportantToggle(unittest.TestCase):
"""Comment and Unimportant each hide behind their own badge -- per ROW, not
per group -- but ONLY when the hunk stands in a group with no real/moved
Expand Down