/* * 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.extapi.model.EditableSModelBase; import jetbrains.mps.extapi.model.SModelBase; import jetbrains.mps.extapi.model.SModelData; import jetbrains.mps.extapi.module.SRepositoryBase; import jetbrains.mps.project.ModuleId; import jetbrains.mps.project.structure.modules.ModuleReference; import jetbrains.mps.smodel.adapter.BootstrapAdapterFactory; import jetbrains.mps.smodel.event.SModelListener; import jetbrains.mps.smodel.loading.ModelLoadingState; import jetbrains.mps.util.IterableUtil; import jetbrains.mps.util.annotation.ToRemove; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.mps.openapi.language.SConcept; import org.jetbrains.mps.openapi.language.SContainmentLink; import org.jetbrains.mps.openapi.language.SReferenceLink; import org.jetbrains.mps.openapi.model.EditableSModel; 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.module.RepositoryAccess; import org.jetbrains.mps.openapi.module.SModule; import org.jetbrains.mps.openapi.module.SModuleId; import org.jetbrains.mps.openapi.module.SRepository; import org.jetbrains.mps.openapi.persistence.ModelSaveException; import org.jetbrains.mps.openapi.persistence.NullDataSource; import java.io.IOException; import java.util.ArrayDeque; /** * Utility factory to create and fill test models * @author Artem Tikhomirov */ final class TestModelFactory { /*package*/ static SConcept ourConcept = BootstrapAdapterFactory.getConcept(1, 2, 3, "C"); /*package*/ static SContainmentLink ourRole = BootstrapAdapterFactory.getContainmentLink(1, 2, 3, 4, "L"); /*package*/ static SReferenceLink ourRef = BootstrapAdapterFactory.getReferenceLink(1, 2, 3, 5, "R"); /* * Blank SNode: 120 bytes (8-byte aligned. in fact, 116, as adding 1 extra field doesn't change its overall size) * SNode with 1 property (name): 144 bytes * SNode with 2 properties: 152 bytes * Without REPO_LOCK: 88 bytes (-32 bytes: 1 field + 16 bytes Object * SNodeId: 24 bytes * Now SNode is 80 bytes (including those 24 of SNodeId, and it's exactly that - adding 1 more field makes it 88) * There are 11 fields in the class, 4*11 + 12 == 56? * ==> * 4 bytes per reference * 16 bytes per object instance - actually 12 as adding 1 field doesn't change the size. * 16 bytes per new Object[0], 24 bytes for an Object[2] (if 4 bytes per ref is active). */ private final boolean myNeedEditableModel; private int myIssuedNodes = 0; private SModel myModel; public TestModelFactory() { this(true); } private TestModelFactory(boolean editable) { myNeedEditableModel = editable; } /** * Create a model with a tree of nodes. * * It's an instance method as I envision model to be owned by this class, which would keep extra info about created model (e.g. * number of initial nodes to get rid of constants like (3*5*2 + 3*5 + 3) from the tests code * * @param nodesAtLevel number of child elements for each parent (i.e. of previous level) element. Each node at level i has nodesAtLevel[i] children */ public SModel createModel(@Nullable int... nodesAtLevel) { final SNode top = createNode(nodesAtLevel); final jetbrains.mps.smodel.SModel modelData = new jetbrains.mps.smodel.SModel(new SModelReference(new ModuleReference("M", ModuleId.regular()), SModelId.generate(), "m")); for (SNode c : top.getChildren(ourRole)) { modelData.addRootNode(c); } assert myNeedEditableModel; return myModel = new TestModelBase(modelData); } public SNode createNode(@Nullable int... childrenAtLevel) { ArrayDeque<SNode> thisLevel = new ArrayDeque<SNode>(); ArrayDeque<SNode> nextLevel = new ArrayDeque<SNode>(); final SNode top = new jetbrains.mps.smodel.SNode(ourConcept); thisLevel.add(top); if (childrenAtLevel == null || childrenAtLevel.length == 0) { return top; } for (int count : childrenAtLevel) { while (!thisLevel.isEmpty()) { SNode parent = thisLevel.removeFirst(); for (int i = 0; i < count; i++) { SNode c = new jetbrains.mps.smodel.SNode(ourConcept); final String v = nextNodeName(i + 1); c.setProperty(SNodeUtil.property_INamedConcept_name, v); c.setProperty(SNodeUtil.property_BaseConcept_alias, v); parent.addChild(ourRole, c); nextLevel.add(c); } } ArrayDeque<SNode> t = thisLevel; thisLevel = nextLevel; nextLevel = t; } return top; } public org.jetbrains.mps.openapi.model.SModel getModel() { assert myModel != null : "call createModel() first"; return myModel; } public void attachTo(SRepository repo) { assert myModel != null : "call createModel() first"; ((SModelBase) myModel).attach(repo); } public SModelData getModelData() { assert myModel != null : "call createModel() first"; return ((SModelBase) myModel).getModelData(); } public org.jetbrains.mps.openapi.model.SNode getRoot(int oneBasedIndex) { assert myModel != null : "call createModel() first"; for (SNode r : myModel.getRootNodes()) { if (--oneBasedIndex > 0) { continue; } return r; } throw new IllegalArgumentException(Integer.toString(IterableUtil.asCollection(myModel.getRootNodes()).size())); } public int countModelNodes() { return countTreeNodes(myModel.getRootNodes()); } void attachAccessListeners(SModelAccessListener l1, INodesReadListener l2, NodeReadAccessInEditorListener l3) { assert myModel != null : "call createModel() first"; myModel.addAccessListener(l1); NodeReadEventsCaster.setNodesReadListener(l2); NodeReadAccessCasterInEditor.setCellBuildNodeReadAccessListener(l3); } void detachAccessListeners(SModelAccessListener l1, INodesReadListener l2, NodeReadAccessInEditorListener l3) { assert myModel != null : "call createModel() first"; NodeReadAccessCasterInEditor.removeCellBuildNodeAccessListener(); NodeReadEventsCaster.removeNodesReadListener(); myModel.removeAccessListener(l1); } public void clearEditableChanged() { assert myNeedEditableModel; assert myModel != null : "call createModel() first"; ((EditableSModel) myModel).setChanged(false); } public boolean isEditableChanged() { assert myNeedEditableModel; assert myModel != null : "call createModel() first"; return ((EditableSModel) myModel).isChanged(); } void attachChangeListeners(SModelListener l1, SModelChangeListener l2) { assert myNeedEditableModel; assert myModel != null : "call createModel() first"; ((SModelInternal) myModel).addModelListener(l1); ((EditableSModel) myModel).addChangeListener(l2); } void detachChangeListeners(SModelListener l1, SModelChangeListener l2) { assert myNeedEditableModel; assert myModel != null : "call createModel() first"; ((SModelInternal) myModel).removeModelListener(l1); ((EditableSModel) myModel).removeChangeListener(l2); } private String nextNodeName(int i) { return String.format("%d-n%d", i, myIssuedNodes++); } // doesn't trigger property/reference reads /*package*/ static int countTreeNodes(Iterable<? extends org.jetbrains.mps.openapi.model.SNode> nodes) { int rv = 0; for (org.jetbrains.mps.openapi.model.SNode n : nodes) { rv++; rv += countTreeNodes(n.getChildren()); } return rv; } // FIXME node add/remove operations don't require EditableSModelBase to dispatch events any more, and we may get back SModelBase as superclass // however, at the moment, ModelListenerTest registers listeners through legacy API (to ensure they work as expected), and unless we drop this // old code after 3.3, this class has to be EditableSModelBase @ToRemove(version = 3.3) private static class TestModelBase extends EditableSModelBase { private final jetbrains.mps.smodel.SModel myModelData; public TestModelBase(jetbrains.mps.smodel.SModel modelData) { super(modelData.getReference(), new NullDataSource()); myModelData = modelData; myModelData.setModelDescriptor(this); setLoadingState(ModelLoadingState.FULLY_LOADED); } @Override public jetbrains.mps.smodel.SModel getSModelInternal() { return myModelData; } @Nullable @Override protected jetbrains.mps.smodel.SModel getCurrentModelInternal() { return myModelData; } @Override protected void doUnload() { // no-op } @Override protected void reloadContents() { // no-op } @Override protected boolean saveModel() throws IOException, ModelSaveException { return false; } } /*package*/ static class TestRepository extends SRepositoryBase { private final org.jetbrains.mps.openapi.module.ModelAccess myModelAccess; public TestRepository(org.jetbrains.mps.openapi.module.ModelAccess ma) { myModelAccess = ma; } @Override public SModule getModule(@NotNull SModuleId ref) { return null; } @NotNull @Override public Iterable<SModule> getModules() { return null; } @NotNull @Override public org.jetbrains.mps.openapi.module.ModelAccess getModelAccess() { return myModelAccess; } @Override public RepositoryAccess getRepositoryAccess() { return null; } @Override public void saveAll() { // no-op } } /*package*/ static class TestModelAccess extends ModelAccessBase { private boolean myCanRead; private boolean myCanWrite; private int myCommandCount = 0; void disableRead() { myCanRead = myCanWrite = false; } void enableRead() { myCanRead = true; myCanWrite = false; } void enableWrite() { myCanRead = myCanWrite = true; } void disableWrite() { myCanRead = myCanWrite = false; } void enterCommand() { if (myCommandCount++ == 0) { enableWrite(); } } void leaveCommand() { if (--myCommandCount == 0) { disableWrite(); } } @Override public boolean canRead() { return myCanRead; } @Override public void checkReadAccess() { if (!canRead()) { throw new IllegalModelAccessError("READ"); } } @Override public boolean canWrite() { return myCanWrite; } @Override public void checkWriteAccess() { if (!canWrite()) { throw new IllegalModelAccessError("WRITE"); } } @Override public void executeCommand(Runnable r) { myCommandCount++; r.run(); myCommandCount--; } @Override public void executeCommandInEDT(Runnable r) { myCommandCount++; r.run(); myCommandCount--; } @Override public void executeUndoTransparentCommand(Runnable r) { r.run(); } @Override public boolean isCommandAction() { return myCommandCount > 0; } } }