1
0
mirror of https://github.com/HorlogeSkynet/archey4 synced 2025-05-19 16:00:27 +02:00

Compare commits

...

2 Commits

Author SHA1 Message Date
Michael Bromilow
f545d948c0
[TESTS] Clean up tests (#76)
Vastly simplifies entry tests that deal with `Configuration` by adding some helper-methods.

The main reasoning behind this is that patching `Configuration.get` in tests is delicate, and refactoring any code very easily breaks this method. The new helper methods allow us to start from a known clean state in our entry tests.

+ Adds helper methods `entry_mock` and `patch_clean_configuration` for tests.
+ Makes `Configuration` into an iterable.
+ `Configuration`'s `update_recursive` method is now public.

* Moves entry tests to a new sub-folder in `archey/test`
* Moves default configuration to `archey.constants`
* Rewrites tests' patches to use the new helper methods.

Co-authored-by: Samuel FORESTIER <dev@samuel.domains>
2020-06-05 00:13:04 +00:00
Samuel FORESTIER
73c1f68e39
[FEATURE] Implements -s option to make Archey take a screenshot (#75)
Co-authored-by: Michael Bromilow <developer@bromilow.uk>
2020-06-04 16:23:43 +00:00
27 changed files with 723 additions and 470 deletions

@ -140,13 +140,13 @@ sudo mv dist/archey /usr/local/bin/
## Usage
```bash
archey
archey --help
```
or if you only want to try this out (for instance, from source) :
```bash
python3 -m archey
python3 -m archey --help
```
## Configuration (optional)

@ -31,7 +31,7 @@ Remain \fImaintained\fR, \fIcommunity-driven\fR and
.IP "-h, --help"
show help message and exit
.IP "-c, --config-path"
.IP "-c, --config-path PATH"
path to a configuration file, or a directory containing a `config.json`
.IP "-d, --distribution IDENTIFIER"
@ -41,6 +41,10 @@ supported distribution identifier to show the logo of, pass `unknown` to list th
output entries data to JSON format, use multiple times to increase
indentation
.IP "-s, --screenshot [FILENAME]"
take a screenshot once execution is done, optionally specify a target
path
.IP "-v, --version"
show program's version number and exit

@ -18,6 +18,7 @@ from archey.output import Output
from archey.configuration import Configuration
from archey.distributions import Distributions
from archey.processes import Processes
from archey.screenshot import take_screenshot
from archey.entries.user import User as e_User
from archey.entries.hostname import Hostname as e_Hostname
from archey.entries.model import Model as e_Model
@ -71,6 +72,7 @@ def args_parsing():
parser = argparse.ArgumentParser(prog='archey')
parser.add_argument(
'-c', '--config-path',
metavar='PATH',
help='path to a configuration file, or a directory containing a `config.json`'
)
parser.add_argument(
@ -84,6 +86,13 @@ def args_parsing():
action='count',
help='output entries data to JSON format, use multiple times to increase indentation'
)
parser.add_argument(
'-s', '--screenshot',
metavar='PATH',
nargs='?',
const=False,
help='take a screenshot once execution is done, optionally specify a target path'
)
parser.add_argument(
'-v', '--version',
action='version',
@ -137,6 +146,11 @@ def main():
output.output()
# Has the screenshot flag been specified ?
if args.screenshot is not None:
# If so, but still _falsy_, pass `None` as no output file has been specified by the user.
take_screenshot((args.screenshot or None))
if __name__ == '__main__':
main()

@ -3,67 +3,22 @@
import os
import sys
import json
from copy import deepcopy
from archey.constants import DEFAULT_CONFIG
from archey.singleton import Singleton
class Configuration(metaclass=Singleton):
"""
The default needed configuration which will be used by Archey is present below.
Values present in the `self._config` dictionary below are needed.
New optional values may be added with `_update_recursive` method.
Values present in `archey.constants.DEFAULT_CONFIG` dictionary are required.
New optional values may be added with `update_recursive` method.
If a `config_path` is passed during instantiation, it will be loaded.
"""
def __init__(self, config_path=None):
self._config = {
'allow_overriding': True,
'parallel_loading': True,
'suppress_warnings': False,
'colors_palette': {
'use_unicode': True,
'honor_ansi_color': True
},
'disk': {
'show_filesystems': ['local'],
'combine_total': True,
'disk_labels': None,
'hide_entry_name': None
},
'default_strings': {
'no_address': 'No Address',
'not_detected': 'Not detected',
'virtual_environment': 'Virtual Environment'
},
'gpu': {
'one_line': True,
'max_count': 2
},
'ip_settings': {
'lan_ip_max_count': 2,
'lan_ip_v6_support': True,
'wan_ip_v6_support': True
},
'limits': {
'ram': {
'warning': 33.3,
'danger': 66.7
},
'disk': {
'warning': 50,
'danger': 75
}
},
'temperature': {
'char_before_unit': ' ',
'sensors_chipsets': [],
'use_fahrenheit': False
},
'timeout': {
'ipv4_detection': 1,
'ipv6_detection': 1
}
}
# Deep-copy `DEFAULT_CONFIG` so we have a local copy to safely mutate.
self._config = deepcopy(DEFAULT_CONFIG)
# Let's "save" `STDERR` file descriptor for `suppress_warnings` option
self._stderr = sys.stderr
@ -99,7 +54,7 @@ class Configuration(metaclass=Singleton):
try:
with open(path) as f_config:
self._update_recursive(self._config, json.load(f_config))
self.update_recursive(self._config, json.load(f_config))
except FileNotFoundError:
return
# For backward compatibility with Python versions prior to 3.5.0
@ -116,7 +71,8 @@ class Configuration(metaclass=Singleton):
else:
self._close_and_restore_sys_stderr()
def _update_recursive(self, old_dict, new_dict):
@classmethod
def update_recursive(cls, old_dict, new_dict):
"""
A method for recursively merging dictionaries as `dict.update()` is not able to do this.
Original snippet taken from here : <https://gist.github.com/angstwad/bf22d1822c38a92ec0a9>
@ -125,7 +81,7 @@ class Configuration(metaclass=Singleton):
if key in old_dict \
and isinstance(old_dict[key], dict) \
and isinstance(value, dict):
self._update_recursive(old_dict[key], value)
cls.update_recursive(old_dict[key], value)
else:
old_dict[key] = value
@ -137,3 +93,7 @@ class Configuration(metaclass=Singleton):
def __del__(self):
self._close_and_restore_sys_stderr()
def __iter__(self):
"""When used as an iterator, directly yield `_config` elements"""
return iter(self._config.items())

@ -1,4 +1,8 @@
"""Logos colors and distributions / logos matching"""
"""
Logos colors definition.
Distributions / logos correspondences.
Default configuration.
"""
import archey.logos as logos
@ -56,3 +60,54 @@ LOGOS_DICT = {
Distributions.UBUNTU: logos.UBUNTU,
Distributions.WINDOWS: logos.WINDOWS
}
# The default needed configuration which will be used by Archey is present below.
DEFAULT_CONFIG = {
'allow_overriding': True,
'parallel_loading': True,
'suppress_warnings': False,
'colors_palette': {
'use_unicode': True,
'honor_ansi_color': True
},
'disk': {
'show_filesystems': ['local'],
'combine_total': True,
'disk_labels': None,
'hide_entry_name': None
},
'default_strings': {
'no_address': 'No Address',
'not_detected': 'Not detected',
'virtual_environment': 'Virtual Environment'
},
'gpu': {
'one_line': True,
'max_count': 2
},
'ip_settings': {
'lan_ip_max_count': 2,
'lan_ip_v6_support': True,
'wan_ip_v6_support': True
},
'limits': {
'ram': {
'warning': 33.3,
'danger': 66.7
},
'disk': {
'warning': 50,
'danger': 75
}
},
'temperature': {
'char_before_unit': ' ',
'sensors_chipsets': [],
'use_fahrenheit': False
},
'timeout': {
'ipv4_detection': 1,
'ipv6_detection': 1
}
}

92
archey/screenshot.py Normal file

@ -0,0 +1,92 @@
"""Simple module doing its best as taking a screenshot of the current screen"""
import os
import sys
import time
from contextlib import ExitStack
from datetime import datetime
from functools import partial
from subprocess import CalledProcessError, DEVNULL, check_call
def take_screenshot(output_file=None):
"""
Simple function trying to take a screenshot using various famous back-end programs.
When supported by the found and available back-end, try to honor `output_file`.
"""
if not output_file or os.path.isdir(output_file):
# When a directory is provided, we've to force `output_file` to represent a **file** path.
output_file = os.path.join(
(output_file or os.getcwd()),
datetime.now().strftime('archey4_screenshot_%Y-%m-%d_%H.%M.%S.png')
)
# Some programs don't accept specific filename as parameters.
# In such cases, we may provide them a target directory instead.
output_dir = os.path.dirname(output_file)
# Back-end programs that _may_ (?) be available across different platforms.
screenshot_tools = {
'Flameshot': ['flameshot', 'full', '-p', output_dir],
'ImageMagick': ['import', '-window', 'root', output_file],
'scrot': ['scrot', '-z', output_file],
'Shutter': ['shutter', '-f', '-o', output_file, '-e'],
}
# Extends the original screenshot tools dictionary according to current platform.
if sys.platform in ('win32', 'cygwin'):
screenshot_tools['SnippingTool'] = ['SnippingTool.exe', '/clip']
elif sys.platform == 'darwin':
screenshot_tools['ScreenCapture'] = [
'screencapture',
'-x',
'-t', output_file.rpart('.')[2],
output_file
]
else: # *NIX systems (and others)...
screenshot_tools['Escrotum'] = ['escrotum', output_file]
screenshot_tools['GNOME-Screenshot'] = ['gnome-screenshot', '-f', output_file]
screenshot_tools['grim'] = ['grim', output_file]
screenshot_tools['KDE-Spectacle'] = ['spectacle', '-b', '-o', output_file]
screenshot_tools['Xfce4-Screenshooter'] = ['xfce4-screenshooter', '-f', '-s', output_dir]
screenshot_tools['Screencap (Android)'] = [
'screencap', # Binary available on Android.
'-p', # It only accepts PNG as image output format.
(output_file.rpart('.')[0] + '.png')
]
# This part purposefully blocks so we wait a little bit before taking the screenshot.
# It prevents taking a screenshot before Archey's output has appeared.
for time_remaining in range(3, 0, -1):
taking_sc_str = 'Taking screenshot in {:1d}...'.format(time_remaining)
print(taking_sc_str, end='', flush=True)
time.sleep(1)
print('\r' + ' ' * len(taking_sc_str), end='\r', flush=True)
time.sleep(0.5)
with ExitStack() as defer_stack:
for screenshot_tool, screenshot_cmd in screenshot_tools.items():
try:
check_call(screenshot_cmd, stderr=DEVNULL)
except FileNotFoundError:
continue
except CalledProcessError as process_error:
defer_stack.callback(partial(
print,
'Couldn\'t take a screenshot with {}: \"{}\".'.format(
screenshot_tool, process_error
),
file=sys.stderr
))
continue
break
else:
defer_stack.callback(partial(
print,
"""\
Sorry, we couldn\'t find any supported program to take a screenshot on your system.
Please install one of the following and try again: {}.\
""".format(', '.join(screenshot_tools.keys())),
file=sys.stderr
))

@ -0,0 +1,202 @@
"""`archey.test.entries` module initialization file"""
from copy import deepcopy
from functools import wraps
import unittest
from unittest.mock import MagicMock, patch
from archey.configuration import Configuration
from archey.constants import DEFAULT_CONFIG
from archey.entry import Entry
class HelperMethods:
"""
This class contains helper methods we commonly use in our entry unit tests.
We kindly borrow `update_recursive` class method from `Configuration` to DRY its implementation.
"""
@staticmethod
def entry_mock(entry, configuration=None):
"""
Creates a placeholder "instance" of the entry class passed, with a clean default
`_configuration` which is optionally updated by `configuration`.
It can be used to very cleanly unit-test instance methods of a class,
by passing it in (after setting appropriate attributes).
The attributes defined are not instance attributes, however since this isn't
technically an instance, they are used in place of the respective instance attributes.
"""
# We spec to the entry so non-existent methods can't be called...
# ...and wrap it, to inherit its methods.
instance_mock = MagicMock(spec=entry, wraps=entry)
# These instance-attributes are quite important, so let's mimic them.
instance_mock.name = str(entry.__name__)
instance_mock.value = None # (entry default)
# Let's initially give the entry configuration the defaults.
# We deep-copy `DEFAULT_CONFIG` to prevent its mutation.
entry_configuration = deepcopy(DEFAULT_CONFIG)
# Then, let's merge in `configuration` recursively.
Configuration.update_recursive(entry_configuration, (configuration or {}))
# Finally, replaces the internal (and private!) `_configuration` attribute by...
# ... the corresponding configuration object.
setattr(instance_mock, '_configuration', entry_configuration)
return instance_mock
@staticmethod
def patch_clean_configuration(method_definition=None, *, configuration=None):
"""
Decorator for an entry test definition, which sets the entry's `_configuration` attribute to
the Archey defaults, optionally updated with `configuration`.
"""
# Let's initially give the entry configuration the defaults.
# We deep-copy `DEFAULT_CONFIG` to prevent its mutation.
entry_configuration = deepcopy(DEFAULT_CONFIG)
# Then, let's merge in `configuration` recursively.
Configuration.update_recursive(entry_configuration, (configuration or {}))
def decorator_patch_clean_configuration(method):
@wraps(method)
def wrapper_patch_clean_configuration(*args, **kwargs):
with patch('archey.entry.Configuration', autospec=True) as config_instance_mock:
# Mock "publicly" used methods.
config_instance_mock().get = entry_configuration.get
config_instance_mock().__iter__ = lambda _: iter(entry_configuration.items())
return method(*args, **kwargs)
return wrapper_patch_clean_configuration
if method_definition is None:
return decorator_patch_clean_configuration
return decorator_patch_clean_configuration(method_definition)
class TestHelperMethods(unittest.TestCase, HelperMethods):
"""This class implements test cases for our helper methods."""
class _SimpleEntry(Entry):
"""A simple (sub-)class inheriting from `Entry` to use in testing."""
def __init__(self):
super().__init__()
self.name = 'meaning_of_life'
self.value = self.meaning_of_life()
def my_name(self):
"""Returns the entry name."""
return self.name
@staticmethod
def meaning_of_life():
"""Widely debated..."""
return 42
def test_entry_mock_defaults(self):
"""Test `entry_mock`s default attributes."""
simple_mock_instance = self.entry_mock(TestHelperMethods._SimpleEntry)
self.assertEqual(simple_mock_instance.name, '_SimpleEntry')
self.assertIsNone(simple_mock_instance.value)
self.assertDictEqual(simple_mock_instance._configuration, DEFAULT_CONFIG) # pylint: disable=protected-access
def test_entry_mock_spec(self):
"""Test `entry_mock`s speccing."""
simple_mock_instance = self.entry_mock(TestHelperMethods._SimpleEntry)
with self.assertRaises(AttributeError):
# We shouldn't be able to call methods that don't exist...
simple_mock_instance.not_a_method()
# ...or get attributes that don't exist.
_ = simple_mock_instance.not_an_attribute
# However we _should_ be able to set an attribute then use it.
simple_mock_instance.is_simple = True
self.assertTrue(simple_mock_instance.is_simple)
def test_entry_mock_wrap(self):
"""Test `entry_mock`s class wrapping."""
simple_mock_instance = self.entry_mock(TestHelperMethods._SimpleEntry)
self.assertEqual(simple_mock_instance.meaning_of_life(), 42)
simple_mock_instance.name = 'A simple entry!'
self.assertEqual(simple_mock_instance.my_name(simple_mock_instance), 'A simple entry!')
@patch.dict(
DEFAULT_CONFIG,
values={
'a_key': 'a_value',
'a_dict': {
'key_1': 1,
'key_2': 2
}
},
clear=True
)
def test_entry_mock_configuration_setting(self):
"""Test `entry_mock`s configuration setting."""
configuration_dict = {
'another_key': 'another_value', # Adding a key-value pair.
'a_dict': {
'key_1': 10 # Updating a nested key-value pair.
}
}
simple_mock_instance = self.entry_mock(TestHelperMethods._SimpleEntry, configuration_dict)
self.assertDictEqual(
simple_mock_instance._configuration, # pylint: disable=protected-access
{
'a_key': 'a_value',
'another_key': 'another_value',
'a_dict': {
'key_1': 10,
'key_2': 2
}
}
)
def test_patch_clean_configuration_defaults(self):
"""Test the default configuration-setting of `patch_clean_configuration."""
@HelperMethods.patch_clean_configuration
def test(self):
simple_entry = TestHelperMethods._SimpleEntry()
self.assertDictEqual(
dict(simple_entry._configuration), # pylint: disable=protected-access
DEFAULT_CONFIG
)
test(self)
@patch.dict(
DEFAULT_CONFIG,
values={
'a_key': 'a_value',
'a_dict': {
'key_1': 1,
'key_2': 2
}
},
clear=True
)
def test_patch_clean_configuration_setting(self):
"""Test `patch_clean_configuration`s configuration setting."""
configuration_dict = {
'another_key': 'another_value', # Adding a key-value pair.
'a_dict': {
'key_1': 10 # Updating a nested key-value pair.
}
}
@HelperMethods.patch_clean_configuration(
configuration=configuration_dict
)
def test(self):
simple_entry = TestHelperMethods._SimpleEntry()
self.assertDictEqual(
dict(simple_entry._configuration), # pylint: disable=protected-access
{
'a_key': 'a_value',
'another_key': 'another_value',
'a_dict': {
'key_1': 10,
'key_2': 2
}
}
)
test(self)

@ -6,6 +6,7 @@ from unittest.mock import call, patch, MagicMock
from archey.colors import Colors
from archey.entries.disk import Disk
from archey.test.entries import HelperMethods
class TestDiskEntry(unittest.TestCase):
@ -13,29 +14,9 @@ class TestDiskEntry(unittest.TestCase):
Here, we mock `check_output` calls to disk utility tools.
"""
def setUp(self):
"""Some useful setup tasks to do before each test, to help us DRY."""
self.disk_instance_mock = MagicMock(spec=Disk, wraps=Disk)
"""We use these mocks so often, it's worth defining them here."""
self.disk_instance_mock = HelperMethods.entry_mock(Disk)
self.output_mock = MagicMock()
# Let's set `Disk` instance mock's attributes to sensible defaults.
self.disk_instance_mock.name = 'Disk'
self.disk_instance_mock.value = None
# We can always change this configuration in tests if need be.
self.disk_instance_mock._configuration = { # pylint: disable=protected-access
'disk': {
'show_filesystems': ['local'],
'combine_total': True,
'disk_labels': None,
'hide_entry_name': None
},
'limits': {
'disk': {
'warning': 50,
'danger': 75
}
},
# Required by all entries:
'default_strings': {'not_detected': 'Not detected'}
}
def test_disk_get_local_filesystems(self):
"""Tests `Disk._get_local_filesystems`."""

@ -4,6 +4,8 @@ import unittest
from unittest.mock import MagicMock, patch
from archey.entries.distro import Distro
from archey.test.entries import HelperMethods
from archey.constants import DEFAULT_CONFIG
class TestDistroEntry(unittest.TestCase):
@ -37,11 +39,8 @@ ARCHITECTURE
'archey.entries.distro.Distributions.get_distro_name',
return_value=None # Soft-failing : No _pretty_ distribution name found...
)
@patch(
'archey.configuration.Configuration.get',
return_value={'not_detected': 'Not detected'}
)
def test_unknown_distro_output(self, _, __, ___):
@HelperMethods.patch_clean_configuration
def test_unknown_distro_output(self, _, __):
"""Test for `distro` and `uname` outputs concatenation"""
distro = Distro()
@ -57,7 +56,7 @@ ARCHITECTURE
)
self.assertEqual(
output_mock.append.call_args[0][1],
'Not detected [ARCHITECTURE]'
DEFAULT_CONFIG['default_strings']['not_detected'] + ' [ARCHITECTURE]'
)

@ -5,18 +5,14 @@ from unittest.mock import MagicMock, patch
from subprocess import CalledProcessError
from archey.test import CustomAssertions
from archey.entries.gpu import GPU
from archey.test import CustomAssertions
from archey.test.entries import HelperMethods
from archey.constants import DEFAULT_CONFIG
class TestGPUEntry(unittest.TestCase, CustomAssertions):
"""Here, we mock the `check_output` call to `lspci` to test the logic"""
@patch(
'archey.configuration.Configuration.get',
side_effect=[
{'max_count': None}
]
)
@patch(
'archey.entries.gpu.check_output',
return_value="""\
@ -25,17 +21,11 @@ XX:YY.H SMBus: BBBBBBBBBBBBBBBB
XX:YY.H VGA compatible controller: GPU-MODEL-NAME
XX:YY.H Audio device: DDDDDDDDDDDDDDDD
""")
def test_match_vga(self, _, __):
@HelperMethods.patch_clean_configuration
def test_match_vga(self, _):
"""Simple 'VGA' device type matching"""
self.assertListEqual(GPU().value, ['GPU-MODEL-NAME'])
@patch(
'archey.configuration.Configuration.get',
side_effect=[
{'max_count': 2},
{'one_line': True}
]
)
@patch(
'archey.entries.gpu.check_output',
return_value="""\
@ -46,7 +36,15 @@ XX:YY.H Display controller: ANOTHER-MATCHING-VIDEO-CONTROLLER
XX:YY.H Audio device: DDDDDDDDDDDDDDDD
XX:YY.H 3D controller: 3D GPU-MODEL-NAME TAKES ADVANTAGE
""")
def test_multi_matches_capped_one_line(self, _, __):
@HelperMethods.patch_clean_configuration(
configuration={
'gpu': {
'one_line': True,
'max_count': 2
}
}
)
def test_multi_matches_capped_one_line(self, _):
"""
Test detection when there are multiple graphical device candidates.
Check that `max_count` and `one_line` are honored too, including on `output` overriding.
@ -65,13 +63,6 @@ XX:YY.H 3D controller: 3D GPU-MODEL-NAME TAKES ADVANTAGE
'3D GPU-MODEL-NAME TAKES ADVANTAGE, GPU-MODEL-NAME'
)
@patch(
'archey.configuration.Configuration.get',
side_effect=[
{'max_count': False},
{'one_line': False}
]
)
@patch(
'archey.entries.gpu.check_output',
return_value="""\
@ -81,7 +72,15 @@ XX:YY.H Display controller: ANOTHER-MATCHING-VIDEO-CONTROLLER
XX:YY.H Audio device: DDDDDDDDDDDDDDDD
XX:YY.H 3D controller: 3D GPU-MODEL-NAME TAKES ADVANTAGE
""")
def test_multi_matches_uncapped_multiple_lines(self, _, __):
@HelperMethods.patch_clean_configuration(
configuration={
'gpu': {
'one_line': False,
'max_count': False
}
}
)
def test_multi_matches_uncapped_multiple_lines(self, _):
"""
Test detection when there are multiple graphical device candidates.
Check that `max_count` and `one_line` are honored too, including on `output` overriding.
@ -104,14 +103,6 @@ XX:YY.H 3D controller: 3D GPU-MODEL-NAME TAKES ADVANTAGE
'ANOTHER-MATCHING-VIDEO-CONTROLLER'
)
@patch(
'archey.configuration.Configuration.get',
side_effect=[
{'max_count': None},
{'one_line': True},
{'not_detected': 'Not detected'}
]
)
@patch(
'archey.entries.gpu.check_output',
return_value="""\
@ -119,7 +110,8 @@ XX:YY.H IDE interface: IIIIIIIIIIIIIIII
XX:YY.H SMBus: BBBBBBBBBBBBBBBB
XX:YY.H Audio device: DDDDDDDDDDDDDDDD
""")
def test_no_match(self, _, __):
@HelperMethods.patch_clean_configuration
def test_no_match(self, _):
"""Test (non-)detection when there is not any graphical candidate"""
gpu = GPU()
@ -129,22 +121,15 @@ XX:YY.H Audio device: DDDDDDDDDDDDDDDD
self.assertListEmpty(gpu.value)
self.assertEqual(
output_mock.append.call_args[0][1],
'Not detected'
DEFAULT_CONFIG['default_strings']['not_detected']
)
@patch(
'archey.configuration.Configuration.get',
side_effect=[
{'max_count': None},
{'one_line': False},
{'not_detected': 'Not detected'}
]
)
@patch(
'archey.entries.gpu.check_output',
side_effect=CalledProcessError(1, 'lspci')
)
def test_lspci_crash(self, _, __):
@HelperMethods.patch_clean_configuration
def test_lspci_crash(self, _):
"""Test (non-)detection due to a crashing `lspci` program"""
gpu = GPU()
@ -154,7 +139,7 @@ XX:YY.H Audio device: DDDDDDDDDDDDDDDD
self.assertListEmpty(gpu.value)
self.assertEqual(
output_mock.append.call_args[0][1],
'Not detected'
DEFAULT_CONFIG['default_strings']['not_detected']
)

@ -5,8 +5,10 @@ from unittest.mock import MagicMock, patch
from netifaces import AF_INET, AF_INET6, AF_LINK
from archey.test import CustomAssertions
from archey.entries.lan_ip import LanIp
from archey.test import CustomAssertions
from archey.test.entries import HelperMethods
from archey.constants import DEFAULT_CONFIG
class TestLanIpEntry(unittest.TestCase, CustomAssertions):
@ -46,14 +48,10 @@ class TestLanIpEntry(unittest.TestCase, CustomAssertions):
}
]
)
@patch(
'archey.configuration.Configuration.get',
side_effect=[
{'lan_ip_v6_support': None}, # Needed key.
{'lan_ip_max_count': False}
]
@HelperMethods.patch_clean_configuration(
configuration={'ip_settings': {'lan_ip_max_count': False}}
)
def test_multiple_interfaces(self, _, __, ___):
def test_multiple_interfaces(self, _, __):
"""Test for multiple interfaces, multiple addresses (including a loopback one)"""
self.assertListEqual(
LanIp().value,
@ -109,14 +107,15 @@ class TestLanIpEntry(unittest.TestCase, CustomAssertions):
}
]
)
@patch(
'archey.configuration.Configuration.get',
side_effect=[
{'lan_ip_v6_support': True},
{'lan_ip_max_count': 2}
]
@HelperMethods.patch_clean_configuration(
configuration={
'ip_settings': {
'lan_ip_max_count': 2,
'lan_ip_v6_support': True
}
}
)
def test_ipv6_and_limit_and_ether(self, _, __, ___):
def test_ipv6_and_limit_and_ether(self, _, __):
"""
Test for IPv6 support, final set length limit and Ethernet interface filtering.
Additionally check the `output` method behavior.
@ -176,14 +175,10 @@ class TestLanIpEntry(unittest.TestCase, CustomAssertions):
}
]
)
@patch(
'archey.configuration.Configuration.get',
side_effect=[
{'lan_ip_v6_support': False},
{'lan_ip_max_count': None}
]
@HelperMethods.patch_clean_configuration(
configuration={'ip_settings': {'lan_ip_v6_support': False}}
)
def test_no_ipv6(self, _, __, ___):
def test_no_ipv6(self, _, __):
"""Test for IPv6 hiding"""
self.assertListEqual(
LanIp().value,
@ -194,14 +189,8 @@ class TestLanIpEntry(unittest.TestCase, CustomAssertions):
'archey.entries.lan_ip.netifaces.interfaces',
return_value=[] # No interface returned by `netifaces`.
)
@patch(
'archey.configuration.Configuration.get',
side_effect=[
{'lan_ip_v6_support': None}, # Needed key.
{'lan_ip_max_count': None} # Needed key.
]
)
def test_no_network_interface(self, _, __):
@HelperMethods.patch_clean_configuration
def test_no_network_interface(self, _):
"""Test when the device does not have any network interface"""
self.assertListEmpty(LanIp().value)
@ -232,15 +221,8 @@ class TestLanIpEntry(unittest.TestCase, CustomAssertions):
}
]
)
@patch(
'archey.configuration.Configuration.get',
side_effect=[
{'lan_ip_v6_support': None}, # Needed key.
{'lan_ip_max_count': None}, # Needed key.
{'no_address': 'No address'}
]
)
def test_no_network_address_output(self, _, __, ___):
@HelperMethods.patch_clean_configuration
def test_no_network_address_output(self, _, __):
"""
Test when the network interface(s) do not have any IP address.
Additionally check the `output` method behavior.
@ -253,7 +235,7 @@ class TestLanIpEntry(unittest.TestCase, CustomAssertions):
self.assertListEmpty(lan_ip.value)
self.assertEqual(
output_mock.append.call_args[0][1],
'No address'
DEFAULT_CONFIG['default_strings']['no_address']
)
@patch(
@ -265,13 +247,8 @@ class TestLanIpEntry(unittest.TestCase, CustomAssertions):
return_value=None, # Let's nastily mute class' outputs.
create=True
)
@patch(
'archey.configuration.Configuration.get',
side_effect=[
{'not_detected': 'Not detected'}
]
)
def test_netifaces_not_available(self, _, __):
@HelperMethods.patch_clean_configuration
def test_netifaces_not_available(self, _):
"""Check `netifaces` is really acting as a (soft-)dependency"""
lan_ip = LanIp()
@ -281,7 +258,7 @@ class TestLanIpEntry(unittest.TestCase, CustomAssertions):
self.assertIsNone(lan_ip.value)
self.assertEqual(
output_mock.append.call_args[0][1],
'Not detected'
DEFAULT_CONFIG['default_strings']['not_detected']
)

@ -6,6 +6,8 @@ import unittest
from unittest.mock import MagicMock, mock_open, patch
from archey.entries.model import Model
from archey.test.entries import HelperMethods
from archey.constants import DEFAULT_CONFIG
class TestModelEntry(unittest.TestCase):
@ -24,6 +26,7 @@ class TestModelEntry(unittest.TestCase):
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')
@ -32,6 +35,7 @@ class TestModelEntry(unittest.TestCase):
'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"""
with patch('archey.entries.model.open', mock_open(), create=True) as mock:
@ -57,6 +61,7 @@ class TestModelEntry(unittest.TestCase):
'MY-LAPTOP-MODEL\n' # `dmidecode` example output
]
)
@HelperMethods.patch_clean_configuration
def test_virtual_environment(self, _, __):
"""Test for virtual machine"""
self.assertEqual(
@ -76,15 +81,12 @@ class TestModelEntry(unittest.TestCase):
FileNotFoundError() # `dmidecode` call will fail
]
)
@patch(
'archey.configuration.Configuration.get',
return_value={'virtual_environment': 'Virtual Environment'}
)
def test_virtual_environment_without_dmidecode(self, _, __, ___):
@HelperMethods.patch_clean_configuration
def test_virtual_environment_without_dmidecode(self, _, __):
"""Test for virtual machine (with a failing `dmidecode` call)"""
self.assertEqual(
Model().value,
'Virtual Environment (xen, xen-domU)'
DEFAULT_CONFIG['default_strings']['virtual_environment'] + ' (xen, xen-domU)'
)
@patch(
@ -95,13 +97,13 @@ class TestModelEntry(unittest.TestCase):
'archey.entries.model.check_output',
return_value='systemd-nspawn\n' # `systemd-detect-virt` example output
)
@patch(
'archey.configuration.Configuration.get',
return_value={'virtual_environment': 'Virtual Environment'}
)
def test_virtual_environment_systemd_alone(self, _, __, ___):
@HelperMethods.patch_clean_configuration
def test_virtual_environment_systemd_alone(self, _, __):
"""Test for virtual environments, with systemd tools and `dmidecode`"""
self.assertEqual(Model().value, 'Virtual Environment (systemd-nspawn)')
self.assertEqual(
Model().value,
DEFAULT_CONFIG['default_strings']['virtual_environment'] + ' (systemd-nspawn)'
)
@patch(
'archey.entries.model.os.getuid',
@ -114,6 +116,7 @@ class TestModelEntry(unittest.TestCase):
'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)')
@ -126,11 +129,8 @@ class TestModelEntry(unittest.TestCase):
'archey.entries.model.check_output',
side_effect=FileNotFoundError()
)
@patch(
'archey.configuration.Configuration.get',
return_value={'not_detected': 'Not detected'}
)
def test_no_match(self, _, __, ___):
@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 = [
@ -146,7 +146,7 @@ class TestModelEntry(unittest.TestCase):
self.assertIsNone(model.value)
self.assertEqual(
output_mock.append.call_args[0][1],
'Not detected'
DEFAULT_CONFIG['default_strings']['not_detected']
)

@ -5,6 +5,8 @@ from unittest.mock import mock_open, patch, MagicMock
from archey.colors import Colors
from archey.entries.ram import RAM
from archey.test.entries import HelperMethods
from archey.constants import DEFAULT_CONFIG
class TestRAMEntry(unittest.TestCase):
@ -19,16 +21,17 @@ class TestRAMEntry(unittest.TestCase):
Mem: 15658 2043 10232 12 3382 13268
Swap: 4095 39 4056
""")
@patch(
'archey.configuration.Configuration.get',
return_value={
'ram': {
'warning': 25,
'danger': 45
},
@HelperMethods.patch_clean_configuration(
configuration={
'limits': {
'ram': {
'warning': 25,
'danger': 45
}
}
}
)
def test_free_dash_m(self, _, __):
def test_free_dash_m(self, _):
"""Test `free -m` output parsing for low RAM use case"""
output_mock = MagicMock()
RAM().output(output_mock)
@ -47,16 +50,17 @@ Swap: 4095 39 4056
Mem: 7412 3341 1503 761 2567 3011
Swap: 7607 5 7602
""")
@patch(
'archey.configuration.Configuration.get',
return_value={
'ram': {
'warning': 33.3,
'danger': 66.7
},
@HelperMethods.patch_clean_configuration(
configuration={
'limits': {
'ram': {
'warning': 33.3,
'danger': 66.7
}
}
}
)
def test_free_dash_m_warning(self, _, __):
def test_free_dash_m_warning(self, _):
"""Test `free -m` output parsing for warning RAM use case"""
output_mock = MagicMock()
RAM().output(output_mock)
@ -75,16 +79,17 @@ Swap: 7607 5 7602
Mem: 15658 12341 624 203 2692 2807
Swap: 4095 160 3935
""")
@patch(
'archey.configuration.Configuration.get',
return_value={
'ram': {
'warning': 33.3,
'danger': 66.7
},
@HelperMethods.patch_clean_configuration(
configuration={
'limits': {
'ram': {
'warning': 33.3,
'danger': 66.7
}
}
}
)
def test_free_dash_m_danger(self, _, __):
def test_free_dash_m_danger(self, _):
"""Test `free -m` output parsing for danger RAM use case"""
output_mock = MagicMock()
RAM().output(output_mock)
@ -151,13 +156,8 @@ SUnreclaim: 113308 kB
side_effect=PermissionError(),
create=True
)
@patch(
'archey.configuration.Configuration.get',
side_effect=[
{'not_detected': 'Not detected'}
]
)
def test_not_detected(self, _, __, ___):
@HelperMethods.patch_clean_configuration
def test_not_detected(self, _, __):
"""Check Archey does not crash when `/proc/meminfo` is not readable"""
ram = RAM()
@ -167,7 +167,7 @@ SUnreclaim: 113308 kB
self.assertIsNone(ram.value)
self.assertEqual(
output_mock.append.call_args[0][1],
'Not detected'
DEFAULT_CONFIG['default_strings']['not_detected']
)

@ -6,6 +6,8 @@ import unittest
from unittest.mock import MagicMock, patch
from archey.entries.shell import Shell
from archey.test.entries import HelperMethods
from archey.constants import DEFAULT_CONFIG
class TestShellEntry(unittest.TestCase):
@ -44,11 +46,8 @@ class TestShellEntry(unittest.TestCase):
'archey.entries.shell.check_output',
side_effect=CalledProcessError(2, 'getent')
)
@patch(
'archey.configuration.Configuration.get',
return_value={'not_detected': 'Not detected'}
)
def test_config_fall_back(self, _, __, ___):
@HelperMethods.patch_clean_configuration
def test_config_fall_back(self, _, __):
"""`id` fails, but Archey must not !"""
shell = Shell()
@ -58,7 +57,7 @@ class TestShellEntry(unittest.TestCase):
self.assertIsNone(shell.value)
self.assertEqual(
output_mock.append.call_args[0][1],
'Not detected'
DEFAULT_CONFIG['default_strings']['not_detected']
)
if __name__ == '__main__':

@ -10,6 +10,7 @@ from unittest.mock import MagicMock, patch
from archey.entries.temperature import Temperature
from archey.test.entries import HelperMethods
class TestTemperatureEntry(unittest.TestCase):
@ -48,15 +49,16 @@ class TestTemperatureEntry(unittest.TestCase):
'archey.entries.temperature.iglob',
return_value=[] # No temperature from file will be retrieved
)
@patch(
'archey.configuration.Configuration.get',
side_effect=[
{'sensors_chipsets': []},
{'use_fahrenheit': False},
{'char_before_unit': ' '}
]
@HelperMethods.patch_clean_configuration(
configuration={
'temperature': {
'sensors_chipsets': [],
'use_fahrenheit': False,
'char_before_unit': ' '
}
}
)
def test_vcgencmd_only_no_max(self, _, __, ___):
def test_vcgencmd_only_no_max(self, _, __):
"""
Test for `vcgencmd` output only (no sensor files).
Only one value is retrieved, so no maximum should be displayed (see #39).
@ -88,15 +90,16 @@ class TestTemperatureEntry(unittest.TestCase):
]
)
@patch('archey.entries.temperature.iglob')
@patch(
'archey.configuration.Configuration.get',
side_effect=[
{'sensors_chipsets': []},
{'use_fahrenheit': False},
{'char_before_unit': ' '}
]
@HelperMethods.patch_clean_configuration(
configuration={
'temperature': {
'sensors_chipsets': [],
'use_fahrenheit': False,
'char_before_unit': ' '
}
}
)
def test_vcgencmd_and_files(self, _, iglob_mock, __):
def test_vcgencmd_and_files(self, iglob_mock, _):
"""Tests `vcgencmd` output AND sensor files"""
iglob_mock.return_value = iter([file.name for file in self._temp_files])
self.assertDictEqual(
@ -117,15 +120,16 @@ class TestTemperatureEntry(unittest.TestCase):
]
)
@patch('archey.entries.temperature.iglob')
@patch(
'archey.configuration.Configuration.get',
side_effect=[
{'sensors_chipsets': []},
{'use_fahrenheit': True},
{'char_before_unit': '@'}
]
@HelperMethods.patch_clean_configuration(
configuration={
'temperature': {
'sensors_chipsets': [],
'use_fahrenheit': True,
'char_before_unit': '@'
}
}
)
def test_files_only_in_fahrenheit(self, _, iglob_mock, __):
def test_files_only_in_fahrenheit(self, iglob_mock, _):
"""Test sensor files only, Fahrenheit (naive) conversion and special degree character"""
iglob_mock.return_value = iter([file.name for file in self._temp_files])
self.assertDictEqual(
@ -149,14 +153,10 @@ class TestTemperatureEntry(unittest.TestCase):
'archey.entries.temperature.iglob',
return_value=[] # No temperature from file will be retrieved.
)
@patch(
'archey.configuration.Configuration.get',
side_effect=[
{'sensors_chipsets': []},
{'not_detected': 'Not detected'}
]
@HelperMethods.patch_clean_configuration(
configuration={'temperature': {'sensors_chipsets': []}}
)
def test_no_output(self, _, __, ___):
def test_no_output(self, _, __):
"""Test when no value could be retrieved (anyhow)"""
self.assertIsNone(Temperature().value)
@ -221,15 +221,16 @@ class TestTemperatureEntry(unittest.TestCase):
FileNotFoundError() # No temperature from `vcgencmd` call.
]
)
@patch(
'archey.configuration.Configuration.get',
side_effect=[
{'sensors_chipsets': []},
{'use_fahrenheit': True},
{'char_before_unit': ' '}
]
@HelperMethods.patch_clean_configuration(
configuration={
'temperature': {
'sensors_chipsets': [],
'use_fahrenheit': True,
'char_before_unit': ' '
}
}
)
def test_sensors_only_in_fahrenheit(self, _, __):
def test_sensors_only_in_fahrenheit(self, _):
"""Test computations around `sensors` output and Fahrenheit (naive) conversion"""
self.assertDictEqual(
Temperature().value,
@ -249,15 +250,16 @@ class TestTemperatureEntry(unittest.TestCase):
]
)
@patch('archey.entries.temperature.iglob')
@patch(
'archey.configuration.Configuration.get',
side_effect=[
{'sensors_chipsets': []},
{'use_fahrenheit': False},
{'char_before_unit': 'o'}
]
@HelperMethods.patch_clean_configuration(
configuration={
'temperature': {
'sensors_chipsets': [],
'use_fahrenheit': False,
'char_before_unit': 'o'
}
}
)
def test_sensors_error_1(self, _, iglob_mock, ___):
def test_sensors_error_1(self, iglob_mock, _):
"""Test `sensors` (hard) failure handling and polling from files in Celsius"""
iglob_mock.return_value = iter([file.name for file in self._temp_files])
@ -294,15 +296,16 @@ class TestTemperatureEntry(unittest.TestCase):
]
)
@patch('archey.entries.temperature.iglob')
@patch(
'archey.configuration.Configuration.get',
side_effect=[
{'sensors_chipsets': []},
{'use_fahrenheit': False},
{'char_before_unit': 'o'}
]
@HelperMethods.patch_clean_configuration(
configuration={
'temperature': {
'sensors_chipsets': [],
'use_fahrenheit': False,
'char_before_unit': 'o'
}
}
)
def test_sensors_error_2(self, _, iglob_mock, ___):
def test_sensors_error_2(self, iglob_mock, _):
"""Test `sensors` (hard) failure handling and polling from files in Celsius"""
iglob_mock.return_value = iter([file.name for file in self._temp_files])
self.assertDictEqual(
@ -326,14 +329,8 @@ class TestTemperatureEntry(unittest.TestCase):
'archey.entries.temperature.iglob',
return_value=[] # No temperature from file will be retrieved.
)
@patch(
'archey.configuration.Configuration.get',
side_effect=[
{'sensors_chipsets': []}, # Needed key.
{'not_detected': 'Not detected'} # Needed key.
]
)
def test_celsius_to_fahrenheit_conversion(self, _, __, ___):
@HelperMethods.patch_clean_configuration
def test_celsius_to_fahrenheit_conversion(self, _, __):
"""Simple tests for the `_convert_to_fahrenheit` static method"""
test_conversion_cases = [
(-273.15, -459.67),

@ -4,6 +4,8 @@ import unittest
from unittest.mock import MagicMock, patch
from archey.entries.terminal import Terminal
from archey.test.entries import HelperMethods
from archey.constants import DEFAULT_CONFIG
class TestTerminalEntry(unittest.TestCase):
@ -20,11 +22,8 @@ class TestTerminalEntry(unittest.TestCase):
},
clear=True
)
@patch(
'archey.configuration.Configuration.get',
return_value={'use_unicode': False}
)
def test_terminal_emulator_term_program(self, _):
@HelperMethods.patch_clean_configuration
def test_terminal_emulator_term_program(self):
"""Check that `TERM_PROGRAM` is honored even if `TERM` or `COLORTERM` is defined"""
self.assertEqual(Terminal().value, 'A-COOL-TERMINAL-EMULATOR')
@ -36,11 +35,8 @@ class TestTerminalEntry(unittest.TestCase):
},
clear=True
)
@patch(
'archey.configuration.Configuration.get',
return_value={'use_unicode': False}
)
def test_terminal_emulator_special_term(self, _):
@HelperMethods.patch_clean_configuration
def test_terminal_emulator_special_term(self):
"""Check that `TERM` is honored even if a "known identifier" could be found"""
self.assertEqual(Terminal().value, 'OH-A-SPECIAL-CASE')
@ -52,11 +48,8 @@ class TestTerminalEntry(unittest.TestCase):
},
clear=True
)
@patch(
'archey.configuration.Configuration.get',
return_value={'use_unicode': False}
)
def test_terminal_emulator_name_normalization(self, _):
@HelperMethods.patch_clean_configuration
def test_terminal_emulator_name_normalization(self):
"""Check that our manual terminal detection as long as name normalization are working"""
self.assertEqual(Terminal().value, 'Konsole')
@ -65,11 +58,14 @@ class TestTerminalEntry(unittest.TestCase):
{'TERM': 'xterm-256color'},
clear=True
)
@patch(
'archey.configuration.Configuration.get',
return_value={'use_unicode': True}
@HelperMethods.patch_clean_configuration(
configuration={
'colors_palette': {
'use_unicode': True
}
}
)
def test_terminal_emulator_term_fallback_and_unicode(self, _):
def test_terminal_emulator_term_fallback_and_unicode(self):
"""Check that `TERM` is honored if present, and Unicode support for the colors palette"""
terminal = Terminal()
@ -90,11 +86,8 @@ class TestTerminalEntry(unittest.TestCase):
{'COLORTERM': 'kmscon'},
clear=True
)
@patch(
'archey.configuration.Configuration.get',
return_value={'use_unicode': False}
)
def test_terminal_emulator_colorterm(self, _):
@HelperMethods.patch_clean_configuration
def test_terminal_emulator_colorterm(self):
"""Check we can detect terminals using the `COLORTERM` environment variable."""
self.assertEqual(Terminal().value, 'KMSCON')
@ -107,11 +100,8 @@ class TestTerminalEntry(unittest.TestCase):
},
clear=True
)
@patch(
'archey.configuration.Configuration.get',
return_value={'use_unicode': False}
)
def test_terminal_emulator_colorterm_override(self, _):
@HelperMethods.patch_clean_configuration
def test_terminal_emulator_colorterm_override(self):
"""
Check we observe terminal using `COLORTERM` even if `TERM` or a "known identifier" is found.
"""
@ -122,14 +112,14 @@ class TestTerminalEntry(unittest.TestCase):
{},
clear=True
)
@patch(
'archey.configuration.Configuration.get',
side_effect=[
{'not_detected': 'Not detected'},
{'use_unicode': False}
]
@HelperMethods.patch_clean_configuration(
configuration={
'colors_palette': {
'use_unicode': False
}
}
)
def test_not_detected(self, _):
def test_not_detected(self):
"""Test terminal emulator (non-)detection, without Unicode support"""
terminal = Terminal()
@ -138,7 +128,7 @@ class TestTerminalEntry(unittest.TestCase):
output = output_mock.append.call_args[0][1]
self.assertIsNone(terminal.value)
self.assertTrue(output.startswith('Not detected'))
self.assertTrue(output.startswith(DEFAULT_CONFIG['default_strings']['not_detected']))
self.assertFalse(output.count('\u2588'))

@ -6,6 +6,8 @@ import unittest
from unittest.mock import MagicMock, patch
from archey.entries.user import User
from archey.test.entries import HelperMethods
from archey.constants import DEFAULT_CONFIG
class TestUserEntry(unittest.TestCase):
@ -41,11 +43,8 @@ class TestUserEntry(unittest.TestCase):
'archey.entries.user.check_output',
side_effect=CalledProcessError(1, 'id', "id: 1000: no such user\n")
)
@patch(
'archey.configuration.Configuration.get',
return_value={'not_detected': 'Not detected'}
)
def test_config_fall_back(self, _, __, ___):
@HelperMethods.patch_clean_configuration
def test_config_fall_back(self, _, __):
"""`id` fails, but Archey must not !"""
user = User()
@ -55,7 +54,7 @@ class TestUserEntry(unittest.TestCase):
self.assertIsNone(user.value)
self.assertEqual(
output_mock.append.call_args[0][1],
'Not detected'
DEFAULT_CONFIG['default_strings']['not_detected']
)

@ -9,6 +9,8 @@ from urllib.error import URLError
from archey.test import CustomAssertions
from archey.entries.wan_ip import WanIp
from archey.test.entries import HelperMethods
from archey.constants import DEFAULT_CONFIG
class TestWanIpEntry(unittest.TestCase, CustomAssertions):
"""
@ -21,15 +23,14 @@ class TestWanIpEntry(unittest.TestCase, CustomAssertions):
'0123::4567:89a:dead:beef\n'
]
)
@patch(
'archey.configuration.Configuration.get',
side_effect=[
{'ipv4_detection': None}, # Needed key.
{'wan_ip_v6_support': True},
{'ipv6_detection': None} # Needed key.
]
@HelperMethods.patch_clean_configuration(
configuration={
'ip_settings': {
'wan_ip_v6_support': True
}
}
)
def test_ipv6_and_ipv4(self, _, __):
def test_ipv6_and_ipv4(self, _):
"""Test the regular case : Both IPv4 and IPv6 are retrieved"""
self.assertEqual(
WanIp().value,
@ -40,14 +41,14 @@ class TestWanIpEntry(unittest.TestCase, CustomAssertions):
'archey.entries.wan_ip.check_output',
return_value='XXX.YY.ZZ.TTT'
)
@patch(
'archey.configuration.Configuration.get',
side_effect=[
{'ipv4_detection': None}, # Needed key.
{'wan_ip_v6_support': False}
]
@HelperMethods.patch_clean_configuration(
configuration={
'ip_settings': {
'wan_ip_v6_support': False
}
}
)
def test_ipv4_only(self, _, __):
def test_ipv4_only(self, _):
"""Test only public IPv4 detection"""
self.assertEqual(
WanIp().value,
@ -62,16 +63,14 @@ class TestWanIpEntry(unittest.TestCase, CustomAssertions):
]
)
@patch('archey.entries.wan_ip.urlopen')
@patch(
'archey.configuration.Configuration.get',
side_effect=[
{'ipv4_detection': None}, # Needed key.
{'wan_ip_v6_support': True},
{'ipv6_detection': None}, # Needed key.
{'ipv6_detection': None} # Needed key.
]
@HelperMethods.patch_clean_configuration(
configuration={
'ip_settings': {
'wan_ip_v6_support': True
}
}
)
def test_ipv6_timeout(self, _, urlopen_mock, ___):
def test_ipv6_timeout(self, urlopen_mock, _):
"""
Test when `dig` call timeout for the IPv6 detection.
Additionally check the `output` method behavior.
@ -100,15 +99,14 @@ class TestWanIpEntry(unittest.TestCase, CustomAssertions):
'archey.entries.wan_ip.urlopen',
side_effect=URLError('<urlopen error timed out>') # `urlopen` call will fail
)
@patch(
'archey.configuration.Configuration.get',
side_effect=[
{'ipv4_detection': None}, # Needed key.
{'ipv4_detection': None}, # Needed key.
{'wan_ip_v6_support': False}
]
@HelperMethods.patch_clean_configuration(
configuration={
'ip_settings': {
'wan_ip_v6_support': False
}
}
)
def test_ipv4_timeout_twice(self, _, __, ___):
def test_ipv4_timeout_twice(self, _, __):
"""Test when both `dig` and `URLOpen` trigger timeouts..."""
self.assertListEmpty(WanIp().value)
@ -120,15 +118,14 @@ class TestWanIpEntry(unittest.TestCase, CustomAssertions):
'archey.entries.wan_ip.urlopen',
side_effect=SocketTimeoutError(1) # `urlopen` call will fail
)
@patch(
'archey.configuration.Configuration.get',
side_effect=[
{'ipv4_detection': None}, # Needed key.
{'ipv4_detection': None}, # Needed key.
{'wan_ip_v6_support': False}
]
@HelperMethods.patch_clean_configuration(
configuration={
'ip_settings': {
'wan_ip_v6_support': False
}
}
)
def test_ipv4_timeout_twice_socket_error(self, _, __, ___):
def test_ipv4_timeout_twice_socket_error(self, _, __):
"""Test when both `dig` timeouts and `URLOpen` raises `socket.timeout`..."""
self.assertListEmpty(WanIp().value)
@ -140,15 +137,14 @@ class TestWanIpEntry(unittest.TestCase, CustomAssertions):
'urllib.request.urlopen',
return_value=None # No object will be returned
)
@patch(
'archey.configuration.Configuration.get',
side_effect=[
{'ipv4_detection': None}, # Needed key.
{'wan_ip_v6_support': False},
{'no_address': 'No Address'}
]
@HelperMethods.patch_clean_configuration(
configuration={
'ip_settings': {
'wan_ip_v6_support': False
}
}
)
def test_no_address(self, _, __, ___):
def test_no_address(self, _, __):
"""
Test when no address could be retrieved.
Additionally check the `output` method behavior.
@ -161,7 +157,7 @@ class TestWanIpEntry(unittest.TestCase, CustomAssertions):
self.assertListEmpty(wan_ip.value)
self.assertEqual(
output_mock.append.call_args[0][1],
'No Address'
DEFAULT_CONFIG['default_strings']['no_address']
)

@ -4,6 +4,8 @@ import unittest
from unittest.mock import MagicMock, patch
from archey.entries.window_manager import WindowManager
from archey.test.entries import HelperMethods
from archey.constants import DEFAULT_CONFIG
class TestWindowManagerEntry(unittest.TestCase):
@ -56,11 +58,8 @@ Window manager's "showing the desktop" mode: OFF
'here'
)
)
@patch(
'archey.configuration.Configuration.get',
return_value={'not_detected': 'Not detected'}
)
def test_no_wmctrl_mismatch(self, _, __):
@HelperMethods.patch_clean_configuration
def test_no_wmctrl_mismatch(self, _):
"""Test (non-detection) when processes list do not contain any known value"""
window_manager = WindowManager()
@ -70,7 +69,7 @@ Window manager's "showing the desktop" mode: OFF
self.assertIsNone(window_manager.value)
self.assertEqual(
output_mock.append.call_args[0][1],
'Not detected'
DEFAULT_CONFIG['default_strings']['not_detected']
)

@ -132,82 +132,78 @@ class TestConfigurationUtil(unittest.TestCase):
)
def test_update_recursive(self):
"""Test for the `_update_recursive` private method"""
configuration = Configuration()
with patch.dict(
configuration._config, # pylint: disable=protected-access
{
'allow_overriding': True,
'suppress_warnings': False,
'default_strings': {
'no_address': 'No Address',
'not_detected': 'Not detected'
},
'colors_palette': {
'use_unicode': False
},
'ip_settings': {
'lan_ip_max_count': 2
},
'temperature': {
'use_fahrenheit': False
}
},
clear=True
):
# We change existing values, add new ones, and omit some others.
configuration._update_recursive( # pylint: disable=protected-access
configuration._config, # pylint: disable=protected-access
{
'suppress_warnings': True,
'colors_palette': {
'use_unicode': False
},
'default_strings': {
'no_address': '\xde\xad \xbe\xef',
'not_detected': 'Not detected',
'virtual_environment': 'Virtual Environment'
},
'temperature': {
'a_weird_new_dict': [
None,
'l33t',
{
'really': 'one_more_?'
}
]
}
}
)
"""Test for the `update_recursive` class method"""
test_dict = {
'allow_overriding': True,
'suppress_warnings': False,
'default_strings': {
'no_address': 'No Address',
'not_detected': 'Not detected'
},
'colors_palette': {
'use_unicode': False
},
'ip_settings': {
'lan_ip_max_count': 2
},
'temperature': {
'use_fahrenheit': False
}
}
self.assertDictEqual(
configuration._config, # pylint: disable=protected-access
{
'allow_overriding': True,
'suppress_warnings': True,
'colors_palette': {
'use_unicode': False
},
'default_strings': {
'no_address': '\xde\xad \xbe\xef',
'not_detected': 'Not detected',
'virtual_environment': 'Virtual Environment'
},
'ip_settings': {
'lan_ip_max_count': 2
},
'temperature': {
'use_fahrenheit': False,
'a_weird_new_dict': [
None,
'l33t',
{
'really': 'one_more_?'
}
]
}
# We change existing values, add new ones, and omit some others.
Configuration.update_recursive(
test_dict,
{
'suppress_warnings': True,
'colors_palette': {
'use_unicode': False
},
'default_strings': {
'no_address': '\xde\xad \xbe\xef',
'not_detected': 'Not detected',
'virtual_environment': 'Virtual Environment'
},
'temperature': {
'a_weird_new_dict': [
None,
'l33t',
{
'really': 'one_more_?'
}
]
}
)
}
)
self.assertDictEqual(
test_dict,
{
'allow_overriding': True,
'suppress_warnings': True,
'colors_palette': {
'use_unicode': False
},
'default_strings': {
'no_address': '\xde\xad \xbe\xef',
'not_detected': 'Not detected',
'virtual_environment': 'Virtual Environment'
},
'ip_settings': {
'lan_ip_max_count': 2
},
'temperature': {
'use_fahrenheit': False,
'a_weird_new_dict': [
None,
'l33t',
{
'really': 'one_more_?'
}
]
}
}
)
def test_instantiation_config_path(self):
"""Test for configuration loading from specific user-defined path"""
@ -242,6 +238,14 @@ class TestConfigurationUtil(unittest.TestCase):
self.assertEqual(configuration.get('ip_settings')['lan_ip_max_count'], 4)
self.assertTrue(configuration.get('temperature')['use_fahrenheit'])
def test__iter__(self):
"""Very simple method checking our `__iter__` implementation"""
configuration = Configuration()
self.assertEqual(
configuration._config, # pylint: disable=protected-access
dict(configuration)
)
if __name__ == '__main__':
unittest.main()

@ -16,7 +16,7 @@ setup(
name='archey4',
version=__version__.lstrip('v'),
description='Archey is a simple system information tool written in Python',
keywords='archey python3 linux system-information monitoring',
keywords='archey python3 linux system-information monitoring screenshot',
url='https://github.com/HorlogeSkynet/archey4',
author='Samuel Forestier', # Not alone
author_email='dev+archey@samuel.domains',