diff --git a/novelwriter/core/status.py b/novelwriter/core/status.py index df5414d9..8909cc98 100644 --- a/novelwriter/core/status.py +++ b/novelwriter/core/status.py @@ -57,8 +57,11 @@ class StatusEntry: @classmethod def duplicate(cls, source: StatusEntry) -> StatusEntry: - """Create a shallow copy of the source object.""" - return dataclasses.replace(source) + """Create a deep copy of the source object.""" + cls = dataclasses.replace(source) + cls.color = QColor(source.color) + cls.icon = QIcon(source.icon) + return cls # END Class StatusEntry diff --git a/tests/mocked.py b/tests/mocked.py index 65b65d93..5f9fc6a3 100644 --- a/tests/mocked.py +++ b/tests/mocked.py @@ -75,7 +75,7 @@ class MockStatusBar: class MockTheme: def __init__(self): - self.baseIconHeight = 10 + self.baseIconHeight = 20 return def getPixmap(self, *a): diff --git a/tests/test_core/test_core_status.py b/tests/test_core/test_core_status.py index 9dd440e5..3c2b8edd 100644 --- a/tests/test_core/test_core_status.py +++ b/tests/test_core/test_core_status.py @@ -26,7 +26,7 @@ from tools import C from PyQt5.QtGui import QColor, QIcon -from novelwriter.core.status import NWStatus +from novelwriter.core.status import NWStatus, StatusEntry, _ShapeCache from novelwriter.enum import nwStatusShape statusKeys = [C.sNew, C.sNote, C.sDraft, C.sFinished] @@ -34,7 +34,36 @@ importKeys = [C.iNew, C.iMinor, C.iMajor, C.iMain] @pytest.mark.core -def testCoreStatus_Internal(mockRnd): +def testCoreStatus_StatusEntry(): + """Test the StatusEntry class.""" + color = QColor(255, 0, 0) + icon = NWStatus.createIcon(24, color, nwStatusShape.CIRCLE) + entry = StatusEntry("Test", color, nwStatusShape.CIRCLE, icon, 42) + + # Check values + assert entry.name == "Test" + assert entry.color is color + assert entry.shape == nwStatusShape.CIRCLE + assert entry.icon is icon + assert entry.count == 42 + + # Make a copy + other = StatusEntry.duplicate(entry) + assert other is not entry + + # Check copy is not the same + assert other.name == "Test" + assert other.color is not color # Not the same object + assert other.color == color # But same colours + assert other.shape == nwStatusShape.CIRCLE + assert other.icon is not icon # Not the same icon, but a copy + assert other.count == 42 + +# END Test testCoreStatus_StatusEntry + + +@pytest.mark.core +def testCoreStatus_Internal(mockGUI, mockRnd): """Test all the internal functions of the NWStatus class.""" nStatus = NWStatus(NWStatus.STATUS) nImport = NWStatus(NWStatus.IMPORT) @@ -82,18 +111,25 @@ def testCoreStatus_Internal(mockRnd): assert nImport._isKey("i12345F") is False # Not a lower case hex value assert nImport._isKey("i12345f") is True # Valid hex value + assert nStatus._checkKey(None) == "s000008" # Creates next key + assert nStatus._checkKey("s654321") == "s654321" # Status key accepted + assert nStatus._checkKey("i123456") == "s000009" # Import key not accepted + + assert nImport._checkKey(None) == "i00000a" # Creates next key + assert nImport._checkKey("s654321") == "i00000b" # Status key not accepted + assert nImport._checkKey("i123456") == "i123456" # Import key accepted + # END Test testCoreStatus_Internal @pytest.mark.core -def testCoreStatus_Iterator(mockRnd): +def testCoreStatus_Iterator(mockGUI, mockRnd): """Test the iterator functions of the NWStatus class.""" nStatus = NWStatus(NWStatus.STATUS) - nStatus.add(None, "New", (100, 100, 100), "SQUARE", 0) - nStatus.add(None, "Note", (200, 50, 0), "SQUARE", 0) - nStatus.add(None, "Draft", (200, 150, 0), "SQUARE", 0) - nStatus.add(None, "Finished", (50, 200, 0), "SQUARE", 0) + nStatus.add(None, "Note", (200, 50, 0), "CIRCLE", 1) + nStatus.add(None, "Draft", (200, 150, 0), "SQUARE", 2) + nStatus.add(None, "Finished", (50, 200, 0), "CIRCLE", 3) # Direct access entry = nStatus[statusKeys[0]] @@ -106,36 +142,63 @@ def testCoreStatus_Iterator(mockRnd): assert len(nStatus._store) == 4 assert len(nStatus) == 4 + # Content : Keys + assert [k for k, _ in nStatus.iterItems()] == [ + "s000000", "s000001", "s000002", "s000003" + ] + + # Content : Names + assert [e.name for _, e in nStatus.iterItems()] == [ + "New", "Note", "Draft", "Finished" + ] + + # Content : Colours + assert [e.color for _, e in nStatus.iterItems()] == [ + QColor(100, 100, 100), QColor(200, 50, 0), QColor(200, 150, 0), QColor(50, 200, 0) + ] + + # Content : Shape + assert [e.shape for _, e in nStatus.iterItems()] == [ + nwStatusShape.SQUARE, nwStatusShape.CIRCLE, nwStatusShape.SQUARE, nwStatusShape.CIRCLE + ] + + # Content : Count + assert [e.count for _, e in nStatus.iterItems()] == [0, 1, 2, 3] + # END Test testCoreStatus_Iterator @pytest.mark.core -def testCoreStatus_Entries(mockRnd): +def testCoreStatus_Entries(mockGUI, mockRnd): """Test all the simple setters for the NWStatus class.""" nStatus = NWStatus(NWStatus.STATUS) - # Write - # ===== + # Add + # === - # Have a key + # Has a key nStatus.add(statusKeys[0], "Entry 1", (200, 100, 50), "SQUARE", 0) assert nStatus[statusKeys[0]].name == "Entry 1" assert nStatus[statusKeys[0]].color == QColor(200, 100, 50) + assert nStatus[statusKeys[0]].shape == nwStatusShape.SQUARE - # Don't have a key + # Doesn't have a key nStatus.add(None, "Entry 2", (210, 110, 60), "SQUARE", 0) assert nStatus[statusKeys[1]].name == "Entry 2" assert nStatus[statusKeys[1]].color == QColor(210, 110, 60) + assert nStatus[statusKeys[1]].shape == nwStatusShape.SQUARE - # Wrong colour spec - nStatus.add(None, "Entry 3", "what?", "SQUARE", 0) # type: ignore + # Wrong colour spec, unknown shape + nStatus.add(None, "Entry 3", "what?", "", 0) # type: ignore assert nStatus[statusKeys[2]].name == "Entry 3" assert nStatus[statusKeys[2]].color == QColor(100, 100, 100) + assert nStatus[statusKeys[2]].shape == nwStatusShape.SQUARE # Wrong colour count - nStatus.add(None, "Entry 4", (10, 20), "SQUARE", 0) # type: ignore + nStatus.add(None, "Entry 4", (10, 20), "CIRCLE", 0) # type: ignore assert nStatus[statusKeys[3]].name == "Entry 4" assert nStatus[statusKeys[3]].color == QColor(100, 100, 100) + assert nStatus[statusKeys[3]].shape == nwStatusShape.CIRCLE # Check # ===== @@ -174,6 +237,15 @@ def testCoreStatus_Entries(mockRnd): assert isinstance(nStatus[statusKeys[3]].icon, QIcon) assert isinstance(nStatus["blablabla"].icon, QIcon) + # Shape Access + # ============ + + assert nStatus[statusKeys[0]].shape == nwStatusShape.SQUARE + assert nStatus[statusKeys[1]].shape == nwStatusShape.SQUARE + assert nStatus[statusKeys[2]].shape == nwStatusShape.SQUARE + assert nStatus[statusKeys[3]].shape == nwStatusShape.CIRCLE + assert nStatus["blablabla"].shape == nwStatusShape.SQUARE + # Increment and Count Access # ========================== @@ -195,37 +267,19 @@ def testCoreStatus_Entries(mockRnd): assert nStatus[statusKeys[2]].count == 0 assert nStatus[statusKeys[3]].count == 0 - # Reorder - # ======= + # Update + # ====== - # cOrder = list(nStatus._store.keys()) - # assert cOrder == statusKeys + assert list(nStatus._store.keys()) == statusKeys - # # Wrong length - # assert nStatus.reorder([]) is False + # Reverse + order: list[tuple[str | None, StatusEntry]] = list(nStatus.iterItems()) + nStatus.update(list(reversed(order))) + assert list(nStatus._store.keys()) == list(reversed(statusKeys)) - # # No change - # assert nStatus.reorder(cOrder) is False - - # # Actual re-order - # nOrder = [ - # statusKeys[0], - # statusKeys[2], - # statusKeys[1], - # statusKeys[3], - # ] - # assert nStatus.reorder(nOrder) is True - # assert list(nStatus._store.keys()) == nOrder - - # # Add an unknown key - # wOrder = nOrder.copy() - # wOrder[3] = nStatus._newKey() - # assert nStatus.reorder(wOrder) is False - # assert list(nStatus._store.keys()) == nOrder - - # # Put it back - # assert nStatus.reorder(cOrder) is True - # assert list(nStatus._store.keys()) == cOrder + # Restore + nStatus.update(order) + assert list(nStatus._store.keys()) == statusKeys # Default # ======= @@ -242,46 +296,31 @@ def testCoreStatus_Entries(mockRnd): nStatus._default = default - # # Remove - # # ====== + # Remove + # ====== + # This uses update with deleted items - # # Non-existing entry - # assert nStatus.remove("blablabla") is False + order: list[tuple[str | None, StatusEntry]] = list(nStatus.iterItems()) - # # Non-zero entry - # nStatus.increment(statusKeys[3]) - # assert nStatus.remove(statusKeys[3]) is False + # Remove Entry 0 + nStatus.update([order[1], order[3], order[2]]) + assert list(nStatus._store.keys()) == [statusKeys[1], statusKeys[3], statusKeys[2]] + assert nStatus._default == statusKeys[1] - # # Delete last entry - # nStatus.resetCounts() - # lastName = nStatus[statusKeys[3]].name - # assert lastName == "Entry 4" - # assert nStatus.remove(statusKeys[3]) is True - # assert nStatus.check(statusKeys[3]) == nStatus._default - # assert nStatus.check(lastName) == nStatus._default - - # # Delete default entry, Entry 2 is new default - # firstName = nStatus[nStatus._default].name - # assert firstName == "Entry 1" - # assert nStatus.remove(nStatus._default) is True # type: ignore - # assert nStatus[firstName].name == "Entry 2" - - # # Remove remaining entries - # assert nStatus.remove(statusKeys[1]) is True - # assert nStatus.remove(statusKeys[2]) is True - - # assert len(nStatus) == 0 - # assert nStatus._default is None + # Remove Entry 1 + nStatus.update([order[3], order[2]]) + assert list(nStatus._store.keys()) == [statusKeys[3], statusKeys[2]] + assert nStatus._default == statusKeys[3] # END Test testCoreStatus_Entries @pytest.mark.core -def testCoreStatus_PackUnpack(mockRnd): - """Test all the pack/unpack of the NWStatus class.""" +def testCoreStatus_Pack(mockGUI, mockRnd): + """Test data packing of the NWStatus class.""" nStatus = NWStatus(NWStatus.STATUS) nStatus.add(None, "New", (100, 100, 100), "SQUARE", 0) - nStatus.add(None, "Note", (200, 50, 0), "SQUARE", 0) + nStatus.add(None, "Note", (200, 50, 0), "CIRCLE", 0) nStatus.add(None, "Draft", (200, 150, 0), "SQUARE", 0) nStatus.add(None, "Finished", (50, 200, 0), "SQUARE", 0) @@ -306,7 +345,7 @@ def testCoreStatus_PackUnpack(mockRnd): "red": "200", "green": "50", "blue": "0", - "shape": "SQUARE", + "shape": "CIRCLE", }), ("Draft", { "key": statusKeys[2], @@ -326,4 +365,30 @@ def testCoreStatus_PackUnpack(mockRnd): }), ] -# END Test testCoreStatus_PackUnpack +# END Test testCoreStatus_Pack + + +@pytest.mark.core +def testCoreStatus_ShapeCache(): + """Test the _ShapeCache class.""" + shapes = _ShapeCache() + + # Generate all shapes + square = shapes.getShape(nwStatusShape.SQUARE) + circle = shapes.getShape(nwStatusShape.CIRCLE) + triangle = shapes.getShape(nwStatusShape.TRIANGLE) + diamond = shapes.getShape(nwStatusShape.DIAMOND) + pentagon = shapes.getShape(nwStatusShape.PENTAGON) + star = shapes.getShape(nwStatusShape.STAR) + pacman = shapes.getShape(nwStatusShape.PACMAN) + + # Request again should return from cache + assert shapes.getShape(nwStatusShape.SQUARE) is square + assert shapes.getShape(nwStatusShape.CIRCLE) is circle + assert shapes.getShape(nwStatusShape.TRIANGLE) is triangle + assert shapes.getShape(nwStatusShape.DIAMOND) is diamond + assert shapes.getShape(nwStatusShape.PENTAGON) is pentagon + assert shapes.getShape(nwStatusShape.STAR) is star + assert shapes.getShape(nwStatusShape.PACMAN) is pacman + +# END Test testCoreStatus_ShapeCache