Skip to content

ci: dogfood ddtest for the internal suite (standalone pilot) - #19870

Draft
gnufede wants to merge 46 commits into
mainfrom
gnufede/ddtest-dogfooding
Draft

ci: dogfood ddtest for the internal suite (standalone pilot)#19870
gnufede wants to merge 46 commits into
mainfrom
gnufede/ddtest-dogfooding

Conversation

@gnufede

@gnufede gnufede commented Aug 26, 2026

Copy link
Copy Markdown
Member

Add an opt-in, manually-triggered CI path that runs the internal riot suite through ddtest (plan + run + file splitting) instead of the legacy riot run <hash> -- --ddtrace path, without any changes to ddtest and without touching the generated-config pipeline.

ddtest stays unchanged; all riot knowledge lives in a new Python bridge (scripts/ddtest-riot.py) that uses riot as a library:

  • riotfile: the internal suite path is now a queryable env var (DDTEST_SUITE_PATH) instead of a literal baked into the pytest command, so riot run internal is unchanged with or without ddtest, and the bridge can read the suite location per venv.
  • scripts/ddtest-riot.py: hashes <pattern> lists riot venvs; venv-env <hash> emits the env that activates a venv (VIRTUAL_ENV, PATH with venv bin first, PYTHONPATH site-packages, RIOT_*, DDTEST_SUITE_PATH), mirroring riot run --pass-env. Pure hint-based path math so it works without interpreters on PATH. Registered in run-script-doctests.py.
  • .gitlab/ddtest.yml: a standalone ddtest-internal-pilot job, gated when: manual + allow_failure: true, that builds its own venv, downloads ddtest, activates the venv via the bridge, and runs ddtest plan/run. Self-contained (no gen_gitlab_config.py wiring, no build_base_venvs dep).
  • .gitlab-ci.yml: include the pilot.

What: opt-in ddtest dogfood path for the internal suite. Why: validate ddtest plan/run + file splitting in real dd-trace-py CI as a first step toward replacing gitlab-level parallelization, keeping riot knowledge out of ddtest.
E2E testing: trigger ddtest-internal-pilot from the GitLab UI on a branch; it runs riot generate --python=3.10, downloads ddtest, activates the internal Py3.10 venv via scripts/ddtest-riot.py venv-env 4f70b3c, runs ddtest plan -p python -f pytest --tests-location tests/internal --min-parallelism 1 --max-parallelism 1, then ddtest run --ci-node 0 --ci-node-workers ncpu, and asserts ddtrace/pytest import in the venv.

Description

Testing

Risks

Additional Notes

Add an opt-in, manually-triggered CI path that runs the `internal` riot
suite through ddtest (plan + run + file splitting) instead of the legacy
`riot run <hash> -- --ddtrace` path, without any changes to ddtest and
without touching the generated-config pipeline.

ddtest stays unchanged; all riot knowledge lives in a new Python bridge
(`scripts/ddtest-riot.py`) that uses riot as a library:

- riotfile: the `internal` suite path is now a queryable env var
  (`DDTEST_SUITE_PATH`) instead of a literal baked into the pytest command,
  so `riot run internal` is unchanged with or without ddtest, and the
  bridge can read the suite location per venv.
- scripts/ddtest-riot.py: `hashes <pattern>` lists riot venvs;
  `venv-env <hash>` emits the env that activates a venv (VIRTUAL_ENV, PATH
  with venv bin first, PYTHONPATH site-packages, RIOT_*, DDTEST_SUITE_PATH),
  mirroring `riot run --pass-env`. Pure hint-based path math so it works
  without interpreters on PATH. Registered in run-script-doctests.py.
- .gitlab/ddtest.yml: a standalone `ddtest-internal-pilot` job, gated
  `when: manual` + `allow_failure: true`, that builds its own venv, downloads
  ddtest, activates the venv via the bridge, and runs `ddtest plan`/`run`.
  Self-contained (no gen_gitlab_config.py wiring, no build_base_venvs dep).
- .gitlab-ci.yml: include the pilot.

What: opt-in ddtest dogfood path for the `internal` suite.
Why: validate ddtest plan/run + file splitting in real dd-trace-py CI as a
first step toward replacing gitlab-level parallelization, keeping riot
knowledge out of ddtest.
E2E testing: trigger `ddtest-internal-pilot` from the GitLab UI on a
branch; it runs `riot generate --python=3.10`, downloads ddtest, activates
the `internal` Py3.10 venv via `scripts/ddtest-riot.py venv-env 4f70b3c`,
runs `ddtest plan -p python -f pytest --tests-location tests/internal
--min-parallelism 1 --max-parallelism 1`, then `ddtest run --ci-node 0
--ci-node-workers ncpu`, and asserts ddtrace/pytest import in the venv.
@cit-pr-commenter-54b7da

cit-pr-commenter-54b7da Bot commented Aug 26, 2026

Copy link
Copy Markdown

Codeowners resolved as

Resolved from the full PR diff against main using the target branch CODEOWNERS file.
CODEOWNERS team requests not listed below are not required by the current file set.

.gitlab/tests.yml                                                       @DataDog/python-guild @DataDog/apm-core-python
docs/ddtest-dogfooding.md                                               @DataDog/python-guild
riotfile.py                                                             @DataDog/apm-python
scripts/ddtest-riot.py                                                  @DataDog/python-guild
scripts/gen_gitlab_config.py                                            @DataDog/python-guild
scripts/run-script-doctests.py                                          @DataDog/python-guild
tests/appsec/suitespec.yml                                              @DataDog/asm-python
tests/contrib/suitespec.yml                                             @DataDog/python-guild
tests/llmobs/suitespec.yml                                              @DataDog/ml-observability
tests/suitespec.yml                                                     @DataDog/python-guild

@cit-pr-commenter-54b7da

cit-pr-commenter-54b7da Bot commented Aug 26, 2026

Copy link
Copy Markdown

Circular import analysis

⚠️ Existing circular imports

There are 3 circular imports that already exist on the base branch and have not been changed by this PR.

ddtrace.errortracking._handled_exceptions.bytecode_injector -> ddtrace.errortracking._handled_exceptions.callbacks -> ddtrace.errortracking._handled_exceptions.collector -> ddtrace.errortracking._handled_exceptions.bytecode_reporting -> ddtrace.errortracking._handled_exceptions.bytecode_injector
ddtrace.llmobs -> ddtrace.llmobs._evaluators -> ddtrace.llmobs._evaluators.format -> ddtrace.llmobs._experiment -> ddtrace.llmobs
ddtrace.appsec._asm_request_context -> ddtrace.appsec._iast._iast_request_context_base -> ddtrace.appsec._iast._iast_env -> ddtrace.appsec._iast.reporter -> ddtrace.appsec._exploit_prevention.stack_traces -> ddtrace.appsec._asm_request_context

@cit-pr-commenter-54b7da

cit-pr-commenter-54b7da Bot commented Aug 26, 2026

Copy link
Copy Markdown

Dependency direction analysis

⚠️ Existing dependency direction violations

There are 240 dependency direction violations that already exist on the base branch and have not been changed by this PR.

Show existing violations (showing 5 of 240 highest severity)
ddtrace.internal.tracemethods -×-> ddtrace.trace  (internal-core -> product:tracing, score=135)
ddtrace.llmobs._integrations.vllm -×-> ddtrace.trace  (product:llmobs -> product:tracing, score=133)
ddtrace.appsec._contrib.django -×-> ddtrace.trace  (product:appsec -> product:tracing, score=133)
ddtrace.profiling.collector.pytorch -×-> ddtrace.trace  (product:profiling -> product:tracing, score=133)
ddtrace.debugging._debugger -×-> ddtrace.trace  (product:debugging -> product:tracing, score=133)

To see all violations, download the layers-base.json and layers-pr.json artifacts from this CI job and run:

uv run --script scripts/import-analysis/layers.py compare layers-base.json layers-pr.json

@datadog-official

datadog-official Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Pipelines  Tests

Unblock PR with BitsAI

⚠️ Warnings

Your PR has failed checks. Please review the issues below and take necessary action before merging.

🚦 62 Pipeline jobs failed

DataDog/apm-reliability/dd-trace-py | build linux serverless: [amd64, cp315-cp315, v113741238-d2b8243-manylinux2014_x86_64, 1] — 🔧 Needs a code fix, caused by this PR

View more details · View in GitLab

DataDog/apm-reliability/dd-trace-py | build linux serverless: [arm64, cp315-cp315, v113741357-d2b8243-manylinux2014_aarch64, 1] — 🔧 Needs a code fix, caused by this PR

View more details · View in GitLab

DataDog/apm-reliability/dd-trace-py | contrib/botocore::ddtest-run: [12ce83b, 3.14, 0] — 🔧 Needs a code fix, caused by this PR

View more details · View in GitLab

View all 62 failed jobs.

ℹ️ Info

No other issues found (see more)

🧪 All tests passed
❄️ No new flaky tests detected

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 5253f9b | Docs | View more details | Give us feedback!

Document the end state we are building toward: running dd-trace-py's test
suites through ddtest (plan/run + file splitting) instead of the legacy
`riot run <hash> -- --ddtrace` path, replacing gitlab-level
parallelization.

Generalizes the standalone `internal` pilot (`.gitlab/ddtest.yml`) to all
test suites via gen_gitlab_config.py, behind a per-suite `ddtest: true`
suitespec key. ddtest stays unchanged; riot knowledge stays in the Python
bridge.

Covers: principles (single-venv ddtest, riot knowledge in the bridge,
suitespec-inherited selection, opt-in per suite, K from suitespec
parallelism), the riotfile/suitespec/gen_gitlab_config.py/tests.yml changes,
the CI graph (ddtest plan downstream of suitespec generation, so it does
not run on deselected suites/venvs), the rollout sequence, and what ddtest
does not change (including why snapshot suites parallelize safely with
per-job testagent services).

What: design doc for generalizing the ddtest pilot to all test jobs.
Why: capture the agreed design (Option 1: ddtest unchanged, riot knowledge
in the bridge) before implementing the gen_gitlab_config.py integration.
E2E testing: doc only; no behavior change.
@gnufede gnufede added changelog/no-changelog A changelog entry is not required for this PR. CI labels Aug 26, 2026
riot's Session.run applies Python str.format(cmdargs=...) to the venv
command before passing it to bash. The unescaped `${DDTEST_SUITE_PATH}` was
parsed as a str.format field `{DDTEST_SUITE_PATH}` (the `$` is literal),
raising `KeyError: 'DDTEST_SUITE_PATH'` during `riot run internal`.

Escape the braces as `${{DDTEST_SUITE_PATH}}` so str.format emits
`${DDTEST_SUITE_PATH}`, which bash then expands from the venv env to
`tests/internal/`. The resulting command is identical to the pre-change
`pytest ... tests/internal/`.

What: fix the riotfile regression from the ddtest dogfooding Step 1.
Why: riot's command formatting is str.format, not bash; `${VAR}` must be
escaped for str.format to survive to bash.
E2E testing: `riot run internal -- --ddtrace` now formats to
`pytest ... --ddtrace ${DDTEST_SUITE_PATH}/` and bash expands it to
`pytest ... --ddtrace tests/internal/`; `riot list internal` unchanged.
The staged deletion of .gitlab/ddtest.yml (part of the uncommitted
full-picture work) was swept into the riotfile fix commit by a
stash/pop, so the remote had the file deleted while .gitlab-ci.yml
still included it — GitLab rejected pipeline creation with
"Local file .gitlab/ddtest.yml does not exist!".

Restore the pilot file from b4040cc so the pushed branch is
self-consistent: .gitlab-ci.yml includes .gitlab/ddtest.yml and the
file exists. The full-picture round (which removes both the pilot and
its include together) remains uncommitted in the working tree.

What: restore the standalone pilot file that the pushed .gitlab-ci.yml
still references.
Why: keep the pushed branch consistent (include + file both present)
until the full picture lands as one unit.
E2E testing: GitLab pipeline creation no longer 400s on the missing
include; the manual ddtest-internal-pilot job is available again.
The pilot was `when: manual`, but manual jobs cannot be triggered on a
branch before it merges. Flip the rules to run automatically on every
pipeline (push and merge_request_event) so the ddtest path is exercised
on every push and its results are observable without merging.

`allow_failure: true` stays so a pilot failure does not block merges
while the ddtest path is being validated; drop it once the pilot is
green.

What: make ddtest-internal-pilot auto-run.
Why: observe ddtest dogfooding results without needing to merge first.
E2E testing: on the next push, ddtest-internal-pilot starts automatically
(stage: tests) and runs riot generate + ddtest plan/run in the Py3.10
internal venv via scripts/ddtest-riot.py.
@pr-commenter

pr-commenter Bot commented Aug 26, 2026

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2026-08-27 07:19:27

Comparing candidate commit c576ee2 in PR branch gnufede/ddtest-dogfooding with baseline commit 3e13bc9 in branch main.

📊 Benchmarking dashboard

Found 0 performance improvements and 5 performance regressions! Performance is the same for 581 metrics, 10 unstable metrics, 1 known flaky benchmarks, 17 flaky benchmarks without significant changes.

Explanation

This is an A/B test comparing a candidate commit's performance against that of a baseline commit. Performance changes are noted in the tables below as:

  • 🟩 = significantly better candidate vs. baseline
  • 🟥 = significantly worse candidate vs. baseline

We compute a confidence interval (CI) over the relative difference of means between metrics from the candidate and baseline commits, considering the baseline as the reference.

If the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD), the change is considered significant.

Feel free to reach out to #apm-benchmarking-platform on Slack if you have any questions.

More details about the CI and significant changes

You can imagine this CI as a range of values that is likely to contain the true difference of means between the candidate and baseline commits.

CIs of the difference of means are often centered around 0%, because often changes are not that big:

---------------------------------(------|---^--------)-------------------------------->
                              -0.6%    0%  0.3%     +1.2%
                                 |          |        |
         lower bound of the CI --'          |        |
sample mean (center of the CI) -------------'        |
         upper bound of the CI ----------------------'

As described above, a change is considered significant if the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD).

For instance, for an execution time metric, this confidence interval indicates a significantly worse performance:

----------------------------------------|---------|---(---------^---------)---------->
                                       0%        1%  1.3%      2.2%      3.1%
                                                  |   |         |         |
       significant impact threshold --------------'   |         |         |
                      lower bound of CI --------------'         |         |
       sample mean (center of the CI) --------------------------'         |
                      upper bound of CI ----------------------------------'

scenario:httppropagationinject-ids_only

  • 🟥 execution_time [+2.317µs; +2.429µs] or [+14.064%; +14.748%]

scenario:iastaspects-upper_aspect

  • 🟥 execution_time [+29.808µs; +35.687µs] or [+12.847%; +15.382%]

scenario:iastaspectsospath-ospathbasename_aspect

  • 🟥 execution_time [+120.954µs; +126.792µs] or [+30.793%; +32.279%]

scenario:telemetryaddmetric-1-count-metric-1-times

  • 🟥 execution_time [+531.056ns; +582.463ns] or [+19.776%; +21.690%]

scenario:tracer-small

  • 🟥 execution_time [+33.050µs; +35.208µs] or [+10.364%; +11.041%]

Unstable benchmarks

These benchmarks have a confidence interval too wide to call a change; treat them as noise rather than signal.

scenario:coreapiscenario-context_with_data_listeners

  • unstable execution_time [-726.074ns; +757.544ns] or [-6.564%; +6.849%]

scenario:coreapiscenario-core_dispatch_1_listener

  • unstable execution_time [-40.882ns; +25.348ns] or [-6.664%; +4.132%]

scenario:coreapiscenario-core_dispatch_50_listeners

  • unstable execution_time [-1711.157ns; +1590.357ns] or [-10.035%; +9.327%]

scenario:coreapiscenario-core_dispatch_exception_listeners

  • unstable execution_time [-1157.446ns; +1300.439ns] or [-9.004%; +10.116%]

scenario:coreapiscenario-core_dispatch_listeners

  • unstable execution_time [-323.776ns; +326.978ns] or [-8.860%; +8.948%]

scenario:coreapiscenario-core_dispatch_no_args_listeners

  • unstable execution_time [-255.302ns; +249.690ns] or [-8.815%; +8.621%]

scenario:coreapiscenario-core_dispatch_with_results_1_listener

  • unstable execution_time [-78.222ns; +66.467ns] or [-6.479%; +5.506%]

scenario:coreapiscenario-core_dispatch_with_results_50_listeners

  • unstable execution_time [-3858.397ns; +4181.873ns] or [-9.368%; +10.153%]

scenario:coreapiscenario-core_dispatch_with_results_listeners

  • unstable execution_time [-724.183ns; +815.505ns] or [-8.837%; +9.951%]

scenario:packagesupdateimporteddependencies-import_many_stdlib_cached

  • unstable execution_time [-58632.089ns; +59270.012ns] or [-9.402%; +9.504%]

Known flaky benchmarks

These benchmarks are marked as flaky and will not trigger a failure. Modify FLAKY_BENCHMARKS_REGEX to control which benchmarks are marked as flaky.

scenario:span-start

  • 🟥 execution_time [+1.458ms; +1.718ms] or [+10.782%; +12.704%]

Known flaky benchmarks without significant changes:

  • scenario:errortrackingflasksqli-baseline
  • scenario:flasksimple-iast-get
  • scenario:iastaspects-casefold_aspect
  • scenario:iastaspects-casefold_noaspect
  • scenario:iastaspects-index_aspect
  • scenario:iastaspects-ljust_noaspect
  • scenario:iastaspects-lower_aspect
  • scenario:iastaspects-replace_aspect
  • scenario:iastaspects-swapcase_aspect
  • scenario:iastaspects-title_noaspect
  • scenario:iastaspects-translate_aspect
  • scenario:iastaspects-translate_noaspect
  • scenario:iastaspects-upper_noaspect
  • scenario:packagespackageforrootmodulemapping-cache_off
  • scenario:packagespackageforrootmodulemapping-cache_on
  • scenario:sethttpmeta-all-enabled
  • scenario:telemetryaddmetric-record-100-metrics

gnufede added 11 commits August 26, 2026 10:44
The pilot rebuilt ddtrace inline via `riot generate`, which compiles every
native extension and OOMKilled the default-sized pod (exit 137) before
ddtest plan/run ever ran.

Move the pilot into the child pipeline (include .gitlab/ddtest.yml from
.gitlab/tests.yml instead of the parent .gitlab-ci.yml) so it can
`needs: build_base_venvs` and restore the prebuilt .riot/venv_py310
artifact. The pilot no longer runs `riot generate` — it reuses the
venv build_base_venvs already produced (with ddtrace compiled), so it
needs only default runner resources and never recompiles.

Also fix --tests-location: ddtest treats it as a raw glob (it does not
combine it with python_files), so "tests/internal" matched nothing;
use "tests/internal/**/test*.py" to actually discover the 50 files.

What: pilot reuses build_base_venvs instead of recompiling ddtrace.
Why: recompiling native extensions inline OOMs the pod; the venv
already exists in the child pipeline via build_base_venvs.
E2E testing: on the next push, ddtest-internal-pilot runs in the child
pipeline (stage: riot), restores the Py3.10 venv, downloads ddtest,
activates it via scripts/ddtest-riot.py, and runs plan/run — no compile.
Update the final-picture design doc with the lesson from the pilot OOM:
ddtest jobs must live in the child pipeline and reuse build_base_venvs
(never run `riot generate` / recompile ddtrace). An earlier pilot built
its own venv inline in the parent pipeline and OOMKilled recompiling
native extensions on a default-sized pod.

- Promote "never compiles ddtrace, reuses build_base_venvs" to principle 1.
- Correct the pilot rollout step: child pipeline (included from
  .gitlab/tests.yml), needs: build_base_venvs, no riot generate, auto-run,
  allow_failure: true. Note the OOM and the fix.
- Drop the stale "allow_failure: true initially / drop it once green"
  wording from the gen_gitlab_config.py step (per earlier feedback).

What: design-doc-only update from the pilot OOM learning.
Why: make the "never recompile" invariant explicit so the full-picture
implementation and any future pilot inherit it.
E2E testing: doc only; no behavior change.
The child pipeline's stages are rewritten by gen_gitlab_config.py from
the selected suites' stages; the `riot`/`exploration` stages in the
tests.yml template are overwritten. The `internal` suite is stage
`core`, so `core` is always present — but `riot` is not, and the
pilot's `stage: riot` made GitLab reject pipeline creation with
"chosen stage riot does not exist".

Use `stage: core` (the internal suite's stage), which is always in the
generated stages.

What: fix pilot stage to one that survives gen_gitlab_config's rewrite.
Why: gen_gitlab_config rewrites stages from selected suites; riot is
not among them.
E2E testing: on the next push, the child pipeline accepts the pilot job
and ddtest-internal-pilot runs in stage core.
The bridge emitted KEY=value lines unquoted, but riot's RIOT_VENV_FULL_PKGS
and _CI_DD_TAGS values contain spaces and '<'/'~' (e.g.
'hypothesis<6.45.1'). When the pilot `eval`s the output, bash read '<' as a
redirection and failed with "syntax error near unexpected token
`newline'".

Shell-quote values with shlex.quote so the output is safe to eval; ddtest
parses lines by splitting on the first '=', so a single-quoted value
round-trips.

What: quote venv-env values so eval is safe.
Why: unquoted values with spaces/metacharacters break the pilot's eval.
E2E testing: `eval "$(python scripts/ddtest-riot.py venv-env <hash>)"`
no longer errors; every value parses as a single shell token.
ddtest run failed with `ModuleNotFoundError: No module named
'envier'`. build_base_venvs only installs ddtrace into the base venv
(venv_py310); the per-hash deps (envier, pytest, ...) are installed
into the prefix (venv_py310_<ident>) lazily by `riot run` via
VenvInstance.prepare. The pilot skipped `riot run` entirely, so the
prefix was never prepared and envier was missing.

Add a prepare step — `riot -P -v run -s <hash> -- true` — before
activating the venv. `-s` skips the ddtrace reinstall (already in the
base from build_base_venvs) but still installs the per-hash deps
into the prefix, exactly as the legacy `riot run -s <hash> -- --ddtrace`
job does before pytest. ddtest then runs against the prepared prefix.

What: prepare the per-hash deps venv before ddtest.
Why: build_base_venvs does not install per-hash deps; riot run does,
and ddtest needs them present.
E2E testing: on the next push, `python -c "import ddtrace, pytest"`
passes (envier importable) and ddtest run executes the internal tests.
The pilot prepared the per-hash venv with `riot -P -v run -s <hash>
-- true` — a no-op riot command run solely to trigger
VenvInstance.prepare. That's a hack: it shells out to riot just to
call prepare, then ddtest runs outside riot.

Replace it with a `prepare` subcommand in the bridge that calls
riot's `VenvInstance.prepare(env, skip_deps=True)` directly — the
same library API `riot run` calls internally. skip_deps=True skips the
ddtrace reinstall (already in the base from build_base_venvs); the
per-hash deps (envier, ...) are installed into the prefix. Same
effect as the dummy riot call, but via the library API.

This is Option A (pragmatic): ddtest still runs outside riot, the
bridge still reconstructs the activation env. The long-term direction
(Option B) is to run ddtest inside riot so riot activates natively
and the bridge's env reconstruction is unnecessary; that's deferred
because it requires solving riot's baked-command obstacle.

What: bridge `prepare` subcommand; pilot calls it instead of riot run.
Why: the dummy riot run was a hack; the library API is the direct
equivalent.
E2E testing: on the next push, the pilot runs
`python scripts/ddtest-riot.py prepare 4f70b3c` (installs envier
etc. via VenvInstance.prepare), then ddtest plan/run with envier
importable.
The pilot's `prepare` step ran but `envier` was still missing at
`ddtest run` time, with no log line showing whether prepare
actually installed deps. Two changes to diagnose:

1. bridge `prepare` subcommand prints "Preparing/Prepared riot
   venv <hash>" markers so the CI log shows prepare ran (or
   silently no-op'd). Exceptions propagate (nonzero exit).

2. pilot verifies `import ddtrace, pytest, envier` immediately
   after prepare+activate, before ddtest plan/run. If prepare
   skipped the deps install (e.g. prefix existed but empty), the
   failure surfaces here with a clear cause instead of a confusing
   ModuleNotFoundError deep in ddtest run.

What: add prepare logging + early import verification.
Why: envier missing after prepare with no signal; need to see
whether prepare ran and whether deps are present.
E2E testing: on the next push, the log shows
"Preparing riot venv 4f70b3c" then either "Prepared" or an error,
and `python -c "import ...envier"` passes or fails loudly before
ddtest.
The bridge computed the base venv path from the Python hint ("3.10" ->
venv_py310), but riot's Interpreter.venv_path uses the full version
(version().replace(".","") -> venv_py31020 for Python 3.10.20 in CI).
So in CI the bridge pointed at venv_py310 while riot's real venv was
venv_py31020. Two consequences:

1. prepare: inst.prepare(skip_deps=True) saw the prefix at the wrong
   path (venv_py310_<ident>), and since it didn't exist there, the
   install was skipped or went to the wrong place — envier never landed
   in the venv ddtest ran in.

2. venv-env: PYTHONPATH pointed at venv_py310/.../site-packages
   (nonexistent) instead of venv_py31020/.../site-packages, so
   `import envier` failed even if installed.

Fix _inst_base_venv to prefer riot's own inst.py.venv_path (the real
path riot uses, including the patch version) and fall back to the
hint-based base_venv() only when the interpreter is not probeable
(local dev without that Python on PATH). This is the env-reconstruction
fragility flagged earlier; using riot's own path property avoids
diverging from riot's venv layout.

What: use inst.py.venv_path for the base venv path in the bridge.
Why: the hint-based path (venv_py310) diverges from riot's real path
(venv_py31020) when the interpreter has a patch version.
E2E testing: in CI, prepare installs envier into the correct prefix
(venv_py31020_<ident>), venv-env PYTHONPATH points at
venv_py31020/.../site-packages, and `import envier` succeeds.
The bridge emitted `KEY=value` lines, which `eval` sets as shell
variables but does NOT export. Child processes (python) therefore
don't see them: `PYTHONPATH` was set in the shell but `python -c
"import pytest"` failed with `ModuleNotFoundError: No module named
'pytest'` because python never received PYTHONPATH.

Emit `export KEY=value` so the variables are exported and inherited by
python subprocesses ddtest spawns. Verified locally: after the fix,
`eval "$(scripts/ddtest-riot.py venv-env <hash>)"` exports PYTHONPATH
and `python -c "import pytest"` succeeds.

What: prefix env lines with `export`.
Why: non-exported shell vars aren't inherited by child processes;
python needs PYTHONPATH (and VIRTUAL_ENV/PATH) in its environment.
E2E testing: in CI, `python -c "import ddtrace, pytest, envier"`
after eval succeeds; ddtest plan/run proceed.
ddtest run with --ci-node-workers ncpu spawned ~47 workers (one per
file) on the many-core k8s pod. All 47 wrote to the same pytest-cov
sqlite database, corrupting it:
  sqlite3.OperationalError: no such table: other_db.file

The legacy path avoids this: testrunner.yml sets
PYTEST_XDIST_AUTO_NUM_WORKERS=4 ("auto/logical doesn't work well
in k8s so we manually set it"). Match it: use 4 workers instead of
ncpu.

What: cap ddtest ci-node-workers at 4.
Why: ncpu on k8s pods is too high; 47 parallel workers corrupt
pytest-cov's sqlite DB.
E2E testing: ddtest run uses 4 xdist workers (matching legacy),
coverage DB no longer corrupts.
Generalize the ddtest dogfooding from a standalone pilot to the
generated-config pipeline. Suites opt in via `ddtest: true` in suitespec;
gen_gitlab_config.py emits ddtest-plan and ddtest-run jobs (per venv,
fanned out via parallel matrix) instead of the legacy riot job for that
suite. ddtest stays unchanged; riot knowledge stays in the bridge
(scripts/ddtest-riot.py).

Changes:
- scripts/gen_gitlab_config.py: SuiteVenvInfo carries per-venv
  (hash, python_hint) pairs; _emit_ddtest_jobs emits ddtest-plan
  (matrix over hashes) + ddtest-run (matrix over hashes x K nodes)
  with matrix-matched needs for per-hash plan artifacts.
- .gitlab/tests.yml: .ddtest_base(+_snapshot), .ddtest_plan (no
  snapshot variant), .ddtest_run(+_snapshot) templates. Prepare
  via bridge, activate, verify import. 4 workers (not ncpu).
  Raw glob --tests-location.
- tests/suitespec.yml: internal: ddtest: true.
- .gitlab/ddtest.yml: removed (pilot superseded); include removed in
  same commit (missing-include lesson).
- docs/ddtest-dogfooding.md: updated.

What: full-picture ddtest integration via gen_gitlab_config.py.
Why: replace gitlab-level parallelization with ddtest plan/run.
E2E testing: gen_gitlab_config emits ddtest-plan + ddtest-run for
internal; each plan prepares venv + writes .testoptimization/; each
run downloads plan + runs files via ddtest run --ci-node-workers 4.
@gnufede
gnufede force-pushed the gnufede/ddtest-dogfooding branch from 7267505 to 7ae1d3f Compare August 26, 2026 12:58
Add `ddtest: true` (with `allow_failure: true` for safety) to all 81
suites that have parallelism > 1 or venvs_per_job > 1 across all
suitespec files. `internal` was already opted in (without
allow_failure, since it's proven).

This generalizes the ddtest dogfooding from the `internal` pilot to
every suite that was previously parallelized at the gitlab level.
Opting in replaces the legacy riot job for that suite with
ddtest-plan + ddtest-run jobs (emitted by gen_gitlab_config.py).

`allow_failure: true` on the 81 new suites so a ddtest regression
does not block merges while each suite's ddtest path is validated in
CI; drop it per-suite as each goes green. `internal` keeps
allow_failure off (already proven by the pilot).

What: ddtest: true on all 81 parallelized suites.
Why: generalize ddtest dogfooding beyond the internal pilot.
E2E testing: gen_gitlab_config emits ddtest-plan + ddtest-run for
each opted suite; each plan prepares its venv, each run downloads its
plan and runs its files via ddtest.
Remove `allow_failure: true` from all 81 ddtest suites so
failures are visible and block merges, as requested. The `internal`
suite (proven by the pilot) never had `allow_failure`.

The `test_process_tags` snapshot failures (service: 'pytest' vs
expected 'tests.internal') are a known `python -m pytest` (ddtest)
vs `pytest` (riot) behavioral difference in ddtrace's own pytest
plugin, not a ddtest infrastructure bug; they will surface as
real failures now and need investigating as a follow-up.

What: remove allow_failure: true from all 81 ddtest suites.
Why: see failures directly instead of hiding them.
E2E testing: ddtest suite failures are now visible in CI.
When running `python -m pytest <files>`, CPython's runpy sets
sys.argv[0] to the pytest module's __init__.py file path (e.g.
/path/to/pytest/__init__.py). detect_service() in
_inferred_base_service.py treated sys.argv[0] as a user-provided
file path, resolved it, and deduce_package_name() walked up to find
pytest/__init__.py, returning "pytest" as the inferred service
name.

When running `pytest <files>` (console script), sys.argv[0] is
"pytest" (a command name, not a file path), which detect_service
skips, then processes the actual test file path, correctly
returning "tests.internal".

Fix: `executable_args = {0}` — always skip sys.argv[0] since it is
never a user-provided test file argument. This makes `python -m pytest`
and `pytest` produce the same service, matching existing
snapshots (no regeneration needed).

This resolves the 18 snapshot test failures
(test_process_tags_{activated,edge_case,error} × 6 Python versions)
seen in the ddtest dogfooding CI.

What: one-line fix in detect_service + 2 test cases.
Why: python -m pytest (ddtest) vs pytest (riot) produced
different span services; the snapshots expect the module name.
E2E testing: both `pytest file.py` and `python -m pytest file.py`
now produce "tests.internal" (matching snapshots).
Revert the core ddtrace fix (detect_service in
_inferred_base_service.py) and replace it with a test-only
workaround: the ddtest .ddtest_base template's before_script
now derives DD_SERVICE from DDTEST_SUITE_PATH (converting
/ to ., e.g. tests/internal -> tests.internal).

Root cause: `python -m pytest` (ddtest) sets sys.argv[0] to
pytest's __init__.py path, which detect_service picks up as a
test path, producing service='pytest' instead of the module
name. The legacy `pytest` (riot) invocation skips sys.argv[0]
and derives the service from the actual test file path.

The core fix (commit 4d64c36) changed detect_service to
always skip sys.argv[0], but that's a behavioral change for
all ddtrace users. This reverts it and uses a test-only
workaround instead: set DD_SERVICE explicitly in the ddtest
job env so the ddtrace pytest plugin reports the same
service as the legacy path.

What: revert core fix; set DD_SERVICE from DDTEST_SUITE_PATH
in ddtest templates.
Why: avoid changing core ddtrace logic; fix is test-only.
E2E testing: snapshot tests in test_process_tags should
see service=tests.internal (matching snapshots).
Add "DDTEST_SUITE_PATH": "<path>" to the env and replace the
literal test path in the command with ${{DDTEST_SUITE_PATH}} for
all 80 ddtest suites (internal was already done). This makes the
test location a queryable env var instead of a literal baked
into the pytest command, so the ddtest bridge can read the
suite location per venv and ddtest can use
--tests-location "${DDTEST_SUITE_PATH}/**/test*.py".

The ${{DDTEST_SUITE_PATH}} escaping is for riot's str.format
(${VAR} must be ${{VAR}} to survive to bash).

Special cases handled:
- contrib::django: child venv with --ignore flags — both paths
  replaced with ${{DDTEST_SUITE_PATH}}/...
- contrib::aiohttp: two test files — both replaced with
  ${{DDTEST_SUITE_PATH}}/...
- contrib::pymemcache: --ignore path — replaced with
  ${{DDTEST_SUITE_PATH}}/autopatch ${{DDTEST_SUITE_PATH}}
- ci_visibility::pytest, ci_visibility::ci_visibility: multi-line
  commands with --ignore — paths replaced, envs merged
- contrib::kafka, contrib::pymemcache, contrib::aiohttp: child
  venvs with command — parent gets env, child command gets path
  replaced
- integration: parent venv gets env + command replaced
- contrib::selenium: -c /dev/null preserved, env merged
- Suites with no env= (dd_coverage, crashtracker, etc.): env= added
  before command=

What: DDTEST_SUITE_PATH for all 80 remaining ddtest suites.
Why: ddtest bridge needs the suite path as an env var to
discover tests per venv.
E2E testing: all 81 ddtest suites have DDTEST_SUITE_PATH in env
and ${{DDTEST_SUITE_PATH}} in command; riot list and
gen_gitlab_config work.
K (per-venv CI node count for file splitting) was incorrectly
derived from the legacy parallelism/venvs_per_job knobs, which
control venv PACKING (how many venvs per CI job) — a different
axis from file splitting. This caused a huge job explosion
(e.g. parallelism=6 → 6 file-groups per venv × 12 venvs = 72 jobs
instead of legacy 6).

Fix: K defaults to 1 (no file splitting — each venv runs all its files
in one job, closest to legacy semantics). A suite can
override K with a new `ddtest_nodes` suitespec key. The legacy
parallelism/venvs_per_job knobs are no longer used for K.

Set `ddtest_nodes: 2` on the `internal` suite to test K=2
file splitting (12 venvs × 2 = 24 run jobs) as the first
non-trivial K.

What: decouple ddtest file splitting (K) from legacy venv packing.
Why: parallelism/venvs_per_job is venv packing, not file splitting;
reusing it for K caused a job explosion.
E2E testing: internal has K=2 (24 run jobs); other suites have
K=1 (one job per venv, no file splitting).
Remove `ddtest: true` from three suites that can't run under ddtest:

- `contrib::aws_lambda`: `datadog-lambda` pins ddtrace 2.20.1, but
  ddtest requires >= 4.10.3. Can't run until datadog-lambda upgrades.
- `contrib::azure_functions:eventhubs`: `_cffi_backend` missing in the
  `wait` venv (native dep issue in wait-for-services.py). Pre-existing,
  not caused by ddtest, but fails visibly without allow_failure.
- `appsec::appsec_integrations_pygoat`: command is `bash run_pygoat.sh ...`
  which starts the pygoat app AND runs pytest. ddtest only runs pytest,
  so the app isn't started → Connection refused. ddtest can't handle
  bash-script commands that start services.

Also revert the DD_SERVICE workaround (`export DD_SERVICE=$(echo
"${DDTEST_SUITE_PATH}" | tr '/' '.')` in .ddtest_base before_script).
It was wrong: it converts the path to dots (tests.contrib.djangorestframework)
but the expected service is the module name from detect_service (django),
not the path-as-dots. The root cause (python -m pytest vs pytest in
detect_service) needs a proper fix in ddtrace, not a workaround.

What: remove 3 unsuitable suites from ddtest; revert DD_SERVICE workaround.
Why: these suites have deps/commands incompatible with ddtest; the
DD_SERVICE workaround produces wrong services for some suites.
E2E testing: 79 ddtest suites remain; DD_SERVICE is no longer set
(snapshots will fail on service mismatch until detect_service is fixed).
ddtest plan takes ~90s, so file splitting (K=2) only makes sense for
suites that take at least 3 minutes (each half > 90s makes the
2x plan overhead worthwhile). For suites < 3 minutes, ddtest
adds overhead with no time gain.

Changes:
- Remove `ddtest: true` from 65 suites that take < 3 minutes
  (leaving them on the legacy riot path).
- Keep `ddtest: true` + `ddtest_nodes: 2` on the 14 suites
  that take > 3 minutes: internal, integration_agent,
  integration_testagent, contrib::opentelemetry, contrib::graphql,
  contrib::botocore, contrib::loguru, contrib::urllib3,
  contrib::structlog, contrib::molten, contrib::mako,
  contrib::logbook, llmobs::mistralai,
  appsec::appsec_threats_fastapi_iast.
- Add `ddtest: true` + `ddtest_nodes: 2` to the 9 new suites
  that were not previously opted in (graphql, loguru, urllib3,
  structlog, molten, mako, logbook, mistralai,
  appsec_threats_fastapi_iast).

The riotfile DDTEST_SUITE_PATH changes remain for all suites
(harmless for non-ddtest suites — legacy riot run still works).

What: limit ddtest to >3min suites; K=2 for those, opt out rest.
Why: ddtest plan overhead (~90s) only pays off for slow suites.
E2E testing: 14 ddtest suites with K=2; 65 suites revert to legacy.
@gnufede

gnufede commented Aug 26, 2026

Copy link
Copy Markdown
Member Author

@DataDog review

…ination

Two fixes:

1. Add DDTEST_SUITE_PATH env + ${{DDTEST_SUITE_PATH}} command to the 9 new
   ddtest suites (graphql, loguru, urllib3, structlog, molten, mako,
   logbook, mistralai, appsec_threats_fastapi_iast). These were added to
   suitespec but not to the riotfile, so --tests-location was empty and
   ddtest discovered 4,035 files (the whole repo) instead of the
   suite's files.

2. Fix PYTHONPATH contamination: the .ddtest_base before_script
   activated the venv with eval "$(scripts/ddtest-riot.py venv-env ...)"
   which exported PYTHONPATH pointing at the suite venv. The riot run -s
   wait -- testagent step (in before_script) inherited that PYTHONPATH
   and loaded modules from the wrong venv (e.g. typing_extensions from
   the molten venv while running in the wait venv). Fix: activate in a
   subshell in before_script (eval "..." && python -c "..."), and
   re-activate in each plan/run script.

What: add missing DDTEST_SUITE_PATH; fix PYTHONPATH leaking into wait.
Why: 9 suites had no suite path (4035 files discovered); PYTHONPATH
   contaminated the wait venv (typing_extensions version conflict).
E2E testing: each ddtest suite discovers only its files; wait-for-services
   runs in a clean env.

@datadog-official datadog-official Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Datadog Autotest: FAIL

The ddtest switch does not keep each suite's discovery path or pytest selection. Matrix needs also combine plan artifacts from unrelated riot hashes, so some new jobs can skip required tests or use a plan for another environment.

Open Bits AI session

🤖 Datadog Autotest · Commit 0f9bff1 · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest

Comment thread .gitlab/tests.yml Outdated
continue
k = _ddtest_k(suite_config)
LOGGER.info("Suite %s: ddtest (venvs=%d, nodes/venv=%d)", suite, len(venvs), k)
_emit_ddtest_jobs(f, suite, stage, clean_name, suite_config, venvs, k)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Keep suite-specific pytest selection

The integration jobs run the wrong test set and can fail under unsupported coverage or tracing settings.

Assertion details
  • Input: Generate integration_agent or integration_testagent through the new ddtest branch.
  • Expected: The generated jobs must keep the suite-specific pytest selection and required arguments.
  • Actual: The ddtest branch does not use the riot pytest command. It loses --no-cov and --ignore-glob='*civisibility*'. It then includes tests that the integration suite must exclude.

Was this helpful? React 👍 or 👎
🤖 Datadog Autotest · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest

Comment thread scripts/gen_gitlab_config.py Outdated
# Dedup PYTHON_VERSIONs: several hashes share a Python version, but
# build_base_venvs only needs to be downloaded once per version.
seen_py: set[str] = set()
for _h, py in venvs:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Use only the matching base environment

Large duplicate artifact downloads increase CI time and resource use for every matrix entry.

Assertion details
  • Input: Run an enabled suite that supports more than one Python version.
  • Expected: Each matrix entry must download only the base artifact for its current Python version.
  • Actual: Each plan and run entry needs the base artifacts for every Python version in the suite. The job uses only its own PYTHON_VERSION.

Was this helpful? React 👍 or 👎
🤖 Datadog Autotest · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest

gnufede added 18 commits August 26, 2026 17:51
scripts/ddtest-riot.py has a PEP 723 inline script metadata
(`# /// script` with `riot>=0.22.0`), so it must be invoked via its
shebang (`#!/usr/bin/env scripts/uv-run-script`) which uses
`uv run --script` to install riot. The ddtest templates were
invoking it as `python scripts/ddtest-riot.py ...` which bypasses
the shebang and runs with the system Python — which doesn't
have `riot` installed:
  ModuleNotFoundError: No module named 'riot'

Fix: use `scripts/ddtest-riot.py ...` (via shebang) instead of
`python scripts/ddtest-riot.py ...`, matching how other
scripts in the repo are invoked (e.g. `scripts/gen_gitlab_config.py`).

What: invoke ddtest-riot.py via shebang, not python.
Why: `python` bypasses uv-run-script, so riot isn't installed.
E2E testing: ddtest-riot.py prepare/venv-env now run via
uv-run-script which installs riot.
Address two PR review findings:

#2 (P1): Keep suite-specific pytest selection
The integration suites have --no-cov and --ignore-glob='*civisibility*' in
their riot commands. ddtest runs 'python -m pytest <files>' and doesn't
see these flags. Fix: extract the pytest flags from the riot command
(everything between 'pytest' and '{cmdargs}') and pass them as
PYTEST_ADDOPTS in the ddtest job variables. ddtest's Python platform
appends --ddtrace to PYTEST_ADDOPTS, so the flags are carried through.

#3 (P1): Match each run to its plan artifact
All plan jobs upload .testoptimization/ as an artifact, and all run jobs
download all of them — they overwrite each other. Fix: partition the
plan artifact by RIOT_HASH. The plan job moves .testoptimization/ to
.testoptimization-${RIOT_HASH}/ and uploads that. The run job renames
.testoptimization-${RIOT_HASH}/ back to .testoptimization/ before running
ddtest. Each run only uses its own venv's plan.

#4 (P2): not addressed — the needs: build_base_venvs matrix downloads
all Python versions' artifacts, but each job only uses its own
PYTHON_VERSION. This matches the legacy pattern (legacy jobs also
needs: build_base_venvs with a matrix), so it's not a regression.

What: PYTEST_ADDOPTS for riot flags; partition plan by RIOT_HASH.
Why: integration suites lost --no-cov/--ignore-glob; all plan
artifacts overwrote each other.
E2E testing: integration suites carry --no-cov --ignore-glob;
each run downloads only its own plan (partitioned by hash).
When running `python -m pytest <files>`, CPython's runpy sets
sys.argv[0] to the pytest module's __init__.py file path (e.g.
/path/to/pytest/__init__.py). detect_service() treated this as
a user-provided file path, and deduce_package_name() walked up
to find pytest/__init__.py, returning "pytest" as the inferred
service name.

When running `pytest <files>` (console script), sys.argv[0] is
"pytest" (a command name), which detect_service skips, then
processes the actual test file path, correctly returning
"tests.internal".

Fix: skip sys.argv[0] when it's a __init__.py file — a
__init__.py is a package marker, never a user-provided test file.
This is scoped: only __init__.py files are skipped (not all
sys.argv[0]), so non-pytest cases like `python my_script.py`
(where my_script.py IS the thing being traced) are unaffected.

This resolves the snapshot test failures (service: 'pytest' vs
expected 'tests.internal'/'tests.contrib.graphql') seen in the
ddtest dogfooding CI.

Two new test cases covering python -m pytest with module
__init__.py as sys.argv[0].

What: skip __init__.py as argv[0] in detect_service (scoped fix).
Why: python -m pytest sets argv[0] to pytest's __init__.py;
detect_service picks it up as a test path → wrong service.
E2E testing: both `pytest file.py` and `python -m pytest file.py`
produce the module name (matching snapshots).
When running via `python -m pytest <files>`, CPython's runpy sets
sys.argv[0] to the pytest module's __init__.py file path.
detect_service() in _inferred_base_service.py treats this as a
user-provided file path and returns 'pytest' as the inferred
service. When running via the `pytest` console script,
sys.argv[0] is 'pytest' (skipped by detect_service), so the
service is correctly inferred from the actual test file path.

Fix: in the pytest plugin's pytest_load_initial_conftests,
after the SessionManager is created, re-derive the service
from sys.argv with __init__.py filtered out (matching the
console-script behavior). If the corrected service differs
from the session manager's service, update it via
session.set_service().

This is scoped to the pytest plugin (not detect_service in
ddtrace.internal.settings), so non-pytest service inference
is unaffected.

Resolves the snapshot test failures (service: 'pytest' vs
expected 'tests.internal'/'tests.contrib.graphql') seen
in the ddtest dogfooding CI.

What: re-derive service in pytest plugin when argv[0] is __init__.py.
Why: python -m pytest (ddtest) sets argv[0] to pytest's __init__.py;
detect_service picks it up → wrong service. Fix in the plugin, not detect_service.
E2E testing: both `pytest file.py` and `python -m pytest file.py` produce
the module name (matching snapshots).
…se_service

The previous fix updated session_manager.service but not
BaseServiceProcessor._global_service, which was set from config.service
at import time (before the plugin fix runs). The _dd.base_service meta tag
is derived from _global_service, so snapshots still saw service='pytest'
on _dd.base_service even though session.service was corrected.

After correcting the service, also update
BaseServiceProcessor._global_service in the tracer's span aggregator
dd_processors list, so _dd.base_service matches the corrected service.

What: fix BaseServiceProcessor._global_service after service correction.
Why: _dd.base_service meta tag was still 'pytest' (from import-time config).
E2E testing: _dd.base_service now matches the corrected service
(tests.internal / tests.contrib.graphql).
Use list[t.Any] instead of t.List[t.Any] (PEP 585), remove unused
type: ignore comment, fix import ordering. All ruff/mypy
checks pass on plugin.py.
mypy error: Cannot call function of unknown type [operator] on
_proc._global_service = ... because _proc is t.Any (from
hasattr). Use setattr() instead of direct attribute access.
mypy error: Cannot call function of unknown type [operator] —
schematize_service_name is a module-level variable from a dict lookup,
mypy can't infer its type. Add type: ignore[operator] on the setattr line.
Verified with scripts/lint typing.
The plan job was fanned out into N×M jobs (one per suite × one per
Python version), causing too much CI runner contention for a step
that's just discovery + Datadog API. Each RIOT_HASH already maps to
exactly one PYTHON_VERSION, so the Python version dimension was
redundant.

Reduce to one plan job per suite (N, not N×M). The plan job loops
over all RIOT_HASHES sequentially: prepare + plan each, partitioning
the plan artifact by hash. The run jobs stay per-hash (they need
parallel execution). Run jobs download the single plan artifact
(which contains all hashes' plans, partitioned by hash) and
restore their own hash's plan.

Generator: plan job has no parallel matrix, sets RIOT_HASHES
variable to a space-separated list of all hashes. Run job
keeps its parallel: matrix over RIOT_HASH × PYTHON_VERSION ×
CI_NODE_INDEX.

Template: .ddtest_plan loops over ${RIOT_HASHES}, artifacts
are .testoptimization-*/ (all hashes).

What: one plan job per suite (loop over hashes sequentially).
Why: N×M plan jobs caused too much runner contention.
E2E testing: internal has 1 plan job (was 12), 24 run jobs (same).
…ssor fix

ddtrace.tracer is an attribute, not a module. Use 'import ddtrace as
_ddtrace; _ddtrace.tracer._span_aggregator' instead of
'import ddtrace.tracer as _tracer'.
python -m pytest sets argv[0] to pytest/__main__.py (not __init__.py).
The fix only checked for __init__.py, so it never triggered for
python -m pytest. Add __main__.py to the filter.
…hon -m pytest

ddtest hardcodes `python -m pytest` for RunTests and DiscoverTests,
which sets sys.argv[0] to pytest's __main__.py. This causes
detect_service to infer service='pytest' instead of the module
name (e.g. 'tests.contrib.graphql'), breaking snapshot tests.

Two changes:

1. ddtest (Go): PyTest.RunTests and DiscoverTests now use commandOverride
   when set (from --command or DD_TEST_OPTIMIZATION_RUNNER_COMMAND env var),
   instead of hardcoding `python -m pytest`. This lets the caller
   use `pytest` (the console script) which sets argv[0]='pytest'
   (skipped by detect_service → correct service inference).

2. gen_gitlab_config.py: set DD_TEST_OPTIMIZATION_RUNNER_COMMAND=pytest
   in ddtest job variables, so ddtest runs `pytest <files>` instead of
   `python -m pytest <files>`.

3. Revert the pytest plugin detect_service fix (ce0f0ca and
   follow-ups) — no longer needed since `pytest` (console script)
   sets argv[0]='pytest' which detect_service already handles.

What: use pytest console script via DD_TEST_OPTIMIZATION_RUNNER_COMMAND.
Why: python -m pytest sets argv[0] to __main__.py → wrong service.
E2E testing: ddtest runs `pytest <files>` (argv[0]='pytest'),
detect_service skips it → correct module name.
Until the commandOverride change is released, build ddtest
from the gnufede/ddtest-dogfooding branch instead of downloading
the released binary. This uses the Go toolchain already
available in the testrunner image.

The .ddtest_base before_script now:
  git clone --depth 1 --branch gnufede/ddtest-dogfooding https://github.com/DataDog/ddtest.git /tmp/ddtest-src
  (cd /tmp/ddtest-src && go build -o "${CI_PROJECT_DIR}/bin/ddtest" .)

Once the ddtest PR is merged and a new release is published, revert to
the curl download with DDTEST_VERSION.

What: build ddtest from source branch in CI.
Why: use the commandOverride change before it's released.
E2E testing: ddtest is built from the branch and used for plan/run.
The testrunner image doesn't have Go installed, so install it
from go.dev before building ddtest from the dogfooding branch.

  curl -fsSL https://go.dev/dl/go1.26.5.linux-amd64.tar.gz | tar xz -C /tmp
  export PATH="/tmp/go/bin:$PATH"
  git clone --depth 1 --branch gnufede/ddtest-dogfooding https://github.com/DataDog/ddtest.git /tmp/ddtest-src
  (cd /tmp/ddtest-src && go build -o "${CI_PROJECT_DIR}/bin/ddtest" .)

This adds ~50s to each ddtest job (Go download + build) but lets us
use the commandOverride change before it's released.

What: install Go in CI to build ddtest from source.
Why: testrunner image has no Go; can't build ddtest without it.
E2E testing: ddtest is built from the branch with Go installed in-job.
The eval "$(scripts/ddtest-riot.py venv-env ...)" && python -c "..."
was NOT a subshell — && chains commands in the same shell, so the
exported PYTHONPATH (pointing at the suite venv) leaked into the
before_script environment. Then riot run -s --pass-env wait
inherited that PYTHONPATH and loaded typing_extensions from the
wrong venv (e.g. molten venv while running in the wait venv).

Fix: wrap in parentheses to create a real subshell:
  (eval "..." && python -c "...")

The env vars are scoped to the subshell and don't leak.

What: use (eval ...) subshell for venv activation in before_script.
Why: && doesn't isolate env vars; PYTHONPATH leaked into wait-for-services.
E2E testing: wait-for-services runs with a clean PYTHONPATH.
The PYTEST_ADDOPTS extracted from riot commands includes
-n 8 --dist=worksteal (xdist worker count). During ddtest's
plan phase (which runs pytest --collect-only), xdist
tries to start workers and crashes with
"Unexpectedly no active workers available".

Fix: strip -n, --dist, and --numprocesses flags from
PYTEST_ADDOPTS. These are for test execution (run),
not collection (plan). ddtest's run phase uses
--ci-node-workers for parallelism, not xdist.

Verified: molten PYTEST_ADDOPTS is now empty (was
"-n 8 --dist=worksteal"); internal is "-v"
(was "-v -n auto --dist=worksteal").

What: strip xdist flags from PYTEST_ADDOPTS.
Why: xdist during plan's --collect-only crashes with no active workers.
E2E testing: plan phase runs without xdist workers.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog/no-changelog A changelog entry is not required for this PR. CI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant