-
Thomas Holene Løkkeborg authored
- Fikset ModuleList fortsatt referer til 'self.parent.soi' noen steder - Reorganisering av SOI vil nå også reorganisere vedlegg. Vedlegg plasseres etter rekkefølge i SOI.attachments listen, uavhengig av sorteringsalgoritmen som er valgt (siden denne bare gjelder for rectpack-sortering). - implementert skikkelig deepcopy for modul-lister i SOI test, siden det ble trøbbel med at modules and attachments i realiteten refererte til samme objekt...
Thomas Holene Løkkeborg authored- Fikset ModuleList fortsatt referer til 'self.parent.soi' noen steder - Reorganisering av SOI vil nå også reorganisere vedlegg. Vedlegg plasseres etter rekkefølge i SOI.attachments listen, uavhengig av sorteringsalgoritmen som er valgt (siden denne bare gjelder for rectpack-sortering). - implementert skikkelig deepcopy for modul-lister i SOI test, siden det ble trøbbel med at modules and attachments i realiteten refererte til samme objekt...
test_soi.py 15.13 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, ModuleNameTaken
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.color = color
self.width = width
self.height = height
self.setAutoFillBackground(True)
palette = self.palette()
palette.setColor(QPalette.Window, QColor(self.color))
self.setPalette(palette)
self.setGeometry(0, 0, self.width, self.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()
def testmodule_cloner(module):
"""Return new TestModule that is a clone of parameter module.
Paramters
---------
module : TestModule
Module to clone.
Returns
-------
TestModule
New TestModule that is a clone of parameter module.
"""
return TestModule(module.color, module.width, module.height)
# 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=deep_copy_of_modules_list(TEST_MODULES, testmodule_cloner),
attachments=deep_copy_of_modules_list(
TEST_MODULES, testmodule_cloner
),
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
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
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 = deep_copy_of_modules_list(
self.soi.modules, testmodule_cloner
)
# 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 = deep_copy_of_modules_list(
input_modules, testmodule_cloner
)
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 needed for y
expected_x = (
page_2_module["meta"]["x"]
+ self.soi.PADDING
+ self.soi.HEADER_WIDTH
)
expected_y = (
page_2_module["meta"]["y"] + self.soi.PADDING * 2 + 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(),
)
def test_add_module(self):
"""Test add module and reorganize and add module listeners."""
reorganize_listener_called = False
new_module_listener_called = False
def listener_reorganize():
# Nonlocal is required to access variable in nesting function's
# scope
nonlocal reorganize_listener_called
reorganize_listener_called = True
def listener_new_module():
# Nonlocal is required to access variable in nesting function's
# scope
nonlocal new_module_listener_called
new_module_listener_called = True
self.soi.add_reorganization_listener(listener_reorganize)
self.soi.add_reorganization_listener(listener_new_module)
module_name = "add module test name"
self.soi.add_module(module_name, TestModule("purple", 100, 100), True)
self.assertTrue(self.soi.module_name_taken(module_name))
self.assertTrue(reorganize_listener_called)
self.assertTrue(new_module_listener_called)
def test_add_module_duplicate_name(self):
"""Test adding module with duplicate name raises exception."""
# Using already added module TEST_MODULES[0] to guarantee name exists
self.assertRaises(
ModuleNameTaken,
lambda: self.soi.add_module(
TEST_MODULES[0]["meta"]["name"],
TestModule("purple", 100, 100),
False,
),
)
self.assertRaises(
ModuleNameTaken,
lambda: self.soi.add_module(
TEST_MODULES[0]["meta"]["name"],
TestModule("purple", 100, 100),
True,
),
)
# Test that unique name doesn't raise exception
try:
self.soi.add_module(
"unique name", TestModule("purple", 100, 100), True
)
except ModuleNameTaken:
self.fail("Exception should not be raised when name is unique")
try:
self.soi.add_module(
"another unique name", TestModule("purple", 100, 100), False
)
except ModuleNameTaken:
self.fail("Exception should not be raised when name is unique")
def test_update_properties(self):
"""Test updating properties and corresponding listener."""
test_title = "a test title"
listener_called = False
def listener_update_properties():
# Nonlocal is required to access variable in nesting function's
# scope
nonlocal listener_called
listener_called = True
self.soi.add_update_property_listener(listener_update_properties)
self.soi.update_properties(title=test_title)
self.assertTrue(listener_called)
self.assertEqual(self.soi.title, test_title)
def test_update_properties_invalid_key(self):
"""Test update_properties properly throws exception if invalid key."""
self.assertRaises(
ValueError,
lambda: self.soi.update_properties(invalid_key="garbage"),
)
def deep_copy_of_modules_list(modules, widget_cloner):
"""Get deep copy of modules list. Works for both modules and attachments.
This is necessary because copying lists in Python only works on one level
of nesting, such that:
```python
a = [[1,2,3]
b = a.copy()
b[0][0] = 9
print(
"nested list in 'a' now updated "
"to '{}'!! Not what we want! ".format(a[0][0])
)
```
There is a package copy that contains deepcopy for this, but we can't use
it because some of our objects "Can't be pickled", quoting the error.
Parameters
----------
modules : list
List of modules.
widget_cloner : function
Function that given a Qt widget as a parameter can return a clone of
that widget. Example usage: `widget_cloner(mywidget)` should return a
widget that is a clone of mywidget.
Returns
-------
list
List of modules, deep copied from input list.
"""
return [
{
"meta": module["meta"].copy(),
"widget": widget_cloner(module["widget"]),
}
for module in modules
]