1
0
mirror of https://github.com/HorlogeSkynet/archey4 synced 2025-06-11 04:00:12 +02:00

[VARIOUS] [BREAKING] Add support for new (specific) distros (#79)

Adds support for Android, NixOS, Slackware. Restores (and fixes) support for CrunchBang (and derivatives).

Breaking changes:
 * Adds `Packages` support for multiple package-managers, adding their totals.

Changes:
 + Adds Android & NixOS logos and colors.
 + Adds Android detection to `Model` entry.
 + Adds Android, NixOS, Crunchbang detection. Fixes CrunchBang (and derivatives) detection.
 + `Distro` and `Model` entry test-cases refactored.

 - `Packages` reverted to count '\n' characters instead of `os.linesep`: see e09434a860.

 * Refactors `Model` entry to prefer static methods.
 * Fixes bug where `Model` has a virtual-machine value when some system tools are missing.
 * Distro-detection refactor, including `Distro` entry, to allow detection 

Co-authored-by: Michael Bromilow <developer@bromilow.uk>
This commit is contained in:
Samuel FORESTIER
2020-06-25 21:48:24 +00:00
committed by GitHub
parent e63feabcf3
commit ad28b3d958
12 changed files with 474 additions and 184 deletions

@ -14,6 +14,7 @@ from archey.distributions import Distributions
# The first element (`[0]`) of each list will be used to display text entries.
COLORS_DICT = {
Distributions.ARCH_LINUX: [Colors.CYAN_BRIGHT, Colors.CYAN_NORMAL],
Distributions.ANDROID: [Colors.GREEN_BRIGHT, Colors.WHITE_BRIGHT],
Distributions.ALPINE_LINUX: [Colors.BLUE_BRIGHT],
Distributions.BUNSENLABS: [Colors.WHITE_BRIGHT, Colors.YELLOW_BRIGHT, Colors.YELLOW_NORMAL],
Distributions.CENTOS: [
@ -26,6 +27,7 @@ COLORS_DICT = {
Distributions.GENTOO: [Colors.MAGENTA_BRIGHT, Colors.WHITE_BRIGHT],
Distributions.KALI_LINUX: [Colors.BLUE_BRIGHT, Colors.WHITE_BRIGHT],
Distributions.MANJARO_LINUX: [Colors.GREEN_BRIGHT],
Distributions.NIXOS: [Colors.BLUE_NORMAL, Colors.CYAN_NORMAL],
Distributions.LINUX: [Colors.WHITE_BRIGHT, Colors.YELLOW_BRIGHT],
Distributions.LINUX_MINT: [Colors.GREEN_BRIGHT, Colors.WHITE_BRIGHT],
Distributions.OPENSUSE: [Colors.GREEN_NORMAL, Colors.WHITE_BRIGHT],
@ -42,6 +44,7 @@ COLORS_DICT = {
# This dictionary contains which logo should be used for each supported distribution.
LOGOS_DICT = {
Distributions.ALPINE_LINUX: logos.ALPINE_LINUX,
Distributions.ANDROID: logos.ANDROID,
Distributions.ARCH_LINUX: logos.ARCH_LINUX,
Distributions.BUNSENLABS: logos.BUNSENLABS,
Distributions.CENTOS: logos.CENTOS,
@ -51,6 +54,7 @@ LOGOS_DICT = {
Distributions.GENTOO: logos.GENTOO,
Distributions.KALI_LINUX: logos.KALI_LINUX,
Distributions.MANJARO_LINUX: logos.MANJARO,
Distributions.NIXOS: logos.NIXOS,
Distributions.LINUX: logos.LINUX,
Distributions.LINUX_MINT: logos.LINUX_MINT,
Distributions.OPENSUSE: logos.OPENSUSE,

@ -4,6 +4,7 @@ Operating Systems detection logic.
Interface to `os-release` (through `distro` module).
"""
import os
import sys
from enum import Enum
@ -19,6 +20,7 @@ class Distributions(Enum):
See <https://distro.readthedocs.io/en/latest/#distro.id>.
"""
ALPINE_LINUX = 'alpine'
ANDROID = 'android'
ARCH_LINUX = 'arch'
BUNSENLABS = 'bunsenlabs'
CENTOS = 'centos'
@ -28,6 +30,7 @@ class Distributions(Enum):
GENTOO = 'gentoo'
KALI_LINUX = 'kali'
MANJARO_LINUX = 'manjaro'
NIXOS = 'nixos'
LINUX = 'linux'
LINUX_MINT = 'linuxmint'
OPENSUSE = 'opensuse'
@ -46,6 +49,34 @@ class Distributions(Enum):
@staticmethod
def run_detection():
"""Entry point of Archey distribution detection logic"""
distribution = Distributions._detection_logic()
# In case nothing got detected the "regular" way, fall-back on the Linux logo.
if not distribution:
# Android systems are currently not being handled by `distro`.
# We imitate Neofetch behavior to manually "detect" them.
# See <https://github.com/nir0s/distro/issues/253>.
if os.path.isdir('/system/app') and os.path.isdir('/system/priv-app'):
return Distributions.ANDROID
return Distributions.LINUX
# Below are brain-dead cases for distributions not properly handled by `distro`.
# One _may_ want to add its own logic to add support for such undetectable systems.
if distribution == Distributions.DEBIAN:
# CrunchBang is tagged as _regular_ Debian by `distro`.
# Below conditions are here to work-around this issue.
# First condition : CrunchBang-Linux and CrunchBang-Monara.
# Second condition : CrunchBang++ (CBPP).
if os.path.isfile('/etc/lsb-release-crunchbang') \
or os.path.isfile('/usr/bin/cbpp-exit'):
return Distributions.CRUNCHBANG
return distribution
@staticmethod
def _detection_logic():
"""Main distribution detection logic, relying on `distro`, handling _common_ cases"""
# Are we running on Windows ?
if sys.platform in ('win32', 'cygwin'):
return Distributions.WINDOWS
@ -67,10 +98,10 @@ class Distributions(Enum):
try:
return Distributions(id_like)
except ValueError:
continue
pass
# At the moment, fall-back to default `Linux` if nothing of the above matched.
return Distributions.LINUX
# Nothing of the above matched, let's return `None` and let the caller handle it.
return None
@staticmethod
def get_distro_name():

@ -11,13 +11,35 @@ class Distro(Entry):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
distro_name = Distributions.get_distro_name()
if not distro_name:
distro_name = self._fetch_android_release()
self.value = {
'name': Distributions.get_distro_name(),
'arch': check_output(
['uname', '-m'],
'name': distro_name,
'arch': self._fetch_architecture()
}
@staticmethod
def _fetch_architecture():
"""Simple wrapper to `uname -m` returning the current system architecture"""
return check_output(
['uname', '-m'],
universal_newlines=True
).rstrip()
@staticmethod
def _fetch_android_release():
"""Simple method to fetch current release on Android systems"""
try:
release = check_output(
['getprop', 'ro.build.version.release'],
universal_newlines=True
).rstrip()
}
except FileNotFoundError:
return None
return 'Android {0}'.format(release)
def output(self, output):

@ -13,18 +13,13 @@ class Model(Entry):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Is this a virtual machine ?
self._check_virtualization()
self.value = \
self._fetch_virtual_env_info() \
or self._fetch_product_name() \
or self._fetch_rasperry_pi_revision() \
or self._fetch_android_device_model() \
# Does the OS know something about the hardware ?
if not self.value:
self._check_product_name()
# Is this machine a Raspberry Pi ?
if not self.value:
self._check_rasperry_pi()
def _check_virtualization(self):
def _fetch_virtual_env_info(self):
"""
Relying on some system tools, tries to gather some details about hypervisor.
When available, relies on systemd.
@ -39,7 +34,7 @@ class Model(Entry):
).rstrip()
except CalledProcessError:
# Not a virtual environment.
return
return None
except FileNotFoundError:
pass
@ -57,6 +52,10 @@ class Model(Entry):
except (FileNotFoundError, CalledProcessError):
pass
# Definitely not a virtual environment.
if not environment:
return None
try:
# Sometimes we may gather info added by hosting service provider this way.
product_name = check_output(
@ -65,39 +64,61 @@ class Model(Entry):
).rstrip()
except (FileNotFoundError, CalledProcessError):
pass
elif not environment:
# No detection tool is available...
return None
# Definitely not a virtual environment.
if not environment:
return
# If we got there, this _should_ be a virtual environment.
self.value = '{0} ({1})'.format(
# If we got there with some info, this _should_ be a virtual environment.
return '{0} ({1})'.format(
product_name or self._configuration.get('default_strings')['virtual_environment'],
environment
)
def _check_product_name(self):
@staticmethod
def _fetch_product_name():
"""Tries to open a specific Linux file, looking for machine's product name"""
try:
with open('/sys/devices/virtual/dmi/id/product_name') as f_product_name:
self.value = f_product_name.read().rstrip()
return f_product_name.read().rstrip()
except FileNotFoundError:
pass
def _check_rasperry_pi(self):
return None
@staticmethod
def _fetch_rasperry_pi_revision():
"""Tries to retrieve 'Hardware' and 'Revision IDs' from `/proc/cpuinfo`"""
try:
with open('/proc/cpuinfo') as f_cpu_info:
cpu_info = f_cpu_info.read()
except (PermissionError, FileNotFoundError):
return
return None
# If the output contains 'Hardware' and 'Revision'...
hardware = re.search('(?<=Hardware\t: ).*', cpu_info)
revision = re.search('(?<=Revision\t: ).*', cpu_info)
if hardware and revision:
# ... let's set a pretty info string with these data
self.value = 'Raspberry Pi {0} (Rev. {1})'.format(
return 'Raspberry Pi {0} (Rev. {1})'.format(
hardware.group(0),
revision.group(0)
)
return None
@staticmethod
def _fetch_android_device_model():
"""Tries to retrieve `brand` and `model` device properties on Android platforms"""
try:
brand = check_output(
['getprop', 'ro.product.brand'],
universal_newlines=True
).rstrip()
model = check_output(
['getprop', 'ro.product.model'],
universal_newlines=True
).rstrip()
except FileNotFoundError:
return None
return '{0} ({1})'.format(brand, model)

@ -11,14 +11,17 @@ PACKAGES_TOOLS = (
{'cmd': ('apk', 'list', '--installed')},
# As of 2020, `apt` is _very_ slow compared to `dpkg` on Debian-based distributions.
# Additional note : `apt`'s CLI is currently not "stable" in Debian terms.
# If `apt` happens to be preferred over `dpkg` in the future, don't forget to remove the latter.
#{'cmd': ('apt', 'list', '-qq', '--installed')},
{'cmd': ('dnf', 'list', 'installed'), 'skew': 1},
{'cmd': ('dpkg', '--get-selections')},
{'cmd': ('emerge', '-ep', 'world'), 'skew': 5},
{'cmd': ('nix-env', '-q')},
{'cmd': ('pacman', '-Q')},
{'cmd': ('pkg_info', '-a')},
{'cmd': ('pkg', '-N', 'info', '-a')},
{'cmd': ('rpm', '-qa')},
{'cmd': ('ls', '-l', '/var/log/packages/')}, # SlackWare.
{'cmd': ('yum', 'list', 'installed'), 'skew': 2},
{'cmd': ('zypper', 'search', '-i'), 'skew': 5}
)
@ -45,7 +48,11 @@ class Packages(Entry):
except (FileNotFoundError, CalledProcessError):
continue
self.value = results.count(os.linesep)
# Here we *may* use `\n` as `universal_newlines` has been set to `True`.
if self.value:
self.value += results.count('\n')
else:
self.value = results.count('\n')
# If any, deduct output skew present due to the packages tool.
if 'skew' in packages_tool:
@ -55,5 +62,4 @@ class Packages(Entry):
if packages_tool['cmd'][0] == 'dpkg':
self.value -= results.count('deinstall')
# At this step, we may break the loop.
break
# Let's just loop over, in case there are multiple package managers.

@ -1,6 +1,7 @@
"""Simple `__init__` file for the `logos` module, to load each distribution logo"""
from archey.logos.alpine_linux import ALPINE_LINUX
from archey.logos.android import ANDROID
from archey.logos.arch_linux import ARCH_LINUX
from archey.logos.bunsenlabs import BUNSENLABS
from archey.logos.centos import CENTOS
@ -10,6 +11,7 @@ from archey.logos.fedora import FEDORA
from archey.logos.gentoo import GENTOO
from archey.logos.kali_linux import KALI_LINUX
from archey.logos.manjaro import MANJARO
from archey.logos.nixos import NIXOS
from archey.logos.linux import LINUX
from archey.logos.linux_mint import LINUX_MINT
from archey.logos.opensuse import OPENSUSE

22
archey/logos/android.py Normal file

@ -0,0 +1,22 @@
"""Android logo"""
ANDROID = [
"""{c[0]} -o o- """,
"""{c[0]} +hydNNNNdyh+ """,
"""{c[0]} +mMMMMMMMMMMMMm+ """,
"""{c[0]} `dMM{c[1]}m:{c[0]}NMMMMMMN{c[1]}:m{c[0]}MMd` """,
"""{c[0]} hMMMMMMMMMMMMMMMMMMh """,
"""{c[0]} .. yyyyyyyyyyyyyyyyyyyy .. """,
"""{c[0]} .mMMm`MMMMMMMMMMMMMMMMMMMM`mMMm. """,
"""{c[0]} :MMMM-MMMMMMMMMMMMMMMMMMMM-MMMM: """,
"""{c[0]} :MMMM-MMMMMMMMMMMMMMMMMMMM-MMMM: """,
"""{c[0]} :MMMM-MMMMMMMMMMMMMMMMMMMM-MMMM: """,
"""{c[0]} :MMMM-MMMMMMMMMMMMMMMMMMMM-MMMM: """,
"""{c[0]} -MMMM-MMMMMMMMMMMMMMMMMMMM-MMMM- """,
"""{c[0]} +yy+ MMMMMMMMMMMMMMMMMMMM +yy+ """,
"""{c[0]} mMMMMMMMMMMMMMMMMMMm """,
"""{c[0]} `/++MMMMh++hMMMM++/` """,
"""{c[0]} MMMMo oMMMM """,
"""{c[0]} MMMMo oMMMM """,
"""{c[0]} oNMm- -mMNs """
]

23
archey/logos/nixos.py Normal file

@ -0,0 +1,23 @@
"""NixOS logo"""
NIXOS = [
"""{c[0]} ::::. {c[1]}':::::{c[0]} {c[1]}::::'{c[0]} """,
"""{c[0]} '::::: {c[1]}':::::.{c[0]} {c[1]}::::'{c[0]} """,
"""{c[0]} ::::: {c[1]}'::::.:::::{c[0]} """,
"""{c[0]} .......:::::..... {c[1]}::::::::{c[0]} """,
"""{c[0]} ::::::::::::::::::. {c[1]}::::::{c[0]} ::::. """,
"""{c[0]} ::::::::::::::::::::: {c[1]}:::::.{c[0]} .::::' """,
"""{c[0]} {c[1]}.....{c[0]} {c[1]}::::'{c[0]} :::::' """,
"""{c[0]} {c[1]}:::::{c[0]} {c[1]}'::'{c[0]} :::::' """,
"""{c[0]} {c[1]}........:::::{c[0]} {c[1]}'{c[0]} :::::::::::. """,
"""{c[0]} {c[1]}:::::::::::::{c[0]} ::::::::::::: """,
"""{c[0]} {c[1]}:::::::::::{c[0]} .. ::::: """,
"""{c[0]} {c[1]}.:::::{c[0]} .::: ::::: """,
"""{c[0]} {c[1]}.:::::{c[0]} ::::: ''''' {c[1]}.....{c[0]} """,
"""{c[0]} {c[1]}:::::{c[0]} ':::::. {c[1]}......:::::::::::::'{c[0]} """,
"""{c[0]} {c[1]}:::{c[0]} ::::::. {c[1]}':::::::::::::::::'{c[0]} """,
"""{c[0]} .:::::::: {c[1]}'::::::::::{c[0]} """,
"""{c[0]} .::::''::::. {c[1]}'::::.{c[0]} """,
"""{c[0]} .::::' ::::. {c[1]}'::::.{c[0]} """,
"""{c[0]} .:::: :::: {c[1]}'::::.{c[0]} """
]

@ -10,18 +10,16 @@ from archey.constants import DEFAULT_CONFIG
class TestDistroEntry(unittest.TestCase):
"""`Distro` entry simple test cases"""
@patch(
'archey.entries.distro.check_output', # `uname` output
return_value="""\
ARCHITECTURE
""")
@patch(
'archey.entries.distro.Distributions.get_distro_name',
return_value="""\
NAME VERSION (CODENAME)\
""")
def test_ok(self, _, __):
"""Test for `distro` and `uname` retrievals"""
return_value='NAME VERSION (CODENAME)'
)
@patch(
'archey.entries.distro.Distro._fetch_architecture',
return_value='ARCHITECTURE'
)
def test_init(self, _, __):
"""Test `Distro` instantiation"""
self.assertDictEqual(
Distro().value,
{
@ -31,16 +29,41 @@ NAME VERSION (CODENAME)\
)
@patch(
'archey.entries.distro.check_output', # `uname` output
return_value="""\
ARCHITECTURE
""")
'archey.entries.distro.check_output',
return_value='x86_64\n' # Imitate `uname` output on AMD64.
)
def test_fetch_architecture(self, _):
"""Test `_fetch_architecture` static method"""
self.assertEqual(
Distro._fetch_architecture(), # pylint: disable=protected-access
'x86_64'
)
@patch(
'archey.entries.distro.check_output',
return_value='10\n' # Imitate `getprop` output on Android 10.
)
def test_fetch_android_release(self, _):
"""Test `_fetch_android_release` static method"""
self.assertEqual(
Distro._fetch_android_release(), # pylint: disable=protected-access
'Android 10'
)
@patch(
'archey.entries.distro.Distributions.get_distro_name',
return_value=None # Soft-failing : No _pretty_ distribution name found...
)
@patch(
'archey.entries.distro.Distro._fetch_architecture',
return_value='ARCHITECTURE'
)
@patch(
'archey.entries.distro.Distro._fetch_android_release',
return_value=None # Not an Android device either...
)
@HelperMethods.patch_clean_configuration
def test_unknown_distro_output(self, _, __):
def test_unknown_distro_output(self, _, __, ___):
"""Test for `distro` and `uname` outputs concatenation"""
distro = Distro()

@ -12,143 +12,186 @@ from archey.constants import DEFAULT_CONFIG
class TestModelEntry(unittest.TestCase):
"""
For this test we have to go through the three possibilities :
For this test we have to go through several eventualities :
* Laptop / Desktop "regular" environments
* Raspberry Pi
* Virtual environment (as a VM or a container)
* Android devices
"""
@patch(
'archey.entries.model.check_output',
side_effect=CalledProcessError(1, 'systemd-detect-virt', "none\n")
)
@patch('archey.entries.model.os.getuid')
@patch('archey.entries.model.check_output')
@HelperMethods.patch_clean_configuration
def test_fetch_virtual_env_info(self, check_output_mock, getuid_mock):
"""Test `_fetch_virtual_env_info` method"""
model_mock = HelperMethods.entry_mock(Model)
with self.subTest('Detected virtual environment.'):
check_output_mock.side_effect = [
FileNotFoundError(), # `systemd-detect-virt` is not available.
'xen\nxen-domU\n', # `virt-what` example output.
'HYPERVISOR-NAME\n' # `dmidecode` example output.
]
getuid_mock.return_value = 0
self.assertEqual(
Model._fetch_virtual_env_info(model_mock), # pylint: disable=protected-access
'HYPERVISOR-NAME (xen, xen-domU)'
)
with self.subTest('Virtual environment without `dmidecode`.'):
check_output_mock.reset_mock()
getuid_mock.reset_mock()
check_output_mock.side_effect = [
FileNotFoundError(), # `systemd-detect-virt` is not available.
'xen\nxen-domU\n', # `virt-what` example output.
FileNotFoundError() # `dmidecode` will fail.
]
getuid_mock.return_value = 0
self.assertEqual(
Model._fetch_virtual_env_info(model_mock), # pylint: disable=protected-access
DEFAULT_CONFIG['default_strings']['virtual_environment'] + ' (xen, xen-domU)'
)
with self.subTest('Virtual environment with systemd only.'):
check_output_mock.reset_mock()
getuid_mock.reset_mock()
check_output_mock.side_effect = [
'systemd-nspawn\n' # `systemd-detect-virt` output.
]
getuid_mock.return_value = 1000 # `virt-what` and `dmidecode` won't be called.
self.assertEqual(
Model._fetch_virtual_env_info(model_mock), # pylint: disable=protected-access
DEFAULT_CONFIG['default_strings']['virtual_environment'] + ' (systemd-nspawn)'
)
with self.subTest('Virtual environment with systemd and `dmidecode`.'):
check_output_mock.reset_mock()
getuid_mock.reset_mock()
check_output_mock.side_effect = [
'systemd-nspawn\n', # `systemd-detect-virt` example output.
# `virt-what` won't be called (systemd call succeeded).
'HYPERVISOR-NAME\n' # `dmidecode` example output.
]
getuid_mock.return_value = 0
self.assertEqual(
Model._fetch_virtual_env_info(model_mock), # pylint: disable=protected-access
'HYPERVISOR-NAME (systemd-nspawn)'
)
with self.subTest('Not a virtual environment (systemd).'):
check_output_mock.reset_mock()
getuid_mock.reset_mock()
check_output_mock.side_effect = CalledProcessError(1, 'systemd-detect-virt', 'none\n')
self.assertIsNone(
Model._fetch_virtual_env_info(model_mock) # pylint: disable=protected-access
)
with self.subTest('Not a virtual environment (virt-what).'):
check_output_mock.reset_mock()
getuid_mock.reset_mock()
check_output_mock.side_effect = [
FileNotFoundError(), # `systemd-detect-virt` won't be available.
'\n' # `virt-what` won't detect anything.
# `dmidecode` won't even be called.
]
getuid_mock.return_value = 0
self.assertIsNone(
Model._fetch_virtual_env_info(model_mock) # pylint: disable=protected-access
)
with self.subTest('Not a virtual environment (no tools, no root)'):
check_output_mock.reset_mock()
getuid_mock.reset_mock()
check_output_mock.side_effect = [
FileNotFoundError() # `systemd-detect-virt` won't be available.
]
getuid_mock.return_value = 1000 # `virt-what` and `dmidecode` won't be called.
self.assertIsNone(
Model._fetch_virtual_env_info(model_mock) # pylint: disable=protected-access
)
@patch(
'archey.entries.model.open',
mock_open(read_data='MY-LAPTOP-MODEL\n'),
create=True
)
@HelperMethods.patch_clean_configuration
def test_regular(self, _):
"""Sometimes, it could be quite simple..."""
self.assertEqual(Model().value, 'MY-LAPTOP-MODEL')
def test_fetch_product_name(self):
"""Test `_fetch_product_name` static method"""
self.assertEqual(
Model._fetch_product_name(), # pylint: disable=protected-access
'MY-LAPTOP-MODEL'
)
@patch(
'archey.entries.model.check_output',
side_effect=CalledProcessError(1, 'systemd-detect-virt', "none\n")
)
@HelperMethods.patch_clean_configuration
def test_raspberry(self, _):
"""Test for a typical Raspberry context"""
def test_fetch_rasperry_pi_revision(self):
"""Test `_fetch_rasperry_pi_revision` static method"""
with patch('archey.entries.model.open', mock_open(), create=True) as mock:
mock.return_value.read.side_effect = [
FileNotFoundError(), # First `open` call will (`/sys/[...]/product_name`)
'Hardware\t: HARDWARE\nRevision\t: REVISION\n'
'Hardware\t: HARDWARE\nRevision\t: REVISION\n',
'processor : 0\ncpu family : X\n'
]
self.assertEqual(
Model().value,
Model._fetch_rasperry_pi_revision(), # pylint: disable=protected-access
'Raspberry Pi HARDWARE (Rev. REVISION)'
)
@patch(
'archey.entries.model.os.getuid',
return_value=0
)
@patch(
'archey.entries.model.check_output',
side_effect=[
FileNotFoundError(), # `systemd-detect-virt` is not available
'xen\nxen-domU\n', # `virt-what` example output
'MY-LAPTOP-MODEL\n' # `dmidecode` example output
]
)
@HelperMethods.patch_clean_configuration
def test_virtual_environment(self, _, __):
"""Test for virtual machine"""
self.assertEqual(
Model().value,
'MY-LAPTOP-MODEL (xen, xen-domU)'
)
@patch(
'archey.entries.model.os.getuid',
return_value=0
)
@patch(
'archey.entries.model.check_output',
side_effect=[
FileNotFoundError(), # `systemd-detect-virt` is not available
'xen\nxen-domU\n', # `virt-what` example output
FileNotFoundError() # `dmidecode` call will fail
]
)
@HelperMethods.patch_clean_configuration
def test_virtual_environment_without_dmidecode(self, _, __):
"""Test for virtual machine (with a failing `dmidecode` call)"""
self.assertEqual(
Model().value,
DEFAULT_CONFIG['default_strings']['virtual_environment'] + ' (xen, xen-domU)'
)
@patch(
'archey.entries.model.os.getuid',
return_value=1000
)
@patch(
'archey.entries.model.check_output',
return_value='systemd-nspawn\n' # `systemd-detect-virt` example output
)
@HelperMethods.patch_clean_configuration
def test_virtual_environment_systemd_alone(self, _, __):
"""Test for virtual environments, with systemd tools and `dmidecode`"""
self.assertEqual(
Model().value,
DEFAULT_CONFIG['default_strings']['virtual_environment'] + ' (systemd-nspawn)'
)
@patch(
'archey.entries.model.os.getuid',
return_value=0
)
@patch(
'archey.entries.model.check_output',
side_effect=[
'systemd-nspawn\n', # `systemd-detect-virt` example output
'MY-LAPTOP-MODEL\n' # `dmidecode` example output
]
)
@HelperMethods.patch_clean_configuration
def test_virtual_environment_systemd_and_dmidecode(self, _, __):
"""Test for virtual environments, with systemd tools and `dmidecode`"""
self.assertEqual(Model().value, 'MY-LAPTOP-MODEL (systemd-nspawn)')
@patch(
'archey.entries.model.os.getuid',
return_value=1000
)
@patch(
'archey.entries.model.check_output',
side_effect=FileNotFoundError()
)
@HelperMethods.patch_clean_configuration
def test_no_match(self, _, __):
"""Test when no information could be retrieved"""
with patch('archey.entries.model.open', mock_open(), create=True) as mock:
mock.return_value.read.side_effect = [
FileNotFoundError(), # First `open` call will (`/sys/[...]/product_name`)
PermissionError() # `/proc/cpuinfo` won't be available
]
model = Model()
output_mock = MagicMock()
model.output(output_mock)
self.assertIsNone(model.value)
self.assertEqual(
output_mock.append.call_args[0][1],
DEFAULT_CONFIG['default_strings']['not_detected']
self.assertIsNone(
Model._fetch_rasperry_pi_revision() # pylint: disable=protected-access
)
@patch(
'archey.entries.model.check_output',
side_effect=[
'PHONE-BRAND\n', # First `getprop` call.
'PHONE-DEVICE\n', # Second `getprop` call.
FileNotFoundError() # Second test will fail.
]
)
def test_fetch_android_device_model(self, _):
"""Test `_fetch_android_device_model` static method"""
self.assertEqual(
Model._fetch_android_device_model(), # pylint: disable=protected-access
'PHONE-BRAND (PHONE-DEVICE)'
)
self.assertIsNone(
Model._fetch_android_device_model() # pylint: disable=protected-access
)
@patch(
'archey.entries.model.Model._fetch_virtual_env_info',
return_value=None # Not a virtual environment...
)
@patch(
'archey.entries.model.Model._fetch_product_name',
return_value=None # No model name could be retrieved...
)
@patch(
'archey.entries.model.Model._fetch_rasperry_pi_revision',
return_value=None # Not a Raspberry Pi device either...
)
@patch(
'archey.entries.model.Model._fetch_android_device_model',
return_value=None # Not an Android device...
)
@HelperMethods.patch_clean_configuration
def test_no_match(self, _, __, ___, ____):
"""Test when no information could be retrieved"""
model = Model()
output_mock = MagicMock()
model.output(output_mock)
self.assertIsNone(model.value)
self.assertEqual(
output_mock.append.call_args[0][1],
DEFAULT_CONFIG['default_strings']['not_detected']
)
if __name__ == '__main__':
unittest.main()

@ -1,6 +1,5 @@
"""Test module for Archey's installed system packages detection module"""
import os
import unittest
from unittest.mock import DEFAULT as DEFAULT_SENTINEL, MagicMock, patch
@ -94,19 +93,33 @@ These are the packages that would be merged, in order:
Calculating dependencies ... done!
[ebuild U ] sys-libs/glibc-2.25-r10 [2.25-r9]
[ebuild R ] sys-apps/busybox-1.28.0 {linesep}\
[ebuild R ] sys-apps/busybox-1.28.0 \n\
[ebuild N ] sys-libs/libcap-2.24-r2 \
USE="pam -static-libs" ABI_X86="(64) -32 (-x32)" {linesep}\
USE="pam -static-libs" ABI_X86="(64) -32 (-x32)" \n\
[ebuild U ] app-misc/pax-utils-1.2.2-r2 [1.1.7]
[ebuild R ] x11-misc/shared-mime-info-1.9 {linesep}\
[ebuild R ] x11-misc/shared-mime-info-1.9 \n\
""".format(linesep=os.linesep))
""")
def test_match_with_emerge(self, check_output_mock):
"""Simple test for the Emerge packages manager"""
check_output_mock.side_effect = self._check_output_side_effect('emerge')
self.assertEqual(Packages().value, 5)
@patch(
'archey.entries.packages.check_output',
return_value="""\
nix-2.3.4
nss-cacert-3.49.2
python3-3.8.2
python3.8-pip-20.1
""")
def test_match_with_nix_env(self, check_output_mock):
"""Simple test for the Emerge packages manager"""
check_output_mock.side_effect = self._check_output_side_effect('nix-env')
self.assertEqual(Packages().value, 4)
@patch(
'archey.entries.packages.check_output',
return_value="""\
@ -180,10 +193,10 @@ MySQL-client-3.23.57-1
Loaded plugins: fastestmirror, langpacks
Installed Packages
GConf2.x86_64 3.2.6-8.el7 @base/$releasever
GeoIP.x86_64 1.5.0-11.el7 @base {linesep}\
ModemManager.x86_64 1.6.0-2.el7 @base {linesep}\
ModemManager-glib.x86_64 1.6.0-2.el7 @base {linesep}\
""".format(linesep=os.linesep))
GeoIP.x86_64 1.5.0-11.el7 @base \n\
ModemManager.x86_64 1.6.0-2.el7 @base \n\
ModemManager-glib.x86_64 1.6.0-2.el7 @base \n\
""")
def test_match_with_yum(self, check_output_mock):
"""Simple test for the Yum packages manager"""
check_output_mock.side_effect = self._check_output_side_effect('yum')
@ -196,20 +209,46 @@ ModemManager-glib.x86_64 1.6.0-2.el7 @base {linesep}\
Loading repository data...
Reading installed packages...
S | Name | Summary | Type {linesep}\
S | Name | Summary | Type \n\
---+---------------+-------------------------------------+------------
i+ | 5201 | Recommended update for xdg-utils | patch {linesep}\
i | GeoIP-data | Free GeoLite country-data for GeoIP | package {linesep}\
i | make | GNU make | package {linesep}\
i+ | 5201 | Recommended update for xdg-utils | patch \n\
i | GeoIP-data | Free GeoLite country-data for GeoIP | package \n\
i | make | GNU make | package \n\
i | GNOME Nibbles | Guide a worm around a maze | application
i | at | A Job Manager | package {linesep}\
""".format(linesep=os.linesep))
i | at | A Job Manager | package \n\
""")
def test_match_with_zypper(self, check_output_mock):
"""Simple test for the Zypper packages manager"""
check_output_mock.side_effect = self._check_output_side_effect('zypper')
self.assertEqual(Packages().value, 5)
@patch(
'archey.entries.packages.PACKAGES_TOOLS',
new=(
{'cmd': ('pkg_tool_1')},
{'cmd': ('pkg_tool_2'), 'skew': 2}
)
)
@patch(
'archey.entries.packages.check_output',
side_effect=[
"""\
sample_package_1_1
sample_package_1_2
""",
"""\
Incredible list of installed packages:
sample_package_2_1
sample_package_2_2
"""
]
)
def test_multiple_package_managers(self, _):
"""Simple test for multiple packages managers"""
self.assertEqual(Packages().value, 4)
@patch('archey.entries.packages.check_output')
@HelperMethods.patch_clean_configuration
def test_no_packages_manager(self, check_output_mock):

@ -58,7 +58,11 @@ class TestDistributionsUtil(unittest.TestCase):
'archey.distributions.distro.id',
return_value='debian'
)
def test_run_detection_known_distro_id(self, _, __):
@patch(
'archey.distributions.os.path.isfile', # Emulate a "regular" Debian file-system.
return_value=False # Any additional check will fail.
)
def test_run_detection_known_distro_id(self, _, __, ___):
"""Test known distribution output"""
self.assertEqual(
Distributions.run_detection(),
@ -81,7 +85,11 @@ class TestDistributionsUtil(unittest.TestCase):
'archey.distributions.distro.like',
return_value='' # No `ID_LIKE` specified.
)
def test_run_detection_unknown_distro_id(self, _, __, ___):
@patch(
'archey.distributions.os.path.isdir', # Make Android detection fails.
return_value=False
)
def test_run_detection_unknown_distro_id(self, _, __, ___, ____):
"""Test unknown distribution output"""
self.assertEqual(
Distributions.run_detection(),
@ -150,13 +158,59 @@ class TestDistributionsUtil(unittest.TestCase):
'archey.distributions.distro.like',
return_value='' # No `ID_LIKE` either...
)
def test_run_detection_both_distro_calls_fail(self, _, __, ___):
@patch(
'archey.distributions.os.path.isdir', # Make Android detection fails.
return_value=False
)
def test_run_detection_both_distro_calls_fail(self, _, __, ___, ____):
"""Test distribution fall-back when `distro` soft-fail two times"""
self.assertEqual(
Distributions.run_detection(),
Distributions.LINUX
)
@patch(
'archey.distributions.sys.platform',
'linux'
)
@patch(
'archey.distributions.check_output',
return_value=b'X.Y.Z-R-ARCH\n'
)
@patch(
'archey.distributions.distro.id',
return_value='debian'
)
@patch(
'archey.distributions.os.path.isfile', # Emulate a CrunchBang file-system.
side_effect=(
lambda file_path: file_path == '/etc/lsb-release-crunchbang'
)
)
def test_run_detection_specific_crunchbang(self, _, __, ___):
"""Test CrunchBang specific detection"""
self.assertEqual(
Distributions.run_detection(),
Distributions.CRUNCHBANG
)
@patch(
'archey.distributions.Distributions._detection_logic',
return_value=None # Base detection logic soft-fails...
)
@patch(
'archey.distributions.os.path.isdir', # Emulate an Android file-system.
side_effect=(
lambda dir_path: dir_path.startswith('/system/') and dir_path.endswith('app')
)
)
def test_run_detection_specific_android(self, _, __):
"""Test Android specific detection"""
self.assertEqual(
Distributions.run_detection(),
Distributions.ANDROID
)
@patch(
'archey.distributions.distro.name',
side_effect=[