diff mbox series

[14/14] oe-selftest: devtool ide-sdk: verify attach debug workflow

Message ID 20260816194127.86607-15-adrian.freihofer@siemens.com
State New
Headers show
Series devtool: ide-sdk: robust attach-mode debugging and test cleanup | expand

Commit Message

AdrianF Aug. 16, 2026, 7:40 p.m. UTC
From: Adrian Freihofer <adrian.freihofer@siemens.com>

Add attach-mode debug verification in ide=code selftests and document
the intended execution flow:

- Verify attach postDebugTask labels against tasks.json.
- Run attach checks before once checks to avoid Build-ID mismatch from
  once/deploy side effects on the still-running service process.
- In attach mode, set a temporary breakpoint on sleep(), continue once
  to hit it, then detach immediately.

This verifies debugger control flow against the already-running loop
without fragile inferior expression evaluation.

Signed-off-by: Adrian Freihofer <adrian.freihofer@siemens.com>
---
 meta/lib/oeqa/selftest/cases/devtool.py | 152 +++++++++++++++++++-----
 1 file changed, 119 insertions(+), 33 deletions(-)
diff mbox series

Patch

diff --git a/meta/lib/oeqa/selftest/cases/devtool.py b/meta/lib/oeqa/selftest/cases/devtool.py
index 5301b6013e..52aa199319 100644
--- a/meta/lib/oeqa/selftest/cases/devtool.py
+++ b/meta/lib/oeqa/selftest/cases/devtool.py
@@ -3339,10 +3339,19 @@  class DevtoolIdeSdkGccTests(DevtoolIdeSdkTests):
     def _verify_launch_json(self, tempdir):
         """Verify the launch.json file created is valid and contains proper debug configurations"""
         launch_json_path = os.path.join(tempdir, '.vscode', 'launch.json')
+        tasks_json_path = os.path.join(tempdir, '.vscode', 'tasks.json')
         self.assertTrue(os.path.exists(launch_json_path), "launch.json file should exist")
+        self.assertTrue(os.path.exists(tasks_json_path), "tasks.json file should exist")
 
         with open(launch_json_path) as launch_j:
             launch_d = json.load(launch_j)
+        with open(tasks_json_path) as tasks_j:
+            tasks_d = json.load(tasks_j)
+        task_labels = {
+            task.get("label")
+            for task in tasks_d.get("tasks", [])
+            if task.get("label")
+        }
 
         self.assertIn("configurations", launch_d)
         configurations = launch_d["configurations"]
@@ -3418,6 +3427,8 @@  class DevtoolIdeSdkGccTests(DevtoolIdeSdkTests):
                 self.assertIn("_attach", task, f"attach configuration '{config_name}' should have attach preLaunchTask")
                 # Verify postDebugTask exists for attach configurations (kill gdbserver)
                 self.assertIn("postDebugTask", config, f"attach configuration '{config_name}' should have postDebugTask")
+                self.assertIn(config["postDebugTask"], task_labels,
+                              f"attach configuration '{config_name}' postDebugTask should exist in tasks.json")
 
             # Categorize configurations
             config_name = config["name"]
@@ -3431,34 +3442,11 @@  class DevtoolIdeSdkGccTests(DevtoolIdeSdkTests):
         self.assertEqual(len(attach_configs), 1, f"Should have one attach configuration, found: {attach_configs}")
 
     def _verify_launch_json_debugging(self, tempdir, qemu, example_exe):
-        """Verify remote debugging and deployment works using launch.json configurations
+        """Verify launch.json debugging configs for one target executable.
 
-        This method tests the VSCode debug configurations by:
-        1. Starting gdbserver on target using tasks.json commands
-        2. Running gdb debugging session with batch commands
-            $BUILDDIR/tmp/work/x86_64-linux/gdb-cross-x86_64/16.3/recipe-sysroot-native/usr/bin/x86_64-poky-linux/x86_64-poky-linux-gdb --batch  \
-            -ex 'set sysroot $BUILDDIR/tmp/work/x86-64-v3-poky-linux/cmake-example/1.0/image'  \
-            -ex 'set substitute-path /usr/include $BUILDDIR/tmp/work/x86-64-v3-poky-linux/cmake-example/1.0/recipe-sysroot/usr/include'  \
-            -ex 'set substitute-path /usr/src/debug/cmake-example/1.0 $BUILDDIR/workspace/sources/cmake-example'  \
-            -ex 'set substitute-path /usr/src/debug $BUILDDIR/tmp/work/qemux86_64-poky-linux/oe-selftest-image/1.0/rootfs-dbg/usr/src/debug'  \
-            -ex 'set solib-search-path $BUILDDIR/tmp/work/qemux86_64-poky-linux/oe-selftest-image/1.0/rootfs-dbg/lib/.debug:\
-                $BUILDDIR/tmp/work/qemux86_64-poky-linux/oe-selftest-image/1.0/rootfs-dbg/usr/lib/.debug:\
-                $BUILDDIR/tmp/work/qemux86_64-poky-linux/oe-selftest-image/1.0/rootfs-dbg/usr/lib/debug:\
-                $BUILDDIR/tmp/work/qemux86_64-poky-linux/oe-selftest-image/1.0/rootfs-dbg/lib:\
-                $BUILDDIR/tmp/work/qemux86_64-poky-linux/oe-selftest-image/1.0/rootfs-dbg/usr/lib:\
-                $BUILDDIR/tmp/work/qemux86_64-poky-linux/oe-selftest-image/1.0/rootfs/lib:\
-                $BUILDDIR/tmp/work/qemux86_64-poky-linux/oe-selftest-image/1.0/rootfs/usr/lib'  \
-            -ex 'file $BUILDDIR/tmp/work/x86-64-v3-poky-linux/cmake-example/1.0/image/usr/bin/cmake-example'  \
-            -ex 'target remote 192.168.7.2:1234'  \
-            -ex 'break main'  \
-            -ex 'continue'  \
-            -ex 'break CppExample::print_json()'  \
-            -ex 'continue'  \
-            -ex 'print CppExample::test_string.compare("cpp-example-lib Magic: 123456789")'  \
-            -ex 'print CppExample::test_string.compare("cpp-example-lib Magic: 123456789aaa")'  \
-            -ex 'list cpp-example-lib.hpp:15,15'  \
-            -ex 'continue'
-        3. Verifying debug output and stopping gdbserver
+        Runs attach-mode first against the already-running service process,
+        then validates the once configuration. This ordering avoids Build-ID
+        mismatch from once/deploy side effects before attach is tested.
         """
         with open(os.path.join(tempdir, '.vscode', 'launch.json')) as launch_j:
             launch_d = json.load(launch_j)
@@ -3474,18 +3462,116 @@  class DevtoolIdeSdkGccTests(DevtoolIdeSdkTests):
         # as the recipe name (e.g. meson-example installs a binary named
         # "mesonex").
         once_config_count = 0
+        attach_config_count = 0
+
+        # Run attach first. The attach test targets the already-running
+        # service process in the image; running a once configuration first can
+        # deploy/update binaries and trigger Build-ID mismatch warnings against
+        # that still-running process.
+        for config in configurations:
+            if f"usr-bin-{example_exe}_attach" in config["name"]:
+                attach_config_count += 1
+                self._verify_launch_config_attach(tempdir, config, qemu, example_exe)
+
         for config in configurations:
             if f"usr-bin-{example_exe}_once" in config["name"]:
                 once_config_count += 1
                 self._verify_launch_config(tempdir, config, tasks, qemu, example_exe,
                                            self._gdb_debug_cpp_example, self._gdb_debug_cpp_example_check)
-            # It works but is not 100% reliable in VSCode
-            # This one: https://github.com/microsoft/vscode-cpptools/issues/4243 ?
-            # elif f"usr-bin-{example_exe}_attach" in config["name"]
-            #     self._verify_launch_config(tempdir, config, tasks, qemu, example_exe)
-            else:
-                continue
         self.assertEqual(once_config_count, 1, f"Should have one once configuration, found: {once_config_count}")
+        self.assertEqual(attach_config_count, 1, f"Should have one attach configuration, found: {attach_config_count}")
+
+    def _verify_launch_config_attach(self, tempdir, launch_config, qemu, example_exe):
+        """Verify attach-mode debugging by exercising debugger control flow.
+
+        The attached service runs an endless loop, and each loop iteration
+        executes sleep(). We break on sleep() to prove that attach-mode gdb is
+        controlling the already-running process at a point that is expected to
+        be reached repeatedly during normal service execution, then detach.
+        """
+        self.assertEqual(launch_config.get("request"), "attach")
+
+        tasks_path = os.path.join(tempdir, '.vscode', 'tasks.json')
+        self.assertExists(tasks_path, 'tasks.json not found at %s' % tasks_path)
+        with open(tasks_path) as tasks_j:
+            tasks_d = json.load(tasks_j)
+        task_by_label = {
+            task.get('label'): task
+            for task in tasks_d.get('tasks', [])
+            if task.get('label')
+        }
+
+        debugger_path = launch_config["miDebuggerPath"]
+        server_addr = launch_config["miDebuggerServerAddress"]
+        prelaunch_task_name = launch_config["preLaunchTask"]
+        post_debug_task_name = launch_config.get("postDebugTask")
+        program = launch_config["program"]
+        additional_so_lib_search_path = launch_config["additionalSOLibSearchPath"]
+        source_file_map = launch_config["sourceFileMap"]
+        setup_commands = launch_config["setupCommands"]
+
+        self.assertTrue(post_debug_task_name,
+                        "attach configuration should define postDebugTask")
+        self.assertIn(prelaunch_task_name, task_by_label)
+        self.assertIn(post_debug_task_name, task_by_label)
+
+        status, _ = qemu.run('test -x /usr/bin/gdbserver')
+        self.assertEqual(status, 0, "gdbserver should be installed on target")
+        status, _ = qemu.run('test -x ' + os.path.join('/usr/bin', example_exe))
+        self.assertEqual(status, 0, "Example binary should be installed on target")
+
+        # The attach preLaunchTask has no dependsOn (it attaches to an already
+        # running service instead of building/deploying), so it can be run
+        # directly without a dependency chain runner.
+        prelaunch_task = task_by_label[prelaunch_task_name]
+        prelaunch_cmd = [prelaunch_task["command"]] + [str(arg)
+                                                         for arg in prelaunch_task.get("args", [])]
+        # The tasks.json command is shell-quoted and escapes '$' for an
+        # intermediate shell, so undo that before passing argv to subprocess.
+        if prelaunch_cmd[-1].startswith('"') and prelaunch_cmd[-1].endswith('"'):
+            prelaunch_cmd[-1] = prelaunch_cmd[-1][1:-1].replace('\\$', '$')
+        runCmd(prelaunch_cmd, output_log=self._cmd_logger)
+
+        # Attach to the service process currently running on the target.
+        status, output = qemu.run("pgrep '^%s$'" % example_exe)
+        self.assertEqual(status, 0, msg="%s service not running: %s" %
+                         (example_exe, output))
+        service_pid = output.strip()
+        self.assertRegex(service_pid, r'^\d+$')
+
+        gdb_batch_cmd = debugger_path + " --batch"
+        for setup_command in setup_commands:
+            setup_cmd = setup_command["text"].strip()
+            if setup_cmd.startswith("-"):
+                continue
+            gdb_batch_cmd += ' -ex ' + shlex.quote(setup_cmd)
+        for k, v in source_file_map.items():
+            gdb_batch_cmd += " -ex 'set substitute-path %s %s'" % (k, v.replace("${workspaceFolder}", tempdir))
+        gdb_batch_cmd += " -ex 'set solib-search-path %s'" % additional_so_lib_search_path
+        gdb_batch_cmd += " -ex 'file %s'" % program
+        gdb_batch_cmd += " -ex 'target extended-remote %s'" % server_addr
+        gdb_batch_cmd += " -ex 'attach %s'" % service_pid
+        gdb_batch_cmd += " -ex 'set breakpoint pending on'"
+        gdb_batch_cmd += " -ex 'tbreak sleep'"
+        gdb_batch_cmd += " -ex 'continue'"
+        gdb_batch_cmd += " -ex 'detach'"
+        self.logger.debug(f"Starting attach gdb session with command: {gdb_batch_cmd}")
+
+        r = runCmd(gdb_batch_cmd, output_log=self._cmd_logger)
+        self.assertEqual(r.status, 0)
+        self.assertIn("sleep", r.output)
+
+        # Stop the persistent attach server and verify the service survives.
+        post_debug_task = task_by_label[post_debug_task_name]
+        post_debug_cmd = [post_debug_task["command"]] + [str(arg)
+                                                          for arg in post_debug_task.get("args", [])]
+        if post_debug_cmd[-1].startswith('"') and post_debug_cmd[-1].endswith('"'):
+            post_debug_cmd[-1] = post_debug_cmd[-1][1:-1].replace('\\$', '$')
+        runCmd(post_debug_cmd, output_log=self._cmd_logger)
+
+        status, output = qemu.run("pgrep '^%s$'" % example_exe)
+        self.assertEqual(status, 0, msg="%s service should still be running after attach stop: %s" %
+                         (example_exe, output))
 
     def _verify_launch_config(self, tempdir, launch_config, tasks, qemu, example_exe, debug_func=None, debug_check_func=None):
         self.assertIsNotNone(launch_config, "Should have at least one launch debug configuration")