mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-19 17:22:48 -05:00
Add unit tests for PythonPathLookup class. (#14133)
This commit is contained in:
@@ -13,19 +13,25 @@ export interface PythonPathInfo {
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface PythonCommand {
|
||||
command: string;
|
||||
args?: string[];
|
||||
}
|
||||
|
||||
export class PythonPathLookup {
|
||||
private condaLocations: string[];
|
||||
private readonly _condaLocations: string[];
|
||||
private readonly _pythonCommands: PythonCommand[];
|
||||
constructor() {
|
||||
if (process.platform !== constants.winPlatform) {
|
||||
let userFolder = process.env['HOME'];
|
||||
this.condaLocations = [
|
||||
this._condaLocations = [
|
||||
'/opt/*conda*/bin/python3',
|
||||
'/usr/share/*conda*/bin/python3',
|
||||
`${userFolder}/*conda*/bin/python3`
|
||||
];
|
||||
} else {
|
||||
let userFolder = process.env['USERPROFILE'].replace(/\\/g, '/').replace('C:', '');
|
||||
this.condaLocations = [
|
||||
this._condaLocations = [
|
||||
'/ProgramData/[Mm]iniconda*/python.exe',
|
||||
'/ProgramData/[Aa]naconda*/python.exe',
|
||||
`${userFolder}/[Mm]iniconda*/python.exe`,
|
||||
@@ -34,34 +40,43 @@ export class PythonPathLookup {
|
||||
`${userFolder}/AppData/Local/Continuum/[Aa]naconda*/python.exe`
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public async getSuggestions(): Promise<PythonPathInfo[]> {
|
||||
let pythonSuggestions = await this.getPythonSuggestions();
|
||||
let condaSuggestions = await this.getCondaSuggestions();
|
||||
this._pythonCommands = [
|
||||
{ command: 'python3.7' },
|
||||
{ command: 'python3.6' },
|
||||
{ command: 'python3' },
|
||||
{ command: 'python' }
|
||||
];
|
||||
|
||||
if (pythonSuggestions) {
|
||||
if (condaSuggestions && condaSuggestions.length > 0) {
|
||||
pythonSuggestions = pythonSuggestions.concat(condaSuggestions);
|
||||
}
|
||||
return this.getInfoForPaths(pythonSuggestions);
|
||||
} else {
|
||||
return [];
|
||||
if (process.platform === constants.winPlatform) {
|
||||
this._pythonCommands.concat([
|
||||
{ command: 'py', args: ['-3.7'] },
|
||||
{ command: 'py', args: ['-3.6'] },
|
||||
{ command: 'py', args: ['-3'] }
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private async getCondaSuggestions(): Promise<string[]> {
|
||||
public async getSuggestions(): Promise<PythonPathInfo[]> {
|
||||
let pythonSuggestions = await this.getPythonSuggestions(this._pythonCommands);
|
||||
let condaSuggestions = await this.getCondaSuggestions(this._condaLocations);
|
||||
|
||||
let allSuggestions = pythonSuggestions.concat(condaSuggestions);
|
||||
return this.getInfoForPaths(allSuggestions);
|
||||
}
|
||||
|
||||
public async getCondaSuggestions(condaLocations: string[]): Promise<string[]> {
|
||||
try {
|
||||
let condaResults = await Promise.all(this.condaLocations.map(location => this.globSearch(location)));
|
||||
let condaResults = await Promise.all(condaLocations.map(location => this.globSearch(location)));
|
||||
let condaFiles = condaResults.reduce((first, second) => first.concat(second));
|
||||
return condaFiles.filter(condaPath => condaPath && condaPath.length > 0);
|
||||
} catch (err) {
|
||||
console.log(`Problem encountered getting Conda installations: ${err}`);
|
||||
console.log(`Problem encountered getting Conda installs: ${err}`);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private globSearch(globPattern: string): Promise<string[]> {
|
||||
public globSearch(globPattern: string): Promise<string[]> {
|
||||
return new Promise<string[]>((resolve, reject) => {
|
||||
glob(globPattern, (err, files) => {
|
||||
if (err) {
|
||||
@@ -72,20 +87,17 @@ export class PythonPathLookup {
|
||||
});
|
||||
}
|
||||
|
||||
private async getPythonSuggestions(): Promise<string[]> {
|
||||
let pathsToCheck = this.getPythonCommands();
|
||||
|
||||
let pythonPaths = await Promise.all(pathsToCheck.map(item => this.getPythonPath(item)));
|
||||
let results: string[];
|
||||
if (pythonPaths) {
|
||||
results = pythonPaths.filter(path => path && path.length > 0);
|
||||
} else {
|
||||
results = [];
|
||||
public async getPythonSuggestions(pythonCommands: PythonCommand[]): Promise<string[]> {
|
||||
try {
|
||||
let pythonPaths = await Promise.all(pythonCommands.map(item => this.getPythonPath(item)));
|
||||
return pythonPaths.filter(path => path && path.length > 0);
|
||||
} catch (err) {
|
||||
console.log(`Problem encountered getting Python installs: ${err}`);
|
||||
}
|
||||
return results;
|
||||
return [];
|
||||
}
|
||||
|
||||
private async getPythonPath(options: { command: string; args?: string[] }): Promise<string | undefined> {
|
||||
public async getPythonPath(options: PythonCommand): Promise<string | undefined> {
|
||||
try {
|
||||
let args = Array.isArray(options.args) ? options.args : [];
|
||||
args = args.concat(['-c', '"import sys;print(sys.executable)"']);
|
||||
@@ -102,20 +114,7 @@ export class PythonPathLookup {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private getPythonCommands(): { command: string; args?: string[] }[] {
|
||||
const paths = ['python3.7', 'python3.6', 'python3', 'python']
|
||||
.map(item => { return { command: item }; });
|
||||
if (process.platform !== constants.winPlatform) {
|
||||
return paths;
|
||||
}
|
||||
|
||||
const versions = ['3.7', '3.6', '3'];
|
||||
return paths.concat(versions.map(version => {
|
||||
return { command: 'py', args: [`-${version}`] };
|
||||
}));
|
||||
}
|
||||
|
||||
private async getInfoForPaths(pythonPaths: string[]): Promise<PythonPathInfo[]> {
|
||||
public async getInfoForPaths(pythonPaths: string[]): Promise<PythonPathInfo[]> {
|
||||
let pathsInfo = await Promise.all(pythonPaths.map(path => this.getInfoForPath(path)));
|
||||
|
||||
// Remove duplicate paths, and entries with missing values
|
||||
@@ -140,7 +139,7 @@ export class PythonPathLookup {
|
||||
});
|
||||
}
|
||||
|
||||
private async getInfoForPath(pythonPath: string): Promise<PythonPathInfo | undefined> {
|
||||
public async getInfoForPath(pythonPath: string): Promise<PythonPathInfo | undefined> {
|
||||
try {
|
||||
// "python --version" returns nothing from executeBufferedCommand with Python 2.X,
|
||||
// so use sys.version_info here instead.
|
||||
|
||||
234
extensions/notebook/src/test/python/pythonPath.test.ts
Normal file
234
extensions/notebook/src/test/python/pythonPath.test.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as sinon from 'sinon';
|
||||
import * as should from 'should';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import * as uuid from 'uuid';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as rimraf from 'rimraf';
|
||||
import * as utils from '../../common/utils';
|
||||
import { PythonPathInfo, PythonPathLookup } from '../../dialog/pythonPathLookup';
|
||||
import * as constants from '../../common/constants';
|
||||
import { promisify } from 'util';
|
||||
|
||||
describe('PythonPathLookup', function () {
|
||||
let pathLookup: PythonPathLookup;
|
||||
|
||||
beforeEach(() => {
|
||||
pathLookup = new PythonPathLookup();
|
||||
});
|
||||
|
||||
afterEach(function (): void {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it('getSuggestions', async () => {
|
||||
sinon.stub(pathLookup, 'getPythonSuggestions').resolves(['testPath/Python38/python']);
|
||||
sinon.stub(pathLookup, 'getCondaSuggestions').resolves(['testPath/Anaconda/conda']);
|
||||
|
||||
let expectedResults: PythonPathInfo[] = [{
|
||||
installDir: 'testPath/Python38/python',
|
||||
version: '3.8.0'
|
||||
}, {
|
||||
installDir: 'testPath/Anaconda/conda',
|
||||
version: '3.6.0'
|
||||
}];
|
||||
|
||||
let getInfoStub = sinon.stub(pathLookup, 'getInfoForPaths').resolves(expectedResults);
|
||||
|
||||
let results = await pathLookup.getSuggestions();
|
||||
should(results).be.deepEqual(expectedResults);
|
||||
should(getInfoStub.callCount).be.equal(1);
|
||||
should(getInfoStub.firstCall.args[0].length).be.equal(2);
|
||||
});
|
||||
|
||||
it('getCondaSuggestions', async () => {
|
||||
sinon.stub(pathLookup, 'globSearch')
|
||||
.onCall(0).resolves(['a', undefined, 'b'])
|
||||
.onCall(1).resolves(['', 'c'])
|
||||
.onCall(2).resolves(['d']);
|
||||
|
||||
// globSearch is mocked out, so only the number of location args matters here
|
||||
let result: string[] = await should(pathLookup.getCondaSuggestions(['1', '2', '3'])).not.be.rejected();
|
||||
should(result).be.deepEqual(['a', 'b', 'c', 'd']);
|
||||
});
|
||||
|
||||
it('getCondaSuggestions - return empty array on error', async () => {
|
||||
sinon.stub(pathLookup, 'globSearch').rejects('Planned test failure.');
|
||||
|
||||
let result: string[] = await should(pathLookup.getCondaSuggestions(['testPath'])).not.be.rejected();
|
||||
should(result).not.be.undefined();
|
||||
should(result.length).be.equal(0);
|
||||
});
|
||||
|
||||
it('getPythonSuggestions', async () => {
|
||||
let expectedPath = 'testPath/Python38';
|
||||
sinon.stub(pathLookup, 'getPythonPath')
|
||||
.onCall(0).resolves(undefined)
|
||||
.onCall(1).resolves(expectedPath)
|
||||
.onCall(2).resolves('');
|
||||
|
||||
// getPythonPath is mocked out, so only the number of command args matters here
|
||||
let result: string[] = await should(pathLookup.getPythonSuggestions([{ command: '1' }, { command: '2' }, { command: '3' }])).not.be.rejected();
|
||||
should(result).not.be.undefined();
|
||||
should(result.length).be.equal(1);
|
||||
should(result[0]).be.equal(expectedPath);
|
||||
});
|
||||
|
||||
it('getPythonSuggestions - return empty array on error', async () => {
|
||||
sinon.stub(pathLookup, 'getPythonPath').rejects('Planned test failure.');
|
||||
|
||||
let result: string[] = await should(pathLookup.getPythonSuggestions([{ command: 'testPath/Python38/python' }])).not.be.rejected();
|
||||
should(result).not.be.undefined();
|
||||
should(result.length).be.equal(0);
|
||||
});
|
||||
|
||||
it('getPythonPath', async () => {
|
||||
let expectedPath = 'testPath/Python38';
|
||||
sinon.stub(utils, 'executeBufferedCommand').resolves(expectedPath);
|
||||
sinon.stub(utils, 'exists').resolves(true);
|
||||
|
||||
let result: string = await should(pathLookup.getPythonPath({ command: 'testPath/Python38/python' })).not.be.rejected();
|
||||
should(result).be.equal(expectedPath);
|
||||
});
|
||||
|
||||
it('getPythonPath - return undefined if resulting path does not exist', async () => {
|
||||
let expectedPath = 'testPath/Python38';
|
||||
sinon.stub(utils, 'executeBufferedCommand').resolves(expectedPath);
|
||||
sinon.stub(utils, 'exists').resolves(false);
|
||||
|
||||
let result: string = await should(pathLookup.getPythonPath({ command: 'testPath/Python38/python' })).not.be.rejected();
|
||||
should(result).be.undefined();
|
||||
});
|
||||
|
||||
it('getPythonPath - return undefined on error', async () => {
|
||||
sinon.stub(utils, 'executeBufferedCommand').rejects('Planned test failure.');
|
||||
|
||||
let result: string = await should(pathLookup.getPythonPath({ command: 'testPath/Python38/python' })).not.be.rejected();
|
||||
should(result).be.undefined();
|
||||
});
|
||||
|
||||
it('getPythonPath - return undefined if command returns no data', async () => {
|
||||
sinon.stub(utils, 'executeBufferedCommand')
|
||||
.onCall(0).resolves(undefined)
|
||||
.onCall(1).resolves('');
|
||||
|
||||
let result: string = await should(pathLookup.getPythonPath({ command: 'testPath/Python38/python' })).not.be.rejected();
|
||||
should(result).be.undefined();
|
||||
|
||||
result = await should(pathLookup.getPythonPath({ command: 'testPath/Python38/python' })).not.be.rejected();
|
||||
should(result).be.undefined();
|
||||
});
|
||||
|
||||
it('globSearch', async () => {
|
||||
let testFolder = path.join(os.tmpdir(), `PythonPathTest_${uuid.v4()}`);
|
||||
await fs.mkdir(testFolder);
|
||||
try {
|
||||
let standaloneFile = path.join(testFolder, 'testFile.txt');
|
||||
let folderFile1 = path.join(testFolder, 'TestFolder1', 'testFile.txt');
|
||||
let folderFile2 = path.join(testFolder, 'TestFolder2', 'testFile.txt');
|
||||
|
||||
await fs.ensureFile(standaloneFile);
|
||||
await fs.ensureFile(folderFile1);
|
||||
await fs.ensureFile(folderFile2);
|
||||
|
||||
let normalizedFolderPath: string;
|
||||
if (process.platform === constants.winPlatform) {
|
||||
let driveColonIndex = testFolder.indexOf(':');
|
||||
normalizedFolderPath = testFolder.substr(driveColonIndex + 1).replace(/\\/g, '/');
|
||||
} else {
|
||||
normalizedFolderPath = testFolder;
|
||||
}
|
||||
|
||||
let searchResults = await pathLookup.globSearch(`${normalizedFolderPath}/*.txt`);
|
||||
should(searchResults).be.deepEqual([standaloneFile]);
|
||||
|
||||
searchResults = await pathLookup.globSearch(`${normalizedFolderPath}/[tT]estFolder*/*.txt`);
|
||||
should(searchResults).be.deepEqual([folderFile1, folderFile2]);
|
||||
|
||||
searchResults = await pathLookup.globSearch(`${normalizedFolderPath}/[tT]estFolder*/*.csv`);
|
||||
should(searchResults).be.deepEqual([]);
|
||||
} finally {
|
||||
await promisify(rimraf)(testFolder);
|
||||
}
|
||||
});
|
||||
|
||||
it('getInfoForPaths', async () => {
|
||||
let expectedPathInfo: PythonPathInfo = {
|
||||
installDir: 'testPath/Python38',
|
||||
version: '3.8.0'
|
||||
};
|
||||
|
||||
let callNumber = 0;
|
||||
sinon.stub(pathLookup, 'getInfoForPath')
|
||||
.onCall(callNumber++).resolves({
|
||||
installDir: undefined,
|
||||
version: ''
|
||||
})
|
||||
.onCall(callNumber++).resolves({
|
||||
installDir: '',
|
||||
version: undefined
|
||||
})
|
||||
.onCall(callNumber++).resolves({
|
||||
installDir: '',
|
||||
version: ''
|
||||
})
|
||||
.onCall(callNumber++).resolves({
|
||||
installDir: 'testPath/Python',
|
||||
version: '2.7.0'
|
||||
})
|
||||
.onCall(callNumber++).resolves(expectedPathInfo)
|
||||
.onCall(callNumber++).resolves(expectedPathInfo)
|
||||
.onCall(callNumber++).rejects('Unexpected number of getInfoForPath calls.');
|
||||
|
||||
// getInfoForPath is mocked out above, so only the number of path arguments matters here
|
||||
let result = await pathLookup.getInfoForPaths(['1', '2', '3', '4', '5', '6']);
|
||||
|
||||
// The path lookup should filter out any invalid path info, any Python 2 info, and any duplicates.
|
||||
// So, we should be left with a single info object using the mocked setup above.
|
||||
should(result).be.deepEqual([expectedPathInfo]);
|
||||
});
|
||||
|
||||
it('getInfoForPaths - empty array arg', async () => {
|
||||
let getInfoStub = sinon.stub(pathLookup, 'getInfoForPath').rejects('Unexpected getInfoForPath call');
|
||||
let result = await pathLookup.getInfoForPaths([]);
|
||||
should(result).not.be.undefined;
|
||||
should(result.length).be.equal(0);
|
||||
should(getInfoStub.callCount).be.equal(0);
|
||||
});
|
||||
|
||||
it('getInfoForPath', async () => {
|
||||
let pythonVersion = '3.8.0';
|
||||
let pythonFolder = 'testPath/Python38';
|
||||
sinon.stub(utils, 'executeBufferedCommand')
|
||||
.onFirstCall().resolves(pythonVersion)
|
||||
.onSecondCall().resolves(pythonFolder);
|
||||
|
||||
let expectedResult: PythonPathInfo = {
|
||||
installDir: pythonFolder,
|
||||
version: pythonVersion
|
||||
};
|
||||
let result = await pathLookup.getInfoForPath(`${pythonFolder}/python`);
|
||||
should(result).deepEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('getInfoForPath - Return undefined string on error', async () => {
|
||||
sinon.stub(utils, 'executeBufferedCommand').rejects('Planned test failure.');
|
||||
|
||||
let pathInfoPromise = pathLookup.getInfoForPath('testPath/Python38/python');
|
||||
let result = await should(pathInfoPromise).not.be.rejected();
|
||||
should(result).be.undefined();
|
||||
});
|
||||
|
||||
it('getInfoForPath - Return undefined if commands return no data', async () => {
|
||||
sinon.stub(utils, 'executeBufferedCommand').resolves('');
|
||||
|
||||
let pathInfoPromise = pathLookup.getInfoForPath('testPath/Python38/python');
|
||||
let result = await should(pathInfoPromise).not.be.rejected();
|
||||
should(result).be.undefined();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user