Skip to content
Snippets Groups Projects
test_soi.py 9.98 KiB
"""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):
        super(TestModule, self).__init__(*args, **kwargs)
        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()


# 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
        expected_x = page_2_module["meta"]["x"] + self.soi.PADDING
        expected_y = (
            page_2_module["meta"]["y"] + self.soi.PADDING * 3 + self.soi.HEIGHT
        )

        self.assertEqual(
            expected_x, self.soi.modules[0]["widget"].pos().x(),
        )

        self.assertEqual(
            expected_y, self.soi.modules[0]["widget"].pos().y(),
        )