Skip to content

Commit a4bec81

Browse files
committed
Optimize attach/detach log stream
1 parent b891a64 commit a4bec81

File tree

5 files changed

+102
-125
lines changed

5 files changed

+102
-125
lines changed

src/extension/commands/git/watch-config.spec.ts

+10-29
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ suite('WatchConfig', () => {
1717
setup(() => {
1818
watcher = (async function* () {
1919
const result = await new Promise<GitRemoteAppsDiff>((resolve) => {
20-
watcherEmitter.once('change', resolve);
20+
watcherEmitter.once('change', () => {
21+
resolve({ added: new Set(), removed: new Set() });
22+
});
2123
});
2224
yield result;
2325
})();
@@ -59,47 +61,26 @@ suite('WatchConfig', () => {
5961
const watchConfig = new WatchConfig();
6062
const abortController = new AbortController();
6163

62-
const generator = await watchConfig.run(abortController, true);
64+
const generator = await watchConfig.run(abortController);
6365

6466
// Simulate initial state
6567
getHerokuAppNamesStub.onFirstCall().resolves(['app1', 'app2']);
6668

6769
// Start the generator
68-
const promise = generator.next();
69-
70-
// Simulate a change event with no actual changes
71-
watcherEmitter.emit('change', { eventType: 'change' });
70+
const result1 = await generator.next();
7271

7372
// Simulate a change event
7473
getHerokuAppNamesStub.onSecondCall().resolves(['app1', 'app3']);
7574

76-
const result = await promise;
77-
assert.deepStrictEqual(result.value, { added: new Set(['app3']), removed: new Set(['app2']) });
78-
79-
// Verify that context was updated
80-
assert(vscodeStub.commands.executeCommand.calledWith('setContext', 'heroku.app-found', true));
81-
82-
abortController.abort();
83-
});
84-
85-
test('should not yield when there are no changes', async () => {
86-
const watchConfig = new WatchConfig();
87-
const abortController = new AbortController();
88-
89-
const generator = await watchConfig.run(abortController, false);
90-
91-
// Simulate no changes
92-
getHerokuAppNamesStub.resolves(['app1', 'app2']);
93-
94-
// Start the generator
9575
const promise = generator.next();
96-
9776
// Simulate a change event with no actual changes
9877
watcherEmitter.emit('change', { eventType: 'change' });
99-
abortController.abort();
100-
const result = await promise;
10178

102-
assert.strictEqual(result.value, undefined);
79+
const result2 = await promise;
80+
81+
assert.deepStrictEqual(result2.value, { added: new Set(['app3']), removed: new Set(['app2']) });
82+
83+
abortController.abort();
10384
});
10485

10586
test('should handle errors gracefully', async () => {
+4-19
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { watch } from 'node:fs/promises';
2-
import * as vscode from 'vscode';
32
import { herokuCommand } from '../../meta/command';
43
import { HerokuCommand } from '../heroku-command';
54
import { findGitConfigFileLocation, getHerokuAppNames } from '../../utils/git-utils';
@@ -17,20 +16,20 @@ export class WatchConfig extends HerokuCommand<AsyncGenerator<GitRemoteAppsDiff>
1716
* Runs the command.
1817
*
1918
* @param abortController AbortController
20-
* @param updateContext boolean
2119
*
2220
* @returns AsyncGenerator<AppNameDiff>
2321
*/
24-
public async run(abortController: AbortController, updateContext = true): Promise<AsyncGenerator<GitRemoteAppsDiff>> {
22+
public async run(abortController: AbortController): Promise<AsyncGenerator<GitRemoteAppsDiff>> {
2523
const configPath = await findGitConfigFileLocation();
26-
let apps = new Set(await this.getUpdatedAppNames(updateContext));
24+
let apps = new Set(await getHerokuAppNames());
2725

2826
return async function* (this: WatchConfig): AsyncGenerator<GitRemoteAppsDiff> {
27+
yield { added: apps, removed: new Set() };
2928
while (!abortController.signal.aborted) {
3029
const watcher = watch(configPath, { signal: abortController.signal });
3130
// eslint-disable-next-line @typescript-eslint/no-unused-vars
3231
for await (const event of watcher) {
33-
const maybeUpdatedApps = new Set(await this.getUpdatedAppNames(updateContext));
32+
const maybeUpdatedApps = new Set(await getHerokuAppNames());
3433
const added = maybeUpdatedApps.difference(apps);
3534
const removed = apps.difference(maybeUpdatedApps);
3635
if (added.size || removed.size) {
@@ -43,18 +42,4 @@ export class WatchConfig extends HerokuCommand<AsyncGenerator<GitRemoteAppsDiff>
4342
}
4443
}.bind(this)();
4544
}
46-
47-
/**
48-
* Gets the updated app names and optionally updates the 'heroku.app-found' context.
49-
*
50-
* @param updateContext boolean indicatig whether to update the 'heroku.app-found' context.
51-
* @returns Promise<Set<string>>
52-
*/
53-
private async getUpdatedAppNames(updateContext: boolean): Promise<Set<string>> {
54-
const apps = new Set(await getHerokuAppNames());
55-
if (updateContext) {
56-
void vscode.commands.executeCommand('setContext', 'heroku.app-found', !!apps.size);
57-
}
58-
return apps;
59-
}
6045
}

src/extension/providers/resource-explorer/heroku-resource-explorer-provider.spec.ts

+22-10
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,19 @@ import { HerokuResourceExplorerProvider } from './heroku-resource-explorer-provi
55
import { App, Dyno, Formation, AddOn } from '@heroku-cli/schema';
66
import { randomUUID } from 'node:crypto';
77
import * as gitUtils from '../../utils/git-utils';
8-
import { LogStreamClient, LogStreamEvents } from './log-stream-client';
98
import { Readable, Writable } from 'node:stream';
9+
import { WatchConfig } from '../../commands/git/watch-config';
1010

1111
suite('HerokuResourceExplorerProvider', () => {
1212
let provider: HerokuResourceExplorerProvider;
1313
let mockContext: vscode.ExtensionContext;
1414
let getSessionStub: sinon.SinonStub;
15+
let vsCodeExecCommandStub: sinon.SinonStub;
1516
let fetchStub: sinon.SinonStub;
1617
let elementTypeMap: Map<unknown, unknown>;
1718
let childParentMap: Map<unknown, unknown>;
1819
let appToResourceMap: Map<unknown, unknown>;
1920
let getHerokuAppNamesStub: sinon.SinonStub;
20-
let logStreamClientStub: LogStreamClient;
2121
let stream: Writable;
2222

2323
const mockApp = { id: 'app1', name: 'test-app', organization: { name: 'test-org' } } as App;
@@ -47,12 +47,19 @@ suite('HerokuResourceExplorerProvider', () => {
4747
}
4848
} as any;
4949

50-
getSessionStub = sinon.stub(vscode.authentication, 'getSession').callsFake(async (providerId: string) => {
51-
if (providerId === 'heroku:auth:login') {
52-
return sessionObject;
53-
}
54-
return undefined;
55-
});
50+
getSessionStub = sinon
51+
.stub(vscode.authentication, 'getSession')
52+
.withArgs('heroku:auth:login')
53+
.resolves(sessionObject);
54+
55+
vsCodeExecCommandStub = sinon.stub(vscode.commands, 'executeCommand');
56+
vsCodeExecCommandStub.withArgs(WatchConfig.COMMAND_ID, sinon.match.any).resolves(
57+
(function* () {
58+
yield { added: new Set([mockApp.name]), removed: new Set() };
59+
})()
60+
);
61+
62+
vsCodeExecCommandStub.callThrough();
5663

5764
// LogStream stub
5865
stream = new Writable();
@@ -71,6 +78,7 @@ suite('HerokuResourceExplorerProvider', () => {
7178
);
7279

7380
fetchStub = sinon.stub(global, 'fetch');
81+
fetchStub.withArgs('https://api.heroku.com/apps/test-app').resolves(new Response(JSON.stringify(mockApp)));
7482
fetchStub.withArgs('https://api.heroku.com/apps/app1').resolves(new Response(JSON.stringify(mockApp)));
7583
fetchStub.withArgs('https://api.heroku.com/apps/app1/dynos').resolves(new Response(JSON.stringify([mockDyno])));
7684
fetchStub.withArgs('https://api.heroku.com/apps/app1/addons').resolves(new Response(JSON.stringify([mockAddOn])));
@@ -95,8 +103,6 @@ suite('HerokuResourceExplorerProvider', () => {
95103
getHerokuAppNamesStub = sinon.stub(gitUtils, 'getHerokuAppNames').resolves(['app1']);
96104

97105
provider = new HerokuResourceExplorerProvider(mockContext);
98-
logStreamClientStub = new LogStreamClient();
99-
Reflect.set(provider, 'logStreamClient', logStreamClientStub);
100106

101107
elementTypeMap = Reflect.get(provider, 'elementTypeMap') as Map<unknown, unknown>;
102108
childParentMap = Reflect.get(provider, 'childParentMap') as Map<unknown, unknown>;
@@ -108,12 +114,14 @@ suite('HerokuResourceExplorerProvider', () => {
108114
});
109115

110116
test('getChildren should return apps when no element is provided', async () => {
117+
await new Promise((resolve) => provider.event(resolve));
111118
const children = await provider.getChildren();
112119
assert.strictEqual(children.length, 1);
113120
assert.deepStrictEqual(children[0], mockApp);
114121
});
115122

116123
test('getChildren should return app categories when an app is provided', async () => {
124+
await new Promise((resolve) => provider.event(resolve));
117125
const [app] = await provider.getChildren(); // Populate appToResourceMap
118126
const children = (await provider.getChildren(app)) as vscode.TreeItem[];
119127
assert.strictEqual(children.length, 4);
@@ -124,6 +132,7 @@ suite('HerokuResourceExplorerProvider', () => {
124132
});
125133

126134
test('getChildren should return formations when FORMATIONS category is provided', async () => {
135+
await new Promise((resolve) => provider.event(resolve));
127136
const [app] = await provider.getChildren(); // Populate appToResourceMap
128137
const categories = (await provider.getChildren(app)) as vscode.TreeItem[];
129138
const formationsCategory = categories.find((c) => c.label === 'FORMATIONS');
@@ -151,6 +160,7 @@ suite('HerokuResourceExplorerProvider', () => {
151160
});
152161

153162
test('onFormationScaledTo should update formation quantity and fire event', async () => {
163+
await new Promise((resolve) => provider.event(resolve));
154164
const [app] = await provider.getChildren(); // Populate appToResourceMap
155165
const categories = (await provider.getChildren(app)) as vscode.TreeItem[]; // gets the categories
156166

@@ -167,6 +177,7 @@ suite('HerokuResourceExplorerProvider', () => {
167177
});
168178

169179
test('onDynoStateChanged should update dyno state and fire event', async () => {
180+
await new Promise((resolve) => provider.event(resolve));
170181
const [app] = await provider.getChildren(); // Populate appToResourceMap
171182
const categories = (await provider.getChildren(app)) as vscode.TreeItem[]; // gets the categories
172183

@@ -184,6 +195,7 @@ suite('HerokuResourceExplorerProvider', () => {
184195
});
185196

186197
test('onDynoStateChanged should add the new dyno when startup occurs', async () => {
198+
await new Promise((resolve) => provider.event(resolve));
187199
const [app] = await provider.getChildren(); // Populate appToResourceMap
188200
const categories = (await provider.getChildren(app)) as vscode.TreeItem[]; // gets the categories
189201

0 commit comments

Comments
 (0)