From 8fc1853bfc718f94b6ca0f255b29df408a640d8f Mon Sep 17 00:00:00 2001 From: Taylor Steinberg Date: Tue, 10 Nov 2026 11:11:29 -0500 Subject: [PATCH] test: cover manifests across content Python versions --- .github/workflows/main.yml | 18 +++++++++++ scripts/content_python_versions.py | 28 +++++++++++++++++ tests/test_manifest_content_pythons.py | 42 ++++++++++++++++++++++++++ 3 files changed, 88 insertions(+) create mode 100644 scripts/content_python_versions.py create mode 100644 tests/test_manifest_content_pythons.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b4ba05fc1..b3a234bed 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -51,6 +51,24 @@ jobs: coverageFile: coverage.xml token: ${{ secrets.GITHUB_TOKEN }} + test-content-python-versions: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 + with: + version: ">=0.9.0" + - run: uv sync --python 3.11 --group test + - name: Install content Pythons + run: | + mapfile -t versions < <(uv run --no-sync python scripts/content_python_versions.py) + test "${#versions[@]}" -gt 0 + uv run --no-sync uv python install --no-bin "${versions[@]}" + - name: Test manifests + run: | + uv run --no-sync pytest tests/test_manifest_content_pythons.py --junitxml=content-python-results.xml + uv run --no-sync python -c 'import xml.etree.ElementTree as ET; root = ET.parse("content-python-results.xml"); assert not root.findall(".//skipped"), "Content Python cases were skipped"' + prerelease-test: runs-on: ubuntu-latest continue-on-error: true diff --git a/scripts/content_python_versions.py b/scripts/content_python_versions.py new file mode 100644 index 000000000..6f2cac48a --- /dev/null +++ b/scripts/content_python_versions.py @@ -0,0 +1,28 @@ +import json +import subprocess + +from packaging.version import Version + + +def content_python_versions(): + result = subprocess.run( + ["uv", "python", "list", "--only-downloads", "--output-format", "json"], + check=True, + capture_output=True, + text=True, + ) + versions = { + (entry["version_parts"]["major"], entry["version_parts"]["minor"]) + for entry in json.loads(result.stdout) + if entry["implementation"] == "cpython" + and entry["variant"] == "default" + and not Version(entry["version"]).is_prerelease + and (entry["version_parts"]["major"], entry["version_parts"]["minor"]) >= (3, 8) + } + if not versions: + raise RuntimeError("uv did not list any supported CPython downloads") + return ["%s.%s" % version for version in sorted(versions)] + + +if __name__ == "__main__": + print("\n".join(content_python_versions())) diff --git a/tests/test_manifest_content_pythons.py b/tests/test_manifest_content_pythons.py new file mode 100644 index 000000000..bed103f38 --- /dev/null +++ b/tests/test_manifest_content_pythons.py @@ -0,0 +1,42 @@ +import json +import os +import subprocess + +import pytest +from click.testing import CliRunner + +from rsconnect.main import cli +from scripts.content_python_versions import content_python_versions + + +@pytest.mark.parametrize("version", content_python_versions()) +def test_manifest_uses_content_python_environment(version, tmp_path): + found = subprocess.run( + ["uv", "python", "find", "--no-project", "--no-python-downloads", "cpython@" + version], + capture_output=True, + text=True, + ) + if found.returncode: + if "No interpreter found" in found.stderr: + pytest.skip("CPython %s is not installed" % version) + found.check_returncode() + + venv = tmp_path / "venv" + subprocess.run(["uv", "venv", "--python", found.stdout.strip(), str(venv)], check=True) + python = venv / ("Scripts/python.exe" if os.name == "nt" else "bin/python") + subprocess.run(["uv", "pip", "install", "--python", str(python), "idna==3.10"], check=True) + expected_version = subprocess.check_output( + [str(python), "-c", "import platform; print(platform.python_version())"], text=True + ).strip() + + project = tmp_path / "project" + project.mkdir() + (project / "app.py").write_text("app = None\n") + result = CliRunner().invoke( + cli, ["write-manifest", "api", str(project), "--python", str(python), "--force-generate", "--exclude-renv"] + ) + assert result.exit_code == 0, result.output + manifest = json.loads((project / "manifest.json").read_text()) + assert manifest["python"]["version"] == expected_version + assert manifest["python"]["package_manager"]["package_file"] == "requirements.txt" + assert "idna==3.10" in (project / "requirements.txt").read_text().splitlines()