Merged
test: add strict serial gtest stress runner #1327
liuxingsenAdd on 25 days ago
test: add strict serial gtest stress runner #1327
Merged
Total: 3 files changed+725-0
| @@ -0,0 +1,263 @@ | |||
| 1 | +#!/usr/bin/env python3 | ||
| 2 | +# Copyright (c) 2026 Huawei Technologies Co., Ltd. | ||
| 3 | +# openUBMC is licensed under Mulan PSL v2. | ||
| 4 | +# You can use this software according to the terms and conditions of the Mulan PSL v2. | ||
| 5 | +# You may obtain a copy of Mulan PSL v2 at: | ||
| 6 | +# http://license.coscl.org.cn/MulanPSL2 | ||
| 7 | +# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, | ||
| 8 | +# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, | ||
| 9 | +# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. | ||
| 10 | +# See the Mulan PSL v2 for more details. | ||
| 11 | + | ||
| 12 | +from collections import Counter | ||
| 13 | +from pathlib import Path | ||
| 14 | +import json | ||
| 15 | +import os | ||
| 16 | +import shutil | ||
| 17 | +import signal | ||
| 18 | +import subprocess | ||
| 19 | +import sys | ||
| 20 | +import tempfile | ||
| 21 | +import unittest | ||
| 22 | +from unittest import mock | ||
| 23 | + | ||
| 24 | +import run_memory_audit_stress as stress | ||
| 25 | + | ||
| 26 | + | ||
| 27 | +class ParseCasesTest(unittest.TestCase): | ||
| 28 | + def test_accepts_run_marker_interleaved_after_runtime_log(self): | ||
| 29 | + output = """\ | ||
| 30 | +DEBUG thread_pool.cpp:494 operator() | io thread 1 started[ RUN ] MucseInterfaceTest.CoolingUpdatesSignedTemperatureBySmbus | ||
| 31 | +[ OK ] MucseInterfaceTest.CoolingUpdatesSignedTemperatureBySmbus (0 ms) | ||
| 32 | +""" | ||
| 33 | + | ||
| 34 | + self.assertEqual( | ||
| 35 | + stress.parse_cases(output), | ||
| 36 | + {"run": 1, "ok": 1, "incomplete": 0}, | ||
| 37 | + ) | ||
| 38 | + | ||
| 39 | + def test_accepts_log_text_interleaved_after_result_name(self): | ||
| 40 | + output = """\ | ||
| 41 | +[ RUN ] MucseInterfaceTest.CoolingUpdatesTemperatureByNcsi | ||
| 42 | +[ OK ] MucseInterfaceTest.CoolingUpdatesTemperatureByNcsiDEBUG runtime started (1 ms) | ||
| 43 | +""" | ||
| 44 | + | ||
| 45 | + self.assertEqual( | ||
| 46 | + stress.parse_cases(output), | ||
| 47 | + {"run": 1, "ok": 1, "incomplete": 0}, | ||
| 48 | + ) | ||
| 49 | + | ||
| 50 | + def test_ignores_repeated_failure_summary_without_run(self): | ||
| 51 | + output = """\ | ||
| 52 | +[ FAILED ] Suite.FailedCase | ||
| 53 | +""" | ||
| 54 | + | ||
| 55 | + self.assertEqual(stress.parse_cases(output), {"run": 0, "incomplete": 0}) | ||
| 56 | + | ||
| 57 | + def test_accepts_multiple_markers_on_one_line(self): | ||
| 58 | + output = """\ | ||
| 59 | +[ RUN ] Suite.First[ OK ] Suite.First (0 ms)[ RUN ] Suite.Second | ||
| 60 | +[ OK ] Suite.Second (0 ms) | ||
| 61 | +""" | ||
| 62 | + | ||
| 63 | + self.assertEqual( | ||
| 64 | + stress.parse_cases(output), | ||
| 65 | + {"run": 2, "ok": 2, "incomplete": 0}, | ||
| 66 | + ) | ||
| 67 | + | ||
| 68 | + | ||
| 69 | + def test_completed_long_name_does_not_finish_pending_short_name(self): | ||
| 70 | + output = """\ | ||
| 71 | +[ RUN ] Suite.A | ||
| 72 | +[ RUN ] Suite.AB | ||
| 73 | +[ OK ] Suite.AB (0 ms) | ||
| 74 | +[ OK ] Suite.AB (0 ms) | ||
| 75 | +""" | ||
| 76 | + for result_name in ("Suite.AB", "Suite.ABDEBUG"): | ||
| 77 | + with self.subTest(result_name=result_name): | ||
| 78 | + observed = output.rsplit("[ OK ]", 1)[0] + f"[ OK ] {result_name} (0 ms)\n" | ||
| 79 | + self.assertEqual(stress.parse_cases(observed), {"run": 2, "ok": 1, "incomplete": 1}) | ||
| 80 | + | ||
| 81 | + def test_exact_pending_name_wins_over_its_shorter_prefix(self): | ||
| 82 | + output = """\ | ||
| 83 | +[ RUN ] Suite.A | ||
| 84 | +[ RUN ] Suite.AB | ||
| 85 | +[ OK ] Suite.AB (0 ms) | ||
| 86 | +""" | ||
| 87 | + self.assertEqual(stress.parse_cases(output), {"run": 2, "ok": 1, "incomplete": 1}) | ||
| 88 | + | ||
| 89 | + | ||
| 90 | +class ProcessBoundaryTest(unittest.TestCase): | ||
| 91 | + def run_interrupted_round(self, error, kill_required=False): | ||
| 92 | + with tempfile.TemporaryDirectory(prefix="stress-process-boundary-") as tmp: | ||
| 93 | + root = Path(tmp) | ||
| 94 | + build = root / "build" | ||
| 95 | + results = root / "results" | ||
| 96 | + build.mkdir() | ||
| 97 | + results.mkdir() | ||
| 98 | + process = mock.Mock(pid=123456) | ||
| 99 | + if kill_required: | ||
| 100 | + process.wait.side_effect = [error, subprocess.TimeoutExpired("meson", 5), -9] | ||
| 101 | + else: | ||
| 102 | + process.wait.side_effect = [error, -15] | ||
| 103 | + with mock.patch.object(stress.subprocess, "Popen", return_value=process), \ | ||
| 104 | + mock.patch.object(stress.os, "killpg") as killpg: | ||
| 105 | + outcome = stress.run_round(build, results, 1, Counter(), 2) | ||
| 106 | + self.assertFalse(outcome["passed"]) | ||
| 107 | + self.assertEqual(killpg.call_args_list[0], mock.call(process.pid, signal.SIGTERM)) | ||
| 108 | + if kill_required: | ||
| 109 | + self.assertEqual(killpg.call_args_list[1], mock.call(process.pid, signal.SIGKILL)) | ||
| 110 | + return outcome | ||
| 111 | + | ||
| 112 | + def test_whole_round_timeout_is_distinct_from_interrupt(self): | ||
| 113 | + outcome = self.run_interrupted_round(subprocess.TimeoutExpired("meson", 2), kill_required=True) | ||
| 114 | + self.assertEqual(outcome["meson_exit_code"], 124) | ||
| 115 | + self.assertFalse(outcome["interrupted"]) | ||
| 116 | + self.assertIn("whole round exceeded 2 seconds", outcome["errors"]) | ||
| 117 | + | ||
| 118 | + def test_keyboard_interrupt_reports_sigint_status(self): | ||
| 119 | + outcome = self.run_interrupted_round(KeyboardInterrupt()) | ||
| 120 | + self.assertEqual(outcome["meson_exit_code"], 128 + signal.SIGINT) | ||
| 121 | + self.assertTrue(outcome["interrupted"]) | ||
| 122 | + self.assertIn("run interrupted", outcome["errors"]) | ||
| 123 | + | ||
| 124 | + def test_sigterm_handler_preserves_its_signal_status(self): | ||
| 125 | + try: | ||
| 126 | + stress.interrupt_on_term(signal.SIGTERM, None) | ||
| 127 | + except KeyboardInterrupt as error: | ||
| 128 | + outcome = self.run_interrupted_round(error) | ||
| 129 | + self.assertEqual(outcome["meson_exit_code"], 128 + signal.SIGTERM) | ||
| 130 | + self.assertTrue(outcome["interrupted"]) | ||
| 131 | + self.assertNotIn("whole round exceeded 2 seconds", outcome["errors"]) | ||
| 132 | + | ||
| 133 | + | ||
| 134 | +class LaunchFailureTest(unittest.TestCase): | ||
| 135 | + def check_launch_failure(self, nonexecutable_meson): | ||
| 136 | + with tempfile.TemporaryDirectory(prefix="stress-launch-failure-") as tmp: | ||
| 137 | + root = Path(tmp) | ||
| 138 | + build = root / "build" | ||
| 139 | + intro = build / "meson-info/intro-tests.json" | ||
| 140 | + intro.parent.mkdir(parents=True) | ||
| 141 | + intro.write_text(json.dumps([{ | ||
| 142 | + "name": "fixture", "suite": ["stress-fixture"], | ||
| 143 | + "cmd": [sys.executable], | ||
| 144 | + }])) | ||
| 145 | + search_path = root / "bin" | ||
| 146 | + search_path.mkdir() | ||
| 147 | + if nonexecutable_meson: | ||
| 148 | + meson = search_path / "meson" | ||
| 149 | + meson.write_text("#!/bin/sh\nexit 0\n") | ||
| 150 | + meson.chmod(0o600) | ||
| 151 | + logs = root / "logs" | ||
| 152 | + result = subprocess.run( | ||
| 153 | + [sys.executable, "-B", str(Path(stress.__file__).resolve()), | ||
| 154 | + "--build-dir", str(build), "--rounds", "2", "--log-root", str(logs)], | ||
| 155 | + env={**os.environ, "PATH": str(search_path)}, | ||
| 156 | + text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, | ||
| 157 | + timeout=10, | ||
| 158 | + ) | ||
| 159 | + self.assertEqual(result.returncode, 1, result.stdout) | ||
| 160 | + summaries = list(logs.glob("*/summary.json")) | ||
| 161 | + self.assertEqual(len(summaries), 1, result.stdout) | ||
| 162 | + summary = json.loads(summaries[0].read_text()) | ||
| 163 | + self.assertFalse(summary["passed"]) | ||
| 164 | + self.assertEqual(summary["requested_rounds"], 2) | ||
| 165 | + self.assertEqual(summary["failed_rounds"], 1) | ||
| 166 | + self.assertEqual(len(summary["rounds"]), 1) | ||
| 167 | + outcome = summary["rounds"][0] | ||
| 168 | + self.assertIsNone(outcome["meson_exit_code"]) | ||
| 169 | + self.assertEqual(outcome["executables_reported"], 0) | ||
| 170 | + self.assertEqual(outcome["missing_suites"], ["stress-fixture:fixture"]) | ||
| 171 | + self.assertTrue(any("cannot start meson test" in error for error in outcome["errors"]), outcome) | ||
| 172 | + self.assertIn("cannot start meson test", Path(outcome["logs"]["console"]).read_text()) | ||
| 173 | + self.assertNotIn("Traceback", result.stdout) | ||
| 174 | + | ||
| 175 | + def test_missing_meson_writes_failed_round_summary(self): | ||
| 176 | + self.check_launch_failure(nonexecutable_meson=False) | ||
| 177 | + | ||
| 178 | + def test_nonexecutable_meson_writes_failed_round_summary(self): | ||
| 179 | + self.check_launch_failure(nonexecutable_meson=True) | ||
| 180 | + | ||
| 181 | + | ||
| 182 | +class LogAndWrapperTest(unittest.TestCase): | ||
| 183 | + def test_invalid_json_identifies_file_and_line(self): | ||
| 184 | + with tempfile.TemporaryDirectory(prefix="stress-log-") as tmp: | ||
| 185 | + path = Path(tmp) / "testlog.json" | ||
| 186 | + path.write_text('{"result": "OK"}\ninvalid JSON\n') | ||
| 187 | + with self.assertRaises(ValueError) as caught: | ||
| 188 | + stress.read_meson_log(path) | ||
| 189 | + self.assertIn(f"{path}:2:", str(caught.exception)) | ||
| 190 | + | ||
| 191 | + def test_wrapper_orders_existing_runtime_paths_and_removes_duplicates(self): | ||
| 192 | + with tempfile.TemporaryDirectory(prefix="stress-wrapper-") as tmp: | ||
| 193 | + root = Path(tmp) | ||
| 194 | + prebuilt = root / ".libmcpp_prebuilt/src" | ||
| 195 | + lua = root / "temp/usr/lib64" | ||
| 196 | + support = root / ".lua_runtime_libs" | ||
| 197 | + existing = root / "test-libs" | ||
| 198 | + for path in (prebuilt, lua, support, existing): | ||
| 199 | + path.mkdir(parents=True) | ||
| 200 | + for name, expected in ( | ||
| 201 | + ("test_driver", [prebuilt, support, lua, existing]), | ||
| 202 | + ("test_lua_plugin_fixture", [lua, prebuilt, support, existing]), | ||
| 203 | + ): | ||
| 204 | + with self.subTest(executable=name), mock.patch.object(stress, "ROOT", root), \ | ||
| 205 | + mock.patch.dict(os.environ, {"LD_LIBRARY_PATH": f"{existing}:{prebuilt}:{root / 'missing'}"}, clear=True), \ | ||
| 206 | + mock.patch.object(stress.os, "execvpe") as execute: | ||
| 207 | + stress.wrapper_exec([name, "arg"]) | ||
| 208 | + self.assertEqual(os.environ["LD_LIBRARY_PATH"], ":".join(map(str, expected))) | ||
| 209 | + self.assertEqual(execute.call_args.args[:2], (name, [name, "arg"])) | ||
| 210 | + | ||
| 211 | + | ||
| 212 | +class GtestCountErrorTest(unittest.TestCase): | ||
| 213 | + def test_accepts_first_and_matching_round_counts(self): | ||
| 214 | + self.assertIsNone(stress.gtest_count_error(11574, None)) | ||
| 215 | + self.assertIsNone(stress.gtest_count_error(11574, 11574)) | ||
| 216 | + | ||
| 217 | + def test_rejects_changed_round_count(self): | ||
| 218 | + self.assertEqual( | ||
| 219 | + stress.gtest_count_error(11573, 11574), | ||
| 220 | + "GoogleTest case count changed from 11574 to 11573", | ||
| 221 | + ) | ||
| 222 | + | ||
| 223 | + | ||
| 224 | +@unittest.skipUnless(shutil.which("meson"), "requires Meson") | ||
| 225 | +class RoundResultTest(unittest.TestCase): | ||
| 226 | + def run_fixture(self, skipped): | ||
| 227 | + with tempfile.TemporaryDirectory(prefix="stress-round-result-") as tmp: | ||
| 228 | + root = Path(tmp) | ||
| 229 | + (root / "meson.build").write_text( | ||
| 230 | + "project('stress-fixture')\n" | ||
| 231 | + "python = import('python').find_installation('python3')\n" | ||
| 232 | + "test('fixture', python, args: files('fixture.py'))\n" | ||
| 233 | + ) | ||
| 234 | + output = "[ RUN ] Suite.First\n[ OK ] Suite.First (0 ms)\n" | ||
| 235 | + if skipped: | ||
| 236 | + output += "[ RUN ] Suite.Second\n[ SKIPPED ] Suite.Second (0 ms)\n" | ||
| 237 | + (root / "fixture.py").write_text(f"print({output!r})\n") | ||
| 238 | + build = root / "build" | ||
| 239 | + setup = subprocess.run( | ||
| 240 | + ["meson", "setup", str(build), str(root)], | ||
| 241 | + text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, | ||
| 242 | + ) | ||
| 243 | + self.assertEqual(setup.returncode, 0, setup.stdout) | ||
| 244 | + results = root / "results" | ||
| 245 | + results.mkdir() | ||
| 246 | + return stress.run_round( | ||
| 247 | + build, results, 1, Counter({"stress-fixture:fixture": 1}), 30, | ||
| 248 | + ) | ||
| 249 | + | ||
| 250 | + def test_accepts_complete_successful_round(self): | ||
| 251 | + outcome = self.run_fixture(skipped=False) | ||
| 252 | + self.assertTrue(outcome["passed"], outcome) | ||
| 253 | + self.assertEqual(outcome["gtest_cases"]["ok"], 1) | ||
| 254 | + | ||
| 255 | + def test_rejects_skipped_cases_even_when_meson_reports_success(self): | ||
| 256 | + outcome = self.run_fixture(skipped=True) | ||
| 257 | + self.assertEqual(outcome["meson_exit_code"], 0, outcome) | ||
| 258 | + self.assertEqual(outcome["gtest_cases"]["skipped"], 1, outcome) | ||
| 259 | + self.assertFalse(outcome["passed"], outcome) | ||
| 260 | + | ||
| 261 | + | ||
O | |||
| 262 | +if __name__ == "__main__": | ||
| 263 | + unittest.main() | ||
| @@ -84,6 +84,151 @@ ASAN 检查的分配边界,因此不能把此次行为回归写成已复现分 | |||
| 84 | 84 | ||
| 85 | --- | 85 | --- |
| 86 | 86 | ||
| 87 | +## 2026-10-14:内存边界与异步生命周期修复拆分 | ||
| 88 | + | ||
| 89 | +原 [!1307](https://gitcode.com/openUBMC/component_drivers/merge_requests/1307) 汇总了协议边界、对象寿命、测试时序和本地构建工具等不同问题。历史压力日志出现过间歇性崩溃,源码检查又发现若干可独立修复的越界及异步收尾路径。不能据此把每条风险都认定为某次历史崩溃的已证实根因,因此本轮按问题类型拆成十个独立 PR,让代码、回归和证据范围可以分别核对。 | ||
| 90 | + | ||
| 91 | +下表是本轮修复范围索引;PR 中的源码与提交对应的验证报告才是逐项验收依据。各分支最初独立建立在同一主线基线上,后续主线变化需要正常整合。 | ||
| 92 | + | ||
| 93 | +| 主题 | 问题触发与关键修复 | 对应 PR | | ||
| 94 | +| --- | --- | --- | | ||
| 95 | +| 1. NCSI 回复与字段复制 | 短回复、设备声明的过长 MAC/SN/PN、分帧日志长度与真实 payload 不一致,可能使读取或复制越界。先校验最小头、固定字段容量、累计偏移和实际接收量,再取字段;检查复制结果,保留设备不在位短回复及旧日志命令回退。异常输入会返回失败,属于运行时代码修复。 | [!1326](https://gitcode.com/openUBMC/component_drivers/merge_requests/1326) | | ||
| 96 | +| 2. 总线、芯片与 SMC 缓冲区 | CAN/I2C 写入长度超过输入向量或缓冲区、芯片短回复、非法 offset 宽度以及 SMC 帧长不一致,可能越界或触发超宽移位。补齐发送/接收边界,小端寄存器按字节解码;保留主线 LocalBus 已提取位域的语义,避免二次掩码。非法请求和短回复的错误返回是实际行为变化。 | [!1335](https://gitcode.com/openUBMC/component_drivers/merge_requests/1335) | | ||
| 97 | +| 3. CSR1 调度任务与宿主引用 | 空闲注册也持有宿主,会阻止宿主进入析构注销;回调内注销又可能等待自身。空闲状态保存弱引用,运行期间才持有宿主;重注册复用仍运行的状态,防止同一任务重入。Aggregate 仅为托管宿主保存弱引用,避开最终释放及未托管对象的失效计数访问。 | [!1329](https://gitcode.com/openUBMC/component_drivers/merge_requests/1329) | | ||
| 98 | +| 4. CSR1 内嵌接口定时器 | 总线、芯片和 Scanner 的接口存储结束后,晚释放的 timer 仍可能回访原父接口;活动回调也可能继续使用接口成员。timer 改为无父对象、成员持有并显式绑定执行器;析构/替换先停止任务,保留单次 timer 句柄用于后续收尾。 | [!1330](https://gitcode.com/openUBMC/component_drivers/merge_requests/1330) | | ||
| 99 | +| 5. 网卡定时器启停 | Broadcom 重复启动时可能先覆盖协议或包编号,而旧回调仍在使用接口;Hisi 等路径也需明确停止与成员销毁顺序。启动前停止旧任务,再更新依赖并建立新 timer;配套夹具等待真实完成事件。它包含正式网卡启停行为修复,不只是延长测试等待时间。 | [!1331](https://gitcode.com/openUBMC/component_drivers/merge_requests/1331) | | ||
| 100 | +| 6. NVMe 遥测与升级收尾 | Huawei/Samsung 遥测回调及升级线程使用宿主或兄弟接口;先拆成员再收尾可能访问失效存储。设备析构入口先停任务,Huawei 升级线程在依赖成员销毁前 join;timer 显式使用宿主或接口配置的执行器。 | [!1332](https://gitcode.com/openUBMC/component_drivers/merge_requests/1332) | | ||
| 101 | +| 7. PRBS 打包字段与 SMART 数值 | 直接把打包回复地址当 `uint32_t*` 读取可能未对齐;128 位 PowerOnHours 高半部移入 `uint64_t` 会发生移位数不小于64的未定义行为。PRBS 经 `memcpy` 取对齐局部值再转小端;SMART 分别解码高低64位,以 `ldexp(high, 64) + low` 形成既有 `double` 属性。浮点舍入仍存在,不能宣称精确保存全部128位整数。 | [!1333](https://gitcode.com/openUBMC/component_drivers/merge_requests/1333) | | ||
| 102 | +| 8. 测试夹具与异步等待 | Scanner、网卡、DERA、RAID 等夹具依赖固定 sleep 或未清理回调,可能在异步工作完成前释放对象。使用完成通知和明确收尾,修正测试释放顺序、恢复相关覆盖并补回 SMBus 帧事务测试。本主题只改测试。 | [!1334](https://gitcode.com/openUBMC/component_drivers/merge_requests/1334) | | ||
| 103 | +| 9. 串行压力统计与说明 | 单看 Meson 退出成功不能证明所有配置程序和 gtest 用例完整执行。保存逐轮日志与 summary,严格拒绝跳过、缺失和未完成;区分超时与中断,避免重复长用例名结束标记误补短用例名。工具不安装 sanitizer 抑制,使用方法见下一节。 | [!1327](https://gitcode.com/openUBMC/component_drivers/merge_requests/1327) | | ||
| 104 | +| 10. 本地 Lua 工具链选择 | 本地 `smart_build.sh` 只看到 lua.h 就可能采用不完整 temp 工具链,切换来源还可能残留可选头或错误复用缓存。统一检查四个核心头,成对选择头/库,并将兼容头纳入软链接清理和缓存指纹。正式 Bingo/Conan recipe 与 CI 构建入口不调用该本地脚本。 | [!1328](https://gitcode.com/openUBMC/component_drivers/merge_requests/1328) | | ||
| 105 | + | ||
| 106 | +### 本轮版本与 CHANGELOG 约定 | ||
| 107 | + | ||
| 108 | +本轮由主题3(!1329)和主题6(!1332)承担服务版本及 CHANGELOG 更新,保留各自既有的 `1.2.530 → 1.2.531` 条目;其余八项恢复为各自主线基线的版本与 CHANGELOG,不再逐项升版。每个 PR 的后续整改也不重复加版本条目,合入时按届时主线正常整合。这是本轮拆分约定,不是“所有运行时代码修复都无需版本记录”的通用规则。 | ||
| 109 | + | ||
| 110 | +主题1、2、4、5、7仍会改变异常输入处理、数值解码或启停/销毁时序;没有单独升版不等于只有测试变化。主题8、9为测试和辅助统计工具,主题10为独立本地构建入口,其变化不修改正式服务接口或正式构建 recipe。 | ||
| 111 | + | ||
| 112 | +### 从 C 的资源清理顺序理解生命周期修改 | ||
| 113 | + | ||
| 114 | +以下片段用于解释上述 PR,不能理解为本文所在分支已经包含其他九项源码。 | ||
| 115 | + | ||
| 116 | +以 `drivers/csr1/bus/common/interface/i_bus.cpp` 为例,修改前后创建 timer 的关键代码是: | ||
| 117 | + | ||
| 118 | +```cpp | ||
| 119 | +// 修改前:接口地址同时作为 timer 的父对象 | ||
| 120 | +m_access_timer = mc::make_shared<mc::timer>(this); | ||
| 121 | + | ||
| 122 | +// 修改后:timer 由成员持有,执行器另行绑定 | ||
| 123 | +m_access_timer = mc::make_shared<mc::timer>(); | ||
| 124 | +auto* owner = get_owner(); | ||
| 125 | +m_access_timer->bind_executor(owner != nullptr ? owner->executor() : executor()); | ||
| 126 | + | ||
| 127 | +// 修改后:先执行析构函数体,再由 C++ 自动销毁成员 | ||
| 128 | +Interface_Bus::~Interface_Bus() | ||
| 129 | +{ | ||
| 130 | + stop_access_timer(); | ||
| 131 | +} | ||
| 132 | +``` | ||
| 133 | + | ||
| 134 | +`this` 是当前接口地址,可类比 C 回调的 `void *ctx`。接口可能是 `struct card` 中按值内嵌的一个字段,timer 的父指针不能在这块存储结束后再使用。`mc::make_shared<mc::timer>()` 的尖括号指定创建的类型,返回由项目 `mc::shared_ptr` 管理的 timer 句柄;省略 `this` 切断的是父对象关系。`auto* owner` 仍是普通借用指针,只由编译器推导类型;条件表达式选择回调被投递到哪个执行器,不会自动保护所有成员并发访问。 | ||
| 135 | + | ||
| 136 | +回调中的 `[this]` 是 lambda 捕获列表,只保存接口地址,类似 C 的函数指针加 ctx;保存地址不会延长接口寿命。因此还必须在 ctx 失效前停止活动工作。`~Interface_Bus()` 是析构函数,会在对象被销毁时执行;C++ 在其函数体执行完以后才自动拆除该类的成员,故这里有机会先做相当于 C `stop_and_wait(ctx)` 的收尾。 | ||
| 137 | + | ||
| 138 | +主题5在更新协议对象和包编号前停止旧 timer,主题6在宿主其他成员销毁前停止遥测,都是对这一顺序的具体应用: | ||
| 139 | + | ||
| 140 | +```text | ||
| 141 | +风险路径 A:旧回调仍用 ctx → ctx 的依赖被覆盖/销毁 → 回调继续读取失效状态 | ||
| 142 | +修复顺序 A:停止旧任务 → 等待已进入的业务回调退出 → 再更新或销毁依赖 | ||
| 143 | +风险路径 B:接口存储结束 → 外部仍持有的 timer 晚释放 → 沿旧父关系回访接口 | ||
| 144 | +修复路径 B:timer 创建时不建立该父关系 → 晚释放不再沿这条关系访问接口 | ||
| 145 | +``` | ||
| 146 | + | ||
| 147 | +图示是需要切断的路径,不表示本轮已重现所有历史崩溃。依据本轮核对的 libmcpp `10829fb9`,`stop()` 取消旧代际并等待其他执行线程上已进入的业务回调;从同一 timer 的回调内停止会避开等待自身。它不保证所有排队闭包已执行,也不保证 timer 已销毁。成员 `reset()` 仅释放当前那份引用,执行器或其他持有者仍可能让 timer 继续存在。无父 timer、智能指针、绑定执行器都不能单独代替收尾,更不能保证任意兄弟成员销毁顺序安全。实际依赖包仍须核对,源码阅读不等于缓存二进制来源已经证明。 | ||
| 148 | + | ||
| 149 | +主题3在 `drivers/csr1/chip/common/interface/i_aggregate.cpp::set_owner_object()` 中进一步处理未托管对象: | ||
| 150 | + | ||
| 151 | +```cpp | ||
| 152 | +// 原汇总实现 | ||
| 153 | +if (owner != nullptr) { | ||
| 154 | + m_owner_weak = owner->weak_from_this(); | ||
| 155 | +} | ||
| 156 | +// 拆分修订后 | ||
| 157 | +if (owner != nullptr && owner->is_managed()) { | ||
| 158 | + m_owner_weak = owner->weak_from_this(); | ||
| 159 | +} | ||
| 160 | +``` | ||
| 161 | + | ||
| 162 | +这里的 `mc::weak_ptr` 是项目封装,其弱计数位于宿主内部,不能套用标准库 `std::weak_ptr` 的内部实现。可把它理解成“借用 ctx,同时保存一项将来仍会访问 ctx 内计数的清理动作”:即使尝试取得强引用的 `lock()` 返回空,稍后释放这个弱引用仍可能需要访问计数。未托管栈对象离开作用域后,这种清理就不安全,因此先用 `is_managed()` 限制适用对象。运行时 `lock()` 成功获得的强引用只延长宿主存活时间,不提供互斥保护。 | ||
| 163 | + | ||
| 164 | +调度器的弱引用让空闲注册不再阻止宿主销毁;运行期间持有宿主则保护尚未完成的工作。相当于 C 中在提交异步操作前增加 ctx 引用计数,完成后再减回去。若操作永不完成,不能仅因超时就释放仍可能被访问的 ctx。`wait_for_tasks()` 还会跳过当前同步执行状态,避免等待自身;从当前回调内注销返回,并不代表当前回调已经结束。 允许异步注销后保留运行状态的前提是任务受托管且其寿命完全绑定宿主(`lifetime_bound_to_owner()`),不能推广为任意异步回调都可安全注销;可替换子任务和未托管任务从任务执行上下文之外注销时仍需等待。 | ||
| 165 | + | ||
| 166 | +主题6的 `drivers/pcie_nvme/huawei/interface/upgrade.cpp` 将等待升级线程提取为: | ||
| 167 | + | ||
| 168 | +```cpp | ||
| 169 | +void NVMe_Upgrade::wait_for_completion() | ||
| 170 | +{ | ||
| 171 | + if (m_upgrade_thread && m_upgrade_thread->joinable()) { | ||
| 172 | + m_upgrade_thread->join(); | ||
| 173 | + } | ||
| 174 | +} | ||
| 175 | +``` | ||
| 176 | + | ||
| 177 | +这与 C 中在释放线程使用的 ctx 之前调用 `pthread_join()` 的目的相同。`joinable()` 表示该线程句柄仍可连接,不等于线程已经结束;`join()` 可能等待。设备析构入口先调用这条收尾,再进入成员销毁;只依赖升级接口自身晚些时候析构,无法保护那些可能更早被销毁的兄弟成员。此处也不承诺任意执行上下文中的等待都不会阻塞。 | ||
| 178 | + | ||
| 179 | +### 本轮证据如何核对 | ||
| 180 | + | ||
| 181 | +- 每项 PR 的测试结果必须标明实际执行的提交 SHA、构建配置及依赖;改动后若复用旧结果,写明原 SHA 和源码/文件等同性依据。README 不固定一组会随检视整改变化的“当前十项总通过数”。 | ||
| 182 | +- 本轮 C++ 定向回归使用 Linux 宿主构建、启用断言,部分依赖复用既有 libmcpp 缓存。源码 checkout `10829fb9` 不足以证明缓存二进制由该源码构建;未补齐来源时,不宣称重编译了最新依赖。 | ||
| 183 | +- 定向回归通过、机器人 CI 的成功标签和原50轮记录是不同证据。未执行的目标板、完整产品、完整多轮压力或 sanitizer 插桩验证必须明确列出;历史 Samsung 第44轮间歇性 SIGSEGV 不能凭一次定向通过就宣称闭环。 | ||
| 184 | +- `DISABLED_` 未执行用例不计入通过数,跳过、禁用及过滤条件要分别披露。压力工具不对白名单跳过放行,也不以统计容差掩盖缺失用例。 | ||
| 185 | +- 缓冲区/数值修复应核对正常值、边界值、短帧、错误长度和异常返回;生命周期修复应核对重复启停、在途销毁、外部保留 timer、最终释放和实际执行器配置。使用 mock 的范围与真实硬件/传输层异步路径要分开说明。 | ||
| 186 | + | ||
| 187 | +--- | ||
| 188 | + | ||
| 189 | +## 串行多轮 gtest 压力测试 | ||
| 190 | + | ||
| 191 | +`scripts/run_memory_audit_stress.py` 对一个已经构建完成的 Meson 测试目录逐轮串行执行,并保存每轮控制台输出、Meson JSON/TXT/XML 日志及 `summary.json`。脚本不负责构建,也不会因发现源码变化自动重编译。 | ||
| 192 | + | ||
| 193 | +在仓库根目录执行,下例的 `builddir` 应替换为本次已经配置并编译完成的测试目录: | ||
| 194 | + | ||
| 195 | +```bash | ||
| 196 | +ninja -C builddir | ||
| 197 | +python3 scripts/test_run_memory_audit_stress.py | ||
| 198 | +python3 scripts/run_memory_audit_stress.py \ | ||
| 199 | + --build-dir builddir \ | ||
| 200 | + --rounds 50 \ | ||
| 201 | + --round-timeout 7200 \ | ||
| 202 | + --log-root test_failure_logs/memory-audit | ||
| 203 | +``` | ||
| 204 | + | ||
| 205 | +需要 Python 3、Meson,以及该构建所需的运行时依赖。`--round-timeout` 是整轮超时秒数,每个测试程序仍受 Meson 自身配置的超时限制。日志目录带 UTC 时间和进程号;首个失败轮次会停止后续轮次,脚本以非零状态退出。超时或中断时,脚本向本轮 Meson 进程组发送终止信号,必要时再强制终止。 | ||
| 206 | + | ||
| 207 | +### 计数与通过条件 | ||
| 208 | + | ||
| 209 | +- 从 `meson-info/intro-tests.json` 读取当前配置的程序清单,拒绝缺失、重复或意外的程序结果。 | ||
| 210 | +- Meson 结果必须全部为 `OK`;成功程序也必须至少执行一个 gtest 用例。 | ||
| 211 | +- gtest 的失败、跳过和仅开始未结束的用例均导致整轮失败。后台日志与 gtest 标记交错时,统计器按待完成用例名称匹配结果,避免将日志交错误记为用例未完成。 | ||
| 212 | +- 首个通过轮次确定实际执行用例数,后续轮次必须一致;全部请求轮次通过后,总结中的 `passed` 才为 `true`。 | ||
| 213 | + | ||
| 214 | +该计数器不主动启用 `DISABLED_` 用例,也不会枚举全部 gtest 用例来校验环境中的过滤条件。多轮数量一致不能证明所有源码用例均已执行。提交测试报告时,应记录构建选项、过滤条件、实际执行数量、跳过和禁用范围。 | ||
| 215 | + | ||
| 216 | +### 构建与运行证据 | ||
| 217 | + | ||
| 218 | +运行前记录源码提交、构建选项和实际加载依赖。包装器会优先加入仓库中的 `.libmcpp_prebuilt` 库目录,并按测试名称调整 `temp/usr/lib64`、`.lua_runtime_libs` 与已有 `LD_LIBRARY_PATH` 的顺序;这些目录不存在时跳过。需先确认其库与当前构建匹配,不能只凭源码提交认定预编译库来自同一版本。 | ||
| 219 | + | ||
| 220 | +本脚本不创建私有 `/tmp`、共享内存、D-Bus 或 Linux 命名空间;有隔离需求时,应通过自己的 CI 或测试环境提供隔离。不要停止其他任务的 D-Bus 或清理其他构建目录。脚本启动的进程组之外的后台服务应由创建它的测试环境负责回收。 | ||
| 221 | + | ||
| 222 | +多轮通过只说明该构建和依赖下的测试结果,不等同于 ASan、UBSan 或其他内存检测通过。需在编译、链接阶段启用相应插桩,并保存完整诊断;仅设置 sanitizer 环境变量不会为现有二进制补上插桩。本工具不配置默认 sanitizer 抑制,也不随附覆盖整个外部依赖头文件的 `vptr` 抑制。外部依赖报错应保留原始证据并单独定位。 | ||
| 223 | + | ||
| 224 | +### 历史记录与回归范围 | ||
| 225 | + | ||
| 226 | +2026-09-27 的历史记录(报告提交 `8bdd569f`)记录了旧构建 50 轮通过,每轮 207 个测试程序、11,566 个 gtest 用例,总计 578,300 次用例运行。该构建未启用 sanitizer 插桩,且之后仍有源码和依赖变化;这些数字不作为当前分支或拆分 PR 的验证结果。历史机器上的私有隔离脚本和日志目录不属于可直接复用的仓库命令。 | ||
| 227 | + | ||
| 228 | +历史审计涉及协议回复和写入缓冲区边界、CSR1 调度及宿主引用、嵌入接口定时器的停止和销毁顺序、NVMe 升级线程收尾,以及 SMART 128 位计数转换。对应修复与回归由各自 PR 独立说明;提交报告应区分已运行的定向回归、新的多轮结果和仍未完成的验证。 | ||
| 229 | + | ||
| 230 | +--- | ||
| 231 | + | ||
| 87 | ## 构建与 tests 编译优化纪要 | 232 | ## 构建与 tests 编译优化纪要 |
| 88 | 233 | ||
| 89 | > **更新日期:2026-04-02** | 234 | > **更新日期:2026-04-02** |


💡 建议:前缀恢复可能把结果错配给同名前缀的待完成用例
恢复逻辑用
startswith+max(pending, key=len)(第 90-97 行)匹配待完成用例。当两个用例名存在前缀包含关系(如Suite.A与Suite.AB)时,Suite.AB的结束标记可能被记到仍待完成的Suite.A上,导致计数错配并掩盖真实的未完成用例。修复建议: 先对待完成用例做精确匹配,仅在所有精确匹配都失败时才回退到前缀匹配,降低错配概率。