mirror of
https://github.com/lucannez64/darling-lol.git
synced 2026-10-26 19:47:42 +00:00
Turns "it does not work" into an attachable report instead of a log-less issue. Three read-only host tools, reachable from the launcher: - doctor.py checks GPU/driver, Vulkan ICDs, display session, memory, disk, overlay layer, core-dump limit, i915 panel self-refresh and prefix/runtime state. --json and --report produce an issue attachment; warnings do not fail. - crash-report.py bundles the doctor checks, process table, dserver and launcher log tails, graphics kernel messages, display state and recent core dumps. Environment is never collected and home/user/host are redacted. - perf-baseline.py parses the game's own netstats.csv into average FPS, p50/p95/p99 frame time, slowest-quarter FPS and hitch count, with --json and --append for before/after comparisons. COMPATIBILITY.md records the verified UHD 620 row with measured numbers (24-32 avg FPS, p99 44-98 ms) and explains how to submit another one.
216 lines
10 KiB
Python
216 lines
10 KiB
Python
"""Host checks used by `darling-lol --doctor`; never starts the client or runtime."""
|
|
import contextlib
|
|
import importlib.util
|
|
import io
|
|
import json
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SCRIPT = ROOT / 'tools/doctor.py'
|
|
SPEC = importlib.util.spec_from_file_location('doctor', SCRIPT)
|
|
doctor = importlib.util.module_from_spec(SPEC)
|
|
SPEC.loader.exec_module(doctor)
|
|
|
|
GIB = 1024 ** 3
|
|
|
|
|
|
def facts(**overrides):
|
|
base = {
|
|
'host': {'distro': 'Test', 'kernel': '6.8.0', 'arch': 'x86_64', 'python': '3.12.0'},
|
|
'session': {'type': 'wayland', 'desktop': 'Hyprland', 'display': ':0', 'wayland_display': 'wayland-1'},
|
|
'memory': {'total': 16 * GIB, 'swap': 8 * GIB, 'total_gib': 16.0, 'swap_gib': 8.0},
|
|
'disk': {'free': 100 * GIB, 'free_gib': 100.0},
|
|
'gpus': [{'card': 'card0', 'driver': 'i915', 'vendor_id': '0x8086',
|
|
'device_id': '0x9b41', 'name': 'UHD 620'}],
|
|
'vulkan_icds': ['/usr/share/vulkan/icd.d/intel_icd.json'],
|
|
'prefix': '/tmp/prefix',
|
|
'prefix_exists': True,
|
|
'prefix_launchd': True,
|
|
'prefix_socket': True,
|
|
'prefix_state': {'pid': 0, 'state': 'absent', 'stale_markers': 0},
|
|
'psr': '0',
|
|
'core_limit': '0',
|
|
'environment': {},
|
|
'tools': {},
|
|
}
|
|
base.update(overrides)
|
|
return base
|
|
|
|
|
|
class CheckTests(unittest.TestCase):
|
|
def test_kernel_rejects_old_release(self):
|
|
self.assertEqual(doctor.check_kernel(facts(host={'kernel': '5.4.0'}))[0], 'warn')
|
|
self.assertEqual(doctor.check_kernel(facts(host={'kernel': '6.8.0-arch1'}))[0], 'ok')
|
|
self.assertEqual(doctor.check_kernel(facts(host={'kernel': 'unparsed'}))[0], 'warn')
|
|
|
|
def test_memory_requires_minimum(self):
|
|
low = facts(memory={'total': 2 * GIB, 'swap': 0, 'total_gib': 2.0, 'swap_gib': 0.0})
|
|
tight = facts(memory={'total': 6 * GIB, 'swap': 0, 'total_gib': 6.0, 'swap_gib': 0.0})
|
|
self.assertEqual(doctor.check_memory(low)[0], 'fail')
|
|
self.assertEqual(doctor.check_memory(tight)[0], 'warn')
|
|
self.assertEqual(doctor.check_memory(facts())[0], 'ok')
|
|
unknown = facts(memory={'total': 0, 'swap': 0, 'total_gib': 0.0, 'swap_gib': 0.0})
|
|
self.assertEqual(doctor.check_memory(unknown)[0], 'ok')
|
|
|
|
def test_disk_space(self):
|
|
self.assertEqual(doctor.check_disk(facts(disk={'free': 1 * GIB, 'free_gib': 1.0}))[0], 'fail')
|
|
self.assertEqual(doctor.check_disk(facts(disk={'free': 10 * GIB, 'free_gib': 10.0}))[0], 'warn')
|
|
self.assertEqual(doctor.check_disk(facts())[0], 'ok')
|
|
|
|
def test_gpu_driver_reports_card(self):
|
|
status, message = doctor.check_gpu(facts())
|
|
self.assertEqual(status, 'ok')
|
|
self.assertIn('UHD 620 (i915)', message)
|
|
self.assertEqual(doctor.check_gpu(facts(gpus=[]))[0], 'fail')
|
|
nouveau = facts(gpus=[{'card': 'card0', 'driver': 'nouveau', 'vendor_id': '0x10de',
|
|
'device_id': '0x1f91', 'name': 'GTX 1650'}])
|
|
self.assertEqual(doctor.check_gpu(nouveau)[0], 'warn')
|
|
|
|
def test_vulkan_icds(self):
|
|
self.assertEqual(doctor.check_vulkan(facts())[0], 'ok')
|
|
status, message = doctor.check_vulkan(facts(vulkan_icds=[]))
|
|
self.assertEqual(status, 'fail')
|
|
self.assertIn('Mesa', message)
|
|
|
|
def test_session_needs_display_on_wayland(self):
|
|
self.assertEqual(doctor.check_session(facts())[0], 'ok')
|
|
missing = facts(session={'type': 'wayland', 'desktop': '', 'display': '', 'wayland_display': 'wayland-1'})
|
|
self.assertEqual(doctor.check_session(missing)[0], 'fail')
|
|
x11 = facts(session={'type': 'x11', 'desktop': 'i3', 'display': ':1', 'wayland_display': ''})
|
|
self.assertEqual(doctor.check_session(x11)[0], 'ok')
|
|
bare = facts(session={'type': '', 'desktop': '', 'display': '', 'wayland_display': ''})
|
|
self.assertEqual(doctor.check_session(bare)[0], 'warn')
|
|
|
|
def test_mangohud_is_a_failure(self):
|
|
self.assertEqual(doctor.check_hud(facts(environment={'MANGOHUD': '1'}))[0], 'fail')
|
|
self.assertEqual(doctor.check_hud(facts(environment={'DISABLE_MANGOHUD': '1'}))[0], 'ok')
|
|
self.assertEqual(doctor.check_hud(facts())[0], 'ok')
|
|
|
|
def test_core_dumps_and_psr(self):
|
|
self.assertEqual(doctor.check_core_dumps(facts(core_limit='unlimited'))[0], 'warn')
|
|
self.assertEqual(doctor.check_core_dumps(facts(core_limit='0'))[0], 'ok')
|
|
self.assertEqual(doctor.check_psr(facts(psr='1'))[0], 'warn')
|
|
self.assertEqual(doctor.check_psr(facts(psr='0'))[0], 'ok')
|
|
|
|
def test_prefix_must_be_installed(self):
|
|
self.assertEqual(doctor.check_prefix(facts(prefix_exists=False))[0], 'fail')
|
|
self.assertEqual(doctor.check_prefix(facts(prefix_launchd=False))[0], 'fail')
|
|
self.assertEqual(doctor.check_prefix(facts())[0], 'ok')
|
|
|
|
def test_runtime_states(self):
|
|
self.assertEqual(doctor.check_runtime(facts(prefix_state={'pid': 42, 'state': 'running',
|
|
'stale_markers': 0}))[0], 'ok')
|
|
self.assertEqual(doctor.check_runtime(facts(prefix_state={'pid': 42, 'state': 'stale',
|
|
'stale_markers': 0}))[0], 'warn')
|
|
self.assertEqual(doctor.check_runtime(facts(prefix_state={'pid': 0, 'state': 'absent',
|
|
'stale_markers': 0}))[0], 'ok')
|
|
|
|
def test_stale_marker_count(self):
|
|
self.assertEqual(doctor.check_stale_markers(
|
|
facts(prefix_state={'pid': 0, 'state': 'absent', 'stale_markers': 2}))[0], 'ok')
|
|
self.assertEqual(doctor.check_stale_markers(
|
|
facts(prefix_state={'pid': 0, 'state': 'absent', 'stale_markers': 10}))[0], 'warn')
|
|
|
|
|
|
class PrefixStateTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.folder = tempfile.TemporaryDirectory()
|
|
self.root = Path(self.folder.name)
|
|
self.prefix = self.root / 'prefix'
|
|
self.prefix.mkdir()
|
|
self.proc = self.root / 'proc'
|
|
self.proc.mkdir()
|
|
|
|
def tearDown(self):
|
|
self.folder.cleanup()
|
|
|
|
def test_absent(self):
|
|
self.assertEqual(doctor.prefix_state(self.prefix, self.proc)['state'], 'absent')
|
|
|
|
def test_running(self):
|
|
(self.prefix / '.init.pid').write_text('123\n')
|
|
status = self.proc / '123'
|
|
status.mkdir()
|
|
(status / 'status').write_text('Name:\tdarlingserver\nState:\tS (sleeping)\n')
|
|
self.assertEqual(doctor.prefix_state(self.prefix, self.proc)['state'], 'running')
|
|
|
|
def test_zombie_and_missing_pid_are_stale(self):
|
|
(self.prefix / '.init.pid').write_text('123\n')
|
|
status = self.proc / '123'
|
|
status.mkdir()
|
|
(status / 'status').write_text('Name:\tdarlingserver\nState:\tZ (zombie)\n')
|
|
self.assertEqual(doctor.prefix_state(self.prefix, self.proc)['state'], 'stale')
|
|
(status / 'status').unlink()
|
|
status.rmdir()
|
|
self.assertEqual(doctor.prefix_state(self.prefix, self.proc)['state'], 'stale')
|
|
|
|
def test_stale_markers_are_counted(self):
|
|
(self.prefix / '.init.pid.stale-1-2').write_text('1')
|
|
(self.prefix / '.init.pid.stale-3-4').write_text('3')
|
|
self.assertEqual(doctor.prefix_state(self.prefix, self.proc)['stale_markers'], 2)
|
|
|
|
|
|
class GpuAndVulkanTests(unittest.TestCase):
|
|
def test_read_gpus_follows_driver_link(self):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
sys_root = Path(folder)
|
|
card = sys_root / 'class/drm/card0/device'
|
|
card.mkdir(parents=True)
|
|
(card / 'vendor').write_text('0x8086\n')
|
|
(card / 'device').write_text('0x9b41\n')
|
|
(card / 'product_name').write_text('Intel UHD 620\n')
|
|
drivers = sys_root / 'drivers/i915'
|
|
drivers.mkdir(parents=True)
|
|
(card / 'driver').symlink_to(drivers)
|
|
gpus = doctor.read_gpus(sys_root)
|
|
self.assertEqual(gpus, [{'card': 'card0', 'driver': 'i915', 'vendor_id': '0x8086',
|
|
'device_id': '0x9b41', 'name': 'Intel UHD 620'}])
|
|
|
|
def test_vulkan_icds_deduplicate(self):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
icd = Path(folder) / 'vulkan/icd.d'
|
|
icd.mkdir(parents=True)
|
|
(icd / 'intel.json').write_text('{}')
|
|
found = doctor.vulkan_icds({'XDG_DATA_DIRS': folder})
|
|
self.assertIn(str(icd / 'intel.json'), found)
|
|
self.assertEqual(len(found), len(set(found)))
|
|
|
|
|
|
class ReportTests(unittest.TestCase):
|
|
def test_run_checks_isolates_a_broken_probe(self):
|
|
results = doctor.run_checks({'host': {}})
|
|
by_id = {result['id']: result for result in results}
|
|
self.assertEqual(len(results), len(doctor.CHECKS))
|
|
self.assertEqual(by_id['kernel']['status'], 'warn')
|
|
self.assertIn('could not run', by_id['kernel']['message'])
|
|
|
|
def test_summary_counts(self):
|
|
results = [{'status': 'ok'}, {'status': 'ok'}, {'status': 'warn'}, {'status': 'fail'}]
|
|
self.assertEqual(doctor.summarize(results), {'ok': 2, 'warn': 1, 'fail': 1})
|
|
|
|
def test_report_has_no_environment_values(self):
|
|
data = facts(environment={'DISPLAY': ':0', 'RCLOL_REGION': 'EUW'})
|
|
document = doctor.report(data, doctor.run_checks(data))
|
|
self.assertEqual(document['schema'], doctor.SCHEMA)
|
|
self.assertEqual(document['environment'], {'DISPLAY': ':0', 'RCLOL_REGION': 'EUW'})
|
|
self.assertNotIn('MANGOHUD', document['environment'])
|
|
|
|
def test_cli_writes_json(self):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
prefix = Path(folder) / 'prefix'
|
|
prefix.mkdir()
|
|
out = Path(folder) / 'doctor.json'
|
|
stream = io.StringIO()
|
|
with contextlib.redirect_stdout(stream):
|
|
code = doctor.main(['--prefix', str(prefix), '--report', str(out)])
|
|
self.assertIn(code, (0, 1))
|
|
document = json.loads(out.read_text())
|
|
self.assertEqual(document['tool'], 'darling-lol doctor')
|
|
self.assertIn('checks', document)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|