From 48d31534f3a1891688250d06993a5b27d00abd30 Mon Sep 17 00:00:00 2001 From: Benjamin Drung Date: Tue, 21 Apr 2026 14:55:37 +0200 Subject: [PATCH] procutils: introduce parse_meminfo() Reduce code duplication by introducing a `parse_meminfo()` function. --- apport/procutils.py | 29 ++++++++++++++++++ data/apport | 16 +++------- tests/system/test_apport_valgrind.py | 7 ++--- tests/system/test_signal_crashes.py | 9 ++---- tests/unit/test_procutils.py | 45 ++++++++++++++++++++++++++++ 5 files changed, 83 insertions(+), 23 deletions(-) create mode 100644 apport/procutils.py create mode 100644 tests/unit/test_procutils.py diff --git a/apport/procutils.py b/apport/procutils.py new file mode 100644 index 000000000..370d8b735 --- /dev/null +++ b/apport/procutils.py @@ -0,0 +1,29 @@ +# Copyright (C) 2026 Canonical Ltd. +# Author: Benjamin Drung +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation; either version 2 of the License, or (at your +# option) any later version. See http://www.gnu.org/copyleft/gpl.html for +# the full text of the license. + +"""Functions to operate on files in /proc.""" + +from collections.abc import Iterable + + +def parse_meminfo(keys: Iterable[str]) -> dict[str, int]: + """Parse /proc/meminfo and return a dictionary for the requested keys.""" + remaining_keys = set(keys) + meminfo = {} + with open("/proc/meminfo", "r", encoding="utf-8") as meminfo_file: + for line in meminfo_file: + key, remaining = line.split(":", maxsplit=1) + if key not in remaining_keys: + continue + value = remaining.strip().split(maxsplit=1)[0] + meminfo[key] = int(value) + remaining_keys.remove(key) + if not remaining_keys: + return meminfo + raise KeyError(f"{' '.join(sorted(remaining_keys))} not found in /proc/meminfo") diff --git a/data/apport b/data/apport index 5e6036e6f..5c948afde 100755 --- a/data/apport +++ b/data/apport @@ -43,6 +43,7 @@ from collections.abc import Callable import apport.fileutils import apport.report +from apport.procutils import parse_meminfo from apport.user_group import UserGroupID from problem_report import CompressedFile @@ -338,20 +339,11 @@ def write_user_coredump( os.close(core_file) -def usable_ram(): +def usable_ram() -> int: """Return how many bytes of RAM is currently available that can be allocated without causing major thrashing.""" - - # abuse our excellent RFC822 parser to parse /proc/meminfo - r = apport.report.Report() - with open("/proc/meminfo", "rb") as f: - r.load(f) - - memfree = int(r["MemFree"].split()[0]) - cached = int(r["Cached"].split()[0]) - writeback = int(r["Writeback"].split()[0]) - - return (memfree + cached - writeback) * 1024 + meminfo = parse_meminfo({"Cached", "MemFree", "Writeback"}) + return (meminfo["MemFree"] + meminfo["Cached"] - meminfo["Writeback"]) * 1024 def _run_with_output_limit_and_timeout( diff --git a/tests/system/test_apport_valgrind.py b/tests/system/test_apport_valgrind.py index d1f82913c..a2b5e0b26 100644 --- a/tests/system/test_apport_valgrind.py +++ b/tests/system/test_apport_valgrind.py @@ -15,14 +15,11 @@ import tempfile import unittest +from apport.procutils import parse_meminfo from tests.helper import get_gnu_coreutils_cmd, skip_if_command_is_missing from tests.paths import local_test_environment -with open("/proc/meminfo", encoding="utf-8") as f: - for line in f.readlines(): - if line.startswith("MemTotal"): - MEM_TOTAL_MiB = int(line.split()[1]) // 1024 - break +MEM_TOTAL_MiB = parse_meminfo({"MemTotal"})["MemTotal"] @skip_if_command_is_missing("valgrind") diff --git a/tests/system/test_signal_crashes.py b/tests/system/test_signal_crashes.py index aed51793b..546d0aabf 100644 --- a/tests/system/test_signal_crashes.py +++ b/tests/system/test_signal_crashes.py @@ -17,6 +17,7 @@ import apport.fileutils import apport.report +from apport.procutils import parse_meminfo from tests.helper import ( get_gnu_coreutils_cmd, get_init_system, @@ -159,12 +160,8 @@ def test_limit_size(self) -> None: assert self.apport_path is not None # determine how much data we have to pump into apport in order to make # sure that it will refuse the core dump - r = apport.report.Report() - with open("/proc/meminfo", "rb") as f: - r.load(f) - totalmb = int(r["MemFree"].split()[0]) + int(r["Cached"].split()[0]) - totalmb = int(totalmb / 1024) - del r + meminfo = parse_meminfo({"Cached", "MemFree"}) + totalmb = (meminfo["MemFree"] + meminfo["Cached"]) // 1024 test_proc = self.create_test_process() try: diff --git a/tests/unit/test_procutils.py b/tests/unit/test_procutils.py new file mode 100644 index 000000000..9ee97a976 --- /dev/null +++ b/tests/unit/test_procutils.py @@ -0,0 +1,45 @@ +# Copyright (C) 2026 Canonical Ltd. +# Author: Benjamin Drung +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation; either version 2 of the License, or (at your +# option) any later version. See http://www.gnu.org/copyleft/gpl.html for +# the full text of the license. + +"""Unit tests for the apport.procutils module.""" + +from unittest.mock import mock_open, patch + +import pytest + +from apport.procutils import parse_meminfo + + +def test_parse_meminfo() -> None: + """Test parse_meminfo() reading all values successfully.""" + open_mock = mock_open( + read_data=( + "MemTotal: 128887668 kB\n" + "MemFree: 81203612 kB\n" + "HugePages_Total: 0\n" + "DirectMap1G: 127926272 kB\n" + ) + ) + with patch("builtins.open", open_mock): + meminfo = parse_meminfo({"MemTotal", "MemFree", "HugePages_Total"}) + assert meminfo["MemTotal"] == 128887668 + assert meminfo["MemFree"] == 81203612 + assert meminfo["HugePages_Total"] == 0 + assert len(meminfo) == 3 + + +def test_parse_meminfo_missing_key() -> None: + """Test parse_meminfo() failing to read the requested keys.""" + open_mock = mock_open( + read_data=("MemTotal: 128887668 kB\nMemFree: 81203612 kB\n") + ) + with patch("builtins.open", open_mock): + with pytest.raises(KeyError) as exc_info: + parse_meminfo({"MemTotal", "MemAvailable"}) + exc_info.match("MemAvailable not found in /proc/meminfo")