diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml new file mode 100644 index 00000000..ae2e6785 --- /dev/null +++ b/.github/workflows/integration.yml @@ -0,0 +1,63 @@ +name: Integration Tests +on: + pull_request: + push: + branches: + - main +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +jobs: + setup-integration-test: + runs-on: ubuntu-latest + outputs: + versions: ${{ steps.versions.outputs.versions }} + steps: + - uses: actions/checkout@v4 + - id: versions + working-directory: ./integration + # The `jq` command is "output compact, raw input, slurp, split on new lines, and remove the last element". This results in a JSON array of Connect versions (e.g., ["2025.01.0", "2024.12.0"]). + run: | + versions=$(make print-versions | jq -c -Rs 'split("\n") | .[:-1]') + echo "versions=$versions" >> "$GITHUB_OUTPUT" + + integration-test: + runs-on: ubuntu-latest + needs: setup-integration-test + strategy: + fail-fast: false + matrix: + CONNECT_VERSION: ${{ fromJson(needs.setup-integration-test.outputs.versions) }} + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - name: Write Posit Connect license to disk + run: echo "$RSC_LICENSE" > ./integration/license.lic + env: + CONNECT_LICENSE: ${{ secrets.RSC_LICENSE }} + - uses: astral-sh/setup-uv@v6 + - run: uv python install + - run: make -C ./integration ${{ matrix.CONNECT_VERSION }} + - uses: actions/upload-artifact@v4 + if: always() + with: + name: ${{ matrix.CONNECT_VERSION }} - Integration Test Report + path: integration/reports/*.xml + + integration-test-report: + needs: integration-test + runs-on: ubuntu-latest + permissions: + checks: write + pull-requests: write + if: always() + steps: + - uses: actions/download-artifact@v4 + with: + path: artifacts + - uses: EnricoMi/publish-unit-test-result-action@v2 + with: + check_name: integration-test-results + comment_mode: off + files: "artifacts/**/*.xml" + report_individual_runs: true diff --git a/Makefile b/Makefile index 10fbf703..c8dbc7e1 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ docs-clean: requirements/dev.txt: pyproject.toml @# allows you to do this... @# make requirements | tee > requirements/some_file.txt - @pip-compile pyproject.toml --rebuild --extra doc --extra test --extra check --output-file=- > $@ + @uv pip compile pyproject.toml --extra doc --extra test --extra check --output-file $@ binder/requirements.txt: requirements/dev.txt cp $< $@ diff --git a/integration/.gitignore b/integration/.gitignore new file mode 100644 index 00000000..dfa40ba7 --- /dev/null +++ b/integration/.gitignore @@ -0,0 +1,3 @@ +logs +reports +license.lic diff --git a/integration/Makefile b/integration/Makefile new file mode 100644 index 00000000..6bc91bda --- /dev/null +++ b/integration/Makefile @@ -0,0 +1,146 @@ +PROJECT_NAME ?= pins + +# Docker settings +DOCKER_COMPOSE ?= docker compose +DOCKER_CONNECT_IMAGE ?= rstudio/rstudio-connect +DOCKER_PROJECT_IMAGE_TAG ?= $(PROJECT_NAME):latest + +# Connect settings +CONNECT_BOOTSTRAP_SECRETKEY ?= $(shell head -c 32 /dev/random | base64) + +# pytest settings +PYTEST_ARGS ?= "-s" + +.DEFAULT_GOAL := latest + +.PHONY: $(CONNECT_VERSIONS) \ + all \ + build \ + down \ + down-% \ + latest \ + test \ + up \ + up-% \ + help + +# Versions +CONNECT_VERSIONS := \ + 2025.07.0 \ + 2025.06.0 \ + 2025.05.0 \ + 2025.04.0 \ + 2025.03.0 \ + 2025.02.0 \ + 2025.01.0 \ + 2024.12.0 \ + 2024.11.0 \ + 2024.09.0 \ + 2024.08.0 \ + 2024.06.0 \ + 2024.05.0 \ + 2024.04.1 \ + 2024.04.0 \ + 2024.03.0 \ + 2024.02.0 \ + 2024.01.0 \ + 2023.12.0 \ + 2023.10.0 \ + 2023.09.0 \ + 2023.07.0 \ + 2023.06.0 \ + 2023.05.0 \ + 2023.01.1 \ + 2023.01.0 \ + 2022.12.0 \ + 2022.11.0 + +clean: + rm -rf logs reports + find . -type d -empty -delete + +# Run test suite for a specific Connect version. +$(CONNECT_VERSIONS): %: down-% up-% + +# Run test suite against all Connect versions. +all: $(CONNECT_VERSIONS:%=%) preview + +# Run test suite against latest Connect version. +latest: + $(MAKE) $(firstword $(CONNECT_VERSIONS)) + +# Run test suite against preview Connect version. +preview: + $(MAKE) \ + DOCKER_CONNECT_IMAGE=rstudio/rstudio-connect-preview \ + DOCKER_CONNECT_IMAGE_TAG=dev-jammy-daily \ + down-preview up-preview + +# Build Dockerfile +build: + make -C .. $(UV_LOCK) + docker build -t $(DOCKER_PROJECT_IMAGE_TAG) .. + +# Tear down resources. +down: $(CONNECT_VERSIONS:%=down-%) +down-%: DOCKER_CONNECT_IMAGE_TAG=jammy-$* +down-%: CONNECT_VERSION=$* +down-%: + CONNECT_BOOTSTRAP_SECRETKEY=$(CONNECT_BOOTSTRAP_SECRETKEY) \ + CONNECT_VERSION=$* \ + DOCKER_CONNECT_IMAGE_TAG=$(DOCKER_CONNECT_IMAGE_TAG) \ + DOCKER_CONNECT_IMAGE=$(DOCKER_CONNECT_IMAGE) \ + DOCKER_PROJECT_IMAGE_TAG=$(DOCKER_PROJECT_IMAGE_TAG) \ + PYTEST_ARGS="$(PYTEST_ARGS)" \ + $(DOCKER_COMPOSE) -p $(PROJECT_NAME)-$(subst .,-,$(CONNECT_VERSION)) down -v + +# Create, start, and run Docker Compose. +up: $(CONNECT_VERSIONS:%=up-%) +up-%: CONNECT_VERSION=$* +up-%: DOCKER_CONNECT_IMAGE_TAG=jammy-$* +up-%: build + CONNECT_BOOTSTRAP_SECRETKEY=$(CONNECT_BOOTSTRAP_SECRETKEY) \ + CONNECT_VERSION=$* \ + DOCKER_CONNECT_IMAGE_TAG=$(DOCKER_CONNECT_IMAGE_TAG) \ + DOCKER_CONNECT_IMAGE=$(DOCKER_CONNECT_IMAGE) \ + DOCKER_PROJECT_IMAGE_TAG=$(DOCKER_PROJECT_IMAGE_TAG) \ + PYTEST_ARGS="$(PYTEST_ARGS)" \ + $(DOCKER_COMPOSE) -p $(PROJECT_NAME)-$(subst .,-,$(CONNECT_VERSION)) up -V --abort-on-container-exit --no-build + +# Show available versions +print-versions: + @printf "%s\n" $(strip $(CONNECT_VERSIONS)) + +# Show help message. +help: + @echo "Makefile Targets:" + @echo " all (default) Run test suite for all Connect versions." + @echo " latest Run test suite for latest Connect version." + @echo " preview Run test suite for preview Connect version." + @echo " Run test suite for the specified Connect version. (e.g., make 2024.05.0)" + @echo " up Start Docker Compose for all Connect versions." + @echo " down Tear down Docker resources for all Connect versions." + @echo " clean Clean up the project directory." + @echo " print-versions Show the available Connect versions." + @echo " help Show this help message." + @echo + @echo "Common Usage:" + @echo " make -j 4 Run test suite in parallel for all Connect versions." + @echo " make latest Run test suite for latest Connect version." + @echo " make preview Run test suite for preview Connect version." + @echo " make 2024.05.0 Run test suite for specific Connect version." + @echo + @echo "Environment Variables:" + @echo " DOCKER_COMPOSE Command to invoke Docker Compose. Default: docker compose" + @echo " DOCKER_CONNECT_IMAGE Docker image name for Connect. Default: rstudio/rstudio-connect" + @echo " DOCKER_PROJECT_IMAGE_TAG Docker image name and tag for the project image. Default: $(PROJECT_NAME):latest" + @echo " PYTEST_ARGS Arguments to pass to pytest. Default: \"-s\"" + +# Run tests. +test: + mkdir -p logs + set -o pipefail; \ + CONNECT_VERSION=${CONNECT_VERSION} \ + CONNECT_API_KEY="$(shell $(UV) run rsconnect bootstrap -i -s http://connect:3939 --raw)" \ + echo "Hello, World!" | \ + tee ./integration/logs/$(CONNECT_VERSION).log; diff --git a/integration/compose.yaml b/integration/compose.yaml new file mode 100644 index 00000000..e52ec59c --- /dev/null +++ b/integration/compose.yaml @@ -0,0 +1,46 @@ +services: + tests: + image: ${DOCKER_PROJECT_IMAGE_TAG} + # Run integration test suite. + # + # Target is relative to the ./integration directory, not the project root + # directory. The execution base directory is determined by the 'WORKDIR' + # in the Dockerfile. + command: make -C ./integration test + environment: + - CONNECT_BOOTSTRAP_SECRETKEY=${CONNECT_BOOTSTRAP_SECRETKEY} + # Port 3939 is the default port for Connect + - CONNECT_SERVER=http://connect:3939 + - CONNECT_VERSION=${CONNECT_VERSION} + - PYTEST_ARGS=${PYTEST_ARGS} + volumes: + - .:/sdk/integration + depends_on: + connect: + condition: service_healthy + networks: + - test + connect: + image: ${DOCKER_CONNECT_IMAGE}:${DOCKER_CONNECT_IMAGE_TAG} + pull_policy: always + environment: + - CONNECT_BOOTSTRAP_ENABLED=true + - CONNECT_BOOTSTRAP_SECRETKEY=${CONNECT_BOOTSTRAP_SECRETKEY} + - CONNECT_APPLICATIONS_PACKAGEAUDITINGENABLED=true + - CONNECT_TENSORFLOW_ENABLED=false + networks: + - test + privileged: true + volumes: + - /var/lib/rstudio-connect + - ./license.lic:/var/lib/rstudio-connect/rstudio-connect.lic:ro + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3939"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 30s + +networks: + test: + driver: bridge diff --git a/requirements/dev.txt b/requirements/dev.txt index 0f8fd751..c222809f 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,16 +1,12 @@ -# -# This file is autogenerated by pip-compile with Python 3.11 -# by the following command: -# -# pip-compile --extra=check --extra=doc --extra=test --output-file=- --strip-extras pyproject.toml -# -adlfs==2024.12.0 +# This file was autogenerated by uv via the following command: +# uv pip compile pyproject.toml --extra doc --extra test --extra check --output-file requirements/dev.txt +adlfs==2025.8.0 # via pins (pyproject.toml) -aiobotocore==2.22.0 +aiobotocore==2.24.2 # via s3fs aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.12.7 +aiohttp==3.12.15 # via # adlfs # aiobotocore @@ -18,7 +14,7 @@ aiohttp==3.12.7 # s3fs aioitertools==0.12.0 # via aiobotocore -aiosignal==1.3.2 +aiosignal==1.4.0 # via aiohttp annotated-types==0.7.0 # via pydantic @@ -37,16 +33,16 @@ attrs==25.3.0 # pytest # referencing # sphobjinv -azure-core==1.34.0 +azure-core==1.35.1 # via # adlfs # azure-identity # azure-storage-blob azure-datalake-store==0.0.53 # via adlfs -azure-identity==1.23.0 +azure-identity==1.25.0 # via adlfs -azure-storage-blob==12.25.1 +azure-storage-blob==12.26.0 # via adlfs backcall==0.2.0 # via ipython @@ -54,36 +50,36 @@ beartype==0.21.0 # via plum-dispatch black==25.1.0 # via quartodoc -botocore==1.37.3 +botocore==1.40.18 # via aiobotocore -build==1.2.2.post1 +build==1.3.0 # via pip-tools cachetools==5.5.2 # via google-auth -certifi==2025.4.26 +certifi==2025.8.3 # via # requests # sphobjinv -cffi==1.17.1 +cffi==2.0.0 # via # azure-datalake-store # cryptography cfgv==3.4.0 # via pre-commit -charset-normalizer==3.4.2 +charset-normalizer==3.4.3 # via requests -click==8.2.1 +click==8.3.0 # via # black # pip-tools # quartodoc colorama==0.4.6 # via griffe -comm==0.2.2 +comm==0.2.3 # via ipykernel -cramjam==2.10.0 +cramjam==2.11.0 # via fastparquet -cryptography==45.0.3 +cryptography==46.0.1 # via # azure-identity # azure-storage-blob @@ -91,9 +87,9 @@ cryptography==45.0.3 # pyjwt databackend==0.0.3 # via pins (pyproject.toml) -databricks-sdk==0.55.0 +databricks-sdk==0.65.0 # via pins (pyproject.toml) -debugpy==1.8.14 +debugpy==1.8.17 # via ipykernel decopatch==1.4.10 # via pytest-cases @@ -101,34 +97,34 @@ decorator==5.2.1 # via # gcsfs # ipython -distlib==0.3.9 +distlib==0.4.0 # via virtualenv -executing==2.2.0 +executing==2.2.1 # via stack-data -fastjsonschema==2.21.1 +fastjsonschema==2.21.2 # via nbformat fastparquet==2024.11.0 # via pins (pyproject.toml) -filelock==3.18.0 +filelock==3.19.1 # via virtualenv -frozenlist==1.6.2 +frozenlist==1.7.0 # via # aiohttp # aiosignal -fsspec==2025.5.1 +fsspec==2025.7.0 # via + # pins (pyproject.toml) # adlfs # fastparquet # gcsfs - # pins (pyproject.toml) # s3fs -gcsfs==2025.5.1 +gcsfs==2025.7.0 # via pins (pyproject.toml) -google-api-core==2.25.0 +google-api-core==2.25.1 # via # google-cloud-core # google-cloud-storage -google-auth==2.40.2 +google-auth==2.40.3 # via # databricks-sdk # gcsfs @@ -140,7 +136,7 @@ google-auth-oauthlib==1.2.2 # via gcsfs google-cloud-core==2.4.3 # via google-cloud-storage -google-cloud-storage==3.1.0 +google-cloud-storage==3.4.0 # via gcsfs google-crc32c==1.7.1 # via @@ -150,11 +146,11 @@ google-resumable-media==2.7.2 # via google-cloud-storage googleapis-common-protos==1.70.0 # via google-api-core -griffe==1.7.3 +griffe==1.14.0 # via quartodoc -humanize==4.12.3 +humanize==4.13.0 # via pins (pyproject.toml) -identify==2.6.12 +identify==2.6.14 # via pre-commit idna==3.10 # via @@ -170,12 +166,12 @@ importlib-resources==6.5.2 # quartodoc iniconfig==2.1.0 # via pytest -ipykernel==6.29.5 +ipykernel==6.30.1 # via pins (pyproject.toml) ipython==8.12.0 # via - # ipykernel # pins (pyproject.toml) + # ipykernel isodate==0.7.2 # via azure-storage-blob jedi==0.19.2 @@ -186,13 +182,13 @@ jmespath==1.0.1 # via # aiobotocore # botocore -joblib==1.5.1 +joblib==1.5.2 # via pins (pyproject.toml) -jsonschema==4.24.0 +jsonschema==4.25.1 # via # nbformat # sphobjinv -jsonschema-specifications==2025.4.1 +jsonschema-specifications==2025.9.1 # via jsonschema jupyter-client==8.6.3 # via @@ -208,7 +204,7 @@ makefun==1.16.0 # via # decopatch # pytest-cases -markdown-it-py==3.0.0 +markdown-it-py==4.0.0 # via rich markupsafe==3.0.2 # via jinja2 @@ -218,14 +214,14 @@ matplotlib-inline==0.1.7 # ipython mdurl==0.1.2 # via markdown-it-py -msal==1.32.3 +msal==1.33.0 # via # azure-datalake-store # azure-identity # msal-extensions msal-extensions==1.3.1 # via azure-identity -multidict==6.4.4 +multidict==6.6.4 # via # aiobotocore # aiohttp @@ -236,21 +232,21 @@ nbclient==0.10.2 # via pins (pyproject.toml) nbformat==5.10.4 # via - # nbclient # pins (pyproject.toml) + # nbclient nest-asyncio==1.6.0 # via ipykernel nodeenv==1.9.1 # via # pre-commit # pyright -numpy==2.2.6 +numpy==2.3.3 # via # fastparquet # pandas # rdata # xarray -oauthlib==3.2.2 +oauthlib==3.3.1 # via requests-oauthlib packaging==25.0 # via @@ -261,13 +257,13 @@ packaging==25.0 # pytest # pytest-cases # xarray -pandas==2.2.3 +pandas==2.3.2 # via - # fastparquet # pins (pyproject.toml) + # fastparquet # rdata # xarray -parso==0.8.4 +parso==0.8.5 # via jedi pathspec==0.12.1 # via black @@ -275,9 +271,11 @@ pexpect==4.9.0 # via ipython pickleshare==0.7.5 # via ipython -pip-tools==7.4.1 +pip==25.2 + # via pip-tools +pip-tools==7.5.0 # via pins (pyproject.toml) -platformdirs==4.3.8 +platformdirs==4.4.0 # via # black # jupyter-core @@ -286,22 +284,22 @@ pluggy==1.6.0 # via pytest plum-dispatch==2.5.7 # via quartodoc -pre-commit==4.2.0 +pre-commit==4.3.0 # via pins (pyproject.toml) -prompt-toolkit==3.0.51 +prompt-toolkit==3.0.52 # via ipython -propcache==0.3.1 +propcache==0.3.2 # via # aiohttp # yarl proto-plus==1.26.1 # via google-api-core -protobuf==6.31.1 +protobuf==6.32.1 # via # google-api-core # googleapis-common-protos # proto-plus -psutil==7.0.0 +psutil==7.1.0 # via ipykernel ptyprocess==0.7.0 # via pexpect @@ -309,7 +307,7 @@ pure-eval==0.2.3 # via stack-data py==1.11.0 # via pytest -pyarrow==20.0.0 +pyarrow==21.0.0 # via pins (pyproject.toml) pyasn1==0.6.1 # via @@ -317,20 +315,18 @@ pyasn1==0.6.1 # rsa pyasn1-modules==0.4.2 # via google-auth -pycparser==2.22 +pycparser==2.23 # via cffi -pydantic==2.11.5 +pydantic==2.11.9 # via quartodoc pydantic-core==2.33.2 # via pydantic -pygments==2.19.1 +pygments==2.19.2 # via # ipython # rich pyjwt==2.10.1 - # via - # msal - # pyjwt + # via msal pyproject-hooks==1.2.0 # via # build @@ -340,9 +336,10 @@ pyright==1.1.372 pytest==7.1.3 # via # pins (pyproject.toml) + # pytest-cases # pytest-dotenv # pytest-parallel -pytest-cases==3.8.6 +pytest-cases==3.9.1 # via pins (pyproject.toml) pytest-dotenv==0.5.2 # via pins (pyproject.toml) @@ -354,7 +351,7 @@ python-dateutil==2.9.0.post0 # botocore # jupyter-client # pandas -python-dotenv==1.1.0 +python-dotenv==1.1.1 # via pytest-dotenv pytz==2025.2 # via pandas @@ -363,20 +360,21 @@ pyyaml==6.0.2 # pins (pyproject.toml) # pre-commit # quartodoc -pyzmq==26.4.0 +pyzmq==27.1.0 # via # ipykernel # jupyter-client -quartodoc==0.10.0 +quartodoc==0.11.1 # via pins (pyproject.toml) -rdata==0.11.2 +rdata==1.0.0 # via pins (pyproject.toml) referencing==0.36.2 # via # jsonschema # jsonschema-specifications -requests==2.32.3 +requests==2.32.5 # via + # pins (pyproject.toml) # azure-core # azure-datalake-store # databricks-sdk @@ -384,14 +382,13 @@ requests==2.32.3 # google-api-core # google-cloud-storage # msal - # pins (pyproject.toml) # quartodoc # requests-oauthlib requests-oauthlib==2.0.0 # via google-auth-oauthlib -rich==14.0.0 +rich==14.1.0 # via plum-dispatch -rpds-py==0.25.1 +rpds-py==0.27.1 # via # jsonschema # referencing @@ -399,8 +396,10 @@ rsa==4.9.1 # via google-auth ruff==0.5.4 # via pins (pyproject.toml) -s3fs==2025.5.1 +s3fs==2025.7.0 # via pins (pyproject.toml) +setuptools==80.9.0 + # via pip-tools six==1.17.0 # via # azure-core @@ -415,13 +414,12 @@ tblib==3.1.0 # via pytest-parallel tomli==2.2.1 # via pytest -tornado==6.5.1 +tornado==6.5.2 # via # ipykernel # jupyter-client traitlets==5.14.3 # via - # comm # ipykernel # ipython # jupyter-client @@ -431,28 +429,27 @@ traitlets==5.14.3 # nbformat types-appdirs==1.4.3.5 # via pins (pyproject.toml) -typing-extensions==4.14.0 +typing-extensions==4.15.0 # via + # pins (pyproject.toml) # azure-core # azure-identity # azure-storage-blob - # pins (pyproject.toml) # plum-dispatch # pydantic # pydantic-core # quartodoc # rdata - # referencing # typing-inspection typing-inspection==0.4.1 # via pydantic tzdata==2025.2 # via pandas -urllib3==2.4.0 +urllib3==2.5.0 # via # botocore # requests -virtualenv==20.31.2 +virtualenv==20.34.0 # via pre-commit watchdog==6.0.0 # via quartodoc @@ -460,17 +457,13 @@ wcwidth==0.2.13 # via prompt-toolkit wheel==0.45.1 # via pip-tools -wrapt==1.17.2 +wrapt==1.17.3 # via aiobotocore -xarray==2025.4.0 +xarray==2025.9.0 # via rdata xxhash==3.5.0 # via pins (pyproject.toml) -yarl==1.20.0 +yarl==1.20.1 # via aiohttp -zipp==3.22.0 +zipp==3.23.0 # via importlib-metadata - -# The following packages are considered to be unsafe in a requirements file: -# pip -# setuptools