Add tests for azdata extension (#11423)

* Add tests for azdata extension

* Fail on stderr

* Skip test for not implemented logic

* Move executeCommand stub

* Add missing packages
This commit is contained in:
Charles Gagnon
2020-07-21 14:13:58 -07:00
committed by GitHub
parent 238d643a8e
commit b57cae5b60
15 changed files with 1217 additions and 12 deletions

View File

@@ -46,7 +46,7 @@ jobs:
steps: steps:
- template: linux/sql-product-build-linux.yml - template: linux/sql-product-build-linux.yml
parameters: parameters:
extensionsToUnitTest: ["admin-tool-ext-win", "agent", "azurecore", "cms", "dacpac", "import", "schema-compare", "notebook", "resource-deployment", "machine-learning", "sql-database-projects"] extensionsToUnitTest: ["admin-tool-ext-win", "agent", "azdata", "azurecore", "cms", "dacpac", "import", "schema-compare", "notebook", "resource-deployment", "machine-learning", "sql-database-projects"]
timeoutInMinutes: 70 timeoutInMinutes: 70
- job: LinuxWeb - job: LinuxWeb

View File

@@ -0,0 +1,21 @@
{
"enabled": true,
"relativeSourcePath": "..",
"relativeCoverageDir": "../../coverage",
"ignorePatterns": [
"**/node_modules/**",
"**/test/**",
"localizedConstants.js",
"extension.js"
],
"reports": [
"cobertura",
"lcov",
"json"
],
"verbose": false,
"remapOptions": {
"basePath": "..",
"useAbsolutePaths": true
}
}

View File

@@ -26,9 +26,19 @@
"which": "^2.0.2" "which": "^2.0.2"
}, },
"devDependencies": { "devDependencies": {
"@types/mocha": "^5.2.5",
"@types/node": "^12.11.7", "@types/node": "^12.11.7",
"@types/request": "^2.48.5", "@types/request": "^2.48.5",
"@types/sinon": "^9.0.4",
"@types/uuid": "^8.0.0", "@types/uuid": "^8.0.0",
"@types/which": "^1.3.2" "@types/which": "^1.3.2",
"mocha": "^5.2.0",
"mocha-junit-reporter": "^1.17.0",
"mocha-multi-reporters": "^1.1.7",
"nock": "^13.0.2",
"should": "^13.2.3",
"sinon": "^9.0.2",
"typemoq": "^2.1.0",
"vscodetestcover": "^1.0.9"
} }
} }

View File

@@ -6,12 +6,14 @@
import * as os from 'os'; import * as os from 'os';
import * as path from 'path'; import * as path from 'path';
import * as uuid from 'uuid'; import * as uuid from 'uuid';
import * as which from 'which';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { HttpClient } from './common/httpClient'; import { HttpClient } from './common/httpClient';
import * as loc from './localizedConstants'; import * as loc from './localizedConstants';
import { executeCommand } from './common/childProcess'; import { executeCommand } from './common/childProcess';
import { searchForCmd } from './common/utils';
export const azdataHostname = 'https://aka.ms';
export const azdataUri = 'azdata-msi';
/** /**
* Information about an azdata installation * Information about an azdata installation
*/ */
@@ -55,7 +57,15 @@ export async function downloadAndInstallAzdata(outputChannel: vscode.OutputChann
const statusDisposable = vscode.window.setStatusBarMessage(loc.installingAzdata); const statusDisposable = vscode.window.setStatusBarMessage(loc.installingAzdata);
try { try {
switch (process.platform) { switch (process.platform) {
case 'win32': await downloadAndInstallAzdataWin32(outputChannel); case 'win32':
await downloadAndInstallAzdataWin32(outputChannel);
break;
case 'darwin':
await installAzdataDarwin();
break;
case 'linux':
await installAzdataLinux();
break;
} }
} finally { } finally {
statusDisposable.dispose(); statusDisposable.dispose();
@@ -69,17 +79,31 @@ export async function downloadAndInstallAzdata(outputChannel: vscode.OutputChann
async function downloadAndInstallAzdataWin32(outputChannel: vscode.OutputChannel): Promise<void> { async function downloadAndInstallAzdataWin32(outputChannel: vscode.OutputChannel): Promise<void> {
const downloadPath = path.join(os.tmpdir(), `azdata-msi-${uuid.v4()}.msi`); const downloadPath = path.join(os.tmpdir(), `azdata-msi-${uuid.v4()}.msi`);
outputChannel.appendLine(loc.downloadingTo('azdata-cli.msi', downloadPath)); outputChannel.appendLine(loc.downloadingTo('azdata-cli.msi', downloadPath));
await HttpClient.download('https://aka.ms/azdata-msi', downloadPath, outputChannel); await HttpClient.download(`${azdataHostname}/${azdataUri}`, downloadPath, outputChannel);
await executeCommand('msiexec', ['/i', downloadPath], outputChannel); await executeCommand('msiexec', ['/i', downloadPath], outputChannel);
} }
/**
* Runs commands to install azdata on MacOS
*/
async function installAzdataDarwin(): Promise<void> {
throw new Error('Not yet implemented');
}
/**
* Runs commands to install azdata on Linux
*/
async function installAzdataLinux(): Promise<void> {
throw new Error('Not yet implemented');
}
/** /**
* Finds azdata specifically on Windows * Finds azdata specifically on Windows
* @param outputChannel Channel used to display diagnostic information * @param outputChannel Channel used to display diagnostic information
*/ */
async function findAzdataWin32(outputChannel: vscode.OutputChannel): Promise<IAzdata> { async function findAzdataWin32(outputChannel: vscode.OutputChannel): Promise<IAzdata> {
const whichPromise = new Promise<string>((c, e) => which('azdata.cmd', (err, path) => err ? e(err) : c(path))); const promise = searchForCmd('azdata.cmd');
return findSpecificAzdata(await whichPromise, outputChannel); return findSpecificAzdata(await promise, outputChannel);
} }
/** /**

View File

@@ -25,10 +25,20 @@ export class ExitCodeError extends Error {
export async function executeCommand(command: string, args?: string[], outputChannel?: vscode.OutputChannel): Promise<string> { export async function executeCommand(command: string, args?: string[], outputChannel?: vscode.OutputChannel): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
outputChannel?.appendLine(loc.executingCommand(command, args ?? [])); outputChannel?.appendLine(loc.executingCommand(command, args ?? []));
const buffers: Buffer[] = []; const stdoutBuffers: Buffer[] = [];
const child = cp.spawn(command, args); const stderrBuffers: Buffer[] = [];
child.stdout.on('data', (b: Buffer) => buffers.push(b)); const child = cp.spawn(command, args, { shell: true });
child.stdout.on('data', (b: Buffer) => stdoutBuffers.push(b));
child.stderr.on('data', (b: Buffer) => stderrBuffers.push(b));
child.on('error', reject); child.on('error', reject);
child.on('exit', code => code ? reject(new ExitCodeError(code)) : resolve(Buffer.concat(buffers).toString('utf8').trim())); child.on('exit', code => {
if (stderrBuffers.length > 0) {
reject(new Error(Buffer.concat(stderrBuffers).toString('utf8').trim()));
} else if (code) {
reject(new ExitCodeError(code));
} else {
resolve(Buffer.concat(stdoutBuffers).toString('utf8').trim());
}
});
}); });
} }

View File

@@ -0,0 +1,15 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as which from 'which';
/**
* Searches for the first instance of the specified executable in the PATH environment variable
* @param exe The executable to search for
*/
export function searchForCmd(exe: string): Promise<string> {
// Note : This is separated out to allow for easy test stubbing
return new Promise<string>((resolve, reject) => which(exe, (err, path) => err ? reject(err) : resolve(path)));
}

View File

@@ -0,0 +1,59 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as TypeMoq from 'typemoq';
import * as azdata from '../azdata';
import * as sinon from 'sinon';
import * as childProcess from '../common/childProcess';
import * as should from 'should';
import * as utils from '../common/utils';
import * as nock from 'nock';
describe('azdata', function () {
let outputChannelMock: TypeMoq.IMock<vscode.OutputChannel>;
beforeEach(function (): void {
outputChannelMock = TypeMoq.Mock.ofType<vscode.OutputChannel>();
});
afterEach(function (): void {
sinon.restore();
nock.cleanAll();
nock.enableNetConnect();
});
describe('findAzdata', function () {
// Mock call to --version to simulate azdata being installed
sinon.stub(childProcess, 'executeCommand').returns(Promise.resolve('v1.0.0'));
it('successful', async function (): Promise<void> {
sinon.stub(utils, 'searchForCmd').returns(Promise.resolve('C:\\path\\to\\azdata.cmd'));
await should(azdata.findAzdata(outputChannelMock.object)).not.be.rejected();
});
it('unsuccessful', async function (): Promise<void> {
sinon.stub(utils, 'searchForCmd').returns(Promise.reject(new Error('Could not find azdata')));
await should(azdata.findAzdata(outputChannelMock.object)).be.rejected();
});
});
// TODO: Install not implemented on linux yet
describe.skip('downloadAndInstallAzdata', function (): void {
it('successful download & install', async function (): Promise<void> {
nock(azdata.azdataHostname)
.get(`/${azdata.azdataUri}`)
.replyWithFile(200, __filename);
const downloadPromise = azdata.downloadAndInstallAzdata(outputChannelMock.object);
await downloadPromise;
});
it('errors on unsuccessful download', async function (): Promise<void> {
nock('https://aka.ms')
.get('/azdata-msi')
.reply(404);
const downloadPromise = azdata.downloadAndInstallAzdata(outputChannelMock.object);
await should(downloadPromise).be.rejected();
});
});
});

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.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as should from 'should';
import * as TypeMoq from 'typemoq';
import { executeCommand } from '../../common/childProcess';
describe('ChildProcess', function () {
[undefined, [], ['test']].forEach(args => {
it(`Output channel is used with ${JSON.stringify(args)} args`, async function (): Promise<void> {
const outputChannelMock = TypeMoq.Mock.ofType<vscode.OutputChannel>();
await executeCommand('echo', args, outputChannelMock.object);
outputChannelMock.verify(x => x.appendLine(TypeMoq.It.isAny()), TypeMoq.Times.once());
});
});
it('Gets expected output', async function (): Promise<void> {
const echoOutput = 'test';
const output = await executeCommand('echo', [echoOutput]);
should(output).equal(echoOutput);
});
it('Invalid command errors', async function (): Promise<void> {
await should(executeCommand('sdfkslkf')).be.rejected();
});
});

View File

@@ -0,0 +1,63 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as should from 'should';
import * as TypeMoq from 'typemoq';
import { HttpClient } from '../../common/httpClient';
import * as path from 'path';
import * as os from 'os';
import * as fs from 'fs';
import * as uuid from 'uuid';
import * as nock from 'nock';
import * as sinon from 'sinon';
import { PassThrough } from 'stream';
describe('HttpClient', function () {
let outputChannelMock: TypeMoq.IMock<vscode.OutputChannel>;
before(function (): void {
outputChannelMock = TypeMoq.Mock.ofType<vscode.OutputChannel>();
});
afterEach(function (): void {
nock.cleanAll();
nock.enableNetConnect();
});
it('downloads file successfully', async function (): Promise<void> {
const downloadPath = path.join(os.tmpdir(), `azdata-httpClientTest-${uuid.v4()}.txt`);
await HttpClient.download('https://raw.githubusercontent.com/microsoft/azuredatastudio/main/README.md', downloadPath, outputChannelMock.object);
// Verify file was downloaded correctly
await fs.promises.stat(downloadPath);
});
it('errors on response stream error', async function (): Promise<void> {
const downloadPath = path.join(os.tmpdir(), `azdata-httpClientTest-error-${uuid.v4()}.txt`);
nock('https://127.0.0.1')
.get('/')
.replyWithError('Unexpected Error');
const downloadPromise = HttpClient.download('https://127.0.0.1', downloadPath, outputChannelMock.object);
await should(downloadPromise).be.rejected();
});
it('errors on write stream error', async function (): Promise<void> {
const downloadPath = path.join(os.tmpdir(), `azdata-httpClientTest-error-${uuid.v4()}.txt`);
const mockWriteStream = new PassThrough();
sinon.stub(fs, 'createWriteStream').returns(<any>mockWriteStream);
nock('https://127.0.0.1')
.get('/')
.reply(200, '');
const downloadPromise = HttpClient.download('https://127.0.0.1', downloadPath, outputChannelMock.object);
try {
// Passthrough streams will throw the error we emit so just no-op and
// let the HttpClient handler handle the error
mockWriteStream.emit('error', 'Unexpected write error');
} catch (err) { }
await should(downloadPromise).be.rejected();
});
});

View File

@@ -0,0 +1,18 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as should from 'should';
import { searchForCmd as searchForExe } from '../../common/utils';
describe('utils', function () {
describe('searchForExe', function(): void {
it('finds exe successfully', async function(): Promise<void> {
await searchForExe('node');
});
it('throws for non-existent exe', async function(): Promise<void> {
await should(searchForExe('someFakeExe')).be.rejected();
});
});
});

View File

@@ -0,0 +1,48 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
const testRunner = require('vscodetestcover');
const suite = 'azdata Extension Tests';
const mochaOptions: any = {
ui: 'bdd',
useColors: true,
timeout: 10000
};
// set relevant mocha options from the environment
if (process.env.ADS_TEST_GREP) {
mochaOptions.grep = process.env.ADS_TEST_GREP;
console.log(`setting options.grep to: ${mochaOptions.grep}`);
}
if (process.env.ADS_TEST_INVERT_GREP) {
mochaOptions.invert = parseInt(process.env.ADS_TEST_INVERT_GREP);
console.log(`setting options.invert to: ${mochaOptions.invert}`);
}
if (process.env.ADS_TEST_TIMEOUT) {
mochaOptions.timeout = parseInt(process.env.ADS_TEST_TIMEOUT);
console.log(`setting options.timeout to: ${mochaOptions.timeout}`);
}
if (process.env.ADS_TEST_RETRIES) {
mochaOptions.retries = parseInt(process.env.ADS_TEST_RETRIES);
console.log(`setting options.retries to: ${mochaOptions.retries}`);
}
if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) {
mochaOptions.reporter = 'mocha-multi-reporters';
mochaOptions.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`)
}
};
}
testRunner.configure(mochaOptions, { coverConfig: '../../coverConfig.json' });
export = testRunner;

File diff suppressed because it is too large Load Diff

View File

@@ -58,6 +58,11 @@ echo *** starting arc tests ***
echo ************************** echo **************************
call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\arc --extensionTestsPath=%~dp0\..\extensions\arc\out\test --user-data-dir=%VSCODEUSERDATADIR% --extensions-dir=%VSCODEEXTENSIONSDIR% --remote-debugging-port=9222 --disable-telemetry --disable-crash-reporter --disable-updates --nogpu call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\arc --extensionTestsPath=%~dp0\..\extensions\arc\out\test --user-data-dir=%VSCODEUSERDATADIR% --extensions-dir=%VSCODEEXTENSIONSDIR% --remote-debugging-port=9222 --disable-telemetry --disable-crash-reporter --disable-updates --nogpu
echo *****************************
echo *** starting azdata tests ***
echo *****************************
call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\azdata --extensionTestsPath=%~dp0\..\extensions\azdata\out\test --user-data-dir=%VSCODEUSERDATADIR% --extensions-dir=%VSCODEEXTENSIONSDIR% --remote-debugging-port=9222 --disable-telemetry --disable-crash-reporter --disable-updates --nogpu
echo ******************************** echo ********************************
echo *** starting azurecore tests *** echo *** starting azurecore tests ***
echo ******************************** echo ********************************

View File

@@ -12,6 +12,7 @@ const os = require('os');
const extensionList = [ const extensionList = [
'admin-tool-ext-win', 'admin-tool-ext-win',
'agent', 'agent',
'azdata',
'azurecore', 'azurecore',
'cms', 'cms',
'dacpac', 'dacpac',

View File

@@ -54,6 +54,11 @@ echo *** starting arc tests ***
echo ************************** echo **************************
"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX --extensionDevelopmentPath=$ROOT/extensions/arc --extensionTestsPath=$ROOT/extensions/arc/out/test --user-data-dir=$VSCODEUSERDATADIR --extensions-dir=$VSCODEEXTDIR --disable-telemetry --disable-crash-reporter --disable-updates --nogpu "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX --extensionDevelopmentPath=$ROOT/extensions/arc --extensionTestsPath=$ROOT/extensions/arc/out/test --user-data-dir=$VSCODEUSERDATADIR --extensions-dir=$VSCODEEXTDIR --disable-telemetry --disable-crash-reporter --disable-updates --nogpu
echo *****************************
echo *** starting azdata tests ***
echo *****************************
"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX --extensionDevelopmentPath=$ROOT/extensions/azdata --extensionTestsPath=$ROOT/extensions/azdata/out/test --user-data-dir=$VSCODEUSERDATADIR --extensions-dir=$VSCODEEXTDIR --disable-telemetry --disable-crash-reporter --disable-updates --nogpu
echo ******************************** echo ********************************
echo *** starting azurecore tests *** echo *** starting azurecore tests ***
echo ******************************** echo ********************************