/******************************************************************************* | |
* Copyright (c) 2018 Aston University. | |
* | |
* This program and the accompanying materials are made available under the | |
* terms of the Eclipse Public License 2.0 which is available at | |
* http://www.eclipse.org/legal/epl-2.0. | |
* | |
* This Source Code may also be made available under the following Secondary | |
* Licenses when the conditions for such availability set forth in the Eclipse | |
* Public License, v. 2.0 are satisfied: GNU General Public License, version 3. | |
* | |
* SPDX-License-Identifier: EPL-2.0 OR GPL-3.0 | |
* | |
* Contributors: | |
* Antonio Garcia-Dominguez - initial API and implementation | |
******************************************************************************/ | |
package org.hawk.integration.tests.emf; | |
import static org.junit.Assert.assertEquals; | |
import static org.junit.Assert.assertTrue; | |
import static org.junit.Assert.fail; | |
import static org.junit.Assume.assumeFalse; | |
import java.io.File; | |
import java.io.IOException; | |
import java.util.ArrayList; | |
import java.util.HashMap; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.Random; | |
import java.util.stream.Collectors; | |
import org.apache.commons.io.FileUtils; | |
import org.apache.commons.io.filefilter.TrueFileFilter; | |
import org.eclipse.emf.common.util.TreeIterator; | |
import org.eclipse.emf.common.util.URI; | |
import org.eclipse.emf.ecore.EObject; | |
import org.eclipse.emf.ecore.EPackage; | |
import org.eclipse.emf.ecore.EStructuralFeature; | |
import org.eclipse.emf.ecore.resource.Resource; | |
import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl; | |
import org.eclipse.emf.ecore.xmi.impl.XMIResourceFactoryImpl; | |
import org.hawk.backend.tests.BackendTestSuite; | |
import org.hawk.backend.tests.factories.IGraphDatabaseFactory; | |
import org.hawk.core.query.IQueryEngine; | |
import org.hawk.epsilon.emc.EOLQueryEngine; | |
import org.hawk.graph.syncValidationListener.SyncValidationListener; | |
import org.hawk.graph.syncValidationListener.SyncValidationListener.ValidationError; | |
import org.hawk.integration.tests.ModelIndexingTest; | |
import org.junit.Before; | |
import org.junit.Rule; | |
import org.junit.Test; | |
import org.junit.rules.TemporaryFolder; | |
import org.junit.runners.Parameterized.Parameters; | |
/** | |
* Integration test case that indexes a fragmented version of the GraBaTs'09 | |
* set0 model, and runs queries on containment subtrees with the | |
* {@link IQueryEngine#PROPERTY_SUBTREECONTEXT} through | |
* {@link EOLQueryEngine#getAllOf(String, String, String)} and | |
* {@link EOLQueryEngine#getAllOfKind(String)}. Has a small program to do the | |
* fragmentation at multiple levels. | |
*/ | |
public class SubtreeContextTest extends ModelIndexingTest { | |
private static class Fragmenter { | |
private static final String pathToMetamodel = "resources/metamodels/JDTAST.ecore"; | |
private static final String pathToOriginal = "resources/models/set0/set0.xmi"; | |
private static final String pathToFragmented = "resources/models/set0-fragmented/set0.xmi"; | |
private ResourceSetImpl rs; | |
public void run() throws IOException { | |
rs = new ResourceSetImpl(); | |
rs.getResourceFactoryRegistry().getExtensionToFactoryMap().put("*", new XMIResourceFactoryImpl()); | |
Resource rEcoreMM = rs.createResource(URI.createFileURI(new File(pathToMetamodel).getAbsolutePath())); | |
rEcoreMM.load(null); | |
for (EObject rawEPackage : rEcoreMM.getContents()) { | |
EPackage epackage = (EPackage) rawEPackage; | |
rs.getPackageRegistry().put(epackage.getNsURI(), epackage); | |
} | |
rs.getResources().remove(rEcoreMM); | |
final File fOriginal = new File(pathToOriginal); | |
Resource rRoot = rs.createResource(URI.createFileURI(fOriginal.getAbsolutePath())); | |
rRoot.load(null); | |
final File fFragmented = new File(pathToFragmented); | |
if (fFragmented.getParentFile().exists()) { | |
FileUtils.deleteDirectory(fFragmented.getParentFile()); | |
} | |
rRoot.setURI(URI.createFileURI(fFragmented.getAbsolutePath())); | |
fragment(rRoot); | |
for (Resource r : rs.getResources()) { | |
r.save(null); | |
} | |
for (Resource r : rs.getResources()) { | |
r.unload(); | |
} | |
rs = null; | |
} | |
private void fragment(Resource rRoot) { | |
File fRoot = new File(rRoot.getURI().toFileString()); | |
File fRootFolder = fRoot.getParentFile(); | |
for (TreeIterator<EObject> itContents = rRoot.getAllContents(); itContents.hasNext();) { | |
EObject eob = itContents.next(); | |
if (rRoot.getContents().contains(eob)) { | |
// No roots (otherwise, we'd have endless recursion) | |
continue; | |
} | |
final String eClassName = eob.eClass().getName(); | |
switch (eClassName) { | |
case "IJavaProject": | |
case "BinaryPackageFragmentRoot": | |
case "IPackageFragment": | |
EStructuralFeature sf = eob.eClass().getEStructuralFeature("elementName"); | |
String name = eob.eGet(sf).toString(); | |
final File fFolder = new File(fRootFolder, String.format("%s_%s", eClassName, name)); | |
fFolder.mkdirs(); | |
final File fChild = new File(fFolder, String.format("%s_%s.xmi", eClassName, name)); | |
final URI uriChild = URI.createFileURI(fChild.getAbsolutePath()); | |
Resource r = rs.createResource(uriChild); | |
itContents.prune(); | |
r.getContents().add(eob); | |
fragment(r); | |
break; | |
} | |
} | |
} | |
} | |
@Rule | |
public GraphChangeListenerRule<SyncValidationListener> syncValidation = new GraphChangeListenerRule<>( | |
new SyncValidationListener()); | |
@Rule | |
public TemporaryFolder modelFolder = new TemporaryFolder(); | |
private File folderOriginal, folderFragmented; | |
private String originalRepoURI, fragmentedRepoURI; | |
@Parameters(name = "{0}") | |
public static Iterable<Object[]> params() { | |
return BackendTestSuite.caseParams(); | |
} | |
public SubtreeContextTest(IGraphDatabaseFactory dbf) { | |
super(dbf, new EMFModelSupportFactory()); | |
} | |
@Before | |
public void setUp() throws Throwable { | |
// Skip for deprecated backends - it is an expensive test | |
assumeFalse(db.getClass().isAnnotationPresent(Deprecated.class)); | |
indexer.registerMetamodels(new File("resources/metamodels/Ecore.ecore"), | |
new File("resources/metamodels/JDTAST.ecore")); | |
folderOriginal = new File("resources/models/set0").getAbsoluteFile(); | |
folderFragmented = new File("resources/models/set0-fragmented").getAbsoluteFile(); | |
originalRepoURI = folderOriginal.toPath().toUri().toString(); | |
fragmentedRepoURI = folderFragmented.toPath().toUri().toString(); | |
} | |
protected void prepareBoth() throws Exception, Throwable { | |
prepareOriginal(); | |
prepareFragmented(); | |
} | |
protected void prepareFragmented() throws Exception, Throwable { | |
requestFolderIndex(folderFragmented); | |
scheduleAndWait(() -> { | |
assertNoValidationErrors("There should be no validation errors on the fragmented model"); | |
return null; | |
}); | |
} | |
private void assertNoValidationErrors(String message) { | |
List<ValidationError> errors = syncValidation.getListener().getErrors(); | |
if (!errors.isEmpty()) { | |
List<String> messages = errors.stream() | |
.map(e -> e.getMessage()) | |
.collect(Collectors.toList()); | |
fail(message + ":\n" + String.join("\n\n", messages)); | |
} | |
} | |
protected void prepareOriginal() throws Exception, Throwable { | |
requestFolderIndex(folderOriginal); | |
scheduleAndWait(() -> { | |
assertNoValidationErrors("There should be no validation errors on the original model"); | |
return null; | |
}); | |
} | |
@Test | |
public void allContents() throws Throwable { | |
prepareBoth(); | |
// Sanity check for .allContents (repo-based) | |
final int originalSize = (int) eol("return Model.allContents.size;", | |
ctx(IQueryEngine.PROPERTY_REPOSITORYCONTEXT, originalRepoURI)); | |
final int fragmentedSize = (int) eol("return Model.allContents.size;", | |
ctx(IQueryEngine.PROPERTY_REPOSITORYCONTEXT, fragmentedRepoURI)); | |
assertEquals(originalSize, fragmentedSize); | |
assertEquals(originalSize * 2, eol("return Model.allContents.size;")); | |
// Now doing it with the subtree context (will be replaced by a breadth-first traversal) | |
final int subtreeOriginalSize = (int) eol("return Model.allContents.size;", ctx( | |
IQueryEngine.PROPERTY_REPOSITORYCONTEXT, originalRepoURI, | |
IQueryEngine.PROPERTY_SUBTREECONTEXT, "/set0.xmi" | |
)); | |
assertEquals(originalSize, subtreeOriginalSize); | |
final int subtreeFragmentedSize = (int) eol("return Model.allContents.size;", ctx( | |
IQueryEngine.PROPERTY_REPOSITORYCONTEXT, fragmentedRepoURI, | |
IQueryEngine.PROPERTY_SUBTREECONTEXT, "/set0.xmi" | |
)); | |
assertEquals(subtreeFragmentedSize, subtreeOriginalSize); | |
// Now limit to the contents of the first IJavaProject | |
final int originalJavaContents = (int) eol("return 1 + IJavaProject.all.first.closure(e|e.eContents).size;", | |
ctx(IQueryEngine.PROPERTY_REPOSITORYCONTEXT, originalRepoURI)); | |
final int fileBasedJavaContents = (int) eol("return Model.allContents.size;", ctx( | |
IQueryEngine.PROPERTY_REPOSITORYCONTEXT, fragmentedRepoURI, | |
IQueryEngine.PROPERTY_FILECONTEXT, "/IJavaProject_org.eclipse.jdt.apt.pluggable.core/*")); | |
final int subtreeJavaContents = (int) eol("return Model.allContents.size;", ctx( | |
IQueryEngine.PROPERTY_REPOSITORYCONTEXT, fragmentedRepoURI, | |
IQueryEngine.PROPERTY_SUBTREECONTEXT, "/IJavaProject_org.eclipse.jdt.apt.pluggable.core/IJavaProject_org.eclipse.jdt.apt.pluggable.core.xmi" | |
)); | |
assertEquals(originalJavaContents, fileBasedJavaContents); | |
assertEquals(originalJavaContents, subtreeJavaContents); | |
} | |
@Test | |
public void getAllOf() throws Throwable { | |
prepareBoth(); | |
final String eolQuery = "return IType.all.size;"; | |
/* | |
* First do the most straightforward approach: find all the files under a folder, | |
* go to the type, then go through all instances filtering by file. | |
*/ | |
final int fileClasses = (int) eol(eolQuery, ctx( | |
IQueryEngine.PROPERTY_REPOSITORYCONTEXT, fragmentedRepoURI, | |
IQueryEngine.PROPERTY_FILECONTEXT, "/IJavaProject_org.eclipse.jdt.apt.pluggable.core/*")); | |
/* This version goes to the file nodes first, then checks types. */ | |
final int fileFirstClasses = (int) eol(eolQuery, ctx( | |
IQueryEngine.PROPERTY_REPOSITORYCONTEXT, fragmentedRepoURI, | |
IQueryEngine.PROPERTY_FILECONTEXT, "/IJavaProject_org.eclipse.jdt.apt.pluggable.core/*", | |
IQueryEngine.PROPERTY_FILEFIRST, "true")); | |
assertEquals(fileClasses, fileFirstClasses); | |
/* This version finds out the files in a containment subtree. */ | |
final int subtreeClasses = (int) eol(eolQuery, ctx( | |
IQueryEngine.PROPERTY_REPOSITORYCONTEXT, fragmentedRepoURI, | |
IQueryEngine.PROPERTY_SUBTREECONTEXT, "/IJavaProject_org.eclipse.jdt.apt.pluggable.core/IJavaProject_org.eclipse.jdt.apt.pluggable.core.xmi" | |
)); | |
assertEquals(fileClasses, subtreeClasses); | |
/* This version finds out the files in a containment subtree, and uses file-first traversal. */ | |
final int subtreeFileFirstClasses = (int) eol(eolQuery, ctx( | |
IQueryEngine.PROPERTY_REPOSITORYCONTEXT, fragmentedRepoURI, | |
IQueryEngine.PROPERTY_SUBTREECONTEXT, "/IJavaProject_org.eclipse.jdt.apt.pluggable.core/IJavaProject_org.eclipse.jdt.apt.pluggable.core.xmi", | |
IQueryEngine.PROPERTY_FILEFIRST, "true" | |
)); | |
assertEquals(fileClasses, subtreeFileFirstClasses); | |
/* This version finds out the files in a containment subtree, and registers derived edges to quickly find instances of a type within a containment subtree. */ | |
final int subtreeClassesDerived = (int) eol(eolQuery, ctx( | |
IQueryEngine.PROPERTY_REPOSITORYCONTEXT, fragmentedRepoURI, | |
IQueryEngine.PROPERTY_SUBTREECONTEXT, "/IJavaProject_org.eclipse.jdt.apt.pluggable.core/IJavaProject_org.eclipse.jdt.apt.pluggable.core.xmi", | |
IQueryEngine.PROPERTY_SUBTREE_DERIVEDALLOF, "true" | |
)); | |
assertEquals(fileClasses, subtreeClassesDerived); | |
} | |
@Test | |
public void getAllOfCountsSubtypes() throws Throwable { | |
prepareFragmented(); | |
/* This version finds out the files in a containment subtree, and registers derived edges to quickly find instances of a type within a containment subtree. */ | |
final int binpkgCount = (int) eol("return BinaryPackageFragmentRoot.all.size;", ctx( | |
IQueryEngine.PROPERTY_REPOSITORYCONTEXT, fragmentedRepoURI, | |
IQueryEngine.PROPERTY_SUBTREECONTEXT, "/set0.xmi", | |
IQueryEngine.PROPERTY_SUBTREE_DERIVEDALLOF, "true" | |
)); | |
final int ipkgCount = (int) eol("return IPackageFragmentRoot.all.size;", ctx( | |
IQueryEngine.PROPERTY_REPOSITORYCONTEXT, fragmentedRepoURI, | |
IQueryEngine.PROPERTY_SUBTREECONTEXT, "/set0.xmi", | |
IQueryEngine.PROPERTY_SUBTREE_DERIVEDALLOF, "true" | |
)); | |
assertTrue(String.format( | |
"IPackageFragmentRoot.all.size should be > than BinaryPackageFragmentRoot.all.size: checking %d > %d", ipkgCount, binpkgCount), | |
ipkgCount > binpkgCount); | |
List<String> pathsTouched = new ArrayList<>(); | |
final Random rnd = new Random(42); | |
for (File f : FileUtils.listFiles(folderFragmented, TrueFileFilter.INSTANCE, TrueFileFilter.INSTANCE)) { | |
if (rnd.nextDouble() < 0.2) { | |
FileUtils.touch(f); | |
pathsTouched.add(f.getName()); | |
} | |
} | |
indexer.requestImmediateSync(); | |
scheduleAndWait(() -> { | |
final int ipkgCount2 = (int) eol("return IPackageFragmentRoot.all.size;", ctx( | |
IQueryEngine.PROPERTY_REPOSITORYCONTEXT, fragmentedRepoURI, | |
IQueryEngine.PROPERTY_SUBTREECONTEXT, "/set0.xmi", | |
IQueryEngine.PROPERTY_SUBTREE_DERIVEDALLOF, "true" | |
)); | |
assertEquals( | |
"IPackageFragmentRoot.all.size should stay the same after touching " + pathsTouched, | |
ipkgCount, ipkgCount2); | |
return null; | |
}); | |
} | |
@Test | |
public void subtreeTraversalScoping() throws Throwable { | |
prepareBoth(); | |
// None of the external package fragment roots should be visible with traversal scoping on | |
final String eolQuery = "return IJavaProject.all.first.externalPackageFragmentRoots.size;"; | |
final int fileClasses = (int) eol(eolQuery, ctx( | |
IQueryEngine.PROPERTY_REPOSITORYCONTEXT, fragmentedRepoURI, | |
IQueryEngine.PROPERTY_FILECONTEXT, "/IJavaProject_org.eclipse.jdt.apt.pluggable.core/*", | |
IQueryEngine.PROPERTY_ENABLE_TRAVERSAL_SCOPING, "true")); | |
assertEquals(0, fileClasses); | |
// Same should happen with the subtree context | |
final int subtreeClasses = (int) eol(eolQuery, ctx( | |
IQueryEngine.PROPERTY_REPOSITORYCONTEXT, fragmentedRepoURI, | |
IQueryEngine.PROPERTY_SUBTREECONTEXT, "/IJavaProject_org.eclipse.jdt.apt.pluggable.core/IJavaProject_org.eclipse.jdt.apt.pluggable.core.xmi", | |
IQueryEngine.PROPERTY_ENABLE_TRAVERSAL_SCOPING, "true")); | |
assertEquals(0, subtreeClasses); | |
} | |
@Test | |
public void getFiles() throws Throwable { | |
prepareBoth(); | |
final int fragmentedFilesCount = (int) eol("return Model.files.size;", ctx( | |
IQueryEngine.PROPERTY_REPOSITORYCONTEXT, fragmentedRepoURI, | |
IQueryEngine.PROPERTY_FILECONTEXT, "/IJavaProject_org.eclipse.jdt.apt.pluggable.core/*")); | |
final int subtreeFilesCount = (int) eol("return Model.files.size;", ctx( | |
IQueryEngine.PROPERTY_REPOSITORYCONTEXT, fragmentedRepoURI, | |
IQueryEngine.PROPERTY_SUBTREECONTEXT, "/IJavaProject_org.eclipse.jdt.apt.pluggable.core/IJavaProject_org.eclipse.jdt.apt.pluggable.core.xmi" | |
)); | |
assertEquals(fragmentedFilesCount, subtreeFilesCount); | |
} | |
private static Map<String, Object> ctx(String... opts) { | |
Map<String, Object> ctx = new HashMap<>(); | |
for (int i = 0; i + 1 < opts.length; i += 2) { | |
final String key = opts[i], value = opts[i + 1]; | |
ctx.put(key, value); | |
} | |
return ctx; | |
} | |
/** | |
* Creates the fragmented version of set0, to help with testing. | |
*/ | |
public static void main(String[] args) throws IOException { | |
new Fragmenter().run(); | |
} | |
} |