diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a2651434f..26fdea7ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,7 +50,7 @@ jobs: run: pnpm run build - name: Run tests run: | - coverage run --branch -m pytest froide/ + pytest -n auto --cov --cov-report= froide/ coverage report --format=markdown >> $GITHUB_STEP_SUMMARY env: DATABASE_URL: postgis://postgres:postgres@localhost/froide diff --git a/.gitignore b/.gitignore index b83b76fe2..a2491cb72 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ local_*.py data/ files/ froide/tests/testdata/*/ -.coverage +.coverage* */_build/ .idea htmlcov/ diff --git a/Makefile b/Makefile index 88824c07d..5da20b8a1 100644 --- a/Makefile +++ b/Makefile @@ -4,11 +4,7 @@ export PYTHONWARNINGS=default test: ruff check - coverage run --branch -m pytest froide/ - coverage report - -testui: - coverage run --branch -m pytest --browser chromium froide/tests/live/ + pytest --cov froide/ .PHONY: htmlcov htmlcov: diff --git a/froide/campaign/tests/test_campaign.py b/froide/campaign/tests/test_campaign.py index 132f2b8a2..747fd0c15 100644 --- a/froide/campaign/tests/test_campaign.py +++ b/froide/campaign/tests/test_campaign.py @@ -60,6 +60,7 @@ def test_campaign_request_match(world, client): @pytest.mark.django_db +@pytest.mark.xdist_group(name="sequential") @pytest.mark.asyncio(loop_scope="session") async def test_campaign_request_match_live( page: Page, live_server, public_body_with_index, dummy_user diff --git a/froide/foirequest/tests/test_api_request.py b/froide/foirequest/tests/test_api_request.py index a09db1aa8..c78a69258 100644 --- a/froide/foirequest/tests/test_api_request.py +++ b/froide/foirequest/tests/test_api_request.py @@ -113,6 +113,7 @@ def test_search(self): response = self.client.get("/api/v1/request/search/?q=Number") self.assertEqual(response.status_code, 200) + @pytest.mark.xdist_group(name="sequential") def test_search_similar(self): factories.delete_index() search_url = "/api/v1/request/search/" diff --git a/froide/foirequest/tests/test_request.py b/froide/foirequest/tests/test_request.py index 77dc74122..76811f499 100644 --- a/froide/foirequest/tests/test_request.py +++ b/froide/foirequest/tests/test_request.py @@ -1561,6 +1561,7 @@ def test_resolution(world, client): @pytest.mark.django_db +@pytest.mark.xdist_group(name="sequential") def test_search(world, client, pb): pb = PublicBody.objects.all()[0] factories.rebuild_index() diff --git a/froide/foirequest/tests/test_web.py b/froide/foirequest/tests/test_web.py index 15273efe2..b44ca8e36 100644 --- a/froide/foirequest/tests/test_web.py +++ b/froide/foirequest/tests/test_web.py @@ -94,6 +94,7 @@ def test_request_prefilled_redirect(world, client): @pytest.mark.django_db +@pytest.mark.xdist_group(name="sequential") def test_list_requests(world, client): factories.rebuild_index() response = client.get(reverse("foirequest-list")) @@ -118,6 +119,7 @@ def test_list_requests(world, client): @pytest.mark.django_db +@pytest.mark.xdist_group(name="sequential") def test_list_jurisdiction_requests(world, client): factories.rebuild_index() juris = Jurisdiction.objects.all()[0] @@ -151,6 +153,7 @@ def test_list_jurisdiction_requests(world, client): @pytest.mark.django_db +@pytest.mark.xdist_group(name="sequential") def test_tagged_requests(world, client): tag_slug = "awesome" req = FoiRequest.published.all()[0] @@ -170,6 +173,7 @@ def test_tagged_requests(world, client): @pytest.mark.django_db +@pytest.mark.xdist_group(name="sequential") def test_publicbody_requests(world, client): factories.rebuild_index() req = FoiRequest.published.all()[0] @@ -188,6 +192,7 @@ def test_publicbody_requests(world, client): @pytest.mark.django_db(transaction=True) +@pytest.mark.xdist_group(name="sequential") def test_list_no_identical(world, client): factories.FoiRequestFactory.create(site=world) factories.rebuild_index() @@ -277,6 +282,7 @@ def test_auth_links(world, client): @pytest.mark.django_db +@pytest.mark.xdist_group(name="sequential") def test_feed(world, client): factories.rebuild_index() @@ -673,6 +679,7 @@ def jurisdiction_with_many_requests_slug(request: pytest.FixtureRequest): @pytest.mark.django_db +@pytest.mark.xdist_group(name="sequential") @pytest.mark.parametrize( "filter_field,filter_value,filter_value_function", [["jurisdiction", None, jurisdiction_with_many_requests_slug], ["q", "*", None]], @@ -727,6 +734,7 @@ def dict_combinations_all_r(sequence): @pytest.mark.django_db +@pytest.mark.xdist_group(name="sequential") def test_request_list_path_filter( client: Client, jurisdiction_with_many_requests: Jurisdiction, diff --git a/froide/helper/tests/test_search.py b/froide/helper/tests/test_search.py index 6f6cd82d3..177f6b3a9 100644 --- a/froide/helper/tests/test_search.py +++ b/froide/helper/tests/test_search.py @@ -44,6 +44,7 @@ def __init__(self, pk): self.query_highlight = None +@pytest.mark.xdist_group(name="sequential") class TestBaseSearchFilterSetQueryPreprocessing: def test_auto_query_without_query_value(self): qs = DummyQS() diff --git a/froide/publicbody/tests/test_search.py b/froide/publicbody/tests/test_search.py index 33da27bdd..3ec53a19a 100644 --- a/froide/publicbody/tests/test_search.py +++ b/froide/publicbody/tests/test_search.py @@ -76,6 +76,7 @@ def publicbody_data(db): } +@pytest.mark.xdist_group(name="sequential") class TestPublicBodyView: """Tests for the publicbody list view and its search/filter functionality.""" @@ -160,6 +161,7 @@ def test_search_filters( assert result_names == set(expected_names) +@pytest.mark.xdist_group(name="sequential") class TestPublicBodySearchAPI: """Tests for the publicbody search API. diff --git a/froide/publicbody/tests/test_web.py b/froide/publicbody/tests/test_web.py index b095896b0..32e25c1f6 100644 --- a/froide/publicbody/tests/test_web.py +++ b/froide/publicbody/tests/test_web.py @@ -6,6 +6,8 @@ from django.test import TestCase from django.urls import reverse +import pytest + from froide.account.factories import UserFactory from froide.foirequest.tests.factories import make_world, rebuild_index from froide.georegion.models import GeoRegion @@ -29,6 +31,7 @@ class PublicBodyTest(TestCase): def setUp(self): self.site = make_world() + @pytest.mark.xdist_group(name="sequential") def test_web_page(self): pb = PublicBody.objects.all()[0] category = CategoryFactory.create(is_topic=True) diff --git a/froide/tests/live/test_redaction.py b/froide/tests/live/test_redaction.py index 8f7330b9e..27c425e70 100644 --- a/froide/tests/live/test_redaction.py +++ b/froide/tests/live/test_redaction.py @@ -40,6 +40,7 @@ def get_colors_from_image(image: Image) -> set: @pytest.mark.django_db +@pytest.mark.xdist_group(name="sequential") @pytest.mark.asyncio(loop_scope="session") @override_settings(SERVE_MEDIA=True) async def test_redaction(world, live_server, settings, page: Page): diff --git a/froide/tests/live/test_request.py b/froide/tests/live/test_request.py index cbb6be977..e12db3193 100644 --- a/froide/tests/live/test_request.py +++ b/froide/tests/live/test_request.py @@ -21,6 +21,7 @@ @pytest.mark.django_db +@pytest.mark.xdist_group(name="sequential") @pytest.mark.asyncio(loop_scope="session") async def test_make_not_logged_in_request( page: Page, live_server, public_body_with_index @@ -79,6 +80,7 @@ async def test_make_not_logged_in_request( @pytest.mark.django_db +@pytest.mark.xdist_group(name="sequential") @pytest.mark.asyncio(loop_scope="session") async def test_make_not_logged_in_request_to_public_body( page: Page, live_server, world @@ -124,6 +126,7 @@ async def test_make_not_logged_in_request_to_public_body( @pytest.mark.django_db +@pytest.mark.xdist_group(name="sequential") @pytest.mark.asyncio(loop_scope="session") async def test_make_logged_in_request( page, live_server, public_body_with_index, dummy_user @@ -160,6 +163,7 @@ async def test_make_logged_in_request( @pytest.mark.django_db +@pytest.mark.xdist_group(name="sequential") @pytest.mark.asyncio(loop_scope="session") async def test_make_logged_in_request_too_many( page: Page, @@ -195,6 +199,7 @@ async def test_make_logged_in_request_too_many( @pytest.mark.django_db +@pytest.mark.xdist_group(name="sequential") @pytest.mark.asyncio(loop_scope="session") async def test_make_request_logged_out_with_existing_account( page: Page, live_server, world @@ -238,6 +243,7 @@ async def test_make_request_logged_out_with_existing_account( @pytest.mark.django_db +@pytest.mark.xdist_group(name="sequential") @pytest.mark.asyncio(loop_scope="session") async def test_edit_request_boilerplate( page: Page, live_server, public_body_with_index, dummy_user @@ -283,6 +289,7 @@ async def test_edit_request_boilerplate( @pytest.mark.django_db +@pytest.mark.xdist_group(name="sequential") @pytest.mark.asyncio(loop_scope="session") async def test_skip_search_similar( page: Page, live_server, world, public_body_with_index @@ -322,6 +329,7 @@ async def test_skip_search_similar( @pytest.mark.django_db +@pytest.mark.xdist_group(name="sequential") @pytest.mark.asyncio(loop_scope="session") @pytest.mark.parametrize( "from_resolution, to_resolution", diff --git a/pyproject.toml b/pyproject.toml index ef7f8e082..a4216240a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,7 +67,6 @@ dependencies = [ [dependency-groups] dev = [ "beautifulsoup4", - "coverage[toml]", "django-coverage-plugin", "django-extended-makemessages>=1.7.1", "django-stubs", @@ -92,6 +91,8 @@ dev = [ "types-python-dateutil", "types-requests", "prek>=0.3.5", + "pytest-xdist>=3.8.0", + "pytest-cov>=7.1.0", ] [build-system] @@ -146,16 +147,12 @@ show_missing = true skip_covered = true exclude_lines = ["pragma: no cover"] -[tool.pytest.ini_options] +[tool.pytest] DJANGO_CONFIGURATION = "Test" DJANGO_SETTINGS_MODULE = "froide.settings" python_files = ["tests.py", "test_*.py", "*_tests.py"] -addopts = ["--reuse-db"] +addopts = ["--reuse-db", "--dist", "loadgroup"] markers = ["no_delivery_mock"] -filterwarnings = [ - "ignore::DeprecationWarning:(?!froide).*", - "ignore::PendingDeprecationWarning:(?!froide)*", -] [tool.mypy] plugins = ["mypy_django_plugin.main"] diff --git a/uv.lock b/uv.lock index 2abe03ad6..41cc9497f 100644 --- a/uv.lock +++ b/uv.lock @@ -1065,6 +1065,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/a9/b200790a22585aeb023d88bd8b9fb222820e2976ce4239d401670116ae3c/elasticsearch_dsl-8.18.0-py3-none-any.whl", hash = "sha256:0522c5bb20c7abae69855109e650bf1166d486cbf706b5e1b29c28936a9102a3", size = 10406, upload-time = "2025-04-16T11:54:12.677Z" }, ] +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + [[package]] name = "factory-boy" version = "3.3.3" @@ -1216,7 +1225,6 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "beautifulsoup4" }, - { name = "coverage" }, { name = "django-coverage-plugin" }, { name = "django-extended-makemessages" }, { name = "django-stubs" }, @@ -1231,9 +1239,11 @@ dev = [ { name = "pyflakes" }, { name = "pytest" }, { name = "pytest-asyncio" }, + { name = "pytest-cov" }, { name = "pytest-django" }, { name = "pytest-factoryboy" }, { name = "pytest-playwright-asyncio" }, + { name = "pytest-xdist" }, { name = "ruff" }, { name = "tblib" }, { name = "text-unidecode" }, @@ -1297,7 +1307,6 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "beautifulsoup4" }, - { name = "coverage", extras = ["toml"] }, { name = "django-coverage-plugin" }, { name = "django-extended-makemessages", specifier = ">=1.7.1" }, { name = "django-stubs" }, @@ -1312,9 +1321,11 @@ dev = [ { name = "pyflakes" }, { name = "pytest" }, { name = "pytest-asyncio" }, + { name = "pytest-cov", specifier = ">=7.1.0" }, { name = "pytest-django" }, { name = "pytest-factoryboy" }, { name = "pytest-playwright-asyncio", specifier = ">=0.7.0" }, + { name = "pytest-xdist", specifier = ">=3.8.0" }, { name = "ruff" }, { name = "tblib" }, { name = "text-unidecode" }, @@ -2554,6 +2565,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/1c/b00940ab9eb8ede7897443b771987f2f4a76f06be02f1b3f01eb7567e24a/pytest_base_url-2.1.0-py3-none-any.whl", hash = "sha256:3ad15611778764d451927b2a53240c1a7a591b521ea44cebfe45849d2d2812e6", size = 5302, upload-time = "2024-01-31T22:42:58.897Z" }, ] +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + [[package]] name = "pytest-django" version = "4.12.0" @@ -2598,6 +2623,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b9/ac/b0e2515fb3a30d24c009907eae2317c71756c57df0bb3eefe19541c3a191/pytest_playwright_asyncio-0.7.2-py3-none-any.whl", hash = "sha256:176b17007b74b9ce9487ca570ec896c0f74a880eff172412f22a0bb66a8d5002", size = 17087, upload-time = "2025-11-24T03:43:25.253Z" }, ] +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + [[package]] name = "python-crontab" version = "3.3.0"