Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion java-extension/com.microsoft.java.test.plugin/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
<plugin>
<extension point="org.eclipse.jdt.ls.core.delegateCommandHandler">
<delegateCommandHandler class="com.microsoft.java.test.plugin.handler.TestDelegateCommandHandler">
<command id="vscode.java.test.get.testpath" />
<command id="vscode.java.test.get.sourcepath" />
<command id="vscode.java.test.update.sourcepath" />
<command id="vscode.java.test.runtime.classpath" />
<command id="vscode.java.test.project.info" />
<command id="vscode.java.test.search.items" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
@SuppressWarnings("restriction")
public class TestDelegateCommandHandler implements IDelegateCommandHandler {

private static final String GET_TEST_SOURCE_PATH = "vscode.java.test.get.testpath";
private static final String GET_SOURCE_PATH = "vscode.java.test.get.sourcepath";
private static final String UPDATE_TEST_SOURCE_PATH = "vscode.java.test.update.sourcepath";
private static final String COMPUTE_RUNTIME_CLASSPATH = "vscode.java.test.runtime.classpath";
private static final String GET_PROJECT_INFO = "vscode.java.test.project.info";
private static final String SEARCH_TEST_ITEMS = "vscode.java.test.search.items";
Expand All @@ -36,8 +37,10 @@ public class TestDelegateCommandHandler implements IDelegateCommandHandler {
public Object executeCommand(String commandId, List<Object> arguments, IProgressMonitor monitor) throws Exception {

switch (commandId) {
case GET_TEST_SOURCE_PATH:
case GET_SOURCE_PATH:
return ProjectTestUtils.listTestSourcePaths(arguments, monitor);
case UPDATE_TEST_SOURCE_PATH:
return ProjectTestUtils.updateTestSourcePaths(arguments, monitor);
case COMPUTE_RUNTIME_CLASSPATH:
return RuntimeClassPathUtils.resolveRuntimeClassPath(arguments);
case GET_PROJECT_INFO:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*******************************************************************************
* Copyright (c) 2019 Microsoft Corporation and others.
* 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
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Microsoft Corporation - initial API and implementation
*******************************************************************************/

package com.microsoft.java.test.plugin.model;

public class Result {
public boolean status;
public String message;

public Result(boolean status, String message) {
this.status = status;
this.message = message;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,34 +11,48 @@

package com.microsoft.java.test.plugin.util;

import com.google.gson.Gson;
import com.microsoft.java.test.plugin.model.Result;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.jdt.core.IClasspathAttribute;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.internal.core.ClasspathEntry;
import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin;
import org.eclipse.jdt.ls.core.internal.ProjectUtils;
import org.eclipse.jdt.ls.core.internal.ResourceUtils;
import org.eclipse.jdt.ls.core.internal.preferences.PreferenceManager;

import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.eclipse.jdt.ls.core.internal.ProjectUtils.WORKSPACE_LINK;

@SuppressWarnings("restriction")
public final class ProjectTestUtils {

private static final String TEST_SCOPE = "test";
private static final String MAVEN_SCOPE_ATTRIBUTE = "maven.scope";
private static final String GRADLE_SCOPE_ATTRIBUTE = "gradle_scope";

/**
* Method to get the valid paths which contains test code
Expand All @@ -49,9 +63,9 @@ public final class ProjectTestUtils {
* @throws JavaModelException
*/
@SuppressWarnings("unchecked")
public static List<TestSourcePath> listTestSourcePaths(List<Object> arguments, IProgressMonitor monitor)
public static List<SourcePathInfo> listTestSourcePaths(List<Object> arguments, IProgressMonitor monitor)
throws JavaModelException {
final List<TestSourcePath> testSourcePathList = new ArrayList<>();
final List<SourcePathInfo> testSourcePathList = new ArrayList<>();
if (arguments == null || arguments.size() == 0) {
return testSourcePathList;
}
Expand Down Expand Up @@ -81,16 +95,70 @@ public static List<TestSourcePath> listTestSourcePaths(List<Object> arguments, I

projectRoot = workspaceLinkFolder;
}
for (final IPath path : getTestPath(javaProject)) {
final IPath relativePath = path.makeRelativeTo(javaProject.getPath());
for (final IPath path : ProjectUtils.listSourcePaths(javaProject)) {
final IPath relativePath = path.makeRelativeTo(projectRoot.getFullPath());
final IPath location = projectRoot.getRawLocation().append(relativePath);
testSourcePathList.add(new TestSourcePath(location.toOSString(), projectName, projectType));
final IPath displayPath = getWorkspacePath(location);
testSourcePathList.add(new SourcePathInfo(location.toOSString(), displayPath.toOSString(),
isTest(javaProject, path), projectName, projectType));
}
}
}
return testSourcePathList;
}

public static Result updateTestSourcePaths(List<Object> arguments, IProgressMonitor monitor) {
if (arguments == null || arguments.size() == 0) {
throw new IllegalArgumentException("The test path input is empty");
}

try {
final Gson gson = new Gson();
final SourcePathInfo[] pathInfos = gson.fromJson((String) arguments.get(0), SourcePathInfo[].class);
for (final SourcePathInfo pathInfo : pathInfos) {
final IPath sourceFolderPath = ResourceUtils.filePathFromURI(pathInfo.path);
IProject targetProject = findBelongedProject(sourceFolderPath);
IPath projectLocation = null;
IContainer projectRootResource = null;
if (targetProject == null) {
final IPath workspaceRoot = ProjectUtils.findBelongedWorkspaceRoot(sourceFolderPath);
if (workspaceRoot == null) {
return new Result(false,
"Cannot find belonged workspace for source path: " + sourceFolderPath.toOSString());
}

targetProject = ProjectUtils.createInvisibleProjectIfNotExist(workspaceRoot);
final IFolder workspaceLink = targetProject.getFolder(ProjectUtils.WORKSPACE_LINK);
projectLocation = workspaceRoot;
projectRootResource = workspaceLink;
} else {
projectLocation = targetProject.getLocation();
projectRootResource = targetProject;
}

final IPath relativeSourcePath = sourceFolderPath.makeRelativeTo(projectLocation);
final IPath sourcePath = relativeSourcePath.isEmpty() ? projectRootResource.getFullPath() :
projectRootResource.getFolder(relativeSourcePath).getFullPath();
final IJavaProject javaProject = JavaCore.create(targetProject);
final IClasspathEntry[] clonedEntries = javaProject.getRawClasspath().clone();
for (int i = 0; i < clonedEntries.length; i++) {
if (clonedEntries[i].getEntryKind() != IClasspathEntry.CPE_SOURCE) {
continue;
}
if (clonedEntries[i].getPath().equals(sourcePath)) {
clonedEntries[i] = updateTestAttributes(clonedEntries[i], pathInfo.isTest);
break;
}
}
javaProject.setRawClasspath(clonedEntries, monitor);
}

return new Result(true, "");
} catch (final OperationCanceledException | CoreException ex) {
return new Result(false, ex.getMessage());
}
}

public static Set<IJavaProject> parseProjects(String uriStr) {
final IPath parentPath = ResourceUtils.filePathFromURI(uriStr);
if (parentPath == null) {
Expand Down Expand Up @@ -144,6 +212,10 @@ public static boolean isTest(IClasspathEntry entry) {
}

for (final IClasspathAttribute attribute : entry.getExtraAttributes()) {
if (MAVEN_SCOPE_ATTRIBUTE.equals(attribute.getName()) ||
GRADLE_SCOPE_ATTRIBUTE.equals(attribute.getName())) {
return TEST_SCOPE.equals(attribute.getValue());
}
if (TEST_SCOPE.equals(attribute.getName())) {
return "true".equalsIgnoreCase(attribute.getValue());
}
Expand All @@ -168,13 +240,65 @@ public static boolean belongToProject(IPath testPath, IProject project) {
return false;
}

public static class TestSourcePath {
private static IProject findBelongedProject(IPath sourceFolder) {
final List<IProject> projects = Stream.of(ProjectUtils.getAllProjects())
.filter(ProjectUtils::isJavaProject)
.sorted(new Comparator<IProject>() {
@Override
public int compare(IProject p1, IProject p2) {
return p2.getLocation().toOSString().length() - p1.getLocation().toOSString().length();
}
})
.collect(Collectors.toList());

for (final IProject project : projects) {
if (project.getLocation().isPrefixOf(sourceFolder)) {
return project;
}
}

return null;
}

private static IClasspathEntry updateTestAttributes(IClasspathEntry entry, boolean isTest) {
final List<IClasspathAttribute> extraAttributes = new LinkedList<>();
for (final IClasspathAttribute attribute : entry.getExtraAttributes()) {
if (!TEST_SCOPE.equals(attribute.getName())) {
extraAttributes.add(attribute);
}
}
if (isTest) {
extraAttributes.add(JavaCore.newClasspathAttribute(TEST_SCOPE, String.valueOf(true)));
}
return JavaCore.newSourceEntry(entry.getPath(), entry.getInclusionPatterns(), entry.getExclusionPatterns(),
entry.getOutputLocation(), extraAttributes.toArray(new IClasspathAttribute[0]));
}

private static IPath getWorkspacePath(IPath path) {
final PreferenceManager manager = JavaLanguageServerPlugin.getPreferencesManager();
final Collection<IPath> rootPaths = manager.getPreferences().getRootPaths();
if (rootPaths != null) {
for (final IPath rootPath : rootPaths) {
if (rootPath.isPrefixOf(path)) {
return path.makeRelativeTo(rootPath.append(".."));
}
}
}

return path;
}

public static class SourcePathInfo {
public String path;
public String displayPath;
public boolean isTest;
public String projectName;
public String projectType;

TestSourcePath(String path, String projectName, String projectType) {
SourcePathInfo(String path, String displayPath, boolean isTest, String projectName, String projectType) {
this.path = path;
this.displayPath = displayPath;
this.isTest = isTest;
this.projectName = projectName;
this.projectType = projectType;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,9 @@ public void acceptSearchMatch(SearchMatch match) throws CoreException {

private static boolean isInTestScope(IJavaElement element) throws JavaModelException {
final IJavaProject project = element.getJavaProject();
if (ProjectUtils.isGeneralJavaProject(project.getProject())) {
return true;
}
for (final IPath sourcePath : ProjectUtils.listSourcePaths(project)) {
if (!ProjectTestUtils.isTest(project, sourcePath)) {
continue;
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,8 @@
"category": "Java"
},
{
"command": "java.test.listTestSourcePaths",
"title": "%contributes.commands.java.test.listTestSourcePaths.title%",
"command": "java.test.updateTestSourcePaths",
"title": "%contributes.commands.java.test.updateTestSourcePaths.title%",
"category": "Java"
}
],
Expand Down
2 changes: 1 addition & 1 deletion package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"contributes.commands.java.test.cancel.title": "Cancel Test Job",
"contributes.commands.java.test.explorer.refresh.title": "Refresh",
"contributes.commands.java.test.config.migrate.title": "Migrate Deprecated 'launch.test.json'",
"contributes.commands.java.test.listTestSourcePaths.title": "List all Java test source paths",
"contributes.commands.java.test.updateTestSourcePaths.title": "Update project test source paths",
"configuration.java.test.report.position.description": "Specify where to show the test report",
"configuration.java.test.log.level.description": "Specify the level of the test logs",
"configuration.java.test.message.hintForDeprecatedConfig.description": "Specify whether the extension will show hint dialog when deprecated configuration file is used",
Expand Down
2 changes: 1 addition & 1 deletion package.nls.zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"contributes.commands.java.test.cancel.title": "取消测试任务",
"contributes.commands.java.test.explorer.refresh.title": "刷新",
"contributes.commands.java.test.config.migrate.title": "迁移已弃用的 'launch.test.json' 文件",
"contributes.commands.java.test.listTestSourcePaths.title": "显示测试源文件所在路径",
"contributes.commands.java.test.updateTestSourcePaths.title": "更新项目测试文件路径",
"configuration.java.test.report.position.description": "设定测试报告的显示位置",
"configuration.java.test.log.level.description": "设定日志级别",
"configuration.java.test.message.hintForDeprecatedConfig.description": "设定插件是否会对使用弃用的配置文件进行提示",
Expand Down
66 changes: 60 additions & 6 deletions src/commands/testPathCommands.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

import { window, workspace, WorkspaceFolder } from 'vscode';
import { getTestSourcePaths } from '../utils/commandUtils';
import { QuickPickItem, Uri, window, workspace, WorkspaceFolder } from 'vscode';
import { testCodeLensProvider } from '../codeLensProvider';
import { testExplorer } from '../explorer/testExplorer';
import { testFileWatcher } from '../testFileWatcher';
import { getTestSourcePaths, updateTestClasspathEntries } from '../utils/commandUtils';

export async function listTestSourcePaths(): Promise<void> {
export async function updateTestSourcePaths(): Promise<void> {
if (!workspace.workspaceFolders) {
window.showInformationMessage('No workspace opened in VS Code.');
return;
Expand All @@ -13,17 +16,68 @@ export async function listTestSourcePaths(): Promise<void> {
if (sourcePaths.length === 0) {
window.showInformationMessage("No Java test source directories found in the workspace, please use the command 'Add Folder to Java Test Source Path' first.");
} else {
window.showQuickPick(sourcePaths.map((sourcePath: ITestSourcePath) => {
const selectedTestPaths: QuickPickItem[] | undefined = await window.showQuickPick(sourcePaths.map((sourcePath: ITestSourcePath) => {
return {
label: sourcePath.path,
label: sourcePath.displayPath,
detail: `$(file-directory) ${sourcePath.projectType} Project: ${sourcePath.projectName}`,
description: sourcePath.path,
picked: sourcePath.isTest,
};
}), { placeHolder: 'All Java source directories recognized by the workspace.'});
}), { placeHolder: 'All Java test source directories recognized by the workspace.', canPickMany: true, ignoreFocusOut: true });
if (!selectedTestPaths) {
return;
}
const changedSourcePath: ITestSourcePath[] = getChangedSourcePath(sourcePaths, selectedTestPaths.map((item: QuickPickItem) => item.description!));
if (changedSourcePath.length > 0) {
const result: IResult | undefined = await updateTestClasspathEntries(changedSourcePath);
if (result && result.status) {
testCodeLensProvider.refresh();
testExplorer.refresh();
testFileWatcher.registerListeners();
window.showInformationMessage('Successfully updated the test source path(s).');
} else {
window.showErrorMessage(`Failed to update the test source path(s). ${result ? result.message : ''}`);
}
}
}
}

function getChangedSourcePath(origin: ITestSourcePath[], selected: string[]): ITestSourcePath[] {
const res: ITestSourcePath[] = [];
for (const originSourcePath of origin) {
if (isInSelectedPath(originSourcePath, selected) && !originSourcePath.isTest) {
res.push(updatePath(originSourcePath, true));
} else if (!isInSelectedPath(originSourcePath, selected) && originSourcePath.isTest) {
res.push(updatePath(originSourcePath, false));
}
}
return res;
}

function isInSelectedPath(pathToSearch: ITestSourcePath, selected: string[]): boolean {
for (const sourcePath of selected) {
if (sourcePath === pathToSearch.path) {
return true;
}
}
return false;
}

function updatePath(sourcePath: ITestSourcePath, isTest: boolean): ITestSourcePath {
sourcePath.path = Uri.file(sourcePath.path).toString();
sourcePath.isTest = isTest;
return sourcePath;
}

export interface ITestSourcePath {
path: string;
displayPath: string;
isTest: boolean;
projectName: string;
projectType: string;
}

export interface IResult {
status: boolean;
message: string;
}
Loading