Skip to content

feat(xtest): make xtest an installable distribution (DSPX-4793) - #598

Draft
dmihalcik-virtru wants to merge 4 commits into
mainfrom
DSPX-4793-xtest-buildable-distribution
Draft

dmihalcik-virtru wants to merge 4 commits into
mainfrom
DSPX-4793-xtest-buildable-distribution

Conversation

@dmihalcik-virtru

Copy link
Copy Markdown
Member

What

xtest is consumed by four downstream repos and cannot currently be installed by any of them. This makes xtest/ a real Python distribution: uv build produces a wheel, and pytest --pyargs xtest.tests works from a directory that is not the source tree.

Steps B1–B3 of DSPX-4793. Design and rationale in spec/DSPX-4793.md.

Why it did not work before

Four things, and fixing any three of them still leaves it uninstallable:

  1. No [build-system]xtest/pyproject.toml:59 said so out loud: "This is a test suite, not a distributable package." Nothing to uv add.
  2. Flat layout — every module sat at the top of xtest/ with no __init__.py. Installed, conftest.py lands at site-packages/conftest.py; pytest's default importmode=prepend derives the module name from the basename, so it and a consumer's own conftest.py both want to be the top-level module conftest and the second one collected raises ImportPathMismatchError. Same for every unqualified test_*.py, and for generic names like sizes, assertions, audit_logs. [tool.ruff.lint.isort] known-first-party enumerating eight top-level names was the tell.
  3. cwd-relative paths, some resolved at import timetdfs.py:671 (f"sdk/{sdk}/dist/{version}/cli.sh"), tdfs.py:947-948, otdfctl.py:55, and conftest.py:692 calling load_otdfctl() at module scope, which probes the filesystem while conftest is being imported.
  4. Data files opened by bare namemanifest.schema.json (via a mandatory SCHEMA_FILE), extra-keys.json, golden/*.tdf.

Commits

docs(spec) rewrite spec/DSPX-4793.md
refactor(xtest) pure move: 56 renames, zero content changes
feat(xtest) everything else

The move is deliberately its own commit so the relocation is readable as a move; review the third commit for actual content. That third commit carries B1's content changes together with B2 and B3 — splitting those apart would have required inventing intermediate states (e.g. a Path(__file__).parents[2] / "data" that exists for exactly one commit) that nobody reviews on their own merits.

Notable decisions

conftest.pyplugin.py, registered as a pytest11 entry point. Not cosmetic. A conftest.py inside an installed package is loaded after the command line is parsed, so its pytest_addoption never runs and all ~20 of the suite's options silently disappear. An entry-point plugin loads during startup, before argument parsing. -p no:xtest opts out.

Markers moved out of [tool.pytest.ini_options] into config.addinivalue_line in the plugin. That ini table is only read from the session's rootdir, which is the consumer's repo once installed — out-of-tree runs emitted four PytestUnknownMarkWarnings before this change.

load_otdfctl() is now lazy (functools.cache) and test_self.py takes the otdfctl fixture instead of constructing the tool at module scope. An always-loaded plugin must not touch the filesystem at import. With no SDK tree, the suite now logs at DEBUG and deselects rather than erroring during collection.

License metadata corrected from BSD-3-Clause to the SPDX expression BSD-3-Clause-Clear, matching the repo LICENSE. Different licence — the Clear variant withholds patent rights. Harmless while nothing was published; wrong the moment a wheel exists.

otdf-local's cross-package read of extra-keys.json is repointed. Its failure mode is return [], i.e. a platform that starts successfully with the wrong key set and no error.

Verification

$ uv build                                    # in xtest/
Successfully built dist/xtest-0.1.0.tar.gz
Successfully built dist/xtest-0.1.0-py3-none-any.whl     # 10,714,554 bytes, 71 members

$ cd /tmp/xtest-gate                          # NOT the source tree
$ uv run --no-project --python 3.14 --with ./xtest-0.1.0-py3-none-any.whl \
      pytest --pyargs xtest.tests --collect-only -q
335/337 tests collected (2 deselected) in 0.41s          # 0 errors, 0 warnings

$ uv run --no-project --python 3.14 --with ./xtest-0.1.0-py3-none-any.whl \
      pytest --no-header -q --pyargs xtest.tests.test_bench_stats ... (9 modules)
208 passed in 5.38s

No ImportPathMismatchError, no PytestUnknownMarkWarning.

In tree, unchanged: uv sync --locked --no-build; uv run --frozen --no-build pytest over the offline list → 208 passed in 5.16s; uv run pytest --collect-only -q → the same 335/337; ruff checkAll checks passed!; ruff format --check49 files already formatted; pyright0 errors, 0 warnings, 0 informations.

New knobs (all with backward-compatible defaults)

--sdk-dir <path>     pytest option; highest precedence
XT_SDK_DIR=<path>    default ./sdk
SCHEMA_FILE=<path>   now an override; was mandatory

Deferred

  • XT_ENV_FILE, though DSPX-4640 lists it under B2. Its only consumers are sdk/{go,java,js}/cli.sh and sdk/go/otdfctl.sh, which belong to DSPX-4791/DSPX-4792. A Python-side accessor nothing reads is dead code, and exporting a resolved value into the subprocess environment would change the env dict that 18 argv/env assertions in test_sdk_commands.py pin.
  • Entry-point registries for container_type/sdk_type/feature_type — DSPX-4794.
  • Any change to sdk/*/cli.sh — DSPX-4791/DSPX-4792. sdk/go/cli.sh:174 walks up for a pyproject.toml containing name = "xtest"; that line and the location of sdk/ are both unchanged, so it still resolves.
  • Publishing to an index. This produces a wheel; nothing tags, signs or uploads it.

Compatibility

Every existing option, env var and marker keeps its name and default, the distribution name and version are unchanged, and an in-tree uv run pytest from xtest/ behaves as before. The four repos pinning opentdf/tests@main must keep working — nothing in the public interface changed, but that is the argument, not the evidence: it needs a run in each consumer. Workflows in this repo are updated for the new layout (check.yml offline test list, xtest.yml test-file args and the extra-keys.json path, and the four now-redundant SCHEMA_FILE lines removed).

Known wart

The wheel is 10.2 MB and 10.0 MiB of it is one golden file, big-java-4.3.0-e0f8caf.tdf, which only test_legacy.py reads. Splitting the corpus behind an extra was considered and rejected for this slice — it doubles the release surface to fix a problem nobody has reported, and it is reversible.

xtest buildable distribution
@coderabbitai

coderabbitai Bot commented Sep 15, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Replace the Jira-dump scaffold with the seven-heading template: why the flat
layout is the install blocker (bare conftest at the site-packages root under
importmode=prepend), the four cwd-relative read sites with verified citations,
the pytest11-plugin-vs-packaged-conftest decision and the alternatives
rejected, and measured acceptance criteria.
Pure relocation, no content changes: 56 files renamed and nothing else, so
the diff is reviewable as a move. The follow-up commit makes it build.

  xtest/*.py                 -> xtest/src/xtest/
  xtest/conftest.py          -> xtest/src/xtest/plugin.py
  xtest/test_*.py            -> xtest/src/xtest/tests/
  xtest/{fixtures,perf}/     -> xtest/src/xtest/{fixtures,perf}/
  xtest/manifest.schema.json -> xtest/src/xtest/data/
  xtest/extra-keys.json      -> xtest/src/xtest/data/
  xtest/golden/              -> xtest/src/xtest/data/golden/

This commit alone does not run: imports, pyproject and the data-file reads
are fixed in the next one.
xtest is consumed by four downstream repos and could not be installed by any
of them: no [build-system] table, a flat layout whose bare conftest.py lands
at the site-packages root and collides with a consumer's own under
importmode=prepend, and data files opened by cwd-relative name.

B1 - buildable. Add [build-system] with the uv_build backend, matching the
otdf-sdk-mgr and otdf-local siblings. Add __init__.py for xtest, xtest.tests
and xtest.data, and qualify every intra-suite import. conftest.py becomes
plugin.py registered as a pytest11 entry point: an installed conftest is
loaded after the command line is parsed, so its pytest_addoption never runs,
which would silently drop all ~20 options. '-p no:xtest' opts out. isort
known-first-party collapses from eight top-level names to one. The license
metadata said BSD-3-Clause while the repo LICENSE is BSD-3-Clause-Clear, a
different license that withholds patent rights; corrected to the SPDX
expression now that a wheel with that metadata can exist.

B2 - no cwd coupling. New xtest.paths.sdk_dir() resolves --sdk-dir >
XT_SDK_DIR > ./sdk, decided in pytest_configure and written back to the
environment for subprocesses; the value is read on every call and never
captured at import. load_otdfctl() moves behind a functools.cache accessor
and test_self.py takes the otdfctl fixture instead of constructing the tool
at module scope, so an environment with no SDK tree deselects rather than
erroring during collection. Markers move from [tool.pytest.ini_options] to
config.addinivalue_line in the plugin: the ini table is only read from the
session rootdir, which is the consumer's repo once installed.

B3 - package data. manifest.schema.json, extra-keys.json and golden/*.tdf
resolve through importlib.resources via xtest.data. SCHEMA_FILE degrades from
mandatory to override, so the four workflow lines setting it come out.
test.env is generated per environment and is deliberately not shipped;
setup-cli-tool/action.yaml is a composite Action and cannot live in a wheel,
so it stays at the repo root.

Also repoints otdf-local's cross-package read of extra-keys.json, which
returns [] when the file is missing and would otherwise have started the
platform with the wrong key set and no error.

Verified: 'uv build' emits a wheel, and from /tmp,
'uv run --no-project --with <wheel> pytest --pyargs xtest.tests
--collect-only' collects 335/337 with no ImportPathMismatchError and no
unknown-marker warnings; the nine offline modules give 208 passed out of
tree. In tree, 'uv sync --locked --no-build' and the existing check.yml
invocation are unchanged in behaviour.

XT_ENV_FILE is deferred: its only consumers are sdk/*/cli.sh, which belongs
to DSPX-4791/DSPX-4792.
@dmihalcik-virtru
dmihalcik-virtru force-pushed the DSPX-4793-xtest-buildable-distribution branch from d0ca5fc to 35963ed Compare September 15, 2026 15:18
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
C Reliability Rating on New Code (required ≥ A)
C Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@github-actions

Copy link
Copy Markdown

X-Test Failure Report

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant