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:
@ -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
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
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=[
|
||||
|
Reference in New Issue
Block a user