Files
WildAi 8389ab4ce6 refactor: log through ComfyUI's own root-logger idiom, re-audit every level
The package installed its own handler, Formatter and level on a
module-level logger, then logged through that logger. ComfyUI core already
installs app.logger.ColoredFormatter on the ROOT logger, so a bare
logging.<level>() call whose message starts with "[ComfyUI-VibeVoice] " is
tagged and coloured for free. This removes the duplicate machinery rather
than extending it -- no vv_logging module, Logger subclass, LoggerAdapter,
vendored ANSI table, new handler, setLevel or propagate.

Three hazards that motivated deleting the setup block rather than moving it:

- propagate=False severed pytest's caplog; the tests only passed because
  __init__.py's `if "pytest" in sys.modules` guard skipped the block.
  Bare root calls propagate by default.
- logger.setLevel(INFO) on the package logger pinned every descendant, so
  ComfyUI's --verbose never applied to this node. Deleting the block fixes it.
- The level must stay a literal at the call site; no env-var switch was
  added. modules/diagnostics.py gates diagnostic CONTENT and is untouched.

Every call site (235 across 34 files) is now a bare root call with the
prefix. The vendored src/vibevoice tree used transformers.utils.logging,
which has no module-level info/warning/debug; those files now import stdlib
logging, so a new call site there would fail loudly instead of silently.

Level audit against ERROR=failure / WARNING=degradation / INFO=user-facing
progress / DEBUG=internals. Deleted as noise: the resample notice (44.1 kHz
reference audio is the normal case) and the two "Successfully loaded
external VibeVoice" confirmations, which duplicated the patcher's load line.
Promoted to WARNING: a mixed-naming GGUF, which is resolved by heuristic
majority vote and silently aliases the rest. Demoted to DEBUG: the four
SageAttention kernel-selection lines, model discovery, shard counts,
per-retry download attempts, attention-mode confirmation and the
save_pretrained notice. Kept at INFO: generation complete, transcription
results, model downloads and load starts.

Judgement calls recorded in docs/2026-10-01-vv-logging-cleanup-design.md.
The two I am least sure of: the ungated memory-census profile line was
demoted rather than gated (adding a gate would not be presentation-only),
and patcher.py's "Loading VibeVoice models for..." was kept at INFO against
the ask's example list because it is the only load-start line the package has.

48 new tests in tests/test_logging_idiom.py plus tests/test_audio_utils.py:
byte-exact ColoredFormatter rendering, no-leftover-machinery, AST prefix and
two-direction level policy, and the 44.1 kHz resample regression (the call
still fires with (44100, 24000); nothing is logged). 37 caplog.at_level
pins that named a module logger were stripped -- they only lowered a named
logger and left root at WARNING, so the record was discarded before capture.

Gate: 1924 passed, 30 skipped, 0 failed (1876 before this change). Twelve
deliberate mutations -- re-deleting a message, re-leveling, re-adding
setLevel and propagate=False, stripping a prefix, moving the resample out
of its branch -- were each caught by at least one test.

Not run: ComfyUI was never launched and no checkpoint was loaded.
2026-10-01 23:08:04 +03:00

207 lines
8.2 KiB
Python

"""Tests for modules/device_utils.py - Device detection and management."""
import logging
import torch
import pytest
from unittest.mock import patch, MagicMock
from ComfyUI_VibeVoice.modules.device_utils import (
DEVICE_AUTO,
DEVICE_CPU,
DEVICE_CUDA,
DEVICE_MPS,
DEVICE_XPU,
DEVICE_NPU,
get_available_devices,
get_device_options,
get_device_option_labels,
get_torch_device,
get_offload_device,
is_gpu_device,
get_device_display_name,
)
class TestGetDeviceOptions:
"""The device combo must offer every value get_torch_device() accepts.
ComfyUI validates a Combo input against its ``options`` list and raises
"Value not in list" *before* ``execute()`` runs, so a supported runtime
value missing from the options makes every saved workflow using it fail
validation. ``"auto"`` is accepted by get_torch_device(), so it must be
listed.
"""
def test_includes_auto(self):
assert DEVICE_AUTO in get_device_options()
def test_auto_is_last_so_widget_indices_are_stable(self):
options = get_device_options()
assert options[-1] == DEVICE_AUTO
# Everything before it keeps its historical index, so existing saved
# widgets_values still line up.
assert options[:-1] == get_available_devices()
def test_every_offered_value_resolves_to_a_device(self):
for option in get_device_options():
assert isinstance(get_torch_device(option), torch.device)
def test_option_labels_cover_every_option(self):
assert set(get_device_option_labels()) == set(get_device_options())
class TestGetAvailableDevices:
"""Test get_available_devices function."""
def test_returns_list(self):
devices = get_available_devices()
assert isinstance(devices, list)
def test_includes_cpu(self):
devices = get_available_devices()
assert DEVICE_CPU in devices
def test_includes_cuda_when_available(self):
with patch("ComfyUI_VibeVoice.modules.device_utils._get_model_management") as mock_mm:
mock_mm.return_value.is_nvidia.return_value = True
mock_mm.return_value.is_amd.return_value = False
mock_mm.return_value.mps_mode.return_value = False
devices = get_available_devices()
assert DEVICE_CUDA in devices
def test_includes_mps_when_available(self):
with patch("ComfyUI_VibeVoice.modules.device_utils._get_model_management") as mock_mm:
mock_mm.return_value.is_nvidia.return_value = False
mock_mm.return_value.is_amd.return_value = False
mock_mm.return_value.mps_mode.return_value = True
devices = get_available_devices()
assert DEVICE_MPS in devices
def test_cpu_only_when_no_gpu(self):
with patch("ComfyUI_VibeVoice.modules.device_utils._get_model_management") as mock_mm:
mock_mm.return_value.is_nvidia.return_value = False
mock_mm.return_value.is_amd.return_value = False
mock_mm.return_value.mps_mode.return_value = False
devices = get_available_devices()
assert devices == [DEVICE_CPU]
def test_excludes_mps_when_unavailable(self):
with patch("ComfyUI_VibeVoice.modules.device_utils._get_model_management") as mock_mm:
mock_mm.return_value.is_nvidia.return_value = False
mock_mm.return_value.is_amd.return_value = False
mock_mm.return_value.mps_mode.return_value = False
devices = get_available_devices()
assert DEVICE_MPS not in devices
class TestGetTorchDevice:
"""Test get_torch_device function."""
def test_cpu_device(self):
device = get_torch_device(DEVICE_CPU)
assert device.type == DEVICE_CPU
def test_none_uses_default(self):
with patch("ComfyUI_VibeVoice.modules.device_utils._get_model_management") as mock_mm:
mock_mm.return_value.get_torch_device.return_value = torch.device("cpu")
device = get_torch_device(None)
assert device is not None
def test_auto_uses_default(self):
with patch("ComfyUI_VibeVoice.modules.device_utils._get_model_management") as mock_mm:
mock_mm.return_value.get_torch_device.return_value = torch.device("cuda")
device = get_torch_device("auto")
assert device.type == "cuda"
def test_mps_available(self):
with patch("ComfyUI_VibeVoice.modules.device_utils._get_model_management") as mock_mm, \
patch("torch.backends.mps.is_available", return_value=True):
mock_mm.return_value.get_torch_device.return_value = torch.device("cuda")
device = get_torch_device(DEVICE_MPS)
assert device.type == DEVICE_MPS
def test_xpu_available(self):
with patch("ComfyUI_VibeVoice.modules.device_utils._get_model_management") as mock_mm, \
patch("torch.xpu") as mock_xpu:
mock_xpu.is_available.return_value = True
mock_mm.return_value.get_torch_device.return_value = torch.device("cuda")
device = get_torch_device(DEVICE_XPU)
assert device.type == DEVICE_XPU
def test_npu_available(self):
# NPU requires a special torch build that registers the "npu" device type.
# On standard builds torch.device("npu") raises, so skip there.
try:
torch.device(DEVICE_NPU)
except RuntimeError:
pytest.skip("torch build does not support the 'npu' device type")
with patch("ComfyUI_VibeVoice.modules.device_utils._get_model_management") as mock_mm, \
patch("torch.npu", create=True) as mock_npu:
mock_npu.is_available.return_value = True
mock_mm.return_value.get_torch_device.return_value = torch.device("cuda")
device = get_torch_device(DEVICE_NPU)
assert device.type == DEVICE_NPU
def test_mps_unavailable_falls_back_and_warns(self, caplog):
with patch("ComfyUI_VibeVoice.modules.device_utils._get_model_management") as mock_mm, \
patch("torch.backends.mps.is_available", return_value=False):
mock_mm.return_value.get_torch_device.return_value = torch.device("cuda")
with caplog.at_level(logging.WARNING):
device = get_torch_device(DEVICE_MPS)
assert device.type == "cuda"
assert any("mps" in r.getMessage() for r in caplog.records), caplog.records
def test_unknown_device_falls_back_and_warns(self, caplog):
with patch("ComfyUI_VibeVoice.modules.device_utils._get_model_management") as mock_mm:
mock_mm.return_value.get_torch_device.return_value = torch.device("cuda")
with caplog.at_level(logging.WARNING):
device = get_torch_device("something_weird")
assert device.type == "cuda"
assert any(
"something_weird" in r.getMessage() for r in caplog.records
), caplog.records
class TestIsGpuDevice:
"""Test is_gpu_device function."""
def test_cuda_is_gpu(self):
assert is_gpu_device(DEVICE_CUDA) is True
def test_cpu_not_gpu(self):
assert is_gpu_device(DEVICE_CPU) is False
def test_mps_is_gpu(self):
assert is_gpu_device(DEVICE_MPS) is True
def test_case_insensitive(self):
assert is_gpu_device("CUDA") is True
assert is_gpu_device("CPU") is False
class TestGetOffloadDevice:
"""Test get_offload_device function."""
def test_returns_device(self):
with patch("ComfyUI_VibeVoice.modules.device_utils._get_model_management") as mock_mm:
mock_mm.return_value.intermediate_device.return_value = torch.device("cpu")
device = get_offload_device()
assert isinstance(device, torch.device)
class TestGetDeviceDisplayName:
"""Test get_device_display_name function."""
def test_cpu_name(self):
with patch("ComfyUI_VibeVoice.modules.device_utils._get_model_management"):
name = get_device_display_name(DEVICE_CPU)
assert "CPU" in name
def test_mps_name(self):
with patch("ComfyUI_VibeVoice.modules.device_utils._get_model_management"):
name = get_device_display_name(DEVICE_MPS)
assert "MPS" in name or "Apple" in name