diff --git a/java-analyzer-bundle.core/src/main/java/io/konveyor/tackle/core/internal/symbol/CustomASTVisitor.java b/java-analyzer-bundle.core/src/main/java/io/konveyor/tackle/core/internal/symbol/CustomASTVisitor.java index 94f3a2b..169fdd6 100644 --- a/java-analyzer-bundle.core/src/main/java/io/konveyor/tackle/core/internal/symbol/CustomASTVisitor.java +++ b/java-analyzer-bundle.core/src/main/java/io/konveyor/tackle/core/internal/symbol/CustomASTVisitor.java @@ -73,15 +73,22 @@ public CustomASTVisitor(String query, SearchMatch match, QueryLocation location) /* * When visiting AST nodes, it may happen that we visit more nodes than * needed. We need to ensure that we are only visiting ones that are found - * in the given search match. I wrote this for methods / constructors where - * I observed that node starts at the beginning of line whereas match starts - * at an offset within that line. However, both end on the same position. This - * could differ for other locations. In that case, change logic based on type of - * the node you get. + * in the given search match. + * + * The match offset/length points to the specific code location (e.g., the method + * call), while the AST node may have slightly different boundaries. We check if + * the match falls within the node's range to handle cases like qualified method + * references where the SearchMatch element is the containing method but the + * offset points to the actual call site. */ private boolean shouldVisit(ASTNode node) { - return (this.match.getOffset() + this.match.getLength()) == - (node.getStartPosition() + node.getLength()); + int matchStart = this.match.getOffset(); + int matchEnd = matchStart + this.match.getLength(); + int nodeStart = node.getStartPosition(); + int nodeEnd = nodeStart + node.getLength(); + boolean result = (matchStart >= nodeStart && matchEnd <= nodeEnd) || + (matchEnd == nodeEnd); + return result; } @Override diff --git a/java-analyzer-bundle.core/src/main/java/io/konveyor/tackle/core/internal/symbol/SymbolProvider.java b/java-analyzer-bundle.core/src/main/java/io/konveyor/tackle/core/internal/symbol/SymbolProvider.java index 86a340c..55e57d3 100644 --- a/java-analyzer-bundle.core/src/main/java/io/konveyor/tackle/core/internal/symbol/SymbolProvider.java +++ b/java-analyzer-bundle.core/src/main/java/io/konveyor/tackle/core/internal/symbol/SymbolProvider.java @@ -208,13 +208,15 @@ default boolean queryQualificationMatches(String query, IJavaElement matchedElem // e.g. java.nio.file.Paths.get(String)/java.nio.file.Paths.get(*) -> java.nio.file.Paths.get // Remove any parentheses and their contents query = query.replaceAll("\\([^|]*\\)", ""); - query = query.replaceAll("(? 0) { // for a query, java.io.paths.File*, queryQualification is java.io.paths queryQualification = query.substring(0, dotIndex); } + + query = query.replaceAll("(? results = searchMethodCalls("io.konveyor.demo.PackageUsageExample.merge"); + results = inFile(results, "SampleApplication.java"); + printResults(results); + assertTrue("[1] Should find usage of merge() call in test-project/SampleApplication.java#callFullyQualifiedMethod()", + results.size() == 1); + + List entityManagerMergeResults = searchMethodCalls("javax.persistence.EntityManager.merge"); + entityManagerMergeResults = inFile(entityManagerMergeResults, "PackageUsageExample.java"); + printResults(entityManagerMergeResults); + assertTrue("[2] Should find usage of entityManager.merge() call in test-project/PackageUsageExample.java#merge()", + entityManagerMergeResults.size() == 1); + + // Half qualified method call should not work + List halfQualifiedResults = searchMethodCalls("PackageUsageExample.merge"); + printResults(halfQualifiedResults); + assertTrue("[3] Should not find any results matching PackageUsageExample.merge", + halfQualifiedResults.size() == 0); + + // make sure patterns for fully qualified method calls work + List mergeStarResults = searchMethodCalls("io.konveyor.demo.PackageUsageExample.merg*"); + printResults(mergeStarResults); + assertTrue("[4] Should find 1 result matching io.konveyor.demo.PackageUsageExample.merg*", + mergeStarResults.size() == 1); + } + + + @Test + public void testNonQualifiedMethodCalls() { + List allResults = searchMethodCalls("merge"); + + List sampleAppResults = inFile(allResults, "SampleApplication.java"); + printResults(sampleAppResults); + assertTrue("[1] Should find usage of merge() call in test-project/SampleApplication.java#callFullyQualifiedMethod()", + sampleAppResults.size() == 1); + + List packageExampleResults = inFile(allResults, "PackageUsageExample.java"); + printResults(packageExampleResults); + assertTrue("[2] Should find usage of merge() call in test-project/PackageUsageExample.java#merge()", + packageExampleResults.size() == 1); + + // make sure pattterns work + List mergeStarResults = searchMethodCalls("merg*"); + printResults(mergeStarResults); + assertTrue("[3] Should find usage of merge() call in test-project/SampleApplication.java#callFullyQualifiedMethod() and test-project/PackageUsageExample.java#merge()", + mergeStarResults.size() == 2); + } +} diff --git a/java-analyzer-bundle.test/src/main/java/io/konveyor/tackle/core/internal/symbol/MethodDeclarationSymbolProviderIntegrationTest.java b/java-analyzer-bundle.test/src/main/java/io/konveyor/tackle/core/internal/symbol/MethodDeclarationSymbolProviderIntegrationTest.java new file mode 100644 index 0000000..b8bcf64 --- /dev/null +++ b/java-analyzer-bundle.test/src/main/java/io/konveyor/tackle/core/internal/symbol/MethodDeclarationSymbolProviderIntegrationTest.java @@ -0,0 +1,111 @@ +package io.konveyor.tackle.core.internal.symbol; + +import static org.junit.Assert.assertTrue; + +import java.util.List; + +import org.eclipse.lsp4j.SymbolInformation; +import org.junit.Test; + +import io.konveyor.tackle.core.internal.testing.AbstractSymbolProviderTest; + + +public class MethodDeclarationSymbolProviderIntegrationTest extends AbstractSymbolProviderTest { + + @Test + public void testFullyQualifiedMethodDeclarations() { + // Test fully qualified method declaration search for SampleApplication.merge + List sampleAppResults = searchMethodDeclarations("io.konveyor.demo.SampleApplication.merge"); + printResults(sampleAppResults); + assertTrue("[1] Should find merge() declaration in SampleApplication.java", + sampleAppResults.size() == 1); + assertTrue("[1b] Result should be in SampleApplication.java", + sampleAppResults.get(0).getLocation().getUri().contains("SampleApplication.java")); + + // Test fully qualified method declaration search for PackageUsageExample.merge + List packageExampleResults = searchMethodDeclarations("io.konveyor.demo.PackageUsageExample.merge"); + printResults(packageExampleResults); + assertTrue("[2] Should find merge() declaration in PackageUsageExample.java", + packageExampleResults.size() == 1); + assertTrue("[2b] Result should be in PackageUsageExample.java", + packageExampleResults.get(0).getLocation().getUri().contains("PackageUsageExample.java")); + + // Test fully qualified method declaration search for ServletExample.merge + List servletResults = searchMethodDeclarations("io.konveyor.demo.ServletExample.merge"); + printResults(servletResults); + assertTrue("[3] Should find merge() declaration in ServletExample.java", + servletResults.size() == 1); + assertTrue("[3b] Result should be in ServletExample.java", + servletResults.get(0).getLocation().getUri().contains("ServletExample.java")); + + // Half qualified method declaration DOES work (different from method calls) + // This is because method declarations are matched by class name + method name + List halfQualifiedResults = searchMethodDeclarations("SampleApplication.merge"); + printResults(halfQualifiedResults); + assertTrue("[4] Should find 1 result matching SampleApplication.merge (half-qualified works for declarations)", + halfQualifiedResults.size() == 1); + assertTrue("[4b] Result should be in SampleApplication.java", + halfQualifiedResults.get(0).getLocation().getUri().contains("SampleApplication.java")); + + // Test pattern matching with wildcard on class name + List patternResults = searchMethodDeclarations("io.konveyor.demo.*.merge"); + printResults(patternResults); + assertTrue("[5] Should find all 3 merge() declarations matching io.konveyor.demo.*.merge", + patternResults.size() == 3); + } + + + @Test + public void testNonQualifiedMethodDeclarations() { + // Search for all "merge" method declarations + List allResults = searchMethodDeclarations("merge"); + printResults(allResults); + + // Should find all 3 merge() declarations in the project + assertTrue("[1] Should find all 3 merge() declarations in the project", + allResults.size() == 3); + + // Verify each file has exactly one merge() declaration + List sampleAppResults = inFile(allResults, "SampleApplication.java"); + assertTrue("[2] Should find exactly 1 merge() declaration in SampleApplication.java", + sampleAppResults.size() == 1); + + List packageExampleResults = inFile(allResults, "PackageUsageExample.java"); + assertTrue("[3] Should find exactly 1 merge() declaration in PackageUsageExample.java", + packageExampleResults.size() == 1); + + List servletResults = inFile(allResults, "ServletExample.java"); + assertTrue("[4] Should find exactly 1 merge() declaration in ServletExample.java", + servletResults.size() == 1); + + // Test pattern matching with wildcard + List mergeStarResults = searchMethodDeclarations("merg*"); + printResults(mergeStarResults); + assertTrue("[5] Should find all 3 merge() declarations matching merg*", + mergeStarResults.size() == 3); + } + + + @Test + public void testMethodDeclarationPatterns() { + // Test various pattern combinations + + // Pattern: method name ending with 'e' + List mergeResults = searchMethodDeclarations("*merge"); + printResults(mergeResults); + assertTrue("[1] Should find all 3 merge() declarations matching *merge", + mergeResults.size() == 3); + + // Test fully qualified pattern with class wildcard + List demoPackageResults = searchMethodDeclarations("io.konveyor.demo.Sample*.merge"); + printResults(demoPackageResults); + assertTrue("[2] Should find merge() in SampleApplication matching io.konveyor.demo.Sample*.merge", + demoPackageResults.size() == 1); + + // Test fully qualified pattern with method wildcard + List sampleAllMethods = searchMethodDeclarations("io.konveyor.demo.SampleApplication.*"); + printResults(sampleAllMethods); + assertTrue("[3] Should find multiple method declarations in SampleApplication matching io.konveyor.demo.SampleApplication.*", + sampleAllMethods.size() > 1); + } +} diff --git a/java-analyzer-bundle.test/src/main/java/io/konveyor/tackle/core/internal/testing/AbstractSymbolProviderTest.java b/java-analyzer-bundle.test/src/main/java/io/konveyor/tackle/core/internal/testing/AbstractSymbolProviderTest.java new file mode 100644 index 0000000..5e309e3 --- /dev/null +++ b/java-analyzer-bundle.test/src/main/java/io/konveyor/tackle/core/internal/testing/AbstractSymbolProviderTest.java @@ -0,0 +1,314 @@ +package io.konveyor.tackle.core.internal.testing; + +import java.util.List; + +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.lsp4j.SymbolInformation; +import org.junit.AfterClass; +import org.junit.Assume; +import org.junit.BeforeClass; + +import io.konveyor.tackle.core.internal.query.AnnotationQuery; + +/** + * Abstract base class for symbol provider integration tests. + * + *

Provides common setup and teardown for tests that need a real Java project + * with full JDT search capabilities. The test project is imported once per test + * class and shared across all test methods.

+ * + *

Subclasses get convenient methods for executing searches by location type.

+ * + *

Usage Example:

+ *
{@code
+ * public class MethodCallSymbolProviderIntegrationTest extends AbstractSymbolProviderTest {
+ *     
+ *     @Test
+ *     public void testFindsSimpleMethodCall() {
+ *         List results = searchMethodCalls("println");
+ *         assertFalse("Should find println calls", results.isEmpty());
+ *     }
+ * }
+ * }
+ * + *

Available Test Projects:

+ *
    + *
  • test-project - Main test project with various Java patterns
  • + *
  • customers-tomcat-legacy - Spring/JPA legacy application
  • + *
+ */ +public abstract class AbstractSymbolProviderTest { + + /** + * Default test project name. Subclasses can override by setting this in + * a @BeforeClass method before calling super.setUpTestProject(). + */ + protected static String testProjectName = "test-project"; + + /** The imported Java project instance. */ + protected static IJavaProject testProject; + + /** Whether to use source-only mode (default) or full mode. */ + protected static String analysisMode = "source-only"; + + /** + * Sets up the test project before any tests run. + * Imports the Maven project and waits for indexing to complete. + * + *

Note: These tests require an Eclipse/OSGi runtime. When run from + * VS Code or as regular JUnit tests, they will be skipped.

+ */ + @BeforeClass + public static void setUpTestProject() throws Exception { + // Check if we're running in an Eclipse/OSGi environment + // These tests require the full Eclipse runtime (ResourcesPlugin, M2E, JDT) + boolean isEclipseRuntime = isRunningInEclipse(); + Assume.assumeTrue( + "Skipping: These tests require Eclipse Plugin Test runtime. " + + "Run via 'mvn verify' or as 'JUnit Plug-in Test' in Eclipse IDE.", + isEclipseRuntime + ); + + System.out.println("\n" + "=".repeat(60)); + System.out.println("Setting up test project: " + testProjectName); + System.out.println("=".repeat(60) + "\n"); + + // Check if project is already imported (from a previous test class) + if (TestProjectManager.projectExists(testProjectName)) { + System.out.println("Project already exists, reusing: " + testProjectName); + testProject = TestProjectManager.getProject(testProjectName); + } else { + testProject = TestProjectManager.importMavenProject(testProjectName); + } + + TestProjectManager.waitForProjectReady(testProject); + + System.out.println("\nTest project ready: " + testProjectName + "\n"); + } + + /** + * Checks if we're running inside an Eclipse/OSGi runtime. + */ + private static boolean isRunningInEclipse() { + try { + // Try to access the workspace - this will fail outside Eclipse runtime + org.eclipse.core.resources.ResourcesPlugin.getWorkspace(); + return true; + } catch (IllegalStateException | NoClassDefFoundError | ExceptionInInitializerError e) { + System.out.println("Not running in Eclipse runtime: " + e.getMessage()); + return false; + } + } + + /** + * Cleans up the test project after all tests complete. + * Override this method with an empty body if you want to keep the project + * between test runs (useful for debugging). + */ + @AfterClass + public static void tearDownTestProject() throws Exception { + System.out.println("\n" + "=".repeat(60)); + System.out.println("Tearing down test project: " + testProjectName); + System.out.println("=".repeat(60) + "\n"); + + if (testProject != null) { + TestProjectManager.deleteProject(testProject); + testProject = null; + } + } + + /** + * Searches for method calls (location type 2). + * + * @param query Search pattern (e.g., "println", "java.io.PrintStream.println") + * @return List of matching symbols + */ + protected List searchMethodCalls(String query) { + return SearchTestHelper.executeSearch( + testProjectName, query, SearchTestHelper.LOCATION_METHOD_CALL, analysisMode); + } + + /** + * Searches for method declarations (location type 13). + * + * @param query Method name pattern (e.g., "processData", "get*") + * @return List of matching symbols + */ + protected List searchMethodDeclarations(String query) { + return SearchTestHelper.executeSearch( + testProjectName, query, SearchTestHelper.LOCATION_METHOD_DECLARATION, analysisMode); + } + + /** + * Searches for constructor calls (location type 3). + * + * @param query Type pattern (e.g., "java.util.ArrayList", "java.io.File") + * @return List of matching symbols + */ + protected List searchConstructorCalls(String query) { + return SearchTestHelper.executeSearch( + testProjectName, query, SearchTestHelper.LOCATION_CONSTRUCTOR_CALL, analysisMode); + } + + /** + * Searches for annotations (location type 4). + * + * @param query Annotation pattern (e.g., "javax.ejb.Stateless") + * @return List of matching symbols + */ + protected List searchAnnotations(String query) { + return SearchTestHelper.executeSearch( + testProjectName, query, SearchTestHelper.LOCATION_ANNOTATION, analysisMode); + } + + /** + * Searches for annotations with element matching. + * + * @param query Annotation pattern + * @param annotationQuery Annotation query with elements + * @return List of matching symbols + */ + protected List searchAnnotations(String query, AnnotationQuery annotationQuery) { + return SearchTestHelper.executeSearch( + testProjectName, query, SearchTestHelper.LOCATION_ANNOTATION, analysisMode, + null, annotationQuery); + } + + /** + * Searches for type references (location type 10). + * + * @param query Type pattern (e.g., "java.util.List", "java.io.*") + * @return List of matching symbols + */ + protected List searchTypes(String query) { + return SearchTestHelper.executeSearch( + testProjectName, query, SearchTestHelper.LOCATION_TYPE, analysisMode); + } + + /** + * Searches for imports (location type 8). + * + * @param query Import pattern (e.g., "java.io.File", "java.util.*") + * @return List of matching symbols + */ + protected List searchImports(String query) { + return SearchTestHelper.executeSearch( + testProjectName, query, SearchTestHelper.LOCATION_IMPORT, analysisMode); + } + + /** + * Searches for inheritance relationships (location type 1). + * + * @param query Base type pattern (e.g., "java.lang.Exception") + * @return List of matching symbols (classes extending the type) + */ + protected List searchInheritance(String query) { + return SearchTestHelper.executeSearch( + testProjectName, query, SearchTestHelper.LOCATION_INHERITANCE, analysisMode); + } + + /** + * Searches for implements relationships (location type 5). + * + * @param query Interface pattern (e.g., "java.io.Serializable") + * @return List of matching symbols (classes implementing the interface) + */ + protected List searchImplements(String query) { + return SearchTestHelper.executeSearch( + testProjectName, query, SearchTestHelper.LOCATION_IMPLEMENTS_TYPE, analysisMode); + } + + /** + * Searches for field declarations (location type 12). + * + * @param query Field type pattern (e.g., "java.lang.String") + * @return List of matching symbols + */ + protected List searchFields(String query) { + return SearchTestHelper.executeSearch( + testProjectName, query, SearchTestHelper.LOCATION_FIELD, analysisMode); + } + + /** + * Searches for variable declarations (location type 9). + * + * @param query Variable type pattern + * @return List of matching symbols + */ + protected List searchVariableDeclarations(String query) { + return SearchTestHelper.executeSearch( + testProjectName, query, SearchTestHelper.LOCATION_VARIABLE_DECLARATION, analysisMode); + } + + /** + * Searches for return types (location type 7). + * + * @param query Return type pattern + * @return List of matching symbols + */ + protected List searchReturnTypes(String query) { + return SearchTestHelper.executeSearch( + testProjectName, query, SearchTestHelper.LOCATION_RETURN_TYPE, analysisMode); + } + + /** + * Searches for class declarations (location type 14). + * + * @param query Class name pattern + * @return List of matching symbols + */ + protected List searchClassDeclarations(String query) { + return SearchTestHelper.executeSearch( + testProjectName, query, SearchTestHelper.LOCATION_CLASS_DECLARATION, analysisMode); + } + + /** + * Searches for package usage (location type 11). + * + * @param query Package pattern (e.g., "java.util", "javax.persistence") + * @return List of matching symbols + */ + protected List searchPackages(String query) { + return SearchTestHelper.executeSearch( + testProjectName, query, SearchTestHelper.LOCATION_PACKAGE, analysisMode); + } + + /** + * Searches for enum constants (location type 6). + * + * @param query Enum constant pattern + * @return List of matching symbols + */ + protected List searchEnumConstants(String query) { + return SearchTestHelper.executeSearch( + testProjectName, query, SearchTestHelper.LOCATION_ENUM_CONSTANT, analysisMode); + } + + /** + * Prints search results for debugging. + */ + protected void printResults(List symbols) { + SearchTestHelper.printResults(symbols); + } + + /** + * Returns a summary of search results. + */ + protected String summarize(List symbols) { + return SearchTestHelper.summarizeResults(symbols); + } + + /** + * Filters results to a specific file. + */ + protected List inFile(List symbols, String fileName) { + return SearchTestHelper.filterByFile(symbols, fileName); + } + + /** + * Filters results by symbol name. + */ + protected List withName(List symbols, String name) { + return SearchTestHelper.filterByName(symbols, name); + } +} diff --git a/java-analyzer-bundle.test/src/main/java/io/konveyor/tackle/core/internal/testing/SearchTestHelper.java b/java-analyzer-bundle.test/src/main/java/io/konveyor/tackle/core/internal/testing/SearchTestHelper.java new file mode 100644 index 0000000..54abf9a --- /dev/null +++ b/java-analyzer-bundle.test/src/main/java/io/konveyor/tackle/core/internal/testing/SearchTestHelper.java @@ -0,0 +1,318 @@ +package io.konveyor.tackle.core.internal.testing; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; +import org.eclipse.lsp4j.SymbolInformation; + +import io.konveyor.tackle.core.internal.SampleDelegateCommandHandler; +import io.konveyor.tackle.core.internal.query.AnnotationQuery; + +/** + * Helper class for executing searches and handling results in integration tests. + * Provides convenient wrappers around the RuleEntry command with filtering and + * debugging utilities. + * + *

Usage Example:

+ *
{@code
+ * // Simple search
+ * List results = SearchTestHelper.executeSearch(
+ *     "test-project", "println", LocationType.METHOD_CALL, "source-only");
+ * 
+ * // Filter and assert
+ * List filtered = SearchTestHelper.filterByFile(
+ *     results, "SampleApplication.java");
+ * assertFalse(filtered.isEmpty());
+ * }
+ */ +@SuppressWarnings("restriction") +public class SearchTestHelper { + public static final int LOCATION_DEFAULT = 0; + public static final int LOCATION_INHERITANCE = 1; + public static final int LOCATION_METHOD_CALL = 2; + public static final int LOCATION_CONSTRUCTOR_CALL = 3; + public static final int LOCATION_ANNOTATION = 4; + public static final int LOCATION_IMPLEMENTS_TYPE = 5; + public static final int LOCATION_ENUM_CONSTANT = 6; + public static final int LOCATION_RETURN_TYPE = 7; + public static final int LOCATION_IMPORT = 8; + public static final int LOCATION_VARIABLE_DECLARATION = 9; + public static final int LOCATION_TYPE = 10; + public static final int LOCATION_PACKAGE = 11; + public static final int LOCATION_FIELD = 12; + public static final int LOCATION_METHOD_DECLARATION = 13; + public static final int LOCATION_CLASS_DECLARATION = 14; + + private static final SampleDelegateCommandHandler commandHandler = new SampleDelegateCommandHandler(); + + + /** + * Executes a search using the RuleEntry command. + * + * @param projectName Name of the project to search in + * @param query Search query pattern + * @param location Location type (use LOCATION_* constants) + * @param analysisMode "source-only" or "full" + * @return List of matching symbols + */ + public static List executeSearch( + String projectName, + String query, + int location, + String analysisMode) { + return executeSearch(projectName, query, location, analysisMode, null, null); + } + + /** + * Executes a search with included paths filter. + * + * @param projectName Name of the project to search in + * @param query Search query pattern + * @param location Location type + * @param analysisMode "source-only" or "full" + * @param includedPaths List of paths to include (null for all) + * @return List of matching symbols + */ + public static List executeSearch( + String projectName, + String query, + int location, + String analysisMode, + List includedPaths) { + return executeSearch(projectName, query, location, analysisMode, includedPaths, null); + } + + /** + * Executes a search with all options including annotation query. + * + * @param projectName Name of the project to search in + * @param query Search query pattern + * @param location Location type + * @param analysisMode "source-only" or "full" + * @param includedPaths List of paths to include (null for all) + * @param annotationQuery Annotation query for filtering (null for none) + * @return List of matching symbols + */ + @SuppressWarnings("unchecked") + public static List executeSearch( + String projectName, + String query, + int location, + String analysisMode, + List includedPaths, + AnnotationQuery annotationQuery) { + + logInfo("Executing search: project=" + projectName + + ", query=" + query + + ", location=" + location + + ", mode=" + analysisMode); + + try { + // Build parameters map matching RuleEntryParams expectations + Map params = new HashMap<>(); + params.put("project", projectName); + params.put("query", query); + params.put("location", String.valueOf(location)); + params.put("analysisMode", analysisMode); + + if (includedPaths != null) { + params.put("includedPaths", new ArrayList<>(includedPaths)); + } + + if (annotationQuery != null) { + Map annotationQueryMap = new HashMap<>(); + annotationQueryMap.put("pattern", annotationQuery.getType()); + + List> elements = new ArrayList<>(); + for (Map.Entry entry : annotationQuery.getElements().entrySet()) { + Map element = new HashMap<>(); + element.put("name", entry.getKey()); + element.put("value", entry.getValue()); + elements.add(element); + } + annotationQueryMap.put("elements", elements); + + params.put("annotationQuery", annotationQueryMap); + } + + // Execute command + List arguments = new ArrayList<>(); + arguments.add(params); + + Object result = commandHandler.executeCommand( + SampleDelegateCommandHandler.RULE_ENTRY_COMMAND_ID, + arguments, + null // IProgressMonitor + ); + + List symbols = (List) result; + logInfo("Search returned " + symbols.size() + " results"); + + return symbols; + + } catch (Exception e) { + logInfo("Search failed: " + e.getMessage()); + throw new RuntimeException("Search execution failed", e); + } + } + + + /** + * Filters results to those in a specific file. + * + * @param symbols List of symbols to filter + * @param fileNameContains Substring that the file URI should contain + * @return Filtered list + */ + public static List filterByFile( + List symbols, + String fileNameContains) { + return symbols.stream() + .filter(s -> s.getLocation() != null && + s.getLocation().getUri() != null && + s.getLocation().getUri().contains(fileNameContains)) + .collect(Collectors.toList()); + } + + /** + * Filters results by symbol name. + * + * @param symbols List of symbols to filter + * @param name Symbol name to match + * @return Filtered list + */ + public static List filterByName( + List symbols, + String name) { + return symbols.stream() + .filter(s -> name.equals(s.getName())) + .collect(Collectors.toList()); + } + + /** + * Filters results by symbol name pattern. + * + * @param symbols List of symbols to filter + * @param namePattern Regex pattern to match against symbol name + * @return Filtered list + */ + public static List filterByNamePattern( + List symbols, + String namePattern) { + return symbols.stream() + .filter(s -> s.getName() != null && s.getName().matches(namePattern)) + .collect(Collectors.toList()); + } + + + + /** + * Pretty-prints search results for debugging. + * + * @param symbols List of symbols to print + */ + public static void printResults(List symbols) { + System.out.println("\n========== Search Results (" + symbols.size() + " total) =========="); + for (int i = 0; i < symbols.size(); i++) { + SymbolInformation s = symbols.get(i); + System.out.println(String.format("[%d] %s", i + 1, formatSymbol(s))); + } + System.out.println("=".repeat(50) + "\n"); + } + + /** + * Formats a single symbol for display. + * + * @param symbol The symbol to format + * @return Formatted string + */ + public static String formatSymbol(SymbolInformation symbol) { + StringBuilder sb = new StringBuilder(); + sb.append(symbol.getName()); + sb.append(" (").append(symbol.getKind()).append(")"); + + if (symbol.getContainerName() != null) { + sb.append(" in ").append(symbol.getContainerName()); + } + + if (symbol.getLocation() != null) { + String uri = symbol.getLocation().getUri(); + // Extract just the filename + int lastSlash = uri.lastIndexOf('/'); + String fileName = lastSlash >= 0 ? uri.substring(lastSlash + 1) : uri; + sb.append(" @ ").append(fileName); + + if (symbol.getLocation().getRange() != null) { + int line = symbol.getLocation().getRange().getStart().getLine() + 1; + sb.append(":").append(line); + } + } + + return sb.toString(); + } + + /** + * Returns a summary string of results. + * + * @param symbols List of symbols + * @return Summary string + */ + public static String summarizeResults(List symbols) { + if (symbols.isEmpty()) { + return "No results"; + } + + // Group by container + Map byContainer = symbols.stream() + .collect(Collectors.groupingBy( + s -> s.getContainerName() != null ? s.getContainerName() : "", + Collectors.counting())); + + StringBuilder sb = new StringBuilder(); + sb.append(symbols.size()).append(" total results: "); + sb.append(byContainer.entrySet().stream() + .map(e -> e.getValue() + " in " + e.getKey()) + .collect(Collectors.joining(", "))); + + return sb.toString(); + } + + + /** + * Checks if any result is in the specified file. + * + * @param symbols List of symbols + * @param fileNameContains Substring to look for in file URI + * @return true if at least one match is found + */ + public static boolean hasResultInFile( + List symbols, + String fileNameContains) { + return !filterByFile(symbols, fileNameContains).isEmpty(); + } + + /** + * Checks if any result has the specified name. + * + * @param symbols List of symbols + * @param name Symbol name to look for + * @return true if at least one match is found + */ + public static boolean hasResultWithName( + List symbols, + String name) { + return !filterByName(symbols, name).isEmpty(); + } + + private static void logInfo(String message) { + System.out.println("[SearchTestHelper] " + message); + try { + JavaLanguageServerPlugin.logInfo("[SearchTestHelper] " + message); + } catch (Exception e) { + } + } +} diff --git a/java-analyzer-bundle.test/src/main/java/io/konveyor/tackle/core/internal/testing/TestProjectManager.java b/java-analyzer-bundle.test/src/main/java/io/konveyor/tackle/core/internal/testing/TestProjectManager.java new file mode 100644 index 0000000..d695123 --- /dev/null +++ b/java-analyzer-bundle.test/src/main/java/io/konveyor/tackle/core/internal/testing/TestProjectManager.java @@ -0,0 +1,472 @@ +package io.konveyor.tackle.core.internal.testing; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.io.FileUtils; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IProjectDescription; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.FileLocator; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.Platform; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; +import org.eclipse.jdt.ls.core.internal.preferences.PreferenceManager; +import org.eclipse.jdt.ls.core.internal.preferences.Preferences; +import org.eclipse.m2e.core.MavenPlugin; +import org.eclipse.m2e.core.embedder.MavenModelManager; +import org.eclipse.m2e.core.project.IProjectConfigurationManager; +import org.eclipse.m2e.core.project.LocalProjectScanner; +import org.eclipse.m2e.core.project.MavenProjectInfo; +import org.eclipse.m2e.core.project.ProjectImportConfiguration; +import org.osgi.framework.Bundle; + +/** + * Manages test project lifecycle for integration tests. + * Handles project import (with Maven support), indexing synchronization, and cleanup. + * + *

This class is designed to be used with shared project instances across tests. + * Projects are imported once per test class and cleaned up after all tests complete.

+ * + *

Usage Example:

+ *
{@code
+ * @BeforeClass
+ * public static void setUp() throws Exception {
+ *     project = TestProjectManager.importMavenProject("test-project");
+ *     TestProjectManager.waitForProjectReady(project);
+ * }
+ * 
+ * @AfterClass
+ * public static void tearDown() throws Exception {
+ *     TestProjectManager.deleteProject(project);
+ * }
+ * }
+ */ +@SuppressWarnings("restriction") +public class TestProjectManager { + + private static final String TEST_BUNDLE_ID = "java-analyzer-bundle.test"; + private static final String PROJECTS_FOLDER = "projects"; + private static final String WORKING_DIR = "target/test-workspaces"; + + private static final IProgressMonitor NULL_MONITOR = new NullProgressMonitor(); + + // Track imported projects for cleanup + private static final List importedProjects = new ArrayList<>(); + + /** + * Imports a Maven project from the test resources. + * The project is copied to a working directory and imported via M2E. + * + * @param projectName Name of the project folder under projects/ (e.g., "test-project") + * @return The imported IJavaProject + * @throws Exception if import fails + */ + public static IJavaProject importMavenProject(String projectName) throws Exception { + logInfo("Importing Maven project: " + projectName); + + // Resolve source project path + File sourceProjectDir = resolveTestProjectPath(projectName); + if (!sourceProjectDir.exists()) { + throw new IllegalArgumentException( + "Test project not found: " + sourceProjectDir.getAbsolutePath()); + } + + // Copy to working directory + File workingDir = getWorkingDirectory(); + File targetProjectDir = new File(workingDir, projectName); + + if (targetProjectDir.exists()) { + logInfo("Cleaning existing working copy: " + targetProjectDir); + FileUtils.deleteDirectory(targetProjectDir); + } + + logInfo("Copying project to: " + targetProjectDir); + FileUtils.copyDirectory(sourceProjectDir, targetProjectDir); + + // Import via M2E + IJavaProject javaProject = importMavenProjectFromDirectory(targetProjectDir); + + importedProjects.add(projectName); + logInfo("Project imported successfully: " + projectName); + + return javaProject; + } + + /** + * Imports a Maven project from the given directory. + */ + private static IJavaProject importMavenProjectFromDirectory(File projectDir) throws Exception { + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + IWorkspaceRoot root = workspace.getRoot(); + + // Use M2E to scan and import the project + MavenModelManager modelManager = MavenPlugin.getMavenModelManager(); + IProjectConfigurationManager configManager = MavenPlugin.getProjectConfigurationManager(); + + // Scan for Maven projects + LocalProjectScanner scanner = new LocalProjectScanner( + Collections.singletonList(projectDir.getAbsolutePath()), + false, // not recursive into subdirectories + modelManager + ); + scanner.run(NULL_MONITOR); + + List projectInfos = new ArrayList<>(); + collectProjects(scanner.getProjects(), projectInfos); + + if (projectInfos.isEmpty()) { + throw new IllegalStateException( + "No Maven projects found in: " + projectDir.getAbsolutePath()); + } + + logInfo("Found " + projectInfos.size() + " Maven project(s) to import"); + + // Import the projects + ProjectImportConfiguration importConfig = new ProjectImportConfiguration(); + List results = + configManager.importProjects(projectInfos, importConfig, NULL_MONITOR); + + // Find the main project + IProject project = null; + for (org.eclipse.m2e.core.project.IMavenProjectImportResult result : results) { + if (result.getProject() != null) { + project = result.getProject(); + logInfo("Imported: " + project.getName()); + break; + } + } + + if (project == null) { + throw new IllegalStateException("Failed to import Maven project"); + } + + return JavaCore.create(project); + } + + /** + * Recursively collects all MavenProjectInfo from the scanner results. + */ + private static void collectProjects( + Collection projects, + List result) { + for (MavenProjectInfo info : projects) { + result.add(info); + collectProjects(info.getProjects(), result); + } + } + + /** + * Waits for all background jobs to complete, ensuring the project is fully ready. + * This includes Maven configuration, dependency resolution, JDT build, and search indexing. + * + * @param project The project to wait for + * @throws InterruptedException if waiting is interrupted + */ + public static void waitForProjectReady(IJavaProject project) throws InterruptedException { + logInfo("Waiting for project to be ready: " + project.getElementName()); + + // Configure workspace root paths for search to work + configureWorkspaceRootPaths(); + + // Wait for Maven jobs + waitForMavenJobs(); + + // Wait for build jobs + waitForBuildJobs(); + + // Wait for JDT indexing + waitForSearchIndex(); + + logInfo("Project ready: " + project.getElementName()); + } + + /** + * Configures the JavaLanguageServerPlugin preferences with workspace root paths. + * This is required for SampleDelegateCommandHandler.search() to work correctly. + */ + private static void configureWorkspaceRootPaths() { + try { + IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); + org.eclipse.core.runtime.IPath workspacePath = root.getLocation(); + + if (workspacePath == null) { + logInfo("Warning: Workspace location is null, using default"); + return; + } + + logInfo("Configuring workspace root path: " + workspacePath.toOSString()); + + PreferenceManager prefManager = JavaLanguageServerPlugin.getPreferencesManager(); + if (prefManager != null) { + Preferences prefs = prefManager.getPreferences(); + if (prefs != null) { + // Set the root paths to include the workspace + List rootPaths = new ArrayList<>(); + rootPaths.add(workspacePath); + prefs.setRootPaths(rootPaths); + logInfo("Workspace root paths configured successfully"); + } else { + logInfo("Warning: Preferences is null"); + } + } else { + logInfo("Warning: PreferenceManager is null"); + } + } catch (Exception e) { + logInfo("Warning: Failed to configure workspace root paths: " + e.getMessage()); + } + } + + /** + * Waits for Maven-related jobs to complete. + */ + private static void waitForMavenJobs() throws InterruptedException { + logInfo("Waiting for Maven jobs..."); + + // Wait for jobs belonging to M2E + Job.getJobManager().join("org.eclipse.m2e", NULL_MONITOR); + + // Additional wait for any straggler jobs + waitForJobsMatching(job -> + job.getClass().getName().contains("maven") || + job.getClass().getName().contains("Maven") || + job.getClass().getName().contains("m2e")); + + logInfo("Maven jobs completed"); + } + + /** + * Waits for workspace build jobs to complete. + */ + private static void waitForBuildJobs() throws InterruptedException { + logInfo("Waiting for build jobs..."); + + // Wait for auto-build + Job.getJobManager().join(ResourcesPlugin.FAMILY_AUTO_BUILD, NULL_MONITOR); + + // Wait for manual builds + Job.getJobManager().join(ResourcesPlugin.FAMILY_MANUAL_BUILD, NULL_MONITOR); + + logInfo("Build jobs completed"); + } + + /** + * Waits for JDT search index to be up-to-date. + */ + public static void waitForSearchIndex() throws InterruptedException { + logInfo("Waiting for JDT search index..."); + + // Wait for indexing jobs + waitForJobsMatching(job -> + job.getClass().getName().contains("Index") || + job.getClass().getName().contains("Reconcile")); + + // Give the indexer a moment to settle + Thread.sleep(500); + + logInfo("Search index ready"); + } + + /** + * Waits for jobs matching the given predicate. + */ + private static void waitForJobsMatching(java.util.function.Predicate matcher) + throws InterruptedException { + int maxWaitMs = 60000; // 60 seconds max + int pollingMs = 100; + long deadline = System.currentTimeMillis() + maxWaitMs; + + while (System.currentTimeMillis() < deadline) { + Job[] jobs = Job.getJobManager().find(null); + boolean foundMatch = false; + + for (Job job : jobs) { + if (matcher.test(job) && job.getState() != Job.NONE) { + foundMatch = true; + break; + } + } + + if (!foundMatch) { + return; // No matching jobs running + } + + Thread.sleep(pollingMs); + } + + logInfo("Warning: Timeout waiting for jobs"); + } + + /** + * Deletes a project from the workspace and cleans up its working directory. + * + * @param project The project to delete + * @throws CoreException if deletion fails + */ + public static void deleteProject(IJavaProject project) throws CoreException { + if (project == null) { + return; + } + + String projectName = project.getElementName(); + logInfo("Deleting project: " + projectName); + + IProject iProject = project.getProject(); + if (iProject.exists()) { + // Delete project and contents + iProject.delete(true, true, NULL_MONITOR); + } + + // Clean up working directory + try { + File workingDir = getWorkingDirectory(); + File projectDir = new File(workingDir, projectName); + if (projectDir.exists()) { + FileUtils.deleteDirectory(projectDir); + } + } catch (IOException e) { + logInfo("Warning: Failed to clean working directory: " + e.getMessage()); + } + + importedProjects.remove(projectName); + logInfo("Project deleted: " + projectName); + } + + /** + * Cleans up all test projects that were imported during this test run. + */ + public static void cleanupAllTestProjects() { + logInfo("Cleaning up all test projects"); + + IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); + + for (String projectName : new ArrayList<>(importedProjects)) { + try { + IProject project = root.getProject(projectName); + if (project.exists()) { + project.delete(true, true, NULL_MONITOR); + } + } catch (CoreException e) { + logInfo("Warning: Failed to delete project " + projectName + ": " + e.getMessage()); + } + } + + // Clean working directory + try { + File workingDir = getWorkingDirectory(); + if (workingDir.exists()) { + FileUtils.deleteDirectory(workingDir); + } + } catch (IOException e) { + logInfo("Warning: Failed to clean working directory: " + e.getMessage()); + } + + importedProjects.clear(); + logInfo("Cleanup completed"); + } + + /** + * Resolves the path to a test project in the projects/ folder. + * Handles both IDE and Maven test execution contexts. + * + * @param projectName Name of the project folder + * @return File pointing to the project directory + */ + public static File resolveTestProjectPath(String projectName) throws IOException { + // Try to resolve from the test bundle + Bundle bundle = Platform.getBundle(TEST_BUNDLE_ID); + if (bundle != null) { + URL projectsUrl = bundle.getEntry(PROJECTS_FOLDER); + if (projectsUrl != null) { + URL resolvedUrl = FileLocator.toFileURL(projectsUrl); + File projectsDir = new File(resolvedUrl.getPath()); + return new File(projectsDir, projectName); + } + } + + // Fallback: Try relative path (for IDE execution) + File projectsDir = new File(PROJECTS_FOLDER); + if (projectsDir.exists()) { + return new File(projectsDir, projectName); + } + + // Another fallback: Try from current working directory + String cwd = System.getProperty("user.dir"); + File cwdProjectsDir = new File(cwd, PROJECTS_FOLDER); + if (cwdProjectsDir.exists()) { + return new File(cwdProjectsDir, projectName); + } + + throw new IOException("Cannot resolve test projects directory. " + + "Tried bundle entry, relative path, and CWD: " + cwd); + } + + /** + * Gets the root directory containing all test projects. + */ + public static File getTestProjectsRoot() throws IOException { + Bundle bundle = Platform.getBundle(TEST_BUNDLE_ID); + if (bundle != null) { + URL projectsUrl = bundle.getEntry(PROJECTS_FOLDER); + if (projectsUrl != null) { + URL resolvedUrl = FileLocator.toFileURL(projectsUrl); + return new File(resolvedUrl.getPath()); + } + } + + // Fallback + return new File(PROJECTS_FOLDER); + } + + /** + * Gets the working directory for test project copies. + */ + private static File getWorkingDirectory() throws IOException { + File workingDir = new File(WORKING_DIR); + FileUtils.forceMkdir(workingDir); + return workingDir; + } + + /** + * Gets the project by name from the workspace. + */ + public static IJavaProject getProject(String projectName) { + IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); + IProject project = root.getProject(projectName); + if (project.exists()) { + return JavaCore.create(project); + } + return null; + } + + /** + * Checks if a project exists in the workspace. + */ + public static boolean projectExists(String projectName) { + IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); + return root.getProject(projectName).exists(); + } + + private static void logInfo(String message) { + System.out.println("[TestProjectManager] " + message); + try { + JavaLanguageServerPlugin.logInfo("[TestProjectManager] " + message); + } catch (Exception e) { + // Ignore if JavaLanguageServerPlugin is not available + } + } +} diff --git a/pom.xml b/pom.xml index 52bbb41..108b36d 100644 --- a/pom.xml +++ b/pom.xml @@ -70,8 +70,11 @@ false ${tycho.test.jvmArgs} true - - 60 + + 240 + + false + true