Files
h3n3-jmwright-CQ-Editor/tests/test_app.py
jdegenstein 339d871516 Bring up to date with mainline CQ-editor ec81ad9
partial pull for tests/tes_app.py only
2022-10-19 13:27:09 -05:00

1437 lines
39 KiB
Python

from path import Path
import os, sys, asyncio
if sys.platform == 'win32':
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
from multiprocessing import Process
import pytest
import pytestqt
import cadquery as cq
from PyQt5.QtCore import Qt, QSettings
from PyQt5.QtWidgets import QFileDialog, QMessageBox
from cq_editor.__main__ import MainWindow
from cq_editor.widgets.editor import Editor
from cq_editor.cq_utils import export, get_occ_color
code = \
'''import cadquery as cq
result = cq.Workplane("XY" )
result = result.box(3, 3, 0.5)
result = result.edges("|Z").fillet(0.125)'''
code_bigger_object = \
'''import cadquery as cq
result = cq.Workplane("XY" )
result = result.box(20, 20, 0.5)
result = result.edges("|Z").fillet(0.125)
'''
code_show_Workplane = \
'''import cadquery as cq
result = cq.Workplane("XY" )
result = result.box(3, 3, 0.5)
result = result.edges("|Z").fillet(0.125)
show_object(result)
'''
code_show_Workplane_named = \
'''import cadquery as cq
result = cq.Workplane("XY" )
result = result.box(3, 3, 0.5)
result = result.edges("|Z").fillet(0.125)
log('test')
show_object(result,name='test')
'''
code_show_Shape = \
'''import cadquery as cq
result = cq.Workplane("XY" )
result = result.box(3, 3, 0.5)
result = result.edges("|Z").fillet(0.125)
show_object(result.val())
'''
code_debug_Workplane = \
'''import cadquery as cq
result = cq.Workplane("XY" )
result = result.box(3, 3, 0.5)
result = result.edges("|Z").fillet(0.125)
debug(result)
'''
code_multi = \
'''import cadquery as cq
result1 = cq.Workplane("XY" ).box(3, 3, 0.5)
result2 = cq.Workplane("XY" ).box(3, 3, 0.5).translate((0,15,0))
'''
code_nested_top = """import test_nested_bottom
"""
code_nested_bottom = """a=1
"""
code_reload_issue = """wire0 = cq.Workplane().lineTo(5, 5).lineTo(10, 0).close().val()
solid1 = cq.Solid.extrudeLinear(cq.Face.makeFromWires(wire0), cq.Vector(0, 0, 1))
r1 = cq.Workplane(solid1).translate((10, 0, 0))
"""
def _modify_file(code, path="test.py"):
with open(path, "w", 1) as f:
f.write(code)
def modify_file(code, path="test.py"):
p = Process(target=_modify_file, args=(code,path))
p.start()
p.join()
def get_center(widget):
pos = widget.pos()
pos.setX(pos.x()+widget.width()//2)
pos.setY(pos.y()+widget.height()//2)
return pos
def get_bottom_left(widget):
pos = widget.pos()
pos.setY(pos.y()+widget.height())
return pos
def get_rgba(ais):
alpha = ais.Transparency()
color = get_occ_color(ais)
return color.redF(), color.greenF(), color.blueF(), alpha
@pytest.fixture
def main(qtbot,mocker):
mocker.patch.object(QMessageBox, 'question', return_value=QMessageBox.Yes)
win = MainWindow()
win.show()
qtbot.addWidget(win)
editor = win.components['editor']
editor.set_text(code)
debugger = win.components['debugger']
debugger._actions['Run'][0].triggered.emit()
return qtbot, win
@pytest.fixture
def main_clean(qtbot,mocker):
mocker.patch.object(QMessageBox, 'question', return_value=QMessageBox.Yes)
win = MainWindow()
win.show()
qtbot.addWidget(win)
qtbot.waitForWindowShown(win)
editor = win.components['editor']
editor.set_text(code)
return qtbot, win
@pytest.fixture
def main_clean_do_not_close(qtbot,mocker):
mocker.patch.object(QMessageBox, 'question', return_value=QMessageBox.No)
win = MainWindow()
win.show()
qtbot.addWidget(win)
qtbot.waitForWindowShown(win)
editor = win.components['editor']
editor.set_text(code)
return qtbot, win
@pytest.fixture
def main_multi(qtbot,mocker):
mocker.patch.object(QMessageBox, 'question', return_value=QMessageBox.Yes)
mocker.patch.object(QFileDialog, 'getSaveFileName', return_value=('out.step',''))
win = MainWindow()
win.show()
qtbot.addWidget(win)
qtbot.waitForWindowShown(win)
editor = win.components['editor']
editor.set_text(code_multi)
debugger = win.components['debugger']
debugger._actions['Run'][0].triggered.emit()
return qtbot, win
def test_render(main):
qtbot, win = main
obj_tree_comp = win.components['object_tree']
editor = win.components['editor']
debugger = win.components['debugger']
console = win.components['console']
log = win.components['log']
# enable CQ reloading
debugger.preferences['Reload CQ'] = True
# check that object was rendered
assert(obj_tree_comp.CQ.childCount() == 1)
# check that object was removed
obj_tree_comp._toolbar_actions[0].triggered.emit()
assert(obj_tree_comp.CQ.childCount() == 0)
# check that object was rendered usin explicit show_object call
editor.set_text(code_show_Workplane)
debugger._actions['Run'][0].triggered.emit()
assert(obj_tree_comp.CQ.childCount() == 1)
obj_tree_comp._toolbar_actions[0].triggered.emit()
assert(obj_tree_comp.CQ.childCount() == 0)
# check that cq.Shape object was rendered using explicit show_object call
editor.set_text(code_show_Shape)
debugger._actions['Run'][0].triggered.emit()
assert(obj_tree_comp.CQ.childCount() == 1)
obj_tree_comp._toolbar_actions[0].triggered.emit()
assert(obj_tree_comp.CQ.childCount() == 0)
# test rendering via console
console.execute(code_show_Workplane)
assert(obj_tree_comp.CQ.childCount() == 1)
obj_tree_comp._toolbar_actions[0].triggered.emit()
assert(obj_tree_comp.CQ.childCount() == 0)
console.execute(code_show_Shape)
assert(obj_tree_comp.CQ.childCount() == 1)
# check object rendering using show_object call with a name specified and
# debug call
editor.set_text(code_show_Workplane_named)
debugger._actions['Run'][0].triggered.emit()
qtbot.wait(100)
assert(obj_tree_comp.CQ.child(0).text(0) == 'test')
assert('test' in log.toPlainText().splitlines()[-1])
# cq reloading check
obj_tree_comp._toolbar_actions[0].triggered.emit()
assert(obj_tree_comp.CQ.childCount() == 0)
editor.set_text(code_reload_issue)
debugger._actions['Run'][0].triggered.emit()
qtbot.wait(100)
assert(obj_tree_comp.CQ.childCount() == 1)
debugger._actions['Run'][0].triggered.emit()
qtbot.wait(100)
assert(obj_tree_comp.CQ.childCount() == 1)
def test_export(main,mocker):
qtbot, win = main
debugger = win.components['debugger']
debugger._actions['Run'][0].triggered.emit()
#set focus
obj_tree = win.components['object_tree'].tree
obj_tree_comp = win.components['object_tree']
qtbot.mouseClick(obj_tree, Qt.LeftButton)
qtbot.keyClick(obj_tree, Qt.Key_Down)
qtbot.keyClick(obj_tree, Qt.Key_Down)
#export STL
mocker.patch.object(QFileDialog, 'getSaveFileName', return_value=('out.stl',''))
obj_tree_comp._export_STL_action.triggered.emit()
assert(os.path.isfile('out.stl'))
#export STEP
mocker.patch.object(QFileDialog, 'getSaveFileName', return_value=('out.step',''))
obj_tree_comp._export_STEP_action.triggered.emit()
assert(os.path.isfile('out.step'))
#clean
os.remove('out.step')
os.remove('out.stl')
def number_visible_items(viewer):
from OCP.AIS import AIS_ListOfInteractive
l = AIS_ListOfInteractive()
viewer_ctx = viewer._get_context()
viewer_ctx.DisplayedObjects(l)
return l.Extent()
def test_inspect(main):
qtbot, win = main
#set focus and make invisible
obj_tree = win.components['object_tree'].tree
qtbot.mouseClick(obj_tree, Qt.LeftButton)
qtbot.keyClick(obj_tree, Qt.Key_Down)
qtbot.keyClick(obj_tree, Qt.Key_Down)
qtbot.keyClick(obj_tree, Qt.Key_Space)
#enable object inspector
insp = win.components['cq_object_inspector']
insp._toolbar_actions[0].toggled.emit(True)
#check if all stack items are visible in the tree
assert(insp.root.childCount() == 3)
#check if correct number of items is displayed
viewer = win.components['viewer']
insp.setCurrentItem(insp.root.child(0))
assert(number_visible_items(viewer) == 4)
insp.setCurrentItem(insp.root.child(1))
assert(number_visible_items(viewer) == 7)
insp.setCurrentItem(insp.root.child(2))
assert(number_visible_items(viewer) == 4)
insp._toolbar_actions[0].toggled.emit(False)
assert(number_visible_items(viewer) == 3)
class event_loop(object):
'''Used to mock the QEventLoop for the debugger component
'''
def __init__(self,callbacks):
self.callbacks = callbacks
self.i = 0
def exec_(self):
if self.i<len(self.callbacks):
self.callbacks[self.i]()
self.i+=1
def exit(self,*args):
pass
def patch_debugger(debugger,event_loop_mock):
debugger.inner_event_loop.exec_ = event_loop_mock.exec_
debugger.inner_event_loop.exit = event_loop_mock.exit
def test_debug(main,mocker):
# store the tracing function
trace_function = sys.gettrace()
def assert_func(x):
'''Neddedd to perform asserts in lambdas
'''
assert(x)
qtbot, win = main
#clear all
obj_tree = win.components['object_tree']
obj_tree.toolbarActions()[0].triggered.emit()
editor = win.components['editor']
editor.set_text(code)
debugger = win.components['debugger']
actions = debugger._actions['Run']
run,debug,step,step_in,cont = actions
variables = win.components['variables_viewer']
viewer = win.components['viewer']
assert(number_visible_items(viewer) == 3)
#check breakpoints
assert(debugger.breakpoints == [])
#check _frames
assert(debugger._frames == [])
#test step through
ev = event_loop([lambda: (assert_func(variables.model().rowCount() == 4),
assert_func(number_visible_items(viewer) == 3),
step.triggered.emit()),
lambda: (assert_func(variables.model().rowCount() == 4),
assert_func(number_visible_items(viewer) == 3),
step.triggered.emit()),
lambda: (assert_func(variables.model().rowCount() == 5),
assert_func(number_visible_items(viewer) == 3),
step.triggered.emit()),
lambda: (assert_func(variables.model().rowCount() == 5),
assert_func(number_visible_items(viewer) == 4),
cont.triggered.emit())])
patch_debugger(debugger,ev)
debug.triggered.emit(True)
assert(variables.model().rowCount() == 2)
assert(number_visible_items(viewer) == 4)
#test exit debug
ev = event_loop([lambda: (step.triggered.emit(),),
lambda: (assert_func(variables.model().rowCount() == 1),
assert_func(number_visible_items(viewer) == 3),
debug.triggered.emit(False),)])
patch_debugger(debugger,ev)
debug.triggered.emit(True)
assert(variables.model().rowCount() == 1)
assert(number_visible_items(viewer) == 3)
#test breakpoint
ev = event_loop([lambda: (cont.triggered.emit(),),
lambda: (assert_func(variables.model().rowCount() == 5),
assert_func(number_visible_items(viewer) == 4),
cont.triggered.emit(),)])
patch_debugger(debugger,ev)
editor.debugger.set_breakpoints([(4,None)])
debug.triggered.emit(True)
assert(variables.model().rowCount() == 2)
assert(number_visible_items(viewer) == 4)
#test breakpoint without using singals
ev = event_loop([lambda: (cont.triggered.emit(),),
lambda: (assert_func(variables.model().rowCount() == 5),
assert_func(number_visible_items(viewer) == 4),
cont.triggered.emit(),)])
patch_debugger(debugger,ev)
editor.debugger.set_breakpoints([(4,None)])
debugger.debug(True)
assert(variables.model().rowCount() == 2)
assert(number_visible_items(viewer) == 4)
#test debug() without using singals
ev = event_loop([lambda: (cont.triggered.emit(),),
lambda: (assert_func(variables.model().rowCount() == 5),
assert_func(number_visible_items(viewer) == 4),
cont.triggered.emit(),)])
patch_debugger(debugger,ev)
editor.set_text(code_debug_Workplane)
editor.debugger.set_breakpoints([(4,None)])
debugger.debug(True)
CQ = obj_tree.CQ
# object 1 (defualt color)
r,g,b,a = get_rgba(CQ.child(0).ais)
assert( a == pytest.approx(0.2) )
assert( r == 1.0 )
assert(variables.model().rowCount() == 2)
assert(number_visible_items(viewer) == 4)
# restore the tracing function
sys.settrace(trace_function)
code_err1 = \
'''import cadquery as cq
(
result = cq.Workplane("XY" ).box(3, 3, 0.5).edges("|Z").fillet(0.125)
'''
code_err2 = \
'''import cadquery as cq
result = cq.Workplane("XY" ).box(3, 3, 0.5).edges("|Z").fillet(0.125)
f()
'''
def test_traceback(main):
# store the tracing function
trace_function = sys.gettrace()
qtbot, win = main
editor = win.components['editor']
debugger = win.components['debugger']
traceback_view = win.components['traceback_viewer']
actions = debugger._actions['Run']
run,debug,step,step_in,cont = actions
editor.set_text(code_err1)
run.triggered.emit()
assert('SyntaxError' in traceback_view.current_exception.text())
debug.triggered.emit()
assert('SyntaxError' in traceback_view.current_exception.text())
assert(debug.isChecked() == False)
editor.set_text(code_err2)
run.triggered.emit()
assert('NameError' in traceback_view.current_exception.text())
assert(hasattr(sys, 'last_traceback'))
del sys.last_traceback
assert(not hasattr(sys, 'last_traceback'))
#test last_traceback with debug
ev = event_loop([lambda: (cont.triggered.emit(),)])
patch_debugger(debugger,ev)
debugger.debug(True)
assert('NameError' in traceback_view.current_exception.text())
assert(hasattr(sys, 'last_traceback'))
# restore the tracing function
sys.settrace(trace_function)
@pytest.fixture
def editor(qtbot):
win = Editor()
win.show()
qtbot.addWidget(win)
return qtbot, win
def conv_line_ends(text):
return '\n'.join(text.splitlines())
def test_editor(monkeypatch,editor):
qtbot, editor = editor
with open('test.py','w') as f:
f.write(code)
#check that no text is present
assert(editor.get_text_with_eol() == '')
#check that loading from file works properly
editor.load_from_file('test.py')
assert(len(editor.get_text_with_eol()) > 0)
assert(conv_line_ends(editor.get_text_with_eol()) == code)
#check that loading from file works properly
editor.new()
assert(editor.get_text_with_eol() == '')
#monkeypatch QFileDialog methods
def filename(*args, **kwargs):
return 'test.py',None
def filename2(*args, **kwargs):
return 'test2.py',None
monkeypatch.setattr(QFileDialog, 'getOpenFileName',
staticmethod(filename))
monkeypatch.setattr(QFileDialog, 'getSaveFileName',
staticmethod(filename2))
#check that open file works properly
editor.open()
assert(conv_line_ends(editor.get_text_with_eol()) == code)
#check that save file works properly
editor.new()
qtbot.mouseClick(editor, Qt.LeftButton)
qtbot.keyClick(editor,Qt.Key_A)
assert(editor.document().isModified() == True)
editor.filename = 'test2.py'
editor.save()
assert(editor.document().isModified() == False)
monkeypatch.setattr(QFileDialog, 'getOpenFileName',
staticmethod(filename2))
editor.open()
assert(editor.get_text_with_eol() == 'a')
#check that save as works properly
os.remove('test2.py')
editor.save_as()
assert(os.path.exists(filename2()[0]))
#test persistance
settings = QSettings('test')
editor.saveComponentState(settings)
editor.new()
assert(editor.get_text_with_eol() == '')
editor.restoreComponentState(settings)
assert(editor.get_text_with_eol() == 'a')
#test error handling
os.remove('test2.py')
assert(not os.path.exists('test2.py'))
editor.restoreComponentState(settings)
@pytest.mark.repeat(1)
def test_editor_autoreload(monkeypatch,editor):
qtbot, editor = editor
TIMEOUT = 500
# start out with autoreload enabled
editor.autoreload(True)
with open('test.py','w') as f:
f.write(code)
assert(editor.get_text_with_eol() == '')
editor.load_from_file('test.py')
assert(len(editor.get_text_with_eol()) > 0)
# wait for reload.
with qtbot.waitSignal(editor.triggerRerender, timeout=TIMEOUT):
# modify file - NB: separate process is needed to avoid Widows quirks
modify_file(code_bigger_object)
# check that editor has updated file contents
assert(code_bigger_object.splitlines()[2] in editor.get_text_with_eol())
# disable autoreload
editor.autoreload(False)
# Wait for reload in case it incorrectly happens. A timeout should occur
# instead because a re-render should not be triggered with autoreload
# disabled.
with pytest.raises(pytestqt.exceptions.TimeoutError):
with qtbot.waitSignal(editor.triggerRerender, timeout=TIMEOUT):
# re-write original file contents
modify_file(code)
# editor should continue showing old contents since autoreload is disabled.
assert(code_bigger_object.splitlines()[2] in editor.get_text_with_eol())
# Saving a file with autoreload disabled should not trigger a rerender.
with pytest.raises(pytestqt.exceptions.TimeoutError):
with qtbot.waitSignal(editor.triggerRerender, timeout=TIMEOUT):
editor.save()
editor.autoreload(True)
# Saving a file with autoreload enabled should trigger a rerender.
with qtbot.waitSignal(editor.triggerRerender, timeout=TIMEOUT):
editor.save()
def test_autoreload_nested(editor):
qtbot, editor = editor
TIMEOUT = 500
editor.autoreload(True)
editor.preferences['Autoreload: watch imported modules'] = True
with open('test_nested_top.py','w') as f:
f.write(code_nested_top)
with open('test_nested_bottom.py','w') as f:
f.write("")
assert(editor.get_text_with_eol() == '')
editor.load_from_file('test_nested_top.py')
assert(len(editor.get_text_with_eol()) > 0)
# wait for reload.
with qtbot.waitSignal(editor.triggerRerender, timeout=TIMEOUT):
# modify file - NB: separate process is needed to avoid Windows quirks
modify_file(code_nested_bottom, 'test_nested_bottom.py')
def test_console(main):
qtbot, win = main
console = win.components['console']
# test execute_command
a = []
console.push_vars({'a' : a})
console.execute_command('a.append(1)')
assert(len(a) == 1)
# test print_text
pos_orig = console._prompt_pos
console.print_text('a')
assert(console._prompt_pos == pos_orig + len('a'))
def test_viewer(main):
qtbot, win = main
viewer = win.components['viewer']
#not sure how to test this, so only smoke tests
#trigger all 'View' actions
actions = viewer._actions['View']
for a in actions: a.trigger()
code_module = \
'''def dummy(): return True'''
code_import = \
'''from module import dummy
assert(dummy())'''
def test_module_import(main):
qtbot, win = main
editor = win.components['editor']
debugger = win.components['debugger']
traceback_view = win.components['traceback_viewer']
#save the dummy module
with open('module.py','w') as f:
f.write(code_module)
#run the code importing this module
editor.set_text(code_import)
debugger._actions['Run'][0].triggered.emit()
#verify that no exception was generated
assert(traceback_view.current_exception.text() == '')
def test_auto_fit_view(main_clean):
def concat(eye,proj,scale):
return eye+proj+(scale,)
def approx_view_properties(eye,proj,scale):
return pytest.approx(eye+proj+(scale,))
qtbot, win = main_clean
editor = win.components['editor']
debugger = win.components['debugger']
viewer = win.components['viewer']
object_tree = win.components['object_tree']
view = viewer.canvas.view
viewer.preferences['Fit automatically'] = False
eye0,proj0,scale0 = view.Eye(),view.Proj(),view.Scale()
# check if camera position is adjusted automatically when rendering for the
# first time
debugger.render()
eye1,proj1,scale1 = view.Eye(),view.Proj(),view.Scale()
assert( concat(eye0,proj0,scale0) != \
approx_view_properties(eye1,proj1,scale1) )
# check if camera position is not changed fter code change
editor.set_text(code_bigger_object)
debugger.render()
eye2,proj2,scale2 = view.Eye(),view.Proj(),view.Scale()
assert( concat(eye1,proj1,scale1) == \
approx_view_properties(eye2,proj2,scale2) )
# check if position is adjusted automatically after erasing all objects
object_tree.removeObjects()
debugger.render()
eye3,proj3,scale3 = view.Eye(),view.Proj(),view.Scale()
assert( concat(eye2,proj2,scale2) != \
approx_view_properties(eye3,proj3,scale3) )
# check if position is adjusted automatically if settings are changed
viewer.preferences['Fit automatically'] = True
editor.set_text(code)
debugger.render()
eye4,proj4,scale4 = view.Eye(),view.Proj(),view.Scale()
assert( concat(eye3,proj3,scale3) != \
approx_view_properties(eye4,proj4,scale4) )
def test_preserve_properties(main):
qtbot, win = main
debugger = win.components['debugger']
debugger._actions['Run'][0].triggered.emit()
object_tree = win.components['object_tree']
object_tree.preferences['Preserve properties on reload'] = True
assert(object_tree.CQ.childCount() == 1)
props = object_tree.CQ.child(0).properties
props['Visible'] = False
props['Color'] = '#caffee'
props['Alpha'] = 0.5
debugger._actions['Run'][0].triggered.emit()
assert(object_tree.CQ.childCount() == 1)
props = object_tree.CQ.child(0).properties
assert(props['Visible'] == False)
assert(props['Color'].name() == '#caffee')
assert(props['Alpha'] == 0.5)
def test_selection(main_multi,mocker):
qtbot, win = main_multi
viewer = win.components['viewer']
object_tree = win.components['object_tree']
CQ = object_tree.CQ
obj1 = CQ.child(0)
obj2 = CQ.child(1)
# export with two selected objects
obj1.setSelected(True)
obj2.setSelected(True)
object_tree._export_STEP_action.triggered.emit()
imported = cq.importers.importStep('out.step')
assert(len(imported.solids().vals()) == 2)
# export with one selected objects
obj2.setSelected(False)
object_tree._export_STEP_action.triggered.emit()
imported = cq.importers.importStep('out.step')
assert(len(imported.solids().vals()) == 1)
# export with one selected objects
obj1.setSelected(False)
CQ.setSelected(True)
object_tree._export_STEP_action.triggered.emit()
imported = cq.importers.importStep('out.step')
assert(len(imported.solids().vals()) == 2)
# check if viewer and object tree are properly connected
CQ.setSelected(False)
obj1.setSelected(True)
obj2.setSelected(True)
ctx = viewer._get_context()
ctx.InitSelected()
shapes = []
while ctx.MoreSelected():
shapes.append(ctx.SelectedShape())
ctx.NextSelected()
assert(len(shapes) == 2)
viewer.fit()
qtbot.mouseClick(viewer.canvas, Qt.LeftButton)
assert(len(object_tree.tree.selectedItems()) == 0)
viewer.sigObjectSelected.emit([obj1.shape_display.wrapped])
assert(len(object_tree.tree.selectedItems()) == 1)
# go through different handleSelection paths
qtbot.mouseClick(object_tree.tree, Qt.LeftButton)
qtbot.keyClick(object_tree.tree, Qt.Key_Down)
qtbot.keyClick(object_tree.tree, Qt.Key_Down)
qtbot.keyClick(object_tree.tree, Qt.Key_Down)
qtbot.keyClick(object_tree.tree, Qt.Key_Down)
assert(object_tree._export_STL_action.isEnabled() == False)
assert(object_tree._export_STEP_action.isEnabled() == False)
assert(object_tree._clear_current_action.isEnabled() == False)
assert(object_tree.properties_editor.isEnabled() == False)
def test_closing(main_clean_do_not_close):
qtbot,win = main_clean_do_not_close
editor = win.components['editor']
# make sure that windows is visible
assert(win.isVisible())
# should not quit
win.close()
assert(win.isVisible())
# should quit
editor.reset_modified()
win.close()
assert(not win.isVisible())
def test_check_for_updates(main,mocker):
qtbot,win = main
# patch requests
import requests
mocker.patch.object(requests.models.Response,'json',
return_value=[{'tag_name' : '0.0.2','draft' : False}])
# stub QMessageBox about
about_stub = mocker.stub()
mocker.patch.object(QMessageBox, 'about', about_stub)
import cadquery
cadquery.__version__ = '0.0.1'
win.check_for_cq_updates()
assert(about_stub.call_args[0][1] == 'Updates available')
cadquery.__version__ = '0.0.3'
win.check_for_cq_updates()
assert(about_stub.call_args[0][1] == 'No updates available')
@pytest.mark.skipif(sys.platform.startswith('linux'),reason='Segfault workaround for linux')
def test_screenshot(main,mocker):
qtbot,win = main
mocker.patch.object(QFileDialog, 'getSaveFileName', return_value=('out.png',''))
viewer = win.components['viewer']
viewer._actions['Tools'][0].triggered.emit()
assert(os.path.exists('out.png'))
def test_resize(main):
qtbot,win = main
editor = win.components['editor']
editor.hide()
qtbot.wait(50)
editor.show()
qtbot.wait(50)
code_simple_step = \
'''import cadquery as cq
imported = cq.importers.importStep('shape.step')
'''
def test_relative_references(main):
# create code with a relative reference in a subdirectory
p = Path('test_relative_references')
p.mkdir_p()
p_code = p.joinpath('code.py')
p_code.write_text(code_simple_step)
# create the referenced step file
shape = cq.Workplane("XY").box(1, 1, 1)
p_step = p.joinpath('shape.step')
export(shape, "step", p_step)
# open code
qtbot, win = main
editor = win.components['editor']
editor.load_from_file(p_code)
# render
debugger = win.components['debugger']
debugger._actions['Run'][0].triggered.emit()
# assert no errors
traceback_view = win.components['traceback_viewer']
assert(traceback_view.current_exception.text() == '')
# assert one object has been rendered
obj_tree_comp = win.components['object_tree']
assert(obj_tree_comp.CQ.childCount() == 1)
# clean up
p_code.remove_p()
p_step.remove_p()
p.rmdir_p()
code_color = \
'''
import cadquery as cq
result = cq.Workplane("XY" ).box(1, 1, 1)
show_object(result, name ='1')
show_object(result, name ='2', options=dict(alpha=0.5,color='red'))
show_object(result, name ='3', options=dict(alpha=0.5,color='#ff0000'))
show_object(result, name ='4', options=dict(alpha=0.5,color=(255,0,0)))
show_object(result, name ='5', options=dict(alpha=0.5,color=(1.,0,0)))
show_object(result, name ='6', options=dict(rgba=(1.,0,0,.5)))
show_object(result, name ='7', options=dict(color=('ff','cc','dd')))
'''
def test_render_colors(main_clean):
qtbot, win = main_clean
obj_tree = win.components['object_tree']
editor = win.components['editor']
debugger = win.components['debugger']
log = win.components['log']
editor.set_text(code_color)
debugger._actions['Run'][0].triggered.emit()
CQ = obj_tree.CQ
# object 1 (defualt color)
assert not CQ.child(0).ais.HasColor()
# object 2
r,g,b,a = get_rgba(CQ.child(1).ais)
assert( a == 0.5 )
assert( r == 1.0 )
assert( g == 0.0 )
# object 3
r,g,b,a = get_rgba(CQ.child(2).ais)
assert( a == 0.5)
assert( r == 1.0 )
# object 4
r,g,b,a = get_rgba(CQ.child(3).ais)
assert( a == 0.5 )
assert( r == 1.0 )
# object 5
r,g,b,a = get_rgba(CQ.child(4).ais)
assert( a == 0.5 )
assert( r == 1.0 )
# object 6
r,g,b,a = get_rgba(CQ.child(5).ais)
assert( a == 0.5 )
assert( r == 1.0 )
# check if error occured
qtbot.wait(100)
assert('Unknown color format' in log.toPlainText().splitlines()[-1])
def test_render_colors_console(main_clean):
qtbot, win = main_clean
obj_tree = win.components['object_tree']
log = win.components['log']
console = win.components['console']
console.execute_command(code_color)
CQ = obj_tree.CQ
# object 1 (defualt color)
assert not CQ.child(0).ais.HasColor()
# object 2
r,g,b,a = get_rgba(CQ.child(1).ais)
assert( a == 0.5 )
assert( r == 1.0 )
# object 3
r,g,b,a = get_rgba(CQ.child(2).ais)
assert( a == 0.5)
assert( r == 1.0 )
# object 4
r,g,b,a = get_rgba(CQ.child(3).ais)
assert( a == 0.5 )
assert( r == 1.0 )
# object 5
r,g,b,a = get_rgba(CQ.child(4).ais)
assert( a == 0.5 )
assert( r == 1.0 )
# object 6
r,g,b,a = get_rgba(CQ.child(5).ais)
assert( a == 0.5 )
assert( r == 1.0 )
# check if error occured
qtbot.wait(100)
assert('Unknown color format' in log.toPlainText().splitlines()[-1])
def test_confirm_new(monkeypatch,editor):
qtbot, editor = editor
#check that initial state is as expected
assert(editor.modified == False)
editor.document().setPlainText(code)
assert(editor.modified == True)
#monkeypatch the confirmation dialog and run both scenarios
def cancel(*args, **kwargs):
return QMessageBox.No
def ok(*args, **kwargs):
return QMessageBox.Yes
monkeypatch.setattr(QMessageBox, 'question',
staticmethod(cancel))
editor.new()
assert(editor.modified == True)
assert(conv_line_ends(editor.get_text_with_eol()) == code)
monkeypatch.setattr(QMessageBox, 'question',
staticmethod(ok))
editor.new()
assert(editor.modified == False)
assert(editor.get_text_with_eol() == '')
code_show_topods = \
'''
import cadquery as cq
result = cq.Workplane("XY" ).box(1, 1, 1)
show_object(result.val().wrapped)
'''
def test_render_topods(main):
qtbot, win = main
obj_tree_comp = win.components['object_tree']
editor = win.components['editor']
debugger = win.components['debugger']
console = win.components['console']
# check that object was rendered
assert(obj_tree_comp.CQ.childCount() == 1)
# check that object was removed
obj_tree_comp._toolbar_actions[0].triggered.emit()
assert(obj_tree_comp.CQ.childCount() == 0)
# check that object was rendered usin explicit show_object call
editor.set_text(code_show_topods)
debugger._actions['Run'][0].triggered.emit()
assert(obj_tree_comp.CQ.childCount() == 1)
# test rendering of topods object via console
console.execute('show(result.val().wrapped)')
assert(obj_tree_comp.CQ.childCount() == 2)
# test rendering of list of topods object via console
console.execute('show([result.val().wrapped,result.val().wrapped])')
assert(obj_tree_comp.CQ.childCount() == 3)
code_show_shape_list = \
'''
import cadquery as cq
result1 = cq.Workplane("XY" ).box(1, 1, 1).val()
result2 = cq.Workplane("XY",origin=(0,1,1)).box(1, 1, 1).val()
show_object(result1)
show_object([result1,result2])
'''
def test_render_shape_list(main):
qtbot, win = main
log = win.components['log']
obj_tree_comp = win.components['object_tree']
editor = win.components['editor']
debugger = win.components['debugger']
console = win.components['console']
# check that object was removed
obj_tree_comp._toolbar_actions[0].triggered.emit()
assert(obj_tree_comp.CQ.childCount() == 0)
# check that object was rendered usin explicit show_object call
editor.set_text(code_show_shape_list)
debugger._actions['Run'][0].triggered.emit()
assert(obj_tree_comp.CQ.childCount() == 2)
# test rendering of Shape via console
console.execute('show(result1)')
console.execute('show([result1,result2])')
assert(obj_tree_comp.CQ.childCount() == 4)
# smoke test exception in show
console.execute('show("a")')
code_show_assy = \
'''import cadquery as cq
result1 = cq.Workplane("XY" ).box(3, 3, 0.5)
assy = cq.Assembly(result1)
show_object(assy)
'''
def test_render_assy(main):
qtbot, win = main
obj_tree_comp = win.components['object_tree']
editor = win.components['editor']
debugger = win.components['debugger']
console = win.components['console']
# check that object was removed
obj_tree_comp._toolbar_actions[0].triggered.emit()
assert(obj_tree_comp.CQ.childCount() == 0)
# check that object was rendered usin explicit show_object call
editor.set_text(code_show_assy)
debugger._actions['Run'][0].triggered.emit()
qtbot.wait(500)
assert(obj_tree_comp.CQ.childCount() == 1)
# test rendering via console
console.execute('show(assy)')
qtbot.wait(500)
assert(obj_tree_comp.CQ.childCount() == 2)
code_show_ais = \
'''import cadquery as cq
from cadquery.occ_impl.assembly import toCAF
import OCP
result1 = cq.Workplane("XY" ).box(3, 3, 0.5)
assy = cq.Assembly(result1)
lab, doc = toCAF(assy)
ais = OCP.XCAFPrs.XCAFPrs_AISObject(lab)
show_object(ais)
'''
def test_render_ais(main):
qtbot, win = main
obj_tree_comp = win.components['object_tree']
editor = win.components['editor']
debugger = win.components['debugger']
console = win.components['console']
# check that object was removed
obj_tree_comp._toolbar_actions[0].triggered.emit()
assert(obj_tree_comp.CQ.childCount() == 0)
# check that object was rendered usin explicit show_object call
editor.set_text(code_show_ais)
debugger._actions['Run'][0].triggered.emit()
qtbot.wait(500)
assert(obj_tree_comp.CQ.childCount() == 1)
# test rendering via console
console.execute('show(ais)')
qtbot.wait(500)
assert(obj_tree_comp.CQ.childCount() == 2)
code_show_sketch = \
'''import cadquery as cq
s1 = cq.Sketch().rect(1,1)
s2 = cq.Sketch().segment((0,0), (0,3.),"s1")
show_object(s1)
show_object(s2)
'''
def test_render_sketch(main):
qtbot, win = main
obj_tree_comp = win.components['object_tree']
editor = win.components['editor']
debugger = win.components['debugger']
console = win.components['console']
# check that object was removed
obj_tree_comp._toolbar_actions[0].triggered.emit()
assert(obj_tree_comp.CQ.childCount() == 0)
# check that object was rendered usin explicit show_object call
editor.set_text(code_show_sketch)
debugger._actions['Run'][0].triggered.emit()
qtbot.wait(500)
assert(obj_tree_comp.CQ.childCount() == 2)
# test rendering via console
console.execute('show(s1); show(s2)')
qtbot.wait(500)
assert(obj_tree_comp.CQ.childCount() == 4)
def test_window_title(monkeypatch, main):
fname = 'test_window_title.py'
with open(fname, 'w') as f:
f.write(code)
qtbot, win = main
#monkeypatch QFileDialog methods
def filename(*args, **kwargs):
return fname, None
monkeypatch.setattr(QFileDialog, 'getOpenFileName',
staticmethod(filename))
win.components["editor"].open()
assert(win.windowTitle().endswith(fname))
# handle a new file
win.components["editor"].new()
# I don't really care what the title is, as long as it's not a filename
assert(not win.windowTitle().endswith('.py'))
def test_module_discovery(tmp_path, editor):
qtbot, editor = editor
with open(tmp_path.joinpath('main.py'), 'w') as f:
f.write('import b')
assert editor.get_imported_module_paths(str(tmp_path.joinpath('main.py'))) == []
tmp_path.joinpath('b.py').touch()
assert editor.get_imported_module_paths(str(tmp_path.joinpath('main.py'))) == [str(tmp_path.joinpath('b.py'))]
def test_launch_syntax_error(tmp_path):
# verify app launches when input file is bad
win = MainWindow()
inputfile = Path(tmp_path).joinpath("syntax_error.py")
modify_file("print(", inputfile)
editor = win.components["editor"]
editor.autoreload(True)
editor.preferences["Autoreload: watch imported modules"] = True
editor.load_from_file(inputfile)
win.show()
assert(win.isVisible())
code_import_module_makebox = \
"""
from module_makebox import *
z = 1
r = makebox(z)
"""
code_module_makebox = \
"""
import cadquery as cq
def makebox(z):
zval = z + 1
return cq.Workplane().box(1, 1, zval)
"""
def test_reload_import_handle_error(tmp_path, main):
TIMEOUT = 500
qtbot, win = main
editor = win.components["editor"]
debugger = win.components["debugger"]
traceback_view = win.components["traceback_viewer"]
editor.autoreload(True)
editor.preferences["Autoreload: watch imported modules"] = True
# save the module and top level script files
module_file = Path(tmp_path).joinpath("module_makebox.py")
script = Path(tmp_path).joinpath("main.py")
modify_file(code_module_makebox, module_file)
modify_file(code_import_module_makebox, script)
# run, verify that no exception was generated
editor.load_from_file(script)
debugger._actions["Run"][0].triggered.emit()
assert(traceback_view.current_exception.text() == "")
# save the module with an error
with qtbot.waitSignal(editor.triggerRerender, timeout=TIMEOUT):
lines = code_module_makebox.splitlines()
lines.remove(" zval = z + 1") # introduce NameError
lines = "\n".join(lines)
modify_file(lines, module_file)
# verify NameError is generated
debugger._actions["Run"][0].triggered.emit()
assert("NameError" in traceback_view.current_exception.text())
# revert the error, verify rerender is triggered
with qtbot.waitSignal(editor.triggerRerender, timeout=TIMEOUT):
modify_file(code_module_makebox, module_file)
# verify that no exception was generated
debugger._actions["Run"][0].triggered.emit()
assert(traceback_view.current_exception.text() == "")
def test_modulefinder(tmp_path, main):
TIMEOUT = 500
qtbot, win = main
editor = win.components["editor"]
debugger = win.components["debugger"]
traceback_view = win.components["traceback_viewer"]
log = win.components['log']
editor.autoreload(True)
editor.preferences["Autoreload: watch imported modules"] = True
script = Path(tmp_path).joinpath("main.py")
Path(tmp_path).joinpath("emptydir").mkdir()
modify_file("#import emptydir", script)
editor.load_from_file(script)
with qtbot.waitSignal(editor.triggerRerender, timeout=TIMEOUT):
modify_file("import emptydir", script)
qtbot.wait(100)
assert("Cannot determine imported modules" in log.toPlainText().splitlines()[-1])