284 lines
11 KiB
TypeScript
284 lines
11 KiB
TypeScript
/**
|
|
* Tests for the connection troubleshooting wizard diagnostic flow.
|
|
*
|
|
* Verifies that the domain-specific error handlers produce the right
|
|
* error messages and actionable recovery steps, and that action callbacks
|
|
* trigger the expected VS Code commands.
|
|
*/
|
|
|
|
import { getDiagnosticsManager, resetDiagnosticsManager } from '../../src/frontend/diagnostics'
|
|
|
|
// Capture the action callbacks passed to showErrorMessage so we can invoke them
|
|
let capturedActions: Array<{ label: string; callback: () => void }> = []
|
|
|
|
jest.mock('vscode', () => {
|
|
// Track the last set of ErrorAction labels that were shown
|
|
const capturedLabels: string[] = []
|
|
|
|
return {
|
|
window: {
|
|
createOutputChannel: jest.fn().mockReturnValue({
|
|
appendLine: jest.fn(),
|
|
clear: jest.fn(),
|
|
show: jest.fn(),
|
|
}),
|
|
// Resolve with the first action label to simulate the user clicking it
|
|
showErrorMessage: jest.fn().mockImplementation((_msg: string, ...labels: string[]) => {
|
|
capturedLabels.splice(0, capturedLabels.length, ...labels)
|
|
return Promise.resolve(labels[0])
|
|
}),
|
|
showWarningMessage: jest.fn().mockImplementation((_msg: string, ...labels: string[]) =>
|
|
Promise.resolve(labels[0])
|
|
),
|
|
showInformationMessage: jest.fn().mockImplementation((_msg: string, ...labels: string[]) =>
|
|
Promise.resolve(labels[0])
|
|
),
|
|
showOpenDialog: jest.fn().mockResolvedValue([{ fsPath: '/path/to/file.svd' }]),
|
|
},
|
|
env: {
|
|
clipboard: { writeText: jest.fn().mockResolvedValue(undefined) },
|
|
openExternal: jest.fn().mockResolvedValue(true),
|
|
},
|
|
Uri: {
|
|
parse: jest.fn((s: string) => ({ toString: () => s })),
|
|
},
|
|
commands: {
|
|
executeCommand: jest.fn().mockResolvedValue(undefined),
|
|
},
|
|
}
|
|
})
|
|
|
|
import * as vscode from 'vscode'
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Helpers
|
|
// -------------------------------------------------------------------------
|
|
|
|
function getManager() {
|
|
return getDiagnosticsManager()
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Test suites
|
|
// -------------------------------------------------------------------------
|
|
|
|
describe('Troubleshooting Wizard — handleConnectionError', () => {
|
|
beforeEach(() => {
|
|
resetDiagnosticsManager()
|
|
jest.clearAllMocks()
|
|
capturedActions = []
|
|
})
|
|
|
|
test('shows an error message mentioning the host:port when provided', () => {
|
|
getManager().handleConnectionError('refused', 'localhost', 3333)
|
|
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
|
|
expect.stringContaining('localhost:3333'),
|
|
expect.any(String),
|
|
expect.any(String),
|
|
expect.any(String)
|
|
)
|
|
})
|
|
|
|
test('shows an error message without host:port when omitted', () => {
|
|
getManager().handleConnectionError('refused')
|
|
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
|
|
expect.stringContaining('debug server'),
|
|
expect.any(String),
|
|
expect.any(String),
|
|
expect.any(String)
|
|
)
|
|
})
|
|
|
|
test('offers "Check Connection", "Restart Debug" and "Show Diagnostics" actions', () => {
|
|
getManager().handleConnectionError('timed out', '192.168.1.1', 2331)
|
|
const callArgs = (vscode.window.showErrorMessage as jest.Mock).mock.calls[0]
|
|
const actionLabels: string[] = callArgs.slice(1)
|
|
expect(actionLabels).toContain('Check Connection')
|
|
expect(actionLabels).toContain('Restart Debug')
|
|
expect(actionLabels).toContain('Show Diagnostics')
|
|
})
|
|
|
|
test('"Restart Debug" action triggers workbench.action.debug.restart command', async () => {
|
|
// The mock resolves with the first label; simulate selecting "Restart Debug" by
|
|
// overriding the mock for this specific call.
|
|
;(vscode.window.showErrorMessage as jest.Mock).mockResolvedValueOnce('Restart Debug')
|
|
getManager().handleConnectionError('refused', 'localhost', 3333)
|
|
// Allow the promise chain to settle
|
|
await Promise.resolve()
|
|
await Promise.resolve()
|
|
expect(vscode.commands.executeCommand).toHaveBeenCalledWith(
|
|
'workbench.action.debug.restart'
|
|
)
|
|
})
|
|
|
|
test('logs the error at "error" level', () => {
|
|
const mgr = getManager()
|
|
mgr.handleConnectionError('refused', 'localhost', 3333)
|
|
const errors = mgr.getLogEntries('error')
|
|
expect(errors.length).toBeGreaterThan(0)
|
|
expect(errors[0].source).toBe('Connection')
|
|
})
|
|
})
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
describe('Troubleshooting Wizard — handleSVDError', () => {
|
|
beforeEach(() => {
|
|
resetDiagnosticsManager()
|
|
jest.clearAllMocks()
|
|
})
|
|
|
|
test('includes the SVD path in the error message when provided', () => {
|
|
getManager().handleSVDError('parse failed', '/project/device.svd')
|
|
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
|
|
expect.stringContaining('device.svd'),
|
|
expect.any(String),
|
|
expect.any(String)
|
|
)
|
|
})
|
|
|
|
test('shows a generic message when no path is provided', () => {
|
|
getManager().handleSVDError('not found')
|
|
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
|
|
expect.stringContaining('SVD file'),
|
|
expect.any(String),
|
|
expect.any(String)
|
|
)
|
|
})
|
|
|
|
test('offers "Locate SVD File" and "Skip SVD Load" actions', () => {
|
|
getManager().handleSVDError('invalid', '/bad.svd')
|
|
const callArgs = (vscode.window.showErrorMessage as jest.Mock).mock.calls[0]
|
|
const actionLabels: string[] = callArgs.slice(1)
|
|
expect(actionLabels).toContain('Locate SVD File')
|
|
expect(actionLabels).toContain('Skip SVD Load')
|
|
})
|
|
|
|
test('"Locate SVD File" action opens a file dialog and reloads SVD', async () => {
|
|
;(vscode.window.showErrorMessage as jest.Mock).mockResolvedValueOnce('Locate SVD File')
|
|
getManager().handleSVDError('bad', '/bad.svd')
|
|
await Promise.resolve()
|
|
await Promise.resolve()
|
|
expect(vscode.window.showOpenDialog).toHaveBeenCalledWith(
|
|
expect.objectContaining({ canSelectFiles: true })
|
|
)
|
|
})
|
|
|
|
test('logs the error at "error" level with SVD source', () => {
|
|
const mgr = getManager()
|
|
mgr.handleSVDError('corrupt', '/a.svd')
|
|
const errors = mgr.getLogEntries('error')
|
|
expect(errors.length).toBeGreaterThan(0)
|
|
expect(errors[0].source).toBe('SVD')
|
|
})
|
|
})
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
describe('Troubleshooting Wizard — handleMemoryError', () => {
|
|
beforeEach(() => {
|
|
resetDiagnosticsManager()
|
|
jest.clearAllMocks()
|
|
})
|
|
|
|
test('includes the hex address in the message when provided', () => {
|
|
getManager().handleMemoryError('fault', 0x20000000)
|
|
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
|
|
expect.stringMatching(/20000000/i),
|
|
expect.any(String),
|
|
expect.any(String)
|
|
)
|
|
})
|
|
|
|
test('shows a generic message when address is omitted', () => {
|
|
getManager().handleMemoryError('fault')
|
|
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
|
|
expect.stringContaining('memory'),
|
|
expect.any(String),
|
|
expect.any(String)
|
|
)
|
|
})
|
|
|
|
test('offers "Target May Not Be Halted" and "Check Address" actions', () => {
|
|
getManager().handleMemoryError('access denied', 0x00)
|
|
const callArgs = (vscode.window.showErrorMessage as jest.Mock).mock.calls[0]
|
|
const actionLabels: string[] = callArgs.slice(1)
|
|
expect(actionLabels).toContain('Target May Not Be Halted')
|
|
expect(actionLabels).toContain('Check Address')
|
|
})
|
|
|
|
test('logs the error at "error" level with Memory source', () => {
|
|
const mgr = getManager()
|
|
mgr.handleMemoryError('bus error', 0x40000000)
|
|
const errors = mgr.getLogEntries('error')
|
|
expect(errors.length).toBeGreaterThan(0)
|
|
expect(errors[0].source).toBe('Memory')
|
|
})
|
|
})
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
describe('Troubleshooting Wizard — handleGDBError generic fallback', () => {
|
|
beforeEach(() => {
|
|
resetDiagnosticsManager()
|
|
jest.clearAllMocks()
|
|
})
|
|
|
|
test('returns false and offers "Show Diagnostics" and "Copy Error" for unknown errors', () => {
|
|
const handled = getManager().handleGDBError('some unknown gdb error xyz')
|
|
expect(handled).toBe(false)
|
|
const callArgs = (vscode.window.showErrorMessage as jest.Mock).mock.calls[0]
|
|
const actionLabels: string[] = callArgs.slice(1)
|
|
expect(actionLabels).toContain('Show Diagnostics')
|
|
expect(actionLabels).toContain('Copy Error')
|
|
})
|
|
|
|
test('"Copy Error" action copies the error message to the clipboard', async () => {
|
|
;(vscode.window.showErrorMessage as jest.Mock).mockResolvedValueOnce('Copy Error')
|
|
getManager().handleGDBError('unknown fatal error')
|
|
await Promise.resolve()
|
|
await Promise.resolve()
|
|
expect(vscode.env.clipboard.writeText).toHaveBeenCalledWith(
|
|
expect.stringContaining('unknown fatal error')
|
|
)
|
|
})
|
|
|
|
test('"Show Diagnostics" action opens the output channel', async () => {
|
|
;(vscode.window.showErrorMessage as jest.Mock).mockResolvedValueOnce('Show Diagnostics')
|
|
// Create the manager first so createOutputChannel has been called
|
|
const mgr = getManager()
|
|
const mockChannel = (vscode.window.createOutputChannel as jest.Mock).mock.results[0].value
|
|
mgr.handleGDBError('unknown error')
|
|
await Promise.resolve()
|
|
await Promise.resolve()
|
|
expect(mockChannel.show).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
describe('Troubleshooting Wizard — exportLog', () => {
|
|
beforeEach(() => {
|
|
resetDiagnosticsManager()
|
|
})
|
|
|
|
test('exported log contains all logged entries', () => {
|
|
const mgr = getManager()
|
|
mgr.error('SrcA', 'msg1')
|
|
mgr.warn('SrcB', 'msg2')
|
|
mgr.info('SrcC', 'msg3')
|
|
const log = mgr.exportLog()
|
|
expect(log).toContain('msg1')
|
|
expect(log).toContain('msg2')
|
|
expect(log).toContain('msg3')
|
|
})
|
|
|
|
test('exported log contains a header with entry count', () => {
|
|
const mgr = getManager()
|
|
mgr.error('S', 'e1')
|
|
mgr.error('S', 'e2')
|
|
const log = mgr.exportLog()
|
|
expect(log).toContain('Total Entries: 2')
|
|
})
|
|
})
|