refactor: Decouple the codelens provider and test file watchers (#1178)

This commit is contained in:
Sheng Chen 2021-04-23 02:09:43 -07:00 коммит произвёл GitHub
Родитель e78bb34729
Коммит e0de9672cc
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
12 изменённых файлов: 179 добавлений и 111 удалений

Просмотреть файл

@ -10,3 +10,4 @@ export * from './src/protocols';
export * from './src/utils/commandUtils';
export * from './src/testFileWatcher';
export * from './src/runners/runnerScheduler';
export * from './src/provider/testSourceProvider';

Просмотреть файл

@ -11,7 +11,6 @@
package com.microsoft.java.test.plugin.util;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jdt.core.IClasspathAttribute;
@ -27,12 +26,11 @@ import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import static org.eclipse.jdt.ls.core.internal.ProjectUtils.WORKSPACE_LINK;
@SuppressWarnings("restriction")
public final class ProjectTestUtils {
@ -49,24 +47,46 @@ public final class ProjectTestUtils {
* @throws JavaModelException
*/
@SuppressWarnings("unchecked")
public static String[] listTestSourcePaths(List<Object> arguments, IProgressMonitor monitor)
public static List<TestSourcePath> listTestSourcePaths(List<Object> arguments, IProgressMonitor monitor)
throws JavaModelException {
final List<String> resultList = new ArrayList<>();
final List<TestSourcePath> resultList = new ArrayList<>();
if (arguments == null || arguments.size() == 0) {
return new String[0];
return Collections.emptyList();
}
final ArrayList<String> uriArray = ((ArrayList<String>) arguments.get(0));
for (final String uri : uriArray) {
final Set<IJavaProject> projectSet = parseProjects(uri);
for (final IJavaProject project : projectSet) {
for (final IPath path : getTestPath(project)) {
final IPath relativePath = path.makeRelativeTo(project.getPath());
resultList.add(project.getProject().getFolder(relativePath).getLocation().toOSString());
}
resultList.addAll(getTestSourcePaths(project));
}
}
return resultList.toArray(new String[resultList.size()]);
return resultList;
}
public static List<TestSourcePath> getTestSourcePaths(IJavaProject project) throws JavaModelException {
final List<TestSourcePath> paths = new LinkedList<>();
for (final IClasspathEntry entry : project.getRawClasspath()) {
// Ignore default project
if (ProjectsManager.DEFAULT_PROJECT_NAME.equals(project.getProject().getName())) {
continue;
}
if (entry.getEntryKind() != ClasspathEntry.CPE_SOURCE) {
continue;
}
if (isTestEntry(entry)) {
paths.add(new TestSourcePath(parseTestSourcePathString(entry, project), true));
continue;
}
// Always return true Eclipse & invisible project
if (ProjectUtils.isGeneralJavaProject(project.getProject())) {
paths.add(new TestSourcePath(parseTestSourcePathString(entry, project), false));
}
}
return paths;
}
public static Set<IJavaProject> parseProjects(String uriStr) {
@ -75,31 +95,29 @@ public final class ProjectTestUtils {
return Collections.emptySet();
}
return Arrays.stream(ProjectUtils.getJavaProjects())
.filter(p -> isProjectBelongToPath(p.getProject(), parentPath))
.filter(p -> ResourceUtils.isContainedIn(ProjectUtils.getProjectRealFolder(p.getProject()),
Arrays.asList(parentPath)))
.collect(Collectors.toSet());
}
public static List<IPath> getTestPath(IJavaProject project) throws JavaModelException {
final IClasspathEntry[] entries = project.getRawClasspath();
return Arrays.stream(entries)
.filter(entry -> isTest(project, entry))
.map(entry -> entry.getPath())
.collect(Collectors.toList());
private static String parseTestSourcePathString(IClasspathEntry entry, IJavaProject project) {
final IPath relativePath = entry.getPath().makeRelativeTo(project.getPath());
return project.getProject().getFolder(relativePath).getLocation().toOSString();
}
public static boolean isTest(IJavaProject project, IPath path) {
public static boolean isTest(IJavaProject project, IPath path, boolean containsGeneral) {
try {
final IClasspathEntry entry = project.getClasspathEntryFor(path);
if (entry == null) {
return false;
}
return isTest(project, entry);
return isTest(project, entry, containsGeneral);
} catch (final JavaModelException e) {
return false;
}
}
public static boolean isTest(IJavaProject project, IClasspathEntry entry) {
public static boolean isTest(IJavaProject project, IClasspathEntry entry, boolean containsGeneral) {
// Ignore default project
if (ProjectsManager.DEFAULT_PROJECT_NAME.equals(project.getProject().getName())) {
return false;
@ -109,12 +127,12 @@ public final class ProjectTestUtils {
return false;
}
// Always return true Eclipse & invisible project
if (ProjectUtils.isGeneralJavaProject(project.getProject())) {
if (isTestEntry(entry)) {
return true;
}
return isTestEntry(entry);
// Always return true Eclipse & invisible project
return containsGeneral && ProjectUtils.isGeneralJavaProject(project.getProject());
}
public static boolean isTestEntry(IClasspathEntry entry) {
@ -133,19 +151,17 @@ public final class ProjectTestUtils {
return false;
}
public static boolean isProjectBelongToPath(IProject project, IPath path) {
// Check for visible project
if (project.getLocation() != null && path.isPrefixOf(project.getLocation())) {
return true;
static class TestSourcePath {
public String testSourcePath;
/**
* All the source paths from eclipse and invisible project will be treated as test source
* even they are not marked as test in the classpath entry, in that case, this field will be false.
*/
public boolean isStrict;
public TestSourcePath(String testSourcePath, boolean isStrict) {
this.testSourcePath = testSourcePath;
this.isStrict = isStrict;
}
// Check for invisible project
final IPath linkedLocation = project.getFolder(WORKSPACE_LINK).getLocation();
if (linkedLocation != null && path.isPrefixOf(linkedLocation)) {
return true;
}
return false;
}
}

Просмотреть файл

@ -290,7 +290,7 @@ public class TestSearchUtils {
private static boolean isInTestScope(IJavaElement element) throws JavaModelException {
final IJavaProject project = element.getJavaProject();
for (final IPath sourcePath : ProjectUtils.listSourcePaths(project)) {
if (!ProjectTestUtils.isTest(project, sourcePath)) {
if (!ProjectTestUtils.isTest(project, sourcePath, true /*containsGeneralProject*/)) {
continue;
}
if (sourcePath.isPrefixOf(element.getPath())) {

Просмотреть файл

@ -2,7 +2,9 @@
// Licensed under the MIT license.
import { ConfigurationChangeEvent, Disposable, DocumentSelector, languages, RelativePattern, workspace } from 'vscode';
import { testSourceProvider } from '../../extension.bundle';
import { ENABLE_EDITOR_SHORTCUTS_KEY } from '../constants/configs';
import { parseDocumentSelector } from '../utils/uiUtils';
import { TestCodeLensProvider } from './TestCodeLensProvider';
class TestCodeLensController implements Disposable {
@ -22,18 +24,14 @@ class TestCodeLensController implements Disposable {
this.setCodeLensVisibility();
}
public registerCodeLensProvider(patterns: RelativePattern[]): void {
public async registerCodeLensProvider(): Promise<void> {
if (this.registeredProvider) {
this.registeredProvider.dispose();
}
const documentSelector: DocumentSelector = patterns.map((p: RelativePattern) => {
return {
language: 'java',
scheme: 'file',
pattern: p,
};
});
const patterns: RelativePattern[] = await testSourceProvider.getTestSourcePattern();
const documentSelector: DocumentSelector = parseDocumentSelector(patterns);
this.registeredProvider = languages.registerCodeLensProvider(documentSelector, this.internalProvider);
}

Просмотреть файл

@ -13,6 +13,7 @@ export namespace JavaTestRunnerDelegateCommands {
export const SEARCH_TEST_CODE_LENS: string = 'vscode.java.test.search.codelens';
export const SEARCH_TEST_LOCATION: string = 'vscode.java.test.search.location';
export const RESOLVE_JUNIT_ARGUMENT: string = 'vscode.java.test.junit.argument';
export const GENERATE_TESTS: string = 'vscode.java.test.generateTests';
}
export namespace JavaTestRunnerCommands {

Просмотреть файл

@ -7,6 +7,7 @@ import * as path from 'path';
import { CodeActionKind, commands, DebugConfiguration, Event, Extension, ExtensionContext, extensions, languages, Range, TreeView, TreeViewExpansionEvent, TreeViewSelectionChangeEvent, Uri, window, workspace } from 'vscode';
import { dispose as disposeTelemetryWrapper, initializeFromJsonFile, instrumentOperation, instrumentOperationAsVsCodeCommand } from 'vscode-extension-telemetry-wrapper';
import { sendInfo } from 'vscode-extension-telemetry-wrapper';
import { testSourceProvider } from '../extension.bundle';
import { TestCodeActionProvider } from './codeActionProvider';
import { testCodeLensController } from './codelens/TestCodeLensController';
import { debugTestsFromExplorer, openTextDocument, runTestsFromExplorer, runTestsFromJavaProjectExplorer } from './commands/explorerCommands';
@ -62,23 +63,35 @@ async function doActivate(_operationId: string, context: ExtensionContext): Prom
if (extensionApi.onDidClasspathUpdate) {
const onDidClasspathUpdate: Event<Uri> = extensionApi.onDidClasspathUpdate;
context.subscriptions.push(onDidClasspathUpdate(async () => {
await testSourceProvider.initialize();
await testFileWatcher.registerListeners(true /*enableDebounce*/);
await testCodeLensController.registerCodeLensProvider();
}));
}
if (extensionApi.onDidServerModeChange) {
const onDidServerModeChange: Event<string> = extensionApi.onDidServerModeChange;
context.subscriptions.push(onDidServerModeChange(async (mode: string) => {
if (serverMode === mode) {
return;
}
// Only re-initialize the component when its lightweight -> standard
if (serverMode !== LanguageServerMode.Hybrid) {
testExplorer.refresh();
await testSourceProvider.initialize();
await testFileWatcher.registerListeners();
await testCodeLensController.registerCodeLensProvider();
}
serverMode = mode;
testExplorer.refresh();
await testFileWatcher.registerListeners();
}));
}
if (extensionApi.onDidProjectsImport) {
const onDidProjectsImport: Event<Uri[]> = extensionApi.onDidProjectsImport;
context.subscriptions.push(onDidProjectsImport(async () => {
await testSourceProvider.initialize();
await testFileWatcher.registerListeners(true /*enableDebounce*/);
await testCodeLensController.registerCodeLensProvider();
}));
}
@ -90,7 +103,9 @@ async function doActivate(_operationId: string, context: ExtensionContext): Prom
progressProvider = javaDebugger.exports?.progressProvider;
}
await testSourceProvider.initialize();
await testFileWatcher.registerListeners();
await testCodeLensController.registerCodeLensProvider();
testExplorer.initialize(context);
const testTreeView: TreeView<ITestItem> = window.createTreeView(testExplorer.testExplorerViewId, { treeDataProvider: testExplorer, showCollapseAll: true });
runnerScheduler.initialize(context);

Просмотреть файл

@ -0,0 +1,53 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
import { RelativePattern, Uri, workspace, WorkspaceFolder } from 'vscode';
import { getTestSourcePaths } from '../utils/commandUtils';
class TestSourcePathProvider {
private testSource: ITestSourcePath[];
public async initialize(): Promise<void> {
this.testSource = [];
if (!workspace.workspaceFolders) {
return;
}
this.testSource = await getTestSourcePaths(workspace.workspaceFolders.map((workspaceFolder: WorkspaceFolder) => workspaceFolder.uri.toString()));
}
public async getTestSourcePattern(containsGeneral: boolean = true): Promise<RelativePattern[]> {
const patterns: RelativePattern[] = [];
const sourcePaths: string[] = await testSourceProvider.getTestSourcePath(containsGeneral);
for (const sourcePath of sourcePaths) {
const normalizedPath: string = Uri.file(sourcePath).fsPath;
const pattern: RelativePattern = new RelativePattern(normalizedPath, '**/*.java');
patterns.push(pattern);
}
return patterns;
}
public async getTestSourcePath(containsGeneral: boolean = true): Promise<string[]> {
if (this.testSource === undefined) {
await this.initialize();
}
if (containsGeneral) {
return this.testSource.map((s: ITestSourcePath) => s.testSourcePath);
}
return this.testSource.filter((s: ITestSourcePath) => s.isStrict)
.map((s: ITestSourcePath) => s.testSourcePath);
}
}
export interface ITestSourcePath {
testSourcePath: string;
/**
* All the source paths from eclipse and invisible project will be treated as test source
* even they are not marked as test in the classpath entry, in that case, this field will be false.
*/
isStrict: boolean;
}
export const testSourceProvider: TestSourcePathProvider = new TestSourcePathProvider();

Просмотреть файл

@ -2,19 +2,17 @@
// Licensed under the MIT license.
import * as _ from 'lodash';
import { Disposable, FileSystemWatcher, RelativePattern, Uri, workspace, WorkspaceFolder } from 'vscode';
import { testCodeLensController } from './codelens/TestCodeLensController';
import { Disposable, FileSystemWatcher, RelativePattern, Uri, workspace } from 'vscode';
import { testSourceProvider } from '../extension.bundle';
import { testExplorer } from './explorer/testExplorer';
import { isStandardServerReady } from './extension';
import { logger } from './logger/logger';
import { ITestItem, TestLevel } from './protocols';
import { testItemModel } from './testItemModel';
import { testResultManager } from './testResultManager';
import { getTestSourcePaths } from './utils/commandUtils';
class TestFileWatcher implements Disposable {
private patterns: RelativePattern[] = [];
private disposables: Disposable[] = [];
private registerListenersDebounce: _.DebouncedFunc<() => Promise<void>> = _.debounce(this.registerListenersInternal, 2 * 1000 /*ms*/);
@ -33,7 +31,6 @@ class TestFileWatcher implements Disposable {
}
}
this.disposables = [];
this.patterns = [];
}
protected async registerListenersInternal(): Promise<void> {
@ -41,24 +38,18 @@ class TestFileWatcher implements Disposable {
return;
}
this.dispose();
if (workspace.workspaceFolders) {
try {
const sourcePaths: string[] = await getTestSourcePaths(workspace.workspaceFolders.map((workspaceFolder: WorkspaceFolder) => workspaceFolder.uri.toString()));
for (const sourcePath of sourcePaths) {
const normalizedPath: string = Uri.file(sourcePath).fsPath;
const pattern: RelativePattern = new RelativePattern(normalizedPath, '**/*.java');
this.patterns.push(pattern);
const watcher: FileSystemWatcher = workspace.createFileSystemWatcher(pattern, true /* ignoreCreateEvents */);
this.registerWatcherListeners(watcher);
this.disposables.push(watcher);
}
} catch (error) {
logger.error('Failed to get the test paths', error);
const watcher: FileSystemWatcher = workspace.createFileSystemWatcher('**/*.java');
try {
const patterns: RelativePattern[] = await testSourceProvider.getTestSourcePattern();
for (const pattern of patterns) {
const watcher: FileSystemWatcher = workspace.createFileSystemWatcher(pattern, true /* ignoreCreateEvents */);
this.registerWatcherListeners(watcher);
this.disposables.push(watcher);
}
testCodeLensController.registerCodeLensProvider(this.patterns);
} catch (error) {
logger.error('Failed to get the test paths', error);
const watcher: FileSystemWatcher = workspace.createFileSystemWatcher('**/*.java');
this.registerWatcherListeners(watcher);
this.disposables.push(watcher);
}
}

Просмотреть файл

@ -2,13 +2,14 @@
// Licensed under the MIT license.
import { CancellationToken, commands, Position, Uri } from 'vscode';
import { ITestSourcePath } from '../../extension.bundle';
import { JavaLanguageServerCommands, JavaTestRunnerDelegateCommands } from '../constants/commands';
import { logger } from '../logger/logger';
import { ILocation, ISearchTestItemParams, ITestItem, TestKind, TestLevel } from '../protocols';
import { IJUnitLaunchArguments } from '../runners/baseRunner/BaseRunner';
export async function getTestSourcePaths(uri: string[]): Promise<string[]> {
return await executeJavaLanguageServerCommand<string[]>(
export async function getTestSourcePaths(uri: string[]): Promise<ITestSourcePath[]> {
return await executeJavaLanguageServerCommand<ITestSourcePath[]>(
JavaTestRunnerDelegateCommands.GET_TEST_SOURCE_PATH, uri) || [];
}
@ -41,16 +42,8 @@ export async function resolveStackTraceLocation(trace: string, projectNames: str
JavaLanguageServerCommands.RESOLVE_STACKTRACE_LOCATION, trace, projectNames) || '';
}
export async function getSourcePaths(): Promise<any> {
const result: any = await executeJavaLanguageServerCommand<any>('java.project.listSourcePaths');
if (result?.data) {
return result.data;
}
return [];
}
export async function generateTests(uri: Uri, startPosition: number): Promise<any> {
return await executeJavaLanguageServerCommand<any>('vscode.java.test.generateTests', uri.toString(), startPosition);
return await executeJavaLanguageServerCommand<any>(JavaTestRunnerDelegateCommands.GENERATE_TESTS, uri.toString(), startPosition);
}
export async function resolveJUnitLaunchArguments(uri: string, fullName: string, testName: string, project: string,

Просмотреть файл

@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
import { window } from 'vscode';
import { DocumentSelector, RelativePattern, window } from 'vscode';
import { showOutputChannel } from '../commands/logCommands';
import { OPEN_OUTPUT_CHANNEL } from '../constants/dialogOptions';
import { testStatusBarProvider } from '../testStatusBarProvider';
@ -14,3 +14,15 @@ export function showError(error: Error): void {
});
testStatusBarProvider.showFailure();
}
export function parseDocumentSelector(patterns: RelativePattern[]): DocumentSelector {
const documentSelector: DocumentSelector = patterns.map((p: RelativePattern) => {
return {
language: 'java',
scheme: 'file',
pattern: p,
};
});
return documentSelector;
}

Просмотреть файл

@ -1,31 +0,0 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
import { commands, RelativePattern } from 'vscode';
import { testCodeLensController, testFileWatcher } from '../../extension.bundle';
import * as assert from 'assert';
import * as sinon from 'sinon';
import * as path from "path";
import { setupTestEnv, Uris } from '../shared';
suite('Test File Watcher Tests', function() {
const sandbox = sinon.createSandbox();
suiteSetup(async function() {
setupTestEnv();
});
test("Should correctly setup code lens provider", async function() {
let spy: sinon.SinonSpy = sandbox.spy(testCodeLensController, 'registerCodeLensProvider');
await testFileWatcher.registerListeners();
const args: RelativePattern[] = spy.getCall(0).args[0];
assert.ok(args.length === 3);
assert.ok(path.relative(args[0].base, path.join(Uris.JUNIT4_TEST_PACKAGE, '..')));
spy.restore();
});
teardown(async function() {
await commands.executeCommand('workbench.action.closeActiveEditor');
});
});

Просмотреть файл

@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
import { RelativePattern } from 'vscode';
import { testSourceProvider } from '../../extension.bundle';
import * as assert from 'assert';
import { setupTestEnv } from '../shared';
suite('Test File Watcher Tests', function() {
suiteSetup(async function() {
setupTestEnv();
});
test("Should correctly get the test source paths", async function() {
const patterns: RelativePattern[] = await testSourceProvider.getTestSourcePattern();
assert.strictEqual(patterns.length, 3);
});
});