diff mbox series

[13/19] oe-selftest: devtool ide-sdk cover vscode remote debugging

Message ID 20250918210754.477049-14-adrian.freihofer@siemens.com
State New
Headers show
Series devtool: ide-sdk: Enhance debugging and testing | expand

Commit Message

AdrianF Sept. 18, 2025, 9:07 p.m. UTC
From: Adrian Freihofer <adrian.freihofer@siemens.com>

This adds more tests to coverage for devtool ide-sdk, with VSCode. The
cmake test case has now a full remote debugging test on Qemu. The test
checks the generated launch.json and tasks.json files, starts
gdbserver and connects to it. The test verifies breakpoints, variables
and source file listing.

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

Patch

diff --git a/meta/lib/oeqa/selftest/cases/devtool.py b/meta/lib/oeqa/selftest/cases/devtool.py
index 8bfb73a927c..5f861fd3ecf 100644
--- a/meta/lib/oeqa/selftest/cases/devtool.py
+++ b/meta/lib/oeqa/selftest/cases/devtool.py
@@ -9,20 +9,23 @@  import os
 import re
 import shutil
 import tempfile
+import time
 import glob
 import fnmatch
 import unittest
 import json
 import logging
+import shlex
 
 from oeqa.selftest.case import OESelftestTestCase
-from oeqa.utils.commands import runCmd, bitbake, get_bb_var, create_temp_layer
+from oeqa.utils.commands import runCmd, Command, bitbake, get_bb_var, create_temp_layer
 from oeqa.utils.commands import get_bb_vars, runqemu, runqemu_check_taps, get_test_layer
 from oeqa.core.decorator import OETestTag
 from bb.utils import mkdirhier, edit_bblayers_conf
 
 oldmetapath = None
 
+
 def setUpModule():
     global templayerdir
     templayerdir = tempfile.mkdtemp(prefix='devtoolqa')
@@ -2523,8 +2526,23 @@  class DevtoolUpgradeTests(DevtoolBase):
         runCmd("grep %s %s" % (modconfopt, codeconfigfile))
 
 
+
+class RunCmdBackground:
+    """Context manager to manage a background subprocess"""
+    def __init__(self, command, output_log=None, **options):
+        self.cmd = Command(command, bg=True, output_log=output_log, **options)
+
+    def __enter__(self):
+        self.cmd.run()
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self.cmd.stop()
+
+
 class DevtoolIdeSdkTests(DevtoolBase):
 
+    MAGIC_STRING_ORIG = "Magic: 123456789"
+
     def setUp(self):
         super().setUp()
         self._cmd_logger = None
@@ -2642,7 +2660,6 @@  class DevtoolIdeSdkTests(DevtoolBase):
                           '%s script not found' % install_deploy_cmd)
         runCmd(install_deploy_cmd, output_log=self._cmd_logger)
 
-        MAGIC_STRING_ORIG = "Magic: 123456789"
         MAGIC_STRING_NEW = "Magic: 987654321"
         ptest_cmd = "ptest-runner " + recipe_name
 
@@ -2655,7 +2672,7 @@  class DevtoolIdeSdkTests(DevtoolBase):
         status, output = qemu.run(example_exe)
         self.assertEqual(status, 0, msg="%s failed: %s" %
                          (example_exe, output))
-        self.assertIn(MAGIC_STRING_ORIG, output)
+        self.assertIn(DevtoolIdeSdkTests.MAGIC_STRING_ORIG, output)
 
         # Verify the unmodified ptests work
         status, output = qemu.run(ptest_cmd)
@@ -2664,13 +2681,13 @@  class DevtoolIdeSdkTests(DevtoolBase):
 
         # Verify remote debugging works
         self._gdb_cross_debugging_multi(
-            qemu, recipe_name, example_exe, MAGIC_STRING_ORIG)
+            qemu, recipe_name, example_exe, DevtoolIdeSdkTests.MAGIC_STRING_ORIG)
 
         # Replace the Magic String in the code, compile and deploy to Qemu
         cpp_example_lib_hpp = os.path.join(tempdir, 'cpp-example-lib.hpp')
         with open(cpp_example_lib_hpp, 'r') as file:
             cpp_code = file.read()
-            cpp_code = cpp_code.replace(MAGIC_STRING_ORIG, MAGIC_STRING_NEW)
+            cpp_code = cpp_code.replace(DevtoolIdeSdkTests.MAGIC_STRING_ORIG, MAGIC_STRING_NEW)
         with open(cpp_example_lib_hpp, 'w') as file:
             file.write(cpp_code)
         runCmd(install_deploy_cmd, cwd=tempdir, output_log=self._cmd_logger)
@@ -2679,7 +2696,7 @@  class DevtoolIdeSdkTests(DevtoolBase):
         status, output = qemu.run(example_exe)
         self.assertEqual(status, 0, msg="%s failed: %s" %
                          (example_exe, output))
-        self.assertNotIn(MAGIC_STRING_ORIG, output)
+        self.assertNotIn(DevtoolIdeSdkTests.MAGIC_STRING_ORIG, output)
         self.assertIn(MAGIC_STRING_NEW, output)
 
         # Verify the modified example ptests work
@@ -2704,6 +2721,24 @@  class DevtoolIdeSdkTests(DevtoolBase):
         self.assertEqual(r.status, 0)
         self.assertIn("GNU gdb", r.output)
 
+    def _gdb_debug_cpp_example(self, magic_string, gdb_start_cmd="run"):
+        """Get a series of gdb commands to debug the cpp-example-lib example"""
+        gdb_batch_cmd = " -ex 'break main' -ex '%s'" % gdb_start_cmd
+        gdb_batch_cmd += " -ex 'break CppExample::print_json()' -ex 'continue'"
+        gdb_batch_cmd += " -ex 'print CppExample::test_string.compare(\"cpp-example-lib %s\")'" % magic_string
+        gdb_batch_cmd += " -ex 'print CppExample::test_string.compare(\"cpp-example-lib %saaa\")'" % magic_string
+        gdb_batch_cmd += " -ex 'list cpp-example-lib.hpp:14,14'"
+        gdb_batch_cmd += " -ex 'continue'"
+        return gdb_batch_cmd
+
+    def _gdb_debug_cpp_example_check(self, gdb_output, magic_string):
+        self.assertIn("Breakpoint 1, main", gdb_output)
+        self.assertIn("$1 = 0", gdb_output)  # test.string.compare equal
+        self.assertIn("$2 = -3", gdb_output)  # test.string.compare longer
+        self.assertIn(
+            'inline static const std::string test_string = "cpp-example-lib %s";' % magic_string, gdb_output)
+        self.assertIn("exited normally", gdb_output)
+
     def _gdb_cross_debugging_multi(self, qemu, recipe_name, example_exe, magic_string):
         """Verify gdb-cross is working
 
@@ -2744,24 +2779,12 @@  class DevtoolIdeSdkTests(DevtoolBase):
         self.assertIn("gdbserver", r.output)
 
         # Test remote debugging works
-        gdb_batch_cmd = " --batch -ex 'break main' -ex 'run'"
-        gdb_batch_cmd += " -ex 'break CppExample::print_json()' -ex 'continue'"
-        gdb_batch_cmd += " -ex 'print CppExample::test_string.compare(\"cpp-example-lib %s\")'" % magic_string
-        gdb_batch_cmd += " -ex 'print CppExample::test_string.compare(\"cpp-example-lib %saaa\")'" % magic_string
-        gdb_batch_cmd += " -ex 'list cpp-example-lib.hpp:14,14'"
-        gdb_batch_cmd += " -ex 'continue'"
-        return gdb_batch_cmd
-
+        gdb_batch_cmd = " --batch " + self._gdb_debug_cpp_example(magic_string)
         r = runCmd(gdb_script + gdb_batch_cmd, output_log=self._cmd_logger)
         self.logger.debug("%s %s returned: %s", gdb_script,
                           gdb_batch_cmd, r.output)
         self.assertEqual(r.status, 0)
-        self.assertIn("Breakpoint 1, main", r.output)
-        self.assertIn("$1 = 0", r.output)  # test.string.compare equal
-        self.assertIn("$2 = -3", r.output)  # test.string.compare longer
-        self.assertIn(
-            'inline static const std::string test_string = "cpp-example-lib %s";' % magic_string, r.output)
-        self.assertIn("exited normally", r.output)
+        self._gdb_debug_cpp_example_check(r.output, magic_string=magic_string)
 
         # Stop the gdbserver
         r = runCmd(gdbserver_script + ' stop', output_log=self._cmd_logger)
@@ -2862,6 +2885,7 @@  class DevtoolIdeSdkTests(DevtoolBase):
             bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@%s -c --ide=none' % (
                 recipe_name, testimage, qemu.ip)
             runCmd(bitbake_sdk_cmd, output_log=self._cmd_logger)
+
             self._gdb_cross()
             self._verify_cmake_preset(tempdir)
             self._devtool_ide_sdk_qemu(tempdir, qemu, recipe_name, example_exe)
@@ -2893,6 +2917,7 @@  class DevtoolIdeSdkTests(DevtoolBase):
             bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@%s -c --ide=none' % (
                 recipe_name, testimage, qemu.ip)
             runCmd(bitbake_sdk_cmd, output_log=self._cmd_logger)
+
             self._gdb_cross()
             self._devtool_ide_sdk_qemu(tempdir, qemu, recipe_name, example_exe)
 
@@ -2904,22 +2929,264 @@  class DevtoolIdeSdkTests(DevtoolBase):
             # after the install and deploy scripts updated the file
             self._verify_conf_file(qemu, conf_file, example_user_group, example_user_group)
 
+    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')
+        self.assertTrue(os.path.exists(launch_json_path), "launch.json file should exist")
+
+        with open(launch_json_path) as launch_j:
+            launch_d = json.load(launch_j)
+
+        self.assertIn("configurations", launch_d)
+        configurations = launch_d["configurations"]
+        self.assertEqual(len(configurations), 3, "Should have exactly three debug configurations")
+
+        # Track configurations found
+        once_configs = []
+        attach_configs = []
+
+        for config in configurations:
+            # Verify required fields exist
+            required_fields = ["name", "type", "request", "program", "cwd", "MIMode",
+                             "miDebuggerPath", "miDebuggerServerAddress"]
+            for field in required_fields:
+                self.assertIn(field, config, f"Configuration '{config.get('name', 'Unknown')}' missing required field: {field}")
+
+            # Verify common configuration values
+            self.assertEqual(config["type"], "cppdbg", f"Configuration '{config['name']}' should use cppdbg type")
+            self.assertEqual(config["request"], "launch", f"Configuration '{config['name']}' should be launch type")
+            self.assertEqual(config["cwd"], "${workspaceFolder}", f"Configuration '{config['name']}' should use workspaceFolder as cwd")
+            self.assertEqual(config["MIMode"], "gdb", f"Configuration '{config['name']}' should use gdb MIMode")
+            self.assertEqual(config.get("externalConsole", False), False, f"Configuration '{config['name']}' should not use external console")
+            self.assertEqual(config.get("stopAtEntry", True), True, f"Configuration '{config['name']}' should stop at entry")
+
+            # Verify program path is absolute and exists conceptually
+            program = config["program"]
+            self.assertTrue(program.startswith("/"), f"Configuration '{config['name']}' program path should be absolute: {program}")
+            self.assertIn("/image/usr/bin/", program, f"Configuration '{config['name']}' program should be in image/usr/bin")
+
+            # Verify debugger path
+            debugger_path = config["miDebuggerPath"]
+            self.assertTrue(debugger_path.endswith("-gdb"), f"Configuration '{config['name']}' debugger should end with -gdb: {debugger_path}")
+            self.assertIn("/recipe-sysroot-native/usr/bin/", debugger_path, f"Configuration '{config['name']}' debugger should be in sysroot-native")
+
+            # Verify server address format
+            server_addr = config["miDebuggerServerAddress"]
+            self.assertRegex(server_addr, r"^\d+\.\d+\.\d+\.\d+:\d+$", f"Configuration '{config['name']}' server address should be IP:PORT format: {server_addr}")
+
+            # Verify additional SO lib search path exists and contains debug paths
+            so_paths = config.get("additionalSOLibSearchPath", [])
+            self.assertIn("/.debug", so_paths, f"Configuration '{config['name']}' should include debug symbol paths")
+            self.assertIn("/rootfs-dbg/", so_paths, f"Configuration '{config['name']}' should include rootfs-dbg paths")
+
+            # Verify source file mappings
+            source_map = config.get("sourceFileMap", {})
+            self.assertIsInstance(source_map, dict, f"Configuration '{config['name']}' sourceFileMap should be a dictionary")
+            self.assertIn("/usr/src/debug", source_map, f"Configuration '{config['name']}' should map /usr/src/debug")
+            self.assertIn("${workspaceFolder}", str(source_map), f"Configuration '{config['name']}' should map to workspaceFolder")
+
+            # Verify setup commands for sysroot
+            setup_commands = config.get("setupCommands", [])
+            self.assertTrue(len(setup_commands) >= 1, f"Configuration '{config['name']}' should have setup commands")
+            sysroot_cmd = setup_commands[0]
+            self.assertIn("sysroot", sysroot_cmd.get("text", ""), f"Configuration '{config['name']}' should set sysroot in setup commands")
+
+            # Verify preLaunchTask exists and matches name pattern
+            task = config.get("preLaunchTask", "")
+            self.assertTrue(task, f"Configuration '{config['name']}' preLaunchTask should not be empty")
+            # Task should contain port number and executable name from config name
+            config_name = config["name"]
+            if "_once" in config_name:
+                self.assertIn("_once", task, f"once configuration '{config_name}' should have once preLaunchTask")
+            elif "_attach" in config_name:
+                self.assertIn("_attach", task, f"attach configuration '{config_name}' should have attach preLaunchTask")
+
+            # Categorize configurations
+            config_name = config["name"]
+            if "_once" in config_name:
+                once_configs.append(config_name)
+            elif "_attach" in config_name:
+                attach_configs.append(config_name)
+
+        # Verify we have expected configuration types
+        self.assertEqual(len(once_configs), 2, f"Should have two once configuration, found: {once_configs}")
+        self.assertEqual(len(attach_configs), 1, f"Should have one attach configuration, found: {attach_configs}")
+
+    def _verify_launch_json_debugging(self, tempdir, qemu, recipe_name, example_exe):
+        """Verify remote debugging and deployment works using launch.json configurations
+
+        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:14,14'  \
+            -ex 'continue'
+        3. Verifying debug output and stopping gdbserver
+        """
+        with open(os.path.join(tempdir, '.vscode', 'launch.json')) as launch_j:
+            launch_d = json.load(launch_j)
+        with open(os.path.join(tempdir, '.vscode', 'tasks.json')) as tasks_j:
+            tasks_d = json.load(tasks_j)
+
+        configurations = launch_d["configurations"]
+        tasks = tasks_d["tasks"]
+
+        # Test one configuration for remote debugging
+        once_config_count = 0
+        for config in configurations:
+            if f"usr-bin-{recipe_name}_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-{recipe_name}_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}")
+
+    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")
+
+        # Extract configuration values for launch.json
+        debugger_path = launch_config["miDebuggerPath"]
+        server_addr = launch_config["miDebuggerServerAddress"]
+        prelaunch_task_name = launch_config["preLaunchTask"]
+        program = launch_config["program"]
+        additional_so_lib_search_path = launch_config["additionalSOLibSearchPath"]
+        source_file_map = launch_config["sourceFileMap"]
+        setup_commands = launch_config["setupCommands"]
+
+        # Find the preLaunchTask in tasks.json
+        prelaunch_task = next(
+            (task for task in tasks if task["label"] == prelaunch_task_name), None)
+        self.assertIsNotNone(prelaunch_task, f"PreLaunchTask '{prelaunch_task_name}' not found in tasks.json")
+
+        # Find the dependsOn task if exists (install and deploy-target)
+        if "dependsOn" in prelaunch_task:
+            depends_task_names = prelaunch_task["dependsOn"]
+            for depends_task_name in depends_task_names:
+                depends_task = next(
+                    (task for task in tasks if task["label"] == depends_task_name), None)
+                self.assertIsNotNone(depends_task, f"DependsOn task '{depends_task_name}' not found in tasks.json")
+                # For simplicity, we assume the dependsOn task is a prerequisite and does not affect the main command
+                self.logger.debug(f"PreLaunchTask '{prelaunch_task_name}' depends on '{depends_task_name}'")
+
+                # Extract command details from dependsOn task
+                depends_task_command = depends_task["command"]
+                depends_task_args = depends_task.get("args", [])
+                self.logger.debug(f"Would execute dependsOn task: {depends_task_command} {' '.join(depends_task_args)}")
+                runCmd(f"{depends_task_command} {' '.join(depends_task_args)}", output_log=self._cmd_logger)
+
+        # Verify task structure and extract command details
+        self.assertEqual(prelaunch_task["type"], "shell", f"Task '{prelaunch_task_name}' should be shell type")
+        task_command = prelaunch_task["command"]
+        task_args = prelaunch_task["args"]
+
+        # The command should be ssh for remote execution
+        self.assertEqual(task_command, "ssh", f"Task '{prelaunch_task_name}' should use ssh command")
+        self.assertTrue(len(task_args) >= 2, f"Task '{prelaunch_task_name}' should have at least 2 args (ssh options and remote command)")
+
+        sshargs = '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'
+        result = runCmd('ssh %s root@%s %s' % (sshargs, qemu.ip, 'test -x /usr/bin/gdbserver'),
+                        output_log=self._cmd_logger)
+        self.assertEqual(result.status, 0, "gdbserver should be installed on target")
+        result = runCmd('ssh %s root@%s %s' % (sshargs, qemu.ip, 'test -x ' + os.path.join('/usr/bin', example_exe)),
+                        output_log=self._cmd_logger)
+        self.assertEqual(result.status, 0, "Example binary should be installed on target")
+
+        # Start gdbserver on target using the task command (keep the ssh connection open while debugging)
+        ssh_gdbserver_cmd = [task_command] + task_args
+        # Fix shell command escaping - remove extra quotes from the last argument
+        # The task_args likely contains a quoted shell command that needs to be unquoted
+        if len(ssh_gdbserver_cmd) > 0 and ssh_gdbserver_cmd[-1].startswith('"') and ssh_gdbserver_cmd[-1].endswith('"'):
+            ssh_gdbserver_cmd[-1] = ssh_gdbserver_cmd[-1][1:-1]  # Remove surrounding quotes
+        self.logger.debug(f"Starting gdbserver with command: {' '.join(ssh_gdbserver_cmd)}")
+        with RunCmdBackground(ssh_gdbserver_cmd, output_log=self._cmd_logger):
+            # Give gdbserver a moment to start
+            time.sleep(1)
+
+            # Verify gdbserver is running on target and listening on expected port
+            result = runCmd('ssh %s root@%s %s' % (sshargs, qemu.ip, 'ps'), output_log=self._cmd_logger)
+            self.assertEqual(result.status, 0, "Failed to check processes on target")
+            self.assertIn("gdbserver", result.output, "gdbserver should be running on target")
+            _, server_port = server_addr.split(':')
+            self.assertIn(server_port, result.output, f"gdbserver should be listening on port {server_port}")
+
+            if debug_func and debug_check_func:
+                # Do a gdb remote session using the once configuration
+                gdb_batch_cmd = debugger_path + " --batch"
+                for setup_command in setup_commands:
+                    # What VSCode does, for tracing add "logging": {"engineLogging": true } to launch.json
+                    setup_cmd = setup_command["text"].strip()
+                    if setup_cmd.startswith("-"):
+                        # Ignore commands starting with '-' as they are VSCode internal commands?
+                        continue
+                    else:
+                        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 remote %s'" % server_addr
+                # Add a basic set of command performing a simple debugging session
+                gdb_batch_cmd += debug_func(DevtoolIdeSdkTests.MAGIC_STRING_ORIG, "continue")
+                self.logger.debug(f"Starting gdb session with command: {gdb_batch_cmd}")
+                r = runCmd(gdb_batch_cmd, output_log=self._cmd_logger)
+                self.logger.debug("%s %s returned: %s", debugger_path, gdb_batch_cmd, r.output)
+                self.assertEqual(r.status, 0)
+                debug_check_func(r.output, DevtoolIdeSdkTests.MAGIC_STRING_ORIG)
+
+    @OETestTag("runqemu")
     def test_devtool_ide_sdk_code_cmake(self):
         """Verify a cmake recipe works with ide=code mode"""
         recipe_name = "cmake-example"
+        example_exe = "cmake-example"
         build_file = "CMakeLists.txt"
         testimage = "oe-selftest-image"
+        build_file = "CMakeLists.txt"
 
         self._check_workspace()
         self._write_bb_config([recipe_name])
-        tempdir = self._devtool_ide_sdk_recipe(
-            recipe_name, build_file, testimage)
-        bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@192.168.17.17 -c --ide=code' % (
-            recipe_name, testimage)
-        runCmd(bitbake_sdk_cmd, output_log=self._cmd_logger)
-        self._verify_cmake_preset(tempdir)
-        self._verify_install_script_code(tempdir,  recipe_name)
-        self._gdb_cross()
+
+        # Verify deployment to Qemu (system mode) works
+        self._check_runqemu_prerequisites()
+        bitbake(testimage)
+        with runqemu(testimage, runqemuparams="nographic") as qemu:
+            tempdir = self._devtool_ide_sdk_recipe(
+                recipe_name, build_file, testimage)
+            bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@%s -c --ide=code' % (
+                recipe_name, testimage, qemu.ip)
+            runCmd(bitbake_sdk_cmd, output_log=self._cmd_logger)
+            self._verify_cmake_preset(tempdir)
+            self._verify_install_script_code(tempdir,  recipe_name)
+            self._gdb_cross()
+
+            # Verify the launch.json file created is valid
+            self._verify_launch_json(tempdir)
+
+            # Verify deployment and remote debugging works
+            self._verify_launch_json_debugging(tempdir, qemu, recipe_name, example_exe)
 
     def test_devtool_ide_sdk_code_meson(self):
         """Verify a meson recipe works with ide=code mode"""