mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-10 08:45:56 +01:00
146 lines
5.7 KiB
TypeScript
146 lines
5.7 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import assert from 'assert';
|
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs';
|
|
import { tmpdir } from 'os';
|
|
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
|
|
import { CancellationError } from '../../../../base/common/errors.js';
|
|
import { DisposableStore } from '../../../../base/common/lifecycle.js';
|
|
import { join } from '../../../../base/common/path.js';
|
|
import { URI } from '../../../../base/common/uri.js';
|
|
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
|
|
import { NullLogService } from '../../../log/common/log.js';
|
|
import { AgentHostWorkspaceFiles } from '../../node/agentHostWorkspaceFiles.js';
|
|
|
|
suite('AgentHostWorkspaceFiles', () => {
|
|
|
|
const disposables = new DisposableStore();
|
|
const tempDirs: string[] = [];
|
|
|
|
function createTempDir(): string {
|
|
const dir = mkdtempSync(`${tmpdir()}/ahp-files-`);
|
|
tempDirs.push(dir);
|
|
return dir;
|
|
}
|
|
|
|
teardown(async () => {
|
|
disposables.clear();
|
|
// On Windows, ripgrep handles may take a tick to release after
|
|
// dispose() kills the child process. Retry rmSync rather than
|
|
// failing on transient EBUSY.
|
|
for (const dir of tempDirs) {
|
|
let lastErr: unknown;
|
|
for (let i = 0; i < 10; i++) {
|
|
try {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
lastErr = undefined;
|
|
break;
|
|
} catch (err) {
|
|
lastErr = err;
|
|
await new Promise(r => setTimeout(r, 50));
|
|
}
|
|
}
|
|
if (lastErr) {
|
|
throw lastErr;
|
|
}
|
|
}
|
|
tempDirs.length = 0;
|
|
});
|
|
|
|
ensureNoDisposablesAreLeakedInTestSuite();
|
|
|
|
test('enumerates files in the working directory', async () => {
|
|
const dir = createTempDir();
|
|
writeFileSync(join(dir, 'a.txt'), 'a');
|
|
mkdirSync(join(dir, 'sub'));
|
|
writeFileSync(join(dir, 'sub', 'b.txt'), 'b');
|
|
|
|
const files = disposables.add(new AgentHostWorkspaceFiles(new NullLogService()));
|
|
const result = await files.getFiles(URI.file(dir), CancellationToken.None);
|
|
const names = result.map(uri => uri.path).sort();
|
|
|
|
assert.ok(names.some(p => p.endsWith('/a.txt')), `expected a.txt in ${names.join(',')}`);
|
|
assert.ok(names.some(p => p.endsWith('/sub/b.txt')), `expected sub/b.txt in ${names.join(',')}`);
|
|
});
|
|
|
|
test('respects .gitignore', async () => {
|
|
const dir = createTempDir();
|
|
writeFileSync(join(dir, '.gitignore'), 'ignored.txt\n');
|
|
writeFileSync(join(dir, 'kept.txt'), 'k');
|
|
writeFileSync(join(dir, 'ignored.txt'), 'i');
|
|
|
|
const files = disposables.add(new AgentHostWorkspaceFiles(new NullLogService()));
|
|
const result = await files.getFiles(URI.file(dir), CancellationToken.None);
|
|
const names = result.map(uri => uri.path);
|
|
|
|
assert.ok(names.some(p => p.endsWith('/kept.txt')));
|
|
assert.ok(!names.some(p => p.endsWith('/ignored.txt')), `ignored.txt should not be listed: ${names.join(',')}`);
|
|
});
|
|
|
|
test('excludes the .git directory', async () => {
|
|
const dir = createTempDir();
|
|
writeFileSync(join(dir, 'a.txt'), 'a');
|
|
mkdirSync(join(dir, '.git'));
|
|
writeFileSync(join(dir, '.git', 'HEAD'), 'ref: refs/heads/main');
|
|
|
|
const files = disposables.add(new AgentHostWorkspaceFiles(new NullLogService()));
|
|
const result = await files.getFiles(URI.file(dir), CancellationToken.None);
|
|
const names = result.map(uri => uri.path);
|
|
|
|
assert.ok(names.some(p => p.endsWith('/a.txt')));
|
|
assert.ok(!names.some(p => p.includes('/.git/')), `.git contents should be excluded: ${names.join(',')}`);
|
|
});
|
|
|
|
test('returns [] for non-file URIs', async () => {
|
|
const files = disposables.add(new AgentHostWorkspaceFiles(new NullLogService()));
|
|
const result = await files.getFiles(URI.parse('vscode-vfs://github/foo/bar'), CancellationToken.None);
|
|
assert.deepStrictEqual(result, []);
|
|
});
|
|
|
|
test('caches concurrent calls for the same working directory', async () => {
|
|
const dir = createTempDir();
|
|
writeFileSync(join(dir, 'a.txt'), 'a');
|
|
|
|
const files = disposables.add(new AgentHostWorkspaceFiles(new NullLogService()));
|
|
const wd = URI.file(dir);
|
|
const [r1, r2] = await Promise.all([
|
|
files.getFiles(wd, CancellationToken.None),
|
|
files.getFiles(wd, CancellationToken.None),
|
|
]);
|
|
assert.strictEqual(r1, r2, 'concurrent calls should share the same promise / result array');
|
|
});
|
|
|
|
test('rejects with CancellationError on cancellation', async () => {
|
|
const dir = createTempDir();
|
|
writeFileSync(join(dir, 'a.txt'), 'a');
|
|
|
|
const files = disposables.add(new AgentHostWorkspaceFiles(new NullLogService()));
|
|
const cts = new CancellationTokenSource();
|
|
const promise = files.getFiles(URI.file(dir), cts.token);
|
|
cts.cancel();
|
|
await assert.rejects(promise, (err: unknown) => err instanceof CancellationError);
|
|
cts.dispose();
|
|
});
|
|
|
|
test('cancelling one caller does not poison concurrent callers sharing the cache', async () => {
|
|
const dir = createTempDir();
|
|
writeFileSync(join(dir, 'a.txt'), 'a');
|
|
|
|
const files = disposables.add(new AgentHostWorkspaceFiles(new NullLogService()));
|
|
const wd = URI.file(dir);
|
|
|
|
const cts = new CancellationTokenSource();
|
|
const cancelled = files.getFiles(wd, cts.token);
|
|
const survivor = files.getFiles(wd, CancellationToken.None);
|
|
cts.cancel();
|
|
cts.dispose();
|
|
|
|
await assert.rejects(cancelled, (err: unknown) => err instanceof CancellationError);
|
|
const result = await survivor;
|
|
assert.ok(result.some(uri => uri.path.endsWith('/a.txt')), `survivor should resolve with files even when first caller cancelled: ${result.map(u => u.path).join(',')}`);
|
|
});
|
|
});
|