From acbdab3491174504f6ba46cbf8d61b9d58bcdf94 Mon Sep 17 00:00:00 2001 From: AndriiFilippov Date: Fri, 17 Jul 2026 13:36:33 +0200 Subject: [PATCH 01/13] 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 02/13] 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 03/13] 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 04/13] 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; } /** From 25feadb0b20caaa8b3473bc34c80cf81b7817467 Mon Sep 17 00:00:00 2001 From: AndriiFilippov Date: Mon, 20 Jul 2026 09:24:19 +0200 Subject: [PATCH 05/13] ci: fixed DefaultCondition bot --- .../idf/ui/test/operations/ProjectTestOperations.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 4123dcb67..6b997e82e 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 @@ -320,12 +320,14 @@ public static void waitForDebugSessionStarted(SWTWorkbenchBot bot) throws IOExce */ public static void waitForDebugStepActionsAvailable(SWTWorkbenchBot bot, long timeout) { - bot.waitUntil(new DefaultCondition() + final SWTWorkbenchBot workbenchBot = bot; + workbenchBot.waitUntil(new DefaultCondition() { @Override public boolean test() throws Exception { - return isToolbarButtonPresent(bot, "Step Over (F6)") || isToolbarButtonPresent(bot, "Step Over"); + return isToolbarButtonPresent(workbenchBot, "Step Over (F6)") + || isToolbarButtonPresent(workbenchBot, "Step Over"); } @Override From a824a95d34db165c245bc7472b8b0266b6ca6f3c Mon Sep 17 00:00:00 2001 From: AndriiFilippov Date: Mon, 20 Jul 2026 12:31:12 +0200 Subject: [PATCH 06/13] ci: improved console reading --- .../project/IDFProjectDebugProcessTest.java | 84 +---------- .../operations/ProjectTestOperations.java | 135 ++++++++++++------ 2 files changed, 94 insertions(+), 125 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 60b0fd8b4..38f033e4c 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 @@ -16,7 +16,6 @@ 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; @@ -35,7 +34,6 @@ 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; /** @@ -106,7 +104,9 @@ public void givenNewProjectBuiltAndFlashedViaUartWhenDebugWithEthernetKitThenDeb assumeTrue("Skipping debug test: ESP32-ETHERNET-KIT board not detected", Fixture.whenSelectEsp32EthernetKitBoard()); - Fixture.whenSwitchToDebugModeAndSelectDebugConfig(); + // Start debug only via Debug As — do not flip Launch Bar mode/config first. + // LaunchBarListener toggles RUN↔DEBUG on descriptor changes and can terminate + // an active OpenOCD session when the Debug perspective opens. Fixture.whenStartDebuggingUsingContextMenu(); Fixture.thenVerifyDebugSessionStarted(); Fixture.thenVerifyNoFatalOpenOcdErrors(); @@ -315,54 +315,6 @@ 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 whenStartDebuggingUsingContextMenu() { ProjectTestOperations.startDebuggingUsingContextMenu(projectName, bot); @@ -375,7 +327,7 @@ private static void thenVerifyDebugSessionStarted() throws Exception private static void thenVerifyNoFatalOpenOcdErrors() { - String consoleText = readConsoleText(); + String consoleText = ProjectTestOperations.readDebugRelatedConsoleText(bot); 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, @@ -401,7 +353,7 @@ private static void whenStepOver() private static void thenVerifyDebugSessionStillActive() { - String consoleText = readConsoleText(); + String consoleText = ProjectTestOperations.readDebugRelatedConsoleText(bot); assertFalse("Fatal OpenOCD error after Step Over.\nConsole:\n" + consoleText, DEBUG_FATAL_ERROR_PATTERN.matcher(consoleText).find()); assertFalse("Debug session terminated unexpectedly after Step Over.\nConsole:\n" + consoleText, @@ -413,7 +365,7 @@ private static void thenVerifyDebugSessionStillActive() private static void whenStopDebugging() { stopDebugSessionAndKillProcesses(); - TestWidgetWaitUtility.waitForOperationsInProgressToFinishAsync(bot); + bot.sleep(2000); } private static void stopDebugSessionAndKillProcesses() @@ -431,14 +383,6 @@ private static void cleanupEnvironment() { } - try - { - TestWidgetWaitUtility.waitForOperationsInProgressToFinishAsync(bot); - } - catch (Exception ignored) - { - } - try { ProjectTestOperations.closeAllProjects(bot); @@ -449,7 +393,6 @@ private static void cleanupEnvironment() } finally { - // Final safety net in case UI cleanup left OpenOCD/GDB running. ProjectTestOperations.killDebugProcesses(); } } @@ -564,20 +507,5 @@ private static String normalizeDetectedChipToIdfTarget(String chipName) } 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 ""; - } - } } } 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 6b997e82e..9aeceb746 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 @@ -256,13 +256,19 @@ public static void acceptDebugPerspectiveSwitchIfPresent(SWTWorkbenchBot bot) * 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. + *

+ * Breakpoint output is usually on the IDF Process Console page of the Console view — + * not whatever console page happens to be selected. * * @param bot current SWT bot reference * @throws IOException if property lookup fails */ public static void waitForDebugSessionStarted(SWTWorkbenchBot bot) throws IOException { - long timeout = DefaultPropertyFetcher.getLongPropertyValue(DEFAULT_FLASH_WAIT_PROPERTY, 120000); + // Prefer a bounded debug timeout; the shared flash wait property is often hours-long. + long timeout = Math.min( + DefaultPropertyFetcher.getLongPropertyValue(DEFAULT_FLASH_WAIT_PROPERTY, 120000), + 180000); long deadline = System.currentTimeMillis() + timeout; boolean perspectiveHandled = false; @@ -273,43 +279,95 @@ public static void waitForDebugSessionStarted(SWTWorkbenchBot bot) throws IOExce perspectiveHandled = acceptDebugPerspectiveSwitchIfPresent(bot, 1000); } - try + String consoleText = readDebugRelatedConsoleText(bot); + + if (consoleText.contains("shutdown command invoked") + || consoleText.contains("dropped 'gdb' connection")) { - SWTBotView view = bot.viewByPartName("Console"); - view.show(); - view.setFocus(); - String consoleText = view.bot().styledText().getText(); - if (consoleText == null) + throw new AssertionError( + "Debug session shut down before a breakpoint suspend was observed.\nConsole:\n" + + consoleText); + } + + if (isSuspendedAtBreakpoint(consoleText)) + { + if (!perspectiveHandled) { - consoleText = ""; + perspectiveHandled = acceptDebugPerspectiveSwitchIfPresent(bot, 30000); } + waitForDebugStepActionsAvailable(bot, 30000); + return; + } + + bot.sleep(1000); + } + + String lastConsole = readDebugRelatedConsoleText(bot); + throw new AssertionError( + "Debug session did not suspend at a breakpoint within timeout (expected 'hit Temporary breakpoint' / 'hit Breakpoint' on IDF Process Console).\nLast console text:\n" + + lastConsole); + } - // 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"); + /** + * Reads console text from the pages where OpenOCD/GDB output typically appears. + */ + public static String readDebugRelatedConsoleText(SWTWorkbenchBot bot) + { + StringBuilder combined = new StringBuilder(); + String[] consolePages = new String[] { "IDF Process Console", "ESP-IDF Console" }; - if (suspendedAtBreakpoint) + for (String consolePage : consolePages) + { + try + { + SWTBotView view = viewConsole(consolePage, bot); + view.show(); + view.setFocus(); + String text = view.bot().styledText().getText(); + if (text != null && !text.isEmpty()) { - if (!perspectiveHandled) + combined.append(text).append('\n'); + if (isSuspendedAtBreakpoint(text)) { - perspectiveHandled = acceptDebugPerspectiveSwitchIfPresent(bot, 30000); + return text; } - waitForDebugStepActionsAvailable(bot, 30000); - return; } } catch (Exception e) { - logger.debug("Waiting for debug console output: {}", e.getMessage()); + logger.debug("Could not read console '{}': {}", consolePage, e.getMessage()); } + } - bot.sleep(1000); + try + { + SWTBotView view = bot.viewByPartName("Console"); + view.show(); + view.setFocus(); + String text = view.bot().styledText().getText(); + if (text != null) + { + combined.append(text); + } + } + catch (Exception e) + { + logger.debug("Could not read default Console view: {}", e.getMessage()); } - throw new AssertionError( - "Debug session did not suspend at a breakpoint within timeout (expected 'hit Temporary breakpoint' / 'hit Breakpoint')"); + return combined.toString(); + } + + private static boolean isSuspendedAtBreakpoint(String consoleText) + { + if (consoleText == null || consoleText.isEmpty()) + { + return false; + } + return consoleText.contains("hit Temporary breakpoint") + || consoleText.contains("hit Breakpoint") + || consoleText.contains("hit breakpoint") + || (consoleText.contains("Temporary breakpoint") && consoleText.contains("app_main")); } /** @@ -326,6 +384,11 @@ public static void waitForDebugStepActionsAvailable(SWTWorkbenchBot bot, long ti @Override public boolean test() throws Exception { + if (!hasActiveLaunch()) + { + throw new AssertionError( + "Debug launch terminated before Step Over became available (OpenOCD/GDB already stopped)"); + } return isToolbarButtonPresent(workbenchBot, "Step Over (F6)") || isToolbarButtonPresent(workbenchBot, "Step Over"); } @@ -391,36 +454,14 @@ public static void stopLaunchUsingLaunchBar(SWTWorkbenchBot bot) } /** - * 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. + * Best-effort cleanup of an active debug session via the debug API and process kill. + * Avoids clicking Launch Bar Stop / Debug Terminate toolbars — those tooltips are ambiguous + * under SWTBot and can race with an active session during perspective changes. * * @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(); From 553c47a00e15e9b999695f3b273ba5fb48f38742 Mon Sep 17 00:00:00 2001 From: AndriiFilippov Date: Tue, 21 Jul 2026 10:18:57 +0200 Subject: [PATCH 07/13] ci: improved console wait. improved after test env clean --- .../project/IDFProjectDebugProcessTest.java | 19 +- .../operations/ProjectTestOperations.java | 220 ++++++++++++++---- 2 files changed, 196 insertions(+), 43 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 38f033e4c..bd0b2a0c8 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 @@ -37,8 +37,8 @@ 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. + * Hardware E2E test: create → build → UART flash (ESP32) → select ESP32-ETHERNET-KIT → + * start OpenOCD/GDB debugging via Debug As and verify the session (Step Over). *

* Mirrors the VS Code hardware debug flow from {@code project-hardware-e2e-test.ts}. * @@ -78,8 +78,10 @@ public static void tearDown() @After public void afterEachTest() { - // Always stop OpenOCD/GDB even when an assertion failed mid-test. + // Always stop OpenOCD/GDB even when an assertion failed mid-test, then leave Debug + // perspective so later suites still start on C/C++ (runs before @AfterClass cleanup). Fixture.stopDebugSessionAndKillProcesses(); + Fixture.openCCppPerspective(); } @Test @@ -373,6 +375,17 @@ private static void stopDebugSessionAndKillProcesses() ProjectTestOperations.stopDebugSessionAndKillProcesses(bot); } + private static void openCCppPerspective() + { + try + { + ProjectTestOperations.openCCppPerspective(bot); + } + catch (Exception ignored) + { + } + } + private static void cleanupEnvironment() { try 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 9aeceb746..60ffb2476 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 @@ -8,6 +8,7 @@ import java.io.IOException; import java.text.MessageFormat; import java.util.Arrays; +import java.util.Locale; import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; @@ -22,6 +23,9 @@ import org.eclipse.debug.core.DebugPlugin; import org.eclipse.debug.core.ILaunch; import org.eclipse.debug.core.ILaunchManager; +import org.eclipse.debug.core.model.IDebugTarget; +import org.eclipse.debug.core.model.IStackFrame; +import org.eclipse.debug.core.model.IThread; import org.eclipse.swt.widgets.MenuItem; import org.eclipse.swtbot.eclipse.finder.SWTWorkbenchBot; import org.eclipse.swtbot.eclipse.finder.widgets.SWTBotEditor; @@ -253,12 +257,12 @@ public static void acceptDebugPerspectiveSwitchIfPresent(SWTWorkbenchBot bot) } /** - * Waits until GDB has suspended at a breakpoint (not merely OpenOCD "Target halted" during + * Waits until GDB has suspended at {@code app_main} (not merely OpenOCD "Target halted" during * reset), dismisses the Debug perspective switch dialog, then waits until the Debug toolbar * is ready for stepping. *

- * Breakpoint output is usually on the IDF Process Console page of the Console view — - * not whatever console page happens to be selected. + * Prefers the Eclipse debug model / Debug view over console-page switching. Repeatedly opening + * "Display Selected Console" leaves the dropdown open and stalls the UI under SWTBot. * * @param bot current SWT bot reference * @throws IOException if property lookup fails @@ -279,7 +283,8 @@ public static void waitForDebugSessionStarted(SWTWorkbenchBot bot) throws IOExce perspectiveHandled = acceptDebugPerspectiveSwitchIfPresent(bot, 1000); } - String consoleText = readDebugRelatedConsoleText(bot); + // Do not flip Console pages while polling — that opens a sticky dropdown menu. + String consoleText = readVisibleConsoleText(bot); if (consoleText.contains("shutdown command invoked") || consoleText.contains("dropped 'gdb' connection")) @@ -289,7 +294,7 @@ public static void waitForDebugSessionStarted(SWTWorkbenchBot bot) throws IOExce + consoleText); } - if (isSuspendedAtBreakpoint(consoleText)) + if (isSuspendedAtBreakpoint(bot, consoleText)) { if (!perspectiveHandled) { @@ -302,72 +307,180 @@ public static void waitForDebugSessionStarted(SWTWorkbenchBot bot) throws IOExce bot.sleep(1000); } - String lastConsole = readDebugRelatedConsoleText(bot); + String lastConsole = readVisibleConsoleText(bot); throw new AssertionError( - "Debug session did not suspend at a breakpoint within timeout (expected 'hit Temporary breakpoint' / 'hit Breakpoint' on IDF Process Console).\nLast console text:\n" + "Debug session did not suspend at app_main within timeout (debug model, Debug view, or visible console).\nLast console text:\n" + lastConsole); } /** - * Reads console text from the pages where OpenOCD/GDB output typically appears. + * Reads text from the Console view page that is currently visible (no console-page switching). + * Switching via "Display Selected Console" leaves a sticky dropdown open under SWTBot and + * stalls the debug wait loop. + */ + public static String readVisibleConsoleText(SWTWorkbenchBot bot) + { + try + { + SWTBotView view = bot.viewByPartName("Console"); + view.show(); + view.setFocus(); + String text = view.bot().styledText().getText(); + return text != null ? text : ""; + } + catch (Exception e) + { + logger.debug("Could not read visible Console view: {}", e.getMessage()); + return ""; + } + } + + /** + * Reads console text used for debug assertions from the currently visible Console page only. */ public static String readDebugRelatedConsoleText(SWTWorkbenchBot bot) { - StringBuilder combined = new StringBuilder(); - String[] consolePages = new String[] { "IDF Process Console", "ESP-IDF Console" }; + return readVisibleConsoleText(bot); + } + + private static boolean isSuspendedAtBreakpoint(SWTWorkbenchBot bot, String consoleText) + { + // Prefer debug model / Debug view — avoid depending on console-page selection. + return isSuspendedAtAppMainInDebugModel() + || isSuspendedAtAppMainInDebugView(bot) + || isSuspendedAtBreakpointInConsole(consoleText); + } - for (String consolePage : consolePages) + private static boolean isSuspendedAtBreakpointInConsole(String consoleText) + { + if (consoleText == null || consoleText.isEmpty()) { - try + return false; + } + return consoleText.contains("hit Temporary breakpoint") + || consoleText.contains("hit Breakpoint") + || consoleText.contains("hit breakpoint") + || (consoleText.contains("Temporary breakpoint") && consoleText.contains("app_main")); + } + + /** + * True when an active debug target has a suspended thread whose stack includes {@code app_main}. + * Prefer this over OpenOCD console text — GDB often suspends in the UI without printing + * {@code hit Temporary breakpoint} on the IDF Process Console. + */ + public static boolean isSuspendedAtAppMainInDebugModel() + { + ILaunchManager launchManager = DebugPlugin.getDefault().getLaunchManager(); + ILaunch[] launches = launchManager.getLaunches(); + if (launches == null) + { + return false; + } + + for (ILaunch launch : launches) + { + if (launch == null || launch.isTerminated()) { - SWTBotView view = viewConsole(consolePage, bot); - view.show(); - view.setFocus(); - String text = view.bot().styledText().getText(); - if (text != null && !text.isEmpty()) + continue; + } + + IDebugTarget[] targets = launch.getDebugTargets(); + if (targets == null) + { + continue; + } + + for (IDebugTarget target : targets) + { + if (target == null || target.isTerminated()) { - combined.append(text).append('\n'); - if (isSuspendedAtBreakpoint(text)) + continue; + } + + try + { + if (!target.hasThreads()) { - return text; + continue; + } + for (IThread thread : target.getThreads()) + { + if (thread == null || !thread.isSuspended() || !thread.hasStackFrames()) + { + continue; + } + for (IStackFrame frame : thread.getStackFrames()) + { + if (frame == null) + { + continue; + } + String name = frame.getName(); + if (name != null && name.toLowerCase(Locale.ENGLISH).contains("app_main")) + { + return true; + } + } } } - } - catch (Exception e) - { - logger.debug("Could not read console '{}': {}", consolePage, e.getMessage()); + catch (DebugException e) + { + logger.debug("Could not inspect debug model for app_main suspend: {}", e.getMessage()); + } } } + return false; + } + private static boolean isSuspendedAtAppMainInDebugView(SWTWorkbenchBot bot) + { try { - SWTBotView view = bot.viewByPartName("Console"); + SWTBotView view = bot.viewByTitle("Debug"); view.show(); - view.setFocus(); - String text = view.bot().styledText().getText(); - if (text != null) - { - combined.append(text); - } + return debugTreeContainsAppMainSuspend(view.bot().tree().getAllItems(), 0); } catch (Exception e) { - logger.debug("Could not read default Console view: {}", e.getMessage()); + logger.debug("Could not inspect Debug view for app_main suspend: {}", e.getMessage()); + return false; } - - return combined.toString(); } - private static boolean isSuspendedAtBreakpoint(String consoleText) + private static boolean debugTreeContainsAppMainSuspend(SWTBotTreeItem[] items, int depth) { - if (consoleText == null || consoleText.isEmpty()) + if (items == null || depth > 8) { return false; } - return consoleText.contains("hit Temporary breakpoint") - || consoleText.contains("hit Breakpoint") - || consoleText.contains("hit breakpoint") - || (consoleText.contains("Temporary breakpoint") && consoleText.contains("app_main")); + + for (SWTBotTreeItem item : items) + { + String text = item.getText(); + if (text != null) + { + String lower = text.toLowerCase(Locale.ENGLISH); + boolean hasAppMain = lower.contains("app_main"); + if (hasAppMain && (lower.contains("breakpoint") || lower.contains("suspended") + || lower.contains("main.c"))) + { + return true; + } + } + + try + { + item.expand(); + if (debugTreeContainsAppMainSuspend(item.getItems(), depth + 1)) + { + return true; + } + } + catch (Exception ignored) + { + } + } + return false; } /** @@ -474,6 +587,33 @@ public static void stopDebugSessionAndKillProcesses(SWTWorkbenchBot bot) killDebugProcesses(); } + /** + * Switches the workbench back to the C/C++ perspective (same path as env setup). + * Best-effort — safe to call from {@code @After} even if already on C/C++. + * + * @param bot current SWT bot reference + */ + public static void openCCppPerspective(SWTWorkbenchBot bot) + { + if (bot == null) + { + return; + } + + try + { + bot.menu("Window").menu("Perspective").menu("Open Perspective").menu("Other...").click(); + TestWidgetWaitUtility.waitForDialogToAppear(bot, "Open Perspective", 10000); + bot.table().select("C/C++"); + bot.button("Open").click(); + bot.sleep(1000); + } + catch (Exception e) + { + logger.warn("Failed to switch back to C/C++ perspective", e); + } + } + /** * Terminates every non-terminated launch registered with the Eclipse debug framework. */ From 1e08af1e7cef8a3f108580dafe0e3787db34ff64 Mon Sep 17 00:00:00 2001 From: AndriiFilippov Date: Tue, 21 Jul 2026 11:53:28 +0200 Subject: [PATCH 08/13] ci: improved Step Over execution. Improved env cleanup --- .../project/IDFProjectDebugProcessTest.java | 53 +- .../operations/ProjectTestOperations.java | 481 +++++++++++++++--- .../selectors/LaunchBarModeSelector.java | 86 ---- 3 files changed, 417 insertions(+), 203 deletions(-) delete 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/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 bd0b2a0c8..1ac722102 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 @@ -21,7 +21,6 @@ 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; @@ -72,16 +71,8 @@ public static void beforeTestClass() throws Exception @AfterClass public static void tearDown() { - Fixture.cleanupEnvironment(); - } - - @After - public void afterEachTest() - { - // Always stop OpenOCD/GDB even when an assertion failed mid-test, then leave Debug - // perspective so later suites still start on C/C++ (runs before @AfterClass cleanup). - Fixture.stopDebugSessionAndKillProcesses(); - Fixture.openCCppPerspective(); + // Must not hang: a stuck @AfterClass blocks every later UI test in the same session. + Fixture.forceCleanWorkbench(); } @Test @@ -340,17 +331,7 @@ private static void thenVerifyNoFatalOpenOcdErrors() private static void whenStepOver() { - ProjectTestOperations.waitForDebugStepActionsAvailable(bot, 15000); - try - { - bot.toolbarButtonWithTooltip("Step Over (F6)").click(); - bot.sleep(3000); - } - catch (WidgetNotFoundException e) - { - bot.toolbarButtonWithTooltip("Step Over").click(); - bot.sleep(3000); - } + ProjectTestOperations.performDebugStepOver(bot); } private static void thenVerifyDebugSessionStillActive() @@ -375,39 +356,15 @@ private static void stopDebugSessionAndKillProcesses() ProjectTestOperations.stopDebugSessionAndKillProcesses(bot); } - private static void openCCppPerspective() - { - try - { - ProjectTestOperations.openCCppPerspective(bot); - } - catch (Exception ignored) - { - } - } - - private static void cleanupEnvironment() + private static void forceCleanWorkbench() { try { - stopDebugSessionAndKillProcesses(); - } - catch (Exception ignored) - { - } - - try - { - ProjectTestOperations.closeAllProjects(bot); - ProjectTestOperations.deleteAllProjects(bot); + ProjectTestOperations.forceCleanWorkbenchAfterDebugTest(bot); } catch (Exception ignored) { } - finally - { - ProjectTestOperations.killDebugProcesses(); - } } private static String getNewOutputPart(String outputBeforeSelection, String outputAfterSelection) 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 60ffb2476..059972e91 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 @@ -15,6 +15,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; @@ -26,12 +27,15 @@ import org.eclipse.debug.core.model.IDebugTarget; import org.eclipse.debug.core.model.IStackFrame; import org.eclipse.debug.core.model.IThread; +import org.eclipse.swt.SWT; import org.eclipse.swt.widgets.MenuItem; import org.eclipse.swtbot.eclipse.finder.SWTWorkbenchBot; import org.eclipse.swtbot.eclipse.finder.widgets.SWTBotEditor; import org.eclipse.swtbot.eclipse.finder.widgets.SWTBotView; import org.eclipse.swtbot.swt.finder.exceptions.WidgetNotFoundException; +import org.eclipse.swtbot.swt.finder.finders.UIThreadRunnable; import org.eclipse.swtbot.swt.finder.matchers.WidgetMatcherFactory; +import org.eclipse.swtbot.swt.finder.results.VoidResult; import org.eclipse.swtbot.swt.finder.waits.Conditions; import org.eclipse.swtbot.swt.finder.waits.DefaultCondition; import org.eclipse.swtbot.swt.finder.widgets.SWTBotButton; @@ -44,6 +48,11 @@ import org.eclipse.swtbot.swt.finder.widgets.SWTBotTree; import org.eclipse.swtbot.swt.finder.widgets.SWTBotTreeItem; import org.eclipse.ui.IPageLayout; +import org.eclipse.ui.IPerspectiveDescriptor; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -65,6 +74,10 @@ public class ProjectTestOperations private static final String DEFAULT_FLASH_WAIT_PROPERTY = "default.project.flash.wait"; + private static final String CDT_PERSPECTIVE_ID = "org.eclipse.cdt.ui.CPerspective"; + + private static final String DEBUG_PERSPECTIVE_ID = "org.eclipse.debug.ui.DebugPerspective"; + private static final Logger logger = LoggerFactory.getLogger(ProjectTestOperations.class); private static final int DELETE_PROJECT_TIMEOUT = 240000; @@ -258,10 +271,10 @@ public static void acceptDebugPerspectiveSwitchIfPresent(SWTWorkbenchBot bot) /** * Waits until GDB has suspended at {@code app_main} (not merely OpenOCD "Target halted" during - * reset), dismisses the Debug perspective switch dialog, then waits until the Debug toolbar - * is ready for stepping. + * reset) and opens the Debug perspective. Does not require the Step Over toolbar button — + * that control is often missing/unreliable under SWTBot even when the session is suspended. *

- * Prefers the Eclipse debug model / Debug view over console-page switching. Repeatedly opening + * Prefers the Eclipse debug model over console-page switching. Repeatedly opening * "Display Selected Console" leaves the dropdown open and stalls the UI under SWTBot. * * @param bot current SWT bot reference @@ -300,7 +313,13 @@ public static void waitForDebugSessionStarted(SWTWorkbenchBot bot) throws IOExce { perspectiveHandled = acceptDebugPerspectiveSwitchIfPresent(bot, 30000); } - waitForDebugStepActionsAvailable(bot, 30000); + openDebugPerspective(bot); + if (!hasActiveLaunch()) + { + throw new AssertionError( + "Debug launch terminated immediately after suspend at app_main.\nConsole:\n" + + consoleText); + } return; } @@ -309,7 +328,7 @@ public static void waitForDebugSessionStarted(SWTWorkbenchBot bot) throws IOExce String lastConsole = readVisibleConsoleText(bot); throw new AssertionError( - "Debug session did not suspend at app_main within timeout (debug model, Debug view, or visible console).\nLast console text:\n" + "Debug session did not suspend at app_main within timeout (debug model or visible console).\nLast console text:\n" + lastConsole); } @@ -345,10 +364,9 @@ public static String readDebugRelatedConsoleText(SWTWorkbenchBot bot) private static boolean isSuspendedAtBreakpoint(SWTWorkbenchBot bot, String consoleText) { - // Prefer debug model / Debug view — avoid depending on console-page selection. - return isSuspendedAtAppMainInDebugModel() - || isSuspendedAtAppMainInDebugView(bot) - || isSuspendedAtBreakpointInConsole(consoleText); + // Prefer the debug model only while polling. Expanding the Debug view tree every + // second can leave SWTBot stuck if a Surefire timeout interrupts mid-expand. + return isSuspendedAtAppMainInDebugModel() || isSuspendedAtBreakpointInConsole(consoleText); } private static boolean isSuspendedAtBreakpointInConsole(String consoleText) @@ -432,86 +450,222 @@ public static boolean isSuspendedAtAppMainInDebugModel() return false; } - private static boolean isSuspendedAtAppMainInDebugView(SWTWorkbenchBot bot) + /** + * Waits until a suspended debug thread can step over, then performs Step Over via the + * debug model (preferred). Falls back to Run menu / F6 / toolbar when needed. + * + * @param bot current SWT bot reference + */ + public static void performDebugStepOver(SWTWorkbenchBot bot) { + acceptDebugPerspectiveSwitchIfPresent(bot, 2000); + openDebugPerspective(bot); + + if (!hasActiveLaunch()) + { + throw new AssertionError("Cannot Step Over — no active debug launch"); + } + + final SWTWorkbenchBot workbenchBot = bot; + workbenchBot.waitUntil(new DefaultCondition() + { + @Override + public boolean test() throws Exception + { + if (!hasActiveLaunch()) + { + throw new AssertionError( + "Debug launch terminated before Step Over became available (OpenOCD/GDB already stopped)"); + } + return canStepOverInDebugModel() + || isToolbarButtonPresent(workbenchBot, "Step Over (F6)") + || isToolbarButtonPresent(workbenchBot, "Step Over") + || isRunMenuStepOverPresent(workbenchBot); + } + + @Override + public String getFailureMessage() + { + return "Debug Step Over action not available — debug session may have terminated or Debug perspective did not finish loading"; + } + }, 30000, 500); + + if (stepOverViaDebugModel()) + { + bot.sleep(3000); + return; + } + try { - SWTBotView view = bot.viewByTitle("Debug"); - view.show(); - return debugTreeContainsAppMainSuspend(view.bot().tree().getAllItems(), 0); + bot.menu("Run").menu("Step Over").click(); + bot.sleep(3000); + return; + } + catch (WidgetNotFoundException ignored) + { + } + + try + { + bot.menu("Run").menu("Step Over (F6)").click(); + bot.sleep(3000); + return; + } + catch (WidgetNotFoundException ignored) + { + } + + try + { + bot.toolbarButtonWithTooltip("Step Over (F6)").click(); + bot.sleep(3000); + return; + } + catch (WidgetNotFoundException ignored) + { + } + + try + { + bot.toolbarButtonWithTooltip("Step Over").click(); + bot.sleep(3000); + return; + } + catch (WidgetNotFoundException ignored) + { + } + + try + { + bot.activeShell().pressShortcut(SWT.NONE, SWT.F6); + bot.sleep(3000); + return; } catch (Exception e) { - logger.debug("Could not inspect Debug view for app_main suspend: {}", e.getMessage()); - return false; + throw new AssertionError("Failed to perform Step Over via debug model, menu, toolbar, or F6", e); } } - private static boolean debugTreeContainsAppMainSuspend(SWTBotTreeItem[] items, int depth) + private static boolean isRunMenuStepOverPresent(SWTWorkbenchBot bot) { - if (items == null || depth > 8) + try + { + bot.menu("Run").menu("Step Over"); + return true; + } + catch (WidgetNotFoundException e) + { + try + { + bot.menu("Run").menu("Step Over (F6)"); + return true; + } + catch (WidgetNotFoundException e2) + { + return false; + } + } + } + + private static boolean canStepOverInDebugModel() + { + ILaunchManager launchManager = DebugPlugin.getDefault().getLaunchManager(); + ILaunch[] launches = launchManager.getLaunches(); + if (launches == null) { return false; } - for (SWTBotTreeItem item : items) + for (ILaunch launch : launches) { - String text = item.getText(); - if (text != null) + if (launch == null || launch.isTerminated()) { - String lower = text.toLowerCase(Locale.ENGLISH); - boolean hasAppMain = lower.contains("app_main"); - if (hasAppMain && (lower.contains("breakpoint") || lower.contains("suspended") - || lower.contains("main.c"))) - { - return true; - } + continue; } - - try + IDebugTarget[] targets = launch.getDebugTargets(); + if (targets == null) { - item.expand(); - if (debugTreeContainsAppMainSuspend(item.getItems(), depth + 1)) - { - return true; - } + continue; } - catch (Exception ignored) + for (IDebugTarget target : targets) { + if (target == null || target.isTerminated()) + { + continue; + } + try + { + if (!target.hasThreads()) + { + continue; + } + for (IThread thread : target.getThreads()) + { + if (thread != null && thread.isSuspended() && thread.canStepOver()) + { + return true; + } + } + } + catch (DebugException e) + { + logger.debug("canStepOverInDebugModel: {}", e.getMessage()); + } } } return false; } - /** - * 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) + private static boolean stepOverViaDebugModel() { - final SWTWorkbenchBot workbenchBot = bot; - workbenchBot.waitUntil(new DefaultCondition() + ILaunchManager launchManager = DebugPlugin.getDefault().getLaunchManager(); + ILaunch[] launches = launchManager.getLaunches(); + if (launches == null) { - @Override - public boolean test() throws Exception + return false; + } + + for (ILaunch launch : launches) + { + if (launch == null || launch.isTerminated()) { - if (!hasActiveLaunch()) - { - throw new AssertionError( - "Debug launch terminated before Step Over became available (OpenOCD/GDB already stopped)"); - } - return isToolbarButtonPresent(workbenchBot, "Step Over (F6)") - || isToolbarButtonPresent(workbenchBot, "Step Over"); + continue; } - - @Override - public String getFailureMessage() + IDebugTarget[] targets = launch.getDebugTargets(); + if (targets == null) { - return "Debug Step Over action not available — debug session may have terminated or Debug perspective did not finish loading"; + continue; + } + for (IDebugTarget target : targets) + { + if (target == null || target.isTerminated()) + { + continue; + } + try + { + if (!target.hasThreads()) + { + continue; + } + for (IThread thread : target.getThreads()) + { + if (thread != null && thread.isSuspended() && thread.canStepOver()) + { + thread.stepOver(); + return true; + } + } + } + catch (DebugException e) + { + logger.warn("stepOverViaDebugModel failed: {}", e.getMessage()); + } } - }, timeout, 500); + } + return false; } private static boolean isToolbarButtonPresent(SWTWorkbenchBot bot, String tooltip) @@ -527,6 +681,45 @@ private static boolean isToolbarButtonPresent(SWTWorkbenchBot bot, String toolti } } + /** + * Opens the Eclipse Debug perspective via Platform UI API (no menus/dialogs). + * + * @param bot current SWT bot reference (may be {@code null}) + */ + public static void openDebugPerspective(SWTWorkbenchBot bot) + { + try + { + UIThreadRunnable.syncExec(new VoidResult() + { + @Override + public void run() + { + IWorkbench workbench = PlatformUI.getWorkbench(); + IWorkbenchWindow window = workbench.getActiveWorkbenchWindow(); + if (window == null || window.getActivePage() == null) + { + return; + } + IPerspectiveDescriptor descriptor = workbench.getPerspectiveRegistry() + .findPerspectiveWithId(DEBUG_PERSPECTIVE_ID); + if (descriptor != null) + { + window.getActivePage().setPerspective(descriptor); + } + } + }); + if (bot != null) + { + bot.sleep(1000); + } + } + catch (Exception e) + { + logger.warn("Failed to open Debug perspective", e); + } + } + /** * Returns {@code true} if an active (non-terminated) Eclipse launch still exists. */ @@ -588,30 +781,180 @@ public static void stopDebugSessionAndKillProcesses(SWTWorkbenchBot bot) } /** - * Switches the workbench back to the C/C++ perspective (same path as env setup). - * Best-effort — safe to call from {@code @After} even if already on C/C++. + * Switches the workbench back to the C/C++ perspective via the Platform UI API. + * Avoids Window → Perspective menus / "Open Perspective" dialogs, which can leave a + * modal shell open under SWTBot and block {@code @AfterClass} cleanup. * - * @param bot current SWT bot reference + * @param bot current SWT bot reference (unused; kept for call-site consistency) */ public static void openCCppPerspective(SWTWorkbenchBot bot) { - if (bot == null) + try { - return; + UIThreadRunnable.syncExec(new VoidResult() + { + @Override + public void run() + { + IWorkbench workbench = PlatformUI.getWorkbench(); + IWorkbenchWindow window = workbench.getActiveWorkbenchWindow(); + if (window == null) + { + return; + } + IWorkbenchPage page = window.getActivePage(); + if (page == null) + { + return; + } + IPerspectiveDescriptor descriptor = workbench.getPerspectiveRegistry() + .findPerspectiveWithId(CDT_PERSPECTIVE_ID); + if (descriptor != null) + { + page.setPerspective(descriptor); + } + } + }); + if (bot != null) + { + closeSecondaryShells(bot); + focusMainWindow(bot.shells()); + } } + catch (Exception e) + { + logger.warn("Failed to switch back to C/C++ perspective", e); + } + } + /** + * Force-cleans workbench state after a debug test (including timeout/failure). + * Uses only Platform/debug APIs — no menus, perspective dialogs, or + * {@code WaitUtils.waitForJobs()} — so cleanup cannot hang the Surefire session + * and poison later UI tests. + * + * @param bot current SWT bot reference (may be {@code null}) + */ + public static void forceCleanWorkbenchAfterDebugTest(SWTWorkbenchBot bot) + { try { - bot.menu("Window").menu("Perspective").menu("Open Perspective").menu("Other...").click(); - TestWidgetWaitUtility.waitForDialogToAppear(bot, "Open Perspective", 10000); - bot.table().select("C/C++"); - bot.button("Open").click(); - bot.sleep(1000); + terminateAllLaunches(); } catch (Exception e) { - logger.warn("Failed to switch back to C/C++ perspective", e); + logger.warn("forceClean: terminateAllLaunches failed", e); } + + killDebugProcesses(); + + try + { + openCCppPerspective(bot); + } + catch (Exception e) + { + logger.warn("forceClean: openCCppPerspective failed", e); + } + + try + { + if (bot != null) + { + closeSecondaryShells(bot); + focusMainWindow(bot.shells()); + } + } + catch (Exception e) + { + logger.warn("forceClean: closeSecondaryShells failed", e); + } + + try + { + closeAllEditorsViaApi(); + } + catch (Exception e) + { + logger.warn("forceClean: closeAllEditorsViaApi failed", e); + } + + try + { + deleteAllProjectsViaWorkspaceApi(); + } + catch (Exception e) + { + logger.warn("forceClean: deleteAllProjectsViaWorkspaceApi failed", e); + } + + killDebugProcesses(); + } + + /** + * Closes all open editors without prompting (UI-thread API). + */ + public static void closeAllEditorsViaApi() + { + UIThreadRunnable.syncExec(new VoidResult() + { + @Override + public void run() + { + IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); + if (window == null || window.getActivePage() == null) + { + return; + } + window.getActivePage().closeAllEditors(false); + } + }); + } + + /** + * Deletes every workspace project via the resources API (no Project Explorer UI, + * no {@code WaitUtils.waitForJobs()}). Safe for {@code @AfterClass} cleanup. + */ + public static void deleteAllProjectsViaWorkspaceApi() + { + UIThreadRunnable.syncExec(new VoidResult() + { + @Override + public void run() + { + IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects(); + if (projects == null) + { + return; + } + for (IProject project : projects) + { + if (project == null || !project.exists()) + { + continue; + } + try + { + if (project.isOpen()) + { + project.close(null); + } + } + catch (CoreException e) + { + logger.debug("Could not close project {}: {}", project.getName(), e.getMessage()); + } + try + { + project.delete(true, true, null); + } + catch (CoreException e) + { + logger.warn("Could not delete project {}: {}", project.getName(), e.getMessage()); + } + } + } + }); } /** 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 deleted file mode 100644 index 0e0e55cc3..000000000 --- a/tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/operations/selectors/LaunchBarModeSelector.java +++ /dev/null @@ -1,86 +0,0 @@ -/******************************************************************************* - * 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 fe7de7b9918833b17898134874d64da3aa687e30 Mon Sep 17 00:00:00 2001 From: AndriiFilippov Date: Tue, 21 Jul 2026 13:46:27 +0200 Subject: [PATCH 09/13] ci: improve StepOver execution --- .../project/IDFProjectDebugProcessTest.java | 6 +- .../operations/ProjectTestOperations.java | 129 +++++------------- 2 files changed, 36 insertions(+), 99 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 1ac722102..7486e304d 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 @@ -87,14 +87,14 @@ public void givenNewProjectBuiltAndFlashedViaUartWhenDebugWithEthernetKitThenDeb Fixture.whenTurnOffOpenSerialMonitorAfterFlashingInLaunchConfig(); String esp32SerialPort = Fixture.whenDetectAndSelectEsp32UartSerialPort(); - assumeTrue("Skipping debug test: no ESP32 UART target detected from Serial Port auto-detection", + assertTrue("No ESP32 UART target detected from Serial Port auto-detection", esp32SerialPort != null); Fixture.whenProjectIsBuiltUsingContextMenu(); Fixture.whenFlashProject(); Fixture.thenVerifyFlashDoneSuccessfully(); - assumeTrue("Skipping debug test: ESP32-ETHERNET-KIT board not detected", + assertTrue("ESP32-ETHERNET-KIT board not detected in New ESP Target Board combo", Fixture.whenSelectEsp32EthernetKitBoard()); // Start debug only via Debug As — do not flip Launch Bar mode/config first. @@ -331,7 +331,7 @@ private static void thenVerifyNoFatalOpenOcdErrors() private static void whenStepOver() { - ProjectTestOperations.performDebugStepOver(bot); + ProjectTestOperations.performDebugStepOver(projectName, bot); } private static void thenVerifyDebugSessionStillActive() 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 059972e91..21a07171d 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 @@ -27,7 +27,6 @@ import org.eclipse.debug.core.model.IDebugTarget; import org.eclipse.debug.core.model.IStackFrame; import org.eclipse.debug.core.model.IThread; -import org.eclipse.swt.SWT; import org.eclipse.swt.widgets.MenuItem; import org.eclipse.swtbot.eclipse.finder.SWTWorkbenchBot; import org.eclipse.swtbot.eclipse.finder.widgets.SWTBotEditor; @@ -451,12 +450,13 @@ public static boolean isSuspendedAtAppMainInDebugModel() } /** - * Waits until a suspended debug thread can step over, then performs Step Over via the - * debug model (preferred). Falls back to Run menu / F6 / toolbar when needed. + * Performs Step Over using the Debug toolbar button, or Project Explorer context menu + * {@code Step Over} on the project. Does not use keyboard shortcuts. * - * @param bot current SWT bot reference + * @param projectName project to use for the context-menu fallback + * @param bot current SWT bot reference */ - public static void performDebugStepOver(SWTWorkbenchBot bot) + public static void performDebugStepOver(String projectName, SWTWorkbenchBot bot) { acceptDebugPerspectiveSwitchIfPresent(bot, 2000); openDebugPerspective(bot); @@ -477,50 +477,41 @@ public boolean test() throws Exception throw new AssertionError( "Debug launch terminated before Step Over became available (OpenOCD/GDB already stopped)"); } + // Ready when the thread can step, or the toolbar button is already visible. return canStepOverInDebugModel() || isToolbarButtonPresent(workbenchBot, "Step Over (F6)") - || isToolbarButtonPresent(workbenchBot, "Step Over") - || isRunMenuStepOverPresent(workbenchBot); + || isToolbarButtonPresent(workbenchBot, "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"; + return "Debug Step Over not ready — debug session may have terminated or is not suspended"; } }, 30000, 500); - if (stepOverViaDebugModel()) + if (clickToolbarStepOver(bot)) { bot.sleep(3000); return; } - try + if (clickProjectContextMenuStepOver(projectName, bot)) { - bot.menu("Run").menu("Step Over").click(); bot.sleep(3000); return; } - catch (WidgetNotFoundException ignored) - { - } - try - { - bot.menu("Run").menu("Step Over (F6)").click(); - bot.sleep(3000); - return; - } - catch (WidgetNotFoundException ignored) - { - } + throw new AssertionError( + "Failed to perform Step Over via toolbar button or Project Explorer context menu"); + } + private static boolean clickToolbarStepOver(SWTWorkbenchBot bot) + { try { bot.toolbarButtonWithTooltip("Step Over (F6)").click(); - bot.sleep(3000); - return; + return true; } catch (WidgetNotFoundException ignored) { @@ -529,96 +520,43 @@ public String getFailureMessage() try { bot.toolbarButtonWithTooltip("Step Over").click(); - bot.sleep(3000); - return; + return true; } catch (WidgetNotFoundException ignored) { - } - - try - { - bot.activeShell().pressShortcut(SWT.NONE, SWT.F6); - bot.sleep(3000); - return; - } - catch (Exception e) - { - throw new AssertionError("Failed to perform Step Over via debug model, menu, toolbar, or F6", e); + return false; } } - private static boolean isRunMenuStepOverPresent(SWTWorkbenchBot bot) + private static boolean clickProjectContextMenuStepOver(String projectName, SWTWorkbenchBot bot) { try { - bot.menu("Run").menu("Step Over"); - return true; - } - catch (WidgetNotFoundException e) - { + SWTBotTreeItem projectItem = fetchProjectFromProjectExplorer(projectName, bot); + if (projectItem == null) + { + return false; + } + projectItem.select(); try { - bot.menu("Run").menu("Step Over (F6)"); + projectItem.contextMenu("Step Over").click(); return true; } - catch (WidgetNotFoundException e2) + catch (WidgetNotFoundException e) { - return false; + projectItem.contextMenu("Step Over (F6)").click(); + return true; } } - } - - private static boolean canStepOverInDebugModel() - { - ILaunchManager launchManager = DebugPlugin.getDefault().getLaunchManager(); - ILaunch[] launches = launchManager.getLaunches(); - if (launches == null) + catch (Exception e) { + logger.debug("Project context menu Step Over failed: {}", e.getMessage()); return false; } - - for (ILaunch launch : launches) - { - if (launch == null || launch.isTerminated()) - { - continue; - } - IDebugTarget[] targets = launch.getDebugTargets(); - if (targets == null) - { - continue; - } - for (IDebugTarget target : targets) - { - if (target == null || target.isTerminated()) - { - continue; - } - try - { - if (!target.hasThreads()) - { - continue; - } - for (IThread thread : target.getThreads()) - { - if (thread != null && thread.isSuspended() && thread.canStepOver()) - { - return true; - } - } - } - catch (DebugException e) - { - logger.debug("canStepOverInDebugModel: {}", e.getMessage()); - } - } - } - return false; } - private static boolean stepOverViaDebugModel() + private static boolean canStepOverInDebugModel() { ILaunchManager launchManager = DebugPlugin.getDefault().getLaunchManager(); ILaunch[] launches = launchManager.getLaunches(); @@ -654,14 +592,13 @@ private static boolean stepOverViaDebugModel() { if (thread != null && thread.isSuspended() && thread.canStepOver()) { - thread.stepOver(); return true; } } } catch (DebugException e) { - logger.warn("stepOverViaDebugModel failed: {}", e.getMessage()); + logger.debug("canStepOverInDebugModel: {}", e.getMessage()); } } } From aa1a0b3cce9da9e98304b0ddc1a53039ad1940ac Mon Sep 17 00:00:00 2001 From: AndriiFilippov Date: Tue, 21 Jul 2026 15:03:33 +0200 Subject: [PATCH 10/13] ci: updated StepOver button name selection --- .../idf/ui/test/operations/ProjectTestOperations.java | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 21a07171d..4708731a7 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 @@ -479,6 +479,7 @@ public boolean test() throws Exception } // Ready when the thread can step, or the toolbar button is already visible. return canStepOverInDebugModel() + || isToolbarButtonPresent(workbenchBot, "Step &Over (F6)") || isToolbarButtonPresent(workbenchBot, "Step Over (F6)") || isToolbarButtonPresent(workbenchBot, "Step Over"); } @@ -508,6 +509,15 @@ public String getFailureMessage() private static boolean clickToolbarStepOver(SWTWorkbenchBot bot) { + try + { + bot.toolbarButtonWithTooltip("Step &Over (F6)").click(); + return true; + } + catch (WidgetNotFoundException ignored) + { + } + try { bot.toolbarButtonWithTooltip("Step Over (F6)").click(); From bf372872bcfca58933f2cf8c649e479f781f6259 Mon Sep 17 00:00:00 2001 From: AndriiFilippov Date: Tue, 21 Jul 2026 16:50:41 +0200 Subject: [PATCH 11/13] ci: improved the Env Cleanup. Active process handler --- .../project/IDFProjectDebugProcessTest.java | 43 +++- .../operations/ProjectTestOperations.java | 214 ++++++++++++++---- 2 files changed, 214 insertions(+), 43 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 7486e304d..4d10acfcf 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 @@ -71,8 +71,7 @@ public static void beforeTestClass() throws Exception @AfterClass public static void tearDown() { - // Must not hang: a stuck @AfterClass blocks every later UI test in the same session. - Fixture.forceCleanWorkbench(); + Fixture.cleanupEnvironment(); } @Test @@ -356,14 +355,48 @@ private static void stopDebugSessionAndKillProcesses() ProjectTestOperations.stopDebugSessionAndKillProcesses(bot); } - private static void forceCleanWorkbench() + private static void cleanupEnvironment() { try { - ProjectTestOperations.forceCleanWorkbenchAfterDebugTest(bot); + // Leave Debug UI before the shared project cleanup used by other UI tests. + ProjectTestOperations.leaveDebugUi(bot); } - catch (Exception ignored) + catch (Exception e) + { + System.err.println("leaveDebugUi failed: " + e.getMessage()); + } + + try + { + TestWidgetWaitUtility.waitForOperationsInProgressToFinishAsync(bot); + } + catch (Exception e) + { + System.err.println("waitForOperationsInProgressToFinishAsync failed: " + e.getMessage()); + ProjectTestOperations.cancelJobsThatBlockWorkbenchIdle(); + } + + try { + ProjectTestOperations.closeAllProjects(bot); + ProjectTestOperations.deleteAllProjects(bot); + } + catch (Exception e) + { + // deleteAllProjects → WaitUtils.waitForJobs can still time out if LSP respawns. + System.err.println("close/delete projects failed, retrying after cancelling jobs: " + e.getMessage()); + ProjectTestOperations.cancelJobsThatBlockWorkbenchIdle(); + try + { + ProjectTestOperations.closeAllProjects(bot); + ProjectTestOperations.deleteAllProjects(bot); + } + catch (Exception e2) + { + System.err.println("project cleanup still failed: " + e2.getMessage()); + e2.printStackTrace(); + } } } 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 4708731a7..da9f23a6d 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 @@ -213,7 +213,7 @@ public static void startDebuggingUsingContextMenu(String projectName, SWTWorkben /** * 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. + * session suspends. Does not check "Remember my decision" so later UI tests are not affected. * * @param bot current SWT bot reference * @param timeout how long to wait for the dialog in milliseconds @@ -231,7 +231,8 @@ public static boolean acceptDebugPerspectiveSwitchIfPresent(SWTWorkbenchBot bot, try { SWTBotCheckBox remember = shell.bot().checkBox("Remember my decision"); - if (!remember.isChecked()) + // Do not persist the decision — it poisons later UI tests in the same workbench. + if (remember.isChecked()) { remember.click(); } @@ -706,6 +707,110 @@ public static void stopLaunchUsingLaunchBar(SWTWorkbenchBot bot) } } + /** + * Leaves the Debug UI before shared project cleanup: terminate OpenOCD/GDB, close editors, + * switch to C/C++, close Debug-related views, and cancel background jobs that would otherwise + * keep {@link WaitUtils#waitForJobs()} from returning (Language Server / indexer), which + * poisons later UI tests' {@code deleteAllProjects}. + * + * @param bot current SWT bot reference + */ + public static void leaveDebugUi(SWTWorkbenchBot bot) + { + stopDebugSessionAndKillProcesses(bot); + closeAllEditorsViaApi(); + openCCppPerspective(bot); + closeDebugRelatedViews(bot); + cancelJobsThatBlockWorkbenchIdle(); + if (bot != null) + { + try + { + closeSecondaryShells(bot); + focusMainWindow(bot.shells()); + } + catch (Exception e) + { + logger.warn("leaveDebugUi: could not focus main window", e); + } + } + } + + /** + * Cancels long-running CDT/LSP/refresh jobs that prevent {@code Job.getJobManager().isIdle()} + * after a hardware debug session. Safe to call from {@code @AfterClass}. + */ + public static void cancelJobsThatBlockWorkbenchIdle() + { + Job[] jobs = Job.getJobManager().find(null); + if (jobs == null) + { + return; + } + + for (Job job : jobs) + { + if (job == null || job.getState() == Job.NONE) + { + continue; + } + + String name = job.getName(); + if (name == null) + { + continue; + } + + String lower = name.toLowerCase(Locale.ENGLISH); + if (lower.contains("language server") || lower.contains("clangd") || lower.contains("indexer") + || lower.contains("c/c++") || lower.contains("cdt ") || lower.contains("reconcil") + || lower.contains("refresh") || lower.contains("building workspace") + || lower.contains("updating") || lower.contains("decorate") + || lower.contains("openocd") || lower.contains("gdb")) + { + logger.info("Cancelling job that may block workbench idle: {}", name); + job.cancel(); + } + } + + try + { + Thread.sleep(2000); + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + } + } + + /** + * Best-effort close of views typically opened by the Debug perspective. + * + * @param bot current SWT bot reference + */ + public static void closeDebugRelatedViews(SWTWorkbenchBot bot) + { + if (bot == null) + { + return; + } + + String[] viewTitles = new String[] { "Debug", "Breakpoints", "Variables", "Expressions", + "Registers", "Memory", "Disassembly", "Modules", "Signals", "Executables" }; + + for (String title : viewTitles) + { + try + { + SWTBotView view = bot.viewByTitle(title); + view.close(); + } + catch (Exception ignored) + { + } + } + } + /** * Best-effort cleanup of an active debug session via the debug API and process kill. * Avoids clicking Launch Bar Stop / Debug Terminate toolbars — those tooltips are ambiguous @@ -776,9 +881,9 @@ public void run() /** * Force-cleans workbench state after a debug test (including timeout/failure). - * Uses only Platform/debug APIs — no menus, perspective dialogs, or - * {@code WaitUtils.waitForJobs()} — so cleanup cannot hang the Surefire session - * and poison later UI tests. + * Stops debug processes, returns to C/C++, then deletes projects. Project deletion + * runs on the calling thread (not the UI thread) — {@code syncExec} + + * {@code IProject.delete} can deadlock / silently no-op under SWTBot. * * @param bot current SWT bot reference (may be {@code null}) */ @@ -835,6 +940,21 @@ public static void forceCleanWorkbenchAfterDebugTest(SWTWorkbenchBot bot) logger.warn("forceClean: deleteAllProjectsViaWorkspaceApi failed", e); } + // Fallback used by every other UI test — UI delete if workspace API left anything. + if (bot != null && workspaceHasProjects()) + { + try + { + logger.warn("forceClean: projects still present after workspace API delete; falling back to UI delete"); + closeAllProjects(bot); + deleteAllProjects(bot); + } + catch (Exception e) + { + logger.warn("forceClean: UI project cleanup failed", e); + } + } + killDebugProcesses(); } @@ -859,49 +979,67 @@ public void run() } /** - * Deletes every workspace project via the resources API (no Project Explorer UI, - * no {@code WaitUtils.waitForJobs()}). Safe for {@code @AfterClass} cleanup. + * Deletes every workspace project via the resources API on the calling thread + * (no UI {@code syncExec}). Safe for {@code @AfterClass} cleanup. */ public static void deleteAllProjectsViaWorkspaceApi() { - UIThreadRunnable.syncExec(new VoidResult() + IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects(); + if (projects == null) { - @Override - public void run() + return; + } + + for (IProject project : projects) + { + if (project == null || !project.exists()) + { + continue; + } + + String name = project.getName(); + try { - IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects(); - if (projects == null) + if (project.isOpen()) { - return; + project.close(null); } - for (IProject project : projects) + } + catch (CoreException e) + { + logger.warn("Could not close project {}: {}", name, e.getMessage()); + } + + try + { + if (project.exists()) { - if (project == null || !project.exists()) - { - continue; - } - try - { - if (project.isOpen()) - { - project.close(null); - } - } - catch (CoreException e) - { - logger.debug("Could not close project {}: {}", project.getName(), e.getMessage()); - } - try - { - project.delete(true, true, null); - } - catch (CoreException e) - { - logger.warn("Could not delete project {}: {}", project.getName(), e.getMessage()); - } + project.delete(IResource.ALWAYS_DELETE_PROJECT_CONTENT | IResource.FORCE, null); + logger.info("Deleted workspace project {}", name); } } - }); + catch (CoreException e) + { + logger.warn("Could not delete project {}: {}", name, e.getMessage()); + } + } + } + + private static boolean workspaceHasProjects() + { + IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects(); + if (projects == null) + { + return false; + } + for (IProject project : projects) + { + if (project != null && project.exists()) + { + return true; + } + } + return false; } /** From aaf7375532068c0405d41d146dd72f4a0a510e1a Mon Sep 17 00:00:00 2001 From: AndriiFilippov Date: Tue, 21 Jul 2026 19:09:39 +0200 Subject: [PATCH 12/13] ci: improve cleanup --- .../project/IDFProjectDebugProcessTest.java | 37 ++++++++++--------- .../operations/ProjectTestOperations.java | 11 +++++- 2 files changed, 29 insertions(+), 19 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 4d10acfcf..9d8a9c399 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 @@ -359,7 +359,6 @@ private static void cleanupEnvironment() { try { - // Leave Debug UI before the shared project cleanup used by other UI tests. ProjectTestOperations.leaveDebugUi(bot); } catch (Exception e) @@ -367,37 +366,41 @@ private static void cleanupEnvironment() System.err.println("leaveDebugUi failed: " + e.getMessage()); } + // Delete via workspace API first — UI deleteAllProjects calls WaitUtils.waitForJobs() + // which times out for ~5 minutes when Language Server jobs never go idle after debug, + // leaving NewProjectDebugProcessTest in the workspace for the next test class. try { - TestWidgetWaitUtility.waitForOperationsInProgressToFinishAsync(bot); + ProjectTestOperations.cancelJobsThatBlockWorkbenchIdle(); + ProjectTestOperations.deleteAllProjectsViaWorkspaceApi(); } catch (Exception e) { - System.err.println("waitForOperationsInProgressToFinishAsync failed: " + e.getMessage()); - ProjectTestOperations.cancelJobsThatBlockWorkbenchIdle(); + System.err.println("deleteAllProjectsViaWorkspaceApi failed: " + e.getMessage()); } try { ProjectTestOperations.closeAllProjects(bot); - ProjectTestOperations.deleteAllProjects(bot); } catch (Exception e) { - // deleteAllProjects → WaitUtils.waitForJobs can still time out if LSP respawns. - System.err.println("close/delete projects failed, retrying after cancelling jobs: " + e.getMessage()); + System.err.println("closeAllProjects failed: " + e.getMessage()); + } + + try + { ProjectTestOperations.cancelJobsThatBlockWorkbenchIdle(); - try - { - ProjectTestOperations.closeAllProjects(bot); - ProjectTestOperations.deleteAllProjects(bot); - } - catch (Exception e2) - { - System.err.println("project cleanup still failed: " + e2.getMessage()); - e2.printStackTrace(); - } + ProjectTestOperations.deleteAllProjects(bot); } + catch (Exception e) + { + System.err.println("UI deleteAllProjects failed (expected if jobs never idle): " + e.getMessage()); + ProjectTestOperations.deleteAllProjectsViaWorkspaceApi(); + } + + ProjectTestOperations.openCCppPerspective(bot); + ProjectTestOperations.killDebugProcesses(); } private static String getNewOutputPart(String outputBeforeSelection, String outputAfterSelection) 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 da9f23a6d..729e72360 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 @@ -861,10 +861,17 @@ public void run() } IPerspectiveDescriptor descriptor = workbench.getPerspectiveRegistry() .findPerspectiveWithId(CDT_PERSPECTIVE_ID); - if (descriptor != null) + if (descriptor == null) + { + logger.warn("C/C++ perspective id not found: {}", CDT_PERSPECTIVE_ID); + return; + } + IPerspectiveDescriptor current = page.getPerspective(); + if (current != null && DEBUG_PERSPECTIVE_ID.equals(current.getId())) { - page.setPerspective(descriptor); + page.closePerspective(current, false, false); } + page.setPerspective(descriptor); } }); if (bot != null) From 6300e9f58a41f1ffd830f4f472cbc98c2daff6b8 Mon Sep 17 00:00:00 2001 From: AndriiFilippov Date: Wed, 22 Jul 2026 17:05:37 +0200 Subject: [PATCH 13/13] ci: StepOver timeout --- .../project/IDFProjectDebugProcessTest.java | 6 + .../operations/ProjectTestOperations.java | 419 +++++++++++++++--- 2 files changed, 359 insertions(+), 66 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 9d8a9c399..388ca8ffd 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 @@ -310,11 +310,15 @@ private static void thenVerifyFlashDoneSuccessfully() throws Exception private static void whenStartDebuggingUsingContextMenu() { ProjectTestOperations.startDebuggingUsingContextMenu(projectName, bot); + // Give OpenOCD/GDB and perspective-switch UI time to appear. + bot.sleep(3000); } private static void thenVerifyDebugSessionStarted() throws Exception { ProjectTestOperations.waitForDebugSessionStarted(bot); + // Settle Debug perspective / toolbar after suspend at app_main. + bot.sleep(3000); } private static void thenVerifyNoFatalOpenOcdErrors() @@ -330,7 +334,9 @@ private static void thenVerifyNoFatalOpenOcdErrors() private static void whenStepOver() { + bot.sleep(2000); ProjectTestOperations.performDebugStepOver(projectName, bot); + bot.sleep(2000); } private static void thenVerifyDebugSessionStillActive() 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 729e72360..8158ca43c 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 @@ -34,6 +34,7 @@ import org.eclipse.swtbot.swt.finder.exceptions.WidgetNotFoundException; import org.eclipse.swtbot.swt.finder.finders.UIThreadRunnable; import org.eclipse.swtbot.swt.finder.matchers.WidgetMatcherFactory; +import org.eclipse.swtbot.swt.finder.results.Result; import org.eclipse.swtbot.swt.finder.results.VoidResult; import org.eclipse.swtbot.swt.finder.waits.Conditions; import org.eclipse.swtbot.swt.finder.waits.DefaultCondition; @@ -43,6 +44,7 @@ import org.eclipse.swtbot.swt.finder.widgets.SWTBotMenu; import org.eclipse.swtbot.swt.finder.widgets.SWTBotShell; import org.eclipse.swtbot.swt.finder.widgets.SWTBotTable; +import org.eclipse.swtbot.swt.finder.widgets.SWTBotToolbarButton; import org.eclipse.swtbot.swt.finder.widgets.SWTBotToolbarDropDownButton; import org.eclipse.swtbot.swt.finder.widgets.SWTBotTree; import org.eclipse.swtbot.swt.finder.widgets.SWTBotTreeItem; @@ -52,6 +54,7 @@ import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.handlers.IHandlerService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -382,17 +385,134 @@ private static boolean isSuspendedAtBreakpointInConsole(String consoleText) } /** - * True when an active debug target has a suspended thread whose stack includes {@code app_main}. + * True when an active debug target has a thread whose stack includes {@code app_main}. * Prefer this over OpenOCD console text — GDB often suspends in the UI without printing * {@code hit Temporary breakpoint} on the IDF Process Console. + *

+ * FreeRTOS-aware GDB often reports the main thread as {@code Running} even while the CPU is + * halted at {@code app_main}; do not require {@link IThread#isSuspended()}. */ public static boolean isSuspendedAtAppMainInDebugModel() + { + return findThreadWithAppMainFrame() != null; + } + + /** + * Performs Step Over via debug model API, Debug toolbar / Run menu, Debug view context menu, + * or Project Explorer fallback. Does not use keyboard shortcuts. + *

+ * FreeRTOS threads often show {@code Running} while halted, so readiness is based on an + * {@code app_main} stack frame (or a visible Step Over control), not {@code isSuspended()} alone. + * + * @param projectName project to use for the context-menu fallback + * @param bot current SWT bot reference + */ + public static void performDebugStepOver(String projectName, SWTWorkbenchBot bot) + { + acceptDebugPerspectiveSwitchIfPresent(bot, 2000); + openDebugPerspective(bot); + // Let Debug perspective / toolbar finish loading before probing Step Over controls. + bot.sleep(3000); + + if (!hasActiveLaunch()) + { + throw new AssertionError("Cannot Step Over — no active debug launch"); + } + + final SWTWorkbenchBot workbenchBot = bot; + try + { + workbenchBot.waitUntil(new DefaultCondition() + { + @Override + public boolean test() throws Exception + { + if (!hasActiveLaunch()) + { + throw new AssertionError( + "Debug launch terminated before Step Over became available (OpenOCD/GDB already stopped)"); + } + acceptDebugPerspectiveSwitchIfPresent(workbenchBot, 200); + // FreeRTOS: app_main frame is enough; isSuspended()/canStepOver() are often false. + return findThreadWithAppMainFrame() != null + || canStepOverInDebugModel() + || isStepOverToolbarPresent(workbenchBot); + } + + @Override + public String getFailureMessage() + { + return "Debug Step Over not ready within timeout — no app_main stack frame, " + + "canStepOver=false, and Step Over toolbar not found. " + + "Launch active=" + hasActiveLaunch() + + ", app_main frame=" + (findThreadWithAppMainFrame() != null) + + ", canStepOver=" + canStepOverInDebugModel() + + ", stepOverToolbar=" + isStepOverToolbarPresent(workbenchBot); + } + }, 60000, 500); + } + catch (AssertionError e) + { + // Ensure CI always shows a non-empty reason (some runners truncate blank AssertionError). + String detail = e.getMessage(); + if (detail == null || detail.trim().isEmpty()) + { + throw new AssertionError( + "Debug Step Over not ready within timeout (empty wait failure). Launch active=" + + hasActiveLaunch() + ", app_main frame=" + + (findThreadWithAppMainFrame() != null), + e); + } + throw e; + } + + bot.sleep(1500); + + if (stepOverViaDebugModel()) + { + bot.sleep(3000); + return; + } + if (stepOverViaDebugCommand()) + { + bot.sleep(3000); + return; + } + if (clickToolbarStepOver(bot)) + { + bot.sleep(3000); + return; + } + if (clickRunMenuStepOver(bot)) + { + bot.sleep(3000); + return; + } + if (clickDebugViewStepOver(bot)) + { + bot.sleep(3000); + return; + } + if (clickProjectContextMenuStepOver(projectName, bot)) + { + bot.sleep(3000); + return; + } + + throw new AssertionError( + "Failed to perform Step Over (debug API, command, toolbar, Run menu, Debug view, " + + "and Project Explorer all failed). Launch active=" + hasActiveLaunch() + + ", app_main frame=" + (findThreadWithAppMainFrame() != null) + + ", canStepOver=" + canStepOverInDebugModel()); + } + + private static IThread findThreadWithAppMainFrame() { ILaunchManager launchManager = DebugPlugin.getDefault().getLaunchManager(); ILaunch[] launches = launchManager.getLaunches(); if (launches == null) { - return false; + return null; } for (ILaunch launch : launches) @@ -401,20 +521,17 @@ public static boolean isSuspendedAtAppMainInDebugModel() { continue; } - IDebugTarget[] targets = launch.getDebugTargets(); if (targets == null) { continue; } - for (IDebugTarget target : targets) { if (target == null || target.isTerminated()) { continue; } - try { if (!target.hasThreads()) @@ -423,7 +540,7 @@ public static boolean isSuspendedAtAppMainInDebugModel() } for (IThread thread : target.getThreads()) { - if (thread == null || !thread.isSuspended() || !thread.hasStackFrames()) + if (thread == null || thread.isTerminated() || !thread.hasStackFrames()) { continue; } @@ -436,109 +553,269 @@ public static boolean isSuspendedAtAppMainInDebugModel() String name = frame.getName(); if (name != null && name.toLowerCase(Locale.ENGLISH).contains("app_main")) { - return true; + return thread; } } } } catch (DebugException e) { - logger.debug("Could not inspect debug model for app_main suspend: {}", e.getMessage()); + logger.debug("findThreadWithAppMainFrame: {}", e.getMessage()); } } } - return false; + return null; } - /** - * Performs Step Over using the Debug toolbar button, or Project Explorer context menu - * {@code Step Over} on the project. Does not use keyboard shortcuts. - * - * @param projectName project to use for the context-menu fallback - * @param bot current SWT bot reference - */ - public static void performDebugStepOver(String projectName, SWTWorkbenchBot bot) + private static boolean stepOverViaDebugModel() { - acceptDebugPerspectiveSwitchIfPresent(bot, 2000); - openDebugPerspective(bot); - - if (!hasActiveLaunch()) - { - throw new AssertionError("Cannot Step Over — no active debug launch"); - } - - final SWTWorkbenchBot workbenchBot = bot; - workbenchBot.waitUntil(new DefaultCondition() + IThread appMainThread = findThreadWithAppMainFrame(); + if (appMainThread != null) { - @Override - public boolean test() throws Exception + try { - if (!hasActiveLaunch()) + if (appMainThread.canStepOver()) { - throw new AssertionError( - "Debug launch terminated before Step Over became available (OpenOCD/GDB already stopped)"); + appMainThread.stepOver(); + return true; } - // Ready when the thread can step, or the toolbar button is already visible. - return canStepOverInDebugModel() - || isToolbarButtonPresent(workbenchBot, "Step &Over (F6)") - || isToolbarButtonPresent(workbenchBot, "Step Over (F6)") - || isToolbarButtonPresent(workbenchBot, "Step Over"); } - - @Override - public String getFailureMessage() + catch (DebugException e) { - return "Debug Step Over not ready — debug session may have terminated or is not suspended"; + logger.debug("stepOverViaDebugModel(app_main): {}", e.getMessage()); } - }, 30000, 500); + } - if (clickToolbarStepOver(bot)) + ILaunchManager launchManager = DebugPlugin.getDefault().getLaunchManager(); + ILaunch[] launches = launchManager.getLaunches(); + if (launches == null) { - bot.sleep(3000); - return; + return false; } + for (ILaunch launch : launches) + { + if (launch == null || launch.isTerminated()) + { + continue; + } + IDebugTarget[] targets = launch.getDebugTargets(); + if (targets == null) + { + continue; + } + for (IDebugTarget target : targets) + { + if (target == null || target.isTerminated()) + { + continue; + } + try + { + if (!target.hasThreads()) + { + continue; + } + for (IThread thread : target.getThreads()) + { + if (thread != null && thread.canStepOver()) + { + thread.stepOver(); + return true; + } + } + } + catch (DebugException e) + { + logger.debug("stepOverViaDebugModel: {}", e.getMessage()); + } + } + } + return false; + } - if (clickProjectContextMenuStepOver(projectName, bot)) + private static boolean stepOverViaDebugCommand() + { + try { - bot.sleep(3000); - return; + Boolean ok = UIThreadRunnable.syncExec(new Result() + { + @Override + public Boolean run() + { + try + { + IHandlerService handlers = PlatformUI.getWorkbench().getService(IHandlerService.class); + if (handlers == null) + { + return Boolean.FALSE; + } + handlers.executeCommand("org.eclipse.debug.ui.commands.StepOver", null); + return Boolean.TRUE; + } + catch (Exception e) + { + logger.debug("stepOverViaDebugCommand: {}", e.getMessage()); + return Boolean.FALSE; + } + } + }); + return Boolean.TRUE.equals(ok); } + catch (Exception e) + { + logger.debug("stepOverViaDebugCommand failed: {}", e.getMessage()); + return false; + } + } - throw new AssertionError( - "Failed to perform Step Over via toolbar button or Project Explorer context menu"); + private static boolean isStepOverToolbarPresent(SWTWorkbenchBot bot) + { + return findStepOverToolbarButton(bot) != null + || isToolbarButtonPresent(bot, "Step &Over (F6)") + || isToolbarButtonPresent(bot, "Step Over (F6)") + || isToolbarButtonPresent(bot, "Step Over"); } - private static boolean clickToolbarStepOver(SWTWorkbenchBot bot) + private static SWTBotToolbarButton findStepOverToolbarButton(SWTWorkbenchBot bot) { try { - bot.toolbarButtonWithTooltip("Step &Over (F6)").click(); - return true; + for (SWTBotToolbarButton button : bot.toolbarButtons()) + { + String tip = button.getToolTipText(); + if (tip != null && tip.toLowerCase(Locale.ENGLISH).contains("step over")) + { + return button; + } + } } - catch (WidgetNotFoundException ignored) + catch (Exception e) { + logger.debug("findStepOverToolbarButton: {}", e.getMessage()); } + return null; + } - try + private static boolean clickToolbarStepOver(SWTWorkbenchBot bot) + { + SWTBotToolbarButton matched = findStepOverToolbarButton(bot); + if (matched != null) { - bot.toolbarButtonWithTooltip("Step Over (F6)").click(); - return true; + try + { + matched.click(); + return true; + } + catch (Exception e) + { + logger.debug("Matched Step Over toolbar click failed: {}", e.getMessage()); + } + } + + String[] tooltips = { "Step &Over (F6)", "Step Over (F6)", "Step Over", "Step Over (F6) (Alt+Shift+O)" }; + for (String tooltip : tooltips) + { + try + { + bot.toolbarButtonWithTooltip(tooltip).click(); + return true; + } + catch (WidgetNotFoundException ignored) + { + } } - catch (WidgetNotFoundException ignored) + return false; + } + + private static boolean clickRunMenuStepOver(SWTWorkbenchBot bot) + { + String[] labels = { "Step Over (F6)", "Step &Over (F6)", "Step Over", "Step &Over" }; + for (String label : labels) { + try + { + bot.menu("Run").menu(label).click(); + return true; + } + catch (WidgetNotFoundException ignored) + { + } + catch (Exception e) + { + logger.debug("Run menu Step Over ({}) failed: {}", label, e.getMessage()); + } } + return false; + } + private static boolean clickDebugViewStepOver(SWTWorkbenchBot bot) + { try { - bot.toolbarButtonWithTooltip("Step Over").click(); - return true; + SWTBotView debugView = bot.viewByPartName("Debug"); + debugView.show(); + debugView.setFocus(); + bot.sleep(1000); + SWTBotTree tree = debugView.bot().tree(); + SWTBotTreeItem appMain = findTreeItemContaining(tree.getAllItems(), "app_main"); + if (appMain == null) + { + return false; + } + appMain.select(); + bot.sleep(500); + try + { + appMain.contextMenu("Step Over").click(); + return true; + } + catch (WidgetNotFoundException e) + { + appMain.contextMenu("Step Over (F6)").click(); + return true; + } } - catch (WidgetNotFoundException ignored) + catch (Exception e) { + logger.debug("Debug view Step Over failed: {}", e.getMessage()); return false; } } + private static SWTBotTreeItem findTreeItemContaining(SWTBotTreeItem[] items, String text) + { + if (items == null) + { + return null; + } + String needle = text.toLowerCase(Locale.ENGLISH); + for (SWTBotTreeItem item : items) + { + if (item == null) + { + continue; + } + String label = item.getText(); + if (label != null && label.toLowerCase(Locale.ENGLISH).contains(needle)) + { + return item; + } + try + { + item.expand(); + } + catch (Exception ignored) + { + } + SWTBotTreeItem nested = findTreeItemContaining(item.getItems(), text); + if (nested != null) + { + return nested; + } + } + return null; + } + private static boolean clickProjectContextMenuStepOver(String projectName, SWTWorkbenchBot bot) { try @@ -601,7 +878,8 @@ private static boolean canStepOverInDebugModel() } for (IThread thread : target.getThreads()) { - if (thread != null && thread.isSuspended() && thread.canStepOver()) + // Do not require isSuspended() — FreeRTOS often reports Running while halted. + if (thread != null && thread.canStepOver()) { return true; } @@ -1658,13 +1936,22 @@ public static void deleteAllProjects(SWTWorkbenchBot bot) public static void launchCommandUsingContextMenu(String projectName, SWTWorkbenchBot bot, String contextMenuLabel) { + // After a Debug-perspective test, Project Explorer / focus may still be on Debug UI — + // restore C/C++ and focus the main window so the context menu actually opens the dialog. + openCCppPerspective(bot); + focusMainWindow(bot.shells()); + SWTBotTreeItem projectItem = fetchProjectFromProjectExplorer(projectName, bot); - if (projectItem != null) + if (projectItem == null) { - projectItem.select(); - projectItem.contextMenu(contextMenuLabel).click(); + throw new WidgetNotFoundException("Project not found in Project Explorer: " + projectName); } - WaitUtils.waitForJobs(); + projectItem.select(); + projectItem.contextMenu(contextMenuLabel).click(); + // Do not WaitUtils.waitForJobs() here. For dialogs like "Run Configurations" the shell + // appears immediately while background jobs (e.g. Language Server) may keep running; + // waiting for idle first makes the caller's waitForDialogToAppear miss a visible dialog + // or time out for the wrong reason. Callers that need jobs to finish should wait themselves. } public static void findInConsole(SWTWorkbenchBot bot, String consoleName, String findText) throws IOException