mirror of
https://github.com/HorlogeSkynet/archey4
synced 2025-05-08 20:00:12 +02:00
[CPU] Reworks entry to correctly deal with dual sockets CPUs
> Now compatible against Python < 3.6 > Caution : new proposal so still API-breaking
This commit is contained in:
parent
25b9c47e69
commit
308b41ad7f
archey
@ -12,15 +12,27 @@ class CPU(Entry):
|
||||
Parse `/proc/cpuinfo` file to retrieve model names.
|
||||
If no information could be retrieved, call `lscpu`.
|
||||
|
||||
`value` attribute is populated as a `dict`.
|
||||
It means that for Python < 3.6, "physical" CPU order **MAY** be lost.
|
||||
`value` attribute is populated as a `list` of `dict`.
|
||||
Each `dict` **SHOULD** contain only one entry (CPU model name as key and cores count as value).
|
||||
"""
|
||||
_MODEL_NAME_REGEXP = re.compile(
|
||||
r'^model name\s*:\s*(.*)$',
|
||||
flags=re.IGNORECASE | re.MULTILINE
|
||||
)
|
||||
_CPUS_COUNT_REGEXP = re.compile(
|
||||
r'^CPU\(s\)\s*:\s*(\d+)$',
|
||||
_PHYSICAL_ID_REGEXP = re.compile(
|
||||
r'^physical id\s*:\s*(\d+)$',
|
||||
flags=re.IGNORECASE | re.MULTILINE
|
||||
)
|
||||
_THREADS_PER_CORE_REGEXP = re.compile(
|
||||
r'^Thread\(s\) per core\s*:\s*(\d+)$',
|
||||
flags=re.IGNORECASE | re.MULTILINE
|
||||
)
|
||||
_CORES_PER_SOCKET_REGEXP = re.compile(
|
||||
r'^Core\(s\) per socket\s*:\s*(\d+)$',
|
||||
flags=re.IGNORECASE | re.MULTILINE
|
||||
)
|
||||
_SOCKETS_REGEXP = re.compile(
|
||||
r'^Socket\(s\)\s*:\s*(\d+)$',
|
||||
flags=re.IGNORECASE | re.MULTILINE
|
||||
)
|
||||
|
||||
@ -40,22 +52,29 @@ class CPU(Entry):
|
||||
"""Read `/proc/cpuinfo` and search for CPU model names occurrences"""
|
||||
try:
|
||||
with open('/proc/cpuinfo') as f_cpu_info:
|
||||
cpu_models = cls._MODEL_NAME_REGEXP.findall(f_cpu_info.read())
|
||||
cpu_info = f_cpu_info.read()
|
||||
except (PermissionError, FileNotFoundError):
|
||||
return {}
|
||||
return []
|
||||
|
||||
model_names = cls._MODEL_NAME_REGEXP.findall(cpu_info)
|
||||
physical_ids = cls._PHYSICAL_ID_REGEXP.findall(cpu_info)
|
||||
|
||||
cpus_list = []
|
||||
|
||||
# Manually de-duplicates CPUs count.
|
||||
cpu_info = {}
|
||||
for cpu_model in cpu_models:
|
||||
for model_name, physical_id in zip(model_names, physical_ids):
|
||||
# Sometimes CPU model names contain extra ugly white-spaces.
|
||||
cpu_model = re.sub(r'\s+', ' ', cpu_model)
|
||||
model_name = re.sub(r'\s+', ' ', model_name)
|
||||
|
||||
if cpu_model not in cpu_info:
|
||||
cpu_info[cpu_model] = 1
|
||||
else:
|
||||
cpu_info[cpu_model] += 1
|
||||
try:
|
||||
cpus_list[int(physical_id)][model_name] += 1
|
||||
except KeyError:
|
||||
# Different CPUs with same physical ids ? Shouldn't happen.
|
||||
cpus_list[int(physical_id)][model_name] = 1
|
||||
except IndexError:
|
||||
cpus_list.append({model_name: 1})
|
||||
|
||||
return cpu_info
|
||||
return cpus_list
|
||||
|
||||
@classmethod
|
||||
def _parse_lscpu_output(cls):
|
||||
@ -65,38 +84,43 @@ class CPU(Entry):
|
||||
env={'LANG': 'C'}, universal_newlines=True
|
||||
)
|
||||
|
||||
cpu_models = cls._MODEL_NAME_REGEXP.findall(cpu_info)
|
||||
cpu_counts = cls._CPUS_COUNT_REGEXP.findall(cpu_info)
|
||||
nb_threads = cls._THREADS_PER_CORE_REGEXP.findall(cpu_info)
|
||||
nb_cores = cls._CORES_PER_SOCKET_REGEXP.findall(cpu_info)
|
||||
nb_sockets = cls._SOCKETS_REGEXP.findall(cpu_info)
|
||||
model_names = cls._MODEL_NAME_REGEXP.findall(cpu_info)
|
||||
|
||||
return {
|
||||
# Sometimes CPU model names contain extra ugly white-spaces.
|
||||
re.sub(r'\s+', ' ', cpu_model): int(cpu_count)
|
||||
for cpu_model, cpu_count in zip(cpu_models, cpu_counts)
|
||||
}
|
||||
cpus_list = []
|
||||
|
||||
for threads, cores, sockets, model_name in zip(
|
||||
nb_threads, nb_cores, nb_sockets, model_names
|
||||
):
|
||||
for _ in range(int(sockets)):
|
||||
# Sometimes CPU model names contain extra ugly white-spaces.
|
||||
cpus_list.append(
|
||||
{re.sub(r'\s+', ' ', model_name): int(threads) * int(cores)}
|
||||
)
|
||||
|
||||
return cpus_list
|
||||
|
||||
def output(self, output):
|
||||
"""Writes CPUs to `output` based on preferences"""
|
||||
def _pre_format(cpu_model, cpu_count):
|
||||
"""Simple closure to format our CPU final entry content"""
|
||||
if cpu_count > 1 and self.options.get('show_count', True):
|
||||
return '{} x {}'.format(cpu_count, cpu_model)
|
||||
|
||||
return cpu_model
|
||||
|
||||
# No CPU could be detected.
|
||||
if not self.value:
|
||||
output.append(self.name, self._default_strings.get('not_detected'))
|
||||
# One-line output is enabled : Join the results !
|
||||
elif self.options.get('one_line', True):
|
||||
output.append(
|
||||
self.name,
|
||||
', '.join([
|
||||
_pre_format(cpu_model, cpu_count)
|
||||
for cpu_model, cpu_count in self.value.items()
|
||||
])
|
||||
)
|
||||
# One-line output has been disabled, add one entry per item.
|
||||
return
|
||||
|
||||
entries = []
|
||||
for cpus in self.value:
|
||||
for model_name, cpu_count in cpus.items():
|
||||
if cpu_count > 1 and self.options.get('show_count', True):
|
||||
entries.append('{} x {}'.format(cpu_count, model_name))
|
||||
else:
|
||||
entries.append(model_name)
|
||||
|
||||
if self.options.get('one_line', True):
|
||||
# One-line output is enabled : Join the results !
|
||||
output.append(self.name, ', '.join(entries))
|
||||
else:
|
||||
for cpu_model, cpu_count in self.value.items():
|
||||
output.append(self.name, _pre_format(cpu_model, cpu_count))
|
||||
# One-line output has been disabled, add one entry per item.
|
||||
for entry in entries:
|
||||
output.append(self.name, entry)
|
||||
|
@ -1,15 +1,15 @@
|
||||
"""Test module for Archey's CPU detection module"""
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, call, mock_open, patch
|
||||
|
||||
from archey.entries.cpu import CPU
|
||||
from archey.test import CustomAssertions
|
||||
from archey.test.entries import HelperMethods
|
||||
from archey.constants import DEFAULT_CONFIG
|
||||
|
||||
|
||||
class TestCPUEntry(unittest.TestCase):
|
||||
class TestCPUEntry(unittest.TestCase, CustomAssertions):
|
||||
"""
|
||||
Here, we mock the `open` call on `/proc/cpuinfo` with fake content.
|
||||
In some cases, `lscpu` output is being mocked too.
|
||||
@ -23,14 +23,15 @@ vendor_id\t: CPU-VENDOR-NAME
|
||||
cpu family\t: X
|
||||
model\t\t: YY
|
||||
model name\t: CPU-MODEL-NAME
|
||||
physical id\t: 0
|
||||
"""),
|
||||
create=True
|
||||
)
|
||||
def test_model_name_match_cpuinfo(self):
|
||||
def test_parse_proc_cpuinfo_one_entry(self):
|
||||
"""Test `/proc/cpuinfo` parsing"""
|
||||
self.assertDictEqual(
|
||||
CPU().value,
|
||||
{'CPU-MODEL-NAME': 1}
|
||||
self.assertListEqual(
|
||||
CPU._parse_proc_cpuinfo(), # pylint: disable=protected-access
|
||||
[{'CPU-MODEL-NAME': 1}]
|
||||
)
|
||||
|
||||
@patch(
|
||||
@ -42,29 +43,32 @@ vendor_id\t: CPU-VENDOR-NAME
|
||||
cpu family\t: X
|
||||
model\t\t: YY
|
||||
model name\t: CPU-MODEL-NAME
|
||||
physical id\t: 0
|
||||
|
||||
processor\t: 0
|
||||
processor\t: 1
|
||||
vendor_id\t: CPU-VENDOR-NAME
|
||||
cpu family\t: X
|
||||
model\t\t: YY
|
||||
model name\t: ANOTHER-CPU-MODEL
|
||||
physical id\t: 1
|
||||
|
||||
processor\t: 0
|
||||
processor\t: 2
|
||||
vendor_id\t: CPU-VENDOR-NAME
|
||||
cpu family\t: X
|
||||
model\t\t: YY
|
||||
model name\t: ANOTHER-CPU-MODEL
|
||||
physical id\t: 1
|
||||
"""),
|
||||
create=True
|
||||
)
|
||||
def test_multiple_cpus_from_proc_cpuinfo(self):
|
||||
def test_parse_proc_cpuinfo_multiple_entries(self):
|
||||
"""Test `/proc/cpuinfo` parsing"""
|
||||
self.assertDictEqual(
|
||||
CPU().value,
|
||||
{
|
||||
'CPU-MODEL-NAME': 1,
|
||||
'ANOTHER-CPU-MODEL': 2
|
||||
}
|
||||
self.assertListEqual(
|
||||
CPU._parse_proc_cpuinfo(), # pylint: disable=protected-access
|
||||
[
|
||||
{'CPU-MODEL-NAME': 1},
|
||||
{'ANOTHER-CPU-MODEL': 2}
|
||||
]
|
||||
)
|
||||
|
||||
@patch(
|
||||
@ -75,37 +79,40 @@ processor\t: 0
|
||||
vendor_id\t: CPU-VENDOR-NAME
|
||||
cpu family\t: X
|
||||
model\t\t: YY
|
||||
model name\t: CPU-MODEL-NAME
|
||||
physical id\t: 0
|
||||
|
||||
processor\t: 1
|
||||
vendor_id\t: CPU-VENDOR-NAME
|
||||
cpu family\t: X
|
||||
model\t\t: YY
|
||||
model name\t: CPU-MODEL-NAME
|
||||
physical id\t: 1
|
||||
|
||||
processor\t: 2
|
||||
vendor_id\t: CPU-VENDOR-NAME
|
||||
cpu family\t: X
|
||||
model\t\t: YY
|
||||
model name\t: CPU-MODEL-NAME
|
||||
physical id\t: 0
|
||||
|
||||
processor\t: 3
|
||||
vendor_id\t: CPU-VENDOR-NAME
|
||||
cpu family\t: X
|
||||
model\t\t: YY
|
||||
model name\t: CPU-MODEL-NAME
|
||||
physical id\t: 1
|
||||
"""),
|
||||
create=True
|
||||
)
|
||||
@patch(
|
||||
'archey.entries.cpu.check_output',
|
||||
return_value="""\
|
||||
Architecture: x86_64
|
||||
CPU op-mode(s): 32-bit, 64-bit
|
||||
Byte Order: Little Endian
|
||||
CPU(s): 4
|
||||
On-line CPU(s) list: 0-3
|
||||
Thread(s) per core: X
|
||||
Core(s) per socket: Y
|
||||
Socket(s): 1
|
||||
NUMA node(s): 1
|
||||
Vendor ID: CPU-VENDOR-NAME
|
||||
CPU family: Z
|
||||
Model: \xde\xad\xbe\xef
|
||||
Model name: CPU-MODEL-NAME-WITHOUT-PROC-CPUINFO
|
||||
""")
|
||||
def test_model_name_match_lscpu(self, _):
|
||||
"""
|
||||
Test model name parsing from `lscpu` output.
|
||||
|
||||
See issue #29 (ARM architectures).
|
||||
`/proc/cpuinfo` will not contain `model name` info.
|
||||
`lscpu` output will be used instead.
|
||||
"""
|
||||
self.assertDictEqual(
|
||||
CPU().value,
|
||||
{'CPU-MODEL-NAME-WITHOUT-PROC-CPUINFO': 4}
|
||||
def test_parse_proc_cpuinfo_one_cpu_dual_socket(self):
|
||||
"""Test `/proc/cpuinfo` parsing for same CPU model across two sockets"""
|
||||
self.assertListEqual(
|
||||
CPU._parse_proc_cpuinfo(), # pylint: disable=protected-access
|
||||
[
|
||||
{'CPU-MODEL-NAME': 2},
|
||||
{'CPU-MODEL-NAME': 2}
|
||||
]
|
||||
)
|
||||
|
||||
@patch(
|
||||
@ -116,15 +123,48 @@ processor\t: 0
|
||||
vendor_id\t: CPU-VENDOR-NAME
|
||||
cpu family\t: X
|
||||
model\t\t: YY
|
||||
model name\t: CPU MODEL\t NAME
|
||||
model name\t: CPU-MODEL-NAME
|
||||
physical id\t: 0
|
||||
|
||||
processor\t: 1
|
||||
vendor_id\t: CPU-VENDOR-NAME
|
||||
cpu family\t: X
|
||||
model\t\t: YY
|
||||
model name\t: ANOTHER-CPU-MODEL
|
||||
physical id\t: 0
|
||||
|
||||
processor\t: 2
|
||||
vendor_id\t: CPU-VENDOR-NAME
|
||||
cpu family\t: X
|
||||
model\t\t: YY
|
||||
model name\t: ANOTHER\tCPU MODEL WITH STRANGE S P A C E S
|
||||
physical id\t: 1
|
||||
|
||||
processor\t: 3
|
||||
vendor_id\t: CPU-VENDOR-NAME
|
||||
cpu family\t: X
|
||||
model\t\t: YY
|
||||
model name\t: ANOTHER\tCPU MODEL WITH STRANGE S P A C E S
|
||||
physical id\t: 1
|
||||
"""),
|
||||
create=True
|
||||
)
|
||||
def test_spaces_squeezing(self):
|
||||
"""Test name sanitizing, needed on some platforms"""
|
||||
self.assertDictEqual(
|
||||
CPU().value,
|
||||
{'CPU MODEL NAME': 1}
|
||||
def test_parse_proc_cpuinfo_multiple_inconsistent_entries(self):
|
||||
"""
|
||||
Test `/proc/cpuinfo` parsing with multiple CPUs sharing same physical ids (unlikely).
|
||||
Also check our model name normalizations applied on white characters.
|
||||
"""
|
||||
self.assertListEqual(
|
||||
CPU._parse_proc_cpuinfo(), # pylint: disable=protected-access
|
||||
[
|
||||
{
|
||||
'CPU-MODEL-NAME': 1,
|
||||
'ANOTHER-CPU-MODEL': 1
|
||||
},
|
||||
{
|
||||
'ANOTHER CPU MODEL WITH STRANGE S P A C E S': 2
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
@patch(
|
||||
@ -132,29 +172,103 @@ model name\t: CPU MODEL\t NAME
|
||||
side_effect=PermissionError(),
|
||||
create=True
|
||||
)
|
||||
def test_parse_proc_cpuinfo_unreadable_file(self, _):
|
||||
"""Check behavior when `/proc/cpuinfo` could not be read from disk"""
|
||||
self.assertListEmpty(CPU._parse_proc_cpuinfo()) # pylint: disable=protected-access
|
||||
|
||||
@patch(
|
||||
'archey.entries.cpu.check_output',
|
||||
return_value="""\
|
||||
side_effect=[
|
||||
"""\
|
||||
Architecture: x86_64
|
||||
CPU op-mode(s): 32-bit, 64-bit
|
||||
Byte Order: Little Endian
|
||||
CPU(s): 4
|
||||
On-line CPU(s) list: 0-3
|
||||
Thread(s) per core: X
|
||||
Core(s) per socket: Y
|
||||
Thread(s) per core: 1
|
||||
Core(s) per socket: 4
|
||||
Socket(s): 1
|
||||
NUMA node(s): 1
|
||||
Vendor ID: CPU-VENDOR-NAME
|
||||
CPU family: Z
|
||||
Model: \xde\xad\xbe\xef
|
||||
Model name: CPU-MODEL-NAME-WITHOUT-PROC-CPUINFO
|
||||
""")
|
||||
def test_proc_cpuinfo_unreadable(self, _, __):
|
||||
"""Check Archey does not crash when `/proc/cpuinfo` is not readable"""
|
||||
self.assertDictEqual(
|
||||
CPU().value,
|
||||
{'CPU-MODEL-NAME-WITHOUT-PROC-CPUINFO': 4}
|
||||
)
|
||||
Model name: CPU-MODEL-NAME
|
||||
""",
|
||||
"""\
|
||||
Architecture: x86_64
|
||||
CPU op-mode(s): 32-bit, 64-bit
|
||||
Byte Order: Little Endian
|
||||
CPU(s): 2
|
||||
On-line CPU(s) list: 0-1
|
||||
Thread(s) per core: 1
|
||||
Core(s) per socket: 2
|
||||
Socket(s): 1
|
||||
NUMA node(s): 1
|
||||
Vendor ID: CPU-VENDOR-NAME
|
||||
CPU family: Z
|
||||
Model: \xde\xad\xbe\xef
|
||||
Model name: CPU-MODEL-NAME
|
||||
|
||||
Architecture: x86_64
|
||||
CPU op-mode(s): 32-bit, 64-bit
|
||||
Byte Order: Little Endian
|
||||
CPU(s): 4
|
||||
On-line CPU(s) list: 0-3
|
||||
Thread(s) per core: 2
|
||||
Core(s) per socket: 2
|
||||
Socket(s): 1
|
||||
NUMA node(s): 1
|
||||
Vendor ID: CPU-VENDOR-NAME
|
||||
CPU family: Z
|
||||
Model: \xde\xad\xbe\xef
|
||||
Model name: ANOTHER-CPU-MODEL
|
||||
""",
|
||||
"""\
|
||||
Architecture: x86_64
|
||||
CPU op-mode(s): 32-bit, 64-bit
|
||||
Byte Order: Little Endian
|
||||
CPU(s): 16
|
||||
On-line CPU(s) list: 0-15
|
||||
Thread(s) per core: 2
|
||||
Core(s) per socket: 4
|
||||
Socket(s): 2
|
||||
NUMA node(s): 2
|
||||
Vendor ID: CPU-VENDOR-NAME
|
||||
CPU family: Z
|
||||
Model: \xde\xad\xbe\xef
|
||||
Model name: CPU-MODEL-NAME
|
||||
"""])
|
||||
def test_parse_lscpu_output(self, _):
|
||||
"""
|
||||
Test model name parsing from `lscpu` output.
|
||||
|
||||
See issue #29 (ARM architectures).
|
||||
`/proc/cpuinfo` will not contain `model name` info.
|
||||
`lscpu` output will be used instead.
|
||||
"""
|
||||
with self.subTest('Simple unique CPU.'):
|
||||
self.assertListEqual(
|
||||
CPU._parse_lscpu_output(), # pylint: disable=protected-access
|
||||
[{'CPU-MODEL-NAME': 4}]
|
||||
)
|
||||
|
||||
with self.subTest('Two CPUs, 1 socket.'):
|
||||
self.assertListEqual(
|
||||
CPU._parse_lscpu_output(), # pylint: disable=protected-access
|
||||
[
|
||||
{'CPU-MODEL-NAME': 2},
|
||||
{'ANOTHER-CPU-MODEL': 4}
|
||||
]
|
||||
)
|
||||
|
||||
with self.subTest('1 CPU, 2 sockets.'):
|
||||
self.assertListEqual(
|
||||
CPU._parse_lscpu_output(), # pylint: disable=protected-access
|
||||
[
|
||||
{'CPU-MODEL-NAME': 8},
|
||||
{'CPU-MODEL-NAME': 8}
|
||||
]
|
||||
)
|
||||
|
||||
@HelperMethods.patch_clean_configuration
|
||||
def test_various_output_configuration(self):
|
||||
@ -162,15 +276,12 @@ Model name: CPU-MODEL-NAME-WITHOUT-PROC-CPUINFO
|
||||
cpu_instance_mock = HelperMethods.entry_mock(CPU)
|
||||
output_mock = MagicMock()
|
||||
|
||||
cpu_instance_mock.value = {
|
||||
'CPU-MODEL-NAME': 1,
|
||||
'ANOTHER-CPU-MODEL': 2
|
||||
}
|
||||
cpu_instance_mock.value = [
|
||||
{'CPU-MODEL-NAME': 1},
|
||||
{'ANOTHER-CPU-MODEL': 2}
|
||||
]
|
||||
|
||||
with self.subTest('Single-line combined output.'):
|
||||
if sys.version_info < (3, 6):
|
||||
self.skipTest("Cannot test behavior on Python < 3.6, skipping.")
|
||||
|
||||
CPU.output(cpu_instance_mock, output_mock)
|
||||
output_mock.append.assert_called_once_with(
|
||||
'CPU',
|
||||
@ -180,9 +291,6 @@ Model name: CPU-MODEL-NAME-WITHOUT-PROC-CPUINFO
|
||||
output_mock.reset_mock()
|
||||
|
||||
with self.subTest('Single-line combined output (no count).'):
|
||||
if sys.version_info < (3, 6):
|
||||
self.skipTest("Cannot test behavior on Python < 3.6, skipping.")
|
||||
|
||||
cpu_instance_mock.options['show_count'] = False
|
||||
|
||||
CPU.output(cpu_instance_mock, output_mock)
|
||||
|
Loading…
x
Reference in New Issue
Block a user