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 @@ -3,6 +3,12 @@
All notable changes to this project are documented here. Versions follow
[semantic versioning](https://semver.org/).

## [1.16.0] — 2026-09-15

### Added

- Add the per-model Overview to terminal-only comparisons.

## [1.15.0]

### Added
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,9 @@ python -m compare_tool baseline.zip current.zip --report report.html
python -m compare_tool old_gen_folder new_gen_folder --no-report
```

Prints a folder tree with every file's verdict, AUTOSAR/A2L changes, and the same
consistency warnings as the HTML report. No code diff or HTML file is generated.
Prints the same per-model Overview as the report, a folder tree with every file's
verdict, AUTOSAR/A2L changes, and consistency warnings. No code diff or HTML file
is generated.

### Open the desktop viewer

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.15.0"
__version__ = "1.16.0"
44 changes: 43 additions & 1 deletion compare_tool/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@

from . import __version__, review, serialize, theme, userrules, zipsource
from .diff_engine import RULES
from .report import build_arxml_report, build_report, consistency_advisories
from .report import (build_arxml_report, build_report, consistency_advisories,
model_overview)
from .view_model import SWC_DISPLAY, iface_kind, swc_item
from .scanner import (scan, summarize, summarize_a2l, summarize_ifaces,
summarize_rte, summarize_swcs)
Expand Down Expand Up @@ -132,6 +133,43 @@ def walk(node, prefix):
return lines


def _terminal_overview_lines(results):
"""Aligned per-model Overview using the report's renderer-neutral rows."""
rows = model_overview(results)
if not rows:
return []

def files_text(parts):
return ' '.join('{} {}'.format(count, label)
if count is not None else label
for _status, count, label in parts)

def autosar_text(parts):
out = []
for added, removed, changed, label in parts:
counts = []
if added:
counts.append('+{}'.format(added))
if removed:
counts.append('-{}'.format(removed))
if changed:
counts.append('~{}'.format(changed))
out.append('{} {}'.format('/'.join(counts), label))
return ' | '.join(out) if out else '-'

rendered = [(row.model, files_text(row.file_parts),
autosar_text(row.autosar_changes)) for row in rows]
model_width = max([len('Model / SWC')] + [len(row[0]) for row in rendered])
files_width = max([len('Files')] + [len(row[1]) for row in rendered])
header = ' {:{}} {:{}} {}'.format(
'Model / SWC', model_width, 'Files', files_width, 'AUTOSAR changes')
lines = ['Overview:', header, ' ' + '-' * (len(header) - 2)]
for model, files, changes in rendered:
lines.append(' {:{}} {:{}} {}'.format(
model, model_width, files, files_width, changes))
return lines


def summary_lines(results, counts, tree=False):
"""Scan summary as plain-text lines the CLI prints: counts, uncompared
paths, modified files and the AUTOSAR/A2L semantic rollups.
Expand Down Expand Up @@ -160,6 +198,10 @@ def summary_lines(results, counts, tree=False):
lines.append(' !! {} -- {}'.format(rel, note))
if tree:
lines.append('')
overview = _terminal_overview_lines(results)
if overview:
lines.extend(overview)
lines.append('')
lines.extend(_terminal_tree_lines(results))
modified_files = () if tree else sorted(results.items())
for rel, r in modified_files:
Expand Down
104 changes: 75 additions & 29 deletions compare_tool/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import datetime
import html
import re
from collections import namedtuple
from pathlib import Path

from . import consistency, filepair, funcname, review, syntax, theme
Expand Down Expand Up @@ -881,6 +882,7 @@ def _row(o_no, o_txt, n_no, n_txt, mode, language=None,
# --- grouping by model / SWC (Embedded Coder AUTOSAR naming convention) ---

SHARED_GROUP = 'Shared / other'
OverviewRow = namedtuple('OverviewRow', 'model file_parts autosar_changes')
# modular arxml export: <Model>_component.arxml, <Model>_interface.arxml, ...
_ARXML_SPLIT_RE = re.compile(
r'(.+)_(component|datatypes?|interfaces?|implementation|behavior|timing)$',
Expand Down Expand Up @@ -970,6 +972,29 @@ def _detail_order(rels, results):
key=lambda p: (_DETAIL_ORDER[results[p]['status']], p))


def _group_counts(rels, results):
"""Raw per-verdict counts for one model group."""
counts = {'real-change': 0, 'comment-only': 0, 'ignorable-only': 0,
'added': 0, 'deleted': 0, 'identical': 0, 'error': 0}
for rel in rels:
counts[results[rel]['status']] += 1
return counts


def _file_count_parts(counts):
"""Visible Overview file counts as ``(status, count, label)`` rows."""
parts = []
for key, label in (('error', 'Error'), ('real-change', 'Modified'),
('added', 'Added'), ('deleted', 'Deleted')):
if counts[key]:
parts.append((key, counts[key], label))
if not parts:
noise = counts['ignorable-only'] + counts['comment-only']
parts.append(('identical', None,
'No functional change' if noise else 'Identical'))
return tuple(parts)


def _counts_html(rels, results):
"""Colored per-status count spans for one model group + raw counts.

Expand All @@ -981,26 +1006,23 @@ def _counts_html(rels, results):
say otherwise (see CLAUDE.md, "the record is never the filtered view").
'Identical' is the verdict word the folder tree and the tags already use,
so the two surfaces name the same state the same way."""
c = {'real-change': 0, 'comment-only': 0, 'ignorable-only': 0, 'added': 0,
'deleted': 0, 'identical': 0, 'error': 0}
for rel in rels:
c[results[rel]['status']] += 1
c = _group_counts(rels, results)
return _file_parts_html(_file_count_parts(c)), c


def _file_parts_html(parts):
bits = []
for key, label, cls in (('error', 'Error', 'cnt-err'),
('real-change', 'Modified', 'cnt-real'),
('added', 'Added', 'cnt-add'),
('deleted', 'Deleted', 'cnt-del')):
if c[key]:
bits.append('<span class="cnt {}">{} {}</span>'.format(cls, c[key], label))
if not bits:
noise = c['ignorable-only'] + c['comment-only']
bits.append('<span class="cnt cnt-id">{}</span>'
.format('No functional change' if noise else 'Identical'))
return ''.join(bits), c
classes = {'error': 'cnt-err', 'real-change': 'cnt-real',
'added': 'cnt-add', 'deleted': 'cnt-del',
'identical': 'cnt-id'}
for status, count, label in parts:
text = '{} {}'.format(count, label) if count is not None else label
bits.append('<span class="cnt {}">{}</span>'.format(classes[status], text))
return ''.join(bits)


def _autosar_chips(rels, results):
"""Compact AUTOSAR change rollup for one model group, e.g.
def _autosar_parts(rels, results):
"""Overview rollup as ``(added, removed, changed, label)`` parts, e.g.
'+1 Interface · +2/−1 Port · ~1 Event · +3 RTE · +1 Characteristic'.

A2L is split by object kind (Characteristic / Measurement) rather than a
Expand Down Expand Up @@ -1035,6 +1057,16 @@ def _autosar_chips(rels, results):
for _n, kind in a['removed']:
a2l_rem[kind] = a2l_rem.get(kind, 0) + 1

parts = [(sa, sr, 0, 'SWC'), (ia, ir, 0, 'Interface')]
parts += [tuple(cats[cat.key]) + (cat.noun,) for cat in SWC_DISPLAY]
parts.append((ra, rr, 0, 'RTE'))
parts += [(a2l_add.get(k, 0), a2l_rem.get(k, 0), 0, a2l_kind_label(k))
for k in A2L_KINDS]
return tuple(part for part in parts if any(part[:3]))


def _autosar_parts_html(parts):
"""Render structured AUTOSAR Overview parts as colored HTML chips."""
def chip(a, r, c, label):
bits = []
if a:
Expand All @@ -1045,23 +1077,37 @@ def chip(a, r, c, label):
bits.append('<span class="a-chg">~{}</span>'.format(c))
return '{} {}'.format('/'.join(bits), label) if bits else ''

chips = [chip(sa, sr, 0, 'SWC'), chip(ia, ir, 0, 'Interface')]
chips += [chip(*(cats[cat.key] + [cat.noun])) for cat in SWC_DISPLAY]
chips.append(chip(ra, rr, 0, 'RTE'))
chips += [chip(a2l_add.get(k, 0), a2l_rem.get(k, 0), 0, a2l_kind_label(k))
for k in A2L_KINDS]
return ' &middot; '.join(c for c in chips if c)
return ' &middot; '.join(chip(*part) for part in parts)


def _autosar_chips(rels, results):
"""HTML AUTOSAR rollup for one model group."""
return _autosar_parts_html(_autosar_parts(rels, results))


def _overview_rows(groups, results):
"""Renderer-neutral rows shared by the HTML and terminal Overview."""
return [OverviewRow(model, _file_count_parts(_group_counts(rels, results)),
_autosar_parts(rels, results))
for model, rels in groups.items()]


def model_overview(results):
"""Renderer-neutral per-model Overview rows, or ``[]`` for a flat tree."""
groups = _model_groups(results)
return _overview_rows(groups, results) if groups else []


def _overview_table(groups, results, model_anchors):
"""Executive per-model rollup table shown at the top of the report."""
rows = []
for m, rels in groups.items():
counts_html, _c = _counts_html(rels, results)
chips = _autosar_chips(rels, results)
name = _esc(m)
if m in model_anchors:
name = '<a onclick="go(\'{}\')">{}</a>'.format(model_anchors[m], name)
for overview in _overview_rows(groups, results):
counts_html = _file_parts_html(overview.file_parts)
chips = _autosar_parts_html(overview.autosar_changes)
name = _esc(overview.model)
if overview.model in model_anchors:
name = '<a onclick="go(\'{}\')">{}</a>'.format(
model_anchors[overview.model], name)
rows.append('<tr><td>{}</td><td>{}</td><td class="aut">{}</td></tr>'
.format(name, counts_html, chips or '&mdash;'))
return ('<h2>Overview</h2><table class="ov">'
Expand Down
5 changes: 4 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,10 @@ write, so the terminal still prints what was found, and the run exits `2`:
immediately after scanning and counting, before any HTML rendering or file
write. `summary_lines(..., tree=True)` prints all scanned paths and reuses the
existing semantic summaries and consistency advisories; it does not fold the
results or print source-code hunks. Old reports are untouched in this mode.
results or print source-code hunks. `report.model_overview` provides structured
rows for both the HTML and terminal Overview, so model ownership, visible file
counts and AUTOSAR rollups stay one decision. Old reports are untouched in this
mode.

The exit code is a contract with somebody's pipeline. `--exit-zero` suppresses
`1`, never `2` — an incomplete compare must never look green.
Expand Down
15 changes: 9 additions & 6 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,14 @@ For a terminal summary without generating HTML:
python -m compare_tool <old_gen_folder> <new_gen_folder> --no-report
```

This prints the counts, a folder tree including **every scanned file**, and the
existing AUTOSAR/A2L summaries: interfaces, SWCs, ports, runnables, events, RTE
access points and calibration objects. Files are labelled `modified`, `identical`,
`added`, `deleted`, `comment-only`, `ignorable-only`, or `error`. There are no code
diffs or hunk details. Folder entries organize the paths; verdicts belong to files.
This prints the counts, the same per-model **Overview** as the HTML report, a
folder tree including **every scanned file**, and the existing AUTOSAR/A2L
summaries: interfaces, SWCs, ports, runnables, events, RTE access points and
calibration objects. The Overview has `Model / SWC`, `Files` and `AUTOSAR changes`
columns and uses the same model grouping and rollup data as the report. Files are
labelled `modified`, `identical`, `added`, `deleted`, `comment-only`,
`ignorable-only`, or `error`. There are no code diffs or hunk details. Folder
entries organize the paths; verdicts belong to files.

The report's consistency advisories are also printed: ARXML/A2L changes without
corresponding generated C changes, and added RTE access while a peer model stayed
Expand All @@ -48,7 +51,7 @@ Either side can be a `.zip` instead of a folder — an Azure DevOps build artifa

| Flag | Meaning |
|---|---|
| `--no-report` | Print a complete file tree, AUTOSAR/A2L summaries and warnings to the terminal without creating HTML or printing code diffs |
| `--no-report` | Print the per-model Overview, complete file tree, AUTOSAR/A2L summaries and warnings to the terminal without creating HTML or printing code diffs |
| `--report out.html` | Report output path (default `compare_report.html`). An existing file there is deleted before the scan starts |
| `--exclude PATTERN` | Skip files matching a glob (relative path or bare file name), repeatable. Example: `--exclude compare_report.html` |
| `--exit-zero` | Always exit 0 even when real changes exist (report-only mode for pipelines). Compare errors still exit 2 |
Expand Down
5 changes: 3 additions & 2 deletions docs/vi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,9 @@ python -m compare_tool baseline.zip current.zip --report report.html
python -m compare_tool old_gen_folder new_gen_folder --no-report
```

In folder tree với verdict của mọi file, AUTOSAR/A2L changes và các cảnh báo
consistency giống HTML report. Không in code diff và không tạo file HTML.
In Overview theo từng model giống report, folder tree với verdict của mọi file,
AUTOSAR/A2L changes và các cảnh báo consistency. Không in code diff và không tạo
file HTML.

### Mở Desktop Viewer

Expand Down
6 changes: 4 additions & 2 deletions docs/vi/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,8 +334,10 @@ giờ dập `2` — một lần compare không trọn vẹn không được phé
`--no-report` truyền `out=None` vào cùng hàm `run_compare`. Hàm trả về sau khi
scan và đếm verdict, trước khi render HTML hay ghi file. `summary_lines(...,
tree=True)` in mọi đường dẫn đã scan, dùng lại summary ngữ nghĩa và consistency
advisory hiện có; không fold kết quả hoặc in code hunk. Report cũ được giữ nguyên
trong chế độ này.
advisory hiện có; không fold kết quả hoặc in code hunk. `report.model_overview`
cấp dữ liệu có cấu trúc cho cả Overview HTML và terminal, nên model ownership,
số file cần hiện và AUTOSAR rollup chỉ được quyết định một chỗ. Report cũ được
giữ nguyên trong chế độ này.

### Viewer

Expand Down
14 changes: 8 additions & 6 deletions docs/vi/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@ python -m compare_tool <thư_mục_gen_cũ> <thư_mục_gen_mới> [--report out
python -m compare_tool <thư_mục_gen_cũ> <thư_mục_gen_mới> --no-report
```

Lệnh in số đếm, folder tree chứa **mọi file đã scan**, và summary AUTOSAR/A2L hiện
có: interface, SWC, port, runnable, event, RTE access point và calibration object.
Mỗi file có nhãn `modified`, `identical`, `added`, `deleted`, `comment-only`,
`ignorable-only` hoặc `error`. Không in code diff hay chi tiết hunk. Các folder
dùng để nhóm đường dẫn; verdict được ghi ở từng file.
Lệnh in số đếm, bảng **Overview** theo từng model giống HTML report, folder tree
chứa **mọi file đã scan**, và summary AUTOSAR/A2L hiện có: interface, SWC, port,
runnable, event, RTE access point và calibration object. Overview có ba cột
`Model / SWC`, `Files`, `AUTOSAR changes`, dùng chung model grouping và dữ liệu
rollup với report. Mỗi file có nhãn `modified`, `identical`, `added`, `deleted`,
`comment-only`, `ignorable-only` hoặc `error`. Không in code diff hay chi tiết
hunk. Các folder dùng để nhóm đường dẫn; verdict được ghi ở từng file.

Các cảnh báo consistency giống report cũng được in: ARXML/A2L thay đổi nhưng
generated C không đổi, hoặc thêm RTE access trong khi model khác vẫn identical.
Expand All @@ -58,7 +60,7 @@ giờ âm thầm rơi xuống so sánh một thư mục rỗng.

| Flag | Ý nghĩa |
|---|---|
| `--no-report` | In đầy đủ file tree, summary AUTOSAR/A2L và cảnh báo trên terminal; không tạo HTML hay in code diff |
| `--no-report` | In Overview theo model, đầy đủ file tree, summary AUTOSAR/A2L và cảnh báo trên terminal; không tạo HTML hay in code diff |
| `--report out.html` | Đường dẫn report (mặc định `compare_report.html`). File cũ ở đó bị xoá trước khi scan bắt đầu |
| `--exclude PATTERN` | Bỏ qua file khớp glob (đường dẫn tương đối hoặc tên file trần), lặp lại được. Ví dụ: `--exclude compare_report.html` |
| `--exit-zero` | Luôn exit 0 kể cả khi có thay đổi thật (chế độ chỉ ghi report cho pipeline). Lỗi compare vẫn exit 2 |
Expand Down
25 changes: 25 additions & 0 deletions tests/test_cli_modes.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,10 +242,35 @@ def test_autosar_and_a2l_summaries_use_the_scan(self):
for heading in ('ARXML interfaces:', 'AUTOSAR behavior:',
'RTE access points:', 'A2L objects:'):
self.assertIn(heading, output)
self.assertIn('Overview:', output)
self.assertIn('Model / SWC', output)
self.assertIn('Files', output)
self.assertIn('AUTOSAR changes', output)
expected_rows = (
'Ctrl 2 Modified +1 Port | ~1 Event | +1 RTE',
'NoiseDemo 4 Modified 1 Deleted '
'+1/-1 Interface | -1 Characteristic | +1 Measurement',
'PedalMap 3 Modified +1 Port | +1 Characteristic',
'SpeedCtrl 1 Added -',
'StaleGen 2 Modified +1 Port | +1 Characteristic',
'TorqueLimiter 2 Modified -',
'Shared / other No functional change -',
)
for row in expected_rows:
self.assertIn(row, output)
self.assertLess(output.index('Overview:'), output.index('Folder tree:'))
for line in summary_lines(results, summarize(results)):
if not line.startswith(' MODIFIED'):
self.assertIn(line, output)

def test_flat_tree_has_no_model_overview(self):
for side in (self.old, self.new):
self._file(side, 'same.c', 'int same;\n')
code, output, _ = self._run()
self.assertEqual(code, 0)
self.assertNotIn('Overview:', output)
self.assertIn('Folder tree:', output)

def test_report_consistency_warnings_remain_visible_with_exit_zero(self):
from compare_tool.report import consistency_advisories
from compare_tool.scanner import scan
Expand Down