feat: add ProcessDock process and port manager
This commit is contained in:
66
tests/test_gui.py
Normal file
66
tests/test_gui.py
Normal 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
85
tests/test_services.py
Normal 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()
|
||||
Reference in New Issue
Block a user