From 5ebc1a87291ba324d8c19a40fbb7bd25a4cefccd Mon Sep 17 00:00:00 2001 From: Raivis Dejus Date: Sun, 29 Mar 2026 16:54:54 +0300 Subject: [PATCH] Adding more tests --- buzz/widgets/icon_presentation.py | 60 ---- tests/cuda_manager_test.py | 316 ++++++++++++++++++++ tests/cuda_setup_test.py | 238 +++++++++++++++ tests/widgets/audio_meter_widget_test.py | 27 ++ tests/widgets/cuda_installer_widget_test.py | 145 +++++++++ 5 files changed, 726 insertions(+), 60 deletions(-) delete mode 100644 buzz/widgets/icon_presentation.py create mode 100644 tests/cuda_manager_test.py create mode 100644 tests/cuda_setup_test.py create mode 100644 tests/widgets/cuda_installer_widget_test.py diff --git a/buzz/widgets/icon_presentation.py b/buzz/widgets/icon_presentation.py deleted file mode 100644 index 6f230971..00000000 --- a/buzz/widgets/icon_presentation.py +++ /dev/null @@ -1,60 +0,0 @@ -from PyQt6.QtGui import QIcon, QPixmap, QPainter, QPalette -from PyQt6.QtCore import QSize -from PyQt6.QtSvg import QSvgRenderer -import os -from buzz.assets import APP_BASE_DIR - -class PresentationIcon: - "Icons for presentation window controls" - def __init__(self, parent, svg_path: str, color: str = None): - self.parent = parent - self.svg_path = svg_path - self.color = color or self.get_default_color() - - - def get_default_color(self) -> str: - """Get default icon color based on theme""" - palette = self.parent.palette() - is_dark = palette.window().color().black() > 127 - - return "#EEE" if is_dark else "#555" - - def get_icon(self) -> QIcon: - """Load SVG icon and return as QIcon""" - #Load from asset first - full_path = os.path.join(APP_BASE_DIR, "assets", "icons", os.path.basename(self.svg_path)) - - if not os.path.exists(full_path): - pixmap = QPixmap(24, 24) - pixmap.fill(self.color) - - return QIcon(pixmap) - - #Load SVG - renderer = QSvgRenderer(full_path) - pixmap = QPixmap(24, 24) - pixmap.fill(Qt.GlobalColor.transparent) - painter = QPainter(pixmap) - renderer.render(painter) - painter.end() - - return QIcon(pixmap) - - - - - - - - - - - - - - - - - - - diff --git a/tests/cuda_manager_test.py b/tests/cuda_manager_test.py new file mode 100644 index 00000000..deb596f4 --- /dev/null +++ b/tests/cuda_manager_test.py @@ -0,0 +1,316 @@ +import subprocess +import sys +from pathlib import Path +from unittest.mock import MagicMock, patch, call + +import pytest + +from buzz.cuda_manager import ( + CUDA_INDEX_URL, + CUDA_NVIDIA_PACKAGES_LINUX, + is_cuda_torch_installed, + is_flatpak, + is_nvidia_gpu_present, + is_snap, + should_offer_cuda_prompt, + _get_install_target, + _get_pip_cmd, + _in_virtualenv, + _pip_install, + _subprocess_hide_window_kwargs, + install_cuda, +) + + +class TestIsSnap: + def test_returns_true_when_snap_env_set(self, monkeypatch): + monkeypatch.setenv("SNAP", "/snap/buzz/current") + assert is_snap() is True + + def test_returns_false_when_snap_env_not_set(self, monkeypatch): + monkeypatch.delenv("SNAP", raising=False) + assert is_snap() is False + + +class TestIsFlatpak: + def test_returns_true_when_flatpak_env_set(self, monkeypatch): + monkeypatch.setenv("FLATPAK_ID", "io.github.chidiwilliams.buzz") + assert is_flatpak() is True + + def test_returns_false_when_flatpak_env_not_set(self, monkeypatch): + monkeypatch.delenv("FLATPAK_ID", raising=False) + assert is_flatpak() is False + + +class TestShouldOfferCudaPrompt: + def test_returns_true_on_windows(self, monkeypatch): + monkeypatch.setattr(sys, "platform", "win32") + assert should_offer_cuda_prompt() is True + + def test_returns_true_on_linux_snap(self, monkeypatch): + monkeypatch.setattr(sys, "platform", "linux") + monkeypatch.setenv("SNAP", "/snap/buzz/current") + monkeypatch.delenv("FLATPAK_ID", raising=False) + assert should_offer_cuda_prompt() is True + + def test_returns_true_on_linux_flatpak(self, monkeypatch): + monkeypatch.setattr(sys, "platform", "linux") + monkeypatch.delenv("SNAP", raising=False) + monkeypatch.setenv("FLATPAK_ID", "io.github.chidiwilliams.buzz") + assert should_offer_cuda_prompt() is True + + def test_returns_false_on_linux_bare(self, monkeypatch): + monkeypatch.setattr(sys, "platform", "linux") + monkeypatch.delenv("SNAP", raising=False) + monkeypatch.delenv("FLATPAK_ID", raising=False) + assert should_offer_cuda_prompt() is False + + def test_returns_false_on_macos(self, monkeypatch): + monkeypatch.setattr(sys, "platform", "darwin") + assert should_offer_cuda_prompt() is False + + +class TestIsCudaTorchInstalled: + def test_returns_true_when_cuda_available(self): + mock_torch = MagicMock() + mock_torch.cuda.is_available.return_value = True + mock_torch.__version__ = "2.0.0+cu118" + mock_torch.version.cuda = "11.8" + with patch.dict("sys.modules", {"torch": mock_torch}): + assert is_cuda_torch_installed() is True + + def test_returns_false_when_cuda_not_available(self): + mock_torch = MagicMock() + mock_torch.cuda.is_available.return_value = False + mock_torch.__version__ = "2.0.0" + mock_torch.version.cuda = None + with patch.dict("sys.modules", {"torch": mock_torch}): + assert is_cuda_torch_installed() is False + + def test_returns_false_when_torch_not_installed(self): + with patch.dict("sys.modules", {"torch": None}): + with patch("builtins.__import__", side_effect=ImportError): + assert is_cuda_torch_installed() is False + + def test_logs_warning_when_cuda_compiled_but_unavailable(self): + mock_torch = MagicMock() + mock_torch.cuda.is_available.return_value = False + mock_torch.__version__ = "2.0.0+cu118" + mock_torch.version.cuda = "11.8" + with patch.dict("sys.modules", {"torch": mock_torch}): + with patch("buzz.cuda_manager.logger") as mock_logger: + is_cuda_torch_installed() + mock_logger.warning.assert_called_once() + + +class TestIsNvidiaGpuPresent: + def test_returns_true_when_nvidia_smi_succeeds(self): + with patch("subprocess.run") as mock_run: + mock_run.return_value = MagicMock(returncode=0) + assert is_nvidia_gpu_present() is True + + def test_falls_back_to_proc_file_when_nvidia_smi_missing(self, tmp_path): + with patch("subprocess.run", side_effect=FileNotFoundError): + with patch("buzz.cuda_manager.Path") as mock_path_cls: + mock_path_cls.return_value.exists.return_value = True + assert is_nvidia_gpu_present() is True + + def test_returns_false_when_nvidia_smi_fails_and_no_proc_file(self): + with patch("subprocess.run") as mock_run: + mock_run.return_value = MagicMock(returncode=1) + with patch("pathlib.Path.exists", return_value=False): + assert is_nvidia_gpu_present() is False + + def test_handles_timeout(self): + with patch("subprocess.run", side_effect=subprocess.TimeoutExpired(["nvidia-smi"], 5)): + with patch("pathlib.Path.exists", return_value=False): + assert is_nvidia_gpu_present() is False + + +class TestInVirtualenv: + def test_returns_true_when_virtual_env_set(self, monkeypatch): + monkeypatch.setenv("VIRTUAL_ENV", "/some/venv") + assert _in_virtualenv() is True + + def test_returns_true_when_prefix_differs(self, monkeypatch): + monkeypatch.delenv("VIRTUAL_ENV", raising=False) + with patch.object(sys, "prefix", "/some/venv"): + with patch.object(sys, "base_prefix", "/usr"): + assert _in_virtualenv() is True + + def test_returns_false_when_no_venv(self, monkeypatch): + monkeypatch.delenv("VIRTUAL_ENV", raising=False) + with patch.object(sys, "prefix", sys.base_prefix): + assert _in_virtualenv() is False + + +class TestGetInstallTarget: + def test_snap_uses_snap_user_data(self, monkeypatch, tmp_path): + snap_dir = tmp_path / "snap_data" + monkeypatch.setenv("SNAP", "/snap/buzz/current") + monkeypatch.setenv("SNAP_USER_DATA", str(snap_dir)) + monkeypatch.delenv("FLATPAK_ID", raising=False) + flags = _get_install_target() + assert flags[0] == "--target" + assert "cuda_packages" in flags[1] + assert str(snap_dir) in flags[1] + + def test_snap_falls_back_to_home_when_no_snap_user_data(self, monkeypatch): + monkeypatch.setenv("SNAP", "/snap/buzz/current") + monkeypatch.delenv("SNAP_USER_DATA", raising=False) + monkeypatch.delenv("FLATPAK_ID", raising=False) + with patch("pathlib.Path.mkdir"): + flags = _get_install_target() + assert flags[0] == "--target" + assert "cuda_packages" in flags[1] + + def test_flatpak_uses_xdg_data_home(self, monkeypatch, tmp_path): + monkeypatch.delenv("SNAP", raising=False) + monkeypatch.setenv("FLATPAK_ID", "io.github.chidiwilliams.buzz") + monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path)) + with patch("pathlib.Path.mkdir"): + flags = _get_install_target() + assert flags[0] == "--target" + assert "buzz" in flags[1] + assert "cuda_packages" in flags[1] + + def test_virtualenv_returns_empty(self, monkeypatch): + monkeypatch.delenv("SNAP", raising=False) + monkeypatch.delenv("FLATPAK_ID", raising=False) + monkeypatch.setenv("VIRTUAL_ENV", "/some/venv") + assert _get_install_target() == [] + + def test_bare_returns_user_flag(self, monkeypatch): + monkeypatch.delenv("SNAP", raising=False) + monkeypatch.delenv("FLATPAK_ID", raising=False) + monkeypatch.delenv("VIRTUAL_ENV", raising=False) + with patch.object(sys, "prefix", sys.base_prefix): + assert _get_install_target() == ["--user"] + + +class TestSubprocessHideWindowKwargs: + def test_returns_empty_on_linux(self, monkeypatch): + monkeypatch.setattr(sys, "platform", "linux") + assert _subprocess_hide_window_kwargs() == {} + + def test_returns_startupinfo_on_windows(self, monkeypatch): + monkeypatch.setattr(sys, "platform", "win32") + # Only run on actual windows, otherwise mock the STARTUPINFO + if sys.platform != "win32": + mock_si = MagicMock() + with patch("subprocess.STARTUPINFO", return_value=mock_si): + with patch("subprocess.STARTF_USESHOWWINDOW", 1): + with patch("subprocess.SW_HIDE", 0): + with patch("subprocess.CREATE_NO_WINDOW", 0x08000000): + result = _subprocess_hide_window_kwargs() + assert "startupinfo" in result + assert "creationflags" in result + + +class TestGetPipCmd: + def test_returns_sys_executable_when_pip_available(self): + with patch("subprocess.run") as mock_run: + mock_run.return_value = MagicMock(returncode=0) + cmd = _get_pip_cmd() + assert cmd == [sys.executable, "-m", "pip"] + + def test_bootstraps_pip_when_not_available(self): + responses = [ + MagicMock(returncode=1), # pip --version fails + MagicMock(returncode=0), # ensurepip succeeds + ] + with patch("subprocess.run", side_effect=responses): + cmd = _get_pip_cmd() + assert cmd == [sys.executable, "-m", "pip"] + + def test_raises_when_ensurepip_also_fails(self): + responses = [ + MagicMock(returncode=1), # pip --version fails + MagicMock(returncode=1), # ensurepip fails + ] + with patch("subprocess.run", side_effect=responses): + with pytest.raises(RuntimeError, match="pip is not available"): + _get_pip_cmd() + + +class TestPipInstall: + def test_calls_pip_with_packages(self): + mock_proc = MagicMock() + mock_proc.stdout = iter(["Collecting torch\n", "Successfully installed\n"]) + mock_proc.returncode = 0 + mock_proc.wait.return_value = None + + with patch("buzz.cuda_manager._get_pip_cmd", return_value=[sys.executable, "-m", "pip"]): + with patch("subprocess.Popen", return_value=mock_proc) as mock_popen: + _pip_install(["torch==2.0.0"], extra_args=["--index-url", "https://example.com"]) + + cmd = mock_popen.call_args[0][0] + assert "torch==2.0.0" in cmd + assert "--index-url" in cmd + + def test_raises_on_nonzero_exit(self): + mock_proc = MagicMock() + mock_proc.stdout = iter([]) + mock_proc.returncode = 1 + mock_proc.wait.return_value = None + + with patch("buzz.cuda_manager._get_pip_cmd", return_value=[sys.executable, "-m", "pip"]): + with patch("subprocess.Popen", return_value=mock_proc): + with pytest.raises(RuntimeError, match="pip install failed"): + _pip_install(["torch==2.0.0"]) + + def test_calls_progress_callback(self): + mock_proc = MagicMock() + mock_proc.stdout = iter(["line1\n", "line2\n"]) + mock_proc.returncode = 0 + mock_proc.wait.return_value = None + + calls = [] + with patch("buzz.cuda_manager._get_pip_cmd", return_value=[sys.executable, "-m", "pip"]): + with patch("subprocess.Popen", return_value=mock_proc): + _pip_install(["pkg"], progress_callback=calls.append) + + assert "line1" in calls + assert "line2" in calls + + +class TestInstallCuda: + def test_calls_pip_install_twice(self, monkeypatch): + monkeypatch.delenv("SNAP", raising=False) + monkeypatch.delenv("FLATPAK_ID", raising=False) + monkeypatch.setenv("VIRTUAL_ENV", "/some/venv") + + with patch("buzz.cuda_manager._pip_install") as mock_pip: + install_cuda() + + assert mock_pip.call_count == 2 + + def test_passes_progress_callback(self, monkeypatch): + monkeypatch.delenv("SNAP", raising=False) + monkeypatch.delenv("FLATPAK_ID", raising=False) + monkeypatch.setenv("VIRTUAL_ENV", "/some/venv") + + messages = [] + with patch("buzz.cuda_manager._pip_install"): + install_cuda(progress_callback=messages.append) + + assert any("NVIDIA" in m for m in messages) + assert any("PyTorch" in m for m in messages) + + def test_linux_excludes_linux_only_packages_on_windows(self, monkeypatch): + monkeypatch.setattr(sys, "platform", "win32") + monkeypatch.delenv("SNAP", raising=False) + monkeypatch.delenv("FLATPAK_ID", raising=False) + + captured = [] + + def fake_pip(packages, **kwargs): + captured.append(packages) + + with patch("buzz.cuda_manager._pip_install", side_effect=fake_pip): + with patch("buzz.cuda_manager._get_install_target", return_value=[]): + install_cuda() + + nvidia_pkgs = captured[0] + for pkg in CUDA_NVIDIA_PACKAGES_LINUX: + assert pkg not in nvidia_pkgs diff --git a/tests/cuda_setup_test.py b/tests/cuda_setup_test.py new file mode 100644 index 00000000..11193c8e --- /dev/null +++ b/tests/cuda_setup_test.py @@ -0,0 +1,238 @@ +import sys +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + + +class TestGetCudaTargetDir: + def test_returns_snap_path(self, monkeypatch, tmp_path): + monkeypatch.setenv("SNAP_USER_DATA", str(tmp_path)) + monkeypatch.delenv("FLATPAK_ID", raising=False) + from buzz.cuda_setup import _get_cuda_target_dir + result = _get_cuda_target_dir() + assert result == tmp_path / "cuda_packages" + + def test_returns_flatpak_path(self, monkeypatch, tmp_path): + monkeypatch.delenv("SNAP_USER_DATA", raising=False) + monkeypatch.setenv("FLATPAK_ID", "io.github.chidiwilliams.buzz") + monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path)) + from buzz.cuda_setup import _get_cuda_target_dir + result = _get_cuda_target_dir() + assert result == tmp_path / "buzz" / "cuda_packages" + + def test_returns_none_when_no_env(self, monkeypatch): + monkeypatch.delenv("SNAP_USER_DATA", raising=False) + monkeypatch.delenv("FLATPAK_ID", raising=False) + from buzz.cuda_setup import _get_cuda_target_dir + result = _get_cuda_target_dir() + assert result is None + + def test_flatpak_falls_back_to_home(self, monkeypatch): + monkeypatch.delenv("SNAP_USER_DATA", raising=False) + monkeypatch.setenv("FLATPAK_ID", "io.github.chidiwilliams.buzz") + monkeypatch.delenv("XDG_DATA_HOME", raising=False) + from buzz.cuda_setup import _get_cuda_target_dir + result = _get_cuda_target_dir() + assert result is not None + assert "cuda_packages" in str(result) + + +class TestGetSitePackagesDirs: + def test_adds_cuda_target_to_sys_path(self, monkeypatch, tmp_path): + cuda_target = tmp_path / "cuda_packages" + cuda_target.mkdir() + monkeypatch.setenv("SNAP_USER_DATA", str(tmp_path)) + monkeypatch.delenv("FLATPAK_ID", raising=False) + + from buzz.cuda_setup import _get_site_packages_dirs + dirs = _get_site_packages_dirs() + + assert cuda_target in dirs + assert str(cuda_target) in sys.path + + def test_returns_list_including_existing_site_packages(self, monkeypatch): + monkeypatch.delenv("SNAP_USER_DATA", raising=False) + monkeypatch.delenv("FLATPAK_ID", raising=False) + + from buzz.cuda_setup import _get_site_packages_dirs + dirs = _get_site_packages_dirs() + assert isinstance(dirs, list) + + +class TestCollectCudaLibDirs: + def test_includes_torch_lib(self, tmp_path): + torch_lib = tmp_path / "torch" / "lib" + torch_lib.mkdir(parents=True) + + from buzz.cuda_setup import _collect_cuda_lib_dirs + dirs = _collect_cuda_lib_dirs(tmp_path) + assert str(torch_lib) in dirs + + def test_includes_nvidia_package_libs(self, tmp_path): + nvidia_cublas = tmp_path / "nvidia" / "cublas" / "lib" + nvidia_cublas.mkdir(parents=True) + + from buzz.cuda_setup import _collect_cuda_lib_dirs + dirs = _collect_cuda_lib_dirs(tmp_path) + assert str(nvidia_cublas) in dirs + + def test_returns_empty_when_no_dirs_exist(self, tmp_path): + from buzz.cuda_setup import _collect_cuda_lib_dirs + dirs = _collect_cuda_lib_dirs(tmp_path) + assert dirs == [] + + +class TestGetNvidiaPackageLibDirs: + def test_finds_nvidia_lib_dirs(self, monkeypatch, tmp_path): + sp = tmp_path / "site-packages" + nvidia_lib = sp / "nvidia" / "cublas" / "lib" + nvidia_lib.mkdir(parents=True) + + monkeypatch.delenv("SNAP_USER_DATA", raising=False) + monkeypatch.delenv("FLATPAK_ID", raising=False) + + with patch("buzz.cuda_setup._get_site_packages_dirs", return_value=[sp]): + from buzz.cuda_setup import _get_nvidia_package_lib_dirs + dirs = _get_nvidia_package_lib_dirs() + + assert nvidia_lib in dirs + + def test_finds_torch_lib_dir(self, monkeypatch, tmp_path): + sp = tmp_path / "site-packages" + torch_lib = sp / "torch" / "lib" + torch_lib.mkdir(parents=True) + + monkeypatch.delenv("SNAP_USER_DATA", raising=False) + monkeypatch.delenv("FLATPAK_ID", raising=False) + + with patch("buzz.cuda_setup._get_site_packages_dirs", return_value=[sp]): + from buzz.cuda_setup import _get_nvidia_package_lib_dirs + dirs = _get_nvidia_package_lib_dirs() + + assert torch_lib in dirs + + +class TestSetupLinuxCuda: + def test_skips_when_no_cuda_target(self, monkeypatch): + monkeypatch.delenv("SNAP_USER_DATA", raising=False) + monkeypatch.delenv("FLATPAK_ID", raising=False) + + with patch("buzz.cuda_setup._get_cuda_target_dir", return_value=None): + from buzz.cuda_setup import _setup_linux_cuda + _setup_linux_cuda() # should not raise + + def test_skips_when_cuda_target_does_not_exist(self, monkeypatch, tmp_path): + nonexistent = tmp_path / "nonexistent" + with patch("buzz.cuda_setup._get_cuda_target_dir", return_value=nonexistent): + from buzz.cuda_setup import _setup_linux_cuda + _setup_linux_cuda() # should not raise + + def test_skips_when_no_torch_lib(self, monkeypatch, tmp_path): + cuda_target = tmp_path / "cuda_packages" + cuda_target.mkdir() + with patch("buzz.cuda_setup._get_cuda_target_dir", return_value=cuda_target): + from buzz.cuda_setup import _setup_linux_cuda + _setup_linux_cuda() # should not raise + + def test_reexecs_when_sentinel_not_in_ld_path(self, monkeypatch, tmp_path): + cuda_target = tmp_path / "cuda_packages" + torch_lib = cuda_target / "torch" / "lib" + torch_lib.mkdir(parents=True) + + monkeypatch.delenv("LD_LIBRARY_PATH", raising=False) + + with patch("buzz.cuda_setup._get_cuda_target_dir", return_value=cuda_target): + with patch("buzz.cuda_setup._collect_cuda_lib_dirs", return_value=[str(torch_lib)]): + with patch("os.execv", side_effect=OSError("test")) as mock_execv: + with patch("buzz.cuda_setup._preload_linux_libraries_fallback") as mock_fallback: + from buzz.cuda_setup import _setup_linux_cuda + _setup_linux_cuda() + + mock_fallback.assert_called_once() + + def test_no_reexec_when_sentinel_already_in_ld_path(self, monkeypatch, tmp_path): + cuda_target = tmp_path / "cuda_packages" + torch_lib = cuda_target / "torch" / "lib" + torch_lib.mkdir(parents=True) + + monkeypatch.setenv("LD_LIBRARY_PATH", str(torch_lib)) + + with patch("buzz.cuda_setup._get_cuda_target_dir", return_value=cuda_target): + with patch("os.execv") as mock_execv: + from buzz.cuda_setup import _setup_linux_cuda + _setup_linux_cuda() + + mock_execv.assert_not_called() + + +class TestSetupWindowsDllDirectories: + def test_calls_add_dll_directory(self, tmp_path): + lib_dir = tmp_path / "lib" + lib_dir.mkdir() + + with patch("buzz.cuda_setup._get_nvidia_package_lib_dirs", return_value=[lib_dir]): + with patch("buzz.cuda_setup.os") as mock_os: + from buzz.cuda_setup import _setup_windows_dll_directories + _setup_windows_dll_directories() + + mock_os.add_dll_directory.assert_called_once_with(str(lib_dir)) + + def test_logs_warning_when_no_lib_dirs(self): + with patch("buzz.cuda_setup._get_nvidia_package_lib_dirs", return_value=[]): + with patch("buzz.cuda_setup.logger") as mock_logger: + from buzz.cuda_setup import _setup_windows_dll_directories + _setup_windows_dll_directories() + + mock_logger.warning.assert_called_once() + + +class TestSetupCudaLibraries: + def test_calls_windows_setup_on_windows(self): + with patch("platform.system", return_value="Windows"): + with patch("buzz.cuda_setup._setup_windows_dll_directories") as mock_win: + from buzz.cuda_setup import setup_cuda_libraries + setup_cuda_libraries() + mock_win.assert_called_once() + + def test_calls_linux_setup_on_linux(self): + with patch("platform.system", return_value="Linux"): + with patch("buzz.cuda_setup._setup_linux_cuda") as mock_linux: + from buzz.cuda_setup import setup_cuda_libraries + setup_cuda_libraries() + mock_linux.assert_called_once() + + def test_does_nothing_on_macos(self): + with patch("platform.system", return_value="Darwin"): + with patch("buzz.cuda_setup._setup_windows_dll_directories") as mock_win: + with patch("buzz.cuda_setup._setup_linux_cuda") as mock_linux: + from buzz.cuda_setup import setup_cuda_libraries + setup_cuda_libraries() + mock_win.assert_not_called() + mock_linux.assert_not_called() + + +class TestPreloadLinuxLibrariesFallback: + def test_loads_so_files(self, tmp_path): + lib_dir = tmp_path / "lib" + lib_dir.mkdir() + so_file = lib_dir / "libfoo.so.1" + so_file.touch() + + with patch("buzz.cuda_setup._get_nvidia_package_lib_dirs", return_value=[lib_dir]): + with patch("ctypes.CDLL") as mock_cdll: + from buzz.cuda_setup import _preload_linux_libraries_fallback + _preload_linux_libraries_fallback() + mock_cdll.assert_called() + + def test_skips_libnvblas(self, tmp_path): + lib_dir = tmp_path / "lib" + lib_dir.mkdir() + skip_file = lib_dir / "libnvblas.so.1" + skip_file.touch() + + with patch("buzz.cuda_setup._get_nvidia_package_lib_dirs", return_value=[lib_dir]): + with patch("ctypes.CDLL") as mock_cdll: + from buzz.cuda_setup import _preload_linux_libraries_fallback + _preload_linux_libraries_fallback() + mock_cdll.assert_not_called() diff --git a/tests/widgets/audio_meter_widget_test.py b/tests/widgets/audio_meter_widget_test.py index d91e5d70..62f9e142 100644 --- a/tests/widgets/audio_meter_widget_test.py +++ b/tests/widgets/audio_meter_widget_test.py @@ -54,3 +54,30 @@ class TestAudioMeterWidget: widget = AudioMeterWidget() qtbot.add_widget(widget) assert widget.height() == 56 + + def test_update_queue_size(self, qtbot: QtBot): + widget = AudioMeterWidget() + qtbot.add_widget(widget) + widget.update_queue_size(5) + assert widget.queue_size == 5 + + def test_reset_amplitude_clears_queue_size(self, qtbot: QtBot): + widget = AudioMeterWidget() + qtbot.add_widget(widget) + widget.update_queue_size(3) + widget.reset_amplitude() + assert widget.queue_size == 0 + + def test_initial_queue_size_is_zero(self, qtbot: QtBot): + widget = AudioMeterWidget() + qtbot.add_widget(widget) + assert widget.queue_size == 0 + + def test_paint_event_does_not_raise(self, qtbot: QtBot): + widget = AudioMeterWidget() + qtbot.add_widget(widget) + widget.show() + widget.update_amplitude(0.5) + widget.update_average_amplitude(0.1) + widget.update_queue_size(2) + widget.repaint() # triggers paintEvent diff --git a/tests/widgets/cuda_installer_widget_test.py b/tests/widgets/cuda_installer_widget_test.py new file mode 100644 index 00000000..e9e22444 --- /dev/null +++ b/tests/widgets/cuda_installer_widget_test.py @@ -0,0 +1,145 @@ +from unittest.mock import MagicMock, patch + +import pytest +from pytestqt.qtbot import QtBot + +from buzz.widgets.cuda_installer_widget import CudaInstallerDialog, _InstallWorker + + +class TestInstallWorker: + def test_calls_install_cuda_and_emits_finished(self): + worker = _InstallWorker() + finished_mock = MagicMock() + worker.signals.finished.connect(finished_mock) + + with patch("buzz.cuda_manager.install_cuda") as mock_install: + worker.run() + + mock_install.assert_called_once() + finished_mock.assert_called_once() + + def test_emits_error_on_exception(self): + worker = _InstallWorker() + error_mock = MagicMock() + worker.signals.error.connect(error_mock) + + with patch("buzz.cuda_manager.install_cuda", side_effect=RuntimeError("fail")): + worker.run() + + error_mock.assert_called_once_with("fail") + + def test_passes_progress_callback_to_install(self): + worker = _InstallWorker() + progress_mock = MagicMock() + worker.signals.progress.connect(progress_mock) + + def fake_install(progress_callback=None): + if progress_callback: + progress_callback("installing...") + + with patch("buzz.cuda_manager.install_cuda", side_effect=fake_install): + worker.run() + + progress_mock.assert_called_once_with("installing...") + + +class TestCudaInstallerDialog: + def test_dialog_creates_with_correct_title(self, qtbot: QtBot): + dialog = CudaInstallerDialog() + qtbot.add_widget(dialog) + assert "GPU" in dialog.windowTitle() or "Nvidia" in dialog.windowTitle() + + def test_install_button_exists(self, qtbot: QtBot): + dialog = CudaInstallerDialog() + qtbot.add_widget(dialog) + assert dialog.install_button is not None + assert dialog.install_button.isEnabled() + + def test_decline_button_exists(self, qtbot: QtBot): + dialog = CudaInstallerDialog() + qtbot.add_widget(dialog) + assert dialog.decline_button is not None + + def test_progress_bar_hidden_initially(self, qtbot: QtBot): + dialog = CudaInstallerDialog() + qtbot.add_widget(dialog) + assert not dialog.progress_bar.isVisible() + + def test_log_view_hidden_initially(self, qtbot: QtBot): + dialog = CudaInstallerDialog() + qtbot.add_widget(dialog) + assert not dialog.log_view.isVisible() + + def test_install_click_shows_progress_bar(self, qtbot: QtBot): + dialog = CudaInstallerDialog() + qtbot.add_widget(dialog) + dialog.show() + + with patch("buzz.widgets.cuda_installer_widget.QThreadPool"): + dialog._on_install_clicked() + + assert dialog.progress_bar.isVisible() + assert dialog.log_view.isVisible() + assert not dialog.install_button.isEnabled() + assert not dialog.decline_button.isEnabled() + + def test_on_progress_appends_to_log(self, qtbot: QtBot): + dialog = CudaInstallerDialog() + qtbot.add_widget(dialog) + dialog._on_progress("step 1") + assert "step 1" in dialog.log_view.toPlainText() + + def test_on_progress_updates_status_label(self, qtbot: QtBot): + dialog = CudaInstallerDialog() + qtbot.add_widget(dialog) + dialog._on_progress("Installing packages...") + assert dialog.status_label.text() != "" + + def test_on_progress_truncates_long_message(self, qtbot: QtBot): + dialog = CudaInstallerDialog() + qtbot.add_widget(dialog) + long_msg = "x" * 200 + dialog._on_progress(long_msg) + assert len(dialog.status_label.text()) <= 80 + + def test_on_finished_re_enables_install_button(self, qtbot: QtBot): + dialog = CudaInstallerDialog() + qtbot.add_widget(dialog) + dialog.install_button.setEnabled(False) + dialog._on_finished() + assert dialog.install_button.isEnabled() + + def test_on_finished_hides_progress_bar(self, qtbot: QtBot): + dialog = CudaInstallerDialog() + qtbot.add_widget(dialog) + dialog.progress_bar.setVisible(True) + dialog._on_finished() + assert not dialog.progress_bar.isVisible() + + def test_on_finished_shows_completion_message(self, qtbot: QtBot): + dialog = CudaInstallerDialog() + qtbot.add_widget(dialog) + dialog._on_finished() + assert "complete" in dialog.status_label.text().lower() or "restart" in dialog.status_label.text().lower() + + def test_on_error_shows_error_message(self, qtbot: QtBot): + dialog = CudaInstallerDialog() + qtbot.add_widget(dialog) + dialog._on_error("something went wrong") + assert "something went wrong" in dialog.status_label.text() + + def test_on_error_re_enables_buttons(self, qtbot: QtBot): + dialog = CudaInstallerDialog() + qtbot.add_widget(dialog) + dialog.install_button.setEnabled(False) + dialog.decline_button.setEnabled(False) + dialog._on_error("fail") + assert dialog.install_button.isEnabled() + assert dialog.decline_button.isEnabled() + + def test_on_error_hides_progress_bar(self, qtbot: QtBot): + dialog = CudaInstallerDialog() + qtbot.add_widget(dialog) + dialog.progress_bar.setVisible(True) + dialog._on_error("fail") + assert not dialog.progress_bar.isVisible()