diff --git a/.claude/skills/add-a-platform/SKILL.md b/.claude/skills/add-a-platform/SKILL.md index fc25c3a..8ff0c29 100644 --- a/.claude/skills/add-a-platform/SKILL.md +++ b/.claude/skills/add-a-platform/SKILL.md @@ -104,7 +104,7 @@ web-wasm を足し、前者に足し忘れた** —— ローカルの全レー | 形 | 実例 | | --- | --- | -| platform **名**のリスト | `.github/workflows/release.yml` に 2 つ(matrix と合わせて同一ファイルに 3 つ) | +| platform **名**のリスト | `.github/workflows/release.yml` に 2 つ(matrix と合わせて同一ファイルに 3 つ)。**`ci-unity.yml` にも 2 つある** —— `unity` job と `tarball` job が、それぞれ「重ねる他 platform」を名前で並べる(2026-09-11 に 2 つ目が増えた。**どちらも fail-closed** で、足りなければ合図が置かれず検査が赤くなる) | | Unity の `BuildTarget` | `PluginGatingTests.cs` の `Slots` | | workflow の matrix | 各 `.yml` | diff --git a/.claude/skills/milestone-complete/SKILL.md b/.claude/skills/milestone-complete/SKILL.md index 7b47c54..a82964f 100644 --- a/.claude/skills/milestone-complete/SKILL.md +++ b/.claude/skills/milestone-complete/SKILL.md @@ -84,12 +84,27 @@ pwsh tools/dev.ps1 test-unity-graphics # M7a。GPU に依る RenderTexture の pwsh tools/dev.ps1 benchmark # M7a。上の 2 レーンから OCVU_BENCH: を集める ``` -**下の 2 つはどの workflow からも走らない**(`test-unity-tarball` / -`test-unity-web` も同じである)。**CI に配線されていないレーンは、判定のときだけが -唯一の実行機会である** —— 回さなければ誰も回さない。 -`tests/UnityProject/Assets/Tests/EditMode/CiVisibilityTests.cs` が -「CI から見えないテスト」を名指しで固定しているが、それが固定しているのは -まさに `test-unity-graphics` の中身である。 +**2026-09-11 より前は、下の 2 つと `test-unity-tarball` がどの workflow からも +走っていなかった**(`test-unity-web` は例外で、**M6 から `web-e2e` job が +同じところを見ている** —— そちらは 2026-09-03 である)。いまは `ci-unity.yml` に +対応するレーンが在る(`Graphics` / `benchmarks` / `tarball` / `web-e2e`)ので、 +**「判定のときだけが唯一の実行機会」ではなくなった** —— ただし +**結び付きの強さは 4 つで違う。** `tarball` job は +`./tools/dev.ps1 test-unity-tarball -PrepareOnly` を**実際に呼ぶ**が、 +`web-e2e` は `dev.ps1` を通さずスクリプトを直接呼び、`Graphics` / +`benchmarks` はその中間である(Unity の起動はいずれも game-ci)。 +したがってローカルで回す意味は残る: **同じ判定 script を通る +別の経路で、同じものが見えるかを突き合わせられる。** + +**「CI に配線されていないレーンは、判定のときだけが唯一の実行機会である」 +という規則そのものは生きている。** **加えて、配線しただけでは足りない** —— +上の 4 つのうち**必須チェックなのは `web-e2e` だけ**で、残る 3 つは +赤くても merge を止めない。 +**判定のときは「CI が走らせているか」と「CI が止めるか」を別々に確かめること。** 配線が無いレーンを作ったら、この一覧に +足すこと —— **走らないレーンは腐る。** 実例が 2 つある(どちらも +`test-unity-tarball`): M4 で期待する binary の数が `3` と直書きされたまま +残り、M7a で Graphics の除外が抜けた。**どちらも無関係な作業の途中で +1 度手で回すまで誰も知らなかった。** **`test-unity-player` は、このマシンでは後始末段階でハングする既知の欠陥を持つ** (`Stop-UnityTestPlayers` の中の `Get-CimInstance`。roadmap の diff --git a/.claude/skills/prove-a-check-works/SKILL.md b/.claude/skills/prove-a-check-works/SKILL.md index e66194c..fa8f23a 100644 --- a/.claude/skills/prove-a-check-works/SKILL.md +++ b/.claude/skills/prove-a-check-works/SKILL.md @@ -629,7 +629,9 @@ cmd | tail -3; echo $? # これは tail の終了コード `-testCategory '!Graphics'` を渡しておらず、`-nographics` の下で `[Category("Graphics")]` の 6 件が skip されずに fail した(実測: 除外前は 66 件中 6 件失敗・exit 1、除外後は 59 件全通過・exit 0)。**直るまで CI は -緑のままだった** —— このレーンはどの workflow からも走らないからである。 +緑のままだった** —— **当時**このレーンはどの workflow からも走らなかったからである +(2026-09-11 に `ci-unity.yml` の `tarball` job として配線した。**ただし必須 +チェックではないので、いまも赤は merge を止めない**)。 **教訓は「一度直したから大丈夫」ではない: 走らないレーンは、直した後も 同じ速さで腐る。** なお `unity-probe.yml` にも同じ抜けが**分かっていて 残してある**(意図的な例外は、意図的だと書いてあれば腐らない)。 @@ -832,6 +834,41 @@ M5 の `calib` で、実装者と再レビュアーが同時に同じソース コメントには「**契約を成立させている唯一のものとして残してあるが、その必要性を 示す再現テストはまだ無い**」と書き、**安全装置として残す判断そのものを記録した。** +## 「原理的に無理」と書いた前提を、誰も測っていないことがある + +**2026-09-11 に踏んだ。** 3 つの文書(`CLAUDE.md` / `docs/roadmap.md` / +`docs/performance.md`)が揃って「CI のレーンは `-nographics` で走るので、 +GPU に依る経路は CI では確かめられない」と書いていた。**その根拠として +挙がっていた実測は、すべてローカルのもの**(`tools/dev.ps1` が +`-nographics` を渡す)で、**CI が何を渡しているかは 1 度も測っていなかった。** + +実物は逆だった。`game-ci/unity-test-runner` がコンテナの中で呼ぶ +`unity-editor` は `xvfb-run -ae /dev/stdout "$UNITY_PATH/Editor/Unity" +-batchmode "$@"` で、**`-nographics` を付けない。** レーンを 1 本足して +投げたら 7 件とも通り、**そのマイルストーンで「CI からは原理的に見えない」 +と判定していた経路が、丸ごと CI に載った。** + +**この形は、失敗した検査より見つけにくい。** 検査は赤くなれば気づくが、 +**「その検査は書けない」という結論は、書かれなかったコードとして残る**ので +何も赤くならない。しかも 3 箇所に書かれていたことが、確からしさの錯覚を +強めていた —— **3 箇所は 3 つの根拠ではない。1 つの根拠の 3 つの写しである。** + +**見分け方**: 「原理的に」「構造上」「〜できない」と書いてある制約を見たら、 +**その根拠が測定なのか推論なのかを問う。** 測定なら、**何を測ったのか**を +問う —— 上の例では測定自体は本物だったが、**測った対象がローカルの +`dev.ps1` で、主張していた対象は CI だった。** + +**やり方**: 制約が正しければ赤くなり、間違っていれば緑になるレーンを +**実際に足して投げる。** このリポジトリには先例がある +(`unity-probe.yml` は「結論を出すための道具」として存在し、 +`game-ci は macOS を支えない` を実測で確定させた)。**推測を文書に +書き足すより、1 回投げるほうが安い。** + +**逆向きの陳腐化にも同じ手が要る。** 「まだできていない」と書いた制約が +解消されたとき、**誰も赤くならない**のは同じである(`nightly.yml` が +毎日動いているのに「schedule でまだ 1 度も走っていない」と書かれ続けた例が +このリポジトリにある)。 + ## やってはいけないこと - 通ったことを、検査していることの証拠として報告する @@ -844,6 +881,7 @@ M5 の `calib` で、実装者と再レビュアーが同時に同じソース - 壊れていても同じ値を返す述語(常に true を返す能力照会など)で assertion を書く - SKIP を合格として数える - **負の対照が取れなかったのに、取れたかのように書く**(上の節) +- **「原理的に無理」と書かれた制約を、根拠を問わずに引き継ぐ**(測定なのか推論なのか、測定なら何を測ったのかを問う。上の節) ## 参照 diff --git a/.github/workflows/ci-unity.yml b/.github/workflows/ci-unity.yml index d4d07d4..1fe9e45 100644 --- a/.github/workflows/ci-unity.yml +++ b/.github/workflows/ci-unity.yml @@ -262,7 +262,16 @@ jobs: # 重ねてから走るので、無条件に全部を要求してよい。** # 末尾の '[' まで含めるのは境界のため。'present: 5' だけだと # 'present: 50' にも当たる。 - requireOutput: 'native plugins present: 6 [' + # **2 つ要求する**(';' 区切り。assert-unity-results.ps1 が split する)。 + # + # 2 つ目は I-1 のレビュー指摘で足した。`ProfileGatingTests` を + # 「define が無ければ無い」から「define の有無と一致する」に変えた + # ことで、**既定のレーンから「立っていないこと」への要求が消えた** —— + # 誰かが ProjectSettings.asset に OCVU_PROFILE_DNN を commit すると、 + # このレーンは陽性の分岐を通って**緑のまま**になる。 + # dnn を切った利用者のビルドが壊れないことを見るはずのレーンが、 + # dnn を入れた状態を見ていることになる。 + requireOutput: 'native plugins present: 6 [;OCVU_PROFILE_DNN is not defined' # **`tools/dev.ps1` の `Test-UnityEditMode` と同じ除外を CI にも渡す。** # # ローカルは `-batchmode -nographics -testCategory '!Graphics'` で @@ -289,6 +298,10 @@ jobs: # 対象にならず、そのまま 1 トークンとして渡る——引用符で囲むと逆に # 引用符ごと 1 トークンになり `!Graphics` に一致しなくなる)。 customParameters: '-testCategory !Graphics' + # **既定 profile。** 空文字を渡すのは requireTest / customParameters と + # 同じ理由で、「渡し忘れ」と「意図的に何も立てない」を区別できる形に + # しておくためである。下の Dnn レーンだけがここに値を持つ。 + defineSymbols: '' # L5: 実際に IL2CPP Player をビルドして、その上で走らせる。 # scripting backend と stripping level は ProjectSettings に # コミットしてあるので(scriptingBackend.Standalone: 1、 @@ -308,6 +321,88 @@ jobs: # 空文字を渡すのは requireTest / requireOutput と同じ理由(渡し忘れと # 意図的な空を区別できる形にする)。 customParameters: '' + defineSymbols: '' + + # **GPU に依る経路を CI で実際に走らせる(`[Category("Graphics")]`)。** + # + # **これを足せたのは、game-ci の Editor が -nographics で走っていない + # と実測で分かったからである。** コンテナの `unity-editor` は + # + # xvfb-run -ae /dev/stdout "$UNITY_PATH/Editor/Unity" -batchmode "$@" + # + # で、**仮想 X の下で、-nographics 無しで**起動する(game-ci/docker の + # images/ubuntu/editor/Dockerfile)。したがって + # `SystemInfo.graphicsDeviceType` が Null にならない可能性があり、 + # ローカル専用だった `dev.ps1 test-unity-graphics` と同じものを + # CI で走らせられる見込みが立った。 + # + # **見込みであって、Dockerfile からは決まらない。** `unityci/base` は + # `--no-install-recommends` で `libglu1` しか入れておらず、Mesa の + # 実装(`libglx-mesa0` / `libgl1-mesa-dri`)が入るかは依存の解決次第 + # である。**だから推測で書かず、このレーンそのものを実測に使った** —— + # 結論は docs/roadmap.md の「GPU 経路を CI に載せる」にある。 + # + # **GPU が無ければ skip ではなく fail する。** `AGraphicsDeviceIsPresent` + # がそう書かれており(GraphicsChecks)、それが正しい —— 「GPU が無い + # から確かめられなかった」を緑にすると、このレーンは在るのに何も + # 見ていない状態になる。 + - lane: Graphics + testMode: EditMode + # GPU に依るのは Editor 側の経路で、IL2CPP は要らない。 + imageFlavour: base + # **名指しする。** category の綴りを間違えると 0 件になり、 + # 「passed が 1 以上」は他のテストでは満たせない(このレーンは + # Graphics だけを走らせる)——それでも、どれが通ったかを要求する + # ほうが強い。GPU が要る 5 件と、このレーンでしか測れない + # benchmark を名指しする。 + # **`EveryCheckInTheSharedBodyIsWiredIntoThisEntryPoint` を落とさない。** + # 共有本体(GraphicsChecks)に足した検査が入口に配線されているかを + # 見る唯一のテストで、**これが assembly から外れても残り 6 件で + # 緑になる。** 名指しの中で最も消えたら困る 1 件である。 + requireTest: 'GraphicsTests.AGraphicsDeviceIsPresent;GraphicsTests.SyncReadbackProducesTheExpectedPixels;GraphicsTests.VerticalFlipIsApplied;GraphicsTests.AsyncMatchesSync;GraphicsTests.TakingTheMatTwiceIsRejected;GraphicsTests.EveryCheckInTheSharedBodyIsWiredIntoThisEntryPoint;GraphicsBenchmarkRunner.MeasureRenderTexturePaths' + requireOutput: '' + # **`!Graphics` の補集合。** 2 つで完全に分かれる。 + customParameters: '-testCategory Graphics' + defineSymbols: '' + + # **非既定 profile(dnn)を Unity の中で実際にコンパイルする。** + # + # M7c が残した穴そのものである —— それまで + # **どの workflow も、どの dev.ps1 のレーンも `OCVU_PROFILE_DNN` を + # 立てていなかった**ので、`CvUnity.Interop.Dnn` / `CvUnity.Dnn` が + # Unity の中でコンパイルできることは人が手で 1 回確かめたきりだった。 + # + # **define は下の step が ProjectSettings.asset に書き込む。** + # `ProfileGatingTests` が「立っていない」方向を、このレーンが + # 「立てれば現れる」方向を見る —— **1 回の実行では原理的に + # 届かない**と同 class の docstring が書いていたのは正しく、 + # 届かせるには実行を 2 つに分けるしかない。 + - lane: DnnEditMode + testMode: EditMode + imageFlavour: base + # **define を立てた側でしか通らないものを名指しする。** + # 立て損ねたら(書き込みが空振りしたら)このレーンは赤くなる。 + requireTest: 'ProfileGatingTests.TheDnnInteropAssemblyMatchesTheDefine;ProfileGatingTests.TheDnnSharedTestAssemblyMatchesTheDefine;ProfileGatingTests.TheDnnPublicApiAssemblyMatchesTheDefine' + # **テスト自身が出した事実を要求する。** 名前だけだと、define が + # 無い側の分岐を通っても同じ名前が Passed で並ぶ。 + requireOutput: 'OCVU_PROFILE_DNN is defined and the dnn assemblies are compiled' + customParameters: '-testCategory !Graphics' + defineSymbols: 'OCVU_PROFILE_DNN' + + # **stripping 済みの実物の Player で、dnn の P/Invoke が生き残ること。** + # + # 既定 profile の `AbiSurfacePlayerTests` と同じ役割を dnn 側で果たす。 + # **Player でしか確かめられない** —— IL2CPP の stripping が消せるのは + # 呼ばれない宣言なので、Editor(Mono) では成立しない。 + - lane: DnnStandalone + testMode: Standalone + imageFlavour: linux-il2cpp + # 4 本の宣言を 1 回ずつ呼ぶ生成物の、手書きの唯一の入口。 + requireTest: 'AbiSurfaceDnnPlayerTests.EveryDnnEntryPointIsReachable' + requireOutput: '' + # EditMode 専用の asmdef は Player に入らないので、除外する対象が無い。 + customParameters: '' + defineSymbols: 'OCVU_PROFILE_DNN' steps: - name: Checkout @@ -467,6 +562,47 @@ jobs: Set-Content -LiteralPath 'tests/UnityProject/ocvu-expect-all-platforms' ` -Value '1' -NoNewline + # **このレーンだけ scripting define symbols を立てる。** + # + # `OCVU_PROFILE_DNN` を立てると `CvUnity.Interop.Dnn` / `CvUnity.Dnn` / + # `CvUnity.Tests.Shared.Dnn` がコンパイル対象に入る(asmdef の + # defineConstraints)。**Unity の compilation pipeline が見るのはここで、 + # `csc.rsp` では届かない** —— あちらはコンパイラに渡る define であって、 + # Unity が defineConstraints を評価するときの集合ではない。 + # + # **書き換えが空振りしたら落とす。** 空振りすると define の無い側の + # 分岐を通って**同じテスト名が Passed で並ぶ**ので、出力からは + # 区別が付かない。入力(ここ)と出力(assert-unity-results.ps1 の + # -RequireOutput)の両方で見る —— 入力だけでは「書いたつもり」を、 + # 出力だけでは「なぜ立たなかったか」を取り逃がす。 + - name: Turn on the scripting define symbols for this lane + if: matrix.defineSymbols != '' + shell: pwsh + run: | + $file = 'tests/UnityProject/ProjectSettings/ProjectSettings.asset' + $text = Get-Content -LiteralPath $file -Raw + + # 既定は空の map。ここが変わったら、黙って no-op にせず落とす。 + $empty = "`n scriptingDefineSymbols: {}`n" + if ($text -notmatch [regex]::Escape($empty)) { + Write-Error "scriptingDefineSymbols is not the expected empty map in $file" + exit 1 + } + + # キーは NamedBuildTarget の名前。同じファイルの scriptingBackend が + # 'Standalone' を使っているので、それに合わせる。 + $filled = "`n scriptingDefineSymbols:`n Standalone: ${{ matrix.defineSymbols }}`n" + $text = $text.Replace($empty, $filled) + Set-Content -LiteralPath $file -Value $text -NoNewline + + # 書けたことを読み直して確かめる。 + $after = Get-Content -LiteralPath $file -Raw + if ($after -notmatch [regex]::Escape('Standalone: ${{ matrix.defineSymbols }}')) { + Write-Error 'failed to write the scripting define symbols' + exit 1 + } + Write-Host '==> scripting define symbols: ${{ matrix.defineSymbols }}' + # **pre-release を固定してある。** これだけが node20 を宣言する action # だった(GitHub は 2025-09 に node20 を非推奨にし、いまは node24 上で # 強制実行している —— 実測: ci-unity の annotation に @@ -553,6 +689,317 @@ jobs: path: artifacts/unity-${{ matrix.lane }} if-no-files-found: warn + # **測った数字を CI が publish する。** + # + # `dev.ps1 benchmark` は M7a から在るが、**どの workflow からも走って + # いなかった** —— しかも唯一 Unity を持つ開発機では、内部で呼ぶ + # `test-unity-player` が後始末でハングする既知の欠陥のせいで**完走しない**。 + # 数字は「ハングしたプロセスを止めてから `run-benchmarks.ps1` を直接叩く」 + # という手順でしか得られていなかった。 + # + # **GPU に依る側が CI で走るようになって初めて、この job が成立した。** + # `GraphicsBenchmarkRunner` は `Graphics` レーンにしか居ないので、 + # そのレーンが無い間はここで集められるのは半分だけだった。 + # + # **時間そのものは判定しない**(設計 D1)。`run-benchmarks.ps1` が落ちるのは + # 「測れなかったとき」だけである —— XML ごとに 1 本も `OCVU_BENCH:` が + # 拾えなかったか、値が 0 だったとき。共有 runner の上で閾値を置くと + # フレークになるので、速い・遅いでは落とさない。 + benchmarks: + name: Publish the benchmarks + runs-on: ubuntu-24.04 + needs: unity + # **必須チェックへ昇格したので、`if: !cancelled()` を付けた。** + # + # 素の `needs:` だと、`unity` が落ちた瞬間にこの job は **skip** になる。 + # **GitHub は skip を required check の合格として通す**ので、必須に + # したまま素の `needs:` を残すと、**依存が落ちているのに緑と同じ意味を + # 持つ**(`unity` job の長いコメントが同じことを書いている)。 + # + # `!cancelled()` なら依存が落ちても走り、集める XML が無いので + # 「artifact が無い」で**赤くなる** —— skip ではなく赤である。 + # + # **`always()` にしない。** この workflow は `cancel-in-progress: true` を + # 宣言しており、`always()` だと追い越された古い run が最後まで走る。 + if: ${{ !cancelled() }} + timeout-minutes: 15 + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Download the Player results + uses: actions/download-artifact@v8 + with: + name: test-results-unity-Standalone + path: bench-in/standalone + + - name: Download the Graphics results + uses: actions/download-artifact@v8 + with: + name: test-results-unity-Graphics + path: bench-in/graphics + + # **これは `dev.ps1` の `Invoke-Benchmark` の 2 つ目の写しである。** + # + # 同じブランチの `tarball` job は「作り方を CI に書き写すと別物になる」 + # という理由で `-PrepareOnly` を作ったのに、こちらは写している + # (レビュー指摘 M-4)。**写したまま残す判断の理由**: あちらが寄せたのは + # **出来る物**(使い捨てプロジェクト)で、こちらが持つのは + # **どのレーンの XML を集めるか**という配線だけである。 + # `dev.ps1` 側はローカルのレーン名(`unity-player.xml` / + # `unity-graphics.xml`)を、CI 側は artifact 名を指すので、 + # **同じ物を指す 1 つの表にはできない。** 集計と判定そのものは + # `run-benchmarks.ps1` に寄せてあり、そこは分かれていない。 + # **3 本目の benchmark レーンを足すときは、両方を直すこと。** + # + # **2 つの XML を名指しで渡す。** `run-benchmarks.ps1` は + # **XML ごとに**「1 本も拾えなかったか」を見るので、片方のレーンの + # benchmark 本体が丸ごと消えても、もう片方の行に紛れて通ることはない。 + - name: Collect the benchmark results + shell: pwsh + run: | + $paths = @() + foreach ($dir in @('bench-in/standalone', 'bench-in/graphics')) { + $xml = Get-ChildItem -Path $dir -Filter '*.xml' -Recurse -File | + Select-Object -First 1 + if (-not $xml) { + Write-Error "no test result XML under $dir" + exit 1 + } + Write-Host "==> $dir -> $($xml.FullName)" + $paths += $xml.FullName + } + ./tools/run-benchmarks.ps1 -XmlPath ($paths -join ';') ` + -OutPath artifacts/benchmarks/latest.json + Get-Content -LiteralPath artifacts/benchmarks/latest.json + + - name: Upload the benchmarks + if: always() + uses: actions/upload-artifact@v7 + with: + name: benchmarks + path: artifacts/benchmarks + if-no-files-found: warn + + # **配る tarball を、使い捨ての Unity プロジェクトに実際に導入する。** + # + # `dev.ps1 test-unity-tarball` は M3 から在るのに、**どの workflow からも + # 走っていなかった** —— roadmap は M4 の時点で「game-ci の action の外で + # Unity を起動する必要があり、CI に載せるのは別作業である」と書き、 + # そのまま置かれていた。 + # + # **走らないレーンは腐る、を 2 度実証した後で載せる。** + # - M4 でモバイルを足したとき、期待する binary の数が `3` と直書きされて + # いて iOS の `.a` を binary と認めなかった + # - M7a で Graphics の除外が抜け、66 件中 6 件が落ちる状態になった + # どちらも**無関係な作業の途中で 1 度手で回すまで誰も知らなかった。** + # + # **`unity` job に相乗りしない。** あちらは `tests/UnityProject` を直接 + # 指すが、このレーンの本体は**使い捨てのプロジェクトを作るところ**にある + # (tarball から解決させ、ディレクトリ参照が 1 つも残っていないことまで + # 見る)。projectPath が違うので matrix には畳めない。 + # + # プロジェクトの作り方は `dev.ps1 test-unity-tarball -PrepareOnly` に寄せて + # ある —— ここに書き写すと、**導入を確かめた tarball と実際に配る + # tarball が別物になる。** + tarball: + name: UPM tarball install (Linux) + runs-on: ubuntu-24.04 + needs: plugin + # `unity` job と同じ理由(そちらの長いコメントを参照)。skip は必須 + # チェックを通すので、材料が無いときは赤くなる形にする。 + if: ${{ !cancelled() }} + timeout-minutes: 90 + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Download the other platforms plugins + uses: actions/download-artifact@v8 + with: + pattern: unity-plugin-* + path: other-plugins + + - name: Assemble every platform into one package + shell: pwsh + run: | + $sources = @('windows-x64', 'macos-arm64', 'android-arm64', 'ios-arm64', 'web-wasm') | + ForEach-Object { "other-plugins/unity-plugin-$_" } + ./tools/assemble-plugins.ps1 -Source ($sources -join ';') + + - name: Resolve the Unity version + id: unity + shell: pwsh + run: | + $file = 'tests/UnityProject/ProjectSettings/ProjectVersion.txt' + $m = Select-String -LiteralPath $file -Pattern '^m_EditorVersion:\s*(\S+)' | + Select-Object -First 1 + if (-not $m) { + Write-Error "could not read m_EditorVersion from $file" + exit 1 + } + $version = $m.Matches[0].Groups[1].Value + "version=$version" >> $env:GITHUB_OUTPUT + "image=unityci/editor:ubuntu-$version-base-3" >> $env:GITHUB_OUTPUT + + - name: Resolve OpenCV config hash + id: opencv-config + shell: pwsh + run: | + Import-Module ./tools/OpenCvConfig.psm1 -Force + $hash = Get-OpenCvConfigHash -Config (Get-OpenCvConfig) + "config-hash=$hash" >> $env:GITHUB_OUTPUT + + - name: Cache OpenCV + uses: actions/cache@v6 + with: + path: third_party/opencv/${{ steps.opencv-config.outputs.config-hash }} + key: opencv-${{ steps.opencv-config.outputs.config-hash }} + + # `unity` job と同じ理由でコンテナの中でビルドする(runner の glibc で + # 作った .so は game-ci のコンテナでは読み込めない。v0.1.0 の欠陥)。 + - name: Build the native plugin in the build container + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + image=$(grep -oP "Container\s*=\s*'\K[^']+" tools/opencv-config.psd1 | head -1) + echo "==> building in $image" + docker run --rm \ + -v "$PWD:/w" -w /w \ + -e GH_TOKEN \ + "$image" \ + bash -c 'bash ./tools/ci/setup-linux-container.sh \ + && pwsh -NoProfile -File ./tools/opencv.ps1 restore \ + && pwsh -NoProfile -File ./tools/dev.ps1 build' + sudo chown -R "$(id -u):$(id -g)" \ + Packages/com.ayutaz.opencv-unity-native/Runtime/Plugins build third_party || true + + - name: Verify the plugin's portability + shell: pwsh + run: ./tools/verify-plugin-portability.ps1 + + # **プロジェクトの作り方はローカルと同じ script を通す。** + # + # `-PrepareOnly` は Unity を起動せず、作ったプロジェクトのパスを + # 最後の 1 行に出す。`-WorkDir` はワークスペースの中でなければ + # ならない —— game-ci はコンテナにワークスペースを mount するので、 + # その外に作ったプロジェクトはコンテナから見えない。 + - name: Prepare a disposable Unity project from the tarball + id: prepare + shell: pwsh + run: | + $work = Join-Path $env:GITHUB_WORKSPACE 'artifacts/tarball-lane' + # **$LASTEXITCODE と、出来た物の存在の両方を見る。** + # + # レビューは「`&` で .ps1 を呼んでも $LASTEXITCODE は設定されない + # ので見ても無駄」と指摘したが、**実測すると設定される** —— + # `exit 7` する .ps1 を `& child.ps1 | Select-Object -Last 1` で + # 呼ぶと $LASTEXITCODE は 7 になった(パイプ越しでも同じ)。 + # `tools/dev.ps1` の失敗経路 `Write-DevFailure` は throw ではなく + # **`exit 1`** なので(そちらの docstring に理由がある)、ここを + # 見ないと**静かに次へ進む。** + # + # **指摘をそのまま適用しかけて、測って引き返した。** このリポジトリの + # 「仮説のまま直さない」がレビューの指摘にも同じだけ掛かる。 + $project = & ./tools/dev.ps1 test-unity-tarball -PrepareOnly -WorkDir $work | + Select-Object -Last 1 + if ($LASTEXITCODE -ne 0) { + Write-Error "dev.ps1 test-unity-tarball -PrepareOnly failed (exit $LASTEXITCODE)" + exit 1 + } + if (-not $project -or -not (Test-Path -LiteralPath $project)) { + Write-Error "the prepared project path does not exist: $project" + exit 1 + } + $rel = [System.IO.Path]::GetRelativePath($env:GITHUB_WORKSPACE, $project).Replace([char]92, [char]47) + "project=$rel" >> $env:GITHUB_OUTPUT + Write-Host "==> prepared project: $rel" + + # **合図を「書く」のではなく「届いたか」を見る。** + # + # `unity` job はここで合図を書くが、このレーンでは `dev.ps1` が + # **木から導出して**置く(Sync-AllPlatformsMarker。6 つ揃っていなければ + # 置かず、置き去りがあれば消す)。ここで書き直すと、導出が壊れていても + # 合図だけは在る状態を自分で作ってしまう。**導出の結果を要求するほうが + # 強い。** + - name: Check the all-platforms marker reached the prepared project + shell: pwsh + run: | + $marker = Join-Path '${{ steps.prepare.outputs.project }}' 'ocvu-expect-all-platforms' + if (-not (Test-Path -LiteralPath $marker)) { + Write-Error "the prepared project has no all-platforms marker at $marker" + exit 1 + } + Write-Host "==> present: $marker" + + - name: Run the EditMode tests in the prepared project + uses: game-ci/unity-test-runner@f7d28f891263d875d47ef34370e9e8dd6087e1ef # v5.0.0-beta.1 + id: tests + env: + UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + with: + projectPath: ${{ steps.prepare.outputs.project }} + unityVersion: ${{ steps.unity.outputs.version }} + customImage: ${{ steps.unity.outputs.image }} + testMode: EditMode + artifactsPath: artifacts/unity-tarball + githubToken: ${{ secrets.GITHUB_TOKEN }} + checkName: Tarball results + customParameters: '-testCategory !Graphics' + + - name: Assert the results + if: always() + shell: pwsh + run: | + $xml = Get-ChildItem -Path 'artifacts/unity-tarball' -Filter '*.xml' -Recurse -File | + Select-Object -First 1 + if (-not $xml) { + Write-Error 'no test result XML was produced under artifacts/unity-tarball' + exit 1 + } + ./tools/assert-unity-results.ps1 ` + -ResultsPath $xml.FullName ` + -Lane 'tarball' ` + -RequireTest 'PluginGatingTests.ExactlyOnePluginTargetsThisEditorOs;PluginGatingTests.EachPluginTargetsOnlyItsOwnEditorOs;PluginGatingTests.NoPluginIsEnabledForEveryPlatform;PluginGatingTests.EachPluginIsEnabledOnlyForItsOwnStandaloneTarget' ` + -RequireOutput 'native plugins present: 6 [;OCVU_PROFILE_DNN is not defined' + + # **テストが通ったことは、tarball で解決された証拠にならない。** + # テストはパッケージの外(プロジェクトの Assets/)に在るので、 + # ディレクトリ参照で解決されても同じ数だけ通る。UPM が書き戻した + # packages-lock.json を読んで、参照が本当に .tgz だったかを見る。 + # ローカルの `dev.ps1` はこれを自分でやっているが、こちらは Unity の + # 起動が game-ci なので、同じ判定をここに置く。 + - name: Check that UPM resolved the package from the tarball + if: always() + shell: pwsh + run: | + $lock = Join-Path '${{ steps.prepare.outputs.project }}' 'Packages/packages-lock.json' + if (-not (Test-Path -LiteralPath $lock)) { + Write-Error "UPM did not write back $lock, so we cannot tell how the package resolved" + exit 1 + } + $manifest = Get-Content -LiteralPath $lock -Raw | ConvertFrom-Json + $entry = $manifest.dependencies.'com.ayutaz.opencv-unity-native' + $resolved = if ($entry) { $entry.version } else { '(no entry)' } + if ($resolved -notlike '*.tgz') { + Write-Error "UPM resolved from a non-tarball reference: $resolved" + exit 1 + } + Write-Host "==> resolved from $resolved" + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v7 + with: + name: test-results-unity-tarball + path: artifacts/unity-tarball + if-no-files-found: warn + # **ブラウザで実際に動かす。** これが「動く」を主張できる唯一のレーンである # (`add-a-platform` skill の「ビルドできる / 配れる / 動く」)。 # diff --git a/CLAUDE.md b/CLAUDE.md index 21fcb6b..94ee6a2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -76,12 +76,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co | `./tools/dev.ps1 test-managed` | L3 のみ。**solution の全テストプロジェクトを回す** —— `CvUnity.Tests.Managed` が P/Invoke 越しに実物の DLL を叩く **196 件**(M7a で `AllocationTests` が加わり 181 → 185、M7c で `DnnInferenceTests` が加わり 185 → 195、レビューの M6 で forward の OpenCV エラー経路を実証するテストが 1 件加わり 196 になった)、`Ocvu.Generator.Tests` が spec と生成器を見る **116 件**(M5 で新設、106 件。M7b で profile 分岐のテストが加わり 113 件、最終レビューの C-1 で `LibraryName` の写しを見る 3 件が加わり 116 件になった)。**件数を書いてあるのはこの行だけである** —— 他所に写すと、テストを 1 件足した瞬間にそちらだけが嘘になる(M5 で実際に 4 箇所が同時に古くなった) | 約 11 秒(**44 件だけだった頃の値**) | | `./tools/dev.ps1 test-asan` | L2(AddressSanitizer) | 約 18 秒(増分。2026-08-28 実測。M1 時点の約 11 秒より伸びているが、原因は未特定——増えたのは L1 側で計測済みの再コンパイル対象と同じファイル群で、M3 固有の変更ではない) | | `./tools/dev.ps1 test-managed-probe` | **CI 専用**。L3 のクラッシュ・ハングプローブ | 約 50 秒(segfault 6 秒 + hang 36 秒) | -| `./tools/dev.ps1 test-unity-editmode` | L4(Unity EditMode、Mono、**59 件**。`-testCategory '!Graphics'` で `test-unity-graphics` 側のテストを除外する) | 約 27 秒(増分。2026-08-28 実測。**Unity 6000.0.82f1・EditMode 10 件のときの値。** その後 M3.5 で 16 件、M4 で 33 件、M5 で 34 件、2026-09 の API 拡張で 48 件、M7a で 53 件、M7b で `ProfileGatingTests`(4 件)が加わり 57 件になったが所要時間は取り直していない。**59 件は 2026-09-10 に実測し直した値**(`unity-editmode.xml` の `testcasecount="59"`)。旧「58 件」は M7 squash(`be5615f`)の時点では正しかった——`tests/UnityProject/Assets/Tests/EditMode/*.cs` の `[Test]`/`[UnityTest]` 属性行を数えると `be5615f` で 65 件、うち Graphics カテゴリ 7 件を除いて 58 件。差の 1 件は M7c(`e1b0930`、"dnn を opt-in profile として足す")が `ProfileGatingTests.cs` に足した `TheDnnPublicApiAssemblyIsAlsoAbsentWithoutItsDefine`(同ファイルの `[Test]` 数が 5→6)で、同じ数え方で `e1b0930` は 66 件、Graphics 7 件を除いて 59 件になる——この行を更新する commit がその後に続かなかっただけである) | +| `./tools/dev.ps1 test-unity-editmode` | L4(Unity EditMode、Mono、**59 件**。`-testCategory '!Graphics'` で Graphics レーン側のテストを除外する) | 約 27 秒(増分。2026-08-28 実測。**Unity 6000.0.82f1・EditMode 10 件のときの値。** その後 M3.5 で 16 件、M4 で 33 件、M5 で 34 件、2026-09 の API 拡張で 48 件、M7a で 53 件、M7b で `ProfileGatingTests`(4 件)が加わり 57 件になったが所要時間は取り直していない。**59 件は 2026-09-10 に実測した値**(`unity-editmode.xml` の `testcasecount="59"`)。**2026-09-11 に一度 58 件へ減らして戻した** —— `CiVisibilityTests` を消したが、**削除の条件を読み違えていた**(同 class の docstring は「graphics レーンを **CI に配線できたら**」と書いており、実際に必要なのは「**merge を止めるようになったら**」だった。`Unity Graphics (Linux)` は非必須なので、`[Category("Graphics")]` を付ける行為はいまも「必須レーンから、止めないレーンへ移す」ままである)。旧「58 件」は M7 squash(`be5615f`)の時点では正しかった——`tests/UnityProject/Assets/Tests/EditMode/*.cs` の `[Test]`/`[UnityTest]` 属性行を数えると `be5615f` で 65 件、うち Graphics カテゴリ 7 件を除いて 58 件。差の 1 件は M7c(`e1b0930`、"dnn を opt-in profile として足す")が `ProfileGatingTests.cs` に足した `TheDnnPublicApiAssemblyIsAlsoAbsentWithoutItsDefine`(同ファイルの `[Test]` 数が 5→6)で、同じ数え方で `e1b0930` は 66 件、Graphics 7 件を除いて 59 件になる——この行を更新する commit がその後に続かなかっただけである) | | `./tools/dev.ps1 test-unity-player` | L5(Unity IL2CPP Player のビルドと実行、**39 件**。`-nographics` で走るので GPU に依る経路は含まない。**M7a で `RenderTextureChecks` の PlayMode 側の対(`RenderTexturePlayerTests`)を足し、35 → 39 になった**) | 約 54 秒(増分・キャッシュ温状態。2026-08-28 実測。cold 実測はまだ無く、roadmap の想定は 5〜20 分。**Unity 6000.0.82f1・10 件での値**) | -| `./tools/dev.ps1 test-unity-graphics` | **M7a。ローカル専用**。`RenderTexture` を使う GPU 依存の経路(`GraphicsTests` / `GraphicsBenchmarkRunner`)を EditMode で検証する。`-nographics` を**付けない**(`-nographics` の下では `graphicsDeviceType` が `Null` になり `RenderTexture.Create()` が true を返しつつ画素が読めなくなるため、実測)。**`ci-unity.yml` からは呼ばれない** —— このレーンが赤くても merge は止まらない。CI から見えないテストの一覧は `tests/UnityProject/Assets/Tests/EditMode/CiVisibilityTests.cs` が名指しで固定している(**一覧は同ファイルの `ExpectedGraphicsOnlyTests` が持つ** —— 本数をここに写すと、1 件足した日にこの行だけが嘘になる) | 未計測 | -| `./tools/dev.ps1 test-unity-tarball` | **UPM tarball の導入検証。** tarball だけを指した使い捨ての Unity プロジェクトを作り、そこで EditMode を走らせる。`-testCategory '!Graphics'` で `test-unity-graphics` 側のテストを除外する(`Test-UnityEditMode` と同じ `$script:UnityGraphicsExclusionCategory` を使う——2026-09-10 まで直書きのコピーで抜けており、GPU に依るテストが「スキップ」ではなく「失敗」していた)。**`-PluginSource` に他 platform の plugin 木(';' 区切り)を渡すと全部入りとして固めてから検証する** —— 渡さなければ従来どおり 1 platform 分で走り、**黙って全部入りのふりはしない** | 約 3 分(2026-08-28 実測。Library/ を持って行かないので Unity が import からやり直す。**Unity 6000.0.82f1・1 platform でのもの**)。**6 platform の全部入りは 2026-09-03 に実測** —— `release.yml` の空撃ちが作った実物の tarball を展開して材料にし、`native plugins present: 6` / `34 passed` / exit 0(**所要時間は取っていない**)。**2026-09-10 に Graphics カテゴリの除外を足した後で実測し直した** —— 除外前は `66 passed=60 failed=6`、除外後は `59 passed` で exit 0(`unity-tarball.xml` の `testcasecount`)| +| `./tools/dev.ps1 test-unity-graphics` | **M7a。**`RenderTexture` を使う GPU 依存の経路(`GraphicsTests` / `GraphicsBenchmarkRunner`)を EditMode で検証する。`-nographics` を**付けない**(`-nographics` の下では `graphicsDeviceType` が `Null` になり `RenderTexture.Create()` が true を返しつつ画素が読めなくなるため、実測)。**2026-09-11 から `ci-unity.yml` にも同じものが在る**(`Graphics` レーン)—— それまで「ローカル専用」で、このレーンが見るものは CI から完全に見えなかった。**成立の根拠は、game-ci の Editor が `-nographics` で走っていないことである**(コンテナの `unity-editor` が `xvfb-run` から起動する)。実測 run 34612557397 で 7 passed。**CI と `dev.ps1` で起動の仕方は違うが、`-testCategory` の値は機械が突き合わせる** —— `tools/tests/OpenCvConfig.Tests.ps1` が「否定形を使う行は 1 本残らず正本と一致」「肯定形はちょうど 1 本で、その補集合」を見る | 未計測 | +| `./tools/dev.ps1 test-unity-tarball` | **UPM tarball の導入検証。** tarball だけを指した使い捨ての Unity プロジェクトを作り、そこで EditMode を走らせる。**2026-09-11 から CI にも在る** —— `ci-unity.yml` の `tarball` job が `-PrepareOnly -WorkDir` でプロジェクトの準備だけをこのレーンに任せ、Unity の起動を game-ci に渡す(**作り方を CI に書き写さない**。分けると「導入を確かめた tarball」と「配る tarball」が別物になる)。`-PrepareOnly` は native をビルドし直さず(host の新しい glibc で上書きすると v0.1.0 の欠陥を再現する)、Unity も探さない(runner には無い)。`manifest.json` の `file:` 参照は**相対パス**で書く —— コンテナではワークスペースが別のパスに mount されるので絶対パスは解決できない。**ローカルも同じ形にする**(分けると CI で通る形を一度も試さないままになる)。`-testCategory '!Graphics'` で `test-unity-graphics` 側のテストを除外する(`Test-UnityEditMode` と同じ `$script:UnityGraphicsExclusionCategory` を使う——2026-09-10 まで直書きのコピーで抜けており、GPU に依るテストが「スキップ」ではなく「失敗」していた)。**`-PluginSource` に他 platform の plugin 木(';' 区切り)を渡すと全部入りとして固めてから検証する** —— 渡さなければ従来どおり 1 platform 分で走り、**黙って全部入りのふりはしない** | 約 3 分(2026-08-28 実測。Library/ を持って行かないので Unity が import からやり直す。**Unity 6000.0.82f1・1 platform でのもの**)。**6 platform の全部入りは 2026-09-03 に実測** —— `release.yml` の空撃ちが作った実物の tarball を展開して材料にし、`native plugins present: 6` / `34 passed` / exit 0(**所要時間は取っていない**)。**2026-09-10 に Graphics カテゴリの除外を足した後で実測し直した** —— 除外前は `66 passed=60 failed=6`、除外後は `59 passed` で exit 0(`unity-tarball.xml` の `testcasecount`)| | `./tools/dev.ps1 test-unity-web` | **M6。** WebGL の Player を建て、**ブラウザで走らせる**(Playwright + headless Chromium)。先に `assert-emscripten-version.ps1` が **Unity 同梱の Emscripten を対応表と突き合わせる** —— **このレーンに置くのが正しい**(WebGL の Player を建てられる時点で Unity と WebGL 支援は必ず在るので、「道具が無いから飛ばす」経路が構造的に生まれない)。**stripping は有効のまま**にする(無効にすると到達性テストが確かめたいことを確かめられない) | **所要時間は取っていない。** 通した実績はある(ローカル・CI とも `failed=0`)。**`passed` も `reachable` も数を写さない** —— `passed` は共有本体 `AbiSurfaceChecks` の検査数(`tools/run-web-e2e.ps1` が数えて**完全一致**を要求する)、`reachable` は spec の entry 数で決まる。**写すと、検査や関数を 1 つ足した日にこの行だけが嘘になる**(実際 8 -> 21 で踏んだ) | -| `./tools/dev.ps1 benchmark` | **M7a。** `test-unity-player` と `test-unity-graphics` を順に走らせ、両方の結果 XML から `OCVU_BENCH:` 行(`TestContext.WriteLine` が結果 XML の `` に入れたもの)を集めて `artifacts/benchmarks/latest.json` へ書く。**時間を判定しない** —— 落ちるのは「測れなかったとき」(0 マイクロ秒)だけで、遅い・速いでは落ちない(設計 D1)。数字の読み方は [性能](docs/performance.md) にまとめてある。**Unity がある唯一のこのマシンでは、このコマンドは現状完走しない** —— 内部で呼ぶ `test-unity-player` が後始末段階(`Stop-UnityTestPlayers` 内の `Get-CimInstance`)でハングする既知の欠陥を踏むため(詳細は `docs/roadmap.md` の M7a の判定「穴を隠さず書く」の項)。数字は、ハングしたプロセスを止めたうえで `run-benchmarks.ps1` を直接叩いて得た | 未計測(内部で呼ぶ 2 レーンの合計) | +| `./tools/dev.ps1 benchmark` | **M7a。** `test-unity-player` と `test-unity-graphics` を順に走らせ、両方の結果 XML から `OCVU_BENCH:` 行(`TestContext.WriteLine` が結果 XML の `` に入れたもの)を集めて `artifacts/benchmarks/latest.json` へ書く。**時間を判定しない** —— 落ちるのは「測れなかったとき」(0(単位は entry ごと。key が `_ns` で終わればナノ秒))だけで、遅い・速いでは落ちない(設計 D1)。数字の読み方は [性能](docs/performance.md) にまとめてある。**2026-09-11 から CI がこれを publish する** —— `ci-unity.yml` の `benchmarks` job が `Standalone` と `Graphics` の結果 XML を集めて `run-benchmarks.ps1` に渡し、`latest.json` を artifact にする(**GPU に依る側が CI で走るようになって初めて成立した** —— `GraphicsBenchmarkRunner` は `Graphics` レーンにしか居ない)。**Unity がある唯一のこのマシンでは、このコマンドは依然として完走しない** —— 内部で呼ぶ `test-unity-player` が後始末段階(`Stop-UnityTestPlayers` 内の `Get-CimInstance`)でハングする既知の欠陥を踏むため(詳細は `docs/roadmap.md` の M7a の判定「穴を隠さず書く」の項)。数字は、ハングしたプロセスを止めたうえで `run-benchmarks.ps1` を直接叩いて得た | 未計測(内部で呼ぶ 2 レーンの合計) | | `./tools/dev.ps1 clean` | `build/` を削除 | — | **`dev.ps1` のレーンは相互排他である。2 つ同時に走らせないこと。** 結果を書くレーンは開始時に `Reset-Results` で `artifacts/test-results/` を**ディレクトリごと消す**ので、後から始めたほうが先行しているほうの結果を消す。**壊れ方が悪い** —— 先行したレーンは赤くならず**無音で止まり**、Unity のレーンなら起動した Player がそのまま置き去りになる。M5 で実測した。 @@ -146,7 +146,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co | `Packages/.../Runtime/UnityIntegration/WebCamTextureConverter.cs` | `WebCamTexture` から `CvMat` を作る(M4)。**新しい C ABI 関数は使わない**(既存の上に立つ純 C#)。既定で上下を反転する —— Unity は左下原点、OpenCV は左上原点 | | `Packages/.../Runtime/UnityIntegration/RenderTextureConverter.cs` | `RenderTexture` から `CvMat` を作る(M7a)。**新しい C ABI 関数は使わない**(既存の `CopyFrom(IntPtr, long, long)` の上に立つ純 C#)。同期の `ToMat` は GPU の転送が済むまで戻らず、`RequestMat`(`AsyncGPUReadback`)は依頼だけして戻る。**`-nographics` では動かない** —— `RenderTexture.Create()` は true を返すが `ReadPixels`/readback の画素が塗った色にならない(実測)。`WebCamTextureConverter` と同じ規約で上下を反転する | | `tests/UnityProject/Assets/Tests/Shared/` | EditMode と PlayMode が**共有する**検証本体(M4)。**本体はここにしか無い** —— 写して 2 つ持つと、片方だけ直って「Editor と Player で同じ結果」を確かめられなくなる。**M5 で `AbiReachabilityChecks.g.cs` が加わった**(生成物)—— spec が載せる宣言を 1 本残らず 1 回ずつ呼ぶ。結果は見ず、**呼べたことだけ**を見る。IL2CPP の stripping が消せるのは**呼ばれない**宣言なので、これを確かめられるのは Player だけである | -| `tools/run-benchmarks.ps1` | Unity の結果 XML から `OCVU_BENCH:` 行を集めて `artifacts/benchmarks/latest.json` へ書く(M7a)。**時間の速い・遅いは判定しない** —— 時間の閾値は共有ランナーの上でフレークになるので、「測れたか」だけを見る: **XML ごとに**(M7a のレビューで直した。連結後に見ると片方のレーンの benchmark 本体が消えても気づけない)1 本以上拾えたか、そして各値が 0 マイクロ秒でないか(測定が効いていない値を publish しない)。**ログではなく結果 XML の `//output` を読む** —— このリポジトリの確立した型は `TestContext.WriteLine` で、NUnit3 がそれを結果 XML の `` ノードへ入れる | +| `tools/run-benchmarks.ps1` | Unity の結果 XML から `OCVU_BENCH:` 行を集めて `artifacts/benchmarks/latest.json` へ書く(M7a)。**時間の速い・遅いは判定しない** —— 時間の閾値は共有ランナーの上でフレークになるので、「測れたか」だけを見る: **XML ごとに**(M7a のレビューで直した。連結後に見ると片方のレーンの benchmark 本体が消えても気づけない)1 本以上拾えたか、そして各値が 0(単位は entry ごと。key が `_ns` で終わればナノ秒)でないか(測定が効いていない値を publish しない)。**ログではなく結果 XML の `//output` を読む** —— このリポジトリの確立した型は `TestContext.WriteLine` で、NUnit3 がそれを結果 XML の `` ノードへ入れる | | `tools/measure-package-size.ps1` | UPM tarball のバイト数を測り、上限(`-MaxBytes`)を超えたら落とす(M7a)。**時間と違ってこれは assert してよい** —— 同じ入力から同じ tarball ができるので CI ランナーの負荷に左右されない。`tools/tests/PackageSize.Tests.ps1` が合成した入力(fake tarball)で発火を確かめ、`release.yml` が全部入りの実物に対して呼ぶ(`-MaxBytes 104857600` = 100 MB。**実測 66 MB に対して約 1.5 倍** —— 倍増を捕まえつつ、通常の増分では発火しない余裕) | | `tools/assert-unity-results.ps1` | Unity のテスト結果 XML の判定。**ローカルと CI の両方がここを通る** —— CI では game-ci が Unity を起動するので起動の仕方は分かれるが、判定は分けない。「0 件で緑にしない」もここが持つ。**`-RequireTest` は「その 1 群だけが消えた」を捕まえる**(M3.5 の追補)—— passed が 1 以上なら緑になるので、`PluginGatingTests` が assembly から外れても残りが通れば気づけない。実測: 結果 XML から gating の 6 件を抜くと `10 passed` になり、以前はそれで緑だった。**`Passed` であることまで見る** —— NUnit は `[Ignore]` を `Skipped` として出力し `failed` に数えないので、存在だけを見ると `[Ignore]` 1 行で満たせる(実測)。**`-RequireOutput` はさらに「どの分岐を通ったか」を見る** —— テストが通っても、合図が届かなければ弱い側の分岐を通っただけで、出力は成功時と 1 バイトも違わない。入力(合図を書く step が在ること)をいくら検査しても届いたことの証明にはならないので、テスト自身が出した事実(`native plugins present: 6`)を要求する。**数は写さず `tools/dev.ps1` の `$AllPlatformBinaries` から導く**(M6 のレビューで「検査が古い数を機械で強制していた」ことが分かったため) | | `tools/pack-upm-tarball.ps1` | UPM tarball を作る唯一の入口。`release.yml` と `dev.ps1 test-unity-tarball` の**両方**がここを通る(作り方が分かれると、導入を確かめた tarball と実際に配る tarball が別物になる)。中身は npm と同じく `package/` の下に入れる — package ID のディレクトリごと固めると UPM が 展開後の root に `package.json` を見つけられず導入に失敗する(M3 で実測)。**M3.5 で `-AllPlatforms` が加わり、これが配る正になった** —— 全 platform 分の binary を 1 つに束ね(**数を写さない。正本は `tools/dev.ps1` の `$AllPlatformBinaries` である**)、名前から版番号を落とす(`com.ayutaz.opencv-unity-native.tgz`)。**存在検査だけにしない**: 全部入りは中身の一覧そのものが契約なので、**全 platform 分が揃っている**ことに加えて**予期しない binary が 1 つでもあれば止め**、固めた後の archive の中を数え直す。`-MaxBytes`(既定 512 MB)を**引数にしてあるのは、既定のままではこの検査が働くところを誰も確かめられないから**である(現在の配布物は上限に対して 1 桁以上小さい) | @@ -167,7 +167,6 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co | `tests/Managed/CvUnity.Tests.Managed/` | L3 の xUnit テスト(net8.0)。`HarnessProbeTests.cs` がクラッシュ・ハングプローブを持つ。**同じ solution に `bindings/generator/Ocvu.Generator.Tests` が同居する**ので、`dev.ps1 test-managed` は両方を回す(**件数はその行にある**) —— 結果 XML は `managed-{assembly}.xml` で assembly ごとに分かれる(固定名だと後勝ちで上書きされ、先に走ったほうの失敗が読めなくなる。M5 で実測) | | `tests/UnityProject/` | L4(EditMode)と L5(IL2CPP Player)用の最小 Unity プロジェクト。UPM パッケージは `manifest.json` から `file:../../../Packages/...` でローカル参照する。**この参照はディレクトリであって tarball ではない**ので、tarball の中身が壊れていてもこのレーンは通る——**tarball からの導入は `dev.ps1 test-unity-tarball` が別に見る**(使い捨てのプロジェクトに tarball だけを入れて EditMode を走らせ、16/16 pass を実測済み。M3 でこのレーンを足して初めて「導入できない tarball」が見つかった。**M3.5 で `-PluginSource` が加わり、他 platform の plugin 木を重ねた全部入りも検証できる** —— このマシンでビルドできるのは 1 platform 分だけなので、他 platform は公開済み release から取ってくる)。**Git URL からの導入はこの構成では成立しない**(binary が git の追跡外にあるため) | | `tests/UnityProject/Assets/Tests/EditMode/PluginGatingTests.cs` | 全部入りで **Unity が自分の platform の plugin だけを有効にしていること**を、`PluginImporter` に問う(M3.5)。`PackageRelease.Tests.ps1` の `.meta` 検査との違いは、YAML を自分で読むか **Unity に読ませた結果を読むか**。**着手前の実測: macOS の `.meta` を「Windows でも有効」に壊しても、既存の EditMode は 10 passed のまま通った**(3 platform で binary のファイル名が違うので `DllImport` の解決が名前の時点で分岐し、壊れた gating に触れない)。同じ壊し方で今は 16 中 3 件が落ちる。**`GetCompatibleWithEditor()` は 3 つとも true を返す** —— `.meta` は Editor を 3 つとも有効にし、振り分けは `GetEditorData("OS")` が持つ。**Editor の可否だけを見る検査は常に true で、何も見ていない**。**「全部揃っていること」を要求するのは合図が置かれたときだけ**で、合図は**環境変数ではなくプロジェクト直下のファイル**(`ocvu-expect-all-platforms`)である —— game-ci がコンテナへ渡す環境変数は 34 個の固定一覧で、任意の名前は届かない(`@v4` の dist で実測)。届かなければこの検査は「合図が無い」分岐に落ちて**要素 1 個でも緑になる** | -| `tests/UnityProject/Assets/Tests/EditMode/CiVisibilityTests.cs` | **CI で走らないテストの一覧を名指しで固定する**(M7a)。`test-unity-graphics`(GPU に依る `RenderTexture` の経路)は `ci-unity.yml` からは呼ばれないので、`[Category("Graphics")]` を付けたテストは CI から完全に見えなくなる。**列挙そのものが契約である** —— このリポジトリは一般に列挙を嫌うが(`check-generated-file-edit.sh` の設計)、ここは例外で、一覧は「CI が見ていないもの」であり短いほうがよく、伸びたら人が気づくべきものだから列挙で持つ。**`Graphics` と `!Graphics` は補集合なので実行時の和集合は常に完全**であり、穴は「テストを見落とすこと」ではなく「一方のレーンが CI に無いこと」である。**graphics レーンを CI に配線できたら、この一覧は空にでき、そのとき検査ごと消してよい** | | `.github/workflows/` | 下の表を参照。9 本ある | | `.github/codeql/codeql-config.yml` | CodeQL の解析範囲と `query-filters`。**触る前に、機械が捕まえられない罠が 2 つあることを知っておくこと。** (1) **filter の順序が既定を決める** — 先頭が `exclude` のときだけ「他は全部含める」が既定になる。先頭に `include:` を足すと既定が反転し、**解析がその 1 規則だけに縮む**(書いた覚えのない規則までまとめて消える)。(2) **`exclude` を `excludes` と綴り間違えても CI は緑のまま**で、外したはずの指摘だけが黙って戻ってくる —— codeql-action が持つ設定の JSON schema は `query-filters` の要素に「何でも通る」分岐を持つため、綴り間違いも 1 要素に exclude と include を両方書く形も schema 検証を通る(実測。schema を取ってきて手元で当てた)。**このうち機械が守るのは (1) と「除外する id の集合」だけ**である(`tools/tests/OpenCvConfig.Tests.ps1` が、全要素が `exclude` であること・先頭が `exclude` であること・id がちょうど `cs/unmanaged-code` と `cs/call-to-unmanaged-code` の 2 つであることを見る)。**守られないのは「その id が今も CodeQL に実在するか」**——上流で規則が改名されれば、綴りとして正しいまま何も外さなくなり、誰も赤くならない | | `.github/dependabot.yml` | `actions/*` の可変タグと `tests/Managed` の NuGet を週 1 で追う。**「いつの間にか変わっていた」を「差分として見える変更」に変える**のが目的で、上流が壊れた版を出したときにこちらは何も変えていないのに壊れる、という経路を塞ぐ。M3 後に追加。**bot の PR はそのままでは merge できない** —— GitHub は Dependabot が起こした実行にリポジトリの secret を渡さないので、必須チェックである Unity の 2 レーンが `Missing Unity License File` で必ず落ちる(実測: PR #49 は他の 25 チェックが全部緑で、この 2 本だけが赤かった)。**だから扱いは「bot の差分を人が出す PR に取り込み、bot の PR は閉じる」である** —— #18〜#26 の 9 件がその形で閉じてある。**`docker://` で始まる `uses:` は Dependabot が構造的に見ない**(parser が読み飛ばす)ので、`ci-lint.yml` の actionlint だけは手で上げる。理由と手順はこのファイルの冒頭コメントにある | @@ -180,7 +179,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co | `ci-native.yml` | push(main) / PR / 手動 | `dev.ps1 test` を 3 platform で(**L0 + L1 + L3**。M5 以降、生成物と spec の一致検査もここに乗る)。**desktop 3 job とも `test-tools-slow` を配線してある。**クロスの 3 job(Android / iOS / **Web**)はビルドのみ | **3 platform とも green**(M3 の PR #8 以降) | | `ci-sanitizers.yml` | push(main) / PR / 手動 | L2。Windows は ASan、Linux は ASan+LSan(リーク検出はこのレーンだけが担う) | green | | `build-opencv.yml` | 手動 + 構成ファイルの変更 | allowlist 構成の OpenCV を**全 platform**でビルドし artifact 公開(**数を写さない。正本は `tools/opencv-config.psd1` の `Toolchains`**) | 稼働中 | -| `ci-unity.yml` | push(main) / PR / 手動 | L4(EditMode)+ L5(IL2CPP Player)。**Unity は ubuntu で走る**が、**2026-08-30 から他 platform の plugin もビルドし、Unity job がそれを重ねてから走る**(**現在は** `windows-2022` / `macos-14` を含む plugin の 5 job で、全部入りの gating を**要素 6 個**の集合に当てる。**M4 の直後は 4 job / 要素 5 個で、M6 で 1 つずつ増えた**)。**M6 で 3 つ目の job `Web browser E2E` が加わった** —— WebGL の Player を建て、**Linux の headless Chromium で実際に走らせる**(Unity Test Framework は WebGL の Player を batchmode から走らせられないので、Player 自身に検証本体を走らせて結果をブラウザのコンソールへ出し、それを外から読む)(Windows で走らせない理由として記録していたものは 2026-08-29 に崩れた。上記参照)。したがって CI の L5 は Linux の IL2CPP Player で、Windows 版はローカルのレーンが担う。Unity の導入とアクティベーションは game-ci、合否の判定は `tools/assert-unity-results.ps1`(ローカルと共通)。Unity の版は `ProjectVersion.txt` から取るので、載せ替えても workflow は変えなくてよい。**`game-ci/unity-test-runner` は `v5.0.0-beta.1` の commit を指している**(2026-09-01。タグではなく SHA —— pre-release のタグは上流が消せる)—— 安定版はどれも `node20` を宣言しており、**この repo で node20 を宣言する action はこれだけだった**。`action.yml` の差は `node20` -> `node24` の 1 行だけで、入力 24 個・出力 2 個は名前も既定値も同一である(実測)。**ただしそれはインタフェースの話で、挙動の証明ではない** —— 中身の `dist/index.js` は実装が game-ci/cli へ移ったことで大きく入れ替わっている。**pre-release を必須チェックに入れている**ことは自覚しておくこと(上流は「main CI ではなく別 workflow で試すのが安全」と書いている)。安定版が出たら差し替える。**beta に切り替えたのは 2026-09-01(PR #57)で、それ以降の green は beta のものである** —— M6 の PR #63(2026-09-03)、API 拡張の PR #69(2026-09-05)、そして M7 の PR #70 / #72 / #73(2026-09-06〜10)がそれに当たる。**この列挙は網羅を主張しているので、PR を 1 本通すたびに伸ばすか、やめるかのどちらかである** ——「#69 で終わっている」状態が実際に起きた。**それより前の green(M3.5 / M4 / M5 の 4 件)は `@v4` のものである** | **green**(2026-08-29 に初めて。main の先端 `68fbdae` でも green)。**ただしM3.5 で Unity を 6000.3.16f1 へ、EditMode を 16 件へ替えた後も green**(run の詳細は `docs/roadmap.md` の M3.5 節。**同じ事実を 2 箇所に書かない**)。**2026-08-30 に 3 platform 構成でも green**(PR #37、run 33290375806。`native plugins present: 3` と gating 4 件の個別 Passed を CI が出した)。**M4 で 5 platform 構成になった後も green**(PR #41。`native plugins present: 5`、EditMode 33 passed / Standalone 18 passed)。**M5 で到達性テストが 1 件加わった後も green**(PR #55、2026-09-01。`==> [EditMode] 34 passed` / `==> [Standalone] 19 passed` で**ローカルの実測と一致**。Standalone は stripping 済みの実物で、生成した P/Invoke が全部解決した)。**M6 で Web が加わった後も green**(PR #63、2026-09-03。`native plugins present: 6` と gating 4 件、そして新しい `Web browser E2E` が `passed=8 failed=0 reachable=28`。**この時点では EditMode / Standalone の件数は M5 から変わっていなかった** —— M6 は platform を足したのであって検証本体を足したのではない)。**2026-09 の API 拡張で 26 本を足した後も green**(PR #69、2026-09-05、run 33945388389。`==> [EditMode] 48 passed` / `==> [Standalone] 33 passed` / `native plugins present: 6` / `Web browser E2E` が `passed=21 failed=0 reachable=54`。**3 レーンとも件数が動いた最初の変更である** —— 公開 API を Unity の中で動かす検査を共有本体に 13 件足したので、**手書きで配線する EditMode / PlayMode と、リフレクションで拾う Web が同時に増えた**)| +| `ci-unity.yml` | push(main) / PR / 手動 | L4(EditMode)+ L5(IL2CPP Player)。**Unity は ubuntu で走る**が、**2026-08-30 から他 platform の plugin もビルドし、Unity job がそれを重ねてから走る**(**現在は** `windows-2022` / `macos-14` を含む plugin の 5 job で、全部入りの gating を**要素 6 個**の集合に当てる。**M4 の直後は 4 job / 要素 5 個で、M6 で 1 つずつ増えた**)。**2026-09-11 に Unity のレーンが 2 本から 5 本になり、job も 2 つ増えた** —— matrix に `Graphics`(`-testCategory Graphics`。GPU に依る経路。**game-ci の Editor が `-nographics` で走っていないので成立する**)、`DnnEditMode` / `DnnStandalone`(`ProjectSettings.asset` に `OCVU_PROFILE_DNN` を書いてから走らせる)が加わり、job としては `tarball`(配る全部入りを使い捨てプロジェクトへ導入する)と `benchmarks`(`Standalone` と `Graphics` の結果から `latest.json` を publish する)が加わった。**M6 で 3 つ目の job `Web browser E2E` が加わった** —— WebGL の Player を建て、**Linux の headless Chromium で実際に走らせる**(Unity Test Framework は WebGL の Player を batchmode から走らせられないので、Player 自身に検証本体を走らせて結果をブラウザのコンソールへ出し、それを外から読む)(Windows で走らせない理由として記録していたものは 2026-08-29 に崩れた。上記参照)。したがって CI の L5 は Linux の IL2CPP Player で、Windows 版はローカルのレーンが担う。Unity の導入とアクティベーションは game-ci、合否の判定は `tools/assert-unity-results.ps1`(ローカルと共通)。Unity の版は `ProjectVersion.txt` から取るので、載せ替えても workflow は変えなくてよい。**`game-ci/unity-test-runner` は `v5.0.0-beta.1` の commit を指している**(2026-09-01。タグではなく SHA —— pre-release のタグは上流が消せる)—— 安定版はどれも `node20` を宣言しており、**この repo で node20 を宣言する action はこれだけだった**。`action.yml` の差は `node20` -> `node24` の 1 行だけで、入力 24 個・出力 2 個は名前も既定値も同一である(実測)。**ただしそれはインタフェースの話で、挙動の証明ではない** —— 中身の `dist/index.js` は実装が game-ci/cli へ移ったことで大きく入れ替わっている。**pre-release を必須チェックに入れている**ことは自覚しておくこと(上流は「main CI ではなく別 workflow で試すのが安全」と書いている)。安定版が出たら差し替える。**beta に切り替えたのは 2026-09-01(PR #57)で、それ以降の green は beta のものである** —— M6 の PR #63(2026-09-03)以降に merge された PR がすべてそれに当たる。**以前ここには PR 番号を列挙していたが、やめた** —— 「この列挙は網羅を主張しているので、PR を 1 本通すたびに伸ばすか、やめるかのどちらかである」と自分で書きながら、**実際に 2 度置いていかれた**(#69 で止まり、直した後また #73 で止まった)。**伸ばし続けられなかったので、やめた側を選んだ。****それより前の green(M3.5 / M4 / M5 の 4 件)は `@v4` のものである** | **green**(2026-08-29 に初めて。main の先端 `68fbdae` でも green)。**ただしM3.5 で Unity を 6000.3.16f1 へ、EditMode を 16 件へ替えた後も green**(run の詳細は `docs/roadmap.md` の M3.5 節。**同じ事実を 2 箇所に書かない**)。**2026-08-30 に 3 platform 構成でも green**(PR #37、run 33290375806。`native plugins present: 3` と gating 4 件の個別 Passed を CI が出した)。**M4 で 5 platform 構成になった後も green**(PR #41。`native plugins present: 5`、EditMode 33 passed / Standalone 18 passed)。**M5 で到達性テストが 1 件加わった後も green**(PR #55、2026-09-01。`==> [EditMode] 34 passed` / `==> [Standalone] 19 passed` で**ローカルの実測と一致**。Standalone は stripping 済みの実物で、生成した P/Invoke が全部解決した)。**M6 で Web が加わった後も green**(PR #63、2026-09-03。`native plugins present: 6` と gating 4 件、そして新しい `Web browser E2E` が `passed=8 failed=0 reachable=28`。**この時点では EditMode / Standalone の件数は M5 から変わっていなかった** —— M6 は platform を足したのであって検証本体を足したのではない)。**2026-09 の API 拡張で 26 本を足した後も green**(PR #69、2026-09-05、run 33945388389。`==> [EditMode] 48 passed` / `==> [Standalone] 33 passed` / `native plugins present: 6` / `Web browser E2E` が `passed=21 failed=0 reachable=54`。**3 レーンとも件数が動いた最初の変更である** —— 公開 API を Unity の中で動かす検査を共有本体に 13 件足したので、**手書きで配線する EditMode / PlayMode と、リフレクションで拾う Web が同時に増えた**)| | `ci-lint.yml` | push(main) / PR / 手動 | 4 job: actionlint(workflow の構文・式・`run:` の中の shell。**版は `1.7.12` に固定してある** —— `:latest` だと上流が規則を足した日に、差分が無いまま必須チェックが赤くなる)/ shellcheck(hook と CI のスクリプト)/ PSScriptAnalyzer(`tools/` の PowerShell、Error のみ)/ 文書の相対リンク検査(コードブロックの中は見ない。0 件なら「壊れていない」ではなく走査が効いていないとして落とす)。**静的に読めば分かる誤りを CI 1 周(10〜20 分)かけて確かめていた**のを埋める。数分で終わるので重いレーンより先に落ちる | green | | `codeql.yml` | push(main) / PR / 週 1 / 手動 | C++ と C#。C++ は**配布する `opencv_unity_native` だけ**をビルドする——`paths-ignore` は C++ に効かず(解析対象は「実際にコンパイルされたもの」)、テストごとビルドすると FetchContent の GoogleTest の指摘が混ざるため。C# は P/Invoke していること自体への 2 規則を `query-filters` で外す(**設計どおりの指摘に本物が埋もれる**ため。実測で open 107 件中 84 件がこれで、その陰に本物が 4 件あった)。OpenCV の cache は `if: matrix.language == 'c-cpp'` で c-cpp の leg に限る——csharp の leg は OpenCV を一度も開かないので、揃えないと cache hit 時に使わない木を落として展開し、miss 時は post step が `Path Validation Error` を出して何も保存しない(後者は「なぜ cache が温まらないのか」を調べる人に偽の手がかりを渡す) | green。埋もれていた本物 4 件の現状は下記 | | `nightly.yml` | 毎日 04:00 UTC / 手動 | 誰も push していない間に壊れることを見つける。**3 job 定義**(速いレーンの job は `lanes` という 2 runner の matrix なので、**実行時は 4 件**になる): Linux 成果物の移植性(`ubuntu:22.04` でビルドし直して GLIBC の要求を見る)/ Windows・macOS の速いレーン(`dev.ps1 test`。結果を `if: always()` で artifact 化する。名前は `matrix.runner` で分ける——`matrix.name` は空白を含み、分けないと 2 つの job が同じ artifact 名を取り合う)/ OpenCV artifact の期限切れ確認 | **2026-08-29 から schedule で毎日走っている。** 内訳(実行回数・成否)は `docs/roadmap.md` のワークフローの節が持つ —— **ここに写さない**(毎日増える数である)。**「schedule でまだ 1 度も走っていない」と書いてあったのは逆向きの陳腐化で、毎日動いている検査を存在しないことにしていた** —— 穴が埋まっても誰も赤くならないので、この向きの誤りは自然には見つからない | @@ -413,8 +412,8 @@ CI が赤くて直した場合、その修正差分にもレビューが要る | 設定 | 値 | 意味 | | --- | --- | --- | | main の branch protection | 有効 | 直接 push は `GH006` で拒否される | -| 必須チェック | **24 本。** `ci-native` の 3 job(`Windows x64 (L1 + L3)` / `macOS arm64 (L1 + L3)` / `Linux x64 (L1 + L3)`)、`ci-sanitizers` の 2 job(`Windows x64 AddressSanitizer (L2)` / `Linux x64 ASan+LSan (L2)`)、`ci-lint` の 4 job(`Workflows (actionlint)` / `Shell scripts (shellcheck)` / `PowerShell (PSScriptAnalyzer)` / `Documentation links`)、`codeql` の 2 job(`Analyze c-cpp` / `Analyze csharp`)、`ci-unity` の 2 job(`Unity EditMode (Linux)` / `Unity Standalone (Linux)`)、**`release` の 6 job(`Package windows-x64` / `macos-arm64` / `linux-x64` / `android-arm64` / `ios-arm64` と `Assemble the release assets`)、`ci-native` のモバイル 2 job(`Android arm64-v8a (cross-build)` / `iOS arm64 (cross-build)`)、M6 の Web 3 job(`ci-native` の `Web wasm32 (cross-build)` / `ci-unity` の `Web browser E2E` / `release` の `Package web-wasm`)**。2 本 → 5 本(M3 完了時)→ 13 本(2026-08-29、PR #30 が 13 本すべてを緑にしたのを見てから拡張)→ **21 本(2026-08-31。`release.yml` を PR でも走らせるようにして 6 本、モバイルのクロスビルド 2 本を足した。どちらも main と PR の両方で安定して緑になったのを見てから拡張した)** → **24 本(2026-09-10。M6 が足した Web の 3 本を昇格した —— #63 以降に merge された 11 本の PR(#63〜#70 と #72〜#74)すべてで 3 本とも success になり、「緑の実績が浅い」という当初の理由が失効したため)**。2026-09-10 に API で実測し、この表と一致 | 全部 pass しないと merge できない | -| 必須でないもの | **9 本**(PR #74 の 33 checks から必須 24 本を引いて実測)。**`ci-unity` の `Plugin ` 5 本、`release` の `Publish the release` の 1 本、`codeql` の集約 check `CodeQL` の 1 本(言語ごとの `Analyze c-cpp` / `Analyze csharp` を個別に必須にしてあるので、集約を足しても止める力は増えない)、`ci-unity` の `EditMode results` / `Standalone results` の 2 本(PR では skip される)。** **M6 が足した Web の 3 本(`ci-native` の `Web wasm32 (cross-build)` / `ci-unity` の `Web browser E2E` / `release` の `Package web-wasm`)は 2026-09-10 に必須へ昇格した** —— モバイルの 2 本と同じ手順である(根拠は上の行に 1 度だけ書いてある)。**したがって Web のレーンは、いまは赤ければ merge を止める。****`Plugin` が 4 本から 5 本になった**のは web-wasm が加わったためである。 `Plugin` は落ちると `unity` job が `if: !cancelled()` で走って材料が無いまま赤くなるので、必須チェックである Unity の 2 レーンがそのまま止める(**skip は required check を通す**ので `needs:` だけでは止まらない —— そこは workflow の構造で塞いである)。**モバイルの 2 本は 2026-08-31 に必須へ昇格した** —— 当初の理由(この構成の CI がまだ 1 度も緑になっていない)が失効し、M4 以降 PR と main で安定して緑になったためである。**`Publish the release` は必須にしない** —— PR では常に skip される job で、**GitHub は skip を合格として通す**ので、必須にしても何も止めない。「見ているが止めない」の典型である。`build-opencv` / `nightly` は PR では起動しない(構成変更・定期実行が trigger)ので、そもそも必須にできない | — | +| 必須チェック | **29 本。** `ci-native` の 3 job(`Windows x64 (L1 + L3)` / `macOS arm64 (L1 + L3)` / `Linux x64 (L1 + L3)`)、`ci-sanitizers` の 2 job(`Windows x64 AddressSanitizer (L2)` / `Linux x64 ASan+LSan (L2)`)、`ci-lint` の 4 job(`Workflows (actionlint)` / `Shell scripts (shellcheck)` / `PowerShell (PSScriptAnalyzer)` / `Documentation links`)、`codeql` の 2 job(`Analyze c-cpp` / `Analyze csharp`)、`ci-unity` の 2 job(`Unity EditMode (Linux)` / `Unity Standalone (Linux)`)、**`release` の 6 job(`Package windows-x64` / `macos-arm64` / `linux-x64` / `android-arm64` / `ios-arm64` と `Assemble the release assets`)、`ci-native` のモバイル 2 job(`Android arm64-v8a (cross-build)` / `iOS arm64 (cross-build)`)、M6 の Web 3 job(`ci-native` の `Web wasm32 (cross-build)` / `ci-unity` の `Web browser E2E` / `release` の `Package web-wasm`)**。2 本 → 5 本(M3 完了時)→ 13 本(2026-08-29、PR #30 が 13 本すべてを緑にしたのを見てから拡張)→ **21 本(2026-08-31。`release.yml` を PR でも走らせるようにして 6 本、モバイルのクロスビルド 2 本を足した。どちらも main と PR の両方で安定して緑になったのを見てから拡張した)** → **29 本(2026-09-11。`ci-unity` に足した 5 本 —— `Unity Graphics (Linux)` / `Unity DnnEditMode (Linux)` / `Unity DnnStandalone (Linux)` / `UPM tarball install (Linux)` / `Publish the benchmarks` —— を昇格した。**これまでの拡張と違い、緑の実績を積む前に所有者の判断で昇格させた**:実績は 1 ブランチ上で多くても 3 run である。**昇格にあわせて `benchmarks` job に `if: !cancelled()` を足した** —— 素の `needs:` だと依存が落ちたとき skip になり、**GitHub は skip を合格として通す**)→ **その前は 24 本(2026-09-10。M6 が足した Web の 3 本を昇格した —— #63 以降に merge された 11 本の PR(#63〜#70 と #72〜#74)すべてで 3 本とも success になり、「緑の実績が浅い」という当初の理由が失効したため)**。2026-09-10 に API で実測し、この表と一致 | 全部 pass しないと merge できない | +| 必須でないもの | **13 本。** `ci-unity` の `Plugin ` 5 本、`release` の `Publish the release` 1 本、`codeql` の集約 check `CodeQL` 1 本(言語ごとの `Analyze c-cpp` / `Analyze csharp` を個別に必須にしてあるので、集約を足しても止める力は増えない)、そして **game-ci が `checkName` から作る check run 6 本**(`EditMode results` / `Standalone results` / `Graphics results` / `DnnEditMode results` / `DnnStandalone results` / `Tarball results`)。**後者を必須にしないのは、PR では `neutral` になり GitHub がそれを合格として通すからである** —— 必須にしても止める力は増えない。**測る時刻に注意する** —— game-ci の `* results` は job の終盤に作られるので、**走っている途中に数えると出てこない**(この行は 1 度 12 本と書いて直した。42 件出揃う前の 36 件の時点で数えていた)。**`ci-unity` の `Plugin ` 5 本、`release` の `Publish the release` の 1 本、`codeql` の集約 check `CodeQL` の 1 本(言語ごとの `Analyze c-cpp` / `Analyze csharp` を個別に必須にしてあるので、集約を足しても止める力は増えない)、`ci-unity` の `EditMode results` / `Standalone results` の 2 本(PR では skip される)。** **M6 が足した Web の 3 本(`ci-native` の `Web wasm32 (cross-build)` / `ci-unity` の `Web browser E2E` / `release` の `Package web-wasm`)は 2026-09-10 に必須へ昇格した** —— モバイルの 2 本と同じ手順である(根拠は上の行に 1 度だけ書いてある)。**したがって Web のレーンは、いまは赤ければ merge を止める。****`Plugin` が 4 本から 5 本になった**のは web-wasm が加わったためである。 `Plugin` は落ちると `unity` job が `if: !cancelled()` で走って材料が無いまま赤くなるので、必須チェックである Unity の 2 レーンがそのまま止める(**skip は required check を通す**ので `needs:` だけでは止まらない —— そこは workflow の構造で塞いである)。**モバイルの 2 本は 2026-08-31 に必須へ昇格した** —— 当初の理由(この構成の CI がまだ 1 度も緑になっていない)が失効し、M4 以降 PR と main で安定して緑になったためである。**`Publish the release` は必須にしない** —— PR では常に skip される job で、**GitHub は skip を合格として通す**ので、必須にしても何も止めない。「見ているが止めない」の典型である。`build-opencv` / `nightly` は PR では起動しない(構成変更・定期実行が trigger)ので、そもそも必須にできない | — | | strict | 有効 | ブランチが main より古いと merge できない(`allow_update_branch` で自動更新可) | | 必須レビュー数 | **0** | PR は必須だが人間の承認は不要 | | enforce_admins | **有効** | 管理者も例外ではない。抜け道は無い | @@ -475,21 +474,16 @@ CodeQL まで見る。それでも次は緑のまま通過する。 - **必須でないレーンが赤いこと。** どのレーンがそれに当たるかはここには 書かない —— 上の「機構として強制されていること」の表が唯一の記載場所で、 正本はさらにその先の GitHub 側の設定である -- **`OCVU_PROFILE_DNN` を立てた Unity。** **どの workflow も、どの `dev.ps1` の - レーンも、この define を立てない**(2026-09-10 に `.github/` と `tools/` を - grep して 0 件)。したがって **`CvUnity.Interop.Dnn` / `CvUnity.Dnn` が Unity の - 中で実際にコンパイルされ、IL2CPP の stripping が dnn の P/Invoke 宣言を - 消さないことは、人が手で 1 回確かめたきりである** —— 次に dnn の ABI が - 変わっても自動では再検証されない(M7c)。**「dnn は CI が見ていない」と - 丸めないこと。** CI が実際に見ているものは 4 つある: L1 の GoogleTest、 - L3(`DnnInferenceTests` が実物の DLL を P/Invoke で叩く)、netstandard2.1 の - shim が `Runtime/Interop.Dnn` と `Runtime/Dnn` を**常に**コンパイルすること - (define を見ない。`CvUnity.Runtime.Shim.csproj` にそう書いてある)、そして - `tools/tests/NonStandardProfileCompile.Tests.ps1` が非既定 profile の生成物を - `dotnet build` に通すこと(`test-tools-slow` → `ci-native` の desktop 3 job。 - **ただし通しているのは合成した `dnnprobe` spec の出力であって、実物の - `NativeMethods.Dnn.g.cs` ではない**)。**穴は「Unity と IL2CPP の側」に - 限られており、そこは丸ごと空いている。** +- ~~**`OCVU_PROFILE_DNN` を立てた Unity。**~~ **2026-09-11 に閉じた。** + `ci-unity.yml` の `DnnEditMode` / `DnnStandalone` レーンが + `ProjectSettings.asset` に define を書いてから Unity を走らせる。実測 + (run 34612557397): EditMode 59 passed で + `OCVU_PROFILE_DNN is defined and the dnn assemblies are compiled` が + 3 つの assembly 分出力され、**stripping 済みの IL2CPP Player で 40 passed** + (`AbiSurfaceDnnPlayerTests.EveryDnnEntryPointIsReachable` を含む)。 + **穴だった「Unity と IL2CPP の側」が、これで両方とも埋まった。** + それ以前から CI が見ていたもの(L1 / L3 / shim / 非既定 profile の + `dotnet build`)は変わらず走る - **macOS 上の Unity の挙動**(CI の macOS job は plugin をビルドするが Unity を 起動しない)。**2026-08-31 に 4 回試して「CI では埋まらない」と確定した** —— game-ci は macOS を支えず(`darwin-platform is not supported`)、Unity Hub で @@ -506,9 +500,13 @@ CodeQL まで見る。それでも次は緑のまま通過する。 他人の issue ではない。** 詳細は `docs/roadmap.md`「担当が無かった制約」。 **したがってこれは「まだ調べていない穴」ではなく、意図して CI の外に置いた ものである** —— Windows 固有の IL2CPP の欠陥だけが CI に映らない -- **全部入りの package。** 3 platform を 1 つに束ねる形自体は M3.5 で成立したが、 - **それを確かめるレーン(`dev.ps1 test-unity-tarball`)はどの workflow からも - 走っていない。** このレーンの実測はローカル(Windows)だけである(**6 platform の全部入りでは 2 回走らせてある** —— 2026-09-03 に `release.yml` の空撃ちが作った tarball を材料に、2026-09-04 に公開する下書きの asset そのものを材料に。どちらも `native plugins present: 6` / exit 0(**件数を写さない** —— 正本は上の `test-unity-tarball` の行で、そちらは 2026-09-10 に取り直してある。当時の値をここに残すと、同じファイルの中に 2 つの件数が並ぶ)。詳細は `docs/roadmap.md` の「配布 その 5」) +- ~~**全部入りの package。**~~ **2026-09-11 に閉じた。** `ci-unity.yml` の + `tarball` job が、6 platform を重ねて固めた tarball を使い捨ての Unity + プロジェクトへ導入し、EditMode を走らせる。プロジェクトの作り方は + `dev.ps1 test-unity-tarball -PrepareOnly` に寄せてあり(**CI に書き写さない** + —— 分けると「導入を確かめた tarball」と「配る tarball」が別物になる)、 + Unity の起動だけが game-ci である。UPM が **`.tgz` で解決したこと**まで + CI が見る。**閉じる前の実測はローカル(Windows)だけだった**(**6 platform の全部入りでは 2 回走らせてある** —— 2026-09-03 に `release.yml` の空撃ちが作った tarball を材料に、2026-09-04 に公開する下書きの asset そのものを材料に。どちらも `native plugins present: 6` / exit 0(**件数を写さない** —— 正本は上の `test-unity-tarball` の行で、そちらは 2026-09-10 に取り直してある。当時の値をここに残すと、同じファイルの中に 2 つの件数が並ぶ)。詳細は `docs/roadmap.md` の「配布 その 5」) - **mobile 実機の挙動。** Android / iOS は CI がビルドし、全部入りにも入るが、 **実機で動かした実績が 1 度も無い**(M4 の完了条件 3 件が閉じていない。 実機が要る 2 件は `docs/m4-device-verification.md` の手順書に落としてある) @@ -517,21 +515,26 @@ CodeQL まで見る。それでも次は緑のまま通過する。 —— 他のブラウザ・実機・モバイルのブラウザの挙動は見ていない - **`dev.ps1 test-unity-web` は、どの workflow からも走らない。** CI の `web-e2e` job は Player を自分で建て、`assert-emscripten-version.ps1` と - `run-web-e2e.ps1` を直接呼ぶ。**上の `test-unity-tarball` と同じ形である** -- **`RenderTexture` を使う公開 API の GPU 経路(M7a)。** - `RenderTextureConverter.ToMat` / `RequestMat` が**実際に画素を運ぶ経路**は - **CI で 1 度も実行されない** —— 検証は `test-unity-graphics` が - 持つが、これはローカル専用で `ci-unity.yml` からは呼ばれず、 - `test-unity-player` / `ci-unity` の 2 レーンは `-nographics` で走るので - この経路を通れない。**引数の検証だけは CI が通る** —— - `RenderTextureChecks.ToMatRejectsNull` が `ToMat(null)` を EditMode と - Standalone の両レーンで呼んでいる。**通っていないのは GPU が要る側である。** - **前 2 者(tarball / web)は「別の経路で同じものを - 見ている」レーンだが、こちらは配る package に入る公開 API の中身が - どの経路からも見られていない**という点で 1 段重い。`[Category("Graphics")]` を - 付けたテストの一覧は - `tests/UnityProject/Assets/Tests/EditMode/CiVisibilityTests.cs` が固定している。 - **`dev.ps1 benchmark` も同じくどの workflow からも走らない** + `run-web-e2e.ps1` を直接呼ぶ。**2026-09-11 に、これが唯一の「直接呼ぶ」形に + なった** —— それまで並んでいた `test-unity-tarball` は、CI が + `dev.ps1 test-unity-tarball -PrepareOnly` を**実際に通る**形へ寄せたためである + (プロジェクトの作り方が 1 つになり、写しが消えた)。web も同じ形にできるかは + 見ていない +- **`RenderTexture` を使う公開 API の GPU 経路(M7a)のうち、Player 側。** + **Editor 側は 2026-09-11 に閉じた** —— `ci-unity.yml` の `Graphics` レーンが + `-testCategory Graphics` で走り、`ToMat` / `RequestMat` が実際に画素を運ぶ + 経路を CI が実行する(実測 run 34612557397: 7 passed。 + `AGraphicsDeviceIsPresent` が通ったので**コンテナには実物の + graphics device が在り**、`AsyncMatchesSync` も通ったので + `AsyncGPUReadback` も動く)。**成立の根拠は、game-ci の Editor が + `-nographics` で走っていないことである** —— コンテナの `unity-editor` は + `xvfb-run -ae /dev/stdout "$UNITY_PATH/Editor/Unity" -batchmode "$@"` で、 + 仮想 X の下から起動する。**「CI では原理的に無理」と書いてあったが、 + 誰も測っていなかった。** + **残っているのは Player(IL2CPP)側である** —— game-ci の `run_tests.sh` は + Standalone Player を `xvfb-run ... -batchmode -nographics` で起動しており、 + **`-nographics` は action 側が固定していてこちらからは外せない。** + したがって GPU に依る経路を Player で確かめる術は、いまの構成には無い - **「ビルドできた」と「動く」の差。** v0.1.0 でこれを踏んだ——3 platform とも ビルドが成功し linkage 検証も配布物生成も通ったのに、Linux の成果物は 古い環境で読み込めなかった。`tools/verify-plugin-portability.ps1` は @@ -560,9 +563,9 @@ CodeQL まで見る。それでも次は緑のまま通過する。 | skill | いつ使うか | | --- | --- | -| `add-abi-function` | `ocvu_` の ABI 関数を追加・変更・削除するとき。L1 → **spec に 1 エントリ + `dev.ps1 generate`** → 実装 → L3 → status 同期の TDD 順序と、所有権・バッファ・例外バリアの規約。**M5 以降、宣言は手で書かない** —— ヘッダにも `NativeMethods.cs` にも手で足さない(足せば `verify-generated` が落とす)。**新しい OpenCV module を足すなら、先にリンクを実証する節**も読むこと —— `COMPONENTS` に足すだけでは binary は 1 バイトも増えず、`cv::` を実際に参照する L1 テストだけが証拠になる。**2026-09 の API 拡張で 1 つ加わった** —— **`docs/api-reference.md` に名前を書くのも同じ commit である**(速いレーンが「spec の全関数名が api-reference に現れること」を fail-fast で見るので、**「文書は最後にまとめる」は成立しない**)。**M7 で 3 つ加わった** —— **`OCVU_SOURCES` はもう手で書く一覧ではない**(正本は `native/modules.cmake` の `OCVU_MODULE_`。新しい module では `#include` 1 行に加えてここに 2 行要る)/ **profile を持つ module に足すときの分岐**(出力先・クラス名・assembly が変わり、**到達性テストの呼び出し元を自分で作らないと生成物が stripping の対象になる**。CI はどのレーンも define を立てないので確認は手動)/ **逆向きの検査は 2 本**(ソースを読む `BindingGenerator.Tests.ps1` と、binary の export 面を読む `verify-exported-symbols.ps1`)| +| `add-abi-function` | `ocvu_` の ABI 関数を追加・変更・削除するとき。L1 → **spec に 1 エントリ + `dev.ps1 generate`** → 実装 → L3 → status 同期の TDD 順序と、所有権・バッファ・例外バリアの規約。**M5 以降、宣言は手で書かない** —— ヘッダにも `NativeMethods.cs` にも手で足さない(足せば `verify-generated` が落とす)。**新しい OpenCV module を足すなら、先にリンクを実証する節**も読むこと —— `COMPONENTS` に足すだけでは binary は 1 バイトも増えず、`cv::` を実際に参照する L1 テストだけが証拠になる。**2026-09 の API 拡張で 1 つ加わった** —— **`docs/api-reference.md` に名前を書くのも同じ commit である**(速いレーンが「spec の全関数名が api-reference に現れること」を fail-fast で見るので、**「文書は最後にまとめる」は成立しない**)。**M7 で 3 つ加わった** —— **`OCVU_SOURCES` はもう手で書く一覧ではない**(正本は `native/modules.cmake` の `OCVU_MODULE_`。新しい module では `#include` 1 行に加えてここに 2 行要る)/ **profile を持つ module に足すときの分岐**(出力先・クラス名・assembly が変わり、**到達性テストの呼び出し元を自分で作らないと生成物が stripping の対象になる**。**2026-09-11 から `ci-unity.yml` の `DnnEditMode` / `DnnStandalone` レーンが `OCVU_PROFILE_DNN` を立てて走るので、確認は手動ではない** —— ただし profile を新設したら、そのレーンを足すのは自分の仕事である)/ **逆向きの検査は 2 本**(ソースを読む `BindingGenerator.Tests.ps1` と、binary の export 面を読む `verify-exported-symbols.ps1`)| | `add-a-platform` | **対象 platform を足す・外す・変えるとき。** 一覧を持つ場所は**多い**(**数を写さない。正本は skill 側の表の行数である** —— skill からも数を落とした。**M7 で 2 つ増えたのに「17」だけが取り残された**ため。M6 で数え直したときは**リポジトリの外にもあった**)、語彙が違うので grep 1 回では揃わない。`.meta` のキー名 / ファイル名の platform 間衝突 / クロスでの `find_package` の閉じ込め / 静的ライブラリの依存の束ね / 新しい third-party など、**クロスビルドが緑になってから CI で 8 回落ちた**罠を、踏んだ順に並べてある。**M7 で 3 つ加わった** —— `release.yml` の「未知の platform は落とす」門は **2 つではなく 3 つ**(M7b が公開面の検査を足した)/表に 2 行(`verify-exported-symbols.ps1` の配線 2 箇所、全部入り tarball の上限 —— **M7c は実際にここで落ちた**)/**構成ハッシュは `opencv-config.psd1` の内容しか見ないので、toolchain file に書いた flag は復元済み artifact に反映されない**(罠 8 の対になる半分)。あわせて **allowlist の限界**(別のライブラリへ静的に取り込まれた third-party は見えない)も罠 5 に入れた | -| `milestone-complete` | マイルストーンの完了を判定するとき。roadmap の完了条件との実測照合、**文書の陳腐化確認**、CI での確定。**M4 で 2 つ加わった** —— 実機が要る条件は「満たすが未実証」ですらなく人が実行する手順書に落とす / **必須チェックの充足は成功件数ではなく名前で突き合わせる**(GitHub は `skipped` と `neutral` も pass として通す)。**M6 でさらに 3 つ** —— **条件つきで先送りした手順書は、条件が満たされた瞬間にその否定文が嘘になる**(しかも中の「済ませた」印は語句の置換では直らず、**そのまま実行すると中身と説明が食い違った Release が出る**)/ **数が動かなくてもその補集合が動く**(M6 は**必須チェックの本数を 1 本も変えずに、必須でないものの本数を変えた**。数が動いていないので**どの機械も捕まえない**)/ **直すときは強いほうから**(要約や図は目に付くので先に直り、**実行手順と散文が残る**)。**2026-09 の API 拡張でもう 1 つ** —— **`.md` だけを洗うと足りない**(事実は検査スクリプトと実装のコメントにも書かれており、そちらは「文書」と呼ばれないので見落とす。**実際に 4 件踏んだ。どれも CI は緑のまま通る**)。**M7 でさらに 3 つ** —— CI に配線が無いレーン(`test-unity-graphics` / `benchmark`)は**判定のときだけが唯一の実行機会**である/`test-unity-player` は後始末でハングする既知の欠陥を持つ(結果 XML には出るので、プロセスを止めて `assert-unity-results.ps1` を直接掛ける)/**判定を複数の節に割ったら、残数を書かず、名指し参照を自分で洗う**(`ci-lint` のリンク検査は backtick 見出し参照を見ない。**索引になる節を 1 つ置かないと「完了した」と書いた文がどこにも無い状態になる** —— M7 で実際にそうなった)| +| `milestone-complete` | マイルストーンの完了を判定するとき。roadmap の完了条件との実測照合、**文書の陳腐化確認**、CI での確定。**M4 で 2 つ加わった** —— 実機が要る条件は「満たすが未実証」ですらなく人が実行する手順書に落とす / **必須チェックの充足は成功件数ではなく名前で突き合わせる**(GitHub は `skipped` と `neutral` も pass として通す)。**M6 でさらに 3 つ** —— **条件つきで先送りした手順書は、条件が満たされた瞬間にその否定文が嘘になる**(しかも中の「済ませた」印は語句の置換では直らず、**そのまま実行すると中身と説明が食い違った Release が出る**)/ **数が動かなくてもその補集合が動く**(M6 は**必須チェックの本数を 1 本も変えずに、必須でないものの本数を変えた**。数が動いていないので**どの機械も捕まえない**)/ **直すときは強いほうから**(要約や図は目に付くので先に直り、**実行手順と散文が残る**)。**2026-09 の API 拡張でもう 1 つ** —— **`.md` だけを洗うと足りない**(事実は検査スクリプトと実装のコメントにも書かれており、そちらは「文書」と呼ばれないので見落とす。**実際に 4 件踏んだ。どれも CI は緑のまま通る**)。**M7 でさらに 3 つ** —— CI に配線が無いレーンは**判定のときだけが唯一の実行機会**である(**M7 当時それに当たったのは `test-unity-graphics` / `benchmark` で、2026-09-11 に両方とも CI へ配線した。規則そのものは生きている**)/`test-unity-player` は後始末でハングする既知の欠陥を持つ(結果 XML には出るので、プロセスを止めて `assert-unity-results.ps1` を直接掛ける)/**判定を複数の節に割ったら、残数を書かず、名指し参照を自分で洗う**(`ci-lint` のリンク検査は backtick 見出し参照を見ない。**索引になる節を 1 つ置かないと「完了した」と書いた文がどこにも無い状態になる** —— M7 で実際にそうなった)| | `prove-a-check-works` | テスト・assertion・検証スクリプト・allowlist・CI ゲート・hook を足すか変えるとき。**壊して落ちることを見るまで、その検査は動くと言えない。** M1 の全タスクが同じ欠陥(著者が列挙した形だけを見る)を生んだので手順にした。**M4 で 6 つの節が加わった** —— 壊す前にコミットする / 置換が空振りしていないか確かめる / 手前に別の門があると番人に到達しない / 述語のゆるさ(構造的に常に真・部分一致・大文字小文字・散文に当たる)/ **自分でパースする検査は本物の解釈を代理できない** / **正本を写さず正本から読む**。**M5 でさらに 4 つ** —— **Unity 経由のクラッシュは赤いテストにならない**(無音で 10 分以上返り、結果が 1 バイトも出ない)/ 手前の門の 2 例目(`System.Text.Json` が先に落とすので、足した検査は 1 度も動いていなかった)/ **アンカーの意味**(.NET の `$` は末尾の `\n` の直前にも当たる。「一致したか」ではなく**値全体を覆ったか**を見る)/ **列挙に基づく門を出口側の構造に基づく門へ変える**(ただし下流に検査が無い所にだけ足す)。**M6 でもう 1 つ** —— **負の対照の「形」が想定と違うことがある**(`-msimd128` を外すと検査が赤くなるのではなく**コンパイルが通らない**。より強い形だが**同じ形ではない**ので、同じ言葉で記録してはいけない ——「壊したら落ちた」と書くと次の人は「検査が落ちた」と読むが、**検査は 1 度も走っていない**)。**2026-09 の API 拡張でその対になる形が出た** —— **負の対照がそもそも取れないことがある**(ステレオの入力を縞から模様に変えたとき、BM 側は縞に戻すと落ちたが **SGBM は縞でも視差を見つけるので落ちなかった**)。**M6 のものが「より強い形」なら、こちらは「弱い形」である** ——「壊したら落ちた」と書けないときは、書けないと書く。**M7 で 5 つ** —— **数を見る検査は、誤った読みに満たされる**(同じ形を 3 回作った実例。フォルダを「2 つ以上読めたか」で守ると 4 つのうち 2 つしか読めない誤読が通り、**合計 3 本に増やしても釣り合わせられた**。閉じたのは「合計は十分に大きいか」を「**フォルダを出すはずだった要素は全部出したか**」に変えたときで、**その構造の門 1 本が数の門 2 本を消した**)/**門を何本置くかは本数ではなく、1 本ずつ単独で何を捕まえるかで決める**(対になって初めて 1 つの門になる形と、束にしても釣り合う形の見分け方)/**検査が当たっていた対象が本番の物ではなく代用物だった** 2 例(開発用 binary / 手書き probe)/**負の対照の 3 例目。壊しても落ちず、かつ必要性を確定させる経路がリポジトリに存在しない**(`.clone()`。「検査が働くことを示せていない」と「そのコードが要ることを示せていない」は別の主張である)/**走らないレーンは直した後も同じ速さで腐る**(`test-unity-tarball` が M7a で再発した)| **hook**(`.claude/settings.json`) diff --git a/README.ja.md b/README.ja.md index e6c009e..3e4c67b 100644 --- a/README.ja.md +++ b/README.ja.md @@ -200,7 +200,8 @@ Symbols** に `OCVU_PROFILE_DNN` を対象 platform 分足したときだけコ # GPU に依る RenderTexture の経路(EditMode、グラフィックス有効)と、 # Player / graphics の 2 レーンから OCVU_BENCH: 行を集める benchmark。 -# **どちらもどの workflow からも走りません**(下の「CI が見ているもの」)。 +# **2026-09-11 から CI にも同等のものが在ります。ただしどちらの check も +# 必須ではありません**(下の「CI が見ているもの」)。 ./tools/dev.ps1 test-unity-graphics ./tools/dev.ps1 benchmark @@ -237,9 +238,11 @@ CI は Unity のレーン以外、すべて同じ `tools/dev.ps1` を呼びま モバイルの列が「クロスビルドのみ」なのは、それが CI にできることの全部だからです —— コンパイルして成果物を検査しますが、**どの実機もそれらを読み込んだことがありません。** -**この表に無いレーンが 4 つあります。どれもどの workflow からも走りません。** `test-unity-web`(WebGL の Player を建ててブラウザで走らせる)は、CI 側が `Web browser E2E` job で同じことを別の形でやっています —— あちらは Player を自分で建て、`dev.ps1` を通さずに同じ 2 つのスクリプトを直接呼びます。`test-unity-tarball`(UPM tarball を使い捨てのプロジェクトへ導入する)はローカル専用で、上に書いた「導入できて通る」という結果は手で測ったものです。残る 2 つ(`test-unity-graphics` と `benchmark`)は後から足したもので、**CI に相当するものが一切ありません。** +**この表に無いレーンが 4 つあります。CI がその `dev.ps1` のコマンドを直接呼ばないからです。** **4 つとも、いまは CI に同等のものが在ります** —— それが変わったのは 2026-09-11 で、それ以前は 2 つに**相当するものが一切ありませんでした**。結び付きが最も強いのは `test-unity-tarball` で、CI は `./tools/dev.ps1 test-unity-tarball -PrepareOnly` を呼んで使い捨てプロジェクトを作り、起動だけを GameCI に渡します —— **CI が導入する tarball と、あなたが手元で導入する tarball は、同じコードが作ります。** 最も緩いのは `test-unity-web` で、CI の `Web browser E2E` job は Player を自分で建て、`dev.ps1` を通さずに同じ 2 つのスクリプトを直接呼びます。`test-unity-graphics` と `benchmark` はその中間で、CI の `Graphics` レーンと `benchmarks` job が同じものを動かし、同じものを集めます。 -**後ろの 2 つは、前の 2 つより重い意味を持ちます。他に同じところを見ているものが無いからです。** `RenderTextureConverter.ToMat` と `RequestMat` は、パッケージに入る公開 API ですが(**次の版から**。上の「現状」)、**実際に画素を運ぶ経路は CI で 1 度も実行されていません。** CI の Unity レーンは 2 つとも `-nographics` で走り、その下では `RenderTexture` の生成は成功するのに読み出した画素が塗った色になりません —— つまり GPU の経路はそこでは動かせず、それを動かす `test-unity-graphics` はローカル専用で merge を止めません。**引数の検証だけは CI が通っています**(`ToMat(null)` を拒むことは両レーンで実行されます)。通っていないのは GPU が要る側です。CI から見えないテストの一覧は `tests/UnityProject/Assets/Tests/EditMode/CiVisibilityTests.cs` が名指しで固定しているので、増えるときは意図して増やすことになります。 +**この 4 つのうち 3 つ(`Graphics` レーン・tarball 導入の job・benchmark の job)は必須ではありません。赤くなっても merge は止まりません。** 残る 1 つ `Web browser E2E` は**必須です** —— 2026-09-10 に昇格しました。この食い違いこそが、上の段落で「CI が走らせている」と「CI が止める」を分けて書いている理由です。 + +**`RenderTexture` の公開 API のうち、GPU が要る側は Editor でだけ実行されるようになりました。** `RenderTextureConverter.ToMat` と `RequestMat` は、パッケージに入る公開 API です(**次の版から**。上の「現状」)。2026-09-11 までは、実際に画素を運ぶ経路は CI で 1 度も実行されていませんでした —— **できないと考えられていたから**です。CI の Unity レーンはどれも `-nographics` で走ると思われており、その下では `RenderTexture` の生成は成功するのに読み出した画素が塗った色になりません。**しかしその前提は 1 度も測られていませんでした。** GameCI の Linux エディタコンテナは Unity を `xvfb` の下で、**`-nographics` を付けずに**起動します。そこで `Graphics` レーンを足したところ、本物の経路が通りました —— `AsyncGPUReadback` の側も含めてです。**IL2CPP の Player は別で、いまも覆われていません**: GameCI は standalone Player を action の中から `-nographics` で起動しており、そこはこちらから変えられません。引数の検証はどのレーンでも通っています(`ToMat(null)` を拒むこと)。通っていないのは Player 側の GPU の経路です。**必須でない `Graphics` レーンにしか居ないテスト**の一覧は `tests/UnityProject/Assets/Tests/EditMode/CiVisibilityTests.cs` が名指しで固定しているので、増えるときは意図して増やすことになります。 Unity のレーンは Linux で走り、Windows の IL2CPP Player はローカルのレーンだけが担います。**これはいまや推測ではなく実測に基づく結論です。** 以前ここに書いてあった理由は、実際には別の action と別のイメージ系統についての上流 issue を挙げていました。2026-08-31 に `windows-2022` で実際に試したところ、**EditMode は動いて 33 件通りました。Standalone は動きませんでした** —— IL2CPP は C++ を生成し、GameCI の Windows コンテナにはそれをコンパイルする MSVC が無いため、`ToolchainNotFoundException` でビルドが落ちます。**したがって Windows 固有の IL2CPP の欠陥は、見落としではなく設計上 CI に映りません。** macOS は同じ時期に 4 回試し、さらに手前で失敗します —— GameCI は darwin を支えず、エディタを直接入れる経路はライセンスが「entitlement 0 件」を返すところで止まります。ロードマップに両方の試行が記録されています。 @@ -247,7 +250,9 @@ Unity のレーンは Linux で走り、Windows の IL2CPP Player はローカ 表の外では、すべての pull request が `actionlint` / `shellcheck` / `PSScriptAnalyzer` とリポジトリ内リンクの検査を走らせ、CodeQL が C++ と C# を解析します。nightly の workflow は Linux 成果物の glibc の下限を再確認し、Windows と macOS で速いレーンを走らせ、固定した OpenCV の artifact が期限切れでないことを確かめます —— **誰も push していない間に壊れるもの**です。**この nightly は 2026-08-29 から毎日 schedule で走っています** —— 2026-09-10 時点で 13 回、直近 8 回は緑です。(以前ここには「まだ schedule で起動したことがない」と書いてありました。1 週間以上のあいだ偽で、**この種の記述は真でなくなっても何も赤くなりません。**) -**pull request で走るレーンのほとんどが merge を止めます。** 必須チェックは 24 本です: desktop 3 platform の契約・P/Invoke・sanitizer、Android・iOS・Web / Wasm のクロスビルド、lint の 4 job、CodeQL の 2 つ、Unity の 2 レーン、ブラウザでの E2E、そして 6 platform 分の配布物をビルド・組み立てる release の 7 job。9 本は意図的に必須にしていません。うち 5 本は Unity のレーンが消費する platform ごとのプラグインをビルドするもので、**どれかが失敗すると Unity のレーンは走ったうえで材料が無いことで赤くなり**、それが merge を止めます。Web / Wasm の 3 本(クロスビルド・ブラウザでの E2E・リリースの梱包)は **2026-09-10 に必須へ昇格しました** —— #63 以降に merge された 11 本の pull request すべてで 3 本とも緑になり、そのうち main でも走る 2 本は main の直近 6 run でも緑だったのを見てからです。残る 4 本は、言語ごとの解析を個別に必須にしてある `CodeQL` の集約 check、pull request では skip される Unity の結果公開 job 2 本、そしてこの節の末尾で述べる `Publish the release` です。**したがって Web のレーンも、赤ければ merge を止めます。****昇格は証拠に従いますが、その証拠は誰かが見に行かないと出てきません。****skip された必須チェックは合格として通る**ので、その守りなしにそれらへ依存すると、壊れたビルドが通ってしまいます。**レーンは安定して緑になってから必須にします** —— ただし過去 2 回踏んだのは、その逆の形でした: **レーンを足したのに必須にしないまま置いたこと**が、「赤いのに merge できる」隙間そのものです。2026-08-29 までは Unity・lint・CodeQL の workflow がすべての pull request で走りながら必須ではなく、**赤いまま merge できました。CI が見ていることと CI が止めることは別で、ゲートなのは後者だけです。** 残る 3 つの workflow(`build-opencv`、`nightly`、`unity-probe`)は pull request では起動しないので、そもそも必須にできません。`release` は 2026-08-31 までその一覧にありました —— いまは pull request でも走ります。tag でしか走らなかった間に配布の経路に欠陥が 3 件たまり、**うち 1 件は「tag を打つとリリースが 1 件も作られない」というものでした。** その `Publish the release` job だけは必須にしません —— pull request では設計上 skip され、**skip は合格として通る**ので、必須にしても何も止まらないからです。 +**pull request で走るレーンのほとんどが merge を止めます。** 必須チェックは 29 本です: desktop 3 platform の契約・P/Invoke・sanitizer、Android・iOS・Web / Wasm のクロスビルド、lint の 4 job、CodeQL の 2 つ、Unity の 2 レーン、ブラウザでの E2E、6 platform 分の配布物をビルド・組み立てる release の 7 job、そして **2026-09-11 に足した `ci-unity` の 5 job**(`Graphics` レーン・`dnn` profile の 2 レーン・tarball 導入の job・benchmark を publish する job)。13 本は意図的に必須にしていません —— うち 6 本は GameCI の結果 check(`EditMode results` とその 5 つの兄弟)で、**pull request では `neutral` になり、GitHub はそれを合格として通す**ので、必須にしても止める力は増えません。(**数えるのは run が終わってからにしてください**: GameCI は結果 check を各 job の終盤に作るので、途中で数えると少なく出ます。ここの数は完了した PR での `gh pr checks` を、保護設定と名前で突き合わせたものです。) + +残る 7 本のうち 5 本は Unity のレーンが消費する platform ごとのプラグインをビルドするもので、**どれかが失敗すると Unity のレーンは走ったうえで材料が無いことで赤くなり**、それが merge を止めます。残る 4 本は、言語ごとの解析を個別に必須にしてある `CodeQL` の集約 check、pull request では skip される Unity の結果公開 job 2 本、そしてこの節の末尾で述べる `Publish the release` です。(Web / Wasm の 3 本(クロスビルド・ブラウザでの E2E・リリースの梱包)は **2026-09-10 に必須へ昇格しました** —— #63 以降に merge された 11 本の pull request すべてで 3 本とも緑になり、そのうち main でも走る 2 本は main の直近 6 run でも緑だったのを見てからです。**したがって Web のレーンも、赤ければ merge を止めます。** 上の 24 本に数えてあります。)**昇格は証拠に従いますが、その証拠は誰かが見に行かないと出てきません。****skip された必須チェックは合格として通る**ので、その守りなしにそれらへ依存すると、壊れたビルドが通ってしまいます。**レーンは安定して緑になってから必須にします** —— ただし過去 2 回踏んだのは、その逆の形でした: **レーンを足したのに必須にしないまま置いたこと**が、「赤いのに merge できる」隙間そのものです。2026-08-29 までは Unity・lint・CodeQL の workflow がすべての pull request で走りながら必須ではなく、**赤いまま merge できました。CI が見ていることと CI が止めることは別で、ゲートなのは後者だけです。** 残る 3 つの workflow(`build-opencv`、`nightly`、`unity-probe`)は pull request では起動しないので、そもそも必須にできません。`release` は 2026-08-31 までその一覧にありました —— いまは pull request でも走ります。tag でしか走らなかった間に配布の経路に欠陥が 3 件たまり、**うち 1 件は「tag を打つとリリースが 1 件も作られない」というものでした。** その `Publish the release` job だけは必須にしません —— pull request では設計上 skip され、**skip は合格として通る**ので、必須にしても何も止まらないからです。 ## 貢献とセキュリティ diff --git a/README.md b/README.md index 1d1325a..c8c4e8e 100644 --- a/README.md +++ b/README.md @@ -288,7 +288,8 @@ All local development after that goes through `tools/dev.ps1`: # The GPU-dependent RenderTexture paths (EditMode, graphics enabled) and the # benchmark lane that collects OCVU_BENCH: lines from the player and graphics -# lanes. Neither runs in any workflow — see "What CI covers" below. +# lanes. CI has run equivalents of both since 2026-09-11, but neither of those +# checks is required — see "What CI covers" below. ./tools/dev.ps1 test-unity-graphics ./tools/dev.ps1 benchmark @@ -330,26 +331,39 @@ different: CI builds a real WebGL player and **runs it in a headless Chromium**, same checks EditMode and the IL2CPP player run also run in a browser. There is no sanitizer lane for Web (a cross-compiled sanitizer cannot run on the host). -Four lanes are absent from that table because they run in no workflow at all. -`test-unity-tarball` installs the UPM tarball into a throwaway project; it is local -only, and the "installs and passes" result above was measured by hand. -`test-unity-web` builds the WebGL player and drives it in a browser; CI covers the -same ground in its own `Web browser E2E` job, which builds the player itself and -calls the same two scripts directly rather than going through `dev.ps1`. -`test-unity-graphics` and `benchmark` were added later and have no CI counterpart -at all. - -**That last pair matters more than the first two, because nothing else covers the -same ground.** `RenderTextureConverter.ToMat` and `RequestMat` are public API in -the package (from the next release — see *Status*), and **the code path that -actually moves pixels has never run in CI.** Both CI Unity lanes run with `-nographics`, where `RenderTexture` -creation succeeds but the pixels read back are not the ones you drew — so the -GPU path cannot be exercised there, and `test-unity-graphics`, which does exercise -it, is local-only and does not block a merge. Argument validation *is* covered -(`ToMat(null)` is rejected in both CI lanes); it is the GPU half that is not. +Four lanes are absent from that table because CI does not invoke those `dev.ps1` +commands directly. **All four now have a CI equivalent** — that changed on +2026-09-11, and before it two of them had none at all. `test-unity-tarball` is the +closest coupling: CI calls `./tools/dev.ps1 test-unity-tarball -PrepareOnly` to build +the throwaway project and then hands the launch to GameCI, so the tarball CI installs +is built by the same code that builds the one you install locally. `test-unity-web` +is the loosest: CI's `Web browser E2E` job builds the player itself and calls the same +two scripts directly rather than going through `dev.ps1`. `test-unity-graphics` and +`benchmark` sit in between — CI runs a `Graphics` lane and a `benchmarks` job that +exercise and collect the same things. + +**Three of those four CI checks are not required, so a red one does not stop a +merge**: the `Graphics` lane, the tarball install job and the benchmark job. The +fourth, `Web browser E2E`, *is* required — it was promoted on 2026-09-10. That +split is why the paragraph above distinguishes "CI runs it" from "CI blocks on +it"; the two are not the same thing. + +**The GPU half of the `RenderTexture` API is now exercised, in the Editor only.** +`RenderTextureConverter.ToMat` and `RequestMat` are public API in the package +(from the next release — see *Status*). Until 2026-09-11 the code path that +actually moves pixels had never run in CI, on the assumption that it could not: +every CI Unity lane was believed to run with `-nographics`, where `RenderTexture` +creation succeeds but the pixels read back are not the ones you drew. **That +assumption was never measured.** GameCI's Linux editor container runs Unity under +`xvfb` *without* `-nographics`, so a `Graphics` lane exercises the real path — it +passes, including the `AsyncGPUReadback` route. **The IL2CPP player is a different +story and is still not covered**: GameCI launches the standalone player with +`-nographics` from inside the action, and that is not ours to change. Argument +validation is covered in every lane (`ToMat(null)` is rejected); the player-side +GPU path is not. `tests/UnityProject/Assets/Tests/EditMode/CiVisibilityTests.cs` pins the list of -tests CI cannot see, so the list has to be edited deliberately rather than growing -by accident. +tests that live only in the non-required `Graphics` lane, so the list has to be +edited deliberately rather than growing by accident. The Unity lanes run on Linux, and the Windows IL2CPP player is covered only by the local lane. **That is now a measured conclusion rather than an assumption.** An earlier @@ -379,20 +393,30 @@ eight of them green. (An earlier version of this paragraph said it had never run a schedule. That was wrong for over a week, and nothing goes red when a statement like that stops being true.) -**Almost every lane that runs on a pull request blocks a merge.** Twenty-four checks are +**Almost every lane that runs on a pull request blocks a merge.** Twenty-nine checks are required: the contract, P/Invoke and sanitizer jobs across the three desktop platforms, the Android, iOS and Web/Wasm cross-builds, the four lint jobs, both CodeQL analyses, both -Unity lanes, the browser end-to-end test, and the seven release jobs that build and -assemble the distributable for all six platforms. -Nine are deliberately not required. Five build the per-platform plugins the Unity lanes +Unity lanes, the browser end-to-end test, the seven release jobs that build and +assemble the distributable for all six platforms, and — **since 2026-09-11** — the +five `ci-unity` jobs added that day: the `Graphics` lane, the two `dnn`-profile +lanes, the tarball-install job and the benchmark-publishing job. +Thirteen are deliberately not required. Six of them are the GameCI results checks +(`EditMode results` and its five siblings): they report `neutral` on pull requests, +and **GitHub counts neutral as passing**, so requiring them would add no stopping +power. (Count these *after* the run finishes: GameCI creates them late in each job, +so counting mid-run undercounts. These numbers come from `gh pr checks` on a +completed PR, matched against the protection settings by name.) + +Of the other seven, five build the per-platform plugins the Unity lanes consume: when one fails the Unity lanes run anyway and go red on the missing input, which -is what stops the merge. The three Web/Wasm checks — its cross-build, its browser -end-to-end test and its release packaging — **were promoted on 2026-09-10**, after -passing on all eleven pull requests merged since #63 and, for the two that also run on -`main`, its last six runs. A red Web lane now stops a merge like any other. The other -four are the aggregate `CodeQL` check, whose two per-language analyses are required -individually; the two Unity result-publishing jobs, which are skipped on pull requests; -and `Publish the release`, described at the end of this section. +is what stops the merge. The other four are the aggregate `CodeQL` check, whose two +per-language analyses are required individually; the two Unity result-publishing jobs, +which are skipped on pull requests; and `Publish the release`, described at the end of +this section. (The three Web/Wasm checks — its cross-build, its browser end-to-end test +and its release packaging — **were promoted on 2026-09-10**, after passing on all eleven +pull requests merged since #63 and, for the two that also run on `main`, its last six +runs. A red Web lane now stops a merge like any other; they are counted among the +twenty-four above.) **Promotion follows evidence, but somebody has to go and look at it.** A skipped required check counts as passing, so depending on one without that guard would let a broken build through. A lane is only made required once it has been reliably green — but the failure diff --git a/docs/README.md b/docs/README.md index 6eb6581..1c11110 100644 --- a/docs/README.md +++ b/docs/README.md @@ -28,7 +28,7 @@ - **M7a(2026-09-06)。** `Texture2D` / `RenderTexture` と `CvMat` の間の低コピー経路の評価と、`./tools/dev.ps1 benchmark` / `test-unity-graphics` が測った実測値 - **時間は公開するが assert しない。** `RenderTexture` の同期・非同期 2 経路は run をまたぐと大小が入れ替わることを実例で示す - native texture pointer は評価のみで実装しない決定と、その理由・再評価の条件 - - `RenderTexture` の経路が `-nographics` で読めない実測と、`test-unity-graphics` が CI に配線されていないこと + - `RenderTexture` の経路が `-nographics` で読めない実測と、**Editor 側は 2026-09-11 に CI へ配線され、Player 側は依然として配線できないこと**(game-ci が Player を `-nographics` で起動する) - [Unity 向け OpenCV 統合の競合調査と初期計画](./unity-opencv-integration-research-and-plan.md) - OpenCV 5.x / 4.x の状況(2026-08-25 時点。**§3 と §4.6 は 2026-08-29 に取り直し、§4.6 の配布と OpenUPM、§8.3 の `imgcodecs` は M3.5(2026-08-30)で更新した** —— 5.0 の目玉が DNN エンジンの書き直しであること、競合の現況、OpenUPM という配布経路) - OpenCV for Unity、OpenCV-plus-Unity、OpenCvSharp、Emgu CV の比較 @@ -121,8 +121,11 @@ native binary には既定で入っています)です。 **依存 allowlist からは見えません**。`WITH_CAROTENE` / `WITH_KLEIDICV` は既定 ON の まま監査していません。**third-party のライセンス集合は platform ごとに実際に違う のに、`THIRD_PARTY_NOTICES.md` は 1 通で全 platform を代表しています。** -`RenderTexture` の画素を運ぶ経路は CI で 1 度も実行されません -(`test-unity-graphics` はローカル専用です)。詳細は roadmap の `M7 の判定` と、 +`RenderTexture` の画素を運ぶ経路は、**Editor 側は 2026-09-11 から CI が +実行します**(`ci-unity.yml` の `Graphics` レーン。経緯は roadmap の +「GPU 経路を CI に載せる」)。**Player(IL2CPP)側は依然として実行されません** +—— game-ci が Standalone Player を `-nographics` で起動しており、その指定は +action の中にあって外せないためです。詳細は roadmap の `M7 の判定` と、 各判定節の「穴を隠さず書く」にあります。 本文中の「推奨」「目標」「案」のうち、まだ実装されていない部分は依然として設計提案であり、実装済み機能や動作確認結果ではありません。両者の区別は各文書内の記述を見て判断してください。競合製品のバージョンや対応状況は変わるため、実装開始時と公開前に再確認します。 diff --git a/docs/api-reference.md b/docs/api-reference.md index d925e82..c16250a 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -957,9 +957,10 @@ CPU から読むには GPU → CPU の転送が要る。 **`-nographics` では動かない。** `RenderTexture.Create()` は true を返すが、 `ReadPixels` / readback が返す画素は描画結果ではなく `205,205,205` になる -(実測。詳細は [性能](./performance.md))。この経路を検証するローカル専用の -`test-unity-graphics` レーンは `-nographics` を付けずに走り、**CI には -配線していない。** +(実測。詳細は [性能](./performance.md))。この経路を検証するレーンは +`-nographics` を付けずに走る —— ローカルの `test-unity-graphics` と、 +**2026-09-11 に足した CI の `Graphics` レーン**(`ci-unity.yml`)である。 +**ただし後者は必須チェックではないので、赤くても merge は止まらない。** ### 2.17 `CvUnity.Dnn.CvDnn` / `CvNet`(M7c で追加、`dnn` profile) diff --git a/docs/performance.md b/docs/performance.md index f213238..dc20f84 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -1,10 +1,13 @@ # 性能 **この文書は測った数字と、測っていないことを書く。** 数字を書く経路は -`./tools/dev.ps1 benchmark` の 1 つだけである —— 内部で `test-unity-player` +**2 つある**。ローカルの `./tools/dev.ps1 benchmark` —— 内部で `test-unity-player` (GPU に依らない経路)と `test-unity-graphics`(GPU に依る経路)を順に走らせ、 両方の結果 XML から集めた `OCVU_BENCH:` 行を `artifacts/benchmarks/latest.json` -へ書く。**`test-unity-graphics` 単体は `latest.json` を 1 バイトも書かない** +へ書く —— と、**2026-09-11 に足した CI の `benchmarks` job**(`ci-unity.yml`)で、 +そちらは `Standalone` と `Graphics` の 2 レーンの結果 artifact を材料に +同じ `run-benchmarks.ps1` を呼び、`latest.json` を artifact として publish する。 +**集計と判定はどちらも `run-benchmarks.ps1` で、そこは分かれていない。****`test-unity-graphics` 単体は `latest.json` を 1 バイトも書かない** —— そちらは Unity のテストレーンにすぎず、収集は `run-benchmarks.ps1` (`benchmark` コマンドだけが呼ぶ)が担う。 @@ -40,14 +43,52 @@ ABI 関数を足すか、反転しないことを選ぶかで、**どちらも ## 測った数字 -| 経路 | µs/回 | レーン | +**単位は key の末尾で決まる** —— `_ns` で終わる key はナノ秒、それ以外は +マイクロ秒である(`tools/run-benchmarks.ps1`。`latest.json` は entry ごとに +`unit` を持つ)。 + +### このマシン(Windows、2026-09-06) + +| 経路 | 実測 | レーン | +| --- | --- | --- | +| `mat_copy_to_pointer` | 29 µs | Player(IL2CPP、`-nographics`) | +| `mat_copy_from_pointer` | 33 µs | Player(IL2CPP、`-nographics`) | +| `texture2d_to_mat` | 292 µs | Player(IL2CPP、`-nographics`) | +| `rendertexture_sync` | 1756〜2562 µs(揺れる。下記) | Graphics(Editor/Mono、グラフィックス有効) | +| `rendertexture_async_request` | 1643〜2841 µs(揺れる。下記) | Graphics(Editor/Mono、グラフィックス有効) | +| `first_pinvoke`(起動時間。**改名前のキー**) | 1 µs(下記の留保つき) | Player(IL2CPP、`-nographics`) | + +### CI(Linux、run 34615630480、2026-09-11) + +**2026-09-11 に CI が publish するようになったので、初めて 2 つの環境の +数字が並んだ。** + +**この run の `latest.json` は旧い形である** —— 全 entry 共通の +`"unit": "microseconds per call"` を持ち、`first_pinvoke_ns` もその下に +並んでいた(**まさにこの節が直した嘘**)。entry ごとに単位を持つ新しい形を +CI が出すのは、この変更が main に入って以降の run からである。 +**数字そのものは同じで、変わるのは単位の書き方だけ**だが、 +「文書が説明している形」と「CI が出している形」が一時的に別物である +ことは書いておく。 + +| 経路 | 実測 | レーン | | --- | --- | --- | -| `mat_copy_to_pointer` | 29 | Player(IL2CPP、`-nographics`) | -| `mat_copy_from_pointer` | 33 | Player(IL2CPP、`-nographics`) | -| `texture2d_to_mat` | 292 | Player(IL2CPP、`-nographics`) | -| `rendertexture_sync` | 1756〜2562(揺れる。下記) | Graphics(Editor/Mono、グラフィックス有効) | -| `rendertexture_async_request` | 1643〜2841(揺れる。下記) | Graphics(Editor/Mono、グラフィックス有効) | -| `first_pinvoke`(起動時間) | 1(下記の留保つき) | Player(IL2CPP、`-nographics`) | +| `mat_copy_to_pointer` | 39 µs | Standalone(IL2CPP、`-nographics`) | +| `mat_copy_from_pointer` | 50 µs | Standalone(IL2CPP、`-nographics`) | +| `texture2d_to_mat` | 52 µs | Standalone(IL2CPP、`-nographics`) | +| `rendertexture_sync` | 810 µs | Graphics(Editor/Mono、xvfb + ソフトウェア GL) | +| `rendertexture_async_request` | 1289 µs | Graphics(Editor/Mono、xvfb + ソフトウェア GL) | +| `first_pinvoke_ns`(起動時間) | 400 ns(下記の留保つき) | Standalone(IL2CPP、`-nographics`) | + +**2 つの表を並べて読むときの注意が 3 つある。** + +1. **`texture2d_to_mat` が 292 µs 対 52 µs で 5 倍以上違う。** 境界のコピー + (29/33 対 39/50)はほぼ同じなので、差は `CvMat.Create` の確保側にある + —— **どちらが「正しい」でもない。** 別の OS・別のアロケータ・別の負荷で + 測った別の数字である。 +2. **CI の `Graphics` レーンの GPU はソフトウェア実装である**(コンテナの + xvfb + Mesa)。実機の GPU の数字ではない。 +3. **どちらも利用者の端末の数字ではない。** **読み方を 3 つ書く。1 つでも欠けると誤読する。** @@ -123,28 +164,42 @@ backend の話になり、M7 の CUDA に関する決定(同梱しない)に **「作れた」が「読める」を意味しないのがこの経路の落とし穴である。** v0.1.0 の「ビルドできた ≠ 動く」と同じ形が、ここにも出た。 - `test-unity-player` / `ci-unity.yml` はどちらも `-nographics` で走るので、 - **`RenderTextureConverter.ToMat` / `RequestMat` はこの 2 つのレーンでは - 検証できない** —— 検証は `test-unity-graphics`(`-nographics` を付けずに - EditMode を走らせる新しいローカル専用レーン)が担う。上下反転だけを行う + `-nographics` で走るレーン(ローカルの `test-unity-player` / + `test-unity-editmode`、CI の `EditMode` / `Standalone`)では + **`RenderTextureConverter.ToMat` / `RequestMat` を検証できない** —— + 検証は `-nographics` を付けないレーンが担う。ローカルの + `test-unity-graphics` と、**2026-09-11 に足した CI の `Graphics` レーン**の + 2 つである(**当初ここには「CI のレーンはどちらも `-nographics` で走る」と + 書いてあったが、それは誤りだった** —— 下の節を参照)。上下反転だけを行う `FillFlipped` は GPU に依存しないので、こちらは通常の EditMode / Player レーンで(合成した配列を使い)検証できる。 2. **`AsyncGPUReadback` は IL2CPP の Player で 1 度も走っていない。** `test-unity-player` も `-nographics` で走るため `supportsAsyncGPUReadback` が `false` になり、`RequestMat` の経路は - **Editor(Mono)の `test-unity-graphics` でしか実行されたことがない。** - 実機の IL2CPP Player でこの経路が動くかどうかは、いまのところ未実証である。 - -**`test-unity-graphics` は CI に配線していない。** `ci-unity.yml` からは -呼ばれないので、**このレーンが赤くても merge は止まらない。** -`tests/UnityProject/Assets/Tests/EditMode/CiVisibilityTests.cs` が -「CI から見えないテストの一覧」を名指しで固定しており、 -`GraphicsTests` / `GraphicsBenchmarkRunner.MeasureRenderTexturePaths` は -その一覧に載っている。新しく `[Category("Graphics")]` を付けたテストが -増えるとこの一覧との不一致で `CiVisibilityTests` 自体が落ちるので、 -「いつの間にか CI から見えなくなっていた」は「差分として見える変更」に -変わる —— ただし変わるのは気づき方であって、**CI に配線されること自体では -ない。** + **Editor(Mono)でしか実行されたことがない** —— 2026-09-11 からは + ローカルの `test-unity-graphics` に加えて **CI の `Graphics` レーン**でも + 走るが、どちらも Editor である。実機の IL2CPP Player でこの経路が動くか + どうかは、いまのところ未実証である。 + +**2026-09-11 に、この経路は CI へ配線された。** `ci-unity.yml` の +`Graphics` レーンが `-testCategory Graphics` で EditMode を走らせる。 + +**成立した理由は、前提が誤っていたことである。** ここには長らく +「CI のレーンは `-nographics` で走るのでこの経路を通れない」と書いてあったが、 +**game-ci が Editor を `-nographics` で起動していないことを誰も測っていなかった。** +コンテナの `unity-editor` は + + xvfb-run -ae /dev/stdout "$UNITY_PATH/Editor/Unity" -batchmode "$@" + +で、仮想 X の下から起動する(game-ci/docker の `images/ubuntu/editor/Dockerfile`)。 +実測(run 34612557397)では `AGraphicsDeviceIsPresent` が通り、 +`SyncReadbackProducesTheExpectedPixels` / `VerticalFlipIsApplied` / +`AsyncMatchesSync` / `TakingTheMatTwiceIsRejected` もすべて通った —— +**コンテナには実物の graphics device が在り、`AsyncGPUReadback` も動く。** + +**Player 側は依然として閉じていない。** game-ci の `run_tests.sh` は +Standalone Player を `xvfb-run ... -batchmode -nographics` で起動しており、 +**`-nographics` は action 側が固定していてこちらからは外せない。** ## startup time @@ -152,6 +207,15 @@ backend の話になり、M7 の CUDA に関する決定(同梱しない)に `BenchmarkRunner.MeasureFirstPInvoke`(`test-unity-player` レーン)で 測った: **1 マイクロ秒**(2026-09-06、このマシン)。 +**2026-09-11 に単位をナノ秒へ変えた**(キーも `first_pinvoke` から +`first_pinvoke_ns` に改名した)。理由は、**CI の Linux Player で実測 0 に +なったから**である(run 34612557397)。`tools/run-benchmarks.ps1` は +0 を「測定が効いていない」として落とすので、そのままでは benchmark を +CI で集められない。**0 が出たのは測定が壊れていたからではなく、 +マイクロ秒では分解能が足りなかったからである** —— 下に書くとおり、 +ここで測っているのは既にライブラリが読み込まれた後の 1 回の呼び出しで、 +それは 1 µs に満たない。**値そのものは正しく、桁の取り方だけが誤っていた。** + **この数字は「native ライブラリの真の初回ロード」を捉えていない。** 同じ Player の実行では `PlayerSmokeTests` など他の PlayMode テストが 先に `CvNative.AbiVersion` や他の `ocvu_` 関数を呼んでおり、NUnit の @@ -171,7 +235,10 @@ backend の話になり、M7 の CUDA に関する決定(同梱しない)に 測った 1 µs は同じ Player 内の他テストが先に P/Invoke を呼んだ後の値である 可能性が高く、真の初回ロード時間ではない - **IL2CPP Player での `RenderTexture` の経路。** `RenderTextureConverter.ToMat` / - `RequestMat` は Editor(Mono)の `test-unity-graphics` でしか実行されたことがない + `RequestMat` は Editor(Mono)でしか実行されたことがない —— ローカルの + `test-unity-graphics` と、CI の `Graphics` レーンの 2 つである。 + **Player 側は原理的に届かない** —— game-ci が Standalone Player を + `-nographics` で起動しており、その指定は action の中にある - **`dnn` の推論(M7c、2026-09-10)。** `./tools/dev.ps1 benchmark` の対象に `dnn` の 項目は無く、`OCVU_BENCH:` 行を `ocvu_dnn_net_forward` から出したことも無い。 **モデルの読み込み・blob 化・forward のいずれについても、このリポジトリは diff --git a/docs/roadmap.md b/docs/roadmap.md index 8444df5..c3bd9cb 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -750,7 +750,7 @@ M5 の module 拡張で `objdetect` / `features` / `geometry` / `calib`、そし | 6 | macOS で Unity に読み込ませたことがない | 「iOS のビルドに macOS runner が要るので M4 で自然に埋まる」と書いていたが、**埋まらなかった。** macOS runner は plugin をビルドするだけで Unity を起動しない | **未解消。2026-08-31 に 4 回試して「CI では閉じない」と確定した**(game-ci は macOS を支えず、Hub で直接入れる経路は Editor が 14 分で入るのにライセンスで止まる)。詳細は下の「担当が無かった制約」 | | 7 | Windows の IL2CPP を CI で回していない | 「game-ci では無理」の根拠に挙げていた issue は、使っていない別 action のものだった —— **そこで実際に投げて根拠を作った** | **未解消。2026-08-31 に「CI で回さない」と結論した** —— `windows-2022` で EditMode は 33 件通ったが、`Standalone` は `ToolchainNotFoundException` で落ちた(game-ci の Windows コンテナに MSVC が無い)。**根拠は実測であって他人の issue ではない。** これは「まだ調べていない穴」ではなく**意図して CI の外に置いたもの**である | | 8 | 対応 CPU アーキテクチャが狭い | Android エミュレータ(x86_64)が無いと開発しづらい | **M4 で決めた: arm64 のみ**(Android は arm64-v8a、iOS は実機の arm64)。**穴は塞いでいない —— 塞がないと決めた。** 増やすと platform が 2 つ増え、OpenCV のビルドも配布物も同じだけ増える(**数を写さない** —— 正本は `tools/dev.ps1` の `$AllPlatformBinaries` で、触る場所は `add-a-platform` skill にある) | -| 9 | 「低コピー連携」を測っていない | §7 の 7 番目に掲げているのに実測が無い | **M7a で着手し、部分的に解消(2026-09-06)。** 割り当ては L3 が機械で保証し(ポインタ経路 0 バイト)、時間は測って公開したが**assert していない**。**`RenderTexture` / `AsyncGPUReadback` の経路は Editor(Mono)の `test-unity-graphics` でしか実行しておらず、そのレーンは CI に配線していない** —— IL2CPP の Player でこの経路が動くかは未実証のままである。**native texture pointer は評価のみで実装していない**(やらないと決めた)。判定と根拠は下の「M7a の判定」節 | +| 9 | 「低コピー連携」を測っていない | §7 の 7 番目に掲げているのに実測が無い | **M7a で着手し、部分的に解消(2026-09-06)。** 割り当ては L3 が機械で保証し(ポインタ経路 0 バイト)、時間は測って公開したが**assert していない**。**`RenderTexture` / `AsyncGPUReadback` の経路は Editor(Mono)でしか実行していない** —— ただし **2026-09-11 に CI へ配線した**(`ci-unity.yml` の `Graphics` レーン。下の「GPU 経路を CI に載せる」)ので、「そのレーンは CI に配線していない」という留保は失効した。**IL2CPP の Player でこの経路が動くかは依然として未実証である** —— game-ci が Player を `-nographics` で起動しており、そこはこちらから外せない。**native texture pointer は評価のみで実装していない**(やらないと決めた)。判定と根拠は下の「M7a の判定」節 | | 10 | 新しい DNN エンジンを載せていない | OpenCV 5 最大の変更。ただし Unity には代替がある。**2026-08-30 の調査で、5.0 に固定して作り込めない根拠が付いた**(根拠と一次情報は M7 節。**ここに再掲しない** —— 根拠を直すと 2 箇所が同時に古くなる) | **M7c で部分的に解消(2026-09-10)。** ONNX をメモリから読んで forward を 1 回走らせる C ABI 4 本を、opt-in profile(`OCVU_PROFILE_DNN`)として出した。**ただし実機で 1 度も動かしておらず、推論の速さも測っていない**(`### M7c の判定`)。位置づけと、そこから出た module 分離の決定は下記 | ### #1 を最優先に置く理由 @@ -852,7 +852,7 @@ Asset Store での配布。 | # | 完了条件 | 判定 | | --- | --- | --- | | 1 | 全 platform の binary を 1 つに収めた tarball を `release.yml` が作る | **満たす(留保あり)**。`tools/assemble-plugins.ps1` と `pack-upm-tarball.ps1 -AllPlatforms` はローカルで実測(3 binary が同梱されていることを、作った archive の中を数えて確認)。**`release.yml` 側の配線も 2026-08-30 に通した**(run 33289128197、`workflow_dispatch`。いまの 2 job 構成での実績)—— 組み立て 3 binary、tarball 61 entries、staged 17 assets、`SHA256SUMS` 17 行(大きさは条件 4 の行にある)。Release は作られていない(publish job は tag に限る)| -| 2 | その tarball を使い捨ての Unity プロジェクトに導入して EditMode が通る | **満たす**。`dev.ps1 test-unity-tarball -PluginSource` に v0.1.1 の macOS / Linux を重ねて全部入りを作り、`==> UPM tarball install: 16 passed`(2026-08-30、このマシン)。**このレーンはどの workflow にも入っていない**(ローカル専用。M3 から変わっていない) | +| 2 | その tarball を使い捨ての Unity プロジェクトに導入して EditMode が通る | **満たす**。`dev.ps1 test-unity-tarball -PluginSource` に v0.1.1 の macOS / Linux を重ねて全部入りを作り、`==> UPM tarball install: 16 passed`(2026-08-30、このマシン)。**当時このレーンはどの workflow にも入っていなかった**(ローカル専用。M3 から変わっていなかった)。**2026-09-11 に `ci-unity.yml` の `tarball` job として載せた**(下の「GPU 経路を CI に載せる」) | | 3 | 全部入りの中で Unity が自分の platform の binary だけを読み込むことを実測する | **満たす**(2026-08-30。M3.5 時点では「満たすが未実証」だった —— 下の「条件 3 を閉じる」)。**CI が 3 platform を同居させた状態で `PluginGatingTests` を走らせ、`==> [EditMode] output says: native plugins present: 3 [` と gating 4 件の個別 Passed を出した**(PR #37、`c070923`、run 33290375806)。`PluginGatingTests`(EditMode 6 件)は書いたが、**自動で走る唯一の場所(`ci-unity.yml`)には 1 platform 分の binary しか無く、6 件が要素 1 個の集合を検査して緑になっていた**。`ci-unity` が他 2 platform を自分でビルドして重ねる形にし、gating が走ったことまで結果 XML で確かめるようにした(下記)。**この判定はその CI が緑になった時点で更新する** —— 「ファイルが存在する」は「CI で実行された」ではない | | 4 | OpenUPM へ登録できる形にする | **(a)(b) は満たす、(c) は未了**。(a) asset 名から版番号を落とした(`com.ayutaz.opencv-unity-native.tgz`)。(b) `pack-upm-tarball.ps1 -MaxBytes`(既定 512 MB)が上限を見る —— 全部入りの実測は 9.6 MB(9,608,334 バイト)。(c) **登録申請を提出し、受理された**(openupm/openupm PR #6843、2026-08-30。`Data validation` が通り自動マージ)。**OpenUPM 側の受理はこの条件に含めていなかった**が、結果として通った —— `https://package.openupm.com/com.ayutaz.opencv-unity-native` が `0.2.0` を配信し、落とした tarball に 3 platform の binary が入っていることを実測した | | 5 | `imgcodecs` を C ABI に出す(メモリ上の byte 列) | **満たす**。`ocvu_imencode` / `ocvu_imdecode`(公開 ABI 18 → 20 本、allowlist は 9 → 11 本)。L1 8 ケース / L3 8 ケース、C# は `CvCodecs`。**着手して初めて `imgcodecs` がリンクされていなかったことが分かった**(下記) | @@ -1157,8 +1157,11 @@ linkage 検証も配布物生成も通ったのに、Linux の `.so` は古い ない** —— 確かめられるのは「同じ配線が作った同じ中身の tarball が導入できる」 ところまでである。**そこは正直に書くこと。** - **このレーンはどの workflow からも走らない**ので、ここで人がやらなければ - 誰もやらない。**platform が増えた版では必ず回すこと** —— 新しい形の binary と + **2026-09-11 から、このレーンは CI にも在る**(`ci-unity.yml` の `tarball` + job。下の「GPU 経路を CI に載せる」)。**それでもここで人が回す理由は 2 つ** —— + CI が固めるのは PR 時点の binary であって**これから配る asset ではない**こと、 + そして `tarball` job は**必須チェックではない**ので赤くても止まらないこと。 + **platform が増えた版では必ず回すこと** —— 新しい形の binary と `.meta` が初めて全部入りに入るのは、その版だからである。 **予行は済んでいる**(完了条件を見よ。空撃ちの成果物で 2026-09-03 に通した)。 @@ -1514,7 +1517,7 @@ Android arm64-v8a と iOS arm64 で実機 smoke test が通る。 | 6 | macOS の Plugin Import Settings を Unity で実測 | **閉じていない**(2026-08-31 に 4 回試して確定)。game-ci は macOS を支えない(`darwin-platform is not supported`)。Unity Hub の CLI で直接入れる経路も試し、**Editor は 14 分で入るところまで到達したが、ライセンスで止まる** —— `Found 0 entitlement groups and 0 free entitlements`。**認証を 2 系統(`-username`/`-password` と `.ulf`)とも試して同じ**なので、認証方法ではなく entitlement 自体が降りていない。**Editor の導入は障害ではなく、障害はライセンスである**(詳細は上記「担当が無かった制約」の節)| | 7 | Windows IL2CPP を CI で回すかの結論 | **満たす**(2026-08-31)。**結論は「諦める」。** `windows-2022` に 2 回投げた —— EditMode は動いた(33 passed、run 33350726005)が、**`Standalone` は `ToolchainNotFoundException` で落ちた**(run 33352025223)。game-ci の Windows コンテナに、IL2CPP が生成した C++ をコンパイルする MSVC が無い。**このとき game-ci 自身は success を返しており、結果 XML の有無を別に見ていなければ逆の結論を書いていた。** 根拠は 2 回の実測で、他人の issue ではない(詳細は上記「担当が無かった制約」の節)| | 8 | 対応 CPU アーキテクチャの決定 | **満たす**。Android arm64-v8a のみ / iOS 実機 arm64 のみ(上記「対応 CPU アーキテクチャの決定」) | -| 9 | モバイルの binary が全部入りに入り `dev.ps1 test-unity-tarball` が通る | **満たす**(2026-08-31、このマシン)。5 platform 分を束ねた tarball を使い捨ての Unity プロジェクトに導入して `==> UPM tarball install: 25 passed`。**ただしこのレーンはどの workflow からも走らない** —— game-ci の action の外で Unity を起動する必要があり、CI に載せるのは別作業である。**M4 でモバイルを足した時点からこのレーンは壊れており**(期待する binary の数が `3` と直書きされ、iOS の `.a` を binary と認めなかった)、無関係な作業の途中で 1 度手で回すまで誰も知らなかった | +| 9 | モバイルの binary が全部入りに入り `dev.ps1 test-unity-tarball` が通る | **満たす**(2026-08-31、このマシン)。5 platform 分を束ねた tarball を使い捨ての Unity プロジェクトに導入して `==> UPM tarball install: 25 passed`。**当時このレーンはどの workflow からも走らなかった** —— game-ci の action の外で Unity を起動する必要があり、「CI に載せるのは別作業である」と書いて置いていた。**その別作業は 2026-09-11 に済んだ**(下の「GPU 経路を CI に載せる」。`ci-unity.yml` の `tarball` job)。**M4 でモバイルを足した時点からこのレーンは壊れており**(期待する binary の数が `3` と直書きされ、iOS の `.a` を binary と認めなかった)、無関係な作業の途中で 1 度手で回すまで誰も知らなかった —— **走らないレーンが腐るのはこれで 2 度目で**(M7a では Graphics の除外が抜けた)、CI に載せた直接の動機でもある | **条件 1・2 は 2026-08-31 に「満たす」へ変えた。** この構成の CI が緑になり、 **実物の成果物に検査が当たった**からである(run 33319185326)。それまでは @@ -1528,9 +1531,10 @@ M2 の条件 7 / M3.5 の条件 3 と同じ基準で「満たすが未実証」 **モバイルの binary が全部入りに入り `dev.ps1 test-unity-tarball` が通ること** (roadmap の最後の条件)は **満たす**。5 platform 分を束ねた tarball を 使い捨ての Unity プロジェクトに導入して `==> UPM tarball install: 25 passed` -(2026-08-31、このマシン)。**ただしこのレーンはどの workflow からも走らない** —— -game-ci の action の外で Unity を起動する必要があり、CI に載せるのは別作業である。 -`milestone-complete` の「CI では原理的に閉じない条件」と同じ扱いで、**人が回す**。 +(2026-08-31、このマシン)。**当時このレーンはどの workflow からも走らなかった** —— +game-ci の action の外で Unity を起動する必要があり、「CI に載せるのは別作業である」と +書いて置いていた。**その別作業は 2026-09-11 に済んだ**(下の +「GPU 経路を CI に載せる」)。 実際、M4 でモバイルを足した時点からこのレーンは壊れており(期待する binary の数が `3` と直書きされ、iOS の `.a` を binary と認めなかった)、**無関係な作業の途中で 1 度手で回すまで誰も知らなかった。** @@ -2297,12 +2301,12 @@ CI 自身での確認は `.superpowers/sdd/2026-09-05-m7c-dnn-profile/task-6-rep | # | 完了条件 | 判定 | | --- | --- | --- | -| 2 | `RenderTexture` / native texture pointer / `AsyncGPUReadback` を使う低コピー経路の評価 | **満たした。ただし実証の範囲は限定的である。** `RenderTextureConverter.ToMat`(同期)と `RequestMat`(`AsyncGPUReadback` を使う非同期)はどちらも実装し、実測した——`-nographics` の下では `RenderTexture.Create()` が true を返すのに読んだ画素が `205,205,205` になる(作れたが読めない)という落とし穴を実際に踏み、上下反転だけを行う `FillFlipped` を GPU 非依存の純粋関数として切り出して既存レーンで検証できる形にした。**残る 2 つの経路(`ToMat` / `RequestMat` そのもの)は Editor(Mono、グラフィックス有効)の `test-unity-graphics` でしか実行したことがない** —— このレーンは CI に配線しておらず、赤くても merge を止めない(`tests/UnityProject/Assets/Tests/EditMode/CiVisibilityTests.cs` が「CI から見えないテスト」として名指しで固定している)。**`AsyncGPUReadback` は IL2CPP の Player で 1 度も走っていない** —— `test-unity-player` は `-nographics` で走るため `supportsAsyncGPUReadback` が `false` になる。**native texture pointer は評価のみで、実装していない**(やらないと決めた—— `GetNativeTexturePtr()` を CPU から読むにはレンダースレッドからグラフィックス API を呼ぶ必要があり、6 platform 分の分岐を持つ新しい subsystem になる。得られるはずのものと再評価の条件は [性能](./performance.md) にある) | +| 2 | `RenderTexture` / native texture pointer / `AsyncGPUReadback` を使う低コピー経路の評価 | **満たした。ただし実証の範囲は限定的である。** `RenderTextureConverter.ToMat`(同期)と `RequestMat`(`AsyncGPUReadback` を使う非同期)はどちらも実装し、実測した——`-nographics` の下では `RenderTexture.Create()` が true を返すのに読んだ画素が `205,205,205` になる(作れたが読めない)という落とし穴を実際に踏み、上下反転だけを行う `FillFlipped` を GPU 非依存の純粋関数として切り出して既存レーンで検証できる形にした。**残る 2 つの経路(`ToMat` / `RequestMat` そのもの)は Editor(Mono、グラフィックス有効)でしか実行したことがない。** **2026-09-11 にこの経路を CI へ配線した**(`ci-unity.yml` の `Graphics` レーン。実測 run 34612557397 で 7 passed)ので、「CI に配線しておらず、赤くても merge を止めない」という当時の記述と、それを支えていた `CiVisibilityTests` の位置づけも変わった —— **「CI から見えないテスト」の台帳から「必須レーンに居ないテスト」の台帳になった**(`Unity Graphics (Linux)` は非必須なので、**「赤くても merge を止めない」の半分はいまも真である**)。経緯は下の「GPU 経路を CI に載せる」。**`AsyncGPUReadback` は IL2CPP の Player で 1 度も走っていない** —— game-ci は Standalone Player を `-nographics` で起動しており(`run_tests.sh`)、**その指定は action 側にあってこちらからは外せない。****native texture pointer は評価のみで、実装していない**(やらないと決めた—— `GetNativeTexturePtr()` を CPU から読むにはレンダースレッドからグラフィックス API を呼ぶ必要があり、6 platform 分の分岐を持つ新しい subsystem になる。得られるはずのものと再評価の条件は [性能](./performance.md) にある) | | 3 | package size、startup time、frame time、allocation の benchmark を公開 | **満たした。ただし性質が 2 つに分かれる。** package size(`PackageSize.Tests.ps1`)と allocation(L3 の `AllocationTests`)は**機械が assert し、CI が守り続ける**——ポインタ経路は 0 バイト、`byte[]` 経路はそれ以上であることを毎回確かめ、tarball が上限を超えれば落ちる。**frame time(境界のコピーと `RenderTexture`)と startup time は、公開したが assert していない**(設計 D1: 共有 CI ランナー上で時間を assert すると必ずフレークになる)。**startup time にはさらに留保がある** —— `BenchmarkRunner.MeasureFirstPInvoke` が実測した 1 µs は、同じ Player 実行内で他の PlayMode テストが先に P/Invoke を呼んでいる可能性が高く、**native ライブラリの真の初回ロードを捉えていない**(測れるものを測っただけで、測れていないものを測れたことにはしていない)。**`RenderTexture` の 2 経路は run をまたぐと大小が入れ替わることを実測した**(run A: sync 2562 / async 2841、run B: sync 1756 / async 1643)——「非同期のほうが速い/遅い」はどちらも主張できず、**時間を assert しない設計判断の裏づけになっている** | **穴を隠さず書く。** -- **M7a は roadmap の差別化の穴 #9(「低コピー連携」を測っていない)を「部分的に解消」にした。** 「解消済み」としなかった理由は、上の 2 経路のうち `RenderTexture` / `AsyncGPUReadback` が実機で動く実行形態(IL2CPP Player)で 1 度も検証されておらず、`test-unity-graphics` が CI に配線されていないため——**満たしたことと実証されたことは同じではない**(`milestone-complete` skill)。 +- **M7a は roadmap の差別化の穴 #9(「低コピー連携」を測っていない)を「部分的に解消」にした。** 「解消済み」としなかった理由は、上の 2 経路のうち `RenderTexture` / `AsyncGPUReadback` が実機で動く実行形態(IL2CPP Player)で 1 度も検証されておらず、`test-unity-graphics` が当時 CI に配線されていなかったため——(**後者は 2026-09-11 に解消した**。下の「GPU 経路を CI に載せる」)**満たしたことと実証されたことは同じではない**(`milestone-complete` skill)。 - **`test-unity-player` はこのマシンで、Player の後始末段階(`Stop-UnityTestPlayers` 内の `Get-CimInstance` 呼び出し)がハングする既知の欠陥を持つ。** テスト自体は完走し結果 XML も書かれるが、レーン全体が無音で固まる(`CLAUDE.md` が書く「Unity のレーンではクラッシュもハングも赤いテストにならない」という形そのもの)。M7a の変更が原因ではない(`git diff` でこの箇所に差分は無い)ので、この作業では直していない——本番の測定は、ハングしたプロセスを終了させたうえで `tools/assert-unity-results.ps1` を結果 XML に直接掛けて確認した(35 passed / exit 0)。 - **`BenchmarkRunner` の `Report` ヘルパーが `BenchmarkRunner.cs` と `GraphicsBenchmarkRunner.cs` に複製されている。** このリポジトリは「本体はここにしか無い」を繰り返し記録しており、片方だけ直る壊れ方をする。M7a では直していない。 - **この計画(M7a)が触れているのは条件 2・3 だけである。** dnn を opt-in profile として足す前提(C ABI / C# の module 分離、条件 4)にも、CUDA / cuDNN の再配布確認(条件 5)にも、条件 1(profile ごとの native artifact 等)にも触れていない。**それぞれの現在の状態は、この節ではなく担当する計画自身の判定にある**(条件 4 は `### M7b の判定`、条件 5 は `### CUDA / cuDNN 同梱の判定`、条件 1 は `### M7c の判定`)。 @@ -2378,7 +2382,7 @@ M0 から M7 までで、M8 は無い。**帰結を先に書く: ここで「次 | # | 完了条件 | 判定の本文 | 満たした | 実証した | | --- | --- | --- | --- | --- | | 1 | profile ごとの native artifact、manifest、third-party notices | `### M7c の判定`(2026-09-10) | はい | **いいえ** —— `dnn` を実機で動かしたことは 1 度も無く、推論の速さも測っていない | -| 2 | `RenderTexture` / native texture pointer / `AsyncGPUReadback` を使う低コピー経路の評価 | `### M7a の判定`(2026-09-06) | はい | **部分的** —— `ToMat` / `RequestMat` の画素を運ぶ経路は Editor の `test-unity-graphics` でしか走っておらず、そのレーンは CI に配線されていない。native texture pointer は評価のみで実装していない | +| 2 | `RenderTexture` / native texture pointer / `AsyncGPUReadback` を使う低コピー経路の評価 | `### M7a の判定`(2026-09-06) | はい | **部分的** —— `ToMat` / `RequestMat` の画素を運ぶ経路は Editor でしか走らない。**2026-09-11 に CI へ配線した**ので「レーンが CI に無い」という留保は失効したが(下の「GPU 経路を CI に載せる」)、**Player(IL2CPP)側は依然として走らない** —— game-ci が Player を `-nographics` で起動する。native texture pointer は評価のみで実装していない | | 3 | package size、startup time、frame time、allocation の benchmark を公開 | `### M7a の判定`(2026-09-06) | はい | **半分** —— package size と allocation は機械が assert し CI が守る。frame time と startup time は公開するが assert しない(設計 D1)。startup time は「native ライブラリの真の初回ロード」を捉えていない | | 4 | `dnn` を足す前に、C ABI と C# の module 分離が済んでいること | `### M7b の判定`(2026-09-06) | はい | はい —— ただし当時証明したのは「機構が働くこと」で、`dnn` で働くことは条件 1 の側(M7c)が示した | | 5 | CUDA / cuDNN を同梱するなら、再配布条件の確認が済んでいること。確認できないなら**同梱しない**と決めて記録する | `### CUDA / cuDNN 同梱の判定`(2026-09-06) | はい | —— **ライセンス条項は読んでいない。** 条件が用意した 2 経路のうち「確認できないので同梱しないと決める」を選んだ形である | @@ -2408,9 +2412,12 @@ M0 から M7 までで、M8 は無い。**帰結を先に書く: ここで「次 は 1 通で全 platform を代表している**(`clapack-lapack_LICENSE` は macOS / iOS に無い) - **CUDA / cuDNN の再配布条件は読んでいない。** 大きさだけで結論が出たので読まずに 済んだ、という形である —— **配布形態が変われば、この宿題はそのまま戻ってくる** -- **`dnn` の到達性は人が手で 1 回確かめたきりである。** `OCVU_PROFILE_DNN` を立てる - workflow も `dev.ps1` のレーンも無いので、次に `dnn` の ABI が変わっても自動では - 再検証されない +- ~~**`dnn` の到達性は人が手で 1 回確かめたきりである。**~~ **2026-09-11 に閉じた** + (下の「GPU 経路を CI に載せる」と同じ作業)。`ci-unity.yml` の + `DnnEditMode` / `DnnStandalone` レーンが `ProjectSettings.asset` に + `OCVU_PROFILE_DNN` を書いてから Unity を走らせる。**stripping 済みの + IL2CPP Player で `AbiSurfaceDnnPlayerTests.EveryDnnEntryPointIsReachable` が + 通ることを CI が要求する**ので、次に dnn の ABI が変われば自動で再検証される **担当が無い。** 上の各項目は、M8 が無い以上「次のマイルストーンで拾う」ことが できない。**拾う予定は無い** —— 拾うなら、そのときに新しい計画を立てることになる。 @@ -2443,6 +2450,134 @@ M0 から M7 までで、M8 は無い。**帰結を先に書く: ここで「次 --- +### GPU 経路を CI に載せる(2026-09-11) + +**M7 が終わった後、「CI が何を見ていないか」を洗い直したところ、 +穴の 1 つは穴ではなかった。** + +この文書と `CLAUDE.md` と [性能](./performance.md) の 3 つが、同じことを +書いていた —— 「CI のレーンは `-nographics` で走るので、`RenderTexture` の +画素を運ぶ経路は CI では確かめられない」。**そう書いた根拠は +`tools/dev.ps1` の側の実測**(`-nographics` の下で `graphicsDeviceType` が +`Null` になり、`RenderTexture.Create()` が true を返すのに `ReadPixels` が +205,205,205 を返す)であって、**CI の側は 1 度も測っていなかった。** + +実物はこうだった。`game-ci/unity-test-runner` がコンテナの中で呼ぶ +`unity-editor` は、game-ci/docker の `images/ubuntu/editor/Dockerfile` が +こう書いている: + +```sh +xvfb-run -ae /dev/stdout "$UNITY_PATH/Editor/Unity" -batchmode "$@" +``` + +**`-batchmode` は付くが `-nographics` は付かない。** 仮想 X の下で起動する +ので、コンテナに GL の実装さえ在れば graphics device は在る。 +**そして在るかどうかは Dockerfile からは決まらなかった** —— +`unityci/base` は `--no-install-recommends` で `libglu1` しか入れておらず、 +Mesa の実装が依存で入るかどうかは解決次第である。 + +**だから推測で書かず、レーンを足してそれ自体を実測に使った。** +`ci-unity.yml` の matrix に `Graphics` レーン(`-testCategory Graphics`)を +足し、ブランチ上で `workflow_dispatch` した。 + +**結果(run 34612557397、2026-09-11): 7 passed。** +`GraphicsTests.AGraphicsDeviceIsPresent` が通ったので **graphics device は +実在し**、`SyncReadbackProducesTheExpectedPixels` と `VerticalFlipIsApplied` が +通ったので **画素が実際に読める**。`AsyncMatchesSync` も通ったので +**`AsyncGPUReadback` も動く**。`GraphicsBenchmarkRunner` も `OCVU_BENCH:` を +出した(**数値はここに写さない。正本は [性能](./performance.md) である**)。 + +**残り 2 つの実測は別の run である。** `tarball` job は **run 34613947274** +(`resolved from file:../../upm/com.ayutaz.opencv-unity-native.tgz` まで出て +いる)、`benchmarks` job は **run 34615630480** で初めて走った —— +34612557397 の時点ではどちらの job もまだ存在しない。**1 本の run で全部を +主張しない。** + +**この作業で CI に載ったものは、下の表のとおりである。** + +| 足したもの | それまでの状態 | +| --- | --- | +| `Graphics` レーン | **ローカル専用**。`[Category("Graphics")]` のテストは CI から完全に見えなかった | +| `DnnEditMode` / `DnnStandalone` レーン | **どの workflow も `OCVU_PROFILE_DNN` を立てていなかった**。Unity の中で dnn の assembly がコンパイルされ、IL2CPP の stripping を生き延びることは人が手で 1 回確かめたきりだった | +| `tarball` job | `dev.ps1 test-unity-tarball` は M3 から在るのに、**どの workflow からも走っていなかった** | +| `benchmarks` job | `dev.ps1 benchmark` も同じ。しかも唯一 Unity を持つ開発機では内部の `test-unity-player` がハングするので**完走しない** | + +**1 度消して、レビューで戻した検査が 1 つある。** +`CiVisibilityTests`(「`[Category("Graphics")]` が付いたテストの台帳」)は、 +その class 自身の docstring が「graphics レーンを **CI に配線できたら**、 +この一覧は空にでき、そのとき検査ごと消してよい」と書いていた。配線したので +消した —— **が、削除の条件はそれでは足りなかった。** + +`Unity Graphics (Linux)` は**必須チェックではない**ので、 +`[Category("Graphics")]` を付ける行為は「CI から完全に消える」から +**「必須レーンから、赤くても merge を止めないレーンへ無言で移る」**に +変わっただけで、**守るべき性質は残っている。** 正しい削除の条件は +**「そのレーンが merge を止めるようになること」**である。 +docstring をそう書き換えたうえで、検査は戻した。 + +**この誤りは条件文の読み違えであって、実測の誤りではない** —— +配線されたことは本当である。**「CI が見ている」と「CI が止める」を +取り違えると、こういう形で守りが 1 段落ちる。** + +**必須チェックへ昇格した(2026-09-11、所有者の判断)。** + +足した 5 本(`Unity Graphics (Linux)` / `Unity DnnEditMode (Linux)` / +`Unity DnnStandalone (Linux)` / `UPM tarball install (Linux)` / +`Publish the benchmarks`)を必須チェックにした。必須は **24 → 29 本**、 +必須でないものは **18 → 13 本**になる(残るのは `Plugin` 5 本・ +`Publish the release`・集約 `CodeQL`・game-ci の結果 check 6 本)。 + +**これはこのリポジトリの手順からの逸脱である。それを記録しておく。** +これまでの昇格はすべて「安定して緑になったのを見てから」で、Web の 3 本は +#63 から #74 まで 11 本の PR で緑を見てから昇格した。**今回の実績は +1 ブランチ上で多くても 3 run である**(`Graphics` と dnn の 2 レーンが +3 run、`tarball` が 2 run、`benchmarks` は 1 run)。**所有者がその差を +承知のうえで、いま昇格させると決めた。** 見込める帰結は 2 つ: + +- **良い側**: 足したレーンが赤ければ merge が止まる。**これが昇格の目的で + ある** —— `tarball` が赤ければ「導入できない tarball」は入らないし、 + `DnnStandalone` が赤ければ stripping が dnn の宣言を消した状態は入らない +- **悪い側**: **これらのレーンがフレークなら、main が固まる。** + 実績が浅いぶん、そのときの原因切り分けは「新しいレーンが不安定なのか、 + 本当の欠陥なのか」から始まる。そうなったら、**必須から外すのは + `gh api ... /protection/required_status_checks` の 1 回の PATCH で戻せる** + +**game-ci の結果 check(`* results`)は昇格させない。** PR では `neutral` に +なり、**GitHub は neutral を合格として通す**ので、必須にしても止める力は +増えない(`EditMode results` / `Standalone results` を必須にしていない +のと同じ理由である)。 + +**`benchmarks` job に `if: ${{ !cancelled() }}` を足した。** 昇格と同じ +commit で入れてある —— 素の `needs: unity` だと、依存が落ちた瞬間に +この job は **skip** になり、**skip は required check を通す。** +必須にしたまま素の `needs:` を残すと、「依存が落ちているのに緑と同じ +意味を持つ」状態を自分で作ることになる。 + +**昇格の順序に注意が要る。** 必須にした 5 本は**この変更が入った +workflow でしか作られない**ので、**この PR が main に入る前に main から +切った branch には現れず、その PR は永久に待たされる。** +(`CLAUDE.md` が「PR で起動しない workflow は必須にできない —— 当たらない +PR では check が現れず、必須にすると永久に merge できない」と書いている +のと同じ形である。)今回は開いている PR が 1 本だけで、そこでは 5 本とも +緑だったので踏んでいない。 + +**閉じなかったものも書く。** + +- **Player(IL2CPP)側の GPU 経路は閉じていない。** game-ci の + `run_tests.sh` は Standalone Player を + `xvfb-run -a -e /dev/stdout ... -batchmode -nographics` で起動しており、 + **`-nographics` は action の中に書かれていてこちらからは外せない。** + したがって `AsyncGPUReadback` が IL2CPP で動くかは、いまの構成では + 確かめる術が無い(Editor では動くことが分かった、が上限である) +- **実機は変わらず 1 度も動かしていない。** Android / iOS / dnn のいずれについても、 + 実機で動かした実績は無いままである + +**この 1 件から取れる一般的な教訓は、`prove-a-check-works` skill に +「『原理的に無理』と書いた前提を、誰も測っていないことがある」として +入れてある。** 個別の経緯はこの節が持つので、そちらには再掲しない。 + +--- + ## 担当が無かった制約(2026-08-29 に全件割り当て、うち 1 件は M3.5 で解消) **M0〜M3 を完了し v0.1.1 を配ったあとで「できないこと」を数え直したときに見つかった diff --git a/tests/UnityProject/Assets/Tests/EditMode/CiVisibilityTests.cs b/tests/UnityProject/Assets/Tests/EditMode/CiVisibilityTests.cs index a585c2f..89a660e 100644 --- a/tests/UnityProject/Assets/Tests/EditMode/CiVisibilityTests.cs +++ b/tests/UnityProject/Assets/Tests/EditMode/CiVisibilityTests.cs @@ -3,8 +3,8 @@ using NUnit.Framework; /// -/// [Category("Graphics")] が付いたテストの集合を、CI から見えないテストの -/// 台帳として名指しで固定する(M7a Task 2 のレビュー指摘)。 +/// [Category("Graphics")] が付いたテストの集合を、**merge を止めないレーンに +/// しか居ないテスト**の台帳として名指しで固定する(M7a Task 2 のレビュー指摘)。 /// /// **`Graphics` と `!Graphics` は補集合なので、実行時の和集合は常に完全である。** /// `test-unity-editmode` と `test-unity-tarball` が `-testCategory '!Graphics'`、 @@ -12,16 +12,27 @@ /// 自体に穴は無い(`!Graphics` 側が 2 レーンに増えても、補集合の性質は /// 変わらない)。 /// -/// **本当の穴は「一方のレーン(graphics)が CI に配線されていない」ことである。** -/// `test-unity-graphics` はローカル専用で、`ci-unity.yml` からは呼ばれない。 -/// つまり `[Category("Graphics")]` を付けた瞬間、そのテストは CI から -/// 完全に見えなくなる —— 誰も赤くならない。 +/// **本当の穴は「一方のレーン(graphics)の側が弱いこと」である。** /// -/// **この一覧は「CI で走らない EditMode のテスト」である。** ここに -/// 載っているものは CI から見えない。**列挙そのものが契約である** —— -/// 短いほうがよく、伸びたときに人が気づくべきものだから、あえて名前で持つ。 -/// graphics レーンを CI に配線できたら、この一覧は空にでき、そのとき -/// この検査ごと消してよい。 +/// **2026-09-11 に穴は 1 段小さくなった。** それまで `test-unity-graphics` は +/// ローカル専用で `ci-unity.yml` からは呼ばれず、`[Category("Graphics")]` を +/// 付けた瞬間そのテストは **CI から完全に見えなくなっていた**。いまは +/// `ci-unity.yml` に `Graphics` レーンが在るので、CI は見る。 +/// +/// **しかし `Unity Graphics (Linux)` は必須チェックではない。** したがって +/// `[Category("Graphics")]` を付ける行為は、いまも **必須レーン +/// (`Unity EditMode (Linux)`)から、赤くても merge を止めないレーンへ +/// そのテストを無言で移す。** 「CI から消える」から「CI が止めなくなる」に +/// 程度が下がっただけで、**性質は残っている** —— +/// `CLAUDE.md` の「CI が『見ている』ことと『止める』ことは別である」。 +/// +/// **この一覧は「merge を止めないレーンにしか居ない EditMode のテスト」である。** +/// **列挙そのものが契約である** —— 短いほうがよく、伸びたときに人が +/// 気づくべきものだから、あえて名前で持つ。**`Unity Graphics (Linux)` を +/// 必須チェックへ昇格できたら、この一覧は空にでき、そのときこの検査ごと +/// 消してよい** —— 削除の条件は「CI に配線されること」ではなく +/// **「merge を止めるようになること」**である(2026-09-11 に、配線だけを +/// 根拠に 1 度消してレビューに差し戻された)。 /// /// **走査するのはこの assembly(EditMode)だけである。** 他の test /// assembly に `[Category("Graphics")]` が付いても、ここは気づかない —— @@ -40,7 +51,9 @@ public class CiVisibilityTests { private const string GraphicsCategoryName = "Graphics"; - /// [Category("Graphics")] を持つ(= CI から見えない)テストの全量。 + /// + /// [Category("Graphics")] を持つ(= merge を止めないレーンにしか居ない)テストの全量。 + /// private static readonly string[] ExpectedGraphicsOnlyTests = { "GraphicsTests.AGraphicsDeviceIsPresent", @@ -89,8 +102,8 @@ bool HasGraphicsCategory(MethodInfo m) => Assert.IsEmpty(unexpectedlyGraphicsOnly, "一覧に無いものが増えた —— 新たに [Category(\"Graphics\")] が付き、" + - "CI から見えなくなったテスト: " + string.Join(", ", unexpectedlyGraphicsOnly) + - "。CI から消えてよいなら ExpectedGraphicsOnlyTests に追記すること。"); + "必須レーンから外れたテスト: " + string.Join(", ", unexpectedlyGraphicsOnly) + + "。merge を止めないレーンへ移してよいなら ExpectedGraphicsOnlyTests に追記すること。"); Assert.IsEmpty(noLongerGraphicsOnly, "一覧にあるものが消えた —— [Category(\"Graphics\")] が外れたか、" + diff --git a/tests/UnityProject/Assets/Tests/EditMode/GraphicsTests.cs b/tests/UnityProject/Assets/Tests/EditMode/GraphicsTests.cs index 06f71ea..146d7e9 100644 --- a/tests/UnityProject/Assets/Tests/EditMode/GraphicsTests.cs +++ b/tests/UnityProject/Assets/Tests/EditMode/GraphicsTests.cs @@ -14,9 +14,14 @@ /// として切り出してあるので Player 側に対(`RenderTexturePlayerTests`)を /// 持てるが、`GraphicsChecks` にはその切り出しが無く、対を作りようがない。 /// -/// **`test-unity-graphics` レーンからだけ走る。** 既存の EditMode / -/// PlayMode レーンは `-nographics` で起動するため、ここに置いた検査は -/// 常に失敗する(`GL.Clear` が効かず 205,205,205 が返る、実測)。 +/// **`-nographics` を付けないレーンからだけ走る。** ローカルの +/// `test-unity-graphics` と、**CI の `Graphics` レーン**(`ci-unity.yml`、 +/// 2026-09-11 から)の 2 つである。既存の EditMode / PlayMode レーンは +/// `-nographics` で起動するため、ここに置いた検査は常に失敗する +/// (`GL.Clear` が効かず 205,205,205 が返る、実測)。 +/// **CI の `Graphics` レーンは必須チェックではない** —— +/// `CiVisibilityTests` がその帰結(ここに置いたテストは merge を止めない)を +/// 台帳として固定している。 /// /// **`[Category("Graphics")]` で振り分ける。** 改訂は /// `-testFilter 'GraphicsChecks'` と書いていたが、それは検証本体 diff --git a/tests/UnityProject/Assets/Tests/EditMode/ProfileGatingTests.cs b/tests/UnityProject/Assets/Tests/EditMode/ProfileGatingTests.cs index 40bc71a..2dbae3d 100644 --- a/tests/UnityProject/Assets/Tests/EditMode/ProfileGatingTests.cs +++ b/tests/UnityProject/Assets/Tests/EditMode/ProfileGatingTests.cs @@ -18,21 +18,22 @@ /// (prove-a-check-works skill。M4 で .meta のキー名がまさにこれで、 /// 自前パースは通り、Unity に問う検査だけが落とした)。 /// -/// **ただしこの一群が自動で見られるのは「切れている」方向だけである。** -/// define が立っていない状態で assembly が現れないことは毎回確かめるが、 -/// **define を立てれば現れること(正の方向)は、ここでは確かめていない** -/// —— それには define を変えて Unity をもう一度走らせる必要があり、 -/// 1 回の EditMode 実行では原理的に届かない。正の方向は人が手で確かめる: +/// **両方向を見る。1 回の実行では届かないので、レーンを 2 つに分けてある。** +/// define が立っていない状態で assembly が現れないことは既定のレーンが、 +/// **define を立てれば現れること(正の方向)は `ci-unity.yml` の +/// `DnnEditMode` レーン**が確かめる —— あちらは /// `ProjectSettings.asset` の `scriptingDefineSymbols` に -/// `Standalone: OCVU_PROFILE_DNN` を置いて `dev.ps1 test-unity-editmode` を -/// 走らせ、`tests/UnityProject/Library/ScriptAssemblies/` に -/// `CvUnity.Interop.Dnn.dll` / `CvUnity.Tests.Shared.Dnn.dll` / -/// `CvUnity.Dnn.dll` が現れることを見る(最後の実測は 2026-09-09、**本物の -/// `bindings/spec/dnn.json` から生成した公開 API を含む** —— この 3 つ目は -/// M7c Task 4 で足した。この実行では上の「切れている」方向の 3 件が -/// 前提を欠いて赤くなる ―― defines が既に `OCVU_PROFILE_DNN` を含むため -/// 意図どおりで、それ以外の全テストは緑のままだった。確認後は -/// `scriptingDefineSymbols` を空 `{}` へ戻す)。 +/// `Standalone: OCVU_PROFILE_DNN` を書いてから Unity を走らせる。 +/// **1 回の EditMode 実行では原理的に届かない**というのは正しく、 +/// 届かせる方法は実行を分けることだけである。 +/// +/// **「どちらの分岐を通ったか」は出力で要求する。** 判定を +/// 「無ければ無い/有れば有る」にすると、**どちらのレーンでもテスト名は +/// 同じように Passed で並ぶ** —— 名前だけを要求する検査は、define を +/// 立て損ねたレーンを緑のまま通す。だから +/// が通った側を +/// `TestContext.WriteLine` に書き、CI は +/// `assert-unity-results.ps1 -RequireOutput` でその行を要求する。 /// /// **「切れている」だけを見る検査は、綴り間違いと区別が付かない。** /// `defineConstraints` の値を打ち間違えても、この 5 件は全部緑になる —— @@ -69,30 +70,68 @@ public void TheStandardInteropAssemblyIsAlwaysCompiled() } /// - /// **define が無ければ dnn の assembly はコンパイルされない。** + /// **dnn の assembly が、define の有無と一致していること。** /// /// これが roadmap の決定 2(「dnn が入らないビルドで参照が壊れない」)の - /// 実体である —— 実行時に EntryPointNotFoundException が出ることではなく、 - /// **参照するコードがビルドを通らないこと。** + /// 実体である —— 実行時に EntryPointNotFoundException が出ないことではなく、 + /// **参照するコードがそもそもビルドに入らないこと。** + /// + /// **両方向を見る。** 以前この一群は「define が立っていない状態で + /// assembly が現れない」だけを見ており、同 class の docstring が + /// 「define を立てれば現れること(正の方向)は 1 回の EditMode 実行では + /// 原理的に届かない」と正しく断っていた。**届かせる方法は実行を 2 つに + /// 分けることだけで**、`ci-unity.yml` の `DnnEditMode` レーンがそれを担う + /// (ProjectSettings.asset に define を書いてからもう一度走らせる)。 + /// 判定を「無ければ無い/有れば有る」に変えたことで、**同じテストが + /// どちらのレーンでも load-bearing になる。** /// - [Test] - public void TheDnnInteropAssemblyIsAbsentWithoutItsDefine() + private static bool DnnDefineIsOn { - var defines = UnityEditor.PlayerSettings.GetScriptingDefineSymbols( - UnityEditor.Build.NamedBuildTarget.Standalone); - - // このプロジェクトは既定で OCVU_PROFILE_DNN を立てていない。 - // **前提が崩れたら、この検査は何も見ていないので落とす。** - Assert.That(defines, Does.Not.Contain(DnnDefine), - "このテストは OCVU_PROFILE_DNN が立っていないことを前提にしている"); + get + { + var raw = UnityEditor.PlayerSettings.GetScriptingDefineSymbols( + UnityEditor.Build.NamedBuildTarget.Standalone); + // **部分一致で見ない。** OCVU_PROFILE_DNN_SOMETHING のような別の + // 記号を立てただけで「立っている」と読んでしまう。 + return raw.Split(';').Select(d => d.Trim()).Contains(DnnDefine); + } + } + /// + /// define の状態に応じて、その assembly が在る/無いことを要求する。 + /// **どちらの分岐を通ったかを出力に残す** —— テストが通ったことと、 + /// 意図した側を確かめたことは別である(`assert-unity-results.ps1` の + /// -RequireOutput が CI でこれを要求する)。 + /// + private static void AssertTheAssemblyMatchesTheDefine(string assembly, string what) + { var names = CompilationPipeline.GetAssemblies(AssembliesType.Editor) .Select(a => a.name).ToList(); - Assert.That(names, Does.Not.Contain(DnnAssembly), - "define が無いのに dnn の assembly がコンパイルされている。" + - "defineConstraints が効いていない"); + // **0 件を「違反なし」と読まない。** 走査が壊れていれば + // Does.Not.Contain は常に真になる。 + Assert.IsNotEmpty(names, "assembly が 1 つも拾えていない。走査が壊れている"); + + if (DnnDefineIsOn) + { + Assert.Contains(assembly, names, + $"OCVU_PROFILE_DNN を立てたのに {what} がコンパイルされていない。" + + "defineConstraints の綴りか、profile の配線が壊れている"); + TestContext.WriteLine( + $"OCVU_PROFILE_DNN is defined and the dnn assemblies are compiled [{assembly}]"); + } + else + { + Assert.That(names, Does.Not.Contain(assembly), + $"define が無いのに {what} がコンパイルされている。defineConstraints が効いていない"); + TestContext.WriteLine( + $"OCVU_PROFILE_DNN is not defined and {assembly} is absent"); + } } + [Test] + public void TheDnnInteropAssemblyMatchesTheDefine() + => AssertTheAssemblyMatchesTheDefine(DnnAssembly, "dnn の Interop assembly"); + /// /// **asmdef のファイル自体は在ること。** /// @@ -116,55 +155,36 @@ public void TheDnnAsmdefExistsOnDisk() /// D8 が新設した 2 つ目の define 制約付き assembly を見ていない。 /// [Test] - public void TheDnnSharedTestAssemblyIsAbsentWithoutItsDefine() - { - var defines = UnityEditor.PlayerSettings.GetScriptingDefineSymbols( - UnityEditor.Build.NamedBuildTarget.Standalone); - - // このプロジェクトは既定で OCVU_PROFILE_DNN を立てていない。 - // **前提が崩れたら、この検査は何も見ていないので落とす。** - Assert.That(defines, Does.Not.Contain(DnnDefine), - "このテストは OCVU_PROFILE_DNN が立っていないことを前提にしている"); - - var names = CompilationPipeline.GetAssemblies(AssembliesType.Editor) - .Select(a => a.name).ToList(); - Assert.That(names, Does.Not.Contain(DnnSharedTestAssembly), - "define が無いのに CvUnity.Tests.Shared.Dnn がコンパイルされている。" + - "defineConstraints が効いていない"); - } + public void TheDnnSharedTestAssemblyMatchesTheDefine() + => AssertTheAssemblyMatchesTheDefine(DnnSharedTestAssembly, "CvUnity.Tests.Shared.Dnn"); /// - /// **dnn の公開 API 層も、define が無ければコンパイルされない。** + /// **dnn の公開 API 層も、同じ define と一致していること。** /// /// Interop.Dnn だけを切っても足りない —— それを参照する /// CvUnity.Dnn が残ると、参照先を失って**利用者のプロジェクトが /// コンパイルエラーになる。** /// [Test] - public void TheDnnPublicApiAssemblyIsAlsoAbsentWithoutItsDefine() - { - var defines = UnityEditor.PlayerSettings.GetScriptingDefineSymbols( - UnityEditor.Build.NamedBuildTarget.Standalone); - - // このプロジェクトは既定で OCVU_PROFILE_DNN を立てていない。 - // **前提が崩れたら、この検査は何も見ていないので落とす。** - Assert.That(defines, Does.Not.Contain(DnnDefine), - "このテストは OCVU_PROFILE_DNN が立っていないことを前提にしている"); - - var names = CompilationPipeline.GetAssemblies(AssembliesType.Editor) - .Select(a => a.name).ToList(); - Assert.That(names, Does.Not.Contain(DnnPublicApiAssembly), - "define が無いのに dnn の公開 API assembly がコンパイルされている"); - } + public void TheDnnPublicApiAssemblyMatchesTheDefine() + => AssertTheAssemblyMatchesTheDefine(DnnPublicApiAssembly, "dnn の公開 API assembly"); /// /// **綴りを見る。** /// - /// 上の 4 件はどれも「define が立っていない状態で assembly が現れない」を - /// 見ている。**`defineConstraints` の値を打ち間違えても、それは全部真である** + /// **この検査の役目は 2026-09-11 に変わった。** それまで上の一群は + /// 「define が立っていない状態で assembly が現れない」だけを見ており、 + /// **`defineConstraints` の値を打ち間違えてもそれは全部真だった** /// —— 存在しない define は決して立たないので、その assembly は - /// **どんな define を立てても永久に現れない。** 「切ってある」と - /// 「壊れている」の区別が付かない。 + /// どんな define を立てても永久に現れない。「切ってある」と + /// 「壊れている」の区別が付かなかった。 + /// + /// いまは `AssertTheAssemblyMatchesTheDefine` が両方向を見るので、 + /// **`DnnEditMode` レーン(define を立てる側)では綴り違いを + /// `Assert.Contains` が直接捕まえる。** それでもこの検査を残すのは、 + /// **既定のレーンだけを走らせたときに何も捕まらない**からである —— + /// `DnnEditMode` は必須チェックではないので、綴りが壊れたまま + /// merge できる経路が残る。ここはどのレーンでも同じことを見る。 /// /// ここは asmdef が実際に綴っている値を読み、この class が使う定数と /// 突き合わせる。**Unity 自身のパーサ()に読ませる** diff --git a/tests/UnityProject/Assets/Tests/PlayMode/AbiSurfaceDnnPlayerTests.cs b/tests/UnityProject/Assets/Tests/PlayMode/AbiSurfaceDnnPlayerTests.cs index 9e525c1..c8db05a 100644 --- a/tests/UnityProject/Assets/Tests/PlayMode/AbiSurfaceDnnPlayerTests.cs +++ b/tests/UnityProject/Assets/Tests/PlayMode/AbiSurfaceDnnPlayerTests.cs @@ -23,10 +23,13 @@ /// この `#if` で自分を守る —— define が立っていなければ、このクラスは /// テストを 1 つも持たない空のクラスになる。 /// -/// **CI はどのレーンも `OCVU_PROFILE_DNN` を立てない**ので、このテストは -/// 自動実行されない。`ProfileGatingTests` が確立した「正の方向は人が手で -/// 確かめる」という規約と同じ形である。手動での実測手順と結果は -/// `docs/roadmap.md` の `### M7c の判定`「穴を隠さず書く」にある。 +/// **`ci-unity.yml` の `DnnStandalone` レーンがこれを走らせる。** +/// そのレーンだけが `ProjectSettings.asset` に `OCVU_PROFILE_DNN` を書いてから +/// IL2CPP Player を建てる。**このテストが自動実行されるようになったのは +/// そのレーンを足してからで**、それまでは人が手で 1 回確かめたきりだった +/// (M7c が残した穴。`docs/roadmap.md` の `### M7c の判定`)。 +/// レーンは `-RequireTest` でこのテスト名を名指ししているので、 +/// **assembly から外れれば CI が赤くなる。** /// public class AbiSurfaceDnnPlayerTests { diff --git a/tests/UnityProject/Assets/Tests/PlayMode/BenchmarkRunner.cs b/tests/UnityProject/Assets/Tests/PlayMode/BenchmarkRunner.cs index c628f7f..5f038b4 100644 --- a/tests/UnityProject/Assets/Tests/PlayMode/BenchmarkRunner.cs +++ b/tests/UnityProject/Assets/Tests/PlayMode/BenchmarkRunner.cs @@ -101,8 +101,21 @@ public IEnumerator MeasureFirstPInvoke() Assert.Greater(version, 0, "ABI version が取れていない"); - long micros = sw.ElapsedTicks * 1_000_000L / Stopwatch.Frequency; - TestContext.WriteLine($"OCVU_BENCH: first_pinvoke={micros}"); + // **ここだけナノ秒で出す。** マイクロ秒では CI の Linux Player で + // **実測 0 になった**(run 34612557397)。収集する側 + // (tools/run-benchmarks.ps1)は 0 を「測定が効いていない」として + // 落とすので、そのままでは benchmark を CI で集められない。 + // + // **0 が出たのは測定が壊れていたからではない。** 上の docstring が + // 書いているとおり、この assembly の別のテストが先に同じ P/Invoke を + // 呼んでいるので、ここで測っているのは**既に読み込まれた後の + // 1 回の呼び出し**であり、それは 1 マイクロ秒に満たない。 + // **分解能が足りないだけで、値そのものは正しい。** + // + // 単位をキー名に書く —— 他の項目はマイクロ秒なので、 + // 名前で区別できないと latest.json を読む人が桁を取り違える。 + long nanos = sw.ElapsedTicks * 1_000_000_000L / Stopwatch.Frequency; + TestContext.WriteLine($"OCVU_BENCH: first_pinvoke_ns={nanos}"); yield return null; } diff --git a/tools/dev.ps1 b/tools/dev.ps1 index 7100109..8c24658 100644 --- a/tools/dev.ps1 +++ b/tools/dev.ps1 @@ -41,7 +41,35 @@ param( クロスの対象を渡す意味が無い(渡されたら止める)。 #> [ValidateSet('windows-x64', 'macos-arm64', 'linux-x64', 'android-arm64', 'ios-arm64', 'web-wasm')] - [string]$Platform + [string]$Platform, + + <# + **`test-unity-tarball` の準備だけを行い、Unity を起動しない。** + + CI では Unity を起動するのが game-ci の action であって `dev.ps1` では + ない(理由は .github/workflows/ci-unity.yml の冒頭にある)。このレーンは + **使い捨ての Unity プロジェクトを作る**ところが本体なので、その作り方を + CI 側に書き写すと「導入を確かめた tarball」と「実際に配る tarball」が + 別物になりうる —— `pack-upm-tarball.ps1` を 1 つに寄せたのと同じ理由で、 + **プロジェクトの作り方も 1 つに寄せる。** + + このレーンは **M4 の時点から「CI に載せるのは別作業である」と + roadmap に書かれたまま残っていた**。載せていなかった間に実際に + 壊れており(M4 で binary の数が 3 と直書きされ iOS の .a を binary と + 認めなかった/M7a で Graphics の除外が抜けた)、どちらも + **無関係な作業の途中で 1 度手で回すまで誰も知らなかった。** + + `-WorkDir` は必須にしてある —— 省略できると一時ディレクトリに作って + そのまま残し、**誰も消さないものが毎回積もる。** + #> + [switch]$PrepareOnly, + + <# + `-PrepareOnly` の出力先。**CI ではワークスペースの中でなければならない** + —— game-ci はコンテナにワークスペースを mount するので、その外に作った + プロジェクトはコンテナから見えない。 + #> + [string]$WorkDir ) $ErrorActionPreference = 'Stop' @@ -648,8 +676,13 @@ function Test-UnityEditMode { `-nographics` を渡さないことと、`GraphicsTests`(`[Category("Graphics")]`) だけを対象にすることである。 - **CI には配線しない**(controller の裁定)。game-ci の Linux コンテナに - グラフィックス装置が在るかは未確認で、投げてみるまで分からない。 + **2026-09-11 に CI へ配線した。** それまでここには「CI には配線しない + (controller の裁定)。game-ci の Linux コンテナにグラフィックス装置が + 在るかは未確認で、投げてみるまで分からない」と書いてあった —— + **投げてみたら在った。** `ci-unity.yml` の `Graphics` レーンが同じ + `-testCategory` で走る(game-ci の Editor は `-nographics` を付けずに + xvfb の下から起動する)。**このローカルのレーンは残す** —— CI と + 起動の仕方が違うので、同じ判定 script を通る別経路の突き合わせになる。 #> function Test-UnityGraphics { Build-Native @@ -706,7 +739,16 @@ function Test-UnityGraphics { かなり遅い。CI とローカルの手動確認のためのレーンで、`test` には含めない。 #> function Test-UnityTarball { - Build-Native + <# + **-PrepareOnly では native をビルドしない。** + + CI ではこの直前に、**Unity が動くコンテナと同じ世代のイメージの中で** + plugin をビルドしてある。ここで host 側(ubuntu-24.04, glibc 2.39)の + 道具でビルドし直すと、**より新しい glibc を要求する .so で静かに + 上書きしてしまう** —— v0.1.0 が実際に配ってしまった欠陥そのもので、 + しかも「ビルドは成功する」ので誰も赤くならない。 + #> + if (-not $PrepareOnly) { Build-Native } # 他 platform の木を渡されたら重ねる。ここで初めて 3 platform が揃う。 $allPlatforms = $false @@ -739,11 +781,16 @@ function Test-UnityTarball { } } - $unity = Get-UnityEditorPath + # **-PrepareOnly では Unity を探さない。** CI の runner に Unity は無く + # (コンテナの中にしかない)、探しに行けばここで落ちる。 + $unity = if ($PrepareOnly) { $null } else { Get-UnityEditorPath } New-Item -ItemType Directory -Force -Path $ResultsDir | Out-Null - $work = Join-Path ([System.IO.Path]::GetTempPath()) ("ocvu-tarball-" + [guid]::NewGuid().ToString('n')) + $work = if ($PrepareOnly) { $WorkDir } else { + Join-Path ([System.IO.Path]::GetTempPath()) ("ocvu-tarball-" + [guid]::NewGuid().ToString('n')) + } New-Item -ItemType Directory -Force -Path $work | Out-Null + $work = (Resolve-Path -LiteralPath $work).Path try { # release.yml と同じ作り方で tarball にする。作り方が違うと、 @@ -862,7 +909,27 @@ function Test-UnityTarball { $manifestPath = Join-Path $project 'Packages/manifest.json' $manifest = Get-Content -LiteralPath $manifestPath -Raw - $tgzUri = 'file:' + ($tgz -replace '\\', '/') + <# + **相対パスで参照する。** 以前は host の絶対パスを書いていたが、 + CI では Unity が**コンテナの中**で動き、ワークスペースは別のパスへ + mount される —— 絶対パスはそこでは解決できない。 + + UPM の `file:` 参照は**プロジェクトの `Packages` フォルダからの相対** + として解決される。実例はこのリポジトリ自身にある: + `tests/UnityProject/Packages/manifest.json` が + `file:../../../Packages/com.ayutaz.opencv-unity-native` で動いている + —— **ただしそれはディレクトリ参照であって tarball ではない。** + tarball を相対 `file:` で参照するのはこのリポジトリで初めての形で、 + **根拠はローカル実測(このマシン、全部入りで `59 passed` / + `resolved from file:../../upm/...tgz`)と CI の run 34613947274** + である。 + + **ローカルでも同じ形にする。** 分けると、CI で通る形をローカルでは + 一度も試していないことになる。 + #> + $packagesDir = Join-Path $project 'Packages' + $relativeTgz = ([System.IO.Path]::GetRelativePath($packagesDir, $tgz)).Replace([char]92, [char]47) + $tgzUri = 'file:' + $relativeTgz $manifest = $manifest -replace '"com\.ayutaz\.opencv-unity-native":\s*"[^"]*"', ('"com.ayutaz.opencv-unity-native": "' + $tgzUri + '"') @@ -913,6 +980,13 @@ function Test-UnityTarball { #> Sync-AllPlatformsMarker -ProjectPath $project + if ($PrepareOnly) { + Write-Host "==> prepared a disposable Unity project at $project" -ForegroundColor Green + # **呼ぶ側(CI)が projectPath として使う。** 最後の 1 行に出す。 + Write-Output $project + return + } + $proc = Start-Process -FilePath $unity -ArgumentList $unityArgs -PassThru -NoNewWindow if (-not $proc.WaitForExit($timeoutMs)) { try { $proc.Kill($true) } catch { } @@ -994,7 +1068,12 @@ function Test-UnityTarball { Write-Host "==> UPM tarball install: $passed passed (resolved from $($entry.version))" -ForegroundColor Green } finally { - Remove-Item -Recurse -Force $work -ErrorAction SilentlyContinue + # **-PrepareOnly のときは消さない。** これから Unity に読ませる + # プロジェクトそのものであり、消す責任は呼ぶ側にある(CI では + # ワークスペースごと捨てられる)。 + if (-not $PrepareOnly) { + Remove-Item -Recurse -Force $work -ErrorAction SilentlyContinue + } } } <# @@ -1284,6 +1363,18 @@ function Test-ManagedProbe { になる —— このリポジトリが繰り返し潰してきた「通るのに何も証明していない」 形そのものである。 #> +foreach ($name in @('PrepareOnly', 'WorkDir')) { + if ($PSBoundParameters.ContainsKey($name) -and $Command -ne 'test-unity-tarball') { + Write-DevFailure "-$name は test-unity-tarball でだけ意味があります(渡されたコマンド: $Command)。" + } +} +if ($PrepareOnly -and -not $WorkDir) { + Write-DevFailure '-PrepareOnly には -WorkDir が要ります(省略すると、誰も消さないプロジェクトが積もる)。' +} +if ($WorkDir -and -not $PrepareOnly) { + Write-DevFailure '-WorkDir は -PrepareOnly と一緒にだけ使えます(Unity を起動するときは一時ディレクトリを使い、必ず消す)。' +} + if ($PSBoundParameters.ContainsKey('Platform') -and $Command -ne 'build') { Write-DevFailure (@( "-Platform は build のときだけ使えます(渡された command: $Command)。" diff --git a/tools/run-benchmarks.ps1 b/tools/run-benchmarks.ps1 index 431b577..844f877 100644 --- a/tools/run-benchmarks.ps1 +++ b/tools/run-benchmarks.ps1 @@ -110,19 +110,80 @@ if ($results.Count -eq 0) { # ここで一括して名指しし、0 の entry があれば exit 1 にする。 $zeroKeys = @($results.Keys | Where-Object { $results[$_] -eq 0 }) if ($zeroKeys.Count -gt 0) { - Write-Error "0 マイクロ秒の entry がある(測定が効いていない): $($zeroKeys -join ', ')" + Write-Error "0 の entry がある(測定が効いていない): $($zeroKeys -join ', ')" exit 1 } +<# + **単位は key の末尾で決まる。** `_ns` で終わる key はナノ秒、それ以外は + マイクロ秒である。 + + **これは 2026-09-11 のレビュー指摘(I-5 / I-2)で足した。** それまで + payload は `unit = 'microseconds per call'` を**全 entry 共通**で持って + おり、`first_pinvoke_ns`(ナノ秒)が入った時点で **publish する成果物が + 嘘をついていた。** しかも key 名に単位を書いた側の意図(「名前で区別 + できないと桁を取り違える」)は、**より強い信号である `unit` フィールドに + 打ち消されていた** —— 取り違えは解消せず、場所が移っただけである。 + + **どの key がどちらかを列挙しない。** 列挙すると、次に `_ns` の項目を + 足した人がここを直し忘れる。**規約(接尾辞)で決める。** +#> +function Get-BenchmarkUnit { + param([Parameter(Mandatory)][string] $Key) + if ($Key -match '_ns$') { 'nanoseconds per call' } else { 'microseconds per call' } +} +function Get-BenchmarkUnitSuffix { + param([Parameter(Mandatory)][string] $Key) + if ($Key -match '_ns$') { 'ns' } else { 'us' } +} + +<# + **扱える接尾辞は `_ns` だけである。それ以外の時間単位は、黙って + マイクロ秒として publish せずに落とす。** + + 規約を守っているのは publish する側だけで、`OCVU_BENCH:` を出す C# 側 + (`BenchmarkRunner` / `GraphicsBenchmarkRunner`)には何の強制も無い —— + 将来 `*_ms` を足すと、**1000 倍ずれた数字が「マイクロ秒」として世に出る。** + + **ここは意図的に列挙である。** 「知らない接尾辞を全部拒む」形にすると + `texture2d_to_mat`(`_mat`)のような正当な key まで落ちる。拒むのは + **時間単位に見えるのに扱えないもの**だけで、既定(マイクロ秒)は + `docs/performance.md` に書いてある。 +#> +$unhandledUnitSuffixes = @('_ms', '_us', '_s', '_sec', '_msec', '_usec', '_nsec', '_micros', '_millis', '_nanos') +$badUnitKeys = @($results.Keys | Where-Object { + $k = $_ + @($unhandledUnitSuffixes | Where-Object { $k.EndsWith($_) }).Count -gt 0 +}) +if ($badUnitKeys.Count -gt 0) { + Write-Error ("扱えない単位の接尾辞を持つ key がある(マイクロ秒として publish しない): " + + "$($badUnitKeys -join ', ')。扱えるのは '_ns' だけで、" + + "接尾辞が無ければマイクロ秒として扱う") + exit 1 +} + +# **単位つきの表に組み替える。** 値だけの map を残さないのは、 +# 読む側が単位を知らずに値を取れる形を publish しないためである。 +$entries = [ordered]@{} +foreach ($k in $results.Keys) { + $entries[$k] = [ordered]@{ + value = $results[$k] + unit = Get-BenchmarkUnit -Key $k + } +} + # **測った環境を必ず併記する**(設計 §6)。数字だけ残すと、 # 測っていない環境についても言ったことになる。 $payload = [ordered]@{ measuredAt = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') os = [System.Runtime.InteropServices.RuntimeInformation]::OSDescription arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() - unit = 'microseconds per call' + # **全 entry 共通の `unit` は持たない。** 単位は entry ごとに在る + # (上の Get-BenchmarkUnit)。共通の 1 つを置くと、単位の違う項目が + # 1 つ入った瞬間に payload 全体が嘘になる。 + unitNote = 'unit は entry ごとに持つ。key が _ns で終わればナノ秒、それ以外はマイクロ秒' note = 'CI ランナーまたは開発機での実測。利用者の端末の数字ではない' - results = $results + results = $entries } $dir = Split-Path -Parent $OutPath @@ -132,4 +193,6 @@ if ($dir -and -not (Test-Path -LiteralPath $dir)) { $payload | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $OutPath -Encoding utf8 Write-Host "==> benchmark: $($results.Count) 件を $OutPath へ書いた" -foreach ($k in $results.Keys) { Write-Host " $k = $($results[$k]) us" } +foreach ($k in $results.Keys) { + Write-Host " $k = $($results[$k]) $(Get-BenchmarkUnitSuffix -Key $k)" +} diff --git a/tools/tests/Benchmarks.Tests.ps1 b/tools/tests/Benchmarks.Tests.ps1 index a81d1eb..51792a3 100644 --- a/tools/tests/Benchmarks.Tests.ps1 +++ b/tools/tests/Benchmarks.Tests.ps1 @@ -11,6 +11,28 @@ #> Set-StrictMode -Version Latest $ErrorActionPreference = 'Continue' + +<# + **未処理の例外を「静かな合格」にしない。** + + このファイルは $ErrorActionPreference = 'Continue' で走る(probe した + コマンドの非終了エラーで中断しないため)。その結果、**検査の途中で + 終了エラーが出ると、残りの assertion が 1 つも走らないまま + `$failures` は 0 のままになり、末尾の判定を通って exit 0 になる。** + + 2026-09-11 に実測で踏んだ: Benchmarks.Tests.ps1 に無いプロパティを + 読む assertion を書いたところ、PropertyNotFoundException が表示された + うえで `==> Benchmarks.Tests: OK` と出て exit 0 になった。**負の対照を + 取ろうとして、対照そのものが素通りした。** + + 先例は tools/tests/PackageRelease.Tests.ps1 で、同じ形の trap を持つ。 +#> +trap { + [Console]::Error.WriteLine("`n未処理の例外でテストが中断しました:") + [Console]::Error.WriteLine($_.ToString()) + [Console]::Error.WriteLine($_.ScriptStackTrace) + exit 1 +} [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new() $repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) @@ -51,9 +73,40 @@ try { & pwsh -NoProfile -File $script -XmlPath $good -OutPath $out 2>&1 | Out-Null Assert-That ($LASTEXITCODE -eq 0) 'a well-formed result XML is accepted' $written = if (Test-Path -LiteralPath $out) { Get-Content -LiteralPath $out -Raw | ConvertFrom-Json } else { $null } - Assert-That ($null -ne $written -and $written.results.copy_to_buffer -eq 42 -and $written.results.startup -eq 7) ` + Assert-That ($null -ne $written -and $written.results.copy_to_buffer.value -eq 42 -and $written.results.startup.value -eq 7) ` 'the measured values reach the published payload' + <# + **単位は entry ごとに正しいこと(2026-09-11 のレビュー I-5 / I-2)。** + + それまで payload は `unit = 'microseconds per call'` を全 entry 共通で + 持っており、ナノ秒の項目(`first_pinvoke_ns`)が 1 つ入った時点で + **publish する成果物が嘘をついていた** —— CI の実測は + `first_pinvoke_ns = 400 us` と表示していた。 + + **混在させて見る。** 片方だけの入力だと、単位を常に片方へ倒す実装でも + 通ってしまう(このリポジトリが繰り返し潰してきた「常に真になる述語」)。 + #> + Remove-Item -LiteralPath $out -Force -ErrorAction SilentlyContinue + $mixed = New-ResultXml 'mixed.xml' @('OCVU_BENCH: copy_to_buffer=42', 'OCVU_BENCH: first_pinvoke_ns=400') + & pwsh -NoProfile -File $script -XmlPath $mixed -OutPath $out 2>&1 | Out-Null + Assert-That ($LASTEXITCODE -eq 0) 'a payload mixing microsecond and nanosecond keys is accepted' + $mixedJson = if (Test-Path -LiteralPath $out) { Get-Content -LiteralPath $out -Raw | ConvertFrom-Json } else { $null } + Assert-That ($null -ne $mixedJson) 'the mixed payload was written (読めなければ以下は空振りする)' + if ($null -ne $mixedJson) { + Assert-That ($mixedJson.results.first_pinvoke_ns.unit -eq 'nanoseconds per call') ` + "a key ending in _ns is published as nanoseconds (saw '$($mixedJson.results.first_pinvoke_ns.unit)')" + Assert-That ($mixedJson.results.copy_to_buffer.unit -eq 'microseconds per call') ` + "a key without _ns is published as microseconds (saw '$($mixedJson.results.copy_to_buffer.unit)')" + # **全 entry 共通の unit を持たないこと。** これが在ると、entry ごとの + # 単位より強い信号として読まれ、単位の違う項目が入った瞬間に嘘になる。 + # **プロパティの有無で見る。** Set-StrictMode の下では、無い + # プロパティを読むと例外になる —— 「$null と等しい」では検査に + # ならず、走らせた側が落ちるだけである(実測)。 + Assert-That (($mixedJson.PSObject.Properties.Name) -notcontains 'unit') ` + 'the payload has no single top-level unit (entry ごとの単位より強い信号を置かない)' + } + # --- **key の書き方がずれると落ちること(レビュー I-2)。** --- # `OCVU_BENCH:` の行は在る(既存の 2 つの門はどちらも満たす)のに、 # 正規表現が key=value を取り出せず $results が空になる形。 @@ -74,6 +127,28 @@ try { "nothing is published when no key=value could be parsed ($($shape.What))" } + <# + **扱えない単位の接尾辞は落ちること。** + + 規約(`_ns` ならナノ秒、それ以外はマイクロ秒)を守っているのは + publish する側だけで、`OCVU_BENCH:` を出す C# 側には強制が無い。 + `*_ms` を足した日に、**1000 倍ずれた数字がマイクロ秒として世に出る。** + #> + foreach ($bad in @('copy_to_buffer_ms', 'startup_sec')) { + Remove-Item -LiteralPath $out -Force -ErrorAction SilentlyContinue + $xml = New-ResultXml ('unit-' + [guid]::NewGuid().ToString('N') + '.xml') @("OCVU_BENCH: $bad=42") + & pwsh -NoProfile -File $script -XmlPath $xml -OutPath $out 2>&1 | Out-Null + Assert-That ($LASTEXITCODE -ne 0) "a key with an unhandled unit suffix ($bad) is rejected" + Assert-That (-not (Test-Path -LiteralPath $out)) "nothing is published for $bad" + } + + # **正当な key は落とさない。** 拒む形が広すぎると、実在の key + # (`texture2d_to_mat` は `_mat` で終わる)まで巻き込む。 + Remove-Item -LiteralPath $out -Force -ErrorAction SilentlyContinue + $legit = New-ResultXml 'legit.xml' @('OCVU_BENCH: texture2d_to_mat=42', 'OCVU_BENCH: first_pinvoke_ns=7') + & pwsh -NoProfile -File $script -XmlPath $legit -OutPath $out 2>&1 | Out-Null + Assert-That ($LASTEXITCODE -eq 0) 'real keys are not caught by the unit-suffix guard' + # --- 0 マイクロ秒は publish しない(既存の門) --- $zero = New-ResultXml 'zero.xml' @('OCVU_BENCH: copy_to_buffer=0') & pwsh -NoProfile -File $script -XmlPath $zero -OutPath $out 2>&1 | Out-Null diff --git a/tools/tests/BindingGenerator.Tests.ps1 b/tools/tests/BindingGenerator.Tests.ps1 index f917cba..3bcdec2 100644 --- a/tools/tests/BindingGenerator.Tests.ps1 +++ b/tools/tests/BindingGenerator.Tests.ps1 @@ -1,6 +1,28 @@ #!/usr/bin/env pwsh Set-StrictMode -Version Latest $ErrorActionPreference = 'Continue' + +<# + **未処理の例外を「静かな合格」にしない。** + + このファイルは $ErrorActionPreference = 'Continue' で走る(probe した + コマンドの非終了エラーで中断しないため)。その結果、**検査の途中で + 終了エラーが出ると、残りの assertion が 1 つも走らないまま + `$failures` は 0 のままになり、末尾の判定を通って exit 0 になる。** + + 2026-09-11 に実測で踏んだ: Benchmarks.Tests.ps1 に無いプロパティを + 読む assertion を書いたところ、PropertyNotFoundException が表示された + うえで `==> Benchmarks.Tests: OK` と出て exit 0 になった。**負の対照を + 取ろうとして、対照そのものが素通りした。** + + 先例は tools/tests/PackageRelease.Tests.ps1 で、同じ形の trap を持つ。 +#> +trap { + [Console]::Error.WriteLine("`n未処理の例外でテストが中断しました:") + [Console]::Error.WriteLine($_.ToString()) + [Console]::Error.WriteLine($_.ScriptStackTrace) + exit 1 +} [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new() $script:failures = @() diff --git a/tools/tests/EmscriptenVersion.Tests.ps1 b/tools/tests/EmscriptenVersion.Tests.ps1 index 4f3ba21..66234c8 100644 --- a/tools/tests/EmscriptenVersion.Tests.ps1 +++ b/tools/tests/EmscriptenVersion.Tests.ps1 @@ -20,6 +20,28 @@ Set-StrictMode -Version Latest $ErrorActionPreference = 'Continue' + +<# + **未処理の例外を「静かな合格」にしない。** + + このファイルは $ErrorActionPreference = 'Continue' で走る(probe した + コマンドの非終了エラーで中断しないため)。その結果、**検査の途中で + 終了エラーが出ると、残りの assertion が 1 つも走らないまま + `$failures` は 0 のままになり、末尾の判定を通って exit 0 になる。** + + 2026-09-11 に実測で踏んだ: Benchmarks.Tests.ps1 に無いプロパティを + 読む assertion を書いたところ、PropertyNotFoundException が表示された + うえで `==> Benchmarks.Tests: OK` と出て exit 0 になった。**負の対照を + 取ろうとして、対照そのものが素通りした。** + + 先例は tools/tests/PackageRelease.Tests.ps1 で、同じ形の trap を持つ。 +#> +trap { + [Console]::Error.WriteLine("`n未処理の例外でテストが中断しました:") + [Console]::Error.WriteLine($_.ToString()) + [Console]::Error.WriteLine($_.ScriptStackTrace) + exit 1 +} [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new() $script:failures = @() diff --git a/tools/tests/NonStandardProfileCompile.Tests.ps1 b/tools/tests/NonStandardProfileCompile.Tests.ps1 index f5f624e..aea17a9 100644 --- a/tools/tests/NonStandardProfileCompile.Tests.ps1 +++ b/tools/tests/NonStandardProfileCompile.Tests.ps1 @@ -1,6 +1,28 @@ #!/usr/bin/env pwsh Set-StrictMode -Version Latest $ErrorActionPreference = 'Continue' + +<# + **未処理の例外を「静かな合格」にしない。** + + このファイルは $ErrorActionPreference = 'Continue' で走る(probe した + コマンドの非終了エラーで中断しないため)。その結果、**検査の途中で + 終了エラーが出ると、残りの assertion が 1 つも走らないまま + `$failures` は 0 のままになり、末尾の判定を通って exit 0 になる。** + + 2026-09-11 に実測で踏んだ: Benchmarks.Tests.ps1 に無いプロパティを + 読む assertion を書いたところ、PropertyNotFoundException が表示された + うえで `==> Benchmarks.Tests: OK` と出て exit 0 になった。**負の対照を + 取ろうとして、対照そのものが素通りした。** + + 先例は tools/tests/PackageRelease.Tests.ps1 で、同じ形の trap を持つ。 +#> +trap { + [Console]::Error.WriteLine("`n未処理の例外でテストが中断しました:") + [Console]::Error.WriteLine($_.ToString()) + [Console]::Error.WriteLine($_.ScriptStackTrace) + exit 1 +} [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new() $script:failures = @() diff --git a/tools/tests/OpenCvConfig.Tests.ps1 b/tools/tests/OpenCvConfig.Tests.ps1 index 76984b8..00fb494 100644 --- a/tools/tests/OpenCvConfig.Tests.ps1 +++ b/tools/tests/OpenCvConfig.Tests.ps1 @@ -958,8 +958,20 @@ if ($markerName) { $declaredRequires = @($unityWorkflow | Where-Object { $_ -match '^\s*requireTest:\s*\S' -and $_ -notmatch "requireTest:\s*''" }) - Assert-That ($declaredRequires.Count -eq 1) ` - "ci-unity.yml declares exactly one non-empty requireTest lane (saw $($declaredRequires.Count))" + # **合計ではなく用途ごとに見る。** + # + # 以前ここは「非空の requireTest はちょうど 1 本」だった。レーンが 1 本 + # しか無かった頃はそれで足りたが、**合計を見る検査は誤った読みに + # 満たされる**(prove-a-check-works skill の「数を見る検査は…」)。 + # いま非空の requireTest は 4 本あり、合計を 4 に書き換えても + # **どの 4 本かは何も保証しない** —— gating を要求する行が消えて + # 別のレーンが 1 本増えれば、合計は 4 のままで緑になる。 + # + # 見るべきは「gating を要求する行がちょうど 1 本ある」ことで、 + # それは次の assertion が持っている。ここは**抽出が空振りしていない** + # ことだけを確かめる。 + Assert-That ($declaredRequires.Count -ge 1) ` + "ci-unity.yml declares at least one non-empty requireTest lane (0 件なら下の検査は空振りする)" Assert-That (@($declaredRequires | Where-Object { $_ -match 'PluginGatingTests' }).Count -eq 1) ` "ci-unity.yml requires the plugin gating tests to have run (saw: $($declaredRequires -join ', '))" @@ -969,8 +981,10 @@ if ($markerName) { $declaredOutputs = @($unityWorkflow | Where-Object { $_ -match '^\s*requireOutput:\s*\S' -and $_ -notmatch "requireOutput:\s*''" }) - Assert-That ($declaredOutputs.Count -eq 1) ` - "ci-unity.yml declares exactly one non-empty requireOutput lane (saw $($declaredOutputs.Count))" + # 上の requireTest と同じ理由で、合計ではなく用途ごとに見る + # (「全 platform を報告させる行がちょうど 1 本」は下の assertion)。 + Assert-That ($declaredOutputs.Count -ge 1) ` + "ci-unity.yml declares at least one non-empty requireOutput lane (0 件なら下の検査は空振りする)" # **数を写さない。正本から導く。** # # ここに `5` と書いていたので、**platform を 6 つにしたときに @@ -985,8 +999,32 @@ if ($markerName) { $platformCount = @([regex]::Matches($binBlock.Groups['body'].Value, "'[^']+'")).Count Assert-That ($platformCount -ge 3) "正本から platform 数を数えられた (got $platformCount)" - Assert-That (@($declaredOutputs | Where-Object { $_ -match "native plugins present: $platformCount \[" }).Count -eq 1) ` - "ci-unity.yml requires the tests to report every platform ($platformCount; saw: $($declaredOutputs -join ', '))" + # **YAML の requireOutput: だけを見ない。** + # + # 2026-09-11 に tarball job が加わり、そちらは matrix を通さず + # `assert-unity-results.ps1 -RequireOutput '...'` に直接書く。**この行は + # `requireOutput:` にも `native plugins present` を書いた側にも数えられず、 + # 古い platform 数が黙って残る場所になっていた**(レビュー指摘 M-1)。 + # + # **この文字列が現れる行を 1 本残らず拾い、全部が正本の数であること**を見る。 + # 「1 本が正しければよい」ではない —— 片方だけ古くなる形が、まさに + # このファイルが何度も記録している失敗である。 + # ここで使えるのは $unityWorkflow(この節の頭で読んだ行の配列)である。 + # $unityWorkflowText はもっと下で定義されるので、ここからは見えない。 + # **要求している行に限る。** 素の部分一致だと、**コメント行 1 本でも + # 「1 本以上ある」を満たせる** —— 本物の要求を全部消してコメントだけ + # 残す形が通る(旧版が持っていた `requireOutput:` という文脈のアンカーを + # 失っていた)。YAML の宣言と PowerShell の引数の両方を受ける。 + $presentLines = @($unityWorkflow | Where-Object { + $_ -match 'native plugins present:' -and $_ -cmatch '(requireOutput:|-RequireOutput)' + }) + Assert-That ($presentLines.Count -ge 1) ` + 'ci-unity.yml requires the all-platform report at least once (0 件なら以下は空振りする)' + foreach ($line in $presentLines) { + Assert-That ($line -match "native plugins present: $platformCount \[") ` + ("ci-unity.yml requires the tests to report every platform " + + "($platformCount; saw: $($line.Trim()))") + } } @@ -1088,47 +1126,140 @@ Assert-That ($literalNegatedCategoryMatches.Count -eq 0) ` # ここで `saw 2` になった)。**リテラル文字列(`'...'`)だけを対象にする** # ことで、`${{ }}` の参照行を構造的に除外する。 $unityWorkflowText = Get-Content -LiteralPath (Join-Path $repoRoot '.github/workflows/ci-unity.yml') -Raw -$ciLiteralLines = @(($unityWorkflowText -split "`r?`n") | Where-Object { - $_ -match "^\s*customParameters:\s*'(?[^']*)'\s*`$" -}) -Assert-That ($ciLiteralLines.Count -eq 2) ` - "ci-unity.yml declares customParameters as a literal string on both matrix lanes (saw $($ciLiteralLines.Count); $($ciLiteralLines -join ' | '))" - -# 2 つのリテラルのうち、非空なのが EditMode 側のはず -# (Standalone は '' を明示している——上の requireTest / requireOutput と -# 同じ「非空を探す」形)。空文字だけになると assert-unity-results.ps1 の -# 照合と同じで「要求したことになっているが何も要求していない」になるので、 -# 非空を要求する。 -$ciCategoryLines = @($ciLiteralLines | Where-Object { $_ -notmatch "customParameters:\s*''\s*`$" }) -Assert-That ($ciCategoryLines.Count -eq 1) ` - "ci-unity.yml declares exactly one non-empty customParameters lane (saw $($ciCategoryLines.Count))" - -$ciCategory = $null -if ($ciCategoryLines.Count -eq 1) { - $ciCatMatch = [regex]::Match($ciCategoryLines[0], '-testCategory\s+(?\S+)') - Assert-That $ciCatMatch.Success ` - "ci-unity.yml's customParameters carries a -testCategory value (saw: $($ciCategoryLines[0].Trim()))" - if ($ciCatMatch.Success) { $ciCategory = $ciCatMatch.Groups['cat'].Value.Trim("'", '"') } +# **matrix の要素ごとに見る。合計を数えない。** +# +# 最初の版はリテラル行の合計(否定形が 1 本以上 / 肯定形がちょうど 1 本)を +# 見ていたが、**否定形が 3 本ある状態では 1 本が '' に落ちても残り 2 本で +# 緑になる**とレビューが実測した。消えうる 1 本は tarball レーンの +# `-testCategory !Graphics` で、**それはまさに M7a で実際に踏んだ回帰である。** +# +# 見るべきは「否定形が何本あるか」ではなく「**否定形を持つはずの要素が +# 全部持っているか**」である。matrix の要素を切り出して、要素ごとに問う。 +$unityLines = $unityWorkflowText -split "`r?`n" + +# matrix entry は 10 スペース + '- lane:' で始まり、その属性は 12 スペース。 +# (このリポジトリの workflow 検査は YAML パーサを使わずインデント規約で +# 切り出す。切れなかったときは空振りではなく落とす、も同じ。) +$entries = @() +$current = $null +$laneHeaders = 0 +foreach ($line in $unityLines) { + # **行末アンカーを付けない。** YAML は `lane: X # コメント` を許すので、 + # `$` で締めると**要素そのものが認識されず、その属性が直前の要素に + # 付く。** 壊れ方が「1 件見落とす」ではなく「別の要素の値に化ける」形に + # なるので、アンカーはここでは有害である。 + if ($line -match '^ - lane:\s*(?\S+)') { + $laneHeaders++ + if ($null -ne $current) { $entries += $current } + $current = @{ Lane = $Matches['lane']; TestMode = $null; Params = $null; HasParams = $false } + continue + } + if ($null -eq $current) { continue } + # 12 スペースより浅い非空行が来たら matrix は終わり + if ($line -match '^\S' -or ($line.Trim() -and $line -notmatch '^ ')) { + $entries += $current + $current = $null + continue + } + if ($line -match "^ testMode:\s*(?\S+)") { $current.TestMode = $Matches['v'] } + if ($line -match "^ customParameters:\s*'(?[^']*)'") { + $current.Params = $Matches['v'] + $current.HasParams = $true + } } +if ($null -ne $current) { $entries += $current } + +# **切り出せたことを先に確かめる。** 0 件なら以下は全部空振りする。 +Assert-That ($entries.Count -ge 2) ` + "ci-unity.yml の unity job から matrix entry を切り出せる (saw $($entries.Count))" + +# **緩い数え方と突き合わせる。** 切り出しが 1 件でも取りこぼしたら、 +# 空振りではなく落とす(このファイルが `jobs:` の切り出しで使っている型)。 +Assert-That ($entries.Count -eq $laneHeaders) ` + "切り出した matrix entry の数が '- lane:' の行数と一致する (entries $($entries.Count) / headers $laneHeaders)" + +<# + **分類から落ちた要素を「違反なし」と読まない。** + + 下の検査は `testMode` で EditMode / Standalone に振り分ける。**どちらにも + 入らなかった要素は、customParameters を一切見られないまま緑になる** —— + レビューが負の対照で実測した(`testMode:` の行末にコメントを足しつつ、 + 同じ要素の `customParameters` を '' にすると、全 assertion が PASS した)。 + + **`$entries.Count -ge 2` のような緩い下限は、落ちた 1 本を吸収する。** + 要素ごとに「知っている testMode を持つか」を問う。 +#> +$knownModes = @('EditMode', 'Standalone') +$unclassified = @($entries | + Where-Object { $null -eq $_.TestMode -or $_.TestMode -notin $knownModes } | + ForEach-Object { "$($_.Lane)='$($_.TestMode)'" }) +Assert-That ($unclassified.Count -eq 0) ` + ("every ci-unity.yml matrix entry declares a testMode this check knows " + + "($($knownModes -join ' / ')); otherwise its customParameters is never inspected " + + "(unclassified: $($unclassified -join ', '))") + +# **全 entry が customParameters をリテラルで宣言していること。** +# 宣言そのものを消せば「渡し忘れ」と「意図的な空」が区別できなくなる。 +$missing = @($entries | Where-Object { -not $_.HasParams } | ForEach-Object { $_.Lane }) +Assert-That ($missing.Count -eq 0) ` + "every ci-unity.yml lane declares customParameters as a literal (missing: $($missing -join ', '))" + +if ($null -ne $localCategory) { + $complement = $localCategory.TrimStart('!') + + # **EditMode を走らせる entry は、1 つを除いて全部が否定形を持つ。** + # 例外の 1 つが補集合(Graphics)レーンで、**ちょうど 1 つであること**も見る。 + $editEntries = @($entries | Where-Object { $_.TestMode -eq 'EditMode' }) + Assert-That ($editEntries.Count -ge 2) ` + "ci-unity.yml has both the excluding and the complement EditMode lanes (saw $($editEntries.Count))" + + $positive = @($editEntries | Where-Object { $_.Params -eq "-testCategory $complement" }) + Assert-That ($positive.Count -eq 1) ` + ("ci-unity.yml runs the complement category ('-testCategory $complement') in exactly one " + + "EditMode lane (saw $($positive.Count): $(($editEntries | ForEach-Object { "$($_.Lane)='$($_.Params)'" }) -join ', '))") + + foreach ($entry in $editEntries) { + if ($entry.Params -eq "-testCategory $complement") { continue } + Assert-That ($entry.Params -eq "-testCategory $localCategory") ` + ("ci-unity.yml lane '$($entry.Lane)' runs EditMode and must exclude the complement " + + "('-testCategory $localCategory'; saw '$($entry.Params)')") + } + + # **Standalone は渡すものが無いことを明示している。** + foreach ($entry in @($entries | Where-Object { $_.TestMode -eq 'Standalone' })) { + Assert-That ($entry.Params -eq '') ` + ("ci-unity.yml lane '$($entry.Lane)' runs Standalone, where the EditMode-only asmdef " + + "is not compiled, so it must declare an empty customParameters (saw '$($entry.Params)')") + } -# **比較そのものは、両方が読めたときにしか行わない。** 上の 2 つの抽出 -# assertion がすでに失敗を記録しているので、ここで無理に比較して -# 「$null -eq $null」のような偶然の一致を PASS と報告する必要は無い。 -if ($null -ne $localCategory -and $null -ne $ciCategory) { - Assert-That ($localCategory -eq $ciCategory) ` - "ci-unity.yml's EditMode -testCategory ('$ciCategory') matches tools/dev.ps1's `$script:UnityGraphicsExclusionCategory ('$localCategory')" + # **matrix の外にも EditMode を走らせる job が在る**(tarball)。 + # そちらは `with:` にリテラルで書くので、matrix の走査には入らない。 + # **名指しで要求する** —— ここが '' に落ちる形が、上で塞いだ穴の実体だった。 + $tarballParams = @($unityLines | Where-Object { + $_ -match "^ customParameters:\s*'(?[^']*)'\s*`$" + }) + Assert-That ($tarballParams.Count -eq 1) ` + "ci-unity.yml declares customParameters directly on exactly one job outside the matrix (saw $($tarballParams.Count))" + if ($tarballParams.Count -eq 1) { + $m = [regex]::Match($tarballParams[0], "customParameters:\s*'(?[^']*)'") + Assert-That ($m.Groups['v'].Value -eq "-testCategory $localCategory") ` + ("the tarball job runs EditMode and must exclude the complement " + + "('-testCategory $localCategory'; saw '$($m.Groups['v'].Value)')") + } } + # **matrix の宣言だけでは足りない。** 上のリテラル判定は # `customParameters: '-testCategory !Graphics'` が matrix 側に「宣言されて # いること」を見るが、それが実際に `game-ci/unity-test-runner` の `with:` へ # 渡っているかは別に見ていない。渡す `with:` 側の行(`customParameters: # ${{ matrix.customParameters }}`)を消しても matrix の宣言はそのまま残るので # 上の検査は緑のままだが、action には何も渡らず、EditMode の除外が効かなく -# なる。壊れ方は GPU の有無に依存する — GPU が無ければ `AGraphicsDeviceIsPresent` -# が本物の欠陥として落ちて気づけるが、GPU が在れば緑のまま `docs/performance.md` -# / `docs/roadmap.md` の「CI で 1 度も実行されていない」という前提が黙って嘘に -# なる。だから `with:` 側の参照がちょうど 1 本在ることを別に assert する。 +# なる。**2026-09-11 に、コンテナに GPU が在ることが実測で分かった**ので、 +# この壊れ方は「気づけない側」に倒れる —— 除外が効かなくなっても +# `AGraphicsDeviceIsPresent` は通り、**両レーンが同じテストを 2 度走らせる +# 状態が緑のまま残る。** 補集合という前提だけが黙って崩れる。 +# だから `with:` 側の参照がちょうど 1 本在ることを別に assert する。 $ciRefLines = @(($unityWorkflowText -split "`r?`n") | Where-Object { $_ -match '^\s*customParameters:\s*\$\{\{\s*matrix\.customParameters\s*\}\}\s*$' }) @@ -1445,6 +1576,29 @@ foreach ($t in $toolsTests) { "tools/dev.ps1 runs $leaf (ToolsTestScriptsFast/Slow に載せないとどこからも走らない)" } +<# + **`$ErrorActionPreference = 'Continue'` で走るテストは trap を持つこと。** + + Continue の下では、**検査の途中で終了エラーが出ても残りの assertion を + 飛ばして `$failures` は 0 のまま**になり、末尾の判定を通って exit 0 に + なる。2026-09-11 に実測で踏んだ —— `Benchmarks.Tests.ps1` に無い + プロパティを読む assertion を書いたところ、例外が表示されたうえで + `==> Benchmarks.Tests: OK` と出て exit 0 になった。**負の対照を取ろうと + して、対照そのものが素通りした。** + + そのとき 4 本が同じ形をしていた(`PackageRelease.Tests.ps1` だけが + 正しい trap を持っていた)。**4 本を直したが、次に書く人は付け忘れる** —— + だから機械に見させる。`'Stop'` のファイルは対象外である(終了エラーが + そのままスクリプトを止め、exit は非 0 になる)。 +#> +foreach ($t in $toolsTests) { + $text = Get-Content -LiteralPath (Join-Path $repoRoot $t) -Raw + if ($text -notmatch "(?m)^\s*\`$ErrorActionPreference\s*=\s*'Continue'") { continue } + Assert-That ($text -match '(?m)^\s*trap\s*\{') ` + ("$(Split-Path -Leaf $t) runs with ErrorActionPreference='Continue', so it needs a trap " + + "(無いと、途中の例外が静かな合格になる)") +} + # --- Release を作る job は、job 単位で tag に限られていること --- # @@ -2201,7 +2355,9 @@ if ($enMatch.Success -and $jaMatch.Success) { 'Thirteen' = 13; 'Fourteen' = 14; 'Fifteen' = 15; 'Sixteen' = 16 'Seventeen' = 17; 'Eighteen' = 18; 'Nineteen' = 19; 'Twenty' = 20 'Twenty-one' = 21; 'Twenty-two' = 22; 'Twenty-three' = 23 - 'Twenty-four' = 24; 'Twenty-five' = 25 + 'Twenty-four' = 24; 'Twenty-five' = 25; 'Twenty-six' = 26 + 'Twenty-seven' = 27; 'Twenty-eight' = 28; 'Twenty-nine' = 29 + 'Thirty' = 30; 'Thirty-one' = 31; 'Thirty-two' = 32 } $enWord = $enMatch.Groups['n'].Value $enCount = if ($spelled.ContainsKey($enWord)) { $spelled[$enWord] } else { $null }