Skip to content

Commit 68fa1f8

Browse files
author
Lukas Geiger
committed
test: add linux platform smoke
1 parent ff80fce commit 68fa1f8

2 files changed

Lines changed: 232 additions & 0 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: linux-platform-smoke
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
9+
jobs:
10+
smoke:
11+
runs-on: ubuntu-latest
12+
env:
13+
PYTEST_DISABLE_PLUGIN_AUTOLOAD: "1"
14+
QT_QPA_PLATFORM: offscreen
15+
PYTHONIOENCODING: utf-8
16+
steps:
17+
- name: Checkout
18+
uses: actions/checkout@v4
19+
20+
- name: Setup Python
21+
uses: actions/setup-python@v5
22+
with:
23+
python-version: "3.12"
24+
25+
- name: Install dependencies
26+
run: |
27+
python -m pip install --upgrade pip
28+
python -m pip install -r requirements.txt pytest
29+
30+
- name: Run desktop regressions
31+
run: |
32+
python -m pytest -q tests/test_lsp_client.py tests/test_save_failure_guards.py tests/test_tab_reordering.py
33+
34+
- name: Run Linux platform smoke
35+
run: python tests/linux_platform_smoke.py

tests/linux_platform_smoke.py

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
"""Reproduzierbarer Linux-Plattform-Smoke für CodeBox.
4+
5+
Der Smoke deckt den aktuell offenen Linux-Desktop-Pfad ab:
6+
- offscreen Start des PySide6-Hauptfensters
7+
- Dateiöffnen mit echten Umlauten ohne LSP-Zwang
8+
- Linux-Pfad für Projektbaum/`xdg-open`
9+
- Linux-Terminalpfad mit `bash`
10+
- lokale Run-Command-Auslösung für Python-Dateien
11+
"""
12+
13+
from __future__ import annotations
14+
15+
import os
16+
import sys
17+
import tempfile
18+
from pathlib import Path
19+
from unittest import mock
20+
21+
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
22+
23+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
24+
sys.path.insert(0, str(PROJECT_ROOT))
25+
26+
from PySide6.QtWidgets import QApplication
27+
28+
from features.project_view import ProjectView
29+
from features.terminal import TerminalWidget
30+
import features.terminal as terminal_module
31+
from ui.main_window import MainWindow
32+
from version import format_window_title
33+
34+
35+
class SmokeFailure(RuntimeError):
36+
pass
37+
38+
39+
def _assert(condition: bool, message: str) -> None:
40+
if not condition:
41+
raise SmokeFailure(message)
42+
43+
44+
def _ensure_app() -> QApplication:
45+
return QApplication.instance() or QApplication([])
46+
47+
48+
class _DummySignal:
49+
def __init__(self) -> None:
50+
self._callbacks = []
51+
52+
def connect(self, callback) -> None:
53+
self._callbacks.append(callback)
54+
55+
56+
class _FakeQProcess:
57+
class ProcessState:
58+
NotRunning = "not_running"
59+
Running = "running"
60+
61+
def __init__(self, parent=None) -> None:
62+
self.parent = parent
63+
self.cwd = None
64+
self.start_args = None
65+
self.writes: list[bytes] = []
66+
self._state = self.ProcessState.NotRunning
67+
self.readyReadStandardOutput = _DummySignal()
68+
self.readyReadStandardError = _DummySignal()
69+
self.finished = _DummySignal()
70+
71+
def setWorkingDirectory(self, cwd: str) -> None:
72+
self.cwd = cwd
73+
74+
def start(self, program: str, args: list[str]) -> None:
75+
self.start_args = (program, args)
76+
self._state = self.ProcessState.Running
77+
78+
def write(self, data: bytes) -> None:
79+
self.writes.append(data)
80+
81+
def state(self):
82+
return self._state
83+
84+
def kill(self) -> None:
85+
self._state = self.ProcessState.NotRunning
86+
87+
def waitForFinished(self, _timeout: int) -> bool:
88+
self._state = self.ProcessState.NotRunning
89+
return True
90+
91+
92+
def _exercise_window_open_and_run() -> None:
93+
print("Test 1: Offscreen-Hauptfenster öffnet Datei und löst Run-Command aus")
94+
app = _ensure_app()
95+
with tempfile.TemporaryDirectory(prefix="codebox-linux-window-") as tmpdir_str:
96+
tmpdir = Path(tmpdir_str)
97+
project_dir = tmpdir / "Projekt Übersicht"
98+
project_dir.mkdir(parents=True)
99+
script_path = project_dir / "überblick.py"
100+
script_path.write_text("print('Grüße aus Linux')\n", encoding="utf-8")
101+
102+
with mock.patch("features.terminal.TerminalWidget._start_shell", lambda self: None):
103+
window = MainWindow()
104+
try:
105+
window._lsp_manager.get_client = lambda _language: None
106+
window.show()
107+
app.processEvents()
108+
109+
_assert(window.windowTitle() == format_window_title(), window.windowTitle())
110+
opened_tab = window.open_path(script_path)
111+
app.processEvents()
112+
113+
_assert(opened_tab is not None, "Datei wurde nicht geöffnet.")
114+
_assert(opened_tab.editor.toPlainText() == "print('Grüße aus Linux')\n", "Editorinhalt stimmt nicht.")
115+
_assert(window.windowTitle() == format_window_title(script_path), window.windowTitle())
116+
_assert(window.lang_label.text() == "Python", window.lang_label.text())
117+
_assert(window.project_view._root_path == project_dir, window.project_view._root_path)
118+
_assert(window.terminal.cwd_label.text() == str(project_dir), window.terminal.cwd_label.text())
119+
_assert(window.output.run_btn.isEnabled(), "Run-Button blieb deaktiviert.")
120+
121+
captured: list[list[str]] = []
122+
window.output.run_command = lambda command: captured.append(command)
123+
window.run_current()
124+
125+
_assert(captured == [["python", "-u", str(script_path)]], repr(captured))
126+
finally:
127+
window.close()
128+
app.processEvents()
129+
print("PASS: Dateiöffnung, Umlaute und Run-Command funktionieren\n")
130+
131+
132+
def _exercise_linux_reveal_in_explorer() -> None:
133+
print("Test 2: Projektbaum verwendet unter Linux xdg-open")
134+
with tempfile.TemporaryDirectory(prefix="codebox-linux-explorer-") as tmpdir_str:
135+
tmpdir = Path(tmpdir_str)
136+
project_dir = tmpdir / "Projekt Übersicht"
137+
project_dir.mkdir(parents=True)
138+
file_path = project_dir / "überblick.py"
139+
file_path.write_text("print('ok')\n", encoding="utf-8")
140+
141+
panel = ProjectView()
142+
with mock.patch.object(sys, "platform", "linux"), mock.patch("subprocess.Popen") as popen_mock:
143+
panel._reveal_in_explorer(file_path)
144+
145+
_assert(
146+
popen_mock.call_args.args[0] == ["xdg-open", str(project_dir)],
147+
repr(popen_mock.call_args),
148+
)
149+
print("PASS: Linux-Dateimanagerpfad ist korrekt\n")
150+
151+
152+
def _exercise_linux_terminal() -> None:
153+
print("Test 3: Terminal nutzt unter Linux bash und cd")
154+
app = _ensure_app()
155+
with tempfile.TemporaryDirectory(prefix="codebox-linux-terminal-") as tmpdir_str:
156+
tmpdir = Path(tmpdir_str)
157+
start_dir = tmpdir / "Start Ä"
158+
next_dir = tmpdir / "Ziel Ö"
159+
start_dir.mkdir(parents=True)
160+
next_dir.mkdir(parents=True)
161+
162+
with mock.patch.object(terminal_module, "QProcess", _FakeQProcess), mock.patch.object(
163+
terminal_module.sys,
164+
"platform",
165+
"linux",
166+
):
167+
widget = TerminalWidget(working_dir=str(start_dir))
168+
try:
169+
app.processEvents()
170+
items = [widget.shell_combo.itemText(i) for i in range(widget.shell_combo.count())]
171+
_assert(items == ["bash", "zsh", "sh"], repr(items))
172+
_assert(widget.process.start_args == ("bash", ["--norc"]), repr(widget.process.start_args))
173+
_assert(widget.process.cwd == str(start_dir), widget.process.cwd)
174+
175+
widget.set_working_dir(str(next_dir))
176+
_assert(widget.cwd_label.text() == str(next_dir), widget.cwd_label.text())
177+
_assert(
178+
widget.process.writes[-1] == f'cd "{next_dir}"\n'.encode("utf-8"),
179+
repr(widget.process.writes),
180+
)
181+
finally:
182+
widget.close()
183+
app.processEvents()
184+
print("PASS: Linux-Terminalpfad ist korrekt\n")
185+
186+
187+
def main() -> int:
188+
print("=== CodeBox Linux Platform Smoke ===\n")
189+
_exercise_window_open_and_run()
190+
_exercise_linux_reveal_in_explorer()
191+
_exercise_linux_terminal()
192+
print("=== ALL TESTS PASSED ===")
193+
return 0
194+
195+
196+
if __name__ == "__main__":
197+
raise SystemExit(main())

0 commit comments

Comments
 (0)