diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..c46743f1 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Unified diff context lines contain a required single-space prefix. +third_party/rules_go_orchestrion/patches/**/0001-full-delta.patch whitespace=-blank-at-eol,-blank-at-eof diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0ab38611..fab5827a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,6 +39,8 @@ jobs: runs-on: ubuntu-latest outputs: docs_only: ${{ steps.classify.outputs.docs_only }} + rules_go_integration_matrix: ${{ steps.rules_go_matrix.outputs.integration_matrix }} + rules_go_upstreams: ${{ steps.rules_go_matrix.outputs.upstreams }} run_full_ci: ${{ steps.classify.outputs.run_full_ci }} steps: - name: Checkout @@ -46,6 +48,11 @@ jobs: with: fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + - name: Classify changed files id: classify shell: bash @@ -77,6 +84,14 @@ jobs: echo "${line}" >> "${GITHUB_OUTPUT}" done < <(./tools/dev/classify_ci_changes.sh "${base_sha}" "${head_sha}") + - name: Generate rules_go CI matrices + id: rules_go_matrix + shell: bash + run: | + python3 tools/dev/materialize_rules_go_fork.py list-upstreams | + python3 -c 'import json, sys; upstreams = [line.strip() for line in sys.stdin if line.strip()]; assert upstreams, "no rules_go upstreams found"; print("upstreams=" + json.dumps(upstreams, separators=(",", ":"))); print("integration_matrix=" + json.dumps({"upstream": upstreams, "module_system": ["workspace", "bzlmod"]}, separators=(",", ":")))' \ + >> "${GITHUB_OUTPUT}" + bazel-tests: needs: changes if: ${{ needs.changes.outputs.run_full_ci == 'true' }} @@ -84,7 +99,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - name: Checkout @@ -109,9 +124,113 @@ jobs: shell: bash run: ./bazelw test //tools/... --test_output=errors + - name: Build examples + timeout-minutes: 20 + shell: bash + run: | + if [[ "${{ runner.os }}" == "Windows" ]]; then + # Ubuntu and macOS build the full example matrix, including Go. + # Keep Windows coverage on the non-Go examples until the vendored + # rules_go Orchestrion stdlib action is stable there. + ./bazelw --output_user_root=C:/b build \ + -- \ + //examples/... \ + -//examples/single_service/src/go-project/... \ + -//examples/multi_service/src/go-project/... + else + ./bazelw build //examples/... + fi + + - name: Test examples + timeout-minutes: 20 + shell: bash + # --enable_runfiles is required on Windows so the dd_topt_java_test + # example can resolve -javaagent via $(rootpath); no-op on Linux/macOS. + run: | + if [[ "${{ runner.os }}" == "Windows" ]]; then + ./bazelw --output_user_root=C:/b test \ + --enable_runfiles \ + --test_output=errors \ + -- \ + //examples/... \ + -//examples/single_service/src/go-project/... \ + -//examples/multi_service/src/go-project/... + else + ./bazelw test //examples/... --test_output=errors + fi + + - name: Exercise single-service runtests script (dry-run) + shell: bash + # Keep CI deterministic and secret-free: this validates script wiring + # and command construction without requiring live Datadog credentials. + run: RUNTESTS_DRY_RUN=1 bash ./examples/single_service/runtests.sh + + - name: Exercise multi-service runtests script (dry-run) + shell: bash + # Keep CI deterministic and secret-free: this validates script wiring + # and command construction without requiring live Datadog credentials. + run: RUNTESTS_DRY_RUN=1 bash ./examples/multi_service/runtests.sh + + - name: Exercise example PowerShell runtests scripts (dry-run) + if: runner.os == 'Windows' + shell: pwsh + run: | + $env:RUNTESTS_DRY_RUN = "1" + foreach ($script in @( + "./examples/single_service/runtests.ps1", + "./examples/multi_service/runtests.ps1" + )) { + & $script + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } + } + + - name: Ensure jq is available (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + if (Get-Command jq -ErrorAction SilentlyContinue) { + jq --version + exit 0 + } + choco install jq --no-progress -y + jq --version + + # Runs the same integration harness on Windows. + - name: Run mock server integration tests (Windows) + if: runner.os == 'Windows' + timeout-minutes: 15 + shell: pwsh + run: ./tools/tests/integration/run_mock_server_tests.ps1 + + bazel-tests-ubuntu-main: + needs: changes + if: ${{ needs.changes.outputs.run_full_ci == 'true' }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Set up Go + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + # Fresh-run policy: keep this lane intentionally cacheless so every CI run + # re-evaluates repository rules from scratch. + # Guardrail: do not add Bazel cache steps (actions/cache, disk cache, + # remote cache) here without explicit maintainer approval. + - name: Run Bazel tests + shell: bash + run: ./bazelw test //tools/... --test_output=errors + - name: Run Bazel tests (go companion module) - if: matrix.os == 'ubuntu-latest' - timeout-minutes: 10 shell: bash run: | ( @@ -121,8 +240,6 @@ jobs: ) - name: Run Bazel tests (python companion module) - if: matrix.os == 'ubuntu-latest' - timeout-minutes: 10 shell: bash run: | ( @@ -132,8 +249,6 @@ jobs: ) - name: Run Bazel tests (java companion module) - if: matrix.os == 'ubuntu-latest' - timeout-minutes: 10 shell: bash run: | ( @@ -143,8 +258,6 @@ jobs: ) - name: Run Bazel tests (nodejs companion module) - if: matrix.os == 'ubuntu-latest' - timeout-minutes: 10 shell: bash run: | ( @@ -154,8 +267,6 @@ jobs: ) - name: Run Bazel tests (dotnet companion module) - if: matrix.os == 'ubuntu-latest' - timeout-minutes: 10 shell: bash run: | ( @@ -165,8 +276,6 @@ jobs: ) - name: Run Bazel tests (ruby companion module) - if: matrix.os == 'ubuntu-latest' - timeout-minutes: 10 shell: bash run: | ( @@ -176,87 +285,91 @@ jobs: ) - name: Build examples - timeout-minutes: 20 shell: bash - run: | - if [[ "${{ runner.os }}" == "Windows" ]]; then - # Linux/macOS already build the full example matrix, including Go. - # Keep Windows coverage on the non-Go examples until the vendored - # rules_go Orchestrion stdlib action is stable there. - ./bazelw --output_user_root=C:/b build \ - -- \ - //examples/... \ - -//examples/single_service/src/go-project/... \ - -//examples/multi_service/src/go-project/... - else - ./bazelw build //examples/... - fi + run: ./bazelw build //examples/... - name: Test examples - timeout-minutes: 20 shell: bash - # --enable_runfiles is required on Windows so the dd_topt_java_test - # example can resolve -javaagent via $(rootpath); no-op on Linux/macOS. - run: | - if [[ "${{ runner.os }}" == "Windows" ]]; then - ./bazelw --output_user_root=C:/b test \ - --enable_runfiles \ - --test_output=errors \ - -- \ - //examples/... \ - -//examples/single_service/src/go-project/... \ - -//examples/multi_service/src/go-project/... - else - ./bazelw test //examples/... --test_output=errors - fi + run: ./bazelw test //examples/... --test_output=errors - name: Exercise single-service runtests script (dry-run) shell: bash - # Keep CI deterministic and secret-free: this validates script wiring - # and command construction without requiring live Datadog credentials. run: RUNTESTS_DRY_RUN=1 bash ./examples/single_service/runtests.sh - name: Exercise multi-service runtests script (dry-run) shell: bash - # Keep CI deterministic and secret-free: this validates script wiring - # and command construction without requiring live Datadog credentials. run: RUNTESTS_DRY_RUN=1 bash ./examples/multi_service/runtests.sh - # Exercises the uploader end-to-end against a local mock server. - - name: Run mock server integration tests (Linux) - if: runner.os == 'Linux' - # The guided Orchestrion bootstrap coverage in this harness now performs - # a full cold-start bootstrap before the mocked runtime assertions. Keep - # this lane bounded, but give it enough headroom to finish on slower - # GitHub-hosted Linux runners. - timeout-minutes: 30 + bazel-tests-ubuntu-mock-server: + needs: changes + if: ${{ needs.changes.outputs.run_full_ci == 'true' }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Set up Go + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + # The harness owns isolated output bases, so it does not benefit from the + # Bazel state created by the main Ubuntu lane. + - name: Run mock server integration tests shell: bash run: ./tools/tests/integration/run_mock_server_tests.sh - - name: Ensure jq is available (Windows) - if: runner.os == 'Windows' - shell: pwsh + bazel-tests-ubuntu: + name: bazel-tests (ubuntu-latest) + needs: + - changes + - bazel-tests-ubuntu-main + - bazel-tests-ubuntu-mock-server + if: ${{ always() }} + runs-on: ubuntu-latest + steps: + - name: Require successful Ubuntu Bazel test shards + shell: bash + env: + CHANGES_RESULT: ${{ needs.changes.result }} + MAIN_RESULT: ${{ needs.bazel-tests-ubuntu-main.result }} + MOCK_SERVER_RESULT: ${{ needs.bazel-tests-ubuntu-mock-server.result }} + RUN_FULL_CI: ${{ needs.changes.outputs.run_full_ci }} run: | - if (Get-Command jq -ErrorAction SilentlyContinue) { - jq --version + if [[ "${CHANGES_RESULT}" != "success" ]]; then + echo "Change classification did not succeed: ${CHANGES_RESULT}" >&2 + exit 1 + fi + + if [[ "${RUN_FULL_CI}" != "true" ]]; then + echo "Ubuntu Bazel tests are not required for this change." exit 0 - } - choco install jq --no-progress -y - jq --version + fi - # Runs the same integration harness on Windows. - - name: Run mock server integration tests (Windows) - if: runner.os == 'Windows' - timeout-minutes: 15 - shell: pwsh - run: ./tools/tests/integration/run_mock_server_tests.ps1 + if [[ "${MAIN_RESULT}" != "success" ]]; then + echo "The main Ubuntu Bazel test shard did not succeed: ${MAIN_RESULT}" >&2 + exit 1 + fi - rules-go-variant-smoke: + if [[ "${MOCK_SERVER_RESULT}" != "success" ]]; then + echo "The Ubuntu mock-server shard did not succeed: ${MOCK_SERVER_RESULT}" >&2 + exit 1 + fi + + rules-go-variant-smoke-shard: + name: rules-go-variant-smoke-shard (${{ matrix.upstream }}) needs: changes if: ${{ needs.changes.outputs.run_full_ci == 'true' }} - # This job runs the smoke harness serially for every supported upstream. - # Keep enough headroom as the registry grows. - timeout-minutes: 75 + strategy: + fail-fast: false + max-parallel: 4 + matrix: + upstream: ${{ fromJSON(needs.changes.outputs.rules_go_upstreams) }} runs-on: ubuntu-latest steps: - name: Checkout @@ -285,17 +398,48 @@ jobs: - name: Run vendored rules_go smoke coverage shell: bash + env: + RULES_GO_UPSTREAM: ${{ matrix.upstream }} + RULES_GO_VARIANT: base + run: ./tools/dev/run_rules_go_variant_smoke.sh + + rules-go-variant-smoke: + needs: + - changes + - rules-go-variant-smoke-shard + if: ${{ always() }} + runs-on: ubuntu-latest + steps: + - name: Require successful rules_go variant smoke shards + shell: bash + env: + CHANGES_RESULT: ${{ needs.changes.result }} + RUN_FULL_CI: ${{ needs.changes.outputs.run_full_ci }} + SHARD_RESULT: ${{ needs.rules-go-variant-smoke-shard.result }} run: | - while IFS= read -r upstream; do - RULES_GO_UPSTREAM="${upstream}" \ - RULES_GO_VARIANT="base" \ - ./tools/dev/run_rules_go_variant_smoke.sh - done < <(python3 tools/dev/materialize_rules_go_fork.py list-upstreams) + if [[ "${CHANGES_RESULT}" != "success" ]]; then + echo "Change classification did not succeed: ${CHANGES_RESULT}" >&2 + exit 1 + fi - workspace-compat: + if [[ "${RUN_FULL_CI}" != "true" ]]; then + echo "rules_go variant smoke coverage is not required for this change." + exit 0 + fi + + if [[ "${SHARD_RESULT}" != "success" ]]; then + echo "One or more rules_go variant smoke shards did not succeed: ${SHARD_RESULT}" >&2 + exit 1 + fi + + workspace-compat-shard: + name: workspace-compat-shard (${{ matrix.upstream }}, ${{ matrix.module_system }}) needs: changes if: ${{ needs.changes.outputs.run_full_ci == 'true' }} - timeout-minutes: 90 + strategy: + fail-fast: false + max-parallel: 4 + matrix: ${{ fromJSON(needs.changes.outputs.rules_go_integration_matrix) }} runs-on: ubuntu-latest steps: - name: Checkout @@ -317,24 +461,160 @@ jobs: GO111MODULE=on go install github.com/bazelbuild/bazelisk@v1.28.1 echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" - - name: Validate Go companion consumer paths + - name: Validate general Go companion consumer path shell: bash + env: + MODULE_SYSTEM: ${{ matrix.module_system }} + ORCHESTRION_MODE: general + RULES_GO_UPSTREAM: ${{ matrix.upstream }} + RULES_GO_VARIANT: base + USE_BAZEL_VERSION: "8.4.1" run: | - while IFS= read -r upstream; do - for mode in general test_optimization; do - USE_BAZEL_VERSION=8.4.1 \ - RULES_GO_UPSTREAM="${upstream}" \ - RULES_GO_VARIANT="base" \ - ORCHESTRION_MODE="${mode}" \ - ./tools/tests/integration/run_workspace_go_integration.sh - - USE_BAZEL_VERSION=8.4.1 \ - RULES_GO_UPSTREAM="${upstream}" \ - RULES_GO_VARIANT="base" \ - ORCHESTRION_MODE="${mode}" \ - ./tools/tests/integration/run_bzlmod_go_integration.sh - done - done < <(python3 tools/dev/materialize_rules_go_fork.py list-upstreams) + case "${MODULE_SYSTEM}" in + workspace) + ./tools/tests/integration/run_workspace_go_integration.sh + ;; + bzlmod) + ./tools/tests/integration/run_bzlmod_go_integration.sh + ;; + *) + echo "Unsupported module system: ${MODULE_SYSTEM}" >&2 + exit 1 + ;; + esac + + - name: Validate Test Optimization Go companion consumer path + shell: bash + env: + MODULE_SYSTEM: ${{ matrix.module_system }} + ORCHESTRION_MODE: test_optimization + RULES_GO_UPSTREAM: ${{ matrix.upstream }} + RULES_GO_VARIANT: base + USE_BAZEL_VERSION: "8.4.1" + run: | + case "${MODULE_SYSTEM}" in + workspace) + ./tools/tests/integration/run_workspace_go_integration.sh + ;; + bzlmod) + ./tools/tests/integration/run_bzlmod_go_integration.sh + ;; + *) + echo "Unsupported module system: ${MODULE_SYSTEM}" >&2 + exit 1 + ;; + esac + + workspace-compat: + needs: + - changes + - workspace-compat-shard + if: ${{ always() }} + runs-on: ubuntu-latest + steps: + - name: Require successful workspace compatibility shards + shell: bash + env: + CHANGES_RESULT: ${{ needs.changes.result }} + RUN_FULL_CI: ${{ needs.changes.outputs.run_full_ci }} + SHARD_RESULT: ${{ needs.workspace-compat-shard.result }} + run: | + if [[ "${CHANGES_RESULT}" != "success" ]]; then + echo "Change classification did not succeed: ${CHANGES_RESULT}" >&2 + exit 1 + fi + + if [[ "${RUN_FULL_CI}" != "true" ]]; then + echo "Workspace compatibility coverage is not required for this change." + exit 0 + fi + + if [[ "${SHARD_RESULT}" != "success" ]]; then + echo "One or more workspace compatibility shards did not succeed: ${SHARD_RESULT}" >&2 + exit 1 + fi + + windows-go-bootstrap-shard: + name: windows-go-bootstrap-shard (${{ matrix.upstream }}, ${{ matrix.module_system }}) + needs: changes + if: ${{ needs.changes.outputs.run_full_ci == 'true' }} + strategy: + fail-fast: false + max-parallel: 4 + matrix: ${{ fromJSON(needs.changes.outputs.rules_go_integration_matrix) }} + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Set up Go + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Install Bazelisk + shell: bash + run: | + GO111MODULE=on go install github.com/bazelbuild/bazelisk@v1.28.1 + echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" + + - name: Validate disabled-to-enabled bootstrap transition + shell: bash + env: + BAZEL_VERSION: "8.4.1" + MODULE_SYSTEM: ${{ matrix.module_system }} + ORCHESTRION_MODE: test_optimization + RULES_GO_UPSTREAM: ${{ matrix.upstream }} + RULES_GO_VARIANT: base + WINDOWS_CONFIG_TRANSITION_ONLY: "1" + run: | + case "${MODULE_SYSTEM}" in + workspace) + ./tools/tests/integration/run_workspace_go_integration.sh + ;; + bzlmod) + ./tools/tests/integration/run_bzlmod_go_integration.sh + ;; + *) + echo "Unsupported module system: ${MODULE_SYSTEM}" >&2 + exit 1 + ;; + esac + + # Keep one consolidated result while the expensive work runs in parallel. + windows-go-bootstrap-smoke: + needs: + - changes + - windows-go-bootstrap-shard + if: ${{ always() }} + runs-on: ubuntu-latest + steps: + - name: Require successful Windows bootstrap shards + shell: bash + env: + CHANGES_RESULT: ${{ needs.changes.result }} + RUN_FULL_CI: ${{ needs.changes.outputs.run_full_ci }} + SHARD_RESULT: ${{ needs.windows-go-bootstrap-shard.result }} + run: | + if [[ "${CHANGES_RESULT}" != "success" ]]; then + echo "Change classification did not succeed: ${CHANGES_RESULT}" >&2 + exit 1 + fi + + if [[ "${RUN_FULL_CI}" != "true" ]]; then + echo "Windows Go bootstrap smoke is not required for this change." + exit 0 + fi + + if [[ "${SHARD_RESULT}" != "success" ]]; then + echo "One or more Windows Go bootstrap shards did not succeed: ${SHARD_RESULT}" >&2 + exit 1 + fi coverage-tools: needs: changes diff --git a/.github/workflows/shared-validation.yml b/.github/workflows/shared-validation.yml index 89cc51a8..d83de416 100644 --- a/.github/workflows/shared-validation.yml +++ b/.github/workflows/shared-validation.yml @@ -199,9 +199,30 @@ jobs: shell: bash run: python3 tools/core/schemas/check_schema_parser_parity.py - rules-go-fork-drift: + rules-go-fork-drift-matrix: + if: ${{ !inputs.docs_only && inputs.run_rules_go_fork_drift }} + runs-on: ubuntu-latest + outputs: + upstreams: ${{ steps.generate.outputs.upstreams }} + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: ${{ inputs.python_version }} + + - name: Generate rules_go profile matrix + id: generate + shell: bash + run: | + python3 tools/dev/materialize_rules_go_fork.py list-upstreams | + python3 -c 'import json, sys; upstreams = [line.strip() for line in sys.stdin if line.strip()]; assert upstreams, "no rules_go upstreams found"; print("upstreams=" + json.dumps(upstreams, separators=(",", ":")))' \ + >> "${GITHUB_OUTPUT}" + + rules-go-fork-drift-global: if: ${{ !inputs.docs_only && inputs.run_rules_go_fork_drift }} - timeout-minutes: 15 runs-on: ubuntu-latest steps: - name: Checkout @@ -229,13 +250,72 @@ jobs: python3 tools/dev/generate_rules_go_fork_maps.py --check RULES_GO_ORCHESTRION_CACHE="${RUNNER_TEMP}/rules_go_orchestrion_cache" \ python3 tools/dev/materialize_rules_go_fork.py check --all - python3 tools/dev/verify_rules_go_profiles.py \ - --public-denylist tools/dev/private_leak_public_denylist.txt - name: Verify rules_go fork release archive contents shell: bash run: python3 tools/dev/check_release_archive_contents.py + rules-go-fork-profile-shard: + name: rules-go-fork-profile-shard (${{ matrix.upstream }}) + needs: rules-go-fork-drift-matrix + if: ${{ !inputs.docs_only && inputs.run_rules_go_fork_drift }} + strategy: + fail-fast: false + max-parallel: 4 + matrix: + upstream: ${{ fromJSON(needs.rules-go-fork-drift-matrix.outputs.upstreams) }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: ${{ inputs.python_version }} + + - name: Set up Go + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version: "1.25.0" + + - name: Verify rules_go consumer patch profile + shell: bash + run: | + python3 tools/dev/verify_rules_go_profiles.py \ + --upstream "${{ matrix.upstream }}" \ + --public-denylist tools/dev/private_leak_public_denylist.txt + + rules-go-fork-drift: + needs: + - rules-go-fork-drift-matrix + - rules-go-fork-drift-global + - rules-go-fork-profile-shard + if: ${{ always() && !inputs.docs_only && inputs.run_rules_go_fork_drift }} + runs-on: ubuntu-latest + steps: + - name: Require successful rules_go fork drift shards + shell: bash + env: + GLOBAL_RESULT: ${{ needs.rules-go-fork-drift-global.result }} + MATRIX_RESULT: ${{ needs.rules-go-fork-drift-matrix.result }} + PROFILE_RESULT: ${{ needs.rules-go-fork-profile-shard.result }} + run: | + if [[ "${MATRIX_RESULT}" != "success" ]]; then + echo "rules_go profile matrix generation did not succeed: ${MATRIX_RESULT}" >&2 + exit 1 + fi + + if [[ "${GLOBAL_RESULT}" != "success" ]]; then + echo "Global rules_go fork drift validation did not succeed: ${GLOBAL_RESULT}" >&2 + exit 1 + fi + + if [[ "${PROFILE_RESULT}" != "success" ]]; then + echo "One or more rules_go profile shards did not succeed: ${PROFILE_RESULT}" >&2 + exit 1 + fi + powershell-lint: if: ${{ !inputs.docs_only && inputs.run_powershell_lint }} timeout-minutes: 20 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b50aa9d..aac1e692 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,22 @@ versioning. ## [Unreleased] +### Changed +- The public Go Bzlmod extension now defaults `enabled_by_env` to `True`, so + omitting `--config=test-optimization` disables metadata sync and Orchestrion + together while the named config enables both. +- Go consumers upgrading from `1.2.0` should rerun `dd_topt_go_bootstrap` with + `--write-bazelrc` before or with the Rule upgrade. The managed `.bazelrc` + update is idempotent and adds both metadata and Orchestrion activation to the + `test-optimization` config. Consumers that deliberately retain manual + always-enabled metadata may set `enabled_by_env = False`, but must also keep + the Orchestrion build setting enabled. + +### Fixed +- Go test analysis now fails with migration guidance when Test Optimization + metadata is enabled but the global Orchestrion build setting is disabled, + preventing a partial upgrade from silently dropping instrumentation. + ## [1.2.0] - 2026-06-03 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index abb7db0b..7cef28c7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,6 +26,9 @@ This product includes software developed at Datadog - Canonical full-repo command: - `./bazelw test //...` + - On macOS with Bazel 8.5.1, append + `--noexperimental_split_xml_generation`. The split XML helper can terminate + with `SIGSEGV` independently of the test target. - Core module tests (repo root): - `./bazelw test //tools/...` @@ -72,6 +75,22 @@ This product includes software developed at Datadog - Mixed-runtime uploader changes are not done until both harnesses still pass: they cover single-context, explicit override, multi-context repo selection, and no-match fallback behavior. +- Test Optimization bootstrap config: + - For config-gated Go and Python onboarding, keep + `--config=test-optimization` as the only user-facing switch. The shared + config entry is + `common:test-optimization --repo_env=DD_TEST_OPTIMIZATION_ENABLED=1`. + - Go additionally sets the existing `rules_go` Orchestrion `enabled=true` + build setting. Python-only consumers must not declare that Go-only label. + - Omitting the config is the documented complete opt-out for Go and Python: + metadata repositories use disabled stubs when `enabled_by_env = True`, Go + aliases select local empty targets, and Python keeps the consumer runner + without Test Optimization wiring. + - Other companions retain their existing enablement contract. Do not add + `enabled_by_env = True` to their sync repositories until their runtime + wrapper implements and tests the disabled export contract. + - Do not add a consumer-local Test Optimization bool flag or a second Go + Orchestrion repository chooser. - Go consumer integration harnesses: - Bzlmod default smoke: `tools/tests/integration/run_bzlmod_go_integration.sh` @@ -85,7 +104,22 @@ This product includes software developed at Datadog `USE_BAZEL_VERSION=8.4.1 RULES_GO_UPSTREAM=v0_61_1 RULES_GO_VARIANT=base tools/tests/integration/run_workspace_go_integration.sh` - Bzlmod base, rules_go v0_61_1: `USE_BAZEL_VERSION=8.4.1 RULES_GO_UPSTREAM=v0_61_1 RULES_GO_VARIANT=base tools/tests/integration/run_bzlmod_go_integration.sh` + - Disabled alias gate for a fresh output root: + `WINDOWS_DISABLED_SMOKE_ONLY=1 RULES_GO_UPSTREAM=v0_60_0 RULES_GO_VARIANT=base tools/tests/integration/run_workspace_go_integration.sh` + - Enabled alias and payload gate with valid Orchestrion pins: + `WINDOWS_ENABLED_SMOKE_ONLY=1 RULES_GO_UPSTREAM=v0_60_0 RULES_GO_VARIANT=base tools/tests/integration/run_workspace_go_integration.sh` + - Same-output-root disabled then enabled transition for the public central + wrapper: + `WINDOWS_CONFIG_TRANSITION_ONLY=1 RULES_GO_UPSTREAM=v0_60_0 RULES_GO_VARIANT=base tools/tests/integration/run_workspace_go_integration.sh` - Each script now validates: + - the same consumer-owned central wrapper call expands to a raw `go_test` + without the named config and to the existing Orchestrion-backed shape + with it + - disabled then enabled resolution on one fixture workspace and output + root, with no intervening clean or shutdown + - no hidden Test Optimization targets, empty Orchestrion aliases, exact + disabled stubs, zero metadata HTTP requests, and no payload while + disabled - normal mode - hermetic mode with the inline CI sandbox/network-blocking flags - strict BEP fresh/cached uploader behavior @@ -138,25 +172,28 @@ This product includes software developed at Datadog - `bazel-tests`: - core tests (`//tools/...`) on Linux/macOS/Windows - - go companion tests (`modules/go`) on Linux/macOS/Windows - - integration harness on Linux/macOS (`.sh`) and Windows (`.ps1`) + - companion module tests on Linux + - mock-server integration harness on Linux (`.sh`) and Windows (`.ps1`) - examples build on Linux/macOS/Windows + - the Linux result aggregates independent main-suite and mock-server shards - `bazel-tests-hermetic`: - core tests with hermetic flags - - go companion tests with hermetic flags + - companion module tests with hermetic flags - scope policy: Linux-only by design today; non-Linux hermetic expansion is tracked separately to keep CI runtime bounded - `workspace-compat`: - - WORKSPACE base - - Bzlmod base + - one shard per supported `rules_go` upstream and WORKSPACE/Bzlmod pair + - general and Test Optimization modes run sequentially inside each shard - the Go integration scripts themselves cover normal mode, hermetic mode, and structural `aquery` checks - `rules-go-variant-smoke`: - vendored `rules_go` variant verification and fast fork regression coverage + - one independent shard per supported upstream - Linux-only by design so the PR gate stays fast and stable - `rules-go-variant-extended`: - nightly/manual vendored `rules_go` variant coverage for slower XML, proto, cross, and cgo regression suites - Utility/lint lanes: - module version alignment check (`tools/dev/check_module_versions.py`) - `.bazelversion` parity check (`tools/dev/check_bazelversion_sync.py`) + - global fork drift checks plus one consumer patch-profile shard per supported `rules_go` upstream - shell scripts, PowerShell, Buildifier, gofmt, schema sync checks, fixture JSON checks, and Python tooling tests - Workflow dependency pinning: - Keep GitHub Actions pinned by commit SHA and preserve the `# vX.Y.Z` comment. diff --git a/MODULE.bazel b/MODULE.bazel index 4d486e2f..6b0643df 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -191,6 +191,15 @@ example_stub_repo = use_extension( ) example_stub_repo.example_stub_repo( name = "test_optimization_data", + enabled = False, + labels = [ + "apps_ruby_example", + "company_product_example", + "example_nodejs_project", + "example_python_modules_python_tests", + "example_python_pkg", + "example_python_tests", + ], service_keys = [ "go_service", "go_service_a", diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 57a19a53..383e2c45 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -341,8 +341,8 @@ }, "//tools/tests:example_stub_repo.bzl%example_stub_repo_extension": { "general": { - "bzlTransitiveDigest": "11ZYe98lW2gzeDxeH56YfpXqfu+Fz/W99x+6gbqhHpo=", - "usagesDigest": "ZvH9tnCNkZsHBe4vntqNT2rwFLibrjPI/w0NHRR5Lwg=", + "bzlTransitiveDigest": "TrTrDHJrP0Rre84rDDioXjXxdcPRZwzJZA21GdNKrkE=", + "usagesDigest": "9cmeLOZZK/XcmUYXh727qZj+Cgnz6JtINwWZZD6BKfg=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, "envVariables": {}, @@ -350,10 +350,18 @@ "test_optimization_data": { "repoRuleId": "@@//tools/tests:example_stub_repo.bzl%example_stub_repo", "attributes": { + "enabled": false, "go_module_included": false, "go_module_path": "example.com/stub", "go_sanitized_module_path": "example_com_stub", - "labels": [], + "labels": [ + "apps_ruby_example", + "company_product_example", + "example_nodejs_project", + "example_python_modules_python_tests", + "example_python_pkg", + "example_python_tests" + ], "out_dir": ".testoptimization", "repo_alias": "test_optimization_data", "service_name": "stub-service", diff --git a/README.md b/README.md index 89c10c2e..2efe4f4e 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,34 @@ or merge it locally instead of vendoring a second complete `rules_go` tree. Use this checklist before your first CI rollout: +For config-gated Go and Python onboarding, the named `test-optimization` +config must include +`common:test-optimization --repo_env=DD_TEST_OPTIMIZATION_ENABLED=1`. The +public Go bootstrap helpers apply metadata gating by default. Manual Go +extension wiring uses the same config-gated default. Direct use of the +low-level core sync API in a config-gated Go or Python setup must opt into +`enabled_by_env = True`. Go +additionally needs +`build:test-optimization --@rules_go//go/private/orchestrion:enabled=true` +for Bzlmod, or the same flag with `@io_bazel_rules_go` for WORKSPACE. +Removing `--config=test-optimization` provides the complete metadata and +runtime opt-out for the Go and Python integrations described below. This +release does not change the enablement contract of the other companions. + +When the selected Go sync export is disabled, `dd_topt_go_test` validates its +macro-only inputs and selected service, then calls the supplied `go_test_rule` +directly under the public target name with the caller's original Go rule +arguments. It does not create the hidden raw test, payload selector, Bazel +metadata, Orchestrion pin, or public wrapper targets. This lets a consumer keep +one central `dd_go_test` entry point while the named config decides whether the +same BUILD call is a raw `go_test` or the existing enabled Test Optimization +shape. + +When a config-gated Python sync is disabled, `dd_topt_py_test` keeps the +consumer's normal runner and test arguments, omits Test Optimization metadata +and payload wiring, and applies the CI Visibility runtime kill switch +automatically. No per-target disable attribute is required. + 1. Keep the generated repo name as `test_optimization_data` (or consistently replace it in labels/commands if you choose another name). 2. Forward sync metadata environment variable names in `.bazelrc` under a named config, then use that config for test, doctor, and upload commands: @@ -248,19 +276,17 @@ test_optimization_sync.test_optimization_sync( use_repo(test_optimization_sync, "test_optimization_data") ``` +The low-level core API remains always enabled by default. Set +`enabled_by_env = True` only when this repository is part of the config-gated +Go or Python onboarding described below. Other companions retain their current +enablement contract. + ```bzl # BUILD.bazel (workspace root) -load("@datadog-rules-test-optimization//tools/core:test_optimization_uploader.bzl", "dd_payload_uploader") -load("@datadog-rules-test-optimization//tools/core:test_optimization_doctor.bzl", "dd_test_optimization_doctor") - -dd_test_optimization_doctor( - name = "dd_test_optimization_doctor", - data = ["@test_optimization_data//:test_optimization_context"], -) +load("@datadog-rules-test-optimization//tools/core:test_optimization_targets.bzl", "dd_test_optimization_targets") -dd_payload_uploader( - name = "dd_upload_payloads", - data = ["@test_optimization_data//:test_optimization_context"], +dd_test_optimization_targets( + name = "test_optimization", ) ``` @@ -438,6 +464,11 @@ Go setup. Manual Go callsites should set ### Bzlmod + Python companion (`dd_topt_py_test`) +Configure the Python sync extension with `enabled_by_env = True` and put +`common:test-optimization --repo_env=DD_TEST_OPTIMIZATION_ENABLED=1` in the +named config. Python does not declare `rules_go` and must not copy the Go-only +Orchestrion build flag. + ```bzl bazel_dep(name = "datadog-rules-test-optimization-python", version = "1.2.0") git_override( @@ -483,7 +514,6 @@ dd_topt_py_test( name = "pkg_py_test", py_test_rule = my_repo_pytest_wrapper, runner_mode = "consumer_runner", - module_identifier = "example.python.pkg", srcs = glob(["test_*.py"]), deps = [ ":pkg_lib", @@ -497,8 +527,11 @@ dd_topt_py_test( In `consumer_runner` mode, pass a repository-owned `py_test_rule` wrapper or an explicit `main` that executes pytest with the ddtrace plugin enabled. The base `rules_python` `py_test` without `main` is rejected because it does not prove -pytest is actually running. Prefer `module_identifier` for payload selection in -this mode so the Datadog macro does not need to synthesize Python `imports`. +pytest is actually running. When the runtime module path and Bazel package path +identify the test, omit `module_identifier` and use the derived fallback. Pass +an explicit `module_identifier` only for a documented repository-specific +exception; the macro does not need to synthesize Python `imports` for the +normal path. Replace `@python_deps` with the repository name generated by your `rules_python` `pip_parse` / `pip.parse` setup. @@ -915,6 +948,7 @@ topt_go.test_optimization_sync( service = "go-service", runtime_name = "go", runtime_version = "1.25.0", + enabled_by_env = True, ) topt_ruby = use_extension( @@ -941,20 +975,11 @@ matching context target so validation and upload enrichment use the correct `context.json` per payload: ```bzl -load("@datadog-rules-test-optimization//tools/core:test_optimization_doctor.bzl", "dd_test_optimization_doctor") -load("@datadog-rules-test-optimization//tools/core:test_optimization_uploader.bzl", "dd_payload_uploader") - -dd_test_optimization_doctor( - name = "dd_test_optimization_doctor", - data = [ - "@test_optimization_data_go//:test_optimization_context", - "@test_optimization_data_ruby//:test_optimization_context", - ], -) +load("@datadog-rules-test-optimization//tools/core:test_optimization_targets.bzl", "dd_test_optimization_targets") -dd_payload_uploader( - name = "dd_upload_payloads", - data = [ +dd_test_optimization_targets( + name = "test_optimization", + context_data = [ "@test_optimization_data_go//:test_optimization_context", "@test_optimization_data_ruby//:test_optimization_context", ], @@ -983,6 +1008,11 @@ test_optimization_sync( ) ``` +This low-level WORKSPACE example preserves the always-enabled core default. +The public Go extension is config-gated by default. Config-gated Python +consumers set `enabled_by_env = True` through their language-specific setup; +the other companions remain unchanged. + For Go in WORKSPACE mode, keep the core and Go companion as separate external repositories and load `dd_topt_go_test` from `@datadog-rules-test-optimization-go//:topt_go_test.bzl`. Prefer the public @@ -1402,14 +1432,14 @@ bep_json="$(mktemp "${TMPDIR:-/tmp}/dd-topt-bep.XXXXXX.json")" artifact_staging_dir="$(mktemp -d "${TMPDIR:-/tmp}/dd-topt-artifacts.XXXXXX")" bazel test --config=test-optimization --build_event_json_file="$bep_json" //... -bazel run //:dd_test_optimization_doctor -- \ +bazel run --config=test-optimization //:dd_test_optimization_doctor -- \ --bep-json="$bep_json" \ --freshness-source=bep \ --freshness-mode=required \ --artifact-source=bep \ --artifact-staging-dir="$artifact_staging_dir" -bazel run //:dd_upload_payloads -- \ +bazel run --config=test-optimization //:dd_upload_payloads -- \ --bep-json="$bep_json" \ --freshness-source=bep \ --freshness-mode=required \ @@ -1478,7 +1508,7 @@ selective remote download flags, enable remote BEP artifact staging. Plain HTTP/HTTPS `outputs.zip` BEP carriers do not need a downloader: ```bash -bazel run //:dd_upload_payloads -- \ +bazel run --config=test-optimization //:dd_upload_payloads -- \ --bep-json="$bep_json" \ --freshness-source=bep \ --freshness-mode=required \ @@ -1563,8 +1593,10 @@ For complete uploader details, use [`docs/Uploader_Reference.md`](docs/Uploader_ ## Convenience macro: dd_topt_go_test The `dd_topt_go_test` macro creates a `go_test` target with Datadog Test -Optimization data/env wiring included, and always runs through an internal -Orchestrion-enabled wrapper target. +Optimization data/env wiring included when the selected sync export is enabled. +When that export is disabled, it delegates directly to the supplied +`go_test_rule` with the public name and original caller kwargs; no +Test Optimization-owned hidden targets or argument mutations are created. By default, it sets `rundir` to the current Bazel package when not explicitly provided. If you enable `stage_sources = True`, it instead defaults `rundir` @@ -1596,6 +1628,19 @@ go_topt.test_optimization_go( use_repo(go_topt, "test_optimization_data") ``` +The Go extension is config-gated by default, so the normal onboarding does not +need an enablement attribute. Consumers upgrading from `1.2.0` must add the +named config before updating the Rule; rerun `dd_topt_go_bootstrap` with +`--write-bazelrc` for an idempotent migration. A consumer that deliberately +keeps manually controlled, always-enabled metadata can set +`enabled_by_env = False`, but it must also keep the Orchestrion build setting +enabled; analysis rejects a partial activation. + +Consumer-owned central wrappers may always delegate capable packages to +`dd_topt_go_test`. The generated export and named config choose the raw or +instrumented shape, so BUILD callsites do not need a Test Optimization +attribute or a second macro name. + `module_path` should match the Go module path from `go.mod`. The sync rule still honors `GO_MODULE_PATH` first for CI overrides, but the explicit attr is the recommended default because it avoids repo-local `--repo_env` glue. @@ -1882,9 +1927,9 @@ Fast checks before diving deep: - Verify sync env forwarding (`DD_API_KEY`, `DD_SITE`, and required `DD_GIT_*`) through `--repo_env`, not `--test_env`. - Force metadata refresh only when you intentionally need fresh backend state: - - `bazel sync --only= --repo_env=FETCH_SALT="$(date +%s)"` + - `bazel sync --config=test-optimization --only= --repo_env=FETCH_SALT="$(date +%s)"` - If Bazel reports WORKSPACE-disabled sync errors, retry with: - `bazel sync --enable_workspace --only= --repo_env=FETCH_SALT="$(date +%s)"` + `bazel sync --enable_workspace --config=test-optimization --only= --repo_env=FETCH_SALT="$(date +%s)"` - Do not add `FETCH_SALT` to normal `bazel test`, doctor, or uploader commands. - Confirm payload files exist under `bazel-testlogs/*/test.outputs/`, or that diff --git a/docs/Configuration_Reference.md b/docs/Configuration_Reference.md index 7e6fae58..0aac3432 100644 --- a/docs/Configuration_Reference.md +++ b/docs/Configuration_Reference.md @@ -364,6 +364,27 @@ invocation. They do not include `FETCH_SALT`, `DD_GIT_*` test env, `DD_API_KEY` test env, upload endpoint test env, or `DD_CIVISIBILITY_AGENTLESS_ENABLED`. +For Go onboarding, the generated block also contains +`common: --repo_env=DD_TEST_OPTIMIZATION_ENABLED=1` and the existing +`rules_go` analysis setting +`build: --@//go/private/orchestrion:enabled=true`. +`--config=` is the single user-facing switch: removing it disables +both metadata resolution and the real Orchestrion aliases. + +For Go, the disabled generated export also changes the macro expansion. +`dd_topt_go_test` performs its macro-input and service-selection validation, +then invokes the supplied `go_test_rule(name = name, **kwargs)` directly. It +does not run importpath inference, select metadata payloads, inject Datadog +environment/data/linker inputs, or create hidden Test Optimization or +Orchestrion targets. The enabled export preserves the existing instrumented +expansion. Consumer-owned wrappers can therefore expose one public Go test +macro in both modes. + +Config-gated Python onboarding uses the same +`DD_TEST_OPTIMIZATION_ENABLED=1` repository environment entry but does not use +the Go-only Orchestrion setting. The Java, NodeJS, .NET, and Ruby companions +retain their existing enablement contract in this release. + ## How data is fetched The sync rule executes HTTP requests with timeouts/retries to: diff --git a/docs/Installation_Reference.md b/docs/Installation_Reference.md index 73cdbe83..39ea1450 100644 --- a/docs/Installation_Reference.md +++ b/docs/Installation_Reference.md @@ -206,6 +206,12 @@ test_optimization_sync.test_optimization_sync( use_repo(test_optimization_sync, "test_optimization_data") ``` +The low-level core sync remains always enabled by default. The public Go +extension is config-gated by default; config-gated Python onboarding sets +`enabled_by_env = True` in its language-specific setup. Do not add that +attribute to another companion until its runtime wrapper implements the +disabled export contract. + Core module note: `datadog-rules-test-optimization` is runtime-agnostic and does not declare language-rule dependencies. Language-specific orchestration lives in companion modules: @@ -387,6 +393,8 @@ Use `--write-bazelrc` to insert or replace the managed The generated config is named `test-optimization` by default: ```text +common:test-optimization --repo_env=DD_TEST_OPTIMIZATION_ENABLED=1 +build:test-optimization --@rules_go//go/private/orchestrion:enabled=true common:test-optimization --repo_env=DD_API_KEY common:test-optimization --repo_env=DD_SITE common:test-optimization --repo_env=DD_GIT_REPOSITORY_URL @@ -414,8 +422,9 @@ repeatable `--bep-json=` flags. This keeps parallel CI jobs and repeated local runs from overwriting or reusing stale BEP files. `FETCH_SALT` is intentionally not part of the generated default config. Use it -only in a separate force-refresh `bazel sync --only=` command when you -deliberately want fresh backend metadata. +only in a separate force-refresh +`bazel sync --config=test-optimization --only=` command when you +deliberately want fresh backend metadata from a config-gated repository. Run Go onboarding commands with this config: @@ -471,7 +480,7 @@ For a first-pass support request after tests have run, the customer can run only the doctor: ```bash -bazel run //:dd_test_optimization_doctor -- \ +bazel run --config=test-optimization //:dd_test_optimization_doctor -- \ --support-bundle .topt/reports/dd-test-optimization-support.zip ``` @@ -540,8 +549,10 @@ Use the default `runner_mode = "managed_pytest"` when the Datadog macro should own pytest execution. Use `runner_mode = "consumer_runner"` when a repository already has a Python test wrapper and must keep control of `main`, `imports`, and internal test policy. In `consumer_runner` mode, pass a custom -`py_test_rule` or an explicit `main` that runs pytest with ddtrace enabled, and -prefer `module_identifier` for payload selection. +`py_test_rule` or an explicit `main` that runs pytest with ddtrace enabled. When +the runtime module path and Bazel package path identify the test, omit +`module_identifier` and use the derived fallback. Keep an explicit +`module_identifier` only for a documented repository-specific exception. Python consumers can generate copy/paste onboarding snippets from the companion without running tests or changing lockfiles: @@ -602,10 +613,16 @@ test_optimization_sync = use_extension( "@datadog-rules-test-optimization//tools/core:test_optimization_sync.bzl", "test_optimization_sync_extension", ) -test_optimization_sync.test_optimization_sync(name = "test_optimization_data") +test_optimization_sync.test_optimization_sync( + name = "test_optimization_data", +) use_repo(test_optimization_sync, "test_optimization_data") ``` +This core-only example preserves the always-enabled contract. Add +`enabled_by_env = True` only when deliberately composing the config-gated Go or +Python flow. + ### Multi-service usage (Bzlmod) Fetch multiple services with one extension and select per-service data by label: @@ -623,6 +640,7 @@ topt_multi.test_optimization_multi_sync( runtime_name = "python", runtime_version = "3.12", debug = True, + enabled_by_env = True, ) use_repo( @@ -784,6 +802,10 @@ test_optimization_sync( ) ``` +This generic WORKSPACE example preserves the always-enabled core default. The +public Go helpers apply metadata gating by default; the Python section adds +`enabled_by_env = True` where its macro can safely consume a disabled export. + ### 3) Depend on generated files in BUILD files ```bzl @@ -802,18 +824,10 @@ filegroup( ```bzl # In root BUILD.bazel -load("@datadog-rules-test-optimization//tools/core:test_optimization_doctor.bzl", "dd_test_optimization_doctor") -load("@datadog-rules-test-optimization//tools/core:test_optimization_uploader.bzl", "dd_payload_uploader") - -dd_test_optimization_doctor( - name = "dd_test_optimization_doctor", - data = ["@test_optimization_data//:test_optimization_context"], -) +load("@datadog-rules-test-optimization//tools/core:test_optimization_targets.bzl", "dd_test_optimization_targets") -dd_payload_uploader( - name = "dd_upload_payloads", - # Provide context.json via runfiles so enrichment can occur - data = ["@test_optimization_data//:test_optimization_context"], +dd_test_optimization_targets( + name = "test_optimization", ) ``` @@ -834,17 +848,9 @@ bazel run --config=test-optimization //:dd_upload_payloads -- \ Multi-service aggregator variant: ```bzl -dd_test_optimization_doctor( - name = "dd_test_optimization_doctor", - data = [ - "@test_optimization_data//:test_optimization_context_service_a", - "@test_optimization_data//:test_optimization_context_service_b", - ], -) - -dd_payload_uploader( - name = "dd_upload_payloads", - data = [ +dd_test_optimization_targets( + name = "test_optimization", + context_data = [ "@test_optimization_data//:test_optimization_context_service_a", "@test_optimization_data//:test_optimization_context_service_b", ], @@ -853,6 +859,18 @@ dd_payload_uploader( ### 5) Forward environment variables in `.bazelrc` +The metadata forwarding entries below apply to every runtime. Config-gated Go +and Python onboarding additionally includes: + +```text +common:test-optimization --repo_env=DD_TEST_OPTIMIZATION_ENABLED=1 +# Go only; use the apparent rules_go repository name: +build:test-optimization --@io_bazel_rules_go//go/private/orchestrion:enabled=true +``` + +Python-only consumers omit the Go line. Java, NodeJS, .NET, and Ruby retain +their existing enablement contract and omit both lines in this release. + ```text # Repository rule (module/repo phase) — affects refetch common:test-optimization --repo_env=DD_API_KEY @@ -1051,9 +1069,14 @@ test_optimization_sync( service = "py-service", runtime_name = "python", runtime_version = "3.12", + enabled_by_env = True, ) ``` +Pair this opt-in repository rule with +`common:test-optimization --repo_env=DD_TEST_OPTIMIZATION_ENABLED=1`. Python +does not use the Go-only `@rules_go//go/private/orchestrion:enabled` flag. + Then in package BUILD files, use either managed pytest mode: ```bzl @@ -1085,7 +1108,6 @@ dd_topt_py_test( name = "pkg_py_test", py_test_rule = repo_py_test, runner_mode = "consumer_runner", - module_identifier = "example.python.pkg", srcs = glob(["test_*.py"]), deps = [ requirement("ddtrace"), @@ -1236,18 +1258,18 @@ http_archive( load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies") load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies") -load("@io_bazel_rules_go//go:orchestrion_workspace.bzl", "go_orchestrion_tool_repo") +load("@datadog-rules-test-optimization-go//:topt_go_orchestrion_repository.bzl", "dd_topt_go_orchestrion_tool_repo") -go_rules_dependencies() -go_register_toolchains(version = "1.25.0") -gazelle_dependencies() - -go_orchestrion_tool_repo( +dd_topt_go_orchestrion_tool_repo( version = "", # Optional. When omitted, the helper uses the fork's current default # shared dd-trace-go version. dd_trace_go_version = "", ) + +go_rules_dependencies() +go_register_toolchains(version = "1.25.0") +gazelle_dependencies() ``` Notes for the helper: @@ -1256,6 +1278,9 @@ Notes for the helper: - `dd_trace_go_version` and `dd_trace_go_versions` are mutually exclusive. - Keep the default tool-repo name `rules_go_orchestrion_tool`; the current fork resolves that name internally. +- Declare the real tool repository before `go_rules_dependencies()`. The fork + supplies its own empty fallback for ordinary WORKSPACE consumers, so do not + load or declare a private stub repository yourself. - Do not configure `patches`, `patch_tool`, or `patch_args` for this integration; repositories that already own their `rules_go` patch stack should generate the public consumer patch profile and rebase or merge it locally. diff --git a/docs/Language_Onboarding.md b/docs/Language_Onboarding.md index c680c168..586f3a25 100644 --- a/docs/Language_Onboarding.md +++ b/docs/Language_Onboarding.md @@ -48,9 +48,7 @@ Shared runtime contract for every language: - `DD_TEST_OPTIMIZATION_CONTEXT_JSON` remains a legacy explicit override, not the recommended mixed-runtime wiring path -Shared `.bazelrc` forwarding. Prefer the generated block from -`dd_topt_go_bootstrap --print-bazelrc-snippet` or -`dd_topt_go_bootstrap --write-bazelrc` for Go workspaces: +Shared `.bazelrc` metadata forwarding for every runtime: ```text common:test-optimization --repo_env=DD_API_KEY @@ -65,6 +63,27 @@ test:test-optimization --remote_download_regex=.*test[.]outputs.* test:test-optimization --zip_undeclared_test_outputs ``` +Go and Python config-gated onboarding also adds the single enable switch below. +Omitting the named config then provides the complete metadata and runtime +opt-out for those two integrations. This release does not change the +enablement contract of the other companions: + +```text +common:test-optimization --repo_env=DD_TEST_OPTIMIZATION_ENABLED=1 +``` + +Go workspaces also add the existing `rules_go` analysis-time setting. Prefer +the generated block from `dd_topt_go_bootstrap --print-bazelrc-snippet` or +`dd_topt_go_bootstrap --write-bazelrc`, which substitutes the consumer's actual +apparent repository name: + +```text +build:test-optimization --@rules_go//go/private/orchestrion:enabled=true +# Use @io_bazel_rules_go instead of @rules_go in WORKSPACE mode. +``` + +Do not add that Go-only line to Python-only or other non-Go workspaces. + Pass `DD_GIT_*` only through `--repo_env`. Never forward it as test environment data because that makes Git metadata part of the test action cache key. For Go/Orchestrion, do not put `DD_TEST_OPTIMIZATION_AGENT_URL` or @@ -212,9 +231,11 @@ For WORKSPACE monorepos, prefer bootstrap `--workspace-mode` to generate the generic local scaffolding. It can write the root doctor/uploader targets, `.bazelrc` block, Orchestrion pin files, and a split wrapper template while leaving `WORKSPACE` placement under repository control. The generated wrapper -template keeps repo-specific policy in a local helper and exposes separate -plain and optimized wrapper functions, so large repositories do not have to -rediscover that split during onboarding. +template keeps repo-specific policy in a local helper and exposes plain and +optimized building blocks. A large repository can keep one public +`dd_go_test`, route its enrolled package set to `dd_topt_go_test` internally, +and rely on `--config=test-optimization` to choose the raw or instrumented +shape without adding per-target Test Optimization attributes. ### Large WORKSPACE monorepos @@ -269,28 +290,40 @@ datadog_go_test_optimization_workspace_repositories( rto_archive_prefix = "rules_test_optimization-", ) -load("@//go:orchestrion_workspace.bzl", "go_orchestrion_tool_repo") +load("@datadog-rules-test-optimization-go//:topt_go_orchestrion_repository.bzl", "dd_topt_go_orchestrion_tool_repo") -go_orchestrion_tool_repo( +dd_topt_go_orchestrion_tool_repo( version = "v1.9.0", dd_trace_go_version = "v2.9.0", ) load( - "@datadog-rules-test-optimization//tools/core:test_optimization_sync.bzl", - "test_optimization_sync", + "@datadog-rules-test-optimization-go//:topt_go_workspace.bzl", + "dd_topt_go_workspace_sync_repositories", ) -test_optimization_sync( +dd_topt_go_workspace_sync_repositories( name = "test_optimization_data", service = "", - runtime_name = "go", runtime_version = "", - runtime_module_path = "", + module_path = "", require_git_metadata = True, ) ``` +The public Go helper enables metadata gating by default. The single +`--config=test-optimization` switch controls both metadata sync and the +Orchestrion aliases; no per-target or per-repository enable attribute is needed. +Without the config, `dd_topt_go_test` delegates directly to the consumer's +original `go_test_rule` under the public name, preserves the caller's Go rule +kwargs, and creates no Test Optimization-owned hidden targets. With the config, +the same BUILD call uses the existing Orchestrion-backed Test Optimization +shape. + +Python follows the same single-switch contract when its sync declaration uses +`enabled_by_env = True`: without the config, the wrapper preserves the normal +test runner but omits Test Optimization metadata, instrumentation, and payloads. + If the environment can fetch Git repositories reliably, use `rules_go_fetch = "git"` and omit the archive attributes. If the environment mirrors or blocks GitHub codeload archives, publish the same commit to a mirror @@ -469,20 +502,11 @@ dd_topt_go_test( ```bzl # root BUILD.bazel -load("@datadog-rules-test-optimization//tools/core:test_optimization_doctor.bzl", "dd_test_optimization_doctor") -load("@datadog-rules-test-optimization//tools/core:test_optimization_uploader.bzl", "dd_payload_uploader") - -dd_test_optimization_doctor( - name = "dd_test_optimization_doctor", - data = [ - "@test_optimization_data//:test_optimization_context_go_service_a", - "@test_optimization_data//:test_optimization_context_go_service_b", - ], -) +load("@datadog-rules-test-optimization//tools/core:test_optimization_targets.bzl", "dd_test_optimization_targets") -dd_payload_uploader( - name = "dd_upload_payloads", - data = [ +dd_test_optimization_targets( + name = "test_optimization", + context_data = [ "@test_optimization_data//:test_optimization_context_go_service_a", "@test_optimization_data//:test_optimization_context_go_service_b", ], @@ -520,6 +544,7 @@ topt.test_optimization_sync( service = "py-service", runtime_name = "python", runtime_version = "3.12", + enabled_by_env = True, ) use_repo(topt, "test_optimization_data") @@ -591,7 +616,6 @@ dd_topt_py_test( name = "pkg_py_test", py_test_rule = repo_py_test, runner_mode = "consumer_runner", - module_identifier = "example.python.pkg", srcs = glob(["test_*.py"]), deps = [ ":pkg_lib", @@ -604,8 +628,16 @@ dd_topt_py_test( `consumer_runner` intentionally rejects the base `rules_python` `py_test` without an explicit `main`; that shape can execute a Python file directly and -does not prove pytest or ddtrace ran. Prefer `module_identifier` for selection -in this mode so the integration does not depend on Python import-path mutation. +does not prove pytest or ddtrace ran. When the runtime module path and Bazel +package path identify the test, omit `module_identifier` and use the derived +fallback. Keep an explicit `module_identifier` only for a documented +repository-specific exception. + +The derived identifier selects a module-specific payload when that module is +present in the synchronized metadata. If the backend has not materialized a +matching module group yet, the selector intentionally uses the exact canonical +payload bundle instead. Adding an explicit `module_identifier` does not create +missing backend metadata, so it is not required for that fallback. ### WORKSPACE single-service @@ -696,6 +728,7 @@ test_optimization_sync( service = "py-service", runtime_name = "python", runtime_version = "3.12", + enabled_by_env = True, ) ``` @@ -775,6 +808,7 @@ topt.test_optimization_multi_sync( services = ["py-service-a", "py-service-b"], runtime_name = "python", runtime_version = "3.12", + enabled_by_env = True, ) use_repo( @@ -806,20 +840,11 @@ dd_topt_py_test( ```bzl # root BUILD.bazel -load("@datadog-rules-test-optimization//tools/core:test_optimization_doctor.bzl", "dd_test_optimization_doctor") -load("@datadog-rules-test-optimization//tools/core:test_optimization_uploader.bzl", "dd_payload_uploader") - -dd_test_optimization_doctor( - name = "dd_test_optimization_doctor", - data = [ - "@test_optimization_data//:test_optimization_context_py_service_a", - "@test_optimization_data//:test_optimization_context_py_service_b", - ], -) +load("@datadog-rules-test-optimization//tools/core:test_optimization_targets.bzl", "dd_test_optimization_targets") -dd_payload_uploader( - name = "dd_upload_payloads", - data = [ +dd_test_optimization_targets( + name = "test_optimization", + context_data = [ "@test_optimization_data//:test_optimization_context_py_service_a", "@test_optimization_data//:test_optimization_context_py_service_b", ], diff --git a/docs/Troubleshooting.md b/docs/Troubleshooting.md index d5af5ded..a63c2fbb 100644 --- a/docs/Troubleshooting.md +++ b/docs/Troubleshooting.md @@ -10,11 +10,38 @@ This product includes software developed at Datadog Examples below assume the generated repository is named `test_optimization_data`. If you used a different `name`, replace labels and -`bazel sync --only=` accordingly. +repository names accordingly. For config-gated Go and Python repositories, +include `--config=test-optimization` in sync, test, doctor, and uploader +commands. Other companions retain their current activation contract. If Bazel reports that sync requires WORKSPACE support, add `--enable_workspace` to sync commands in this document. +## Test Optimization config and disabled mode + +For config-gated Go and Python onboarding, +`--config=test-optimization` is the single user-facing enablement switch. Both +languages set the metadata repository environment: + +```bazelrc +common:test-optimization --repo_env=DD_TEST_OPTIMIZATION_ENABLED=1 +``` + +Go additionally sets the existing `rules_go` Orchestrion flag: + +```bazelrc +build:test-optimization --@rules_go//go/private/orchestrion:enabled=true +``` + +When the config is omitted, the public Go extension's config-gated default and +repositories explicitly configured with `enabled_by_env = True` generate the +documented no-fetch stubs. Go aliases select local empty targets; Python keeps +the normal consumer runner without Test Optimization metadata or payload +wiring. Python-only consumers omit the Go line. For WORKSPACE Go, replace +`@rules_go` with the apparent repository name used by that workspace. Java, +NodeJS, .NET, and Ruby retain their existing enablement contract in this +release. + ## Quick triage map | Symptom | First checks | Likely section | @@ -41,7 +68,7 @@ from the same run. For the simplest customer ask after tests have already run, use the doctor directly: ```bash -bazel run //:dd_test_optimization_doctor -- \ +bazel run --config=test-optimization //:dd_test_optimization_doctor -- \ --support-bundle .topt/reports/dd-test-optimization-support.zip ``` @@ -207,10 +234,10 @@ values automatically. 2. **Force refetch only when intentional** with a cache-busting salt: ```bash - bazel sync --only= --repo_env=FETCH_SALT="$(date +%s)" + bazel sync --config=test-optimization --only= --repo_env=FETCH_SALT="$(date +%s)" ``` ```powershell - bazel sync --only= --repo_env=FETCH_SALT="$(Get-Date -UFormat %s)" + bazel sync --config=test-optimization --only= --repo_env=FETCH_SALT="$(Get-Date -UFormat %s)" ``` Do not put `FETCH_SALT` in `.bazelrc`, `bazel test`, doctor, or uploader commands. It deliberately breaks the repository-rule cache key and should be @@ -237,6 +264,8 @@ values automatically. debug = True, # Verbose logging ) ``` + Preserve the repository's existing `enabled_by_env` value; enabling debug + logging must not change its activation contract. ## Published Go pins @@ -378,10 +407,10 @@ global root `BUILD.bazel` wiring unrelated to Test Optimization. 3. **Check DD_TEST_OPTIMIZATION_PAYLOADS_IN_FILES**: The macro should set this to `"true"`. Verify your test environment: ```bash - bazel test //your:test --test_output=all 2>&1 | grep DD_TEST_OPTIMIZATION + bazel test --config=test-optimization //your:test --test_output=all 2>&1 | grep DD_TEST_OPTIMIZATION ``` ```powershell - bazel test //your:test --test_output=all *>&1 | Select-String "DD_TEST_OPTIMIZATION" + bazel test --config=test-optimization //your:test --test_output=all *>&1 | Select-String "DD_TEST_OPTIMIZATION" ``` PowerShell uses `*>&1` (not Bash `2>&1`) to merge stderr/stdout. @@ -869,8 +898,8 @@ client-side Bazel behavior. - PowerShell: set `$env:DD_API_KEY` and `$env:DD_SITE` first, then run `bazel run --config=test-optimization //:dd_upload_payloads` - Quote paths containing spaces and avoid `eval`-style wrappers. - For refetch debugging, use: - - `bazel sync --only= --repo_env=FETCH_SALT=` - - if required by workspace mode: `bazel sync --enable_workspace --only= --repo_env=FETCH_SALT=` + - `bazel sync --config=test-optimization --only= --repo_env=FETCH_SALT=` + - if required by workspace mode: `bazel sync --enable_workspace --config=test-optimization --only= --repo_env=FETCH_SALT=` ## Getting help @@ -878,10 +907,10 @@ If issues persist: 1. **Enable debug mode** and capture full output: ```bash - bazel sync --only= --repo_env=FETCH_SALT= 2>&1 | tee debug.log + bazel sync --config=test-optimization --only= --repo_env=FETCH_SALT= 2>&1 | tee debug.log ``` ```powershell - bazel sync --only= --repo_env=FETCH_SALT= *>&1 | Tee-Object -FilePath debug.log + bazel sync --config=test-optimization --only= --repo_env=FETCH_SALT= *>&1 | Tee-Object -FilePath debug.log ``` 2. **Collect diagnostic info**: diff --git a/docs/Uploader_Reference.md b/docs/Uploader_Reference.md index 66fbac35..fec6e848 100644 --- a/docs/Uploader_Reference.md +++ b/docs/Uploader_Reference.md @@ -35,6 +35,24 @@ The `test-optimization` config should contain the recommended Bazel test flags: `--remote_download_minimal`, `--remote_download_regex=.*test[.]outputs.*`, and `--zip_undeclared_test_outputs`. +For Go consumers using the reusable bootstrap, keep the phase-correct enablement +in the same config: + +```bazelrc +common:test-optimization --repo_env=DD_TEST_OPTIMIZATION_ENABLED=1 +build:test-optimization --@rules_go//go/private/orchestrion:enabled=true +``` + +This is one user-facing switch. Removing `--config=test-optimization` disables +both metadata resolution and the Orchestrion analysis aliases; it does not +require a second bool flag. In WORKSPACE repositories, use the apparent +`rules_go` repository name configured by that workspace. + +Config-gated Python consumers use only the +`DD_TEST_OPTIMIZATION_ENABLED=1` entry and omit the Go-specific Orchestrion +line. Java, NodeJS, .NET, and Ruby retain their existing enablement contract in +this release. + ```bash # Vendor the full tools/test_optimization/ helper directory, or set # DD_TEST_OPTIMIZATION_SUPPORT_BUNDLE_COLLECTOR to create_support_bundle.py. @@ -308,7 +326,7 @@ For first-pass support, the doctor can create a doctor-only bundle without the wrapper: ```bash -bazel run //:dd_test_optimization_doctor -- \ +bazel run --config=test-optimization //:dd_test_optimization_doctor -- \ --support-bundle .topt/reports/dd-test-optimization-support.zip ``` diff --git a/docs/go_orchestrion_bazel_deep_dive.md b/docs/go_orchestrion_bazel_deep_dive.md index 317d6edb..8b68c9a4 100644 --- a/docs/go_orchestrion_bazel_deep_dive.md +++ b/docs/go_orchestrion_bazel_deep_dive.md @@ -278,14 +278,17 @@ in one place. Implementation: - [topt_go_orchestrion.bzl](../modules/go/topt_go_orchestrion.bzl) -The wrapper rule exists for one reason: it applies a function transition that -sets: +The wrapper rule applies a function transition that sets only: ```bzl -"@rules_go//go/private/orchestrion:enabled": True "@rules_go//go/private/orchestrion:mode": "general" or "test_optimization" ``` +The transition deliberately preserves the existing +`@rules_go//go/private/orchestrion:enabled` setting. The user-facing +`--config=test-optimization` config enables that setting during analysis; +omitting the config leaves it at the `rules_go` default of `False`. + The wrapper then symlinks the executable produced by the raw target and returns the same runfiles. @@ -769,7 +772,7 @@ sequenceDiagram User->>Macro: declare Go test Macro->>Macro: select payload data + add pin files Macro->>Wrapper: public test target - Wrapper->>RG: build raw go_test with transition enabled + Wrapper->>RG: build raw go_test with mode transition; preserve enabled flag RG->>RG: compile customer packages RG->>Orch: compile woven stdlib as needed RG->>Orch: prepare synthetic testmain helper packagefiles diff --git a/docs/internal_monorepo_go_rollout_guide.md b/docs/internal_monorepo_go_rollout_guide.md index d1c858c1..20c9e2c3 100644 --- a/docs/internal_monorepo_go_rollout_guide.md +++ b/docs/internal_monorepo_go_rollout_guide.md @@ -49,15 +49,16 @@ SHA256, and archive prefix generated from the same published commit. - Use a commit that is reachable from `origin/main`; never publish feature-branch SHAs into consumer snippets. -- Configure `go_orchestrion_tool_repo(...)` with the current supported +- Configure `dd_topt_go_orchestrion_tool_repo(...)` with the current supported Orchestrion version and the current supported `dd-trace-go` Bazel-mode - version. -- Configure `test_optimization_sync(...)` with: + version. Do not load the underlying `rules_go` repository rule directly. +- Configure `dd_topt_go_workspace_sync_repositories(...)` with: - `service` - - `runtime_name = "go"` - `runtime_version` - - `runtime_module_path` + - `module_path` - `require_git_metadata = True` + The public helper supplies `runtime_name = "go"` and config-gated metadata + sync by default. - Keep repository-specific scheduling, Docker, tags, platform constraints, and flaky policy in the repository-local wrapper layer. - Set `orchestrion_mode = "test_optimization"` in the optimized wrapper for @@ -78,6 +79,17 @@ SHA256, and archive prefix generated from the same published commit. `--artifact-staging-dir=`. - Pass `DD_GIT_*` only through `--repo_env`, never through `--test_env`. - Pass uploader credentials at `bazel run` time, not into test actions. +- Keep one user-facing `test-optimization` config with both phase-correct + switches: + + ```bazelrc + common:test-optimization --repo_env=DD_TEST_OPTIMIZATION_ENABLED=1 + build:test-optimization --@io_bazel_rules_go//go/private/orchestrion:enabled=true + ``` + + Removing `--config=test-optimization` disables both metadata repositories + and Orchestrion aliases. The public Go helpers enable metadata gating by + default; they do not dynamically control Orchestrion repository declaration. ## Bootstrap Flow @@ -105,6 +117,16 @@ bazel run @datadog-rules-test-optimization-go//:dd_topt_go_bootstrap -- \ --control-target "//path/to/plain/control:go_default_test" ``` +### Updating an existing managed config + +When upgrading from the current release, rerun the same bootstrap with +`--write-bazelrc`. It replaces the content between the Datadog-managed markers +with the current single-config contract, preserves all content outside those +markers, and is idempotent. This adds both +`DD_TEST_OPTIMIZATION_ENABLED=1` and the existing `rules_go` Orchestrion flag to +the named config; consumers do not need a separate migration mode or a second +bool flag. + If the repository owns checked-in `go_repository(...)` declarations, run the repository-owned refresh command after targeted Go module sync and rerun bootstrap with `--check-go-repositories`. Bootstrap should verify those pins; it diff --git a/docs/rules_go_variant_maintenance_guide.md b/docs/rules_go_variant_maintenance_guide.md index 9affcdb1..1d8df97c 100644 --- a/docs/rules_go_variant_maintenance_guide.md +++ b/docs/rules_go_variant_maintenance_guide.md @@ -263,3 +263,21 @@ they consume a complete base tree. Repositories that already own their `rules_go` patch stack should instead use a generated public consumer patch profile as a local rebase or merge input, then verify the regenerated private patch in their private patch order. + +## Test Optimization Alias Contract + +The public base trees keep the existing +`//go/private/orchestrion:enabled` setting as the analysis-time control. Their +stable Orchestrion aliases select package-local empty targets when the setting +is false and the real `rules_go_orchestrion_tool` files when it is true. +Consumers should expose one config that sets both effects: + +```bazelrc +common:test-optimization --repo_env=DD_TEST_OPTIMIZATION_ENABLED=1 +build:test-optimization --@rules_go//go/private/orchestrion:enabled=true +``` + +The public Go extension reads the metadata environment by default; low-level +repositories do so when explicitly configured with `enabled_by_env = True`. +Removing the config is the opt-out and must not require a consumer-owned +duplicate bool flag or stub repository. diff --git a/examples/README.md b/examples/README.md index 56b5fbdd..191ab5f6 100644 --- a/examples/README.md +++ b/examples/README.md @@ -39,6 +39,20 @@ repository, use `./bazelw` for local development convenience. - Provide sync credentials via environment and forward them to repository rules: - shell/CI secret: `DD_API_KEY` - `.bazelrc`: `common --repo_env=DD_API_KEY` (and optionally `common --repo_env=DD_SITE`) +- For config-gated Go and Python Test Optimization, make the documented config + the only user-facing switch: + + ```bazelrc + common:test-optimization --repo_env=DD_TEST_OPTIMIZATION_ENABLED=1 + # Go only: + build:test-optimization --@rules_go//go/private/orchestrion:enabled=true + ``` + + Removing `--config=test-optimization` disables metadata resolution and the + matching Go/Python runtime wiring. Go additionally disables Orchestrion + analysis; in WORKSPACE mode, use the apparent `rules_go` repo name configured + by the workspace. Python-only consumers omit the Go line. Other companions + retain their existing enablement contract in this release. ## Single-service (classic) @@ -214,8 +228,10 @@ runner. Repositories with an internal Python test wrapper should use `runner_mode = "consumer_runner"` and pass that wrapper through `py_test_rule`. In that mode the Datadog macro does not inject `run_pytest.py`, does not set -`main`, and does not synthesize `imports`. Prefer `module_identifier` for -payload selection so onboarding does not depend on import-path mutation. +`main`, and does not synthesize `imports`. When the runtime module path and +Bazel package path identify the test, omit `module_identifier` and use the +derived fallback. Keep an explicit `module_identifier` only for a documented +repository-specific exception. BUILD.bazel (Java companion): @@ -289,17 +305,10 @@ dd_topt_ruby_test( Root BUILD.bazel (one doctor and one uploader per workspace): ```bzl -load("@datadog-rules-test-optimization//tools/core:test_optimization_doctor.bzl", "dd_test_optimization_doctor") -load("@datadog-rules-test-optimization//tools/core:test_optimization_uploader.bzl", "dd_payload_uploader") - -dd_test_optimization_doctor( - name = "dd_test_optimization_doctor", - data = ["@test_optimization_data//:test_optimization_context"], -) +load("@datadog-rules-test-optimization//tools/core:test_optimization_targets.bzl", "dd_test_optimization_targets") -dd_payload_uploader( - name = "dd_upload_payloads", - data = ["@test_optimization_data//:test_optimization_context"], +dd_test_optimization_targets( + name = "test_optimization", ) ``` @@ -472,6 +481,7 @@ topt_go.test_optimization_sync( service = "go-service", runtime_name = "go", runtime_version = "1.25.0", + enabled_by_env = True, ) topt_py = use_extension( @@ -483,6 +493,7 @@ topt_py.test_optimization_sync( service = "py-service", runtime_name = "python", runtime_version = "3.12", + enabled_by_env = True, ) use_repo(topt_go, "test_optimization_data_go") @@ -531,28 +542,19 @@ Doctor/uploader package (multi-service). Simple examples may put these in the root package, but large monorepos should prefer a lightweight package: ```bzl -load("@datadog-rules-test-optimization//tools/core:test_optimization_doctor.bzl", "dd_test_optimization_doctor") -load("@datadog-rules-test-optimization//tools/core:test_optimization_uploader.bzl", "dd_payload_uploader") +load("@datadog-rules-test-optimization//tools/core:test_optimization_targets.bzl", "dd_test_optimization_targets") -dd_test_optimization_doctor( - name = "dd_test_optimization_doctor", - data = [ - "@test_optimization_data//:test_optimization_context_go_service_a", - "@test_optimization_data//:test_optimization_context_go_service_b", - ], -) - -dd_payload_uploader( - name = "dd_upload_payloads", - data = [ - "@test_optimization_data//:test_optimization_context_go_service_a", - "@test_optimization_data//:test_optimization_context_go_service_b", - ], +dd_test_optimization_targets( + name = "test_optimization", + context_data = [ + "@test_optimization_data//:test_optimization_context_go_service_a", + "@test_optimization_data//:test_optimization_context_go_service_b", + ], ) ``` Mixed-runtime example rule: - keep one sync repo per runtime/service - keep one logical uploader for the workspace; package-local placement is fine -- add every matching context target to uploader `data` +- pass every matching context target through `context_data` - do not use `DD_TEST_OPTIMIZATION_CONTEXT_JSON` as the normal mixed-runtime path diff --git a/examples/common/runtests_common.ps1 b/examples/common/runtests_common.ps1 index 39ea56a7..31983cc3 100644 --- a/examples/common/runtests_common.ps1 +++ b/examples/common/runtests_common.ps1 @@ -17,12 +17,16 @@ function Invoke-RunCmd { ) if ($env:RUNTESTS_DRY_RUN -eq "1") { - Write-Output ("[dry-run] {0} {1}" -f $Command, ($Args -join " ")) + if ($Args -notcontains "--config=test-optimization") { + throw "dry-run command is missing --config=test-optimization: $Command $($Args -join ' ')" + } + Write-Host ("[dry-run] {0} {1}" -f $Command, ($Args -join " ")) return 0 } - & $Command @Args - return $LASTEXITCODE + & $Command @Args | Out-Host + $exitCode = $LASTEXITCODE + return $exitCode } # Handle Get-BazelCommand behavior. @@ -61,22 +65,22 @@ function Invoke-ExampleRunTests { ) Write-Output "--- non-hermetic run" - $rc = Invoke-RunCmd -Command $bazelCmd -Args @("test", "//src/go-project/...", "--test_output=streamed", "--test_arg=-test.v", "--sandbox_debug", "--remote_download_minimal", "--remote_download_regex=.*test[.]outputs.*", "--zip_undeclared_test_outputs", "--build_event_json_file=$nonHermeticBep") + $rc = Invoke-RunCmd -Command $bazelCmd -Args @("test", "--config=test-optimization", "//src/go-project/...", "--test_output=streamed", "--test_arg=-test.v", "--sandbox_debug", "--remote_download_minimal", "--remote_download_regex=.*test[.]outputs.*", "--zip_undeclared_test_outputs", "--build_event_json_file=$nonHermeticBep") if ($rc -ne 0) { $testStatus = $rc } Write-Output "--- hermetic run" - $rc = Invoke-RunCmd -Command $bazelCmd -Args @("test", "//src/go-project/...", "--test_output=streamed", "--test_arg=-test.v", "--sandbox_debug", "--config=hermetic", "--remote_download_minimal", "--remote_download_regex=.*test[.]outputs.*", "--zip_undeclared_test_outputs", "--build_event_json_file=$hermeticBep") + $rc = Invoke-RunCmd -Command $bazelCmd -Args @("test", "--config=test-optimization", "--config=hermetic", "//src/go-project/...", "--test_output=streamed", "--test_arg=-test.v", "--sandbox_debug", "--remote_download_minimal", "--remote_download_regex=.*test[.]outputs.*", "--zip_undeclared_test_outputs", "--build_event_json_file=$hermeticBep") if ($rc -ne 0) { $testStatus = $rc } Write-Output "--- validating payloads" - $doctorStatus = Invoke-RunCmd -Command $bazelCmd -Args (@("run", "//:dd_test_optimization_doctor", "--") + $bepArgs) + $doctorStatus = Invoke-RunCmd -Command $bazelCmd -Args (@("run", "--config=test-optimization", "//:dd_test_optimization_doctor", "--") + $bepArgs) if ($doctorStatus -ne 0) { if ($testStatus -ne 0) { exit $testStatus } exit $doctorStatus } Write-Output "--- validating upload enrichment" - $dryRunStatus = Invoke-RunCmd -Command $bazelCmd -Args (@("run", "//:dd_upload_payloads", "--") + $bepArgs + @("--dry-run", "--validate-enrichment")) + $dryRunStatus = Invoke-RunCmd -Command $bazelCmd -Args (@("run", "--config=test-optimization", "//:dd_upload_payloads", "--") + $bepArgs + @("--dry-run", "--validate-enrichment")) if ($dryRunStatus -ne 0) { if ($testStatus -ne 0) { exit $testStatus } exit $dryRunStatus @@ -84,7 +88,7 @@ function Invoke-ExampleRunTests { Write-Output "--- uploading payloads" if (-not $env:DD_SITE) { $env:DD_SITE = "datadoghq.com" } - $uploadRc = Invoke-RunCmd -Command $bazelCmd -Args (@("run", "//:dd_upload_payloads", "--") + $bepArgs) + $uploadRc = Invoke-RunCmd -Command $bazelCmd -Args (@("run", "--config=test-optimization", "//:dd_upload_payloads", "--") + $bepArgs) if ($testStatus -ne 0) { exit $testStatus } exit $uploadRc diff --git a/examples/common/runtests_common.sh b/examples/common/runtests_common.sh index 6ac130a6..f7a46e1a 100644 --- a/examples/common/runtests_common.sh +++ b/examples/common/runtests_common.sh @@ -41,6 +41,18 @@ run_example_runtests() { # Handle run cmd behavior. run_cmd() { if [[ "${RUNTESTS_DRY_RUN:-0}" == "1" ]]; then + local arg + local has_test_optimization_config=0 + for arg in "$@"; do + if [[ "$arg" == "--config=test-optimization" ]]; then + has_test_optimization_config=1 + break + fi + done + if [[ "$has_test_optimization_config" -ne 1 ]]; then + echo "error: dry-run command is missing --config=test-optimization: $*" >&2 + return 1 + fi echo "[dry-run] $*" return 0 fi @@ -48,13 +60,13 @@ run_example_runtests() { } echo "--- non-hermetic run" - run_cmd "${bazelw}" test //src/go-project/... --test_output=streamed --test_arg=-test.v --sandbox_debug --remote_download_minimal --remote_download_regex=.*test[.]outputs.* --zip_undeclared_test_outputs --build_event_json_file="$non_hermetic_bep" || test_status=$? + run_cmd "${bazelw}" test --config=test-optimization //src/go-project/... --test_output=streamed --test_arg=-test.v --sandbox_debug --remote_download_minimal --remote_download_regex=.*test[.]outputs.* --zip_undeclared_test_outputs --build_event_json_file="$non_hermetic_bep" || test_status=$? echo "--- hermetic run" - run_cmd "${bazelw}" test //src/go-project/... --test_output=streamed --test_arg=-test.v --sandbox_debug --config=hermetic --remote_download_minimal --remote_download_regex=.*test[.]outputs.* --zip_undeclared_test_outputs --build_event_json_file="$hermetic_bep" || test_status=$? + run_cmd "${bazelw}" test --config=test-optimization --config=hermetic //src/go-project/... --test_output=streamed --test_arg=-test.v --sandbox_debug --remote_download_minimal --remote_download_regex=.*test[.]outputs.* --zip_undeclared_test_outputs --build_event_json_file="$hermetic_bep" || test_status=$? echo "--- validating payloads" - run_cmd "${bazelw}" run //:dd_test_optimization_doctor -- "${bep_args[@]}" || doctor_status=$? + run_cmd "${bazelw}" run --config=test-optimization //:dd_test_optimization_doctor -- "${bep_args[@]}" || doctor_status=$? if [[ "$doctor_status" -ne 0 ]]; then if [[ "$test_status" -ne 0 ]]; then return "$test_status" @@ -63,7 +75,7 @@ run_example_runtests() { fi echo "--- validating upload enrichment" - run_cmd "${bazelw}" run //:dd_upload_payloads -- "${bep_args[@]}" --dry-run --validate-enrichment || dry_run_status=$? + run_cmd "${bazelw}" run --config=test-optimization //:dd_upload_payloads -- "${bep_args[@]}" --dry-run --validate-enrichment || dry_run_status=$? if [[ "$dry_run_status" -ne 0 ]]; then if [[ "$test_status" -ne 0 ]]; then return "$test_status" @@ -73,7 +85,7 @@ run_example_runtests() { echo "--- uploading payloads" # Requires DD_API_KEY and DD_SITE environment variables. - DD_API_KEY="${DD_API_KEY:-}" DD_SITE="${DD_SITE:-datadoghq.com}" run_cmd "${bazelw}" run //:dd_upload_payloads -- "${bep_args[@]}" || upload_status=$? + DD_API_KEY="${DD_API_KEY:-}" DD_SITE="${DD_SITE:-datadoghq.com}" run_cmd "${bazelw}" run --config=test-optimization //:dd_upload_payloads -- "${bep_args[@]}" || upload_status=$? if [[ "$test_status" -ne 0 ]]; then return "$test_status" diff --git a/examples/multi_service/.bazelrc b/examples/multi_service/.bazelrc index 4a6123d2..ef0a75d7 100644 --- a/examples/multi_service/.bazelrc +++ b/examples/multi_service/.bazelrc @@ -1,6 +1,10 @@ # Required on Windows for dd_topt_java_test: -javaagent paths only resolve under symlinked runfiles. build --enable_runfiles +# One user-facing switch for Go metadata bootstrap and Orchestrion analysis. +common:test-optimization --repo_env=DD_TEST_OPTIMIZATION_ENABLED=1 +build:test-optimization --@rules_go//go/private/orchestrion:enabled=true + # Hermetic config (sandbox, stable env, network blocking) build:hermetic --incompatible_strict_action_env build:hermetic --spawn_strategy=sandboxed @@ -14,4 +18,3 @@ test:hermetic --test_env=LC_ALL=C # Uncomment and provide values via shell/CI secret store: # common --repo_env=DD_API_KEY # common --repo_env=DD_SITE - diff --git a/examples/multi_service/BUILD.bazel b/examples/multi_service/BUILD.bazel index 62600767..e93fed1c 100644 --- a/examples/multi_service/BUILD.bazel +++ b/examples/multi_service/BUILD.bazel @@ -4,21 +4,12 @@ # This product includes software developed at Datadog # (https://www.datadoghq.com/) Copyright 2025-Present Datadog, Inc. -load("@datadog-rules-test-optimization//tools/core:test_optimization_doctor.bzl", "dd_test_optimization_doctor") -load("@datadog-rules-test-optimization//tools/core:test_optimization_uploader.bzl", "dd_payload_uploader") +load("@datadog-rules-test-optimization//tools/core:test_optimization_targets.bzl", "dd_test_optimization_targets") # ONE doctor and ONE uploader target per workspace. -dd_test_optimization_doctor( - name = "dd_test_optimization_doctor", - data = [ - "@test_optimization_data//:test_optimization_context_go_service_a", - "@test_optimization_data//:test_optimization_context_go_service_b", - ], -) - -dd_payload_uploader( - name = "dd_upload_payloads", - data = [ +dd_test_optimization_targets( + name = "test_optimization", + context_data = [ "@test_optimization_data//:test_optimization_context_go_service_a", "@test_optimization_data//:test_optimization_context_go_service_b", ], diff --git a/examples/single_service/.bazelrc b/examples/single_service/.bazelrc index 4a6123d2..ef0a75d7 100644 --- a/examples/single_service/.bazelrc +++ b/examples/single_service/.bazelrc @@ -1,6 +1,10 @@ # Required on Windows for dd_topt_java_test: -javaagent paths only resolve under symlinked runfiles. build --enable_runfiles +# One user-facing switch for Go metadata bootstrap and Orchestrion analysis. +common:test-optimization --repo_env=DD_TEST_OPTIMIZATION_ENABLED=1 +build:test-optimization --@rules_go//go/private/orchestrion:enabled=true + # Hermetic config (sandbox, stable env, network blocking) build:hermetic --incompatible_strict_action_env build:hermetic --spawn_strategy=sandboxed @@ -14,4 +18,3 @@ test:hermetic --test_env=LC_ALL=C # Uncomment and provide values via shell/CI secret store: # common --repo_env=DD_API_KEY # common --repo_env=DD_SITE - diff --git a/examples/single_service/BUILD.bazel b/examples/single_service/BUILD.bazel index 0e135c5a..0cb76cc6 100644 --- a/examples/single_service/BUILD.bazel +++ b/examples/single_service/BUILD.bazel @@ -4,16 +4,9 @@ # This product includes software developed at Datadog # (https://www.datadoghq.com/) Copyright 2025-Present Datadog, Inc. -load("@datadog-rules-test-optimization//tools/core:test_optimization_doctor.bzl", "dd_test_optimization_doctor") -load("@datadog-rules-test-optimization//tools/core:test_optimization_uploader.bzl", "dd_payload_uploader") +load("@datadog-rules-test-optimization//tools/core:test_optimization_targets.bzl", "dd_test_optimization_targets") # ONE doctor and ONE uploader target per workspace. -dd_test_optimization_doctor( - name = "dd_test_optimization_doctor", - data = ["@test_optimization_data//:test_optimization_context"], -) - -dd_payload_uploader( - name = "dd_upload_payloads", - data = ["@test_optimization_data//:test_optimization_context"], +dd_test_optimization_targets( + name = "test_optimization", ) diff --git a/examples/single_service/src/python-project/BUILD.bazel b/examples/single_service/src/python-project/BUILD.bazel index 49a32aa4..e4e77a5f 100644 --- a/examples/single_service/src/python-project/BUILD.bazel +++ b/examples/single_service/src/python-project/BUILD.bazel @@ -8,6 +8,8 @@ load("@datadog-rules-test-optimization-python//:topt_py_test.bzl", "dd_topt_py_t load("@example_pip//:requirements.bzl", "requirement") load("@test_optimization_data//:export.bzl", "topt_data") +_TEST_OPTIMIZATION_EXPECTED = "1" if topt_data.get("enabled", True) else "0" + exports_files( [ "requirements.in", @@ -25,6 +27,7 @@ py_library( dd_topt_py_test( name = "hello_test", srcs = ["main_test.py"], + env = {"EXAMPLE_EXPECT_TEST_OPTIMIZATION": _TEST_OPTIMIZATION_EXPECTED}, imports = ["example/python/project"], main = "main_test.py", py_test_rule = py_test, @@ -38,6 +41,7 @@ dd_topt_py_test( dd_topt_py_test( name = "hello_pytest_test", srcs = ["pytest_test.py"], + env = {"EXAMPLE_EXPECT_TEST_OPTIMIZATION": _TEST_OPTIMIZATION_EXPECTED}, target_compatible_with = select({ "@platforms//os:windows": ["@platforms//:incompatible"], "//conditions:default": [], diff --git a/examples/single_service/src/python-project/main_test.py b/examples/single_service/src/python-project/main_test.py index 714e8160..c77dd54a 100644 --- a/examples/single_service/src/python-project/main_test.py +++ b/examples/single_service/src/python-project/main_test.py @@ -61,8 +61,13 @@ def test_greeting(self): module = _load_main_module() self.assertEqual("Hello from Python!", module.get_greeting()) - def test_manifest_metadata_files_present(self): + def test_manifest_metadata_contract(self): + expected = os.getenv("EXAMPLE_EXPECT_TEST_OPTIMIZATION", "1") == "1" manifest_rloc = os.getenv("DD_TEST_OPTIMIZATION_MANIFEST_FILE", "") + if not expected: + self.assertFalse(manifest_rloc, "disabled tests must not receive Test Optimization metadata") + return + self.assertTrue(manifest_rloc, "DD_TEST_OPTIMIZATION_MANIFEST_FILE should be set by dd_topt_py_test") manifest_path = _resolve_runfile(manifest_rloc) diff --git a/examples/single_service/src/python-project/pytest_test.py b/examples/single_service/src/python-project/pytest_test.py index d8bf67d3..f3badcfd 100644 --- a/examples/single_service/src/python-project/pytest_test.py +++ b/examples/single_service/src/python-project/pytest_test.py @@ -64,13 +64,13 @@ def test_greeting(): assert module.get_greeting() == "Hello from Python!" -def test_manifest_env_set(): +def test_manifest_env_contract(): + expected = os.getenv("EXAMPLE_EXPECT_TEST_OPTIMIZATION", "1") == "1" manifest_rloc = os.getenv("DD_TEST_OPTIMIZATION_MANIFEST_FILE", "") - assert manifest_rloc, "DD_TEST_OPTIMIZATION_MANIFEST_FILE should be set by dd_topt_py_test" - + if not expected: + assert not manifest_rloc, "disabled tests must not receive Test Optimization metadata" + return -def test_manifest_metadata_files_present(): - manifest_rloc = os.getenv("DD_TEST_OPTIMIZATION_MANIFEST_FILE", "") assert manifest_rloc, "DD_TEST_OPTIMIZATION_MANIFEST_FILE should be set by dd_topt_py_test" manifest_path = _resolve_runfile(manifest_rloc) diff --git a/modules/dotnet/MODULE.bazel.lock b/modules/dotnet/MODULE.bazel.lock index 2dd9dae1..34afb0ca 100644 --- a/modules/dotnet/MODULE.bazel.lock +++ b/modules/dotnet/MODULE.bazel.lock @@ -140,8 +140,8 @@ "moduleExtensions": { "//tests:example_stub_repo.bzl%example_stub_repo_extension": { "general": { - "bzlTransitiveDigest": "zC3aEdxLSUhPV6iwgCNk7hQr+1eO+rAQ5znCfGieX3g=", - "usagesDigest": "8+UVmqdhqtmEmW3r9vMffABIBlZnL2Mwd3je/6nAzu8=", + "bzlTransitiveDigest": "8Z0dqG8n5A2E4sAbWxvij+vncAodaOVqDuvzF/sm6wE=", + "usagesDigest": "wLZKZW3et9AZB9PdsRyNX8IC9uzvvBMQbjMnvx8yB2s=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, "envVariables": {}, diff --git a/modules/go/BUILD.bazel b/modules/go/BUILD.bazel index 9e2e73b8..d736d37e 100644 --- a/modules/go/BUILD.bazel +++ b/modules/go/BUILD.bazel @@ -11,6 +11,8 @@ exports_files( "topt_go_test.bzl", "topt_go_infer.bzl", "topt_go_extension.bzl", + "topt_go_orchestrion_repository.bzl", + "topt_go_workspace.bzl", ], visibility = ["//visibility:public"], ) diff --git a/modules/go/MODULE.bazel.lock b/modules/go/MODULE.bazel.lock index 1d6ef91c..1611b1d4 100644 --- a/modules/go/MODULE.bazel.lock +++ b/modules/go/MODULE.bazel.lock @@ -148,7 +148,7 @@ "moduleExtensions": { "//tests:example_stub_repo.bzl%example_stub_repo_extension": { "general": { - "bzlTransitiveDigest": "pLQ+JPOKAu2+EI916bm9tEvq/JkYkgWy+BM9tmAwBUY=", + "bzlTransitiveDigest": "PoT1vHfLILb9IHzanYU8Hjy3Lc2nLwTn2trTYqR3g/E=", "usagesDigest": "yco8+ejjp1y3GIF8UGLO928CMMuVMZAHdJKNqhZnoJo=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -157,6 +157,7 @@ "test_optimization_data": { "repoRuleId": "@@datadog-rules-test-optimization+//tools/tests:example_stub_repo.bzl%example_stub_repo", "attributes": { + "enabled": true, "go_module_included": false, "go_module_path": "example.com/stub", "go_sanitized_module_path": "example_com_stub", diff --git a/modules/go/tests/BUILD.bazel b/modules/go/tests/BUILD.bazel index 3076e960..c22954a0 100644 --- a/modules/go/tests/BUILD.bazel +++ b/modules/go/tests/BUILD.bazel @@ -4,12 +4,20 @@ # This product includes software developed at Datadog # (https://www.datadoghq.com/) Copyright 2025-Present Datadog, Inc. +load( + ":test_extension.bzl", + "go_multi_specs_propagate_non_default_enablement_and_flaky_tests_test", + "go_single_spec_propagates_non_default_enablement_and_flaky_tests_test", + "go_specs_default_to_config_gated_test", +) load( ":test_macro.bzl", "go_macro_ci_visibility_opt_out_target", "go_macro_ci_visibility_opt_out_wiring_test", "go_macro_default_general_public_wrapper_mode_target", "go_macro_default_general_public_wrapper_mode_test", + "go_macro_disabled_raw_target", + "go_macro_disabled_raw_wiring_test", "go_macro_env_none_target", "go_macro_env_none_wiring_test", "go_macro_explicit_service_target", @@ -18,6 +26,8 @@ load( "go_macro_general_mode_linker_flags_wiring_test", "go_macro_multi_service_target", "go_macro_multi_service_wiring_test", + "go_macro_orchestrion_enablement_mismatch_failure_test", + "go_macro_orchestrion_enablement_mismatch_target", "go_macro_orchestrion_pin_files_provider_test", "go_macro_orchestrion_pin_files_target", "go_macro_orchestrion_pin_files_wiring_test", @@ -114,6 +124,13 @@ load( "select_module_group_name_test", "service_mapping_entries_filters_non_service_test", ) +load( + ":test_workspace_helpers.bzl", + "go_workspace_multi_specs_test", + "go_workspace_single_specs_test", + "go_workspace_specs_default_to_config_gated_test", + "orchestrion_call_spec_test", +) # Go-specific Starlark tests for orchestration selection and macro wiring. service_mapping_entries_filters_non_service_test( @@ -122,6 +139,58 @@ service_mapping_entries_filters_non_service_test( timeout = "short", ) +go_single_spec_propagates_non_default_enablement_and_flaky_tests_test( + name = "go_single_spec_propagates_non_default_enablement_and_flaky_tests_test", + size = "small", + timeout = "short", +) + +go_multi_specs_propagate_non_default_enablement_and_flaky_tests_test( + name = "go_multi_specs_propagate_non_default_enablement_and_flaky_tests_test", + size = "small", + timeout = "short", +) + +go_specs_default_to_config_gated_test( + name = "go_specs_default_to_config_gated_test", + size = "small", + timeout = "short", +) + +go_workspace_single_specs_test( + name = "go_workspace_single_specs_test", + size = "small", + timeout = "short", +) + +go_workspace_multi_specs_test( + name = "go_workspace_multi_specs_test", + size = "small", + timeout = "short", +) + +go_workspace_specs_default_to_config_gated_test( + name = "go_workspace_specs_default_to_config_gated_test", + size = "small", + timeout = "short", +) + +orchestrion_call_spec_test( + name = "orchestrion_call_spec_test", + size = "small", + timeout = "short", +) + +test_suite( + name = "workspace_helpers_tests", + tests = [ + ":go_workspace_multi_specs_test", + ":go_workspace_single_specs_test", + ":go_workspace_specs_default_to_config_gated_test", + ":orchestrion_call_spec_test", + ], +) + resolve_topt_service_key_prefers_exact_then_sanitized_test( name = "resolve_topt_service_key_prefers_exact_then_sanitized_test", size = "small", @@ -288,6 +357,16 @@ go_macro_single_service_target( tags = ["manual"], ) +go_macro_disabled_raw_target( + name = "go_macro_disabled_raw_target", + tags = ["manual"], +) + +go_macro_disabled_raw_wiring_test( + name = "go_macro_disabled_raw_wiring_test", + target_under_test = ":go_macro_disabled_raw_target", +) + go_macro_single_service_wiring_test( name = "go_macro_single_service_wiring_test", target_under_test = ":go_macro_single_service_target__raw_go_test", @@ -568,6 +647,16 @@ validate_test_optimization_pin_files_missing_go_mod_failure_test( target_under_test = ":validate_test_optimization_pin_files_missing_go_mod_target", ) +go_macro_orchestrion_enablement_mismatch_target( + name = "go_macro_orchestrion_enablement_mismatch_target", + tags = ["manual"], +) + +go_macro_orchestrion_enablement_mismatch_failure_test( + name = "go_macro_orchestrion_enablement_mismatch_failure_test", + target_under_test = ":go_macro_orchestrion_enablement_mismatch_target", +) + wrapper_output_name_target_rule( name = "wrapper_output_name_non_windows_target", executable_basename = "hello_test__raw_go_test", @@ -652,9 +741,11 @@ test_suite( ":build_module_labels_valid_test", ":go_macro_ci_visibility_opt_out_wiring_test", ":go_macro_default_general_public_wrapper_mode_test", + ":go_macro_disabled_raw_wiring_test", ":go_macro_env_none_wiring_test", ":go_macro_explicit_service_wiring_test", ":go_macro_multi_service_wiring_test", + ":go_macro_orchestrion_enablement_mismatch_failure_test", ":go_macro_orchestrion_pin_files_provider_test", ":go_macro_orchestrion_pin_files_wiring_test", ":go_macro_public_wrapper_test", @@ -673,7 +764,12 @@ test_suite( ":go_macro_test_optimization_linker_opt_out_wiring_test", ":go_macro_test_optimization_mode_wiring_test", ":go_macro_test_optimization_public_wrapper_mode_test", + ":go_multi_specs_propagate_non_default_enablement_and_flaky_tests_test", + ":go_single_spec_propagates_non_default_enablement_and_flaky_tests_test", + ":go_specs_default_to_config_gated_test", ":go_stub_includes_manifest_in_files_test", + ":go_workspace_multi_specs_test", + ":go_workspace_single_specs_test", ":has_go_mod_pin_test", ":has_package_local_go_mod_test", ":normalize_user_data_handles_none_test", @@ -681,6 +777,7 @@ test_suite( ":orch_transition_forwards_mode_test", ":orch_wrapper_materialized_actual_non_windows_test", ":orch_wrapper_materialized_actual_windows_test", + ":orchestrion_call_spec_test", ":orchestrion_metadata_enabled_test", ":resolve_topt_service_key_missing_failure_test", ":resolve_topt_service_key_prefers_exact_then_sanitized_test", @@ -702,6 +799,7 @@ test_suite( ":validate_orchestrion_mode_test", ":validate_test_optimization_pin_files_missing_go_mod_failure_test", ":windows_wrapper_uses_file_payload_mode_test", + ":workspace_helpers_tests", ":wrapper_output_name_non_windows_test", ":wrapper_output_name_windows_test", ], diff --git a/modules/go/tests/test_extension.bzl b/modules/go/tests/test_extension.bzl new file mode 100644 index 00000000..607fdd64 --- /dev/null +++ b/modules/go/tests/test_extension.bzl @@ -0,0 +1,75 @@ +# Unless explicitly stated otherwise all files in this repository are licensed under +# the Apache 2.0 License. +# +# This product includes software developed at Datadog +# (https://www.datadoghq.com/) Copyright 2025-Present Datadog, Inc. + +"""Unit tests for the Go Bzlmod extension sync-spec builders.""" + +load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest") +load( + "@datadog-rules-test-optimization-go//:topt_go_extension.bzl", + "build_go_multi_repo_specs_for_tests", + "build_go_single_repo_spec_for_tests", +) + +def _go_single_spec_propagates_non_default_enablement_and_flaky_tests_test(ctx): + env = unittest.begin(ctx) + spec = build_go_single_repo_spec_for_tests( + name = "test_optimization_data_go", + service = "go-service", + module_path = "example.com/repo", + runtime_version = "1.25.9", + enabled = False, + enabled_by_env = False, + flaky_tests = False, + ) + asserts.equals(env, False, spec["enabled"]) + asserts.equals(env, False, spec["enabled_by_env"]) + asserts.equals(env, False, spec["flaky_tests"]) + asserts.equals(env, "go", spec["runtime_name"]) + return unittest.end(env) + +def _go_multi_specs_propagate_non_default_enablement_and_flaky_tests_test(ctx): + env = unittest.begin(ctx) + specs = build_go_multi_repo_specs_for_tests( + name = "test_optimization_data_go", + services = ["go-service-a"], + module_path = "example.com/repo", + runtime_version = "1.25.9", + enabled = False, + enabled_by_env = False, + flaky_tests = False, + ) + asserts.equals(env, 1, len(specs)) + asserts.equals(env, False, specs[0]["enabled"]) + asserts.equals(env, False, specs[0]["enabled_by_env"]) + asserts.equals(env, False, specs[0]["flaky_tests"]) + asserts.equals(env, "go", specs[0]["runtime_name"]) + return unittest.end(env) + +def _go_specs_default_to_config_gated_test(ctx): + env = unittest.begin(ctx) + single = build_go_single_repo_spec_for_tests( + name = "test_optimization_data_go", + service = "go-service", + ) + multi = build_go_multi_repo_specs_for_tests( + name = "test_optimization_data_go", + services = ["go-service"], + ) + asserts.equals(env, True, single["enabled_by_env"]) + asserts.equals(env, True, multi[0]["enabled_by_env"]) + return unittest.end(env) + +go_single_spec_propagates_non_default_enablement_and_flaky_tests_test = unittest.make( + _go_single_spec_propagates_non_default_enablement_and_flaky_tests_test, +) + +go_multi_specs_propagate_non_default_enablement_and_flaky_tests_test = unittest.make( + _go_multi_specs_propagate_non_default_enablement_and_flaky_tests_test, +) + +go_specs_default_to_config_gated_test = unittest.make( + _go_specs_default_to_config_gated_test, +) diff --git a/modules/go/tests/test_macro.bzl b/modules/go/tests/test_macro.bzl index 7d90955f..d9d5b2cb 100644 --- a/modules/go/tests/test_macro.bzl +++ b/modules/go/tests/test_macro.bzl @@ -39,6 +39,8 @@ load( ) load("@rules_go//go/private/orchestrion:pin_files.bzl", "OrchestrionPinFilesInfo") +_ORCHESTRION_ENABLED_SETTING = str(Label("@rules_go//go/private/orchestrion:enabled")) + ToptGoMacroCaptureInfo = provider( doc = "Captured arguments forwarded by dd_topt_go_test to the underlying go_test rule.", fields = { @@ -162,6 +164,13 @@ fake_executable_rule = rule( executable = True, ) +def _fake_metadata_impl(ctx): + out = ctx.actions.declare_file(ctx.label.name + ".json") + ctx.actions.write(out, "{}\n") + return [DefaultInfo(files = depset([out]))] + +fake_metadata_rule = rule(implementation = _fake_metadata_impl) + def _wrapper_output_name_target_impl(ctx): return [WrapperOutputNameInfo( output_name = select_wrapper_output_name_for_tests( @@ -180,8 +189,9 @@ wrapper_output_name_target_rule = rule( }, ) -def _single_service_topt_data(): +def _single_service_topt_data(enabled = True): return { + "enabled": enabled, "repo_name": "test_optimization_data", "service_name": "go-service", "manifest_path": ".testoptimization/manifest.txt", @@ -222,6 +232,20 @@ def go_macro_single_service_target(name, tags = None): tags = tags, ) +def go_macro_disabled_raw_target(name, tags = None): + """Target under test for the strict disabled raw go_test branch.""" + dd_topt_go_test( + name = name, + topt_data = _single_service_topt_data(enabled = False), + go_test_rule = _go_test_capture_rule, + data = [":test_macro.bzl"], + env = {"CUSTOM_ENV": "disabled"}, + gc_linkopts = ["-disabled-link-flag"], + importpath = "example.com/disabled/pkg", + rundir = "disabled/rundir", + tags = tags, + ) + def go_macro_multi_service_target(name, tags = None): """Target-under-test: sanitized service-key selection wiring.""" dd_topt_go_test( @@ -429,9 +453,14 @@ def orch_wrapper_materialized_actual_non_windows_target(name, tags = None): executable_name = "hello_test__raw_go_test", tags = ["manual"], ) + fake_metadata_rule( + name = name + "_metadata", + tags = ["manual"], + ) orch_go_test( name = name, actual = ":" + name + "_actual", + metadata = ":" + name + "_metadata", tags = tags, ) @@ -443,9 +472,23 @@ def orch_wrapper_materialized_actual_windows_target(name, tags = None): is_windows = True, tags = ["manual"], ) + fake_metadata_rule( + name = name + "_metadata", + tags = ["manual"], + ) orch_go_test( name = name, actual = ":" + name + "_actual", + metadata = ":" + name + "_metadata", + tags = tags, + ) + +def go_macro_orchestrion_enablement_mismatch_target(name, tags = None): + """Target under test for an incomplete config-gated Go upgrade.""" + dd_topt_go_test( + name = name, + topt_data = _single_service_topt_data(enabled = True), + go_test_rule = _go_test_capture_rule, tags = tags, ) @@ -482,6 +525,20 @@ def _go_macro_single_service_wiring_test_impl(ctx): asserts.true(env, captured.rundir.endswith("tests")) return analysistest.end(env) +def _go_macro_disabled_raw_wiring_test_impl(ctx): + """Assert disabled metadata forwards caller kwargs to one raw public test.""" + env = analysistest.begin(ctx) + target = analysistest.target_under_test(env) + captured = target[ToptGoMacroCaptureInfo] + + asserts.equals(env, 1, len(captured.data_labels)) + asserts.true(env, _has_label_suffix(captured.data_labels, ":test_macro.bzl")) + asserts.equals(env, {"CUSTOM_ENV": "disabled"}, captured.env) + asserts.equals(env, ["-disabled-link-flag"], captured.gc_linkopts) + asserts.equals(env, "example.com/disabled/pkg", captured.importpath) + asserts.equals(env, "disabled/rundir", captured.rundir) + return analysistest.end(env) + def _go_macro_multi_service_wiring_test_impl(ctx): """Assert multi-service key resolution and passthrough attributes.""" env = analysistest.begin(ctx) @@ -699,9 +756,14 @@ def _go_macro_public_wrapper_test_impl(ctx): env = analysistest.begin(ctx) target = analysistest.target_under_test(env) files = target[DefaultInfo].files.to_list() - asserts.equals(env, 2, len(files)) + materialized_metadata = ( + "go_macro_single_service_target__wrapped_" + + "go_macro_single_service_target_topt_bazel_metadata.json" + ) + asserts.equals(env, 3, len(files)) asserts.true(env, _has_file_basename(files, "go_macro_single_service_target")) asserts.true(env, _has_file_basename(files, "go_macro_single_service_target__wrapped_go_macro_single_service_target__raw_go_test.sh")) + asserts.true(env, _has_file_basename(files, materialized_metadata)) run_env = target[RunEnvironmentInfo].environment manifest_env = run_env.get("DD_TEST_OPTIMIZATION_MANIFEST_FILE") asserts.true(env, manifest_env != None) @@ -834,6 +896,14 @@ def _validate_test_optimization_pin_files_missing_go_mod_failure_test_impl(ctx): asserts.expect_failure(env, "requires a package-local go.mod or explicit orchestrion_pin_files") return analysistest.end(env) +def _go_macro_orchestrion_enablement_mismatch_failure_test_impl(ctx): + """Assert a partial upgrade fails instead of silently dropping instrumentation.""" + env = analysistest.begin(ctx) + asserts.expect_failure(env, "Test Optimization metadata is enabled but Orchestrion is disabled") + asserts.expect_failure(env, "--config=test-optimization") + asserts.expect_failure(env, "--write-bazelrc") + return analysistest.end(env) + def _wrapper_output_name_non_windows_test_impl(ctx): """Assert non-Windows wrapper names remain extensionless.""" env = analysistest.begin(ctx) @@ -851,9 +921,12 @@ def _wrapper_output_name_windows_test_impl(ctx): def _windows_wrapper_uses_file_payload_mode_test_impl(ctx): """Assert Windows launchers preserve Bazel file mode instead of proxying uploads.""" env = unittest.begin(ctx) - content = windows_wrapper_content_for_tests("raw.exe") + content = windows_wrapper_content_for_tests("raw.exe", "target_metadata.json") asserts.true(env, "bazel_target_metadata.json" in content) + asserts.true(env, '"%SCRIPT_DIR%target_metadata.json"' in content) + asserts.false(env, "META_BASENAME" in content) asserts.true(env, '"%ACTUAL%" %*' in content) + asserts.true(env, "exit /b %ERRORLEVEL%" in content) asserts.false(env, "DD_TRACE_AGENT_URL" in content) asserts.false(env, "DD_CIVISIBILITY_AGENTLESS_ENABLED" in content) asserts.false(env, "DD_CIVISIBILITY_AGENTLESS_URL" in content) @@ -887,40 +960,56 @@ def _validate_orchestrion_mode_test_impl(ctx): return unittest.end(env) def _orch_transition_forwards_mode_test_impl(ctx): - """Assert the wrapper transition enables Orchestrion and forwards the mode.""" + """Assert the wrapper transition forwards only the Orchestrion mode.""" env = unittest.begin(ctx) result = orch_transition_impl_for_tests(None, struct(orchestrion_mode = "test_optimization")) - asserts.equals(env, True, result["@rules_go//go/private/orchestrion:enabled"]) + asserts.equals(env, 1, len(result)) asserts.equals(env, "test_optimization", result["@rules_go//go/private/orchestrion:mode"]) + asserts.false(env, "@rules_go//go/private/orchestrion:enabled" in result) return unittest.end(env) def _orch_wrapper_materialized_actual_non_windows_test_impl(ctx): - """Assert the wrapper target ships the sibling raw executable.""" + """Assert the wrapper target ships transitioned inputs as siblings.""" env = analysistest.begin(ctx) target = analysistest.target_under_test(env) files = target[DefaultInfo].files.to_list() runfiles = target[DefaultInfo].default_runfiles.files.to_list() - asserts.equals(env, 2, len(files)) + materialized_metadata = ( + "orch_wrapper_materialized_actual_non_windows_target__wrapped_" + + "orch_wrapper_materialized_actual_non_windows_target_metadata.json" + ) + asserts.equals(env, 3, len(files)) asserts.true(env, _has_file_basename(files, "orch_wrapper_materialized_actual_non_windows_target")) asserts.true(env, _has_file_basename(files, "orch_wrapper_materialized_actual_non_windows_target__wrapped_hello_test__raw_go_test")) + asserts.true(env, _has_file_basename(files, materialized_metadata)) asserts.true(env, _has_file_basename(runfiles, "orch_wrapper_materialized_actual_non_windows_target__wrapped_hello_test__raw_go_test")) + asserts.true(env, _has_file_basename(runfiles, materialized_metadata)) return analysistest.end(env) def _orch_wrapper_materialized_actual_windows_test_impl(ctx): - """Assert the Windows wrapper target carries the sibling raw executable.""" + """Assert the Windows wrapper target ships transitioned inputs as siblings.""" env = analysistest.begin(ctx) target = analysistest.target_under_test(env) files = target[DefaultInfo].files.to_list() runfiles = target[DefaultInfo].default_runfiles.files.to_list() - asserts.equals(env, 2, len(files)) + materialized_metadata = ( + "orch_wrapper_materialized_actual_windows_target__wrapped_" + + "orch_wrapper_materialized_actual_windows_target_metadata.json" + ) + asserts.equals(env, 3, len(files)) asserts.true(env, _has_file_basename(files, "orch_wrapper_materialized_actual_windows_target.bat")) asserts.true(env, _has_file_basename(files, "orch_wrapper_materialized_actual_windows_target__wrapped_hello_test__raw_go_test.exe")) + asserts.true(env, _has_file_basename(files, materialized_metadata)) asserts.true(env, _has_file_basename(runfiles, "orch_wrapper_materialized_actual_windows_target__wrapped_hello_test__raw_go_test.exe")) + asserts.true(env, _has_file_basename(runfiles, materialized_metadata)) return analysistest.end(env) go_macro_single_service_wiring_test = analysistest.make( _go_macro_single_service_wiring_test_impl, ) +go_macro_disabled_raw_wiring_test = analysistest.make( + _go_macro_disabled_raw_wiring_test_impl, +) go_macro_multi_service_wiring_test = analysistest.make( _go_macro_multi_service_wiring_test_impl, ) @@ -995,12 +1084,21 @@ go_macro_explicit_service_wiring_test = analysistest.make( ) go_macro_public_wrapper_test = analysistest.make( _go_macro_public_wrapper_test_impl, + config_settings = { + _ORCHESTRION_ENABLED_SETTING: True, + }, ) go_macro_test_optimization_public_wrapper_mode_test = analysistest.make( _go_macro_test_optimization_public_wrapper_mode_test_impl, + config_settings = { + _ORCHESTRION_ENABLED_SETTING: True, + }, ) go_macro_default_general_public_wrapper_mode_test = analysistest.make( _go_macro_default_general_public_wrapper_mode_test_impl, + config_settings = { + _ORCHESTRION_ENABLED_SETTING: True, + }, ) resolve_topt_service_key_missing_failure_test = analysistest.make( _resolve_topt_service_key_missing_failure_test_impl, @@ -1018,6 +1116,10 @@ validate_test_optimization_pin_files_missing_go_mod_failure_test = analysistest. _validate_test_optimization_pin_files_missing_go_mod_failure_test_impl, expect_failure = True, ) +go_macro_orchestrion_enablement_mismatch_failure_test = analysistest.make( + _go_macro_orchestrion_enablement_mismatch_failure_test_impl, + expect_failure = True, +) wrapper_output_name_non_windows_test = analysistest.make( _wrapper_output_name_non_windows_test_impl, ) diff --git a/modules/go/tests/test_workspace_helpers.bzl b/modules/go/tests/test_workspace_helpers.bzl new file mode 100644 index 00000000..f4513f56 --- /dev/null +++ b/modules/go/tests/test_workspace_helpers.bzl @@ -0,0 +1,147 @@ +# Unless explicitly stated otherwise all files in this repository are licensed under +# the Apache 2.0 License. +# +# This product includes software developed at Datadog +# (https://www.datadoghq.com/) Copyright 2025-Present Datadog, Inc. + +"""Unit tests for the public WORKSPACE bootstrap helpers.""" + +load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest") +load( + "@datadog-rules-test-optimization-go//:topt_go_orchestrion_repository.bzl", + "build_orchestrion_repo_call_for_tests", +) +load( + "@datadog-rules-test-optimization-go//:topt_go_workspace.bzl", + "build_go_workspace_sync_specs_for_tests", +) + +def _assert_sync_spec(env, spec, name, service, expected_enabled_by_env): + asserts.equals(env, name, spec["name"]) + asserts.equals(env, name, spec["repo_name"]) + asserts.equals(env, service, spec["service"]) + asserts.equals(env, "go", spec["runtime_name"]) + asserts.equals(env, "1.25.9", spec["runtime_version"]) + asserts.equals(env, "arm64", spec["runtime_arch"]) + asserts.equals(env, "example.com/workspace", spec["runtime_module_path"]) + asserts.equals(env, "custom_topt", spec["out_dir"]) + asserts.equals(env, False, spec["enabled"]) + asserts.equals(env, expected_enabled_by_env, spec["enabled_by_env"]) + asserts.equals(env, 11, spec["http_connect_timeout_seconds"]) + asserts.equals(env, 22, spec["http_max_time_seconds"]) + asserts.equals(env, 3, spec["http_retry_attempts"]) + asserts.equals(env, 4, spec["http_retry_delay_seconds"]) + asserts.equals(env, 5, spec["http_execute_timeout_buffer_seconds"]) + asserts.equals(env, False, spec["known_tests"]) + asserts.equals(env, False, spec["test_management"]) + asserts.equals(env, False, spec["flaky_tests"]) + asserts.equals(env, True, spec["require_git_metadata"]) + asserts.equals(env, True, spec["debug"]) + +def _go_workspace_single_specs_test(ctx): + env = unittest.begin(ctx) + result = build_go_workspace_sync_specs_for_tests( + name = "test_optimization_data_go", + service = "go-service", + runtime_version = "1.25.9", + module_path = "example.com/workspace", + enabled = False, + enabled_by_env = False, + runtime_arch = "arm64", + out_dir = "custom_topt", + http_connect_timeout_seconds = 11, + http_max_time_seconds = 22, + http_retry_attempts = 3, + http_retry_delay_seconds = 4, + http_execute_timeout_buffer_seconds = 5, + known_tests = False, + test_management = False, + flaky_tests = False, + require_git_metadata = True, + debug = True, + ) + asserts.equals(env, 1, len(result["sync_specs"])) + _assert_sync_spec(env, result["sync_specs"][0], "test_optimization_data_go", "go-service", False) + asserts.equals(env, None, result["aggregate_spec"]) + return unittest.end(env) + +def _go_workspace_multi_specs_test(ctx): + env = unittest.begin(ctx) + result = build_go_workspace_sync_specs_for_tests( + name = "test_optimization_data_go", + services = ["go-service-a", "go-service-a", "go-service-b"], + runtime_version = "1.25.9", + module_path = "example.com/workspace", + enabled = False, + enabled_by_env = False, + runtime_arch = "arm64", + out_dir = "custom_topt", + http_connect_timeout_seconds = 11, + http_max_time_seconds = 22, + http_retry_attempts = 3, + http_retry_delay_seconds = 4, + http_execute_timeout_buffer_seconds = 5, + known_tests = False, + test_management = False, + flaky_tests = False, + require_git_metadata = True, + debug = True, + ) + asserts.equals(env, ["go_service_a", "go_service_a_2", "go_service_b"], result["aggregate_spec"]["service_keys"]) + asserts.equals( + env, + [ + "test_optimization_data_go_go_service_a", + "test_optimization_data_go_go_service_a_2", + "test_optimization_data_go_go_service_b", + ], + result["aggregate_spec"]["repo_names"], + ) + for i in range(3): + _assert_sync_spec( + env, + result["sync_specs"][i], + result["aggregate_spec"]["repo_names"][i], + ["go-service-a", "go-service-a", "go-service-b"][i], + False, + ) + return unittest.end(env) + +def _go_workspace_specs_default_to_config_gated_test(ctx): + env = unittest.begin(ctx) + result = build_go_workspace_sync_specs_for_tests( + name = "test_optimization_data_go", + service = "go-service", + runtime_version = "1.25.9", + module_path = "example.com/workspace", + ) + asserts.equals(env, True, result["sync_specs"][0]["enabled_by_env"]) + return unittest.end(env) + +def _orchestrion_call_spec_test(ctx): + env = unittest.begin(ctx) + call = build_orchestrion_repo_call_for_tests( + dd_trace_go_version = "v2.9.0", + dd_trace_go_versions = {"example.com/service": "v2.8.0"}, + version = "v1.9.0", + log_timing = True, + ) + asserts.equals( + env, + { + "name": "rules_go_orchestrion_tool", + "dd_trace_go_version": "v2.9.0", + "dd_trace_go_versions": {"example.com/service": "v2.8.0"}, + "enabled_by_env": True, + "version": "v1.9.0", + "log_timing": True, + }, + call, + ) + asserts.false(env, "enabled" in call) + return unittest.end(env) + +go_workspace_single_specs_test = unittest.make(_go_workspace_single_specs_test) +go_workspace_multi_specs_test = unittest.make(_go_workspace_multi_specs_test) +go_workspace_specs_default_to_config_gated_test = unittest.make(_go_workspace_specs_default_to_config_gated_test) +orchestrion_call_spec_test = unittest.make(_orchestrion_call_spec_test) diff --git a/modules/go/tools/dd_topt_go_bootstrap/main.go b/modules/go/tools/dd_topt_go_bootstrap/main.go index 356ffe15..ba606071 100644 --- a/modules/go/tools/dd_topt_go_bootstrap/main.go +++ b/modules/go/tools/dd_topt_go_bootstrap/main.go @@ -1363,6 +1363,17 @@ func bazelrcSnippet(cfg config) (string, error) { for _, key := range bazelrcRepoEnvKeys { fmt.Fprintf(&buf, "common:%s --repo_env=%s\n", cfg.bazelrcConfig, key) } + fmt.Fprintf(&buf, "common:%s --repo_env=DD_TEST_OPTIMIZATION_ENABLED=1\n", cfg.bazelrcConfig) + rulesGoRepoName := cfg.rulesGoRepoName + if !cfg.workspaceMode { + // Bzlmod exposes rules_go under its module name. The WORKSPACE + // repository name is configurable because repository rules may + // remap it (the default there remains io_bazel_rules_go). + rulesGoRepoName = "rules_go" + } else if rulesGoRepoName == "" { + rulesGoRepoName = defaultRulesGoRepoName + } + fmt.Fprintf(&buf, "build:%s --@%s//go/private/orchestrion:enabled=true\n", cfg.bazelrcConfig, rulesGoRepoName) fmt.Fprintf(&buf, "test:%s --remote_download_minimal\n", cfg.bazelrcConfig) fmt.Fprintf(&buf, "test:%s --remote_download_regex=.*test[.]outputs.*\n", cfg.bazelrcConfig) fmt.Fprintf(&buf, "test:%s --zip_undeclared_test_outputs\n", cfg.bazelrcConfig) @@ -1371,6 +1382,17 @@ func bazelrcSnippet(cfg config) (string, error) { return buf.String(), nil } +func apparentRulesGoRepoName(cfg config) string { + rulesGoRepoName := cfg.rulesGoRepoName + if !cfg.workspaceMode { + return "rules_go" + } + if rulesGoRepoName == "" { + return defaultRulesGoRepoName + } + return rulesGoRepoName +} + // writeBazelrcBlock inserts or replaces the managed .bazelrc block. func writeBazelrcBlock(cfg config) error { snippet, err := bazelrcSnippet(cfg) @@ -1420,6 +1442,7 @@ func validationScript(cfg config) (string, error) { fmt.Fprintf(&buf, "SYNC_REPO=%s\n", shellQuote(cfg.syncRepoName)) fmt.Fprintf(&buf, "DOCTOR_TARGET=%s\n", shellQuote(cfg.validationDoctorTarget)) fmt.Fprintf(&buf, "UPLOAD_TARGET=%s\n", shellQuote(cfg.validationUploadTarget)) + fmt.Fprintf(&buf, "RULES_GO_ENABLED_LABEL=%s\n", shellQuote("@"+apparentRulesGoRepoName(cfg)+"//go/private/orchestrion:enabled")) buf.WriteString("WORKSPACE_DIR=\"$(pwd -P)\"\n") buf.WriteString("BEP_TMP_ROOT=\"\"\n") buf.WriteString("BEP_JSON_DIR=\"\"\n") @@ -1539,6 +1562,32 @@ run_step() { "$@" } +validate_disabled_bootstrap() { + local alias_files + log "validate ordinary no-config bootstrap" + if ! env -u DD_API_KEY -u DD_SITE -u DD_TEST_OPTIMIZATION_ENABLED \ + "${BAZEL}" query "@${SYNC_REPO}//:test_optimization_files"; then + warn "ordinary no-config metadata bootstrap failed" + return 1 + fi + alias_files="$( + env -u DD_API_KEY -u DD_SITE -u DD_TEST_OPTIMIZATION_ENABLED \ + "${BAZEL}" cquery \ + "${RULES_GO_ENABLED_LABEL%:enabled}:tool_binary" --output=files + )" || return $? + if [[ -n "${alias_files}" ]]; then + warn "ordinary no-config Orchestrion alias unexpectedly exposed files: ${alias_files}" + return 1 + fi + + log "validate explicit disabled precedence" + env -u DD_API_KEY -u DD_SITE -u DD_TEST_OPTIMIZATION_ENABLED \ + "${BAZEL}" query "--config=${BAZEL_CONFIG}" \ + --repo_env=DD_TEST_OPTIMIZATION_ENABLED=0 \ + "--${RULES_GO_ENABLED_LABEL}=false" \ + "@${SYNC_REPO}//:test_optimization_files" +} + upload=0 while (($#)); do case "$1" in @@ -1564,6 +1613,10 @@ done trap cleanup EXIT check_disk +if ! validate_disabled_bootstrap; then + warn "disabled bootstrap validation failed; skipping enabled validation" + exit 1 +fi run_step "sync ${SYNC_REPO}" "${BAZEL}" sync "${SYNC_FLAGS[@]}" "--repo_env=FETCH_SALT=$(date +%s)" "--only=${SYNC_REPO}" sync_status=$? if (( sync_status != 0 )); then @@ -1882,15 +1935,15 @@ datadog_go_test_optimization_workspace_repositories( buf.WriteString(")\n\n") buf.WriteString(fmt.Sprintf(`load("@%s//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies") -load("@%s//go:orchestrion_workspace.bzl", "go_orchestrion_tool_repo") +load("@datadog-rules-test-optimization-go//:topt_go_orchestrion_repository.bzl", "dd_topt_go_orchestrion_tool_repo") -go_rules_dependencies() -go_register_toolchains(version = "") -go_orchestrion_tool_repo( +dd_topt_go_orchestrion_tool_repo( version = "%s", %s ) -`, cfg.rulesGoRepoName, cfg.rulesGoRepoName, cfg.orchestrionVersion, workspaceSnippetTracerConfig(cfg))) +go_rules_dependencies() +go_register_toolchains(version = "") +`, cfg.rulesGoRepoName, cfg.orchestrionVersion, workspaceSnippetTracerConfig(cfg))) if cfg.workspaceMode || strings.TrimSpace(cfg.service) != "" || strings.TrimSpace(cfg.runtimeVersion) != "" { buf.WriteString("\n") buf.WriteString(workspaceSyncSnippet(cfg)) @@ -1901,16 +1954,19 @@ go_orchestrion_tool_repo( // workspaceSyncSnippet renders the repository-rule call that fetches Test // Optimization metadata during WORKSPACE repository resolution. func workspaceSyncSnippet(cfg config) string { - return fmt.Sprintf(`load("@datadog-rules-test-optimization//tools/core:test_optimization_sync.bzl", "test_optimization_sync") + modulePathLine := "" + if cfg.goModulePath != "" { + modulePathLine = fmt.Sprintf(" module_path = %q,\n", cfg.goModulePath) + } + return fmt.Sprintf(`load("@datadog-rules-test-optimization-go//:topt_go_workspace.bzl", "dd_topt_go_workspace_sync_repositories") -test_optimization_sync( +dd_topt_go_workspace_sync_repositories( name = "%s", service = "%s", - runtime_name = "go", runtime_version = "%s", - require_git_metadata = True, +%s require_git_metadata = True, ) -`, cfg.syncRepoName, cfg.service, cfg.runtimeVersion) +`, cfg.syncRepoName, cfg.service, cfg.runtimeVersion, modulePathLine) } func workspaceSnippetTracerConfig(cfg config) string { @@ -2276,34 +2332,29 @@ func ensureGuidedRootBuild(cfg config) error { } } - text, err = ensureLoadStatement(text, `load("@datadog-rules-test-optimization//tools/core:test_optimization_doctor.bzl", "dd_test_optimization_doctor")`) - if err != nil { - return err - } - text, err = ensureLoadStatement(text, `load("@datadog-rules-test-optimization//tools/core:test_optimization_uploader.bzl", "dd_payload_uploader")`) + text, err = ensureLoadStatement(text, `load("@datadog-rules-test-optimization//tools/core:test_optimization_targets.bzl", "dd_test_optimization_targets")`) if err != nil { return err } doctorBlock := fmt.Sprintf(`%s -dd_test_optimization_doctor( - name = "%s", - data = ["@%s//:test_optimization_context"], +dd_test_optimization_targets( + name = "test_optimization", + sync_repo_name = "%s", + doctor_name = "%s", + uploader_name = "%s", %s) %s -`, doctorBlockStart, cfg.doctorTargetName, cfg.syncRepoName, renderExpectedTargetsAttr(cfg.expectedTargets), doctorBlockEnd) +`, doctorBlockStart, cfg.syncRepoName, cfg.doctorTargetName, cfg.uploaderTargetName, renderExpectedTargetsAttr(cfg.expectedTargets), doctorBlockEnd) text, err = replaceManagedSection(text, doctorBlockStart, doctorBlockEnd, doctorBlock) if err != nil { return err } uploaderBlock := fmt.Sprintf(`%s -dd_payload_uploader( - name = "%s", - data = ["@%s//:test_optimization_context"], -) +# The doctor/uploader pair is generated by dd_test_optimization_targets above. %s -`, uploaderBlockStart, cfg.uploaderTargetName, cfg.syncRepoName, uploaderBlockEnd) +`, uploaderBlockStart, uploaderBlockEnd) text, err = replaceManagedSection(text, uploaderBlockStart, uploaderBlockEnd, uploaderBlock) if err != nil { return err @@ -3066,10 +3117,22 @@ func bootstrapSyncCommands(cfg config) [][]string { switch mode { case "targeted": - commands = append(commands, - append([]string{"list", "-mod=mod", "-tags=tools"}, orchestrionToolPackages...), - append([]string{"list", "-mod=readonly", "-tags=tools"}, orchestrionToolPackages...), - ) + // Seed every selected module version explicitly before loading package + // roots. On a cold cache, go list may otherwise query @latest even + // though go.mod already contains the intended requirements. + commands = append(commands, []string{"mod", "download", "github.com/DataDog/orchestrion@" + cfg.orchestrionVersion}) + for _, modulePath := range ddTraceGoModules { + commands = append(commands, []string{"mod", "download", modulePath + "@" + versions[modulePath]}) + } + // Resolve package roots one at a time. A single go list spanning several + // module roots can ignore freshly edited requirements on a cold module + // cache and fall back to @latest for some roots. + for _, packagePath := range orchestrionToolPackages { + commands = append(commands, []string{"list", "-mod=mod", "-tags=tools", packagePath}) + } + for _, packagePath := range orchestrionToolPackages { + commands = append(commands, []string{"list", "-mod=readonly", "-tags=tools", packagePath}) + } case "tidy": commands = append(commands, []string{"get", "github.com/DataDog/dd-trace-go/v2/orchestrion@" + versions["github.com/DataDog/dd-trace-go/v2"]}, @@ -3350,17 +3413,22 @@ func normalizedGoEnv(env []string) []string { func setEnvValue(env []string, key, value string) []string { prefix := key + "=" + normalized := make([]string, 0, len(env)+1) replaced := false - for idx, entry := range env { + for _, entry := range env { if strings.HasPrefix(entry, prefix) { - env[idx] = prefix + value - replaced = true + if !replaced { + normalized = append(normalized, prefix+value) + replaced = true + } + continue } + normalized = append(normalized, entry) } if !replaced { - env = append(env, prefix+value) + normalized = append(normalized, prefix+value) } - return env + return normalized } func envValue(env []string, key string) string { @@ -3379,15 +3447,18 @@ func orchestrionBootstrapEnv() []string { goModCache := filepath.Join(cacheRoot, "pkg", "mod") goBuildCache := filepath.Join(cacheRoot, "cache") - env = append(env, - "GO111MODULE=on", - "GOWORK=off", - "GOPATH="+cacheRoot, - "GOMODCACHE="+goModCache, - "GOCACHE="+goBuildCache, - "GOPROXY=https://proxy.golang.org,direct", - "GOSUMDB=sum.golang.org", - ) + env = setEnvValue(env, "GO111MODULE", "on") + env = setEnvValue(env, "GOWORK", "off") + env = setEnvValue(env, "GOPATH", cacheRoot) + env = setEnvValue(env, "GOMODCACHE", goModCache) + env = setEnvValue(env, "GOCACHE", goBuildCache) + env = setEnvValue(env, "GOPROXY", "https://proxy.golang.org,direct") + env = setEnvValue(env, "GOSUMDB", "sum.golang.org") + // The Orchestrion and dd-trace-go pins are public modules. Do not let a + // developer's broad GOPRIVATE/GONOSUMDB settings bypass the public proxy + // and leave the isolated module cache with incomplete package metadata. + env = setEnvValue(env, "GOPRIVATE", "") + env = setEnvValue(env, "GONOSUMDB", "") return normalizedGoEnv(env) } diff --git a/modules/go/tools/dd_topt_go_bootstrap/main_test.go b/modules/go/tools/dd_topt_go_bootstrap/main_test.go index 0e1da47a..7192778f 100644 --- a/modules/go/tools/dd_topt_go_bootstrap/main_test.go +++ b/modules/go/tools/dd_topt_go_bootstrap/main_test.go @@ -101,6 +101,9 @@ func TestManagedModuleBlockIncludesRulesGoExtension(t *testing.T) { if !strings.Contains(got, `version = "v1.9.0"`) { t.Fatalf("expected orchestrion version in managed block:\n%s", got) } + if strings.Contains(got, `enabled_by_env`) { + t.Fatalf("expected managed block to rely on the config-gated extension default:\n%s", got) + } if !strings.Contains(got, `dd_trace_go_version = "v2.5.0"`) { t.Fatalf("expected dd-trace-go version in managed block:\n%s", got) } @@ -172,6 +175,11 @@ func TestWorkspaceSnippetSupportsMixedFetchModes(t *testing.T) { t.Fatalf("workspace snippet missing %q:\n%s", want, got) } } + toolRepoCall := strings.Index(got, "\ndd_topt_go_orchestrion_tool_repo(") + dependenciesCall := strings.Index(got, "\ngo_rules_dependencies()\n") + if toolRepoCall < 0 || dependenciesCall < 0 || toolRepoCall > dependenciesCall { + t.Fatalf("workspace snippet must declare the real Orchestrion repository before rules_go installs its fallback:\n%s", got) + } } func TestWorkspaceSnippetFallsBackToRulesGoCommit(t *testing.T) { @@ -236,10 +244,12 @@ func TestWorkspaceModeSnippetIncludesSyncAndBaseVariant(t *testing.T) { for _, want := range []string{ `rules_go_variant = "base"`, `rules_go_repo_name = "io_bazel_rules_go"`, - `load("@datadog-rules-test-optimization//tools/core:test_optimization_sync.bzl", "test_optimization_sync")`, + `load("@datadog-rules-test-optimization-go//:topt_go_workspace.bzl", "dd_topt_go_workspace_sync_repositories")`, + `load("@datadog-rules-test-optimization-go//:topt_go_orchestrion_repository.bzl", "dd_topt_go_orchestrion_tool_repo")`, + `dd_topt_go_workspace_sync_repositories(`, + `dd_topt_go_orchestrion_tool_repo(`, `name = "test_optimization_data_worker"`, `service = "worker"`, - `runtime_name = "go"`, `runtime_version = "1.25.9"`, `require_git_metadata = True`, } { @@ -412,6 +422,8 @@ func TestBazelrcSnippetUsesRepoEnvOnlyForSyncMetadata(t *testing.T) { `common:test-optimization --repo_env=DD_TEST_OPTIMIZATION_AGENTLESS_URL`, `common:test-optimization --repo_env=DD_GIT_REPOSITORY_URL`, `common:test-optimization --repo_env=DD_PR_NUMBER`, + `common:test-optimization --repo_env=DD_TEST_OPTIMIZATION_ENABLED=1`, + `build:test-optimization --@rules_go//go/private/orchestrion:enabled=true`, `test:test-optimization --remote_download_minimal`, `test:test-optimization --remote_download_regex=.*test[.]outputs.*`, `test:test-optimization --zip_undeclared_test_outputs`, @@ -654,6 +666,7 @@ func TestValidationScriptUsesConfiguredFlowAndUploadOptIn(t *testing.T) { `SYNC_REPO='test_optimization_data_worker'`, `DOCTOR_TARGET='//:dd_test_optimization_doctor'`, `UPLOAD_TARGET='//:dd_upload_payloads'`, + `RULES_GO_ENABLED_LABEL='@rules_go//go/private/orchestrion:enabled'`, `WORKSPACE_DIR="$(pwd -P)"`, `BEP_TMP_ROOT=""`, `BEP_JSON_DIR=""`, @@ -682,6 +695,12 @@ func TestValidationScriptUsesConfiguredFlowAndUploadOptIn(t *testing.T) { `--report-json`, `mktemp -d "${tmp_parent%/}/dd-go-topt.XXXXXX"`, `sync -> controls -> instrumented tests -> doctor -> dry-run uploader -> optional upload`, + `validate ordinary no-config bootstrap`, + `validate explicit disabled precedence`, + `query "@${SYNC_REPO}//:test_optimization_files"`, + `"${BAZEL}" cquery \ + "${RULES_GO_ENABLED_LABEL%:enabled}:tool_binary"`, + `--repo_env=DD_TEST_OPTIMIZATION_ENABLED=0`, `upload skipped; rerun with --upload`, `${BAZEL}" shutdown`, } { @@ -799,6 +818,9 @@ func TestValidationScriptRunsWithNoControlTargets(t *testing.T) { } logText := string(logBytes) for _, want := range []string{ + "query @test_optimization_data//:test_optimization_files", + "cquery @rules_go//go/private/orchestrion:tool_binary --output=files", + "query --config=test-optimization --repo_env=DD_TEST_OPTIMIZATION_ENABLED=0 --@rules_go//go/private/orchestrion:enabled=false @test_optimization_data//:test_optimization_files", "sync --config=test-optimization --repo_env=FETCH_SALT=", "test --config=test-optimization --build_event_json_file=", "//pkg:go_default_test", @@ -1176,6 +1198,14 @@ test:old --test_env=DD_GIT_BRANCH=main if strings.Contains(text, "--test_env=DD_GIT_BRANCH") || strings.Count(text, bazelrcBlockStart) != 1 { t.Fatalf("expected old managed block to be replaced:\n%s", text) } + for _, want := range []string{ + `common:test-optimization --repo_env=DD_TEST_OPTIMIZATION_ENABLED=1`, + `build:test-optimization --@rules_go//go/private/orchestrion:enabled=true`, + } { + if !strings.Contains(text, want) { + t.Fatalf("migrated managed block missing %q:\n%s", want, text) + } + } } func TestWriteBazelrcBlockRejectsPathTraversal(t *testing.T) { @@ -1538,6 +1568,9 @@ func TestManagedGuidedModuleBlockIncludesModulePath(t *testing.T) { if !strings.Contains(got, `module_path = "github.com/DataDog/example-service"`) { t.Fatalf("expected guided block to include explicit module_path:\n%s", got) } + if strings.Contains(got, `enabled_by_env`) { + t.Fatalf("expected guided block to rely on the config-gated extension default:\n%s", got) + } } func TestWriteStarterOrchestrionYML(t *testing.T) { @@ -1608,6 +1641,31 @@ func TestBootstrapSyncCommandsTargetedModeAvoidsGoModTidy(t *testing.T) { if !strings.Contains(joined, "list -mod=readonly -tags=tools github.com/DataDog/orchestrion") { t.Fatalf("targeted bootstrap sync must verify readonly module completeness:\n%s", joined) } + for _, mode := range []string{"-mod=mod", "-mod=readonly"} { + for _, packagePath := range orchestrionToolPackages { + want := "list " + mode + " -tags=tools " + packagePath + if !strings.Contains(joined, want) { + t.Fatalf("targeted bootstrap sync missing sequential package resolution %q:\n%s", want, joined) + } + } + } + expectedDownloads := []string{ + "github.com/DataDog/orchestrion@v1.9.0", + "github.com/DataDog/dd-trace-go/v2@v2.9.0", + "github.com/DataDog/dd-trace-go/contrib/net/http/v2@v2.9.0", + "github.com/DataDog/dd-trace-go/contrib/log/slog/v2@v2.9.0", + } + for _, moduleVersion := range expectedDownloads { + want := "mod download " + moduleVersion + if !strings.Contains(joined, want) { + t.Fatalf("targeted bootstrap sync missing exact module download %q:\n%s", want, joined) + } + } + for _, command := range got { + if len(command) > 5 && command[0] == "list" { + t.Fatalf("targeted bootstrap sync must resolve one package root per go list command: %#v", command) + } + } } func TestBootstrapSyncCommandsDefaultsToTargetedMode(t *testing.T) { @@ -2145,6 +2203,42 @@ func TestNormalizedGoEnvForcesGoWorkOff(t *testing.T) { } } +func TestOrchestrionBootstrapEnvUsesPublicModuleResolution(t *testing.T) { + got := orchestrionBootstrapEnv() + cacheRoot := filepath.Join(os.TempDir(), sharedOrchestrionCacheDirName) + for key, want := range map[string]string{ + "GOPATH": cacheRoot, + "GOMODCACHE": filepath.Join(cacheRoot, "pkg", "mod"), + "GOCACHE": filepath.Join(cacheRoot, "cache"), + } { + if value := envValue(got, key); value != want { + t.Fatalf("%s=%q, want %q", key, value, want) + } + prefix := key + "=" + count := 0 + for _, entry := range got { + if strings.HasPrefix(entry, prefix) { + count++ + } + } + if count != 1 { + t.Fatalf("%s appears %d times in bootstrap environment, want exactly once", key, count) + } + } + if envValue(got, "GOPROXY") != "https://proxy.golang.org,direct" { + t.Fatalf("GOPROXY=%q, want public proxy", envValue(got, "GOPROXY")) + } + if envValue(got, "GOSUMDB") != "sum.golang.org" { + t.Fatalf("GOSUMDB=%q, want public checksum database", envValue(got, "GOSUMDB")) + } + if envValue(got, "GOPRIVATE") != "" { + t.Fatalf("GOPRIVATE=%q, want empty for public bootstrap modules", envValue(got, "GOPRIVATE")) + } + if envValue(got, "GONOSUMDB") != "" { + t.Fatalf("GONOSUMDB=%q, want empty for public bootstrap modules", envValue(got, "GONOSUMDB")) + } +} + func TestNormalizeDDTraceGoVersionMutatesConfigBeforePersistence(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("shell-script based helper test is Unix-only") diff --git a/modules/go/topt_go_extension.bzl b/modules/go/topt_go_extension.bzl index 37056518..28fc3992 100644 --- a/modules/go/topt_go_extension.bzl +++ b/modules/go/topt_go_extension.bzl @@ -44,16 +44,106 @@ def _record_repo_owner_or_fail(seen_repo_owners, repo_name, owner, tag_name): ) seen_repo_owners[repo_name] = owner +def _build_go_single_repo_spec( + name, + service, + module_path = "", + runtime_version = "", + runtime_arch = "", + out_dir = "", + http_connect_timeout_seconds = HTTP_POLICY_ATTR_UNSET, + http_max_time_seconds = HTTP_POLICY_ATTR_UNSET, + http_retry_attempts = HTTP_POLICY_ATTR_UNSET, + http_retry_delay_seconds = HTTP_POLICY_ATTR_UNSET, + http_execute_timeout_buffer_seconds = HTTP_POLICY_ATTR_UNSET, + known_tests = True, + test_management = True, + flaky_tests = True, + enabled = True, + enabled_by_env = True, + require_git_metadata = False, + debug = False): + """Build the complete sync spec for one Go service repository.""" + return { + "name": name, + "repo_name": name, + "out_dir": out_dir, + "service": service, + "runtime_name": "go", + "runtime_version": runtime_version, + "runtime_arch": runtime_arch, + "runtime_module_path": module_path, + "http_connect_timeout_seconds": http_connect_timeout_seconds, + "http_max_time_seconds": http_max_time_seconds, + "http_retry_attempts": http_retry_attempts, + "http_retry_delay_seconds": http_retry_delay_seconds, + "http_execute_timeout_buffer_seconds": http_execute_timeout_buffer_seconds, + "known_tests": known_tests, + "test_management": test_management, + "flaky_tests": flaky_tests, + "enabled": enabled, + "enabled_by_env": enabled_by_env, + "require_git_metadata": require_git_metadata, + "debug": debug, + } + +def _build_go_multi_repo_specs( + name, + services, + module_path = "", + runtime_version = "", + runtime_arch = "", + out_dir = "", + http_connect_timeout_seconds = HTTP_POLICY_ATTR_UNSET, + http_max_time_seconds = HTTP_POLICY_ATTR_UNSET, + http_retry_attempts = HTTP_POLICY_ATTR_UNSET, + http_retry_delay_seconds = HTTP_POLICY_ATTR_UNSET, + http_execute_timeout_buffer_seconds = HTTP_POLICY_ATTR_UNSET, + known_tests = True, + test_management = True, + flaky_tests = True, + enabled = True, + enabled_by_env = True, + require_git_metadata = False, + debug = False): + """Build the per-service sync specs for a multi-service Go tag.""" + service_keys = _compute_service_keys(services) + repo_names = _compute_repo_names(name, service_keys) + specs = [] + for i in range(len(services)): + specs.append(_build_go_single_repo_spec( + name = repo_names[i], + service = services[i], + module_path = module_path, + runtime_version = runtime_version, + runtime_arch = runtime_arch, + out_dir = out_dir, + http_connect_timeout_seconds = http_connect_timeout_seconds, + http_max_time_seconds = http_max_time_seconds, + http_retry_attempts = http_retry_attempts, + http_retry_delay_seconds = http_retry_delay_seconds, + http_execute_timeout_buffer_seconds = http_execute_timeout_buffer_seconds, + known_tests = known_tests, + test_management = test_management, + flaky_tests = flaky_tests, + enabled = enabled, + enabled_by_env = enabled_by_env, + require_git_metadata = require_git_metadata, + debug = debug, + )) + return specs + +build_go_single_repo_spec_for_tests = _build_go_single_repo_spec +build_go_multi_repo_specs_for_tests = _build_go_multi_repo_specs + def _materialize_single_service_repo(call): - test_optimization_sync( + test_optimization_sync(**_build_go_single_repo_spec( name = call.name, - repo_name = call.name, - out_dir = call.out_dir, service = call.service, - runtime_name = "go", + module_path = call.module_path, runtime_version = call.runtime_version, runtime_arch = call.runtime_arch, - runtime_module_path = call.module_path, + out_dir = call.out_dir, http_connect_timeout_seconds = call.http_connect_timeout_seconds, http_max_time_seconds = call.http_max_time_seconds, http_retry_attempts = call.http_retry_attempts, @@ -61,34 +151,38 @@ def _materialize_single_service_repo(call): http_execute_timeout_buffer_seconds = call.http_execute_timeout_buffer_seconds, known_tests = call.known_tests, test_management = call.test_management, + flaky_tests = call.flaky_tests, + enabled = call.enabled, + enabled_by_env = call.enabled_by_env, require_git_metadata = call.require_git_metadata, debug = call.debug, - ) + )) def _materialize_multi_service_repos(call): service_keys = _compute_service_keys(call.services) repo_names = _compute_repo_names(call.name, service_keys) - for i in range(len(call.services)): - test_optimization_sync( - name = repo_names[i], - repo_name = repo_names[i], - out_dir = call.out_dir, - service = call.services[i], - runtime_name = "go", - runtime_version = call.runtime_version, - runtime_arch = call.runtime_arch, - runtime_module_path = call.module_path, - http_connect_timeout_seconds = call.http_connect_timeout_seconds, - http_max_time_seconds = call.http_max_time_seconds, - http_retry_attempts = call.http_retry_attempts, - http_retry_delay_seconds = call.http_retry_delay_seconds, - http_execute_timeout_buffer_seconds = call.http_execute_timeout_buffer_seconds, - known_tests = call.known_tests, - test_management = call.test_management, - require_git_metadata = call.require_git_metadata, - debug = call.debug, - ) + for spec in _build_go_multi_repo_specs( + name = call.name, + services = call.services, + module_path = call.module_path, + runtime_version = call.runtime_version, + runtime_arch = call.runtime_arch, + out_dir = call.out_dir, + http_connect_timeout_seconds = call.http_connect_timeout_seconds, + http_max_time_seconds = call.http_max_time_seconds, + http_retry_attempts = call.http_retry_attempts, + http_retry_delay_seconds = call.http_retry_delay_seconds, + http_execute_timeout_buffer_seconds = call.http_execute_timeout_buffer_seconds, + known_tests = call.known_tests, + test_management = call.test_management, + flaky_tests = call.flaky_tests, + enabled = call.enabled, + enabled_by_env = call.enabled_by_env, + require_git_metadata = call.require_git_metadata, + debug = call.debug, + ): + test_optimization_sync(**spec) test_optimization_multi_aggregate( name = call.name, @@ -137,6 +231,9 @@ test_optimization_go_extension = module_extension( "http_execute_timeout_buffer_seconds": attr.int(default = HTTP_POLICY_ATTR_UNSET), "known_tests": attr.bool(default = True), "test_management": attr.bool(default = True), + "flaky_tests": attr.bool(default = True), + "enabled": attr.bool(default = True), + "enabled_by_env": attr.bool(default = True), "require_git_metadata": attr.bool(default = False), "debug": attr.bool(default = False), }), diff --git a/modules/go/topt_go_orchestrion.bzl b/modules/go/topt_go_orchestrion.bzl index bf33ce3e..a58363a4 100644 --- a/modules/go/topt_go_orchestrion.bzl +++ b/modules/go/topt_go_orchestrion.bzl @@ -6,13 +6,14 @@ """Internal Orchestrion wrapper rule for Go tests.""" +load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") + _BAZEL_TARGET_METADATA_OUTPUT = "bazel_target_metadata.json" _ORCHESTRION_MODE_GENERAL = "general" _ORCHESTRION_MODE_TEST_OPTIMIZATION = "test_optimization" def _orch_transition_impl(_settings, _attr): return { - "@rules_go//go/private/orchestrion:enabled": True, "@rules_go//go/private/orchestrion:mode": _attr.orchestrion_mode, } @@ -21,10 +22,7 @@ orch_transition_impl_for_tests = _orch_transition_impl orch_transition = transition( implementation = _orch_transition_impl, inputs = [], - outputs = [ - "@rules_go//go/private/orchestrion:enabled", - "@rules_go//go/private/orchestrion:mode", - ], + outputs = ["@rules_go//go/private/orchestrion:mode"], ) def _first_target(dep): @@ -54,14 +52,17 @@ def _wrapped_actual_output_name(label_name, executable_basename): """Return the wrapper-owned sibling executable name used at test runtime.""" return label_name + "__wrapped_" + executable_basename -def _unix_wrapper_content(actual_filename): +def _wrapped_metadata_output_name(label_name, metadata_basename): + """Return the wrapper-owned sibling metadata name used at test runtime.""" + return label_name + "__wrapped_" + metadata_basename + +def _unix_wrapper_content(actual_filename, metadata_filename): """Render the Unix launcher used by the Orchestrion wrapper target.""" return """#!/usr/bin/env bash set -euo pipefail script_dir="$(cd "$(dirname "$0")" && pwd)" actual="$script_dir/%s" -metadata_basename="${DD_TEST_OPTIMIZATION_BAZEL_TARGET_METADATA_BASENAME:-}" undeclared_dir="${TEST_UNDECLARED_OUTPUTS_DIR:-}" if [[ ! -x "$actual" ]]; then @@ -69,23 +70,22 @@ if [[ ! -x "$actual" ]]; then exit 1 fi -if [[ -n "$metadata_basename" && -n "$undeclared_dir" ]]; then - metadata_source="$script_dir/$metadata_basename" +if [[ -n "$undeclared_dir" ]]; then + metadata_source="$script_dir/%s" if [[ -f "$metadata_source" ]]; then cp "$metadata_source" "$undeclared_dir/%s" fi fi "$actual" "$@" -""" % (actual_filename, _BAZEL_TARGET_METADATA_OUTPUT) +""" % (actual_filename, metadata_filename, _BAZEL_TARGET_METADATA_OUTPUT) -def _windows_wrapper_content(actual_filename): +def _windows_wrapper_content(actual_filename, metadata_filename): """Render the Windows launcher used by the Orchestrion wrapper target.""" return """@echo off setlocal set "SCRIPT_DIR=%%~dp0" set "ACTUAL=%%SCRIPT_DIR%%%s" -set "META_BASENAME=%%DD_TEST_OPTIMIZATION_BAZEL_TARGET_METADATA_BASENAME%%" set "UNDECLARED_DIR=%%TEST_UNDECLARED_OUTPUTS_DIR%%" if not exist "%%ACTUAL%%" ( @@ -93,36 +93,48 @@ if not exist "%%ACTUAL%%" ( exit /b 1 ) -if not "%%META_BASENAME%%"=="" if not "%%UNDECLARED_DIR%%"=="" ( - set "META_SOURCE=%%SCRIPT_DIR%%%%META_BASENAME%%" - if exist "%%META_SOURCE%%" copy /Y "%%META_SOURCE%%" "%%UNDECLARED_DIR%%\\%s" >nul +if not "%%UNDECLARED_DIR%%"=="" ( + if exist "%%SCRIPT_DIR%%%s" copy /Y "%%SCRIPT_DIR%%%s" "%%UNDECLARED_DIR%%\\%s" >nul ) "%%ACTUAL%%" %%* +exit /b %%ERRORLEVEL%% """ % ( actual_filename.replace("/", "\\"), + metadata_filename.replace("/", "\\"), + metadata_filename.replace("/", "\\"), _BAZEL_TARGET_METADATA_OUTPUT, ) def _orch_go_test_impl(ctx): + if ctx.attr.test_optimization_enabled and not ctx.attr._orchestrion_enabled[BuildSettingInfo].value: + fail( + "orch_go_test: Test Optimization metadata is enabled but Orchestrion is disabled; " + + "run with --config=test-optimization. Consumers upgrading an existing setup should " + + "rerun dd_topt_go_bootstrap with --write-bazelrc before building tests.", + ) + dep_exe, dep_runfiles = _dep_exec_and_runfiles(ctx.attr.actual) dep_run_environment = _dep_run_environment_info(ctx.attr.actual) + metadata_file = ctx.file.metadata is_windows = ctx.target_platform_has_constraint(ctx.attr._windows_constraint[platform_common.ConstraintValueInfo]) out = ctx.actions.declare_file(_select_wrapper_output_name(ctx.label.name, dep_exe.basename, is_windows)) actual_out = ctx.actions.declare_file(_wrapped_actual_output_name(ctx.label.name, dep_exe.basename), sibling = out) + metadata_out = ctx.actions.declare_file(_wrapped_metadata_output_name(ctx.label.name, metadata_file.basename), sibling = out) - # Materialize the raw test binary next to the wrapper so the launcher does - # not have to guess which configuration-specific execroot path Bazel chose. + # Materialize transitioned inputs next to the wrapper so the launcher does + # not have to guess which configuration-specific execroot paths Bazel chose. ctx.actions.symlink(output = actual_out, target_file = dep_exe) + ctx.actions.symlink(output = metadata_out, target_file = metadata_file) ctx.actions.write( output = out, - content = _windows_wrapper_content(actual_out.basename) if is_windows else _unix_wrapper_content(actual_out.basename), + content = _windows_wrapper_content(actual_out.basename, metadata_out.basename) if is_windows else _unix_wrapper_content(actual_out.basename, metadata_out.basename), is_executable = True, ) providers = [DefaultInfo( - files = depset([out, actual_out]), - runfiles = dep_runfiles.merge(ctx.runfiles(files = [actual_out])), + files = depset([out, actual_out, metadata_out]), + runfiles = dep_runfiles.merge(ctx.runfiles(files = [actual_out, metadata_out])), executable = out, )] if dep_run_environment: @@ -138,6 +150,12 @@ orch_go_test = rule( cfg = orch_transition, doc = "The underlying raw go_test target built with Orchestrion enabled.", ), + "metadata": attr.label( + mandatory = True, + allow_single_file = True, + cfg = orch_transition, + doc = "Bazel-owned target metadata copied next to emitted test payloads.", + ), "orchestrion_mode": attr.string( default = _ORCHESTRION_MODE_GENERAL, values = [ @@ -146,9 +164,17 @@ orch_go_test = rule( ], doc = "Internal Orchestrion mode forwarded to the raw go_test target.", ), + "test_optimization_enabled": attr.bool( + default = False, + doc = "Whether the selected generated metadata repository is enabled.", + ), "_allowlist_function_transition": attr.label( default = "@bazel_tools//tools/allowlists/function_transition_allowlist", ), + "_orchestrion_enabled": attr.label( + default = "@rules_go//go/private/orchestrion:enabled", + providers = [BuildSettingInfo], + ), "_windows_constraint": attr.label(default = "@platforms//os:windows"), }, test = True, diff --git a/modules/go/topt_go_orchestrion_repository.bzl b/modules/go/topt_go_orchestrion_repository.bzl new file mode 100644 index 00000000..e1445d24 --- /dev/null +++ b/modules/go/topt_go_orchestrion_repository.bzl @@ -0,0 +1,44 @@ +# Unless explicitly stated otherwise all files in this repository are licensed under +# the Apache 2.0 License. +# +# This product includes software developed at Datadog +# (https://www.datadoghq.com/) Copyright 2025-Present Datadog, Inc. + +"""Thin WORKSPACE wrapper around rules_go's public Orchestrion repository API.""" + +load( + "@rules_go//go:orchestrion_workspace.bzl", + "go_orchestrion_tool_repo", +) + +_DEFAULT_TOOL_REPO_NAME = "rules_go_orchestrion_tool" + +def _build_orchestrion_repo_call( + dd_trace_go_version = "", + dd_trace_go_versions = {}, + version = "", + log_timing = False): + """Build the fixed-name public rules_go repository call.""" + return { + "name": _DEFAULT_TOOL_REPO_NAME, + "dd_trace_go_version": dd_trace_go_version, + "dd_trace_go_versions": dd_trace_go_versions, + "enabled_by_env": True, + "version": version, + "log_timing": log_timing, + } + +def dd_topt_go_orchestrion_tool_repo( + dd_trace_go_version = "", + dd_trace_go_versions = {}, + version = "", + log_timing = False): + """Declare the real Orchestrion repository through rules_go's public API.""" + go_orchestrion_tool_repo(**_build_orchestrion_repo_call( + dd_trace_go_version = dd_trace_go_version, + dd_trace_go_versions = dd_trace_go_versions, + version = version, + log_timing = log_timing, + )) + +build_orchestrion_repo_call_for_tests = _build_orchestrion_repo_call diff --git a/modules/go/topt_go_test.bzl b/modules/go/topt_go_test.bzl index 069c153f..a9469c20 100644 --- a/modules/go/topt_go_test.bzl +++ b/modules/go/topt_go_test.bzl @@ -25,7 +25,8 @@ Notes: test_optimization_uploader.bzl) and run it via `bazel run` after tests. Macro design constraints: -- This macro creates a hidden raw `go_test` plus a public wrapper target. +- Enabled exports create a hidden raw `go_test` plus a public wrapper target. + Disabled exports create only the caller's public raw `go_test`. - It does not create upload targets and does not alter workspace-level upload behavior. - Runtime behavior must remain hermetic: tests write payloads to @@ -269,8 +270,9 @@ def dd_topt_go_test( **kwargs): """Define a Go test with Datadog Test Optimization support. - This macro creates a hidden raw go_test target plus a public - Orchestrion-enabled wrapper target. Payloads are written to Bazel's + For an enabled export, this macro creates a hidden raw go_test target plus a + public Orchestrion-enabled wrapper target. For a disabled export, it creates + only the caller's public raw go_test. Enabled payloads are written to Bazel's TEST_UNDECLARED_OUTPUTS_DIR and collected in bazel-testlogs//test.outputs/. After running tests, use a single workspace-level uploader target to upload @@ -326,13 +328,8 @@ def dd_topt_go_test( if topt_data == None or not _is_dict(topt_data): fail_with_prefix("dd_topt_go_test", "topt_data is required and must be the dict from @//:export.bzl (single-service) or the aggregator mapping") _validate_orchestrion_mode(orchestrion_mode) - test_binary_linker_optimization_requested = ( - orchestrion_mode == _ORCHESTRION_MODE_TEST_OPTIMIZATION and - enable_test_binary_linker_optimization - ) - test_binary_linker_optimization_applied = ( - _TEST_BINARY_LINKER_OPTIMIZATION_APPLIED if test_binary_linker_optimization_requested else False - ) + if go_test_rule == None: + fail_with_prefix("dd_topt_go_test", "go_test_rule override cannot be None") # Support both shapes: # 1) Single-service dict with keys: repo_name, labels, set, runtimes @@ -352,6 +349,20 @@ def dd_topt_go_test( selected_key = _resolve_topt_service_key(service_entries, topt_service) _svc = service_entries[selected_key] + if not bool(_svc.get("enabled", True)): + go_test_rule( + name = name, + **kwargs + ) + return + + test_binary_linker_optimization_requested = ( + orchestrion_mode == _ORCHESTRION_MODE_TEST_OPTIMIZATION and + enable_test_binary_linker_optimization + ) + test_binary_linker_optimization_applied = ( + _TEST_BINARY_LINKER_OPTIMIZATION_APPLIED if test_binary_linker_optimization_requested else False + ) wrapper_kwargs, raw_passthrough = split_test_wrapper_kwargs(kwargs) # ------------------------------------------------------------------ @@ -558,8 +569,9 @@ def dd_topt_go_test( # Signal to the library that payloads should be written to files # (TEST_UNDECLARED_OUTPUTS_DIR) regardless of caller input. "DD_TEST_OPTIMIZATION_PAYLOADS_IN_FILES": "true", - # The Orchestrion wrapper copies this file into test.outputs so the - # uploader can enrich payloads with target-specific Bazel metadata. + # Keep the target metadata basename available to the test runtime. The + # wrapper also receives the generated target directly and copies it + # into test.outputs for uploader enrichment. "DD_TEST_OPTIMIZATION_BAZEL_TARGET_METADATA_BASENAME": metadata_name + ".json", } if ci_visibility_enabled: @@ -570,9 +582,6 @@ def dd_topt_go_test( macro_name = "dd_topt_go_test", ) - if go_test_rule == None: - fail_with_prefix("dd_topt_go_test", "go_test_rule override cannot be None") - # Use the repository root when staged sources need repo-relative lookup. # Otherwise keep the package directory default to preserve existing tests. if "rundir" not in kwargs: @@ -602,6 +611,8 @@ def dd_topt_go_test( orch_go_test( name = name, actual = ":" + raw_name, + metadata = ":" + metadata_name, orchestrion_mode = orchestrion_mode, + test_optimization_enabled = bool(_svc.get("enabled", True)), **wrapper_kwargs ) diff --git a/modules/go/topt_go_workspace.bzl b/modules/go/topt_go_workspace.bzl new file mode 100644 index 00000000..49813080 --- /dev/null +++ b/modules/go/topt_go_workspace.bzl @@ -0,0 +1,139 @@ +# Unless explicitly stated otherwise all files in this repository are licensed under +# the Apache 2.0 License. +# +# This product includes software developed at Datadog +# (https://www.datadoghq.com/) Copyright 2025-Present Datadog, Inc. + +"""WORKSPACE metadata bootstrap for Go consumers.""" + +load( + "@datadog-rules-test-optimization//tools/core:common_utils.bzl", + "dedup_keys", + "sanitize_label_fragment", +) +load( + "@datadog-rules-test-optimization//tools/core:test_optimization_multi_sync.bzl", + "test_optimization_multi_aggregate", +) +load( + "@datadog-rules-test-optimization//tools/core:test_optimization_sync.bzl", + "test_optimization_sync", +) + +def _service_keys(services): + return dedup_keys([sanitize_label_fragment(service) for service in services]) + +def _build_go_workspace_sync_specs( + name, + runtime_version, + module_path, + service = None, + services = [], + enabled = True, + enabled_by_env = True, + runtime_arch = "", + out_dir = "", + http_connect_timeout_seconds = -1, + http_max_time_seconds = -1, + http_retry_attempts = -1, + http_retry_delay_seconds = -1, + http_execute_timeout_buffer_seconds = -1, + known_tests = True, + test_management = True, + flaky_tests = True, + require_git_metadata = False, + debug = False): + """Build metadata sync calls and the optional aggregate call.""" + if service and services: + fail("set either service or services, not both") + if not service and not services: + fail("one of service or services is required") + + service_values = [service] if service else services + keys = [] if service else _service_keys(services) + repo_names = [name] if service else ["%s_%s" % (name, key) for key in keys] + sync_specs = [] + for i in range(len(service_values)): + sync_specs.append({ + "name": repo_names[i], + "repo_name": repo_names[i], + "service": service_values[i], + "runtime_name": "go", + "runtime_version": runtime_version, + "runtime_arch": runtime_arch, + "runtime_module_path": module_path, + "out_dir": out_dir, + "enabled": enabled, + "enabled_by_env": enabled_by_env, + "http_connect_timeout_seconds": http_connect_timeout_seconds, + "http_max_time_seconds": http_max_time_seconds, + "http_retry_attempts": http_retry_attempts, + "http_retry_delay_seconds": http_retry_delay_seconds, + "http_execute_timeout_buffer_seconds": http_execute_timeout_buffer_seconds, + "known_tests": known_tests, + "test_management": test_management, + "flaky_tests": flaky_tests, + "require_git_metadata": require_git_metadata, + "debug": debug, + }) + aggregate_spec = None + if not service: + aggregate_spec = { + "name": name, + "service_keys": keys, + "repo_names": repo_names, + "debug": debug, + } + return { + "sync_specs": sync_specs, + "aggregate_spec": aggregate_spec, + } + +build_go_workspace_sync_specs_for_tests = _build_go_workspace_sync_specs + +def dd_topt_go_workspace_sync_repositories( + name, + runtime_version, + module_path = "", + service = None, + services = [], + enabled = True, + enabled_by_env = True, + runtime_arch = "", + out_dir = "", + http_connect_timeout_seconds = -1, + http_max_time_seconds = -1, + http_retry_attempts = -1, + http_retry_delay_seconds = -1, + http_execute_timeout_buffer_seconds = -1, + known_tests = True, + test_management = True, + flaky_tests = True, + require_git_metadata = False, + debug = False): + specs = _build_go_workspace_sync_specs( + name = name, + runtime_version = runtime_version, + module_path = module_path, + service = service, + services = services, + enabled = enabled, + enabled_by_env = enabled_by_env, + runtime_arch = runtime_arch, + out_dir = out_dir, + http_connect_timeout_seconds = http_connect_timeout_seconds, + http_max_time_seconds = http_max_time_seconds, + http_retry_attempts = http_retry_attempts, + http_retry_delay_seconds = http_retry_delay_seconds, + http_execute_timeout_buffer_seconds = http_execute_timeout_buffer_seconds, + known_tests = known_tests, + test_management = test_management, + flaky_tests = flaky_tests, + require_git_metadata = require_git_metadata, + debug = debug, + ) + for sync_spec in specs["sync_specs"]: + test_optimization_sync(**sync_spec) + aggregate_spec = specs["aggregate_spec"] + if aggregate_spec: + test_optimization_multi_aggregate(**aggregate_spec) diff --git a/modules/java/MODULE.bazel.lock b/modules/java/MODULE.bazel.lock index c602cdda..6ed3a69d 100644 --- a/modules/java/MODULE.bazel.lock +++ b/modules/java/MODULE.bazel.lock @@ -140,8 +140,8 @@ "moduleExtensions": { "//tests:example_stub_repo.bzl%example_stub_repo_extension": { "general": { - "bzlTransitiveDigest": "zC3aEdxLSUhPV6iwgCNk7hQr+1eO+rAQ5znCfGieX3g=", - "usagesDigest": "WUBevKMAOfvH9naco2wxzvnDbEMGelUuwm0wvsegGYc=", + "bzlTransitiveDigest": "8Z0dqG8n5A2E4sAbWxvij+vncAodaOVqDuvzF/sm6wE=", + "usagesDigest": "6NwGJ7RyhJOwtM7+EPNpuNyawB974PzmTqQARABaogw=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, "envVariables": {}, diff --git a/modules/nodejs/MODULE.bazel.lock b/modules/nodejs/MODULE.bazel.lock index ce7dabff..fb767431 100644 --- a/modules/nodejs/MODULE.bazel.lock +++ b/modules/nodejs/MODULE.bazel.lock @@ -140,8 +140,8 @@ "moduleExtensions": { "//tests:example_stub_repo.bzl%example_stub_repo_extension": { "general": { - "bzlTransitiveDigest": "zC3aEdxLSUhPV6iwgCNk7hQr+1eO+rAQ5znCfGieX3g=", - "usagesDigest": "irePOqLrnrDswqq6uNfKWfqwmNWA2wVLir9YmhV+HBw=", + "bzlTransitiveDigest": "8Z0dqG8n5A2E4sAbWxvij+vncAodaOVqDuvzF/sm6wE=", + "usagesDigest": "lqQSWgxAEY9YXf27tmAuYKvioDk6WYdJzSpQkWFpeTE=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, "envVariables": {}, diff --git a/modules/python/MODULE.bazel b/modules/python/MODULE.bazel index e3740a5a..297c1ef7 100644 --- a/modules/python/MODULE.bazel +++ b/modules/python/MODULE.bazel @@ -22,6 +22,12 @@ example_stub_repo = use_extension( ) example_stub_repo.example_stub_repo( name = "test_optimization_data", + labels = [ + "example_python_modules_python_tests", + "example_python_pkg", + "example_python_stub_tests", + "example_python_tests", + ], service_keys = [ "py_service", "ruby_service", diff --git a/modules/python/MODULE.bazel.lock b/modules/python/MODULE.bazel.lock index 3b4da633..ef5fb12a 100644 --- a/modules/python/MODULE.bazel.lock +++ b/modules/python/MODULE.bazel.lock @@ -148,8 +148,8 @@ "moduleExtensions": { "//tests:example_stub_repo.bzl%example_stub_repo_extension": { "general": { - "bzlTransitiveDigest": "zC3aEdxLSUhPV6iwgCNk7hQr+1eO+rAQ5znCfGieX3g=", - "usagesDigest": "Njje2Mz52TKjSVkVkgxLXaZa/9f/0VBc5vCkeOpAhDE=", + "bzlTransitiveDigest": "NHlcGDHChvBcvl50+UhxkmlFTJUEj9A2ARhtKx4kY+0=", + "usagesDigest": "UDiAhPtQ9KsYnDQ8zl34syCpr+iVmZjdd5stUk67Umk=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, "envVariables": {}, @@ -157,10 +157,16 @@ "test_optimization_data": { "repoRuleId": "@@datadog-rules-test-optimization+//tools/tests:example_stub_repo.bzl%example_stub_repo", "attributes": { + "enabled": true, "go_module_included": false, "go_module_path": "example.com/stub", "go_sanitized_module_path": "example_com_stub", - "labels": [], + "labels": [ + "example_python_modules_python_tests", + "example_python_pkg", + "example_python_stub_tests", + "example_python_tests" + ], "out_dir": ".testoptimization", "repo_alias": "test_optimization_data", "service_name": "stub-service", diff --git a/modules/python/tests/BUILD.bazel b/modules/python/tests/BUILD.bazel index 54b30b41..c96819d8 100644 --- a/modules/python/tests/BUILD.bazel +++ b/modules/python/tests/BUILD.bazel @@ -32,10 +32,16 @@ load( "py_macro_consumer_runner_with_main_target", "py_macro_default_rule_detection_target_rule", "py_macro_default_rule_detection_test", + "py_macro_disabled_consumer_runner_target", + "py_macro_disabled_consumer_runner_test", + "py_macro_disabled_managed_pytest_target", + "py_macro_disabled_managed_pytest_test", "py_macro_env_none_target", "py_macro_env_none_wiring_test", "py_macro_explicit_service_target", "py_macro_explicit_service_wiring_test", + "py_macro_fallback_payloads_target", + "py_macro_fallback_payloads_test", "py_macro_invalid_runner_mode_failure_test", "py_macro_invalid_runner_mode_target_rule", "py_macro_managed_pytest_kwargs_target", @@ -81,6 +87,8 @@ load( "selector_override_target", "selector_override_test", "selector_payload_fixture_targets", + "selector_prefixed_fallback_target", + "selector_prefixed_fallback_test", ) load( ":test_selection_utils.bzl", @@ -93,11 +101,13 @@ load( "build_module_labels_unsanitized_entry_failure_test", "build_module_labels_unsanitized_entry_target_rule", "build_module_labels_valid_test", + "build_python_fallback_identifier_test", "normalize_python_identifier_edge_cases_test", "normalize_user_data_handles_none_test", "normalize_user_data_invalid_type_failure_test", "normalize_user_data_invalid_type_target_rule", "py_stub_includes_manifest_in_files_test", + "resolve_python_selector_inputs_test", "resolve_topt_service_key_prefers_exact_then_sanitized_test", "select_module_group_name_test", "service_mapping_entries_filters_non_service_test", @@ -149,6 +159,18 @@ build_module_labels_valid_test( timeout = "short", ) +build_python_fallback_identifier_test( + name = "build_python_fallback_identifier_test", + size = "small", + timeout = "short", +) + +resolve_python_selector_inputs_test( + name = "resolve_python_selector_inputs_test", + size = "small", + timeout = "short", +) + py_stub_includes_manifest_in_files_test( name = "py_stub_includes_manifest_in_files_test", size = "small", @@ -227,6 +249,16 @@ selector_no_match_fallback_test( target_under_test = ":selector_no_match_fallback_target", ) +selector_prefixed_fallback_target( + name = "selector_prefixed_fallback_target", + tags = ["manual"], +) + +selector_prefixed_fallback_test( + name = "selector_prefixed_fallback_test", + target_under_test = ":selector_prefixed_fallback_target", +) + selector_include_disabled_target( name = "selector_include_disabled_target", tags = ["manual"], @@ -372,6 +404,26 @@ py_macro_managed_pytest_kwargs_test( target_under_test = ":py_macro_managed_pytest_kwargs_target__raw_python_test", ) +py_macro_disabled_consumer_runner_target( + name = "py_macro_disabled_consumer_runner_target", + tags = ["consumer_tag"], +) + +py_macro_disabled_consumer_runner_test( + name = "py_macro_disabled_consumer_runner_test", + target_under_test = ":py_macro_disabled_consumer_runner_target", +) + +py_macro_disabled_managed_pytest_target( + name = "py_macro_disabled_managed_pytest_target", + tags = ["consumer_tag"], +) + +py_macro_disabled_managed_pytest_test( + name = "py_macro_disabled_managed_pytest_test", + target_under_test = ":py_macro_disabled_managed_pytest_target", +) + py_macro_consumer_runner_no_rule_no_main_target_rule( name = "py_macro_consumer_runner_no_rule_no_main_target", tags = ["manual"], @@ -439,6 +491,16 @@ py_macro_public_wrapper_test( target_under_test = ":py_macro_single_service_target", ) +py_macro_fallback_payloads_target( + name = "py_macro_fallback_payloads_target", + tags = ["manual"], +) + +py_macro_fallback_payloads_test( + name = "py_macro_fallback_payloads_test", + target_under_test = ":py_macro_fallback_payloads_target_topt_payloads", +) + py_macro_multi_service_target( name = "py_macro_multi_service_target", tags = ["manual"], @@ -567,6 +629,7 @@ test_suite( ":build_module_labels_invalid_shape_failure_test", ":build_module_labels_unsanitized_entry_failure_test", ":build_module_labels_valid_test", + ":build_python_fallback_identifier_test", ":normalize_python_identifier_edge_cases_test", ":normalize_user_data_handles_none_test", ":normalize_user_data_invalid_type_failure_test", @@ -583,14 +646,18 @@ test_suite( ":py_macro_consumer_runner_validation_helpers_test", ":py_macro_consumer_runner_wiring_test", ":py_macro_default_rule_detection_test", + ":py_macro_disabled_consumer_runner_test", + ":py_macro_disabled_managed_pytest_test", ":py_macro_env_none_wiring_test", ":py_macro_explicit_service_wiring_test", + ":py_macro_fallback_payloads_test", ":py_macro_invalid_runner_mode_failure_test", ":py_macro_managed_pytest_kwargs_test", ":py_macro_multi_service_wiring_test", ":py_macro_select_inputs_wiring_test", ":py_macro_single_service_wiring_test", ":py_stub_includes_manifest_in_files_test", + ":resolve_python_selector_inputs_test", ":resolve_topt_service_key_missing_failure_test", ":resolve_topt_service_key_prefers_exact_then_sanitized_test", ":resolve_topt_service_key_unknown_failure_test", @@ -608,6 +675,7 @@ test_suite( ":selector_omits_flaky_tests_test", ":selector_override_miss_failure_test", ":selector_override_test", + ":selector_prefixed_fallback_test", ":service_mapping_entries_filters_non_service_test", ], ) diff --git a/modules/python/tests/test_macro.bzl b/modules/python/tests/test_macro.bzl index db065961..0a5f38b5 100644 --- a/modules/python/tests/test_macro.bzl +++ b/modules/python/tests/test_macro.bzl @@ -17,6 +17,7 @@ load( "validate_runner_mode_for_tests", ) load("@rules_python//python:py_test.bzl", _default_py_test = "py_test") +load("@test_optimization_data//:export.bzl", _stub_topt_data = "topt_data") ToptPyMacroCaptureInfo = provider( doc = "Captured arguments forwarded by dd_topt_py_test to py_test_rule.", @@ -257,7 +258,11 @@ def _single_service_topt_data(): "repo_name": "test_optimization_data", "service_name": "py-service", "manifest_path": ".testoptimization/manifest.txt", - "labels": [], + "labels": [ + "example_python_modules_python_tests", + "example_python_pkg", + "example_python_tests", + ], "set": {}, "runtimes": { "go": { @@ -278,6 +283,11 @@ def _single_service_topt_data(): }, } +def _disabled_single_service_topt_data(): + disabled = dict(_single_service_topt_data()) + disabled["enabled"] = False + return disabled + def _multi_service_topt_data(): selected = _single_service_topt_data() not_selected = dict(selected) @@ -322,6 +332,15 @@ def py_macro_env_none_target(name, tags = None): tags = tags, ) +def py_macro_fallback_payloads_target(name, tags = None): + """Exercise package-derived fallback against the shared dev stub export.""" + dd_topt_py_test( + name = name, + topt_data = _stub_topt_data, + py_test_rule = _py_test_capture_rule, + tags = tags, + ) + def py_macro_explicit_service_target(name, tags = None): dd_topt_py_test( name = name, @@ -479,6 +498,36 @@ def py_macro_managed_pytest_kwargs_target(name, tags = None): tags = tags, ) +def py_macro_disabled_consumer_runner_target(name, tags = None): + """Disabled sync preserves the consumer runner without instrumentation.""" + dd_topt_py_test( + name = name, + topt_data = _disabled_single_service_topt_data(), + py_test_rule = _py_test_kwargs_capture_macro, + runner_mode = "consumer_runner", + srcs = ["consumer_runner_main.py"], + data = [":test_macro.bzl"], + dd_requirements = ["pytest"], + env = select({ + "//conditions:default": { + "CUSTOM_ENV": "preserved", + "DD_CIVISIBILITY_ENABLED": "true", + }, + }), + args = ["-k", "consumer"], + tags = tags, + ) + +def py_macro_disabled_managed_pytest_target(name, tags = None): + """Disabled sync keeps the managed pytest runner but omits instrumentation.""" + dd_topt_py_test( + name = name, + topt_data = _disabled_single_service_topt_data(), + py_test_rule = _py_test_kwargs_capture_macro, + srcs = ["consumer_runner_main.py"], + tags = tags, + ) + def _py_macro_consumer_runner_no_rule_no_main_target_impl(_ctx): dd_topt_py_test( name = "should_not_be_created", @@ -669,6 +718,41 @@ def _py_macro_managed_pytest_kwargs_test_impl(ctx): ) return analysistest.end(env) +def _assert_no_test_optimization_wiring(env, captured): + asserts.equals(env, "false", captured.env.get("DD_CIVISIBILITY_ENABLED")) + asserts.equals(env, None, captured.env.get("DD_SERVICE")) + asserts.equals(env, None, captured.env.get("DD_TEST_OPTIMIZATION_MANIFEST_FILE")) + asserts.equals(env, None, captured.env.get("DD_TEST_OPTIMIZATION_PAYLOADS_IN_FILES")) + asserts.equals(env, None, captured.env.get("DD_TEST_OPTIMIZATION_BAZEL_TARGET_METADATA_BASENAME")) + for label in captured.data_labels: + asserts.false(env, "topt_payloads" in label) + asserts.false(env, ".testoptimization" in label) + +def _py_macro_disabled_consumer_runner_test_impl(ctx): + env = analysistest.begin(ctx) + captured = analysistest.target_under_test(env)[ToptPyKwargsCaptureInfo] + _assert_no_test_optimization_wiring(env, captured) + asserts.equals(env, "preserved", captured.env.get("CUSTOM_ENV")) + asserts.true(env, captured.saw_args) + asserts.false(env, captured.saw_imports) + asserts.false(env, captured.saw_main) + asserts.false(env, captured.saw_run_pytest) + asserts.equals(env, ["pytest"], captured.dd_requirements) + asserts.true(env, _has_label_suffix(captured.data_labels, ":test_macro.bzl")) + asserts.false(env, "manual" in captured.tags) + return analysistest.end(env) + +def _py_macro_disabled_managed_pytest_test_impl(ctx): + env = analysistest.begin(ctx) + captured = analysistest.target_under_test(env)[ToptPyKwargsCaptureInfo] + _assert_no_test_optimization_wiring(env, captured) + asserts.true(env, captured.saw_args) + asserts.true(env, captured.saw_imports) + asserts.true(env, captured.saw_main) + asserts.true(env, captured.saw_run_pytest) + asserts.false(env, "manual" in captured.tags) + return analysistest.end(env) + def _py_macro_consumer_runner_no_rule_no_main_failure_test_impl(ctx): env = analysistest.begin(ctx) asserts.expect_failure(env, "requires a consumer-owned Python test runner") @@ -724,6 +808,12 @@ py_macro_consumer_runner_select_env_test = analysistest.make( py_macro_managed_pytest_kwargs_test = analysistest.make( _py_macro_managed_pytest_kwargs_test_impl, ) +py_macro_disabled_consumer_runner_test = analysistest.make( + _py_macro_disabled_consumer_runner_test_impl, +) +py_macro_disabled_managed_pytest_test = analysistest.make( + _py_macro_disabled_managed_pytest_test_impl, +) py_macro_consumer_runner_no_rule_no_main_failure_test = analysistest.make( _py_macro_consumer_runner_no_rule_no_main_failure_test_impl, expect_failure = True, @@ -867,6 +957,31 @@ def _py_macro_public_wrapper_test_impl(ctx): asserts.equals(env, "1", run_env.get("CUSTOM_ENV")) return analysistest.end(env) +def _py_macro_fallback_payloads_test_impl(ctx): + env = analysistest.begin(ctx) + target = analysistest.target_under_test(env) + paths = [file.short_path for file in target[DefaultInfo].files.to_list()] + + expected_module_known_tests = False + full_bundle_known_tests = False + for path in paths: + if path.endswith("/.testoptimization/module_example_python_stub_tests/known_tests.json"): + expected_module_known_tests = True + if path.endswith("/.testoptimization/cache/http/known_tests.json"): + full_bundle_known_tests = True + + asserts.true( + env, + expected_module_known_tests, + msg = "macro-generated selector must expose module_example_python_stub_tests: %s" % paths, + ) + asserts.false( + env, + full_bundle_known_tests, + msg = "macro-generated selector must not fall back to the full known-tests bundle: %s" % paths, + ) + return analysistest.end(env) + def _resolve_topt_service_key_missing_target_impl(_ctx): resolve_topt_service_key_for_tests( { @@ -951,6 +1066,9 @@ py_macro_explicit_service_wiring_test = analysistest.make( py_macro_public_wrapper_test = analysistest.make( _py_macro_public_wrapper_test_impl, ) +py_macro_fallback_payloads_test = analysistest.make( + _py_macro_fallback_payloads_test_impl, +) resolve_topt_service_key_missing_failure_test = analysistest.make( _resolve_topt_service_key_missing_failure_test_impl, expect_failure = True, diff --git a/modules/python/tests/test_payloads_selector.bzl b/modules/python/tests/test_payloads_selector.bzl index 570981c6..ea6ec296 100644 --- a/modules/python/tests/test_payloads_selector.bzl +++ b/modules/python/tests/test_payloads_selector.bzl @@ -89,6 +89,10 @@ def selector_payload_fixture_targets(): name = "module_example_python_fallback_pkg", marker = "module:fallback", ) + _payload_marker( + name = "module_domains_ffe_apps_apis_query_validator_internal_validator_tests", + marker = "module:dd-source-prefixed-fallback", + ) _payload_marker( name = "module_custom_override", marker = "module:override", @@ -188,6 +192,20 @@ def selector_no_match_fallback_target(name, tags = None): tags = tags, ) +def selector_prefixed_fallback_target(name, tags = None): + """Select a dd-source-style module path without falling back to full files.""" + topt_py_payloads_selector( + name = name, + imports = [], + deps = [], + attribute_candidates = [], + fallback_identifier = "domains.ffe.apps.apis.query_validator.internal.validator.tests", + full_files = ":full_payload", + module_groups = [":module_domains_ffe_apps_apis_query_validator_internal_validator_tests"], + include_per_module = True, + tags = tags, + ) + def selector_include_disabled_target(name, tags = None): topt_py_payloads_selector( name = name, @@ -328,6 +346,12 @@ def _selector_no_match_fallback_test_impl(ctx): _assert_selected(env, target, "full_payload") return analysistest.end(env) +def _selector_prefixed_fallback_test_impl(ctx): + env = analysistest.begin(ctx) + target = analysistest.target_under_test(env) + _assert_selected(env, target, "module_domains_ffe_apps_apis_query_validator_internal_validator_tests") + return analysistest.end(env) + def _selector_include_disabled_test_impl(ctx): env = analysistest.begin(ctx) target = analysistest.target_under_test(env) @@ -382,6 +406,9 @@ selector_fallback_test = analysistest.make( selector_no_match_fallback_test = analysistest.make( _selector_no_match_fallback_test_impl, ) +selector_prefixed_fallback_test = analysistest.make( + _selector_prefixed_fallback_test_impl, +) selector_include_disabled_test = analysistest.make( _selector_include_disabled_test_impl, ) diff --git a/modules/python/tests/test_selection_utils.bzl b/modules/python/tests/test_selection_utils.bzl index 0bce5e3d..ecd27cd7 100644 --- a/modules/python/tests/test_selection_utils.bzl +++ b/modules/python/tests/test_selection_utils.bzl @@ -15,7 +15,9 @@ load( load( "@datadog-rules-test-optimization-python//:topt_py_test.bzl", "build_module_labels_for_tests", + "build_python_fallback_identifier_for_tests", "normalize_user_data_for_tests", + "resolve_python_selector_inputs_for_tests", "resolve_topt_service_key_for_tests", "service_mapping_entries_for_tests", ) @@ -83,6 +85,83 @@ def _normalize_python_identifier_edge_cases_test(ctx): asserts.equals(env, "", normalize_python_identifier_for_tests(None)) return unittest.end(env) +def _build_python_fallback_identifier_test(ctx): + env = unittest.begin(ctx) + asserts.equals( + env, + "example.project.app", + build_python_fallback_identifier_for_tests("app", {"module_path": "example.project"}), + ) + asserts.equals( + env, + "domains.ffe.apps.apis.query_validator.internal.validator.tests", + build_python_fallback_identifier_for_tests( + "domains/ffe/apps/apis/query_validator/internal/validator/tests", + {"module_path": "domains.ffe.apps.apis.query_validator"}, + ), + ) + asserts.equals( + env, + "domains.ffe.apps.apis.query_validator", + build_python_fallback_identifier_for_tests( + "domains/ffe/apps/apis/query_validator", + {"module_path": "domains.ffe.apps.apis.query_validator"}, + ), + ) + asserts.equals( + env, + "app", + build_python_fallback_identifier_for_tests("app", {}), + ) + asserts.equals( + env, + "example.project.app.tests", + build_python_fallback_identifier_for_tests("example//project\\app//tests", {"module_path": "example.project"}), + ) + return unittest.end(env) + +def _resolve_python_selector_inputs_test(ctx): + env = unittest.begin(ctx) + derived = resolve_python_selector_inputs_for_tests( + module_identifier = None, + imports_candidates = [], + deps_labels = [], + importpath_candidate = None, + module_path_candidate = None, + fallback_identifier = "example.project.tests", + module_groups = ["@repo//:module_example_project_tests"], + module_included = False, + ) + asserts.equals(env, "", derived["explicit_identifier"]) + asserts.equals(env, "example.project.tests", derived["fallback_identifier"]) + asserts.equals(env, True, derived["include_per_module"]) + + explicit = resolve_python_selector_inputs_for_tests( + module_identifier = "explicit.module", + imports_candidates = [], + deps_labels = [], + importpath_candidate = None, + module_path_candidate = None, + fallback_identifier = "fallback.module", + module_groups = ["@repo//:module_explicit_module"], + module_included = False, + ) + asserts.equals(env, "explicit.module", explicit["explicit_identifier"]) + asserts.equals(env, True, explicit["include_per_module"]) + + no_groups = resolve_python_selector_inputs_for_tests( + module_identifier = None, + imports_candidates = [], + deps_labels = [], + importpath_candidate = None, + module_path_candidate = None, + fallback_identifier = "example.project.tests", + module_groups = [], + module_included = False, + ) + asserts.equals(env, False, no_groups["include_per_module"]) + return unittest.end(env) + def _normalize_user_data_handles_none_test(ctx): env = unittest.begin(ctx) asserts.equals(env, [], normalize_user_data_for_tests(None)) @@ -202,6 +281,8 @@ service_mapping_entries_filters_non_service_test = unittest.make(_service_mappin resolve_topt_service_key_prefers_exact_then_sanitized_test = unittest.make(_resolve_topt_service_key_prefers_exact_then_sanitized_test) select_module_group_name_test = unittest.make(_select_module_group_name_test) normalize_python_identifier_edge_cases_test = unittest.make(_normalize_python_identifier_edge_cases_test) +build_python_fallback_identifier_test = unittest.make(_build_python_fallback_identifier_test) +resolve_python_selector_inputs_test = unittest.make(_resolve_python_selector_inputs_test) normalize_user_data_handles_none_test = unittest.make(_normalize_user_data_handles_none_test) build_module_labels_valid_test = unittest.make(_build_module_labels_valid_test) py_stub_includes_manifest_in_files_test = unittest.make(_py_stub_includes_manifest_in_files_test) diff --git a/modules/python/tools/dd_topt_py_bootstrap/main.py b/modules/python/tools/dd_topt_py_bootstrap/main.py index 6baf58d0..b2be07a9 100644 --- a/modules/python/tools/dd_topt_py_bootstrap/main.py +++ b/modules/python/tools/dd_topt_py_bootstrap/main.py @@ -164,6 +164,7 @@ def render_bazelrc_snippet(args: argparse.Namespace) -> str: lines = [ "# Datadog metadata is resolved during repository/module analysis.", "# These values are repo_env, not test_env, so tests do not receive secrets.", + f"common:{args.bazelrc_config} --repo_env=DD_TEST_OPTIMIZATION_ENABLED=1", ] lines.extend(f"common:{args.bazelrc_config} --repo_env={key}" for key in SYNC_REPO_ENV_KEYS) lines.append(f"test:{args.bazelrc_config} --remote_download_minimal") @@ -249,6 +250,7 @@ def render_workspace_snippet(args: argparse.Namespace) -> str: ' runtime_name = "python",', f" runtime_version = {_quote(args.runtime_version)},", f" runtime_module_path = {_quote(args.runtime_module_path)},", + " enabled_by_env = True,", ")", ] ) @@ -310,6 +312,7 @@ def render_bzlmod_snippet(args: argparse.Namespace) -> str: ' runtime_name = "python",', f" runtime_version = {_quote(args.runtime_version)},", f" runtime_module_path = {_quote(args.runtime_module_path)},", + " enabled_by_env = True,", ")", f"use_repo(test_optimization_sync, {_quote(args.sync_repo_name)})", ] @@ -346,9 +349,12 @@ def render_test_snippet(args: argparse.Namespace) -> str: if args.runner_mode == "consumer_runner": load_line = "# load(\"//path/to:python_rules.bzl\", \"your_py_test_rule\")" rule_ref = "your_py_test_rule" + module_identifier_line = "" if args.py_test_rule_load_label and args.py_test_rule_symbol: load_line = f"load({_quote(args.py_test_rule_load_label)}, {_quote(args.py_test_rule_symbol)})" rule_ref = args.py_test_rule_symbol + if args.module_identifier: + module_identifier_line = f" module_identifier = {_quote(args.module_identifier)},\n" return dedent( f""" load("@datadog-rules-test-optimization-python//:topt_py_test.bzl", "dd_topt_py_test") @@ -360,8 +366,7 @@ def render_test_snippet(args: argparse.Namespace) -> str: topt_data = topt_data, runner_mode = "consumer_runner", py_test_rule = {rule_ref}, - module_identifier = {_quote(args.module_identifier or args.runtime_module_path)}, - srcs = ["test_example.py"], + {module_identifier_line} srcs = ["test_example.py"], deps = [ requirement("ddtrace"), requirement("pytest"), diff --git a/modules/python/tools/dd_topt_py_bootstrap/main_test.py b/modules/python/tools/dd_topt_py_bootstrap/main_test.py index 43d5ec73..7d0f6364 100644 --- a/modules/python/tools/dd_topt_py_bootstrap/main_test.py +++ b/modules/python/tools/dd_topt_py_bootstrap/main_test.py @@ -45,6 +45,8 @@ def test_workspace_snippet_contains_helper(self) -> None: snippet = main.render_workspace_snippet(args) self.assertIn("datadog_python_test_optimization_workspace_repositories", snippet) self.assertIn("test_optimization_sync", snippet) + self.assertIn("enabled_by_env = True", snippet) + self.assertNotIn("rules_go", snippet) self.assertNotRegex(snippet, r"(?m)^ (load|# Declare|datadog_python_test_optimization_workspace_repositories|test_optimization_sync)") def test_bzlmod_snippet_contains_bazel_dep(self) -> None: @@ -54,6 +56,8 @@ def test_bzlmod_snippet_contains_bazel_dep(self) -> None: snippet = main.render_bzlmod_snippet(args) self.assertIn('bazel_dep(name = "datadog-rules-test-optimization"', snippet) self.assertIn('bazel_dep(name = "datadog-rules-test-optimization-python"', snippet) + self.assertIn("enabled_by_env = True", snippet) + self.assertNotIn("rules_go", snippet) self.assertNotRegex(snippet, r"(?m)^ (bazel_dep|archive_override|git_override|test_optimization_sync|use_repo)") def test_bzlmod_archive_snippet_emits_sha256_pin(self) -> None: @@ -86,7 +90,9 @@ def test_bazelrc_has_no_forbidden_test_env_or_fetch_salt(self) -> None: """Generated .bazelrc keeps secrets and git metadata out of test_env.""" args = _args() snippet = main.render_bazelrc_snippet(args) + self.assertIn("common:test-optimization --repo_env=DD_TEST_OPTIMIZATION_ENABLED=1", snippet) self.assertIn("common:test-optimization --repo_env=DD_API_KEY", snippet) + self.assertNotIn("orchestrion:enabled", snippet) self.assertIn("test:test-optimization --remote_download_minimal", snippet) self.assertIn("test:test-optimization --remote_download_regex=.*test[.]outputs.*", snippet) self.assertIn("test:test-optimization --zip_undeclared_test_outputs", snippet) @@ -182,7 +188,7 @@ def test_managed_pytest_snippet_lists_consumer_dependencies(self) -> None: self.assertIn('requirement("pytest")', snippet) def test_consumer_runner_snippet_contains_module_identifier(self) -> None: - """Consumer runner examples preserve module selection guidance.""" + """An explicit module-selection exception is preserved in output.""" args = _args( "--runner-mode=consumer_runner", "--module-identifier=example.python.app", @@ -194,6 +200,17 @@ def test_consumer_runner_snippet_contains_module_identifier(self) -> None: self.assertIn('module_identifier = "example.python.app"', snippet) self.assertIn("py_test_rule = dd_py_test", snippet) + def test_consumer_runner_snippet_uses_derived_module_identifier_by_default(self) -> None: + """The normal onboarding path does not require per-target identifiers.""" + args = _args( + "--runner-mode=consumer_runner", + "--py-test-rule-load-label=//tools:python.bzl", + "--py-test-rule-symbol=dd_py_test", + ) + snippet = main.render_test_snippet(args) + self.assertNotIn("module_identifier", snippet) + self.assertIn('srcs = ["test_example.py"]', snippet) + def test_write_modes_are_idempotent_and_preserve_user_content(self) -> None: """Managed block writes preserve unmanaged content and replace only generated content.""" with tempfile.TemporaryDirectory() as tmp: @@ -209,6 +226,28 @@ def test_write_modes_are_idempotent_and_preserve_user_content(self) -> None: self.assertIn("# user content", second) self.assertEqual(1, second.count(main.BEGIN_MARKER)) + def test_write_bazelrc_upgrades_an_older_managed_block(self) -> None: + """Re-running onboarding upgrades managed config without touching user content.""" + with tempfile.TemporaryDirectory() as tmp: + path = Path(tmp) / ".bazelrc" + path.write_text( + "# user content\n" + f"{main.BEGIN_MARKER}\n" + "common:test-optimization --repo_env=DD_API_KEY\n" + f"{main.END_MARKER}\n", + encoding="utf-8", + ) + args = _args("--write-bazelrc", f"--bazelrc-path={path}") + main._validate_args(args) + main.write_outputs(args) + content = path.read_text(encoding="utf-8") + self.assertIn("# user content", content) + self.assertEqual(1, content.count(main.BEGIN_MARKER)) + self.assertIn( + "common:test-optimization --repo_env=DD_TEST_OPTIMIZATION_ENABLED=1", + content, + ) + def test_write_targets_creates_parent_directories(self) -> None: """Target write mode creates lightweight packages on demand.""" with tempfile.TemporaryDirectory() as tmp: diff --git a/modules/python/topt_py_test.bzl b/modules/python/topt_py_test.bzl index fef4d0ff..c0e79c40 100644 --- a/modules/python/topt_py_test.bzl +++ b/modules/python/topt_py_test.bzl @@ -69,12 +69,22 @@ def _build_module_labels(sync_repo_name, labels): build_module_labels_for_tests = _build_module_labels +def _normalize_python_fallback_part(value): + """Normalize a workspace package or runtime module path to dotted form.""" + dotted = (value or "").replace("\\", ".").replace("/", ".") + return ".".join([part for part in dotted.split(".") if part]) + def _build_python_fallback_identifier(package_path, runtime_info): - pkg_dotted = (package_path or "").replace("/", ".") - module_path = ((runtime_info or {}).get("module_path") or "") - if module_path: - return (module_path + "." + pkg_dotted) if pkg_dotted else module_path - return pkg_dotted + """Build a generic prefix-aware Python module fallback identifier.""" + package_dotted = _normalize_python_fallback_part(package_path) + module_path = _normalize_python_fallback_part((runtime_info or {}).get("module_path")) + if not module_path: + return package_dotted + if not package_dotted: + return module_path + if package_dotted == module_path or package_dotted.startswith(module_path + "."): + return package_dotted + return module_path + "." + package_dotted def _has_non_empty_value(value): """Return True when a macro input is present and materially non-empty.""" @@ -86,6 +96,40 @@ def _has_non_empty_value(value): return len(value) > 0 return True +def _resolve_python_selector_inputs( + module_identifier, + imports_candidates, + deps_labels, + importpath_candidate, + module_path_candidate, + fallback_identifier, + module_groups, + module_included): + """Resolve selector inputs once so production and tests share the contract.""" + module_groups = module_groups or [] + uses_explicit_inference = ( + _has_non_empty_value(module_identifier) or + _has_non_empty_value(imports_candidates) or + _has_non_empty_value(deps_labels) or + _has_non_empty_value(importpath_candidate) or + _has_non_empty_value(module_path_candidate) + ) + uses_derived_fallback = _has_non_empty_value(fallback_identifier) and len(module_groups) > 0 + if uses_explicit_inference or uses_derived_fallback: + include_per_module = True + elif module_included != None: + include_per_module = bool(module_included) + else: + include_per_module = len(module_groups) > 0 + return { + "explicit_identifier": module_identifier or "", + "fallback_identifier": fallback_identifier or "", + "include_per_module": include_per_module, + } + +build_python_fallback_identifier_for_tests = _build_python_fallback_identifier +resolve_python_selector_inputs_for_tests = _resolve_python_selector_inputs + def _is_default_py_test_rule(py_test_rule): """Return True when a py_test_rule value is the rules_python base py_test macro.""" return py_test_rule == _default_py_test @@ -118,6 +162,25 @@ def _validate_consumer_runner_inputs(py_test_rule_was_explicit, py_test_rule_is_ "for the built-in pytest runner.", ) +def _define_uninstrumented_py_test(name, py_test_rule, runner_mode, kwargs): + """Define the consumer test without Datadog wiring when sync is disabled.""" + test_kwargs = dict(kwargs) + test_kwargs["env"] = _merge_user_env( + test_kwargs.get("env"), + {"DD_CIVISIBILITY_ENABLED": "false"}, + macro_name = "dd_topt_py_test", + ) + if runner_mode == _RUNNER_MODE_MANAGED_PYTEST and test_kwargs.get("main") == None: + pkg_path = native.package_name() + test_kwargs["srcs"] = _append_data_dependencies(test_kwargs.get("srcs"), [_RUN_PYTEST]) + test_kwargs["main"] = _RUN_PYTEST + if "args" not in test_kwargs: + test_kwargs["args"] = [pkg_path] if pkg_path else [] + if "imports" not in test_kwargs: + test_kwargs["imports"] = [pkg_path] if pkg_path else [] + test_kwargs["name"] = name + py_test_rule(**test_kwargs) + # Public aliases for unit tests. validate_runner_mode_for_tests = _validate_runner_mode validate_consumer_runner_inputs_for_tests = _validate_consumer_runner_inputs @@ -158,6 +221,20 @@ def dd_topt_py_test( py_test_rule_is_default = _is_default_py_test_rule(py_test_rule) _svc = _select_service_entry_or_fail(topt_data, topt_service) + if runner_mode == _RUNNER_MODE_CONSUMER_RUNNER: + _validate_consumer_runner_inputs( + py_test_rule_was_explicit, + py_test_rule_is_default, + kwargs.get("main"), + ) + + # A disabled sync repository exports the same schema with enabled = False. + # Keep the consumer's test runnable, but do not create selectors, wrappers, + # metadata targets, Datadog env, or payload-producing instrumentation. + if not _svc.get("enabled", True): + _define_uninstrumented_py_test(name, py_test_rule, runner_mode, kwargs) + return + wrapper_kwargs, raw_passthrough = split_test_wrapper_kwargs(kwargs) user_data = kwargs.pop("data", None) @@ -178,9 +255,6 @@ def dd_topt_py_test( user_srcs = kwargs.pop("srcs", None) user_main = kwargs.pop("main", None) - if runner_mode == _RUNNER_MODE_CONSUMER_RUNNER: - _validate_consumer_runner_inputs(py_test_rule_was_explicit, py_test_rule_is_default, user_main) - # args is a wrapper-only attr; split_test_wrapper_kwargs already moved it to wrapper_kwargs. user_args = wrapper_kwargs.pop("args", None) @@ -195,25 +269,19 @@ def dd_topt_py_test( if type(module_path_candidate) == type("") and module_path_candidate: attribute_candidates.append(module_path_candidate) - uses_inference = ( - _has_non_empty_value(module_identifier) or - _has_non_empty_value(imports_candidates) or - _has_non_empty_value(deps_labels) or - _has_non_empty_value(importpath_candidate) or - _has_non_empty_value(module_path_candidate) - ) - if uses_inference: - include_per_module_files = True - else: - module_included = _python.get("module_included") if _is_dict(_python) else None - if module_included != None: - include_per_module_files = bool(module_included) - else: - include_per_module_files = bool(_svc.get("labels")) - files_label = "@%s//:test_optimization_files" % sync_repo_name module_labels = _build_module_labels(sync_repo_name, _svc.get("labels")) fallback_identifier = _build_python_fallback_identifier(native.package_name(), _python) + selector_inputs = _resolve_python_selector_inputs( + module_identifier = module_identifier, + imports_candidates = imports_candidates, + deps_labels = deps_labels, + importpath_candidate = importpath_candidate, + module_path_candidate = module_path_candidate, + fallback_identifier = fallback_identifier, + module_groups = module_labels, + module_included = _python.get("module_included") if _is_dict(_python) else None, + ) selector_name = name + "_topt_payloads" metadata_name = name + "_topt_bazel_metadata" @@ -222,11 +290,11 @@ def dd_topt_py_test( deps = deps_labels, imports = imports_candidates, attribute_candidates = attribute_candidates, - explicit_identifier = module_identifier or "", - fallback_identifier = fallback_identifier, + explicit_identifier = selector_inputs["explicit_identifier"], + fallback_identifier = selector_inputs["fallback_identifier"], full_files = files_label, module_groups = module_labels, - include_per_module = include_per_module_files, + include_per_module = selector_inputs["include_per_module"], module_label_override = module_label_override, importpath = importpath_candidate if importpath_candidate != None else "", module_path = module_path_candidate if module_path_candidate != None else "", diff --git a/modules/ruby/MODULE.bazel.lock b/modules/ruby/MODULE.bazel.lock index 7299adf2..b9ec6196 100644 --- a/modules/ruby/MODULE.bazel.lock +++ b/modules/ruby/MODULE.bazel.lock @@ -140,8 +140,8 @@ "moduleExtensions": { "//tests:example_stub_repo.bzl%example_stub_repo_extension": { "general": { - "bzlTransitiveDigest": "zC3aEdxLSUhPV6iwgCNk7hQr+1eO+rAQ5znCfGieX3g=", - "usagesDigest": "gJmZLd6nd/xMP9rApCY4mVIpVZPOzoIiqCGWDFbM4VA=", + "bzlTransitiveDigest": "8Z0dqG8n5A2E4sAbWxvij+vncAodaOVqDuvzF/sm6wE=", + "usagesDigest": "D5EtmFTCTKSRH5kAymUIV/dmXm5DQEi4R3poqsT00uw=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, "envVariables": {}, diff --git a/third_party/rgo/v0_60_0/base.CHANGED_FILES.md b/third_party/rgo/v0_60_0/base.CHANGED_FILES.md index f11018e0..f8068104 100644 --- a/third_party/rgo/v0_60_0/base.CHANGED_FILES.md +++ b/third_party/rgo/v0_60_0/base.CHANGED_FILES.md @@ -12,8 +12,8 @@ This file is generated. Do not edit by hand. ## Summary -- Total changed paths: `52` -- Modified files: `28` +- Total changed paths: `54` +- Modified files: `30` - Added files: `24` - Removed files: `0` @@ -24,11 +24,13 @@ This file is generated. Do not edit by hand. - `docs/doc_helpers.bzl` - `go/extensions.bzl` - `go/private/BUILD.bazel` +- `go/private/actions/BUILD.bazel` - `go/private/actions/archive.bzl` - `go/private/actions/compilepkg.bzl` - `go/private/actions/link.bzl` - `go/private/actions/stdlib.bzl` - `go/private/context.bzl` +- `go/private/repositories.bzl` - `go/private/rules/library.bzl` - `go/private/rules/stdlib.bzl` - `go/private/rules/test.bzl` diff --git a/third_party/rgo/v0_60_0/base/go/orchestrion_workspace.bzl b/third_party/rgo/v0_60_0/base/go/orchestrion_workspace.bzl index 8d85b877..f1d725a2 100644 --- a/third_party/rgo/v0_60_0/base/go/orchestrion_workspace.bzl +++ b/third_party/rgo/v0_60_0/base/go/orchestrion_workspace.bzl @@ -13,6 +13,7 @@ def go_orchestrion_tool_repo( version = "", dd_trace_go_version = "", dd_trace_go_versions = None, + enabled_by_env = False, log_timing = False): """Create the `rules_go_orchestrion_tool` repository in WORKSPACE mode. @@ -25,6 +26,9 @@ def go_orchestrion_tool_repo( target module when instrumentation is enabled. dd_trace_go_versions: Optional per-module dd-trace-go version mapping. Mutually exclusive with `dd_trace_go_version`. + enabled_by_env: Gate repository materialization on the Test Optimization + repository environment. Generic Orchestrion callers should keep the + default. log_timing: Emit structured bootstrap timing probes while building the Orchestrion tool repository. """ @@ -50,5 +54,6 @@ def go_orchestrion_tool_repo( version = version, dd_trace_go_version = dd_trace_go_version, dd_trace_go_versions = dd_trace_go_versions, + enabled_by_env = enabled_by_env, log_timing = log_timing, ) diff --git a/third_party/rgo/v0_60_0/base/go/private/actions/BUILD.bazel b/third_party/rgo/v0_60_0/base/go/private/actions/BUILD.bazel index 3bd3d155..439d4f21 100644 --- a/third_party/rgo/v0_60_0/base/go/private/actions/BUILD.bazel +++ b/third_party/rgo/v0_60_0/base/go/private/actions/BUILD.bazel @@ -43,6 +43,7 @@ bzl_library( deps = [ ":utils", "//go/private:mode", + "//go/private/orchestrion:pin_files", "@bazel_skylib//lib:shell", ], ) @@ -54,6 +55,7 @@ bzl_library( deps = [ "//go/private:common", "//go/private:mode", + "//go/private/orchestrion:pin_files", "//go/private:rpath", "@bazel_skylib//lib:collections", ], @@ -66,6 +68,7 @@ bzl_library( deps = [ ":utils", "//go/private:mode", + "//go/private/orchestrion:pin_files", "//go/private:providers", "//go/private:sdk", ], diff --git a/third_party/rgo/v0_60_0/base/go/private/orchestrion/BUILD b/third_party/rgo/v0_60_0/base/go/private/orchestrion/BUILD index 488d4adf..31a01c33 100644 --- a/third_party/rgo/v0_60_0/base/go/private/orchestrion/BUILD +++ b/third_party/rgo/v0_60_0/base/go/private/orchestrion/BUILD @@ -1,3 +1,4 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") load("@bazel_skylib//rules:common_settings.bzl", "bool_flag", "string_flag") filegroup( @@ -13,6 +14,12 @@ filegroup( visibility = ["//visibility:public"], ) +bzl_library( + name = "pin_files", + srcs = ["pin_files.bzl"], + visibility = ["//go:__subpackages__"], +) + bool_flag( name = "enabled", build_setting_default = False, @@ -29,37 +36,80 @@ string_flag( visibility = ["//visibility:public"], ) -# Proxy target for the orchestrion tool binary. -# This always points to the rules_go_orchestrion_tool repo. -# The repo provides an empty filegroup by default, or the actual orchestrion -# binary when configured via module extension. -# go_context_data checks the :enabled flag to determine whether to use this. +config_setting( + name = "enabled_config", + flag_values = {":enabled": "true"}, +) + +filegroup( + name = "disabled_tool_binary", + srcs = [], +) + +filegroup( + name = "disabled_dd_trace_go_version_file", + srcs = [], +) + +filegroup( + name = "disabled_dd_trace_go_module_proxy_files", + srcs = [], +) + +filegroup( + name = "disabled_dd_trace_go_module_proxy_root_marker", + srcs = [], +) + +filegroup( + name = "disabled_orchestrion_tool_version_file", + srcs = [], +) + +# Stable Orchestrion aliases select package-local empty targets by default and +# only reference the real tool repository when the public :enabled flag is set. +# go_context_data also checks :enabled before consuming these files. alias( name = "tool_binary", - actual = "@rules_go_orchestrion_tool//:orchestrion", + actual = select({ + ":enabled_config": "@rules_go_orchestrion_tool//:orchestrion", + "//conditions:default": ":disabled_tool_binary", + }), visibility = ["//visibility:public"], ) alias( name = "dd_trace_go_version_file", - actual = "@rules_go_orchestrion_tool//:dd_trace_go_version_file", + actual = select({ + ":enabled_config": "@rules_go_orchestrion_tool//:dd_trace_go_version_file", + "//conditions:default": ":disabled_dd_trace_go_version_file", + }), visibility = ["//visibility:public"], ) alias( name = "dd_trace_go_module_proxy_files", - actual = "@rules_go_orchestrion_tool//:dd_trace_go_module_proxy_files", + actual = select({ + ":enabled_config": "@rules_go_orchestrion_tool//:dd_trace_go_module_proxy_files", + "//conditions:default": ":disabled_dd_trace_go_module_proxy_files", + }), visibility = ["//visibility:public"], ) alias( name = "dd_trace_go_module_proxy_root_marker", - actual = "@rules_go_orchestrion_tool//:dd_trace_go_module_proxy_root_marker", + actual = select({ + ":enabled_config": "@rules_go_orchestrion_tool//:dd_trace_go_module_proxy_root_marker", + "//conditions:default": ":disabled_dd_trace_go_module_proxy_root_marker", + }), visibility = ["//visibility:public"], ) alias( name = "orchestrion_tool_version_file", - actual = "@rules_go_orchestrion_tool//:orchestrion_tool_version_file", + actual = select({ + ":enabled_config": "@rules_go_orchestrion_tool//:orchestrion_tool_version_file", + "//conditions:default": ":disabled_orchestrion_tool_version_file", + }), visibility = ["//visibility:public"], ) diff --git a/third_party/rgo/v0_60_0/base/go/private/orchestrion/extensions.bzl b/third_party/rgo/v0_60_0/base/go/private/orchestrion/extensions.bzl index 05652331..42702622 100644 --- a/third_party/rgo/v0_60_0/base/go/private/orchestrion/extensions.bzl +++ b/third_party/rgo/v0_60_0/base/go/private/orchestrion/extensions.bzl @@ -1024,8 +1024,55 @@ orchestrion_extension_test_helpers = struct( powershell_single_quoted_literal = _powershell_single_quoted_literal, ) +_TEST_OPTIMIZATION_ENABLED_ENV = "DD_TEST_OPTIMIZATION_ENABLED" +_TEST_OPTIMIZATION_ENABLED_VALUES = ["1", "true", "yes", "on"] + +def _orchestrion_repository_enabled(ctx): + if not ctx.attr.enabled_by_env: + return True + value = ctx.os.environ.get(_TEST_OPTIMIZATION_ENABLED_ENV, "") + return value.strip().lower() in _TEST_OPTIMIZATION_ENABLED_VALUES + +def _write_empty_orchestrion_repository(ctx): + ctx.file("BUILD.bazel", """# Generated by rules_go orchestrion extension +# Orchestrion is disabled +filegroup( + name = "orchestrion", + srcs = [], + visibility = ["//visibility:public"], +) + +filegroup( + name = "dd_trace_go_version_file", + srcs = [], + visibility = ["//visibility:public"], +) + +filegroup( + name = "dd_trace_go_module_proxy_files", + srcs = [], + visibility = ["//visibility:public"], +) + +filegroup( + name = "dd_trace_go_module_proxy_root_marker", + srcs = [], + visibility = ["//visibility:public"], +) + +filegroup( + name = "orchestrion_tool_version_file", + srcs = [], + visibility = ["//visibility:public"], +) +""") + def _orchestrion_build_impl(ctx): """Build orchestrion from source.""" + if not _orchestrion_repository_enabled(ctx): + _write_empty_orchestrion_repository(ctx) + return + total_start_ms = _probe_now_ms(ctx) version = ctx.attr.version go_path = _find_go_binary(ctx) @@ -1311,44 +1358,15 @@ _orchestrion_build = repository_rule( "version": attr.string(mandatory = True, doc = "Orchestrion version to build"), "dd_trace_go_version": attr.string(default = "", doc = "dd-trace-go version to validate against the target module for Orchestrion-backed instrumentation"), "dd_trace_go_versions": attr.string_dict(doc = "Per-module dd-trace-go versions to validate against the target module for Orchestrion-backed instrumentation"), + "enabled_by_env": attr.bool(default = False, doc = "Gate repository materialization on the Test Optimization repository environment"), "log_timing": attr.bool(default = False, doc = "Emit structured timing probes while building Orchestrion"), }, + environ = [_TEST_OPTIMIZATION_ENABLED_ENV], ) def _orchestrion_empty_impl(ctx): """Create an empty placeholder repo.""" - ctx.file("BUILD.bazel", """# Generated by rules_go orchestrion extension -# No orchestrion configured -filegroup( - name = "orchestrion", - srcs = [], - visibility = ["//visibility:public"], -) - -filegroup( - name = "dd_trace_go_version_file", - srcs = [], - visibility = ["//visibility:public"], -) - -filegroup( - name = "dd_trace_go_module_proxy_files", - srcs = [], - visibility = ["//visibility:public"], -) - -filegroup( - name = "dd_trace_go_module_proxy_root_marker", - srcs = [], - visibility = ["//visibility:public"], -) - -filegroup( - name = "orchestrion_tool_version_file", - srcs = [], - visibility = ["//visibility:public"], -) -""") + _write_empty_orchestrion_repository(ctx) _orchestrion_empty = repository_rule( implementation = _orchestrion_empty_impl, @@ -1365,6 +1383,7 @@ def _orchestrion_ext_impl(module_ctx): version = "" dd_trace_go_version = "" dd_trace_go_versions = {} + enabled_by_env = True log_timing = False for mod in module_ctx.modules: for from_source in mod.tags.from_source: @@ -1372,6 +1391,7 @@ def _orchestrion_ext_impl(module_ctx): if from_source.dd_trace_go_version and from_source.dd_trace_go_versions: fail("dd_trace_go_version and dd_trace_go_versions cannot both be set in orchestrion.from_source()") version = from_source.version + enabled_by_env = from_source.enabled_by_env log_timing = from_source.log_timing if from_source.dd_trace_go_version: dd_trace_go_version = from_source.dd_trace_go_version @@ -1387,6 +1407,7 @@ def _orchestrion_ext_impl(module_ctx): version = version, dd_trace_go_version = dd_trace_go_version, dd_trace_go_versions = dd_trace_go_versions, + enabled_by_env = enabled_by_env, log_timing = log_timing, ) else: @@ -1407,6 +1428,10 @@ _from_source = tag_class( "dd_trace_go_versions": attr.string_dict( doc = "Per-module dd-trace-go versions to validate against the target module for Orchestrion-backed instrumentation.", ), + "enabled_by_env": attr.bool( + default = True, + doc = "Gate repository materialization on the Test Optimization repository environment.", + ), "log_timing": attr.bool( default = False, doc = "Emit structured timing probes while building the Orchestrion tool repository.", diff --git a/third_party/rgo/v0_60_0/base/go/private/repositories.bzl b/third_party/rgo/v0_60_0/base/go/private/repositories.bzl index 94adead5..27895206 100644 --- a/third_party/rgo/v0_60_0/base/go/private/repositories.bzl +++ b/third_party/rgo/v0_60_0/base/go/private/repositories.bzl @@ -17,6 +17,7 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") load("//go/private:common.bzl", "MINIMUM_BAZEL_VERSION") load("//go/private:nogo.bzl", "DEFAULT_NOGO", "go_register_nogo") +load("//go/private/orchestrion:extensions.bzl", "orchestrion_empty_repository") load("//go/private:polyfill_bazel_features.bzl", "polyfill_bazel_features") load("//go/private/skylib/lib:versions.bzl", "versions") load("//proto:gogo.bzl", "gogo_special_proto") @@ -39,6 +40,15 @@ def go_rules_dependencies(force = False): if getattr(native, "bazel_version", None): versions.check(MINIMUM_BAZEL_VERSION, bazel_version = native.bazel_version) + # Keep the stable Orchestrion repository mapping available to ordinary + # WORKSPACE consumers. Test Optimization consumers declare the real tool + # repository before calling go_rules_dependencies(), so this fallback does + # not replace or fetch it. + _maybe( + orchestrion_empty_repository, + name = "rules_go_orchestrion_tool", + ) + if force: wrapper = _always else: diff --git a/third_party/rgo/v0_61_1/base.CHANGED_FILES.md b/third_party/rgo/v0_61_1/base.CHANGED_FILES.md index cab689be..bed6af76 100644 --- a/third_party/rgo/v0_61_1/base.CHANGED_FILES.md +++ b/third_party/rgo/v0_61_1/base.CHANGED_FILES.md @@ -12,8 +12,8 @@ This file is generated. Do not edit by hand. ## Summary -- Total changed paths: `53` -- Modified files: `29` +- Total changed paths: `54` +- Modified files: `30` - Added files: `24` - Removed files: `0` @@ -30,6 +30,7 @@ This file is generated. Do not edit by hand. - `go/private/actions/link.bzl` - `go/private/actions/stdlib.bzl` - `go/private/context.bzl` +- `go/private/repositories.bzl` - `go/private/rules/library.bzl` - `go/private/rules/stdlib.bzl` - `go/private/rules/test.bzl` diff --git a/third_party/rgo/v0_61_1/base/go/orchestrion_workspace.bzl b/third_party/rgo/v0_61_1/base/go/orchestrion_workspace.bzl index 8d85b877..f1d725a2 100644 --- a/third_party/rgo/v0_61_1/base/go/orchestrion_workspace.bzl +++ b/third_party/rgo/v0_61_1/base/go/orchestrion_workspace.bzl @@ -13,6 +13,7 @@ def go_orchestrion_tool_repo( version = "", dd_trace_go_version = "", dd_trace_go_versions = None, + enabled_by_env = False, log_timing = False): """Create the `rules_go_orchestrion_tool` repository in WORKSPACE mode. @@ -25,6 +26,9 @@ def go_orchestrion_tool_repo( target module when instrumentation is enabled. dd_trace_go_versions: Optional per-module dd-trace-go version mapping. Mutually exclusive with `dd_trace_go_version`. + enabled_by_env: Gate repository materialization on the Test Optimization + repository environment. Generic Orchestrion callers should keep the + default. log_timing: Emit structured bootstrap timing probes while building the Orchestrion tool repository. """ @@ -50,5 +54,6 @@ def go_orchestrion_tool_repo( version = version, dd_trace_go_version = dd_trace_go_version, dd_trace_go_versions = dd_trace_go_versions, + enabled_by_env = enabled_by_env, log_timing = log_timing, ) diff --git a/third_party/rgo/v0_61_1/base/go/private/orchestrion/BUILD b/third_party/rgo/v0_61_1/base/go/private/orchestrion/BUILD index 488d4adf..177eeaf2 100644 --- a/third_party/rgo/v0_61_1/base/go/private/orchestrion/BUILD +++ b/third_party/rgo/v0_61_1/base/go/private/orchestrion/BUILD @@ -29,37 +29,80 @@ string_flag( visibility = ["//visibility:public"], ) -# Proxy target for the orchestrion tool binary. -# This always points to the rules_go_orchestrion_tool repo. -# The repo provides an empty filegroup by default, or the actual orchestrion -# binary when configured via module extension. -# go_context_data checks the :enabled flag to determine whether to use this. +config_setting( + name = "enabled_config", + flag_values = {":enabled": "true"}, +) + +filegroup( + name = "disabled_tool_binary", + srcs = [], +) + +filegroup( + name = "disabled_dd_trace_go_version_file", + srcs = [], +) + +filegroup( + name = "disabled_dd_trace_go_module_proxy_files", + srcs = [], +) + +filegroup( + name = "disabled_dd_trace_go_module_proxy_root_marker", + srcs = [], +) + +filegroup( + name = "disabled_orchestrion_tool_version_file", + srcs = [], +) + +# Stable Orchestrion aliases select package-local empty targets by default and +# only reference the real tool repository when the public :enabled flag is set. +# go_context_data also checks :enabled before consuming these files. alias( name = "tool_binary", - actual = "@rules_go_orchestrion_tool//:orchestrion", + actual = select({ + ":enabled_config": "@rules_go_orchestrion_tool//:orchestrion", + "//conditions:default": ":disabled_tool_binary", + }), visibility = ["//visibility:public"], ) alias( name = "dd_trace_go_version_file", - actual = "@rules_go_orchestrion_tool//:dd_trace_go_version_file", + actual = select({ + ":enabled_config": "@rules_go_orchestrion_tool//:dd_trace_go_version_file", + "//conditions:default": ":disabled_dd_trace_go_version_file", + }), visibility = ["//visibility:public"], ) alias( name = "dd_trace_go_module_proxy_files", - actual = "@rules_go_orchestrion_tool//:dd_trace_go_module_proxy_files", + actual = select({ + ":enabled_config": "@rules_go_orchestrion_tool//:dd_trace_go_module_proxy_files", + "//conditions:default": ":disabled_dd_trace_go_module_proxy_files", + }), visibility = ["//visibility:public"], ) alias( name = "dd_trace_go_module_proxy_root_marker", - actual = "@rules_go_orchestrion_tool//:dd_trace_go_module_proxy_root_marker", + actual = select({ + ":enabled_config": "@rules_go_orchestrion_tool//:dd_trace_go_module_proxy_root_marker", + "//conditions:default": ":disabled_dd_trace_go_module_proxy_root_marker", + }), visibility = ["//visibility:public"], ) alias( name = "orchestrion_tool_version_file", - actual = "@rules_go_orchestrion_tool//:orchestrion_tool_version_file", + actual = select({ + ":enabled_config": "@rules_go_orchestrion_tool//:orchestrion_tool_version_file", + "//conditions:default": ":disabled_orchestrion_tool_version_file", + }), visibility = ["//visibility:public"], ) diff --git a/third_party/rgo/v0_61_1/base/go/private/orchestrion/extensions.bzl b/third_party/rgo/v0_61_1/base/go/private/orchestrion/extensions.bzl index 05652331..42702622 100644 --- a/third_party/rgo/v0_61_1/base/go/private/orchestrion/extensions.bzl +++ b/third_party/rgo/v0_61_1/base/go/private/orchestrion/extensions.bzl @@ -1024,8 +1024,55 @@ orchestrion_extension_test_helpers = struct( powershell_single_quoted_literal = _powershell_single_quoted_literal, ) +_TEST_OPTIMIZATION_ENABLED_ENV = "DD_TEST_OPTIMIZATION_ENABLED" +_TEST_OPTIMIZATION_ENABLED_VALUES = ["1", "true", "yes", "on"] + +def _orchestrion_repository_enabled(ctx): + if not ctx.attr.enabled_by_env: + return True + value = ctx.os.environ.get(_TEST_OPTIMIZATION_ENABLED_ENV, "") + return value.strip().lower() in _TEST_OPTIMIZATION_ENABLED_VALUES + +def _write_empty_orchestrion_repository(ctx): + ctx.file("BUILD.bazel", """# Generated by rules_go orchestrion extension +# Orchestrion is disabled +filegroup( + name = "orchestrion", + srcs = [], + visibility = ["//visibility:public"], +) + +filegroup( + name = "dd_trace_go_version_file", + srcs = [], + visibility = ["//visibility:public"], +) + +filegroup( + name = "dd_trace_go_module_proxy_files", + srcs = [], + visibility = ["//visibility:public"], +) + +filegroup( + name = "dd_trace_go_module_proxy_root_marker", + srcs = [], + visibility = ["//visibility:public"], +) + +filegroup( + name = "orchestrion_tool_version_file", + srcs = [], + visibility = ["//visibility:public"], +) +""") + def _orchestrion_build_impl(ctx): """Build orchestrion from source.""" + if not _orchestrion_repository_enabled(ctx): + _write_empty_orchestrion_repository(ctx) + return + total_start_ms = _probe_now_ms(ctx) version = ctx.attr.version go_path = _find_go_binary(ctx) @@ -1311,44 +1358,15 @@ _orchestrion_build = repository_rule( "version": attr.string(mandatory = True, doc = "Orchestrion version to build"), "dd_trace_go_version": attr.string(default = "", doc = "dd-trace-go version to validate against the target module for Orchestrion-backed instrumentation"), "dd_trace_go_versions": attr.string_dict(doc = "Per-module dd-trace-go versions to validate against the target module for Orchestrion-backed instrumentation"), + "enabled_by_env": attr.bool(default = False, doc = "Gate repository materialization on the Test Optimization repository environment"), "log_timing": attr.bool(default = False, doc = "Emit structured timing probes while building Orchestrion"), }, + environ = [_TEST_OPTIMIZATION_ENABLED_ENV], ) def _orchestrion_empty_impl(ctx): """Create an empty placeholder repo.""" - ctx.file("BUILD.bazel", """# Generated by rules_go orchestrion extension -# No orchestrion configured -filegroup( - name = "orchestrion", - srcs = [], - visibility = ["//visibility:public"], -) - -filegroup( - name = "dd_trace_go_version_file", - srcs = [], - visibility = ["//visibility:public"], -) - -filegroup( - name = "dd_trace_go_module_proxy_files", - srcs = [], - visibility = ["//visibility:public"], -) - -filegroup( - name = "dd_trace_go_module_proxy_root_marker", - srcs = [], - visibility = ["//visibility:public"], -) - -filegroup( - name = "orchestrion_tool_version_file", - srcs = [], - visibility = ["//visibility:public"], -) -""") + _write_empty_orchestrion_repository(ctx) _orchestrion_empty = repository_rule( implementation = _orchestrion_empty_impl, @@ -1365,6 +1383,7 @@ def _orchestrion_ext_impl(module_ctx): version = "" dd_trace_go_version = "" dd_trace_go_versions = {} + enabled_by_env = True log_timing = False for mod in module_ctx.modules: for from_source in mod.tags.from_source: @@ -1372,6 +1391,7 @@ def _orchestrion_ext_impl(module_ctx): if from_source.dd_trace_go_version and from_source.dd_trace_go_versions: fail("dd_trace_go_version and dd_trace_go_versions cannot both be set in orchestrion.from_source()") version = from_source.version + enabled_by_env = from_source.enabled_by_env log_timing = from_source.log_timing if from_source.dd_trace_go_version: dd_trace_go_version = from_source.dd_trace_go_version @@ -1387,6 +1407,7 @@ def _orchestrion_ext_impl(module_ctx): version = version, dd_trace_go_version = dd_trace_go_version, dd_trace_go_versions = dd_trace_go_versions, + enabled_by_env = enabled_by_env, log_timing = log_timing, ) else: @@ -1407,6 +1428,10 @@ _from_source = tag_class( "dd_trace_go_versions": attr.string_dict( doc = "Per-module dd-trace-go versions to validate against the target module for Orchestrion-backed instrumentation.", ), + "enabled_by_env": attr.bool( + default = True, + doc = "Gate repository materialization on the Test Optimization repository environment.", + ), "log_timing": attr.bool( default = False, doc = "Emit structured timing probes while building the Orchestrion tool repository.", diff --git a/third_party/rgo/v0_61_1/base/go/private/repositories.bzl b/third_party/rgo/v0_61_1/base/go/private/repositories.bzl index 865c41fd..c57f7198 100644 --- a/third_party/rgo/v0_61_1/base/go/private/repositories.bzl +++ b/third_party/rgo/v0_61_1/base/go/private/repositories.bzl @@ -17,6 +17,7 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") load("//go/private:common.bzl", "MINIMUM_BAZEL_VERSION") load("//go/private:nogo.bzl", "DEFAULT_NOGO", "go_register_nogo") +load("//go/private/orchestrion:extensions.bzl", "orchestrion_empty_repository") load("//go/private:polyfill_bazel_features.bzl", "polyfill_bazel_features") load("//go/private/skylib/lib:versions.bzl", "versions") load("//proto:gogo.bzl", "gogo_special_proto") @@ -39,6 +40,15 @@ def go_rules_dependencies(force = False): if getattr(native, "bazel_version", None): versions.check(MINIMUM_BAZEL_VERSION, bazel_version = native.bazel_version) + # Keep the stable Orchestrion repository mapping available to ordinary + # WORKSPACE consumers. Test Optimization consumers declare the real tool + # repository before calling go_rules_dependencies(), so this fallback does + # not replace or fetch it. + _maybe( + orchestrion_empty_repository, + name = "rules_go_orchestrion_tool", + ) + if force: wrapper = _always else: diff --git a/third_party/rules_go_orchestrion/patches/v0_60_0/base/0001-full-delta.patch b/third_party/rules_go_orchestrion/patches/v0_60_0/base/0001-full-delta.patch index 73645e7d..1490368f 100644 --- a/third_party/rules_go_orchestrion/patches/v0_60_0/base/0001-full-delta.patch +++ b/third_party/rules_go_orchestrion/patches/v0_60_0/base/0001-full-delta.patch @@ -1030,10 +1030,10 @@ index a02bda6..3d2f91a 100644 +orchestrion = _orchestrion_ext diff --git a/go/orchestrion_workspace.bzl b/go/orchestrion_workspace.bzl new file mode 100644 -index 0000000..8d85b87 +index 0000000..f1d725a --- /dev/null +++ b/go/orchestrion_workspace.bzl -@@ -0,0 +1,54 @@ +@@ -0,0 +1,59 @@ +"""Public WORKSPACE macro for configuring the Orchestrion tool repository.""" + +load( @@ -1049,6 +1049,7 @@ index 0000000..8d85b87 + version = "", + dd_trace_go_version = "", + dd_trace_go_versions = None, ++ enabled_by_env = False, + log_timing = False): + """Create the `rules_go_orchestrion_tool` repository in WORKSPACE mode. + @@ -1061,6 +1062,9 @@ index 0000000..8d85b87 + target module when instrumentation is enabled. + dd_trace_go_versions: Optional per-module dd-trace-go version mapping. + Mutually exclusive with `dd_trace_go_version`. ++ enabled_by_env: Gate repository materialization on the Test Optimization ++ repository environment. Generic Orchestrion callers should keep the ++ default. + log_timing: Emit structured bootstrap timing probes while building the + Orchestrion tool repository. + """ @@ -1086,6 +1090,7 @@ index 0000000..8d85b87 + version = version, + dd_trace_go_version = dd_trace_go_version, + dd_trace_go_versions = dd_trace_go_versions, ++ enabled_by_env = enabled_by_env, + log_timing = log_timing, + ) diff --git a/go/private/BUILD.bazel b/go/private/BUILD.bazel @@ -1108,6 +1113,34 @@ index e18aa94..bdbf308 100644 "//go/private/rules:all_files", "//go/private/skylib/lib:all_files", "//go/private/tools:all_files", +diff --git a/go/private/actions/BUILD.bazel b/go/private/actions/BUILD.bazel +index 3bd3d15..439d4f2 100644 +--- a/go/private/actions/BUILD.bazel ++++ b/go/private/actions/BUILD.bazel +@@ -43,6 +43,7 @@ bzl_library( + deps = [ + ":utils", + "//go/private:mode", ++ "//go/private/orchestrion:pin_files", + "@bazel_skylib//lib:shell", + ], + ) +@@ -54,6 +55,7 @@ bzl_library( + deps = [ + "//go/private:common", + "//go/private:mode", ++ "//go/private/orchestrion:pin_files", + "//go/private:rpath", + "@bazel_skylib//lib:collections", + ], +@@ -66,6 +68,7 @@ bzl_library( + deps = [ + ":utils", + "//go/private:mode", ++ "//go/private/orchestrion:pin_files", + "//go/private:providers", + "//go/private:sdk", + ], diff --git a/go/private/actions/archive.bzl b/go/private/actions/archive.bzl index 8b17c19..544d389 100644 --- a/go/private/actions/archive.bzl @@ -1909,10 +1942,11 @@ index 64758f8..a6d95ca 100644 It is a common dependency of all Go targets.""", diff --git a/go/private/orchestrion/BUILD b/go/private/orchestrion/BUILD new file mode 100644 -index 0000000..488d4ad +index 0000000..31a01c3 --- /dev/null +++ b/go/private/orchestrion/BUILD -@@ -0,0 +1,65 @@ +@@ -0,0 +1,115 @@ ++load("@bazel_skylib//:bzl_library.bzl", "bzl_library") +load("@bazel_skylib//rules:common_settings.bzl", "bool_flag", "string_flag") + +filegroup( @@ -1928,6 +1962,12 @@ index 0000000..488d4ad + visibility = ["//visibility:public"], +) + ++bzl_library( ++ name = "pin_files", ++ srcs = ["pin_files.bzl"], ++ visibility = ["//go:__subpackages__"], ++) ++ +bool_flag( + name = "enabled", + build_setting_default = False, @@ -1944,46 +1984,89 @@ index 0000000..488d4ad + visibility = ["//visibility:public"], +) + -+# Proxy target for the orchestrion tool binary. -+# This always points to the rules_go_orchestrion_tool repo. -+# The repo provides an empty filegroup by default, or the actual orchestrion -+# binary when configured via module extension. -+# go_context_data checks the :enabled flag to determine whether to use this. ++config_setting( ++ name = "enabled_config", ++ flag_values = {":enabled": "true"}, ++) ++ ++filegroup( ++ name = "disabled_tool_binary", ++ srcs = [], ++) ++ ++filegroup( ++ name = "disabled_dd_trace_go_version_file", ++ srcs = [], ++) ++ ++filegroup( ++ name = "disabled_dd_trace_go_module_proxy_files", ++ srcs = [], ++) ++ ++filegroup( ++ name = "disabled_dd_trace_go_module_proxy_root_marker", ++ srcs = [], ++) ++ ++filegroup( ++ name = "disabled_orchestrion_tool_version_file", ++ srcs = [], ++) ++ ++# Stable Orchestrion aliases select package-local empty targets by default and ++# only reference the real tool repository when the public :enabled flag is set. ++# go_context_data also checks :enabled before consuming these files. +alias( + name = "tool_binary", -+ actual = "@rules_go_orchestrion_tool//:orchestrion", ++ actual = select({ ++ ":enabled_config": "@rules_go_orchestrion_tool//:orchestrion", ++ "//conditions:default": ":disabled_tool_binary", ++ }), + visibility = ["//visibility:public"], +) + +alias( + name = "dd_trace_go_version_file", -+ actual = "@rules_go_orchestrion_tool//:dd_trace_go_version_file", ++ actual = select({ ++ ":enabled_config": "@rules_go_orchestrion_tool//:dd_trace_go_version_file", ++ "//conditions:default": ":disabled_dd_trace_go_version_file", ++ }), + visibility = ["//visibility:public"], +) + +alias( + name = "dd_trace_go_module_proxy_files", -+ actual = "@rules_go_orchestrion_tool//:dd_trace_go_module_proxy_files", ++ actual = select({ ++ ":enabled_config": "@rules_go_orchestrion_tool//:dd_trace_go_module_proxy_files", ++ "//conditions:default": ":disabled_dd_trace_go_module_proxy_files", ++ }), + visibility = ["//visibility:public"], +) + +alias( + name = "dd_trace_go_module_proxy_root_marker", -+ actual = "@rules_go_orchestrion_tool//:dd_trace_go_module_proxy_root_marker", ++ actual = select({ ++ ":enabled_config": "@rules_go_orchestrion_tool//:dd_trace_go_module_proxy_root_marker", ++ "//conditions:default": ":disabled_dd_trace_go_module_proxy_root_marker", ++ }), + visibility = ["//visibility:public"], +) + +alias( + name = "orchestrion_tool_version_file", -+ actual = "@rules_go_orchestrion_tool//:orchestrion_tool_version_file", ++ actual = select({ ++ ":enabled_config": "@rules_go_orchestrion_tool//:orchestrion_tool_version_file", ++ "//conditions:default": ":disabled_orchestrion_tool_version_file", ++ }), + visibility = ["//visibility:public"], +) diff --git a/go/private/orchestrion/extensions.bzl b/go/private/orchestrion/extensions.bzl new file mode 100644 -index 0000000..0565233 +index 0000000..4270262 --- /dev/null +++ b/go/private/orchestrion/extensions.bzl -@@ -0,0 +1,1422 @@ +@@ -0,0 +1,1447 @@ +# Copyright 2024 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); @@ -3010,8 +3093,55 @@ index 0000000..0565233 + powershell_single_quoted_literal = _powershell_single_quoted_literal, +) + ++_TEST_OPTIMIZATION_ENABLED_ENV = "DD_TEST_OPTIMIZATION_ENABLED" ++_TEST_OPTIMIZATION_ENABLED_VALUES = ["1", "true", "yes", "on"] ++ ++def _orchestrion_repository_enabled(ctx): ++ if not ctx.attr.enabled_by_env: ++ return True ++ value = ctx.os.environ.get(_TEST_OPTIMIZATION_ENABLED_ENV, "") ++ return value.strip().lower() in _TEST_OPTIMIZATION_ENABLED_VALUES ++ ++def _write_empty_orchestrion_repository(ctx): ++ ctx.file("BUILD.bazel", """# Generated by rules_go orchestrion extension ++# Orchestrion is disabled ++filegroup( ++ name = "orchestrion", ++ srcs = [], ++ visibility = ["//visibility:public"], ++) ++ ++filegroup( ++ name = "dd_trace_go_version_file", ++ srcs = [], ++ visibility = ["//visibility:public"], ++) ++ ++filegroup( ++ name = "dd_trace_go_module_proxy_files", ++ srcs = [], ++ visibility = ["//visibility:public"], ++) ++ ++filegroup( ++ name = "dd_trace_go_module_proxy_root_marker", ++ srcs = [], ++ visibility = ["//visibility:public"], ++) ++ ++filegroup( ++ name = "orchestrion_tool_version_file", ++ srcs = [], ++ visibility = ["//visibility:public"], ++) ++""") ++ +def _orchestrion_build_impl(ctx): + """Build orchestrion from source.""" ++ if not _orchestrion_repository_enabled(ctx): ++ _write_empty_orchestrion_repository(ctx) ++ return ++ + total_start_ms = _probe_now_ms(ctx) + version = ctx.attr.version + go_path = _find_go_binary(ctx) @@ -3297,44 +3427,15 @@ index 0000000..0565233 + "version": attr.string(mandatory = True, doc = "Orchestrion version to build"), + "dd_trace_go_version": attr.string(default = "", doc = "dd-trace-go version to validate against the target module for Orchestrion-backed instrumentation"), + "dd_trace_go_versions": attr.string_dict(doc = "Per-module dd-trace-go versions to validate against the target module for Orchestrion-backed instrumentation"), ++ "enabled_by_env": attr.bool(default = False, doc = "Gate repository materialization on the Test Optimization repository environment"), + "log_timing": attr.bool(default = False, doc = "Emit structured timing probes while building Orchestrion"), + }, ++ environ = [_TEST_OPTIMIZATION_ENABLED_ENV], +) + +def _orchestrion_empty_impl(ctx): + """Create an empty placeholder repo.""" -+ ctx.file("BUILD.bazel", """# Generated by rules_go orchestrion extension -+# No orchestrion configured -+filegroup( -+ name = "orchestrion", -+ srcs = [], -+ visibility = ["//visibility:public"], -+) -+ -+filegroup( -+ name = "dd_trace_go_version_file", -+ srcs = [], -+ visibility = ["//visibility:public"], -+) -+ -+filegroup( -+ name = "dd_trace_go_module_proxy_files", -+ srcs = [], -+ visibility = ["//visibility:public"], -+) -+ -+filegroup( -+ name = "dd_trace_go_module_proxy_root_marker", -+ srcs = [], -+ visibility = ["//visibility:public"], -+) -+ -+filegroup( -+ name = "orchestrion_tool_version_file", -+ srcs = [], -+ visibility = ["//visibility:public"], -+) -+""") ++ _write_empty_orchestrion_repository(ctx) + +_orchestrion_empty = repository_rule( + implementation = _orchestrion_empty_impl, @@ -3351,6 +3452,7 @@ index 0000000..0565233 + version = "" + dd_trace_go_version = "" + dd_trace_go_versions = {} ++ enabled_by_env = True + log_timing = False + for mod in module_ctx.modules: + for from_source in mod.tags.from_source: @@ -3358,6 +3460,7 @@ index 0000000..0565233 + if from_source.dd_trace_go_version and from_source.dd_trace_go_versions: + fail("dd_trace_go_version and dd_trace_go_versions cannot both be set in orchestrion.from_source()") + version = from_source.version ++ enabled_by_env = from_source.enabled_by_env + log_timing = from_source.log_timing + if from_source.dd_trace_go_version: + dd_trace_go_version = from_source.dd_trace_go_version @@ -3373,6 +3476,7 @@ index 0000000..0565233 + version = version, + dd_trace_go_version = dd_trace_go_version, + dd_trace_go_versions = dd_trace_go_versions, ++ enabled_by_env = enabled_by_env, + log_timing = log_timing, + ) + else: @@ -3393,6 +3497,10 @@ index 0000000..0565233 + "dd_trace_go_versions": attr.string_dict( + doc = "Per-module dd-trace-go versions to validate against the target module for Orchestrion-backed instrumentation.", + ), ++ "enabled_by_env": attr.bool( ++ default = True, ++ doc = "Gate repository materialization on the Test Optimization repository environment.", ++ ), + "log_timing": attr.bool( + default = False, + doc = "Emit structured timing probes while building the Orchestrion tool repository.", @@ -3452,6 +3560,34 @@ index 0000000..a3f4e39 + }, + doc = "Groups Orchestrion pin files without treating every runtime data file as a build input.", +) +diff --git a/go/private/repositories.bzl b/go/private/repositories.bzl +index 94adead..2789520 100644 +--- a/go/private/repositories.bzl ++++ b/go/private/repositories.bzl +@@ -17,6 +17,7 @@ + load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + load("//go/private:common.bzl", "MINIMUM_BAZEL_VERSION") + load("//go/private:nogo.bzl", "DEFAULT_NOGO", "go_register_nogo") ++load("//go/private/orchestrion:extensions.bzl", "orchestrion_empty_repository") + load("//go/private:polyfill_bazel_features.bzl", "polyfill_bazel_features") + load("//go/private/skylib/lib:versions.bzl", "versions") + load("//proto:gogo.bzl", "gogo_special_proto") +@@ -39,6 +40,15 @@ def go_rules_dependencies(force = False): + if getattr(native, "bazel_version", None): + versions.check(MINIMUM_BAZEL_VERSION, bazel_version = native.bazel_version) + ++ # Keep the stable Orchestrion repository mapping available to ordinary ++ # WORKSPACE consumers. Test Optimization consumers declare the real tool ++ # repository before calling go_rules_dependencies(), so this fallback does ++ # not replace or fetch it. ++ _maybe( ++ orchestrion_empty_repository, ++ name = "rules_go_orchestrion_tool", ++ ) ++ + if force: + wrapper = _always + else: diff --git a/go/private/rules/library.bzl b/go/private/rules/library.bzl index ae1ce57..e195072 100644 --- a/go/private/rules/library.bzl diff --git a/third_party/rules_go_orchestrion/patches/v0_61_1/base/0001-full-delta.patch b/third_party/rules_go_orchestrion/patches/v0_61_1/base/0001-full-delta.patch index 8fced3bc..92b3657f 100644 --- a/third_party/rules_go_orchestrion/patches/v0_61_1/base/0001-full-delta.patch +++ b/third_party/rules_go_orchestrion/patches/v0_61_1/base/0001-full-delta.patch @@ -1042,10 +1042,10 @@ index a02bda6..3d2f91a 100644 +orchestrion = _orchestrion_ext diff --git a/go/orchestrion_workspace.bzl b/go/orchestrion_workspace.bzl new file mode 100644 -index 0000000..8d85b87 +index 0000000..f1d725a --- /dev/null +++ b/go/orchestrion_workspace.bzl -@@ -0,0 +1,54 @@ +@@ -0,0 +1,59 @@ +"""Public WORKSPACE macro for configuring the Orchestrion tool repository.""" + +load( @@ -1061,6 +1061,7 @@ index 0000000..8d85b87 + version = "", + dd_trace_go_version = "", + dd_trace_go_versions = None, ++ enabled_by_env = False, + log_timing = False): + """Create the `rules_go_orchestrion_tool` repository in WORKSPACE mode. + @@ -1073,6 +1074,9 @@ index 0000000..8d85b87 + target module when instrumentation is enabled. + dd_trace_go_versions: Optional per-module dd-trace-go version mapping. + Mutually exclusive with `dd_trace_go_version`. ++ enabled_by_env: Gate repository materialization on the Test Optimization ++ repository environment. Generic Orchestrion callers should keep the ++ default. + log_timing: Emit structured bootstrap timing probes while building the + Orchestrion tool repository. + """ @@ -1098,6 +1102,7 @@ index 0000000..8d85b87 + version = version, + dd_trace_go_version = dd_trace_go_version, + dd_trace_go_versions = dd_trace_go_versions, ++ enabled_by_env = enabled_by_env, + log_timing = log_timing, + ) diff --git a/go/private/BUILD.bazel b/go/private/BUILD.bazel @@ -1937,10 +1942,10 @@ index 049b8ec..97b5629 100644 It is a common dependency of all Go targets.""", diff --git a/go/private/orchestrion/BUILD b/go/private/orchestrion/BUILD new file mode 100644 -index 0000000..488d4ad +index 0000000..177eeaf --- /dev/null +++ b/go/private/orchestrion/BUILD -@@ -0,0 +1,65 @@ +@@ -0,0 +1,108 @@ +load("@bazel_skylib//rules:common_settings.bzl", "bool_flag", "string_flag") + +filegroup( @@ -1972,46 +1977,89 @@ index 0000000..488d4ad + visibility = ["//visibility:public"], +) + -+# Proxy target for the orchestrion tool binary. -+# This always points to the rules_go_orchestrion_tool repo. -+# The repo provides an empty filegroup by default, or the actual orchestrion -+# binary when configured via module extension. -+# go_context_data checks the :enabled flag to determine whether to use this. ++config_setting( ++ name = "enabled_config", ++ flag_values = {":enabled": "true"}, ++) ++ ++filegroup( ++ name = "disabled_tool_binary", ++ srcs = [], ++) ++ ++filegroup( ++ name = "disabled_dd_trace_go_version_file", ++ srcs = [], ++) ++ ++filegroup( ++ name = "disabled_dd_trace_go_module_proxy_files", ++ srcs = [], ++) ++ ++filegroup( ++ name = "disabled_dd_trace_go_module_proxy_root_marker", ++ srcs = [], ++) ++ ++filegroup( ++ name = "disabled_orchestrion_tool_version_file", ++ srcs = [], ++) ++ ++# Stable Orchestrion aliases select package-local empty targets by default and ++# only reference the real tool repository when the public :enabled flag is set. ++# go_context_data also checks :enabled before consuming these files. +alias( + name = "tool_binary", -+ actual = "@rules_go_orchestrion_tool//:orchestrion", ++ actual = select({ ++ ":enabled_config": "@rules_go_orchestrion_tool//:orchestrion", ++ "//conditions:default": ":disabled_tool_binary", ++ }), + visibility = ["//visibility:public"], +) + +alias( + name = "dd_trace_go_version_file", -+ actual = "@rules_go_orchestrion_tool//:dd_trace_go_version_file", ++ actual = select({ ++ ":enabled_config": "@rules_go_orchestrion_tool//:dd_trace_go_version_file", ++ "//conditions:default": ":disabled_dd_trace_go_version_file", ++ }), + visibility = ["//visibility:public"], +) + +alias( + name = "dd_trace_go_module_proxy_files", -+ actual = "@rules_go_orchestrion_tool//:dd_trace_go_module_proxy_files", ++ actual = select({ ++ ":enabled_config": "@rules_go_orchestrion_tool//:dd_trace_go_module_proxy_files", ++ "//conditions:default": ":disabled_dd_trace_go_module_proxy_files", ++ }), + visibility = ["//visibility:public"], +) + +alias( + name = "dd_trace_go_module_proxy_root_marker", -+ actual = "@rules_go_orchestrion_tool//:dd_trace_go_module_proxy_root_marker", ++ actual = select({ ++ ":enabled_config": "@rules_go_orchestrion_tool//:dd_trace_go_module_proxy_root_marker", ++ "//conditions:default": ":disabled_dd_trace_go_module_proxy_root_marker", ++ }), + visibility = ["//visibility:public"], +) + +alias( + name = "orchestrion_tool_version_file", -+ actual = "@rules_go_orchestrion_tool//:orchestrion_tool_version_file", ++ actual = select({ ++ ":enabled_config": "@rules_go_orchestrion_tool//:orchestrion_tool_version_file", ++ "//conditions:default": ":disabled_orchestrion_tool_version_file", ++ }), + visibility = ["//visibility:public"], +) diff --git a/go/private/orchestrion/extensions.bzl b/go/private/orchestrion/extensions.bzl new file mode 100644 -index 0000000..0565233 +index 0000000..4270262 --- /dev/null +++ b/go/private/orchestrion/extensions.bzl -@@ -0,0 +1,1422 @@ +@@ -0,0 +1,1447 @@ +# Copyright 2024 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); @@ -3038,8 +3086,55 @@ index 0000000..0565233 + powershell_single_quoted_literal = _powershell_single_quoted_literal, +) + ++_TEST_OPTIMIZATION_ENABLED_ENV = "DD_TEST_OPTIMIZATION_ENABLED" ++_TEST_OPTIMIZATION_ENABLED_VALUES = ["1", "true", "yes", "on"] ++ ++def _orchestrion_repository_enabled(ctx): ++ if not ctx.attr.enabled_by_env: ++ return True ++ value = ctx.os.environ.get(_TEST_OPTIMIZATION_ENABLED_ENV, "") ++ return value.strip().lower() in _TEST_OPTIMIZATION_ENABLED_VALUES ++ ++def _write_empty_orchestrion_repository(ctx): ++ ctx.file("BUILD.bazel", """# Generated by rules_go orchestrion extension ++# Orchestrion is disabled ++filegroup( ++ name = "orchestrion", ++ srcs = [], ++ visibility = ["//visibility:public"], ++) ++ ++filegroup( ++ name = "dd_trace_go_version_file", ++ srcs = [], ++ visibility = ["//visibility:public"], ++) ++ ++filegroup( ++ name = "dd_trace_go_module_proxy_files", ++ srcs = [], ++ visibility = ["//visibility:public"], ++) ++ ++filegroup( ++ name = "dd_trace_go_module_proxy_root_marker", ++ srcs = [], ++ visibility = ["//visibility:public"], ++) ++ ++filegroup( ++ name = "orchestrion_tool_version_file", ++ srcs = [], ++ visibility = ["//visibility:public"], ++) ++""") ++ +def _orchestrion_build_impl(ctx): + """Build orchestrion from source.""" ++ if not _orchestrion_repository_enabled(ctx): ++ _write_empty_orchestrion_repository(ctx) ++ return ++ + total_start_ms = _probe_now_ms(ctx) + version = ctx.attr.version + go_path = _find_go_binary(ctx) @@ -3325,44 +3420,15 @@ index 0000000..0565233 + "version": attr.string(mandatory = True, doc = "Orchestrion version to build"), + "dd_trace_go_version": attr.string(default = "", doc = "dd-trace-go version to validate against the target module for Orchestrion-backed instrumentation"), + "dd_trace_go_versions": attr.string_dict(doc = "Per-module dd-trace-go versions to validate against the target module for Orchestrion-backed instrumentation"), ++ "enabled_by_env": attr.bool(default = False, doc = "Gate repository materialization on the Test Optimization repository environment"), + "log_timing": attr.bool(default = False, doc = "Emit structured timing probes while building Orchestrion"), + }, ++ environ = [_TEST_OPTIMIZATION_ENABLED_ENV], +) + +def _orchestrion_empty_impl(ctx): + """Create an empty placeholder repo.""" -+ ctx.file("BUILD.bazel", """# Generated by rules_go orchestrion extension -+# No orchestrion configured -+filegroup( -+ name = "orchestrion", -+ srcs = [], -+ visibility = ["//visibility:public"], -+) -+ -+filegroup( -+ name = "dd_trace_go_version_file", -+ srcs = [], -+ visibility = ["//visibility:public"], -+) -+ -+filegroup( -+ name = "dd_trace_go_module_proxy_files", -+ srcs = [], -+ visibility = ["//visibility:public"], -+) -+ -+filegroup( -+ name = "dd_trace_go_module_proxy_root_marker", -+ srcs = [], -+ visibility = ["//visibility:public"], -+) -+ -+filegroup( -+ name = "orchestrion_tool_version_file", -+ srcs = [], -+ visibility = ["//visibility:public"], -+) -+""") ++ _write_empty_orchestrion_repository(ctx) + +_orchestrion_empty = repository_rule( + implementation = _orchestrion_empty_impl, @@ -3379,6 +3445,7 @@ index 0000000..0565233 + version = "" + dd_trace_go_version = "" + dd_trace_go_versions = {} ++ enabled_by_env = True + log_timing = False + for mod in module_ctx.modules: + for from_source in mod.tags.from_source: @@ -3386,6 +3453,7 @@ index 0000000..0565233 + if from_source.dd_trace_go_version and from_source.dd_trace_go_versions: + fail("dd_trace_go_version and dd_trace_go_versions cannot both be set in orchestrion.from_source()") + version = from_source.version ++ enabled_by_env = from_source.enabled_by_env + log_timing = from_source.log_timing + if from_source.dd_trace_go_version: + dd_trace_go_version = from_source.dd_trace_go_version @@ -3401,6 +3469,7 @@ index 0000000..0565233 + version = version, + dd_trace_go_version = dd_trace_go_version, + dd_trace_go_versions = dd_trace_go_versions, ++ enabled_by_env = enabled_by_env, + log_timing = log_timing, + ) + else: @@ -3421,6 +3490,10 @@ index 0000000..0565233 + "dd_trace_go_versions": attr.string_dict( + doc = "Per-module dd-trace-go versions to validate against the target module for Orchestrion-backed instrumentation.", + ), ++ "enabled_by_env": attr.bool( ++ default = True, ++ doc = "Gate repository materialization on the Test Optimization repository environment.", ++ ), + "log_timing": attr.bool( + default = False, + doc = "Emit structured timing probes while building the Orchestrion tool repository.", @@ -3480,6 +3553,34 @@ index 0000000..a3f4e39 + }, + doc = "Groups Orchestrion pin files without treating every runtime data file as a build input.", +) +diff --git a/go/private/repositories.bzl b/go/private/repositories.bzl +index 865c41f..c57f719 100644 +--- a/go/private/repositories.bzl ++++ b/go/private/repositories.bzl +@@ -17,6 +17,7 @@ + load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + load("//go/private:common.bzl", "MINIMUM_BAZEL_VERSION") + load("//go/private:nogo.bzl", "DEFAULT_NOGO", "go_register_nogo") ++load("//go/private/orchestrion:extensions.bzl", "orchestrion_empty_repository") + load("//go/private:polyfill_bazel_features.bzl", "polyfill_bazel_features") + load("//go/private/skylib/lib:versions.bzl", "versions") + load("//proto:gogo.bzl", "gogo_special_proto") +@@ -39,6 +40,15 @@ def go_rules_dependencies(force = False): + if getattr(native, "bazel_version", None): + versions.check(MINIMUM_BAZEL_VERSION, bazel_version = native.bazel_version) + ++ # Keep the stable Orchestrion repository mapping available to ordinary ++ # WORKSPACE consumers. Test Optimization consumers declare the real tool ++ # repository before calling go_rules_dependencies(), so this fallback does ++ # not replace or fetch it. ++ _maybe( ++ orchestrion_empty_repository, ++ name = "rules_go_orchestrion_tool", ++ ) ++ + if force: + wrapper = _always + else: diff --git a/go/private/rules/library.bzl b/go/private/rules/library.bzl index 1b9bfeb..79c0b41 100644 --- a/go/private/rules/library.bzl diff --git a/third_party/rules_go_orchestrion/profiles/workspace_runtime.json b/third_party/rules_go_orchestrion/profiles/workspace_runtime.json index dfa3531c..7c0b23c4 100644 --- a/third_party/rules_go_orchestrion/profiles/workspace_runtime.json +++ b/third_party/rules_go_orchestrion/profiles/workspace_runtime.json @@ -7,12 +7,14 @@ "/BUILD.bazel", "go/orchestrion_workspace.bzl", "go/private/BUILD.bazel", + "go/private/actions/BUILD.bazel", "go/private/actions/archive.bzl", "go/private/actions/compilepkg.bzl", "go/private/actions/link.bzl", "go/private/actions/stdlib.bzl", "go/private/context.bzl", "go/private/orchestrion/**", + "go/private/repositories.bzl", "go/private/rules/library.bzl", "go/private/rules/stdlib.bzl", "go/private/rules/test.bzl", diff --git a/tools/agent-skills/go-test-optimization-onboarding/SKILL.md b/tools/agent-skills/go-test-optimization-onboarding/SKILL.md index 4f2cfde1..726e79a3 100644 --- a/tools/agent-skills/go-test-optimization-onboarding/SKILL.md +++ b/tools/agent-skills/go-test-optimization-onboarding/SKILL.md @@ -47,7 +47,7 @@ Keep the RFC contract intact: `DD_TEST_OPTIMIZATION_REPORT_DIR` or wrapper `--report-dir`, and configure wrapper `--support-bundle` or `DD_TEST_OPTIMIZATION_SUPPORT_BUNDLE` for complete escalation artifacts. For first-pass customer troubleshooting after - tests have run, ask for `bazel run //:dd_test_optimization_doctor -- --support-bundle=` + tests have run, ask for `bazel run --config=test-optimization //:dd_test_optimization_doctor -- --support-bundle=` with any matching BEP/artifact flags. For bundle triage, inspect `summary.md`, `diagnostics.json`, `reports/doctor-report.json`, optional uploader reports, and diff --git a/tools/agent-skills/go-test-optimization-onboarding/references/bzlmod-onboarding.md b/tools/agent-skills/go-test-optimization-onboarding/references/bzlmod-onboarding.md index d5fbe906..7b21132b 100644 --- a/tools/agent-skills/go-test-optimization-onboarding/references/bzlmod-onboarding.md +++ b/tools/agent-skills/go-test-optimization-onboarding/references/bzlmod-onboarding.md @@ -34,6 +34,17 @@ git_override( bazel_dep(name = "rules_go", version = "0.60.0") ``` +Add these lines to the named config used by test, doctor, and uploader: + +```text +common:test-optimization --repo_env=DD_TEST_OPTIMIZATION_ENABLED=1 +build:test-optimization --@rules_go//go/private/orchestrion:enabled=true +``` + +The config is the only user-facing switch. Removing +`--config=test-optimization` disables metadata fetching and selects the local +empty Orchestrion aliases; no second Test Optimization flag is required. + Use a commit that is reachable from `origin/main`. Do not publish branch-only commits in consumer snippets because squash merges can make them disappear. The `rules_go` version must match the selected Datadog-managed fork support @@ -144,17 +155,10 @@ use_repo(datadog_go_topt, "test_optimization_data") Then add root doctor and uploader targets: ```bzl -load("@datadog-rules-test-optimization//tools/core:test_optimization_doctor.bzl", "dd_test_optimization_doctor") -load("@datadog-rules-test-optimization//tools/core:test_optimization_uploader.bzl", "dd_payload_uploader") - -dd_test_optimization_doctor( - name = "dd_test_optimization_doctor", - data = ["@test_optimization_data//:test_optimization_context"], -) +load("@datadog-rules-test-optimization//tools/core:test_optimization_targets.bzl", "dd_test_optimization_targets") -dd_payload_uploader( - name = "dd_upload_payloads", - data = ["@test_optimization_data//:test_optimization_context"], +dd_test_optimization_targets( + name = "test_optimization", ) ``` @@ -317,20 +321,11 @@ Root doctor/uploader targets must include every service context that can appear in emitted payloads: ```bzl -load("@datadog-rules-test-optimization//tools/core:test_optimization_doctor.bzl", "dd_test_optimization_doctor") -load("@datadog-rules-test-optimization//tools/core:test_optimization_uploader.bzl", "dd_payload_uploader") - -dd_test_optimization_doctor( - name = "dd_test_optimization_doctor", - data = [ - "@test_optimization_data//:test_optimization_context_go_service_a", - "@test_optimization_data//:test_optimization_context_go_service_b", - ], -) +load("@datadog-rules-test-optimization//tools/core:test_optimization_targets.bzl", "dd_test_optimization_targets") -dd_payload_uploader( - name = "dd_upload_payloads", - data = [ +dd_test_optimization_targets( + name = "test_optimization", + context_data = [ "@test_optimization_data//:test_optimization_context_go_service_a", "@test_optimization_data//:test_optimization_context_go_service_b", ], diff --git a/tools/agent-skills/go-test-optimization-onboarding/references/troubleshooting.md b/tools/agent-skills/go-test-optimization-onboarding/references/troubleshooting.md index 58429a43..2579e156 100644 --- a/tools/agent-skills/go-test-optimization-onboarding/references/troubleshooting.md +++ b/tools/agent-skills/go-test-optimization-onboarding/references/troubleshooting.md @@ -146,7 +146,7 @@ intentionally lacks backend full-bundle data. ## Diagnostic Reports When logs are long or ambiguous, first ask for a doctor-only support bundle with -`bazel run //:dd_test_optimization_doctor -- --support-bundle=` plus any +`bazel run --config=test-optimization //:dd_test_optimization_doctor -- --support-bundle=` plus any matching BEP/artifact flags. Use the CI wrapper bundle when uploader dry-run or upload results matter. If a repository cannot use either bundle mode, collect `doctor-report.json`, `uploader-dry-run-report.json`, optional diff --git a/tools/agent-skills/go-test-optimization-onboarding/references/validation-checklist.md b/tools/agent-skills/go-test-optimization-onboarding/references/validation-checklist.md index f28bcdf4..a5683828 100644 --- a/tools/agent-skills/go-test-optimization-onboarding/references/validation-checklist.md +++ b/tools/agent-skills/go-test-optimization-onboarding/references/validation-checklist.md @@ -91,7 +91,7 @@ cat "$(bazel info output_base)/external/test_optimization_data_/exp ## Test, Doctor, Dry-Run, Upload For the simplest customer troubleshooting request after tests have run, use -`bazel run //:dd_test_optimization_doctor -- --support-bundle=` with any +`bazel run --config=test-optimization //:dd_test_optimization_doctor -- --support-bundle=` with any matching BEP/artifact flags. Prefer the CI wrapper when the repository can vendor the helper directory and you need uploader dry-run or upload coverage: diff --git a/tools/agent-skills/go-test-optimization-onboarding/references/workspace-onboarding.md b/tools/agent-skills/go-test-optimization-onboarding/references/workspace-onboarding.md index f9895a7b..cf46b13b 100644 --- a/tools/agent-skills/go-test-optimization-onboarding/references/workspace-onboarding.md +++ b/tools/agent-skills/go-test-optimization-onboarding/references/workspace-onboarding.md @@ -81,6 +81,16 @@ currently `v0_60_0`, which preserves the existing Use the repository's existing `rules_go` repo name when it is already established. The Go companion maps its `@rules_go` dependency to that name. +Add these lines to the named config used by test, doctor, and uploader: + +```text +common:test-optimization --repo_env=DD_TEST_OPTIMIZATION_ENABLED=1 +build:test-optimization --@io_bazel_rules_go//go/private/orchestrion:enabled=true +``` + +`--config=test-optimization` is the only user-facing switch. Omitting it +renders disabled metadata stubs and selects the local empty Orchestrion aliases. + If the repository uses a non-default fetch model, set it explicitly: ```bzl @@ -223,31 +233,40 @@ rewriting unrelated dependencies. ## Sync And Orchestrion Setup Declare the Orchestrion tool repository and Test Optimization sync repository -near the repository's existing Go toolchain wiring: +through the reusable Go companion helpers near the repository's existing Go +toolchain wiring: ```bzl -load("@io_bazel_rules_go//go:orchestrion_workspace.bzl", "go_orchestrion_tool_repo") -load("@datadog-rules-test-optimization//tools/core:test_optimization_sync.bzl", "test_optimization_sync") +load("@datadog-rules-test-optimization-go//:topt_go_orchestrion_repository.bzl", "dd_topt_go_orchestrion_tool_repo") +load("@datadog-rules-test-optimization-go//:topt_go_workspace.bzl", "dd_topt_go_workspace_sync_repositories") -go_orchestrion_tool_repo( +dd_topt_go_orchestrion_tool_repo( dd_trace_go_version = "v2.9.0", version = "v1.9.0", ) -test_optimization_sync( +# Call the repository's existing go_rules_dependencies() wiring after the +# real tool repository. rules_go supplies the disabled fallback itself. + +dd_topt_go_workspace_sync_repositories( name = "test_optimization_data_", debug = True, require_git_metadata = True, - runtime_module_path = "", - runtime_name = "go", + module_path = "", runtime_version = "", service = "", ) ``` -Use the actual `rules_go` repository name in the `go_orchestrion_tool_repo` -load. For example, if the repository maps rules_go to `io_bazel_rules_go`, load -from `@io_bazel_rules_go//go:orchestrion_workspace.bzl`. +The public Go helper is config-gated by default. Do not add a second enable +attribute to each repository or test target. + +The helper loads the public Orchestrion repository API through the Go +companion's repository mapping, so consumers do not load it from their apparent +`rules_go` repository directly. The apparent repository name still belongs in +the `.bazelrc` analysis-time flag shown above. Do not load +`orchestrion_empty_repository`; the fork creates that fallback internally for +ordinary WORKSPACE use. If the repository already defines a Go version constant for Bazel toolchains, reuse that constant for `runtime_version` instead of hardcoding another copy. @@ -367,7 +386,7 @@ test sandbox. Do not add `FETCH_SALT` to the normal config. Use it only in a separate, explicit force-refresh command such as -`bazel sync --only= --repo_env=FETCH_SALT="$(date +%s)"` when rollout +`bazel sync --config=test-optimization --only= --repo_env=FETCH_SALT="$(date +%s)"` when rollout owners intentionally need fresh backend metadata. Never add: @@ -402,21 +421,17 @@ providers also need `--bep-artifact-downloader=`. Add one doctor and one uploader target at the repository root: ```bzl -load("@datadog-rules-test-optimization//tools/core:test_optimization_doctor.bzl", "dd_test_optimization_doctor") -load("@datadog-rules-test-optimization//tools/core:test_optimization_uploader.bzl", "dd_payload_uploader") +load("@datadog-rules-test-optimization//tools/core:test_optimization_targets.bzl", "dd_test_optimization_targets") -dd_test_optimization_doctor( - name = "dd_test_optimization_doctor", - data = ["@test_optimization_data_//:test_optimization_context"], +dd_test_optimization_targets( + name = "test_optimization", + sync_repo_name = "test_optimization_data_", expected_targets = [ "//path/to/pilot:go_default_test", ], -) - -dd_payload_uploader( - name = "dd_upload_payloads", - data = ["@test_optimization_data_//:test_optimization_context"], - fail_on_error = True, + uploader_kwargs = { + "fail_on_error": True, + }, ) ``` diff --git a/tools/agent-skills/python-test-optimization-onboarding/SKILL.md b/tools/agent-skills/python-test-optimization-onboarding/SKILL.md index 6f1d0bcd..9b54c0b1 100644 --- a/tools/agent-skills/python-test-optimization-onboarding/SKILL.md +++ b/tools/agent-skills/python-test-optimization-onboarding/SKILL.md @@ -47,7 +47,7 @@ Keep the RFC contract intact: `DD_TEST_OPTIMIZATION_REPORT_DIR` or wrapper `--report-dir`, and configure wrapper `--support-bundle` or `DD_TEST_OPTIMIZATION_SUPPORT_BUNDLE` for complete escalation artifacts. For first-pass customer troubleshooting after - tests have run, ask for `bazel run //:dd_test_optimization_doctor -- --support-bundle=` + tests have run, ask for `bazel run --config=test-optimization //:dd_test_optimization_doctor -- --support-bundle=` with any matching BEP/artifact flags. For bundle triage, inspect `summary.md`, `diagnostics.json`, `reports/doctor-report.json`, optional uploader reports, and @@ -116,7 +116,7 @@ Every successful Python onboarding should end with these pieces: support bundle for the simplest initial customer request. Keep individual reports for local inspection and manual fallback flows. - `FETCH_SALT` is used only for a separate, explicit - `bazel sync --only= --repo_env=FETCH_SALT="$(date +%s)"` refresh, never + `bazel sync --config=test-optimization --only= --repo_env=FETCH_SALT="$(date +%s)"` refresh, never as part of normal test, doctor, or uploader commands. - Real upload happens only after tests, doctor, and dry-run enrichment pass. diff --git a/tools/agent-skills/python-test-optimization-onboarding/references/bzlmod-onboarding.md b/tools/agent-skills/python-test-optimization-onboarding/references/bzlmod-onboarding.md index 8346c7a7..95501185 100644 --- a/tools/agent-skills/python-test-optimization-onboarding/references/bzlmod-onboarding.md +++ b/tools/agent-skills/python-test-optimization-onboarding/references/bzlmod-onboarding.md @@ -50,11 +50,20 @@ topt.test_optimization_sync( service = "", runtime_name = "python", runtime_version = "", + enabled_by_env = True, ) use_repo(topt, "test_optimization_data") ``` +Enable this sync only through the named Bazel config: + +```text +common:test-optimization --repo_env=DD_TEST_OPTIMIZATION_ENABLED=1 +``` + +Do not add a `rules_go` Orchestrion flag to a Python-only consumer. + ## Doctor And Uploader Targets Add one logical doctor/uploader pair. In monorepos, prefer a lightweight package diff --git a/tools/agent-skills/python-test-optimization-onboarding/references/consumer-runner.md b/tools/agent-skills/python-test-optimization-onboarding/references/consumer-runner.md index 1896deb2..a896b0cd 100644 --- a/tools/agent-skills/python-test-optimization-onboarding/references/consumer-runner.md +++ b/tools/agent-skills/python-test-optimization-onboarding/references/consumer-runner.md @@ -44,7 +44,6 @@ dd_topt_py_test( name = "pkg_py_test", py_test_rule = repo_pytest_wrapper, runner_mode = "consumer_runner", - module_identifier = "example.python.pkg", srcs = glob(["test_*.py"]), deps = [ ":pkg_lib", @@ -55,8 +54,10 @@ dd_topt_py_test( ) ``` -Prefer `module_identifier` in consumer-runner mode because the Datadog macro -does not need to synthesize Python imports to infer the module. +When the runtime module path and Bazel package path identify the test, omit +`module_identifier` and use the derived fallback. Keep an explicit +`module_identifier` only for a documented repository-specific exception; the +Datadog macro does not need to synthesize Python imports for the normal path. ## Validation diff --git a/tools/agent-skills/python-test-optimization-onboarding/references/troubleshooting.md b/tools/agent-skills/python-test-optimization-onboarding/references/troubleshooting.md index 78760602..4ffbdc34 100644 --- a/tools/agent-skills/python-test-optimization-onboarding/references/troubleshooting.md +++ b/tools/agent-skills/python-test-optimization-onboarding/references/troubleshooting.md @@ -82,7 +82,7 @@ and run those package-local labels before changing instrumentation. If metadata refetches repeatedly, check whether `.bazelrc` or scripts set `FETCH_SALT` by default. It should appear only in an explicit -`bazel sync --only= --repo_env=FETCH_SALT="$(date +%s)"` force-refresh +`bazel sync --config=test-optimization --only= --repo_env=FETCH_SALT="$(date +%s)"` force-refresh command. ## Remote Outputs Missing @@ -116,7 +116,7 @@ Then re-run tests before running doctor and uploader. ## Diagnostic Reports When logs are long or ambiguous, first ask for a doctor-only support bundle with -`bazel run //:dd_test_optimization_doctor -- --support-bundle=` plus any +`bazel run --config=test-optimization //:dd_test_optimization_doctor -- --support-bundle=` plus any matching BEP/artifact flags. Use the CI wrapper bundle when uploader dry-run or upload results matter. If a repository cannot use either bundle mode, collect `doctor-report.json`, `uploader-dry-run-report.json`, optional diff --git a/tools/agent-skills/python-test-optimization-onboarding/references/validation-checklist.md b/tools/agent-skills/python-test-optimization-onboarding/references/validation-checklist.md index 9e2494d2..6fead286 100644 --- a/tools/agent-skills/python-test-optimization-onboarding/references/validation-checklist.md +++ b/tools/agent-skills/python-test-optimization-onboarding/references/validation-checklist.md @@ -72,7 +72,7 @@ bazel sync --enable_workspace --config=test-optimization \ ## Test, Doctor, Dry-Run, Upload For the simplest customer troubleshooting request after tests have run, use -`bazel run //:dd_test_optimization_doctor -- --support-bundle=` with any +`bazel run --config=test-optimization //:dd_test_optimization_doctor -- --support-bundle=` with any matching BEP/artifact flags. Prefer the CI wrapper when the repository can vendor the helper directory and you need uploader dry-run or upload coverage: diff --git a/tools/agent-skills/python-test-optimization-onboarding/references/workspace-onboarding.md b/tools/agent-skills/python-test-optimization-onboarding/references/workspace-onboarding.md index 9d94b61e..b2550efe 100644 --- a/tools/agent-skills/python-test-optimization-onboarding/references/workspace-onboarding.md +++ b/tools/agent-skills/python-test-optimization-onboarding/references/workspace-onboarding.md @@ -139,9 +139,18 @@ test_optimization_sync( service = "", runtime_name = "python", runtime_version = "3.12", + enabled_by_env = True, ) ``` +Enable this sync only through the named Bazel config: + +```text +common:test-optimization --repo_env=DD_TEST_OPTIMIZATION_ENABLED=1 +``` + +Do not add a `rules_go` Orchestrion flag to a Python-only consumer. + Add one logical doctor/uploader pair. Prefer a lightweight package in monorepos: ```bzl @@ -189,7 +198,6 @@ dd_topt_py_test( name = "pkg_py_test", py_test_rule = repo_py_test, runner_mode = "consumer_runner", - module_identifier = "example.python.pkg", srcs = glob(["test_*.py"]), deps = [ requirement("ddtrace"), diff --git a/tools/agent-skills/rules-go-orchestrion-upstream-migration/SKILL.md b/tools/agent-skills/rules-go-orchestrion-upstream-migration/SKILL.md index 9b1aa375..de6a3720 100644 --- a/tools/agent-skills/rules-go-orchestrion-upstream-migration/SKILL.md +++ b/tools/agent-skills/rules-go-orchestrion-upstream-migration/SKILL.md @@ -129,6 +129,22 @@ A migration is done only when all of these are true: - The final report names the target upstream, changed-path counts, validation results, and any remaining external blockers. +## Test Optimization Alias Contract + +When validating a consumer that uses Test Optimization, preserve the stable +alias contract from the vendored base tree: + +```bazelrc +common:test-optimization --repo_env=DD_TEST_OPTIMIZATION_ENABLED=1 +build:test-optimization --@rules_go//go/private/orchestrion:enabled=true +``` + +`--config=test-optimization` is the only user-facing switch. Omitting it must +leave metadata bootstrap disabled for public Go extension repositories, and +for low-level repositories explicitly configured with `enabled_by_env = True`, +while selecting local empty Orchestrion aliases. Do not add a consumer-local +duplicate bool flag or collapse the real and empty repository rules. + ## Stop Conditions Stop and escalate instead of guessing when: diff --git a/tools/core/test_optimization_multi_sync.bzl b/tools/core/test_optimization_multi_sync.bzl index 535f409e..d4d5f420 100644 --- a/tools/core/test_optimization_multi_sync.bzl +++ b/tools/core/test_optimization_multi_sync.bzl @@ -59,6 +59,54 @@ def _compute_repo_names(base_name, service_keys): repo_names.append("%s_%s" % (base_name, key)) return repo_names +def _build_multi_sync_repo_specs( + name, + services, + runtime_name = "", + runtime_version = "", + runtime_arch = "", + runtime_module_path = "", + out_dir = ".testoptimization", + http_connect_timeout_seconds = -1, + http_max_time_seconds = -1, + http_retry_attempts = -1, + http_retry_delay_seconds = -1, + http_execute_timeout_buffer_seconds = -1, + known_tests = True, + test_management = True, + flaky_tests = True, + enabled = True, + enabled_by_env = False, + require_git_metadata = False, + debug = False): + """Build complete per-service sync specs for a multi-service tag.""" + service_keys = _compute_service_keys(services) + repo_names = _compute_repo_names(name, service_keys) + specs = [] + for i in range(len(services)): + specs.append({ + "repo": repo_names[i], + "service": services[i], + "out_dir": out_dir, + "runtime_name": runtime_name, + "runtime_version": runtime_version, + "runtime_arch": runtime_arch, + "runtime_module_path": runtime_module_path, + "http_connect_timeout_seconds": http_connect_timeout_seconds, + "http_max_time_seconds": http_max_time_seconds, + "http_retry_attempts": http_retry_attempts, + "http_retry_delay_seconds": http_retry_delay_seconds, + "http_execute_timeout_buffer_seconds": http_execute_timeout_buffer_seconds, + "known_tests": known_tests, + "test_management": test_management, + "flaky_tests": flaky_tests, + "enabled": enabled, + "enabled_by_env": enabled_by_env, + "require_git_metadata": require_git_metadata, + "debug": debug, + }) + return specs + def _record_multi_repo_owner_or_fail(seen_repo_owners, repo_name, owner): """Record generated repo owner and fail when names collide.""" prev_owner = seen_repo_owners.get(repo_name) @@ -159,6 +207,7 @@ def _render_aggregate_export_bzl(): # Public aliases for unit tests. compute_multi_service_keys_for_tests = _compute_service_keys compute_multi_repo_names_for_tests = _compute_repo_names +build_multi_sync_repo_specs_for_tests = _build_multi_sync_repo_specs render_multi_aggregate_bzl_for_tests = _render_aggregate_bzl render_multi_aggregate_export_bzl_for_tests = _render_aggregate_export_bzl record_multi_repo_owner_or_fail_for_tests = _record_multi_repo_owner_or_fail @@ -227,12 +276,27 @@ def _test_optimization_multi_sync_extension_impl(module_ctx): # Repo names are positional with keys/services; keep list order intact. per_repo_names = _compute_repo_names(name, keys) - service_repo_entries = [] - for i in range(len(services)): - service_repo_entries.append({ - "service": services[i], - "repo": per_repo_names[i], - }) + service_repo_entries = _build_multi_sync_repo_specs( + name = name, + services = services, + runtime_name = call.runtime_name, + runtime_version = call.runtime_version, + runtime_arch = call.runtime_arch, + runtime_module_path = call.runtime_module_path, + out_dir = call.out_dir, + http_connect_timeout_seconds = call.http_connect_timeout_seconds, + http_max_time_seconds = call.http_max_time_seconds, + http_retry_attempts = call.http_retry_attempts, + http_retry_delay_seconds = call.http_retry_delay_seconds, + http_execute_timeout_buffer_seconds = call.http_execute_timeout_buffer_seconds, + known_tests = call.known_tests, + test_management = call.test_management, + flaky_tests = call.flaky_tests, + enabled = call.enabled, + enabled_by_env = call.enabled_by_env, + require_git_metadata = call.require_git_metadata, + debug = call.debug, + ) # Validate generated repository names before instantiating any repos. for repo_name in [name] + per_repo_names: @@ -240,7 +304,6 @@ def _test_optimization_multi_sync_extension_impl(module_ctx): # Phase 2: materialize one sync repository per original service. for entry in service_repo_entries: - svc = entry["service"] repo = entry["repo"] # Preserve original service string for API requests; only keys @@ -248,24 +311,26 @@ def _test_optimization_multi_sync_extension_impl(module_ctx): test_optimization_sync( name = repo, repo_name = repo, - out_dir = call.out_dir, # optional; if set, used per-repo - service = svc, # override/DD_SERVICE for this service - runtime_name = call.runtime_name, - runtime_version = call.runtime_version, - runtime_arch = call.runtime_arch, - runtime_module_path = call.runtime_module_path, + out_dir = entry["out_dir"], # optional; if set, used per-repo + service = entry["service"], # override/DD_SERVICE for this service + runtime_name = entry["runtime_name"], + runtime_version = entry["runtime_version"], + runtime_arch = entry["runtime_arch"], + runtime_module_path = entry["runtime_module_path"], # Optional HTTP timeout/retry policy overrides. # Use -1 to keep default/env behavior. - http_connect_timeout_seconds = call.http_connect_timeout_seconds, - http_max_time_seconds = call.http_max_time_seconds, - http_retry_attempts = call.http_retry_attempts, - http_retry_delay_seconds = call.http_retry_delay_seconds, - http_execute_timeout_buffer_seconds = call.http_execute_timeout_buffer_seconds, - known_tests = call.known_tests, - test_management = call.test_management, - flaky_tests = call.flaky_tests, - require_git_metadata = call.require_git_metadata, - debug = call.debug, + http_connect_timeout_seconds = entry["http_connect_timeout_seconds"], + http_max_time_seconds = entry["http_max_time_seconds"], + http_retry_attempts = entry["http_retry_attempts"], + http_retry_delay_seconds = entry["http_retry_delay_seconds"], + http_execute_timeout_buffer_seconds = entry["http_execute_timeout_buffer_seconds"], + known_tests = entry["known_tests"], + test_management = entry["test_management"], + flaky_tests = entry["flaky_tests"], + enabled = entry["enabled"], + enabled_by_env = entry["enabled_by_env"], + require_git_metadata = entry["require_git_metadata"], + debug = entry["debug"], ) # Phase 3: publish one aggregate repo exposing stable aliases. @@ -300,6 +365,8 @@ test_optimization_multi_sync_extension = module_extension( "known_tests": attr.bool(default = True), "test_management": attr.bool(default = True), "flaky_tests": attr.bool(default = True), + "enabled": attr.bool(default = True), + "enabled_by_env": attr.bool(default = False), "require_git_metadata": attr.bool(default = False), "debug": attr.bool(default = False), }), diff --git a/tools/core/test_optimization_sync.bzl b/tools/core/test_optimization_sync.bzl index 4b293e59..f93e3544 100644 --- a/tools/core/test_optimization_sync.bzl +++ b/tools/core/test_optimization_sync.bzl @@ -81,6 +81,7 @@ load( _first_env_from_environ = "first_env_from_environ", _normalize_env_data = "normalize_env_data", _normalize_ref = "normalize_ref", + _resolve_service_and_environment = "resolve_service_and_environment", _sanitize_repository_url = "sanitize_repository_url", _set_context_tag_from_env = "set_context_tag_from_env", ) @@ -112,6 +113,44 @@ HTTP_EXECUTE_TIMEOUT_SECONDS = ( # Sentinel value used by optional integer attrs where 0 can be meaningful. HTTP_POLICY_ATTR_UNSET = -1 +_TEST_OPTIMIZATION_ENABLED_VALUES = ["1", "true", "yes", "on"] + +def _is_test_optimization_enabled(enabled, enabled_by_env, env_value): + """Resolve repository bootstrap enablement from attrs and repo environment.""" + if not enabled: + return False + if not enabled_by_env: + return True + return (env_value or "").strip().lower() in _TEST_OPTIMIZATION_ENABLED_VALUES + +_TEST_OPTIMIZATION_REPOSITORY_BASE_ENVIRON = [ + "DD_API_KEY", + "DD_SITE", + "DD_TEST_OPTIMIZATION_AGENTLESS_URL", + "DD_TEST_OPTIMIZATION_HTTP_CONNECT_TIMEOUT_SECONDS", + "DD_TEST_OPTIMIZATION_HTTP_MAX_TIME_SECONDS", + "DD_TEST_OPTIMIZATION_HTTP_RETRY_ATTEMPTS", + "DD_TEST_OPTIMIZATION_HTTP_RETRY_DELAY_SECONDS", + "DD_TEST_OPTIMIZATION_HTTP_EXECUTE_TIMEOUT_BUFFER_SECONDS", + "FETCH_SALT", + "GIT_DIRTY", + "GO_MODULE_PATH", + "PYTHON_MODULE_PATH", + "JAVA_MODULE_PATH", + "NODEJS_MODULE_PATH", + "DOTNET_MODULE_PATH", + "RUBY_MODULE_PATH", + "OS", + "ComSpec", + "COMSPEC", + "PROCESSOR_ARCHITECTURE", + "PROCESSOR_ARCHITEW6432", +] + +_TEST_OPTIMIZATION_REPOSITORY_ENVIRON = _TEST_OPTIMIZATION_REPOSITORY_BASE_ENVIRON + _ALL_SYNC_ENV_KEYS + [ + "DD_TEST_OPTIMIZATION_ENABLED", +] + # ########################################################################## # Tools functions # ########################################################################## @@ -1213,7 +1252,8 @@ def _render_export_bzl( dotnet_module_included = False, ruby_module_path = "", sanitized_ruby_module_path = "", - ruby_module_included = False): + ruby_module_included = False, + enabled = True): """Render export.bzl content consumed by macros and BUILD files.""" repo_name_lit = json.encode(repo_name or "") service_name_lit = json.encode(service_name or "") @@ -1239,6 +1279,7 @@ def _render_export_bzl( return ( "# Generated by test_optimization_sync; unified exports for test optimization info\n" + "topt_data = {\n" + + " \"enabled\": %s,\n" % ("True" if enabled else "False") + " \"repo_name\": %s,\n" % repo_name_lit + " \"service_name\": %s,\n" % service_name_lit + " \"manifest_path\": %s,\n" % manifest_file_lit + @@ -1288,6 +1329,237 @@ def _render_module_runfiles_bzl(repo_name, manifest_root): ")\n" ) +def _render_disabled_context_json(repo_name, out_dir, env_data, runtime_name = "", runtime_version = "", runtime_arch = ""): + """Render the minimal context contract without local repository probing.""" + tags = { + "service.name": env_data.get("service") or "unnamed-service", + "env": env_data.get("environment") or "CI", + "bazel.rule_name": TEST_BAZEL_RULE_NAME, + "bazel.rule_version": TEST_BAZEL_RULE_VERSION, + "topt.sync.enabled": False, + "topt.sync.repository_name": repo_name or "", + "topt.sync.out_dir": out_dir or TEST_OPT_DIR, + } + if runtime_name: + tags["runtime.name"] = runtime_name + if runtime_version: + tags["runtime.version"] = runtime_version + if runtime_arch: + tags["runtime.architecture"] = runtime_arch + git_tags = { + "repository_url": "git.repository_url", + "branch": "git.branch", + "tag": "git.tag", + "sha": "git.commit.sha", + "head_sha": "git.commit.head.sha", + "commit_message": "git.commit.message", + "head_message": "git.commit.head.message", + "commit_author_name": "git.commit.author.name", + "commit_author_email": "git.commit.author.email", + "commit_author_date": "git.commit.author.date", + "commit_committer_name": "git.commit.committer.name", + "commit_committer_email": "git.commit.committer.email", + "commit_committer_date": "git.commit.committer.date", + "head_author_name": "git.commit.head.author.name", + "head_author_email": "git.commit.head.author.email", + "head_author_date": "git.commit.head.author.date", + "head_committer_name": "git.commit.head.committer.name", + "head_committer_email": "git.commit.head.committer.email", + "head_committer_date": "git.commit.head.committer.date", + "pr_base_branch": "git.pull_request.base_branch", + "pr_base_branch_sha": "git.pull_request.base_branch_sha", + "pr_base_branch_head_sha": "git.pull_request.base_branch_head_sha", + "pr_number": "pr.number", + } + for source_key, tag_key in git_tags.items(): + if env_data.get(source_key): + tags[tag_key] = env_data.get(source_key) + return json.encode(tags) + "\n" + +def _render_disabled_telemetry_facts_json(service_name, runtime_name, environment): + """Render the single disabled-sync telemetry fact.""" + facts = _new_telemetry_facts( + service_name, + runtime_name = runtime_name or "", + env = environment or "", + ) + _append_telemetry_count(facts, "sync.disabled") + return json.encode(facts) + "\n" + +def _render_disabled_settings_json(): + """Render the canonical disabled settings response.""" + return json.encode({ + "data": { + "attributes": { + "known_tests_enabled": False, + "test_management": {"enabled": False}, + "flaky_test_retries_enabled": False, + }, + }, + }) + "\n" + +def _render_disabled_known_tests_json(): + """Render the canonical empty known-tests response.""" + return json.encode({"data": {"attributes": {"tests": {}}}}) + "\n" + +def _render_disabled_test_management_json(): + """Render the canonical empty test-management response.""" + return json.encode({"data": {"attributes": {"modules": {}}}}) + "\n" + +def _render_disabled_flaky_tests_json(): + """Render the canonical empty flaky-tests response.""" + return json.encode({"data": []}) + "\n" + +def _render_disabled_export(repo_name, service, runtime_name, runtime_module_path, out_dir): + """Render a disabled export using the same schema as a live sync repo.""" + sanitized_module_path = sanitize_label_fragment(runtime_module_path) if runtime_module_path else "" + values = { + "go_module_path": "", + "sanitized_go_module_path": "", + "python_module_path": "", + "sanitized_python_module_path": "", + "java_module_path": "", + "sanitized_java_module_path": "", + "nodejs_module_path": "", + "sanitized_nodejs_module_path": "", + "dotnet_module_path": "", + "sanitized_dotnet_module_path": "", + "ruby_module_path": "", + "sanitized_ruby_module_path": "", + } + if runtime_name == "go": + values["go_module_path"] = runtime_module_path or "" + values["sanitized_go_module_path"] = sanitized_module_path + elif runtime_name == "python": + values["python_module_path"] = runtime_module_path or "" + values["sanitized_python_module_path"] = sanitized_module_path + elif runtime_name == "java": + values["java_module_path"] = runtime_module_path or "" + values["sanitized_java_module_path"] = sanitized_module_path + elif runtime_name == "nodejs": + values["nodejs_module_path"] = runtime_module_path or "" + values["sanitized_nodejs_module_path"] = sanitized_module_path + elif runtime_name == "dotnet": + values["dotnet_module_path"] = runtime_module_path or "" + values["sanitized_dotnet_module_path"] = sanitized_module_path + elif runtime_name == "ruby": + values["ruby_module_path"] = runtime_module_path or "" + values["sanitized_ruby_module_path"] = sanitized_module_path + return _render_export_bzl( + repo_name = repo_name, + service_name = service, + labels = [], + set_literal = "{}", + manifest_file = "%s/manifest.txt" % out_dir, + go_module_path = values["go_module_path"], + sanitized_go_module_path = values["sanitized_go_module_path"], + go_module_included = False, + python_module_path = values["python_module_path"], + sanitized_python_module_path = values["sanitized_python_module_path"], + python_module_included = False, + java_module_path = values["java_module_path"], + sanitized_java_module_path = values["sanitized_java_module_path"], + java_module_included = False, + nodejs_module_path = values["nodejs_module_path"], + sanitized_nodejs_module_path = values["sanitized_nodejs_module_path"], + nodejs_module_included = False, + dotnet_module_path = values["dotnet_module_path"], + sanitized_dotnet_module_path = values["sanitized_dotnet_module_path"], + dotnet_module_included = False, + ruby_module_path = values["ruby_module_path"], + sanitized_ruby_module_path = values["sanitized_ruby_module_path"], + ruby_module_included = False, + enabled = False, + ) + +def _render_disabled_build(settings_file, manifest_file, known_tests_file, test_management_file, flaky_tests_file, context_file, telemetry_facts_file): + """Render public disabled targets with the same file labels as live repos.""" + return ( + 'load(":module_runfiles.bzl", "topt_module_files")\n\n' + + "filegroup(\n" + + ' name = "test_optimization_files",\n' + + " srcs = %s,\n" % repr([settings_file, manifest_file, known_tests_file, test_management_file, flaky_tests_file]) + + ' visibility = ["//visibility:public"],\n' + + ")\n\n" + + "filegroup(\n" + + ' name = "test_optimization_context",\n' + + " srcs = %s,\n" % repr([context_file, telemetry_facts_file]) + + ' visibility = ["//visibility:public"],\n' + + ")\n\n" + + 'exports_files(["export.bzl", %s], visibility = ["//visibility:public"])\n' % repr(manifest_file) + ) + +def _explicit_runtime_module_path(ctx, runtime_name): + """Resolve only explicit module-path inputs for disabled exports.""" + if ctx.attr.runtime_module_path: + return ctx.attr.runtime_module_path + env_key_by_runtime = { + "go": "GO_MODULE_PATH", + "python": "PYTHON_MODULE_PATH", + "java": "JAVA_MODULE_PATH", + "nodejs": "NODEJS_MODULE_PATH", + "dotnet": "DOTNET_MODULE_PATH", + "ruby": "RUBY_MODULE_PATH", + } + return ctx.os.environ.get(env_key_by_runtime.get(runtime_name, "")) or "" + +def _write_disabled_repository(ctx, out_dir, env_data, debug): + """Write a complete no-fetch repository with stable public labels.""" + settings_file = "%s/cache/http/settings.json" % out_dir + known_tests_file = "%s/cache/http/known_tests.json" % out_dir + test_management_file = "%s/cache/http/test_management.json" % out_dir + flaky_tests_file = "%s/cache/http/flaky_tests.json" % out_dir + manifest_file = "%s/manifest.txt" % out_dir + context_file = "%s/context.json" % out_dir + telemetry_facts_file = "%s/telemetry_facts.json" % out_dir + for output_file in [settings_file, known_tests_file, test_management_file, flaky_tests_file, manifest_file, context_file, telemetry_facts_file]: + _ensure_parent_directory(ctx, output_file, debug) + + repo_name = getattr(ctx.attr, "repo_name", None) or ctx.name or "" + runtime_name = (ctx.attr.runtime_name or "").strip() + runtime_module_path = _explicit_runtime_module_path(ctx, runtime_name) + ctx.file(manifest_file, "version=1\n") + ctx.file(settings_file, _render_disabled_settings_json()) + ctx.file(known_tests_file, _render_disabled_known_tests_json()) + ctx.file(test_management_file, _render_disabled_test_management_json()) + ctx.file(flaky_tests_file, _render_disabled_flaky_tests_json()) + ctx.file( + context_file, + _render_disabled_context_json( + repo_name, + out_dir, + env_data, + runtime_name = runtime_name, + runtime_version = ctx.attr.runtime_version or "", + runtime_arch = ctx.attr.runtime_arch or "", + ), + ) + ctx.file( + telemetry_facts_file, + _render_disabled_telemetry_facts_json( + env_data.get("service") or "unnamed-service", + runtime_name, + env_data.get("environment") or "CI", + ), + ) + ctx.file("module_runfiles.bzl", _render_module_runfiles_bzl(ctx.name, _dirname(manifest_file))) + ctx.file( + "export.bzl", + _render_disabled_export(repo_name, env_data.get("service"), runtime_name, runtime_module_path, out_dir), + ) + ctx.file( + "BUILD", + _render_disabled_build( + settings_file, + manifest_file, + known_tests_file, + test_management_file, + flaky_tests_file, + context_file, + telemetry_facts_file, + ), + ) + def _partition_unix_headers(headers): """Split public headers from DD-API-KEY for secure Unix curl transport.""" public_headers = {} @@ -1565,6 +1837,17 @@ detect_runtime_module_path_for_tests = _detect_runtime_module_path dirname_for_tests = _dirname normalize_out_dir_or_fail_for_tests = _normalize_out_dir_or_fail render_export_bzl_for_tests = _render_export_bzl +render_disabled_context_json_for_tests = _render_disabled_context_json +render_disabled_flaky_tests_json_for_tests = _render_disabled_flaky_tests_json +render_disabled_known_tests_json_for_tests = _render_disabled_known_tests_json +render_disabled_settings_json_for_tests = _render_disabled_settings_json +render_disabled_test_management_json_for_tests = _render_disabled_test_management_json +render_disabled_telemetry_facts_json_for_tests = _render_disabled_telemetry_facts_json +render_disabled_export_for_tests = _render_disabled_export +render_disabled_build_for_tests = _render_disabled_build +is_test_optimization_enabled_for_tests = _is_test_optimization_enabled +repository_environ_for_tests = _TEST_OPTIMIZATION_REPOSITORY_ENVIRON +resolve_service_and_environment_for_tests = _resolve_service_and_environment fnv1a_32_for_tests = _fnv1a_32 clone_payload_with_detached_attributes_for_tests = _clone_payload_with_detached_attributes http_connect_timeout_seconds_for_tests = HTTP_CONNECT_TIMEOUT_SECONDS @@ -2264,6 +2547,28 @@ def _impl(ctx): log_info("Starting repository rule implementation") ctx.report_progress("test_optimization_sync: starting") + sync_enabled = _is_test_optimization_enabled( + ctx.attr.enabled, + ctx.attr.enabled_by_env, + ctx.os.environ.get("DD_TEST_OPTIMIZATION_ENABLED", ""), + ) + if not sync_enabled: + out_dir = _normalize_out_dir_or_fail(ctx.attr.out_dir or TEST_OPT_DIR) + resolved = _resolve_service_and_environment(ctx.attr.service, ctx.os.environ) + disabled_env_data = { + "service": resolved["service"], + "environment": resolved["environment"], + } + + # Only explicit DD_GIT_* values are allowed in disabled mode. This + # path deliberately never calls `_collect_env` or local Git helpers. + _apply_dd_git_overrides(disabled_env_data, ctx.os.environ) + _normalize_env_data(disabled_env_data) + _write_disabled_repository(ctx, out_dir, disabled_env_data, debug) + log_info("Test Optimization disabled; wrote no-fetch repository stubs") + ctx.report_progress("test_optimization_sync: disabled") + return + # ------------------------------------------------------------------ # Phase 1: Resolve/validate required inputs and output paths. # ------------------------------------------------------------------ @@ -2667,6 +2972,7 @@ def _impl(ctx): ruby_module_path = ruby_module_path, sanitized_ruby_module_path = sanitized_ruby_module_path, ruby_module_included = ruby_module_included, + enabled = True, ) ctx.file("export.bzl", export_bzl) @@ -2819,38 +3125,12 @@ test_optimization_sync = repository_rule( "known_tests": attr.bool(default = True), "test_management": attr.bool(default = True), "flaky_tests": attr.bool(default = True), + "enabled": attr.bool(default = True), + "enabled_by_env": attr.bool(default = False), "require_git_metadata": attr.bool(default = False), "debug": attr.bool(default = False), # Toggle verbose debug logging }, - environ = [ - # Keep this list intentionally broad: every env var that can influence - # generated outputs must be declared so Bazel cache keys stay correct. - # Environment variables treated as rule inputs - "DD_API_KEY", # Required: Datadog API key for authentication - "DD_SITE", # Optional: Datadog site; ex: app.datadoghq.com, datadoghq.eu - "DD_TEST_OPTIMIZATION_AGENTLESS_URL", # Optional: override Datadog API base URL (test/dev) - "DD_TEST_OPTIMIZATION_HTTP_CONNECT_TIMEOUT_SECONDS", # Optional: override connect timeout - "DD_TEST_OPTIMIZATION_HTTP_MAX_TIME_SECONDS", # Optional: override request max time - "DD_TEST_OPTIMIZATION_HTTP_RETRY_ATTEMPTS", # Optional: override retry attempts - "DD_TEST_OPTIMIZATION_HTTP_RETRY_DELAY_SECONDS", # Optional: override retry delay - "DD_TEST_OPTIMIZATION_HTTP_EXECUTE_TIMEOUT_BUFFER_SECONDS", # Optional: override outer timeout buffer - "FETCH_SALT", # Optional: cache-busting salt to force re-fetch - "GIT_DIRTY", # Optional: working tree state; triggers refetch on change - "GO_MODULE_PATH", # Optional: explicit Go module path override for export.bzl - "PYTHON_MODULE_PATH", # Optional: explicit Python module path override for export.bzl - "JAVA_MODULE_PATH", # Optional: explicit Java module path override for export.bzl - "NODEJS_MODULE_PATH", # Optional: explicit NodeJS module path override for export.bzl - "DOTNET_MODULE_PATH", # Optional: explicit Dotnet module path override for export.bzl - "RUBY_MODULE_PATH", # Optional: explicit Ruby module path override for export.bzl - # Host OS hints used for cross-platform behavior and request configuration - "OS", # Windows OS marker (used in _is_windows and _detect_os_info) - "ComSpec", # Windows command processor path - "COMSPEC", # Alternate casing for Windows command processor - "PROCESSOR_ARCHITECTURE", # Windows arch detection - "PROCESSOR_ARCHITEW6432", # Windows WOW64 arch detection - # Shared CI/git metadata inputs are centralized in test_optimization_sync_env.bzl - # so provider extraction and repository_rule cache keys cannot drift apart. - ] + _ALL_SYNC_ENV_KEYS, + environ = _TEST_OPTIMIZATION_REPOSITORY_ENVIRON, # Repository rules run during workspace/module resolution; keep local=True # so host tooling/env detection remains predictable across environments. local = True, @@ -2926,6 +3206,8 @@ def _test_optimization_sync_extension_impl(module_ctx): known_tests = test_optimization_call.known_tests, test_management = test_optimization_call.test_management, flaky_tests = test_optimization_call.flaky_tests, + enabled = test_optimization_call.enabled, + enabled_by_env = test_optimization_call.enabled_by_env, require_git_metadata = test_optimization_call.require_git_metadata, debug = call_debug, ) @@ -2956,6 +3238,8 @@ test_optimization_sync_extension = module_extension( "known_tests": attr.bool(default = True), "test_management": attr.bool(default = True), "flaky_tests": attr.bool(default = True), + "enabled": attr.bool(default = True), + "enabled_by_env": attr.bool(default = False), "require_git_metadata": attr.bool(default = False), "debug": attr.bool(default = False), }), diff --git a/tools/core/test_optimization_sync_env.bzl b/tools/core/test_optimization_sync_env.bzl index c420db1b..f2645cfb 100644 --- a/tools/core/test_optimization_sync_env.bzl +++ b/tools/core/test_optimization_sync_env.bzl @@ -384,12 +384,20 @@ def normalize_ref(name): break return name +def resolve_service_and_environment(attr_service, environ): + """Resolve service/environment without probing the local checkout.""" + return { + "service": attr_service or environ.get("DD_SERVICE") or "unnamed-service", + "environment": environ.get("DD_ENV") or "CI", + } + def _new_env_data(attr_service, environ): + resolved = resolve_service_and_environment(attr_service, environ) return { "dd_site": environ.get("DD_SITE") or "", "dd_api_base": environ.get("DD_TEST_OPTIMIZATION_AGENTLESS_URL") or "", - "service": (attr_service or environ.get("DD_SERVICE") or "unnamed-service"), - "environment": environ.get("DD_ENV") or "CI", + "service": resolved["service"], + "environment": resolved["environment"], "repository_url": "", "branch": "", "tag": "", diff --git a/tools/dev/materialize_rules_go_fork.py b/tools/dev/materialize_rules_go_fork.py index c342a512..fb349e50 100644 --- a/tools/dev/materialize_rules_go_fork.py +++ b/tools/dev/materialize_rules_go_fork.py @@ -311,6 +311,11 @@ def check_selection(registry: ForkRegistry, selection: ForkSelection) -> int: return 0 +def render_upstream_ids(upstream_ids: list[str]) -> bytes: + """Render shell-consumable upstream ids with platform-independent LF endings.""" + return "".join("%s\n" % upstream_id for upstream_id in upstream_ids).encode("utf-8") + + def main(argv: list[str] | None = None) -> int: """Parse CLI arguments and run materializer commands.""" parser = argparse.ArgumentParser(description=__doc__) @@ -341,8 +346,7 @@ def main(argv: list[str] | None = None) -> int: print(repo_relative_path(registry.repo_root, selection.tree_path)) return 0 if args.command == "list-upstreams": - for upstream_id in registry.upstream_ids(): - print(upstream_id) + sys.stdout.buffer.write(render_upstream_ids(registry.upstream_ids())) return 0 if args.command == "check": selections = registry.selections() if args.all else [resolve_selection(registry, args)] diff --git a/tools/tests/core/BUILD.bazel b/tools/tests/core/BUILD.bazel index b4b31fcb..66ba93cc 100644 --- a/tools/tests/core/BUILD.bazel +++ b/tools/tests/core/BUILD.bazel @@ -55,6 +55,8 @@ load( ) load( "//tools/tests/core:test_multi_sync_utils.bzl", + "build_multi_sync_specs_deduplicates_sanitized_service_keys_test", + "build_multi_sync_specs_propagates_non_default_enablement_test", "compute_repo_names_test", "compute_service_keys_dedups_collisions_test", "compute_service_keys_edge_cases_test", @@ -82,6 +84,17 @@ load( "python_missing_rules_python_repo_failure_test", "python_missing_rules_python_repo_target_rule", ) +load( + "//tools/tests/core:test_sync_disabled_utils.bzl", + "disabled_cache_payloads_contract_test", + "disabled_context_contract_test", + "disabled_export_and_build_shape_test", + "disabled_telemetry_contract_test", + "enabled_false_values_test", + "enabled_truthy_values_test", + "repository_environment_contains_bootstrap_inputs_test", + "service_environment_resolution_test", +) load( "//tools/tests/core:test_sync_utils.bzl", "apply_dd_git_overrides_test", @@ -112,8 +125,8 @@ load( "decode_json_object_non_json_target_rule", "decode_json_object_valid_test", "dirname_test", - "example_stub_custom_out_dir_test", "example_stub_context_contains_doctor_metadata_test", + "example_stub_custom_out_dir_test", "example_stub_export_manifest_path_test", "example_stub_export_string_escaping_test", "example_stub_includes_manifest_in_files_test", @@ -248,6 +261,12 @@ load( "removed_complete_variant_target_rule", ) +disabled_cache_payloads_contract_test( + name = "disabled_cache_payloads_contract_test", + size = "small", + timeout = "short", +) + filegroup( name = "legacy_context_wrapper", srcs = ["//tools/tests/core/testdata:context.json"], @@ -619,6 +638,48 @@ module_label_map_flaky_modules_test( timeout = "short", ) +enabled_truthy_values_test( + name = "enabled_truthy_values_test", + size = "small", + timeout = "short", +) + +enabled_false_values_test( + name = "enabled_false_values_test", + size = "small", + timeout = "short", +) + +repository_environment_contains_bootstrap_inputs_test( + name = "repository_environment_contains_bootstrap_inputs_test", + size = "small", + timeout = "short", +) + +service_environment_resolution_test( + name = "service_environment_resolution_test", + size = "small", + timeout = "short", +) + +disabled_context_contract_test( + name = "disabled_context_contract_test", + size = "small", + timeout = "short", +) + +disabled_telemetry_contract_test( + name = "disabled_telemetry_contract_test", + size = "small", + timeout = "short", +) + +disabled_export_and_build_shape_test( + name = "disabled_export_and_build_shape_test", + size = "small", + timeout = "short", +) + normalize_ref_test( name = "normalize_ref_test", size = "small", @@ -1033,6 +1094,18 @@ validate_service_name_too_long_failure_test( target_under_test = ":validate_service_name_too_long_target", ) +build_multi_sync_specs_propagates_non_default_enablement_test( + name = "build_multi_sync_specs_propagates_non_default_enablement_test", + size = "small", + timeout = "short", +) + +build_multi_sync_specs_deduplicates_sanitized_service_keys_test( + name = "build_multi_sync_specs_deduplicates_sanitized_service_keys_test", + size = "small", + timeout = "short", +) + compute_service_keys_dedups_collisions_test( name = "compute_service_keys_dedups_collisions_test", size = "small", @@ -1443,6 +1516,8 @@ test_suite( ":archive_archive_specs_test", ":base_git_specs_test", ":bash_curl_retry_flags_test", + ":build_multi_sync_specs_deduplicates_sanitized_service_keys_test", + ":build_multi_sync_specs_propagates_non_default_enablement_test", ":clone_payload_with_detached_attributes_test", ":clone_payload_with_nested_structure_test", ":codeowners_compile_regex_edge_cases_test", @@ -1478,14 +1553,20 @@ test_suite( ":dedup_keys_test", ":default_context_data_test", ":dirname_test", + ":disabled_cache_payloads_contract_test", + ":disabled_context_contract_test", + ":disabled_export_and_build_shape_test", + ":disabled_telemetry_contract_test", ":doctor_controlled_attr_failure_test", ":doctor_kwargs_test", ":duplicate_names_failure_test", ":empty_name_failure_test", + ":enabled_false_values_test", + ":enabled_truthy_values_test", + ":example_stub_context_contains_doctor_metadata_test", ":example_stub_custom_out_dir_test", ":example_stub_export_manifest_path_test", ":example_stub_export_string_escaping_test", - ":example_stub_context_contains_doctor_metadata_test", ":example_stub_includes_manifest_in_files_test", ":example_stub_manifest_content_matches_sync_contract_test", ":example_stub_module_targets_test", @@ -1497,6 +1578,8 @@ test_suite( ":explicit_upstream_base_specs_test", ":export_bzl_escaping_test", ":export_bzl_manifest_path_test", + ":repository_environment_contains_bootstrap_inputs_test", + ":service_environment_resolution_test", ":first_env_ctx_test", ":first_env_from_environ_test", ":fnv1a_symbol_distinguishes_common_symbols_test", diff --git a/tools/tests/core/test_multi_sync_utils.bzl b/tools/tests/core/test_multi_sync_utils.bzl index 349df4f0..33424a2a 100644 --- a/tools/tests/core/test_multi_sync_utils.bzl +++ b/tools/tests/core/test_multi_sync_utils.bzl @@ -8,6 +8,7 @@ load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts", "unittest") load( "//tools/core:test_optimization_multi_sync.bzl", + "build_multi_sync_repo_specs_for_tests", "compute_multi_repo_names_for_tests", "compute_multi_service_keys_for_tests", "record_multi_repo_owner_or_fail_for_tests", @@ -15,6 +16,50 @@ load( "render_multi_aggregate_export_bzl_for_tests", ) +def _build_multi_sync_specs_propagates_non_default_enablement_test(ctx): + env = unittest.begin(ctx) + specs = build_multi_sync_repo_specs_for_tests( + name = "test_optimization_data_go", + services = ["service-a", "service-b"], + runtime_name = "go", + runtime_version = "1.25.9", + runtime_module_path = "example.com/repo", + enabled = False, + enabled_by_env = True, + flaky_tests = False, + ) + asserts.equals(env, 2, len(specs)) + asserts.equals(env, False, specs[0]["enabled"]) + asserts.equals(env, True, specs[0]["enabled_by_env"]) + asserts.equals(env, False, specs[0]["flaky_tests"]) + asserts.equals(env, "test_optimization_data_go_service_a", specs[0]["repo"]) + asserts.equals(env, "service-a", specs[0]["service"]) + asserts.equals(env, "test_optimization_data_go_service_b", specs[1]["repo"]) + return unittest.end(env) + +def _build_multi_sync_specs_deduplicates_sanitized_service_keys_test(ctx): + env = unittest.begin(ctx) + specs = build_multi_sync_repo_specs_for_tests( + name = "test_optimization_data_go", + services = ["service-a", "service.a", "service_a"], + runtime_name = "go", + runtime_version = "1.25.9", + runtime_module_path = "example.com/repo", + enabled_by_env = True, + ) + repo_names = [spec["repo"] for spec in specs] + asserts.equals( + env, + [ + "test_optimization_data_go_service_a", + "test_optimization_data_go_service_a_2", + "test_optimization_data_go_service_a_3", + ], + repo_names, + ) + asserts.equals(env, 3, len({name: True for name in repo_names})) + return unittest.end(env) + def _compute_service_keys_dedups_collisions_test(ctx): """Validate sanitized-key collision deduping for multi-service inputs.""" env = unittest.begin(ctx) @@ -119,6 +164,8 @@ def _record_multi_repo_owner_duplicate_failure_test_impl(ctx): compute_service_keys_dedups_collisions_test = unittest.make(_compute_service_keys_dedups_collisions_test) compute_service_keys_edge_cases_test = unittest.make(_compute_service_keys_edge_cases_test) compute_repo_names_test = unittest.make(_compute_repo_names_test) +build_multi_sync_specs_propagates_non_default_enablement_test = unittest.make(_build_multi_sync_specs_propagates_non_default_enablement_test) +build_multi_sync_specs_deduplicates_sanitized_service_keys_test = unittest.make(_build_multi_sync_specs_deduplicates_sanitized_service_keys_test) record_multi_repo_owner_success_test = unittest.make(_record_multi_repo_owner_success_test) render_multi_aggregate_bzl_contains_expected_targets_test = unittest.make(_render_multi_aggregate_bzl_contains_expected_targets_test) render_multi_aggregate_export_bzl_reexports_mapping_test = unittest.make(_render_multi_aggregate_export_bzl_reexports_mapping_test) diff --git a/tools/tests/core/test_sync_disabled_utils.bzl b/tools/tests/core/test_sync_disabled_utils.bzl new file mode 100644 index 00000000..4a574ae8 --- /dev/null +++ b/tools/tests/core/test_sync_disabled_utils.bzl @@ -0,0 +1,202 @@ +# Unless explicitly stated otherwise all files in this repository are licensed under +# the Apache 2.0 License. +# +# This product includes software developed at Datadog +# (https://www.datadoghq.com/) Copyright 2025-Present Datadog, Inc. + +"""Unit tests for disabled Test Optimization repository rendering.""" + +load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest") +load("//tools/core:common_utils.bzl", "RULES_VERSION") +load( + "//tools/core:test_optimization_sync.bzl", + "is_test_optimization_enabled_for_tests", + "render_disabled_build_for_tests", + "render_disabled_context_json_for_tests", + "render_disabled_export_for_tests", + "render_disabled_flaky_tests_json_for_tests", + "render_disabled_known_tests_json_for_tests", + "render_disabled_settings_json_for_tests", + "render_disabled_telemetry_facts_json_for_tests", + "render_disabled_test_management_json_for_tests", + "repository_environ_for_tests", + "resolve_service_and_environment_for_tests", +) + +def _enabled_truthy_values_test(ctx): + env = unittest.begin(ctx) + for value in ["1", "true", "True", "TRUE", "yes", "Yes", "YES", "on", "ON"]: + asserts.true( + env, + is_test_optimization_enabled_for_tests(True, True, value), + "expected truthy value %s" % value, + ) + return unittest.end(env) + +def _enabled_false_values_test(ctx): + env = unittest.begin(ctx) + for value in ["", "0", "false", "False", "no", "off", "disabled"]: + asserts.false( + env, + is_test_optimization_enabled_for_tests(True, True, value), + "expected false value %s" % value, + ) + asserts.false(env, is_test_optimization_enabled_for_tests(False, False, "1")) + asserts.true(env, is_test_optimization_enabled_for_tests(True, False, "")) + return unittest.end(env) + +def _repository_environment_contains_bootstrap_inputs_test(ctx): + env = unittest.begin(ctx) + for key in [ + "DD_API_KEY", + "DD_TEST_OPTIMIZATION_HTTP_MAX_TIME_SECONDS", + "FETCH_SALT", + "GO_MODULE_PATH", + "OS", + "DD_GIT_COMMIT_SHA", + "DD_TEST_OPTIMIZATION_ENABLED", + ]: + asserts.true(env, key in repository_environ_for_tests, "missing repository environ key %s" % key) + return unittest.end(env) + +def _service_environment_resolution_test(ctx): + env = unittest.begin(ctx) + asserts.equals( + env, + {"service": "attr-service", "environment": "attr-env"}, + resolve_service_and_environment_for_tests("attr-service", {"DD_SERVICE": "env-service", "DD_ENV": "attr-env"}), + ) + asserts.equals( + env, + {"service": "env-service", "environment": "env"}, + resolve_service_and_environment_for_tests("", {"DD_SERVICE": "env-service", "DD_ENV": "env"}), + ) + asserts.equals( + env, + {"service": "unnamed-service", "environment": "CI"}, + resolve_service_and_environment_for_tests("", {}), + ) + return unittest.end(env) + +def _disabled_context_contract_test(ctx): + env = unittest.begin(ctx) + content = render_disabled_context_json_for_tests( + "test_optimization_data", + "custom_topt", + { + "service": "service-name", + "environment": "test", + "repository_url": "https://github.com/example/repo", + "sha": "abc123", + }, + runtime_name = "go", + runtime_version = "1.25.0", + runtime_arch = "amd64", + ) + decoded = json.decode(content) + asserts.equals(env, { + "bazel.rule_name": "datadog-rules-test-optimization", + "bazel.rule_version": RULES_VERSION, + "env": "test", + "git.commit.sha": "abc123", + "git.repository_url": "https://github.com/example/repo", + "runtime.architecture": "amd64", + "runtime.name": "go", + "runtime.version": "1.25.0", + "service.name": "service-name", + "topt.sync.enabled": False, + "topt.sync.out_dir": "custom_topt", + "topt.sync.repository_name": "test_optimization_data", + }, decoded) + + without_runtime = json.decode(render_disabled_context_json_for_tests( + "test_optimization_data", + "custom_topt", + {"service": "service-name", "environment": "test"}, + )) + asserts.false(env, "runtime.name" in without_runtime) + asserts.false(env, "runtime.version" in without_runtime) + asserts.false(env, "runtime.architecture" in without_runtime) + return unittest.end(env) + +def _disabled_telemetry_contract_test(ctx): + env = unittest.begin(ctx) + decoded = json.decode(render_disabled_telemetry_facts_json_for_tests("service-name", "go", "CI")) + asserts.equals(env, { + "counts": [{"name": "sync.disabled", "value": 1, "tags": []}], + "distributions": [], + "env": "CI", + "runtime_name": "go", + "schema_version": 1, + "service_name": "service-name", + }, decoded) + return unittest.end(env) + +def _disabled_cache_payloads_contract_test(ctx): + env = unittest.begin(ctx) + asserts.equals(env, { + "data": { + "attributes": { + "flaky_test_retries_enabled": False, + "known_tests_enabled": False, + "test_management": {"enabled": False}, + }, + }, + }, json.decode(render_disabled_settings_json_for_tests())) + asserts.equals( + env, + {"data": {"attributes": {"tests": {}}}}, + json.decode(render_disabled_known_tests_json_for_tests()), + ) + asserts.equals( + env, + {"data": {"attributes": {"modules": {}}}}, + json.decode(render_disabled_test_management_json_for_tests()), + ) + asserts.equals(env, {"data": []}, json.decode(render_disabled_flaky_tests_json_for_tests())) + return unittest.end(env) + +def _disabled_export_and_build_shape_test(ctx): + env = unittest.begin(ctx) + export = render_disabled_export_for_tests( + "test_optimization_data", + "go-service", + "go", + "example.com/repo", + "custom_topt", + ) + asserts.true(env, '"repo_name": "test_optimization_data"' in export) + asserts.true(env, '"service_name": "go-service"' in export) + asserts.true(env, '"enabled": False' in export) + asserts.true(env, '"manifest_path": "custom_topt/manifest.txt"' in export) + asserts.true(env, '"module_path": "example.com/repo"' in export) + + build = render_disabled_build_for_tests( + "custom_topt/cache/http/settings.json", + "custom_topt/manifest.txt", + "custom_topt/cache/http/known_tests.json", + "custom_topt/cache/http/test_management.json", + "custom_topt/cache/http/flaky_tests.json", + "custom_topt/context.json", + "custom_topt/telemetry_facts.json", + ) + for fragment in [ + 'name = "test_optimization_files"', + 'name = "test_optimization_context"', + "custom_topt/cache/http/settings.json", + "custom_topt/cache/http/known_tests.json", + "custom_topt/cache/http/test_management.json", + "custom_topt/cache/http/flaky_tests.json", + "custom_topt/telemetry_facts.json", + ]: + asserts.true(env, fragment in build, "missing BUILD fragment %s" % fragment) + return unittest.end(env) + +enabled_truthy_values_test = unittest.make(_enabled_truthy_values_test) +enabled_false_values_test = unittest.make(_enabled_false_values_test) +repository_environment_contains_bootstrap_inputs_test = unittest.make(_repository_environment_contains_bootstrap_inputs_test) +service_environment_resolution_test = unittest.make(_service_environment_resolution_test) +disabled_context_contract_test = unittest.make(_disabled_context_contract_test) +disabled_cache_payloads_contract_test = unittest.make(_disabled_cache_payloads_contract_test) +disabled_telemetry_contract_test = unittest.make(_disabled_telemetry_contract_test) +disabled_export_and_build_shape_test = unittest.make(_disabled_export_and_build_shape_test) diff --git a/tools/tests/core/test_sync_utils.bzl b/tools/tests/core/test_sync_utils.bzl index 8516d447..2a115189 100644 --- a/tools/tests/core/test_sync_utils.bzl +++ b/tools/tests/core/test_sync_utils.bzl @@ -1247,6 +1247,7 @@ def _export_bzl_manifest_path_test(ctx): sanitized_ruby_module_path = "apps_ruby_service", ruby_module_included = False, ) + asserts.true(env, '"enabled": True' in content) asserts.true(env, "\"service_name\": \"service-name\"" in content) asserts.true(env, "\"manifest_path\": \".testoptimization/manifest.txt\"" in content) asserts.true(env, "\"runtimes\": {" in content) @@ -1527,7 +1528,9 @@ def _example_stub_export_manifest_path_test(ctx): go_module_path = "example.com/workspace-go-integration", go_sanitized_module_path = "example_com_workspace_go_integration", go_module_included = True, + enabled = False, ) + asserts.true(env, '"enabled": False' in content) asserts.true(env, '"repo_name": "test_optimization_data"' in content) asserts.true(env, '"manifest_path": "custom_topt/manifest.txt"' in content) asserts.true(env, '"service_name": "workspace-go-service"' in content) diff --git a/tools/tests/example_stub_repo.bzl b/tools/tests/example_stub_repo.bzl index 74609c05..5f4a9f2e 100644 --- a/tools/tests/example_stub_repo.bzl +++ b/tools/tests/example_stub_repo.bzl @@ -70,6 +70,9 @@ def _render_stub_module_runfiles_bzl(repo_name, manifest_root): " test_management = getattr(ctx.file, \"test_management\", None)\n" + " if test_management:\n" + " files.append(test_management)\n" + + " flaky_tests = getattr(ctx.file, \"flaky_tests\", None)\n" + + " if flaky_tests:\n" + + " files.append(flaky_tests)\n" + " return DefaultInfo(files = depset(files), runfiles = ctx.runfiles(files = files))\n" + "\n" + "topt_module_files = rule(\n" + @@ -79,6 +82,7 @@ def _render_stub_module_runfiles_bzl(repo_name, manifest_root): " \"manifest\": attr.label(allow_single_file = True, mandatory = True),\n" + " \"known_tests\": attr.label(allow_single_file = True),\n" + " \"test_management\": attr.label(allow_single_file = True),\n" + + " \"flaky_tests\": attr.label(allow_single_file = True),\n" + " },\n" + ")\n" ) @@ -91,7 +95,8 @@ def _render_stub_export( manifest_path, go_module_path, go_sanitized_module_path, - go_module_included): + go_module_included, + enabled = True): """Render export.bzl content for the stub repository.""" mapping_lines = [] for key in service_keys: @@ -99,6 +104,7 @@ def _render_stub_export( return ( "topt_data = {\n" + + " \"enabled\": %s,\n" % ("True" if enabled else "False") + " \"repo_name\": %s,\n" % _bzl_string_literal(repo_name) + " \"service_name\": %s,\n" % _bzl_string_literal(service_name) + " \"manifest_path\": %s,\n" % _bzl_string_literal(manifest_path) + @@ -151,7 +157,8 @@ def _render_stub_build( telemetry_facts, service_keys = None, module_labels = None, - manifest_root = ".testoptimization"): + manifest_root = ".testoptimization", + flaky_tests = None): """Render BUILD content for stub repo targets.""" def _append_filegroups(name_suffix, srcs): @@ -170,7 +177,9 @@ def _render_stub_build( ")\n\n", ) - files_srcs = [settings, manifest, known_tests, test_management] + if flaky_tests == None: + flaky_tests = "%s/cache/http/flaky_tests.json" % manifest_root + files_srcs = [settings, manifest, known_tests, test_management, flaky_tests] lines = ['load(":module_runfiles.bzl", "topt_module_files")\n\n'] _append_filegroups("", files_srcs) for key in list(service_keys or []): @@ -183,6 +192,7 @@ def _render_stub_build( (" manifest = %s,\n" % repr(manifest)) + (" known_tests = %s,\n" % repr(_module_known_tests_path(manifest_root, label))) + (" test_management = %s,\n" % repr(_module_test_management_path(manifest_root, label))) + + (" flaky_tests = %s,\n" % repr(flaky_tests)) + ' visibility = ["//visibility:public"],\n' + ")\n\n", ) @@ -205,13 +215,18 @@ def _example_stub_repo_impl(ctx): settings = "%s/cache/http/settings.json" % out_dir known_tests = "%s/cache/http/known_tests.json" % out_dir test_management = "%s/cache/http/test_management.json" % out_dir + flaky_tests = "%s/cache/http/flaky_tests.json" % out_dir context = "%s/context.json" % out_dir telemetry_facts = "%s/telemetry_facts.json" % out_dir ctx.file(manifest, _stub_manifest_content()) - ctx.file(settings, "{}\n") + ctx.file( + settings, + '{"data": {"attributes": {"known_tests_enabled": false, "test_management": {"enabled": false}, "flaky_test_retries_enabled": false}}}\n', + ) ctx.file(known_tests, '{"data": {"attributes": {"tests": {}}}}\n') ctx.file(test_management, '{"data": {"attributes": {"modules": {}}}}\n') + ctx.file(flaky_tests, '{"data": []}\n') ctx.file(context, _render_stub_context(ctx.attr.service_name)) ctx.file(telemetry_facts, _render_stub_telemetry_facts(ctx.attr.service_name)) @@ -242,6 +257,7 @@ def _example_stub_repo_impl(ctx): go_module_path = ctx.attr.go_module_path, go_sanitized_module_path = ctx.attr.go_sanitized_module_path, go_module_included = ctx.attr.go_module_included, + enabled = ctx.attr.enabled, ) ctx.file("export.bzl", export) ctx.file("module_runfiles.bzl", _render_stub_module_runfiles_bzl(ctx.name, out_dir)) @@ -256,12 +272,14 @@ def _example_stub_repo_impl(ctx): service_keys = service_keys, module_labels = module_labels, manifest_root = out_dir, + flaky_tests = flaky_tests, ) ctx.file("BUILD", build) example_stub_repo = repository_rule( implementation = _example_stub_repo_impl, attrs = { + "enabled": attr.bool(default = True), "go_module_included": attr.bool(default = False), "go_module_path": attr.string(default = "example.com/stub"), "go_sanitized_module_path": attr.string(default = "example_com_stub"), @@ -279,6 +297,7 @@ def _example_stub_repo_extension_impl(module_ctx): for call in mod.tags.example_stub_repo: example_stub_repo( name = call.name, + enabled = call.enabled, go_module_included = call.go_module_included, go_module_path = call.go_module_path, go_sanitized_module_path = call.go_sanitized_module_path, @@ -293,6 +312,7 @@ example_stub_repo_extension = module_extension( implementation = _example_stub_repo_extension_impl, tag_classes = { "example_stub_repo": tag_class(attrs = { + "enabled": attr.bool(default = True), "go_module_included": attr.bool(default = False), "go_module_path": attr.string(default = "example.com/stub"), "go_sanitized_module_path": attr.string(default = "example_com_stub"), diff --git a/tools/tests/integration/go_integration_mock_server.sh b/tools/tests/integration/go_integration_mock_server.sh new file mode 100644 index 00000000..7ca3e83e --- /dev/null +++ b/tools/tests/integration/go_integration_mock_server.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash + +# Shared deterministic metadata server lifecycle for Go integration smokes. + +GO_INTEGRATION_MOCK_SERVER_PID="" +GO_INTEGRATION_MOCK_SERVER_LOG="" +declare -a GO_INTEGRATION_MOCK_REPO_ENVS=() + +stop_go_integration_mock_server() { + if [[ -n "$GO_INTEGRATION_MOCK_SERVER_PID" ]]; then + kill "$GO_INTEGRATION_MOCK_SERVER_PID" >/dev/null 2>&1 || true + wait "$GO_INTEGRATION_MOCK_SERVER_PID" >/dev/null 2>&1 || true + GO_INTEGRATION_MOCK_SERVER_PID="" + fi +} + +start_go_integration_mock_server() { + local tmp_root="$1" + local module_identifier="$2" + local fixtures_dir="$tmp_root/enabled-metadata-fixtures" + local port + local ready=0 + local server_out="$tmp_root/enabled-metadata-server.out" + + rm -rf "$fixtures_dir" + mkdir -p "$fixtures_dir" + cp "$REPO_ROOT/tools/tests/integration/fixtures/settings.json" "$fixtures_dir/settings.json" + "$PYTHON" - "$fixtures_dir" "$module_identifier" <<'PY' +import json +import pathlib +import sys + +fixtures_dir = pathlib.Path(sys.argv[1]) +module_identifier = sys.argv[2] +(fixtures_dir / "known_tests.json").write_text(json.dumps({ + "data": { + "id": "1", + "type": "ci_app_libraries_tests", + "attributes": {"tests": {module_identifier: {"suite": {"test": {}}}}}, + }, +}), encoding="utf-8") +(fixtures_dir / "test_management.json").write_text(json.dumps({ + "data": { + "id": "1", + "type": "ci_app_libraries_tests", + "attributes": {"modules": {module_identifier: {"tests": {}}}}, + }, +}), encoding="utf-8") +PY + + port="$("$PYTHON" - <<'PY' +import socket + +sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +sock.bind(("127.0.0.1", 0)) +print(sock.getsockname()[1]) +sock.close() +PY +)" + GO_INTEGRATION_MOCK_SERVER_LOG="$tmp_root/enabled-metadata-requests.jsonl" + : >"$GO_INTEGRATION_MOCK_SERVER_LOG" + + "$PYTHON" -u "$REPO_ROOT/tools/tests/integration/mock_dd_server.py" \ + --fixtures "$fixtures_dir" \ + --log "$GO_INTEGRATION_MOCK_SERVER_LOG" \ + --port "$port" >"$server_out" 2>&1 & + GO_INTEGRATION_MOCK_SERVER_PID=$! + + for _ in $(seq 1 100); do + if "$PYTHON" - "$port" <<'PY' +import socket +import sys + +sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +sock.settimeout(0.2) +try: + sock.connect(("127.0.0.1", int(sys.argv[1]))) +except OSError: + raise SystemExit(1) +finally: + sock.close() +PY + then + ready=1 + break + fi + if ! kill -0 "$GO_INTEGRATION_MOCK_SERVER_PID" >/dev/null 2>&1; then + cat "$server_out" >&2 + echo "error: enabled metadata server exited before becoming ready" >&2 + return 1 + fi + sleep 0.1 + done + + if [[ "$ready" != "1" ]]; then + cat "$server_out" >&2 + echo "error: timed out waiting for enabled metadata server" >&2 + return 1 + fi + + GO_INTEGRATION_MOCK_REPO_ENVS=( + --repo_env=DD_API_KEY=mock + --repo_env=DD_TEST_OPTIMIZATION_AGENTLESS_URL="http://127.0.0.1:$port" + ) +} + +assert_go_integration_metadata_requests() { + "$PYTHON" - "$GO_INTEGRATION_MOCK_SERVER_LOG" <<'PY' +import json +import sys + +required = { + "/api/v2/libraries/tests/services/setting", + "/api/v2/ci/libraries/tests", + "/api/v2/test/libraries/test-management/tests", +} +with open(sys.argv[1], encoding="utf-8") as request_log: + observed = {json.loads(line)["path"] for line in request_log if line.strip()} +missing = sorted(required - observed) +if missing: + raise SystemExit(f"enabled metadata smoke did not call required endpoints: {missing}; observed={sorted(observed)}") +PY +} diff --git a/tools/tests/integration/run_bzlmod_go_integration.sh b/tools/tests/integration/run_bzlmod_go_integration.sh index af02bb68..c09ecc4a 100755 --- a/tools/tests/integration/run_bzlmod_go_integration.sh +++ b/tools/tests/integration/run_bzlmod_go_integration.sh @@ -42,6 +42,11 @@ BAZEL_OUTPUT_USER_ROOT="${BAZEL_OUTPUT_USER_ROOT:-$TMP_ROOT/bazel_output_user_ro GO_VERSION="${GO_VERSION:-1.25.0}" ORCHESTRION_VERSION="${ORCHESTRION_VERSION:-v1.9.0}" ORCHESTRION_MODE="${ORCHESTRION_MODE:-general}" +ORCHESTRION_DISABLED_SENTINEL="${ORCHESTRION_DISABLED_SENTINEL:-0}" +ORCHESTRION_DISABLED_SENTINEL_VERSION="v0.0.0-rto-disabled-fetch-sentinel" +WINDOWS_DISABLED_SMOKE_ONLY="${WINDOWS_DISABLED_SMOKE_ONLY:-0}" +WINDOWS_ENABLED_SMOKE_ONLY="${WINDOWS_ENABLED_SMOKE_ONLY:-0}" +WINDOWS_CONFIG_TRANSITION_ONLY="${WINDOWS_CONFIG_TRANSITION_ONLY:-0}" DD_TRACE_GO_VERSION="${DD_TRACE_GO_VERSION:-v2.9.0}" SERVICE_NAME="${SERVICE_NAME:-bzlmod-go-service}" MODULE_IMPORTPATH="${MODULE_IMPORTPATH:-example.com/bzlmod-go-integration}" @@ -80,14 +85,47 @@ BAZEL_EXTRA_ARGS+=( "--repo_env=DD_GIT_COMMIT_SHA=${FIXTURE_GIT_COMMIT_SHA}" ) +source "$REPO_ROOT/tools/tests/integration/go_integration_mock_server.sh" + +shutdown_bazel_workspace_servers() { + local workspace_dir + + # Bazel owns one server per generated workspace. Windows cannot remove their + # output bases until every server releases its files. + if [[ -d "$WORKSPACE_ROOT" ]]; then + for workspace_dir in "$WORKSPACE_ROOT"/*; do + [[ -d "$workspace_dir" ]] || continue + ( + cd "$workspace_dir" + USE_BAZEL_VERSION="$BAZEL_VERSION" "$BAZEL" --output_user_root="$BAZEL_OUTPUT_USER_ROOT" shutdown + ) >/dev/null 2>&1 || true + done + fi + + ( + cd "$REPO_ROOT" + USE_BAZEL_VERSION="$BAZEL_VERSION" "$BAZEL" --output_user_root="$BAZEL_OUTPUT_USER_ROOT" shutdown + ) >/dev/null 2>&1 || true +} + cleanup() { - USE_BAZEL_VERSION="$BAZEL_VERSION" "$BAZEL" --output_user_root="$BAZEL_OUTPUT_USER_ROOT" shutdown >/dev/null 2>&1 || true + local status=$? + + set +e + stop_go_integration_mock_server + shutdown_bazel_workspace_servers if [[ "${KEEP_TMP:-0}" == "1" ]]; then echo "KEEP_TMP=1: workspace fixtures left at $TMP_ROOT" - return + return "$status" fi chmod -R u+w "$TMP_ROOT" 2>/dev/null || true - rm -rf "$TMP_ROOT" + if ! rm -rf "$TMP_ROOT" 2>/dev/null; then + sleep 2 + if ! rm -rf "$TMP_ROOT" 2>/dev/null; then + echo "warning: unable to remove temporary workspace $TMP_ROOT" >&2 + fi + fi + return "$status" } trap cleanup EXIT INT TERM HUP @@ -160,6 +198,20 @@ sha256_file() { exit 1 } +file_uri() { + local path="$1" + + if command -v cygpath >/dev/null 2>&1; then + path="$(cygpath -m "$path")" + fi + "$PYTHON" - "$path" <<'PY' +from pathlib import Path +import sys + +print(Path(sys.argv[1]).resolve().as_uri()) +PY +} + wall_time_ns() { "$PYTHON" - <<'PY' import time @@ -368,7 +420,7 @@ PY run_bep_freshness_scenario() { local ws_dir="$1" local mode="$2" - local -a bzlmod_flags=(--enable_bzlmod) + local -a bzlmod_flags=(--enable_bzlmod --config=test-optimization) local bep_dir="$ws_dir/.topt/bep-${mode}" local fresh_bep="$bep_dir/fresh.bep.json" local cached_bep="$bep_dir/cached.bep.json" @@ -509,17 +561,26 @@ create_fixture_archive() { tar -czf "$ARCHIVE_PATH" "$ARCHIVE_NAME" ) ARCHIVE_SHA256="$(sha256_file "$ARCHIVE_PATH")" - ARCHIVE_URL="file://$ARCHIVE_PATH" + ARCHIVE_URL="$(file_uri "$ARCHIVE_PATH")" +} + +write_fixture_bazelrc() { + local ws_dir="$1" + local rules_go_repo="$2" + + cat > "$ws_dir/.bazelrc" < "$ws_dir/BUILD.bazel" <<'EOF' -load("@datadog-rules-test-optimization//tools/core:test_optimization_doctor.bzl", "dd_test_optimization_doctor") -load("@datadog-rules-test-optimization//tools/core:test_optimization_uploader.bzl", "dd_payload_uploader") +load("@datadog-rules-test-optimization//tools/core:test_optimization_targets.bzl", "dd_test_optimization_targets") exports_files([ "go.mod", @@ -528,23 +589,35 @@ exports_files([ "orchestrion.yml", ]) -dd_test_optimization_doctor( - name = "dd_test_optimization_doctor", - data = ["@test_optimization_data//:test_optimization_context"], +dd_test_optimization_targets( + name = "test_optimization", + sync_repo_name = "test_optimization_data", expected_targets = ["//app:hello_test"], ) +EOF -dd_payload_uploader( - name = "dd_upload_payloads", - data = ["@test_optimization_data//:test_optimization_context"], -) + cat > "$ws_dir/tools/build/BUILD.bazel" <<'EOF' +exports_files(["dd_go_test.bzl"]) +EOF + + cat > "$ws_dir/tools/build/dd_go_test.bzl" <<'EOF' +load("@rules_go//go:def.bzl", _go_test = "go_test") +load("@datadog-rules-test-optimization-go//:topt_go_test.bzl", "dd_topt_go_test") +load("@test_optimization_data//:export.bzl", "topt_data") + +def dd_go_test(name, **kwargs): + dd_topt_go_test( + name = name, + go_test_rule = _go_test, + topt_data = topt_data, + **kwargs + ) EOF cat > "$ws_dir/app/BUILD.bazel" < "$ws_dir/go.sum" + return + fi + if [[ "$DD_TRACE_GO_VERSION" == "v2.9.0" && "$ORCHESTRION_VERSION" == "v1.9.0" ]]; then cat > "$ws_dir/go.sum" <<'EOF' github.com/DataDog/dd-trace-go/contrib/log/slog/v2 v2.9.0 h1:o5PABRmFQQ1uJcog3PnNF9+182EODnjHB6fjGTFkOIs= @@ -875,21 +982,19 @@ EOF cat >> "$ws_dir/MODULE.bazel" < "$ws_dir/app/hello_test.go" <<'EOF' +package main + +import "testing" + +func TestDisabledBootstrap(t *testing.T) { + if greeting() != "Hello, Bzlmod!" { + t.Fatalf("unexpected greeting %q", greeting()) + } +} +EOF +} + +run_disabled_no_fetch_smoke() { + local ws_dir="$WORKSPACE_ROOT/disabled" + local resolved_repos="$TMP_ROOT/disabled-resolved-repositories.bzl" + local alias_log="$TMP_ROOT/disabled-aliases.log" + local genquery_log="$TMP_ROOT/disabled-genquery.log" + local repository_files="$TMP_ROOT/disabled-orchestrion-repository.files" + local test_log="$TMP_ROOT/disabled-test.log" + local test_target_path="${HELLO_TEST_TARGET#//}" + local test_package="${test_target_path%%:*}" + local test_name="${test_target_path#*:}" + local -a disabled_flags=( + "${BAZEL_EXTRA_ARGS[@]}" + --enable_bzlmod + ) + local -a disabled_env=( + env + -u DD_API_KEY + -u DD_SITE + -u DD_TEST_OPTIMIZATION_ENABLED + ) + local aliases=( + tool_binary + dd_trace_go_version_file + dd_trace_go_module_proxy_files + dd_trace_go_module_proxy_root_marker + orchestrion_tool_version_file + ) + + rm -rf "$ws_dir" + mkdir -p "$ws_dir" + write_module_file "$ws_dir" + write_shared_fixture_sources "$ws_dir" + write_fixture_bazelrc "$ws_dir" "rules_go" + write_disabled_fixture_test "$ws_dir" + + : > "$alias_log" + for alias in "${aliases[@]}"; do + if ! ( + cd "$ws_dir" + "${disabled_env[@]}" USE_BAZEL_VERSION="$BAZEL_VERSION" "$BAZEL" --output_user_root="$BAZEL_OUTPUT_USER_ROOT" cquery \ + "${disabled_flags[@]}" \ + --experimental_repository_resolved_file="$resolved_repos" \ + "@rules_go//go/private/orchestrion:$alias" --output=files \ + >"$TMP_ROOT/disabled-alias-$alias.out" + ) 2>"$TMP_ROOT/disabled-alias-$alias.err"; then + echo "error: disabled Bzlmod alias query failed for $alias" >&2 + cat "$TMP_ROOT/disabled-alias-$alias.err" >&2 + exit 1 + fi + cat "$TMP_ROOT/disabled-alias-$alias.out" >>"$alias_log" + done + if [[ -s "$alias_log" ]]; then + echo "error: disabled Bzlmod Orchestrion aliases exposed files" >&2 + cat "$alias_log" >&2 + exit 1 + fi + + if ! ( + # Avoid Git Bash converting //pkg:target into a Windows filesystem path. + cd "$ws_dir/$test_package" + "${disabled_env[@]}" USE_BAZEL_VERSION="$BAZEL_VERSION" "$BAZEL" --output_user_root="$BAZEL_OUTPUT_USER_ROOT" build \ + "${disabled_flags[@]}" \ + --experimental_repository_resolved_file="$resolved_repos" \ + ":${test_name}_deps_query" + ) >"$genquery_log" 2>&1; then + echo "error: disabled Bzlmod genquery failed" >&2 + cat "$genquery_log" >&2 + exit 1 + fi + + if ! ( + cd "$ws_dir" + "${disabled_env[@]}" USE_BAZEL_VERSION="$BAZEL_VERSION" "$BAZEL" --output_user_root="$BAZEL_OUTPUT_USER_ROOT" cquery \ + "${disabled_flags[@]}" \ + @rules_go_orchestrion_tool//:orchestrion --output=files + ) >"$repository_files" 2>>"$genquery_log"; then + echo "error: disabled Bzlmod Orchestrion repository query failed" >&2 + cat "$genquery_log" >&2 + exit 1 + fi + if [[ -s "$repository_files" ]]; then + echo "error: disabled Bzlmod Orchestrion repository exposed real files" >&2 + cat "$repository_files" >&2 + exit 1 + fi + + if ! ( + # Avoid Git Bash converting //pkg:target into a Windows filesystem path. + cd "$ws_dir/$test_package" + "${disabled_env[@]}" USE_BAZEL_VERSION="$BAZEL_VERSION" "$BAZEL" --output_user_root="$BAZEL_OUTPUT_USER_ROOT" test \ + "${disabled_flags[@]}" \ + --experimental_repository_resolved_file="$resolved_repos" \ + ":$test_name" + ) >"$test_log" 2>&1; then + echo "error: disabled Bzlmod test failed" >&2 + cat "$test_log" >&2 + exit 1 + fi + + if grep -Ein 'building orchestrion|downloading orchestrion|Could not find .go. binary' "$test_log"; then + echo "error: disabled Bzlmod smoke attempted the real Orchestrion bootstrap" >&2 + cat "$test_log" >&2 + exit 1 + fi + if grep -En 'building orchestrion|downloading orchestrion|Could not find .go. binary' "$genquery_log"; then + echo "error: disabled Bzlmod genquery attempted the real Orchestrion bootstrap" >&2 + cat "$genquery_log" >&2 + exit 1 + fi + + local testlogs + if ! testlogs="$(cd "$ws_dir" && "${disabled_env[@]}" USE_BAZEL_VERSION="$BAZEL_VERSION" "$BAZEL" --output_user_root="$BAZEL_OUTPUT_USER_ROOT" info "${disabled_flags[@]}" bazel-testlogs)"; then + echo "error: unable to locate disabled Bzlmod test logs" >&2 + exit 1 + fi + if find "$testlogs" -path '*/test.outputs/payloads/tests/*.json' -print -quit | grep -q .; then + echo "error: disabled Bzlmod smoke emitted Test Optimization payloads" >&2 + exit 1 + fi + + local public_kind + public_kind="$( + cd "$ws_dir/$test_package" + "${disabled_env[@]}" USE_BAZEL_VERSION="$BAZEL_VERSION" "$BAZEL" --output_user_root="$BAZEL_OUTPUT_USER_ROOT" query \ + "${disabled_flags[@]}" \ + ":$test_name" \ + --output=label_kind + )" + if [[ "$public_kind" != "go_test rule "* ]]; then + echo "error: disabled Bzlmod public target kind is '$public_kind', want raw go_test rule" >&2 + exit 1 + fi + + local hidden_name + for hidden_name in \ + "${test_name}__raw_go_test" \ + "${test_name}_topt_payloads" \ + "${test_name}_topt_bazel_metadata" \ + "${test_name}_orchestrion_pin_files"; do + if ( + cd "$ws_dir/$test_package" + "${disabled_env[@]}" USE_BAZEL_VERSION="$BAZEL_VERSION" "$BAZEL" --output_user_root="$BAZEL_OUTPUT_USER_ROOT" query \ + "${disabled_flags[@]}" \ + ":$hidden_name" + ) >/dev/null 2>&1; then + echo "error: disabled Bzlmod branch created enabled-only target :$hidden_name" >&2 + exit 1 + fi + done +} + +run_windows_enabled_smoke() { + local reuse_disabled_workspace="${1:-0}" + local ws_dir="$WORKSPACE_ROOT/windows-enabled" + local resolved_repos="$TMP_ROOT/windows-enabled-resolved-repositories.bzl" + local test_log="$TMP_ROOT/windows-enabled-test.log" + local test_target_path="${HELLO_TEST_TARGET#//}" + local test_package="${test_target_path%%:*}" + local test_name="${test_target_path#*:}" + local -a bazel_test_flags=() + + if [[ "$(uname -s)" == "Darwin" && "$BAZEL_VERSION" == "8.5.1" ]]; then + bazel_test_flags+=(--noexperimental_split_xml_generation) + fi + + if [[ "$reuse_disabled_workspace" == "1" ]]; then + ws_dir="$WORKSPACE_ROOT/disabled" + fi + if [[ -z "$GO_INTEGRATION_MOCK_SERVER_PID" ]]; then + start_go_integration_mock_server "$TMP_ROOT" "$MODULE_IMPORTPATH" + fi + + local -a enabled_flags=( + "${BAZEL_EXTRA_ARGS[@]}" + "${GO_INTEGRATION_MOCK_REPO_ENVS[@]}" + --enable_bzlmod + --config=test-optimization + ) + local aliases=( + tool_binary + dd_trace_go_version_file + dd_trace_go_module_proxy_files + dd_trace_go_module_proxy_root_marker + orchestrion_tool_version_file + ) + + if [[ "$reuse_disabled_workspace" != "1" ]]; then + rm -rf "$ws_dir" + mkdir -p "$ws_dir" + write_module_file "$ws_dir" + write_shared_fixture_sources "$ws_dir" + write_fixture_bazelrc "$ws_dir" "rules_go" + fi + + for alias in "${aliases[@]}"; do + local alias_files="$TMP_ROOT/windows-enabled-bzlmod-$alias.files" + local alias_log="$TMP_ROOT/windows-enabled-bzlmod-$alias.log" + if ! ( + cd "$ws_dir" + USE_BAZEL_VERSION="$BAZEL_VERSION" "$BAZEL" --output_user_root="$BAZEL_OUTPUT_USER_ROOT" cquery \ + "${enabled_flags[@]}" \ + --experimental_repository_resolved_file="$resolved_repos" \ + "@rules_go//go/private/orchestrion:$alias" --output=files + ) >"$alias_files" 2>"$alias_log"; then + echo "error: enabled Bzlmod Orchestrion alias $alias failed analysis" >&2 + cat "$alias_log" >&2 + exit 1 + fi + if [[ ! -s "$alias_files" ]]; then + echo "error: enabled Bzlmod Orchestrion alias $alias exposed no files" >&2 + cat "$alias_log" >&2 + exit 1 + fi + if ! grep -Eq 'rules_go_orchestrion_tool' "$alias_files"; then + echo "error: enabled Bzlmod Orchestrion alias $alias did not expose real repository files" >&2 + cat "$alias_files" >&2 + cat "$alias_log" >&2 + exit 1 + fi + done + + if ! ( + # Avoid Git Bash converting //pkg:target into a Windows filesystem path. + cd "$ws_dir/$test_package" + USE_BAZEL_VERSION="$BAZEL_VERSION" "$BAZEL" --output_user_root="$BAZEL_OUTPUT_USER_ROOT" test \ + "${enabled_flags[@]}" \ + --test_output=errors \ + --verbose_failures \ + "${bazel_test_flags[@]}" \ + --experimental_repository_resolved_file="$resolved_repos" \ + ":$test_name" + ) >"$test_log" 2>&1; then + echo "error: enabled Bzlmod test failed" >&2 + cat "$test_log" >&2 + exit 1 + fi + + local testlogs + testlogs="$(cd "$ws_dir" && USE_BAZEL_VERSION="$BAZEL_VERSION" "$BAZEL" --output_user_root="$BAZEL_OUTPUT_USER_ROOT" info "${enabled_flags[@]}" bazel-testlogs)" + if ! find "$testlogs" -path '*/test.outputs/payloads/tests/*.json' -print -quit | grep -q .; then + echo "error: enabled Bzlmod smoke emitted no Test Optimization payloads" >&2 + cat "$test_log" >&2 + exit 1 + fi + local public_kind + public_kind="$( + cd "$ws_dir/$test_package" + USE_BAZEL_VERSION="$BAZEL_VERSION" "$BAZEL" --output_user_root="$BAZEL_OUTPUT_USER_ROOT" query \ + "${enabled_flags[@]}" \ + ":$test_name" \ + --output=label_kind + )" + if [[ "$public_kind" != "orch_go_test rule "* ]]; then + echo "error: enabled Bzlmod public target kind is '$public_kind', want orch_go_test rule" >&2 + exit 1 + fi + assert_go_integration_metadata_requests +} + mkdir -p "$WORKSPACE_ROOT" + +if [[ "$WINDOWS_DISABLED_SMOKE_ONLY" == "1" || "$ORCHESTRION_DISABLED_SENTINEL" == "1" ]]; then + ORCHESTRION_VERSION="$ORCHESTRION_DISABLED_SENTINEL_VERSION" + ORCHESTRION_MODE="test_optimization" + create_fixture_archive + run_disabled_no_fetch_smoke + exit 0 +fi + +if [[ "$WINDOWS_ENABLED_SMOKE_ONLY" == "1" ]]; then + ORCHESTRION_MODE="test_optimization" + create_fixture_archive + run_windows_enabled_smoke + exit 0 +fi + +if [[ "$WINDOWS_CONFIG_TRANSITION_ONLY" == "1" ]]; then + ORCHESTRION_MODE="test_optimization" + create_fixture_archive + start_go_integration_mock_server "$TMP_ROOT" "$MODULE_IMPORTPATH" + run_disabled_no_fetch_smoke + if [[ -s "$GO_INTEGRATION_MOCK_SERVER_LOG" ]]; then + echo "error: disabled Bzlmod phase contacted the metadata server" >&2 + cat "$GO_INTEGRATION_MOCK_SERVER_LOG" >&2 + exit 1 + fi + run_windows_enabled_smoke 1 + exit 0 +fi + create_fixture_archive +start_go_integration_mock_server "$TMP_ROOT" "$MODULE_IMPORTPATH" +BAZEL_EXTRA_ARGS+=("${GO_INTEGRATION_MOCK_REPO_ENVS[@]}") if [[ "$INTEGRATION_SCENARIO_MODE" == "measure" ]]; then if [[ -z "$MEASURE_OUTPUT_PATH" ]]; then @@ -1081,6 +1494,7 @@ if [[ "$INTEGRATION_SCENARIO_MODE" == "measure" ]]; then exit 1 fi run_fixture + assert_go_integration_metadata_requests exit 0 fi @@ -1090,3 +1504,4 @@ if [[ "$INTEGRATION_SCENARIO_MODE" != "full" ]]; then fi run_fixture +assert_go_integration_metadata_requests diff --git a/tools/tests/integration/run_mock_server_tests.ps1 b/tools/tests/integration/run_mock_server_tests.ps1 index f6772564..6d79c299 100644 --- a/tools/tests/integration/run_mock_server_tests.ps1 +++ b/tools/tests/integration/run_mock_server_tests.ps1 @@ -285,6 +285,31 @@ function Get-JsonValue { return $null } +function Get-JsonCollectionCount { + param($Value) + if ($null -eq $Value) { return 0 } + if ($Value -is [System.Collections.IDictionary]) { return $Value.Keys.Count } + if ($Value -is [System.Array]) { return $Value.Length } + return 1 +} + +function Assert-JsonKeysExact { + param( + $Object, + [string[]]$ExpectedKeys, + [string]$Label + ) + if (-not ($Object -is [System.Collections.IDictionary])) { + throw "$Label must be a JSON object" + } + $actual = @($Object.Keys | ForEach-Object { [string]$_ } | Sort-Object) + $expected = @($ExpectedKeys | Sort-Object) + $difference = @(Compare-Object -ReferenceObject $expected -DifferenceObject $actual) + if ($difference.Count -ne 0) { + throw "$Label has unexpected keys (expected=$($expected -join ','), actual=$($actual -join ','))" + } +} + # Collect metric names from a telemetry message-batch while accepting either # array-backed or singleton-object JSON materialization. function Get-TelemetryMetricNames { @@ -456,6 +481,7 @@ test_optimization_sync = use_extension( test_optimization_sync.test_optimization_sync( name = "test_optimization_data", + enabled_by_env = True, service = "mock-service", runtime_name = "go", runtime_version = "1.2.3", @@ -463,6 +489,7 @@ test_optimization_sync.test_optimization_sync( test_optimization_sync.test_optimization_sync( name = "test_optimization_data_nodejs", + enabled_by_env = True, service = "mock-service-nodejs", runtime_name = "nodejs", runtime_version = "1.2.3", @@ -470,6 +497,7 @@ test_optimization_sync.test_optimization_sync( test_optimization_sync.test_optimization_sync( name = "test_optimization_data_dotnet", + enabled_by_env = True, service = "mock-service-dotnet", runtime_name = "dotnet", runtime_version = "1.2.3", @@ -477,6 +505,7 @@ test_optimization_sync.test_optimization_sync( test_optimization_sync.test_optimization_sync( name = "test_optimization_data_ruby", + enabled_by_env = True, service = "mock-service-ruby", runtime_name = "ruby", runtime_version = "1.2.3", @@ -509,6 +538,7 @@ filegroup( $bazelFlags = @("--output_base=$syncMetadataFetchOutBase") $repoEnvs = @( "--repo_env=DD_API_KEY=mock", + "--repo_env=DD_TEST_OPTIMIZATION_ENABLED=1", "--repo_env=DD_TEST_OPTIMIZATION_AGENTLESS_URL=http://127.0.0.1:$port", "--repo_env=DD_ENV=ci", "--repo_env=DD_GIT_REPOSITORY_URL=https://example.com/repo.git", @@ -523,6 +553,271 @@ filegroup( "--repo_env=GITHUB_SHA=", "--repo_env=GITHUB_EVENT_PATH=" ) + + # ------------------------------------------------------------------------- + # Scenario: disabled repositories render complete local stubs and never fetch. + # ------------------------------------------------------------------------- + # Run this before the enabled sync so the request-log delta is isolated from + # the baseline API traffic below. + $disabledOutputBase = Join-Path $tempRoot "disabled_sync_out" + $disabledLogStart = @(Read-JsonLog -Path $mockLog).Count + $disabledRepoEnvs = @( + "--repo_env=DD_TEST_OPTIMIZATION_ENABLED=0", + "--repo_env=DD_ENV=ci", + "--repo_env=DD_GIT_REPOSITORY_URL=", + "--repo_env=DD_GIT_BRANCH=", + "--repo_env=DD_GIT_TAG=", + "--repo_env=DD_GIT_COMMIT_SHA=", + "--repo_env=DD_GIT_HEAD_COMMIT=", + "--repo_env=DD_GIT_COMMIT_MESSAGE=", + "--repo_env=DD_GIT_HEAD_MESSAGE=", + "--repo_env=DD_GIT_COMMIT_AUTHOR_NAME=", + "--repo_env=DD_GIT_COMMIT_AUTHOR_EMAIL=", + "--repo_env=DD_GIT_COMMIT_AUTHOR_DATE=", + "--repo_env=DD_GIT_COMMIT_COMMITTER_NAME=", + "--repo_env=DD_GIT_COMMIT_COMMITTER_EMAIL=", + "--repo_env=DD_GIT_COMMIT_COMMITTER_DATE=", + "--repo_env=DD_GIT_HEAD_AUTHOR_NAME=", + "--repo_env=DD_GIT_HEAD_AUTHOR_EMAIL=", + "--repo_env=DD_GIT_HEAD_AUTHOR_DATE=", + "--repo_env=DD_GIT_HEAD_COMMITTER_NAME=", + "--repo_env=DD_GIT_HEAD_COMMITTER_EMAIL=", + "--repo_env=DD_GIT_HEAD_COMMITTER_DATE=", + "--repo_env=DD_GIT_PR_BASE_BRANCH=", + "--repo_env=DD_GIT_PR_BASE_BRANCH_SHA=", + "--repo_env=DD_GIT_PR_BASE_BRANCH_HEAD_SHA=", + "--repo_env=DD_PR_NUMBER=", + "--repo_env=DISABLE_CI_METADATA=1" + ) + $savedApiKey = $env:DD_API_KEY + $savedSite = $env:DD_SITE + $savedEnabled = $env:DD_TEST_OPTIMIZATION_ENABLED + $hadApiKey = Test-Path Env:DD_API_KEY + $hadSite = Test-Path Env:DD_SITE + $hadEnabled = Test-Path Env:DD_TEST_OPTIMIZATION_ENABLED + Push-Location $syncWorkspace + try { + Remove-Item Env:DD_API_KEY -ErrorAction SilentlyContinue + Remove-Item Env:DD_SITE -ErrorAction SilentlyContinue + Remove-Item Env:DD_TEST_OPTIMIZATION_ENABLED -ErrorAction SilentlyContinue + $disabledCqueryOutput = @( + Invoke-BazelCommand -BazelInvoker $bazelInvoker -BazelArgs @( + "--output_base=$disabledOutputBase", + "cquery", + "@test_optimization_data//:test_optimization_files", + "--output=files" + ) + $disabledRepoEnvs + ) + $disabledCqueryExitCode = Get-NativeExitCode + if ($disabledCqueryExitCode -ne 0) { + throw "disabled sync cquery failed with exit code $disabledCqueryExitCode" + } + $disabledContextCqueryOutput = @( + Invoke-BazelCommand -BazelInvoker $bazelInvoker -BazelArgs @( + "--output_base=$disabledOutputBase", + "cquery", + "@test_optimization_data//:test_optimization_context", + "--output=files" + ) + $disabledRepoEnvs + ) + $disabledContextCqueryExitCode = Get-NativeExitCode + if ($disabledContextCqueryExitCode -ne 0) { + throw "disabled sync context cquery failed with exit code $disabledContextCqueryExitCode" + } + $disabledCqueryOutput += $disabledContextCqueryOutput + $disabledBuildOutput = @( + Invoke-BazelCommand -BazelInvoker $bazelInvoker -BazelArgs @( + "--output_base=$disabledOutputBase", + "build", + "@test_optimization_data//:test_optimization_files" + ) + $disabledRepoEnvs + ) + $disabledBuildExitCode = Get-NativeExitCode + if ($disabledBuildExitCode -ne 0) { + throw "disabled sync build failed with exit code $disabledBuildExitCode" + } + $disabledContextBuildOutput = @( + Invoke-BazelCommand -BazelInvoker $bazelInvoker -BazelArgs @( + "--output_base=$disabledOutputBase", + "build", + "@test_optimization_data//:test_optimization_context" + ) + $disabledRepoEnvs + ) + $disabledContextBuildExitCode = Get-NativeExitCode + if ($disabledContextBuildExitCode -ne 0) { + throw "disabled sync context build failed with exit code $disabledContextBuildExitCode" + } + } finally { + if ($hadApiKey) { $env:DD_API_KEY = $savedApiKey } else { Remove-Item Env:DD_API_KEY -ErrorAction SilentlyContinue } + if ($hadSite) { $env:DD_SITE = $savedSite } else { Remove-Item Env:DD_SITE -ErrorAction SilentlyContinue } + if ($hadEnabled) { $env:DD_TEST_OPTIMIZATION_ENABLED = $savedEnabled } else { Remove-Item Env:DD_TEST_OPTIMIZATION_ENABLED -ErrorAction SilentlyContinue } + Pop-Location + } + $disabledCqueryText = (@($disabledCqueryOutput) -join "`n") + foreach ($requiredFile in @("settings.json", "known_tests.json", "test_management.json", "flaky_tests.json", "manifest.txt", "context.json")) { + if (-not $disabledCqueryText.Contains($requiredFile)) { + throw "disabled sync cquery is missing $requiredFile" + } + } + $disabledSettings = $null + $disabledSettingsRelativePath = @($disabledCqueryText -split "`r?`n" | Where-Object { $_ -match 'test_optimization_data.*[\\/]\.testoptimization[\\/]cache[\\/]http[\\/]settings\.json$' } | Select-Object -First 1) + $disabledCqueryBases = @( + $disabledOutputBase, + (Join-Path $disabledOutputBase "execroot/_main"), + (Join-Path $disabledOutputBase "execroot/__main__") + ) + foreach ($relativePath in $disabledSettingsRelativePath) { + foreach ($basePath in $disabledCqueryBases) { + $candidatePath = Join-Path $basePath $relativePath.Trim() + if (Test-Path -LiteralPath $candidatePath -PathType Leaf) { + $disabledSettings = Get-Item -LiteralPath $candidatePath + break + } + } + if ($disabledSettings) { break } + } + if (-not $disabledSettings) { + throw "disabled sync did not materialize the expected stub repository" + } + $disabledRepoDir = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $disabledSettings.FullName))) + $disabledSettingsMap = Read-JsonMap -JsonText (Get-Content -LiteralPath $disabledSettings.FullName -Raw -Encoding UTF8) + Assert-JsonKeysExact -Object $disabledSettingsMap -ExpectedKeys @("data") -Label "disabled settings.json" + $disabledSettingsData = Get-JsonValue -Object $disabledSettingsMap -Key "data" + Assert-JsonKeysExact -Object $disabledSettingsData -ExpectedKeys @("attributes") -Label "disabled settings.json data" + $disabledSettingsAttributes = Get-JsonValue -Object $disabledSettingsData -Key "attributes" + Assert-JsonKeysExact -Object $disabledSettingsAttributes -ExpectedKeys @("flaky_test_retries_enabled", "known_tests_enabled", "test_management") -Label "disabled settings.json attributes" + $disabledTestManagementSetting = Get-JsonValue -Object $disabledSettingsAttributes -Key "test_management" + Assert-JsonKeysExact -Object $disabledTestManagementSetting -ExpectedKeys @("enabled") -Label "disabled settings.json test_management" + if ((Get-JsonValue -Object $disabledSettingsAttributes -Key "known_tests_enabled") -ne $false -or + (Get-JsonValue -Object $disabledTestManagementSetting -Key "enabled") -ne $false -or + (Get-JsonValue -Object $disabledSettingsAttributes -Key "flaky_test_retries_enabled") -ne $false) { + throw "disabled settings.json does not contain the canonical false flags" + } + $knownTestsValue = Read-JsonMap -JsonText (Get-Content -LiteralPath (Join-Path $disabledRepoDir ".testoptimization/cache/http/known_tests.json") -Raw -Encoding UTF8) + Assert-JsonKeysExact -Object $knownTestsValue -ExpectedKeys @("data") -Label "disabled known_tests.json" + $knownTestsAttributes = Get-JsonValue -Object $knownTestsValue -Key "data" + Assert-JsonKeysExact -Object $knownTestsAttributes -ExpectedKeys @("attributes") -Label "disabled known_tests.json data" + $knownTestsAttributes = Get-JsonValue -Object $knownTestsAttributes -Key "attributes" + Assert-JsonKeysExact -Object $knownTestsAttributes -ExpectedKeys @("tests") -Label "disabled known_tests.json attributes" + $knownTestsMap = Get-JsonValue -Object $knownTestsAttributes -Key "tests" + if ((Get-JsonCollectionCount $knownTestsMap) -ne 0) { throw "disabled known_tests.json is not empty" } + $testManagementValue = Read-JsonMap -JsonText (Get-Content -LiteralPath (Join-Path $disabledRepoDir ".testoptimization/cache/http/test_management.json") -Raw -Encoding UTF8) + Assert-JsonKeysExact -Object $testManagementValue -ExpectedKeys @("data") -Label "disabled test_management.json" + $testManagementAttributes = Get-JsonValue -Object $testManagementValue -Key "data" + Assert-JsonKeysExact -Object $testManagementAttributes -ExpectedKeys @("attributes") -Label "disabled test_management.json data" + $testManagementAttributes = Get-JsonValue -Object $testManagementAttributes -Key "attributes" + Assert-JsonKeysExact -Object $testManagementAttributes -ExpectedKeys @("modules") -Label "disabled test_management.json attributes" + $testManagementMap = Get-JsonValue -Object $testManagementAttributes -Key "modules" + if ((Get-JsonCollectionCount $testManagementMap) -ne 0) { throw "disabled test_management.json is not empty" } + $flakyValue = Read-JsonMap -JsonText (Get-Content -LiteralPath (Join-Path $disabledRepoDir ".testoptimization/cache/http/flaky_tests.json") -Raw -Encoding UTF8) + Assert-JsonKeysExact -Object $flakyValue -ExpectedKeys @("data") -Label "disabled flaky_tests.json" + $flakyData = Get-JsonValue -Object $flakyValue -Key "data" + if ((Get-JsonCollectionCount $flakyData) -ne 0) { throw "disabled flaky_tests.json is not empty" } + $disabledContextPath = Join-Path $disabledRepoDir ".testoptimization/context.json" + $disabledContext = Read-JsonMap -JsonText (Get-Content -LiteralPath $disabledContextPath -Raw -Encoding UTF8) + Assert-JsonKeysExact -Object $disabledContext -ExpectedKeys @( + "bazel.rule_name", + "bazel.rule_version", + "env", + "runtime.name", + "runtime.version", + "service.name", + "topt.sync.enabled", + "topt.sync.out_dir", + "topt.sync.repository_name" + ) -Label "disabled context.json" + if ((Get-JsonValue -Object $disabledContext -Key "bazel.rule_name") -ne "datadog-rules-test-optimization" -or + [string]::IsNullOrWhiteSpace([string](Get-JsonValue -Object $disabledContext -Key "bazel.rule_version")) -or + (Get-JsonValue -Object $disabledContext -Key "env") -ne "ci" -or + (Get-JsonValue -Object $disabledContext -Key "runtime.name") -ne "go" -or + (Get-JsonValue -Object $disabledContext -Key "runtime.version") -ne "1.2.3" -or + (Get-JsonValue -Object $disabledContext -Key "service.name") -ne "mock-service" -or + (Get-JsonValue -Object $disabledContext -Key "topt.sync.enabled") -ne $false -or + (Get-JsonValue -Object $disabledContext -Key "topt.sync.out_dir") -ne ".testoptimization" -or + (Get-JsonValue -Object $disabledContext -Key "topt.sync.repository_name") -ne "test_optimization_data") { + throw "disabled context.json does not match the canonical repository context" + } + $disabledTelemetryPath = Join-Path $disabledRepoDir ".testoptimization/telemetry_facts.json" + $disabledTelemetry = Read-JsonMap -JsonText (Get-Content -LiteralPath $disabledTelemetryPath -Raw -Encoding UTF8) + Assert-JsonKeysExact -Object $disabledTelemetry -ExpectedKeys @("counts", "distributions", "env", "runtime_name", "schema_version", "service_name") -Label "disabled telemetry_facts.json" + $disabledCounts = @(Get-JsonValue -Object $disabledTelemetry -Key "counts") + $disabledDistributions = Get-JsonValue -Object $disabledTelemetry -Key "distributions" + if ($disabledCounts.Count -ne 1 -or (Get-JsonCollectionCount $disabledDistributions) -ne 0) { + throw "disabled telemetry_facts.json must contain exactly one count and no distributions" + } + $disabledCount = $disabledCounts[0] + Assert-JsonKeysExact -Object $disabledCount -ExpectedKeys @("name", "tags", "value") -Label "disabled telemetry count" + if ((Get-JsonValue -Object $disabledTelemetry -Key "schema_version") -ne 1 -or + (Get-JsonValue -Object $disabledTelemetry -Key "service_name") -ne "mock-service" -or + (Get-JsonValue -Object $disabledTelemetry -Key "runtime_name") -ne "go" -or + (Get-JsonValue -Object $disabledTelemetry -Key "env") -ne "ci" -or + (Get-JsonValue -Object $disabledCount -Key "name") -ne "sync.disabled" -or + (Get-JsonValue -Object $disabledCount -Key "value") -ne 1 -or + (Get-JsonCollectionCount (Get-JsonValue -Object $disabledCount -Key "tags")) -ne 0) { + throw "disabled telemetry_facts.json does not match the canonical disabled fact" + } + $disabledService = [string](Get-JsonValue -Object $disabledContext -Key "service.name") + $disabledEnvironment = [string](Get-JsonValue -Object $disabledContext -Key "env") + $disabledLogEnd = @(Read-JsonLog -Path $mockLog).Count + if ($disabledLogStart -ne $disabledLogEnd) { + throw "disabled sync unexpectedly contacted the mock metadata server" + } + + # Re-evaluate the same canonical repository in the same output base with the + # config gate enabled. A cached disabled repository must not survive the + # DD_TEST_OPTIMIZATION_ENABLED transition from 0 to 1. + $toggleSalt = "integration-toggle-$([Guid]::NewGuid().ToString('N'))" + Push-Location $syncWorkspace + try { + Invoke-BazelCommand -BazelInvoker $bazelInvoker -BazelArgs (@( + "--output_base=$disabledOutputBase", + "fetch", + "@test_optimization_data//:test_optimization_files", + "--repo_env=FETCH_SALT=$toggleSalt" + ) + $repoEnvs) + $toggleFetchExitCode = Get-NativeExitCode + if ($toggleFetchExitCode -ne 0) { + throw "enabled re-evaluation of the disabled repository failed with exit code $toggleFetchExitCode" + } + Invoke-BazelCommand -BazelInvoker $bazelInvoker -BazelArgs (@( + "--output_base=$disabledOutputBase", + "cquery", + "@test_optimization_data//:test_optimization_context", + "--output=files" + ) + $repoEnvs) | Out-Null + $toggleCqueryExitCode = Get-NativeExitCode + if ($toggleCqueryExitCode -ne 0) { + throw "enabled context cquery after disabled sync failed with exit code $toggleCqueryExitCode" + } + } finally { + Pop-Location + } + if (-not (Test-Path -LiteralPath $disabledSettings.FullName -PathType Leaf)) { + throw "enabled sync did not rematerialize the same canonical repository path" + } + $enabledSettingsMap = Read-JsonMap -JsonText (Get-Content -LiteralPath $disabledSettings.FullName -Raw -Encoding UTF8) + $enabledSettingsAttributes = Get-JsonValue -Object (Get-JsonValue -Object $enabledSettingsMap -Key "data") -Key "attributes" + if ((Get-JsonValue -Object $enabledSettingsAttributes -Key "known_tests_enabled") -ne $true -or + (Get-JsonValue -Object (Get-JsonValue -Object $enabledSettingsAttributes -Key "test_management") -Key "enabled") -ne $true) { + throw "enabled re-evaluation retained the disabled settings stub" + } + $enabledContext = Read-JsonMap -JsonText (Get-Content -LiteralPath $disabledContextPath -Raw -Encoding UTF8) + if ((Get-JsonValue -Object $enabledContext -Key "service.name") -ne $disabledService -or + (Get-JsonValue -Object $enabledContext -Key "env") -ne $disabledEnvironment -or + $enabledContext.Contains("topt.sync.enabled")) { + throw "enabled re-evaluation changed service/environment or retained disabled context state" + } + $enabledToggleEntries = @(Read-NewLogEntries -StartIndex $disabledLogEnd -Path $mockLog) + foreach ($expectedPath in @( + "/api/v2/libraries/tests/services/setting", + "/api/v2/ci/libraries/tests", + "/api/v2/test/libraries/test-management/tests" + )) { + if (@($enabledToggleEntries | Where-Object { $_.path -eq $expectedPath }).Count -eq 0) { + throw "enabled re-evaluation missed expected metadata request $expectedPath" + } + } + Push-Location $syncWorkspace try { Invoke-BazelCommand -BazelInvoker $bazelInvoker -BazelArgs (@($bazelFlags + @("fetch", "//:all_sync_payloads") + $repoEnvs)) diff --git a/tools/tests/integration/run_mock_server_tests.sh b/tools/tests/integration/run_mock_server_tests.sh index 17016008..6b2db837 100755 --- a/tools/tests/integration/run_mock_server_tests.sh +++ b/tools/tests/integration/run_mock_server_tests.sh @@ -225,6 +225,7 @@ test_optimization_sync = use_extension( test_optimization_sync.test_optimization_sync( name = "test_optimization_data", + enabled_by_env = True, service = "mock-service", runtime_name = "go", runtime_version = "1.2.3", @@ -232,6 +233,7 @@ test_optimization_sync.test_optimization_sync( test_optimization_sync.test_optimization_sync( name = "test_optimization_data_nodejs", + enabled_by_env = True, service = "mock-service-nodejs", runtime_name = "nodejs", runtime_version = "1.2.3", @@ -239,6 +241,7 @@ test_optimization_sync.test_optimization_sync( test_optimization_sync.test_optimization_sync( name = "test_optimization_data_dotnet", + enabled_by_env = True, service = "mock-service-dotnet", runtime_name = "dotnet", runtime_version = "1.2.3", @@ -246,6 +249,7 @@ test_optimization_sync.test_optimization_sync( test_optimization_sync.test_optimization_sync( name = "test_optimization_data_ruby", + enabled_by_env = True, service = "mock-service-ruby", runtime_name = "ruby", runtime_version = "1.2.3", @@ -272,6 +276,17 @@ sh_test( timeout = "short", ) +sh_test( + name = "disabled_metadata_runfiles_test", + srcs = ["disabled_metadata_runfiles_test.sh"], + data = [ + "@test_optimization_data//:test_optimization_context", + "@test_optimization_data//:test_optimization_files", + ], + size = "small", + timeout = "short", +) + dd_payload_uploader( name = "dd_upload_payloads", ) @@ -433,9 +448,82 @@ JSON_EOF PAYLOAD_EOF chmod +x payload_writer.sh +cat > disabled_metadata_runfiles_test.sh <<'DISABLED_RUNFILES_EOF' +#!/usr/bin/env bash +set -euo pipefail + +runfiles_root="${RUNFILES_DIR:-${TEST_SRCDIR:-}}" +if [[ -z "$runfiles_root" || ! -d "$runfiles_root" ]]; then + echo "error: Bazel runfiles directory is unavailable" >&2 + exit 1 +fi + +settings_path="$(find -L "$runfiles_root" -type f \ + -path '*test_optimization_data/.testoptimization/cache/http/settings.json' \ + -print -quit)" +if [[ -z "$settings_path" ]]; then + echo "error: disabled settings.json was not present in test runfiles" >&2 + exit 1 +fi +repo_dir="${settings_path%/.testoptimization/cache/http/settings.json}" + +jq -e '. == { + "data": { + "attributes": { + "flaky_test_retries_enabled": false, + "known_tests_enabled": false, + "test_management": {"enabled": false} + } + } +}' "$settings_path" >/dev/null +jq -e '. == {"data": {"attributes": {"tests": {}}}}' \ + "$repo_dir/.testoptimization/cache/http/known_tests.json" >/dev/null +jq -e '. == {"data": {"attributes": {"modules": {}}}}' \ + "$repo_dir/.testoptimization/cache/http/test_management.json" >/dev/null +jq -e '. == {"data": []}' \ + "$repo_dir/.testoptimization/cache/http/flaky_tests.json" >/dev/null +jq -e ' + keys == [ + "bazel.rule_name", + "bazel.rule_version", + "env", + "runtime.name", + "runtime.version", + "service.name", + "topt.sync.enabled", + "topt.sync.out_dir", + "topt.sync.repository_name" + ] and + ."bazel.rule_name" == "datadog-rules-test-optimization" and + (."bazel.rule_version" | type == "string" and length > 0) and + .env == "ci" and + ."runtime.name" == "go" and + ."runtime.version" == "1.2.3" and + ."service.name" == "mock-service" and + ."topt.sync.enabled" == false and + ."topt.sync.out_dir" == ".testoptimization" and + ."topt.sync.repository_name" == "test_optimization_data" +' \ + "$repo_dir/.testoptimization/context.json" >/dev/null +jq -e '. == { + "counts": [{"name": "sync.disabled", "tags": [], "value": 1}], + "distributions": [], + "env": "ci", + "runtime_name": "go", + "schema_version": 1, + "service_name": "mock-service" +}' \ + "$repo_dir/.testoptimization/telemetry_facts.json" >/dev/null +DISABLED_RUNFILES_EOF +chmod +x disabled_metadata_runfiles_test.sh + BAZEL="$REPO_ROOT/bazelw" OUT_BASE="$TMP_WS/.bazel_out" BAZEL_FLAGS=(--output_base="$OUT_BASE") +BAZEL_TEST_FLAGS=() +if [[ "$(uname -s)" == "Darwin" && "${USE_BAZEL_VERSION:-}" == "8.5.1" ]]; then + BAZEL_TEST_FLAGS+=(--noexperimental_split_xml_generation) +fi # --------------------------------------------------------------------------- # Scenario: declaring sync repos is lazy until a target actually consumes them. @@ -443,10 +531,13 @@ BAZEL_FLAGS=(--output_base="$OUT_BASE") # Large monorepos may declare Test Optimization once near workspace setup while # only instrumenting a small pilot target set. Plain targets that do not load or # depend on @test_optimization_data must keep working without sync credentials. -env -u DD_API_KEY -u DD_SITE "$BAZEL" "${BAZEL_FLAGS[@]}" test //:write_payloads_test +env -u DD_API_KEY -u DD_SITE "$BAZEL" "${BAZEL_FLAGS[@]}" test //:write_payloads_test \ + "${BAZEL_TEST_FLAGS[@]}" SYNC_LAZINESS_LOG="$TMP_WS/sync_laziness_requires_api_key.log" -if env -u DD_API_KEY -u DD_SITE "$BAZEL" "${BAZEL_FLAGS[@]}" build //:dd_upload_payloads_with_context >"$SYNC_LAZINESS_LOG" 2>&1; then +if env -u DD_API_KEY -u DD_SITE -u DD_TEST_OPTIMIZATION_ENABLED \ + "$BAZEL" "${BAZEL_FLAGS[@]}" build //:dd_upload_payloads_with_context \ + --repo_env=DD_TEST_OPTIMIZATION_ENABLED=1 >"$SYNC_LAZINESS_LOG" 2>&1; then echo "error: target consuming @test_optimization_data succeeded without DD_API_KEY" cat "$SYNC_LAZINESS_LOG" || true exit 1 @@ -460,6 +551,7 @@ fi # Provide deterministic repo metadata for fixtures + payload enrichment. REPO_ENVS=( --repo_env=DD_API_KEY=mock + --repo_env=DD_TEST_OPTIMIZATION_ENABLED=1 --repo_env=DD_TEST_OPTIMIZATION_AGENTLESS_URL=http://127.0.0.1:$PORT --repo_env=DD_ENV=ci --repo_env=DD_GIT_REPOSITORY_URL=https://example.com/repo.git @@ -474,6 +566,182 @@ REPO_ENVS=( --repo_env=GITHUB_EVENT_PATH= ) +# --------------------------------------------------------------------------- +# Scenario: disabled repositories render complete local stubs and never fetch. +# --------------------------------------------------------------------------- +# Keep this phase before the enabled sync so the request-log delta is isolated +# from the baseline API traffic below. +DISABLED_OUTPUT_BASE="$TMP_WS/.bazel_disabled_out" +DISABLED_CQUERY_OUT="$TMP_WS/disabled-cquery.out" +DISABLED_REPO_ENVS=( + --repo_env=DD_TEST_OPTIMIZATION_ENABLED=0 + --repo_env=DD_ENV=ci + --repo_env=DISABLE_CI_METADATA=1 +) +if [[ -f "$LOG_FILE" ]]; then + DISABLED_LOG_START="$(wc -l < "$LOG_FILE" | tr -d '[:space:]')" +else + DISABLED_LOG_START=0 +fi +env -u DD_API_KEY -u DD_SITE -u DD_TEST_OPTIMIZATION_ENABLED \ + "$BAZEL" --output_base="$DISABLED_OUTPUT_BASE" cquery \ + @test_optimization_data//:test_optimization_files --output=files \ + "${DISABLED_REPO_ENVS[@]}" >"$DISABLED_CQUERY_OUT" +env -u DD_API_KEY -u DD_SITE -u DD_TEST_OPTIMIZATION_ENABLED \ + "$BAZEL" --output_base="$DISABLED_OUTPUT_BASE" cquery \ + @test_optimization_data//:test_optimization_context --output=files \ + "${DISABLED_REPO_ENVS[@]}" >>"$DISABLED_CQUERY_OUT" +env -u DD_API_KEY -u DD_SITE -u DD_TEST_OPTIMIZATION_ENABLED \ + "$BAZEL" --output_base="$DISABLED_OUTPUT_BASE" build \ + @test_optimization_data//:test_optimization_files \ + "${DISABLED_REPO_ENVS[@]}" +env -u DD_API_KEY -u DD_SITE -u DD_TEST_OPTIMIZATION_ENABLED \ + "$BAZEL" --output_base="$DISABLED_OUTPUT_BASE" build \ + @test_optimization_data//:test_optimization_context \ + "${DISABLED_REPO_ENVS[@]}" +env -u DD_API_KEY -u DD_SITE -u DD_TEST_OPTIMIZATION_ENABLED \ + "$BAZEL" --output_base="$DISABLED_OUTPUT_BASE" test \ + //:disabled_metadata_runfiles_test \ + "${BAZEL_TEST_FLAGS[@]}" \ + "${DISABLED_REPO_ENVS[@]}" + +for required in settings.json known_tests.json test_management.json flaky_tests.json manifest.txt context.json; do + if ! grep -q "$required" "$DISABLED_CQUERY_OUT"; then + echo "error: disabled sync cquery is missing $required" + cat "$DISABLED_CQUERY_OUT" + exit 1 + fi +done + +DISABLED_REPO_DIR="$(find "$DISABLED_OUTPUT_BASE" -type f \ + -path '*/external/*test_optimization_data/.testoptimization/cache/http/settings.json' \ + -print -quit)" +if [[ -z "$DISABLED_REPO_DIR" ]]; then + echo "error: disabled sync did not materialize the expected stub repository" + exit 1 +fi +DISABLED_REPO_DIR="${DISABLED_REPO_DIR%/.testoptimization/cache/http/settings.json}" +jq -e '. == { + "data": { + "attributes": { + "flaky_test_retries_enabled": false, + "known_tests_enabled": false, + "test_management": {"enabled": false} + } + } +}' "$DISABLED_REPO_DIR/.testoptimization/cache/http/settings.json" >/dev/null +jq -e '. == {"data": {"attributes": {"tests": {}}}}' \ + "$DISABLED_REPO_DIR/.testoptimization/cache/http/known_tests.json" >/dev/null +jq -e '. == {"data": {"attributes": {"modules": {}}}}' \ + "$DISABLED_REPO_DIR/.testoptimization/cache/http/test_management.json" >/dev/null +jq -e '. == {"data": []}' \ + "$DISABLED_REPO_DIR/.testoptimization/cache/http/flaky_tests.json" >/dev/null +jq -e ' + keys == [ + "bazel.rule_name", + "bazel.rule_version", + "env", + "runtime.name", + "runtime.version", + "service.name", + "topt.sync.enabled", + "topt.sync.out_dir", + "topt.sync.repository_name" + ] and + ."bazel.rule_name" == "datadog-rules-test-optimization" and + (."bazel.rule_version" | type == "string" and length > 0) and + .env == "ci" and + ."runtime.name" == "go" and + ."runtime.version" == "1.2.3" and + ."service.name" == "mock-service" and + ."topt.sync.enabled" == false and + ."topt.sync.out_dir" == ".testoptimization" and + ."topt.sync.repository_name" == "test_optimization_data" +' \ + "$DISABLED_REPO_DIR/.testoptimization/context.json" >/dev/null +jq -e '. == { + "counts": [{"name": "sync.disabled", "tags": [], "value": 1}], + "distributions": [], + "env": "ci", + "runtime_name": "go", + "schema_version": 1, + "service_name": "mock-service" +}' \ + "$DISABLED_REPO_DIR/.testoptimization/telemetry_facts.json" >/dev/null +DISABLED_SERVICE="$(jq -r '."service.name"' "$DISABLED_REPO_DIR/.testoptimization/context.json")" +DISABLED_ENVIRONMENT="$(jq -r '.env' "$DISABLED_REPO_DIR/.testoptimization/context.json")" +if [[ -f "$LOG_FILE" ]]; then + DISABLED_LOG_END="$(wc -l < "$LOG_FILE" | tr -d '[:space:]')" +else + DISABLED_LOG_END=0 +fi +if [[ "$DISABLED_LOG_START" != "$DISABLED_LOG_END" ]]; then + echo "error: disabled sync unexpectedly contacted the mock metadata server" + exit 1 +fi + +# Re-evaluate the same canonical repository in the same output base with the +# config gate enabled. This guards against a disabled repository remaining +# cached after DD_TEST_OPTIMIZATION_ENABLED changes from 0 to 1. +TOGGLE_SYNC_SALT="integration-toggle-${RANDOM}-$(date +%s)" +"$BAZEL" --output_base="$DISABLED_OUTPUT_BASE" fetch \ + @test_optimization_data//:test_optimization_files \ + --repo_env=FETCH_SALT="$TOGGLE_SYNC_SALT" \ + "${REPO_ENVS[@]}" +"$BAZEL" --output_base="$DISABLED_OUTPUT_BASE" cquery \ + @test_optimization_data//:test_optimization_context --output=files \ + "${REPO_ENVS[@]}" >/dev/null + +ENABLED_REPO_SETTINGS="$(find "$DISABLED_OUTPUT_BASE" -type f \ + -path '*/external/*test_optimization_data/.testoptimization/cache/http/settings.json' \ + -print -quit)" +if [[ -z "$ENABLED_REPO_SETTINGS" ]]; then + echo "error: enabled sync did not rematerialize the canonical repository" + exit 1 +fi +ENABLED_REPO_DIR="${ENABLED_REPO_SETTINGS%/.testoptimization/cache/http/settings.json}" +if [[ "$ENABLED_REPO_DIR" != "$DISABLED_REPO_DIR" ]]; then + echo "error: disabled-to-enabled sync used a different repository path" + printf 'disabled: %s\nenabled: %s\n' "$DISABLED_REPO_DIR" "$ENABLED_REPO_DIR" + exit 1 +fi +jq -e ' + .data.attributes.known_tests_enabled == true and + .data.attributes.test_management.enabled == true +' "$ENABLED_REPO_SETTINGS" >/dev/null +jq -e --arg service "$DISABLED_SERVICE" --arg environment "$DISABLED_ENVIRONMENT" ' + ."service.name" == $service and + .env == $environment and + (has("topt.sync.enabled") | not) +' "$ENABLED_REPO_DIR/.testoptimization/context.json" >/dev/null + +LOG_FILE="$LOG_FILE" LOG_START="$DISABLED_LOG_END" "$PYTHON" - <<'PY' +import json +import os +import sys + +required = { + "/api/v2/libraries/tests/services/setting", + "/api/v2/ci/libraries/tests", + "/api/v2/test/libraries/test-management/tests", +} +with open(os.environ["LOG_FILE"], "r", encoding="utf-8") as handle: + records = [] + for line in handle: + try: + records.append(json.loads(line)) + except json.JSONDecodeError: + continue +records = records[int(os.environ["LOG_START"]):] +seen = {record.get("path") for record in records} +missing = required - seen +if missing: + print("error: enabled re-evaluation of the disabled repository missed metadata requests") + for path in sorted(missing): + print(f" - {path}") + sys.exit(1) +PY + # --------------------------------------------------------------------------- # Scenario: baseline sync + uploader run (agentless) with fixture assertions. # --------------------------------------------------------------------------- @@ -588,6 +856,7 @@ done unset DD_TEST_OPTIMIZATION_AGENT_URL "$BAZEL" "${BAZEL_FLAGS[@]}" test //:write_payloads_test \ + "${BAZEL_TEST_FLAGS[@]}" \ "${REPO_ENVS[@]}" # Use Bazel's testlogs location to find payloads for the uploader. @@ -2679,6 +2948,10 @@ run_retry_sync_case \ # - dual local_path_override (repo root + modules/go) MULTI_WS="$TMP_WS/ws_multi" mkdir -p "$MULTI_WS" +cat > "$MULTI_WS/.bazelrc" <<'MULTI_BAZELRC_EOF' +common:test-optimization --repo_env=DD_TEST_OPTIMIZATION_ENABLED=1 +build:test-optimization --@rules_go//go/private/orchestrion:enabled=true +MULTI_BAZELRC_EOF cat > "$MULTI_WS/MODULE.bazel" <