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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ cmake --build build --target package_portable
冷构建 24 分钟做完、缓存没写回,下一轮又白等一次冷构建)。
- **判定 CI 成败只认 `gh run view <id> --json status,conclusion,attempt`**。`gh run watch <id> --exit-status | tail` 未加 `set -o pipefail` 时退出码取 `tail` 的 0,失败 run 会被看成成功(2026-11-08 踩过一次假绿)。纯 `doc/` / `docs/` / `AGENTS.md` 提交不触发 CI:两个 workflow 的 `push` 与 `pull_request` 都带 `paths` 过滤(`CMakeLists.txt` / `cmake/**` / `app/**` / `scripts/**` / `sdk/**` / workflow 自身),这类提交不出现在 run 列表里属预期,别误判成「CI 挂了没跑」。
- **判 flake 四条证据**:① 同一代码在 PR run 已绿;② 失败目标不在本次改动文件;③ 该目标常态耗时吻合;④ `gh run rerun <id> --failed` 全绿。Linux 侧有低概率单目标 flake(2026-11-08:`sdk_badinput_rejection_tests` 的 `getMusicCount()==1` 20s 等待超时;PR #111 合并后 `sdk_interaction_tests` 的 `seek to middle moves position forward` 还出现过 `SIGSEGV` 进程崩溃、14 断言全过的新形态)。**别为此放宽超时或改动 BadInput 55 例基线**,只做一次 rerun 取证。当 ② 不成立时用替代取证:比对 merge commit 与 PR 分支头的 tree(相同即逐字节同代码)、用 doctest suite 过滤器确认失败用例与新增用例零交集、确认生产包装层默认关闭时行为等价。日志里 grep 到的 `sigsegv` 多数是 g3log 示例 `main_sigsegv.cpp` 的编译输出,别当成历史崩溃。
- **异步事件的会话起点一律按事件内容定位,不要用固定快照下标猜**(2026-11-09 `afterMonotonic` 基线假红,[#115](https://github.com/wt159/SoundBridge/issues/115) / [#116](https://github.com/wt159/SoundBridge/issues/116)):`sdk/test/unit/test_player_integration.cpp` 的 `position callbacks stay monotonic and a legal seek still rewinds` 曾用 `beforeCount+1` 当「seek 后的新会话起点」,但 `Player::seek` 经 `m_workQueue.asyncRunTask` 异步生效,`first` 快照到 seek 生效之间旧会话仍会追加 position;只要追加 ≥1 个值,合法回退 100 就落在 `beforeCount+k`,被 `afterMonotonic` 误判为「新会话内回退」→ `sdk_interaction_tests` 低概率假红(11-09 PR #114 稳定性对照两组各 9/10、11-08 C1/C5 master 9/10 同源)。正解是在序列中定位实际回退点 `rewindAt`(首个 `< before`),只检查其后的新会话单调;真回退仍会被抓到、断言条数与所有数值不变,**这不是放宽判据**。修判据前必须先抓到失败序列实证(临时诊断只写 stdout、取证后零残留),再做「注入真回退必红 / 还原绿」的变异验证与同轮数 `-j4` 对照;备选「生产 `resetFloor` 二次上报」若成立须另开新票,不在测试票里改生产代码。
- **新增测试专用钩子(哪怕默认关闭)也要走稳定性闸门**:release/acquire 内存序、只在控制线程读写、绝不落解码线程 / 消费主循环 / 判据主循环;放行前必须做 master 与分支**同轮数** `ctest -j4` 对照(2026-11-08 C1/C5:分支 10/10 vs master 9/10 才放行),单目标串行绿不算证据。落在解码线程或判据主循环的额外写会把 `-j4` 假红率从 0 抬到约 30%(O1 对照实验)。(2026-11-09 反例:#112 / #113 / PR #114 的 `stop()` 清槽 + 两个测试钩子,虽通过变异验证、两轴 review 与 PR 阶段 CI 双绿,但分支两组 `-j4` 各 9/10、master 同轮数 10/10,按「假红率高于基线即撤回」的闸门**全部撤回、PR 关闭、改动未进 master**;取证与 resolution 见 [PR #114](https://github.com/wt159/SoundBridge/pull/114)。CI 双绿不能替代同轮数对照;失败用例与改动零交集也不是事后放宽闸门的理由。)
- **`SIGSEGV` flake 的「反复出现」门槛(方向②闸门)**:截至 2026-11-08,全历史 50 个 `sdk-coverage` run 里 `test case CRASHED: SIGSEGV` **仅 1 次**(push run `42488656322` attempt 1,`sdk_interaction_tests` / `test_player_integration.cpp:726` 的 `seek to middle moves position forward`),`rerun --failed` 后 attempt 2 全绿;其余 6 个历史 `failure` 分别是编译错误、`skip not allowed in CI`、断言失败、第三方 boost 拷贝、`Build coverage target` 等待超时,形态都不同。**满足任一才算「反复出现」、才另开票重审**:① 不同 `headSha` 上同形态 SIGSEGV ≥2 次且 `rerun --failed` 不能稳定消解;② 本机 `ctest -j4` 同轮数对照中该用例可稳定复现(≥2/10);③ 拿到崩溃栈 / ASan 报告指向具体竞态。单次偶发 + rerun 全绿 → 维持观测,不改判据 / TIMEOUT / 坏输入 55 例基线。取证:`gh run list --workflow sdk-coverage.yml --limit 50 --json databaseId,conclusion,attempt` 列候选,再对每个 `failure` run 用 `gh run view <id> --log-failed | grep -Ei "CRASHED|SIGSEGV"`;**run 已是 attempt 2 成功时 `--log-failed` 看不到 attempt 1**,须用 `gh api repos/<owner>/<repo>/actions/runs/<id>/attempts/1/jobs` 补查(注意 `gh api` 不吃 `-R`)。 (2026-11-09 补:门槛②已实测 —— 本机 Windows / MinGW730 跑 10 轮 `ctest -j4` 全套 14/14 全绿(57–60s/轮,`sdk_interaction_tests` 13.58–14.21s)、**0 次 SIGSEGV**;另用 `UnitTests -ts="MusicPlayer Interactive Paths" -tc="seek to middle moves position forward"` 单用例连跑 100 次,**0 崩溃 0 非零退出**(127s)。**平台局限**:CI 那次 SIGSEGV 发生在 Linux ubuntu-24.04,本地实验是 Windows,不能等价外推;三条门槛至 11-09 均未命中,维持观测,不另开票、不改生产代码。)**2026-11-09 一次性破例留痕**:用户明确指令「强制重审」,主动打破本条门槛、不等第二次出现,开 [#112](https://github.com/wt159/SoundBridge/issues/112) / [#113](https://github.com/wt159/SoundBridge/issues/113) 并出 PR #114;该 effort 事前写死边界「无复现、无崩溃栈、CI 在 Linux / 本机在 Windows,交付状态契约加固 + 判据 + 稳定性对照,**不是修复 SIGSEGV**」,且因上述稳定性闸门未过而**全部撤回,终态仍是不改生产代码**。三条门槛本身不删、不放宽,本次破例不构成后续自动启动 effort 的先例;SIGSEGV 根因仍未定位,H1 在 Windows 侧 240 次窗口命中 0 崩溃已证伪、H2 未改、H3/H4 未验。取证与反悔条件见 `doc/player-state-contract.md` §10.4 与 `doc/streaming-playback-spec.md` §12.6。
- **真触发时 seek 竞态的最小重审范围**(2026-11-08 只读勘察;日志无崩溃栈,以下均未证实为根因):① `AudioStreamDecoder::seekToMs()`(`AudioStreamDecoder.cpp:219-233`)只写 `m_seekRequestMs` 单槽,native RA 定位在解码线程 `exchange(-1)` 后执行;`cancelRequested()` 把「槽内有值」当取消(`:457`),seek 期间刻意不置 `m_abortDecode`(`:226-231`,#59/#60/#65 已知时序坑),而 stop 靠 `m_stopRequest + m_abortDecode` 中断读回调 —— **「seek 槽未消费完就 stop」是最可疑的叠加窗口**。② 析构期已有 issue #70 的 `SDL_CloseAudioDevice` 双释放防护(`MusicPlayer.cpp:153-157`:`m_workQueue.shutdown()` 必须先于 `_releaseAudio()`);`stop()` 是异步队列任务(`:219`),`_stop()` 已走过一次 `_releaseAudio()`(`:369-370`),析构再走一次,重复路径幂等性无专门判据覆盖。③ **已排除**:`m_seekBaseMs` / `m_seekDiscardBytes` 虽非 atomic,但只在解码线程内读写(seek 分支 `:538/:614/:677/:975` 与 EOS 汇总 `:345/:353/:366`);消费线程经 `MusicPlayer.cpp:555` 调的 `positionMs()`(`:430-441`)只读 atomic `m_segmentBaseBytes` + `m_ring->readPos()`,**不构成数据竞争**,别重复怀疑这条。
Expand Down
8 changes: 7 additions & 1 deletion sdk/test/unit/test_player_integration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1396,8 +1396,14 @@ TEST_SUITE("PublicPlayer Interactive Paths")
},
3000);
const std::vector<uint64_t> after = callbacks.positionValues();
// seek 是异步 WorkQueue 任务;first 快照到 seek 生效之间旧会话仍会追加 position。
// 新会话起点必须按序列内容定位实际回退点,不能用固定快照下标猜。
size_t rewindAt = after.size();
for (size_t i = beforeCount; i < after.size(); ++i) {
if (after[i] < before) { rewindAt = i; break; }
}
bool afterMonotonic = true;
for (size_t i = beforeCount + 1; i < after.size(); ++i) {
for (size_t i = rewindAt + 1; i < after.size(); ++i) {
if (after[i] < after[i - 1]) {
afterMonotonic = false;
}
Expand Down
Loading