Skip to content

Commit ae86ae4

Browse files
authored
Fix to return env variables of interpreter that is not current interpreter (#10251)
For #10250 * Bug fix
1 parent 2a8c475 commit ae86ae4

File tree

4 files changed

+126
-33
lines changed

4 files changed

+126
-33
lines changed

news/2 Fixes/10250.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Ensure to correctly return env variables of the activated interpreter, when dealing with non-workspace interpreters.

src/client/common/logger.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,11 @@ function trace(message: string, options: LogOptions = LogOptions.None, logLevel?
290290
// tslint:disable-next-line:no-any
291291
function writeToLog(elapsedTime: number, returnValue?: any, ex?: Error) {
292292
const messagesToLog = [message];
293-
messagesToLog.push(`Class name = ${className}, completed in ${elapsedTime}ms`);
293+
messagesToLog.push(
294+
`Class name = ${className}, completed in ${elapsedTime}ms, has a ${
295+
returnValue ? 'truthy' : 'falsy'
296+
} return value`
297+
);
294298
if ((options && LogOptions.Arguments) === LogOptions.Arguments) {
295299
messagesToLog.push(argsToLogString(args));
296300
}

src/client/interpreter/activation/service.ts

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,20 @@ import '../../common/extensions';
66
import { inject, injectable } from 'inversify';
77
import * as path from 'path';
88

9+
import { IWorkspaceService } from '../../common/application/types';
910
import { PYTHON_WARNINGS } from '../../common/constants';
1011
import { LogOptions, traceDecorators, traceError, traceVerbose } from '../../common/logger';
1112
import { IPlatformService } from '../../common/platform/types';
1213
import { IProcessServiceFactory } from '../../common/process/types';
1314
import { ITerminalHelper, TerminalShellType } from '../../common/terminal/types';
1415
import { ICurrentProcess, IDisposable, Resource } from '../../common/types';
15-
import {
16-
cacheResourceSpecificInterpreterData,
17-
clearCachedResourceSpecificIngterpreterData
18-
} from '../../common/utils/decorators';
16+
import { InMemoryCache } from '../../common/utils/cacheUtils';
1917
import { OSType } from '../../common/utils/platform';
2018
import { IEnvironmentVariablesProvider } from '../../common/variables/types';
2119
import { EXTENSION_ROOT_DIR } from '../../constants';
2220
import { captureTelemetry, sendTelemetryEvent } from '../../telemetry';
2321
import { EventName } from '../../telemetry/constants';
24-
import { PythonInterpreter } from '../contracts';
22+
import { IInterpreterService, PythonInterpreter } from '../contracts';
2523
import { IEnvironmentActivationService } from './types';
2624

2725
const getEnvironmentPrefix = 'e8b39361-0157-4923-80e1-22d70d46dee6';
@@ -39,15 +37,24 @@ const defaultShells = {
3937
@injectable()
4038
export class EnvironmentActivationService implements IEnvironmentActivationService, IDisposable {
4139
private readonly disposables: IDisposable[] = [];
40+
private readonly activatedEnvVariablesCache = new Map<string, InMemoryCache<NodeJS.ProcessEnv | undefined>>();
4241
constructor(
4342
@inject(ITerminalHelper) private readonly helper: ITerminalHelper,
4443
@inject(IPlatformService) private readonly platform: IPlatformService,
4544
@inject(IProcessServiceFactory) private processServiceFactory: IProcessServiceFactory,
4645
@inject(ICurrentProcess) private currentProcess: ICurrentProcess,
46+
@inject(IWorkspaceService) private workspace: IWorkspaceService,
47+
@inject(IInterpreterService) private interpreterService: IInterpreterService,
4748
@inject(IEnvironmentVariablesProvider) private readonly envVarsService: IEnvironmentVariablesProvider
4849
) {
4950
this.envVarsService.onDidEnvironmentVariablesChange(
50-
this.onDidEnvironmentVariablesChange,
51+
() => this.activatedEnvVariablesCache.clear(),
52+
this,
53+
this.disposables
54+
);
55+
56+
this.interpreterService.onDidChangeInterpreter(
57+
() => this.activatedEnvVariablesCache.clear(),
5158
this,
5259
this.disposables
5360
);
@@ -58,11 +65,33 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi
5865
}
5966
@traceDecorators.verbose('getActivatedEnvironmentVariables', LogOptions.Arguments)
6067
@captureTelemetry(EventName.PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES, { failed: false }, true)
61-
@cacheResourceSpecificInterpreterData('ActivatedEnvironmentVariables', cacheDuration)
6268
public async getActivatedEnvironmentVariables(
6369
resource: Resource,
6470
interpreter?: PythonInterpreter,
6571
allowExceptions?: boolean
72+
): Promise<NodeJS.ProcessEnv | undefined> {
73+
// Cache key = resource + interpreter.
74+
const workspaceKey = this.workspace.getWorkspaceFolderIdentifier(resource);
75+
const interpreterPath = this.platform.isWindows ? interpreter?.path.toLowerCase() : interpreter?.path;
76+
const cacheKey = `${workspaceKey}_${interpreterPath}`;
77+
78+
if (this.activatedEnvVariablesCache.get(cacheKey)?.hasData) {
79+
return this.activatedEnvVariablesCache.get(cacheKey)!.data;
80+
}
81+
82+
// Cache only if successful, else keep trying & failing if necessary.
83+
const cache = new InMemoryCache<NodeJS.ProcessEnv | undefined>(cacheDuration, '');
84+
return this.getActivatedEnvironmentVariablesImpl(resource, interpreter, allowExceptions).then(vars => {
85+
cache.data = vars;
86+
this.activatedEnvVariablesCache.set(cacheKey, cache);
87+
return vars;
88+
});
89+
}
90+
91+
public async getActivatedEnvironmentVariablesImpl(
92+
resource: Resource,
93+
interpreter?: PythonInterpreter,
94+
allowExceptions?: boolean
6695
): Promise<NodeJS.ProcessEnv | undefined> {
6796
const shellInfo = defaultShells[this.platform.osType];
6897
if (!shellInfo) {
@@ -138,9 +167,7 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi
138167
}
139168
}
140169
}
141-
protected onDidEnvironmentVariablesChange(affectedResource: Resource) {
142-
clearCachedResourceSpecificIngterpreterData('ActivatedEnvironmentVariables', affectedResource);
143-
}
170+
144171
protected fixActivationCommands(commands: string[]): string[] {
145172
// Replace 'source ' with '. ' as that works in shell exec
146173
return commands.map(cmd => cmd.replace(/^source\s+/, '. '));

src/test/interpreters/activation/service.unit.test.ts

Lines changed: 83 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import { EOL } from 'os';
77
import * as path from 'path';
88
import { SemVer } from 'semver';
99
import { anything, capture, instance, mock, verify, when } from 'ts-mockito';
10-
import * as typemoq from 'typemoq';
11-
import { EventEmitter, Uri, workspace as workspaceType, WorkspaceConfiguration } from 'vscode';
10+
import { EventEmitter, Uri } from 'vscode';
11+
import { IWorkspaceService } from '../../../client/common/application/types';
12+
import { WorkspaceService } from '../../../client/common/application/workspace';
1213
import { PlatformService } from '../../../client/common/platform/platformService';
1314
import { IPlatformService } from '../../../client/common/platform/types';
1415
import { CurrentProcess } from '../../../client/common/process/currentProcess';
@@ -18,15 +19,14 @@ import { IProcessService, IProcessServiceFactory } from '../../../client/common/
1819
import { TerminalHelper } from '../../../client/common/terminal/helper';
1920
import { ITerminalHelper } from '../../../client/common/terminal/types';
2021
import { ICurrentProcess } from '../../../client/common/types';
21-
import { clearCache } from '../../../client/common/utils/cacheUtils';
2222
import { getNamesAndValues } from '../../../client/common/utils/enum';
2323
import { Architecture, OSType } from '../../../client/common/utils/platform';
2424
import { EnvironmentVariablesProvider } from '../../../client/common/variables/environmentVariablesProvider';
2525
import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types';
2626
import { EXTENSION_ROOT_DIR } from '../../../client/constants';
2727
import { EnvironmentActivationService } from '../../../client/interpreter/activation/service';
28-
import { InterpreterType, PythonInterpreter } from '../../../client/interpreter/contracts';
29-
import { mockedVSCodeNamespaces } from '../../vscode-mock';
28+
import { IInterpreterService, InterpreterType, PythonInterpreter } from '../../../client/interpreter/contracts';
29+
import { InterpreterService } from '../../../client/interpreter/interpreterService';
3030

3131
const getEnvironmentPrefix = 'e8b39361-0157-4923-80e1-22d70d46dee6';
3232
const defaultShells = {
@@ -45,8 +45,10 @@ suite('Interpreters Activation - Python Environment Variables', () => {
4545
let processService: IProcessService;
4646
let currentProcess: ICurrentProcess;
4747
let envVarsService: IEnvironmentVariablesProvider;
48-
let workspace: typemoq.IMock<typeof workspaceType>;
49-
48+
let workspace: IWorkspaceService;
49+
let interpreterService: IInterpreterService;
50+
let onDidChangeEnvVariables: EventEmitter<Uri | undefined>;
51+
let onDidChangeInterpreter: EventEmitter<void>;
5052
const pythonInterpreter: PythonInterpreter = {
5153
path: '/foo/bar/python.exe',
5254
version: new SemVer('3.6.6-final'),
@@ -63,29 +65,22 @@ suite('Interpreters Activation - Python Environment Variables', () => {
6365
processService = mock(ProcessService);
6466
currentProcess = mock(CurrentProcess);
6567
envVarsService = mock(EnvironmentVariablesProvider);
66-
workspace = mockedVSCodeNamespaces.workspace!;
67-
when(envVarsService.onDidEnvironmentVariablesChange).thenReturn(new EventEmitter<Uri | undefined>().event);
68+
interpreterService = mock(InterpreterService);
69+
workspace = mock(WorkspaceService);
70+
onDidChangeEnvVariables = new EventEmitter<Uri | undefined>();
71+
onDidChangeInterpreter = new EventEmitter<void>();
72+
when(envVarsService.onDidEnvironmentVariablesChange).thenReturn(onDidChangeEnvVariables.event);
73+
when(interpreterService.onDidChangeInterpreter).thenReturn(onDidChangeInterpreter.event);
6874
service = new EnvironmentActivationService(
6975
instance(helper),
7076
instance(platform),
7177
instance(processServiceFactory),
7278
instance(currentProcess),
79+
instance(workspace),
80+
instance(interpreterService),
7381
instance(envVarsService)
7482
);
75-
76-
const cfg = typemoq.Mock.ofType<WorkspaceConfiguration>();
77-
workspace
78-
.setup(w => w.getConfiguration(typemoq.It.isValue('python'), typemoq.It.isAny()))
79-
.returns(() => cfg.object);
80-
workspace.setup(w => w.workspaceFolders).returns(() => []);
81-
cfg.setup(c => c.inspect(typemoq.It.isValue('pythonPath'))).returns(() => {
82-
return { globalValue: 'GlobalValuepython' } as any;
83-
});
84-
clearCache();
8583
}
86-
teardown(() => {
87-
mockedVSCodeNamespaces.workspace!.reset();
88-
});
8984

9085
function title(resource?: Uri, interpreter?: PythonInterpreter) {
9186
return `${resource ? 'With a resource' : 'Without a resource'}${interpreter ? ' and an interpreter' : ''}`;
@@ -262,6 +257,72 @@ suite('Interpreters Activation - Python Environment Variables', () => {
262257
verify(envVarsService.getEnvironmentVariables(resource)).once();
263258
verify(processService.shellExec(anything(), anything())).once();
264259
});
260+
test('Cache Variables', async () => {
261+
const cmd = ['1', '2'];
262+
const varsFromEnv = { one: '11', two: '22', HELLO: 'xxx' };
263+
const stdout = `${getEnvironmentPrefix}${EOL}${JSON.stringify(varsFromEnv)}`;
264+
when(platform.osType).thenReturn(osType.value);
265+
when(
266+
helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter)
267+
).thenResolve(cmd);
268+
when(processServiceFactory.create(resource)).thenResolve(instance(processService));
269+
when(envVarsService.getEnvironmentVariables(resource)).thenResolve({});
270+
when(processService.shellExec(anything(), anything())).thenResolve({ stdout: stdout });
271+
272+
const env = await service.getActivatedEnvironmentVariables(resource, interpreter);
273+
const env2 = await service.getActivatedEnvironmentVariables(resource, interpreter);
274+
const env3 = await service.getActivatedEnvironmentVariables(resource, interpreter);
275+
276+
expect(env).to.deep.equal(varsFromEnv);
277+
// All same objects.
278+
expect(env)
279+
.to.equal(env2)
280+
.to.equal(env3);
281+
282+
// All methods invoked only once.
283+
verify(
284+
helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter)
285+
).once();
286+
verify(processServiceFactory.create(resource)).once();
287+
verify(envVarsService.getEnvironmentVariables(resource)).once();
288+
verify(processService.shellExec(anything(), anything())).once();
289+
});
290+
async function testClearingCache(bustCache: Function) {
291+
const cmd = ['1', '2'];
292+
const varsFromEnv = { one: '11', two: '22', HELLO: 'xxx' };
293+
const stdout = `${getEnvironmentPrefix}${EOL}${JSON.stringify(varsFromEnv)}`;
294+
when(platform.osType).thenReturn(osType.value);
295+
when(
296+
helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter)
297+
).thenResolve(cmd);
298+
when(processServiceFactory.create(resource)).thenResolve(instance(processService));
299+
when(envVarsService.getEnvironmentVariables(resource)).thenResolve({});
300+
when(processService.shellExec(anything(), anything())).thenResolve({ stdout: stdout });
301+
302+
const env = await service.getActivatedEnvironmentVariables(resource, interpreter);
303+
bustCache();
304+
const env2 = await service.getActivatedEnvironmentVariables(resource, interpreter);
305+
306+
expect(env).to.deep.equal(varsFromEnv);
307+
// Objects are different (not same reference).
308+
expect(env).to.not.equal(env2);
309+
// However variables are the same.
310+
expect(env).to.deep.equal(env2);
311+
312+
// All methods invoked twice as cache was blown.
313+
verify(
314+
helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter)
315+
).twice();
316+
verify(processServiceFactory.create(resource)).twice();
317+
verify(envVarsService.getEnvironmentVariables(resource)).twice();
318+
verify(processService.shellExec(anything(), anything())).twice();
319+
}
320+
test('Cache Variables get cleared when changing interpreter', async () => {
321+
await testClearingCache(onDidChangeInterpreter.fire.bind(onDidChangeInterpreter));
322+
});
323+
test('Cache Variables get cleared when changing env variables file', async () => {
324+
await testClearingCache(onDidChangeEnvVariables.fire.bind(onDidChangeEnvVariables));
325+
});
265326
});
266327
});
267328
});

0 commit comments

Comments
 (0)