diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e8909045..f4f8b3c7e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -116,76 +116,6 @@ jobs: if: '!cancelled()' run: uv run python manage.py validate_templates - test: - runs-on: ubuntu-latest - env: - DB_ROOT_PASSWORD: "root-password" - DB_PASSWORD: "user-password" - DB_USER: citest - container: python:3.13.12-alpine3.22 - services: - db: - image: mariadb:10.11.16-jammy - env: - MARIADB_ROOT_PASSWORD: ${{ env.DB_ROOT_PASSWORD }} - # ensure that user has permissions for test DB to be used by pytest - MARIADB_DATABASE: test_opal - MARIADB_USER: ${{ env.DB_USER }} - MARIADB_PASSWORD: ${{ env.DB_PASSWORD }} - - steps: - - name: Install Git - run: | - apk add --no-cache git git-lfs - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - lfs: true - - name: Install dependencies - run: | - pip install uv - echo "Installed uv version is $(uv --version)" - # install dependencies for mysqlclient - apk add --no-cache build-base mariadb-dev mariadb-client chromium - uv sync --locked - chromium --version - - name: Prepare environment - # set up env file for DB service - # use sample env file - # create additional DBs for legacy DB tests (OpalDB & QuestionnaireDB) - run: | - cp .env.sample .env - sed -i "s/^DATABASE_USER=.*/DATABASE_USER=$DB_USER/" .env - sed -i "s/^DATABASE_PASSWORD=.*/DATABASE_PASSWORD=$DB_PASSWORD/" .env - sed -i "s/^DATABASE_HOST=.*/DATABASE_HOST=db/" .env - # set up legacy DB connection - # reuse the same database as for the other tests to make it easier - sed -i "s/^LEGACY_DATABASE_HOST=.*/LEGACY_DATABASE_HOST=db/" .env - sed -i "s/^LEGACY_DATABASE_PORT=.*/LEGACY_DATABASE_PORT=3306/" .env - sed -i "s/^LEGACY_DATABASE_USER=.*/LEGACY_DATABASE_USER=$DB_USER/" .env - sed -i "s/^LEGACY_DATABASE_PASSWORD=.*/LEGACY_DATABASE_PASSWORD=$DB_PASSWORD/" .env - # generate secret key - SECRET_KEY=$(python -c "import secrets; print(secrets.token_urlsafe())") - sed -i "s/^SECRET_KEY=.*/SECRET_KEY=$SECRET_KEY/" .env - MYSQL_PWD=$DB_ROOT_PASSWORD mariadb -u root -h db --skip-ssl -e "GRANT ALL PRIVILEGES ON \`test_OpalDB\`.* TO \`$DB_USER\`@\`%\`;" - MYSQL_PWD=$DB_ROOT_PASSWORD mariadb -u root -h db --skip-ssl -e "GRANT ALL PRIVILEGES ON \`test_QuestionnaireDB\`.* TO \`$DB_USER\`@\`%\`;" - - name: Run pytest - run: | - uv run pytest --version - # -m "" runs all tests, even the ones marked as slow - uv run coverage run -m pytest -m "" -v --junitxml=test-report.xml - # see: https://github.com/dorny/test-reporter/issues/244 - # - name: Publish Test Results - # uses: dorny/test-reporter@v1.9.1 - # if: '!cancelled()' - # with: - # name: Tests - # path: ./test-report.xml - # reporter: java-junit - - name: Check coverage - run: | - uv run coverage report - markdownlint: permissions: @@ -195,6 +125,9 @@ jobs: run-reuse-workflow: uses: opalmedapps/.github/.github/workflows/reuse.yaml@main + test: + uses: ./.github/workflows/test.yml + build-docs: runs-on: ubuntu-latest needs: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..c93625385 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,127 @@ +# SPDX-FileCopyrightText: Copyright (C) 2025 Opal Health Informatics Group at the Research Institute of the McGill University Health Centre +# +# SPDX-License-Identifier: AGPL-3.0-or-later +name: test + +on: + workflow_call: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-test-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + seed: + runs-on: ubuntu-latest + outputs: + seed: ${{ steps.calculate-seed.outputs.seed }} + steps: + - name: Create seed + id: calculate-seed + run: echo "seed=$(python3 -c 'import random; print(random.Random().getrandbits(32));')" >> "$GITHUB_OUTPUT" + + # usage of pytest-split and matrix strategy based on: + # https://github.com/jerry-git/pytest-split-gh-actions-demo/blob/master/.github/workflows/test.yml + pytest: + runs-on: ubuntu-latest + needs: seed + strategy: + fail-fast: false + matrix: + group: [1, 2, 3, 4] + env: + DB_ROOT_PASSWORD: "root-password" + DB_PASSWORD: "user-password" + DB_USER: citest + SEED: ${{ needs.seed.outputs.seed }} + container: python:3.13.12-alpine3.22 + services: + db: + image: mariadb:10.11.16-jammy + env: + MARIADB_ROOT_PASSWORD: ${{ env.DB_ROOT_PASSWORD }} + # ensure that user has permissions for test DB to be used by pytest + MARIADB_DATABASE: test_opal + MARIADB_USER: ${{ env.DB_USER }} + MARIADB_PASSWORD: ${{ env.DB_PASSWORD }} + + steps: + - name: Install Git + run: | + apk add --no-cache git git-lfs + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + lfs: true + - name: Install dependencies + run: | + pip install uv + echo "Installed uv version is $(uv --version)" + # install dependencies for mysqlclient + apk add --no-cache build-base mariadb-dev mariadb-client chromium + uv sync --locked + - name: Prepare environment + # set up env file for DB service + # use sample env file + # create additional DBs for legacy DB tests (OpalDB & QuestionnaireDB) + run: | + cp .env.sample .env + sed -i "s/^DATABASE_USER=.*/DATABASE_USER=$DB_USER/" .env + sed -i "s/^DATABASE_PASSWORD=.*/DATABASE_PASSWORD=$DB_PASSWORD/" .env + sed -i "s/^DATABASE_HOST=.*/DATABASE_HOST=db/" .env + # set up legacy DB connection + # reuse the same database as for the other tests to make it easier + sed -i "s/^LEGACY_DATABASE_HOST=.*/LEGACY_DATABASE_HOST=db/" .env + sed -i "s/^LEGACY_DATABASE_PORT=.*/LEGACY_DATABASE_PORT=3306/" .env + sed -i "s/^LEGACY_DATABASE_USER=.*/LEGACY_DATABASE_USER=$DB_USER/" .env + sed -i "s/^LEGACY_DATABASE_PASSWORD=.*/LEGACY_DATABASE_PASSWORD=$DB_PASSWORD/" .env + # generate secret key + SECRET_KEY=$(python -c "import secrets; print(secrets.token_urlsafe())") + sed -i "s/^SECRET_KEY=.*/SECRET_KEY=$SECRET_KEY/" .env + MYSQL_PWD=$DB_ROOT_PASSWORD mariadb -u root -h db --skip-ssl -e "GRANT ALL PRIVILEGES ON \`test_OpalDB\`.* TO \`$DB_USER\`@\`%\`;" + MYSQL_PWD=$DB_ROOT_PASSWORD mariadb -u root -h db --skip-ssl -e "GRANT ALL PRIVILEGES ON \`test_QuestionnaireDB\`.* TO \`$DB_USER\`@\`%\`;" + - name: Run pytest + # use pytest-cov to disable coverage fail + # use the same seed for each group + run: | + uv run pytest --version + # -m "" runs all tests, even the ones marked as slow + uv run pytest --cov --cov-fail-under=0 --cov-report= --randomly-seed="$SEED" --splitting-algorithm=least_duration --splits 4 --group ${{ matrix.group }} -m "" + pwd + - name: Upload coverage + uses: actions/upload-artifact@v7.0.0 + with: + name: coverage-${{ matrix.group }} + path: .coverage + include-hidden-files: true + if-no-files-found: error + + coverage: + needs: pytest + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 + id: setup-uv + with: + # renovate: datasource=pypi dependency=uv + version: "0.10.7" + - name: Install dependencies + run: uv sync --locked --only-dev + - name: Download coverage reports + uses: actions/download-artifact@v8.0.0 + - name: Check coverage + run: | + pwd + ls -la coverage-* + ls -la opal/ + uv run coverage combine coverage*/.coverage* + ls -la + uv run coverage report diff --git a/pyproject.toml b/pyproject.toml index 45e4a9ec3..72958110c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,10 +70,12 @@ dev = [ "pandas-stubs==3.0.0.260204", "prek==0.3.4", "pytest==9.0.2", + "pytest-cov==7.0.0", "pytest-django==4.11.1", "pytest-mock==3.15.1", "pytest-randomly==4.0.1", "pytest-socket==0.7.0", + "pytest-split==0.11.0", "pytest-sugar==1.1.1", "ruff==0.15.4", "types-beautifulsoup4==4.12.0.20250516", diff --git a/renovate.json5 b/renovate.json5 index 461ebdec6..13266b0cc 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -101,5 +101,9 @@ "matchPackageNames": ["/uv-pre-commit|uv$/"], "groupName": "uv", }, + // Group upload and download artifact actions + { + "matchPackageNames": ["actions/upload-artifact", "actions/download-artifact"], + }, ], } diff --git a/uv.lock b/uv.lock index d16753c07..375215373 100644 --- a/uv.lock +++ b/uv.lock @@ -69,10 +69,12 @@ dev = [ { name = "pandas-stubs" }, { name = "prek" }, { name = "pytest" }, + { name = "pytest-cov" }, { name = "pytest-django" }, { name = "pytest-mock" }, { name = "pytest-randomly" }, { name = "pytest-socket" }, + { name = "pytest-split" }, { name = "pytest-sugar" }, { name = "ruff" }, { name = "types-beautifulsoup4" }, @@ -144,10 +146,12 @@ dev = [ { name = "pandas-stubs", specifier = "==3.0.0.260204" }, { name = "prek", specifier = "==0.3.4" }, { name = "pytest", specifier = "==9.0.2" }, + { name = "pytest-cov", specifier = "==7.0.0" }, { name = "pytest-django", specifier = "==4.11.1" }, { name = "pytest-mock", specifier = "==3.15.1" }, { name = "pytest-randomly", specifier = "==4.0.1" }, { name = "pytest-socket", specifier = "==0.7.0" }, + { name = "pytest-split", specifier = "==0.11.0" }, { name = "pytest-sugar", specifier = "==1.1.1" }, { name = "ruff", specifier = "==0.15.4" }, { name = "types-beautifulsoup4", specifier = "==4.12.0.20250516" }, @@ -1774,6 +1778,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + [[package]] name = "pytest-django" version = "4.11.1" @@ -1822,6 +1840,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/19/58/5d14cb5cb59409e491ebe816c47bf81423cd03098ea92281336320ae5681/pytest_socket-0.7.0-py3-none-any.whl", hash = "sha256:7e0f4642177d55d317bbd58fc68c6bd9048d6eadb2d46a89307fa9221336ce45", size = 6754, upload-time = "2024-01-28T20:17:22.105Z" }, ] +[[package]] +name = "pytest-split" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/16/8af4c5f2ceb3640bb1f78dfdf5c184556b10dfe9369feaaad7ff1c13f329/pytest_split-0.11.0.tar.gz", hash = "sha256:8ebdb29cc72cc962e8eb1ec07db1eeb98ab25e215ed8e3216f6b9fc7ce0ec2b5", size = 13421, upload-time = "2026-02-03T09:14:31.469Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/a1/d4423657caaa8be9b31e491592b49cebdcfd434d3e74512ce71f6ec39905/pytest_split-0.11.0-py3-none-any.whl", hash = "sha256:899d7c0f5730da91e2daf283860eb73b503259cb416851a65599368849c7f382", size = 11911, upload-time = "2026-02-03T09:14:33.708Z" }, +] + [[package]] name = "pytest-sugar" version = "1.1.1"