Skip to content

Commit 8ebf0fe

Browse files
Lukas Geigerclaude
andcommitted
feat: add macOS/Linux source-platform smoke and CI workflow
- tests/source_platform_smoke.py: 6-check headless smoke (Non-GUI imports, PySide6, profile roundtrip with umlauts, decode_header_str, dict roundtrip, MainWindow offscreen start); local 6/6 passing - .github/workflows/source-platform-smoke.yml: GitHub Actions CI on ubuntu-latest and macos-latest, PySide6-only install, offscreen Qt - .gitignore: add docs/superpowers/ exclusion for agent planning artifacts - CHANGELOG.md: document smoke in [Unreleased] Added Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent cee1f02 commit 8ebf0fe

4 files changed

Lines changed: 178 additions & 0 deletions

File tree

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: UniversalMailCleaner source-platform smoke
2+
3+
on:
4+
push:
5+
branches: [main, master]
6+
paths:
7+
- "mail_imap_cleaner_v1.py"
8+
- "tests/source_platform_smoke.py"
9+
pull_request:
10+
paths:
11+
- "mail_imap_cleaner_v1.py"
12+
- "tests/source_platform_smoke.py"
13+
workflow_dispatch:
14+
15+
jobs:
16+
smoke:
17+
name: Smoke (${{ matrix.os }})
18+
runs-on: ${{ matrix.os }}
19+
strategy:
20+
fail-fast: false
21+
matrix:
22+
os: [ubuntu-latest, macos-latest]
23+
24+
env:
25+
QT_QPA_PLATFORM: offscreen
26+
PYTHONIOENCODING: utf-8
27+
28+
steps:
29+
- uses: actions/checkout@v4
30+
31+
- name: Set up Python
32+
uses: actions/setup-python@v5
33+
with:
34+
python-version: "3.11"
35+
36+
- name: Install system Qt libs (Linux only)
37+
if: runner.os == 'Linux'
38+
run: |
39+
sudo apt-get update -qq
40+
sudo apt-get install -y --no-install-recommends \
41+
libgl1 libegl1 libglib2.0-0 libdbus-1-3 \
42+
libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 \
43+
libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 \
44+
libxcb-xinerama0 libxcb-xfixes0
45+
46+
- name: Install PySide6
47+
run: pip install "PySide6>=6.5.0"
48+
49+
- name: Run source-platform smoke
50+
run: python tests/source_platform_smoke.py

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ env/
1616
.env/
1717

1818
# ---- Interne Steuerungsdateien ----
19+
docs/superpowers/
1920
AUFGABEN.txt
2021
TEST.txt
2122
TESTS.txt

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.1.0/).
66
## [Unreleased]
77

88
### Added
9+
- Source-platform smoke (`tests/source_platform_smoke.py`) for macOS and Linux with
10+
GitHub Actions CI on ubuntu-latest and macos-latest (PySide6 offscreen, 6 checks)
911
- Gmail cleanup rules now run against Gmail API accounts in addition to IMAP accounts
1012
- Large-mail scan, delete, and undo now work for Gmail API accounts
1113
- Large-item scan now optionally includes Google Drive files for Gmail API accounts

tests/source_platform_smoke.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
"""
2+
Source-platform smoke for UniversalMailCleaner.
3+
Runs headless on macOS and Linux (QT_QPA_PLATFORM=offscreen).
4+
Exit 0 = all checks passed, Exit 1 = at least one check failed.
5+
"""
6+
import sys
7+
import os
8+
import tempfile
9+
from pathlib import Path
10+
11+
# Ensure project root is on sys.path so modules resolve without install
12+
ROOT = Path(__file__).resolve().parent.parent
13+
sys.path.insert(0, str(ROOT))
14+
15+
PASS = "✓"
16+
FAIL = "✗"
17+
results: list[tuple[str, bool, str]] = []
18+
19+
20+
def check(name: str, ok: bool, detail: str = "") -> None:
21+
results.append((name, ok, detail))
22+
status = PASS if ok else FAIL
23+
line = f" [{status}] {name}"
24+
if detail:
25+
line += f": {detail}"
26+
print(line)
27+
28+
29+
def run_checks() -> None:
30+
# --- Check 1: Non-GUI module imports ---
31+
try:
32+
import models # noqa: F401
33+
import imap_client # noqa: F401
34+
import gmail_service # noqa: F401
35+
import profile_exchange # noqa: F401
36+
check("Non-GUI imports (models, imap_client, gmail_service, profile_exchange)", True)
37+
except Exception as exc:
38+
check("Non-GUI imports", False, str(exc))
39+
40+
# --- Check 2: PySide6 import ---
41+
try:
42+
from PySide6.QtWidgets import QApplication # noqa: F401
43+
from PySide6.QtCore import Qt # noqa: F401
44+
check("PySide6 import (QApplication, Qt)", True)
45+
except Exception as exc:
46+
check("PySide6 import", False, str(exc))
47+
48+
# --- Check 3: Profile write + read roundtrip with German umlauts ---
49+
try:
50+
from models import MailAccount, CleanRule
51+
from profile_exchange import write_profile, read_profile
52+
53+
account = MailAccount(name="Büro Köln", host="imap.example.de", user="müller@example.de")
54+
rule = CleanRule(name="Alte Ü-Mails", target_account="Büro Köln",
55+
filter_type="older_than_days", value="90")
56+
57+
with tempfile.NamedTemporaryFile(suffix=".json", delete=False, mode="w") as f:
58+
tmp_path = f.name
59+
60+
try:
61+
write_profile(tmp_path, [account], [rule], None)
62+
loaded_accounts, loaded_rules, _ = read_profile(tmp_path)
63+
assert loaded_accounts[0].name == "Büro Köln", f"name mismatch: {loaded_accounts[0].name!r}"
64+
assert loaded_rules[0].name == "Alte Ü-Mails", f"rule name mismatch: {loaded_rules[0].name!r}"
65+
raw = Path(tmp_path).read_text(encoding="utf-8")
66+
assert "Büro" in raw, "Umlaut not preserved as UTF-8"
67+
check("Profile write+read roundtrip (umlauts)", True)
68+
finally:
69+
Path(tmp_path).unlink(missing_ok=True)
70+
except Exception as exc:
71+
check("Profile write+read roundtrip", False, str(exc))
72+
73+
# --- Check 4: decode_header_str with encoded header ---
74+
try:
75+
from imap_client import decode_header_str
76+
encoded = "=?utf-8?b?UmVjaG51bmcgZsO8ciBLw7ZsbiBCw7xy?="
77+
result = decode_header_str(encoded)
78+
assert isinstance(result, str) and len(result) > 0, f"unexpected: {result!r}"
79+
check("decode_header_str (encoded header)", True, repr(result))
80+
except Exception as exc:
81+
check("decode_header_str", False, str(exc))
82+
83+
# --- Check 5: MailAccount + CleanRule dict roundtrip ---
84+
try:
85+
from models import MailAccount, CleanRule
86+
acc = MailAccount(name="Privat", host="imap.gmx.net", user="test@gmx.de", port=993)
87+
rule = CleanRule(name="Spam", target_account="Privat",
88+
filter_type="sender", value="spam@junk.de")
89+
acc2 = MailAccount.from_dict(acc.to_dict())
90+
rule2 = CleanRule.from_dict(rule.to_dict())
91+
assert acc2.name == acc.name and acc2.host == acc.host
92+
assert rule2.value == rule.value
93+
check("MailAccount + CleanRule dict roundtrip", True)
94+
except Exception as exc:
95+
check("MailAccount + CleanRule dict roundtrip", False, str(exc))
96+
97+
# --- Check 6: Headless MainWindow start ---
98+
try:
99+
from PySide6.QtWidgets import QApplication
100+
app = QApplication.instance() or QApplication(sys.argv[:1])
101+
import mail_imap_cleaner_v1 as m
102+
win = m.MainWindow()
103+
assert win is not None
104+
win.close()
105+
check("Headless MainWindow start (offscreen)", True)
106+
except Exception as exc:
107+
check("Headless MainWindow start", False, str(exc))
108+
109+
110+
def main() -> int:
111+
print("UniversalMailCleaner source-platform smoke")
112+
print(f" Python {sys.version}")
113+
print(f" Platform: {sys.platform}")
114+
print(f" QT_QPA_PLATFORM={os.environ.get('QT_QPA_PLATFORM', '(not set)')}")
115+
print()
116+
run_checks()
117+
print()
118+
passed = sum(1 for _, ok, _ in results if ok)
119+
total = len(results)
120+
print(f"Result: {passed}/{total} checks passed")
121+
return 0 if passed == total else 1
122+
123+
124+
if __name__ == "__main__":
125+
sys.exit(main())

0 commit comments

Comments
 (0)