Alanren/integration test (#3657)

* add an extension for integration tests

* setup ads before running test

* test setup

* test cases

* bash script

* shorter temp folder name

* code cleanup

* add commented out original code

* fix test error

* test result path

* rename results file

* change file path

* report smoke test results

* test stablize

* test stablization and configurable test servers

* fix smoke test error

* connection provider

* simplify the integration test script

* add comment

* fix tslint error

* address PR comments

* add temp log to check whether the environment variable is already set

* remove temp log

* move api definition to testapi typing file

* exclude integration tests extension

* address comments
This commit is contained in:
Alan Ren
2019-01-18 17:00:30 -08:00
committed by GitHub
parent 3e7a09c1e3
commit eb67b299de
47 changed files with 5668 additions and 107 deletions

10
.vscode/launch.json vendored
View File

@@ -152,6 +152,16 @@
"args": [
"--extensionDevelopmentPath=${workspaceRoot}/extensions/debug-auto-launch"
]
},
{
"type": "node",
"request": "launch",
"name": "Launch Smoke Test",
"program": "${workspaceFolder}/test/smoke/test/index.js",
"cwd": "${workspaceFolder}/test/smoke",
"env": {
"BUILD_ARTIFACTSTAGINGDIRECTORY": "${workspaceFolder}"
}
}
],
"compounds": [

View File

@@ -65,6 +65,8 @@ const excludedExtensions = [
'vscode-colorize-tests',
'ms-vscode.node-debug',
'ms-vscode.node-debug2',
// {{SQL CARBON EDIT}}
'integration-tests',
];
// {{SQL CARBON EDIT}}

View File

@@ -0,0 +1,17 @@
// A launch configuration that compiles the extension and then opens it inside a new window
{
"version": "0.1.0",
"configurations": [
{
"name": "Launch Tests",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": ["${workspaceFolder}/../../", "${workspaceFolder}/test", "--extensionDevelopmentPath=${workspaceFolder}", "--extensionTestsPath=${workspaceFolder}/out" ],
"stopOnEntry": false,
"sourceMaps": true,
"outDir": "${workspaceFolder}/out",
"preLaunchTask": "npm"
}
]
}

View File

@@ -0,0 +1,31 @@
// Available variables which can be used inside of strings.
// ${workspaceFolder}: the root folder of the team
// ${file}: the current opened file
// ${relativeFile}: the current opened file relative to cwd
// ${fileBasename}: the current opened file's basename
// ${fileDirname}: the current opened file's dirname
// ${fileExtname}: the current opened file's extension
// ${cwd}: the current working directory of the spawned process
// A task runner that calls a custom npm script that compiles the extension.
{
"version": "0.1.0",
// we want to run npm
"command": "npm",
// the command is a shell script
"isShellCommand": true,
// show the output window only if unrecognized errors occur.
"showOutput": "silent",
// we run the custom script "compile" as defined in package.json
"args": ["run", "compile", "--loglevel", "silent"],
// The tsc compiler is started in watching mode
"isWatching": true,
// use the standard tsc in watch mode problem matcher to find compile problems in the output.
"problemMatcher": "$tsc-watch"
}

View File

@@ -0,0 +1 @@
copy the extension installers to this folder

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,58 @@
{
"name": "integration-tests",
"description": "Integration Tests",
"version": "0.0.1",
"publisher": "Microsoft",
"private": true,
"engines": {
"vscode": "*",
"sqlops": "*"
},
"activationEvents": [
"*"
],
"main": "./out/main",
"extensionDependencies": [
"Microsoft.agent",
"Microsoft.import",
"Microsoft.profiler",
"Microsoft.mssql",
"Microsoft.notebook"
],
"contributes": {
"configuration": {
"type": "object",
"title": "ADS Integration Test Configuration",
"properties": {
"test.testSetupCompleted": {
"type": "boolean",
"default": false,
"description": ""
}
}
},
"commands": [{
"command": "test.setupIntegrationTest",
"title": "Setup Integration Test",
"category": "Test"
},
{
"command": "test.waitForExtensionsToLoad",
"title": "Wait For Extensions To Load",
"category": "Test"
}
]
},
"scripts": {
"vscode:prepublish": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:vscode-colorize-tests ./tsconfig.json",
"postinstall": "node ./node_modules/vscode/bin/install"
},
"devDependencies": {
"@types/node": "7.0.43",
"@types/chai": "3.4.34",
"mocha-junit-reporter": "^1.17.0",
"mocha-multi-reporters": "^1.1.7",
"vscode": "1.1.5",
"chai": "3.5.0"
}
}

View File

@@ -0,0 +1,36 @@
/*---------------------------------------------------------------------------------------------
* 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 { context } from './testContext';
const path = require('path');
const testRunner = require('vscode/lib/testrunner');
const suite = 'Integration Tests';
const options: any = {
ui: 'tdd',
useColors: true,
timeout: 600000
};
if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) {
options.reporter = 'mocha-multi-reporters';
options.reporterOptions = {
reporterEnabled: 'spec, mocha-junit-reporter',
mochaJunitReporterReporterOptions: {
testsuitesTitle: `${suite} ${process.platform}`,
mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`)
}
};
}
if (!vscode.workspace.getConfiguration('test')['testSetupCompleted']) {
context.RunTest = false;
}
testRunner.configure(options);
export = testRunner;

View File

@@ -0,0 +1,77 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as vscode from 'vscode';
import * as sqlops from 'sqlops';
import { normalize, join } from 'path';
import * as fs from 'fs';
const TEST_SETUP_COMPLETED_TEXT: string = 'Test Setup Completed';
const EXTENSION_LOADED_TEXT: string = 'Test Extension Loaded';
const ALL_EXTENSION_LOADED_TEXT: string = 'All Extensions Loaded';
var statusBarItemTimer: NodeJS.Timer;
export function activate(context: vscode.ExtensionContext) {
var statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
vscode.commands.registerCommand('test.setupIntegrationTest', async () => {
let extensionInstallersFolder = normalize(join(__dirname, '../extensionInstallers'));
let installers = fs.readdirSync(extensionInstallersFolder);
for (let i = 0; i < installers.length; i++) {
if (installers[i].endsWith('.vsix')) {
let installerFullPath = join(extensionInstallersFolder, installers[i]);
await sqlops.extensions.install(installerFullPath);
}
}
await setConfiguration('workbench.enablePreviewFeatures', true);
await setConfiguration('workbench.showConnectDialogOnStartup', false);
await setConfiguration('test.testSetupCompleted', true);
showStatusBarItem(statusBarItem, TEST_SETUP_COMPLETED_TEXT);
});
vscode.commands.registerCommand('test.waitForExtensionsToLoad', async () => {
let expectedExtensions = ['Microsoft.agent', 'Microsoft.import', 'Microsoft.mssql', 'Microsoft.profiler'];
do {
let extensions = vscode.extensions.all.filter(ext => { return expectedExtensions.indexOf(ext.id) !== -1; });
let isReady = true;
for (let i = 0; i < extensions.length; i++) {
let extension = extensions[i];
isReady = isReady && extension.isActive;
if (!isReady) {
break;
}
}
if (isReady) {
showStatusBarItem(statusBarItem, ALL_EXTENSION_LOADED_TEXT);
break;
} else {
await new Promise(resolve => { setTimeout(resolve, 1000); });
}
}
while (true);
});
showStatusBarItem(statusBarItem, EXTENSION_LOADED_TEXT);
}
function showStatusBarItem(statusBarItem: vscode.StatusBarItem, text: string) {
statusBarItem.text = text;
statusBarItem.tooltip = text;
statusBarItem.show();
clearTimeout(statusBarItemTimer);
statusBarItemTimer = setTimeout(function () {
statusBarItem.hide();
}, 5000);
}
// this method is called when your extension is deactivated
export function deactivate(): void {
}
async function setConfiguration(name: string, value: any) {
await vscode.workspace.getConfiguration().update(name, value, true);
}

View File

@@ -0,0 +1,29 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'mocha';
import * as sqlops from 'sqlops';
import { context } from './testContext';
import { getDefaultTestingServer } from './testConfig';
import { connectToServer } from './utils';
import assert = require('assert');
if (context.RunTest) {
suite('Object Explorer integration test suite', () => {
test('context menu test', async function () {
await connectToServer(await getDefaultTestingServer());
let nodes = <sqlops.objectexplorer.ObjectExplorerNode[]>await sqlops.objectexplorer.getActiveConnectionNodes();
assert(nodes.length === 1, `expecting 1 active connection, actual: ${nodes.length}`);
let actions = await sqlops.objectexplorer.getNodeActions(nodes[0].connectionId, nodes[0].nodePath);
const expectedActions = ['Manage', 'New Query', 'Disconnect', 'Delete Connection', 'Refresh', 'Launch Profiler'];
const expectedString = expectedActions.join(',');
const actualString = actions.join(',');
assert(expectedActions.length === actions.length && expectedString === actualString, `Expected actions: "${expectedString}", Actual actions: "${actualString}"`);
});
});
}

View File

@@ -0,0 +1,21 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'mocha';
import * as vscode from 'vscode';
import { context } from './testContext';
if (!context.RunTest) {
suite('integration test setup', () => {
test('test setup', async function () {
//Prepare the environment and make it ready for testing
await vscode.commands.executeCommand('test.setupIntegrationTest');
//Reload the window, this is required for some changes made by the 'test.setupIntegrationTest' to work
await vscode.commands.executeCommand('workbench.action.reloadWindow');
});
});
}

View File

@@ -0,0 +1,92 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/*
TODO: Due to a runtime error, I duplicated this file at these 2 locations:
$/extensions/integration-test/src/testConfig.ts
$/test/smoke/src/sql/testConfig.ts
for now, make sure to keep both files in sync.
*/
interface ITestServerProfile {
serverName: string;
userName: string;
password: string;
authenticationType: AuthenticationType;
database: string;
provider: ConnectionProvider;
version: string;
}
interface INameDisplayNamePair {
name: string;
displayName: string;
}
export enum AuthenticationType {
Windows,
SqlLogin
}
export enum ConnectionProvider {
SQLServer
}
var connectionProviderMapping = {};
var authenticationTypeMapping = {};
connectionProviderMapping[ConnectionProvider.SQLServer] = { name: 'MSSQL', displayName: 'Microsoft SQL Server' };
authenticationTypeMapping[AuthenticationType.SqlLogin] = { name: 'SqlLogin', displayName: 'SQL Login' };
authenticationTypeMapping[AuthenticationType.Windows] = { name: 'Integrated', displayName: 'Windows Authentication' };
export class TestServerProfile {
constructor(private _profile: ITestServerProfile) { }
public get serverName(): string { return this._profile.serverName; }
public get userName(): string { return this._profile.userName; }
public get password(): string { return this._profile.password; }
public get database(): string { return this._profile.database; }
public get version(): string { return this._profile.version; }
public get provider(): ConnectionProvider { return this._profile.provider; }
public get providerName(): string { return getEnumMappingEntry(connectionProviderMapping, this.provider).name; }
public get providerDisplayName(): string { return getEnumMappingEntry(connectionProviderMapping, this.provider).displayName; }
public get authenticationType(): AuthenticationType { return this._profile.authenticationType; }
public get authenticationTypeName(): string { return getEnumMappingEntry(authenticationTypeMapping, this.authenticationType).name; }
public get authenticationTypeDisplayName(): string { return getEnumMappingEntry(authenticationTypeMapping, this.authenticationType).displayName; }
}
var TestingServers: TestServerProfile[] = [
new TestServerProfile(
{
serverName: 'SQLTOOLS2017-3',
userName: '',
password: '',
authenticationType: AuthenticationType.Windows,
database: 'master',
provider: ConnectionProvider.SQLServer,
version: '2017'
})
];
function getEnumMappingEntry(mapping: any, enumValue: any): INameDisplayNamePair {
let entry = mapping[enumValue];
if (entry) {
return entry;
} else {
throw `Unknown enum type: ${enumValue.toString()}`;
}
}
export async function getDefaultTestingServer(): Promise<TestServerProfile> {
let servers = await getTestingServers();
return servers[0];
}
export async function getTestingServers(): Promise<TestServerProfile[]> {
let promise = new Promise<TestServerProfile[]>(resolve => {
resolve(TestingServers);
});
await promise;
return promise;
}

View File

@@ -0,0 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export var context = {
RunTest: true
};

View File

@@ -0,0 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/// <reference path='../../../../src/sql/sqlops.d.ts'/>
/// <reference path='../../../../src/sql/sqlops.proposed.d.ts'/>
/// <reference path='../../../../src/sql/sqlops.test.d.ts'/>
/// <reference types='@types/node'/>

View File

@@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import assert = require('assert');
import * as sqlops from 'sqlops';
import * as vscode from 'vscode';
import { TestServerProfile } from './testConfig';
export async function connectToServer(server: TestServerProfile) {
let connectionProfile: sqlops.IConnectionProfile = {
serverName: server.serverName,
databaseName: server.database,
authenticationType: server.authenticationTypeName,
providerName: server.providerName,
connectionName: '',
userName: server.userName,
password: server.password,
savePassword: false,
groupFullName: undefined,
saveProfile: true,
id: undefined,
groupId: undefined,
options: {}
};
await ensureConnectionViewOpened();
let result = <sqlops.ConnectionResult>await sqlops.connection.connect(connectionProfile);
assert(result.connected, `Failed to connect to "${connectionProfile.serverName}", error code: ${result.errorCode}, error message: ${result.errorMessage}`);
//workaround
//wait for OE to load
await new Promise(c => setTimeout(c, 3000));
}
export async function ensureConnectionViewOpened() {
await vscode.commands.executeCommand('workbench.view.connections');
}

View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "ES5",
"outDir": "out",
"noUnusedLocals": true,
"lib": [
"es2015"
],
"sourceMap": true
},
"include": [
"src/**/*"
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,20 @@
setlocal
pushd %~dp0\..
set VSCODEUSERDATADIR=%TMP%\adsuser-%RANDOM%-%TIME:~6,5%
set VSCODEEXTENSIONSDIR=%TMP%\adsext-%RANDOM%-%TIME:~6,5%
echo %VSCODEUSERDATADIR%
echo %VSCODEEXTENSIONSDIR%
@echo OFF
call .\scripts\code.bat --extensionDevelopmentPath=%~dp0\..\extensions\integration-tests --extensionTestsPath=%~dp0\..\extensions\integration-tests\out --user-data-dir=%VSCODEUSERDATADIR% --extensions-dir=%VSCODEEXTENSIONSDIR%
if %errorlevel% neq 0 exit /b %errorlevel%
rmdir /s /q %VSCODEUSERDATADIR%
rmdir /s /q %VSCODEEXTENSIONSDIR%
popd
endlocal

View File

@@ -0,0 +1,23 @@
#!/bin/bash
set -e
if [[ "$OSTYPE" == "darwin"* ]]; then
realpath() { [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"; }
ROOT=$(dirname $(dirname $(realpath "$0")))
VSCODEUSERDATADIR=`mktemp -d -t 'myuserdatadir'`
VSCODEEXTDIR=`mktemp -d -t 'myextdir'`
else
ROOT=$(dirname $(dirname $(readlink -f $0)))
VSCODEUSERDATADIR=`mktemp -d 2>/dev/null`
VSCODEEXTDIR=`mktemp -d 2>/dev/null`
fi
cd $ROOT
echo $VSCODEUSERDATADIR
echo $VSCODEEXTDIR
./scripts/code.sh --extensionDevelopmentPath=$ROOT/extensions/integration-tests --extensionTestsPath=$ROOT/extensions/integration-tests/out --user-data-dir=$VSCODEUSERDATADIR --extensions-dir=$VSCODEEXTDIR
rm -r $VSCODEUSERDATADIR
rm -r $VSCODEEXTDIR

View File

@@ -15,6 +15,7 @@ import { ConnectionProviderProperties, IConnectionProviderRegistry, Extensions a
import * as TaskUtilities from 'sql/workbench/common/taskUtilities';
import { IObjectExplorerService } from 'sql/parts/objectExplorer/common/objectExplorerService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { warn } from 'sql/base/common/log';
@@ -30,7 +31,8 @@ export class CommandLineService implements ICommandLineProcessing {
@IQueryEditorService private _queryEditorService: IQueryEditorService,
@IObjectExplorerService private _objectExplorerService: IObjectExplorerService,
@IEditorService private _editorService: IEditorService,
@ICommandService private _commandService: ICommandService
@ICommandService private _commandService: ICommandService,
@IWorkspaceConfigurationService private _configurationService: IWorkspaceConfigurationService
) {
let profile = null;
if (this._environmentService) {
@@ -76,8 +78,8 @@ export class CommandLineService implements ICommandLineProcessing {
let self = this;
return new Promise<void>((resolve, reject) => {
if (!self._commandName && !self._connectionProfile && !self._connectionManagementService.hasRegisteredServers()) {
let showConnectDialogOnStartup: boolean = this._configurationService.getValue('workbench.showConnectDialogOnStartup');
if (showConnectDialogOnStartup && !self._commandName && !self._connectionProfile && !self._connectionManagementService.hasRegisteredServers()) {
// prompt the user for a new connection on startup if no profiles are registered
self._connectionManagementService.showConnectionDialog()
.then(() => {

View File

@@ -77,6 +77,11 @@ export interface IObjectExplorerService {
getTreeNode(connectionId: string, nodePath: string): Thenable<TreeNode>;
refreshNodeInView(connectionId: string, nodePath: string): Thenable<TreeNode>;
/**
* For Testing purpose only. Get the context menu actions for an object explorer node.
*/
getNodeActions(connectionId: string, nodePath: string): Thenable<string[]>;
}
interface SessionStatus {
@@ -522,6 +527,17 @@ export class ObjectExplorerService implements IObjectExplorerService {
return Object.values(this._activeObjectExplorerNodes);
}
/**
* For Testing purpose only. Get the context menu actions for an object explorer node
*/
public getNodeActions(connectionId: string, nodePath: string): Thenable<string[]> {
return this.getTreeNode(connectionId, nodePath).then(node => {
return this._serverTreeView.treeActionProvider.getActions(this._serverTreeView.tree, this.getTreeItem(node)).then((actions) => {
return actions.filter(action => action.label).map(action => action.label);
});
});
}
public async refreshNodeInView(connectionId: string, nodePath: string): Promise<TreeNode> {
// Get the tree node and call refresh from the provider
let treeNode = await this.getTreeNode(connectionId, nodePath);

View File

@@ -31,6 +31,7 @@ import { Event, Emitter } from 'vs/base/common/event';
import { TreeNode, TreeItemCollapsibleState } from 'sql/parts/objectExplorer/common/treeNode';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { SERVER_GROUP_CONFIG, SERVER_GROUP_AUTOEXPAND_CONFIG } from 'sql/parts/objectExplorer/serverGroupDialog/serverGroup.contribution';
import { ServerTreeActionProvider } from 'sql/parts/objectExplorer/viewlet/serverTreeActionProvider';
const $ = builder.$;
@@ -46,7 +47,7 @@ export class ServerTreeView {
private _tree: ITree;
private _toDispose: IDisposable[] = [];
private _onSelectionOrFocusChange: Emitter<void>;
private _actionProvider: ServerTreeActionProvider;
constructor(
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
@IInstantiationService private _instantiationService: IInstantiationService,
@@ -63,6 +64,7 @@ export class ServerTreeView {
this);
this._treeSelectionHandler = this._instantiationService.createInstance(TreeSelectionHandler);
this._onSelectionOrFocusChange = new Emitter();
this._actionProvider = this._instantiationService.createInstance(ServerTreeActionProvider);
}
/**
@@ -79,6 +81,14 @@ export class ServerTreeView {
return this._onSelectionOrFocusChange.event;
}
public get treeActionProvider(): ServerTreeActionProvider {
return this._actionProvider;
}
public get tree(): ITree {
return this._tree;
}
/**
* Render the view body
*/
@@ -98,7 +108,6 @@ export class ServerTreeView {
this._connectionManagementService.showConnectionDialog();
}));
}
this._tree = TreeCreationUtils.createRegisteredServersTree(container, this._instantiationService);
//this._tree.setInput(undefined);
this._toDispose.push(this._tree.onDidChangeSelection((event) => this.onSelected(event)));

View File

@@ -133,7 +133,7 @@ declare module 'sqlops' {
* @param index index to insert the component to
* @param itemLayout Item Layout
*/
insertFormItem(formComponent: FormComponent | FormComponentGroup, index?: number, itemLayout?: FormItemLayout);
insertFormItem(formComponent: FormComponent | FormComponentGroup, index?: number, itemLayout?: FormItemLayout): void;
/**
* Removes a from item from the from
@@ -1209,7 +1209,7 @@ declare module 'sqlops' {
* Registers a save handler for this editor. This will be called if [supportsSave](#ModelViewEditorOptions.supportsSave)
* is set to true and the editor is marked as dirty
*/
registerSaveHandler(handler: () => Thenable<boolean>);
registerSaveHandler(handler: () => Thenable<boolean>): void;
}
}
@@ -1349,6 +1349,13 @@ declare module 'sqlops' {
}
export interface ConnectionResult {
connected: boolean;
connectionId: string;
errorMessage: string;
errorCode: number;
}
export namespace connection {
/**
* List the databases that can be accessed from the given connection
@@ -1371,6 +1378,12 @@ declare module 'sqlops' {
* @param callback
*/
export function openConnectionDialog(providers?: string[], initialConnectionProfile?: IConnectionProfile, connectionCompletionOptions?: IConnectionCompletionOptions): Thenable<connection.Connection>;
/**
* Opens the connection and add it to object explorer and opens the dashboard and returns the ConnectionResult
* @param connectionProfile connection profile
*/
export function connect(connectionProfile: IConnectionProfile): Thenable<ConnectionResult>;
}
export namespace nb {
@@ -2319,5 +2332,4 @@ declare module 'sqlops' {
//#endregion
}
}

23
src/sql/sqlops.test.d.ts vendored Normal file
View File

@@ -0,0 +1,23 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// This is the place for APIs used for testing
import * as core from 'sqlops';
import * as vscode from 'vscode';
declare module 'sqlops' {
export namespace extensions {
export function install(vsixPath: string): Thenable<string>;
}
export namespace objectexplorer {
/**
* get object explorer node context menu actions
*/
export function getNodeActions(connectionId: string, nodePath: string): Thenable<string[]>;
}
}

View File

@@ -47,4 +47,8 @@ export class ExtHostConnectionManagement extends ExtHostConnectionManagementShap
public $getUriForConnection(connectionId: string): Thenable<string> {
return this._proxy.$getUriForConnection(connectionId);
}
public $connect(connectionProfile: sqlops.IConnectionProfile): Thenable<sqlops.ConnectionResult> {
return this._proxy.$connect(connectionProfile);
}
}

View File

@@ -0,0 +1,23 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { IMainContext } from 'vs/workbench/api/node/extHost.protocol';
import { ExtHostExtensionManagementShape, MainThreadExtensionManagementShape, SqlMainContext } from 'sql/workbench/api/node/sqlExtHost.protocol';
export class ExtHostExtensionManagement implements ExtHostExtensionManagementShape {
private readonly _proxy: MainThreadExtensionManagementShape;
constructor(_mainContext: IMainContext) {
this._proxy = _mainContext.getProxy(SqlMainContext.MainThreadExtensionManagement);
}
$install(vsixPath: string): Thenable<string> {
return this._proxy.$install(vsixPath);
}
}

View File

@@ -105,7 +105,7 @@ class ModelViewEditorImpl extends ModelViewPanelImpl implements sqlops.workspace
this._proxy.$setDirty(this.handle, value);
}
registerSaveHandler(handler: () => Thenable<boolean>) {
registerSaveHandler(handler: () => Thenable<boolean>): void {
this._saveHandler = handler;
}

View File

@@ -30,6 +30,10 @@ export class ExtHostObjectExplorer implements ExtHostObjectExplorerShape {
public $findNodes(connectionId: string, type: string, schema: string, name: string, database: string, parentObjectNames: string[]): Thenable<sqlops.objectexplorer.ObjectExplorerNode[]> {
return this._proxy.$findNodes(connectionId, type, schema, name, database, parentObjectNames).then(results => results.map(result => new ExtHostObjectExplorerNode(result, connectionId, this._proxy)));
}
public $getNodeActions(connectionId: string, nodePath: string): Thenable<string[]> {
return this._proxy.$getNodeActions(connectionId, nodePath);
}
}
class ExtHostObjectExplorerNode implements sqlops.objectexplorer.ObjectExplorerNode {

View File

@@ -15,6 +15,9 @@ import * as TaskUtilities from 'sql/workbench/common/taskUtilities';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { isUndefinedOrNull } from 'vs/base/common/types';
import { generateUuid } from 'vs/base/common/uuid';
import { ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService';
import { ConnectionProfile } from 'sql/parts/connection/common/connectionProfile';
@extHostNamedCustomer(SqlMainContext.MainThreadConnectionManagement)
export class MainThreadConnectionManagement implements MainThreadConnectionManagementShape {
@@ -28,6 +31,7 @@ export class MainThreadConnectionManagement implements MainThreadConnectionManag
@IObjectExplorerService private _objectExplorerService: IObjectExplorerService,
@IEditorService private _workbenchEditorService: IEditorService,
@IConnectionDialogService private _connectionDialogService: IConnectionDialogService,
@ICapabilitiesService private _capabilitiesService: ICapabilitiesService
) {
if (extHostContext) {
this._proxy = extHostContext.getProxy(SqlExtHostContext.ExtHostConnectionManagement);
@@ -101,4 +105,23 @@ export class MainThreadConnectionManagement implements MainThreadConnectionManag
};
return connection;
}
public $connect(connectionProfile: IConnectionProfile): Thenable<sqlops.ConnectionResult> {
let profile = new ConnectionProfile(this._capabilitiesService, connectionProfile);
profile.id = generateUuid();
return this._connectionManagementService.connectAndSaveProfile(profile, undefined, {
saveTheConnection: true,
showDashboard: true,
params: undefined,
showConnectionDialogOnError: true,
showFirewallRuleOnError: true
}).then((result) => {
return <sqlops.ConnectionResult>{
connected: result.connected,
connectionId: result.connected ? profile.id : undefined,
errorCode: result.errorCode,
errorMessage: result.errorMessage
};
});
}
}

View File

@@ -0,0 +1,32 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { SqlMainContext, MainThreadExtensionManagementShape } from 'sql/workbench/api/node/sqlExtHost.protocol';
import { IExtHostContext } from 'vs/workbench/api/node/extHost.protocol';
import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
@extHostNamedCustomer(SqlMainContext.MainThreadExtensionManagement)
export class MainThreadExtensionManagement implements MainThreadExtensionManagementShape {
private _toDispose: IDisposable[];
constructor(
extHostContext: IExtHostContext,
@IExtensionManagementService private _extensionService: IExtensionManagementService
) {
this._toDispose = [];
}
public dispose(): void {
this._toDispose = dispose(this._toDispose);
}
public $install(vsixPath: string): Thenable<string> {
return this._extensionService.install(vsixPath).then((value: void) => { return undefined; }, (reason: any) => { return reason ? reason.toString() : undefined; });
}
}

View File

@@ -78,4 +78,8 @@ export class MainThreadObjectExplorer implements MainThreadObjectExplorerShape {
public $refresh(connectionId: string, nodePath: string): Thenable<sqlops.NodeInfo> {
return this._objectExplorerService.refreshNodeInView(connectionId, nodePath).then(node => node.toNodeInfo());
}
public $getNodeActions(connectionId: string, nodePath: string): Thenable<string[]> {
return this._objectExplorerService.getNodeActions(connectionId, nodePath);
}
}

View File

@@ -39,6 +39,7 @@ import { ExtHostQueryEditor } from 'sql/workbench/api/node/extHostQueryEditor';
import { ExtHostBackgroundTaskManagement } from './extHostBackgroundTaskManagement';
import { ExtHostNotebook } from 'sql/workbench/api/node/extHostNotebook';
import { ExtHostNotebookDocumentsAndEditors } from 'sql/workbench/api/node/extHostNotebookDocumentsAndEditors';
import { ExtHostExtensionManagement } from 'sql/workbench/api/node/extHostExtensionManagement';
export interface ISqlExtensionApiFactory {
vsCodeFactory(extension: IExtensionDescription): typeof vscode;
@@ -77,6 +78,7 @@ export function createApiFactory(
const extHostQueryEditor = rpcProtocol.set(SqlExtHostContext.ExtHostQueryEditor, new ExtHostQueryEditor(rpcProtocol));
const extHostNotebook = rpcProtocol.set(SqlExtHostContext.ExtHostNotebook, new ExtHostNotebook(rpcProtocol));
const extHostNotebookDocumentsAndEditors = rpcProtocol.set(SqlExtHostContext.ExtHostNotebookDocumentsAndEditors, new ExtHostNotebookDocumentsAndEditors(rpcProtocol));
const extHostExtensionManagement = rpcProtocol.set(SqlExtHostContext.ExtHostExtensionManagement, new ExtHostExtensionManagement(rpcProtocol));
return {
@@ -129,6 +131,9 @@ export function createApiFactory(
},
getUriForConnection(connectionId: string): Thenable<string> {
return extHostConnectionManagement.$getUriForConnection(connectionId);
},
connect(connectionProfile: sqlops.IConnectionProfile): Thenable<sqlops.ConnectionResult> {
return extHostConnectionManagement.$connect(connectionProfile);
}
};
@@ -152,6 +157,9 @@ export function createApiFactory(
},
findNodes(connectionId: string, type: string, schema: string, name: string, database: string, parentObjectNames: string[]): Thenable<sqlops.objectexplorer.ObjectExplorerNode[]> {
return extHostObjectExplorer.$findNodes(connectionId, type, schema, name, database, parentObjectNames);
},
getNodeActions(connectionId: string, nodePath: string): Thenable<string[]> {
return extHostObjectExplorer.$getNodeActions(connectionId, nodePath);
}
};
@@ -421,6 +429,12 @@ export function createApiFactory(
}
};
const extensions: typeof sqlops.extensions = {
install(vsixPath: string): Thenable<string> {
return extHostExtensionManagement.$install(vsixPath);
}
};
const nb = {
get notebookDocuments() {
return extHostNotebookDocumentsAndEditors.getAllDocuments().map(doc => doc.document);
@@ -483,7 +497,8 @@ export function createApiFactory(
SqlThemeIcon: sqlExtHostTypes.SqlThemeIcon,
TreeComponentItem: sqlExtHostTypes.TreeComponentItem,
nb: nb,
AzureResource: sqlExtHostTypes.AzureResource
AzureResource: sqlExtHostTypes.AzureResource,
extensions: extensions,
};
}
};

View File

@@ -26,6 +26,7 @@ import 'sql/workbench/api/node/mainThreadModelViewDialog';
import 'sql/workbench/api/node/mainThreadNotebook';
import 'sql/workbench/api/node/mainThreadNotebookDocumentsAndEditors';
import 'sql/workbench/api/node/mainThreadAccountManagement';
import 'sql/workbench/api/node/mainThreadExtensionManagement';
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
export class SqlExtHostContribution implements IWorkbenchContribution {

View File

@@ -546,6 +546,7 @@ export interface MainThreadConnectionManagementShape extends IDisposable {
$listDatabases(connectionId: string): Thenable<string[]>;
$getConnectionString(connectionId: string, includePassword: boolean): Thenable<string>;
$getUriForConnection(connectionId: string): Thenable<string>;
$connect(connectionProfile: sqlops.IConnectionProfile): Thenable<sqlops.ConnectionResult>;
}
export interface MainThreadCredentialManagementShape extends IDisposable {
@@ -580,8 +581,8 @@ export const SqlMainContext = {
MainThreadModelViewDialog: createMainId<MainThreadModelViewDialogShape>('MainThreadModelViewDialog'),
MainThreadQueryEditor: createMainId<MainThreadQueryEditorShape>('MainThreadQueryEditor'),
MainThreadNotebook: createMainId<MainThreadNotebookShape>('MainThreadNotebook'),
MainThreadNotebookDocumentsAndEditors: createMainId<MainThreadNotebookDocumentsAndEditorsShape>('MainThreadNotebookDocumentsAndEditors')
MainThreadNotebookDocumentsAndEditors: createMainId<MainThreadNotebookDocumentsAndEditorsShape>('MainThreadNotebookDocumentsAndEditors'),
MainThreadExtensionManagement: createMainId<MainThreadExtensionManagementShape>('MainThreadExtensionManagement')
};
export const SqlExtHostContext = {
@@ -602,7 +603,8 @@ export const SqlExtHostContext = {
ExtHostModelViewDialog: createExtId<ExtHostModelViewDialogShape>('ExtHostModelViewDialog'),
ExtHostQueryEditor: createExtId<ExtHostQueryEditorShape>('ExtHostQueryEditor'),
ExtHostNotebook: createExtId<ExtHostNotebookShape>('ExtHostNotebook'),
ExtHostNotebookDocumentsAndEditors: createExtId<ExtHostNotebookDocumentsAndEditorsShape>('ExtHostNotebookDocumentsAndEditors')
ExtHostNotebookDocumentsAndEditors: createExtId<ExtHostNotebookDocumentsAndEditorsShape>('ExtHostNotebookDocumentsAndEditors'),
ExtHostExtensionManagement: createExtId<ExtHostExtensionManagementShape>('ExtHostExtensionManagement')
};
export interface MainThreadDashboardShape extends IDisposable {
@@ -708,6 +710,7 @@ export interface MainThreadObjectExplorerShape extends IDisposable {
$isExpanded(connectionId: string, nodePath: string): Thenable<boolean>;
$findNodes(connectionId: string, type: string, schema: string, name: string, database: string, parentObjectNames: string[]): Thenable<sqlops.NodeInfo[]>;
$refresh(connectionId: string, nodePath: string): Thenable<sqlops.NodeInfo>;
$getNodeActions(connectionId: string, nodePath: string): Thenable<string[]>;
}
export interface ExtHostModelViewDialogShape {
@@ -838,3 +841,11 @@ export interface MainThreadNotebookDocumentsAndEditorsShape extends IDisposable
$tryShowNotebookDocument(resource: UriComponents, options: INotebookShowOptions): TPromise<string>;
$tryApplyEdits(id: string, modelVersionId: number, edits: ISingleNotebookEditOperation[], opts: IUndoStopOptions): TPromise<boolean>;
}
export interface ExtHostExtensionManagementShape {
$install(vsixPath: string): Thenable<string>;
}
export interface MainThreadExtensionManagementShape extends IDisposable {
$install(vsixPath: string): Thenable<string>;
}

View File

@@ -37,3 +37,16 @@ Registry.as<IConfigurationRegistry>(ConfigExtensions.Configuration).registerConf
}
}
});
Registry.as<IConfigurationRegistry>(ConfigExtensions.Configuration).registerConfiguration({
'id': 'showConnectDialogOnStartup',
'title': nls.localize('showConnectDialogOnStartup', 'Show connect dialog on startup'),
'type': 'object',
'properties': {
'workbench.showConnectDialogOnStartup': {
'type': 'boolean',
'default': true,
'description': nls.localize('showConnectDialogOnStartup', 'Show connect dialog on startup')
}
}
});

View File

@@ -32,6 +32,8 @@ import { ConnectionStore } from 'sql/parts/connection/common/connectionStore';
import { TestConnectionManagementService } from 'sqltest/stubs/connectionManagementService.test';
import { ICommandService, ICommandEvent, CommandsRegistry } from 'vs/platform/commands/common/commands';
import { TestCommandService } from 'vs/editor/test/browser/editorTestServices';
import { WorkspaceConfigurationTestService } from 'sqltest/stubs/workspaceConfigurationTestService';
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
class TestParsedArgs implements ParsedArgs {
[arg: string]: any;
@@ -112,11 +114,11 @@ suite('commandLineService tests', () => {
});
function getCommandLineService(connectionManagementService: IConnectionManagementService,
configurationService: IWorkspaceConfigurationService,
environmentService?: IEnvironmentService,
capabilitiesService?: ICapabilitiesService,
commandService?: ICommandService
) : CommandLineService
{
): CommandLineService {
let service = new CommandLineService(
connectionManagementService,
capabilitiesService,
@@ -124,11 +126,18 @@ suite('commandLineService tests', () => {
undefined,
undefined,
undefined,
commandService
commandService,
configurationService
);
return service;
}
function getConfigurationServiceMock(showConnectDialogOnStartup: boolean): TypeMoq.Mock<IWorkspaceConfigurationService> {
let configurationService = TypeMoq.Mock.ofType<IWorkspaceConfigurationService>(WorkspaceConfigurationTestService);
configurationService.setup((c) => c.getValue(TypeMoq.It.isAnyString())).returns((config: string) => showConnectDialogOnStartup);
return configurationService;
}
test('processCommandLine shows connection dialog by default', done => {
const connectionManagementService: TypeMoq.Mock<IConnectionManagementService>
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
@@ -140,13 +149,30 @@ suite('commandLineService tests', () => {
connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.returns(() => new Promise<string>((resolve, reject) => { resolve('unused'); }))
.verifiable(TypeMoq.Times.never());
let service = getCommandLineService(connectionManagementService.object);
const configurationService = getConfigurationServiceMock(true);
let service = getCommandLineService(connectionManagementService.object, configurationService.object);
service.processCommandLine().then(() => {
connectionManagementService.verifyAll();
done();
}, error => { assert.fail(error, null, 'processCommandLine rejected ' + error); done(); });
});
test('processCommandLine does nothing if no server name and command name is provided and the configuration \'workbench.showConnectDialogOnStartup\' is set to false, even if registered servers exist', done => {
const connectionManagementService: TypeMoq.Mock<IConnectionManagementService>
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
connectionManagementService.setup((c) => c.showConnectionDialog()).verifiable(TypeMoq.Times.never());
connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => false);
connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.verifiable(TypeMoq.Times.never());
const configurationService = getConfigurationServiceMock(false);
let service = getCommandLineService(connectionManagementService.object, configurationService.object);
service.processCommandLine();
connectionManagementService.verifyAll();
done();
});
test('processCommandLine does nothing if registered servers exist and no server name is provided', done => {
const connectionManagementService: TypeMoq.Mock<IConnectionManagementService>
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
@@ -156,7 +182,8 @@ suite('commandLineService tests', () => {
connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.returns(() => new Promise<string>((resolve, reject) => { resolve('unused'); }))
.verifiable(TypeMoq.Times.never());
let service = getCommandLineService(connectionManagementService.object);
const configurationService = getConfigurationServiceMock(true);
let service = getCommandLineService(connectionManagementService.object, configurationService.object);
service.processCommandLine().then(() => {
connectionManagementService.verifyAll();
done();
@@ -177,7 +204,8 @@ suite('commandLineService tests', () => {
connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.isAny(), 'connection', true))
.returns(() => new Promise<string>((resolve, reject) => { resolve('unused'); }))
.verifiable(TypeMoq.Times.once());
let service = getCommandLineService(connectionManagementService.object, environmentService.object, capabilitiesService);
const configurationService = getConfigurationServiceMock(true);
let service = getCommandLineService(connectionManagementService.object, configurationService.object, environmentService.object, capabilitiesService);
service.processCommandLine().then(() => {
environmentService.verifyAll();
connectionManagementService.verifyAll();
@@ -201,7 +229,8 @@ suite('commandLineService tests', () => {
commandService.setup(c => c.executeCommand('mycommand'))
.returns(() => TPromise.wrap(1))
.verifiable(TypeMoq.Times.once());
let service = getCommandLineService(connectionManagementService.object, environmentService.object, capabilitiesService, commandService.object);
const configurationService = getConfigurationServiceMock(true);
let service = getCommandLineService(connectionManagementService.object, configurationService.object, environmentService.object, capabilitiesService, commandService.object);
service.processCommandLine().then(() => {
connectionManagementService.verifyAll();
commandService.verifyAll();
@@ -229,7 +258,8 @@ suite('commandLineService tests', () => {
commandService.setup(c => c.executeCommand('mycommand', TypeMoq.It.is<ConnectionProfile>(p => p.serverName === 'myserver')))
.returns(() => TPromise.wrap(1))
.verifiable(TypeMoq.Times.once());
let service = getCommandLineService(connectionManagementService.object, environmentService.object, capabilitiesService, commandService.object);
const configurationService = getConfigurationServiceMock(true);
let service = getCommandLineService(connectionManagementService.object, configurationService.object, environmentService.object, capabilitiesService, commandService.object);
service.processCommandLine().then(() => {
connectionManagementService.verifyAll();
commandService.verifyAll();
@@ -250,7 +280,8 @@ suite('commandLineService tests', () => {
commandService.setup(c => c.executeCommand('mycommand'))
.returns(() => TPromise.wrapError(new Error('myerror')))
.verifiable(TypeMoq.Times.once());
let service = getCommandLineService(connectionManagementService.object, environmentService.object, capabilitiesService, commandService.object);
const configurationService = getConfigurationServiceMock(true);
let service = getCommandLineService(connectionManagementService.object, configurationService.object, environmentService.object, capabilitiesService, commandService.object);
service.processCommandLine().then(() => {
assert.fail(1, null, 'processCommandLine should reject when executeCommand errors out');
done();

View File

@@ -151,6 +151,9 @@ export class Button extends Disposable {
this.$el.addClass('monaco-text-button');
}
this.$el.text(value);
//{{SQL CARBON EDIT}}
this.$el.attr('aria-label', value);
//{{END}}
if (this.options.title) {
this.$el.title(value);
}

View File

@@ -62,8 +62,15 @@ export class Application {
async start(): Promise<any> {
await this._start();
//{{SQL CARBON EDIT}}
await this.code.waitForElement('.object-explorer-view');
//Original
/*
await this.code.waitForElement('.explorer-folders-view');
await this.code.waitForActiveElement(`.editor-instance[id="workbench.editor.walkThroughPart"] > div > div[tabIndex="0"]`);
*/
//{{END}}
}
async restart(options: { workspaceOrFolder?: string, extraArgs?: string[] }): Promise<any> {

View File

@@ -20,6 +20,11 @@ import { Editors } from '../editor/editors';
import { Code } from '../../vscode/code';
import { Terminal } from '../terminal/terminal';
// {{SQL CARBON EDIT}}
import { ConnectionDialog } from '../../sql/connectionDialog/connectionDialog';
import { Profiler } from '../../sql/profiler/profiler';
// {{END}}
export interface Commands {
runCommand(command: string): Promise<any>;
}
@@ -42,6 +47,11 @@ export class Workbench {
readonly keybindingsEditor: KeybindingsEditor;
readonly terminal: Terminal;
// {{SQL CARBON EDIT}}
readonly connectionDialog: ConnectionDialog;
readonly profiler: Profiler;
// {{END}}
constructor(code: Code, userDataPath: string) {
this.editors = new Editors(code);
this.quickopen = new QuickOpen(code, this.editors);
@@ -58,6 +68,10 @@ export class Workbench {
this.settingsEditor = new SettingsEditor(code, userDataPath, this.editors, this.editor, this.quickopen);
this.keybindingsEditor = new KeybindingsEditor(code);
this.terminal = new Terminal(code);
// {{SQL CARBON EDIT}}
this.connectionDialog = new ConnectionDialog(code);
this.profiler = new Profiler(code, this.quickopen);
// {{END}}
}
}

View File

@@ -12,7 +12,10 @@ import * as rimraf from 'rimraf';
import * as mkdirp from 'mkdirp';
import { ncp } from 'ncp';
import { Application, Quality } from './application';
//{{SQL CARBON EDIT}}
import { setup as runProfilerTests } from './sql/profiler/profiler.test';
//Original
/*
import { setup as setupDataMigrationTests } from './areas/workbench/data-migration.test';
import { setup as setupDataLossTests } from './areas/workbench/data-loss.test';
import { setup as setupDataExplorerTests } from './areas/explorer/explorer.test';
@@ -27,6 +30,8 @@ import { setup as setupDataExtensionTests } from './areas/extensions/extensions.
import { setup as setupTerminalTests } from './areas/terminal/terminal.test';
import { setup as setupDataMultirootTests } from './areas/multiroot/multiroot.test';
import { setup as setupDataLocalizationTests } from './areas/workbench/localization.test';
*/
//{{END}}
import { MultiLogger, Logger, ConsoleLogger, FileLogger } from './logger';
const tmpDir = tmp.dirSync({ prefix: 't' }) as { name: string; removeCallback: Function; };
@@ -250,14 +255,32 @@ after(async function () {
await new Promise((c, e) => rimraf(testDataPath, { maxBusyTries: 10 }, err => err ? e(err) : c()));
});
//{{SQL CARBON EDIT}}
/*
describe('Data Migration', () => {
setupDataMigrationTests(userDataDir, createApp);
});
*/
//{{END}}
describe('Test', () => {
describe('Smoke Test', () => {
before(async function () {
const app = createApp(quality);
await app!.start();
//{{SQL CARBON EDIT}}
const testExtLoadedText = 'Test Extension Loaded';
const testSetupCompletedText = 'Test Setup Completed';
const allExtensionsLoadedText = 'All Extensions Loaded';
const setupTestCommand = 'Test: Setup Integration Test';
const waitForExtensionsCommand = 'Test: Wait For Extensions To Load';
await app.workbench.statusbar.waitForStatusbarText(testExtLoadedText, testExtLoadedText);
await app.workbench.quickopen.runCommand(setupTestCommand);
await app.workbench.statusbar.waitForStatusbarText(testSetupCompletedText, testSetupCompletedText);
await app!.reload();
await app.workbench.statusbar.waitForStatusbarText(testExtLoadedText, testExtLoadedText);
await app.workbench.quickopen.runCommand(waitForExtensionsCommand);
await app.workbench.statusbar.waitForStatusbarText(allExtensionsLoadedText, allExtensionsLoadedText);
//{{END}}
this.app = app;
});
@@ -295,6 +318,10 @@ describe('Test', () => {
});
}
//{{SQL CARBON EDIT}}
runProfilerTests();
//Original
/*
setupDataLossTests();
setupDataExplorerTests();
setupDataPreferencesTests();
@@ -308,4 +335,6 @@ describe('Test', () => {
setupTerminalTests();
setupDataMultirootTests();
setupDataLocalizationTests();
*/
//{{END}}
});

View File

@@ -0,0 +1,51 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Code } from '../../vscode/code';
import { waitForNewDialog, clickDialogButton } from '../sqlutils';
import { TestServerProfile, AuthenticationType } from '../testConfig';
const CONNECTION_DIALOG_TITLE = 'Connection';
const CONNECTION_DIALOG_SELECTOR: string = '.modal-dialog .modal-content .modal-body .connection-dialog';
const CONNECTION_DETAIL_CONTROL_SELECTOR: string = '.connection-type .connection-table .connection-input';
const SERVER_INPUT_ARIA_LABEL = 'Server';
const USERNAME_INPUT_ARIA_LABEL = 'User name';
const PASSWORD_INPUT_ARIA_LABEL = 'Password';
const AUTH_TYPE_ARIA_LABEL = 'Authentication type';
const CONNECT_BUTTON_ARIA_LABEL = 'Connect';
export class ConnectionDialog {
constructor(private code: Code) { }
async waitForConnectionDialog(): Promise<void> {
await waitForNewDialog(this.code, CONNECTION_DIALOG_TITLE);
}
async connect(profile: TestServerProfile): Promise<void> {
await this.code.waitForSetValue(this.getInputCssSelector(SERVER_INPUT_ARIA_LABEL), profile.serverName);
if (profile.authenticationType === AuthenticationType.SqlLogin) {
await this.code.waitAndClick(this.getSelectCssSelector(AUTH_TYPE_ARIA_LABEL));
await this.selectAuthType(profile.authenticationTypeDisplayName);
await this.code.waitForSetValue(this.getInputCssSelector(USERNAME_INPUT_ARIA_LABEL), profile.userName);
await this.code.waitForSetValue(this.getInputCssSelector(PASSWORD_INPUT_ARIA_LABEL), profile.password);
}
await clickDialogButton(this.code, CONNECT_BUTTON_ARIA_LABEL);
}
private getInputCssSelector(ariaLabel: string): string {
return `${CONNECTION_DIALOG_SELECTOR} ${CONNECTION_DETAIL_CONTROL_SELECTOR} .monaco-inputbox input[aria-label="${ariaLabel}"]`;
}
private getSelectCssSelector(ariaLabel: string): string {
return `${CONNECTION_DIALOG_SELECTOR} ${CONNECTION_DETAIL_CONTROL_SELECTOR} select[aria-label="${ariaLabel}"]`;
}
private async selectAuthType(authType: string) {
await this.code.waitAndClick(`.context-view.bottom.left .monaco-select-box-dropdown-container .select-box-dropdown-list-container .monaco-list .monaco-scrollable-element .monaco-list-rows div[aria-label="${authType}"][class*="monaco-list-row"]`);
}
}

View File

@@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Application } from '../../application';
import { getDefaultTestingServer } from '../testConfig';
export function setup() {
describe('profiler test suite', () => {
it('Launch profiler test', async function () {
const app = this.app as Application;
await app.workbench.profiler.launchProfiler();
await app.workbench.connectionDialog.waitForConnectionDialog();
await app.workbench.connectionDialog.connect(await getDefaultTestingServer());
await app.workbench.profiler.waitForNewSessionDialogAndStart();
});
});
}

View File

@@ -0,0 +1,28 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Code } from '../../vscode/code';
import { QuickOpen } from '../../areas/quickopen/quickopen';
import { waitForNewDialog, clickDialogButton } from '../sqlutils';
const NEW_SESSION_DIALOG_TITLE: string = 'Start New Profiler Session';
export class Profiler {
constructor(private code: Code, private quickopen: QuickOpen) { }
async launchProfiler(): Promise<void> {
await this.quickopen.runCommand('Profiler: Launch Profiler');
}
async waitForNewSessionDialog() {
await waitForNewDialog(this.code, NEW_SESSION_DIALOG_TITLE);
}
async waitForNewSessionDialogAndStart() {
await this.waitForNewSessionDialog();
await clickDialogButton(this.code, 'Start');
}
}

View File

@@ -0,0 +1,9 @@
import { Code } from '../vscode/code';
export async function waitForNewDialog(code: Code, title: string) {
await code.waitForElement(`div[aria-label="${title}"][class="modal fade flyout-dialog"]`);
}
export async function clickDialogButton(code: Code, title: string) {
await code.waitAndClick(`.modal-dialog .modal-content .modal-footer .right-footer .footer-button a[aria-label="${title}"][aria-disabled="false"]`);
}

View File

@@ -0,0 +1,92 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/*
TODO: Due to a runtime error, I duplicated this file at these 2 locations:
$/extensions/integration-test/src/testConfig.ts
$/test/smoke/src/sql/testConfig.ts
for now, make sure to keep both files in sync.
*/
interface ITestServerProfile {
serverName: string;
userName: string;
password: string;
authenticationType: AuthenticationType;
database: string;
provider: ConnectionProvider;
version: string;
}
interface INameDisplayNamePair {
name: string;
displayName: string;
}
export enum AuthenticationType {
Windows,
SqlLogin
}
export enum ConnectionProvider {
SQLServer
}
var connectionProviderMapping = {};
var authenticationTypeMapping = {};
connectionProviderMapping[ConnectionProvider.SQLServer] = { name: 'MSSQL', displayName: 'Microsoft SQL Server' };
authenticationTypeMapping[AuthenticationType.SqlLogin] = { name: 'SqlLogin', displayName: 'SQL Login' };
authenticationTypeMapping[AuthenticationType.Windows] = { name: 'Integrated', displayName: 'Windows Authentication' };
export class TestServerProfile {
constructor(private _profile: ITestServerProfile) { }
public get serverName(): string { return this._profile.serverName; }
public get userName(): string { return this._profile.userName; }
public get password(): string { return this._profile.password; }
public get database(): string { return this._profile.database; }
public get version(): string { return this._profile.version; }
public get provider(): ConnectionProvider { return this._profile.provider; }
public get providerName(): string { return getEnumMappingEntry(connectionProviderMapping, this.provider).name; }
public get providerDisplayName(): string { return getEnumMappingEntry(connectionProviderMapping, this.provider).displayName; }
public get authenticationType(): AuthenticationType { return this._profile.authenticationType; }
public get authenticationTypeName(): string { return getEnumMappingEntry(authenticationTypeMapping, this.authenticationType).name; }
public get authenticationTypeDisplayName(): string { return getEnumMappingEntry(authenticationTypeMapping, this.authenticationType).displayName; }
}
var TestingServers: TestServerProfile[] = [
new TestServerProfile(
{
serverName: 'SQLTOOLS2017-3',
userName: '',
password: '',
authenticationType: AuthenticationType.Windows,
database: 'master',
provider: ConnectionProvider.SQLServer,
version: '2017'
})
];
function getEnumMappingEntry(mapping: any, enumValue: any): INameDisplayNamePair {
let entry = mapping[enumValue];
if (entry) {
return entry;
} else {
throw `Unknown enum type: ${enumValue.toString()}`;
}
}
export async function getDefaultTestingServer(): Promise<TestServerProfile> {
let servers = await getTestingServers();
return servers[0];
}
export async function getTestingServers(): Promise<TestServerProfile[]> {
let promise = new Promise<TestServerProfile[]>(resolve => {
resolve(TestingServers);
});
await promise;
return promise;
}

View File

@@ -217,7 +217,9 @@ export class Code {
}
async waitForWindowIds(fn: (windowIds: number[]) => boolean): Promise<void> {
await poll(() => this.driver.getWindowIds(), fn, `get window ids`);
// {{SQL CARBON EDIT}}
await poll(() => this.driver.getWindowIds(), fn, `get window ids`, 600, 100);
// {{END}}
}
async dispatchKeybinding(keybinding: string): Promise<void> {

View File

@@ -18,7 +18,9 @@ const opts = minimist(args, {
const options = {
useColors: true,
timeout: 60000,
//{{SQL CARBON EDIT}}
timeout: 60000 * 2,
//{{END}}
slow: 30000,
grep: opts['f']
};