Adding unit tests for schema compare service (#13642)

This commit is contained in:
Leila Lali
2020-12-07 14:42:38 -08:00
committed by GitHub
parent 099e94fa2d
commit 9977e83380
12 changed files with 703 additions and 319 deletions

View File

@@ -14,7 +14,6 @@ import { TelemetryReporter, TelemetryViews } from './telemetry';
import { getTelemetryErrorType, getEndpointName, verifyConnectionAndGetOwnerUri, getRootPath } from './utils';
import { SchemaCompareDialog } from './dialogs/schemaCompareDialog';
import { isNullOrUndefined } from 'util';
import { ApiWrapper } from './common/apiWrapper';
// Do not localize this, this is used to decide the icon for the editor.
// TODO : In future icon should be decided based on language id (scmp) and not resource name
@@ -69,7 +68,7 @@ export class SchemaCompareMainWindow {
public sourceEndpointInfo: mssql.SchemaCompareEndpointInfo;
public targetEndpointInfo: mssql.SchemaCompareEndpointInfo;
constructor(private apiWrapper: ApiWrapper, private schemaCompareService?: mssql.ISchemaCompareService, private extensionContext?: vscode.ExtensionContext) {
constructor(private schemaCompareService?: mssql.ISchemaCompareService, private extensionContext?: vscode.ExtensionContext) {
this.SchemaCompareActionMap = new Map<Number, string>();
this.SchemaCompareActionMap[mssql.SchemaUpdateAction.Delete] = loc.deleteAction;
this.SchemaCompareActionMap[mssql.SchemaUpdateAction.Change] = loc.changeAction;
@@ -87,7 +86,7 @@ export class SchemaCompareMainWindow {
let profile = context ? <azdata.IConnectionProfile>context.connectionProfile : undefined;
let sourceDacpac = context as string;
if (profile) {
let ownerUri = await this.apiWrapper.getUriForConnection((profile.id));
let ownerUri = await azdata.connection.getUriForConnection((profile.id));
this.sourceEndpointInfo = {
endpointType: mssql.SchemaCompareEndpointType.Database,
serverDisplayName: `${profile.serverName} ${profile.userName}`,
@@ -295,11 +294,11 @@ export class SchemaCompareMainWindow {
}
this.comparisonResult = await service.schemaCompare(this.operationId, this.sourceEndpointInfo, this.targetEndpointInfo, azdata.TaskExecutionMode.execute, this.deploymentOptions);
if (!this.comparisonResult || !this.comparisonResult.success) {
TelemetryReporter.createErrorEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaComparisonFailed', undefined, getTelemetryErrorType(this.comparisonResult.errorMessage))
TelemetryReporter.createErrorEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaComparisonFailed', undefined, getTelemetryErrorType(this.comparisonResult?.errorMessage))
.withAdditionalProperties({
operationId: this.comparisonResult.operationId
}).send();
this.apiWrapper.showErrorMessage(loc.compareErrorMessage(this.comparisonResult.errorMessage));
vscode.window.showErrorMessage(loc.compareErrorMessage(this.comparisonResult?.errorMessage));
return;
}
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaComparisonFinished')
@@ -407,69 +406,76 @@ export class SchemaCompareMainWindow {
this.tablelistenersToDispose.push(this.differencesTable.onCellAction(async (rowState) => {
let checkboxState = <azdata.ICheckboxCellActionEventArgs>rowState;
if (checkboxState) {
// show an info notification the first time when trying to exclude to notify the user that it may take some time to calculate affected dependencies
if (this.showIncludeExcludeWaitingMessage) {
this.showIncludeExcludeWaitingMessage = false;
vscode.window.showInformationMessage(loc.includeExcludeInfoMessage);
}
let diff = this.comparisonResult.differences[checkboxState.row];
const result = await service.schemaCompareIncludeExcludeNode(this.comparisonResult.operationId, diff, checkboxState.checked, azdata.TaskExecutionMode.execute);
let checkboxesToChange = [];
if (result.success) {
this.saveExcludeState(checkboxState);
// dependencies could have been included or excluded as a result, so save their exclude states
result.affectedDependencies.forEach(difference => {
// find the row of the difference and set its checkbox
const diffEntryKey = this.createDiffEntryKey(difference);
if (this.diffEntryRowMap.has(diffEntryKey)) {
const row = this.diffEntryRowMap.get(diffEntryKey);
checkboxesToChange.push({ row: row, column: 2, columnName: 'Include', checked: difference.included });
const dependencyCheckBoxState: azdata.ICheckboxCellActionEventArgs = {
checked: difference.included,
row: row,
column: 2,
columnName: undefined
};
this.saveExcludeState(dependencyCheckBoxState);
}
});
} else {
// failed because of dependencies
if (result.blockingDependencies) {
// show the first dependent that caused this to fail in the warning message
const diffEntryName = this.createName(diff.sourceValue ? diff.sourceValue : diff.targetValue);
const firstDependentName = this.createName(result.blockingDependencies[0].sourceValue ? result.blockingDependencies[0].sourceValue : result.blockingDependencies[0].targetValue);
let cannotExcludeMessage: string;
let cannotIncludeMessage: string;
if (firstDependentName) {
cannotExcludeMessage = loc.cannotExcludeMessageDependent(diffEntryName, firstDependentName);
cannotIncludeMessage = loc.cannotIncludeMessageDependent(diffEntryName, firstDependentName);
} else {
cannotExcludeMessage = loc.cannotExcludeMessage(diffEntryName);
cannotIncludeMessage = loc.cannotIncludeMessage(diffEntryName);
}
vscode.window.showWarningMessage(checkboxState.checked ? cannotIncludeMessage : cannotExcludeMessage);
} else {
vscode.window.showWarningMessage(result.errorMessage);
}
// set checkbox back to previous state
checkboxesToChange.push({ row: checkboxState.row, column: checkboxState.column, columnName: 'Include', checked: !checkboxState.checked });
}
if (checkboxesToChange.length > 0) {
this.differencesTable.updateCells = checkboxesToChange;
}
await this.applyIncludeExclude(checkboxState);
}
}));
}
public async applyIncludeExclude(checkboxState: azdata.ICheckboxCellActionEventArgs): Promise<void> {
const service = await this.getService();
// show an info notification the first time when trying to exclude to notify the user that it may take some time to calculate affected dependencies
if (this.showIncludeExcludeWaitingMessage) {
this.showIncludeExcludeWaitingMessage = false;
vscode.window.showInformationMessage(loc.includeExcludeInfoMessage);
}
let diff = this.comparisonResult.differences[checkboxState.row];
const result = await service.schemaCompareIncludeExcludeNode(this.comparisonResult.operationId, diff, checkboxState.checked, azdata.TaskExecutionMode.execute);
let checkboxesToChange = [];
if (result.success) {
this.saveExcludeState(checkboxState);
// dependencies could have been included or excluded as a result, so save their exclude states
result.affectedDependencies.forEach(difference => {
// find the row of the difference and set its checkbox
const diffEntryKey = this.createDiffEntryKey(difference);
if (this.diffEntryRowMap.has(diffEntryKey)) {
const row = this.diffEntryRowMap.get(diffEntryKey);
checkboxesToChange.push({ row: row, column: 2, columnName: 'Include', checked: difference.included });
const dependencyCheckBoxState: azdata.ICheckboxCellActionEventArgs = {
checked: difference.included,
row: row,
column: 2,
columnName: undefined
};
this.saveExcludeState(dependencyCheckBoxState);
}
});
} else {
// failed because of dependencies
if (result.blockingDependencies) {
// show the first dependent that caused this to fail in the warning message
const diffEntryName = this.createName(diff.sourceValue ? diff.sourceValue : diff.targetValue);
const firstDependentName = this.createName(result.blockingDependencies[0].sourceValue ? result.blockingDependencies[0].sourceValue : result.blockingDependencies[0].targetValue);
let cannotExcludeMessage: string;
let cannotIncludeMessage: string;
if (firstDependentName) {
cannotExcludeMessage = loc.cannotExcludeMessageDependent(diffEntryName, firstDependentName);
cannotIncludeMessage = loc.cannotIncludeMessageDependent(diffEntryName, firstDependentName);
} else {
cannotExcludeMessage = loc.cannotExcludeMessage(diffEntryName);
cannotIncludeMessage = loc.cannotIncludeMessage(diffEntryName);
}
vscode.window.showWarningMessage(checkboxState.checked ? cannotIncludeMessage : cannotExcludeMessage);
} else {
vscode.window.showWarningMessage(result.errorMessage);
}
// set checkbox back to previous state
checkboxesToChange.push({ row: checkboxState.row, column: checkboxState.column, columnName: 'Include', checked: !checkboxState.checked });
}
if (checkboxesToChange.length > 0) {
this.differencesTable.updateCells = checkboxesToChange;
}
}
// save state based on source name if present otherwise target name (parity with SSDT)
private saveExcludeState(rowState: azdata.ICheckboxCellActionEventArgs) {
if (rowState) {
this.differencesTable.data[rowState.row][2] = rowState.checked;
if (this.differencesTable.data[rowState.row]?.length > 2) {
this.differencesTable.data[rowState.row][2] = rowState.checked;
}
let diff = this.comparisonResult.differences[rowState.row];
let key = (diff.sourceValue && diff.sourceValue.length > 0) ? this.createName(diff.sourceValue) : this.createName(diff.targetValue);
if (key) {
@@ -647,7 +653,7 @@ export class SchemaCompareMainWindow {
});
}
private async cancelCompare() {
public async cancelCompare() {
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareCancelStarted')
.withAdditionalProperties({
@@ -691,28 +697,32 @@ export class SchemaCompareMainWindow {
}).component();
this.generateScriptButton.onDidClick(async (click) => {
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareGenerateScriptStarted')
.withAdditionalProperties({
'startTime': Date.now().toString(),
'operationId': this.comparisonResult.operationId
}).send();
const service = await this.getService();
const result = await service.schemaCompareGenerateScript(this.comparisonResult.operationId, this.targetEndpointInfo.serverName, this.targetEndpointInfo.databaseName, azdata.TaskExecutionMode.script);
if (!result || !result.success) {
TelemetryReporter.createErrorEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareGenerateScriptFailed', undefined, getTelemetryErrorType(result.errorMessage))
.withAdditionalProperties({
'operationId': this.comparisonResult.operationId
}).send();
vscode.window.showErrorMessage(loc.generateScriptErrorMessage(result.errorMessage));
}
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareGenerateScriptEnded')
.withAdditionalProperties({
'endTime': Date.now().toString(),
'operationId': this.comparisonResult.operationId
}).send();
await this.generateScript();
});
}
public async generateScript(): Promise<void> {
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareGenerateScriptStarted')
.withAdditionalProperties({
'startTime': Date.now().toString(),
'operationId': this.comparisonResult.operationId
}).send();
const service = await this.getService();
const result = await service.schemaCompareGenerateScript(this.comparisonResult.operationId, this.targetEndpointInfo.serverName, this.targetEndpointInfo.databaseName, azdata.TaskExecutionMode.script);
if (!result || !result.success) {
TelemetryReporter.createErrorEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareGenerateScriptFailed', undefined, getTelemetryErrorType(result.errorMessage))
.withAdditionalProperties({
'operationId': this.comparisonResult.operationId
}).send();
vscode.window.showErrorMessage(loc.generateScriptErrorMessage(result.errorMessage));
}
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareGenerateScriptEnded')
.withAdditionalProperties({
'endTime': Date.now().toString(),
'operationId': this.comparisonResult.operationId
}).send();
}
private createOptionsButton(view: azdata.ModelView) {
this.optionsButton = view.modelBuilder.button().withProperties({
label: loc.options,
@@ -741,43 +751,47 @@ export class SchemaCompareMainWindow {
},
}).component();
this.applyButton.onDidClick(async (click) => {
await this.publishChanges();
});
}
public async publishChanges(): Promise<void> {
// need only yes button - since the modal dialog has a default cancel
const yesString = loc.YesButtonText;
this.applyButton.onDidClick(async (click) => {
await vscode.window.showWarningMessage(loc.applyConfirmation, { modal: true }, yesString).then(async (result) => {
if (result === yesString) {
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareApplyStarted')
.withAdditionalProperties({
'startTime': Date.now().toString(),
'operationId': this.comparisonResult.operationId
}).send();
vscode.window.showWarningMessage(loc.applyConfirmation, { modal: true }, yesString).then(async (result) => {
if (result === yesString) {
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareApplyStarted')
// disable apply and generate script buttons because the results are no longer valid after applying the changes
this.setButtonsForRecompare();
const service = await this.getService();
const result = await service.schemaComparePublishChanges(this.comparisonResult.operationId, this.targetEndpointInfo.serverName, this.targetEndpointInfo.databaseName, azdata.TaskExecutionMode.execute);
if (!result || !result.success) {
TelemetryReporter.createErrorEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareApplyFailed', undefined, getTelemetryErrorType(result.errorMessage))
.withAdditionalProperties({
'startTime': Date.now().toString(),
'operationId': this.comparisonResult.operationId
}).send();
vscode.window.showErrorMessage(loc.applyErrorMessage(result.errorMessage));
// disable apply and generate script buttons because the results are no longer valid after applying the changes
this.setButtonsForRecompare();
const service = await this.getService();
const result = await service.schemaComparePublishChanges(this.comparisonResult.operationId, this.targetEndpointInfo.serverName, this.targetEndpointInfo.databaseName, azdata.TaskExecutionMode.execute);
if (!result || !result.success) {
TelemetryReporter.createErrorEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareApplyFailed', undefined, getTelemetryErrorType(result.errorMessage))
.withAdditionalProperties({
'operationId': this.comparisonResult.operationId
}).send();
vscode.window.showErrorMessage(loc.applyErrorMessage(result.errorMessage));
// reenable generate script and apply buttons if apply failed
this.generateScriptButton.enabled = true;
this.generateScriptButton.title = loc.generateScriptEnabledMessage;
this.applyButton.enabled = true;
this.applyButton.title = loc.applyEnabledMessage;
}
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareApplyEnded')
.withAdditionalProperties({
'endTime': Date.now().toString(),
'operationId': this.comparisonResult.operationId
}).send();
// reenable generate script and apply buttons if apply failed
this.generateScriptButton.enabled = true;
this.generateScriptButton.title = loc.generateScriptEnabledMessage;
this.applyButton.enabled = true;
this.applyButton.title = loc.applyEnabledMessage;
}
});
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareApplyEnded')
.withAdditionalProperties({
'endTime': Date.now().toString(),
'operationId': this.comparisonResult.operationId
}).send();
}
});
}
@@ -922,60 +936,64 @@ export class SchemaCompareMainWindow {
}).component();
this.openScmpButton.onDidClick(async (click) => {
TelemetryReporter.sendActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareOpenScmpStarted');
const rootPath = getRootPath();
let fileUris = await vscode.window.showOpenDialog(
{
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
defaultUri: vscode.Uri.file(rootPath),
openLabel: loc.open,
filters: {
'scmp Files': ['scmp'],
}
}
);
if (!fileUris || fileUris.length === 0) {
return;
}
let fileUri = fileUris[0];
const service = await this.getService();
let startTime = Date.now();
const result = await service.schemaCompareOpenScmp(fileUri.fsPath);
if (!result || !result.success) {
TelemetryReporter.sendErrorEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareOpenScmpFailed', undefined, getTelemetryErrorType(result.errorMessage));
vscode.window.showErrorMessage(loc.openScmpErrorMessage(result.errorMessage));
return;
}
this.sourceEndpointInfo = await this.constructEndpointInfo(result.sourceEndpointInfo, loc.sourceTitle, this.apiWrapper);
this.targetEndpointInfo = await this.constructEndpointInfo(result.targetEndpointInfo, loc.targetTitle, this.apiWrapper);
this.updateSourceAndTarget();
this.setDeploymentOptions(result.deploymentOptions);
this.scmpSourceExcludes = result.excludedSourceElements;
this.scmpTargetExcludes = result.excludedTargetElements;
this.sourceTargetSwitched = result.originalTargetName !== this.targetEndpointInfo.databaseName;
// clear out any old results
this.resetForNewCompare();
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareOpenScmpEnded')
.withAdditionalProperties({
elapsedTime: (Date.now() - startTime).toString()
}).send();
await this.openScmp();
});
}
private async constructEndpointInfo(endpoint: mssql.SchemaCompareEndpointInfo, caller: string, apiWrapper: ApiWrapper): Promise<mssql.SchemaCompareEndpointInfo> {
public async openScmp(): Promise<void> {
TelemetryReporter.sendActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareOpenScmpStarted');
const rootPath = getRootPath();
let fileUris = await vscode.window.showOpenDialog(
{
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
defaultUri: vscode.Uri.file(rootPath),
openLabel: loc.open,
filters: {
'scmp Files': ['scmp'],
}
}
);
if (!fileUris || fileUris.length === 0) {
return;
}
let fileUri = fileUris[0];
const service = await this.getService();
let startTime = Date.now();
const result = await service.schemaCompareOpenScmp(fileUri.fsPath);
if (!result || !result.success) {
TelemetryReporter.sendErrorEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareOpenScmpFailed', undefined, getTelemetryErrorType(result.errorMessage));
vscode.window.showErrorMessage(loc.openScmpErrorMessage(result.errorMessage));
return;
}
this.sourceEndpointInfo = await this.constructEndpointInfo(result.sourceEndpointInfo, loc.sourceTitle);
this.targetEndpointInfo = await this.constructEndpointInfo(result.targetEndpointInfo, loc.targetTitle);
this.updateSourceAndTarget();
this.setDeploymentOptions(result.deploymentOptions);
this.scmpSourceExcludes = result.excludedSourceElements;
this.scmpTargetExcludes = result.excludedTargetElements;
this.sourceTargetSwitched = result.originalTargetName !== this.targetEndpointInfo.databaseName;
// clear out any old results
this.resetForNewCompare();
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareOpenScmpEnded')
.withAdditionalProperties({
elapsedTime: (Date.now() - startTime).toString()
}).send();
}
private async constructEndpointInfo(endpoint: mssql.SchemaCompareEndpointInfo, caller: string): Promise<mssql.SchemaCompareEndpointInfo> {
let ownerUri;
let endpointInfo;
if (endpoint && endpoint.endpointType === mssql.SchemaCompareEndpointType.Database) {
// only set endpoint info if able to connect to the database
ownerUri = await verifyConnectionAndGetOwnerUri(endpoint, caller, apiWrapper);
ownerUri = await verifyConnectionAndGetOwnerUri(endpoint, caller);
}
if (ownerUri) {
endpointInfo = endpoint;
@@ -1007,44 +1025,48 @@ export class SchemaCompareMainWindow {
}).component();
this.saveScmpButton.onDidClick(async (click) => {
const rootPath = getRootPath();
const filePath = await vscode.window.showSaveDialog(
{
defaultUri: vscode.Uri.file(rootPath),
saveLabel: loc.save,
filters: {
'scmp Files': ['scmp'],
}
}
);
if (!filePath) {
return;
}
// convert include/exclude maps to arrays of object ids
let sourceExcludes: mssql.SchemaCompareObjectId[] = this.convertExcludesToObjectIds(this.originalSourceExcludes);
let targetExcludes: mssql.SchemaCompareObjectId[] = this.convertExcludesToObjectIds(this.originalTargetExcludes);
let startTime = Date.now();
TelemetryReporter.sendActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareSaveScmp');
const service = await this.getService();
const result = await service.schemaCompareSaveScmp(this.sourceEndpointInfo, this.targetEndpointInfo, azdata.TaskExecutionMode.execute, this.deploymentOptions, filePath.fsPath, sourceExcludes, targetExcludes);
if (!result || !result.success) {
TelemetryReporter.createErrorEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareSaveScmpFailed', undefined, getTelemetryErrorType(result.errorMessage))
.withAdditionalProperties({
operationId: this.comparisonResult.operationId
}).send();
vscode.window.showErrorMessage(loc.saveScmpErrorMessage(result.errorMessage));
}
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareSaveScmpEnded')
.withAdditionalProperties({
elapsedTime: (Date.now() - startTime).toString(),
operationId: this.comparisonResult.operationId
});
await this.saveScmp();
});
}
public async saveScmp(): Promise<void> {
const rootPath = getRootPath();
const filePath = await vscode.window.showSaveDialog(
{
defaultUri: vscode.Uri.file(rootPath),
saveLabel: loc.save,
filters: {
'scmp Files': ['scmp'],
}
}
);
if (!filePath) {
return;
}
// convert include/exclude maps to arrays of object ids
let sourceExcludes: mssql.SchemaCompareObjectId[] = this.convertExcludesToObjectIds(this.originalSourceExcludes);
let targetExcludes: mssql.SchemaCompareObjectId[] = this.convertExcludesToObjectIds(this.originalTargetExcludes);
let startTime = Date.now();
TelemetryReporter.sendActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareSaveScmp');
const service = await this.getService();
const result = await service.schemaCompareSaveScmp(this.sourceEndpointInfo, this.targetEndpointInfo, azdata.TaskExecutionMode.execute, this.deploymentOptions, filePath.fsPath, sourceExcludes, targetExcludes);
if (!result || !result.success) {
TelemetryReporter.createErrorEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareSaveScmpFailed', undefined, getTelemetryErrorType(result.errorMessage))
.withAdditionalProperties({
operationId: this.comparisonResult.operationId
}).send();
vscode.window.showErrorMessage(loc.saveScmpErrorMessage(result.errorMessage));
}
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareSaveScmpEnded')
.withAdditionalProperties({
elapsedTime: (Date.now() - startTime).toString(),
operationId: this.comparisonResult.operationId
});
}
/**
* Converts excluded diff entries into object ids which are needed to save them in an scmp
*/
@@ -1071,7 +1093,7 @@ export class SchemaCompareMainWindow {
}
private async getService(): Promise<mssql.ISchemaCompareService> {
if (isNullOrUndefined(this.schemaCompareService)) {
if (this.schemaCompareService === null || this.schemaCompareService === undefined) {
this.schemaCompareService = (vscode.extensions.getExtension(mssql.extension.name).exports as mssql.IExtension).schemaCompare;
}
return this.schemaCompareService;