Skip to content

Commit 4ea4085

Browse files
authored
Merge branch 'microsoft:main' into conda-fix
2 parents e5c983f + c6b1e27 commit 4ea4085

4 files changed

Lines changed: 210 additions & 12 deletions

File tree

‎src/common/inlineScript/cacheLayout.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,10 +151,13 @@ export async function restoreMetaJsonBackupUnderLock(
151151
}
152152

153153
const validBackups: Array<{ readonly path: string; readonly metadata: InlineScriptEnvMeta }> = [];
154+
let hasUnsupportedBackup = false;
154155
for (const entry of entries.filter((name) => META_JSON_BACKUP_FILENAME_RE.test(name))) {
155156
const result = await inspectMetaJsonFile(path.join(envDir.fsPath, entry));
156157
if (result.kind === 'valid' && isCompatible(result.metadata)) {
157158
validBackups.push({ path: path.join(envDir.fsPath, entry), metadata: result.metadata });
159+
} else if (result.kind === 'unsupported') {
160+
hasUnsupportedBackup = true;
158161
} else if (result.kind === 'unavailable' || result.kind === 'missing') {
159162
// A listed candidate changing or becoming unreadable is an
160163
// uncertain scan; preserve the entry rather than rebuilding it.
@@ -163,7 +166,7 @@ export async function restoreMetaJsonBackupUnderLock(
163166
}
164167

165168
if (validBackups.length === 0) {
166-
return { kind: 'missing' };
169+
return { kind: hasUnsupportedBackup ? 'unsupported' : 'missing' };
167170
}
168171

169172
// `lastUsedAt` is schema-validated canonical ISO text. Prefer the newest

‎src/features/envManagers.ts‎

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
import {
1919
InlineScriptRouteabilityChangeEvent,
2020
InlineScriptRoutingRegistry,
21+
getInlineScriptRoutingKey,
2122
} from '../common/inlineScript/routingRegistry';
2223
import { traceError, traceVerbose } from '../common/logging';
2324
import { StopWatch } from '../common/stopWatch';
@@ -420,19 +421,29 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
420421
}
421422

422423
if (scope instanceof Uri) {
423-
this.updateInlineRoutingOverride(scope, manager, environment);
424-
this.clearInlineActiveSelection(scope, manager, inlineClearOperation);
425424
if (
426-
clearingInlineRoutingOverride &&
427-
(await this.publishEffectiveEnvironmentAfterOverrideClear(
425+
this.commitInlineRoutingOperation(
428426
scope,
429427
manager,
430-
key,
431428
operation,
429+
inlineClearOperation,
432430
inlineOverrideHandoffOperation,
433-
))
431+
)
434432
) {
435-
return;
433+
this.updateInlineRoutingOverride(scope, manager, environment);
434+
this.clearInlineActiveSelection(scope, manager, inlineOverrideHandoffOperation ?? inlineClearOperation);
435+
if (
436+
clearingInlineRoutingOverride &&
437+
(await this.publishEffectiveEnvironmentAfterOverrideClear(
438+
scope,
439+
manager,
440+
key,
441+
operation,
442+
inlineOverrideHandoffOperation,
443+
))
444+
) {
445+
return;
446+
}
436447
}
437448
}
438449
if (!publishInlineSelection) {
@@ -511,8 +522,20 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
511522
await setAllManagerSettings(settings);
512523
}
513524
selections.forEach((selection) => {
514-
this.updateInlineRoutingOverride(selection.scope, manager, environment);
515-
this.clearInlineActiveSelection(selection.scope, manager, selection.inlineClearOperation);
525+
// Only a current (non-superseded) PEP 723 routing operation may mutate the
526+
// per-script override; the ordinary project/global selection lane below is
527+
// committed independently so a stale inline op cannot suppress it.
528+
if (
529+
this.commitInlineRoutingOperation(
530+
selection.scope,
531+
manager,
532+
selection.operation,
533+
selection.inlineClearOperation,
534+
)
535+
) {
536+
this.updateInlineRoutingOverride(selection.scope, manager, environment);
537+
this.clearInlineActiveSelection(selection.scope, manager, selection.inlineClearOperation);
538+
}
516539
if (!selection.publishInlineSelection) {
517540
return;
518541
}
@@ -932,6 +955,28 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
932955
}
933956
}
934957

958+
private commitInlineRoutingOperation(
959+
scope: Uri,
960+
manager: InternalEnvironmentManager,
961+
selectionOperation: number,
962+
inlineClearOperation?: number,
963+
inlineOverrideHandoffOperation?: number,
964+
): boolean {
965+
// Gate strictly to the manually enabled PEP 723 routing feature and to file .py scopes.
966+
// For the feature-off or non-script case, proceed exactly as before.
967+
if (!this.inlineScriptRouting || getInlineScriptRoutingKey(scope) === undefined) {
968+
return true;
969+
}
970+
const operation =
971+
manager.id === INLINE_SCRIPT_MANAGER_ID
972+
? selectionOperation
973+
: (inlineOverrideHandoffOperation ?? inlineClearOperation);
974+
return (
975+
operation === undefined ||
976+
this.commitSelectionOperation(this.getInlineScriptSelectionKey(scope), operation)
977+
);
978+
}
979+
935980
private canPersistManagerSettingForScope(
936981
scope: Uri,
937982
manager: InternalEnvironmentManager,

‎src/test/common/inlineScript/cacheLayout.unit.test.ts‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -330,18 +330,29 @@ suite('inlineScriptCacheLayout', () => {
330330
assert.strictEqual(await fs.pathExists(getMetaJsonPath(envDir).fsPath), false);
331331
});
332332

333-
test('rejects temp, malformed, unsupported, and oversized artifacts without restoring them', async () => {
333+
test('rejects temp, malformed, and oversized artifacts without restoring them', async () => {
334334
const finalPath = getMetaJsonPath(envDir).fsPath;
335335
await fs.writeFile(`${finalPath}.tmp-abcdef123456`, JSON.stringify(makeMeta()));
336336
await fs.writeFile(`${finalPath}.backup-ABCDEF123456`, JSON.stringify(makeMeta()));
337337
await writeBackup('111111111111', 'not json');
338-
await writeBackup('222222222222', JSON.stringify({ ...makeMeta(), schemaVersion: 99 }));
339338
await writeBackup('333333333333', Buffer.alloc(1024 * 1024 + 1, 0x20));
340339

341340
assert.deepStrictEqual(await restoreMetaJsonBackupUnderLock(envDir), { kind: 'missing' });
342341
assert.strictEqual(await fs.pathExists(finalPath), false);
343342
});
344343

344+
test('preserves a future-schema backup as unsupported when the primary is absent', async () => {
345+
const finalPath = getMetaJsonPath(envDir).fsPath;
346+
const backup = backupPath('222222222222');
347+
await writeBackup('111111111111', 'not json');
348+
await writeBackup('222222222222', JSON.stringify({ ...makeMeta(), schemaVersion: 99 }));
349+
await writeBackup('333333333333', Buffer.alloc(1024 * 1024 + 1, 0x20));
350+
351+
assert.deepStrictEqual(await restoreMetaJsonBackupUnderLock(envDir), { kind: 'unsupported' });
352+
assert.strictEqual(await fs.pathExists(finalPath), false);
353+
assert.strictEqual(await fs.pathExists(backup), true);
354+
});
355+
345356
test('rejects a symlink backup without restoring it', async function () {
346357
const externalPath = path.join(tmpDir, 'external-meta.json');
347358
await fs.writeFile(externalPath, JSON.stringify(makeMeta()));

‎src/test/features/envManagers.lastKnown.unit.test.ts‎

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,145 @@ suite('PythonEnvironmentManagers getLastKnownEnvironment', () => {
299299
assert.deepStrictEqual(events.map((event) => event.new), [second]);
300300
});
301301

302+
test('does not let an older non-inline selection install an override after a newer inline selection', async () => {
303+
const script = Uri.file('/workspace/project/script.py');
304+
projectsByUri.set(script.toString(), { name: 'project', uri: Uri.file('/workspace/project') });
305+
let releaseOlderSelection: (() => void) | undefined;
306+
let signalOlderSelection: (() => void) | undefined;
307+
const olderSelectionStarted = new Promise<void>((resolve) => {
308+
signalOlderSelection = resolve;
309+
});
310+
const olderSelectionGate = new Promise<void>((resolve) => {
311+
releaseOlderSelection = resolve;
312+
});
313+
const selectedSet = sinon.stub().callsFake(async () => {
314+
signalOlderSelection!();
315+
await olderSelectionGate;
316+
});
317+
let selectedEnvironment: PythonEnvironment;
318+
const selectedId = registerManager(async () => selectedEnvironment, selectedSet, 'venv');
319+
let inlineEnvironment: PythonEnvironment;
320+
const inlineId = registerManager(async () => inlineEnvironment, async () => undefined, 'inline-script');
321+
selectedEnvironment = {
322+
...makeEnv('selected'),
323+
envId: { id: 'selected', managerId: selectedId },
324+
};
325+
inlineEnvironment = {
326+
...makeEnv('inline'),
327+
envId: { id: 'inline', managerId: inlineId },
328+
};
329+
defaultManagerId = selectedId;
330+
markInlineScript(script);
331+
const events: DidChangeEnvironmentEventArgs[] = [];
332+
envManagers.onDidChangeActiveEnvironment((event) => events.push(event));
333+
334+
const olderSelection = envManagers.setEnvironment(script, selectedEnvironment, false);
335+
await olderSelectionStarted;
336+
await envManagers.setEnvironment(script, inlineEnvironment, false);
337+
releaseOlderSelection!();
338+
await olderSelection;
339+
340+
// The stale non-inline selection must not hijack the script's PEP 723 routing…
341+
assert.strictEqual(envManagers.getEnvironmentManager(script)?.id, inlineId);
342+
assert.strictEqual(envManagers.getLastKnownEnvironment(script), inlineEnvironment);
343+
assert.ok(events.some((event) => event.new === inlineEnvironment));
344+
// …but its ordinary containing-project selection lane is independent and is not suppressed.
345+
assert.ok(events.some((event) => event.new === selectedEnvironment));
346+
});
347+
348+
test('does not let an older batch inline selection clear a newer non-inline override', async () => {
349+
const script = Uri.file('/workspace/project/script.py');
350+
projectsByUri.set(script.toString(), { name: 'project', uri: Uri.file('/workspace/project') });
351+
let releaseOlderBatch: (() => void) | undefined;
352+
let signalOlderBatch: (() => void) | undefined;
353+
const olderBatchStarted = new Promise<void>((resolve) => {
354+
signalOlderBatch = resolve;
355+
});
356+
const olderBatchGate = new Promise<void>((resolve) => {
357+
releaseOlderBatch = resolve;
358+
});
359+
let selectedEnvironment: PythonEnvironment;
360+
const selectedId = registerManager(async () => selectedEnvironment, async () => undefined, 'venv');
361+
const inlineSet = sinon.stub().callsFake(async () => {
362+
signalOlderBatch!();
363+
await olderBatchGate;
364+
});
365+
let inlineEnvironment: PythonEnvironment;
366+
const inlineId = registerManager(async () => inlineEnvironment, inlineSet, 'inline-script');
367+
selectedEnvironment = {
368+
...makeEnv('selected'),
369+
envId: { id: 'selected', managerId: selectedId },
370+
};
371+
inlineEnvironment = {
372+
...makeEnv('inline'),
373+
envId: { id: 'inline', managerId: inlineId },
374+
};
375+
defaultManagerId = selectedId;
376+
markInlineScript(script);
377+
const events: DidChangeEnvironmentEventArgs[] = [];
378+
envManagers.onDidChangeActiveEnvironment((event) => events.push(event));
379+
380+
const olderBatch = envManagers.setEnvironments([script], inlineEnvironment, false);
381+
await olderBatchStarted;
382+
await envManagers.setEnvironment(script, selectedEnvironment, false);
383+
const eventsAfterNewerSelection = [...events];
384+
releaseOlderBatch!();
385+
await olderBatch;
386+
387+
assert.strictEqual(envManagers.getEnvironmentManager(script)?.id, selectedId);
388+
assert.strictEqual(envManagers.getLastKnownEnvironment(script), selectedEnvironment);
389+
assert.deepStrictEqual(events, eventsAfterNewerSelection);
390+
});
391+
392+
test('routes each same-project script to its own override in a batch non-inline selection', async () => {
393+
const project = { name: 'project', uri: Uri.file('/workspace/project') };
394+
const firstScript = Uri.file('/workspace/project/first.py');
395+
const secondScript = Uri.file('/workspace/project/second.py');
396+
projectsByUri.set(firstScript.toString(), project);
397+
projectsByUri.set(secondScript.toString(), project);
398+
let selectedEnvironment: PythonEnvironment;
399+
const selectedId = registerManager(async () => selectedEnvironment, async () => undefined, 'venv');
400+
registerManager(async () => undefined, async () => undefined, 'inline-script');
401+
selectedEnvironment = { ...makeEnv('selected'), envId: { id: 'selected', managerId: selectedId } };
402+
markInlineScript(firstScript);
403+
markInlineScript(secondScript);
404+
405+
await envManagers.setEnvironments([firstScript, secondScript], selectedEnvironment, false);
406+
407+
// Each script commits on its own per-file inline key, so the shared containing-project
408+
// revision cannot make the first script skip installing its routing override.
409+
assert.strictEqual(envManagers.getEnvironmentManager(firstScript)?.id, selectedId);
410+
assert.strictEqual(envManagers.getEnvironmentManager(secondScript)?.id, selectedId);
411+
});
412+
413+
test('applies a normal non-inline batch across distinct projects without a routing registry', async () => {
414+
recreateEnvManagersWithoutRouting();
415+
const projectOne = { name: 'one', uri: Uri.file('/workspace/one') };
416+
const projectTwo = { name: 'two', uri: Uri.file('/workspace/two') };
417+
projectsByUri.set(projectOne.uri.toString(), projectOne);
418+
projectsByUri.set(projectTwo.uri.toString(), projectTwo);
419+
const managerSet = sinon.stub().resolves();
420+
let selectedEnvironment: PythonEnvironment;
421+
const managerId = registerManager(async () => selectedEnvironment, managerSet, 'venv');
422+
selectedEnvironment = { ...makeEnv('selected'), envId: { id: 'selected', managerId } };
423+
const settings = sinon.stub(settingHelpers, 'setAllManagerSettings').resolves();
424+
const events: DidChangeEnvironmentEventArgs[] = [];
425+
envManagers.onDidChangeActiveEnvironment((event) => events.push(event));
426+
427+
await envManagers.setEnvironments([projectOne.uri, projectTwo.uri], selectedEnvironment);
428+
429+
assert.strictEqual(managerSet.callCount, 1);
430+
assert.deepStrictEqual(managerSet.firstCall.args[0], [projectOne.uri, projectTwo.uri]);
431+
assert.strictEqual(envManagers.getLastKnownEnvironment(projectOne.uri), selectedEnvironment);
432+
assert.strictEqual(envManagers.getLastKnownEnvironment(projectTwo.uri), selectedEnvironment);
433+
assert.deepStrictEqual(
434+
events.map((event) => event.new),
435+
[selectedEnvironment, selectedEnvironment],
436+
);
437+
assert.strictEqual(settings.callCount, 1);
438+
assert.strictEqual(settings.firstCall.args[0].length, 2);
439+
});
440+
302441
test('publishes inline environments with the same ID at different paths', async () => {
303442
const scope = Uri.file('/workspace/script.py');
304443
const managerId = registerManager(async () => undefined, async () => undefined, 'inline-script');

0 commit comments

Comments
 (0)