From 405951b5dbaa23257f7755b6ef4e3ca8c80258d4 Mon Sep 17 00:00:00 2001 From: llbbl Date: Wed, 3 Sep 2025 16:53:58 +0000 Subject: [PATCH] feat: Set up comprehensive Python testing infrastructure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Poetry package manager with pyproject.toml configuration - Configure pytest with coverage reporting, custom markers, and strict options - Set up testing directory structure with unit/integration separation - Create comprehensive shared fixtures for Blender addon testing - Add validation tests to verify testing infrastructure functionality - Configure .gitignore for testing artifacts and development files - Support 80% test coverage threshold with HTML/XML reporting 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .gitignore | 103 ++++++++++++ poetry.lock | 283 +++++++++++++++++++++++++++++++++ pyproject.toml | 95 +++++++++++ tests/__init__.py | 0 tests/conftest.py | 173 ++++++++++++++++++++ tests/integration/__init__.py | 0 tests/test_setup_validation.py | 262 ++++++++++++++++++++++++++++++ tests/unit/__init__.py | 0 8 files changed, 916 insertions(+) create mode 100644 .gitignore create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/integration/__init__.py create mode 100644 tests/test_setup_validation.py create mode 100644 tests/unit/__init__.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c56e65f --- /dev/null +++ b/.gitignore @@ -0,0 +1,103 @@ +# Testing +.pytest_cache/ +.coverage +htmlcov/ +coverage.xml +.tox/ +.nox/ + +# Claude settings +.claude/* + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Virtual environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# IDEs and editors +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store +Thumbs.db + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +Pipfile.lock + +# Celery +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# Temporary files +*.tmp +*.temp +.tmp/ +temp/ + +# Logs +*.log +logs/ \ No newline at end of file diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..6047be4 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,283 @@ +# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["test"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.2.7" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.7" +groups = ["test"] +files = [ + {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, + {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, + {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, + {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, + {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, + {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, + {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, + {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, + {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, + {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, + {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, + {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, + {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, + {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, + {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, + {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, + {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, + {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +groups = ["test"] +markers = "python_version < \"3.11\"" +files = [ + {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, + {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "importlib-metadata" +version = "6.7.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.7" +groups = ["test"] +markers = "python_version == \"3.7\"" +files = [ + {file = "importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5"}, + {file = "importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4"}, +] + +[package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = ">=0.5" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +testing = ["flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\"", "pytest-perf (>=0.9.2)", "pytest-ruff"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +groups = ["test"] +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "packaging" +version = "24.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +groups = ["test"] +files = [ + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, +] + +[[package]] +name = "pluggy" +version = "1.2.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.7" +groups = ["test"] +files = [ + {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, + {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pytest" +version = "7.4.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +groups = ["test"] +files = [ + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.7" +groups = ["test"] +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "pytest-mock" +version = "3.11.1" +description = "Thin-wrapper around the mock package for easier use with pytest" +optional = false +python-versions = ">=3.7" +groups = ["test"] +files = [ + {file = "pytest-mock-3.11.1.tar.gz", hash = "sha256:7f6b125602ac6d743e523ae0bfa71e1a697a2f5534064528c6ff84c2f7c2fc7f"}, + {file = "pytest_mock-3.11.1-py3-none-any.whl", hash = "sha256:21c279fff83d70763b05f8874cc9cfb3fcacd6d354247a976f9529d19f9acf39"}, +] + +[package.dependencies] +pytest = ">=5.0" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +groups = ["test"] +markers = "python_full_version <= \"3.11.0a6\"" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "typing-extensions" +version = "4.7.1" +description = "Backported and Experimental Type Hints for Python 3.7+" +optional = false +python-versions = ">=3.7" +groups = ["test"] +markers = "python_version < \"3.11\"" +files = [ + {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, + {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, +] + +[[package]] +name = "zipp" +version = "3.15.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.7" +groups = ["test"] +markers = "python_version == \"3.7\"" +files = [ + {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, + {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8 ; python_version < \"3.12\"", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\""] + +[metadata] +lock-version = "2.1" +python-versions = "^3.7" +content-hash = "c21e2c1bbbadcf3e471abb3d864f176d6dcf9d4b872590fbe7f87f6b500ed7df" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..01cf166 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,95 @@ +[tool.poetry] +name = "extra-image-list" +version = "0.2.9" +description = "An alternative image list for UV/Image Editor in Blender" +authors = ["Miki (meshlogic) ", "Rombout Versluijs "] +readme = "README.md" +license = "GPL-3.0" +homepage = "https://github.com/schroef/Extra-Image-List" +repository = "https://github.com/schroef/Extra-Image-List" +keywords = ["blender", "addon", "uv", "image", "editor"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: End Users/Desktop", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "Programming Language :: Python :: 3", + "Topic :: Multimedia :: Graphics :: 3D Modeling", + "Topic :: Software Development :: Libraries :: Python Modules", +] +packages = [{include = "*.py", from = "."}] +package-mode = false + +[tool.poetry.dependencies] +python = "^3.7" + +[tool.poetry.group.test.dependencies] +pytest = "^7.0.0" +pytest-cov = "^4.0.0" +pytest-mock = "^3.10.0" + +# Using group dependencies for testing instead of scripts +# Run tests with: poetry run pytest + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py", "*_test.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = [ + "--strict-markers", + "--strict-config", + "--verbose", + "--cov=.", + "--cov-report=term-missing", + "--cov-report=html:htmlcov", + "--cov-report=xml:coverage.xml", + "--cov-fail-under=80", +] +markers = [ + "unit: marks tests as unit tests (fast, isolated)", + "integration: marks tests as integration tests (slower, with dependencies)", + "slow: marks tests as slow (deselect with '-m \"not slow\"')", +] + +[tool.coverage.run] +source = ["."] +omit = [ + "tests/*", + "htmlcov/*", + ".venv/*", + "venv/*", + "*.egg-info/*", + "build/*", + "dist/*", + ".pytest_cache/*", + ".coverage", + "setup.py", + "__init__.py", +] +branch = true + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "if self.debug:", + "if settings.DEBUG", + "raise AssertionError", + "raise NotImplementedError", + "if 0:", + "if __name__ == .__main__.:", + "class .*\\bProtocol\\):", + "@(abc\\.)?abstractmethod", +] +show_missing = true +precision = 2 + +[tool.coverage.html] +directory = "htmlcov" + +[tool.coverage.xml] +output = "coverage.xml" \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..89e8fb0 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,173 @@ +import os +import tempfile +import pytest +from pathlib import Path +from unittest.mock import Mock, MagicMock + + +@pytest.fixture +def temp_dir(): + """Create a temporary directory for tests that gets cleaned up automatically.""" + with tempfile.TemporaryDirectory() as tmp_dir: + yield Path(tmp_dir) + + +@pytest.fixture +def temp_file(): + """Create a temporary file for tests that gets cleaned up automatically.""" + with tempfile.NamedTemporaryFile(mode='w+', delete=False) as tmp_file: + yield Path(tmp_file.name) + + # Clean up the file after the test + try: + os.unlink(tmp_file.name) + except FileNotFoundError: + pass + + +@pytest.fixture +def mock_bpy(): + """Mock the bpy module for testing Blender addon functionality.""" + mock_bpy = MagicMock() + + # Mock common bpy structures + mock_bpy.data.images = [] + mock_bpy.types.Scene = MagicMock() + mock_bpy.props.EnumProperty = MagicMock() + mock_bpy.props.StringProperty = MagicMock() + mock_bpy.props.BoolProperty = MagicMock() + mock_bpy.props.IntProperty = MagicMock() + mock_bpy.props.PointerProperty = MagicMock() + mock_bpy.utils.register_class = MagicMock() + mock_bpy.utils.unregister_class = MagicMock() + mock_bpy.context.scene = MagicMock() + mock_bpy.context.space_data = MagicMock() + mock_bpy.context.active_object = MagicMock() + mock_bpy.app.handlers.depsgraph_update_post = MagicMock() + + return mock_bpy + + +@pytest.fixture +def mock_blender_context(): + """Mock Blender context for testing.""" + context = MagicMock() + context.scene = MagicMock() + context.space_data = MagicMock() + context.space_data.image = None + context.active_object = MagicMock() + context.active_object.type = 'MESH' + context.active_object.active_material = MagicMock() + context.screen.areas = [] + + return context + + +@pytest.fixture +def mock_image(): + """Mock Blender image datablock for testing.""" + image = MagicMock() + image.name = "test_image.png" + image.filepath = "/path/to/test_image.png" + image.source = 'FILE' + image.has_data = True + image.users = 1 + image.use_fake_user = False + image.packed_file = None + image.size = [1024, 1024] + image.depth = 32 + + return image + + +@pytest.fixture +def sample_image_list(): + """Create a sample list of mock images for testing.""" + images = [] + for i in range(5): + image = MagicMock() + image.name = f"image_{i}.png" + image.filepath = f"/path/to/image_{i}.png" + image.source = 'FILE' + image.has_data = True + image.users = 1 if i < 3 else 0 # First 3 have users, last 2 don't + image.use_fake_user = i == 4 # Last one is fake user + image.packed_file = None if i != 2 else MagicMock() # Third one is packed + image.size = [512 * (i + 1), 512 * (i + 1)] + image.depth = 24 + images.append(image) + + return images + + +@pytest.fixture +def mock_addon_preferences(): + """Mock addon preferences for testing.""" + prefs = MagicMock() + prefs.style = 'PREVIEW' + prefs.rows = 4 + prefs.cols = 6 + prefs.clean_enabled = False + prefs.clear_mode = 'NO USERS' + prefs.info = False + prefs.image_id = 0 + + return prefs + + +@pytest.fixture(autouse=True) +def reset_environment(): + """Reset environment variables and state before each test.""" + # Store original environment + original_env = dict(os.environ) + + yield + + # Restore original environment + os.environ.clear() + os.environ.update(original_env) + + +@pytest.fixture +def sample_config(): + """Provide sample configuration data for tests.""" + return { + "preview_rows": 4, + "preview_cols": 6, + "default_style": "PREVIEW", + "clean_enabled": False, + "show_info": True, + } + + +class MockBlenderOperator: + """Mock Blender operator for testing.""" + + def __init__(self): + self.bl_idname = "test.operator" + self.bl_label = "Test Operator" + self.bl_description = "Test operator description" + + def execute(self, context): + return {'FINISHED'} + + @classmethod + def poll(cls, context): + return True + + +@pytest.fixture +def mock_operator(): + """Provide a mock Blender operator for testing.""" + return MockBlenderOperator() + + +@pytest.fixture +def isolated_filesystem(temp_dir): + """Change to a temporary directory for the test and restore afterwards.""" + original_cwd = os.getcwd() + os.chdir(temp_dir) + + yield temp_dir + + os.chdir(original_cwd) \ No newline at end of file diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_setup_validation.py b/tests/test_setup_validation.py new file mode 100644 index 0000000..2f52074 --- /dev/null +++ b/tests/test_setup_validation.py @@ -0,0 +1,262 @@ +""" +Validation tests to verify that the testing infrastructure is set up correctly. + +These tests ensure that: +- pytest is working correctly +- fixtures are available and functional +- coverage reporting works +- different test markers work +- mocking capabilities are available +""" + +import pytest +import tempfile +import os +from pathlib import Path +from unittest.mock import Mock, patch, MagicMock + + +class TestPytestSetup: + """Test that pytest is configured correctly.""" + + def test_basic_assertion(self): + """Test basic pytest functionality.""" + assert True + assert 1 + 1 == 2 + assert "hello" in "hello world" + + def test_parametrized_test(self): + """Test parametrized test functionality.""" + @pytest.mark.parametrize("input,expected", [ + (1, 2), + (2, 4), + (3, 6), + ]) + def param_test(input, expected): + assert input * 2 == expected + + param_test(1, 2) + param_test(2, 4) + param_test(3, 6) + + +class TestFixtures: + """Test that fixtures work correctly.""" + + def test_temp_dir_fixture(self, temp_dir): + """Test the temp_dir fixture.""" + assert isinstance(temp_dir, Path) + assert temp_dir.exists() + assert temp_dir.is_dir() + + # Create a test file in the temp directory + test_file = temp_dir / "test.txt" + test_file.write_text("test content") + assert test_file.exists() + assert test_file.read_text() == "test content" + + def test_temp_file_fixture(self, temp_file): + """Test the temp_file fixture.""" + assert isinstance(temp_file, Path) + + # Write to the temp file + temp_file.write_text("test content") + assert temp_file.read_text() == "test content" + + def test_mock_bpy_fixture(self, mock_bpy): + """Test the mock_bpy fixture.""" + assert hasattr(mock_bpy, 'data') + assert hasattr(mock_bpy, 'types') + assert hasattr(mock_bpy, 'context') + assert hasattr(mock_bpy.data, 'images') + + # Test that mock methods work + mock_bpy.utils.register_class(Mock()) + mock_bpy.utils.register_class.assert_called_once() + + def test_mock_blender_context_fixture(self, mock_blender_context): + """Test the mock_blender_context fixture.""" + assert hasattr(mock_blender_context, 'scene') + assert hasattr(mock_blender_context, 'space_data') + assert hasattr(mock_blender_context, 'active_object') + assert mock_blender_context.active_object.type == 'MESH' + + def test_mock_image_fixture(self, mock_image): + """Test the mock_image fixture.""" + assert mock_image.name == "test_image.png" + assert mock_image.filepath == "/path/to/test_image.png" + assert mock_image.source == 'FILE' + assert mock_image.has_data is True + + def test_sample_image_list_fixture(self, sample_image_list): + """Test the sample_image_list fixture.""" + assert len(sample_image_list) == 5 + + # Check that images have expected properties + for i, image in enumerate(sample_image_list): + assert image.name == f"image_{i}.png" + assert image.size == [512 * (i + 1), 512 * (i + 1)] + + def test_sample_config_fixture(self, sample_config): + """Test the sample_config fixture.""" + assert sample_config["preview_rows"] == 4 + assert sample_config["preview_cols"] == 6 + assert sample_config["default_style"] == "PREVIEW" + + def test_isolated_filesystem_fixture(self, isolated_filesystem): + """Test the isolated_filesystem fixture.""" + current_dir = Path.cwd() + assert current_dir == isolated_filesystem + + # Create a file in the isolated filesystem + test_file = current_dir / "isolated_test.txt" + test_file.write_text("isolated content") + assert test_file.exists() + + +class TestMocking: + """Test mocking capabilities.""" + + def test_unittest_mock(self): + """Test basic unittest.mock functionality.""" + mock_obj = Mock() + mock_obj.method.return_value = "mocked result" + + result = mock_obj.method() + assert result == "mocked result" + mock_obj.method.assert_called_once() + + def test_patch_decorator(self): + """Test patch decorator functionality.""" + @patch('os.path.exists') + def test_with_patch(mock_exists): + mock_exists.return_value = True + result = os.path.exists("/fake/path") + assert result is True + mock_exists.assert_called_once_with("/fake/path") + + test_with_patch() + + def test_patch_context_manager(self): + """Test patch context manager functionality.""" + with patch('os.getcwd') as mock_getcwd: + mock_getcwd.return_value = "/mocked/path" + result = os.getcwd() + assert result == "/mocked/path" + mock_getcwd.assert_called_once() + + def test_magicmock(self): + """Test MagicMock functionality.""" + magic_mock = MagicMock() + + # Test magic methods + magic_mock.__len__.return_value = 5 + assert len(magic_mock) == 5 + + # Test attribute access + magic_mock.attr.method.return_value = "magic result" + assert magic_mock.attr.method() == "magic result" + + +@pytest.mark.unit +class TestUnitMarker: + """Test the unit test marker.""" + + def test_unit_test(self): + """This is a unit test.""" + assert True + + +@pytest.mark.integration +class TestIntegrationMarker: + """Test the integration test marker.""" + + def test_integration_test(self): + """This is an integration test.""" + assert True + + +@pytest.mark.slow +class TestSlowMarker: + """Test the slow test marker.""" + + def test_slow_test(self): + """This is a slow test.""" + import time + time.sleep(0.01) # Simulate a slow test + assert True + + +class TestCoverageSetup: + """Test that coverage reporting is working.""" + + def test_coverage_includes_this_function(self): + """This test ensures coverage is tracking this function.""" + def dummy_function(): + return "This function should appear in coverage" + + result = dummy_function() + assert result == "This function should appear in coverage" + + def test_branch_coverage(self): + """Test branch coverage tracking.""" + condition = True + + if condition: + result = "condition was true" + else: + result = "condition was false" # pragma: no cover + + assert result == "condition was true" + + +class TestErrorHandling: + """Test error handling and exception testing.""" + + def test_expected_exception(self): + """Test that expected exceptions work correctly.""" + with pytest.raises(ValueError): + raise ValueError("This is expected") + + def test_exception_message(self): + """Test checking exception messages.""" + with pytest.raises(ValueError, match="specific message"): + raise ValueError("specific message") + + def test_no_exception_raised(self): + """Test that no exception is raised when expected.""" + def safe_function(): + return "no exception" + + result = safe_function() + assert result == "no exception" + + +def test_module_level_function(): + """Test that module-level functions work in pytest.""" + assert callable(test_module_level_function) + assert test_module_level_function.__name__ == "test_module_level_function" + + +class TestDataDriven: + """Test data-driven testing capabilities.""" + + @pytest.mark.parametrize("input_value,expected", [ + ("hello", 5), + ("world", 5), + ("pytest", 6), + ("", 0), + ]) + def test_string_length(self, input_value, expected): + """Test parametrized string length calculation.""" + assert len(input_value) == expected + + @pytest.mark.parametrize("a,b,expected", [ + (1, 2, 3), + (0, 0, 0), + (-1, 1, 0), + (10, -5, 5), + ]) + def test_addition(self, a, b, expected): + """Test parametrized addition.""" + assert a + b == expected \ No newline at end of file diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29