Skip to content

Commit

Permalink
LSP: Create test class code action added. (apache#4939)
Browse files Browse the repository at this point in the history
  • Loading branch information
dbalek authored Nov 9, 2022
1 parent be075e2 commit 47632f8
Showing 1 changed file with 311 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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<String> 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<CodeAction> 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<Object, List<String>> validCombinations = getValidCombinations(info);
if (validCombinations == null || validCombinations.isEmpty()) {
return Collections.emptyList();
}
List<CodeAction> result = new ArrayList<>();
for (Map.Entry<Object, List<String>> 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<String> getCommands() {
return commands;
}

@Override
public CompletableFuture<Object> processCommand(NbCodeLanguageClient client, String command, List<Object> 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<? extends Lookup.Item<TestCreatorProvider>> providers = Lookup.getDefault().lookupResult(TestCreatorProvider.class).allItems();
for (final Lookup.Item<TestCreatorProvider> 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<Object, List<String>> getValidCombinations(CompilationInfo info) {
List<String> testingFrameworks = getTestingFrameworks(info.getFileObject());
if (testingFrameworks.isEmpty()) {
return null;
}
Map<Object, List<String>> validCombinations = new HashMap<>();
for (Object location : getLocations(info.getFileObject())) {
String targetFolderPath = getTargetFolderPath(location);
List<String> 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<String> getTestingFrameworks(FileObject fileObject) {
List<String> testingFrameworks = new ArrayList<>();
Collection<? extends Lookup.Item<TestCreatorProvider>> testCreatorProviders = Lookup.getDefault().lookupResult(TestCreatorProvider.class).allItems();
for (Lookup.Item<TestCreatorProvider> 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<? extends CommonTestUtilProvider> 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<? extends GuiUtilsProvider> 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;
}
}

0 comments on commit 47632f8

Please sign in to comment.