/*******************************************************************************
* Copyright (c) 2011 SAP AG and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* SAP AG - initial API and implementation
******************************************************************************/
package com.sap.furcas.referenceresolving.tests;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import java.io.File;
import java.util.Collection;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.ocl.ecore.opposites.DefaultOppositeEndFinder;
import org.eclipse.ocl.ecore.opposites.OppositeEndFinder;
import org.junit.BeforeClass;
import org.junit.Test;
import com.sap.furcas.metamodel.FURCAS.textblocks.LexedToken;
import com.sap.furcas.metamodel.FURCAS.textblocks.TextBlock;
import com.sap.furcas.metamodel.FURCAS.textblocks.TextblocksPackage;
import com.sap.furcas.metamodel.FURCAS.textblocks.Version;
import com.sap.furcas.runtime.referenceresolving.Activator;
import com.sap.furcas.runtime.referenceresolving.TokenChanger;
import com.sap.furcas.runtime.textblocks.model.TextBlocksModel;
import com.sap.furcas.runtime.textblocks.modifcation.TbChangeUtil;
import com.sap.furcas.runtime.textblocks.modifcation.TbVersionUtil;
import com.sap.furcas.test.fixture.ScenarioFixtureData;
import com.sap.ide.cts.parser.errorhandling.SemanticParserException;
import com.sap.ide.cts.parser.errorhandling.SemanticParserException.Component;
/**
* Tests NestedScopes TCS and metamodel and impact analysis behavior on renames using the NestedScopes language.
*
* @author Sebastian Schlag (D049672)
*
*/
public class TestNestedScopesWithTextBlocks extends AbstractReferenceResolvingTest {
private static final String LANGUAGE = "NestedScopesTestSyntax";
private static final File TCS = ScenarioFixtureData.NESTED_SCOPE_TCS;
private static final File METAMODEL = ScenarioFixtureData.NESTED_SCOPE_METAMODEL;
@BeforeClass
public static void setupParser() throws Exception {
setupParser(LANGUAGE, TCS, METAMODEL);
}
/**
* TCS and metamodel test: Usages should be bound to the corresponding definition.
*/
@Test
public void testResolvableBindingsBasicExample() throws Exception {
String sample = "{ def a; use a;" + "{ def b; use b; }" + "}";
setupModelFromTextToParse(sample);
assertNotNull(rootElement);
EObject defA = getStatementNonNestingLevelM(1, 0);
assertEquals("Definition", defA.eClass().getName());
assertEquals("a", defA.eGet(defA.eClass().getEStructuralFeature("name")));
EObject useA = getStatementNonNestingLevelM(2, 0);
assertEquals("Usage", useA.eClass().getName());
assertSame(useA.eGet(useA.eClass().getEStructuralFeature("boundDefinition")), defA);
EObject defB = getStatementNonNestingLevelM(1, 1);
assertEquals("Definition", defB.eClass().getName());
assertEquals("b", defB.eGet(defB.eClass().getEStructuralFeature("name")));
EObject useB = getStatementNonNestingLevelM(2, 1);
assertEquals("Usage", useB.eClass().getName());
assertSame(useB.eGet(useB.eClass().getEStructuralFeature("boundDefinition")), defB);
}
/**
* TCS and metamodel test: Definition of a should not be visible outside the inner scope. Therefore the boundDefinition
* property of Usage "a" should not be set.
*/
@Test
public void testDefinitionNotVisibleOutsideOfScope() {
boolean failed = false;
String sample = "{" + "{ def a; }" + "use a; }";
try {
setupModelFromTextToParse(sample);
} catch (SemanticParserException e) {
assertTrue(e.getComponentThatFailed() == Component.SEMANTIC_ANALYSIS);
failed = true;
}
// if semantic analysis failed the root element should have been created.
// It should be valid in all aspect except for the missing reference
rootElement = TbVersionUtil.getOtherVersion(rootTextBlock, Version.CURRENT).getCorrespondingModelElements().iterator().next();
EObject useA = getStatementNonNestingLevelM(2, 0);
assertEquals("Usage", useA.eClass().getName());
assertFalse(useA.eIsSet((useA.eClass().getEStructuralFeature("boundDefinition"))));
assertTrue(failed);
}
/**
* TCS and metamodel test: "Use before declaration" should not be possible.
*/
@Test
public void testUseBeforeDeclaration() {
boolean failed = false;
String sample = "{use a; def a;}";
try {
setupModelFromTextToParse(sample);
} catch (SemanticParserException e) {
assertTrue(e.getComponentThatFailed() == Component.SEMANTIC_ANALYSIS);
failed = true;
}
// if semantic analysis failed the root element should have been created.
// It should be valid in all aspect except for the missing reference
rootElement = TbVersionUtil.getOtherVersion(rootTextBlock, Version.CURRENT).getCorrespondingModelElements().iterator().next();
EObject useA = getStatementNonNestingLevelM(1, 0);
assertEquals("Usage", useA.eClass().getName());
assertFalse(useA.eIsSet((useA.eClass().getEStructuralFeature("boundDefinition"))));
assertTrue(failed);
}
/**
* TCS and metamodel test: Usage should be bound to the innermost definition of a.
*/
@Test
public void testShadowing() throws SemanticParserException {
String sample = "{ def a;" + "{def a; use a;}" + "}";
setupModelFromTextToParse(sample);
assertNotNull(rootElement);
EObject firstDefA = getStatementNonNestingLevelM(1, 0);
assertEquals("Definition", firstDefA.eClass().getName());
assertEquals("a", firstDefA.eGet(firstDefA.eClass().getEStructuralFeature("name")));
EObject secondDefA = getStatementNonNestingLevelM(1, 1);
assertEquals("Definition", secondDefA.eClass().getName());
assertEquals("a", secondDefA.eGet(secondDefA.eClass().getEStructuralFeature("name")));
EObject useA = getStatementNonNestingLevelM(2, 1);
assertEquals("Usage", useA.eClass().getName());
assertSame(useA.eGet(useA.eClass().getEStructuralFeature("boundDefinition")), secondDefA);
assertNotSame(useA.eGet(useA.eClass().getEStructuralFeature("boundDefinition")), firstDefA);
}
/**
* After renaming the definition of "a" to "b", the usage's boundDefinition attribute should be bound to the new definition of
* "b" inside the inner scope instead of being bound to the definition in the outer scope.
*/
@Test
public void testRebindingToDefinitionInInnerScope() throws SemanticParserException {
String sample = "{ def b;" + "{ def a; use b; }" + "}";
setupModelFromTextToParse(sample);
assertNotNull(rootElement);
EObject bDefinitionOuterScope = getStatementNonNestingLevelM(1, 0);
EObject definitionInnerScope = getStatementNonNestingLevelM(1, 1);
EObject bUsageInnerScope = getStatementNonNestingLevelM(2, 1);
assertEquals("Definition", definitionInnerScope.eClass().getName());
assertEquals("a", definitionInnerScope.eGet(definitionInnerScope.eClass().getEStructuralFeature("name")));
assertEquals("Usage", bUsageInnerScope.eClass().getName());
assertSame(bUsageInnerScope.eGet(bUsageInnerScope.eClass().getEStructuralFeature("boundDefinition")),bDefinitionOuterScope);
renameElementOnModel(definitionInnerScope, "b");
assertEquals("b", definitionInnerScope.eGet(definitionInnerScope.eClass().getEStructuralFeature("name")));
assertSame(bUsageInnerScope.eGet(bUsageInnerScope.eClass().getEStructuralFeature("boundDefinition")),definitionInnerScope);
}
/**
* Tests explicitly that Impact Analysis chooses the correct element to which the usage is bound out of all elements contained
* in the lookup scope. Thus after renaming the definition of "b" to "d" the usage should still be bound to this definition.
*/
@Test
public void testChoosingOfcorrectLookupScopeElemen() throws SemanticParserException {
String sample = "{ def a; def b; def c; use b; }";
setupModelFromTextToParse(sample);
EObject bDefinition = getStatementNonNestingLevelM(2, 0);
EObject bUsage = getStatementNonNestingLevelM(4, 0);
assertSame(bDefinition, bUsage.eGet(bUsage.eClass().getEStructuralFeature("boundDefinition")));
renameElementOnModel(bDefinition, "d");
assertSame(bDefinition, bUsage.eGet(bUsage.eClass().getEStructuralFeature("boundDefinition")));
}
/**
* Tests that if a usage is bound and it's bound definition is in the current lookup scope, Impact Analysis updates the
* reference according to the result of referenceBy on the bound element. Thus after renaming the definition of "b" to "a" the
* usage should still be bound to this definition (meaning that the usage is also renamed since it was directly bound to this
* definition).
*
* This test case also asserts that a {@link TokenChanger} registered with the
* {@link Activator} will be requested to update the token value.
*/
@Test
public void testCorrectBindingIfBoundElementIsStillInLookupScopeAfterRename() throws SemanticParserException {
String sample = "{ def b; use b; }";
final boolean[] receivedRequestToUpdateTokenValue = new boolean[1];
TokenChanger tokenChanger = new TokenChanger() {
@Override
public void requestTokenValueChange(LexedToken token, String oldTokenValue, String newTokenValue) {
receivedRequestToUpdateTokenValue[0] = oldTokenValue.equals("b") && newTokenValue.equals("a");
}
};
syntaxRegistry.addTokenChanger(tokenChanger);
setupModelFromTextToParse(sample);
assertNotNull(rootElement);
EObject bDefinition = getStatementNonNestingLevelM(1, 0);
EObject bUsage = getStatementNonNestingLevelM(2, 1);
assertEquals("Definition", bDefinition.eClass().getName());
assertEquals("b", bDefinition.eGet(bDefinition.eClass().getEStructuralFeature("name")));
assertEquals("Usage", bUsage.eClass().getName());
assertSame(bUsage.eGet(bUsage.eClass().getEStructuralFeature("boundDefinition")), bDefinition);
renameElementOnModel(bDefinition, "a");
assertSame(bUsage.eGet(bUsage.eClass().getEStructuralFeature("boundDefinition")), bDefinition);
assertTrue("Expected to receive request to update token value for a token with value \"b\" to \"a\" but didn't",
receivedRequestToUpdateTokenValue[0]);
syntaxRegistry.removeTokenValueChanger(tokenChanger);
}
/**
* Tests that if a usage was bound, but the definition to which it was bound ("def a") is no longer contained in the lookup
* scope due to shadowing, Impact Analysis uses the name of this previously bound definition to perform a fresh lookup and sets the reference
* based on the lookup result.
*/
@Test
public void testCorrectBindingIfBoundElementIsNoLongerInLookupScopeAfterRenameWithShadowing() throws SemanticParserException {
String sample = "{ def a;" + "{ def b; use a;}" + "}";
setupModelFromTextToParse(sample);
EObject aDefinition = getStatementNonNestingLevelM(1, 0);
EObject bDefinition = getStatementNonNestingLevelM(1, 1);
EObject aUsage = getStatementNonNestingLevelM(2, 1);
assertSame(aDefinition, aUsage.eGet(aUsage.eClass().getEStructuralFeature("boundDefinition")));
renameElementOnModel(bDefinition, "a");
assertSame(bDefinition, aUsage.eGet(aUsage.eClass().getEStructuralFeature("boundDefinition")));
}
/**
* Tests that if a usage initially was bound, but the definition to which it was bound ("def a") is no longer contained in the lookup
* scope due to a textual rename of the usage's token value, Impact Analysis breaks the boundDefinition reference.
*/
@Test
public void testCorrectBindingIfBoundElementIsNoLongerInLookupScopeAfterRenameWithoutShadowing() throws SemanticParserException {
String sample = "{ def a; { def b; use a;} }";
setupModelFromTextToParse(sample);
EObject aDefinition = getStatementNonNestingLevelM(1, 0);
EObject bDefinition = getStatementNonNestingLevelM(1, 1);
EObject aUsage = getStatementNonNestingLevelM(2, 1);
assertSame(aDefinition, aUsage.eGet(aUsage.eClass().getEStructuralFeature("boundDefinition")));
renameElementOnTextBlock("{ def ".length(), "a".length(), "d");
OppositeEndFinder oppositeEndFinder = DefaultOppositeEndFinder.getInstance();
LexedToken newLexedTokenOfUsage = findCurrentReferenceTokenReferencing(aDefinition, oppositeEndFinder);
assertNotNull(newLexedTokenOfUsage);
//assertEquals("d", newLexedTokenOfUsage.getValue());
assertEquals("d", aDefinition.eGet(aDefinition.eClass().getEStructuralFeature("name")));
//assertFalse("boundDefinition reference should not be set",
// aUsage.eIsSet((aUsage.eClass().getEStructuralFeature("boundDefinition"))));
renameElementOnModel(bDefinition, "a");
//assertFalse(aUsage.eIsSet((aUsage.eClass().getEStructuralFeature("boundDefinition"))));
}
private LexedToken findCurrentReferenceTokenReferencing(EObject aDefinition, OppositeEndFinder oppositeEndFinder) {
for (EObject o : oppositeEndFinder.navigateOppositePropertyWithBackwardScope(TextblocksPackage.eINSTANCE.getLexedToken_ReferencedElements(), aDefinition)) {
if (EcoreUtil.getRootContainer(o) == rootTextBlock) {
return (LexedToken) o;
}
}
return null;
}
/**
* Tests that if a usage was not bound to a definition before a rename, Impact analysis performs a
* fresh lookup. It then should set the reference based on the lookup result.
*/
@Test
public void testCorrectBindingIfElementWasNotBoundBeforeRenameVariant1() throws SemanticParserException {
String sample = "{ def a;" + "{ def b; use a; }" + "}";
setupModelFromTextToParse(sample);
EObject aDefinition = getStatementNonNestingLevelM(1, 0);
EObject bDefinition = getStatementNonNestingLevelM(1, 1);
EObject aUsage = getStatementNonNestingLevelM(2, 1);
assertEquals("Definition", aDefinition.eClass().getName());
EcoreUtil.delete(aDefinition);
assertFalse(aUsage.eIsSet(aUsage.eClass().getEStructuralFeature("boundDefinition")));
renameElementOnModel(bDefinition, "a");
assertSame(bDefinition, aUsage.eGet(aUsage.eClass().getEStructuralFeature("boundDefinition")));
}
/**
* Variant of testCorrectBindingIfElementWasNotBoundBeforeRename1 that
* checks that a rename operation does not accidentally trigger the binding
* of a usage to a wrong definition.
*/
@Test
public void testCorrectBindingIfElementWasNotBoundBeforeRenameVariant2() throws SemanticParserException {
String sample = "{ def a;" + "{ def b; use a; }" + "}";
setupModelFromTextToParse(sample);
EObject aDefinition = getStatementNonNestingLevelM(1, 0);
EObject bDefinition = getStatementNonNestingLevelM(1, 1);
EObject aUsage = getStatementNonNestingLevelM(2, 1);
assertEquals("Definition", aDefinition.eClass().getName());
EcoreUtil.delete(aDefinition);
assertFalse(aUsage.eIsSet(aUsage.eClass().getEStructuralFeature("boundDefinition")));
renameElementOnModel(bDefinition, "c");
assertFalse(aUsage.eIsSet(aUsage.eClass().getEStructuralFeature("boundDefinition")));
}
private EObject getStatementNonNestingLevelM(int n, int m) {
EObject currentBlock = rootElement;
for (int nestingLevel = 0; nestingLevel < m; nestingLevel++) {
@SuppressWarnings("unchecked")
Collection<EObject> statements = (Collection<EObject>) currentBlock.eGet(currentBlock.eClass().getEStructuralFeature("statementsInBlock"));
for (EObject statement : statements) {
if (statement.eClass().getName().equals("Block")) {
currentBlock = statement;
}
}
}
@SuppressWarnings("unchecked")
Collection<EObject> statmentsInBlockM = (Collection<EObject>) currentBlock.eGet(currentBlock.eClass().getEStructuralFeature("statementsInBlock"));
return (EObject) statmentsInBlockM.toArray()[n - 1];
}
private void renameElementOnModel(EObject element, String newValue) {
element.eSet(element.eClass().getEStructuralFeature("name"), newValue);
}
private void renameElementOnTextBlock(int replacedRegionOffset, int replacedRegionLength, String newText) throws SemanticParserException {
TextBlocksModel model = new TextBlocksModel(rootTextBlock);
model.replace(replacedRegionOffset, replacedRegionLength, newText);
TextBlock currentVersionTb = incrementalParserFacade.parseIncrementally(rootTextBlock);
rootTextBlock = (TextBlock) TbChangeUtil.cleanUp(currentVersionTb);
}
}