feat: add ProcessDock process and port manager

This commit is contained in:
王鹏
2026-07-17 09:14:45 +08:00
commit 13bb37585f
22 changed files with 2201 additions and 0 deletions

66
tests/test_gui.py Normal file
View File

@@ -0,0 +1,66 @@
from __future__ import annotations
import os
import unittest
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PySide6.QtWidgets import QApplication # noqa: E402
from processdock.ui import MainWindow # noqa: E402
class StubService:
def snapshot(self):
return {"processes": [], "summary": {}, "warnings": [], "refreshed_at": "00:00:00"}
class GuiSmokeTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.application = QApplication.instance() or QApplication([])
def setUp(self) -> None:
self.window = MainWindow(service=StubService(), auto_refresh=False)
def tearDown(self) -> None:
self.window.close()
self.application.processEvents()
def test_window_contains_table_search_and_detail_panel(self) -> None:
self.assertEqual(self.window.model.columnCount(), 8)
self.assertIn("端口", self.window.search_box.placeholderText())
self.assertEqual(self.window.detail_panel.stack.currentIndex(), 0)
def test_filter_finds_port(self) -> None:
process = {
"pid": 4420,
"name": "node.exe",
"ports": [3000],
"port_details": [{"protocol": "TCP", "address": "0.0.0.0", "port": 3000}],
"cpu_percent": 3.0,
"memory_bytes": 188743680,
"memory_text": "180.0 MB",
"memory_percent": 1.0,
"username": "wang",
"status": "running",
"status_label": "运行中",
"exe": r"D:\node\node.exe",
"command": "node server.js",
"create_time": 1.0,
"create_time_text": "2026-07-17 08:20:11",
"ppid": 100,
"access_limited": False,
"is_current": False,
"can_terminate": True,
"can_open_location": True,
}
self.window.model.set_processes([process])
self.window.search_box.setText("3000")
self.assertEqual(self.window.proxy.rowCount(), 1)
self.window.search_box.setText("8080")
self.assertEqual(self.window.proxy.rowCount(), 0)
if __name__ == "__main__":
unittest.main()

85
tests/test_services.py Normal file
View File

@@ -0,0 +1,85 @@
from __future__ import annotations
import os
import subprocess
import sys
import unittest
from processdock.services import (
ProcessService,
ProtectedProcessError,
format_command,
humanize_bytes,
matches_process,
)
class FormattingTests(unittest.TestCase):
def test_humanize_bytes(self) -> None:
self.assertEqual(humanize_bytes(0), "0 B")
self.assertEqual(humanize_bytes(1024), "1.0 KB")
self.assertEqual(humanize_bytes(512 * 1024 * 1024), "512.0 MB")
def test_format_command_preserves_arguments(self) -> None:
command = format_command(["python", "demo app.py", "--port", "8080"])
self.assertIn("python", command)
self.assertIn("demo app.py", command)
self.assertIn("8080", command)
def test_searches_name_pid_port_path_and_command(self) -> None:
process = {
"pid": 9824,
"name": "java.exe",
"username": "wang",
"exe": r"D:\jdk-17\bin\java.exe",
"command": "java -jar demo.jar",
"ports": [8080, 9000],
}
for query in ("JAVA", "9824", "8080", "jdk-17", "demo.jar", "wang"):
with self.subTest(query=query):
self.assertTrue(matches_process(process, query))
self.assertFalse(matches_process(process, "nginx"))
class LiveServiceTests(unittest.TestCase):
def test_snapshot_contains_current_process_and_required_fields(self) -> None:
snapshot = ProcessService().snapshot()
current = next(item for item in snapshot["processes"] if item["pid"] == os.getpid())
required = {
"pid",
"name",
"ports",
"cpu_percent",
"memory_bytes",
"username",
"status_label",
"exe",
"command",
"create_time_text",
"ppid",
}
self.assertTrue(required.issubset(current))
self.assertIn("process_count", snapshot["summary"])
def test_service_refuses_to_terminate_itself(self) -> None:
service = ProcessService()
with self.assertRaises(ProtectedProcessError):
service.terminate(os.getpid(), force=True)
def test_force_terminates_owned_child_process(self) -> None:
child = subprocess.Popen(
[sys.executable, "-c", "import time; time.sleep(30)"],
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
)
try:
result = ProcessService().terminate(child.pid, force=True)
self.assertTrue(result.success)
self.assertFalse(result.still_running)
finally:
if child.poll() is None:
child.kill()
child.wait()
if __name__ == "__main__":
unittest.main()