From 4310bb49970b2a48ffdb36c4d2a785a69f2f02d8 Mon Sep 17 00:00:00 2001 From: Benjamin Drung Date: Wed, 20 May 2026 13:09:18 +0200 Subject: [PATCH] procutils: introduce parse_meminfo() Reduce code duplication by introducing a `parse_meminfo()` function. --- apport/procutils.py | 51 ++++++++++++++++++++++++++++ data/apport | 16 +++------ tests/system/test_apport_valgrind.py | 7 ++-- tests/system/test_signal_crashes.py | 9 ++--- tests/unit/test_procutils.py | 50 +++++++++++++++++++++++++++ 5 files changed, 110 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..10c380dab --- /dev/null +++ b/apport/procutils.py @@ -0,0 +1,51 @@ +# 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.""" + +import dataclasses + + +@dataclasses.dataclass +class Meminfo: + """Data from /proc/meminfo. Values are in KiB. + + Only the needed values from /proc/meminfo are provided. + This dataclass can be extended when needed. + """ + + mem_total: int + mem_free: int + mem_available: int + cached: int + writeback: int + + +def parse_meminfo() -> Meminfo: + """Parse /proc/meminfo and return a dictionary for the requested keys.""" + remaining_keys = {"MemTotal", "MemFree", "MemAvailable", "Cached", "Writeback"} + 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, unit = remaining.strip().split(maxsplit=2) + assert unit == "kB" or value == "0" + meminfo[key] = int(value) + remaining_keys.remove(key) + if not remaining_keys: + return Meminfo( + meminfo["MemTotal"], + meminfo["MemFree"], + meminfo["MemAvailable"], + meminfo["Cached"], + meminfo["Writeback"], + ) + raise KeyError(f"{', '.join(sorted(remaining_keys))} not found in /proc/meminfo") diff --git a/data/apport b/data/apport index 52bd43534..a2690434f 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 @@ -339,20 +340,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() + return (meminfo.mem_free + 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 294ddcfe9..6bef28b8f 100644 --- a/tests/system/test_apport_valgrind.py +++ b/tests/system/test_apport_valgrind.py @@ -17,14 +17,11 @@ import pytest +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().mem_total // 1024 @skip_if_command_is_missing("valgrind") diff --git a/tests/system/test_signal_crashes.py b/tests/system/test_signal_crashes.py index aed51793b..30baf2505 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() + totalmb = (meminfo.mem_free + 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..6d3d7f17e --- /dev/null +++ b/tests/unit/test_procutils.py @@ -0,0 +1,50 @@ +# 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 Meminfo, parse_meminfo + + +def test_parse_meminfo() -> None: + """Test parse_meminfo() reading all values successfully.""" + open_mock = mock_open( + read_data=( + "MemTotal: 64249228 kB\n" + "MemFree: 25066812 kB\n" + "MemAvailable: 40437820 kB\n" + "Buffers: 587236 kB\n" + "Cached: 19694816 kB\n" + "Writeback: 0 kB\n" + ) + ) + with patch("builtins.open", open_mock): + meminfo = parse_meminfo() + assert meminfo == Meminfo(64249228, 25066812, 40437820, 19694816, 0) + + +def test_parse_meminfo_missing_key() -> None: + """Test parse_meminfo() failing to read the requested keys.""" + open_mock = mock_open( + read_data=( + "MemTotal: 64249228 kB\n" + "MemFree: 23434628 kB\n" + "MemAvailable: 38809072 kB\n" + "Buffers: 589372 kB\n" + "Cached: 20099444 kB\n" + ) + ) + with patch("builtins.open", open_mock): + with pytest.raises(KeyError) as exc_info: + parse_meminfo() + exc_info.match("Writeback not found in /proc/meminfo")