From: Adrian Freihofer <[email protected]> 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 <[email protected]> --- meta/lib/oeqa/selftest/cases/devtool.py | 152 +++++++++++++++++++----- 1 file changed, 119 insertions(+), 33 deletions(-) diff --git a/meta/lib/oeqa/selftest/cases/devtool.py b/meta/lib/oeqa/selftest/cases/devtool.py index ab846b8dc3..ef9ebf0f30 100644 --- a/meta/lib/oeqa/selftest/cases/devtool.py +++ b/meta/lib/oeqa/selftest/cases/devtool.py @@ -3399,10 +3399,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"] @@ -3480,6 +3489,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"] @@ -3495,34 +3506,11 @@ class DevtoolIdeSdkGccTests(DevtoolIdeSdkTests): "Each debug configuration should use a distinct server address") 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) @@ -3538,18 +3526,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") -- 2.55.0
-=-=-=-=-=-=-=-=-=-=-=- Links: You receive all messages sent to this group. View/Reply Online (#243666): https://lists.openembedded.org/g/openembedded-core/message/243666 Mute This Topic: https://lists.openembedded.org/mt/120804345/21656 Group Owner: [email protected] Unsubscribe: https://lists.openembedded.org/g/openembedded-core/unsub [[email protected]] -=-=-=-=-=-=-=-=-=-=-=-
