Files
glances/tests/test_plugin_docker.py
2026-05-31 17:34:25 +02:00

120 lines
4.1 KiB
Python
Executable File

#!/usr/bin/env python
#
# Glances - An eye on your system
#
# SPDX-FileCopyrightText: 2026 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Tests for the Docker container engine (issue #3559 performance fix)."""
from unittest.mock import MagicMock, patch
import pytest
from glances.plugins.containers.engines import docker as docker_engine
class FakeImage:
def __init__(self, tags):
self.tags = tags
class FakeContainer:
"""Minimal container double counting image lookups and inspects (reload)."""
def __init__(self, cid, status="exited", tags=None):
self.id = cid
self.name = f"name-{cid}"
self.attrs = {
"State": {"Status": status},
"Created": "2026-01-01T00:00:00Z",
"Config": {"Entrypoint": None, "Cmd": ["sh"]},
"HostConfig": {},
}
self._tags = tags if tags is not None else [f"img-{cid}:latest"]
self.ports = {}
self.image_access_count = 0
self.reload_count = 0
@property
def image(self):
# container.image performs an inspect_image API call in docker-py
self.image_access_count += 1
return FakeImage(self._tags)
def reload(self):
# reload() performs the per-container inspect in docker-py
self.reload_count += 1
def make_extension(containers):
"""Build a DockerExtension without connecting to a real daemon."""
ext = docker_engine.DockerExtension.__new__(docker_engine.DockerExtension)
ext.disable = False
ext.display_error = True
ext.ext_name = "containers (Docker)"
ext.stats_fetchers = {}
ext.image_cache = {}
ext.client = MagicMock()
ext.client.containers.list.return_value = containers
return ext
def test_list_is_sparse():
"""The list must be fetched sparse (single API call, no per-container inspect)."""
containers = [FakeContainer("a"), FakeContainer("b")]
ext = make_extension(containers)
with patch.object(docker_engine, "DockerStatsFetcher", MagicMock()):
ext.update(all_tag=True)
ext.client.containers.list.assert_called_once_with(all=True, sparse=True)
def test_containers_are_inspected_every_cycle():
"""Each container is inspected (reload) once per update cycle (fresh status/uptime)."""
containers = [FakeContainer("a"), FakeContainer("b"), FakeContainer("c")]
ext = make_extension(containers)
with patch.object(docker_engine, "DockerStatsFetcher", MagicMock()):
ext.update(all_tag=True)
ext.update(all_tag=True)
assert all(c.reload_count == 2 for c in containers)
def test_image_is_cached_across_cycles():
"""Issue #3559: image (inspect_image) must be looked up once, then cached."""
containers = [FakeContainer("a"), FakeContainer("b")]
ext = make_extension(containers)
with patch.object(docker_engine, "DockerStatsFetcher", MagicMock()):
ext.update(all_tag=True)
ext.update(all_tag=True)
ext.update(all_tag=True)
# 3 cycles, but the immutable image is only fetched once per container
assert all(c.image_access_count == 1 for c in containers)
def test_image_field_format_preserved():
"""The exposed image field keeps its historical shape (tuple of joined tags)."""
containers = [FakeContainer("a", tags=["redis:7"])]
ext = make_extension(containers)
with patch.object(docker_engine, "DockerStatsFetcher", MagicMock()):
_, stats = ext.update(all_tag=True)
assert stats[0]["image"] == ("redis:7",)
def test_image_cache_evicts_removed_containers():
"""Image cache must not grow unbounded: entries for gone containers are dropped."""
first = [FakeContainer("a"), FakeContainer("b")]
ext = make_extension(first)
with patch.object(docker_engine, "DockerStatsFetcher", MagicMock()):
ext.update(all_tag=True)
assert set(ext.image_cache) == {"a", "b"}
# Container "b" disappears
ext.client.containers.list.return_value = [FakeContainer("a")]
ext.update(all_tag=True)
assert set(ext.image_cache) == {"a"}
if __name__ == "__main__":
pytest.main([__file__, "-v"])