Code refactoring for extension testing (#10529)

* Setting up tests on import extension

* -Added API wrappers for all the azdata and vscode APIs to make them easily mockable
-Added some unit tests for the import extension
-Some code logic separations

* -added code report for the import extension in ci

* Did some more code refractoring

* -Added json report generation

* updated vscodetestcoverage to latest version in import extension.

* -remove duplicate codecoverageConfig.json
This commit is contained in:
Aasim Khan
2020-06-16 13:24:48 -07:00
committed by GitHub
parent 94bc0d9559
commit f725ee96b9
22 changed files with 1356 additions and 231 deletions

View File

@@ -5,12 +5,14 @@
import * as azdata from 'azdata';
import { ImportDataModel } from './models';
import { ApiWrapper } from '../../common/apiWrapper';
export abstract class BasePage {
protected readonly wizardPage: azdata.window.WizardPage;
protected readonly model: ImportDataModel;
protected readonly view: azdata.ModelView;
protected _apiWrapper: ApiWrapper;
/**
* This method constructs all the elements of the page.
@@ -42,8 +44,8 @@ export abstract class BasePage {
*/
public abstract setupNavigationValidator(): void;
protected async getServerValues(): Promise<{ connection: azdata.connection.Connection, displayName: string, name: string }[]> {
let cons = await azdata.connection.getActiveConnections();
public async getServerValues(): Promise<{ connection: azdata.connection.Connection, displayName: string, name: string }[]> {
let cons = await this._apiWrapper.getActiveConnections();
// This user has no active connections ABORT MISSION
if (!cons || cons.length === 0) {
return undefined;
@@ -90,10 +92,10 @@ export abstract class BasePage {
return values;
}
protected async getDatabaseValues(): Promise<{ displayName: string, name: string }[]> {
public async getDatabaseValues(): Promise<{ displayName: string, name: string }[]> {
let idx = -1;
let count = -1;
let values = (await azdata.connection.listDatabases(this.model.server.connectionId)).map(db => {
let values = (await this._apiWrapper.listDatabases(this.model.server.connectionId)).map(db => {
count++;
if (this.model.database && db === this.model.database) {
idx = count;
@@ -109,10 +111,7 @@ export abstract class BasePage {
let tmp = values[0];
values[0] = values[idx];
values[idx] = tmp;
} else {
this.deleteDatabaseValues();
}
return values;
}
@@ -121,8 +120,4 @@ export abstract class BasePage {
delete this.model.serverId;
delete this.model.database;
}
protected deleteDatabaseValues() {
return;
}
}

View File

@@ -8,6 +8,7 @@ import * as azdata from 'azdata';
import { FlatFileProvider } from '../../services/contracts';
import { FlatFileWizard } from '../flatFileWizard';
import { BasePage } from './basePage';
import { ApiWrapper } from '../../common/apiWrapper';
export abstract class ImportPage extends BasePage {
@@ -17,12 +18,14 @@ export abstract class ImportPage extends BasePage {
protected readonly view: azdata.ModelView;
protected readonly provider: FlatFileProvider;
protected constructor(instance: FlatFileWizard, wizardPage: azdata.window.WizardPage, model: ImportDataModel, view: azdata.ModelView, provider: FlatFileProvider) {
constructor(instance: FlatFileWizard, wizardPage: azdata.window.WizardPage, model: ImportDataModel, view: azdata.ModelView, provider: FlatFileProvider, apiWrapper: ApiWrapper) {
super();
this.instance = instance;
this.wizardPage = wizardPage;
this.model = model;
this.view = view;
this.provider = provider;
this._apiWrapper = apiWrapper;
}
}

View File

@@ -3,8 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import * as azdata from 'azdata';
import { FlatFileProvider } from '../services/contracts';
import { ImportDataModel } from './api/models';
@@ -14,16 +12,23 @@ import { FileConfigPage } from './pages/fileConfigPage';
import { ProsePreviewPage } from './pages/prosePreviewPage';
import { ModifyColumnsPage } from './pages/modifyColumnsPage';
import { SummaryPage } from './pages/summaryPage';
const localize = nls.loadMessageBundle();
import { ApiWrapper } from '../common/apiWrapper';
import * as constants from '../common/constants';
export class FlatFileWizard {
private readonly provider: FlatFileProvider;
private wizard: azdata.window.Wizard;
public wizard: azdata.window.Wizard;
public page1: azdata.window.WizardPage;
public page2: azdata.window.WizardPage;
public page3: azdata.window.WizardPage;
public page4: azdata.window.WizardPage;
private importAnotherFileButton: azdata.window.Button;
constructor(provider: FlatFileProvider) {
constructor(
provider: FlatFileProvider,
private _apiWrapper: ApiWrapper
) {
this.provider = provider;
}
@@ -38,37 +43,24 @@ export class FlatFileWizard {
let pages: Map<number, ImportPage> = new Map<number, ImportPage>();
let currentConnection = await azdata.connection.getCurrentConnection();
let connectionId: string = await this.getConnectionId();
let connectionId: string;
if (!currentConnection) {
connectionId = (await azdata.connection.openConnectionDialog(['MSSQL'])).connectionId;
if (!connectionId) {
vscode.window.showErrorMessage(localize('import.needConnection', "Please connect to a server before using this wizard."));
return;
}
} else {
if (currentConnection.providerId !== 'MSSQL') {
vscode.window.showErrorMessage(localize('import.needSQLConnection', "SQL Server Import extension does not support this type of connection"));
return;
}
connectionId = currentConnection.connectionId;
if (!connectionId) {
return;
}
model.serverId = connectionId;
this.wizard = azdata.window.createWizard(localize('flatFileImport.wizardName', "Import flat file wizard"));
let page1 = azdata.window.createWizardPage(localize('flatFileImport.page1Name', "Specify Input File"));
let page2 = azdata.window.createWizardPage(localize('flatFileImport.page2Name', "Preview Data"));
let page3 = azdata.window.createWizardPage(localize('flatFileImport.page3Name', "Modify Columns"));
let page4 = azdata.window.createWizardPage(localize('flatFileImport.page4Name', "Summary"));
this.wizard = this._apiWrapper.createWizard(constants.wizardNameText);
this.page1 = this._apiWrapper.createWizardPage(constants.page1NameText);
this.page2 = this._apiWrapper.createWizardPage(constants.page2NameText);
this.page3 = this._apiWrapper.createWizardPage(constants.page3NameText);
this.page4 = this._apiWrapper.createWizardPage(constants.page4NameText);
let fileConfigPage: FileConfigPage;
page1.registerContent(async (view) => {
fileConfigPage = new FileConfigPage(this, page1, model, view, this.provider);
this.page1.registerContent(async (view) => {
fileConfigPage = new FileConfigPage(this, this.page1, model, view, this.provider, this._apiWrapper);
pages.set(0, fileConfigPage);
await fileConfigPage.start().then(() => {
fileConfigPage.setupNavigationValidator();
@@ -77,29 +69,29 @@ export class FlatFileWizard {
});
let prosePreviewPage: ProsePreviewPage;
page2.registerContent(async (view) => {
prosePreviewPage = new ProsePreviewPage(this, page2, model, view, this.provider);
this.page2.registerContent(async (view) => {
prosePreviewPage = new ProsePreviewPage(this, this.page2, model, view, this.provider, this._apiWrapper);
pages.set(1, prosePreviewPage);
await prosePreviewPage.start();
});
let modifyColumnsPage: ModifyColumnsPage;
page3.registerContent(async (view) => {
modifyColumnsPage = new ModifyColumnsPage(this, page3, model, view, this.provider);
this.page3.registerContent(async (view) => {
modifyColumnsPage = new ModifyColumnsPage(this, this.page3, model, view, this.provider, this._apiWrapper);
pages.set(2, modifyColumnsPage);
await modifyColumnsPage.start();
});
let summaryPage: SummaryPage;
page4.registerContent(async (view) => {
summaryPage = new SummaryPage(this, page4, model, view, this.provider);
this.page4.registerContent(async (view) => {
summaryPage = new SummaryPage(this, this.page4, model, view, this.provider, this._apiWrapper);
pages.set(3, summaryPage);
await summaryPage.start();
});
this.importAnotherFileButton = azdata.window.createButton(localize('flatFileImport.importNewFile', "Import new file"));
this.importAnotherFileButton = this._apiWrapper.createButton(constants.importNewFileText);
this.importAnotherFileButton.onClick(() => {
//TODO replace this with proper cleanup for all the pages
this.wizard.close();
@@ -126,11 +118,33 @@ export class FlatFileWizard {
//not needed for this wizard
this.wizard.generateScriptButton.hidden = true;
this.wizard.pages = [page1, page2, page3, page4];
this.wizard.pages = [this.page1, this.page2, this.page3, this.page4];
this.wizard.open();
}
public async getConnectionId(): Promise<string> {
let currentConnection = await this._apiWrapper.getCurrentConnection();
let connectionId: string;
if (!currentConnection) {
let connection = await this._apiWrapper.openConnectionDialog(constants.supportedProviders);
if (!connection) {
this._apiWrapper.showErrorMessage(constants.needConnectionText);
return undefined;
}
connectionId = connection.connectionId;
} else {
if (currentConnection.providerId !== 'MSSQL') {
this._apiWrapper.showErrorMessage(constants.needSqlConnectionText);
return undefined;
}
connectionId = currentConnection.connectionId;
}
return connectionId;
}
public setImportAnotherFileVisibility(visibility: boolean) {
this.importAnotherFileButton.hidden = !visibility;
}

View File

@@ -5,13 +5,8 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { ImportDataModel } from '../api/models';
import { ImportPage } from '../api/importPage';
import { FlatFileProvider } from '../../services/contracts';
import { FlatFileWizard } from '../flatFileWizard';
const localize = nls.loadMessageBundle();
import * as constants from '../../common/constants';
export class FileConfigPage extends ImportPage {
@@ -28,10 +23,6 @@ export class FileConfigPage extends ImportPage {
private tableNames: string[] = [];
public constructor(instance: FlatFileWizard, wizardPage: azdata.window.WizardPage, model: ImportDataModel, view: azdata.ModelView, provider: FlatFileProvider) {
super(instance, wizardPage, model, view, provider);
}
async start(): Promise<boolean> {
let schemaComponent = await this.createSchemaDropdown();
let tableNameComponent = await this.createTableNameBox();
@@ -96,7 +87,7 @@ export class FileConfigPage extends ImportPage {
return {
component: this.serverDropdown,
title: localize('flatFileImport.serverDropdownTitle', "Server the database is in")
title: constants.serverDropDownTitleText
};
}
@@ -124,8 +115,8 @@ export class FileConfigPage extends ImportPage {
this.databaseDropdown.onValueChanged(async (db) => {
this.model.database = (<azdata.CategoryValue>this.databaseDropdown.value).name;
//this.populateTableNames();
let connectionProvider = azdata.dataprotocol.getProvider<azdata.ConnectionProvider>(this.model.server.providerName, azdata.DataProviderType.ConnectionProvider);
let connectionUri = await azdata.connection.getUriForConnection(this.model.server.connectionId);
let connectionProvider = this._apiWrapper.getProvider<azdata.ConnectionProvider>(this.model.server.providerName, azdata.DataProviderType.ConnectionProvider);
let connectionUri = await this._apiWrapper.getUriForConnection(this.model.server.connectionId);
connectionProvider.changeDatabase(connectionUri, this.model.database);
this.populateSchemaDropdown();
});
@@ -134,7 +125,7 @@ export class FileConfigPage extends ImportPage {
return {
component: this.databaseLoader,
title: localize('flatFileImport.databaseDropdownTitle', "Database the table is created in")
title: constants.databaseDropdownTitleText
};
}
@@ -178,7 +169,7 @@ export class FileConfigPage extends ImportPage {
required: true
}).component();
this.fileButton = this.view.modelBuilder.button().withProperties({
label: localize('flatFileImport.browseFiles', "Browse"),
label: constants.browseFilesText,
}).component();
this.fileButton.onDidClick(async (click) => {
@@ -187,7 +178,7 @@ export class FileConfigPage extends ImportPage {
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
openLabel: localize('flatFileImport.openFile', "Open"),
openLabel: constants.openFileText,
filters: {
'CSV/TXT Files': ['csv', 'txt'],
'All Files': ['*']
@@ -227,7 +218,7 @@ export class FileConfigPage extends ImportPage {
return {
component: this.fileTextBox,
title: localize('flatFileImport.fileTextboxTitle', "Location of the file to be imported"),
title: constants.fileTextboxTitleText,
actions: [this.fileButton]
};
}
@@ -256,7 +247,7 @@ export class FileConfigPage extends ImportPage {
return {
component: this.tableNameTextBox,
title: localize('flatFileImport.tableTextboxTitle', "New table name"),
title: constants.tableTextboxTitleText,
};
}
@@ -274,20 +265,31 @@ export class FileConfigPage extends ImportPage {
return {
component: this.schemaLoader,
title: localize('flatFileImport.schemaTextboxTitle', "Table schema"),
title: constants.schemaTextboxTitleText,
};
}
private async populateSchemaDropdown(): Promise<boolean> {
public async populateSchemaDropdown(): Promise<boolean> {
this.schemaLoader.loading = true;
let connectionUri = await azdata.connection.getUriForConnection(this.model.server.connectionId);
let queryProvider = azdata.dataprotocol.getProvider<azdata.QueryProvider>(this.model.server.providerName, azdata.DataProviderType.QueryProvider);
let values = await this.getSchemaValues();
const query = `SELECT name FROM sys.schemas`;
this.model.schema = values[0].name;
let results = await queryProvider.runQueryAndReturn(connectionUri, query);
this.schemaDropdown.updateProperties({
values: values
});
this.schemaLoader.loading = false;
return true;
}
public async getSchemaValues(): Promise<{ displayName: string, name: string }[]> {
let connectionUri = await this._apiWrapper.getUriForConnection(this.model.server.connectionId);
let queryProvider = this._apiWrapper.getProvider<azdata.QueryProvider>(this.model.server.providerName, azdata.DataProviderType.QueryProvider);
let results = await queryProvider.runQueryAndReturn(connectionUri, constants.selectSchemaQuery);
let idx = -1;
let count = -1;
@@ -311,15 +313,7 @@ export class FileConfigPage extends ImportPage {
values[0] = values[idx];
values[idx] = tmp;
}
this.model.schema = values[0].name;
this.schemaDropdown.updateProperties({
values: values
});
this.schemaLoader.loading = false;
return true;
return values;
}
protected deleteServerValues() {

View File

@@ -4,13 +4,9 @@
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as nls from 'vscode-nls';
import { ColumnMetadata, ImportDataModel } from '../api/models';
import { ColumnMetadata } from '../api/models';
import { ImportPage } from '../api/importPage';
import { FlatFileProvider } from '../../services/contracts';
import { FlatFileWizard } from '../flatFileWizard';
const localize = nls.loadMessageBundle();
import * as constants from '../../common/constants';
export class ModifyColumnsPage extends ImportPage {
private readonly categoryValues = [
@@ -54,11 +50,6 @@ export class ModifyColumnsPage extends ImportPage {
private text: azdata.TextComponent;
private form: azdata.FormContainer;
public constructor(instance: FlatFileWizard, wizardPage: azdata.window.WizardPage, model: ImportDataModel, view: azdata.ModelView, provider: FlatFileProvider) {
super(instance, wizardPage, model, view, provider);
}
private static convertMetadata(column: ColumnMetadata): any[] {
return [column.columnName, column.dataType, false, column.nullable];
}
@@ -105,20 +96,20 @@ export class ModifyColumnsPage extends ImportPage {
async onPageEnter(): Promise<boolean> {
this.loading.loading = true;
await this.populateTable();
this.instance.changeNextButtonLabel(localize('flatFileImport.importData', "Import Data"));
this.instance.changeNextButtonLabel(constants.importDataText);
this.loading.loading = false;
return true;
}
async onPageLeave(): Promise<boolean> {
this.instance.changeNextButtonLabel(localize('flatFileImport.next', "Next"));
this.instance.changeNextButtonLabel(constants.nextText);
return undefined;
}
async cleanup(): Promise<boolean> {
delete this.model.proseColumns;
this.instance.changeNextButtonLabel(localize('flatFileImport.next', "Next"));
this.instance.changeNextButtonLabel(constants.nextText);
return true;
}
@@ -139,23 +130,23 @@ export class ModifyColumnsPage extends ImportPage {
this.table.updateProperties({
height: 400,
columns: [{
displayName: localize('flatFileImport.columnName', "Column Name"),
displayName: constants.columnNameText,
valueType: azdata.DeclarativeDataType.string,
width: '150px',
isReadOnly: false
}, {
displayName: localize('flatFileImport.dataType', "Data Type"),
displayName: constants.dataTypeText,
valueType: azdata.DeclarativeDataType.editableCategory,
width: '150px',
isReadOnly: false,
categoryValues: this.categoryValues
}, {
displayName: localize('flatFileImport.primaryKey', "Primary Key"),
displayName: constants.primaryKeyText,
valueType: azdata.DeclarativeDataType.boolean,
width: '100px',
isReadOnly: false
}, {
displayName: localize('flatFileImport.allowNulls', "Allow Nulls"),
displayName: constants.allowNullsText,
valueType: azdata.DeclarativeDataType.boolean,
isReadOnly: false,
width: '100px'

View File

@@ -4,19 +4,11 @@
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as nls from 'vscode-nls';
import { ImportDataModel } from '../api/models';
import { ImportPage } from '../api/importPage';
import { FlatFileProvider } from '../../services/contracts';
import { FlatFileWizard } from '../flatFileWizard';
const localize = nls.loadMessageBundle();
import * as constants from '../../common/constants';
export class ProsePreviewPage extends ImportPage {
private readonly successTitle: string = localize('flatFileImport.prosePreviewMessage', "This operation analyzed the input file structure to generate the preview below for up to the first 50 rows.");
private readonly failureTitle: string = localize('flatFileImport.prosePreviewMessageFail', "This operation was unsuccessful. Please try a different input file.");
private table: azdata.TableComponent;
private loading: azdata.LoadingComponent;
private form: azdata.FormContainer;
@@ -24,10 +16,6 @@ export class ProsePreviewPage extends ImportPage {
private resultTextComponent: azdata.TextComponent;
private isSuccess: boolean;
public constructor(instance: FlatFileWizard, wizardPage: azdata.window.WizardPage, model: ImportDataModel, view: azdata.ModelView, provider: FlatFileProvider) {
super(instance, wizardPage, model, view, provider);
}
async start(): Promise<boolean> {
this.table = this.view.modelBuilder.table().withProperties<azdata.TableComponentProperties>({
data: undefined,
@@ -35,7 +23,7 @@ export class ProsePreviewPage extends ImportPage {
forceFitColumns: azdata.ColumnSizingMode.DataFit
}).component();
this.refresh = this.view.modelBuilder.button().withProperties({
label: localize('flatFileImport.refresh', "Refresh"),
label: constants.refreshText,
isFile: false
}).component();
@@ -47,7 +35,7 @@ export class ProsePreviewPage extends ImportPage {
this.resultTextComponent = this.view.modelBuilder.text()
.withProperties({
value: this.isSuccess ? this.successTitle : this.failureTitle
value: this.isSuccess ? constants.successTitleText : constants.failureTitleText
}).component();
this.form = this.view.modelBuilder.formContainer().withFormItems([
@@ -84,14 +72,14 @@ export class ProsePreviewPage extends ImportPage {
await this.populateTable(this.model.proseDataPreview, this.model.proseColumns.map(c => c.columnName));
this.isSuccess = true;
if (this.form) {
this.resultTextComponent.value = this.successTitle;
this.resultTextComponent.value = constants.successTitleText;
}
return true;
} else {
await this.populateTable([], []);
this.isSuccess = false;
if (this.form) {
this.resultTextComponent.value = this.failureTitle + '\n' + (error ?? '');
this.resultTextComponent.value = constants.failureTitleText + '\n' + (error ?? '');
}
return false;
}

View File

@@ -4,15 +4,10 @@
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as nls from 'vscode-nls';
import { ImportDataModel } from '../api/models';
import { ImportPage } from '../api/importPage';
import { FlatFileProvider, InsertDataResponse } from '../../services/contracts';
import { FlatFileWizard } from '../flatFileWizard';
const localize = nls.loadMessageBundle();
import { InsertDataResponse } from '../../services/contracts';
import * as constants from '../../common/constants';
export class SummaryPage extends ImportPage {
private table: azdata.TableComponent;
@@ -20,10 +15,6 @@ export class SummaryPage extends ImportPage {
private loading: azdata.LoadingComponent;
private form: azdata.FormContainer;
public constructor(instance: FlatFileWizard, wizardPage: azdata.window.WizardPage, model: ImportDataModel, view: azdata.ModelView, provider: FlatFileProvider) {
super(instance, wizardPage, model, view, provider);
}
async start(): Promise<boolean> {
this.table = this.view.modelBuilder.table().component();
this.statusText = this.view.modelBuilder.text().component();
@@ -33,11 +24,11 @@ export class SummaryPage extends ImportPage {
[
{
component: this.table,
title: localize('flatFileImport.importInformation', "Import information")
title: constants.importInformationText
},
{
component: this.loading,
title: localize('flatFileImport.importStatus', "Import status")
title: constants.importStatusText
}
]
).component();
@@ -70,11 +61,11 @@ export class SummaryPage extends ImportPage {
private populateTable() {
this.table.updateProperties({
data: [
[localize('flatFileImport.serverName', "Server name"), this.model.server.providerName],
[localize('flatFileImport.databaseName', "Database name"), this.model.database],
[localize('flatFileImport.tableName', "Table name"), this.model.table],
[localize('flatFileImport.tableSchema', "Table schema"), this.model.schema],
[localize('flatFileImport.fileImport', "File to be imported"), this.model.filePath]],
[constants.serverNameText, this.model.server.providerName],
[constants.databaseText, this.model.database],
[constants.tableNameText, this.model.table],
[constants.tableSchemaText, this.model.schema],
[constants.fileImportText, this.model.filePath]],
columns: ['Object type', 'Name'],
width: 600,
height: 200
@@ -96,9 +87,11 @@ export class SummaryPage extends ImportPage {
let result: InsertDataResponse;
let err;
let includePasswordInConnectionString = (this.model.server.options.connectionId === 'Integrated') ? false : true;
try {
result = await this.provider.sendInsertDataRequest({
connectionString: await this.getConnectionString(),
connectionString: await this._apiWrapper.getConnectionString(this.model.server.connectionId, includePasswordInConnectionString),
//TODO check what SSMS uses as batch size
batchSize: 500
});
@@ -118,7 +111,7 @@ export class SummaryPage extends ImportPage {
// TODO: When sql statements are in, implement this.
//let rows = await this.getCountRowsInserted();
//if (rows < 0) {
updateText = localize('flatFileImport.success.norows', "✔ You have successfully inserted the data into a table.");
updateText = constants.updateText;
//} else {
//updateText = localize('flatFileImport.success.rows', '✔ You have successfully inserted {0} rows.', rows);
//}
@@ -129,25 +122,6 @@ export class SummaryPage extends ImportPage {
return true;
}
/**
* Gets the connection string to send to the middleware
*/
private async getConnectionString(): Promise<string> {
let options = this.model.server.options;
let connectionString: string;
if (options.authenticationType === 'Integrated') {
connectionString = `Data Source=${options.server + (options.port ? `,${options.port}` : '')};Initial Catalog=${this.model.database};Integrated Security=True`;
} else {
let credentials = await azdata.connection.getCredentials(this.model.server.connectionId);
connectionString = `Data Source=${options.server + (options.port ? `,${options.port}` : '')};Initial Catalog=${this.model.database};Integrated Security=False;User Id=${options.user};Password=${credentials.password}`;
}
// TODO: Fix this, it's returning undefined string.
//await azdata.connection.getConnectionString(this.model.server.connectionId, true);
return connectionString;
}
// private async getCountRowsInserted(): Promise<Number> {
// let connectionUri = await azdata.connection.getUriForConnection(this.model.server.connectionId);
// let queryProvider = azdata.dataprotocol.getProvider<azdata.QueryProvider>(this.model.server.providerName, azdata.DataProviderType.QueryProvider);