From 7f2cea665af424e1a46a6abbd87a49a7e8d03e9c Mon Sep 17 00:00:00 2001 From: llbbl Date: Mon, 1 Sep 2025 20:46:04 +0000 Subject: [PATCH] Set up comprehensive Python testing infrastructure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Poetry package management with pyproject.toml - Configure pytest with coverage reporting (80% threshold) - Create testing directory structure (unit/integration) - Add shared fixtures in conftest.py for common mocking - Set up development dependencies (pytest, pytest-cov, pytest-mock) - Create validation tests to verify infrastructure works - Update .gitignore with Python and testing artifacts - Structure code as proper auto_epp Python module 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .gitignore | 62 ++++++++ auto_epp/__init__.py | 12 ++ auto_epp/main.py | 133 ++++++++++++++++ poetry.lock | 283 +++++++++++++++++++++++++++++++++ pyproject.toml | 74 +++++++++ tests/__init__.py | 0 tests/conftest.py | 146 +++++++++++++++++ tests/integration/__init__.py | 0 tests/test_setup_validation.py | 75 +++++++++ tests/unit/__init__.py | 0 10 files changed, 785 insertions(+) create mode 100644 auto_epp/__init__.py create mode 100644 auto_epp/main.py 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 index e69de29..e07210d 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1,62 @@ +# Claude Code settings +.claude/ + +# Testing +.pytest_cache/ +.coverage +htmlcov/ +coverage.xml +.tox/ +.cache + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +.venv/ + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# IDEs +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Temporary files +*.tmp +*.temp +.tmp/ \ No newline at end of file diff --git a/auto_epp/__init__.py b/auto_epp/__init__.py new file mode 100644 index 0000000..30906d5 --- /dev/null +++ b/auto_epp/__init__.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 +""" +auto-epp: AMD CPU Energy Performance Preference Manager + +A Python script that manages the energy performance preferences (EPP) +of your AMD CPU using the AMD-Pstate driver. +""" + +from .main import main + +__version__ = "0.1.0" +__all__ = ["main"] \ No newline at end of file diff --git a/auto_epp/main.py b/auto_epp/main.py new file mode 100644 index 0000000..7d5ce91 --- /dev/null +++ b/auto_epp/main.py @@ -0,0 +1,133 @@ +#!/bin/python3 + +import os +import time +import sys +import configparser + +CONFIG_FILE = "/etc/auto-epp.conf" +DEFAULT_CONFIG = """# see available epp state by running: cat /sys/devices/system/cpu/cpu0/cpufreq/energy_performance_available_preferences +[Settings] +epp_state_for_AC=balance_performance +epp_state_for_BAT=power +""" + +def check_root(): + if os.geteuid() == 0: + return + else: + print("auto-epp must be run with root privileges.") + sys.stdout.flush() + exit(1) + +def check_driver(): + scaling_driver_path = "/sys/devices/system/cpu/cpu0/cpufreq/scaling_driver" + try: + with open(scaling_driver_path) as f: + scaling_driver = f.read()[:-1] + except: + scaling_driver = None + if scaling_driver == "amd-pstate-epp": + return + else: + print("The system not runing amd-pstate-epp") + sys.stdout.flush() + exit(1) + +def read_config(): + if not os.path.exists(CONFIG_FILE): + with open(CONFIG_FILE, 'w') as file: + file.write(DEFAULT_CONFIG) + config = configparser.ConfigParser() + config.read(CONFIG_FILE) + epp_state_for_AC = config.get("Settings", "epp_state_for_AC") + epp_state_for_BAT = config.get("Settings", "epp_state_for_BAT") + return epp_state_for_AC, epp_state_for_BAT + +def charging(): + power_supply_path = "/sys/class/power_supply/" + power_supplies = os.listdir(power_supply_path) + # sort it so AC is 'always' first + power_supplies = sorted(power_supplies) + if len(power_supplies) == 0: + return True + else: + for supply in power_supplies: + try: + with open(power_supply_path + supply + "/type") as f: + supply_type = f.read()[:-1] + if supply_type == "Mains": + # we found an AC + try: + with open(power_supply_path + supply + "/online") as f: + val = int(f.read()[:-1]) + if val == 1: + # we are definitely charging + return True + except FileNotFoundError: + # we could not find online, check next item + continue + elif supply_type == "Battery": + # we found a battery, check if its being discharged + try: + with open(power_supply_path + supply + "/status") as f: + val = str(f.read()[:-1]) + if val == "Discharging": + # we found a discharging battery + return False + except FileNotFoundError: + # could not find status, check the next item + continue + else: + # continue to next item because current is not + # "Mains" or "Battery" + continue + except FileNotFoundError: + # could not find type, check the next item + continue + # we cannot determine discharging state, assume we are on powercable + return True + +def set_governor(): + get_governor_file_path = '/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor' + try: + # get current governor + with open(get_governor_file_path) as gov_file: + cur_governor = gov_file.read()[:-1] + if cur_governor != "powersave": + print(f'Current governor "{cur_governor}" is not "powersave". Setting governor to "powersave"') + sys.stdout.flush() + + # setting governor to powersave + cpu_count = os.cpu_count() + for cpu in range(cpu_count): + governor_file_path = f'/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_governor' + with open(governor_file_path, 'w') as file: + file.write("powersave") + except: + exit(1) + +def set_epp(epp_value): + cpu_count = os.cpu_count() + for cpu in range(cpu_count): + epp_file_path = f'/sys/devices/system/cpu/cpu{cpu}/cpufreq/energy_performance_preference' + try: + with open(epp_file_path, 'w') as file: + file.write(epp_value) + except: + exit(1) + +def main(): + check_root() + check_driver() + epp_state_for_AC, epp_state_for_BAT = read_config() + while True: + set_governor() + if charging(): + set_epp(epp_state_for_AC) + else: + set_epp(epp_state_for_BAT) + time.sleep(2) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..686d901 --- /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 = "c3170df8866308fdf14e2da51f2c80319bae0fc6accacf3c4f2f634abc768fc8" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f061768 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,74 @@ +[tool.poetry] +name = "auto-epp" +version = "0.1.0" +description = "A Python script that manages the energy performance preferences (EPP) of your AMD CPU using the AMD-Pstate driver" +authors = ["jothi-prasath "] +readme = "README.md" +packages = [{include = "auto_epp", from = "."}] + +[tool.poetry.dependencies] +python = "^3.7" + +[tool.poetry.group.test.dependencies] +pytest = "^7.4.0" +pytest-cov = "^4.1.0" +pytest-mock = "^3.11.1" + +[tool.poetry.scripts] +auto-epp = "auto_epp:main" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.pytest.ini_options] +minversion = "6.0" +addopts = [ + "-ra", + "--strict-markers", + "--strict-config", + "--cov=auto_epp", + "--cov-report=term-missing", + "--cov-report=html:htmlcov", + "--cov-report=xml:coverage.xml", + "--cov-fail-under=80" +] +testpaths = ["tests"] +python_files = ["test_*.py", "*_test.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +markers = [ + "unit: Unit tests", + "integration: Integration tests", + "slow: Slow running tests" +] + +[tool.coverage.run] +source = ["auto_epp"] +omit = [ + "tests/*", + "*/tests/*", + "*/__pycache__/*", + "*/venv/*", + "*/.venv/*" +] + +[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 +skip_covered = false + +[tool.coverage.html] +directory = "htmlcov" + 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..2dbc225 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,146 @@ +""" +Shared pytest fixtures for auto-epp testing. + +This module provides common fixtures and utilities for testing +the auto-epp AMD CPU energy performance manager. +""" + +import os +import tempfile +import pytest +from unittest.mock import patch, mock_open, MagicMock + + +@pytest.fixture +def temp_dir(): + """Create a temporary directory for testing.""" + with tempfile.TemporaryDirectory() as tmpdir: + yield tmpdir + + +@pytest.fixture +def mock_config_file(): + """Mock configuration file content.""" + return """# see available epp state by running: cat /sys/devices/system/cpu/cpu0/cpufreq/energy_performance_available_preferences +[Settings] +epp_state_for_AC=balance_performance +epp_state_for_BAT=power +""" + + +@pytest.fixture +def mock_root_user(): + """Mock root user privileges.""" + with patch('os.geteuid', return_value=0): + yield + + +@pytest.fixture +def mock_non_root_user(): + """Mock non-root user.""" + with patch('os.geteuid', return_value=1000): + yield + + +@pytest.fixture +def mock_amd_driver(): + """Mock AMD pstate-epp driver.""" + with patch('builtins.open', mock_open(read_data='amd-pstate-epp')): + yield + + +@pytest.fixture +def mock_non_amd_driver(): + """Mock non-AMD driver.""" + with patch('builtins.open', mock_open(read_data='intel_pstate')): + yield + + +@pytest.fixture +def mock_charging_state(): + """Mock system charging state (AC power).""" + def mock_listdir(path): + if path == "/sys/class/power_supply/": + return ["AC0", "BAT0"] + return [] + + def mock_open_file(filename, mode='r'): + if "/type" in filename and "AC0" in filename: + return mock_open(read_data='Mains\n')() + elif "/online" in filename and "AC0" in filename: + return mock_open(read_data='1\n')() + elif "/type" in filename and "BAT0" in filename: + return mock_open(read_data='Battery\n')() + elif "/status" in filename and "BAT0" in filename: + return mock_open(read_data='Charging\n')() + return mock_open()() + + with patch('os.listdir', side_effect=mock_listdir), \ + patch('builtins.open', side_effect=mock_open_file): + yield + + +@pytest.fixture +def mock_battery_state(): + """Mock system on battery power.""" + def mock_listdir(path): + if path == "/sys/class/power_supply/": + return ["AC0", "BAT0"] + return [] + + def mock_open_file(filename, mode='r'): + if "/type" in filename and "AC0" in filename: + return mock_open(read_data='Mains\n')() + elif "/online" in filename and "AC0" in filename: + return mock_open(read_data='0\n')() + elif "/type" in filename and "BAT0" in filename: + return mock_open(read_data='Battery\n')() + elif "/status" in filename and "BAT0" in filename: + return mock_open(read_data='Discharging\n')() + return mock_open()() + + with patch('os.listdir', side_effect=mock_listdir), \ + patch('builtins.open', side_effect=mock_open_file): + yield + + +@pytest.fixture +def mock_cpu_count(): + """Mock CPU count.""" + with patch('os.cpu_count', return_value=8): + yield + + +@pytest.fixture +def mock_sys_exit(): + """Mock system exit to prevent test termination.""" + with patch('sys.exit') as mock_exit: + yield mock_exit + + +@pytest.fixture +def mock_config_parser(): + """Mock configparser for configuration testing.""" + mock_config = MagicMock() + mock_config.get.side_effect = lambda section, key: { + ('Settings', 'epp_state_for_AC'): 'balance_performance', + ('Settings', 'epp_state_for_BAT'): 'power' + }.get((section, key), 'default') + + with patch('configparser.ConfigParser', return_value=mock_config): + yield mock_config + + +@pytest.fixture +def mock_file_operations(): + """Mock file operations for testing file I/O.""" + mock_files = {} + + def mock_open_with_tracking(filename, mode='r'): + if 'w' in mode: + mock_files[filename] = mock_open()() + return mock_files[filename] + return mock_open(read_data=mock_files.get(filename, ''))() + + with patch('builtins.open', side_effect=mock_open_with_tracking): + yield mock_files \ 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..45687f2 --- /dev/null +++ b/tests/test_setup_validation.py @@ -0,0 +1,75 @@ +""" +Validation tests to ensure the testing infrastructure is set up correctly. + +These tests verify that pytest, coverage, and all fixtures work as expected. +""" + +import pytest +import sys +from unittest.mock import patch + + +def test_pytest_is_working(): + """Basic test to verify pytest is functioning.""" + assert True + + +def test_import_auto_epp_module(): + """Test that the auto_epp module can be imported.""" + import auto_epp + assert auto_epp.__version__ == "0.1.0" + assert hasattr(auto_epp, 'main') + + +def test_fixtures_are_working(temp_dir, mock_config_file): + """Test that pytest fixtures are working correctly.""" + assert temp_dir is not None + assert len(temp_dir) > 0 + assert "[Settings]" in mock_config_file + assert "epp_state_for_AC" in mock_config_file + + +@pytest.mark.unit +def test_unit_marker(): + """Test that unit marker works.""" + assert True + + +@pytest.mark.integration +def test_integration_marker(): + """Test that integration marker works.""" + assert True + + +@pytest.mark.slow +def test_slow_marker(): + """Test that slow marker works.""" + assert True + + +def test_mock_fixtures(mock_root_user, mock_amd_driver): + """Test that mock fixtures work correctly.""" + import os + + # Test root user mock + assert os.geteuid() == 0 + + # Test AMD driver mock (file read mocking) + with open('/sys/devices/system/cpu/cpu0/cpufreq/scaling_driver') as f: + content = f.read() + assert 'amd-pstate-epp' in content + + +def test_coverage_integration(): + """Test that coverage measurement is working.""" + from auto_epp.main import CONFIG_FILE + assert CONFIG_FILE == "/etc/auto-epp.conf" + + +def test_pytest_mock_dependency(): + """Test that pytest-mock is available.""" + try: + import pytest_mock + assert pytest_mock is not None + except ImportError: + pytest.fail("pytest-mock should be available") \ 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