From 7e049b3eaa17cf01f726ee8ff70da4dcf7557f87 Mon Sep 17 00:00:00 2001 From: jgershen-oai Date: Mon, 28 Sep 2026 23:12:39 +0000 Subject: [PATCH] Balance Windows Bazel test shards using duration estimates (#49103) ## Why Hash-based sharding does not account for differences in test duration. Use estimated test time to balance the Windows Bazel workload across shards. ## What changed - Add checked-in duration weights and a deterministic selector that assigns the longest targets first to the least-loaded shard. - Assign every queried target exactly once, default missing weights to one second, and ignore stale weights. - Run queries and selection in the foreground so failures stop the job before tests run with a partial target list. - Emit LF-delimited target labels for Git Bash and report estimated shard loads on stderr. ## Testing Add seven passing selector tests covering deterministic coverage and balancing, missing and stale weights, invalid inputs, duration-file overrides, and CLI output. Run them on the first Windows CI shard. GitOrigin-RevId: d415ae093bfe57794ee53a2eb9d712022202dad8 --- .../scripts/select_windows_bazel_targets.py | 103 +++++++ .../test_select_windows_bazel_targets.py | 147 ++++++++++ .../scripts/windows_bazel_test_durations.tsv | 253 ++++++++++++++++++ .github/workflows/bazel.yml | 39 ++- 4 files changed, 520 insertions(+), 22 deletions(-) create mode 100644 .github/scripts/select_windows_bazel_targets.py create mode 100644 .github/scripts/test_select_windows_bazel_targets.py create mode 100644 .github/scripts/windows_bazel_test_durations.tsv diff --git a/.github/scripts/select_windows_bazel_targets.py b/.github/scripts/select_windows_bazel_targets.py new file mode 100644 index 0000000000..226bd70402 --- /dev/null +++ b/.github/scripts/select_windows_bazel_targets.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 + +"""Assign Windows Bazel test targets using checked-in duration estimates.""" + +import argparse +import sys +from pathlib import Path + + +DEFAULT_DURATION_SECONDS = 1 +DURATIONS_PATH = Path(__file__).with_name("windows_bazel_test_durations.tsv") + + +def read_durations(path: Path) -> dict[str, int]: + durations = {} + for line_number, line in enumerate( + path.read_text(encoding="utf-8").splitlines(), 1 + ): + if not line.strip() or line.lstrip().startswith("#"): + continue + fields = line.split("\t") + if len(fields) != 2 or not fields[0] or not fields[1].isdecimal(): + raise ValueError( + f"{path}:{line_number}: expected targetpositive integer" + ) + target, duration_text = fields + duration = int(duration_text) + if duration < 1: + raise ValueError(f"{path}:{line_number}: duration must be positive") + if target in durations: + raise ValueError(f"{path}:{line_number}: duplicate target {target}") + durations[target] = duration + return durations + + +def assign_targets( + targets: list[str], durations: dict[str, int], shard_count: int +) -> tuple[list[list[str]], list[int]]: + if shard_count < 1: + raise ValueError("shard count must be positive") + if not targets: + raise ValueError("no Bazel test targets were provided") + if any(not target.strip() for target in targets): + raise ValueError("Bazel test target labels must not be empty") + if len(targets) != len(set(targets)): + raise ValueError("duplicate Bazel test target labels were provided") + + assignments: list[list[str]] = [[] for _ in range(shard_count)] + loads = [0] * shard_count + # Place the longest targets first, breaking ties by label and shard number. + for target in sorted( + targets, + key=lambda target: (-durations.get(target, DEFAULT_DURATION_SECONDS), target), + ): + shard = min(range(shard_count), key=lambda shard: (loads[shard], shard)) + assignments[shard].append(target) + loads[shard] += durations.get(target, DEFAULT_DURATION_SECONDS) + for assignment in assignments: + assignment.sort() + return assignments, loads + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--shard", required=True, type=int) + parser.add_argument("--shard-count", required=True, type=int) + parser.add_argument( + "--durations", + type=Path, + default=DURATIONS_PATH, + help="use this timing file instead of the checked-in default", + ) + args = parser.parse_args() + if not 1 <= args.shard <= args.shard_count: + parser.error("shard must be between 1 and shard count") + + try: + durations = read_durations(args.durations) + targets = sys.stdin.read().splitlines() + assignments, loads = assign_targets(targets, durations, args.shard_count) + except (OSError, ValueError) as error: + parser.error(str(error)) + + selected_targets = assignments[args.shard - 1] + if not selected_targets: + parser.error( + f"no Bazel test targets selected for shard {args.shard}/{args.shard_count}" + ) + + target_set = set(targets) + print( + f"Windows Bazel shards: targets={[len(assignment) for assignment in assignments]}, " + f"estimated test-seconds={loads}, default weights={len(target_set - durations.keys())}, " + f"unused weights={len(durations.keys() - target_set)}. " + f"Selected shard {args.shard}/{args.shard_count}.", + file=sys.stderr, + ) + # Keep LF delimiters when native Windows Python feeds Git Bash mapfile. + sys.stdout.buffer.write(("\n".join(selected_targets) + "\n").encode("utf-8")) + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/test_select_windows_bazel_targets.py b/.github/scripts/test_select_windows_bazel_targets.py new file mode 100644 index 0000000000..0c708ba4c5 --- /dev/null +++ b/.github/scripts/test_select_windows_bazel_targets.py @@ -0,0 +1,147 @@ +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +from select_windows_bazel_targets import DURATIONS_PATH +from select_windows_bazel_targets import assign_targets +from select_windows_bazel_targets import read_durations + + +class SelectWindowsBazelTargetsTest(unittest.TestCase): + def test_balances_targets_with_complete_deterministic_coverage(self) -> None: + durations = { + f"//test:{name}": duration + for name, duration in zip("abcdefgh", range(8, 0, -1)) + } + targets = list(durations) + + assignments, loads = assign_targets(targets, durations, 4) + + self.assertEqual(loads, [9, 9, 9, 9]) + self.assertEqual( + sorted(target for shard in assignments for target in shard), sorted(targets) + ) + self.assertEqual( + assign_targets(list(reversed(targets)), durations, 4), (assignments, loads) + ) + self.assertTrue(all(shard == sorted(shard) for shard in assignments)) + + def test_missing_weights_use_one_and_unused_weights_do_not_create_targets( + self, + ) -> None: + targets = ["//test:c", "//test:a", "//test:b"] + + assignments, loads = assign_targets(targets, {"//test:stale": 100}, 2) + + self.assertEqual(assignments, [["//test:a", "//test:c"], ["//test:b"]]) + self.assertEqual(loads, [2, 1]) + + def test_rejects_empty_or_duplicate_target_input(self) -> None: + for targets in ([], [""], ["//test:a", "//test:a"]): + with self.subTest(targets=targets), self.assertRaises(ValueError): + assign_targets(targets, {}, 4) + + def test_reads_duration_file_and_rejects_invalid_weights(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + path = Path(temp_dir) / "durations.tsv" + path.write_text( + "# comment\n\n//test:a\t12\n//test:b\t1\n", encoding="utf-8" + ) + self.assertEqual(read_durations(path), {"//test:a": 12, "//test:b": 1}) + + for contents in ( + "//test:a 12\n", + "//test:a\t0\n", + "//test:a\t-1\n", + "//test:a\t1.5\n", + "//test:a\t1\n//test:a\t2\n", + ): + with self.subTest(contents=contents), self.assertRaises(ValueError): + path.write_text(contents, encoding="utf-8") + read_durations(path) + + def test_cli_uses_checked_in_weights_and_writes_only_lf_delimited_targets( + self, + ) -> None: + durations = read_durations(DURATIONS_PATH) + known = sorted(durations, key=lambda target: (-durations[target], target))[:4] + self.assertEqual(len(known), 4) + targets = [*known, "//test:new-target"] + expected, _ = assign_targets(targets, durations, 4) + script = DURATIONS_PATH.with_name("select_windows_bazel_targets.py") + + for shard, assignment in enumerate(expected, 1): + command = [ + sys.executable, + str(script), + "--shard", + str(shard), + "--shard-count", + "4", + ] + for ordered_targets in (targets, list(reversed(targets))): + with self.subTest(shard=shard, ordered_targets=ordered_targets): + result = subprocess.run( + command, + input=("\r\n".join(ordered_targets) + "\r\n").encode(), + capture_output=True, + check=True, + ) + self.assertEqual( + result.stdout, ("\n".join(assignment) + "\n").encode() + ) + self.assertIn(b"default weights=1", result.stderr) + + def test_cli_uses_only_the_requested_duration_file(self) -> None: + script = DURATIONS_PATH.with_name("select_windows_bazel_targets.py").resolve() + with tempfile.TemporaryDirectory() as temp_dir: + path = Path(temp_dir) / "durations.tsv" + path.write_text("//test:a\t1\n//test:b\t9\n", encoding="utf-8") + command = [sys.executable, str(script), "--shard-count", "2"] + for shard, expected in ((1, b"//test:b\n"), (2, b"//test:a\n//test:c\n")): + with self.subTest(shard=shard): + result = subprocess.run( + [*command, "--shard", str(shard), "--durations", path.name], + input=b"//test:a\n//test:b\n//test:c\n", + cwd=temp_dir, + capture_output=True, + check=True, + ) + self.assertEqual(result.stdout, expected) + self.assertIn(b"estimated test-seconds=[9, 2]", result.stderr) + self.assertIn(b"unused weights=0", result.stderr) + + result = subprocess.run( + [*command, "--shard", "1", "--durations", "missing.tsv"], + input=b"//test:a\n", + cwd=temp_dir, + capture_output=True, + check=False, + ) + self.assertNotEqual(result.returncode, 0) + self.assertEqual(result.stdout, b"") + self.assertIn(b"missing.tsv", result.stderr) + + def test_cli_rejects_bad_or_empty_selection_without_partial_output(self) -> None: + script = DURATIONS_PATH.with_name("select_windows_bazel_targets.py") + for targets, error in ( + (b"", b"no Bazel test targets were provided"), + (b"//test:a\n//test:a\n", b"duplicate Bazel test target labels"), + (b"//test:a\n", b"no Bazel test targets selected for shard 2/2"), + ): + with self.subTest(targets=targets): + result = subprocess.run( + [sys.executable, str(script), "--shard", "2", "--shard-count", "2"], + input=targets, + capture_output=True, + check=False, + ) + self.assertNotEqual(result.returncode, 0) + self.assertEqual(result.stdout, b"") + self.assertIn(error, result.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/scripts/windows_bazel_test_durations.tsv b/.github/scripts/windows_bazel_test_durations.tsv new file mode 100644 index 0000000000..32bcc24783 --- /dev/null +++ b/.github/scripts/windows_bazel_test_durations.tsv @@ -0,0 +1,253 @@ +# Windows Bazel duration weights; columns: targetseconds. +# Seeded from 24 successful public openai/codex Windows job logs across six main +# runs, September 21-23, 2026. Public GitHub Actions run IDs: +# 35638529858, 35665540272, 35694732932, 35912411800, 35919297566, 35924169500. +# To refresh, take the median summed inner-run time (N * average) from clean, +# uncached PASSED summaries, excluding partial cache hits and FLAKY results. +# Round up to positive integer seconds. These weights exclude build/download time. +# Every queried target is assigned; missing weights default to 1 and stale entries are ignored. +//codex-rs/agent-graph-store:agent-graph-store-unit-tests 76 +//codex-rs/agent-identity:agent-identity-unit-tests 66 +//codex-rs/agent-roles:agent-roles-unit-tests 48 +//codex-rs/analytics:analytics-unit-tests 29 +//codex-rs/app-server-client:app-server-client-unit-tests 51 +//codex-rs/app-server-daemon:app-server-daemon-unit-tests 4 +//codex-rs/app-server-protocol:app-server-protocol-unit-tests 19 +//codex-rs/app-server-test-client:app-server-test-client-unit-tests 25 +//codex-rs/app-server-test-client:codex-app-server-test-client-bin-unit-tests 1 +//codex-rs/app-server-transport:app-server-transport-unit-tests 65 +//codex-rs/app-server/tests/common:common-unit-tests 26 +//codex-rs/app-server:app-server-all-test-windows-cross 2835 +//codex-rs/app-server:app-server-unit-tests 399 +//codex-rs/app-server:codex-app-server-bin-unit-tests 48 +//codex-rs/app-server:codex-app-server-test-notify-capture-bin-unit-tests 1 +//codex-rs/app-server:exec-server-bin-unit-tests 1 +//codex-rs/app-server:test_notify_capture-bin-unit-tests 1 +//codex-rs/apply-patch:apply-patch-all-test-windows-cross 50 +//codex-rs/apply-patch:apply-patch-unit-tests 48 +//codex-rs/apply-patch:apply_patch-bin-unit-tests 24 +//codex-rs/arg0:arg0-unit-tests 24 +//codex-rs/backend-client:backend-client-unit-tests 41 +//codex-rs/chatgpt:chatgpt-all-test-windows-cross 26 +//codex-rs/chatgpt:chatgpt-unit-tests 27 +//codex-rs/cli:cli-app_server-test-windows-cross 3 +//codex-rs/cli:cli-app_server_daemon-test-windows-cross 1 +//codex-rs/cli:cli-cloud_auth-test-windows-cross 47 +//codex-rs/cli:cli-cloud_config-test-windows-cross 3 +//codex-rs/cli:cli-daemon_startup-test-windows-cross 4 +//codex-rs/cli:cli-daemon_telemetry-test-windows-cross 1 +//codex-rs/cli:cli-debug_clear_memories-test-windows-cross 4 +//codex-rs/cli:cli-debug_models-test-windows-cross 2 +//codex-rs/cli:cli-delete-test-windows-cross 2 +//codex-rs/cli:cli-doctor_enterprise_network-test-windows-cross 3 +//codex-rs/cli:cli-doctor_path_safety-test-windows-cross 47 +//codex-rs/cli:cli-exec_server-test-windows-cross 39 +//codex-rs/cli:cli-exec_server_websocket_auth-test-windows-cross 4 +//codex-rs/cli:cli-execpolicy-test-windows-cross 2 +//codex-rs/cli:cli-features-test-windows-cross 3 +//codex-rs/cli:cli-login-test-windows-cross 4 +//codex-rs/cli:cli-marketplace_add-test-windows-cross 2 +//codex-rs/cli:cli-marketplace_remove-test-windows-cross 1 +//codex-rs/cli:cli-marketplace_upgrade-test-windows-cross 2 +//codex-rs/cli:cli-mcp_add_remove-test-windows-cross 3 +//codex-rs/cli:cli-mcp_list-test-windows-cross 2 +//codex-rs/cli:cli-mcp_login-test-windows-cross 2 +//codex-rs/cli:cli-plugin_cli-test-windows-cross 7 +//codex-rs/cli:cli-queue-test-windows-cross 1 +//codex-rs/cli:cli-sandbox_cloud_config-test-windows-cross 48 +//codex-rs/cli:cli-sandbox_network_proxy-test-windows-cross 1 +//codex-rs/cli:cli-sandbox_tty-test-windows-cross 1 +//codex-rs/cli:cli-unit-tests 2 +//codex-rs/cli:cli-update-test-windows-cross 47 +//codex-rs/cli:cli-worktree-test-windows-cross 12 +//codex-rs/cli:codex-bin-unit-tests 60 +//codex-rs/cli:logs_client-bin-unit-tests 1 +//codex-rs/cloud-config:cloud-config-unit-tests 47 +//codex-rs/cloud-tasks-client:cloud-tasks-client-unit-tests 35 +//codex-rs/cloud-tasks-mock-client:cloud-tasks-mock-client-unit-tests 59 +//codex-rs/cloud-tasks:cloud-tasks-env_filter-test-windows-cross 1 +//codex-rs/cloud-tasks:cloud-tasks-unit-tests 1 +//codex-rs/code-mode-host:code-mode-host-grpc-test-windows-cross 28 +//codex-rs/code-mode-host:code-mode-host-grpc_notifications-test-windows-cross 25 +//codex-rs/code-mode-host:code-mode-host-grpc_tcp-test-windows-cross 1 +//codex-rs/code-mode-host:code-mode-host-grpc_timing-test-windows-cross 7 +//codex-rs/code-mode-host:code-mode-host-stdio-test-windows-cross 11 +//codex-rs/code-mode-host:code-mode-host-unit-tests 2 +//codex-rs/code-mode-host:codex-code-mode-host-bin-unit-tests 1 +//codex-rs/code-mode-protocol:code-mode-protocol-unit-tests 62 +//codex-rs/code-mode-runtime:code-mode-runtime-array_sort-test-windows-cross 24 +//codex-rs/code-mode-runtime:code-mode-runtime-jit-test-windows-cross 1 +//codex-rs/code-mode-runtime:code-mode-runtime-unit-tests 7 +//codex-rs/code-mode:code-mode-unit-tests 49 +//codex-rs/codex-api:codex-api-clients-test-windows-cross 75 +//codex-rs/codex-api:codex-api-models_integration-test-windows-cross 66 +//codex-rs/codex-api:codex-api-realtime_websocket_e2e-test-windows-cross 63 +//codex-rs/codex-api:codex-api-realtime_websocket_tls-test-windows-cross 66 +//codex-rs/codex-api:codex-api-sse_end_to_end-test-windows-cross 48 +//codex-rs/codex-api:codex-api-unit-tests 60 +//codex-rs/codex-home:codex-home-unit-tests 13 +//codex-rs/codex-mcp:codex-mcp-unit-tests 61 +//codex-rs/config-schema:codex-write-config-schema-bin-unit-tests 48 +//codex-rs/config:config-unit-tests 75 +//codex-rs/connectors:connectors-unit-tests 39 +//codex-rs/context-fragments:context-fragments-unit-tests 59 +//codex-rs/core-api:core-api-unit-tests 1 +//codex-rs/core-plugins:core-plugins-unit-tests 20 +//codex-rs/core/tests/common:common-unit-tests 48 +//codex-rs/core:core-all-test-windows-cross 3162 +//codex-rs/core:core-mcp_trace_propagation-test-windows-cross 2 +//codex-rs/core:core-responses_headers-test-windows-cross 5 +//codex-rs/core:core-unit-tests 251 +//codex-rs/exec-server-protocol:exec-server-protocol-unit-tests 48 +//codex-rs/exec-server/tests/support:support-unit-tests 48 +//codex-rs/exec-server:exec-server-accepted_websocket-test-windows-cross 1 +//codex-rs/exec-server:exec-server-capability_discovery-test-windows-cross 1 +//codex-rs/exec-server:exec-server-chatgpt_cloudflare_affinity-test-windows-cross 1 +//codex-rs/exec-server:exec-server-deferred_environment-test-windows-cross 1 +//codex-rs/exec-server:exec-server-environment-test-windows-cross 10 +//codex-rs/exec-server:exec-server-environment_config-test-windows-cross 1 +//codex-rs/exec-server:exec-server-exec_process-test-windows-cross 1 +//codex-rs/exec-server:exec-server-file_stream-test-windows-cross 1 +//codex-rs/exec-server:exec-server-file_system_unix-test-windows-cross 1 +//codex-rs/exec-server:exec-server-file_system_windows-test-windows-cross 11 +//codex-rs/exec-server:exec-server-forward-test-windows-cross 10 +//codex-rs/exec-server:exec-server-health-test-windows-cross 1 +//codex-rs/exec-server:exec-server-http_client-test-windows-cross 2 +//codex-rs/exec-server:exec-server-http_request-test-windows-cross 1 +//codex-rs/exec-server:exec-server-http_request_logging-test-windows-cross 3 +//codex-rs/exec-server:exec-server-initialize-test-windows-cross 1 +//codex-rs/exec-server:exec-server-macos_fs_spawn-test-windows-cross 1 +//codex-rs/exec-server:exec-server-process-test-windows-cross 4 +//codex-rs/exec-server:exec-server-relay-test-windows-cross 8 +//codex-rs/exec-server:exec-server-selected_capability_roots-test-windows-cross 1 +//codex-rs/exec-server:exec-server-unit-tests 116 +//codex-rs/exec-server:exec-server-websocket-test-windows-cross 1 +//codex-rs/exec:codex-exec-bin-unit-tests 1 +//codex-rs/exec:exec-all-test-windows-cross 331 +//codex-rs/exec:exec-event_processor_with_json_output-test-windows-cross 1 +//codex-rs/exec:exec-unit-tests 47 +//codex-rs/ext/agent-message-board:agent-message-board-local_board-test-windows-cross 46 +//codex-rs/ext/agent-message-board:agent-message-board-unit-tests 30 +//codex-rs/ext/agent:agent-agent_service-test-windows-cross 3 +//codex-rs/ext/agent:agent-unit-tests 1 +//codex-rs/ext/connectors:connectors-unit-tests 47 +//codex-rs/ext/extension-api:extension-api-capabilities-test-windows-cross 14 +//codex-rs/ext/extension-api:extension-api-registry-test-windows-cross 31 +//codex-rs/ext/extension-api:extension-api-state-test-windows-cross 57 +//codex-rs/ext/extension-api:extension-api-unit-tests 35 +//codex-rs/ext/git-attribution:git-attribution-unit-tests 30 +//codex-rs/ext/goal:goal-accounting-test-windows-cross 1 +//codex-rs/ext/goal:goal-goal_extension_backend-test-windows-cross 9 +//codex-rs/ext/goal:goal-steering-test-windows-cross 1 +//codex-rs/ext/goal:goal-unit-tests 47 +//codex-rs/ext/guardian-reviewer:guardian-reviewer-unit-tests 36 +//codex-rs/ext/guardian-v2:guardian-v2-unit-tests 50 +//codex-rs/ext/history-notes:history-notes-history_notes_extension-test-windows-cross 1 +//codex-rs/ext/history-notes:history-notes-image_model_input-test-windows-cross 2 +//codex-rs/ext/history-notes:history-notes-unit-tests 1 +//codex-rs/ext/image-generation:image-generation-unit-tests 47 +//codex-rs/ext/mcp:mcp-hosted_apps_mcp-test-windows-cross 26 +//codex-rs/ext/mcp:mcp-plugin_mcp-test-windows-cross 5 +//codex-rs/ext/mcp:mcp-unit-tests 48 +//codex-rs/ext/memories:memories-unit-tests 28 +//codex-rs/ext/queue:queue-queue_service-test-windows-cross 9 +//codex-rs/ext/queue:queue-unit-tests 25 +//codex-rs/ext/skills:skills-executor_file_system_authority-test-windows-cross 36 +//codex-rs/ext/skills:skills-skills_extension-test-windows-cross 7 +//codex-rs/ext/skills:skills-unit-tests 8 +//codex-rs/ext/web-search:web-search-unit-tests 1 +//codex-rs/external-agent-migration:external-agent-migration-unit-tests 4 +//codex-rs/features:features-unit-tests 47 +//codex-rs/feedback:feedback-unit-tests 77 +//codex-rs/file-system:file-system-unit-tests 48 +//codex-rs/git-utils:git-utils-unit-tests 83 +//codex-rs/guardian-context:guardian-context-cache_prefix-test-windows-cross 66 +//codex-rs/guardian-context:guardian-context-unit-tests 67 +//codex-rs/history:history-unit-tests 59 +//codex-rs/hooks:hooks-unit-tests 16 +//codex-rs/hooks:write_hooks_schema_fixtures-bin-unit-tests 48 +//codex-rs/lmstudio:lmstudio-unit-tests 1 +//codex-rs/login:login-all-test-windows-cross 5 +//codex-rs/login:login-unit-tests 320 +//codex-rs/memories/read:read-unit-tests 62 +//codex-rs/memories/write:write-unit-tests 20 +//codex-rs/message-history:message-history-unit-tests 74 +//codex-rs/model-provider-info:model-provider-info-unit-tests 63 +//codex-rs/model-provider:model-provider-unit-tests 64 +//codex-rs/models-manager:models-manager-unit-tests 35 +//codex-rs/mxc-sandbox:mxc-sandbox-unit-tests 36 +//codex-rs/network-proxy:codex-network-proxy-bin-unit-tests 48 +//codex-rs/network-proxy:network-proxy-unit-tests 85 +//codex-rs/network-proxy:network-proxy-windows_stable_ingress-test-windows-cross 37 +//codex-rs/ollama:ollama-unit-tests 1 +//codex-rs/otel:otel-tests-test-windows-cross 17 +//codex-rs/otel:otel-unit-tests 48 +//codex-rs/plugin:plugin-unit-tests 74 +//codex-rs/prompts:prompts-unit-tests 62 +//codex-rs/protocol:protocol-unit-tests 67 +//codex-rs/response-debug-context:response-debug-context-unit-tests 66 +//codex-rs/rmcp-client:rmcp-client-foreign_stdio_cwd-test-windows-cross 48 +//codex-rs/rmcp-client:rmcp-client-mcp_2026_discovery-test-windows-cross 4 +//codex-rs/rmcp-client:rmcp-client-mcp_2026_message_limits-test-windows-cross 4 +//codex-rs/rmcp-client:rmcp-client-mcp_2026_mrtr-test-windows-cross 2 +//codex-rs/rmcp-client:rmcp-client-mcp_2026_oauth_discovery-test-windows-cross 1 +//codex-rs/rmcp-client:rmcp-client-mcp_2026_sse_discovery-test-windows-cross 1 +//codex-rs/rmcp-client:rmcp-client-mcp_2026_stdio-test-windows-cross 39 +//codex-rs/rmcp-client:rmcp-client-mcp_2026_stdio_discovery-test-windows-cross 2 +//codex-rs/rmcp-client:rmcp-client-mcp_events-test-windows-cross 2 +//codex-rs/rmcp-client:rmcp-client-process_group_cleanup-test-windows-cross 1 +//codex-rs/rmcp-client:rmcp-client-resources-test-windows-cross 1 +//codex-rs/rmcp-client:rmcp-client-stdio_descriptor_boundary-test-windows-cross 1 +//codex-rs/rmcp-client:rmcp-client-stdio_message_limits-test-windows-cross 3 +//codex-rs/rmcp-client:rmcp-client-stdio_stderr_cleanup-test-windows-cross 1 +//codex-rs/rmcp-client:rmcp-client-streamable_http_oauth_startup-test-windows-cross 6 +//codex-rs/rmcp-client:rmcp-client-streamable_http_oauth_store_pinning-test-windows-cross 38 +//codex-rs/rmcp-client:rmcp-client-streamable_http_oauth_tool_call-test-windows-cross 2 +//codex-rs/rmcp-client:rmcp-client-streamable_http_recovery-test-windows-cross 51 +//codex-rs/rmcp-client:rmcp-client-streamable_http_remote-test-windows-cross 3 +//codex-rs/rmcp-client:rmcp-client-streamable_http_test_support-test-windows-cross 1 +//codex-rs/rmcp-client:rmcp-client-streamable_http_user_agent-test-windows-cross 1 +//codex-rs/rmcp-client:rmcp-client-unit-tests 50 +//codex-rs/rmcp-client:rmcp_test_server-bin-unit-tests 39 +//codex-rs/rmcp-client:test_mcp_2026_discovery_stdio_server-bin-unit-tests 43 +//codex-rs/rmcp-client:test_mcp_2026_stdio_server-bin-unit-tests 25 +//codex-rs/rmcp-client:test_stdio_server-bin-unit-tests 41 +//codex-rs/rmcp-client:test_streamable_http_server-bin-unit-tests 40 +//codex-rs/rollout-trace:rollout-trace-unit-tests 76 +//codex-rs/rollout:rollout-unit-tests 56 +//codex-rs/sandboxing:sandboxing-unit-tests 74 +//codex-rs/secrets:secrets-unit-tests 57 +//codex-rs/shell-command:shell-command-unit-tests 60 +//codex-rs/shell-escalation:codex-execve-wrapper-bin-unit-tests 62 +//codex-rs/shell-escalation:shell-escalation-unit-tests 60 +//codex-rs/skills:skills-unit-tests 59 +//codex-rs/state:state-unit-tests 242 +//codex-rs/test-binary-support:test-binary-support-unit-tests 48 +//codex-rs/thread-manager-sample:codex-thread-manager-sample-bin-unit-tests 47 +//codex-rs/thread-store:thread-store-unit-tests 95 +//codex-rs/tools:tools-json_schema_policy_fixtures-test-windows-cross 34 +//codex-rs/tools:tools-unit-tests 37 +//codex-rs/tui:codex-tui-bin-unit-tests 2 +//codex-rs/tui:md-events-bin-unit-tests 1 +//codex-rs/tui:tui-all-test-windows-cross 36 +//codex-rs/tui:tui-manager_dependency_regression-test-windows-cross 2 +//codex-rs/tui:tui-test_backend-test-windows-cross 25 +//codex-rs/tui:tui-unit-tests 1003 +//codex-rs/utils/approval-presets:approval-presets-unit-tests 66 +//codex-rs/utils/audio:audio-unit-tests 76 +//codex-rs/utils/cli:cli-unit-tests 64 +//codex-rs/utils/git-discovery:git-discovery-unit-tests 59 +//codex-rs/utils/oss:oss-unit-tests 1 +//codex-rs/utils/output-truncation:output-truncation-unit-tests 60 +//codex-rs/utils/plugins:plugins-unit-tests 36 +//codex-rs/utils/sandbox-summary:sandbox-summary-unit-tests 66 +//codex-rs/windows-sandbox-rs:codex-command-runner-bin-unit-tests 1 +//codex-rs/windows-sandbox-rs:codex-windows-managed-deny-probe-bin-unit-tests 1 +//codex-rs/windows-sandbox-rs:codex-windows-sandbox-setup-bin-unit-tests 1 +//codex-rs/windows-sandbox-rs:windows-sandbox-rs-helper_manifest-test-windows-cross 1 +//codex-rs/windows-sandbox-rs:windows-sandbox-rs-unit-tests 23 +//codex-rs/windows-sandbox-service:codex-windows-sandbox-service-bin-unit-tests 46 +//codex-rs/windows-sandbox-service:windows-sandbox-service-unit-tests 12 +//codex-rs/worktree:worktree-settings-test-windows-cross 59 +//codex-rs/worktree:worktree-unit-tests 76 +//codex-rs/worktree:worktree-worktree-test-windows-cross 92 diff --git a/.github/workflows/bazel.yml b/.github/workflows/bazel.yml index 568086a93d..ede5a7f070 100644 --- a/.github/workflows/bazel.yml +++ b/.github/workflows/bazel.yml @@ -160,6 +160,11 @@ jobs: shell: pwsh run: python .github/scripts/test_run_bazel_with_buildbuddy.py + - name: Test Windows Bazel target selector + if: matrix.shard == 1 + shell: pwsh + run: python -m unittest discover -s .github/scripts -p test_select_windows_bazel_targets.py + - name: Prepare Bazel CI id: prepare_bazel uses: ./.github/actions/prepare-bazel-ci @@ -180,29 +185,19 @@ jobs: set -euo pipefail bazel_test_query='tests(//...) except tests(//third_party/v8:all) except attr(tags, "manual", tests(//...))' - mapfile -t bazel_targets < <( - ./.github/scripts/run-bazel-query-ci.sh --output=label -- "${bazel_test_query}" \ - | LC_ALL=C sort - ) + bazel_targets_file="$(mktemp)" + selected_targets_file="$(mktemp)" + trap 'rm -f "${bazel_targets_file}" "${selected_targets_file}"' EXIT - selected_targets=() - for bazel_target in "${bazel_targets[@]}"; do - target_bucket="$( - printf '%s\n' "${bazel_target}" \ - | cksum \ - | awk -v shard_count="${BAZEL_TEST_SHARD_COUNT}" '{ print ($1 % shard_count) + 1 }' - )" - if [[ "${target_bucket}" == "${BAZEL_TEST_SHARD}" ]]; then - selected_targets+=("${bazel_target}") - fi - done - - if [[ ${#selected_targets[@]} -eq 0 ]]; then - echo "No Bazel test targets selected for Windows shard ${BAZEL_TEST_SHARD}/${BAZEL_TEST_SHARD_COUNT}." >&2 - exit 1 - fi - - echo "Selected ${#selected_targets[@]} of ${#bazel_targets[@]} Bazel test targets for Windows shard ${BAZEL_TEST_SHARD}/${BAZEL_TEST_SHARD_COUNT}." + # Keep query and selection in the foreground so failures cannot leave + # different jobs assigning different partial target lists. + ./.github/scripts/run-bazel-query-ci.sh --output=label -- "${bazel_test_query}" \ + | LC_ALL=C sort > "${bazel_targets_file}" + python .github/scripts/select_windows_bazel_targets.py \ + --shard "${BAZEL_TEST_SHARD}" \ + --shard-count "${BAZEL_TEST_SHARD_COUNT}" \ + < "${bazel_targets_file}" > "${selected_targets_file}" + mapfile -t selected_targets < "${selected_targets_file}" bazel_test_args=( test