Move file IO to dedicated thread

This commit is contained in:
Rebecca Breu 2021-04-13 17:08:23 +02:00
parent a907343d04
commit d2c26d98d6
13 changed files with 425 additions and 129 deletions

View file

@ -18,12 +18,16 @@ from PyQt6 import QtCore, QtGui
class InsertItems(QtGui.QUndoCommand):
def __init__(self, scene, items):
def __init__(self, scene, items, ignore_first_redo=False):
super().__init__('Insert items')
self.scene = scene
self.items = items
self.ignore_first_redo = ignore_first_redo
def redo(self):
if self.ignore_first_redo:
self.ignore_first_redo = False
return
self.scene.clearSelection()
for item in self.items:
self.scene.addItem(item)

View file

@ -15,23 +15,86 @@
import logging
from PyQt6 import QtCore, QtGui
from beeref import commands
from beeref.fileio.errors import BeeFileIOError
from beeref.fileio.sql import SQLiteIO
from beeref.items import BeePixmapItem
__all__ = ['load', 'save', 'BeeFileIOError']
__all__ = [
'load_bee',
'save_bee',
'load_images',
'ThreadedLoader',
'BeeFileIOError',
]
logger = logging.getLogger('BeeRef')
def load(filename, scene, progress=None):
def load_bee(filename, scene, worker=None):
"""Load BeeRef native file."""
logger.info(f'Loading from file {filename}...')
io = SQLiteIO(filename, scene, readonly=True, progress=progress)
io = SQLiteIO(filename, scene, readonly=True, worker=worker)
return io.read()
def save(filename, scene, create_new=False, progress=None):
def save_bee(filename, scene, create_new=False, worker=None):
"""Save BeeRef native file."""
logger.info(f'Saving to file {filename}...')
logger.debug(f'Create new: {create_new}')
io = SQLiteIO(filename, scene, create_new, progress=progress)
io = SQLiteIO(filename, scene, create_new, worker=worker)
io.write()
logger.info('Saved!')
def load_images(filenames, pos, scene, worker):
"""Add images to existing scene."""
errors = []
items = []
worker.begin_processing.emit(len(filenames))
for i, filename in enumerate(filenames):
logger.info(f'Loading image from file {filename}')
img = QtGui.QImage(filename)
worker.progress.emit(i)
if img.isNull():
errors.append(filename)
continue
item = BeePixmapItem(img, filename)
item.set_pos_center(pos)
pos.setX(pos.x() + 50)
pos.setY(pos.y() + 50)
scene.add_item_later(item, selected=True)
items.append(item)
if worker.canceled:
break
scene.undo_stack.push(
commands.InsertItems(scene, items, ignore_first_redo=True))
worker.finished.emit('', errors)
class ThreadedIO(QtCore.QThread):
"""Dedicated thread for loading and saving."""
progress = QtCore.pyqtSignal(int)
finished = QtCore.pyqtSignal(str, list)
begin_processing = QtCore.pyqtSignal(int)
def __init__(self, func, *args, **kwargs):
super().__init__()
self.func = func
self.args = args
self.kwargs = kwargs
self.kwargs['worker'] = self
self.canceled = False
def run(self):
self.func(*self.args, **self.kwargs)
def on_canceled(self):
self.canceled = True

View file

@ -43,9 +43,10 @@ def handle_sqlite_errors(func):
func(self, *args, **kwargs)
except sqlite3.Error as e:
logger.exception(f'Error while reading/writing {self.filename}')
if self.progress:
self.progress.setValue(self.progress.maximum())
raise BeeFileIOError(msg=str(e), filename=self.filename) from e
if self.worker:
self.worker.finished.emit('', [str(e)])
else:
raise BeeFileIOError(msg=str(e), filename=self.filename) from e
return wrapper
@ -55,12 +56,12 @@ class SQLiteIO:
APPLICATION_ID = 2060242126
def __init__(self, filename, scene, create_new=False, readonly=False,
progress=None):
worker=None):
self.scene = scene
self.create_new = create_new
self.filename = filename
self.readonly = readonly
self.progress = progress
self.worker = worker
def __del__(self):
self._close_connection()
@ -130,24 +131,27 @@ class SQLiteIO:
'SELECT items.id, x, y, z, scale, rotation, flip, filename, '
'sqlar.data '
'FROM items INNER JOIN sqlar on sqlar.item_id = items.id')
if self.progress:
self.progress.setMaximum(len(rows))
if self.worker:
self.worker.begin_processing.emit(len(rows))
for i, row in enumerate(rows):
item = BeePixmapItem(QtGui.QImage(), filename=row[7])
item.save_id = row[0]
item.pixmap_from_bytes(row[8])
item.setPos(row[1], row[2])
self.scene.addItem(item)
self.scene.add_item_later(item)
item.setZValue(row[3])
item.setScale(row[4])
item.setRotation(row[5])
if row[6] == -1:
item.do_flip()
if self.progress:
self.progress.setValue(i)
if self.progress.wasCanceled():
break
if self.worker:
self.worker.progress.emit(i)
if self.worker.canceled:
self.worker.finished.emit('', [])
return
if self.worker:
self.worker.finished.emit(self.filename, [])
@handle_sqlite_errors
def write(self):
@ -168,8 +172,8 @@ class SQLiteIO:
def write_data(self):
to_delete = self.fetchall('SELECT id from ITEMS')
to_save = list(self.scene.items_for_save())
if self.progress:
self.progress.setMaximum(len(to_save))
if self.worker:
self.worker.begin_processing.emit(len(to_save))
for i, item in enumerate(to_save):
logger.debug(f'Saving {item} with id {item.save_id}')
if item.save_id:
@ -177,12 +181,14 @@ class SQLiteIO:
to_delete.remove((item.save_id,))
else:
self.insert_item(item)
if self.progress:
self.progress.setValue(i)
if self.progress.wasCanceled():
if self.worker:
self.worker.progress.emit(i)
if self.worker.canceled:
break
self.delete_items(to_delete)
self.connection.commit()
if self.worker:
self.worker.finished.emit(self.filename, [])
def delete_items(self, to_delete):
self.exmany('DELETE FROM items WHERE id=?', to_delete)

View file

@ -44,10 +44,20 @@ class WelcomeOverlay(QtWidgets.QWidget):
class BeeProgressDialog(QtWidgets.QProgressDialog):
def __init__(self, label, maximum=100, parent=None):
def __init__(self, label, worker, maximum=100, parent=None):
super().__init__(label, 'Cancel', 0, maximum, parent=parent)
self.setMinimumDuration(2)
self.setMinimumDuration(0)
self.setWindowModality(Qt.WindowModality.WindowModal)
worker.begin_processing.connect(self.on_begin_processing)
worker.progress.connect(self.on_progress)
worker.finished.connect(self.on_finished)
self.canceled.connect(worker.on_canceled)
def setValue(self, value):
super().setValue(value + 1)
def on_progress(self, value):
self.setValue(value + 1)
def on_begin_processing(self, value):
self.setMaximum(value)
def on_finished(self, filename, errors):
self.close()

View file

@ -13,6 +13,7 @@
# You should have received a copy of the GNU General Public License
# along with BeeRef. If not, see <https://www.gnu.org/licenses/>.
from queue import Queue
import logging
import math
@ -38,6 +39,7 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
self.rubberband_item = RubberbandItem()
self.selectionChanged.connect(self.on_selection_change)
self.changed.connect(self.on_change)
self.items_to_add = Queue()
def normalize_width_or_height(self, mode):
"""Scale the selected images to have the same width or height, as
@ -205,3 +207,19 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
and not self.multi_select_item.rotate_active):
self.multi_select_item.fit_selection_area(
self.get_selection_rect())
def add_item_later(self, item, selected=False):
"""Keep an item for adding later via ``add_delayed_items``"""
self.items_to_add.put((item, selected))
def add_delayed_items(self):
"""Adds items added via ``add_items_later``"""
while not self.items_to_add.empty():
item, selected = self.items_to_add.get()
self.addItem(item)
if selected:
item.setSelected(True)
item.bring_to_front()
self.max_z = max(self.max_z, item.zValue())

View file

@ -72,7 +72,8 @@ class BaseItemMixin:
def setZValue(self, value):
logger.debug(f'Setting z-value for {self} to {value}')
super().setZValue(value)
self.scene().max_z = max(self.scene().max_z, value)
if self.scene():
self.scene().max_z = max(self.scene().max_z, value)
def bring_to_front(self):
self.setZValue(self.scene().max_z + 0.001)
@ -536,7 +537,7 @@ class MultiSelectItem(SelectableMixin,
def fit_selection_area(self, rect):
"""Updates itself to fit the given selection area."""
logging.debug(f'Fit selection area to {rect}')
logger.debug(f'Fit selection area to {rect}')
# Only update when values have changed, otherwise we end up in an
# infinite event loop sceneChange -> itemChange -> sceneChange ...

View file

@ -78,7 +78,7 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin):
def on_scene_changed(self, region):
if not self.scene.items():
logger.info('No items in scene')
logger.debug('No items in scene')
self.setTransform(QtGui.QTransform())
self.welcome_overlay.show()
else:
@ -138,23 +138,32 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin):
def on_action_flip_vertically(self):
self.scene.flip_items(vertical=True)
def open_from_file(self, filename):
logger.info(f'Opening file {filename}')
self.clear_scene()
try:
progress = BeeProgressDialog(
'Loading %s' % filename,
parent=self)
fileio.load(filename, self.scene, progress)
def on_items_loaded(self, value):
self.scene.add_delayed_items()
def on_loading_finished(self, filename, errors):
if filename:
self.filename = filename
progress.close()
except fileio.BeeFileIOError:
if errors:
QtWidgets.QMessageBox.warning(
self,
'Problem loading file',
('<p>Problem loading file %s</p>'
'<p>Not accessible or not a proper bee file</p>') % filename)
def open_from_file(self, filename):
logger.info(f'Opening file {filename}')
self.clear_scene()
self.worker = fileio.ThreadedIO(
fileio.load_bee, filename, self.scene)
self.worker.progress.connect(self.on_items_loaded)
self.worker.finished.connect(self.on_loading_finished)
self.progress = BeeProgressDialog(
'Loading %s' % filename,
worker=self.worker,
parent=self)
self.worker.start()
def on_action_open(self):
filename, f = QtWidgets.QFileDialog.getOpenFileName(
parent=self,
@ -164,44 +173,59 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin):
self.open_from_file(filename)
self.filename = filename
def on_saving_finished(self, filename, errors):
if filename:
self.filename = filename
else:
QtWidgets.QMessageBox.warning(
self,
'Problem saving file',
('<p>Problem saving file %s</p>'
'<p>File/directory not accessible</p>') % filename)
def do_save(self, filename, create_new):
if not filename.endswith('.bee'):
filename = f'{filename}.bee'
self.worker = fileio.ThreadedIO(
fileio.save_bee, filename, self.scene, create_new=create_new)
self.worker.finished.connect(self.on_saving_finished)
self.progress = BeeProgressDialog(
'Saving %s' % filename,
worker=self.worker,
parent=self)
self.worker.start()
def on_action_save_as(self):
filename, f = QtWidgets.QFileDialog.getSaveFileName(
parent=self,
caption='Save file',
filter='BeeRef File (*.bee)')
if filename:
if not filename.endswith('.bee'):
filename = f'{filename}.bee'
try:
progress = BeeProgressDialog(
'Saving %s' % filename,
parent=self)
fileio.save(filename, self.scene, create_new=True,
progress=progress)
self.filename = filename
progress.close()
except fileio.BeeFileIOError:
QtWidgets.QMessageBox.warning(
self,
'Problem saving file',
('<p>Problem saving file %s</p>'
'<p>File/directory not accessible</p>') % filename)
self.do_save(filename, create_new=True)
def on_action_save(self):
if not self.filename:
self.on_action_save_as()
else:
progress = BeeProgressDialog(
'Saving %s' % self.filename,
parent=self)
fileio.save(self.filename, self.scene, create_new=False,
progress=progress)
progress.close()
self.do_save(self.filename, create_new=False)
def on_action_quit(self):
logger.info('User quit. Exiting...')
self.app.quit()
def on_insert_images_finished(self, filename, errors):
if errors:
errornames = [
f'<li>{fn}</li>' for fn in errors]
errornames = '<ul>%s</ul>' % '\n'.join(errornames)
msg = ('{errors} image(s) out of {total} '
'could not be opened:'.format(
errors=len(errors), total=len(errors)))
QtWidgets.QMessageBox.warning(
self,
'Problem loading images',
msg + errornames)
def on_action_insert_images(self):
formats = self.get_supported_image_formats(QtGui.QImageReader)
filenames, f = QtWidgets.QFileDialog.getOpenFileNames(
@ -209,42 +233,19 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin):
caption='Select one ore more images to open',
filter=f'Images ({formats})')
pos = self.mapToScene(self.get_view_center())
errors = []
items = []
progress = BeeProgressDialog(
'Loading images...', len(filenames), parent=self)
for i, filename in enumerate(filenames):
logger.info(f'Loading image from file {filename}')
img = QtGui.QImage(filename)
progress.setValue(i)
if progress.wasCanceled():
break
if img.isNull():
errors.append(filename)
continue
item = BeePixmapItem(img, filename)
item.set_pos_center(pos)
items.append(item)
pos.setX(pos.x() + 50)
pos.setY(pos.y() + 50)
progress.close()
self.undo_stack.push(commands.InsertItems(self.scene, items))
if errors:
errornames = [
f'<li>{fn}</li>' for fn in errors]
errornames = '<ul>%s</ul>' % '\n'.join(errornames)
msg = ('{errors} image(s) out of {total} '
'could not be opened:'.format(
errors=len(errors), total=len(filenames)))
QtWidgets.QMessageBox.warning(
self,
'Problem loading images',
msg + errornames)
self.scene.clearSelection()
self.worker = fileio.ThreadedIO(
fileio.load_images,
filenames,
self.mapToScene(self.get_view_center()),
self.scene)
self.worker.progress.connect(self.on_items_loaded)
self.worker.finished.connect(self.on_insert_images_finished)
self.progress = BeeProgressDialog(
'Loading images',
worker=self.worker,
parent=self)
self.worker.start()
def on_action_paste(self):
logger.info('Pasting from clipboard...')

View file

@ -20,6 +20,12 @@ class BeeTestCase(TestCase):
inst = QtWidgets.QApplication.instance()
cls.app = inst if inst else QtWidgets.QApplication([])
def queue2list(self, queue):
qlist = []
while not queue.empty():
qlist.append(queue.get())
return qlist
# @classmethod
# def tearDownClass(cls):
# cls.app.quit()

View file

@ -1,21 +1,87 @@
import os.path
import tempfile
from unittest.mock import patch
from unittest.mock import MagicMock, patch
from PyQt6 import QtCore
from beeref import fileio
from beeref import commands
from beeref.scene import BeeGraphicsScene
from ..base import BeeTestCase
@patch('beeref.fileio.sql.SQLiteIO.write')
def test_save_create_new_false(write_mock):
def test_save_bee_create_new_false(write_mock):
with tempfile.TemporaryDirectory() as dirname:
fname = os.path.join(dirname, 'test.bee')
fileio.save(fname, 'myscene', create_new=False)
fileio.save_bee(fname, 'myscene', create_new=False)
write_mock.assert_called_once()
@patch('beeref.fileio.sql.SQLiteIO.read')
def test_write(read_mock):
def test_read_bee(read_mock):
with tempfile.TemporaryDirectory() as dirname:
fname = os.path.join(dirname, 'test.bee')
fileio.load(fname, 'myscene')
fileio.load_bee(fname, 'myscene')
read_mock.assert_called_once()
class LoadImagesTestCase(BeeTestCase):
def setUp(self):
self.scene = BeeGraphicsScene(MagicMock())
def test_loads(self):
worker = MagicMock(canceled=False)
fileio.load_images([self.imgfilename3x3],
QtCore.QPointF(5, 6), self.scene, worker)
worker.begin_processing.emit.assert_called_once_with(1)
worker.progress.emit.assert_called_once_with(0)
worker.finished.emit.assert_called_once_with('', [])
items = self.queue2list(self.scene.items_to_add)
assert len(items) == 1
item = items[0][0]
args = self.scene.undo_stack.push.call_args_list[0][0]
cmd = args[0]
assert isinstance(cmd, commands.InsertItems)
assert cmd.items == [item]
assert cmd.scene == self.scene
assert cmd.ignore_first_redo is True
assert item.pos() == QtCore.QPointF(3.5, 4.5)
def test_canceled(self):
worker = MagicMock(canceled=True)
fileio.load_images([self.imgfilename3x3, self.imgfilename3x3],
QtCore.QPointF(5, 6), self.scene, worker)
worker.begin_processing.emit.assert_called_once_with(2)
worker.progress.emit.assert_called_once_with(0)
worker.finished.emit.assert_called_once_with('', [])
items = self.queue2list(self.scene.items_to_add)
assert len(items) == 1
item = items[0][0]
args = self.scene.undo_stack.push.call_args_list[0][0]
cmd = args[0]
assert isinstance(cmd, commands.InsertItems)
assert cmd.items == [item]
assert cmd.scene == self.scene
assert cmd.ignore_first_redo is True
assert item.pos() == QtCore.QPointF(3.5, 4.5)
def test_error(self):
worker = MagicMock(canceled=False)
fileio.load_images(['foo.jpg', self.imgfilename3x3],
QtCore.QPointF(5, 6), self.scene, worker)
worker.begin_processing.emit.assert_called_once_with(2)
worker.progress.emit.assert_any_call(0)
worker.progress.emit.assert_any_call(1)
worker.finished.emit.assert_called_once_with('', ['foo.jpg'])
items = self.queue2list(self.scene.items_to_add)
assert len(items) == 1
item = items[0][0]
args = self.scene.undo_stack.push.call_args_list[0][0]
cmd = args[0]
assert isinstance(cmd, commands.InsertItems)
assert cmd.items == [item]
assert cmd.scene == self.scene
assert cmd.ignore_first_redo is True
assert item.pos() == QtCore.QPointF(3.5, 4.5)

View file

@ -189,14 +189,28 @@ class SQLiteIOWriteTestCase(BeeTestCase):
assert result[0] == 1
def test_updates_progress(self):
progress = MagicMock()
worker = MagicMock(canceled=False)
io = SQLiteIO(':memory:', self.scene, create_new=True,
progress=progress)
worker=worker)
item = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item)
io.write()
progress.setMaximum.assert_called_once_with(1)
progress.setValue.assert_called_once_with(0)
worker.begin_processing.emit.assert_called_once_with(1)
worker.progress.emit.assert_called_once_with(0)
worker.finished.emit.assert_called_once_with(':memory:', [])
def test_canceled(self):
worker = MagicMock(canceled=True)
io = SQLiteIO(':memory:', self.scene, create_new=True,
worker=worker)
item = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item)
item = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item)
io.write()
worker.begin_processing.emit.assert_called_once_with(2)
worker.progress.emit.assert_called_once_with(0)
worker.finished.emit.assert_called_once_with(':memory:', [])
class SQLiteIOReadTestCase(BeeTestCase):
@ -220,8 +234,8 @@ class SQLiteIOReadTestCase(BeeTestCase):
io = SQLiteIO(fname, self.scene, readonly=True)
io.read()
assert len(self.scene.items()) == 1
item = self.scene.items()[0]
item, selected = self.scene.items_to_add.get()
assert selected is False
assert item.save_id == 1
assert item.pos().x() == 22.2
assert item.pos().y() == 33.3
@ -232,11 +246,12 @@ class SQLiteIOReadTestCase(BeeTestCase):
assert item.filename == 'bee.png'
assert item.width == 3
assert item.height == 3
assert self.scene.items_to_add.empty() is True
def test_updates_progress(self):
progress = MagicMock()
worker = MagicMock(canceled=False)
io = SQLiteIO(':memory:', self.scene, create_new=True,
progress=progress)
worker=worker)
io.create_schema_on_new()
io.ex('INSERT INTO items (type, x, y, z, scale, filename) '
@ -245,8 +260,29 @@ class SQLiteIOReadTestCase(BeeTestCase):
io.ex('INSERT INTO sqlar (item_id, data) VALUES (?, ?)', (1, b''))
io.connection.commit()
io.read()
progress.setMaximum.assert_called_once_with(1)
progress.setValue.assert_called_once_with(0)
worker.begin_processing.emit.assert_called_once_with(1)
worker.progress.emit.assert_called_once_with(0)
worker.finished.emit.assert_called_once_with(':memory:', [])
def test_canceled(self):
worker = MagicMock(canceled=True)
io = SQLiteIO(':memory:', self.scene, create_new=True,
worker=worker)
io.create_schema_on_new()
io.ex('INSERT INTO items (type, x, y, z, scale, filename) '
'VALUES (?, ?, ?, ?, ?, ?) ',
('pixmap', 0, 0, 0, 1, 'bee.png'))
io.ex('INSERT INTO sqlar (item_id, data) VALUES (?, ?)', (1, b''))
io.ex('INSERT INTO items (type, x, y, z, scale, filename) '
'VALUES (?, ?, ?, ?, ?, ?) ',
('pixmap', 50, 50, 0, 1, 'bee2.png'))
io.ex('INSERT INTO sqlar (item_id, data) VALUES (?, ?)', (1, b''))
io.connection.commit()
io.read()
worker.begin_processing.emit.assert_called_once_with(2)
worker.progress.emit.assert_called_once_with(0)
worker.finished.emit.assert_called_once_with('', [])
def test_raises_error_when_file_borked(self):
with tempfile.TemporaryDirectory() as dirname:
@ -259,6 +295,20 @@ class SQLiteIOReadTestCase(BeeTestCase):
io.read()
assert exinfo.value.filename == fname
def test_emits_error_message_when_file_borked(self):
with tempfile.TemporaryDirectory() as dirname:
fname = os.path.join(dirname, 'test.bee')
with open(fname, 'w') as f:
f.write('foobar')
worker = MagicMock()
io = SQLiteIO(fname, self.scene, readonly=True, worker=worker)
io.read()
worker.finished.emit.assert_called_once()
args = worker.finished.emit.call_args_list[0][0]
assert args[0] == ''
assert len(args[1]) == 1
def test_reads_raises_error_when_file_empty(self):
with tempfile.TemporaryDirectory() as dirname:
fname = os.path.join(dirname, 'test.bee')

View file

@ -30,6 +30,26 @@ class InsertItemsTestCase(BeeTestCase):
assert list(scene.items_for_save()) == [item1]
assert item1.isSelected() is False
@patch('beeref.scene.BeeGraphicsScene.views')
def test_ignore_first_redo(self, views_mock):
scene = BeeGraphicsScene(None)
view = MagicMock(get_scale=MagicMock(return_value=1))
views_mock.return_value = [view]
scene.update_selection = MagicMock()
scene.max_z = 5
item1 = BeePixmapItem(QtGui.QImage())
scene.addItem(item1)
item2 = BeePixmapItem(QtGui.QImage())
command = commands.InsertItems(scene, [item2], ignore_first_redo=True)
command.redo()
assert list(scene.items_for_save()) == [item1]
assert item1.isSelected() is False
command.redo()
assert list(scene.items_for_save()) == [item1, item2]
assert item1.isSelected() is False
assert item2.isSelected() is True
item2.zValue() > 5
class DeleteItemsTestCase(BeeTestCase):

View file

@ -77,7 +77,7 @@ class BeeGraphicsSceneTestCase(BeeTestCase):
self.scene.flip_items(vertical=True)
args = self.scene.undo_stack.push.call_args_list[0][0]
cmd = args[0]
isinstance(cmd, commands.FlipItems)
assert isinstance(cmd, commands.FlipItems)
assert cmd.items == [item]
assert cmd.anchor == QtCore.QPointF(60, 50)
assert cmd.vertical is True
@ -272,7 +272,7 @@ class BeeGraphicsSceneTestCase(BeeTestCase):
self.scene.undo_stack.push.assert_called_once()
args = self.scene.undo_stack.push.call_args_list[0][0]
cmd = args[0]
isinstance(cmd, commands.MoveItemsBy)
assert isinstance(cmd, commands.MoveItemsBy)
assert cmd.items == [item]
assert cmd.ignore_first_redo is True
assert cmd.delta.x() == 10
@ -473,3 +473,26 @@ class BeeGraphicsSceneTestCase(BeeTestCase):
self.scene.multi_select_item.rotate_active = True
self.scene.on_change(None)
self.scene.multi_select_item.fit_selection_area.assert_not_called()
def test_add_delayed_items_unselected(self):
item = BeePixmapItem(QtGui.QImage())
item.setZValue(0.33)
self.scene.add_item_later(item, selected=False)
self.scene.add_delayed_items()
assert self.scene.items() == [item]
assert item.isSelected() is False
assert self.scene.max_z == 0.33
def test_add_delayed_items_selected(self):
self.scene.max_z = 0.6
item = BeePixmapItem(QtGui.QImage())
item.setZValue(0.33)
self.scene.add_item_later(item, selected=True)
self.scene.add_delayed_items()
assert self.scene.items() == [item]
assert item.isSelected() is True
assert item.zValue() > 0.6
def test_add_delayed_items_when_no_items(self):
self.scene.add_delayed_items()
assert self.scene.items() == []

View file

@ -2,6 +2,8 @@ import os.path
import tempfile
from unittest.mock import MagicMock, patch
from pytest import mark
from PyQt6 import QtGui, QtWidgets
from beeref.items import BeePixmapItem
@ -87,29 +89,44 @@ class BeeGraphicsViewTestCase(BeeTestCase):
root = os.path.dirname(__file__)
filename = os.path.join(root, 'assets', 'test1item.bee')
self.view.open_from_file(filename)
assert len(self.view.scene.items()) == 1
self.view.worker.wait()
items = self.queue2list(self.view.scene.items_to_add)
assert len(items) == 1
item, selected = items[0]
assert items[0][0].pixmap()
assert items[0][1] is False
clear_mock.assert_called_once_with()
@patch('PyQt6.QtWidgets.QMessageBox.warning')
def test_open_from_file_when_error(self, warn_mock):
# FIXME: #1
# Can't check signal handling currently
self.view.open_from_file('uieauiae')
self.view.worker.wait()
assert self.view.scene.items_to_add.empty() is True
assert len(self.view.scene.items()) == 0
warn_mock.assert_called_once()
@patch('PyQt6.QtWidgets.QFileDialog.getOpenFileName')
def test_on_action_open(self, dialog_mock):
# FIXME: #1
# Can't check signal handling currently
root = os.path.dirname(__file__)
dialog_mock.return_value = (
os.path.join(root, 'assets', 'test1item.bee'),
None)
self.view.on_action_open()
assert len(self.view.scene.items()) == 1
self.view.worker.wait()
items = self.queue2list(self.view.scene.items_to_add)
assert len(items) == 1
assert items[0][0].pixmap()
assert items[0][1] is False
@patch('PyQt6.QtWidgets.QFileDialog.getOpenFileName')
def test_on_action_open_when_no_filename(self, dialog_mock):
@patch('beeref.view.BeeGraphicsView.on_action_open')
def test_on_action_open_when_no_filename(self, dialog_mock, open_mock):
dialog_mock.return_value = (None, None)
self.view.on_action_open()
assert len(self.view.scene.items()) == 0
open_mock.assert_not_called()
@patch('PyQt6.QtWidgets.QFileDialog.getSaveFileName')
def test_on_action_save_as(self, dialog_mock):
@ -120,10 +137,11 @@ class BeeGraphicsViewTestCase(BeeTestCase):
assert os.path.exists(filename) is False
dialog_mock.return_value = (filename, None)
self.view.on_action_save_as()
self.view.worker.wait()
assert os.path.exists(filename) is True
@patch('PyQt6.QtWidgets.QFileDialog.getSaveFileName')
@patch('beeref.fileio.save')
@patch('beeref.view.BeeGraphicsView.do_save')
def test_on_action_save_as_when_no_filename(self, save_mock, dialog_mock):
item = BeePixmapItem(QtGui.QImage(self.imgfilename3x3))
self.view.scene.addItem(item)
@ -140,8 +158,10 @@ class BeeGraphicsViewTestCase(BeeTestCase):
assert os.path.exists(filename) is False
dialog_mock.return_value = (filename, None)
self.view.on_action_save_as()
self.view.worker.wait()
assert os.path.exists(f'{filename}.bee') is True
@mark.skip('needs pytest-qt')
@patch('PyQt6.QtWidgets.QMessageBox.warning')
@patch('PyQt6.QtWidgets.QFileDialog.getSaveFileName')
@patch('beeref.fileio.save')
@ -165,6 +185,7 @@ class BeeGraphicsViewTestCase(BeeTestCase):
self.view.filename = os.path.join(tmpdir, 'test.bee')
assert os.path.exists(self.view.filename) is False
self.view.on_action_save()
self.view.worker.wait()
assert os.path.exists(self.view.filename) is True
@patch('beeref.view.BeeGraphicsView.on_action_save_as')
@ -178,23 +199,30 @@ class BeeGraphicsViewTestCase(BeeTestCase):
@patch('beeref.scene.BeeGraphicsScene.clearSelection')
@patch('PyQt6.QtWidgets.QFileDialog.getOpenFileNames')
def test_on_action_insert_images(self, dialog_mock, clear_mock):
# FIXME: #1
# Can't check signal handling currently
dialog_mock.return_value = ([self.imgfilename3x3], None)
self.view.on_action_insert_images()
assert len(self.view.scene.items()) == 1
assert self.view.scene.items()[0].isSelected() is True
self.view.worker.wait()
items = self.queue2list(self.view.scene.items_to_add)
assert len(items) == 1
assert items[0][0].pixmap()
assert items[0][1] is True
clear_mock.assert_called_once_with()
@patch('beeref.scene.BeeGraphicsScene.clearSelection')
@patch('PyQt6.QtWidgets.QMessageBox.warning')
@patch('PyQt6.QtWidgets.QFileDialog.getOpenFileNames')
def test_on_action_insert_images_when_error(
self, dialog_mock, warn_mock, clear_mock):
def test_on_action_insert_images_when_error(self, dialog_mock, clear_mock):
# FIXME: #1
# Can't check signal handling currently
dialog_mock.return_value = (
[self.imgfilename3x3, 'iaeiae', 'trntrn'], None)
self.view.on_action_insert_images()
assert len(self.view.scene.items()) == 1
assert self.view.scene.items()[0].isSelected() is True
warn_mock.assert_called_once()
self.view.worker.wait()
items = self.queue2list(self.view.scene.items_to_add)
assert len(items) == 1
assert items[0][0].pixmap()
assert items[0][1] is True
clear_mock.assert_called_once_with()
@patch('beeref.scene.BeeGraphicsScene.clearSelection')