From acbdab3491174504f6ba46cbf8d61b9d58bcdf94 Mon Sep 17 00:00:00 2001 From: AndriiFilippov Date: Fri, 17 Jul 2026 13:36:33 +0200 Subject: [PATCH 1/4] ci: add Debug test --- .../META-INF/MANIFEST.MF | 1 + .../project/IDFProjectDebugProcessTest.java | 682 ++++++++++++++++++ .../operations/ProjectTestOperations.java | 178 +++++ .../selectors/LaunchBarModeSelector.java | 86 +++ 4 files changed, 947 insertions(+) create mode 100644 tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/executable/cases/project/IDFProjectDebugProcessTest.java create mode 100644 tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/operations/selectors/LaunchBarModeSelector.java diff --git a/tests/com.espressif.idf.ui.test/META-INF/MANIFEST.MF b/tests/com.espressif.idf.ui.test/META-INF/MANIFEST.MF index 1c3f156e7..19dcf5a22 100644 --- a/tests/com.espressif.idf.ui.test/META-INF/MANIFEST.MF +++ b/tests/com.espressif.idf.ui.test/META-INF/MANIFEST.MF @@ -7,6 +7,7 @@ Bundle-RequiredExecutionEnvironment: JavaSE-21 Automatic-Module-Name: com.espressif.idf.tests Require-Bundle: org.eclipse.swtbot.go;bundle-version="2.7.0", org.eclipse.launchbar.core, + org.eclipse.debug.core, slf4j.api, com.espressif.idf.ui;bundle-version="1.0.1" Bundle-ActivationPolicy: lazy diff --git a/tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/executable/cases/project/IDFProjectDebugProcessTest.java b/tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/executable/cases/project/IDFProjectDebugProcessTest.java new file mode 100644 index 000000000..51a719776 --- /dev/null +++ b/tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/executable/cases/project/IDFProjectDebugProcessTest.java @@ -0,0 +1,682 @@ +/******************************************************************************* + * Copyright 2026 Espressif Systems (Shanghai) PTE LTD. All rights reserved. + * Use is subject to license terms. + *******************************************************************************/ +package com.espressif.idf.ui.test.executable.cases.project; + +import static org.eclipse.swtbot.swt.finder.waits.Conditions.widgetIsEnabled; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.lang3.SystemUtils; +import org.eclipse.swtbot.eclipse.finder.SWTWorkbenchBot; +import org.eclipse.swtbot.eclipse.finder.widgets.SWTBotView; +import org.eclipse.swtbot.swt.finder.exceptions.WidgetNotFoundException; +import org.eclipse.swtbot.swt.finder.junit.SWTBotJunit4ClassRunner; +import org.eclipse.swtbot.swt.finder.widgets.SWTBotCheckBox; +import org.eclipse.swtbot.swt.finder.widgets.SWTBotCombo; +import org.eclipse.swtbot.swt.finder.widgets.SWTBotShell; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.MethodSorters; + +import com.espressif.idf.ui.test.common.WorkBenchSWTBot; +import com.espressif.idf.ui.test.common.utility.TestWidgetWaitUtility; +import com.espressif.idf.ui.test.operations.EnvSetupOperations; +import com.espressif.idf.ui.test.operations.ProjectTestOperations; +import com.espressif.idf.ui.test.operations.selectors.LaunchBarConfigSelector; +import com.espressif.idf.ui.test.operations.selectors.LaunchBarModeSelector; +import com.espressif.idf.ui.test.operations.selectors.LaunchBarTargetSelector; + +/** + * Hardware E2E test: create → build → UART flash (ESP32) → switch to debug config with + * ESP32-ETHERNET-KIT → start debugging and verify the session. + *

+ * Mirrors the VS Code hardware debug flow from {@code project-hardware-e2e-test.ts}. + * + * @author Andrii Filippov + * + */ +@SuppressWarnings("restriction") +@RunWith(SWTBotJunit4ClassRunner.class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class IDFProjectDebugProcessTest +{ + private static final String PROJECT_NAME = "NewProjectDebugProcessTest"; + private static final String ESP32_TARGET = "esp32"; + private static final String ETHERNET_KIT_BOARD_PREFIX = "ESP32-ETHERNET-KIT"; + private static final Pattern DEBUG_FATAL_ERROR_PATTERN = Pattern.compile( + "Target failure|Error: .*failed to halt|OpenOCD failed|LIBUSB_ERROR|failed to connect", + Pattern.CASE_INSENSITIVE); + + private static final Pattern[] TARGET_DETECTION_PATTERNS = new Pattern[] { + Pattern.compile("Connected to\\s+(ESP32[-A-Z0-9]*)\\b", Pattern.CASE_INSENSITIVE), + Pattern.compile("Chip type:\\s*(ESP32[-A-Z0-9]*)\\b", Pattern.CASE_INSENSITIVE), + Pattern.compile("Detecting chip type\\.\\.\\.\\s*(ESP32[-A-Z0-9]*)\\b", Pattern.CASE_INSENSITIVE) + }; + + @BeforeClass + public static void beforeTestClass() throws Exception + { + Fixture.loadEnv(); + } + + @AfterClass + public static void tearDown() + { + Fixture.cleanupEnvironment(); + } + + @After + public void afterEachTest() + { + // Always stop OpenOCD/GDB even when an assertion failed mid-test. + Fixture.stopDebugSessionAndKillProcesses(); + } + + @Test + public void givenNewProjectBuiltAndFlashedViaUartWhenDebugWithEthernetKitThenDebugSessionStarts() + throws Exception + { + assumeTrue("Linux only: hardware debug test requires Linux CI/lab boards", SystemUtils.IS_OS_LINUX); + + Fixture.givenNewEspressifIDFProjectIsSelected("EspressIf", "Espressif IDF Project"); + Fixture.givenProjectNameIs(PROJECT_NAME); + Fixture.whenNewProjectIsSelected(); + Fixture.whenTurnOffOpenSerialMonitorAfterFlashingInLaunchConfig(); + + String esp32SerialPort = Fixture.whenDetectEsp32UartSerialPortFromNewEspTargetDialog(); + assumeTrue("Skipping debug test: no ESP32 UART target detected from Serial Port auto-detection", + esp32SerialPort != null); + + Fixture.whenSelectLaunchTargetSerialPort(esp32SerialPort); + Fixture.whenProjectIsBuiltUsingContextMenu(); + Fixture.whenFlashProject(); + Fixture.thenVerifyFlashDoneSuccessfully(); + + assumeTrue("Skipping debug test: ESP32-ETHERNET-KIT board not detected", + Fixture.whenSelectEsp32EthernetKitBoard()); + + Fixture.whenSwitchToDebugModeAndSelectDebugConfig(); + Fixture.whenStartDebugging(); + Fixture.thenVerifyDebugSessionStarted(); + Fixture.thenVerifyNoFatalOpenOcdErrors(); + Fixture.whenStepOver(); + Fixture.thenVerifyDebugSessionStillActive(); + Fixture.whenStopDebugging(); + } + + private static class Fixture + { + private static SWTWorkbenchBot bot; + private static String category; + private static String subCategory; + private static String projectName; + + private static void loadEnv() throws Exception + { + bot = WorkBenchSWTBot.getBot(); + EnvSetupOperations.setupEspressifEnv(bot); + bot.sleep(1000); + ProjectTestOperations.deleteAllProjects(bot); + } + + private static void givenNewEspressifIDFProjectIsSelected(String category, String subCategory) + { + Fixture.category = category; + Fixture.subCategory = subCategory; + } + + private static void givenProjectNameIs(String projectName) + { + Fixture.projectName = projectName; + } + + private static void whenNewProjectIsSelected() throws Exception + { + ProjectTestOperations.setupProject(projectName, category, subCategory, bot); + TestWidgetWaitUtility.waitForOperationsInProgressToFinishAsync(bot); + } + + private static void whenTurnOffOpenSerialMonitorAfterFlashingInLaunchConfig() throws Exception + { + LaunchBarConfigSelector configSelector = new LaunchBarConfigSelector(bot); + configSelector.clickEdit(); + + TestWidgetWaitUtility.waitForDialogToAppear(bot, "Edit Configuration", 20000); + + bot.cTabItem("Main").activate(); + + SWTBotCheckBox checkBox = bot.checkBox("Open Serial Monitor After Flashing"); + if (checkBox.isChecked()) + { + checkBox.click(); + } + + bot.button("OK").click(); + } + + /** + * Uses the same New ESP Target serial-port auto-detection as + * {@code NewEspressifIDFProjectFlashProcessTest}, then returns the first port mapped to esp32. + */ + private static String whenDetectEsp32UartSerialPortFromNewEspTargetDialog() throws Exception + { + TargetPort[] detectedTargets = whenCollectDetectedTargetsFromNewEspTargetDialog(); + + assumeFalse("Skipping hardware debug test: no ESP targets were detected from Serial Port auto-detection", + detectedTargets.length == 0); + + for (TargetPort targetPort : detectedTargets) + { + if (ESP32_TARGET.equals(targetPort.target)) + { + System.out.println("Using ESP32 UART port for flash: " + targetPort.port); + return targetPort.port; + } + } + + System.out.println("No esp32 target among detected ports"); + return null; + } + + private static TargetPort[] whenCollectDetectedTargetsFromNewEspTargetDialog() throws Exception + { + LaunchBarTargetSelector targetSelector = new LaunchBarTargetSelector(bot); + targetSelector.clickEdit(); + + TestWidgetWaitUtility.waitForDialogToAppear(bot, "New ESP Target", 20000); + + SWTBotShell shell = bot.shell("New ESP Target"); + shell.setFocus(); + + SWTBotCheckBox detailedOutput = bot.checkBox("Enable detailed output"); + if (!detailedOutput.isChecked()) + { + detailedOutput.click(); + } + + SWTBotCombo serialPortCombo = bot.comboBoxWithLabel("Serial Port:"); + String[] serialPorts = serialPortCombo.items(); + + List detectedTargets = new ArrayList<>(); + + for (String serialPort : serialPorts) + { + if (serialPort == null || serialPort.trim().isEmpty()) + { + continue; + } + + System.out.println("Checking serial port: " + serialPort); + + String outputBeforeSelection = readTargetDetectionOutput(); + + serialPortCombo.setSelection(serialPort); + + // Wait for target auto-detection output to be printed. + bot.sleep(3000); + + String outputAfterSelection = readTargetDetectionOutput(); + String newOutput = getNewOutputPart(outputBeforeSelection, outputAfterSelection); + + String detectedTarget = extractTargetFromDetectionOutput(newOutput); + + if (detectedTarget == null || detectedTarget.trim().isEmpty()) + { + System.out.println("No ESP target detected for serial port: " + serialPort); + continue; + } + + System.out.println("Detected ESP target: " + detectedTarget + " on port: " + serialPort); + detectedTargets.add(new TargetPort(detectedTarget, serialPort)); + } + + bot.button("Cancel").click(); + + List uniqueTargets = keepFirstPortPerTarget(detectedTargets); + return uniqueTargets.toArray(new TargetPort[0]); + } + + private static List keepFirstPortPerTarget(List targets) + { + Map uniqueTargets = new LinkedHashMap<>(); + + for (TargetPort targetPort : targets) + { + uniqueTargets.putIfAbsent(targetPort.target, targetPort); + } + + return new ArrayList<>(uniqueTargets.values()); + } + + private static void whenSelectLaunchTargetSerialPort(String portPrefixOrExact) throws Exception + { + LaunchBarTargetSelector targetSelector = new LaunchBarTargetSelector(bot); + targetSelector.clickEdit(); + + TestWidgetWaitUtility.waitForDialogToAppear(bot, "New ESP Target", 20000); + + SWTBotShell shell = bot.shell("New ESP Target"); + shell.setFocus(); + + SWTBotCheckBox detailedOutput = bot.checkBox("Enable detailed output"); + if (!detailedOutput.isChecked()) + { + detailedOutput.click(); + } + + SWTBotCombo serialPortCombo = bot.comboBoxWithLabel("Serial Port:"); + selectComboItemByExactOrPrefix(serialPortCombo, portPrefixOrExact); + + TestWidgetWaitUtility.waitForOperationsInProgressToFinishSync(bot); + shell.setFocus(); + bot.button("Finish").click(); + TestWidgetWaitUtility.waitForOperationsInProgressToFinishAsync(bot); + } + + /** + * Selects an ESP32-ETHERNET-KIT board entry from the New ESP Target Board combo. + * + * @return true if a matching board was found and selected + */ + private static boolean whenSelectEsp32EthernetKitBoard() throws Exception + { + LaunchBarTargetSelector targetSelector = new LaunchBarTargetSelector(bot); + targetSelector.clickEdit(); + + TestWidgetWaitUtility.waitForDialogToAppear(bot, "New ESP Target", 20000); + + SWTBotShell shell = bot.shell("New ESP Target"); + shell.setFocus(); + + // Ensure IDF target is esp32 so Ethernet Kit boards are listed. + try + { + bot.comboBoxWithLabel("IDF Target").setSelection(ESP32_TARGET); + bot.sleep(2000); + } + catch (WidgetNotFoundException ignored) + { + // Label text may differ slightly across versions; Board combo is still attempted. + } + + SWTBotCombo boardCombo = bot.comboBoxWithLabel("Board:"); + String[] boards = boardCombo.items(); + String match = null; + + for (String board : boards) + { + if (board != null && board.startsWith(ETHERNET_KIT_BOARD_PREFIX)) + { + match = board; + break; + } + } + + if (match == null) + { + System.out.println("ESP32-ETHERNET-KIT not found in Board combo. Available: " + + String.join(", ", boards)); + bot.button("Cancel").click(); + return false; + } + + System.out.println("Selecting board: " + match); + boardCombo.setSelection(match); + TestWidgetWaitUtility.waitForOperationsInProgressToFinishSync(bot); + shell.setFocus(); + bot.button("Finish").click(); + TestWidgetWaitUtility.waitForOperationsInProgressToFinishAsync(bot); + return true; + } + + private static void whenProjectIsBuiltUsingContextMenu() throws IOException + { + ProjectTestOperations.buildProjectUsingContextMenu(projectName, bot); + ProjectTestOperations.waitForProjectBuild(bot); + TestWidgetWaitUtility.waitForOperationsInProgressToFinishAsync(bot); + } + + private static void whenFlashProject() throws IOException + { + ProjectTestOperations.launchCommandUsingContextMenu(projectName, bot, "Run Configurations..."); + + TestWidgetWaitUtility.waitForDialogToAppear(bot, "Run Configurations", 10000); + + bot.tree().getTreeItem("ESP-IDF Application").select(); + bot.tree().getTreeItem("ESP-IDF Application").expand(); + bot.tree().getTreeItem("ESP-IDF Application").getNode(projectName).select(); + + bot.waitUntil(widgetIsEnabled(bot.button("Run")), 5000); + bot.button("Run").click(); + } + + private static void thenVerifyFlashDoneSuccessfully() throws Exception + { + ProjectTestOperations.waitForProjectFlash(bot); + } + + private static void whenSwitchToDebugModeAndSelectDebugConfig() throws Exception + { + LaunchBarModeSelector modeSelector; + try + { + modeSelector = new LaunchBarModeSelector(bot); + } + catch (WidgetNotFoundException e) + { + modeSelector = new LaunchBarModeSelector(bot, false); + } + modeSelector.select("Debug"); + bot.sleep(1000); + + LaunchBarConfigSelector configSelector = new LaunchBarConfigSelector(bot); + String primaryDebugConfig = projectName + " Debug"; + String fallbackDebugConfig = projectName + " Configuration"; + + if (!trySelectLaunchConfig(configSelector, primaryDebugConfig) + && !trySelectLaunchConfig(configSelector, fallbackDebugConfig)) + { + System.out.println("Debug config not found in Launch Bar; creating via Debug Configurations..."); + ProjectTestOperations.createDebugConfiguration(projectName, bot); + bot.sleep(1000); + + assumeTrue("Could not select a debug launch configuration for project: " + projectName, + trySelectLaunchConfig(configSelector, primaryDebugConfig) + || trySelectLaunchConfig(configSelector, fallbackDebugConfig) + || trySelectLaunchConfig(configSelector, projectName)); + } + + TestWidgetWaitUtility.waitForOperationsInProgressToFinishAsync(bot); + } + + private static boolean trySelectLaunchConfig(LaunchBarConfigSelector configSelector, String configName) + { + try + { + configSelector.select(configName); + System.out.println("Selected launch configuration: " + configName); + return true; + } + catch (WidgetNotFoundException e) + { + return false; + } + } + + private static void whenStartDebugging() + { + ProjectTestOperations.startDebuggingUsingLaunchBar(bot); + } + + private static void thenVerifyDebugSessionStarted() throws Exception + { + ProjectTestOperations.waitForDebugSessionStarted(bot); + } + + private static void thenVerifyNoFatalOpenOcdErrors() + { + String consoleText = readConsoleText(); + assertFalse("Fatal OpenOCD error detected during debug session.\nConsole:\n" + consoleText, + DEBUG_FATAL_ERROR_PATTERN.matcher(consoleText).find()); + } + + private static void whenStepOver() + { + try + { + bot.toolbarButtonWithTooltip("Step Over (F6)").click(); + bot.sleep(3000); + } + catch (WidgetNotFoundException e) + { + // Some Eclipse versions use a slightly different tooltip. + bot.toolbarButtonWithTooltip("Step Over").click(); + bot.sleep(3000); + } + } + + private static void thenVerifyDebugSessionStillActive() + { + String consoleText = readConsoleText(); + assertFalse("Fatal OpenOCD error after Step Over.\nConsole:\n" + consoleText, + DEBUG_FATAL_ERROR_PATTERN.matcher(consoleText).find()); + + boolean stopAvailable = true; + try + { + bot.toolbarButtonWithTooltip("Stop"); + } + catch (WidgetNotFoundException e) + { + stopAvailable = false; + } + assertTrue("Debug/launch session appears to have ended (Stop button missing)", stopAvailable); + } + + private static void whenStopDebugging() + { + stopDebugSessionAndKillProcesses(); + TestWidgetWaitUtility.waitForOperationsInProgressToFinishAsync(bot); + } + + private static void stopDebugSessionAndKillProcesses() + { + ProjectTestOperations.stopDebugSessionAndKillProcesses(bot); + } + + private static void cleanupEnvironment() + { + try + { + stopDebugSessionAndKillProcesses(); + } + catch (Exception ignored) + { + } + + try + { + TestWidgetWaitUtility.waitForOperationsInProgressToFinishAsync(bot); + } + catch (Exception ignored) + { + } + + try + { + ProjectTestOperations.closeAllProjects(bot); + ProjectTestOperations.deleteAllProjects(bot); + } + catch (Exception ignored) + { + } + finally + { + // Final safety net in case UI cleanup left OpenOCD/GDB running. + ProjectTestOperations.killDebugProcesses(); + } + } + + private static void selectComboItemByExactOrPrefix(SWTBotCombo combo, String portPrefixOrExact) + { + try + { + combo.setSelection(portPrefixOrExact); + } + catch (Exception ignored) + { + String[] items = combo.items(); + String match = null; + for (String item : items) + { + if (item != null && item.startsWith(portPrefixOrExact)) + { + match = item; + break; + } + } + if (match == null) + { + throw new AssertionError("No serial port matched: " + portPrefixOrExact + " ; available=" + + String.join(", ", items)); + } + combo.setSelection(match); + } + } + + private static String getNewOutputPart(String outputBeforeSelection, String outputAfterSelection) + { + if (outputAfterSelection == null) + { + return ""; + } + if (outputBeforeSelection == null || outputBeforeSelection.isEmpty()) + { + return outputAfterSelection; + } + if (outputAfterSelection.startsWith(outputBeforeSelection)) + { + return outputAfterSelection.substring(outputBeforeSelection.length()); + } + return outputAfterSelection; + } + + private static String readTargetDetectionOutput() + { + try + { + return bot.styledText().getText(); + } + catch (Exception ignored) + { + } + + String bestCandidate = ""; + for (int i = 0; i < 10; i++) + { + try + { + String text = bot.text(i).getText(); + if (text != null && containsChipInfo(text)) + { + return text; + } + if (text != null && text.length() > bestCandidate.length()) + { + bestCandidate = text; + } + } + catch (Exception ignored) + { + break; + } + } + return bestCandidate == null ? "" : bestCandidate; + } + + private static boolean containsChipInfo(String text) + { + return text != null && (text.contains("Connected to ESP32") || text.contains("Chip type:") + || text.contains("Detecting chip type")); + } + + private static String extractTargetFromDetectionOutput(String output) + { + if (output == null || output.trim().isEmpty()) + { + return null; + } + for (Pattern pattern : TARGET_DETECTION_PATTERNS) + { + Matcher matcher = pattern.matcher(output); + if (matcher.find()) + { + return normalizeDetectedChipToIdfTarget(matcher.group(1)); + } + } + return null; + } + + private static String normalizeDetectedChipToIdfTarget(String chipName) + { + if (chipName == null) + { + return null; + } + String chip = chipName.trim().toUpperCase(Locale.ROOT); + if (chip.startsWith("ESP32-C61")) + { + return "esp32c61"; + } + if (chip.startsWith("ESP32-C6")) + { + return "esp32c6"; + } + if (chip.startsWith("ESP32-C5")) + { + return "esp32c5"; + } + if (chip.startsWith("ESP32-H2")) + { + return "esp32h2"; + } + if (chip.startsWith("ESP32-S3")) + { + return "esp32s3"; + } + if (chip.startsWith("ESP32-S2")) + { + return "esp32s2"; + } + if (chip.startsWith("ESP32")) + { + return "esp32"; + } + return null; + } + + private static String readConsoleText() + { + try + { + SWTBotView view = bot.viewByPartName("Console"); + view.show(); + view.setFocus(); + return view.bot().styledText().getText(); + } + catch (Exception e) + { + return ""; + } + } + + private static class TargetPort + { + final String target; + final String port; + + TargetPort(String target, String port) + { + this.target = target; + this.port = port; + } + } + } +} diff --git a/tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/operations/ProjectTestOperations.java b/tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/operations/ProjectTestOperations.java index e09df6a66..1d15d748c 100644 --- a/tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/operations/ProjectTestOperations.java +++ b/tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/operations/ProjectTestOperations.java @@ -9,6 +9,7 @@ import java.text.MessageFormat; import java.util.Arrays; import java.util.Optional; +import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -17,6 +18,10 @@ import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.debug.core.DebugException; +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.ILaunchManager; import org.eclipse.swt.widgets.MenuItem; import org.eclipse.swtbot.eclipse.finder.SWTWorkbenchBot; import org.eclipse.swtbot.eclipse.finder.widgets.SWTBotEditor; @@ -141,6 +146,179 @@ public static void createDebugConfiguration(String projectName, SWTWorkbenchBot } + /** + * Starts debugging via the Launch Bar Launch button and accepts the Debug perspective switch if prompted. + * + * @param bot current SWT bot reference + */ + public static void startDebuggingUsingLaunchBar(SWTWorkbenchBot bot) + { + bot.toolbarButtonWithTooltip("Launch").click(); + acceptDebugPerspectiveSwitchIfPresent(bot); + } + + /** + * Accepts the Eclipse "Confirm Perspective Switch" dialog when it appears after starting a debug session. + * + * @param bot current SWT bot reference + */ + public static void acceptDebugPerspectiveSwitchIfPresent(SWTWorkbenchBot bot) + { + try + { + TestWidgetWaitUtility.waitForDialogToAppear(bot, "Confirm Perspective Switch", 15000); + SWTBotShell shell = bot.shell("Confirm Perspective Switch"); + shell.setFocus(); + try + { + bot.button("Switch").click(); + } + catch (WidgetNotFoundException e) + { + bot.button("Yes").click(); + } + } + catch (Exception ignored) + { + // Perspective switch may already be remembered / suppressed. + } + } + + /** + * Waits until the Console view shows a successful OpenOCD / GDB debug session start. + * + * @param bot current SWT bot reference + * @throws IOException if property lookup fails + */ + public static void waitForDebugSessionStarted(SWTWorkbenchBot bot) throws IOException + { + SWTBotView view = bot.viewByPartName("Console"); + view.setFocus(); + TestWidgetWaitUtility.waitUntilViewContains(bot, "Listening on port 3333", view, + DefaultPropertyFetcher.getLongPropertyValue(DEFAULT_FLASH_WAIT_PROPERTY, 120000)); + } + + /** + * Stops the active launch / debug session using the Launch Bar Stop button. + * + * @param bot current SWT bot reference + */ + public static void stopLaunchUsingLaunchBar(SWTWorkbenchBot bot) + { + try + { + bot.toolbarButtonWithTooltip("Stop").click(); + bot.sleep(2000); + } + catch (WidgetNotFoundException e) + { + logger.warn("Stop button not found while trying to stop launch/debug session"); + } + } + + /** + * Best-effort cleanup of an active debug session: Launch Bar Stop, terminate all + * Eclipse launches, then force-kill leftover OpenOCD / GDB processes. Safe to call + * from {@code @After} / {@code @AfterClass} even when the test failed or hung mid-session. + * + * @param bot current SWT bot reference (may be {@code null} if UI is unavailable) + */ + public static void stopDebugSessionAndKillProcesses(SWTWorkbenchBot bot) + { + if (bot != null) + { + try + { + stopLaunchUsingLaunchBar(bot); + } + catch (Exception e) + { + logger.warn("Failed to stop launch via Launch Bar during debug cleanup", e); + } + + try + { + bot.toolbarButtonWithTooltip("Terminate").click(); + bot.sleep(1000); + } + catch (Exception ignored) + { + // Terminate toolbar button is only present in the Debug perspective. + } + } + + try + { + terminateAllLaunches(); + } + catch (Exception e) + { + logger.warn("Failed to terminate Eclipse launches during debug cleanup", e); + } + + killDebugProcesses(); + } + + /** + * Terminates every non-terminated launch registered with the Eclipse debug framework. + */ + public static void terminateAllLaunches() + { + ILaunchManager launchManager = DebugPlugin.getDefault().getLaunchManager(); + ILaunch[] launches = launchManager.getLaunches(); + if (launches == null) + { + return; + } + + for (ILaunch launch : launches) + { + if (launch == null || launch.isTerminated()) + { + continue; + } + try + { + launch.terminate(); + } + catch (DebugException e) + { + logger.warn("Failed to terminate launch: " + launch, e); + } + } + } + + /** + * Force-terminates OpenOCD and ESP GDB processes left behind by a debug session. + * Mirrors the VS Code UI-test {@code killDebugProcesses} helper. Exit status from + * {@code pkill} when no process matches is ignored. + */ + public static void killDebugProcesses() + { + String[] patterns = new String[] { "openocd", "xtensa-esp.*-gdb", "riscv32-esp.*-gdb" }; + for (String pattern : patterns) + { + try + { + Process process = new ProcessBuilder("pkill", "-f", pattern).redirectErrorStream(true).start(); + process.waitFor(5, TimeUnit.SECONDS); + } + catch (Exception e) + { + logger.debug("pkill for pattern '{}' skipped or failed: {}", pattern, e.getMessage()); + } + } + + try + { + Thread.sleep(1500); + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + } + } + public static void openProjectComponentYMLFileInTextEditorUsingContextMenu(String projectName, SWTWorkbenchBot bot) { SWTBotTreeItem projectItem = fetchProjectFromProjectExplorer(projectName, bot); diff --git a/tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/operations/selectors/LaunchBarModeSelector.java b/tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/operations/selectors/LaunchBarModeSelector.java new file mode 100644 index 000000000..0e0e55cc3 --- /dev/null +++ b/tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/operations/selectors/LaunchBarModeSelector.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * Copyright 2026 Espressif Systems (Shanghai) PTE LTD. All rights reserved. + * Use is subject to license terms. + *******************************************************************************/ + +package com.espressif.idf.ui.test.operations.selectors; + +import static org.eclipse.swtbot.swt.finder.matchers.WidgetMatcherFactory.withText; + +import org.eclipse.launchbar.ui.controls.internal.CSelector; +import org.eclipse.launchbar.ui.controls.internal.LaunchBarWidgetIds; +import org.eclipse.launchbar.ui.controls.internal.ModeSelector; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Widget; +import org.eclipse.swtbot.swt.finder.SWTBot; +import org.eclipse.swtbot.swt.finder.SWTBotWidget; +import org.eclipse.swtbot.swt.finder.exceptions.WidgetNotFoundException; +import org.eclipse.swtbot.swt.finder.matchers.WidgetMatcherFactory; +import org.eclipse.swtbot.swt.finder.results.Result; +import org.eclipse.swtbot.swt.finder.widgets.AbstractSWTBotControl; + +/** + * Helper to interact with the CDT Launch Bar mode selector (Run / Debug). + * + * @author Andrii Filippov + * + */ +@SuppressWarnings("restriction") +@SWTBotWidget(clasz = CSelector.class, preferredName = "cselector") +public class LaunchBarModeSelector extends AbstractSWTBotControl +{ + public LaunchBarModeSelector(ModeSelector modeSelector) throws WidgetNotFoundException + { + super(modeSelector); + } + + public LaunchBarModeSelector(SWTBot bot) + { + this(bot.widget(WidgetMatcherFactory.withTooltip("Launch Mode"))); + } + + public LaunchBarModeSelector(SWTBot bot, boolean unused) + { + this(bot.widget(WidgetMatcherFactory.widgetOfType(ModeSelector.class))); + } + + public SWTBot bot() + { + return new SWTBot(widget); + } + + public void click(int x, int y) + { + notify(SWT.MouseEnter); + notify(SWT.MouseMove); + notify(SWT.Activate); + notify(SWT.FocusIn); + notify(SWT.MouseDown, createMouseEvent(x, y, 1, SWT.NONE, 1)); + notify(SWT.MouseUp, createMouseEvent(x, y, 1, SWT.BUTTON1, 1)); + } + + @Override + public LaunchBarModeSelector click() + { + Point size = syncExec((Result) () -> widget.getSize()); + click(size.x / 2, size.y / 2); + return this; + } + + private void clickOnInternalWidget(int x, int y, Widget internalWidget) + { + notify(SWT.MouseDown, createMouseEvent(x, y, 1, SWT.NONE, 1), internalWidget); + notify(SWT.MouseUp, createMouseEvent(x, y, 1, SWT.BUTTON1, 1), internalWidget); + } + + public LaunchBarModeSelector select(String text) + { + click(); + Label itemToSelect = bot().shellWithId(LaunchBarWidgetIds.POPUP).bot().widget(withText(text)); + Point itemToSelectLocation = syncExec((Result) () -> itemToSelect.getLocation()); + clickOnInternalWidget(itemToSelectLocation.x, itemToSelectLocation.y, itemToSelect); + return this; + } +} From ce1459307b973ca2ec7b8cc20904a308d965d509 Mon Sep 17 00:00:00 2001 From: AndriiFilippov Date: Fri, 17 Jul 2026 15:00:29 +0200 Subject: [PATCH 2/4] ci: improved target selection. Improved Debug execution --- .../project/IDFProjectDebugProcessTest.java | 138 +++--------------- .../operations/ProjectTestOperations.java | 45 +++++- 2 files changed, 62 insertions(+), 121 deletions(-) diff --git a/tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/executable/cases/project/IDFProjectDebugProcessTest.java b/tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/executable/cases/project/IDFProjectDebugProcessTest.java index 51a719776..45d154472 100644 --- a/tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/executable/cases/project/IDFProjectDebugProcessTest.java +++ b/tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/executable/cases/project/IDFProjectDebugProcessTest.java @@ -7,15 +7,10 @@ import static org.eclipse.swtbot.swt.finder.waits.Conditions.widgetIsEnabled; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.junit.Assume.assumeFalse; import static org.junit.Assume.assumeTrue; import java.io.IOException; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -100,11 +95,10 @@ public void givenNewProjectBuiltAndFlashedViaUartWhenDebugWithEthernetKitThenDeb Fixture.whenNewProjectIsSelected(); Fixture.whenTurnOffOpenSerialMonitorAfterFlashingInLaunchConfig(); - String esp32SerialPort = Fixture.whenDetectEsp32UartSerialPortFromNewEspTargetDialog(); + String esp32SerialPort = Fixture.whenDetectAndSelectEsp32UartSerialPort(); assumeTrue("Skipping debug test: no ESP32 UART target detected from Serial Port auto-detection", esp32SerialPort != null); - Fixture.whenSelectLaunchTargetSerialPort(esp32SerialPort); Fixture.whenProjectIsBuiltUsingContextMenu(); Fixture.whenFlashProject(); Fixture.thenVerifyFlashDoneSuccessfully(); @@ -113,7 +107,7 @@ public void givenNewProjectBuiltAndFlashedViaUartWhenDebugWithEthernetKitThenDeb Fixture.whenSelectEsp32EthernetKitBoard()); Fixture.whenSwitchToDebugModeAndSelectDebugConfig(); - Fixture.whenStartDebugging(); + Fixture.whenStartDebuggingUsingContextMenu(); Fixture.thenVerifyDebugSessionStarted(); Fixture.thenVerifyNoFatalOpenOcdErrors(); Fixture.whenStepOver(); @@ -172,30 +166,12 @@ private static void whenTurnOffOpenSerialMonitorAfterFlashingInLaunchConfig() th } /** - * Uses the same New ESP Target serial-port auto-detection as - * {@code NewEspressifIDFProjectFlashProcessTest}, then returns the first port mapped to esp32. + * Opens New ESP Target, scans serial ports with detailed output, and stops as soon as + * an esp32 chip is detected. Finishes the dialog with that port selected. + * + * @return the selected ESP32 serial port, or {@code null} if none was found */ - private static String whenDetectEsp32UartSerialPortFromNewEspTargetDialog() throws Exception - { - TargetPort[] detectedTargets = whenCollectDetectedTargetsFromNewEspTargetDialog(); - - assumeFalse("Skipping hardware debug test: no ESP targets were detected from Serial Port auto-detection", - detectedTargets.length == 0); - - for (TargetPort targetPort : detectedTargets) - { - if (ESP32_TARGET.equals(targetPort.target)) - { - System.out.println("Using ESP32 UART port for flash: " + targetPort.port); - return targetPort.port; - } - } - - System.out.println("No esp32 target among detected ports"); - return null; - } - - private static TargetPort[] whenCollectDetectedTargetsFromNewEspTargetDialog() throws Exception + private static String whenDetectAndSelectEsp32UartSerialPort() throws Exception { LaunchBarTargetSelector targetSelector = new LaunchBarTargetSelector(bot); targetSelector.clickEdit(); @@ -214,8 +190,6 @@ private static TargetPort[] whenCollectDetectedTargetsFromNewEspTargetDialog() t SWTBotCombo serialPortCombo = bot.comboBoxWithLabel("Serial Port:"); String[] serialPorts = serialPortCombo.items(); - List detectedTargets = new ArrayList<>(); - for (String serialPort : serialPorts) { if (serialPort == null || serialPort.trim().isEmpty()) @@ -226,7 +200,6 @@ private static TargetPort[] whenCollectDetectedTargetsFromNewEspTargetDialog() t System.out.println("Checking serial port: " + serialPort); String outputBeforeSelection = readTargetDetectionOutput(); - serialPortCombo.setSelection(serialPort); // Wait for target auto-detection output to be printed. @@ -234,7 +207,6 @@ private static TargetPort[] whenCollectDetectedTargetsFromNewEspTargetDialog() t String outputAfterSelection = readTargetDetectionOutput(); String newOutput = getNewOutputPart(outputBeforeSelection, outputAfterSelection); - String detectedTarget = extractTargetFromDetectionOutput(newOutput); if (detectedTarget == null || detectedTarget.trim().isEmpty()) @@ -244,50 +216,21 @@ private static TargetPort[] whenCollectDetectedTargetsFromNewEspTargetDialog() t } System.out.println("Detected ESP target: " + detectedTarget + " on port: " + serialPort); - detectedTargets.add(new TargetPort(detectedTarget, serialPort)); - } - - bot.button("Cancel").click(); - - List uniqueTargets = keepFirstPortPerTarget(detectedTargets); - return uniqueTargets.toArray(new TargetPort[0]); - } - - private static List keepFirstPortPerTarget(List targets) - { - Map uniqueTargets = new LinkedHashMap<>(); - - for (TargetPort targetPort : targets) - { - uniqueTargets.putIfAbsent(targetPort.target, targetPort); - } - - return new ArrayList<>(uniqueTargets.values()); - } - - private static void whenSelectLaunchTargetSerialPort(String portPrefixOrExact) throws Exception - { - LaunchBarTargetSelector targetSelector = new LaunchBarTargetSelector(bot); - targetSelector.clickEdit(); - - TestWidgetWaitUtility.waitForDialogToAppear(bot, "New ESP Target", 20000); - - SWTBotShell shell = bot.shell("New ESP Target"); - shell.setFocus(); - SWTBotCheckBox detailedOutput = bot.checkBox("Enable detailed output"); - if (!detailedOutput.isChecked()) - { - detailedOutput.click(); + if (ESP32_TARGET.equals(detectedTarget)) + { + System.out.println("ESP32 UART port found — stopping discovery and applying: " + serialPort); + TestWidgetWaitUtility.waitForOperationsInProgressToFinishSync(bot); + shell.setFocus(); + bot.button("Finish").click(); + TestWidgetWaitUtility.waitForOperationsInProgressToFinishAsync(bot); + return serialPort; + } } - SWTBotCombo serialPortCombo = bot.comboBoxWithLabel("Serial Port:"); - selectComboItemByExactOrPrefix(serialPortCombo, portPrefixOrExact); - - TestWidgetWaitUtility.waitForOperationsInProgressToFinishSync(bot); - shell.setFocus(); - bot.button("Finish").click(); - TestWidgetWaitUtility.waitForOperationsInProgressToFinishAsync(bot); + System.out.println("No esp32 target among detected ports"); + bot.button("Cancel").click(); + return null; } /** @@ -420,9 +363,9 @@ private static boolean trySelectLaunchConfig(LaunchBarConfigSelector configSelec } } - private static void whenStartDebugging() + private static void whenStartDebuggingUsingContextMenu() { - ProjectTestOperations.startDebuggingUsingLaunchBar(bot); + ProjectTestOperations.startDebuggingUsingContextMenu(projectName, bot); } private static void thenVerifyDebugSessionStarted() throws Exception @@ -514,33 +457,6 @@ private static void cleanupEnvironment() } } - private static void selectComboItemByExactOrPrefix(SWTBotCombo combo, String portPrefixOrExact) - { - try - { - combo.setSelection(portPrefixOrExact); - } - catch (Exception ignored) - { - String[] items = combo.items(); - String match = null; - for (String item : items) - { - if (item != null && item.startsWith(portPrefixOrExact)) - { - match = item; - break; - } - } - if (match == null) - { - throw new AssertionError("No serial port matched: " + portPrefixOrExact + " ; available=" - + String.join(", ", items)); - } - combo.setSelection(match); - } - } - private static String getNewOutputPart(String outputBeforeSelection, String outputAfterSelection) { if (outputAfterSelection == null) @@ -666,17 +582,5 @@ private static String readConsoleText() return ""; } } - - private static class TargetPort - { - final String target; - final String port; - - TargetPort(String target, String port) - { - this.target = target; - this.port = port; - } - } } } diff --git a/tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/operations/ProjectTestOperations.java b/tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/operations/ProjectTestOperations.java index 1d15d748c..e662e8a1d 100644 --- a/tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/operations/ProjectTestOperations.java +++ b/tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/operations/ProjectTestOperations.java @@ -28,6 +28,7 @@ import org.eclipse.swtbot.eclipse.finder.widgets.SWTBotView; import org.eclipse.swtbot.swt.finder.exceptions.WidgetNotFoundException; import org.eclipse.swtbot.swt.finder.matchers.WidgetMatcherFactory; +import org.eclipse.swtbot.swt.finder.waits.Conditions; import org.eclipse.swtbot.swt.finder.waits.DefaultCondition; import org.eclipse.swtbot.swt.finder.widgets.SWTBotButton; import org.eclipse.swtbot.swt.finder.widgets.SWTBotCheckBox; @@ -147,13 +148,49 @@ public static void createDebugConfiguration(String projectName, SWTWorkbenchBot } /** - * Starts debugging via the Launch Bar Launch button and accepts the Debug perspective switch if prompted. + * Starts debugging via Project Explorer context menu: Debug As → Debug Configurations..., + * selects the ESP-IDF OpenOCD debug config, clicks Debug, and accepts the perspective switch if prompted. * - * @param bot current SWT bot reference + * @param projectName project whose debug configuration should be launched + * @param bot current SWT bot reference */ - public static void startDebuggingUsingLaunchBar(SWTWorkbenchBot bot) + public static void startDebuggingUsingContextMenu(String projectName, SWTWorkbenchBot bot) { - bot.toolbarButtonWithTooltip("Launch").click(); + SWTBotTreeItem projectItem = fetchProjectFromProjectExplorer(projectName, bot); + if (projectItem == null) + { + throw new WidgetNotFoundException("Project not found in Project Explorer: " + projectName); + } + + projectItem.select(); + projectItem.contextMenu("Debug As").menu("Debug Configurations...").click(); + + TestWidgetWaitUtility.waitForDialogToAppear(bot, "Debug Configurations", 10000); + + bot.tree().getTreeItem("ESP-IDF GDB OpenOCD Debugging").select(); + bot.tree().getTreeItem("ESP-IDF GDB OpenOCD Debugging").expand(); + + String primaryDebugConfig = projectName + " Debug"; + String fallbackDebugConfig = projectName + " Configuration"; + try + { + bot.tree().getTreeItem("ESP-IDF GDB OpenOCD Debugging").getNode(primaryDebugConfig).select(); + } + catch (WidgetNotFoundException e) + { + try + { + bot.tree().getTreeItem("ESP-IDF GDB OpenOCD Debugging").getNode(fallbackDebugConfig).select(); + } + catch (WidgetNotFoundException e2) + { + // Last resort: use the first child config under the OpenOCD type. + bot.tree().getTreeItem("ESP-IDF GDB OpenOCD Debugging").getNode(0).select(); + } + } + + bot.waitUntil(Conditions.widgetIsEnabled(bot.button("Debug")), 5000); + bot.button("Debug").click(); acceptDebugPerspectiveSwitchIfPresent(bot); } From 9f95e8bc4c2159df86e8931040f0f8fbb76029a6 Mon Sep 17 00:00:00 2001 From: AndriiFilippov Date: Fri, 17 Jul 2026 16:35:28 +0200 Subject: [PATCH 3/4] ci: added Confirm Perspective Switch --- .../project/IDFProjectDebugProcessTest.java | 3 + .../operations/ProjectTestOperations.java | 84 ++++++++++++++++--- 2 files changed, 77 insertions(+), 10 deletions(-) diff --git a/tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/executable/cases/project/IDFProjectDebugProcessTest.java b/tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/executable/cases/project/IDFProjectDebugProcessTest.java index 45d154472..cbc4dfc56 100644 --- a/tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/executable/cases/project/IDFProjectDebugProcessTest.java +++ b/tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/executable/cases/project/IDFProjectDebugProcessTest.java @@ -371,6 +371,8 @@ private static void whenStartDebuggingUsingContextMenu() private static void thenVerifyDebugSessionStarted() throws Exception { ProjectTestOperations.waitForDebugSessionStarted(bot); + // Extra safety: dialog can linger if it appeared after the wait loop exited. + ProjectTestOperations.acceptDebugPerspectiveSwitchIfPresent(bot, 5000); } private static void thenVerifyNoFatalOpenOcdErrors() @@ -382,6 +384,7 @@ private static void thenVerifyNoFatalOpenOcdErrors() private static void whenStepOver() { + ProjectTestOperations.acceptDebugPerspectiveSwitchIfPresent(bot, 3000); try { bot.toolbarButtonWithTooltip("Step Over (F6)").click(); diff --git a/tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/operations/ProjectTestOperations.java b/tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/operations/ProjectTestOperations.java index e662e8a1d..07d696c7d 100644 --- a/tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/operations/ProjectTestOperations.java +++ b/tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/operations/ProjectTestOperations.java @@ -191,21 +191,38 @@ public static void startDebuggingUsingContextMenu(String projectName, SWTWorkben bot.waitUntil(Conditions.widgetIsEnabled(bot.button("Debug")), 5000); bot.button("Debug").click(); - acceptDebugPerspectiveSwitchIfPresent(bot); + // Dialog usually appears later, when GDB suspends — also handled in waitForDebugSessionStarted. + acceptDebugPerspectiveSwitchIfPresent(bot, 5000); } /** - * Accepts the Eclipse "Confirm Perspective Switch" dialog when it appears after starting a debug session. + * Accepts the Eclipse "Confirm Perspective Switch" dialog when it appears after the debug + * session suspends. Checks "Remember my decision" so CI is less likely to see it again. * - * @param bot current SWT bot reference + * @param bot current SWT bot reference + * @param timeout how long to wait for the dialog in milliseconds */ - public static void acceptDebugPerspectiveSwitchIfPresent(SWTWorkbenchBot bot) + public static void acceptDebugPerspectiveSwitchIfPresent(SWTWorkbenchBot bot, long timeout) { try { - TestWidgetWaitUtility.waitForDialogToAppear(bot, "Confirm Perspective Switch", 15000); + TestWidgetWaitUtility.waitForDialogToAppear(bot, "Confirm Perspective Switch", timeout); SWTBotShell shell = bot.shell("Confirm Perspective Switch"); + shell.activate(); shell.setFocus(); + + try + { + SWTBotCheckBox remember = bot.checkBox("Remember my decision"); + if (!remember.isChecked()) + { + remember.click(); + } + } + catch (WidgetNotFoundException ignored) + { + } + try { bot.button("Switch").click(); @@ -214,6 +231,7 @@ public static void acceptDebugPerspectiveSwitchIfPresent(SWTWorkbenchBot bot) { bot.button("Yes").click(); } + bot.sleep(1000); } catch (Exception ignored) { @@ -222,17 +240,63 @@ public static void acceptDebugPerspectiveSwitchIfPresent(SWTWorkbenchBot bot) } /** - * Waits until the Console view shows a successful OpenOCD / GDB debug session start. + * @see #acceptDebugPerspectiveSwitchIfPresent(SWTWorkbenchBot, long) + */ + public static void acceptDebugPerspectiveSwitchIfPresent(SWTWorkbenchBot bot) + { + acceptDebugPerspectiveSwitchIfPresent(bot, 30000); + } + + /** + * Waits until OpenOCD/GDB shows a successful debug start, then dismisses the Debug + * perspective switch dialog that appears when the target suspends at the breakpoint. * * @param bot current SWT bot reference * @throws IOException if property lookup fails */ public static void waitForDebugSessionStarted(SWTWorkbenchBot bot) throws IOException { - SWTBotView view = bot.viewByPartName("Console"); - view.setFocus(); - TestWidgetWaitUtility.waitUntilViewContains(bot, "Listening on port 3333", view, - DefaultPropertyFetcher.getLongPropertyValue(DEFAULT_FLASH_WAIT_PROPERTY, 120000)); + long timeout = DefaultPropertyFetcher.getLongPropertyValue(DEFAULT_FLASH_WAIT_PROPERTY, 120000); + long deadline = System.currentTimeMillis() + timeout; + + while (System.currentTimeMillis() < deadline) + { + // Dialog can appear as soon as GDB hits the breakpoint and blocks further UI. + acceptDebugPerspectiveSwitchIfPresent(bot, 1000); + + try + { + SWTBotView view = bot.viewByPartName("Console"); + view.show(); + view.setFocus(); + String consoleText = view.bot().styledText().getText(); + if (consoleText == null) + { + consoleText = ""; + } + + boolean started = consoleText.toLowerCase().contains("listening on port 3333") + || consoleText.contains("Target halted") + || consoleText.contains("hit Temporary breakpoint") + || consoleText.contains("hit Breakpoint"); + + if (started) + { + // Perspective switch is triggered by suspend — wait for it explicitly. + acceptDebugPerspectiveSwitchIfPresent(bot, 30000); + return; + } + } + catch (Exception e) + { + logger.debug("Waiting for debug console output: {}", e.getMessage()); + } + + bot.sleep(1000); + } + + throw new AssertionError( + "Debug session did not start within timeout (expected OpenOCD/GDB halt or Listening on port 3333)"); } /** From 3f9fc01131dde432760d83d9744cbae9866c3e58 Mon Sep 17 00:00:00 2001 From: AndriiFilippov Date: Fri, 17 Jul 2026 17:44:19 +0200 Subject: [PATCH 4/4] ci: treat ready only when GDB hits a breakpoint --- .../project/IDFProjectDebugProcessTest.java | 24 ++-- .../operations/ProjectTestOperations.java | 105 +++++++++++++++--- 2 files changed, 97 insertions(+), 32 deletions(-) diff --git a/tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/executable/cases/project/IDFProjectDebugProcessTest.java b/tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/executable/cases/project/IDFProjectDebugProcessTest.java index cbc4dfc56..60b0fd8b4 100644 --- a/tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/executable/cases/project/IDFProjectDebugProcessTest.java +++ b/tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/executable/cases/project/IDFProjectDebugProcessTest.java @@ -371,8 +371,6 @@ private static void whenStartDebuggingUsingContextMenu() private static void thenVerifyDebugSessionStarted() throws Exception { ProjectTestOperations.waitForDebugSessionStarted(bot); - // Extra safety: dialog can linger if it appeared after the wait loop exited. - ProjectTestOperations.acceptDebugPerspectiveSwitchIfPresent(bot, 5000); } private static void thenVerifyNoFatalOpenOcdErrors() @@ -380,11 +378,15 @@ private static void thenVerifyNoFatalOpenOcdErrors() String consoleText = readConsoleText(); assertFalse("Fatal OpenOCD error detected during debug session.\nConsole:\n" + consoleText, DEBUG_FATAL_ERROR_PATTERN.matcher(consoleText).find()); + assertFalse("Debug session already shut down before assertions.\nConsole:\n" + consoleText, + consoleText.contains("shutdown command invoked") + || consoleText.contains("dropped 'gdb' connection")); + assertTrue("Expected an active debug launch after suspend", ProjectTestOperations.hasActiveLaunch()); } private static void whenStepOver() { - ProjectTestOperations.acceptDebugPerspectiveSwitchIfPresent(bot, 3000); + ProjectTestOperations.waitForDebugStepActionsAvailable(bot, 15000); try { bot.toolbarButtonWithTooltip("Step Over (F6)").click(); @@ -392,7 +394,6 @@ private static void whenStepOver() } catch (WidgetNotFoundException e) { - // Some Eclipse versions use a slightly different tooltip. bot.toolbarButtonWithTooltip("Step Over").click(); bot.sleep(3000); } @@ -403,17 +404,10 @@ private static void thenVerifyDebugSessionStillActive() String consoleText = readConsoleText(); assertFalse("Fatal OpenOCD error after Step Over.\nConsole:\n" + consoleText, DEBUG_FATAL_ERROR_PATTERN.matcher(consoleText).find()); - - boolean stopAvailable = true; - try - { - bot.toolbarButtonWithTooltip("Stop"); - } - catch (WidgetNotFoundException e) - { - stopAvailable = false; - } - assertTrue("Debug/launch session appears to have ended (Stop button missing)", stopAvailable); + assertFalse("Debug session terminated unexpectedly after Step Over.\nConsole:\n" + consoleText, + consoleText.contains("shutdown command invoked")); + assertTrue("Debug launch terminated unexpectedly after Step Over", + ProjectTestOperations.hasActiveLaunch()); } private static void whenStopDebugging() diff --git a/tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/operations/ProjectTestOperations.java b/tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/operations/ProjectTestOperations.java index 07d696c7d..4123dcb67 100644 --- a/tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/operations/ProjectTestOperations.java +++ b/tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/operations/ProjectTestOperations.java @@ -201,8 +201,9 @@ public static void startDebuggingUsingContextMenu(String projectName, SWTWorkben * * @param bot current SWT bot reference * @param timeout how long to wait for the dialog in milliseconds + * @return {@code true} if the dialog was found and dismissed */ - public static void acceptDebugPerspectiveSwitchIfPresent(SWTWorkbenchBot bot, long timeout) + public static boolean acceptDebugPerspectiveSwitchIfPresent(SWTWorkbenchBot bot, long timeout) { try { @@ -213,7 +214,7 @@ public static void acceptDebugPerspectiveSwitchIfPresent(SWTWorkbenchBot bot, lo try { - SWTBotCheckBox remember = bot.checkBox("Remember my decision"); + SWTBotCheckBox remember = shell.bot().checkBox("Remember my decision"); if (!remember.isChecked()) { remember.click(); @@ -225,17 +226,21 @@ public static void acceptDebugPerspectiveSwitchIfPresent(SWTWorkbenchBot bot, lo try { - bot.button("Switch").click(); + shell.bot().button("Switch").click(); } catch (WidgetNotFoundException e) { - bot.button("Yes").click(); + shell.bot().button("Yes").click(); } - bot.sleep(1000); + + // Give the Debug perspective time to finish opening before further toolbar clicks. + bot.sleep(2000); + return true; } catch (Exception ignored) { // Perspective switch may already be remembered / suppressed. + return false; } } @@ -248,8 +253,9 @@ public static void acceptDebugPerspectiveSwitchIfPresent(SWTWorkbenchBot bot) } /** - * Waits until OpenOCD/GDB shows a successful debug start, then dismisses the Debug - * perspective switch dialog that appears when the target suspends at the breakpoint. + * Waits until GDB has suspended at a breakpoint (not merely OpenOCD "Target halted" during + * reset), dismisses the Debug perspective switch dialog, then waits until the Debug toolbar + * is ready for stepping. * * @param bot current SWT bot reference * @throws IOException if property lookup fails @@ -258,11 +264,14 @@ public static void waitForDebugSessionStarted(SWTWorkbenchBot bot) throws IOExce { long timeout = DefaultPropertyFetcher.getLongPropertyValue(DEFAULT_FLASH_WAIT_PROPERTY, 120000); long deadline = System.currentTimeMillis() + timeout; + boolean perspectiveHandled = false; while (System.currentTimeMillis() < deadline) { - // Dialog can appear as soon as GDB hits the breakpoint and blocks further UI. - acceptDebugPerspectiveSwitchIfPresent(bot, 1000); + if (!perspectiveHandled) + { + perspectiveHandled = acceptDebugPerspectiveSwitchIfPresent(bot, 1000); + } try { @@ -275,15 +284,19 @@ public static void waitForDebugSessionStarted(SWTWorkbenchBot bot) throws IOExce consoleText = ""; } - boolean started = consoleText.toLowerCase().contains("listening on port 3333") - || consoleText.contains("Target halted") - || consoleText.contains("hit Temporary breakpoint") - || consoleText.contains("hit Breakpoint"); + // Require an actual GDB breakpoint hit. "Target halted" alone appears during + // OpenOCD reset and is too early — acting on it causes flaky teardown. + boolean suspendedAtBreakpoint = consoleText.contains("hit Temporary breakpoint") + || consoleText.contains("hit Breakpoint") + || consoleText.contains("hit breakpoint"); - if (started) + if (suspendedAtBreakpoint) { - // Perspective switch is triggered by suspend — wait for it explicitly. - acceptDebugPerspectiveSwitchIfPresent(bot, 30000); + if (!perspectiveHandled) + { + perspectiveHandled = acceptDebugPerspectiveSwitchIfPresent(bot, 30000); + } + waitForDebugStepActionsAvailable(bot, 30000); return; } } @@ -296,7 +309,65 @@ public static void waitForDebugSessionStarted(SWTWorkbenchBot bot) throws IOExce } throw new AssertionError( - "Debug session did not start within timeout (expected OpenOCD/GDB halt or Listening on port 3333)"); + "Debug session did not suspend at a breakpoint within timeout (expected 'hit Temporary breakpoint' / 'hit Breakpoint')"); + } + + /** + * Waits until Debug perspective step actions are available (session is alive and UI ready). + * + * @param bot current SWT bot reference + * @param timeout timeout in milliseconds + */ + public static void waitForDebugStepActionsAvailable(SWTWorkbenchBot bot, long timeout) + { + bot.waitUntil(new DefaultCondition() + { + @Override + public boolean test() throws Exception + { + return isToolbarButtonPresent(bot, "Step Over (F6)") || isToolbarButtonPresent(bot, "Step Over"); + } + + @Override + public String getFailureMessage() + { + return "Debug Step Over action not available — debug session may have terminated or Debug perspective did not finish loading"; + } + }, timeout, 500); + } + + private static boolean isToolbarButtonPresent(SWTWorkbenchBot bot, String tooltip) + { + try + { + bot.toolbarButtonWithTooltip(tooltip); + return true; + } + catch (WidgetNotFoundException e) + { + return false; + } + } + + /** + * Returns {@code true} if an active (non-terminated) Eclipse launch still exists. + */ + public static boolean hasActiveLaunch() + { + ILaunchManager launchManager = DebugPlugin.getDefault().getLaunchManager(); + ILaunch[] launches = launchManager.getLaunches(); + if (launches == null) + { + return false; + } + for (ILaunch launch : launches) + { + if (launch != null && !launch.isTerminated()) + { + return true; + } + } + return false; } /**