1
0
mirror of https://github.com/HorlogeSkynet/archey4 synced 2025-05-08 08:00:13 +02:00

Compare commits

...

3 Commits

Author SHA1 Message Date
Michael Bromilow
5b9efa5982
[TEMPERATURE] Re-add fallback if none detected.
Accidentally removed in 2188def81aa0a10a7ebdb943e029397865994acf.

Sidenote: We should probably add a test for `output` on entries now.
2020-05-13 03:58:55 +01:00
Michael Bromilow
2188def81a
[ENTRY] Modify value attribute to use objects.
Rationale: Having the raw data in a consistent place makes it far easier
to implement an API with our entries.

* Removed `_format_to_json` attribute introduced in e709d0e97c486504e7027912186ad58b7f547aad.
* Modified all entries to place a generic object (list, string, dict)
  into the `value` attribute which suits the data best.
* Moved pretty-formatting logic to an overriden `output` method.
* Tweaked API implementation to use individual entries rather than their
  outputs (we now deal with the `value` attribute in the API which
  should contain all of an entry's data, even if it outputs multiple
  lines).
* Modified tests as appropriate :)
2020-05-13 03:37:11 +01:00
Michael Bromilow
e709d0e97c
[ENTRIES] Add JSON formatted output to entries
Adds proper JSON formatting to the following entries when using the `-j`
option:
* Disk
* LanIp
* RAM
* Temperature
* Terminal
* Uptime
* WanIp

Additionally adds a new attribute `_format_to_json` to the `Entry` base
class.
2020-05-12 22:30:13 +01:00
19 changed files with 445 additions and 215 deletions

@ -5,7 +5,6 @@ from datetime import datetime
import json
from archey._version import __version__
from archey.colors import Colors
class API:
@ -14,12 +13,12 @@ class API:
At the moment, only JSON has been implemented.
Feel free to contribute to add other formats as needed.
"""
def __init__(self, results):
self.results = results
def __init__(self, entries):
self.entries = entries
def json_serialization(self, pretty_print=False):
"""
JSON serialization of results.
JSON serialization of entries.
Set `pretty_print` to `True` to enable output indentation.
Note: For Python < 3.6, the keys order is not guaranteed.
@ -31,14 +30,8 @@ class API:
'date': datetime.now().isoformat()
}
}
for result in self.results:
document['data'].setdefault(
result[0], []
).append(
Colors.remove_colors(result[1])
if isinstance(result[1], str)
else result[1]
)
for entry in self.entries:
document['data'][entry.name] = entry.value
return json.dumps(
document,

@ -24,24 +24,15 @@ class Disk(Entry):
# Check whether at least one media could be found.
if not self._usage['total']:
self.value = self._configuration.get('default_strings')['not_detected']
self.value = None
return
# Fetch the user-defined disk limits from configuration.
disk_limits = self._configuration.get('limits')['disk']
self.value = {
'used': self._usage['used'],
'total': self._usage['total'],
'unit': 'GiB' # for now
}
# Based on the disk percentage usage, select the corresponding level color.
level_color = Colors.get_level_color(
(self._usage['used'] / (self._usage['total'] or 1)) * 100,
disk_limits['warning'], disk_limits['danger']
)
self.value = '{0}{1} GiB{2} / {3} GiB'.format(
level_color,
round(self._usage['used'], 1),
Colors.CLEAR,
round(self._usage['total'], 1)
)
def _run_df_usage(self):
try:
@ -134,3 +125,30 @@ class Disk(Entry):
self._usage['total'] += sum(logical_device_size)
self._usage['used'] += sum(logical_device_used)
def output(self, output):
"""Adds the entry to `output` after formatting with colour and units."""
# Fetch the user-defined disk limits from configuration.
disk_limits = self._configuration.get('limits')['disk']
# Based on the disk percentage usage, select the corresponding level color.
level_color = Colors.get_level_color(
(self.value['used'] / (self.value['total'] or 1)) * 100,
disk_limits['warning'], disk_limits['danger']
)
try:
output.append(
self.name,
'{0}{1} {unit}{2} / {3} {unit}'.format(
level_color,
round(self.value['used'], 1),
Colors.CLEAR,
round(self.value['total'], 1),
unit=self.value['unit']
)
)
except TypeError:
# We didn't find any disks, fall back to the default entry behaviour.
super().output(output)

@ -38,5 +38,17 @@ class LanIp(Entry):
if lan_ip_max_count is not False:
ip_addresses = ip_addresses[:lan_ip_max_count]
self.value = ', '.join(ip_addresses) or \
self._configuration.get('default_strings')['no_address']
self.value = ip_addresses or self._configuration.get('default_strings')['no_address']
def output(self, output):
"""Adds the entry to `output` after pretty-formatting the IP address list."""
if isinstance(self.value, list):
# If we found IP addresses, join them together nicely.
output.append(
self.name,
', '.join(self.value)
)
else:
# Otherwise go with the default behaviour for the "no address" string.
super().output(output)

@ -46,6 +46,21 @@ class RAM(Entry):
if used < 0:
used = total - ram['MemFree']
self.value = {
'used': used,
'total': total,
'unit': 'MiB'
}
def output(self, output):
"""
Adds the entry to `output` after pretty-formatting the RAM usage with colour and units.
"""
# DRY some constants
used = self.value['used']
total = self.value['total']
unit = self.value['unit']
# Fetch the user-defined RAM limits from configuration.
ram_limits = self._configuration.get('limits')['ram']
@ -55,9 +70,13 @@ class RAM(Entry):
ram_limits['warning'], ram_limits['danger']
)
self.value = '{0}{1} MiB{2} / {3} MiB'.format(
level_color,
int(used),
Colors.CLEAR,
int(total)
output.append(
self.name,
'{0}{1} {unit}{2} / {3} {unit}'.format(
level_color,
int(used),
Colors.CLEAR,
int(total),
unit=unit
)
)

@ -32,7 +32,7 @@ class Temperature(Entry):
# No value could be fetched...
if not self._temps:
self.value = self._configuration.get('default_strings')['not_detected']
self.value = None
return
# Let's DRY some constants once.
@ -45,19 +45,20 @@ class Temperature(Entry):
self._temps[i] = self._convert_to_fahrenheit(self._temps[i])
# Final average computation.
self.value = '{0}{1}{2}'.format(
str(round(sum(self._temps) / len(self._temps), 1)),
char_before_unit,
'F' if use_fahrenheit else 'C'
final_temperature = float(round(sum(self._temps) / len(self._temps), 1))
# Set ourselves a max_temperature (the hottest of multiple values), if there is one.
max_temperature = (
float(round(max(self._temps), 1))
if len(self._temps) > 1 else None
)
# Multiple values ? Show the hottest.
if len(self._temps) > 1:
self.value += ' (Max. {0}{1}{2})'.format(
str(round(max(self._temps), 1)),
char_before_unit,
'F' if use_fahrenheit else 'C'
)
self.value = {
'temperature': final_temperature,
'max_temperature': max_temperature,
'char_before_unit': char_before_unit,
'unit': 'F' if use_fahrenheit else 'C'
}
def _run_sensors(self, whitelisted_chips):
# Uses the `sensors` program (from LM-Sensors) to interrogate thermal chip-sets.
@ -126,3 +127,31 @@ class Temperature(Entry):
Simple Celsius to Fahrenheit conversion method
"""
return temp * (9 / 5) + 32
def output(self, output):
"""Adds the entry to `output` after pretty-formatting with units."""
if not self.value:
# Fall back to the default behaviour if no temperatures were detected.
super().output(output)
return
# DRY some constants
char_before_unit = self.value['char_before_unit']
unit = self.value['unit']
max_temperature_string = " (Max. {0}{1}{2}".format(
self.value['max_temperature'],
char_before_unit,
unit
) if self.value['max_temperature'] else ''
output.append(
self.name,
'{0}{1}{2}{3}'.format(
self.value['temperature'],
char_before_unit,
unit,
max_temperature_string
)
)

@ -19,6 +19,13 @@ class Terminal(Entry):
self._configuration.get('default_strings')['not_detected']
)
self.value = terminal
def output(self, output):
"""
Adds the entry to `output` after pretty-formatting with colours and potentially unicode.
"""
# On systems with non-Unicode locales, we imitate '\u2588' character
# ... with '#' to display the terminal colors palette.
# This is the default option for backward compatibility.
@ -32,4 +39,7 @@ class Terminal(Entry):
) for i in range(37, 30, -1)
])
self.value = '{0} {1}'.format(terminal, colors)
output.append(
self.name,
'{0} {1}'.format(self.value, colors)
)

@ -23,34 +23,11 @@ class Uptime(Entry):
hours, uptime_seconds = divmod(uptime_seconds, 3600)
minutes = uptime_seconds // 60
uptime = ''
if days:
uptime += str(days) + ' day'
if days > 1:
uptime += 's'
if hours or minutes:
if bool(hours) != bool(minutes):
uptime += ' and '
else:
uptime += ', '
if hours:
uptime += str(hours) + ' hour'
if hours > 1:
uptime += 's'
if minutes:
uptime += ' and '
if minutes:
uptime += str(minutes) + ' minute'
if minutes > 1:
uptime += 's'
elif not days and not hours:
uptime = '< 1 minute'
self.value = uptime
self.value = {
'days': days,
'hours': hours,
'minutes': minutes,
}
def _get_uptime_delta(self):
"""
@ -175,3 +152,42 @@ class Uptime(Entry):
minutes=int(uptime_args.get('minutes') or 0),
seconds=int(uptime_args.get('seconds') or 0)
)
def output(self, output):
"""Adds the entry to `output` after pretty-formatting the uptime to a string."""
days = self.value['days']
hours = self.value['hours']
minutes = self.value['minutes']
uptime = ''
if days:
uptime += str(days) + ' day'
if days > 1:
uptime += 's'
if hours or minutes:
if bool(hours) != bool(minutes):
uptime += ' and '
else:
uptime += ', '
if hours:
uptime += str(hours) + ' hour'
if hours > 1:
uptime += 's'
if minutes:
uptime += ' and '
if minutes:
uptime += str(minutes) + ' minute'
if minutes > 1:
uptime += 's'
elif not days and not hours:
uptime = '< 1 minute'
output.append(
self.name,
uptime
)

@ -21,9 +21,11 @@ class WanIp(Entry):
else:
ipv6_addr = None
self.value = ', '.join(
filter(None, (ipv4_addr, ipv6_addr))
) or self._configuration.get('default_strings')['no_address']
self.value = (
list(filter(None, (ipv4_addr, ipv6_addr)))
or self._configuration.get('default_strings')['no_address']
)
def _retrieve_ipv4_address(self):
try:
@ -73,3 +75,16 @@ class WanIp(Entry):
ipv6_addr = response.read().decode().strip()
return ipv6_addr
def output(self, output):
"""Adds the entry to `output` after pretty-formatting our list of IP addresses."""
if isinstance(self.value, list):
# If we found IP addresses, join them together nicely.
output.append(
self.name,
', '.join(self.value)
)
else:
# Otherwise go with the default behaviour for the "no address" string.
super().output(output)

@ -9,14 +9,23 @@ class Entry(AbstractBaseClass):
"""Module base class"""
@abstractmethod
def __init__(self, name=None, value=None):
# Each entry will have `name` (key) and `value` attributes.
# `None` by default.
# Each entry will have always have the following attributes...
# `name` (key);
# `value` (value of entry as an appropriate object)
# ...which are `None` by default.
self.name = name
self.value = value
# Propagates a reference to `Configuration` singleton to each inheriting class.
self._configuration = Configuration()
def output(self, output):
"""Output the results to output. Can be overridden by subclasses."""
output.append(self.name, self.value)
if self.value:
# Let's assume we can just use `__str__` on the object in value,
# and create a single-line output with it.
output.append(self.name, str(self.value))
else:
# If the value is falsy leave a generic "Not detected" message for this entry.
output.append(self.name, self._configuration.get('default_strings')['not_detected'])

@ -71,17 +71,14 @@ class Output:
def append(self, key, value):
"""Append a pre-formatted entry to the final output content"""
if self.format_to_json:
self._results.append((key, value))
else:
self._results.append(
'{color}{key}:{clear} {value}'.format(
color=self._colors_palette[0],
key=key,
clear=Colors.CLEAR,
value=value
)
self._results.append(
'{color}{key}:{clear} {value}'.format(
color=self._colors_palette[0],
key=key,
clear=Colors.CLEAR,
value=value
)
)
def output(self):
"""
@ -89,13 +86,12 @@ class Output:
First we get entries to add their outputs to the results and then
calls specific `output` methods based (for instance) on preferred format.
"""
# Iterate through the entries and run their output method to add their content.
for entry in self._entries:
entry.output(self)
if self.format_to_json:
self._output_json()
else:
# Iterate through the entries and run their output method to add their content.
for entry in self._entries:
entry.output(self)
self._output_text()
def _output_json(self):
@ -104,7 +100,7 @@ class Output:
See `archey.api.JSONAPI` for further documentation.
"""
print(
API(self._results).json_serialization(pretty_print=True)
API(self._entries).json_serialization(pretty_print=True)
)
def _output_text(self):

@ -2,6 +2,7 @@
import json
import unittest
from unittest.mock import Mock
from archey.api import API
@ -14,23 +15,38 @@ class TestApiUtil(unittest.TestCase):
"""
Check that our JSON serialization is working as expected.
"""
api_instance = API([
('simple', 'test'),
('some', 'more'),
('simple', 42),
('simple', '\x1b[31m???\x1b[0m')
])
mocked_entries = [
Mock(value='test'),
Mock(value='more'),
Mock(value=42),
Mock(
value={
'complex': {
'dictionary': True
}
}
),
]
# Since we can't assign a Mock's `name` attribute on creation, we'll do it here.
# Note: Since each entry is only present once, all `name` attributes are always unique.
for idx, name in enumerate(('simple1', 'some', 'simple2', 'simple3')):
mocked_entries[idx].name = name
api_instance = API(mocked_entries)
output_json_document = json.loads(api_instance.json_serialization())
self.assertDictEqual(
output_json_document['data'],
{
'simple': [
'test',
42,
'???'
],
'some': ['more']
'simple1': 'test',
'some': 'more',
'simple2': 42,
'simple3': {
'complex': {
'dictionary': True
}
}
}
)
self.assertIn('meta', output_json_document)

@ -3,10 +3,10 @@
from subprocess import CalledProcessError
import unittest
from unittest.mock import patch
from unittest.mock import patch, MagicMock
from archey.colors import Colors
from archey.entries.disk import Disk
from archey.colors import Colors
class TestDiskEntry(unittest.TestCase):
@ -38,8 +38,15 @@ total 305809MB 47006MB 243149MB 17% -
)
def test_df_only(self, _, __):
"""Test computations around `df` output at disk regular level"""
disk = Disk().value
self.assertTrue(all(i in disk for i in [str(Colors.GREEN_NORMAL), '45.9', '298.6']))
output_mock = MagicMock()
Disk().output(output_mock)
self.assertEqual(
output_mock.append.call_args.args[1],
'{0}45.9 GiB{1} / 298.6 GiB'.format(
Colors.GREEN_NORMAL,
Colors.CLEAR
)
)
@patch(
'archey.entries.disk.check_output',
@ -66,8 +73,15 @@ total 305809MB 257598MB 46130MB 84% -
)
def test_df_only_warning(self, _, __):
"""Test computations around `df` output at disk warning level"""
disk = Disk().value
self.assertTrue(all(i in disk for i in [str(Colors.YELLOW_NORMAL), '251.6', '298.6']))
output_mock = MagicMock()
Disk().output(output_mock)
self.assertEqual(
output_mock.append.call_args.args[1],
'{0}251.6 GiB{1} / 298.6 GiB'.format(
Colors.YELLOW_NORMAL,
Colors.CLEAR
)
)
@patch(
'archey.entries.disk.check_output',
@ -137,19 +151,16 @@ System,single: Size:0.01GiB, Used:0.00GiB (1.03%)
"""
]
)
@patch(
'archey.configuration.Configuration.get',
return_value={
'disk': {
'warning': 50,
'danger': 75
}
}
)
def test_df_and_btrfs(self, _, __):
def test_df_and_btrfs(self, _):
"""Test computations around `df` and `btrfs` outputs"""
disk = Disk().value
self.assertTrue(all(i in disk for i in [str(Colors.GREEN_NORMAL), '989.3', '4501.1']))
self.assertDictEqual(
Disk().value,
{
'used': 989.304296875,
'total': 4501.1016015625,
'unit': 'GiB'
}
)
@patch(
'archey.entries.disk.check_output',
@ -218,19 +229,16 @@ System,RAID1: Size:0.01GiB, Used:0.00GiB
"""
]
)
@patch(
'archey.configuration.Configuration.get',
return_value={
'disk': {
'warning': 50,
'danger': 75
}
}
)
def test_btrfs_only_with_raid_configuration(self, _, __):
def test_btrfs_only_with_raid_configuration(self, _):
"""Test computations around `btrfs` outputs with a RAID-1 setup"""
disk = Disk().value
self.assertTrue(all(i in disk for i in [str(Colors.GREEN_NORMAL), '943.4', '4202.5']))
self.assertDictEqual(
Disk().value,
{
'used': 943.4,
'total': 4202.46,
'unit': 'GiB'
}
)
@patch(
'archey.entries.disk.check_output',
@ -239,13 +247,9 @@ System,RAID1: Size:0.01GiB, Used:0.00GiB
CalledProcessError(1, 'df', "df: unrecognized option: l\n")
]
)
@patch(
'archey.configuration.Configuration.get',
return_value={'not_detected': 'Not detected'}
)
def test_df_failing(self, _, __):
def test_df_failing(self, _):
"""Test df call failing against the BusyBox implementation"""
self.assertEqual(Disk().value, 'Not detected')
self.assertEqual(Disk().value, None)
@patch(
'archey.entries.disk.check_output',
@ -254,13 +258,9 @@ System,RAID1: Size:0.01GiB, Used:0.00GiB
CalledProcessError(1, 'df', "df: no file systems processed\n")
]
)
@patch(
'archey.configuration.Configuration.get',
return_value={'not_detected': 'Not detected'}
)
def test_no_recognised_disks(self, _, __):
def test_no_recognised_disks(self, _):
"""Test df failing to detect any valid file-systems"""
self.assertEqual(Disk().value, 'Not detected')
self.assertEqual(Disk().value, None)
if __name__ == '__main__':

@ -54,9 +54,9 @@ class TestLanIpEntry(unittest.TestCase):
)
def test_multiple_interfaces(self, _, __, ___):
"""Test for multiple interfaces, multiple addresses (including a loopback one)"""
self.assertEqual(
self.assertListEqual(
LanIp().value,
'192.168.0.11, 192.168.1.11, 172.34.56.78'
['192.168.0.11', '192.168.1.11', '172.34.56.78']
)
@patch(
@ -119,7 +119,7 @@ class TestLanIpEntry(unittest.TestCase):
"""Test for IPv6 support, final set length limit and Ethernet interface filtering"""
self.assertEqual(
LanIp().value,
'192.168.1.55, 2001::45:6789:abcd:6817'
['192.168.1.55', '2001::45:6789:abcd:6817']
)
@patch(
@ -172,9 +172,9 @@ class TestLanIpEntry(unittest.TestCase):
)
def test_no_ipv6(self, _, __, ___):
"""Test for IPv6 hiding"""
self.assertEqual(
self.assertListEqual(
LanIp().value,
'192.168.1.55'
['192.168.1.55']
)
@patch(

@ -2,7 +2,7 @@
import json
import unittest
from unittest.mock import patch
from unittest.mock import patch, Mock
from collections import namedtuple
from archey.colors import Colors
@ -506,21 +506,27 @@ O \x1b[0;31m\x1b[0m...\x1b[0m\
def test_json_output_format(self, print_mock, _):
"""Test how the `output` method handles JSON preferred formatting of entries"""
output = Output(format_to_json=True)
output._results = [ # pylint: disable=protected-access
('test', 'test'),
('name', 0xDEAD)
# We can't set the `name` attribute of a mock on its creation,
# so this is a little bit messy...
mocked_entries = [
Mock(value='test'),
Mock(value=0xDEAD)
]
mocked_entries[0].name = 'test'
mocked_entries[1].name = 'name'
output._entries = mocked_entries # pylint: disable=protected-access
output.output()
# Check that `print` output is properly formatted as JSON, with expected results.
output_json_data = json.loads(print_mock.call_args[0][0])['data']
self.assertListEqual(
self.assertEqual(
output_json_data['test'],
['test']
'test'
)
self.assertListEqual(
self.assertEqual(
output_json_data['name'],
[0xDEAD]
0xDEAD
)

@ -1,7 +1,7 @@
"""Test module for Archey's RAM usage detection module"""
import unittest
from unittest.mock import mock_open, patch
from unittest.mock import mock_open, patch, MagicMock
from archey.colors import Colors
from archey.entries.ram import RAM
@ -15,9 +15,9 @@ class TestRAMEntry(unittest.TestCase):
@patch(
'archey.entries.ram.check_output',
return_value="""\
total used free shared buff/cache available
Mem: 7412 3341 1503 761 2567 3011
Swap: 7607 5 7602
total used free shared buff/cache available
Mem: 15658 2043 10232 12 3382 13268
Swap: 4095 39 4056
""")
@patch(
'archey.configuration.Configuration.get',
@ -29,16 +29,23 @@ Swap: 7607 5 7602
}
)
def test_free_dash_m(self, _, __):
"""Test `free -m` output parsing for low RAM use case and tweaked limits"""
ram = RAM().value
self.assertTrue(all(i in ram for i in [str(Colors.RED_NORMAL), '3341', '7412']))
"""Test `free -m` output parsing for low RAM use case"""
output_mock = MagicMock()
RAM().output(output_mock)
self.assertEqual(
output_mock.append.call_args.args[1],
'{0}2043 MiB{1} / 15658 MiB'.format(
Colors.GREEN_NORMAL,
Colors.CLEAR
)
)
@patch(
'archey.entries.ram.check_output',
return_value="""\
total used free shared buff/cache available
Mem: 15658 2043 10232 12 3382 13268
Swap: 4095 39 4056
total used free shared buff/cache available
Mem: 7412 3341 1503 761 2567 3011
Swap: 7607 5 7602
""")
@patch(
'archey.configuration.Configuration.get',
@ -51,8 +58,15 @@ Swap: 4095 39 4056
)
def test_free_dash_m_warning(self, _, __):
"""Test `free -m` output parsing for warning RAM use case"""
ram = RAM().value
self.assertTrue(all(i in ram for i in [str(Colors.GREEN_NORMAL), '2043', '15658']))
output_mock = MagicMock()
RAM().output(output_mock)
self.assertEqual(
output_mock.append.call_args.args[1],
'{0}3341 MiB{1} / 7412 MiB'.format(
Colors.YELLOW_NORMAL,
Colors.CLEAR
)
)
@patch(
'archey.entries.ram.check_output',
@ -72,22 +86,20 @@ Swap: 4095 160 3935
)
def test_free_dash_m_danger(self, _, __):
"""Test `free -m` output parsing for danger RAM use case"""
ram = RAM().value
self.assertTrue(all(i in ram for i in [str(Colors.RED_NORMAL), '12341', '15658']))
output_mock = MagicMock()
RAM().output(output_mock)
self.assertEqual(
output_mock.append.call_args.args[1],
'{0}12341 MiB{1} / 15658 MiB'.format(
Colors.RED_NORMAL,
Colors.CLEAR
)
)
@patch(
'archey.entries.ram.check_output',
side_effect=IndexError() # `free` call will fail
)
@patch(
'archey.configuration.Configuration.get',
return_value={
'ram': {
'warning': 33.3,
'danger': 66.7
},
}
)
@patch(
'archey.entries.ram.open',
mock_open(
@ -119,10 +131,16 @@ SUnreclaim: 113308 kB
"""), # Some lines have been ignored as they are useless for computations.
create=True
)
def test_proc_meminfo(self, _, __):
def test_proc_meminfo(self, _):
"""Test `/proc/meminfo` parsing (when `free` is not available)"""
ram = RAM().value
self.assertTrue(all(i in ram for i in [str(Colors.YELLOW_NORMAL), '3739', '7403']))
self.assertDictEqual(
RAM().value,
{
'used': 3739.296875,
'total': 7403.3203125,
'unit': 'MiB'
}
)
if __name__ == '__main__':

@ -61,7 +61,15 @@ class TestTemperatureEntry(unittest.TestCase):
Test for `vcgencmd` output only (no sensor files).
Only one value is retrieved, so no maximum is displayed (see #39).
"""
self.assertEqual(Temperature().value, '42.8 C')
self.assertDictEqual(
Temperature().value,
{
'temperature': 42.8,
'max_temperature': None,
'char_before_unit': ' ',
'unit': 'C'
}
)
@patch(
'archey.entries.temperature.check_output',
@ -82,7 +90,15 @@ class TestTemperatureEntry(unittest.TestCase):
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.assertEqual(Temperature().value, '45.0 C (Max. 50.0 C)')
self.assertDictEqual(
Temperature().value,
{
'temperature': 45.0,
'max_temperature': 50.0,
'char_before_unit': ' ',
'unit': 'C'
}
)
@patch(
'archey.entries.temperature.check_output',
@ -103,9 +119,14 @@ class TestTemperatureEntry(unittest.TestCase):
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.assertEqual(
self.assertDictEqual(
Temperature().value,
'116.0@F (Max. 122.0@F)' # 46.7 and 50.0 converted into Fahrenheit.
{
'temperature': 116.0, # 46.7 degrees C in Fahrenheit.
'max_temperature': 122.0, # 50 degrees C in Fahrenheit
'char_before_unit': '@',
'unit': 'F'
}
)
@patch(
@ -128,7 +149,7 @@ class TestTemperatureEntry(unittest.TestCase):
)
def test_no_output(self, _, __, ___):
"""Test when no value could be retrieved (anyhow)"""
self.assertEqual(Temperature().value, 'Not detected')
self.assertEqual(Temperature().value, None)
@patch(
'archey.entries.temperature.check_output', # Mock the `sensors` call.
@ -201,9 +222,14 @@ class TestTemperatureEntry(unittest.TestCase):
)
def test_sensors_only_in_fahrenheit(self, _, __):
"""Test computations around `sensors` output and Fahrenheit (naive) conversion"""
self.assertEqual(
self.assertDictEqual(
Temperature().value,
'126.6 F (Max. 237.2 F)' # 52.6 and 114.0 converted into Fahrenheit.
{
'temperature': 126.6, # (52.6 C in F)
'max_temperature': 237.2, # (114.0 C in F)
'char_before_unit': ' ',
'unit': 'F'
}
)
@patch(
@ -225,9 +251,14 @@ class TestTemperatureEntry(unittest.TestCase):
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])
self.assertEqual(
self.assertDictEqual(
Temperature().value,
'46.7oC (Max. 50.0oC)'
{
'temperature': 46.7,
'max_temperature': 50.0,
'char_before_unit': 'o',
'unit': 'C'
}
)
@patch(
@ -255,9 +286,14 @@ class TestTemperatureEntry(unittest.TestCase):
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.assertEqual(
self.assertDictEqual(
Temperature().value,
'46.7oC (Max. 50.0oC)'
{
'temperature': 46.7,
'max_temperature': 50.0,
'char_before_unit': 'o',
'unit': 'C'
}
)
@patch(

@ -1,7 +1,7 @@
"""Test module for Archey's terminal detection module"""
import unittest
from unittest.mock import patch
from unittest.mock import patch, MagicMock
from archey.entries.terminal import Terminal
@ -24,7 +24,9 @@ class TestTerminalEntry(unittest.TestCase):
)
def test_without_unicode(self, _, __):
"""Test simple output, without Unicode support (default)"""
output = Terminal().value
output_mock = MagicMock()
Terminal().output(output_mock)
output = output_mock.append.call_args.args[1]
self.assertTrue(output.startswith('TERMINAL '))
self.assertEqual(output.count('#'), 7 * 2)
@ -41,7 +43,9 @@ class TestTerminalEntry(unittest.TestCase):
)
def test_with_unicode(self, _, __):
"""Test simple output, with Unicode support !"""
output = Terminal().value
output_mock = MagicMock()
Terminal().output(output_mock)
output = output_mock.append.call_args.args[1]
self.assertTrue(output.startswith('TERMINAL '))
self.assertEqual(output.count('\u2588'), 7 * 2)
@ -58,10 +62,13 @@ class TestTerminalEntry(unittest.TestCase):
)
def test_not_detected(self, _, __):
"""Test simple output, with Unicode support !"""
output = Terminal().value
output_mock = MagicMock()
Terminal().output(output_mock)
output = output_mock.append.call_args.args[1]
self.assertTrue(output.startswith('Not detected '))
self.assertEqual(output.count('#'), 7 * 2)
if __name__ == '__main__':
unittest.main()

@ -1,7 +1,7 @@
"""Test module for Archey's uptime detection module"""
import unittest
from unittest.mock import mock_open, patch
from unittest.mock import mock_open, patch, MagicMock
from datetime import timedelta
from itertools import product
@ -21,7 +21,12 @@ class TestUptimeEntry(unittest.TestCase):
)
def test_warming_up(self):
"""Test when the device has just been started..."""
self.assertEqual(Uptime().value, '< 1 minute')
output_mock = MagicMock()
Uptime().output(output_mock)
self.assertEqual(
output_mock.append.call_args.args[1],
'< 1 minute'
)
@patch(
'archey.entries.uptime.open',
@ -32,7 +37,12 @@ class TestUptimeEntry(unittest.TestCase):
)
def test_minutes_only(self):
"""Test when only minutes should be displayed"""
self.assertEqual(Uptime().value, '2 minutes')
output_mock = MagicMock()
Uptime().output(output_mock)
self.assertEqual(
output_mock.append.call_args.args[1],
'2 minutes'
)
@patch(
'archey.entries.uptime.open',
@ -43,7 +53,12 @@ class TestUptimeEntry(unittest.TestCase):
)
def test_hours_and_minute(self):
"""Test when only hours AND minutes should be displayed"""
self.assertEqual(Uptime().value, '2 hours and 1 minute')
output_mock = MagicMock()
Uptime().output(output_mock)
self.assertEqual(
output_mock.append.call_args.args[1],
'2 hours and 1 minute'
)
@patch(
'archey.entries.uptime.open',
@ -54,7 +69,12 @@ class TestUptimeEntry(unittest.TestCase):
)
def test_day_and_hour_and_minutes(self):
"""Test when only days, hours AND minutes should be displayed"""
self.assertEqual(Uptime().value, '1 day, 1 hour and 2 minutes')
output_mock = MagicMock()
Uptime().output(output_mock)
self.assertEqual(
output_mock.append.call_args.args[1],
'1 day, 1 hour and 2 minutes'
)
@patch(
'archey.entries.uptime.open',
@ -65,7 +85,12 @@ class TestUptimeEntry(unittest.TestCase):
)
def test_days_and_minutes(self):
"""Test when only days AND minutes should be displayed"""
self.assertEqual(Uptime().value, '3 days and 3 minutes')
output_mock = MagicMock()
Uptime().output(output_mock)
self.assertEqual(
output_mock.append.call_args.args[1],
'3 days and 3 minutes'
)
@patch(
'archey.entries.uptime.open',
@ -86,7 +111,12 @@ class TestUptimeEntry(unittest.TestCase):
Test when we can't access /proc/uptime on Linux/macOS/BSD.
We only test one clock as all clocks rely on the same built-in `time.clock_gettime` method.
"""
self.assertEqual(Uptime().value, '16 minutes')
output_mock = MagicMock()
Uptime().output(output_mock)
self.assertEqual(
output_mock.append.call_args.args[1],
'16 minutes'
)
@patch('archey.entries.uptime.check_output')
def test_uptime_fallback(self, check_output_mock):

@ -33,7 +33,7 @@ class TestWanIpEntry(unittest.TestCase):
"""Test the regular case : Both IPv4 and IPv6 are retrieved"""
self.assertEqual(
WanIp().value,
'XXX.YY.ZZ.TTT, 0123::4567:89a:dead:beef'
['XXX.YY.ZZ.TTT', '0123::4567:89a:dead:beef']
)
@patch(
@ -51,7 +51,7 @@ class TestWanIpEntry(unittest.TestCase):
"""Test only public IPv4 detection"""
self.assertEqual(
WanIp().value,
'XXX.YY.ZZ.TTT'
['XXX.YY.ZZ.TTT']
)
@patch(
@ -75,9 +75,9 @@ class TestWanIpEntry(unittest.TestCase):
"""Test when `dig` call timeout for the IPv6 detection"""
urlopen_mock.return_value.read.return_value = b'0123::4567:89a:dead:beef\n'
self.assertEqual(
self.assertListEqual(
WanIp().value,
'XXX.YY.ZZ.TTT, 0123::4567:89a:dead:beef'
['XXX.YY.ZZ.TTT', '0123::4567:89a:dead:beef']
)
@patch(