From 1c1058a6d67cc78a725709ca26d1211acdce2351 Mon Sep 17 00:00:00 2001 From: yousan Date: Thu, 10 Sep 2026 15:47:10 +0900 Subject: [PATCH 1/4] =?UTF-8?q?fix(dev.ps1):=20test-unity-tarball=20?= =?UTF-8?q?=E3=81=8B=E3=82=89=20Graphics=20=E3=82=AB=E3=83=86=E3=82=B4?= =?UTF-8?q?=E3=83=AA=E3=81=AE=E3=83=86=E3=82=B9=E3=83=88=E3=82=92=E9=99=A4?= =?UTF-8?q?=E5=A4=96=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -testCategory '!Graphics' が無いまま -nographics で EditMode を走らせて いたため、GraphicsTests / GraphicsBenchmarkRunner が「スキップ」ではなく 「失敗」していた(graphicsDeviceType が Null になり、GL.Clear/ReadPixels が実際には描画しないまま 205,205,205 を返す。実測 2026-09-05)。 Test-UnityEditMode には M7a の 57212dc で同じ除外が入ったが、このレーンは 足し忘れた形で残った。CLAUDE.md が記録するとおり test-unity-tarball は どの workflow からも走らないため、誰も気づかないまま残っていた。 実測: - 修正前: 66 testcasecount, 60 passed, 6 failed (GraphicsTests.AGraphicsDeviceIsPresent / AsyncMatchesSync / SyncReadbackProducesTheExpectedPixels / TakingTheMatTwiceIsRejected / VerticalFlipIsApplied, GraphicsBenchmarkRunner.MeasureRenderTexturePaths) - 修正後: 59 testcasecount(Graphics カテゴリ 7 件を除外), 59 passed, 0 failed --- tools/dev.ps1 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tools/dev.ps1 b/tools/dev.ps1 index d82a55d..f9f43c4 100644 --- a/tools/dev.ps1 +++ b/tools/dev.ps1 @@ -859,11 +859,23 @@ function Test-UnityTarball { $results = Join-Path $ResultsDir 'unity-tarball.xml' $log = Join-Path $ResultsDir 'unity-tarball.log' + # -testCategory '!Graphics' は GraphicsTests(M7a Task 2)を除外する。 + # このレーンも -testPlatform EditMode で走るので、Test-UnityEditMode の + # 上の docstring と同じ理由が当てはまる: -nographics の下では + # graphicsDeviceType が Null になり、GL.Clear / ReadPixels が実際には + # 描画しないまま 205,205,205 を返す(実測、2026-09-05)。**GPU に依る + # 検査は test-unity-graphics に分けてある** —— ここで除外しないと、 + # GraphicsTests を足しただけでこのレーンが恒久的に赤くなる。 + # この抜けは M7a の 57212dc で EditMode / Graphics の 2 レーンには + # 足したが、このレーンには足し忘れた形で入った。CI からは見えない + # レーンなので(CLAUDE.md: test-unity-tarball はどの workflow からも + # 走らない)、誰も気づかないまま残っていた。 $unityArgs = @( '-projectPath', $project, '-runTests', '-testPlatform', 'EditMode', '-testResults', $results, '-logFile', $log, - '-batchmode', '-nographics' + '-batchmode', '-nographics', + '-testCategory', '!Graphics' ) <# タイムアウトを付ける。CLAUDE.md の不変条件「テストは必ずタイムアウト From fb38c0d9413dcce993a3a701958c46c1a1c645d5 Mon Sep 17 00:00:00 2001 From: yousan Date: Thu, 10 Sep 2026 15:50:21 +0900 Subject: [PATCH 2/4] =?UTF-8?q?ci:=20mymindstorm/setup-emsdk=20=E3=82=92?= =?UTF-8?q?=20v14=20=E3=81=8B=E3=82=89=20v16=20=E3=81=B8=E4=B8=8A=E3=81=92?= =?UTF-8?q?=E3=82=8B=20(#71)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dependabot の PR #71 を人手で取り込む。GitHub は Dependabot 起動の run に repository secret を渡さないため、必須の Unity 2 レーンが `Missing Unity License File` で必ず落ちてボットの PR は green にならない (CLAUDE.md が記録する既定の扱い。#18-#26 と同じ形で処理する)。 4 ファイルとも同じ 1 行を上げるだけの変更(build-opencv.yml / ci-native.yml / ci-unity.yml / release.yml)。tools/tests/OpenCvConfig.Tests.ps1 が 「同じ action が全 workflow で単一の版に揃っていること」を見ているので、 実際に 3 ファイルだけ上げて FAIL を確認してから 4 つ目を直した: FAIL mymindstorm/setup-emsdk is referenced at one version across the workflows (saw: v14, v16) 4 ファイルとも揃えた後は PASS(`dev.ps1 test-tools`、exit 0)。 インストールされる Emscripten の版はこの action 自身の版とは別の `with: version:` パラメータで、`tools/emscripten-versions.psd1` から `steps.emver.outputs.version` 経由で渡る。今回はその解決ロジックにも 表にも触れていないので、Unity 同梱の Emscripten との対応は変わらない。 --- .github/workflows/build-opencv.yml | 2 +- .github/workflows/ci-native.yml | 2 +- .github/workflows/ci-unity.yml | 2 +- .github/workflows/release.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-opencv.yml b/.github/workflows/build-opencv.yml index 137b6ce..ee9b3a5 100644 --- a/.github/workflows/build-opencv.yml +++ b/.github/workflows/build-opencv.yml @@ -124,7 +124,7 @@ jobs: - name: Set up Emscripten if: matrix.platform == 'web-wasm' - uses: mymindstorm/setup-emsdk@v14 + uses: mymindstorm/setup-emsdk@v16 with: version: ${{ steps.emver.outputs.version }} diff --git a/.github/workflows/ci-native.yml b/.github/workflows/ci-native.yml index 5481f6e..6c17cd2 100644 --- a/.github/workflows/ci-native.yml +++ b/.github/workflows/ci-native.yml @@ -375,7 +375,7 @@ jobs: - name: Set up Emscripten if: matrix.platform == 'web-wasm' - uses: mymindstorm/setup-emsdk@v14 + uses: mymindstorm/setup-emsdk@v16 with: version: ${{ steps.emver.outputs.version }} diff --git a/.github/workflows/ci-unity.yml b/.github/workflows/ci-unity.yml index 12ce80d..d4d07d4 100644 --- a/.github/workflows/ci-unity.yml +++ b/.github/workflows/ci-unity.yml @@ -125,7 +125,7 @@ jobs: - name: Set up Emscripten if: matrix.platform == 'web-wasm' - uses: mymindstorm/setup-emsdk@v14 + uses: mymindstorm/setup-emsdk@v16 with: version: ${{ steps.emver.outputs.version }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f108b0c..632bcfa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -119,7 +119,7 @@ jobs: - name: Set up Emscripten if: matrix.platform == 'web-wasm' - uses: mymindstorm/setup-emsdk@v14 + uses: mymindstorm/setup-emsdk@v16 with: version: ${{ steps.emver.outputs.version }} From fce38ec6a1f1039555a01a30b95f42a790527760 Mon Sep 17 00:00:00 2001 From: yousan Date: Thu, 10 Sep 2026 16:12:26 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix(review):=20-testCategory=20=E3=82=92?= =?UTF-8?q?=E5=8D=98=E4=B8=80=E5=AE=9A=E7=BE=A9=E3=81=AB=E3=81=97=E3=80=81?= =?UTF-8?q?CLAUDE.md=20=E3=81=A8=E5=8F=B0=E5=B8=B3=E3=81=AE=E8=A8=98?= =?UTF-8?q?=E8=BF=B0=E3=82=92=E7=9B=B4=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit review-1(重要): Test-UnityTarball に直書きした '!Graphics' は、 OpenCvConfig.Tests.ps1 の突き合わせが Test-UnityEditMode しか見ていな かったため何にも縛られない第三のコピーだった。tools/dev.ps1 に $script:UnityGraphicsExclusionCategory を正本として 1 箇所定義し、 Test-UnityEditMode / Test-UnityTarball の両方をそこから読ませる。 検査側は (a) 定義がちょうど 1 回あること、(b) 両関数がその変数を 実際に -testCategory の引数として渡していること(コメント内の言及では 満たせない)、(c) 否定形の -testCategory をクォート文字列で直書きした 箇所が定義以外に無いこと、の 3 つを見るよう書き直した。 負の対照で確認: Test-UnityTarball の引数を再び '!Graphics' の直書きに 戻すと、(b) と (c) がそれぞれ独立に FAIL する(Test-UnityEditMode 側は PASS のまま)。戻すと 3 つとも PASS、dev.ps1 test も exit 0(116/116)。 途中で見つけた検査自体の欠陥: (b) の最初の版は関数本体の中に変数名の 「文字列」が現れるかだけを見ており、引数を直書きに戻してもコメントに 変数名を残せば PASS してしまった。コード上の引数の形 ('-testCategory', $script:UnityGraphicsExclusionCategory)そのものを 要求するよう直した。 review-3: tools/dev.ps1 のコメントが引用していた 57212dc は main から 辿れない未マージブランチの commit。squash 後の be5615f に差し替えた。 review-4: Test-UnityTarball 側のコメントを Test-UnityEditMode の定義へ のポインタに縮め、205,205,205 の説明と「どの workflow からも走らない」 という陳腐化しやすい記述を重複させるのをやめた。 review-2: CLAUDE.md の test-unity-tarball 行に Graphics カテゴリ除外の 記述が無かったので足した。test-unity-editmode 行の「58 件」は実測で 確かめると 59 件だった(unity-editmode.xml の testcasecount="59"、 tarball 側の 59 passed とも一致)ので実測値に直した。57 件からの 2 件分の由来は追っていない、と正直に書いた。tarball 行にも除外前後の 実測(66 passed/60 passed/6 failed → 59 passed/0 failed)を足した。 review-5: CiVisibilityTests.cs のコメントが「!Graphics 側は 1 レーン」 という前提で書かれていたのを、test-unity-tarball を含む 2 レーンに 直した。 review-6: unity-probe.yml は対象外(レビューで妥当と判定済み)。 --- CLAUDE.md | 4 +- .../Tests/EditMode/CiVisibilityTests.cs | 12 ++-- tools/dev.ps1 | 47 ++++++++----- tools/tests/OpenCvConfig.Tests.ps1 | 69 +++++++++++++------ 4 files changed, 86 insertions(+), 46 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 94dda0a..2607e02 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -76,10 +76,10 @@ 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、**58 件**。`-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 件になったが所要時間は取り直していない) | +| `./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"`)——57 からの 2 件分の由来は未確認で、この行を直す作業では追っていない) | | `./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 を走らせる。**`-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(**所要時間は取っていない**)| +| `./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-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 clean` | `build/` を削除 | — | diff --git a/tests/UnityProject/Assets/Tests/EditMode/CiVisibilityTests.cs b/tests/UnityProject/Assets/Tests/EditMode/CiVisibilityTests.cs index 54a1eae..a585c2f 100644 --- a/tests/UnityProject/Assets/Tests/EditMode/CiVisibilityTests.cs +++ b/tests/UnityProject/Assets/Tests/EditMode/CiVisibilityTests.cs @@ -7,8 +7,10 @@ /// 台帳として名指しで固定する(M7a Task 2 のレビュー指摘)。 /// /// **`Graphics` と `!Graphics` は補集合なので、実行時の和集合は常に完全である。** -/// `test-unity-editmode` が `-testCategory '!Graphics'`、`test-unity-graphics` -/// が `-testCategory 'Graphics'` で走る限り、振り分け自体に穴は無い。 +/// `test-unity-editmode` と `test-unity-tarball` が `-testCategory '!Graphics'`、 +/// `test-unity-graphics` が `-testCategory 'Graphics'` で走る限り、振り分け +/// 自体に穴は無い(`!Graphics` 側が 2 レーンに増えても、補集合の性質は +/// 変わらない)。 /// /// **本当の穴は「一方のレーン(graphics)が CI に配線されていない」ことである。** /// `test-unity-graphics` はローカル専用で、`ci-unity.yml` からは呼ばれない。 @@ -24,9 +26,9 @@ /// **走査するのはこの assembly(EditMode)だけである。** 他の test /// assembly に `[Category("Graphics")]` が付いても、ここは気づかない —— /// **それでこの検査の目的は満たされる。** カテゴリで振り分けているのは -/// EditMode のレーンだけで(`test-unity-editmode` が `!Graphics`、 -/// `test-unity-graphics` が `Graphics`)、PlayMode / Standalone のレーンは -/// `-testCategory` を渡さない。つまり **他所でカテゴリを付けても、その +/// EditMode のレーンだけで(`test-unity-editmode` と `test-unity-tarball` が +/// `!Graphics`、`test-unity-graphics` が `Graphics`)、PlayMode / Standalone +/// のレーンは `-testCategory` を渡さない。つまり **他所でカテゴリを付けても、その /// テストは CI から消えない。** 消えるのはここに在るものだけなので、 /// 台帳もここだけを見る。**振り分けの前提が変われば、この段は嘘になる。** /// diff --git a/tools/dev.ps1 b/tools/dev.ps1 index f9f43c4..ed7af27 100644 --- a/tools/dev.ps1 +++ b/tools/dev.ps1 @@ -557,6 +557,24 @@ $script:GatingTestNames = @( 'PluginGatingTests.EachPluginIsEnabledOnlyForItsOwnStandaloneTarget' ) +<# + GraphicsTests / GraphicsBenchmarkRunner(M7a Task 2、GPU に依る検査)を + 除外する -testCategory の値。**正本はここ 1 箇所。** + + -nographics の下では graphicsDeviceType が Null になり、GL.Clear の + 直後の ReadPixels が実際には描画しないまま 205,205,205 を返す + (実測、2026-09-05)。GPU に依る検査は test-unity-graphics に分けて + あるので、-nographics で EditMode を走らせるレーン(Test-UnityEditMode / + Test-UnityTarball)はどちらもこれを除外しないと、GraphicsTests を + 足しただけで恒久的に赤くなる。 + + 文字列として 2 度書かない —— tools/tests/OpenCvConfig.Tests.ps1 が + ci-unity.yml の customParameters とこの値を突き合わせるので、直書きの + コピーを増やすとそちらの検査から外れる(実際に Test-UnityTarball に + 直書きしたコピーができ、どこにも縛られていなかった。2026-09-10)。 +#> +$script:UnityGraphicsExclusionCategory = '!Graphics' + function Sync-AllPlatformsMarker { param([Parameter(Mandatory)][string] $ProjectPath) @@ -589,18 +607,15 @@ function Test-UnityEditMode { # -batchmode -nographics は CI とローカルで同じ条件にするため常に付ける。 # - # -testCategory '!Graphics' は GraphicsTests(M7a Task 2)を除外する。 - # -nographics の下では graphicsDeviceType が Null になり、GL.Clear / - # ReadPixels が実際には描画しないまま 205,205,205 を返す(実測、 - # 2026-09-05)。**GPU に依る検査は test-unity-graphics に分けてある** - # —— ここで除外しないと、GraphicsTests を足しただけでこのレーンが - # 恒久的に赤くなる。 + # -testCategory は GPU に依る検査(GraphicsTests / GraphicsBenchmarkRunner) + # を除外する。理由と正本は $script:UnityGraphicsExclusionCategory の + # 定義側に書いてある。 $unityArgs = @( '-projectPath', $project, '-runTests', '-testPlatform', 'EditMode', '-testResults', $results, '-logFile', $log, '-batchmode', '-nographics', - '-testCategory', '!Graphics' + '-testCategory', $script:UnityGraphicsExclusionCategory ) $proc = Start-Process -FilePath $unity -ArgumentList $unityArgs -Wait -PassThru -NoNewWindow $exit = $proc.ExitCode @@ -859,23 +874,17 @@ function Test-UnityTarball { $results = Join-Path $ResultsDir 'unity-tarball.xml' $log = Join-Path $ResultsDir 'unity-tarball.log' - # -testCategory '!Graphics' は GraphicsTests(M7a Task 2)を除外する。 - # このレーンも -testPlatform EditMode で走るので、Test-UnityEditMode の - # 上の docstring と同じ理由が当てはまる: -nographics の下では - # graphicsDeviceType が Null になり、GL.Clear / ReadPixels が実際には - # 描画しないまま 205,205,205 を返す(実測、2026-09-05)。**GPU に依る - # 検査は test-unity-graphics に分けてある** —— ここで除外しないと、 - # GraphicsTests を足しただけでこのレーンが恒久的に赤くなる。 - # この抜けは M7a の 57212dc で EditMode / Graphics の 2 レーンには - # 足したが、このレーンには足し忘れた形で入った。CI からは見えない - # レーンなので(CLAUDE.md: test-unity-tarball はどの workflow からも - # 走らない)、誰も気づかないまま残っていた。 + # -testCategory は Test-UnityEditMode と同じ定義を使う + # ($script:UnityGraphicsExclusionCategory。理由はその定義側にある)。 + # このレーンも -testPlatform EditMode で走るので同じ理由が当てはまる。 + # 2026-09-10 まで足し忘れていた(be5615f で Test-UnityEditMode / + # Test-UnityGraphics には足したが、ここは漏れた)。 $unityArgs = @( '-projectPath', $project, '-runTests', '-testPlatform', 'EditMode', '-testResults', $results, '-logFile', $log, '-batchmode', '-nographics', - '-testCategory', '!Graphics' + '-testCategory', $script:UnityGraphicsExclusionCategory ) <# タイムアウトを付ける。CLAUDE.md の不変条件「テストは必ずタイムアウト diff --git a/tools/tests/OpenCvConfig.Tests.ps1 b/tools/tests/OpenCvConfig.Tests.ps1 index 4441914..902e286 100644 --- a/tools/tests/OpenCvConfig.Tests.ps1 +++ b/tools/tests/OpenCvConfig.Tests.ps1 @@ -992,13 +992,13 @@ if ($markerName) { # --- ci-unity の EditMode レーンが、ローカルの -testCategory 除外と一致すること --- # -# tools/dev.ps1 の Test-UnityEditMode は GraphicsTests / GraphicsBenchmarkRunner -# (M7a Task 2、GPU に依る検査)を -testCategory '!Graphics' で除外する。 -# -nographics の下では graphicsDeviceType が Null になり、GL.Clear の直後の -# ReadPixels が実際には描画しないまま 205,205,205 を返す(実測、2026-09-05)。 -# GraphicsChecks.AGraphicsDeviceIsPresent は GPU が無ければ skip ではなく -# fail する設計なので、この除外を欠いたまま Unity を起動すると本物の欠陥として -# 落ちる。 +# tools/dev.ps1 は GraphicsTests / GraphicsBenchmarkRunner(M7a Task 2、GPU に +# 依る検査)を除外する値を $script:UnityGraphicsExclusionCategory 1 箇所に +# 持つ(定義側の docstring に理由がある)。-nographics で EditMode を走らせる +# レーン(Test-UnityEditMode と Test-UnityTarball)は両方ともこれを使う +# —— **直書きのコピーを許さない。** 2026-09-10 に実際に Test-UnityTarball が +# 直書きのコピーを持ち、どこからも縛られていなかった(この検査は +# Test-UnityEditMode しか見ていなかった)。 # # ci-unity.yml は「CI はローカルと同一のコマンドを呼ぶ」の意図的な例外 # (game-ci が Unity を起動し、tools/dev.ps1 ではない)なので、この 2 つの @@ -1014,22 +1014,51 @@ if ($markerName) { # 変わらず 1 のままになる(PluginGatingTests の合図名と同じ形)。 $devPs1Raw = Get-Content -LiteralPath (Join-Path $repoRoot 'tools/dev.ps1') -Raw -# Test-UnityGraphics にも '-testCategory' が在る(値は 'Graphics'、 -# -nographics を渡さない側)ので、関数本体を名前で切り出してから読む。 -# このファイルのトップレベル関数はすべて列 0 の '}' で閉じる規約 -# (tools/dev.ps1 の他の関数と同じ)なので、それを終端として使う。 -$editModeFnMatch = [regex]::Match($devPs1Raw, '(?ms)^function Test-UnityEditMode \{.*?^\}') -Assert-That $editModeFnMatch.Success ` - 'tools/dev.ps1 から Test-UnityEditMode 関数を切り出せる (切り出せなければ以下は空振りする)' +# 正本は $script:UnityGraphicsExclusionCategory の定義そのもの +# (関数本体の中の文字列ではない)。 +$exclusionDefMatches = [regex]::Matches($devPs1Raw, + "(?m)^\`$script:UnityGraphicsExclusionCategory\s*=\s*'(?[^']+)'\s*`$") +Assert-That ($exclusionDefMatches.Count -eq 1) ` + "tools/dev.ps1 defines `$script:UnityGraphicsExclusionCategory exactly once (saw $($exclusionDefMatches.Count); 読み取れなければ以下は空振りする)" $localCategory = $null -if ($editModeFnMatch.Success) { - $localCatMatch = [regex]::Match($editModeFnMatch.Value, "'-testCategory',\s*'(?[^']+)'") - Assert-That $localCatMatch.Success ` - "Test-UnityEditMode から -testCategory の値を読み取れる (読み取れなければ以下は空振りする)" - if ($localCatMatch.Success) { $localCategory = $localCatMatch.Groups['cat'].Value } +if ($exclusionDefMatches.Count -eq 1) { $localCategory = $exclusionDefMatches[0].Groups['cat'].Value } + +# Test-UnityGraphics にも '-testCategory' が在る(値は 'Graphics'、 +# -nographics を渡さない側、正の値なので同じ定義を使わない)ので、 +# 対象の関数だけを名前で切り出してから読む。このファイルのトップレベル +# 関数はすべて列 0 の '}' で閉じる規約(tools/dev.ps1 の他の関数と同じ) +# なので、それを終端として使う。 +# +# **定義を持つだけでは足りない。** 両方のレーンが実際にそれを参照している +# ことを見る —— 変数を定義しただけで呼び出し側が直書きの文字列のままでも、 +# 説明のコメントに変数名を書いておけば下の素朴な部分文字列一致は通って +# しまう(実測: 引数を直書きに戻し、コメントだけ変数名を残す形を試したら +# 最初の版はここを PASS のまま見逃した)。だから **コード上の引数の形** +# (`'-testCategory', $script:UnityGraphicsExclusionCategory`)そのものを +# 探し、コメント内の言及では満たせないようにする。 +foreach ($fnName in @('Test-UnityEditMode', 'Test-UnityTarball')) { + $fnMatch = [regex]::Match($devPs1Raw, "(?ms)^function $fnName \{.*?^\}") + Assert-That $fnMatch.Success ` + "tools/dev.ps1 から $fnName 関数を切り出せる (切り出せなければ以下は空振りする)" + if ($fnMatch.Success) { + $usagePattern = "'-testCategory',\s*\`$script:UnityGraphicsExclusionCategory\b" + Assert-That ($fnMatch.Value -match $usagePattern) ` + "$fnName passes `$script:UnityGraphicsExclusionCategory as the -testCategory argument (not a literal, and not just mentioned in a comment)" + } } +# **コピーが増えていないことも見る。** 定義行そのもの(上の +# $exclusionDefMatches が既に見ている)を除いて、否定形の -testCategory を +# クォートされた文字列リテラルで直書きした箇所が 1 つも無いこと。これが +# 「新しいレーンを足したときにまた直書きする」ことを止める唯一の門である +# —— 定義と使用箇所の assertion だけでは、3 つ目のレーンが `'!Graphics'` +# を再び書いても気づけない。 +$literalNegatedCategoryMatches = [regex]::Matches($devPs1Raw, "'-testCategory',\s*'!(?[^']+)'") +Assert-That ($literalNegatedCategoryMatches.Count -eq 0) ` + ("tools/dev.ps1 has no literal negated -testCategory value outside " + + "`$script:UnityGraphicsExclusionCategory (saw $($literalNegatedCategoryMatches.Count))") + # ci-unity.yml 側。**単純な「customParameters: の行を探す」では足りない** —— # matrix の 2 つの lane(リテラル文字列)に加えて、customParameters を実際に # action へ渡す with: の 1 行(`${{ matrix.customParameters }}`)も同じ @@ -1066,7 +1095,7 @@ if ($ciCategoryLines.Count -eq 1) { # 「$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 Test-UnityEditMode ('$localCategory')" + "ci-unity.yml's EditMode -testCategory ('$ciCategory') matches tools/dev.ps1's `$script:UnityGraphicsExclusionCategory ('$localCategory')" } # **matrix の宣言だけでは足りない。** 上のリテラル判定は From f7a831d621b9c4afcdfe38117c2d5cef3fa5e6b1 Mon Sep 17 00:00:00 2001 From: yousan Date: Thu, 10 Sep 2026 16:32:17 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix(review):=20overclaimed=20check=20messag?= =?UTF-8?q?es,=20quote-class=20and=20indent=20gaps,=2059=20=E4=BB=B6?= =?UTF-8?q?=E3=81=AE=E7=94=B1=E6=9D=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit review-A: (b)($fnName が $script:UnityGraphicsExclusionCategory を渡し ているか)のメッセージが「コメントでは満たせない」と言い切っていたが、 実際には満たせる。負の対照で再現した: 引数を直書きの '!Graphics' に戻し、 コメントに同じ引数の形をそのまま書くと (b) は PASS のままだった。実際に 直書きへの回帰を捕まえるのは (c)(否定形リテラルの直書き検査)で、 (b)/(c) は対で初めて 1 つの門になる。両方のコメントとメッセージをその とおりに直した。 review-C: (c) は単一引用符の直書きしか見ておらず、`"-testCategory", "!Graphics"`(有効な PowerShell で挙動は同じ)を素通ししていた。負の 対照で確認済み(saw 0 のまま、(b) だけが落ちた)。クォート文字をクラス `['"]` に広げ、同じ入力で saw 1 になることを確認。splatting や文字列 連結はまだ素通りする——コメントにその限界を明記した。 review-D: (a)($script:UnityGraphicsExclusionCategory の定義がちょうど 1 回)の正規表現が `^` を列 0 に固定しており、関数内のインデントされた 再代入を数えていなかった。負の対照で確認済み(ダミーの再代入を関数内に 足しても saw 1 のまま緑だった)。`^\s*` に広げ、同じ入力で saw 2 になり 落ちることを確認。 3 つとも「負の対照を取り直す」よう指示されたとおり実施: 各々、直した 検査に対して壊れた入力を与えて落ちることを見てから、正しい入力に戻して 緑に戻ることを確認した(ログは report に貼ってある)。 review-B: CLAUDE.md の「57 件からの 2 件分の由来は未確認」を実際に確認 した。tests/UnityProject/Assets/Tests/EditMode/*.cs の [Test]/[UnityTest] 属性行を commit ごとに数えると、be5615f(M7 squash)で 65 件 (Graphics 7 件を除いて 58 件——旧記述はこの時点では正しかった)、 e1b0930(M7c、dnn プロファイル追加)で 66 件(除いて 59 件)。差の 1 件は ProfileGatingTests.cs に足された TheDnnPublicApiAssemblyIsAlsoAbsentWithoutItsDefine (同ファイル 5→6)で、この行を更新する commit がその後に続かなかった だけだと判明した。 review-E: tools/dev.ps1 内で「2026-09-10 に Test-UnityTarball が直書きの コピーを持っていた」という同じ経緯が定義側と Test-UnityTarball 側の 両方に書かれていたので、定義側(正本)だけに残し、Test-UnityTarball 側は 定義へのポインタに縮めた。 --- CLAUDE.md | 2 +- tools/dev.ps1 | 7 +++--- tools/tests/OpenCvConfig.Tests.ps1 | 39 +++++++++++++++++++++++------- 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 2607e02..72f90ec 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -76,7 +76,7 @@ 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"`)——57 からの 2 件分の由来は未確認で、この行を直す作業では追っていない) | +| `./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-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`)| diff --git a/tools/dev.ps1 b/tools/dev.ps1 index ed7af27..7100109 100644 --- a/tools/dev.ps1 +++ b/tools/dev.ps1 @@ -875,10 +875,9 @@ function Test-UnityTarball { $results = Join-Path $ResultsDir 'unity-tarball.xml' $log = Join-Path $ResultsDir 'unity-tarball.log' # -testCategory は Test-UnityEditMode と同じ定義を使う - # ($script:UnityGraphicsExclusionCategory。理由はその定義側にある)。 - # このレーンも -testPlatform EditMode で走るので同じ理由が当てはまる。 - # 2026-09-10 まで足し忘れていた(be5615f で Test-UnityEditMode / - # Test-UnityGraphics には足したが、ここは漏れた)。 + # ($script:UnityGraphicsExclusionCategory。理由と経緯はその + # 定義側にある)。このレーンも -testPlatform EditMode で走るので + # 同じ理由が当てはまる。 $unityArgs = @( '-projectPath', $project, '-runTests', '-testPlatform', 'EditMode', diff --git a/tools/tests/OpenCvConfig.Tests.ps1 b/tools/tests/OpenCvConfig.Tests.ps1 index 902e286..76984b8 100644 --- a/tools/tests/OpenCvConfig.Tests.ps1 +++ b/tools/tests/OpenCvConfig.Tests.ps1 @@ -1017,7 +1017,7 @@ $devPs1Raw = Get-Content -LiteralPath (Join-Path $repoRoot 'tools/dev.ps1') -Raw # 正本は $script:UnityGraphicsExclusionCategory の定義そのもの # (関数本体の中の文字列ではない)。 $exclusionDefMatches = [regex]::Matches($devPs1Raw, - "(?m)^\`$script:UnityGraphicsExclusionCategory\s*=\s*'(?[^']+)'\s*`$") + "(?m)^\s*\`$script:UnityGraphicsExclusionCategory\s*=\s*'(?[^']+)'\s*`$") Assert-That ($exclusionDefMatches.Count -eq 1) ` "tools/dev.ps1 defines `$script:UnityGraphicsExclusionCategory exactly once (saw $($exclusionDefMatches.Count); 読み取れなければ以下は空振りする)" @@ -1034,9 +1034,19 @@ if ($exclusionDefMatches.Count -eq 1) { $localCategory = $exclusionDefMatches[0] # ことを見る —— 変数を定義しただけで呼び出し側が直書きの文字列のままでも、 # 説明のコメントに変数名を書いておけば下の素朴な部分文字列一致は通って # しまう(実測: 引数を直書きに戻し、コメントだけ変数名を残す形を試したら -# 最初の版はここを PASS のまま見逃した)。だから **コード上の引数の形** +# 最初の版はここを PASS のまま見逃した)。そこで **コード上の引数の形** # (`'-testCategory', $script:UnityGraphicsExclusionCategory`)そのものを -# 探し、コメント内の言及では満たせないようにする。 +# 探すよう狭めた。 +# +# **それでもこの assertion 単体は「直書きに戻していない」ことの証明には +# ならない。** コメントが偶然この引数の形そのもの(この関数を直書きに +# 戻したうえで、コメントに `'-testCategory', +# $script:UnityGraphicsExclusionCategory` という文字列を書く)を含んで +# いれば、この assertion は再び PASS する(実測。正規表現はコードと +# コメントを区別しない)。**直書きへの回帰を実際に捕まえるのは、下の +# 「コピーが増えていない」assertion(否定形の -testCategory リテラルを +# 探す)である。この 2 つは対になって初めて 1 つの門になり、どちらか +# 片方だけでは足りない。** foreach ($fnName in @('Test-UnityEditMode', 'Test-UnityTarball')) { $fnMatch = [regex]::Match($devPs1Raw, "(?ms)^function $fnName \{.*?^\}") Assert-That $fnMatch.Success ` @@ -1044,17 +1054,28 @@ foreach ($fnName in @('Test-UnityEditMode', 'Test-UnityTarball')) { if ($fnMatch.Success) { $usagePattern = "'-testCategory',\s*\`$script:UnityGraphicsExclusionCategory\b" Assert-That ($fnMatch.Value -match $usagePattern) ` - "$fnName passes `$script:UnityGraphicsExclusionCategory as the -testCategory argument (not a literal, and not just mentioned in a comment)" + "$fnName's text contains the '-testCategory', `$script:UnityGraphicsExclusionCategory argument shape (this alone does not prove the argument isn't hardcoded elsewhere in a way a comment happens to echo — see the paired literal-copy check below)" } } # **コピーが増えていないことも見る。** 定義行そのもの(上の # $exclusionDefMatches が既に見ている)を除いて、否定形の -testCategory を -# クォートされた文字列リテラルで直書きした箇所が 1 つも無いこと。これが -# 「新しいレーンを足したときにまた直書きする」ことを止める唯一の門である -# —— 定義と使用箇所の assertion だけでは、3 つ目のレーンが `'!Graphics'` -# を再び書いても気づけない。 -$literalNegatedCategoryMatches = [regex]::Matches($devPs1Raw, "'-testCategory',\s*'!(?[^']+)'") +# クォートされた文字列リテラルで直書きした箇所が 1 つも無いこと。 +# +# **単独で万能ではない、と正直に書く。** 上の「$fnName が +# $script:UnityGraphicsExclusionCategory を引数として渡している」assertion +# は、コメントがたまたま同じ引数の形(`'-testCategory', +# $script:UnityGraphicsExclusionCategory`)を書いていれば、引数自体を +# 直書きに戻していても PASS してしまう(実測)。実際に直書きへの回帰を +# 捕まえるのはこちらの assertion であり、**2 つは対になって初めて 1 つの +# 門になる** —— どちらか片方だけでは足りない。 +# +# クォート文字は `'` と `"` の両方を見る——シングルクォートだけを見ていた +# 版は `"-testCategory", "!Graphics"`(有効な PowerShell で挙動は同じ)を +# 素通ししていた(実測)。**それでも万能ではない**: 引数を splatting や +# 文字列連結で組み立てた形はどちらも素通りする。ここで捕まえるのは +# 「素朴な直書き」だけで、意図的に検査を迂回する書き方までは狙っていない。 +$literalNegatedCategoryMatches = [regex]::Matches($devPs1Raw, '[''"]-testCategory[''"],\s*[''"]!(?[^''"]+)[''"]') Assert-That ($literalNegatedCategoryMatches.Count -eq 0) ` ("tools/dev.ps1 has no literal negated -testCategory value outside " + "`$script:UnityGraphicsExclusionCategory (saw $($literalNegatedCategoryMatches.Count))")