Skip to content

Commit

Permalink
Windows store locator (#14162)
Browse files Browse the repository at this point in the history
* Initial commit for windows store locator

* More tests

* Simplify locator

* Tweaks

* Test fixes

* Fix tests
  • Loading branch information
karthiknadig authored Oct 1, 2020
1 parent cc093b0 commit cfe12a7
Show file tree
Hide file tree
Showing 7 changed files with 316 additions and 24 deletions.
8 changes: 8 additions & 0 deletions src/client/pythonEnvironments/common/externalDependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,11 @@ export function getGlobalPersistentStore<T>(key: string): IPersistentStore<T> {
set(value: T) { return state.updateValue(value); },
};
}

export async function getFileInfo(filePath: string): Promise<{ctime:number, mtime:number}> {
const data = await fsapi.lstat(filePath);
return {
ctime: data.ctime.getUTCDate(),
mtime: data.mtime.getUTCDate(),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@
import * as fsapi from 'fs-extra';
import * as path from 'path';
import { traceWarning } from '../../../../common/logger';
import { getEnvironmentVariable } from '../../../../common/utils/platform';
import { Architecture, getEnvironmentVariable } from '../../../../common/utils/platform';
import {
PythonEnvInfo, PythonEnvKind, PythonReleaseLevel, PythonVersion,
} from '../../../base/info';
import { parseVersion } from '../../../base/info/pythonVersion';
import { ILocator, IPythonEnvsIterator } from '../../../base/locator';
import { PythonEnvsWatcher } from '../../../base/watcher';
import { getFileInfo } from '../../../common/externalDependencies';
import { isWindowsPythonExe } from '../../../common/windowsUtils';

/**
Expand Down Expand Up @@ -107,5 +114,51 @@ export async function getWindowsStorePythonExes(): Promise<string[]> {
.filter(isWindowsPythonExe);
}

// tslint:disable-next-line: no-suspicious-comment
// TODO: The above APIs will be consumed by the Windows Store locator class when we have it.
export class WindowsStoreLocator extends PythonEnvsWatcher implements ILocator {
private readonly kind:PythonEnvKind = PythonEnvKind.WindowsStore;

public iterEnvs(): IPythonEnvsIterator {
const buildEnvInfo = (exe:string) => this.buildEnvInfo(exe);
const iterator = async function* () {
const exes = await getWindowsStorePythonExes();
yield* exes.map(buildEnvInfo);
};
return iterator();
}

public async resolveEnv(env: string | PythonEnvInfo): Promise<PythonEnvInfo | undefined> {
const executablePath = typeof env === 'string' ? env : env.executable.filename;
if (await isWindowsStoreEnvironment(executablePath)) {
return this.buildEnvInfo(executablePath);
}
return undefined;
}

private async buildEnvInfo(exe:string): Promise<PythonEnvInfo> {
let version:PythonVersion;
try {
version = parseVersion(path.basename(exe));
} catch (e) {
version = {
major: 3,
minor: -1,
micro: -1,
release: { level: PythonReleaseLevel.Final, serial: -1 },
sysVersion: undefined,
};
}
return {
name: '',
location: '',
kind: this.kind,
executable: {
filename: exe,
sysPrefix: '',
...(await getFileInfo(exe)),
},
version,
arch: Architecture.x64,
distro: { org: 'Microsoft' },
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Not a real exe.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Not a real exe.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Not a real exe.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Not a real exe.
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,259 @@
// Licensed under the MIT License.

import * as assert from 'assert';
import { zip } from 'lodash';
import * as path from 'path';
import * as sinon from 'sinon';
import { ExecutionResult } from '../../../../client/common/process/types';
import * as platformApis from '../../../../client/common/utils/platform';
import * as storeApis from '../../../../client/pythonEnvironments/discovery/locators/services/windowsStoreLocator';
import {
PythonEnvInfo, PythonEnvKind, PythonReleaseLevel, PythonVersion,
} from '../../../../client/pythonEnvironments/base/info';
import { InterpreterInformation } from '../../../../client/pythonEnvironments/base/info/interpreter';
import { parseVersion } from '../../../../client/pythonEnvironments/base/info/pythonVersion';
import * as externalDep from '../../../../client/pythonEnvironments/common/externalDependencies';
import { getWindowsStorePythonExes, WindowsStoreLocator } from '../../../../client/pythonEnvironments/discovery/locators/services/windowsStoreLocator';
import { getEnvs } from '../../base/common';
import { TEST_LAYOUT_ROOT } from '../../common/commonTestConstants';

suite('Windows Store Utils', () => {
let getEnvVar: sinon.SinonStub;
const testLocalAppData = path.join(TEST_LAYOUT_ROOT, 'storeApps');
const testStoreAppRoot = path.join(testLocalAppData, 'Microsoft', 'WindowsApps');
setup(() => {
getEnvVar = sinon.stub(platformApis, 'getEnvironmentVariable');
getEnvVar.withArgs('LOCALAPPDATA').returns(testLocalAppData);
});
teardown(() => {
getEnvVar.restore();
suite('Windows Store', () => {
suite('Utils', () => {
let getEnvVar: sinon.SinonStub;
const testLocalAppData = path.join(TEST_LAYOUT_ROOT, 'storeApps');
const testStoreAppRoot = path.join(testLocalAppData, 'Microsoft', 'WindowsApps');

setup(() => {
getEnvVar = sinon.stub(platformApis, 'getEnvironmentVariable');
getEnvVar.withArgs('LOCALAPPDATA').returns(testLocalAppData);
});

teardown(() => {
getEnvVar.restore();
});

test('Store Python Interpreters', async () => {
const expected = [
path.join(testStoreAppRoot, 'python.exe'),
path.join(testStoreAppRoot, 'python3.7.exe'),
path.join(testStoreAppRoot, 'python3.8.exe'),
path.join(testStoreAppRoot, 'python3.exe'),
];

const actual = await getWindowsStorePythonExes();
assert.deepEqual(actual, expected);
});
});
test('Store Python Interpreters', async () => {
const expected = [
path.join(testStoreAppRoot, 'python.exe'),
path.join(testStoreAppRoot, 'python3.7.exe'),
path.join(testStoreAppRoot, 'python3.8.exe'),
path.join(testStoreAppRoot, 'python3.exe'),
];

const actual = await storeApis.getWindowsStorePythonExes();
assert.deepEqual(actual, expected);

suite('Locator', () => {
let stubShellExec: sinon.SinonStub;
let getEnvVar: sinon.SinonStub;

const testLocalAppData = path.join(TEST_LAYOUT_ROOT, 'storeApps');
const testStoreAppRoot = path.join(testLocalAppData, 'Microsoft', 'WindowsApps');
const pathToData = new Map<string, {
versionInfo:(string|number)[],
sysPrefix: string,
sysVersion: string,
is64Bit: boolean
}>();

const python383data = {
versionInfo: [3, 8, 3, 'final', 0],
sysPrefix: 'path',
sysVersion: '3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:37:02) [MSC v.1924 64 bit (AMD64)]',
is64Bit: true,
};

const python379data = {
versionInfo: [3, 7, 9, 'final', 0],
sysPrefix: 'path',
sysVersion: '3.7.9 (tags/v3.7.9:13c94747c7, Aug 17 2020, 16:30:00) [MSC v.1900 64 bit (AMD64)]',
is64Bit: true,
};

pathToData.set(path.join(testStoreAppRoot, 'python.exe'), python383data);
pathToData.set(path.join(testStoreAppRoot, 'python3.exe'), python383data);
pathToData.set(path.join(testStoreAppRoot, 'python3.8.exe'), python383data);
pathToData.set(path.join(testStoreAppRoot, 'python3.7.exe'), python379data);

function createExpectedInterpreterInfo(
executable: string,
sysVersion?: string,
sysPrefix?: string,
versionStr?:string,
): InterpreterInformation {
let version:PythonVersion;
try {
version = parseVersion(versionStr ?? path.basename(executable));
if (sysVersion) {
version.sysVersion = sysVersion;
}
} catch (e) {
version = {
major: 3,
minor: -1,
micro: -1,
release: { level: PythonReleaseLevel.Final, serial: -1 },
sysVersion,
};
}
return {
version,
arch: platformApis.Architecture.x64,
executable: {
filename: executable,
sysPrefix: sysPrefix ?? '',
ctime: -1,
mtime: -1,
},
};
}

setup(() => {
stubShellExec = sinon.stub(externalDep, 'shellExecute');
stubShellExec.callsFake((command:string) => {
if (command.indexOf('notpython.exe') > 0) {
return Promise.resolve<ExecutionResult<string>>({ stdout: '' });
}
if (command.indexOf('python3.7.exe') > 0) {
return Promise.resolve<ExecutionResult<string>>({ stdout: JSON.stringify(python379data) });
}
return Promise.resolve<ExecutionResult<string>>({ stdout: JSON.stringify(python383data) });
});

getEnvVar = sinon.stub(platformApis, 'getEnvironmentVariable');
getEnvVar.withArgs('LOCALAPPDATA').returns(testLocalAppData);
});

teardown(() => {
stubShellExec.restore();
getEnvVar.restore();
});

function assertEnvEqual(actual:PythonEnvInfo | undefined, expected: PythonEnvInfo | undefined):void {
assert.notStrictEqual(actual, undefined);
assert.notStrictEqual(expected, undefined);

if (actual) {
// ensure ctime and mtime are greater than -1
assert.ok(actual?.executable.ctime > -1);
assert.ok(actual?.executable.mtime > -1);

// No need to match these, so reset them
actual.executable.ctime = -1;
actual.executable.mtime = -1;

assert.deepStrictEqual(actual, expected);
}
}

test('iterEnvs()', async () => {
const expectedEnvs = [...pathToData.keys()]
.sort((a: string, b: string) => a.localeCompare(b))
.map((k): PythonEnvInfo|undefined => {
const data = pathToData.get(k);
if (data) {
return {

name: '',
location: '',
kind: PythonEnvKind.WindowsStore,
distro: { org: 'Microsoft' },
...createExpectedInterpreterInfo(k),
};
}
return undefined;
});

const locator = new WindowsStoreLocator();
const iterator = locator.iterEnvs();
const actualEnvs = (await getEnvs(iterator))
.sort((a, b) => a.executable.filename.localeCompare(b.executable.filename));

zip(actualEnvs, expectedEnvs).forEach((value) => {
const [actual, expected] = value;
assertEnvEqual(actual, expected);
});
});

test('resolveEnv(string)', async () => {
const python38path = path.join(testStoreAppRoot, 'python3.8.exe');
const expected = {

name: '',
location: '',
kind: PythonEnvKind.WindowsStore,
distro: { org: 'Microsoft' },
...createExpectedInterpreterInfo(python38path),
};

const locator = new WindowsStoreLocator();
const actual = await locator.resolveEnv(python38path);

assertEnvEqual(actual, expected);
});

test('resolveEnv(PythonEnvInfo)', async () => {
const python38path = path.join(testStoreAppRoot, 'python3.8.exe');
const expected = {

name: '',
location: '',
kind: PythonEnvKind.WindowsStore,
distro: { org: 'Microsoft' },
...createExpectedInterpreterInfo(python38path),
};

// Partially filled in env info object
const input:PythonEnvInfo = {
name: '',
location: '',
kind: PythonEnvKind.WindowsStore,
distro: { org: 'Microsoft' },
arch: platformApis.Architecture.x64,
executable: {
filename: python38path,
sysPrefix: '',
ctime: -1,
mtime: -1,
},
version: {
major: 3,
minor: -1,
micro: -1,
release: { level: PythonReleaseLevel.Final, serial: -1 },
},
};

const locator = new WindowsStoreLocator();
const actual = await locator.resolveEnv(input);

assertEnvEqual(actual, expected);
});
test('resolveEnv(string): forbidden path', async () => {
const python38path = path.join(testLocalAppData, 'Program Files', 'WindowsApps', 'python3.8.exe');
const expected = {

name: '',
location: '',
kind: PythonEnvKind.WindowsStore,
distro: { org: 'Microsoft' },
...createExpectedInterpreterInfo(python38path),
};

const locator = new WindowsStoreLocator();
const actual = await locator.resolveEnv(python38path);

assertEnvEqual(actual, expected);
});
test('resolveEnv(string): Non store python', async () => {
// Use a non store root path
const python38path = path.join(testLocalAppData, 'python3.8.exe');

const locator = new WindowsStoreLocator();
const actual = await locator.resolveEnv(python38path);

assert.deepStrictEqual(actual, undefined);
});
});
});

0 comments on commit cfe12a7

Please sign in to comment.