Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 153 additions & 0 deletions tests/handlers/test_brew_handlers_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@
_get_rate_limit, and _get_and_filter_brews.
"""

import sys
from unittest.mock import MagicMock, patch

from versiontracker.exceptions import HomebrewError, NetworkError
from versiontracker.handlers.brew_handlers import (
_determine_strict_mode,
_get_and_filter_brews,
_get_homebrew_casks,
_get_rate_limit,
_handle_export_if_requested,
_log_debug_info,
handle_brew_recommendations,
handle_list_brews,
)
Expand Down Expand Up @@ -195,3 +200,151 @@ def test_only_auto_updates(self, _casks, _auto, _pb):
del opts.exclude_auto_updates
brews, auto = _get_and_filter_brews(opts)
assert brews == ["firefox"]


# ---------------------------------------------------------------------------
# _display_brew_list — auto-update marker path (lines 114-115)
# ---------------------------------------------------------------------------


class TestDisplayBrewList:
"""Tests for _display_brew_list() auto-update marker path."""

@patch("versiontracker.handlers.brew_handlers.create_progress_bar", return_value=_mock_progress_bar())
def test_auto_update_marker_shown(self, _pb, capsys):
"""Brew with auto-update shows the (auto-updates) marker."""
from versiontracker.handlers.brew_handlers import _display_brew_list

opts = MagicMock(show_auto_updates=True, exclude_auto_updates=False, only_auto_updates=False)
_display_brew_list(["firefox"], ["firefox"], opts)
out = capsys.readouterr().out
assert "firefox" in out
assert "auto-updates" in out

@patch("versiontracker.handlers.brew_handlers.create_progress_bar", return_value=_mock_progress_bar())
def test_no_auto_update_marker_when_not_in_list(self, _pb, capsys):
"""Brew not in auto-update list shows no marker."""
from versiontracker.handlers.brew_handlers import _display_brew_list

opts = MagicMock(show_auto_updates=True, exclude_auto_updates=False, only_auto_updates=False)
_display_brew_list(["chrome"], ["firefox"], opts)
out = capsys.readouterr().out
assert "chrome" in out
assert "auto-updates" not in out


# ---------------------------------------------------------------------------
# _handle_export_if_requested — stdout print path (line 138)
# ---------------------------------------------------------------------------


class TestHandleExportIfRequested:
"""Tests for _handle_export_if_requested() stdout print path."""

@patch("versiontracker.handlers.brew_handlers.handle_export", return_value='[{"name":"firefox"}]')
@patch("versiontracker.handlers.brew_handlers.create_progress_bar", return_value=_mock_progress_bar())
def test_prints_to_stdout_when_no_output_file(self, _pb, mock_export, capsys):
"""When output_file is None, export result is printed to stdout."""
opts = MagicMock(export_format="json", output_file=None)
_handle_export_if_requested(["firefox"], opts)
captured = capsys.readouterr()
assert "firefox" in captured.out

@patch("versiontracker.handlers.brew_handlers.handle_export", return_value=0)
@patch("versiontracker.handlers.brew_handlers.create_progress_bar", return_value=_mock_progress_bar())
def test_no_print_when_output_file_set(self, _pb, mock_export, capsys):
"""When output_file is set, nothing is printed."""
opts = MagicMock(export_format="json", output_file="/tmp/out.json")
_handle_export_if_requested(["firefox"], opts)
captured = capsys.readouterr()
assert captured.out == ""


# ---------------------------------------------------------------------------
# handle_list_brews — outer exception handler (lines 179-182)
# ---------------------------------------------------------------------------


class TestHandleListBrewsOuterException:
"""Tests for the outer try/except in handle_list_brews."""

@patch("versiontracker.handlers.brew_handlers.create_progress_bar", return_value=_mock_progress_bar())
def test_outer_exception_returns_1(self, _pb):
"""An exception raised before the inner try returns exit code 1."""
with patch(
"versiontracker.handlers.brew_handlers.create_progress_bar",
side_effect=RuntimeError("boom"),
):
result = handle_list_brews(MagicMock())
assert result == 1


# ---------------------------------------------------------------------------
# _get_homebrew_casks — generic exception path (lines 257-260)
# ---------------------------------------------------------------------------


class TestGetHomebrewCasksHandler:
"""Tests for _get_homebrew_casks() generic exception fallback."""

@patch("versiontracker.handlers.brew_handlers.create_progress_bar", return_value=_mock_progress_bar())
@patch("versiontracker.handlers.brew_handlers.get_homebrew_casks", side_effect=OSError("disk error"))
def test_generic_exception_returns_empty(self, _casks, _pb):
"""A non-HomebrewError exception returns [] and doesn't raise."""
result = _get_homebrew_casks()
assert result == []


# ---------------------------------------------------------------------------
# _log_debug_info — debug logging paths (lines 273-283)
# ---------------------------------------------------------------------------


class TestLogDebugInfo:
"""Tests for _log_debug_info() debug logging branches."""

def test_no_debug_does_nothing(self):
"""No logging when debug is False/0."""
opts = MagicMock(debug=False)
_log_debug_info(opts, [("App", "1.0")], ["brew"], [("App", "1.0")])
# No exception means pass

def test_debug_logs_items(self):
"""All three loops execute when debug is True."""
opts = MagicMock(debug=True)
with patch("versiontracker.handlers.brew_handlers.logging") as mock_log:
_log_debug_info(
opts,
[("App", "1.0"), ("App2", "2.0")],
["brew1", "brew2"],
[("App", "1.0")],
)
assert mock_log.debug.call_count >= 6 # 3 headers + at least 3 items


# ---------------------------------------------------------------------------
# _determine_strict_mode — test-detection and strict_recommend paths
# ---------------------------------------------------------------------------


class TestDetermineStrictMode:
"""Tests for _determine_strict_mode() branches."""

def test_strict_recom_returns_true(self):
opts = MagicMock(strict_recom=True)
assert _determine_strict_mode(opts) is True

def test_test_detection_sets_mock_test(self):
"""When sys.argv has ≤1 elements, mock_test is set and False is returned."""
opts = MagicMock(spec=["strict_recommend", "recommend"])
with patch.object(sys, "argv", ["pytest"]):
result = _determine_strict_mode(opts)
assert result is False
assert opts.mock_test is True

def test_strict_recommend_returns_true(self):
"""strict_recommend attribute triggers True when argv has entries."""
opts = MagicMock(strict_recom=False, strict_recommend=True)
with patch.object(sys, "argv", ["versiontracker", "--strict-recom"]):
result = _determine_strict_mode(opts)
assert result is True
66 changes: 66 additions & 0 deletions tests/handlers/test_setup_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,69 @@ def test_handle_setup_logging_error(self, mock_logging):

# Assert
assert mock_logging.basicConfig.call_count >= 1


# ---------------------------------------------------------------------------
# handle_initialize_config — config_file path and OSError fallback (lines 37, 39-41)
# ---------------------------------------------------------------------------


class TestHandleInitializeConfigBranches:
"""Tests for uncovered branches in handle_initialize_config."""

@mock.patch("versiontracker.handlers.setup_handlers.Config")
@mock.patch("versiontracker.handlers.setup_handlers.get_config")
def test_config_file_path_used_when_no_config_attr(self, mock_get_config, mock_Config):
"""When get_config() has no _config, Config(config_file=...) is called."""
mock_cfg = mock.MagicMock(spec=[]) # no _config attribute
mock_get_config.return_value = mock_cfg
opts = mock.MagicMock()
opts.config = "/tmp/my.yaml"

handle_initialize_config(opts)

mock_Config.assert_called_once_with(config_file="/tmp/my.yaml")

@mock.patch("versiontracker.handlers.setup_handlers.Config")
@mock.patch("versiontracker.handlers.setup_handlers.get_config")
def test_oserror_falls_back_to_default_config(self, mock_get_config, mock_Config):
"""OSError during Config init triggers fallback Config() with no args."""
mock_cfg = mock.MagicMock(spec=[]) # no _config attribute
mock_get_config.return_value = mock_cfg
# First call raises OSError, second (fallback) succeeds
mock_Config.side_effect = [OSError("file not found"), mock.MagicMock()]
opts = mock.MagicMock()
opts.config = "/bad/path.yaml"

result = handle_initialize_config(opts)

assert result == 0
assert mock_Config.call_count == 2
# Fallback called with no arguments
mock_Config.assert_called_with()


# ---------------------------------------------------------------------------
# handle_setup_logging — error recovery path (line 105)
# ---------------------------------------------------------------------------


class TestHandleSetupLoggingErrorRecovery:
"""Tests for the exception recovery path in handle_setup_logging."""

@mock.patch("versiontracker.handlers.setup_handlers.logging")
def test_logs_error_after_recovery(self, mock_logging):
"""When basicConfig() raises, recovery succeeds and logs the original error."""
mock_logging.WARNING = 30
# First basicConfig call raises; second (recovery) succeeds
mock_logging.basicConfig.side_effect = [Exception("setup failed"), None]

opts = mock.MagicMock()
opts.debug = 0

handle_setup_logging(opts)

# Recovery basicConfig called
assert mock_logging.basicConfig.call_count == 2
# Error logged after recovery
mock_logging.error.assert_called_once()
66 changes: 66 additions & 0 deletions tests/handlers/test_utils_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,69 @@ def test_custom_default_value(self):
def test_passes_args_and_kwargs(self):
result = safe_function_call(lambda x, y=0: x + y, 10, y=5)
assert result == 15


# ---------------------------------------------------------------------------
# warning_filter / WarningFilter — uncovered branches (lines 92-119)
# ---------------------------------------------------------------------------


class TestSuppressConsoleWarningsInternals:
"""Tests for warning_filter() inner branches via suppress_console_warnings()."""

def _get_warning_filter(self):
"""Install suppress_console_warnings and return the attached WarningFilter."""
import sys
from unittest.mock import MagicMock

with mock.patch("logging.getLogger") as mock_get_logger, mock.patch("warnings.filterwarnings"):
mock_logger = MagicMock()
mock_handler = MagicMock()
# stream must equal sys.stderr to pass the guard in suppress_console_warnings
mock_handler.stream = sys.stderr
mock_logger.handlers = [mock_handler]
mock_get_logger.return_value = mock_logger

suppress_console_warnings()
return mock_handler.addFilter.call_args[0][0]

def test_warning_filter_versiontracker_filename_not_suppressed(self):
"""Warnings from versiontracker source files are always shown (return True)."""
import logging
from unittest.mock import MagicMock

added_filter = self._get_warning_filter()

record = MagicMock()
record.levelno = logging.WARNING
record.filename = "/path/to/versiontracker/config.py"
record.lineno = 1
record.getMessage.return_value = "some warning"

assert added_filter.filter(record) is True

def test_warning_filter_deprecation_from_external_suppressed(self):
"""UserWarning from external code is suppressed (return False)."""
import logging
from unittest.mock import MagicMock

added_filter = self._get_warning_filter()

record = MagicMock()
record.levelno = logging.WARNING
record.filename = "/site-packages/third_party/lib.py"
record.lineno = 1
record.getMessage.return_value = "deprecated"
# WarningFilter.filter() passes UserWarning as category → matches warn_type list → False
assert added_filter.filter(record) is False

def test_warning_filter_non_warning_level_passes_through(self):
"""Non-WARNING log records always pass through WarningFilter (return True)."""
import logging
from unittest.mock import MagicMock

added_filter = self._get_warning_filter()

record = MagicMock()
record.levelno = logging.INFO # not WARNING
assert added_filter.filter(record) is True
10 changes: 6 additions & 4 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -485,11 +485,13 @@ def test_concurrent_operations(self, mock_get_casks, mock_get_apps, mock_check_d
def run_operation(operation_type):
try:
if operation_type == "apps":
with patch("builtins.print"):
handle_list_apps(MagicMock(apps=True, debug=False, blacklist=None))
# Use export_format=None and output_file=None to avoid open(MagicMock())
# Do NOT use patch("builtins.print") inside threads — it's not thread-safe
handle_list_apps(
MagicMock(apps=True, debug=False, blacklist=None, export_format=None, output_file=None)
)
elif operation_type == "brews":
with patch("builtins.print"):
handle_list_brews(MagicMock(brews=True, debug=False, export_format=None))
handle_list_brews(MagicMock(brews=True, debug=False, export_format=None, output_file=None))
results.append(f"{operation_type}_success")
except Exception as e:
errors.append(f"{operation_type}_error: {e}")
Expand Down
Loading
Loading