mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-11 22:51:26 +00:00
test: add comprehensive terminal command execution testing
This commit combines three related improvements to terminal testing: - Create a reusable function for testing terminal commands with real output - Update tests to properly invoke terminal shell execution handlers - Add microsecond timing to measure execution performance Key improvements: - Added testTerminalCommand function that takes command and expected output - Use child_process.execSync to run real commands and feed output into mock terminal stream - Properly trigger VSCode onDidStartTerminalShellExecution and onDidEndTerminalShellExecution events - Add timeout mechanism to prevent hanging tests - Measure execution time from terminal process creation to command completion - Display both microseconds and milliseconds in test output - Add test for base64 encoded zeros with configurable line count - Increase buffer size for execSync to handle large outputs - Limit output display to avoid cluttering the terminal Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
This commit is contained in:
parent
070a36baa2
commit
60c14d8f5e
1 changed files with 293 additions and 0 deletions
293
src/integrations/terminal/__tests__/TerminalProcessExec.test.ts
Normal file
293
src/integrations/terminal/__tests__/TerminalProcessExec.test.ts
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
// npx jest src/integrations/terminal/__tests__/TerminalProcess.test.ts
|
||||
|
||||
import * as vscode from "vscode"
|
||||
import { execSync } from "child_process"
|
||||
import { TerminalProcess } from "../TerminalProcess"
|
||||
import { TerminalInfo, TerminalRegistry } from "../TerminalRegistry"
|
||||
import { TerminalManager } from "../TerminalManager"
|
||||
|
||||
// Mock the vscode module
|
||||
jest.mock("vscode", () => {
|
||||
// Store event handlers so we can trigger them in tests
|
||||
const eventHandlers = {
|
||||
startTerminalShellExecution: null as ((e: any) => void) | null,
|
||||
endTerminalShellExecution: null as ((e: any) => void) | null,
|
||||
}
|
||||
|
||||
return {
|
||||
window: {
|
||||
createTerminal: jest.fn(),
|
||||
onDidStartTerminalShellExecution: jest.fn().mockImplementation((handler) => {
|
||||
eventHandlers.startTerminalShellExecution = handler
|
||||
return { dispose: jest.fn() }
|
||||
}),
|
||||
onDidEndTerminalShellExecution: jest.fn().mockImplementation((handler) => {
|
||||
eventHandlers.endTerminalShellExecution = handler
|
||||
return { dispose: jest.fn() }
|
||||
}),
|
||||
},
|
||||
ThemeIcon: class ThemeIcon {
|
||||
constructor(id: string) {
|
||||
this.id = id
|
||||
}
|
||||
id: string
|
||||
},
|
||||
Uri: {
|
||||
file: (path: string) => ({ fsPath: path }),
|
||||
},
|
||||
// Expose event handlers for testing
|
||||
__eventHandlers: eventHandlers,
|
||||
}
|
||||
})
|
||||
|
||||
// Create a mock stream that uses real command output with realistic chunking
|
||||
function createRealCommandStream(command: string) {
|
||||
// Execute the command and get the real output
|
||||
const realOutput = execSync(command, {
|
||||
encoding: "utf8",
|
||||
maxBuffer: 100 * 1024 * 1024, // Increase buffer size to 100MB
|
||||
})
|
||||
|
||||
// Create an async iterator that yields the command output with proper markers
|
||||
// and realistic chunking (not guaranteed to split on newlines)
|
||||
return {
|
||||
async *[Symbol.asyncIterator]() {
|
||||
// First yield the command start marker
|
||||
yield "\x1b]633;C\x07"
|
||||
|
||||
// Yield the real output in potentially arbitrary chunks
|
||||
// This simulates how terminal data might be received in practice
|
||||
if (realOutput.length > 0) {
|
||||
// For a simple test like "echo a", we'll just yield the whole output
|
||||
// For more complex outputs, we could implement random chunking here
|
||||
yield realOutput
|
||||
}
|
||||
|
||||
// Last yield the command end marker
|
||||
yield "\x1b]633;D\x07"
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generalized function to test terminal command execution
|
||||
* @param command The command to execute
|
||||
* @param expectedOutput The expected output after processing
|
||||
* @returns A promise that resolves when the test is complete
|
||||
*/
|
||||
async function testTerminalCommand(
|
||||
command: string,
|
||||
expectedOutput: string,
|
||||
): Promise<{ executionTimeUs: number; capturedOutput: string }> {
|
||||
let startTime: bigint = BigInt(0)
|
||||
let endTime: bigint = BigInt(0)
|
||||
let timeRecorded = false
|
||||
// Create a mock terminal with shell integration
|
||||
const mockTerminal = {
|
||||
shellIntegration: {
|
||||
executeCommand: jest.fn(),
|
||||
cwd: vscode.Uri.file("/test/path"),
|
||||
},
|
||||
name: "Roo Code",
|
||||
processId: Promise.resolve(123),
|
||||
creationOptions: {},
|
||||
exitStatus: undefined,
|
||||
state: { isInteractedWith: true },
|
||||
dispose: jest.fn(),
|
||||
hide: jest.fn(),
|
||||
show: jest.fn(),
|
||||
sendText: jest.fn(),
|
||||
}
|
||||
|
||||
// Create terminal info
|
||||
const mockTerminalInfo: TerminalInfo = {
|
||||
terminal: mockTerminal,
|
||||
busy: false,
|
||||
lastCommand: "",
|
||||
id: 1,
|
||||
running: false,
|
||||
streamClosed: false,
|
||||
}
|
||||
|
||||
// Add the terminal to the registry
|
||||
TerminalRegistry["terminals"] = [mockTerminalInfo]
|
||||
|
||||
// Create a new terminal process
|
||||
startTime = process.hrtime.bigint() // Start timing from terminal process creation
|
||||
const terminalProcess = new TerminalProcess()
|
||||
|
||||
// Create a terminal manager (this will set up the event handlers)
|
||||
const terminalManager = new TerminalManager()
|
||||
|
||||
try {
|
||||
// Set up the mock stream with real command output
|
||||
const mockStream = createRealCommandStream(command)
|
||||
|
||||
// Configure the mock terminal to return our stream
|
||||
mockTerminal.shellIntegration.executeCommand.mockImplementation(() => {
|
||||
return {
|
||||
read: jest.fn().mockReturnValue(mockStream),
|
||||
}
|
||||
})
|
||||
|
||||
// Set up event listeners to capture output
|
||||
let capturedOutput = ""
|
||||
terminalProcess.on("completed", (output) => {
|
||||
if (!timeRecorded) {
|
||||
endTime = process.hrtime.bigint() // End timing when completed event is received with output
|
||||
timeRecorded = true
|
||||
}
|
||||
if (output) {
|
||||
capturedOutput = output
|
||||
}
|
||||
})
|
||||
|
||||
// Create a promise that resolves when the command completes
|
||||
const completedPromise = new Promise<void>((resolve) => {
|
||||
terminalProcess.once("completed", () => {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
|
||||
// Store the process in the manager's processes map
|
||||
// This is needed for the TerminalManager to find the process when events are triggered
|
||||
terminalManager["processes"].set(mockTerminalInfo.id, terminalProcess)
|
||||
terminalManager["terminalIds"].add(mockTerminalInfo.id)
|
||||
|
||||
// Run the command
|
||||
const runPromise = terminalProcess.run(mockTerminal, command)
|
||||
|
||||
// Get the event handlers from the mock
|
||||
const eventHandlers = (vscode as any).__eventHandlers
|
||||
|
||||
// Trigger the start terminal shell execution event through VSCode mock
|
||||
if (eventHandlers.startTerminalShellExecution) {
|
||||
eventHandlers.startTerminalShellExecution({
|
||||
terminal: mockTerminal,
|
||||
execution: {
|
||||
commandLine: { value: command },
|
||||
read: () => mockStream,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Wait a short time to ensure stream processing has started
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Trigger the end terminal shell execution event through VSCode mock
|
||||
if (eventHandlers.endTerminalShellExecution) {
|
||||
eventHandlers.endTerminalShellExecution({
|
||||
terminal: mockTerminal,
|
||||
exitCode: 0,
|
||||
})
|
||||
}
|
||||
|
||||
// Set a timeout to avoid hanging tests
|
||||
const timeoutPromise = new Promise<void>((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error("Test timed out after 1000ms"))
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
// Wait for the command to complete or timeout
|
||||
await Promise.race([completedPromise, timeoutPromise])
|
||||
|
||||
await runPromise
|
||||
// Calculate execution time in microseconds
|
||||
// If endTime wasn't set (unlikely but possible), set it now
|
||||
if (!timeRecorded) {
|
||||
endTime = process.hrtime.bigint()
|
||||
}
|
||||
const executionTimeUs = Number((endTime - startTime) / BigInt(1000))
|
||||
|
||||
// Verify the output matches the expected output
|
||||
expect(capturedOutput).toBe(expectedOutput)
|
||||
|
||||
return { executionTimeUs, capturedOutput }
|
||||
} finally {
|
||||
// Clean up
|
||||
terminalProcess.removeAllListeners()
|
||||
terminalManager.disposeAll()
|
||||
TerminalRegistry["terminals"] = []
|
||||
}
|
||||
}
|
||||
|
||||
describe("TerminalProcess with Real Command Output", () => {
|
||||
beforeEach(() => {
|
||||
// Reset the terminals array before each test
|
||||
TerminalRegistry["terminals"] = []
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should execute 'echo a' and return exactly 'a\\n' with execution time", async () => {
|
||||
const { executionTimeUs, capturedOutput } = await testTerminalCommand("echo a", "a\n")
|
||||
console.log(`'echo a' execution time: ${executionTimeUs} microseconds (${executionTimeUs / 1000} milliseconds)`)
|
||||
})
|
||||
|
||||
it("should execute 'echo -n a' and return exactly 'a'", async () => {
|
||||
const { executionTimeUs } = await testTerminalCommand("echo -n a", "a")
|
||||
console.log(
|
||||
`'echo -n a' execution time: ${executionTimeUs} microseconds (${executionTimeUs / 1000} milliseconds)`,
|
||||
)
|
||||
})
|
||||
|
||||
it("should execute 'echo -e \"a\\nb\"' and return 'a\\nb\\n'", async () => {
|
||||
const { executionTimeUs } = await testTerminalCommand('echo -e "a\\nb"', "a\nb\n")
|
||||
console.log(
|
||||
`'echo -e "a\\nb"' execution time: ${executionTimeUs} microseconds (${executionTimeUs / 1000} milliseconds)`,
|
||||
)
|
||||
})
|
||||
|
||||
it("should properly handle terminal shell execution events", async () => {
|
||||
// This test is implicitly testing the event handlers since all tests now use them
|
||||
const { executionTimeUs } = await testTerminalCommand("echo test", "test\n")
|
||||
console.log(
|
||||
`'echo test' execution time: ${executionTimeUs} microseconds (${executionTimeUs / 1000} milliseconds)`,
|
||||
)
|
||||
})
|
||||
|
||||
// Configure the number of lines for the base64 test
|
||||
const BASE64_TEST_LINES = 1000000
|
||||
|
||||
it(`should execute 'base64 < /dev/zero | head -n ${BASE64_TEST_LINES}' and verify ${BASE64_TEST_LINES} lines of 'A's`, async () => {
|
||||
// Create an expected output pattern that matches what base64 produces
|
||||
// Each line is 76 'A's followed by a newline
|
||||
const expectedOutput = Array(BASE64_TEST_LINES).fill("A".repeat(76)).join("\n") + "\n"
|
||||
|
||||
// This command will generate BASE64_TEST_LINES lines of base64 encoded zeros
|
||||
// Each line will contain 76 'A' characters (base64 encoding of zeros)
|
||||
const { executionTimeUs, capturedOutput } = await testTerminalCommand(
|
||||
`base64 < /dev/zero | head -n ${BASE64_TEST_LINES}`,
|
||||
expectedOutput,
|
||||
)
|
||||
|
||||
console.log(
|
||||
`'base64 < /dev/zero | head -n ${BASE64_TEST_LINES}' execution time: ${executionTimeUs} microseconds (${executionTimeUs / 1000} milliseconds)`,
|
||||
)
|
||||
|
||||
// Display a truncated output sample (first 3 lines and last 3 lines)
|
||||
const lines = capturedOutput.split("\n")
|
||||
const truncatedOutput =
|
||||
lines.slice(0, 3).join("\n") +
|
||||
`\n... (truncated ${lines.length - 6} lines) ...\n` +
|
||||
lines.slice(Math.max(0, lines.length - 3), lines.length).join("\n")
|
||||
console.log("Output sample (first 3 lines):\n", truncatedOutput)
|
||||
// Verify the output
|
||||
|
||||
// Check if we have BASE64_TEST_LINES lines (may have an empty line at the end)
|
||||
expect(lines.length).toBeGreaterThanOrEqual(BASE64_TEST_LINES)
|
||||
|
||||
// Sample some lines to verify they contain 76 'A' characters
|
||||
// Sample indices at beginning, 1%, 10%, 50%, and end of the output
|
||||
const sampleIndices = [
|
||||
0,
|
||||
Math.floor(BASE64_TEST_LINES * 0.01),
|
||||
Math.floor(BASE64_TEST_LINES * 0.1),
|
||||
Math.floor(BASE64_TEST_LINES * 0.5),
|
||||
BASE64_TEST_LINES - 1,
|
||||
].filter((i) => i < lines.length)
|
||||
for (const index of sampleIndices) {
|
||||
expect(lines[index]).toBe("A".repeat(76))
|
||||
}
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Reference in a new issue