Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/git/src/test/repositoryCache.test.ts
4774 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import 'mocha';
7
import * as assert from 'assert';
8
import { RepositoryCache } from '../repositoryCache';
9
import { Event, EventEmitter, LogLevel, LogOutputChannel, Memento, Uri, WorkspaceFolder } from 'vscode';
10
11
class InMemoryMemento implements Memento {
12
private store = new Map<string, any>();
13
14
constructor(initial?: Record<string, any>) {
15
if (initial) {
16
for (const k of Object.keys(initial)) {
17
this.store.set(k, initial[k]);
18
}
19
}
20
}
21
22
get<T>(key: string): T | undefined;
23
get<T>(key: string, defaultValue: T): T;
24
get<T>(key: string, defaultValue?: T): T | undefined {
25
if (this.store.has(key)) {
26
return this.store.get(key);
27
}
28
return defaultValue as (T | undefined);
29
}
30
31
update(key: string, value: any): Thenable<void> {
32
this.store.set(key, value);
33
return Promise.resolve();
34
}
35
36
keys(): readonly string[] {
37
return Array.from(this.store.keys());
38
}
39
}
40
41
class MockLogOutputChannel implements LogOutputChannel {
42
logLevel: LogLevel = LogLevel.Info;
43
onDidChangeLogLevel: Event<LogLevel> = new EventEmitter<LogLevel>().event;
44
trace(_message: string, ..._args: any[]): void { }
45
debug(_message: string, ..._args: any[]): void { }
46
info(_message: string, ..._args: any[]): void { }
47
warn(_message: string, ..._args: any[]): void { }
48
error(_error: string | Error, ..._args: any[]): void { }
49
name: string = 'MockLogOutputChannel';
50
append(_value: string): void { }
51
appendLine(_value: string): void { }
52
replace(_value: string): void { }
53
clear(): void { }
54
show(_column?: unknown, _preserveFocus?: unknown): void { }
55
hide(): void { }
56
dispose(): void { }
57
}
58
59
class TestRepositoryCache extends RepositoryCache {
60
constructor(memento: Memento, logger: LogOutputChannel, private readonly _workspaceFileProp: Uri | undefined, private readonly _workspaceFoldersProp: readonly WorkspaceFolder[] | undefined) {
61
super(memento, logger);
62
}
63
64
protected override get _workspaceFile() {
65
return this._workspaceFileProp;
66
}
67
68
protected override get _workspaceFolders() {
69
return this._workspaceFoldersProp;
70
}
71
}
72
73
suite('RepositoryCache', () => {
74
75
test('set & get basic', () => {
76
const memento = new InMemoryMemento();
77
const folder = Uri.file('/workspace/repo');
78
const cache = new TestRepositoryCache(memento, new MockLogOutputChannel(), undefined, [{ uri: folder, name: 'workspace', index: 0 }]);
79
80
cache.set('https://example.com/repo.git', folder.fsPath);
81
const folders = cache.get('https://example.com/repo.git')!.map(folder => folder.workspacePath);
82
assert.ok(folders, 'folders should be defined');
83
assert.deepStrictEqual(folders, [folder.fsPath]);
84
});
85
86
test('inner LRU capped at 10 entries', () => {
87
const memento = new InMemoryMemento();
88
const workspaceFolders: WorkspaceFolder[] = [];
89
for (let i = 1; i <= 12; i++) {
90
workspaceFolders.push({ uri: Uri.file(`/ws/folder-${i.toString().padStart(2, '0')}`), name: `folder-${i.toString().padStart(2, '0')}`, index: i - 1 });
91
}
92
const cache = new TestRepositoryCache(memento, new MockLogOutputChannel(), undefined, workspaceFolders);
93
const repo = 'https://example.com/repo.git';
94
for (let i = 1; i <= 12; i++) {
95
cache.set(repo, Uri.file(`/ws/folder-${i.toString().padStart(2, '0')}`).fsPath);
96
}
97
const folders = cache.get(repo)!.map(folder => folder.workspacePath);
98
assert.strictEqual(folders.length, 10, 'should only retain 10 most recent folders');
99
assert.ok(!folders.includes(Uri.file('/ws/folder-01').fsPath), 'oldest folder-01 should be evicted');
100
assert.ok(!folders.includes(Uri.file('/ws/folder-02').fsPath), 'second oldest folder-02 should be evicted');
101
assert.ok(folders.includes(Uri.file('/ws/folder-12').fsPath), 'latest folder should be present');
102
});
103
104
test('outer LRU capped at 30 repos', () => {
105
const memento = new InMemoryMemento();
106
const workspaceFolders: WorkspaceFolder[] = [];
107
for (let i = 1; i <= 35; i++) {
108
workspaceFolders.push({ uri: Uri.file(`/ws/r${i}`), name: `r${i}`, index: i - 1 });
109
}
110
const cache = new TestRepositoryCache(memento, new MockLogOutputChannel(), undefined, workspaceFolders);
111
for (let i = 1; i <= 35; i++) {
112
const repo = `https://example.com/r${i}.git`;
113
cache.set(repo, Uri.file(`/ws/r${i}`).fsPath);
114
}
115
assert.strictEqual(cache.get('https://example.com/r1.git'), undefined, 'oldest repo should be trimmed');
116
assert.ok(cache.get('https://example.com/r35.git'), 'newest repo should remain');
117
});
118
119
test('delete removes folder and prunes empty repo', () => {
120
const memento = new InMemoryMemento();
121
const workspaceFolders: WorkspaceFolder[] = [];
122
workspaceFolders.push({ uri: Uri.file(`/ws/a`), name: `a`, index: 0 });
123
workspaceFolders.push({ uri: Uri.file(`/ws/b`), name: `b`, index: 1 });
124
125
const cache = new TestRepositoryCache(memento, new MockLogOutputChannel(), undefined, workspaceFolders);
126
const repo = 'https://example.com/repo.git';
127
const a = Uri.file('/ws/a').fsPath;
128
const b = Uri.file('/ws/b').fsPath;
129
cache.set(repo, a);
130
cache.set(repo, b);
131
assert.deepStrictEqual(new Set(cache.get(repo)?.map(folder => folder.workspacePath)), new Set([a, b]));
132
cache.delete(repo, a);
133
assert.deepStrictEqual(cache.get(repo)!.map(folder => folder.workspacePath), [b]);
134
cache.delete(repo, b);
135
assert.strictEqual(cache.get(repo), undefined, 'repo should be pruned when last folder removed');
136
});
137
138
test('normalizes URLs with trailing .git', () => {
139
const memento = new InMemoryMemento();
140
const folder = Uri.file('/workspace/repo');
141
const cache = new TestRepositoryCache(memento, new MockLogOutputChannel(), undefined, [{ uri: folder, name: 'workspace', index: 0 }]);
142
143
// Set with .git extension
144
cache.set('https://example.com/repo.git', folder.fsPath);
145
146
// Should be able to get with or without .git
147
const withGit = cache.get('https://example.com/repo.git');
148
const withoutGit = cache.get('https://example.com/repo');
149
150
assert.ok(withGit, 'should find repo when querying with .git');
151
assert.ok(withoutGit, 'should find repo when querying without .git');
152
assert.deepStrictEqual(withGit, withoutGit, 'should return same result regardless of .git suffix');
153
});
154
155
test('normalizes URLs with trailing slashes and .git', () => {
156
const memento = new InMemoryMemento();
157
const folder = Uri.file('/workspace/repo');
158
const cache = new TestRepositoryCache(memento, new MockLogOutputChannel(), undefined, [{ uri: folder, name: 'workspace', index: 0 }]);
159
160
// Set with .git and trailing slashes
161
cache.set('https://example.com/repo.git///', folder.fsPath);
162
163
// Should be able to get with various combinations
164
const variations = [
165
'https://example.com/repo.git///',
166
'https://example.com/repo.git/',
167
'https://example.com/repo.git',
168
'https://example.com/repo/',
169
'https://example.com/repo'
170
];
171
172
const results = variations.map(url => cache.get(url));
173
174
// All should return the same non-undefined result
175
assert.ok(results[0], 'should find repo with original URL');
176
for (let i = 1; i < results.length; i++) {
177
assert.deepStrictEqual(results[i], results[0], `variation ${variations[i]} should return same result`);
178
}
179
});
180
181
test('handles URLs without .git correctly', () => {
182
const memento = new InMemoryMemento();
183
const folder = Uri.file('/workspace/repo');
184
const cache = new TestRepositoryCache(memento, new MockLogOutputChannel(), undefined, [{ uri: folder, name: 'workspace', index: 0 }]);
185
186
// Set without .git extension
187
cache.set('https://example.com/repo', folder.fsPath);
188
189
// Should be able to get with or without .git
190
const withoutGit = cache.get('https://example.com/repo');
191
const withGit = cache.get('https://example.com/repo.git');
192
193
assert.ok(withoutGit, 'should find repo when querying without .git');
194
assert.ok(withGit, 'should find repo when querying with .git');
195
assert.deepStrictEqual(withoutGit, withGit, 'should return same result regardless of .git suffix');
196
});
197
});
198
199