-
-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathsmoke_gui.py
More file actions
239 lines (206 loc) · 11.1 KB
/
Copy pathsmoke_gui.py
File metadata and controls
239 lines (206 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# GUI smoke test with a fake tkinter (CI containers have no real Tk).
# Verifies: the App builds in PL and EN, discovers every shipped language, and a
# language switch rebuilds the UI while keeping settings AND the session state.
# The tick loop runs and no raw translation
# keys leak into widget texts, profiles behave, CSV export rotates a stale header.
import json
import os
import re
import sys
# This script prints Polish text (a validation message contains "musi byc liczba"
# with diacritics). On Windows the console defaults to cp1252, which cannot encode
# 'c-acute', and the whole smoke run died with UnicodeEncodeError *while printing a
# PASS line*. Force UTF-8 so the output can carry any language it tests.
try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
except (AttributeError, ValueError): # very old Python / already-wrapped stream
pass
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "tests"))
import fake_tk # noqa: E402
tk = fake_tk.install()
fake_tk.single_monitor() # one monitor, exactly the fake screen (see its docstring)
# The user's own files (UI state, profiles, crash log) must not be written into
# the repository by a smoke run. Same helper the pytest GUI harness uses; without
# it this script overwrote the developer's real bean_network_tester_ui.json and
# left a crashes/ folder behind on every `pytest tests`.
import user_files # noqa: E402
user_files.redirect_to_temp()
import bean_network_tester as n # noqa: E402 (after the fakes)
fails = []
def check(name, cond, detail=""):
print(f" [{'OK' if cond else 'FAIL'}] {name} {detail}")
if not cond:
fails.append(name)
# A raw key on screen is what T() gives back for a key that is in NO language
# file: a typo, or a key built at run time (the Tools tab names its field choices
# `tools.exprtest.field.<key>` from the field registry). The prefixes come from
# the English file instead of being written out here: the written-out list had
# fallen seven prefixes behind and could not see a key with two dots, so
# `app.tabs.tools` or `tools.exprtest.tab` would have leaked past it.
with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "lang", "en.json"),
encoding="utf-8") as _en_file:
EN_TEXTS = {k: v for k, v in json.load(_en_file).items() if not k.startswith("_")}
RAW_KEY = re.compile(r"^(%s)(\.[a-z0-9_]+)+$"
% "|".join(sorted({re.escape(k.split(".")[0]) for k in EN_TEXTS})))
def leaked_keys(root):
return [t for t in fake_tk.texts(root) if RAW_KEY.match(t.strip())]
# The pattern proves it can see before anything relies on it seeing nothing.
check("GUI: the raw-key pattern matches every key in the English file",
all(RAW_KEY.match(k) for k in EN_TEXTS),
f"({[k for k in EN_TEXTS if not RAW_KEY.match(k)][:5]})")
check("GUI: the raw-key pattern matches no translated English text",
not any(RAW_KEY.match(v.strip()) for v in EN_TEXTS.values()),
f"({[v for v in EN_TEXTS.values() if RAW_KEY.match(v.strip())][:5]})")
# -- build in Polish ---------------------------------------------------------
n.set_language("pl")
root = tk.Tk()
app = n.App(root)
check("GUI: App builds with the fake tkinter (PL)", True)
language_codes = set(app._lang_name2code.values())
check("GUI: language selector lists discovered languages",
language_codes == {"en", "pl", "zh"}, f"({app._lang_name2code})")
# -- no raw translation keys in widget texts (regression: missing T()) -------
check("GUI: widget texts are translated (no raw i18n keys)", not leaked_keys(root),
f"({sorted(set(leaked_keys(root)))[:8]})")
_leak = tk.Label(root, text="tools.exprtest.tab") # a nested key, as T() leaks it
check("GUI: the translation check sees a raw key in the widget tree",
"tools.exprtest.tab" in leaked_keys(root))
_leak.destroy()
# -- tick loop + widget refreshers -------------------------------------------
# Every page and every sub-tab, read from the page registry - this loop used to
# name three pages, so a fourth would have been built and never ticked here. The
# Tools tab builds a panel when its tab is first shown, so the translation check
# is repeated after the walk: those panels did not exist at the first one.
from beantester.gui.pages import PAGES # noqa: E402
app._tick()
for page_def in PAGES:
app.select_page(page_def.id)
page = app.pages[page_def.id]
for sub_id, _label in getattr(page, "SUBPAGES", ()):
page.select(sub_id)
app._tick()
app._tick()
app.select_page(PAGES[0].id) # where the steps below have always started
app._tick()
check("GUI: _tick() runs on every page and sub-tab (stats, chart, tables, tools)", True)
check("GUI: every page and sub-tab is translated once it has been shown",
not leaked_keys(root), f"({sorted(set(leaked_keys(root)))[:8]})")
# -- worker-thread logging goes through the queue, never straight to widgets --
import threading as _threading # noqa: E402
_t = _threading.Thread(target=lambda: app.log("worker-thread line"))
_t.start(); _t.join()
check("GUI: worker-thread log is queued, not applied immediately",
not any("worker-thread line" in l for l in app._log_lines))
app._tick()
check("GUI: queued worker-thread log appears after a main-thread tick",
any("worker-thread line" in l for l in app._log_lines))
# -- settings round-trip through the widgets ---------------------------------
# the form starts on "Perfect network": nothing is impaired until the user says so
s = app._settings_from_widgets()
check("GUI: the form starts on a perfect link",
s["latency"] == 0.0 and s["jitter"] == 0.0 and s["loss"] == 0.0
and s["filter"] == "both",
f"(lat={s['latency']}, loss={s['loss']}, filter={s['filter']})")
app.vars["latency"].set("100")
s = app._settings_from_widgets()
check("GUI: settings read from widgets", s["latency"] == 100.0, f"(lat={s['latency']})")
app.vars["latency"].set("0")
# -- translated exception from a widget field --------------------------------
app.loss_var.set("abc")
try:
app._settings_from_widgets()
check("GUI: invalid field raises", False)
except ValueError as e:
check("GUI: invalid field raises a translated (PL) message",
"musi być liczbą" in str(e) and "Utrata" in str(e), f"({e})")
app.loss_var.set("250")
try:
app._settings_from_widgets()
check("GUI: out-of-range field raises", False)
except ValueError as e:
check("GUI: out-of-range field raises a translated range error",
"zakresie" in str(e), f"({e})")
app.loss_var.set("1")
# -- language switch (PL -> EN) keeps settings and rebuilds ------------------
app.down_var.set("512")
app.lang_var.set("English")
app._switch_language()
check("GUI: language switch rebuilds into English", app._lang == "en")
check("GUI: settings survive the language switch", float(app.down_var.get()) == 512.0,
f"(down={app.down_var.get()})")
check("GUI: no raw i18n keys after the language switch", not leaked_keys(root),
f"({sorted(set(leaked_keys(root)))[:8]})")
try:
app.loss_var.set("abc")
app._settings_from_widgets()
except ValueError as e:
check("GUI: exception now in English", "must be a number" in str(e), f"({e})")
app.loss_var.set("1")
# -- switch back to Polish ----------------------------------------------------
app.lang_var.set("Polski")
app._switch_language()
check("GUI: switch back to Polish", app._lang == "pl")
# -- profiles: scope warning, reserved names ----------------------------------
# (the app uses its own themed dialogs, not the native white message boxes)
import beantester.gui.dialogs as _dlg # noqa: E402
warnings, errors = [], []
_dlg.show_warning = lambda parent, title, msg: warnings.append((title, msg))
_dlg.show_error = lambda parent, title, msg: errors.append((title, msg))
app.profiles.persist = lambda: None # no file writes from the smoke run
app.rst_var.set("5") # active setting outside profile scope
_dlg.ask_string = lambda *a, **k: None # user cancels the name dialog
app._save_profile()
check("GUI: profile save warns about out-of-scope settings",
len(warnings) == 1 and app._short_label("fields.rst") in warnings[0][1],
f"({warnings})")
check("GUI: cancelled name dialog saves nothing", not app.profiles.names())
app.rst_var.set("0")
warnings.clear()
_dlg.ask_string = lambda *a, **k: n.T("presets.terrible") # preset name (PL)
app._save_profile()
check("GUI: no scope warning when only profile fields are set", not warnings)
check("GUI: preset name is rejected for a user profile",
len(errors) == 1 and not app.profiles.names(), f"({errors})")
_dlg.ask_string = lambda *a, **k: "Moje VPN"
app._save_profile()
check("GUI: valid profile name is saved", app.profiles.names() == ["Moje VPN"])
check("GUI: the picker offers presets then own profiles, nothing else",
app._profile_names() == [n.T(k) for k in n.PRESETS] + ["Moje VPN"],
f"({app._profile_names()[-3:]})")
# -- preset only fills the form; it never applies by itself -------------------
app.profile_var.set(n.T("presets.terrible"))
app._load_selected_profile()
check("GUI: picking a preset fills the form",
float(app.loss_var.get()) == 10.0 and float(app.lat_var.get()) == 300.0,
f"(loss={app.loss_var.get()}, lat={app.lat_var.get()})")
# -- CSV export: stale header rotates the old file, never misaligns rows ------
import csv as _csv # noqa: E402
import tempfile as _tempfile # noqa: E402
import beantester.gui.csv_export as _appmod # noqa: E402
_tmpdir = _tempfile.mkdtemp()
_appmod.CSV_FILE = os.path.join(_tmpdir, "stats.csv")
app._export_csv()
app._export_csv() # same header -> plain append
with open(_appmod.CSV_FILE, newline="", encoding="utf-8") as _f:
_rows = list(_csv.reader(_f))
# `capture_narrowed` sits between the timestamp and the counters: it records which
# world each row's numbers were measured in (csv_export.CSV_SESSION_COLUMNS).
_SCOPE_HEAD = ["time", "capture_narrowed", "packets_seen"]
check("GUI: CSV append keeps a single current header",
len(_rows) == 3 and _rows[0][:3] == _SCOPE_HEAD, f"({_rows[:1]})")
with open(_appmod.CSV_FILE, "w", encoding="utf-8") as _f:
_f.write("time,old_col\n1,2\n") # legacy column layout
app._export_csv()
_rotated = [x for x in os.listdir(_tmpdir) if x != "stats.csv"]
with open(_appmod.CSV_FILE, newline="", encoding="utf-8") as _f:
_first = next(_csv.reader(_f), [])
check("GUI: CSV with a stale header is rotated aside and restarted",
len(_rotated) == 1 and _first[:3] == _SCOPE_HEAD, f"(rotated={_rotated})")
# -- no raw translation keys or ASCII-Polish leaking into the log -------------
joined = "\n".join(app._log_lines)
leaks = [w for w in ("tips.", "frames.", "buttons.", "zaklocen", "Jezyk") if w in joined]
check("GUI: log has no raw keys / legacy ASCII-Polish", not leaks, f"({leaks})")
print()
print(f"GUI smoke: {'OK' if not fails else f'{len(fails)} FAIL'}")
sys.exit(1 if fails else 0)