diff --git a/extensions/data-workspace/coverConfig.json b/extensions/data-workspace/coverConfig.json index 7c935463c5..cc99c6298c 100644 --- a/extensions/data-workspace/coverConfig.json +++ b/extensions/data-workspace/coverConfig.json @@ -4,7 +4,8 @@ "relativeCoverageDir": "../../coverage", "ignorePatterns": [ "**/node_modules/**", - "**/test/**" + "**/test/**", + "main.js" ], "reports": [ "cobertura", diff --git a/extensions/data-workspace/package.json b/extensions/data-workspace/package.json index f0a9685ad5..eb1d463ab0 100644 --- a/extensions/data-workspace/package.json +++ b/extensions/data-workspace/package.json @@ -97,11 +97,13 @@ }, "devDependencies": { "@types/mocha": "^5.2.5", + "@types/sinon": "^9.0.4", "mocha": "^5.2.0", "mocha-junit-reporter": "^1.17.0", "mocha-multi-reporters": "^1.1.7", "typemoq": "^2.1.0", "vscodetestcover": "^1.1.0", - "should": "^13.2.3" + "should": "^13.2.3", + "sinon": "^9.0.2" } } diff --git a/extensions/data-workspace/src/common/constants.ts b/extensions/data-workspace/src/common/constants.ts new file mode 100644 index 0000000000..deae08ead1 --- /dev/null +++ b/extensions/data-workspace/src/common/constants.ts @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { EOL } from 'os'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +export const ExtensionActivationErrorMessage = (extensionId: string, err: any): string => { return localize('activateExtensionFailed', "Failed to load the project provider extension '{0}'. Error message: {1}", extensionId, err.message ?? err); }; +export const UnknownProjectsErrorMessage = (projectFiles: string[]): string => { return localize('UnknownProjectsError', "No provider was found for the following projects: {0}", projectFiles.join(EOL)); }; + +export const SelectProjectFileActionName = localize('SelectProjectFileActionName', "Select"); + diff --git a/extensions/data-workspace/src/common/interfaces.ts b/extensions/data-workspace/src/common/interfaces.ts index 2091e3933b..8cba0ad109 100644 --- a/extensions/data-workspace/src/common/interfaces.ts +++ b/extensions/data-workspace/src/common/interfaces.ts @@ -45,13 +45,19 @@ export interface IWorkspaceService { /** * Gets the project files in current workspace */ - getProjectsInWorkspace(): Promise; + getProjectsInWorkspace(): Promise; /** * Gets the project provider by project file - * @param projectFilePath The full path of the project file + * @param projectFileUri The Uri of the project file */ - getProjectProvider(projectFilePath: string): Promise; + getProjectProvider(projectFileUri: vscode.Uri): Promise; + + /** + * Adds the projects to workspace, if a project is not in the workspace folder, its containing folder will be added to the workspace + * @param projectFiles the list of project files to be added, the project file should be absolute path. + */ + addProjectsToWorkspace(projectFiles: vscode.Uri[]): Promise; } /** diff --git a/extensions/data-workspace/src/common/workspaceTreeDataProvider.ts b/extensions/data-workspace/src/common/workspaceTreeDataProvider.ts index b4d1d1cd46..e8ed4bb930 100644 --- a/extensions/data-workspace/src/common/workspaceTreeDataProvider.ts +++ b/extensions/data-workspace/src/common/workspaceTreeDataProvider.ts @@ -5,9 +5,7 @@ import * as vscode from 'vscode'; import { IWorkspaceService, WorkspaceTreeItem as WorkspaceTreeItem } from './interfaces'; -import * as nls from 'vscode-nls'; -import { EOL } from 'os'; -const localize = nls.loadMessageBundle(); +import { UnknownProjectsErrorMessage } from './constants'; /** * Tree data provider for the workspace main view @@ -36,11 +34,10 @@ export class WorkspaceTreeDataProvider implements vscode.TreeDataProvider 0) { - vscode.window.showErrorMessage(localize('UnknownProjectsError', "No provider was found for the following projects: {0}", unknownProjects.join(EOL))); + vscode.window.showErrorMessage(UnknownProjectsErrorMessage(unknownProjects)); } return treeItems; } diff --git a/extensions/data-workspace/src/dataworkspace.d.ts b/extensions/data-workspace/src/dataworkspace.d.ts index cebb5ed7a0..98dee292fd 100644 --- a/extensions/data-workspace/src/dataworkspace.d.ts +++ b/extensions/data-workspace/src/dataworkspace.d.ts @@ -27,9 +27,9 @@ declare module 'dataworkspace' { export interface IProjectProvider { /** * Gets the tree data provider for the given project file - * @param projectFilePath The full path of the project file + * @param projectFile The Uri of the project file */ - getProjectTreeDataProvider(projectFilePath: string): Promise>; + getProjectTreeDataProvider(projectFile: vscode.Uri): Promise>; /** * Gets the supported project types diff --git a/extensions/data-workspace/src/main.ts b/extensions/data-workspace/src/main.ts index e728f7aeea..f0537e92a7 100644 --- a/extensions/data-workspace/src/main.ts +++ b/extensions/data-workspace/src/main.ts @@ -4,16 +4,40 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import * as path from 'path'; import * as dataworkspace from 'dataworkspace'; import { WorkspaceTreeDataProvider } from './common/workspaceTreeDataProvider'; import { WorkspaceService } from './services/workspaceService'; import { DataWorkspaceExtension } from './dataWorkspaceExtension'; +import { SelectProjectFileActionName } from './common/constants'; export async function activate(context: vscode.ExtensionContext): Promise { const workspaceService = new WorkspaceService(); const workspaceTreeDataProvider = new WorkspaceTreeDataProvider(workspaceService); context.subscriptions.push(vscode.window.registerTreeDataProvider('dataworkspace.views.main', workspaceTreeDataProvider)); - context.subscriptions.push(vscode.commands.registerCommand('projects.addProject', () => { + context.subscriptions.push(vscode.commands.registerCommand('projects.addProject', async () => { + // To Sakshi - You can replace the implementation with your complete dialog implementation + // but all the code here should be reusable by you + if (vscode.workspace.workspaceFile) { + const filter: { [name: string]: string[] } = {}; + const projectTypes = await workspaceService.getAllProjectTypes(); + projectTypes.forEach(type => { + filter[type.displayName] = projectTypes.map(projectType => projectType.projectFileExtension); + }); + let fileUris = await vscode.window.showOpenDialog({ + canSelectFiles: true, + canSelectFolders: false, + canSelectMany: false, + defaultUri: vscode.Uri.file(path.dirname(vscode.workspace.workspaceFile.path)), + openLabel: SelectProjectFileActionName, + filters: filter + }); + if (!fileUris || fileUris.length === 0) { + return; + } + await workspaceService.addProjectsToWorkspace(fileUris); + workspaceTreeDataProvider.refresh(); + } })); context.subscriptions.push(vscode.commands.registerCommand('dataworkspace.refresh', () => { diff --git a/extensions/data-workspace/src/services/workspaceService.ts b/extensions/data-workspace/src/services/workspaceService.ts index c41a7fa3cd..5126f470e0 100644 --- a/extensions/data-workspace/src/services/workspaceService.ts +++ b/extensions/data-workspace/src/services/workspaceService.ts @@ -8,14 +8,44 @@ import * as dataworkspace from 'dataworkspace'; import * as path from 'path'; import { IWorkspaceService } from '../common/interfaces'; import { ProjectProviderRegistry } from '../common/projectProviderRegistry'; -import * as nls from 'vscode-nls'; import Logger from '../common/logger'; +import { ExtensionActivationErrorMessage } from '../common/constants'; -const localize = nls.loadMessageBundle(); const WorkspaceConfigurationName = 'dataworkspace'; const ProjectsConfigurationName = 'projects'; export class WorkspaceService implements IWorkspaceService { + async addProjectsToWorkspace(projectFiles: vscode.Uri[]): Promise { + if (vscode.workspace.workspaceFile) { + const currentProjects: vscode.Uri[] = await this.getProjectsInWorkspace(); + const newWorkspaceFolders: string[] = []; + let newProjectFileAdded = false; + for (const projectFile of projectFiles) { + if (currentProjects.findIndex((p: vscode.Uri) => p.fsPath === projectFile.fsPath) === -1) { + currentProjects.push(projectFile); + newProjectFileAdded = true; + + // if the relativePath and the original path is the same, that means the project file is not under + // any workspace folders, we should add the parent folder of the project file to the workspace + const relativePath = vscode.workspace.asRelativePath(projectFile, false); + if (vscode.Uri.file(relativePath).fsPath === projectFile.fsPath) { + newWorkspaceFolders.push(path.dirname(projectFile.path)); + } + } + } + + if (newProjectFileAdded) { + // Save the new set of projects to the workspace configuration. + await this.setWorkspaceConfigurationValue(ProjectsConfigurationName, currentProjects.map(project => this.toRelativePath(project))); + } + + if (newWorkspaceFolders.length > 0) { + // second parameter is null means don't remove any workspace folders + vscode.workspace.updateWorkspaceFolders(vscode.workspace.workspaceFolders!.length, null, ...(newWorkspaceFolders.map(folder => ({ uri: vscode.Uri.file(folder) })))); + } + } + } + async getAllProjectTypes(): Promise { await this.ensureProviderExtensionLoaded(); const projectTypes: dataworkspace.IProjectType[] = []; @@ -25,16 +55,12 @@ export class WorkspaceService implements IWorkspaceService { return projectTypes; } - async getProjectsInWorkspace(): Promise { - if (vscode.workspace.workspaceFile) { - const projects = vscode.workspace.getConfiguration(WorkspaceConfigurationName).get(ProjectsConfigurationName); - return projects.map(project => path.isAbsolute(project) ? project : path.join(vscode.workspace.rootPath!, project)); - } - return []; + async getProjectsInWorkspace(): Promise { + return vscode.workspace.workspaceFile ? this.getWorkspaceConfigurationValue(ProjectsConfigurationName).map(project => this.toUri(project)) : []; } - async getProjectProvider(projectFilePath: string): Promise { - const projectType = path.extname(projectFilePath).replace(/\./g, ''); + async getProjectProvider(projectFile: vscode.Uri): Promise { + const projectType = path.extname(projectFile.path).replace(/\./g, ''); let provider = ProjectProviderRegistry.getProviderByProjectType(projectType); if (!provider) { await this.ensureProviderExtensionLoaded(projectType); @@ -70,7 +96,32 @@ export class WorkspaceService implements IWorkspaceService { try { await extension.activate(); } catch (err) { - Logger.error(localize('activateExtensionFailed', "Failed to load the project provider extension '{0}'. Error message: {1}", extension.id, err.message ?? err)); + Logger.error(ExtensionActivationErrorMessage(extension.id, err)); } } + + getWorkspaceConfigurationValue(configurationName: string): T { + return vscode.workspace.getConfiguration(WorkspaceConfigurationName).get(configurationName) as T; + } + + async setWorkspaceConfigurationValue(configurationName: string, value: any): Promise { + await vscode.workspace.getConfiguration(WorkspaceConfigurationName).update(configurationName, value, vscode.ConfigurationTarget.Workspace); + } + + /** + * Gets the relative path to the workspace file + * @param filePath the absolute path + */ + private toRelativePath(filePath: vscode.Uri): string { + return path.relative(path.dirname(vscode.workspace.workspaceFile!.path!), filePath.path); + } + + /** + * Gets the Uri of the given relative path + * @param relativePath the relative path + */ + private toUri(relativePath: string): vscode.Uri { + const fullPath = path.join(path.dirname(vscode.workspace.workspaceFile!.path!), relativePath); + return vscode.Uri.file(fullPath); + } } diff --git a/extensions/data-workspace/src/test/projectProviderRegistry.test.ts b/extensions/data-workspace/src/test/projectProviderRegistry.test.ts index f983eaca08..e88cd15218 100644 --- a/extensions/data-workspace/src/test/projectProviderRegistry.test.ts +++ b/extensions/data-workspace/src/test/projectProviderRegistry.test.ts @@ -23,7 +23,7 @@ export function createProjectProvider(projectTypes: IProjectType[]): IProjectPro const treeDataProvider = new MockTreeDataProvider(); const projectProvider: IProjectProvider = { supportedProjectTypes: projectTypes, - getProjectTreeDataProvider: (projectFile: string): Promise> => { + getProjectTreeDataProvider: (projectFile: vscode.Uri): Promise> => { return Promise.resolve(treeDataProvider); } }; diff --git a/extensions/data-workspace/src/test/workspaceService.test.ts b/extensions/data-workspace/src/test/workspaceService.test.ts new file mode 100644 index 0000000000..c1a5e5ff75 --- /dev/null +++ b/extensions/data-workspace/src/test/workspaceService.test.ts @@ -0,0 +1,211 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'mocha'; +import * as vscode from 'vscode'; +import * as sinon from 'sinon'; +import * as should from 'should'; +import * as path from 'path'; +import { WorkspaceService } from '../services/workspaceService'; +import { ProjectProviderRegistry } from '../common/projectProviderRegistry'; +import { createProjectProvider } from './projectProviderRegistry.test'; + +const DefaultWorkspaceFilePath = '/test/folder/ws.code-workspace'; + +/** + * Create a stub for vscode.workspace.workspaceFile + * @param workspaceFilePath The workspace file to return + */ +function stubWorkspaceFile(workspaceFilePath: string | undefined): sinon.SinonStub { + return sinon.stub(vscode.workspace, 'workspaceFile').value(workspaceFilePath ? vscode.Uri.file(workspaceFilePath) : undefined); +} + +/** + * Create a stub for vscode.workspace.getConfiguration + * @param returnValue the configuration value to return + */ +function stubGetConfigurationValue(getStub?: sinon.SinonStub, updateStub?: sinon.SinonStub): sinon.SinonStub { + return sinon.stub(vscode.workspace, 'getConfiguration').returns({ + get: (configurationName: string) => { + return getStub!(configurationName); + }, + update: (section: string, value: any, configurationTarget?: vscode.ConfigurationTarget | boolean, overrideInLanguage?: boolean) => { + updateStub!(section, value, configurationTarget); + } + } as vscode.WorkspaceConfiguration); +} + +/** + * Create a stub for vscode.extensions.all + * @param extensions extensions to return + */ +function stubAllExtensions(extensions: vscode.Extension[]): sinon.SinonStub { + return sinon.stub(vscode.extensions, 'all').value(extensions); +} + +function createMockExtension(id: string, isActive: boolean, projectTypes: string[] | undefined): { extension: vscode.Extension, activationStub: sinon.SinonStub } { + const activationStub = sinon.stub(); + const extension: vscode.Extension = { + id: id, + isActive: isActive, + packageJSON: {}, + activate: () => { return activationStub(); } + } as vscode.Extension; + extension.packageJSON.contributes = projectTypes === undefined ? undefined : { projects: projectTypes }; + return { + extension: extension, + activationStub: activationStub + }; +} + +suite('WorkspaceService Tests', function (): void { + const service = new WorkspaceService(); + + this.afterEach(() => { + sinon.restore(); + }); + + test('test getProjectsInWorkspace', async () => { + // No workspace is loaded + stubWorkspaceFile(undefined); + let projects = await service.getProjectsInWorkspace(); + should.strictEqual(projects.length, 0, 'no projects should be returned when no workspace is loaded'); + + // from this point on, workspace is loaded + stubWorkspaceFile(DefaultWorkspaceFilePath); + + // No projects are present in the workspace file + const getConfigurationStub = stubGetConfigurationValue(sinon.stub().returns([])); + projects = await service.getProjectsInWorkspace(); + should.strictEqual(projects.length, 0, 'no projects should be returned when projects are present in the workspace file'); + getConfigurationStub.restore(); + + // Projects are present + stubGetConfigurationValue(sinon.stub().returns(['abc.sqlproj', 'folder1/abc1.sqlproj', 'folder2/abc2.sqlproj'])); + projects = await service.getProjectsInWorkspace(); + should.strictEqual(projects.length, 3, 'there should be 2 projects'); + const project1 = vscode.Uri.file('/test/folder/abc.sqlproj'); + const project2 = vscode.Uri.file('/test/folder/folder1/abc1.sqlproj'); + const project3 = vscode.Uri.file('/test/folder/folder2/abc2.sqlproj'); + should.strictEqual(projects[0].path, project1.path); + should.strictEqual(projects[1].path, project2.path); + should.strictEqual(projects[2].path, project3.path); + }); + + test('test getAllProjectTypes', async () => { + // extensions that are already activated + const extension1 = createMockExtension('ext1', true, ['csproj']); // with projects contribution + const extension2 = createMockExtension('ext2', true, []); // with empty projects contribution + const extension3 = createMockExtension('ext3', true, undefined); // with no contributes in packageJSON + + // extensions that are still not activated + const extension4 = createMockExtension('ext4', false, ['sqlproj']); // with projects contribution + const extension5 = createMockExtension('ext5', false, ['dbproj']); // with projects contribution but activate() will throw error + extension5.activationStub.throws(); // extension activation failure shouldn't cause the getAllProjectTypes() call to fail + const extension6 = createMockExtension('ext6', false, undefined); // with no contributes in packageJSON + const extension7 = createMockExtension('ext7', false, []); // with empty projects contribution + + stubAllExtensions([extension1, extension2, extension3, extension4, extension5, extension6, extension7].map(ext => ext.extension)); + + const provider1 = createProjectProvider([ + { + projectFileExtension: 'testproj', + icon: '', + displayName: 'test project' + }, { + projectFileExtension: 'testproj1', + icon: '', + displayName: 'test project 1' + } + ]); + const provider2 = createProjectProvider([ + { + projectFileExtension: 'sqlproj', + icon: '', + displayName: 'sql project' + } + ]); + sinon.stub(ProjectProviderRegistry, 'providers').value([provider1, provider2]); + const consoleErrorStub = sinon.stub(console, 'error'); + const projectTypes = await service.getAllProjectTypes(); + should.strictEqual(projectTypes.length, 3); + should.strictEqual(projectTypes[0].projectFileExtension, 'testproj'); + should.strictEqual(projectTypes[1].projectFileExtension, 'testproj1'); + should.strictEqual(projectTypes[2].projectFileExtension, 'sqlproj'); + should.strictEqual(extension1.activationStub.notCalled, true, 'extension1.activate() should not have been called'); + should.strictEqual(extension2.activationStub.notCalled, true, 'extension2.activate() should not have been called'); + should.strictEqual(extension3.activationStub.notCalled, true, 'extension3.activate() should not have been called'); + should.strictEqual(extension4.activationStub.calledOnce, true, 'extension4.activate() should have been called'); + should.strictEqual(extension5.activationStub.called, true, 'extension5.activate() should have been called'); + should.strictEqual(extension6.activationStub.notCalled, true, 'extension6.activate() should not have been called'); + should.strictEqual(extension7.activationStub.notCalled, true, 'extension7.activate() should not have been called'); + should.strictEqual(consoleErrorStub.calledOnce, true, 'Logger.error should be called once'); + }); + + test('test getProjectProvider', async () => { + const extension1 = createMockExtension('ext1', true, ['csproj']); + const extension2 = createMockExtension('ext2', false, ['sqlproj']); + const extension3 = createMockExtension('ext3', false, ['dbproj']); + stubAllExtensions([extension1, extension2, extension3].map(ext => ext.extension)); + const getProviderByProjectTypeStub = sinon.stub(ProjectProviderRegistry, 'getProviderByProjectType'); + getProviderByProjectTypeStub.onFirstCall().returns(undefined); + getProviderByProjectTypeStub.onSecondCall().returns(createProjectProvider([ + { + projectFileExtension: 'sqlproj', + icon: '', + displayName: 'test project' + } + ])); + let provider = await service.getProjectProvider(vscode.Uri.file('abc.sqlproj')); + should.notStrictEqual(provider, undefined, 'Provider should be returned for sqlproj'); + should.strictEqual(provider!.supportedProjectTypes[0].projectFileExtension, 'sqlproj'); + should.strictEqual(extension1.activationStub.notCalled, true, 'the ext1.activate() should not have been called for sqlproj'); + should.strictEqual(extension2.activationStub.calledOnce, true, 'the ext2.activate() should have been called once after requesting sqlproj provider'); + should.strictEqual(extension3.activationStub.notCalled, true, 'the ext3.activate() should not have been called for sqlproj'); + + getProviderByProjectTypeStub.reset(); + getProviderByProjectTypeStub.returns(createProjectProvider([{ + projectFileExtension: 'csproj', + icon: '', + displayName: 'test cs project' + }])); + provider = await service.getProjectProvider(vscode.Uri.file('abc.csproj')); + should.notStrictEqual(provider, undefined, 'Provider should be returned for csproj'); + should.strictEqual(provider!.supportedProjectTypes[0].projectFileExtension, 'csproj'); + should.strictEqual(extension1.activationStub.notCalled, true, 'the ext1.activate() should not have been called for csproj'); + should.strictEqual(extension2.activationStub.calledOnce, true, 'the ext2.activate() should still have been called once'); + should.strictEqual(extension3.activationStub.notCalled, true, 'the ext3.activate() should not have been called for csproj'); + }); + + test('test addProjectsToWorkspace', async () => { + const processPath = (original: string): string => { + return original.replace(/\//g, path.sep); + }; + stubWorkspaceFile(DefaultWorkspaceFilePath); + const updateConfigurationStub = sinon.stub(); + const getConfigurationStub = sinon.stub().returns([processPath('folder1/proj2.sqlproj')]); + stubGetConfigurationValue(getConfigurationStub, updateConfigurationStub); + const asRelativeStub = sinon.stub(vscode.workspace, 'asRelativePath'); + sinon.stub(vscode.workspace, 'workspaceFolders').value(['.']); + asRelativeStub.onFirstCall().returns(`proj1.sqlproj`); + asRelativeStub.onSecondCall().returns(processPath('/test/other/proj3.sqlproj')); + const updateWorkspaceFoldersStub = sinon.stub(vscode.workspace, 'updateWorkspaceFolders'); + await service.addProjectsToWorkspace([ + vscode.Uri.file('/test/folder/proj1.sqlproj'), // within the workspace folder + vscode.Uri.file('/test/folder/folder1/proj2.sqlproj'), //already exists + vscode.Uri.file('/test/other/proj3.sqlproj') // outside of workspace folder + ]); + should.strictEqual(updateConfigurationStub.calledOnce, true, 'update configuration should have been called once'); + should.strictEqual(updateWorkspaceFoldersStub.calledOnce, true, 'updateWorkspaceFolders should have been called once'); + should.strictEqual(updateConfigurationStub.calledWith('projects', sinon.match.array.deepEquals([ + processPath('folder1/proj2.sqlproj'), + processPath('proj1.sqlproj'), + processPath('../other/proj3.sqlproj') + ]), vscode.ConfigurationTarget.Workspace), true, 'updateConfiguration parameters does not match expectation'); + should.strictEqual(updateWorkspaceFoldersStub.calledWith(1, null, sinon.match((arg) => { + return arg.uri.path === '/test/other'; + })), true, 'updateWorkspaceFolder parameters does not match expectation'); + }); +}); diff --git a/extensions/data-workspace/src/test/workspaceTreeDataProvider.test.ts b/extensions/data-workspace/src/test/workspaceTreeDataProvider.test.ts new file mode 100644 index 0000000000..e3b8b59ca8 --- /dev/null +++ b/extensions/data-workspace/src/test/workspaceTreeDataProvider.test.ts @@ -0,0 +1,90 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'mocha'; +import * as sinon from 'sinon'; +import * as vscode from 'vscode'; +import * as should from 'should'; +import { WorkspaceTreeDataProvider } from '../common/workspaceTreeDataProvider'; +import { WorkspaceService } from '../services/workspaceService'; +import { WorkspaceTreeItem } from '../common/interfaces'; +import { IProjectProvider } from 'dataworkspace'; +import { MockTreeDataProvider } from './projectProviderRegistry.test'; + +suite('workspaceTreeDataProvider Tests', function (): void { + const workspaceService = new WorkspaceService(); + const treeProvider = new WorkspaceTreeDataProvider(workspaceService); + + this.afterEach(() => { + sinon.restore(); + }); + + test('test refresh()', () => { + const treeDataChangeHandler = sinon.stub(); + treeProvider.onDidChangeTreeData!((e) => { + treeDataChangeHandler(e); + }); + treeProvider.refresh(); + should.strictEqual(treeDataChangeHandler.calledOnce, true); + }); + + test('test getTreeItem()', () => { + const getTreeItemStub = sinon.stub(); + treeProvider.getTreeItem(({ + treeDataProvider: ({ + getTreeItem: (arg: WorkspaceTreeItem) => { + return getTreeItemStub(arg); + } + }) as vscode.TreeDataProvider + }) as WorkspaceTreeItem); + should.strictEqual(getTreeItemStub.calledOnce, true); + }); + + test('test getChildren() for non-root element', async () => { + const getChildrenStub = sinon.stub().resolves([]); + const element = { + treeDataProvider: ({ + getChildren: (arg: any) => { + return getChildrenStub(arg); + } + }) as vscode.TreeDataProvider, + element: 'obj1' + }; + const children = await treeProvider.getChildren(element); + should.strictEqual(children.length, 0, 'children count should be 0'); + should.strictEqual(getChildrenStub.calledWithExactly('obj1'), true, 'getChildren parameter should be obj1') + }); + + test('test getChildren() for root element', async () => { + const getProjectsInWorkspaceStub = sinon.stub(workspaceService, 'getProjectsInWorkspace').resolves( + [ + vscode.Uri.file('test/proj1/proj1.sqlproj'), + vscode.Uri.file('test/proj2/proj2.csproj') + ] + ); + const treeDataProvider = new MockTreeDataProvider(); + const projectProvider: IProjectProvider = { + supportedProjectTypes: [{ + projectFileExtension: 'sqlproj', + icon: '', + displayName: 'sql project' + }], + getProjectTreeDataProvider: (projectFile: vscode.Uri): Promise> => { + return Promise.resolve(treeDataProvider); + } + }; + const getProjectProviderStub = sinon.stub(workspaceService, 'getProjectProvider'); + getProjectProviderStub.onFirstCall().resolves(undefined); + getProjectProviderStub.onSecondCall().resolves(projectProvider); + sinon.stub(treeDataProvider, 'getChildren').resolves(['treeitem1']); + const showErrorMessageStub = sinon.stub(vscode.window, 'showErrorMessage'); + const children = await treeProvider.getChildren(undefined); + should.strictEqual(children.length, 1, 'there should be 1 tree item returned'); + should.strictEqual(children[0].element, 'treeitem1'); + should.strictEqual(getProjectsInWorkspaceStub.calledOnce, true, 'getProjectsInWorkspaceStub should be called'); + should.strictEqual(getProjectProviderStub.calledTwice, true, 'getProjectProvider should be called twice'); + should.strictEqual(showErrorMessageStub.calledOnce, true, 'showErrorMessage should be called once'); + }); +}); diff --git a/extensions/data-workspace/yarn.lock b/extensions/data-workspace/yarn.lock index 6aeea87b40..1c326587f0 100644 --- a/extensions/data-workspace/yarn.lock +++ b/extensions/data-workspace/yarn.lock @@ -182,11 +182,59 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd" integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw== +"@sinonjs/commons@^1", "@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.7.2": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.1.tgz#e7df00f98a203324f6dc7cc606cad9d4a8ab2217" + integrity sha512-892K+kWUUi3cl+LlqEWIDrhvLgdL79tECi8JZUyq6IviKy/DNhuzCRlbHUjxK89f4ypPMMaFnFuR9Ie6DoIMsw== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^6.0.0", "@sinonjs/fake-timers@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz#293674fccb3262ac782c7aadfdeca86b10c75c40" + integrity sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA== + dependencies: + "@sinonjs/commons" "^1.7.0" + +"@sinonjs/formatio@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-5.0.1.tgz#f13e713cb3313b1ab965901b01b0828ea6b77089" + integrity sha512-KaiQ5pBf1MpS09MuA0kp6KBQt2JUOQycqVG1NZXvzeaXe5LGFqAKueIS0bw4w0P9r7KuBSVdUk5QjXsUdu2CxQ== + dependencies: + "@sinonjs/commons" "^1" + "@sinonjs/samsam" "^5.0.2" + +"@sinonjs/samsam@^5.0.2", "@sinonjs/samsam@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-5.1.0.tgz#3afe719232b541bb6cf3411a4c399a188de21ec0" + integrity sha512-42nyaQOVunX5Pm6GRJobmzbS7iLI+fhERITnETXzzwDZh+TtDr/Au3yAvXVjFmZ4wEUaE4Y3NFZfKv0bV0cbtg== + dependencies: + "@sinonjs/commons" "^1.6.0" + lodash.get "^4.4.2" + type-detect "^4.0.8" + +"@sinonjs/text-encoding@^0.7.1": + version "0.7.1" + resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" + integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== + "@types/mocha@^5.2.5": version "5.2.7" resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.7.tgz#315d570ccb56c53452ff8638738df60726d5b6ea" integrity sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ== +"@types/sinon@^9.0.4": + version "9.0.5" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-9.0.5.tgz#56b2a12662dd8c7d081cdc511af5f872cb37377f" + integrity sha512-4CnkGdM/5/FXDGqL32JQ1ttVrGvhOoesLLF7VnTh4KdjK5N5VQOtxaylFqqTjnHx55MnD9O02Nbk5c1ELC8wlQ== + dependencies: + "@types/sinonjs__fake-timers" "*" + +"@types/sinonjs__fake-timers@*": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz#681df970358c82836b42f989188d133e218c458e" + integrity sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA== + ansi-regex@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" @@ -329,6 +377,11 @@ diff@3.5.0: resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== +diff@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + escape-string-regexp@1.0.5, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -428,6 +481,11 @@ is-buffer@~1.1.1: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + istanbul-lib-coverage@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49" @@ -503,6 +561,16 @@ json5@^2.1.2: dependencies: minimist "^1.2.5" +just-extend@^4.0.2: + version "4.1.0" + resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.1.0.tgz#7278a4027d889601640ee0ce0e5a00b992467da4" + integrity sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA== + +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= + lodash@^4.16.4, lodash@^4.17.13, lodash@^4.17.4: version "4.17.19" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" @@ -602,6 +670,17 @@ ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== +nise@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/nise/-/nise-4.0.4.tgz#d73dea3e5731e6561992b8f570be9e363c4512dd" + integrity sha512-bTTRUNlemx6deJa+ZyoCUTRvH3liK5+N6VQZ4NIw90AgDXY6iPnsqplNFf6STcj+ePk0H/xqxnP75Lr0J0Fq3A== + dependencies: + "@sinonjs/commons" "^1.7.0" + "@sinonjs/fake-timers" "^6.0.0" + "@sinonjs/text-encoding" "^0.7.1" + just-extend "^4.0.2" + path-to-regexp "^1.7.0" + once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -619,6 +698,13 @@ path-parse@^1.0.6: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== +path-to-regexp@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" + integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== + dependencies: + isarray "0.0.1" + pify@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" @@ -702,6 +788,19 @@ should@^13.2.3: should-type-adaptors "^1.0.1" should-util "^1.0.0" +sinon@^9.0.2: + version "9.0.3" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-9.0.3.tgz#bffc3ec28c936332cd2a41833547b9eed201ecff" + integrity sha512-IKo9MIM111+smz9JGwLmw5U1075n1YXeAq8YeSFlndCLhAL5KGn6bLgu7b/4AYHTV/LcEMcRm2wU2YiL55/6Pg== + dependencies: + "@sinonjs/commons" "^1.7.2" + "@sinonjs/fake-timers" "^6.0.1" + "@sinonjs/formatio" "^5.0.1" + "@sinonjs/samsam" "^5.1.0" + diff "^4.0.2" + nise "^4.0.4" + supports-color "^7.1.0" + source-map@^0.5.0: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -750,6 +849,11 @@ to-fast-properties@^2.0.0: resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= +type-detect@4.0.8, type-detect@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + typemoq@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/typemoq/-/typemoq-2.1.0.tgz#4452ce360d92cf2a1a180f0c29de2803f87af1e8" diff --git a/extensions/sql-database-projects/src/projectProvider/projectProvider.ts b/extensions/sql-database-projects/src/projectProvider/projectProvider.ts index 40f3c6d6c1..6ba2d64c25 100644 --- a/extensions/sql-database-projects/src/projectProvider/projectProvider.ts +++ b/extensions/sql-database-projects/src/projectProvider/projectProvider.ts @@ -15,11 +15,11 @@ export class SqlDatabaseProjectProvider implements dataworkspace.IProjectProvide /** * Gets the project tree data provider - * @param projectFilePath The project file path + * @param projectFile The project file Uri */ - async getProjectTreeDataProvider(projectFilePath: string): Promise> { + async getProjectTreeDataProvider(projectFilePath: vscode.Uri): Promise> { const provider = new SqlDatabaseProjectTreeViewProvider(); - const project = await Project.openProject(projectFilePath); + const project = await Project.openProject(projectFilePath.fsPath); provider.load([project]); return provider; }