test: update smoke and regression suites for dual-renderer paths
Offscreen harnesses now resize+show before load, use per-instance webengine profile dirs and explicit exit codes; compaction and bash stream suites follow the renamed internals.
This commit is contained in:
+189
-55
@@ -1,75 +1,209 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""问题 3 取证:右侧栏 bash 层「参数/输出」框的滚动条 QSS 现状(离屏截图 + 几何/样式打印)"""
|
||||
"""P2-02 诊断:右侧 Bash 面板滚动条与横纵交汇角(离屏)
|
||||
|
||||
测量并断言:
|
||||
S1 代码框(#bl_code)横滚动条实际厚度 = 8px、sizeHint 一致
|
||||
S2 代码框竖滚动条实际厚度 = 8px、sizeHint 一致
|
||||
S3 section 滚动区(#bl_scroll)竖滚动条实际厚度 = 8px
|
||||
S4 面板滚动条箭头 extent = 0(箭头隐藏)
|
||||
S5 交汇角像素 = 代码框背景 #fbfcfe(无原生亮色 corner 方块)
|
||||
S6 无泄漏:未命名 QPlainTextEdit 的滚动条仍是原生口径(≠8px、箭头>0)
|
||||
S7 无泄漏:附件预览滚动条保持自身 6px 口径
|
||||
并生成局部截图(面板全貌 + 代码框角落放大)打印测量值。
|
||||
|
||||
运行: QT_QPA_PLATFORM=offscreen python tests/diag_panel_scrollbar.py
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = "--disable-gpu"
|
||||
os.environ.setdefault("HAOCODE_RENDER", "software")
|
||||
os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = "--disable-gpu" # 绕过 AMD 核显 context lost
|
||||
os.environ.setdefault("PYTHONIOENCODING", "utf-8")
|
||||
|
||||
import core.db_manager as _dbm # noqa: E402
|
||||
_dbm._DEFAULT_DB = os.path.join(tempfile.gettempdir(), f"haocode_q3_{os.getpid()}.db")
|
||||
_cfg = os.path.join(tempfile.gettempdir(), f"haocode_q3_{os.getpid()}.json")
|
||||
open(_cfg, "w", encoding="utf-8").write('{"providers": {}}')
|
||||
os.environ["HAOCODE_CONFIG_FILE"] = _cfg
|
||||
# 铁律(P0-01):临时数据库 + 临时配置必须在 import MainWindow 之前完成
|
||||
from tests._test_env import isolate # noqa: E402
|
||||
_TMP = isolate("panelscroll", config={"providers": {}, "mode_switch": True})
|
||||
_DB_TMP = _TMP["db"]
|
||||
_CFG_TMP = _TMP["config"]
|
||||
|
||||
from PyQt6.QtWidgets import QApplication # noqa: E402
|
||||
from PyQt6.QtWidgets import (QApplication, QPlainTextEdit, QStyle, # noqa: E402
|
||||
QStyleOptionSlider) # noqa: E402
|
||||
from PyQt6.QtTest import QTest # noqa: E402
|
||||
from PyQt6.QtCore import Qt # noqa: E402
|
||||
from ui.views.main_window import MainWindow # noqa: E402
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
w = MainWindow()
|
||||
w.resize(1400, 950)
|
||||
w.show()
|
||||
for _ in range(40):
|
||||
app.processEvents()
|
||||
QTest.qWait(20)
|
||||
ok = True
|
||||
OUT_DIR = os.path.join(os.path.dirname(__file__), "..", "docs", "agent-handoff", "evidence")
|
||||
|
||||
p = w.bash_panel
|
||||
p.expand_btn.click()
|
||||
for _ in range(30):
|
||||
app.processEvents()
|
||||
QTest.qWait(20)
|
||||
|
||||
# 造一个「已完成 + 长输出」的层(长输出会把垂直/水平滚动条都逼出来)
|
||||
long_cmd = "python train.py --epochs 300 --batch-size 64 --lr 0.0001 --data /data/detr/annotations.json"
|
||||
long_out = "\n".join(
|
||||
f"[{i:04d}] epoch loss=0.{i % 9}{i % 7} lr=0.0001 mAP=0.{40 + i % 20} "
|
||||
f"very_long_tail_to_force_horizontal_scrollbar_{i}"
|
||||
for i in range(120))
|
||||
p.on_started("q3_lay", "bash", {"command": long_cmd})
|
||||
p.on_finished("q3_lay", "bash", True, long_out)
|
||||
for _ in range(20):
|
||||
app.processEvents()
|
||||
QTest.qWait(20)
|
||||
def check(name, cond, extra=""):
|
||||
global ok
|
||||
print((" PASS " if cond else " FAIL ") + name + ("" if cond else f" {extra}"), flush=True)
|
||||
if not cond:
|
||||
ok = False
|
||||
|
||||
lay = p._layers["q3_lay"]
|
||||
lay.toggle() # 展开层 → 显示「参数」「输出」两块
|
||||
for _ in range(20):
|
||||
app.processEvents()
|
||||
QTest.qWait(20)
|
||||
|
||||
for nm, box in (("参数 arg_box", lay.arg_box), ("输出 out_box", lay.out_box)):
|
||||
vb = box.verticalScrollBar()
|
||||
hb = box.horizontalScrollBar()
|
||||
print(f"--- {nm}")
|
||||
print(f" objectName={box.objectName()} 尺寸={box.width()}x{box.height()}")
|
||||
print(f" 垂直滚动条: 可见={vb.isVisible()} 宽={vb.width()} 需要={vb.maximum() > 0}")
|
||||
print(f" 水平滚动条: 可见={hb.isVisible()} 高={hb.height()} 需要={hb.maximum() > 0}")
|
||||
print(f" box 自身 stylesheet = {box.styleSheet()!r}")
|
||||
print(f" box 背景角色 = {box.palette().base().color().name()}")
|
||||
def settle(ms=300):
|
||||
for _ in range(int(ms / 20) + 1):
|
||||
app.processEvents()
|
||||
QTest.qWait(20)
|
||||
|
||||
img = lay.grab().toImage()
|
||||
out = os.path.join(os.path.dirname(os.path.abspath(__file__)), "_tmp_q3_layer.png")
|
||||
img.save(out)
|
||||
print("\n层截图 ->", out, img.width(), "x", img.height())
|
||||
|
||||
# 再抓取面板整块(看滚动条在面板里的观感)
|
||||
img2 = p.grab().toImage()
|
||||
out2 = os.path.join(os.path.dirname(os.path.abspath(__file__)), "_tmp_q3_panel.png")
|
||||
img2.save(out2)
|
||||
print("面板截图 ->", out2, img2.width(), "x", img2.height())
|
||||
def arrow_extent(sb, orient=Qt.Orientation.Vertical):
|
||||
"""滚动条箭头子控件(sub-line)的实际尺寸(px):QSS 把 add-line/sub-line 置 0 后应为 0。
|
||||
用 sb.style()(样式表代理风格)才能反映 QSS 效果;PyQt6 参数序 = (cc, opt, sc, widget)。
|
||||
返回 -2 表示无法测量(样式代理缺失等),调用方不得把 -2 当作 0。"""
|
||||
try:
|
||||
st = sb.style()
|
||||
if orient == Qt.Orientation.Vertical:
|
||||
return st.subControlRect(QStyle.ComplexControl.CC_ScrollBar,
|
||||
QStyleOptionSlider(),
|
||||
QStyle.SubControl.SC_ScrollBarSubLine, sb).height()
|
||||
return st.subControlRect(QStyle.ComplexControl.CC_ScrollBar,
|
||||
QStyleOptionSlider(),
|
||||
QStyle.SubControl.SC_ScrollBarSubLine, sb).width()
|
||||
except Exception as e:
|
||||
print(f" (arrow_extent 测量异常: {e})", flush=True)
|
||||
return -1
|
||||
|
||||
os.remove(_cfg)
|
||||
|
||||
window = MainWindow()
|
||||
window.resize(1400, 800)
|
||||
window.show()
|
||||
settle(400)
|
||||
panel = window.bash_panel
|
||||
panel.expand_btn.click()
|
||||
settle(500)
|
||||
|
||||
# ---- 造数据:一个展开的层,out_box 同时触发横/纵滚动条 ----
|
||||
panel.on_started("sc1", "bash", {"command": "python long_report.py --all --verbose"})
|
||||
panel.on_finished("sc1", "bash", True,
|
||||
"$ python long_report.py --all --verbose\n"
|
||||
+ "\n".join(f"line-{i:03d}" for i in range(60))
|
||||
+ "\n[exit 0] (1.2s)")
|
||||
settle(300)
|
||||
lay = panel._layers["sc1"]
|
||||
lay.toggle()
|
||||
settle(200)
|
||||
# 超宽单行(NoWrap)→ 横向滚动条;再追加 60 行 → 同时触发纵向滚动条
|
||||
panel._layers["sc1"].out_box.setPlainText("X" * 3000 + "\n"
|
||||
+ "\n".join(f"tail-{i:03d}" for i in range(60)))
|
||||
settle(200)
|
||||
|
||||
ob = lay.out_box
|
||||
sbh = ob.horizontalScrollBar()
|
||||
sbv = ob.verticalScrollBar()
|
||||
check("S0.1 前置:横滚动条可见(超宽单行)", sbh.isVisible(), str(sbh.isVisible()))
|
||||
check("S0.2 前置:竖滚动条可见(60+ 行超出 230px 上限)", sbv.isVisible(), str(sbv.isVisible()))
|
||||
|
||||
# ---- S1/S2 代码框滚动条厚度 ----
|
||||
check("S1 代码框横滚动条实际厚度 = 8px", sbh.height() == 8, f"h={sbh.height()}")
|
||||
check("S1b 代码框横滚动条 sizeHint 厚 = 8px", sbh.sizeHint().height() == 8,
|
||||
f"{sbh.sizeHint().height()}")
|
||||
check("S2 代码框竖滚动条实际厚度 = 8px", sbv.width() == 8, f"w={sbv.width()}")
|
||||
check("S2b 代码框竖滚动条 sizeHint 宽 = 8px", sbv.sizeHint().width() == 8,
|
||||
f"{sbv.sizeHint().width()}")
|
||||
|
||||
# ---- S3 section 滚动区竖滚动条厚度 ----
|
||||
# 让运行中栏溢出:再加 12 个已完成层(复用 P11 的溢出手法不必,层数即可)
|
||||
for i in range(12):
|
||||
cid = f"scf{i}"
|
||||
panel.on_started(cid, "bash", {"command": f"cmd-{cid}"})
|
||||
panel.on_finished(cid, "bash", True, f"$ cmd-{cid}\nok\n[exit 0] (0.1s)")
|
||||
settle(300)
|
||||
sdv = panel.sec_done.scroll.verticalScrollBar()
|
||||
check("S0.3 前置:已完成栏溢出、竖滚动条可见", sdv.isVisible(), str(sdv.isVisible()))
|
||||
check("S3 section 竖滚动条实际厚度 = 8px", sdv.width() == 8, f"w={sdv.width()}")
|
||||
|
||||
# ---- S4 箭头 extent ----
|
||||
print(f" 测量:代码框 H 箭头 extent = {arrow_extent(sbh, Qt.Orientation.Horizontal)}px, "
|
||||
f"V = {arrow_extent(sbv)}px; section V = {arrow_extent(sdv)}px",
|
||||
flush=True)
|
||||
check("S4 代码框横滚动条箭头 extent = 0", arrow_extent(sbh, Qt.Orientation.Horizontal) == 0,
|
||||
str(arrow_extent(sbh, Qt.Orientation.Horizontal)))
|
||||
check("S4b 代码框竖滚动条箭头 extent = 0", arrow_extent(sbv) == 0, str(arrow_extent(sbv)))
|
||||
check("S4c section 竖滚动条箭头 extent = 0", arrow_extent(sdv) == 0, str(arrow_extent(sdv)))
|
||||
|
||||
# ---- S5 交汇角(render 到透明 pixmap 取样;offscreen 下文档区背景不填充,
|
||||
# 但样式子控件(滚动条/边框/corner)正常渲染,可直接断言)----
|
||||
from PyQt6.QtGui import QPainter, QPixmap # noqa: E402
|
||||
w, h = ob.width(), ob.height()
|
||||
pm = QPixmap(w, h); pm.fill(Qt.GlobalColor.transparent)
|
||||
pr = QPainter(pm); ob.render(pr); pr.end()
|
||||
img = pm.toImage()
|
||||
def px_at(x, y):
|
||||
c = img.pixelColor(x, y)
|
||||
return (c.red(), c.green(), c.blue())
|
||||
CORNER_BG = (0xfb, 0xfc, 0xfe) # #bl_code 背景色 = QSS 里 corner 规则的目标色
|
||||
n_bg = n_white = 0
|
||||
for dy in range(8):
|
||||
for dx in range(8):
|
||||
c = px_at(w - 1 - dx, h - 1 - dy)
|
||||
if c == CORNER_BG:
|
||||
n_bg += 1
|
||||
if c == (255, 255, 255):
|
||||
n_white += 1
|
||||
# 健全性:样式确已作用到该框(文本色 #243043 与 handle #d0d0d0 应在渲染图中出现)
|
||||
n_text = sum(1 for y in range(0, h, 2) for x in range(0, w, 2)
|
||||
if px_at(x, y) == (0x24, 0x30, 0x43))
|
||||
n_handle = sum(1 for y in range(h - 10, h) for x in range(0, w - 12, 2)
|
||||
if px_at(x, y) == (0xd0, 0xd0, 0xd0))
|
||||
print(f" 测量:corner 8x8 内 #fbfcfe 像素 = {n_bg},亮白(255,255,255)像素 = {n_white},"
|
||||
f"文本色像素 = {n_text},handle色像素 = {n_handle}", flush=True)
|
||||
check("S5 健全性:样式已作用于该框(文本色出现)", n_text > 0, f"n_text={n_text}")
|
||||
check("S5b 健全性:handle #d0d0d0 出现在横滚动条带", n_handle > 0, f"n_handle={n_handle}")
|
||||
check("S5c 交汇角渲染出 #fbfcfe(= 代码框背景,::corner 规则生效)",
|
||||
n_bg >= 1, f"n_bg={n_bg}")
|
||||
check("S5d 交汇角无原生亮白方块(255,255,255)", n_white == 0, f"n_white={n_white}")
|
||||
img.save(os.path.join(OUT_DIR, "p2-02-outbox-render.png"))
|
||||
|
||||
# ---- S6 无泄漏:未命名 QPlainTextEdit 仍为原生口径 ----
|
||||
probe = QPlainTextEdit()
|
||||
probe.setPlainText("Y" * 3000)
|
||||
probe.resize(200, 120)
|
||||
probe.show()
|
||||
settle(150)
|
||||
psb = probe.horizontalScrollBar()
|
||||
probe_sb_extent = arrow_extent(psb, Qt.Orientation.Horizontal)
|
||||
check("S6 未命名代码框横滚动条非面板口径(原生厚≠8 或 有箭头)",
|
||||
(psb.height() != 8) or probe_sb_extent > 0,
|
||||
f"h={psb.height()} extent={probe_sb_extent}")
|
||||
probe.close()
|
||||
|
||||
# ---- S7 无泄漏:附件预览滚动条保持自身 6px 口径 ----
|
||||
att_sb = window.attachment_scroll_area.horizontalScrollBar()
|
||||
att_hint_h = att_sb.sizeHint().height()
|
||||
check("S7 附件预览横滚动条保持 6px(自身 QSS 未被面板规则覆盖)",
|
||||
att_hint_h == 6, f"sizeHint.h={att_hint_h}")
|
||||
|
||||
# ---- 截图(局部:面板全貌 + 代码框角落放大 4x)----
|
||||
# 注意:offscreen 下 ob.grab() 的文档区不填充(黑图),角落放大图从 S5 的
|
||||
# render 图(样式子控件已正常渲染)裁出,才有证据价值
|
||||
os.makedirs(OUT_DIR, exist_ok=True)
|
||||
panel_path = os.path.join(OUT_DIR, "p2-02-panel.png")
|
||||
panel.grab().save(panel_path)
|
||||
crop = img.copy(max(0, w - 60), max(0, h - 60), 60, 60)
|
||||
scaled = crop.scaled(240, 240,
|
||||
Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.FastTransformation)
|
||||
corner_path = os.path.join(OUT_DIR, "p2-02-codebox-corner-4x.png")
|
||||
scaled.save(corner_path)
|
||||
print(f" 截图:{panel_path}", flush=True)
|
||||
print(f" 截图:{corner_path}(代码框右下角 60x60 → 4x)", flush=True)
|
||||
|
||||
# ---- 收尾测量汇总 ----
|
||||
print(f"\n 汇总:代码框 H={sbh.height()}px V={sbv.width()}px | "
|
||||
f"section V={sdv.width()}px | corner #fbfcfe 像素={n_bg} | "
|
||||
f"未命名框 H={psb.height()}px extent={probe_sb_extent} | 附件 H hint={att_hint_h}px",
|
||||
flush=True)
|
||||
|
||||
print("\n===== " + ("ALL PASS" if ok else "HAS FAILURES") + " =====", flush=True)
|
||||
if os.path.exists(_DB_TMP):
|
||||
os.remove(_DB_TMP)
|
||||
if os.path.exists(_CFG_TMP):
|
||||
os.remove(_CFG_TMP)
|
||||
# offscreen 铁律:os._exit 强制收尾(QtWebEngine 子进程可能不回收)
|
||||
os._exit(0 if ok else 1)
|
||||
|
||||
Reference in New Issue
Block a user