diff --git a/.github/workflows/xtest.yml b/.github/workflows/xtest.yml index 6bbd042b..c238066d 100644 --- a/.github/workflows/xtest.yml +++ b/.github/workflows/xtest.yml @@ -48,6 +48,11 @@ on: type: string default: "" description: "Comma-separated feature names to treat as supported regardless of what each SDK's `cli.sh supports` reports. Use when evaluating a fix that has not been released yet: the version gates live in this repo and answer 'no' for exactly those unreleased builds, so the cells would otherwise skip. An unknown name fails the run rather than being ignored." + force-platform-supports: + required: false + type: string + default: "" + description: "Comma-separated platform features to treat as supported. Does not override SDK gates or enable service configuration. Unknown names fail the run." workflow_call: inputs: platform-ref: @@ -86,6 +91,11 @@ on: required: false type: string default: "" + force-platform-supports: + required: false + type: string + default: "" + description: "Comma-separated platform features to treat as supported. Does not override SDK gates or enable service configuration. Unknown names fail the run." schedule: - cron: "30 6 * * *" # 0630 UTC - cron: "0 5 * * 1,3" # 500 UTC (Monday, Wednesday) @@ -101,6 +111,7 @@ concurrency: # nothing" -- so the PR gate and the nightlies are unaffected. env: XT_FORCE_SUPPORTS: ${{ inputs.force-supports }} + XT_FORCE_PLATFORM_SUPPORTS: ${{ inputs.force-platform-supports }} jobs: resolve-versions: diff --git a/AGENTS.md b/AGENTS.md index ffa86444..bad4c54b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -61,6 +61,10 @@ See `xtest/AGENTS.md` for the full table of `--sdks`, `--containers`, - `XT_FORCE_SUPPORTS` — comma-separated feature names to treat as supported regardless of what each SDK's `cli.sh supports` reports. See below. +- `XT_FORCE_PLATFORM_SUPPORTS` — comma-separated platform features to treat as + supported, independent of SDK overrides. It does not enable service settings. + In CI, use `force-platform-supports`. See `xtest/README.md` for usage. + ### Evaluating an unreleased fix: `XT_FORCE_SUPPORTS` `SDK.supports(feature)` answers from the `supports` case statements in diff --git a/xtest/AGENTS.md b/xtest/AGENTS.md index 5997f809..db4f8ff4 100644 --- a/xtest/AGENTS.md +++ b/xtest/AGENTS.md @@ -37,6 +37,7 @@ Beyond the repo-wide ones in `../AGENTS.md`: |----------|---------| | `XT_TMP_DIR` | Root for generated fixtures and ciphertexts (default `tmp/`). Point at a large volume for `medium`/`large` runs. | | `XT_FORCE_SUPPORTS` | Comma-separated features to treat as supported, bypassing the `cli.sh supports` gate. For evaluating a fix before it releases — see `../AGENTS.md`. Unknown names raise. | +| `XT_FORCE_PLATFORM_SUPPORTS` | Comma-separated platform features to treat as supported. Independent of SDK overrides; does not enable service configuration. Unknown names raise. | ## Authoring a New Test diff --git a/xtest/README.md b/xtest/README.md index 6bdfcc40..ddb0035d 100644 --- a/xtest/README.md +++ b/xtest/README.md @@ -116,6 +116,13 @@ uv sync --extra dev pytest ``` +#### Testing unreleased platform features + +Set `XT_FORCE_PLATFORM_SUPPORTS` to a comma-separated list of platform features +to bypass their test gates. In CI, use the `force-platform-supports` input. +SDK overrides use `XT_FORCE_SUPPORTS` separately. These overrides do not enable +service configuration. + #### Run TDF Tests ```shell diff --git a/xtest/tdfs.py b/xtest/tdfs.py index 8f3bf8e0..2cf1053a 100644 --- a/xtest/tdfs.py +++ b/xtest/tdfs.py @@ -10,7 +10,7 @@ import zipfile from collections.abc import Callable from pathlib import Path -from typing import Any, Literal, TypeIs, get_args +from typing import Any, Literal, TypeIs, cast, get_args import jsonschema import pytest @@ -207,8 +207,10 @@ def is_sdk_type(val: str) -> TypeIs[sdk_type]: ] -def _parse_forced_supports(raw: str) -> frozenset[str]: - """Parse ``XT_FORCE_SUPPORTS`` into a set of feature names. +def _parse_forced_supports( + raw: str, *, source: str = "XT_FORCE_SUPPORTS" +) -> frozenset[feature_type]: + """Parse an SDK or platform override into validated feature names. An unrecognised name is a hard error rather than a no-op. The override exists to turn a skip into a real result, so a typo that quietly left the @@ -220,10 +222,10 @@ def _parse_forced_supports(raw: str) -> frozenset[str]: unknown = names - known if unknown: raise ValueError( - f"XT_FORCE_SUPPORTS names unknown feature(s) {sorted(unknown)}; " + f"{source} names unknown feature(s) {sorted(unknown)}; " f"valid features are {sorted(known)}" ) - return frozenset(names) + return cast(frozenset[feature_type], frozenset(names)) #: Features to treat as supported no matter what the SDK reports. @@ -248,6 +250,20 @@ def _parse_forced_supports(raw: str) -> frozenset[str]: ) +# Platform overrides are independent of SDK overrides and service configuration. +FORCED_PLATFORM_SUPPORTS = _parse_forced_supports( + os.environ.get("XT_FORCE_PLATFORM_SUPPORTS", ""), + source="XT_FORCE_PLATFORM_SUPPORTS", +) + +if FORCED_PLATFORM_SUPPORTS: + logger.warning( + "XT_FORCE_PLATFORM_SUPPORTS is set: treating %s as supported by the " + "platform. SDK gates and service configuration are unchanged.", + ", ".join(sorted(FORCED_PLATFORM_SUPPORTS)), + ) + + container_version = Literal["4.2.2", "4.3.0"] policy_type = Literal["plaintext", "encrypted"] @@ -266,6 +282,7 @@ class PlatformFeatureSet(BaseModel): def __init__(self, **kwargs: dict[str, Any]): super().__init__(**kwargs) + self.features.update(FORCED_PLATFORM_SUPPORTS) v = os.getenv("PLATFORM_VERSION") if not v: print("PLATFORM_VERSION unset or empty; defaulting to 0.9.0") diff --git a/xtest/test_tdfs_units.py b/xtest/test_tdfs_units.py index e6d86ed7..d79da777 100644 --- a/xtest/test_tdfs_units.py +++ b/xtest/test_tdfs_units.py @@ -41,6 +41,62 @@ def test_unknown_name_raises(self): tdfs._parse_forced_supports("hexles") +# --- Platform forced support ------------------------------------------------ + + +@pytest.mark.parametrize("platform_version", ["0.12.0", "main", ""]) +@pytest.mark.parametrize("forced", [False, True]) +def test_platform_forced_supports( + monkeypatch: pytest.MonkeyPatch, platform_version: str, forced: bool +): + monkeypatch.setenv("PLATFORM_VERSION", platform_version) + # A source tag alone must not imply feature support. + monkeypatch.setenv("PLATFORM_TAG", "main") + monkeypatch.setattr(tdfs, "FORCED_SUPPORTS", tdfs._parse_forced_supports("dpop")) + monkeypatch.setattr( + tdfs, + "FORCED_PLATFORM_SUPPORTS", + tdfs._parse_forced_supports("dpop_nonce_challenge" if forced else ""), + ) + monkeypatch.setattr(tdfs, "_fetch_well_known", lambda: None) + + features = tdfs.PlatformFeatureSet() + assert ("dpop_nonce_challenge" in features.features) is forced + # An SDK override must not force the corresponding platform feature. + assert "dpop" not in features.features + + +def test_platform_forced_supports_do_not_leak(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv("PLATFORM_VERSION", "main") + monkeypatch.setattr( + tdfs, + "FORCED_PLATFORM_SUPPORTS", + tdfs._parse_forced_supports("dpop_nonce_challenge"), + ) + forced = tdfs.PlatformFeatureSet() + monkeypatch.setattr(tdfs, "FORCED_PLATFORM_SUPPORTS", frozenset()) + unforced = tdfs.PlatformFeatureSet() + assert "dpop_nonce_challenge" in forced.features + assert "dpop_nonce_challenge" not in unforced.features + + +def test_platform_override_does_not_override_sdk(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(tdfs, "FORCED_SUPPORTS", frozenset()) + monkeypatch.setattr( + tdfs, + "FORCED_PLATFORM_SUPPORTS", + tdfs._parse_forced_supports("dpop_nonce_challenge"), + ) + sdk = object.__new__(tdfs.SDK) + sdk._supports = {"dpop_nonce_challenge": False} + assert not sdk.supports("dpop_nonce_challenge") + + +def test_unknown_platform_override_names_source(): + with pytest.raises(ValueError, match="XT_FORCE_PLATFORM_SUPPORTS names unknown"): + tdfs._parse_forced_supports("dpop_typo", source="XT_FORCE_PLATFORM_SUPPORTS") + + # --- tdfs.zip64_reader_is_broken ----------------------------------------------