Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swapping vscode calls for ApiWrapper for testability #10267

Merged
merged 7 commits into from
May 6, 2020
Merged
Show file tree
Hide file tree
Changes from all 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
149 changes: 149 additions & 0 deletions extensions/sql-database-projects/src/common/apiWrapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import * as azdata from 'azdata';

/**
* Wrapper class to act as a facade over VSCode and Data APIs and allow us to test / mock callbacks into
* this API from our code
*/
export class ApiWrapper {
public createOutputChannel(name: string): vscode.OutputChannel {
return vscode.window.createOutputChannel(name);
}

public createTerminalWithOptions(options: vscode.TerminalOptions): vscode.Terminal {
return vscode.window.createTerminal(options);
}

public getCurrentConnection(): Thenable<azdata.connection.ConnectionProfile> {
return azdata.connection.getCurrentConnection();
}

public getCredentials(connectionId: string): Thenable<{ [name: string]: string }> {
return azdata.connection.getCredentials(connectionId);
}

public registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): vscode.Disposable {
return vscode.commands.registerCommand(command, callback, thisArg);
}

public executeCommand<T>(command: string, ...rest: any[]): Thenable<T | undefined> {
return vscode.commands.executeCommand(command, ...rest);
}

public registerTaskHandler(taskId: string, handler: (profile: azdata.IConnectionProfile) => void): void {
azdata.tasks.registerTask(taskId, handler);
}

public registerTreeDataProvider<T>(viewId: string, treeDataProvider: vscode.TreeDataProvider<T>): vscode.Disposable {
return vscode.window.registerTreeDataProvider(viewId, treeDataProvider);
}

public getUriForConnection(connectionId: string): Thenable<string> {
return azdata.connection.getUriForConnection(connectionId);
}

public getProvider<T extends azdata.DataProvider>(providerId: string, providerType: azdata.DataProviderType): T {
return azdata.dataprotocol.getProvider<T>(providerId, providerType);
}

public showErrorMessage(message: string, ...items: string[]): Thenable<string | undefined> {
return vscode.window.showErrorMessage(message, ...items);
}

public showInformationMessage(message: string, ...items: string[]): Thenable<string | undefined> {
return vscode.window.showInformationMessage(message, ...items);
}

public showOpenDialog(options: vscode.OpenDialogOptions): Thenable<vscode.Uri[] | undefined> {
return vscode.window.showOpenDialog(options);
}

public startBackgroundOperation(operationInfo: azdata.BackgroundOperationInfo): void {
azdata.tasks.startBackgroundOperation(operationInfo);
}

public openExternal(target: vscode.Uri): Thenable<boolean> {
return vscode.env.openExternal(target);
}

public getExtension(extensionId: string): vscode.Extension<any> | undefined {
return vscode.extensions.getExtension(extensionId);
}

public getConfiguration(section?: string, resource?: vscode.Uri | null): vscode.WorkspaceConfiguration {
return vscode.workspace.getConfiguration(section, resource);
}

public workspaceFolders(): readonly vscode.WorkspaceFolder[] | undefined {
return vscode.workspace.workspaceFolders;
}

public createTab(title: string): azdata.window.DialogTab {
return azdata.window.createTab(title);
}

public createModelViewDialog(title: string, dialogName?: string, isWide?: boolean): azdata.window.Dialog {
return azdata.window.createModelViewDialog(title, dialogName, isWide);
}

public createWizard(title: string): azdata.window.Wizard {
return azdata.window.createWizard(title);
}

public createWizardPage(title: string): azdata.window.WizardPage {
return azdata.window.createWizardPage(title);
}

public openDialog(dialog: azdata.window.Dialog): void {
return azdata.window.openDialog(dialog);
}

public getAllAccounts(): Thenable<azdata.Account[]> {
return azdata.accounts.getAllAccounts();
}

public getSecurityToken(account: azdata.Account, resource: azdata.AzureResource): Thenable<{ [key: string]: any }> {
return azdata.accounts.getSecurityToken(account, resource);
}

public showQuickPick<T extends vscode.QuickPickItem>(items: T[] | Thenable<T[]>, options?: vscode.QuickPickOptions, token?: vscode.CancellationToken): Thenable<T | undefined> {
return vscode.window.showQuickPick(items, options, token);
}

public showInputBox(options?: vscode.InputBoxOptions, token?: vscode.CancellationToken): Thenable<string | undefined> {
return vscode.window.showInputBox(options, token);
}

public listDatabases(connectionId: string): Thenable<string[]> {
return azdata.connection.listDatabases(connectionId);
}

public openTextDocument(options?: { language?: string; content?: string; }): Thenable<vscode.TextDocument> {
return vscode.workspace.openTextDocument(options);
}

public connect(fileUri: string, connectionId: string): Thenable<void> {
return azdata.queryeditor.connect(fileUri, connectionId);
}

public runQuery(fileUri: string, options?: Map<string, string>, runCurrentQuery?: boolean): void {
azdata.queryeditor.runQuery(fileUri, options, runCurrentQuery);
}

public showTextDocument(uri: vscode.Uri, options?: vscode.TextDocumentShowOptions): Thenable<vscode.TextEditor> {
return vscode.window.showTextDocument(uri, options);
}

public createButton(label: string, position?: azdata.window.DialogButtonPosition): azdata.window.Button {
return azdata.window.createButton(label, position);
}

public registerWidget(widgetId: string, handler: (view: azdata.ModelView) => void): void {
azdata.ui.registerModelViewProvider(widgetId, handler);
}
}
83 changes: 42 additions & 41 deletions extensions/sql-database-projects/src/controllers/mainController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,36 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import * as templates from '../templates/templates';
import * as constants from '../common/constants';
import * as path from 'path';

import { Uri, Disposable, ExtensionContext, WorkspaceFolder } from 'vscode';
import { ApiWrapper } from '../common/apiWrapper';
import { SqlDatabaseProjectTreeViewProvider } from './databaseProjectTreeViewProvider';
import { getErrorMessage } from '../common/utils';
import { ProjectsController } from './projectController';
import { BaseProjectTreeItem } from '../models/tree/baseTreeItem';
import { NetCoreTool } from '../tools/netcoreTool';
import { Project } from '../models/project';

const SQL_DATABASE_PROJECTS_VIEW_ID = 'sqlDatabaseProjectsView';

/**
* The main controller class that initializes the extension
*/
export default class MainController implements vscode.Disposable {
protected _context: vscode.ExtensionContext;
export default class MainController implements Disposable {
protected dbProjectTreeViewProvider: SqlDatabaseProjectTreeViewProvider = new SqlDatabaseProjectTreeViewProvider();
protected projectsController: ProjectsController;
protected netcoreTool: NetCoreTool;

public constructor(context: vscode.ExtensionContext) {
this._context = context;
this.projectsController = new ProjectsController(this.dbProjectTreeViewProvider);
public constructor(private context: ExtensionContext, private apiWrapper: ApiWrapper) {
this.projectsController = new ProjectsController(apiWrapper, this.dbProjectTreeViewProvider);
this.netcoreTool = new NetCoreTool();
}

public get extensionContext(): vscode.ExtensionContext {
return this._context;
public get extensionContext(): ExtensionContext {
return this.context;
}

public deactivate(): void {
Expand All @@ -44,27 +44,26 @@ export default class MainController implements vscode.Disposable {

private async initializeDatabaseProjects(): Promise<void> {
// init commands
vscode.commands.registerCommand('sqlDatabaseProjects.new', async () => { await this.createNewProject(); });
vscode.commands.registerCommand('sqlDatabaseProjects.open', async () => { await this.openProjectFromFile(); });
vscode.commands.registerCommand('sqlDatabaseProjects.close', (node: BaseProjectTreeItem) => { this.projectsController.closeProject(node); });
vscode.commands.registerCommand('sqlDatabaseProjects.properties', async (node: BaseProjectTreeItem) => { await vscode.window.showErrorMessage(`Properties not yet implemented: ${node.uri.path}`); }); // TODO

vscode.commands.registerCommand('sqlDatabaseProjects.build', async (node: BaseProjectTreeItem) => { await this.projectsController.build(node); });
vscode.commands.registerCommand('sqlDatabaseProjects.deploy', async (node: BaseProjectTreeItem) => { await this.projectsController.deploy(node); });
vscode.commands.registerCommand('sqlDatabaseProjects.import', async (node: BaseProjectTreeItem) => { await this.projectsController.import(node); });


vscode.commands.registerCommand('sqlDatabaseProjects.newScript', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPrompt(node, templates.script); });
vscode.commands.registerCommand('sqlDatabaseProjects.newTable', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPrompt(node, templates.table); });
vscode.commands.registerCommand('sqlDatabaseProjects.newView', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPrompt(node, templates.view); });
vscode.commands.registerCommand('sqlDatabaseProjects.newStoredProcedure', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPrompt(node, templates.storedProcedure); });
vscode.commands.registerCommand('sqlDatabaseProjects.newItem', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPrompt(node); });
vscode.commands.registerCommand('sqlDatabaseProjects.newFolder', async (node: BaseProjectTreeItem) => { await this.projectsController.addFolderPrompt(node); });
this.apiWrapper.registerCommand('sqlDatabaseProjects.new', async () => { await this.createNewProject(); });
this.apiWrapper.registerCommand('sqlDatabaseProjects.open', async () => { await this.openProjectFromFile(); });
this.apiWrapper.registerCommand('sqlDatabaseProjects.close', (node: BaseProjectTreeItem) => { this.projectsController.closeProject(node); });
this.apiWrapper.registerCommand('sqlDatabaseProjects.properties', async (node: BaseProjectTreeItem) => { await this.apiWrapper.showErrorMessage(`Properties not yet implemented: ${node.uri.path}`); }); // TODO

this.apiWrapper.registerCommand('sqlDatabaseProjects.build', async (node: BaseProjectTreeItem) => { await this.projectsController.build(node); });
this.apiWrapper.registerCommand('sqlDatabaseProjects.deploy', async (node: BaseProjectTreeItem) => { await this.projectsController.deploy(node); });
this.apiWrapper.registerCommand('sqlDatabaseProjects.import', async (node: BaseProjectTreeItem) => { await this.projectsController.import(node); });

this.apiWrapper.registerCommand('sqlDatabaseProjects.newScript', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPromptFromNode(node, templates.script); });
this.apiWrapper.registerCommand('sqlDatabaseProjects.newTable', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPromptFromNode(node, templates.table); });
this.apiWrapper.registerCommand('sqlDatabaseProjects.newView', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPromptFromNode(node, templates.view); });
this.apiWrapper.registerCommand('sqlDatabaseProjects.newStoredProcedure', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPromptFromNode(node, templates.storedProcedure); });
this.apiWrapper.registerCommand('sqlDatabaseProjects.newItem', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPromptFromNode(node); });
this.apiWrapper.registerCommand('sqlDatabaseProjects.newFolder', async (node: BaseProjectTreeItem) => { await this.projectsController.addFolderPrompt(node); });

// init view
this.extensionContext.subscriptions.push(vscode.window.registerTreeDataProvider(SQL_DATABASE_PROJECTS_VIEW_ID, this.dbProjectTreeViewProvider));
this.extensionContext.subscriptions.push(this.apiWrapper.registerTreeDataProvider(SQL_DATABASE_PROJECTS_VIEW_ID, this.dbProjectTreeViewProvider));

await templates.loadTemplates(path.join(this._context.extensionPath, 'resources', 'templates'));
await templates.loadTemplates(path.join(this.context.extensionPath, 'resources', 'templates'));

// ensure .net core is installed
this.netcoreTool.findOrInstallNetCore();
Expand All @@ -80,7 +79,7 @@ export default class MainController implements vscode.Disposable {

filter[constants.sqlDatabaseProject] = ['sqlproj'];

let files: vscode.Uri[] | undefined = await vscode.window.showOpenDialog({ filters: filter });
let files: Uri[] | undefined = await this.apiWrapper.showOpenDialog({ filters: filter });

if (files) {
for (const file of files) {
Expand All @@ -89,48 +88,50 @@ export default class MainController implements vscode.Disposable {
}
}
catch (err) {
vscode.window.showErrorMessage(getErrorMessage(err));
this.apiWrapper.showErrorMessage(getErrorMessage(err));
}
}

/**
* Creates a new SQL database project from a template, prompting the user for a name and location
*/
public async createNewProject(): Promise<void> {
public async createNewProject(): Promise<Project | undefined> {
try {
let newProjName = await vscode.window.showInputBox({
let newProjName = await this.apiWrapper.showInputBox({
prompt: constants.newDatabaseProjectName,
value: `DatabaseProject${this.projectsController.projects.length + 1}`
// TODO: Smarter way to suggest a name. Easy if we prompt for location first, but that feels odd...
});

newProjName = newProjName?.trim();

if (!newProjName) {
// TODO: is this case considered an intentional cancellation (shouldn't warn) or an error case (should warn)?
vscode.window.showErrorMessage(constants.projectNameRequired);
return;
this.apiWrapper.showErrorMessage(constants.projectNameRequired);
return undefined;
}

let selectionResult = await vscode.window.showOpenDialog({
let selectionResult = await this.apiWrapper.showOpenDialog({
canSelectFiles: false,
canSelectFolders: true,
canSelectMany: false,
defaultUri: vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders[0].uri : undefined
defaultUri: this.apiWrapper.workspaceFolders() ? (this.apiWrapper.workspaceFolders() as WorkspaceFolder[])[0].uri : undefined
});

if (!selectionResult) {
vscode.window.showErrorMessage(constants.projectLocationRequired);
return;
this.apiWrapper.showErrorMessage(constants.projectLocationRequired);
return undefined;
}

// TODO: what if the selected folder is outside the workspace?

const newProjFolderUri = (selectionResult as vscode.Uri[])[0];
console.log(newProjFolderUri.fsPath);
const newProjFilePath = await this.projectsController.createNewProject(newProjName as string, newProjFolderUri as vscode.Uri);
await this.projectsController.openProject(vscode.Uri.file(newProjFilePath));
const newProjFolderUri = (selectionResult as Uri[])[0];
const newProjFilePath = await this.projectsController.createNewProject(newProjName as string, newProjFolderUri as Uri);
return this.projectsController.openProject(Uri.file(newProjFilePath));
}
catch (err) {
vscode.window.showErrorMessage(getErrorMessage(err));
this.apiWrapper.showErrorMessage(getErrorMessage(err));
return undefined;
}
}

Expand Down
Loading