Swapping vscode calls for ApiWrapper for testability (#10267)

* swapping vscode calls for apiwrapper for testability

* Adding mainController tests

* Adding unit tests for input validation

* Adding project controller tests, reorganizing error handling

* Removing commented-out code
This commit is contained in:
Benjin Dubishar
2020-05-06 14:16:27 -07:00
committed by GitHub
parent 0ace033a6f
commit 80901c9a7b
10 changed files with 373 additions and 108 deletions

View File

@@ -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);
}
}

View File

@@ -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 {
@@ -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
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
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); });
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); });
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.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();
@@ -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) {
@@ -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;
}
}

View File

@@ -3,7 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as path from 'path';
import * as constants from '../common/constants';
import * as dataSources from '../models/dataSources/dataSources';
@@ -11,6 +10,8 @@ import * as utils from '../common/utils';
import * as UUID from 'vscode-languageclient/lib/utils/uuid';
import * as templates from '../templates/templates';
import { Uri, QuickPickItem } from 'vscode';
import { ApiWrapper } from '../common/apiWrapper';
import { Project } from '../models/project';
import { SqlDatabaseProjectTreeViewProvider } from './databaseProjectTreeViewProvider';
import { promises as fs } from 'fs';
@@ -26,7 +27,7 @@ export class ProjectsController {
projects: Project[] = [];
constructor(projTreeViewProvider: SqlDatabaseProjectTreeViewProvider) {
constructor(private apiWrapper: ApiWrapper, projTreeViewProvider: SqlDatabaseProjectTreeViewProvider) {
this.projectTreeViewProvider = projTreeViewProvider;
}
@@ -35,29 +36,29 @@ export class ProjectsController {
this.projectTreeViewProvider.load(this.projects);
}
public async openProject(projectFile: vscode.Uri): Promise<Project> {
public async openProject(projectFile: Uri): Promise<Project> {
for (const proj of this.projects) {
if (proj.projectFilePath === projectFile.fsPath) {
vscode.window.showInformationMessage(constants.projectAlreadyOpened(projectFile.fsPath));
this.apiWrapper.showInformationMessage(constants.projectAlreadyOpened(projectFile.fsPath));
return proj;
}
}
// Read project file
const newProject = new Project(projectFile.fsPath);
await newProject.readProjFile();
this.projects.push(newProject);
// Read datasources.json (if present)
const dataSourcesFilePath = path.join(path.dirname(projectFile.fsPath), constants.dataSourcesFileName);
try {
// Read project file
await newProject.readProjFile();
this.projects.push(newProject);
// Read datasources.json (if present)
const dataSourcesFilePath = path.join(path.dirname(projectFile.fsPath), constants.dataSourcesFileName);
newProject.dataSources = await dataSources.load(dataSourcesFilePath);
}
catch (err) {
if (err instanceof dataSources.NoDataSourcesFileError) {
// TODO: prompt to create new datasources.json; for now, swallow
console.log(`No ${constants.dataSourcesFileName} file found.`);
}
else {
throw err;
@@ -69,7 +70,7 @@ export class ProjectsController {
return newProject;
}
public async createNewProject(newProjName: string, folderUri: vscode.Uri, projectGuid?: string): Promise<string> {
public async createNewProject(newProjName: string, folderUri: Uri, projectGuid?: string): Promise<string> {
if (projectGuid && !UUID.isUUID(projectGuid)) {
throw new Error(`Specified GUID is invalid: '${projectGuid}'`);
}
@@ -114,17 +115,17 @@ export class ProjectsController {
public async build(treeNode: BaseProjectTreeItem) {
const project = this.getProjectContextFromTreeNode(treeNode);
await vscode.window.showErrorMessage(`Build not yet implemented: ${project.projectFilePath}`); // TODO
await this.apiWrapper.showErrorMessage(`Build not yet implemented: ${project.projectFilePath}`); // TODO
}
public async deploy(treeNode: BaseProjectTreeItem) {
const project = this.getProjectContextFromTreeNode(treeNode);
await vscode.window.showErrorMessage(`Deploy not yet implemented: ${project.projectFilePath}`); // TODO
await this.apiWrapper.showErrorMessage(`Deploy not yet implemented: ${project.projectFilePath}`); // TODO
}
public async import(treeNode: BaseProjectTreeItem) {
const project = this.getProjectContextFromTreeNode(treeNode);
await vscode.window.showErrorMessage(`Import not yet implemented: ${project.projectFilePath}`); // TODO
await this.apiWrapper.showErrorMessage(`Import not yet implemented: ${project.projectFilePath}`); // TODO
}
public async addFolderPrompt(treeNode: BaseProjectTreeItem) {
@@ -135,26 +136,28 @@ export class ProjectsController {
return; // user cancelled
}
const relativeFolderPath = this.prependContextPath(treeNode, newFolderName);
const relativeFolderPath = path.join(this.getRelativePath(treeNode), newFolderName);
await project.addFolderItem(relativeFolderPath);
this.refreshProjectsTree();
}
public async addItemPrompt(treeNode: BaseProjectTreeItem, itemTypeName?: string) {
const project = this.getProjectContextFromTreeNode(treeNode);
public async addItemPromptFromNode(treeNode: BaseProjectTreeItem, itemTypeName?: string) {
await this.addItemPrompt(this.getProjectContextFromTreeNode(treeNode), this.getRelativePath(treeNode), itemTypeName);
}
public async addItemPrompt(project: Project, relativePath: string, itemTypeName?: string) {
if (!itemTypeName) {
let itemFriendlyNames: string[] = [];
const items: QuickPickItem[] = [];
for (const itemType of templates.projectScriptTypes()) {
itemFriendlyNames.push(itemType.friendlyName);
items.push({ label: itemType.friendlyName });
}
itemTypeName = await vscode.window.showQuickPick(itemFriendlyNames, {
itemTypeName = (await this.apiWrapper.showQuickPick(items, {
canPickMany: false
});
}))?.label;
if (!itemTypeName) {
return; // user cancelled
@@ -162,7 +165,9 @@ export class ProjectsController {
}
const itemType = templates.projectScriptTypeMap()[itemTypeName.toLocaleLowerCase()];
const itemObjectName = await this.promptForNewObjectName(itemType, project);
let itemObjectName = await this.promptForNewObjectName(itemType, project);
itemObjectName = itemObjectName?.trim();
if (!itemObjectName) {
return; // user cancelled
@@ -171,11 +176,11 @@ export class ProjectsController {
// TODO: file already exists?
const newFileText = this.macroExpansion(itemType.templateScript, { 'OBJECT_NAME': itemObjectName });
const relativeFilePath = this.prependContextPath(treeNode, itemObjectName + '.sql');
const relativeFilePath = path.join(relativePath, itemObjectName + '.sql');
const newEntry = await project.addScriptItem(relativeFilePath, newFileText);
vscode.commands.executeCommand('vscode.open', newEntry.fsUri);
this.apiWrapper.executeCommand('vscode.open', newEntry.fsUri);
this.refreshProjectsTree();
}
@@ -216,7 +221,7 @@ export class ProjectsController {
// TODO: ask project for suggested name that doesn't conflict
const suggestedName = itemType.friendlyName.replace(new RegExp('\s', 'g'), '') + '1';
const itemObjectName = await vscode.window.showInputBox({
const itemObjectName = await this.apiWrapper.showInputBox({
prompt: constants.newObjectNamePrompt(itemType.friendlyName),
value: suggestedName,
});
@@ -224,13 +229,8 @@ export class ProjectsController {
return itemObjectName;
}
private prependContextPath(treeNode: BaseProjectTreeItem, objectName: string): string {
if (treeNode instanceof FolderNode) {
return path.join(utils.trimUri(treeNode.root.uri, treeNode.uri), objectName);
}
else {
return objectName;
}
private getRelativePath(treeNode: BaseProjectTreeItem): string {
return treeNode instanceof FolderNode ? utils.trimUri(treeNode.root.uri, treeNode.uri) : '';
}
//#endregion

View File

@@ -5,12 +5,13 @@
import * as vscode from 'vscode';
import MainController from './controllers/mainController';
import { ApiWrapper } from './common/apiWrapper';
let controllers: MainController[] = [];
export async function activate(context: vscode.ExtensionContext): Promise<void> {
// Start the main controller
const mainController = new MainController(context);
const mainController = new MainController(context, new ApiWrapper());
controllers.push(mainController);
context.subscriptions.push(mainController);

View File

@@ -3,14 +3,13 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as path from 'path';
import * as xmldom from 'xmldom';
import * as constants from '../common/constants';
import { Uri } from 'vscode';
import { promises as fs } from 'fs';
import { DataSource } from './dataSources/dataSources';
import { getErrorMessage } from '../common/utils';
/**
* Class representing a Project, and providing functions for operating on it
@@ -35,14 +34,7 @@ export class Project {
*/
public async readProjFile() {
const projFileText = await fs.readFile(this.projectFilePath);
try {
this.projFileXmlDoc = new xmldom.DOMParser().parseFromString(projFileText.toString());
}
catch (err) {
vscode.window.showErrorMessage(err);
return;
}
this.projFileXmlDoc = new xmldom.DOMParser().parseFromString(projFileText.toString());
// find all folders and files to include
@@ -93,7 +85,7 @@ export class Project {
}
private createProjectEntry(relativePath: string, entryType: EntryType): ProjectEntry {
return new ProjectEntry(vscode.Uri.file(path.join(this.projectFolderPath, relativePath)), relativePath, entryType);
return new ProjectEntry(Uri.file(path.join(this.projectFolderPath, relativePath)), relativePath, entryType);
}
private findOrCreateItemGroup(containedTag?: string): any {
@@ -134,21 +126,15 @@ export class Project {
}
private async addToProjFile(entry: ProjectEntry) {
try {
switch (entry.type) {
case EntryType.File:
this.addFileToProjFile(entry.relativePath);
break;
case EntryType.Folder:
this.addFolderToProjFile(entry.relativePath);
}
switch (entry.type) {
case EntryType.File:
this.addFileToProjFile(entry.relativePath);
break;
case EntryType.Folder:
this.addFolderToProjFile(entry.relativePath);
}
await this.serializeToProjFile(this.projFileXmlDoc);
}
catch (err) {
vscode.window.showErrorMessage(getErrorMessage(err));
return;
}
await this.serializeToProjFile(this.projFileXmlDoc);
}
private async serializeToProjFile(projFileContents: any) {
@@ -165,11 +151,11 @@ export class ProjectEntry {
/**
* Absolute file system URI
*/
fsUri: vscode.Uri;
fsUri: Uri;
relativePath: string;
type: EntryType;
constructor(uri: vscode.Uri, relativePath: string, type: EntryType) {
constructor(uri: Uri, relativePath: string, type: EntryType) {
this.fsUri = uri;
this.relativePath = relativePath;
this.type = type;

View File

@@ -0,0 +1,68 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as should from 'should';
import * as path from 'path';
import * as os from 'os';
import * as TypeMoq from 'typemoq';
import * as vscode from 'vscode';
import * as baselines from './baselines/baselines';
import * as templates from '../templates/templates';
import * as constants from '../common/constants';
import { createContext, TestContext } from './testContext';
import MainController from '../controllers/mainController';
import { shouldThrowSpecificError } from './testUtils';
let testContext: TestContext;
describe('MainController: main controller operations', function (): void {
before(async function (): Promise<void> {
testContext = createContext();
await templates.loadTemplates(path.join(__dirname, '..', '..', 'resources', 'templates'));
await baselines.loadBaselines();
});
beforeEach(async function (): Promise<void> {
testContext.apiWrapper.reset();
});
it('Should create new project through MainController', async function (): Promise<void> {
const projFileDir = path.join(os.tmpdir(), `TestProject_${new Date().getTime()}`);
testContext.apiWrapper.setup(x => x.showInputBox(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('MyProjectName'));
testContext.apiWrapper.setup(x => x.showOpenDialog(TypeMoq.It.isAny())).returns(() => Promise.resolve([vscode.Uri.file(projFileDir)]));
testContext.apiWrapper.setup(x => x.workspaceFolders()).returns(() => undefined);
testContext.apiWrapper.setup(x => x.showErrorMessage(TypeMoq.It.isAny())).returns((s) => {
console.log(s);
return Promise.resolve(s);
});
const controller = new MainController(testContext.context, testContext.apiWrapper.object);
const proj = await controller.createNewProject();
should(proj).not.equal(undefined);
});
it('Should show error when no project name', async function (): Promise<void> {
for (const name of ['', ' ', undefined]) {
testContext.apiWrapper.reset();
testContext.apiWrapper.setup(x => x.showInputBox(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(name));
testContext.apiWrapper.setup(x => x.showErrorMessage(TypeMoq.It.isAny())).returns((s) => { throw new Error(s); });
const controller = new MainController(testContext.context, testContext.apiWrapper.object);
await shouldThrowSpecificError(async () => await controller.createNewProject(), constants.projectNameRequired, `case: '${name}'`);
}
});
it('Should show error when no location name', async function (): Promise<void> {
testContext.apiWrapper.setup(x => x.showInputBox(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('MyProjectName'));
testContext.apiWrapper.setup(x => x.showOpenDialog(TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined));
testContext.apiWrapper.setup(x => x.showErrorMessage(TypeMoq.It.isAny())).returns((s) => { throw new Error(s); });
const controller = new MainController(testContext.context, testContext.apiWrapper.object);
await shouldThrowSpecificError(async () => await controller.createNewProject(), constants.projectLocationRequired);
});
});

View File

@@ -7,6 +7,7 @@ import * as should from 'should';
import * as path from 'path';
import * as os from 'os';
import * as vscode from 'vscode';
import * as TypeMoq from 'typemoq';
import * as baselines from './baselines/baselines';
import * as templates from '../templates/templates';
import * as testUtils from './testUtils';
@@ -14,15 +15,20 @@ import * as testUtils from './testUtils';
import { SqlDatabaseProjectTreeViewProvider } from '../controllers/databaseProjectTreeViewProvider';
import { ProjectsController } from '../controllers/projectController';
import { promises as fs } from 'fs';
import { createContext, TestContext } from './testContext';
import { Project } from '../models/project';
let testContext: TestContext;
describe('ProjectsController: project controller operations', function (): void {
before(async function () : Promise<void> {
before(async function (): Promise<void> {
testContext = createContext();
await templates.loadTemplates(path.join(__dirname, '..', '..', 'resources', 'templates'));
await baselines.loadBaselines();
});
it('Should create new sqlproj file with correct values', async function (): Promise<void> {
const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider());
const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider());
const projFileDir = path.join(os.tmpdir(), `TestProject_${new Date().getTime()}`);
const projFilePath = await projController.createNewProject('TestProjectName', vscode.Uri.file(projFileDir), 'BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575');
@@ -38,11 +44,26 @@ describe('ProjectsController: project controller operations', function (): void
const sqlProjPath = await testUtils.createTestSqlProj(baselines.openProjectFileBaseline, folderPath);
await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, folderPath);
const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider());
const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider());
const project = await projController.openProject(vscode.Uri.file(sqlProjPath));
should(project.files.length).equal(9); // detailed sqlproj tests in their own test file
should(project.dataSources.length).equal(2); // detailed datasources tests in their own test file
});
it('Should return silently when no object name provided', async function (): Promise<void> {
for (const name of ['', ' ', undefined]) {
testContext.apiWrapper.reset();
testContext.apiWrapper.setup(x => x.showInputBox(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(name));
testContext.apiWrapper.setup(x => x.showErrorMessage(TypeMoq.It.isAny())).returns((s) => { console.log('we throwin'); throw new Error(s); });
const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider());
const project = new Project('FakePath');
should(project.files.length).equal(0);
await projController.addItemPrompt(new Project('FakePath'), '', templates.script);
should(project.files.length).equal(0, 'Expected to return without throwing an exception or adding a file when an empty/undefined name is provided.');
}
});
});

View File

@@ -14,8 +14,8 @@ describe('Templates: loading templates from disk', function (): void {
});
it('Should throw error when attempting to use templates before loaded from file', async function (): Promise<void> {
shouldThrowSpecificError(() => templates.projectScriptTypeMap(), 'Templates must be loaded from file before attempting to use.');
shouldThrowSpecificError(() => templates.projectScriptTypes(), 'Templates must be loaded from file before attempting to use.');
await shouldThrowSpecificError(() => templates.projectScriptTypeMap(), 'Templates must be loaded from file before attempting to use.');
await shouldThrowSpecificError(() => templates.projectScriptTypes(), 'Templates must be loaded from file before attempting to use.');
});
it('Should load all templates from files', async function (): Promise<void> {

View File

@@ -0,0 +1,39 @@
/*---------------------------------------------------------------------------------------------
* 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 path from 'path';
import * as TypeMoq from 'typemoq';
import { ApiWrapper } from '../common/apiWrapper';
export interface TestContext {
apiWrapper: TypeMoq.IMock<ApiWrapper>;
context: vscode.ExtensionContext;
}
export function createContext(): TestContext {
let extensionPath = path.join(__dirname, '..', '..');
return {
apiWrapper: TypeMoq.Mock.ofType(ApiWrapper),
context: {
subscriptions: [],
workspaceState: {
get: () => { return undefined; },
update: () => { return Promise.resolve(); }
},
globalState: {
get: () => { return Promise.resolve(); },
update: () => { return Promise.resolve(); }
},
extensionPath: extensionPath,
asAbsolutePath: () => { return ''; },
storagePath: '',
globalStoragePath: '',
logPath: '',
extensionUri: vscode.Uri.parse('')
},
};
}

View File

@@ -11,10 +11,10 @@ import { promises as fs } from 'fs';
import should = require('should');
import { AssertionError } from 'assert';
export function shouldThrowSpecificError(block: Function, expectedMessage: string) {
export async function shouldThrowSpecificError(block: Function, expectedMessage: string, details?: string) {
let succeeded = false;
try {
block();
await block();
succeeded = true;
}
catch (err) {
@@ -22,7 +22,7 @@ export function shouldThrowSpecificError(block: Function, expectedMessage: strin
}
if (succeeded) {
throw new AssertionError({ message: 'Operation succeeded, but expected failure with exception: "' + expectedMessage + '"' });
throw new AssertionError({ message: `Operation succeeded, but expected failure with exception: "${expectedMessage}".${details ? ' ' + details : ''}` });
}
}