diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 000000000..effdbbcf7 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,38 @@ +# 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 + +[run] +source = opal +# store relative paths to make it easier to combine different coverages +relative_files = True +omit = + .*, + **/tests/*, + # **/migrations/*, + opal/wsgi.py, + opal/asgi.py, + # omit prod settings + opal/settings_prod.py, + # omit sidebar menu + opal/templates/sidebar_menu.html, + opal/templates/components/menu_group.html, + opal/templates/components/menu_item.html +branch = True +plugins = + django_coverage_plugin + +[report] +fail_under = 100 +precision = 2 +show_missing = True +skip_empty = True +skip_covered = True +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + # Don't require coverage on TYPE_CHECKING imports + if TYPE_CHECKING: + + # Don't complain if non-runnable code isn't run: + if __name__ == .__main__.: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d4187368..67bca1a43 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,76 +114,6 @@ jobs: # if: failure() # run: exit 1 - test: - runs-on: ubuntu-latest - env: - DB_ROOT_PASSWORD: "root-password" - DB_PASSWORD: "user-password" - DB_USER: citest - container: python:3.12.9-alpine3.20 - services: - db: - image: mariadb:10.11.11-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@v4.2.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 - 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: contents: read diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..f549fe065 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,131 @@ +# 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: + push: + branches: + - main + pull_request: + workflow_dispatch: + merge_group: + +concurrency: + group: ${{ github.workflow }}-${{ 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.12.9-alpine3.20 + services: + db: + image: mariadb:10.11.11-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@v4.2.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@v4.6.2 + 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@v4.2.2 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@v5.4.1 + id: setup-uv + with: + # renovate: datasource=pypi dependency=uv + version: "0.6.12" + - name: Install dependencies + run: uv sync --locked --only-dev + - name: Download coverage reports + uses: actions/download-artifact@v4.2.1 + - 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 b1a6cb2bc..1d3ba52ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,10 +63,12 @@ dev = [ "pandas-stubs==2.2.3.250308", "pre-commit==4.2.0", "pytest==8.3.5", + "pytest-cov==6.1.1", "pytest-django==4.11.1", "pytest-mock==3.14.0", "pytest-randomly==3.16.0", "pytest-socket==0.7.0", + "pytest-split==0.10.0", "pytest-sugar==1.0.0", "ruff==0.11.4", "types-beautifulsoup4==4.12.0.20250204", diff --git a/setup.cfg b/setup.cfg index 5157ed08f..1c59fd834 100644 --- a/setup.cfg +++ b/setup.cfg @@ -44,37 +44,3 @@ warn_unreachable = False [mypy-*.factories] disallow_untyped_calls = False - -[coverage:run] -source = opal -; include = opal/* -omit = - .*, - **/tests/*, - # **/migrations/*, - opal/wsgi.py, - opal/asgi.py, - # omit prod settings - opal/settings_prod.py, - # omit sidebar menu - opal/templates/sidebar_menu.html, - opal/templates/components/menu_group.html, - opal/templates/components/menu_item.html -branch = True -plugins = - django_coverage_plugin - -[coverage:report] -fail_under = 100 -precision = 2 -show_missing = True -skip_empty = True -skip_covered = True -exclude_lines = - # Have to re-enable the standard pragma - pragma: no cover - # Don't require coverage on TYPE_CHECKING imports - if TYPE_CHECKING: - - # Don't complain if non-runnable code isn't run: - if __name__ == .__main__.: diff --git a/uv.lock b/uv.lock index 997e86a60..04ee6693a 100644 --- a/uv.lock +++ b/uv.lock @@ -56,10 +56,12 @@ dev = [ { name = "pandas-stubs" }, { name = "pre-commit" }, { 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" }, @@ -129,10 +131,12 @@ dev = [ { name = "pandas-stubs", specifier = "==2.2.3.250308" }, { name = "pre-commit", specifier = "==4.2.0" }, { name = "pytest", specifier = "==8.3.5" }, + { name = "pytest-cov", specifier = "==6.1.1" }, { name = "pytest-django", specifier = "==4.11.1" }, { name = "pytest-mock", specifier = "==3.14.0" }, { name = "pytest-randomly", specifier = "==3.16.0" }, { name = "pytest-socket", specifier = "==0.7.0" }, + { name = "pytest-split", specifier = "==0.10.0" }, { name = "pytest-sugar", specifier = "==1.0.0" }, { name = "ruff", specifier = "==0.11.4" }, { name = "types-beautifulsoup4", specifier = "==4.12.0.20250204" }, @@ -1549,6 +1553,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, ] +[[package]] +name = "pytest-cov" +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841 }, +] + [[package]] name = "pytest-django" version = "4.11.1" @@ -1597,6 +1614,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 }, ] +[[package]] +name = "pytest-split" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/d7/e30ba44adf83f15aee3f636daea54efadf735769edc0f0a7d98163f61038/pytest_split-0.10.0.tar.gz", hash = "sha256:adf80ba9fef7be89500d571e705b4f963dfa05038edf35e4925817e6b34ea66f", size = 13903 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/a7/cad88e9c1109a5c2a320d608daa32e5ee008ccbc766310f54b1cd6b3d69c/pytest_split-0.10.0-py3-none-any.whl", hash = "sha256:466096b086a7147bcd423c6e6c2e57fc62af1c5ea2e256b4ed50fc030fc3dddc", size = 11961 }, +] + [[package]] name = "pytest-sugar" version = "1.0.0"