From 47632f8d5f35588db847e198ec977c15ee6c9fb2 Mon Sep 17 00:00:00 2001 From: Dusan Balek Date: Wed, 9 Nov 2022 17:52:15 +0100 Subject: [PATCH] LSP: Create test class code action added. (#4939) --- .../server/protocol/TestClassGenerator.java | 311 ++++++++++++++++++ 1 file changed, 311 insertions(+) create mode 100644 java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TestClassGenerator.java diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TestClassGenerator.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TestClassGenerator.java new file mode 100644 index 000000000000..7a352e078790 --- /dev/null +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TestClassGenerator.java @@ -0,0 +1,311 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.modules.java.lsp.server.protocol; + +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSyntaxException; +import com.sun.source.tree.ClassTree; +import com.sun.source.util.SourcePositions; +import com.sun.source.util.TreePath; +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import org.eclipse.lsp4j.CodeAction; +import org.eclipse.lsp4j.CodeActionKind; +import org.eclipse.lsp4j.CodeActionParams; +import org.eclipse.lsp4j.MessageParams; +import org.eclipse.lsp4j.MessageType; +import org.eclipse.lsp4j.ShowDocumentParams; +import org.netbeans.api.java.classpath.ClassPath; +import org.netbeans.api.java.project.JavaProjectConstants; +import org.netbeans.api.java.queries.UnitTestForSourceQuery; +import org.netbeans.api.java.source.ClasspathInfo; +import org.netbeans.api.java.source.CompilationController; +import org.netbeans.api.java.source.CompilationInfo; +import org.netbeans.api.java.source.JavaSource; +import org.netbeans.api.java.source.TreeUtilities; +import org.netbeans.api.project.FileOwnerQuery; +import org.netbeans.api.project.Project; +import org.netbeans.api.project.ProjectUtils; +import org.netbeans.api.project.SourceGroup; +import org.netbeans.modules.gsf.testrunner.api.TestCreatorProvider; +import org.netbeans.modules.gsf.testrunner.plugin.CommonTestUtilProvider; +import org.netbeans.modules.gsf.testrunner.plugin.GuiUtilsProvider; +import org.netbeans.modules.java.lsp.server.Utils; +import org.netbeans.modules.parsing.api.ResultIterator; +import org.openide.filesystems.FileChangeAdapter; +import org.openide.filesystems.FileChangeListener; +import org.openide.filesystems.FileEvent; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileUtil; +import org.openide.filesystems.URLMapper; +import org.openide.util.Lookup; +import org.openide.util.NbBundle; +import org.openide.util.RequestProcessor; +import org.openide.util.lookup.ServiceProvider; + +/** + * + * @author Dusan Balek + */ +@ServiceProvider(service = CodeActionsProvider.class, position = 100) +public final class TestClassGenerator extends CodeActionsProvider { + + private static final String GENERATE_TEST_CLASS_COMMAND = "java.generate.testClass"; + + private final Set commands = Collections.singleton(GENERATE_TEST_CLASS_COMMAND); + + @Override + @NbBundle.Messages({ + "# {0} - the testing framework to be used, e.g. JUnit, TestNG,...", + "# {1} - the location where the test class will be created", + "DN_GenerateTestClass=Create Test Class [{0} in {1}]" + }) + public List getCodeActions(ResultIterator resultIterator, CodeActionParams params) throws Exception { + CompilationController info = CompilationController.get(resultIterator.getParserResult()); + if (info == null) { + return Collections.emptyList(); + } + info.toPhase(JavaSource.Phase.RESOLVED); + int offset = getOffset(info, params.getRange().getStart()); + TreePath tp = info.getTreeUtilities().pathFor(offset); + if (!TreeUtilities.CLASS_TREE_KINDS.contains(tp.getLeaf().getKind())) { + return Collections.emptyList(); + } + ClassTree cls = (ClassTree) tp.getLeaf(); + SourcePositions sourcePositions = info.getTrees().getSourcePositions(); + int startPos = (int) sourcePositions.getStartPosition(tp.getCompilationUnit(), cls); + String code = info.getText(); + if (startPos < 0 || offset < 0 || offset < startPos || offset >= code.length()) { + return Collections.emptyList(); + } + String headerText = code.substring(startPos, offset); + int idx = headerText.indexOf('{'); + if (idx >= 0) { + return Collections.emptyList(); + } + ClassPath cp = info.getClasspathInfo().getClassPath(ClasspathInfo.PathKind.SOURCE); + FileObject fileObject = info.getFileObject(); + if (!fileObject.isValid()) { + return Collections.emptyList(); + } + FileObject root = cp.findOwnerRoot(fileObject); + if (root == null) { + return Collections.emptyList(); + } + Map> validCombinations = getValidCombinations(info); + if (validCombinations == null || validCombinations.isEmpty()) { + return Collections.emptyList(); + } + List result = new ArrayList<>(); + for (Map.Entry> entrySet : validCombinations.entrySet()) { + Object location = entrySet.getKey(); + for (String testingFramework : entrySet.getValue()) { + result.add((createCodeAction(Bundle.DN_GenerateTestClass(testingFramework, getLocationText(location)), CodeActionKind.Refactor, null, GENERATE_TEST_CLASS_COMMAND, Utils.toUri(fileObject), testingFramework, Utils.toUri(getTargetFolder(location))))); + } + } + return result; + } + + @Override + public Set getCommands() { + return commands; + } + + @Override + public CompletableFuture processCommand(NbCodeLanguageClient client, String command, List arguments) { + try { + if (arguments.size() > 2) { + String uri = ((JsonPrimitive) arguments.get(0)).getAsString(); + FileObject fileObject = Utils.fromUri(uri); + if (fileObject == null) { + throw new IllegalArgumentException(String.format("Cannot resolve source file from uri: %s", uri)); + } + String testingFramework = ((JsonPrimitive) arguments.get(1)).getAsString(); + String targetUri = ((JsonPrimitive) arguments.get(2)).getAsString(); + FileObject targetFolder = Utils.fromUri(targetUri); + if (targetFolder == null) { + throw new IllegalArgumentException(String.format("Cannot resolve target folder from uri: %s", targetUri)); + } + Collection> providers = Lookup.getDefault().lookupResult(TestCreatorProvider.class).allItems(); + for (final Lookup.Item provider : providers) { + if (provider.getDisplayName().equals(testingFramework)) { + final TestCreatorProvider.Context context = new TestCreatorProvider.Context(new FileObject[]{fileObject}); + context.setSingleClass(true); + context.setTargetFolder(targetFolder); + context.setTestClassName(getPreffiledName(fileObject, testingFramework)); + FileChangeListener fcl = new FileChangeAdapter() { + @Override + public void fileDataCreated(FileEvent fe) { + RequestProcessor.getDefault().post(() -> { + client.showDocument(new ShowDocumentParams(Utils.toUri(fe.getFile()))); + }, 1000); + } + }; + targetFolder.addRecursiveListener(fcl); + try { + provider.getInstance().createTests(context); + } finally { + RequestProcessor.getDefault().post(() -> { + targetFolder.removeRecursiveListener(fcl); + }, 1000); + } + } + } + } else { + throw new IllegalArgumentException(String.format("Illegal number of arguments received for command: %s", command)); + } + } catch (JsonSyntaxException | IllegalArgumentException | MalformedURLException ex) { + client.showMessage(new MessageParams(MessageType.Error, ex.getLocalizedMessage())); + } + return CompletableFuture.completedFuture(true); + } + + private static String getLocationText(Object location) { + String text = location instanceof SourceGroup + ? ((SourceGroup) location).getDisplayName() + : location instanceof FileObject + ? FileUtil.getFileDisplayName((FileObject) location) + : location.toString(); + return text; + } + + private static Map> getValidCombinations(CompilationInfo info) { + List testingFrameworks = getTestingFrameworks(info.getFileObject()); + if (testingFrameworks.isEmpty()) { + return null; + } + Map> validCombinations = new HashMap<>(); + for (Object location : getLocations(info.getFileObject())) { + String targetFolderPath = getTargetFolderPath(location); + List framework2Add = new ArrayList<>(); + for (String framework : testingFrameworks) { + String preffiledName = getPreffiledName(info.getFileObject(), framework); + preffiledName = preffiledName.replace('.', File.separatorChar).concat(".java"); + String path = targetFolderPath.concat(File.separator).concat(preffiledName); + File f = new File(path); + FileObject fo = FileUtil.toFileObject(f); + if (fo == null) { + framework2Add.add(framework); + } + } + if (!framework2Add.isEmpty()) { + validCombinations.put(location, framework2Add); + } + } + return validCombinations; + } + + private static List getTestingFrameworks(FileObject fileObject) { + List testingFrameworks = new ArrayList<>(); + Collection> testCreatorProviders = Lookup.getDefault().lookupResult(TestCreatorProvider.class).allItems(); + for (Lookup.Item provider : testCreatorProviders) { + if (provider.getInstance().enable(new FileObject[]{fileObject})) { + testingFrameworks.add(provider.getDisplayName()); + } + } + return testingFrameworks; + } + + private static Object[] getLocations(FileObject activeFO) { + Object[] locations = null; + Collection testUtilProviders = Lookup.getDefault().lookupAll(CommonTestUtilProvider.class); + for (CommonTestUtilProvider provider : testUtilProviders) { + locations = provider.getTestTargets(activeFO); + break; + } + if (locations != null && locations.length == 0) { + SourceGroup sourceGroupOwner = findSourceGroupOwner(activeFO); + if (sourceGroupOwner != null) { + locations = UnitTestForSourceQuery.findUnitTests(sourceGroupOwner.getRootFolder()); + } + } + return locations != null ? locations : new Object[0]; + } + + private static String getPreffiledName(FileObject fileObj, String selectedFramework) { + ClassPath cp = ClassPath.getClassPath(fileObj, ClassPath.SOURCE); + String className = cp.getResourceName(fileObj, '.', false); + return className + getTestingFrameworkSuffix(selectedFramework) + "Test"; + } + + private static String getTestingFrameworkSuffix(String selectedFramework) { + if (selectedFramework == null) { + return ""; + } + String testngFramework = ""; + Collection providers = Lookup.getDefault().lookupAll(GuiUtilsProvider.class); + for (GuiUtilsProvider provider : providers) { + testngFramework = provider.getTestngFramework(); + break; + } + return selectedFramework.equals(testngFramework) ? "NG" : ""; + } + + private static FileObject getTargetFolder(Object selectedLocation) { + if (selectedLocation == null) { + return null; + } + if (selectedLocation instanceof SourceGroup) { + return ((SourceGroup) selectedLocation).getRootFolder(); + } + if (selectedLocation instanceof URL) { + return URLMapper.findFileObject((URL) selectedLocation); + } + assert selectedLocation instanceof FileObject; + return (FileObject) selectedLocation; + } + + private static String getTargetFolderPath(Object selectedLocation) { + if (selectedLocation == null) { + return null; + } + if (selectedLocation instanceof SourceGroup) { + return ((SourceGroup) selectedLocation).getRootFolder().getPath(); + } + if (selectedLocation instanceof URL) { + return ((URL) selectedLocation).getPath(); + } + assert selectedLocation instanceof FileObject; + return ((FileObject) selectedLocation).getPath(); + } + + private static SourceGroup findSourceGroupOwner(FileObject file) { + final Project project = FileOwnerQuery.getOwner(file); + if (project != null) { + final SourceGroup[] sourceGroups = ProjectUtils.getSources(project).getSourceGroups(JavaProjectConstants.SOURCES_TYPE_JAVA); + for (int i = 0; i < sourceGroups.length; i++) { + SourceGroup srcGroup = sourceGroups[i]; + FileObject root = srcGroup.getRootFolder(); + if (((file==root)||(FileUtil.isParentOf(root,file))) && srcGroup.contains(file)) { + return srcGroup; + } + } + } + return null; + } +}