pax_global_header00006660000000000000000000000064151540521740014516gustar00rootroot0000000000000052 comment=91f03b069fc3b40a2da9228b3ca63904c1775c25 amd-debug-tools-0.2.15/000077500000000000000000000000001515405217400145665ustar00rootroot00000000000000amd-debug-tools-0.2.15/.github/000077500000000000000000000000001515405217400161265ustar00rootroot00000000000000amd-debug-tools-0.2.15/.github/workflows/000077500000000000000000000000001515405217400201635ustar00rootroot00000000000000amd-debug-tools-0.2.15/.github/workflows/ci.yml000066400000000000000000000061221515405217400213020ustar00rootroot00000000000000name: CI on: push: pull_request: workflow_run: workflows: ["mirror"] types: - completed jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.x" - name: Build release distributions run: | python -m pip install build python -m build - name: Store the distribution packages uses: actions/upload-artifact@v4 with: name: python-package-distributions path: dist/ publish-to-pypi: name: >- Publish Python 🐍 distribution πŸ“¦ to PyPI if: startsWith(github.ref, 'refs/tags/') needs: - build runs-on: ubuntu-latest environment: name: pypi url: https://pypi.org/p/amd-debug-tools permissions: id-token: write steps: - name: Download all the dists uses: actions/download-artifact@v4 with: name: python-package-distributions path: dist/ - name: Publish distribution πŸ“¦ to PyPI uses: pypa/gh-action-pypi-publish@release/v1 github-release: name: >- Sign the Python 🐍 distribution πŸ“¦ with Sigstore and upload them to GitHub Release needs: - publish-to-pypi runs-on: ubuntu-latest permissions: contents: write # IMPORTANT: mandatory for making GitHub Releases id-token: write # IMPORTANT: mandatory for sigstore steps: - name: Download all the dists uses: actions/download-artifact@v4 with: name: python-package-distributions path: dist/ - name: Sign the dists with Sigstore uses: sigstore/gh-action-sigstore-python@v3.0.0 with: inputs: >- ./dist/*.tar.gz ./dist/*.whl - name: Create GitHub Release env: GITHUB_TOKEN: ${{ github.token }} run: >- gh release create "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --notes "" - name: Upload artifact signatures to GitHub Release env: GITHUB_TOKEN: ${{ github.token }} # Upload to GitHub Release using the `gh` CLI. # `dist/` contains the built packages, and the # sigstore-produced signatures and certificates. run: >- gh release upload "$GITHUB_REF_NAME" dist/** --repo "$GITHUB_REPOSITORY" coverage: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.x" - name: install apt deps run: | sudo apt update sudo apt install libsystemd-dev -y - name: install deps run: | pip install . - name: Generate Report run: | pip install pytest-cov pytest --cov --junitxml=junit.xml - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} - name: Upload test results to Codecov if: ${{ !cancelled() }} uses: codecov/test-results-action@v1 with: token: ${{ secrets.CODECOV_TOKEN }} amd-debug-tools-0.2.15/.github/workflows/mirror.yml000066400000000000000000000007711515405217400222250ustar00rootroot00000000000000name: 'mirror' on: push: branches: - __mirror schedule: # Run everyday at 3 AM UTC - cron: '0 3 * * *' workflow_dispatch: permissions: contents: write jobs: mirror: runs-on: ubuntu-latest name: mirror steps: - name: mirror id: mirror uses: bridgelightcloud/github-mirror-action@v3 with: origin: git://git.kernel.org/pub/scm/linux/kernel/git/superm1/amd-debug-tools.git GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} amd-debug-tools-0.2.15/.gitignore000066400000000000000000000001401515405217400165510ustar00rootroot00000000000000/.vscode *.txt __pycache__ amd*.egg-info build dist amd*.md *.html deb_dist/ *.tar.gz .coverage amd-debug-tools-0.2.15/.pre-commit-config.yaml000066400000000000000000000002261515405217400210470ustar00rootroot00000000000000default_stages: [pre-commit] repos: - repo: https://github.com/ambv/black rev: 25.1.0 hooks: - id: black exclude_types: [symlink] amd-debug-tools-0.2.15/.vscode/000077500000000000000000000000001515405217400161275ustar00rootroot00000000000000amd-debug-tools-0.2.15/.vscode/settings.json000066400000000000000000000064041515405217400206660ustar00rootroot00000000000000{ "cSpell.words": [ "abled", "acpica", "alarmtimer", "ASIC", "aspm", "ASPM", "asus", "ASUS", "cmos", "CPPC", "cpuid", "cpuidle", "cpus", "crashkernel", "cryptdevice", "cryptkey", "cstates", "cysystemd", "dbus", "dmar", "dmcub", "Dmcub", "DMCUB", "DMUB", "dpia", "DPIA", "DSDT", "earlycon", "earlyprintk", "edid", "edids", "ENDC", "ertm", "evmisc", "FACP", "geteuid", "gobject", "gpiolib", "gpios", "guids", "hpet", "HPET", "hsmp", "Hsmp", "HSMP", "hwirq", "hwsleep", "Iasl", "idlemask", "Iommu", "irqs", "IVRS", "kconfig", "klass", "kmajor", "kminor", "kwin", "levelname", "logf", "logind", "luks", "minv", "modalias", "netdev", "netroot", "nfsaddrs", "nfsroot", "noplymouth", "notsupported", "nowatchdog", "ostree", "powerprofilesctl", "powersave", "ppfeaturemask", "prefcore", "prereq", "propf", "reques", "resumeflags", "rhgb", "rootflags", "rootfstype", "roothash", "sadm", "seeked", "showopts", "sscanf", "SSDT", "subleaf", "sysfs", "tmpd", "UBTC", "uefi", "uevent", "Unserviced", "usec", "userspace", "usrflags", "usrfstype", "virt", "wakealarm", "xhci", "Xhci", "zswap" ], "python.testing.unittestArgs": [ "-v", "-s", "./src", "-p", "test_*.py" ], "python.testing.pytestEnabled": true, "python.testing.unittestEnabled": false, "cSpell.ignoreWords": [ "barplot", "intf", "line", "message", "showindex", "tablefmt", "xlabel", "xticks", "ylabel", "yscale" ] } amd-debug-tools-0.2.15/LICENSE000066400000000000000000000020521515405217400155720ustar00rootroot00000000000000Copyright (c) 2025 Advanced Micro Devices Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. amd-debug-tools-0.2.15/Makefile000066400000000000000000000002451515405217400162270ustar00rootroot00000000000000build: python3 -m build check: pytest coverage: coverage run -m unittest coverage report clean: rm -rf build src/*.egg-info build dist amd*.txt amd*md amd*html amd-debug-tools-0.2.15/README.md000066400000000000000000000037611515405217400160540ustar00rootroot00000000000000# Helpful tools for debugging AMD Zen systems [![codecov](https://codecov.io/github/superm1/amd-debug-tools/graph/badge.svg?token=Z9WTBZADGT)](https://codecov.io/github/superm1/amd-debug-tools) [![PyPI](https://img.shields.io/pypi/v/amd-debug-tools.svg)](https://pypi.org/project/amd-debug-tools/) This repository hosts open tools that are useful for debugging issues on AMD systems. ## Installation ### Distro (Arch) `amd-debug-tools` has been [packaged for Arch Linux](https://archlinux.org/packages/extra/any/amd-debug-tools/) (and derivatives). You can install it using: pacman -Sy amd-debug-tools ### Using a python wheel (Generic) It is suggested to install tools in a virtual environment either using `pipx` or `python3 -m venv`. #### From PyPI `amd-debug-tools` is distributed as a python wheel, which is a binary package format for Python. To install from PyPI, run the following command: pipx install amd-debug-tools ### From source To build the package from source, you will need to the `python3-build` package natively installed by your distribution package manager. Then you can generate and install a wheel by running the following commands: python3 -m build pipx install dist/amd-debug-tools-*.whl ### Ensuring path If you have not used a `pipx` environment before, you may need to run the following command to set up the environment: pipx ensurepath This will add the `pipx` environment to your path. ## Running in-tree Documentation about running directly from a git checkout is available [here](https://github.com/superm1/amd-debug-tools/blob/master/docs/in-tree.md). ## Tools Each tool has its own individual documentation page: * [amd-s2idle](https://github.com/superm1/amd-debug-tools/blob/master/docs/amd-s2idle.md) * [amd-bios](https://github.com/superm1/amd-debug-tools/blob/master/docs/amd-bios.md) * [amd-pstate](https://github.com/superm1/amd-debug-tools/blob/master/docs/amd-pstate.md) * [amd-ttm](https://github.com/superm1/amd-debug-tools/blob/master/docs/amd-ttm.md) amd-debug-tools-0.2.15/amd_bios.py000077700000000000000000000000001515405217400216522src/launcher.pyustar00rootroot00000000000000amd-debug-tools-0.2.15/amd_pstate.py000077700000000000000000000000001515405217400222162src/launcher.pyustar00rootroot00000000000000amd-debug-tools-0.2.15/amd_s2idle.py000077700000000000000000000000001515405217400221002src/launcher.pyustar00rootroot00000000000000amd-debug-tools-0.2.15/amd_ttm.py000077700000000000000000000000001515405217400215222src/launcher.pyustar00rootroot00000000000000amd-debug-tools-0.2.15/docs/000077500000000000000000000000001515405217400155165ustar00rootroot00000000000000amd-debug-tools-0.2.15/docs/amd-bios.md000066400000000000000000000014761515405217400175430ustar00rootroot00000000000000# BIOS log parser `amd-bios` is a a tool that can be used to enable or disable BIOS AML debug logging -and to parse a kernel log that contains BIOS logs. ## `amd-bios trace` Modify BIOS AML trace debug logging. One of the following arguments must be set for this command: --enable Enable BIOS AML tracing --disable Disable BIOS AML tracing The following optional arguments are supported for this command: --tool-debug Enable tool debug logging ## `amd-bios parse` Parses a kernel log that contains BIOS AML debug logging and produces a report. The following optional arguments are supported for this command: --input INPUT Optional input file to parse --tool-debug Enable tool debug logging ## `amd-bios --version` This will print the version of the tool and exit. amd-debug-tools-0.2.15/docs/amd-pstate.md000066400000000000000000000003321515405217400200750ustar00rootroot00000000000000# AMD P-State issue triage tool `amd-pstate` is a tool used for identification of issues with amd-pstate. It will capture some state from the system as well as from the machine specific registers that amd-pstate uses. amd-debug-tools-0.2.15/docs/amd-s2idle.md000066400000000000000000000064241515405217400177670ustar00rootroot00000000000000# s2idle debugging tool `amd-s2idle` is a tool used for analyzing the entry and exit of the s2idle state of a Linux system. It is intended to use with Linux kernel 6.1 or later and works by hooking into dynamic debugging messages and events that are generated by the kernel. For analysis of power consumption issues it can be hooked into `systemd` to run a command to capture data right before and after the system enters and exits the s2idle state. 4 high level commands are supported. ## `amd-s2idle install` This will install the systemd hook so that data will be captured before and after the system enters and exits the s2idle state. This will also install a bash completion script that can be used for other commands. **NOTE:** This command is only supported when run from a venv. ## `amd-s2idle uninstall` This will uninstall the systemd hook and remove the bash completion script. **NOTE:** This command is only supported when run from a venv. ## `amd-s2idle test` This will run a suspend cycle with a timer based wakeup and capture relevant data into a database and produce a report. This can also be used to run multiple cycles. The following optional arguments are supported for this command: --count COUNT Number of cycles to run --duration DURATION Duration of the cycle in seconds --wait WAIT Time to wait before starting the cycle in seconds --format FORMAT Format of the report to produce (html, txt or md) --report-file File to write the report to --force Run a test cycle even if the system fails to pass prerequisite checks --random Run sleep cycles for random durations and waits, using the --duration and --wait arguments as an upper bound --logind Use logind to suspend the system --tool-debug Enable debug logging --bios-debug Enable BIOS debug logging instead of notify logging If the tool is launched with an environment that can call `xdg-open`, the report will be opened in a browser. ## `amd-s2idle report` This will produce a report from the data captured by the `test` command and/or from the systemd hook. The report will default to 60 days of data. The following optional arguments are supported for this command: --since SINCE Date to start the report from --until UNTIL Date to end the report at --format FORMAT Format of the report to produce (html, txt or md) --report-file File to write the report to --tool-debug Enable tool debug logging --report-debug --no-report-debug Include debug messages in report (WARNING: can significantly increase report size) If the tool is launched with an environment that can call `xdg-open`, the report will be opened in a browser. ## `amd-s2idle --version` This will print the version of the tool and exit. ## Debug output All commands support the `--tool-debug` argument which will enable extra debug output. This is often needed for debugging issues with a particular cycle. **NOTE:** enabling debug output significantly increases the size of the report. It's suggested that you use `--since` and `--until` to focus on the cycles that you are interested in.amd-debug-tools-0.2.15/docs/amd-ttm.md000066400000000000000000000026721515405217400174120ustar00rootroot00000000000000# Translation Table Manager (TTM) page setting tool `amd-ttm` is a tool used for managing the TTM memory settings on AMD systems. It manipulates the amount of memory allocated for the TTM. This amount can be increased or decreased by changing the kernel’s Translation Table Manager (TTM) page setting available at `/sys/module/ttm/parameters/pages_limit`. ## Querying current TTM settings Running the tool with no arguments will display the current TTM settings. ``` ❯ amd-ttm πŸ’» Current TTM pages limit: 16469033 pages (62.82 GB) πŸ’» Total system memory: 125.65 GB ``` ## Setting new TTM value Setting a new TTM page size is done by using the `--set` argument with the new limit (in GB). The system must be rebooted for it to take effect and you will be prompted to do this automatically. ``` ❯ amd-ttm --set 100 🐧 Successfully set TTM pages limit to 26214400 pages (100.00 GB) 🐧 Configuration written to /etc/modprobe.d/ttm.conf β—‹ NOTE: You need to reboot for changes to take effect. Would you like to reboot the system now? (y/n): y ``` ## Clearing the TTM value To revert back to the kernel defaults, run the tool with the `--clear` argument. The system must be rebooted for it to take effect and you will be prompted to do this automatically. The kernel default (at the time of writing) is system memory / 2. ``` ❯ amd-ttm --clear 🐧 Configuration /etc/modprobe.d/ttm.conf removed Would you like to reboot the system now? (y/n): y ```amd-debug-tools-0.2.15/docs/in-tree.md000066400000000000000000000005431515405217400174050ustar00rootroot00000000000000# Running in-tree If you want to run the tools in tree, you need to make sure that distro dependencies that would normally install into a venv are installed. This can be done by running: ./install_deps.py After dependencies are installed, you can run the tools by running: ./amd_s2idle.py ./amd_bios.py ./amd_pstate.py ./amd_ttm.py amd-debug-tools-0.2.15/install_deps.py000077700000000000000000000000001515405217400225562src/launcher.pyustar00rootroot00000000000000amd-debug-tools-0.2.15/psr.py000077500000000000000000000051161515405217400157520ustar00rootroot00000000000000#!/usr/bin/python3 # SPDX-License-Identifier: MIT """PSR identification script for AMD systems""" import sys import os PSR_SUPPORT = { 0: "PSR Unsupported", 1: "PSR 1", 2: "PSR 2 (eDP 1.4)", 3: "PSR 2 with Y coordinates (eDP 1.4a)", 4: "PSR 2 with Y coordinates (eDP 1.4b or eDP 1.5)", } TCON = {0x001CF8: "Parade"} def decode_psr_support(f): f.seek(0x70) v = int.from_bytes(f.read(1), "little") print("β—‹ %s [%d]" % (PSR_SUPPORT[v], v)) def get_id_string(f): f.seek(0x400) oui = f.read(3) id = f.read(2) f.seek(0x40F) resv_40f = f.read(1) v = int.from_bytes(oui, "big") if v in TCON: oui_str = TCON[v] else: oui_str = "-".join("{:02x}".format(c) for c in oui) print("β—‹ Sink OUI: %s" % oui_str) print("β—‹ resv_40f: " + ":".join("{:02x}".format(c) for c in resv_40f)) print("β—‹ ID String: " + "-".join("{:02x}".format(c) for c in reversed(id))) def get_psr_error(f): f.seek(0x2006) err = f.read(3) print("β—‹ PSR Status: " + "-".join("{:02x}".format(c) for c in err)) def get_dmcub(): base = "/sys/kernel/debug/dri" for num in range(0, 3): fw_info = os.path.join(base, "%s" % num, "amdgpu_firmware_info") if not os.path.exists(fw_info): continue with open(fw_info, "r") as f: for line in f.read().split("\n"): if "DMCUB" in line: print( "DRI device {device} DMCUB F/W version: {version}".format( device=num, version=line.split()[-1] ) ) def discover_gpu(): gpus = [] try: from pyudev import Context except ModuleNotFoundError: sys.exit("Missing pyudev, please install") context = Context() for dev in context.list_devices(subsystem="drm_dp_aux_dev"): if not "eDP" in dev.sys_path: continue gpus += [dev.device_node] return gpus if __name__ == "__main__": gpus = discover_gpu() if not gpus: sys.exit("failed to find drm_dp_aux_dev") get_dmcub() for gpu in gpus: try: with open(gpu, "rb") as f: try: decode_psr_support(f) get_id_string(f) get_psr_error(f) except OSError: print( "Could not read DPCD, skipping. If the panel is off, please turn on and try again." ) continue except PermissionError: sys.exit("run as root") amd-debug-tools-0.2.15/pyproject.toml000066400000000000000000000022261515405217400175040ustar00rootroot00000000000000[build-system] requires = ["setuptools>=80", "setuptools-scm>=8"] build-backend = "setuptools.build_meta" [tool.setuptools] package-dir = {"" = "src"} [tool.setuptools.package-data] amd_debug = ["s2idle-hook"] "amd_debug.templates" = ["*"] "amd_debug.bash" = ["amd-s2idle"] [tool.coverage.run] branch = true source = ["src"] omit = ["src/launcher.py"] [tool.setuptools_scm] [project] name = "amd-debug-tools" authors = [{ name = "Mario Limonciello", email = "superm1@kernel.org" }] description = "debug tools for AMD systems" readme = "README.md" requires-python = ">=3.7" classifiers = [ "Programming Language :: Python :: 3", "Operating System :: POSIX :: Linux", ] dependencies = [ "dbus-fast", "pyudev", "packaging", "pandas", "jinja2", "tabulate", "seaborn", "cysystemd", "Jinja2", "matplotlib", "seaborn", ] dynamic = ["version"] license = "MIT" [project.urls] "Homepage" = "https://web.git.kernel.org/pub/scm/linux/kernel/git/superm1/amd-debug-tools.git/" [project.scripts] amd-s2idle = "amd_debug:amd_s2idle" amd-bios = "amd_debug:amd_bios" amd-pstate = "amd_debug:amd_pstate" amd-ttm = "amd_debug:amd_ttm" amd-debug-tools-0.2.15/src/000077500000000000000000000000001515405217400153555ustar00rootroot00000000000000amd-debug-tools-0.2.15/src/amd_debug/000077500000000000000000000000001515405217400172645ustar00rootroot00000000000000amd-debug-tools-0.2.15/src/amd_debug/__init__.py000066400000000000000000000023441515405217400214000ustar00rootroot00000000000000# SPDX-License-Identifier: MIT def amd_s2idle(): """Launch the amd-s2idle tool.""" from . import s2idle # pylint: disable=import-outside-toplevel return s2idle.main() def amd_bios(): """Launch the amd-bios tool.""" from . import bios # pylint: disable=import-outside-toplevel return bios.main() def amd_pstate(): """Launch the amd-pstate tool.""" from . import pstate # pylint: disable=import-outside-toplevel return pstate.main() def amd_ttm(): """Launch the amd-ttm tool.""" from . import ttm # pylint: disable=import-outside-toplevel return ttm.main() def install_dep_superset(): """Install all superset dependencies.""" from . import installer # pylint: disable=import-outside-toplevel return installer.install_dep_superset() def launch_tool(tool_name): """Launch a tool with the given name and arguments.""" tools = { "amd_s2idle.py": amd_s2idle, "amd_bios.py": amd_bios, "amd_pstate.py": amd_pstate, "amd_ttm.py": amd_ttm, "install_deps.py": install_dep_superset, } if tool_name in tools: return tools[tool_name]() else: print(f"\033[91mUnknown exe: {tool_name}\033[0m") return 1 amd-debug-tools-0.2.15/src/amd_debug/acpi.py000066400000000000000000000061061515405217400205550ustar00rootroot00000000000000# SPDX-License-Identifier: MIT import os import logging from amd_debug.common import BIT, read_file ACPI_METHOD = "M460" def search_acpi_tables(pattern): """Search for a pattern in ACPI tables""" p = os.path.join("/", "sys", "firmware", "acpi", "tables") for fn in os.listdir(p): if not fn.startswith("SSDT") and not fn.startswith("DSDT"): continue fp = os.path.join(p, fn) with open(fp, "rb") as file: content = file.read() if pattern.encode() in content: return True return False class AcpicaTracer: """Class for ACPI tracing""" def __init__(self): self.acpi_base = os.path.join("/", "sys", "module", "acpi", "parameters") keys = [ "trace_debug_layer", "trace_debug_level", "trace_method_name", "trace_state", ] self.original = {} self.supported = False for key in keys: fname = os.path.join(self.acpi_base, key) if not os.path.exists(fname): logging.debug("ACPI Notify() debugging not available") return v = read_file(fname) if v and v != "(null)": self.original[key] = v self.supported = True def _write_expected(self, expected): for key, value in expected.items(): p = os.path.join(self.acpi_base, key) if isinstance(value, int): t = str(int(value)) else: t = value with open(p, "w", encoding="utf-8") as w: w.write(t) def trace_notify(self): """Trace notify events""" if not self.supported: return False expected = { "trace_debug_layer": BIT(2), "trace_debug_level": BIT(2), "trace_state": "enable", } self._write_expected(expected) logging.debug("Enabled ACPI debugging for ACPI_LV_INFO/ACPI_EVENTS") return True def trace_bios(self): """Trace BIOS events""" if not self.supported: return False if not search_acpi_tables(ACPI_METHOD): logging.debug( "will not work on this system: ACPI tables do not contain %s", ACPI_METHOD, ) return False expected = { "trace_debug_layer": BIT(7), "trace_debug_level": BIT(4), "trace_method_name": f"\\{ACPI_METHOD}", "trace_state": "method", } self._write_expected(expected) logging.debug("Enabled ACPI debugging for BIOS") return True def disable(self): """Disable ACPI tracing""" if not self.supported: return False expected = { "trace_state": "disable", } self._write_expected(expected) return True def restore(self): """Restore original ACPI tracing settings""" if not self.supported: return False self._write_expected(self.original) return True amd-debug-tools-0.2.15/src/amd_debug/bash/000077500000000000000000000000001515405217400202015ustar00rootroot00000000000000amd-debug-tools-0.2.15/src/amd_debug/bash/amd-s2idle000066400000000000000000000021661515405217400220520ustar00rootroot00000000000000_cmd_list=( 'install' 'uninstall' 'test' 'report' 'version' ) _general_opts=( ) _report_opts=( '--since' '--until' '--report-file' '--format' '--report-debug' ) _test_opts=( '--wait' '--count' '--duration' '--report-file' '--format' '--force' '--logind' '--tool-debug' '--report-debug' ) _format_opts=( 'md' 'html' 'txt' ) _show_test_opts() { COMPREPLY+=( $(compgen -W '${_test_opts[@]}' -- "$cur") ) } _show_report_opts() { COMPREPLY+=( $(compgen -W '${_report_opts[@]}' -- "$cur") ) } _show_format_completion() { COMPREPLY+=( $(compgen -W '${_format_opts[@]}' -- "$cur") ) } _amd_s2idle() { local cur prev command arg args COMPREPLY=() _get_comp_words_by_ref cur prev _get_first_arg _count_args case $prev in --log|--duration|--count|--wait|--since|--report-file) return 0 ;; --format) _show_format_completion return 0 ;; esac case $arg in test) _show_test_opts ;; report) _show_report_opts ;; *) #find first command if [[ "$args" = "1" ]]; then COMPREPLY=( $(compgen -W '${_cmd_list[@]}' -- "$cur") ) fi ;; esac return 0 } complete -F _amd_s2idle amd-s2idle amd-debug-tools-0.2.15/src/amd_debug/battery.py000066400000000000000000000060371515405217400213160ustar00rootroot00000000000000# SPDX-License-Identifier: MIT from pyudev import Context def _get_property(prop, key, fallback="") -> str: """Get the property from the given key""" try: return prop.get(key, fallback) except UnicodeDecodeError: return "" class Batteries: def __init__(self) -> None: self.pyudev = Context() def _get_battery(self, name) -> object: """Get the battery device object for the given name""" for dev in self.pyudev.list_devices( subsystem="power_supply", POWER_SUPPLY_TYPE="Battery" ): if not "PNP0C0A" in dev.device_path: continue desc = _get_property(dev.properties, "POWER_SUPPLY_NAME", "Unknown") if desc != name: continue return dev return None def get_batteries(self) -> list: """Get a list of battery names on the system""" names = [] for dev in self.pyudev.list_devices( subsystem="power_supply", POWER_SUPPLY_TYPE="Battery" ): if not "PNP0C0A" in dev.device_path: continue names.append(_get_property(dev.properties, "POWER_SUPPLY_NAME", "Unknown")) return names def get_energy_unit(self, name) -> str: """Get the energy unit for the given battery name""" dev = self._get_battery(name) if not dev: return "" energy = _get_property(dev.properties, "POWER_SUPPLY_ENERGY_NOW") if energy: return "Β΅Wh" return "Β΅Ah" def get_energy(self, name) -> int: """Get the current energy for the given battery name""" dev = self._get_battery(name) if not dev: return "" energy = _get_property(dev.properties, "POWER_SUPPLY_ENERGY_NOW") if not energy: energy = _get_property(dev.properties, "POWER_SUPPLY_CHARGE_NOW") return energy def get_energy_full(self, name) -> int: """Get the energy when full for the given battery name""" dev = self._get_battery(name) if not dev: return "" energy = _get_property(dev.properties, "POWER_SUPPLY_ENERGY_FULL") if not energy: energy = _get_property(dev.properties, "POWER_SUPPLY_CHARGE_FULL") return energy def get_description_string(self, name) -> str: """Get a description string for the given battery name""" dev = self._get_battery(name) if not dev: return "" man = _get_property(dev.properties, "POWER_SUPPLY_MANUFACTURER", "") model = _get_property(dev.properties, "POWER_SUPPLY_MODEL_NAME", "") full = self.get_energy_full(name) full_design = _get_property(dev.properties, "POWER_SUPPLY_ENERGY_FULL_DESIGN") if not full_design: full_design = _get_property( dev.properties, "POWER_SUPPLY_CHARGE_FULL_DESIGN" ) percent = float(full) / int(full_design) return f"Battery {name} ({man} {model}) is operating at {percent:.2%} of design" amd-debug-tools-0.2.15/src/amd_debug/bios.py000066400000000000000000000076661515405217400206110ustar00rootroot00000000000000# SPDX-License-Identifier: MIT """s2idle analysis tool""" import argparse import re import sys from amd_debug.common import ( AmdTool, fatal_error, get_log_priority, minimum_kernel, print_color, relaunch_sudo, show_log_info, version, ) from amd_debug.kernel import get_kernel_log, sscanf_bios_args from amd_debug.acpi import AcpicaTracer class AmdBios(AmdTool): """ AmdBios is a class which fetches the BIOS events from kernel logs. """ def __init__(self, inf, debug): log_prefix = "bios" if debug else None super().__init__(log_prefix) self.kernel_log = get_kernel_log(inf) def set_tracing(self, enable): """Run the action""" relaunch_sudo() if not minimum_kernel(6, 16): print_color( "Support for BIOS debug logging was merged in mainline 6.16, " "this tool may not work correctly unless support is manually " "backported", "🚦", ) tracer = AcpicaTracer() ret = tracer.trace_bios() if enable else tracer.disable() if ret: action = "enabled" if enable else "disabled" print_color(f"Set BIOS tracing to {action}", "βœ…") else: fatal_error( "BIOS tracing not supported, please check your kernel for CONFIG_ACPI_DEBUG" ) return True def _analyze_kernel_log_line(self, line, priority): """Analyze a line from the kernel log""" bios_args = sscanf_bios_args(line) if bios_args: if isinstance(bios_args, str): print_color(bios_args, "πŸ–΄") else: return else: # strip timestamp t = re.sub(r"^\[\s*\d+\.\d+\]", "", line).strip() print_color(t, get_log_priority(priority)) def run(self): """Exfiltrate from the kernel log""" self.kernel_log.process_callback(self._analyze_kernel_log_line) return True def parse_args(): """Parse command line arguments""" parser = argparse.ArgumentParser( description="Parse a combined kernel/BIOS log.", ) subparsers = parser.add_subparsers(help="Possible commands", dest="command") parse_cmd = subparsers.add_parser( "parse", help="Parse log for kernel and BIOS messages" ) parse_cmd.add_argument( "--input", help="Optional input file to parse", ) parse_cmd.add_argument( "--tool-debug", action="store_true", help="Enable tool debug logging", ) trace_cmd = subparsers.add_parser("trace", help="Enable or disable tracing") trace_cmd.add_argument( "--enable", action="store_true", help="Enable BIOS AML tracing", ) trace_cmd.add_argument( "--disable", action="store_true", help="Disable BIOS AML tracing", ) trace_cmd.add_argument( "--tool-debug", action="store_true", help="Enable tool debug logging", ) parser.add_argument( "--version", action="store_true", help="Show version information" ) if len(sys.argv) == 1: parser.print_help(sys.stderr) sys.exit(1) args = parser.parse_args() if args.command == "trace": if args.enable and args.disable: sys.exit("can't set both enable and disable") if not args.enable and not args.disable: sys.exit("must set either enable or disable") return args def main() -> None | int: """Main function""" args = parse_args() ret = False if args.command == "trace": app = AmdBios(None, args.tool_debug) ret = app.set_tracing(True if args.enable else False) elif args.command == "parse": app = AmdBios(args.input, args.tool_debug) ret = app.run() elif args.version: print(version()) show_log_info() if ret is False: return 1 return amd-debug-tools-0.2.15/src/amd_debug/common.py000066400000000000000000000314041515405217400211300ustar00rootroot00000000000000# SPDX-License-Identifier: MIT """ This module contains common utility functions and classes for various amd-debug-tools. """ import asyncio import importlib.metadata import logging import os import platform import time import struct import subprocess import sys from ast import literal_eval from datetime import date, timedelta class Colors: """Colors for the terminal""" DEBUG = "\033[90m" HEADER = "\033[95m" OK = "\033[94m" WARNING = "\033[32m" FAIL = "\033[91m" ENDC = "\033[0m" UNDERLINE = "\033[4m" def read_file(fn) -> str: """Read a file and return the contents""" with open(fn, "r", encoding="utf-8") as r: return r.read().strip() def compare_file(fn, expect) -> bool: """Compare a file to an expected string""" return read_file(fn) == expect def get_group_color(group) -> str: """Get the color for a group""" if group == "🚦": color = Colors.WARNING elif group == "πŸ—£οΈ": color = Colors.HEADER elif any(mk in group for mk in ["πŸ’―", "🚫"]): color = Colors.UNDERLINE elif any(mk in group for mk in ["🦟", "πŸ–΄"]): color = Colors.DEBUG elif any(mk in group for mk in ["❌", "πŸ‘€"]): color = Colors.FAIL elif any(mk in group for mk in ["βœ…", "πŸ”‹", "🐧", "πŸ’»", "β—‹", "πŸ’€", "πŸ₯±"]): color = Colors.OK else: color = group return color def print_color(message, group) -> None: """Print a message with a color""" prefix = f"{group} " suffix = Colors.ENDC color = get_group_color(group) if color == group: prefix = "" log_txt = f"{prefix}{message}".strip() if any(c in color for c in [Colors.OK, Colors.HEADER, Colors.UNDERLINE]): logging.info(log_txt) elif color == Colors.WARNING: logging.warning(log_txt) elif color == Colors.FAIL: logging.error(log_txt) else: logging.debug(log_txt) if "TERM" in os.environ and os.environ["TERM"] == "dumb": suffix = "" color = "" print(f"{prefix}{color}{message}{suffix}") def colorize_choices(choices, default) -> str: """Output a list of choices with colors, where the default is highlighted""" if default not in choices: raise ValueError(f"Default choice '{default}' not in choices") choices = [c for c in choices if c != default] choices = [f"{Colors.OK}{default}{Colors.ENDC}"] + choices return ", ".join(choices) def fatal_error(message): """Prints a fatal error message and exits""" _configure_log(None) print_color(message, "πŸ‘€") sys.exit(1) def apply_prefix_wrapper(header, message): """Apply a prefix to wrap a newline delimitted message""" s = f"{header.strip()}\n" lines = message.strip().split("\n") for i, line in enumerate(lines): line = line.strip() if not line: continue if i == len(lines) - 1: s += f"└─ {line}\n" continue s += f"β”‚ {line}\n" return s def show_log_info(): """Show log information""" logger = logging.getLogger() for handler in logger.handlers: if isinstance(handler, logging.FileHandler): filename = handler.baseFilename if filename != "/dev/null": print(f"Debug logs are saved to: {filename}") def _configure_log(prefix) -> str: """Configure logging for the tool""" if len(logging.root.handlers) > 0: return if prefix: user = os.environ.get("SUDO_USER") home = os.path.expanduser(f"~{user if user else ''}") path = os.environ.get("XDG_DATA_HOME") or os.path.join( home, ".local", "share", "amd-debug-tools" ) os.makedirs(path, exist_ok=True) log = os.path.join( path, f"{prefix}-{date.today()}.txt", ) if not os.path.exists(log): with open(log, "w", encoding="utf-8") as f: f.write("") if "SUDO_UID" in os.environ: os.chown(path, int(os.environ["SUDO_UID"]), int(os.environ["SUDO_GID"])) os.chown(log, int(os.environ["SUDO_UID"]), int(os.environ["SUDO_GID"])) level = logging.DEBUG else: log = "/dev/null" level = logging.WARNING # for saving a log file for analysis logging.basicConfig( format="%(asctime)s %(levelname)s:\t%(message)s", filename=log, level=level, ) return log def check_lockdown(): """Check if the system is in lockdown""" fn = os.path.join("/", "sys", "kernel", "security", "lockdown") if not os.path.exists(fn): return False lockdown = read_file(fn) if lockdown.split()[0] != "[none]": return lockdown return False def print_temporary_message(msg) -> int: """Print a temporary message to the console""" print(msg, end="\r", flush=True) return len(msg) def clear_temporary_message(msg_len) -> None: """Clear a temporary message from the console""" print(" " * msg_len, end="\r") def run_countdown(action, t) -> bool: """Run a countdown timer""" msg = "" if t < 0: return False if t == 0: return True while t > 0: msg = f"{action} in {timedelta(seconds=t)}" print_temporary_message(msg) time.sleep(1) t -= 1 clear_temporary_message(len(msg)) return True def get_distro() -> str: """Get the distribution name""" distro = "unknown" if os.path.exists("/etc/os-release"): with open("/etc/os-release", "r", encoding="utf-8") as f: for line in f: if line.startswith("ID="): return line.split("=")[1].strip().strip('"') if os.path.exists("/etc/arch-release"): return "arch" elif os.path.exists("/etc/fedora-release"): return "fedora" elif os.path.exists("/etc/debian_version"): return "debian" return distro def get_pretty_distro() -> str: """Get the pretty distribution name""" distro = "Unknown" if os.path.exists("/etc/os-release"): with open("/etc/os-release", "r", encoding="utf-8") as f: for line in f: if line.startswith("PRETTY_NAME="): distro = line.split("=")[1].strip().strip('"') break return distro def bytes_to_gb(bytes_value): """Convert bytes to GB""" return bytes_value * 4096 / (1024 * 1024 * 1024) def gb_to_pages(gb_value): """Convert GB into bytes""" return int(gb_value * (1024 * 1024 * 1024) / 4096) def reboot(): """Reboot the system""" async def reboot_dbus_fast(): """Reboot using dbus-fast""" try: from dbus_fast.aio import ( # pylint: disable=import-outside-toplevel MessageBus, ) from dbus_fast import BusType # pylint: disable=import-outside-toplevel bus = await MessageBus(bus_type=BusType.SYSTEM).connect() introspection = await bus.introspect( "org.freedesktop.login1", "/org/freedesktop/login1" ) proxy_obj = bus.get_proxy_object( "org.freedesktop.login1", "/org/freedesktop/login1", introspection ) interface = proxy_obj.get_interface("org.freedesktop.login1.Manager") await interface.call_reboot(True) except ImportError: return False return True def reboot_dbus(): """Reboot using python-dbus""" try: import dbus # pylint: disable=import-outside-toplevel bus = dbus.SystemBus() obj = bus.get_object("org.freedesktop.login1", "/org/freedesktop/login1") intf = dbus.Interface(obj, "org.freedesktop.login1.Manager") intf.Reboot(True) except ImportError: return False return True loop = asyncio.get_event_loop() result = loop.run_until_complete(reboot_dbus_fast()) if not result: return reboot_dbus() return True def get_system_mem(): """Get the total system memory in GB using /proc/meminfo""" with open(os.path.join("/", "proc", "meminfo"), "r", encoding="utf-8") as f: for line in f: if line.startswith("MemTotal:"): # MemTotal line format: "MemTotal: 16384516 kB" # Extract the number and convert from kB to GB mem_kb = int(line.split()[1]) return mem_kb / (1024 * 1024) raise ValueError("Could not find MemTotal in /proc/meminfo") def is_root() -> bool: """Check if the user is root""" return os.geteuid() == 0 def BIT(num): # pylint: disable=invalid-name """Return a bit shifted value""" return 1 << num def get_log_priority(num): """Maps an integer debug level to a priority type""" if num: try: num = int(num) except ValueError: return num if num == 7: return "🦟" elif num == 4: return "🚦" elif num <= 3: return "❌" return "β—‹" def minimum_kernel(major, minor) -> bool: """Checks if the kernel version is at least major.minor""" ver = platform.uname().release.split(".") kmajor = int(ver[0]) kminor = int(ver[1]) if kmajor > int(major): return True if kmajor < int(major): return False return kminor >= int(minor) def systemd_in_use() -> bool: """Check if systemd is in use""" # Check if /proc/1/comm exists and read its contents init_daemon = read_file("/proc/1/comm") return init_daemon == "systemd" def get_property_pyudev(properties, key, fallback=""): """Get a property from a udev device""" try: return properties.get(key, fallback) except UnicodeDecodeError: return "" def find_ip_version(base_path, kind, hw_ver) -> bool: """Determine if an IP version is present on the system""" b = os.path.join(base_path, "ip_discovery", "die", "0", kind, "0") for key, expected_value in hw_ver.items(): p = os.path.join(b, key) if not os.path.exists(p): return False v = int(read_file(p)) if v != expected_value: return False return True def read_msr(msr, cpu): """Read a Model-Specific Register (MSR) value from the CPU.""" p = f"/dev/cpu/{cpu}/msr" if not os.path.exists(p) and is_root(): os.system("modprobe msr") try: f = os.open(p, os.O_RDONLY) except OSError as exc: raise PermissionError from exc try: os.lseek(f, msr, os.SEEK_SET) val = struct.unpack("Q", os.read(f, 8))[0] except OSError as exc: raise PermissionError from exc finally: os.close(f) return val def relaunch_sudo() -> None: """Relaunch the script with sudo if not already running as root""" if not is_root(): logging.debug("Relaunching with sudo") env_vars = [] for var in ["DISPLAY", "WAYLAND_DISPLAY", "XAUTHORITY", "XDG_RUNTIME_DIR", "DBUS_SESSION_BUS_ADDRESS", "XDG_SESSION_TYPE"]: value = os.environ.get(var) if value: env_vars.append(f"{var}={value}") if env_vars: sudo_cmd = ["sudo"] + [f"--preserve-env={var.split('=')[0]}" for var in env_vars] + sys.argv else: sudo_cmd = ["sudo"] + sys.argv os.execvp("sudo", sudo_cmd) def running_ssh(): return "SSH_CLIENT" in os.environ or "SSH_TTY" in os.environ def convert_string_to_bool(str_value) -> bool: """convert a string to a boolean value""" try: value = literal_eval(str_value) except (SyntaxError, ValueError): value = None sys.exit(f"Invalid entry: {str_value}") return bool(value) def _git_describe() -> str: """Get the git description of the current commit""" try: result = subprocess.check_output( ["git", "log", "-1", '--format=commit %h ("%s")'], cwd=os.path.dirname(__file__), text=True, stderr=subprocess.DEVNULL, ) return result.strip() except subprocess.CalledProcessError: return None except FileNotFoundError: return None def version() -> str: """Get version of the tool""" ver = "unknown" try: ver = importlib.metadata.version("amd-debug-tools") except importlib.metadata.PackageNotFoundError: pass describe = _git_describe() if describe: ver = f"{ver} [{describe}]" return ver class AmdTool: """Base class for AMD tools""" def __init__(self, prefix): self.log = _configure_log(prefix) logging.debug("command: %s (module: %s)", sys.argv, type(self).__name__) logging.debug("Version: %s", version()) if os.uname().sysname != "Linux": raise RuntimeError("This tool only runs on Linux") amd-debug-tools-0.2.15/src/amd_debug/database.py000066400000000000000000000254541515405217400214140ustar00rootroot00000000000000# SPDX-License-Identifier: MIT from datetime import datetime import sqlite3 import os from amd_debug.common import read_file SCHEMA_VERSION = 1 def migrate(cur, user_version) -> None: """Migrate sqlite database schema""" cur.execute("PRAGMA user_version") val = cur.fetchone()[0] # Schema 1 # - add priority column if val == 0: cur.execute("ALTER TABLE debug ADD COLUMN priority INTEGER") # Update schema if necessary if val != user_version: cur.execute(f"PRAGMA user_version = {user_version}") class SleepDatabase: """Database class to store sleep cycle data""" def __init__(self, dbf=None) -> None: self.db = None self.last_suspend = None self.cycle_data_cnt = 0 self.debug_cnt = 0 if not dbf: # if we were packaged we would have a directory in /var/lib path = os.path.join("/", "var", "lib", "amd-s2idle") if not os.path.exists(path): path = os.path.join("/", "var", "local", "lib", "amd-s2idle") os.makedirs(path, exist_ok=True) dbf = os.path.join(path, "data.db") new = not os.path.exists(dbf) self.db = sqlite3.connect(dbf) cur = self.db.cursor() cur.execute( "CREATE TABLE IF NOT EXISTS prereq_data (" "t0 INTEGER," "id INTEGER," "message TEXT," "symbol TEXT," "PRIMARY KEY(t0, id))" ) cur.execute( "CREATE TABLE IF NOT EXISTS debug (" "t0 INTEGER," "id INTEGER," "message TEXT," "priority INTEGER," "PRIMARY KEY(t0, id))" ) cur.execute( "CREATE TABLE IF NOT EXISTS cycle (" "t0 INTEGER PRIMARY KEY," "t1 INTEGER," "requested INTEGER," "gpio TEXT," "wake_irq TEXT," "kernel REAL," "hw REAL)" ) cur.execute( "CREATE TABLE IF NOT EXISTS cycle_data (" "t0 INTEGER," "id INTEGER," "message TEXT," "symbol TEXT," "PRIMARY KEY(t0, id))" ) cur.execute( "CREATE TABLE IF NOT EXISTS battery (" "t0 INTEGER PRIMARY KEY," "name TEXT," "b0 INTEGER," "b1 INTEGER," "full INTEGER," "unit TEXT)" ) self.prereq_data_cnt = 0 if new: cur.execute(f"PRAGMA user_version = {SCHEMA_VERSION}") else: migrate(cur, SCHEMA_VERSION) def __del__(self) -> None: if self.db: self.db.close() def start_cycle(self, timestamp): """Start a new sleep cycle""" assert self.db self.last_suspend = timestamp # increment the counters so that systemd hooks work cur = self.db.cursor() cur.execute( "SELECT MAX(id) FROM cycle_data WHERE t0=?", (int(self.last_suspend.strftime("%Y%m%d%H%M%S")),), ) val = cur.fetchone()[0] if val is not None: self.cycle_data_cnt = val + 1 else: self.cycle_data_cnt = 0 cur.execute( "SELECT MAX(id) FROM debug WHERE t0=?", (int(self.last_suspend.strftime("%Y%m%d%H%M%S")),), ) val = cur.fetchone()[0] if val is not None: self.debug_cnt = val + 1 else: self.debug_cnt = 0 def sync(self) -> None: """Sync the database to disk""" assert self.db self.db.commit() def record_debug(self, message, level=6) -> None: """Helper function to record a message to debug database""" assert self.last_suspend assert self.db cur = self.db.cursor() cur.execute( "INSERT into debug (t0, id, message, priority) VALUES (?, ?, ?, ?)", ( int(self.last_suspend.strftime("%Y%m%d%H%M%S")), self.debug_cnt, message, level, ), ) self.debug_cnt += 1 def record_debug_file(self, fn): """Helper function to record the entire contents of a file to debug database""" try: contents = read_file(fn) self.record_debug(contents) except PermissionError: self.record_debug(f"Unable to capture {fn}") def record_battery_energy(self, name, energy, full, unit): """Helper function to record battery energy""" assert self.db assert self.last_suspend cur = self.db.cursor() cur.execute( "SELECT * FROM battery WHERE t0=?", (int(self.last_suspend.strftime("%Y%m%d%H%M%S")),), ) if cur.fetchone(): cur.execute( "UPDATE battery SET b1=? WHERE t0=?", (energy, int(self.last_suspend.strftime("%Y%m%d%H%M%S"))), ) else: cur.execute( """ INSERT into battery (t0, name, b0, b1, full, unit) VALUES (?, ?, ?, ?, ?, ?) """, ( int(self.last_suspend.strftime("%Y%m%d%H%M%S")), name, energy, None, full, unit, ), ) def record_cycle_data(self, message, symbol) -> None: """Helper function to record a message to cycle_data database""" assert self.last_suspend assert self.db cur = self.db.cursor() cur.execute( """ INSERT into cycle_data (t0, id, message, symbol) VALUES (?, ?, ?, ?) """, ( ( int(self.last_suspend.strftime("%Y%m%d%H%M%S")), self.cycle_data_cnt, message, symbol, ) ), ) self.cycle_data_cnt += 1 def record_cycle( self, requested_duration=0, active_gpios="", wakeup_irqs="", kernel_duration=0, hw_sleep_duration=0, ) -> None: """Helper function to record a sleep cycle into the cycle database""" assert self.last_suspend assert self.db cur = self.db.cursor() cur.execute( """ REPLACE INTO cycle (t0, t1, requested, gpio, wake_irq, kernel, hw) VALUES (?, ?, ?, ?, ?, ?, ?) """, ( int(self.last_suspend.strftime("%Y%m%d%H%M%S")), int(datetime.now().strftime("%Y%m%d%H%M%S")), requested_duration, str(active_gpios) if active_gpios else "", str(wakeup_irqs), kernel_duration, hw_sleep_duration, ), ) def record_prereq(self, message, symbol) -> None: """Helper function to record a message to prereq_data database""" assert self.last_suspend assert self.db cur = self.db.cursor() cur.execute( """ INSERT into prereq_data (t0, id, message, symbol) VALUES (?, ?, ?, ?) """, ( ( int(self.last_suspend.strftime("%Y%m%d%H%M%S")), self.prereq_data_cnt, message, symbol, ) ), ) self.prereq_data_cnt += 1 def report_prereq(self, t0) -> list: """Helper function to report the prereq_data database""" assert self.db if t0 is None: return [] cur = self.db.cursor() cur.execute( "SELECT * FROM prereq_data WHERE t0=?", (int(t0.strftime("%Y%m%d%H%M%S")),), ) return cur.fetchall() def report_debug(self, t0) -> list: """Helper function to report the debug database""" assert self.db if t0 is None: return [] cur = self.db.cursor() cur.execute( "SELECT message, priority FROM debug WHERE t0=?", (int(t0.strftime("%Y%m%d%H%M%S")),), ) return cur.fetchall() def report_cycle(self, t0=None) -> list: """Helper function to report a cycle from database""" assert self.db if t0 is None: assert self.last_suspend t0 = self.last_suspend cur = self.db.cursor() cur.execute( "SELECT * FROM cycle WHERE t0=?", (int(t0.strftime("%Y%m%d%H%M%S")),), ) return cur.fetchall() def report_cycle_data(self, t0=None) -> str: """Helper function to report a table matching a timestamp from cycle_data database""" assert self.db if t0 is None: assert self.last_suspend t0 = self.last_suspend cur = self.db.cursor() cur.execute( "SELECT message, symbol FROM cycle_data WHERE t0=? ORDER BY symbol", (int(t0.strftime("%Y%m%d%H%M%S")),), ) data = "" for row in cur.fetchall(): data += f"{row[1]} {row[0]}\n" return data def report_battery(self, t0=None) -> list: """Helper function to report a line from battery database""" assert self.db if t0 is None: assert self.last_suspend t0 = self.last_suspend cur = self.db.cursor() cur.execute( "SELECT * FROM battery WHERE t0=?", (int(t0.strftime("%Y%m%d%H%M%S")),), ) return cur.fetchall() def get_last_prereq_ts(self) -> int: """Helper function to report the last line from prereq database""" assert self.db cur = self.db.cursor() cur.execute("SELECT * FROM prereq_data ORDER BY t0 DESC LIMIT 1") result = cur.fetchone() return result[0] if result else 0 def get_last_cycle(self) -> list: """Helper function to report the last line from battery database""" assert self.db cur = self.db.cursor() cur.execute("SELECT t0 FROM cycle ORDER BY t0 DESC LIMIT 1") return cur.fetchone() def report_summary_dataframe(self, since, until) -> object: """Helper function to report a dataframe from the database""" assert self.db import pandas as pd # pylint: disable=import-outside-toplevel pd.set_option("display.precision", 2) return pd.read_sql_query( sql="SELECT cycle.t0, cycle.t1, hw, requested, gpio, wake_irq, b0, b1, full FROM cycle LEFT JOIN battery ON cycle.t0 = battery.t0 WHERE cycle.t0 >= ? and cycle.t0 <= ?", con=self.db, params=( int(since.strftime("%Y%m%d%H%M%S")), int(until.strftime("%Y%m%d%H%M%S")), ), ) amd-debug-tools-0.2.15/src/amd_debug/display.py000066400000000000000000000016321515405217400213050ustar00rootroot00000000000000# SPDX-License-Identifier: MIT """Display analysis""" import os from pyudev import Context from amd_debug.common import read_file class Display: """Display analysis""" def __init__(self): self.pyudev = Context() self.edid = [] for dev in self.pyudev.list_devices(subsystem="drm"): if not "card" in dev.device_path: continue p = os.path.join(dev.sys_path, "status") if not os.path.exists(p): continue f = read_file(p) if f != "connected": continue p = os.path.join(dev.sys_path, "enabled") f = read_file(p) if f != "enabled": continue self.edid.append(os.path.join(dev.sys_path, "edid")) def get_edid(self) -> list: """Get the path for EDID data for all connected displays""" return self.edid amd-debug-tools-0.2.15/src/amd_debug/failures.py000066400000000000000000000574761515405217400214730ustar00rootroot00000000000000# SPDX-License-Identifier: MIT from datetime import timedelta from amd_debug.common import print_color class S0i3Failure: """Base class for all S0i3 failures""" def __init__(self): self.explanation = "" self.url = "" self.description = "" def get_failure(self): """Prints the failure message""" if self.description: print_color(self.description, "🚦") if self.explanation: print(self.explanation) if self.url: print(f"For more information on this failure see:{self.url}") def get_description(self) -> str: """Returns the description of the failure""" return self.description def __str__(self) -> str: if self.url: url = f"For more information on this failure see:{self.url}" else: url = "" return f"{self.explanation}{url}" class RtcAlarmWrong(S0i3Failure): """RTC alarm is not configured to use ACPI""" def __init__(self): super().__init__() self.description = "rtc_cmos is not configured to use ACPI alarm" self.explanation = ( "Some problems can occur during wakeup cycles if the HPET RTC " "emulation is used to wake systems. This can manifest in " "unexpected wakeups or high power consumption." ) self.url = "https://github.com/systemd/systemd/issues/24279" class MissingGpu(S0i3Failure): """GPU device is missing""" def __init__(self): super().__init__() self.description = "GPU device is missing" self.explanation = ( "Running the s2idle sequence without an integrated GPU is likely " "to cause problems. If you have a mux in BIOS, enable the integrated " "GPU." ) class MissingAmdgpu(S0i3Failure): """AMDGPU driver is missing""" def __init__(self): super().__init__() self.description = "AMDGPU driver is missing" self.explanation = ( "The amdgpu driver is used for hardware acceleration as well " "as coordination of the power states for certain IP blocks on the SOC. " "Be sure that you have enabled CONFIG_AMDGPU in your kernel." ) class MissingAmdgpuFirmware(S0i3Failure): """AMDGPU firmware is missing""" def __init__(self, errors): super().__init__() self.description = "AMDGPU firmware is missing" self.explanation = ( "The amdgpu driver loads firmware from /lib/firmware/amdgpu " "In some cases missing firmware will prevent a successful " "suspend cycle." "Upgrade to a newer snapshot at https://gitlab.com/kernel-firmware/linux-firmware" ) self.url = "https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1053856" for error in errors: self.explanation += f"{error}" class AmdgpuPpFeatureMask(S0i3Failure): """AMDGPU ppfeaturemask has been changed""" def __init__(self): super().__init__() self.description = "AMDGPU ppfeaturemask changed" self.explanation = ( "The ppfeaturemask for the amdgpu driver has been changed " "Modifying this from the defaults may cause the system to not " "enter hardware sleep." ) self.url = "https://gitlab.freedesktop.org/drm/amd/-/issues/2808#note_2379968" class MissingAmdPmc(S0i3Failure): """AMD-PMC driver is missing""" def __init__(self): super().__init__() self.description = "AMD-PMC driver is missing" self.explanation = ( "The amd-pmc driver is required for the kernel to instruct the " "soc to enter the hardware sleep state. " "Be sure that you have enabled CONFIG_AMD_PMC in your kernel. " "" "If CONFIG_AMD_PMC is enabled but the amd-pmc driver isn't loading " "then you may have found a bug and should report it." ) class MissingThunderbolt(S0i3Failure): """Thunderbolt driver is missing""" def __init__(self): super().__init__() self.description = "thunderbolt driver is missing" self.explanation = ( "The thunderbolt driver is required for the USB4 routers included " "with the SOC to enter the proper power states. " "Be sure that you have enabled CONFIG_USB4 in your kernel." ) class MissingXhciHcd(S0i3Failure): """xhci_hcd driver is missing""" def __init__(self): super().__init__() self.description = "xhci_hcd driver is missing" self.explanation = ( "The xhci_hcd driver is required for the USB3 controllers included " "with the SOC to enter the proper power states. " "Be sure that you have enabled CONFIG_XHCI_PCI in your kernel." ) class MissingDriver(S0i3Failure): """driver is missing""" def __init__(self, slot): super().__init__() self.description = f"{slot} driver is missing" self.explanation = ( f"No driver has been bound to PCI device {slot} " "Without a driver, the hardware may be able to enter a low power " "state, but there may be spurious wake up events." ) class AcpiBiosError(S0i3Failure): """ACPI BIOS errors detected""" def __init__(self, errors): super().__init__() self.description = "ACPI BIOS Errors detected" self.explanation = ( "When running a firmware component utilized for s2idle " "the ACPI interpreter in the Linux kernel encountered some " "problems. This usually means it's a bug in the system BIOS " "that should be fixed the system manufacturer." "" "You may have problems with certain devices after resume or high " "power consumption when this error occurs." ) for error in errors: self.explanation += f"{error}" class UnsupportedModel(S0i3Failure): """Unsupported CPU model""" def __init__(self): super().__init__() self.description = "Unsupported CPU model" self.explanation = ( "This model does not support hardware s2idle. " "Attempting to run s2idle will use a pure software suspend " "and will not yield tangible power savings." ) class UserNvmeConfiguration(S0i3Failure): """User has disabled NVME ACPI support""" def __init__(self): super().__init__() self.description = "NVME ACPI support is disabled" self.explanation = ( "The kernel command line has been configured to not support " "NVME ACPI support. This is required for the NVME device to " "enter the proper power state." ) class AcpiNvmeStorageD3Enable(S0i3Failure): """NVME device is missing ACPI attributes""" def __init__(self, disk, num_ssds): super().__init__() self.description = f"{disk} missing ACPI attributes" self.explanation = ( "An NVME device was found, but it doesn't specify the StorageD3Enable " "attribute in the device specific data (_DSD). " "This is a BIOS bug, but it may be possible to work around in the kernel. " ) if num_ssds > 1: self.explanation += ( "" "If you added an aftermarket SSD to your system, the system vendor might not have added this " "property to the BIOS for the second port which could cause this behavior. " "" "Please re-run this script with the --acpidump argument and file a bug to " "investigate." ) self.url = "https://bugzilla.kernel.org/show_bug.cgi?id=216440" class DevSlpHostIssue(S0i3Failure): """AHCI controller doesn't support DevSlp""" def __init__(self): super().__init__() self.description = "AHCI controller doesn't support DevSlp" self.explanation = ( "The AHCI controller is not configured to support DevSlp. " "This must be enabled in BIOS for s2idle in Linux." ) class DevSlpDiskIssue(S0i3Failure): """SATA disk doesn't support DevSlp""" def __init__(self): super().__init__() self.description = "SATA disk doesn't support DevSlp" self.explanation = ( "The SATA disk does not support DevSlp. " "s2idle in Linux requires SATA disks that support this feature." ) class SleepModeWrong(S0i3Failure): """System is not configured for Modern Standby""" def __init__(self): super().__init__() self.description = ( "The system hasn't been configured for Modern Standby in BIOS setup" ) self.explanation = ( "AMD systems must be configured for Modern Standby in BIOS setup " "for s2idle to function properly in Linux. " "On some OEM systems this is referred to as 'Windows' sleep mode. " "If the BIOS is configured for S3 and you manually select s2idle " "in /sys/power/mem_sleep, the system will not enter the deepest hardware state." ) class DeepSleep(S0i3Failure): """Deep sleep is configured on the kernel command line""" def __init__(self): super().__init__() self.description = ( "The kernel command line is asserting the system to use deep sleep" ) self.explanation = ( "Adding mem_sleep_default=deep doesn't work on AMD systems. " "Please remove it from the kernel command line." ) class FadtWrong(S0i3Failure): """FADT doesn't support low power idle""" def __init__(self): super().__init__() self.description = ( "The kernel didn't emit a message that low power idle was supported" ) self.explanation = ( "Low power idle is a bit documented in the FADT to indicate that " "low power idle is supported. " "Only newer kernels support emitting this message, so if you run on " "an older kernel you may get a false negative. " "When launched as root this script will try to directly introspect the " "ACPI tables to confirm this." ) class Irq1Workaround(S0i3Failure): """IRQ1 wakeup source is active""" def __init__(self): super().__init__() self.description = "The wakeup showed an IRQ1 wakeup source, which might be a platform firmware bug" self.explanation = ( "A number of Renoir, Lucienne, Cezanne, & Barcelo platforms have a platform firmware " "bug where IRQ1 is triggered during s0i3 resume. " "You may have tripped up on this bug as IRQ1 was active during resume. " "If you didn't press a keyboard key to wakeup the system then this can be " "the cause of spurious wakeups." "" "To fix it, first try to upgrade to the latest firmware from your manufacturer. " "If you're already upgraded to the latest firmware you can use one of two workarounds: " " 1. Manually disable wakeups from IRQ1 by running this command each boot: " " echo 'disabled' | sudo tee /sys/bus/serio/devices/serio0/power/wakeup " " 2. Use the below linked patch in your kernel." ) self.url = "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/drivers/platform/x86/amd/pmc.c?id=8e60615e8932167057b363c11a7835da7f007106" class KernelRingBufferWrapped(S0i3Failure): """Kernel ringbuffer has wrapped""" def __init__(self): super().__init__() self.description = "Kernel ringbuffer has wrapped" self.explanation = ( "This script relies upon analyzing the kernel log for markers. " "The kernel's log provided by dmesg uses a ring buffer. " "When the ring buffer fills up it will wrap around and overwrite old messages. " "" "In this case it's not possible to look for some of these markers " "" "Passing the pre-requisites check won't be possible without rebooting the machine. " "If you are sure your system meets pre-requisites, you can re-run the script using. " "the systemd logger or with --force." ) class AmdHsmpBug(S0i3Failure): """AMD HSMP is built into the kernel""" def __init__(self): super().__init__() self.description = "amd-hsmp built in to kernel" self.explanation = ( "The kernel has been compiled with CONFIG_AMD_HSMP=y. " "This has been shown to cause suspend failures on some systems. " "" "Either recompile the kernel without CONFIG_AMD_HSMP, " "or use initcall_blacklist=hsmp_plt_init on your kernel command line to avoid triggering problems " "" ) self.url = "https://gitlab.freedesktop.org/drm/amd/-/issues/2414" class WCN6855Bug(S0i3Failure): """WCN6855 firmware causes spurious wakeups""" def __init__(self): super().__init__() self.description = "The firmware loaded for the WCN6855 causes spurious wakeups" self.explanation = ( "During s2idle on AMD systems PCIe devices are put into D3cold. During wakeup they're transitioned back " "into the state they were before s2idle. For many implementations this is D3hot. " "If an ACPI event has been triggered by the EC, the hardware will resume from s2idle, " "but the kernel should process the event and then put it back into s2idle. " "" "When this bug occurs, a GPIO connected to the WLAN card is active on the system making " "he GPIO controller IRQ also active. The kernel sees that the ACPI event IRQ and GPIO " "controller IRQ are both active and resumes the system. " "" "Some non-exhaustive events that will trigger this behavior: " " * Suspending the system and then closing the lid. " " * Suspending the system and then unplugging the AC adapter. " " * Suspending the system and the EC notifying the OS of a battery level change. " "" "This issue is fixed by updated WCN6855 firmware which will avoid triggering the GPIO. " "The version string containing the fix is 'WLAN.HSP.1.1-03125-QCAHSPSWPL_V1_V2_SILICONZ_LITE-3.6510.23' " ) self.url = "https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git/commit/?id=c7a57ef688f7d99d8338a5d8edddc8836ff0e6de" class I2CHidBug(S0i3Failure): """I2C HID device causes spurious wakeups""" def __init__(self, name, remediation): super().__init__() self.description = f"The {name} device has been reported to cause high power consumption and spurious wakeups" self.explanation = ( "I2C devices work in an initiator/receiver relationship where the device is the receiver. In order for the receiver to indicate " "the initiator needs to read data they will assert an attention GPIO pin. " "When a device misbehaves it may assert this pin spuriously which can cause the SoC to wakeup prematurely. " "This typically manifests as high power consumption at runtime and spurious wakeups at suspend. " "" "This issue can be worked around by unbinding the device from the kernel using this command: " "" f"{remediation}" "" "To fix this issue permanently the kernel will need to avoid binding to this device. " ) self.url = "https://gitlab.freedesktop.org/drm/amd/-/issues/2812" class SpuriousWakeup(S0i3Failure): """System woke up prematurely""" def __init__(self, requested, wake): super().__init__() self.description = ( f"Userspace wasn't asleep at least {timedelta(seconds=requested)}" ) self.explanation = ( f"The system was programmed to sleep for {timedelta(seconds=requested)}, but woke up prematurely after {wake}. " "This typically happens when the system was woken up from a non-timer based source. " "If you didn't intentionally wake it up, then there may be a kernel or firmware bug." ) class LowHardwareSleepResidency(S0i3Failure): """System had low hardware sleep residency""" def __init__(self, duration, percent): super().__init__() self.description = "System had low hardware sleep residency" self.explanation = ( f"The system was asleep for {timedelta(seconds=duration)}, but only spent {percent/100:.2%} " "of this time in a hardware sleep state. In sleep cycles that are at least " "60 seconds long it's expected you spend above 90 percent of the cycle in " "hardware sleep." ) class MSRFailure(S0i3Failure): """MSR access failed""" def __init__(self): super().__init__() self.description = "PC6 or CC6 state disabled" self.explanation = ( "The PC6 state of the package or the CC6 state of CPU cores was disabled. " "This will prevent the system from getting to the deepest sleep state over suspend." ) class TaintedKernel(S0i3Failure): """Kernel is tainted""" def __init__(self): super().__init__() self.description = "Kernel is tainted" self.explanation = ( "A tainted kernel may exhibit unpredictable bugs that are difficult for this script to characterize. " "If this is intended behavior run the tool with --force. " ) self.url = "https://gitlab.freedesktop.org/drm/amd/-/issues/3089" class DMArNotEnabled(S0i3Failure): """DMAr is not enabled""" def __init__(self): super().__init__() self.description = "Pre-boot DMA protection disabled" self.explanation = ( "Pre-boot IOMMU DMA protection has been disabled. " "When the IOMMU is enabled this platform requires pre-boot DMA protection for suspend to work. " ) class MissingIommuACPI(S0i3Failure): """IOMMU ACPI table errors""" def __init__(self, device): super().__init__() self.description = f"Device {device} missing from ACPI tables" self.explanation = ( f"The ACPI device {device} is required for suspend to work when the IOMMU is enabled. " "Please check your BIOS settings and if configured correctly, report a bug to your system vendor." ) self.url = "https://gitlab.freedesktop.org/drm/amd/-/issues/3738#note_2667140" class MissingIommuPolicy(S0i3Failure): """ACPI table errors""" def __init__(self, device): super().__init__() self.description = f"Device {device} does not have IOMMU policy applied" self.explanation = ( f"The ACPI device {device} is present but no IOMMU policy was set for it." "This generally happens if the HID or UID don't match the ACPI IVRS table." ) class IommuPageFault(S0i3Failure): """IOMMU Page fault""" def __init__(self, device): super().__init__() self.description = f"Page fault reported for {device}" self.explanation = ( f"The IOMMU reports a page fault caused by {device}. This can prevent suspend/resume from functioning properly" "The page fault can be the device itself, a problem in the firmware or a problem in the kernel." "Report a bug for further triage and investigation." ) class SMTNotEnabled(S0i3Failure): """SMT is not enabled""" def __init__(self): super().__init__() self.description = "SMT is not enabled" self.explanation = ( "Disabling SMT prevents cores from going into the correct state." ) class ASpmWrong(S0i3Failure): """ASPM is overridden""" def __init__(self): super().__init__() self.description = "ASPM is overridden" self.explanation = ( " Modifying ASPM may prevent PCIe devices from going into the " " correct state and lead to system stability issues. " ) class UnservicedGpio(S0i3Failure): """GPIO is not serviced""" def __init__(self): super().__init__() self.description = "GPIO interrupt is not serviced" self.explanation = ( "All GPIO controllers interrupts must be serviced to enter " "hardware sleep." "Make sure that all drivers necessary to service GPIOs are loaded. " "The most common cause is that i2c-hid-acpi is not loaded but the " "machine contains an I2C touchpad." ) class DmiNotSetup(S0i3Failure): """DMI isn't setup""" def __init__(self): super().__init__() self.description = "DMI data was not scanned" self.explanation = ( "If DMI data hasn't been scanned then quirks that are dependent " "upon DMI won't be loaded. " "Most notably, this will prevent the rtc-cmos driver from setting. " "up properly by default. It may also prevent other drivers from working." ) class LimitedCores(S0i3Failure): """Number of CPU cores limited""" def __init__(self, actual_cores, max_cores): super().__init__() self.description = "CPU cores have been limited" self.explanation = ( f"The CPU cores have been limited to {max_cores}, but the system " f"actually has {actual_cores}. Limiting the cores will prevent the " "the system from going into a hardware sleep state. " "This is typically solved by increasing the kernel config CONFIG_NR_CPUS." ) class RogAllyOldMcu(S0i3Failure): """MCU firwmare is too old""" def __init__(self, vmin, actual): super().__init__() self.description = "Rog Ally MCU firmware is too old" self.explanation = ( f"The MCU is version {actual}, but needs to be at least {vmin}" f"to avoid major issues with interactions with suspend" ) class RogAllyMcuPowerSave(S0i3Failure): """MCU powersave is disabled""" def __init__(self): super().__init__() self.description = "Rog Ally MCU power save is disabled" self.explanation = ( "The MCU powersave feature is disabled which will cause problems " "with the controller after suspend/resume." ) class DmcubTooOld(S0i3Failure): """DMCUB microcode is too old""" def __init__(self, current, expected): super().__init__() self.description = "DMCUB microcode is too old" self.explanation = ( f"The DMCUB microcode version {hex(current)} is older than the" f"minimum suggested version {hex(expected)}." ) class MissingIsp4PlatformDriver(S0i3Failure): """ISP4 platform driver is missing""" def __init__(self): super().__init__() self.description = "ISP4 platform driver is missing" self.explanation = ( "The ISP4 platform driver is required for the camera interface included " "with the SOC to enter the proper power states. " "Be sure that you have enabled CONFIG_AMD_ISP_PLATFORM in your kernel." ) class MissingAmdCaptureModule(S0i3Failure): """AMD Capture module is missing""" def __init__(self): super().__init__() self.description = "AMD Capture module is missing" self.explanation = ( "The amd_capture module is required for the camera interface included " "with the SOC to enter the proper power states. " "Be sure that the amd_capture module is loaded." "" "If the module is not available, disable the camera in the BIOS to prevent issues." ) self.url = "https://gitlab.freedesktop.org/drm/amd/-/issues/4869" amd-debug-tools-0.2.15/src/amd_debug/installer.py000066400000000000000000000337201515405217400216400ustar00rootroot00000000000000# SPDX-License-Identifier: MIT """ This module contains installer support for amd-debug-tools. """ import argparse import os import shutil import subprocess from amd_debug.common import ( print_color, get_distro, read_file, systemd_in_use, show_log_info, fatal_error, relaunch_sudo, AmdTool, ) class Headers: # pylint: disable=too-few-public-methods """Headers for the script""" MissingIasl = "ACPI extraction tool `iasl` is missing" MissingEdidDecode = "EDID decoding tool `edid-decode` is missing" MissingDiEdidDecode = "EDID decoding tool `di-edid-decode` is missing" MissingEthtool = "Ethtool is missing" InstallAction = "Attempting to install" MissingFwupd = "Firmware update library `fwupd` is missing" MissingPyudev = "Udev access library `pyudev` is missing" MissingPackaging = "Python library `packaging` is missing" MissingPandas = "Data library `pandas` is missing" MissingTabulate = "Data library `tabulate` is missing" MissingJinja2 = "Template library `jinja2` is missing" MissingSeaborn = "Data visualization library `seaborn` is missing" UnknownDistro = "No distro installation support available, install manually" class DistroPackage: """Base class for distro packages""" def __init__(self, deb, rpm, arch, message): self.deb = deb self.rpm = rpm self.arch = arch self.message = message def install(self): """Install the package for a given distro""" relaunch_sudo() show_install_message(self.message) dist = get_distro() if dist in ("ubuntu", "debian"): if not self.deb: return False installer = ["apt", "install", self.deb] elif dist == "fedora": if not self.rpm: return False release = read_file("/usr/lib/os-release") variant = None for line in release.split("\n"): if line.startswith("VARIANT_ID"): variant = line.split("=")[-1] if variant not in ("workstation", "kde"): return False installer = ["dnf", "install", "-y", self.rpm] elif dist == "arch" or os.path.exists("/etc/arch-release"): if not self.arch: return False installer = ["pacman", "-Sy", self.arch] else: print_color(Headers.UnknownDistro, "πŸ‘€") return True try: subprocess.check_call(installer) except subprocess.CalledProcessError as e: fatal_error(e) return True class PyUdevPackage(DistroPackage): """Pyudev package""" def __init__(self): super().__init__( deb="python3-pyudev", rpm="python3-pyudev", arch="python-pyudev", message=Headers.MissingPyudev, ) class PackagingPackage(DistroPackage): """Packaging package""" def __init__(self): super().__init__( deb="python3-packaging", rpm=None, arch="python-packaging", message=Headers.MissingPackaging, ) class PandasPackage(DistroPackage): """Class for handling the pandas package""" def __init__(self): super().__init__( deb="python3-pandas", rpm="python3-pandas", arch="python-pandas", message=Headers.MissingPandas, ) class TabulatePackage(DistroPackage): """Class for handling the tabulate package""" def __init__(self): super().__init__( deb="python3-tabulate", rpm="python3-tabulate", arch="python-tabulate", message=Headers.MissingTabulate, ) class Jinja2Package(DistroPackage): """Class for handling the jinja2 package""" def __init__(self): super().__init__( deb="python3-jinja2", rpm="python3-jinja2", arch="python-jinja", message=Headers.MissingJinja2, ) class SeabornPackage(DistroPackage): """Class for handling the seaborn package""" def __init__(self): super().__init__( deb="python3-seaborn", rpm="python3-seaborn", arch="python-seaborn", message=Headers.MissingSeaborn, ) class IaslPackage(DistroPackage): """Iasl package""" def __init__(self): super().__init__( deb="acpica-tools", rpm="acpica-tools", arch="acpica", message=Headers.MissingIasl, ) class EthtoolPackage(DistroPackage): """Ethtool package""" def __init__(self): super().__init__( deb="ethtool", rpm="ethtool", arch="ethtool", message=Headers.MissingEthtool, ) class EdidDecodePackage(DistroPackage): """Edid-Decode package""" def __init__(self): super().__init__( deb="edid-decode", rpm="edid-decode", arch=None, message=Headers.MissingEdidDecode, ) class DisplayInfoPackage(DistroPackage): """display info package""" def __init__(self): super().__init__( deb="libdisplay-info-bin", rpm="libdisplay-info-tools", arch="libdisplay-info", message=Headers.MissingDiEdidDecode, ) class FwupdPackage(DistroPackage): """Fwupd package""" def __init__(self): super().__init__( deb="gir1.2-fwupd-2.0", rpm=None, arch=None, message=Headers.MissingFwupd, ) def show_install_message(message): """Show an install message""" action = Headers.InstallAction message = f"{message}. {action}." print_color(message, "πŸ‘€") class Installer(AmdTool): """Installer class""" def __init__(self, tool_debug): log_prefix = "installer" if tool_debug else None super().__init__(log_prefix) self.systemd = systemd_in_use() self.systemd_path = os.path.join("/", "lib", "systemd", "system-sleep") # test if fwupd can report device firmware versions try: import gi # pylint: disable=import-outside-toplevel from gi.repository import ( # pylint: disable=import-outside-toplevel GLib as _, ) gi.require_version("Fwupd", "2.0") from gi.repository import ( # pylint: disable=import-outside-toplevel Fwupd as _, ) self.fwupd = True except ImportError: self.fwupd = False except ValueError: self.fwupd = False self.requirements = [] def set_requirements(self, *args): """Set the requirements for the installer""" self.requirements = args def install_dependencies(self) -> bool: """Install the dependencies""" if "iasl" in self.requirements: try: iasl = subprocess.call(["iasl", "-v"], stdout=subprocess.DEVNULL) == 0 except FileNotFoundError: iasl = False if not iasl: package = IaslPackage() if not package.install(): return False if "ethtool" in self.requirements: try: ethtool = ( subprocess.call(["ethtool", "-h"], stdout=subprocess.DEVNULL) == 0 ) except FileNotFoundError: ethtool = False if not ethtool: package = EthtoolPackage() if not package.install(): return False # can be satisified by either edid-decode or di-edid-decode if "edid-decode" in self.requirements: try: di_edid = ( subprocess.call( ["di-edid-decode", "--help"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) == 255 ) except FileNotFoundError: di_edid = False try: edid = ( subprocess.call( ["edid-decode", "--help"], stdout=subprocess.DEVNULL ) == 255 ) except FileNotFoundError: edid = False if not di_edid and not edid: # try to install di-edid-decode first package = DisplayInfoPackage() if package.install(): return True # fall back to edid-decode instead package = EdidDecodePackage() if not package.install(): return False if "fwupd" in self.requirements and not self.fwupd: package = FwupdPackage() if not package.install(): return False if "pyudev" in self.requirements: try: import pyudev as _ # pylint: disable=import-outside-toplevel except ModuleNotFoundError: package = PyUdevPackage() if not package.install(): return False if "packaging" in self.requirements: try: import packaging as _ # pylint: disable=import-outside-toplevel except ModuleNotFoundError: package = PackagingPackage() if not package.install(): return False if "pandas" in self.requirements: try: import pandas as _ # pylint: disable=import-outside-toplevel except ModuleNotFoundError: package = PandasPackage() if not package.install(): return False if "tabulate" in self.requirements: try: import tabulate as _ # pylint: disable=import-outside-toplevel except ModuleNotFoundError: package = TabulatePackage() if not package.install(): return False if "jinja2" in self.requirements: try: import jinja2 as _ # pylint: disable=import-outside-toplevel except ModuleNotFoundError: package = Jinja2Package() if not package.install(): return False if "seaborn" in self.requirements: try: import seaborn as _ # pylint: disable=import-outside-toplevel except ModuleNotFoundError: package = SeabornPackage() if not package.install(): return False return True def _check_systemd(self) -> bool: """Check if the systemd path exists""" if not os.path.exists(self.systemd_path): print_color( f"Systemd path does not exist: {self.systemd_path}", "❌", ) return os.path.exists(self.systemd_path) def remove(self) -> bool: """Remove the amd-s2idle hook""" if self._check_systemd(): f = "s2idle-hook" t = os.path.join(self.systemd_path, f) os.remove(t) print_color( f"Removed {f} from {self.systemd_path}", "βœ…", ) else: print_color("Systemd path does not exist, not removing hook", "🚦") f = "amd-s2idle" d = os.path.join( "/", "usr", "local", "share", "bash-completion", "completions", ) t = os.path.join(d, f) if os.path.exists(t): os.remove(t) print_color(f"Removed {f} from {d}", "βœ…") return True def install(self) -> bool: """Install the amd-s2idle hook""" import amd_debug # pylint: disable=import-outside-toplevel d = os.path.dirname(amd_debug.__file__) if self._check_systemd(): f = "s2idle-hook" s = os.path.join(d, f) t = os.path.join(self.systemd_path, f) with open(s, "r", encoding="utf-8") as r: with open(t, "w", encoding="utf-8") as w: for line in r.readlines(): if 'parser.add_argument("--path"' in line: line = line.replace( 'default=""', f"default=\"{os.path.join(d, '..')}\"", ) w.write(line) os.chmod(t, 0o755) print_color( f"Installed {f} to {self.systemd_path}", "βœ…", ) else: print_color("Systemd path does not exist, not installing hook", "🚦") f = "amd-s2idle" s = os.path.join(d, "bash", f) t = os.path.join( "/", "usr", "local", "share", "bash-completion", "completions", ) os.makedirs(t, exist_ok=True) shutil.copy(s, t) print_color(f"Installed {f} to {t}", "βœ…") return True def parse_args(): """Parse command line arguments""" parser = argparse.ArgumentParser( description="Install dependencies for AMD debug tools", ) parser.add_argument( "--tool-debug", action="store_true", help="Enable tool debug logging", ) return parser.parse_args() def install_dep_superset() -> None | int: """Install all python superset dependencies""" args = parse_args() tool = Installer(tool_debug=args.tool_debug) tool.set_requirements( "iasl", "ethtool", "jinja2", "pyudev", "packaging", "pandas", "seaborn", "tabulate", "edid-decode", ) ret = tool.install_dependencies() if ret: print_color("All dependencies installed", "βœ…") show_log_info() if ret is False: return 1 return amd-debug-tools-0.2.15/src/amd_debug/kernel.py000066400000000000000000000300631515405217400211200ustar00rootroot00000000000000# SPDX-License-Identifier: MIT """Kernel log analysis""" import logging import re import os import subprocess from datetime import timedelta from amd_debug.common import systemd_in_use, read_file, fatal_error def get_kernel_command_line() -> str: """Get the kernel command line""" cmdline = read_file(os.path.join("/proc", "cmdline")) # borrowed from https://github.com/fwupd/fwupd/blob/1.9.5/libfwupdplugin/fu-common-linux.c#L95 filtered = [ "apparmor", "audit", "auto", "bluetooth.disable_ertm", "boot", "BOOT_IMAGE", "console", "crashkernel", "cryptdevice", "cryptkey", "dm", "earlycon", "earlyprintk", "ether", "init", "initrd", "ip", "LANG", "loglevel", "luks.key", "luks.name", "luks.options", "luks.uuid", "mitigations", "mount.usr", "mount.usrflags", "mount.usrfstype", "netdev", "netroot", "nfsaddrs", "nfs.nfs4_unique_id", "nfsroot", "noplymouth", "nowatchdog", "ostree", "preempt", "quiet", "rd.dm.uuid", "rd.luks.allow-discards", "rd.luks.key", "rd.luks.name", "rd.luks.options", "rd.luks.uuid", "rd.lvm.lv", "rd.lvm.vg", "rd.md.uuid", "rd.systemd.mask", "rd.systemd.wants", "resume", "resumeflags", "rhgb", "ro", "root", "rootflags", "rootfstype", "roothash", "rw", "security", "selinux", "showopts", "splash", "swap", "systemd.machine_id", "systemd.mask", "systemd.show_status", "systemd.unit", "systemd.verity_root_data", "systemd.verity_root_hash", "systemd.wants", "udev.log_priority", "verbose", "vt.handoff", "zfs", "zswap.enabled", ] # remove anything that starts with something in filtered from cmdline return " ".join([x for x in cmdline.split() if not x.startswith(tuple(filtered))]) def sscanf_bios_args(line): """Extracts the format string and arguments from a BIOS trace line""" if re.search(r"ex_trace_point", line): return True elif re.search(r"ex_trace_args", line): parts = line.split(": ", 1) if len(parts) < 2: return None t = parts[1].strip() match = re.match(r'"(.*?)"(,.*)', t) if match: format_string = match.group(1).strip().replace("\\n", "") args_part = match.group(2).strip(", ") arguments = [arg.strip() for arg in args_part.split(",")] format_specifiers = re.findall(r"%([xXdD])", format_string) converted_args = [] arg_index = 0 for _specifier in format_specifiers: if arg_index < len(arguments): value = arguments[arg_index] if value == "Unknown": converted_args.append(-1) else: try: converted_args.append(int(value, 16)) except ValueError: return None arg_index += 1 else: break try: return format_string % tuple(converted_args) except TypeError: return None else: # If no format string is found, assume no format modifiers and return True return True # evmisc-0132 ev_queue_notify_reques: Dispatching Notify on [UBTC] (Device) Value 0x80 (Status Change) Node 00000000851b15c1 elif re.search(r"ev_queue_notify_reques", line): parts = line.split(": ", 1) if len(parts) < 2: return None return parts[1].split("Node")[0].strip() return None class KernelLogger: """Base class for kernel loggers""" def __init__(self): pass def seek(self): """Seek to the beginning of the log""" def seek_tail(self, tim=None): """Seek to the end of the log""" def process_callback(self, callback, priority=None): """Process the log""" def match_line(self, _matches) -> str: """Find lines that match all matches""" return "" def match_pattern(self, _pattern) -> str: """Find lines that match a pattern""" return "" def get_full_log(self) -> str: """Get the full log as a string""" return "" class InputFile(KernelLogger): """Class for input file parsing""" def __init__(self, fname): self.since_support = False self.buffer = "" self.seeked = False self.buffer = read_file(fname) def process_callback(self, callback, priority=None): """Process the log""" for entry in self.buffer.split("\n"): callback(entry, priority) def get_full_log(self): """Get the full log as a string""" return self.buffer class DmesgLogger(KernelLogger): """Class for dmesg logging""" def __init__(self): self.since_support = False self.buffer = "" self.seeked = False cmd = ["dmesg", "-h"] result = subprocess.run(cmd, check=True, capture_output=True) for line in result.stdout.decode("utf-8").split("\n"): if "--since" in line: self.since_support = True logging.debug("dmesg since support: %d", self.since_support) self.command = ["dmesg", "-t", "-k"] self._refresh_head() def _refresh_head(self): self.buffer = "" self.seeked = False result = subprocess.run(self.command, check=True, capture_output=True) if result.returncode == 0: self.buffer = result.stdout.decode("utf-8") def seek(self): """Seek to the beginning of the log""" if self.seeked: self._refresh_head() def seek_tail(self, tim=None): """Seek to the end of the log""" if tim: if self.since_support: # look 10 seconds back because dmesg time isn't always accurate fuzz = tim - timedelta(seconds=10) cmd = self.command + [ "--time-format=iso", f"--since={fuzz.strftime('%Y-%m-%dT%H:%M:%S')}", ] else: cmd = self.command result = subprocess.run(cmd, check=True, capture_output=True) if result.returncode == 0: self.buffer = result.stdout.decode("utf-8") if self.since_support: self.seeked = True def process_callback(self, callback, _priority=None): """Process the log""" for entry in self.buffer.split("\n"): callback(entry, _priority) def match_line(self, matches): """Find lines that match all matches""" for entry in self.buffer.split("\n"): for match in matches: if match not in entry: break return entry return "" def match_pattern(self, pattern) -> str: for entry in self.buffer.split("\n"): if re.search(pattern, entry): return entry return "" def capture_header(self): """Capture the header of the log""" return self.buffer.split("\n")[0] def get_full_log(self): """Get the full log as a string""" return self.buffer class CySystemdLogger(KernelLogger): """Class for logging using systemd journal using cython""" def __init__(self): from cysystemd.reader import ( JournalReader, JournalOpenMode, Rule, ) # pylint: disable=import-outside-toplevel boot_reader = JournalReader() boot_reader.open(JournalOpenMode.SYSTEM) boot_reader.seek_tail() boot_reader.skip_previous(1) current_boot_id = None for entry in boot_reader: if hasattr(entry, "data") and "_BOOT_ID" in entry.data: current_boot_id = entry.data["_BOOT_ID"] break if not current_boot_id: raise RuntimeError("Unable to find current boot ID") rules = Rule("_BOOT_ID", current_boot_id) & Rule("_TRANSPORT", "kernel") self.journal = JournalReader() self.journal.open(JournalOpenMode.SYSTEM) self.journal.add_filter(rules) def seek(self): """Seek to the beginning of the log""" self.journal.seek_head() def seek_tail(self, tim=None): """Seek to the end of the log""" if tim: timestamp_usec = int(tim.timestamp() * 1_000_000) self.journal.seek_realtime_usec(timestamp_usec) else: self.journal.seek_tail() def process_callback(self, callback, _priority=None): """Process the log""" for entry in self.journal: callback(entry["MESSAGE"], entry["PRIORITY"]) def match_line(self, matches): """Find lines that match all matches""" for entry in self.journal: for match in matches: if match not in entry["MESSAGE"]: break return entry["MESSAGE"] return None def match_pattern(self, pattern): """Find lines that match a pattern""" for entry in self.journal: if re.search(pattern, entry["MESSAGE"]): return entry["MESSAGE"] return None def get_full_log(self): """Get the full log as a string""" self.seek() return "\n".join([entry["MESSAGE"] for entry in self.journal]) class SystemdLogger(KernelLogger): """Class for logging using systemd journal""" def __init__(self): from systemd import journal # pylint: disable=import-outside-toplevel self.journal = journal.Reader() self.journal.this_boot() self.journal.log_level(journal.LOG_INFO) self.journal.add_match(_TRANSPORT="kernel") self.journal.add_match(PRIORITY=journal.LOG_DEBUG) def seek(self): """Seek to the beginning of the log""" self.journal.seek_head() def seek_tail(self, tim=None): if tim: self.journal.seek_realtime(tim) else: self.journal.seek_tail() def process_callback(self, callback, _priority=None): """Process the log""" for entry in self.journal: callback(entry["MESSAGE"], entry["PRIORITY"]) def match_line(self, matches): """Find lines that match all matches""" for entry in self.journal: for match in matches: if match not in entry["MESSAGE"]: break return entry["MESSAGE"] return "" def match_pattern(self, pattern): """Find lines that match a pattern""" for entry in self.journal: if re.search(pattern, entry["MESSAGE"]): return entry["MESSAGE"] return "" def get_full_log(self): """Get the full log as a string""" self.seek() return "\n".join([entry["MESSAGE"] for entry in self.journal]) def get_kernel_log(input_file=None) -> KernelLogger: """Get the kernel log provider""" kernel_log = None if input_file: kernel_log = InputFile(input_file) elif systemd_in_use(): try: kernel_log = CySystemdLogger() except ImportError: kernel_log = None except RuntimeError as e: logging.debug(e) kernel_log = None if not kernel_log: try: kernel_log = SystemdLogger() except ModuleNotFoundError: pass if not kernel_log: try: kernel_log = DmesgLogger() except subprocess.CalledProcessError as e: fatal_error(f"{e}") kernel_log = KernelLogger() logging.debug("Kernel log provider: %s", kernel_log.__class__.__name__) return kernel_log amd-debug-tools-0.2.15/src/amd_debug/prerequisites.py000066400000000000000000001550131515405217400225470ustar00rootroot00000000000000# SPDX-License-Identifier: MIT """ This module contains the s0i3 prerequisite validator for amd-debug-tools. """ import configparser import logging import os import platform import re import shutil import subprocess import tempfile import struct from datetime import datetime from packaging import version import pyudev from amd_debug.wake import WakeIRQ from amd_debug.display import Display from amd_debug.kernel import get_kernel_log, SystemdLogger, CySystemdLogger, DmesgLogger from amd_debug.common import ( apply_prefix_wrapper, BIT, clear_temporary_message, find_ip_version, get_distro, get_pretty_distro, get_property_pyudev, is_root, minimum_kernel, print_color, print_temporary_message, read_file, read_msr, AmdTool, ) from amd_debug.battery import Batteries from amd_debug.database import SleepDatabase from amd_debug.failures import ( AcpiNvmeStorageD3Enable, AmdHsmpBug, AmdgpuPpFeatureMask, ASpmWrong, DeepSleep, DevSlpDiskIssue, DevSlpHostIssue, DMArNotEnabled, DmcubTooOld, DmiNotSetup, FadtWrong, I2CHidBug, KernelRingBufferWrapped, LimitedCores, MissingAmdCaptureModule, MissingAmdgpu, MissingAmdgpuFirmware, MissingAmdPmc, MissingGpu, MissingDriver, MissingIommuACPI, MissingIommuPolicy, MissingIsp4PlatformDriver, MissingThunderbolt, MissingXhciHcd, MSRFailure, RogAllyMcuPowerSave, RogAllyOldMcu, SleepModeWrong, SMTNotEnabled, TaintedKernel, UnservicedGpio, UnsupportedModel, WCN6855Bug, ) # test if fwupd can report device firmware versions try: import gi from gi.repository import GLib as _ gi.require_version("Fwupd", "2.0") from gi.repository import Fwupd # pylint: disable=wrong-import-position FWUPD = True except ImportError: FWUPD = False except ValueError: FWUPD = False class Headers: """Headers for the script""" NvmeSimpleSuspend = "platform quirk: setting simple suspend" RootError = "Must be executed by root user" BrokenPrerequisites = "Your system does not meet s2idle prerequisites!" ExplanationReport = "Explanations for your system" class PrerequisiteValidator(AmdTool): """Class to validate the prerequisites for s2idle""" def __init__(self, tool_debug): log_prefix = "s2idle" if tool_debug else None super().__init__(log_prefix) self.kernel_log = get_kernel_log() if not is_root(): raise PermissionError("not root user") self.cpu_family = None self.cpu_model = None self.cpu_model_string = None self.pyudev = pyudev.Context() self.failures = [] self.db = SleepDatabase() self.db.start_cycle(datetime.now()) self.debug = tool_debug self.distro = get_distro() self.cmdline = read_file(os.path.join("/proc", "cmdline")) self.irqs = [] self.smu_version = "" self.smu_program = "" self.display = Display() def capture_once(self): """Capture the prerequisites once""" if not self.db.get_last_prereq_ts(): self.run() def capture_nvidia(self): """Capture the NVIDIA GPU state""" p = os.path.join("/", "proc", "driver", "nvidia", "version") if not os.path.exists(p): return True try: self.db.record_debug_file(p) except PermissionError: self.db.record_prereq("NVIDIA GPU version not readable", "πŸ‘€") return True p = os.path.join("/", "proc", "driver", "nvidia", "gpus") if not os.path.exists(p): return True for root, _dirs, files in os.walk(p, topdown=False): for f in files: try: self.db.record_debug(f"NVIDIA {f}") self.db.record_debug_file(os.path.join(root, f)) except PermissionError: self.db.record_prereq("NVIDIA GPU {f} not readable", "πŸ‘€") return True def capture_edid(self): """Capture and decode the EDID data""" edids = self.display.get_edid() if len(edids) == 0: self.db.record_debug("No EDID data found") return True for p in edids: output = None for tool in ["di-edid-decode", "edid-decode"]: try: cmd = [tool, p] output = subprocess.check_output( cmd, stderr=subprocess.DEVNULL ).decode("utf-8", errors="ignore") break except FileNotFoundError: self.db.record_debug(f"{cmd} not installed") except subprocess.CalledProcessError as _e: pass if not output: self.db.record_prereq("Failed to capture EDID table", "πŸ‘€") else: self.db.record_debug(apply_prefix_wrapper(f"EDID for {p}:", output)) return True def check_amdgpu(self): """Check for the AMDGPU driver""" count = 0 for device in self.pyudev.list_devices(subsystem="pci"): klass = device.properties.get("PCI_CLASS") if klass not in ["30000", "38000"]: continue pci_id = device.properties.get("PCI_ID") if not pci_id.startswith("1002"): continue count += 1 if device.properties.get("DRIVER") != "amdgpu": self.db.record_prereq("GPU driver `amdgpu` not loaded", "❌") self.failures += [MissingAmdgpu()] return False slot = device.properties.get("PCI_SLOT_NAME") self.db.record_prereq(f"GPU driver `amdgpu` bound to {slot}", "βœ…") if count == 0: self.db.record_prereq("Integrated GPU not found", "❌") self.failures += [MissingGpu()] return False return True def check_amdgpu_parameters(self): """Check for AMDGPU parameters""" p = os.path.join("/", "sys", "module", "amdgpu", "parameters", "ppfeaturemask") if os.path.exists(p): v = read_file(p) if v != "0xfff7bfff": self.db.record_prereq(f"AMDGPU ppfeaturemask overridden to {v}", "❌") self.failures += [AmdgpuPpFeatureMask()] return False if not self.kernel_log: message = "Unable to test for amdgpu from kernel log" self.db.record_prereq(message, "🚦") return True self.kernel_log.seek() match = self.kernel_log.match_pattern("Direct firmware load for amdgpu.*failed") if match and not "amdgpu/isp" in match: self.db.record_prereq("GPU firmware missing", "❌") self.failures += [MissingAmdgpuFirmware([match])] return False return True def check_wcn6855_bug(self): """Check if WCN6855 WLAN is affected by a bug that causes spurious wakeups""" if not self.kernel_log: message = "Unable to test for wcn6855 bug from kernel log" self.db.record_prereq(message, "🚦") return True wcn6855 = False self.kernel_log.seek() if self.kernel_log.match_pattern("ath11k_pci.*wcn6855"): match = self.kernel_log.match_pattern("ath11k_pci.*fw_version") if match: self.db.record_debug(f"WCN6855 version string: {match}") objects = match.split() for i, obj in enumerate(objects): if obj == "fw_build_id": wcn6855 = objects[i + 1] if wcn6855: components = wcn6855.split(".") if int(components[-1]) >= 37 or int(components[-1]) == 23: self.db.record_prereq( f"WCN6855 WLAN (fw build id {wcn6855})", "βœ…", ) else: self.db.record_prereq( f"WCN6855 WLAN may cause spurious wakeups (fw version {wcn6855})", "❌", ) self.failures += [WCN6855Bug()] return True def check_storage(self): """Check if storage devices are supported""" has_sata = False has_ahci = False valid_nvme = {} invalid_nvme = {} valid_sata = False valid_ahci = False if not self.kernel_log: message = "Unable to test storage from kernel log" self.db.record_prereq(message, "🚦") return True for dev in self.pyudev.list_devices(subsystem="pci", DRIVER="nvme"): # https://git.kernel.org/torvalds/c/e79a10652bbd3 if minimum_kernel(6, 10): break pci_slot_name = dev.properties["PCI_SLOT_NAME"] vendor = dev.properties.get("ID_VENDOR_FROM_DATABASE", "") model = dev.properties.get("ID_MODEL_FROM_DATABASE", "") message = f"{vendor} {model}" self.kernel_log.seek() pattern = f"{pci_slot_name}.*{Headers.NvmeSimpleSuspend}" if self.kernel_log.match_pattern(pattern): valid_nvme[pci_slot_name] = message if pci_slot_name not in valid_nvme: invalid_nvme[pci_slot_name] = message for dev in self.pyudev.list_devices(subsystem="pci", DRIVER="ahci"): has_ahci = True break for dev in self.pyudev.list_devices(subsystem="block", ID_BUS="ata"): has_sata = True break # Test AHCI if has_ahci: self.kernel_log.seek() pattern = "ahci.*flags.*sadm.*sds" if self.kernel_log.match_pattern(pattern): valid_ahci = True # Test SATA if has_sata: self.kernel_log.seek() pattern = "ata.*Features.*Dev-Sleep" if self.kernel_log.match_pattern(pattern): valid_sata = True if invalid_nvme: for disk, _name in invalid_nvme.items(): message = f"NVME {invalid_nvme[disk].strip()} is not configured for s2idle in BIOS" self.db.record_prereq(message, "❌") num = len(invalid_nvme) + len(valid_nvme) self.failures += [AcpiNvmeStorageD3Enable(invalid_nvme[disk], num)] if valid_nvme: for disk, _name in valid_nvme.items(): message = ( f"NVME {valid_nvme[disk].strip()} is configured for s2idle in BIOS" ) self.db.record_prereq(message, "βœ…") if has_sata: if valid_sata: message = "SATA supports DevSlp feature" self.db.record_prereq(message, "βœ…") else: message = "SATA does not support DevSlp feature" self.db.record_prereq(message, "❌") self.failures += [DevSlpDiskIssue()] if has_ahci: if valid_ahci: message = "AHCI is configured for DevSlp in BIOS" self.db.record_prereq(message, "βœ…") else: message = "AHCI is not configured for DevSlp in BIOS" self.db.record_prereq(message, "🚦") self.failures += [DevSlpHostIssue()] return ( (len(invalid_nvme) == 0) and (valid_sata or not has_sata) and (valid_ahci or not has_sata) ) def check_amd_xdna(self): """Check for AMD XDNA driver""" for device in self.pyudev.list_devices(subsystem="pci", PCI_CLASS="118000"): slot = device.properties["PCI_SLOT_NAME"] driver = device.properties.get("DRIVER") pci_id = device.properties.get("PCI_ID") if not pci_id.startswith("1022"): continue if not driver: self.db.record_prereq(f"NPU device in {slot} missing driver", "🚦") self.failures += [MissingDriver(slot)] return False p = os.path.join(device.sys_path, "fw_version") if os.path.exists(p): xdna_fw_version = read_file(p) self.db.record_prereq( f"NPU loaded with firmware {xdna_fw_version}", "βœ…" ) return True def check_amd_hsmp(self): """Check for AMD HSMP driver""" # not needed to check in newer kernels # see https://github.com/torvalds/linux/commit/77f1972bdcf7513293e8bbe376b9fe837310ee9c if minimum_kernel(6, 10): return True f = os.path.join("/", "boot", f"config-{platform.uname().release}") if os.path.exists(f): kconfig = read_file(f) if "CONFIG_AMD_HSMP=y" in kconfig: self.db.record_prereq( "HSMP driver `amd_hsmp` driver may conflict with amd_pmc", "❌", ) self.failures += [AmdHsmpBug()] return False cmdline = read_file(os.path.join("/proc", "cmdline")) blocked = "initcall_blacklist=hsmp_plt_init" in cmdline p = os.path.join("/", "sys", "module", "amd_hsmp") if os.path.exists(p) and not blocked: self.db.record_prereq("`amd_hsmp` driver may conflict with amd_pmc", "❌") self.failures += [AmdHsmpBug()] return False self.db.record_prereq( f"HSMP driver `amd_hsmp` not detected (blocked: {blocked})", "βœ…", ) return True def check_amd_pmc(self): """Check if the amd_pmc driver is loaded""" for device in self.pyudev.list_devices(subsystem="platform", DRIVER="amd_pmc"): message = "PMC driver `amd_pmc` loaded" p = os.path.join(device.sys_path, "smu_program") v = os.path.join(device.sys_path, "smu_fw_version") if os.path.exists(v): try: self.smu_version = read_file(v) self.smu_program = read_file(p) except TimeoutError: self.db.record_prereq( "failed to communicate using `amd_pmc` driver", "❌" ) return False message += f" (Program {self.smu_program} Firmware {self.smu_version})" self.db.record_prereq(message, "βœ…") return True self.failures += [MissingAmdPmc()] self.db.record_prereq( "PMC driver `amd_pmc` did not bind to any ACPI device", "❌" ) return False def check_wlan(self): """Checks for WLAN device""" for device in self.pyudev.list_devices(subsystem="pci", PCI_CLASS="28000"): slot = device.properties["PCI_SLOT_NAME"] driver = device.properties.get("DRIVER") if not driver: self.db.record_prereq(f"WLAN device in {slot} missing driver", "🚦") self.failures += [MissingDriver(slot)] return False self.db.record_prereq(f"WLAN driver `{driver}` bound to {slot}", "βœ…") return True def check_usb3(self): """Check for the USB4 controller""" slots = [] for device in self.pyudev.list_devices(subsystem="pci", PCI_CLASS="C0330"): slot = device.properties["PCI_SLOT_NAME"] if device.properties.get("DRIVER") != "xhci_hcd": self.db.record_prereq( f"USB3 controller for {slot} not using `xhci_hcd` driver", "❌" ) self.failures += [MissingXhciHcd()] return False slots += [slot] if slots: self.db.record_prereq( f"USB3 driver `xhci_hcd` bound to {', '.join(slots)}", "βœ…" ) return True def check_dpia_pg_dmcub(self): """Check if DMUB is new enough to PG DPIA when no USB4 present""" usb4_found = False for device in self.pyudev.list_devices(subsystem="pci", PCI_CLASS="C0340"): usb4_found = True break if usb4_found: self.db.record_debug("USB4 routers found, no need to check DMCUB version") return True # Check if matching DCN present for device in self.pyudev.list_devices(subsystem="pci"): current = None klass = device.properties.get("PCI_CLASS") if klass not in ["30000", "38000"]: continue pci_id = device.properties.get("PCI_ID") if not pci_id.startswith("1002"): continue hw_ver = {"major": 3, "minor": 5, "revision": 0} if not find_ip_version(device.sys_path, "DMU", hw_ver): continue # DCN was found, lookup version from sysfs p = os.path.join(device.sys_path, "fw_version", "dmcub_fw_version") if os.path.exists(p): current = int(read_file(p), 16) # no sysfs, try to look for version from debugfs if not current: slot = device.properties["PCI_SLOT_NAME"] p = os.path.join( "/", "sys", "kernel", "debug", "dri", slot, "amdgpu_firmware_info" ) contents = read_file(p) for line in contents.split("\n"): if not line.startswith("DMCUB"): continue current = int(line.split()[-1], 16) if current: expected = 0x09001B00 if current >= expected: return True self.db.record_prereq("DMCUB Firmware is outdated", "❌") self.failures += [DmcubTooOld(current, expected)] return False return True def check_usb4(self): """Check if the thunderbolt driver is loaded""" slots = [] for device in self.pyudev.list_devices(subsystem="pci", PCI_CLASS="C0340"): slot = device.properties["PCI_SLOT_NAME"] if device.properties.get("DRIVER") != "thunderbolt": self.db.record_prereq("USB4 driver `thunderbolt` missing", "❌") self.failures += [MissingThunderbolt()] return False slots += [slot] if slots: self.db.record_prereq( f"USB4 driver `thunderbolt` bound to {', '.join(slots)}", "βœ…" ) return True def check_sleep_mode(self): """Check if the system is configured for s2idle""" fn = os.path.join("/", "sys", "power", "mem_sleep") if not os.path.exists(fn): self.db.record_prereq("Kernel doesn't support sleep", "❌") return False cmdline = read_file(os.path.join("/proc", "cmdline")) if "mem_sleep_default=deep" in cmdline: self.db.record_prereq( "Kernel command line is configured for 'deep' sleep", "❌" ) self.failures += [DeepSleep()] return False if "[s2idle]" not in read_file(fn): self.failures += [SleepModeWrong()] self.db.record_prereq( "System isn't configured for s2idle in firmware setup", "❌" ) return False self.db.record_prereq("System is configured for s2idle", "βœ…") return True def capture_smbios(self): """Capture the SMBIOS (DMI) information""" p = os.path.join("/", "sys", "class", "dmi", "id") if not os.path.exists(p): self.db.record_prereq("DMI data was not setup", "🚦") self.failures += [DmiNotSetup()] return False else: keys = {} filtered = [ "product_serial", "board_serial", "board_asset_tag", "chassis_asset_tag", "chassis_serial", "modalias", "uevent", "product_uuid", ] for root, _dirs, files in os.walk(p, topdown=False): files.sort() for fname in files: if "power" in root: continue if fname in filtered: continue contents = read_file(os.path.join(root, fname)) keys[fname] = contents if ( "sys_vendor" not in keys or "product_name" not in keys or "product_family" not in keys ): self.db.record_prereq("DMI data not found", "❌") self.failures += [DmiNotSetup()] return False self.db.record_prereq( f"{keys['sys_vendor']} {keys['product_name']} ({keys['product_family']})", "πŸ’»", ) debug_str = "DMI|value\n" for key, value in keys.items(): if ( "product_name" in key or "sys_vendor" in key or "product_family" in key ): continue debug_str += f"{key}| {value}\n" self.db.record_debug(debug_str) return True def check_lps0(self): """Check if LPS0 is enabled""" for m in ["acpi", "acpi_x86"]: p = os.path.join("/", "sys", "module", m, "parameters", "sleep_no_lps0") if not os.path.exists(p): continue fail = read_file(p) == "Y" if fail: self.db.record_prereq("LPS0 _DSM disabled", "❌") else: self.db.record_prereq("LPS0 _DSM enabled", "βœ…") return not fail self.db.record_prereq("LPS0 _DSM not found", "πŸ‘€") return False def get_cpu_vendor(self) -> str: """Fetch information about the CPU vendor""" p = os.path.join("/", "proc", "cpuinfo") vendor = "" cpu = read_file(p) for line in cpu.split("\n"): if "vendor_id" in line: vendor = line.split()[-1] continue elif "cpu family" in line: self.cpu_family = int(line.split()[-1]) continue elif "model name" in line: self.cpu_model_string = line.split(":")[-1].strip() continue elif "model" in line: self.cpu_model = int(line.split()[-1]) continue if self.cpu_family and self.cpu_model and self.cpu_model_string: self.db.record_prereq( f"{self.cpu_model_string} " f"(family {self.cpu_family:x} model {self.cpu_model:x})", "πŸ’»", ) break return vendor # See https://github.com/torvalds/linux/commit/ec6c0503190417abf8b8f8e3e955ae583a4e50d4 def check_fadt(self): """Check the kernel emitted a message indicating FADT had a bit set.""" found = False if not self.kernel_log: message = "Unable to test FADT from kernel log" self.db.record_prereq(message, "🚦") else: self.kernel_log.seek() matches = ["Low-power S0 idle used by default for system suspend"] found = self.kernel_log.match_line(matches) # try to look at FACP directly if not found (older kernel compat) if not found: self.db.record_debug("Fetching low power idle bit directly from FADT") target = os.path.join("/", "sys", "firmware", "acpi", "tables", "FACP") try: with open(target, "rb") as r: r.seek(0x70) found = struct.unpack("> 8) & 0xFF if level_type == 0: self.db.record_prereq( "Unable to discover CPU topology, didn't find socket level", "🚦", ) return True if level_type == 4: break _, cpu_count, _, _ = read_cpuid(0, 0x80000026, level) if cpu_count > max_cpus: self.db.record_prereq( f"The kernel has been limited to {max_cpus} CPU cores, " f"but the system has {cpu_count} cores", "❌", ) self.failures += [LimitedCores(cpu_count, max_cpus)] return False self.db.record_debug(f"CPU core count: {cpu_count} max: {max_cpus}") except FileNotFoundError: self.db.record_prereq( "Unable to check CPU topology: cpuid kernel module not loaded", "❌" ) return False except PermissionError: self.db.record_prereq("CPUID checks unavailable", "🚦") return True def check_msr(self): """Check if PC6 or CC6 has been disabled""" def check_bits(value, mask): return value & mask expect = { 0xC0010292: BIT(32), # PC6 0xC0010296: (BIT(22) | BIT(14) | BIT(6)), # CC6 } try: for reg, expect_val in expect.items(): val = read_msr(reg, 0) if not check_bits(val, expect_val): self.failures += [MSRFailure()] return False self.db.record_prereq("PC6 and CC6 enabled", "βœ…") except FileNotFoundError: self.db.record_prereq( "Unable to check MSRs: MSR kernel module not loaded", "❌" ) return False except PermissionError: self.db.record_prereq("MSR checks unavailable", "🚦") return True def check_smt(self): """Check if SMT is enabled""" p = os.path.join("/", "sys", "devices", "system", "cpu", "smt", "control") v = read_file(p) self.db.record_debug(f"SMT control: {v}") if v == "notsupported": return True p = os.path.join("/", "sys", "devices", "system", "cpu", "smt", "active") v = read_file(p) if v == "0": self.failures += [SMTNotEnabled()] self.db.record_prereq("SMT is not enabled", "❌") return False self.db.record_prereq("SMT enabled", "βœ…") return True def check_iommu(self): """Check IOMMU configuration""" affected_1a = ( list(range(0x20, 0x2F)) + list(range(0x60, 0x6F)) + list(range(0x70, 0x7F)) ) debug_str = "" if self.cpu_family == 0x1A and self.cpu_model in affected_1a: found_iommu = False found_acpi = False for dev in self.pyudev.list_devices(subsystem="iommu"): found_iommu = True debug_str += f"Found IOMMU {dev.sys_path}\n" break # User turned off IOMMU, no problems if not found_iommu: self.db.record_prereq("IOMMU disabled", "βœ…") return True # Look for MSFT0201 in DSDT/SSDT for dev in self.pyudev.list_devices(subsystem="acpi"): if "MSFT0201" in dev.sys_path: found_acpi = True if not found_acpi: self.db.record_prereq( "IOMMU is misconfigured: missing MSFT0201 ACPI device", "❌" ) self.failures += [MissingIommuACPI("MSFT0201")] return False # Check that policy is bound to it for dev in self.pyudev.list_devices(subsystem="platform"): if "MSFT0201" in dev.sys_path: p = os.path.join(dev.sys_path, "iommu") if not os.path.exists(p): self.failures += [MissingIommuPolicy("MSFT0201")] return False # Check pre-boot DMA p = os.path.join("/", "sys", "firmware", "acpi", "tables", "IVRS") with open(p, "rb") as f: data = f.read() if len(data) < 40: raise ValueError( "IVRS table appears too small to contain virtualization info." ) virt_info = struct.unpack_from("I", data, 36)[0] debug_str += f"IVRS: Virtualization info: 0x{virt_info:x}\n" found_ivrs_dmar = (virt_info & 0x2) != 0 # check for MSFT0201 in IVRS (alternative to pre-boot DMA) target_bytes = "MSFT0201".encode("utf-8") found_ivrs_msft0201 = data.find(target_bytes) != -1 debug_str += f"IVRS: Found MSFT0201: {found_ivrs_msft0201}" self.db.record_debug(debug_str) if not found_ivrs_dmar and not found_ivrs_msft0201: self.db.record_prereq( "IOMMU is misconfigured: Pre-boot DMA protection not enabled", "❌" ) self.failures += [DMArNotEnabled()] return False self.db.record_prereq("IOMMU properly configured", "βœ…") return True def check_port_pm_override(self): """Check for PCIe port power management override""" if self.cpu_family != 0x19: return True if self.cpu_model not in [0x74, 0x78]: return True if not self.smu_version: return True if version.parse(self.smu_version) > version.parse("76.60.0"): return True if version.parse(self.smu_version) < version.parse("76.18.0"): return True cmdline = read_file(os.path.join("/proc", "cmdline")) if "pcie_port_pm=off" in cmdline: return True self.db.record_prereq( "Platform may hang resuming. " "Upgrade your firmware or add pcie_port_pm=off to kernel command " "line if you have problems.", "🚦", ) return False def check_taint(self): """Check if the kernel is tainted""" fn = os.path.join("/", "proc", "sys", "kernel", "tainted") taint = int(read_file(fn)) # ignore kernel warnings taint &= ~BIT(9) if taint != 0: self.db.record_prereq(f"Kernel is tainted: {taint}", "🚦") self.failures += [TaintedKernel()] return True def run(self): """Run the prerequisites check""" msg = "Checking prerequisites, please wait" print_temporary_message(msg) info = [ self.capture_smbios, self.capture_kernel_version, self.capture_battery, self.capture_linux_firmware, self.capture_logind, self.capture_pci_acpi, self.capture_edid, self.capture_nvidia, self.capture_cstates, ] checks = [] vendor = self.get_cpu_vendor() if vendor == "AuthenticAMD": info += [ self.capture_disabled_pins, ] checks += [ self.check_aspm, self.check_i2c_hid, self.check_pinctrl_amd, self.check_amd_hsmp, self.check_amd_xdna, self.check_amd_pmc, self.check_amd_cpu_hpet_wa, self.check_port_pm_override, self.check_usb3, self.check_usb4, self.check_sleep_mode, self.check_storage, self.check_wcn6855_bug, self.check_amdgpu, self.check_amdgpu_parameters, self.check_cpu, self.check_msr, self.check_smt, self.check_iommu, self.check_asus_rog_ally, self.check_dpia_pg_dmcub, self.check_isp4, ] checks += [ self.check_fadt, self.check_logger, self.check_lps0, self.check_permissions, self.check_wlan, self.check_taint, self.capture_acpi, self.map_acpi_path, self.check_device_firmware, self.check_network, ] for i in info: i() result = True for check in checks: if not check(): result = False if not result: self.db.record_prereq(Headers.BrokenPrerequisites, "🚫") self.db.record_debug(self.kernel_log.get_full_log()) self.db.sync() clear_temporary_message(len(msg)) return result def report(self) -> None: """Print a report of the results of the checks.""" ts = self.db.get_last_prereq_ts() t0 = datetime.strptime(str(ts), "%Y%m%d%H%M%S") for row in self.db.report_prereq(t0): print_color(row[2], row[3]) for row in self.db.report_debug(t0): for line in row[0].split("\n"): if self.debug: print_color(line, "🦟") else: logging.debug(line) if len(self.failures) == 0: return print_color(Headers.ExplanationReport, "πŸ—£οΈ") for item in self.failures: item.get_failure() amd-debug-tools-0.2.15/src/amd_debug/pstate.py000066400000000000000000000225341515405217400211440ustar00rootroot00000000000000# SPDX-License-Identifier: MIT """CPPC triage script for AMD systems""" import os import argparse import re import sys import pandas as pd from tabulate import tabulate from pyudev import Context from amd_debug.common import ( AmdTool, get_pretty_distro, print_color, read_file, read_msr, relaunch_sudo, show_log_info, version, ) class MSR: # pylint: disable=too-few-public-methods """MSR addresses for CPPC""" MSR_AMD_CPPC_CAP1 = 0xC00102B0 MSR_AMD_CPPC_ENABLE = 0xC00102B1 MSR_AMD_CPPC_CAP2 = 0xC00102B2 MSR_AMD_CPPC_REQ = 0xC00102B3 MSR_AMD_CPPC_STATUS = 0xC00102B4 def amd_cppc_cap_lowest_perf(x): """Return the lowest performance value from the given input.""" return x & 0xFF def amd_cppc_cap_lownonlin_perf(x): """Return the lowest nonlinear performance value from the given input.""" return (x >> 8) & 0xFF def amd_cppc_cap_nominal_perf(x): """Return the nominal performance value from the given input.""" return (x >> 16) & 0xFF def amd_cppc_cap_highest_perf(x): """Return the highest performance value from the given input.""" return (x >> 24) & 0xFF def amd_cppc_max_perf(x): """Return the maximum performance value from the given input.""" return x & 0xFF def amd_cppc_min_perf(x): """Return the minimum performance value from the given input.""" return (x >> 8) & 0xFF def amd_cppc_des_perf(x): """Return the desired performance value from the given input.""" return (x >> 16) & 0xFF def amd_cppc_epp_perf(x): """Return the energy performance preference value from the given input.""" return (x >> 24) & 0xFF class AmdPstateTriage(AmdTool): """Class for handling the triage process""" def __init__(self, logging): log_prefix = "pstate" if logging else None super().__init__(log_prefix) relaunch_sudo() pretty = get_pretty_distro() print_color(f"{pretty}", "🐧") self.context = Context() def gather_amd_pstate_info(self): """Gather AMD Pstate global information""" for f in ("status", "prefcore"): p = os.path.join("/", "sys", "devices", "system", "cpu", "amd_pstate", f) if os.path.exists(p): print_color(f"'{f}':\t{read_file(p)}", "β—‹") def gather_kernel_info(self): """Gather kernel information""" print_color(f"Kernel:\t{os.uname().release}", "🐧") def gather_scheduler_info(self): """Gather information about the scheduler""" procfs = os.path.join("/", "proc", "sys", "kernel", "sched_itmt_enabled") debugfs = os.path.join( "/", "sys", "kernel", "debug", "x86", "sched_itmt_enabled" ) for p in [procfs, debugfs]: if os.path.exists(p): val = read_file(p) print_color(f"ITMT:\t{val}", "🐧") def gather_cpu_info(self): """Gather a dataframe of CPU information""" df = pd.DataFrame( columns=[ "CPU #", "CPU Min Freq", "CPU Nonlinear Freq", "CPU Max Freq", "Scaling Min Freq", "Scaling Max Freq", "Energy Performance Preference", "Prefcore", "Boost", ] ) for device in self.context.list_devices(subsystem="cpu"): p = os.path.join(device.sys_path, "cpufreq") if not os.path.exists(p): continue row = [ int(re.findall(r"\d+", f"{device.sys_name}")[0]), read_file(os.path.join(p, "cpuinfo_min_freq")), read_file(os.path.join(p, "amd_pstate_lowest_nonlinear_freq")), read_file(os.path.join(p, "cpuinfo_max_freq")), read_file(os.path.join(p, "scaling_min_freq")), read_file(os.path.join(p, "scaling_max_freq")), read_file(os.path.join(p, "energy_performance_preference")), read_file(os.path.join(p, "amd_pstate_prefcore_ranking")), read_file(os.path.join(p, "boost")), ] df = pd.concat( [pd.DataFrame([row], columns=df.columns), df], ignore_index=True ) cpuinfo = read_file("/proc/cpuinfo") model = re.findall(r"model name\s+:\s+(.*)", cpuinfo)[0] print_color(f"CPU:\t\t{model}", "πŸ’»") df = df.sort_values(by="CPU #") print_color( "Per-CPU sysfs files\n%s" % tabulate(df, headers="keys", tablefmt="psql", showindex=False), "πŸ”‹", ) def gather_msrs(self): """Gather MSR information""" cpus = [] for device in self.context.list_devices(subsystem="cpu"): cpu = int(re.findall(r"\d+", f"{device.sys_name}")[0]) cpus.append(cpu) cpus.sort() df = pd.DataFrame( columns=[ "CPU #", "Min Perf", "Max Perf", "Desired Perf", "Energy Performance Perf", ] ) msr_df = pd.DataFrame( columns=[ "CPU #", "Enable", "Status", "Cap 1", "Cap 2", "Request", ] ) cap_df = pd.DataFrame( columns=[ "CPU #", "Lowest Perf", "Nonlinear Perf", "Nominal Perf", "Highest Perf", ] ) try: for cpu in cpus: enable = read_msr(MSR.MSR_AMD_CPPC_ENABLE, cpu) status = read_msr(MSR.MSR_AMD_CPPC_STATUS, cpu) cap1 = read_msr(MSR.MSR_AMD_CPPC_CAP1, cpu) cap2 = read_msr(MSR.MSR_AMD_CPPC_CAP2, cpu) req = read_msr(MSR.MSR_AMD_CPPC_REQ, cpu) row = [ cpu, amd_cppc_min_perf(req), amd_cppc_max_perf(req), amd_cppc_des_perf(req), amd_cppc_epp_perf(req), ] df = pd.concat( [pd.DataFrame([row], columns=df.columns), df], ignore_index=True ) row = [ cpu, enable, status, hex(cap1), hex(cap2), hex(req), ] msr_df = pd.concat( [pd.DataFrame([row], columns=msr_df.columns), msr_df], ignore_index=True, ) row = [ cpu, amd_cppc_cap_lowest_perf(cap1), amd_cppc_cap_lownonlin_perf(cap1), amd_cppc_cap_nominal_perf(cap1), amd_cppc_cap_highest_perf(cap1), ] cap_df = pd.concat( [pd.DataFrame([row], columns=cap_df.columns), cap_df], ignore_index=True, ) except FileNotFoundError: print_color("Unable to check MSRs: MSR kernel module not loaded", "❌") return False except PermissionError: print_color("MSR checks unavailable", "🚦") return msr_df = msr_df.sort_values(by="CPU #") print_color( "CPPC MSRs\n%s" % tabulate(msr_df, headers="keys", tablefmt="psql", showindex=False), "πŸ”‹", ) cap_df = cap_df.sort_values(by="CPU #") print_color( "MSR_AMD_CPPC_CAP1 (decoded)\n%s" % tabulate(cap_df, headers="keys", tablefmt="psql", showindex=False), "πŸ”‹", ) df = df.sort_values(by="CPU #") print_color( "MSR_AMD_CPPC_REQ (decoded)\n%s" % tabulate(df, headers="keys", tablefmt="psql", showindex=False), "πŸ”‹", ) def run(self): """Run the triage process""" self.gather_kernel_info() self.gather_amd_pstate_info() self.gather_scheduler_info() try: self.gather_cpu_info() except FileNotFoundError: print_color("Unable to gather CPU information", "❌") return False self.gather_msrs() return True def parse_args(): """Parse command line arguments.""" parser = argparse.ArgumentParser( description="Collect useful information for debugging amd-pstate issues.", epilog="Arguments are optional", ) subparsers = parser.add_subparsers(help="Possible commands", dest="command") triage_cmd = subparsers.add_parser("triage", help="Run amd-pstate triage") triage_cmd.add_argument( "--tool-debug", action="store_true", help="Enable tool debug logging", ) parser.add_argument( "--version", action="store_true", help="Show version information" ) if len(sys.argv) == 1: parser.print_help(sys.stderr) sys.exit(1) return parser.parse_args() def main() -> None | int: """Main function""" args = parse_args() ret = False if args.version: print(version()) return elif args.command == "triage": triage = AmdPstateTriage(args.tool_debug) ret = triage.run() show_log_info() if ret is False: return 1 return amd-debug-tools-0.2.15/src/amd_debug/s2idle-hook000066400000000000000000000032371515405217400213340ustar00rootroot00000000000000#!/usr/bin/python3 # SPDX-License-Identifier: MIT import argparse import logging import sys def main(action, debug): """Main function to run the systemd hook""" try: from amd_debug.validator import ( SleepValidator, ) # pylint: disable=import-outside-toplevel from amd_debug.prerequisites import ( PrerequisiteValidator, ) # pylint: disable=import-outside-toplevel except ModuleNotFoundError: return app = SleepValidator(tool_debug=debug, bios_debug=False) if action == "pre": prereq = PrerequisiteValidator(tool_debug=debug) prereq.capture_once() app.systemd_pre_hook() elif action == "post": app.systemd_post_hook() def parse_args(): """Parse command line arguments""" parser = argparse.ArgumentParser( description="amd-s2idle systemd hook", ) parser.add_argument( "action", choices=["pre", "post"], help="Action to perform", ) parser.add_argument( "--debug", action="store_true", ) parser.add_argument( "mode", help="Mode to perform", ) parser.add_argument("--log", default="/dev/null") parser.add_argument("--path", default="") if len(sys.argv) == 1: parser.print_help(sys.stderr) sys.exit(1) return parser.parse_args() if __name__ == "__main__": args = parse_args() if args.mode != "suspend": sys.exit(0) if args.path: sys.path.append(args.path) logging.basicConfig( filename=args.log, level=logging.DEBUG if args.debug else logging.WARNING ) main(args.action, args.debug) amd-debug-tools-0.2.15/src/amd_debug/s2idle.py000066400000000000000000000323721515405217400210270ustar00rootroot00000000000000# SPDX-License-Identifier: MIT """s2idle analysis tool""" import argparse import sys import os import subprocess import sqlite3 from datetime import date, timedelta, datetime from amd_debug.common import ( convert_string_to_bool, colorize_choices, is_root, relaunch_sudo, show_log_info, version, running_ssh, ) from amd_debug.validator import SleepValidator from amd_debug.installer import Installer from amd_debug.prerequisites import PrerequisiteValidator from amd_debug.sleep_report import SleepReport class Defaults: """Default values for the script""" duration = 10 wait = 4 count = 1 since = date.today() - timedelta(days=60) until = date.today() + timedelta(days=1) format_choices = ["txt", "md", "html", "stdout"] boolean_choices = ["true", "false"] class Headers: """Headers for the script""" DurationDescription = "How long should suspend cycles last (seconds)" WaitDescription = "How long to wait in between suspend cycles (seconds)" CountDescription = "How many suspend cycles to run" SinceDescription = "What date to start report data" UntilDescription = "What date to end report data" LogDescription = "Location of log file" ReportFileDescription = "Location of report file" FormatDescription = "What format to output the report in" MaxDurationDescription = "What is the maximum suspend cycle length (seconds)" MaxWaitDescription = "What is the maximum time between suspend cycles (seconds)" ReportDebugDescription = "Enable debug output in report (increased size)" def display_report_file(fname, fmt) -> None: """Display report file""" if fmt != "html": return if not is_root(): subprocess.call(["xdg-open", fname]) return user = os.environ.get("SUDO_USER") if user: env_vars = [] for var in ["DISPLAY", "WAYLAND_DISPLAY", "XAUTHORITY", "XDG_RUNTIME_DIR", "DBUS_SESSION_BUS_ADDRESS", "XDG_SESSION_TYPE"]: value = os.environ.get(var) if value: env_vars.append(f"{var}={value}") if env_vars: cmd = ["sudo", "-u", user] + [f"env"] + env_vars + ["xdg-open", fname] subprocess.call(cmd) else: print( "Unable to detect graphical session environment. " "Report saved to: " + fname ) def get_report_file(report_file, extension) -> str: """Prompt user for report file""" if extension == "stdout": return "" if not report_file: return f"amd-s2idle-report-{date.today()}.{extension}" return report_file def get_report_format() -> str: """Get report format""" if running_ssh(): return "txt" return "html" def prompt_report_arguments(since, until, fname, fmt, report_debug) -> list: """Prompt user for report configuration""" if not since: default = Defaults.since since = input(f"{Headers.SinceDescription} (default {default})? ") if not since: since = default.isoformat() try: since = datetime.fromisoformat(since) except ValueError as e: sys.exit(f"Invalid date, use YYYY-MM-DD: {e}") if not until: default = Defaults.until until = input(f"{Headers.SinceDescription} (default {default})? ") if not until: until = default.isoformat() try: until = datetime.fromisoformat(until) except ValueError as e: sys.exit(f"Invalid date, use YYYY-MM-DD: {e}") if not fmt: fmt = input( f"{Headers.FormatDescription} ({colorize_choices(Defaults.format_choices, get_report_format())})? " ) if not fmt: fmt = get_report_format() if fmt not in Defaults.format_choices: sys.exit(f"Invalid format: {fmt}") if report_debug is None: inp = ( input( f"{Headers.ReportDebugDescription} ({colorize_choices(Defaults.boolean_choices, 'true')})? " ) .lower() .capitalize() ) report_debug = True if not inp else convert_string_to_bool(inp) return [since, until, get_report_file(fname, fmt), fmt, report_debug] def prompt_test_arguments(duration, wait, count, rand) -> list: """Prompt user for test configuration""" if not duration: if rand: question = Headers.MaxDurationDescription else: question = Headers.DurationDescription duration = input(f"{question} (default {Defaults.duration})? ") if not duration: duration = Defaults.duration try: duration = int(duration) except ValueError as e: sys.exit(f"Invalid duration: {e}") if not wait: if rand: question = Headers.MaxWaitDescription else: question = Headers.WaitDescription wait = input(f"{question} (default {Defaults.wait})? ") if not wait: wait = Defaults.wait try: wait = int(wait) except ValueError as e: sys.exit(f"Invalid wait: {e}") if not count: count = input(f"{Headers.CountDescription} (default {Defaults.count})? ") if not count: count = Defaults.count try: count = int(count) except ValueError as e: sys.exit(f"Invalid count: {e}") return [duration, wait, count] def report(since, until, fname, fmt, tool_debug, report_debug) -> bool: """Generate a report from previous sleep cycles""" try: since, until, fname, fmt, report_debug = prompt_report_arguments( since, until, fname, fmt, report_debug ) except KeyboardInterrupt: sys.exit("\nReport generation cancelled") try: app = SleepReport( since=since, until=until, fname=fname, fmt=fmt, tool_debug=tool_debug, report_debug=report_debug, ) except sqlite3.OperationalError as e: print(f"Failed to generate report: {e}") return False except PermissionError as e: print(f"Failed to generate report: {e}") return False try: app.run() except PermissionError as e: print(f"Failed to generate report: {e}") return False except ValueError as e: print(f"Failed to generate report: {e}") return False display_report_file(fname, fmt) return True def run_test_cycle( duration, wait, count, fmt, fname, force, debug, rand, logind, bios_debug ) -> bool: """Run a test""" app = Installer(tool_debug=debug) app.set_requirements("iasl", "ethtool", "edid-decode") if not app.install_dependencies(): print("Failed to install dependencies") return False try: duration, wait, count = prompt_test_arguments(duration, wait, count, rand) total_seconds = (duration + wait) * count until_time = datetime.now() + timedelta(seconds=total_seconds) since, until, fname, fmt, report_debug = prompt_report_arguments( datetime.now().isoformat(), until_time.isoformat(), fname, fmt, True ) except KeyboardInterrupt: sys.exit("\nTest cancelled") try: app = PrerequisiteValidator(debug) run = app.run() except PermissionError as e: print(f"Failed to run prerequisite check: {e}") return False app.report() if run or force: app = SleepValidator(tool_debug=debug, bios_debug=bios_debug) run = app.run( duration=duration, wait=wait, count=count, rand=rand, logind=logind, ) until = datetime.now() else: since = None until = None app = SleepReport( since=since, until=until, fname=fname, fmt=fmt, tool_debug=debug, report_debug=report_debug, ) app.run() # open report in browser if it's html display_report_file(fname, fmt) return True def install(debug) -> None: """Install the tool""" installer = Installer(tool_debug=debug) installer.set_requirements("iasl", "ethtool", "edid-decode") if not installer.install_dependencies(): sys.exit("Failed to install dependencies") try: app = PrerequisiteValidator(debug) run = app.run() except PermissionError as e: sys.exit(f"Failed to run prerequisite check: {e}") if not run: app.report() sys.exit("Failed to meet prerequisites") if not installer.install(): sys.exit("Failed to install") def uninstall(debug) -> None: """Uninstall the tool""" app = Installer(tool_debug=debug) if not app.remove(): sys.exit("Failed to remove") def parse_args(): """Parse command line arguments""" parser = argparse.ArgumentParser( description="Swiss army knife for analyzing Linux s2idle problems", epilog="The tool can run an immediate test with the 'test' command " "or can be used to hook into systemd for building reports later.\n" "All optional arguments will be prompted if needed.\n" "To use non-interactively, please populate all optional arguments.", ) subparsers = parser.add_subparsers(help="Possible commands", dest="action") # 'test' command test_cmd = subparsers.add_parser("test", help="Run amd-s2idle test and report") test_cmd.add_argument("--count", help=Headers.CountDescription) test_cmd.add_argument( "--duration", help=Headers.DurationDescription, ) test_cmd.add_argument( "--wait", help=Headers.WaitDescription, ) test_cmd.add_argument( "--logind", action="store_true", help="Use logind to suspend system" ) test_cmd.add_argument( "--random", action="store_true", help="Run sleep cycles for random durations and wait, using the " "--duration and --wait arguments as an upper bound", ) test_cmd.add_argument( "--force", action="store_true", help="Run suspend test even if prerequisites failed", ) test_cmd.add_argument( "--format", choices=Defaults.format_choices, help="Report format", ) test_cmd.add_argument( "--tool-debug", action="store_true", help="Enable tool debug logging", ) test_cmd.add_argument( "--bios-debug", action="store_true", help="Enable BIOS debug logging instead of notify logging", ) test_cmd.add_argument("--report-file", help=Headers.ReportFileDescription) # 'report' command report_cmd = subparsers.add_parser( "report", help="Generate amd-s2idle report from previous runs" ) report_cmd.add_argument( "--since", help=Headers.SinceDescription, ) report_cmd.add_argument( "--until", default=Defaults.until.isoformat(), help=Headers.UntilDescription, ) report_cmd.add_argument("--report-file", help=Headers.ReportFileDescription) report_cmd.add_argument( "--format", choices=Defaults.format_choices, help="Report format", ) report_cmd.add_argument( "--tool-debug", action="store_true", help="Enable tool debug logging", ) report_cmd.add_argument( "--report-debug", action=argparse.BooleanOptionalAction, help="Include debug messages in report (WARNING: can significantly increase report size)", ) # if running in a venv, install/uninstall hook options if sys.prefix != sys.base_prefix: install_cmd = subparsers.add_parser( "install", help="Install systemd s2idle hook" ) uninstall_cmd = subparsers.add_parser( "uninstall", help="Uninstall systemd s2idle hook" ) install_cmd.add_argument( "--tool-debug", action="store_true", help="Enable tool debug logging", ) uninstall_cmd.add_argument( "--tool-debug", action="store_true", help="Enable tool debug logging", ) parser.add_argument( "--version", action="store_true", help="Show version information" ) if len(sys.argv) == 1: parser.print_help(sys.stderr) sys.exit(1) return parser.parse_args() def main() -> None | int: """Main function""" args = parse_args() ret = False if args.action == "install": relaunch_sudo() install(args.tool_debug) elif args.action == "uninstall": relaunch_sudo() uninstall(args.tool_debug) elif args.action == "report": ret = report( args.since, args.until, args.report_file, args.format, args.tool_debug, args.report_debug, ) elif args.action == "test": relaunch_sudo() ret = run_test_cycle( args.duration, args.wait, args.count, args.format, args.report_file, args.force, args.tool_debug, args.random, args.logind, args.bios_debug, ) elif args.version: print(version()) return else: sys.exit("no action specified") show_log_info() if ret is False: return 1 return amd-debug-tools-0.2.15/src/amd_debug/sleep_report.py000066400000000000000000000416221515405217400223460ustar00rootroot00000000000000# SPDX-License-Identifier: MIT import os import re import math from datetime import datetime, timedelta import numpy as np from tabulate import tabulate from jinja2 import Environment, FileSystemLoader import pandas as pd from amd_debug.database import SleepDatabase from amd_debug.common import ( AmdTool, Colors, version, clear_temporary_message, get_group_color, get_log_priority, print_color, print_temporary_message, ) from amd_debug.failures import ( SpuriousWakeup, LowHardwareSleepResidency, ) from amd_debug.wake import WakeIRQ, WakeGPIO def remove_duplicates(x): """Remove duplicates from a string""" temp = re.findall(r"\d+", x) res = list(map(int, temp)) return list(set(res)) def format_gpio_as_str(x): """Format GPIO as a nicer format""" ret = [] for y in remove_duplicates(x): ret.append(str(WakeGPIO(y))) return ", ".join(ret) def format_irq_as_str(x): """Format IRQ as a nicer format""" ret = [] for y in remove_duplicates(x): ret.append(str(WakeIRQ(y))) return ", ".join(ret) def format_as_human(x): """Format as a human readable date""" return datetime.strptime(str(x), "%Y%m%d%H%M%S") def format_as_seconds(x): """Format as seconds""" return format_as_human(x).timestamp() def format_watts(val): """Format watts as a nicer format""" return f"{val:.02f}W" def format_percent(val): """Format percent as a nicer format""" return f"{val:.02f}%" def format_timedelta(val): """Format seconds as a nicer format""" if math.isnan(val): val = 0 return str(timedelta(seconds=val)) def parse_hw_sleep(hw): """Parse the hardware sleep value, throwing out garbage values""" if hw > 1: return 0 return hw * 100 class SleepReport(AmdTool): """Sleep report class""" def __init__(self, since, until, fname, fmt, tool_debug, report_debug): log_prefix = "s2idle" if tool_debug else None super().__init__(log_prefix) self.db = SleepDatabase() self.fname = fname self.since = since self.until = until self.debug = report_debug self.format = fmt self.failures = [] if since and until: self.df = self.db.report_summary_dataframe(self.since, self.until) self.pre_process_dataframe() else: self.df = pd.DataFrame( columns=[ "t0", "t1", "requested", "hw", "b0", "b1", "full", "wake_irq", "gpio", ] ) self.battery_svg = None self.hwsleep_svg = None def analyze_duration(self, index, t0, t1, requested, hw): """Analyze the duration of the cycle""" duration = t1 - t0 if duration.total_seconds() >= 60 and hw < 90: failure = LowHardwareSleepResidency(duration.seconds, hw) problem = failure.get_description() data = str(failure) if self.format == "html": self.failures.append( {"cycle_num": index, "problem": problem, "data": data} ) else: self.failures.append([index, problem, data]) if not math.isnan(requested): min_suspend_duration = timedelta(seconds=requested * 0.9) expected_wake_time = t0 + min_suspend_duration if t1 < expected_wake_time: failure = SpuriousWakeup(requested, duration) problem = failure.get_description() data = str(failure) if self.format == "html": self.failures.append( {"cycle_num": index, "problem": problem, "data": data} ) else: self.failures.append([index, problem, data]) def pre_process_dataframe(self): """Pre-process the pandas dataframe""" self.df["Duration"] = self.df["t1"].apply(format_as_seconds) - self.df[ "t0" ].apply(format_as_seconds) self.df["Duration"] = self.df["Duration"].replace(0, np.nan) self.df["Hardware Sleep"] = (self.df["hw"] / self.df["Duration"]).apply( parse_hw_sleep ) if not self.df["b0"].isnull().all(): self.df["Battery Start"] = self.df["b0"] / self.df["full"] * 100 self.df["Battery Delta"] = ( (self.df["b1"] - self.df["b0"]) / self.df["full"] * 100 ) self.df["Battery Ave Rate"] = ( (self.df["b1"] - self.df["b0"]) / self.df["Duration"] / 360 ) # Wake sources self.df["Wake Pin"] = self.df["gpio"].apply(format_gpio_as_str) self.df["Wake Interrupt"] = self.df["wake_irq"].apply(format_irq_as_str) del self.df["gpio"] del self.df["wake_irq"] # Look for spurious wakeups and low hardware residency [ self.analyze_duration(index, t0, t1, requested, hw) for index, t0, t1, requested, hw in zip( self.df.index, self.df["t0"].apply(format_as_human), self.df["t1"].apply(format_as_human), self.df["requested"], self.df["Hardware Sleep"], ) ] del self.df["requested"] # Only keep data needed self.df.rename(columns={"t0": "Start Time"}, inplace=True) self.df["Start Time"] = self.df["Start Time"].apply(format_as_human) del self.df["b1"] del self.df["b0"] del self.df["full"] del self.df["t1"] del self.df["hw"] def post_process_dataframe(self): """Display pandas dataframe in a more user friendly format""" self.df["Duration"] = self.df["Duration"].apply(format_timedelta) self.df["Hardware Sleep"] = self.df["Hardware Sleep"].apply(format_percent) if "Battery Start" in self.df.columns: self.df["Battery Start"] = self.df["Battery Start"].apply(format_percent) self.df["Battery Delta"] = self.df["Battery Delta"].apply(format_percent) self.df["Battery Ave Rate"] = self.df["Battery Ave Rate"].apply( format_watts ) def convert_table_dataframe(self, content): """Convert a table like dataframe to an HTML table""" header = False rows = [] for line in content.split("\n"): # only include header once if "int|active" in line: if header: continue header = True line = line.strip("β”‚") line = line.replace("β”œβ”€", "└─") if "|" in line: # first column missing '|' rows.append(line.replace("\t", "|")) columns = [row.split("|") for row in rows] df = pd.DataFrame(columns[1:], columns=columns[0]) return df.to_html(index=False, justify="center", col_space=30) def get_prereq_data(self): """Get the prereq data""" prereq = [] prereq_debug = [] tables = [ "int|active", "ACPI name", "PCI Slot", "DMI|value", ] ts = self.db.get_last_prereq_ts() if not ts: return [], "", [] t0 = datetime.strptime(str(ts), "%Y%m%d%H%M%S") for row in self.db.report_prereq(t0): prereq.append({"symbol": row[3], "text": row[2]}) if self.debug: for row in self.db.report_debug(t0): content = row[0] if self.format == "html" and [ table for table in tables if table in content ]: content = self.convert_table_dataframe(content) prereq_debug.append({"data": f"{content.strip()}"}) return prereq, t0, prereq_debug def get_cycle_data(self): """Get the cycle data""" cycles = [] debug = [] tables = ["Wakeup Source"] num = 0 for cycle in self.df["Start Time"]: if self.format == "html": data = "" for line in self.db.report_cycle_data(cycle).split("\n"): data += f"

{line}

" cycles.append({"cycle_num": num, "data": data}) else: cycles.append([num, self.db.report_cycle_data(cycle)]) if self.debug: messages = [] priorities = [] for row in self.db.report_debug(cycle): content = row[0] if self.format == "html" and [ table for table in tables if table in content ]: content = self.convert_table_dataframe(content) messages.append(content) priorities.append(get_log_priority(row[1])) debug.append( {"cycle_num": num, "messages": messages, "priorities": priorities} ) num += 1 return cycles, debug def build_template(self, inc_prereq) -> str: """Build the template for the report using jinja2""" import amd_debug # pylint: disable=import-outside-toplevel # Load the template p = os.path.dirname(amd_debug.__file__) environment = Environment(loader=FileSystemLoader(os.path.join(p, "templates"))) template = environment.get_template(self.format) # Load the prereq data prereq = None prereq_debug = None prereq_date = None if inc_prereq: prereq, prereq_date, prereq_debug = self.get_prereq_data() # Load the cycle and/or debug data if not self.df.empty: cycles, debug = self.get_cycle_data() self.post_process_dataframe() failures = None if self.format == "md": summary = self.df.to_markdown(floatfmt=".02f") cycle_data = tabulate( cycles, headers=["Cycle", "data"], tablefmt="pipe" ) if self.failures: failures = tabulate( self.failures, headers=["Cycle", "Problem", "Explanation"], tablefmt="pipe", ) elif self.format == "txt": summary = tabulate( self.df, headers=self.df.columns, tablefmt="fancy_grid" ) cycle_data = tabulate( cycles, headers=["Cycle", "data"], tablefmt="fancy_grid" ) if self.failures: failures = tabulate( self.failures, headers=["Cycle", "Problem", "Explanation"], tablefmt="fancy_grid", ) elif self.format == "html": summary = "" row = 0 # we will use javascript to highlight the high values for line in self.df.to_html( table_id="summary", render_links=True ).split("\n"): if "" in line: line = line.replace( "", f'', ) row = row + 1 summary += line cycle_data = cycles failures = self.failures # only show one cycle in stdout output even if we found more else: df = self.df.tail(1) summary = tabulate( df, headers=self.df.columns, tablefmt="fancy_grid", showindex=False ) if cycles[-1][0] == df.index.start: cycle_data = cycles[-1][-1] else: cycle_data = None if self.failures and self.failures[-1][0] == df.index.start: failures = self.failures[-1][-1] else: cycles = [] debug = [] cycle_data = [] summary = "No sleep cycles found in the database." failures = None # let it burn context = { "prereq": prereq, "prereq_date": prereq_date, "cycle_data": cycle_data, "summary": summary, "prereq_debug_data": prereq_debug, "debug_data": debug, "date": datetime.now(), "version": version(), "battery_svg": self.battery_svg, "hwsleep_svg": self.hwsleep_svg, "failures": failures, } if self.fname: with open(self.fname, "w", encoding="utf-8") as f: f.write(template.render(context)) if "SUDO_UID" in os.environ: os.chown( self.fname, int(os.environ["SUDO_UID"]), int(os.environ["SUDO_GID"]) ) return "Report written to {f}".format(f=self.fname) else: return template.render(context) def build_battery_chart(self): """Build a battery chart using matplotlib and seaborn""" import matplotlib.pyplot as plt # pylint: disable=import-outside-toplevel import seaborn as sns # pylint: disable=import-outside-toplevel import io # pylint: disable=import-outside-toplevel if "Battery Ave Rate" not in self.df.columns: return plt.set_loglevel("warning") _fig, ax1 = plt.subplots() ax1.plot( self.df["Battery Ave Rate"], color="green", label="Charge/Discharge Rate" ) ax2 = ax1.twinx() sns.barplot( x=self.df.index, y=self.df["Battery Delta"], color="grey", label="Battery Change", alpha=0.3, ) max_range = int(len(self.df.index) / 10) if max_range: ax1.set_xticks(range(0, len(self.df.index), max_range)) ax1.set_xlabel("Cycle") ax1.set_ylabel("Rate (Watts)") ax2.set_ylabel("Battery Change (%)") lines, labels = ax1.get_legend_handles_labels() lines2, labels2 = ax2.get_legend_handles_labels() ax2.legend( lines + lines2, labels + labels2, loc="lower left", bbox_to_anchor=(0, 1) ) battery_svg = io.BytesIO() plt.savefig(battery_svg, format="svg") battery_svg.seek(0) self.battery_svg = battery_svg.read().decode("utf-8") def build_hw_sleep_chart(self): """Build the hardware sleep chart using matplotlib and seaborn""" import matplotlib.pyplot as plt # pylint: disable=import-outside-toplevel import seaborn as sns # pylint: disable=import-outside-toplevel import io # pylint: disable=import-outside-toplevel plt.set_loglevel("warning") _fig, ax1 = plt.subplots() ax1.plot( self.df["Hardware Sleep"], color="red", label="Hardware Sleep", ) ax2 = ax1.twinx() sns.barplot( x=self.df.index, y=self.df["Duration"] / 60, color="grey", label="Cycle Duration", alpha=0.3, ) max_range = int(len(self.df.index) / 10) if max_range: ax1.set_xticks(range(0, len(self.df.index), max_range)) ax1.set_xlabel("Cycle") ax1.set_ylabel("Percent") ax2.set_yscale("log") ax2.set_ylabel("Duration (minutes)") lines, labels = ax1.get_legend_handles_labels() lines2, labels2 = ax2.get_legend_handles_labels() ax2.legend( lines + lines2, labels + labels2, loc="lower left", bbox_to_anchor=(0, 1) ) hwsleep_svg = io.BytesIO() plt.savefig(hwsleep_svg, format="svg") hwsleep_svg.seek(0) self.hwsleep_svg = hwsleep_svg.read().decode("utf-8") def run(self, inc_prereq=True): """Run the report""" characters = print_temporary_message("Building report, please wait...") if not self.df.empty: # Build charts in the page for html format if len(self.df.index) > 1 and self.format == "html": self.build_battery_chart() self.build_hw_sleep_chart() # Render the template using jinja msg = self.build_template(inc_prereq) clear_temporary_message(characters) for line in msg.split("\n"): color = Colors.OK text = line.strip() if not text: continue for group in ["πŸ—£οΈ", "❌", "🚦", "🦟", "🚫", "β—‹"]: if line.startswith(group): text = line.split(group)[-1] color = get_group_color(group) break print_color(text, color) amd-debug-tools-0.2.15/src/amd_debug/templates/000077500000000000000000000000001515405217400212625ustar00rootroot00000000000000amd-debug-tools-0.2.15/src/amd_debug/templates/html000066400000000000000000000343641515405217400221630ustar00rootroot00000000000000 Linux s2idle Power Report

Linux s2idle Power Report

s2idle report created {{ date }} using amd-s2idle {{version}}

{% if prereq %} {% for obj in prereq %} {% endfor %}
{{obj.symbol}} {{obj.text}}
{% endif %} {% if prereq_debug_data %}

{% for obj in prereq_debug_data %} {% endfor %}
{{obj.data}}
{% endif %}

Summary

{% if battery_svg %}

Battery

{{ battery_svg | safe }} {% endif %} {% if hwsleep_svg %}

Low power state residency

{{ hwsleep_svg | safe }} {% endif %} {% if cycle_data|length > 1 %}

Cycle data

Choose a single cycle to see messages and data for that cycle.

Filter cycle range

and

Filter minimum session length

{% else %} {% endif %} {{ summary }} {% if cycle_data %}

{% for obj in cycle_data %} {% endfor %}
{{obj.data}}
{% endif %} {% if failures %}

{% for obj in failures %} {% endfor %}
{{obj.problem}} {{obj.data}}
{% endif %} {% if debug_data %}

{% for obj in debug_data %} {% endfor %}
{% for index in range(obj.messages | length) %}
{{obj.messages[index]}}
{% endfor %}
{% endif %} amd-debug-tools-0.2.15/src/amd_debug/templates/md000066400000000000000000000014231515405217400216050ustar00rootroot00000000000000# s2idle report created on {{ date }} using amd-s2idle {{ version }} {% if prereq %} ## βš“ Prerequisite checks Measured {{ prereq_date }}. {% filter indent(width=4) %} {% for obj in prereq %}{{ obj.symbol }} {{ obj.text }} {% endfor %} {% endfilter %} {% endif %} {% if prereq_debug_data %}🦟 Prereq Debug Data {% filter indent(width=4) %} {% for obj in prereq_debug_data %} {{obj.data}}{% endfor %} {% endfilter %}{% endif %} {% if cycle_data %}## 🚴 Cycle Data {{ cycle_data }} {% endif %} {% if debug_data %} ## 🦟 Debug Data {% for debug in debug_data %}Cycle {{ debug.cycle_num }} {% filter indent(width=4) %} {% for message in debug.messages %}{{ message }} {% endfor %} {% endfilter %} {% endfor %} {% endif %} ## Summary {{ summary }} ## Failures reported {{ failures }}amd-debug-tools-0.2.15/src/amd_debug/templates/stdout000066400000000000000000000007251515405217400225330ustar00rootroot00000000000000πŸ—£οΈSummary {{ summary }} {% if prereq_debug_data %}🦟Prereq Debug Data {% for obj in prereq_debug_data %} 🦟{{obj.data}} {% endfor %}{% endif %} {% if cycle_data %}{{ cycle_data }}{% endif %} {% if debug_data %}πŸ—£οΈDebug Data{% for obj in debug_data %} πŸ’―Cycle {{ obj.cycle_num }}{% for index in range(obj.messages | length) %} {{obj.priorities[index]}}{{obj.messages[index]}}{% endfor %} {% endfor %}{% endif %} {% if failures %}{{ failures }}{% endif %}amd-debug-tools-0.2.15/src/amd_debug/templates/txt000066400000000000000000000012111515405217400220170ustar00rootroot00000000000000s2idle report created on {{ date }} using amd-s2idle {{ version }} {% if prereq %} βš“ Prerequisite checks Measured {{ prereq_date }}. {% for obj in prereq %} {{ obj.symbol }} {{ obj.text }} {% endfor %}{% endif %} {% if cycle_data %}Cycle Data {{ cycle_data }} {% endif %} {% if prereq_debug_data %}🦟 Debug Data {% for obj in prereq_debug_data %}{{obj.data}} {% endfor %}{% endif %} {% if debug_data %}🦟 Debug Data {% for debug in debug_data %}Cycle {{ debug.cycle_num }} {% for message in debug.messages %}{{ message }} {% endfor %}{% endfor %}{% endif %} Summary {{ summary }} {% if failures %} Failures reported {{ failures }} {% endif %}amd-debug-tools-0.2.15/src/amd_debug/ttm.py000066400000000000000000000110641515405217400204440ustar00rootroot00000000000000# SPDX-License-Identifier: MIT """TTM configuration tool""" import asyncio import os import argparse from amd_debug.common import ( AmdTool, bytes_to_gb, gb_to_pages, get_system_mem, relaunch_sudo, print_color, reboot, version, ) TTM_PARAM_PATH = "/sys/module/ttm/parameters/pages_limit" MODPROBE_CONF_PATH = "/etc/modprobe.d/ttm.conf" # Maximum percentage of total system memory to allow for TTM MAX_MEMORY_PERCENTAGE = 90 def maybe_reboot() -> bool: """Prompt to reboot system""" response = input("Would you like to reboot the system now? (y/n): ").strip().lower() if response in ("y", "yes"): return reboot() return True class AmdTtmTool(AmdTool): """Class for handling TTM page configuration""" def __init__(self, logging): log_prefix = "ttm" if logging else None super().__init__(log_prefix) def get(self) -> bool: """Read current page limit""" try: with open(TTM_PARAM_PATH, "r", encoding="utf-8") as f: pages = int(f.read().strip()) gb_value = bytes_to_gb(pages) print_color( f"Current TTM pages limit: {pages} pages ({gb_value:.2f} GB)", "πŸ’»" ) except FileNotFoundError: print_color(f"Error: Could not find {TTM_PARAM_PATH}", "❌") return False total = get_system_mem() if total > 0: print_color(f"Total system memory: {total:.2f} GB", "πŸ’»") return True def set(self, gb_value) -> bool: """Set a new page limit""" relaunch_sudo() # Check against system memory total = get_system_mem() if total > 0: max_recommended_gb = total * MAX_MEMORY_PERCENTAGE / 100 if gb_value > total: print_color( f"{gb_value:.2f} GB is greater than total system memory ({total:.2f} GB)", "❌", ) return False if gb_value > max_recommended_gb: print_color( f"Warning: The requested value ({gb_value:.2f} GB) exceeds {MAX_MEMORY_PERCENTAGE}% of your system memory ({max_recommended_gb:.2f} GB).", "🚦", ) response = ( input( "This could cause system instability. Continue anyway? (y/n): " ) .strip() .lower() ) if response not in ("y", "yes"): print_color("Operation cancelled.", "🚦") return False pages = gb_to_pages(gb_value) with open(MODPROBE_CONF_PATH, "w", encoding="utf-8") as f: f.write(f"options ttm pages_limit={pages}\n") print_color( f"Successfully set TTM pages limit to {pages} pages ({gb_value:.2f} GB)", "🐧", ) print_color(f"Configuration written to {MODPROBE_CONF_PATH}", "🐧") print_color("NOTE: You need to reboot for changes to take effect.", "β—‹") return maybe_reboot() def clear(self) -> bool: """Clears the page limit""" if not os.path.exists(MODPROBE_CONF_PATH): print_color(f"{MODPROBE_CONF_PATH} doesn't exist", "❌") return False relaunch_sudo() os.remove(MODPROBE_CONF_PATH) print_color(f"Configuration {MODPROBE_CONF_PATH} removed", "🐧") return maybe_reboot() def parse_args(): """Parse command line arguments.""" parser = argparse.ArgumentParser(description="Manage TTM pages limit") parser.add_argument("--set", type=float, help="Set pages limit in GB") parser.add_argument( "--clear", action="store_true", help="Clear a previously set page limit" ) parser.add_argument( "--version", action="store_true", help="Show version information" ) parser.add_argument( "--tool-debug", action="store_true", help="Enable tool debug logging", ) return parser.parse_args() def main() -> None | int: """Main function""" args = parse_args() tool = AmdTtmTool(args.tool_debug) ret = False if args.version: print(version()) return elif args.set is not None: if args.set <= 0: print("Error: GB value must be greater than 0") return 1 ret = tool.set(args.set) elif args.clear: ret = tool.clear() else: ret = tool.get() if ret is False: return 1 return amd-debug-tools-0.2.15/src/amd_debug/validator.py000066400000000000000000001027371515405217400216350ustar00rootroot00000000000000# SPDX-License-Identifier: MIT import glob import math import os import re import random import subprocess import time from datetime import timedelta, datetime from packaging import version from pyudev import Context from amd_debug.sleep_report import SleepReport from amd_debug.database import SleepDatabase from amd_debug.battery import Batteries from amd_debug.kernel import get_kernel_log, get_kernel_command_line, sscanf_bios_args from amd_debug.common import ( print_color, read_file, check_lockdown, run_countdown, BIT, AmdTool, ) from amd_debug.acpi import AcpicaTracer from amd_debug.failures import ( AcpiBiosError, Irq1Workaround, LowHardwareSleepResidency, SpuriousWakeup, RtcAlarmWrong, IommuPageFault, ) class Headers: """Header strings for the debug output""" Irq1Workaround = "Disabling IRQ1 wakeup source to avoid platform firmware bug" WokeFromIrq = "Woke up from IRQ" LastCycleResults = "Results from last s2idle cycle" CycleCount = "Suspend cycle" SuspendDuration = "Suspend timer programmed for" def soc_needs_irq1_wa(family, model, smu_version): """Check if the SoC needs the IRQ1 workaround""" if family == 0x17: if model in [0x68, 0x60]: return True elif family == 0x19: if model == 0x50: return version.parse(smu_version) < version.parse("64.66.0") return False def toggle_pm_debug(enable): """Enable or disable pm_debug_messages""" pm_debug_messages = os.path.join("/", "sys", "power", "pm_debug_messages") with open(pm_debug_messages, "w", encoding="utf-8") as w: w.write("1" if enable else "0") def pm_debugging(func): """Decorator to enable pm_debug_messages""" def runner(*args, **kwargs): toggle_pm_debug(True) ret = func(*args, **kwargs) toggle_pm_debug(False) return ret return runner class SleepValidator(AmdTool): """Class to validate the sleep state""" def __init__(self, tool_debug, bios_debug): log_prefix = "s2idle" if tool_debug else None super().__init__(log_prefix) self.pyudev = Context() self.kernel_log = get_kernel_log() self.db = SleepDatabase() self.batteries = Batteries() self.acpica = AcpicaTracer() self.bios_debug = bios_debug self.cpu_family = "" self.cpu_model = "" self.cpu_model_string = "" self.smu_version = "" self.smu_program = "" self.last_suspend = datetime.now() self.requested_duration = 0 self.userspace_duration = 0 self.kernel_duration = 0 self.hw_sleep_duration = 0 self.failures = [] self.gpes = {} self.display_debug = tool_debug self.lockdown = check_lockdown() self.logind = False self.upep = False self.cycle_count = 0 self.upep = False self.upep_microsoft = False self.wakeup_irqs = [] self.idle_masks = [] self.acpi_errors = [] self.active_gpios = [] self.irq1_workaround = False self.thermal = {} self.wakeup_count = {} self.page_faults = [] self.notify_devices = [] def capture_running_compositors(self): """Capture information about known compositor processes found""" known_compositors = [ "kwin_wayland", "gnome-shell", "cosmic-session", "hyprland", ] # Get a list of all process directories in /proc process_dirs = glob.glob("/proc/[0-9]*") # Extract and print the process names for proc_dir in process_dirs: p = os.path.join(proc_dir, "exe") if not os.path.exists(p): continue exe = os.path.basename(os.readlink(p)).split()[0] if exe in known_compositors: self.db.record_debug(f"{exe} compositor is running") def capture_power_profile(self): """Capture power profile information""" cmd = ["/usr/bin/powerprofilesctl"] if os.path.exists(cmd[0]): try: output = subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode( "utf-8" ) self.db.record_debug("Power Profiles:") lines = output.split("\n") lines = [line for line in lines if line.strip()] for line in lines: prefix = "β”‚ " if line != lines[-1] else "└─" self.db.record_debug(f"{prefix}{line.strip()}") except subprocess.CalledProcessError as e: self.db.record_debug("Failed to run powerprofilesctl: %s", e.output) def capture_battery(self): """Capture battery energy levels""" for name in self.batteries.get_batteries(): unit = self.batteries.get_energy_unit(name) energy = self.batteries.get_energy(name) full = self.batteries.get_energy_full(name) self.db.record_debug(f"{name} energy level is {energy} {unit}") report_unit = "W" if unit == "Β΅Wh" else "A" self.db.record_battery_energy(name, energy, full, report_unit) def check_rtc_cmos(self): """Check if the RTC is configured to use ACPI alarm""" p = os.path.join( "/", "sys", "module", "rtc_cmos", "parameters", "use_acpi_alarm" ) val = read_file(p) if val == "N": self.db.record_cycle_data( "`rtc_cmos` not configured to use ACPI alarm", "🚦" ) self.failures += [RtcAlarmWrong()] def check_gpes(self): """Capture general purpose event count""" base = os.path.join("/", "sys", "firmware", "acpi", "interrupts") for root, _dirs, files in os.walk(base, topdown=False): for fname in files: if not fname.startswith("gpe") or fname == "gpe_all": continue target = os.path.join(root, fname) val = 0 with open(target, "r", encoding="utf-8") as r: val = int(r.read().split()[0]) if fname in self.gpes and self.gpes[fname] != val: self.db.record_debug( f"{fname} increased from {self.gpes[fname]} to {val}", ) self.gpes[fname] = val def capture_wake_sources(self): """Capture possible wakeup sources""" def get_input_sibling_name(pyudev, parent): """Get the name of the input sibling""" for inp in pyudev.list_devices(subsystem="input", parent=parent): if not "NAME" in inp.properties: continue return inp.properties["NAME"] return "" devices = [] for wake_dev in self.pyudev.list_devices(subsystem="wakeup"): p = os.path.join(wake_dev.sys_path, "device", "power", "wakeup") if not os.path.exists(p): continue wake_en = read_file(p) name = "" sys_name = wake_dev.sys_path # determine the type of device it hangs off of acpi = wake_dev.find_parent(subsystem="acpi") serio = wake_dev.find_parent(subsystem="serio") rtc = wake_dev.find_parent(subsystem="rtc") pci = wake_dev.find_parent(subsystem="pci") mhi = wake_dev.find_parent(subsystem="mhi") pnp = wake_dev.find_parent(subsystem="pnp") hid = wake_dev.find_parent(subsystem="hid") thunderbolt_device = wake_dev.find_parent( subsystem="thunderbolt", device_type="thunderbolt_device" ) thunderbolt_domain = wake_dev.find_parent( subsystem="thunderbolt", device_type="thunderbolt_domain" ) i2c = wake_dev.find_parent(subsystem="i2c") if i2c is not None: sys_name = i2c.sys_name name = get_input_sibling_name(self.pyudev, i2c) elif thunderbolt_device is not None: if "USB4_TYPE" in thunderbolt_device.properties: name = ( f'USB4 {thunderbolt_device.properties["USB4_TYPE"]} controller' ) sys_name = thunderbolt_device.sys_name elif thunderbolt_domain is not None: name = "Thunderbolt domain" sys_name = thunderbolt_domain.sys_name elif serio is not None: sys_name = serio.sys_name name = get_input_sibling_name(self.pyudev, serio) elif rtc is not None: sys_name = rtc.sys_name for _parent in self.pyudev.list_devices( subsystem="platform", parent=rtc, DRIVER="alarmtimer" ): name = "Real Time Clock alarm timer" break elif mhi is not None: sys_name = mhi.sys_name name = "Mobile Broadband host interface" elif hid is not None: name = hid.properties["HID_NAME"] sys_name = hid.sys_name elif pci is not None: sys_name = pci.sys_name if ( "ID_PCI_SUBCLASS_FROM_DATABASE" in pci.properties and "ID_VENDOR_FROM_DATABASE" in pci.properties ): name = f'{pci.properties["ID_VENDOR_FROM_DATABASE"]} {pci.properties["ID_PCI_SUBCLASS_FROM_DATABASE"]}' else: name = f"PCI {pci.properties['PCI_CLASS']}" elif acpi is not None: sys_name = acpi.sys_name if acpi.driver == "button": for inp in self.pyudev.list_devices(subsystem="input", parent=acpi): if not "NAME" in inp.properties: continue name = f"ACPI {inp.properties['NAME']}" break elif acpi.driver in ["battery", "ac"]: for ps in self.pyudev.list_devices( subsystem="power_supply", parent=acpi ): if not "POWER_SUPPLY_NAME" in ps.properties: continue name = f"ACPI {ps.properties['POWER_SUPPLY_TYPE']}" elif pnp is not None: name = "Plug-n-play" if pnp.driver == "rtc_cmos": name = f"{name} Real Time Clock" sys_name = pnp.sys_name name = name.replace('"', "") devices.append(f"{name}|{sys_name}|{wake_en}") devices.sort() debug_str = "Wakeup Source|Linux Device|Status\n" for dev in devices: debug_str += f"{dev}\n" self.db.record_debug(debug_str) def capture_lid(self) -> None: """Capture lid state""" p = os.path.join("/", "proc", "acpi", "button", "lid") for root, _dirs, files in os.walk(p): for fname in files: p = os.path.join(root, fname) state = read_file(p).split(":")[1].strip() self.db.record_debug(f"ACPI Lid ({p}): {state}") def capture_wakeup_irq_data(self) -> bool: """Capture the wakeup IRQ to the log""" p = os.path.join("/", "sys", "power", "pm_wakeup_irq") try: n = read_file(p) p = os.path.join("/", "sys", "kernel", "irq", n) chip_name = read_file(os.path.join(p, "chip_name")) name = read_file(os.path.join(p, "name")) hw = read_file(os.path.join(p, "hwirq")) actions = read_file(os.path.join(p, "actions")) message = f"{Headers.WokeFromIrq} {n} ({chip_name} {hw}-{name} {actions})" self.db.record_debug(message) irq = int(n) if irq and irq not in self.wakeup_irqs: self.wakeup_irqs += [irq] except OSError: pass return True def capture_amdgpu_ips_status(self): """Capture the AMDGPU IPS status""" for device in self.pyudev.list_devices(subsystem="pci", PCI_CLASS="38000"): pci_id = device.properties.get("PCI_ID") if not pci_id.startswith("1002"): continue slot = device.properties.get("PCI_SLOT_NAME") p = os.path.join( "/", "sys", "kernel", "debug", "dri", slot, "amdgpu_dm_ips_status" ) if not os.path.exists(p): continue self.db.record_debug("IPS status") try: lines = read_file(p).split("\n") for line in lines: prefix = "β”‚ " if line != lines[-1] else "└─" self.db.record_debug(f"{prefix}{line}") except PermissionError: if self.lockdown: self.db.record_debug( "Unable to gather IPS state data due to kernel lockdown." ) else: self.db.record_debug("Failed to read IPS state data") def capture_thermal(self): """Capture thermal zone information""" devs = [] for dev in self.pyudev.list_devices(subsystem="acpi", DRIVER="thermal"): devs.append(dev) if not devs: return self.db.record_debug("Thermal zones") for dev in devs: prefix = "β”œβ”€ " if dev != devs[-1] else "└─" detail_prefix = "β”‚ \t" if dev != devs[-1] else " \t" name = os.path.basename(dev.device_path) p = os.path.join(dev.sys_path, "thermal_zone") temp = int(read_file(os.path.join(p, "temp"))) / 1000 self.db.record_debug(f"{prefix}{name}") if name not in self.thermal: self.db.record_debug(f"{detail_prefix} temp: {temp}Β°C") else: self.db.record_debug( f"{detail_prefix} {self.thermal[name]}Β°C -> {temp}Β°C" ) # handle all trip points trip_count = 0 for f in os.listdir(p): if "trip_point" not in f: continue if "temp" not in f: continue trip_count = trip_count + 1 for i in range(0, trip_count): f = os.path.join(p, f"trip_point_{i}_type") trip_type = read_file(f) f = os.path.join(p, f"trip_point_{i}_temp") trip = int(read_file(f)) / 1000 if name not in self.thermal: self.db.record_debug(f"{detail_prefix} {trip_type} trip: {trip}Β°C") if temp > trip: self.db.record_prereq( f"Thermal zone {name} past trip point {trip_type}: {trip}Β°C", "🌑️", ) return False self.thermal[name] = temp def capture_input_wakeup_count(self): """Capture wakeup count for input related devices""" def get_wakeup_count(device): """Get the wakeup count for a device""" p = os.path.join(device.sys_path, "power", "wakeup") if not os.path.exists(p): return None p = os.path.join(device.sys_path, "power", "wakeup_count") if not os.path.exists(p): return None return read_file(p) wakeup_count = {} for device in self.pyudev.list_devices(subsystem="input"): count = get_wakeup_count(device) if count is not None: wakeup_count[device.sys_path] = count continue # iterate parents until finding one with a wakeup count # or no more parents parent = device.parent while parent is not None: count = get_wakeup_count(parent) if count is not None: wakeup_count[parent.sys_path] = count break parent = parent.parent # diff the count for device, count in wakeup_count.items(): if device not in self.wakeup_count: continue if self.wakeup_count[device] == count: continue self.db.record_debug( f"Woke up from input source {device} ({self.wakeup_count[device]}->{count})", ) self.wakeup_count = wakeup_count def capture_hw_sleep(self) -> bool: """Check for hardware sleep state""" # try from kernel 6.4's suspend stats interface first because it works # even with kernel lockdown p = os.path.join("/", "sys", "power", "suspend_stats", "last_hw_sleep") if os.path.exists(p): self.hw_sleep_duration = int(read_file(p)) / 10**6 if not os.path.exists(p) and not self.hw_sleep_duration: p = os.path.join("/", "sys", "kernel", "debug", "amd_pmc", "smu_fw_info") try: val = read_file(p) for line in val.split("\n"): if "Last S0i3 Status" in line: continue if "Time (in us) in S0i3" in line: self.hw_sleep_duration = int(line.split(":")[1]) / 10**6 except PermissionError: if self.lockdown: self.db.record_cycle_data( "Unable to gather hardware sleep data with lockdown engaged", "🚦", ) else: self.db.record_cycle_data( "Failed to read hardware sleep data", "🚦" ) return False except FileNotFoundError: self.db.record_debug(f"HW sleep statistics file {p} is missing") if not self.hw_sleep_duration: self.db.record_cycle_data("Did not reach hardware sleep state", "❌") return self.hw_sleep_duration > 0 def capture_command_line(self): """Capture the kernel command line to debug""" self.db.record_debug(f"/proc/cmdline: {get_kernel_command_line()}") def _analyze_kernel_log_line(self, line, priority): bios_args = sscanf_bios_args(line) if bios_args: if isinstance(bios_args, str): line = bios_args priority = 7 else: return elif "Timekeeping suspended for" in line: self.cycle_count += 1 for f in line.split(): try: self.kernel_duration += float(f) except ValueError: pass elif "Successfully transitioned to state" in line: self.upep = True if "Successfully transitioned to state lps0 ms entry" in line: self.upep_microsoft = True elif "_DSM function" in line: self.upep = True if "_DSM function 7" in line: self.upep_microsoft = True elif "Last suspend in deepest state for" in line: for f in line.split(): if not f.endswith("us"): continue try: self.hw_sleep_duration += float(f.strip("us")) / 10**6 except ValueError: pass elif "Triggering wakeup from IRQ" in line: irq = int(line.split()[-1]) if irq and irq not in self.wakeup_irqs: self.wakeup_irqs += [irq] elif "SMU idlemask s0i3" in line: self.idle_masks += [line.split()[-1]] elif "ACPI BIOS Error" in line or "ACPI Error" in line: self.acpi_errors += [line] elif re.search("GPIO.*is active", line): self.active_gpios += re.findall( r"\d+", re.search("GPIO.*is active", line).group() ) elif Headers.Irq1Workaround in line: self.irq1_workaround = True # AMD-Vi: Event logged [IO_PAGE_FAULT device=0000:00:0c.0 domain=0x0000 address=0x7e800000 flags=0x0050] elif "Event logged [IO_PAGE_FAULT" in line: # get the device from string device = re.search(r"device=(.*?) domain", line) if device: device = device.group(1) if device not in self.page_faults: self.page_faults += [device] # evmisc-0132 ev_queue_notify_reques: Dispatching Notify on [UBTC] (Device) Value 0x80 (Status Change) Node 0000000080144eee if "Dispatching Notify on" in line: # add device without the [] to notify_devices if it's not already there device = re.search(r"\[(.*?)\]", line) if device: device = device.group(1) if device not in self.notify_devices: self.notify_devices += [device] priority = 7 self.db.record_debug(line, priority) def analyze_kernel_log(self): """Analyze one of the lines from the kernel log""" self.kernel_log.process_callback(self._analyze_kernel_log_line) if self.cycle_count: self.db.record_cycle_data( f"Hardware sleep cycle count: {self.cycle_count}", "πŸ’€", ) if self.wakeup_irqs: if 1 in self.wakeup_irqs and soc_needs_irq1_wa( self.cpu_family, self.cpu_model, self.smu_version ): if self.irq1_workaround: self.db.record_cycle_data( "Kernel workaround for IRQ1 issue utilized", "β—‹" ) else: self.db.record_cycle_data("IRQ1 found during wakeup", "🚦") self.failures += [Irq1Workaround()] if self.idle_masks: bit_changed = 0 for i, mask_i in enumerate(self.idle_masks): for _j, mask_j in enumerate(self.idle_masks[i:], start=i): if mask_i != mask_j: bit_changed = bit_changed | (int(mask_i, 16) & ~int(mask_j, 16)) if bit_changed: for bit in range(0, 31): if bit_changed & BIT(bit): self.db.record_debug( f"Idle mask bit {bit} (0x{BIT(bit):x}) changed during suspend", ) if self.upep: if self.upep_microsoft: self.db.record_debug("Used Microsoft uPEP GUID in LPS0 _DSM") else: self.db.record_debug("Used AMD uPEP GUID in LPS0 _DSM") if self.acpi_errors: self.db.record_cycle_data("ACPI BIOS errors found", "❌") self.failures += [AcpiBiosError(self.acpi_errors)] if self.page_faults: self.db.record_cycle_data("Page faults found", "❌") self.failures += [IommuPageFault(self.page_faults)] if self.notify_devices: self.db.record_cycle_data( f"Notify devices {self.notify_devices} found during suspend", "πŸ’€" ) def analyze_duration(self, t0, t1, requested, kernel, hw): """Analyze the duration of the last cycle""" userspace_duration = t1 - t0 min_suspend_duration = timedelta(seconds=requested * 0.9) expected_wake_time = t0 + min_suspend_duration if t1 > expected_wake_time: print_color( f"Userspace suspended for {userspace_duration}", "βœ…", ) else: print_color( f"Userspace suspended for {userspace_duration} (< minimum expected {min_suspend_duration})", "❌", ) self.failures += [SpuriousWakeup(requested, userspace_duration)] percent = float(kernel) / userspace_duration.total_seconds() print_color( f"Kernel suspended for total of {timedelta(seconds=kernel)} ({percent:.2%})", "βœ…", ) percent = float(hw / userspace_duration.total_seconds()) if userspace_duration.total_seconds() >= 60: if percent > 0.9: symbol = "βœ…" else: symbol = "❌" self.failures += [ LowHardwareSleepResidency(userspace_duration, percent) ] else: symbol = "βœ…" print_color( "In a hardware sleep state for {time} {percent_msg}".format( time=timedelta(seconds=hw), percent_msg="" if not percent else "({:.2%})".format(percent), ), symbol, ) def post(self): """Post-process the suspend test results""" self.cycle_count = 0 self.upep = False self.upep_microsoft = False self.wakeup_irqs = [] self.idle_masks = [] self.acpi_errors = [] self.active_gpios = [] self.notify_devices = [] self.page_faults = [] self.irq1_workaround = False checks = [ self.capture_wakeup_irq_data, self.analyze_kernel_log, self.check_gpes, self.capture_lid, self.check_rtc_cmos, self.capture_hw_sleep, self.capture_battery, self.capture_amdgpu_ips_status, self.capture_thermal, self.capture_input_wakeup_count, self.acpica.restore, ] for check in checks: check() self.db.record_cycle( self.requested_duration, ",".join(str(gpio) for gpio in self.active_gpios), ",".join(str(irq) for irq in self.wakeup_irqs), int(self.kernel_duration), int(self.hw_sleep_duration), ) def prep(self): """Prepare the system for suspend testing""" self.last_suspend = datetime.now() self.kernel_log.seek_tail(self.last_suspend) self.db.start_cycle(self.last_suspend) self.kernel_duration = 0 self.hw_sleep_duration = 0 self.capture_battery() self.check_gpes() self.capture_lid() self.capture_command_line() self.capture_wake_sources() self.capture_running_compositors() self.capture_power_profile() self.capture_amdgpu_ips_status() self.capture_thermal() self.capture_input_wakeup_count() if self.bios_debug: self.acpica.trace_bios() else: self.acpica.trace_notify() self.db.record_cycle() def program_wakealarm(self): """Program the RTC wakealarm to wake the system after the requested duration""" wakealarm = None for device in self.pyudev.list_devices(subsystem="rtc"): wakealarm = os.path.join(device.sys_path, "wakealarm") if wakealarm: with open(wakealarm, "w", encoding="utf-8") as w: w.write("0") with open(wakealarm, "w", encoding="utf-8") as w: w.write("+%s\n" % self.requested_duration) else: print_color("No RTC device found, please manually wake system", "🚦") def toggle_nvidia(self, value): """Write to the NVIDIA suspend interface""" p = os.path.join("/", "proc", "driver", "nvidia", "suspend") if not os.path.exists(p): return True fd = os.open(p, os.O_WRONLY | os.O_SYNC) try: os.write(fd, value) except OSError as e: self.db.record_cycle_data(f"Failed to set {value} in NVIDIA {e}", "❌") return False finally: os.close(fd) self.db.record_debug(f"Wrote {value} to NVIDIA driver") return True @pm_debugging def suspend_system(self): """Suspend the system using the dbus or sysfs interface""" def get_wakeup_count(): """Get the wakeup count""" p = os.path.join("/", "sys", "power", "wakeup_count") if not os.path.exists(p): return 0 try: with open(p, "r", encoding="utf-8") as r: return int(r.read()) except OSError: return 0 if self.logind: try: import dbus bus = dbus.SystemBus() obj = bus.get_object( "org.freedesktop.login1", "/org/freedesktop/login1" ) intf = dbus.Interface(obj, "org.freedesktop.login1.Manager") propf = dbus.Interface(obj, "org.freedesktop.DBus.Properties") if intf.CanSuspend() != "yes": self.db.record_cycle_data("Unable to suspend", "❌") return False intf.Suspend(True) while propf.Get("org.freedesktop.login1.Manager", "PreparingForSleep"): time.sleep(1) return True except dbus.exceptions.DBusException as e: self.db.record_cycle_data( f"Unable to communicate with logind: {e}", "❌" ) return False except ImportError: self.db.record_cycle_data("Missing dbus", "❌") return False else: if not self.toggle_nvidia(b"suspend"): return False old = get_wakeup_count() p = os.path.join("/", "sys", "power", "state") fd = os.open(p, os.O_WRONLY | os.O_SYNC) try: os.write(fd, b"mem") except OSError as e: new = get_wakeup_count() self.db.record_cycle_data( f"Failed to set suspend state ({old} -> {new}): {e}", "❌" ) return False finally: os.close(fd) if not self.toggle_nvidia(b"resume"): return False return True def unlock_session(self): """Unlock the session using logind""" if self.logind: try: import dbus bus = dbus.SystemBus() obj = bus.get_object( "org.freedesktop.login1", "/org/freedesktop/login1" ) intf = dbus.Interface(obj, "org.freedesktop.login1.Manager") intf.UnlockSessions() except dbus.exceptions.DBusException as e: self.db.record_cycle_data( f"Unable to communicate with logind: {e}", "❌" ) return False return True def run(self, duration, count, wait, rand, logind): """Run the suspend test""" min_duration = 4 if not count: return True if logind: self.logind = True if rand: if duration <= min_duration: print_color(f"Invalid max duration {duration}", "❌") self.db.sync() self.report_cycle() return False print_color( f"Running {count} cycle random test with min duration of {min_duration}s, max duration of {duration}s and a max wait of {wait}s", "πŸ—£οΈ", ) elif count > 1: length = timedelta(seconds=(duration + wait) * count) print_color( f"Running {count} cycles (Test finish expected @ {datetime.now() + length})".format(), "πŸ—£οΈ", ) for i in range(1, count + 1): if rand: self.requested_duration = random.randint(min_duration, duration) requested_wait = random.randint(1, wait) else: self.requested_duration = duration requested_wait = wait run_countdown("Suspending system", math.ceil(requested_wait / 2)) self.prep() self.db.record_debug( f"{Headers.SuspendDuration} {timedelta(seconds=self.requested_duration)}", ) if count > 1: header = f"{Headers.CycleCount} {i}: " else: header = "" print_color( f"{header}Started at {self.last_suspend} (cycle finish expected @ {datetime.now() + timedelta(seconds=self.requested_duration + requested_wait)})", "πŸ—£οΈ", ) self.program_wakealarm() if not self.suspend_system(): self.db.sync() self.report_cycle() return False run_countdown("Collecting data", math.ceil(requested_wait / 2)) self.post() self.db.sync() self.report_cycle() self.unlock_session() return True def systemd_pre_hook(self): """Called before suspend""" self.prep() self.db.sync() toggle_pm_debug(True) def systemd_post_hook(self): """Called after resume""" toggle_pm_debug(False) t0 = self.db.get_last_cycle() self.last_suspend = datetime.strptime(str(t0[0]), "%Y%m%d%H%M%S") self.kernel_log.seek_tail(self.last_suspend) self.db.start_cycle(self.last_suspend) self.post() self.db.sync() def report_cycle(self): """Report the results of the last cycle""" print_color(Headers.LastCycleResults, "πŸ—£οΈ") app = SleepReport( since=self.last_suspend, until=self.last_suspend, fname=None, fmt="stdout", tool_debug=self.display_debug, report_debug=False, ) app.run(inc_prereq=False) return amd-debug-tools-0.2.15/src/amd_debug/wake.py000066400000000000000000000110571515405217400205710ustar00rootroot00000000000000# SPDX-License-Identifier: MIT import os from pyudev import Context from amd_debug.common import read_file class WakeGPIO: """Class for wake GPIOs""" def __init__(self, num): self.num = int(num) self.name = "" def __str__(self): if self.name: return f"{self.num} ({self.name})" return f"{self.num}" class WakeIRQ: """Class for wake IRQs""" def __init__(self, num, context=Context()): self.num = num p = os.path.join("/", "sys", "kernel", "irq", str(num)) try: self.chip_name = read_file(os.path.join(p, "chip_name")) except (PermissionError, FileNotFoundError): self.chip_name = "" try: self.actions = read_file(os.path.join(p, "actions")) except (PermissionError, FileNotFoundError): self.actions = "" self.driver = "" self.name = "" try: wakeup = read_file(os.path.join(p, "wakeup")) except (PermissionError, FileNotFoundError): wakeup = "" # This is an IRQ tied to _AEI if self.chip_name == "amd_gpio": try: hw_gpio = read_file(os.path.join(p, "hwirq")) self.name = f"GPIO {hw_gpio}" except (PermissionError, FileNotFoundError): self.name = "GPIO (unknown)" # legacy IRQs elif "IR-IO-APIC" in self.chip_name: if self.actions == "acpi": self.name = "ACPI SCI" elif self.actions == "i8042": self.name = "PS/2 controller" elif self.actions == "pinctrl_amd": self.name = "GPIO Controller" elif self.actions == "rtc0": self.name = "RTC" elif self.actions == "timer": self.name = "Timer" self.actions = "" elif "PCI-MSI" in self.chip_name: bdf = self.chip_name.split("-")[-1] for dev in context.list_devices(subsystem="pci"): if dev.device_path.endswith(bdf): vendor = dev.properties.get("ID_VENDOR_FROM_DATABASE") desc = dev.properties.get("ID_PCI_CLASS_FROM_DATABASE") if not desc: desc = dev.properties.get("ID_PCI_INTERFACE_FROM_DATABASE") name = dev.properties.get("PCI_SLOT_NAME") self.driver = dev.properties.get("DRIVER") self.name = f"{vendor} {desc} ({name})" break # "might" look like an ACPI device, try to follow it if not self.name and self.actions: p = os.path.join("/", "sys", "bus", "acpi", "devices", self.actions) if os.path.exists(p): for directory in os.listdir(p): if "physical_node" not in directory: continue for root, _dirs, files in os.walk( os.path.join(p, directory), followlinks=True ): if "name" in files: try: self.name = read_file(os.path.join(root, "name")) except (PermissionError, FileNotFoundError): pass t = os.path.join(root, "driver") if os.path.exists(t): try: self.driver = os.path.basename(os.readlink(t)) except (PermissionError, FileNotFoundError, OSError): pass break if self.name: break # If the name isn't descriptive try to guess further if self.driver and self.actions == self.name: if self.driver == "i2c_hid_acpi": self.name = f"{self.name} I2C HID device" # check if it's disabled if not self.name and wakeup == "disabled": self.name = "Disabled interrupt" def __str__(self): actions = f" ({self.actions})" if self.actions else "" return f"{self.name}{actions}" if __name__ == "__main__": from tabulate import tabulate pyudev = Context() p = os.path.join("/sys", "kernel", "irq") irqs = [] for d in os.listdir(p): if os.path.isdir(os.path.join(p, d)): w = WakeIRQ(d, pyudev) irqs.append([int(d), str(WakeIRQ(d, pyudev))]) irqs.sort() print(tabulate(irqs, tablefmt="fancy_grid")) amd-debug-tools-0.2.15/src/launcher.py000077500000000000000000000016631515405217400175410ustar00rootroot00000000000000#!/usr/bin/python3 # SPDX-License-Identifier: MIT """ This module is a launcher for the AMD Debug Tools package. It is meant for launching various tools within the package without installation. """ import sys import os URL = "git://git.kernel.org/pub/scm/linux/kernel/git/superm1/amd-debug-tools.git" try: import amd_debug from amd_debug.common import fatal_error except ModuleNotFoundError: sys.exit( f"\033[91m{sys.argv[0]} can not be run standalone.\n" f"\033[0m\033[94mCheck out the full branch from {URL}\033[0m" ) def main(): """Main function to launch the appropriate tool based on the script name.""" try: return amd_debug.launch_tool(os.path.basename(sys.argv[0])) except ModuleNotFoundError as e: fatal_error( f"Missing dependency: {e}\n" f"Run ./install_deps.py to install dependencies." ) if __name__ == "__main__": sys.exit(main()) amd-debug-tools-0.2.15/src/test_acpi.py000066400000000000000000000060651515405217400177110ustar00rootroot00000000000000#!/usr/bin/python3 # SPDX-License-Identifier: MIT """ This module contains unit tests for the acpi functions in the amd-debug-tools package. """ from unittest.mock import patch, mock_open, call import logging import unittest from amd_debug.acpi import search_acpi_tables, AcpicaTracer, ACPI_METHOD class TestAcpi(unittest.TestCase): """Test acpi functions""" @classmethod def setUpClass(cls): logging.basicConfig(filename="/dev/null", level=logging.DEBUG) def test_search_acpi_tables(self): """Test search_acpi_tables function""" pattern = "test_pattern" bad_pattern = "bad_pattern" mock_listdir = ["ABA", "SSDT1", "DSDT2", "SSDT3"] mock_file_content = b"test_pattern" with patch("os.listdir", return_value=mock_listdir), patch( "builtins.open", mock_open(read_data=mock_file_content) ): result = search_acpi_tables(pattern) self.assertTrue(result) with patch("os.listdir", return_value=mock_listdir), patch( "builtins.open", mock_open(read_data=mock_file_content) ): result = search_acpi_tables(bad_pattern) self.assertFalse(result) with patch("os.listdir", return_value=["OTHER1", "OTHER2"]), patch( "builtins.open", mock_open(read_data=b"no_match") ): result = search_acpi_tables(pattern) self.assertFalse(result) def test_acpica_tracer_missing_bios(self): """Test AcpicaTracer class when ACPI tracing is not supported""" mock_listdir = ["SSDT1", "DSDT2", "SSDT3"] with patch("os.listdir", return_value=mock_listdir), patch( "builtins.open", mock_open(read_data=b"foo") ), patch("os.path.exists", return_value=True): tracer = AcpicaTracer() self.assertTrue(tracer.supported) self.assertFalse(tracer.trace_bios()) def test_acpica_tracer(self): """Test AcpicaTracer class""" mock_listdir = ["SSDT1", "DSDT2", "SSDT3"] mock_file_content = bytes(ACPI_METHOD, "utf-8") with patch("os.listdir", return_value=mock_listdir), patch( "builtins.open", mock_open(read_data=mock_file_content) ), patch("os.path.exists", return_value=True): tracer = AcpicaTracer() self.assertTrue(tracer.supported) self.assertTrue(tracer.trace_notify()) self.assertTrue(tracer.trace_bios()) self.assertTrue(tracer.disable()) self.assertTrue(tracer.restore()) def test_acpica_trace_no_acpi_debug(self): """Test AcpicaTracer class when ACPI tracing is not supported""" with patch("os.path.exists", return_value=False), patch( "amd_debug.common.open", mock_open(read_data="foo") ): tracer = AcpicaTracer() self.assertFalse(tracer.supported) self.assertFalse(tracer.trace_notify()) self.assertFalse(tracer.trace_bios()) self.assertFalse(tracer.disable()) self.assertFalse(tracer.restore()) amd-debug-tools-0.2.15/src/test_batteries.py000066400000000000000000000064401515405217400207540ustar00rootroot00000000000000#!/usr/bin/python3 # SPDX-License-Identifier: MIT """ This module contains unit tests for the battery functions in the amd-debug-tools package. """ import unittest import logging from unittest.mock import MagicMock, patch from amd_debug.battery import Batteries class TestBatteries(unittest.TestCase): """Test battery functions""" @classmethod def setUpClass(cls): logging.basicConfig(filename="/dev/null", level=logging.DEBUG) @patch("amd_debug.battery.Context") def setUp(self, mock_context): """Set up a mock context for testing""" self.mock_context = mock_context.return_value self.batteries = Batteries() def test_get_batteries(self): """Test getting battery names""" mock_device = MagicMock() mock_device.device_path = "/devices/LNXSYSTM:00/device:00/PNP0C0A:00" mock_device.properties = {"POWER_SUPPLY_NAME": "BAT0"} self.mock_context.list_devices.return_value = [mock_device] result = self.batteries.get_batteries() self.assertEqual(result, ["BAT0"]) def test_get_energy_unit(self): """Test getting energy unit for a battery""" mock_device = MagicMock() mock_device.device_path = "/devices/LNXSYSTM:00/device:00/PNP0C0A:00" mock_device.properties = { "POWER_SUPPLY_NAME": "BAT0", "POWER_SUPPLY_ENERGY_NOW": "50000", } self.mock_context.list_devices.return_value = [mock_device] result = self.batteries.get_energy_unit("BAT0") self.assertEqual(result, "Β΅Wh") def test_get_energy(self): """Test getting current energy for a battery""" mock_device = MagicMock() mock_device.device_path = "/devices/LNXSYSTM:00/device:00/PNP0C0A:00" mock_device.properties = { "POWER_SUPPLY_NAME": "BAT0", "POWER_SUPPLY_ENERGY_NOW": "50000", } self.mock_context.list_devices.return_value = [mock_device] result = self.batteries.get_energy("BAT0") self.assertEqual(result, "50000") def test_get_energy_full(self): """Test getting full energy for a battery""" mock_device = MagicMock() mock_device.device_path = "/devices/LNXSYSTM:00/device:00/PNP0C0A:00" mock_device.properties = { "POWER_SUPPLY_NAME": "BAT0", "POWER_SUPPLY_ENERGY_FULL": "60000", } self.mock_context.list_devices.return_value = [mock_device] result = self.batteries.get_energy_full("BAT0") self.assertEqual(result, "60000") def test_get_description_string(self): """Test getting description string for a battery""" mock_device = MagicMock() mock_device.device_path = "/devices/LNXSYSTM:00/device:00/PNP0C0A:00" mock_device.properties = { "POWER_SUPPLY_NAME": "BAT0", "POWER_SUPPLY_MANUFACTURER": "ACME", "POWER_SUPPLY_MODEL_NAME": "SuperBattery", "POWER_SUPPLY_ENERGY_FULL": "60000", "POWER_SUPPLY_ENERGY_FULL_DESIGN": "80000", } self.mock_context.list_devices.return_value = [mock_device] result = self.batteries.get_description_string("BAT0") self.assertEqual( result, "Battery BAT0 (ACME SuperBattery) is operating at 75.00% of design", ) amd-debug-tools-0.2.15/src/test_bios.py000066400000000000000000000220741515405217400177270ustar00rootroot00000000000000#!/usr/bin/python3 # SPDX-License-Identifier: MIT """ This module contains unit tests for the bios tool in the amd-debug-tools package. """ import argparse import logging import unittest from unittest.mock import patch, MagicMock from amd_debug.bios import AmdBios, parse_args, main class TestAmdBios(unittest.TestCase): """Test AmdBios class""" @classmethod def setUpClass(cls): logging.basicConfig(filename="/dev/null", level=logging.DEBUG) @patch("amd_debug.bios.get_kernel_log") def test_init(self, mock_get_kernel_log): """Test initialization of AmdBios class""" mock_kernel_log = MagicMock() mock_get_kernel_log.return_value = mock_kernel_log app = AmdBios("test_input", debug=True) self.assertEqual(app.kernel_log, mock_kernel_log) @patch("amd_debug.bios.relaunch_sudo") @patch("amd_debug.bios.minimum_kernel") @patch("amd_debug.bios.AcpicaTracer") @patch("amd_debug.bios.print_color") @patch("subprocess.run") def test_set_tracing_enable( self, _mock_run, _mock_print, mock_acpica_tracer, mock_minimum_kernel, mock_relaunch_sudo, ): """Test enabling tracing""" mock_minimum_kernel.return_value = True mock_tracer = MagicMock() mock_acpica_tracer.return_value = mock_tracer mock_tracer.trace_bios.return_value = True app = AmdBios(None, debug=False) result = app.set_tracing(enable=True) mock_relaunch_sudo.assert_called_once() mock_tracer.trace_bios.assert_called_once() self.assertTrue(result) @patch("amd_debug.bios.relaunch_sudo") @patch("amd_debug.bios.minimum_kernel") @patch("amd_debug.bios.AcpicaTracer") @patch("amd_debug.bios.print_color") @patch("subprocess.run") def test_set_tracing_disable( self, _mock_run, _mock_print, mock_acpica_tracer, mock_minimum_kernel, mock_relaunch_sudo, ): """Test disabling tracing""" mock_minimum_kernel.return_value = True mock_tracer = MagicMock() mock_acpica_tracer.return_value = mock_tracer mock_tracer.disable.return_value = True app = AmdBios(None, debug=False) result = app.set_tracing(enable=False) mock_relaunch_sudo.assert_called_once() mock_tracer.disable.assert_called_once() self.assertTrue(result) @patch("amd_debug.bios.sscanf_bios_args") @patch("amd_debug.bios.print_color") @patch("subprocess.run") def test_analyze_kernel_log_line( self, _mock_run, mock_print_color, mock_sscanf_bios_args ): """Test analyzing kernel log line""" mock_sscanf_bios_args.return_value = "BIOS argument found" app = AmdBios(None, debug=False) app._analyze_kernel_log_line( # pylint: disable=protected-access "test log line", priority="INFO" ) mock_sscanf_bios_args.assert_called_once_with("test log line") mock_print_color.assert_called_once_with("BIOS argument found", "πŸ–΄") @patch("amd_debug.bios.sscanf_bios_args") @patch("amd_debug.bios.print_color") @patch("subprocess.run") def test_analyze_kernel_log_line_no_bios_args( self, _mock_run, mock_print_color, mock_sscanf_bios_args ): """Test analyzing kernel log line with no BIOS arguments""" mock_sscanf_bios_args.return_value = None app = AmdBios(None, debug=False) app._analyze_kernel_log_line( # pylint: disable=protected-access "[123.456] test log line", priority="INFO" ) mock_sscanf_bios_args.assert_called_once_with("[123.456] test log line") mock_print_color.assert_called_once_with("test log line", "INFO") @patch("amd_debug.bios.get_kernel_log") def test_run(self, _mock_run): """Test run method""" mock_kernel_log = MagicMock() app = AmdBios(None, debug=False) app.kernel_log = mock_kernel_log app.run() mock_kernel_log.process_callback.assert_called_once_with( app._analyze_kernel_log_line # pylint: disable=protected-access ) @patch("sys.argv", ["bios.py", "parse", "--input", "test.log", "--tool-debug"]) def test_parse_args_parse_command(self): """Test parse_args with parse command""" args = parse_args() self.assertEqual(args.command, "parse") self.assertEqual(args.input, "test.log") self.assertTrue(args.tool_debug) @patch("sys.argv", ["bios.py", "trace", "--enable", "--tool-debug"]) def test_parse_args_trace_enable(self): """Test parse_args with trace enable command""" args = parse_args() self.assertEqual(args.command, "trace") self.assertTrue(args.enable) self.assertFalse(args.disable) self.assertTrue(args.tool_debug) @patch("sys.argv", ["bios.py", "trace", "--disable"]) def test_parse_args_trace_disable(self): """Test parse_args with trace disable command""" args = parse_args() self.assertEqual(args.command, "trace") self.assertFalse(args.enable) self.assertTrue(args.disable) @patch("sys.argv", ["bios.py", "--version"]) def test_parse_args_version_command(self): """Test parse_args with version command""" args = parse_args() self.assertTrue(args.version) @patch("sys.argv", ["bios.py"]) @patch("argparse.ArgumentParser.print_help") @patch("sys.exit") def test_parse_args_no_arguments(self, mock_exit, mock_print_help): """Test parse_args with no arguments""" parse_args() mock_print_help.assert_called_once() mock_exit.assert_called_once_with(1) @patch("sys.argv", ["bios.py", "trace", "--enable", "--disable"]) @patch("sys.exit") def test_parse_args_conflicting_trace_arguments(self, mock_exit): """Test parse_args with conflicting trace arguments""" parse_args() mock_exit.assert_called_once_with("can't set both enable and disable") @patch("sys.argv", ["bios.py", "trace"]) @patch("sys.exit") def test_parse_args_missing_trace_arguments(self, mock_exit): """Test parse_args with missing trace arguments""" parse_args() mock_exit.assert_called_once_with("must set either enable or disable") @patch("amd_debug.bios.AmdBios") @patch("amd_debug.bios.parse_args") @patch("amd_debug.bios.version") @patch("amd_debug.bios.show_log_info") def test_main_trace_command( self, mock_show_log_info, _mock_version, mock_parse_args, mock_amd_bios ): """Test main function with trace command""" mock_app = MagicMock() mock_amd_bios.return_value = mock_app mock_parse_args.return_value = argparse.Namespace( command="trace", enable=True, disable=False, tool_debug=True ) mock_app.set_tracing.return_value = True result = main() mock_parse_args.assert_called_once() mock_amd_bios.assert_called_once_with(None, True) mock_app.set_tracing.assert_called_once_with(True) mock_show_log_info.assert_called_once() self.assertIsNone(result) @patch("amd_debug.bios.AmdBios") @patch("amd_debug.bios.parse_args") @patch("amd_debug.bios.version") @patch("amd_debug.bios.show_log_info") def test_main_parse_command( self, mock_show_log_info, _mock_version, mock_parse_args, mock_amd_bios ): """Test main function with parse command""" mock_app = MagicMock() mock_amd_bios.return_value = mock_app mock_parse_args.return_value = argparse.Namespace( command="parse", input="test.log", tool_debug=True ) mock_app.run.return_value = True result = main() mock_parse_args.assert_called_once() mock_amd_bios.assert_called_once_with("test.log", True) mock_app.run.assert_called_once() mock_show_log_info.assert_called_once() self.assertIsNone(result) @patch("amd_debug.bios.parse_args") @patch("amd_debug.bios.version") @patch("amd_debug.bios.show_log_info") @patch("amd_debug.bios.print") def test_main_version_command( self, _mock_print, mock_show_log_info, mock_version, mock_parse_args ): """Test main function with version command""" mock_parse_args.return_value = argparse.Namespace(version=True, command=None) mock_version.return_value = "1.0.0" result = main() mock_parse_args.assert_called_once() mock_version.assert_called_once() mock_show_log_info.assert_called_once() self.assertEqual(result, 1) @patch("amd_debug.bios.parse_args") @patch("amd_debug.bios.show_log_info") def test_main_invalid_command(self, mock_show_log_info, mock_parse_args): """Test main function with an invalid command""" mock_parse_args.return_value = argparse.Namespace( version=False, command="invalid" ) result = main() mock_parse_args.assert_called_once() mock_show_log_info.assert_called_once() self.assertEqual(result, 1) amd-debug-tools-0.2.15/src/test_common.py000077500000000000000000000504251515405217400202670ustar00rootroot00000000000000#!/usr/bin/python3 # SPDX-License-Identifier: MIT """ This module contains unit tests for the common functions in the amd-debug-tools package. """ from unittest.mock import patch, mock_open, call, Mock import asyncio import builtins import logging import tempfile import unittest import os from platform import uname_result import sys from amd_debug.common import ( apply_prefix_wrapper, bytes_to_gb, Colors, convert_string_to_bool, colorize_choices, check_lockdown, compare_file, find_ip_version, fatal_error, gb_to_pages, get_distro, get_log_priority, get_pretty_distro, get_system_mem, is_root, minimum_kernel, print_color, reboot, run_countdown, systemd_in_use, running_ssh, ) color_dict = { "🚦": Colors.WARNING, "🦟": Colors.DEBUG, "❌": Colors.FAIL, "πŸ‘€": Colors.FAIL, "βœ…": Colors.OK, "πŸ”‹": Colors.OK, "🐧": Colors.OK, "πŸ’»": Colors.OK, "β—‹": Colors.OK, "πŸ’€": Colors.OK, "πŸ’―": Colors.UNDERLINE, "🚫": Colors.UNDERLINE, "πŸ—£οΈ": Colors.HEADER, } class TestCommon(unittest.TestCase): """Test common functions""" @classmethod def setUpClass(cls): logging.basicConfig(filename="/dev/null", level=logging.DEBUG) def test_read_compare_file(self): """Test read_file and compare_file strip files correctly""" f = tempfile.NamedTemporaryFile() f.write("foo bar baz\n ".encode("utf-8")) f.seek(0) self.assertTrue(compare_file(f.name, "foo bar baz")) def test_countdown(self): """Test countdown function""" result = run_countdown("Full foo", 1) self.assertTrue(result) result = run_countdown("Half foo", 0.5) self.assertTrue(result) result = run_countdown("No foo", 0) self.assertTrue(result) result = run_countdown("Negative foo", -1) self.assertFalse(result) @patch("os.path.exists", return_value=True) @patch("builtins.open", new_callable=mock_open, read_data="ID=foo\nVERSION_ID=bar") def test_get_distro_known(self, mock_exists, _mock_open): """Test get_distro function""" distro = get_distro() mock_exists.assert_has_calls( [ call("/etc/os-release", "r", encoding="utf-8"), call().__enter__(), call().__iter__(), call().__exit__(None, None, None), ] ) self.assertEqual(distro, "foo") @patch("os.path.exists", return_value=False) def test_get_distro_unknown(self, mock_exists): """Test get_distro function""" distro = get_distro() mock_exists.assert_has_calls( [ call("/etc/os-release"), call("/etc/arch-release"), call("/etc/fedora-release"), call("/etc/debian_version"), ] ) self.assertEqual(distro, "unknown") @patch("os.path.exists", return_value=True) @patch("builtins.open", new_callable=mock_open, read_data="PRETTY_NAME=Foo") def test_get_pretty_distro_known(self, mock_exists, _mock_open): """Test get_distro function""" distro = get_pretty_distro() self.assertEqual(distro, "Foo") mock_exists.assert_has_calls( [ call("/etc/os-release", "r", encoding="utf-8"), call().__enter__(), call().__iter__(), call().__exit__(None, None, None), ] ) @patch("os.path.exists", return_value=False) def test_get_pretty_distro_unknown(self, mock_exists): """Test get_distro function""" distro = get_pretty_distro() self.assertEqual(distro, "Unknown") mock_exists.assert_has_calls( [ call("/etc/os-release"), ] ) @patch("os.path.exists", return_value=True) @patch( "builtins.open", new_callable=mock_open, read_data="[none] integrity confidentiality", ) def test_lockdown_pass(self, mock_exists, _mock_file): """Test lockdown function""" lockdown = check_lockdown() mock_exists.assert_called_once_with( "/sys/kernel/security/lockdown", "r", encoding="utf-8" ) self.assertFalse(lockdown) @patch("os.path.exists", return_value=True) @patch( "builtins.open", new_callable=mock_open, read_data="none [integrity] confidentiality", ) def test_lockdown_fail_integrity(self, mock_exists, _mock_file): """Test lockdown function""" lockdown = check_lockdown() mock_exists.assert_called_once_with( "/sys/kernel/security/lockdown", "r", encoding="utf-8" ) self.assertTrue(lockdown) @patch("os.path.exists", return_value=True) @patch( "builtins.open", new_callable=mock_open, read_data="none integrity [confidentiality]", ) def test_lockdown_fail_confidentiality(self, mock_exists, _mock_file): """Test lockdown function""" lockdown = check_lockdown() mock_exists.assert_called_once_with( "/sys/kernel/security/lockdown", "r", encoding="utf-8" ) self.assertTrue(lockdown) @patch("os.path.exists", return_value=False) def test_lockdown_missing(self, mock_exists): """Test lockdown function""" lockdown = check_lockdown() mock_exists.assert_called_once_with("/sys/kernel/security/lockdown") self.assertFalse(lockdown) @patch("builtins.print") def test_print_color(self, mocked_print): """Test print_color function for all expected levels""" message = "foo" # test all color groups for group, color in color_dict.items(): prefix = f"{group} " print_color(message, group) mocked_print.assert_called_once_with( f"{prefix}{color}{message}{Colors.ENDC}" ) mocked_print.reset_mock() # call without a group print_color(message, Colors.WARNING) mocked_print.assert_called_once_with(f"{Colors.WARNING}{message}{Colors.ENDC}") mocked_print.reset_mock() # test dumb terminal os.environ["TERM"] = "dumb" print_color(message, Colors.WARNING) mocked_print.assert_called_once_with(f"{message}") @patch("builtins.print") def test_fatal_error(self, mocked_print): """Test fatal_error function""" with patch("sys.exit") as mock_exit: fatal_error("foo") mocked_print.assert_called_once_with(f"πŸ‘€ {Colors.FAIL}foo{Colors.ENDC}") mock_exit.assert_called_once_with(1) @patch("os.geteuid", return_value=0) def test_is_root_true(self, mock_geteuid): """Test is_root function when user is root""" self.assertTrue(is_root()) mock_geteuid.assert_called_once() self.assertEqual(mock_geteuid.call_count, 1) @patch("os.geteuid", return_value=1000) def test_is_root_false(self, mock_geteuid): """Test is_root function when user is not root""" self.assertFalse(is_root()) mock_geteuid.assert_called_once() self.assertEqual(mock_geteuid.call_count, 1) def test_get_log_priority(self): """Test get_log_priority works for expected values""" ret = get_log_priority(None) self.assertEqual(ret, "β—‹") ret = get_log_priority("foo") self.assertEqual(ret, "foo") ret = get_log_priority("3") self.assertEqual(ret, "❌") ret = get_log_priority(4) self.assertEqual(ret, "🚦") ret = get_log_priority(7) self.assertEqual(ret, "🦟") def test_minimum_kernel(self): """Test minimum_kernel function""" with patch("platform.uname") as mock_uname: mock_uname.return_value = uname_result( system="Linux", node="foo", release="6.12.0-rc5", version="baz", machine="x86_64", ) self.assertTrue(minimum_kernel("6", "12")) self.assertFalse(minimum_kernel("6", "13")) self.assertTrue(minimum_kernel(5, 1)) self.assertFalse(minimum_kernel(7, 1)) with self.assertRaises(ValueError): minimum_kernel("foo", "bar") with self.assertRaises(TypeError): minimum_kernel(None, None) def test_systemd_in_use(self): """Test systemd_in_use function""" with patch( "builtins.open", new_callable=mock_open, read_data="systemd" ) as mock_file: self.assertTrue(systemd_in_use()) mock_file.assert_called_once_with("/proc/1/comm", "r", encoding="utf-8") with patch( "builtins.open", new_callable=mock_open, read_data="upstart" ) as mock_file: self.assertFalse(systemd_in_use()) mock_file.assert_called_once_with("/proc/1/comm", "r", encoding="utf-8") def test_running_in_ssh(self): """Test running_in_ssh function""" with patch("os.environ", {"SSH_TTY": "/dev/pts/0"}): self.assertTrue(running_ssh()) with patch("os.environ", {}): self.assertFalse(running_ssh()) def test_apply_prefix_wrapper(self): """Test apply_prefix_wrapper function""" header = "Header:" message = "Line 1\nLine 2\nLine 3" expected_output = "Header:\n" "β”‚ Line 1\n" "β”‚ Line 2\n" "└─ Line 3\n" self.assertEqual(apply_prefix_wrapper(header, message), expected_output) # Test with a single line message message = "Single Line" expected_output = "Header:\n└─ Single Line\n" self.assertEqual(apply_prefix_wrapper(header, message), expected_output) # Test with an empty message message = "" expected_output = "Header:\n" self.assertEqual(apply_prefix_wrapper(header, message), expected_output) # Test with leading/trailing whitespace in the message message = " Line 1\nLine 2 \n Line 3 " expected_output = "Header:\n" "β”‚ Line 1\n" "β”‚ Line 2\n" "└─ Line 3\n" self.assertEqual(apply_prefix_wrapper(header, message), expected_output) def test_colorize_choices_with_default(self): """Test colorize_choices function with a default value""" choices = ["option1", "option2", "option3"] default = "option2" expected_output = f"{Colors.OK}{default}{Colors.ENDC}, option1, option3" self.assertEqual(colorize_choices(choices, default), expected_output) def test_colorize_choices_without_default(self): """Test colorize_choices function when default is not in choices""" choices = ["option1", "option2", "option3"] default = "option4" with self.assertRaises(ValueError) as context: colorize_choices(choices, default) self.assertEqual( str(context.exception), "Default choice 'option4' not in choices" ) def test_colorize_choices_empty_list(self): """Test colorize_choices function with an empty list""" choices = [] default = "option1" with self.assertRaises(ValueError) as context: colorize_choices(choices, default) self.assertEqual( str(context.exception), "Default choice 'option1' not in choices" ) def test_colorize_choices_single_choice(self): """Test colorize_choices function with a single choice""" choices = ["option1"] default = "option1" expected_output = f"{Colors.OK}{default}{Colors.ENDC}" self.assertEqual(colorize_choices(choices, default), expected_output) @patch("amd_debug.common.read_file") @patch("os.path.exists") def test_find_ip_version_found(self, mock_exists, mock_read_file): """Test find_ip_version returns True when expected value is found""" base_path = "/foo" kind = "bar" hw_ver = {"baz": 42} # Simulate file exists and value matches def exists_side_effect(path): return True mock_exists.side_effect = exists_side_effect mock_read_file.return_value = "42" result = find_ip_version(base_path, kind, hw_ver) self.assertTrue(result) b = os.path.join(base_path, "ip_discovery", "die", "0", kind, "0") expected_path = os.path.join(b, "baz") mock_exists.assert_any_call(expected_path) mock_read_file.assert_any_call(expected_path) @patch("amd_debug.common.read_file") @patch("os.path.exists") def test_find_ip_version_not_found_due_to_missing_file( self, mock_exists, mock_read_file ): """Test find_ip_version returns False if file does not exist""" base_path = "/foo" kind = "bar" hw_ver = {"baz": 42} # Simulate file does not exist mock_exists.return_value = False result = find_ip_version(base_path, kind, hw_ver) self.assertFalse(result) b = os.path.join(base_path, "ip_discovery", "die", "0", kind, "0") expected_path = os.path.join(b, "baz") mock_exists.assert_any_call(expected_path) mock_read_file.assert_not_called() @patch("amd_debug.common.read_file") @patch("os.path.exists") def test_find_ip_version_not_found_due_to_value_mismatch( self, mock_exists, mock_read_file ): """Test find_ip_version returns False if value does not match""" base_path = "/foo" kind = "bar" hw_ver = {"baz": 42} # Simulate file exists but value does not match mock_exists.return_value = True mock_read_file.return_value = "99" result = find_ip_version(base_path, kind, hw_ver) self.assertFalse(result) b = os.path.join(base_path, "ip_discovery", "die", "0", kind, "0") expected_path = os.path.join(b, "baz") mock_exists.assert_any_call(expected_path) mock_read_file.assert_any_call(expected_path) @patch("amd_debug.common.read_file") @patch("os.path.exists") def test_find_ip_version_multiple_keys(self, mock_exists, mock_read_file): """Test find_ip_version with multiple keys in hw_ver""" base_path = "/foo" kind = "bar" hw_ver = {"baz": 42, "qux": 99} # First key: file exists, value does not match # Second key: file exists, value matches def exists_side_effect(path): return True def read_file_side_effect(path): if path.endswith("baz"): return "0" if path.endswith("qux"): return "99" return "0" mock_exists.side_effect = exists_side_effect mock_read_file.side_effect = read_file_side_effect result = find_ip_version(base_path, kind, hw_ver) self.assertFalse(result) def test_convert_string_to_bool_true_values(self): """Test convert_string_to_bool returns True for truthy string values""" self.assertTrue(convert_string_to_bool("True")) self.assertTrue(convert_string_to_bool("1")) self.assertTrue(convert_string_to_bool("'nonempty'")) self.assertTrue(convert_string_to_bool('"nonempty"')) def test_convert_string_to_bool_false_values(self): """Test convert_string_to_bool returns False for falsy string values""" self.assertFalse(convert_string_to_bool("False")) self.assertFalse(convert_string_to_bool("0")) self.assertFalse(convert_string_to_bool("''")) self.assertFalse(convert_string_to_bool('""')) self.assertFalse(convert_string_to_bool("None")) def test_convert_string_to_bool_invalid_syntax(self): """Test convert_string_to_bool exits on invalid syntax""" with patch("sys.exit") as mock_exit: convert_string_to_bool("not_a_bool") mock_exit.assert_called_once_with("Invalid entry: not_a_bool") def test_convert_string_to_bool_invalid_value(self): """Test convert_string_to_bool exits on invalid value""" with patch("sys.exit") as mock_exit: convert_string_to_bool("[unclosed_list") mock_exit.assert_called_once_with("Invalid entry: [unclosed_list") def test_bytes_to_gb(self): """Test bytes_to_gb conversion""" # 4096 bytes should be 4096*4096/(1024*1024*1024) GB self.assertAlmostEqual(bytes_to_gb(1), 4096 / (1024 * 1024 * 1024)) self.assertAlmostEqual(bytes_to_gb(0), 0) self.assertAlmostEqual(bytes_to_gb(1024), 1024 * 4096 / (1024 * 1024 * 1024)) def test_gb_to_pages(self): """Test gb_to_pages conversion""" # 1 GB should be int(1 * (1024*1024*1024) / 4096) self.assertEqual(gb_to_pages(1), int((1024 * 1024 * 1024) / 4096)) self.assertEqual(gb_to_pages(0), 0) self.assertEqual(gb_to_pages(2), int(2 * (1024 * 1024 * 1024) / 4096)) @patch( "builtins.open", new_callable=mock_open, read_data="MemTotal: 16384516 kB\n", ) @patch("os.path.join", return_value="/proc/meminfo") def test_get_system_mem_valid(self, _mock_join, mock_file): """Test get_system_mem returns correct value""" expected_gb = 16384516 / (1024 * 1024) self.assertAlmostEqual(get_system_mem(), expected_gb) mock_file.assert_called_once_with("/proc/meminfo", "r", encoding="utf-8") def test_reboot_dbus_fast_success(self): """Test reboot returns True when reboot_dbus_fast succeeds""" # Create a mock loop that properly handles coroutines def mock_run_until_complete(coro): # Consume the coroutine to prevent the warning try: # Close the coroutine to prevent the warning coro.close() except (AttributeError, RuntimeError): pass return True mock_loop = Mock() mock_loop.run_until_complete.side_effect = mock_run_until_complete with patch("amd_debug.common.asyncio.get_event_loop", return_value=mock_loop): result = reboot() self.assertTrue(result) mock_loop.run_until_complete.assert_called_once() @patch("asyncio.get_event_loop") def test_reboot_dbus_fast_failure_and_dbus_success(self, mock_get_event_loop): """Test reboot falls back to reboot_dbus when reboot_dbus_fast fails""" # Create a mock loop that properly handles coroutines def mock_run_until_complete(coro): # Consume the coroutine to prevent the warning try: coro.close() except (AttributeError, RuntimeError): pass return False mock_loop = Mock() mock_loop.run_until_complete.side_effect = mock_run_until_complete mock_get_event_loop.return_value = mock_loop # Mock the dbus module to avoid ImportError in CI mock_dbus = Mock() mock_bus = Mock() mock_obj = Mock() mock_intf = Mock() mock_dbus.SystemBus.return_value = mock_bus mock_bus.get_object.return_value = mock_obj mock_obj.get_interface.return_value = mock_intf mock_dbus.Interface = Mock(return_value=mock_intf) with patch.dict("sys.modules", {"dbus": mock_dbus}): result = reboot() self.assertTrue(result) @patch("asyncio.get_event_loop") def test_reboot_dbus_fast_failure_and_dbus_failure(self, mock_get_event_loop): """Test reboot returns False when both reboot_dbus_fast and reboot_dbus fail""" # Create a mock loop that properly handles coroutines def mock_run_until_complete(coro): # Consume the coroutine to prevent the warning try: coro.close() except (AttributeError, RuntimeError): pass return False mock_loop = Mock() mock_loop.run_until_complete.side_effect = mock_run_until_complete mock_get_event_loop.return_value = mock_loop # Mock the import to raise ImportError when dbus is imported original_import = builtins.__import__ def mock_import(name, *args, **kwargs): if name == "dbus": raise ImportError("No module named 'dbus'") return original_import(name, *args, **kwargs) with patch("builtins.__import__", side_effect=mock_import): result = reboot() self.assertFalse(result) amd-debug-tools-0.2.15/src/test_database.py000066400000000000000000000234721515405217400205420ustar00rootroot00000000000000#!/usr/bin/python3 # SPDX-License-Identifier: MIT """ This module contains unit tests for the datbase functions in the amd-debug-tools package. """ import unittest from datetime import datetime from amd_debug.database import SleepDatabase class TestSleepDatabase(unittest.TestCase): """Test SleepDatabase class""" def setUp(self): """Set up mocks and an in-memory database for testing""" # Initialize SleepDatabase after mocks are set up self.db = SleepDatabase(dbf=":memory:") def test_start_cycle(self): """Test starting a new sleep cycle""" timestamp = datetime.now() self.db.start_cycle(timestamp) self.assertEqual(self.db.last_suspend, timestamp) self.assertEqual(self.db.cycle_data_cnt, 0) self.assertEqual(self.db.debug_cnt, 0) def test_record_debug(self): """Test recording a debug message""" timestamp = datetime.now() self.db.start_cycle(timestamp) self.db.record_debug("Test debug message", level=5) cur = self.db.db.cursor() cur.execute( "SELECT message, priority FROM debug WHERE t0=?", (int(timestamp.strftime("%Y%m%d%H%M%S")),), ) result = cur.fetchone() self.assertEqual(result, ("Test debug message", 5)) def test_record_battery_energy(self): """Test recording battery energy""" timestamp = datetime.now() self.db.start_cycle(timestamp) self.db.record_battery_energy("Battery1", 50, 100, "mWh") cur = self.db.db.cursor() cur.execute( "SELECT name, b0, b1, full, unit FROM battery WHERE t0=?", (int(timestamp.strftime("%Y%m%d%H%M%S")),), ) result = cur.fetchone() self.assertEqual(result, ("Battery1", 50, None, 100, "mWh")) def test_record_cycle_data(self): """Test recording cycle data""" timestamp = datetime.now() self.db.start_cycle(timestamp) self.db.record_cycle_data("Test cycle data", "symbol1") cur = self.db.db.cursor() cur.execute( "SELECT message, symbol FROM cycle_data WHERE t0=?", (int(timestamp.strftime("%Y%m%d%H%M%S")),), ) result = cur.fetchone() self.assertEqual(result, ("Test cycle data", "symbol1")) def test_record_cycle(self): """Test recording a sleep cycle""" timestamp = datetime.now() self.db.start_cycle(timestamp) self.db.record_cycle( requested_duration=100, active_gpios="GPIO1", wakeup_irqs="IRQ1", kernel_duration=1.5, hw_sleep_duration=2.5, ) cur = self.db.db.cursor() cur.execute( "SELECT requested, gpio, wake_irq, kernel, hw FROM cycle WHERE t0=?", (int(timestamp.strftime("%Y%m%d%H%M%S")),), ) result = cur.fetchone() self.assertEqual(result, (100, "GPIO1", "IRQ1", 1.5, 2.5)) def test_report_debug(self): """Test reporting debug messages""" timestamp = datetime.now() self.db.start_cycle(timestamp) self.db.record_debug("Test debug message", level=5) result = self.db.report_debug(timestamp) self.assertEqual(result, [("Test debug message", 5)]) def test_report_battery(self): """Test reporting battery data""" timestamp = datetime.now() self.db.start_cycle(timestamp) self.db.record_battery_energy("Battery1", 50, 100, "mWh") result = self.db.report_battery(timestamp) self.assertEqual( result, [ ( int(timestamp.strftime("%Y%m%d%H%M%S")), "Battery1", 50, None, 100, "mWh", ) ], ) def test_record_prereq(self): """Test recording a prereq message""" timestamp = datetime.now() self.db.start_cycle(timestamp) self.db.record_prereq("Test prereq message", "symbol1") cur = self.db.db.cursor() cur.execute( "SELECT message, symbol FROM prereq_data WHERE t0=?", (int(timestamp.strftime("%Y%m%d%H%M%S")),), ) result = cur.fetchone() self.assertEqual(result, ("Test prereq message", "symbol1")) def test_report_prereq(self): """Test reporting prereq messages""" timestamp = datetime.now() self.db.start_cycle(timestamp) self.db.record_prereq("Test prereq message 1", "symbol1") self.db.record_prereq("Test prereq message 2", "symbol2") result = self.db.report_prereq(timestamp) self.assertEqual( result, [ ( int(timestamp.strftime("%Y%m%d%H%M%S")), 0, "Test prereq message 1", "symbol1", ), ( int(timestamp.strftime("%Y%m%d%H%M%S")), 1, "Test prereq message 2", "symbol2", ), ], ) def test_report_prereq_no_data(self): """Test reporting prereq messages when no data exists""" timestamp = datetime.now() self.db.start_cycle(timestamp) result = self.db.report_prereq(timestamp) self.assertEqual(result, []) def test_report_prereq_none_timestamp(self): """Test reporting prereq messages with None timestamp""" result = self.db.report_prereq(None) self.assertEqual(result, []) def test_report_cycle(self): """Test reporting a cycle from the database""" timestamp = datetime.now() self.db.start_cycle(timestamp) self.db.record_cycle( requested_duration=100, active_gpios="GPIO1", wakeup_irqs="IRQ1", kernel_duration=1.5, hw_sleep_duration=2.5, ) result = self.db.report_cycle(timestamp) self.assertEqual( result, [ ( int(timestamp.strftime("%Y%m%d%H%M%S")), int(datetime.now().strftime("%Y%m%d%H%M%S")), 100, "GPIO1", "IRQ1", 1.5, 2.5, ) ], ) def test_report_cycle_no_data(self): """Test reporting a cycle when no data exists""" timestamp = datetime.now() self.db.start_cycle(timestamp) result = self.db.report_cycle(timestamp) self.assertEqual(result, []) def test_report_cycle_none_timestamp(self): """Test reporting a cycle with None timestamp""" timestamp = datetime.now() self.db.start_cycle(timestamp) self.db.record_cycle( requested_duration=100, active_gpios="GPIO1", wakeup_irqs="IRQ1", kernel_duration=1.5, hw_sleep_duration=2.5, ) result = self.db.report_cycle(None) self.assertEqual( result, [ ( int(timestamp.strftime("%Y%m%d%H%M%S")), int(datetime.now().strftime("%Y%m%d%H%M%S")), 100, "GPIO1", "IRQ1", 1.5, 2.5, ) ], ) def test_get_last_cycle(self): """Test getting the last cycle from the database""" timestamp = datetime.now() self.db.start_cycle(timestamp) self.db.record_cycle( requested_duration=100, active_gpios="GPIO1", wakeup_irqs="IRQ1", kernel_duration=1.5, hw_sleep_duration=2.5, ) result = self.db.get_last_cycle() self.assertEqual(result, (int(timestamp.strftime("%Y%m%d%H%M%S")),)) def test_get_last_cycle_no_data(self): """Test getting the last cycle when no data exists""" result = self.db.get_last_cycle() self.assertIsNone(result) def test_get_last_prereq_ts(self): """Test getting the last prereq timestamp from the database""" timestamp = datetime.now() self.db.start_cycle(timestamp) self.db.record_prereq("Test prereq message 1", "symbol1") self.db.record_prereq("Test prereq message 2", "symbol2") result = self.db.get_last_prereq_ts() self.assertEqual(result, int(timestamp.strftime("%Y%m%d%H%M%S"))) def test_get_last_prereq_ts_no_data(self): """Test getting the last prereq timestamp when no data exists""" result = self.db.get_last_prereq_ts() self.assertEqual(result, 0) def test_report_cycle_data(self): """Test reporting cycle data""" timestamp = datetime.now() self.db.start_cycle(timestamp) self.db.record_cycle_data("Test cycle data 1", "symbol1") self.db.record_cycle_data("Test cycle data 2", "symbol2") result = self.db.report_cycle_data(timestamp) expected_result = "symbol1 Test cycle data 1\nsymbol2 Test cycle data 2\n" self.assertEqual(result, expected_result) def test_report_cycle_data_no_data(self): """Test reporting cycle data when no data exists""" timestamp = datetime.now() self.db.start_cycle(timestamp) result = self.db.report_cycle_data(timestamp) self.assertEqual(result, "") def test_report_cycle_data_none_timestamp(self): """Test reporting cycle data with None timestamp""" timestamp = datetime.now() self.db.start_cycle(timestamp) self.db.record_cycle_data("Test cycle data 1", "symbol1") result = self.db.report_cycle_data(None) expected_result = "symbol1 Test cycle data 1\n" self.assertEqual(result, expected_result) amd-debug-tools-0.2.15/src/test_display.py000066400000000000000000000132651515405217400204420ustar00rootroot00000000000000#!/usr/bin/python3 # SPDX-License-Identifier: MIT """ This module contains unit tests for the display functions in the amd-debug-tools package. """ import logging import unittest from unittest.mock import patch, MagicMock from amd_debug.display import Display class TestDisplay(unittest.TestCase): """Unit tests for the Display class in amd-debug-tools""" @classmethod def setUpClass(cls): logging.basicConfig(filename="/dev/null", level=logging.DEBUG) @patch("amd_debug.display.Context") @patch("amd_debug.display.read_file") @patch("os.path.exists") def test_display_initialization(self, mock_exists, mock_read_file, mock_context): """Test the Display class initialization and EDID retrieval""" # Mock the pyudev Context and its list_devices method mock_device = MagicMock() mock_device.device_path = "/devices/card0" mock_device.sys_path = "/sys/devices/card0" mock_device.sys_name = "card0" mock_context.return_value.list_devices.return_value = [mock_device] # Mock os.path.exists and read_file behavior mock_exists.side_effect = lambda path: "status" in path or "enabled" in path mock_read_file.side_effect = lambda path: ( "connected" if "status" in path else "enabled" ) # Initialize the Display class display = Display() # Verify the EDID paths are correctly set expected_edid = ["/sys/devices/card0/edid"] self.assertEqual(display.get_edid(), expected_edid) mock_context.assert_called_once() @patch("amd_debug.display.Context") def test_no_connected_displays(self, mock_context): """Test the Display class when no connected displays are found""" # Mock the pyudev Context to return no devices mock_context.return_value.list_devices.return_value = [] # Initialize the Display class display = Display() # Verify the EDID dictionary is empty self.assertEqual(display.get_edid(), []) @patch("amd_debug.display.Context") def test_device_without_card(self, mock_context): """Test the Display class with a device that does not have 'card' in the name""" # Mock the pyudev Context to return a device without 'card' in the name mock_device = MagicMock() mock_device.device_path = "/devices/other_device" mock_device.sys_path = "/sys/devices/other_device" mock_device.sys_name = "other_device" mock_context.return_value.list_devices.return_value = [mock_device] # Initialize the Display class display = Display() # Verify the EDID dictionary is empty self.assertEqual(display.get_edid(), []) @patch("amd_debug.display.Context") @patch("amd_debug.display.read_file") @patch("os.path.exists") def test_device_not_enabled(self, mock_exists, mock_read_file, mock_context): """Test the Display class with a device that is not enabled""" # Mock the pyudev Context to return a device that is not enabled mock_device = MagicMock() mock_device.device_path = "/devices/card0" mock_device.sys_path = "/sys/devices/card0" mock_device.sys_name = "card0" mock_context.return_value.list_devices.return_value = [mock_device] # Mock os.path.exists and read_file behavior mock_exists.side_effect = lambda path: "status" in path or "enabled" in path mock_read_file.side_effect = lambda path: ( "connected" if "status" in path else "disabled" ) # Initialize the Display class display = Display() # Verify the EDID dictionary is empty self.assertEqual(display.get_edid(), []) @patch("amd_debug.display.Context") @patch("amd_debug.display.read_file") @patch("os.path.exists") def test_missing_status_file(self, mock_exists, mock_read_file, mock_context): """Test the Display class when the status file is missing""" # Mock the pyudev Context to return a device with a missing status file mock_device = MagicMock() mock_device.device_path = "/devices/card0" mock_device.sys_path = "/sys/devices/card0" mock_device.sys_name = "card0" mock_context.return_value.list_devices.return_value = [mock_device] # Mock os.path.exists to return False for the status file mock_exists.side_effect = lambda path: "enabled" in path mock_read_file.side_effect = lambda path: "enabled" if "enabled" in path else "" # Initialize the Display class display = Display() # Verify the EDID dictionary is empty self.assertEqual(display.get_edid(), []) @patch("amd_debug.display.Context") @patch("amd_debug.display.read_file") @patch("os.path.exists") def test_status_not_connected(self, mock_exists, mock_read_file, mock_context): """Test the Display class when the status file does not indicate connected""" # Mock the pyudev Context to return a device with a status file that does not indicate connected mock_device = MagicMock() mock_device.device_path = "/devices/card0" mock_device.sys_path = "/sys/devices/card0" mock_device.sys_name = "card0" mock_context.return_value.list_devices.return_value = [mock_device] # Mock os.path.exists and read_file behavior mock_exists.side_effect = lambda path: "status" in path or "enabled" in path mock_read_file.side_effect = lambda path: ( "not_connected" if "status" in path else "enabled" ) # Initialize the Display class display = Display() # Verify the EDID dictionary is empty self.assertEqual(display.get_edid(), []) amd-debug-tools-0.2.15/src/test_failures.py000066400000000000000000000152751515405217400206120ustar00rootroot00000000000000#!/usr/bin/python3 # SPDX-License-Identifier: MIT """ This module contains unit tests for the failure functions in the amd-debug-tools package. """ from unittest.mock import patch, call import logging import unittest import os import amd_debug.failures class TestFailures(unittest.TestCase): """Test failure functions""" @classmethod def setUpClass(cls): logging.basicConfig(filename="/dev/null", level=logging.DEBUG) @patch("builtins.print") def test_failures(self, mocked_print): """Test failure functions""" cls = amd_debug.failures.RtcAlarmWrong() self.assertEqual( cls.get_description(), "rtc_cmos is not configured to use ACPI alarm" ) self.assertEqual( str(cls), "Some problems can occur during wakeup cycles if the HPET RTC " "emulation is used to wake systems. This can manifest in unexpected " "wakeups or high power consumption.For more information on this failure " "see:https://github.com/systemd/systemd/issues/24279", ) cls = amd_debug.failures.MissingAmdgpu() self.assertEqual(cls.get_description(), "AMDGPU driver is missing") cls = amd_debug.failures.MissingAmdgpuFirmware(["foo", "bar"]) self.assertEqual(cls.get_description(), "AMDGPU firmware is missing") cls = amd_debug.failures.AmdgpuPpFeatureMask() self.assertEqual(cls.get_description(), "AMDGPU ppfeaturemask changed") cls = amd_debug.failures.MissingAmdPmc() self.assertEqual(cls.get_description(), "AMD-PMC driver is missing") cls = amd_debug.failures.MissingThunderbolt() self.assertEqual(cls.get_description(), "thunderbolt driver is missing") cls = amd_debug.failures.MissingXhciHcd() self.assertEqual(cls.get_description(), "xhci_hcd driver is missing") cls = amd_debug.failures.MissingDriver("4") self.assertEqual(cls.get_description(), "4 driver is missing") cls = amd_debug.failures.AcpiBiosError("5") self.assertEqual(cls.get_description(), "ACPI BIOS Errors detected") cls = amd_debug.failures.UnsupportedModel() self.assertEqual(cls.get_description(), "Unsupported CPU model") cls = amd_debug.failures.UserNvmeConfiguration() self.assertEqual(cls.get_description(), "NVME ACPI support is disabled") cls = amd_debug.failures.AcpiNvmeStorageD3Enable("foo", 2) self.assertEqual(cls.get_description(), "foo missing ACPI attributes") cls = amd_debug.failures.DevSlpHostIssue() self.assertEqual( cls.get_description(), "AHCI controller doesn't support DevSlp" ) cls = amd_debug.failures.DevSlpDiskIssue() self.assertEqual(cls.get_description(), "SATA disk doesn't support DevSlp") cls = amd_debug.failures.SleepModeWrong() self.assertEqual( cls.get_description(), "The system hasn't been configured for Modern Standby in BIOS setup", ) cls = amd_debug.failures.DeepSleep() self.assertEqual( cls.get_description(), "The kernel command line is asserting the system to use deep sleep", ) cls = amd_debug.failures.FadtWrong() self.assertEqual( cls.get_description(), "The kernel didn't emit a message that low power idle was supported", ) cls = amd_debug.failures.Irq1Workaround() self.assertEqual( cls.get_description(), "The wakeup showed an IRQ1 wakeup source, which might be a platform firmware bug", ) cls = amd_debug.failures.KernelRingBufferWrapped() self.assertEqual(cls.get_description(), "Kernel ringbuffer has wrapped") cls = amd_debug.failures.AmdHsmpBug() self.assertEqual(cls.get_description(), "amd-hsmp built in to kernel") cls = amd_debug.failures.WCN6855Bug() self.assertEqual( cls.get_description(), "The firmware loaded for the WCN6855 causes spurious wakeups", ) cls = amd_debug.failures.I2CHidBug("touchpad", "block") self.assertEqual( cls.get_description(), "The touchpad device has been reported to cause high power consumption and spurious wakeups", ) cls = amd_debug.failures.SpuriousWakeup(1, 0) self.assertEqual( cls.get_description(), "Userspace wasn't asleep at least 0:00:01" ) cls = amd_debug.failures.LowHardwareSleepResidency(5, 30) self.assertEqual( cls.get_description(), "System had low hardware sleep residency" ) cls = amd_debug.failures.MSRFailure() self.assertEqual(cls.get_description(), "PC6 or CC6 state disabled") cls = amd_debug.failures.TaintedKernel() self.assertEqual(cls.get_description(), "Kernel is tainted") cls = amd_debug.failures.DMArNotEnabled() self.assertEqual(cls.get_description(), "Pre-boot DMA protection disabled") cls = amd_debug.failures.MissingIommuACPI("foo") self.assertEqual(cls.get_description(), "Device foo missing from ACPI tables") cls = amd_debug.failures.MissingIommuPolicy("foo") self.assertEqual( cls.get_description(), "Device foo does not have IOMMU policy applied" ) cls = amd_debug.failures.IommuPageFault("foo") self.assertEqual(cls.get_description(), "Page fault reported for foo") cls = amd_debug.failures.SMTNotEnabled() self.assertEqual(cls.get_description(), "SMT is not enabled") cls = amd_debug.failures.ASpmWrong() self.assertEqual(cls.get_description(), "ASPM is overridden") cls = amd_debug.failures.UnservicedGpio() self.assertEqual(cls.get_description(), "GPIO interrupt is not serviced") cls = amd_debug.failures.DmiNotSetup() self.assertEqual(cls.get_description(), "DMI data was not scanned") cls = amd_debug.failures.LimitedCores(10, 7) self.assertEqual(cls.get_description(), "CPU cores have been limited") cls = amd_debug.failures.RogAllyOldMcu(1, 2) self.assertEqual(cls.get_description(), "Rog Ally MCU firmware is too old") os.environ["TERM"] = "dumb" cls = amd_debug.failures.RogAllyMcuPowerSave() self.assertEqual(cls.get_description(), "Rog Ally MCU power save is disabled") failure = "The MCU powersave feature is disabled which will cause problems with the controller after suspend/resume." self.assertEqual(str(cls), failure) cls.get_failure() mocked_print.assert_has_calls( [ call("🚦 Rog Ally MCU power save is disabled"), call(failure), ] ) amd-debug-tools-0.2.15/src/test_installer.py000066400000000000000000000306231515405217400207670ustar00rootroot00000000000000#!/usr/bin/python3 # SPDX-License-Identifier: MIT """ This module contains unit tests for the installer functions in the amd-debug-tools package. """ from unittest.mock import patch, mock_open import logging import unittest from amd_debug.installer import Installer class TestInstaller(unittest.TestCase): """Test installer functions""" @classmethod def setUpClass(cls): logging.basicConfig(filename="/dev/null", level=logging.DEBUG) def setUp(self): self.installer = Installer(tool_debug=False) @patch("builtins.print") @patch("shutil.copy", return_value=None) @patch("os.chmod", return_value=None) @patch("builtins.open") @patch("subprocess.call", return_value=0) @patch("os.makedirs", return_value=None) def test_install_hook( self, _mock_mkdir, _mock_call, _mock_open, _mock_chmod, _mock_shutil, _mock_print, ): """Test install hook function""" self.installer.install() @patch("builtins.print") @patch("shutil.copy", return_value=None) @patch("os.chmod", return_value=None) @patch("builtins.open") @patch("subprocess.call", return_value=0) @patch("os.makedirs", return_value=None) def test_install_hook_missing_path( self, _mock_mkdir, _mock_call, _mock_open, _mock_chmod, _mock_shutil, _mock_print, ): """Test install hook function, but some paths are missing""" with patch("os.path.exists", return_value=False): self.installer.install() @patch("builtins.print") @patch("os.remove", return_value=None) @patch("builtins.open") @patch("amd_debug.installer.get_distro", return_value="ubuntu") @patch("subprocess.call", return_value=0) def test_remove_hook( self, _mock_call, _mock_get_distro, _mock_open, _mock_remove, _mock_print ): """Test remove hook function""" with patch("os.path.exists", return_value=True): self.installer.remove() @patch("builtins.print") @patch("os.path.exists", return_value=False) @patch("subprocess.call", return_value=0) def test_remove_hook_missing_path(self, _mock_call, _mock_exists, _mock_print): """Test remove hook function when the file is missing""" self.installer.remove() @patch("builtins.print") @patch("subprocess.call", return_value=0) def test_already_installed_iasl(self, _mock_call, _mock_print): """Test that an already installed iasl is found""" self.installer.set_requirements("iasl") ret = self.installer.install_dependencies() self.assertTrue(ret) @patch("builtins.print") @patch("amd_debug.installer.get_distro", return_value="ubuntu") @patch("os.execvp", return_value=None) @patch("subprocess.check_call", return_value=0) @patch("subprocess.call", return_value=1) def test_install_iasl_ubuntu( self, _mock_call, _mock_check_call, _mock_distro, _fake_sudo, _mock_print ): """Test install requirements function""" self.installer.set_requirements("iasl") ret = self.installer.install_dependencies() _mock_check_call.assert_called_once_with(["apt", "install", "acpica-tools"]) self.assertTrue(ret) @patch("builtins.print") @patch("amd_debug.installer.get_distro", return_value="fedora") @patch( "builtins.open", new_callable=mock_open, read_data="VARIANT_ID=workstation\n" ) @patch("os.execvp", return_value=None) @patch("subprocess.check_call", return_value=0) @patch("subprocess.call", return_value=1) def test_install_iasl_fedora( self, _mock_call, _mock_check_call, _mock_variant, _mock_distro, _fake_sudo, _mock_print, ): """Test install requirements function""" self.installer.set_requirements("iasl") ret = self.installer.install_dependencies() _mock_check_call.assert_called_once_with( ["dnf", "install", "-y", "acpica-tools"] ) self.assertTrue(ret) @patch("builtins.print") @patch("amd_debug.installer.get_distro", return_value="fedora") @patch("builtins.open", new_callable=mock_open, read_data="VARIANT_ID=kde\n") @patch("os.execvp", return_value=None) @patch("subprocess.check_call", return_value=0) @patch("subprocess.call", return_value=1) def test_install_iasl_fedora_kde( self, _mock_call, _mock_check_call, _mock_variant, _mock_distro, _fake_sudo, _mock_print, ): """Test install requirements function on Fedora KDE""" self.installer.set_requirements("iasl") ret = self.installer.install_dependencies() _mock_check_call.assert_called_once_with( ["dnf", "install", "-y", "acpica-tools"] ) self.assertTrue(ret) @patch("builtins.print") @patch("amd_debug.installer.get_distro", return_value="ubuntu") @patch("os.execvp", return_value=None) @patch("subprocess.check_call", return_value=0) @patch("subprocess.call", return_value=1) def test_install_ethtool_ubuntu( self, _mock_call, _mock_check_call, _mock_distro, _fake_sudo, _mock_print ): """Test install requirements function""" self.installer.set_requirements("ethtool") ret = self.installer.install_dependencies() _mock_check_call.assert_called_once_with(["apt", "install", "ethtool"]) self.assertTrue(ret) @patch("builtins.print") @patch("amd_debug.installer.get_distro", return_value="fedora") @patch( "builtins.open", new_callable=mock_open, read_data="VARIANT_ID=workstation\n" ) @patch("os.execvp", return_value=None) @patch("subprocess.check_call", return_value=0) @patch("subprocess.call", return_value=1) def test_install_ethtool_fedora( self, _mock_call, _mock_check_call, _mock_variant, _mock_distro, _fake_sudo, _mock_print, ): """Test install requirements function""" self.installer.set_requirements("ethtool") ret = self.installer.install_dependencies() _mock_check_call.assert_called_once_with(["dnf", "install", "-y", "ethtool"]) self.assertTrue(ret) @patch("builtins.print") @patch("amd_debug.installer.get_distro", return_value="fedora") @patch("builtins.open", new_callable=mock_open, read_data="VARIANT_ID=kde\n") @patch("os.execvp", return_value=None) @patch("subprocess.check_call", return_value=0) @patch("subprocess.call", return_value=1) def test_install_ethtool_fedora_kde( self, _mock_call, _mock_check_call, _mock_variant, _mock_distro, _fake_sudo, _mock_print, ): """Test install requirements function on Fedora KDE""" self.installer.set_requirements("ethtool") ret = self.installer.install_dependencies() _mock_check_call.assert_called_once_with(["dnf", "install", "-y", "ethtool"]) self.assertTrue(ret) @patch("builtins.print") @patch("amd_debug.installer.get_distro", return_value="arch") @patch("os.execvp", return_value=None) @patch("subprocess.check_call", return_value=0) @patch("subprocess.call", return_value=1) def test_install_ethtool_arch( self, _mock_call, _mock_check_call, _mock_distro, _fake_sudo, _mock_print, ): """Test install requirements function""" self.installer.set_requirements("ethtool") ret = self.installer.install_dependencies() _mock_check_call.assert_called_once_with(["pacman", "-Sy", "ethtool"]) self.assertTrue(ret) @patch("builtins.print") @patch("os.path.exists", return_value=False) @patch("os.execvp", return_value=None) @patch("amd_debug.installer.get_distro", return_value="gentoo") @patch("subprocess.call", return_value=1) def test_install_iasl_gentoo( self, _mock_call, _mock_distro, _fake_sudo, _mock_exists, _mock_print ): """Test install requirements function""" self.installer.set_requirements("iasl", "ethtool") ret = self.installer.install_dependencies() self.assertTrue(ret) @patch("builtins.print") @patch("amd_debug.installer.get_distro", return_value="ubuntu") @patch("os.execvp", return_value=None) @patch("subprocess.check_call", return_value=0) @patch("subprocess.call", return_value=1) def test_install_edid_decode_ubuntu( self, _mock_call, _mock_check_call, _mock_distro, _fake_sudo, _mock_print ): """Test install requirements function for edid-decode on Ubuntu""" self.installer.set_requirements("edid-decode") ret = self.installer.install_dependencies() _mock_check_call.assert_called_once_with( ["apt", "install", "libdisplay-info-bin"] ) self.assertTrue(ret) @patch("builtins.print") @patch("amd_debug.installer.get_distro", return_value="fedora") @patch( "builtins.open", new_callable=mock_open, read_data="VARIANT_ID=workstation\n" ) @patch("os.execvp", return_value=None) @patch("subprocess.check_call", return_value=0) @patch("subprocess.call", return_value=1) def test_install_edid_decode_fedora( self, _mock_call, _mock_check_call, _mock_variant, _mock_distro, _fake_sudo, _mock_print, ): """Test install requirements function for edid-decode on Fedora""" self.installer.set_requirements("edid-decode") ret = self.installer.install_dependencies() _mock_check_call.assert_called_once_with( ["dnf", "install", "-y", "libdisplay-info-tools"] ) self.assertTrue(ret) @patch("builtins.print") @patch("amd_debug.installer.get_distro", return_value="fedora") @patch("builtins.open", new_callable=mock_open, read_data="VARIANT_ID=kde\n") @patch("os.execvp", return_value=None) @patch("subprocess.check_call", return_value=0) @patch("subprocess.call", return_value=1) def test_install_edid_decode_fedora_kde( self, _mock_call, _mock_check_call, _mock_variant, _mock_distro, _fake_sudo, _mock_print, ): """Test install requirements function for edid-decode on Fedora KDE""" self.installer.set_requirements("edid-decode") ret = self.installer.install_dependencies() _mock_check_call.assert_called_once_with( ["dnf", "install", "-y", "libdisplay-info-tools"] ) self.assertTrue(ret) @patch("builtins.print") @patch("amd_debug.installer.get_distro", return_value="arch") @patch("os.execvp", return_value=None) @patch("subprocess.check_call", return_value=0) @patch("subprocess.call", return_value=1) def test_install_edid_decode_arch( self, _mock_call, _mock_check_call, _mock_distro, _fake_sudo, _mock_print, ): """Test install requirements function for edid-decode on Arch""" self.installer.set_requirements("edid-decode") ret = self.installer.install_dependencies() _mock_check_call.assert_called_once_with(["pacman", "-Sy", "libdisplay-info"]) self.assertTrue(ret) @patch("builtins.print") @patch("os.path.exists", return_value=False) @patch("os.execvp", return_value=None) @patch("amd_debug.installer.get_distro", return_value="gentoo") @patch("subprocess.call", return_value=1) def test_install_edid_decode_gentoo( self, _mock_call, _mock_distro, _fake_sudo, _mock_exists, _mock_print ): """Test install requirements function for edid-decode on unsupported distro""" self.installer.set_requirements("edid-decode") ret = self.installer.install_dependencies() self.assertTrue(ret) @patch("builtins.print") @patch("os.path.exists", return_value=False) @patch("os.execvp", return_value=None) @patch("amd_debug.installer.get_distro", return_value="gentoo") @patch("subprocess.call", return_value=255) def test_install_edid_decode_present( self, _mock_call, _mock_distro, _fake_sudo, _mock_exists, _mock_print ): """Test install requirements function for edid-decode on unsupported distro""" self.installer.set_requirements("edid-decode") ret = self.installer.install_dependencies() self.assertTrue(ret) amd-debug-tools-0.2.15/src/test_kernel.py000066400000000000000000000174151515405217400202560ustar00rootroot00000000000000#!/usr/bin/python3 # SPDX-License-Identifier: MIT """ This module contains unit tests for the kernel log functions in the amd-debug-tools package. """ from unittest.mock import patch, mock_open import unittest import logging from amd_debug.kernel import sscanf_bios_args, get_kernel_command_line, DmesgLogger class TestKernelLog(unittest.TestCase): """Test Common kernel scan functions""" @classmethod def setUpClass(cls): logging.basicConfig(filename="/dev/null", level=logging.DEBUG) def test_get_kernel_command_line(self): """Test get_kernel_command_line function""" # Test case with a valid kernel command line that is fully filtered kernel_cmdline = "quiet splash" expected_output = "" with patch( "amd_debug.common.open", new_callable=mock_open, read_data=kernel_cmdline ) as _mock_file: result = get_kernel_command_line() self.assertEqual(result, expected_output) # Test case with an empty kernel command line kernel_cmdline = "" expected_output = "" with patch( "amd_debug.common.open", new_callable=mock_open, read_data=kernel_cmdline ) as _mock_file: result = get_kernel_command_line() self.assertEqual(result, expected_output) # Test case with a kernel command line containing special characters kernel_cmdline = "quiet splash --debug=1" expected_output = "--debug=1" with patch( "amd_debug.common.open", new_callable=mock_open, read_data=kernel_cmdline ) as _mock_file: result = get_kernel_command_line() self.assertEqual(result, expected_output) # Test case with a kernel command line containing special characters kernel_cmdline = "quiet splash initrd=foo modprobe.blacklist=foo" expected_output = "modprobe.blacklist=foo" with patch( "amd_debug.common.open", new_callable=mock_open, read_data=kernel_cmdline ) as _mock_file: result = get_kernel_command_line() self.assertEqual(result, expected_output) def test_sscanf_bios_args(self): """Test sscanf_bios_args function""" # Test case with a valid line line = 'ex_trace_args: "format_string", 0x1234, 0x5678' expected_output = "format_string" result = sscanf_bios_args(line) self.assertEqual(result, expected_output) # Test case with an invalid line line = 'invalid_line: "format_string", 0x1234, 0x5678' result = sscanf_bios_args(line) self.assertIsNone(result) # Test case with a line containing "Unknown" line = 'ex_trace_args: "format_string", Unknown, 0x5678' expected_output = "format_string" result = sscanf_bios_args(line) self.assertEqual(result, expected_output) # make sure that lines with ex_trace_point are not parsed line = 'ex_trace_point: "format_string", 0x1234, 0x5678' result = sscanf_bios_args(line) self.assertTrue(result) # test a real post code line line = 'ex_trace_args: " POST CODE: %X ACPI TIMER: %X TIME: %d.%d ms\\n", b0003f33, 83528798, 0, 77, 0, 0' expected_output = "POST CODE: B0003F33 ACPI TIMER: 83528798 TIME: 0.119 ms" result = sscanf_bios_args(line) self.assertEqual(result, expected_output) # test a real _REG print line = 'ex_trace_args: " OEM-ASL-PCIe Address (0x%X)._REG (%d %d) PCSA = %d\\n", ec303000, 2, 0, 0, 0, 0' expected_output = "OEM-ASL-PCIe Address (0xEC303000)._REG (2 0) PCSA = 0" result = sscanf_bios_args(line) self.assertEqual(result, expected_output) # test case of too may arguments line = 'ex_trace_args : " APGE = %d\\n", 1, 0, 0, 0, 0, 0' expected_output = "APGE = 1" result = sscanf_bios_args(line) self.assertEqual(result, expected_output) # test case for Dispatch notify line = "evmisc-0132 ev_queue_notify_reques: Dispatching Notify on [UBTC] (Device) Value 0x80 (Status Change) Node 00000000851b15c1" expected_output = ( "Dispatching Notify on [UBTC] (Device) Value 0x80 (Status Change)" ) result = sscanf_bios_args(line) self.assertEqual(result, expected_output) class TestDmesgLogger(unittest.TestCase): """Test Dmesg logger functions""" @classmethod @patch("subprocess.run") def setUpClass(cls, _mock_run=None): logging.basicConfig(filename="/dev/null", level=logging.DEBUG) def test_dmesg_logger_initialization(self): """Test initialization of DmesgLogger""" with patch("subprocess.run") as mock_run: # Mock the subprocess output for dmesg -h mock_run.return_value.stdout = b"--since supported\n" mock_run.return_value.returncode = 0 logger = DmesgLogger() self.assertTrue(logger.since_support) self.assertEqual(logger.command, ["dmesg", "-t", "-k"]) def test_dmesg_logger_refresh_head(self): """Test _refresh_head method of DmesgLogger""" with patch("subprocess.run") as mock_run: # Mock the subprocess output for dmesg mock_run.return_value.stdout = b"line1\nline2\n" mock_run.return_value.returncode = 0 logger = DmesgLogger() logger._refresh_head() # pylint: disable=protected-access self.assertEqual(logger.buffer, "line1\nline2\n") def test_dmesg_logger_seek_tail(self): """Test seek_tail method of DmesgLogger""" with patch("subprocess.run") as mock_run: # Mock the subprocess output for dmesg mock_run.return_value.stdout = b"line1\nline2\n" mock_run.return_value.returncode = 0 logger = DmesgLogger() logger.seek_tail() self.assertEqual(logger.buffer, "line1\nline2\n") def test_dmesg_logger_process_callback(self): """Test process_callback method of DmesgLogger""" with patch("subprocess.run") as mock_run: # Mock the subprocess output for dmesg mock_run.return_value.stdout = b"line1\nline2\n" mock_run.return_value.returncode = 0 logger = DmesgLogger() logger._refresh_head() # pylint: disable=protected-access mock_callback = unittest.mock.Mock() logger.process_callback(mock_callback) mock_callback.assert_any_call("line1", None) mock_callback.assert_any_call("line2", None) def test_dmesg_logger_match_line(self): """Test match_line method of DmesgLogger""" with patch("subprocess.run") as mock_run: # Mock the subprocess output for dmesg mock_run.return_value.stdout = b"line1\nline2\n" mock_run.return_value.returncode = 0 logger = DmesgLogger() logger._refresh_head() # pylint: disable=protected-access result = logger.match_line(["line1"]) self.assertEqual(result, "line1") result = logger.match_line(["nonexistent"]) self.assertEqual(result, "") def test_dmesg_logger_match_pattern(self): """Test match_pattern method of DmesgLogger""" with patch("subprocess.run") as mock_run: # Mock the subprocess output for dmesg mock_run.return_value.stdout = b"line1\nline2\n" mock_run.return_value.returncode = 0 logger = DmesgLogger() logger._refresh_head() # pylint: disable=protected-access result = logger.match_pattern(r"line\d") self.assertEqual(result, "line1") result = logger.match_pattern(r"nonexistent") self.assertEqual(result, "") amd-debug-tools-0.2.15/src/test_launcher.py000066400000000000000000000033051515405217400205700ustar00rootroot00000000000000#!/usr/bin/python3 # SPDX-License-Identifier: MIT """ This module contains unit tests for the launcher in the amd-debug-tools package. """ from unittest.mock import patch import logging import unittest import amd_debug class TestLauncher(unittest.TestCase): """Test launcher functions""" @classmethod def setUpClass(cls): logging.basicConfig(filename="/dev/null", level=logging.DEBUG) def setUp(self): launcher = None def test_launcher_unknown(self): """Test launching as unknown exe""" with patch("builtins.print") as mock_print: result = amd_debug.launch_tool("unknown_exe.py") mock_print.assert_called_once_with( "\033[91mUnknown exe: unknown_exe.py\033[0m" ) self.assertIsNotNone(result) def test_launcher_amd_s2idle(self): """Test launching amd_s2idle""" with patch("amd_debug.s2idle.main") as mock_main: amd_debug.launch_tool("amd_s2idle.py") mock_main.assert_called_once() def test_launcher_amd_bios(self): """Test launching amd_bios""" with patch("amd_debug.bios.main") as mock_main: amd_debug.launch_tool("amd_bios.py") mock_main.assert_called_once() def test_launcher_amd_pstate(self): """Test launching amd_pstate""" with patch("amd_debug.pstate.main") as mock_main: amd_debug.launch_tool("amd_pstate.py") mock_main.assert_called_once() def test_launcher_amd_ttm(self): """Test launching amd_ttm""" with patch("amd_debug.ttm.main") as mock_main: amd_debug.launch_tool("amd_ttm.py") mock_main.assert_called_once() amd-debug-tools-0.2.15/src/test_prerequisites.py000066400000000000000000003346241515405217400217060ustar00rootroot00000000000000#!/usr/bin/python3 # SPDX-License-Identifier: MIT """ This module contains unit tests for the prerequisite functions in the amd-debug-tools package. """ import logging import unittest import subprocess import struct from unittest.mock import patch, MagicMock, mock_open from amd_debug.prerequisites import PrerequisiteValidator from amd_debug.failures import * from amd_debug.common import apply_prefix_wrapper, BIT class TestPrerequisiteValidator(unittest.TestCase): @classmethod def setUpClass(cls): logging.basicConfig(filename="/dev/null", level=logging.DEBUG) @patch("amd_debug.prerequisites.is_root", return_value=True) @patch("amd_debug.prerequisites.get_kernel_log") @patch("amd_debug.prerequisites.get_distro", return_value="Ubuntu") @patch("amd_debug.prerequisites.read_file", return_value="mocked_cmdline") @patch("amd_debug.prerequisites.pyudev.Context") @patch("amd_debug.prerequisites.SleepDatabase") def setUp( self, MockSleepDatabase, MockPyudev, _mock_read_file, _mock_get_distro, mock_get_kernel_log, _mock_is_root, ): self.mock_db = MockSleepDatabase.return_value self.mock_pyudev = MockPyudev.return_value self.mock_kernel_log = mock_get_kernel_log.return_value self.validator = PrerequisiteValidator(tool_debug=True) def test_check_amdgpu_no_driver(self): """Test check_amdgpu with no driver present""" self.mock_pyudev.list_devices.return_value = [ MagicMock( properties={"PCI_CLASS": "30000", "PCI_ID": "1002abcd", "DRIVER": None} ) ] result = self.validator.check_amdgpu() self.assertFalse(result) self.assertTrue( any(isinstance(f, MissingAmdgpu) for f in self.validator.failures) ) def test_check_amdgpu_with_driver(self): """Test check_amdgpu with driver present""" self.mock_pyudev.list_devices.return_value = [ MagicMock( properties={ "PCI_CLASS": "30000", "PCI_ID": "1002abcd", "DRIVER": "amdgpu", } ) ] result = self.validator.check_amdgpu() self.assertTrue(result) def test_check_wcn6855_bug_no_bug(self): """Test check_wcn6855_bug with no bug present""" self.mock_kernel_log.match_pattern.side_effect = lambda pattern: None result = self.validator.check_wcn6855_bug() self.assertTrue(result) def test_check_storage_no_nvme(self): """Test check_storage with no NVMe devices""" self.mock_pyudev.list_devices.return_value = [] result = self.validator.check_storage() self.assertTrue(result) @patch("amd_debug.prerequisites.minimum_kernel", return_value=True) def test_check_amd_hsmp_new_kernel(self, _mock_minimum_kernel): """Test check_amd_hsmp with CONFIG_AMD_HSMP=y and kernel version >= 6.10""" result = self.validator.check_amd_hsmp() self.assertTrue(result) @patch("amd_debug.prerequisites.read_file", return_value="CONFIG_AMD_HSMP=y") @patch("os.path.exists", return_value=True) @patch("amd_debug.prerequisites.minimum_kernel", return_value=False) def test_check_amd_hsmp_conflict( self, _mock_min_kernel, _mock_exists, _mock_read_file ): """Test check_amd_hsmp with CONFIG_AMD_HSMP=y and kernel version < 6.10""" result = self.validator.check_amd_hsmp() self.assertFalse(result) self.assertTrue(any(isinstance(f, AmdHsmpBug) for f in self.validator.failures)) def test_check_amd_pmc_no_driver(self): """Test check_amd_pmc with no driver""" self.mock_pyudev.list_devices.return_value = [] result = self.validator.check_amd_pmc() self.assertFalse(result) self.assertTrue( any(isinstance(f, MissingAmdPmc) for f in self.validator.failures) ) def test_check_sleep_mode_not_supported(self): """Test check_sleep_mode with no sleep mode support""" with patch("os.path.exists", return_value=False): result = self.validator.check_sleep_mode() self.assertFalse(result) def test_check_sleep_mode_s2idle(self): """Test check_sleep_mode with s2idle mode""" with patch("os.path.exists", return_value=True), patch( "amd_debug.prerequisites.read_file", return_value="[s2idle]" ): result = self.validator.check_sleep_mode() self.assertTrue(result) def test_check_port_pm_override_non_family_19(self): """Test check_port_pm_override with non-family 0x19 CPU""" self.validator.cpu_family = 0x18 result = self.validator.check_port_pm_override() self.assertTrue(result) def test_check_port_pm_override_non_matching_model(self): """Test check_port_pm_override with non-matching CPU model""" self.validator.cpu_family = 0x19 self.validator.cpu_model = 0x72 result = self.validator.check_port_pm_override() self.assertTrue(result) @patch("amd_debug.prerequisites.version.parse") def test_check_port_pm_override_smu_version_too_high(self, mock_version_parse): """Test check_port_pm_override with SMU version > 76.60.0""" self.validator.cpu_family = 0x19 self.validator.cpu_model = 0x74 mock_version_parse.side_effect = lambda v: v if isinstance(v, str) else None self.validator.smu_version = "76.61.0" result = self.validator.check_port_pm_override() self.assertTrue(result) @patch("amd_debug.prerequisites.version.parse") def test_check_port_pm_override_smu_version_missing(self, mock_version_parse): """Test check_port_pm_override with SMU version undefined""" self.validator.cpu_family = 0x19 self.validator.cpu_model = 0x74 mock_version_parse.side_effect = lambda v: v if isinstance(v, str) else None self.validator.smu_version = "" result = self.validator.check_port_pm_override() self.assertTrue(result) @patch("amd_debug.prerequisites.version.parse") def test_check_port_pm_override_smu_version_too_low(self, mock_version_parse): """Test check_port_pm_override with SMU version < 76.18.0""" self.validator.cpu_family = 0x19 self.validator.cpu_model = 0x74 mock_version_parse.side_effect = lambda v: v if isinstance(v, str) else None self.validator.smu_version = "76.17.0" result = self.validator.check_port_pm_override() self.assertTrue(result) @patch("amd_debug.prerequisites.read_file", return_value="pcie_port_pm=off") def test_check_port_pm_override_cmdline_override(self, mock_read_file): """Test check_port_pm_override with pcie_port_pm=off in cmdline""" self.validator.cpu_family = 0x19 self.validator.cpu_model = 0x74 self.validator.smu_version = "76.50.0" result = self.validator.check_port_pm_override() self.assertTrue(result) @patch("amd_debug.prerequisites.read_file", return_value="mocked_cmdline") def test_check_port_pm_override_no_override(self, mock_read_file): """Test check_port_pm_override without pcie_port_pm=off in cmdline""" self.validator.cpu_family = 0x19 self.validator.cpu_model = 0x74 self.validator.smu_version = "76.50.0" result = self.validator.check_port_pm_override() self.assertFalse(result) def test_check_iommu_disabled(self): """Test check_iommu when IOMMU is disabled""" self.validator.cpu_family = 0x1A self.validator.cpu_model = 0x20 self.mock_pyudev.list_devices.return_value = [] result = self.validator.check_iommu() self.assertTrue(result) self.mock_db.record_prereq.assert_called_with("IOMMU disabled", "βœ…") @patch( "amd_debug.prerequisites.open", new_callable=unittest.mock.mock_open, read_data=b"\x00" * 45, ) @patch("amd_debug.prerequisites.os.path.exists", return_value=True) def test_check_iommu_no_dma_protection_no_msft0201(self, _mock_open, _mock_exists): """Test check_iommu when DMA protection is not enabled and no MSFT0201 in IVRS""" self.validator.cpu_family = 0x1A self.validator.cpu_model = 0x20 iommu_device = MagicMock(sys_path="/sys/devices/iommu") acpi_device = MagicMock(sys_path="/sys/devices/acpi/MSFT0201") platform_device = MagicMock(sys_path="/sys/devices/platform/MSFT0201") self.mock_pyudev.list_devices.side_effect = [ [iommu_device], [acpi_device], [platform_device], ] result = self.validator.check_iommu() self.assertFalse(result) self.assertTrue( any(isinstance(f, DMArNotEnabled) for f in self.validator.failures) ) self.mock_db.record_prereq.assert_called_with( "IOMMU is misconfigured: Pre-boot DMA protection not enabled", "❌" ) @patch( "amd_debug.prerequisites.open", new_callable=unittest.mock.mock_open, read_data=b"\x00" * 45 + "MSFT0201".encode("utf-8"), ) @patch("amd_debug.prerequisites.os.path.exists", return_value=True) def test_check_iommu_no_dma_protection_BUT_msft0201(self, _mock_open, _mock_exists): """Test check_iommu when DMA protection is not enabled BUT MSFT0201 is in IVRS""" self.validator.cpu_family = 0x1A self.validator.cpu_model = 0x20 iommu_device = MagicMock(sys_path="/sys/devices/iommu") acpi_device = MagicMock(sys_path="/sys/devices/acpi/MSFT0201") platform_device = MagicMock(sys_path="/sys/devices/platform/MSFT0201") self.mock_pyudev.list_devices.side_effect = [ [iommu_device], [acpi_device], [platform_device], ] result = self.validator.check_iommu() self.assertTrue(result) @patch( "amd_debug.prerequisites.open", new_callable=unittest.mock.mock_open, read_data=b"\x00" * 36 + b"\xff" * 4, ) def test_check_iommu_missing_acpi_device(self, _mock_open): """Test check_iommu when MSFT0201 ACPI device is missing""" self.validator.cpu_family = 0x1A self.validator.cpu_model = 0x20 iommu_device = MagicMock(sys_path="/sys/devices/iommu") self.mock_pyudev.list_devices.side_effect = [ [iommu_device], [], [], ] result = self.validator.check_iommu() self.assertFalse(result) self.assertTrue( any(isinstance(f, MissingIommuACPI) for f in self.validator.failures) ) self.mock_db.record_prereq.assert_called_with( "IOMMU is misconfigured: missing MSFT0201 ACPI device", "❌" ) @patch( "amd_debug.prerequisites.open", new_callable=unittest.mock.mock_open, read_data=b"\x00" * 36 + b"\xff" * 4, ) def test_check_iommu_missing_policy(self, _mock_open): """Test check_iommu when policy is not bound to MSFT0201""" self.validator.cpu_family = 0x1A self.validator.cpu_model = 0x20 iommu_device = MagicMock(sys_path="/sys/devices/iommu") acpi_device = MagicMock(sys_path="/sys/devices/acpi/MSFT0201") platform_device = MagicMock(sys_path="/sys/devices/platform/MSFT0201") self.mock_pyudev.list_devices.side_effect = [ [iommu_device], [acpi_device], [platform_device], ] result = self.validator.check_iommu() self.assertFalse(result) self.assertTrue( any(isinstance(f, MissingIommuPolicy) for f in self.validator.failures) ) @patch( "amd_debug.prerequisites.open", new_callable=unittest.mock.mock_open, read_data=b"\x00" * 36 + b"\xff" * 4, ) @patch("amd_debug.prerequisites.os.path.exists", return_value=True) def test_check_iommu_properly_configured(self, _mock_open, _mock_exists): """Test check_iommu when IOMMU is properly configured""" self.validator.cpu_family = 0x1A self.validator.cpu_model = 0x20 iommu_device = MagicMock(sys_path="/sys/devices/iommu") acpi_device = MagicMock(sys_path="/sys/devices/acpi/MSFT0201") platform_device = MagicMock(sys_path="/sys/devices/platform/MSFT0201") self.mock_pyudev.list_devices.side_effect = [ [iommu_device], [acpi_device], [platform_device], ] result = self.validator.check_iommu() self.assertTrue(result) self.mock_db.record_prereq.assert_called_with("IOMMU properly configured", "βœ…") @patch("amd_debug.prerequisites.read_file") @patch("amd_debug.validator.print_color") def test_check_taint_not_tainted(self, _mock_print_color, mock_read_file): """Test check_taint when the kernel is not tainted""" mock_read_file.return_value = "0" result = self.validator.check_taint() self.assertTrue(result) self.assertFalse( any(isinstance(f, TaintedKernel) for f in self.validator.failures) ) @patch("amd_debug.prerequisites.read_file") @patch("amd_debug.validator.print_color") def test_check_taint_tainted(self, _mock_print_color, mock_read_file): """Test check_taint when the kernel is tainted""" mock_read_file.return_value = str( BIT(9) | 1 ) # Kernel warnings ignored, other taint present result = self.validator.check_taint() self.assertTrue(result) self.assertTrue( any(isinstance(f, TaintedKernel) for f in self.validator.failures) ) @patch("amd_debug.prerequisites.read_file") @patch("amd_debug.validator.print_color") def test_check_taint_file_not_found(self, _mock_print_color, mock_read_file): """Test check_taint when the tainted file is not found""" mock_read_file.side_effect = FileNotFoundError with self.assertRaises(FileNotFoundError): self.validator.check_taint() @patch("amd_debug.prerequisites.read_file") @patch("amd_debug.validator.print_color") def test_check_taint_invalid_value(self, _mock_print_color, mock_read_file): """Test check_taint when the tainted file contains invalid data""" mock_read_file.return_value = "invalid" with self.assertRaises(ValueError): self.validator.check_taint() @patch("amd_debug.prerequisites.read_file") def test_check_smt_not_supported(self, mock_read_file): """Test check_smt when SMT is not supported""" mock_read_file.side_effect = ["notsupported"] result = self.validator.check_smt() self.assertTrue(result) self.mock_db.record_debug.assert_called_with("SMT control: notsupported") @patch("amd_debug.prerequisites.read_file") def test_check_smt_disabled(self, mock_read_file): """Test check_smt when SMT is disabled""" mock_read_file.side_effect = ["on", "0"] result = self.validator.check_smt() self.assertFalse(result) self.assertTrue( any(isinstance(f, SMTNotEnabled) for f in self.validator.failures) ) self.mock_db.record_prereq.assert_called_with("SMT is not enabled", "❌") @patch("amd_debug.prerequisites.read_file") def test_check_smt_enabled(self, mock_read_file): """Test check_smt when SMT is enabled""" mock_read_file.side_effect = ["on", "1"] result = self.validator.check_smt() self.assertTrue(result) self.mock_db.record_prereq.assert_called_with("SMT enabled", "βœ…") @patch("amd_debug.prerequisites.read_msr") def test_check_msr_pc6_disabled(self, mock_read_msr): """Test check_msr when PC6 is disabled""" mock_read_msr.side_effect = lambda reg, _: ( 0 if reg == 0xC0010292 else BIT(22) | BIT(14) | BIT(6) ) result = self.validator.check_msr() self.assertFalse(result) self.assertTrue(any(isinstance(f, MSRFailure) for f in self.validator.failures)) @patch("amd_debug.prerequisites.read_msr") def test_check_msr_cc6_disabled(self, mock_read_msr): """Test check_msr when CC6 is disabled""" mock_read_msr.side_effect = lambda reg, _: BIT(32) if reg == 0xC0010292 else 0 result = self.validator.check_msr() self.assertFalse(result) self.assertTrue(any(isinstance(f, MSRFailure) for f in self.validator.failures)) @patch("amd_debug.prerequisites.read_msr") def test_check_msr_enabled(self, mock_read_msr): """Test check_msr when PC6 and CC6 are enabled""" mock_read_msr.side_effect = lambda reg, _: ( BIT(32) if reg == 0xC0010292 else (BIT(22) | BIT(14) | BIT(6)) ) result = self.validator.check_msr() self.assertTrue(result) self.mock_db.record_prereq.assert_called_with("PC6 and CC6 enabled", "βœ…") @patch("amd_debug.prerequisites.read_msr") def test_check_msr_file_not_found(self, mock_read_msr): """Test check_msr when MSR file is not found""" mock_read_msr.side_effect = FileNotFoundError result = self.validator.check_msr() self.assertFalse(result) self.mock_db.record_prereq.assert_called_with( "Unable to check MSRs: MSR kernel module not loaded", "❌" ) @patch("amd_debug.prerequisites.read_msr") def test_check_msr_permission_error(self, mock_read_msr): """Test check_msr when there is a permission error""" mock_read_msr.side_effect = PermissionError result = self.validator.check_msr() self.assertTrue(result) self.mock_db.record_prereq.assert_called_with("MSR checks unavailable", "🚦") @patch("amd_debug.prerequisites.read_file") @patch("amd_debug.prerequisites.os.path.exists", return_value=True) def test_check_cpu_unsupported_model(self, mock_path_exists, mock_read_file): """Test check_cpu with an unsupported CPU model""" self.validator.cpu_family = 0x19 self.validator.cpu_model = 0x08 mock_read_file.return_value = "7" result = self.validator.check_cpu() self.assertFalse(result) self.assertTrue( any(isinstance(f, UnsupportedModel) for f in self.validator.failures) ) self.mock_db.record_prereq.assert_called_with( "This CPU model does not support hardware sleep over s2idle", "❌" ) @patch("builtins.open", new_callable=mock_open) @patch("amd_debug.prerequisites.read_file") @patch("amd_debug.prerequisites.os.path.exists") def test_check_cpu_limited_cores_single_ccd( self, mock_path_exists, mock_read_file, mock_file_open ): """Test check_cpu with artificially limited CPUs on single-CCD system""" self.validator.cpu_family = 0x19 self.validator.cpu_model = 0x74 mock_read_file.return_value = "7" # kernel_max = 7 (8 cores) mock_path_exists.return_value = True # Simulate finding socket level at subleaf 1 # First call: subleaf 0, level_type = 1 (not socket) # Second call: subleaf 1, level_type = 4 (socket level) # Third call: read cpu_count from subleaf 1 mock_file_open.return_value.read.side_effect = [ struct.pack("4I", 0, 0, 0x100, 0), # subleaf 0: level_type = 1 (thread) struct.pack( "4I", 0, 0, 0x400, 0 ), # subleaf 1: level_type = 4 (socket) - FOUND struct.pack("4I", 0, 16, 0, 0), # subleaf 1: cpu_count = 16 ] result = self.validator.check_cpu() self.assertFalse(result) self.assertTrue( any(isinstance(f, LimitedCores) for f in self.validator.failures) ) self.mock_db.record_prereq.assert_called_with( "The kernel has been limited to 8 CPU cores, but the system has 16 cores", "❌", ) @patch("builtins.open", new_callable=mock_open) @patch("amd_debug.prerequisites.read_file") @patch("amd_debug.prerequisites.os.path.exists") def test_check_cpu_limited_cores_multi_ccd( self, mock_path_exists, mock_read_file, mock_file_open ): """Test check_cpu with multi-CCD system (tests socket-level counting)""" self.validator.cpu_family = 0x19 self.validator.cpu_model = 0x74 mock_read_file.return_value = "15" # kernel_max = 15 (16 cores) mock_path_exists.return_value = True # Simulate multi-CCD: iterate through levels to find socket # subleaf 0: level_type = 1 (thread) # subleaf 1: level_type = 2 (core) # subleaf 2: level_type = 3 (complex/CCD) # subleaf 3: level_type = 4 (socket) - FOUND mock_file_open.return_value.read.side_effect = [ struct.pack("4I", 0, 0, 0x100, 0), # subleaf 0: level_type = 1 (thread) struct.pack("4I", 0, 0, 0x200, 0), # subleaf 1: level_type = 2 (core) struct.pack( "4I", 0, 0, 0x300, 0 ), # subleaf 2: level_type = 3 (complex/CCD) struct.pack( "4I", 0, 0, 0x400, 0 ), # subleaf 3: level_type = 4 (socket) - FOUND struct.pack("4I", 0, 32, 0, 0), # subleaf 3: cpu_count = 32 ] result = self.validator.check_cpu() self.assertFalse(result) self.assertTrue( any(isinstance(f, LimitedCores) for f in self.validator.failures) ) self.mock_db.record_prereq.assert_called_with( "The kernel has been limited to 16 CPU cores, but the system has 32 cores", "❌", ) @patch("builtins.open", new_callable=mock_open) @patch("amd_debug.prerequisites.read_file") @patch("amd_debug.prerequisites.os.path.exists") def test_check_cpu_not_limited( self, mock_path_exists, mock_read_file, mock_file_open ): """Test check_cpu when CPUs are not artificially limited""" self.validator.cpu_family = 0x19 self.validator.cpu_model = 0x74 mock_read_file.return_value = "31" # kernel_max = 31 (32 cores) mock_path_exists.return_value = True # Simulate finding socket level mock_file_open.return_value.read.side_effect = [ struct.pack("4I", 0, 0, 0x100, 0), # subleaf 0: level_type = 1 struct.pack("4I", 0, 0, 0x400, 0), # subleaf 1: level_type = 4 (socket) struct.pack("4I", 0, 16, 0, 0), # subleaf 1: cpu_count = 16 ] result = self.validator.check_cpu() self.assertTrue(result) self.mock_db.record_debug.assert_called_with("CPU core count: 16 max: 32") @patch("builtins.open", new_callable=mock_open) @patch("amd_debug.prerequisites.read_file") @patch("amd_debug.prerequisites.os.path.exists") def test_check_cpu_socket_level_at_boundary( self, mock_path_exists, mock_read_file, mock_file_open ): """Test check_cpu when socket level is found at the last checked subleaf""" self.validator.cpu_family = 0x19 self.validator.cpu_model = 0x74 mock_read_file.return_value = "15" # kernel_max = 15 (16 cores) mock_path_exists.return_value = True # Socket level found at subleaf 4 (last iteration) mock_file_open.return_value.read.side_effect = [ struct.pack("4I", 0, 0, 0x100, 0), # subleaf 0: level_type = 1 struct.pack("4I", 0, 0, 0x200, 0), # subleaf 1: level_type = 2 struct.pack("4I", 0, 0, 0x300, 0), # subleaf 2: level_type = 3 struct.pack("4I", 0, 0, 0x000, 0), # subleaf 3: level_type = 0 struct.pack("4I", 0, 0, 0x400, 0), # subleaf 4: level_type = 4 (socket) struct.pack("4I", 0, 16, 0, 0), # subleaf 4: cpu_count = 16 ] result = self.validator.check_cpu() self.assertTrue(result) self.mock_db.record_prereq.assert_called_with( "Unable to discover CPU topology, didn't find socket level", "🚦" ) @patch("builtins.open", new_callable=mock_open) @patch("amd_debug.prerequisites.read_file") @patch("amd_debug.prerequisites.os.path.exists") def test_check_cpu_socket_level_first_subleaf( self, mock_path_exists, mock_read_file, mock_file_open ): """Test check_cpu when socket level is found at first subleaf""" self.validator.cpu_family = 0x19 self.validator.cpu_model = 0x74 mock_read_file.return_value = "7" # kernel_max = 7 (8 cores) mock_path_exists.return_value = True # Socket level found immediately at subleaf 0 mock_file_open.return_value.read.side_effect = [ struct.pack( "4I", 0, 0, 0x400, 0 ), # subleaf 0: level_type = 4 (socket) - FOUND struct.pack("4I", 0, 8, 0, 0), # subleaf 0: cpu_count = 8 ] result = self.validator.check_cpu() self.assertTrue(result) self.mock_db.record_debug.assert_called_with("CPU core count: 8 max: 8") @patch("builtins.open", side_effect=FileNotFoundError) @patch("amd_debug.prerequisites.read_file") @patch("amd_debug.prerequisites.os.path.exists") def test_check_cpu_cpuid_file_not_found( self, mock_path_exists, mock_read_file, mock_file_open ): """Test check_cpu when cpuid kernel module is not loaded""" self.validator.cpu_family = 0x19 self.validator.cpu_model = 0x74 mock_read_file.return_value = "7" mock_path_exists.return_value = False result = self.validator.check_cpu() self.assertFalse(result) self.mock_db.record_prereq.assert_called_with( "Unable to check CPU topology: cpuid kernel module not loaded", "❌" ) @patch("builtins.open", side_effect=PermissionError) @patch("amd_debug.prerequisites.read_file") @patch("amd_debug.prerequisites.os.path.exists") def test_check_cpu_cpuid_permission_error( self, mock_path_exists, mock_read_file, mock_file_open ): """Test check_cpu when there is a permission error accessing cpuid""" self.validator.cpu_family = 0x19 self.validator.cpu_model = 0x74 mock_read_file.return_value = "7" mock_path_exists.return_value = True result = self.validator.check_cpu() self.assertTrue(result) self.mock_db.record_prereq.assert_called_with("CPUID checks unavailable", "🚦") @patch("amd_debug.prerequisites.os.walk") @patch( "amd_debug.prerequisites.open", new_callable=unittest.mock.mock_open, read_data=b"mocked_data", ) @patch("amd_debug.prerequisites.tempfile.mkdtemp", return_value="/mocked/tempdir") @patch("amd_debug.prerequisites.subprocess.check_call") @patch("amd_debug.prerequisites.shutil.rmtree") def test_capture_acpi_success( self, mock_rmtree, mock_check_call, mock_mkdtemp, mock_open, mock_walk ): """Test capture_acpi when ACPI tables are successfully captured""" mock_walk.return_value = [ ("/sys/firmware/acpi/tables", [], ["SSDT1", "IVRS", "OTHER"]), ] result = self.validator.capture_acpi() self.assertTrue(result) mock_check_call.assert_called_with( [ "iasl", "-p", "/mocked/tempdir/acpi", "-d", "/sys/firmware/acpi/tables/IVRS", ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) self.mock_db.record_debug_file.assert_called_with("/mocked/tempdir/acpi.dsl") mock_rmtree.assert_called_with("/mocked/tempdir") @patch("amd_debug.prerequisites.os.walk") @patch( "amd_debug.prerequisites.open", new_callable=unittest.mock.mock_open, read_data=b"mocked_data", ) @patch("amd_debug.prerequisites.tempfile.mkdtemp", return_value="/mocked/tempdir") @patch( "amd_debug.prerequisites.subprocess.check_call", side_effect=subprocess.CalledProcessError(1, "iasl"), ) @patch("amd_debug.prerequisites.shutil.rmtree") def test_capture_acpi_subprocess_error( self, mock_rmtree, mock_check_call, mock_mkdtemp, mock_open, mock_walk ): """Test capture_acpi when subprocess.check_call raises an error""" mock_walk.return_value = [ ("/sys/firmware/acpi/tables", [], ["SSDT1", "IVRS", "OTHER"]), ] result = self.validator.capture_acpi() self.assertTrue(result) self.mock_db.record_prereq.assert_called_with( "Failed to capture ACPI table: None", "πŸ‘€" ) mock_rmtree.assert_called_with("/mocked/tempdir") @patch("amd_debug.prerequisites.os.walk") @patch( "amd_debug.prerequisites.open", new_callable=unittest.mock.mock_open, read_data=b"mocked_data", ) @patch("amd_debug.prerequisites.tempfile.mkdtemp", return_value="/mocked/tempdir") @patch("amd_debug.prerequisites.subprocess.check_call") @patch("amd_debug.prerequisites.shutil.rmtree") def test_capture_acpi_no_matching_files( self, mock_rmtree, mock_check_call, mock_mkdtemp, mock_open, mock_walk ): """Test capture_acpi when no matching ACPI tables are found""" mock_walk.return_value = [ ("/sys/firmware/acpi/tables", [], ["OTHER"]), ] result = self.validator.capture_acpi() self.assertTrue(result) mock_check_call.assert_not_called() self.mock_db.record_debug_file.assert_not_called() mock_rmtree.assert_not_called() @patch("amd_debug.prerequisites.os.path.exists") @patch("amd_debug.prerequisites.read_file") @patch("amd_debug.prerequisites.os.readlink") def test_map_acpi_path_with_devices( self, mock_readlink, mock_read_file, mock_path_exists ): """Test map_acpi_path with valid ACPI devices""" mock_path_exists.side_effect = lambda p: "path" in p or "driver" in p mock_read_file.side_effect = lambda p: "mocked_path" if "path" in p else "1" mock_readlink.return_value = "/mocked/driver" self.mock_pyudev.list_devices.return_value = [ MagicMock(sys_path="/sys/devices/acpi/device1", sys_name="device1"), MagicMock(sys_path="/sys/devices/acpi/device2", sys_name="device2"), ] result = self.validator.map_acpi_path() self.assertTrue(result) self.mock_db.record_debug.assert_called_with( "ACPI name | ACPI path | Kernel driver\ndevice1 | mocked_path | driver\ndevice2 | mocked_path | driver\n" ) @patch("amd_debug.prerequisites.os.path.exists") @patch("amd_debug.prerequisites.read_file") def test_map_acpi_path_no_devices(self, mock_read_file, mock_path_exists): """Test map_acpi_path with no valid ACPI devices""" mock_path_exists.return_value = False self.mock_pyudev.list_devices.return_value = [] result = self.validator.map_acpi_path() self.assertTrue(result) @patch("amd_debug.prerequisites.os.path.exists") @patch("amd_debug.prerequisites.read_file") def test_map_acpi_path_device_with_status_zero( self, mock_read_file, mock_path_exists ): """Test map_acpi_path when a device has status 0""" mock_path_exists.side_effect = lambda p: "path" in p or "status" in p mock_read_file.side_effect = lambda p: "mocked_path" if "path" in p else "0" self.mock_pyudev.list_devices.return_value = [ MagicMock(sys_path="/sys/devices/acpi/device1", sys_name="device1") ] result = self.validator.map_acpi_path() self.assertTrue(result) @patch("amd_debug.prerequisites.os.path.exists") @patch("amd_debug.prerequisites.read_file") def test_map_acpi_path_device_without_driver( self, mock_read_file, mock_path_exists ): """Test map_acpi_path when a device does not have a driver""" mock_path_exists.side_effect = lambda p: "path" in p mock_read_file.side_effect = lambda p: "mocked_path" self.mock_pyudev.list_devices.return_value = [ MagicMock(sys_path="/sys/devices/acpi/device1", sys_name="device1") ] result = self.validator.map_acpi_path() self.assertTrue(result) self.mock_db.record_debug.assert_called_with( "ACPI name | ACPI path | Kernel driver\ndevice1 | mocked_path | None\n" ) @patch("amd_debug.prerequisites.read_file") @patch("amd_debug.prerequisites.os.path.exists") def test_capture_pci_acpi_with_acpi_path(self, mock_path_exists, mock_read_file): """Test capture_pci_acpi when ACPI paths exist for devices""" mock_path_exists.side_effect = lambda p: "firmware_node/path" in p mock_read_file.side_effect = lambda p: "mocked_acpi_path" self.mock_pyudev.list_devices.return_value = [ MagicMock( properties={ "PCI_ID": "1234abcd", "PCI_SLOT_NAME": "0000:00:1f.0", "ID_PCI_SUBCLASS_FROM_DATABASE": "ISA bridge", "ID_VENDOR_FROM_DATABASE": "Intel Corporation", }, parent=MagicMock(subsystem="platform"), sys_path="/sys/devices/pci0000:00/0000:00:1f.0", ) ] self.validator.capture_pci_acpi() self.mock_db.record_debug.assert_called_with( "PCI Slot | Vendor | Class | ID | ACPI path\n└─0000:00:1f.0 | Intel Corporation | ISA bridge | 1234abcd | mocked_acpi_path\n" ) @patch("amd_debug.prerequisites.read_file") @patch("amd_debug.prerequisites.os.path.exists") def test_capture_pci_acpi_without_acpi_path(self, mock_path_exists, mock_read_file): """Test capture_pci_acpi when ACPI paths do not exist for devices""" mock_path_exists.return_value = False self.mock_pyudev.list_devices.return_value = [ MagicMock( properties={ "PCI_ID": "5678efgh", "PCI_SLOT_NAME": "0000:01:00.0", "ID_PCI_SUBCLASS_FROM_DATABASE": "VGA compatible controller", "ID_VENDOR_FROM_DATABASE": "NVIDIA Corporation", }, parent=MagicMock(subsystem="pci"), sys_path="/sys/devices/pci0000:01/0000:01:00.0", ) ] self.validator.capture_pci_acpi() self.mock_db.record_debug.assert_called_with( "PCI Slot | Vendor | Class | ID | ACPI path\n└─0000:01:00.0 | NVIDIA Corporation | VGA compatible controller | 5678efgh | \n" ) @patch("amd_debug.prerequisites.read_file") @patch("amd_debug.prerequisites.os.path.exists") def test_capture_pci_acpi_multiple_devices(self, mock_path_exists, mock_read_file): """Test capture_pci_acpi with multiple devices""" mock_path_exists.side_effect = lambda p: "firmware_node/path" in p mock_read_file.side_effect = lambda p: "mocked_acpi_path" if "path" in p else "" self.mock_pyudev.list_devices.return_value = [ MagicMock( properties={ "PCI_ID": "1234abcd", "PCI_SLOT_NAME": "0000:00:1f.0", "ID_PCI_SUBCLASS_FROM_DATABASE": "ISA bridge", "ID_VENDOR_FROM_DATABASE": "Intel Corporation", }, parent=MagicMock(subsystem="platform"), sys_path="/sys/devices/pci0000:00/0000:00:1f.0", ), MagicMock( properties={ "PCI_ID": "5678efgh", "PCI_SLOT_NAME": "0000:01:00.0", "ID_PCI_SUBCLASS_FROM_DATABASE": "VGA compatible controller", "ID_VENDOR_FROM_DATABASE": "NVIDIA Corporation", }, parent=MagicMock(subsystem="pci"), sys_path="/sys/devices/pci0000:01/0000:01:00.0", ), ] self.validator.capture_pci_acpi() self.mock_db.record_debug.assert_called_with( "PCI Slot | Vendor | Class | ID | ACPI path\nβ”‚ 0000:00:1f.0 | Intel Corporation | ISA bridge | 1234abcd | mocked_acpi_path\n└─0000:01:00.0 | NVIDIA Corporation | VGA compatible controller | 5678efgh | mocked_acpi_path\n" ) def test_capture_pci_acpi_no_devices(self): """Test capture_pci_acpi when no PCI devices are found""" self.mock_pyudev.list_devices.return_value = [] self.validator.capture_pci_acpi() self.mock_db.record_debug.assert_called_with( "PCI Slot | Vendor | Class | ID | ACPI path\n" ) @patch("amd_debug.prerequisites.read_file") def test_check_aspm_default_policy(self, mock_read_file): """Test check_aspm when the policy is set to default""" mock_read_file.return_value = "[default]" result = self.validator.check_aspm() self.assertTrue(result) self.mock_db.record_prereq.assert_called_with( "ASPM policy set to 'default'", "βœ…" ) @patch("amd_debug.prerequisites.read_file") def test_check_aspm_non_default_policy(self, mock_read_file): """Test check_aspm when the policy is not set to default""" mock_read_file.return_value = "[performance]" result = self.validator.check_aspm() self.assertFalse(result) self.mock_db.record_prereq.assert_called_with( "ASPM policy set to [performance]", "❌" ) self.assertTrue(any(isinstance(f, ASpmWrong) for f in self.validator.failures)) @patch("amd_debug.prerequisites.read_file") def test_check_aspm_empty_policy(self, mock_read_file): """Test check_aspm when the policy file is empty""" mock_read_file.return_value = "" result = self.validator.check_aspm() self.assertFalse(result) self.mock_db.record_prereq.assert_called_with("ASPM policy set to ", "❌") self.assertTrue(any(isinstance(f, ASpmWrong) for f in self.validator.failures)) @patch("amd_debug.prerequisites.read_file") def test_check_aspm_file_not_found(self, mock_read_file): """Test check_aspm when the policy file is not found""" mock_read_file.side_effect = FileNotFoundError with self.assertRaises(FileNotFoundError): self.validator.check_aspm() @patch("amd_debug.prerequisites.os.path.exists") @patch("amd_debug.prerequisites.read_file") def test_check_i2c_hid_no_devices(self, mock_read_file, mock_path_exists): """Test check_i2c_hid when no I2C HID devices are found""" self.mock_pyudev.list_devices.return_value = [] result = self.validator.check_i2c_hid() self.assertTrue(result) @patch("amd_debug.prerequisites.os.path.exists") @patch("amd_debug.prerequisites.read_file") def test_check_i2c_hid_with_devices(self, mock_read_file, mock_path_exists): """Test check_i2c_hid when I2C HID devices are found""" mock_path_exists.side_effect = ( lambda p: "firmware_node/path" in p or "firmware_node/hid" in p ) mock_read_file.side_effect = lambda p: ( "mocked_path" if "path" in p else "mocked_hid" ) self.mock_pyudev.list_devices.return_value = [ MagicMock( properties={"NAME": "I2C Device 1"}, find_parent=MagicMock( return_value=MagicMock(sys_path="/sys/devices/i2c-1") ), ), MagicMock( properties={"NAME": "I2C Device 2"}, find_parent=MagicMock( return_value=MagicMock(sys_path="/sys/devices/i2c-2") ), ), ] result = self.validator.check_i2c_hid() self.assertTrue(result) self.mock_db.record_debug.assert_called_with( "I2C HID devices:\n" "β”‚ I2C Device 1 [mocked_hid] : mocked_path\n" "└─I2C Device 2 [mocked_hid] : mocked_path\n" ) @patch("amd_debug.prerequisites.os.path.exists") @patch("amd_debug.prerequisites.read_file") def test_check_i2c_hid_with_buggy_device(self, mock_read_file, mock_path_exists): """Test check_i2c_hid when a buggy I2C HID device is found""" mock_path_exists.side_effect = ( lambda p: "firmware_node/path" in p or "firmware_node/hid" in p ) mock_read_file.side_effect = lambda p: ( "mocked_path" if "path" in p else "mocked_hid" ) self.mock_pyudev.list_devices.return_value = [ MagicMock( properties={"NAME": "IDEA5002"}, find_parent=MagicMock( return_value=MagicMock( sys_path="/sys/devices/i2c-1", driver="mock_driver" ) ), ) ] result = self.validator.check_i2c_hid() self.assertFalse(result) self.mock_db.record_prereq.assert_called_with( "IDEA5002 may cause spurious wakeups", "❌" ) self.assertTrue(any(isinstance(f, I2CHidBug) for f in self.validator.failures)) @patch("amd_debug.prerequisites.os.path.exists") @patch("amd_debug.prerequisites.read_file") def test_check_i2c_hid_missing_firmware_node( self, mock_read_file, mock_path_exists ): """Test check_i2c_hid when firmware_node paths are missing""" mock_path_exists.return_value = False self.mock_pyudev.list_devices.return_value = [ MagicMock( properties={"NAME": "I2C Device 1"}, find_parent=MagicMock( return_value=MagicMock(sys_path="/sys/devices/i2c-1") ), ) ] result = self.validator.check_i2c_hid() self.assertTrue(result) self.mock_db.record_debug.assert_called_with( "I2C HID devices:\n└─I2C Device 1 [] : \n" ) @patch("amd_debug.prerequisites.os.path.exists", return_value=True) @patch( "amd_debug.prerequisites.open", new_callable=unittest.mock.mock_open, read_data=b"\x00" * 0x70 + b"\x20\x00\x00\x00", ) @patch("amd_debug.prerequisites.struct.unpack", return_value=(0x00200000,)) def test_check_fadt_supports_low_power_idle( self, mock_unpack, mock_open, mock_path_exists ): """Test check_fadt when ACPI FADT supports Low-power S0 idle""" self.mock_kernel_log.match_line.return_value = False result = self.validator.check_fadt() self.assertTrue(result) self.mock_db.record_prereq.assert_called_with( "ACPI FADT supports Low-power S0 idle", "βœ…" ) @patch("amd_debug.prerequisites.os.path.exists", return_value=True) @patch( "amd_debug.prerequisites.open", new_callable=unittest.mock.mock_open, read_data=b"\x00" * 0x70 + b"\x00\x00\x00\x00", ) @patch("amd_debug.prerequisites.struct.unpack", return_value=(0x00000000,)) def test_check_fadt_does_not_support_low_power_idle( self, mock_unpack, mock_open, mock_path_exists ): """Test check_fadt when ACPI FADT does not support Low-power S0 idle""" self.mock_kernel_log.match_line.return_value = False result = self.validator.check_fadt() self.assertFalse(result) self.assertTrue(any(isinstance(f, FadtWrong) for f in self.validator.failures)) def test_check_fadt_file_not_found(self): """Test check_fadt when FADT file is not found""" self.mock_kernel_log.match_line.return_value = False result = self.validator.check_fadt() self.assertTrue(result) self.mock_db.record_prereq.assert_called_with("FADT check unavailable", "🚦") @patch("amd_debug.prerequisites.os.path.exists", return_value=True) @patch("amd_debug.prerequisites.open", side_effect=PermissionError) def test_check_fadt_permission_error(self, mock_open, mock_path_exists): """Test check_fadt when there is a permission error accessing the FADT file""" self.mock_kernel_log.match_line.return_value = False result = self.validator.check_fadt() self.assertTrue(result) self.mock_db.record_prereq.assert_called_with("FADT check unavailable", "🚦") def test_check_fadt_kernel_log_match(self): """Test check_fadt when kernel log contains the required message""" self.mock_kernel_log.match_line.return_value = True result = self.validator.check_fadt() self.assertTrue(result) self.mock_db.record_prereq.assert_called_with( "ACPI FADT supports Low-power S0 idle", "βœ…" ) @patch( "amd_debug.prerequisites.open", new_callable=unittest.mock.mock_open, read_data=b"\x00" * 0x70 + b"\x00\x00\x00\x00", ) def test_check_fadt_no_kernel_log(self, _mock_open): """Test check_fadt when kernel log is not available""" self.validator.kernel_log = None result = self.validator.check_fadt() self.assertFalse(result) self.mock_db.record_prereq.assert_called_with( "ACPI FADT doesn't support Low-power S0 idle", "❌" ) @patch("amd_debug.prerequisites.read_file") def test_get_cpu_vendor_all_fields_present(self, mock_read_file): """Test get_cpu_vendor when all fields are present in /proc/cpuinfo""" mock_read_file.return_value = ( "vendor_id\t: AuthenticAMD\n" "cpu family\t: 23\n" "model\t\t: 1\n" "model name\t: AMD Ryzen 7 3700X\n" ) vendor = self.validator.get_cpu_vendor() self.assertEqual(vendor, "AuthenticAMD") self.assertEqual(self.validator.cpu_family, 23) self.assertEqual(self.validator.cpu_model, 1) self.assertEqual(self.validator.cpu_model_string, "AMD Ryzen 7 3700X") self.mock_db.record_prereq.assert_called_with( "AMD Ryzen 7 3700X (family 17 model 1)", "πŸ’»" ) @patch("amd_debug.prerequisites.read_file") def test_get_cpu_vendor_missing_model_name(self, mock_read_file): """Test get_cpu_vendor when model name is missing in /proc/cpuinfo""" mock_read_file.return_value = ( "vendor_id\t: AuthenticAMD\n" "cpu family\t: 23\n" "model\t\t: 1\n" ) vendor = self.validator.get_cpu_vendor() self.assertEqual(vendor, "AuthenticAMD") self.assertEqual(self.validator.cpu_family, 23) self.assertEqual(self.validator.cpu_model, 1) self.assertIsNone(self.validator.cpu_model_string) self.mock_db.record_prereq.assert_not_called() @patch("amd_debug.prerequisites.read_file") def test_get_cpu_vendor_missing_vendor_id(self, mock_read_file): """Test get_cpu_vendor when vendor_id is missing in /proc/cpuinfo""" mock_read_file.return_value = ( "cpu family\t: 23\n" "model\t\t: 1\n" "model name\t: AMD Ryzen 7 3700X\n" ) vendor = self.validator.get_cpu_vendor() self.assertEqual(vendor, "") self.assertEqual(self.validator.cpu_family, 23) self.assertEqual(self.validator.cpu_model, 1) self.assertEqual(self.validator.cpu_model_string, "AMD Ryzen 7 3700X") self.mock_db.record_prereq.assert_called_with( "AMD Ryzen 7 3700X (family 17 model 1)", "πŸ’»" ) @patch("amd_debug.prerequisites.read_file") def test_get_cpu_vendor_missing_cpu_family(self, mock_read_file): """Test get_cpu_vendor when cpu family is missing in /proc/cpuinfo""" mock_read_file.return_value = ( "vendor_id\t: AuthenticAMD\n" "model\t\t: 1\n" "model name\t: AMD Ryzen 7 3700X\n" ) vendor = self.validator.get_cpu_vendor() self.assertEqual(vendor, "AuthenticAMD") self.assertIsNone(self.validator.cpu_family) self.assertEqual(self.validator.cpu_model, 1) self.assertEqual(self.validator.cpu_model_string, "AMD Ryzen 7 3700X") self.mock_db.record_prereq.assert_not_called() @patch("amd_debug.prerequisites.read_file") def test_get_cpu_vendor_missing_model(self, mock_read_file): """Test get_cpu_vendor when model is missing in /proc/cpuinfo""" mock_read_file.return_value = ( "vendor_id\t: AuthenticAMD\n" "cpu family\t: 23\n" "model name\t: AMD Ryzen 7 3700X\n" ) vendor = self.validator.get_cpu_vendor() self.assertEqual(vendor, "AuthenticAMD") self.assertEqual(self.validator.cpu_family, 23) self.assertIsNone(self.validator.cpu_model) self.assertEqual(self.validator.cpu_model_string, "AMD Ryzen 7 3700X") self.mock_db.record_prereq.assert_not_called() @patch("amd_debug.prerequisites.read_file") def test_get_cpu_vendor_empty_cpuinfo(self, mock_read_file): """Test get_cpu_vendor when /proc/cpuinfo is empty""" mock_read_file.return_value = "" vendor = self.validator.get_cpu_vendor() self.assertEqual(vendor, "") self.assertIsNone(self.validator.cpu_family) self.assertIsNone(self.validator.cpu_model) self.assertIsNone(self.validator.cpu_model_string) self.mock_db.record_prereq.assert_not_called() def test_check_usb4_driver_missing(self): """Test check_usb4 when the thunderbolt driver is missing""" self.mock_pyudev.list_devices.return_value = [ MagicMock( properties={ "PCI_SLOT_NAME": "0000:00:1d.0", "DRIVER": None, } ) ] result = self.validator.check_usb4() self.assertFalse(result) self.assertTrue( any(isinstance(f, MissingThunderbolt) for f in self.validator.failures) ) self.mock_db.record_prereq.assert_called_with( "USB4 driver `thunderbolt` missing", "❌" ) def test_check_usb4_driver_present(self): """Test check_usb4 when the thunderbolt driver is present""" self.mock_pyudev.list_devices.return_value = [ MagicMock( properties={ "PCI_SLOT_NAME": "0000:00:1d.0", "DRIVER": "thunderbolt", } ) ] result = self.validator.check_usb4() self.assertTrue(result) self.mock_db.record_prereq.assert_called_with( "USB4 driver `thunderbolt` bound to 0000:00:1d.0", "βœ…" ) def test_check_usb4_no_devices(self): """Test check_usb4 when no USB4 devices are found""" self.mock_pyudev.list_devices.return_value = [] result = self.validator.check_usb4() self.assertTrue(result) self.mock_db.record_prereq.assert_not_called() @patch("amd_debug.prerequisites.os.path.exists", return_value=False) def test_capture_smbios_not_setup(self, mock_path_exists): """Test capture_smbios when DMI data is not set up""" self.validator.capture_smbios() self.mock_db.record_prereq.assert_called_with("DMI data was not setup", "🚦") self.assertTrue( any(isinstance(f, DmiNotSetup) for f in self.validator.failures) ) @patch("amd_debug.prerequisites.os.walk") @patch("amd_debug.prerequisites.read_file") @patch("amd_debug.prerequisites.os.path.exists", return_value=True) def test_capture_smbios_success( self, mock_path_exists, mock_read_file, mock_os_walk ): """Test capture_smbios when DMI data is successfully captured""" mock_os_walk.return_value = [ ( "/sys/class/dmi/id", [], ["sys_vendor", "product_name", "product_family", "chassis_type"], ) ] mock_read_file.side_effect = lambda path: { "/sys/class/dmi/id/sys_vendor": "MockVendor", "/sys/class/dmi/id/product_name": "MockProduct", "/sys/class/dmi/id/product_family": "MockFamily", "/sys/class/dmi/id/chassis_type": "Desktop", }.get(path, "") result = self.validator.capture_smbios() self.assertTrue(result) self.mock_db.record_prereq.assert_called_with( "MockVendor MockProduct (MockFamily)", "πŸ’»" ) self.mock_db.record_debug.assert_called_with( "DMI|value\nchassis_type| Desktop\n" ) @patch("amd_debug.prerequisites.os.walk") @patch("amd_debug.prerequisites.read_file") @patch("amd_debug.prerequisites.os.path.exists", return_value=True) def test_capture_smbios_filtered_keys( self, _mock_path_exists, mock_read_file, mock_os_walk ): """Test capture_smbios when filtered keys are present""" mock_os_walk.return_value = [ ( "/sys/class/dmi/id", [], ["sys_vendor", "product_name", "product_family", "product_serial"], ) ] mock_read_file.side_effect = lambda path: { "/sys/class/dmi/id/sys_vendor": "MockVendor", "/sys/class/dmi/id/product_name": "MockProduct", "/sys/class/dmi/id/product_family": "MockFamily", "/sys/class/dmi/id/product_serial": "12345", }.get(path, "") result = self.validator.capture_smbios() self.assertTrue(result) self.mock_db.record_prereq.assert_called_with( "MockVendor MockProduct (MockFamily)", "πŸ’»" ) self.mock_db.record_debug.assert_called_with("DMI|value\n") @patch("amd_debug.prerequisites.os.walk") @patch("amd_debug.prerequisites.read_file") @patch("amd_debug.prerequisites.os.path.exists", return_value=True) def test_capture_smbios_missing_keys( self, _mock_path_exists, mock_read_file, mock_os_walk ): """Test capture_smbios when required keys are missing""" mock_os_walk.return_value = [("/sys/class/dmi/id", [], ["chassis_type"])] mock_read_file.side_effect = lambda path: { "/sys/class/dmi/id/chassis_type": "Desktop", }.get(path, "") result = self.validator.capture_smbios() self.assertTrue( any(isinstance(f, DmiNotSetup) for f in self.validator.failures) ) self.assertFalse(result) @patch("amd_debug.prerequisites.os.path.exists") @patch("amd_debug.prerequisites.read_file") def test_check_lps0_enabled(self, mock_read_file, mock_path_exists): """Test check_lps0 when LPS0 is enabled""" mock_path_exists.return_value = True mock_read_file.return_value = "N" result = self.validator.check_lps0() self.assertTrue(result) self.mock_db.record_prereq.assert_called_with("LPS0 _DSM enabled", "βœ…") @patch("amd_debug.prerequisites.os.path.exists") @patch("amd_debug.prerequisites.read_file") def test_check_lps0_disabled(self, mock_read_file, mock_path_exists): """Test check_lps0 when LPS0 is disabled""" mock_path_exists.return_value = True mock_read_file.return_value = "Y" result = self.validator.check_lps0() self.assertFalse(result) self.mock_db.record_prereq.assert_called_with("LPS0 _DSM disabled", "❌") @patch("amd_debug.prerequisites.os.path.exists") def test_check_lps0_not_found(self, mock_path_exists): """Test check_lps0 when LPS0 parameter is not found""" mock_path_exists.return_value = False result = self.validator.check_lps0() self.assertFalse(result) self.mock_db.record_prereq.assert_called_with("LPS0 _DSM not found", "πŸ‘€") @patch("amd_debug.prerequisites.os.path.exists") @patch( "amd_debug.prerequisites.open", new_callable=unittest.mock.mock_open, read_data="ignore_wake_value", ) def test_capture_disabled_pins_with_parameters(self, _mock_open, mock_path_exists): """Test capture_disabled_pins when parameters are present and configured""" mock_path_exists.side_effect = ( lambda path: "ignore_wake" in path or "ignore_interrupt" in path ) self.validator.capture_disabled_pins() self.mock_db.record_debug.assert_called_with( "Disabled pins:\n/sys/module/gpiolib_acpi/parameters/ignore_wake is configured to ignore_wake_value\n/sys/module/gpiolib_acpi/parameters/ignore_interrupt is configured to ignore_wake_value\n" ) @patch("amd_debug.prerequisites.os.path.exists") @patch( "amd_debug.prerequisites.open", new_callable=unittest.mock.mock_open, read_data="(null)", ) def test_capture_disabled_pins_with_null_values(self, _mock_open, mock_path_exists): mock_path_exists.side_effect = ( lambda path: "ignore_wake" in path or "ignore_interrupt" in path ) self.validator.capture_disabled_pins() self.mock_db.record_debug.assert_not_called() @patch("amd_debug.prerequisites.os.path.exists", return_value=False) def test_capture_disabled_pins_no_parameters(self, _mock_path_exists): """Test capture_disabled_pins when parameters are not present""" self.validator.capture_disabled_pins() self.mock_db.record_debug.assert_not_called() @patch("amd_debug.prerequisites.os.listdir") @patch("amd_debug.prerequisites.os.path.isdir") @patch("amd_debug.prerequisites.WakeIRQ") def test_capture_irq_with_irqs(self, MockWakeIRQ, mock_isdir, mock_listdir): """Test capture_irq when IRQ directories are present""" mock_listdir.return_value = ["1", "2", "3"] mock_isdir.side_effect = lambda path: path.endswith(("1", "2", "3")) MockWakeIRQ.side_effect = lambda irq, pyudev: f"WakeIRQ-{irq}" result = self.validator.capture_irq() self.assertTrue(result) self.assertEqual( self.validator.irqs, [[1, "WakeIRQ-1"], [2, "WakeIRQ-2"], [3, "WakeIRQ-3"]], ) self.mock_db.record_debug.assert_any_call("Interrupts") self.mock_db.record_debug.assert_any_call("β”‚ 1: WakeIRQ-1") self.mock_db.record_debug.assert_any_call("β”‚ 2: WakeIRQ-2") self.mock_db.record_debug.assert_any_call("└─3: WakeIRQ-3") @patch("amd_debug.prerequisites.os.listdir") @patch("amd_debug.prerequisites.os.path.isdir") def test_capture_irq_no_irqs(self, mock_isdir, mock_listdir): """Test capture_irq when no IRQ directories are present""" mock_listdir.return_value = [] mock_isdir.return_value = False result = self.validator.capture_irq() self.assertTrue(result) self.assertEqual(self.validator.irqs, []) self.mock_db.record_debug.assert_called_with("Interrupts") @patch("amd_debug.prerequisites.os.listdir") @patch("amd_debug.prerequisites.os.path.isdir") @patch("amd_debug.prerequisites.WakeIRQ") def test_capture_irq_mixed_entries(self, mock_wake_irq, mock_isdir, mock_listdir): """Test capture_irq with mixed valid and invalid IRQ directories""" mock_listdir.return_value = ["1", "invalid", "2"] mock_isdir.side_effect = lambda path: path.endswith(("1", "2")) mock_wake_irq.side_effect = lambda irq, pyudev: f"WakeIRQ-{irq}" result = self.validator.capture_irq() self.assertTrue(result) self.assertEqual( self.validator.irqs, [[1, "WakeIRQ-1"], [2, "WakeIRQ-2"]], ) self.mock_db.record_debug.assert_any_call("Interrupts") self.mock_db.record_debug.assert_any_call("β”‚ 1: WakeIRQ-1") self.mock_db.record_debug.assert_any_call("└─2: WakeIRQ-2") @patch("amd_debug.prerequisites.os.path.exists", return_value=True) @patch("builtins.open", new_callable=unittest.mock.mock_open) def test_check_permissions_success(self, _mock_open, _mock_path_exists): """Test check_permissions when the user has write permissions""" result = self.validator.check_permissions() self.assertTrue(result) self.mock_db.record_prereq.assert_not_called() @patch("amd_debug.prerequisites.os.path.exists", return_value=True) @patch("builtins.open", side_effect=PermissionError) def test_check_permissions_permission_error(self, _mock_open, _mock_path_exists): """Test check_permissions when the user lacks write permissions""" result = self.validator.check_permissions() self.assertFalse(result) @patch("amd_debug.prerequisites.os.path.exists", return_value=False) @patch("builtins.open", side_effect=FileNotFoundError) def test_check_permissions_file_not_found(self, _mock_open, _mock_path_exists): """Test check_permissions when the /sys/power/state file is not found""" result = self.validator.check_permissions() self.assertFalse(result) self.mock_db.record_prereq.assert_called_with( "Kernel doesn't support power management", "❌" ) @patch("amd_debug.prerequisites.read_file") @patch("amd_debug.prerequisites.os.path.exists", return_value=True) def test_check_pinctrl_amd_driver_loaded(self, mock_path_exists, mock_read_file): """Test check_pinctrl_amd when the driver is loaded and debug information is available""" mock_read_file.return_value = ( "trigger\n" "edge\n" "level\n" "WAKE_INT_MASTER_REG: 8000\n" ) self.mock_pyudev.list_devices.return_value = [ MagicMock(properties={"DRIVER": "amd_gpio"}) ] result = self.validator.check_pinctrl_amd() self.assertTrue(result) self.mock_db.record_prereq.assert_called_with( "GPIO driver `pinctrl_amd` available", "βœ…" ) self.mock_db.record_debug.assert_called_with("trigger\nedge\nlevel\n") @patch("amd_debug.prerequisites.read_file") @patch("amd_debug.prerequisites.os.path.exists", return_value=True) def test_check_pinctrl_amd_driver_loaded_with_permission_error( self, mock_path_exists, mock_read_file ): """Test check_pinctrl_amd when the driver is loaded but debug file cannot be read due to permission error""" mock_read_file.side_effect = PermissionError self.mock_pyudev.list_devices.return_value = [ MagicMock(properties={"DRIVER": "amd_gpio"}) ] result = self.validator.check_pinctrl_amd() self.assertTrue(result) self.mock_db.record_prereq.assert_called_with( "GPIO driver `pinctrl_amd` available", "βœ…" ) self.mock_db.record_debug.assert_called_with( "Unable to capture /sys/kernel/debug/gpio" ) @patch("amd_debug.prerequisites.read_file") @patch("amd_debug.prerequisites.os.path.exists", return_value=True) def test_check_pinctrl_amd_unserviced_gpio(self, _mock_path_exists, mock_read_file): """Test check_pinctrl_amd when unserviced GPIO is detected""" mock_read_file.return_value = "πŸ”₯" self.mock_pyudev.list_devices.return_value = [ MagicMock(properties={"DRIVER": "amd_gpio"}) ] result = self.validator.check_pinctrl_amd() self.assertFalse(result) self.assertTrue( any(isinstance(f, UnservicedGpio) for f in self.validator.failures) ) @patch("amd_debug.prerequisites.read_file") @patch("amd_debug.prerequisites.os.path.exists", return_value=True) def test_check_pinctrl_amd_no_debug_info(self, mock_path_exists, mock_read_file): """Test check_pinctrl_amd when the driver is loaded but no debug information is available""" mock_read_file.return_value = "" self.mock_pyudev.list_devices.return_value = [ MagicMock(properties={"DRIVER": "amd_gpio"}) ] result = self.validator.check_pinctrl_amd() self.assertTrue(result) self.mock_db.record_prereq.assert_called_with( "GPIO driver `pinctrl_amd` available", "βœ…" ) self.mock_db.record_debug.assert_not_called() def test_check_pinctrl_amd_driver_not_loaded(self): """Test check_pinctrl_amd when the driver is not loaded""" self.mock_pyudev.list_devices.return_value = [] result = self.validator.check_pinctrl_amd() self.assertFalse(result) self.mock_db.record_prereq.assert_called_with( "GPIO driver `pinctrl_amd` not loaded", "❌" ) @patch("amd_debug.prerequisites.os.path.exists") @patch("amd_debug.prerequisites.read_file") def test_check_asus_rog_ally_mcu_version_too_old( self, mock_read_file, mock_path_exists ): """Test check_asus_rog_ally when MCU version is too old""" mock_path_exists.side_effect = lambda p: "mcu_version" in p mock_read_file.side_effect = lambda p: "318" if "mcu_version" in p else "" self.mock_pyudev.list_devices.side_effect = [ [MagicMock(sys_path="/sys/devices/hid1", properties={"HID_ID": "1ABE"})], [], ] result = self.validator.check_asus_rog_ally() self.assertFalse(result) self.mock_db.record_prereq.assert_called_with( "ROG Ally MCU firmware too old", "❌" ) self.assertTrue( any(isinstance(f, RogAllyOldMcu) for f in self.validator.failures) ) @patch("amd_debug.prerequisites.os.path.exists") @patch("amd_debug.prerequisites.read_file") def test_check_asus_rog_ally_mcu_version_valid( self, mock_read_file, mock_path_exists ): """Test check_asus_rog_ally when MCU version is valid""" mock_path_exists.side_effect = lambda p: "mcu_version" in p mock_read_file.side_effect = lambda p: "320" if "mcu_version" in p else "" self.mock_pyudev.list_devices.side_effect = [ [MagicMock(sys_path="/sys/devices/hid1", properties={"HID_ID": "1ABE"})], [], ] result = self.validator.check_asus_rog_ally() self.assertTrue(result) @patch("amd_debug.prerequisites.os.path.exists") @patch("amd_debug.prerequisites.read_file") def test_check_asus_rog_ally_mcu_powersave_disabled( self, mock_read_file, mock_path_exists ): """Test check_asus_rog_ally when MCU powersave is disabled""" mock_path_exists.side_effect = lambda p: "current_value" in p mock_read_file.side_effect = lambda p: "0" if "current_value" in p else "" self.mock_pyudev.list_devices.side_effect = [ [], [MagicMock(sys_path="/sys/devices/firmware1")], ] result = self.validator.check_asus_rog_ally() self.assertFalse(result) self.mock_db.record_prereq.assert_called_with( "Rog Ally doesn't have MCU powersave enabled", "❌" ) self.assertTrue( any(isinstance(f, RogAllyMcuPowerSave) for f in self.validator.failures) ) @patch("amd_debug.prerequisites.os.path.exists") @patch("amd_debug.prerequisites.read_file") def test_check_asus_rog_ally_mcu_powersave_enabled( self, mock_read_file, mock_path_exists ): """Test check_asus_rog_ally when MCU powersave is enabled""" mock_path_exists.side_effect = lambda p: "current_value" in p mock_read_file.side_effect = lambda p: "1" if "current_value" in p else "" self.mock_pyudev.list_devices.side_effect = [ [], [MagicMock(sys_path="/sys/devices/firmware1")], ] result = self.validator.check_asus_rog_ally() self.assertTrue(result) @patch("amd_debug.prerequisites.os.path.exists") @patch("amd_debug.prerequisites.read_file") def test_check_asus_rog_ally_no_devices(self, _mock_read_file, mock_path_exists): """Test check_asus_rog_ally when no devices are found""" mock_path_exists.return_value = False self.mock_pyudev.list_devices.side_effect = [[], []] result = self.validator.check_asus_rog_ally() self.assertTrue(result) @patch("amd_debug.prerequisites.subprocess.check_output") def test_check_network_wol_supported_and_enabled(self, mock_check_output): """Test check_network when WoL is supported and enabled""" self.mock_pyudev.list_devices.return_value = [ MagicMock(properties={"INTERFACE": "eth0"}) ] mock_check_output.return_value = ( "Supports Wake-on: g\n" "Wake-on: g\n" ).encode("utf-8") result = self.validator.check_network() self.assertTrue(result) self.mock_db.record_debug.assert_called_with("eth0 supports WoL") self.mock_db.record_prereq.assert_called_with("eth0 has WoL enabled", "βœ…") @patch("amd_debug.prerequisites.subprocess.check_output") def test_check_network_wol_supported_but_disabled(self, mock_check_output): """Test check_network when WoL is supported but disabled""" self.mock_pyudev.list_devices.return_value = [ MagicMock(properties={"INTERFACE": "eth0"}) ] mock_check_output.return_value = ( "Supports Wake-on: g\n" "Wake-on: d\n" ).encode("utf-8") result = self.validator.check_network() self.assertTrue(result) self.mock_db.record_debug.assert_called_with("eth0 supports WoL") self.mock_db.record_prereq.assert_called_with( "Platform may have low hardware sleep residency with Wake-on-lan disabled. Run `ethtool -s eth0 wol g` to enable it if necessary.", "🚦", ) @patch("amd_debug.prerequisites.subprocess.check_output") def test_check_network_wol_not_supported(self, mock_check_output): """Test check_network when WoL is not supported""" self.mock_pyudev.list_devices.return_value = [ MagicMock(properties={"INTERFACE": "eth0"}) ] mock_check_output.return_value = ("Supports Wake-on: d\n").encode("utf-8") result = self.validator.check_network() self.assertTrue(result) self.mock_db.record_debug.assert_called_with("eth0 doesn't support WoL (d)") @patch("amd_debug.prerequisites.subprocess.check_output") def test_check_network_no_devices(self, mock_check_output): """Test check_network when no network devices are found""" self.mock_pyudev.list_devices.return_value = [] result = self.validator.check_network() self.assertTrue(result) self.mock_db.record_debug.assert_not_called() self.mock_db.record_prereq.assert_not_called() @patch( "amd_debug.prerequisites.subprocess.check_output", side_effect=subprocess.CalledProcessError(1, "ethtool"), ) def test_check_network_ethtool_error(self, mock_check_output): """Test check_network when ethtool command fails""" self.mock_pyudev.list_devices.return_value = [ MagicMock(properties={"INTERFACE": "eth0"}) ] with self.assertRaises(subprocess.CalledProcessError): self.validator.check_network() self.mock_db.record_debug.assert_not_called() self.mock_db.record_prereq.assert_not_called() @patch("amd_debug.prerequisites.version.parse") def test_check_amd_cpu_hpet_wa_family_17_model_68(self, mock_version_parse): """Test check_amd_cpu_hpet_wa for family 0x17, model 0x68""" self.validator.cpu_family = 0x17 self.validator.cpu_model = 0x68 result = self.validator.check_amd_cpu_hpet_wa() self.assertTrue(result) self.mock_db.record_prereq.assert_called_with( "Timer based wakeup doesn't work properly for your ASIC/firmware, please manually wake the system", "🚦", ) @patch("amd_debug.prerequisites.version.parse") def test_check_amd_cpu_hpet_wa_family_17_model_60(self, mock_version_parse): """Test check_amd_cpu_hpet_wa for family 0x17, model 0x60""" self.validator.cpu_family = 0x17 self.validator.cpu_model = 0x60 result = self.validator.check_amd_cpu_hpet_wa() self.assertTrue(result) self.mock_db.record_prereq.assert_called_with( "Timer based wakeup doesn't work properly for your ASIC/firmware, please manually wake the system", "🚦", ) @patch("amd_debug.prerequisites.version.parse") def test_check_amd_cpu_hpet_wa_family_19_model_50_smu_version_low( self, mock_version_parse ): """Test check_amd_cpu_hpet_wa for family 0x19, model 0x50 with SMU version < 64.53.0""" self.validator.cpu_family = 0x19 self.validator.cpu_model = 0x50 self.validator.smu_version = "64.52.0" mock_version_parse.side_effect = lambda v: v if isinstance(v, str) else None result = self.validator.check_amd_cpu_hpet_wa() self.assertTrue(result) self.mock_db.record_prereq.assert_called_with( "Timer based wakeup doesn't work properly for your ASIC/firmware, please manually wake the system", "🚦", ) @patch("amd_debug.prerequisites.version.parse") def test_check_amd_cpu_hpet_wa_family_19_model_50_smu_version_high( self, mock_version_parse ): """Test check_amd_cpu_hpet_wa for family 0x19, model 0x50 with SMU version >= 64.53.0""" self.validator.cpu_family = 0x19 self.validator.cpu_model = 0x50 self.validator.smu_version = "64.53.0" mock_version_parse.side_effect = lambda v: v if isinstance(v, str) else None result = self.validator.check_amd_cpu_hpet_wa() self.assertTrue(result) self.mock_db.record_prereq.assert_not_called() def test_check_amd_cpu_hpet_wa_family_19_non_matching_model(self): """Test check_amd_cpu_hpet_wa for family 0x19 with non-matching model""" self.validator.cpu_family = 0x19 self.validator.cpu_model = 0x51 result = self.validator.check_amd_cpu_hpet_wa() self.assertTrue(result) self.mock_db.record_prereq.assert_not_called() def test_check_amd_cpu_hpet_wa_non_matching_family(self): """Test check_amd_cpu_hpet_wa for non-matching CPU family""" self.validator.cpu_family = 0x18 self.validator.cpu_model = 0x68 result = self.validator.check_amd_cpu_hpet_wa() self.assertTrue(result) self.mock_db.record_prereq.assert_not_called() @patch("amd_debug.prerequisites.os.path.exists") def test_capture_linux_firmware_debug_files_exist(self, mock_path_exists): """Test capture_linux_firmware when debug files exist""" mock_path_exists.side_effect = lambda path: "amdgpu_firmware_info" in path self.validator.distro = "ubuntu" self.validator.capture_linux_firmware() self.mock_db.record_debug_file.assert_any_call( "/sys/kernel/debug/dri/0/amdgpu_firmware_info" ) self.mock_db.record_debug_file.assert_any_call( "/sys/kernel/debug/dri/1/amdgpu_firmware_info" ) @patch("amd_debug.prerequisites.os.path.exists") def test_capture_linux_firmware_debug_files_missing(self, mock_path_exists): """Test capture_linux_firmware when debug files are missing""" mock_path_exists.return_value = False self.validator.distro = "ubuntu" self.validator.capture_linux_firmware() self.mock_db.record_debug_file.assert_not_called() def test_check_wlan_no_devices(self): """Test check_wlan when no WLAN devices are found""" self.mock_pyudev.list_devices.return_value = [] result = self.validator.check_wlan() self.assertTrue(result) self.mock_db.record_prereq.assert_not_called() def test_check_wlan_missing_driver(self): """Test check_wlan when a WLAN device is missing a driver""" self.mock_pyudev.list_devices.return_value = [ MagicMock(properties={"PCI_SLOT_NAME": "0000:00:1f.0", "DRIVER": None}) ] result = self.validator.check_wlan() self.assertFalse(result) self.mock_db.record_prereq.assert_called_with( "WLAN device in 0000:00:1f.0 missing driver", "🚦" ) self.assertTrue( any(isinstance(f, MissingDriver) for f in self.validator.failures) ) def test_check_wlan_with_driver(self): """Test check_wlan when a WLAN device has a driver""" self.mock_pyudev.list_devices.return_value = [ MagicMock(properties={"PCI_SLOT_NAME": "0000:00:1f.0", "DRIVER": "iwlwifi"}) ] result = self.validator.check_wlan() self.assertTrue(result) self.mock_db.record_prereq.assert_called_with( "WLAN driver `iwlwifi` bound to 0000:00:1f.0", "βœ…" ) def test_check_wlan_multiple_devices(self): """Test check_wlan with multiple WLAN devices""" self.mock_pyudev.list_devices.return_value = [ MagicMock( properties={"PCI_SLOT_NAME": "0000:00:1f.0", "DRIVER": "iwlwifi"} ), MagicMock(properties={"PCI_SLOT_NAME": "0000:00:1f.1", "DRIVER": None}), ] result = self.validator.check_wlan() self.assertFalse(result) self.mock_db.record_prereq.assert_any_call( "WLAN driver `iwlwifi` bound to 0000:00:1f.0", "βœ…" ) self.mock_db.record_prereq.assert_any_call( "WLAN device in 0000:00:1f.1 missing driver", "🚦" ) self.assertTrue( any(isinstance(f, MissingDriver) for f in self.validator.failures) ) def test_check_usb3_no_devices(self): """Test check_usb3 when no USB3 devices are found""" self.mock_pyudev.list_devices.return_value = [] result = self.validator.check_usb3() self.assertTrue(result) self.mock_db.record_prereq.assert_not_called() def test_check_usb3_driver_missing(self): """Test check_usb3 when the xhci_hcd driver is missing""" self.mock_pyudev.list_devices.return_value = [ MagicMock( properties={ "PCI_SLOT_NAME": "0000:00:1d.0", "DRIVER": None, } ) ] result = self.validator.check_usb3() self.assertFalse(result) self.assertTrue( any(isinstance(f, MissingXhciHcd) for f in self.validator.failures) ) self.mock_db.record_prereq.assert_called_with( "USB3 controller for 0000:00:1d.0 not using `xhci_hcd` driver", "❌" ) def test_check_usb3_driver_present(self): """Test check_usb3 when the xhci_hcd driver is present""" self.mock_pyudev.list_devices.return_value = [ MagicMock( properties={ "PCI_SLOT_NAME": "0000:00:1d.0", "DRIVER": "xhci_hcd", } ) ] result = self.validator.check_usb3() self.assertTrue(result) self.mock_db.record_prereq.assert_called_with( "USB3 driver `xhci_hcd` bound to 0000:00:1d.0", "βœ…" ) def test_check_usb3_multiple_devices(self): """Test check_usb3 with multiple USB3 devices""" self.mock_pyudev.list_devices.return_value = [ MagicMock( properties={ "PCI_SLOT_NAME": "0000:00:1d.0", "DRIVER": "xhci_hcd", } ), MagicMock( properties={ "PCI_SLOT_NAME": "0000:00:1d.1", "DRIVER": None, } ), ] result = self.validator.check_usb3() self.assertFalse(result) self.mock_db.record_prereq.assert_any_call( "USB3 controller for 0000:00:1d.1 not using `xhci_hcd` driver", "❌" ) self.assertTrue( any(isinstance(f, MissingXhciHcd) for f in self.validator.failures) ) def test_check_amd_pmc_driver_loaded(self): """Test check_amd_pmc when the driver is loaded""" self.mock_pyudev.list_devices.return_value = [ MagicMock( sys_path="/sys/devices/platform/amd_pmc", properties={"DRIVER": "amd_pmc"}, ) ] with patch("amd_debug.prerequisites.os.path.exists", return_value=True), patch( "amd_debug.prerequisites.read_file", side_effect=["mock_version", "mock_program"], ): result = self.validator.check_amd_pmc() self.assertTrue(result) self.mock_db.record_prereq.assert_called_with( "PMC driver `amd_pmc` loaded (Program mock_program Firmware mock_version)", "βœ…", ) def test_check_amd_pmc_driver_loaded_timeout_error(self): """Test check_amd_pmc when a TimeoutError occurs while reading files""" self.mock_pyudev.list_devices.return_value = [ MagicMock( sys_path="/sys/devices/platform/amd_pmc", properties={"DRIVER": "amd_pmc"}, ) ] with patch("amd_debug.prerequisites.os.path.exists", return_value=True), patch( "amd_debug.prerequisites.read_file", side_effect=TimeoutError ): result = self.validator.check_amd_pmc() self.assertFalse(result) self.mock_db.record_prereq.assert_called_with( "failed to communicate using `amd_pmc` driver", "❌" ) def test_check_amd_pmc_driver_not_loaded(self): """Test check_amd_pmc when the driver is not loaded""" self.mock_pyudev.list_devices.return_value = [] result = self.validator.check_amd_pmc() self.assertFalse(result) self.assertTrue( any(isinstance(f, MissingAmdPmc) for f in self.validator.failures) ) self.mock_db.record_prereq.assert_called_with( "PMC driver `amd_pmc` did not bind to any ACPI device", "❌" ) def test_check_amd_pmc_driver_loaded_no_version_info(self): """Test check_amd_pmc when the driver is loaded but version info is missing""" self.mock_pyudev.list_devices.return_value = [ MagicMock( sys_path="/sys/devices/platform/amd_pmc", properties={"DRIVER": "amd_pmc"}, ) ] with patch("amd_debug.prerequisites.os.path.exists", return_value=False): result = self.validator.check_amd_pmc() self.assertTrue(result) self.mock_db.record_prereq.assert_called_with( "PMC driver `amd_pmc` loaded", "βœ…" ) @patch("amd_debug.prerequisites.minimum_kernel", return_value=True) def test_check_storage_new_kernel(self, _mock_minimum_kernel): """Test check_storage when kernel version >= 6.10""" self.mock_pyudev.list_devices.return_value = [ MagicMock(properties={"PCI_SLOT_NAME": "0000:00:1f.0", "DRIVER": "nvme"}) ] result = self.validator.check_storage() self.assertTrue(result) def test_check_storage_no_kernel_log(self): """Test check_storage when kernel log is unavailable""" self.validator.kernel_log = None result = self.validator.check_storage() self.assertTrue(result) self.mock_db.record_prereq.assert_called_with( "Unable to test storage from kernel log", "🚦" ) @patch("amd_debug.prerequisites.subprocess.check_output") def test_capture_edid_no_edid_data(self, mock_check_output): """Test capture_edid when no EDID data is found""" self.validator.display.get_edid = MagicMock(return_value={}) result = self.validator.capture_edid() self.assertTrue(result) self.mock_db.record_debug.assert_called_with("No EDID data found") mock_check_output.assert_not_called() @patch("amd_debug.prerequisites.subprocess.check_output") def test_capture_edid_file_not_found(self, mock_check_output): """Test capture_edid when edid-decode is not installed""" self.validator.display.get_edid = MagicMock( return_value={"Monitor1": "/path/to/edid"} ) mock_check_output.side_effect = FileNotFoundError result = self.validator.capture_edid() self.assertTrue(result) self.mock_db.record_prereq.assert_called_with( "Failed to capture EDID table", "πŸ‘€" ) @patch("amd_debug.prerequisites.subprocess.check_output") def test_capture_edid_subprocess_error(self, mock_check_output): """Test capture_edid when subprocess.check_output raises an error""" self.validator.display.get_edid = MagicMock( return_value={"Monitor1": "/path/to/edid"} ) mock_check_output.side_effect = subprocess.CalledProcessError( returncode=1, cmd="edid-decode", output=b"Error decoding EDID" ) result = self.validator.capture_edid() self.assertTrue(result) self.mock_db.record_prereq.assert_called_with( "Failed to capture EDID table", "πŸ‘€" ) @patch("amd_debug.prerequisites.subprocess.check_output") def test_capture_edid_success(self, mock_check_output): """Test capture_edid when EDID data is successfully decoded""" self.validator.display.get_edid = MagicMock( return_value={"Monitor1": "/path/to/edid"} ) mock_check_output.return_value = b"Decoded EDID data" result = self.validator.capture_edid() self.assertTrue(result) self.mock_db.record_debug.assert_called_with( apply_prefix_wrapper("EDID for Monitor1:", "Decoded EDID data") ) @patch("amd_debug.prerequisites.find_ip_version", return_value=True) @patch("amd_debug.prerequisites.os.path.exists") @patch("amd_debug.prerequisites.read_file") def test_check_dpia_pg_dmcub_usb4_found( self, mock_read_file, mock_path_exists, mock_find_ip_version ): """Test check_dpia_pg_dmcub when USB4 routers are found""" usb4_device = MagicMock() self.mock_pyudev.list_devices.side_effect = [ [usb4_device], # First call: USB4 present ] result = self.validator.check_dpia_pg_dmcub() self.assertTrue(result) self.mock_db.record_debug.assert_called_with( "USB4 routers found, no need to check DMCUB version" ) @patch("amd_debug.prerequisites.clear_temporary_message") def test_run_failure_records_kernel_log(self, _mock_clear): """Test that the whole kernel log is recorded when prerequisites fail""" # Mock all check and info methods to avoid system access check_methods = [ "check_aspm", "check_i2c_hid", "check_pinctrl_amd", "check_amd_hsmp", "check_amd_xdna", "check_amd_pmc", "check_amd_cpu_hpet_wa", "check_port_pm_override", "check_usb3", "check_usb4", "check_sleep_mode", "check_storage", "check_wcn6855_bug", "check_amdgpu", "check_amdgpu_parameters", "check_cpu", "check_msr", "check_smt", "check_iommu", "check_asus_rog_ally", "check_dpia_pg_dmcub", "check_isp4", "check_fadt", "check_logger", "check_lps0", "check_permissions", "check_wlan", "check_taint", "capture_acpi", "map_acpi_path", "check_device_firmware", "check_network", "capture_disabled_pins", ] for method in check_methods: setattr(self.validator, method, MagicMock(return_value=True)) # Mock one check to fail self.validator.check_permissions.return_value = False # Mock get_cpu_vendor self.validator.get_cpu_vendor = MagicMock(return_value="AuthenticAMD") # Set a mock kernel log mock_log_content = "Mocked full kernel log content" self.mock_kernel_log.get_full_log.return_value = mock_log_content # Run the validator with patch( "amd_debug.prerequisites.print_temporary_message", return_value="msg" ): result = self.validator.run() self.assertFalse(result) # Verify Headers.BrokenPrerequisites was recorded from amd_debug.prerequisites import Headers self.mock_db.record_prereq.assert_any_call(Headers.BrokenPrerequisites, "🚫") # Verify the whole kernel log was recorded to debug self.mock_db.record_debug.assert_called_with(mock_log_content) @patch("amd_debug.prerequisites.find_ip_version", return_value=True) @patch("amd_debug.prerequisites.os.path.exists", return_value=True) @patch("amd_debug.prerequisites.read_file", return_value="0x90001B01") @patch("amd_debug.prerequisites.version.parse") def test_check_dpia_pg_dmcub_dmcub_fw_version_new_enough( self, mock_version_parse, mock_read_file, mock_path_exists, mock_find_ip_version ): """Test check_dpia_pg_dmcub when DMCUB firmware version is new enough""" self.mock_pyudev.list_devices.side_effect = [ [], # First call: no USB4 [ MagicMock( properties={ "PCI_CLASS": "30000", "PCI_ID": "1002abcd", "PCI_SLOT_NAME": "0000:01:00.0", }, sys_path="/sys/devices/pci0000:01/0000:01:00.0", ) ], ] with patch("builtins.open", new_callable=mock_open, read_data="3") as mock_file: handlers = ( mock_file.return_value, mock_open(read_data="5").return_value, mock_open(read_data="0").return_value, ) mock_open.side_effect = handlers result = self.validator.check_dpia_pg_dmcub() self.assertTrue(result) self.mock_db.record_prereq.assert_not_called() @patch("amd_debug.prerequisites.find_ip_version", return_value=True) @patch("amd_debug.prerequisites.os.path.exists", return_value=True) @patch("amd_debug.prerequisites.read_file", return_value="0x8001B00") def test_check_dpia_pg_dmcub_dmcub_fw_version_too_old( self, mock_read_file, mock_path_exists, mock_find_ip_version ): """Test check_dpia_pg_dmcub when DMCUB firmware version is too old""" self.mock_pyudev.list_devices.side_effect = [ [], # First call: no USB4 [ MagicMock( properties={ "PCI_CLASS": "30000", "PCI_ID": "1002abcd", "PCI_SLOT_NAME": "0000:01:00.0", }, sys_path="/sys/devices/pci0000:01/0000:01:00.0", ) ], ] result = self.validator.check_dpia_pg_dmcub() self.assertFalse(result) self.mock_db.record_prereq.assert_called_with( "DMCUB Firmware is outdated", "❌" ) self.assertTrue( any(isinstance(f, DmcubTooOld) for f in self.validator.failures) ) @patch("amd_debug.prerequisites.find_ip_version", return_value=True) @patch("amd_debug.prerequisites.os.path.exists", return_value=False) @patch( "amd_debug.prerequisites.read_file", side_effect=[ "", # sysfs read returns empty, so fallback to debugfs "DMCUB fw: 09001B00\nOther line\n", # debugfs read ], ) def test_check_dpia_pg_dmcub_debugfs_version_new_enough( self, mock_read_file, mock_path_exists, mock_find_ip_version ): """Test check_dpia_pg_dmcub when DMCUB version is found in debugfs and is new enough""" self.mock_pyudev.list_devices.side_effect = [ [], # First call: no USB4 [ MagicMock( properties={ "PCI_CLASS": "30000", "PCI_ID": "1002abcd", "PCI_SLOT_NAME": "0", }, sys_path="/sys/devices/pci0000:01/0000:01:00.0", ) ], ] result = self.validator.check_dpia_pg_dmcub() self.assertTrue(result) self.mock_db.record_prereq.assert_not_called() @patch("amd_debug.prerequisites.find_ip_version", return_value=True) @patch("amd_debug.prerequisites.os.path.exists", return_value=False) @patch( "amd_debug.prerequisites.read_file", side_effect=[ "DMCUB fw: 0x08001B00\nOther line\n", # debugfs read ], ) def test_check_dpia_pg_dmcub_debugfs_version_too_old( self, mock_read_file, mock_path_exists, mock_find_ip_version ): """Test check_dpia_pg_dmcub when DMCUB version is found in debugfs and is too old""" self.mock_pyudev.list_devices.side_effect = [ [], # First call: no USB4 [ MagicMock( properties={ "PCI_CLASS": "30000", "PCI_ID": "1002abcd", "PCI_SLOT_NAME": "0", }, sys_path="/sys/devices/pci0000:01/0000:01:00.0", ) ], ] result = self.validator.check_dpia_pg_dmcub() self.assertFalse(result) self.mock_db.record_prereq.assert_called_with( "DMCUB Firmware is outdated", "❌" ) self.assertTrue( any(isinstance(f, DmcubTooOld) for f in self.validator.failures) ) @patch("amd_debug.prerequisites.find_ip_version", return_value=False) def test_check_dpia_pg_dmcub_no_matching_dcn(self, mock_find_ip_version): """Test check_dpia_pg_dmcub when no matching DCN is found""" self.mock_pyudev.list_devices.side_effect = [ [], # First call: no USB4 [ MagicMock( properties={ "PCI_CLASS": "30000", "PCI_ID": "1002abcd", "PCI_SLOT_NAME": "0", }, sys_path="/sys/devices/pci0000:01/0000:01:00.0", ) ], ] result = self.validator.check_dpia_pg_dmcub() self.assertTrue(result) self.mock_db.record_prereq.assert_not_called() @patch("amd_debug.prerequisites.os.path.exists") def test_capture_nvidia_version_file_missing(self, mock_exists): """Test capture_nvidia when /proc/driver/nvidia/version does not exist""" mock_exists.side_effect = lambda p: False if "version" in p else True result = self.validator.capture_nvidia() self.assertTrue(result) self.mock_db.record_debug_file.assert_not_called() self.mock_db.record_prereq.assert_not_called() @patch("amd_debug.prerequisites.os.path.exists") def test_capture_nvidia_gpus_dir_missing(self, mock_exists): """Test capture_nvidia when /proc/driver/nvidia/gpus does not exist""" def exists_side_effect(path): if "version" in path: return True if "gpus" in path: return False return True mock_exists.side_effect = exists_side_effect result = self.validator.capture_nvidia() self.assertTrue(result) self.mock_db.record_debug_file.assert_called_once_with( "/proc/driver/nvidia/version" ) self.mock_db.record_prereq.assert_not_called() @patch("amd_debug.prerequisites.os.walk") @patch("amd_debug.prerequisites.os.path.exists") def test_capture_nvidia_success(self, mock_exists, mock_walk): """Test capture_nvidia when NVIDIA GPU files are present and readable""" mock_exists.side_effect = lambda p: True mock_walk.return_value = [ ("/proc/driver/nvidia/gpus/0000:01:00.0", [], ["info", "power"]) ] result = self.validator.capture_nvidia() self.assertTrue(result) self.mock_db.record_debug_file.assert_any_call("/proc/driver/nvidia/version") self.mock_db.record_debug.assert_any_call("NVIDIA info") self.mock_db.record_debug_file.assert_any_call( "/proc/driver/nvidia/gpus/0000:01:00.0/info" ) self.mock_db.record_debug.assert_any_call("NVIDIA power") self.mock_db.record_debug_file.assert_any_call( "/proc/driver/nvidia/gpus/0000:01:00.0/power" ) @patch("amd_debug.prerequisites.os.walk") @patch("amd_debug.prerequisites.os.path.exists") def test_capture_nvidia_permission_error_on_version(self, mock_exists, mock_walk): """Test capture_nvidia when PermissionError occurs reading version file""" mock_exists.side_effect = lambda p: True if "version" in p else False self.mock_db.record_debug_file.side_effect = PermissionError result = self.validator.capture_nvidia() self.assertTrue(result) self.mock_db.record_prereq.assert_called_with( "NVIDIA GPU version not readable", "πŸ‘€" ) @patch("amd_debug.prerequisites.os.walk") @patch("amd_debug.prerequisites.os.path.exists") def test_capture_nvidia_permission_error_on_gpu_file(self, mock_exists, mock_walk): """Test capture_nvidia when PermissionError occurs reading a GPU file""" mock_exists.side_effect = lambda p: True mock_walk.return_value = [ ("/proc/driver/nvidia/gpus/0000:01:00.0", [], ["info"]) ] self.mock_db.record_debug_file.side_effect = [None, PermissionError] result = self.validator.capture_nvidia() self.assertTrue(result) self.mock_db.record_debug.assert_any_call("NVIDIA info") self.mock_db.record_prereq.assert_called_with( "NVIDIA GPU {f} not readable", "πŸ‘€" ) @patch("amd_debug.prerequisites.os.walk") @patch( "builtins.open", new_callable=unittest.mock.mock_open, read_data=b"C1 state info", ) def test_capture_cstates_single_file(self, mock_open, mock_walk): """Test capture_cstates with a single cpuidle file""" mock_walk.return_value = [ ("/sys/bus/cpu/devices/cpu0/cpuidle", [], ["state1"]), ] self.validator.capture_cstates() self.mock_db.record_debug.assert_called_with( "ACPI C-state information\n└─/sys/bus/cpu/devices/cpu0/cpuidle/state1: C1 state info" ) @patch("amd_debug.prerequisites.os.walk") @patch("builtins.open", new_callable=mock_open) def test_capture_cstates_multiple_files(self, mock_open_func, mock_walk): """Test capture_cstates with multiple cpuidle files""" # Setup mock file reads for two files file_contents = { "/sys/bus/cpu/devices/cpu0/cpuidle/state1": b"C1 info", "/sys/bus/cpu/devices/cpu0/cpuidle/state2": b"C2 info", } def side_effect(path, mode="rb"): mock_file = mock_open(read_data=file_contents[path])() return mock_file mock_open_func.side_effect = side_effect mock_walk.return_value = [ ("/sys/bus/cpu/devices/cpu0/cpuidle", [], ["state1", "state2"]), ] self.validator.capture_cstates() # The prefix logic is based on order, so check for both lines debug_call = self.mock_db.record_debug.call_args[0][0] self.assertIn("/sys/bus/cpu/devices/cpu0/cpuidle/state1: C1 info", debug_call) self.assertIn("/sys/bus/cpu/devices/cpu0/cpuidle/state2: C2 info", debug_call) self.assertTrue(debug_call.startswith("ACPI C-state information\n")) @patch("amd_debug.prerequisites.os.walk") @patch("builtins.open", new_callable=mock_open, read_data=b"") def test_capture_cstates_empty_files(self, _mock_open, mock_walk): """Test capture_cstates with empty cpuidle files""" mock_walk.return_value = [ ("/sys/bus/cpu/devices/cpu0/cpuidle", [], ["state1"]), ] self.validator.capture_cstates() self.mock_db.record_debug.assert_called_with( "ACPI C-state information\n└─/sys/bus/cpu/devices/cpu0/cpuidle/state1: " ) @patch("amd_debug.prerequisites.os.walk") @patch("amd_debug.prerequisites.open", side_effect=PermissionError) def test_capture_cstates_permission_error(self, _mock_open, mock_walk): """Test capture_cstates when reading cpuidle files raises PermissionError""" mock_walk.return_value = [ ("/sys/bus/cpu/devices/cpu0/cpuidle", [], ["state1"]), ] with self.assertRaises(PermissionError): self.validator.capture_cstates() self.mock_db.record_debug.assert_not_called() @patch("amd_debug.prerequisites.os.walk") def test_capture_cstates_no_files(self, mock_walk): """Test capture_cstates when no cpuidle files are present""" mock_walk.return_value = [ ("/sys/bus/cpu/devices/cpu0/cpuidle", [], []), ] self.validator.capture_cstates() self.mock_db.record_debug.assert_called_with("ACPI C-state information\n") @patch("amd_debug.prerequisites.read_file") def test_check_pinctrl_amd_driver_loaded_with_missing_file_error( self, mock_read_file ): """Test check_pinctrl_amd when the driver is loaded but debug file is missing""" mock_read_file.side_effect = FileNotFoundError self.mock_pyudev.list_devices.return_value = [ MagicMock(properties={"DRIVER": "amd_gpio"}) ] result = self.validator.check_pinctrl_amd() self.assertTrue(result) self.mock_db.record_prereq.assert_called_with( "GPIO debugfs not available", "πŸ‘€" ) def test_check_amdgpu_no_devices(self): """Test check_amdgpu when no PCI devices are found""" self.mock_pyudev.list_devices.return_value = [] result = self.validator.check_amdgpu() self.assertFalse(result) self.mock_db.record_prereq.assert_called_with("Integrated GPU not found", "❌") self.assertTrue(any(isinstance(f, MissingGpu) for f in self.validator.failures)) def test_check_amdgpu_non_amd_devices(self): """Test check_amdgpu when PCI devices are present but not AMD GPUs""" self.mock_pyudev.list_devices.return_value = [ MagicMock( properties={ "PCI_CLASS": "30000", "PCI_ID": "8086abcd", "DRIVER": "i915", } ), ] result = self.validator.check_amdgpu() self.assertFalse(result) self.mock_db.record_prereq.assert_called_with("Integrated GPU not found", "❌") self.assertTrue(any(isinstance(f, MissingGpu) for f in self.validator.failures)) def test_check_amdgpu_driver_not_loaded(self): """Test check_amdgpu when AMD GPU is present but driver is not loaded""" self.mock_pyudev.list_devices.return_value = [ MagicMock( properties={"PCI_CLASS": "20000", "PCI_ID": "1111abcd", "DRIVER": None} ), MagicMock( properties={"PCI_CLASS": "30000", "PCI_ID": "1002abcd", "DRIVER": None} ), ] result = self.validator.check_amdgpu() self.assertFalse(result) self.mock_db.record_prereq.assert_called_with( "GPU driver `amdgpu` not loaded", "❌" ) self.assertTrue( any(isinstance(f, MissingAmdgpu) for f in self.validator.failures) ) def test_check_amdgpu_driver_loaded(self): """Test check_amdgpu when AMD GPU is present and driver is loaded""" self.mock_pyudev.list_devices.return_value = [ MagicMock( properties={ "PCI_CLASS": "30000", "PCI_ID": "1002abcd", "DRIVER": "amdgpu", "PCI_SLOT_NAME": "0000:01:00.0", } ), ] result = self.validator.check_amdgpu() self.assertTrue(result) self.mock_db.record_prereq.assert_called_with( "GPU driver `amdgpu` bound to 0000:01:00.0", "βœ…" ) def test_check_amdgpu_multiple_devices_mixed(self): """Test check_amdgpu with multiple devices, one with driver loaded, one without""" self.mock_pyudev.list_devices.return_value = [ MagicMock( properties={ "PCI_CLASS": "30000", "PCI_ID": "1002abcd", "DRIVER": "amdgpu", "PCI_SLOT_NAME": "0000:01:00.0", } ), MagicMock( properties={"PCI_CLASS": "30000", "PCI_ID": "1002abcd", "DRIVER": None} ), ] result = self.validator.check_amdgpu() self.assertFalse(result) self.mock_db.record_prereq.assert_any_call( "GPU driver `amdgpu` bound to 0000:01:00.0", "βœ…" ) self.mock_db.record_prereq.assert_any_call( "GPU driver `amdgpu` not loaded", "❌" ) self.assertTrue( any(isinstance(f, MissingAmdgpu) for f in self.validator.failures) ) @patch("amd_debug.prerequisites.os.path.exists") @patch("amd_debug.prerequisites.os.readlink") def test_check_isp4_no_devices(self, _mock_readlink, _mock_path_exists): """Test check_isp4 when no ISP4 camera devices are found""" self.mock_pyudev.list_devices.return_value = [] result = self.validator.check_isp4() self.assertTrue(result) self.mock_db.record_prereq.assert_not_called() @patch("amd_debug.prerequisites.os.path.exists") @patch("amd_debug.prerequisites.os.readlink") def test_check_isp4_device_without_path(self, _mock_readlink, mock_path_exists): """Test check_isp4 when ACPI device exists but path file is missing""" mock_path_exists.return_value = False self.mock_pyudev.list_devices.return_value = [ MagicMock(sys_path="/sys/devices/acpi/OMNI5C10:00", sys_name="OMNI5C10:00") ] result = self.validator.check_isp4() self.assertTrue(result) self.mock_db.record_prereq.assert_not_called() @patch("amd_debug.prerequisites.os.path.exists") @patch("amd_debug.prerequisites.os.readlink") def test_check_isp4_driver_not_bound(self, _mock_readlink, mock_path_exists): """Test check_isp4 when ISP4 driver is not bound to the device""" mock_path_exists.side_effect = lambda p: "path" in p self.mock_pyudev.list_devices.return_value = [ MagicMock(sys_path="/sys/devices/acpi/OMNI5C10:00", sys_name="OMNI5C10:00") ] result = self.validator.check_isp4() self.assertFalse(result) self.mock_db.record_prereq.assert_called_with( "ISP4 platform camera driver 'amd-isp4' not bound to OMNI5C10:00", "❌" ) self.assertTrue( any( isinstance(f, MissingIsp4PlatformDriver) for f in self.validator.failures ) ) @patch("amd_debug.prerequisites.os.path.exists") @patch( "amd_debug.prerequisites.os.readlink", return_value="/sys/devices/platform/drivers/amd-isp4", ) def test_check_isp4_driver_bound_but_module_not_loaded( self, _mock_readlink, mock_path_exists ): """Test check_isp4 when driver is bound but amd_capture module is not loaded""" def exists_side_effect(path): if "path" in path or "driver" in path: return True if "amd_capture" in path: return False return True mock_path_exists.side_effect = exists_side_effect self.mock_pyudev.list_devices.return_value = [ MagicMock(sys_path="/sys/devices/acpi/OMNI5C10:00", sys_name="OMNI5C10:00") ] result = self.validator.check_isp4() self.assertFalse(result) self.mock_db.record_prereq.assert_any_call( "ISP4 platform camera driver 'amd-isp4' bound to OMNI5C10:00", "βœ…" ) self.mock_db.record_prereq.assert_any_call( "Camera driver module 'amd_capture' not loaded", "❌" ) self.assertTrue( any(isinstance(f, MissingAmdCaptureModule) for f in self.validator.failures) ) @patch("amd_debug.prerequisites.os.path.exists") @patch( "amd_debug.prerequisites.os.readlink", return_value="/sys/devices/platform/drivers/amd-isp4", ) def test_check_isp4_fully_configured(self, _mock_readlink, mock_path_exists): """Test check_isp4 when ISP4 is fully configured with driver and module""" mock_path_exists.return_value = True self.mock_pyudev.list_devices.return_value = [ MagicMock(sys_path="/sys/devices/acpi/OMNI5C10:00", sys_name="OMNI5C10:00") ] result = self.validator.check_isp4() self.assertTrue(result) self.mock_db.record_prereq.assert_any_call( "ISP4 platform camera driver 'amd-isp4' bound to OMNI5C10:00", "βœ…" ) self.mock_db.record_prereq.assert_any_call( "Camera driver module 'amd_capture' loaded", "βœ…" ) @patch("amd_debug.prerequisites.os.path.exists") @patch( "amd_debug.prerequisites.os.readlink", return_value="/sys/devices/platform/drivers/other-driver", ) def test_check_isp4_wrong_driver(self, _mock_readlink, mock_path_exists): """Test check_isp4 when wrong driver is bound to the device""" mock_path_exists.side_effect = lambda p: "path" in p or "driver" in p self.mock_pyudev.list_devices.return_value = [ MagicMock(sys_path="/sys/devices/acpi/OMNI5C10:00", sys_name="OMNI5C10:00") ] result = self.validator.check_isp4() self.assertFalse(result) self.mock_db.record_prereq.assert_called_with( "ISP4 platform camera driver 'amd-isp4' not bound to OMNI5C10:00", "❌" ) self.assertTrue( any( isinstance(f, MissingIsp4PlatformDriver) for f in self.validator.failures ) ) @patch("amd_debug.prerequisites.os.path.exists") @patch( "amd_debug.prerequisites.os.readlink", return_value="/sys/devices/platform/drivers/amd-isp4", ) def test_check_isp4_multiple_devices(self, _mock_readlink, mock_path_exists): """Test check_isp4 with multiple ISP4 camera devices""" mock_path_exists.return_value = True self.mock_pyudev.list_devices.return_value = [ MagicMock(sys_path="/sys/devices/acpi/OMNI5C10:00", sys_name="OMNI5C10:00"), MagicMock(sys_path="/sys/devices/acpi/OMNI5C10:01", sys_name="OMNI5C10:01"), ] result = self.validator.check_isp4() self.assertTrue(result) self.mock_db.record_prereq.assert_any_call( "ISP4 platform camera driver 'amd-isp4' bound to OMNI5C10:00", "βœ…" ) self.mock_db.record_prereq.assert_any_call( "ISP4 platform camera driver 'amd-isp4' bound to OMNI5C10:01", "βœ…" ) @patch("amd_debug.prerequisites.os.path.exists") def test_check_isp4_device_not_starting_with_omni5c10(self, mock_path_exists): """Test check_isp4 when ACPI devices don't match OMNI5C10 pattern""" mock_path_exists.return_value = True self.mock_pyudev.list_devices.return_value = [ MagicMock(sys_path="/sys/devices/acpi/OTHER:00", sys_name="OTHER:00") ] result = self.validator.check_isp4() self.assertTrue(result) self.mock_db.record_prereq.assert_not_called() amd-debug-tools-0.2.15/src/test_pstate.py000066400000000000000000000136521515405217400202750ustar00rootroot00000000000000#!/usr/bin/python3 # SPDX-License-Identifier: MIT """ This module contains unit tests for the pstate tool in the amd-debug-tools package. """ import logging import unittest from unittest.mock import patch, MagicMock from amd_debug.pstate import ( AmdPstateTriage, amd_cppc_cap_lowest_perf, amd_cppc_cap_lownonlin_perf, amd_cppc_cap_nominal_perf, amd_cppc_cap_highest_perf, amd_cppc_max_perf, amd_cppc_min_perf, amd_cppc_des_perf, amd_cppc_epp_perf, ) class TestAmdPstateTriage(unittest.TestCase): """Test AmdPstateTriage class""" @classmethod def setUpClass(cls): logging.basicConfig(filename="/dev/null", level=logging.DEBUG) @patch("amd_debug.pstate.relaunch_sudo") @patch("amd_debug.pstate.get_pretty_distro", return_value="Test Distro") @patch("amd_debug.pstate.print_color") @patch("amd_debug.pstate.Context") def test_init( self, _mock_context, mock_print_color, mock_get_pretty_distro, mock_relaunch_sudo, ): """Test initialization of AmdPstateTriage class""" triage = AmdPstateTriage(logging=True) mock_relaunch_sudo.assert_called_once() mock_get_pretty_distro.assert_called_once() mock_print_color.assert_called_with("Test Distro", "🐧") self.assertIsNotNone(triage.context) @patch("amd_debug.pstate.os.path.exists", return_value=True) @patch("amd_debug.pstate.read_file", return_value="test_value") @patch("amd_debug.pstate.print_color") @patch("amd_debug.pstate.relaunch_sudo") def test_gather_amd_pstate_info( self, _mock_relaunch_sudo, mock_print_color, mock_read_file, mock_path_exists ): """Test gather_amd_pstate_info method""" triage = AmdPstateTriage(logging=False) triage.gather_amd_pstate_info() mock_path_exists.assert_called() mock_read_file.assert_called() mock_print_color.assert_any_call("'status':\ttest_value", "β—‹") mock_print_color.assert_any_call("'prefcore':\ttest_value", "β—‹") @patch( "amd_debug.pstate.os.uname", return_value=MagicMock(sysname="Linux", release="5.15.0"), ) @patch("amd_debug.pstate.print_color") @patch("amd_debug.pstate.relaunch_sudo") def test_gather_kernel_info( self, _mock_relaunch_sudo, mock_print_color, mock_uname ): """Test gather_kernel_info method""" triage = AmdPstateTriage(logging=False) triage.gather_kernel_info() mock_uname.assert_called() mock_print_color.assert_called_with("Kernel:\t5.15.0", "🐧") @patch("amd_debug.pstate.os.path.exists", return_value=True) @patch("amd_debug.pstate.read_file", return_value="1") @patch("amd_debug.pstate.print_color") @patch("amd_debug.pstate.relaunch_sudo") def test_gather_scheduler_info( self, _mock_relaunch_sudo, mock_print_color, mock_read_file, mock_path_exists ): """Test gather_scheduler_info method""" triage = AmdPstateTriage(logging=False) triage.gather_scheduler_info() mock_path_exists.assert_called() mock_read_file.assert_called() mock_print_color.assert_any_call("ITMT:\t1", "🐧") @patch("amd_debug.pstate.read_msr") @patch("amd_debug.pstate.print_color") @patch("amd_debug.pstate.Context") @patch("amd_debug.pstate.relaunch_sudo") def test_gather_msrs( self, _mock_relaunch_sudo, mock_context, mock_print_color, mock_read_msr ): """Test gather_msrs method""" # Mock the list of CPUs mock_context.return_value.list_devices.return_value = [ MagicMock(sys_name="cpu0"), MagicMock(sys_name="cpu1"), ] # Mock MSR values for the CPUs mock_read_msr.side_effect = [ 0x1, # MSR_AMD_CPPC_ENABLE for cpu0 0x2, # MSR_AMD_CPPC_STATUS for cpu0 0x12345678, # MSR_AMD_CPPC_CAP1 for cpu0 0x87654321, # MSR_AMD_CPPC_CAP2 for cpu0 0xABCDEF, # MSR_AMD_CPPC_REQ for cpu0 0x1, # MSR_AMD_CPPC_ENABLE for cpu1 0x2, # MSR_AMD_CPPC_STATUS for cpu1 0x12345678, # MSR_AMD_CPPC_CAP1 for cpu1 0x87654321, # MSR_AMD_CPPC_CAP2 for cpu1 0xABCDEF, # MSR_AMD_CPPC_REQ for cpu1 ] triage = AmdPstateTriage(logging=False) result = triage.gather_msrs() # Assert that MSR values were read for both CPUs self.assertEqual(mock_read_msr.call_count, 10) # Assert that print_color was called to display the MSR information self.assertTrue(mock_print_color.called) # Assert that the method returned True (indicating success) self.assertIsNone(result) def test_amd_cppc_cap_lowest_perf(self): """Test amd_cppc_cap_lowest_perf function""" self.assertEqual(amd_cppc_cap_lowest_perf(0x12345678), 0x78) def test_amd_cppc_cap_lownonlin_perf(self): """Test amd_cppc_cap_lownonlin_perf function""" self.assertEqual(amd_cppc_cap_lownonlin_perf(0x12345678), 0x56) def test_amd_cppc_cap_nominal_perf(self): """Test amd_cppc_cap_nominal_perf function""" self.assertEqual(amd_cppc_cap_nominal_perf(0x12345678), 0x34) def test_amd_cppc_cap_highest_perf(self): """Test amd_cppc_cap_highest_perf function""" self.assertEqual(amd_cppc_cap_highest_perf(0x12345678), 0x12) def test_amd_cppc_max_perf(self): """Test amd_cppc_max_perf function""" self.assertEqual(amd_cppc_max_perf(0x12345678), 0x78) def test_amd_cppc_min_perf(self): """Test amd_cppc_min_perf function""" self.assertEqual(amd_cppc_min_perf(0x12345678), 0x56) def test_amd_cppc_des_perf(self): """Test amd_cppc_des_perf function""" self.assertEqual(amd_cppc_des_perf(0x12345678), 0x34) def test_amd_cppc_epp_perf(self): """Test amd_cppc_epp_perf function""" self.assertEqual(amd_cppc_epp_perf(0x12345678), 0x12) amd-debug-tools-0.2.15/src/test_s2idle.py000066400000000000000000001023171515405217400201540ustar00rootroot00000000000000#!/usr/bin/python3 # SPDX-License-Identifier: MIT """ This module contains unit tests for the s2idle tool in the amd-debug-tools package. """ import argparse import sys import unittest import logging import sqlite3 from datetime import datetime from unittest.mock import patch from amd_debug.s2idle import ( parse_args, main, install, uninstall, run_test_cycle, display_report_file, report, prompt_report_arguments, Defaults, ) class TestParseArgs(unittest.TestCase): """Test parse_args function""" @classmethod def setUpClass(cls): logging.basicConfig(filename="/dev/null", level=logging.DEBUG) def setUp(self): self.default_sys_argv = sys.argv def tearDown(self): sys.argv = self.default_sys_argv @patch("sys.stderr") def test_no_arguments(self, _mock_print): """Test parse_args with no arguments""" sys.argv = ["s2idle.py"] with self.assertRaises(SystemExit): parse_args() def test_test_command_with_arguments(self): """Test parse_args with test command and arguments""" sys.argv = [ "s2idle.py", "test", "--count", "5", "--duration", "10", "--wait", "3", "--format", "txt", "--tool-debug", ] args = parse_args() self.assertEqual(args.action, "test") self.assertEqual(args.count, "5") self.assertEqual(args.duration, "10") self.assertEqual(args.wait, "3") self.assertEqual(args.format, "txt") self.assertTrue(args.tool_debug) def test_report_command_with_arguments(self): """Test parse_args with report command and arguments""" sys.argv = [ "s2idle.py", "report", "--since", "2023-01-01", "--until", "2023-02-01", "--format", "html", "--report-debug", ] args = parse_args() self.assertEqual(args.action, "report") self.assertEqual(args.since, "2023-01-01") self.assertEqual(args.until, "2023-02-01") self.assertEqual(args.format, "html") self.assertTrue(args.report_debug) @patch("sys.prefix", "amd_debug.s2idle") @patch("sys.base_prefix", "foo") def test_install_command(self): """Test parse_args with install command""" sys.argv = ["s2idle.py", "install", "--tool-debug"] args = parse_args() self.assertEqual(args.action, "install") self.assertTrue(args.tool_debug) @patch("sys.prefix", "amd_debug.s2idle") @patch("sys.base_prefix", "foo") def test_uninstall_command(self): """Test parse_args with uninstall command""" sys.argv = ["s2idle.py", "uninstall", "--tool-debug"] args = parse_args() self.assertEqual(args.action, "uninstall") self.assertTrue(args.tool_debug) @patch("sys.prefix", "amd_debug.s2idle") @patch("sys.base_prefix", "amd_debug.s2idle") @patch("sys.stderr") def test_hidden_install_command(self, _mock_print): """Test parse_args with install command""" sys.argv = ["s2idle.py", "install", "--tool-debug"] with self.assertRaises(SystemExit): parse_args() def test_version_command(self): """Test parse_args with version command""" sys.argv = ["s2idle.py", "--version"] args = parse_args() self.assertTrue(args.version) class TestMainFunction(unittest.TestCase): """Test main function""" @classmethod def setUpClass(cls): logging.basicConfig(filename="/dev/null", level=logging.DEBUG) def setUp(self): self.default_sys_argv = sys.argv def tearDown(self): sys.argv = self.default_sys_argv @patch("amd_debug.s2idle.relaunch_sudo") @patch("amd_debug.s2idle.install") def test_main_install(self, mock_install, mock_relaunch_sudo): """Test main function with install action""" sys.argv = ["s2idle.py", "install", "--tool-debug"] with patch("amd_debug.s2idle.parse_args") as mock_parse_args: mock_parse_args.return_value = argparse.Namespace( action="install", tool_debug=True ) main() mock_relaunch_sudo.assert_called_once() mock_install.assert_called_once_with(True) @patch("amd_debug.s2idle.relaunch_sudo") @patch("amd_debug.s2idle.uninstall") def test_main_uninstall(self, mock_uninstall, mock_relaunch_sudo): """Test main function with uninstall action""" sys.argv = ["s2idle.py", "uninstall", "--tool-debug"] with patch("amd_debug.s2idle.parse_args") as mock_parse_args: mock_parse_args.return_value = argparse.Namespace( action="uninstall", tool_debug=True ) main() mock_relaunch_sudo.assert_called_once() mock_uninstall.assert_called_once_with(True) @patch("amd_debug.s2idle.report") def test_main_report(self, mock_report): """Test main function with report action""" sys.argv = ["s2idle.py", "report", "--since", "2023-01-01"] with patch("amd_debug.s2idle.parse_args") as mock_parse_args: mock_parse_args.return_value = argparse.Namespace( action="report", since="2023-01-01", until="2023-02-01", report_file=None, format="html", tool_debug=False, report_debug=False, ) mock_report.return_value = True result = main() mock_report.assert_called_once_with( "2023-01-01", "2023-02-01", None, "html", False, False ) self.assertIsNone(result) @patch("amd_debug.s2idle.relaunch_sudo") @patch("amd_debug.s2idle.run_test_cycle") def test_main_run_test_cycle(self, mock_test, mock_relaunch_sudo): """Test main function with test action""" sys.argv = ["s2idle.py", "test", "--count", "5"] with patch("amd_debug.s2idle.parse_args") as mock_parse_args: mock_parse_args.return_value = argparse.Namespace( action="test", duration=None, wait=None, count="5", format="txt", report_file=None, force=False, tool_debug=False, random=False, logind=False, bios_debug=False, ) mock_test.return_value = True result = main() mock_relaunch_sudo.assert_called_once() mock_test.assert_called_once_with( None, None, "5", "txt", None, False, False, False, False, False ) self.assertIsNone(result) @patch("amd_debug.s2idle.version") def test_main_version(self, mock_version): """Test main function with version action""" sys.argv = ["s2idle.py", "version"] with patch("amd_debug.s2idle.parse_args") as mock_parse_args: mock_parse_args.return_value = argparse.Namespace(version=True, action=None) mock_version.return_value = "1.0.0" with patch("builtins.print") as mock_print: result = main() mock_version.assert_called_once() mock_print.assert_called_once_with("1.0.0") self.assertIsNone(result) def test_main_no_action(self): """Test main function with no action specified""" sys.argv = ["s2idle.py"] with patch("amd_debug.s2idle.parse_args") as mock_parse_args: mock_parse_args.return_value = argparse.Namespace( version=False, action=None ) with self.assertRaises(SystemExit) as cm: main() self.assertEqual(cm.exception.code, "no action specified") @patch("amd_debug.s2idle.Installer") def test_uninstall_success(self, mock_installer): """Test uninstall function when removal is successful""" mock_app = mock_installer.return_value mock_app.remove.return_value = True uninstall(debug=True) mock_installer.assert_called_once_with(tool_debug=True) mock_app.remove.assert_called_once() class TestInstallFunction(unittest.TestCase): """Test main function""" @patch("amd_debug.s2idle.Installer") @patch("sys.exit") def test_uninstall_failure(self, mock_sys_exit, mock_installer): """Test uninstall function when removal fails""" mock_app = mock_installer.return_value mock_app.remove.return_value = False uninstall(debug=True) mock_installer.assert_called_once_with(tool_debug=True) mock_app.remove.assert_called_once() mock_sys_exit.assert_called_once_with("Failed to remove") @patch("amd_debug.s2idle.Installer") @patch("amd_debug.s2idle.PrerequisiteValidator") @patch("sys.exit") def test_install_success( self, mock_sys_exit, mock_prerequisite_validator, mock_installer ): """Test install function when installation is successful""" mock_installer_instance = mock_installer.return_value mock_installer_instance.install_dependencies.return_value = True mock_installer_instance.install.return_value = True mock_prerequisite_instance = mock_prerequisite_validator.return_value mock_prerequisite_instance.run.return_value = True install(debug=True) mock_installer.assert_called_once_with(tool_debug=True) mock_installer_instance.set_requirements.assert_called_once_with( "iasl", "ethtool", "edid-decode" ) mock_installer_instance.install_dependencies.assert_called_once() mock_prerequisite_validator.assert_called_once_with(True) mock_prerequisite_instance.run.assert_called_once() mock_installer_instance.install.assert_called_once() mock_sys_exit.assert_not_called() @patch("amd_debug.s2idle.Installer") def test_install_dependencies_failure(self, mock_installer): """Test install function when dependency installation fails""" mock_installer_instance = mock_installer.return_value mock_installer_instance.install_dependencies.return_value = False with self.assertRaises(SystemExit): install(debug=True) @patch("amd_debug.s2idle.Installer") @patch("amd_debug.s2idle.PrerequisiteValidator") @patch("sys.exit") def test_prerequisite_check_failure( self, mock_sys_exit, mock_prerequisite_validator, mock_installer ): """Test install function when prerequisite check fails""" mock_installer_instance = mock_installer.return_value mock_installer_instance.install_dependencies.return_value = True mock_prerequisite_instance = mock_prerequisite_validator.return_value mock_prerequisite_instance.run.return_value = False install(debug=True) mock_installer.assert_called_once_with(tool_debug=True) mock_installer_instance.set_requirements.assert_called_once_with( "iasl", "ethtool", "edid-decode" ) mock_installer_instance.install_dependencies.assert_called_once() mock_prerequisite_validator.assert_called_once_with(True) mock_prerequisite_instance.run.assert_called_once() mock_prerequisite_instance.report.assert_called_once() mock_sys_exit.assert_called_once_with("Failed to meet prerequisites") @patch("amd_debug.s2idle.Installer") @patch("amd_debug.s2idle.PrerequisiteValidator") @patch("sys.exit") def test_install_failure( self, mock_sys_exit, mock_prerequisite_validator, mock_installer ): """Test install function when installation fails""" mock_installer_instance = mock_installer.return_value mock_installer_instance.install_dependencies.return_value = True mock_installer_instance.install.return_value = False mock_prerequisite_instance = mock_prerequisite_validator.return_value mock_prerequisite_instance.run.return_value = True install(debug=True) mock_installer.assert_called_once_with(tool_debug=True) mock_installer_instance.set_requirements.assert_called_once_with( "iasl", "ethtool", "edid-decode" ) mock_installer_instance.install_dependencies.assert_called_once() mock_prerequisite_validator.assert_called_once_with(True) mock_prerequisite_instance.run.assert_called_once() mock_installer_instance.install.assert_called_once() mock_sys_exit.assert_called_once_with("Failed to install") class TestTestFunction(unittest.TestCase): """Test the test function""" @classmethod def setUpClass(cls): logging.basicConfig(filename="/dev/null", level=logging.DEBUG) @patch("amd_debug.s2idle.Installer") @patch("amd_debug.s2idle.PrerequisiteValidator") @patch("amd_debug.s2idle.SleepValidator") @patch("amd_debug.s2idle.SleepReport") @patch("amd_debug.s2idle.prompt_test_arguments") @patch("amd_debug.s2idle.prompt_report_arguments") @patch("amd_debug.s2idle.display_report_file") @patch("amd_debug.s2idle.datetime") def test_test_success( self, mock_datetime, mock_display_report_file, mock_prompt_report_arguments, mock_prompt_test_arguments, mock_sleep_report, mock_sleep_validator, mock_prerequisite_validator, mock_installer, ): """Test the test function when everything succeeds""" from datetime import datetime mock_datetime.now.return_value = datetime(2023, 2, 1, 0, 0, 0) mock_datetime.side_effect = lambda *args, **kwargs: datetime(*args, **kwargs) mock_installer_instance = mock_installer.return_value mock_installer_instance.install_dependencies.return_value = True mock_prerequisite_instance = mock_prerequisite_validator.return_value mock_prerequisite_instance.run.return_value = True mock_prompt_test_arguments.return_value = (10, 5, 3) mock_prompt_report_arguments.return_value = ( "2023-01-01", "2023-02-01", "report.html", "html", True, ) mock_sleep_validator_instance = mock_sleep_validator.return_value mock_sleep_report_instance = mock_sleep_report.return_value result = run_test_cycle( duration=None, wait=None, count=None, fmt=None, fname=None, force=False, debug=True, rand=False, logind=False, bios_debug=False, ) mock_installer.assert_called_once_with(tool_debug=True) mock_installer_instance.set_requirements.assert_called_once_with( "iasl", "ethtool", "edid-decode" ) mock_installer_instance.install_dependencies.assert_called_once() mock_prerequisite_validator.assert_called_once_with(True) mock_prerequisite_instance.run.assert_called_once() mock_prerequisite_instance.report.assert_called_once() mock_prompt_test_arguments.assert_called_once_with(None, None, None, False) mock_prompt_report_arguments.assert_called_once() mock_sleep_validator.assert_called_once_with(tool_debug=True, bios_debug=False) mock_sleep_validator_instance.run.assert_called_once_with( duration=10, wait=5, count=3, rand=False, logind=False ) from datetime import datetime mock_sleep_report.assert_called_once_with( since="2023-01-01", until=datetime(2023, 2, 1, 0, 0, 0), fname="report.html", fmt="html", tool_debug=True, report_debug=True, ) mock_sleep_report_instance.run.assert_called_once() mock_display_report_file.assert_called_once_with("report.html", "html") self.assertTrue(result) @patch("amd_debug.s2idle.Installer") @patch("builtins.print") def test_test_install_dependencies_failure(self, _mock_print, mock_installer): """Test the test function when dependency installation fails""" mock_installer_instance = mock_installer.return_value mock_installer_instance.install_dependencies.return_value = False result = run_test_cycle( duration=None, wait=None, count=None, fmt=None, fname=None, force=False, debug=True, rand=False, logind=False, bios_debug=False, ) mock_installer.assert_called_once_with(tool_debug=True) mock_installer_instance.set_requirements.assert_called_once_with( "iasl", "ethtool", "edid-decode" ) mock_installer_instance.install_dependencies.assert_called_once() self.assertFalse(result) @patch("amd_debug.s2idle.Installer") @patch("amd_debug.s2idle.PrerequisiteValidator") @patch("amd_debug.s2idle.SleepValidator") @patch("amd_debug.s2idle.SleepReport") @patch("amd_debug.s2idle.prompt_test_arguments") @patch("amd_debug.s2idle.prompt_report_arguments") @patch("amd_debug.s2idle.display_report_file") def test_test_prerequisite_failure( self, mock_display_report_file, mock_prompt_report_arguments, mock_prompt_test_arguments, mock_sleep_report, mock_sleep_validator, mock_prerequisite_validator, mock_installer, ): """Test the test function when prerequisite check fails""" mock_installer_instance = mock_installer.return_value mock_installer_instance.install_dependencies.return_value = True mock_prerequisite_instance = mock_prerequisite_validator.return_value mock_prerequisite_instance.run.return_value = False mock_prompt_test_arguments.return_value = (10, 5, 3) mock_prompt_report_arguments.return_value = ( "2023-01-01", "2023-02-01", "report.html", "html", True, ) mock_sleep_validator_instance = mock_sleep_validator.return_value mock_sleep_report_instance = mock_sleep_report.return_value result = run_test_cycle( duration=None, wait=None, count=None, fmt=None, fname=None, force=False, debug=True, rand=False, logind=False, bios_debug=False, ) mock_installer.assert_called_once_with(tool_debug=True) mock_installer_instance.set_requirements.assert_called_once_with( "iasl", "ethtool", "edid-decode" ) mock_installer_instance.install_dependencies.assert_called_once() mock_prerequisite_validator.assert_called_once_with(True) mock_prerequisite_instance.run.assert_called_once() mock_prerequisite_instance.report.assert_called_once() mock_prompt_test_arguments.assert_called_once_with(None, None, None, False) mock_prompt_report_arguments.assert_called_once() mock_sleep_validator_instance.assert_not_called() mock_sleep_report.assert_called_once_with( since=None, until=None, fname="report.html", fmt="html", tool_debug=True, report_debug=True, ) mock_sleep_report_instance.run.assert_called_once() mock_display_report_file.assert_called_once_with("report.html", "html") self.assertTrue(result) @patch("amd_debug.s2idle.Installer") @patch("amd_debug.s2idle.PrerequisiteValidator") @patch("amd_debug.s2idle.prompt_test_arguments", side_effect=KeyboardInterrupt) @patch("amd_debug.prerequisites.SleepDatabase") @patch("amd_debug.validator.SleepDatabase") def test_test_keyboard_interrupt( self, _mock_sleep_db_validator, _mock_sleep_db_prerequisite, mock_prompt_test_arguments, mock_prerequisite_validator, mock_installer, ): """Test the test function when interrupted by the user""" mock_installer_instance = mock_installer.return_value mock_installer_instance.install_dependencies.return_value = True mock_prerequisite_instance = mock_prerequisite_validator.return_value mock_prerequisite_instance.run.return_value = True with self.assertRaises(SystemExit): run_test_cycle( duration=None, wait=None, count=None, fmt=None, fname=None, force=False, debug=True, rand=False, logind=False, bios_debug=False, ) mock_installer.assert_called_once_with(tool_debug=True) mock_installer_instance.set_requirements.assert_called_once_with( "iasl", "ethtool", "edid-decode" ) mock_installer_instance.install_dependencies.assert_called_once() mock_prerequisite_validator.assert_not_called() mock_prerequisite_instance.run.assert_not_called() mock_prerequisite_instance.report.assert_not_called() mock_prompt_test_arguments.assert_called_once_with(None, None, None, False) class TestDisplayReportFile(unittest.TestCase): """Test display_report_file function""" @patch("amd_debug.s2idle.is_root", return_value=False) @patch("subprocess.call") def test_display_report_file_non_html(self, mock_subprocess_call, mock_is_root): """Test display_report_file when format is not html""" display_report_file("report.txt", "txt") mock_is_root.assert_not_called() mock_subprocess_call.assert_not_called() @patch("amd_debug.s2idle.is_root", return_value=False) @patch("subprocess.call") def test_display_report_file_html_non_root( self, mock_subprocess_call, mock_is_root ): """Test display_report_file when format is html and user is not root""" display_report_file("report.html", "html") mock_is_root.assert_called_once() mock_subprocess_call.assert_called_once_with(["xdg-open", "report.html"]) @patch("amd_debug.s2idle.is_root", return_value=True) @patch( "os.environ.get", side_effect=lambda key: "testuser" if key == "SUDO_USER" else "foo", ) @patch("subprocess.call") def test_display_report_file_html_root_with_user( self, mock_subprocess_call, mock_env_get, mock_is_root ): """Test display_report_file when format is html, user is root, and SUDO_USER is set""" display_report_file("report.html", "html") mock_is_root.assert_called_once() mock_env_get.assert_any_call("SUDO_USER") call_args = mock_subprocess_call.call_args[0][0] assert call_args[0] == "sudo" assert call_args[1] == "-u" assert call_args[2] == "testuser" assert call_args[3] == "env" assert "xdg-open" in call_args assert "report.html" in call_args @patch("amd_debug.s2idle.is_root", return_value=True) @patch("os.environ.get", side_effect=lambda key: None) @patch("builtins.print") def test_display_report_file_html_root_without_user( self, mock_print, mock_env_get, mock_is_root ): """Test display_report_file when format is html, user is root, and SUDO_USER is not set""" display_report_file("report.html", "html") mock_is_root.assert_called_once() mock_env_get.assert_any_call("SUDO_USER") mock_print.assert_not_called() class TestReportFunction(unittest.TestCase): """Test the report function""" @patch("amd_debug.s2idle.prompt_report_arguments") @patch("amd_debug.s2idle.SleepReport") @patch("amd_debug.s2idle.display_report_file") def test_report_success( self, mock_display_report_file, mock_sleep_report, mock_prompt_report_arguments ): """Test the report function when everything succeeds""" mock_prompt_report_arguments.return_value = ( "2023-01-01", "2023-02-01", "report.html", "html", True, ) mock_sleep_report_instance = mock_sleep_report.return_value result = report( since=None, until=None, fname=None, fmt=None, tool_debug=True, report_debug=None, ) mock_prompt_report_arguments.assert_called_once_with( None, None, None, None, None ) mock_sleep_report.assert_called_once_with( since="2023-01-01", until="2023-02-01", fname="report.html", fmt="html", tool_debug=True, report_debug=True, ) mock_sleep_report_instance.run.assert_called_once() mock_display_report_file.assert_called_once_with("report.html", "html") self.assertTrue(result) @patch("amd_debug.s2idle.prompt_report_arguments", side_effect=KeyboardInterrupt) def test_report_keyboard_interrupt(self, mock_prompt_report_arguments): """Test the report function when interrupted by the user""" with self.assertRaises(SystemExit): report( since=None, until=None, fname=None, fmt=None, tool_debug=True, report_debug=None, ) mock_prompt_report_arguments.assert_called_once_with( None, None, None, None, None ) @patch("amd_debug.s2idle.prompt_report_arguments") @patch( "amd_debug.s2idle.SleepReport", side_effect=sqlite3.OperationalError("DB error") ) @patch("builtins.print") def test_report_sqlite_error( self, _mock_print, mock_sleep_report, mock_prompt_report_arguments ): """Test the report function when a SQLite error occurs""" mock_prompt_report_arguments.return_value = ( "2023-01-01", "2023-02-01", "report.html", "html", True, ) result = report( since=None, until=None, fname=None, fmt=None, tool_debug=True, report_debug=None, ) mock_prompt_report_arguments.assert_called_once_with( None, None, None, None, None ) mock_sleep_report.assert_called_once_with( since="2023-01-01", until="2023-02-01", fname="report.html", fmt="html", tool_debug=True, report_debug=True, ) self.assertFalse(result) @patch("amd_debug.s2idle.prompt_report_arguments") @patch( "amd_debug.s2idle.SleepReport", side_effect=PermissionError("Permission denied") ) @patch("builtins.print") def test_report_permission_error( self, _mock_print, mock_sleep_report, mock_prompt_report_arguments ): """Test the report function when a PermissionError occurs""" mock_prompt_report_arguments.return_value = ( "2023-01-01", "2023-02-01", "report.html", "html", True, ) result = report( since=None, until=None, fname=None, fmt=None, tool_debug=True, report_debug=None, ) mock_prompt_report_arguments.assert_called_once_with( None, None, None, None, None ) mock_sleep_report.assert_called_once_with( since="2023-01-01", until="2023-02-01", fname="report.html", fmt="html", tool_debug=True, report_debug=True, ) self.assertFalse(result) @patch("amd_debug.s2idle.prompt_report_arguments") @patch("amd_debug.s2idle.SleepReport") @patch("builtins.print") def test_report_run_error( self, _mock_print, mock_sleep_report, mock_prompt_report_arguments ): """Test the report function when an error occurs during app.run()""" mock_prompt_report_arguments.return_value = ( "2023-01-01", "2023-02-01", "report.html", "html", True, ) mock_sleep_report_instance = mock_sleep_report.return_value mock_sleep_report_instance.run.side_effect = ValueError("Invalid value") result = report( since=None, until=None, fname=None, fmt=None, tool_debug=True, report_debug=None, ) mock_prompt_report_arguments.assert_called_once_with( None, None, None, None, None ) mock_sleep_report.assert_called_once_with( since="2023-01-01", until="2023-02-01", fname="report.html", fmt="html", tool_debug=True, report_debug=True, ) mock_sleep_report_instance.run.assert_called_once() self.assertFalse(result) class TestPromptReportArguments(unittest.TestCase): """Test prompt_report_arguments function""" @patch("builtins.input", side_effect=["2023-01-01", "2023-02-01", "html", "true"]) @patch("amd_debug.s2idle.get_report_file", return_value="report.html") @patch("amd_debug.s2idle.get_report_format", return_value="html") def test_prompt_report_arguments_success( self, mock_get_report_format, mock_get_report_file, _mock_input ): """Test prompt_report_arguments with valid inputs""" result = prompt_report_arguments(None, None, None, None, None) self.assertEqual(result[0], datetime(2023, 1, 1)) self.assertEqual(result[1], datetime(2023, 2, 1)) self.assertEqual(result[2], "report.html") self.assertEqual(result[3], "html") mock_get_report_file.assert_called_once_with(None, "html") mock_get_report_format.assert_called_once() @patch( "builtins.input", side_effect=["invalid-date", "2023-01-01", "2023-02-01", "html", "true"], ) @patch("sys.exit") def test_prompt_report_arguments_invalid_since_date(self, mock_exit, mock_input): """Test prompt_report_arguments with invalid 'since' date""" mock_exit.side_effect = SystemExit with self.assertRaises(SystemExit): prompt_report_arguments(None, None, None, None, None) mock_exit.assert_called_once_with( "Invalid date, use YYYY-MM-DD: Invalid isoformat string: 'invalid-date'" ) @patch( "builtins.input", side_effect=["2023-01-01", "invalid-date", "2023-02-01", "html", "true"], ) @patch("sys.exit") def test_prompt_report_arguments_invalid_until_date(self, mock_exit, mock_input): """Test prompt_report_arguments with invalid 'until' date""" mock_exit.side_effect = SystemExit with self.assertRaises(SystemExit): prompt_report_arguments(None, None, None, None, None) mock_exit.assert_called_once_with( "Invalid date, use YYYY-MM-DD: Invalid isoformat string: 'invalid-date'" ) @patch( "builtins.input", side_effect=["2023-01-01", "2023-02-01", "invalid-format", "true"], ) @patch("amd_debug.s2idle.get_report_format", return_value="html") @patch("sys.exit") def test_prompt_report_arguments_invalid_format( self, mock_exit, mock_get_report_format, mock_input ): """Test prompt_report_arguments with invalid format""" mock_exit.side_effect = SystemExit with self.assertRaises(SystemExit): prompt_report_arguments(None, None, None, None, None) mock_exit.assert_called_once_with("Invalid format: invalid-format") mock_get_report_format.assert_called_once() @patch( "builtins.input", side_effect=["2023-01-01", "2023-02-01", "html", "foo_the_bar"], ) @patch("amd_debug.s2idle.get_report_format", return_value="html") @patch("sys.exit") def test_prompt_report_arguments_invalid_report( self, mock_exit, mock_get_report_format, mock_input ): """Test prompt_report_arguments with invalid format""" mock_exit.side_effect = SystemExit with self.assertRaises(SystemExit): prompt_report_arguments(None, None, None, None, None) mock_exit.assert_called_once_with("Invalid entry: Foo_the_bar") mock_get_report_format.assert_called_once() @patch("builtins.input", side_effect=["", "", "", ""]) @patch( "amd_debug.s2idle.get_report_file", return_value="amd-s2idle-report-2023-01-01.html", ) @patch("amd_debug.s2idle.get_report_format", return_value="html") def test_prompt_report_arguments_defaults( self, mock_get_report_format, mock_get_report_file, mock_input ): """Test prompt_report_arguments with default values""" result = prompt_report_arguments(None, None, None, None, None) self.assertEqual(datetime.date(result[0]), Defaults.since) self.assertEqual(datetime.date(result[1]), Defaults.until) self.assertEqual(result[2], "amd-s2idle-report-2023-01-01.html") self.assertEqual(result[3], "html") self.assertEqual(result[4], True) mock_get_report_file.assert_called_once_with(None, "html") mock_get_report_format.assert_called() amd-debug-tools-0.2.15/src/test_sleep_report.py000066400000000000000000000154121515405217400214740ustar00rootroot00000000000000#!/usr/bin/python3 # SPDX-License-Identifier: MIT """ This module contains unit tests for the s2idle tool in the amd-debug-tools package. """ import math import unittest from datetime import datetime from unittest.mock import patch import pandas as pd from amd_debug.sleep_report import ( remove_duplicates, format_gpio_as_str, format_irq_as_str, format_as_human, format_as_seconds, format_watts, format_percent, format_timedelta, parse_hw_sleep, SleepReport, ) from amd_debug.wake import WakeGPIO, WakeIRQ class TestSleepReportUtils(unittest.TestCase): """Unit tests for the sleep report utilities.""" def test_remove_duplicates(self): """Test the remove_duplicates function.""" self.assertEqual(remove_duplicates("1, 2, 2, 3"), [1, 2, 3]) self.assertEqual(remove_duplicates("4 4 5 6"), [4, 5, 6]) self.assertEqual(remove_duplicates(""), []) def test_format_gpio_as_str(self): """Test the format_gpio_as_str function.""" self.assertEqual(format_gpio_as_str("1, 2, 2, 3"), "1, 2, 3") self.assertEqual(format_gpio_as_str("4 4 5 6"), "4, 5, 6") self.assertEqual(format_gpio_as_str(""), "") @patch("amd_debug.wake.read_file") @patch("os.path.exists") @patch("os.listdir") @patch("os.walk") def test_format_irq_as_str( self, _mock_os_walk, _mock_os_listdir, mock_os_path_exists, mock_read_file ): """Test the format_irq_as_str function.""" mock_read_file.side_effect = lambda path: { "/sys/kernel/irq/20/chip_name": "", "/sys/kernel/irq/20/actions": "", "/sys/kernel/irq/20/wakeup": "disabled", }.get(path, "") # Mocking os.path.exists mock_os_path_exists.return_value = False self.assertEqual(format_irq_as_str("20"), "Disabled interrupt") self.assertEqual(format_irq_as_str(""), "") def test_format_as_human(self): """Test the format_as_human function.""" self.assertEqual( format_as_human("20231010123045"), datetime(2023, 10, 10, 12, 30, 45), ) with self.assertRaises(ValueError): format_as_human("invalid_date") def test_format_as_seconds(self): """Test the format_as_seconds function.""" self.assertEqual( format_as_seconds("20231010123045"), datetime(2023, 10, 10, 12, 30, 45).timestamp(), ) with self.assertRaises(ValueError): format_as_seconds("invalid_date") def test_format_watts(self): """Test the format_watts function.""" self.assertEqual(format_watts(12.3456), "12.35W") self.assertEqual(format_watts(0), "0.00W") def test_format_percent(self): """Test the format_percent function.""" self.assertEqual(format_percent(12.3456), "12.35%") self.assertEqual(format_percent(0), "0.00%") def test_format_timedelta(self): """Test the format_timedelta function.""" self.assertEqual(format_timedelta(3600), "1:00:00") self.assertEqual(format_timedelta(3661), "1:01:01") def test_parse_hw_sleep(self): """Test the parse_hw_sleep function.""" self.assertEqual(parse_hw_sleep(0.5), 50) self.assertEqual(parse_hw_sleep(1.0), 100) self.assertEqual(parse_hw_sleep(1.5), 0) class TestSleepReport(unittest.TestCase): """Unit tests for the SleepReport class.""" @patch("amd_debug.sleep_report.SleepDatabase") def setUp(self, MockSleepDatabase): """Set up a mock SleepReport instance for testing.""" self.mock_db = MockSleepDatabase.return_value self.mock_db.report_summary_dataframe.return_value = pd.DataFrame( { "t0": [datetime(2023, 10, 10, 12, 0, 0).strftime("%Y%m%d%H%M%S")], "t1": [datetime(2023, 10, 10, 12, 30, 0).strftime("%Y%m%d%H%M%S")], "hw": [50], "requested": [1], "gpio": ["1, 2"], "wake_irq": ["1"], "b0": [90], "b1": [85], "full": [100], } ) self.since = datetime(2023, 10, 9, 0, 0, 0) self.until = datetime(2023, 10, 12, 0, 0, 0) self.report = SleepReport( since=self.since, until=self.until, fname=None, fmt="txt", tool_debug=False, report_debug=False, ) def test_analyze_duration(self): """Test the analyze_duration method.""" self.report.analyze_duration( index=0, t0=datetime(2023, 10, 10, 12, 0, 0), t1=datetime(2023, 10, 10, 12, 30, 0), requested=20, hw=50, ) self.assertEqual(len(self.report.failures), 2) @patch("amd_debug.sleep_report.Environment") @patch("amd_debug.sleep_report.FileSystemLoader") def test_build_template(self, _mock_fsl, mock_env): """Test the build_template method.""" mock_template = mock_env.return_value.get_template.return_value mock_template.render.return_value = "Rendered Template" result = self.report.build_template(inc_prereq=False) self.assertEqual(result, "Rendered Template") @patch("matplotlib.pyplot.savefig") def test_build_battery_chart(self, mock_savefig): """Test the build_battery_chart method.""" self.report.build_battery_chart() self.assertIsNotNone(self.report.battery_svg) mock_savefig.assert_called_once() @patch("matplotlib.pyplot.savefig") def test_build_hw_sleep_chart(self, mock_savefig): """Test the build_hw_sleep_chart method.""" self.report.build_hw_sleep_chart() self.assertIsNotNone(self.report.hwsleep_svg) mock_savefig.assert_called_once() def test_pre_process_dataframe_zero_duration(self): """Test the pre_process_dataframe method when t0 and t1 are the same.""" # Mock the dataframe with t0 and t1 being the same self.report.df = pd.DataFrame( { "t0": [datetime(2023, 10, 10, 12, 0, 0).strftime("%Y%m%d%H%M%S")], "t1": [datetime(2023, 10, 10, 12, 0, 0).strftime("%Y%m%d%H%M%S")], "hw": [50], "requested": [1], "gpio": ["1, 2"], "wake_irq": ["1"], "b0": [90], "b1": [85], "full": [100], } ) # Call the method self.report.pre_process_dataframe() # Verify the dataframe was processed correctly self.assertTrue( self.report.df["Duration"].isna().iloc[0] ) # Duration should be NaN self.assertTrue( math.isnan(self.report.df["Hardware Sleep"].iloc[0]) ) # Hardware Sleep should be NaN amd-debug-tools-0.2.15/src/test_ttm.py000066400000000000000000000244231515405217400175770ustar00rootroot00000000000000#!/usr/bin/python3 # SPDX-License-Identifier: MIT """ This module contains unit tests for the ttm tool in the amd-debug-tools package. """ import unittest import sys import logging from unittest import mock from amd_debug.ttm import main, parse_args, AmdTtmTool, maybe_reboot class TestParseArgs(unittest.TestCase): """Test parse_args function""" @classmethod def setUpClass(cls): logging.basicConfig(filename="/dev/null", level=logging.DEBUG) def setUp(self): self.default_sys_argv = sys.argv def tearDown(self): sys.argv = self.default_sys_argv @mock.patch.object(sys, "argv", new=["ttm", "--version"]) def test_parse_args_version(self): """Test version argument""" args = parse_args() self.assertTrue(args.version) self.assertFalse(args.set) self.assertFalse(args.clear) @mock.patch.object(sys, "argv", new=["ttm", "--clear"]) def test_parse_args_clear(self): """Test clear argument""" args = parse_args() self.assertFalse(args.version) self.assertFalse(args.set) self.assertTrue(args.clear) class TestMainFunction(unittest.TestCase): """Test main() function logic""" @mock.patch("amd_debug.ttm.parse_args") @mock.patch("amd_debug.ttm.version", return_value="1.2.3") @mock.patch("builtins.print") def test_main_version(self, mock_print, _mock_version, mock_parse_args): """Test main function with version argument""" mock_parse_args.return_value = mock.Mock( version=True, set=None, clear=False, tool_debug=False ) ret = main() mock_print.assert_called_with("1.2.3") self.assertIsNone(ret) @mock.patch("amd_debug.ttm.parse_args") @mock.patch("amd_debug.ttm.AmdTtmTool") @mock.patch("builtins.print") def test_main_set_invalid(self, mock_print, _mock_tool, mock_parse_args): """Test main function with invalid set argument""" mock_parse_args.return_value = mock.Mock( version=False, set=0, clear=False, tool_debug=False ) ret = main() mock_print.assert_called_with("Error: GB value must be greater than 0") self.assertEqual(ret, 1) @mock.patch("amd_debug.ttm.parse_args") @mock.patch("amd_debug.ttm.AmdTtmTool") def test_main_set_valid(self, mock_tool, mock_parse_args): """Test main function with set argument""" instance = mock_tool.return_value instance.set.return_value = True mock_parse_args.return_value = mock.Mock( version=False, set=2, clear=False, tool_debug=False ) ret = main() instance.set.assert_called_with(2) self.assertIsNone(ret) @mock.patch("amd_debug.ttm.parse_args") @mock.patch("amd_debug.ttm.AmdTtmTool") def test_main_set_failed(self, mock_tool, mock_parse_args): instance = mock_tool.return_value instance.set.return_value = False mock_parse_args.return_value = mock.Mock( version=False, set=2, clear=False, tool_debug=False ) ret = main() instance.set.assert_called_with(2) self.assertEqual(ret, 1) @mock.patch("amd_debug.ttm.parse_args") @mock.patch("amd_debug.ttm.AmdTtmTool") def test_main_clear_success(self, mock_tool, mock_parse_args): """Test main function with clear argument""" instance = mock_tool.return_value instance.clear.return_value = True mock_parse_args.return_value = mock.Mock( version=False, set=None, clear=True, tool_debug=False ) ret = main() instance.clear.assert_called_once() self.assertIsNone(ret) @mock.patch("amd_debug.ttm.parse_args") @mock.patch("amd_debug.ttm.AmdTtmTool") def test_main_clear_failed(self, mock_tool, mock_parse_args): """Test main function with clear argument failure""" instance = mock_tool.return_value instance.clear.return_value = False mock_parse_args.return_value = mock.Mock( version=False, set=None, clear=True, tool_debug=False ) ret = main() instance.clear.assert_called_once() self.assertEqual(ret, 1) @mock.patch("amd_debug.ttm.parse_args") @mock.patch("amd_debug.ttm.AmdTtmTool") def test_main_get_success(self, mock_tool, mock_parse_args): """Test main function with get argument""" instance = mock_tool.return_value instance.get.return_value = True mock_parse_args.return_value = mock.Mock( version=False, set=None, clear=False, tool_debug=False ) ret = main() instance.get.assert_called_once() self.assertIsNone(ret) @mock.patch("amd_debug.ttm.parse_args") @mock.patch("amd_debug.ttm.AmdTtmTool") def test_main_get_failed(self, mock_tool, mock_parse_args): """Test main function with get argument failure""" instance = mock_tool.return_value instance.get.return_value = False mock_parse_args.return_value = mock.Mock( version=False, set=None, clear=False, tool_debug=False ) ret = main() instance.get.assert_called_once() self.assertEqual(ret, 1) class TestMaybeReboot(unittest.TestCase): """Test maybe_reboot function""" @mock.patch("builtins.input", return_value="y") @mock.patch("amd_debug.ttm.reboot", return_value=True) def test_maybe_reboot_yes(self, mock_reboot, _mock_input): """Test reboot confirmation and execution""" result = maybe_reboot() mock_reboot.assert_called_once() self.assertTrue(result) @mock.patch("builtins.input", return_value="n") @mock.patch("amd_debug.ttm.reboot", return_value=True) def test_maybe_reboot_no(self, mock_reboot, _mock_input): """Test reboot confirmation without execution""" result = maybe_reboot() mock_reboot.assert_not_called() self.assertTrue(result) class TestAmdTtmTool(unittest.TestCase): """Unit tests for AmdTtmTool class""" def setUp(self): self.tool = AmdTtmTool(logging=False) @mock.patch("builtins.open", new_callable=mock.mock_open, read_data="4096") @mock.patch("amd_debug.ttm.bytes_to_gb", return_value=4.0) @mock.patch("amd_debug.ttm.print_color") @mock.patch("amd_debug.ttm.get_system_mem", return_value=16.0) def test_get_success(self, _mock_mem, mock_print, _mock_bytes_to_gb, mock_open): """Test get() when TTM_PARAM_PATH exists""" result = self.tool.get() mock_open.assert_called_once_with( "/sys/module/ttm/parameters/pages_limit", "r", encoding="utf-8" ) mock_print.assert_any_call( "Current TTM pages limit: 4096 pages (4.00 GB)", "πŸ’»" ) mock_print.assert_any_call("Total system memory: 16.00 GB", "πŸ’»") self.assertTrue(result) @mock.patch("builtins.open", side_effect=FileNotFoundError) @mock.patch("amd_debug.ttm.print_color") def test_get_file_not_found(self, mock_print, _mock_open): """Test get() when TTM_PARAM_PATH does not exist""" result = self.tool.get() mock_print.assert_called_with( "Error: Could not find /sys/module/ttm/parameters/pages_limit", "❌" ) self.assertFalse(result) @mock.patch("amd_debug.ttm.relaunch_sudo", return_value=True) @mock.patch("amd_debug.ttm.get_system_mem", return_value=8.0) @mock.patch("amd_debug.ttm.print_color") def test_set_gb_greater_than_total( self, mock_print, _mock_mem, _mock_relaunch_sudo ): """Test set() when gb_value > total system memory""" result = self.tool.set(16) mock_print.assert_any_call( "16.00 GB is greater than total system memory (8.00 GB)", "❌" ) self.assertFalse(result) @mock.patch("amd_debug.ttm.relaunch_sudo", return_value=True) @mock.patch("amd_debug.ttm.get_system_mem", return_value=10.0) @mock.patch("amd_debug.ttm.print_color") @mock.patch("builtins.input", return_value="n") def test_set_gb_exceeds_max_percentage_cancel( self, _mock_input, mock_print, _mock_mem, mock_relaunch_sudo ): """Test set() when gb_value exceeds max percentage and user cancels""" result = self.tool.set(9.5) self.assertFalse(result) mock_print.assert_any_call("Operation cancelled.", "🚦") @mock.patch("amd_debug.ttm.relaunch_sudo", return_value=True) @mock.patch("amd_debug.ttm.get_system_mem", return_value=10.0) @mock.patch("amd_debug.ttm.gb_to_pages", return_value=20480) @mock.patch("amd_debug.ttm.print_color") @mock.patch("builtins.open", new_callable=mock.mock_open) @mock.patch("builtins.input", return_value="y") @mock.patch("amd_debug.ttm.maybe_reboot", return_value=True) def test_set_success( self, _mock_reboot, _mock_input, mock_open, mock_print, _mock_gb_to_pages, _mock_mem, _relaunch_sudo, ): """Test set() success path""" result = self.tool.set(5) mock_open.assert_called_once_with( "/etc/modprobe.d/ttm.conf", "w", encoding="utf-8" ) mock_print.assert_any_call( "Successfully set TTM pages limit to 20480 pages (5.00 GB)", "🐧" ) self.assertTrue(result) @mock.patch("os.path.exists", return_value=False) @mock.patch("amd_debug.ttm.print_color") def test_clear_file_not_exists(self, mock_print, _mock_exists): """Test clear() when config file does not exist""" result = self.tool.clear() mock_print.assert_called_with("/etc/modprobe.d/ttm.conf doesn't exist", "❌") self.assertFalse(result) @mock.patch("os.path.exists", return_value=True) @mock.patch("amd_debug.ttm.relaunch_sudo", return_value=True) @mock.patch("os.remove") @mock.patch("amd_debug.ttm.print_color") @mock.patch("amd_debug.ttm.maybe_reboot", return_value=True) def test_clear_success( self, _mock_reboot, mock_print, mock_remove, _mock_relaunch_sudo, _mock_exists ): """Test clear() success path""" result = self.tool.clear() mock_remove.assert_called_once_with("/etc/modprobe.d/ttm.conf") mock_print.assert_any_call( "Configuration /etc/modprobe.d/ttm.conf removed", "🐧" ) self.assertTrue(result) amd-debug-tools-0.2.15/src/test_validator.py000066400000000000000000001072221515405217400207570ustar00rootroot00000000000000#!/usr/bin/python3 # SPDX-License-Identifier: MIT """ This module contains unit tests for the validator functions in the amd-debug-tools package. """ from unittest.mock import patch, mock_open, Mock import os import logging import unittest import math from datetime import datetime from amd_debug.validator import pm_debugging, soc_needs_irq1_wa, SleepValidator class TestValidatorHelpers(unittest.TestCase): """Test validator Helper functions""" @classmethod def setUpClass(cls): logging.basicConfig(filename="/dev/null", level=logging.DEBUG) def test_soc_needs_irq1_wa(self): """Test if the SOC should apply an IRQ1 workaround""" ret = soc_needs_irq1_wa(0x17, 0x68, "1.2.3") self.assertTrue(ret) ret = soc_needs_irq1_wa(0x17, 0x69, "1.2.3") self.assertFalse(ret) ret = soc_needs_irq1_wa(0x19, 0x51, "64.65.0") self.assertFalse(ret) ret = soc_needs_irq1_wa(0x19, 0x50, "64.65.0") self.assertTrue(ret) ret = soc_needs_irq1_wa(0x19, 0x50, "64.66.0") self.assertFalse(ret) def test_pm_debugging(self): """Test pm_debugging decorator""" @pm_debugging def test_function(): return "Test function executed" # Mock /sys/power/pm_debug_messages existing and all ACPI existing with patch("amd_debug.validator.open", new_callable=mock_open, read_data="0") as mock_file: handlers = ( mock_file.return_value, mock_open(read_data="0").return_value, mock_open(read_data="0").return_value, mock_open(read_data="0").return_value, ) mock_open.side_effect = handlers result = test_function() self.assertEqual(result, "Test function executed") # Mock /sys/power/pm_debug_messages missing with patch( "amd_debug.validator.open", side_effect=FileNotFoundError("not found") ) as mock_file: with self.assertRaises(FileNotFoundError): result = test_function() class TestValidator(unittest.TestCase): """Test validator functions""" @classmethod def setUpClass(cls): logging.basicConfig(filename="/dev/null", level=logging.DEBUG) @patch("amd_debug.validator.SleepDatabase") @patch("subprocess.run") def setUp(self, _db_mock, _mock_run): """Set up a mock context for testing""" self.validator = SleepValidator(tool_debug=True, bios_debug=False) def test_capture_running_compositors(self): """Test capture_running_compositors method""" with patch("glob.glob", return_value=["/proc/1234", "/proc/5678"]), patch( "os.path.exists", return_value=True ), patch( "os.readlink", side_effect=["/usr/bin/kwin_wayland", "/usr/bin/gnome-shell"] ), patch.object( self.validator.db, "record_debug" ) as mock_record_debug: self.validator.capture_running_compositors() mock_record_debug.assert_any_call("kwin_wayland compositor is running") mock_record_debug.assert_any_call("gnome-shell compositor is running") def test_capture_power_profile(self): """Test capture_power_profile method""" with patch("os.path.exists", return_value=True), patch( "subprocess.check_output", return_value=b"Performance\nBalanced\nPower Saver", ), patch.object(self.validator.db, "record_debug") as mock_record_debug: self.validator.capture_power_profile() mock_record_debug.assert_any_call("Power Profiles:") mock_record_debug.assert_any_call("β”‚ Performance") mock_record_debug.assert_any_call("β”‚ Balanced") mock_record_debug.assert_any_call("└─Power Saver") def test_capture_battery(self): """Test capture_battery method""" with patch.object( self.validator.batteries, "get_batteries", return_value=["BAT0"] ), patch.object( self.validator.batteries, "get_energy_unit", return_value="Β΅Wh" ), patch.object( self.validator.batteries, "get_energy", return_value=50000 ), patch.object( self.validator.batteries, "get_energy_full", return_value=60000 ), patch.object( self.validator.db, "record_debug" ) as mock_record_debug, patch.object( self.validator.db, "record_battery_energy" ) as mock_record_battery_energy: self.validator.capture_battery() mock_record_debug.assert_called_with("BAT0 energy level is 50000 Β΅Wh") mock_record_battery_energy.assert_called_with("BAT0", 50000, 60000, "W") def test_check_rtc_cmos(self): """Test check_rtc_cmos method""" with patch( "os.path.join", return_value="/sys/module/rtc_cmos/parameters/use_acpi_alarm", ), patch("builtins.open", mock_open(read_data="N")), patch.object( self.validator.db, "record_cycle_data" ) as mock_record_cycle_data: self.validator.check_rtc_cmos() mock_record_cycle_data.assert_called_with( "`rtc_cmos` not configured to use ACPI alarm", "🚦" ) def test_capture_wake_sources(self): """Test capture_wake_sources method""" mock_pyudev = patch.object(self.validator, "pyudev").start() mock_record_debug = patch.object(self.validator.db, "record_debug").start() mock_read_file = patch("amd_debug.validator.read_file").start() mock_os_path_exists = patch("os.path.exists").start() # Mock wakeup devices mock_wakeup_device = mock_pyudev.list_devices.return_value = [ unittest.mock.Mock( sys_path="/sys/devices/pci0000:00/0000:00:14.0", find_parent=lambda subsystem, **kwargs: None, ) ] # Mock wakeup file existence and content mock_os_path_exists.return_value = True mock_read_file.return_value = "enabled" # Mock device properties mock_wakeup_device[0].properties = {"PCI_CLASS": "0x0c0330"} mock_wakeup_device[0].sys_path = "/sys/devices/pci0000:00/0000:00:14.0" self.validator.capture_wake_sources() # Validate debug messages mock_record_debug.assert_any_call( "Wakeup Source|Linux Device|Status\n|/sys/devices/pci0000:00/0000:00:14.0|enabled\n" ) # Stop patches patch.stopall() def test_capture_lid(self): """Test capture_lid method""" with patch("os.walk", return_value=[("/", [], ["lid0", "lid1"])]), patch( "os.path.join", side_effect=lambda *args: "/".join(args) ), patch( "amd_debug.validator.read_file", side_effect=["state: open", "state: closed"], ), patch.object( self.validator.db, "record_debug" ) as mock_record_debug: self.validator.capture_lid() mock_record_debug.assert_any_call("ACPI Lid (//lid0): open") mock_record_debug.assert_any_call("ACPI Lid (//lid1): closed") def test_capture_wakeup_irq_data(self): """Test capture_wakeup_irq_data method""" with patch("os.path.join", side_effect=lambda *args: "/".join(args)), patch( "amd_debug.validator.read_file", side_effect=[ "123", # IRQ number "chip_name_mock", # Chip name "irq_name_mock", # IRQ name "hw_mock", # Hardware IRQ "actions_mock", # Actions ], ), patch.object(self.validator.db, "record_debug") as mock_record_debug: result = self.validator.capture_wakeup_irq_data() self.assertTrue(result) mock_record_debug.assert_called_once_with( "Woke up from IRQ 123 (chip_name_mock hw_mock-irq_name_mock actions_mock)" ) def test_capture_thermal(self): """Test capture_thermal method""" # Mock pyudev devices mock_pyudev = patch.object(self.validator, "pyudev").start() mock_record_debug = patch.object(self.validator.db, "record_debug").start() mock_record_prereq = patch.object(self.validator.db, "record_prereq").start() mock_read_file = patch("amd_debug.validator.read_file").start() mock_os_listdir = patch("os.listdir").start() # Mock thermal devices mock_device = unittest.mock.Mock() mock_device.device_path = "/devices/LNXTHERM:00" mock_device.sys_path = "/sys/devices/LNXTHERM:00" mock_pyudev.list_devices.return_value = [mock_device] # Mock thermal zone files mock_read_file.side_effect = [ "45000", # Current temperature in millidegrees "critical", # Trip point 0 type "50000", # Trip point 0 temperature in millidegrees ] mock_os_listdir.return_value = ["trip_point_0_type", "trip_point_0_temp"] # Call the method result = self.validator.capture_thermal() # Validate debug messages mock_record_debug.assert_any_call("Thermal zones") mock_record_debug.assert_any_call("└─LNXTHERM:00") mock_record_debug.assert_any_call(" \t temp: 45.0Β°C") mock_record_debug.assert_any_call(" \t critical trip: 50.0Β°C") # Ensure no prereq was recorded since temp < trip mock_record_prereq.assert_not_called() # Stop patches patch.stopall() def test_capture_input_wakeup_count(self): """Test capture_input_wakeup_count method""" # Mock pyudev devices mock_pyudev = patch.object(self.validator, "pyudev").start() mock_record_debug = patch.object(self.validator.db, "record_debug").start() mock_read_file = patch("amd_debug.validator.read_file").start() mock_os_path_exists = patch("os.path.exists").start() # Mock input devices mock_device = unittest.mock.Mock() mock_device.sys_path = "/sys/devices/input0" mock_device.parent = None mock_pyudev.list_devices.return_value = [mock_device] # Mock wakeup file existence and content mock_os_path_exists.side_effect = ( lambda path: "wakeup" in path or "wakeup_count" in path ) mock_read_file.side_effect = ["5"] # Wakeup count # Set initial wakeup count self.validator.wakeup_count = {"/sys/devices/input0": "3"} # Call the method self.validator.capture_input_wakeup_count() # Validate debug messages mock_record_debug.assert_called_once_with( "Woke up from input source /sys/devices/input0 (3->5)" ) # Stop patches patch.stopall() def test_capture_hw_sleep_suspend_stats(self): """Test capture_hw_sleep stats method""" # Case 1: Suspend stats file exists and contains valid data with patch( "os.path.exists", side_effect=lambda path: "suspend_stats" in path ), patch("amd_debug.validator.read_file", return_value="1000000"), patch.object( self.validator.db, "record_cycle_data" ) as mock_record_cycle_data: result = self.validator.capture_hw_sleep() self.assertTrue(result) self.assertEqual(self.validator.hw_sleep_duration, 1.0) mock_record_cycle_data.assert_not_called() def test_capture_hw_sleep_smu_fw_info(self): """Test capture_hw_sleep smu_fw_info method""" # Case 2: Suspend stats file does not exist, fallback to smu_fw_info with patch( "os.path.exists", side_effect=lambda path: "suspend_stats" not in path ), patch( "amd_debug.validator.read_file", side_effect=[ "Last S0i3 Status: Success\nTime (in us) in S0i3: 2000000", # smu_fw_info content ], ), patch.object( self.validator.db, "record_cycle_data" ) as mock_record_cycle_data: result = self.validator.capture_hw_sleep() self.assertTrue(result) self.assertEqual(self.validator.hw_sleep_duration, 2.0) mock_record_cycle_data.assert_not_called() def test_capture_hw_sleep_smu_fw_info_lockdown(self): """Test capture_hw_sleep smu_fw_info method while locked down""" # Case 3: PermissionError while reading smu_fw_info with lockdown enabled self.validator.lockdown = True with patch("os.path.exists", return_value=False), patch( "amd_debug.validator.read_file", side_effect=PermissionError ), patch.object( self.validator.db, "record_cycle_data" ) as mock_record_cycle_data: result = self.validator.capture_hw_sleep() self.assertFalse(result) mock_record_cycle_data.assert_called_once_with( "Unable to gather hardware sleep data with lockdown engaged", "🚦" ) def test_capture_hw_sleep_smu_fw_info_missing(self): """Test capture_hw_sleep smu_fw_info missing method""" # Case 4: FileNotFoundError while reading smu_fw_info self.validator.lockdown = False with patch("os.path.exists", return_value=False), patch( "amd_debug.validator.read_file", side_effect=FileNotFoundError ), patch.object(self.validator.db, "record_debug") as mock_record: result = self.validator.capture_hw_sleep() self.assertFalse(result) mock_record.assert_called_once_with( "HW sleep statistics file /sys/kernel/debug/amd_pmc/smu_fw_info is missing" ) def test_capture_amdgpu_ips_status(self): """Test capture_amdgpu_ips_status method""" # Mock pyudev devices mock_pyudev = patch.object(self.validator, "pyudev").start() mock_record_debug = patch.object(self.validator.db, "record_debug").start() mock_read_file = patch("amd_debug.validator.read_file").start() mock_os_path_exists = patch("os.path.exists").start() # Mock PCI devices mock_device = unittest.mock.Mock() mock_device.properties = { "PCI_ID": "1002:abcd", "PCI_SLOT_NAME": "0000:01:00.0", } mock_pyudev.list_devices.return_value = [mock_device] # Case 1: IPS status file exists and is readable mock_os_path_exists.return_value = True mock_read_file.return_value = "IPS Enabled\nIPS Level: 2" self.validator.capture_amdgpu_ips_status() # Validate debug messages mock_record_debug.assert_any_call("IPS status") mock_record_debug.assert_any_call("β”‚ IPS Enabled") mock_record_debug.assert_any_call("└─IPS Level: 2") # Case 2: IPS status file does not exist mock_os_path_exists.return_value = False self.validator.capture_amdgpu_ips_status() # Case 3: PermissionError while reading IPS status file mock_os_path_exists.return_value = True mock_read_file.side_effect = PermissionError self.validator.lockdown = True self.validator.capture_amdgpu_ips_status() mock_record_debug.assert_any_call( "Unable to gather IPS state data due to kernel lockdown." ) # Case 4: PermissionError without lockdown self.validator.lockdown = False self.validator.capture_amdgpu_ips_status() mock_record_debug.assert_any_call("Failed to read IPS state data") # Stop patches patch.stopall() def test_analyze_kernel_log(self): """Test analyze_kernel_log method""" # Mock kernel log lines mock_kernel_log_lines = [ "Timekeeping suspended for 123456 us", "Successfully transitioned to state lps0 ms entry", "Triggering wakeup from IRQ 5", "ACPI BIOS Error (bug): Something went wrong", "Event logged [IO_PAGE_FAULT device=0000:00:0c.0 domain=0x0000 address=0x7e800000 flags=0x0050]", "Dispatching Notify on [UBTC] (Device) Value 0x80 (Status Change)", ] # Mock kernel log processing mock_process_callback = patch.object( self.validator.kernel_log, "process_callback" ).start() mock_process_callback.side_effect = lambda callback: [ callback(line, 7) for line in mock_kernel_log_lines ] # Mock database recording mock_record_cycle_data = patch.object( self.validator.db, "record_cycle_data" ).start() mock_record_debug = patch.object(self.validator.db, "record_debug").start() # Call the method self.validator.analyze_kernel_log() # Validate recorded cycle data mock_record_cycle_data.assert_any_call("Hardware sleep cycle count: 1", "πŸ’€") mock_record_cycle_data.assert_any_call("ACPI BIOS errors found", "❌") mock_record_cycle_data.assert_any_call("Page faults found", "❌") mock_record_cycle_data.assert_any_call( "Notify devices ['UBTC'] found during suspend", "πŸ’€" ) # Validate recorded debug messages mock_record_debug.assert_any_call("Used Microsoft uPEP GUID in LPS0 _DSM") mock_record_debug.assert_any_call("Triggering wakeup from IRQ 5", 7) # Stop patches patch.stopall() def test_prep(self): """Test prep method""" with patch("amd_debug.validator.datetime") as mock_datetime, patch.object( self.validator.kernel_log, "seek_tail" ) as mock_seek_tail, patch.object( self.validator.db, "start_cycle" ) as mock_start_cycle, patch.object( self.validator, "capture_battery" ) as mock_capture_battery, patch.object( self.validator, "check_gpes" ) as mock_check_gpes, patch.object( self.validator, "capture_lid" ) as mock_capture_lid, patch.object( self.validator, "capture_command_line" ) as mock_capture_command_line, patch.object( self.validator, "capture_wake_sources" ) as mock_capture_wake_sources, patch.object( self.validator, "capture_running_compositors" ) as mock_capture_running_compositors, patch.object( self.validator, "capture_power_profile" ) as mock_capture_power_profile, patch.object( self.validator, "capture_amdgpu_ips_status" ) as mock_capture_amdgpu_ips_status, patch.object( self.validator, "capture_thermal" ) as mock_capture_thermal, patch.object( self.validator, "capture_input_wakeup_count" ) as mock_capture_input_wakeup_count, patch.object( self.validator.acpica, "trace_bios" ) as mock_trace_bios, patch.object( self.validator.acpica, "trace_notify" ) as mock_trace_notify, patch.object( self.validator.db, "record_cycle" ) as mock_record_cycle: # Mock datetime mock_datetime.now.return_value = "mocked_datetime" # Set bios_debug to True and test self.validator.bios_debug = True self.validator.prep() mock_seek_tail.assert_called_once() mock_start_cycle.assert_called_once_with("mocked_datetime") mock_capture_battery.assert_called_once() mock_check_gpes.assert_called_once() mock_capture_lid.assert_called_once() mock_capture_command_line.assert_called_once() mock_capture_wake_sources.assert_called_once() mock_capture_running_compositors.assert_called_once() mock_capture_power_profile.assert_called_once() mock_capture_amdgpu_ips_status.assert_called_once() mock_capture_thermal.assert_called_once() mock_capture_input_wakeup_count.assert_called_once() mock_trace_bios.assert_called_once() mock_trace_notify.assert_not_called() mock_record_cycle.assert_called_once() # Reset mocks mock_seek_tail.reset_mock() mock_start_cycle.reset_mock() mock_capture_battery.reset_mock() mock_check_gpes.reset_mock() mock_capture_lid.reset_mock() mock_capture_command_line.reset_mock() mock_capture_wake_sources.reset_mock() mock_capture_running_compositors.reset_mock() mock_capture_power_profile.reset_mock() mock_capture_amdgpu_ips_status.reset_mock() mock_capture_thermal.reset_mock() mock_capture_input_wakeup_count.reset_mock() mock_trace_bios.reset_mock() mock_trace_notify.reset_mock() mock_record_cycle.reset_mock() # Set bios_debug to False and test self.validator.bios_debug = False self.validator.prep() mock_seek_tail.assert_called_once() mock_start_cycle.assert_called_once_with("mocked_datetime") mock_capture_battery.assert_called_once() mock_check_gpes.assert_called_once() mock_capture_lid.assert_called_once() mock_capture_command_line.assert_called_once() mock_capture_wake_sources.assert_called_once() mock_capture_running_compositors.assert_called_once() mock_capture_power_profile.assert_called_once() mock_capture_amdgpu_ips_status.assert_called_once() mock_capture_thermal.assert_called_once() mock_capture_input_wakeup_count.assert_called_once() mock_trace_bios.assert_not_called() mock_trace_notify.assert_called_once() mock_record_cycle.assert_called_once() def test_post(self): """Test post method""" with patch.object( self.validator, "analyze_kernel_log" ) as mock_analyze_kernel_log, patch.object( self.validator, "capture_wakeup_irq_data" ) as mock_capture_wakeup_irq_data, patch.object( self.validator, "check_gpes" ) as mock_check_gpes, patch.object( self.validator, "capture_lid" ) as mock_capture_lid, patch.object( self.validator, "check_rtc_cmos" ) as mock_check_rtc_cmos, patch.object( self.validator, "capture_hw_sleep" ) as mock_capture_hw_sleep, patch.object( self.validator, "capture_battery" ) as mock_capture_battery, patch.object( self.validator, "capture_amdgpu_ips_status" ) as mock_capture_amdgpu_ips_status, patch.object( self.validator, "capture_thermal" ) as mock_capture_thermal, patch.object( self.validator, "capture_input_wakeup_count" ) as mock_capture_input_wakeup_count, patch.object( self.validator.acpica, "restore" ) as mock_acpica_restore, patch.object( self.validator.db, "record_cycle" ) as mock_record_cycle: # Set mock return values mock_analyze_kernel_log.return_value = None mock_capture_wakeup_irq_data.return_value = None mock_check_gpes.return_value = None mock_capture_lid.return_value = None mock_check_rtc_cmos.return_value = None mock_capture_hw_sleep.return_value = None mock_capture_battery.return_value = None mock_capture_amdgpu_ips_status.return_value = None mock_capture_thermal.return_value = None mock_capture_input_wakeup_count.return_value = None mock_acpica_restore.return_value = None # Set attributes for record_cycle self.validator.requested_duration = 60 self.validator.active_gpios = ["GPIO1"] self.validator.wakeup_irqs = ["5"] self.validator.kernel_duration = 1.5 self.validator.hw_sleep_duration = 1.0 # Call the method self.validator.post() # Assert all checks were called mock_analyze_kernel_log.assert_called_once() mock_capture_wakeup_irq_data.assert_called_once() mock_check_gpes.assert_called_once() mock_capture_lid.assert_called_once() mock_check_rtc_cmos.assert_called_once() mock_capture_hw_sleep.assert_called_once() mock_capture_battery.assert_called_once() mock_capture_amdgpu_ips_status.assert_called_once() mock_capture_thermal.assert_called_once() mock_capture_input_wakeup_count.assert_called_once() mock_acpica_restore.assert_called_once() # Assert record_cycle was called with correct arguments mock_record_cycle.assert_called_once_with( self.validator.requested_duration, ",".join(str(gpio) for gpio in self.validator.active_gpios), ",".join(str(irq) for irq in self.validator.wakeup_irqs), int(self.validator.kernel_duration), int(self.validator.hw_sleep_duration), ) def test_program_wakealarm(self): """Test program_wakealarm method""" # Mock pyudev devices mock_pyudev = patch.object(self.validator, "pyudev").start() _mock_record_debug = patch.object(self.validator.db, "record_debug").start() mock_print_color = patch("amd_debug.validator.print_color").start() mock_open_file = patch("builtins.open", mock_open()).start() # Case 1: RTC device exists mock_device = unittest.mock.Mock() mock_device.sys_path = "/sys/class/rtc/rtc0" mock_pyudev.list_devices.return_value = [mock_device] self.validator.requested_duration = 60 self.validator.program_wakealarm() # Validate file writes mock_open_file.assert_any_call( "/sys/class/rtc/rtc0/wakealarm", "w", encoding="utf-8" ) mock_open_file().write.assert_any_call("0") mock_open_file().write.assert_any_call("+60\n") # Case 2: No RTC device found mock_pyudev.list_devices.return_value = [] self.validator.program_wakealarm() # Validate print_color call mock_print_color.assert_called_once_with( "No RTC device found, please manually wake system", "🚦" ) # Stop patches patch.stopall() @patch("amd_debug.validator.SleepReport") @patch("amd_debug.validator.print_color") def test_report_cycle(self, mock_print_color, mock_sleep_report): """Test report_cycle method""" # Mock SleepReport instance mock_report_instance = mock_sleep_report.return_value mock_report_instance.run.return_value = None # Set attributes for the test self.validator.last_suspend = "mocked_last_suspend" self.validator.display_debug = True # Call the method self.validator.report_cycle() # Assert print_color was called with correct arguments mock_print_color.assert_called_once_with("Results from last s2idle cycle", "πŸ—£οΈ") # Assert SleepReport was instantiated with correct arguments mock_sleep_report.assert_called_once_with( since="mocked_last_suspend", until="mocked_last_suspend", fname=None, fmt="stdout", tool_debug=True, report_debug=False, ) # Assert run method of SleepReport was called mock_report_instance.run.assert_called_once_with(inc_prereq=False) @patch("amd_debug.validator.run_countdown") @patch("amd_debug.validator.random.randint") @patch("amd_debug.validator.datetime") @patch.object(SleepValidator, "prep") @patch.object(SleepValidator, "program_wakealarm") @patch.object(SleepValidator, "suspend_system") @patch.object(SleepValidator, "post") @patch.object(SleepValidator, "unlock_session") @patch.object(SleepValidator, "report_cycle") @patch("amd_debug.validator.print_color") def test_run( self, _mock_print_color, mock_report_cycle, mock_unlock_session, mock_post, mock_suspend_system, mock_program_wakealarm, mock_prep, mock_datetime, mock_randint, mock_run_countdown, ): """Test the run method""" # Mock datetime mock_datetime.now.return_value = datetime(2023, 1, 1, 12, 0, 0) # Mock suspend_system to return True mock_suspend_system.return_value = True # Test case 1: count is 0 result = self.validator.run( duration=10, count=0, wait=5, rand=False, logind=False ) self.assertTrue(result) # Test case 2: logind is True self.validator.run(duration=10, count=1, wait=5, rand=False, logind=True) self.assertTrue(self.validator.logind) # Test case 3: Randomized test mock_randint.side_effect = [7, 3] # Random duration and wait self.validator.run(duration=10, count=1, wait=5, rand=True, logind=False) mock_randint.assert_any_call(4, 10) mock_randint.assert_any_call(1, 5) mock_run_countdown.assert_any_call("Suspending system", math.ceil(3 / 2)) mock_run_countdown.assert_any_call("Collecting data", math.ceil(3 / 2)) mock_prep.assert_called() mock_program_wakealarm.assert_called() mock_suspend_system.assert_called() mock_post.assert_called() mock_report_cycle.assert_called() mock_unlock_session.assert_called() # Test case 4: Randomized test, but too short of a duration result = self.validator.run( duration=4, count=1, wait=5, rand=True, logind=False ) self.assertFalse(result) mock_report_cycle.assert_called() # Test case 5: Multiple cycles self.validator.run(duration=10, count=2, wait=5, rand=False, logind=False) self.assertEqual(mock_prep.call_count, 4) # Includes previous calls self.assertEqual(mock_program_wakealarm.call_count, 4) self.assertEqual(mock_suspend_system.call_count, 4) self.assertEqual(mock_post.call_count, 4) self.assertEqual(mock_report_cycle.call_count, 5) self.assertEqual(mock_unlock_session.call_count, 3) # Test case 6: suspend_system fails mock_suspend_system.return_value = False result = self.validator.run( duration=10, count=1, wait=5, rand=False, logind=False ) self.assertFalse(result) mock_report_cycle.assert_called() @patch("os.path.exists") @patch("builtins.open", new_callable=mock_open, read_data="3") @patch("os.write") @patch("os.open") @patch("os.close") def test_suspend_system_sysfs_success( self, mock_os_close, mock_os_open, mock_os_write, _mock_open_file, mock_path_exists, ): """Test suspend_system method using sysfs interface with success""" # Mock wakeup_count file existence mock_path_exists.side_effect = lambda path: "wakeup_count" in path # Mock os.open and os.write mock_os_open.return_value = 3 mock_os_write.return_value = None # Call the method result = self.validator.suspend_system() # Assert the method returned True self.assertTrue(result) # Assert os.open and os.write were called mock_os_open.assert_called_once_with( "/sys/power/state", os.O_WRONLY | os.O_SYNC ) mock_os_write.assert_called_once_with(3, b"mem") mock_os_close.assert_called_once_with(3) @patch("os.path.exists") @patch("builtins.open", new_callable=mock_open, read_data="3") @patch("os.write") @patch("os.open") @patch("os.close") def test_suspend_system_sysfs_failure( self, mock_os_close, mock_os_open, mock_os_write, _mock_open_file, mock_path_exists, ): """Test suspend_system method using sysfs interface with failure""" # Mock wakeup_count file existence mock_path_exists.side_effect = lambda path: "wakeup_count" in path # Mock os.open to raise OSError mock_os_open.return_value = 3 mock_os_write.side_effect = OSError("Failed to write to state") # Call the method result = self.validator.suspend_system() # Assert the method returned False self.assertFalse(result) # Assert os.open and os.write were called mock_os_open.assert_called_once_with( "/sys/power/state", os.O_WRONLY | os.O_SYNC ) mock_os_write.assert_called_once_with(3, b"mem") mock_os_close.assert_called_once_with(3) @patch("os.path.exists") @patch("builtins.open", new_callable=mock_open) @patch("os.write") @patch("os.open") @patch("os.close") def test_suspend_system_sysfs_no_wakeup_count( self, mock_os_close, mock_os_open, mock_os_write, _mock_open_file, mock_path_exists, ): """Test suspend_system method using sysfs interface with no wakeup_count file""" # Mock wakeup_count file does not exist mock_path_exists.return_value = False # Mock os.open and os.write mock_os_open.return_value = 3 mock_os_write.return_value = None # Call the method result = self.validator.suspend_system() # Assert the method returned True self.assertTrue(result) # Assert os.open and os.write were called mock_os_open.assert_called_once_with( "/sys/power/state", os.O_WRONLY | os.O_SYNC ) mock_os_write.assert_called_once_with(3, b"mem") mock_os_close.assert_called_once_with(3) @patch("os.path.exists") @patch("os.open") @patch("os.write") @patch("os.close") def test_toggle_nvidia_file_not_exists( self, mock_close, mock_write, mock_open, mock_exists ): """Test toggle_nvidia returns True if NVIDIA suspend file does not exist""" mock_exists.return_value = False result = self.validator.toggle_nvidia(b"suspend") self.assertTrue(result) mock_open.assert_not_called() mock_write.assert_not_called() mock_close.assert_not_called() @patch("os.path.exists") @patch("os.open") @patch("os.write") @patch("os.close") def test_toggle_nvidia_success( self, mock_close, mock_write, mock_open, mock_exists ): """Test toggle_nvidia writes value and returns True on success""" mock_exists.return_value = True mock_open.return_value = 42 mock_write.return_value = None with patch.object(self.validator.db, "record_debug") as mock_record_debug: result = self.validator.toggle_nvidia(b"suspend") self.assertTrue(result) mock_open.assert_called_once_with( "/proc/driver/nvidia/suspend", os.O_WRONLY | os.O_SYNC ) mock_write.assert_called_once_with(42, b"suspend") mock_close.assert_called_once_with(42) mock_record_debug.assert_called_once_with( "Wrote b'suspend' to NVIDIA driver" ) @patch("os.path.exists") @patch("os.open") @patch("os.write") @patch("os.close") def test_toggle_nvidia_oserror( self, mock_close, mock_write, mock_open, mock_exists ): """Test toggle_nvidia handles OSError and returns False""" mock_exists.return_value = True mock_open.return_value = 99 mock_write.side_effect = OSError("write error") with patch.object( self.validator.db, "record_cycle_data" ) as mock_record_cycle_data: result = self.validator.toggle_nvidia(b"resume") self.assertFalse(result) mock_open.assert_called_once_with( "/proc/driver/nvidia/suspend", os.O_WRONLY | os.O_SYNC ) mock_write.assert_called_once_with(99, b"resume") mock_close.assert_called_once_with(99) mock_record_cycle_data.assert_called_once() amd-debug-tools-0.2.15/src/test_wake.py000066400000000000000000000157601515405217400177260ustar00rootroot00000000000000#!/usr/bin/python3 # SPDX-License-Identifier: MIT """ This module contains unit tests for the wake GPIO and IRQ functions in the amd-debug-tools package. """ import logging import unittest from unittest.mock import patch, MagicMock from amd_debug.wake import WakeGPIO, WakeIRQ class TestWakeGPIO(unittest.TestCase): """Test WakeGPIO class""" @classmethod def setUpClass(cls): logging.basicConfig(filename="/dev/null", level=logging.DEBUG) def test_wake_gpio_initialization(self): """Test initialization of WakeGPIO class""" gpio = WakeGPIO(5) self.assertEqual(gpio.num, 5) self.assertEqual(gpio.name, "") def test_wake_gpio_str(self): """Test string representation of WakeGPIO class""" gpio = WakeGPIO(5) self.assertEqual(str(gpio), "5") gpio.name = "Test GPIO" self.assertEqual(str(gpio), "5 (Test GPIO)") class TestWakeIRQ(unittest.TestCase): """Test WakeIRQ class""" @classmethod def setUpClass(cls): logging.basicConfig(filename="/dev/null", level=logging.DEBUG) @patch("amd_debug.wake.read_file") @patch("os.path.exists") @patch("os.listdir") @patch("os.walk") def test_wake_irq_initialization( self, mock_os_walk, mock_os_listdir, mock_os_path_exists, mock_read_file ): """Test initialization of WakeIRQ class""" # Mocking file reads mock_read_file.side_effect = lambda path: { "/sys/kernel/irq/10/chip_name": "amd_gpio", "/sys/kernel/irq/10/actions": "test_action", "/sys/kernel/irq/10/wakeup": "enabled", "/sys/kernel/irq/10/hwirq": "42", }.get(path, "") # Mocking os.path.exists mock_os_path_exists.return_value = False # Mocking os.listdir and os.walk mock_os_listdir.return_value = [] mock_os_walk.return_value = [] irq = WakeIRQ(10) self.assertEqual(irq.num, 10) self.assertEqual(irq.chip_name, "amd_gpio") self.assertEqual(irq.name, "GPIO 42") @patch("amd_debug.wake.read_file") @patch("os.path.exists") @patch("os.listdir") @patch("os.walk") def test_wake_irq_disabled_interrupt( self, _mock_os_walk, _mock_os_listdir, mock_os_path_exists, mock_read_file ): """Test initialization of WakeIRQ class with disabled interrupt""" # Mocking file reads mock_read_file.side_effect = lambda path: { "/sys/kernel/irq/20/chip_name": "", "/sys/kernel/irq/20/actions": "", "/sys/kernel/irq/20/wakeup": "disabled", }.get(path, "") # Mocking os.path.exists mock_os_path_exists.return_value = False irq = WakeIRQ(20) self.assertEqual(irq.name, "Disabled interrupt") @patch("amd_debug.wake.read_file") @patch("os.path.exists") @patch("os.listdir") @patch("os.walk") @patch("pyudev.Context.list_devices") def test_wake_irq_pci_msi( self, mock_list_devices, _mock_os_walk, _mock_os_listdir, _mock_os_path_exists, mock_read_file, ): """Test initialization of WakeIRQ class with PCI-MSI""" # Mocking file reads mock_read_file.side_effect = lambda path: { "/sys/kernel/irq/30/chip_name": "PCI-MSI-0000:00:1f.2", "/sys/kernel/irq/30/actions": "", "/sys/kernel/irq/30/wakeup": "enabled", }.get(path, "") # Mocking pyudev context mock_device = MagicMock() mock_device.device_path = "/devices/pci0000:00/0000:00:1f.2" mock_device.properties = { "ID_VENDOR_FROM_DATABASE": "Intel Corporation", "ID_PCI_CLASS_FROM_DATABASE": "SATA controller", "PCI_SLOT_NAME": "0000:00:1f.2", "DRIVER": "ahci", } mock_list_devices.return_value = [mock_device] irq = WakeIRQ(30) self.assertEqual(irq.name, "Intel Corporation SATA controller (0000:00:1f.2)") self.assertEqual(irq.driver, "ahci") @patch("amd_debug.wake.read_file") @patch("os.path.exists") @patch("os.listdir") @patch("os.walk") @patch("pyudev.Context.list_devices") def test_wake_irq_legacy_irq( self, _mock_list_devices, _mock_os_walk, mock_os_listdir, mock_os_path_exists, mock_read_file, ): """Test initialization of WakeIRQ class with legacy IRQs""" # Mocking file reads mock_read_file.side_effect = lambda path: { "/sys/kernel/irq/40/chip_name": "IR-IO-APIC", "/sys/kernel/irq/40/actions": "acpi", "/sys/kernel/irq/40/wakeup": "enabled", }.get(path, "") # Mocking os.path.exists mock_os_path_exists.return_value = False # Mocking os.listdir mock_os_listdir.return_value = [] irq = WakeIRQ(40) self.assertEqual(irq.name, "ACPI SCI") @patch("amd_debug.wake.read_file") @patch("os.path.exists") @patch("os.listdir") @patch("os.walk") def test_wake_irq_acpi_device( self, mock_os_walk, mock_os_listdir, mock_os_path_exists, mock_read_file ): """Test initialization of WakeIRQ class with ACPI device""" # Mocking file reads mock_read_file.side_effect = lambda path: { "/sys/kernel/irq/50/chip_name": "", "/sys/kernel/irq/50/actions": "acpi_device", "/sys/kernel/irq/50/wakeup": "enabled", "/sys/bus/acpi/devices/acpi_device/physical_node/name": "ACPI Device Name", }.get(path, "") # Mocking os.path.exists def exists_side_effect(path): return path in [ "/sys/bus/acpi/devices/acpi_device", "/sys/bus/acpi/devices/acpi_device/physical_node", "/sys/bus/acpi/devices/acpi_device/physical_node/name", ] mock_os_path_exists.side_effect = exists_side_effect # Mocking os.listdir mock_os_listdir.return_value = ["physical_node"] # Mocking os.walk mock_os_walk.return_value = [ ("/sys/bus/acpi/devices/acpi_device/physical_node", [], ["name"]) ] irq = WakeIRQ(50) self.assertEqual(irq.name, "ACPI Device Name") @patch("amd_debug.wake.read_file") @patch("os.path.exists") @patch("os.listdir") @patch("os.walk") def test_wake_irq_i2c_hid_device( self, _mock_os_walk, _mock_os_listdir, mock_os_path_exists, mock_read_file ): """Test initialization of WakeIRQ class with I2C HID device""" # Mocking file reads mock_read_file.side_effect = lambda path: { "/sys/kernel/irq/60/chip_name": "", "/sys/kernel/irq/60/actions": "i2c_hid_device", "/sys/kernel/irq/60/wakeup": "enabled", }.get(path, "") # Mocking os.path.exists mock_os_path_exists.return_value = False irq = WakeIRQ(60) irq.driver = "i2c_hid_acpi" irq.name = "i2c_hid_device" self.assertEqual(irq.name, "i2c_hid_device")