diff --git a/Extension/.scripts/vscode.ts b/Extension/.scripts/vscode.ts index de22ac20b..18bd8bbc3 100644 --- a/Extension/.scripts/vscode.ts +++ b/Extension/.scripts/vscode.ts @@ -4,13 +4,12 @@ * ------------------------------------------------------------------------------------------ */ import { downloadAndUnzipVSCode, resolveCliArgsFromVSCodeExecutablePath } from '@vscode/test-electron'; -import { createHash } from 'crypto'; -import { tmpdir } from 'os'; import { resolve } from 'path'; import { verbose } from '../src/Utility/Text/streams'; import { mkdir, readJson, rimraf, write } from './common'; +import { getVSCodeTestIsolate } from './vscodeTestPath'; -export const isolated = resolve(tmpdir(), '.vscode-test', createHash('sha256').update(__dirname).digest('hex').substring(0, 6)); +export const isolated = getVSCodeTestIsolate(__dirname); export const extensionsDir = resolve(isolated, 'extensions'); export const userDir = resolve(isolated, 'user-data'); export const settings = resolve(userDir, "User", 'settings.json'); diff --git a/Extension/.scripts/vscodeTestPath.ts b/Extension/.scripts/vscodeTestPath.ts new file mode 100644 index 000000000..2ce8559da --- /dev/null +++ b/Extension/.scripts/vscodeTestPath.ts @@ -0,0 +1,51 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +import { createHash } from 'crypto'; +import { homedir } from 'os'; +import { posix, win32 } from 'path'; + +function isFullyQualifiedPath(value: string, platform: NodeJS.Platform): boolean { + const path = platform === 'win32' ? win32 : posix; + return path.isAbsolute(value) && (platform !== 'win32' || path.parse(value).root.length > 1); +} + +export function getVSCodeTestIsolate( + scriptDirectory: string, + platform: NodeJS.Platform = process.platform, + environment: NodeJS.ProcessEnv = process.env, + homeDirectory: string = homedir()): string { + const path = platform === 'win32' ? win32 : posix; + const override = environment.CPPTOOLS_VSCODE_TEST_ROOT; + let root: string; + + if (override) { + if (!isFullyQualifiedPath(override, platform)) { + throw new Error('CPPTOOLS_VSCODE_TEST_ROOT must be a fully qualified absolute path.'); + } + root = override; + } else { + switch (platform) { + case 'win32': { + const localAppData = environment.LOCALAPPDATA; + const cacheDirectory = localAppData && isFullyQualifiedPath(localAppData, platform) ? localAppData : path.resolve(homeDirectory, 'AppData', 'Local'); + root = path.resolve(cacheDirectory, 'Microsoft', 'vscode-cpptools', 'vscode-test'); + break; + } + case 'darwin': + root = path.resolve(homeDirectory, 'Library', 'Caches', 'vscode-cpptools', 'vscode-test'); + break; + default: { + const xdgCacheHome = environment.XDG_CACHE_HOME; + const cacheDirectory = xdgCacheHome && isFullyQualifiedPath(xdgCacheHome, platform) ? xdgCacheHome : path.resolve(homeDirectory, '.cache'); + root = path.resolve(cacheDirectory, 'vscode-cpptools', 'vscode-test'); + break; + } + } + } + + const worktreeHash = createHash('sha256').update(scriptDirectory).digest('hex').substring(0, 6); + return path.resolve(root, worktreeHash); +} diff --git a/Extension/readme.developer.md b/Extension/readme.developer.md index a145707ec..d4f2ae342 100644 --- a/Extension/readme.developer.md +++ b/Extension/readme.developer.md @@ -81,17 +81,33 @@ The scripts for this repository now support running VS Code and the extension in completely isolated environment (separate install of VS Code, private extensions and user folders, etc). -The scripts that install VS Code place it in a `$ENV:TMP/.vscode-test/` folder where -`` is a has calculated from the extension folder (this permits multiple checkouts of -the source repository and each gets it's own isolated environment). +The scripts that install VS Code retain it in a per-user cache directory: + +* Windows: `%LOCALAPPDATA%\Microsoft\vscode-cpptools\vscode-test\` (or + `%USERPROFILE%\AppData\Local\Microsoft\vscode-cpptools\vscode-test\` if `%LOCALAPPDATA%` is unavailable) +* macOS: `~/Library/Caches/vscode-cpptools/vscode-test/` +* Linux: `${XDG_CACHE_HOME:-~/.cache}/vscode-cpptools/vscode-test/` + +The environment-based cache locations are used only when they are fully qualified. On Windows, +they must include a drive or UNC share. Unsupported values fall back to the per-user locations shown above. + +`` is a six-character hash calculated from the checkout's `.scripts` directory path. This permits multiple +checkouts of the source repository, with each checkout retaining its own isolated `cache`, +`extensions`, and `user-data` folders across runs. Set `CPPTOOLS_VSCODE_TEST_ROOT` to a fully qualified +absolute directory to override the platform-specific `vscode-test` root. The same Windows drive or UNC +share requirement applies. The checkout-specific `` is still appended to the override. The [`test scripts`](#yarn-test) will automatically install and use this isolated environment. You can invoke VS Code from the command line using the [`yarn code`](#yarn-code) script. -If you want to remove the isolate environment use the `yarn code reset` or `yarn test reset` scripts -to delete the folders and remove all of the configuration files. Next time you use the `yarn test` or -`yarn code` commands, it will reinstall a fresh isolated environment. +If you want to remove the isolated environment use the `yarn code reset` or `yarn test reset` scripts +to delete only the current checkout's hashed folder and remove all of its configuration files. Next +time you use the `yarn test` or `yarn code` commands, it will reinstall a fresh isolated environment. + +Isolates created by earlier versions under the system temporary directory are not migrated or +removed automatically. After ensuring that no test runs are using them, you can remove the old +`.vscode-test` folder from the system temporary directory once to reclaim that space. The Isolated environment has the theme automatically set to blue so that it is visually distinct from your normal VS Code environment. diff --git a/Extension/test/unit/vscodeTestPath.test.ts b/Extension/test/unit/vscodeTestPath.test.ts new file mode 100644 index 000000000..bbd5b7978 --- /dev/null +++ b/Extension/test/unit/vscodeTestPath.test.ts @@ -0,0 +1,102 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +import * as assert from 'assert'; +import { createHash } from 'crypto'; +import { describe, it } from 'mocha'; +import { posix, win32 } from 'path'; +import { getVSCodeTestIsolate } from '../../.scripts/vscodeTestPath'; + +const posixScriptDirectory = '/worktrees/agent/Extension/.scripts'; +const windowsScriptDirectory = 'C:\\worktrees\\agent\\Extension\\.scripts'; + +function getWorktreeHash(scriptDirectory: string): string { + return createHash('sha256').update(scriptDirectory).digest('hex').substring(0, 6); +} + +describe('VS Code test isolate path', () => { + it('uses XDG_CACHE_HOME on Linux', () => { + assert.strictEqual( + getVSCodeTestIsolate(posixScriptDirectory, 'linux', { XDG_CACHE_HOME: '/cache' }, '/home/developer'), + posix.resolve('/cache', 'vscode-cpptools', 'vscode-test', getWorktreeHash(posixScriptDirectory))); + }); + + it('falls back to the user cache directory on Linux', () => { + const expected = posix.resolve('/home/developer', '.cache', 'vscode-cpptools', 'vscode-test', getWorktreeHash(posixScriptDirectory)); + + assert.strictEqual( + getVSCodeTestIsolate(posixScriptDirectory, 'linux', {}, '/home/developer'), + expected); + assert.strictEqual( + getVSCodeTestIsolate(posixScriptDirectory, 'linux', { XDG_CACHE_HOME: 'relative-cache' }, '/home/developer'), + expected); + }); + + it('uses the user cache directory on macOS', () => { + assert.strictEqual( + getVSCodeTestIsolate(posixScriptDirectory, 'darwin', {}, '/Users/developer'), + posix.resolve('/Users/developer', 'Library', 'Caches', 'vscode-cpptools', 'vscode-test', getWorktreeHash(posixScriptDirectory))); + }); + + it('uses LOCALAPPDATA on Windows', () => { + assert.strictEqual( + getVSCodeTestIsolate(windowsScriptDirectory, 'win32', { LOCALAPPDATA: 'D:\\LocalAppData' }, 'C:\\Users\\developer'), + win32.resolve('D:\\LocalAppData', 'Microsoft', 'vscode-cpptools', 'vscode-test', getWorktreeHash(windowsScriptDirectory))); + }); + + it('falls back to the user profile on Windows', () => { + const expected = win32.resolve('C:\\Users\\developer', 'AppData', 'Local', 'Microsoft', 'vscode-cpptools', 'vscode-test', getWorktreeHash(windowsScriptDirectory)); + + assert.strictEqual( + getVSCodeTestIsolate(windowsScriptDirectory, 'win32', {}, 'C:\\Users\\developer'), + expected); + assert.strictEqual( + getVSCodeTestIsolate(windowsScriptDirectory, 'win32', { LOCALAPPDATA: 'relative-cache' }, 'C:\\Users\\developer'), + expected); + assert.strictEqual( + getVSCodeTestIsolate(windowsScriptDirectory, 'win32', { LOCALAPPDATA: '\\relative-cache' }, 'C:\\Users\\developer'), + expected); + }); + + it('honors CPPTOOLS_VSCODE_TEST_ROOT without sharing worktree isolates', () => { + const environment = { CPPTOOLS_VSCODE_TEST_ROOT: '/test-root', XDG_CACHE_HOME: '/cache' }; + const first = getVSCodeTestIsolate('/worktrees/first/Extension/.scripts', 'linux', environment, '/home/developer'); + const firstAgain = getVSCodeTestIsolate('/worktrees/first/Extension/.scripts', 'linux', environment, '/home/developer'); + const second = getVSCodeTestIsolate('/worktrees/second/Extension/.scripts', 'linux', environment, '/home/developer'); + + assert.strictEqual(first, firstAgain); + assert.match(posix.basename(first), /^[0-9a-f]{6}$/); + assert.notStrictEqual(first, second); + assert.strictEqual(posix.dirname(first), '/test-root'); + assert.strictEqual(posix.dirname(second), '/test-root'); + }); + + it('accepts fully qualified Windows CPPTOOLS_VSCODE_TEST_ROOT values', () => { + const driveRoot = 'D:\\test-root'; + const uncRoot = '\\\\server\\share\\test-root'; + + assert.strictEqual( + getVSCodeTestIsolate(windowsScriptDirectory, 'win32', { CPPTOOLS_VSCODE_TEST_ROOT: driveRoot }, 'C:\\Users\\developer'), + win32.resolve(driveRoot, getWorktreeHash(windowsScriptDirectory))); + assert.strictEqual( + getVSCodeTestIsolate(windowsScriptDirectory, 'win32', { CPPTOOLS_VSCODE_TEST_ROOT: uncRoot }, 'C:\\Users\\developer'), + win32.resolve(uncRoot, getWorktreeHash(windowsScriptDirectory))); + }); + + it('rejects non-fully-qualified CPPTOOLS_VSCODE_TEST_ROOT values', () => { + assert.throws( + () => getVSCodeTestIsolate(posixScriptDirectory, 'linux', { CPPTOOLS_VSCODE_TEST_ROOT: 'test-root' }, '/home/developer'), + /CPPTOOLS_VSCODE_TEST_ROOT must be a fully qualified absolute path/); + assert.throws( + () => getVSCodeTestIsolate(windowsScriptDirectory, 'win32', { CPPTOOLS_VSCODE_TEST_ROOT: 'C:' }, 'C:\\Users\\developer'), + /CPPTOOLS_VSCODE_TEST_ROOT must be a fully qualified absolute path/); + assert.throws( + () => getVSCodeTestIsolate(windowsScriptDirectory, 'win32', { CPPTOOLS_VSCODE_TEST_ROOT: '\\test-root' }, 'C:\\Users\\developer'), + /CPPTOOLS_VSCODE_TEST_ROOT must be a fully qualified absolute path/); + assert.throws( + () => getVSCodeTestIsolate(windowsScriptDirectory, 'win32', { CPPTOOLS_VSCODE_TEST_ROOT: '/test-root' }, 'C:\\Users\\developer'), + /CPPTOOLS_VSCODE_TEST_ROOT must be a fully qualified absolute path/); + }); +});