From 1e95bba0ef3e6b9e8094c2767aff5c46d087e00f Mon Sep 17 00:00:00 2001 From: Andrew Beveridge Date: Sat, 28 Dec 2024 23:46:53 -0500 Subject: [PATCH] Added tests for CLI and controller, 100% coverage --- lyrics_transcriber/cli/main.py | 8 +- poetry.lock | 176 +++++++++++++++++++- pyproject.toml | 11 ++ tests/__init__.py | 0 tests/cli/__init__.py | 0 tests/cli/test_main.py | 174 +++++++++++++++++++ tests/conftest.py | 17 ++ tests/core/__init__.py | 0 tests/core/test_controller.py | 296 +++++++++++++++++++++++++++++++++ 9 files changed, 675 insertions(+), 7 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/cli/__init__.py create mode 100644 tests/cli/test_main.py create mode 100644 tests/conftest.py create mode 100644 tests/core/__init__.py create mode 100644 tests/core/test_controller.py diff --git a/lyrics_transcriber/cli/main.py b/lyrics_transcriber/cli/main.py index 84d9c00..776cdd1 100755 --- a/lyrics_transcriber/cli/main.py +++ b/lyrics_transcriber/cli/main.py @@ -4,7 +4,7 @@ import os from pathlib import Path from typing import Dict -import pkg_resources +from importlib.metadata import version from dotenv import load_dotenv from lyrics_transcriber import LyricsTranscriber @@ -27,7 +27,7 @@ def create_arg_parser() -> argparse.ArgumentParser: ) # Version - package_version = pkg_resources.get_distribution("lyrics-transcriber").version + package_version = version("lyrics-transcriber") parser.add_argument("-v", "--version", action="version", version=f"%(prog)s {package_version}") # Optional arguments @@ -192,7 +192,3 @@ def main() -> None: except Exception as e: logger.error(f"Processing failed: {str(e)}") exit(1) - - -if __name__ == "__main__": - main() diff --git a/poetry.lock b/poetry.lock index 6c39203..d75f7ed 100644 --- a/poetry.lock +++ b/poetry.lock @@ -215,6 +215,83 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "coverage" +version = "7.6.10" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.9" +files = [ + {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] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + [[package]] name = "docx2txt" version = "0.8" @@ -242,6 +319,20 @@ requests = ">=2.16.2" six = ">=1.12.0" stone = ">=2,<3.3.3" +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + [[package]] name = "idna" version = "3.10" @@ -256,6 +347,17 @@ files = [ [package.extras] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +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 = "karaoke-lyrics-processor" version = "0.4.1" @@ -496,6 +598,21 @@ docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-a test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] type = ["mypy (>=1.11.2)"] +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + [[package]] name = "ply" version = "3.11" @@ -528,6 +645,63 @@ files = [ {file = "pyperclip-1.9.0.tar.gz", hash = "sha256:b7de0142ddc81bfc5c7507eea19da920b92252b548b96186caf94a5e2527d310"}, ] +[[package]] +name = "pytest" +version = "8.3.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "6.0.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, + {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, +] + +[package.dependencies] +coverage = {version = ">=7.5", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] + +[[package]] +name = "pytest-mock" +version = "3.14.0" +description = "Thin-wrapper around the mock package for easier use with pytest" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, + {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, +] + +[package.dependencies] +pytest = ">=6.2.5" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + [[package]] name = "python-docx" version = "1.1.2" @@ -817,4 +991,4 @@ zstd = ["zstandard (>=0.18.0)"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "4f593a2a489ebc809775db3a27a44f8e7a4f990884d4cb6103e439b9b5d549d7" +content-hash = "4cd2a6cbb184dd3b17b771299b5abed2aafd65b7551884cabb82dcd9b30ba513" diff --git a/pyproject.toml b/pyproject.toml index bfbb8e7..6ff71bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,9 @@ pydub = ">=0.25" [tool.poetry.group.dev.dependencies] black = ">=23" +pytest = ">=7.0" +pytest-cov = ">=4.0" +pytest-mock = ">=3.10" [tool.black] line-length = 140 @@ -33,3 +36,11 @@ lyrics-transcriber = 'lyrics_transcriber.cli.main:main' [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +addopts = "-v --cov=lyrics_transcriber --cov-report=term-missing" +filterwarnings = [ + "ignore:'audioop' is deprecated:DeprecationWarning" +] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cli/__init__.py b/tests/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cli/test_main.py b/tests/cli/test_main.py new file mode 100644 index 0000000..3fe364b --- /dev/null +++ b/tests/cli/test_main.py @@ -0,0 +1,174 @@ +import pytest +from pathlib import Path +from unittest.mock import patch, Mock +from lyrics_transcriber.cli.main import create_arg_parser, create_configs, validate_args, setup_logging, get_config_from_env, main + + +def test_create_arg_parser(): + parser = create_arg_parser() + assert parser is not None + + # Test parsing of various arguments + args = parser.parse_args(["test.mp3", "--artist", "Test Artist", "--title", "Test Song"]) + assert args.audio_filepath == "test.mp3" + assert args.artist == "Test Artist" + assert args.title == "Test Song" + + # Test default values + args = parser.parse_args(["test.mp3"]) + assert args.log_level == "INFO" + assert args.video_resolution == "360p" + assert args.video_background_color == "black" + assert args.cache_dir == Path("/tmp/lyrics-transcriber-cache/") + assert not args.render_video + + +def test_validate_args_no_audio_file(capsys, test_logger): + parser = create_arg_parser() + args = parser.parse_args([]) + + with pytest.raises(SystemExit) as exc_info: + validate_args(args, parser, test_logger) + assert exc_info.value.code == 1 + + +def test_validate_args_missing_title(sample_audio_file, test_logger): + parser = create_arg_parser() + args = parser.parse_args([sample_audio_file, "--artist", "Test Artist"]) + + with pytest.raises(SystemExit) as exc_info: + validate_args(args, parser, test_logger) + assert exc_info.value.code == 1 + + +def test_validate_args_nonexistent_file(test_logger): + parser = create_arg_parser() + args = parser.parse_args(["nonexistent.mp3"]) + + with pytest.raises(SystemExit) as exc_info: + validate_args(args, parser, test_logger) + assert exc_info.value.code == 1 + + +def test_validate_args_valid(sample_audio_file, test_logger): + parser = create_arg_parser() + args = parser.parse_args([sample_audio_file, "--artist", "Test Artist", "--title", "Test Song"]) + + # Should not raise any exceptions + validate_args(args, parser, test_logger) + + +def test_setup_logging(): + logger = setup_logging("DEBUG") + assert logger.level == 10 # DEBUG level + + logger = setup_logging("INFO") + assert logger.level == 20 # INFO level + + # Test formatter + assert len(logger.handlers) == 1 + handler = logger.handlers[0] + assert handler.formatter is not None + assert "%(asctime)s.%(msecs)03d" in handler.formatter._fmt + + +@patch("os.getenv") +def test_get_config_from_env(mock_getenv): + # Setup mock environment variables + mock_getenv.side_effect = lambda x: { + "AUDIOSHAKE_API_TOKEN": "test_audioshake", + "GENIUS_API_TOKEN": "test_genius", + "SPOTIFY_COOKIE_SP_DC": "test_spotify", + "RUNPOD_API_KEY": "test_runpod", + "WHISPER_RUNPOD_ID": "test_whisper", + }.get(x) + + config = get_config_from_env() + + assert config["audioshake_api_token"] == "test_audioshake" + assert config["genius_api_token"] == "test_genius" + assert config["spotify_cookie"] == "test_spotify" + assert config["runpod_api_key"] == "test_runpod" + assert config["whisper_runpod_id"] == "test_whisper" + + +def test_create_configs(): + parser = create_arg_parser() + args = parser.parse_args( + [ + "test.mp3", + "--audioshake_api_token", + "cli_audioshake", + "--genius_api_token", + "cli_genius", + "--spotify_cookie", + "cli_spotify", + "--output_dir", + "test_output", + "--render_video", + "--video_resolution", + "1080p", + "--video_background_color", + "blue", + ] + ) + + env_config = { + "audioshake_api_token": "env_audioshake", + "genius_api_token": "env_genius", + "spotify_cookie": "env_spotify", + "runpod_api_key": "env_runpod", + "whisper_runpod_id": "env_whisper", + } + + transcriber_config, lyrics_config, output_config = create_configs(args, env_config) + + # CLI args should take precedence over env vars + assert transcriber_config.audioshake_api_token == "cli_audioshake" + assert lyrics_config.genius_api_token == "cli_genius" + assert lyrics_config.spotify_cookie == "cli_spotify" + + # Env vars should be used when CLI args aren't provided + assert transcriber_config.runpod_api_key == "env_runpod" + assert transcriber_config.whisper_runpod_id == "env_whisper" + + # Output config should reflect CLI args + assert output_config.output_dir == "test_output" + assert output_config.render_video is True + assert output_config.video_resolution == "1080p" + assert output_config.video_background_color == "blue" + + +@patch("lyrics_transcriber.cli.main.LyricsTranscriber") +def test_main_successful_run(mock_transcriber_class, sample_audio_file, test_logger): + mock_transcriber = Mock() + mock_transcriber_class.return_value = mock_transcriber + + # Setup mock results + mock_results = Mock() + mock_results.lrc_filepath = "output.lrc" + mock_results.ass_filepath = "output.ass" + mock_results.video_filepath = "output.mp4" + mock_transcriber.process.return_value = mock_results + + # Run main with test arguments + with patch("sys.argv", ["lyrics-transcriber", sample_audio_file]): + main() + + # Verify transcriber was initialized and process was called + mock_transcriber_class.assert_called_once() + mock_transcriber.process.assert_called_once() + + +@patch("lyrics_transcriber.cli.main.LyricsTranscriber") +def test_main_error_handling(mock_transcriber_class, sample_audio_file): + mock_transcriber = Mock() + mock_transcriber_class.return_value = mock_transcriber + mock_transcriber.process.side_effect = Exception("Test error") + + # Run main with test arguments + with pytest.raises(SystemExit) as exc_info: + with patch("sys.argv", ["lyrics-transcriber", sample_audio_file]): + main() + + assert exc_info.value.code == 1 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..a44d783 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,17 @@ +import pytest +import logging +from pathlib import Path + +@pytest.fixture +def sample_audio_file(tmp_path): + """Create a dummy audio file for testing.""" + audio_file = tmp_path / "test_audio.mp3" + audio_file.write_bytes(b"dummy audio content") + return str(audio_file) + +@pytest.fixture +def test_logger(): + """Create a logger for testing.""" + logger = logging.getLogger("test_logger") + logger.setLevel(logging.DEBUG) + return logger \ No newline at end of file diff --git a/tests/core/__init__.py b/tests/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/core/test_controller.py b/tests/core/test_controller.py new file mode 100644 index 0000000..065b9af --- /dev/null +++ b/tests/core/test_controller.py @@ -0,0 +1,296 @@ +import pytest +from unittest.mock import Mock, patch +from lyrics_transcriber.core.controller import LyricsTranscriber, TranscriberConfig, LyricsConfig, OutputConfig +import logging + + +@pytest.fixture +def mock_lyrics_fetcher(): + return Mock() + + +@pytest.fixture +def mock_corrector(): + return Mock() + + +@pytest.fixture +def mock_output_generator(): + return Mock() + + +@pytest.fixture +def mock_whisper_transcriber(): + return Mock() + + +@pytest.fixture +def mock_audioshake_transcriber(): + return Mock() + + +@pytest.fixture +def basic_transcriber(sample_audio_file, test_logger, mock_lyrics_fetcher, mock_corrector, mock_output_generator): + return LyricsTranscriber( + audio_filepath=sample_audio_file, + artist="Test Artist", + title="Test Song", + logger=test_logger, + lyrics_fetcher=mock_lyrics_fetcher, + corrector=mock_corrector, + output_generator=mock_output_generator, + ) + + +def test_lyrics_transcriber_initialization(basic_transcriber): + assert basic_transcriber.audio_filepath is not None + assert basic_transcriber.artist == "Test Artist" + assert basic_transcriber.title == "Test Song" + assert basic_transcriber.output_prefix == "Test Artist - Test Song" + + +@patch("lyrics_transcriber.core.controller.AudioShakeTranscriber") +@patch("lyrics_transcriber.core.controller.WhisperTranscriber") +def test_transcriber_with_configs( + mock_whisper_class, + mock_audioshake_class, + sample_audio_file, + test_logger, + mock_lyrics_fetcher, + mock_corrector, + mock_output_generator, + mock_whisper_transcriber, + mock_audioshake_transcriber, +): + # Setup mock transcriber instances + mock_whisper_class.return_value = mock_whisper_transcriber + mock_audioshake_class.return_value = mock_audioshake_transcriber + + transcriber_config = TranscriberConfig(audioshake_api_token="test_token", runpod_api_key="test_key", whisper_runpod_id="test_id") + + lyrics_config = LyricsConfig(genius_api_token="test_token", spotify_cookie="test_cookie") + + output_config = OutputConfig(output_dir="test_output", cache_dir="test_cache", render_video=True) + + transcriber = LyricsTranscriber( + audio_filepath=sample_audio_file, + transcriber_config=transcriber_config, + lyrics_config=lyrics_config, + output_config=output_config, + logger=test_logger, + lyrics_fetcher=mock_lyrics_fetcher, + corrector=mock_corrector, + output_generator=mock_output_generator, + ) + + # Verify config values + assert transcriber.transcriber_config.audioshake_api_token == "test_token" + assert transcriber.lyrics_config.genius_api_token == "test_token" + assert transcriber.output_config.render_video is True + + # Verify transcribers were initialized correctly + mock_whisper_class.assert_called_once_with(logger=test_logger, runpod_api_key="test_key", endpoint_id="test_id") + mock_audioshake_class.assert_called_once_with(api_token="test_token", logger=test_logger) + + +def test_process_with_artist_and_title(basic_transcriber, mock_lyrics_fetcher): + # Setup mock returns + mock_lyrics_fetcher.fetch_lyrics.return_value = { + "lyrics": "Test lyrics", + "source": "test", + "genius_lyrics": "Test genius lyrics", + "spotify_lyrics": "Test spotify lyrics", + "spotify_lyrics_data": {"test": "data"}, + } + + # Run process + result = basic_transcriber.process() + + # Verify lyrics fetching was called + mock_lyrics_fetcher.fetch_lyrics.assert_called_once_with("Test Artist", "Test Song") + + # Verify results + assert result.lyrics_text == "Test lyrics" + assert result.lyrics_source == "test" + + +def test_process_without_artist_and_title(sample_audio_file, test_logger, mock_lyrics_fetcher, mock_corrector, mock_output_generator): + transcriber = LyricsTranscriber( + audio_filepath=sample_audio_file, + logger=test_logger, + lyrics_fetcher=mock_lyrics_fetcher, + corrector=mock_corrector, + output_generator=mock_output_generator, + ) + + result = transcriber.process() + + # Verify lyrics fetching was not called + mock_lyrics_fetcher.fetch_lyrics.assert_not_called() + + +def test_generate_outputs(basic_transcriber, mock_output_generator): + # Setup mock returns + mock_output_generator.generate_outputs.return_value = {"lrc": "test.lrc", "ass": "test.ass", "video": "test.mp4"} + + # Setup transcription data + basic_transcriber.results.transcription_corrected = {"test": "data"} + + # Run generate_outputs + basic_transcriber.generate_outputs() + + # Verify output generation was called correctly + mock_output_generator.generate_outputs.assert_called_once_with( + transcription_data={"test": "data"}, + output_prefix="Test Artist - Test Song", + audio_filepath=basic_transcriber.audio_filepath, + render_video=False, + ) + + # Verify results + assert basic_transcriber.results.lrc_filepath == "test.lrc" + assert basic_transcriber.results.ass_filepath == "test.ass" + assert basic_transcriber.results.video_filepath == "test.mp4" + + +def test_initialize_transcribers_with_no_config(basic_transcriber): + """Test transcriber initialization when no API tokens are provided""" + transcribers = basic_transcriber._initialize_transcribers() + assert len(transcribers) == 0 + + +def test_logger_initialization_without_existing_logger(sample_audio_file): + """Test that logger is properly initialized when none is provided""" + transcriber = LyricsTranscriber(audio_filepath=sample_audio_file) + assert transcriber.logger is not None + assert transcriber.logger.level == logging.DEBUG + assert len(transcriber.logger.handlers) == 1 + + +def test_transcribe_with_failed_transcriber(basic_transcriber, mock_whisper_transcriber): + """Test transcription handling when a transcriber fails""" + # Setup mock transcriber that raises an exception + mock_whisper_transcriber.transcribe.side_effect = Exception("Transcription failed") + basic_transcriber.transcribers = {"whisper": mock_whisper_transcriber} + + # Should not raise exception + basic_transcriber.transcribe() + assert basic_transcriber.results.transcription_primary is None + + +def test_correct_lyrics_with_failed_correction(basic_transcriber, mock_corrector): + """Test correction handling when correction fails""" + # Setup initial transcription data + basic_transcriber.results.transcription_primary = {"test": "data"} + mock_corrector.run_corrector.side_effect = Exception("Correction failed") + + # Should not raise exception and should use primary transcription as fallback + basic_transcriber.correct_lyrics() + assert basic_transcriber.results.transcription_corrected == {"test": "data"} + + +def test_process_with_failed_output_generation(basic_transcriber, mock_output_generator): + """Test process handling when output generation fails""" + # Setup successful transcription but failed output generation + basic_transcriber.results.transcription_corrected = {"test": "data"} + mock_output_generator.generate_outputs.side_effect = Exception("Output generation failed") + + with pytest.raises(Exception): + basic_transcriber.process() + + +def test_fetch_lyrics_with_failed_fetcher(basic_transcriber, mock_lyrics_fetcher): + """Test lyrics fetching when the fetcher fails""" + mock_lyrics_fetcher.fetch_lyrics.side_effect = Exception("Failed to fetch lyrics") + + # Should not raise exception + basic_transcriber.fetch_lyrics() + + # Verify results remain None + assert basic_transcriber.results.lyrics_text is None + assert basic_transcriber.results.lyrics_source is None + assert basic_transcriber.results.lyrics_genius is None + assert basic_transcriber.results.lyrics_spotify is None + assert basic_transcriber.results.spotify_lyrics_data is None + + +def test_fetch_lyrics_with_empty_result(basic_transcriber, mock_lyrics_fetcher): + """Test lyrics fetching when no lyrics are found""" + mock_lyrics_fetcher.fetch_lyrics.return_value = { + "lyrics": None, + "source": None, + "genius_lyrics": None, + "spotify_lyrics": None, + "spotify_lyrics_data": None, + } + + basic_transcriber.fetch_lyrics() + + # Verify empty results are handled + assert basic_transcriber.results.lyrics_text is None + assert basic_transcriber.results.lyrics_source is None + + +def test_transcribe_with_multiple_transcribers(basic_transcriber, mock_whisper_transcriber, mock_audioshake_transcriber): + """Test transcription with multiple transcribers where first one fails""" + # Setup transcribers + mock_whisper_transcriber.transcribe.side_effect = Exception("Whisper failed") + mock_audioshake_transcriber.transcribe.return_value = {"test": "audioshake_data"} + basic_transcriber.transcribers = {"whisper": mock_whisper_transcriber, "audioshake": mock_audioshake_transcriber} + + basic_transcriber.transcribe() + + # Verify AudioShake result was used as primary when Whisper failed + assert basic_transcriber.results.transcription_whisper is None + assert basic_transcriber.results.transcription_audioshake == {"test": "audioshake_data"} + assert basic_transcriber.results.transcription_primary == {"test": "audioshake_data"} + + +def test_process_with_successful_correction(basic_transcriber, mock_corrector): + """Test successful correction process""" + # Setup mock data + basic_transcriber.results.transcription_primary = {"test": "data"} + mock_corrector.run_corrector.return_value = {"test": "corrected_data"} + + # Run correction + basic_transcriber.correct_lyrics() + + # Verify correction was successful + assert basic_transcriber.results.transcription_corrected == {"test": "corrected_data"} + + +def test_transcribe_with_successful_whisper(basic_transcriber, mock_whisper_transcriber): + """Test successful whisper transcription""" + # Setup mock + mock_whisper_transcriber.transcribe.return_value = {"test": "whisper_data"} + basic_transcriber.transcribers = {"whisper": mock_whisper_transcriber} + + # Run transcription + basic_transcriber.transcribe() + + # Verify whisper results were stored + assert basic_transcriber.results.transcription_whisper == {"test": "whisper_data"} + assert basic_transcriber.results.transcription_primary == {"test": "whisper_data"} + + +def test_process_full_successful_workflow(basic_transcriber, mock_lyrics_fetcher, mock_corrector, mock_whisper_transcriber): + """Test a complete successful workflow""" + # Setup mocks + mock_lyrics_fetcher.fetch_lyrics.return_value = { + "lyrics": "Test lyrics", + "source": "test", + "genius_lyrics": "Test genius lyrics", + "spotify_lyrics": "Test spotify lyrics", + "spotify_lyrics_data": {"test": "data"} + } + basic_transcriber.transcribers = {"whisper": mock_whisper_transcriber} + mock_whisper_transcriber.transcribe.return_value = {"test": "whisper_data"} + mock_corrector.run_corrector.return_value = {"test": "corrected_data"} + + # Run full process + result = basic_transcriber.process() + + # Verify complete workflow + assert result.lyrics_text == "Test lyrics" + assert result.transcription_whisper == {"test": "whisper_data"} + assert result.transcription_corrected == {"test": "corrected_data"}