From 3457face45d87c17794847473039001898559015 Mon Sep 17 00:00:00 2001 From: askalf <263217947+askalf@users.noreply.github.com> Date: Thu, 24 Sep 2026 07:55:22 +0000 Subject: [PATCH 1/2] fix(quicklook): keep a history for gpu_mem and gpu_proc `gpu_mem` and `gpu_proc` are selectable in `[quicklook] list=` but were never added to `items_history_list`, so `get_raw_history()` returns None for them. With --sparkline, `_msg_cpu` iterates over that None and the curses UI exits with "'NoneType' object is not iterable". --- glances/plugins/quicklook/__init__.py | 2 ++ tests/test_plugin_quicklook.py | 50 +++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/glances/plugins/quicklook/__init__.py b/glances/plugins/quicklook/__init__.py index 03f57b42df..2a23320abd 100644 --- a/glances/plugins/quicklook/__init__.py +++ b/glances/plugins/quicklook/__init__.py @@ -80,6 +80,8 @@ {'name': 'mem', 'description': 'MEM percent usage', 'y_unit': '%'}, {'name': 'swap', 'description': 'SWAP percent usage', 'y_unit': '%'}, {'name': 'load', 'description': 'LOAD percent usage', 'y_unit': '%'}, + {'name': 'gpu_mem', 'description': 'GPU memory percent usage', 'y_unit': '%'}, + {'name': 'gpu_proc', 'description': 'GPU processor percent usage', 'y_unit': '%'}, ] diff --git a/tests/test_plugin_quicklook.py b/tests/test_plugin_quicklook.py index ea86549709..690e78b119 100755 --- a/tests/test_plugin_quicklook.py +++ b/tests/test_plugin_quicklook.py @@ -10,6 +10,7 @@ """Tests for the Quicklook plugin stats list configuration.""" import os +from argparse import Namespace import pytest @@ -224,3 +225,52 @@ def test_internal_spaces_are_kept(self, tmp_path): plugin = QuicklookPlugin(args=None, config=Config(config_dir=os.fspath(config_file))) assert plugin.get_limits('alias') == ['sda1:System Disk', 'sdb1:Data Disk'] + + +class TestQuicklookGpuHistory: + """`gpu_mem` and `gpu_proc` can be listed, so --sparkline must find a history for them. + + Only the items in `items_history_list` are historised; for any other key + `get_raw_history()` returns None, and the sparkline branch of `_msg_cpu` iterates + over it: `[quicklook] list=cpu,gpu_mem` with --sparkline crashed the curses UI + with "'NoneType' object is not iterable". + """ + + @staticmethod + def _plugin(tmp_path, list_value): + config_file = tmp_path / 'glances-gpu.conf' + config_file.write_text(f'[quicklook]\nlist={list_value}\n', encoding='utf-8') + args = Namespace(sparkline=True, client=None, disable_history=False, disable_quicklook=False, percpu=False) + plugin = QuicklookPlugin(args=args, config=Config(config_dir=os.fspath(config_file))) + for percent in (10.0, 20.0, 30.0): + plugin.stats = { + 'cpu': percent, + 'percpu': [], + 'mem': percent, + 'swap': percent, + 'load': percent, + 'gpu_mem': percent, + 'gpu_proc': percent + 1, + } + plugin.update_stats_history() + plugin.update_views() + return plugin, args + + GPU_HISTORY = [('gpu_mem', [10.0, 20.0, 30.0]), ('gpu_proc', [11.0, 21.0, 31.0])] + + @pytest.mark.parametrize(('key', 'expected'), GPU_HISTORY) + def test_the_gpu_entries_are_historised(self, tmp_path, key, expected): + plugin, _ = self._plugin(tmp_path, f'cpu,{key}') + + assert [value for _, value in plugin.get_raw_history(item=key)] == expected + + @pytest.mark.parametrize(('key', 'expected'), GPU_HISTORY) + def test_a_gpu_entry_is_drawn_as_a_sparkline_of_its_history(self, tmp_path, key, expected): + sparklines = pytest.importorskip('sparklines').sparklines + plugin, args = self._plugin(tmp_path, f'cpu,{key}') + + lines = [line['msg'] for line in plugin.msg_curse(args, max_width=40)] + + # max_width=40 gives a 34-cell sparkline, padded on the right with None + drawn = sparklines(expected + [None] * (34 - len(expected)), minimum=0, maximum=100)[0] + assert lines[lines.index(f'{key.upper():4} ') + 2] == f'{drawn}{expected[-1]:5.1f}%' From eccf26b6713ff27157e8e47241643a03c7d52b71 Mon Sep 17 00:00:00 2001 From: askalf <263217947+askalf@users.noreply.github.com> Date: Fri, 25 Sep 2026 14:07:14 +0000 Subject: [PATCH 2/2] test(quicklook): pin GPU history persisting outside the quicklook list History is per plugin, not per displayed stat: a GPU entry keeps building history while list= omits it, exactly like swap today. --- tests/test_plugin_quicklook.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_plugin_quicklook.py b/tests/test_plugin_quicklook.py index 690e78b119..6dec17c9da 100755 --- a/tests/test_plugin_quicklook.py +++ b/tests/test_plugin_quicklook.py @@ -274,3 +274,15 @@ def test_a_gpu_entry_is_drawn_as_a_sparkline_of_its_history(self, tmp_path, key, # max_width=40 gives a 34-cell sparkline, padded on the right with None drawn = sparklines(expected + [None] * (34 - len(expected)), minimum=0, maximum=100)[0] assert lines[lines.index(f'{key.upper():4} ') + 2] == f'{drawn}{expected[-1]:5.1f}%' + + @pytest.mark.parametrize(('key', 'expected'), GPU_HISTORY) + def test_a_gpu_entry_is_historised_even_when_not_in_the_list(self, tmp_path, key, expected): + """History is per plugin, not per displayed stat -- exactly like `swap` today. + + `update_stats_history` iterates `get_items_history_list()`, not `stats_list`, + so a GPU entry keeps building history while only `cpu` is in `list=`. + """ + plugin, _ = self._plugin(tmp_path, 'cpu') + + assert key not in plugin.stats_list + assert [value for _, value in plugin.get_raw_history(item=key)] == expected