diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1e776722..ed5cc3af 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,26 +15,26 @@ permissions: pull-requests: write jobs: - test: + test-python-versions: strategy: fail-fast: false matrix: os: [ubuntu-latest] - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] include: - os: macos-latest - python-version: '3.9' + python-version: '3.13' - os: windows-latest - python-version: '3.9' + python-version: '3.13' runs-on: ${{ matrix.os }} name: test (py${{ matrix.python-version }} ${{ matrix.os }}) steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - run: pip install '.[test]' @@ -52,12 +52,12 @@ jobs: runs-on: ubuntu-latest continue-on-error: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v6 with: python-version: 3.8.x - run: pip install --pre '.[test]' @@ -67,18 +67,18 @@ jobs: - run: make test-3.8 distributions: - needs: test + needs: test-python-versions strategy: matrix: package_name: ["rsconnect_python", "rsconnect"] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v6 with: python-version: 3.8.x - name: Install uv # see scripts/temporary-rename @@ -103,10 +103,10 @@ jobs: uses: pypa/gh-action-pypi-publish@release/v1 docs: - needs: test + needs: test-python-versions runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 env: @@ -117,7 +117,7 @@ jobs: python-version: 3.12 - name: build docs run: make docs - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v5 with: name: docs path: site/ @@ -136,12 +136,52 @@ jobs: - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') run: make promote-docs-in-s3 - test-rsconnect: - name: "Integration tests against latest Connect" + test-connect-versions: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + version: + - "release" # special value that always points to the latest Connect release + - "2025.09.0" # jammy + - "2025.03.0" # jammy + - "2024.09.0" # jammy + - "2024.03.0" # jammy + - "2023.09.0" # jammy + - "2023.03.0" # bionic + - "2022.10.0" # bionic + name: Integration tests against Connect ${{ matrix.version }} + env: + python-version: 3.13 + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-python@v6 + with: + python-version: ${{ env.python-version }} + + - name: Install dependencies + run: pip install '.[test]' + + - run: pip freeze + + - run: rsconnect version + + - name: Run integration tests + uses: posit-dev/with-connect@main + with: + version: ${{ matrix.version }} + # License file valid until 2026-12-05 + license: ${{ secrets.CONNECT_LICENSE_FILE }} + command: | + make test-${{ env.python-version }} + + test-dev-connect: + name: "Integration tests against dev Connect" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: python-version: 3.12.4 - name: Install dependencies diff --git a/tests/test_api.py b/tests/test_api.py index 26261c92..b42f816c 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -25,7 +25,7 @@ class TestAPI(TestCase): def test_executor_init(self): connect_server = require_connect() api_key = require_api_key() - ce = RSConnectExecutor(None, None, connect_server, api_key, True, None) + ce = RSConnectExecutor(url=connect_server, api_key=api_key, insecure=True) self.assertEqual(ce.remote_server.url, connect_server) def test_output_task_log(self): @@ -52,7 +52,7 @@ def test_output_task_log(self): def test_make_deployment_name(self): connect_server = require_connect() api_key = require_api_key() - ce = RSConnectExecutor(None, None, connect_server, api_key, True, None) + ce = RSConnectExecutor(url=connect_server, api_key=api_key, insecure=True) self.assertEqual(ce.make_deployment_name("title", False), "title") self.assertEqual(ce.make_deployment_name("Title", False), "title") self.assertEqual(ce.make_deployment_name("My Title", False), "my_title") diff --git a/tests/test_bundle.py b/tests/test_bundle.py index 7cab72ed..dc952d6b 100644 --- a/tests/test_bundle.py +++ b/tests/test_bundle.py @@ -132,6 +132,11 @@ def test_make_notebook_source_bundle1(self): "package_file": "requirements.txt", }, }, + "environment": { + "python": { + "requires": ">=3.8", + }, + }, "files": { "dummy.ipynb": { "checksum": ipynb_hash, diff --git a/tests/test_environment.py b/tests/test_environment.py index 4da52db7..9d883315 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -55,6 +55,7 @@ def test_file(self): source="file", ), python_interpreter=sys.executable, + python_version_requirement=">=3.8", ) self.assertEqual(expected, result) @@ -276,12 +277,12 @@ def fake_inspect_environment( class TestEnvironmentDeprecations: def test_override_python_version(self): with mock.patch.object(rsconnect.environment.logger, "warning") as mock_warning: - result = Environment.create_python_environment(get_dir("pip1"), override_python_version=None) + result = Environment.create_python_environment(get_dir("pip1-no-version"), override_python_version=None) assert mock_warning.call_count == 0 assert result.python_version_requirement is None with mock.patch.object(rsconnect.environment.logger, "warning") as mock_warning: - result = Environment.create_python_environment(get_dir("pip1"), override_python_version="3.8") + result = Environment.create_python_environment(get_dir("pip1-no-version"), override_python_version="3.8") assert mock_warning.call_count == 1 mock_warning.assert_called_once_with( "The --override-python-version option is deprecated, " diff --git a/tests/test_main.py b/tests/test_main.py index d6d2e59d..dfaa908a 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -23,6 +23,7 @@ optional_target, require_api_key, require_connect, + require_connect_version, ) @@ -88,6 +89,7 @@ def test_ping_api_key(self): assert "OK" in result.output def test_deploy(self): + require_connect_version("2025.03.0") target = optional_target(get_dir(join("pip1", "dummy.ipynb"))) runner = CliRunner() args = self.create_deploy_args("notebook", target) @@ -290,6 +292,7 @@ def post_application_deploy_callback(request, uri, response_headers): os.environ["CONNECT_SERVER"] = original_server_value # noinspection SpellCheckingInspection + @pytest.mark.skip(reason="Skipping R manifest test (requires R 3.5, docker containers have moved on).") def test_deploy_manifest(self): target = optional_target(get_manifest_path("shinyapp")) runner = CliRunner() @@ -299,7 +302,8 @@ def test_deploy_manifest(self): # noinspection SpellCheckingInspection @httpretty.activate(verbose=True, allow_net_connect=False) - def test_deploy_manifest_shinyapps(self): + @mock.patch("rsconnect.api.webbrowser.open_new") + def test_deploy_manifest_shinyapps(self, mock_open_browser): original_api_key_value = os.environ.pop("CONNECT_API_KEY", None) original_server_value = os.environ.pop("CONNECT_SERVER", None) @@ -474,7 +478,8 @@ def post_deploy_callback(request, uri, response_headers): os.environ["CONNECT_SERVER"] = original_server_value @httpretty.activate(verbose=True, allow_net_connect=False) - def test_redeploy_manifest_shinyapps(self): + @mock.patch("rsconnect.api.webbrowser.open_new") + def test_redeploy_manifest_shinyapps(self, mock_open_browser): original_api_key_value = os.environ.pop("CONNECT_API_KEY", None) original_server_value = os.environ.pop("CONNECT_SERVER", None) @@ -641,6 +646,7 @@ def post_deploy_callback(request, uri, response_headers): os.environ["CONNECT_SERVER"] = original_server_value def test_deploy_api(self): + require_connect_version("2025.03.0") target = optional_target(get_api_path("flask")) runner = CliRunner() args = self.create_deploy_args("api", target) @@ -656,6 +662,7 @@ def test_deploy_api_fail_verify(self): assert result.exit_code == 1, result.output def test_deploy_api_fail_no_verify(self): + require_connect_version("2025.03.0") target = optional_target(get_api_path("flask-bad")) runner = CliRunner() args = self.create_deploy_args("api", target) diff --git a/tests/testdata/api/flask-bad/.python-version b/tests/testdata/api/flask-bad/.python-version new file mode 100644 index 00000000..221a9649 --- /dev/null +++ b/tests/testdata/api/flask-bad/.python-version @@ -0,0 +1 @@ +>=3.8 diff --git a/tests/testdata/api/flask/.python-version b/tests/testdata/api/flask/.python-version new file mode 100644 index 00000000..221a9649 --- /dev/null +++ b/tests/testdata/api/flask/.python-version @@ -0,0 +1 @@ +>=3.8 diff --git a/tests/testdata/py3/pip1-no-version/dummy.ipynb b/tests/testdata/py3/pip1-no-version/dummy.ipynb new file mode 100644 index 00000000..76fe3342 --- /dev/null +++ b/tests/testdata/py3/pip1-no-version/dummy.ipynb @@ -0,0 +1,52 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'this is a notebook'" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"this is a notebook\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tests/testdata/py3/pip1-no-version/requirements.txt b/tests/testdata/py3/pip1-no-version/requirements.txt new file mode 100644 index 00000000..40218701 --- /dev/null +++ b/tests/testdata/py3/pip1-no-version/requirements.txt @@ -0,0 +1,3 @@ +numpy +pandas +matplotlib diff --git a/tests/testdata/py3/pip1/.python-version b/tests/testdata/py3/pip1/.python-version new file mode 100644 index 00000000..221a9649 --- /dev/null +++ b/tests/testdata/py3/pip1/.python-version @@ -0,0 +1 @@ +>=3.8 diff --git a/tests/utils.py b/tests/utils.py index 1dfdcb78..274c62e3 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -3,8 +3,10 @@ import jwt import re from os.path import join, dirname, exists +from packaging import version import pytest +from rsconnect.api import RSConnectServer, RSConnectClient def apply_common_args(args: list, server=None, key=None, cacert=None, insecure=False): @@ -42,6 +44,29 @@ def require_api_key(): return connect_api_key +def require_connect_version(min_version: str): + """ + Skip test if Connect server version is less than min_version. + + Args: + min_version: Minimum required version (e.g., "2025.03.0") + """ + connect_server = require_connect() + api_key = require_api_key() + + server = RSConnectServer(connect_server, api_key) + client = RSConnectClient(server) + + try: + settings = client.server_settings() + server_version = settings["version"] + + if version.parse(server_version) < version.parse(min_version): + pytest.skip(f"Connect server {server_version} < {min_version}") + except Exception as e: + pytest.skip(f"Could not determine Connect server version: {e}") + + def get_dir(name): py_version = "py%d" % sys.version_info[0] # noinspection SpellCheckingInspection