From f478e50bff2bb719400c74f8eaf9b1ec0f9fc3b5 Mon Sep 17 00:00:00 2001 From: Jason Giancono Date: Sun, 24 Mar 2024 01:43:57 +0800 Subject: [PATCH] refactor tests to just use pytest --- .../test-suite-unreleased-django.yaml | 2 +- .github/workflows/test-suite.yaml | 8 +- .pre-commit-config.yaml | 1 + Makefile | 3 + collectfasta/tests/command/test_command.py | 234 +++++++----------- collectfasta/tests/command/test_disable.py | 32 ++- collectfasta/tests/conftest.py | 214 ++++++++++++++++ .../strategies/test_caching_hash_strategy.py | 76 +++--- .../tests/strategies/test_hash_strategy.py | 38 ++- collectfasta/tests/test_settings.py | 1 + collectfasta/tests/utils.py | 60 ----- conftest.py | 20 -- setup.cfg | 5 +- test-requirements.txt | 1 + 14 files changed, 383 insertions(+), 312 deletions(-) create mode 100644 collectfasta/tests/conftest.py diff --git a/.github/workflows/test-suite-unreleased-django.yaml b/.github/workflows/test-suite-unreleased-django.yaml index 4a1a19d..f318e43 100644 --- a/.github/workflows/test-suite-unreleased-django.yaml +++ b/.github/workflows/test-suite-unreleased-django.yaml @@ -21,4 +21,4 @@ jobs: pip install --upgrade -r test-requirements.txt pip install . - name: Run tests - run: make docker-up && coverage run -m pytest + run: make docker-up && coverage run -m pytest --speedtest diff --git a/.github/workflows/test-suite.yaml b/.github/workflows/test-suite.yaml index 0f04b39..9420f36 100644 --- a/.github/workflows/test-suite.yaml +++ b/.github/workflows/test-suite.yaml @@ -46,9 +46,7 @@ jobs: if: github.event_name == 'push' && github.repository == 'jasongi/collectfasta' run: coverage run -m pytest - name: Run tests against docker env - run: make docker-up && coverage run -m pytest - - name: Run speed tests against docker env - run: make test-speed + run: make docker-up && coverage run -m pytest -svv --speed-test test-3-9: @@ -81,6 +79,4 @@ jobs: if: github.event_name == 'push' && github.repository == 'jasongi/collectfasta' run: coverage run -m pytest - name: Run tests against docker env - run: make docker-up && coverage run -m pytest - - name: Run speed tests against docker env - run: make test-speed + run: make docker-up && coverage run -m pytest --speedtest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a4333df..be5b761 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -56,6 +56,7 @@ repos: args: [] additional_dependencies: - pytest + - pytest-mock - pytest-django - typing-extensions - django-stubs>=1.4.0 diff --git a/Makefile b/Makefile index 4b6d4ec..4206f1e 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,9 @@ test: test-docker: docker-up pytest -svv; docker compose down +test-docker-all: docker-up + pytest -x --speedtest -svv; docker compose down + test-docker-ff: docker-up pytest -svv -x; docker compose down diff --git a/collectfasta/tests/command/test_command.py b/collectfasta/tests/command/test_command.py index aa66042..a2bb2ec 100644 --- a/collectfasta/tests/command/test_command.py +++ b/collectfasta/tests/command/test_command.py @@ -1,252 +1,200 @@ import timeit -from typing import Callable -from typing import Iterator -from unittest import TestCase -from unittest import mock +import pytest from django.core.exceptions import ImproperlyConfigured from django.test import override_settings as override_django_settings from django.test.utils import override_settings +from pytest_mock import MockerFixture from collectfasta.management.commands.collectstatic import Command -from collectfasta.tests.utils import assert_static_file_number +from collectfasta.tests.conftest import StrategyFixture +from collectfasta.tests.conftest import aws_backends_only +from collectfasta.tests.conftest import cloud_backends_only +from collectfasta.tests.conftest import exclude_two_pass +from collectfasta.tests.conftest import live_test +from collectfasta.tests.conftest import speed_test +from collectfasta.tests.conftest import two_pass_only from collectfasta.tests.utils import clean_static_dir from collectfasta.tests.utils import create_static_file from collectfasta.tests.utils import create_two_referenced_static_files -from collectfasta.tests.utils import live_test from collectfasta.tests.utils import make_100_files -from collectfasta.tests.utils import make_test -from collectfasta.tests.utils import many from collectfasta.tests.utils import override_setting from collectfasta.tests.utils import override_storage_attr -from collectfasta.tests.utils import speed_test from .utils import call_collectstatic -def generate_storage_variations( - storages: dict, strategies: dict -) -> Iterator[tuple[str, override_settings]]: - for storage_name, storage_settings in storages.items(): - for strategy_name, strategy_settings in strategies.items(): - yield ( - f"{storage_name}_{strategy_name}", - override_django_settings( - STORAGES={"staticfiles": {"BACKEND": storage_settings}}, - COLLECTFASTA_STRATEGY=strategy_settings, - ), - ) - - -aws_backend_confs = { - **dict( - generate_storage_variations( - { - "boto3": "storages.backends.s3.S3Storage", - "boto3static": "storages.backends.s3.S3StaticStorage", - }, - { - "base": "collectfasta.strategies.boto3.Boto3Strategy", - }, - ) - ), - **dict( - generate_storage_variations( - { - "boto3manifest": "storages.backends.s3.S3ManifestStaticStorage", - }, - { - "base": "collectfasta.strategies.boto3.Boto3Strategy", - "memory_2pass": "collectfasta.strategies.boto3.Boto3ManifestMemoryStrategy", # noqa E501 - "file_2pass": "collectfasta.strategies.boto3.Boto3ManifestFileSystemStrategy", # noqa E501 - }, - ) - ), -} - - -gcloud_backend_confs = { - "gcloud": override_django_settings( - STORAGES={ - "staticfiles": { - "BACKEND": "collectfasta.tests.utils.GoogleCloudStorageTest", - }, - }, - COLLECTFASTA_STRATEGY="collectfasta.strategies.gcloud.GoogleCloudStrategy", - ), -} -filesystem_backend_confs = { - "filesystem": override_django_settings( - STORAGES={ - "staticfiles": { - "BACKEND": "django.core.files.storage.FileSystemStorage", - }, - }, - COLLECTFASTA_STRATEGY="collectfasta.strategies.filesystem.FileSystemStrategy", - ), - "cachingfilesystem": override_django_settings( - STORAGES={ - "staticfiles": { - "BACKEND": "django.core.files.storage.FileSystemStorage", - }, - }, - COLLECTFASTA_STRATEGY=( - "collectfasta.strategies.filesystem.CachingFileSystemStrategy" - ), - ), -} -all_backend_confs = { - **aws_backend_confs, - **gcloud_backend_confs, - **filesystem_backend_confs, -} - -make_test_aws_backends: Callable = many(**aws_backend_confs) -make_test_all_backends: Callable = many(**all_backend_confs) -make_test_cloud_backends: Callable = many(**aws_backend_confs, **gcloud_backend_confs) - - -@make_test_all_backends @live_test -def test_basics(case: TestCase) -> None: +def test_basics(strategy: StrategyFixture) -> None: clean_static_dir() create_two_referenced_static_files() - assert_static_file_number(2, call_collectstatic(), case) + assert ( + f"{strategy.expected_copied_files(2)} static files copied." + in call_collectstatic() + ) # file state should now be cached - case.assertIn("0 static files copied.", call_collectstatic()) + assert "0 static files copied." in call_collectstatic() -@make_test_all_backends @live_test -def test_only_copies_new(case: TestCase) -> None: +def test_only_copies_new(strategy: StrategyFixture) -> None: clean_static_dir() create_two_referenced_static_files() - assert_static_file_number(2, call_collectstatic(), case) + assert ( + f"{strategy.expected_copied_files(2)} static files copied." + in call_collectstatic() + ) create_two_referenced_static_files() - assert_static_file_number(2, call_collectstatic(), case) + # Since the files were already created and are expected to be cached/not copied again, + # we expect 0 new files to be copied. + assert ( + f"{strategy.expected_copied_files(2)} static files copied." + in call_collectstatic() + ) -@make_test_all_backends @live_test @override_setting("threads", 5) -def test_threads(case: TestCase) -> None: +def test_threads(strategy: StrategyFixture) -> None: clean_static_dir() create_two_referenced_static_files() - assert_static_file_number(2, call_collectstatic(), case) + assert ( + f"{strategy.expected_copied_files(2)} static files copied." + in call_collectstatic() + ) # file state should now be cached - case.assertIn("0 static files copied.", call_collectstatic()) + assert "0 static files copied." in call_collectstatic() -@make_test_cloud_backends -@live_test +@cloud_backends_only @speed_test -def test_basics_cloud_speed(case: TestCase) -> None: +def test_basics_cloud_speed(strategy: StrategyFixture) -> None: clean_static_dir() make_100_files() - assert_static_file_number(100, call_collectstatic(), case) + assert ( + f"{strategy.expected_copied_files(100)} static files copied." + in call_collectstatic() + ) def collectstatic_one(): - assert_static_file_number(2, call_collectstatic(), case) + assert ( + f"{strategy.expected_copied_files(2)} static files copied." + in call_collectstatic() + ) create_two_referenced_static_files() ittook = timeit.timeit(collectstatic_one, number=1) print(f"it took {ittook} seconds") -@make_test_cloud_backends -@live_test +@cloud_backends_only @speed_test @override_settings( INSTALLED_APPS=["django.contrib.staticfiles"], COLLECTFASTA_STRATEGY=None ) -def test_no_collectfasta_cloud_speed(case: TestCase) -> None: +def test_no_collectfasta_cloud_speed(strategy: StrategyFixture) -> None: clean_static_dir() make_100_files() - - case.assertIn("100 static files copied", call_collectstatic()) + assert "100 static files copied" in call_collectstatic() def collectstatic_one(): - case.assertIn("2 static files copied", call_collectstatic()) + assert "2 static files copied" in call_collectstatic() create_two_referenced_static_files() ittook = timeit.timeit(collectstatic_one, number=1) print(f"it took {ittook} seconds") -@make_test -def test_dry_run(case: TestCase) -> None: +@exclude_two_pass +def test_dry_run(strategy: StrategyFixture) -> None: clean_static_dir() create_static_file() result = call_collectstatic(dry_run=True) - case.assertIn("1 static file copied.", result) - case.assertTrue("Pretending to copy", result) + assert "1 static file copied." in result + assert "Pretending to copy" in result result = call_collectstatic(dry_run=True) - case.assertIn("1 static file copied.", result) - case.assertTrue("Pretending to copy", result) - case.assertTrue("Pretending to delete", result) + assert "1 static file copied." in result + assert "Pretending to copy" in result + assert "Pretending to delete" in result -@make_test_aws_backends +@two_pass_only +def test_dry_run_two_pass(strategy: StrategyFixture) -> None: + clean_static_dir() + create_static_file() + result = call_collectstatic(dry_run=True) + assert "0 static files copied." in result + assert "Pretending to copy" in result + result = call_collectstatic(dry_run=True) + assert "0 static files copied." in result + assert "Pretending to copy" in result + assert "Pretending to delete" in result + + +@aws_backends_only @live_test @override_storage_attr("gzip", True) @override_setting("aws_is_gzipped", True) -def test_aws_is_gzipped(case: TestCase) -> None: +def test_aws_is_gzipped(strategy: StrategyFixture) -> None: clean_static_dir() create_two_referenced_static_files() - assert_static_file_number(2, call_collectstatic(), case) + assert ( + f"{strategy.expected_copied_files(2)} static files copied." + in call_collectstatic() + ) # file state should now be cached - case.assertIn("0 static files copied.", call_collectstatic()) + assert "0 static files copied." in call_collectstatic() -@make_test @override_django_settings(STORAGES={}, COLLECTFASTA_STRATEGY=None) -def test_raises_for_no_configured_strategy(case: TestCase) -> None: - with case.assertRaises(ImproperlyConfigured): +def test_raises_for_no_configured_strategy() -> None: + with pytest.raises(ImproperlyConfigured): Command._load_strategy() -@make_test_all_backends @live_test -@mock.patch("collectfasta.strategies.base.Strategy.post_copy_hook", autospec=True) -def test_calls_post_copy_hook(_case: TestCase, post_copy_hook: mock.MagicMock) -> None: +def test_calls_post_copy_hook(strategy: StrategyFixture, mocker: MockerFixture) -> None: + post_copy_hook = mocker.patch( + "collectfasta.strategies.base.Strategy.post_copy_hook", autospec=True + ) clean_static_dir() (path_one, path_two) = create_two_referenced_static_files() cmd = Command() cmd.run_from_argv(["manage.py", "collectstatic", "--noinput"]) post_copy_hook.assert_has_calls( [ - mock.call(mock.ANY, path_one.name, path_one.name, mock.ANY), - mock.call( - mock.ANY, + mocker.call(mocker.ANY, path_one.name, path_one.name, mocker.ANY), + mocker.call( + mocker.ANY, f"{path_one.name.replace('.html','')}_folder/{path_two.name}", f"{path_one.name.replace('.html','')}_folder/{path_two.name}", - mock.ANY, + mocker.ANY, ), ], any_order=True, ) -@make_test_all_backends @live_test -@mock.patch("collectfasta.strategies.base.Strategy.on_skip_hook", autospec=True) -def test_calls_on_skip_hook(case: TestCase, on_skip_hook: mock.MagicMock) -> None: +def test_calls_on_skip_hook(strategy: StrategyFixture, mocker: MockerFixture) -> None: + on_skip_hook = mocker.patch( + "collectfasta.strategies.base.Strategy.on_skip_hook", autospec=True + ) clean_static_dir() (path_one, path_two) = create_two_referenced_static_files() cmd = Command() cmd.run_from_argv(["manage.py", "collectstatic", "--noinput"]) on_skip_hook.assert_not_called() cmd.run_from_argv(["manage.py", "collectstatic", "--noinput"]) - on_skip_hook.assert_has_calls( [ - mock.call(mock.ANY, path_one.name, path_one.name, mock.ANY), - mock.call( - mock.ANY, + mocker.call(mocker.ANY, path_one.name, path_one.name, mocker.ANY), + mocker.call( + mocker.ANY, f"{path_one.name.replace('.html','')}_folder/{path_two.name}", f"{path_one.name.replace('.html','')}_folder/{path_two.name}", - mock.ANY, + mocker.ANY, ), ], any_order=True, diff --git a/collectfasta/tests/command/test_disable.py b/collectfasta/tests/command/test_disable.py index 71d4d1f..3fad9bc 100644 --- a/collectfasta/tests/command/test_disable.py +++ b/collectfasta/tests/command/test_disable.py @@ -1,18 +1,15 @@ -from unittest import TestCase -from unittest import mock - from django.test import override_settings as override_django_settings +from pytest_mock import MockerFixture +from collectfasta.tests.conftest import StrategyFixture +from collectfasta.tests.conftest import live_test from collectfasta.tests.utils import clean_static_dir from collectfasta.tests.utils import create_static_file -from collectfasta.tests.utils import live_test -from collectfasta.tests.utils import make_test from collectfasta.tests.utils import override_setting from .utils import call_collectstatic -@make_test @override_django_settings( STORAGES={ "staticfiles": { @@ -20,32 +17,33 @@ }, }, ) -def test_disable_collectfasta_with_default_storage(case: TestCase) -> None: +def test_disable_collectfasta_with_default_storage() -> None: clean_static_dir() create_static_file() - case.assertIn("1 static file copied", call_collectstatic(disable_collectfasta=True)) + assert "1 static file copied" in call_collectstatic(disable_collectfasta=True) -@make_test @live_test -def test_disable_collectfasta(case: TestCase) -> None: +def test_disable_collectfasta(strategy: StrategyFixture) -> None: clean_static_dir() create_static_file() - case.assertIn( - "1 static file copied.", call_collectstatic(disable_collectfasta=True) - ) + assert "1 static file copied" in call_collectstatic(disable_collectfasta=True) @override_setting("enabled", False) -@mock.patch("collectfasta.management.commands.collectstatic.Command._load_strategy") -def test_no_load_with_disable_setting(mocked_load_strategy: mock.MagicMock) -> None: +def test_no_load_with_disable_setting(mocker: MockerFixture) -> None: + mocked_load_strategy = mocker.patch( + "collectfasta.management.commands.collectstatic.Command._load_strategy" + ) clean_static_dir() call_collectstatic() mocked_load_strategy.assert_not_called() -@mock.patch("collectfasta.management.commands.collectstatic.Command._load_strategy") -def test_no_load_with_disable_flag(mocked_load_strategy: mock.MagicMock) -> None: +def test_no_load_with_disable_flag(mocker: MockerFixture) -> None: + mocked_load_strategy = mocker.patch( + "collectfasta.management.commands.collectstatic.Command._load_strategy" + ) clean_static_dir() call_collectstatic(disable_collectfasta=True) mocked_load_strategy.assert_not_called() diff --git a/collectfasta/tests/conftest.py b/collectfasta/tests/conftest.py new file mode 100644 index 0000000..3d870da --- /dev/null +++ b/collectfasta/tests/conftest.py @@ -0,0 +1,214 @@ +import os +import shutil +from typing import Any +from typing import Generator +from typing import Optional + +import pytest +from django.conf import settings +from django.test import override_settings as override_django_settings +from pytest import Collector +from pytest import CollectReport +from pytest import hookimpl + + +def composed(*decs): + def deco(f): + for dec in reversed(decs): + f = dec(f) + return f + + return deco + + +S3_STORAGE_BACKEND = "storages.backends.s3.S3Storage" +S3_STATIC_STORAGE_BACKEND = "storages.backends.s3.S3StaticStorage" +S3_MANIFEST_STATIC_STORAGE_BACKEND = "storages.backends.s3.S3ManifestStaticStorage" +GOOGLE_CLOUD_STORAGE_BACKEND = "collectfasta.tests.utils.GoogleCloudStorageTest" +FILE_SYSTEM_STORAGE_BACKEND = "django.core.files.storage.FileSystemStorage" + +BOTO3_STRATEGY = "collectfasta.strategies.boto3.Boto3Strategy" +BOTO3_MANIFEST_MEMORY_STRATEGY = ( + "collectfasta.strategies.boto3.Boto3ManifestMemoryStrategy" +) +BOTO3_MANIFEST_FILE_SYSTEM_STRATEGY = ( + "collectfasta.strategies.boto3.Boto3ManifestFileSystemStrategy" +) +GOOGLE_CLOUD_STRATEGY = "collectfasta.strategies.gcloud.GoogleCloudStrategy" +FILE_SYSTEM_STRATEGY = "collectfasta.strategies.filesystem.FileSystemStrategy" +CACHING_FILE_SYSTEM_STRATEGY = ( + "collectfasta.strategies.filesystem.CachingFileSystemStrategy" +) + +S3_BACKENDS = [ + S3_STORAGE_BACKEND, + S3_STATIC_STORAGE_BACKEND, + S3_MANIFEST_STATIC_STORAGE_BACKEND, +] +BACKENDS = [ + *S3_BACKENDS, + GOOGLE_CLOUD_STORAGE_BACKEND, + FILE_SYSTEM_STORAGE_BACKEND, +] + +STRATEGIES = [ + BOTO3_STRATEGY, + BOTO3_MANIFEST_MEMORY_STRATEGY, + BOTO3_MANIFEST_FILE_SYSTEM_STRATEGY, + GOOGLE_CLOUD_STRATEGY, + FILE_SYSTEM_STRATEGY, + CACHING_FILE_SYSTEM_STRATEGY, +] + +COMPATIBLE_STRATEGIES_FOR_BACKENDS = { + S3_STORAGE_BACKEND: [BOTO3_STRATEGY], + S3_STATIC_STORAGE_BACKEND: [BOTO3_STRATEGY], + S3_MANIFEST_STATIC_STORAGE_BACKEND: [ + BOTO3_STRATEGY, + BOTO3_MANIFEST_MEMORY_STRATEGY, + BOTO3_MANIFEST_FILE_SYSTEM_STRATEGY, + ], + GOOGLE_CLOUD_STORAGE_BACKEND: [GOOGLE_CLOUD_STRATEGY], + FILE_SYSTEM_STORAGE_BACKEND: [FILE_SYSTEM_STRATEGY, CACHING_FILE_SYSTEM_STRATEGY], +} + + +@hookimpl(hookwrapper=True) +def pytest_make_collect_report( + collector: Collector, +) -> Generator[Optional[CollectReport], Any, None]: + outcome = yield None + report: Optional[CollectReport] = outcome.get_result() + if report: + kept = [] + for item in report.result: + m = item.get_closest_marker("uncollect_if") + if m: + func = m.kwargs["func"] + if not (hasattr(item, "callspec") and hasattr(item.callspec, "params")): + raise ValueError( + "uncollect_if can only be run on parametrized tests" + ) + if func(**item.callspec.params): + continue + kept.append(item) + report.result = kept + outcome.force_result(report) + + +def two_n_plus_1(files): + return files * 2 + 1 + + +def n(files): + return files + + +def short_name(backend, strategy): + return f"{backend.split('.')[-1]}:{strategy.split('.')[-1]}" + + +def params_for_backends(): + for backend in BACKENDS: + for strategy in COMPATIBLE_STRATEGIES_FOR_BACKENDS[backend]: + yield pytest.param( + (backend, strategy), + marks=[pytest.mark.backend(backend), pytest.mark.strategy(strategy)], + id=short_name(backend, strategy), + ) + + +S3_BACKENDS = [ + S3_STORAGE_BACKEND, + S3_STATIC_STORAGE_BACKEND, + S3_MANIFEST_STATIC_STORAGE_BACKEND, +] + + +class StrategyFixture: + def __init__(self, expected_copied_files, backend, strategy, two_pass): + self.backend = backend + self.strategy = strategy + self.expected_copied_files = expected_copied_files + self.two_pass = two_pass + + +@pytest.fixture(params=params_for_backends()) +def strategy(request): + backend, strategy = request.param + if strategy in ( + BOTO3_MANIFEST_MEMORY_STRATEGY, + BOTO3_MANIFEST_FILE_SYSTEM_STRATEGY, + ) and backend in (S3_MANIFEST_STATIC_STORAGE_BACKEND): + expected_copied_files = two_n_plus_1 + else: + expected_copied_files = n + with override_django_settings( + STORAGES={"staticfiles": {"BACKEND": backend}}, + COLLECTFASTA_STRATEGY=strategy, + ): + yield StrategyFixture( + expected_copied_files, + backend, + strategy, + two_pass=strategy + in ( + BOTO3_MANIFEST_MEMORY_STRATEGY, + BOTO3_MANIFEST_FILE_SYSTEM_STRATEGY, + ), + ) + + +def uncollect_if_not_s3(strategy: tuple[str, str], **kwargs: dict) -> bool: + backend, _ = strategy + return backend not in S3_BACKENDS + + +def uncollect_if_not_cloud(strategy: tuple[str, str], **kwargs: dict) -> bool: + backend, _ = strategy + return backend not in S3_BACKENDS and backend != GOOGLE_CLOUD_STORAGE_BACKEND + + +live_test = pytest.mark.live_test +speed_test_mark = pytest.mark.speed_test + +speed_test = composed( + live_test, + speed_test_mark, + pytest.mark.skipif( + "not config.getoption('speedtest')", + reason="no --speedtest flag", + ), +) + +aws_backends_only = pytest.mark.uncollect_if(func=uncollect_if_not_s3) +cloud_backends_only = pytest.mark.uncollect_if(func=uncollect_if_not_cloud) + + +def uncollect_if_not_two_pass(strategy: tuple[str, str], **kwargs: dict) -> bool: + _, strategy_str = strategy + return strategy_str not in ( + BOTO3_MANIFEST_MEMORY_STRATEGY, + BOTO3_MANIFEST_FILE_SYSTEM_STRATEGY, + ) + + +def uncollect_if_two_pass(strategy: tuple[str, str], **kwargs: dict) -> bool: + return not uncollect_if_not_two_pass(strategy, **kwargs) + + +two_pass_only = pytest.mark.uncollect_if(func=uncollect_if_not_two_pass) +exclude_two_pass = pytest.mark.uncollect_if(func=uncollect_if_two_pass) + + +@pytest.fixture(autouse=True) +def create_test_directories(): + paths = (settings.STATICFILES_DIRS[0], settings.STATIC_ROOT, settings.MEDIA_ROOT) + for path in paths: + if not os.path.exists(path): + os.makedirs(path) + try: + yield + finally: + for path in paths: + shutil.rmtree(path) diff --git a/collectfasta/tests/strategies/test_caching_hash_strategy.py b/collectfasta/tests/strategies/test_caching_hash_strategy.py index 38fcbb5..ff8b38e 100644 --- a/collectfasta/tests/strategies/test_caching_hash_strategy.py +++ b/collectfasta/tests/strategies/test_caching_hash_strategy.py @@ -1,12 +1,10 @@ import string -from unittest import TestCase -from unittest import mock from django.core.files.storage import FileSystemStorage +from pytest_mock import MockerFixture from collectfasta import settings from collectfasta.strategies.base import CachingHashStrategy -from collectfasta.tests.utils import make_test hash_characters = string.ascii_letters + string.digits @@ -19,55 +17,53 @@ def get_remote_file_hash(self, prefixed_path: str) -> None: pass -@make_test -def test_get_cache_key(case: TestCase) -> None: +def test_get_cache_key() -> None: strategy = Strategy() cache_key = strategy.get_cache_key("/some/random/path") prefix_len = len(settings.cache_key_prefix) - case.assertTrue(cache_key.startswith(settings.cache_key_prefix)) - case.assertEqual(32 + prefix_len, len(cache_key)) + # case.assertTrue(cache_key.startswith(settings.cache_key_prefix)) + assert cache_key.startswith(settings.cache_key_prefix) + assert len(cache_key) == 32 + prefix_len expected_chars = hash_characters + "_" for c in cache_key: - case.assertIn(c, expected_chars) + assert c in expected_chars -@make_test -def test_gets_and_invalidates_hash(case: TestCase) -> None: +def test_gets_and_invalidates_hash(mocker: MockerFixture) -> None: strategy = Strategy() expected_hash = "hash" - with mock.patch.object( - strategy, "get_remote_file_hash", new=mock.MagicMock(return_value=expected_hash) - ) as mocked: - # empty cache - result_hash = strategy.get_cached_remote_file_hash("path", "prefixed_path") - case.assertEqual(result_hash, expected_hash) - mocked.assert_called_once_with("prefixed_path") - - # populated cache - mocked.reset_mock() - result_hash = strategy.get_cached_remote_file_hash("path", "prefixed_path") - case.assertEqual(result_hash, expected_hash) - mocked.assert_not_called() - - # test destroy_etag - mocked.reset_mock() - strategy.invalidate_cached_hash("path") - result_hash = strategy.get_cached_remote_file_hash("path", "prefixed_path") - case.assertEqual(result_hash, expected_hash) - mocked.assert_called_once_with("prefixed_path") - - -@make_test -def test_post_copy_hook_primes_cache(case: TestCase) -> None: + mocked = mocker.patch.object( + strategy, + "get_remote_file_hash", + new=mocker.MagicMock(return_value=expected_hash), + ) + # empty cache + result_hash = strategy.get_cached_remote_file_hash("path", "prefixed_path") + assert result_hash == expected_hash + mocked.assert_called_once_with("prefixed_path") + + # populated cache + mocked.reset_mock() + result_hash = strategy.get_cached_remote_file_hash("path", "prefixed_path") + assert result_hash == expected_hash + mocked.assert_not_called() + + # test destroy_etag + mocked.reset_mock() + strategy.invalidate_cached_hash("path") + result_hash = strategy.get_cached_remote_file_hash("path", "prefixed_path") + assert result_hash == expected_hash + mocked.assert_called_once_with("prefixed_path") + + +def test_post_copy_hook_primes_cache(mocker: MockerFixture) -> None: filename = "123abc" expected_hash = "abc123" strategy = Strategy() - with mock.patch.object( + mocker.patch.object( strategy, "get_local_file_hash", return_value=expected_hash, autospec=True - ): - strategy.post_copy_hook(filename, filename, strategy.remote_storage) - - case.assertEqual( - expected_hash, strategy.get_cached_remote_file_hash(filename, filename) ) + strategy.post_copy_hook(filename, filename, strategy.remote_storage) + + assert strategy.get_cached_remote_file_hash(filename, filename) == expected_hash diff --git a/collectfasta/tests/strategies/test_hash_strategy.py b/collectfasta/tests/strategies/test_hash_strategy.py index 07cb10f..747e49d 100644 --- a/collectfasta/tests/strategies/test_hash_strategy.py +++ b/collectfasta/tests/strategies/test_hash_strategy.py @@ -1,13 +1,11 @@ import re import tempfile -from unittest import TestCase -from unittest import mock from django.contrib.staticfiles.storage import StaticFilesStorage from django.core.files.storage import FileSystemStorage +from pytest_mock import MockerFixture from collectfasta.strategies.base import HashStrategy -from collectfasta.tests.utils import make_test class Strategy(HashStrategy[FileSystemStorage]): @@ -18,36 +16,28 @@ def get_remote_file_hash(self, prefixed_path: str) -> None: pass -@make_test -def test_get_file_hash(case: TestCase) -> None: +def test_get_file_hash() -> None: strategy = Strategy() local_storage = StaticFilesStorage() with tempfile.NamedTemporaryFile(dir=local_storage.base_location) as f: f.write(b"spam") hash_ = strategy.get_local_file_hash(f.name, local_storage) - case.assertTrue(re.fullmatch(r"^[A-z0-9]{32}$", hash_) is not None) + assert re.fullmatch(r"^[A-z0-9]{32}$", hash_) is not None -@make_test -def test_should_copy_file(case: TestCase) -> None: +def test_should_copy_file(mocker: MockerFixture) -> None: strategy = Strategy() local_storage = StaticFilesStorage() remote_hash = "foo" - mock_remote_hash = mock.patch.object( - strategy, "get_remote_file_hash", mock.MagicMock(return_value=remote_hash) + mocker.patch.object( + strategy, "get_remote_file_hash", mocker.MagicMock(return_value=remote_hash) ) - - with mock_remote_hash: - with mock.patch.object( - strategy, "get_local_file_hash", mock.MagicMock(return_value=remote_hash) - ): - case.assertFalse( - strategy.should_copy_file("path", "prefixed_path", local_storage) - ) - with mock.patch.object( - strategy, "get_local_file_hash", mock.MagicMock(return_value="bar") - ): - case.assertTrue( - strategy.should_copy_file("path", "prefixed_path", local_storage) - ) + mocker.patch.object( + strategy, "get_local_file_hash", mocker.MagicMock(return_value=remote_hash) + ) + assert not strategy.should_copy_file("path", "prefixed_path", local_storage) + mocker.patch.object( + strategy, "get_local_file_hash", mocker.MagicMock(return_value="bar") + ) + assert strategy.should_copy_file("path", "prefixed_path", local_storage) diff --git a/collectfasta/tests/test_settings.py b/collectfasta/tests/test_settings.py index a9c168d..2a2d078 100644 --- a/collectfasta/tests/test_settings.py +++ b/collectfasta/tests/test_settings.py @@ -56,6 +56,7 @@ def test_settings_with_threads(): {"AWS_IS_GZIPPED": "yes"}, {"GZIP_CONTENT_TYPES": "not tuple"}, ), + ids=lambda x: list(x.keys())[0], ) def test_invalid_setting_type_raises_value_error(django_settings): with override_settings(**django_settings): diff --git a/collectfasta/tests/utils.py b/collectfasta/tests/utils.py index 8404be9..3b25279 100644 --- a/collectfasta/tests/utils.py +++ b/collectfasta/tests/utils.py @@ -2,17 +2,13 @@ import os import pathlib import random -import unittest import uuid from concurrent.futures import ThreadPoolExecutor from typing import Any from typing import Callable -from typing import Type from typing import TypeVar from typing import cast -from unittest import TestCase -import pytest from django.conf import settings as django_settings from django.utils.module_loading import import_string from storages.backends.gcloud import GoogleCloudStorage @@ -20,29 +16,11 @@ from collectfasta import settings -live_test = pytest.mark.live_test - -speed_test_mark = pytest.mark.speed_test -speed_test_option = pytest.mark.skipif("not config.getoption('speedtest')") - - -def speed_test(func): - return speed_test_mark(speed_test_option(func)) - - static_dir: Final = pathlib.Path(django_settings.STATICFILES_DIRS[0]) F = TypeVar("F", bound=Callable[..., Any]) -def assert_static_file_number(files: int, output: str, case: TestCase) -> None: - # if it's a manifest, there will be 2*N + 1 files copied - if "manifest" in case.id() and "2pass" in case.id(): - case.assertIn(f"{files*2+1} static files copied.", output) - else: - case.assertIn(f"{files} static files copied.", output) - - def make_100_files(): with ThreadPoolExecutor(max_workers=5) as executor: for _ in range(50): @@ -71,44 +49,6 @@ def __init__(self, *args, **kwargs): self._client = get_fake_client() -def make_test(func: F) -> Type[unittest.TestCase]: - """ - Creates a class that inherits from `unittest.TestCase` with the decorated - function as a method. Create tests like this: - - >>> fn = lambda x: 1337 - >>> @make_test - ... def test_fn(case): - ... case.assertEqual(fn(), 1337) - """ - case = type(func.__name__, (unittest.TestCase,), {func.__name__: func}) - case.__module__ = func.__module__ - return case - - -def many(**mutations: Callable[[F], F]) -> Callable[[F], Type[unittest.TestCase]]: - def test(func: F) -> Type[unittest.TestCase]: - """ - Creates a class that inherits from `unittest.TestCase` with the decorated - function as a method. Create tests like this: - - >>> fn = lambda x: 1337 - >>> @make_test - ... def test_fn(case): - ... case.assertEqual(fn(), 1337) - """ - case_dict = { - "test_%s" % mutation_name: mutation(func) - for mutation_name, mutation in mutations.items() - } - - case = type(func.__name__, (unittest.TestCase,), case_dict) - case.__module__ = func.__module__ - return case - - return test - - def create_two_referenced_static_files() -> tuple[pathlib.Path, pathlib.Path]: """Create a static file, then another file with a reference to the file""" path = create_static_file() diff --git a/conftest.py b/conftest.py index ec48cb2..c5193c2 100644 --- a/conftest.py +++ b/conftest.py @@ -1,10 +1,3 @@ -import os -import shutil - -import pytest -from django.conf import settings - - def pytest_addoption(parser): parser.addoption( "--speedtest", @@ -13,16 +6,3 @@ def pytest_addoption(parser): default=False, help="run the test on many files (not for live environments", ) - - -@pytest.fixture(autouse=True) -def create_test_directories(): - paths = (settings.STATICFILES_DIRS[0], settings.STATIC_ROOT, settings.MEDIA_ROOT) - for path in paths: - if not os.path.exists(path): - os.makedirs(path) - try: - yield - finally: - for path in paths: - shutil.rmtree(path) diff --git a/setup.cfg b/setup.cfg index 9e91471..5527fe7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,7 +40,10 @@ universal = true DJANGO_SETTINGS_MODULE = collectfasta.tests.settings markers = live_test: tests that interact with real APIs if credentials are set, docker if they aren't - speed_test: tests that upload lots of files + speed_test: tests that test processing many static files + backend(backend): test that runs on a specific backend - generated by the strategy fixture + strategy(strategy): test that runs on a specific backend - generated by the strategy fixture + uncollect_if: uncollect tests that match the given condition (see https://github.com/pytest-dev/pytest/issues/3730#issuecomment-567142496) [flake8] exclude = appveyor, .idea, .git, .venv, .tox, __pycache__, *.egg-info, build diff --git a/test-requirements.txt b/test-requirements.txt index b779956..cae8a2a 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -5,6 +5,7 @@ django-storages boto3 google-cloud-storage pytest +pytest-mock pytest-django django-stubs boto3-stubs[s3]