diff --git a/.github/workflows/test-fakeredis.yml b/.github/workflows/test-fakeredis.yml index b9b530d02..598e70c57 100644 --- a/.github/workflows/test-fakeredis.yml +++ b/.github/workflows/test-fakeredis.yml @@ -70,7 +70,11 @@ jobs: working-directory: tests/fakeredis run: | poetry run pytest test/ \ - --ignore test/test_hypothesis.py \ + --ignore test/test_hypothesis/test_hash.py \ + --ignore test/test_hypothesis/test_set.py \ + --ignore test/test_hypothesis/test_zset.py \ + --ignore test/test_hypothesis/test_joint.py \ + --ignore test/test_hypothesis/test_transaction.py \ --ignore test/test_mixins/test_bitmap_commands.py \ --junit-xml=results-tests.xml --html=report-tests.html -v continue-on-error: true # For now to mark the flow as successful diff --git a/tests/fakeredis/poetry.lock b/tests/fakeredis/poetry.lock index 77a710918..9d7edc93d 100644 --- a/tests/fakeredis/poetry.lock +++ b/tests/fakeredis/poetry.lock @@ -1,31 +1,31 @@ -# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. [[package]] name = "async-timeout" -version = "4.0.3" +version = "5.0.1" description = "Timeout context manager for asyncio programs" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, - {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, + {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, + {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, ] [[package]] name = "attrs" -version = "24.2.0" +version = "24.3.0" description = "Classes Without Boilerplate" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, - {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, + {file = "attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"}, + {file = "attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff"}, ] [package.extras] benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] @@ -43,73 +43,73 @@ files = [ [[package]] name = "coverage" -version = "7.6.4" +version = "7.6.10" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" files = [ - {file = "coverage-7.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07"}, - {file = "coverage-7.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0"}, - {file = "coverage-7.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7c8b95bf47db6d19096a5e052ffca0a05f335bc63cef281a6e8fe864d450a72"}, - {file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ed9281d1b52628e81393f5eaee24a45cbd64965f41857559c2b7ff19385df51"}, - {file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0809082ee480bb8f7416507538243c8863ac74fd8a5d2485c46f0f7499f2b491"}, - {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d541423cdd416b78626b55f123412fcf979d22a2c39fce251b350de38c15c15b"}, - {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58809e238a8a12a625c70450b48e8767cff9eb67c62e6154a642b21ddf79baea"}, - {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c9b8e184898ed014884ca84c70562b4a82cbc63b044d366fedc68bc2b2f3394a"}, - {file = "coverage-7.6.4-cp310-cp310-win32.whl", hash = "sha256:6bd818b7ea14bc6e1f06e241e8234508b21edf1b242d49831831a9450e2f35fa"}, - {file = "coverage-7.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:06babbb8f4e74b063dbaeb74ad68dfce9186c595a15f11f5d5683f748fa1d172"}, - {file = "coverage-7.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b"}, - {file = "coverage-7.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25"}, - {file = "coverage-7.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546"}, - {file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b"}, - {file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e"}, - {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718"}, - {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db"}, - {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522"}, - {file = "coverage-7.6.4-cp311-cp311-win32.whl", hash = "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf"}, - {file = "coverage-7.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19"}, - {file = "coverage-7.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2"}, - {file = "coverage-7.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117"}, - {file = "coverage-7.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613"}, - {file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27"}, - {file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52"}, - {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2"}, - {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1"}, - {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5"}, - {file = "coverage-7.6.4-cp312-cp312-win32.whl", hash = "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17"}, - {file = "coverage-7.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08"}, - {file = "coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9"}, - {file = "coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba"}, - {file = "coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c"}, - {file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06"}, - {file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f"}, - {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b"}, - {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21"}, - {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a"}, - {file = "coverage-7.6.4-cp313-cp313-win32.whl", hash = "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e"}, - {file = "coverage-7.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963"}, - {file = "coverage-7.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f"}, - {file = "coverage-7.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806"}, - {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11"}, - {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3"}, - {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a"}, - {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc"}, - {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70"}, - {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef"}, - {file = "coverage-7.6.4-cp313-cp313t-win32.whl", hash = "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e"}, - {file = "coverage-7.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1"}, - {file = "coverage-7.6.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9cb7fa111d21a6b55cbf633039f7bc2749e74932e3aa7cb7333f675a58a58bf3"}, - {file = "coverage-7.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11a223a14e91a4693d2d0755c7a043db43d96a7450b4f356d506c2562c48642c"}, - {file = "coverage-7.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a413a096c4cbac202433c850ee43fa326d2e871b24554da8327b01632673a076"}, - {file = "coverage-7.6.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00a1d69c112ff5149cabe60d2e2ee948752c975d95f1e1096742e6077affd376"}, - {file = "coverage-7.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f76846299ba5c54d12c91d776d9605ae33f8ae2b9d1d3c3703cf2db1a67f2c0"}, - {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fe439416eb6380de434886b00c859304338f8b19f6f54811984f3420a2e03858"}, - {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0294ca37f1ba500667b1aef631e48d875ced93ad5e06fa665a3295bdd1d95111"}, - {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6f01ba56b1c0e9d149f9ac85a2f999724895229eb36bd997b61e62999e9b0901"}, - {file = "coverage-7.6.4-cp39-cp39-win32.whl", hash = "sha256:bc66f0bf1d7730a17430a50163bb264ba9ded56739112368ba985ddaa9c3bd09"}, - {file = "coverage-7.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:c481b47f6b5845064c65a7bc78bc0860e635a9b055af0df46fdf1c58cebf8e8f"}, - {file = "coverage-7.6.4-pp39.pp310-none-any.whl", hash = "sha256:3c65d37f3a9ebb703e710befdc489a38683a5b152242664b973a7b7b22348a4e"}, - {file = "coverage-7.6.4.tar.gz", hash = "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73"}, + {file = "coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78"}, + {file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5"}, + {file = "coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244"}, + {file = "coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e"}, + {file = "coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3"}, + {file = "coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377"}, + {file = "coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8"}, + {file = "coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609"}, + {file = "coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853"}, + {file = "coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852"}, + {file = "coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359"}, + {file = "coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247"}, + {file = "coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9"}, + {file = "coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694"}, + {file = "coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6"}, + {file = "coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e"}, + {file = "coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe"}, + {file = "coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2"}, + {file = "coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312"}, + {file = "coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d"}, + {file = "coverage-7.6.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a"}, + {file = "coverage-7.6.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27"}, + {file = "coverage-7.6.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4"}, + {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f"}, + {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25"}, + {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315"}, + {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90"}, + {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d"}, + {file = "coverage-7.6.10-cp39-cp39-win32.whl", hash = "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18"}, + {file = "coverage-7.6.10-cp39-cp39-win_amd64.whl", hash = "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59"}, + {file = "coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f"}, + {file = "coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23"}, ] [package.dependencies] @@ -134,13 +134,13 @@ test = ["pytest (>=6)"] [[package]] name = "fakeredis" -version = "2.26.1" +version = "2.26.2" description = "Python implementation of redis API, can be used for testing purposes." optional = false python-versions = "<4.0,>=3.7" files = [ - {file = "fakeredis-2.26.1-py3-none-any.whl", hash = "sha256:68a5615d7ef2529094d6958677e30a6d30d544e203a5ab852985c19d7ad57e32"}, - {file = "fakeredis-2.26.1.tar.gz", hash = "sha256:69f4daafe763c8014a6dbf44a17559c46643c95447b3594b3975251a171b806d"}, + {file = "fakeredis-2.26.2-py3-none-any.whl", hash = "sha256:86d4129df001efc25793cb334008160fccc98425d9f94de47884a92b63988c14"}, + {file = "fakeredis-2.26.2.tar.gz", hash = "sha256:3ee5003a314954032b96b1365290541346c9cc24aab071b52cc983bb99ecafbf"}, ] [package.dependencies] @@ -160,13 +160,13 @@ probabilistic = ["pyprobables (>=0.6,<0.7)"] [[package]] name = "hypothesis" -version = "6.115.5" +version = "6.123.2" description = "A library for property-based testing" optional = false python-versions = ">=3.9" files = [ - {file = "hypothesis-6.115.5-py3-none-any.whl", hash = "sha256:b7733459ae9a93020fac3b91b41473c9b85e975139a152a70d88f3a5caa3fa3f"}, - {file = "hypothesis-6.115.5.tar.gz", hash = "sha256:4768c5fb426b305462ed31032d6e216a31daaefb1dc3134fdf2795b7961d7cb3"}, + {file = "hypothesis-6.123.2-py3-none-any.whl", hash = "sha256:0a8bf07753f1436f1b8697a13ea955f3fef3ef7b477c2972869b1d142bcdb30e"}, + {file = "hypothesis-6.123.2.tar.gz", hash = "sha256:02c25552783764146b191c69eef69d8375827b58a75074055705ab8fdbc95fc5"}, ] [package.dependencies] @@ -175,10 +175,10 @@ exceptiongroup = {version = ">=1.0.0", markers = "python_version < \"3.11\""} sortedcontainers = ">=2.1.0,<3.0.0" [package.extras] -all = ["black (>=19.10b0)", "click (>=7.0)", "crosshair-tool (>=0.0.74)", "django (>=4.2)", "dpcontracts (>=0.4)", "hypothesis-crosshair (>=0.0.16)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.19.3)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2024.2)"] +all = ["black (>=19.10b0)", "click (>=7.0)", "crosshair-tool (>=0.0.78)", "django (>=4.2)", "dpcontracts (>=0.4)", "hypothesis-crosshair (>=0.0.18)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.19.3)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2024.2)"] cli = ["black (>=19.10b0)", "click (>=7.0)", "rich (>=9.0.0)"] codemods = ["libcst (>=0.3.16)"] -crosshair = ["crosshair-tool (>=0.0.74)", "hypothesis-crosshair (>=0.0.16)"] +crosshair = ["crosshair-tool (>=0.0.78)", "hypothesis-crosshair (>=0.0.18)"] dateutil = ["python-dateutil (>=1.4)"] django = ["django (>=4.2)"] dpcontracts = ["dpcontracts (>=0.4)"] @@ -204,13 +204,13 @@ files = [ [[package]] name = "jinja2" -version = "3.1.4" +version = "3.1.5" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ - {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, - {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, + {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, + {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, ] [package.dependencies] @@ -227,6 +227,8 @@ optional = false python-versions = "*" files = [ {file = "jsonpath-ng-1.7.0.tar.gz", hash = "sha256:f6f5f7fd4e5ff79c785f1573b394043b39849fb2bb47bcead935d12b00beab3c"}, + {file = "jsonpath_ng-1.7.0-py2-none-any.whl", hash = "sha256:898c93fc173f0c336784a3fa63d7434297544b7198124a68f9a3ef9597b0ae6e"}, + {file = "jsonpath_ng-1.7.0-py3-none-any.whl", hash = "sha256:f3d7f9e848cba1b6da28c55b1c26ff915dc9e0b1ba7e752a53d6da8d5cbd00b6"}, ] [package.dependencies] @@ -399,13 +401,13 @@ files = [ [[package]] name = "packaging" -version = "24.1" +version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] [[package]] @@ -436,24 +438,24 @@ files = [ [[package]] name = "pyprobables" -version = "0.6.0" +version = "0.6.1" description = "Probabilistic data structures in python" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "pyprobables-0.6.0-py3-none-any.whl", hash = "sha256:85a23655e2d5e87a99e01003be8b48e9dfcc0c45e65c02f84f777b99235347db"}, - {file = "pyprobables-0.6.0.tar.gz", hash = "sha256:a4e72bdb4d3513121b33377728c9eafd2ae8495d5201d6a90abc3d52d9a17901"}, + {file = "pyprobables-0.6.1-py3-none-any.whl", hash = "sha256:090d0c973f9e160f15927e8eb911dabf126285a7a1ecd478b7a9e04149e28392"}, + {file = "pyprobables-0.6.1.tar.gz", hash = "sha256:64b4d165d51beff05e716c01231c8a5503297844e58adee8771e5e7af130321d"}, ] [[package]] name = "pytest" -version = "8.3.3" +version = "8.3.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, - {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, + {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, ] [package.dependencies] @@ -573,13 +575,13 @@ pytest = ">=7.0.0" [[package]] name = "redis" -version = "5.2.0" +version = "5.2.1" description = "Python client for Redis database and key-value store" optional = false python-versions = ">=3.8" files = [ - {file = "redis-5.2.0-py3-none-any.whl", hash = "sha256:ae174f2bb3b1bf2b09d54bf3e51fbc1469cf6c10aa03e21141f51969801a7897"}, - {file = "redis-5.2.0.tar.gz", hash = "sha256:0b1087665a771b1ff2e003aa5bdd354f15a70c9e25d5a7dbf9c722c16528a7b0"}, + {file = "redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4"}, + {file = "redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f"}, ] [package.dependencies] @@ -602,13 +604,43 @@ files = [ [[package]] name = "tomli" -version = "2.0.2" +version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" files = [ - {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, - {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] [[package]] diff --git a/tests/fakeredis/test/test_hypothesis.py b/tests/fakeredis/test/test_hypothesis.py deleted file mode 100644 index 79e2e8fb8..000000000 --- a/tests/fakeredis/test/test_hypothesis.py +++ /dev/null @@ -1,842 +0,0 @@ -import functools -import math -import operator -import sys -from typing import Any, Tuple, Union - -import hypothesis -import hypothesis.stateful -import hypothesis.strategies as st -import pytest -import redis -from hypothesis.stateful import rule, initialize, precondition -from hypothesis.strategies import SearchStrategy - -import fakeredis -from fakeredis._server import _create_version - -self_strategy = st.runner() - - -@st.composite -def sample_attr(draw, name): - """Strategy for sampling a specific attribute from a state machine""" - machine = draw(self_strategy) - values = getattr(machine, name) - position = draw(st.integers(min_value=0, max_value=len(values) - 1)) - return values[position] - - -def server_info() -> Tuple[str, Union[None, Tuple[int, ...]]]: - """Returns server's version or None if server is not running""" - client = None - try: - client = redis.Redis("localhost", port=6380, db=2) - client_info = client.info() - server_type = "dragonfly" if "dragonfly_version" in client_info else "redis" - server_version = ( - client_info["redis_version"] if server_type != "dragonfly" else (7, 0) - ) - server_version = _create_version(server_version) or (7,) - return server_type, server_version - except redis.ConnectionError as e: - print(e) - pytest.exit("Redis is not running") - return "redis", (6,) - finally: - if hasattr(client, "close"): - client.close() # Absent in older versions of redis-py - - -server_type, redis_ver = server_info() - -keys = sample_attr("keys") -fields = sample_attr("fields") -values = sample_attr("values") -scores = sample_attr("scores") - -int_as_bytes = st.builds(lambda x: str(default_normalize(x)).encode(), st.integers()) -float_as_bytes = st.builds( - lambda x: repr(default_normalize(x)).encode(), st.floats(width=32) -) -counts = st.integers(min_value=-3, max_value=3) | st.integers() -limits = st.just(()) | st.tuples(st.just("limit"), counts, counts) -# Redis has an integer overflow bug in swapdb, so we confine the numbers to -# a limited range (https://github.com/antirez/redis/issues/5737). -dbnums = st.integers(min_value=0, max_value=3) | st.integers( - min_value=-1000, max_value=1000 -) -# The filter is to work around https://github.com/antirez/redis/issues/5632 -patterns = st.text( - alphabet=st.sampled_from("[]^$*.?-azAZ\\\r\n\t") -) | st.binary().filter(lambda x: b"\0" not in x) -score_tests = scores | st.builds(lambda x: b"(" + repr(x).encode(), scores) -string_tests = st.sampled_from([b"+", b"-"]) | st.builds( - operator.add, st.sampled_from([b"(", b"["]), fields -) -# Redis has integer overflow bugs in time computations, which is why we set a maximum. -expires_seconds = st.integers(min_value=100000, max_value=10000000000) -expires_ms = st.integers(min_value=100000000, max_value=10000000000000) - - -class WrappedException: - """Wraps an exception for the purposes of comparison.""" - - def __init__(self, exc): - self.wrapped = exc - - def __str__(self): - return str(self.wrapped) - - def __repr__(self): - return "WrappedException({!r})".format(self.wrapped) - - def __eq__(self, other): - if not isinstance(other, WrappedException): - return NotImplemented - if type(self.wrapped) != type(other.wrapped): # noqa: E721 - return False - return True - # return self.wrapped.args == other.wrapped.args - - def __ne__(self, other): - if not isinstance(other, WrappedException): - return NotImplemented - return not self == other - - -def wrap_exceptions(obj): - if isinstance(obj, list): - return [wrap_exceptions(item) for item in obj] - elif isinstance(obj, Exception): - return WrappedException(obj) - else: - return obj - - -def sort_list(lst): - if isinstance(lst, list): - return sorted(lst) - else: - return lst - - -def normalize_if_number(x): - try: - res = float(x) - return x if math.isnan(res) else res - except ValueError: - return x - - -def flatten(args): - if isinstance(args, (list, tuple)): - for arg in args: - yield from flatten(arg) - elif args is not None: - yield args - - -def default_normalize(x: Any) -> Any: - if redis_ver >= (7,) and (isinstance(x, float) or isinstance(x, int)): - return 0 + x - - return x - - -class Command: - def __init__(self, *args): - args = list(flatten(args)) - args = [default_normalize(x) for x in args] - self.args = tuple(args) - - def __repr__(self): - parts = [repr(arg) for arg in self.args] - return "Command({})".format(", ".join(parts)) - - @staticmethod - def encode(arg): - encoder = redis.connection.Encoder("utf-8", "replace", False) - return encoder.encode(arg) - - @property - def normalize(self): - command = self.encode(self.args[0]).lower() if self.args else None - # Functions that return a list in arbitrary order - unordered = { - b"keys", - b"sort", - b"hgetall", - b"hkeys", - b"hvals", - b"sdiff", - b"sinter", - b"sunion", - b"smembers", - } - if command in unordered: - return sort_list - else: - return normalize_if_number - - @property - def testable(self): - """Whether this command is suitable for a test. - - The fuzzer can create commands with behaviour that is - non-deterministic, not supported, or which hits redis bugs. - """ - N = len(self.args) - if N == 0: - return False - command = self.encode(self.args[0]).lower() - if not command.split(): - return False - if command == b"keys" and N == 2 and self.args[1] != b"*": - return False - # Redis will ignore a NULL character in some commands but not others, - # e.g., it recognises EXEC\0 but not MULTI\00. - # Rather than try to reproduce this quirky behavior, just skip these tests. - if b"\0" in command: - return False - return True - - -def commands(*args, **kwargs): - return st.builds(functools.partial(Command, **kwargs), *args) - - -# # TODO: all expiry-related commands -common_commands = ( - commands(st.sampled_from(["del", "persist", "type", "unlink"]), keys) - | commands(st.just("exists"), st.lists(keys)) - | commands(st.just("keys"), st.just("*")) - # Disabled for now due to redis giving wrong answers - # (https://github.com/antirez/redis/issues/5632) - # | commands(st.just('keys'), patterns) - | commands(st.just("move"), keys, dbnums) - | commands(st.sampled_from(["rename", "renamenx"]), keys, keys) - # TODO: find a better solution to sort instability than throwing - # away the sort entirely with normalize. This also prevents us - # using LIMIT. - | commands( - st.just("sort"), - keys, - st.none() | st.just("asc"), - st.none() | st.just("desc"), - st.none() | st.just("alpha"), - ) -) - - -def build_zstore(command, dest, sources, weights, aggregate): - args = [command, dest, len(sources)] - args += [source[0] for source in sources] - if weights: - args.append("weights") - args += [source[1] for source in sources] - if aggregate: - args += ["aggregate", aggregate] - return Command(args) - - -zset_no_score_create_commands = commands( - st.just("zadd"), keys, st.lists(st.tuples(st.just(0), fields), min_size=1) -) -zset_no_score_commands = ( # TODO: test incr - commands( - st.just("zadd"), - keys, - st.none() | st.just("nx"), - st.none() | st.just("xx"), - st.none() | st.just("ch"), - st.none() | st.just("incr"), - st.lists(st.tuples(st.just(0), fields)), - ) - | commands(st.just("zlexcount"), keys, string_tests, string_tests) - | commands( - st.sampled_from(["zrangebylex", "zrevrangebylex"]), - keys, - string_tests, - string_tests, - limits, - ) - | commands(st.just("zremrangebylex"), keys, string_tests, string_tests) -) - -bad_commands = ( - # redis-py splits the command on spaces, and hangs if that ends up - # being an empty list - commands( - st.text().filter(lambda x: bool(x.split())), st.lists(st.binary() | st.text()) - ) -) - -attrs = st.fixed_dictionaries( - { - "keys": st.lists(st.binary(), min_size=2, max_size=5, unique=True), - "fields": st.lists(st.binary(), min_size=2, max_size=5, unique=True), - "values": st.lists( - st.binary() | int_as_bytes | float_as_bytes, - min_size=2, - max_size=5, - unique=True, - ), - "scores": st.lists(st.floats(width=32), min_size=2, max_size=5, unique=True), - } -) - - -@hypothesis.settings(max_examples=1000) -class CommonMachine(hypothesis.stateful.RuleBasedStateMachine): - create_command_strategy = st.nothing() - - def __init__(self): - super().__init__() - try: - self.real = redis.StrictRedis("localhost", port=6380, db=2) - self.real.ping() - except redis.ConnectionError: - pytest.skip("redis is not running") - if self.real.info("server").get("arch_bits") != 64: - self.real.connection_pool.disconnect() - pytest.skip("redis server is not 64-bit") - self.fake = fakeredis.FakeStrictRedis( - server=fakeredis.FakeServer(version=redis_ver), port=6380, db=2 - ) - # Disable the response parsing so that we can check the raw values returned - self.fake.response_callbacks.clear() - self.real.response_callbacks.clear() - self.transaction_normalize = [] - self.keys = [] - self.fields = [] - self.values = [] - self.scores = [] - self.initialized_data = False - try: - self.real.execute_command("discard") - except redis.ResponseError: - pass - self.real.flushall() - - def teardown(self): - self.real.connection_pool.disconnect() - self.fake.connection_pool.disconnect() - super().teardown() - - @staticmethod - def _evaluate(client, command): - try: - result = client.execute_command(*command.args) - if result != "QUEUED": - result = command.normalize(result) - exc = None - except Exception as e: - result = exc = e - return wrap_exceptions(result), exc - - def _compare(self, command): - fake_result, fake_exc = self._evaluate(self.fake, command) - real_result, real_exc = self._evaluate(self.real, command) - - if fake_exc is not None and real_exc is None: - print( - "{} raised on only on fake when running {}".format(fake_exc, command), - file=sys.stderr, - ) - raise fake_exc - elif real_exc is not None and fake_exc is None: - assert real_exc == fake_exc, "Expected exception {} not raised".format( - real_exc - ) - elif ( - real_exc is None - and isinstance(real_result, list) - and command.args - and command.args[0].lower() == "exec" - ): - assert fake_result is not None - # Transactions need to use the normalize functions of the - # component commands. - assert len(self.transaction_normalize) == len(real_result) - assert len(self.transaction_normalize) == len(fake_result) - for n, r, f in zip(self.transaction_normalize, real_result, fake_result): - assert n(f) == n(r) - self.transaction_normalize = [] - else: - assert fake_result == real_result or ( - type(fake_result) is float and fake_result == pytest.approx(real_result) - ), "Discrepancy when running command {}, fake({}) != real({})".format( - command, fake_result, real_result - ) - if real_result == b"QUEUED": - # Since redis removes the distinction between simple strings and - # bulk strings, this might not actually indicate that we're in a - # transaction. But it is extremely unlikely that hypothesis will - # find such examples. - self.transaction_normalize.append(command.normalize) - if len(command.args) == 1 and Command.encode(command.args[0]).lower() in ( - b"discard", - b"exec", - ): - self.transaction_normalize = [] - - @initialize(attrs=attrs) - def init_attrs(self, attrs): - for key, value in attrs.items(): - setattr(self, key, value) - - # hypothesis doesn't allow ordering of @initialize, so we have to put - # preconditions on rules to ensure we call init_data exactly once and - # after init_attrs. - @precondition(lambda self: not self.initialized_data) - @rule( - commands=self_strategy.flatmap( - lambda self: st.lists(self.create_command_strategy) - ) - ) - def init_data(self, commands): - for command in commands: - self._compare(command) - self.initialized_data = True - - @precondition(lambda self: self.initialized_data) - @rule(command=self_strategy.flatmap(lambda self: self.command_strategy)) - def one_command(self, command): - self._compare(command) - - -class BaseTest: - """Base class for test classes.""" - - command_strategy: SearchStrategy - create_command_strategy = st.nothing() - command_strategy_redis7 = st.nothing() - - @pytest.mark.slow - def test(self): - class Machine(CommonMachine): - create_command_strategy = self.create_command_strategy - command_strategy = ( - self.command_strategy | self.command_strategy_redis7 - if redis_ver >= (7,) - else self.command_strategy - ) - - hypothesis.settings.register_profile( - "debug", max_examples=10, verbosity=hypothesis.Verbosity.debug - ) - hypothesis.settings.load_profile("debug") - hypothesis.stateful.run_state_machine_as_test(Machine) - - -class TestConnection(BaseTest): - # TODO: tests for select - connection_commands = ( - commands(st.just("echo"), values) - | commands(st.just("ping"), st.lists(values, max_size=2)) - | commands(st.just("swapdb"), dbnums, dbnums) - ) - command_strategy = connection_commands | common_commands - - -class TestString(BaseTest): - string_commands = ( - commands(st.just("append"), keys, values) - | commands(st.just("bitcount"), keys) - | commands(st.just("bitcount"), keys, values, values) - | commands(st.sampled_from(["incr", "decr"]), keys) - | commands(st.sampled_from(["incrby", "decrby"]), keys, values) - | commands(st.just("get"), keys) - | commands(st.just("getbit"), keys, counts) - | commands( - st.just("setbit"), - keys, - counts, - st.integers(min_value=0, max_value=1) | st.integers(), - ) - | commands(st.sampled_from(["substr", "getrange"]), keys, counts, counts) - | commands(st.just("getset"), keys, values) - | commands(st.just("mget"), st.lists(keys)) - | commands( - st.sampled_from(["mset", "msetnx"]), st.lists(st.tuples(keys, values)) - ) - | commands( - st.just("set"), - keys, - values, - st.none() | st.just("nx"), - st.none() | st.just("xx"), - st.none() | st.just("keepttl"), - ) - | commands(st.just("setex"), keys, expires_seconds, values) - | commands(st.just("psetex"), keys, expires_ms, values) - | commands(st.just("setnx"), keys, values) - | commands(st.just("setrange"), keys, counts, values) - | commands(st.just("strlen"), keys) - ) - create_command_strategy = commands(st.just("set"), keys, values) - command_strategy = string_commands | common_commands - - -class TestHash(BaseTest): - hash_commands = ( - commands(st.just("hset"), keys, st.lists(st.tuples(fields, values))) - | commands(st.just("hdel"), keys, st.lists(fields)) - | commands(st.just("hexists"), keys, fields) - | commands(st.just("hget"), keys, fields) - | commands(st.sampled_from(["hgetall", "hkeys", "hvals"]), keys) - | commands(st.just("hincrby"), keys, fields, st.integers()) - | commands(st.just("hlen"), keys) - | commands(st.just("hmget"), keys, st.lists(fields)) - | commands(st.just("hset"), keys, st.lists(st.tuples(fields, values))) - | commands(st.just("hsetnx"), keys, fields, values) - | commands(st.just("hstrlen"), keys, fields) - ) - command_strategy_redis7 = ( - commands( - st.just("hpersist"), - st.just("fields"), - st.just(2), - st.lists(fields, min_size=2, max_size=2), - ) - | commands( - st.just("hexpiretime"), - st.just("fields"), - st.just(2), - st.lists(fields, min_size=2, max_size=2), - ) - | commands( - st.just("hpexpiretime"), - st.just("fields"), - st.just(2), - st.lists(fields, min_size=2, max_size=2), - ) - | commands( - st.just("hexpire"), - keys, - expires_seconds, - st.none() | st.just("nx"), - st.none() | st.just("xx"), - st.none() | st.just("gt"), - st.none() | st.just("lt"), - st.just("fields"), - st.just(2), - st.lists(fields, min_size=2, max_size=2), - ) - | commands( - st.just("hpexpire"), - keys, - expires_ms, - st.none() | st.just("nx"), - st.none() | st.just("xx"), - st.none() | st.just("gt"), - st.none() | st.just("lt"), - st.just("fields"), - st.just(2), - st.lists(fields, min_size=2, max_size=2), - ) - ) - create_command_strategy = commands( - st.just("hset"), keys, st.lists(st.tuples(fields, values), min_size=1) - ) - command_strategy = hash_commands | common_commands - - -class TestList(BaseTest): - # TODO: blocking commands - list_commands = ( - commands(st.just("lindex"), keys, counts) - | commands( - st.just("linsert"), - keys, - st.sampled_from(["before", "after", "BEFORE", "AFTER"]) | st.binary(), - values, - values, - ) - | commands(st.just("llen"), keys) - | commands( - st.sampled_from(["lpop", "rpop"]), - keys, - st.just(None) | st.just([]) | st.integers(), - ) - | commands( - st.sampled_from(["lpush", "lpushx", "rpush", "rpushx"]), - keys, - st.lists(values), - ) - | commands(st.just("lrange"), keys, counts, counts) - | commands(st.just("lrem"), keys, counts, values) - | commands(st.just("lset"), keys, counts, values) - | commands(st.just("ltrim"), keys, counts, counts) - | commands(st.just("rpoplpush"), keys, keys) - ) - create_command_strategy = commands( - st.just("rpush"), keys, st.lists(values, min_size=1) - ) - command_strategy = list_commands | common_commands - - -class TestSet(BaseTest): - set_commands = ( - commands( - st.just("sadd"), - keys, - st.lists( - fields, - ), - ) - | commands(st.just("scard"), keys) - | commands(st.sampled_from(["sdiff", "sinter", "sunion"]), st.lists(keys)) - | commands( - st.sampled_from(["sdiffstore", "sinterstore", "sunionstore"]), - keys, - st.lists(keys), - ) - | commands(st.just("sismember"), keys, fields) - | commands(st.just("smembers"), keys) - | commands(st.just("smove"), keys, keys, fields) - | commands(st.just("srem"), keys, st.lists(fields)) - ) - # TODO: - # - find a way to test srandmember, spop which are random - # - sscan - create_command_strategy = commands( - st.just("sadd"), keys, st.lists(fields, min_size=1) - ) - command_strategy = set_commands | common_commands - - -class TestZSet(BaseTest): - zset_commands = ( - commands( - st.just("zadd"), - keys, - st.none() | st.just("nx"), - st.none() | st.just("xx"), - st.none() | st.just("ch"), - st.none() | st.just("incr"), - st.lists(st.tuples(scores, fields)), - ) - | commands(st.just("zcard"), keys) - | commands(st.just("zcount"), keys, score_tests, score_tests) - | commands(st.just("zincrby"), keys, scores, fields) - | commands( - st.sampled_from(["zrange", "zrevrange"]), - keys, - counts, - counts, - st.none() | st.just("withscores"), - ) - | commands( - st.sampled_from(["zrangebyscore", "zrevrangebyscore"]), - keys, - score_tests, - score_tests, - limits, - st.none() | st.just("withscores"), - ) - | commands(st.sampled_from(["zrank", "zrevrank"]), keys, fields) - | commands(st.just("zrem"), keys, st.lists(fields)) - | commands(st.just("zremrangebyrank"), keys, counts, counts) - | commands(st.just("zremrangebyscore"), keys, score_tests, score_tests) - | commands(st.just("zscore"), keys, fields) - | st.builds( - build_zstore, - command=st.sampled_from(["zunionstore", "zinterstore"]), - dest=keys, - sources=st.lists(st.tuples(keys, float_as_bytes)), - weights=st.booleans(), - aggregate=st.sampled_from([None, "sum", "min", "max"]), - ) - ) - # TODO: zscan, zpopmin/zpopmax, bzpopmin/bzpopmax, probably more - create_command_strategy = commands( - st.just("zadd"), keys, st.lists(st.tuples(scores, fields), min_size=1) - ) - command_strategy = zset_commands | common_commands - - -class TestZSetNoScores(BaseTest): - create_command_strategy = zset_no_score_create_commands - command_strategy = zset_no_score_commands | common_commands - - -class TestTransaction(BaseTest): - transaction_commands = ( - commands(st.sampled_from(["multi", "discard", "exec", "unwatch"])) - | commands(st.just("watch"), keys) - | commands(st.just("append"), keys, values) - | commands(st.just("bitcount"), keys) - | commands(st.just("bitcount"), keys, values, values) - | commands(st.sampled_from(["incr", "decr"]), keys) - | commands(st.sampled_from(["incrby", "decrby"]), keys, values) - | commands(st.just("get"), keys) - | commands(st.just("getbit"), keys, counts) - | commands( - st.just("setbit"), - keys, - counts, - st.integers(min_value=0, max_value=1) | st.integers(), - ) - | commands(st.sampled_from(["substr", "getrange"]), keys, counts, counts) - | commands(st.just("getset"), keys, values) - | commands(st.just("mget"), st.lists(keys)) - | commands( - st.sampled_from(["mset", "msetnx"]), st.lists(st.tuples(keys, values)) - ) - | commands( - st.just("set"), - keys, - values, - st.none() | st.just("nx"), - st.none() | st.just("xx"), - st.none() | st.just("keepttl"), - ) - | commands(st.just("setex"), keys, expires_seconds, values) - | commands(st.just("psetex"), keys, expires_ms, values) - | commands(st.just("setnx"), keys, values) - | commands(st.just("setrange"), keys, counts, values) - | commands(st.just("strlen"), keys) - ) - create_command_strategy = TestString.create_command_strategy - command_strategy = transaction_commands | common_commands - - -class TestServer(BaseTest): - # TODO: real redis raises an error if there is a save already in progress. - # Find a better way to test this. commands(st.just('bgsave')) - server_commands = ( - commands(st.just("dbsize")) - | commands( - st.sampled_from(["flushdb", "flushall"]), st.sampled_from([[], "async"]) - ) - # TODO: result is non-deterministic - # | commands(st.just('lastsave')) - | commands(st.just("save")) - ) - create_command_strategy = TestString.create_command_strategy - command_strategy = server_commands | TestString.string_commands | common_commands - - -class TestJoint(BaseTest): - create_command_strategy = ( - TestString.create_command_strategy - | TestHash.create_command_strategy - | TestList.create_command_strategy - | TestSet.create_command_strategy - | TestZSet.create_command_strategy - ) - command_strategy = ( - TestServer.server_commands - | TestConnection.connection_commands - | TestString.string_commands - | TestHash.hash_commands - | TestList.list_commands - | TestSet.set_commands - | TestZSet.zset_commands - | common_commands - | bad_commands - ) - - -@st.composite -def delete_arg(draw, commands): - command = draw(commands) - if command.args: - pos = draw(st.integers(min_value=0, max_value=len(command.args) - 1)) - command.args = command.args[:pos] + command.args[pos + 1 :] - return command - - -@st.composite -def command_args(draw, commands): - """Generate an argument from some command""" - command = draw(commands) - hypothesis.assume(len(command.args)) - return draw(st.sampled_from(command.args)) - - -def mutate_arg(draw, commands, mutate): - command = draw(commands) - if command.args: - pos = draw(st.integers(min_value=0, max_value=len(command.args) - 1)) - arg = mutate(Command.encode(command.args[pos])) - command.args = command.args[:pos] + (arg,) + command.args[pos + 1 :] - return command - - -@st.composite -def replace_arg(draw, commands, replacements): - return mutate_arg(draw, commands, lambda arg: draw(replacements)) - - -@st.composite -def uppercase_arg(draw, commands): - return mutate_arg(draw, commands, lambda arg: arg.upper()) - - -@st.composite -def prefix_arg(draw, commands, prefixes): - return mutate_arg(draw, commands, lambda arg: draw(prefixes) + arg) - - -@st.composite -def suffix_arg(draw, commands, suffixes): - return mutate_arg(draw, commands, lambda arg: arg + draw(suffixes)) - - -@st.composite -def add_arg(draw, commands, arguments): - command = draw(commands) - arg = draw(arguments) - pos = draw(st.integers(min_value=0, max_value=len(command.args))) - command.args = command.args[:pos] + (arg,) + command.args[pos:] - return command - - -@st.composite -def swap_args(draw, commands): - command = draw(commands) - if len(command.args) >= 2: - pos1 = draw(st.integers(min_value=0, max_value=len(command.args) - 1)) - pos2 = draw(st.integers(min_value=0, max_value=len(command.args) - 1)) - hypothesis.assume(pos1 != pos2) - args = list(command.args) - arg1 = args[pos1] - arg2 = args[pos2] - args[pos1] = arg2 - args[pos2] = arg1 - command.args = tuple(args) - return command - - -def mutated_commands(commands): - args = st.sampled_from( - [ - b"withscores", - b"xx", - b"nx", - b"ex", - b"px", - b"weights", - b"aggregate", - b"", - b"0", - b"-1", - b"nan", - b"inf", - b"-inf", - ] - ) | command_args(commands) - affixes = st.sampled_from([b"\0", b"-", b"+", b"\t", b"\n", b"0000"]) | st.binary() - return st.recursive( - commands, - lambda x: delete_arg(x) - | replace_arg(x, args) - | uppercase_arg(x) - | prefix_arg(x, affixes) - | suffix_arg(x, affixes) - | add_arg(x, args) - | swap_args(x), - ) diff --git a/tests/fakeredis/test/test_hypothesis/__init__.py b/tests/fakeredis/test/test_hypothesis/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fakeredis/test/test_hypothesis/_server_info.py b/tests/fakeredis/test/test_hypothesis/_server_info.py new file mode 100644 index 000000000..de17e4042 --- /dev/null +++ b/tests/fakeredis/test/test_hypothesis/_server_info.py @@ -0,0 +1,25 @@ +from typing import Tuple, Union + +import pytest +import redis + + +def server_info() -> Tuple[str, Union[None, Tuple[int, ...]]]: + """Returns server's version or None if server is not running""" + client = None + try: + client = redis.Redis("localhost", port=6380, db=2) + client_info = client.info() + server_type = "dragonfly" if "dragonfly_version" in client_info else "redis" + server_version = (7, 0) + return server_type, server_version + except redis.ConnectionError as e: + print(e) + pytest.exit("Redis is not running") + return "redis", (6,) + finally: + if hasattr(client, "close"): + client.close() # Absent in older versions of redis-py + + +server_type, redis_ver = server_info() diff --git a/tests/fakeredis/test/test_hypothesis/base.py b/tests/fakeredis/test/test_hypothesis/base.py new file mode 100644 index 000000000..4ed93c6d2 --- /dev/null +++ b/tests/fakeredis/test/test_hypothesis/base.py @@ -0,0 +1,354 @@ +import functools +import math +import sys +from typing import Any, List, Tuple, Type, Optional + +import fakeredis +import hypothesis +import hypothesis.stateful +import hypothesis.strategies as st +import pytest +import redis +from hypothesis.stateful import rule, initialize, precondition +from hypothesis.strategies import SearchStrategy + +from ._server_info import redis_ver + +self_strategy = st.runner() + +MAX_INT = 2_147_483_647 +MIN_INT = -2_147_483_648 + + +@st.composite +def sample_attr(draw, name): + """Strategy for sampling a specific attribute from a state machine""" + machine = draw(self_strategy) + values = getattr(machine, name) + position = draw(st.integers(min_value=0, max_value=len(values) - 1)) + return values[position] + + +keys = sample_attr("keys") +fields = sample_attr("fields") +values = sample_attr("values") +scores = sample_attr("scores") + +ints = st.integers(min_value=MIN_INT, max_value=MAX_INT) +int_as_bytes = st.builds(lambda x: str(_default_normalize(x)).encode(), ints) +float_as_bytes = st.builds( + lambda x: repr(_default_normalize(x)).encode(), st.floats(width=32) +) +counts = st.integers(min_value=-3, max_value=3) | ints +# Redis has an integer overflow bug in swapdb, so we confine the numbers to +# a limited range (https://github.com/antirez/redis/issues/5737). +dbnums = st.integers(min_value=0, max_value=3) | st.integers( + min_value=-1000, max_value=1000 +) +# The filter is to work around https://github.com/antirez/redis/issues/5632 +patterns = st.text( + alphabet=st.sampled_from("[]^$*.?-azAZ\\\r\n\t") +) | st.binary().filter(lambda x: b"\0" not in x) + +# Redis has integer overflow bugs in time computations, which is why we set a maximum. +expires_seconds = st.integers(min_value=100000, max_value=MAX_INT) +expires_ms = st.integers(min_value=100000000, max_value=MAX_INT) + + +class WrappedException: + """Wraps an exception for the purposes of comparison.""" + + def __init__(self, exc): + self.wrapped = exc + + def __str__(self): + return str(self.wrapped) + + def __repr__(self): + return "WrappedException({!r})".format(self.wrapped) + + def __eq__(self, other): + if not isinstance(other, WrappedException): + return NotImplemented + if type(self.wrapped) != type(other.wrapped): # noqa: E721 + return False + return True + # return self.wrapped.args == other.wrapped.args + + def __ne__(self, other): + if not isinstance(other, WrappedException): + return NotImplemented + return not self == other + + +def _wrap_exceptions(obj): + if isinstance(obj, list): + return [_wrap_exceptions(item) for item in obj] + elif isinstance(obj, Exception): + return WrappedException(obj) + else: + return obj + + +def _sort_list(lst): + if isinstance(lst, list): + return sorted(lst) + else: + return lst + + +def _normalize_if_number(x): + try: + res = float(x) + return x if math.isnan(res) else res + except ValueError: + return x + + +def _flatten(args): + if isinstance(args, (list, tuple)): + for arg in args: + yield from _flatten(arg) + elif args is not None: + yield args + + +def _default_normalize(x: Any) -> Any: + if redis_ver >= (7,) and (isinstance(x, float) or isinstance(x, int)): + return 0 + x + + return x + + +class Command: + def __init__(self, *args): + args = list(_flatten(args)) + args = [_default_normalize(x) for x in args] + self.args = tuple(args) + + def __repr__(self): + parts = [repr(arg) for arg in self.args] + return "Command({})".format(", ".join(parts)) + + @staticmethod + def encode(arg): + encoder = redis.connection.Encoder("utf-8", "replace", False) + return encoder.encode(arg) + + @property + def normalize(self): + command = self.encode(self.args[0]).lower() if self.args else None + # Functions that return a list in arbitrary order + unordered = { + b"keys", + b"sort", + b"hgetall", + b"hkeys", + b"hvals", + b"sdiff", + b"sinter", + b"sunion", + b"smembers", + } + if command in unordered: + return _sort_list + else: + return _normalize_if_number + + @property + def testable(self) -> bool: + """Whether this command is suitable for a test. + + The fuzzer can create commands with behaviour that is non-deterministic, not supported, or which hits redis bugs. + """ + N = len(self.args) + if N == 0: + return False + command = self.encode(self.args[0]).lower() + if not command.split(): + return False + if command == b"keys" and N == 2 and self.args[1] != b"*": + return False + # Redis will ignore a NULL character in some commands but not others, + # e.g., it recognises EXEC\0 but not MULTI\00. + # Rather than try to reproduce this quirky behavior, just skip these tests. + if b"\0" in command: + return False + return True + + +def zero_or_more(*args) -> List[SearchStrategy]: + return [st.none() | st.just(arg) for arg in args] + + +def commands(*args, **kwargs): + return st.builds(functools.partial(Command, **kwargs), *args) + + +# # TODO: all expiry-related commands +common_commands = ( + commands(st.sampled_from(["del", "persist", "type", "unlink"]), keys) + | commands(st.just("exists"), st.lists(keys)) + | commands(st.just("keys"), st.just("*")) + # Disabled for now due to redis giving wrong answers + # (https://github.com/antirez/redis/issues/5632) + # | commands(st.just('keys'), patterns) + | commands(st.just("move"), keys, dbnums) + | commands(st.sampled_from(["rename", "renamenx"]), keys, keys) + # TODO: find a better solution to sort instability than throwing + # away the sort entirely with normalize. This also prevents us + # using LIMIT. + | commands(st.just("sort"), keys, *zero_or_more("asc", "desc", "alpha")) +) + +attrs = st.fixed_dictionaries( + { + "keys": st.lists(st.binary(), min_size=2, max_size=5, unique=True), + "fields": st.lists(st.binary(), min_size=2, max_size=5, unique=True), + "values": st.lists( + st.binary() | int_as_bytes | float_as_bytes, + min_size=2, + max_size=5, + unique=True, + ), + "scores": st.lists(st.floats(width=32), min_size=2, max_size=5, unique=True), + } +) + + +@hypothesis.settings(max_examples=1000) +class CommonMachine(hypothesis.stateful.RuleBasedStateMachine): + create_command_strategy = st.nothing() + + def __init__(self): + super().__init__() + try: + self.real = redis.StrictRedis("localhost", port=6380, db=2) + self.real.ping() + except redis.ConnectionError: + pytest.skip("redis is not running") + if self.real.info("server").get("arch_bits") != 64: + self.real.connection_pool.disconnect() + pytest.skip("redis server is not 64-bit") + self.fake = fakeredis.FakeStrictRedis( + server=fakeredis.FakeServer(version=redis_ver), port=6380, db=2 + ) + # Disable the response parsing so that we can check the raw values returned + self.fake.response_callbacks.clear() + self.real.response_callbacks.clear() + self.transaction_normalize = [] + self.keys = [] + self.fields = [] + self.values = [] + self.scores = [] + self.initialized_data = False + try: + self.real.execute_command("discard") + except redis.ResponseError: + pass + self.real.flushall() + + def teardown(self) -> None: + self.real.connection_pool.disconnect() + self.fake.connection_pool.disconnect() + super().teardown() + + @staticmethod + def _evaluate( + client: redis.Redis, command + ) -> Tuple[Any, Optional[Type[Exception]]]: + try: + result = client.execute_command(*command.args) + if result != "QUEUED": + result = command.normalize(result) + exc = None + except Exception as e: + result = exc = e + return _wrap_exceptions(result), exc + + def _compare(self, command: Command) -> None: + fake_result, fake_exc = self._evaluate(self.fake, command) + real_result, real_exc = self._evaluate(self.real, command) + + if fake_exc is not None and real_exc is None: + print( + f"{fake_exc} raised on only on fake when running {command}", + file=sys.stderr, + ) + raise fake_exc + elif real_exc is not None and fake_exc is None: + assert real_exc == fake_exc, f"Expected exception {real_exc} not raised" + elif ( + real_exc is None + and isinstance(real_result, list) + and command.args + and command.args[0].lower() == "exec" + ): + assert fake_result is not None + # Transactions need to use the normalize functions of the component commands. + assert len(self.transaction_normalize) == len(real_result) + assert len(self.transaction_normalize) == len(fake_result) + for n, r, f in zip(self.transaction_normalize, real_result, fake_result): + assert n(f) == n(r) + self.transaction_normalize = [] + else: + assert fake_result == real_result or ( + type(fake_result) is float and fake_result == pytest.approx(real_result) + ), f"Discrepancy when running command {command}, fake({fake_result}) != real({real_result})" + if real_result == b"QUEUED": + # Since redis removes the distinction between simple strings and + # bulk strings, this might not actually indicate that we're in a + # transaction. But it is extremely unlikely that hypothesis will + # find such examples. + self.transaction_normalize.append(command.normalize) + if len(command.args) == 1 and Command.encode(command.args[0]).lower() in ( + b"discard", + b"exec", + ): + self.transaction_normalize = [] + + @initialize(attrs=attrs) + def init_attrs(self, attrs): + for key, value in attrs.items(): + setattr(self, key, value) + + # hypothesis doesn't allow ordering of @initialize, so we have to put + # preconditions on rules to ensure we call init_data exactly once and + # after init_attrs. + @precondition(lambda self: not self.initialized_data) + @rule( + commands=self_strategy.flatmap( + lambda self: st.lists(self.create_command_strategy) + ) + ) + def init_data(self, commands) -> None: + for command in commands: + self._compare(command) + self.initialized_data = True + + @precondition(lambda self: self.initialized_data) + @rule(command=self_strategy.flatmap(lambda self: self.command_strategy)) + def one_command(self, command: Command) -> None: + self._compare(command) + + +class BaseTest: + """Base class for test classes.""" + + command_strategy: SearchStrategy + create_command_strategy = st.nothing() + + @pytest.mark.slow + def test(self): + class Machine(CommonMachine): + create_command_strategy = self.create_command_strategy + command_strategy = self.command_strategy + + # hypothesis.settings.register_profile( + # "debug", max_examples=10, verbosity=hypothesis.Verbosity.debug + # ) + hypothesis.settings.register_profile( + "debug", verbosity=hypothesis.Verbosity.debug + ) + hypothesis.settings.load_profile("debug") + hypothesis.stateful.run_state_machine_as_test(Machine) diff --git a/tests/fakeredis/test/test_hypothesis/test_connection.py b/tests/fakeredis/test/test_hypothesis/test_connection.py new file mode 100644 index 000000000..55daaa00f --- /dev/null +++ b/tests/fakeredis/test/test_hypothesis/test_connection.py @@ -0,0 +1,13 @@ +import hypothesis.strategies as st + +from test.test_hypothesis.base import BaseTest, commands, values, common_commands + + +class TestConnection(BaseTest): + # TODO: tests for select + connection_commands = ( + commands(st.just("echo"), values) + | commands(st.just("ping"), st.lists(values, max_size=2)) + # | commands(st.just("swapdb"), dbnums, dbnums) + ) + command_strategy = connection_commands | common_commands diff --git a/tests/fakeredis/test/test_hypothesis/test_hash.py b/tests/fakeredis/test/test_hypothesis/test_hash.py new file mode 100644 index 000000000..49f6dcfa3 --- /dev/null +++ b/tests/fakeredis/test/test_hypothesis/test_hash.py @@ -0,0 +1,46 @@ +import hypothesis.strategies as st + +from test.test_hypothesis.base import ( + BaseTest, + commands, + values, + keys, + common_commands, + fields, + ints, + expires_seconds, +) + + +class TestHash(BaseTest): + hash_commands = ( + commands(st.just("hset"), keys, st.lists(st.tuples(fields, values))) + | commands(st.just("hdel"), keys, st.lists(fields)) + | commands(st.just("hexists"), keys, fields) + | commands(st.just("hget"), keys, fields) + | commands(st.sampled_from(["hgetall", "hkeys", "hvals"]), keys) + | commands(st.just("hincrby"), keys, fields, ints) + | commands(st.just("hlen"), keys) + | commands(st.just("hmget"), keys, st.lists(fields)) + | commands(st.just("hset"), keys, st.lists(st.tuples(fields, values))) + | commands(st.just("hsetnx"), keys, fields, values) + | commands(st.just("hstrlen"), keys, fields) + | commands( + st.just("hpersist"), + st.just("fields"), + st.just(2), + st.lists(fields, min_size=2, max_size=2), + ) + | commands( + st.just("hexpire"), + keys, + expires_seconds, + st.just("fields"), + st.just(2), + st.lists(fields, min_size=2, max_size=2), + ) + ) + create_command_strategy = commands( + st.just("hset"), keys, st.lists(st.tuples(fields, values), min_size=1) + ) + command_strategy = hash_commands | common_commands diff --git a/tests/fakeredis/test/test_hypothesis/test_joint.py b/tests/fakeredis/test/test_hypothesis/test_joint.py new file mode 100644 index 000000000..a4837897e --- /dev/null +++ b/tests/fakeredis/test/test_hypothesis/test_joint.py @@ -0,0 +1,38 @@ +import hypothesis.strategies as st + +from test.test_hypothesis.base import BaseTest, common_commands, commands +from test.test_hypothesis.test_connection import TestConnection +from test.test_hypothesis.test_hash import TestHash +from test.test_hypothesis.test_list import TestList +from test.test_hypothesis.test_server import TestServer +from test.test_hypothesis.test_set import TestSet +from test.test_hypothesis.test_string import TestString, string_commands +from test.test_hypothesis.test_zset import TestZSet + +bad_commands = ( + # redis-py splits the command on spaces, and hangs if that ends up being an empty list + commands( + st.text().filter(lambda x: bool(x.split())), st.lists(st.binary() | st.text()) + ) +) + + +class TestJoint(BaseTest): + create_command_strategy = ( + TestString.create_command_strategy + | TestHash.create_command_strategy + | TestList.create_command_strategy + | TestSet.create_command_strategy + | TestZSet.create_command_strategy + ) + command_strategy = ( + TestServer.server_commands + | TestConnection.connection_commands + | string_commands + | TestHash.hash_commands + | TestList.list_commands + | TestSet.set_commands + | TestZSet.zset_commands + | common_commands + | bad_commands + ) diff --git a/tests/fakeredis/test/test_hypothesis/test_list.py b/tests/fakeredis/test/test_hypothesis/test_list.py new file mode 100644 index 000000000..2c14de76a --- /dev/null +++ b/tests/fakeredis/test/test_hypothesis/test_list.py @@ -0,0 +1,45 @@ +import hypothesis.strategies as st + +from test.test_hypothesis.base import ( + BaseTest, + commands, + values, + keys, + common_commands, + counts, + ints, +) + + +class TestList(BaseTest): + # TODO: blocking commands + list_commands = ( + commands(st.just("lindex"), keys, counts) + | commands( + st.just("linsert"), + keys, + st.sampled_from(["before", "after", "BEFORE", "AFTER"]) | st.binary(), + values, + values, + ) + | commands(st.just("llen"), keys) + | commands( + st.sampled_from(["lpop", "rpop"]), + keys, + st.just(None) | st.just([]) | ints, + ) + | commands( + st.sampled_from(["lpush", "lpushx", "rpush", "rpushx"]), + keys, + st.lists(values), + ) + | commands(st.just("lrange"), keys, counts, counts) + | commands(st.just("lrem"), keys, counts, values) + | commands(st.just("lset"), keys, counts, values) + | commands(st.just("ltrim"), keys, counts, counts) + | commands(st.just("rpoplpush"), keys, keys) + ) + create_command_strategy = commands( + st.just("rpush"), keys, st.lists(values, min_size=1) + ) + command_strategy = list_commands | common_commands diff --git a/tests/fakeredis/test/test_hypothesis/test_server.py b/tests/fakeredis/test/test_hypothesis/test_server.py new file mode 100644 index 000000000..ca786607c --- /dev/null +++ b/tests/fakeredis/test/test_hypothesis/test_server.py @@ -0,0 +1,24 @@ +import hypothesis.strategies as st + +from test.test_hypothesis.base import ( + BaseTest, + commands, + common_commands, + keys, + values, +) +from test.test_hypothesis.test_string import string_commands + + +class TestServer(BaseTest): + # TODO: real redis raises an error if there is a save already in progress. + # Find a better way to test this. commands(st.just('bgsave')) + server_commands = ( + commands(st.just("dbsize")) + | commands(st.sampled_from(["flushdb", "flushall"])) + # TODO: result is non-deterministic + # | commands(st.just('lastsave')) + | commands(st.just("save")) + ) + create_command_strategy = commands(st.just("set"), keys, values) + command_strategy = server_commands | string_commands | common_commands diff --git a/tests/fakeredis/test/test_hypothesis/test_set.py b/tests/fakeredis/test/test_hypothesis/test_set.py new file mode 100644 index 000000000..2e392e9cd --- /dev/null +++ b/tests/fakeredis/test/test_hypothesis/test_set.py @@ -0,0 +1,30 @@ +import hypothesis.strategies as st + +from test.test_hypothesis.base import ( + BaseTest, + commands, + keys, + common_commands, + fields, +) + + +class TestSet(BaseTest): + set_commands = ( + commands(st.just("sadd"), keys, st.lists(fields)) + | commands(st.just("scard"), keys) + | commands(st.sampled_from(["sdiff", "sinter", "sunion"]), st.lists(keys)) + | commands( + st.sampled_from(["sdiffstore", "sinterstore", "sunionstore"]), + keys, + st.lists(keys), + ) + | commands(st.just("sismember"), keys, fields) + | commands(st.just("smembers"), keys) + | commands(st.just("smove"), keys, keys, fields) + | commands(st.just("srem"), keys, st.lists(fields)) + ) + create_command_strategy = commands( + st.just("sadd"), keys, st.lists(fields, min_size=1) + ) + command_strategy = set_commands | common_commands diff --git a/tests/fakeredis/test/test_hypothesis/test_string.py b/tests/fakeredis/test/test_hypothesis/test_string.py new file mode 100644 index 000000000..ae49859ed --- /dev/null +++ b/tests/fakeredis/test/test_hypothesis/test_string.py @@ -0,0 +1,59 @@ +import hypothesis.strategies as st + +from test.test_hypothesis.base import ( + BaseTest, + commands, + values, + keys, + common_commands, + counts, + int_as_bytes, + zero_or_more, + ints, + expires_seconds, + expires_ms, +) + +optional_bitcount_range = st.just(()) | st.tuples(int_as_bytes, int_as_bytes) +# todo: Should be addressed +# str_len = st.integers(min_value=-3, max_value=3) | st.integers( +# min_value=-2147483647, max_value=2147483648 +# ) +str_len = st.integers(min_value=-3, max_value=3) | st.integers( + min_value=-3000, max_value=3000 +) + +string_commands = ( + commands(st.just("append"), keys, values) + | commands(st.just("bitcount"), keys, optional_bitcount_range) + | commands(st.sampled_from(["incr", "decr"]), keys) + | commands(st.sampled_from(["incrby", "decrby"]), keys, values) + | commands(st.just("get"), keys) + | commands(st.just("getbit"), keys, counts) + | commands( + st.just("setbit"), + keys, + counts, + st.integers(min_value=0, max_value=1) | ints, + ) + | commands(st.sampled_from(["substr", "getrange"]), keys, str_len, counts) + | commands(st.just("getset"), keys, values) + | commands(st.just("mget"), st.lists(keys)) + | commands(st.sampled_from(["mset", "msetnx"]), st.lists(st.tuples(keys, values))) + | commands( + st.just("set"), + keys, + values, + *zero_or_more("nx", "xx", "keepttl"), + ) + | commands(st.just("setex"), keys, expires_seconds, values) + | commands(st.just("psetex"), keys, expires_ms, values) + | commands(st.just("setnx"), keys, values) + | commands(st.just("setrange"), keys, str_len, values) + | commands(st.just("strlen"), keys) +) + + +class TestString(BaseTest): + create_command_strategy = commands(st.just("set"), keys, values) + command_strategy = string_commands | common_commands diff --git a/tests/fakeredis/test/test_hypothesis/test_transaction.py b/tests/fakeredis/test/test_hypothesis/test_transaction.py new file mode 100644 index 000000000..779121be4 --- /dev/null +++ b/tests/fakeredis/test/test_hypothesis/test_transaction.py @@ -0,0 +1,54 @@ +import hypothesis.strategies as st + +from test.test_hypothesis.base import ( + BaseTest, + commands, + values, + keys, + common_commands, + counts, + zero_or_more, + ints, + expires_seconds, + expires_ms, +) +from test.test_hypothesis.test_string import TestString + + +class TestTransaction(BaseTest): + transaction_commands = ( + commands(st.sampled_from(["multi", "discard", "exec", "unwatch"])) + | commands(st.just("watch"), keys) + | commands(st.just("append"), keys, values) + | commands(st.just("bitcount"), keys) + | commands(st.just("bitcount"), keys, values, values) + | commands(st.sampled_from(["incr", "decr"]), keys) + | commands(st.sampled_from(["incrby", "decrby"]), keys, values) + | commands(st.just("get"), keys) + | commands(st.just("getbit"), keys, counts) + | commands( + st.just("setbit"), + keys, + counts, + st.integers(min_value=0, max_value=1) | ints, + ) + | commands(st.sampled_from(["substr", "getrange"]), keys, counts, counts) + | commands(st.just("getset"), keys, values) + | commands(st.just("mget"), st.lists(keys)) + | commands( + st.sampled_from(["mset", "msetnx"]), st.lists(st.tuples(keys, values)) + ) + | commands( + st.just("set"), + keys, + values, + *zero_or_more("nx", "xx", "keepttl"), + ) + | commands(st.just("setex"), keys, expires_seconds, values) + | commands(st.just("psetex"), keys, expires_ms, values) + | commands(st.just("setnx"), keys, values) + | commands(st.just("setrange"), keys, counts, values) + | commands(st.just("strlen"), keys) + ) + create_command_strategy = TestString.create_command_strategy + command_strategy = transaction_commands | common_commands diff --git a/tests/fakeredis/test/test_hypothesis/test_zset.py b/tests/fakeredis/test/test_hypothesis/test_zset.py new file mode 100644 index 000000000..cf5a0c42f --- /dev/null +++ b/tests/fakeredis/test/test_hypothesis/test_zset.py @@ -0,0 +1,109 @@ +import operator + +import hypothesis.strategies as st + +from test.test_hypothesis.base import ( + BaseTest, + commands, + keys, + common_commands, + counts, + fields, + zero_or_more, + scores, + Command, + float_as_bytes, +) + +score_tests = scores | st.builds(lambda x: b"(" + repr(x).encode(), scores) +limits = st.just(()) | st.tuples(st.just("limit"), counts, counts) +string_tests = st.sampled_from([b"+", b"-"]) | st.builds( + operator.add, st.sampled_from([b"(", b"["]), fields +) +zset_no_score_create_commands = commands( + st.just("zadd"), keys, st.lists(st.tuples(st.just(0), fields), min_size=1) +) +zset_no_score_commands = ( # TODO: test incr + commands( + st.just("zadd"), + keys, + *zero_or_more("nx", "xx", "ch", "incr"), + st.lists(st.tuples(st.just(0), fields)), + ) + | commands(st.just("zlexcount"), keys, string_tests, string_tests) + | commands( + st.sampled_from(["zrangebylex", "zrevrangebylex"]), + keys, + string_tests, + string_tests, + limits, + ) + | commands(st.just("zremrangebylex"), keys, string_tests, string_tests) +) + + +def optional(arg): + return st.none() | st.just(arg) + + +def build_zstore(command, dest, sources, weights, aggregate) -> Command: + args = [command, dest, len(sources)] + args += [source[0] for source in sources] + if weights: + args.append("weights") + args += [source[1] for source in sources] + if aggregate: + args += ["aggregate", aggregate] + return Command(args) + + +class TestZSet(BaseTest): + zset_commands = ( + commands( + st.just("zadd"), + keys, + *zero_or_more("nx", "xx", "ch", "incr"), + st.lists(st.tuples(scores, fields)), + ) + | commands(st.just("zcard"), keys) + | commands(st.just("zcount"), keys, score_tests, score_tests) + | commands(st.just("zincrby"), keys, scores, fields) + | commands( + st.sampled_from(["zrange", "zrevrange"]), + keys, + counts, + counts, + optional("withscores"), + ) + | commands( + st.sampled_from(["zrangebyscore", "zrevrangebyscore"]), + keys, + score_tests, + score_tests, + limits, + optional("withscores"), + ) + | commands(st.sampled_from(["zrank", "zrevrank"]), keys, fields) + | commands(st.just("zrem"), keys, st.lists(fields)) + | commands(st.just("zremrangebyrank"), keys, counts, counts) + | commands(st.just("zremrangebyscore"), keys, score_tests, score_tests) + | commands(st.just("zscore"), keys, fields) + | st.builds( + build_zstore, + command=st.sampled_from(["zunionstore", "zinterstore"]), + dest=keys, + sources=st.lists(st.tuples(keys, float_as_bytes)), + weights=st.booleans(), + aggregate=st.sampled_from([None, "sum", "min", "max"]), + ) + ) + # TODO: zscan, zpopmin/zpopmax, bzpopmin/bzpopmax, probably more + create_command_strategy = commands( + st.just("zadd"), keys, st.lists(st.tuples(scores, fields), min_size=1) + ) + command_strategy = zset_commands | common_commands + + +class TestZSetNoScores(BaseTest): + create_command_strategy = zset_no_score_create_commands + command_strategy = zset_no_score_commands | common_commands diff --git a/tests/fakeredis/test/test_issues.py b/tests/fakeredis/test/test_issues.py new file mode 100644 index 000000000..8ce2e7d91 --- /dev/null +++ b/tests/fakeredis/test/test_issues.py @@ -0,0 +1,141 @@ +import pytest +import redis.client + + +def test_causes_crash(r: redis.Redis): + key = b"}W\xfa\x87\xf4" + key2 = b"\xf3\xba\x00\xa1\x1c\xac\x01A\x8b\xc4\xe9\xe2\xa8" + r.rpush(key, b"!\xef\x9e\xd2", b"1175417134") + r.rpoplpush(key, key) + r.lrange(key, -1, 14795) + with pytest.raises(redis.ResponseError): + r.rename(key2, key2) + r.lrange(key, 2, 0) + r.sort(key, alpha=True) + r.llen(key) + r.keys("*") + r.keys("*") + r.lindex(key, 1) + r.exists(key, key2, key, key2, key2, key2, key2, key) + r.linsert(key, "AFTER", b"inf", b"!\xef\x9e\xd2") + with pytest.raises(redis.ResponseError): + r.linsert( + key, + b"W8\xe9&", + b"-43950", + b"-43950", + ) + r.rpoplpush(key, key) + with pytest.raises(redis.ResponseError): + r.exists() + r.lrem(key2, -56700, b"-6.816602725023744e+16") + r.lrem(key, -3, b"1175417134") + r.llen(key2) + r.lrem(key, -3, b"!\xef\x9e\xd2") + + +def test_another_test_causes_crash(r: redis.Redis): + key1 = b"\xc2\xdb" + key2 = b"z`\xf8,\xe2\x02\xb3\x85\xc5" + key3 = b"\xf4<\xe1\xb6\xcb\xde\xaf" + key4 = b"\xad" + r.rpush(key1, b"i\x05\x0b\xb1") + r.rpush( + key2, + b"i\x05\x0b\xb1", + b"i\x05\x0b\xb1", + b"\\h\xf2", + b"\\h\xf2", + b"i\x05\x0b\xb1", + b"\\h\xf2", + ) + r.rpush( + key3, + b"\\h\xf2", + b"i\x05\x0b\xb1", + b"i\x05\x0b\xb1", + b"\\h\xf2", + b"\\h\xf2", + b"\\h\xf2", + b"\\h\xf2", + b"i\x05\x0b\xb1", + ) + r.rpush( + key1, + b"i\x05\x0b\xb1", + b"\\h\xf2", + b"i\x05\x0b\xb1", + b"\\h\xf2", + b"i\x05\x0b\xb1", + b"\\h\xf2", + b"\\h\xf2", + b"i\x05\x0b\xb1", + ) + r.rpush(key2, b"i\x05\x0b\xb1") + + r.lpop(b"") + with pytest.raises(redis.ResponseError): + r.rpushx(key4) + r.move(b"", 1) + with pytest.raises(redis.ResponseError): + r.move(key3, -730) + r.ltrim(key3, -51547, -2) + r.rpoplpush(key4, b"") + + r.rpush(key4, b"i\x05\x0b\xb1", b"\\h\xf2", b"\\h\xf2") + r.persist(key2) + r.exists(key1, key1, key1) + + r.ltrim(b"", -12584, -3) + r.lrem(key4, -1, b"i\x05\x0b\xb1") + with pytest.raises(redis.ResponseError): + r.linsert(key2, b"\xa5", b"\\h\xf2", b"i\x05\x0b\xb1") + r.linsert(key2, "BEFORE", b"\\h\xf2", b"\\h\xf2") + r.ltrim(key2, 1, -2_147_483_648) + r.ltrim(key1, -4200252, 1) + r.rpush( + b"", + b"i\x05\x0b\xb1", + b"i\x05\x0b\xb1", + b"i\x05\x0b\xb1", + b"\\h\xf2", + b"\\h\xf2", + b"i\x05\x0b\xb1", + ) + with pytest.raises(redis.ResponseError): + r.rpop(key1, -2_147_483_648) + r.lrem(key1, 77, b"i\x05\x0b\xb1") + r.rpoplpush(b"", key2) + r.ltrim(b"", 0, 1) + r.unlink(b"") + r.ltrim(key1, 0, 0) + r.lrem(key3, 31029, b"\\h\xf2") + r.lrange(key1, -2, -91) + r.rpoplpush(key1, key2) + r.rpush( + key1, + b"\\h\xf2", + b"\\h\xf2", + b"\\h\xf2", + b"\\h\xf2", + b"\\h\xf2", + b"\\h\xf2", + b"\\h\xf2", + b"\\h\xf2", + b"\\h\xf2", + b"\\h\xf2", + b"\\h\xf2", + b"i\x05\x0b\xb1", + b"i\x05\x0b\xb1", + b"\\h\xf2", + ) + r.ltrim(key1, 0, 18) + r.keys("*") + with pytest.raises(redis.ResponseError): + r.move(key4, 993) + r.lrange(b"", 0, 38001) + with pytest.raises(redis.ResponseError): + r.sort(key4) + r.lindex(key1, -2) + r.rpoplpush(key4, key4) + r.lrem(key4, -18528, b"\\h\xf2")