From 851e8750dd32e58554433d1ebebd9fc8d012d4ba Mon Sep 17 00:00:00 2001 From: Jordan Cook Date: Sun, 4 Dec 2022 13:05:10 -0600 Subject: Add tests for pypy3.9 --- .github/workflows/build.yml | 12 ++++++++++-- .github/workflows/deploy.yml | 5 ++++- HISTORY.md | 4 +++- noxfile.py | 9 +++++++-- tests/conftest.py | 7 +++++++ tests/integration/base_cache_test.py | 2 ++ tests/integration/test_sqlite.py | 12 ++++++++++-- tests/unit/test_session.py | 9 ++++++--- 8 files changed, 49 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3e7079a..380f48c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', 'pypy3.9'] fail-fast: false services: nginx: @@ -56,11 +56,19 @@ jobs: run: poetry install -v -E all # Run tests with coverage report - - name: Run tests + - name: Run unit + integration tests + if: ${{ !contains(matrix.python-version, 'pypy') }} run: | source $VENV nox -e cov -- xml + # pypy tests aren't run in parallel, so too slow for integration tests + - name: Run unit tests only + if: ${{ contains(matrix.python-version, 'pypy') }} + run: | + source $VENV + pytest tests/unit + # Latest python version: send coverage report to codecov - name: "Upload coverage report to Codecov" if: ${{ matrix.python-version == env.LATEST_PY_VERSION }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 13085d1..c627762 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -115,8 +115,11 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', 'pypy3.9'] requests-version: [2.22, 2.23, 2.24, 2.25, 2.26, 2.27, latest] + exclude: + - os: windows-latest + python-version: 'pypy3.9' fail-fast: false defaults: run: diff --git a/HISTORY.md b/HISTORY.md index 202a398..5f88393 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -100,7 +100,9 @@ * **redis-py:** Fix forwarding connection parameters passed to `RedisCache` for redis-py 4.2 and python <=3.8 * **pymongo:** Fix forwarding connection parameters passed to `MongoCache` for pymongo 4.1 and python <=3.8 * **cattrs:** Add compatibility with cattrs 22.2 -* **python:** Add tests to ensure compatibility with python 3.11 +* **python:** + * Add tests and support for python 3.11 + * Add tests and support for pypy 3.9 🪲 **Bugfixes:** * Fix usage of memory backend with `install_cache()` diff --git a/noxfile.py b/noxfile.py index b25854c..997aa32 100644 --- a/noxfile.py +++ b/noxfile.py @@ -7,6 +7,7 @@ Notes: * All other commands: the current environment will be used instead of creating new ones * Run `nox -l` to see all available commands """ +import platform from os import getenv from os.path import join from shutil import rmtree @@ -22,7 +23,7 @@ LIVE_DOCS_IGNORE = ['*.pyc', '*.tmp', join('**', 'modules', '*')] LIVE_DOCS_WATCH = ['requests_cache', 'examples'] CLEAN_DIRS = ['dist', 'build', join('docs', '_build'), join('docs', 'modules')] -PYTHON_VERSIONS = ['3.7', '3.8', '3.9', '3.10'] +PYTHON_VERSIONS = ['3.7', '3.8', '3.9', '3.10', '3.11', 'pypy3.9'] UNIT_TESTS = join('tests', 'unit') INTEGRATION_TESTS = join('tests', 'integration') STRESS_TEST_MULTIPLIER = 10 @@ -30,6 +31,8 @@ DEFAULT_COVERAGE_FORMATS = ['html', 'term'] # Run tests in parallel, grouped by test module XDIST_ARGS = '--numprocesses=auto --dist=loadfile' +IS_PYPY = platform.python_implementation() == 'PyPy' + @session(python=PYTHON_VERSIONS) def test(session): @@ -60,7 +63,9 @@ def clean(session): @session(python=False, name='cov') def coverage(session): """Run tests and generate coverage report""" - cmd = f'pytest {UNIT_TESTS} {INTEGRATION_TESTS} -rs {XDIST_ARGS} --cov'.split(' ') + cmd = f'pytest {UNIT_TESTS} {INTEGRATION_TESTS} -rs --cov'.split(' ') + if not IS_PYPY: + cmd += XDIST_ARGS.split(' ') # Add coverage formats cov_formats = session.posargs or DEFAULT_COVERAGE_FORMATS diff --git a/tests/conftest.py b/tests/conftest.py index 84710ae..bb69fd5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,6 +9,7 @@ Note: The protocol ``http(s)+mock://`` helps :py:class:`requests_mock.Adapter` p https://requests-mock.readthedocs.io/en/latest/adapter.html """ import os +import platform import warnings from contextlib import contextmanager from datetime import datetime, timedelta @@ -290,3 +291,9 @@ def ignore_deprecation(): # Some tests must disable url normalization to retain the custom `http+mock://` protocol patch_normalize_url = patch('requests_cache.cache_keys.normalize_url', side_effect=lambda x, y: x) + +# TODO: Debug OperationalErrors with pypy +skip_pypy = pytest.mark.skipif( + platform.python_implementation() == 'PyPy', + reason='pypy-specific database locking issue', +) diff --git a/tests/integration/base_cache_test.py b/tests/integration/base_cache_test.py index a7d2412..385e8fe 100644 --- a/tests/integration/base_cache_test.py +++ b/tests/integration/base_cache_test.py @@ -31,6 +31,7 @@ from tests.conftest import ( USE_PYTEST_HTTPBIN, assert_delta_approx_equal, httpbin, + skip_pypy, ) logger = getLogger(__name__) @@ -327,6 +328,7 @@ class BaseCacheTest: query_dict = parse_qs(query) assert query_dict['api_key'] == ['REDACTED'] + @skip_pypy @pytest.mark.parametrize('post_type', ['data', 'json']) def test_filter_request_post_data(self, post_type): method = 'POST' diff --git a/tests/integration/test_sqlite.py b/tests/integration/test_sqlite.py index 06b17cb..af94610 100644 --- a/tests/integration/test_sqlite.py +++ b/tests/integration/test_sqlite.py @@ -12,6 +12,7 @@ from platformdirs import user_cache_dir from requests_cache.backends import BaseCache, SQLiteCache, SQLiteDict from requests_cache.backends.sqlite import MEMORY_URI from requests_cache.models import CachedResponse +from tests.conftest import skip_pypy from tests.integration.base_cache_test import BaseCacheTest from tests.integration.base_storage_test import CACHE_NAME, BaseStorageTest @@ -132,11 +133,12 @@ class TestSQLiteDict(BaseStorageTest): assert 2 not in cache assert cache._can_commit is True + @skip_pypy @pytest.mark.parametrize('kwargs', [{'fast_save': True}, {'wal': True}]) def test_pragma(self, kwargs): """Test settings that make additional PRAGMA statements""" - cache_1 = self.init_cache(1, **kwargs) - cache_2 = self.init_cache(2, **kwargs) + cache_1 = self.init_cache('cache_1', **kwargs) + cache_2 = self.init_cache('cache_2', **kwargs) n = 500 for i in range(n): @@ -146,6 +148,7 @@ class TestSQLiteDict(BaseStorageTest): assert set(cache_1.keys()) == {f'key_{i}' for i in range(n)} assert set(cache_2.values()) == {f'value_{i}' for i in range(n)} + @skip_pypy @pytest.mark.parametrize('limit', [None, 50]) def test_sorted__by_size(self, limit): cache = self.init_cache() @@ -163,6 +166,7 @@ class TestSQLiteDict(BaseStorageTest): for i, item in enumerate(items): assert prev_item is None or len(prev_item) > len(item) + @skip_pypy def test_sorted__reversed(self): cache = self.init_cache() @@ -174,12 +178,14 @@ class TestSQLiteDict(BaseStorageTest): for i, item in enumerate(items): assert item == f'value_{100-i}' + @skip_pypy def test_sorted__invalid_sort_key(self): cache = self.init_cache() cache['key_1'] = 'value_1' with pytest.raises(ValueError): list(cache.sorted(key='invalid_key')) + @skip_pypy @pytest.mark.parametrize('limit', [None, 50]) def test_sorted__by_expires(self, limit): cache = self.init_cache() @@ -198,6 +204,7 @@ class TestSQLiteDict(BaseStorageTest): for i, item in enumerate(items): assert prev_item is None or prev_item.expires < item.expires + @skip_pypy def test_sorted__exclude_expired(self): cache = self.init_cache() now = datetime.utcnow() @@ -220,6 +227,7 @@ class TestSQLiteDict(BaseStorageTest): assert prev_item is None or prev_item.expires < item.expires assert item.status_code % 2 == 0 + @skip_pypy def test_sorted__error(self): """sorted() should handle deserialization errors and not return invalid responses""" diff --git a/tests/unit/test_session.py b/tests/unit/test_session.py index e7495cc..e8ec72d 100644 --- a/tests/unit/test_session.py +++ b/tests/unit/test_session.py @@ -668,7 +668,7 @@ def test_stale_while_revalidate(mock_session): mock_session.get(mocked_url_2, expire_after=timedelta(seconds=-2)) assert mock_session.cache.contains(url=MOCKED_URL_ETAG) - # First, let's just make sure the correct method is called + # First, check that the correct method is called mock_session.mock_adapter.register_uri('GET', MOCKED_URL_ETAG, status_code=304) with patch.object(CachedSession, '_resend_async') as mock_send: response = mock_session.get(MOCKED_URL_ETAG) @@ -683,10 +683,13 @@ def test_stale_while_revalidate(mock_session): with patch.object(CachedSession, '_send_and_cache', side_effect=slow_request) as mock_send: response = mock_session.get(mocked_url_2, expire_after=60) assert response.from_cache is True and response.is_expired is True - assert time() - start < 0.1 - sleep(1) # Background thread may be a bit slow on CI runner + assert time() - start < 0.1 # Response should be returned immediately; request takes 0.1s + sleep(1) # Background thread may be slow on CI runner mock_send.assert_called() + # An extra sleep AFTER patching magically fixes this test on pypy, and I have no idea why + sleep(1) + # Finally, check that the cached response has been refreshed response = mock_session.get(mocked_url_2) assert response.from_cache is True and response.is_expired is False -- cgit v1.2.1