Skip to content

Commit 0667f42

Browse files
committed
Adding unit test to branch
1 parent 9c5b840 commit 0667f42

File tree

2 files changed

+326
-0
lines changed

2 files changed

+326
-0
lines changed

__mocks__/vscode.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,23 @@ const StatusBarAlignment = {
3030
Right: 2
3131
};
3232

33+
const MockTreeItemCollapsibleState = {
34+
None: 0,
35+
Collapsed: 1,
36+
Expanded: 2
37+
};
38+
39+
class MockTreeItem {
40+
label;
41+
description;
42+
contextValue;
43+
collapsibleState;
44+
constructor(label, collapsibleState) {
45+
this.label = label;
46+
this.collapsibleState = collapsibleState;
47+
}
48+
}
49+
3350
module.exports = {
3451
EventEmitter: jest.fn(() => {
3552
const callbacks = [];
@@ -43,6 +60,8 @@ module.exports = {
4360
};
4461
}),
4562
Uri: URI,
63+
TreeItem: MockTreeItem,
64+
TreeItemCollapsibleState: MockTreeItemCollapsibleState,
4665
window: {
4766
createOutputChannel: jest.fn(() => ({
4867
appendLine: jest.fn(),
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
/**
2+
* Copyright 2025 Arm Limited
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/* eslint-disable @typescript-eslint/no-explicit-any */
18+
import * as vscode from 'vscode';
19+
import { debugSessionFactory, extensionContextFactory } from '../../__test__/vscode.factory';
20+
//import { LiveWatchTreeDataProvider } from './live-watch';
21+
import { GDBTargetDebugSession, GDBTargetDebugTracker } from '../../debug-session';
22+
import { gdbTargetConfiguration } from '../../debug-configuration/debug-configuration.factory';
23+
import { GDBTargetConfiguration } from '../../debug-configuration';
24+
25+
// Inline mock for registerTreeDataProvider specific to these tests
26+
const registerTreeDataProviderMock = jest.fn(() => ({ dispose: jest.fn() }));
27+
28+
import { LiveWatchTreeDataProvider } from './live-watch';
29+
30+
31+
describe('LiveWatchTreeDataProvider', () => {
32+
let liveWatchTreeDataProvider: LiveWatchTreeDataProvider;
33+
let tracker: GDBTargetDebugTracker;
34+
let debugSession: vscode.DebugSession;
35+
let gdbtargetDebugSession: GDBTargetDebugSession;
36+
let debugConfig: GDBTargetConfiguration;
37+
38+
// Helper: create a dummy node
39+
function makeNode(expression = 'x', value = '1', id = 1) {
40+
return { id, expression, value, parent: undefined };
41+
}
42+
43+
beforeEach(() => {
44+
// Mock the ExtensionContext
45+
const mockContext = extensionContextFactory();
46+
debugConfig = gdbTargetConfiguration();
47+
liveWatchTreeDataProvider = new LiveWatchTreeDataProvider(mockContext);
48+
tracker = new GDBTargetDebugTracker();
49+
tracker.activate(mockContext);
50+
debugSession = debugSessionFactory(debugConfig);
51+
gdbtargetDebugSession = new GDBTargetDebugSession(debugSession);
52+
});
53+
54+
describe('session management and connection tests', () => {
55+
it('should activate the live watch tree data provider', () => {
56+
(vscode.window).registerTreeDataProvider = registerTreeDataProviderMock;
57+
liveWatchTreeDataProvider.activate(tracker);
58+
});
59+
60+
it('registers the live watch tree data provider', async () => {
61+
(vscode.window).registerTreeDataProvider = registerTreeDataProviderMock;
62+
registerTreeDataProviderMock.mockClear();
63+
liveWatchTreeDataProvider.activate(tracker);
64+
expect(registerTreeDataProviderMock).toHaveBeenCalledWith('cmsis-debugger.liveWatch', liveWatchTreeDataProvider);
65+
});
66+
67+
it('manages session lifecycles correctly', async () => {
68+
(vscode.window).registerTreeDataProvider = registerTreeDataProviderMock;
69+
liveWatchTreeDataProvider.activate(tracker);
70+
// No active session yet
71+
expect((liveWatchTreeDataProvider as any).activeSession).toBeUndefined();
72+
// Add session (should not set active session yet)
73+
(tracker as any)._onWillStartSession.fire(gdbtargetDebugSession);
74+
expect((liveWatchTreeDataProvider as any).activeSession).toBeUndefined();
75+
// Activate session
76+
(tracker as any)._onDidChangeActiveDebugSession.fire(gdbtargetDebugSession);
77+
expect((liveWatchTreeDataProvider as any).activeSession?.session.id).toEqual(gdbtargetDebugSession.session.id);
78+
expect((liveWatchTreeDataProvider as any).activeSession?.session.name).toEqual(gdbtargetDebugSession.session.name);
79+
// Deactivate session
80+
(tracker as any)._onDidChangeActiveDebugSession.fire(undefined);
81+
expect((liveWatchTreeDataProvider as any).activeSession).toBeUndefined();
82+
// Reactivate session
83+
(tracker as any)._onDidChangeActiveDebugSession.fire(gdbtargetDebugSession);
84+
expect((liveWatchTreeDataProvider as any).activeSession).toBeDefined();
85+
// Stop session should clear active session
86+
(tracker as any)._onWillStopSession.fire(gdbtargetDebugSession);
87+
expect((liveWatchTreeDataProvider as any).activeSession).toBeUndefined();
88+
});
89+
90+
it('refreshes on stopped event and on onDidChangeActiveStackItem', async () => {
91+
const refreshSpy = jest.spyOn(liveWatchTreeDataProvider as any, 'refresh').mockResolvedValue('');
92+
liveWatchTreeDataProvider.activate(tracker);
93+
// Activate session
94+
(tracker as any)._onDidChangeActiveDebugSession.fire(gdbtargetDebugSession);
95+
expect((liveWatchTreeDataProvider as any).activeSession?.session.id).toEqual(gdbtargetDebugSession.session.id);
96+
// Fire stopped event
97+
(tracker as any)._onStopped.fire({ session: gdbtargetDebugSession });
98+
expect(refreshSpy).toHaveBeenCalled();
99+
refreshSpy.mockClear();
100+
// Fire onDidChangeActiveStackItem event
101+
(tracker as any)._onDidChangeActiveStackItem.fire({ item: { frameId: 1 } });
102+
expect(refreshSpy).toHaveBeenCalled();
103+
});
104+
105+
it('calls save function when extension is deactivating', async () => {
106+
const saveSpy = jest.spyOn(liveWatchTreeDataProvider as any, 'save').mockResolvedValue('');
107+
liveWatchTreeDataProvider.activate(tracker);
108+
await liveWatchTreeDataProvider.deactivate();
109+
expect(saveSpy).toHaveBeenCalled();
110+
});
111+
112+
it('reassigns IDs sequentially for restored nodes on construction', () => {
113+
const storedNodes = [
114+
{ id: 5, expression: 'expression1', value: 'some-value', parent: undefined },
115+
{ id: 9, expression: 'expression2', value: 'some-value', parent: undefined }
116+
];
117+
const mockContext: any = {
118+
subscriptions: [],
119+
workspaceState: {
120+
get: (key: string) => key === 'cmsis-debugger.liveWatch.tree.items' ? storedNodes : undefined,
121+
update: jest.fn()
122+
}
123+
};
124+
const provider = new LiveWatchTreeDataProvider(mockContext);
125+
const roots = (provider as any).roots;
126+
expect(roots.length).toBe(2);
127+
expect(roots[0].id).toBe(0);
128+
expect(roots[1].id).toBe(1);
129+
expect((provider as any).nodeID).toBe(2);
130+
});
131+
});
132+
133+
describe('tree data methods', () => {
134+
it('getChildren returns roots when no element is passed', async () => {
135+
(liveWatchTreeDataProvider as any).roots = [makeNode('node-1', '1', 1), makeNode('node-2', '2', 2)];
136+
const children = await liveWatchTreeDataProvider.getChildren();
137+
expect(children.length).toBe(2);
138+
expect(children[0].expression).toBe('node-1');
139+
expect(children[1].expression).toBe('node-2');
140+
});
141+
142+
it('getChildren returns children of element', async () => {
143+
const childNode = makeNode('childNode', '2', 2);
144+
const parent = { ...makeNode('parentNode', '1', 1), children: [childNode] };
145+
(liveWatchTreeDataProvider as any).roots = [parent];
146+
const children = await liveWatchTreeDataProvider.getChildren(parent);
147+
expect(children.length).toBe(1);
148+
expect(children[0].expression).toBe('childNode');
149+
});
150+
151+
it('getTreeItem returns correct TreeItem', () => {
152+
const node = makeNode('expression', 'value', 1);
153+
const item = liveWatchTreeDataProvider.getTreeItem(node);
154+
expect(item.label).toBe('expression = ');
155+
expect(item.description).toBe('value');
156+
expect(item.contextValue).toBe('expression');
157+
});
158+
});
159+
160+
describe('node management', () => {
161+
it('add creates a new root node', async () => {
162+
jest.spyOn(liveWatchTreeDataProvider as any, 'evaluate').mockResolvedValue('1234');
163+
await (liveWatchTreeDataProvider as any).add('expression');
164+
expect((liveWatchTreeDataProvider as any).roots.length).toBe(1);
165+
expect((liveWatchTreeDataProvider as any).roots[0].expression).toBe('expression');
166+
expect((liveWatchTreeDataProvider as any).roots[0].value).toBe('1234');
167+
});
168+
169+
it('clear removes all nodes', async () => {
170+
(liveWatchTreeDataProvider as any).roots = [makeNode('expression', '1', 1)];
171+
await (liveWatchTreeDataProvider as any).clear();
172+
expect((liveWatchTreeDataProvider as any).roots.length).toBe(0);
173+
});
174+
175+
it('delete removes a node by id', async () => {
176+
(liveWatchTreeDataProvider as any).roots = [makeNode('node-1', '1', 1), makeNode('node-2', '2', 2)];
177+
await (liveWatchTreeDataProvider as any).delete({ id: 1 });
178+
expect((liveWatchTreeDataProvider as any).roots.length).toBe(1);
179+
expect((liveWatchTreeDataProvider as any).roots[0].id).toBe(2);
180+
});
181+
182+
it('rename updates node expression', async () => {
183+
const node = makeNode('node-1', '1', 1);
184+
(liveWatchTreeDataProvider as any).roots = [node];
185+
await (liveWatchTreeDataProvider as any).rename(node, 'node-1-renamed');
186+
expect(node.expression).toBe('node-1-renamed');
187+
});
188+
});
189+
190+
describe('refresh', () => {
191+
it('refresh updates all root node values', async () => {
192+
const node = makeNode('expression', 'old-value', 1);
193+
(liveWatchTreeDataProvider as any).roots = [node];
194+
jest.spyOn(liveWatchTreeDataProvider as any, 'evaluate').mockResolvedValue('new-value');
195+
await (liveWatchTreeDataProvider as any).refresh();
196+
expect(node.value).toBe('new-value');
197+
});
198+
199+
it('refresh(node) updates only that node', async () => {
200+
const node = makeNode('expression', 'old-value', 1);
201+
(liveWatchTreeDataProvider as any).roots = [node];
202+
jest.spyOn(liveWatchTreeDataProvider as any, 'evaluate').mockResolvedValue('new-value');
203+
await (liveWatchTreeDataProvider as any).refresh(node);
204+
expect(node.value).toBe('new-value');
205+
});
206+
});
207+
208+
describe('command registration', () => {
209+
beforeEach(() => {
210+
(vscode.commands as any).registerCommand?.mockClear?.();
211+
(vscode.window as any).registerTreeDataProvider = registerTreeDataProviderMock;
212+
});
213+
214+
function getRegisteredHandler(commandId: string) {
215+
const calls = (vscode.commands.registerCommand as jest.Mock).mock.calls;
216+
const match = calls.find(c => c[0] === commandId);
217+
return match ? match[1] : undefined;
218+
}
219+
220+
it('registers all live watch commands on activate', () => {
221+
liveWatchTreeDataProvider.activate(tracker);
222+
const calls = (vscode.commands.registerCommand as jest.Mock).mock.calls.map(call => call[0]);
223+
expect(calls).toEqual(expect.arrayContaining([
224+
'cmsis-debugger.liveWatch.add',
225+
'cmsis-debugger.liveWatch.deleteAll',
226+
'cmsis-debugger.liveWatch.delete',
227+
'cmsis-debugger.liveWatch.refresh',
228+
'cmsis-debugger.liveWatch.modify'
229+
]));
230+
});
231+
232+
it('add command adds a node when expression provided', async () => {
233+
(vscode.window as any).showInputBox = jest.fn().mockResolvedValue('expression');
234+
const evaluateSpy = jest.spyOn(liveWatchTreeDataProvider as any, 'evaluate').mockResolvedValue('someValue');
235+
liveWatchTreeDataProvider.activate(tracker);
236+
const handler = getRegisteredHandler('cmsis-debugger.liveWatch.add');
237+
expect(handler).toBeDefined();
238+
await handler();
239+
const roots = (liveWatchTreeDataProvider as any).roots;
240+
expect(roots.length).toBe(1);
241+
expect(roots[0].expression).toBe('expression');
242+
expect(evaluateSpy).toHaveBeenCalledWith('expression');
243+
});
244+
245+
it('add command does nothing when expression undefined', async () => {
246+
(vscode.window as any).showInputBox = jest.fn().mockResolvedValue(undefined);
247+
liveWatchTreeDataProvider.activate(tracker);
248+
const handler = getRegisteredHandler('cmsis-debugger.liveWatch.add');
249+
await handler();
250+
expect((liveWatchTreeDataProvider as any).roots.length).toBe(0);
251+
});
252+
253+
it('deleteAll command clears roots', async () => {
254+
(liveWatchTreeDataProvider as any).roots = [makeNode('nodeA','1',1), makeNode('nodeB','2',2)];
255+
const clearSpy = jest.spyOn(liveWatchTreeDataProvider as any, 'clear');
256+
liveWatchTreeDataProvider.activate(tracker);
257+
const handler = getRegisteredHandler('cmsis-debugger.liveWatch.deleteAll');
258+
await handler();
259+
expect(clearSpy).toHaveBeenCalled();
260+
expect((liveWatchTreeDataProvider as any).roots.length).toBe(0);
261+
});
262+
263+
it('delete command removes provided node', async () => {
264+
(liveWatchTreeDataProvider as any).roots = [makeNode('nodeA','1',1), makeNode('nodeB','2',2)];
265+
const deleteSpy = jest.spyOn(liveWatchTreeDataProvider as any, 'delete');
266+
liveWatchTreeDataProvider.activate(tracker);
267+
const handler = getRegisteredHandler('cmsis-debugger.liveWatch.delete');
268+
const target = (liveWatchTreeDataProvider as any).roots[0];
269+
await handler(target);
270+
expect(deleteSpy).toHaveBeenCalledWith(target);
271+
expect((liveWatchTreeDataProvider as any).roots.some((r: any) => r.id === target.id)).toBe(false);
272+
});
273+
274+
it('modify command renames a node when expression provided', async () => {
275+
const node = makeNode('oldExpression','1',1);
276+
(liveWatchTreeDataProvider as any).roots = [node];
277+
(vscode.window as any).showInputBox = jest.fn().mockResolvedValue('newExpression');
278+
const renameSpy = jest.spyOn(liveWatchTreeDataProvider as any, 'rename');
279+
liveWatchTreeDataProvider.activate(tracker);
280+
const handler = getRegisteredHandler('cmsis-debugger.liveWatch.modify');
281+
await handler(node);
282+
expect(renameSpy).toHaveBeenCalledWith(node,'newExpression');
283+
expect(node.expression).toBe('newExpression');
284+
});
285+
286+
it('modify command does nothing when expression undefined', async () => {
287+
const node = makeNode('oldExpression','1',1);
288+
(liveWatchTreeDataProvider as any).roots = [node];
289+
(vscode.window as any).showInputBox = jest.fn().mockResolvedValue(undefined);
290+
const renameSpy = jest.spyOn(liveWatchTreeDataProvider as any, 'rename');
291+
liveWatchTreeDataProvider.activate(tracker);
292+
const handler = getRegisteredHandler('cmsis-debugger.liveWatch.modify');
293+
await handler(node);
294+
expect(renameSpy).not.toHaveBeenCalled();
295+
expect(node.expression).toBe('oldExpression');
296+
});
297+
298+
it('refresh command triggers provider.refresh()', async () => {
299+
const refreshSpy = jest.spyOn(liveWatchTreeDataProvider as any, 'refresh').mockResolvedValue(undefined);
300+
liveWatchTreeDataProvider.activate(tracker);
301+
const handler = getRegisteredHandler('cmsis-debugger.liveWatch.refresh');
302+
await handler();
303+
expect(refreshSpy).toHaveBeenCalled();
304+
});
305+
});
306+
});
307+
/* eslint-enable @typescript-eslint/no-explicit-any */

0 commit comments

Comments
 (0)