autopkgtest-runner 0.1.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- autopkgtest_runner-0.1.4/LICENSE +17 -0
- autopkgtest_runner-0.1.4/PKG-INFO +43 -0
- autopkgtest_runner-0.1.4/README.md +32 -0
- autopkgtest_runner-0.1.4/pyproject.toml +18 -0
- autopkgtest_runner-0.1.4/setup.cfg +4 -0
- autopkgtest_runner-0.1.4/src/autopkgtest_runner/__init__.py +1 -0
- autopkgtest_runner-0.1.4/src/autopkgtest_runner/__main__.py +1 -0
- autopkgtest_runner-0.1.4/src/autopkgtest_runner/accessibility.py +66 -0
- autopkgtest_runner-0.1.4/src/autopkgtest_runner/main.py +417 -0
- autopkgtest_runner-0.1.4/src/autopkgtest_runner.egg-info/PKG-INFO +43 -0
- autopkgtest_runner-0.1.4/src/autopkgtest_runner.egg-info/SOURCES.txt +13 -0
- autopkgtest_runner-0.1.4/src/autopkgtest_runner.egg-info/dependency_links.txt +1 -0
- autopkgtest_runner-0.1.4/src/autopkgtest_runner.egg-info/entry_points.txt +2 -0
- autopkgtest_runner-0.1.4/src/autopkgtest_runner.egg-info/top_level.txt +1 -0
- autopkgtest_runner-0.1.4/tests/test_release_sanity.py +25 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
GNU GENERAL PUBLIC LICENSE
|
|
2
|
+
Version 3, 29 June 2007
|
|
3
|
+
|
|
4
|
+
Copyright (C) 2026 Daniel Nylander <daniel@danielnylander.se>
|
|
5
|
+
|
|
6
|
+
This program is free software: you can redistribute it and/or modify
|
|
7
|
+
it under the terms of the GNU General Public License as published by
|
|
8
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
9
|
+
(at your option) any later version.
|
|
10
|
+
|
|
11
|
+
This program is distributed in the hope that it will be useful,
|
|
12
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
GNU General Public License for more details.
|
|
15
|
+
|
|
16
|
+
You should have received a copy of the GNU General Public License
|
|
17
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: autopkgtest-runner
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary: GTK4 frontend for autopkgtest Debian test framework
|
|
5
|
+
Author-email: Daniel Nylander <daniel@danielnylander.se>
|
|
6
|
+
License-Expression: GPL-3.0-or-later
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Dynamic: license-file
|
|
11
|
+
|
|
12
|
+
# Autopkgtest Runner
|
|
13
|
+
|
|
14
|
+
Frontend for the autopkgtest Debian test framework.
|
|
15
|
+
|
|
16
|
+
Built with GTK4/Adwaita. Part of the [Danne L10n Suite](https://github.com/yeager/debian-repo).
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
## Installation from PyPI
|
|
21
|
+
|
|
22
|
+
GTK4, libadwaita and PyGObject must be installed from your distribution (they
|
|
23
|
+
are native system libraries and are not installed by pip). On Debian/Ubuntu:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
sudo apt install python3-gi gir1.2-gtk-4.0 gir1.2-adw-1 pipx
|
|
27
|
+
pipx install --system-site-packages autopkgtest-runner
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
### Debian/Ubuntu
|
|
33
|
+
```bash
|
|
34
|
+
sudo apt install autopkgtest-runner
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## License
|
|
38
|
+
|
|
39
|
+
GPL-3.0
|
|
40
|
+
|
|
41
|
+
## Author
|
|
42
|
+
|
|
43
|
+
Daniel Nylander — [danielnylander.se](https://danielnylander.se)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Autopkgtest Runner
|
|
2
|
+
|
|
3
|
+
Frontend for the autopkgtest Debian test framework.
|
|
4
|
+
|
|
5
|
+
Built with GTK4/Adwaita. Part of the [Danne L10n Suite](https://github.com/yeager/debian-repo).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
## Installation from PyPI
|
|
10
|
+
|
|
11
|
+
GTK4, libadwaita and PyGObject must be installed from your distribution (they
|
|
12
|
+
are native system libraries and are not installed by pip). On Debian/Ubuntu:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
sudo apt install python3-gi gir1.2-gtk-4.0 gir1.2-adw-1 pipx
|
|
16
|
+
pipx install --system-site-packages autopkgtest-runner
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Debian/Ubuntu
|
|
22
|
+
```bash
|
|
23
|
+
sudo apt install autopkgtest-runner
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## License
|
|
27
|
+
|
|
28
|
+
GPL-3.0
|
|
29
|
+
|
|
30
|
+
## Author
|
|
31
|
+
|
|
32
|
+
Daniel Nylander — [danielnylander.se](https://danielnylander.se)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
name = "autopkgtest-runner"
|
|
8
|
+
version = "0.1.4"
|
|
9
|
+
description = "GTK4 frontend for autopkgtest Debian test framework"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "GPL-3.0-or-later"
|
|
12
|
+
authors = [{name = "Daniel Nylander", email = "daniel@danielnylander.se"}]
|
|
13
|
+
|
|
14
|
+
[project.scripts]
|
|
15
|
+
autopkgtest-runner = "autopkgtest_runner.main:main"
|
|
16
|
+
|
|
17
|
+
[tool.setuptools.packages.find]
|
|
18
|
+
where = ["src"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.4"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .main import main
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Accessibility features: zoom, high contrast, ATK."""
|
|
2
|
+
import gi
|
|
3
|
+
gi.require_version('Gtk', '4.0')
|
|
4
|
+
from gi.repository import Gtk, Gdk, Gio
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AccessibilityManager:
|
|
8
|
+
"""Manages accessibility features for a GTK4 window."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, window, app=None):
|
|
11
|
+
self._window = window
|
|
12
|
+
self._app = app or window.get_application()
|
|
13
|
+
self._font_scale = 1.0
|
|
14
|
+
self._high_contrast = False
|
|
15
|
+
self._css = Gtk.CssProvider()
|
|
16
|
+
Gtk.StyleContext.add_provider_for_display(
|
|
17
|
+
Gdk.Display.get_default(), self._css,
|
|
18
|
+
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + 1
|
|
19
|
+
)
|
|
20
|
+
self._setup_actions()
|
|
21
|
+
|
|
22
|
+
def _setup_actions(self):
|
|
23
|
+
if self._app is None:
|
|
24
|
+
return
|
|
25
|
+
actions = [
|
|
26
|
+
('zoom-in', self._zoom_in, ['<Control>plus', '<Control>equal']),
|
|
27
|
+
('zoom-out', self._zoom_out, ['<Control>minus']),
|
|
28
|
+
('zoom-reset', self._zoom_reset, ['<Control>0']),
|
|
29
|
+
('toggle-high-contrast', self._toggle_hc, ['<Control><Shift>h']),
|
|
30
|
+
]
|
|
31
|
+
for name, cb, accels in actions:
|
|
32
|
+
if not self._app.lookup_action(name):
|
|
33
|
+
action = Gio.SimpleAction.new(name, None)
|
|
34
|
+
action.connect('activate', lambda a, p, c=cb: c())
|
|
35
|
+
self._app.add_action(action)
|
|
36
|
+
self._app.set_accels_for_action(f'app.{name}', accels)
|
|
37
|
+
|
|
38
|
+
def _apply_css(self):
|
|
39
|
+
css = f'window {{ font-size: {self._font_scale}em; }}'
|
|
40
|
+
if self._high_contrast:
|
|
41
|
+
css += """
|
|
42
|
+
window.high-contrast {
|
|
43
|
+
border: 2px solid @accent_color;
|
|
44
|
+
font-weight: bold;
|
|
45
|
+
}"""
|
|
46
|
+
self._css.load_from_string(css.encode())
|
|
47
|
+
|
|
48
|
+
def _zoom_in(self):
|
|
49
|
+
self._font_scale = min(self._font_scale + 0.1, 3.0)
|
|
50
|
+
self._apply_css()
|
|
51
|
+
|
|
52
|
+
def _zoom_out(self):
|
|
53
|
+
self._font_scale = max(self._font_scale - 0.1, 0.5)
|
|
54
|
+
self._apply_css()
|
|
55
|
+
|
|
56
|
+
def _zoom_reset(self):
|
|
57
|
+
self._font_scale = 1.0
|
|
58
|
+
self._apply_css()
|
|
59
|
+
|
|
60
|
+
def _toggle_hc(self):
|
|
61
|
+
self._high_contrast = not self._high_contrast
|
|
62
|
+
if self._high_contrast:
|
|
63
|
+
self._window.add_css_class('high-contrast')
|
|
64
|
+
else:
|
|
65
|
+
self._window.remove_css_class('high-contrast')
|
|
66
|
+
self._apply_css()
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
"""Autopkgtest Runner — GTK4 frontend for running Debian autopkgtests."""
|
|
2
|
+
import gi
|
|
3
|
+
gi.require_version("Gtk", "4.0")
|
|
4
|
+
gi.require_version("Adw", "1")
|
|
5
|
+
from gi.repository import Gtk, Adw, Gdk, Gio, GLib, Pango
|
|
6
|
+
|
|
7
|
+
import gettext
|
|
8
|
+
import locale
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import json
|
|
12
|
+
import datetime
|
|
13
|
+
import threading
|
|
14
|
+
import subprocess
|
|
15
|
+
import re
|
|
16
|
+
from autopkgtest_runner.accessibility import AccessibilityManager
|
|
17
|
+
|
|
18
|
+
LOCALE_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "po")
|
|
19
|
+
if not os.path.isdir(LOCALE_DIR):
|
|
20
|
+
LOCALE_DIR = "/usr/share/locale"
|
|
21
|
+
gettext.bindtextdomain("autopkgtest-runner", LOCALE_DIR)
|
|
22
|
+
gettext.textdomain("autopkgtest-runner")
|
|
23
|
+
_ = gettext.gettext
|
|
24
|
+
|
|
25
|
+
APP_ID = "se.danielnylander.autopkgtest.runner"
|
|
26
|
+
SETTINGS_DIR = os.path.join(
|
|
27
|
+
os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")),
|
|
28
|
+
"autopkgtest-runner"
|
|
29
|
+
)
|
|
30
|
+
SETTINGS_FILE = os.path.join(SETTINGS_DIR, "settings.json")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _load_settings():
|
|
34
|
+
if os.path.exists(SETTINGS_FILE):
|
|
35
|
+
with open(SETTINGS_FILE) as f:
|
|
36
|
+
return json.load(f)
|
|
37
|
+
return {"welcome_shown": False}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _save_settings(s):
|
|
41
|
+
os.makedirs(SETTINGS_DIR, exist_ok=True)
|
|
42
|
+
with open(SETTINGS_FILE, "w") as f:
|
|
43
|
+
json.dump(s, f, indent=2)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class AutopkgtestRunnerWindow(Adw.ApplicationWindow):
|
|
50
|
+
def __init__(self, app):
|
|
51
|
+
super().__init__(application=app, title=_("Autopkgtest Runner"), default_width=1000, default_height=700)
|
|
52
|
+
self.settings = _load_settings()
|
|
53
|
+
|
|
54
|
+
self._test_results = []
|
|
55
|
+
self._running = False
|
|
56
|
+
|
|
57
|
+
main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
58
|
+
|
|
59
|
+
# Header
|
|
60
|
+
headerbar = Adw.HeaderBar()
|
|
61
|
+
title_widget = Adw.WindowTitle(title=_("Autopkgtest Runner"), subtitle="")
|
|
62
|
+
headerbar.set_title_widget(title_widget)
|
|
63
|
+
self._title_widget = title_widget
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
open_btn = Gtk.Button(icon_name="folder-open-symbolic", tooltip_text=_("Open package directory"))
|
|
67
|
+
open_btn.connect("clicked", self._on_open_pkg)
|
|
68
|
+
headerbar.pack_start(open_btn)
|
|
69
|
+
|
|
70
|
+
self._run_btn = Gtk.Button(label=_("Run Tests"))
|
|
71
|
+
self._run_btn.add_css_class("suggested-action")
|
|
72
|
+
self._run_btn.set_sensitive(False)
|
|
73
|
+
self._run_btn.connect("clicked", self._on_run)
|
|
74
|
+
headerbar.pack_end(self._run_btn)
|
|
75
|
+
|
|
76
|
+
# Menu
|
|
77
|
+
menu = Gio.Menu()
|
|
78
|
+
menu.append(_("Settings"), "app.settings")
|
|
79
|
+
menu.append(_("Copy Debug Info"), "app.copy-debug")
|
|
80
|
+
menu.append(_("Keyboard Shortcuts"), "app.shortcuts")
|
|
81
|
+
menu.append(_("About Autopkgtest Runner"), "app.about")
|
|
82
|
+
menu_btn = Gtk.MenuButton(icon_name="open-menu-symbolic", menu_model=menu)
|
|
83
|
+
headerbar.pack_end(menu_btn)
|
|
84
|
+
|
|
85
|
+
main_box.append(headerbar)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# Paned: test list + output
|
|
89
|
+
paned = Gtk.Paned(orientation=Gtk.Orientation.VERTICAL)
|
|
90
|
+
paned.set_vexpand(True)
|
|
91
|
+
|
|
92
|
+
# Top: test list
|
|
93
|
+
top_scroll = Gtk.ScrolledWindow(min_content_height=200)
|
|
94
|
+
self._test_list = Gtk.ListBox()
|
|
95
|
+
self._test_list.set_selection_mode(Gtk.SelectionMode.NONE)
|
|
96
|
+
self._test_list.add_css_class("boxed-list")
|
|
97
|
+
self._test_list.set_margin_start(12)
|
|
98
|
+
self._test_list.set_margin_end(12)
|
|
99
|
+
self._test_list.set_margin_top(8)
|
|
100
|
+
top_scroll.set_child(self._test_list)
|
|
101
|
+
paned.set_start_child(top_scroll)
|
|
102
|
+
|
|
103
|
+
# Bottom: output log
|
|
104
|
+
bottom_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
105
|
+
out_label = Gtk.Label(label=_("Test Output"), xalign=0)
|
|
106
|
+
out_label.add_css_class("heading")
|
|
107
|
+
out_label.set_margin_start(12)
|
|
108
|
+
out_label.set_margin_top(4)
|
|
109
|
+
bottom_box.append(out_label)
|
|
110
|
+
|
|
111
|
+
out_scroll = Gtk.ScrolledWindow(vexpand=True)
|
|
112
|
+
self._output_view = Gtk.TextView(editable=False, monospace=True)
|
|
113
|
+
self._output_view.set_top_margin(8)
|
|
114
|
+
self._output_view.set_left_margin(8)
|
|
115
|
+
out_scroll.set_child(self._output_view)
|
|
116
|
+
bottom_box.append(out_scroll)
|
|
117
|
+
paned.set_end_child(bottom_box)
|
|
118
|
+
paned.set_position(300)
|
|
119
|
+
|
|
120
|
+
main_box.append(paned)
|
|
121
|
+
|
|
122
|
+
self._pkg_dir = None
|
|
123
|
+
|
|
124
|
+
# Status bar
|
|
125
|
+
self._status = Gtk.Label(label=_("Ready"), xalign=0)
|
|
126
|
+
self._status.set_margin_start(12)
|
|
127
|
+
self._status.set_margin_end(12)
|
|
128
|
+
self._status.set_margin_top(4)
|
|
129
|
+
self._status.set_margin_bottom(4)
|
|
130
|
+
self._status.add_css_class("dim-label")
|
|
131
|
+
main_box.append(self._status)
|
|
132
|
+
|
|
133
|
+
self.set_content(main_box)
|
|
134
|
+
|
|
135
|
+
if not self.settings.get("welcome_shown"):
|
|
136
|
+
GLib.idle_add(self._show_welcome)
|
|
137
|
+
|
|
138
|
+
def _show_welcome(self):
|
|
139
|
+
dialog = Adw.Dialog()
|
|
140
|
+
dialog.set_title(_("Welcome"))
|
|
141
|
+
dialog.set_content_width(420)
|
|
142
|
+
dialog.set_content_height(480)
|
|
143
|
+
|
|
144
|
+
page = Adw.StatusPage()
|
|
145
|
+
page.set_icon_name("system-run-symbolic")
|
|
146
|
+
page.set_title(_("Welcome to Autopkgtest Runner"))
|
|
147
|
+
page.set_description(_("Run Debian autopkgtests easily.\n\n"
|
|
148
|
+
"✓ Run autopkgtest on local packages\n"
|
|
149
|
+
"✓ Visual pass/fail results\n"
|
|
150
|
+
"✓ Live test output streaming\n"
|
|
151
|
+
"✓ Test history and comparison\n"
|
|
152
|
+
"✓ Export test reports"))
|
|
153
|
+
|
|
154
|
+
btn = Gtk.Button(label=_("Get Started"))
|
|
155
|
+
btn.add_css_class("suggested-action")
|
|
156
|
+
btn.add_css_class("pill")
|
|
157
|
+
btn.set_halign(Gtk.Align.CENTER)
|
|
158
|
+
btn.set_margin_top(12)
|
|
159
|
+
btn.connect("clicked", self._on_welcome_close, dialog)
|
|
160
|
+
page.set_child(btn)
|
|
161
|
+
|
|
162
|
+
box = Adw.ToolbarView()
|
|
163
|
+
hb = Adw.HeaderBar()
|
|
164
|
+
hb.set_show_title(False)
|
|
165
|
+
box.add_top_bar(hb)
|
|
166
|
+
box.set_content(page)
|
|
167
|
+
dialog.set_child(box)
|
|
168
|
+
dialog.present(self)
|
|
169
|
+
|
|
170
|
+
def _on_welcome_close(self, btn, dialog):
|
|
171
|
+
self.settings["welcome_shown"] = True
|
|
172
|
+
_save_settings(self.settings)
|
|
173
|
+
dialog.close()
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _on_open_pkg(self, btn):
|
|
177
|
+
dialog = Gtk.FileDialog()
|
|
178
|
+
dialog.set_title(_("Select package directory"))
|
|
179
|
+
dialog.select_folder(self, None, self._on_folder_selected)
|
|
180
|
+
|
|
181
|
+
def _on_folder_selected(self, dialog, result):
|
|
182
|
+
try:
|
|
183
|
+
f = dialog.select_folder_finish(result)
|
|
184
|
+
self._pkg_dir = f.get_path()
|
|
185
|
+
self._run_btn.set_sensitive(True)
|
|
186
|
+
self._title_widget.set_subtitle(os.path.basename(self._pkg_dir))
|
|
187
|
+
# Check for debian/tests
|
|
188
|
+
test_dir = os.path.join(self._pkg_dir, "debian", "tests")
|
|
189
|
+
if os.path.isdir(test_dir):
|
|
190
|
+
control = os.path.join(test_dir, "control")
|
|
191
|
+
if os.path.exists(control):
|
|
192
|
+
with open(control) as fh:
|
|
193
|
+
self._status.set_text(_("Found test control: %s") % control)
|
|
194
|
+
else:
|
|
195
|
+
self._status.set_text(_("No debian/tests/control found"))
|
|
196
|
+
else:
|
|
197
|
+
self._status.set_text(_("No debian/tests directory"))
|
|
198
|
+
except:
|
|
199
|
+
pass
|
|
200
|
+
|
|
201
|
+
def _on_run(self, btn):
|
|
202
|
+
if not self._pkg_dir:
|
|
203
|
+
return
|
|
204
|
+
if self._running:
|
|
205
|
+
self._running = False
|
|
206
|
+
self._run_btn.set_label(_("Run Tests"))
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
self._running = True
|
|
210
|
+
self._run_btn.set_label(_("Stop"))
|
|
211
|
+
self._run_btn.remove_css_class("suggested-action")
|
|
212
|
+
self._run_btn.add_css_class("destructive-action")
|
|
213
|
+
self._output_view.get_buffer().set_text("")
|
|
214
|
+
threading.Thread(target=self._run_tests, daemon=True).start()
|
|
215
|
+
|
|
216
|
+
def _run_tests(self):
|
|
217
|
+
try:
|
|
218
|
+
proc = subprocess.Popen(
|
|
219
|
+
["autopkgtest", self._pkg_dir, "--", "null"],
|
|
220
|
+
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
|
221
|
+
text=True
|
|
222
|
+
)
|
|
223
|
+
for line in iter(proc.stdout.readline, ""):
|
|
224
|
+
if not self._running:
|
|
225
|
+
proc.terminate()
|
|
226
|
+
break
|
|
227
|
+
GLib.idle_add(self._append_output, line)
|
|
228
|
+
proc.wait()
|
|
229
|
+
GLib.idle_add(self._test_done, proc.returncode)
|
|
230
|
+
except FileNotFoundError:
|
|
231
|
+
GLib.idle_add(self._append_output, _("autopkgtest not installed. Install with: sudo apt install autopkgtest\n"))
|
|
232
|
+
GLib.idle_add(self._test_done, -1)
|
|
233
|
+
except Exception as e:
|
|
234
|
+
GLib.idle_add(self._append_output, str(e) + "\n")
|
|
235
|
+
GLib.idle_add(self._test_done, -1)
|
|
236
|
+
|
|
237
|
+
def _append_output(self, text):
|
|
238
|
+
buf = self._output_view.get_buffer()
|
|
239
|
+
buf.insert(buf.get_end_iter(), text)
|
|
240
|
+
|
|
241
|
+
def _test_done(self, returncode):
|
|
242
|
+
self._running = False
|
|
243
|
+
self._run_btn.set_label(_("Run Tests"))
|
|
244
|
+
self._run_btn.remove_css_class("destructive-action")
|
|
245
|
+
self._run_btn.add_css_class("suggested-action")
|
|
246
|
+
|
|
247
|
+
if returncode == 0:
|
|
248
|
+
icon = "✅"
|
|
249
|
+
status_text = _("All tests passed")
|
|
250
|
+
elif returncode == -1:
|
|
251
|
+
icon = "⚠️"
|
|
252
|
+
status_text = _("Could not run tests")
|
|
253
|
+
else:
|
|
254
|
+
icon = "❌"
|
|
255
|
+
status_text = _("Tests failed (exit code %(code)d)") % {"code": returncode}
|
|
256
|
+
|
|
257
|
+
row = Adw.ActionRow()
|
|
258
|
+
row.set_title(f"{icon} {status_text}")
|
|
259
|
+
row.set_subtitle(datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
|
260
|
+
self._test_list.prepend(row)
|
|
261
|
+
|
|
262
|
+
ts = datetime.datetime.now().strftime("%H:%M:%S")
|
|
263
|
+
self._status.set_text(_("%(time)s — %(status)s") % {"time": ts, "status": status_text})
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class AutopkgtestRunnerApp(Adw.Application):
|
|
267
|
+
def __init__(self):
|
|
268
|
+
super().__init__(application_id=APP_ID, flags=Gio.ApplicationFlags.FLAGS_NONE)
|
|
269
|
+
self.window = None
|
|
270
|
+
|
|
271
|
+
for name, callback in [
|
|
272
|
+
("settings", self._on_settings),
|
|
273
|
+
("copy-debug", self._on_copy_debug),
|
|
274
|
+
("shortcuts", self._on_shortcuts),
|
|
275
|
+
("about", self._on_about),
|
|
276
|
+
("quit", self._on_quit),
|
|
277
|
+
]:
|
|
278
|
+
action = Gio.SimpleAction.new(name, None)
|
|
279
|
+
action.connect("activate", callback)
|
|
280
|
+
self.add_action(action)
|
|
281
|
+
|
|
282
|
+
self.set_accels_for_action("app.quit", ["<Ctrl>q"])
|
|
283
|
+
self.set_accels_for_action("app.shortcuts", ["<Ctrl>slash"])
|
|
284
|
+
|
|
285
|
+
def do_activate(self):
|
|
286
|
+
if not self.window:
|
|
287
|
+
self.window = AutopkgtestRunnerWindow(self)
|
|
288
|
+
self.window.present()
|
|
289
|
+
|
|
290
|
+
def _on_settings(self, *_args):
|
|
291
|
+
if not self.window:
|
|
292
|
+
return
|
|
293
|
+
dialog = Adw.PreferencesDialog()
|
|
294
|
+
dialog.set_title(_("Settings"))
|
|
295
|
+
page = Adw.PreferencesPage()
|
|
296
|
+
|
|
297
|
+
group = Adw.PreferencesGroup(title=_("Test Runner"))
|
|
298
|
+
row = Adw.ComboRow(title=_("Virtualization"))
|
|
299
|
+
row.set_model(Gtk.StringList.new(["null", "schroot", "lxc", "qemu"]))
|
|
300
|
+
group.add(row)
|
|
301
|
+
page.add(group)
|
|
302
|
+
dialog.add(page)
|
|
303
|
+
dialog.present(self.window)
|
|
304
|
+
|
|
305
|
+
def _on_copy_debug(self, *_args):
|
|
306
|
+
if not self.window:
|
|
307
|
+
return
|
|
308
|
+
from . import __version__
|
|
309
|
+
info = (
|
|
310
|
+
f"Autopkgtest Runner {__version__}\n"
|
|
311
|
+
f"Python {sys.version}\n"
|
|
312
|
+
f"GTK {Gtk.MAJOR_VERSION}.{Gtk.MINOR_VERSION}\n"
|
|
313
|
+
f"Adw {Adw.MAJOR_VERSION}.{Adw.MINOR_VERSION}\n"
|
|
314
|
+
f"OS: {os.uname().sysname} {os.uname().release}\n"
|
|
315
|
+
)
|
|
316
|
+
clipboard = Gdk.Display.get_default().get_clipboard()
|
|
317
|
+
clipboard.set(info)
|
|
318
|
+
self.window._status.set_text(_("Debug info copied"))
|
|
319
|
+
|
|
320
|
+
def _on_shortcuts(self, *_args):
|
|
321
|
+
if self.window:
|
|
322
|
+
dialog = Gtk.ShortcutsWindow(transient_for=self.window)
|
|
323
|
+
section = Gtk.ShortcutsSection(visible=True)
|
|
324
|
+
group = Gtk.ShortcutsGroup(title=_("General"), visible=True)
|
|
325
|
+
for accel, title in [
|
|
326
|
+
("<Ctrl>q", _("Quit")),
|
|
327
|
+
("<Ctrl>slash", _("Keyboard shortcuts")),
|
|
328
|
+
]:
|
|
329
|
+
group.append(Gtk.ShortcutsShortcut(accelerator=accel, title=title, visible=True))
|
|
330
|
+
section.append(group)
|
|
331
|
+
dialog.append(section)
|
|
332
|
+
dialog.present()
|
|
333
|
+
|
|
334
|
+
def _on_about(self, *_args):
|
|
335
|
+
from . import __version__
|
|
336
|
+
dialog = Adw.AboutDialog(
|
|
337
|
+
application_name=_("Autopkgtest Runner"),
|
|
338
|
+
application_icon="system-run-symbolic",
|
|
339
|
+
version=__version__,
|
|
340
|
+
developer_name="Daniel Nylander",
|
|
341
|
+
website="https://github.com/yeager/autopkgtest-runner",
|
|
342
|
+
license_type=Gtk.License.GPL_3_0,
|
|
343
|
+
issue_url="https://github.com/yeager/autopkgtest-runner/issues",
|
|
344
|
+
comments=_("Run autopkgtests locally without sbuild. Visual test output with pass/fail indicators."),
|
|
345
|
+
)
|
|
346
|
+
dialog.present(self.window)
|
|
347
|
+
|
|
348
|
+
def _on_quit(self, *_args):
|
|
349
|
+
self.quit()
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def main():
|
|
353
|
+
app = AutopkgtestRunnerApp()
|
|
354
|
+
app.run(sys.argv)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
# --- Session restore ---
|
|
358
|
+
import json as _json
|
|
359
|
+
import os as _os
|
|
360
|
+
|
|
361
|
+
def _save_session(window, app_name):
|
|
362
|
+
config_dir = _os.path.join(_os.path.expanduser('~'), '.config', app_name)
|
|
363
|
+
_os.makedirs(config_dir, exist_ok=True)
|
|
364
|
+
state = {'width': window.get_width(), 'height': window.get_height(),
|
|
365
|
+
'maximized': window.is_maximized()}
|
|
366
|
+
try:
|
|
367
|
+
with open(_os.path.join(config_dir, 'session.json'), 'w') as f:
|
|
368
|
+
_json.dump(state, f)
|
|
369
|
+
except OSError:
|
|
370
|
+
pass
|
|
371
|
+
|
|
372
|
+
def _restore_session(window, app_name):
|
|
373
|
+
path = _os.path.join(_os.path.expanduser('~'), '.config', app_name, 'session.json')
|
|
374
|
+
try:
|
|
375
|
+
with open(path) as f:
|
|
376
|
+
state = _json.load(f)
|
|
377
|
+
window.set_default_size(state.get('width', 800), state.get('height', 600))
|
|
378
|
+
if state.get('maximized'):
|
|
379
|
+
window.maximize()
|
|
380
|
+
except (FileNotFoundError, _json.JSONDecodeError, OSError):
|
|
381
|
+
pass
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
# --- Fullscreen toggle (F11) ---
|
|
385
|
+
def _setup_fullscreen(window, app):
|
|
386
|
+
"""Add F11 fullscreen toggle."""
|
|
387
|
+
from gi.repository import Gio
|
|
388
|
+
if not app.lookup_action('toggle-fullscreen'):
|
|
389
|
+
action = Gio.SimpleAction.new('toggle-fullscreen', None)
|
|
390
|
+
action.connect('activate', lambda a, p: (
|
|
391
|
+
window.unfullscreen() if window.is_fullscreen() else window.fullscreen()
|
|
392
|
+
))
|
|
393
|
+
app.add_action(action)
|
|
394
|
+
app.set_accels_for_action('app.toggle-fullscreen', ['F11'])
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
# --- Plugin system ---
|
|
398
|
+
import importlib.util
|
|
399
|
+
import os as _pos
|
|
400
|
+
|
|
401
|
+
def _load_plugins(app_name):
|
|
402
|
+
"""Load plugins from ~/.config/<app>/plugins/."""
|
|
403
|
+
plugin_dir = _pos.path.join(_pos.path.expanduser('~'), '.config', app_name, 'plugins')
|
|
404
|
+
plugins = []
|
|
405
|
+
if not _pos.path.isdir(plugin_dir):
|
|
406
|
+
return plugins
|
|
407
|
+
for fname in sorted(_pos.listdir(plugin_dir)):
|
|
408
|
+
if fname.endswith('.py') and not fname.startswith('_'):
|
|
409
|
+
path = _pos.path.join(plugin_dir, fname)
|
|
410
|
+
try:
|
|
411
|
+
spec = importlib.util.spec_from_file_location(fname[:-3], path)
|
|
412
|
+
mod = importlib.util.module_from_spec(spec)
|
|
413
|
+
spec.loader.exec_module(mod)
|
|
414
|
+
plugins.append(mod)
|
|
415
|
+
except Exception as e:
|
|
416
|
+
print(f"Plugin {fname}: {e}")
|
|
417
|
+
return plugins
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: autopkgtest-runner
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary: GTK4 frontend for autopkgtest Debian test framework
|
|
5
|
+
Author-email: Daniel Nylander <daniel@danielnylander.se>
|
|
6
|
+
License-Expression: GPL-3.0-or-later
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Dynamic: license-file
|
|
11
|
+
|
|
12
|
+
# Autopkgtest Runner
|
|
13
|
+
|
|
14
|
+
Frontend for the autopkgtest Debian test framework.
|
|
15
|
+
|
|
16
|
+
Built with GTK4/Adwaita. Part of the [Danne L10n Suite](https://github.com/yeager/debian-repo).
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
## Installation from PyPI
|
|
21
|
+
|
|
22
|
+
GTK4, libadwaita and PyGObject must be installed from your distribution (they
|
|
23
|
+
are native system libraries and are not installed by pip). On Debian/Ubuntu:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
sudo apt install python3-gi gir1.2-gtk-4.0 gir1.2-adw-1 pipx
|
|
27
|
+
pipx install --system-site-packages autopkgtest-runner
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
### Debian/Ubuntu
|
|
33
|
+
```bash
|
|
34
|
+
sudo apt install autopkgtest-runner
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## License
|
|
38
|
+
|
|
39
|
+
GPL-3.0
|
|
40
|
+
|
|
41
|
+
## Author
|
|
42
|
+
|
|
43
|
+
Daniel Nylander — [danielnylander.se](https://danielnylander.se)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/autopkgtest_runner/__init__.py
|
|
5
|
+
src/autopkgtest_runner/__main__.py
|
|
6
|
+
src/autopkgtest_runner/accessibility.py
|
|
7
|
+
src/autopkgtest_runner/main.py
|
|
8
|
+
src/autopkgtest_runner.egg-info/PKG-INFO
|
|
9
|
+
src/autopkgtest_runner.egg-info/SOURCES.txt
|
|
10
|
+
src/autopkgtest_runner.egg-info/dependency_links.txt
|
|
11
|
+
src/autopkgtest_runner.egg-info/entry_points.txt
|
|
12
|
+
src/autopkgtest_runner.egg-info/top_level.txt
|
|
13
|
+
tests/test_release_sanity.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
autopkgtest_runner
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import unittest
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
6
|
+
|
|
7
|
+
class ReleaseSanityTests(unittest.TestCase):
|
|
8
|
+
def test_python_sources_do_not_call_locale_bindtextdomain(self):
|
|
9
|
+
for path in ROOT.rglob("*.py"):
|
|
10
|
+
if ".git" in path.parts or "build" in path.parts or "tests" in path.parts:
|
|
11
|
+
continue
|
|
12
|
+
source = path.read_text(encoding="utf-8")
|
|
13
|
+
self.assertNotIn("locale." + "bindtextdomain(", source, str(path))
|
|
14
|
+
|
|
15
|
+
def test_project_and_package_versions_match(self):
|
|
16
|
+
project = (ROOT / "pyproject.toml").read_text(encoding="utf-8")
|
|
17
|
+
metadata_version = re.search(r'(?m)^version\s*=\s*["\']([^"\']+)', project)
|
|
18
|
+
package_init = ROOT / "src/autopkgtest_runner" / "__init__.py"
|
|
19
|
+
source_version = re.search(r'__version__\s*=\s*["\']([^"\']+)', package_init.read_text(encoding="utf-8"))
|
|
20
|
+
self.assertIsNotNone(metadata_version)
|
|
21
|
+
self.assertIsNotNone(source_version)
|
|
22
|
+
self.assertEqual(metadata_version.group(1), source_version.group(1))
|
|
23
|
+
|
|
24
|
+
if __name__ == "__main__":
|
|
25
|
+
unittest.main()
|