"""Exercise Qt and backend shutdown with no device transport in the UI.""" import sys import os import time from station import runtime_client as runtime runtime.start(offline=True) import station.operator_app as ui app=ui.QApplication.instance();app.setFont(ui.QFont('Noto Sans SC',11));app.setStyleSheet(ui._STYLE) window=ui.StationApp();window.resize(1440,900);window.show() window._apply_theme('dark',persist=False) assert app.property('darkTheme') is True assert window._theme_selector.currentData()=='dark' window._apply_theme('light',persist=False) assert app.property('darkTheme') is False # A first status message and its expiry must not resize the dashboard. app.processEvents() content_before=window.centralWidget().geometry() window.statusBar().showMessage('刷新相机 · 状态提示 '*30) app.processEvents() assert window.centralWidget().geometry()==content_before window.statusBar().clearMessage();app.processEvents() assert window.centralWidget().geometry()==content_before assert window._theme_selector.currentData()=='light' assert 'rclpy' not in sys.modules assert 'serial' not in sys.modules assert 'station.operator_console' not in sys.modules assert set(window._pages) == {'main','calibration','settings','ros_monitor'} # Offline display uses the configured HOME, while real feedback always wins. assert window._home_poses[window._arm] != [0.]*7 assert window._arm_sketch._pose_key[0] == tuple(round(q,3) for q in window._home_poses[window._arm]) assert not window._arm_sketch._live from unittest.mock import patch live_snap=dict(window._snap) live_snap['arm_live']={window._arm:{'pose_live':True,'q':[.12]*7}} with patch.object(ui,'status',return_value=live_snap): window._refresh() assert window._arm_sketch._pose_key[0]==(.12,)*7 assert window._arm_sketch._live window._refresh() # Calibration follows arm/mode and shares the existing camera preview path. from unittest.mock import patch window._show_page('calibration') window._snap['camera_ownership']=[{'id':cam['id'],'owned':True} for cam in ui.camera_view.targets()] with patch.object(ui.camera_view, 'watch') as watch: window._calibration_mode.setCurrentIndex(0) window._sync_camera_watch() wrist=window._calibration_camera() assert wrist and wrist['arm']==window._arm assert wrist['id']+'_color' in watch.call_args.args[0] window._calibration_mode.setCurrentIndex(1) outside=window._calibration_camera() assert outside and not outside.get('arm') assert outside['id']+'_color' in watch.call_args.args[0] window._calibration_mode.setCurrentIndex(0) window._show_page('main') # A photo must preserve the current RGB frame; no capture/device operation is needed. import tempfile from unittest.mock import patch sample=bytes([12,84,192])*(32*18) selected=next(cam for _page,cam,_label in window._camera_labels if cam.get('arm')==window._arm) with tempfile.TemporaryDirectory() as pictures: with patch.object(ui.Path,'home',return_value=ui.Path(pictures)), patch.object(ui.camera_view,'previews',return_value={selected['id']+'_color':(sample,32,18)}): window._take_photo() photos=list(ui.Path(pictures).rglob('*.png')) assert len(photos)==1 image=ui.QImage(str(photos[0])) assert (image.width(),image.height())==(32,18) assert image.pixelColor(0,0).getRgb()[:3]==(12,84,192) window._open_camera(selected['id']) assert selected['id'] in window._camera_windows window._camera_windows[selected['id']].close() assert not window._camera_windows # Device controls only submit explicit actions; changing settings never writes to hardware. assert not hasattr(window,'_grip_force') and not hasattr(window,'_grip_target') assert window._gripper_probe_button.text()=='连接 / 探测' assert window._sensor_refresh_button.text()=='刷新传感器' assert window._sensor_reset_button.text()=='重置底图' assert window._sensor_reset_button.size()==window._sensor_refresh_button.size() assert window._gripper_probe_button.size()==window._sensor_refresh_button.size() assert window._gripper_probe_button.height()==28 original_arm=window._arm window._set_arm('91') assert window._gripper_sketch._cad_static and window._gripper_sketch._gl_count>0 assert window._gripper_sketch._caption()=='' window._set_arm(original_arm) assert not window._gripper_sketch._cad_static assert window._gripper_sketch._caption()=='' from PySide6.QtCore import QByteArray,QBuffer,QIODevice sample_image=ui.QImage(32,32,ui.QImage.Format.Format_RGB888);sample_image.fill(ui.QColor('#1683ff')) encoded=QByteArray();buffer=QBuffer(encoded);buffer.open(QIODevice.OpenModeFlag.WriteOnly) assert sample_image.save(buffer,'JPG');buffer.close() fake_snap=dict(window._snap) fake_snap['backend']={**fake_snap.get('backend',{}),'error':''} fake_snap['arm_live']={original_arm:{'tcp_pose':[500.,120.,325.,179.2,.5,88.1]}} fake_snap['tactile']={'left':{'connected':True,'fps':29.,'fnormal':12000.,'fshearx':20000.,'fsheary':-10000., 'force_n':{'fnormal':.477,'fshearx':.65,'fsheary':-.31}}} fake_snap['history_n']={'left':[{'t':time.time()-.1,'fnormal':.477,'fshearx':.65,'fsheary':-.31}]} with patch.object(ui,'status',return_value=fake_snap),patch.object(ui.runtime.tactile_view,'frame',return_value=bytes(encoded)): window._refresh();window._paint_images() assert [v.text() for v in window._tcp_values]==['500.0','120.0','325.0','179.2','0.5','88.1'] assert '(29fps)' in window._flow_slots[0][0].text() assert '·' not in window._flow_slots[0][0].text() assert '当前法向力 0.48N' in window._flow_slots[0][0].text() assert not hasattr(window,'_flow_details') assert window._flow_axis_charts['left']['x']._samples[-1]['fshearx']==.65 assert window._flow_axis_charts['left']['y']._samples[-1]['fsheary']==-.31 assert window._flow_axis_charts['left']['x']._scale==window._flow_axis_charts['left']['y']._scale==10. assert window._flow_axis_charts['left']['x'].MAX_SAMPLES==150 app.processEvents() for triplet in window._flow_triplets.values(): rectangles=[item.geometry() for item in triplet.items] assert all(rect.width()==rect.height() for rect in rectangles),rectangles assert len({rect.width() for rect in rectangles})==1,rectangles assert len({rect.y() for rect in rectangles})==1,rectangles assert not window._flow_slots[0][1]._source.isNull() # Recent samples are spread across a shorter window without altering source data. chart=window._flow_axis_charts['left']['x'] history=[{'seq':i,'fshearx':.2,'fsheary':-.2} for i in range(1800)] chart.set_samples(history) assert len(chart._samples)==150 and chart._samples[0]['seq']==1650 assert history[-1]['fshearx']==.2 assert ui._display_force(.299)==ui._display_force(-.299)==0. assert ui._display_force(.3)==.3 and ui._display_force(-.3)==-.3 assert window._flow_slots[0][0].textFormat()==ui.Qt.TextFormat.PlainText title=window._flow_slots[0][0] triplet=window._flow_triplets['left'] assert title.geometry().top()>=triplet.geometry().bottom() fake_snap['history_n']['left'][-1]['fnormal']=.25 window._refresh();window._paint_images() assert '当前法向力 0N' in title.text() errors=[];ticks=[] duration=float(os.environ.get("ORISYS_GUI_TEST_SECONDS","2.2")) started=time.monotonic() frames=[bytes([v,40,80])*(320*180) for v in (30,70,120)] def tick(): ticks.append(time.monotonic()) if duration>10: if len(ticks)%100==0: window._show_page(("main","calibration")[(len(ticks)//100)%2]) for key in ui.camera_view._PREVIEW_WANT: ui.camera_view._PREVIEW[key]=(frames[len(ticks)%3],320,180) ui.camera_view._META[key]=(320,180,20.) original=sys.excepthook def fail(*args): errors.append(str(args[1]));original(*args) sys.excepthook=fail beat=ui.QTimer();beat.timeout.connect(tick);beat.start(50) # The GUI must not serialize read-only requests behind a long arm operation. parallel=[] def fake_command(action,aid,*args,**kwargs): parallel.append((action,time.monotonic())) if action=='stage_track': time.sleep(.65) parallel.append(('motion_finished',time.monotonic())) return {'ok':True,'message':'isolated test'} ui.run_action=fake_command ui.stop_started=lambda *args:(parallel.append(('stop',time.monotonic())),{'ok':True,'message':'isolated stop'})[1] ui.QTimer.singleShot(220,lambda:window._ask('stage_track')) ui.QTimer.singleShot(260,lambda:window._ask('handeye_show')) ui.QTimer.singleShot(280,lambda:window._ask('stage_track')) ui.QTimer.singleShot(300,window._stop_arm) ui.QTimer.singleShot(200,lambda:window._start(lambda:(time.sleep(.7),{'ok':True,'message':'test'})[1],'test')) ui.QTimer.singleShot(500,lambda:window._show_page('calibration')) ui.QTimer.singleShot(1000,lambda:window._show_page('calibration')) ui.QTimer.singleShot(1500,lambda:window._show_page('ros_monitor')) ui.QTimer.singleShot(1900,lambda:window._show_page('main')) ui.QTimer.singleShot(round(duration*1000),window.close) ui.QTimer.singleShot(round((duration+15)*1000),app.quit) app.exec() assert len(ticks)>20,len(ticks) assert [name for name,_ in parallel].count('stage_track')==1,parallel assert dict(parallel)['handeye_show']