From 286ddddd90e245e1e36a30a4b6be33eadaa966be Mon Sep 17 00:00:00 2001 From: longvo920 Date: Sun, 23 Aug 2026 12:35:27 +0700 Subject: [PATCH 1/7] test: lay the fixtures out like a real codegen export The suite scanned four fixture trees whose shape existed only for the tests: files called comment_only.c and rename_only.c, sitting in src/ and rules/ folders no Embedded Coder export ever produces. fixtures/demo already held a byte-identical copy of every one of them, so the same content was maintained twice and a test path told a reader nothing about where the file would really be found. There is now one tree. Each model owns a _autosar_rtw/ folder of generated C, the ARXML export sits under arxml/ and the calibration files under a2l/, so a path in an assertion is a path a reviewer would see. Six models carry the whole matrix between them, and the noise rules live in NoiseDemo rather than in files named after the rule they exercise. Verdicts are unchanged throughout: the old and new sides of each carried-over file kept their exact content, and any rewrite (an #include, a header guard) was applied to both sides so the diff between them could not move. Three files did have to change place, because a realistic layout exposes constraints the flat one hid -- a lone added .c forms a one-file model and falls into Shared, so the added file is now SpeedCtrl_data.c, the deleted one NoiseDemo_types.h, and the two shared ARXMLs became NoiseDemo's modular interface/datatype pair. Two pinned numbers moved with the larger tree (A2L updated 1 -> 3 modified, 8 -> 17 reviewable changes). One test was passing for the wrong reason and is fixed here: test_noise_only_files_not_listed asserted that names like uuid_only.arxml were absent, which stayed true for free once nothing was called that any more. 715 tests pass; ruff clean; the fixture scan still exits 1 for the CI gate. --- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 4 +- tests/fixtures/demo/README.md | 82 +++++---- .../new/{models => Ctrl_autosar_rtw}/Ctrl.c | 0 .../new/{models => Ctrl_autosar_rtw}/Ctrl.h | 0 .../demo/new/Ctrl_autosar_rtw/Ctrl_private.h | 14 ++ .../{models => Ctrl_autosar_rtw}/Ctrl_types.h | 0 .../demo/new/Ctrl_autosar_rtw/Rte_Ctrl.h | 12 ++ .../demo/new/Ctrl_autosar_rtw/rtwtypes.h | 28 +++ .../new/NoiseDemo_autosar_rtw/NoiseDemo.c} | 2 +- .../new/NoiseDemo_autosar_rtw/NoiseDemo.h | 13 ++ .../NoiseDemo_data.c} | 2 +- .../NoiseDemo_autosar_rtw/NoiseDemo_private.h | 6 + .../new/NoiseDemo_autosar_rtw/Rte_NoiseDemo.h | 12 ++ .../new/NoiseDemo_autosar_rtw/ert_main.c} | 4 +- .../demo/new/NoiseDemo_autosar_rtw/rtwtypes.h | 28 +++ .../new/{ => PedalMap_autosar_rtw}/PedalMap.c | 0 .../demo/new/PedalMap_autosar_rtw/PedalMap.h | 22 +++ .../PedalMap_autosar_rtw/PedalMap_private.h | 14 ++ .../new/PedalMap_autosar_rtw/PedalMap_types.h | 14 ++ .../new/PedalMap_autosar_rtw/Rte_PedalMap.h | 12 ++ .../demo/new/PedalMap_autosar_rtw/rtwtypes.h | 28 +++ .../new/SpeedCtrl_autosar_rtw/Rte_SpeedCtrl.h | 12 ++ .../{ => SpeedCtrl_autosar_rtw}/SpeedCtrl.c | 0 .../{ => SpeedCtrl_autosar_rtw}/SpeedCtrl.h | 0 .../SpeedCtrl_autosar_rtw/SpeedCtrl_data.c | 12 ++ .../SpeedCtrl_autosar_rtw/SpeedCtrl_private.h | 14 ++ .../SpeedCtrl_autosar_rtw/SpeedCtrl_types.h | 14 ++ .../demo/new/SpeedCtrl_autosar_rtw/rtwtypes.h | 28 +++ .../new/StaleGen_autosar_rtw/Rte_StaleGen.h | 12 ++ .../new/{ => StaleGen_autosar_rtw}/StaleGen.c | 0 .../demo/new/StaleGen_autosar_rtw/StaleGen.h | 22 +++ .../StaleGen_autosar_rtw/StaleGen_private.h | 14 ++ .../new/StaleGen_autosar_rtw/StaleGen_types.h | 14 ++ .../demo/new/StaleGen_autosar_rtw/rtwtypes.h | 28 +++ .../Rte_TorqueLimiter.h | 12 ++ .../TorqueLimiter.c | 0 .../TorqueLimiter_autosar_rtw/TorqueLimiter.h | 22 +++ .../TorqueLimiter_data.c} | 2 +- .../TorqueLimiter_private.h | 14 ++ .../TorqueLimiter_types.h | 14 ++ .../new/TorqueLimiter_autosar_rtw/rtwtypes.h | 28 +++ .../a2l/comment_only.a2l => a2l/Ctrl.a2l} | 0 .../{rules/a2l/cal.a2l => a2l/NoiseDemo.a2l} | 0 .../fixtures/demo/new/{ => a2l}/PedalMap.a2l | 0 .../fixtures/demo/new/{ => a2l}/StaleGen.a2l | 0 .../{models => arxml}/Ctrl_component.arxml | 0 .../NoiseDemo_component.arxml} | 0 .../NoiseDemo_datatype.arxml} | 0 .../NoiseDemo_implementation.arxml} | 0 .../NoiseDemo_interface.arxml} | 0 .../PedalMap_component.arxml} | 0 .../SpeedCtrl_component.arxml} | 0 .../StaleGen_component.arxml} | 0 .../TorqueLimiter_component.arxml} | 0 tests/fixtures/demo/new/models/rtwtypes.h | 4 - tests/fixtures/demo/new/rules/src/added.c | 6 - .../demo/new/rules/src/comment_only.c | 15 -- .../fixtures/demo/new/rules/src/real_change.c | 12 -- tests/fixtures/demo/new/rules/src/same.h | 6 - .../old/{models => Ctrl_autosar_rtw}/Ctrl.c | 0 .../old/{models => Ctrl_autosar_rtw}/Ctrl.h | 0 .../demo/old/Ctrl_autosar_rtw/Ctrl_private.h | 14 ++ .../{models => Ctrl_autosar_rtw}/Ctrl_types.h | 0 .../demo/old/Ctrl_autosar_rtw/Rte_Ctrl.h | 12 ++ .../demo/old/Ctrl_autosar_rtw/rtwtypes.h | 28 +++ .../old/NoiseDemo_autosar_rtw/NoiseDemo.c} | 2 +- .../old/NoiseDemo_autosar_rtw/NoiseDemo.h | 13 ++ .../NoiseDemo_data.c} | 2 +- .../NoiseDemo_autosar_rtw/NoiseDemo_private.h | 6 + .../NoiseDemo_autosar_rtw/NoiseDemo_types.h | 13 ++ .../old/NoiseDemo_autosar_rtw/Rte_NoiseDemo.h | 12 ++ .../old/NoiseDemo_autosar_rtw/ert_main.c} | 4 +- .../demo/old/NoiseDemo_autosar_rtw/rtwtypes.h | 28 +++ .../old/{ => PedalMap_autosar_rtw}/PedalMap.c | 0 .../demo/old/PedalMap_autosar_rtw/PedalMap.h | 22 +++ .../PedalMap_autosar_rtw/PedalMap_private.h | 14 ++ .../old/PedalMap_autosar_rtw/PedalMap_types.h | 14 ++ .../old/PedalMap_autosar_rtw/Rte_PedalMap.h | 12 ++ .../demo/old/PedalMap_autosar_rtw/rtwtypes.h | 28 +++ .../old/SpeedCtrl_autosar_rtw/Rte_SpeedCtrl.h | 12 ++ .../{ => SpeedCtrl_autosar_rtw}/SpeedCtrl.c | 0 .../{ => SpeedCtrl_autosar_rtw}/SpeedCtrl.h | 0 .../SpeedCtrl_autosar_rtw/SpeedCtrl_private.h | 14 ++ .../SpeedCtrl_autosar_rtw/SpeedCtrl_types.h | 14 ++ .../demo/old/SpeedCtrl_autosar_rtw/rtwtypes.h | 28 +++ .../old/StaleGen_autosar_rtw/Rte_StaleGen.h | 12 ++ .../old/{ => StaleGen_autosar_rtw}/StaleGen.c | 0 .../demo/old/StaleGen_autosar_rtw/StaleGen.h | 22 +++ .../StaleGen_autosar_rtw/StaleGen_private.h | 14 ++ .../old/StaleGen_autosar_rtw/StaleGen_types.h | 14 ++ .../demo/old/StaleGen_autosar_rtw/rtwtypes.h | 28 +++ .../Rte_TorqueLimiter.h | 12 ++ .../TorqueLimiter.c | 0 .../TorqueLimiter_autosar_rtw/TorqueLimiter.h | 22 +++ .../TorqueLimiter_data.c} | 2 +- .../TorqueLimiter_private.h | 14 ++ .../TorqueLimiter_types.h | 14 ++ .../old/TorqueLimiter_autosar_rtw/rtwtypes.h | 28 +++ .../a2l/comment_only.a2l => a2l/Ctrl.a2l} | 0 .../{rules/a2l/cal.a2l => a2l/NoiseDemo.a2l} | 0 .../fixtures/demo/old/{ => a2l}/PedalMap.a2l | 0 .../fixtures/demo/old/{ => a2l}/StaleGen.a2l | 0 .../{models => arxml}/Ctrl_component.arxml | 0 .../NoiseDemo_component.arxml} | 0 .../NoiseDemo_datatype.arxml} | 0 .../NoiseDemo_implementation.arxml} | 0 .../NoiseDemo_interface.arxml} | 0 .../PedalMap_component.arxml} | 0 .../SpeedCtrl_component.arxml} | 0 .../StaleGen_component.arxml} | 0 .../TorqueLimiter_component.arxml} | 0 tests/fixtures/demo/old/models/rtwtypes.h | 4 - .../demo/old/rules/src/comment_only.c | 15 -- tests/fixtures/demo/old/rules/src/deleted.h | 6 - .../fixtures/demo/old/rules/src/real_change.c | 12 -- tests/fixtures/demo/old/rules/src/same.h | 6 - tests/fixtures/model_new/Ctrl.c | 10 -- tests/fixtures/model_new/Ctrl.h | 5 - tests/fixtures/model_new/Ctrl_component.arxml | 45 ----- tests/fixtures/model_new/Ctrl_types.h | 4 - tests/fixtures/model_new/rtwtypes.h | 4 - tests/fixtures/model_old/Ctrl.c | 9 - tests/fixtures/model_old/Ctrl.h | 5 - tests/fixtures/model_old/Ctrl_component.arxml | 41 ----- tests/fixtures/model_old/Ctrl_types.h | 4 - tests/fixtures/model_old/rtwtypes.h | 4 - tests/fixtures/new/a2l/cal.a2l | 12 -- tests/fixtures/new/a2l/comment_only.a2l | 8 - tests/fixtures/new/arxml/admindata.arxml | 16 -- tests/fixtures/new/arxml/iface.arxml | 26 --- tests/fixtures/new/arxml/real_change.arxml | 13 -- tests/fixtures/new/arxml/uuid_only.arxml | 18 -- tests/fixtures/new/src/added.c | 6 - tests/fixtures/new/src/rename_conflict.c | 10 -- tests/fixtures/new/src/rename_only.c | 15 -- tests/fixtures/new/src/same.h | 6 - tests/fixtures/old/a2l/cal.a2l | 13 -- tests/fixtures/old/a2l/comment_only.a2l | 8 - tests/fixtures/old/arxml/admindata.arxml | 16 -- tests/fixtures/old/arxml/iface.arxml | 26 --- tests/fixtures/old/arxml/real_change.arxml | 13 -- tests/fixtures/old/arxml/uuid_only.arxml | 18 -- tests/fixtures/old/src/deleted.h | 6 - tests/fixtures/old/src/rename_conflict.c | 10 -- tests/fixtures/old/src/rename_only.c | 15 -- tests/fixtures/old/src/same.h | 6 - tests/test_demo.py | 91 ++++++---- tests/test_diffpane_qt.py | 163 +++++++++--------- tests/test_engine.py | 65 +++---- tests/test_report.py | 162 +++++++++-------- tests/test_review.py | 19 +- tests/test_summary_model.py | 9 +- 153 files changed, 1270 insertions(+), 752 deletions(-) rename tests/fixtures/demo/new/{models => Ctrl_autosar_rtw}/Ctrl.c (100%) rename tests/fixtures/demo/new/{models => Ctrl_autosar_rtw}/Ctrl.h (100%) create mode 100644 tests/fixtures/demo/new/Ctrl_autosar_rtw/Ctrl_private.h rename tests/fixtures/demo/new/{models => Ctrl_autosar_rtw}/Ctrl_types.h (100%) create mode 100644 tests/fixtures/demo/new/Ctrl_autosar_rtw/Rte_Ctrl.h create mode 100644 tests/fixtures/demo/new/Ctrl_autosar_rtw/rtwtypes.h rename tests/fixtures/{new/src/real_change.c => demo/new/NoiseDemo_autosar_rtw/NoiseDemo.c} (87%) create mode 100644 tests/fixtures/demo/new/NoiseDemo_autosar_rtw/NoiseDemo.h rename tests/fixtures/demo/new/{rules/src/rename_only.c => NoiseDemo_autosar_rtw/NoiseDemo_data.c} (90%) create mode 100644 tests/fixtures/demo/new/NoiseDemo_autosar_rtw/NoiseDemo_private.h create mode 100644 tests/fixtures/demo/new/NoiseDemo_autosar_rtw/Rte_NoiseDemo.h rename tests/fixtures/{new/src/comment_only.c => demo/new/NoiseDemo_autosar_rtw/ert_main.c} (87%) create mode 100644 tests/fixtures/demo/new/NoiseDemo_autosar_rtw/rtwtypes.h rename tests/fixtures/demo/new/{ => PedalMap_autosar_rtw}/PedalMap.c (100%) create mode 100644 tests/fixtures/demo/new/PedalMap_autosar_rtw/PedalMap.h create mode 100644 tests/fixtures/demo/new/PedalMap_autosar_rtw/PedalMap_private.h create mode 100644 tests/fixtures/demo/new/PedalMap_autosar_rtw/PedalMap_types.h create mode 100644 tests/fixtures/demo/new/PedalMap_autosar_rtw/Rte_PedalMap.h create mode 100644 tests/fixtures/demo/new/PedalMap_autosar_rtw/rtwtypes.h create mode 100644 tests/fixtures/demo/new/SpeedCtrl_autosar_rtw/Rte_SpeedCtrl.h rename tests/fixtures/demo/new/{ => SpeedCtrl_autosar_rtw}/SpeedCtrl.c (100%) rename tests/fixtures/demo/new/{ => SpeedCtrl_autosar_rtw}/SpeedCtrl.h (100%) create mode 100644 tests/fixtures/demo/new/SpeedCtrl_autosar_rtw/SpeedCtrl_data.c create mode 100644 tests/fixtures/demo/new/SpeedCtrl_autosar_rtw/SpeedCtrl_private.h create mode 100644 tests/fixtures/demo/new/SpeedCtrl_autosar_rtw/SpeedCtrl_types.h create mode 100644 tests/fixtures/demo/new/SpeedCtrl_autosar_rtw/rtwtypes.h create mode 100644 tests/fixtures/demo/new/StaleGen_autosar_rtw/Rte_StaleGen.h rename tests/fixtures/demo/new/{ => StaleGen_autosar_rtw}/StaleGen.c (100%) create mode 100644 tests/fixtures/demo/new/StaleGen_autosar_rtw/StaleGen.h create mode 100644 tests/fixtures/demo/new/StaleGen_autosar_rtw/StaleGen_private.h create mode 100644 tests/fixtures/demo/new/StaleGen_autosar_rtw/StaleGen_types.h create mode 100644 tests/fixtures/demo/new/StaleGen_autosar_rtw/rtwtypes.h create mode 100644 tests/fixtures/demo/new/TorqueLimiter_autosar_rtw/Rte_TorqueLimiter.h rename tests/fixtures/demo/new/{ => TorqueLimiter_autosar_rtw}/TorqueLimiter.c (100%) create mode 100644 tests/fixtures/demo/new/TorqueLimiter_autosar_rtw/TorqueLimiter.h rename tests/fixtures/demo/new/{rules/src/rename_conflict.c => TorqueLimiter_autosar_rtw/TorqueLimiter_data.c} (80%) create mode 100644 tests/fixtures/demo/new/TorqueLimiter_autosar_rtw/TorqueLimiter_private.h create mode 100644 tests/fixtures/demo/new/TorqueLimiter_autosar_rtw/TorqueLimiter_types.h create mode 100644 tests/fixtures/demo/new/TorqueLimiter_autosar_rtw/rtwtypes.h rename tests/fixtures/demo/new/{rules/a2l/comment_only.a2l => a2l/Ctrl.a2l} (100%) rename tests/fixtures/demo/new/{rules/a2l/cal.a2l => a2l/NoiseDemo.a2l} (100%) rename tests/fixtures/demo/new/{ => a2l}/PedalMap.a2l (100%) rename tests/fixtures/demo/new/{ => a2l}/StaleGen.a2l (100%) rename tests/fixtures/demo/new/{models => arxml}/Ctrl_component.arxml (100%) rename tests/fixtures/demo/new/{rules/arxml/uuid_only.arxml => arxml/NoiseDemo_component.arxml} (100%) rename tests/fixtures/demo/new/{rules/arxml/real_change.arxml => arxml/NoiseDemo_datatype.arxml} (100%) rename tests/fixtures/demo/new/{rules/arxml/admindata.arxml => arxml/NoiseDemo_implementation.arxml} (100%) rename tests/fixtures/demo/new/{rules/arxml/iface.arxml => arxml/NoiseDemo_interface.arxml} (100%) rename tests/fixtures/demo/new/{PedalMap.arxml => arxml/PedalMap_component.arxml} (100%) rename tests/fixtures/demo/new/{SpeedCtrl.arxml => arxml/SpeedCtrl_component.arxml} (100%) rename tests/fixtures/demo/new/{StaleGen.arxml => arxml/StaleGen_component.arxml} (100%) rename tests/fixtures/demo/new/{TorqueLimiter.arxml => arxml/TorqueLimiter_component.arxml} (100%) delete mode 100644 tests/fixtures/demo/new/models/rtwtypes.h delete mode 100644 tests/fixtures/demo/new/rules/src/added.c delete mode 100644 tests/fixtures/demo/new/rules/src/comment_only.c delete mode 100644 tests/fixtures/demo/new/rules/src/real_change.c delete mode 100644 tests/fixtures/demo/new/rules/src/same.h rename tests/fixtures/demo/old/{models => Ctrl_autosar_rtw}/Ctrl.c (100%) rename tests/fixtures/demo/old/{models => Ctrl_autosar_rtw}/Ctrl.h (100%) create mode 100644 tests/fixtures/demo/old/Ctrl_autosar_rtw/Ctrl_private.h rename tests/fixtures/demo/old/{models => Ctrl_autosar_rtw}/Ctrl_types.h (100%) create mode 100644 tests/fixtures/demo/old/Ctrl_autosar_rtw/Rte_Ctrl.h create mode 100644 tests/fixtures/demo/old/Ctrl_autosar_rtw/rtwtypes.h rename tests/fixtures/{old/src/real_change.c => demo/old/NoiseDemo_autosar_rtw/NoiseDemo.c} (87%) create mode 100644 tests/fixtures/demo/old/NoiseDemo_autosar_rtw/NoiseDemo.h rename tests/fixtures/demo/old/{rules/src/rename_only.c => NoiseDemo_autosar_rtw/NoiseDemo_data.c} (89%) create mode 100644 tests/fixtures/demo/old/NoiseDemo_autosar_rtw/NoiseDemo_private.h create mode 100644 tests/fixtures/demo/old/NoiseDemo_autosar_rtw/NoiseDemo_types.h create mode 100644 tests/fixtures/demo/old/NoiseDemo_autosar_rtw/Rte_NoiseDemo.h rename tests/fixtures/{old/src/comment_only.c => demo/old/NoiseDemo_autosar_rtw/ert_main.c} (86%) create mode 100644 tests/fixtures/demo/old/NoiseDemo_autosar_rtw/rtwtypes.h rename tests/fixtures/demo/old/{ => PedalMap_autosar_rtw}/PedalMap.c (100%) create mode 100644 tests/fixtures/demo/old/PedalMap_autosar_rtw/PedalMap.h create mode 100644 tests/fixtures/demo/old/PedalMap_autosar_rtw/PedalMap_private.h create mode 100644 tests/fixtures/demo/old/PedalMap_autosar_rtw/PedalMap_types.h create mode 100644 tests/fixtures/demo/old/PedalMap_autosar_rtw/Rte_PedalMap.h create mode 100644 tests/fixtures/demo/old/PedalMap_autosar_rtw/rtwtypes.h create mode 100644 tests/fixtures/demo/old/SpeedCtrl_autosar_rtw/Rte_SpeedCtrl.h rename tests/fixtures/demo/old/{ => SpeedCtrl_autosar_rtw}/SpeedCtrl.c (100%) rename tests/fixtures/demo/old/{ => SpeedCtrl_autosar_rtw}/SpeedCtrl.h (100%) create mode 100644 tests/fixtures/demo/old/SpeedCtrl_autosar_rtw/SpeedCtrl_private.h create mode 100644 tests/fixtures/demo/old/SpeedCtrl_autosar_rtw/SpeedCtrl_types.h create mode 100644 tests/fixtures/demo/old/SpeedCtrl_autosar_rtw/rtwtypes.h create mode 100644 tests/fixtures/demo/old/StaleGen_autosar_rtw/Rte_StaleGen.h rename tests/fixtures/demo/old/{ => StaleGen_autosar_rtw}/StaleGen.c (100%) create mode 100644 tests/fixtures/demo/old/StaleGen_autosar_rtw/StaleGen.h create mode 100644 tests/fixtures/demo/old/StaleGen_autosar_rtw/StaleGen_private.h create mode 100644 tests/fixtures/demo/old/StaleGen_autosar_rtw/StaleGen_types.h create mode 100644 tests/fixtures/demo/old/StaleGen_autosar_rtw/rtwtypes.h create mode 100644 tests/fixtures/demo/old/TorqueLimiter_autosar_rtw/Rte_TorqueLimiter.h rename tests/fixtures/demo/old/{ => TorqueLimiter_autosar_rtw}/TorqueLimiter.c (100%) create mode 100644 tests/fixtures/demo/old/TorqueLimiter_autosar_rtw/TorqueLimiter.h rename tests/fixtures/demo/old/{rules/src/rename_conflict.c => TorqueLimiter_autosar_rtw/TorqueLimiter_data.c} (80%) create mode 100644 tests/fixtures/demo/old/TorqueLimiter_autosar_rtw/TorqueLimiter_private.h create mode 100644 tests/fixtures/demo/old/TorqueLimiter_autosar_rtw/TorqueLimiter_types.h create mode 100644 tests/fixtures/demo/old/TorqueLimiter_autosar_rtw/rtwtypes.h rename tests/fixtures/demo/old/{rules/a2l/comment_only.a2l => a2l/Ctrl.a2l} (100%) rename tests/fixtures/demo/old/{rules/a2l/cal.a2l => a2l/NoiseDemo.a2l} (100%) rename tests/fixtures/demo/old/{ => a2l}/PedalMap.a2l (100%) rename tests/fixtures/demo/old/{ => a2l}/StaleGen.a2l (100%) rename tests/fixtures/demo/old/{models => arxml}/Ctrl_component.arxml (100%) rename tests/fixtures/demo/old/{rules/arxml/uuid_only.arxml => arxml/NoiseDemo_component.arxml} (100%) rename tests/fixtures/demo/old/{rules/arxml/real_change.arxml => arxml/NoiseDemo_datatype.arxml} (100%) rename tests/fixtures/demo/old/{rules/arxml/admindata.arxml => arxml/NoiseDemo_implementation.arxml} (100%) rename tests/fixtures/demo/old/{rules/arxml/iface.arxml => arxml/NoiseDemo_interface.arxml} (100%) rename tests/fixtures/demo/old/{PedalMap.arxml => arxml/PedalMap_component.arxml} (100%) rename tests/fixtures/demo/old/{SpeedCtrl.arxml => arxml/SpeedCtrl_component.arxml} (100%) rename tests/fixtures/demo/old/{StaleGen.arxml => arxml/StaleGen_component.arxml} (100%) rename tests/fixtures/demo/old/{TorqueLimiter.arxml => arxml/TorqueLimiter_component.arxml} (100%) delete mode 100644 tests/fixtures/demo/old/models/rtwtypes.h delete mode 100644 tests/fixtures/demo/old/rules/src/comment_only.c delete mode 100644 tests/fixtures/demo/old/rules/src/deleted.h delete mode 100644 tests/fixtures/demo/old/rules/src/real_change.c delete mode 100644 tests/fixtures/demo/old/rules/src/same.h delete mode 100644 tests/fixtures/model_new/Ctrl.c delete mode 100644 tests/fixtures/model_new/Ctrl.h delete mode 100644 tests/fixtures/model_new/Ctrl_component.arxml delete mode 100644 tests/fixtures/model_new/Ctrl_types.h delete mode 100644 tests/fixtures/model_new/rtwtypes.h delete mode 100644 tests/fixtures/model_old/Ctrl.c delete mode 100644 tests/fixtures/model_old/Ctrl.h delete mode 100644 tests/fixtures/model_old/Ctrl_component.arxml delete mode 100644 tests/fixtures/model_old/Ctrl_types.h delete mode 100644 tests/fixtures/model_old/rtwtypes.h delete mode 100644 tests/fixtures/new/a2l/cal.a2l delete mode 100644 tests/fixtures/new/a2l/comment_only.a2l delete mode 100644 tests/fixtures/new/arxml/admindata.arxml delete mode 100644 tests/fixtures/new/arxml/iface.arxml delete mode 100644 tests/fixtures/new/arxml/real_change.arxml delete mode 100644 tests/fixtures/new/arxml/uuid_only.arxml delete mode 100644 tests/fixtures/new/src/added.c delete mode 100644 tests/fixtures/new/src/rename_conflict.c delete mode 100644 tests/fixtures/new/src/rename_only.c delete mode 100644 tests/fixtures/new/src/same.h delete mode 100644 tests/fixtures/old/a2l/cal.a2l delete mode 100644 tests/fixtures/old/a2l/comment_only.a2l delete mode 100644 tests/fixtures/old/arxml/admindata.arxml delete mode 100644 tests/fixtures/old/arxml/iface.arxml delete mode 100644 tests/fixtures/old/arxml/real_change.arxml delete mode 100644 tests/fixtures/old/arxml/uuid_only.arxml delete mode 100644 tests/fixtures/old/src/deleted.h delete mode 100644 tests/fixtures/old/src/rename_conflict.c delete mode 100644 tests/fixtures/old/src/rename_only.c delete mode 100644 tests/fixtures/old/src/same.h diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 575e93c..aa4f1f9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -94,7 +94,7 @@ jobs: run: | for tool in "dist/compare-tool.exe" "python dist/compare_tool.pyz"; do set +e - $tool tests/fixtures/old tests/fixtures/new --report out.html + $tool tests/fixtures/demo/old tests/fixtures/demo/new --report out.html code=$? set -e test "$code" -eq 1 || { echo "::error::$tool exited $code, expected 1"; exit 1; } diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b7586e6..fe1b193 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,7 +43,7 @@ jobs: shell: bash run: | set +e - python dist/compare_tool.pyz tests/fixtures/old tests/fixtures/new --report pyz.html + python dist/compare_tool.pyz tests/fixtures/demo/old tests/fixtures/demo/new --report pyz.html code=$? set -e test "$code" -eq 1 # fixtures hold real changes @@ -104,7 +104,7 @@ jobs: shell: bash run: | set +e - python -m compare_tool tests/fixtures/old tests/fixtures/new --report report.html + python -m compare_tool tests/fixtures/demo/old tests/fixtures/demo/new --report report.html code=$? set -e test "$code" -eq 1 # fixtures contain real changes -> exit code 1 expected diff --git a/tests/fixtures/demo/README.md b/tests/fixtures/demo/README.md index feb8bfb..d261fd8 100644 --- a/tests/fixtures/demo/README.md +++ b/tests/fixtures/demo/README.md @@ -1,47 +1,67 @@ # Demo tree -One before/after pair, one compare, that shows the whole tool: every noise rule -plus the three newest features. Run it and look at the report and the terminal: +One before/after pair that shows the whole tool in a single compare, laid out +the way an Embedded Coder AUTOSAR export actually lands on disk: each model owns +a `_autosar_rtw/` folder of generated C, the ARXML export sits under +`arxml/`, and the calibration files under `a2l/`. + +This is also the suite's only fixture pair — every test scans this tree, so a +path in an assertion is a path a reviewer would really see. ```bash python -m compare_tool tests/fixtures/demo/old tests/fixtures/demo/new \ --report demo.html --json demo.json --sarif demo.sarif ``` -Four top-level models make the newest features' point. `rules/` and `models/` -are the tool's own noise-rule and model-grouping fixtures folded in beside -them, so the same run also shows comment/uuid/timestamp/rename noise, an added -file, a deleted file, and Modified files sitting right next to what does not -count — everything a reviewer would otherwise need several compares to see. +``` +old/ + Ctrl_autosar_rtw/ Ctrl.c Ctrl.h Ctrl_private.h Ctrl_types.h + Rte_Ctrl.h rtwtypes.h + NoiseDemo_autosar_rtw/ NoiseDemo.c NoiseDemo_data.c ert_main.c … + PedalMap_autosar_rtw/ + SpeedCtrl_autosar_rtw/ + StaleGen_autosar_rtw/ + TorqueLimiter_autosar_rtw/ + arxml/ one _component.arxml per model, plus NoiseDemo's + modular _interface / _datatype / _implementation + a2l/ Ctrl.a2l NoiseDemo.a2l PedalMap.a2l StaleGen.a2l +``` -Four models, each making one point: +Grouping keys off the **file stem**, not the folder, so a model's C, its ARXML +under `arxml/` and its A2L under `a2l/` still land in one group in the Overview. -| Model | Files | What it shows | -|---|---|---| -| **SpeedCtrl** | `.c` `.h` `.arxml` | **Reordered statements are noise.** `SpeedCtrl.c` emits the same three independent gains in a different order (and a new timestamp). It is filed under **Unimportant**, not Modified — the values are identical, and the tool proves it before hiding it. | -| **StaleGen** | `.c` `.arxml` `.a2l` | **Cross-artifact consistency.** The ARXML gained a port and the A2L gained a characteristic, but the C is byte-for-byte unchanged — the interface and calibration moved without the code. The report and the terminal flag *"ARXML and A2L changed but the generated C did not"*, the usual sign of a stale regenerate. | -| **TorqueLimiter** | `.c` `.arxml` | **A code-only change is normal.** The C changed (a gain went 1.25 → 1.45) while the ARXML did not. A logic edit touches no interface, so this is **not** flagged — the check only fires when a surface changed without the code following. | -| **PedalMap** | `.c` `.arxml` `.a2l` | **The healthy case, plus machine output.** The C, the ARXML (a new `Scaled` port) and the A2L (a new `K_PedalOffset`) all changed together, so no flag — and the AUTOSAR summary lists the new port and characteristic. | +## Six models, each making one point -`rules/` (noise coverage, one file per rule) and `models/` (the `Ctrl` model, -for the model-grouping / Overview table): +| Model | What it shows | +|---|---| +| **SpeedCtrl** | **Reordered statements are noise.** `SpeedCtrl.c` emits the same three independent gains in a different order (and a new timestamp), so it is filed under **Unimportant**, not Modified — the values are identical, and the tool proves it before hiding it. Its `SpeedCtrl_data.c` is **Added**: a file the regenerate started emitting. | +| **StaleGen** | **Cross-artifact consistency.** The ARXML gained a port and the A2L gained a characteristic, but `StaleGen.c` is byte-for-byte unchanged — the interface and calibration moved without the code. The report and the terminal flag *"ARXML and A2L changed but the generated C did not"*, the usual sign of a stale regenerate. | +| **TorqueLimiter** | **A code-only change is normal.** `TorqueLimiter.c` changed (a gain went 1.25 → 1.45) while its ARXML did not, so it is **not** flagged — the check only fires when a surface changed without the code following. Its `TorqueLimiter_data.c` carries a rename the mapping cannot fully explain, which therefore stays **Modified**. | +| **PedalMap** | **The healthy case, plus machine output.** The C, the ARXML (a new `Scaled` port) and the A2L (a new `K_PedalOffset`) all changed together, so no flag — and the AUTOSAR summary lists the new port and characteristic. | +| **Ctrl** | **The +RTE quick-regen advisory.** `Ctrl.c` gains an `Rte_Write_Out2_Diag` while StaleGen's C stays identical, and its `TIMING-EVENT` period goes `0.01s → 0.02s`. Flagged *"gained an RTE access while a peer model stayed identical"*. | +| **NoiseDemo** | **Every ignorable kind in one model**, so the noise rules can be read side by side with what is real. | -| Path | What it shows | +## Where each noise rule lives + +| Path | Verdict | |---|---| -| `rules/src/comment_only.c` | **Comment** — banner/comment churn only | -| `rules/src/rename_only.c` | **Unimportant** — a consistent 1-to-1 identifier rename | -| `rules/src/real_change.c` | **Modified** — a real change beside a comment change | -| `rules/src/added.c` / `deleted.h` | **Added** / **Deleted** | -| `rules/arxml/uuid_only.arxml` | **Unimportant** — `UUID="…"` churn only | -| `rules/arxml/admindata.arxml` | **Unimportant** — `` timestamp churn | -| `rules/arxml/iface.arxml` | **Modified** — a port-interface change, alongside a UUID bump | -| `rules/a2l/comment_only.a2l` | **Comment** | -| `rules/a2l/cal.a2l` | **Modified** — a calibration object change | -| `models/Ctrl.*` | a second model (Overview grouping); its `.c` also gains an `Rte_Write_Out2_Diag` access while StaleGen's C stays identical, so it triggers the **+RTE quick-regen** advisory — *"gained an RTE access while a peer model stayed identical"* | - -What the outputs carry: - -- **`demo.html`** — the human report: a *Consistency check* section (below the AUTOSAR changes) names StaleGen (surfaces changed, C did not) and Ctrl (+RTE while a peer stayed identical), `SpeedCtrl.c` sits under Unimportant with its rows greyed until you click, and the folder tree/Overview show every verdict at once. +| `NoiseDemo_autosar_rtw/ert_main.c` | **Comment** — banner churn only | +| `NoiseDemo_autosar_rtw/NoiseDemo_data.c` | **Unimportant** — a consistent 1-to-1 identifier rename | +| `NoiseDemo_autosar_rtw/NoiseDemo.c` | **Modified** — a real change beside a comment change | +| `NoiseDemo_autosar_rtw/NoiseDemo_types.h` | **Deleted** | +| `SpeedCtrl_autosar_rtw/SpeedCtrl_data.c` | **Added** | +| `TorqueLimiter_autosar_rtw/TorqueLimiter_data.c` | **Modified** — a rename the mapping cannot explain stays real | +| `arxml/NoiseDemo_component.arxml` | **Unimportant** — `UUID="…"` churn only | +| `arxml/NoiseDemo_implementation.arxml` | **Unimportant** — `` timestamp churn | +| `arxml/NoiseDemo_interface.arxml` | **Modified** — a port-interface change, alongside a UUID bump | +| `arxml/NoiseDemo_datatype.arxml` | **Modified** — a data type renamed | +| `a2l/Ctrl.a2l` | **Comment** | +| `a2l/NoiseDemo.a2l` | **Modified** — a calibration object change | +| every `rtwtypes.h`, `*_private.h`, `Rte_*.h` | **Identical** — the bulk a real compare is made of | + +## What the outputs carry + +- **`demo.html`** — the human report: a *Consistency check* section (below the AUTOSAR changes) names StaleGen (surfaces changed, C did not) and Ctrl (+RTE while a peer stayed identical), `SpeedCtrl.c` sits under Unimportant with its rows greyed until you click, and the folder tree / Overview show every verdict at once. - **`demo.json`** — the whole scan under a versioned schema, including the same exit code the process returns. - **`demo.sarif`** — only the files that need action. Unimportant, Comment and identical files are *not* findings. diff --git a/tests/fixtures/demo/new/models/Ctrl.c b/tests/fixtures/demo/new/Ctrl_autosar_rtw/Ctrl.c similarity index 100% rename from tests/fixtures/demo/new/models/Ctrl.c rename to tests/fixtures/demo/new/Ctrl_autosar_rtw/Ctrl.c diff --git a/tests/fixtures/demo/new/models/Ctrl.h b/tests/fixtures/demo/new/Ctrl_autosar_rtw/Ctrl.h similarity index 100% rename from tests/fixtures/demo/new/models/Ctrl.h rename to tests/fixtures/demo/new/Ctrl_autosar_rtw/Ctrl.h diff --git a/tests/fixtures/demo/new/Ctrl_autosar_rtw/Ctrl_private.h b/tests/fixtures/demo/new/Ctrl_autosar_rtw/Ctrl_private.h new file mode 100644 index 0000000..2c5fae1 --- /dev/null +++ b/tests/fixtures/demo/new/Ctrl_autosar_rtw/Ctrl_private.h @@ -0,0 +1,14 @@ +/* + * File: Ctrl_private.h + * + * Code generated for Simulink model 'Ctrl'. + */ + +#ifndef RTW_HEADER_Ctrl_private_h_ +#define RTW_HEADER_Ctrl_private_h_ + +#include "rtwtypes.h" + +extern void Ctrl_initialize(void); + +#endif /* RTW_HEADER_Ctrl_private_h_ */ diff --git a/tests/fixtures/demo/new/models/Ctrl_types.h b/tests/fixtures/demo/new/Ctrl_autosar_rtw/Ctrl_types.h similarity index 100% rename from tests/fixtures/demo/new/models/Ctrl_types.h rename to tests/fixtures/demo/new/Ctrl_autosar_rtw/Ctrl_types.h diff --git a/tests/fixtures/demo/new/Ctrl_autosar_rtw/Rte_Ctrl.h b/tests/fixtures/demo/new/Ctrl_autosar_rtw/Rte_Ctrl.h new file mode 100644 index 0000000..153c28b --- /dev/null +++ b/tests/fixtures/demo/new/Ctrl_autosar_rtw/Rte_Ctrl.h @@ -0,0 +1,12 @@ +/* + * File: Rte_Ctrl.h + * + * RTE header generated for AUTOSAR software component 'Ctrl'. + */ + +#ifndef RTE_CTRL_H +#define RTE_CTRL_H + +#include "Rte_Type.h" + +#endif /* RTE_CTRL_H */ diff --git a/tests/fixtures/demo/new/Ctrl_autosar_rtw/rtwtypes.h b/tests/fixtures/demo/new/Ctrl_autosar_rtw/rtwtypes.h new file mode 100644 index 0000000..381ed05 --- /dev/null +++ b/tests/fixtures/demo/new/Ctrl_autosar_rtw/rtwtypes.h @@ -0,0 +1,28 @@ +/* + * rtwtypes.h + * + * Code generation for model shared utilities. + */ + +#ifndef RTWTYPES_H +#define RTWTYPES_H + +typedef signed char int8_T; +typedef unsigned char uint8_T; +typedef short int16_T; +typedef unsigned short uint16_T; +typedef int int32_T; +typedef unsigned int uint32_T; +typedef float real32_T; +typedef double real_T; +typedef unsigned char boolean_T; + +#ifndef TRUE +#define TRUE (1U) +#endif + +#ifndef FALSE +#define FALSE (0U) +#endif + +#endif /* RTWTYPES_H */ diff --git a/tests/fixtures/new/src/real_change.c b/tests/fixtures/demo/new/NoiseDemo_autosar_rtw/NoiseDemo.c similarity index 87% rename from tests/fixtures/new/src/real_change.c rename to tests/fixtures/demo/new/NoiseDemo_autosar_rtw/NoiseDemo.c index bc6bd34..c4286ec 100644 --- a/tests/fixtures/new/src/real_change.c +++ b/tests/fixtures/demo/new/NoiseDemo_autosar_rtw/NoiseDemo.c @@ -1,5 +1,5 @@ /* Generated on : Tue Feb 17 08:45:01 2026 */ -#include "real_change.h" +#include "NoiseDemo.h" void Calc_step(void) { diff --git a/tests/fixtures/demo/new/NoiseDemo_autosar_rtw/NoiseDemo.h b/tests/fixtures/demo/new/NoiseDemo_autosar_rtw/NoiseDemo.h new file mode 100644 index 0000000..78096c3 --- /dev/null +++ b/tests/fixtures/demo/new/NoiseDemo_autosar_rtw/NoiseDemo.h @@ -0,0 +1,13 @@ +/* + * File: NoiseDemo.h + * Code generated for Simulink model 'NoiseDemo'. + */ + +#ifndef RTW_HEADER_NoiseDemo_h_ +#define RTW_HEADER_NoiseDemo_h_ + +#include "rtwtypes.h" + +extern void Calc_step(void); + +#endif diff --git a/tests/fixtures/demo/new/rules/src/rename_only.c b/tests/fixtures/demo/new/NoiseDemo_autosar_rtw/NoiseDemo_data.c similarity index 90% rename from tests/fixtures/demo/new/rules/src/rename_only.c rename to tests/fixtures/demo/new/NoiseDemo_autosar_rtw/NoiseDemo_data.c index 740171a..4c087eb 100644 --- a/tests/fixtures/demo/new/rules/src/rename_only.c +++ b/tests/fixtures/demo/new/NoiseDemo_autosar_rtw/NoiseDemo_data.c @@ -1,4 +1,4 @@ -#include "rename_only.h" +#include "NoiseDemo.h" void Sub_step(void) { diff --git a/tests/fixtures/demo/new/NoiseDemo_autosar_rtw/NoiseDemo_private.h b/tests/fixtures/demo/new/NoiseDemo_autosar_rtw/NoiseDemo_private.h new file mode 100644 index 0000000..3377ce1 --- /dev/null +++ b/tests/fixtures/demo/new/NoiseDemo_autosar_rtw/NoiseDemo_private.h @@ -0,0 +1,6 @@ +#ifndef RTW_HEADER_NoiseDemo_private_h_ +#define RTW_HEADER_NoiseDemo_private_h_ + +extern void Sub_step(void); + +#endif diff --git a/tests/fixtures/demo/new/NoiseDemo_autosar_rtw/Rte_NoiseDemo.h b/tests/fixtures/demo/new/NoiseDemo_autosar_rtw/Rte_NoiseDemo.h new file mode 100644 index 0000000..414b3fd --- /dev/null +++ b/tests/fixtures/demo/new/NoiseDemo_autosar_rtw/Rte_NoiseDemo.h @@ -0,0 +1,12 @@ +/* + * File: Rte_NoiseDemo.h + * + * RTE header generated for AUTOSAR software component 'NoiseDemo'. + */ + +#ifndef RTE_NOISEDEMO_H +#define RTE_NOISEDEMO_H + +#include "Rte_Type.h" + +#endif /* RTE_NOISEDEMO_H */ diff --git a/tests/fixtures/new/src/comment_only.c b/tests/fixtures/demo/new/NoiseDemo_autosar_rtw/ert_main.c similarity index 87% rename from tests/fixtures/new/src/comment_only.c rename to tests/fixtures/demo/new/NoiseDemo_autosar_rtw/ert_main.c index 4f93e59..4490c60 100644 --- a/tests/fixtures/new/src/comment_only.c +++ b/tests/fixtures/demo/new/NoiseDemo_autosar_rtw/ert_main.c @@ -1,11 +1,11 @@ /* - * File: comment_only.c + * File: ert_main.c * Code generated for Simulink model 'Model'. * Model version : 1.43 * Simulink Coder version : 9.8 (R2023a) 19-Nov-2022 * C/C++ source code generated on : Tue Feb 17 08:45:01 2026 */ -#include "comment_only.h" +#include "NoiseDemo.h" /* Model step function (regenerated) */ void Model_step(void) diff --git a/tests/fixtures/demo/new/NoiseDemo_autosar_rtw/rtwtypes.h b/tests/fixtures/demo/new/NoiseDemo_autosar_rtw/rtwtypes.h new file mode 100644 index 0000000..381ed05 --- /dev/null +++ b/tests/fixtures/demo/new/NoiseDemo_autosar_rtw/rtwtypes.h @@ -0,0 +1,28 @@ +/* + * rtwtypes.h + * + * Code generation for model shared utilities. + */ + +#ifndef RTWTYPES_H +#define RTWTYPES_H + +typedef signed char int8_T; +typedef unsigned char uint8_T; +typedef short int16_T; +typedef unsigned short uint16_T; +typedef int int32_T; +typedef unsigned int uint32_T; +typedef float real32_T; +typedef double real_T; +typedef unsigned char boolean_T; + +#ifndef TRUE +#define TRUE (1U) +#endif + +#ifndef FALSE +#define FALSE (0U) +#endif + +#endif /* RTWTYPES_H */ diff --git a/tests/fixtures/demo/new/PedalMap.c b/tests/fixtures/demo/new/PedalMap_autosar_rtw/PedalMap.c similarity index 100% rename from tests/fixtures/demo/new/PedalMap.c rename to tests/fixtures/demo/new/PedalMap_autosar_rtw/PedalMap.c diff --git a/tests/fixtures/demo/new/PedalMap_autosar_rtw/PedalMap.h b/tests/fixtures/demo/new/PedalMap_autosar_rtw/PedalMap.h new file mode 100644 index 0000000..f6333f6 --- /dev/null +++ b/tests/fixtures/demo/new/PedalMap_autosar_rtw/PedalMap.h @@ -0,0 +1,22 @@ +/* + * File: PedalMap.h + * Code generated for Simulink model 'PedalMap'. + * Model version : 4.07 + */ + +#ifndef RTW_HEADER_PedalMap_h_ +#define RTW_HEADER_PedalMap_h_ + +#include "rtwtypes.h" + +typedef struct { + real_T Raw; +} ExtU_PedalMap_T; + +typedef struct { + real_T Scaled; +} ExtY_PedalMap_T; + +extern void PedalMap_step(void); + +#endif diff --git a/tests/fixtures/demo/new/PedalMap_autosar_rtw/PedalMap_private.h b/tests/fixtures/demo/new/PedalMap_autosar_rtw/PedalMap_private.h new file mode 100644 index 0000000..0360ae8 --- /dev/null +++ b/tests/fixtures/demo/new/PedalMap_autosar_rtw/PedalMap_private.h @@ -0,0 +1,14 @@ +/* + * File: PedalMap_private.h + * + * Code generated for Simulink model 'PedalMap'. + */ + +#ifndef RTW_HEADER_PedalMap_private_h_ +#define RTW_HEADER_PedalMap_private_h_ + +#include "rtwtypes.h" + +extern void PedalMap_initialize(void); + +#endif /* RTW_HEADER_PedalMap_private_h_ */ diff --git a/tests/fixtures/demo/new/PedalMap_autosar_rtw/PedalMap_types.h b/tests/fixtures/demo/new/PedalMap_autosar_rtw/PedalMap_types.h new file mode 100644 index 0000000..ac9bd7e --- /dev/null +++ b/tests/fixtures/demo/new/PedalMap_autosar_rtw/PedalMap_types.h @@ -0,0 +1,14 @@ +/* + * File: PedalMap_types.h + * + * Code generated for Simulink model 'PedalMap'. + */ + +#ifndef RTW_HEADER_PedalMap_types_h_ +#define RTW_HEADER_PedalMap_types_h_ + +#include "rtwtypes.h" + +typedef struct P_PedalMap_T_ P_PedalMap_T; + +#endif /* RTW_HEADER_PedalMap_types_h_ */ diff --git a/tests/fixtures/demo/new/PedalMap_autosar_rtw/Rte_PedalMap.h b/tests/fixtures/demo/new/PedalMap_autosar_rtw/Rte_PedalMap.h new file mode 100644 index 0000000..976cce5 --- /dev/null +++ b/tests/fixtures/demo/new/PedalMap_autosar_rtw/Rte_PedalMap.h @@ -0,0 +1,12 @@ +/* + * File: Rte_PedalMap.h + * + * RTE header generated for AUTOSAR software component 'PedalMap'. + */ + +#ifndef RTE_PEDALMAP_H +#define RTE_PEDALMAP_H + +#include "Rte_Type.h" + +#endif /* RTE_PEDALMAP_H */ diff --git a/tests/fixtures/demo/new/PedalMap_autosar_rtw/rtwtypes.h b/tests/fixtures/demo/new/PedalMap_autosar_rtw/rtwtypes.h new file mode 100644 index 0000000..381ed05 --- /dev/null +++ b/tests/fixtures/demo/new/PedalMap_autosar_rtw/rtwtypes.h @@ -0,0 +1,28 @@ +/* + * rtwtypes.h + * + * Code generation for model shared utilities. + */ + +#ifndef RTWTYPES_H +#define RTWTYPES_H + +typedef signed char int8_T; +typedef unsigned char uint8_T; +typedef short int16_T; +typedef unsigned short uint16_T; +typedef int int32_T; +typedef unsigned int uint32_T; +typedef float real32_T; +typedef double real_T; +typedef unsigned char boolean_T; + +#ifndef TRUE +#define TRUE (1U) +#endif + +#ifndef FALSE +#define FALSE (0U) +#endif + +#endif /* RTWTYPES_H */ diff --git a/tests/fixtures/demo/new/SpeedCtrl_autosar_rtw/Rte_SpeedCtrl.h b/tests/fixtures/demo/new/SpeedCtrl_autosar_rtw/Rte_SpeedCtrl.h new file mode 100644 index 0000000..402d0b9 --- /dev/null +++ b/tests/fixtures/demo/new/SpeedCtrl_autosar_rtw/Rte_SpeedCtrl.h @@ -0,0 +1,12 @@ +/* + * File: Rte_SpeedCtrl.h + * + * RTE header generated for AUTOSAR software component 'SpeedCtrl'. + */ + +#ifndef RTE_SPEEDCTRL_H +#define RTE_SPEEDCTRL_H + +#include "Rte_Type.h" + +#endif /* RTE_SPEEDCTRL_H */ diff --git a/tests/fixtures/demo/new/SpeedCtrl.c b/tests/fixtures/demo/new/SpeedCtrl_autosar_rtw/SpeedCtrl.c similarity index 100% rename from tests/fixtures/demo/new/SpeedCtrl.c rename to tests/fixtures/demo/new/SpeedCtrl_autosar_rtw/SpeedCtrl.c diff --git a/tests/fixtures/demo/new/SpeedCtrl.h b/tests/fixtures/demo/new/SpeedCtrl_autosar_rtw/SpeedCtrl.h similarity index 100% rename from tests/fixtures/demo/new/SpeedCtrl.h rename to tests/fixtures/demo/new/SpeedCtrl_autosar_rtw/SpeedCtrl.h diff --git a/tests/fixtures/demo/new/SpeedCtrl_autosar_rtw/SpeedCtrl_data.c b/tests/fixtures/demo/new/SpeedCtrl_autosar_rtw/SpeedCtrl_data.c new file mode 100644 index 0000000..65f1400 --- /dev/null +++ b/tests/fixtures/demo/new/SpeedCtrl_autosar_rtw/SpeedCtrl_data.c @@ -0,0 +1,12 @@ +/* + * File: SpeedCtrl_data.c + * + * Code generated for Simulink model 'SpeedCtrl'. + */ + +#include "SpeedCtrl.h" + +void SpeedCtrl_initialize(void) +{ + rtY.SpeedRequest = 0.0; +} diff --git a/tests/fixtures/demo/new/SpeedCtrl_autosar_rtw/SpeedCtrl_private.h b/tests/fixtures/demo/new/SpeedCtrl_autosar_rtw/SpeedCtrl_private.h new file mode 100644 index 0000000..84b14e5 --- /dev/null +++ b/tests/fixtures/demo/new/SpeedCtrl_autosar_rtw/SpeedCtrl_private.h @@ -0,0 +1,14 @@ +/* + * File: SpeedCtrl_private.h + * + * Code generated for Simulink model 'SpeedCtrl'. + */ + +#ifndef RTW_HEADER_SpeedCtrl_private_h_ +#define RTW_HEADER_SpeedCtrl_private_h_ + +#include "rtwtypes.h" + +extern void SpeedCtrl_initialize(void); + +#endif /* RTW_HEADER_SpeedCtrl_private_h_ */ diff --git a/tests/fixtures/demo/new/SpeedCtrl_autosar_rtw/SpeedCtrl_types.h b/tests/fixtures/demo/new/SpeedCtrl_autosar_rtw/SpeedCtrl_types.h new file mode 100644 index 0000000..8f4c099 --- /dev/null +++ b/tests/fixtures/demo/new/SpeedCtrl_autosar_rtw/SpeedCtrl_types.h @@ -0,0 +1,14 @@ +/* + * File: SpeedCtrl_types.h + * + * Code generated for Simulink model 'SpeedCtrl'. + */ + +#ifndef RTW_HEADER_SpeedCtrl_types_h_ +#define RTW_HEADER_SpeedCtrl_types_h_ + +#include "rtwtypes.h" + +typedef struct P_SpeedCtrl_T_ P_SpeedCtrl_T; + +#endif /* RTW_HEADER_SpeedCtrl_types_h_ */ diff --git a/tests/fixtures/demo/new/SpeedCtrl_autosar_rtw/rtwtypes.h b/tests/fixtures/demo/new/SpeedCtrl_autosar_rtw/rtwtypes.h new file mode 100644 index 0000000..381ed05 --- /dev/null +++ b/tests/fixtures/demo/new/SpeedCtrl_autosar_rtw/rtwtypes.h @@ -0,0 +1,28 @@ +/* + * rtwtypes.h + * + * Code generation for model shared utilities. + */ + +#ifndef RTWTYPES_H +#define RTWTYPES_H + +typedef signed char int8_T; +typedef unsigned char uint8_T; +typedef short int16_T; +typedef unsigned short uint16_T; +typedef int int32_T; +typedef unsigned int uint32_T; +typedef float real32_T; +typedef double real_T; +typedef unsigned char boolean_T; + +#ifndef TRUE +#define TRUE (1U) +#endif + +#ifndef FALSE +#define FALSE (0U) +#endif + +#endif /* RTWTYPES_H */ diff --git a/tests/fixtures/demo/new/StaleGen_autosar_rtw/Rte_StaleGen.h b/tests/fixtures/demo/new/StaleGen_autosar_rtw/Rte_StaleGen.h new file mode 100644 index 0000000..fc047ea --- /dev/null +++ b/tests/fixtures/demo/new/StaleGen_autosar_rtw/Rte_StaleGen.h @@ -0,0 +1,12 @@ +/* + * File: Rte_StaleGen.h + * + * RTE header generated for AUTOSAR software component 'StaleGen'. + */ + +#ifndef RTE_STALEGEN_H +#define RTE_STALEGEN_H + +#include "Rte_Type.h" + +#endif /* RTE_STALEGEN_H */ diff --git a/tests/fixtures/demo/new/StaleGen.c b/tests/fixtures/demo/new/StaleGen_autosar_rtw/StaleGen.c similarity index 100% rename from tests/fixtures/demo/new/StaleGen.c rename to tests/fixtures/demo/new/StaleGen_autosar_rtw/StaleGen.c diff --git a/tests/fixtures/demo/new/StaleGen_autosar_rtw/StaleGen.h b/tests/fixtures/demo/new/StaleGen_autosar_rtw/StaleGen.h new file mode 100644 index 0000000..aa97866 --- /dev/null +++ b/tests/fixtures/demo/new/StaleGen_autosar_rtw/StaleGen.h @@ -0,0 +1,22 @@ +/* + * File: StaleGen.h + * Code generated for Simulink model 'StaleGen'. + * Model version : 3.02 + */ + +#ifndef RTW_HEADER_StaleGen_h_ +#define RTW_HEADER_StaleGen_h_ + +#include "rtwtypes.h" + +typedef struct { + real_T In; +} ExtU_StaleGen_T; + +typedef struct { + real_T Out; +} ExtY_StaleGen_T; + +extern void StaleGen_step(void); + +#endif diff --git a/tests/fixtures/demo/new/StaleGen_autosar_rtw/StaleGen_private.h b/tests/fixtures/demo/new/StaleGen_autosar_rtw/StaleGen_private.h new file mode 100644 index 0000000..c7fb71e --- /dev/null +++ b/tests/fixtures/demo/new/StaleGen_autosar_rtw/StaleGen_private.h @@ -0,0 +1,14 @@ +/* + * File: StaleGen_private.h + * + * Code generated for Simulink model 'StaleGen'. + */ + +#ifndef RTW_HEADER_StaleGen_private_h_ +#define RTW_HEADER_StaleGen_private_h_ + +#include "rtwtypes.h" + +extern void StaleGen_initialize(void); + +#endif /* RTW_HEADER_StaleGen_private_h_ */ diff --git a/tests/fixtures/demo/new/StaleGen_autosar_rtw/StaleGen_types.h b/tests/fixtures/demo/new/StaleGen_autosar_rtw/StaleGen_types.h new file mode 100644 index 0000000..9377204 --- /dev/null +++ b/tests/fixtures/demo/new/StaleGen_autosar_rtw/StaleGen_types.h @@ -0,0 +1,14 @@ +/* + * File: StaleGen_types.h + * + * Code generated for Simulink model 'StaleGen'. + */ + +#ifndef RTW_HEADER_StaleGen_types_h_ +#define RTW_HEADER_StaleGen_types_h_ + +#include "rtwtypes.h" + +typedef struct P_StaleGen_T_ P_StaleGen_T; + +#endif /* RTW_HEADER_StaleGen_types_h_ */ diff --git a/tests/fixtures/demo/new/StaleGen_autosar_rtw/rtwtypes.h b/tests/fixtures/demo/new/StaleGen_autosar_rtw/rtwtypes.h new file mode 100644 index 0000000..381ed05 --- /dev/null +++ b/tests/fixtures/demo/new/StaleGen_autosar_rtw/rtwtypes.h @@ -0,0 +1,28 @@ +/* + * rtwtypes.h + * + * Code generation for model shared utilities. + */ + +#ifndef RTWTYPES_H +#define RTWTYPES_H + +typedef signed char int8_T; +typedef unsigned char uint8_T; +typedef short int16_T; +typedef unsigned short uint16_T; +typedef int int32_T; +typedef unsigned int uint32_T; +typedef float real32_T; +typedef double real_T; +typedef unsigned char boolean_T; + +#ifndef TRUE +#define TRUE (1U) +#endif + +#ifndef FALSE +#define FALSE (0U) +#endif + +#endif /* RTWTYPES_H */ diff --git a/tests/fixtures/demo/new/TorqueLimiter_autosar_rtw/Rte_TorqueLimiter.h b/tests/fixtures/demo/new/TorqueLimiter_autosar_rtw/Rte_TorqueLimiter.h new file mode 100644 index 0000000..28293c3 --- /dev/null +++ b/tests/fixtures/demo/new/TorqueLimiter_autosar_rtw/Rte_TorqueLimiter.h @@ -0,0 +1,12 @@ +/* + * File: Rte_TorqueLimiter.h + * + * RTE header generated for AUTOSAR software component 'TorqueLimiter'. + */ + +#ifndef RTE_TORQUELIMITER_H +#define RTE_TORQUELIMITER_H + +#include "Rte_Type.h" + +#endif /* RTE_TORQUELIMITER_H */ diff --git a/tests/fixtures/demo/new/TorqueLimiter.c b/tests/fixtures/demo/new/TorqueLimiter_autosar_rtw/TorqueLimiter.c similarity index 100% rename from tests/fixtures/demo/new/TorqueLimiter.c rename to tests/fixtures/demo/new/TorqueLimiter_autosar_rtw/TorqueLimiter.c diff --git a/tests/fixtures/demo/new/TorqueLimiter_autosar_rtw/TorqueLimiter.h b/tests/fixtures/demo/new/TorqueLimiter_autosar_rtw/TorqueLimiter.h new file mode 100644 index 0000000..9802428 --- /dev/null +++ b/tests/fixtures/demo/new/TorqueLimiter_autosar_rtw/TorqueLimiter.h @@ -0,0 +1,22 @@ +/* + * File: TorqueLimiter.h + * Code generated for Simulink model 'TorqueLimiter'. + */ + +#ifndef RTW_HEADER_TorqueLimiter_h_ +#define RTW_HEADER_TorqueLimiter_h_ + +#include "rtwtypes.h" + +typedef struct { + real_T PedalPosition; + real_T MotorSpeed; +} ExtU_TorqueLimiter_T; + +typedef struct { + real_T TorqueCmd; +} ExtY_TorqueLimiter_T; + +extern void Rte_Runnable_TorqueLimiter_Step(void); + +#endif diff --git a/tests/fixtures/demo/new/rules/src/rename_conflict.c b/tests/fixtures/demo/new/TorqueLimiter_autosar_rtw/TorqueLimiter_data.c similarity index 80% rename from tests/fixtures/demo/new/rules/src/rename_conflict.c rename to tests/fixtures/demo/new/TorqueLimiter_autosar_rtw/TorqueLimiter_data.c index 0fa12a1..a6033d8 100644 --- a/tests/fixtures/demo/new/rules/src/rename_conflict.c +++ b/tests/fixtures/demo/new/TorqueLimiter_autosar_rtw/TorqueLimiter_data.c @@ -1,4 +1,4 @@ -#include "rename_conflict.h" +#include "TorqueLimiter.h" void Conf_step(void) { diff --git a/tests/fixtures/demo/new/TorqueLimiter_autosar_rtw/TorqueLimiter_private.h b/tests/fixtures/demo/new/TorqueLimiter_autosar_rtw/TorqueLimiter_private.h new file mode 100644 index 0000000..5b4e8a7 --- /dev/null +++ b/tests/fixtures/demo/new/TorqueLimiter_autosar_rtw/TorqueLimiter_private.h @@ -0,0 +1,14 @@ +/* + * File: TorqueLimiter_private.h + * + * Code generated for Simulink model 'TorqueLimiter'. + */ + +#ifndef RTW_HEADER_TorqueLimiter_private_h_ +#define RTW_HEADER_TorqueLimiter_private_h_ + +#include "rtwtypes.h" + +extern void TorqueLimiter_initialize(void); + +#endif /* RTW_HEADER_TorqueLimiter_private_h_ */ diff --git a/tests/fixtures/demo/new/TorqueLimiter_autosar_rtw/TorqueLimiter_types.h b/tests/fixtures/demo/new/TorqueLimiter_autosar_rtw/TorqueLimiter_types.h new file mode 100644 index 0000000..64d5931 --- /dev/null +++ b/tests/fixtures/demo/new/TorqueLimiter_autosar_rtw/TorqueLimiter_types.h @@ -0,0 +1,14 @@ +/* + * File: TorqueLimiter_types.h + * + * Code generated for Simulink model 'TorqueLimiter'. + */ + +#ifndef RTW_HEADER_TorqueLimiter_types_h_ +#define RTW_HEADER_TorqueLimiter_types_h_ + +#include "rtwtypes.h" + +typedef struct P_TorqueLimiter_T_ P_TorqueLimiter_T; + +#endif /* RTW_HEADER_TorqueLimiter_types_h_ */ diff --git a/tests/fixtures/demo/new/TorqueLimiter_autosar_rtw/rtwtypes.h b/tests/fixtures/demo/new/TorqueLimiter_autosar_rtw/rtwtypes.h new file mode 100644 index 0000000..381ed05 --- /dev/null +++ b/tests/fixtures/demo/new/TorqueLimiter_autosar_rtw/rtwtypes.h @@ -0,0 +1,28 @@ +/* + * rtwtypes.h + * + * Code generation for model shared utilities. + */ + +#ifndef RTWTYPES_H +#define RTWTYPES_H + +typedef signed char int8_T; +typedef unsigned char uint8_T; +typedef short int16_T; +typedef unsigned short uint16_T; +typedef int int32_T; +typedef unsigned int uint32_T; +typedef float real32_T; +typedef double real_T; +typedef unsigned char boolean_T; + +#ifndef TRUE +#define TRUE (1U) +#endif + +#ifndef FALSE +#define FALSE (0U) +#endif + +#endif /* RTWTYPES_H */ diff --git a/tests/fixtures/demo/new/rules/a2l/comment_only.a2l b/tests/fixtures/demo/new/a2l/Ctrl.a2l similarity index 100% rename from tests/fixtures/demo/new/rules/a2l/comment_only.a2l rename to tests/fixtures/demo/new/a2l/Ctrl.a2l diff --git a/tests/fixtures/demo/new/rules/a2l/cal.a2l b/tests/fixtures/demo/new/a2l/NoiseDemo.a2l similarity index 100% rename from tests/fixtures/demo/new/rules/a2l/cal.a2l rename to tests/fixtures/demo/new/a2l/NoiseDemo.a2l diff --git a/tests/fixtures/demo/new/PedalMap.a2l b/tests/fixtures/demo/new/a2l/PedalMap.a2l similarity index 100% rename from tests/fixtures/demo/new/PedalMap.a2l rename to tests/fixtures/demo/new/a2l/PedalMap.a2l diff --git a/tests/fixtures/demo/new/StaleGen.a2l b/tests/fixtures/demo/new/a2l/StaleGen.a2l similarity index 100% rename from tests/fixtures/demo/new/StaleGen.a2l rename to tests/fixtures/demo/new/a2l/StaleGen.a2l diff --git a/tests/fixtures/demo/new/models/Ctrl_component.arxml b/tests/fixtures/demo/new/arxml/Ctrl_component.arxml similarity index 100% rename from tests/fixtures/demo/new/models/Ctrl_component.arxml rename to tests/fixtures/demo/new/arxml/Ctrl_component.arxml diff --git a/tests/fixtures/demo/new/rules/arxml/uuid_only.arxml b/tests/fixtures/demo/new/arxml/NoiseDemo_component.arxml similarity index 100% rename from tests/fixtures/demo/new/rules/arxml/uuid_only.arxml rename to tests/fixtures/demo/new/arxml/NoiseDemo_component.arxml diff --git a/tests/fixtures/demo/new/rules/arxml/real_change.arxml b/tests/fixtures/demo/new/arxml/NoiseDemo_datatype.arxml similarity index 100% rename from tests/fixtures/demo/new/rules/arxml/real_change.arxml rename to tests/fixtures/demo/new/arxml/NoiseDemo_datatype.arxml diff --git a/tests/fixtures/demo/new/rules/arxml/admindata.arxml b/tests/fixtures/demo/new/arxml/NoiseDemo_implementation.arxml similarity index 100% rename from tests/fixtures/demo/new/rules/arxml/admindata.arxml rename to tests/fixtures/demo/new/arxml/NoiseDemo_implementation.arxml diff --git a/tests/fixtures/demo/new/rules/arxml/iface.arxml b/tests/fixtures/demo/new/arxml/NoiseDemo_interface.arxml similarity index 100% rename from tests/fixtures/demo/new/rules/arxml/iface.arxml rename to tests/fixtures/demo/new/arxml/NoiseDemo_interface.arxml diff --git a/tests/fixtures/demo/new/PedalMap.arxml b/tests/fixtures/demo/new/arxml/PedalMap_component.arxml similarity index 100% rename from tests/fixtures/demo/new/PedalMap.arxml rename to tests/fixtures/demo/new/arxml/PedalMap_component.arxml diff --git a/tests/fixtures/demo/new/SpeedCtrl.arxml b/tests/fixtures/demo/new/arxml/SpeedCtrl_component.arxml similarity index 100% rename from tests/fixtures/demo/new/SpeedCtrl.arxml rename to tests/fixtures/demo/new/arxml/SpeedCtrl_component.arxml diff --git a/tests/fixtures/demo/new/StaleGen.arxml b/tests/fixtures/demo/new/arxml/StaleGen_component.arxml similarity index 100% rename from tests/fixtures/demo/new/StaleGen.arxml rename to tests/fixtures/demo/new/arxml/StaleGen_component.arxml diff --git a/tests/fixtures/demo/new/TorqueLimiter.arxml b/tests/fixtures/demo/new/arxml/TorqueLimiter_component.arxml similarity index 100% rename from tests/fixtures/demo/new/TorqueLimiter.arxml rename to tests/fixtures/demo/new/arxml/TorqueLimiter_component.arxml diff --git a/tests/fixtures/demo/new/models/rtwtypes.h b/tests/fixtures/demo/new/models/rtwtypes.h deleted file mode 100644 index 8362173..0000000 --- a/tests/fixtures/demo/new/models/rtwtypes.h +++ /dev/null @@ -1,4 +0,0 @@ -#ifndef RTWTYPES_H -#define RTWTYPES_H -typedef unsigned char boolean_T; -#endif diff --git a/tests/fixtures/demo/new/rules/src/added.c b/tests/fixtures/demo/new/rules/src/added.c deleted file mode 100644 index 6855653..0000000 --- a/tests/fixtures/demo/new/rules/src/added.c +++ /dev/null @@ -1,6 +0,0 @@ -#include "added.h" - -void New_step(void) -{ - rtY.Out9 = 0.0; -} diff --git a/tests/fixtures/demo/new/rules/src/comment_only.c b/tests/fixtures/demo/new/rules/src/comment_only.c deleted file mode 100644 index 4f93e59..0000000 --- a/tests/fixtures/demo/new/rules/src/comment_only.c +++ /dev/null @@ -1,15 +0,0 @@ -/* - * File: comment_only.c - * Code generated for Simulink model 'Model'. - * Model version : 1.43 - * Simulink Coder version : 9.8 (R2023a) 19-Nov-2022 - * C/C++ source code generated on : Tue Feb 17 08:45:01 2026 - */ -#include "comment_only.h" - -/* Model step function (regenerated) */ -void Model_step(void) -{ - /* Outport: '/Out1' */ - rtY.Out1 = rtU.In1 * 2.0; -} diff --git a/tests/fixtures/demo/new/rules/src/real_change.c b/tests/fixtures/demo/new/rules/src/real_change.c deleted file mode 100644 index bc6bd34..0000000 --- a/tests/fixtures/demo/new/rules/src/real_change.c +++ /dev/null @@ -1,12 +0,0 @@ -/* Generated on : Tue Feb 17 08:45:01 2026 */ -#include "real_change.h" - -void Calc_step(void) -{ - /* saturation limit */ - if (rtU.In1 > 10) { - rtY.Out1 = 10; - } else { - rtY.Out1 = rtU.In1; - } -} diff --git a/tests/fixtures/demo/new/rules/src/same.h b/tests/fixtures/demo/new/rules/src/same.h deleted file mode 100644 index a3d7f02..0000000 --- a/tests/fixtures/demo/new/rules/src/same.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef SAME_H -#define SAME_H - -extern void Sub_step(void); - -#endif diff --git a/tests/fixtures/demo/old/models/Ctrl.c b/tests/fixtures/demo/old/Ctrl_autosar_rtw/Ctrl.c similarity index 100% rename from tests/fixtures/demo/old/models/Ctrl.c rename to tests/fixtures/demo/old/Ctrl_autosar_rtw/Ctrl.c diff --git a/tests/fixtures/demo/old/models/Ctrl.h b/tests/fixtures/demo/old/Ctrl_autosar_rtw/Ctrl.h similarity index 100% rename from tests/fixtures/demo/old/models/Ctrl.h rename to tests/fixtures/demo/old/Ctrl_autosar_rtw/Ctrl.h diff --git a/tests/fixtures/demo/old/Ctrl_autosar_rtw/Ctrl_private.h b/tests/fixtures/demo/old/Ctrl_autosar_rtw/Ctrl_private.h new file mode 100644 index 0000000..2c5fae1 --- /dev/null +++ b/tests/fixtures/demo/old/Ctrl_autosar_rtw/Ctrl_private.h @@ -0,0 +1,14 @@ +/* + * File: Ctrl_private.h + * + * Code generated for Simulink model 'Ctrl'. + */ + +#ifndef RTW_HEADER_Ctrl_private_h_ +#define RTW_HEADER_Ctrl_private_h_ + +#include "rtwtypes.h" + +extern void Ctrl_initialize(void); + +#endif /* RTW_HEADER_Ctrl_private_h_ */ diff --git a/tests/fixtures/demo/old/models/Ctrl_types.h b/tests/fixtures/demo/old/Ctrl_autosar_rtw/Ctrl_types.h similarity index 100% rename from tests/fixtures/demo/old/models/Ctrl_types.h rename to tests/fixtures/demo/old/Ctrl_autosar_rtw/Ctrl_types.h diff --git a/tests/fixtures/demo/old/Ctrl_autosar_rtw/Rte_Ctrl.h b/tests/fixtures/demo/old/Ctrl_autosar_rtw/Rte_Ctrl.h new file mode 100644 index 0000000..153c28b --- /dev/null +++ b/tests/fixtures/demo/old/Ctrl_autosar_rtw/Rte_Ctrl.h @@ -0,0 +1,12 @@ +/* + * File: Rte_Ctrl.h + * + * RTE header generated for AUTOSAR software component 'Ctrl'. + */ + +#ifndef RTE_CTRL_H +#define RTE_CTRL_H + +#include "Rte_Type.h" + +#endif /* RTE_CTRL_H */ diff --git a/tests/fixtures/demo/old/Ctrl_autosar_rtw/rtwtypes.h b/tests/fixtures/demo/old/Ctrl_autosar_rtw/rtwtypes.h new file mode 100644 index 0000000..381ed05 --- /dev/null +++ b/tests/fixtures/demo/old/Ctrl_autosar_rtw/rtwtypes.h @@ -0,0 +1,28 @@ +/* + * rtwtypes.h + * + * Code generation for model shared utilities. + */ + +#ifndef RTWTYPES_H +#define RTWTYPES_H + +typedef signed char int8_T; +typedef unsigned char uint8_T; +typedef short int16_T; +typedef unsigned short uint16_T; +typedef int int32_T; +typedef unsigned int uint32_T; +typedef float real32_T; +typedef double real_T; +typedef unsigned char boolean_T; + +#ifndef TRUE +#define TRUE (1U) +#endif + +#ifndef FALSE +#define FALSE (0U) +#endif + +#endif /* RTWTYPES_H */ diff --git a/tests/fixtures/old/src/real_change.c b/tests/fixtures/demo/old/NoiseDemo_autosar_rtw/NoiseDemo.c similarity index 87% rename from tests/fixtures/old/src/real_change.c rename to tests/fixtures/demo/old/NoiseDemo_autosar_rtw/NoiseDemo.c index 2e796cb..89c90b3 100644 --- a/tests/fixtures/old/src/real_change.c +++ b/tests/fixtures/demo/old/NoiseDemo_autosar_rtw/NoiseDemo.c @@ -1,5 +1,5 @@ /* Generated on : Mon Jan 05 10:12:33 2026 */ -#include "real_change.h" +#include "NoiseDemo.h" void Calc_step(void) { diff --git a/tests/fixtures/demo/old/NoiseDemo_autosar_rtw/NoiseDemo.h b/tests/fixtures/demo/old/NoiseDemo_autosar_rtw/NoiseDemo.h new file mode 100644 index 0000000..78096c3 --- /dev/null +++ b/tests/fixtures/demo/old/NoiseDemo_autosar_rtw/NoiseDemo.h @@ -0,0 +1,13 @@ +/* + * File: NoiseDemo.h + * Code generated for Simulink model 'NoiseDemo'. + */ + +#ifndef RTW_HEADER_NoiseDemo_h_ +#define RTW_HEADER_NoiseDemo_h_ + +#include "rtwtypes.h" + +extern void Calc_step(void); + +#endif diff --git a/tests/fixtures/demo/old/rules/src/rename_only.c b/tests/fixtures/demo/old/NoiseDemo_autosar_rtw/NoiseDemo_data.c similarity index 89% rename from tests/fixtures/demo/old/rules/src/rename_only.c rename to tests/fixtures/demo/old/NoiseDemo_autosar_rtw/NoiseDemo_data.c index 6436f05..aa3d967 100644 --- a/tests/fixtures/demo/old/rules/src/rename_only.c +++ b/tests/fixtures/demo/old/NoiseDemo_autosar_rtw/NoiseDemo_data.c @@ -1,4 +1,4 @@ -#include "rename_only.h" +#include "NoiseDemo.h" void Sub_step(void) { diff --git a/tests/fixtures/demo/old/NoiseDemo_autosar_rtw/NoiseDemo_private.h b/tests/fixtures/demo/old/NoiseDemo_autosar_rtw/NoiseDemo_private.h new file mode 100644 index 0000000..3377ce1 --- /dev/null +++ b/tests/fixtures/demo/old/NoiseDemo_autosar_rtw/NoiseDemo_private.h @@ -0,0 +1,6 @@ +#ifndef RTW_HEADER_NoiseDemo_private_h_ +#define RTW_HEADER_NoiseDemo_private_h_ + +extern void Sub_step(void); + +#endif diff --git a/tests/fixtures/demo/old/NoiseDemo_autosar_rtw/NoiseDemo_types.h b/tests/fixtures/demo/old/NoiseDemo_autosar_rtw/NoiseDemo_types.h new file mode 100644 index 0000000..113fb51 --- /dev/null +++ b/tests/fixtures/demo/old/NoiseDemo_autosar_rtw/NoiseDemo_types.h @@ -0,0 +1,13 @@ +/* + * File: NoiseDemo_types.h + * Code generated for Simulink model 'NoiseDemo'. + */ + +#ifndef RTW_HEADER_NoiseDemo_types_h_ +#define RTW_HEADER_NoiseDemo_types_h_ + +#include "rtwtypes.h" + +typedef struct P_NoiseDemo_T_ P_NoiseDemo_T; + +#endif diff --git a/tests/fixtures/demo/old/NoiseDemo_autosar_rtw/Rte_NoiseDemo.h b/tests/fixtures/demo/old/NoiseDemo_autosar_rtw/Rte_NoiseDemo.h new file mode 100644 index 0000000..414b3fd --- /dev/null +++ b/tests/fixtures/demo/old/NoiseDemo_autosar_rtw/Rte_NoiseDemo.h @@ -0,0 +1,12 @@ +/* + * File: Rte_NoiseDemo.h + * + * RTE header generated for AUTOSAR software component 'NoiseDemo'. + */ + +#ifndef RTE_NOISEDEMO_H +#define RTE_NOISEDEMO_H + +#include "Rte_Type.h" + +#endif /* RTE_NOISEDEMO_H */ diff --git a/tests/fixtures/old/src/comment_only.c b/tests/fixtures/demo/old/NoiseDemo_autosar_rtw/ert_main.c similarity index 86% rename from tests/fixtures/old/src/comment_only.c rename to tests/fixtures/demo/old/NoiseDemo_autosar_rtw/ert_main.c index 329aa4a..e68f462 100644 --- a/tests/fixtures/old/src/comment_only.c +++ b/tests/fixtures/demo/old/NoiseDemo_autosar_rtw/ert_main.c @@ -1,11 +1,11 @@ /* - * File: comment_only.c + * File: ert_main.c * Code generated for Simulink model 'Model'. * Model version : 1.42 * Simulink Coder version : 9.8 (R2023a) 19-Nov-2022 * C/C++ source code generated on : Mon Jan 05 10:12:33 2026 */ -#include "comment_only.h" +#include "NoiseDemo.h" /* Model step function */ void Model_step(void) diff --git a/tests/fixtures/demo/old/NoiseDemo_autosar_rtw/rtwtypes.h b/tests/fixtures/demo/old/NoiseDemo_autosar_rtw/rtwtypes.h new file mode 100644 index 0000000..381ed05 --- /dev/null +++ b/tests/fixtures/demo/old/NoiseDemo_autosar_rtw/rtwtypes.h @@ -0,0 +1,28 @@ +/* + * rtwtypes.h + * + * Code generation for model shared utilities. + */ + +#ifndef RTWTYPES_H +#define RTWTYPES_H + +typedef signed char int8_T; +typedef unsigned char uint8_T; +typedef short int16_T; +typedef unsigned short uint16_T; +typedef int int32_T; +typedef unsigned int uint32_T; +typedef float real32_T; +typedef double real_T; +typedef unsigned char boolean_T; + +#ifndef TRUE +#define TRUE (1U) +#endif + +#ifndef FALSE +#define FALSE (0U) +#endif + +#endif /* RTWTYPES_H */ diff --git a/tests/fixtures/demo/old/PedalMap.c b/tests/fixtures/demo/old/PedalMap_autosar_rtw/PedalMap.c similarity index 100% rename from tests/fixtures/demo/old/PedalMap.c rename to tests/fixtures/demo/old/PedalMap_autosar_rtw/PedalMap.c diff --git a/tests/fixtures/demo/old/PedalMap_autosar_rtw/PedalMap.h b/tests/fixtures/demo/old/PedalMap_autosar_rtw/PedalMap.h new file mode 100644 index 0000000..f6333f6 --- /dev/null +++ b/tests/fixtures/demo/old/PedalMap_autosar_rtw/PedalMap.h @@ -0,0 +1,22 @@ +/* + * File: PedalMap.h + * Code generated for Simulink model 'PedalMap'. + * Model version : 4.07 + */ + +#ifndef RTW_HEADER_PedalMap_h_ +#define RTW_HEADER_PedalMap_h_ + +#include "rtwtypes.h" + +typedef struct { + real_T Raw; +} ExtU_PedalMap_T; + +typedef struct { + real_T Scaled; +} ExtY_PedalMap_T; + +extern void PedalMap_step(void); + +#endif diff --git a/tests/fixtures/demo/old/PedalMap_autosar_rtw/PedalMap_private.h b/tests/fixtures/demo/old/PedalMap_autosar_rtw/PedalMap_private.h new file mode 100644 index 0000000..0360ae8 --- /dev/null +++ b/tests/fixtures/demo/old/PedalMap_autosar_rtw/PedalMap_private.h @@ -0,0 +1,14 @@ +/* + * File: PedalMap_private.h + * + * Code generated for Simulink model 'PedalMap'. + */ + +#ifndef RTW_HEADER_PedalMap_private_h_ +#define RTW_HEADER_PedalMap_private_h_ + +#include "rtwtypes.h" + +extern void PedalMap_initialize(void); + +#endif /* RTW_HEADER_PedalMap_private_h_ */ diff --git a/tests/fixtures/demo/old/PedalMap_autosar_rtw/PedalMap_types.h b/tests/fixtures/demo/old/PedalMap_autosar_rtw/PedalMap_types.h new file mode 100644 index 0000000..ac9bd7e --- /dev/null +++ b/tests/fixtures/demo/old/PedalMap_autosar_rtw/PedalMap_types.h @@ -0,0 +1,14 @@ +/* + * File: PedalMap_types.h + * + * Code generated for Simulink model 'PedalMap'. + */ + +#ifndef RTW_HEADER_PedalMap_types_h_ +#define RTW_HEADER_PedalMap_types_h_ + +#include "rtwtypes.h" + +typedef struct P_PedalMap_T_ P_PedalMap_T; + +#endif /* RTW_HEADER_PedalMap_types_h_ */ diff --git a/tests/fixtures/demo/old/PedalMap_autosar_rtw/Rte_PedalMap.h b/tests/fixtures/demo/old/PedalMap_autosar_rtw/Rte_PedalMap.h new file mode 100644 index 0000000..976cce5 --- /dev/null +++ b/tests/fixtures/demo/old/PedalMap_autosar_rtw/Rte_PedalMap.h @@ -0,0 +1,12 @@ +/* + * File: Rte_PedalMap.h + * + * RTE header generated for AUTOSAR software component 'PedalMap'. + */ + +#ifndef RTE_PEDALMAP_H +#define RTE_PEDALMAP_H + +#include "Rte_Type.h" + +#endif /* RTE_PEDALMAP_H */ diff --git a/tests/fixtures/demo/old/PedalMap_autosar_rtw/rtwtypes.h b/tests/fixtures/demo/old/PedalMap_autosar_rtw/rtwtypes.h new file mode 100644 index 0000000..381ed05 --- /dev/null +++ b/tests/fixtures/demo/old/PedalMap_autosar_rtw/rtwtypes.h @@ -0,0 +1,28 @@ +/* + * rtwtypes.h + * + * Code generation for model shared utilities. + */ + +#ifndef RTWTYPES_H +#define RTWTYPES_H + +typedef signed char int8_T; +typedef unsigned char uint8_T; +typedef short int16_T; +typedef unsigned short uint16_T; +typedef int int32_T; +typedef unsigned int uint32_T; +typedef float real32_T; +typedef double real_T; +typedef unsigned char boolean_T; + +#ifndef TRUE +#define TRUE (1U) +#endif + +#ifndef FALSE +#define FALSE (0U) +#endif + +#endif /* RTWTYPES_H */ diff --git a/tests/fixtures/demo/old/SpeedCtrl_autosar_rtw/Rte_SpeedCtrl.h b/tests/fixtures/demo/old/SpeedCtrl_autosar_rtw/Rte_SpeedCtrl.h new file mode 100644 index 0000000..402d0b9 --- /dev/null +++ b/tests/fixtures/demo/old/SpeedCtrl_autosar_rtw/Rte_SpeedCtrl.h @@ -0,0 +1,12 @@ +/* + * File: Rte_SpeedCtrl.h + * + * RTE header generated for AUTOSAR software component 'SpeedCtrl'. + */ + +#ifndef RTE_SPEEDCTRL_H +#define RTE_SPEEDCTRL_H + +#include "Rte_Type.h" + +#endif /* RTE_SPEEDCTRL_H */ diff --git a/tests/fixtures/demo/old/SpeedCtrl.c b/tests/fixtures/demo/old/SpeedCtrl_autosar_rtw/SpeedCtrl.c similarity index 100% rename from tests/fixtures/demo/old/SpeedCtrl.c rename to tests/fixtures/demo/old/SpeedCtrl_autosar_rtw/SpeedCtrl.c diff --git a/tests/fixtures/demo/old/SpeedCtrl.h b/tests/fixtures/demo/old/SpeedCtrl_autosar_rtw/SpeedCtrl.h similarity index 100% rename from tests/fixtures/demo/old/SpeedCtrl.h rename to tests/fixtures/demo/old/SpeedCtrl_autosar_rtw/SpeedCtrl.h diff --git a/tests/fixtures/demo/old/SpeedCtrl_autosar_rtw/SpeedCtrl_private.h b/tests/fixtures/demo/old/SpeedCtrl_autosar_rtw/SpeedCtrl_private.h new file mode 100644 index 0000000..84b14e5 --- /dev/null +++ b/tests/fixtures/demo/old/SpeedCtrl_autosar_rtw/SpeedCtrl_private.h @@ -0,0 +1,14 @@ +/* + * File: SpeedCtrl_private.h + * + * Code generated for Simulink model 'SpeedCtrl'. + */ + +#ifndef RTW_HEADER_SpeedCtrl_private_h_ +#define RTW_HEADER_SpeedCtrl_private_h_ + +#include "rtwtypes.h" + +extern void SpeedCtrl_initialize(void); + +#endif /* RTW_HEADER_SpeedCtrl_private_h_ */ diff --git a/tests/fixtures/demo/old/SpeedCtrl_autosar_rtw/SpeedCtrl_types.h b/tests/fixtures/demo/old/SpeedCtrl_autosar_rtw/SpeedCtrl_types.h new file mode 100644 index 0000000..8f4c099 --- /dev/null +++ b/tests/fixtures/demo/old/SpeedCtrl_autosar_rtw/SpeedCtrl_types.h @@ -0,0 +1,14 @@ +/* + * File: SpeedCtrl_types.h + * + * Code generated for Simulink model 'SpeedCtrl'. + */ + +#ifndef RTW_HEADER_SpeedCtrl_types_h_ +#define RTW_HEADER_SpeedCtrl_types_h_ + +#include "rtwtypes.h" + +typedef struct P_SpeedCtrl_T_ P_SpeedCtrl_T; + +#endif /* RTW_HEADER_SpeedCtrl_types_h_ */ diff --git a/tests/fixtures/demo/old/SpeedCtrl_autosar_rtw/rtwtypes.h b/tests/fixtures/demo/old/SpeedCtrl_autosar_rtw/rtwtypes.h new file mode 100644 index 0000000..381ed05 --- /dev/null +++ b/tests/fixtures/demo/old/SpeedCtrl_autosar_rtw/rtwtypes.h @@ -0,0 +1,28 @@ +/* + * rtwtypes.h + * + * Code generation for model shared utilities. + */ + +#ifndef RTWTYPES_H +#define RTWTYPES_H + +typedef signed char int8_T; +typedef unsigned char uint8_T; +typedef short int16_T; +typedef unsigned short uint16_T; +typedef int int32_T; +typedef unsigned int uint32_T; +typedef float real32_T; +typedef double real_T; +typedef unsigned char boolean_T; + +#ifndef TRUE +#define TRUE (1U) +#endif + +#ifndef FALSE +#define FALSE (0U) +#endif + +#endif /* RTWTYPES_H */ diff --git a/tests/fixtures/demo/old/StaleGen_autosar_rtw/Rte_StaleGen.h b/tests/fixtures/demo/old/StaleGen_autosar_rtw/Rte_StaleGen.h new file mode 100644 index 0000000..fc047ea --- /dev/null +++ b/tests/fixtures/demo/old/StaleGen_autosar_rtw/Rte_StaleGen.h @@ -0,0 +1,12 @@ +/* + * File: Rte_StaleGen.h + * + * RTE header generated for AUTOSAR software component 'StaleGen'. + */ + +#ifndef RTE_STALEGEN_H +#define RTE_STALEGEN_H + +#include "Rte_Type.h" + +#endif /* RTE_STALEGEN_H */ diff --git a/tests/fixtures/demo/old/StaleGen.c b/tests/fixtures/demo/old/StaleGen_autosar_rtw/StaleGen.c similarity index 100% rename from tests/fixtures/demo/old/StaleGen.c rename to tests/fixtures/demo/old/StaleGen_autosar_rtw/StaleGen.c diff --git a/tests/fixtures/demo/old/StaleGen_autosar_rtw/StaleGen.h b/tests/fixtures/demo/old/StaleGen_autosar_rtw/StaleGen.h new file mode 100644 index 0000000..aa97866 --- /dev/null +++ b/tests/fixtures/demo/old/StaleGen_autosar_rtw/StaleGen.h @@ -0,0 +1,22 @@ +/* + * File: StaleGen.h + * Code generated for Simulink model 'StaleGen'. + * Model version : 3.02 + */ + +#ifndef RTW_HEADER_StaleGen_h_ +#define RTW_HEADER_StaleGen_h_ + +#include "rtwtypes.h" + +typedef struct { + real_T In; +} ExtU_StaleGen_T; + +typedef struct { + real_T Out; +} ExtY_StaleGen_T; + +extern void StaleGen_step(void); + +#endif diff --git a/tests/fixtures/demo/old/StaleGen_autosar_rtw/StaleGen_private.h b/tests/fixtures/demo/old/StaleGen_autosar_rtw/StaleGen_private.h new file mode 100644 index 0000000..c7fb71e --- /dev/null +++ b/tests/fixtures/demo/old/StaleGen_autosar_rtw/StaleGen_private.h @@ -0,0 +1,14 @@ +/* + * File: StaleGen_private.h + * + * Code generated for Simulink model 'StaleGen'. + */ + +#ifndef RTW_HEADER_StaleGen_private_h_ +#define RTW_HEADER_StaleGen_private_h_ + +#include "rtwtypes.h" + +extern void StaleGen_initialize(void); + +#endif /* RTW_HEADER_StaleGen_private_h_ */ diff --git a/tests/fixtures/demo/old/StaleGen_autosar_rtw/StaleGen_types.h b/tests/fixtures/demo/old/StaleGen_autosar_rtw/StaleGen_types.h new file mode 100644 index 0000000..9377204 --- /dev/null +++ b/tests/fixtures/demo/old/StaleGen_autosar_rtw/StaleGen_types.h @@ -0,0 +1,14 @@ +/* + * File: StaleGen_types.h + * + * Code generated for Simulink model 'StaleGen'. + */ + +#ifndef RTW_HEADER_StaleGen_types_h_ +#define RTW_HEADER_StaleGen_types_h_ + +#include "rtwtypes.h" + +typedef struct P_StaleGen_T_ P_StaleGen_T; + +#endif /* RTW_HEADER_StaleGen_types_h_ */ diff --git a/tests/fixtures/demo/old/StaleGen_autosar_rtw/rtwtypes.h b/tests/fixtures/demo/old/StaleGen_autosar_rtw/rtwtypes.h new file mode 100644 index 0000000..381ed05 --- /dev/null +++ b/tests/fixtures/demo/old/StaleGen_autosar_rtw/rtwtypes.h @@ -0,0 +1,28 @@ +/* + * rtwtypes.h + * + * Code generation for model shared utilities. + */ + +#ifndef RTWTYPES_H +#define RTWTYPES_H + +typedef signed char int8_T; +typedef unsigned char uint8_T; +typedef short int16_T; +typedef unsigned short uint16_T; +typedef int int32_T; +typedef unsigned int uint32_T; +typedef float real32_T; +typedef double real_T; +typedef unsigned char boolean_T; + +#ifndef TRUE +#define TRUE (1U) +#endif + +#ifndef FALSE +#define FALSE (0U) +#endif + +#endif /* RTWTYPES_H */ diff --git a/tests/fixtures/demo/old/TorqueLimiter_autosar_rtw/Rte_TorqueLimiter.h b/tests/fixtures/demo/old/TorqueLimiter_autosar_rtw/Rte_TorqueLimiter.h new file mode 100644 index 0000000..28293c3 --- /dev/null +++ b/tests/fixtures/demo/old/TorqueLimiter_autosar_rtw/Rte_TorqueLimiter.h @@ -0,0 +1,12 @@ +/* + * File: Rte_TorqueLimiter.h + * + * RTE header generated for AUTOSAR software component 'TorqueLimiter'. + */ + +#ifndef RTE_TORQUELIMITER_H +#define RTE_TORQUELIMITER_H + +#include "Rte_Type.h" + +#endif /* RTE_TORQUELIMITER_H */ diff --git a/tests/fixtures/demo/old/TorqueLimiter.c b/tests/fixtures/demo/old/TorqueLimiter_autosar_rtw/TorqueLimiter.c similarity index 100% rename from tests/fixtures/demo/old/TorqueLimiter.c rename to tests/fixtures/demo/old/TorqueLimiter_autosar_rtw/TorqueLimiter.c diff --git a/tests/fixtures/demo/old/TorqueLimiter_autosar_rtw/TorqueLimiter.h b/tests/fixtures/demo/old/TorqueLimiter_autosar_rtw/TorqueLimiter.h new file mode 100644 index 0000000..9802428 --- /dev/null +++ b/tests/fixtures/demo/old/TorqueLimiter_autosar_rtw/TorqueLimiter.h @@ -0,0 +1,22 @@ +/* + * File: TorqueLimiter.h + * Code generated for Simulink model 'TorqueLimiter'. + */ + +#ifndef RTW_HEADER_TorqueLimiter_h_ +#define RTW_HEADER_TorqueLimiter_h_ + +#include "rtwtypes.h" + +typedef struct { + real_T PedalPosition; + real_T MotorSpeed; +} ExtU_TorqueLimiter_T; + +typedef struct { + real_T TorqueCmd; +} ExtY_TorqueLimiter_T; + +extern void Rte_Runnable_TorqueLimiter_Step(void); + +#endif diff --git a/tests/fixtures/demo/old/rules/src/rename_conflict.c b/tests/fixtures/demo/old/TorqueLimiter_autosar_rtw/TorqueLimiter_data.c similarity index 80% rename from tests/fixtures/demo/old/rules/src/rename_conflict.c rename to tests/fixtures/demo/old/TorqueLimiter_autosar_rtw/TorqueLimiter_data.c index 83b9caf..eebffba 100644 --- a/tests/fixtures/demo/old/rules/src/rename_conflict.c +++ b/tests/fixtures/demo/old/TorqueLimiter_autosar_rtw/TorqueLimiter_data.c @@ -1,4 +1,4 @@ -#include "rename_conflict.h" +#include "TorqueLimiter.h" void Conf_step(void) { diff --git a/tests/fixtures/demo/old/TorqueLimiter_autosar_rtw/TorqueLimiter_private.h b/tests/fixtures/demo/old/TorqueLimiter_autosar_rtw/TorqueLimiter_private.h new file mode 100644 index 0000000..5b4e8a7 --- /dev/null +++ b/tests/fixtures/demo/old/TorqueLimiter_autosar_rtw/TorqueLimiter_private.h @@ -0,0 +1,14 @@ +/* + * File: TorqueLimiter_private.h + * + * Code generated for Simulink model 'TorqueLimiter'. + */ + +#ifndef RTW_HEADER_TorqueLimiter_private_h_ +#define RTW_HEADER_TorqueLimiter_private_h_ + +#include "rtwtypes.h" + +extern void TorqueLimiter_initialize(void); + +#endif /* RTW_HEADER_TorqueLimiter_private_h_ */ diff --git a/tests/fixtures/demo/old/TorqueLimiter_autosar_rtw/TorqueLimiter_types.h b/tests/fixtures/demo/old/TorqueLimiter_autosar_rtw/TorqueLimiter_types.h new file mode 100644 index 0000000..64d5931 --- /dev/null +++ b/tests/fixtures/demo/old/TorqueLimiter_autosar_rtw/TorqueLimiter_types.h @@ -0,0 +1,14 @@ +/* + * File: TorqueLimiter_types.h + * + * Code generated for Simulink model 'TorqueLimiter'. + */ + +#ifndef RTW_HEADER_TorqueLimiter_types_h_ +#define RTW_HEADER_TorqueLimiter_types_h_ + +#include "rtwtypes.h" + +typedef struct P_TorqueLimiter_T_ P_TorqueLimiter_T; + +#endif /* RTW_HEADER_TorqueLimiter_types_h_ */ diff --git a/tests/fixtures/demo/old/TorqueLimiter_autosar_rtw/rtwtypes.h b/tests/fixtures/demo/old/TorqueLimiter_autosar_rtw/rtwtypes.h new file mode 100644 index 0000000..381ed05 --- /dev/null +++ b/tests/fixtures/demo/old/TorqueLimiter_autosar_rtw/rtwtypes.h @@ -0,0 +1,28 @@ +/* + * rtwtypes.h + * + * Code generation for model shared utilities. + */ + +#ifndef RTWTYPES_H +#define RTWTYPES_H + +typedef signed char int8_T; +typedef unsigned char uint8_T; +typedef short int16_T; +typedef unsigned short uint16_T; +typedef int int32_T; +typedef unsigned int uint32_T; +typedef float real32_T; +typedef double real_T; +typedef unsigned char boolean_T; + +#ifndef TRUE +#define TRUE (1U) +#endif + +#ifndef FALSE +#define FALSE (0U) +#endif + +#endif /* RTWTYPES_H */ diff --git a/tests/fixtures/demo/old/rules/a2l/comment_only.a2l b/tests/fixtures/demo/old/a2l/Ctrl.a2l similarity index 100% rename from tests/fixtures/demo/old/rules/a2l/comment_only.a2l rename to tests/fixtures/demo/old/a2l/Ctrl.a2l diff --git a/tests/fixtures/demo/old/rules/a2l/cal.a2l b/tests/fixtures/demo/old/a2l/NoiseDemo.a2l similarity index 100% rename from tests/fixtures/demo/old/rules/a2l/cal.a2l rename to tests/fixtures/demo/old/a2l/NoiseDemo.a2l diff --git a/tests/fixtures/demo/old/PedalMap.a2l b/tests/fixtures/demo/old/a2l/PedalMap.a2l similarity index 100% rename from tests/fixtures/demo/old/PedalMap.a2l rename to tests/fixtures/demo/old/a2l/PedalMap.a2l diff --git a/tests/fixtures/demo/old/StaleGen.a2l b/tests/fixtures/demo/old/a2l/StaleGen.a2l similarity index 100% rename from tests/fixtures/demo/old/StaleGen.a2l rename to tests/fixtures/demo/old/a2l/StaleGen.a2l diff --git a/tests/fixtures/demo/old/models/Ctrl_component.arxml b/tests/fixtures/demo/old/arxml/Ctrl_component.arxml similarity index 100% rename from tests/fixtures/demo/old/models/Ctrl_component.arxml rename to tests/fixtures/demo/old/arxml/Ctrl_component.arxml diff --git a/tests/fixtures/demo/old/rules/arxml/uuid_only.arxml b/tests/fixtures/demo/old/arxml/NoiseDemo_component.arxml similarity index 100% rename from tests/fixtures/demo/old/rules/arxml/uuid_only.arxml rename to tests/fixtures/demo/old/arxml/NoiseDemo_component.arxml diff --git a/tests/fixtures/demo/old/rules/arxml/real_change.arxml b/tests/fixtures/demo/old/arxml/NoiseDemo_datatype.arxml similarity index 100% rename from tests/fixtures/demo/old/rules/arxml/real_change.arxml rename to tests/fixtures/demo/old/arxml/NoiseDemo_datatype.arxml diff --git a/tests/fixtures/demo/old/rules/arxml/admindata.arxml b/tests/fixtures/demo/old/arxml/NoiseDemo_implementation.arxml similarity index 100% rename from tests/fixtures/demo/old/rules/arxml/admindata.arxml rename to tests/fixtures/demo/old/arxml/NoiseDemo_implementation.arxml diff --git a/tests/fixtures/demo/old/rules/arxml/iface.arxml b/tests/fixtures/demo/old/arxml/NoiseDemo_interface.arxml similarity index 100% rename from tests/fixtures/demo/old/rules/arxml/iface.arxml rename to tests/fixtures/demo/old/arxml/NoiseDemo_interface.arxml diff --git a/tests/fixtures/demo/old/PedalMap.arxml b/tests/fixtures/demo/old/arxml/PedalMap_component.arxml similarity index 100% rename from tests/fixtures/demo/old/PedalMap.arxml rename to tests/fixtures/demo/old/arxml/PedalMap_component.arxml diff --git a/tests/fixtures/demo/old/SpeedCtrl.arxml b/tests/fixtures/demo/old/arxml/SpeedCtrl_component.arxml similarity index 100% rename from tests/fixtures/demo/old/SpeedCtrl.arxml rename to tests/fixtures/demo/old/arxml/SpeedCtrl_component.arxml diff --git a/tests/fixtures/demo/old/StaleGen.arxml b/tests/fixtures/demo/old/arxml/StaleGen_component.arxml similarity index 100% rename from tests/fixtures/demo/old/StaleGen.arxml rename to tests/fixtures/demo/old/arxml/StaleGen_component.arxml diff --git a/tests/fixtures/demo/old/TorqueLimiter.arxml b/tests/fixtures/demo/old/arxml/TorqueLimiter_component.arxml similarity index 100% rename from tests/fixtures/demo/old/TorqueLimiter.arxml rename to tests/fixtures/demo/old/arxml/TorqueLimiter_component.arxml diff --git a/tests/fixtures/demo/old/models/rtwtypes.h b/tests/fixtures/demo/old/models/rtwtypes.h deleted file mode 100644 index 8362173..0000000 --- a/tests/fixtures/demo/old/models/rtwtypes.h +++ /dev/null @@ -1,4 +0,0 @@ -#ifndef RTWTYPES_H -#define RTWTYPES_H -typedef unsigned char boolean_T; -#endif diff --git a/tests/fixtures/demo/old/rules/src/comment_only.c b/tests/fixtures/demo/old/rules/src/comment_only.c deleted file mode 100644 index 329aa4a..0000000 --- a/tests/fixtures/demo/old/rules/src/comment_only.c +++ /dev/null @@ -1,15 +0,0 @@ -/* - * File: comment_only.c - * Code generated for Simulink model 'Model'. - * Model version : 1.42 - * Simulink Coder version : 9.8 (R2023a) 19-Nov-2022 - * C/C++ source code generated on : Mon Jan 05 10:12:33 2026 - */ -#include "comment_only.h" - -/* Model step function */ -void Model_step(void) -{ - /* Outport: '/Out1' */ - rtY.Out1 = rtU.In1 * 2.0; -} diff --git a/tests/fixtures/demo/old/rules/src/deleted.h b/tests/fixtures/demo/old/rules/src/deleted.h deleted file mode 100644 index f0e6007..0000000 --- a/tests/fixtures/demo/old/rules/src/deleted.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef DELETED_H -#define DELETED_H - -extern void Old_step(void); - -#endif diff --git a/tests/fixtures/demo/old/rules/src/real_change.c b/tests/fixtures/demo/old/rules/src/real_change.c deleted file mode 100644 index 2e796cb..0000000 --- a/tests/fixtures/demo/old/rules/src/real_change.c +++ /dev/null @@ -1,12 +0,0 @@ -/* Generated on : Mon Jan 05 10:12:33 2026 */ -#include "real_change.h" - -void Calc_step(void) -{ - /* saturation limit */ - if (rtU.In1 > 5) { - rtY.Out1 = 5; - } else { - rtY.Out1 = rtU.In1; - } -} diff --git a/tests/fixtures/demo/old/rules/src/same.h b/tests/fixtures/demo/old/rules/src/same.h deleted file mode 100644 index a3d7f02..0000000 --- a/tests/fixtures/demo/old/rules/src/same.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef SAME_H -#define SAME_H - -extern void Sub_step(void); - -#endif diff --git a/tests/fixtures/model_new/Ctrl.c b/tests/fixtures/model_new/Ctrl.c deleted file mode 100644 index a89f768..0000000 --- a/tests/fixtures/model_new/Ctrl.c +++ /dev/null @@ -1,10 +0,0 @@ -/* Model step function */ -#include "Ctrl.h" - -void Ctrl_Step(void) -{ - Float32 u; - (void) Rte_Read_In1_Speed(&u); - (void) Rte_Write_Out1_Cmd(u * 3.0F); - (void) Rte_Write_Out2_Diag(1U); -} diff --git a/tests/fixtures/model_new/Ctrl.h b/tests/fixtures/model_new/Ctrl.h deleted file mode 100644 index 9118c98..0000000 --- a/tests/fixtures/model_new/Ctrl.h +++ /dev/null @@ -1,5 +0,0 @@ -#ifndef CTRL_H -#define CTRL_H -#include "rtwtypes.h" -void Ctrl_Step(void); -#endif diff --git a/tests/fixtures/model_new/Ctrl_component.arxml b/tests/fixtures/model_new/Ctrl_component.arxml deleted file mode 100644 index 2675647..0000000 --- a/tests/fixtures/model_new/Ctrl_component.arxml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - Components - - - Ctrl - - - In1 - /Interfaces/If_Speed - - - Out1 - /Interfaces/If_Cmd - - - Out2 - /Interfaces/If_Diag - - - - - IB - - - TE_Step - /Components/Ctrl/IB/Ctrl_Step - 0.02 - - - - - Ctrl_Step - Ctrl_Step - - - - - - - - - diff --git a/tests/fixtures/model_new/Ctrl_types.h b/tests/fixtures/model_new/Ctrl_types.h deleted file mode 100644 index 2c62bad..0000000 --- a/tests/fixtures/model_new/Ctrl_types.h +++ /dev/null @@ -1,4 +0,0 @@ -#ifndef CTRL_TYPES_H -#define CTRL_TYPES_H -typedef float Float32; -#endif diff --git a/tests/fixtures/model_new/rtwtypes.h b/tests/fixtures/model_new/rtwtypes.h deleted file mode 100644 index 8362173..0000000 --- a/tests/fixtures/model_new/rtwtypes.h +++ /dev/null @@ -1,4 +0,0 @@ -#ifndef RTWTYPES_H -#define RTWTYPES_H -typedef unsigned char boolean_T; -#endif diff --git a/tests/fixtures/model_old/Ctrl.c b/tests/fixtures/model_old/Ctrl.c deleted file mode 100644 index 490be36..0000000 --- a/tests/fixtures/model_old/Ctrl.c +++ /dev/null @@ -1,9 +0,0 @@ -/* Model step function */ -#include "Ctrl.h" - -void Ctrl_Step(void) -{ - Float32 u; - (void) Rte_Read_In1_Speed(&u); - (void) Rte_Write_Out1_Cmd(u * 2.0F); -} diff --git a/tests/fixtures/model_old/Ctrl.h b/tests/fixtures/model_old/Ctrl.h deleted file mode 100644 index 9118c98..0000000 --- a/tests/fixtures/model_old/Ctrl.h +++ /dev/null @@ -1,5 +0,0 @@ -#ifndef CTRL_H -#define CTRL_H -#include "rtwtypes.h" -void Ctrl_Step(void); -#endif diff --git a/tests/fixtures/model_old/Ctrl_component.arxml b/tests/fixtures/model_old/Ctrl_component.arxml deleted file mode 100644 index ac6153a..0000000 --- a/tests/fixtures/model_old/Ctrl_component.arxml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - Components - - - Ctrl - - - In1 - /Interfaces/If_Speed - - - Out1 - /Interfaces/If_Cmd - - - - - IB - - - TE_Step - /Components/Ctrl/IB/Ctrl_Step - 0.01 - - - - - Ctrl_Step - Ctrl_Step - - - - - - - - - diff --git a/tests/fixtures/model_old/Ctrl_types.h b/tests/fixtures/model_old/Ctrl_types.h deleted file mode 100644 index 2c62bad..0000000 --- a/tests/fixtures/model_old/Ctrl_types.h +++ /dev/null @@ -1,4 +0,0 @@ -#ifndef CTRL_TYPES_H -#define CTRL_TYPES_H -typedef float Float32; -#endif diff --git a/tests/fixtures/model_old/rtwtypes.h b/tests/fixtures/model_old/rtwtypes.h deleted file mode 100644 index 8362173..0000000 --- a/tests/fixtures/model_old/rtwtypes.h +++ /dev/null @@ -1,4 +0,0 @@ -#ifndef RTWTYPES_H -#define RTWTYPES_H -typedef unsigned char boolean_T; -#endif diff --git a/tests/fixtures/new/a2l/cal.a2l b/tests/fixtures/new/a2l/cal.a2l deleted file mode 100644 index 60ec067..0000000 --- a/tests/fixtures/new/a2l/cal.a2l +++ /dev/null @@ -1,12 +0,0 @@ -/* generated by demo toolchain -- Tue Feb 17 2026 */ -ASAP2_VERSION 1 71 -/begin PROJECT Demo "" - /begin MODULE Ctrl "" - /begin MEASUREMENT EngSpd "engine speed" UWORD CM_EngSpd 1 100 0 8000 - ECU_ADDRESS 0x40001000 - /end MEASUREMENT - /begin MEASUREMENT VehSpd "vehicle speed" UWORD CM_VehSpd 1 100 0 300 - ECU_ADDRESS 0x40001004 - /end MEASUREMENT - /end MODULE -/end PROJECT diff --git a/tests/fixtures/new/a2l/comment_only.a2l b/tests/fixtures/new/a2l/comment_only.a2l deleted file mode 100644 index 1152b0f..0000000 --- a/tests/fixtures/new/a2l/comment_only.a2l +++ /dev/null @@ -1,8 +0,0 @@ -/* generated Tue */ -ASAP2_VERSION 1 71 -/begin PROJECT Demo "" - /begin MODULE Ctrl "" - /begin MEASUREMENT EngSpd "engine speed" UWORD CM_EngSpd 1 100 0 8000 - /end MEASUREMENT - /end MODULE -/end PROJECT diff --git a/tests/fixtures/new/arxml/admindata.arxml b/tests/fixtures/new/arxml/admindata.arxml deleted file mode 100644 index 01e9734..0000000 --- a/tests/fixtures/new/arxml/admindata.arxml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - 9.9 - 2026-02-17T08:45:01 - - - - - - Interfaces - - - diff --git a/tests/fixtures/new/arxml/iface.arxml b/tests/fixtures/new/arxml/iface.arxml deleted file mode 100644 index d48f7a8..0000000 --- a/tests/fixtures/new/arxml/iface.arxml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - Interfaces - - - If_Speed - - - Speed - - - - - If_Torque - - - Torque - - - - - - - diff --git a/tests/fixtures/new/arxml/real_change.arxml b/tests/fixtures/new/arxml/real_change.arxml deleted file mode 100644 index 60d177a..0000000 --- a/tests/fixtures/new/arxml/real_change.arxml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - DataTypes - - - Velocity_T - - - - - diff --git a/tests/fixtures/new/arxml/uuid_only.arxml b/tests/fixtures/new/arxml/uuid_only.arxml deleted file mode 100644 index ee1b464..0000000 --- a/tests/fixtures/new/arxml/uuid_only.arxml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - ComponentTypes - - - Controller - - - Out1 - - - - - - - diff --git a/tests/fixtures/new/src/added.c b/tests/fixtures/new/src/added.c deleted file mode 100644 index 6855653..0000000 --- a/tests/fixtures/new/src/added.c +++ /dev/null @@ -1,6 +0,0 @@ -#include "added.h" - -void New_step(void) -{ - rtY.Out9 = 0.0; -} diff --git a/tests/fixtures/new/src/rename_conflict.c b/tests/fixtures/new/src/rename_conflict.c deleted file mode 100644 index 0fa12a1..0000000 --- a/tests/fixtures/new/src/rename_conflict.c +++ /dev/null @@ -1,10 +0,0 @@ -#include "rename_conflict.h" - -void Conf_step(void) -{ - real_T rtb_B; - - rtb_B = rtU.In1 * 2.0; - rtY.Out1 = rtb_B + 1.0; - rtY.Out2 = rtb_C + 2.0; -} diff --git a/tests/fixtures/new/src/rename_only.c b/tests/fixtures/new/src/rename_only.c deleted file mode 100644 index 740171a..0000000 --- a/tests/fixtures/new/src/rename_only.c +++ /dev/null @@ -1,15 +0,0 @@ -#include "rename_only.h" - -void Sub_step(void) -{ - real_T rtb_Sum_k2j; - real_T rtb_Gain_p0f; - - rtb_Sum_k2j = rtU.In1 + rtU.In2; - rtb_Gain_p0f = rtb_Sum_k2j * 3.5; - if (rtb_Sum_k2j > 0.0) { - rtY.Out1 = rtb_Gain_p0f; - } else { - rtY.Out1 = rtb_Sum_k2j; - } -} diff --git a/tests/fixtures/new/src/same.h b/tests/fixtures/new/src/same.h deleted file mode 100644 index a3d7f02..0000000 --- a/tests/fixtures/new/src/same.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef SAME_H -#define SAME_H - -extern void Sub_step(void); - -#endif diff --git a/tests/fixtures/old/a2l/cal.a2l b/tests/fixtures/old/a2l/cal.a2l deleted file mode 100644 index be9196c..0000000 --- a/tests/fixtures/old/a2l/cal.a2l +++ /dev/null @@ -1,13 +0,0 @@ -/* generated by demo toolchain -- Mon Jan 05 2026 */ -ASAP2_VERSION 1 71 -/begin PROJECT Demo "" - /begin MODULE Ctrl "" - /begin MEASUREMENT EngSpd "engine speed" UWORD CM_EngSpd 1 100 0 8000 - ECU_ADDRESS 0x40001000 - /end MEASUREMENT - /begin CHARACTERISTIC K_Gain "controller gain" VALUE 0x80001000 __Scalar 100 CM_Gain 0 10 - /begin IF_DATA XCP - /end IF_DATA - /end CHARACTERISTIC - /end MODULE -/end PROJECT diff --git a/tests/fixtures/old/a2l/comment_only.a2l b/tests/fixtures/old/a2l/comment_only.a2l deleted file mode 100644 index 6f9b37d..0000000 --- a/tests/fixtures/old/a2l/comment_only.a2l +++ /dev/null @@ -1,8 +0,0 @@ -/* generated Mon */ -ASAP2_VERSION 1 71 -/begin PROJECT Demo "" - /begin MODULE Ctrl "" - /begin MEASUREMENT EngSpd "engine speed" UWORD CM_EngSpd 1 100 0 8000 - /end MEASUREMENT - /end MODULE -/end PROJECT diff --git a/tests/fixtures/old/arxml/admindata.arxml b/tests/fixtures/old/arxml/admindata.arxml deleted file mode 100644 index 0a3593f..0000000 --- a/tests/fixtures/old/arxml/admindata.arxml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - 9.8 - 2026-01-05T10:12:33 - - - - - - Interfaces - - - diff --git a/tests/fixtures/old/arxml/iface.arxml b/tests/fixtures/old/arxml/iface.arxml deleted file mode 100644 index 0d2f4ad..0000000 --- a/tests/fixtures/old/arxml/iface.arxml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - Interfaces - - - If_Speed - - - Speed - - - - - If_Diag - - - ReadDtc - - - - - - - diff --git a/tests/fixtures/old/arxml/real_change.arxml b/tests/fixtures/old/arxml/real_change.arxml deleted file mode 100644 index 6a8ea65..0000000 --- a/tests/fixtures/old/arxml/real_change.arxml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - DataTypes - - - Speed_T - - - - - diff --git a/tests/fixtures/old/arxml/uuid_only.arxml b/tests/fixtures/old/arxml/uuid_only.arxml deleted file mode 100644 index cb0fc1b..0000000 --- a/tests/fixtures/old/arxml/uuid_only.arxml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - ComponentTypes - - - Controller - - - Out1 - - - - - - - diff --git a/tests/fixtures/old/src/deleted.h b/tests/fixtures/old/src/deleted.h deleted file mode 100644 index f0e6007..0000000 --- a/tests/fixtures/old/src/deleted.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef DELETED_H -#define DELETED_H - -extern void Old_step(void); - -#endif diff --git a/tests/fixtures/old/src/rename_conflict.c b/tests/fixtures/old/src/rename_conflict.c deleted file mode 100644 index 83b9caf..0000000 --- a/tests/fixtures/old/src/rename_conflict.c +++ /dev/null @@ -1,10 +0,0 @@ -#include "rename_conflict.h" - -void Conf_step(void) -{ - real_T rtb_A; - - rtb_A = rtU.In1 * 2.0; - rtY.Out1 = rtb_A + 1.0; - rtY.Out2 = rtb_A + 2.0; -} diff --git a/tests/fixtures/old/src/rename_only.c b/tests/fixtures/old/src/rename_only.c deleted file mode 100644 index 6436f05..0000000 --- a/tests/fixtures/old/src/rename_only.c +++ /dev/null @@ -1,15 +0,0 @@ -#include "rename_only.h" - -void Sub_step(void) -{ - real_T rtb_Sum1; - real_T rtb_Gain2; - - rtb_Sum1 = rtU.In1 + rtU.In2; - rtb_Gain2 = rtb_Sum1 * 3.5; - if (rtb_Sum1 > 0.0) { - rtY.Out1 = rtb_Gain2; - } else { - rtY.Out1 = rtb_Sum1; - } -} diff --git a/tests/fixtures/old/src/same.h b/tests/fixtures/old/src/same.h deleted file mode 100644 index a3d7f02..0000000 --- a/tests/fixtures/old/src/same.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef SAME_H -#define SAME_H - -extern void Sub_step(void); - -#endif diff --git a/tests/test_demo.py b/tests/test_demo.py index cb9b0d3..fa7fbfc 100644 --- a/tests/test_demo.py +++ b/tests/test_demo.py @@ -1,16 +1,19 @@ -"""The demo tree under fixtures/demo: one folder pair covering every noise rule -and the three newest features in a single compare. - -`fixtures/demo/old` vs `fixtures/demo/new` is the pair a human runs (see -fixtures/demo/README.md). Four top-level models make the newest features' -point; `rules/` and `models/` are copies of the tool's own noise-rule and -model-grouping fixtures, folded in so the same one compare also shows every -ignorable kind (comment, uuid, timestamp, rename), an added and a deleted file, -side by side with what is real. Copies, not moves -- `tests/fixtures/old`, -`new`, `model_old` and `model_new` stay put, since other tests pin exact -counts and paths against them. - -These tests lock what the merged demo claims, so it can never quietly stop +"""The demo tree under fixtures/demo: the suite's only fixture pair, laid out +the way an Embedded Coder AUTOSAR export actually lands on disk. + +`fixtures/demo/old` vs `fixtures/demo/new` is both what the tests scan and what +a human runs (see fixtures/demo/README.md). Each model owns a +`_autosar_rtw/` folder of generated C, the ARXML export sits under +`arxml/` and the calibration files under `a2l/` -- so a path in an assertion +below is a path a reviewer would really see. + +Six models carry the whole matrix between them: SpeedCtrl (reorder is noise), +StaleGen (surfaces moved, code did not), TorqueLimiter (code-only change), +PedalMap (everything moved together), Ctrl (+RTE while a peer stayed +identical), and NoiseDemo (every ignorable kind, plus an added and a deleted +file). + +These tests lock what the demo claims, so it can never quietly stop demonstrating what it says it does.""" import json @@ -36,7 +39,7 @@ def setUpClass(cls): # --- feature 1: provably-safe statement reorder folds to noise --- def test_reorder_folds_speedctrl_to_unimportant(self): - r = self.res['SpeedCtrl.c'] + r = self.res['SpeedCtrl_autosar_rtw/SpeedCtrl.c'] self.assertEqual(r['status'], 'ignorable-only') self.assertIn('reorder', _kinds(r)) self.assertNotIn('real', _kinds(r)) @@ -57,32 +60,41 @@ def test_consistency_advisories_name_the_out_of_step_models(self): self.assertEqual(set(by_model), {'StaleGen', 'Ctrl'}) def test_stale_model_verdicts_drive_the_flag(self): - self.assertEqual(self.res['StaleGen.arxml']['status'], 'real-change') - self.assertEqual(self.res['StaleGen.a2l']['status'], 'real-change') - self.assertEqual(self.res['StaleGen.c']['status'], 'identical') + self.assertEqual(self.res['arxml/StaleGen_component.arxml']['status'], + 'real-change') + self.assertEqual(self.res['a2l/StaleGen.a2l']['status'], 'real-change') + self.assertEqual(self.res['StaleGen_autosar_rtw/StaleGen.c']['status'], + 'identical') def test_code_only_change_is_not_flagged(self): # TorqueLimiter's C changed (a gain) but its ARXML did not -- a logic # edit touches no interface, so this is normal and must NOT be flagged - self.assertEqual(self.res['TorqueLimiter.c']['status'], 'real-change') - self.assertEqual(self.res['TorqueLimiter.arxml']['status'], 'identical') + self.assertEqual( + self.res['TorqueLimiter_autosar_rtw/TorqueLimiter.c']['status'], + 'real-change') + self.assertEqual( + self.res['arxml/TorqueLimiter_component.arxml']['status'], + 'identical') self.assertNotIn('TorqueLimiter', [m for m, _ in consistency_advisories(self.res)]) def test_surfaces_and_code_changing_together_is_quiet(self): # PedalMap changed its C, its ARXML (a new port) and its A2L together - self.assertEqual(self.res['PedalMap.c']['status'], 'real-change') - self.assertEqual(self.res['PedalMap.arxml']['status'], 'real-change') - self.assertEqual(self.res['PedalMap.a2l']['status'], 'real-change') + self.assertEqual(self.res['PedalMap_autosar_rtw/PedalMap.c']['status'], + 'real-change') + self.assertEqual(self.res['arxml/PedalMap_component.arxml']['status'], + 'real-change') + self.assertEqual(self.res['a2l/PedalMap.a2l']['status'], 'real-change') self.assertNotIn('PedalMap', [m for m, _ in consistency_advisories(self.res)]) def test_autosar_summary_sees_the_new_objects(self): swc = summarize_swcs(self.res) ports = [(rel, name) for rel, _swc, name, _desc in swc['ports']['added']] - self.assertIn(('PedalMap.arxml', 'Scaled'), ports) + self.assertIn(('arxml/PedalMap_component.arxml', 'Scaled'), ports) added, _removed = summarize_a2l(self.res) - self.assertIn(('PedalMap.a2l', 'K_PedalOffset', 'CHARACTERISTIC'), added) + self.assertIn(('a2l/PedalMap.a2l', 'K_PedalOffset', 'CHARACTERISTIC'), + added) # --- feature 5: machine-readable output --- @@ -92,13 +104,20 @@ def test_sarif_lists_only_actionable_files(self): for r in log['runs'][0]['results']} # the reordered file, the stale (identical) C, and any Unimportant / # Comment file are NOT findings - for rel in ('SpeedCtrl.c', 'StaleGen.c', 'rules/arxml/uuid_only.arxml', - 'rules/src/comment_only.c', 'rules/src/rename_only.c'): + for rel in ('SpeedCtrl_autosar_rtw/SpeedCtrl.c', + 'StaleGen_autosar_rtw/StaleGen.c', + 'arxml/NoiseDemo_component.arxml', + 'NoiseDemo_autosar_rtw/ert_main.c', + 'NoiseDemo_autosar_rtw/NoiseDemo_data.c'): self.assertNotIn(rel, uris) - for rel in ('TorqueLimiter.c', 'PedalMap.c', 'PedalMap.arxml', - 'PedalMap.a2l', 'StaleGen.arxml', 'StaleGen.a2l', - 'rules/src/added.c', 'rules/src/deleted.h', - 'rules/src/real_change.c'): + for rel in ('TorqueLimiter_autosar_rtw/TorqueLimiter.c', + 'PedalMap_autosar_rtw/PedalMap.c', + 'arxml/PedalMap_component.arxml', + 'a2l/PedalMap.a2l', 'arxml/StaleGen_component.arxml', + 'a2l/StaleGen.a2l', + 'SpeedCtrl_autosar_rtw/SpeedCtrl_data.c', + 'NoiseDemo_autosar_rtw/NoiseDemo_types.h', + 'NoiseDemo_autosar_rtw/NoiseDemo.c'): self.assertIn(rel, uris) # --- one compare, every noise kind --- @@ -113,18 +132,24 @@ def test_added_and_deleted_are_represented(self): self.assertIn('added', statuses) self.assertIn('deleted', statuses) - def test_model_grouping_still_separates_the_four_demo_models(self): + def test_model_grouping_separates_every_demo_model(self): + # grouping keys off the file stem, so a model's C, its ARXML under + # arxml/ and its A2L under a2l/ land together despite the folders from compare_tool.report import _model_groups groups = _model_groups(self.res) - for model in ('SpeedCtrl', 'StaleGen', 'TorqueLimiter', 'PedalMap'): + for model in ('SpeedCtrl', 'StaleGen', 'TorqueLimiter', 'PedalMap', + 'Ctrl', 'NoiseDemo'): self.assertIn(model, groups) + self.assertIn('arxml/StaleGen_component.arxml', groups['StaleGen']) + self.assertIn('a2l/StaleGen.a2l', groups['StaleGen']) def test_json_round_trips_and_carries_the_reorder(self): counts = {k: 0 for k in ('identical', 'comment-only', 'ignorable-only', 'real-change', 'added', 'deleted', 'error')} text = serialize.dumps(self.res, counts, 'old', 'new', 1) doc = json.loads(text) - speed = next(f for f in doc['files'] if f['path'] == 'SpeedCtrl.c') + speed = next(f for f in doc['files'] + if f['path'] == 'SpeedCtrl_autosar_rtw/SpeedCtrl.c') self.assertEqual(speed['status'], 'ignorable-only') self.assertIn('reorder', {h['kind'] for h in speed['hunks']}) diff --git a/tests/test_diffpane_qt.py b/tests/test_diffpane_qt.py index df7a6b6..d4a5161 100644 --- a/tests/test_diffpane_qt.py +++ b/tests/test_diffpane_qt.py @@ -20,6 +20,7 @@ from compare_tool.scanner import scan FIX = Path(__file__).parent / 'fixtures' +DEMO = FIX / 'demo' _APP = None @@ -77,13 +78,13 @@ class TestCaretDoesNotPaint(unittest.TestCase): say by accident. """ - REL = 'a2l/comment_only.a2l' - OTHER = 'a2l/cal.a2l' + REL = 'a2l/Ctrl.a2l' + OTHER = 'a2l/NoiseDemo.a2l' def setUp(self): from compare_tool.qtviewer.diffpane import DiffPane self.app = _app() - self.results = scan(FIX / 'old', FIX / 'new') + self.results = scan(DEMO / 'old', DEMO / 'new') self.pane = DiffPane() self.pane.resize(1200, 600) self.pane.setAttribute(Qt.WA_DontShowOnScreen, True) @@ -92,7 +93,7 @@ def setUp(self): def _open(self, rel): self.pane.show_file(rel, self.results[rel], - str(FIX / 'old'), str(FIX / 'new')) + str(DEMO / 'old'), str(DEMO / 'new')) for _ in range(5): self.app.processEvents() @@ -135,7 +136,7 @@ class TestCurrentFunctionCaption(unittest.TestCase): def setUp(self): from compare_tool.qtviewer.diffpane import DiffPane self.app = _app() - self.results = scan(FIX / 'old', FIX / 'new') + self.results = scan(DEMO / 'old', DEMO / 'new') self.pane = DiffPane() self.pane.resize(1000, 600) self.pane.setAttribute(Qt.WA_DontShowOnScreen, True) @@ -144,32 +145,32 @@ def setUp(self): def _open(self, rel): self.pane.show_file(rel, self.results[rel], - str(FIX / 'old'), str(FIX / 'new')) + str(DEMO / 'old'), str(DEMO / 'new')) for _ in range(5): self.app.processEvents() def test_opens_on_the_changed_functions_scope(self): - # the real change in real_change.c is inside Calc_step, so the file + # the real change in NoiseDemo.c is inside Calc_step, so the file # opens with that named in the caption -- not the banner line above it - self._open('src/real_change.c') + self._open('NoiseDemo_autosar_rtw/NoiseDemo.c') self.assertIn('Calc_step', self.pane._fn.text()) self.assertTrue(self.pane._fn.isVisible()) def test_row_labels_align_with_rows(self): - self._open('src/real_change.c') + self._open('NoiseDemo_autosar_rtw/NoiseDemo.c') self.assertEqual(len(self.pane._row_fn), len(self.pane.rows)) self.assertIn('Calc_step', self.pane._row_fn) def test_one_sided_file_is_captioned_too(self): # a whole added file still has a scope: New_step - self._open('src/added.c') - self.assertIn('New_step', self.pane._row_fn) + self._open('SpeedCtrl_autosar_rtw/SpeedCtrl_data.c') + self.assertIn('SpeedCtrl_initialize', self.pane._row_fn) def test_caption_clears_between_files(self): - # deleted.h holds only a declaration -- no function body -- so its + # NoiseDemo_types.h holds only a typedef -- no function body -- so its # caption must not keep the previous file's function name - self._open('src/real_change.c') - self._open('src/deleted.h') + self._open('NoiseDemo_autosar_rtw/NoiseDemo.c') + self._open('NoiseDemo_autosar_rtw/NoiseDemo_types.h') self.assertNotIn('Calc_step', self.pane._fn.text()) @@ -180,7 +181,7 @@ class TestMinimapOnOneSidedFiles(unittest.TestCase): def setUp(self): from compare_tool.qtviewer.diffpane import DiffPane self.app = _app() - self.results = scan(FIX / 'old', FIX / 'new') + self.results = scan(DEMO / 'old', DEMO / 'new') self.pane = DiffPane() self.pane.resize(1200, 500) self.pane.setAttribute(Qt.WA_DontShowOnScreen, True) @@ -189,12 +190,12 @@ def setUp(self): def _open(self, rel): self.pane.show_file(rel, self.results[rel], - str(FIX / 'old'), str(FIX / 'new')) + str(DEMO / 'old'), str(DEMO / 'new')) for _ in range(5): self.app.processEvents() def test_added_and_deleted_files_have_a_map(self): - for rel, side in (('src/added.c', 'new'), ('src/deleted.h', 'old')): + for rel, side in (('SpeedCtrl_autosar_rtw/SpeedCtrl_data.c', 'new'), ('NoiseDemo_autosar_rtw/NoiseDemo_types.h', 'old')): self._open(rel) self.assertTrue(self.pane.minimap._rows, rel) # driven by the pane that holds the text, or the slider sits at the @@ -205,12 +206,12 @@ def test_added_and_deleted_files_have_a_map(self): def test_a_one_sided_map_carries_no_diff_colour(self): # the pane is already one solid colour; repeating it on the map would # be a rectangle carrying no information - self._open('src/added.c') + self._open('SpeedCtrl_autosar_rtw/SpeedCtrl_data.c') self.assertEqual({r.mode for r in self.pane.minimap._rows}, {'ctx'}) def test_two_pane_files_go_back_to_the_baseline_editor(self): - self._open('src/added.c') - self._open('src/real_change.c') + self._open('SpeedCtrl_autosar_rtw/SpeedCtrl_data.c') + self._open('NoiseDemo_autosar_rtw/NoiseDemo.c') self.assertIs(self.pane.minimap._editor, self.pane.old_edit) self.assertTrue(self.pane.minimap._rows) @@ -222,7 +223,7 @@ class TestStickyHeaderAndScrollbar(unittest.TestCase): def setUp(self): from compare_tool.qtviewer.diffpane import DiffPane self.app = _app() - self.results = scan(FIX / 'old', FIX / 'new') + self.results = scan(DEMO / 'old', DEMO / 'new') self.pane = DiffPane() self.pane.resize(1000, 400) self.pane.setAttribute(Qt.WA_DontShowOnScreen, True) @@ -231,12 +232,12 @@ def setUp(self): def _open(self, rel): self.pane.show_file(rel, self.results[rel], - str(FIX / 'old'), str(FIX / 'new')) + str(DEMO / 'old'), str(DEMO / 'new')) for _ in range(6): self.app.processEvents() def _long_c(self): - """A tall two-pane file: real_change.c is a handful of lines, so its + """A tall two-pane file: NoiseDemo.c is a handful of lines, so its function signature never scrolls off and the sticky would never show. Build one long enough that it does.""" tmp = Path(tempfile.mkdtemp()) @@ -250,7 +251,7 @@ def _long_c(self): return tmp def test_only_the_driving_pane_shows_a_vertical_scrollbar(self): - self._open('src/real_change.c') # two-pane: old drives + self._open('NoiseDemo_autosar_rtw/NoiseDemo.c') # two-pane: old drives self.assertEqual(self.pane.old_edit.verticalScrollBarPolicy(), Qt.ScrollBarAsNeeded) self.assertEqual(self.pane.new_edit.verticalScrollBarPolicy(), @@ -260,7 +261,7 @@ def test_added_file_shows_no_scrollbar_over_the_minimap(self): # a whole added file's text is on the NEW (right) pane, which drives -- # but the minimap sits immediately to its right, so a bar there would # collide with it. It stays off; the minimap, wheel and keyboard scroll. - self._open('src/added.c') + self._open('SpeedCtrl_autosar_rtw/SpeedCtrl_data.c') self.assertIs(self.pane._drive, self.pane.new_edit) self.assertEqual(self.pane.new_edit.verticalScrollBarPolicy(), Qt.ScrollBarAlwaysOff) @@ -431,10 +432,10 @@ def _check(self, old, new): 'row {!r} landed on line {}'.format(item.text(0), landed)) def test_every_row_lands_on_its_own_object(self): - self._check(FIX / 'old', FIX / 'new') + self._check(DEMO / 'old', DEMO / 'new') def test_every_row_lands_on_its_own_object_in_the_model_fixture(self): - self._check(FIX / 'model_old', FIX / 'model_new') + self._check(DEMO / 'old', DEMO / 'new') @unittest.skipUnless(HAVE_QT, 'PySide6 not installed') @@ -444,7 +445,7 @@ class TestFindInFile(unittest.TestCase): def setUp(self): from compare_tool.qtviewer.diffpane import DiffPane self.app = _app() - self.results = scan(FIX / 'old', FIX / 'new') + self.results = scan(DEMO / 'old', DEMO / 'new') self.pane = DiffPane() self.pane.resize(1200, 600) self.pane.setAttribute(Qt.WA_DontShowOnScreen, True) @@ -453,12 +454,12 @@ def setUp(self): def _open(self, rel): self.pane.show_file(rel, self.results[rel], - str(FIX / 'old'), str(FIX / 'new')) + str(DEMO / 'old'), str(DEMO / 'new')) for _ in range(5): self.app.processEvents() def test_a_hit_on_either_side_counts_once(self): - self._open('src/rename_conflict.c') + self._open('TorqueLimiter_autosar_rtw/TorqueLimiter_data.c') hits = self.pane.find_matches('rtb_') self.assertTrue(hits) for row in hits: @@ -468,7 +469,7 @@ def test_a_hit_on_either_side_counts_once(self): self.assertEqual(len(hits), len(set(hits))) def test_search_is_case_insensitive_and_empty_finds_nothing(self): - self._open('src/rename_conflict.c') + self._open('TorqueLimiter_autosar_rtw/TorqueLimiter_data.c') self.assertEqual(self.pane.find_matches('RTB_'), self.pane.find_matches('rtb_')) self.assertEqual(self.pane.find_matches(' '), []) @@ -476,12 +477,12 @@ def test_search_is_case_insensitive_and_empty_finds_nothing(self): def test_a_whole_added_file_is_searchable(self): # it renders through the one-sided path, which used to leave .rows # empty -- a file with no rows is a file the find box cannot see - self._open('src/added.c') + self._open('SpeedCtrl_autosar_rtw/SpeedCtrl_data.c') self.assertTrue(self.pane.rows) self.assertTrue(self.pane.find_matches('void')) def test_stepping_wraps_and_reports_position(self): - self._open('src/rename_conflict.c') + self._open('TorqueLimiter_autosar_rtw/TorqueLimiter_data.c') self.pane.open_find() self.pane._find_edit.setText('rtb_') for _ in range(5): @@ -506,7 +507,7 @@ def _type(self, text): def test_a_query_that_stops_matching_takes_its_highlights_with_it(self): # typing on past the last hit ("begin" -> "beginal") said "No match" # while the old word stayed lit, which reads as the wrong answer - self._open('a2l/comment_only.a2l') + self._open('a2l/Ctrl.a2l') self.pane.open_find() self._type('begin') self.assertGreater(self._match_marks(), 0) @@ -515,7 +516,7 @@ def test_a_query_that_stops_matching_takes_its_highlights_with_it(self): self.assertEqual(self._match_marks(), 0) def test_clearing_the_box_clears_the_marks(self): - self._open('a2l/comment_only.a2l') + self._open('a2l/Ctrl.a2l') self.pane.open_find() self._type('begin') self._type('') @@ -523,7 +524,7 @@ def test_clearing_the_box_clears_the_marks(self): self.assertEqual(self.pane._find_count.text(), '') def test_closing_the_bar_clears_the_marks(self): - self._open('a2l/comment_only.a2l') + self._open('a2l/Ctrl.a2l') self.pane.open_find() self._type('begin') self.pane.close_find() @@ -532,7 +533,7 @@ def test_closing_the_bar_clears_the_marks(self): self.assertEqual(self._match_marks(), 0) def test_every_occurrence_is_marked_not_only_the_current_one(self): - self._open('a2l/comment_only.a2l') + self._open('a2l/Ctrl.a2l') self.pane.open_find() self._type('begin') # one mark per occurrence per pane that has the row @@ -543,26 +544,26 @@ def test_the_current_change_marker_survives_a_cleared_search(self): # tracked separately; clearing the search used to wipe the block the # reviewer is standing on as well (back when both shared one # extraSelections list) - self._open('src/rename_conflict.c') + self._open('TorqueLimiter_autosar_rtw/TorqueLimiter_data.c') self.pane.open_find() self._type('rtb_') self._type('') self.assertTrue(self.pane.old_edit._cur_rows or self.pane.new_edit._cur_rows) def test_leaving_a_file_takes_its_marks_with_it(self): - self._open('src/rename_conflict.c') + self._open('TorqueLimiter_autosar_rtw/TorqueLimiter_data.c') self.pane.open_find() self._type('rtb_') - self._open('src/real_change.c') # no rtb_ in this one + self._open('NoiseDemo_autosar_rtw/NoiseDemo.c') # no rtb_ in this one self.assertEqual(self._match_marks(), 0) def test_the_query_survives_a_file_change_without_moving_the_pane(self): - self._open('src/rename_conflict.c') + self._open('TorqueLimiter_autosar_rtw/TorqueLimiter_data.c') self.pane.open_find() self.pane._find_edit.setText('rtb_') for _ in range(5): self.app.processEvents() - self._open('src/real_change.c') + self._open('NoiseDemo_autosar_rtw/NoiseDemo.c') self.assertEqual(self.pane._find_edit.text(), 'rtb_') # opening a file parks on its FIRST CHANGE; a query carried over from # another file must not quietly scroll somewhere else @@ -582,15 +583,15 @@ class TestChangeNavigationStopsAtTheEnd(unittest.TestCase): def setUp(self): from compare_tool.qtviewer.diffpane import DiffPane self.app = _app() - self.results = scan(FIX / 'old', FIX / 'new') + self.results = scan(DEMO / 'old', DEMO / 'new') self.pane = DiffPane() self.pane.resize(1200, 600) self.pane.setAttribute(Qt.WA_DontShowOnScreen, True) self.pane.show() self.addCleanup(self.pane.close) - self.pane.show_file('src/rename_conflict.c', - self.results['src/rename_conflict.c'], - str(FIX / 'old'), str(FIX / 'new')) + self.pane.show_file('TorqueLimiter_autosar_rtw/TorqueLimiter_data.c', + self.results['TorqueLimiter_autosar_rtw/TorqueLimiter_data.c'], + str(DEMO / 'old'), str(DEMO / 'new')) for _ in range(5): self.app.processEvents() @@ -607,10 +608,10 @@ def test_prev_stops_at_the_first_change(self): self.assertFalse(self.pane.prev_change()) def test_a_file_with_no_change_stops_never_claims_to_move(self): - # same.h is identical -- no hunks at all, real or noise -- so there is + # NoiseDemo_private.h is identical -- no hunks at all, real or noise -- # truly nothing to step to regardless of what is muted - self.pane.show_file('src/same.h', self.results['src/same.h'], - str(FIX / 'old'), str(FIX / 'new')) + self.pane.show_file('NoiseDemo_autosar_rtw/NoiseDemo_private.h', self.results['NoiseDemo_autosar_rtw/NoiseDemo_private.h'], + str(DEMO / 'old'), str(DEMO / 'new')) for _ in range(5): self.app.processEvents() self.assertFalse(self.pane.next_change()) @@ -621,8 +622,8 @@ def test_a_noise_only_file_is_still_navigable_while_unmuted(self): # (the default, nothing muted) they are themselves valid F7/F8 stops, # but landing on one offers nothing to sign off -- noise never enters # the review record (review.REVIEWABLE) - self.pane.show_file('src/rename_only.c', self.results['src/rename_only.c'], - str(FIX / 'old'), str(FIX / 'new')) + self.pane.show_file('NoiseDemo_autosar_rtw/NoiseDemo_data.c', self.results['NoiseDemo_autosar_rtw/NoiseDemo_data.c'], + str(DEMO / 'old'), str(DEMO / 'new')) for _ in range(5): self.app.processEvents() self.assertTrue(self.pane._stops) @@ -642,11 +643,11 @@ def test_stepping_moves_the_gutter_arrow_on_both_panes(self): def test_leaving_a_file_clears_the_arrow_it_left_behind(self): self.pane.first_change() self.assertTrue(self.pane.old_edit._cur_rows) - # same.h has no hunks at all -- the marker from the previous file + # NoiseDemo_private.h has no hunks at all -- the marker from the # must not still be sitting there once a file with nothing to mark # replaces it - self.pane.show_file('src/same.h', self.results['src/same.h'], - str(FIX / 'old'), str(FIX / 'new')) + self.pane.show_file('NoiseDemo_autosar_rtw/NoiseDemo_private.h', self.results['NoiseDemo_autosar_rtw/NoiseDemo_private.h'], + str(DEMO / 'old'), str(DEMO / 'new')) for _ in range(5): self.app.processEvents() self.assertEqual(self.pane.old_edit._cur_rows, frozenset()) @@ -660,7 +661,7 @@ class TestWindowLevelReviewFlow(unittest.TestCase): def setUp(self): from compare_tool.qtviewer.app import MainWindow self.app = _app() - self.win = MainWindow(str(FIX / 'old'), str(FIX / 'new')) + self.win = MainWindow(str(DEMO / 'old'), str(DEMO / 'new')) self.win.resize(1400, 800) self.win.setAttribute(Qt.WA_DontShowOnScreen, True) self.win.show() @@ -680,38 +681,38 @@ def test_a_finished_scan_opens_on_the_first_change(self): if self.win._is_nav(r))) def test_flipping_a_compare_rule_does_not_move_the_reviewer(self): - self.win._reselect('src/rename_conflict.c') + self.win._reselect('TorqueLimiter_autosar_rtw/TorqueLimiter_data.c') self._settle_ui() self.win.cb_comment.setChecked(False) self._settle_ui() - self.assertEqual(self.win._selected_rel(), 'src/rename_conflict.c') + self.assertEqual(self.win._selected_rel(), 'TorqueLimiter_autosar_rtw/TorqueLimiter_data.c') def test_comment_only_and_ignorable_only_files_are_walkable_by_default(self): - # comment_only.c / admindata.arxml keep their noise verdict while the + # ert_main.c / NoiseDemo_implementation.arxml keep their noise verdict # matching checkbox is ticked (the default) -- F7/F8 must walk into # them too, not stop at real-change / added / deleted / error alone - self.assertEqual(self.win.results['src/comment_only.c']['status'], + self.assertEqual(self.win.results['NoiseDemo_autosar_rtw/ert_main.c']['status'], 'comment-only') - self.assertTrue(self.win._is_nav('src/comment_only.c')) - self.assertEqual(self.win.results['arxml/admindata.arxml']['status'], + self.assertTrue(self.win._is_nav('NoiseDemo_autosar_rtw/ert_main.c')) + self.assertEqual(self.win.results['arxml/NoiseDemo_implementation.arxml']['status'], 'ignorable-only') - self.assertTrue(self.win._is_nav('arxml/admindata.arxml')) + self.assertTrue(self.win._is_nav('arxml/NoiseDemo_implementation.arxml')) def test_unticking_a_rule_folds_its_files_out_of_the_walk(self): - self.assertTrue(self.win._is_nav('src/comment_only.c')) + self.assertTrue(self.win._is_nav('NoiseDemo_autosar_rtw/ert_main.c')) self.win.cb_comment.setChecked(False) self._settle_ui() - self.assertEqual(self.win.results['src/comment_only.c']['status'], 'identical') - self.assertFalse(self.win._is_nav('src/comment_only.c')) + self.assertEqual(self.win.results['NoiseDemo_autosar_rtw/ert_main.c']['status'], 'identical') + self.assertFalse(self.win._is_nav('NoiseDemo_autosar_rtw/ert_main.c')) def test_next_change_stops_inside_a_shown_comment_only_file_with_no_unit(self): nav = [r for r in self.win._tree_rels() if self.win._is_nav(r)] - self.assertIn('src/comment_only.c', nav) - self.win._reselect(nav[nav.index('src/comment_only.c') - 1]) + self.assertIn('NoiseDemo_autosar_rtw/ert_main.c', nav) + self.win._reselect(nav[nav.index('NoiseDemo_autosar_rtw/ert_main.c') - 1]) self._settle_ui() self.win._next_change() self._settle_ui() - self.assertEqual(self.win._selected_rel(), 'src/comment_only.c') + self.assertEqual(self.win._selected_rel(), 'NoiseDemo_autosar_rtw/ert_main.c') self.assertTrue(self.win.diff._stops) self.assertIsNone(self.win.diff.current_unit()) @@ -745,7 +746,7 @@ def test_the_walk_wraps_back_to_the_first_file(self): def test_prev_change_steps_back_a_file_at_its_last_change(self): nav = [r for r in self.win._tree_rels() if self.win._is_nav(r)] - target = 'src/rename_conflict.c' # the fixture file with two changes + target = 'TorqueLimiter_autosar_rtw/TorqueLimiter_data.c' # the fixture file with two changes self.assertIn(target, nav) # the file after it, wrapping: rename_conflict.c is the last nav file # in this fixture, so this also covers stepping back over the wrap @@ -771,11 +772,11 @@ def test_hide_identical_drops_only_identical_rows(self): self.assertEqual(self.win._tree_rels(), before) def test_hide_identical_keeps_the_file_on_screen(self): - self.win._reselect('src/real_change.c') + self.win._reselect('NoiseDemo_autosar_rtw/NoiseDemo.c') self._settle_ui() self.win.cb_hide_identical.setChecked(True) self._settle_ui() - self.assertEqual(self.win._selected_rel(), 'src/real_change.c') + self.assertEqual(self.win._selected_rel(), 'NoiseDemo_autosar_rtw/NoiseDemo.c') def test_hiding_rows_never_changes_a_verdict_or_the_counts(self): # the filter is display-only: the record the export is built from must @@ -800,19 +801,19 @@ class TestMutedCategories(unittest.TestCase): the minimap and F7/F8. """ - REL = 'a2l/cal.a2l' # one comment hunk and one real one + REL = 'a2l/NoiseDemo.a2l' # one comment hunk and one real one def setUp(self): from compare_tool.qtviewer.diffpane import DiffPane self.app = _app() - self.results = scan(FIX / 'old', FIX / 'new') + self.results = scan(DEMO / 'old', DEMO / 'new') self.pane = DiffPane() self.addCleanup(self.pane.deleteLater) def _show(self, muted=()): self.pane.set_muted_modes(muted) self.pane.show_file(self.REL, self.results[self.REL], - str(FIX / 'old'), str(FIX / 'new')) + str(DEMO / 'old'), str(DEMO / 'new')) for _ in range(5): self.app.processEvents() return self.pane.rows @@ -889,7 +890,7 @@ def setUp(self): self.theme = theme self.app = _app() self.addCleanup(theme.set_current, theme.DEFAULT) - self.win = MainWindow(str(FIX / 'old'), str(FIX / 'new')) + self.win = MainWindow(str(DEMO / 'old'), str(DEMO / 'new')) self.win.resize(1200, 800) self.win.setAttribute(Qt.WA_DontShowOnScreen, True) self.win.show() @@ -907,13 +908,13 @@ def _row_bgs(self): def test_the_viewer_opens_in_the_theme_it_was_asked_for(self): from compare_tool.qtviewer.app import MainWindow - win = MainWindow(str(FIX / 'old'), str(FIX / 'new'), + win = MainWindow(str(DEMO / 'old'), str(DEMO / 'new'), theme_name=self.theme.LIGHT) self.addCleanup(win.close) self.assertEqual(self.theme.current(), self.theme.LIGHT) def test_switching_repaints_the_diff_rows_not_just_the_chrome(self): - self.win._reselect('src/real_change.c') + self.win._reselect('NoiseDemo_autosar_rtw/NoiseDemo.c') self._settle_ui() dark = self._row_bgs() self.assertIn(self.theme.color('del-bg', self.theme.DARK), dark) @@ -934,17 +935,17 @@ def first_colour(): self.assertNotEqual(first_colour(), dark) def test_the_file_on_screen_survives_the_switch(self): - self.win._reselect('src/real_change.c') + self.win._reselect('NoiseDemo_autosar_rtw/NoiseDemo.c') self._settle_ui() self.win._set_theme(self.theme.LIGHT) self._settle_ui() - self.assertEqual(self.win._selected_rel(), 'src/real_change.c') - self.assertEqual(self.win.diff._rel, 'src/real_change.c') + self.assertEqual(self.win._selected_rel(), 'NoiseDemo_autosar_rtw/NoiseDemo.c') + self.assertEqual(self.win.diff._rel, 'NoiseDemo_autosar_rtw/NoiseDemo.c') def test_the_change_being_read_survives_the_switch(self): # re-rendering the file parks on change 1; a colour switch is not a # navigation command, so the reviewer must come back to where they were - self.win._reselect('src/rename_conflict.c') + self.win._reselect('TorqueLimiter_autosar_rtw/TorqueLimiter_data.c') self._settle_ui() self.assertTrue(self.win.diff.next_change()) self._settle_ui() diff --git a/tests/test_engine.py b/tests/test_engine.py index c65426a..9ee7b5d 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -8,6 +8,7 @@ summarize_ifaces) FIX = Path(__file__).parent / 'fixtures' +DEMO = FIX / 'demo' def kinds(result): @@ -565,7 +566,7 @@ def test_moved_block_with_comment_change_still_moved(self): class TestFixtureTree(unittest.TestCase): @classmethod def setUpClass(cls): - cls.results = scan(FIX / 'old', FIX / 'new') + cls.results = scan(DEMO / 'old', DEMO / 'new') def expect(self, rel, status): self.assertIn(rel, self.results) @@ -573,22 +574,22 @@ def expect(self, rel, status): '{}: {}'.format(rel, self.results[rel])) def test_statuses(self): - self.expect('src/comment_only.c', 'comment-only') - self.expect('src/rename_only.c', 'ignorable-only') - self.expect('src/rename_conflict.c', 'real-change') - self.expect('src/real_change.c', 'real-change') - self.expect('src/same.h', 'identical') - self.expect('src/added.c', 'added') - self.expect('src/deleted.h', 'deleted') - self.expect('arxml/uuid_only.arxml', 'ignorable-only') - self.expect('arxml/admindata.arxml', 'ignorable-only') - self.expect('arxml/real_change.arxml', 'real-change') - self.expect('arxml/iface.arxml', 'real-change') - self.expect('a2l/comment_only.a2l', 'comment-only') - self.expect('a2l/cal.a2l', 'real-change') + self.expect('NoiseDemo_autosar_rtw/ert_main.c', 'comment-only') + self.expect('NoiseDemo_autosar_rtw/NoiseDemo_data.c', 'ignorable-only') + self.expect('TorqueLimiter_autosar_rtw/TorqueLimiter_data.c', 'real-change') + self.expect('NoiseDemo_autosar_rtw/NoiseDemo.c', 'real-change') + self.expect('NoiseDemo_autosar_rtw/NoiseDemo_private.h', 'identical') + self.expect('SpeedCtrl_autosar_rtw/SpeedCtrl_data.c', 'added') + self.expect('NoiseDemo_autosar_rtw/NoiseDemo_types.h', 'deleted') + self.expect('arxml/NoiseDemo_component.arxml', 'ignorable-only') + self.expect('arxml/NoiseDemo_implementation.arxml', 'ignorable-only') + self.expect('arxml/NoiseDemo_datatype.arxml', 'real-change') + self.expect('arxml/NoiseDemo_interface.arxml', 'real-change') + self.expect('a2l/Ctrl.a2l', 'comment-only') + self.expect('a2l/NoiseDemo.a2l', 'real-change') def test_a2l_diff_recorded(self): - r = self.results['a2l/cal.a2l'] + r = self.results['a2l/NoiseDemo.a2l'] self.assertEqual(r['a2l'], { 'added': [('VehSpd', 'MEASUREMENT')], 'removed': [('K_Gain', 'CHARACTERISTIC')], @@ -596,11 +597,11 @@ def test_a2l_diff_recorded(self): def test_a2l_summary_flattened(self): added, removed = summarize_a2l(self.results) - self.assertIn(('a2l/cal.a2l', 'VehSpd', 'MEASUREMENT'), added) - self.assertIn(('a2l/cal.a2l', 'K_Gain', 'CHARACTERISTIC'), removed) + self.assertIn(('a2l/NoiseDemo.a2l', 'VehSpd', 'MEASUREMENT'), added) + self.assertIn(('a2l/NoiseDemo.a2l', 'K_Gain', 'CHARACTERISTIC'), removed) def test_iface_diff_recorded(self): - r = self.results['arxml/iface.arxml'] + r = self.results['arxml/NoiseDemo_interface.arxml'] self.assertEqual(r['ifaces'], { 'added': [('/Interfaces/If_Torque', 'SENDER-RECEIVER-INTERFACE')], 'removed': [('/Interfaces/If_Diag', 'CLIENT-SERVER-INTERFACE')], @@ -608,25 +609,25 @@ def test_iface_diff_recorded(self): def test_iface_summary_flattened(self): added, removed = summarize_ifaces(self.results) - self.assertIn(('arxml/iface.arxml', '/Interfaces/If_Torque', + self.assertIn(('arxml/NoiseDemo_interface.arxml', '/Interfaces/If_Torque', 'SENDER-RECEIVER-INTERFACE'), added) - self.assertIn(('arxml/iface.arxml', '/Interfaces/If_Diag', + self.assertIn(('arxml/NoiseDemo_interface.arxml', '/Interfaces/If_Diag', 'CLIENT-SERVER-INTERFACE'), removed) def test_exclude_patterns(self): - results = scan(FIX / 'old', FIX / 'new', - exclude=['same.h', 'arxml/*']) - self.assertNotIn('src/same.h', results) - self.assertNotIn('arxml/iface.arxml', results) - self.assertIn('src/real_change.c', results) + results = scan(DEMO / 'old', DEMO / 'new', + exclude=['NoiseDemo_private.h', 'arxml/*']) + self.assertNotIn('NoiseDemo_autosar_rtw/NoiseDemo_private.h', results) + self.assertNotIn('arxml/NoiseDemo_interface.arxml', results) + self.assertIn('NoiseDemo_autosar_rtw/NoiseDemo.c', results) def test_rename_map_recorded(self): - r = self.results['src/rename_only.c'] + r = self.results['NoiseDemo_autosar_rtw/NoiseDemo_data.c'] self.assertEqual(r['renames'], {'rtb_Sum1': 'rtb_Sum_k2j', 'rtb_Gain2': 'rtb_Gain_p0f'}) def test_real_change_c_has_one_real_hunk(self): - r = self.results['src/real_change.c'] + r = self.results['NoiseDemo_autosar_rtw/NoiseDemo.c'] real = [h for h in r['hunks'] if h['kind'] == 'real'] ign = [h for h in r['hunks'] if h['kind'] != 'real'] self.assertEqual(len(real), 1) @@ -665,7 +666,7 @@ def test_comment_next_to_a_real_change_is_still_modified(self): self.assertEqual(compare_pair(old, new, 'f.c')['status'], 'real-change') def test_counts_are_separate(self): - counts = summarize(scan(FIX / 'old', FIX / 'new')) + counts = summarize(scan(DEMO / 'old', DEMO / 'new')) self.assertTrue(counts['comment-only']) self.assertTrue(counts['ignorable-only']) @@ -676,8 +677,8 @@ class TestFoldRules(unittest.TestCase): @classmethod def setUpClass(cls): - cls.plain = scan(FIX / 'old', FIX / 'new') - cls.folded = scan(FIX / 'old', FIX / 'new', fold=('ignorable-only',)) + cls.plain = scan(DEMO / 'old', DEMO / 'new') + cls.folded = scan(DEMO / 'old', DEMO / 'new', fold=('ignorable-only',)) def test_noise_only_files_become_identical(self): noisy = [p for p, r in self.plain.items() if r['status'] == 'ignorable-only'] @@ -704,8 +705,8 @@ def test_hunks_kept_so_the_diff_is_still_viewable(self): self.assertEqual(self.folded[p]['hunks'], self.plain[p]['hunks']) def test_real_change_is_not_foldable(self): - results = scan(FIX / 'old', FIX / 'new', fold=('real-change',)) - self.assertEqual(results['src/real_change.c']['status'], 'real-change') + results = scan(DEMO / 'old', DEMO / 'new', fold=('real-change',)) + self.assertEqual(results['NoiseDemo_autosar_rtw/NoiseDemo.c']['status'], 'real-change') class TestArxmlShadowStillAligns(unittest.TestCase): diff --git a/tests/test_report.py b/tests/test_report.py index f7d13eb..3f290bf 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -12,6 +12,7 @@ from compare_tool.scanner import scan FIX = Path(__file__).parent / 'fixtures' +DEMO = FIX / 'demo' # mirrors the fragmented-report case: two uuid changes 2 lines apart -- # their 3-line contexts overlap, so they must render as ONE table @@ -98,11 +99,11 @@ def test_report_carries_no_redundant_hunk_composition_text(self): # rows already show now that noise beside a real change always # renders in full (see TestNoiseBesideRealAlwaysShows) -- neither # earns its place any more - results = scan(FIX / 'old', FIX / 'new') - page = build_report(results, FIX / 'old', FIX / 'new') + results = scan(DEMO / 'old', DEMO / 'new') + page = build_report(results, DEMO / 'old', DEMO / 'new') sect = next(s for s in page.split('
')[0] + 'data-p="NoiseDemo_autosar_rtw/NoiseDemo.c"' in s).split('
')[0] # the header may now carry an "Affected: " hint (which functions # changed -- not a recount of the rows), so the check is on the # composition wording itself, not the hcount span it once rode in on @@ -112,23 +113,23 @@ def test_report_carries_no_redundant_hunk_composition_text(self): self.assertNotIn('comment + real', sect) def test_report_captions_the_enclosing_function(self): - # the real hunk in real_change.c sits inside Calc_step; the group gets + # the real hunk in NoiseDemo.c sits inside Calc_step; the group gets # a caption naming it, and the file header lists it as Affected - results = scan(FIX / 'old', FIX / 'new') - page = build_report(results, FIX / 'old', FIX / 'new') + results = scan(DEMO / 'old', DEMO / 'new') + page = build_report(results, DEMO / 'old', DEMO / 'new') sect = next(s for s in page.split('
')[0] + 'data-p="NoiseDemo_autosar_rtw/NoiseDemo.c"' in s).split('
')[0] header = sect.split('
')[0] self.assertIn('Affected: Calc_step', header) self.assertIn('class="fnhdr"', sect) self.assertIn('Calc_step', sect.split('', self.page) @@ -509,8 +511,8 @@ class TestIfaceSection(unittest.TestCase): @classmethod def setUpClass(cls): - results = scan(FIX / 'old', FIX / 'new') - cls.page = build_report(results, FIX / 'old', FIX / 'new') + results = scan(DEMO / 'old', DEMO / 'new') + cls.page = build_report(results, DEMO / 'old', DEMO / 'new') def test_section_lists_added_and_removed(self): self.assertIn('AUTOSAR changes', self.page) @@ -537,8 +539,11 @@ def test_the_section_stays_and_says_so_when_there_is_nothing_to_list(self): # it used to vanish, which is exactly the run where the reviewer most # needs the answer: an absent heading looks like the report forgot to # check, while "no AUTOSAR-level changes" IS the finding - results = scan(FIX / 'old', FIX / 'new', exclude=['arxml/*', 'a2l/*']) - page = build_report(results, FIX / 'old', FIX / 'new') + # Ctrl.c goes too: its new Rte_Write_Out2_Diag is itself an + # AUTOSAR-level change, and this case is about having none at all + results = scan(DEMO / 'old', DEMO / 'new', + exclude=['arxml/*', 'a2l/*', 'Ctrl.c']) + page = build_report(results, DEMO / 'old', DEMO / 'new') self.assertIn('

AUTOSAR changes

', page) self.assertIn('No AUTOSAR-level changes', page) self.assertNotIn('Port interfaces', page) @@ -627,7 +632,7 @@ def test_the_legend_still_has_no_noise_colour_swatch_for_it(self): # the muted grey gets its own swatch (sw-mut) now that noise can be # revealed; what it must NOT do is reuse or resemble the real-change # red/green swatches, which is what sw-min / sw-cmt would have implied - page = build_report(scan(FIX / 'old', FIX / 'new'), FIX / 'old', FIX / 'new') + page = build_report(scan(DEMO / 'old', DEMO / 'new'), DEMO / 'old', DEMO / 'new') legend = page.split('class="legend"')[1].split('
')[0] self.assertNotIn('sw-min', legend) self.assertNotIn('sw-cmt', legend) @@ -641,15 +646,15 @@ class TestOldSideNaming(unittest.TestCase): @staticmethod def _results(): - return scan(FIX / 'old', FIX / 'new') + return scan(DEMO / 'old', DEMO / 'new') def test_without_a_label_the_folder_name_is_used(self): - page = build_report(self._results(), FIX / 'old', FIX / 'new') + page = build_report(self._results(), DEMO / 'old', DEMO / 'new') self.assertIn('BASELINE old', page) def test_a_label_replaces_the_folder_name_on_the_old_side_only(self): - page = build_report(self._results(), FIX / 'old', FIX / 'new', + page = build_report(self._results(), DEMO / 'old', DEMO / 'new', old_label='a1b2c3d 2026-07-20 raise the limit') self.assertIn('a1b2c3d 2026-07-20 raise the limit', page) self.assertNotIn('>old', page) @@ -658,17 +663,17 @@ def test_a_label_replaces_the_folder_name_on_the_old_side_only(self): def test_the_real_path_stays_on_hover(self): # the temp folder is still where the files were read from: hiding it # entirely would make a failed compare impossible to trace - page = build_report(self._results(), FIX / 'old', FIX / 'new', + page = build_report(self._results(), DEMO / 'old', DEMO / 'new', old_label='a1b2c3d') - self.assertIn('title="{}"'.format(FIX / 'old'), page) + self.assertIn('title="{}"'.format(DEMO / 'old'), page) def test_the_arxml_report_names_the_commit_too(self): - page = build_arxml_report(self._results(), FIX / 'old', FIX / 'new', + page = build_arxml_report(self._results(), DEMO / 'old', DEMO / 'new', old_label='a1b2c3d raise the limit') self.assertIn('a1b2c3d raise the limit', page) def test_a_label_is_escaped_like_any_other_text(self): - page = build_report(self._results(), FIX / 'old', FIX / 'new', + page = build_report(self._results(), DEMO / 'old', DEMO / 'new', old_label='fix ') self.assertNotIn('', page) self.assertIn('<script>', page) @@ -682,17 +687,17 @@ class TestCurrentSideNaming(unittest.TestCase): @staticmethod def _results(): - return scan(FIX / 'old', FIX / 'new') + return scan(DEMO / 'old', DEMO / 'new') def test_a_label_replaces_the_folder_name_on_the_new_side_only(self): - page = build_report(self._results(), FIX / 'old', FIX / 'new', + page = build_report(self._results(), DEMO / 'old', DEMO / 'new', new_label='PR 312') self.assertIn('>PR 312', page) self.assertNotIn('>new', page) self.assertIn('>old', page) # BASELINE is still its own folder def test_both_sides_can_be_named_at_once(self): - page = build_report(self._results(), FIX / 'old', FIX / 'new', + page = build_report(self._results(), DEMO / 'old', DEMO / 'new', old_label='build 4820', new_label='build 4821') self.assertIn('>build 4820', page) self.assertIn('>build 4821', page) @@ -700,17 +705,17 @@ def test_both_sides_can_be_named_at_once(self): def test_the_real_path_stays_on_hover(self): # naming a side must not cost the reader the folder it was read from: # a rerun of a failed compare needs the actual path - page = build_report(self._results(), FIX / 'old', FIX / 'new', + page = build_report(self._results(), DEMO / 'old', DEMO / 'new', new_label='PR 312') - self.assertIn('title="{}"'.format(FIX / 'new'), page) + self.assertIn('title="{}"'.format(DEMO / 'new'), page) def test_the_arxml_report_takes_it_too(self): - page = build_arxml_report(self._results(), FIX / 'old', FIX / 'new', + page = build_arxml_report(self._results(), DEMO / 'old', DEMO / 'new', new_label='PR 312') self.assertIn('>PR 312', page) def test_a_label_is_escaped_like_any_other_text(self): - page = build_report(self._results(), FIX / 'old', FIX / 'new', + page = build_report(self._results(), DEMO / 'old', DEMO / 'new', new_label='') self.assertNotIn('', page) self.assertIn('<script>', page) @@ -725,10 +730,10 @@ class TestPageTheme(unittest.TestCase): """ def setUp(self): - self.results = scan(FIX / 'old', FIX / 'new') + self.results = scan(DEMO / 'old', DEMO / 'new') def _page(self, **kw): - return build_report(self.results, FIX / 'old', FIX / 'new', **kw) + return build_report(self.results, DEMO / 'old', DEMO / 'new', **kw) def test_the_default_is_dark(self): from compare_tool import theme @@ -775,7 +780,7 @@ def test_opening_a_report_does_not_answer_the_preference_for_the_reader(self): self.assertIn('if(save){try{localStorage.setItem', js) def test_the_arxml_report_switches_too(self): - page = build_arxml_report(self.results, FIX / 'old', FIX / 'new', + page = build_arxml_report(self.results, DEMO / 'old', DEMO / 'new', theme_name='light') self.assertIn('', page) self.assertIn('id="thm"', page) @@ -803,7 +808,8 @@ def test_utility_pair_stays_shared(self): def test_modular_arxml_export_names_model(self): g = _model_groups(self._results( ['Ctrl_component.arxml', 'Ctrl_interface.arxml', 'other.txt'])) - self.assertEqual(g['Ctrl'], ['Ctrl_component.arxml', 'Ctrl_interface.arxml']) + self.assertEqual(g['Ctrl'], + ['Ctrl_component.arxml', 'Ctrl_interface.arxml']) def test_longest_model_name_wins(self): paths = ['Ctrl.c', 'Ctrl.h', 'Ctrl_types.h', @@ -849,8 +855,8 @@ class TestModelReport(unittest.TestCase): @classmethod def setUpClass(cls): - cls.results = scan(FIX / 'model_old', FIX / 'model_new') - cls.page = build_report(cls.results, FIX / 'model_old', FIX / 'model_new') + cls.results = scan(DEMO / 'old', DEMO / 'new') + cls.page = build_report(cls.results, DEMO / 'old', DEMO / 'new') def test_overview_table_lists_model(self): self.assertIn('Overview', self.page) @@ -886,11 +892,11 @@ def test_per_file_notes(self): def test_filter_plumbing_present(self): self.assertIn('id="flt"', self.page) self.assertIn('function flt(', self.page) - self.assertIn('data-p="Ctrl.c"', self.page) + self.assertIn('data-p="Ctrl_autosar_rtw/Ctrl.c"', self.page) def test_scanner_attached_semantics(self): - self.assertIn('swc', self.results['Ctrl_component.arxml']) - self.assertIn('rte', self.results['Ctrl.c']) + self.assertIn('swc', self.results['arxml/Ctrl_component.arxml']) + self.assertIn('rte', self.results['Ctrl_autosar_rtw/Ctrl.c']) class TestOverviewCountsStayTrue(unittest.TestCase): @@ -931,8 +937,8 @@ class TestOneSidedContentPicksItsSide(unittest.TestCase): @classmethod def setUpClass(cls): - cls.page = build_report(scan(FIX / 'old', FIX / 'new'), - FIX / 'old', FIX / 'new') + cls.page = build_report(scan(DEMO / 'old', DEMO / 'new'), + DEMO / 'old', DEMO / 'new') def _rows(self, rel): # the folder tree carries data-p too, so the section is the chunk that @@ -943,7 +949,7 @@ def _rows(self, rel): return re.findall(r'(.*?)', sect.split('')[0]) def test_added_content_sits_in_the_current_half(self): - rows = self._rows('src/added.c') + rows = self._rows('SpeedCtrl_autosar_rtw/SpeedCtrl_data.c') self.assertTrue(rows) for row in rows: cells = re.findall(r']*>', row) @@ -953,7 +959,7 @@ def test_added_content_sits_in_the_current_half(self): self.assertIn('', row) def test_deleted_content_sits_in_the_baseline_half(self): - rows = self._rows('src/deleted.h') + rows = self._rows('NoiseDemo_autosar_rtw/NoiseDemo_types.h') self.assertTrue(rows) for row in rows: self.assertTrue(row.endswith(''), @@ -976,8 +982,8 @@ class TestModelGroupHidesWhenEmpty(unittest.TestCase): or a future badge would hide the files and leave the header behind.""" def setUp(self): - self.page = build_report(scan(FIX / 'model_old', FIX / 'model_new'), - FIX / 'model_old', FIX / 'model_new') + self.page = build_report(scan(DEMO / 'old', DEMO / 'new'), + DEMO / 'old', DEMO / 'new') def test_every_css_file_toggle_is_in_the_mv_table(self): # body.hide-X .sec-Y and body.hide-rev details.file.file-rev @@ -1005,26 +1011,28 @@ class TestArxmlOnlyReport(unittest.TestCase): @classmethod def setUpClass(cls): - cls.results = scan(FIX / 'old', FIX / 'new', + cls.results = scan(DEMO / 'old', DEMO / 'new', include=['*.arxml', '*.xml', '*.a2l']) - cls.page = build_arxml_report(cls.results, FIX / 'old', FIX / 'new') + cls.page = build_arxml_report(cls.results, DEMO / 'old', DEMO / 'new') def test_include_filter_limits_scan_to_arxml_and_a2l(self): - self.assertIn('arxml/real_change.arxml', self.results) - self.assertIn('a2l/cal.a2l', self.results) - self.assertNotIn('src/real_change.c', self.results) + self.assertIn('arxml/NoiseDemo_datatype.arxml', self.results) + self.assertIn('a2l/NoiseDemo.a2l', self.results) + self.assertNotIn('NoiseDemo_autosar_rtw/NoiseDemo.c', self.results) def test_page_lists_updated_files_per_type(self): self.assertIn('ARXML / A2L Update Report', self.page) self.assertIn('Updated ARXML files', self.page) - self.assertIn('arxml/real_change.arxml', self.page) - self.assertIn('arxml/iface.arxml', self.page) + self.assertIn('arxml/NoiseDemo_datatype.arxml', self.page) + self.assertIn('arxml/NoiseDemo_interface.arxml', self.page) self.assertIn('Updated A2L files', self.page) - self.assertIn('a2l/cal.a2l', self.page) + self.assertIn('a2l/NoiseDemo.a2l', self.page) def test_per_type_verdict_badges(self): self.assertIn('ARXML updated:', self.page) - self.assertIn('A2L updated: 1 modified', self.page) + # NoiseDemo, PedalMap and StaleGen each changed a calibration object; + # Ctrl.a2l only churned a comment and is not counted + self.assertIn('A2L updated: 3 modified', self.page) def test_page_carries_autosar_summary(self): self.assertIn('AUTOSAR changes', self.page) @@ -1036,17 +1044,29 @@ def test_page_carries_autosar_summary(self): def test_noise_only_files_not_listed(self): files_block = self.page.split('Updated ARXML files')[1] - self.assertNotIn('uuid_only.arxml', files_block) - self.assertNotIn('admindata.arxml', files_block) - self.assertNotIn('comment_only.a2l', files_block) + # UUID churn, an ADMIN-DATA timestamp and a comment-only A2L are all + # noise: they are in the scan but must not be listed as updates + self.assertNotIn('NoiseDemo_component.arxml', files_block) + self.assertNotIn('NoiseDemo_implementation.arxml', files_block) + self.assertNotIn('Ctrl.a2l', files_block) self.assertIn('noise-only differences', self.page) def test_no_changes_stated_explicitly_when_only_noise(self): - results = scan(FIX / 'old', FIX / 'new', - exclude=['real_change.arxml', 'iface.arxml', 'cal.a2l']) + # every ARXML/A2L that really changed is excluded, leaving only the + # noise-only ones -- the report must still say so out loud + results = scan(DEMO / 'old', DEMO / 'new', + exclude=['NoiseDemo_datatype.arxml', + 'NoiseDemo_interface.arxml', + 'Ctrl_component.arxml', + 'PedalMap_component.arxml', + 'StaleGen_component.arxml', + 'NoiseDemo.a2l', 'PedalMap.a2l', + 'StaleGen.a2l']) # C-file real changes present but must not count as arxml/a2l update - self.assertEqual(results['src/real_change.c']['status'], 'real-change') - page = build_arxml_report(results, FIX / 'old', FIX / 'new') + self.assertEqual( + results['NoiseDemo_autosar_rtw/NoiseDemo.c']['status'], + 'real-change') + page = build_arxml_report(results, DEMO / 'old', DEMO / 'new') self.assertIn('ARXML: no changes', page) self.assertIn('A2L: no changes', page) self.assertIn('No ARXML or A2L updates', page) diff --git a/tests/test_review.py b/tests/test_review.py index de94365..8b345c1 100644 --- a/tests/test_review.py +++ b/tests/test_review.py @@ -16,6 +16,7 @@ from compare_tool.scanner import scan FIX = Path(__file__).parent / 'fixtures' +DEMO = FIX / 'demo' # match the emitted markup, not the CSS rule that carries the same class name # (the stylesheet ships with every report, review or no review) @@ -295,20 +296,20 @@ def test_a_file_with_no_units_cannot_be_signed_off(self): class TestReportRendering(unittest.TestCase): def setUp(self): - self.results = scan(FIX / 'old', FIX / 'new') - self.rel = 'src/real_change.c' + self.results = scan(DEMO / 'old', DEMO / 'new') + self.rel = 'NoiseDemo_autosar_rtw/NoiseDemo.c' r = self.results[self.rel] - old = (FIX / 'old' / self.rel).read_text() - new = (FIX / 'new' / self.rel).read_text() + old = (DEMO / 'old' / self.rel).read_text() + new = (DEMO / 'new' / self.rel).read_text() self.unit = review.units(r, old.split('\n'), new.split('\n'))[0] self.store = review.ReviewStore() def _page(self, store=None): - return build_report(self.results, FIX / 'old', FIX / 'new', + return build_report(self.results, DEMO / 'old', DEMO / 'new', self.store if store is None else store) def test_no_store_means_no_review_markup_at_all(self): - page = build_report(self.results, FIX / 'old', FIX / 'new') + page = build_report(self.results, DEMO / 'old', DEMO / 'new') self.assertNotIn(_NOTE, page) self.assertNotIn(_BADGE, page) @@ -318,7 +319,7 @@ def test_note_and_badge_appear(self): page = self._page() self.assertIn('Gain raised for the new plant.', page) self.assertIn('✓ Reviewed', page) - self.assertIn('1 of 8 Reviewed', page) + self.assertIn('1 of 17 Reviewed', page) def test_a_note_without_the_tick_still_shows_but_does_not_hide(self): self.store.set(self.rel, self.unit.key, 'Asking the integrator.', False, @@ -327,7 +328,7 @@ def test_a_note_without_the_tick_still_shows_but_does_not_hide(self): self.assertIn('Asking the integrator.', page) self.assertIn('rvnote pending', page) self.assertNotIn(_GRP_REV, page) - self.assertIn('0 of 8 Reviewed', page) + self.assertIn('0 of 17 Reviewed', page) def test_reviewed_change_is_marked_hideable_but_stays_in_the_record(self): self.store.set(self.rel, self.unit.key, 'ok', True, self.unit.label) @@ -350,7 +351,7 @@ def test_signature_does_not_survive_the_change_being_regenerated(self): page = self._page() self.assertNotIn('signed off long ago', page) self.assertNotIn(_GRP_REV, page) - self.assertIn('0 of 8 Reviewed', page) + self.assertIn('0 of 17 Reviewed', page) def test_unreadable_review_file_is_loud_in_the_report(self): broken = review.ReviewStore(path='x.json', error='ValueError: bad') diff --git a/tests/test_summary_model.py b/tests/test_summary_model.py index 85d767a..e681ec0 100644 --- a/tests/test_summary_model.py +++ b/tests/test_summary_model.py @@ -7,13 +7,14 @@ from compare_tool.scanner import scan FIX = Path(__file__).parent / 'fixtures' +DEMO = FIX / 'demo' class TestSummarySections(unittest.TestCase): @classmethod def setUpClass(cls): - cls.sections = dict(summary_sections(scan(FIX / 'old', FIX / 'new'))) - cls.model = dict(summary_sections(scan(FIX / 'model_old', FIX / 'model_new'))) + cls.sections = dict(summary_sections(scan(DEMO / 'old', DEMO / 'new'))) + cls.model = dict(summary_sections(scan(DEMO / 'old', DEMO / 'new'))) def test_updated_files_section_is_gone(self): # the folder tree already lists every changed file, so the panel no @@ -44,10 +45,10 @@ def test_behaviour_sections_from_the_model_fixtures(self): for r in self.model['RTE access points'])) def test_no_changes_gives_no_sections(self): - self.assertEqual(summary_sections(scan(FIX / 'old', FIX / 'old')), []) + self.assertEqual(summary_sections(scan(DEMO / 'old', DEMO / 'old')), []) def test_every_row_points_at_a_real_file(self): - results = scan(FIX / 'old', FIX / 'new') + results = scan(DEMO / 'old', DEMO / 'new') for _title, rows in summary_sections(results): for row in rows: self.assertIn(row.rel, results) From c029cbc1807ff7b1eff76138691af4c45286a99f Mon Sep 17 00:00:00 2001 From: longvo920 Date: Sun, 23 Aug 2026 12:51:02 +0700 Subject: [PATCH 2/7] feat(viewer): stop the category toggles from re-judging files Unticking Comment or Unimportant used to re-judge every affected file to Identical. That put the tree at odds with the exported report, which is always built from the raw scan, and it made the viewer say "Identical" about a file whose comments really had moved -- the one word a reviewer is entitled to read as "nothing differs here at all". The toggles are now what they always looked like: a reading aid. Those rows go grey and drop off the minimap and F7/F8, and nothing else changes. The verdict, the counts and the exported report all keep saying Comment or Unimportant, and Hide identical leaves such a file in the tree -- it hides files that are identical, not files whose differences the reviewer chose to play down. _is_nav now drops a muted category itself rather than relying on the fold having rewritten the status, so F7/F8 still walks past a greyed-out file instead of dead-ending in one with no stops left. Both noise verdicts also share the same almost-equal mark in the tree now. They are one answer to "must I read this?", and the Status word beside the mark is what separates a moved banner from a renamed identifier. --- CLAUDE.md | 22 ++++++-- compare_tool/qtviewer/app.py | 96 ++++++++++++++++++-------------- compare_tool/qtviewer/dialogs.py | 20 ++++--- compare_tool/qtviewer/tree.py | 7 ++- docs/architecture.md | 58 ++++++++++--------- docs/usage.md | 6 +- docs/vi/usage.md | 6 +- tests/test_diffpane_qt.py | 31 +++++++++-- 8 files changed, 153 insertions(+), 93 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d4e4efb..7bd0550 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -50,12 +50,22 @@ place, `diff_engine._status_of`. Only noise verdicts are foldable (`scanner.FOLDABLE`). `real-change`, `added`, `deleted` and `error` can **never** be folded away by a UI toggle. -Folding a category in the viewer changes the file's verdict and **greys** its -rows (`view_model.mute_rows`) — it does not remove them. The lines stay -readable, and only the "where should I look next" surfaces (minimap, F7/F8) -stop counting them. Collapsing them to a `⋯ N lines hidden` placeholder was -tried and reverted: a regenerated file is mostly banner churn, so it took the -context the surviving hunks have to be read in. +**A viewer toggle never changes a verdict.** Unticking `Comment` or +`Unimportant` **greys** those rows (`view_model.mute_rows`) and drops them from +the minimap and `F7`/`F8` — nothing else. The file keeps saying Comment, the +counts keep counting it, and `Hide identical` still leaves it in the tree, +because it is not identical. Re-judging it to `identical` is what the viewer +used to do, and it was wrong twice over: the tree then disagreed with the +exported report about the same file, and "Identical" is the one word a reviewer +is entitled to read as *nothing differs here at all*. + +Removing those rows instead of greying them was tried and reverted too: a +regenerated file is mostly banner churn, so a `⋯ N lines hidden` placeholder +took away the context the surviving hunks have to be read in. + +Both noise verdicts wear the same `≈` mark in the tree — they are one answer to +"must I read this?", and the Status word beside it says which kind of nothing +it is. ## 3. One seam per shared decision diff --git a/compare_tool/qtviewer/app.py b/compare_tool/qtviewer/app.py index 144f089..90d2217 100644 --- a/compare_tool/qtviewer/app.py +++ b/compare_tool/qtviewer/app.py @@ -25,7 +25,7 @@ from ..diff_engine import RULES from ..main import default_report_name from ..report import build_arxml_report, build_report, consistency_advisories -from ..scanner import apply_fold, summarize +from ..scanner import summarize from .advisories import AdvisoryPanel from .dialogs import show_about, show_release_notes, show_user_guide from .diffpane import DiffPane @@ -154,32 +154,35 @@ def __init__(self, old=None, new=None, exclude=(), arxml_only=False, self.filter_edit.setClearButtonEnabled(True) self.filter_edit.textChanged.connect(self._refresh_tree) - # compare-rule toggles: unticking a category means "do not report it - # separately" -- the tree is rescanned and each such file comes back as - # Identical (nothing left) or Modified (real changes underneath). + # reading toggles: unticking a category greys its lines out and drops + # them from the minimap and F7/F8. The VERDICT is left alone -- a + # comment-only file keeps saying Comment. Re-judging it to Identical is + # what this used to do, and it made the tree disagree with the exported + # report about the same file; worse, "Identical" is the one word a + # reviewer is entitled to read as "nothing differs here at all". self.cb_comment = QCheckBox('Comment') self.cb_comment.setChecked(True) self.cb_comment.setToolTip( - 'Untick to ignore comment-only differences: each such file is then ' - 'reported as Identical or Modified.') + 'Untick to grey out comment-only differences. The file keeps its ' + 'Comment verdict; its lines just stop competing for attention and ' + 'drop out of F7/F8.') self.cb_comment.toggled.connect(self._apply_rules) self.cb_unimportant = QCheckBox('Unimportant') self.cb_unimportant.setChecked(True) self.cb_unimportant.setToolTip( - 'Untick to ignore the other unimportant differences (UUIDs, ' - 'timestamps, renames, whitespace): each such file is then reported ' - 'as Identical or Modified.') + 'Untick to grey out the other unimportant differences (UUIDs, ' + 'timestamps, renames, whitespace). The file keeps its Unimportant ' + 'verdict; its lines drop out of F7/F8.') self.cb_unimportant.toggled.connect(self._apply_rules) - # a display filter, NOT a compare rule: it removes rows from the tree - # without touching a verdict, which is why it sits apart from the two - # above. A regenerated tree is mostly untouched files, and scrolling + # a display filter over the tree, which is why it sits apart from the + # two above. A regenerated tree is mostly untouched files, and scrolling # past hundreds of '=' rows to reach five changed ones is its own way # of hiding them. self.cb_hide_identical = QCheckBox('Hide identical') self.cb_hide_identical.setToolTip( - 'Leave only the files with a difference in the tree. Nothing is ' - 're-judged: verdicts, counts and the exported report are unchanged. ' - 'Files folded to Identical by the two boxes on the left go too.') + 'Leave only the files with a difference in the tree. Only genuinely ' + 'identical files go: a Comment or Unimportant file stays, greyed or ' + 'not. Verdicts, counts and the exported report are unchanged.') self.cb_hide_identical.toggled.connect(self._refresh_tree_keep_selection) rules = QHBoxLayout() rules.setContentsMargins(0, 0, 0, 0) @@ -906,23 +909,24 @@ def _checkout(self, root, sub, commit): # --- scan lifecycle --- - # switching a category off changes BOTH places it shows: the file's verdict - # (status -> Identical/Modified) and how its lines are painted in the diff - # panes -- greyed out, and dropped from the minimap and from F7/F8. Leaving - # a wall of red and green in the code after saying those do not count was - # the worst of both; taking the lines away instead cost the context the - # remaining changes have to be read in. - _FOLD_MODE = {'comment-only': 'comment', 'ignorable-only': 'minor'} - - def _fold(self): - """Change categories the current rules do NOT report separately; those - files come back Identical (or Modified when real changes remain).""" - fold = [] + # switching a category off changes how its lines are PAINTED -- greyed + # out, and dropped from the minimap and from F7/F8 -- and nothing else. + # Leaving a wall of red and green in the code after saying those do not + # count was the worst of both; taking the lines away instead cost the + # context the remaining changes have to be read in. The verdict is not + # part of it: a file the reviewer greyed out still differs, and the tree + # has to keep saying so. + _MUTE_MODE = {'comment-only': 'comment', 'ignorable-only': 'minor'} + + def _muted_statuses(self): + """Verdicts whose lines the reviewer has switched off. They are played + down in the panes, never re-judged.""" + muted = [] if not self.cb_comment.isChecked(): - fold.append('comment-only') + muted.append('comment-only') if not self.cb_unimportant.isChecked(): - fold.append('ignorable-only') - return tuple(fold) + muted.append('ignorable-only') + return tuple(muted) def _start_scan(self): if not (self.old and self.new): @@ -985,15 +989,18 @@ def _on_done(self, results): self._apply_rules() def _apply_rules(self): - """Re-judge the scanned tree under the current category toggles. Pure + """Re-paint the scanned tree under the current category toggles. Pure bookkeeping on results already in memory -- no second walk of the disk, - so a toggle is instant and the folders are read exactly once.""" + so a toggle is instant and the folders are read exactly once. + + The results themselves are the raw scan: a toggle decides what is + greyed, never what a file's verdict is.""" if not self._raw_results: return keep = self._selected_rel() - fold = self._fold() - self.results = apply_fold(self._raw_results, fold) - self.diff.set_muted_modes([self._FOLD_MODE[f] for f in fold]) + muted = self._muted_statuses() + self.results = dict(self._raw_results) + self.diff.set_muted_modes([self._MUTE_MODE[s] for s in muted]) self._refresh_tree() self._reselect(keep) # keep the reviewer on the file they were reading if self._autoselect: @@ -1248,13 +1255,11 @@ def _selected_rel(self): # finished scan opens on the first of these, and F7/F8 step between them # once the current file runs out of changes. # - # comment-only / ignorable-only are noise verdicts, but self.results is the - # FOLDED view (_apply_rules): a file only keeps one of those statuses while - # its checkbox is ticked (unfolded), and apply_fold turns it into - # 'identical' the moment it is unticked. So including them here means F7/F8 - # walks into a comment/unimportant-only file exactly while it is shown on - # screen, and stops treating it as navigable the instant it is hidden -- - # with no extra state to keep in sync with the checkboxes. + # comment-only / ignorable-only are noise verdicts, but they are walkable + # while their checkbox is ticked, so a reviewer can step through + # everything. _is_nav drops them again the moment the box is unticked: + # those rows are greyed and off the minimap, so there is nothing in such a + # file for F7/F8 to stop on, and stepping into it would dead-end. _NAV_STATUS = ('error', 'real-change', 'added', 'deleted', 'comment-only', 'ignorable-only') @@ -1280,7 +1285,12 @@ def walk(item): def _is_nav(self, rel): r = self.results.get(rel) - return bool(r) and r['status'] in self._NAV_STATUS + if not r: + return False + # a greyed-out category has no stops left in it + if r['status'] in self._muted_statuses(): + return False + return r['status'] in self._NAV_STATUS def _select_first_change(self): """Open the first file a reviewer would have to read. Called once per diff --git a/compare_tool/qtviewer/dialogs.py b/compare_tool/qtviewer/dialogs.py index 82ce9b3..13bbd25 100644 --- a/compare_tool/qtviewer/dialogs.py +++ b/compare_tool/qtviewer/dialogs.py @@ -40,28 +40,34 @@ - A folder shows its heaviest child verdict - Box above the tree filters by path - `Hide identical` leaves only the files with a difference. It is a view, not a - rule: verdicts, counts and the exported report do not change + rule: verdicts, counts and the exported report do not change. Only genuinely + identical files go -- a Comment or Unimportant file stays in the tree - Right-click a row: show it in Explorer, or copy its full path | Mark | Verdict | Meaning | |---|---|---| | `≠` | Modified | Real changes | -| `≉` | Comment | Only comments differ | +| `≈` | Comment | Only comments differ | | `≈` | Unimportant | UUIDs, timestamps, renames, whitespace | | `+` | Added | Exists only in CURRENT | | `−` | Deleted | Exists only in BASELINE | | `=` | Identical | No difference | | `‼` | NOT compared | Treat as changed | -## 4. Fold the noise +Comment and Unimportant share the `≈` mark: both answer "no, you do not have +to read this". The Status word says which kind of nothing it is. -- Untick `Comment` / `Unimportant` -- affected files re-judge instantly -- Those lines are **greyed out, not removed**: they stay where they are, keep - their line numbers, and lose their red/green +## 4. Quiet the noise + +- Untick `Comment` / `Unimportant` -- those lines go grey instantly +- They are **greyed out, not removed**: they stay where they are, keep their + line numbers, and lose their red/green - They also drop off the minimap and out of `F7` / `F8`, so nothing sends you back to them +- The **verdict does not change**: a comment-only file still reads Comment in + the tree, and still appears as Comment in an exported report - Tick back on to bring the colour back -- Real changes can never be folded away +- Real changes can never be quietened ## 5. Read the diff diff --git a/compare_tool/qtviewer/tree.py b/compare_tool/qtviewer/tree.py index c4ed199..26467eb 100644 --- a/compare_tool/qtviewer/tree.py +++ b/compare_tool/qtviewer/tree.py @@ -18,8 +18,11 @@ 'real-change': ('≠', 'Modified', 'st-real'), # not-equal sign # the two noise verdicts are grey on purpose: grey is what "this does not # count" looks like, and it keeps red/green meaning removed/added only - 'comment-only': ('≉', 'Comment', 'st-cmt'), # comments only - 'ignorable-only': ('≈', 'Unimportant', 'st-ign'), # almost-equal + # both carry the same almost-equal sign: they are the same answer to + # "does this differ in a way I have to read?" -- the Status word beside it + # is what separates a moved banner from a renamed identifier + 'comment-only': ('≈', 'Comment', 'st-cmt'), + 'ignorable-only': ('≈', 'Unimportant', 'st-ign'), 'added': ('+', 'Added', 'st-add'), 'deleted': ('−', 'Deleted', 'st-del'), # minus sign 'identical': ('=', 'Identical', 'st-id'), diff --git a/docs/architecture.md b/docs/architecture.md index 5753a76..285f937 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -217,31 +217,39 @@ be exact. changes, one-sided files and errors are absent from that tuple **by construction**, so no caller mistake can hide one. -### Folding is a pure function, not a rescan - -`scanner.apply_fold` re-judges an already scanned tree under different rules -with no disk access: the hunks already say what kind each difference is, so -re-reading every file to learn the same thing is pure waste. It copies rather -than mutates, so the rules can be toggled back and forth. The viewer keeps the -untouched scan in `MainWindow._raw_results` and folds into `self.results` for -display. - -Folding a category changes two things, and only these two: the file's -**verdict** (it comes back `identical`, or `real-change` if something real -remains) and how its rows are **painted** — `view_model.mute_rows` greys them, -the minimap stops striping them and `F7`/`F8` stop landing on them. The lines -themselves stay on screen. The hunks are never touched, so the exported report, -built from `_raw_results`, cannot notice that a category was folded. - -Navigation follows what is on screen, not what is reviewable. A shown (unfolded) -comment or Unimportant hunk **is** an `F7`/`F8` stop, and a file whose whole -verdict is `comment-only` / `ignorable-only` is in `MainWindow._NAV_STATUS`, so -the walk crosses into it. Both fall out again the moment the category is folded, -because `apply_fold` has by then re-judged that file `identical` — one source of -truth, no second flag to keep in step with the checkboxes. What navigation must -never do is imply a sign-off: `DiffPane._stop_units` carries `None` for those -stops, so `current_unit()` reports nothing to review there. Only `real` and -`moved` are reviewable (`review.REVIEWABLE`), navigable or not. +### A viewer toggle is a paint decision, not a verdict + +`MainWindow._muted_statuses()` reads the two checkboxes; `_apply_rules` passes +the matching row modes to `DiffPane.set_muted_modes` and otherwise hands the +**raw scan** straight through to `self.results`. Nothing is re-judged, so a +toggle is instant and the folders are read exactly once. + +Unticking a category changes how its rows are **painted** and nothing else: +`view_model.mute_rows` greys them, the minimap stops striping them, `F7`/`F8` +stop landing on them. The lines stay on screen, the hunks are untouched, and +the verdict is untouched — a comment-only file still reads Comment in the tree, +in the counts, and in the exported report. `Hide identical` therefore leaves it +alone: it hides files that are identical, not files whose differences the +reviewer chose to play down. + +That the verdict is left alone is the whole point. When the toggle re-judged +the file `identical`, the tree and the exported report (built from +`_raw_results`) disagreed about the same file, and the tree made the one claim +this tool must never make about a file that really differs. + +`scanner.apply_fold` / `fold_status` still implement the old collapse for +`scan(fold=…)` — a CLI-side option — and `scanner.FOLDABLE` still names the only +two statuses any caller may collapse. Real changes, one-sided files and errors +are absent from that tuple **by construction**. + +Navigation follows what is on screen. A shown comment or Unimportant hunk **is** +an `F7`/`F8` stop, and such a file is in `MainWindow._NAV_STATUS`, so the walk +crosses into it. `_is_nav` drops it again the moment its box is unticked — +those rows are greyed and off the minimap, so the file has no stops left and +stepping into it would dead-end. What navigation must never do is imply a +sign-off: `DiffPane._stop_units` carries `None` for those stops, so +`current_unit()` reports nothing to review there. Only `real` and `moved` are +reviewable (`review.REVIEWABLE`), navigable or not. ## The result dict is the contract diff --git a/docs/usage.md b/docs/usage.md index a0413b4..3bd65fb 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -66,15 +66,15 @@ Once a file is open, a small caption next to its name tracks whatever you're loo - The scan **opens on the first change** — you never land on an empty pane next to a tree full of results. - `F8` / `F7` step through the changes in the open file, then carry on into the next (or previous) changed file once you run out, wrapping around at the end. `Ctrl+Home` / `Ctrl+End` stay inside the current file. Comment and noise files join that walk while their category is ticked on, but stopping on one signs off nothing — only real and moved changes ever enter the review record. - `Ctrl+F` finds text in the open file, either side, with `F3` / `Shift+F3` to step through matches and `Esc` to close it. The query survives moving to another file, so you can chase one identifier across the whole compare. -- `Hide identical` narrows the tree down to files that actually differ. It's purely a view — verdicts, counts and the exported report are untouched by it. -- Unticking `Comment` / `Unimportant` greys those lines out rather than deleting them: they keep their place and their line numbers, just lose their red/green colouring and drop off the minimap and the `F7`/`F8` walk. Left ticked, which is the default, they keep their colour and behave like any other stop. +- `Hide identical` narrows the tree down to files that actually differ. Only genuinely identical files go: a Comment or Unimportant file stays, greyed or not. It's purely a view — verdicts, counts and the exported report are untouched by it. +- Unticking `Comment` / `Unimportant` greys those lines out rather than deleting them: they keep their place and their line numbers, just lose their red/green colouring and drop off the minimap and the `F7`/`F8` walk. Left ticked, which is the default, they keep their colour and behave like any other stop. The **verdict does not change** either way — a comment-only file still reads Comment in the tree and in an exported report. - Wherever you currently are is marked with a small arrow in the line-number gutter, on both panes, so `F7`/`F8` visibly move you even in a file too short to scroll. - `☀ Light` / `☾ Dark` in the toolbar swaps the colour scheme on the fly; `--theme` just picks which one it opens in. C, C++, ARXML/XML, A2L, Python, JSON and YAML are all syntax-coloured in either theme. | Mark | Verdict | Meaning | |---|---|---| | `≠` | Modified | real changes | -| `≉` | Comment | only comments differ | +| `≈` | Comment | only comments differ | | `≈` | Unimportant | UUIDs, timestamps, renames, whitespace | | `+` | Added | file exists only in CURRENT | | `−` | Deleted | file exists only in BASELINE | diff --git a/docs/vi/usage.md b/docs/vi/usage.md index b53275f..f0830ac 100644 --- a/docs/vi/usage.md +++ b/docs/vi/usage.md @@ -95,15 +95,15 @@ thật sự rời khỏi hàm. - Scan **mở sẵn ở change đầu tiên** — bạn không bao giờ rơi vào một pane trống trong khi cây bên cạnh đầy kết quả. - `F8` / `F7` nhảy qua các change trong file đang mở, rồi đi tiếp sang file có change kế tiếp (hoặc trước đó) một khi hết, vòng lại khi tới cuối. `Ctrl+Home` / `Ctrl+End` giữ nguyên trong file hiện tại. File comment / noise vẫn nằm trong lộ trình đó chừng nào rule của nó còn tick, nhưng dừng ở một file như vậy không ký duyệt được gì — chỉ change thật và block moved mới vào bản ghi review. - `Ctrl+F` tìm text trong file đang mở, cả hai bên, với `F3` / `Shift+F3` để nhảy qua các kết quả và `Esc` để đóng. Query còn nguyên khi bạn chuyển sang file khác, nên truy một identifier xuyên suốt cả lần compare được. -- `Hide identical` thu cây lại còn đúng các file thật sự khác nhau. Đây thuần là một view — verdict, số đếm và report export ra đều không đổi vì nó. -- Bỏ tick `Comment` / `Unimportant` làm mờ các dòng đó chứ không xoá đi: chúng ở nguyên chỗ cũ, giữ số dòng, chỉ mất màu đỏ/xanh và biến khỏi minimap lẫn lộ trình `F7`/`F8`. Để nguyên tick — mặc định là vậy — chúng giữ màu và là điểm dừng như mọi change khác. +- `Hide identical` thu cây lại còn đúng các file thật sự khác nhau. Chỉ file identical thật mới bị ẩn: file Comment hay Unimportant vẫn nằm đó, dù đang bị làm mờ hay không. Đây thuần là một view — verdict, số đếm và report export ra đều không đổi vì nó. +- Bỏ tick `Comment` / `Unimportant` làm mờ các dòng đó chứ không xoá đi: chúng ở nguyên chỗ cũ, giữ số dòng, chỉ mất màu đỏ/xanh và biến khỏi minimap lẫn lộ trình `F7`/`F8`. Để nguyên tick — mặc định là vậy — chúng giữ màu và là điểm dừng như mọi change khác. **Verdict không đổi** trong cả hai trường hợp: file chỉ khác comment vẫn hiện Comment trên cây và trong report export ra. - Chỗ bạn đang đứng được đánh dấu bằng một mũi tên nhỏ trong cột số dòng, ở cả hai pane, nên `F7`/`F8` vẫn thấy rõ là có nhảy kể cả trong một file ngắn tới mức không có gì để cuộn. - `☀ Light` / `☾ Dark` trên toolbar đổi bảng màu ngay lập tức; `--theme` chỉ chọn màu lúc mở. C, C++, ARXML/XML, A2L, Python, JSON và YAML đều được tô cú pháp ở cả hai theme. | Marker | Verdict | Nghĩa | |---|---|---| | `≠` | Modified | có thay đổi thật | -| `≉` | Comment | chỉ khác comment | +| `≈` | Comment | chỉ khác comment | | `≈` | Unimportant | UUID, timestamp, rename, whitespace | | `+` | Added | file chỉ có ở CURRENT | | `−` | Deleted | file chỉ có ở BASELINE | diff --git a/tests/test_diffpane_qt.py b/tests/test_diffpane_qt.py index d4a5161..df49336 100644 --- a/tests/test_diffpane_qt.py +++ b/tests/test_diffpane_qt.py @@ -698,12 +698,35 @@ def test_comment_only_and_ignorable_only_files_are_walkable_by_default(self): 'ignorable-only') self.assertTrue(self.win._is_nav('arxml/NoiseDemo_implementation.arxml')) - def test_unticking_a_rule_folds_its_files_out_of_the_walk(self): - self.assertTrue(self.win._is_nav('NoiseDemo_autosar_rtw/ert_main.c')) + def test_unticking_a_rule_takes_its_files_out_of_the_walk(self): + rel = 'NoiseDemo_autosar_rtw/ert_main.c' + self.assertTrue(self.win._is_nav(rel)) + self.win.cb_comment.setChecked(False) + self._settle_ui() + self.assertFalse(self.win._is_nav(rel)) + + def test_unticking_a_rule_never_re_judges_the_file(self): + # greying a category is a reading aid, not a verdict: calling a file + # Identical when its comments really did move is the one claim this + # tool must never make, and it would put the tree at odds with the + # exported report, which is always built from the raw scan + rel = 'NoiseDemo_autosar_rtw/ert_main.c' + for checked in (False, True): + self.win.cb_comment.setChecked(checked) + self._settle_ui() + self.assertEqual(self.win.results[rel]['status'], 'comment-only') + + def test_hide_identical_keeps_a_greyed_out_file(self): + # 'Hide identical' hides files that are identical -- not files whose + # differences the reviewer chose to play down + rel = 'NoiseDemo_autosar_rtw/ert_main.c' self.win.cb_comment.setChecked(False) + self.win.cb_hide_identical.setChecked(True) self._settle_ui() - self.assertEqual(self.win.results['NoiseDemo_autosar_rtw/ert_main.c']['status'], 'identical') - self.assertFalse(self.win._is_nav('NoiseDemo_autosar_rtw/ert_main.c')) + self.assertIn(rel, self.win._tree_rels()) + # a genuinely identical file is gone in the same breath + self.assertNotIn('NoiseDemo_autosar_rtw/NoiseDemo.h', + self.win._tree_rels()) def test_next_change_stops_inside_a_shown_comment_only_file_with_no_unit(self): nav = [r for r in self.win._tree_rels() if self.win._is_nav(r)] From 535db9b6b83dc314113a9882232bee28a23022dd Mon Sep 17 00:00:00 2001 From: longvo920 Date: Sun, 23 Aug 2026 13:01:16 +0700 Subject: [PATCH 3/7] feat(viewer): make the whole left column collapsible and resizable The consistency heads-up sat outside the splitter, pinned under it. That made the one pane whose length is least predictable -- a folder full of stale models -- the only one that could not be dragged, and it took its height off the tree without asking. All three panes are now Section widgets in one vertical splitter: Files, Quick changes, Consistency. Each has a bar you click to fold it away, the way an editor sidebar does it, and folding hands that height to the panes still open. A pane remembers the height it had, so folding one to read something else and opening it again puts it back where the reviewer had it rather than wherever Qt's redistribution left it. Folding caps the widget at its bar rather than hiding it. A hidden widget drops out of the splitter altogether, which throws away the dragged size and makes the handle jump the next time it comes back; the bar also has to survive, or there is no way back into a pane once it is shut. The Consistency section only appears when a scan has something to say and carries the count on its bar, so it still says "2 heads-ups" while folded. The category checkboxes moved above the panes: they drive the diff view as much as the tree, so they should not disappear with the tree when Files is folded. Section headers are QPushButton, not QToolButton -- only the former honours text-align:left from a stylesheet, and a centred pane title reads as a heading for the whole column instead of a bar you can click. --- compare_tool/qtviewer/app.py | 158 ++++++++++++++++++++++++++--- compare_tool/qtviewer/dialogs.py | 6 ++ compare_tool/qtviewer/section.py | 94 ++++++++++++++++++ docs/architecture.md | 2 +- docs/usage.md | 1 + docs/vi/usage.md | 1 + tests/test_sections_qt.py | 165 +++++++++++++++++++++++++++++++ 7 files changed, 410 insertions(+), 17 deletions(-) create mode 100644 compare_tool/qtviewer/section.py create mode 100644 tests/test_sections_qt.py diff --git a/compare_tool/qtviewer/app.py b/compare_tool/qtviewer/app.py index 90d2217..e9ac293 100644 --- a/compare_tool/qtviewer/app.py +++ b/compare_tool/qtviewer/app.py @@ -31,6 +31,7 @@ from .diffpane import DiffPane from .icons import ACCENT, app_icon, icon, std_icon from .pickers import pick_commit, pick_folders +from .section import Section from .summary import SummaryPanel from .tree import (STATUS, build_nodes, filter_nodes, move_tooltip, review_color, review_state, status_color, status_label) @@ -185,7 +186,7 @@ def __init__(self, old=None, new=None, exclude=(), arxml_only=False, 'not. Verdicts, counts and the exported report are unchanged.') self.cb_hide_identical.toggled.connect(self._refresh_tree_keep_selection) rules = QHBoxLayout() - rules.setContentsMargins(0, 0, 0, 0) + rules.setContentsMargins(6, 4, 6, 4) rules.addWidget(QLabel('Report:')) rules.addWidget(self.cb_comment) rules.addWidget(self.cb_unimportant) @@ -194,33 +195,60 @@ def __init__(self, old=None, new=None, exclude=(), arxml_only=False, tree_box = QWidget() lv = QVBoxLayout(tree_box) - lv.setContentsMargins(6, 6, 6, 0) + lv.setContentsMargins(6, 4, 6, 0) lv.setSpacing(4) lv.addWidget(self.filter_edit) - lv.addLayout(rules) lv.addWidget(self.tree, 1) # quick-changes rollup under the tree: the same "what changed in the # model / calibration" view --arxml-only gives, without leaving the app self.summary = SummaryPanel() self.summary.fileActivated.connect(self._jump_to_name) + + # cross-artifact / cross-model heads-up, last in the column -- the same + # list the report and the CLI print, read from the raw scan. + self.advisories = AdvisoryPanel() + + # all three panes are collapsible and all three are in ONE splitter, so + # every one of them can be dragged to the height the reviewer wants. + # The advisories used to sit outside it, pinned: the one pane whose + # length is least predictable (a folder full of stale models) was the + # one that could not be resized. + self.sec_files = Section('FILES', tree_box) + self.sec_changes = Section('QUICK CHANGES', self.summary) + self.sec_consistency = Section('CONSISTENCY', self.advisories) + self._sections = (self.sec_files, self.sec_changes, + self.sec_consistency) + + # remembered height per pane, so folding one and opening it again + # puts it back where the reviewer had it + self._sec_height = {} left = QSplitter(Qt.Vertical) - left.addWidget(tree_box) - left.addWidget(self.summary) + for sec in self._sections: + left.addWidget(sec) + sec.toggled.connect( + lambda expanded, s=sec: self._on_section_toggled(s, expanded)) + # a folded pane must not be draggable back open by the handle alone -- + # the header is the way in, and a stray drag would leave a sliver of a + # pane the reviewer explicitly folded + for i in range(left.count()): + left.setCollapsible(i, False) left.setStretchFactor(0, 3) left.setStretchFactor(1, 1) - left.setSizes([560, 240]) + left.setStretchFactor(2, 0) + self.left_split = left + # nothing to say yet: the consistency pane starts folded, and stays out + # of the way until a scan gives it something + self.sec_consistency.set_expanded(False) + self.sec_consistency.setVisible(False) + left.setSizes([560, 240, 0]) - # cross-artifact / cross-model heads-up, pinned below the rollup so it is - # the last thing in the left column -- the same list the report and the - # CLI print, read from the raw scan. Hidden until it has something to say. - self.advisories = AdvisoryPanel() left_col = QWidget() lc = QVBoxLayout(left_col) lc.setContentsMargins(0, 0, 0, 0) lc.setSpacing(0) + lc.addLayout(rules) lc.addWidget(left, 1) - lc.addWidget(self.advisories) self.diff = DiffPane() self.diff.unitChanged.connect(self._on_unit_changed) @@ -937,7 +965,7 @@ def _start_scan(self): self.banner.setVisible(False) self.tree.clear() self.summary.set_results({}) - self.advisories.set_advisories(()) + self._show_advisories(()) self.diff.clear() self._raw_results = {} self.results = {} @@ -975,7 +1003,7 @@ def _on_done(self, results): self.summary.set_results(results) # same rule for the advisories: read from the raw scan, so a collapsed # category can never hide a desync heads-up - self.advisories.set_advisories(consistency_advisories(results)) + self._show_advisories(consistency_advisories(results)) self.progress.setRange(0, 1) self.progress.setValue(1) self.progress.setVisible(False) @@ -1025,6 +1053,96 @@ def _apply_rules(self): else: self._set_state('ready', 'Ready') + # a pane never re-opens at whatever height is left over: the reviewer + # dragged it to a size once, and folding it away to read something else is + # not them changing their mind about that size + _SEC_MIN = 90 + + def _on_section_toggled(self, sec, expanded): + """Fold: hand that pane's height to the panes still open. Unfold: take + it back from them, in proportion to what each has to spare.""" + sizes = self.left_split.sizes() + if len(sizes) != len(self._sections): + return + i = self._sections.index(sec) + bar = sec.header_height() + others = [n for n, s in enumerate(self._sections) + if n != i and s.is_expanded() and s.isVisible()] + if not others: + return # nothing to trade with; leave the bars stacked + if not expanded: + self._sec_height[sec] = max(sizes[i], self._SEC_MIN) + self._spread(sizes, sizes[i] - bar, others) + sizes[i] = bar + else: + want = max(self._sec_height.get(sec, 200), self._SEC_MIN) + got = self._spread(sizes, -(want - sizes[i]), others) + sizes[i] += -got + self.left_split.setSizes(sizes) + + def _spread(self, sizes, amount, idx): + """Add `amount` px across `idx` (negative takes away). Returns what was + actually moved -- a pane is never squeezed below _SEC_MIN, so asking + for more than the column has spare gives back less than was asked.""" + if amount >= 0: + share, rest = divmod(amount, len(idx)) + for n, i in enumerate(idx): + sizes[i] += share + (rest if n == 0 else 0) + return amount + spare = [max(0, sizes[i] - self._SEC_MIN) for i in idx] + take = min(-amount, sum(spare)) + left = take + for n, i in enumerate(idx): + cut = min(spare[n], left) if n < len(idx) - 1 else left + cut = min(cut, spare[n]) + sizes[i] -= cut + left -= cut + return -(take - left) + + def _resize_sections(self): + """Re-cap every folded pane at its bar. Used after the panes change on + their own -- a scan revealing the consistency pane, say.""" + sizes = self.left_split.sizes() + if len(sizes) != len(self._sections): + return + freed = 0 + for i, sec in enumerate(self._sections): + if not sec.is_expanded(): + bar = sec.header_height() + freed += max(0, sizes[i] - bar) + sizes[i] = bar + open_idx = [i for i, s in enumerate(self._sections) + if s.is_expanded() and s.isVisible()] + if freed and open_idx: + self._spread(sizes, freed, open_idx) + elif not open_idx: + return + self.left_split.setSizes(sizes) + + def _show_advisories(self, advisories): + """Fill the consistency pane and open it only when it has something to + say. A clean compare spends no height on it, and a reviewer who folded + it stays folded -- reopening a pane someone shut is the tool arguing + with them.""" + self.advisories.set_advisories(advisories) + # AdvisoryPanel hides itself when empty; inside a section the SECTION is + # what has to go, or an empty bar is left behind + had = self.sec_consistency.isVisible() + n = len(advisories) + self.sec_consistency.setVisible(bool(n)) + self.sec_consistency.set_suffix( + '{} heads-up{}'.format(n, '' if n == 1 else 's') if n else '') + if n and not had: + self.sec_consistency.set_expanded(True) + sizes = self.left_split.sizes() + want = min(140, max(80, 28 * n + 34)) + if len(sizes) == 3 and sizes[2] < want: + take = want - sizes[2] + sizes[2] = want + sizes[0] = max(120, sizes[0] - take) + self.left_split.setSizes(sizes) + self._resize_sections() + def _on_fail(self, msg): self.progress.setVisible(False) self.counts_label.setText('') @@ -1386,9 +1504,17 @@ def _on_select(self): color:{chrome-checked-fg}; }} QToolBar#main QToolButton:checked:hover {{ background:{chrome-checked-hover}; }} QFrame#reviewbar {{ background:{chrome-bar-bg}; border-top:1px solid {border}; }} -/* consistency heads-up pinned at the bottom of the left column: a band of its - own, set off from the quick-changes rollup above it by a top border */ -QFrame#advisorypanel {{ background:{chrome-bar-bg}; border-top:1px solid {border}; }} +/* left-column section bars. Small, flat and quiet: a bar the eye skips over + until it is looking for one, the way an editor sidebar names its panes. The + arrow lives in the label (see section.py) so no icon theme is needed. */ +QPushButton#sectionhead {{ background:{chrome-bar-bg}; color:{header-fg}; + border:none; border-top:1px solid {border}; padding:5px 8px; + font-size:11px; font-weight:bold; text-align:left; }} +QPushButton#sectionhead:hover {{ background:{chrome-hover}; color:{fg-strong}; }} +QPushButton#sectionhead:focus {{ outline:none; }} +/* consistency heads-up at the bottom of the left column: a band of its own, + set off from the quick-changes rollup above it by a top border */ +QFrame#advisorypanel {{ background:{chrome-bar-bg}; }} /* the scroll area AND its viewport: the viewport is a child widget that fills itself with the Base colour, which paints a lighter block under the header instead of letting the band show through */ diff --git a/compare_tool/qtviewer/dialogs.py b/compare_tool/qtviewer/dialogs.py index 13bbd25..c202ed7 100644 --- a/compare_tool/qtviewer/dialogs.py +++ b/compare_tool/qtviewer/dialogs.py @@ -36,6 +36,12 @@ ## 3. Read the tree +- The left column is three panes -- **Files**, **Quick changes**, + **Consistency**. Click a bar to fold that pane away and give its height to + the ones still open; click again and it comes back the size it was +- Drag any bar to resize -- all three share one splitter +- **Consistency** only appears when the scan has a heads-up, and its bar says + how many even while folded - Every file listed -- a verdict never removes a row on its own - A folder shows its heaviest child verdict - Box above the tree filters by path diff --git a/compare_tool/qtviewer/section.py b/compare_tool/qtviewer/section.py new file mode 100644 index 0000000..492898d --- /dev/null +++ b/compare_tool/qtviewer/section.py @@ -0,0 +1,94 @@ +"""One collapsible pane of the left column, the way an editor sidebar does it. + +A `Section` is a header bar plus a content widget. Clicking the header folds the +content away and leaves the bar behind, so a reviewer who is not using the +quick-changes rollup can give that height to the folder tree without losing the +way back to it. + +Collapsing is done by capping the widget's height at the header, not by hiding +the widget: a hidden widget drops out of its `QSplitter` altogether, which +throws away the size the reviewer had dragged it to and makes the handle jump +the next time it comes back. +""" + +from PySide6.QtCore import Signal +from PySide6.QtWidgets import (QPushButton, QSizePolicy, QVBoxLayout, + QWidget) + +# ▾ / ▸ rather than a styled ::indicator: the arrow has to read the same on a +# box with no icon theme installed, which is where this tool usually runs. +_ARROW = {True: '▾', False: '▸'} + +# Qt's own "no maximum"; PySide6 does not re-export QWIDGETSIZE_MAX +_NO_MAX = (1 << 24) - 1 + + +class Section(QWidget): + """Header + content. `toggled(bool)` fires with the new expanded state.""" + + toggled = Signal(bool) + + def __init__(self, title, content, expanded=True): + super().__init__() + self._content = content + self._title = title + + # QPushButton, not QToolButton: only the former honours + # `text-align:left` from a stylesheet, and a centred pane title reads + # as a heading for the whole column rather than a bar you can click + self.header = QPushButton() + self.header.setObjectName('sectionhead') + self.header.setCheckable(True) + self.header.setChecked(expanded) + self.header.setFlat(True) + self.header.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.header.setToolTip('Click to fold this panel away') + self.header.clicked.connect(self._on_click) + + lay = QVBoxLayout(self) + lay.setContentsMargins(0, 0, 0, 0) + lay.setSpacing(0) + lay.addWidget(self.header) + lay.addWidget(content, 1) + + self._suffix = '' + self.set_expanded(expanded) + + # --- state --- + + def is_expanded(self): + return self.header.isChecked() + + def set_expanded(self, expanded): + """Fold or unfold, without emitting `toggled` -- callers that flip a + section programmatically already know they did.""" + self.header.setChecked(expanded) + self._content.setVisible(expanded) + self._relabel() + if expanded: + self.setMaximumHeight(_NO_MAX) + else: + # the bar itself stays, so the splitter keeps a handle to drag and + # the reviewer keeps a way back in + self.setMaximumHeight(self.header.sizeHint().height()) + + def header_height(self): + return self.header.sizeHint().height() + + def set_suffix(self, suffix): + """A count or a warning beside the title, e.g. '2 heads-ups'. Shown on + the bar so a folded section can still say it has something in it.""" + self._suffix = suffix or '' + self._relabel() + + # --- internals --- + + def _on_click(self): + self.set_expanded(self.header.isChecked()) + self.toggled.emit(self.header.isChecked()) + + def _relabel(self): + text = '{} {}'.format(_ARROW[self.header.isChecked()], self._title) + if self._suffix: + text += ' {}'.format(self._suffix) + self.header.setText(text) diff --git a/docs/architecture.md b/docs/architecture.md index 285f937..45ab60f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -85,7 +85,7 @@ second surface can reuse it without the mapping being written twice. compare_tool/ ├── main.py # entry point: picks the CLI or the viewer, run_compare() core ├── resources.py # finds the shipped icons/logo, in a checkout and in the .exe -├── qtviewer/ # PySide6 side-by-side viewer (app, diff pane, minimap, dialogs) +├── qtviewer/ # PySide6 side-by-side viewer (app, diff pane, minimap, dialogs, section.py = the collapsible left-column panes) ├── scanner.py # walks both trees, pairs files by relative path ├── diff_engine.py # two-pass diff (raw + normalized), hunk classification, moved-block detection ├── linediff.py # the line matcher both passes share: patience anchoring, exact fallback diff --git a/docs/usage.md b/docs/usage.md index 3bd65fb..b318e34 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -63,6 +63,7 @@ Once a file is open, a small caption next to its name tracks whatever you're loo ### Reading a scan +- The left column is three panes — **Files**, **Quick changes** and **Consistency**. Click a bar to fold that pane away and hand its height to the panes still open; click again and it comes back the size it was. All three share one splitter, so any of them can be dragged to the height you want. The Consistency pane only appears when the scan has a heads-up, and its bar keeps the count even folded. - The scan **opens on the first change** — you never land on an empty pane next to a tree full of results. - `F8` / `F7` step through the changes in the open file, then carry on into the next (or previous) changed file once you run out, wrapping around at the end. `Ctrl+Home` / `Ctrl+End` stay inside the current file. Comment and noise files join that walk while their category is ticked on, but stopping on one signs off nothing — only real and moved changes ever enter the review record. - `Ctrl+F` finds text in the open file, either side, with `F3` / `Shift+F3` to step through matches and `Esc` to close it. The query survives moving to another file, so you can chase one identifier across the whole compare. diff --git a/docs/vi/usage.md b/docs/vi/usage.md index f0830ac..cb07802 100644 --- a/docs/vi/usage.md +++ b/docs/vi/usage.md @@ -92,6 +92,7 @@ thật sự rời khỏi hàm. ### Đọc một lần scan +- Cột bên trái gồm ba pane — **Files**, **Quick changes** và **Consistency**. Bấm vào thanh tiêu đề để gập pane đó lại và nhường chiều cao cho các pane còn mở; bấm lần nữa thì nó mở lại đúng kích thước cũ. Cả ba nằm chung một splitter nên kéo pane nào cũng được. Pane Consistency chỉ hiện khi lần scan có cảnh báo, và thanh của nó vẫn ghi số cảnh báo kể cả lúc đang gập. - Scan **mở sẵn ở change đầu tiên** — bạn không bao giờ rơi vào một pane trống trong khi cây bên cạnh đầy kết quả. - `F8` / `F7` nhảy qua các change trong file đang mở, rồi đi tiếp sang file có change kế tiếp (hoặc trước đó) một khi hết, vòng lại khi tới cuối. `Ctrl+Home` / `Ctrl+End` giữ nguyên trong file hiện tại. File comment / noise vẫn nằm trong lộ trình đó chừng nào rule của nó còn tick, nhưng dừng ở một file như vậy không ký duyệt được gì — chỉ change thật và block moved mới vào bản ghi review. - `Ctrl+F` tìm text trong file đang mở, cả hai bên, với `F3` / `Shift+F3` để nhảy qua các kết quả và `Esc` để đóng. Query còn nguyên khi bạn chuyển sang file khác, nên truy một identifier xuyên suốt cả lần compare được. diff --git a/tests/test_sections_qt.py b/tests/test_sections_qt.py new file mode 100644 index 0000000..ce29a35 --- /dev/null +++ b/tests/test_sections_qt.py @@ -0,0 +1,165 @@ +"""Collapsible panes in the viewer's left column. + +Skipped when PySide6 is absent, like the other Qt tests, so the suite still +runs headless. What is pinned here is the behaviour a reviewer would notice: +a pane folds to its bar, comes back the size it was, and all three live in one +splitter so every one of them can be dragged. +""" +import os +import time +import unittest +from pathlib import Path + +os.environ.setdefault('QT_QPA_PLATFORM', 'offscreen') + +try: + from PySide6.QtWidgets import QApplication + HAVE_QT = True +except ImportError: # pragma: no cover + HAVE_QT = False + +FIX = Path(__file__).parent / 'fixtures' +_APP = None + + +def _app(): + global _APP + if _APP is None: + _APP = QApplication.instance() or QApplication([]) + return _APP + + +def _settle(app, win, timeout=30.0): + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + app.processEvents() + worker = getattr(win, 'worker', None) + if win._raw_results and not (worker and worker.isRunning()): + for _ in range(5): + app.processEvents() + return + time.sleep(0.01) + raise AssertionError('scan did not finish within {}s'.format(timeout)) + + +@unittest.skipUnless(HAVE_QT, 'PySide6 not installed') +class TestSectionWidget(unittest.TestCase): + def setUp(self): + from PySide6.QtWidgets import QLabel + from compare_tool.qtviewer.section import Section + _app() + self.content = QLabel('body') + self.sec = Section('FILES', self.content) + + def test_starts_expanded_with_its_content(self): + self.assertTrue(self.sec.is_expanded()) + self.assertFalse(self.content.isHidden()) + + def test_folding_hides_the_content_and_caps_the_height(self): + self.sec.set_expanded(False) + self.assertFalse(self.sec.is_expanded()) + self.assertTrue(self.content.isHidden()) + # capped at the bar, so the bar itself survives -- a pane the reviewer + # folded must leave a way back in + self.assertEqual(self.sec.maximumHeight(), self.sec.header_height()) + + def test_unfolding_lifts_the_cap_again(self): + self.sec.set_expanded(False) + self.sec.set_expanded(True) + self.assertFalse(self.content.isHidden()) + self.assertGreater(self.sec.maximumHeight(), self.sec.header_height()) + + def test_the_bar_says_which_way_it_is(self): + self.sec.set_expanded(True) + self.assertIn('FILES', self.sec.header.text()) + open_mark = self.sec.header.text()[0] + self.sec.set_expanded(False) + self.assertNotEqual(self.sec.header.text()[0], open_mark) + + def test_suffix_rides_along_on_the_bar(self): + # a folded pane still has to be able to say it holds something + self.sec.set_suffix('2 heads-ups') + self.sec.set_expanded(False) + self.assertIn('2 heads-ups', self.sec.header.text()) + + +@unittest.skipUnless(HAVE_QT, 'PySide6 not installed') +class TestLeftColumnSections(unittest.TestCase): + @classmethod + def setUpClass(cls): + from compare_tool.qtviewer.app import MainWindow, apply_theme + cls.app = _app() + apply_theme(cls.app) # the QSS the real entry point applies + demo = FIX / 'demo' + cls.win = MainWindow(str(demo / 'old'), str(demo / 'new')) + cls.win.resize(1400, 900) + cls.win.show() + _settle(cls.app, cls.win) + + @classmethod + def tearDownClass(cls): + cls.win.close() + + def setUp(self): + for sec in self.win._sections: + sec.set_expanded(True) + self.win.left_split.setSizes([460, 230, 120]) + self._settle_ui() + + def _settle_ui(self): + for _ in range(40): + self.app.processEvents() + + def test_all_three_panes_share_one_splitter(self): + # the advisories used to be pinned outside it, which is exactly why + # that pane -- the one whose length is least predictable -- was the one + # that could not be resized + self.assertEqual(self.win.left_split.count(), 3) + self.assertEqual(len(self.win.left_split.sizes()), 3) + for sec in self.win._sections: + self.assertIs(sec.parent(), self.win.left_split) + + def test_folding_a_pane_gives_its_height_to_the_others(self): + before = self.win.left_split.sizes() + self.win.sec_changes.header.click() + self._settle_ui() + after = self.win.left_split.sizes() + self.assertEqual(after[1], self.win.sec_changes.header_height()) + self.assertGreater(after[0] + after[2], before[0] + before[2]) + self.assertEqual(sum(after), sum(before)) + + def test_a_pane_comes_back_the_size_it_was(self): + want = self.win.left_split.sizes()[1] + self.win.sec_changes.header.click() # fold + self._settle_ui() + self.win.sec_changes.header.click() # and back + self._settle_ui() + # within a pixel or two of where the reviewer had left it -- folding a + # pane to read something else is not them resizing it + self.assertAlmostEqual(self.win.left_split.sizes()[1], want, delta=4) + + def test_folding_the_tree_leaves_its_bar_behind(self): + self.win.sec_files.header.click() + self._settle_ui() + # the tree is inside the section's content widget, so it is the + # content that gets hidden -- isVisible() is what a reviewer sees + self.assertFalse(self.win.tree.isVisible()) + self.assertEqual(self.win.left_split.sizes()[0], + self.win.sec_files.header_height()) + self.win.sec_files.header.click() + self._settle_ui() + self.assertTrue(self.win.tree.isVisible()) + + def test_consistency_pane_carries_the_count_on_its_bar(self): + # this fixture has two, and the bar has to say so even folded + self.assertFalse(self.win.sec_consistency.isHidden()) + self.assertIn('2 heads-ups', self.win.sec_consistency.header.text()) + + def test_the_consistency_pane_goes_away_when_there_is_nothing_to_say(self): + self.win._show_advisories(()) + self._settle_ui() + self.assertTrue(self.win.sec_consistency.isHidden()) + + +if __name__ == '__main__': + unittest.main() From ccf6164ee43a0aac1ca92be977d9c4e833c61440 Mon Sep 17 00:00:00 2001 From: longvo920 Date: Sun, 23 Aug 2026 13:22:20 +0700 Subject: [PATCH 4/7] fix(report): one source for the verdict mark, and tidy the folded column MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The report's tree and the viewer's tree each carried their own table of verdict marks. They drifted: a comment-only file read '≉' in the report and '≈' in the viewer, so the same file looked like two different findings depending on which surface you opened. Both now read view_model.VERDICT_MARK, and a test pins them to it so a copy cannot creep back in. Two things the real window showed that an offscreen grab could not: - with every pane folded, Qt centred the splitter in the column and left the three bars adrift halfway down an empty panel. Folded flat, the splitter is capped and top-aligned, so the bars stack under the toolbar. - the Comment / Unimportant / Hide identical row floated above all three panes and was stranded mid-panel once they were all folded. It belongs to the tree, so it now lives inside the Files pane and folds away with it. --- CLAUDE.md | 8 +++--- compare_tool/qtviewer/app.py | 27 +++++++++++++++++--- compare_tool/qtviewer/section.py | 4 +-- compare_tool/qtviewer/tree.py | 44 +++++++++++++++----------------- compare_tool/report.py | 23 +++++++++-------- compare_tool/view_model.py | 19 ++++++++++++++ docs/usage.md | 2 +- docs/vi/usage.md | 2 +- tests/test_qtviewer.py | 18 +++++++++++++ tests/test_sections_qt.py | 33 ++++++++++++++++++++++++ 10 files changed, 137 insertions(+), 43 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7bd0550..00e3f98 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -63,9 +63,11 @@ Removing those rows instead of greying them was tried and reverted too: a regenerated file is mostly banner churn, so a `⋯ N lines hidden` placeholder took away the context the surviving hunks have to be read in. -Both noise verdicts wear the same `≈` mark in the tree — they are one answer to -"must I read this?", and the Status word beside it says which kind of nothing -it is. +Both noise verdicts wear the same `≈` mark, in the report's tree and the +viewer's alike — they are one answer to "must I read this?", and the label +beside the mark says which kind of nothing it is. The mark lives once, in +`view_model.VERDICT_MARK`: the two trees each kept a copy and drifted, so the +same file read `≉` in one and `≈` in the other. ## 3. One seam per shared decision diff --git a/compare_tool/qtviewer/app.py b/compare_tool/qtviewer/app.py index e9ac293..5f60d7e 100644 --- a/compare_tool/qtviewer/app.py +++ b/compare_tool/qtviewer/app.py @@ -31,7 +31,7 @@ from .diffpane import DiffPane from .icons import ACCENT, app_icon, icon, std_icon from .pickers import pick_commit, pick_folders -from .section import Section +from .section import NO_MAX, Section from .summary import SummaryPanel from .tree import (STATUS, build_nodes, filter_nodes, move_tooltip, review_color, review_state, status_color, status_label) @@ -197,6 +197,7 @@ def __init__(self, old=None, new=None, exclude=(), arxml_only=False, lv = QVBoxLayout(tree_box) lv.setContentsMargins(6, 4, 6, 0) lv.setSpacing(4) + lv.addLayout(rules) lv.addWidget(self.filter_edit) lv.addWidget(self.tree, 1) @@ -247,8 +248,8 @@ def __init__(self, old=None, new=None, exclude=(), arxml_only=False, lc = QVBoxLayout(left_col) lc.setContentsMargins(0, 0, 0, 0) lc.setSpacing(0) - lc.addLayout(rules) lc.addWidget(left, 1) + self._left_layout = lc self.diff = DiffPane() self.diff.unitChanged.connect(self._on_unit_changed) @@ -1069,7 +1070,8 @@ def _on_section_toggled(self, sec, expanded): others = [n for n, s in enumerate(self._sections) if n != i and s.is_expanded() and s.isVisible()] if not others: - return # nothing to trade with; leave the bars stacked + self._park_column() # last pane folded: stack them at the top + return if not expanded: self._sec_height[sec] = max(sizes[i], self._SEC_MIN) self._spread(sizes, sizes[i] - bar, others) @@ -1079,6 +1081,7 @@ def _on_section_toggled(self, sec, expanded): got = self._spread(sizes, -(want - sizes[i]), others) sizes[i] += -got self.left_split.setSizes(sizes) + self._park_column() def _spread(self, sizes, amount, idx): """Add `amount` px across `idx` (negative takes away). Returns what was @@ -1099,6 +1102,21 @@ def _spread(self, sizes, amount, idx): left -= cut return -(take - left) + def _park_column(self): + """With every pane folded the splitter is shorter than the column it + sits in. Left alone, Qt centres it and the bars end up adrift halfway + down an empty panel; capped and top-aligned they stack under the + toolbar, which is where a row of collapsed headers belongs.""" + shown = [s for s in self._sections if s.isVisible()] + if any(s.is_expanded() for s in shown): + self._left_layout.setAlignment(self.left_split, Qt.Alignment(0)) + self.left_split.setMaximumHeight(NO_MAX) + return + bars = sum(s.header_height() for s in shown) + handles = self.left_split.handleWidth() * max(0, len(shown) - 1) + self.left_split.setMaximumHeight(bars + handles) + self._left_layout.setAlignment(self.left_split, Qt.AlignTop) + def _resize_sections(self): """Re-cap every folded pane at its bar. Used after the panes change on their own -- a scan revealing the consistency pane, say.""" @@ -1116,8 +1134,11 @@ def _resize_sections(self): if freed and open_idx: self._spread(sizes, freed, open_idx) elif not open_idx: + self.left_split.setSizes(sizes) + self._park_column() return self.left_split.setSizes(sizes) + self._park_column() def _show_advisories(self, advisories): """Fill the consistency pane and open it only when it has something to diff --git a/compare_tool/qtviewer/section.py b/compare_tool/qtviewer/section.py index 492898d..3c28c4c 100644 --- a/compare_tool/qtviewer/section.py +++ b/compare_tool/qtviewer/section.py @@ -20,7 +20,7 @@ _ARROW = {True: '▾', False: '▸'} # Qt's own "no maximum"; PySide6 does not re-export QWIDGETSIZE_MAX -_NO_MAX = (1 << 24) - 1 +NO_MAX = (1 << 24) - 1 class Section(QWidget): @@ -66,7 +66,7 @@ def set_expanded(self, expanded): self._content.setVisible(expanded) self._relabel() if expanded: - self.setMaximumHeight(_NO_MAX) + self.setMaximumHeight(NO_MAX) else: # the bar itself stays, so the splitter keeps a handle to drag and # the reviewer keeps a way back in diff --git a/compare_tool/qtviewer/tree.py b/compare_tool/qtviewer/tree.py index 26467eb..0424418 100644 --- a/compare_tool/qtviewer/tree.py +++ b/compare_tool/qtviewer/tree.py @@ -8,26 +8,24 @@ from collections import namedtuple from .. import filepair, theme - -# status -> (tree marker, display label, theme role). Mirrors the HTML -# report's verdict vocabulary (Modified / Unimportant / Added / Deleted / -# Identical) and reads its colours from the SAME roles the report's CSS does, -# so the viewer and the report cannot disagree about what Modified looks like -# -- in either theme. -STATUS = { - 'real-change': ('≠', 'Modified', 'st-real'), # not-equal sign - # the two noise verdicts are grey on purpose: grey is what "this does not - # count" looks like, and it keeps red/green meaning removed/added only - # both carry the same almost-equal sign: they are the same answer to - # "does this differ in a way I have to read?" -- the Status word beside it - # is what separates a moved banner from a renamed identifier - 'comment-only': ('≈', 'Comment', 'st-cmt'), - 'ignorable-only': ('≈', 'Unimportant', 'st-ign'), - 'added': ('+', 'Added', 'st-add'), - 'deleted': ('−', 'Deleted', 'st-del'), # minus sign - 'identical': ('=', 'Identical', 'st-id'), - 'error': ('!', 'NOT compared', 'st-err'), -} +from ..view_model import VERDICT_MARK + +# status -> (tree marker, display label, theme role). The mark comes from +# view_model.VERDICT_MARK, which the report's tree reads too; the colours are +# the SAME theme roles the report's CSS uses. Neither surface owns a copy, so +# they cannot disagree about what a verdict looks like -- in either theme. +# +# The two noise verdicts are grey on purpose: grey is what "this does not +# count" looks like, and it keeps red/green meaning removed/added only. +STATUS = {status: (VERDICT_MARK[status],) + rest for status, rest in { + 'real-change': ('Modified', 'st-real'), + 'comment-only': ('Comment', 'st-cmt'), + 'ignorable-only': ('Unimportant', 'st-ign'), + 'added': ('Added', 'st-add'), + 'deleted': ('Deleted', 'st-del'), + 'identical': ('Identical', 'st-id'), + 'error': ('NOT compared', 'st-err'), +}.items()} def status_color(status): @@ -144,9 +142,9 @@ def filter_nodes(nodes, text='', hide_identical=False): past hundreds of `=` rows to reach five changed ones is its own kind of hiding -- the reviewer gets to make that call with a button they can undo. - Note this composes with the compare rules rather than fighting them: a - folded category re-judges its files as identical, so hiding identical - files hides those too. That is the point -- both say "this does not count". + Only a genuinely identical file goes. A Comment or Unimportant file whose + rows the reviewer greyed out with the category toggles is still a file that + differs, and it keeps its row: greying is a reading aid, not a verdict. """ text = text.strip().lower() if not text and not hide_identical: diff --git a/compare_tool/report.py b/compare_tool/report.py index efe2882..57d452d 100644 --- a/compare_tool/report.py +++ b/compare_tool/report.py @@ -15,7 +15,8 @@ from .diff_engine import ruleset_for from .scanner import (looks_binary, read_text, summarize, summarize_a2l, summarize_ifaces, summarize_rte, summarize_swcs) -from .view_model import (A2L_KINDS, SWC_DISPLAY, a2l_kind_label, char_span, +from .view_model import (A2L_KINDS, SWC_DISPLAY, VERDICT_MARK, + a2l_kind_label, char_span, iface_kind, mode_of, swc_item) CONTEXT = 3 @@ -777,15 +778,17 @@ def _row(o_no, o_txt, n_no, n_txt, mode, language=None, # status -> (tree marker, marker css class, section css class for badge toggling) # 'error' has no body.hide-* CSS rule on purpose: it can never be hidden -_TREE = { - 'real-change': ('≠', 't-real', 'sec-real'), # ≠ - 'comment-only': ('≉', 't-cmt', 'sec-cmt'), # ≉ comments only - 'ignorable-only': ('≈', 't-ign', 'sec-ign'), # ≈ minor - 'added': ('+', 't-add', 'sec-add'), - 'deleted': ('−', 't-del', 'sec-del'), # − - 'identical': ('=', 't-id', 'sec-id'), - 'error': ('!', 't-err', 'sec-err'), -} +# the mark itself comes from view_model.VERDICT_MARK, which the viewer's tree +# reads too -- the two used to keep their own copy and drifted apart +_TREE = {status: (VERDICT_MARK[status],) + cls for status, cls in { + 'real-change': ('t-real', 'sec-real'), + 'comment-only': ('t-cmt', 'sec-cmt'), + 'ignorable-only': ('t-ign', 'sec-ign'), + 'added': ('t-add', 'sec-add'), + 'deleted': ('t-del', 'sec-del'), + 'identical': ('t-id', 'sec-id'), + 'error': ('t-err', 'sec-err'), +}.items()} # status -> (display label, tag css class); the wording a reviewer already # expects from a folder compare -- Modified/Added/Deleted for the file's fate, # Unimportant/Identical for the ones that need no reading diff --git a/compare_tool/view_model.py b/compare_tool/view_model.py index e7bf1f5..00e0f64 100644 --- a/compare_tool/view_model.py +++ b/compare_tool/view_model.py @@ -49,6 +49,25 @@ # says WHY it was played down (uuid, comment, rename, …). MUTED = 'muted' +# The one-character mark a verdict wears in a file tree, for BOTH trees -- the +# report's and the viewer's. It lived in each of them once and they drifted: +# the same file read '≉' in one and '≈' in the other, which is the whole +# failure mode "one seam per shared decision" exists to stop. +# +# Comment and Unimportant deliberately share a mark. They are one answer to +# "must I read this?"; the label beside the mark (Comment / Unimportant) is +# what separates a moved banner from a renamed identifier, and each surface +# spells that label its own way. +VERDICT_MARK = { + 'real-change': '≠', + 'comment-only': '≈', + 'ignorable-only': '≈', + 'added': '+', + 'deleted': '−', + 'identical': '=', + 'error': '!', +} + # How the SWC sub-categories are SPELLED, in the two forms the surfaces need: # `title` heads a section, `noun` sits inline in a chip or a one-line note. # Both are capitalised: these are labels, and a lower-case one beside 'SWC', diff --git a/docs/usage.md b/docs/usage.md index b318e34..0e609d6 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -208,7 +208,7 @@ Each real change shows three lines of context on either side of it — not the w - Hunks that fall **outside every window** show nothing at all until you click `Unimportant`, at which point they appear flat grey exactly where they sit in the file. - A file with **no** real change at all keeps its full context, and its collapsed hunks keep a `⋯ N lines hidden` placeholder rather than vanishing. -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).) +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).) `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. diff --git a/docs/vi/usage.md b/docs/vi/usage.md index cb07802..de41a09 100644 --- a/docs/vi/usage.md +++ b/docs/vi/usage.md @@ -349,7 +349,7 @@ xung quanh: Các dòng đó không bị xoá khỏi file — chỉ là report không hiện chúng ra. File mà khác biệt *chỉ* là comment thì không có mục chi tiết riêng; nó chỉ giữ marker -`≉` và được đếm vào `Comment` trên cây. (Vì sao cửa sổ ngữ cảnh hẹp như vậy chứ +`≈` và được đếm vào `Comment` trên cây. (Vì sao cửa sổ ngữ cảnh hẹp như vậy 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).) diff --git a/tests/test_qtviewer.py b/tests/test_qtviewer.py index d9ab6fe..b3fdd9a 100644 --- a/tests/test_qtviewer.py +++ b/tests/test_qtviewer.py @@ -63,6 +63,24 @@ def test_every_status_has_metadata(self): for name in theme.THEMES: self.assertTrue(theme.color(role, name).startswith('#')) + def test_the_two_trees_mark_a_verdict_the_same_way(self): + # the viewer's tree and the report's tree each kept their own copy of + # the marks once, and they drifted: the same comment-only file read + # '≉' in the report and '≈' in the viewer. Both read VERDICT_MARK now, + # and this is what stops a copy creeping back in. + from compare_tool.report import _TREE + from compare_tool.view_model import VERDICT_MARK + for st in PRIO: + self.assertEqual(STATUS[st][0], VERDICT_MARK[st], st) + self.assertEqual(_TREE[st][0], VERDICT_MARK[st], st) + + def test_the_two_noise_verdicts_share_one_mark(self): + # they are one answer to "must I read this?"; the label beside the mark + # is what tells a moved banner from a renamed identifier + self.assertEqual(STATUS['comment-only'][0], STATUS['ignorable-only'][0]) + self.assertNotEqual(STATUS['comment-only'][1], + STATUS['ignorable-only'][1]) + class TestFilterNodes(unittest.TestCase): def _nodes(self, mapping): diff --git a/tests/test_sections_qt.py b/tests/test_sections_qt.py index ce29a35..42c3cad 100644 --- a/tests/test_sections_qt.py +++ b/tests/test_sections_qt.py @@ -150,6 +150,39 @@ def test_folding_the_tree_leaves_its_bar_behind(self): self._settle_ui() self.assertTrue(self.win.tree.isVisible()) + def test_folding_everything_stacks_the_bars_at_the_top(self): + # left alone Qt centres a splitter shorter than the space it is given, + # which left the three bars adrift halfway down an empty panel + for sec in self.win._sections: + if sec.is_expanded(): + sec.header.click() + self._settle_ui() + tops = [sec.mapTo(self.win.left_split, sec.rect().topLeft()).y() + for sec in self.win._sections if sec.isVisible()] + self.assertEqual(tops[0], 0) + self.assertEqual(tops, sorted(tops)) + # and the splitter itself is capped, so it cannot claim the empty space + self.assertLess(self.win.left_split.maximumHeight(), 200) + + def test_opening_one_again_lets_the_column_fill(self): + for sec in self.win._sections: + if sec.is_expanded(): + sec.header.click() + self._settle_ui() + self.win.sec_files.header.click() + self._settle_ui() + self.assertGreater(self.win.left_split.maximumHeight(), 1000) + self.assertTrue(self.win.tree.isVisible()) + + def test_the_rule_checkboxes_live_inside_the_files_pane(self): + # they used to float above all three panes, which left them stranded + # mid-panel once every pane was folded + self.assertTrue(self.win.cb_comment.isVisible()) + self.win.sec_files.header.click() + self._settle_ui() + self.assertFalse(self.win.cb_comment.isVisible()) + self.assertFalse(self.win.cb_hide_identical.isVisible()) + def test_consistency_pane_carries_the_count_on_its_bar(self): # this fixture has two, and the bar has to say so even folded self.assertFalse(self.win.sec_consistency.isHidden()) From 88a41fbd968123f99fa01d6973426bf1363a2ebd Mon Sep 17 00:00:00 2001 From: longvo920 Date: Sun, 23 Aug 2026 13:32:32 +0700 Subject: [PATCH 5/7] fix(viewer): stop the consistency pane sprawling when it is the only one open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fold Files and Quick changes and the Consistency pane got the whole column -- but its scroll area was still capped at 120px from the days it was pinned under the splitter and could not be resized. Qt then had ~700px of unclaimed height to place, so the text ended up stranded in the middle of an empty panel with a gap above it and below it. The pane is a section the reviewer can drag now, so the height is theirs to set: the cap is gone and the scroll area fills whatever the section is given, scrolling past that. Its own "⚠ Consistency — 2 heads-ups" heading is gone too. The section bar directly above it already reads "CONSISTENCY 2 heads-ups", and in a panel this narrow a row spent restating the line above it is a row not spent on which model is out of step. The count still shows while the pane is folded, which is what the heading was really for. --- compare_tool/qtviewer/advisories.py | 42 +++++++++++++---------------- docs/usage.md | 2 +- docs/vi/usage.md | 4 +-- tests/test_advisory_panel_qt.py | 12 ++++----- tests/test_sections_qt.py | 7 +++++ 5 files changed, 35 insertions(+), 32 deletions(-) diff --git a/compare_tool/qtviewer/advisories.py b/compare_tool/qtviewer/advisories.py index 3316d34..c148eb5 100644 --- a/compare_tool/qtviewer/advisories.py +++ b/compare_tool/qtviewer/advisories.py @@ -1,6 +1,6 @@ -"""Consistency advisories under the quick-changes panel: the same cross-artifact -and cross-model heads-up the HTML report and the CLI print, shown live in the -viewer's bottom-left. +"""Consistency advisories: the same cross-artifact and cross-model heads-up +the HTML report and the CLI print, shown live in the viewer's Consistency +section (last of the three panes in the left column). Display only, exactly like the report's block -- it names the model and the caution, never folds a file, moves a count or changes the exit code. The text @@ -19,17 +19,15 @@ class AdvisoryPanel(QFrame): - """Pinned strip at the very bottom of the left column. Hidden outright when - there is nothing to say, so a clean compare spends no height on it.""" + """Body of the left column's Consistency section. Hidden outright when + there is nothing to say, so a clean compare spends no height on it -- the + section around it goes with it (see MainWindow._show_advisories).""" def __init__(self): super().__init__() self.setObjectName('advisorypanel') self._advisories = [] - self._header = QLabel() - self._header.setObjectName('advisoryhead') - self._body = QLabel() self._body.setWordWrap(True) self._body.setAlignment(Qt.AlignTop) @@ -37,20 +35,22 @@ def __init__(self): # ticket, and selection never triggers navigation self._body.setTextInteractionFlags(Qt.TextSelectableByMouse) - # bounded height: a folder full of stale models must not eat the tree - # above it, so past a few rows the strip scrolls instead of growing + # the pane is a section the reviewer can drag, so the height is theirs + # to set: the panel fills whatever it is given and scrolls past that. + # It used to be capped at 120px from the days it was pinned under the + # splitter and could not be resized -- with the cap still in, opening + # this pane alone left the text stranded in the middle of an empty + # panel with the rest of the height unused. scroll = QScrollArea() scroll.setWidgetResizable(True) scroll.setFrameShape(QFrame.NoFrame) scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - scroll.setMaximumHeight(120) scroll.setWidget(self._body) lay = QVBoxLayout(self) lay.setContentsMargins(8, 6, 8, 8) lay.setSpacing(4) - lay.addWidget(self._header) - lay.addWidget(scroll) + lay.addWidget(scroll, 1) self.setVisible(False) def set_advisories(self, advisories): @@ -61,19 +61,15 @@ def set_advisories(self, advisories): if not self._advisories: self.setVisible(False) return - n = len(self._advisories) - self._header.setText('⚠ Consistency — {} heads-up{}'.format( - n, '' if n == 1 else 's')) - # apply_theme owns both the header style and the body render, so the - # first show is painted in the current theme without a separate init call - self.apply_theme() + # no heading of its own: the section bar above already says + # "CONSISTENCY 2 heads-ups", and repeating it here spends a row of a + # narrow panel restating what the reviewer just read + self.apply_theme() # paints the body in the current theme self.setVisible(True) def apply_theme(self): - """Colours are stamped per label, so a theme switch has to repaint them - from the advisories the panel was last given.""" - self._header.setStyleSheet( - 'color:{}; font-weight:bold;'.format(theme.c('mv-fg'))) + """Colours are stamped into the body's markup, so a theme switch has to + re-render it from the advisories the panel was last given.""" self._render() def _render(self): diff --git a/docs/usage.md b/docs/usage.md index 0e609d6..32f4ad3 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -184,7 +184,7 @@ Files are grouped by **Simulink model**, following the Embedded Coder AUTOSAR na A model's ARXML declares its interface — which ports, runnables and events it has. Its A2L declares the calibration and measurement variables. The generated C has to implement both: add a port in the ARXML and the code needs a matching `Rte_*` call, add a characteristic in the A2L and the code needs a matching variable. -That relationship only runs **one way**. When a port, runnable, event or calibration variable is added or removed in the ARXML or A2L while that model's C file doesn't change by a single byte, something is wrong: the report (just below the AUTOSAR changes), the viewer (bottom-left, under the quick-changes panel) and the terminal all name that model. The usual cause is a regenerate that didn't finish, or that skipped a model. A file-by-file diff can't catch it, because each file is fine on its own — what's wrong is that the two no longer agree. +That relationship only runs **one way**. When a port, runnable, event or calibration variable is added or removed in the ARXML or A2L while that model's C file doesn't change by a single byte, something is wrong: the report (just below the AUTOSAR changes), the viewer (its Consistency pane, last in the left column) and the terminal all name that model. The usual cause is a regenerate that didn't finish, or that skipped a model. A file-by-file diff can't catch it, because each file is fine on its own — what's wrong is that the two no longer agree. The check is measured **per access point, not per file**: it needs a port interface or an SWC port/runnable/event added or removed in the ARXML, or a calibration object added or removed in the A2L. Every export also rewrites the shared library packages — base types, compu-methods, units — which changes plenty of bytes without touching a single port or runnable, so those alone never raise the advisory. diff --git a/docs/vi/usage.md b/docs/vi/usage.md index de41a09..05ed1ed 100644 --- a/docs/vi/usage.md +++ b/docs/vi/usage.md @@ -290,8 +290,8 @@ characteristic trong A2L thì trong code phải có thêm biến tương ứng. Quan hệ này chỉ đi **một chiều**. Khi ARXML hoặc A2L có thêm/bớt một port, runnable, event hay biến calibration mà file C của model đó không đổi một byte -nào, thì có gì đó sai: report (ngay dưới phần AUTOSAR changes), viewer (góc -dưới bên trái, dưới panel quick-changes) và terminal đều nêu tên model đó ra. +nào, thì có gì đó sai: report (ngay dưới phần AUTOSAR changes), viewer (pane +Consistency, cuối cột bên trái) và terminal đều nêu tên model đó ra. Nguyên nhân thường gặp là lần regenerate chạy chưa xong hoặc chạy thiếu model. Một diff xem từng file riêng lẻ không phát hiện được, vì bản thân từng file đều bình thường — cái sai nằm ở chỗ hai file không khớp nhau. diff --git a/tests/test_advisory_panel_qt.py b/tests/test_advisory_panel_qt.py index 74ea030..fe913b0 100644 --- a/tests/test_advisory_panel_qt.py +++ b/tests/test_advisory_panel_qt.py @@ -1,4 +1,4 @@ -"""Consistency advisory panel (viewer bottom-left). +"""Consistency advisory panel (the viewer's Consistency section). Skipped when PySide6 is absent, so the rest of the suite still runs headless (see the "viewer logic that can be Qt-free must be Qt-free" rule). The advisory @@ -53,21 +53,21 @@ def test_empty_is_hidden(self): self.panel.set_advisories([]) self.assertFalse(self.panel.isVisible()) - def test_populated_is_visible_and_counts(self): + def test_populated_is_visible_and_lists_every_model(self): self.panel.set_advisories([('Ctrl', 'gained an RTE access'), ('Brake', 'ARXML changed but the C did not')]) # QWidget.isVisible() is False until shown, but isVisibleTo(parent) and # the non-hidden flag both report the intent set here self.assertFalse(self.panel.isHidden()) - self.assertIn('2 heads-ups', self.panel._header.text()) body = self.panel._body.text() self.assertIn('Ctrl', body) self.assertIn('Brake', body) - def test_singular_header(self): + def test_the_panel_does_not_restate_the_section_bar(self): + # the bar above it already reads "CONSISTENCY 2 heads-ups"; a heading + # in here would spend a row of a narrow panel saying it twice self.panel.set_advisories([('Ctrl', 'gained an RTE access')]) - self.assertIn('1 heads-up', self.panel._header.text()) - self.assertNotIn('heads-ups', self.panel._header.text()) + self.assertNotIn('heads-up', self.panel._body.text()) def test_html_in_message_is_escaped(self): self.panel.set_advisories([('A', 'x & y ')]) diff --git a/tests/test_sections_qt.py b/tests/test_sections_qt.py index 42c3cad..f623ca5 100644 --- a/tests/test_sections_qt.py +++ b/tests/test_sections_qt.py @@ -188,6 +188,13 @@ def test_consistency_pane_carries_the_count_on_its_bar(self): self.assertFalse(self.win.sec_consistency.isHidden()) self.assertIn('2 heads-ups', self.win.sec_consistency.header.text()) + def test_one_heads_up_reads_singular_on_the_bar(self): + self.win._show_advisories([('Ctrl', 'gained an RTE access')]) + self._settle_ui() + text = self.win.sec_consistency.header.text() + self.assertIn('1 heads-up', text) + self.assertNotIn('heads-ups', text) + def test_the_consistency_pane_goes_away_when_there_is_nothing_to_say(self): self.win._show_advisories(()) self._settle_ui() From c94e1e0c72e8f8afd232b224866c50456d89e1eb Mon Sep 17 00:00:00 2001 From: longvo920 Date: Sun, 23 Aug 2026 13:38:26 +0700 Subject: [PATCH 6/7] fix(viewer): remember a pane's size when it is the last one folded Folding the last open pane takes an early exit -- there is nobody to hand the height to -- and the line that records the size sat after it. Fold the tree and the rollup, drag Consistency to fill the column, fold that too, then open the tree and come back: Consistency reopened at the 200px default instead of the height it had. Recording now happens before the exit. Also drops a redundant ternary in _spread. Both branches computed min(spare[n], left); 20k random cases confirm the two forms never differ, so it was only there to be re-read. --- compare_tool/qtviewer/app.py | 9 ++++++--- tests/test_sections_qt.py | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/compare_tool/qtviewer/app.py b/compare_tool/qtviewer/app.py index 5f60d7e..ca5f165 100644 --- a/compare_tool/qtviewer/app.py +++ b/compare_tool/qtviewer/app.py @@ -1067,13 +1067,17 @@ def _on_section_toggled(self, sec, expanded): return i = self._sections.index(sec) bar = sec.header_height() + # remember the height FIRST: folding the last open pane returns early + # below, and skipping this there lost the size -- reopening some other + # pane and coming back to this one then landed on the default + if not expanded: + self._sec_height[sec] = max(sizes[i], self._SEC_MIN) others = [n for n, s in enumerate(self._sections) if n != i and s.is_expanded() and s.isVisible()] if not others: self._park_column() # last pane folded: stack them at the top return if not expanded: - self._sec_height[sec] = max(sizes[i], self._SEC_MIN) self._spread(sizes, sizes[i] - bar, others) sizes[i] = bar else: @@ -1096,8 +1100,7 @@ def _spread(self, sizes, amount, idx): take = min(-amount, sum(spare)) left = take for n, i in enumerate(idx): - cut = min(spare[n], left) if n < len(idx) - 1 else left - cut = min(cut, spare[n]) + cut = min(spare[n], left) sizes[i] -= cut left -= cut return -(take - left) diff --git a/tests/test_sections_qt.py b/tests/test_sections_qt.py index f623ca5..920168a 100644 --- a/tests/test_sections_qt.py +++ b/tests/test_sections_qt.py @@ -138,6 +138,23 @@ def test_a_pane_comes_back_the_size_it_was(self): # pane to read something else is not them resizing it self.assertAlmostEqual(self.win.left_split.sizes()[1], want, delta=4) + def test_the_last_pane_folded_still_remembers_its_size(self): + # folding the last open pane takes an early exit (there is nobody to + # hand the height to). Recording the size has to happen before that + # exit, or reopening a different pane first and coming back to this one + # lands on the default instead of where the reviewer had it. + self.win.sec_files.header.click() + self.win.sec_changes.header.click() + self._settle_ui() + want = self.win.left_split.sizes()[2] + self.win.sec_consistency.header.click() # the last one open + self._settle_ui() + self.win.sec_files.header.click() # a DIFFERENT pane first + self._settle_ui() + self.win.sec_consistency.header.click() # now back to it + self._settle_ui() + self.assertAlmostEqual(self.win.left_split.sizes()[2], want, delta=8) + def test_folding_the_tree_leaves_its_bar_behind(self): self.win.sec_files.header.click() self._settle_ui() From 43d6e04a79b6a8182de1817f8e5f35d425468447 Mon Sep 17 00:00:00 2001 From: longvo920 Date: Sun, 23 Aug 2026 13:42:30 +0700 Subject: [PATCH 7/7] chore: drop apply_fold, and log this release's user-visible changes apply_fold re-judged an already-scanned tree so the viewer could collapse a category on a toggle. That is the behaviour this branch removed, and nothing else ever called it -- there is no --fold flag, so no shipped front end reached it. scan(fold=) and fold_status stay: a caller can still ask for the collapse up front. The Vietnamese architecture doc still described the old fold path in full; it now matches the English one. --- CHANGELOG.md | 20 ++++++++++++++++ compare_tool/scanner.py | 18 --------------- docs/architecture.md | 9 ++++---- docs/vi/architecture.md | 51 +++++++++++++++++++++++------------------ 4 files changed, 54 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 871ee48..42503c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,26 @@ All notable changes to this project are documented here. Versions follow ## [Unreleased] +### Added + +- **Fold and resize the panes in the viewer's left column.** The folder tree, + the quick-changes rollup and the consistency heads-up each have a bar you can + click to fold that pane away and give its height to the others; click again + and it comes back the size you left it. All three can be dragged, including + the heads-up pane, which was previously pinned at a fixed height. + +### Changed + +- **Turning off Comment or Unimportant no longer reports those files as + Identical.** Their lines still grey out and drop off the minimap and F7/F8, + but the file keeps its real verdict everywhere — in the tree, in the counts + and in an exported report. `Hide identical` follows suit and hides only files + that genuinely have no difference. Previously a file whose comments really + had moved could read as Identical on screen while the exported report said + otherwise. +- Improve how the two noise verdicts are marked in the tree, so the report and + the viewer no longer show the same file differently. + ## [1.11.0] — 2026-08-22 ### Added diff --git a/compare_tool/scanner.py b/compare_tool/scanner.py index b989590..107a646 100644 --- a/compare_tool/scanner.py +++ b/compare_tool/scanner.py @@ -160,24 +160,6 @@ def fold_status(result, fold): return result -def apply_fold(results, fold): - """Re-judge an ALREADY scanned tree under different compare rules, with no - disk access: the verdict a fold produces is a pure function of the hunks, - so re-reading every file to learn it would be pure waste. Returns a new - dict of (copied) results; the input is left untouched so the rules can be - changed back and forth without a rescan.""" - fold = tuple(f for f in fold if f in FOLDABLE) - if not fold: - return dict(results) - out = {} - for rel, r in results.items(): - if r['status'] in fold: - r = dict(r, notes=list(r['notes'])) - fold_status(r, fold) - out[rel] = r - return out - - def _shadow_lines(rel, text): """The file's non-blank shadow lines, under its own ruleset. diff --git a/docs/architecture.md b/docs/architecture.md index 45ab60f..2256e73 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -237,10 +237,11 @@ the file `identical`, the tree and the exported report (built from `_raw_results`) disagreed about the same file, and the tree made the one claim this tool must never make about a file that really differs. -`scanner.apply_fold` / `fold_status` still implement the old collapse for -`scan(fold=…)` — a CLI-side option — and `scanner.FOLDABLE` still names the only -two statuses any caller may collapse. Real changes, one-sided files and errors -are absent from that tuple **by construction**. +`scanner.fold_status` still implements that collapse for `scan(fold=…)`, where +a caller asks for it up front, and `scanner.FOLDABLE` still names the only two +statuses any caller may collapse — real changes, one-sided files and errors are +absent from that tuple **by construction**. `apply_fold`, which re-judged an +already-scanned tree, went with the behaviour it existed for. Navigation follows what is on screen. A shown comment or Unimportant hunk **is** an `F7`/`F8` stop, and such a file is in `MainWindow._NAV_STATUS`, so the walk diff --git a/docs/vi/architecture.md b/docs/vi/architecture.md index d0465e1..92af0cb 100644 --- a/docs/vi/architecture.md +++ b/docs/vi/architecture.md @@ -165,28 +165,35 @@ thành `mixed`. đổi thật, file một bên và lỗi vắng mặt khỏi tuple đó **do cấu trúc**, nên không lỗi lập trình nào ở phía caller giấu được chúng. -### Fold là hàm thuần, không phải scan lại - -`scanner.apply_fold` phán lại một cây đã scan theo bộ luật khác mà không đụng đĩa: -hunk đã nói sẵn mỗi khác biệt thuộc loại gì, nên đọc lại toàn bộ file để biết đúng -cái đó là phí công. Nó copy chứ không sửa tại chỗ, nên bật tắt luật qua lại thoải -mái. Viewer giữ nguyên lần scan gốc trong `MainWindow._raw_results` và fold vào -`self.results` để hiển thị. - -Fold một nhóm chỉ đổi đúng hai thứ: **verdict** của file (thành `identical`, hoặc -`real-change` nếu còn thay đổi thật) và cách các dòng đó được **tô** — -`view_model.mute_rows` làm chúng xám đi, minimap thôi kẻ vạch cho chúng, còn -`F7`/`F8` thôi dừng ở đó. Bản thân các dòng vẫn nằm trên màn hình. Hunk -không bị đụng tới, nên report xuất ra từ `_raw_results` không thể biết là có nhóm -nào đã bị fold. - -Navigation đi theo cái đang hiện trên màn hình, không phải cái review được. Hunk -comment hoặc Unimportant đang hiện (chưa fold) **là** một điểm dừng `F7`/`F8`, và -file mà verdict cả file là `comment-only` / `ignorable-only` thì nằm trong -`MainWindow._NAV_STATUS` nên lộ trình đi vào file đó. Cả hai rơi ra ngay khi -nhóm bị fold, vì lúc đó `apply_fold` đã xử lại file thành `identical` — một nguồn -sự thật duy nhất, không phải giữ thêm cờ nào đồng bộ với checkbox. Cái mà -navigation tuyệt đối không được làm là ngụ ý đã ký duyệt: +### Toggle của viewer là quyết định tô màu, không phải verdict + +`MainWindow._muted_statuses()` đọc hai checkbox; `_apply_rules` truyền các row +mode tương ứng cho `DiffPane.set_muted_modes`, còn `self.results` nhận thẳng +**lần scan gốc**. Không có gì bị phán lại, nên một lần bật tắt là tức thì và hai +thư mục chỉ được đọc đúng một lần. + +Bỏ tick một nhóm chỉ đổi cách các dòng đó được **tô**, không gì khác: +`view_model.mute_rows` làm chúng xám đi, minimap thôi kẻ vạch, `F7`/`F8` thôi +dừng ở đó. Các dòng vẫn nằm trên màn hình, hunk không bị đụng, và verdict cũng +không: file chỉ khác comment vẫn hiện Comment trên cây, trong số đếm, và trong +report xuất ra. `Hide identical` vì thế để yên nó — nút đó ẩn file identical, +không ẩn file mà người review chọn làm mờ đi. + +Chuyện verdict được để yên chính là điểm mấu chốt. Hồi toggle còn phán lại file +thành `identical`, cây và report xuất ra (dựng từ `_raw_results`) nói khác nhau +về cùng một file, và cây đưa ra đúng lời khẳng định mà tool này không bao giờ +được phép nói về một file thật sự có khác biệt. + +`scanner.fold_status` vẫn hiện thực cách thu gọn cũ cho `scan(fold=…)`, nơi +caller yêu cầu ngay từ đầu, và `scanner.FOLDABLE` vẫn liệt kê đúng hai status mà +caller được phép thu gọn. + +Navigation đi theo cái đang hiện trên màn hình. Hunk comment hoặc Unimportant +đang hiện **là** một điểm dừng `F7`/`F8`, và file mà cả verdict là +`comment-only` / `ignorable-only` nằm trong `MainWindow._NAV_STATUS` nên lộ +trình đi vào đó. `_is_nav` loại nó ra ngay khi checkbox bị bỏ tick — các dòng đó +đã xám và biến khỏi minimap, nên file không còn điểm dừng nào, bước vào chỉ là +đi vào ngõ cụt. Cái mà navigation tuyệt đối không được làm là ngụ ý đã ký duyệt: `DiffPane._stop_units` mang `None` cho các stop đó, nên `current_unit()` báo là không có gì để review ở đây. Chỉ `real` và `moved` mới review được (`review.REVIEWABLE`), dù có navigate tới hay không.