Regression tests for symbol dependency handling

This commit is contained in:
Kris De Volder
2019-06-14 15:45:13 -07:00
parent c8c0637570
commit dd98511c1c
9 changed files with 361 additions and 6 deletions

View File

@@ -284,6 +284,10 @@ public class SpringSymbolIndex implements InitializingBean {
}
}
public SpringIndexerJava getJavaIndexer() {
return springIndexerJava;
}
public CompletableFuture<Void> deleteProject(IJavaProject project) {
try {
if (project.getElementName() == null) {

View File

@@ -0,0 +1,15 @@
/*******************************************************************************
* Copyright (c) 2019 Pivotal, Inc.
* 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
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.utils;
public interface FileScanListener {
void fileScanned(String path);
}

View File

@@ -76,10 +76,15 @@ public class SpringIndexerJava implements SpringIndexer {
private final SymbolCache cache;
private final JavaProjectFinder projectFinder;
private boolean scanTestJavaSources = false;
private FileScanListener fileScanListener = null; //used by test code only
private final DependencyTracker dependencyTracker = new DependencyTracker();
static class DependencyTracker {
public DependencyTracker getDependencyTracker() {
return dependencyTracker;
}
public static class DependencyTracker {
private Multimap<String, String> dependencies = MultimapBuilder.hashKeys().hashSetValues().build();
@@ -190,7 +195,6 @@ public class SpringIndexerJava implements SpringIndexer {
List<CachedSymbol> generatedSymbols = new ArrayList<CachedSymbol>();
AtomicReference<TextDocument> docRef = new AtomicReference<>();
String file = UriUtil.toFileString(docURI);
Set<String> changedTypes = new HashSet<>();
SpringIndexerJavaContext context = new SpringIndexerJavaContext(project, cu, docURI, file,
lastModified, docRef, content, generatedSymbols, SCAN_PASS.ONE, new ArrayList<>(), changedTypes);
@@ -206,10 +210,17 @@ public class SpringIndexerJava implements SpringIndexer {
}
Set<String> scannedFiles = new HashSet<>();
scannedFiles.add(file);
fileScannedEvent(file);
scanAffectedFiles(project, changedTypes, scannedFiles);
}
}
private void fileScannedEvent(String file) {
if (fileScanListener!=null) {
fileScanListener.fileScanned(file);
}
}
private void scanAffectedFiles(IJavaProject project, Set<String> changedTypes, Set<String> scannedFiles) {
log.info("Start scanning affected files for types {}", changedTypes);
//TODO: optimise? When multiple files are 'affected', we could try to parse and scan them in batch.
@@ -231,6 +242,7 @@ public class SpringIndexerJava implements SpringIndexer {
log.debug("Should also scan affected file: {}", file);
File f = new File(file);
scanAffectedFile(project, UriUtil.toUri(f).toString(), f.lastModified(), FileUtils.readFileToString(f), changedTypes);
fileScannedEvent(file);
}
}
} catch (Exception e) {
@@ -600,4 +612,8 @@ public class SpringIndexerJava implements SpringIndexer {
}
}
public void setFileScanListener(FileScanListener fileScanListener) {
this.fileScanListener = fileScanListener;
}
}

View File

@@ -0,0 +1,165 @@
package org.springframework.ide.vscode.boot.java.requestmapping.test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import org.apache.commons.io.FileUtils;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.SymbolInformation;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.ide.vscode.boot.app.SpringSymbolIndex;
import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest;
import org.springframework.ide.vscode.boot.bootiful.SymbolProviderTestConf;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.maven.java.MavenJavaProject;
import org.springframework.ide.vscode.commons.util.UriUtil;
import org.springframework.ide.vscode.commons.util.text.LanguageId;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness;
import org.springframework.ide.vscode.project.harness.ProjectsHarness;
import org.springframework.ide.vscode.project.harness.ProjectsHarness.CustomizableProjectContent;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@BootLanguageServerTest
@Import(SymbolProviderTestConf.class)
public class RequestMappingDependentConstantChangedTest {
@Autowired private BootLanguageServerHarness harness;
@Autowired private JavaProjectFinder projectFinder;
@Autowired private SpringSymbolIndex indexer;
ProjectsHarness projects = ProjectsHarness.INSTANCE;
private MavenJavaProject project;
private Path directory;
@Before
public void setup() throws Exception {
harness.intialize(null);
project = (MavenJavaProject) projects.mavenProject("test-request-mapping-symbols", false, new ProjectsHarness.ProjectCustomizer() {
@Override
public void customize(CustomizableProjectContent projectContent) throws Exception {
//dummy (forces every test to use a new copy of the project, we do this because project
//files are being mutated!
}
});
directory = new File(project.getLocationUri()).toPath();
// trigger project creation
projectFinder.find(new TextDocumentIdentifier(project.getLocationUri().toString())).get();
CompletableFuture<Void> initProject = indexer.waitOperation();
initProject.get(5, TimeUnit.SECONDS);
}
@Test
public void testSimpleRequestMappingSymbolFromConstantInDifferentClass() throws Exception {
String docUri = directory.resolve("src/main/java/org/test/SimpleMappingClassWithConstantInDifferentClass.java").toUri().toString();
String constantsUri = directory.resolve("src/main/java/org/test/Constants.java").toUri().toString();
List<? extends SymbolInformation> symbols = indexer.getSymbols(docUri);
assertEquals(1, symbols.size());
assertSymbol(docUri, "@/path/from/constant", "@RequestMapping(Constants.REQUEST_MAPPING_PATH)");
TestFileScanListener fileScanListener = new TestFileScanListener();
indexer.getJavaIndexer().setFileScanListener(fileScanListener);
replaceInFile(constantsUri, "path/from/constant", "/changed-path");
fileScanListener.assertScannedUris(constantsUri, docUri);
fileScanListener.assertScannedUri(constantsUri, 1);
fileScanListener.assertScannedUri(docUri, 1);
symbols = indexer.getSymbols(docUri);
assertSymbolCount(1, symbols);
assertSymbol(docUri, "@/changed-path", "@RequestMapping(Constants.REQUEST_MAPPING_PATH)");
}
@Test public void testCyclicalDependency() throws Exception {
//cyclical dependency between two files (ping refers pong and vice versa)
String pingUri = directory.resolve("src/main/java/org/test/PingConstantRequestMapping.java").toUri().toString();
String pongUri = directory.resolve("src/main/java/org/test/PongConstantRequestMapping.java").toUri().toString();
{
List<? extends SymbolInformation> symbols = indexer.getSymbols(pingUri);
for (SymbolInformation s : symbols) {
System.out.println(s.getName());
}
assertSymbolCount(1, symbols);
assertSymbol(pingUri, "@/pong -- GET", "@GetMapping(PongConstantRequestMapping.PONG)");
}
{
List<? extends SymbolInformation> symbols = indexer.getSymbols(pongUri);
assertSymbolCount(1, symbols);
assertSymbol(pongUri, "@/ping -- GET", "@GetMapping(PingConstantRequestMapping.PING)");
}
replaceInFile(pingUri, "/ping", "/changed");
{
List<? extends SymbolInformation> symbols = indexer.getSymbols(pingUri);
assertSymbolCount(1, symbols);
assertSymbol(pingUri, "@/pong -- GET", "@GetMapping(PongConstantRequestMapping.PONG)");
}
{
List<? extends SymbolInformation> symbols = indexer.getSymbols(pongUri);
assertSymbolCount(1, symbols);
assertSymbol(pongUri, "@/changed -- GET", "@GetMapping(PingConstantRequestMapping.PING)");
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////
private void assertSymbolCount(int expectedCount, List<? extends SymbolInformation> symbols) {
if (symbols.size()!=expectedCount) {
StringBuilder found = new StringBuilder();
for (SymbolInformation s : symbols) {
found.append(s.getName());
found.append("\n");
}
fail("Expected "+expectedCount+" symbols but found "+symbols.size()+":\n"+found);
}
}
private void assertSymbol(String docUri, String name, String coveredText) throws Exception {
List<? extends SymbolInformation> symbols = indexer.getSymbols(docUri);
Optional<? extends SymbolInformation> maybeSymbol = symbols.stream().filter(s -> name.equals(s.getName())).findFirst();
assertTrue(maybeSymbol.isPresent());
TextDocument doc = new TextDocument(docUri, LanguageId.JAVA);
doc.setText(FileUtils.readFileToString(UriUtil.toFile(docUri)));
SymbolInformation symbol = maybeSymbol.get();
Location loc = symbol.getLocation();
assertEquals(docUri, loc.getUri());
int start = doc.toOffset(loc.getRange().getStart());
int end = doc.toOffset(loc.getRange().getEnd());
String actualCoveredText = doc.textBetween(start, end);
assertEquals(coveredText, actualCoveredText);
}
public void replaceInFile(String docUri, String find, String replace) throws Exception {
File target = UriUtil.toFile(docUri);
String oldContent = FileUtils.readFileToString(target, "UTF8");
assertTrue(oldContent.contains(find));
String newContent = oldContent.replace(find, replace);
FileUtils.write(target, newContent, "UTF8");
indexer.updateDocument(docUri, null, "triggered by test code").get();
}
}

View File

@@ -16,9 +16,12 @@ import static org.junit.Assert.assertTrue;
import java.io.File;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import org.apache.commons.io.FileUtils;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.SymbolInformation;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.junit.Before;
@@ -29,11 +32,17 @@ import org.springframework.context.annotation.Import;
import org.springframework.ide.vscode.boot.app.SpringSymbolIndex;
import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest;
import org.springframework.ide.vscode.boot.bootiful.SymbolProviderTestConf;
import org.springframework.ide.vscode.boot.java.utils.SpringIndexerJava.DependencyTracker;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.util.UriUtil;
import org.springframework.ide.vscode.commons.util.text.LanguageId;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness;
import org.springframework.ide.vscode.project.harness.ProjectsHarness;
import org.springframework.test.context.junit4.SpringRunner;
import com.google.common.collect.ImmutableSet;
/**
* @author Martin Lippert
*/
@@ -73,9 +82,45 @@ public class RequestMappingSymbolProviderTest {
@Test
public void testSimpleRequestMappingSymbolFromConstantInDifferentClass() throws Exception {
String docUri = directory.toPath().resolve("src/main/java/org/test/SimpleMappingClassWithConstantInDifferentClass.java").toUri().toString();
String constantsUri = directory.toPath().resolve("src/main/java/org/test/Constants.java").toUri().toString();
List<? extends SymbolInformation> symbols = indexer.getSymbols(docUri);
assertEquals(1, symbols.size());
assertTrue(containsSymbol(symbols, "@/path/from/constant", docUri, 6, 1, 6, 48));
//Verify whether dependency tracker logics works properly for this example.
DependencyTracker dt = indexer.getJavaIndexer().getDependencyTracker();
assertEquals(ImmutableSet.of("Lorg/test/Constants;"), dt.getAllDependencies().get(UriUtil.toFileString(docUri)));
TestFileScanListener fileScanListener = new TestFileScanListener();
indexer.getJavaIndexer().setFileScanListener(fileScanListener);
indexer.updateDocument(constantsUri, FileUtils.readFileToString(UriUtil.toFile(constantsUri)), "test triggered").get();
fileScanListener.assertScannedUris(constantsUri, docUri);
fileScanListener.assertScannedUri(constantsUri, 1);
fileScanListener.assertScannedUri(docUri, 1);
}
@Test
public void testCyclicalRequestMappingDependency() throws Exception {
//Cyclical dependency:
//file a => file b => file a
//This has the potential to cause infinite loop.
String pingUri = directory.toPath().resolve("src/main/java/org/test/PingConstantRequestMapping.java").toUri().toString();
String pongUri = directory.toPath().resolve("src/main/java/org/test/PongConstantRequestMapping.java").toUri().toString();
assertSymbol(pingUri, "@/pong -- GET", "@GetMapping(PongConstantRequestMapping.PONG)");
assertSymbol(pongUri, "@/ping -- GET", "@GetMapping(PingConstantRequestMapping.PING)");
TestFileScanListener fileScanListener = new TestFileScanListener();
indexer.getJavaIndexer().setFileScanListener(fileScanListener);
indexer.updateDocument(pingUri, null, "test triggered").get();
fileScanListener.assertScannedUris(pingUri, pongUri);
fileScanListener.reset();
fileScanListener.assertScannedUris(/*none*/);
indexer.updateDocument(pongUri, null, "test triggered").get();
fileScanListener.assertScannedUris(pingUri, pongUri);
}
@Test
@@ -190,4 +235,21 @@ public class RequestMappingSymbolProviderTest {
return false;
}
private void assertSymbol(String docUri, String name, String coveredText) throws Exception {
List<? extends SymbolInformation> symbols = indexer.getSymbols(docUri);
Optional<? extends SymbolInformation> maybeSymbol = symbols.stream().filter(s -> name.equals(s.getName())).findFirst();
assertTrue(maybeSymbol.isPresent());
TextDocument doc = new TextDocument(docUri, LanguageId.JAVA);
doc.setText(FileUtils.readFileToString(UriUtil.toFile(docUri)));
SymbolInformation symbol = maybeSymbol.get();
Location loc = symbol.getLocation();
assertEquals(docUri, loc.getUri());
int start = doc.toOffset(loc.getRange().getStart());
int end = doc.toOffset(loc.getRange().getEnd());
String actualCoveredText = doc.textBetween(start, end);
assertEquals(coveredText, actualCoveredText);
}
}

View File

@@ -0,0 +1,58 @@
package org.springframework.ide.vscode.boot.java.requestmapping.test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.TreeSet;
import org.springframework.ide.vscode.boot.java.utils.FileScanListener;
import org.springframework.ide.vscode.commons.util.UriUtil;
public class TestFileScanListener implements FileScanListener {
public final List<String> scannedFiles = new ArrayList<String>();
@Override
public void fileScanned(String path) {
scannedFiles.add(path);
}
public void assertScanned(String path) {
assertTrue(scannedFiles.contains(path));
}
public void assertScanned(String path, int scanCount) {
assertEquals(scanCount, scannedFiles.stream().filter(e -> e.equals(path)).count());
}
public void assertScannedUris(String... docUris) {
List<String> docPaths = new ArrayList<>();
for (String docUri : docUris) {
docPaths.add(UriUtil.toFileString(docUri));
}
Collections.sort(docPaths);
StringBuilder expected = new StringBuilder();
for (String string : docPaths) {
expected.append(string+"\n");
}
TreeSet<String> actualPaths = new TreeSet<>();
for (String string : scannedFiles) {
actualPaths.add(string);
}
StringBuilder actual = new StringBuilder();
for (String string : actualPaths) {
actual.append(string+"\n");
}
assertEquals(expected.toString(), actual.toString());
}
public void assertScannedUri(String docUri, int scanCount) {
assertScanned(UriUtil.toFileString(docUri), scanCount);
}
public void reset() {
scannedFiles.clear();
}
}

View File

@@ -10,6 +10,8 @@
*******************************************************************************/
package org.springframework.ide.vscode.project.harness;
import static org.junit.Assert.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
@@ -73,7 +75,7 @@ public class ProjectsHarness {
File target = new File(projectRoot, path);
IOUtil.pipe(new ByteArrayInputStream(content.getBytes("UTF8")), target);
}
public void createType(String fqName, String sourceCode) throws Exception {
String sourceFile = sourceFolder()+"/"+fqName.replace('.', '/')+".java";
createFile(sourceFile, sourceCode);
@@ -98,7 +100,7 @@ public class ProjectsHarness {
this.fileObserver = fileObserver;
}
public IJavaProject project(ProjectType type, String name, ProjectCustomizer customizer, boolean build) throws Exception {
public IJavaProject project(ProjectType type, String name, boolean build, ProjectCustomizer customizer) throws Exception {
Tuple3<ProjectType, String, ProjectCustomizer> key = Tuples.of(type, name, customizer);
return cache.get(key, () -> {
Path baseProjectPath = getProjectPath(name);
@@ -122,7 +124,7 @@ public class ProjectsHarness {
}
}
public IJavaProject project(ProjectType type, String name, boolean build) throws Exception {
private IJavaProject project(ProjectType type, String name, boolean build) throws Exception {
return cache.get(type + "/" + name, () -> {
Path testProjectPath = getProjectPath(name);
return createProject(type, testProjectPath, build);
@@ -138,8 +140,12 @@ public class ProjectsHarness {
return Paths.get(resource);
}
public MavenJavaProject mavenProject(String name, boolean build, ProjectCustomizer customizer) throws Exception {
return (MavenJavaProject) project(ProjectType.MAVEN, name, build, customizer);
}
public MavenJavaProject mavenProject(String name, ProjectCustomizer customizer) throws Exception {
return (MavenJavaProject) project(ProjectType.MAVEN, name, customizer, true);
return (MavenJavaProject) project(ProjectType.MAVEN, name, true, customizer);
}
public MavenJavaProject mavenProject(String name) throws Exception {

View File

@@ -0,0 +1,15 @@
package org.test;
import org.springframework.web.bind.annotation.GetMapping;
public class PingConstantRequestMapping {
public final static String PING = "/ping";
@GetMapping(PongConstantRequestMapping.PONG)
public String hello() {
return "oy?";
}
}

View File

@@ -0,0 +1,14 @@
package org.test;
import org.springframework.web.bind.annotation.GetMapping;
public class PongConstantRequestMapping {
public static final String PONG="/pong";
@GetMapping(PingConstantRequestMapping.PING)
public String hello() {
return "oy?";
}
}