-
Notifications
You must be signed in to change notification settings - Fork 20
Expand file tree
/
Copy pathexercise_editor.py
More file actions
637 lines (519 loc) · 20.7 KB
/
Copy pathexercise_editor.py
File metadata and controls
637 lines (519 loc) · 20.7 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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
"""Headless smoke test: build the main window, create a project, open an
EditorWindow, and load a Python file. Flushes out PyQt6 runtime errors in the
editor/project path without manual clicking.
Run with QT_QPA_PLATFORM=offscreen.
"""
import os
import sys
import shutil
import warnings
import faulthandler
faulthandler.enable()
warnings.filterwarnings("ignore")
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import Extensions.qscintilla_compat # noqa: E402, F401
from PyQt6.QtCore import Qt # noqa: E402
from PyQt6.QtWidgets import QApplication, QFileDialog, QMessageBox # noqa: E402
# Make modal dialogs non-blocking so a stray error path can't hang the test.
QMessageBox.warning = staticmethod(lambda *a, **k: None)
QMessageBox.critical = staticmethod(lambda *a, **k: None)
QMessageBox.information = staticmethod(lambda *a, **k: None)
QMessageBox.question = staticmethod(lambda *a, **k: None)
def _ensure_app():
instance = QApplication.instance()
if instance is None:
return QApplication(sys.argv)
return instance
app = _ensure_app()
from Pcode import Pcode # noqa: E402
from Extensions.Projects.Projects import CreateProjectThread # noqa: E402
def make_project(projects_dir):
proj_path = os.path.join(projects_dir, "SmokeTest")
if os.path.exists(proj_path):
shutil.rmtree(proj_path)
thread = CreateProjectThread()
thread.projDataDict = {
"location": projects_dir,
"name": "SmokeTest",
"type": "Desktop Application",
"windowtype": "Console",
"mainscript": "main.py",
"importdir": "",
}
thread.run() # synchronous
if thread.error:
raise RuntimeError("project creation failed: %s" % thread.error)
main_script = os.path.join(proj_path, "src", "main.py")
with open(main_script, "w") as f:
f.write("import os\n\n\ndef hello(name):\n return 'hi ' + name\n\n"
"print(hello('world'))\n")
return proj_path
def main():
win = Pcode()
print("STEP main-window OK:", win.windowTitle())
projects_dir = os.path.abspath(win.useData.appPathDict["projectsdir"])
os.makedirs(projects_dir, exist_ok=True)
proj_path = make_project(projects_dir)
print("STEP project-created OK:", proj_path)
try:
win.loadProject(proj_path, show=True, new=True)
except Exception as err:
import traceback as tb
print("LOADPROJECT FAILED:", err)
tb.print_exc()
raise
print("STEP project-loaded OK")
# Find the EditorWindow we just added and poke the editor a little.
stack = win.projectWindowStack
editor_window = None
for i in range(stack.count()):
w = stack.widget(i)
if hasattr(w, "editorTabWidget"):
editor_window = w
break
if editor_window is None:
raise RuntimeError("no EditorWindow found in stack")
print("STEP editor-window OK")
etw = editor_window.editorTabWidget
editor = etw.getEditor() if hasattr(etw, "getEditor") else None
if editor is not None:
editor.setText("x = 1\n")
print("STEP editor-settext OK, lines:", editor.lines())
exercise_save(etw)
exercise_run(editor_window, win, proj_path)
exercise_completion(editor)
exercise_find_replace(editor_window)
exercise_find_in_files(editor_window, proj_path)
exercise_settings(win)
exercise_library(win)
exercise_project_view(editor_window)
exercise_rope_rename(editor_window, proj_path)
exercise_snippets(win)
exercise_export(proj_path, projects_dir)
exercise_filedialog_enums()
exercise_mouse_events(editor, editor_window)
exercise_command_palette(win)
exercise_themes(win)
exercise_about(win)
exercise_assistant(editor_window, etw)
exercise_tasks(editor_window, etw)
exercise_profiler(editor_window)
exercise_diff(etw)
exercise_color_scheme(win)
exercise_build_profile(editor_window)
exercise_build_freeze(editor_window, win)
exercise_outline(editor_window)
exercise_file_explorer(editor_window)
exercise_bookmarks(editor_window, etw)
exercise_git_panel(editor_window)
exercise_go_to_definition(editor_window, proj_path)
print("ALL OK")
def exercise_save(etw):
"""Exercise the file save path."""
editor = etw.getEditor()
editor.setText("import os\n\n\ndef greet(who):\n return 'hi ' + who\n")
saved = etw.save()
print("STEP editor-save OK, saved:", saved)
def exercise_run(editor_window, win, proj_path):
"""Exercise the run/output path via QProcess against a real interpreter."""
run_widget = editor_window.runWidget
interpreter = sys.executable
# Make sure the run path believes a usable interpreter exists.
win.useData.SETTINGS.setdefault("InstalledInterpreters", [])
if interpreter not in win.useData.SETTINGS["InstalledInterpreters"]:
win.useData.SETTINGS["InstalledInterpreters"].append(interpreter)
run_widget.projectData["DefaultInterpreter"] = interpreter
script = os.path.join(proj_path, "src", "main.py")
run_widget.runModule(script, "main", True, False, "")
finished = run_widget.runProcess.waitForFinished(10000)
# Queued slots (readyRead / finished) are delivered via the event loop, so
# pump it briefly to let stdout drain into the output widget.
for _ in range(20):
app.processEvents()
captured = run_widget.text()
clean_exit = ">>> Exit: 0" in captured
print("STEP editor-run OK, finished:", finished,
"| clean exit:", clean_exit)
def exercise_completion(editor):
"""Exercise the rope-backed autocompletion path synchronously."""
if editor is None:
print("STEP editor-completion SKIPPED (no editor)")
return
from Extensions.CodeEditor import AutoCompletionThread
source = "import os\nos."
thread = AutoCompletionThread()
thread.sourcedir = editor.refactor.root
thread.ropeProject = editor.refactor.getProject()
thread.source = source
thread.offset = len(source)
thread.lineText = "os."
thread.column = 3
results = thread.completions()
n = len(results) if results else 0
print("STEP editor-completion OK, proposals:", n)
def exercise_find_replace(editor_window):
"""Exercise the in-editor find/replace path."""
etw = editor_window.editorTabWidget
sw = editor_window.searchWidget
editor = etw.getEditor()
editor.setText("alpha beta alpha gamma alpha\n")
sw.matchCase = False
sw.matchWholeWord = False
sw.matchRegExp = False
sw.wrapAround = True
sw.findLine.setText("alpha")
sw.find()
sw.findNext()
# Use a replacement that does not contain the search term, otherwise a
# case-insensitive replaceAll would re-match its own output forever.
sw.replaceLine.setText("delta")
sw.replaceAll()
text = etw.getEditor().text()
print("STEP editor-find-replace OK, replacements:", text.count("delta"))
def exercise_find_in_files(editor_window, proj_path):
"""Exercise the find-in-files search across the project source tree."""
fif = editor_window.findInFiles
fif.regExp = False
fif.matchWholeWord = False
fif.matchCase = False
fif.recursive = True
fif.findtextLine.setText("hello")
fif.filterEdit.setText("*.py")
fif.projectBox.setChecked(True)
fif.find()
finished = fif.findThread.wait(10000)
print("STEP editor-find-in-files OK, thread finished:", finished)
def exercise_settings(win):
"""Open the settings dialog and cycle through every tab."""
sw = win.settingsWidget
sw.show()
tabs = sw.settingsTab
for i in range(tabs.count()):
tabs.setCurrentIndex(i)
app.processEvents()
titles = [tabs.tabText(i) for i in range(tabs.count())]
sw.hide()
print("STEP settings-dialog OK, tabs:", titles)
def exercise_library(win):
"""Open the library viewer and exercise its advanced-search thread."""
lib = win.library
lib.show()
app.processEvents()
search = lib.advancedSearch
search.searchLine.setText("def")
search.startSearch()
finished = search.finderThread.wait(10000)
lib.hide()
print("STEP library OK, search finished:", finished)
def exercise_project_view(editor_window):
"""Show the project view and confirm its source tree is populated."""
pv = editor_window.projectManager.projectView
pv.show()
app.processEvents()
pv.hide()
print("STEP project-view OK, type:", type(pv).__name__)
def exercise_rope_rename(editor_window, proj_path):
"""Exercise the rope-backed rename refactor synchronously."""
from Extensions.Refactor.Refactor import RenameThread
refactor = editor_window.editorTabWidget.refactor
project = refactor.ropeProject
mod_path = os.path.join(proj_path, "src", "lib_mod.py")
source = "def old_name():\n return 1\n\n\nold_name()\n"
with open(mod_path, "w") as f:
f.write(source)
project.validate()
offset = source.index("old_name")
thread = RenameThread()
thread.new_name = "new_name"
thread.path = mod_path
thread.ropeProject = project
thread.offset = offset
thread.run() # synchronous
if thread.error is not None:
raise RuntimeError("rope rename failed: %s" % thread.error)
renamed = "new_name" in open(mod_path).read()
print("STEP rope-rename OK, changed files:",
len(thread.changedFiles), "| renamed in source:", renamed)
def exercise_snippets(win):
"""Exercise the snippets manager add/edit/save path."""
sm = win.settingsWidget.snippetEditor
snippet_name = "smoke_snippet.py"
snippet_path = os.path.join(sm.path, snippet_name)
with open(snippet_path, "w") as f:
f.write("")
sm.loadSnippetList()
found = sm.snippetsListWidget.findItems(
snippet_name, Qt.MatchFlag.MatchExactly)
sm.snippetsListWidget.setCurrentItem(found[0])
sm.snippetViewer.setReadOnly(False)
sm.snippetViewer.setPlainText("print('snippet body')\n")
sm.saveSnippet()
saved = "snippet body" in open(snippet_path).read()
if os.path.exists(snippet_path):
os.remove(snippet_path)
print("STEP snippets OK, saved body:", saved)
def exercise_export(proj_path, projects_dir):
"""Exercise the project export (zip archive) thread."""
from Extensions.Projects.ProjectManager.ProjectManager import ExportThread
dest_base = os.path.join(projects_dir, "SmokeExport")
thread = ExportThread()
thread.fileName = dest_base
thread.path = proj_path
thread.run() # synchronous
if thread.error is not None:
raise RuntimeError("export failed: %s" % thread.error)
archive = dest_base + ".zip"
exists = os.path.exists(archive)
if exists:
os.remove(archive)
print("STEP export OK, archive created:", exists)
def exercise_filedialog_enums():
"""Guard the QFileDialog option flags used by the open/browse dialogs.
These are accessed at module/handler scope across Start, ProjectView,
FileExplorer and FindInFiles; an offscreen window never clicks them, so
assert the flattened Qt6 enums resolve and combine.
"""
options = (QFileDialog.Option.DontResolveSymlinks
| QFileDialog.Option.ShowDirsOnly)
assert QFileDialog.AcceptMode.AcceptOpen is not None
assert options is not None
print("STEP filedialog-enums OK")
def exercise_mouse_events(editor, editor_window):
"""Dispatch a synthetic mouse move/double-click through the editor and run
widget handlers.
Catches Qt6 event-API regressions (QMouseEvent.globalPos/x/y/posF removed,
Qt.MidButton alias) that an offscreen window never triggers by clicking.
Only re-raise AttributeError (a removed-API signal); swallow unrelated
runtime errors that depend on real layout/painting.
"""
from PyQt6.QtGui import QMouseEvent
from PyQt6.QtCore import QPointF, QEvent
pt = QPointF(8.0, 8.0)
def make(kind):
return QMouseEvent(kind, pt, pt,
Qt.MouseButton.NoButton,
Qt.MouseButton.NoButton,
Qt.KeyboardModifier.NoModifier)
targets = []
if editor is not None:
# Force the hover path so globalPosition()/pos() are exercised.
try:
editor.useData.SETTINGS["DocOnHover"] = "True"
except Exception:
pass
targets.append((editor, "mouseMoveEvent"))
rw = getattr(editor_window, "runWidget", None)
if rw is not None:
targets.append((rw, "mouseMoveEvent"))
targets.append((rw, "mouseDoubleClickEvent"))
for widget, handler in targets:
fn = getattr(widget, handler, None)
if fn is None:
continue
kind = (QEvent.Type.MouseButtonDblClick
if "Double" in handler else QEvent.Type.MouseMove)
try:
fn(make(kind))
except AttributeError:
raise
except Exception:
pass
print("STEP mouse-events OK")
def exercise_command_palette(win):
"""Build the palette commands, filter them, and run a safe one."""
palette = win.commandPalette
commands = win.buildCommands()
palette.setCommands(commands)
palette._refilter("")
assert palette.listWidget.count() == len(commands)
palette._refilter("thm") # fuzzy: matches "Theme: ..." entries
filtered = palette.listWidget.count()
assert filtered >= 1
# Run the "Go to Library" command then back to Editor (no dialogs).
win.projectSwitcher.setButton("LIBRARY")
win.projectSwitcher.setButton("EDITOR")
print("STEP command-palette OK, commands:", len(commands),
"| filtered:", filtered)
def exercise_themes(win):
"""Apply each theme through the app to flush stylesheet build errors."""
from Extensions import StyleSheet
for name in ("Dark", "System", "Light"):
win.applyTheme(name)
assert len(StyleSheet.globalStyle) > 0
print("STEP themes OK")
def exercise_about(win):
"""Construct the About dialog (external library version table)."""
from Extensions.About import About
dlg = About(win)
dlg.show()
app.processEvents()
rows = dlg.view.widget(0).topLevelItemCount()
dlg.hide()
print("STEP about OK, library rows:", rows)
def exercise_assistant(editor_window, etw):
"""Exercise pyflakes + pep8 checker threads on editor source."""
assistant = editor_window.assistantWidget
editor = etw.getEditor()
editor.setText("import os\nx = 1\n")
assistant.runCheck()
assistant.codeCheckerThread.wait(10000)
assistant.pep8CheckerThread.wait(10000)
for _ in range(10):
app.processEvents()
alerts = assistant.errorView.topLevelItemCount()
pep8_items = assistant.pep8View.topLevelItemCount()
print("STEP assistant OK, pyflakes alerts:", alerts,
"| pep8 items:", pep8_items)
def exercise_tasks(editor_window, etw):
"""Exercise the TODO/FIXME task finder on editor source."""
from Extensions.BottomWidgets.TasksWidget import TaskFinderThread
source = "# TODO: smoke task\n# FIXME: another\npass\n"
etw.getEditor().setText(source)
thread = TaskFinderThread()
thread.findTasks(source)
finished = thread.wait(5000)
print("STEP tasks OK, finished:", finished, "| found:", len(thread.results))
def exercise_profiler(editor_window):
"""Load a cProfile stats file into the profiler tree."""
import cProfile
os.makedirs("temp", exist_ok=True)
prof_path = os.path.join("temp", "smoke_profile")
cProfile.run("sum(range(50))", prof_path)
profiler = editor_window.profiler
profiler.viewProfile(prof_path)
rows = profiler.topLevelItemCount()
print("STEP profiler OK, rows:", rows)
def exercise_diff(etw):
"""Exercise unified diff generation in the diff viewer."""
from Extensions.Diff import DiffWindow
class _TextSource(object):
def __init__(self, text):
self._text = text
def text(self):
return self._text
before = "alpha\nbeta\n"
after = "alpha\ngamma\n"
diff = DiffWindow(
editor=_TextSource(after),
snapShot=_TextSource(before))
changed = diff.generateUnifiedDiff()
lines = diff.lines()
print("STEP diff OK, changed:", changed, "| lines:", lines)
def exercise_color_scheme(win):
"""Open the color-scheme settings tab and load the default Python style."""
cs = win.settingsWidget.colorScheme
cs.show()
app.processEvents()
cs.groupChanged()
app.processEvents()
if cs.schemeNameBox.count() > 0:
cs.updateScheme()
app.processEvents()
cs.hide()
print("STEP color-scheme OK, schemes:", cs.schemeNameBox.count())
def exercise_build_profile(editor_window):
"""Load the cx_Freeze build profile for a Desktop Application project."""
build = editor_window.projectManager.build
if build is None:
print("STEP build-profile SKIPPED (no build widget)")
return
profile = build.buildConfig.load()
assert profile.get("name") or profile.get("base")
print("STEP build-profile OK, keys:", len(profile))
def exercise_build_freeze(editor_window, win):
"""Run cx_Freeze synchronously and verify a build artifact is produced.
Skipped when PCODE_SKIP_BUILD=1 (saves ~30s on local quick runs).
"""
if os.environ.get("PCODE_SKIP_BUILD") == "1":
print("STEP build-freeze SKIPPED (PCODE_SKIP_BUILD=1)")
return
build = editor_window.projectManager.build
if build is None:
print("STEP build-freeze SKIPPED (no build widget)")
return
settings = editor_window.projectData["settings"]
settings["DefaultInterpreter"] = sys.executable
settings["UseVirtualEnv"] = "False"
profile = build.buildConfig.load()
thread = build.buildThread
thread.profile = profile
thread.projectPathDict = editor_window.projectPathDict
thread.projectSettings = settings
thread.useData = win.useData
thread.missing = []
thread.run()
if thread.error is not None:
raise RuntimeError("build freeze failed: %s" % thread.error)
builddir = editor_window.projectPathDict["builddir"]
main_stem = os.path.splitext(
os.path.basename(editor_window.projectPathDict["mainscript"]))[0]
candidates = [main_stem + ".exe", main_stem]
if os.path.isdir(builddir):
names = set(os.listdir(builddir))
artifact = any(name in names for name in candidates)
else:
artifact = False
if not artifact:
raise RuntimeError("build freeze produced no executable in %s" % builddir)
print("STEP build-freeze OK, artifact:", main_stem,
"| missing modules:", len(thread.missing))
def exercise_outline(editor_window):
"""Populate the outline tree from Python source."""
outline = editor_window.outline
etw = editor_window.editorTabWidget
editor = etw.getEditor()
editor.setText("class Smoke:\n def method(self):\n pass\n")
outline.startOutline()
outline.pythonOutlineThread.wait(10000)
for _ in range(10):
app.processEvents()
items = outline.topLevelItemCount()
print("STEP outline OK, top-level items:", items)
def exercise_file_explorer(editor_window):
"""Show the file explorer sidebar tab."""
fe = editor_window.fileExplorer
editor_window.sideBottomTab.setCurrentWidget(fe)
app.processEvents()
model = fe.model()
rows = model.rowCount(fe.rootIndex()) if model is not None else 0
print("STEP file-explorer OK, roots:", rows)
def exercise_bookmarks(editor_window, etw):
"""Toggle a bookmark and refresh the bookmark panel."""
editor = etw.getEditor()
editor.setText("line0\nline1\nline2\n")
editor.toggleBookmark(0, 1)
etw.bookmarksChanged.emit()
for _ in range(10):
app.processEvents()
for i in range(editor_window.bottomStack.count()):
w = editor_window.bottomStack.widget(i)
if w.__class__.__name__ == "BookmarkWidget":
w.load()
count = w.topLevelItemCount()
break
else:
count = 0
print("STEP bookmarks OK, count:", count)
def exercise_git_panel(editor_window):
"""Refresh the read-only git status panel."""
panel = editor_window.gitPanel
panel.refresh()
text = panel.output.toPlainText()
print("STEP git-panel OK, chars:", len(text))
def exercise_go_to_definition(editor_window, proj_path):
"""Exercise rope find-definition on project source."""
etw = editor_window.editorTabWidget
mod_path = os.path.join(proj_path, "src", "lib_def.py")
source = "def target():\n return 1\n\n\ntarget()\n"
with open(mod_path, "w") as f:
f.write(source)
etw.loadfile(mod_path)
editor = etw.getEditor()
editor.setCursorPosition(2, 4)
refactor = editor_window.editorTabWidget.refactor
refactor.findDefinition()
for _ in range(10):
app.processEvents()
print("STEP go-to-definition OK")
if __name__ == "__main__":
main()