cbd4e62371
`update_now()` is part if the protocol that Gaphas views use to communicate with the model (`Diagram`). By decoupling those we can move away from a model where diagram updates are done asynchronously. We need our updates to happen within the context of a transaction. A possitive side effect may be that tearing, when moveing something in a diagram, is a thing of the past.
430 lines
13 KiB
Python
430 lines
13 KiB
Python
import logging
|
|
|
|
import pytest
|
|
|
|
from gaphor import UML
|
|
from gaphor.application import Application
|
|
from gaphor.core import Transaction, event_handler
|
|
from gaphor.core.modeling import AssociationUpdated, Diagram
|
|
from gaphor.diagram.copypaste import copy_full, paste_full
|
|
from gaphor.diagram.tests.fixtures import connect
|
|
from gaphor.UML.classes import (
|
|
AssociationItem,
|
|
ClassItem,
|
|
ContainmentItem,
|
|
GeneralizationItem,
|
|
)
|
|
from gaphor.UML.interactions import MessageItem
|
|
from gaphor.UML.interactions.interactionstoolbox import reflexive_message_config
|
|
|
|
|
|
@pytest.fixture
|
|
def application():
|
|
app = Application()
|
|
yield app
|
|
app.shutdown()
|
|
|
|
|
|
@pytest.fixture
|
|
def session(application):
|
|
return application.new_session()
|
|
|
|
|
|
@pytest.fixture
|
|
def event_manager(session):
|
|
return session.get_service("event_manager")
|
|
|
|
|
|
@pytest.fixture
|
|
def element_factory(session):
|
|
return session.get_service("element_factory")
|
|
|
|
|
|
@pytest.fixture
|
|
def undo_manager(session):
|
|
return session.get_service("undo_manager")
|
|
|
|
|
|
def test_class_association_undo_redo(event_manager, element_factory, undo_manager):
|
|
diagram, ci1, ci2, a = set_up_class_and_association(event_manager, element_factory)
|
|
|
|
def get_connected(handle):
|
|
"""Get item connected to line via handle."""
|
|
if cinfo := diagram.connections.get_connection(handle):
|
|
return cinfo.connected
|
|
return None
|
|
|
|
undo_manager.clear_undo_stack()
|
|
assert not undo_manager.can_undo()
|
|
|
|
with Transaction(event_manager):
|
|
ci2.unlink()
|
|
|
|
assert undo_manager.can_undo()
|
|
|
|
for _i in range(3):
|
|
assert 9 == len(diagram.connections.solver.constraints)
|
|
|
|
undo_manager.undo_transaction()
|
|
|
|
assert 18 == len(diagram.connections.solver.constraints)
|
|
|
|
a = next(element_factory.select(AssociationItem))
|
|
assert ci1 == get_connected(a.head)
|
|
assert ci2.id == get_connected(a.tail).id
|
|
|
|
undo_manager.redo_transaction()
|
|
|
|
|
|
def set_up_class_and_association(event_manager, element_factory):
|
|
with Transaction(event_manager):
|
|
diagram = element_factory.create(Diagram)
|
|
|
|
assert 0 == len(diagram.connections.solver.constraints)
|
|
|
|
with Transaction(event_manager):
|
|
ci1 = diagram.create(ClassItem, subject=element_factory.create(UML.Class))
|
|
assert 8 == len(diagram.connections.solver.constraints)
|
|
|
|
with Transaction(event_manager):
|
|
ci2 = diagram.create(ClassItem, subject=element_factory.create(UML.Class))
|
|
assert 16 == len(diagram.connections.solver.constraints)
|
|
|
|
with Transaction(event_manager):
|
|
a = diagram.create(AssociationItem)
|
|
|
|
connect(a, a.head, ci1)
|
|
connect(a, a.tail, ci2)
|
|
|
|
# Diagram, Association, 2x Class, Property, LiteralSpecification
|
|
assert 12 == len(element_factory.lselect())
|
|
assert 18 == len(diagram.connections.solver.constraints)
|
|
|
|
return diagram, ci1, ci2, a
|
|
|
|
|
|
def test_diagram_item_can_undo_and_redo(
|
|
event_manager, element_factory, undo_manager, caplog
|
|
):
|
|
caplog.set_level(logging.INFO)
|
|
with Transaction(event_manager):
|
|
diagram = element_factory.create(Diagram)
|
|
|
|
with Transaction(event_manager):
|
|
cls = diagram.create(ClassItem, subject=element_factory.create(UML.Class))
|
|
cls.subject.name = "name"
|
|
cls.matrix.translate(10, 10)
|
|
|
|
undo_manager.undo_transaction()
|
|
undo_manager.redo_transaction()
|
|
new_cls = diagram.ownedPresentation[0]
|
|
assert new_cls.matrix.tuple() == (1, 0, 0, 1, 10, 10)
|
|
assert new_cls.subject, element_factory.select()
|
|
assert new_cls.subject.name == "name"
|
|
assert not caplog.records
|
|
|
|
|
|
def test_diagram_item_should_not_end_up_in_element_factory(
|
|
event_manager, element_factory, undo_manager
|
|
):
|
|
with Transaction(event_manager):
|
|
diagram = element_factory.create(Diagram)
|
|
|
|
with Transaction(event_manager):
|
|
cls = diagram.create(ClassItem, subject=element_factory.create(UML.Class))
|
|
|
|
undo_manager.undo_transaction()
|
|
undo_manager.redo_transaction()
|
|
|
|
assert cls not in element_factory.lselect(), element_factory.lselect()
|
|
|
|
|
|
def test_delete_and_undo_diagram_item(event_manager, element_factory, undo_manager):
|
|
with Transaction(event_manager):
|
|
diagram = element_factory.create(Diagram)
|
|
|
|
with Transaction(event_manager):
|
|
subject = element_factory.create(UML.Class)
|
|
subject.name = "Name"
|
|
cls = diagram.create(ClassItem, subject=subject)
|
|
|
|
with Transaction(event_manager):
|
|
cls.unlink()
|
|
|
|
undo_manager.undo_transaction()
|
|
|
|
new_cls = diagram.ownedPresentation[0]
|
|
new_elem = element_factory.lookup(subject.id)
|
|
|
|
assert new_cls in new_elem.presentation
|
|
assert new_cls.subject
|
|
assert new_elem.name == "Name"
|
|
|
|
|
|
def test_delete_and_undo_model_element(event_manager, element_factory, undo_manager):
|
|
with Transaction(event_manager):
|
|
diagram = element_factory.create(Diagram)
|
|
|
|
with Transaction(event_manager):
|
|
subject = element_factory.create(UML.Class)
|
|
subject.name = "Name"
|
|
diagram.create(ClassItem, subject=subject)
|
|
|
|
with Transaction(event_manager):
|
|
subject.unlink()
|
|
|
|
undo_manager.undo_transaction()
|
|
|
|
new_cls = diagram.ownedPresentation[0]
|
|
new_elem = element_factory.lookup(subject.id)
|
|
|
|
assert new_cls in new_elem.presentation
|
|
assert new_cls.subject
|
|
assert new_elem.name == "Name"
|
|
|
|
|
|
def test_deleted_diagram_item_should_not_end_up_in_element_factory(
|
|
event_manager, element_factory, undo_manager
|
|
):
|
|
with Transaction(event_manager):
|
|
diagram = element_factory.create(Diagram)
|
|
cls = diagram.create(ClassItem, subject=element_factory.create(UML.Class))
|
|
|
|
with Transaction(event_manager):
|
|
cls.unlink()
|
|
|
|
undo_manager.undo_transaction()
|
|
|
|
assert cls not in element_factory.lselect(), element_factory.lselect()
|
|
|
|
undo_manager.redo_transaction()
|
|
|
|
assert cls not in element_factory.lselect(), element_factory.lselect()
|
|
|
|
|
|
def test_undo_should_not_cause_warnings(
|
|
event_manager, element_factory, undo_manager, caplog
|
|
):
|
|
caplog.set_level(logging.INFO)
|
|
with Transaction(event_manager):
|
|
diagram = element_factory.create(Diagram)
|
|
|
|
with Transaction(event_manager):
|
|
diagram.create(ClassItem, subject=element_factory.create(UML.Class))
|
|
|
|
assert not caplog.records
|
|
|
|
undo_manager.undo_transaction()
|
|
|
|
assert not diagram.ownedPresentation
|
|
assert not caplog.records
|
|
|
|
|
|
def test_can_undo_connected_generalization(
|
|
event_manager, element_factory, undo_manager, caplog
|
|
):
|
|
caplog.set_level(logging.INFO)
|
|
with Transaction(event_manager):
|
|
diagram: Diagram = element_factory.create(Diagram)
|
|
general = diagram.create(ClassItem, subject=element_factory.create(UML.Class))
|
|
specific = diagram.create(ClassItem, subject=element_factory.create(UML.Class))
|
|
|
|
with Transaction(event_manager):
|
|
generalization = diagram.create(GeneralizationItem)
|
|
connect(generalization, generalization.head, general)
|
|
connect(generalization, generalization.tail, specific)
|
|
|
|
assert not caplog.records
|
|
|
|
undo_manager.undo_transaction()
|
|
|
|
assert not list(diagram.select(GeneralizationItem))
|
|
assert not caplog.records
|
|
|
|
undo_manager.redo_transaction()
|
|
new_generalization_item = next(diagram.select(GeneralizationItem))
|
|
new_generalization = next(element_factory.select(UML.Generalization))
|
|
|
|
assert len(list(diagram.select(GeneralizationItem))) == 1
|
|
assert len(element_factory.lselect(UML.Generalization)) == 1
|
|
assert new_generalization_item.subject is new_generalization
|
|
assert not caplog.records
|
|
|
|
|
|
def test_can_undo_connected_association(
|
|
event_manager, element_factory, undo_manager, caplog
|
|
):
|
|
caplog.set_level(logging.INFO)
|
|
with Transaction(event_manager):
|
|
diagram: Diagram = element_factory.create(Diagram)
|
|
parent = diagram.create(ClassItem, subject=element_factory.create(UML.Class))
|
|
child = diagram.create(ClassItem, subject=element_factory.create(UML.Class))
|
|
|
|
with Transaction(event_manager):
|
|
association = diagram.create(AssociationItem)
|
|
connect(association, association.head, parent)
|
|
connect(association, association.tail, child)
|
|
|
|
assert not caplog.records
|
|
|
|
undo_manager.undo_transaction()
|
|
|
|
assert not list(diagram.select(AssociationItem))
|
|
assert not caplog.records
|
|
|
|
undo_manager.redo_transaction()
|
|
new_association_item = next(diagram.select(AssociationItem))
|
|
new_association = next(element_factory.select(UML.Association))
|
|
|
|
assert len(list(diagram.select(AssociationItem))) == 1
|
|
assert len(element_factory.lselect(UML.Association)) == 1
|
|
|
|
assert len(new_association.memberEnd) == 2
|
|
assert new_association_item.subject is new_association
|
|
assert new_association_item.head_subject
|
|
assert new_association_item.tail_subject
|
|
assert not caplog.records
|
|
|
|
|
|
def test_can_undo_diagram_with_content(event_manager, element_factory, undo_manager):
|
|
with Transaction(event_manager):
|
|
diagram: Diagram = element_factory.create(Diagram)
|
|
diagram.create(ClassItem, subject=element_factory.create(UML.Class))
|
|
|
|
with Transaction(event_manager):
|
|
diagram.unlink()
|
|
|
|
undo_manager.undo_transaction()
|
|
|
|
new_diagram = element_factory.lookup(diagram.id)
|
|
|
|
assert new_diagram
|
|
assert new_diagram.ownedPresentation
|
|
|
|
|
|
def test_reflexive_message_undo(event_manager, element_factory, undo_manager):
|
|
with Transaction(event_manager):
|
|
diagram: Diagram = element_factory.create(Diagram)
|
|
|
|
with Transaction(event_manager):
|
|
message = diagram.create(MessageItem)
|
|
reflexive_message_config(message)
|
|
|
|
undo_manager.undo_transaction()
|
|
|
|
|
|
def test_delete_item_with_subject_owning_diagram(
|
|
event_manager, element_factory, undo_manager
|
|
):
|
|
with Transaction(event_manager):
|
|
diagram: Diagram = element_factory.create(Diagram)
|
|
klass = element_factory.create(UML.Class)
|
|
class_item = diagram.create(ClassItem, subject=klass)
|
|
diagram.element = klass
|
|
|
|
with Transaction(event_manager):
|
|
class_item.unlink()
|
|
|
|
undo_manager.undo_transaction()
|
|
|
|
|
|
def test_reconnect_on_same_element(event_manager, element_factory, undo_manager):
|
|
def copy_pos(pos):
|
|
return tuple(map(float, pos))
|
|
|
|
with Transaction(event_manager):
|
|
diagram: Diagram = element_factory.create(Diagram)
|
|
klass = element_factory.create(UML.Class)
|
|
class_item = diagram.create(ClassItem, subject=klass)
|
|
association = diagram.create(AssociationItem)
|
|
connect(association, association.head, class_item)
|
|
original_handle_pos = copy_pos(association.head.pos)
|
|
|
|
with Transaction(event_manager):
|
|
association.head.pos = (class_item.width, class_item.height)
|
|
connect(association, association.head, class_item)
|
|
new_handle_pos = copy_pos(association.head.pos)
|
|
|
|
assert original_handle_pos != new_handle_pos
|
|
|
|
undo_manager.undo_transaction()
|
|
diagram.update(diagram.ownedPresentation)
|
|
|
|
assert original_handle_pos == copy_pos(association.head.pos)
|
|
|
|
|
|
def test_exception_raised_during_undo(event_manager, element_factory, undo_manager):
|
|
package: UML.Package = next(element_factory.select(UML.Package))
|
|
|
|
with Transaction(event_manager):
|
|
klass = element_factory.create(UML.Class)
|
|
klass.package = package
|
|
|
|
with Transaction(event_manager):
|
|
klass.unlink()
|
|
|
|
@event_handler(AssociationUpdated)
|
|
def raise_an_exception(event):
|
|
if event.property is UML.Class.package:
|
|
raise ValueError("Test exception")
|
|
|
|
event_manager.subscribe(raise_an_exception)
|
|
|
|
with pytest.raises(ExceptionGroup):
|
|
undo_manager.undo_transaction()
|
|
|
|
|
|
def test_exception_raised_during_undo_from_event_handler(
|
|
event_manager, element_factory, undo_manager
|
|
):
|
|
class UndoRequested:
|
|
pass
|
|
|
|
package: UML.Package = next(element_factory.select(UML.Package))
|
|
|
|
with Transaction(event_manager):
|
|
klass = element_factory.create(UML.Class)
|
|
klass.package = package
|
|
|
|
with Transaction(event_manager):
|
|
klass.unlink()
|
|
|
|
@event_handler(AssociationUpdated)
|
|
def raise_an_exception(event):
|
|
if event.property is UML.Class.package:
|
|
raise ValueError("Test exception")
|
|
|
|
event_manager.subscribe(raise_an_exception)
|
|
|
|
@event_handler(UndoRequested)
|
|
def on_undo_requested(event):
|
|
undo_manager.undo_transaction()
|
|
|
|
event_manager.subscribe(on_undo_requested)
|
|
|
|
with pytest.raises(ExceptionGroup):
|
|
event_manager.handle(UndoRequested())
|
|
|
|
# Handle remaining events on the queue
|
|
event_manager.handle()
|
|
|
|
|
|
def test_undo_paste_full(create, diagram, event_manager, undo_manager):
|
|
"""Triggers an error when pasted data is undone.
|
|
|
|
Reproduce Hypothesis test found in https://github.com/gaphor/gaphor/issues/2895.
|
|
"""
|
|
|
|
with Transaction(event_manager):
|
|
class_item = create(ClassItem, UML.Class)
|
|
containment_item = create(ContainmentItem)
|
|
|
|
connect(containment_item, containment_item.head, class_item)
|
|
|
|
copy_buffer = copy_full([containment_item, class_item])
|
|
|
|
with Transaction(event_manager):
|
|
paste_full(copy_buffer, diagram)
|
|
|
|
undo_manager.undo_transaction()
|