import contextlib import io import json import subprocess import tempfile import unittest from pathlib import Path from unittest.mock import patch try: from hydra import doctor except ImportError: doctor = None class DoctorTests(unittest.TestCase): def setUp(self): self.assertIsNotNone(doctor, 'read-only doctor module is missing') self.tmp = tempfile.TemporaryDirectory() self.addCleanup(self.tmp.cleanup) self.root = Path(self.tmp.name) for name in ('proc', 'sys', 'dev'): (self.root / name).mkdir() def file(self, name, value=''): path = self.root / name path.parent.mkdir(parents=True, exist_ok=True) path.write_text(value) return path def report(self, **kwargs): return doctor.doctor_report(proc_root=self.root / 'proc', sys_root=self.root / 'sys', dev_root=self.root / 'dev', find_tool=lambda _: None, **kwargs) def test_absent_hardware_is_reported_without_false_functional_claims(self): result = self.report() self.assertEqual(result['firmware']['uefi'], False) self.assertIsNone(result['firmware']['secure_boot']) self.assertEqual(result['storage']['devices'], []) self.assertFalse(result['kvm']['device_present']) self.assertTrue(all(not value for value in result['tools'].values())) self.assertIn('Wi-Fi connection', ' '.join(result['manual_checks'])) self.assertIn('144 Hz', ' '.join(result['manual_checks'])) json.dumps(result) def test_fake_hardware_reports_presence_and_driver_without_identifiers(self): self.file('proc/cpuinfo', 'processor : 0\nflags : vmx sse\nSerial : PRIVATE_SERIAL\n') self.file('proc/meminfo', 'MemTotal: 8388608 kB\nMemAvailable: 4194304 kB\n') secure = self.file('sys/firmware/efi/efivars/SecureBoot-8be4df61-93ca-11d2-aa0d-00e098032b8c') secure.write_bytes(b'\x07\0\0\0\x01') self.file('dev/kvm') self.file('sys/module/kvm_intel/refcnt', '0') self.file('sys/class/block/nvme0n1/size', '2097152') self.file('sys/class/block/nvme0n1/device/serial', 'PRIVATE_SERIAL') self.file('sys/class/block/nvme0n1p1/partition', '1') self.file('sys/class/drm/card0-eDP-1/status', 'connected') self.file('sys/class/drm/card0-eDP-1/modes', '1920x1080\nPRIVATE_USER\n') self.file('sys/bus/pci/devices/0000:00:02.0/class', '0x030000') driver = self.root / 'sys/bus/pci/drivers/i915' driver.mkdir(parents=True) (self.root / 'sys/bus/pci/devices/0000:00:02.0/driver').symlink_to(driver) self.file('sys/class/net/wlan0/wireless/present') self.file('sys/class/net/wlan0/address', 'aa:bb:cc:dd:ee:ff') self.file('sys/class/net/wlan0/ssid', 'PRIVATE_SSID') self.file('sys/class/sound/card0/id', 'PRIVATE_USER') self.file('sys/class/input/event0/device/name', 'PRIVATE_USER') self.file('sys/class/input/event0/device/capabilities/key', hex((1 << 30) | (1 << 44))[2:]) result = self.report() self.assertEqual(result['cpu']['virtualization_flags'], ['vmx']) self.assertEqual(result['memory']['total_mib'], 8192) self.assertEqual(result['firmware']['secure_boot'], True) self.assertEqual(result['storage']['devices'][0]['size_bytes'], 1073741824) self.assertEqual(result['graphics']['drivers'], ['i915']) self.assertEqual(result['network']['wireless_devices'], 1) self.assertEqual(result['audio']['cards'], 1) self.assertEqual(result['input']['keyboards'], 1) encoded = json.dumps(result) for secret in ('PRIVATE_SERIAL', 'PRIVATE_USER', 'PRIVATE_SSID', 'aa:bb:cc:dd:ee:ff'): self.assertNotIn(secret, encoded) def test_probes_timeout_independently_and_discard_untrusted_output(self): calls = [] def run(command, **kwargs): calls.append((command, kwargs)) if len(calls) == 1: raise subprocess.TimeoutExpired(command, kwargs['timeout'], output='PRIVATE_SSID') return subprocess.CompletedProcess(command, 5, 'PRIVATE_USER 192.0.2.1 aa:bb:cc:dd:ee:ff', '') result = doctor.doctor_report(proc_root=self.root / 'proc', sys_root=self.root / 'sys', dev_root=self.root / 'dev', find_tool=lambda name: '/usr/bin/' + name, run=run) self.assertIn('timeout', result['services'].values()) self.assertIn('unknown', result['services'].values()) self.assertNotIn('PRIVATE', json.dumps(result)) self.assertLessEqual(len(calls), 8) for argv, options in calls: self.assertEqual(argv[0], '/usr/bin/systemctl') self.assertIn('is-active', argv) self.assertLessEqual(options['timeout'], 1) self.assertFalse(options.get('shell', False)) def test_reads_are_bounded_and_invalid_numbers_are_unknown(self): self.file('proc/meminfo', 'MemTotal: invalid kB\n' + 'secret' * 100000) self.file('sys/class/block/sda/size', '-1') self.file('sys/class/drm/card0-HDMI-A-1/status', 'private text') result = self.report() self.assertIsNone(result['memory']['total_mib']) self.assertIsNone(result['storage']['devices'][0]['size_bytes']) self.assertEqual(result['graphics']['connectors'][0]['status'], 'unknown') self.assertLess(len(json.dumps(result)), 10000) def test_text_and_json_outputs_share_report_and_manual_limits(self): with patch.object(doctor, 'doctor_report', return_value=self.report()): output = io.StringIO() with contextlib.redirect_stdout(output): self.assertEqual(doctor.doctor_main(['--json']), 0) self.assertEqual(json.loads(output.getvalue())['firmware']['uefi'], False) output = io.StringIO() with contextlib.redirect_stdout(output): self.assertEqual(doctor.doctor_main([]), 0) self.assertIn('Manual checks', output.getvalue()) self.assertIn('suspend', output.getvalue())