>[!warning]- Info > Author: [email protected] > Publish date: 2026/07/02 > Last update: 2026/07/14 > Status: WIP > Revision: 12 > Visibility: Public >[!note]- AI Assistance > This document was drafted with the assistance of [Claude](https://claude.ai) (Anthropic). The technical content reflects work done by the author; Claude helped structure and articulate it. # Modularizing `chrome/test` A phased approach to decoupling `chrome/test` from its monolithic `BUILD.gn`, following the Bedrock modularization pattern. > [!summary] TL;DR > - **The problem:** `chrome/test/BUILD.gn` is ~11,000 lines — one of the largest files in the tree, aggregating sources from 26 subdirectories directly. > - **The goal:** Give each logical subdirectory of `chrome/test` its own `BUILD.gn`, mirroring the ~105-file pattern already achieved in `chrome/browser/ui`. > - **The approach:** Three phases — start with leaf modules (`interaction`, `user_education`, `permissions`…), tackle the large `base/` infrastructure split, then complete remaining specialized directories. > - **Biggest win:** `chrome/test/base/` — 131 source files, no `BUILD.gn` historically; extracting it unlocks downstream modularization of the entire tree. ## Background Project Bedrock's primary mandate is fixing the `Browser` god-object in `chrome/browser`. The structural half of that work — giving each subdirectory its own `BUILD.gn` and `OWNERS`, enforcing visibility, breaking circular deps — applies directly to `chrome/test`. The pattern: instead of one massive `BUILD.gn` reaching into child directories via relative source paths like `"base/testing_profile.cc"`, each subdirectory owns a target and the parent references it via a GN dep. There are two complementary efforts: | Effort | What it extracts | Code movement? | |--------|-----------------|----------------| | **This work — `chrome/test/` modularization** | Test *support* targets (`InProcessBrowserTest`, `TestingProfile`…) into per-subdirectory `BUILD.gn` files | None — `BUILD.gn` reorganization only | | **Bedrock team — `chrome/browser/` modularization** | Test *case* source declarations out of `chrome/test/BUILD.gn` into the module's own `:browser_tests` targets | None — declarations move, files stay | --- ## Current State ``` chrome/test/ ├── BUILD.gn ← monolith, shrinking ├── base/ ← Phase 2 (current — see progress table) ├── interaction/ ← ✅ done ├── user_education/ ← ✅ done ├── permissions/ ← ✅ done ├── views/ ← ✅ done ├── gpu/ ← ✅ done ├── v8/ ← ✅ done ├── enterprise/ ← Python-only, skip ├── accessibility/ ← standalone tool, skip ├── webapps/ ← test data only, skip ├── delayload/ ← Windows-only, defer ├── android/ ← BUILD.gn exists; 8 APK/Java sources remain in root ├── chromedriver/ ← BUILD.gn exists (standalone binary) └── (fuzzing, mini_installer, payments, etc.) ← BUILD.gn exists, fully modularized ``` ### Progress | Phase | Status | Notes | |---|---|---| | Phase 1 — leaf modules (`interaction`, `user_education`, `permissions`, `views`, `gpu`, `v8`) | ✅ All landed | All temp monolith deps resolved | | Phase 2a — `base/` thin targets (`:in_process_browser_test`, `:ui_test_utils`, `:test_launcher_utils`) + subdir dep cleanup | ✅ Landed | `chromod/chrome-test-base-0..3` | | Phase 2b — `:in_process_browser_test_mixin`, `:chrome_test_launcher` | ✅ Landed | | | Phase 2b — `:testing_profile` thin target | ⏳ In review | `chromod/chrome-test-base-4` | | Phase 2b — `:testing_profile` full + `:testing_browser_process` | ⏳ In review | `chromod/chrome-test-base-5`; uses `allow_circular_includes_from` — see below | | Phase 2b — `:testing_profile` Pattern A split (header+impl) | ⏳ In review | `chromod/chrome-test-base-6`; breaks extensions cycle; cros ash cycle deferred | | Phase 2b — Forward-declare `ScopedCrosSettingsTestHelper` in testing_profile.h | ⏳ In review | `chromod/chrome-test-base-7`; fully removes ash/settings dep from header target | | Phase 2c — `:profile_lifecycle_test_utils` | ⏳ In review | `chromod/chrome-test-base-8`; BUILD.gn-only; profile_waiter + profile_deletion_observer + profile_destruction_waiter | | Phase 2c — `:browser_window_waiters` | ⏳ In review | `chromod/chrome-test-base-9`; browser_closed_waiter + browser_created_waiter | | Phase 2c — remaining `base/` sources | ⬜ Next | See cluster map below | | Phase 3 — remaining dirs (`android/` leftovers…) | ⬜ | After `base/` complete | | Phase 3a — Monolith → thin aggregator | ⬜ | After ALL sources extracted | | Phase 3b — Consumer migration + monolith deletion | ⬜ | After 3a; requires coordination | > [!tip] Phase 2a thin targets > `:in_process_browser_test`, `:ui_test_utils`, `:test_launcher_utils` are currently header-only. Their `.cc` implementations remain in the monolith until the pre-existing cycle `//chrome/browser/ui:test_support → //chrome/test:test_support_ui` is broken. --- ## chrome/test/base — Extraction Status ### Already extracted | Target | Files | Notes | |--------|-------|-------| | `:test_launcher_utils` | `test_launcher_utils.h` | Thin header-only; `.cc` pending cycle fix (see below) | | `:in_process_browser_test` | `in_process_browser_test.h` | Thin header-only (non-android); `.cc` pending cycle fix | | `:ui_test_utils` | `ui_test_utils.h` | Thin header-only (non-android); `.cc` pending cycle fix | | `:in_process_browser_test_mixin` | `in_process_browser_test_mixin.{cc,h}` | Full | | `:chrome_test_launcher` | `chrome_test_launcher.{cc,h}` | Full | | `:testing_profile` | `testing_profile.h`, `fake_profile_manager.h`, `testing_profile_key.h`, `testing_profile_manager.h` | Header-only; `allow_circular_includes_from` entries in TBP pending removal (see below) | | `:testing_profile_impl` | `testing_profile.cc`, `fake_profile_manager.cc`, `testing_profile_key.cc`, `testing_profile_manager.cc` | Full impl | | `:testing_browser_process` | `testing_browser_process.{cc,h}`, `testing_browser_process_platform_part.{cc,h}` | Full; has 3 remaining `allow_circular_includes_from` (see below) | | `:profile_lifecycle_test_utils` | `profile_{waiter,deletion_observer,destruction_waiter}.{cc,h}` | Full | | `:browser_window_waiters` | `browser_{closed,created}_waiter.{cc,h}` | Full | ### Phase 2c — cluster map (audited 2026-07-14) Dep analysis done by reading all remaining `.h`/`.cc` files and cross-referencing with `gn gen --ide=json` project.json for precise header→target resolution. #### Forced cluster: `:test_suites` Files: `chrome_test_suite.{cc,h}` + `chrome_unit_test_suite.{cc,h}` Reason: `chrome_unit_test_suite.h` inherits from `ChromeTestSuite` and includes `chrome_test_suite.h` directly — tightly coupled. Heavy deps: `//chrome/app:test_support`, `//chrome/browser:browser_process`, `//chrome/browser/ui/webui:configs`, `//chrome/browser/ui:ui`, `//chrome/browser/profiles:profile_manager`, `//chrome/browser/lifetime`, `//chrome/common:constants`, `//chrome/test/base:testing_browser_process`, and more. Biggest lift remaining. #### Separate targets despite similar names: `chrome_render_view_*` - `chrome_render_view_host_test_harness` — browser-side, wraps `TestingProfile`; deps: `:testing_profile` + `//content/test:test_support` - `chrome_render_view_test` — renderer-side, no `Profile`; deps: `//content/test:test_support`, `//services/service_manager/public/cpp:cpp`, `//third_party/blink/public/common:headers` Different process boundary, different dep profiles → extract as separate targets. #### Clean standalones (deps fully audited) | Target | public_deps | impl-only deps | |--------|-------------|----------------| | `autocomplete_change_observer` | `//base`, `//components/omnibox/browser:browser` | `//chrome/browser/omnibox:omnibox`, `//chrome/browser/profiles` | | `find_result_waiter` | `//base`, `//components/find_in_page:find_in_page`, `//ui/gfx` | *(none extra)* | | `scoped_browser_locale` | `//base` | `//chrome/browser:browser_process` | | `search_test_utils` | `//base` | `//components/search_engines:search_engines`, `//content/public/test:test_support` | | `tracing` | `//base` | `//content/public/browser:browser_sources`, `//content/public/test:test_support`, `//services/tracing/public/mojom` | | `web_feature_histogram_tester` | `//base/test:test_support`, `//third_party/blink/public/mojom:web_feature_mojo_bindings` | *(none extra)* | | `scoped_metrics_service_for_synthetic_trials` | `//chrome/test/base:testing_browser_process`, `//chrome/test/base:testing_profile`, `//components/metrics:test_support`, `//components/variations:variations` + more | *(none extra)* | #### Complete existing thin target: `:test_launcher_utils` Add `test_launcher_utils.cc` — new deps: `//chrome/common:constants`, `//chrome/common:non_code_constants`, `//chrome/common:url_constants`, `//components/os_crypt/common:switches`, `//components/password_manager/core/browser:password_switches`, `//ui/display:display`; aura-only: `//ui/wm:wm`. #### Blocked: `chrome_test_utils` `chrome_test_utils.h` includes `chrome/test/base/platform_browser_test.h`, which lives in `test_support_ui`. Pre-fix: extract `platform_browser_test.h` as a thin header-only target (same pattern as `:in_process_browser_test`). #### Suggested extraction order 1. ✅ `:browser_window_waiters` — CL9 2. Standalones batch: `find_result_waiter`, `scoped_browser_locale`, `search_test_utils`, `tracing`, `web_feature_histogram_tester`, `autocomplete_change_observer` 3. Complete `:test_launcher_utils` (add `.cc`) 4. `chrome_render_view_host_test_harness` + `chrome_render_view_test` (separate) 5. `scoped_metrics_service_for_synthetic_trials` 6. `:test_suites` (heaviest) 7. `chrome_test_utils` (needs `platform_browser_test.h` thin-target pre-fix first) ### Deferred — TBP `allow_circular_includes_from` cleanup Three entries remain in `testing_browser_process`'s `allow_circular_includes_from` that need structural fixes before removal: | Entry | Root cause | Fix | |-------|-----------|-----| | `testing_profile_impl` | `testing_profile_manager.cc` includes `testing_browser_process.h` | Remove/forward-declare TBP.h in `testing_profile_manager.cc` | | `extensions:test_support` | `extensions:test_support` sources include `testing_browser_process.h` | TBP Pattern A split (thin header-only + impl) | | `ash/settings:test_support`, `ash/system_web_apps/test_support` | Same — include `testing_browser_process.h` on cros | TBP Pattern A split | The Pattern A split for TBP mirrors what was done for `testing_profile` in CL6-CL7: produce a thin `testing_browser_process` target with only the header deps it needs, and a `testing_browser_process_impl` that carries the heavy ash/extensions deps. The three targets that currently include `testing_browser_process.h` can then dep on the thin target directly, eliminating the cycles. ### Phase 2c — remaining `base/` sources to audit and extract These files still live in `//chrome/test:test_support` sources. Each needs a dep audit before extraction. | Files | Likely complexity | Notes | |-------|------------------|-------| | `find_result_waiter.{cc,h}` | Low | Probably just `//base` + find-in-page deps | | `scoped_browser_locale.{cc,h}` | Low | Probably just locale deps | | `scoped_metrics_service_for_synthetic_trials.{cc,h}` | Low | Metrics deps only | | `chrome_test_utils.{cc,h}` | Low–medium | Used by many tests; check for profile/browser deps | | `tracing.{cc,h}` | Low–medium | Tracing deps | | `web_feature_histogram_tester.{cc,h}` | Low–medium | Histogram/metrics deps | | `search_test_utils.{cc,h}` | Medium | Search service deps | | `web_ui_mocha_browser_test.{cc,h}` | Medium | WebUI + browser test harness deps | | `chrome_test_suite.{cc,h}`, `chrome_unit_test_suite.{cc,h}` | High | Test suite infrastructure; many deps | | `chrome_render_view_host_test_harness.{cc,h}`, `chrome_render_view_test.{cc,h}` | High | Render process deps; may require staging | | `test_launcher_utils.cc`, `in_process_browser_test.cc`, `ui_test_utils.cc` | Blocked | Blocked by `//chrome/browser/ui:test_support → //chrome/test:test_support_ui` cycle; thin targets already extracted | ### Long-term blocker — `browser/ui:test_support` cycle The three thin targets (`:test_launcher_utils`, `:in_process_browser_test`, `:ui_test_utils`) have their `.cc` impls stuck in the monolith because moving them would require resolving: ``` //chrome/browser/ui:test_support → //chrome/test:test_support_ui → (would dep on) //chrome/browser/ui ``` Breaking this requires either a Pattern A split of `//chrome/browser/ui:test_support` or a pre-extraction of whatever `//chrome/browser/ui:test_support` needs from `test_support_ui`. Deferred until the Bedrock team's `chrome/browser/ui` modularization creates a natural opportunity. --- ## BUILD.gn Patterns ### New per-directory `BUILD.gn` ```gn # chrome/test/interaction/BUILD.gn source_set("test_support") { testonly = true sources = [ "interactive_browser_test.cc", "interactive_browser_test.h", # ... ] public_deps = [ "//ui/base/interaction:interaction", ] deps = [ "//chrome/test/base:in_process_browser_test", "//chrome/browser/ui", "//content/public/test:test_support", ] # testonly=true prevents production code from depending here regardless # of visibility. visibility = [ "//chrome/browser/*", "//chrome/test/*", ] } ``` ### Root `chrome/test/BUILD.gn` after modularization ```gn static_library("test_support") { testonly = true # Before: "interaction/interactive_browser_test.cc", ... # After: dep on the child target public_deps = [ "//chrome/test/base:test_support", "//chrome/test/interaction:test_support", "//chrome/test/user_education:test_support", # ... ] } ``` ### Validation commands ```sh gn check out/linux # desktop gn check out/cros # ChromeOS (catches ash/extensions include cycles) gn check out/android # Android ``` Always run all three before uploading — platform-specific deps can hide gn check failures that only surface in one config. --- ## Handling Circular Dependencies ### Background: dep-graph cycle vs. include cycle GN cycle detection operates on the **dep graph** (`deps`/`public_deps` entries), not on `#include` lines. Two targets can mutually include each other's headers at compile time while keeping the dep graph a DAG — as long as only one dep edge exists and `allow_circular_includes_from` is declared. > [!info] Key insight > `allow_circular_includes_from` is a **header-include permission** — invisible to the dep graph. If target A has B in its direct deps, A can declare `allow_circular_includes_from = ["//B"]` to permit B's sources to `#include` A's headers. GN sees a DAG, `gn check` stays green. ### Case 1 — `interaction:interaction` / `test_support_ui` (include cycle) `test_support_ui` deps on `interaction:interaction` (via `public_deps`), but interaction source files include headers still compiled inside `test_support_ui` (`in_process_browser_test.h`, etc.). A naïve reading suggests a cycle — but it isn't, because `interaction:interaction` has **no dep edge back** to `test_support_ui`. ``` test_support_ui ──public_deps──► interaction:interaction test_support_ui ──allow_circular_includes_from──► interaction:interaction ↑ interaction's files may #include test_support_ui headers ``` A related issue: `interaction:interaction` needed `test_switches.h` from `test_support`. Adding `//chrome/test:test_support` as a dep would close a true dep-graph cycle (`test_support_ui → interaction → test_support → test_support_ui`). Fix: extract `test_switches.{h,cc}` into a thin `//chrome/test:test_switches` with zero deps. > [!warning] > `allow_circular_includes_from` cannot rescue a **true dep-graph cycle** — GN errors regardless. True cycles require structural fixes (pre-extraction of a thin target, Pattern A split, etc.). ### Case 2 — `testing_profile` / `testing_browser_process` extraction (reverse-dep problem) When extracting `testing_profile` and `testing_browser_process` into standalone targets, `gn check` failed on three ash targets and one extensions target whose source files include `testing_profile.h` or `testing_browser_process.h`: | Target | Includes | |---|---| | `//chrome/browser/ash/settings:test_support` | both `testing_profile.h` and `testing_browser_process.h` | | `//chrome/browser/ash/system_web_apps/test_support` | `testing_browser_process.h` (×2 files) | | `//chrome/browser/extensions:test_support` | `testing_browser_process.h` | Adding a direct dep from these targets to the new targets would create a dep-graph cycle because `testing_profile` and `testing_browser_process` *already depend on* those ash/extensions targets. **Interim fix:** `allow_circular_includes_from` in the extracted targets: ```gn source_set("testing_profile") { allow_circular_includes_from = [] # initialize; extended in blocks below if (enable_extensions_core) { deps += [ "//chrome/browser/extensions:test_support" ] # TODO(crbug.com/353332589): Remove with Pattern A split. allow_circular_includes_from += [ "//chrome/browser/extensions:test_support" ] } if (is_chromeos) { public_deps += [ "//chrome/browser/ash/settings:test_support" ] # TODO(crbug.com/353332589): Remove with Pattern A split. allow_circular_includes_from += [ "//chrome/browser/ash/settings:test_support" ] } } source_set("testing_browser_process") { allow_circular_includes_from = [ "//chrome/test/base:testing_profile" ] # existing if (enable_extensions_core) { deps += [ "//chrome/browser/extensions", "//chrome/browser/extensions:test_support", # new direct dep (was only transitive) ] # TODO(crbug.com/353332589): Remove with Pattern A split. allow_circular_includes_from += [ "//chrome/browser/extensions:test_support" ] } if (is_chromeos) { deps += [ "//chrome/browser/ash/policy/core", "//chrome/browser/media_galleries", "//chrome/browser/ash/settings:test_support", # new "//chrome/browser/ash/system_web_apps/test_support", # new ] # TODO(crbug.com/353332589): Remove with Pattern A split. allow_circular_includes_from += [ "//chrome/browser/ash/settings:test_support", "//chrome/browser/ash/system_web_apps/test_support", ] } } ``` **Key constraint:** The target listed in `allow_circular_includes_from` must be a **DIRECT** dep of the declaring target. If it was only transitive before, add it as a direct dep first. **CL6 — Pattern A split (`chromod/chrome-test-base-6`):** Splits `testing_profile` into: - `testing_profile` — thin header-only (`.h` only; on cros, still needs `ash/settings:test_support` because `testing_profile.h` has a `ScopedCrosSettingsTestHelper` by-value member) - `testing_profile_impl` — full `.cc` + heavy deps (ash, extensions) Result: `extensions:test_support` can now dep directly on `testing_profile` (header-only) — the extensions `allow_circular_includes_from` entry in `testing_profile` is removed. The cros case (`ash/settings:test_support`) still requires `allow_circular_includes_from` because `testing_profile.h` directly includes from `ash/settings:test_support` (preventing `ash/settings` from deping back). **Remaining deferred:** - *cros*: Forward-declare `ScopedCrosSettingsTestHelper` in `testing_profile.h`, use `unique_ptr` for the member → header dep on `ash/settings:test_support` moves to `.cc` → `ash/settings:test_support` can dep on `testing_profile` directly → remove `allow_circular_includes_from` from `testing_profile` and TBP. - *all platforms*: Remove `testing_profile_manager.cc`'s include of `testing_browser_process.h` → TBP can drop `allow_circular_includes_from = ["testing_profile_impl"]`. --- ## Explicit Dep Audit (required for "done") After extracting a new module, a required follow-up step is a dep scope audit: find every external consumer of the new module's headers and narrow `public_deps` to `deps` wherever safe. Without this step, the new module leaks transitively through the monolith just as the original sources did. ### Checklist 1. For each header in the new module, run `gn refs` to find all external GN targets. 2. For each depending target: check whether any of its *own* public headers include the new module's headers. If not, move the new module from `public_deps` to `deps`. 3. Where it must stay in `public_deps`, add a `TODO(crbug.com/353332589)` naming the specific public header that blocks the cleanup. ### Example: `chrome/test/views` `test_support_ui`: `//chrome/test/views` moved `public_deps` → `deps`. Safe because `in_process_browser_test.h` (the public header) includes no views headers. `test_support`: `//chrome/test/views` stays in `public_deps` — blocked by `browser_with_test_window_test.h`, which uses `ChromeTestViewsDelegate<>` as a concrete member field (full definition required). Unblocks when that header is componentized in Phase 2c. --- ## References - [Project Bedrock design doc](https://chromium.googlesource.com/chromium/src/+/main/docs/modularization.md) - `chrome/test/interaction/BUILD.gn` — first CL in this effort; reference for BUILD.gn pattern - `chrome/test/base/BUILD.gn` — active extraction target - `chrome/test/BUILD.gn` — monolith, being reduced - Bug tracker: [crbug.com/353332589](https://crbug.com/353332589)