diff --git a/CHANGELOG.md b/CHANGELOG.md index a973858..a444c21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 9a72531..f940ab5 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/compare_tool/__init__.py b/compare_tool/__init__.py index 76e27f2..da8606e 100644 --- a/compare_tool/__init__.py +++ b/compare_tool/__init__.py @@ -1,3 +1,3 @@ """CodeGen Compare Tool - AUTOSAR MATLAB codegen diff with noise filtering.""" -__version__ = "1.15.0" +__version__ = "1.16.0" diff --git a/compare_tool/main.py b/compare_tool/main.py index 7db0d5d..50b2163 100644 --- a/compare_tool/main.py +++ b/compare_tool/main.py @@ -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) @@ -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. @@ -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: diff --git a/compare_tool/report.py b/compare_tool/report.py index 28e964d..1519e1f 100644 --- a/compare_tool/report.py +++ b/compare_tool/report.py @@ -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 @@ -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: _component.arxml, _interface.arxml, ... _ARXML_SPLIT_RE = re.compile( r'(.+)_(component|datatypes?|interfaces?|implementation|behavior|timing)$', @@ -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. @@ -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('{} {}'.format(cls, c[key], label)) - if not bits: - noise = c['ignorable-only'] + c['comment-only'] - bits.append('{}' - .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('{}'.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 @@ -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: @@ -1045,23 +1077,37 @@ def chip(a, r, c, label): bits.append('~{}'.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 ' · '.join(c for c in chips if c) + return ' · '.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 = '{}'.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 = '{}'.format( + model_anchors[overview.model], name) rows.append('{}{}{}' .format(name, counts_html, chips or '—')) return ('

Overview

' diff --git a/docs/architecture.md b/docs/architecture.md index 9facbdd..1ac79fe 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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. diff --git a/docs/usage.md b/docs/usage.md index 394ec92..86b8f9b 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -27,11 +27,14 @@ For a terminal summary without generating HTML: python -m compare_tool --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 @@ -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 | diff --git a/docs/vi/README.md b/docs/vi/README.md index 9cb51c9..fb194c2 100644 --- a/docs/vi/README.md +++ b/docs/vi/README.md @@ -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 diff --git a/docs/vi/architecture.md b/docs/vi/architecture.md index 2bd63ae..b5ae7b9 100644 --- a/docs/vi/architecture.md +++ b/docs/vi/architecture.md @@ -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 diff --git a/docs/vi/usage.md b/docs/vi/usage.md index d36e484..8347e47 100644 --- a/docs/vi/usage.md +++ b/docs/vi/usage.md @@ -31,11 +31,13 @@ python -m compare_tool [--report out python -m compare_tool --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. @@ -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 | diff --git a/tests/test_cli_modes.py b/tests/test_cli_modes.py index 1b0b19f..ae15996 100644 --- a/tests/test_cli_modes.py +++ b/tests/test_cli_modes.py @@ -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