Files
glances/tests/test_glances_curses.py
T
datrixlab d2836579c5 Let --disable-separator and --disable-bg override glances.conf
_GlancesCurses.load_config read [outputs] separator and disable_bg with the
command-line value passed in as the default, so any value present in the
file won: with separator=True in glances.conf, `glances --disable-separator`
still drew the separator, and so did --disable-unicode, undoing main.py's
"Unicode => No separator". With disable_bg=False in the file, --disable-bg
was ignored. docs/config.rst says options given on the command line
override the configuration files.

Both flags only move one way -- --disable-separator can only turn the
separator off and --disable-bg can only turn the background off -- so keep
the value when the flag is set and let the file decide otherwise. With no
flag, or with the key absent, the result is unchanged.
2026-09-11 12:14:21 +07:00

251 lines
8.5 KiB
Python

import curses
from types import SimpleNamespace
from unittest.mock import MagicMock, Mock
import pytest
from glances.outputs.glances_curses import _GlancesCurses
@pytest.fixture
def glancescreen():
"""Create a lightweight _GlancesCurses instance for helper method testing.
The full curses interface is intentionally bypassed in order to isolate
and test layout/helper logic independently from terminal rendering.
"""
# Bypass full curses initialization and create a lightweight instance
screen = _GlancesCurses.__new__(_GlancesCurses)
# Mock runtime arguments required by helper methods
screen.args = SimpleNamespace(
disable_cpu=False,
disable_mem=False,
disable_load=False,
disable_quicklook=False,
full_quicklook=False,
)
# Mock terminal dimensions
screen.term_window = Mock()
screen.term_window.getmaxyx.return_value = (24, 120)
# Mock rendering-related methods to isolate layout logic
screen.display_plugin = Mock()
screen.new_column = Mock()
# Initialize required internal state
screen.space_between_column = 3
screen._quicklook_max_width = 100
screen._top = ['cpu', 'mem', 'load']
return screen
class TestDisplayTopHelpers:
"""Tests for helper methods extracted from __display_top."""
def test_get_stats_summary(self, glancescreen):
"""Ensure plugin width totals and active plugin counts are computed correctly."""
stat_display = {
'cpu': {'msgdict': ['cpu']},
'mem': {'msgdict': ['mem']},
'load': {'msgdict': []},
}
plugin_widths = {
'cpu': 10,
'mem': 20,
'load': 5,
}
stats_width, stats_number = glancescreen._get_stats_summary(
stat_display,
plugin_widths,
)
assert stats_width == 35 # nosec B101
assert stats_number == 2 # nosec B101
def test_compute_spacing_single_plugin(self, glancescreen):
"""Ensure spacing logic behaves correctly when only one plugin is displayed."""
stat_display = {
'cpu': {'msgdict': ['cpu']},
}
plugin_widths = {
'cpu': 10,
}
glancescreen._top = ['cpu']
plugin_display_optional, _ = glancescreen._compute_spacing_and_optional(
stat_display,
plugin_widths,
stats_width=10,
stats_number=1,
)
assert glancescreen.space_between_column == 0 # nosec B101
assert plugin_display_optional['cpu'] is True # nosec B101
def test_compute_spacing_disables_optional_stats(
self,
glancescreen,
):
"""Ensure optional CPU and MEM display elements are disabled when spacing is constrained."""
stat_display = {
'cpu': {'msgdict': ['cpu']},
'mem': {'msgdict': ['mem']},
}
plugin_widths = {
'cpu': 80,
'mem': 80,
}
glancescreen._top = ['cpu', 'mem']
# Simulate a constrained terminal width
glancescreen.term_window.getmaxyx.return_value = (24, 40)
# Mock reduced widths after optional content removal
glancescreen.get_stats_display_width = MagicMock(return_value=20)
plugin_display_optional, _ = glancescreen._compute_spacing_and_optional(
stat_display,
plugin_widths,
stats_width=160,
stats_number=2,
)
assert plugin_display_optional['cpu'] is False # nosec B101
assert plugin_display_optional['mem'] is False # nosec B101
assert glancescreen.space_between_column >= 1 # nosec B101
def test_get_plugin_width(self, glancescreen):
"""Ensure plugin widths are correctly retrieved and mapped."""
stat_display = {
'cpu': {'msgdict': ['cpu']},
'mem': {'msgdict': ['mem']},
}
glancescreen._top = ['cpu', 'mem']
glancescreen.args.disable_cpu = False
glancescreen.args.disable_mem = False
glancescreen.get_stats_display_width = MagicMock(side_effect=[10, 20])
plugin_widths = glancescreen._get_plugin_width(stat_display)
assert plugin_widths == {
'cpu': 10,
'mem': 20,
} # nosec B101
def test_handle_quicklook_plugin_unavailable(
self,
glancescreen,
):
"""Ensure unavailable quicklook plugins are handled gracefully."""
stats = Mock()
# Simulate unavailable quicklook plugin
stats.get_plugin.side_effect = AttributeError
stat_display = {
'quicklook': {'msgdict': []},
}
plugin_widths = {}
result_widths, result_stats_width = glancescreen._handle_quicklook_for_display(
stat_display,
stats,
plugin_widths,
stats_width=0,
stats_number=1,
)
assert result_widths == plugin_widths # nosec B101
assert result_stats_width == 0 # nosec B101
class TestCursorDisable:
"""Test cursor disabling in client/server mode (see issue #3221)."""
@pytest.fixture
def screen(self, glancescreen):
"""Extend the lightweight screen with cursor-related state."""
glancescreen.args.disable_cursor = True
glancescreen.args.cursor_process_name_position = 3
glancescreen.args.arrow_keys_sort = False
return glancescreen
def _dispatch(self, screen, key):
"""Invoke the dispatch table with a given pressed key."""
screen.pressedkey = key
screen.catch_other_actions_maybe_return_to_browser(return_to_browser=False)
def test_process_name_left_is_noop_when_cursor_disabled(self, screen):
"""Left arrow must not scroll the process name when cursor is disabled."""
self._dispatch(screen, curses.KEY_LEFT)
assert screen.args.cursor_process_name_position == 3 # nosec B101
def test_process_name_right_is_noop_when_cursor_disabled(self, screen):
"""Right arrow must not scroll the process name when cursor is disabled."""
self._dispatch(screen, curses.KEY_RIGHT)
assert screen.args.cursor_process_name_position == 3 # nosec B101
def test_process_name_right_advances_when_cursor_enabled(self, glancescreen):
"""When cursor is enabled, right arrow advances the name position."""
glancescreen.args.disable_cursor = False
glancescreen.args.cursor_process_name_position = 3
glancescreen.args.arrow_keys_sort = False
self._dispatch(glancescreen, curses.KEY_RIGHT)
assert glancescreen.args.cursor_process_name_position == 4 # nosec B101
def test_process_name_left_decrements_when_cursor_enabled(self, glancescreen):
"""When cursor is enabled, left arrow decrements the name position."""
glancescreen.args.disable_cursor = False
glancescreen.args.cursor_process_name_position = 3
glancescreen.args.arrow_keys_sort = False
self._dispatch(glancescreen, curses.KEY_LEFT)
assert glancescreen.args.cursor_process_name_position == 2 # nosec B101
class TestLoadConfigPrecedence:
"""docs/config.rst: options given on the command line override the file."""
@staticmethod
def load(outputs, **args):
from glances.config import Config
config = Config()
config.parser.read_dict({'outputs': outputs})
screen = _GlancesCurses.__new__(_GlancesCurses)
screen.args = SimpleNamespace(**args)
screen._left_sidebar = ['network']
screen.load_config(config)
return screen.args
def test_disable_separator_flag_beats_config(self):
# --disable-separator, or --disable-unicode via main.py, leaves it False.
args = self.load({'separator': 'True'}, enable_separator=False, disable_bg=False)
assert args.enable_separator is False
def test_disable_bg_flag_beats_config(self):
args = self.load({'disable_bg': 'False'}, enable_separator=True, disable_bg=True)
assert args.disable_bg is True
def test_config_applies_when_no_flag_is_given(self):
args = self.load({'separator': 'False', 'disable_bg': 'True'}, enable_separator=True, disable_bg=False)
assert args.enable_separator is False
assert args.disable_bg is True
def test_defaults_are_kept_when_the_keys_are_absent(self):
args = self.load({'left_menu': 'network'}, enable_separator=True, disable_bg=False)
assert args.enable_separator is True
assert args.disable_bg is False