Track test groups as list instead of a dict

By only including selected tests we can skip checking for the
enabled/disabled status.  This will also simplify the process
of disabling future tests for a disk if a failure is detected.
This commit is contained in:
2Shirt 2022-04-05 14:45:14 -06:00
parent 5ffa6d8261
commit fc2bb07d11
Signed by: 2Shirt
GPG key ID: 152FAC923B0E132C
3 changed files with 101 additions and 101 deletions

View file

@ -16,9 +16,21 @@ ATTRIBUTE_COLORS = (
) )
# NOTE: Force 4K read block size for disks >= 3TB # NOTE: Force 4K read block size for disks >= 3TB
BADBLOCKS_LARGE_DISK = 3 * 1024**4 BADBLOCKS_LARGE_DISK = 3 * 1024**4
BADBLOCKS_REGEX = re.compile(
r'^Pass completed, (\d+) bad blocks found. .(\d+)/(\d+)/(\d+) errors',
re.IGNORECASE,
)
CPU_CRITICAL_TEMP = 99 CPU_CRITICAL_TEMP = 99
CPU_FAILURE_TEMP = 90 CPU_FAILURE_TEMP = 90
CPU_TEST_MINUTES = 7 CPU_TEST_MINUTES = 7
IO_GRAPH_WIDTH = 40
IO_ALT_TEST_SIZE_FACTOR = 0.01
IO_BLOCK_SIZE = 512 * 1024
IO_CHUNK_SIZE = 32 * 1024**2
IO_MINIMUM_TEST_SIZE = 10 * 1024**3
IO_RATE_REGEX = re.compile(
r'(?P<bytes>\d+) bytes.* (?P<seconds>\S+) s(?:,|ecs )',
)
KEY_NVME = 'nvme_smart_health_information_log' KEY_NVME = 'nvme_smart_health_information_log'
KEY_SMART = 'ata_smart_attributes' KEY_SMART = 'ata_smart_attributes'
KNOWN_DISK_ATTRIBUTES = { KNOWN_DISK_ATTRIBUTES = {
@ -116,6 +128,18 @@ SMC_IDS = {
'Tp5P': {'CPU Temp': False, 'Source': 'PSU2 Secondary Component'}, 'Tp5P': {'CPU Temp': False, 'Source': 'PSU2 Secondary Component'},
'TS0C': {'CPU Temp': False, 'Source': 'CPU B DIMM Exit Ambient'}, 'TS0C': {'CPU Temp': False, 'Source': 'CPU B DIMM Exit Ambient'},
} }
STATUS_COLORS = {
'Passed': 'GREEN',
'Aborted': 'YELLOW',
'N/A': 'YELLOW',
'Skipped': 'YELLOW',
'Unknown': 'YELLOW',
'Working': 'YELLOW',
'Denied': 'RED',
'ERROR': 'RED',
'Failed': 'RED',
'TimedOut': 'RED',
}
TEMP_COLORS = { TEMP_COLORS = {
float('-inf'): 'CYAN', float('-inf'): 'CYAN',
00: 'BLUE', 00: 'BLUE',

View file

@ -10,15 +10,24 @@ import re
import subprocess import subprocess
import time import time
from collections import OrderedDict
from docopt import docopt from docopt import docopt
from wk import cfg, debug, exe, graph, log, net, std, tmux from wk import cfg, debug, exe, graph, log, net, std, tmux
from wk import os as wk_os from wk import os as wk_os
from wk.cfg.hw import (
BADBLOCKS_REGEX,
IO_GRAPH_WIDTH,
IO_ALT_TEST_SIZE_FACTOR,
IO_BLOCK_SIZE,
IO_CHUNK_SIZE,
IO_MINIMUM_TEST_SIZE,
IO_RATE_REGEX,
STATUS_COLORS,
)
from wk.hw import disk as hw_disk from wk.hw import disk as hw_disk
from wk.hw import sensors as hw_sensors from wk.hw import sensors as hw_sensors
from wk.hw import system as hw_system from wk.hw import system as hw_system
from wk.hw.test import Test from wk.hw.test import Test, TestGroup
# STATIC VARIABLES # STATIC VARIABLES
@ -34,18 +43,15 @@ Options:
-q --quick Skip menu and perform a quick check -q --quick Skip menu and perform a quick check
''' '''
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
BADBLOCKS_REGEX = re.compile( TEST_GROUPS = {
r'^Pass completed, (\d+) bad blocks found. .(\d+)/(\d+)/(\d+) errors', # Also used to build the menu options
re.IGNORECASE, ## NOTE: This needs to be above MENU_SETS
) 'CPU & Cooling': 'cpu_stress_tests',
IO_GRAPH_WIDTH = 40 'Disk Attributes': 'disk_attribute_check',
IO_ALT_TEST_SIZE_FACTOR = 0.01 'Disk Self-Test': 'disk_self_test',
IO_BLOCK_SIZE = 512 * 1024 'Disk Surface Scan': 'disk_surface_scan',
IO_CHUNK_SIZE = 32 * 1024**2 'Disk I/O Benchmark': 'disk_io_benchmark',
IO_MINIMUM_TEST_SIZE = 10 * 1024**3 }
IO_RATE_REGEX = re.compile(
r'(?P<bytes>\d+) bytes.* (?P<seconds>\S+) s(?:,|ecs )',
)
MENU_ACTIONS = ( MENU_ACTIONS = (
'Audio Test', 'Audio Test',
'Keyboard Test', 'Keyboard Test',
@ -57,16 +63,9 @@ MENU_ACTIONS_SECRET = (
'Matrix', 'Matrix',
'Tubes', 'Tubes',
) )
MENU_OPTIONS = (
'CPU & Cooling',
'Disk Attributes',
'Disk Self-Test',
'Disk Surface Scan',
'Disk I/O Benchmark',
)
MENU_OPTIONS_QUICK = ('Disk Attributes',) MENU_OPTIONS_QUICK = ('Disk Attributes',)
MENU_SETS = { MENU_SETS = {
'Full Diagnostic': (*MENU_OPTIONS,), 'Full Diagnostic': (*TEST_GROUPS,),
'Disk Diagnostic': ( 'Disk Diagnostic': (
'Disk Attributes', 'Disk Attributes',
'Disk Self-Test', 'Disk Self-Test',
@ -79,19 +78,6 @@ MENU_TOGGLES = (
'Skip USB Benchmarks', 'Skip USB Benchmarks',
) )
PLATFORM = std.PLATFORM PLATFORM = std.PLATFORM
STATUS_COLORS = {
'Passed': 'GREEN',
'Aborted': 'YELLOW',
'N/A': 'YELLOW',
'Skipped': 'YELLOW',
'Unknown': 'YELLOW',
'Working': 'YELLOW',
'Denied': 'RED',
'ERROR': 'RED',
'Failed': 'RED',
'TimedOut': 'RED',
}
# Error Classes # Error Classes
class DeviceTooSmallError(RuntimeError): class DeviceTooSmallError(RuntimeError):
@ -107,33 +93,7 @@ class State():
self.log_dir = None self.log_dir = None
self.panes = {} self.panes = {}
self.system = None self.system = None
self.tests = OrderedDict({ self.test_groups = []
'CPU & Cooling': {
'Enabled': False,
'Function': cpu_stress_tests,
'Objects': [],
},
'Disk Attributes': {
'Enabled': False,
'Function': disk_attribute_check,
'Objects': [],
},
'Disk Self-Test': {
'Enabled': False,
'Function': disk_self_test,
'Objects': [],
},
'Disk Surface Scan': {
'Enabled': False,
'Function': disk_surface_scan,
'Objects': [],
},
'Disk I/O Benchmark': {
'Enabled': False,
'Function': disk_io_benchmark,
'Objects': [],
},
})
self.top_text = std.color_string('Hardware Diagnostics', 'GREEN') self.top_text = std.color_string('Hardware Diagnostics', 'GREEN')
# Init tmux and start a background process to maintain layout # Init tmux and start a background process to maintain layout
@ -142,8 +102,8 @@ class State():
def abort_testing(self): def abort_testing(self):
"""Set unfinished tests as aborted and cleanup tmux panes.""" """Set unfinished tests as aborted and cleanup tmux panes."""
for details in self.tests.values(): for group in self.test_groups:
for test in details['Objects']: for test in group.test_objects:
if test.status in ('Pending', 'Working'): if test.status in ('Pending', 'Working'):
test.set_status('Aborted') test.set_status('Aborted')
@ -269,8 +229,7 @@ class State():
self.disks.clear() self.disks.clear()
self.layout.clear() self.layout.clear()
self.layout.update(cfg.hw.TMUX_LAYOUT) self.layout.update(cfg.hw.TMUX_LAYOUT)
for test_data in self.tests.values(): self.test_groups.clear()
test_data['Objects'].clear()
# Set log # Set log
self.log_dir = log.format_log_path() self.log_dir = log.format_log_path()
@ -300,9 +259,10 @@ class State():
# Add test objects # Add test objects
for name, details in menu.options.items(): for name, details in menu.options.items():
self.tests[name]['Enabled'] = details['Selected']
if not details['Selected']: if not details['Selected']:
# Only add selected options
continue continue
if 'CPU' in name: if 'CPU' in name:
# Create two Test objects which will both be used by cpu_stress_tests # Create two Test objects which will both be used by cpu_stress_tests
# NOTE: Prime95 should be added first # NOTE: Prime95 should be added first
@ -312,12 +272,23 @@ class State():
self.system.tests.append( self.system.tests.append(
Test(dev=self.system, label='Cooling', name=name), Test(dev=self.system, label='Cooling', name=name),
) )
self.tests[name]['Objects'].extend(self.system.tests) self.test_groups.append(
elif 'Disk' in name: TestGroup(
name=name,
function=globals()[TEST_GROUPS[name]],
test_objects=self.system.tests,
),
)
if 'Disk' in name:
test_group = TestGroup(
name=name, function=globals()[TEST_GROUPS[name]],
)
for disk in self.disks: for disk in self.disks:
test_obj = Test(dev=disk, label=disk.path.name, name=name) test_obj = Test(dev=disk, label=disk.path.name, name=name)
disk.tests.append(test_obj) disk.tests.append(test_obj)
self.tests[name]['Objects'].append(test_obj) test_group.test_objects.append(test_obj)
self.test_groups.append(test_group)
# Run safety checks # Run safety checks
self.disk_safety_checks(prep=True) self.disk_safety_checks(prep=True)
@ -413,16 +384,12 @@ class State():
report = [] report = []
width = cfg.hw.TMUX_SIDE_WIDTH width = cfg.hw.TMUX_SIDE_WIDTH
for name, details in self.tests.items(): for group in self.test_groups:
if not details['Enabled']: report.append(std.color_string(group.name, 'BLUE'))
continue for test in group.test_objects:
# Add test details
report.append(std.color_string(name, 'BLUE'))
for test_obj in details['Objects']:
report.append(std.color_string( report.append(std.color_string(
[test_obj.label, f'{test_obj.status:>{width-len(test_obj.label)}}'], [test.label, f'{test.status:>{width-len(test.label)}}'],
[None, STATUS_COLORS.get(test_obj.status, None)], [None, STATUS_COLORS.get(test.status, None)],
sep='', sep='',
)) ))
@ -471,7 +438,7 @@ def build_menu(cli_mode=False, quick_mode=False):
menu.add_action(action) menu.add_action(action)
for action in MENU_ACTIONS_SECRET: for action in MENU_ACTIONS_SECRET:
menu.add_action(action, {'Hidden': True}) menu.add_action(action, {'Hidden': True})
for option in MENU_OPTIONS: for option in TEST_GROUPS:
menu.add_option(option, {'Selected': True}) menu.add_option(option, {'Selected': True})
for toggle in MENU_TOGGLES: for toggle in MENU_TOGGLES:
menu.add_toggle(toggle, {'Selected': True}) menu.add_toggle(toggle, {'Selected': True})
@ -1362,21 +1329,18 @@ def run_diags(state, menu, quick_mode=False):
state.init_diags(menu) state.init_diags(menu)
# Just return if no tests were selected # Just return if no tests were selected
if not any(details['Enabled'] for details in state.tests.values()): if not state.test_groups:
std.print_warning('No tests selected?') std.print_warning('No tests selected?')
std.pause() std.pause()
return return
# Run tests # Run tests
for name, details in state.tests.items(): for group in state.test_groups:
if not details['Enabled']:
# Skip disabled tests
continue
# Run test(s) # Run test(s)
function = details['Function'] function = group.function
args = [details['Objects']] args = [group.test_objects]
if name == 'Disk I/O Benchmark': if group.name == 'Disk I/O Benchmark':
args.append(menu.toggles['Skip USB Benchmarks']['Selected']) args.append(menu.toggles['Skip USB Benchmarks']['Selected'])
std.clear_screen() std.clear_screen()
try: try:
@ -1388,15 +1352,17 @@ def run_diags(state, menu, quick_mode=False):
break break
# Run safety checks # Run safety checks
if name.startswith('Disk'): if group.name.startswith('Disk'):
state.disk_safety_checks(wait_for_self_tests=name != 'Disk Attributes') state.disk_safety_checks(
wait_for_self_tests=group.name != 'Disk Attributes',
)
# Handle aborts # Handle aborts
if aborted: if aborted:
for details in state.tests.values(): for group in state.test_groups:
for test_obj in details['Objects']: for test in group.test_objects:
if test_obj.status == 'Pending': if test.status == 'Pending':
test_obj.set_status('Aborted') test.set_status('Aborted')
# Show results # Show results
show_results(state) show_results(state)
@ -1462,17 +1428,19 @@ def show_results(state):
state.update_top_pane('Results') state.update_top_pane('Results')
# CPU Tests # CPU Tests
cpu_tests_enabled = [data['Enabled'] for name, data in state.tests.items() cpu_tests_enabled = [
if name.startswith('CPU')] group.name for group in state.test_groups if 'CPU' in group.name
if any(cpu_tests_enabled): ]
if cpu_tests_enabled:
std.print_success('CPU:') std.print_success('CPU:')
std.print_report(state.system.generate_report()) std.print_report(state.system.generate_report())
std.print_standard(' ') std.print_standard(' ')
# Disk Tests # Disk Tests
disk_tests_enabled = [data['Enabled'] for name, data in state.tests.items() disk_tests_enabled = [
if name.startswith('Disk')] group.name for group in state.test_groups if 'Disk' in group.name
if any(disk_tests_enabled): ]
if disk_tests_enabled:
std.print_success(f'Disk{"s" if len(state.disks) > 1 else ""}:') std.print_success(f'Disk{"s" if len(state.disks) > 1 else ""}:')
for disk in state.disks: for disk in state.disks:
std.print_report(disk.generate_report()) std.print_report(disk.generate_report())

View file

@ -2,7 +2,7 @@
# vim: sts=2 sw=2 ts=2 # vim: sts=2 sw=2 ts=2
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Any from typing import Any, Callable
@dataclass(slots=True) @dataclass(slots=True)
class Test: class Test:
@ -25,3 +25,11 @@ class Test:
return return
self.status = status self.status = status
@dataclass(slots=True)
class TestGroup:
"""Object for tracking groups of tests."""
name: str
function: Callable
test_objects: list[Test] = field(default_factory=list)