"""Test the soi module. To view the SOI during manual testing the following snippet is useful: ```python from soitool.inline_editable_soi_view import InlineEditableSOIView self.soi.reorganize() view = InlineEditableSOIView(self.soi) view.show() app.exec_() ``` """ import unittest from PySide2.QtWidgets import QApplication, QWidget from PySide2 import QtGui from PySide2.QtGui import QPalette, QColor from soitool.soi import SOI, ModuleLargerThanBinError from soitool.modules.module_base import ModuleBase if isinstance(QtGui.qApp, type(None)): app = QApplication([]) else: app = QtGui.qApp class Meta(type(ModuleBase), type(QWidget)): """Used as a metaclass to enable multiple inheritance.""" class TestModule(ModuleBase, QWidget, metaclass=Meta): """A simple module of given width, height and color for testing.""" def __init__(self, color, width, height, *args, **kwargs): self.type = "TestModule" QWidget.__init__(self, *args, **kwargs) ModuleBase.__init__(self) self.setAutoFillBackground(True) palette = self.palette() palette.setColor(QPalette.Window, QColor(color)) self.setPalette(palette) self.setGeometry(0, 0, width, height) def get_size(self): """Override.""" size = self.size() return size.width(), size.height() def set_pos(self, pos): """Override.""" self.move(pos) def render_onto_pdf(self): """Not used.""" raise NotImplementedError() def get_as_dict(self): """Not used.""" raise NotImplementedError() # The modules below have sizes that make the ideal for testing. # Sorting them by width should yield # 1. wide_module # 2. big_module # 3. tall_module # Sorting them by height should yield # 1. tall_module # 2. big_module # 3. wide_module # And sorting them by area should yield # 1. big_module # 2. tall_module # 3. wide_module # Note that when sorting by area the two modules 'tall_module' and # 'wide_module' have the same area, but 'tall_module' should come first # because sorting should be stable # https://en.wikipedia.org/wiki/Category:Stable_sorts # NOTE the only meta information necessary here is "name", if we assume # automatic sorting.. TEST_MODULES = [ { "widget": TestModule("red", 100, 400), "meta": {"x": 0, "y": 0, "page": 1, "name": "tall_module"}, }, { "widget": TestModule("blue", 400, 100), "meta": {"x": 0, "y": 0, "page": 1, "name": "wide_module"}, }, { "widget": TestModule("orange", 300, 300), "meta": {"x": 0, "y": 0, "page": 1, "name": "big_module"}, }, ] class TestSOI(unittest.TestCase): """TestCase for the SOI class.""" def setUp(self): """Prepare SOI to test on. Note that the SOI can be modified by individual tests before use, this simply sets the default. """ self.soi = SOI( modules=list(TEST_MODULES), attachments=list(TEST_MODULES), algorithm_sort="none", placement_strategy="auto", ) def sort_modules_and_assert_order(self, sorting_algorithm, expected_order): """Sort the SOI modules using algorithm and expect order. This function was created to reduce code-duplication only. Parameters ---------- sorting_algorithm : str name of sorting algorithm to pass to the SOI expected_order : list of string names names of modules in the expected order """ self.soi.algorithm_sort = sorting_algorithm self.soi.sort_modules() self.assertEqual( expected_order, [module["meta"]["name"] for module in self.soi.modules], f"Modules should have expected order when sorting algorithm is " "set to '{sorting_algorithm}'", ) def test_sort_modules_none(self): """Test sort algorithm 'none'.""" self.sort_modules_and_assert_order( "none", ["tall_module", "wide_module", "big_module"] ) def test_sort_modules_width(self): """Test sort algorithm 'width'.""" self.sort_modules_and_assert_order( "width", ["wide_module", "big_module", "tall_module"] ) def test_sort_modules_height(self): """Test sort algorithm 'height'.""" self.sort_modules_and_assert_order( "height", ["tall_module", "big_module", "wide_module"] ) def test_sort_modules_area(self): """Test sort algorithm 'area'.""" self.sort_modules_and_assert_order( "area", ["big_module", "tall_module", "wide_module"] ) def test_reorganization_listener(self): """Test reorganization listener functionality of SOI.""" listener_was_called = False def listener(): # nonlocal is required to access variable in nesting function's # scope nonlocal listener_was_called listener_was_called = True self.soi.add_reorganization_listener(listener) self.soi.reorganize() self.assertTrue(listener_was_called) def test_reorganize_of_too_big_module(self): """Test exception thrown by reorganize in case of too large modules.""" maximum_width = self.soi.CONTENT_WIDTH maximum_height = self.soi.CONTENT_HEIGHT - self.soi.HEADER_HEIGHT module_too_wide = { "widget": TestModule("red", maximum_width + 1, 100), "meta": {"x": 0, "y": 0, "page": 1, "name": "too_wide"}, } module_too_tall = { "widget": TestModule("red", 100, maximum_height + 1), "meta": {"x": 0, "y": 0, "page": 1, "name": "too_tall"}, } module_maximum_size = { "widget": TestModule("red", maximum_width, maximum_height), "meta": {"x": 0, "y": 0, "page": 1, "name": "maximum_size"}, } # too wide module should raise exception self.soi.modules = [module_too_wide] self.assertRaises( ModuleLargerThanBinError, self.soi.reorganize, ) # too tall module should raise exception self.soi.modules = [module_too_tall] self.assertRaises( ModuleLargerThanBinError, self.soi.reorganize, ) # module with maximum size shouldn't cause any exception self.soi.modules = [module_maximum_size] try: self.soi.reorganize() except ModuleLargerThanBinError: self.fail( "ModuleLargerThanBinError was raised even though module was " "not too large" ) def test_packing(self): """Test that 4 test modules are packed as we expect them to be. Only the algorithms "MaxRectsBl" and "GuillotineBssfSas" are tested. This is because if these two work, we can assume other choices of algorithm will work as well. The modules in the SOI have been chosen such that these two algorithms should yield different packings. This test was constructed by first running the algorithms on our modules and inspecting the resulting packing, and then this packing is used for testing. This way if reorganize gives a different packing in the future this test should break. It is required that reorganize yields the same packing given the same input. Note that this test does not check the actual module widget positions, only the meta positions. """ # append module of maximum size maximum_width = self.soi.CONTENT_WIDTH maximum_height = self.soi.CONTENT_HEIGHT - self.soi.HEADER_HEIGHT module_maximum_size = { "widget": TestModule("red", maximum_width, maximum_height), "meta": {"x": 0, "y": 0, "page": 1, "name": "maximum_size"}, } self.soi.modules.append(module_maximum_size) # store modules so we can use the exact same input for both algorithms input_modules = self.soi.modules # test packing with Guillotine algorithm expected_module_packing_metadata_guillotinebssfsas = [ {"x": 0, "y": 0, "page": 1, "name": "tall_module"}, {"x": 100, "y": 0, "page": 1, "name": "wide_module"}, {"x": 100, "y": 100, "page": 1, "name": "big_module"}, {"x": 0, "y": 0, "page": 2, "name": "maximum_size"}, ] self.soi.algorithm_pack = "GuillotineBssfSas" self.soi.reorganize() self.assertEqual( expected_module_packing_metadata_guillotinebssfsas, [module["meta"] for module in self.soi.modules], ) # restore modules and test packing with MaxRects self.soi.modules = input_modules expected_module_packing_metadata_maxrectsbl = [ {"x": 0, "y": 0, "page": 1, "name": "tall_module"}, {"x": 100, "y": 0, "page": 1, "name": "wide_module"}, {"x": 500, "y": 0, "page": 1, "name": "big_module"}, {"x": 0, "y": 0, "page": 2, "name": "maximum_size"}, ] self.soi.algorithm_pack = "MaxRectsBl" self.soi.reorganize() self.assertEqual( expected_module_packing_metadata_maxrectsbl, [module["meta"] for module in self.soi.modules], ) def test_module_widget_update_position(self): """Test update of real widget position to match meta position.""" page_2_module = { "widget": TestModule("red", 100, 100), "meta": {"x": 100, "y": 100, "page": 2, "name": "maximum_size"}, } self.soi.modules = [page_2_module] # using [0] below because we're guaranteed there is only one # module, and it's the one we want to test self.soi.update_module_widget_position(self.soi.modules[0]) # calculate expected widget positions. See SOI class docstring for an # explanation of the calculation neededfor y expected_x = page_2_module["meta"]["x"] + self.soi.PADDING expected_y = ( page_2_module["meta"]["y"] + self.soi.PADDING * 2 + self.soi.HEIGHT + self.soi.HEADER_HEIGHT ) self.assertEqual( expected_x, self.soi.modules[0]["widget"].pos().x(), ) self.assertEqual( expected_y, self.soi.modules[0]["widget"].pos().y(), )