mirror of
https://github.com/HorlogeSkynet/archey4
synced 2025-05-08 08:00:13 +02:00
Compare commits
3 Commits
32cdee50f6
...
5b9efa5982
Author | SHA1 | Date | |
---|---|---|---|
![]() |
5b9efa5982 | ||
![]() |
2188def81a | ||
![]() |
e709d0e97c |
@ -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(
|
||||
|
Loading…
x
Reference in New Issue
Block a user