Path: blob/main/extensions/git/src/test/repositoryCache.test.ts
4774 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import 'mocha';6import * as assert from 'assert';7import { RepositoryCache } from '../repositoryCache';8import { Event, EventEmitter, LogLevel, LogOutputChannel, Memento, Uri, WorkspaceFolder } from 'vscode';910class InMemoryMemento implements Memento {11private store = new Map<string, any>();1213constructor(initial?: Record<string, any>) {14if (initial) {15for (const k of Object.keys(initial)) {16this.store.set(k, initial[k]);17}18}19}2021get<T>(key: string): T | undefined;22get<T>(key: string, defaultValue: T): T;23get<T>(key: string, defaultValue?: T): T | undefined {24if (this.store.has(key)) {25return this.store.get(key);26}27return defaultValue as (T | undefined);28}2930update(key: string, value: any): Thenable<void> {31this.store.set(key, value);32return Promise.resolve();33}3435keys(): readonly string[] {36return Array.from(this.store.keys());37}38}3940class MockLogOutputChannel implements LogOutputChannel {41logLevel: LogLevel = LogLevel.Info;42onDidChangeLogLevel: Event<LogLevel> = new EventEmitter<LogLevel>().event;43trace(_message: string, ..._args: any[]): void { }44debug(_message: string, ..._args: any[]): void { }45info(_message: string, ..._args: any[]): void { }46warn(_message: string, ..._args: any[]): void { }47error(_error: string | Error, ..._args: any[]): void { }48name: string = 'MockLogOutputChannel';49append(_value: string): void { }50appendLine(_value: string): void { }51replace(_value: string): void { }52clear(): void { }53show(_column?: unknown, _preserveFocus?: unknown): void { }54hide(): void { }55dispose(): void { }56}5758class TestRepositoryCache extends RepositoryCache {59constructor(memento: Memento, logger: LogOutputChannel, private readonly _workspaceFileProp: Uri | undefined, private readonly _workspaceFoldersProp: readonly WorkspaceFolder[] | undefined) {60super(memento, logger);61}6263protected override get _workspaceFile() {64return this._workspaceFileProp;65}6667protected override get _workspaceFolders() {68return this._workspaceFoldersProp;69}70}7172suite('RepositoryCache', () => {7374test('set & get basic', () => {75const memento = new InMemoryMemento();76const folder = Uri.file('/workspace/repo');77const cache = new TestRepositoryCache(memento, new MockLogOutputChannel(), undefined, [{ uri: folder, name: 'workspace', index: 0 }]);7879cache.set('https://example.com/repo.git', folder.fsPath);80const folders = cache.get('https://example.com/repo.git')!.map(folder => folder.workspacePath);81assert.ok(folders, 'folders should be defined');82assert.deepStrictEqual(folders, [folder.fsPath]);83});8485test('inner LRU capped at 10 entries', () => {86const memento = new InMemoryMemento();87const workspaceFolders: WorkspaceFolder[] = [];88for (let i = 1; i <= 12; i++) {89workspaceFolders.push({ uri: Uri.file(`/ws/folder-${i.toString().padStart(2, '0')}`), name: `folder-${i.toString().padStart(2, '0')}`, index: i - 1 });90}91const cache = new TestRepositoryCache(memento, new MockLogOutputChannel(), undefined, workspaceFolders);92const repo = 'https://example.com/repo.git';93for (let i = 1; i <= 12; i++) {94cache.set(repo, Uri.file(`/ws/folder-${i.toString().padStart(2, '0')}`).fsPath);95}96const folders = cache.get(repo)!.map(folder => folder.workspacePath);97assert.strictEqual(folders.length, 10, 'should only retain 10 most recent folders');98assert.ok(!folders.includes(Uri.file('/ws/folder-01').fsPath), 'oldest folder-01 should be evicted');99assert.ok(!folders.includes(Uri.file('/ws/folder-02').fsPath), 'second oldest folder-02 should be evicted');100assert.ok(folders.includes(Uri.file('/ws/folder-12').fsPath), 'latest folder should be present');101});102103test('outer LRU capped at 30 repos', () => {104const memento = new InMemoryMemento();105const workspaceFolders: WorkspaceFolder[] = [];106for (let i = 1; i <= 35; i++) {107workspaceFolders.push({ uri: Uri.file(`/ws/r${i}`), name: `r${i}`, index: i - 1 });108}109const cache = new TestRepositoryCache(memento, new MockLogOutputChannel(), undefined, workspaceFolders);110for (let i = 1; i <= 35; i++) {111const repo = `https://example.com/r${i}.git`;112cache.set(repo, Uri.file(`/ws/r${i}`).fsPath);113}114assert.strictEqual(cache.get('https://example.com/r1.git'), undefined, 'oldest repo should be trimmed');115assert.ok(cache.get('https://example.com/r35.git'), 'newest repo should remain');116});117118test('delete removes folder and prunes empty repo', () => {119const memento = new InMemoryMemento();120const workspaceFolders: WorkspaceFolder[] = [];121workspaceFolders.push({ uri: Uri.file(`/ws/a`), name: `a`, index: 0 });122workspaceFolders.push({ uri: Uri.file(`/ws/b`), name: `b`, index: 1 });123124const cache = new TestRepositoryCache(memento, new MockLogOutputChannel(), undefined, workspaceFolders);125const repo = 'https://example.com/repo.git';126const a = Uri.file('/ws/a').fsPath;127const b = Uri.file('/ws/b').fsPath;128cache.set(repo, a);129cache.set(repo, b);130assert.deepStrictEqual(new Set(cache.get(repo)?.map(folder => folder.workspacePath)), new Set([a, b]));131cache.delete(repo, a);132assert.deepStrictEqual(cache.get(repo)!.map(folder => folder.workspacePath), [b]);133cache.delete(repo, b);134assert.strictEqual(cache.get(repo), undefined, 'repo should be pruned when last folder removed');135});136137test('normalizes URLs with trailing .git', () => {138const memento = new InMemoryMemento();139const folder = Uri.file('/workspace/repo');140const cache = new TestRepositoryCache(memento, new MockLogOutputChannel(), undefined, [{ uri: folder, name: 'workspace', index: 0 }]);141142// Set with .git extension143cache.set('https://example.com/repo.git', folder.fsPath);144145// Should be able to get with or without .git146const withGit = cache.get('https://example.com/repo.git');147const withoutGit = cache.get('https://example.com/repo');148149assert.ok(withGit, 'should find repo when querying with .git');150assert.ok(withoutGit, 'should find repo when querying without .git');151assert.deepStrictEqual(withGit, withoutGit, 'should return same result regardless of .git suffix');152});153154test('normalizes URLs with trailing slashes and .git', () => {155const memento = new InMemoryMemento();156const folder = Uri.file('/workspace/repo');157const cache = new TestRepositoryCache(memento, new MockLogOutputChannel(), undefined, [{ uri: folder, name: 'workspace', index: 0 }]);158159// Set with .git and trailing slashes160cache.set('https://example.com/repo.git///', folder.fsPath);161162// Should be able to get with various combinations163const variations = [164'https://example.com/repo.git///',165'https://example.com/repo.git/',166'https://example.com/repo.git',167'https://example.com/repo/',168'https://example.com/repo'169];170171const results = variations.map(url => cache.get(url));172173// All should return the same non-undefined result174assert.ok(results[0], 'should find repo with original URL');175for (let i = 1; i < results.length; i++) {176assert.deepStrictEqual(results[i], results[0], `variation ${variations[i]} should return same result`);177}178});179180test('handles URLs without .git correctly', () => {181const memento = new InMemoryMemento();182const folder = Uri.file('/workspace/repo');183const cache = new TestRepositoryCache(memento, new MockLogOutputChannel(), undefined, [{ uri: folder, name: 'workspace', index: 0 }]);184185// Set without .git extension186cache.set('https://example.com/repo', folder.fsPath);187188// Should be able to get with or without .git189const withoutGit = cache.get('https://example.com/repo');190const withGit = cache.get('https://example.com/repo.git');191192assert.ok(withoutGit, 'should find repo when querying without .git');193assert.ok(withGit, 'should find repo when querying with .git');194assert.deepStrictEqual(withoutGit, withGit, 'should return same result regardless of .git suffix');195});196});197198199