/* * Copyright 2003-2017 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jetbrains.mps.smodel; import jetbrains.mps.smodel.ModelUndoTest.TestUndoHandler; import jetbrains.mps.smodel.TestModelFactory.TestModelAccess; import jetbrains.mps.smodel.TestModelFactory.TestRepository; import jetbrains.mps.smodel.event.SModelChildEvent; import jetbrains.mps.smodel.event.SModelPropertyEvent; import jetbrains.mps.smodel.event.SModelReferenceEvent; import jetbrains.mps.smodel.event.SModelRootEvent; import jetbrains.mps.util.IterableUtil; import org.jetbrains.mps.openapi.language.SConcept; import org.jetbrains.mps.openapi.language.SContainmentLink; import org.jetbrains.mps.openapi.language.SProperty; import org.jetbrains.mps.openapi.model.SModel; import org.jetbrains.mps.openapi.model.SModelAccessListener; import org.jetbrains.mps.openapi.model.SModelChangeListener; import org.jetbrains.mps.openapi.model.SNode; import org.jetbrains.mps.openapi.model.SNodeId; import org.jetbrains.mps.openapi.model.SNodeReference; import org.jetbrains.mps.openapi.model.SReference; import org.jetbrains.mps.openapi.module.SRepository; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ErrorCollector; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import static jetbrains.mps.smodel.TestModelFactory.ourConcept; import static jetbrains.mps.smodel.TestModelFactory.ourRef; import static jetbrains.mps.smodel.TestModelFactory.ourRole; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.notNullValue; /** * Check contemporary and legacy model listener approaches, ensure they (not) get notified as expected. * Contemporary listeners are checked indirectly, by means of legacy listeners being registered. Internally, we * use only new listeners, and thus ensure we check both functionality of new listeners and compatibility with legacy code. * * Lives in [kernel] module for now as its dependencies are here, and not in [smodel] * * @author Artem Tikhomirov */ public class ModelListenerTest { @Rule public ErrorCollector myErrors = new ErrorCollector(); private final TestModelAccess myTestModelAccess = new TestModelAccess(); private final SRepository myTestRepo = new TestRepository(myTestModelAccess); @Before public void setUp() { TestUndoHandler uh = new TestUndoHandler(); uh.needsUndo(false); // undo is not our focus here, we merely need to avoid NPE from ModelAccess.instance().isInsideCommand() UndoHelper.getInstance().setUndoHandler(uh); } /** * Check all three model notification approaches work. */ @Test public void testNodeReadNotify() { final TestModelFactory m1f = new TestModelFactory(); SModel m1 = m1f.createModel(3, 5, 2, 3); final int actualNodes = m1f.countModelNodes(); AccessCountListener1 cl1 = new AccessCountListener1(); AccessCountListener2 cl2 = new AccessCountListener2(); AccessCountListener3 cl3 = new AccessCountListener3(); m1f.attachAccessListeners(cl1, cl2, cl3); readTreeNodes(m1.getRootNodes()); Assert.assertEquals(actualNodes * 3, cl1.myVisitedNodes); Assert.assertEquals(actualNodes * 2, cl1.myPropertiesRead); Assert.assertEquals(0, cl1.myReferencesRead); // NodeReadEventsCaster doesn't send events unless model.canFireEvent is true (which is false // as long SModel is not in a repository (!SNodeOperations.isRegistered(myModelDescriptor)) Assert.assertEquals(0, cl2.myVisitedNodes); Assert.assertEquals(0, cl2.myPropertiesRead); Assert.assertEquals(0, cl2.myReferencesRead); Assert.assertEquals(0, cl2.myChildrenRead); // No notifications for NodeReadAccessCasterInEditor as well Assert.assertEquals(0, cl3.myVisitedNodes); Assert.assertEquals(0, cl3.myPropertiesRead); Assert.assertEquals(0, cl3.myReferencesRead); // do the same, with the model attached to a repository cl1.reset(); cl2.reset(); cl3.reset(); myTestModelAccess.enableRead(); m1f.attachTo(myTestRepo); // readTreeNodes notifies 1 read per iteration over child, +1 for getProperties, +1 for getReferences() readTreeNodes(m1.getRootNodes()); final int expectedNodeReadCount = actualNodes * 3; // // SModelAccessListener myErrors.checkThat(cl1.myVisitedNodes, equalTo(expectedNodeReadCount)); myErrors.checkThat(cl1.myPropertiesRead, equalTo(actualNodes * 2)); myErrors.checkThat(cl1.myReferencesRead, equalTo(0)); // // NodeReadEventsCaster myErrors.checkThat(cl2.myVisitedNodes, equalTo(expectedNodeReadCount)); myErrors.checkThat(cl2.myVisitedNodes, equalTo(cl1.myVisitedNodes)); myErrors.checkThat(cl2.myPropertiesRead, equalTo(actualNodes * 2)); myErrors.checkThat(cl2.myReferencesRead, equalTo(0)); myErrors.checkThat("NodeReadEventsCaster.fireNodeChildReadAccess is never used", cl2.myChildrenRead, equalTo(0)); // // NodeReadAccessCasterInEditor myErrors.checkThat(cl3.myVisitedNodes, equalTo(expectedNodeReadCount)); myErrors.checkThat(cl3.myVisitedNodes, equalTo(cl1.myVisitedNodes)); myErrors.checkThat(cl3.myPropertiesRead, equalTo(actualNodes * 2)); myErrors.checkThat(cl3.myReferencesRead, equalTo(0)); m1f.detachAccessListeners(cl1, cl2, cl3); } /** * Single child iterated over with getChildren() shall dispatch node read event, see https://youtrack.jetbrains.com/issue/MPS-18766 * The problem was due to iterator not dispatching nodeRead for the element iterator was initialized with. */ @Test public void testSingleChildIteratorNotify() { final TestModelFactory m1f = new TestModelFactory(); final SModel m1 = m1f.createModel(1, 1, 1); myTestModelAccess.enableRead(); m1f.attachTo(myTestRepo); AccessCountListener1 cl1 = new AccessCountListener1(); AccessCountListener2 cl2 = new AccessCountListener2(); AccessCountListener3 cl3 = new AccessCountListener3(); final SNode r1 = m1f.getRoot(1); m1f.attachAccessListeners(cl1, cl2, cl3); final SNode n1 = r1.getChildren().iterator().next(); Assert.assertNotNull(n1); // FIXME make sure we've got notification exactly for the node we're interested in (i.e. child of a root) myErrors.checkThat(cl1.myVisitedNodes, equalTo(1)); myErrors.checkThat(cl2.myVisitedNodes, equalTo(1)); myErrors.checkThat(cl3.myVisitedNodes, equalTo(1)); cl1.reset(); cl2.reset(); cl3.reset(); final SNode n2 = r1.getChildren(ourRole).iterator().next(); myErrors.checkThat(cl1.myVisitedNodes, equalTo(1)); myErrors.checkThat(cl2.myVisitedNodes, equalTo(1)); myErrors.checkThat(cl3.myVisitedNodes, equalTo(1)); m1f.detachAccessListeners(cl1, cl2, cl3); Assert.assertNotNull(n2); Assert.assertEquals(n1, n2); } /** * Capture state of node read notifications as we iterate children of a given role. */ @Test public void testChildrenNextNotify() { final TestModelFactory m1f = new TestModelFactory(); final SModel m1 = m1f.createModel(1, 3, 1); myTestModelAccess.enableRead(); m1f.attachTo(myTestRepo); AccessCountListener1 cl1 = new AccessCountListener1(); AccessCountListener2 cl2 = new AccessCountListener2(); AccessCountListener3 cl3 = new AccessCountListener3(); final SNode r1 = m1f.getRoot(1); // getChildren(role) is the one to check, as ChildrenIterator#getNext(node) calls for node.getContainmentLink(), which triggers another nodeRead final Iterator<? extends SNode> childIterator = r1.getChildren(ourRole).iterator(); m1f.attachAccessListeners(cl1, cl2, cl3); final SNode n1 = childIterator.next(); final SNode n2 = childIterator.next(); final SNode n3 = childIterator.next(); m1f.detachAccessListeners(cl1, cl2, cl3); Assert.assertNotNull(n1); Assert.assertNotNull(n2); Assert.assertNotNull(n3); // 3 for each node + 2 for getNext(node) calls myErrors.checkThat(cl1.myVisitedNodes, equalTo(3)); myErrors.checkThat(cl2.myVisitedNodes, equalTo(3)); myErrors.checkThat(cl3.myVisitedNodes, equalTo(3)); } /** * Explicitly state convention whether node.children.isEmpty/isNotEmpty which ends up with children.iterator.hasNext() shall * trigger model read event for the first child. */ @Test public void testChildrenHasNextNotify() { final TestModelFactory m1f = new TestModelFactory(); final TestModelFactory m2f = new TestModelFactory(); final SModel m1 = m1f.createModel(1, 1); final SModel m2 = m2f.createModel(1, 3); myTestModelAccess.enableRead(); m1f.attachTo(myTestRepo); m2f.attachTo(myTestRepo); AccessCountListener1 cl1 = new AccessCountListener1(); AccessCountListener2 cl2 = new AccessCountListener2(); AccessCountListener3 cl3 = new AccessCountListener3(); // // collection{single element}.hasNext final SNode r1 = m1f.getRoot(1); m1f.attachAccessListeners(cl1, cl2, cl3); Assert.assertTrue(r1.getChildren().iterator().hasNext()); myErrors.checkThat(cl1.myVisitedNodes, equalTo(0)); myErrors.checkThat(cl2.myVisitedNodes, equalTo(0)); myErrors.checkThat(cl3.myVisitedNodes, equalTo(0)); cl1.reset(); cl2.reset(); cl3.reset(); // just in case accessor with role is different Assert.assertTrue(r1.getChildren(ourRole).iterator().hasNext()); myErrors.checkThat(cl1.myVisitedNodes, equalTo(0)); myErrors.checkThat(cl2.myVisitedNodes, equalTo(0)); myErrors.checkThat(cl3.myVisitedNodes, equalTo(0)); m1f.detachAccessListeners(cl1, cl2, cl3); // // collection{multiple elements}.hasNext cl1.reset(); cl2.reset(); cl3.reset(); final SNode r2 = m2f.getRoot(1); m2f.attachAccessListeners(cl1, cl2, cl3); Assert.assertTrue(r2.getChildren().iterator().hasNext()); myErrors.checkThat(cl1.myVisitedNodes, equalTo(0)); myErrors.checkThat(cl2.myVisitedNodes, equalTo(0)); myErrors.checkThat(cl3.myVisitedNodes, equalTo(0)); cl1.reset(); cl2.reset(); cl3.reset(); Assert.assertTrue(r2.getChildren(ourRole).iterator().hasNext()); myErrors.checkThat(cl1.myVisitedNodes, equalTo(0)); myErrors.checkThat(cl2.myVisitedNodes, equalTo(0)); myErrors.checkThat(cl3.myVisitedNodes, equalTo(0)); m2f.detachAccessListeners(cl1, cl2, cl3); // // collection{empty}.hasNext cl1.reset(); cl2.reset(); cl3.reset(); final SNode n1 = r1.getChildren(ourRole).iterator().next(); // n1 is leaf node m1f.attachAccessListeners(cl1, cl2, cl3); Assert.assertFalse(n1.getChildren().iterator().hasNext()); myErrors.checkThat(cl1.myVisitedNodes, equalTo(0)); myErrors.checkThat(cl2.myVisitedNodes, equalTo(0)); myErrors.checkThat(cl3.myVisitedNodes, equalTo(0)); cl1.reset(); cl2.reset(); cl3.reset(); Assert.assertFalse(n1.getChildren(ourRole).iterator().hasNext()); myErrors.checkThat(cl1.myVisitedNodes, equalTo(0)); myErrors.checkThat(cl2.myVisitedNodes, equalTo(0)); myErrors.checkThat(cl3.myVisitedNodes, equalTo(0)); m1f.detachAccessListeners(cl1, cl2, cl3); } /** * Read notifications of SNode.getProperty() and SNode.hasProperty() */ @Test public void testPropertyReadNotify() { final TestModelFactory m1f = new TestModelFactory(); SModel m1 = m1f.createModel(3, 5); myTestModelAccess.enableRead(); m1f.attachTo(myTestRepo); AccessCountListener1 cl1 = new AccessCountListener1(); AccessCountListener2 cl2 = new AccessCountListener2(); AccessCountListener3 cl3 = new AccessCountListener3(); final SNode r1 = m1f.getRoot(1); m1f.attachAccessListeners(cl1, cl2, cl3); // // hasProperty boolean shouldHave = r1.hasProperty(SNodeUtil.property_INamedConcept_name); myErrors.checkThat(shouldHave, equalTo(true)); myErrors.checkThat(cl1.myVisitedNodes, equalTo(0)); myErrors.checkThat(cl2.myVisitedNodes, equalTo(0)); myErrors.checkThat(cl3.myVisitedNodes, equalTo(1)); // cl3.propertyExistenceAccess() dispatches unclassifiedNodeRead myErrors.checkThat(cl1.myPropertiesRead, equalTo(1)); myErrors.checkThat(cl2.myPropertiesRead, equalTo(1)); myErrors.checkThat(cl3.myPropertiesRead, equalTo(0)); myErrors.checkThat(cl3.getExistenceReadAccessProperties().size(), equalTo(1)); // // getProperty cl1.reset(); cl2.reset(); cl3.reset(); cl3.getExistenceReadAccessProperties().clear(); r1.getProperty(SNodeUtil.property_INamedConcept_name); myErrors.checkThat(cl1.myVisitedNodes, equalTo(0)); myErrors.checkThat(cl2.myVisitedNodes, equalTo(0)); myErrors.checkThat(cl3.myVisitedNodes, equalTo(0)); myErrors.checkThat(cl1.myPropertiesRead, equalTo(1)); myErrors.checkThat(cl2.myPropertiesRead, equalTo(1)); myErrors.checkThat(cl3.myPropertiesRead, equalTo(1)); myErrors.checkThat(cl3.getExistenceReadAccessProperties().size(), equalTo(0)); m1f.detachAccessListeners(cl1, cl2, cl3); } /** * Read notifications of SNode.getReference() and SNode.getReferenceTarget() */ @Test public void testReferenceReadNotify() { final TestModelFactory m1f = new TestModelFactory(); SModel m1 = m1f.createModel(3, 5); final SNode r1 = m1f.getRoot(1); final SNode r2c1 = m1f.getRoot(2).getFirstChild(); r1.setReferenceTarget(ourRef, r2c1); myTestModelAccess.enableRead(); m1f.attachTo(myTestRepo); // AccessCountListener1 cl1 = new AccessCountListener1(); AccessCountListener2 cl2 = new AccessCountListener2(); AccessCountListener3 cl3 = new AccessCountListener3(); m1f.attachAccessListeners(cl1, cl2, cl3); // // getReference() final SReference ref1 = r1.getReference(ourRef); myErrors.checkThat(cl1.myVisitedNodes, equalTo(0)); myErrors.checkThat(cl2.myVisitedNodes, equalTo(0)); myErrors.checkThat(cl3.myVisitedNodes, equalTo(0)); myErrors.checkThat(cl1.myReferencesRead, equalTo(1)); myErrors.checkThat(cl2.myReferencesRead, equalTo(1)); // no NodeEditorCasterInEditor update, it is notified from StaticReference#getTargetNode_internal myErrors.checkThat(cl3.myReferencesRead, equalTo(0)); myErrors.checkThat(ref1.getTargetNode(), equalTo(r2c1)); // // getReferenceTarget() cl1.reset(); cl2.reset(); cl3.reset(); final SNode t = r1.getReferenceTarget(ourRef); myErrors.checkThat(cl1.myVisitedNodes, equalTo(1)); // 1 for target node myErrors.checkThat(cl2.myVisitedNodes, equalTo(1)); // StaticReference.getTargetNode_internal myErrors.checkThat(cl3.myVisitedNodes, equalTo(1)); // StaticReference.getTargetNode_internal myErrors.checkThat(cl1.myReferencesRead, equalTo(1)); myErrors.checkThat(cl2.myReferencesRead, equalTo(1)); myErrors.checkThat(cl3.myReferencesRead, equalTo(0)); // see getReference() part, above myErrors.checkThat(t, equalTo(r2c1)); m1f.detachAccessListeners(cl1, cl2, cl3); } /** * Test notifications around add/remove of a child node * * FWIW, might be interesting to look at node read notifications - SModel.fireChildRemovedEvent() does an * extra children walk (to find out index of anchor), thus triggering superfluous node read notifications */ @Test public void testNodeAddRemoveNotify() { final TestModelFactory m1f = new TestModelFactory(); SModel m1 = m1f.createModel(3, 2); myTestModelAccess.enterCommand(); m1f.attachTo(myTestRepo); final SNode r1 = m1f.getRoot(1); final SNode c = m1f.createNode(); ChangeListener1 cl1 = new ChangeListener1(); ChangeListener2 cl2 = new ChangeListener2(); m1f.attachChangeListeners(cl1, cl2); r1.addChild(ourRole, c); myErrors.checkThat(cl1.myAdded.size(), equalTo(1)); myErrors.checkThat(cl2.myAdded.size(), equalTo(1)); myErrors.checkThat(cl1.myAdded.contains(c), equalTo(true)); myErrors.checkThat(cl2.myAdded.contains(c), equalTo(true)); myErrors.checkThat(m1f.isEditableChanged(), equalTo(true)); // cl1.reset(); cl2.reset(); m1f.clearEditableChanged(); r1.removeChild(c); myErrors.checkThat(cl1.myRemoved.size(), equalTo(1)); myErrors.checkThat(cl1.myBeforeRemoved.size(), equalTo(1)); myErrors.checkThat(cl2.myRemoved.size(), equalTo(1)); myErrors.checkThat(cl1.myRemoved.contains(c), equalTo(true)); myErrors.checkThat(cl1.myBeforeRemoved.contains(c), equalTo(true)); myErrors.checkThat(cl2.myRemoved.contains(c), equalTo(true)); myErrors.checkThat(m1f.isEditableChanged(), equalTo(true)); // cl1.reset(); cl2.reset(); final SNode anchor = r1.getFirstChild(); m1f.clearEditableChanged(); r1.insertChildBefore(ourRole, c, anchor); myErrors.checkThat(c.getNextSibling(), equalTo(anchor)); myErrors.checkThat(cl1.myAdded.size(), equalTo(1)); myErrors.checkThat(cl2.myAdded.size(), equalTo(1)); myErrors.checkThat(cl1.myAdded.contains(c), equalTo(true)); myErrors.checkThat(cl2.myAdded.contains(c), equalTo(true)); myErrors.checkThat(m1f.isEditableChanged(), equalTo(true)); m1f.detachChangeListeners(cl1, cl2); } /** * addRootNode/removeRootNode used to dispatch events and update setChanged * in a way different from add/remove of an ordinary node */ @Test public void testRootAddRemoveNotify() { final TestModelFactory m1f = new TestModelFactory(); SModel m1 = m1f.createModel(2, 2); myTestModelAccess.enterCommand(); m1f.attachTo(myTestRepo); final SNode c = m1f.createNode(); ChangeListener1 cl1 = new ChangeListener1(); ChangeListener2 cl2 = new ChangeListener2(); m1f.attachChangeListeners(cl1, cl2); // m1.addRootNode(c); myErrors.checkThat(cl1.myAddedRoots.size(), equalTo(1)); myErrors.checkThat(cl2.myAdded.size(), equalTo(1)); myErrors.checkThat(cl1.myAddedRoots.contains(c), equalTo(true)); myErrors.checkThat(cl2.myAdded.contains(c), equalTo(true)); myErrors.checkThat(IterableUtil.asCollection(m1.getRootNodes()).size(), equalTo(3)); myErrors.checkThat(m1f.isEditableChanged(), equalTo(true)); // cl1.reset(); cl2.reset(); m1f.clearEditableChanged(); m1.removeRootNode(c); myErrors.checkThat(cl1.myRemovedRoots.size(), equalTo(1)); myErrors.checkThat(cl1.myBeforeRemovedRoots.size(), equalTo(1)); myErrors.checkThat(cl2.myRemoved.size(), equalTo(1)); myErrors.checkThat(cl1.myRemovedRoots.contains(c), equalTo(true)); myErrors.checkThat(cl1.myBeforeRemovedRoots.contains(c), equalTo(true)); myErrors.checkThat(cl2.myRemoved.contains(c), equalTo(true)); myErrors.checkThat(IterableUtil.asCollection(m1.getRootNodes()).size(), equalTo(2)); myErrors.checkThat(m1f.isEditableChanged(), equalTo(true)); // // use SNode.delete m1.addRootNode(c); Assert.assertEquals(3, IterableUtil.asCollection(m1.getRootNodes()).size()); cl1.reset(); cl2.reset(); m1f.clearEditableChanged(); c.delete(); myErrors.checkThat(cl1.myRemovedRoots.size(), equalTo(1)); myErrors.checkThat(cl1.myBeforeRemovedRoots.size(), equalTo(1)); myErrors.checkThat(cl2.myRemoved.size(), equalTo(1)); myErrors.checkThat(cl1.myRemovedRoots.contains(c), equalTo(true)); myErrors.checkThat(cl1.myBeforeRemovedRoots.contains(c), equalTo(true)); myErrors.checkThat(cl2.myRemoved.contains(c), equalTo(true)); myErrors.checkThat(IterableUtil.asCollection(m1.getRootNodes()).size(), equalTo(2)); myErrors.checkThat(m1f.isEditableChanged(), equalTo(true)); m1f.detachChangeListeners(cl1, cl2); } @Test public void testPropertyChangeNotify() { final TestModelFactory m1f = new TestModelFactory(); SModel m1 = m1f.createModel(3, 2); myTestModelAccess.enterCommand(); m1f.attachTo(myTestRepo); final SNode r1 = m1f.getRoot(1); ChangeListener1 cl1 = new ChangeListener1(); ChangeListener2 cl2 = new ChangeListener2(); m1f.attachChangeListeners(cl1, cl2); final String newValue = "XXX"; r1.setProperty(SNodeUtil.property_INamedConcept_name, newValue); m1f.detachChangeListeners(cl1, cl2); Assert.assertEquals(newValue, r1.getProperty(SNodeUtil.property_INamedConcept_name)); myErrors.checkThat(cl1.myChangedProperties.size(), equalTo(1)); myErrors.checkThat(cl1.myChangedProperties.contains(SNodeUtil.property_INamedConcept_name.getName()), equalTo(true)); myErrors.checkThat(cl2.myChangedProperties.size(), equalTo(1)); myErrors.checkThat(cl2.myChangedProperties.contains(SNodeUtil.property_INamedConcept_name.getName()), equalTo(true)); } @Test public void testReferenceChangeNotify() { final TestModelFactory m1f = new TestModelFactory(); SModel m1 = m1f.createModel(3, 2); final SNode r1 = m1f.getRoot(1); final SNode r2c1 = m1f.getRoot(2).getFirstChild(); final SNode r3c2 = m1f.getRoot(3).getFirstChild().getNextSibling(); myTestModelAccess.enterCommand(); m1f.attachTo(myTestRepo); ChangeListener1 cl1 = new ChangeListener1(); ChangeListener2 cl2 = new ChangeListener2(); m1f.attachChangeListeners(cl1, cl2); // create, with setReferenceTarget() r1.setReferenceTarget(ourRef, r2c1); myErrors.checkThat(cl1.myAddedRef.size(), equalTo(1)); myErrors.checkThat(cl1.myRemovedRef.size(), equalTo(0)); myErrors.checkThat(cl2.myChangedReferences.size(), equalTo(1)); myErrors.checkThat(cl2.myChangedReferences.contains(ourRef.getRoleName()), equalTo(true)); // create, with setReference() cl1.reset(); cl2.reset(); r2c1.setReference(ourRef, jetbrains.mps.smodel.SReference.create(ourRef, r2c1, r3c2)); myErrors.checkThat(cl1.myAddedRef.size(), equalTo(1)); myErrors.checkThat(cl1.myRemovedRef.size(), equalTo(0)); myErrors.checkThat(cl2.myChangedReferences.size(), equalTo(1)); myErrors.checkThat(cl2.myChangedReferences.contains(ourRef.getRoleName()), equalTo(true)); // change, with setReference cl1.reset(); cl2.reset(); r1.setReference(ourRef, jetbrains.mps.smodel.SReference.create(ourRef, r1, r3c2)); myErrors.checkThat(cl1.myAddedRef.size(), equalTo(1)); myErrors.checkThat(cl1.myRemovedRef.size(), equalTo(1)); myErrors.checkThat(cl2.myChangedReferences.size(), equalTo(1)); myErrors.checkThat(cl2.myChangedReferences.contains(ourRef.getRoleName()), equalTo(true)); // change, with setReferenceTarget cl1.reset(); cl2.reset(); r2c1.setReferenceTarget(ourRef, r1); myErrors.checkThat(cl1.myAddedRef.size(), equalTo(1)); myErrors.checkThat(cl1.myRemovedRef.size(), equalTo(1)); myErrors.checkThat(cl2.myChangedReferences.size(), equalTo(1)); myErrors.checkThat(cl2.myChangedReferences.contains(ourRef.getRoleName()), equalTo(true)); // delete, with setReference() cl1.reset(); cl2.reset(); r2c1.setReference(ourRef, null); myErrors.checkThat(cl1.myAddedRef.size(), equalTo(0)); myErrors.checkThat(cl1.myRemovedRef.size(), equalTo(1)); myErrors.checkThat(cl2.myChangedReferences.size(), equalTo(1)); myErrors.checkThat(cl2.myChangedReferences.contains(ourRef.getRoleName()), equalTo(true)); // delete, with setReferenceTarget() cl1.reset(); cl2.reset(); r1.setReferenceTarget(ourRef, null); myErrors.checkThat(cl1.myAddedRef.size(), equalTo(0)); myErrors.checkThat(cl1.myRemovedRef.size(), equalTo(1)); myErrors.checkThat(cl2.myChangedReferences.size(), equalTo(1)); myErrors.checkThat(cl2.myChangedReferences.contains(ourRef.getRoleName()), equalTo(true)); m1f.detachChangeListeners(cl1, cl2); } /** * Explicitly state convention that access to node's meta-model or auxiliary features doesn't trigger read events * {@link org.jetbrains.mps.openapi.model.SNode#getConcept()}, * {@link org.jetbrains.mps.openapi.model.SNode#getContainmentLink()} * {@link org.jetbrains.mps.openapi.model.SNode#getReference()}} * {@link org.jetbrains.mps.openapi.model.SNode#getNodeId()} * {@link org.jetbrains.mps.openapi.model.SNode#toString()}} * * FIXME what about read notifications from getName() and getPresentation()? */ @Test public void testNoReadNotifyForMeta() { final TestModelFactory m1f = new TestModelFactory(); SModel m1 = m1f.createModel(3, 2); myTestModelAccess.enableRead(); m1f.attachTo(myTestRepo); final SNode r1 = m1f.getRoot(1); final SNode r1c1 = r1.getFirstChild(); AccessCountListener1 cl1 = new AccessCountListener1(); AccessCountListener2 cl2 = new AccessCountListener2(); AccessCountListener3 cl3 = new AccessCountListener3(); m1f.attachAccessListeners(cl1, cl2, cl3); // // getConcept() SConcept c = r1.getConcept(); myErrors.checkThat(c, equalTo(ourConcept)); myErrors.checkThat(cl1.myVisitedNodes, equalTo(0)); myErrors.checkThat(cl2.myVisitedNodes, equalTo(0)); myErrors.checkThat(cl3.myVisitedNodes, equalTo(0)); // // SNodePointer:getReference() cl1.reset(); cl2.reset(); cl3.reset(); final SNodeReference ptr = r1.getReference(); myErrors.checkThat(ptr, notNullValue()); myErrors.checkThat(cl1.myVisitedNodes, equalTo(0)); myErrors.checkThat(cl2.myVisitedNodes, equalTo(0)); myErrors.checkThat(cl3.myVisitedNodes, equalTo(0)); // // getContainmentLink() cl1.reset(); cl2.reset(); cl3.reset(); final SContainmentLink roleInParent = r1c1.getContainmentLink(); myErrors.checkThat(roleInParent, equalTo(ourRole)); myErrors.checkThat(cl1.myVisitedNodes, equalTo(0)); myErrors.checkThat(cl2.myVisitedNodes, equalTo(0)); myErrors.checkThat(cl3.myVisitedNodes, equalTo(0)); // // getNodeId() cl1.reset(); cl2.reset(); cl3.reset(); SNodeId nid = r1c1.getNodeId(); myErrors.checkThat(nid, notNullValue()); myErrors.checkThat(cl1.myVisitedNodes, equalTo(0)); myErrors.checkThat(cl2.myVisitedNodes, equalTo(0)); myErrors.checkThat(cl3.myVisitedNodes, equalTo(0)); // // toString() cl1.reset(); cl2.reset(); cl3.reset(); String presentation = r1c1.toString(); myErrors.checkThat(presentation, notNullValue()); myErrors.checkThat(cl1.myVisitedNodes, equalTo(0)); myErrors.checkThat(cl2.myVisitedNodes, equalTo(0)); myErrors.checkThat(cl3.myVisitedNodes, equalTo(0)); m1f.detachAccessListeners(cl1, cl2, cl3); } /** * Node could get attached to a repository in two cases - when a repository is available * the moment node is added to model, and another case is when model got attached to a repository * later and nodes get to know their repository the moment they are accessed from the model. * * There's a flaw in the second scenario: * Node is attached to a repository, and its children are attached the moment we iterate over them. * However, if we get a non-root node from a model by id, and then obtain its parent, the parent node * won't be attached to a repository. * * I use ModelAccess.disableRead() here just to discover the fact SNode.myRepository field is null. Indeed, * real code won't mess with non-read model activities. */ @Test public void testNodeHierarchyAttach() { final TestModelFactory m1f = new TestModelFactory(); SModel m1 = m1f.createModel(3, 2); final SNodeId r2c1 = m1f.getRoot(2).getFirstChild().getNodeId(); myTestModelAccess.enableRead(); m1f.attachTo(myTestRepo); final SNode notRoot = m1.getNode(r2c1); // [sanity] - check that disabled read indeed triggers IMAE myTestModelAccess.disableRead(); boolean gotAccessError = false; try { notRoot.getProperty(SNodeUtil.property_INamedConcept_name); } catch (IllegalModelAccessError e) { // expected, ignored gotAccessError = true; } Assert.assertTrue("Model belongs to a repository, SNode.getProperty without read access shall fail", gotAccessError); myTestModelAccess.enableRead(); SNode parent = notRoot.getParent(); myTestModelAccess.disableRead(); try { parent.getProperty(SNodeUtil.property_INamedConcept_name); Assert.fail("getParent() for a node, which is obtained through model.getNodeId(), shall get SRepository and fully-functional checkModelRead"); } catch (IllegalModelAccessError e) { // expected, ignored } } /** * SNode.getNextSibling() and getPrevSibling() shall dispatch read notification for node's parent * and for the sibling node. * Reason to dispatch parent read is that sibling change (change in result of node.getSibling()) * occurs through change in parent (i.e. addition/removal of a node) * * Reason to dispatch sibling read is that we notify node read on first access to node, and do not notify node read * or property/reference access. */ @Test public void testSiblingReadNotify() { final TestModelFactory m1f = new TestModelFactory(); SModel m1 = m1f.createModel(2, 3); myTestModelAccess.enableRead(); m1f.attachTo(myTestRepo); final SNode r1 = m1f.getRoot(1); final SNode r1c1 = r1.getFirstChild(); AccessCountListener1 cl1 = new AccessCountListener1(); AccessCountListener2 cl2 = new AccessCountListener2(); AccessCountListener3 cl3 = new AccessCountListener3(); m1f.attachAccessListeners(cl1, cl2, cl3); final SNode r1c2 = r1c1.getNextSibling(); myErrors.checkThat(cl1.myVisitedNodes, equalTo(2)); myErrors.checkThat(cl2.myVisitedNodes, equalTo(2)); myErrors.checkThat(cl3.myVisitedNodes, equalTo(2)); cl1.reset(); cl2.reset(); cl3.reset(); final SNode ps = r1c2.getPrevSibling(); myErrors.checkThat(cl1.myVisitedNodes, equalTo(2)); myErrors.checkThat(cl2.myVisitedNodes, equalTo(2)); myErrors.checkThat(cl3.myVisitedNodes, equalTo(2)); Assert.assertSame(r1c1, ps); m1f.detachAccessListeners(cl1, cl2, cl3); } /** * Capture read notifications of * {@link SModel#getNode(org.jetbrains.mps.openapi.model.SNodeId)}, * {@link org.jetbrains.mps.openapi.model.SNode#getParent()} */ @Test public void testReadNotifyOther() { final TestModelFactory m1f = new TestModelFactory(); SModel m1 = m1f.createModel(2, 3, 2); myTestModelAccess.enableRead(); m1f.attachTo(myTestRepo); final SNode r1 = m1f.getRoot(1); final SNode r1c2 = r1.getFirstChild().getNextSibling(); final SNodeId r1c2c1 = r1c2.getFirstChild().getNodeId(); AccessCountListener1 cl1 = new AccessCountListener1(); AccessCountListener2 cl2 = new AccessCountListener2(); AccessCountListener3 cl3 = new AccessCountListener3(); m1f.attachAccessListeners(cl1, cl2, cl3); SNode n = m1.getNode(r1c2c1); myErrors.checkThat(cl1.myVisitedNodes, equalTo(1)); myErrors.checkThat(cl2.myVisitedNodes, equalTo(1)); myErrors.checkThat(cl3.myVisitedNodes, equalTo(1)); Assert.assertNotNull(n); Assert.assertSame(r1c2c1, n.getNodeId()); cl1.reset(); cl2.reset(); cl3.reset(); SNode p = n.getParent(); myErrors.checkThat(cl1.myVisitedNodes, equalTo(1)); myErrors.checkThat(cl2.myVisitedNodes, equalTo(1)); myErrors.checkThat(cl3.myVisitedNodes, equalTo(1)); Assert.assertSame(r1c2, p); m1f.detachAccessListeners(cl1, cl2, cl3); } /** * {@link org.jetbrains.mps.openapi.model.SModelChangeListener} shall dispatch events for unregistered models as well. * {@link jetbrains.mps.smodel.event.SModelListener} DOES NOT dispatch events for unregistered models. */ @Test public void testChangeNotifyNoRepo() { final TestModelFactory m1f = new TestModelFactory(); SModel m1 = m1f.createModel(2, 3); myTestModelAccess.enableRead(); final SNode r1 = m1f.getRoot(1); Assert.assertNull(m1.getRepository()); ChangeListener1 cl1 = new ChangeListener1(); ChangeListener2 cl2 = new ChangeListener2(); m1f.attachChangeListeners(cl1, cl2); SNode c = m1f.createNode(); r1.addChild(ourRole, c); myErrors.checkThat(cl1.myAdded.size(), equalTo(0)); myErrors.checkThat(cl2.myAdded.size(), equalTo(1)); // cl1.reset(); cl2.reset(); c.setProperty(SNodeUtil.property_INamedConcept_name, "XXX"); myErrors.checkThat(cl1.myChangedProperties.size(), equalTo(0)); myErrors.checkThat(cl2.myChangedProperties.size(), equalTo(1)); // cl1.reset(); cl2.reset(); c.delete(); myErrors.checkThat(cl1.myRemoved.size(), equalTo(0)); myErrors.checkThat(cl2.myRemoved.size(), equalTo(1)); m1f.detachChangeListeners(cl1, cl2); } // read every property and every reference of an each node in sub-tree /*package*/ static void readTreeNodes(Iterable<? extends org.jetbrains.mps.openapi.model.SNode> nodes) { for (SNode n : nodes) { // 1 nodeRead per next() for (SProperty p : n.getProperties()) { // 1 nodeRead n.getProperty(p); } for (org.jetbrains.mps.openapi.model.SReference r : n.getReferences()) { // 1 nodeRead n.getReferenceTarget(r.getLink()); } readTreeNodes(n.getChildren()); // twice per each child - both hasNext and next trigger read event } } /*package*/ static class AccessCountListener1 implements SModelAccessListener { public int myVisitedNodes; public int myPropertiesRead; public int myReferencesRead; public void reset() { myVisitedNodes = myPropertiesRead = myReferencesRead = 0; } @Override public synchronized void nodeRead(SNode node) { myVisitedNodes++; } @Override public void propertyRead(SNode node, String name) { myPropertiesRead++; } @Override public void referenceRead(SNode node, String role) { myReferencesRead++; } } /*package*/ static class AccessCountListener2 extends AbstractNodesReadListener { public int myVisitedNodes; public int myPropertiesRead; public int myReferencesRead; public int myChildrenRead; @Override public void nodeChildReadAccess(SNode node, String childRole, SNode child) { myChildrenRead++; } @Override public void nodePropertyReadAccess(SNode node, String propertyName, String value) { myPropertiesRead++; } @Override public void nodeReferentReadAccess(SNode node, String referentRole, SNode referent) { myReferencesRead++; } @Override public void nodeUnclassifiedReadAccess(SNode node) { myVisitedNodes++; } public void reset() { myVisitedNodes = myPropertiesRead = myReferencesRead = myChildrenRead = 0; } } /*package*/ static class AccessCountListener3 extends NodeReadAccessInEditorListener { public int myVisitedNodes; public int myPropertiesRead; public int myReferencesRead; @Override public void nodePropertyReadAccess(SNode node, String propertyName, String value) { Assert.fail("NodeReadAccessCasterInEditor doesn't call this method from NodeReadAccessInEditorListener"); } @Override public void propertyDirtyReadAccess(SNode node, String propertyName) { myPropertiesRead++; } @Override public void nodeReferentReadAccess(SNode node, String referentRole, SNode referent) { myReferencesRead++; } @Override public void nodeUnclassifiedReadAccess(SNode node) { myVisitedNodes++; } public void reset() { myVisitedNodes = myPropertiesRead = myReferencesRead = 0; } } private static class ChangeListener1 extends SModelAdapter { public final List<SNode> myAdded = new ArrayList<SNode>(); public final List<SNode> myAddedRoots = new ArrayList<SNode>(); public final List<SNode> myRemoved = new ArrayList<SNode>(); public final List<SNode> myRemovedRoots = new ArrayList<SNode>(); public final List<SNode> myBeforeRemoved = new ArrayList<SNode>(); public final List<SNode> myBeforeRemovedRoots = new ArrayList<SNode>(); public final List<String> myChangedProperties = new ArrayList<String>(); public final List<SReference> myAddedRef = new ArrayList<SReference>(); public final List<SReference> myRemovedRef = new ArrayList<SReference>(); @Override public void childAdded(SModelChildEvent event) { myAdded.add(event.getChild()); } @Override public void childRemoved(SModelChildEvent event) { myRemoved.add(event.getChild()); } @Override public void beforeChildRemoved(SModelChildEvent event) { myBeforeRemoved.add(event.getChild()); } @Override public void propertyChanged(SModelPropertyEvent event) { myChangedProperties.add(event.getPropertyName()); } @Override public void referenceAdded(SModelReferenceEvent event) { myAddedRef.add(event.getReference()); } @Override public void referenceRemoved(SModelReferenceEvent event) { myRemovedRef.add(event.getReference()); } @Override public void rootAdded(SModelRootEvent event) { myAddedRoots.add(event.getRoot()); } @Override public void rootRemoved(SModelRootEvent event) { myRemovedRoots.add(event.getRoot()); } @Override public void beforeRootRemoved(SModelRootEvent event) { myBeforeRemovedRoots.add(event.getRoot()); } /*package*/ void reset() { myAdded.clear(); myRemoved.clear(); myBeforeRemoved.clear(); myChangedProperties.clear(); myAddedRef.clear(); myRemovedRef.clear(); myAddedRoots.clear(); myRemovedRoots.clear(); myBeforeRemovedRoots.clear(); } } private static class ChangeListener2 implements SModelChangeListener { // use list, not set to check for number of events, even if they come for the same object, to notice excessive notifications public final List<SNode> myAdded = new ArrayList<SNode>(); public final List<SNode> myRemoved = new ArrayList<SNode>(); public final List<String> myChangedProperties = new ArrayList<String>(); public final List<String> myChangedReferences = new ArrayList<String>(); @Override public void nodeAdded(SModel model, SNode parent, String role, SNode child) { myAdded.add(child); } @Override public void nodeRemoved(SModel model, SNode parent, String role, SNode child) { myRemoved.add(child); } @Override public void propertyChanged(SNode node, String propertyName, String oldValue, String newValue) { myChangedProperties.add(propertyName); } @Override public void referenceChanged(SNode node, String role, SReference oldRef, SReference newRef) { myChangedReferences.add(role); } /*package*/ void reset() { myAdded.clear(); myRemoved.clear(); myChangedProperties.clear(); myChangedReferences.clear(); } } }