Skip to content

Commit 81b6018

Browse files
author
Kartik Raj
committed
Add tests
1 parent 6a2ad6d commit 81b6018

File tree

11 files changed

+117
-36
lines changed

11 files changed

+117
-36
lines changed

src/client/pythonEnvironments/common/environmentIdentifier.ts

Lines changed: 21 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,11 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
import * as fsapi from 'fs-extra';
54
import * as path from 'path';
65
import { traceWarning } from '../../common/logger';
7-
import { createDeferred } from '../../common/utils/async';
86
import { getEnvironmentVariable } from '../../common/utils/platform';
97
import { EnvironmentType } from '../info';
10-
11-
function pathExists(absPath: string): Promise<boolean> {
12-
const deferred = createDeferred<boolean>();
13-
fsapi.exists(absPath, (result) => {
14-
deferred.resolve(result);
15-
});
16-
return deferred.promise;
17-
}
18-
19-
function readFile(filePath: string): Promise<string> {
20-
return fsapi.readFile(filePath, 'utf-8');
21-
}
8+
import { pathExists, readFile } from './externalDependencies';
229

2310
/**
2411
* Checks if the given interpreter path belongs to a conda environment. Using
@@ -127,11 +114,11 @@ async function isWindowsStoreEnvironment(interpreterPath: string): Promise<boole
127114
}
128115

129116
/**
130-
* Checks if a Pipfile associated with the provided directory exists.
117+
* Returns the path to Pipfile associated with the provided directory.
131118
* @param cwd the directory to look into
132119
* @param lookIntoParentDirectories set to true if we should also search for Pipfile in parent directory
133120
*/
134-
async function checkIfPipFileExists(cwd: string, lookIntoParentDirectories: boolean): Promise<boolean> {
121+
async function getAssociatedPipfile(cwd: string, lookIntoParentDirectories: boolean): Promise<string | undefined> {
135122
const pipFileName = getEnvironmentVariable('PIPENV_PIPFILE') || 'Pipfile';
136123
let depthToSearch = 1;
137124
if (lookIntoParentDirectories) {
@@ -144,15 +131,16 @@ async function checkIfPipFileExists(cwd: string, lookIntoParentDirectories: bool
144131
depthToSearch = 3;
145132
}
146133
}
147-
while (depthToSearch > 0 && cwd === path.dirname(cwd)) {
134+
while (depthToSearch > 0 && cwd !== path.dirname(cwd)) {
135+
const pipFile = path.join(cwd, pipFileName);
148136
// eslint-disable-next-line no-await-in-loop
149-
if (await pathExists(path.join(cwd, pipFileName))) {
150-
return true;
137+
if (await pathExists(pipFile)) {
138+
return pipFile;
151139
}
152140
cwd = path.dirname(cwd);
153141
depthToSearch -= 1;
154142
}
155-
return false;
143+
return undefined;
156144
}
157145

158146
/**
@@ -173,7 +161,7 @@ async function isLocalPipenvEnvironment(interpreterPath: string): Promise<boolea
173161
return false;
174162
}
175163
const directoryWhereVenvResides = path.dirname(path.dirname(path.dirname(interpreterPath)));
176-
if (await checkIfPipFileExists(directoryWhereVenvResides, false)) {
164+
if (await getAssociatedPipfile(directoryWhereVenvResides, false)) {
177165
// The directory must contain a Pipfile
178166
return true;
179167
}
@@ -185,31 +173,33 @@ async function isLocalPipenvEnvironment(interpreterPath: string): Promise<boolea
185173
* @param interpreterPath Absolute path to any python interpreter.
186174
*/
187175
async function isGlobalPipenvEnvironment(interpreterPath: string): Promise<boolean> {
188-
// Global pipenv environments have a .project file with the absolute path to the project.
189-
// Also, the name of the project is used as a prefix in the environment folder.
176+
// Global pipenv environments have a .project file with the absolute path to the project
177+
// See https://github.com/pypa/pipenv/blob/9299ae1f7353bdd523a1829f3c7cad0ee67c2e3b/CHANGELOG.rst#L754
178+
// Also, the name of the directory where Pipfile resides is used as a prefix in the environment folder.
190179
// This is the layout we wish to verify.
191-
// <Environment folder> <--- check if the name of the project is used as a prefix
180+
// <Environment folder>
192181
// |__ .project <--- check if .project exists here
193182
// |__ Scripts/bin
194183
// |__ python <--- interpreterPath
195184
const dotProjectFile = path.join(path.dirname(path.dirname(interpreterPath)), '.project');
196-
if (!await pathExists(dotProjectFile)) {
185+
if (!(await pathExists(dotProjectFile))) {
197186
return false;
198187
}
199188

200189
const project = await readFile(dotProjectFile);
201-
if (!await pathExists(project)) {
190+
if (!(await pathExists(project))) {
202191
return false;
203192
}
204193

205-
// The name of the project is used as a prefix in the environment folder.
206-
if (interpreterPath.indexOf(`${path.sep}${path.basename(project)}-`) === -1) {
194+
// PIPENV_NO_INHERIT is used to tell pipenv not to look for Pipfile in parent directories
195+
// https://pipenv.pypa.io/en/latest/advanced/#pipenv.environments.PIPENV_NO_INHERIT
196+
const pipFile = await getAssociatedPipfile(project, !getEnvironmentVariable('PIPENV_NO_INHERIT'));
197+
if (!pipFile) {
207198
return false;
208199
}
209200

210-
// PIPENV_NO_INHERIT is used to tell pipenv not to look for Pipfile in parent directories
211-
// https://pipenv.pypa.io/en/latest/advanced/#pipenv.environments.PIPENV_NO_INHERIT
212-
if (!checkIfPipFileExists(project, !getEnvironmentVariable('PIPENV_NO_INHERIT'))) {
201+
// The name of the directory where Pipfile resides is used as a prefix in the environment folder.
202+
if (interpreterPath.indexOf(`${path.sep}${path.basename(path.dirname(pipFile))}-`) === -1) {
213203
return false;
214204
}
215205

src/client/pythonEnvironments/common/externalDependencies.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4+
import * as fsapi from 'fs-extra';
45
import { ExecutionResult, IProcessServiceFactory } from '../../common/process/types';
6+
import { createDeferred } from '../../common/utils/async';
57
import { IServiceContainer } from '../../ioc/types';
68

79
let internalServiceContainer: IServiceContainer;
@@ -17,3 +19,15 @@ export async function shellExecute(command: string, timeout: number): Promise<Ex
1719
const proc = await getProcessFactory().create();
1820
return proc.shellExec(command, { timeout });
1921
}
22+
23+
export function pathExists(absPath: string): Promise<boolean> {
24+
const deferred = createDeferred<boolean>();
25+
fsapi.exists(absPath, (result) => {
26+
deferred.resolve(result);
27+
});
28+
return deferred.promise;
29+
}
30+
31+
export function readFile(filePath: string): Promise<string> {
32+
return fsapi.readFile(filePath, 'utf-8');
33+
}

src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as path from 'path';
66
import * as sinon from 'sinon';
77
import * as platformApis from '../../../client/common/utils/platform';
88
import { identifyEnvironment } from '../../../client/pythonEnvironments/common/environmentIdentifier';
9+
import * as externalDependencies from '../../../client/pythonEnvironments/common/externalDependencies';
910
import { EnvironmentType } from '../../../client/pythonEnvironments/info';
1011

1112
suite('Environment Identifier', () => {
@@ -19,8 +20,9 @@ suite('Environment Identifier', () => {
1920
'test',
2021
'pythonEnvironments',
2122
'common',
22-
'envlayouts'
23+
'envlayouts',
2324
);
25+
let getEnvVar: sinon.SinonStub;
2426
suite('Conda', () => {
2527
test('Conda layout with conda-meta and python binary in the same directory', async () => {
2628
const interpreterPath: string = path.join(testLayoutsRoot, 'conda1', 'python.exe');
@@ -34,8 +36,46 @@ suite('Environment Identifier', () => {
3436
});
3537
});
3638

39+
suite('Pipenv', () => {
40+
let readFile: sinon.SinonStub;
41+
setup(() => {
42+
getEnvVar = sinon.stub(platformApis, 'getEnvironmentVariable');
43+
readFile = sinon.stub(externalDependencies, 'readFile');
44+
});
45+
46+
teardown(() => {
47+
readFile.restore();
48+
getEnvVar.restore();
49+
});
50+
51+
test('Path to a general global pipenv environment', async () => {
52+
const expectedDotProjectFile = path.join(testLayoutsRoot, 'pipenv', 'globalEnvironments', 'project2-vnNIWe9P', '.project');
53+
const expectedProjectFile = path.join(testLayoutsRoot, 'pipenv', 'project2');
54+
readFile.withArgs(expectedDotProjectFile).resolves(expectedProjectFile);
55+
const interpreterPath: string = path.join(testLayoutsRoot, 'pipenv', 'globalEnvironments', 'project2-vnNIWe9P', 'bin', 'python');
56+
const envType: EnvironmentType = await identifyEnvironment(interpreterPath);
57+
assert.equal(envType, EnvironmentType.Pipenv);
58+
});
59+
60+
test('Path to a global pipenv environment whose Pipfile lies at 3 levels above the project the environment is associated with', async () => {
61+
getEnvVar.withArgs('PIPENV_MAX_DEPTH').returns('5');
62+
const expectedDotProjectFile = path.join(testLayoutsRoot, 'pipenv', 'globalEnvironments', 'grandparent-2s1eXEJ2', '.project');
63+
const expectedProjectFile = path.join(testLayoutsRoot, 'pipenv', 'grandparent', 'parent', 'child', 'project3');
64+
readFile.withArgs(expectedDotProjectFile).resolves(expectedProjectFile);
65+
const interpreterPath: string = path.join(testLayoutsRoot, 'pipenv', 'globalEnvironments', 'grandparent-2s1eXEJ2', 'Scripts', 'python.exe');
66+
const envType: EnvironmentType = await identifyEnvironment(interpreterPath);
67+
assert.equal(envType, EnvironmentType.Pipenv);
68+
});
69+
70+
test('Path to a local pipenv environment with a custom Pipfile name', async () => {
71+
getEnvVar.withArgs('PIPENV_PIPFILE').returns('CustomPipfileName');
72+
const interpreterPath: string = path.join(testLayoutsRoot, 'pipenv', 'project1', '.venv', 'Scripts', 'python.exe');
73+
const envType: EnvironmentType = await identifyEnvironment(interpreterPath);
74+
assert.equal(envType, EnvironmentType.Pipenv);
75+
});
76+
});
77+
3778
suite('Windows Store', () => {
38-
let getEnvVar: sinon.SinonStub;
3979
const fakeLocalAppDataPath = 'X:\\users\\user\\AppData\\Local';
4080
const fakeProgramFilesPath = 'X:\\Program Files';
4181
const executable = ['python.exe', 'python3.exe', 'python3.8.exe'];
@@ -59,7 +99,7 @@ suite('Environment Identifier', () => {
5999
'Microsoft',
60100
'WindowsApps',
61101
'PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0',
62-
exe
102+
exe,
63103
);
64104
const envType: EnvironmentType = await identifyEnvironment(interpreterPath);
65105
assert.deepEqual(envType, EnvironmentType.WindowsStore);
@@ -69,7 +109,7 @@ suite('Environment Identifier', () => {
69109
fakeProgramFilesPath,
70110
'WindowsApps',
71111
'PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0',
72-
exe
112+
exe,
73113
);
74114
const envType: EnvironmentType = await identifyEnvironment(interpreterPath);
75115
assert.deepEqual(envType, EnvironmentType.WindowsStore);
@@ -86,7 +126,7 @@ suite('Environment Identifier', () => {
86126
fakeProgramFilesPath,
87127
'WindowsApps',
88128
'PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0',
89-
exe
129+
exe,
90130
);
91131
const envType: EnvironmentType = await identifyEnvironment(interpreterPath);
92132
assert.deepEqual(envType, EnvironmentType.WindowsStore);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Absolute path to \src\test\pythonEnvironments\common\envlayouts\pipenv\grandparent\parent\child\project3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Not real python exe
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Absolute path to \src\test\pythonEnvironments\common\envlayouts\pipenv\project2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Not a real python binary
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[[source]]
2+
name = "pypi"
3+
url = "https://pypi.org/simple"
4+
verify_ssl = true
5+
6+
[dev-packages]
7+
8+
[packages]
9+
10+
[requires]
11+
python_version = "3.7"

src/test/pythonEnvironments/common/envlayouts/pipenv/grandparent/parent/child/project3/dummyFile

Whitespace-only changes.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[[source]]
2+
name = "pypi"
3+
url = "https://pypi.org/simple"
4+
verify_ssl = true
5+
6+
[dev-packages]
7+
8+
[packages]
9+
10+
[requires]
11+
python_version = "3.7"
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[[source]]
2+
name = "pypi"
3+
url = "https://pypi.org/simple"
4+
verify_ssl = true
5+
6+
[dev-packages]
7+
8+
[packages]
9+
10+
[requires]
11+
python_version = "3.7"

0 commit comments

Comments
 (0)