sqlbench 0.1.0__py3-none-any.whl

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.
sqlbench/database.py ADDED
@@ -0,0 +1,215 @@
1
+ """SQLite database for storing connections and saved queries."""
2
+
3
+ import sqlite3
4
+ from pathlib import Path
5
+
6
+
7
+ class Database:
8
+ def __init__(self, db_path=None):
9
+ if db_path is None:
10
+ db_path = Path(__file__).parent / "iutil.db"
11
+ self.db_path = db_path
12
+ self._init_db()
13
+
14
+ def _get_conn(self):
15
+ return sqlite3.connect(self.db_path)
16
+
17
+ def _init_db(self):
18
+ with self._get_conn() as conn:
19
+ # Check if we need to migrate old connections table
20
+ cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='connections'")
21
+ if cursor.fetchone():
22
+ # Check existing columns
23
+ cursor = conn.execute("PRAGMA table_info(connections)")
24
+ columns = [row[1] for row in cursor.fetchall()]
25
+
26
+ # Migration: add new columns if needed
27
+ needs_migration = 'db_type' not in columns or 'id' not in columns
28
+
29
+ if needs_migration:
30
+ conn.execute("ALTER TABLE connections RENAME TO connections_old")
31
+ conn.execute("""
32
+ CREATE TABLE connections (
33
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
34
+ name TEXT NOT NULL UNIQUE,
35
+ db_type TEXT NOT NULL DEFAULT 'ibmi',
36
+ host TEXT NOT NULL,
37
+ port INTEGER,
38
+ database TEXT,
39
+ user TEXT NOT NULL,
40
+ password TEXT NOT NULL
41
+ )
42
+ """)
43
+ # Migrate data - existing connections become IBM i type
44
+ if 'id' in columns:
45
+ conn.execute("""
46
+ INSERT INTO connections (id, name, db_type, host, user, password)
47
+ SELECT id, name, 'ibmi', host, user, password FROM connections_old
48
+ """)
49
+ else:
50
+ conn.execute("""
51
+ INSERT INTO connections (name, db_type, host, user, password)
52
+ SELECT name, 'ibmi', host, user, password FROM connections_old
53
+ """)
54
+ conn.execute("DROP TABLE connections_old")
55
+ else:
56
+ conn.execute("""
57
+ CREATE TABLE connections (
58
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
59
+ name TEXT NOT NULL UNIQUE,
60
+ db_type TEXT NOT NULL DEFAULT 'ibmi',
61
+ host TEXT NOT NULL,
62
+ port INTEGER,
63
+ database TEXT,
64
+ user TEXT NOT NULL,
65
+ password TEXT NOT NULL
66
+ )
67
+ """)
68
+ conn.execute("""
69
+ CREATE TABLE IF NOT EXISTS saved_queries (
70
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
71
+ name TEXT NOT NULL,
72
+ sql TEXT NOT NULL,
73
+ connection_name TEXT,
74
+ db_type TEXT,
75
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
76
+ )
77
+ """)
78
+ # Migration: add db_type column if it doesn't exist
79
+ cursor = conn.execute("PRAGMA table_info(saved_queries)")
80
+ columns = [row[1] for row in cursor.fetchall()]
81
+ if 'db_type' not in columns:
82
+ conn.execute("ALTER TABLE saved_queries ADD COLUMN db_type TEXT")
83
+ conn.execute("""
84
+ CREATE TABLE IF NOT EXISTS settings (
85
+ key TEXT PRIMARY KEY,
86
+ value TEXT
87
+ )
88
+ """)
89
+ conn.execute("""
90
+ CREATE TABLE IF NOT EXISTS saved_tabs (
91
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
92
+ tab_type TEXT NOT NULL,
93
+ connection_name TEXT NOT NULL,
94
+ tab_data TEXT,
95
+ tab_order INTEGER
96
+ )
97
+ """)
98
+ conn.commit()
99
+
100
+ # Connection methods
101
+ def get_connections(self):
102
+ with self._get_conn() as conn:
103
+ conn.row_factory = sqlite3.Row
104
+ cursor = conn.execute(
105
+ "SELECT id, name, db_type, host, port, database, user FROM connections ORDER BY name"
106
+ )
107
+ return [dict(row) for row in cursor.fetchall()]
108
+
109
+ def get_connection(self, name):
110
+ with self._get_conn() as conn:
111
+ conn.row_factory = sqlite3.Row
112
+ cursor = conn.execute(
113
+ "SELECT id, name, db_type, host, port, database, user, password FROM connections WHERE name = ?",
114
+ (name,)
115
+ )
116
+ row = cursor.fetchone()
117
+ return dict(row) if row else None
118
+
119
+ def get_connection_by_id(self, conn_id):
120
+ with self._get_conn() as conn:
121
+ conn.row_factory = sqlite3.Row
122
+ cursor = conn.execute(
123
+ "SELECT id, name, db_type, host, port, database, user, password FROM connections WHERE id = ?",
124
+ (conn_id,)
125
+ )
126
+ row = cursor.fetchone()
127
+ return dict(row) if row else None
128
+
129
+ def save_connection(self, name, db_type, host, port, database, user, password, conn_id=None):
130
+ with self._get_conn() as conn:
131
+ if conn_id:
132
+ # Update existing connection
133
+ conn.execute(
134
+ """UPDATE connections SET name = ?, db_type = ?, host = ?, port = ?,
135
+ database = ?, user = ?, password = ? WHERE id = ?""",
136
+ (name, db_type, host, port, database, user, password, conn_id)
137
+ )
138
+ else:
139
+ # Insert new connection
140
+ conn.execute(
141
+ """INSERT INTO connections (name, db_type, host, port, database, user, password)
142
+ VALUES (?, ?, ?, ?, ?, ?, ?)""",
143
+ (name, db_type, host, port, database, user, password)
144
+ )
145
+ conn.commit()
146
+
147
+ def delete_connection(self, conn_id):
148
+ with self._get_conn() as conn:
149
+ conn.execute("DELETE FROM connections WHERE id = ?", (conn_id,))
150
+ conn.commit()
151
+
152
+ # Saved query methods
153
+ def get_saved_queries(self, db_type=None):
154
+ with self._get_conn() as conn:
155
+ conn.row_factory = sqlite3.Row
156
+ if db_type:
157
+ cursor = conn.execute(
158
+ "SELECT id, name, sql, connection_name, db_type FROM saved_queries WHERE db_type = ? OR db_type IS NULL ORDER BY name",
159
+ (db_type,)
160
+ )
161
+ else:
162
+ cursor = conn.execute(
163
+ "SELECT id, name, sql, connection_name, db_type FROM saved_queries ORDER BY name"
164
+ )
165
+ return [dict(row) for row in cursor.fetchall()]
166
+
167
+ def save_query(self, name, sql, connection_name=None, db_type=None):
168
+ with self._get_conn() as conn:
169
+ conn.execute(
170
+ """INSERT INTO saved_queries (name, sql, connection_name, db_type)
171
+ VALUES (?, ?, ?, ?)""",
172
+ (name, sql, connection_name, db_type)
173
+ )
174
+ conn.commit()
175
+
176
+ def delete_query(self, query_id):
177
+ with self._get_conn() as conn:
178
+ conn.execute("DELETE FROM saved_queries WHERE id = ?", (query_id,))
179
+ conn.commit()
180
+
181
+ # Tab state methods
182
+ def save_tabs(self, tabs):
183
+ """Save tab state. tabs is a list of dicts with type, connection, data."""
184
+ with self._get_conn() as conn:
185
+ conn.execute("DELETE FROM saved_tabs")
186
+ for i, tab in enumerate(tabs):
187
+ conn.execute(
188
+ "INSERT INTO saved_tabs (tab_type, connection_name, tab_data, tab_order) VALUES (?, ?, ?, ?)",
189
+ (tab["type"], tab["connection"], tab.get("data", ""), i)
190
+ )
191
+ conn.commit()
192
+
193
+ def get_saved_tabs(self):
194
+ """Get saved tab state."""
195
+ with self._get_conn() as conn:
196
+ conn.row_factory = sqlite3.Row
197
+ cursor = conn.execute(
198
+ "SELECT tab_type, connection_name, tab_data FROM saved_tabs ORDER BY tab_order"
199
+ )
200
+ return [dict(row) for row in cursor.fetchall()]
201
+
202
+ # Settings methods
203
+ def get_setting(self, key, default=None):
204
+ with self._get_conn() as conn:
205
+ cursor = conn.execute("SELECT value FROM settings WHERE key = ?", (key,))
206
+ row = cursor.fetchone()
207
+ return row[0] if row else default
208
+
209
+ def set_setting(self, key, value):
210
+ with self._get_conn() as conn:
211
+ conn.execute(
212
+ "INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)",
213
+ (key, value)
214
+ )
215
+ conn.commit()
@@ -0,0 +1 @@
1
+ """Dialog modules for SQLBench."""
@@ -0,0 +1,356 @@
1
+ """Connection management dialog."""
2
+
3
+ import tkinter as tk
4
+ from tkinter import ttk, messagebox
5
+ import threading
6
+
7
+ from sqlbench.adapters import get_adapter_choices, get_adapter
8
+
9
+
10
+ class ConnectionDialog:
11
+ def __init__(self, parent, db, edit_name=None, app=None):
12
+ self.db = db
13
+ self.edit_name = edit_name
14
+ self.app = app
15
+ self._current_id = None # Track current connection ID
16
+ self._connections = [] # Store connections with IDs
17
+ self.top = tk.Toplevel(parent)
18
+ self.top.title("Connection" if edit_name else "Manage Connections")
19
+ self.top.geometry("720x500")
20
+ self.top.transient(parent)
21
+ self.top.grab_set()
22
+
23
+ # Apply theme
24
+ self._apply_theme()
25
+
26
+ self._create_widgets()
27
+ self._refresh_list()
28
+
29
+ # If editing specific connection, load it
30
+ if edit_name:
31
+ self._load_connection_by_name(edit_name)
32
+
33
+ # ESC to close
34
+ self.top.bind("<Escape>", lambda e: self.top.destroy())
35
+
36
+ def _apply_theme(self):
37
+ """Apply dark/light theme colors."""
38
+ is_dark = self.app.dark_mode_var.get() if self.app else False
39
+ if is_dark:
40
+ self.bg = "#2b2b2b"
41
+ self.fg = "#a9b7c6"
42
+ self.list_bg = "#313335"
43
+ self.select_bg = "#214283"
44
+ self.select_fg = "#a9b7c6"
45
+ self.status_fg = "#a9b7c6" # For "Testing..." message
46
+ else:
47
+ self.bg = "#f0f0f0"
48
+ self.fg = "#000000"
49
+ self.list_bg = "#ffffff"
50
+ self.select_bg = "#0078d4"
51
+ self.select_fg = "#ffffff"
52
+ self.status_fg = "#000000"
53
+
54
+ self.top.configure(bg=self.bg)
55
+
56
+ def _create_widgets(self):
57
+ # Left side - list
58
+ list_frame = ttk.Frame(self.top)
59
+ list_frame.pack(side=tk.LEFT, fill=tk.BOTH, padx=5, pady=5)
60
+
61
+ self.conn_listbox = tk.Listbox(list_frame, width=25,
62
+ bg=self.list_bg, fg=self.fg,
63
+ selectbackground=self.select_bg,
64
+ selectforeground=self.select_fg,
65
+ highlightthickness=0)
66
+ self.conn_listbox.pack(fill=tk.BOTH, expand=True)
67
+ self.conn_listbox.bind("<<ListboxSelect>>", self._on_select)
68
+
69
+ btn_frame = ttk.Frame(list_frame)
70
+ btn_frame.pack(fill=tk.X, pady=5)
71
+ ttk.Button(btn_frame, text="New", command=self._new).pack(side=tk.LEFT, padx=2)
72
+ ttk.Button(btn_frame, text="Delete", command=self._delete).pack(side=tk.LEFT, padx=2)
73
+
74
+ # Right side - details
75
+ detail_frame = ttk.LabelFrame(self.top, text="Connection Details")
76
+ detail_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=5, pady=5)
77
+
78
+ # Configure column weights so entries expand
79
+ detail_frame.columnconfigure(1, weight=1)
80
+
81
+ row = 0
82
+
83
+ # Name
84
+ ttk.Label(detail_frame, text="Name:").grid(row=row, column=0, sticky=tk.W, padx=5, pady=5)
85
+ self.name_entry = ttk.Entry(detail_frame, width=40)
86
+ self.name_entry.grid(row=row, column=1, columnspan=2, sticky=tk.EW, padx=5, pady=5)
87
+ row += 1
88
+
89
+ # Database Type
90
+ ttk.Label(detail_frame, text="Type:").grid(row=row, column=0, sticky=tk.W, padx=5, pady=5)
91
+ self.db_type_var = tk.StringVar(value="ibmi")
92
+ self.db_type_combo = ttk.Combobox(
93
+ detail_frame,
94
+ textvariable=self.db_type_var,
95
+ values=[choice[1] for choice in get_adapter_choices()],
96
+ state="readonly",
97
+ width=37
98
+ )
99
+ self.db_type_combo.grid(row=row, column=1, columnspan=2, sticky=tk.W, padx=5, pady=5)
100
+ self.db_type_combo.bind("<<ComboboxSelected>>", self._on_type_change)
101
+ # Map display names to db_type keys
102
+ self._type_map = {choice[1]: choice[0] for choice in get_adapter_choices()}
103
+ self._type_map_reverse = {choice[0]: choice[1] for choice in get_adapter_choices()}
104
+ self.db_type_combo.set("IBM i")
105
+ row += 1
106
+
107
+ # Host
108
+ ttk.Label(detail_frame, text="Host:").grid(row=row, column=0, sticky=tk.W, padx=5, pady=5)
109
+ self.host_entry = ttk.Entry(detail_frame, width=40)
110
+ self.host_entry.grid(row=row, column=1, columnspan=2, sticky=tk.EW, padx=5, pady=5)
111
+ row += 1
112
+
113
+ # Port
114
+ self.port_label = ttk.Label(detail_frame, text="Port:")
115
+ self.port_label.grid(row=row, column=0, sticky=tk.W, padx=5, pady=5)
116
+ self.port_entry = ttk.Entry(detail_frame, width=10)
117
+ self.port_entry.grid(row=row, column=1, sticky=tk.W, padx=5, pady=5)
118
+ row += 1
119
+
120
+ # Database
121
+ self.database_label = ttk.Label(detail_frame, text="Database:")
122
+ self.database_label.grid(row=row, column=0, sticky=tk.W, padx=5, pady=5)
123
+ self.database_entry = ttk.Entry(detail_frame, width=40)
124
+ self.database_entry.grid(row=row, column=1, columnspan=2, sticky=tk.EW, padx=5, pady=5)
125
+ row += 1
126
+
127
+ # User
128
+ ttk.Label(detail_frame, text="User:").grid(row=row, column=0, sticky=tk.W, padx=5, pady=5)
129
+ self.user_entry = ttk.Entry(detail_frame, width=40)
130
+ self.user_entry.grid(row=row, column=1, columnspan=2, sticky=tk.EW, padx=5, pady=5)
131
+ row += 1
132
+
133
+ # Password
134
+ ttk.Label(detail_frame, text="Password:").grid(row=row, column=0, sticky=tk.W, padx=5, pady=5)
135
+ self.pass_entry = ttk.Entry(detail_frame, width=40, show="*")
136
+ self.pass_entry.grid(row=row, column=1, columnspan=2, sticky=tk.EW, padx=5, pady=5)
137
+ row += 1
138
+
139
+ # Test and Save buttons
140
+ btn_frame = ttk.Frame(detail_frame)
141
+ btn_frame.grid(row=row, column=0, columnspan=3, pady=20)
142
+
143
+ self.test_btn = ttk.Button(btn_frame, text="Test", command=self._test)
144
+ self.test_btn.pack(side=tk.LEFT, padx=5)
145
+
146
+ ttk.Button(btn_frame, text="Save", command=self._save).pack(side=tk.LEFT, padx=5)
147
+
148
+ # Status label for test results (with word wrap)
149
+ self.test_status = ttk.Label(detail_frame, text="", wraplength=350)
150
+ self.test_status.grid(row=row + 1, column=0, columnspan=3, padx=10, pady=5)
151
+
152
+ # Initial visibility based on default type
153
+ self._update_field_visibility()
154
+
155
+ def _on_type_change(self, event=None):
156
+ """Handle database type change."""
157
+ self._update_field_visibility()
158
+
159
+ def _update_field_visibility(self):
160
+ """Show/hide fields based on database type."""
161
+ display_name = self.db_type_combo.get()
162
+ db_type = self._type_map.get(display_name, "ibmi")
163
+ adapter = get_adapter(db_type)
164
+
165
+ # Show/hide port field
166
+ if adapter.default_port:
167
+ self.port_label.grid()
168
+ self.port_entry.grid()
169
+ if not self.port_entry.get():
170
+ self.port_entry.delete(0, tk.END)
171
+ self.port_entry.insert(0, str(adapter.default_port))
172
+ else:
173
+ self.port_label.grid_remove()
174
+ self.port_entry.grid_remove()
175
+
176
+ # Show/hide database field
177
+ if adapter.requires_database:
178
+ self.database_label.grid()
179
+ self.database_entry.grid()
180
+ else:
181
+ self.database_label.grid_remove()
182
+ self.database_entry.grid_remove()
183
+
184
+ def _refresh_list(self):
185
+ self.conn_listbox.delete(0, tk.END)
186
+ self._connections = self.db.get_connections()
187
+ for conn in self._connections:
188
+ # Show type indicator
189
+ db_type = conn.get("db_type", "ibmi")
190
+ type_indicator = {"ibmi": "[i]", "mysql": "[M]", "postgresql": "[P]"}.get(db_type, "[?]")
191
+ self.conn_listbox.insert(tk.END, f"{type_indicator} {conn['name']}")
192
+
193
+ def _load_connection_by_name(self, name):
194
+ """Load a specific connection into the form by name."""
195
+ conn = self.db.get_connection(name)
196
+ if conn:
197
+ self._current_id = conn["id"]
198
+ self._fill_form(conn)
199
+
200
+ def _fill_form(self, conn):
201
+ """Fill the form with connection data."""
202
+ self.name_entry.delete(0, tk.END)
203
+ self.name_entry.insert(0, conn["name"])
204
+
205
+ # Set database type
206
+ db_type = conn.get("db_type", "ibmi")
207
+ display_name = self._type_map_reverse.get(db_type, "IBM i")
208
+ self.db_type_combo.set(display_name)
209
+ self._update_field_visibility()
210
+
211
+ self.host_entry.delete(0, tk.END)
212
+ self.host_entry.insert(0, conn["host"])
213
+
214
+ self.port_entry.delete(0, tk.END)
215
+ if conn.get("port"):
216
+ self.port_entry.insert(0, str(conn["port"]))
217
+ else:
218
+ # Set default port for type
219
+ adapter = get_adapter(db_type)
220
+ if adapter.default_port:
221
+ self.port_entry.insert(0, str(adapter.default_port))
222
+
223
+ self.database_entry.delete(0, tk.END)
224
+ if conn.get("database"):
225
+ self.database_entry.insert(0, conn["database"])
226
+
227
+ self.user_entry.delete(0, tk.END)
228
+ self.user_entry.insert(0, conn["user"])
229
+
230
+ self.pass_entry.delete(0, tk.END)
231
+ self.pass_entry.insert(0, conn["password"])
232
+
233
+ def _on_select(self, event):
234
+ selection = self.conn_listbox.curselection()
235
+ if not selection:
236
+ return
237
+
238
+ idx = selection[0]
239
+ if idx < len(self._connections):
240
+ conn = self._connections[idx]
241
+ self._current_id = conn["id"]
242
+ # Fetch full connection with password
243
+ full_conn = self.db.get_connection_by_id(conn["id"])
244
+ if full_conn:
245
+ self._fill_form(full_conn)
246
+
247
+ def _new(self):
248
+ self._current_id = None
249
+ self.name_entry.delete(0, tk.END)
250
+ self.db_type_combo.set("IBM i")
251
+ self._update_field_visibility()
252
+ self.host_entry.delete(0, tk.END)
253
+ self.port_entry.delete(0, tk.END)
254
+ self.database_entry.delete(0, tk.END)
255
+ self.user_entry.delete(0, tk.END)
256
+ self.pass_entry.delete(0, tk.END)
257
+ self.name_entry.focus()
258
+
259
+ def _save(self):
260
+ name = self.name_entry.get().strip()
261
+ display_name = self.db_type_combo.get()
262
+ db_type = self._type_map.get(display_name, "ibmi")
263
+ host = self.host_entry.get().strip()
264
+ port_str = self.port_entry.get().strip()
265
+ port = int(port_str) if port_str else None
266
+ database = self.database_entry.get().strip() or None
267
+ user = self.user_entry.get().strip()
268
+ password = self.pass_entry.get()
269
+
270
+ if not all([name, host, user]):
271
+ messagebox.showwarning("Missing Fields", "Name, Host, and User are required.")
272
+ return
273
+
274
+ # Validate required database for certain types
275
+ adapter = get_adapter(db_type)
276
+ if adapter.requires_database and not database:
277
+ messagebox.showwarning("Missing Fields", "Database name is required for this connection type.")
278
+ return
279
+
280
+ self.db.save_connection(name, db_type, host, port, database, user, password, conn_id=self._current_id)
281
+ self._refresh_list()
282
+ messagebox.showinfo("Saved", f"Connection '{name}' saved.")
283
+
284
+ def _delete(self):
285
+ selection = self.conn_listbox.curselection()
286
+ if not selection:
287
+ return
288
+
289
+ idx = selection[0]
290
+ if idx < len(self._connections):
291
+ conn = self._connections[idx]
292
+ if messagebox.askyesno("Confirm Delete", f"Delete connection '{conn['name']}'?"):
293
+ self.db.delete_connection(conn["id"])
294
+ self._refresh_list()
295
+ self._new()
296
+
297
+ def _test(self):
298
+ """Test the connection with current form values."""
299
+ display_name = self.db_type_combo.get()
300
+ db_type = self._type_map.get(display_name, "ibmi")
301
+ host = self.host_entry.get().strip()
302
+ port_str = self.port_entry.get().strip()
303
+ try:
304
+ port = int(port_str) if port_str else None
305
+ except ValueError:
306
+ self.test_status.config(text="Port must be a number", foreground="red")
307
+ return
308
+ database = self.database_entry.get().strip() or None
309
+ user = self.user_entry.get().strip()
310
+ password = self.pass_entry.get()
311
+
312
+ if not all([host, user]):
313
+ self.test_status.config(text="Host and User are required", foreground="red")
314
+ return
315
+
316
+ adapter = get_adapter(db_type)
317
+
318
+ # Use default port if not specified
319
+ if port is None and adapter.default_port:
320
+ port = adapter.default_port
321
+ if adapter.requires_database and not database:
322
+ self.test_status.config(text="Database name required", foreground="red")
323
+ return
324
+
325
+ # Disable test button and show testing status
326
+ self.test_btn.config(state=tk.DISABLED)
327
+ self.test_status.config(text="Testing connection...", foreground=self.status_fg)
328
+ self.top.update()
329
+
330
+ # Run test in background thread
331
+ def do_test():
332
+ try:
333
+ conn = adapter.connect(host, user, password, port, database)
334
+ # Try a simple query to verify connection
335
+ cursor = conn.cursor()
336
+ cursor.execute(adapter.get_version_query())
337
+ version = cursor.fetchone()[0] if cursor.description else "Connected"
338
+ cursor.close()
339
+ conn.close()
340
+ self.top.after(0, self._test_success, str(version)[:50])
341
+ except Exception as e:
342
+ self.top.after(0, self._test_failure, str(e))
343
+
344
+ thread = threading.Thread(target=do_test, daemon=True)
345
+ thread.start()
346
+
347
+ def _test_success(self, version):
348
+ """Handle successful connection test."""
349
+ self.test_btn.config(state=tk.NORMAL)
350
+ self.test_status.config(text=f"Success! {version}", foreground="green")
351
+
352
+ def _test_failure(self, error):
353
+ """Handle failed connection test."""
354
+ self.test_btn.config(state=tk.NORMAL)
355
+ print(f"Connection test failed: {error}") # Debug output
356
+ self.test_status.config(text=f"Failed: {error}", foreground="red")