From 1bb39bfd3ed1c910689de88fb16c247f58bc9e65 Mon Sep 17 00:00:00 2001 From: Brian Ignacio Date: Fri, 26 Sep 2025 20:41:43 +0800 Subject: [PATCH 1/7] use serialport to read unity output parse test results --- src/espIdf/unitTest/adapter.ts | 169 ++++++++--- .../unitTest/unityRunner/serialCapture.ts | 278 ++++++++++++++++++ src/espIdf/unitTest/unityRunner/types.ts | 57 ++++ .../unitTest/unityRunner/unityParser.ts | 131 +++++++++ .../unitTest/unityRunner/unityTestRunner.ts | 189 ++++++++++++ 5 files changed, 776 insertions(+), 48 deletions(-) create mode 100644 src/espIdf/unitTest/unityRunner/serialCapture.ts create mode 100644 src/espIdf/unitTest/unityRunner/types.ts create mode 100644 src/espIdf/unitTest/unityRunner/unityParser.ts create mode 100644 src/espIdf/unitTest/unityRunner/unityTestRunner.ts diff --git a/src/espIdf/unitTest/adapter.ts b/src/espIdf/unitTest/adapter.ts index 115fcafc6..e42db8952 100644 --- a/src/espIdf/unitTest/adapter.ts +++ b/src/espIdf/unitTest/adapter.ts @@ -21,7 +21,6 @@ import { basename } from "path"; import { ExtensionContext, TestController, - window, tests, Uri, CancellationToken, @@ -30,12 +29,17 @@ import { TestItem, TestMessage, workspace, + Location, + Range, } from "vscode"; import { EspIdfTestItem, idfTestData } from "./types"; import { runPyTestWithTestCase } from "./testExecution"; import { configurePyTestUnitApp } from "./configure"; import { getFileList, getTestComponents } from "./utils"; import { ESP } from "../../config"; +import { UnityTestRunner } from "./unityRunner/unityTestRunner"; +import { readParameter } from "../../idfConfiguration"; +import { UnityParserOptions } from "./unityRunner/types"; const unitTestControllerId = "IDF_UNIT_TEST_CONTROLLER"; const unitTestControllerLabel = "ESP-IDF Unit test controller"; @@ -68,70 +72,139 @@ export class UnitTest { const queue: TestItem[] = []; if (request.include) { - request.include.forEach((test) => queue.push(test)); + request.include.forEach((test) => { + test.children.forEach((testChild) => { + queue.push(testChild); + }); + queue.push(test); + }); } else { this.unitTestController.items.forEach((t) => queue.push(t)); } + let workspaceFolderUri: Uri | undefined; if (!this.unitTestAppUri) { - let workspaceFolderUri = ESP.GlobalConfiguration.store.get( - ESP.GlobalConfiguration.SELECTED_WORKSPACE_FOLDER - ); - if (!workspaceFolderUri) { - workspaceFolderUri = workspace.workspaceFolders - ? workspace.workspaceFolders[0].uri + try { + // Get stored workspace folder URI and ensure it's a proper vscode.Uri object + const storedUri = ESP.GlobalConfiguration.store.get( + ESP.GlobalConfiguration.SELECTED_WORKSPACE_FOLDER + ); + let workspaceFolder = workspace.getWorkspaceFolder(storedUri); + + workspaceFolderUri = workspaceFolder + ? workspaceFolder.uri : undefined; - } - if (!workspaceFolderUri) { + + // Fallback to first workspace folder if no stored URI or conversion failed + if (!workspaceFolderUri && workspace.workspaceFolders?.length > 0) { + workspaceFolderUri = workspace.workspaceFolders[0].uri; + } + + if (!workspaceFolderUri) { + console.warn( + "No workspace folder available for unit test configuration" + ); + return; + } + + this.unitTestAppUri = await configurePyTestUnitApp( + workspaceFolderUri, + this.testComponents, + cancelToken + ); + } catch (error) { + console.error("Failed to configure unit test app:", error); return; } - this.unitTestAppUri = await configurePyTestUnitApp( - workspaceFolderUri, - this.testComponents, - cancelToken - ); } - while (queue.length > 0 && !cancelToken.isCancellationRequested) { - const test = queue.pop(); + const runner = new UnityTestRunner(); + const serialPort = readParameter( + "idf.port", + workspaceFolderUri + ) as string; + const baudRate = + (readParameter("idf.baudRate", workspaceFolderUri) as number) || 115200; + const runnerOptions: UnityParserOptions = { + port: serialPort, + baudRate: baudRate, + showOutput: true, + }; + try { + await runner.runFromSerial(runnerOptions); - if (request.exclude?.includes(test)) { - continue; - } + while (queue.length > 0 && !cancelToken.isCancellationRequested) { + const test = queue.pop(); - const idfTestitem = idfTestData.get(test); + if (request.exclude?.includes(test)) { + continue; + } - if (testRun.token.isCancellationRequested) { - testRun.skipped(test); - } else if (idfTestitem.type !== "suite") { - testRun.appendOutput(`Running ${test.id}\r\n`); - testRun.started(test); - const startTime = Date.now(); - try { - const result = await runPyTestWithTestCase( - this.unitTestAppUri, - idfTestitem.testName, - cancelToken - ); - result - ? testRun.passed(test, Date.now() - startTime) - : testRun.failed( - test, - new TestMessage("Error in test"), - Date.now() - startTime + const idfTestitem = idfTestData.get(test); + + if (testRun.token.isCancellationRequested) { + testRun.skipped(test); + } else if (idfTestitem.type !== "suite") { + testRun.appendOutput(`Running ${test.id}\r\n`); + testRun.started(test); + const startTime = Date.now(); + try { + const result = await runner.runTestFromSerialByName(test.label); + if (!result) { + throw new Error("No result from test execution"); + } + testRun.appendOutput( + result.output + `\r\n`, + test.uri && typeof result.lineNumber === "number" + ? new Location( + test.uri, + new Range(result.lineNumber, 0, result.lineNumber, 0) + ) + : undefined, + test + ); + if (result.status === "PASS") { + testRun.passed(test, result.duration); + } else if (result.status === "FAIL") { + const message = new TestMessage( + result.message || "Test failed" ); - } catch (error) { - testRun.failed( - test, - new TestMessage(error.message), - Date.now() - startTime - ); + testRun.failed(test, message, result.duration); + } else if (result.status === "IGNORE") { + testRun.skipped(test); + } + // const result = await runPyTestWithTestCase( + // this.unitTestAppUri, + // idfTestitem.testName, + // cancelToken + // ); + // result + // ? testRun.passed(test, Date.now() - startTime) + // : testRun.failed( + // test, + // new TestMessage("Error in test"), + // Date.now() - startTime + // ); + } catch (error) { + testRun.failed( + test, + new TestMessage(error.message), + Date.now() - startTime + ); + runner.stop(); + } + testRun.appendOutput(`\r\nCompleted ${test.id}\r\n`); } - testRun.appendOutput(`Completed ${test.id}\r\n`); + test.children.forEach((t) => queue.push(t)); } - test.children.forEach((t) => queue.push(t)); + } catch (error) { + testRun.appendOutput(`Error: ${error.message}\r\n`); + runner.stop(); + testRun.end(); + } finally { + runner.stop(); + testRun.end(); } - testRun.end(); }; this.unitTestController.createRunProfile( diff --git a/src/espIdf/unitTest/unityRunner/serialCapture.ts b/src/espIdf/unitTest/unityRunner/serialCapture.ts new file mode 100644 index 000000000..ba94dcab0 --- /dev/null +++ b/src/espIdf/unitTest/unityRunner/serialCapture.ts @@ -0,0 +1,278 @@ +/** + * Serial Port Capture for Unity Test Output + * Handles serial communication with ESP32 devices + */ + +import { SerialPort } from 'serialport'; +import { ReadlineParser } from '@serialport/parser-readline'; +import { UnityParserOptions, SerialPortConfig } from "./types" +import { EventEmitter } from 'events'; +import { promises } from 'fs'; + +export class UnitySerialCapture extends EventEmitter { + private port: SerialPort | null = null; + private parser: ReadlineParser | null = null; + private config: SerialPortConfig; + private isCapturing = false; + private capturedLines: string[] = []; + + constructor(config: SerialPortConfig) { + super(); + this.config = config; + } + + /** + * Connect to serial port + */ + async connect(): Promise { + try { + this.port = new SerialPort({ + path: this.config.port, + baudRate: this.config.baudRate, + autoOpen: false + }); + + this.parser = this.port.pipe(new ReadlineParser({ delimiter: '\n' })); + + // Set up event handlers + this.port.on('open', () => { + console.log(`Connected to ${this.config.port} at ${this.config.baudRate} baud`); + this.emit('connected'); + }); + + this.port.on('error', (err) => { + console.error(`Serial port error: ${err.message}`); + this.emit('error', err); + }); + + this.parser.on('data', (line: string) => { + const trimmedLine = line.trim(); + if (trimmedLine) { + this.capturedLines.push(trimmedLine); + this.emit('data', trimmedLine); + console.log(`[${new Date().toISOString()}] ${trimmedLine}`); + } + }); + + // Open the port + await new Promise((resolve, reject) => { + if (!this.port) { + reject(new Error('Port not initialized')); + return; + } + + this.port.open((err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + + // Perform hard reset after successful connection + await this.hardReset(); + + return true; + } catch (error) { + console.error(`Failed to connect to ${this.config.port}:`, error); + return false; + } + } + + /** + * Disconnect from serial port + */ + async disconnect(): Promise { + if (this.port && this.port.isOpen) { + await new Promise((resolve) => { + this.port!.close(() => { + console.log('Disconnected from serial port'); + this.emit('disconnected'); + resolve(); + }); + }); + } + this.port = null; + this.parser = null; + this.isCapturing = false; + } + + /** + * Hard reset the serial port + * This method performs a hardware reset by toggling DTR and RTS lines + */ + async hardReset(): Promise { + if (!this.port || !this.port.isOpen) { + throw new Error('Port not connected'); + } + + try { + console.log('Performing hard reset...'); + + // Set DTR and RTS to false (reset state) + this.port.set({ dtr: false, rts: false }); + await new Promise(resolve => setTimeout(resolve, 100)); // Wait 100ms + + // Set DTR and RTS to true (normal state) + this.port.set({ dtr: true, rts: true }); + await new Promise(resolve => setTimeout(resolve, 100)); // Wait 100ms + + // Set DTR to false and RTS to true (boot mode for ESP32) + this.port.set({ dtr: false, rts: true }); + await new Promise(resolve => setTimeout(resolve, 100)); // Wait 100ms + + // Set both to true (normal operation) + this.port.set({ dtr: true, rts: true }); + await new Promise(resolve => setTimeout(resolve, 100)); // Wait 100ms + + console.log('Hard reset completed'); + this.emit('hardResetCompleted'); + } catch (error) { + console.error('Hard reset failed:', error); + this.emit('hardResetFailed', error); + throw error; + } + } + + /** + * Start capturing output + */ + startCapture(): void { + this.isCapturing = true; + this.capturedLines = []; + this.emit('captureStarted'); + } + + /** + * Stop capturing output + */ + stopCapture(): void { + this.isCapturing = false; + this.emit('captureStopped'); + } + + /** + * Capture output for a specified duration + */ + async captureForDuration(durationMs: number): Promise { + this.startCapture(); + + return new Promise((resolve) => { + const timeout = setTimeout(() => { + this.stopCapture(); + resolve([...this.capturedLines]); + }, durationMs); + + // Allow manual stopping + this.once('captureStopped', () => { + clearTimeout(timeout); + resolve([...this.capturedLines]); + }); + }); + } + + /** + * Get captured lines + */ + getCapturedLines(): string[] { + return [...this.capturedLines]; + } + + /** + * Clear captured lines + */ + clearCapturedLines(): void { + this.capturedLines = []; + } + + /** + * Save captured output to file + */ + async saveToFile(filePath: string): Promise { + const content = this.capturedLines.join('\n'); + await promises.writeFile(filePath, content, 'utf-8'); + console.log(`Output saved to ${filePath}`); + } + + /** + * Check if port is connected + */ + isConnected(): boolean { + return this.port !== null && this.port.isOpen; + } + + /** + * Get port information + */ + getPortInfo(): { port: string; baudRate: number; isOpen: boolean } { + return { + port: this.config.port, + baudRate: this.config.baudRate, + isOpen: this.isConnected() + }; + } + + /** + * List available serial ports + */ + static async listPorts(): Promise> { + try { + const ports = await SerialPort.list(); + return ports.map(port => ({ + path: port.path, + manufacturer: port.manufacturer, + serialNumber: port.serialNumber, + pnpId: port.pnpId, + locationId: port.locationId, + vendorId: port.vendorId, + productId: port.productId + })); + } catch (error) { + console.error('Failed to list serial ports:', error); + return []; + } + } + + /** + * Wait for specific pattern in output + */ + async waitForPattern(pattern: RegExp, timeoutMs: number = 10000): Promise { + return new Promise((resolve) => { + const timeout = setTimeout(() => { + this.parser?.removeListener('data', onData); + resolve(null); + }, timeoutMs); + + const onData = (line: string) => { + if (pattern.test(line)) { + clearTimeout(timeout); + this.parser?.removeListener('data', onData); + resolve(line); + } + }; + + this.parser?.on('data', onData); + }); + } + + /** + * Send command to serial port + */ + async sendCommand(command: string): Promise { + if (!this.port || !this.port.isOpen) { + throw new Error('Port not connected'); + } + + return new Promise((resolve, reject) => { + this.port!.write(command + '\n', (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + } +} + diff --git a/src/espIdf/unitTest/unityRunner/types.ts b/src/espIdf/unitTest/unityRunner/types.ts new file mode 100644 index 000000000..286867060 --- /dev/null +++ b/src/espIdf/unitTest/unityRunner/types.ts @@ -0,0 +1,57 @@ +/** + * TypeScript interfaces for Unity test results + */ + +export interface UnityTestResult { + name: string; + status: 'PASS' | 'FAIL' | 'IGNORE'; + message?: string; + line?: number; + duration?: number; +} + +export interface IndividualTestResult { + filePath: string; + lineNumber: number; + testName: string; + status: 'PASS' | 'FAIL' | 'IGNORE'; + duration: number; + message?: string; + output: string; +} + +export interface UnityTestSuite { + name: string; + tests: UnityTestResult[]; + totalTests: number; + passedTests: number; + failedTests: number; + ignoredTests: number; + duration: number; +} + +export interface UnityTestSummary { + totalTests: number; + totalFailures: number; + totalIgnored: number; + totalDuration: number; +} + +export interface UnityParseResult { + suites: UnityTestSuite[]; + summary: UnityTestSummary; +} + +export interface SerialPortConfig { + port: string; + baudRate: number; + timeout?: number; +} + +export interface UnityParserOptions { + port: string; + baudRate?: number; + timeout?: number; + showOutput?: boolean; +} + diff --git a/src/espIdf/unitTest/unityRunner/unityParser.ts b/src/espIdf/unitTest/unityRunner/unityParser.ts new file mode 100644 index 000000000..a388e6997 --- /dev/null +++ b/src/espIdf/unitTest/unityRunner/unityParser.ts @@ -0,0 +1,131 @@ +/** + * Unity Test Output Parser + * Parses Unity test framework output and converts to structured data + */ + +import { IndividualTestResult } from './types'; + +export class UnityParser { + + // Patterns for individual test parsing + private individualTestStartPattern = /Running\s+(.+?)\s*\.\.\./; + private individualTestResultPattern = /^(.+):(\d+):(.+?):(PASS|FAIL|IGNORE)(?:[:]\s*(.+))?$/; + private individualTestDurationPattern = /Test ran in\s+(\d+)(?:ms|s)/; + + /** + * Parse individual test result from a single test run + * Expects format like: + * Running Mean of an empty array is zero... + * /path/to/file.c:16:Mean of an empty array is zero:PASS + * Test ran in 17ms + */ + parseIndividualTest(lines: string[]): IndividualTestResult | null { + let testName = ''; + let filePath = ''; + let lineNumber = 0; + let status: 'PASS' | 'FAIL' | 'IGNORE' = 'PASS'; + let duration = 0; + let message = ''; + + for (const line of lines) { + const trimmedLine = line.trim(); + if (!trimmedLine) continue; + + // Check for test start + const startMatch = this.individualTestStartPattern.exec(trimmedLine); + if (startMatch) { + testName = startMatch[1]; + continue; + } + + // Check for test result line + const resultMatch = this.individualTestResultPattern.exec(trimmedLine); + if (resultMatch) { + filePath = resultMatch[1]; + lineNumber = parseInt(resultMatch[2], 10); + const resultTestName = resultMatch[3]; + status = resultMatch[4] as 'PASS' | 'FAIL' | 'IGNORE'; + message = resultMatch[5] || ''; + continue; + } + + // Check for duration + const durationMatch = this.individualTestDurationPattern.exec(trimmedLine); + if (durationMatch) { + const durationValue = parseInt(durationMatch[1], 10); + // Keep in milliseconds + duration = durationValue; + continue; + } + } + + // Return result if we have the essential information + if (filePath && testName && status) { + return { + filePath, + lineNumber, + testName, + status, + duration, + message: message || undefined, + output: lines.slice(1).join('\r\n') + }; + } + + return null; + } + + /** + * Parse multiple individual test results from output + */ + parseIndividualTests(lines: string[]): IndividualTestResult[] { + const results: IndividualTestResult[] = []; + let currentTestLines: string[] = []; + let inTest = false; + + for (const line of lines) { + const trimmedLine = line.trim(); + + // Check if we're starting a new test + if (this.individualTestStartPattern.test(trimmedLine)) { + // If we were already in a test, parse the previous one + if (inTest && currentTestLines.length > 0) { + const result = this.parseIndividualTest(currentTestLines); + if (result) { + results.push(result); + } + } + // Start new test + currentTestLines = [line]; + inTest = true; + continue; + } + + // If we're in a test, collect lines until we hit a separator or end + if (inTest) { + if (trimmedLine === '-----------------------' || trimmedLine === '') { + // End of test, parse it + const result = this.parseIndividualTest(currentTestLines); + if (result) { + results.push(result); + } + currentTestLines = []; + inTest = false; + } else { + currentTestLines.push(line); + } + } + } + + // Parse the last test if we were still in one + if (inTest && currentTestLines.length > 0) { + const result = this.parseIndividualTest(currentTestLines); + if (result) { + results.push(result); + } + } + + return results; + } +} + diff --git a/src/espIdf/unitTest/unityRunner/unityTestRunner.ts b/src/espIdf/unitTest/unityRunner/unityTestRunner.ts new file mode 100644 index 000000000..a3d1e6b16 --- /dev/null +++ b/src/espIdf/unitTest/unityRunner/unityTestRunner.ts @@ -0,0 +1,189 @@ +/** + * Unity Test Runner + * Main class that orchestrates serial capture, parsing, and XML generation + */ + +import { UnityParser } from "./unityParser"; +import { UnitySerialCapture } from "./serialCapture"; +import { UnityParserOptions, UnityParseResult, IndividualTestResult } from "./types"; +import { EventEmitter } from "events"; +import { OutputChannel } from "../../../logger/outputChannel"; +import { EOL } from "os"; + +export class UnityTestRunner extends EventEmitter { + private parser: UnityParser; + private serialCapture: UnitySerialCapture | null = null; + private menuLines: string[] = []; + private currentStartIndex: number = -1; + private receivedLines: string[] = []; + + constructor() { + super(); + this.parser = new UnityParser(); + } + + /** + * Run Unity tests from serial port + */ + async runFromSerial(options: UnityParserOptions) { + const { + port, + baudRate = 115200, + timeout = 10000, + showOutput = true, + } = options; + + try { + // Create serial capture instance + this.serialCapture = new UnitySerialCapture({ port, baudRate, timeout }); + + // Set up event handlers + this.serialCapture.on("connected", () => { + this.emit("connected"); + OutputChannel.appendLine( + `Connected to ${port} at ${baudRate} baud.`, + "Unity Test Runner" + ); + }); + + this.serialCapture.on("disconnected", () => { + this.emit("disconnected"); + OutputChannel.appendLine( + `Disconnected from ${port}.`, + "Unity Test Runner" + ); + }); + + this.serialCapture.on("error", (error) => { + this.emit("error", error); + OutputChannel.appendLine( + `Error: ${error.message}`, + "Unity Test Runner" + ); + }); + + if (showOutput) { + this.serialCapture.on("data", (line) => { + this.emit("data", line); + OutputChannel.appendLine(`Received: ${line}`, "Unity Test Runner"); + this.receivedLines.push(line); + }); + } + + // Connect to serial port + const connected = await this.serialCapture.connect(); + if (!connected) { + throw new Error(`Failed to connect to ${port}`); + } + + this.emit("captureStarted"); + + // Wait a moment for connection to stabilize + await this.delay(1000); + + this.serialCapture.sendCommand("\n"); // Send newline to wake up device + await this.delay(500); + + const readyIndex = this.receivedLines.findIndex((line) => + line.includes("Press ENTER to see the list of tests.") + ); + if (readyIndex === -1) { + throw new Error("Did not receive readiness prompt from device."); + } + this.menuLines = this.receivedLines.slice(readyIndex + 1); + this.currentStartIndex = this.receivedLines.length - 1; + + } catch (error) { + this.emit("error", error); + throw error; + } + } + + async runTestFromSerialByName(testName: string): Promise { + const testIndex = this.menuLines.findIndex((line) => + line.includes(testName) + ); + if (testIndex === -1) { + OutputChannel.appendLine( + `Test "${testName}" not found in menu. Skipping.`, + "Unity Test Runner" + ); + return { + filePath: '', + lineNumber: 0, + testName: testName, + status: 'FAIL', + duration: 0, + message: `Test "${testName}" not found in menu`, + output: this.menuLines.join(EOL) + }; + } + this.currentStartIndex = this.receivedLines.length - 1; + this.serialCapture.sendCommand(`${testIndex}\n`); + await this.delay(50); + const testExecutionLines = this.receivedLines.slice( + this.currentStartIndex + 1 + ); + + if (testExecutionLines.length === 0) { + OutputChannel.appendLine( + `No test execution output received.`, + "Unity Test Runner" + ); + return { + filePath: '', + lineNumber: 0, + testName: testName, + status: 'FAIL', + duration: 0, + message: 'No test execution output received', + output: testExecutionLines.join(EOL) + }; + } + + const executionOutput = this.parser.parseIndividualTest(testExecutionLines); + if (!executionOutput) { + return { + filePath: '', + lineNumber: 0, + testName: testName, + status: 'FAIL', + duration: 0, + message: 'Failed to parse test output', + output: testExecutionLines.join(EOL) + }; + } + return executionOutput; + } + + async stop(): Promise { + if (this.serialCapture) { + await this.serialCapture.disconnect(); + this.serialCapture = null; + } + } + + /** + * List available serial ports + */ + static async listPorts(): Promise< + Array<{ + path: string; + manufacturer?: string; + serialNumber?: string; + pnpId?: string; + locationId?: string; + vendorId?: string; + productId?: string; + }> + > { + return UnitySerialCapture.listPorts(); + } + + /** + * Utility method for delays + */ + private delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } +} From 8655d8bad2a5b65df872d8d0ade4775b99cfe431 Mon Sep 17 00:00:00 2001 From: Brian Ignacio Date: Mon, 13 Oct 2025 18:08:51 +0800 Subject: [PATCH 2/7] use readSerialPort instead --- src/espIdf/unitTest/adapter.ts | 20 +++--------------- templates/unity-app/conftest.py | 29 --------------------------- templates/unity-app/test_unit_case.py | 28 -------------------------- 3 files changed, 3 insertions(+), 74 deletions(-) delete mode 100644 templates/unity-app/conftest.py delete mode 100644 templates/unity-app/test_unit_case.py diff --git a/src/espIdf/unitTest/adapter.ts b/src/espIdf/unitTest/adapter.ts index e42db8952..2f3c2b185 100644 --- a/src/espIdf/unitTest/adapter.ts +++ b/src/espIdf/unitTest/adapter.ts @@ -38,7 +38,7 @@ import { configurePyTestUnitApp } from "./configure"; import { getFileList, getTestComponents } from "./utils"; import { ESP } from "../../config"; import { UnityTestRunner } from "./unityRunner/unityTestRunner"; -import { readParameter } from "../../idfConfiguration"; +import { readParameter, readSerialPort } from "../../idfConfiguration"; import { UnityParserOptions } from "./unityRunner/types"; const unitTestControllerId = "IDF_UNIT_TEST_CONTROLLER"; @@ -119,10 +119,8 @@ export class UnitTest { } const runner = new UnityTestRunner(); - const serialPort = readParameter( - "idf.port", - workspaceFolderUri - ) as string; + + const serialPort = await readSerialPort(workspaceFolderUri, false); const baudRate = (readParameter("idf.baudRate", workspaceFolderUri) as number) || 115200; const runnerOptions: UnityParserOptions = { @@ -173,18 +171,6 @@ export class UnitTest { } else if (result.status === "IGNORE") { testRun.skipped(test); } - // const result = await runPyTestWithTestCase( - // this.unitTestAppUri, - // idfTestitem.testName, - // cancelToken - // ); - // result - // ? testRun.passed(test, Date.now() - startTime) - // : testRun.failed( - // test, - // new TestMessage("Error in test"), - // Date.now() - startTime - // ); } catch (error) { testRun.failed( test, diff --git a/templates/unity-app/conftest.py b/templates/unity-app/conftest.py deleted file mode 100644 index a650c4733..000000000 --- a/templates/unity-app/conftest.py +++ /dev/null @@ -1,29 +0,0 @@ -''' -Project: ESP-IDF VSCode Extension -File Created: Thursday, 20th July 2023 1:02:21 pm -Copyright 2023 Espressif Systems (Shanghai) CO LTD - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -''' - - -def pytest_addoption(parser): - parser.addoption("--test-name", action="store", default="test-name") - - -def pytest_generate_tests(metafunc): - # This is called for every test. Only get/set command line arguments - # if the argument is specified in the list of test "fixturenames". - option_value = metafunc.config.option.test_name - if 'test_name' in metafunc.fixturenames and option_value is not None: - metafunc.parametrize("test_name", [option_value]) diff --git a/templates/unity-app/test_unit_case.py b/templates/unity-app/test_unit_case.py deleted file mode 100644 index eac6363eb..000000000 --- a/templates/unity-app/test_unit_case.py +++ /dev/null @@ -1,28 +0,0 @@ -''' -Project: ESP-IDF VSCode Extension -File Created: Thursday, 20th July 2023 12:58:32 pm -Copyright 2023 Espressif Systems (Shanghai) CO LTD - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -''' -import pytest -from pytest_embedded import Dut - - -def test_unit_test(dut: Dut, test_name: str) -> None: - if test_name == "TEST_ALL": - dut.run_all_single_board_cases() - elif test_name[:11] == "TEST_GROUP=": - dut.run_all_single_board_cases(test_name[12:]) - else: - dut.run_single_board_case(test_name) From 6e07beec1280114a8bcf22283d6b62a3c0968319 Mon Sep 17 00:00:00 2001 From: Brian Ignacio Date: Fri, 17 Oct 2025 12:08:19 +0800 Subject: [PATCH 3/7] rm console log references and pytest --- src/espIdf/unitTest/adapter.ts | 16 ++-- src/espIdf/unitTest/configure.ts | 75 +------------------ .../unitTest/unityRunner/serialCapture.ts | 23 +++--- 3 files changed, 21 insertions(+), 93 deletions(-) diff --git a/src/espIdf/unitTest/adapter.ts b/src/espIdf/unitTest/adapter.ts index 2f3c2b185..e9f0c3f44 100644 --- a/src/espIdf/unitTest/adapter.ts +++ b/src/espIdf/unitTest/adapter.ts @@ -33,20 +33,19 @@ import { Range, } from "vscode"; import { EspIdfTestItem, idfTestData } from "./types"; -import { runPyTestWithTestCase } from "./testExecution"; -import { configurePyTestUnitApp } from "./configure"; -import { getFileList, getTestComponents } from "./utils"; +import { configureUnityApp } from "./configure"; +import { getFileList } from "./utils"; import { ESP } from "../../config"; import { UnityTestRunner } from "./unityRunner/unityTestRunner"; import { readParameter, readSerialPort } from "../../idfConfiguration"; import { UnityParserOptions } from "./unityRunner/types"; +import { Logger } from "../../logger/logger"; const unitTestControllerId = "IDF_UNIT_TEST_CONTROLLER"; const unitTestControllerLabel = "ESP-IDF Unit test controller"; export class UnitTest { public unitTestController: TestController; - private testComponents: string[]; private unitTestAppUri: Uri; constructor(context: ExtensionContext) { @@ -60,7 +59,6 @@ export class UnitTest { ) => { this.clearExistingTestCaseItems(); const fileList = await getFileList(); - this.testComponents = await getTestComponents(fileList); await this.loadTests(fileList); }; @@ -101,19 +99,18 @@ export class UnitTest { } if (!workspaceFolderUri) { - console.warn( + Logger.warn( "No workspace folder available for unit test configuration" ); return; } - this.unitTestAppUri = await configurePyTestUnitApp( + this.unitTestAppUri = await configureUnityApp( workspaceFolderUri, - this.testComponents, cancelToken ); } catch (error) { - console.error("Failed to configure unit test app:", error); + Logger.error("Failed to configure unit test app:", error, "unitTest runHandler configurePytestUnitApp"); return; } } @@ -206,7 +203,6 @@ export class UnitTest { if (!item) { const fileList = await getFileList(); await this.loadTests(fileList); - this.testComponents = await getTestComponents(fileList); return; } const espIdfTestItem = await this.getTestsForFile(item.uri); diff --git a/src/espIdf/unitTest/configure.ts b/src/espIdf/unitTest/configure.ts index 4d26a5aa7..72e2e3632 100644 --- a/src/espIdf/unitTest/configure.ts +++ b/src/espIdf/unitTest/configure.ts @@ -21,28 +21,20 @@ import { ESP } from "../../config"; import { join } from "path"; import { copy, pathExists, readFile, writeFile } from "fs-extra"; import { readParameter, readSerialPort } from "../../idfConfiguration"; -import { startPythonReqsProcess } from "../../utils"; -import { runTaskForCommand } from "./testExecution"; import { buildCommand } from "../../build/buildCmd"; import { verifyCanFlash } from "../../flash/flashCmd"; import { jtagFlashCommand } from "../../flash/jtagCmd"; import { flashCommand } from "../../flash/uartFlash"; import { OutputChannel } from "../../logger/outputChannel"; import { Logger } from "../../logger/logger"; -import { getVirtualEnvPythonPath } from "../../pythonManager"; import { getFileList, getTestComponents } from "./utils"; import { getIdfTargetFromSdkconfig } from "../../workspaceConfig"; -export async function configurePyTestUnitApp( +export async function configureUnityApp( workspaceFolder: Uri, - testComponents: string[], cancelToken?: CancellationToken ) { try { - const isPyTestInstalled = await checkPytestRequirements(workspaceFolder); - if (!isPyTestInstalled) { - await installPyTestPackages(workspaceFolder, cancelToken); - } let unitTestAppUri = Uri.joinPath(workspaceFolder, "unity-app"); const doesUnitTestAppExists = await pathExists(unitTestAppUri.fsPath); if (!doesUnitTestAppExists) { @@ -56,9 +48,9 @@ export async function configurePyTestUnitApp( const msg = error && error.message ? error.message - : "Error configuring PyTest Unit App for project"; + : "Error configuring Unity App for project"; OutputChannel.appendLine(msg, "idf-unit-test"); - Logger.error(msg, error, "configurePyTestUnitApp"); + Logger.error(msg, error, "configureUnityApp"); } } @@ -95,67 +87,6 @@ export async function updateTestComponents( } } -export async function checkPytestRequirements(workspaceFolder: Uri) { - const idfPath = readParameter("idf.espIdfPath", workspaceFolder); - const pythonBinPath = await getVirtualEnvPythonPath(workspaceFolder); - let requirementsPath = join( - idfPath, - "tools", - "requirements", - "requirements.pytest.txt" - ); - let checkResult: string; - try { - const doesPyTestRequirementsExists = await pathExists(requirementsPath); - if (!doesPyTestRequirementsExists) { - requirementsPath = join( - extensions.getExtension(ESP.extensionID).extensionPath, - "requirements.pytest.txt" - ); - } - checkResult = await startPythonReqsProcess( - pythonBinPath, - idfPath, - requirementsPath - ); - } catch (error) { - checkResult = error && error.message ? error.message : " are not satisfied"; - } - if (checkResult.indexOf("are satisfied") > -1) { - return true; - } - return false; -} - -export async function installPyTestPackages( - workspaceFolder: Uri, - cancelToken?: CancellationToken -) { - const idfPath = readParameter("idf.espIdfPath", workspaceFolder); - const pythonBinPath = await getVirtualEnvPythonPath(workspaceFolder); - let requirementsPath = join( - idfPath, - "tools", - "requirements", - "requirements.pytest.txt" - ); - - const doesPyTestRequirementsExists = await pathExists(requirementsPath); - if (!doesPyTestRequirementsExists) { - requirementsPath = join( - extensions.getExtension(ESP.extensionID).extensionPath, - "requirements.pytest.txt" - ); - } - - await runTaskForCommand( - workspaceFolder, - `"${pythonBinPath}" -m pip install --upgrade --no-warn-script-location -r "${requirementsPath}" --extra-index-url https://dl.espressif.com/pypi`, - "Install Pytest", - cancelToken - ); -} - export async function buildTestApp( unitTestAppDirPath: Uri, cancelToken: CancellationToken diff --git a/src/espIdf/unitTest/unityRunner/serialCapture.ts b/src/espIdf/unitTest/unityRunner/serialCapture.ts index ba94dcab0..9a2335da8 100644 --- a/src/espIdf/unitTest/unityRunner/serialCapture.ts +++ b/src/espIdf/unitTest/unityRunner/serialCapture.ts @@ -8,6 +8,7 @@ import { ReadlineParser } from '@serialport/parser-readline'; import { UnityParserOptions, SerialPortConfig } from "./types" import { EventEmitter } from 'events'; import { promises } from 'fs'; +import { Logger } from '../../../logger/logger'; export class UnitySerialCapture extends EventEmitter { private port: SerialPort | null = null; @@ -36,12 +37,12 @@ export class UnitySerialCapture extends EventEmitter { // Set up event handlers this.port.on('open', () => { - console.log(`Connected to ${this.config.port} at ${this.config.baudRate} baud`); + Logger.info(`Connected to ${this.config.port} at ${this.config.baudRate} baud`); this.emit('connected'); }); this.port.on('error', (err) => { - console.error(`Serial port error: ${err.message}`); + Logger.error(`Serial port error: ${err.message}`, err, "UnitySerialCapture port error"); this.emit('error', err); }); @@ -50,7 +51,7 @@ export class UnitySerialCapture extends EventEmitter { if (trimmedLine) { this.capturedLines.push(trimmedLine); this.emit('data', trimmedLine); - console.log(`[${new Date().toISOString()}] ${trimmedLine}`); + Logger.info(`[${new Date().toISOString()}] ${trimmedLine}`); } }); @@ -75,7 +76,7 @@ export class UnitySerialCapture extends EventEmitter { return true; } catch (error) { - console.error(`Failed to connect to ${this.config.port}:`, error); + Logger.error(`Failed to connect to ${this.config.port}:`, error, "UnitySerialCapture connect"); return false; } } @@ -87,7 +88,7 @@ export class UnitySerialCapture extends EventEmitter { if (this.port && this.port.isOpen) { await new Promise((resolve) => { this.port!.close(() => { - console.log('Disconnected from serial port'); + Logger.info('Disconnected from serial port'); this.emit('disconnected'); resolve(); }); @@ -108,7 +109,7 @@ export class UnitySerialCapture extends EventEmitter { } try { - console.log('Performing hard reset...'); + Logger.info('Performing hard reset...'); // Set DTR and RTS to false (reset state) this.port.set({ dtr: false, rts: false }); @@ -125,11 +126,11 @@ export class UnitySerialCapture extends EventEmitter { // Set both to true (normal operation) this.port.set({ dtr: true, rts: true }); await new Promise(resolve => setTimeout(resolve, 100)); // Wait 100ms - - console.log('Hard reset completed'); + + Logger.info('Hard reset completed'); this.emit('hardResetCompleted'); } catch (error) { - console.error('Hard reset failed:', error); + Logger.error('Hard reset failed:', error, "UnitySerialCapture hardReset"); this.emit('hardResetFailed', error); throw error; } @@ -192,7 +193,7 @@ export class UnitySerialCapture extends EventEmitter { async saveToFile(filePath: string): Promise { const content = this.capturedLines.join('\n'); await promises.writeFile(filePath, content, 'utf-8'); - console.log(`Output saved to ${filePath}`); + Logger.info(`Output saved to ${filePath}`); } /** @@ -229,7 +230,7 @@ export class UnitySerialCapture extends EventEmitter { productId: port.productId })); } catch (error) { - console.error('Failed to list serial ports:', error); + Logger.error('Failed to list serial ports:', error, "UnitySerialCapture listPorts"); return []; } } From 45e40ce03afc4d3bc13c67b85f01b220ada785ea Mon Sep 17 00:00:00 2001 From: Brian Ignacio Date: Fri, 17 Oct 2025 12:24:02 +0800 Subject: [PATCH 4/7] rm isCapturing from UnitySerialCapture --- .../unitTest/unityRunner/serialCapture.ts | 41 +------------------ 1 file changed, 1 insertion(+), 40 deletions(-) diff --git a/src/espIdf/unitTest/unityRunner/serialCapture.ts b/src/espIdf/unitTest/unityRunner/serialCapture.ts index 9a2335da8..7b67c2b39 100644 --- a/src/espIdf/unitTest/unityRunner/serialCapture.ts +++ b/src/espIdf/unitTest/unityRunner/serialCapture.ts @@ -5,7 +5,7 @@ import { SerialPort } from 'serialport'; import { ReadlineParser } from '@serialport/parser-readline'; -import { UnityParserOptions, SerialPortConfig } from "./types" +import { SerialPortConfig } from "./types" import { EventEmitter } from 'events'; import { promises } from 'fs'; import { Logger } from '../../../logger/logger'; @@ -14,7 +14,6 @@ export class UnitySerialCapture extends EventEmitter { private port: SerialPort | null = null; private parser: ReadlineParser | null = null; private config: SerialPortConfig; - private isCapturing = false; private capturedLines: string[] = []; constructor(config: SerialPortConfig) { @@ -96,7 +95,6 @@ export class UnitySerialCapture extends EventEmitter { } this.port = null; this.parser = null; - this.isCapturing = false; } /** @@ -136,43 +134,6 @@ export class UnitySerialCapture extends EventEmitter { } } - /** - * Start capturing output - */ - startCapture(): void { - this.isCapturing = true; - this.capturedLines = []; - this.emit('captureStarted'); - } - - /** - * Stop capturing output - */ - stopCapture(): void { - this.isCapturing = false; - this.emit('captureStopped'); - } - - /** - * Capture output for a specified duration - */ - async captureForDuration(durationMs: number): Promise { - this.startCapture(); - - return new Promise((resolve) => { - const timeout = setTimeout(() => { - this.stopCapture(); - resolve([...this.capturedLines]); - }, durationMs); - - // Allow manual stopping - this.once('captureStopped', () => { - clearTimeout(timeout); - resolve([...this.capturedLines]); - }); - }); - } - /** * Get captured lines */ From 9f667b1451c268b7a79c551b252305f03cab904d Mon Sep 17 00:00:00 2001 From: Brian Ignacio Date: Fri, 17 Oct 2025 12:40:30 +0800 Subject: [PATCH 5/7] update docs, rm pyTestEmbeddedServices setting --- .../en/additionalfeatures/unit-testing.rst | 45 +---- docs_espressif/en/settings.rst | 7 +- .../zh_CN/additionalfeatures/unit-testing.rst | 45 +---- docs_espressif/zh_CN/commands.rst | 2 - docs_espressif/zh_CN/settings.rst | 3 - package.json | 12 -- package.nls.es.json | 1 - package.nls.json | 1 - package.nls.pt.json | 1 - package.nls.ru.json | 1 - package.nls.zh-CN.json | 1 - requirements.pytest.txt | 29 ---- src/espIdf/unitTest/testExecution.ts | 154 ------------------ 13 files changed, 12 insertions(+), 290 deletions(-) delete mode 100644 requirements.pytest.txt delete mode 100644 src/espIdf/unitTest/testExecution.ts diff --git a/docs_espressif/en/additionalfeatures/unit-testing.rst b/docs_espressif/en/additionalfeatures/unit-testing.rst index 1641043a4..d5be60eda 100644 --- a/docs_espressif/en/additionalfeatures/unit-testing.rst +++ b/docs_espressif/en/additionalfeatures/unit-testing.rst @@ -42,55 +42,20 @@ This is the structure from the `ESP-IDF unit_test example `_ to run tests on ESP-IDF devices. The **idf.pyTestEmbeddedServices** configuration setting allows you to specify which embedded services to use when running pytest commands. - -By default, the extension uses ``["esp", "idf"]`` as the embedded services. These services provide the following functionality: - -* **esp**: Enables Espressif-specific functionality including automatic target detection and port confirmation using `esptool` -* **idf**: Provides ESP-IDF project support including automatic flashing of built binaries and parsing of binary information - -You can customize the embedded services by modifying the **idf.pyTestEmbeddedServices** setting in your VS Code settings. For example, you might want to add additional services like: - -* **serial**: For basic serial port communication -* **jtag**: For OpenOCD/GDB utilities -* **qemu**: For running tests on QEMU instead of real hardware -* **wokwi**: For running tests on Wokwi simulation platform - -For a complete list of available services and their capabilities, refer to the `pytest-embedded Services Documentation `_. - -.. note:: - The embedded services you choose will affect the pytest command that gets executed. Make sure the services you specify are compatible with your testing environment and requirements. - Running the tests -------------------------------------------- When you click on the Testing Tab in the `Visual Studio Code Activity bar `_, the extension will try to find all test files and test cases and save the list of test components to add later in step 3. -.. note:: - User needs to install ESP-IDF PyTest python requirements by selecting menu **View** > **Command Palette** and type **ESP-IDF Unit Test: Install ESP-IDF PyTest requirements**. Select the command and see the pytest package installation output. - When it press the run button on a test, it will configure the current project before the tests as follows: -1. Check that PyTest requirements from ESP-IDF are satisfied. - -.. note:: - Unit tests in this extension requires `ESP-IDF PyTest requirements `_ to be installed in your Python virtual environment. - -2. Install ESP-IDF PyTest requirements if they are not found in the python current virtual environment specified in **idf.toolsPath** configuration setting in settings.json. - -3. Copy the unity-app from the extension template and add the test components to the main CMakeLists.txt ``TEST_COMPONENTS`` cmake variable. The extension unity-app is a basic ESP-IDF application with a unity menu that will be built and flashed on the current **idf.port** serial device with all test cases that were found during exploration step. - -.. note:: - You can also create, build and flash the unity test application using the **ESP-IDF Unit Test: Install ESP-IDF PyTest requirements** extension command, which will copy build and flash to your device the generated unit testing application. +1. Copy the unity-app from the extension template and add the test components to the main CMakeLists.txt ``TEST_COMPONENTS`` cmake variable. The extension unity-app is a basic ESP-IDF application with a unity menu that will be built and flashed on the current **idf.port** serial device with all test cases that were found during exploration step. -4. Runs `pytest-embedded `_ a plugin that extends PyTest to run on esp-idf devices and output the results as XML file in the unity-app directory. This is executed as an extension task and the output shown in the terminal (similar to Build and Flash tasks). The pytest command uses the embedded services specified in the **idf.pyTestEmbeddedServices** configuration setting (default: ``["esp", "idf"]``). +2. Build and flash the unity-app to the device. .. note:: - You can customize the embedded services used by pytest by modifying the **idf.pyTestEmbeddedServices** setting in your VS Code settings. This allows you to specify different services or add additional ones as needed for your testing environment. + You can also create, build and flash the unity test application using the **ESP-IDF Unit Test: Build Unit Test App** and **ESP-IDF Unit Test: Flash Unit Test App** extension commands respectively, which will copy build and flash to your device the generated unit testing application. -5. The XML results file is parsed and test results are updated in the Testing tab with test duration. +3. Capture the serial output from the device and parse the test results to show them in the ``Testing`` tab. The output from serial port is also shown in the ``ESP-IDF`` output channel. -6. You can refresh the tests and build the unity-app again with the ``Refresh Tests`` button from the Testing tab. +4. You can refresh the tests and build the unity-app again with the ``Refresh Tests`` button from the ``Testing`` tab. diff --git a/docs_espressif/en/settings.rst b/docs_espressif/en/settings.rst index f0c64d2b1..04e5f445c 100644 --- a/docs_espressif/en/settings.rst +++ b/docs_espressif/en/settings.rst @@ -170,10 +170,10 @@ These settings are used to configure the code coverage colors. - Background color for uncovered lines in dark theme for gcov coverage -PyTest Specific Settings +Unit test Specific Settings ------------------------ -These settings are used to configure unit testing with PyTest. +These settings are used to configure unit testing. .. list-table:: :widths: 25 75 @@ -183,13 +183,10 @@ These settings are used to configure unit testing with PyTest. - Description * - **idf.unitTestFilePattern** - Glob pattern for unit test files to discover (default: ``**/test/test_*.c``) - * - **idf.pyTestEmbeddedServices** - - List of embedded services for pytest execution (default: ``["esp", "idf"]``) This is how the extension uses them: 1. **idf.unitTestFilePattern** is used by the extension to discover unit test files in your project. The default pattern :code:`**/test/test_*.c` looks for C files names starting with "test" in any "test" directory. -2. **idf.pyTestEmbeddedServices** specifies the embedded services to use when running pytest commands. These services are passed to the pytest command as the :code:`--embedded-services` parameter. Extension Behaviour Settings diff --git a/docs_espressif/zh_CN/additionalfeatures/unit-testing.rst b/docs_espressif/zh_CN/additionalfeatures/unit-testing.rst index 71056b5b0..0be97701c 100644 --- a/docs_espressif/zh_CN/additionalfeatures/unit-testing.rst +++ b/docs_espressif/zh_CN/additionalfeatures/unit-testing.rst @@ -42,55 +42,20 @@ .. note:: 您可以通过修改 VS Code 设置中的 **idf.unitTestFilePattern** 设置来自定义测试文件发现模式。这允许您为测试文件使用不同的命名约定或目录结构。 -PyTest 嵌入式服务配置 --------------------------------------- - -扩展使用 `pytest-embedded `_ 在 ESP-IDF 设备上运行测试。**idf.pyTestEmbeddedServices** 配置设置允许您指定运行 pytest 命令时要使用的嵌入式服务。 - -默认情况下,扩展使用 ``["esp", "idf"]`` 作为嵌入式服务。这些服务提供以下功能: - -* **esp**:启用乐鑫特定功能,包括使用 `esptool` 进行自动目标检测和端口确认 -* **idf**:提供 ESP-IDF 项目支持,包括自动烧录构建的二进制文件和解析二进制信息 - -您可以通过修改 VS Code 设置中的 **idf.pyTestEmbeddedServices** 设置来自定义嵌入式服务。例如,您可能想要添加其他服务,如: - -* **serial**:用于基本串口通信 -* **jtag**:用于 OpenOCD/GDB 工具 -* **qemu**:用于在 QEMU 而不是真实硬件上运行测试 -* **wokwi**:用于在 Wokwi 仿真平台上运行测试 - -有关可用服务及其功能的完整列表,请参阅 `pytest-embedded 服务文档 `_。 - -.. note:: - 您选择的嵌入式服务将影响执行的 pytest 命令。确保您指定的服务与您的测试环境和要求兼容。 - 运行测试 -------------------------------------------- 当您点击 `Visual Studio Code 活动栏 `_ 中的测试选项卡时,扩展将尝试查找所有测试文件和测试用例,并保存测试组件列表以便在步骤 3 中添加。 -.. note:: - 用户需要通过选择菜单 **查看** > **命令面板** 并输入 **ESP-IDF 单元测试:安装 ESP-IDF PyTest 要求** 来安装 ESP-IDF PyTest python 要求。选择命令并查看 pytest 包安装输出。 - 当按下测试上的运行按钮时,它将在测试前按如下方式配置当前项目: -1. 检查 ESP-IDF 的 PyTest 要求是否满足。 - -.. note:: - 此扩展中的单元测试需要在您的 Python 虚拟环境中安装 `ESP-IDF PyTest 要求 `_。 - -2. 如果在 settings.json 中 **idf.toolsPath** 配置设置指定的 python 当前虚拟环境中未找到,则安装 ESP-IDF PyTest 要求。 - -3. 从扩展模板复制 unity-app 并将测试组件添加到主 CMakeLists.txt 的 ``TEST_COMPONENTS`` cmake 变量中。扩展 unity-app 是一个基本的 ESP-IDF 应用程序,带有 unity 菜单,将在当前 **idf.port** 串行设备上构建和烧录,包含在探索步骤中找到的所有测试用例。 - -.. note:: - 您也可以使用 **ESP-IDF 单元测试:安装 ESP-IDF PyTest 要求** 扩展命令创建、构建和烧录 unity 测试应用程序,该命令将复制构建并烧录生成的单元测试应用程序到您的设备。 +1. 从扩展模板复制 unity-app 并将测试组件添加到主 CMakeLists.txt 的 ``TEST_COMPONENTS`` cmake 变量中。扩展 unity-app 是一个基本的 ESP-IDF 应用程序,带有 unity 菜单,将在当前 **idf.port** 串行设备上构建和烧录,包含在探索步骤中找到的所有测试用例。 -4. 运行 `pytest-embedded `_,这是一个扩展 PyTest 以在 esp-idf 设备上运行的插件,并在 unity-app 目录中以 XML 文件形式输出结果。这作为扩展任务执行,输出显示在终端中(类似于构建和烧录任务)。pytest 命令使用 **idf.pyTestEmbeddedServices** 配置设置中指定的嵌入式服务(默认:``["esp", "idf"]``)。 +2. 构建并烧录 unity-app 到设备。 .. note:: - 您可以通过修改 VS Code 设置中的 **idf.pyTestEmbeddedServices** 设置来自定义 pytest 使用的嵌入式服务。这允许您指定不同的服务或根据需要为测试环境添加其他服务。 + 您也可以使用 **ESP-IDF 单元测试:构建单元测试应用** 和 **ESP-IDF 单元测试:烧录单元测试应用** 扩展命令分别创建、构建和烧录 unity 测试应用程序,这些命令将复制构建并烧录生成的单元测试应用程序到您的设备。 -5. 解析 XML 结果文件,并在测试选项卡中更新测试结果,显示测试持续时间。 +3. 捕获设备的串口输出并解析测试结果以在 ``测试`` 选项卡中显示。串口输出也会显示在 ``ESP-IDF`` 输出通道中。 -6. 您可以使用测试选项卡中的 ``刷新测试`` 按钮刷新测试并再次构建 unity-app。 +4. 您可以使用 ``测试`` 选项卡中的 ``刷新测试`` 按钮刷新测试并再次构建 unity-app。 diff --git a/docs_espressif/zh_CN/commands.rst b/docs_espressif/zh_CN/commands.rst index 2dd9b03d3..c19d7fc5e 100644 --- a/docs_espressif/zh_CN/commands.rst +++ b/docs_espressif/zh_CN/commands.rst @@ -134,5 +134,3 @@ - 将当前项目的单元测试应用程序烧录到连接的设备上。详情请参阅 :ref:`单元测试 `。 * - 单元测试:构建并烧录单元测试应用程序 - 复制当前项目中的单元测试应用程序,构建当前项目并将单元测试应用程序烧录到连接的设备上。详情请参阅 :ref:`单元测试 `。 - * - 单元测试:安装 ESP-IDF PyTest 依赖项 - - 安装 ESP-IDF Pytest 依赖项,以便能够执行 ESP-IDF 单元测试。详情请参阅 :ref:`单元测试 `。 diff --git a/docs_espressif/zh_CN/settings.rst b/docs_espressif/zh_CN/settings.rst index 989ea1bfb..a397b9a39 100644 --- a/docs_espressif/zh_CN/settings.rst +++ b/docs_espressif/zh_CN/settings.rst @@ -171,13 +171,10 @@ PyTest 相关设置 - 描述 * - **idf.unitTestFilePattern** - 用于发现单元测试文件的 glob 模式(默认值:``**/test/test_*.c``) - * - **idf.pyTestEmbeddedServices** - - pytest 执行的内嵌服务列表(默认值:``["esp", "idf"]``) 扩展将按照以下方式使用上述设置: 1. **idf.unitTestFilePattern** 用于扩展在项目中发现单元测试文件。默认模式 :code:`**/test/test_*.c` 会在任何 "test" 目录中查找以 "test" 开头的 C 文件。 -2. **idf.pyTestEmbeddedServices** 指定运行 pytest 命令时使用的内嵌服务。这些服务会作为 :code:`--embedded-services` 参数传递给 pytest 命令。 扩展行为设置 diff --git a/package.json b/package.json index 7e57822eb..9c5030051 100644 --- a/package.json +++ b/package.json @@ -1278,18 +1278,6 @@ "scope": "resource", "default": "**/test/test_*.c" }, - "idf.pyTestEmbeddedServices": { - "type": "array", - "description": "%param.pyTestEmbeddedServices.title%", - "scope": "resource", - "default": [ - "esp", - "idf" - ], - "items": { - "type": "string" - } - }, "idf.serialPortDetectionTimeout": { "type": "number", "default": 60, diff --git a/package.nls.es.json b/package.nls.es.json index 11ff94d0c..7a073c146 100644 --- a/package.nls.es.json +++ b/package.nls.es.json @@ -186,7 +186,6 @@ "param.usbSerialPortFilters.productId": "Número hexadecimal del USB productID como texto.", "param.hasWalkthroughBeenShown": "Indica si se ha mostrado el recorrido de bienvenida", "param.unitTestFilePattern.title": "Patrón glob para descubrir archivos de prueba unitaria", - "param.pyTestEmbeddedServices.title": "Lista de servicios integrados para la ejecución de pytest", "param.serialPortDetectionTimeout": "Tiempo de espera en segundos para la detección del puerto serie usando esptool.py", "trace.poll_period.description": "poll_period se establecerá para el rastreo de la aplicación", "trace.skip_size.description": "skip_size se establecerá para el rastreo de la aplicación", diff --git a/package.nls.json b/package.nls.json index 9cb08f006..f07d63dab 100644 --- a/package.nls.json +++ b/package.nls.json @@ -187,7 +187,6 @@ "param.usbSerialPortFilters.productId": "USB Product ID hex number as string, e.g., 0x6010", "param.hasWalkthroughBeenShown": "Has the walkthrough been shown", "param.unitTestFilePattern.title": "Glob pattern for unit test files to discover", - "param.pyTestEmbeddedServices.title": "List of embedded services for pytest execution", "param.serialPortDetectionTimeout": "Timeout in seconds for serial port detection using esptool.py", "trace.poll_period.description": "poll_period will be set for the apptrace", "trace.skip_size.description": "skip_size will be set for the apptrace", diff --git a/package.nls.pt.json b/package.nls.pt.json index 7c4d82d29..99391ae09 100644 --- a/package.nls.pt.json +++ b/package.nls.pt.json @@ -185,7 +185,6 @@ "param.usbSerialPortFilters.productId": "Número hexadecimal de USB productID como string.", "param.hasWalkthroughBeenShown": "Mostrar o guia de introdução na inicialização da extensão", "param.unitTestFilePattern.title": "Padrão glob para descobrir arquivos de teste unitário", - "param.pyTestEmbeddedServices.title": "Lista de serviços incorporados para execução do pytest", "param.serialPortDetectionTimeout": "Tempo limite em segundos para detecção de porta serial usando esptool.py", "trace.poll_period.description": "poll_period será definido para o apptrace", "trace.skip_size.description": "skip_size será definido para o apptrace", diff --git a/package.nls.ru.json b/package.nls.ru.json index eacf7e65f..92dc4a8a2 100644 --- a/package.nls.ru.json +++ b/package.nls.ru.json @@ -186,7 +186,6 @@ "param.usbSerialPortFilters.productId": "Шестнадцатеричное число USB Product ID в виде строки, например, 0x6010", "param.hasWalkthroughBeenShown": "Показывать руководство по настройке ESP-IDF при первом запуске", "param.unitTestFilePattern.title": "Шаблон glob для обнаружения файлов модульных тестов", - "param.pyTestEmbeddedServices.title": "Список встроенных сервисов для выполнения pytest", "param.serialPortDetectionTimeout": "Тайм-аут в секундах для обнаружения последовательного порта с помощью esptool.py", "trace.poll_period.description": "для apptrace будет установлен параметр poll_ period", "trace.skip_size.description": "для apptrace будет установлен параметр skip_size", diff --git a/package.nls.zh-CN.json b/package.nls.zh-CN.json index c7da2ed3e..a68d9ed9c 100644 --- a/package.nls.zh-CN.json +++ b/package.nls.zh-CN.json @@ -187,7 +187,6 @@ "param.usbSerialPortFilters.productId": "USB 产品 ID 为十六进制格式的的字符串,例如 0x6010", "param.hasWalkthroughBeenShown": "是否已经展示教程", "param.unitTestFilePattern.title": "用于发现单元测试文件的 glob 模式", - "param.pyTestEmbeddedServices.title": "pytest 执行的内嵌服务列表", "param.serialPortDetectionTimeout": "使用 esptool.py 检测串口时的超时时间(秒)", "trace.poll_period.description": "设置 apptrace 的 poll_period 参数", "trace.skip_size.description": "设置 apptrace 的 skip_size 参数", diff --git a/requirements.pytest.txt b/requirements.pytest.txt deleted file mode 100644 index e49457672..000000000 --- a/requirements.pytest.txt +++ /dev/null @@ -1,29 +0,0 @@ -# Python package requirements for pytest in ESP-IDF. -# This feature can be enabled by running "install.{sh,bat,ps1,fish} --enable-pytest" - -pytest-embedded-serial-esp -pytest-embedded-idf -pytest-embedded-jtag -pytest-embedded-qemu -pytest-rerunfailures -pytest-timeout - -# build -idf-build-apps - -# dependencies in pytest test scripts -scapy -websocket-client -netifaces -rangehttpserver -dbus-python; sys_platform == 'linux' -protobuf -paho-mqtt -paramiko -netmiko - -# iperf_test_util -pyecharts - -# for twai tests, communicate with socket can device (e.g. Canable) -python-can \ No newline at end of file diff --git a/src/espIdf/unitTest/testExecution.ts b/src/espIdf/unitTest/testExecution.ts deleted file mode 100644 index 3ef208370..000000000 --- a/src/espIdf/unitTest/testExecution.ts +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Project: ESP-IDF VSCode Extension - * File Created: Wednesday, 19th July 2023 12:05:38 pm - * Copyright 2023 Espressif Systems (Shanghai) CO LTD - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { readFile } from "fs-extra"; -import { - CancellationToken, - ShellExecution, - ShellExecutionOptions, - TaskPanelKind, - TaskPresentationOptions, - TaskRevealKind, - TaskScope, - Uri, - workspace, -} from "vscode"; -import { NotificationMode, readParameter } from "../../idfConfiguration"; -import { appendIdfAndToolsToPath } from "../../utils"; -import { OutputChannel } from "../../logger/outputChannel"; -import { parseStringPromise } from "xml2js"; -import { TaskManager } from "../../taskManager"; -import { Logger } from "../../logger/logger"; - -export async function runPyTestWithTestCase( - workspaceFolder: Uri, - testName: string, - cancelToken?: CancellationToken -) { - try { - const embeddedServices = readParameter( - "idf.pyTestEmbeddedServices", - workspaceFolder - ) as string[]; - const servicesString = embeddedServices ? embeddedServices.join(",") : "esp,idf"; - - await runTaskForCommand( - workspaceFolder, - `pytest --junitxml test.xml --skip-autoflash y --embedded-services ${servicesString} -s --test-name '${testName}'`, - "PyTest Run", - cancelToken - ); - const unitTestResults = await readFile( - Uri.joinPath(workspaceFolder, "test.xml").fsPath - ); - - const xmlResults = await parseStringPromise(unitTestResults); - - if (xmlResults.testsuites) { - for (const testSuite of xmlResults.testsuites.testsuite) { - for (const testCase of testSuite.testcase) { - if ( - Object.prototype.hasOwnProperty.call(testCase, "failure") || - Object.prototype.hasOwnProperty.call(testCase, "error") || - Object.prototype.hasOwnProperty.call(testCase, "skipped") - ) { - return false; - } else { - return true; - } - } - } - } - } catch (error) { - const msg = - error && error.message - ? error.message - : "Error configuring PyTest Unit App for project"; - OutputChannel.appendLine(msg, "idf-unit-test"); - Logger.error(msg, error, "runPyTestWithTestCase"); - } -} - -export async function runTaskForCommand( - workspaceFolder: Uri, - cmdString: string, - taskName: string, - cancelToken: CancellationToken -) { - cancelToken.onCancellationRequested(() => { - TaskManager.cancelTasks(); - }); - const modifiedEnv = await appendIdfAndToolsToPath(workspaceFolder); - - const options: ShellExecutionOptions = { - cwd: workspaceFolder.fsPath, - env: modifiedEnv, - }; - const shellExecutablePath = readParameter( - "idf.customTerminalExecutable", - workspaceFolder - ) as string; - const shellExecutableArgs = readParameter( - "idf.customTerminalExecutableArgs", - workspaceFolder - ) as string[]; - if (shellExecutablePath) { - options.executable = shellExecutablePath; - } - - if (shellExecutableArgs && shellExecutableArgs.length) { - options.shellArgs = shellExecutableArgs; - } - - const notificationMode = readParameter( - "idf.notificationMode", - workspaceFolder - ) as string; - const showTaskOutput = - notificationMode === NotificationMode.All || - notificationMode === NotificationMode.Output - ? TaskRevealKind.Always - : TaskRevealKind.Silent; - - const testRunPresentationOptions = { - reveal: showTaskOutput, - showReuseMessage: false, - clear: true, - panel: TaskPanelKind.Shared, - } as TaskPresentationOptions; - - const curWorkspaceFolder = workspace.workspaceFolders.find( - (w) => w.uri === workspaceFolder - ); - - const testRunExecution = new ShellExecution(cmdString, options); - - TaskManager.addTask( - { - type: "esp-idf", - command: taskName, - taskId: "idf-test-run-task", - }, - curWorkspaceFolder || TaskScope.Workspace, - "ESP-IDF " + taskName, - testRunExecution, - ["espIdf"], - testRunPresentationOptions - ); - await TaskManager.runTasks(); -} From 3d5015d29c3dc45ba1d1465d09346151775321b9 Mon Sep 17 00:00:00 2001 From: Brian Ignacio Date: Fri, 17 Oct 2025 12:44:15 +0800 Subject: [PATCH 6/7] fix doc issue --- docs_espressif/en/settings.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs_espressif/en/settings.rst b/docs_espressif/en/settings.rst index 04e5f445c..4592f1ef5 100644 --- a/docs_espressif/en/settings.rst +++ b/docs_espressif/en/settings.rst @@ -171,7 +171,7 @@ These settings are used to configure the code coverage colors. Unit test Specific Settings ------------------------- +----------------------------- These settings are used to configure unit testing. From 0afc570e5e77a60256facae22ed17cbe1495d446 Mon Sep 17 00:00:00 2001 From: Brian Ignacio Date: Fri, 17 Oct 2025 14:13:48 +0800 Subject: [PATCH 7/7] rm installPyTest command --- package.json | 5 ---- package.nls.es.json | 1 - package.nls.json | 1 - package.nls.pt.json | 1 - package.nls.ru.json | 1 - package.nls.zh-CN.json | 1 - src/extension.ts | 54 ------------------------------------------ 7 files changed, 64 deletions(-) diff --git a/package.json b/package.json index 9c5030051..fe43db367 100644 --- a/package.json +++ b/package.json @@ -1760,11 +1760,6 @@ "title": "%espIdf.selectNotificationMode.title%", "category": "ESP-IDF" }, - { - "command": "espIdf.unitTest.installPyTest", - "title": "%espIdf.unitTest.installPyTest.title%", - "category": "ESP-IDF" - }, { "command": "espIdf.unitTest.buildFlashUnitTestApp", "title": "%espIdf.unitTest.buildFlashUnitTestApp.title%", diff --git a/package.nls.es.json b/package.nls.es.json index 7a073c146..6a5e7dc97 100644 --- a/package.nls.es.json +++ b/package.nls.es.json @@ -94,7 +94,6 @@ "espIdf.unitTest.buildFlashUnitTestApp.title": "Prueba unitaria: Construir y flashear la aplicación de prueba unitaria para pruebas", "espIdf.unitTest.buildUnitTestApp.title": "Prueba unitaria: Construir aplicación de prueba unitaria", "espIdf.unitTest.flashUnitTestApp.title": "Prueba unitaria: Flashear aplicación de prueba unitaria", - "espIdf.unitTest.installPyTest.title": "Prueba unitaria: Instalar requisitos de PyTest de ESP-IDF", "espIdf.webview.nvsPartitionEditor.title": "Abrir Editor de Partición NVS", "espIdf.welcome.title": "Bienvenido", "espIdf.viewAsHex.title": "Ver como Hexadecimal", diff --git a/package.nls.json b/package.nls.json index f07d63dab..b0380950f 100644 --- a/package.nls.json +++ b/package.nls.json @@ -94,7 +94,6 @@ "espIdf.unitTest.buildFlashUnitTestApp.title": "Unit Test: Build and Flash Unit Test App for Testing", "espIdf.unitTest.buildUnitTestApp.title": "Unit Test: Build Unit Test App", "espIdf.unitTest.flashUnitTestApp.title": "Unit Test: Flash Unit Test App", - "espIdf.unitTest.installPyTest.title": "Unit Test: Install ESP-IDF PyTest Requirements", "espIdf.viewAsHex.title": "View as Hex", "espIdf.hexView.copyValue.title": "Copy value to clipboard", "espIdf.hexView.deleteElement.title": "Delete hex value from list", diff --git a/package.nls.pt.json b/package.nls.pt.json index 99391ae09..464acbc68 100644 --- a/package.nls.pt.json +++ b/package.nls.pt.json @@ -94,7 +94,6 @@ "espIdf.unitTest.buildFlashUnitTestApp.title": "Teste de unidade: crie e atualize o aplicativo de teste de unidade para teste", "espIdf.unitTest.buildUnitTestApp.title": "Teste de unidade: criar aplicativo de teste de unidade", "espIdf.unitTest.flashUnitTestApp.title": "Teste de unidade: atualizar aplicativo de teste de unidade", - "espIdf.unitTest.installPyTest.title": "Teste de unidade: instale os requisitos do ESP-IDF PyTest", "espIdf.webview.nvsPartitionEditor.title": "Abra o Editor de Partição NVS", "espIdf.welcome.title": "Bem-vindo", "espIdf.viewAsHex.title": "Ver como Hexadecimal", diff --git a/package.nls.ru.json b/package.nls.ru.json index 92dc4a8a2..406c59df8 100644 --- a/package.nls.ru.json +++ b/package.nls.ru.json @@ -94,7 +94,6 @@ "espIdf.unitTest.buildFlashUnitTestApp.title": "Unit Test: Сборка и прошивка Unit Test App для тестирования", "espIdf.unitTest.buildUnitTestApp.title": "Unit Test: Сборка Unit Test App", "espIdf.unitTest.flashUnitTestApp.title": "Unit Test: Прошивка Unit Test App", - "espIdf.unitTest.installPyTest.title": "Unit Test: Установка требований ESP-IDF PyTest.", "espIdf.viewAsHex.title": "Просмотреть как шестнадцатеричное", "espIdf.hexView.copyValue.title": "Скопировать значение в буфер обмена", "espIdf.hexView.deleteElement.title": "Удалить шестнадцатеричное значение из списка", diff --git a/package.nls.zh-CN.json b/package.nls.zh-CN.json index a68d9ed9c..cd068ae28 100644 --- a/package.nls.zh-CN.json +++ b/package.nls.zh-CN.json @@ -94,7 +94,6 @@ "espIdf.unitTest.buildFlashUnitTestApp.title": "单元测试:构建并烧录单元测试应用程序", "espIdf.unitTest.buildUnitTestApp.title": "单元测试:构建单元测试应用程序", "espIdf.unitTest.flashUnitTestApp.title": "单元测试:烧录单元测试应用程序", - "espIdf.unitTest.installPyTest.title": "单元测试:安装 ESP-IDF PyTest 依赖项", "espIdf.viewAsHex.title": "以十六进制查看", "espIdf.hexView.copyValue.title": "复制值到剪贴板", "espIdf.hexView.deleteElement.title": "从列表中删除十六进制值", diff --git a/src/extension.ts b/src/extension.ts index 685196d89..b9d03abc9 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -145,10 +145,8 @@ import { UnitTest } from "./espIdf/unitTest/adapter"; import { buildFlashTestApp, buildTestApp, - checkPytestRequirements, copyTestAppProject, flashTestApp, - installPyTestPackages, } from "./espIdf/unitTest/configure"; import { getFileList, getTestComponents } from "./espIdf/unitTest/utils"; import { saveDefSdkconfig } from "./espIdf/menuconfig/saveDefConfig"; @@ -1761,58 +1759,6 @@ export async function activate(context: vscode.ExtensionContext) { }); }); - registerIDFCommand("espIdf.unitTest.installPyTest", () => { - return PreCheck.perform([openFolderCheck], async () => { - try { - const isPyTestInstalled = await checkPytestRequirements(workspaceRoot); - if (isPyTestInstalled) { - return Logger.infoNotify( - vscode.l10n.t("PyTest python packages are already installed.") - ); - } - } catch (error) { - const msg = - error && error.message - ? error.message - : "Error checking PyTest python packages"; - OutputChannel.appendLine(msg, "idf-unit-test"); - Logger.error(msg, error, "extension checkPytestRequirements"); - } - - const notificationMode = idfConf.readParameter( - "idf.notificationMode", - workspaceRoot - ) as string; - const ProgressLocation = - notificationMode === idfConf.NotificationMode.All || - notificationMode === idfConf.NotificationMode.Notifications - ? vscode.ProgressLocation.Notification - : vscode.ProgressLocation.Window; - vscode.window.withProgress( - { - cancellable: true, - location: ProgressLocation, - title: "ESP-IDF:", - }, - async ( - progress: vscode.Progress<{ message: string; increment?: number }>, - cancelToken: vscode.CancellationToken - ) => { - try { - await installPyTestPackages(workspaceRoot, cancelToken); - } catch (error) { - const msg = - error && error.message - ? error.message - : "Error installing PyTest python packages"; - OutputChannel.appendLine(msg, "idf-unit-test"); - Logger.error(msg, error, "extension installPyTestPackages"); - } - } - ); - }); - }); - registerIDFCommand("espIdf.unitTest.buildUnitTestApp", () => { return PreCheck.perform([openFolderCheck], async () => { const notificationMode = idfConf.readParameter(