From cff554f3b299e04e5b1f67d42a02af695d3e500e Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Mon, 4 May 2026 13:34:00 -0400 Subject: [PATCH 01/13] feat(xtest): version-qualified SDK params and per-version CI matrix - Add `tdfs.parse_sdk_spec()` supporting `go`, `go:*`, `go:main`, `go:v0.18.0` specifiers - Update `--sdks`, `--sdks-encrypt`, `--sdks-decrypt` CLI options to accept version-qualified values via new `sdk_spec_type` validator - Replace `defaulted_list_opt`/`all_versions_of` expansion in `pytest_generate_tests` with `sdk_specs_opt` + `parse_sdk_spec` - Emit `sdk-version-list` (e.g. `["go:main","java:latest"]`) from `resolve-versions` job and drive `xct` matrix from it instead of a hardcoded `["go","java","js"]`; each SDK version now gets its own matrix cell - Replace `env.FOCUS_SDK == 'go'/'java'` step conditions with `startsWith(matrix.sdk-version, 'go:')` / `'java:'` - Update artifact names to include the full `sdk-version` specifier Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/xtest.yml | 22 +++++++++++---- xtest/conftest.py | 55 +++++++++++++++++-------------------- xtest/tdfs.py | 15 ++++++++++ 3 files changed, 56 insertions(+), 36 deletions(-) diff --git a/.github/workflows/xtest.yml b/.github/workflows/xtest.yml index 14c15019..9c400bb4 100644 --- a/.github/workflows/xtest.yml +++ b/.github/workflows/xtest.yml @@ -82,6 +82,7 @@ jobs: go: ${{ steps.version-info.outputs.go-version-info }} java: ${{ steps.version-info.outputs.java-version-info }} js: ${{ steps.version-info.outputs.js-version-info }} + sdk-version-list: ${{ steps.version-info.outputs.sdk-version-list }} env: PLATFORM_REF: "${{ inputs.platform-ref }}" JS_REF: "${{ inputs.js-ref }}" @@ -185,6 +186,15 @@ jobs: core.setOutput('all', JSON.stringify(versionData)); + const sdkVersionList = []; + for (const [sdkType, refInfo] of Object.entries(versionData)) { + if (sdkType === 'platform') continue; + for (const { tag, err } of refInfo) { + if (!err) sdkVersionList.push(`${sdkType}:${tag}`); + } + } + core.setOutput('sdk-version-list', JSON.stringify(sdkVersionList)); + core.summary.addHeading('Versions under Test', 3); function artifactLink(sdkType, tag, release, head, source) { @@ -263,12 +273,12 @@ jobs: pull-requests: write # Add comments to PRs env: FOCUS_SDK: ${{ inputs.focus-sdk || 'all' }} - ENCRYPT_SDK: ${{ matrix.sdk }} + ENCRYPT_SDK: ${{ matrix.sdk-version }} strategy: fail-fast: false matrix: platform-tag: ${{ fromJSON(needs.resolve-versions.outputs.platform-tag-list) }} - sdk: ["go", "java", "js"] + sdk-version: ${{ fromJSON(needs.resolve-versions.outputs.sdk-version-list) }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: @@ -408,7 +418,7 @@ jobs: if: >- steps.detect-otdfctl.outputs.otdfctl-source != 'platform' && fromJson(steps.configure-go.outputs.heads)[0] != null - && env.FOCUS_SDK == 'go' + && startsWith(matrix.sdk-version, 'go:') && contains(fromJSON(needs.resolve-versions.outputs.heads), matrix.platform-tag) env: PLATFORM_WORKING_DIR: ${{ steps.run-platform.outputs.platform-working-dir }} @@ -456,7 +466,7 @@ jobs: - name: pre-release protocol buffers for java-sdk if: >- fromJson(steps.configure-java.outputs.heads)[0] != null - && (env.FOCUS_SDK == 'go' || env.FOCUS_SDK == 'java') + && (startsWith(matrix.sdk-version, 'go:') || startsWith(matrix.sdk-version, 'java:')) && contains(fromJSON(needs.resolve-versions.outputs.heads), matrix.platform-tag) run: |- echo "Replacing .env files for java-sdk..." @@ -702,14 +712,14 @@ jobs: id: upload-artifact if: success() || failure() with: - name: ${{ job.status == 'success' && '✅' || job.status == 'failure' && '❌' }} ${{ matrix.sdk }}-${{matrix.platform-tag}} + name: ${{ job.status == 'success' && '✅' || job.status == 'failure' && '❌' }} ${{ matrix.sdk-version }}-${{matrix.platform-tag}} path: otdftests/xtest/test-results/*.html - name: Upload audit logs on failure uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 if: failure() with: - name: audit-logs-${{ matrix.sdk }}-${{ matrix.platform-tag }} + name: audit-logs-${{ matrix.sdk-version }}-${{ matrix.platform-tag }} path: otdftests/xtest/test-results/audit-logs/*.log if-no-files-found: ignore diff --git a/xtest/conftest.py b/xtest/conftest.py index ce6a7131..3d2fd912 100644 --- a/xtest/conftest.py +++ b/xtest/conftest.py @@ -60,6 +60,15 @@ def is_a(v: str) -> typing.Any: return is_a +def sdk_spec_type(v: str) -> str: + """Validate SDK specifiers: 'go', 'go:*', 'go:main', 'go:v0.18.0', etc.""" + for spec in v.split(): + sdk_type_val = spec.split(":", 1)[0] + if sdk_type_val not in typing.get_args(tdfs.sdk_type): + raise ValueError(f"Invalid SDK type: {sdk_type_val!r}") + return v + + def pytest_addoption(parser: pytest.Parser): """Add custom CLI options for pytest.""" parser.addoption( @@ -94,18 +103,18 @@ def pytest_addoption(parser: pytest.Parser): ) parser.addoption( "--sdks", - help=f"select which sdks to run by default, unless overridden, one or more of {englist(typing.get_args(tdfs.sdk_type))}", - type=is_type_or_list_of_types(tdfs.sdk_type), + help=f"select which sdks to run by default, unless overridden; one or more of {englist(typing.get_args(tdfs.sdk_type))}, optionally version-qualified (e.g. go:main, go:v0.18.0, go:*)", + type=sdk_spec_type, ) parser.addoption( "--sdks-decrypt", - help="select which sdks to run for decrypt only", - type=is_type_or_list_of_types(tdfs.sdk_type), + help="select which sdks to run for decrypt only; accepts same format as --sdks", + type=sdk_spec_type, ) parser.addoption( "--sdks-encrypt", - help="select which sdks to run for encrypt only", - type=is_type_or_list_of_types(tdfs.sdk_type), + help="select which sdks to run for encrypt only; accepts same format as --sdks", + type=sdk_spec_type, ) @@ -139,43 +148,29 @@ def list_opt(name: str, t: typing.Any) -> list[str]: raise ValueError(f"Invalid value for {name}: {i}, must be one of {ttt}") return a - def defaulted_list_opt[T]( - names: list[str], t: typing.Any, default: list[T] - ) -> list[T]: + def sdk_specs_opt(names: list[str]) -> list[str]: + """Return SDK specifier tokens from the first matching option, or all sdk types.""" for name in names: v = metafunc.config.getoption(name) if v: - return cast(list[T], list_opt(name, t)) - return default + return v.split() + return list(typing.get_args(tdfs.sdk_type)) subject_sdks: set[tdfs.SDK] = set() if "encrypt_sdk" in metafunc.fixturenames: - encrypt_sdks: list[tdfs.sdk_type] = [] - encrypt_sdks = defaulted_list_opt( - ["--sdks-encrypt", "--sdks"], - tdfs.sdk_type, - list(typing.get_args(tdfs.sdk_type)), - ) - # convert list of sdk_type to list of SDK objects e_sdks = [ - v - for sdks in [tdfs.all_versions_of(sdk) for sdk in encrypt_sdks] - for v in sdks + sdk + for spec in sdk_specs_opt(["--sdks-encrypt", "--sdks"]) + for sdk in tdfs.parse_sdk_spec(spec) ] metafunc.parametrize("encrypt_sdk", e_sdks, ids=[str(x) for x in e_sdks]) subject_sdks |= set(e_sdks) if "decrypt_sdk" in metafunc.fixturenames: - decrypt_sdks: list[tdfs.sdk_type] = [] - decrypt_sdks = defaulted_list_opt( - ["--sdks-decrypt", "--sdks"], - tdfs.sdk_type, - list(typing.get_args(tdfs.sdk_type)), - ) d_sdks = [ - v - for sdks in [tdfs.all_versions_of(sdk) for sdk in decrypt_sdks] - for v in sdks + sdk + for spec in sdk_specs_opt(["--sdks-decrypt", "--sdks"]) + for sdk in tdfs.parse_sdk_spec(spec) ] metafunc.parametrize("decrypt_sdk", d_sdks, ids=[str(x) for x in d_sdks]) subject_sdks |= set(d_sdks) diff --git a/xtest/tdfs.py b/xtest/tdfs.py index 0c4c9c61..575c11a6 100644 --- a/xtest/tdfs.py +++ b/xtest/tdfs.py @@ -515,6 +515,21 @@ def all_versions_of(sdk: sdk_type) -> list[SDK]: return versions +def parse_sdk_spec(spec: str) -> list[SDK]: + """Parse an SDK specifier into SDK objects. + + Supports: + - "go" or "go:*" → all versions in sdk/go/dist/ + - "go:main" or "go:v0.18.0" → only that specific version + """ + if ":" in spec: + sdk_type_val, version = spec.split(":", 1) + if version == "*": + return all_versions_of(sdk_type_val) + return [SDK(sdk_type_val, version)] + return all_versions_of(spec) + + def skip_if_unsupported(sdk: SDK, *features: feature_type): pfs = get_platform_features() pfs.skip_if_unsupported(*features) From 1cc03562ddfc72a845325315e15383ef00410887 Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Mon, 4 May 2026 17:03:58 -0400 Subject: [PATCH 02/13] fixup pyright --- xtest/pyproject.toml | 2 ++ xtest/tdfs.py | 9 ++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/xtest/pyproject.toml b/xtest/pyproject.toml index 3e7b6d08..f40020b0 100644 --- a/xtest/pyproject.toml +++ b/xtest/pyproject.toml @@ -87,6 +87,8 @@ indent-style = "space" [tool.pyright] pythonVersion = "3.14" typeCheckingMode = "standard" +venvPath = "." +venv = ".venv" include = ["."] exclude = ["**/__pycache__", ".venv", ".ruff_cache", ".pytest_cache", "sdk", "tmp", "golden"] reportMissingImports = true diff --git a/xtest/tdfs.py b/xtest/tdfs.py index 575c11a6..a5597d88 100644 --- a/xtest/tdfs.py +++ b/xtest/tdfs.py @@ -8,7 +8,7 @@ import zipfile from collections.abc import Callable from pathlib import Path -from typing import Any, Literal +from typing import Any, Literal, TypeIs, get_args import jsonschema import pytest @@ -23,6 +23,11 @@ sdk_type = Literal["go", "java", "js"] + +def is_sdk_type(val: str) -> TypeIs[sdk_type]: + return val in get_args(sdk_type) + + focus_type = Literal[sdk_type, "all"] container_type = Literal[ @@ -524,9 +529,11 @@ def parse_sdk_spec(spec: str) -> list[SDK]: """ if ":" in spec: sdk_type_val, version = spec.split(":", 1) + assert is_sdk_type(sdk_type_val), f"Unknown SDK type: {sdk_type_val!r}" if version == "*": return all_versions_of(sdk_type_val) return [SDK(sdk_type_val, version)] + assert is_sdk_type(spec), f"Unknown SDK type: {spec!r}" return all_versions_of(spec) From a55003f689857571b2e4813351378a6103bf2441 Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Mon, 4 May 2026 17:14:47 -0400 Subject: [PATCH 03/13] Update xtest/conftest.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- xtest/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xtest/conftest.py b/xtest/conftest.py index 3d2fd912..5b1a9573 100644 --- a/xtest/conftest.py +++ b/xtest/conftest.py @@ -64,7 +64,7 @@ def sdk_spec_type(v: str) -> str: """Validate SDK specifiers: 'go', 'go:*', 'go:main', 'go:v0.18.0', etc.""" for spec in v.split(): sdk_type_val = spec.split(":", 1)[0] - if sdk_type_val not in typing.get_args(tdfs.sdk_type): + if not tdfs.is_sdk_type(sdk_type_val): raise ValueError(f"Invalid SDK type: {sdk_type_val!r}") return v From 32bbf6b6708eb7471352b5fe88ff040505b92b65 Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Mon, 4 May 2026 17:15:01 -0400 Subject: [PATCH 04/13] Update xtest/tdfs.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- xtest/tdfs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/xtest/tdfs.py b/xtest/tdfs.py index a5597d88..cc1e8189 100644 --- a/xtest/tdfs.py +++ b/xtest/tdfs.py @@ -533,7 +533,8 @@ def parse_sdk_spec(spec: str) -> list[SDK]: if version == "*": return all_versions_of(sdk_type_val) return [SDK(sdk_type_val, version)] - assert is_sdk_type(spec), f"Unknown SDK type: {spec!r}" + if not is_sdk_type(spec): + raise ValueError(f"Unknown SDK type: {spec!r}") return all_versions_of(spec) From bce4da7cf169f297504545f8a7b9e5c40b5a3e39 Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Mon, 4 May 2026 17:15:12 -0400 Subject: [PATCH 05/13] Update xtest/tdfs.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- xtest/tdfs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/xtest/tdfs.py b/xtest/tdfs.py index cc1e8189..848b45ac 100644 --- a/xtest/tdfs.py +++ b/xtest/tdfs.py @@ -529,7 +529,8 @@ def parse_sdk_spec(spec: str) -> list[SDK]: """ if ":" in spec: sdk_type_val, version = spec.split(":", 1) - assert is_sdk_type(sdk_type_val), f"Unknown SDK type: {sdk_type_val!r}" + if not is_sdk_type(sdk_type_val): + raise ValueError(f"Unknown SDK type: {sdk_type_val!r}") if version == "*": return all_versions_of(sdk_type_val) return [SDK(sdk_type_val, version)] From 126062ff8e46a4e5207fe511d6e5985bbafc017d Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Tue, 5 May 2026 08:30:17 -0400 Subject: [PATCH 06/13] fix(xtest): use '@' instead of ':' as sdk specifier separator Colons are not allowed in GitHub Actions artifact names and are problematic in file names; '@' is the idiomatic version separator (npm, Go module proxy, etc.). Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/xtest.yml | 6 +++--- xtest/conftest.py | 4 ++-- xtest/tdfs.py | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/xtest.yml b/.github/workflows/xtest.yml index 9c400bb4..69dff104 100644 --- a/.github/workflows/xtest.yml +++ b/.github/workflows/xtest.yml @@ -190,7 +190,7 @@ jobs: for (const [sdkType, refInfo] of Object.entries(versionData)) { if (sdkType === 'platform') continue; for (const { tag, err } of refInfo) { - if (!err) sdkVersionList.push(`${sdkType}:${tag}`); + if (!err) sdkVersionList.push(`${sdkType}@${tag}`); } } core.setOutput('sdk-version-list', JSON.stringify(sdkVersionList)); @@ -418,7 +418,7 @@ jobs: if: >- steps.detect-otdfctl.outputs.otdfctl-source != 'platform' && fromJson(steps.configure-go.outputs.heads)[0] != null - && startsWith(matrix.sdk-version, 'go:') + && startsWith(matrix.sdk-version, 'go@') && contains(fromJSON(needs.resolve-versions.outputs.heads), matrix.platform-tag) env: PLATFORM_WORKING_DIR: ${{ steps.run-platform.outputs.platform-working-dir }} @@ -466,7 +466,7 @@ jobs: - name: pre-release protocol buffers for java-sdk if: >- fromJson(steps.configure-java.outputs.heads)[0] != null - && (startsWith(matrix.sdk-version, 'go:') || startsWith(matrix.sdk-version, 'java:')) + && (startsWith(matrix.sdk-version, 'go@') || startsWith(matrix.sdk-version, 'java@')) && contains(fromJSON(needs.resolve-versions.outputs.heads), matrix.platform-tag) run: |- echo "Replacing .env files for java-sdk..." diff --git a/xtest/conftest.py b/xtest/conftest.py index 5b1a9573..4067bfc5 100644 --- a/xtest/conftest.py +++ b/xtest/conftest.py @@ -61,9 +61,9 @@ def is_a(v: str) -> typing.Any: def sdk_spec_type(v: str) -> str: - """Validate SDK specifiers: 'go', 'go:*', 'go:main', 'go:v0.18.0', etc.""" + """Validate SDK specifiers: 'go', 'go@*', 'go@main', 'go@v0.18.0', etc.""" for spec in v.split(): - sdk_type_val = spec.split(":", 1)[0] + sdk_type_val = spec.split("@", 1)[0] if not tdfs.is_sdk_type(sdk_type_val): raise ValueError(f"Invalid SDK type: {sdk_type_val!r}") return v diff --git a/xtest/tdfs.py b/xtest/tdfs.py index 848b45ac..c602c4d5 100644 --- a/xtest/tdfs.py +++ b/xtest/tdfs.py @@ -524,11 +524,11 @@ def parse_sdk_spec(spec: str) -> list[SDK]: """Parse an SDK specifier into SDK objects. Supports: - - "go" or "go:*" → all versions in sdk/go/dist/ - - "go:main" or "go:v0.18.0" → only that specific version + - "go" or "go@*" → all versions in sdk/go/dist/ + - "go@main" or "go@v0.18.0" → only that specific version """ - if ":" in spec: - sdk_type_val, version = spec.split(":", 1) + if "@" in spec: + sdk_type_val, version = spec.split("@", 1) if not is_sdk_type(sdk_type_val): raise ValueError(f"Unknown SDK type: {sdk_type_val!r}") if version == "*": From e2fb79a0bc6f19c60925e66cebf6dc3bedf19d6e Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Tue, 5 May 2026 09:02:51 -0400 Subject: [PATCH 07/13] fix(xtest): harden sdk specifier validation and CI matrix guard - Fail xtest capstone when xct matrix is skipped (empty sdk-version-list would previously produce a green run with zero tests executed) - Correct --sdks help text examples from ':' to '@' separator - Reject empty version strings (e.g. 'go@') and whitespace-only input in sdk_spec_type at argument-parse time with a clear error message - Wrap e_sdks/d_sdks comprehensions in pytest_generate_tests to convert FileNotFoundError/ValueError into pytest.UsageError Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/xtest.yml | 4 ++-- xtest/conftest.py | 43 +++++++++++++++++++++++-------------- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/.github/workflows/xtest.yml b/.github/workflows/xtest.yml index 69dff104..bf120510 100644 --- a/.github/workflows/xtest.yml +++ b/.github/workflows/xtest.yml @@ -775,9 +775,9 @@ jobs: contents: read steps: - name: Assert all matrix jobs passed - if: ${{ needs.xct.result == 'failure' || needs.xct.result == 'cancelled' }} + if: ${{ needs.xct.result == 'failure' || needs.xct.result == 'cancelled' || needs.xct.result == 'skipped' }} run: |- - echo "xct matrix had failures (overall result: ${XCT_RESULT}). Marking xtest failed." >> "$GITHUB_STEP_SUMMARY" + echo "xct matrix failed or was skipped (overall result: ${XCT_RESULT}). Marking xtest failed." >> "$GITHUB_STEP_SUMMARY" exit 1 env: XCT_RESULT: ${{ needs.xct.result }} diff --git a/xtest/conftest.py b/xtest/conftest.py index 4067bfc5..920f6f82 100644 --- a/xtest/conftest.py +++ b/xtest/conftest.py @@ -61,11 +61,16 @@ def is_a(v: str) -> typing.Any: def sdk_spec_type(v: str) -> str: - """Validate SDK specifiers: 'go', 'go@*', 'go@main', 'go@v0.18.0', etc.""" - for spec in v.split(): - sdk_type_val = spec.split("@", 1)[0] - if not tdfs.is_sdk_type(sdk_type_val): - raise ValueError(f"Invalid SDK type: {sdk_type_val!r}") + """Validate a whitespace-separated list of SDK specifiers: 'go', 'go@*', 'go@main java@v1.2.0', etc.""" + specs = v.split() + if not specs: + raise ValueError("At least one SDK specifier is required") + for spec in specs: + parts = spec.split("@", 1) + if not tdfs.is_sdk_type(parts[0]): + raise ValueError(f"Invalid SDK type: {parts[0]!r}") + if len(parts) == 2 and not parts[1]: + raise ValueError(f"Empty version in SDK specifier {spec!r}; use e.g. go@main, go@v0.18.0, go@*") return v @@ -103,7 +108,7 @@ def pytest_addoption(parser: pytest.Parser): ) parser.addoption( "--sdks", - help=f"select which sdks to run by default, unless overridden; one or more of {englist(typing.get_args(tdfs.sdk_type))}, optionally version-qualified (e.g. go:main, go:v0.18.0, go:*)", + help=f"select which sdks to run by default, unless overridden; one or more of {englist(typing.get_args(tdfs.sdk_type))}, optionally version-qualified (e.g. go@main, go@v0.18.0, go@*)", type=sdk_spec_type, ) parser.addoption( @@ -159,19 +164,25 @@ def sdk_specs_opt(names: list[str]) -> list[str]: subject_sdks: set[tdfs.SDK] = set() if "encrypt_sdk" in metafunc.fixturenames: - e_sdks = [ - sdk - for spec in sdk_specs_opt(["--sdks-encrypt", "--sdks"]) - for sdk in tdfs.parse_sdk_spec(spec) - ] + try: + e_sdks = [ + sdk + for spec in sdk_specs_opt(["--sdks-encrypt", "--sdks"]) + for sdk in tdfs.parse_sdk_spec(spec) + ] + except (FileNotFoundError, ValueError) as e: + raise pytest.UsageError(str(e)) from e metafunc.parametrize("encrypt_sdk", e_sdks, ids=[str(x) for x in e_sdks]) subject_sdks |= set(e_sdks) if "decrypt_sdk" in metafunc.fixturenames: - d_sdks = [ - sdk - for spec in sdk_specs_opt(["--sdks-decrypt", "--sdks"]) - for sdk in tdfs.parse_sdk_spec(spec) - ] + try: + d_sdks = [ + sdk + for spec in sdk_specs_opt(["--sdks-decrypt", "--sdks"]) + for sdk in tdfs.parse_sdk_spec(spec) + ] + except (FileNotFoundError, ValueError) as e: + raise pytest.UsageError(str(e)) from e metafunc.parametrize("decrypt_sdk", d_sdks, ids=[str(x) for x in d_sdks]) subject_sdks |= set(d_sdks) From f6159d73e858155b363e427635592f40dfaf825c Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Tue, 5 May 2026 09:03:57 -0400 Subject: [PATCH 08/13] style: ruff format conftest.py Co-Authored-By: Claude Sonnet 4.6 --- xtest/conftest.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/xtest/conftest.py b/xtest/conftest.py index 920f6f82..ec0a21b9 100644 --- a/xtest/conftest.py +++ b/xtest/conftest.py @@ -70,7 +70,9 @@ def sdk_spec_type(v: str) -> str: if not tdfs.is_sdk_type(parts[0]): raise ValueError(f"Invalid SDK type: {parts[0]!r}") if len(parts) == 2 and not parts[1]: - raise ValueError(f"Empty version in SDK specifier {spec!r}; use e.g. go@main, go@v0.18.0, go@*") + raise ValueError( + f"Empty version in SDK specifier {spec!r}; use e.g. go@main, go@v0.18.0, go@*" + ) return v From f60fc29adc8a866e4d4e48c375d7e178c9a8398e Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Tue, 5 May 2026 09:18:36 -0400 Subject: [PATCH 09/13] fixup legacy --- .github/workflows/xtest.yml | 2 +- xtest/tdfs.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/xtest.yml b/.github/workflows/xtest.yml index bf120510..44f7f3bf 100644 --- a/.github/workflows/xtest.yml +++ b/.github/workflows/xtest.yml @@ -569,7 +569,7 @@ jobs: ######## RUN THE TESTS ############# - name: Run legacy decryption tests run: |- - uv run pytest -n auto --dist worksteal --html=test-results/sdk-${FOCUS_SDK}-${PLATFORM_TAG}.html --self-contained-html --sdks-encrypt "${ENCRYPT_SDK}" -ra -v --focus "$FOCUS_SDK" test_legacy.py + uv run pytest -n auto --dist worksteal --html=test-results/sdk-${FOCUS_SDK}-${PLATFORM_TAG}.html --self-contained-html --sdks-decrypt "${ENCRYPT_SDK}" -ra -v --focus "$FOCUS_SDK" test_legacy.py working-directory: otdftests/xtest env: PLATFORM_DIR: "../../${{ steps.run-platform.outputs.platform-working-dir }}" diff --git a/xtest/tdfs.py b/xtest/tdfs.py index c602c4d5..46443bc3 100644 --- a/xtest/tdfs.py +++ b/xtest/tdfs.py @@ -359,6 +359,7 @@ def simple_container(container: container_type) -> container_type: class SDK: sdk: sdk_type + version: str _supports: dict[feature_type, bool] def __init__(self, sdk: sdk_type, version: str = "main"): From 75a2f0b96d0e23def4409c48066761a989dd186f Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Tue, 5 May 2026 09:35:00 -0400 Subject: [PATCH 10/13] feat(xtest): skip released-only SDK pairs on non-dispatch CI runs Add SDK.is_released() to detect semver-tagged versions, a --skip-released-pairs pytest flag, and a pytest_runtest_setup hook that automatically skips any round-trip test where both encrypt_sdk and decrypt_sdk are released artifacts. The workflow sets SKIP_RELEASED_PAIRS on all non-workflow_dispatch triggers so PR runs skip redundant old-vs-old pairs without requiring changes to individual test files. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/xtest.yml | 32 +++++++++++++++++--------------- xtest/conftest.py | 17 +++++++++++++++++ xtest/tdfs.py | 3 +++ 3 files changed, 37 insertions(+), 15 deletions(-) diff --git a/.github/workflows/xtest.yml b/.github/workflows/xtest.yml index 44f7f3bf..96637a4f 100644 --- a/.github/workflows/xtest.yml +++ b/.github/workflows/xtest.yml @@ -274,6 +274,7 @@ jobs: env: FOCUS_SDK: ${{ inputs.focus-sdk || 'all' }} ENCRYPT_SDK: ${{ matrix.sdk-version }} + SKIP_RELEASED_PAIRS: ${{ github.event_name != 'workflow_dispatch' }} strategy: fail-fast: false matrix: @@ -569,7 +570,8 @@ jobs: ######## RUN THE TESTS ############# - name: Run legacy decryption tests run: |- - uv run pytest -n auto --dist worksteal --html=test-results/sdk-${FOCUS_SDK}-${PLATFORM_TAG}.html --self-contained-html --sdks-decrypt "${ENCRYPT_SDK}" -ra -v --focus "$FOCUS_SDK" test_legacy.py + skip_flag=$([[ "$SKIP_RELEASED_PAIRS" == "true" ]] && echo "--skip-released-pairs" || echo "") + uv run pytest -n auto --dist worksteal --html=test-results/sdk-${FOCUS_SDK}-${PLATFORM_TAG}.html --self-contained-html --sdks-decrypt "${ENCRYPT_SDK}" -ra -v --focus "$FOCUS_SDK" $skip_flag test_legacy.py working-directory: otdftests/xtest env: PLATFORM_DIR: "../../${{ steps.run-platform.outputs.platform-working-dir }}" @@ -578,7 +580,8 @@ jobs: - name: Run all standard xtests if: ${{ env.FOCUS_SDK == 'all' }} run: |- - uv run pytest -n auto --dist loadscope --html=test-results/sdk-${FOCUS_SDK}-${PLATFORM_TAG}.html --self-contained-html --sdks-encrypt "${ENCRYPT_SDK}" -ra -v test_tdfs.py test_policytypes.py test_pqc.py + skip_flag=$([[ "$SKIP_RELEASED_PAIRS" == "true" ]] && echo "--skip-released-pairs" || echo "") + uv run pytest -n auto --dist loadscope --html=test-results/sdk-${FOCUS_SDK}-${PLATFORM_TAG}.html --self-contained-html --sdks-encrypt "${ENCRYPT_SDK}" -ra -v $skip_flag test_tdfs.py test_policytypes.py test_pqc.py working-directory: otdftests/xtest env: PLATFORM_DIR: "../../${{ steps.run-platform.outputs.platform-working-dir }}" @@ -588,7 +591,8 @@ jobs: - name: Run xtests focusing on a specific SDK if: ${{ env.FOCUS_SDK != 'all' }} run: |- - uv run pytest -n auto --dist loadscope --html=test-results/sdk-${FOCUS_SDK}-${PLATFORM_TAG}.html --self-contained-html --sdks-encrypt "${ENCRYPT_SDK}" -ra -v --focus "$FOCUS_SDK" test_tdfs.py test_policytypes.py test_pqc.py + skip_flag=$([[ "$SKIP_RELEASED_PAIRS" == "true" ]] && echo "--skip-released-pairs" || echo "") + uv run pytest -n auto --dist loadscope --html=test-results/sdk-${FOCUS_SDK}-${PLATFORM_TAG}.html --self-contained-html --sdks-encrypt "${ENCRYPT_SDK}" -ra -v --focus "$FOCUS_SDK" $skip_flag test_tdfs.py test_policytypes.py test_pqc.py working-directory: otdftests/xtest env: PLATFORM_DIR: "../../${{ steps.run-platform.outputs.platform-working-dir }}" @@ -683,18 +687,16 @@ jobs: - name: Run attribute based configuration tests if: ${{ steps.multikas.outputs.supported == 'true' }} - run: >- - uv run pytest - -ra - -v - --numprocesses auto - --dist loadscope - --html test-results/attributes-${FOCUS_SDK}-${PLATFORM_TAG}.html - --self-contained-html - --audit-log-dir test-results/audit-logs - --sdks-encrypt "${ENCRYPT_SDK}" - --focus "$FOCUS_SDK" - test_abac.py + run: |- + skip_flag=$([[ "$SKIP_RELEASED_PAIRS" == "true" ]] && echo "--skip-released-pairs" || echo "") + uv run pytest -ra -v --numprocesses auto --dist loadscope \ + --html test-results/attributes-${FOCUS_SDK}-${PLATFORM_TAG}.html \ + --self-contained-html \ + --audit-log-dir test-results/audit-logs \ + --sdks-encrypt "${ENCRYPT_SDK}" \ + --focus "$FOCUS_SDK" \ + $skip_flag \ + test_abac.py working-directory: otdftests/xtest env: PLATFORM_DIR: "../../${{ steps.run-platform.outputs.platform-working-dir }}" diff --git a/xtest/conftest.py b/xtest/conftest.py index ec0a21b9..d5cc584d 100644 --- a/xtest/conftest.py +++ b/xtest/conftest.py @@ -108,6 +108,11 @@ def pytest_addoption(parser: pytest.Parser): action="store_true", help="disable automatic KAS audit log collection", ) + parser.addoption( + "--skip-released-pairs", + action="store_true", + help="skip round-trip tests where all SDKs are released artifacts", + ) parser.addoption( "--sdks", help=f"select which sdks to run by default, unless overridden; one or more of {englist(typing.get_args(tdfs.sdk_type))}, optionally version-qualified (e.g. go@main, go@v0.18.0, go@*)", @@ -211,6 +216,18 @@ def sdk_specs_opt(names: list[str]) -> list[str]: metafunc.parametrize("container", containers) +def pytest_runtest_setup(item: pytest.Item): + if not item.config.getoption("--skip-released-pairs", default=False): + return + params = getattr(item, "callspec", None) + if params is None: + return + e = params.params.get("encrypt_sdk") + d = params.params.get("decrypt_sdk") + if e is not None and d is not None and e.is_released() and d.is_released(): + pytest.skip(f"released-only pair ({e} × {d})") + + # Core fixtures @pytest.fixture(scope="session") def pt_file(tmp_dir: Path, size: str) -> Path: diff --git a/xtest/tdfs.py b/xtest/tdfs.py index 46443bc3..9ba8ad7f 100644 --- a/xtest/tdfs.py +++ b/xtest/tdfs.py @@ -384,6 +384,9 @@ def __eq__(self, other: object) -> bool: def __hash__(self) -> int: return hash((self.sdk, self.version)) + def is_released(self) -> bool: + return bool(re.match(r"^v\d+\.\d+\.\d+", self.version)) + def encrypt( self, pt_file: Path, From 404c3ff74207c8d20009fa745717df91f58b64b9 Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Tue, 5 May 2026 09:41:51 -0400 Subject: [PATCH 11/13] fix(xtest): don't skip released SDK pairs when testing a head platform If the platform under test is a head build (e.g. main), all SDK pairs are meaningful for backward-compat coverage, so --skip-released-pairs must not fire. Only skip when both the platform tag is a released artifact and the event is not a workflow_dispatch. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/xtest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/xtest.yml b/.github/workflows/xtest.yml index 96637a4f..e73ec4f7 100644 --- a/.github/workflows/xtest.yml +++ b/.github/workflows/xtest.yml @@ -274,7 +274,7 @@ jobs: env: FOCUS_SDK: ${{ inputs.focus-sdk || 'all' }} ENCRYPT_SDK: ${{ matrix.sdk-version }} - SKIP_RELEASED_PAIRS: ${{ github.event_name != 'workflow_dispatch' }} + SKIP_RELEASED_PAIRS: ${{ github.event_name != 'workflow_dispatch' && !contains(fromJSON(needs.resolve-versions.outputs.heads), matrix.platform-tag) }} strategy: fail-fast: false matrix: From 0d199a3bf3f92caf97cd374e59dd5f9562709653 Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Tue, 5 May 2026 11:49:24 -0400 Subject: [PATCH 12/13] fix(xtest): address PR review comments on sdk version detection and artifact names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Broaden `SDK.is_released()` regex to recognize JS-style tags (`sdk/0.9.0`, `0.9.0-beta.84`) in addition to `v`-prefixed semver; use `fullmatch` to avoid partial matches so `--skip-released-pairs` correctly prunes JS pairs - Sanitize `matrix.sdk-version` before using it in artifact names: JS tags like `sdk/0.9.0` produce names with `/` which `actions/upload-artifact` rejects; new step outputs a `/`→`-` sanitized value used by both upload steps Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/xtest.yml | 9 +++++++-- xtest/tdfs.py | 6 +++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/xtest.yml b/.github/workflows/xtest.yml index e73ec4f7..ed89c4b0 100644 --- a/.github/workflows/xtest.yml +++ b/.github/workflows/xtest.yml @@ -709,19 +709,24 @@ jobs: KAS_KM1_LOG_FILE: "../../${{ steps.kas-km1.outputs.log-file }}" KAS_KM2_LOG_FILE: "../../${{ steps.kas-km2.outputs.log-file }}" + - name: Sanitize sdk-version for artifact name + id: artifact-name + if: success() || failure() + run: echo "sdk_version=${ENCRYPT_SDK//\//-}" >> "$GITHUB_OUTPUT" + - name: Upload artifact uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 id: upload-artifact if: success() || failure() with: - name: ${{ job.status == 'success' && '✅' || job.status == 'failure' && '❌' }} ${{ matrix.sdk-version }}-${{matrix.platform-tag}} + name: ${{ job.status == 'success' && '✅' || job.status == 'failure' && '❌' }} ${{ steps.artifact-name.outputs.sdk_version }}-${{ matrix.platform-tag }} path: otdftests/xtest/test-results/*.html - name: Upload audit logs on failure uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 if: failure() with: - name: audit-logs-${{ matrix.sdk-version }}-${{ matrix.platform-tag }} + name: audit-logs-${{ steps.artifact-name.outputs.sdk_version }}-${{ matrix.platform-tag }} path: otdftests/xtest/test-results/audit-logs/*.log if-no-files-found: ignore diff --git a/xtest/tdfs.py b/xtest/tdfs.py index 9ba8ad7f..586e2a5d 100644 --- a/xtest/tdfs.py +++ b/xtest/tdfs.py @@ -385,7 +385,11 @@ def __hash__(self) -> int: return hash((self.sdk, self.version)) def is_released(self) -> bool: - return bool(re.match(r"^v\d+\.\d+\.\d+", self.version)) + return bool( + re.fullmatch( + r"(?:sdk/)?v?\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?", self.version + ) + ) def encrypt( self, From 7bc0f728fb9252e17885652b713a3584d8c75ab9 Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Tue, 5 May 2026 14:27:32 -0400 Subject: [PATCH 13/13] style(xtest): enable ruff PERF rules and fix violations Co-Authored-By: Claude Sonnet 4.6 --- xtest/audit_logs.py | 24 ++++++++++++------------ xtest/pyproject.toml | 1 + xtest/tdfs.py | 10 +++++----- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/xtest/audit_logs.py b/xtest/audit_logs.py index 51f3f292..c894110c 100644 --- a/xtest/audit_logs.py +++ b/xtest/audit_logs.py @@ -1711,10 +1711,10 @@ def _raise_assertion_error( if matching: context.append("Matching logs:") - for log in matching[:10]: - context.append( - f" [{log.timestamp}] {log.service_name}: {log.raw_line}" - ) + context.extend( + f" [{log.timestamp}] {log.service_name}: {log.raw_line}" + for log in matching[:10] + ) if len(matching) > 10: context.append(f" ... and {len(matching) - 10} more") context.append("") @@ -1734,10 +1734,10 @@ def _raise_assertion_error( context.append( f"Logs before timeout (last {len(recent_logs)} of {len(all_logs)}):" ) - for log in recent_logs: - context.append( - f" [{log.timestamp}] {log.service_name}: {log.raw_line}" - ) + context.extend( + f" [{log.timestamp}] {log.service_name}: {log.raw_line}" + for log in recent_logs + ) # Show timeout marker if timeout_time: @@ -1751,10 +1751,10 @@ def _raise_assertion_error( context.append( f"Logs AFTER timeout ({len(late_to_show)} of {len(late_logs)} late arrivals):" ) - for log in late_to_show: - context.append( - f" [{log.timestamp}] {log.service_name}: {log.raw_line}" - ) + context.extend( + f" [{log.timestamp}] {log.service_name}: {log.raw_line}" + for log in late_to_show + ) if len(late_logs) > 10: context.append(f" ... and {len(late_logs) - 10} more late arrivals") context.append("") diff --git a/xtest/pyproject.toml b/xtest/pyproject.toml index f40020b0..7ff2d9ec 100644 --- a/xtest/pyproject.toml +++ b/xtest/pyproject.toml @@ -70,6 +70,7 @@ select = [ "B", # flake8-bugbear "C4", # flake8-comprehensions "UP", # pyupgrade + "PERF", # Perflint ] ignore = [ "E501", # line too long (handled by formatter) diff --git a/xtest/tdfs.py b/xtest/tdfs.py index 586e2a5d..4501604c 100644 --- a/xtest/tdfs.py +++ b/xtest/tdfs.py @@ -518,14 +518,14 @@ def _uncached_supports(self, feature: feature_type) -> bool: def all_versions_of(sdk: sdk_type) -> list[SDK]: - versions: list[SDK] = [] sdk_path = os.path.join("sdk", sdk, "dist") if not os.path.isdir(sdk_path): return [] - for version in os.listdir(sdk_path): - if os.path.isdir(os.path.join(sdk_path, version)): - versions.append(SDK(sdk, version)) - return versions + return [ + SDK(sdk, version) + for version in os.listdir(sdk_path) + if os.path.isdir(os.path.join(sdk_path, version)) + ] def parse_sdk_spec(spec: str) -> list[SDK]: