Motivation
rules_itest intentionally gives every service_test a fresh service graph, as clarified in #27. That should remain the default.
For expensive service graphs, however, users may have many independent tests that can safely share one graph. Running one service_test per member repeats setup, while replacing the tests with one aggregate runner loses Bazel's per-test cache keys, reporting, retries, sharding, and XML results.
I'd like to propose an explicitly opt-in shared mode with these semantics:
native.test_suite(
name = "_raw_storage_tests",
tests = [
":reader_test",
":writer_test",
],
tags = ["manual"],
)
service_test(
name = "storage_tests",
test = ":_raw_storage_tests",
services = [":storage_environment"],
shared_service_key = "storage-tests",
)
The exact public API is one of the decisions below.
Proposed behavior
- Expand an explicit suite into one ordinary Bazel test wrapper per member. Bazel continues to cache, schedule, shard, retry, and report every member independently.
- Coordinate compatible executing wrappers through a generic host-local broker keyed by output base/configuration and an opaque
shared_service_key.
- The first cache-miss wrapper elects an owner and starts the existing
itest_service graph. Other executing wrappers acquire authenticated IPC leases and receive its runtime environment.
- If every member is a Bazel test-cache hit, no wrapper executes, so no broker or service starts.
- Closing a wrapper's connection releases its lease. After zero leases for a configurable grace period—30 seconds by default—the owner runs the existing dependency-ordered shutdown.
- Startup failure is broadcast to waiting wrappers. Test exit, failure, cancellation, service failure, owner failure, SIGINT, and SIGTERM release leases and trigger bounded cleanup. Stale coordination state is recoverable.
- The mechanism remains resource-agnostic: it knows about
itest_service graphs and opaque environment values, not databases, Redis, Docker, or downstream-specific policy.
- Existing executable-target
service_test behavior remains unchanged when shared mode is not requested.
Deliberate limitations
This is an opt-in, non-hermetic execution mode:
- Initially Linux and macOS only, using Unix-domain IPC.
- Shared wrappers require
no-sandbox and no-remote-exec, while retaining Bazel test-result caching.
- It is not a strict Bazel invocation identity. Bazel exposes no test-side "last suite member" callback. The lifecycle coalesces overlapping or nearby executing actions using leases plus the idle grace period.
- Cleanup cannot be guaranteed after SIGKILL, kernel failure, or host loss, although subsequent executions recover stale discovery state.
- Different output bases/configurations do not share a graph.
Decisions requested
-
Public Starlark API: should service_test(test = ":suite") be supported by turning the exported symbol into a symbolic finalizer macro and retaining the raw rule under another name, or would an additive API such as shared_service_test_suite(...) be preferable to avoid changing the existing symbol?
-
Shared-state model: is an authenticated host-local owner/broker with connection leases and idle shutdown acceptable as an explicitly non-hermetic mode, or would maintainers prefer a different ownership boundary?
-
Platform and execution scope: is Linux/macOS plus no-sandbox/no-remote-exec an acceptable initial contract, with Windows and remote execution rejected clearly?
-
Timeout semantics: the prototype separates acquisition/startup, underlying test-body, idle, and shutdown timeouts. Should Bazel's wrapper timeout cover the complete owner-supervision lifecycle while a separate test_timeout begins only after services are ready? Should generated wrappers inherit each member's timeout rather than defaulting to eternal?
A first implementation can deliberately support only concrete, direct, same-package suites with explicit members. Nested, implicit tag-based, configurable, and cross-package suites would fail clearly rather than acquire surprising semantics.
Prior art and evidence
I searched the existing issues and PRs. #27 is the closest related issue, but documents the current no-sharing behavior rather than proposing an opt-in model. #69 concerns external services and is orthogonal.
If the execution model and API direction are acceptable, I can split the implementation into reviewable prerequisite, broker/lifecycle, Starlark expansion, and generic end-to-end test changes.
Motivation
rules_itestintentionally gives everyservice_testa fresh service graph, as clarified in #27. That should remain the default.For expensive service graphs, however, users may have many independent tests that can safely share one graph. Running one
service_testper member repeats setup, while replacing the tests with one aggregate runner loses Bazel's per-test cache keys, reporting, retries, sharding, and XML results.I'd like to propose an explicitly opt-in shared mode with these semantics:
The exact public API is one of the decisions below.
Proposed behavior
shared_service_key.itest_servicegraph. Other executing wrappers acquire authenticated IPC leases and receive its runtime environment.itest_servicegraphs and opaque environment values, not databases, Redis, Docker, or downstream-specific policy.service_testbehavior remains unchanged when shared mode is not requested.Deliberate limitations
This is an opt-in, non-hermetic execution mode:
no-sandboxandno-remote-exec, while retaining Bazel test-result caching.Decisions requested
Public Starlark API: should
service_test(test = ":suite")be supported by turning the exported symbol into a symbolic finalizer macro and retaining the raw rule under another name, or would an additive API such asshared_service_test_suite(...)be preferable to avoid changing the existing symbol?Shared-state model: is an authenticated host-local owner/broker with connection leases and idle shutdown acceptable as an explicitly non-hermetic mode, or would maintainers prefer a different ownership boundary?
Platform and execution scope: is Linux/macOS plus
no-sandbox/no-remote-execan acceptable initial contract, with Windows and remote execution rejected clearly?Timeout semantics: the prototype separates acquisition/startup, underlying test-body, idle, and shutdown timeouts. Should Bazel's wrapper
timeoutcover the complete owner-supervision lifecycle while a separatetest_timeoutbegins only after services are ready? Should generated wrappers inherit each member's timeout rather than defaulting toeternal?A first implementation can deliberately support only concrete, direct, same-package suites with explicit members. Nested, implicit tag-based, configurable, and cross-package suites would fail clearly rather than acquire surprising semantics.
Prior art and evidence
I searched the existing issues and PRs. #27 is the closest related issue, but documents the current no-sharing behavior rather than proposing an opt-in model. #69 concerns external services and is orthogonal.
If the execution model and API direction are acceptable, I can split the implementation into reviewable prerequisite, broker/lifecycle, Starlark expansion, and generic end-to-end test changes.