Skip to content

Commit

Permalink
[RAM] Adds support for BSD and its derivatives, including Darwin/macOS
Browse files Browse the repository at this point in the history
> See related : #69 & #95.
  • Loading branch information
Samuel FORESTIER committed Mar 9, 2021
1 parent 7281557 commit 2047505
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 81 deletions.
48 changes: 44 additions & 4 deletions archey/entries/ram.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""RAM usage detection class"""

import platform
import re

from subprocess import check_output
Expand Down Expand Up @@ -32,10 +33,17 @@ def _get_used_total_values(self) -> Tuple[float, float]:
Returns a tuple containing used and total RAM values.
Tries a variety of methods, increasing compatibility for a wide range of systems.
"""
try:
return self._run_free_dash_m()
except (IndexError, FileNotFoundError):
pass
if platform.system() == 'Linux':
try:
return self._run_free_dash_m()
except (IndexError, FileNotFoundError):
pass
else:
# Darwin or any other BSD-based system.
try:
return self._run_sysctl_and_vmstat()
except FileNotFoundError:
pass

try:
return self._read_proc_meminfo()
Expand Down Expand Up @@ -85,6 +93,38 @@ def _read_proc_meminfo() -> Tuple[float, float]:

return used, total

@staticmethod
def _run_sysctl_and_vmstat() -> Tuple[float, float]:
"""From `sysctl` and `vm_stat` calls, compute used and total system RAM values"""
total = float(
check_output(
['sysctl', '-n', 'hw.memsize'],
universal_newlines=True
)
)

vm_stat_lines = check_output('vm_stat', universal_newlines=True).splitlines()

# From first heading line, fetch system page size (default to 4096 bytes).
page_size_match = re.match(
r'^Mach Virtual Memory Statistics: \(page size of (\d+) bytes\)$',
vm_stat_lines[0]
)
page_size = (int(page_size_match.group(1)) if page_size_match else 4096)

# Store memory information into a dictionary.
mem_info = {}
for line in vm_stat_lines[1:]: # We ignore the first heading line.
key, value = line.split(':', maxsplit=1)
mem_info[key] = int(value.lstrip().rstrip('.'))

# Here we imitate Ansible behavior to compute used RAM.
used = total - (
mem_info['Pages wired down'] + mem_info['Pages active'] + mem_info['Pages inactive']
) * page_size

return (used / 1024**2), (total / 1024**2)


def output(self, output):
"""
Expand Down
168 changes: 91 additions & 77 deletions archey/test/entries/test_archey_ram.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,60 +21,13 @@ class TestRAMEntry(unittest.TestCase):
Mem: 15658 2043 10232 12 3382 13268
Swap: 4095 39 4056
""")
def test_free_dash_m(self, _):
"""Test `free -m` output parsing for low RAM use case"""
output_mock = MagicMock()
RAM(options={
'warning': 25,
'danger': 45
}).output(output_mock)
self.assertEqual(
output_mock.append.call_args[0][1],
f'{Colors.GREEN_NORMAL}2043 MiB{Colors.CLEAR} / 15658 MiB'
)

@patch(
'archey.entries.ram.check_output',
return_value="""\
total used free shared buff/cache available
Mem: 7412 3341 1503 761 2567 3011
Swap: 7607 5 7602
""")
def test_free_dash_m_warning(self, _):
"""Test `free -m` output parsing for warning RAM use case"""
output_mock = MagicMock()
RAM(options={
'warning': 33.3,
'danger': 66.7
}).output(output_mock)
self.assertEqual(
output_mock.append.call_args[0][1],
f'{Colors.YELLOW_NORMAL}3341 MiB{Colors.CLEAR} / 7412 MiB'
def test_run_free_dash_m(self, _):
"""Test `_run_free_dash_m` output parsing"""
self.assertTupleEqual(
RAM._run_free_dash_m(), # pylint: disable=protected-access
(2043.0, 15658.0)
)

@patch(
'archey.entries.ram.check_output',
return_value="""\
total used free shared buff/cache available
Mem: 15658 12341 624 203 2692 2807
Swap: 4095 160 3935
""")
def test_free_dash_m_danger(self, _):
"""Test `free -m` output parsing for danger RAM use case"""
output_mock = MagicMock()
RAM(options={
'warning': 25,
'danger': 45
}).output(output_mock)
self.assertEqual(
output_mock.append.call_args[0][1],
f'{Colors.RED_NORMAL}12341 MiB{Colors.CLEAR} / 15658 MiB'
)

@patch(
'archey.entries.ram.check_output',
side_effect=IndexError() # `free` call will fail
)
@patch(
'archey.entries.ram.open',
mock_open(
Expand Down Expand Up @@ -104,38 +57,99 @@ def test_free_dash_m_danger(self, _):
SReclaimable: 200792 kB
SUnreclaim: 113308 kB
""")) # Some lines have been ignored as they are useless for computations.
def test_proc_meminfo(self, _):
"""Test `/proc/meminfo` parsing (when `free` is not available)"""
self.assertDictEqual(
RAM().value,
{
'used': 3739.296875,
'total': 7403.3203125,
'unit': 'MiB'
}
def test_read_proc_meminfo(self):
"""Test `_read_proc_meminfo` content parsing"""
self.assertTupleEqual(
RAM._read_proc_meminfo(), # pylint: disable=protected-access
(3739.296875, 7403.3203125)
)

@patch(
'archey.entries.ram.check_output',
side_effect=IndexError() # `free` call will fail
)
@patch(
'archey.entries.ram.open',
side_effect=PermissionError()
)
@HelperMethods.patch_clean_configuration
def test_not_detected(self, _, __):
"""Check Archey does not crash when `/proc/meminfo` is not readable"""
ram = RAM()
side_effect=[
'8589934592\n',
"""\
Mach Virtual Memory Statistics: (page size of 4096 bytes)
Pages free: 55114.
Pages active: 511198.
Pages inactive: 488363.
Pages speculative: 22646.
Pages throttled: 0.
Pages wired down: 666080.
Pages purgeable: 56530.
"Translation faults": 170435998.
Pages copy-on-write: 3496023.
Pages zero filled: 96454484.
Pages reactivated: 12101726.
Pages purged: 6728288.
File-backed pages: 445114.
Anonymous pages: 577093.
Pages stored in compressor: 2019211.
Pages occupied by compressor: 353431.
Decompressions: 10535599.
Compressions: 19723567.
Pageins: 7586286.
Pageouts: 388644.
Swapins: 2879182.
Swapouts: 3456015.
"""])
def test_run_sysctl_and_vmstat(self, _):
"""Check `sysctl` and `vm_stat` parsing logic"""
self.assertTupleEqual(
RAM._run_sysctl_and_vmstat(), # pylint: disable=protected-access
(1685.58984375, 8192.0)
)

@HelperMethods.patch_clean_configuration
def test_various_output_configuration(self):
"""Test `output` overloading based on user preferences"""
ram_instance_mock = HelperMethods.entry_mock(RAM)
output_mock = MagicMock()
ram.output(output_mock)

self.assertIsNone(ram.value)
self.assertEqual(
output_mock.append.call_args[0][1],
DEFAULT_CONFIG['default_strings']['not_detected']
)
with self.subTest('Output in case of non-detection.'):
RAM.output(ram_instance_mock, output_mock)
self.assertEqual(
output_mock.append.call_args[0][1],
DEFAULT_CONFIG['default_strings']['not_detected']
)

output_mock.reset_mock()

with self.subTest('"Normal" output (green).'):
ram_instance_mock.value = {
'used': 2043.0,
'total': 15658.0,
'unit': 'MiB'
}
ram_instance_mock.options = {
'warning_use_percent': 33.3,
'danger_use_percent': 66.7
}

RAM.output(ram_instance_mock, output_mock)
self.assertEqual(
output_mock.append.call_args[0][1],
f'{Colors.GREEN_NORMAL}2043 MiB{Colors.CLEAR} / 15658 MiB'
)

output_mock.reset_mock()

with self.subTest('"Danger" output (red).'):
ram_instance_mock.value = {
'used': 7830.0,
'total': 15658.0,
'unit': 'MiB'
}
ram_instance_mock.options = {
'warning_use_percent': 25,
'danger_use_percent': 50
}

RAM.output(ram_instance_mock, output_mock)
self.assertEqual(
output_mock.append.call_args[0][1],
f'{Colors.RED_NORMAL}7830 MiB{Colors.CLEAR} / 15658 MiB'
)


if __name__ == '__main__':
Expand Down

0 comments on commit 2047505

Please sign in to comment.