Skip to content

Commit

Permalink
Add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
karthiknadig committed Jul 17, 2024
1 parent 854d676 commit e084def
Show file tree
Hide file tree
Showing 2 changed files with 324 additions and 26 deletions.
64 changes: 38 additions & 26 deletions src/client/pythonEnvironments/nativeAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ function makeExecutablePath(prefix?: string): string {
if (!prefix) {
return process.platform === 'win32' ? 'python.exe' : 'python';
}
return process.platform === 'win32' ? `${prefix}\\python.exe` : `${prefix}/bin/python`;
return process.platform === 'win32' ? path.join(prefix, 'python.exe') : path.join(prefix, 'python');
}

function toArch(a: string | undefined): Architecture {
Expand Down Expand Up @@ -157,7 +157,9 @@ function toPythonEnvInfo(finder: NativePythonFinder, nativeEnv: NativeEnvInfo):
const arch = toArch(nativeEnv.arch);
const version: PythonVersion = parseVersion(nativeEnv.version ?? '');
const name = getName(nativeEnv, kind);
const displayName = nativeEnv.version ? getDisplayName(version, kind, arch, name) : 'Python';
const displayName = nativeEnv.version
? getDisplayName(version, kind, arch, name)
: nativeEnv.displayName ?? 'Python';

return {
name,
Expand Down Expand Up @@ -227,31 +229,41 @@ class NativePythonEnvironments implements IDiscoveryAPI, Disposable {
setImmediate(async () => {
try {
for await (const native of this.finder.refresh()) {
if (!validEnv(this.finder, native)) {
// eslint-disable-next-line no-continue
continue;
}
try {
if (validEnv(this.finder, native)) {
if (!native.version) {
if (
this.finder.categoryToKind(native.kind) === PythonEnvKind.Conda &&
!native.executable
) {
// This is a conda env without python, no point trying to resolve this.
// There is nothing to resolve
this.addEnv(native);
} else {
this.resolveEnv(native.executable ?? native.prefix)
.then(() => {
this.addEnv(native);
})
.ignoreErrors();
}
} else {
const version = parseVersion(native.version);
if (version.micro < 0 || version.minor < 0 || version.major < 0) {
this.resolveEnv(native.executable ?? native.prefix).ignoreErrors();
} else {
this.addEnv(native);
}
}
const envPath = native.executable ?? native.prefix;
const version = native.version ? parseVersion(native.version) : undefined;

if (this.finder.categoryToKind(native.kind) === PythonEnvKind.Conda && !native.executable) {
// This is a conda env without python, no point trying to resolve this.
// There is nothing to resolve
this.addEnv(native);
} else if (
envPath &&
(!version || version.major < 0 || version.minor < 0 || version.micro < 0)
) {
// We have a path, but no version info, try to resolve the environment.
this.finder
.resolve(envPath)
.then((env) => {
if (env) {
this.addEnv(env);
}
})
.ignoreErrors();
} else if (
envPath &&
version &&
version.major >= 0 &&
version.minor >= 0 &&
version.micro >= 0
) {
this.addEnv(native);
} else {
traceError(`Failed to process environment: ${JSON.stringify(native)}`);
}
} catch (err) {
traceError(`Failed to process environment: ${err}`);
Expand Down
286 changes: 286 additions & 0 deletions src/test/pythonEnvironments/nativeAPI.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
/* eslint-disable class-methods-use-this */

import { assert } from 'chai';
import * as path from 'path';
import * as typemoq from 'typemoq';
import * as sinon from 'sinon';
import * as nativeAPI from '../../client/pythonEnvironments/nativeAPI';
import { IDiscoveryAPI } from '../../client/pythonEnvironments/base/locator';
import {
NativeEnvInfo,
NativePythonFinder,
} from '../../client/pythonEnvironments/base/locators/common/nativePythonFinder';
import { Architecture } from '../../client/common/utils/platform';
import { PythonEnvInfo, PythonEnvKind, PythonEnvType } from '../../client/pythonEnvironments/base/info';
import { isWindows } from '../../client/common/platform/platformService';

suite('Native Python API', () => {
let api: IDiscoveryAPI;
let mockFinder: typemoq.IMock<NativePythonFinder>;

const basicEnv: NativeEnvInfo = {
displayName: 'Basic Python',
name: 'basic_python',
executable: '/usr/bin/python',
kind: 'system',
version: `3.12.0`,
prefix: '/usr/bin',
};

const basicEnv2: NativeEnvInfo = {
displayName: 'Basic Python',
name: 'basic_python',
executable: '/usr/bin/python',
kind: 'system',
version: undefined, // this is intentionally set to trigger resolve
prefix: '/usr/bin',
};

const expectedBasicEnv: PythonEnvInfo = {
arch: Architecture.Unknown,
detailedDisplayName: "Python 3.12.0 ('basic_python')",
display: "Python 3.12.0 ('basic_python')",
distro: { org: '' },
executable: { filename: '/usr/bin/python', sysPrefix: '/usr/bin', ctime: -1, mtime: -1 },
kind: PythonEnvKind.System,
location: '/usr/bin',
source: [],
name: 'basic_python',
type: undefined,
version: { sysVersion: '3.12.0', major: 3, minor: 12, micro: 0 },
};

const conda: NativeEnvInfo = {
displayName: 'Conda Python',
name: 'conda_python',
executable: '/home/user/.conda/envs/conda_python/python',
kind: 'conda',
version: `3.12.0`,
prefix: '/home/user/.conda/envs/conda_python',
};

const conda1: NativeEnvInfo = {
displayName: 'Conda Python',
name: 'conda_python',
executable: '/home/user/.conda/envs/conda_python/python',
kind: 'conda',
version: undefined, // this is intentionally set to test conda without python
prefix: '/home/user/.conda/envs/conda_python',
};

const conda2: NativeEnvInfo = {
displayName: 'Conda Python',
name: 'conda_python',
executable: undefined, // this is intentionally set to test env with no executable
kind: 'conda',
version: undefined, // this is intentionally set to test conda without python
prefix: '/home/user/.conda/envs/conda_python',
};

const exePath = isWindows()
? path.join('/home/user/.conda/envs/conda_python', 'python.exe')
: path.join('/home/user/.conda/envs/conda_python', 'python');

const expectedConda1: PythonEnvInfo = {
arch: Architecture.Unknown,
detailedDisplayName: "Python 3.12.0 ('conda_python')",
display: "Python 3.12.0 ('conda_python')",
distro: { org: '' },
executable: {
filename: '/home/user/.conda/envs/conda_python/python',
sysPrefix: '/home/user/.conda/envs/conda_python',
ctime: -1,
mtime: -1,
},
kind: PythonEnvKind.Conda,
location: '/home/user/.conda/envs/conda_python',
source: [],
name: 'conda_python',
type: PythonEnvType.Conda,
version: { sysVersion: '3.12.0', major: 3, minor: 12, micro: 0 },
};

const expectedConda2: PythonEnvInfo = {
arch: Architecture.Unknown,
detailedDisplayName: 'Conda Python',
display: 'Conda Python',
distro: { org: '' },
executable: {
filename: exePath,
sysPrefix: '/home/user/.conda/envs/conda_python',
ctime: -1,
mtime: -1,
},
kind: PythonEnvKind.Conda,
location: '/home/user/.conda/envs/conda_python',
source: [],
name: 'conda_python',
type: PythonEnvType.Conda,
version: { sysVersion: undefined, major: -1, minor: -1, micro: -1 },
};

setup(() => {
mockFinder = typemoq.Mock.ofType<NativePythonFinder>();

mockFinder
.setup((f) => f.categoryToKind(typemoq.It.isAny()))
.returns((category: string) => {
switch (category.toLowerCase()) {
case 'conda':
return PythonEnvKind.Conda;
case 'system':
case 'homebrew':
case 'macpythonorg':
case 'maccommandlinetools':
case 'macxcode':
case 'windowsregistry':
case 'linuxglobal':
return PythonEnvKind.System;
case 'globalpaths':
return PythonEnvKind.OtherGlobal;
case 'pyenv':
return PythonEnvKind.Pyenv;
case 'poetry':
return PythonEnvKind.Poetry;
case 'pipenv':
return PythonEnvKind.Pipenv;
case 'pyenvvirtualenv':
return PythonEnvKind.VirtualEnv;
case 'venv':
return PythonEnvKind.Venv;
case 'virtualenv':
return PythonEnvKind.VirtualEnv;
case 'virtualenvwrapper':
return PythonEnvKind.VirtualEnvWrapper;
case 'windowsstore':
return PythonEnvKind.MicrosoftStore;
default: {
return PythonEnvKind.Unknown;
}
}
});

api = nativeAPI.createNativeEnvironmentsApi(mockFinder.object);
});

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

test('Trigger refresh without resolve', async () => {
mockFinder
.setup((f) => f.refresh())
.returns(() => {
async function* generator() {
yield* [basicEnv];
}
return generator();
})
.verifiable(typemoq.Times.once());

mockFinder.setup((f) => f.resolve(typemoq.It.isAny())).verifiable(typemoq.Times.never());

await api.triggerRefresh();
const actual = api.getEnvs();
assert.deepEqual(actual, [expectedBasicEnv]);
});

test('Trigger refresh with resolve', async () => {
mockFinder
.setup((f) => f.refresh())
.returns(() => {
async function* generator() {
yield* [basicEnv2];
}
return generator();
})
.verifiable(typemoq.Times.once());

mockFinder
.setup((f) => f.resolve(typemoq.It.isAny()))
.returns(() => Promise.resolve(basicEnv))
.verifiable(typemoq.Times.once());

api.triggerRefresh();
await api.getRefreshPromise();

const actual = api.getEnvs();
assert.deepEqual(actual, [expectedBasicEnv]);
});

test('Trigger refresh and use refresh promise API', async () => {
mockFinder
.setup((f) => f.refresh())
.returns(() => {
async function* generator() {
yield* [basicEnv];
}
return generator();
})
.verifiable(typemoq.Times.once());

mockFinder.setup((f) => f.resolve(typemoq.It.isAny())).verifiable(typemoq.Times.never());

api.triggerRefresh();
await api.getRefreshPromise();

const actual = api.getEnvs();
assert.deepEqual(actual, [expectedBasicEnv]);
});

test('Conda environment with resolve', async () => {
mockFinder
.setup((f) => f.refresh())
.returns(() => {
async function* generator() {
yield* [conda1];
}
return generator();
})
.verifiable(typemoq.Times.once());
mockFinder
.setup((f) => f.resolve(typemoq.It.isAny()))
.returns(() => Promise.resolve(conda))
.verifiable(typemoq.Times.once());

await api.triggerRefresh();
const actual = api.getEnvs();
assert.deepEqual(actual, [expectedConda1]);
});

test('Conda environment with no python', async () => {
mockFinder
.setup((f) => f.refresh())
.returns(() => {
async function* generator() {
yield* [conda2];
}
return generator();
})
.verifiable(typemoq.Times.once());
mockFinder.setup((f) => f.resolve(typemoq.It.isAny())).verifiable(typemoq.Times.never());

await api.triggerRefresh();
const actual = api.getEnvs();
assert.deepEqual(actual, [expectedConda2]);
});

test('Refresh promise undefined after refresh', async () => {
mockFinder
.setup((f) => f.refresh())
.returns(() => {
async function* generator() {
yield* [basicEnv];
}
return generator();
})
.verifiable(typemoq.Times.once());

mockFinder.setup((f) => f.resolve(typemoq.It.isAny())).verifiable(typemoq.Times.never());

await api.triggerRefresh();
assert.isUndefined(api.getRefreshPromise());
});
});

0 comments on commit e084def

Please sign in to comment.