<!DOCTYPE html>
<html data-lt-installed="true">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  </head>
  <body style="padding-bottom: 1px;">
    <p>Hi, Sergey,</p>
    <p>thanks for the patch! Please see my comments.</p>
    <p>Sergey</p>
    <div class="moz-cite-prefix">On 5/19/26 15:39, Sergey Kaplun wrote:<br>
    </div>
    <blockquote type="cite"
      cite="mid:20260519123913.178775-2-skaplun@tarantool.org">
      <pre wrap="" class="moz-quote-pre">From: Maxim Kokryashkin <a class="moz-txt-link-rfc2396E" href="mailto:m.kokryashkin@tarantool.org"><m.kokryashkin@tarantool.org></a></pre>
    </blockquote>
    <p>It looks like the patch was made by Maxim K. and you took it as
      is, but it is not so.</p>
    <p>The latest version of this patch is here [1] and there several
      changes introduced by you.</p>
    <p>I suppose these changes should be described and <span
        class="HwtZe" lang="en"><span class="jCAhz"><span class="ryNqvb">you
            should to somehow</span></span></span></p>
    <p><span class="HwtZe" lang="en"><span class="jCAhz"><span
            class="ryNqvb">say that you changed the patch a little (for
            example using Git trailer "Co-authored-by:"). </span></span></span></p>
    <p><span class="HwtZe" lang="en"><span class="jCAhz ChMk0b"><span
            class="ryNqvb">Otherwise, we are following bad practices,
            let's not be like those people we usually joke about.</span></span></span></p>
    <p><span class="HwtZe" lang="en"><span class="jCAhz ChMk0b"><span
            class="ryNqvb">The full diff is below:</span></span></span></p>
    <p><span class="HwtZe" lang="en"><span class="jCAhz ChMk0b"><span
            class="ryNqvb">--- dbg/debug-extension-tests.py   
            2026-05-20 16:02:11.854999756 +0300<br>
            +++ test/tarantool-debugger-tests/debug-extension-tests.py 
              2026-05-20 16:02:43.124696812 +0300<br>
            @@ -1,4 +1,6 @@<br>
            -# This file provides tests for LuaJIT debug extensions for
            lldb and gdb.<br>
            +# This file provides tests for LuaJIT debug extensions for
            lldb<br>
            +# and gdb.<br>
            +<br>
             import os<br>
             import re<br>
             import subprocess<br>
            @@ -11,12 +13,27 @@<br>
             LEGACY = re.match(r'^2\.', sys.version)<br>
             <br>
             LUAJIT_BINARY = os.environ['LUAJIT_TEST_BINARY']<br>
            -EXTENSION = os.environ['DEBUGGER_EXTENSION_PATH']<br>
            +EXTENSION_PATH = os.environ['DEBUGGER_EXTENSION_PATH']<br>
             DEBUGGER = os.environ['DEBUGGER_COMMAND']<br>
             LLDB = 'lldb' in DEBUGGER<br>
            +EXTENSION = EXTENSION_PATH + ('/luajit_lldb.py' if LLDB
            else '/luajit-gdb.py')<br>
             TIMEOUT = 10<br>
             <br>
            -RUN_CMD_FILE = '-s' if LLDB else '-x'<br>
            +# Don't run any initialization scripts.<br>
            +RUN_CMD_FILE = []<br>
            +<br>
            +if LLDB:<br>
            +    RUN_CMD_FILE = [<br>
            +        '--batch',<br>
            +        '--no-lldbinit',<br>
            +        '--no-use-colors',<br>
            +        '--source-quietly',<br>
            +        '--source'<br>
            +    ]<br>
            +else:<br>
            +    # GDB.<br>
            +    RUN_CMD_FILE = ['--batch', '--nx', '--quiet',
            '--command']<br>
            +<br>
             INFERIOR_ARGS = '--' if LLDB else '--args'<br>
             PROCESS_RUN = 'process launch' if LLDB else 'r'<br>
             LOAD_EXTENSION = (<br>
            @@ -24,6 +41,11 @@<br>
             ).format(ext=EXTENSION)<br>
             <br>
             <br>
            +RX_ADDR = r'0x[a-f0-9]+'<br>
            +RX_HASH = RX_ADDR  # The same pattern for hexademic values.<br>
            +RX_FRAME = r'\[(S|\s)(B|\s)(T|\s)(M|\s)\]'<br>
            +<br>
            +<br>
             def persist(data):<br>
                 tmp = tempfile.NamedTemporaryFile(mode='w')<br>
                 tmp.write(data)<br>
            @@ -33,32 +55,39 @@<br>
             <br>
             def execute_process(cmd, timeout=TIMEOUT):<br>
                 if LEGACY:<br>
            -        # XXX: The Python 2.7 version of `subprocess.Popen`
            doesn't have a<br>
            -        # timeout option, so the required functionality was
            implemented via<br>
            -        # `threading.Timer`.<br>
            -        process = subprocess.Popen(cmd,
            stdout=subprocess.PIPE)<br>
            +        # XXX: The Python 2.7 version of `subprocess.Popen`<br>
            +        # doesn't have a timeout option, so the required<br>
            +        # functionality was implemented via
            `threading.Timer`.<br>
            +        process = subprocess.Popen(<br>
            +            cmd,<br>
            +            stdout=subprocess.PIPE,<br>
            +            stderr=subprocess.PIPE,<br>
            +            # This prevents sending of SIGSTTOU to the test
            when<br>
            +            # running by `make'. Stdin is unused anyway.<br>
            +            stdin=subprocess.DEVNULL<br>
            +        )<br>
                     timer = Timer(TIMEOUT, process.kill)<br>
                     timer.start()<br>
                     stdout, _ = process.communicate()<br>
                     timer.cancel()<br>
             <br>
            -        # XXX: If the timeout is exceeded and the process
            is killed by the<br>
            -        # timer, then the return code is non-zero, and we
            are going to blow up.<br>
            +        # XXX: If the timeout is exceeded and the process
            is<br>
            +        # killed by the timer, then the return code is
            non-zero,<br>
            +        # and we are going to blow up.<br>
                     assert process.returncode == 0<br>
                     return stdout.decode('ascii')<br>
                 else:<br>
            -        process = subprocess.run(cmd, capture_output=True,
            timeout=TIMEOUT)<br>
            -        return process.stdout.decode('ascii')<br>
            -<br>
            -<br>
            -def filter_debugger_output(output):<br>
            -    descriptor = '(lldb)' if LLDB else '(gdb)'<br>
            -    return ''.join(<br>
            -        filter(<br>
            -            lambda line: not line.startswith(descriptor),<br>
            -            output.splitlines(True),<br>
            -        ),<br>
            -    )<br>
            +        process = subprocess.run(<br>
            +            cmd,<br>
            +            stdout=subprocess.PIPE,<br>
            +            stderr=subprocess.PIPE,<br>
            +            # This prevents sending of SIGSTTOU to the test
            when<br>
            +            # running by `make'. Stdin is unused anyway.<br>
            +            stdin=subprocess.DEVNULL,<br>
            +            universal_newlines=True,<br>
            +            timeout=TIMEOUT<br>
            +        )<br>
            +        return process.stdout<br>
             <br>
             <br>
             class TestCaseBase(unittest.TestCase):<br>
            @@ -79,13 +108,13 @@<br>
                     script_file = persist(cls.lua_script)<br>
                     process_cmd = [<br>
                         DEBUGGER,<br>
            -            RUN_CMD_FILE,<br>
            +            *RUN_CMD_FILE,<br>
                         cmd_file.name,<br>
                         INFERIOR_ARGS,<br>
                         LUAJIT_BINARY,<br>
                         script_file.name,<br>
                     ]<br>
            -        cls.output =
            filter_debugger_output(execute_process(process_cmd))<br>
            +        cls.output = execute_process(process_cmd)<br>
                     cmd_file.close()<br>
                     script_file.close()<br>
             <br>
            @@ -101,14 +130,14 @@<br>
                 location = 'lj_cf_print'<br>
                 lua_script = 'print(1)'<br>
                 pattern = (<br>
            -        'lj-tv command intialized\n'<br>
            -        'lj-state command intialized\n'<br>
            -        'lj-arch command intialized\n'<br>
            -        'lj-gc command intialized\n'<br>
            -        'lj-str command intialized\n'<br>
            -        'lj-tab command intialized\n'<br>
            -        'lj-stack command intialized\n'<br>
            -        'LuaJIT debug extension is successfully loaded\n'<br>
            +        r'lj-arch command initialized\n'<br>
            +        r'lj-tv command initialized\n'<br>
            +        r'lj-str command initialized\n'<br>
            +        r'lj-tab command initialized\n'<br>
            +        r'lj-stack command initialized\n'<br>
            +        r'lj-state command initialized\n'<br>
            +        r'lj-gc command initialized\n'<br>
            +        r'.*is successfully loaded'<br>
                 )<br>
             <br>
             <br>
            @@ -117,9 +146,9 @@<br>
                 location = 'lj_cf_print'<br>
                 lua_script = 'print(1)'<br>
                 pattern = (<br>
            -        'LJ_64: (True|False), '<br>
            -        'LJ_GC64: (True|False), '<br>
            -        'LJ_DUALNUM: (True|False)'<br>
            +        r'LJ_64: (True|False), '<br>
            +        r'LJ_GC64: (True|False), '<br>
            +        r'LJ_DUALNUM: (True|False)'<br>
                 )<br>
             <br>
             <br>
            @@ -128,9 +157,9 @@<br>
                 location = 'lj_cf_print'<br>
                 lua_script = 'print(1)'<br>
                 pattern = (<br>
            -        'VM state: [A-Z]+\n'<br>
            -        'GC state: [A-Z]+\n'<br>
            -        'JIT state: [A-Z]+\n'<br>
            +        r'VM state: [A-Z]+\n'<br>
            +        r'GC state: [A-Z]+\n'<br>
            +        r'JIT state: [A-Z]+\n'<br>
                 )<br>
             <br>
             <br>
            @@ -139,19 +168,19 @@<br>
                 location = 'lj_cf_print'<br>
                 lua_script = 'print(1)'<br>
                 pattern = (<br>
            -        'GC stats: [A-Z]+\n'<br>
            -        '\ttotal: \d+\n'<br>
            -        '\tthreshold: \d+\n'<br>
            -        '\tdebt: \d+\n'<br>
            -        '\testimate: \d+\n'<br>
            -        '\tstepmul: \d+\n'<br>
            -        '\tpause: \d+\n'<br>
            -        '\tsweepstr: \d+/\d+\n'<br>
            -        '\troot: \d+ objects\n'<br>
            -        '\tgray: \d+ objects\n'<br>
            -        '\tgrayagain: \d+ objects\n'<br>
            -        '\tweak: \d+ objects\n'<br>
            -        '\tmmudata: \d+ objects\n'<br>
            +        r'GC stats: [A-Z]+\n'<br>
            +        r'\ttotal: \d+\n'<br>
            +        r'\tthreshold: \d+\n'<br>
            +        r'\tdebt: \d+\n'<br>
            +        r'\testimate: \d+\n'<br>
            +        r'\tstepmul: \d+\n'<br>
            +        r'\tpause: \d+\n'<br>
            +        r'\tsweepstr: \d+/\d+\n'<br>
            +        r'\troot: \d+ objects\n'<br>
            +        r'\tgray: \d+ objects\n'<br>
            +        r'\tgrayagain: \d+ objects\n'<br>
            +        r'\tweak: \d+ objects\n'<br>
            +        r'\tmmudata: \d+ objects\n'<br>
                 )<br>
             <br>
             <br>
            @@ -160,17 +189,15 @@<br>
                 location = 'lj_cf_print'<br>
                 lua_script = 'print(1)'<br>
                 pattern = (<br>
            -        '-+ Red <a class="moz-txt-link-freetext" href="zone:\s+\d+">zone:\s+\d+</a> slots -+\n'<br>
            -        '(0x[a-zA-Z0-9]+\s+\[(S|\s)(B|\s)(T|\s)(M|\s)\]
            VALUE: nil\n?)*\n'<br>
            -        '-+ <a class="moz-txt-link-freetext" href="Stack:\s+\d+">Stack:\s+\d+</a> slots -+\n'<br>
            -        '(0x[A-Za-z0-9]+(:0x[A-Za-z0-9]+)?\s+'<br>
            -        '\[(S|\s)(B|\s)(T|\s)(M|\s)\].*\n?)+\n'<br>
            +        r'-+ Red <a class="moz-txt-link-freetext" href="zone:\s+\d+">zone:\s+\d+</a> slots -+\n'<br>
            +        r'(' + RX_ADDR + r'\s+' + RX_FRAME + r' VALUE:
            nil\n?)*\n'<br>
            +        r'-+ <a class="moz-txt-link-freetext" href="Stack:\s+\d+">Stack:\s+\d+</a> slots -+\n'<br>
            +        r'(' + RX_ADDR + r'(:' + RX_ADDR + r')?\s+' +
            RX_FRAME + r'.*\n?)+\n'<br>
                 )<br>
             <br>
             <br>
             class TestLJTV(TestCaseBase):<br>
                 location = 'lj_cf_print'<br>
            -    lua_script = 'print(1)'<br>
                 extension_cmds = (<br>
                     'lj-tv L->base\n'<br>
                     'lj-tv L->base + 1\n'<br>
            @@ -205,46 +232,55 @@<br>
                 )<br>
             <br>
                 pattern = (<br>
            -        'nil\n'<br>
            -        'false\n'<br>
            -        'true\n'<br>
            -        'string \"hello\" @ 0x[a-zA-Z0-9]+\n'<br>
            -        'table @ 0x[a-zA-Z0-9]+ \(asize: \d+, hmask:
            0x[a-zA-Z0-9]+\)\n'<br>
            -        '(number|integer) .*1.*\n'<br>
            -        'number 1.1\d+\n'<br>
            -        'thread @ 0x[a-zA-Z0-9]+\n'<br>
            -        'cdata @ 0x[a-zA-Z0-9]+\n'<br>
            -        'Lua function @ 0x[a-zA-Z0-9]+, [0-9]+ upvalues,
            .+:[0-9]+\n'<br>
            -        'fast function #[0-9]+\n'<br>
            -        'C function @ 0x[a-zA-Z0-9]+\n'<br>
            +        r'nil\n'<br>
            +        r'false\n'<br>
            +        r'true\n'<br>
            +        r'string \"hello\" @ ' + RX_ADDR + r'\n'<br>
            +        r'table @ ' + RX_ADDR + r' \(asize: \d+, hmask: ' +
            RX_HASH + r'\)\n'<br>
            +        r'(number|integer) .*1.*\n'<br>
            +        r'number 1.1\d+\n'<br>
            +        r'thread @ ' + RX_ADDR + r'\n'<br>
            +        r'cdata @ ' + RX_ADDR + r'\n'<br>
            +        r'Lua function @ ' + RX_ADDR + r', [0-9]+ upvalues,
            .+:[0-9]+\n'<br>
            +        r'fast function #[0-9]+\n'<br>
            +        r'C function @ ' + RX_ADDR + r'\n'<br>
                 )<br>
             <br>
             <br>
             class TestLJStr(TestCaseBase):<br>
            -    extension_cmds = 'lj-str fname'<br>
            +    extension_cmds = (<br>
            +        # XXX: Get the value to the stack slot for the
            variable.<br>
            +        'n\n'<br>
            +        'lj-str fname\n'<br>
            +    )<br>
                 location = 'lj_cf_dofile'<br>
                 lua_script = 'pcall(dofile("name"))'<br>
            -    pattern = 'String: .* \[\d+ bytes\] with hash
            0x[a-zA-Z0-9]+'<br>
            +    pattern = r'String: .* \[\d+ bytes\] with hash ' +
            RX_HASH<br>
             <br>
             <br>
             class TestLJTab(TestCaseBase):<br>
            -    extension_cmds = 'lj-tab t'<br>
            +    extension_cmds = (<br>
            +        # XXX: Get the value to the stack slot for the
            variable.<br>
            +        'n\n'<br>
            +        'lj-tab t\n'<br>
            +    )<br>
                 location = 'lj_cf_unpack'<br>
                 lua_script = 'unpack({1; a = 1})'<br>
                 pattern = (<br>
            -        'Array part: 3 slots\n'<br>
            -        '0x[a-zA-Z0-9]+: \[0\]: nil\n'<br>
            -        '0x[a-zA-Z0-9]+: \[1\]: .+ 1\n'<br>
            -        '0x[a-zA-Z0-9]+: \[2\]: nil\n'<br>
            -        'Hash part: 2 nodes\n'<br>
            -        '0x[a-zA-Z0-9]+: { string "a" @ 0x[a-zA-Z0-9]+ }
            => '<br>
            -        '{ .+ 1 }; next = 0x0\n'<br>
            -        '0x[a-zA-Z0-9]+: { nil } => { nil }; next =
            0x0\n'<br>
            +        r'Array part: 3 slots\n' +<br>
            +        RX_ADDR + r': \[0\]: nil\n' +<br>
            +        RX_ADDR + r': \[1\]: .+ 1\n' +<br>
            +        RX_ADDR + r': \[2\]: nil\n' +<br>
            +        r'Hash part: 2 nodes\n' +<br>
            +        RX_ADDR + r': { string "a" @ ' + RX_ADDR + r' }
            => ' +<br>
            +        r'{ .+ 1 }; next = 0x0\n' +<br>
            +        RX_ADDR + r': { nil } => { nil }; next = 0x0\n'<br>
                 )<br>
             <br>
             <br>
             for test_cls in TestCaseBase.__subclasses__():<br>
                 test_cls.test = lambda self: self.check()<br>
             <br>
            -if __name__ == '__main__':<br>
            +# FIXME: skip for LLDB since most commands are not working
            anyway.<br>
            +if __name__ == '__main__' and not LLDB:<br>
                 unittest.main(verbosity=2)<br>
            <br>
          </span></span></span><span class="ZSCsVd"></span></p>
    <div class="OvtS8d"></div>
    <p>[1]:
<a class="moz-txt-link-freetext" href="https://lists.tarantool.org/tarantool-patches/cover.1712182830.git.m.kokryashkin@tarantool.org/">https://lists.tarantool.org/tarantool-patches/cover.1712182830.git.m.kokryashkin@tarantool.org/</a></p>
    <blockquote type="cite"
      cite="mid:20260519123913.178775-2-skaplun@tarantool.org">
      <pre wrap="" class="moz-quote-pre">This patch adds tests for LuaJIT debugging extensions for lldb and gdb.
The tests are written in Python's unittest framework [1].

Most of the tests are failed for the lldb due to outdated extension
sources and overcomplicated hard-coded C structures fields
introspection. Hence, tests for LLDB are disabled since they are failing
anyway.

The tarantool-debugger-tests target is introduced. This target is
included in the LuaJIT-check-all target but not in the LuaJIT-test
target to avoid it running for all LuaJIT builds by default in CI.</pre>
    </blockquote>
    <p>I don't get why we cannot run debugger tests together with other
      tests.</p>
    <p>dbg extension must work with all LuaJIT configurations that we
      test in CI,</p>
    <p>so I suppose we should run these (dbg) tests with all other
      regression tests.</p>
    <p>For example, the proposed GHA workflows doesn't cover DUALNUM
      build,</p>
    <p><span class="HwtZe" lang="en"><span class="jCAhz ChMk0b"><span
            class="ryNqvb">сan I be sure that the extension will work
            with DUALNUM build? Seems no.</span></span></span></p>
    <blockquote type="cite"
      cite="mid:20260519123913.178775-2-skaplun@tarantool.org">
      <pre wrap="" class="moz-quote-pre">

[1]: <a class="moz-txt-link-freetext" href="https://docs.python.org/3/library/unittest.html">https://docs.python.org/3/library/unittest.html</a>
---
 test/CMakeLists.txt                           |   7 +
 test/tarantool-debugger-tests/CMakeLists.txt  |  93 ++++++
 .../debug-extension-tests.py                  | 286 ++++++++++++++++++
 3 files changed, 386 insertions(+)
 create mode 100644 test/tarantool-debugger-tests/CMakeLists.txt
 create mode 100644 test/tarantool-debugger-tests/debug-extension-tests.py

diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index f48afa25..26b15892 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -175,6 +175,7 @@ add_subdirectory(LuaJIT-tests)
 add_subdirectory(PUC-Rio-Lua-5.1-tests)
 add_subdirectory(lua-Harness-tests)
 add_subdirectory(tarantool-c-tests)
+add_subdirectory(tarantool-debugger-tests)
 add_subdirectory(tarantool-tests)
 
 # Each testsuite has its own CMake target, but combining these
@@ -186,6 +187,9 @@ add_subdirectory(tarantool-tests)
 # command that runs all generated CMake tests.
 add_custom_target(${PROJECT_NAME}-test
   COMMAND ${CMAKE_CTEST_COMMAND} ${CTEST_FLAGS}
+    # Omit this target in LuaJIT-test since we don't want to set
+    # up and run debuggers for every build.
+    --label-exclude tarantool-debugger-tests</pre>
    </blockquote>
    see the comment above
    <blockquote type="cite"
      cite="mid:20260519123913.178775-2-skaplun@tarantool.org">
      <pre wrap="" class="moz-quote-pre">
   DEPENDS tarantool-c-tests-deps
           tarantool-tests-deps
           lua-Harness-tests-deps
@@ -195,5 +199,8 @@ add_custom_target(${PROJECT_NAME}-test
 
 add_custom_target(${PROJECT_NAME}-check-all
   DEPENDS ${PROJECT_NAME}-test
+          # Omit this target in LuaJIT-test since we don't want to
+          # set up and run debuggers for every build.
+          tarantool-debugger-tests
           ${PROJECT_NAME}-lint
 )
diff --git a/test/tarantool-debugger-tests/CMakeLists.txt b/test/tarantool-debugger-tests/CMakeLists.txt
new file mode 100644
index 00000000..7fd0debc
--- /dev/null
+++ b/test/tarantool-debugger-tests/CMakeLists.txt
@@ -0,0 +1,93 @@
+set(TEST_SUITE_NAME "tarantool-debugger-tests")
+
+# XXX: The call produces both test and target
+# <tarantool-debugger-tests-deps> as a side effect.
+add_test_suite_target(tarantool-debugger-tests
+  LABELS ${TEST_SUITE_NAME}
+  DEPENDS ${LUAJIT_TEST_BINARY}
+)
+
+# Debug info is required for testing of extensions.
+if(NOT (CMAKE_BUILD_TYPE MATCHES Debug))
+  message(WARNING
+    "Not a DEBUG build, tarantool-debugger-tests is dummy"
+  )</pre>
    </blockquote>
    <p>it is not dummy, it doesn't exist at all:</p>
    <p>cmake -S . -B build -DCMAKE_BUILD_TYPE=Release</p>
    <p>cd build</p>
    <p>make tarantool-debugger-tests<br>
      make[3]: *** No rule to make target 'src/luajit', needed by
'test/tarantool-debugger-tests/CMakeFiles/tarantool-debugger-tests-deps'. 
      Stop.<br>
      <br>
    </p>
    <blockquote type="cite"
      cite="mid:20260519123913.178775-2-skaplun@tarantool.org">
      <pre wrap="" class="moz-quote-pre">
+  return()
+endif()
+
+# MacOS asks for permission to debug a process even when the
+# machine is set into development mode. To solve the issue,
+# it is required to add relevant users to the `_developer` user
+# group in macOS. Disabled for now.
+if(CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND DEFINED ENV{CI})
+  message(WARNING
+    "Interactive debugging is unavailable for macOS CI builds,"
+    " tarantool-debugger-tests is dummy"
+  )</pre>
    </blockquote>
    the same as above, it is not dummy
    <blockquote type="cite"
      cite="mid:20260519123913.178775-2-skaplun@tarantool.org">
      <pre wrap="" class="moz-quote-pre">
+  return()
+endif()
+
+if(CMAKE_VERSION VERSION_LESS "3.12")
+  # <a class="moz-txt-link-freetext" href="TODO:Can">TODO:Can</a> remove this after upgrading to CMake >= 3.12.</pre>
    </blockquote>
    s/<a class="moz-txt-link-freetext" href="TODO:/TODO/">TODO:/TODO/</a>
    <blockquote type="cite"
      cite="mid:20260519123913.178775-2-skaplun@tarantool.org">
      <pre wrap="" class="moz-quote-pre">
+  find_package(PythonInterp)
+  if(NOT PYTHONINTERP_FOUND)
+    message(WARNING "`python` is not found, tarantool-debugger-tests is dummy")</pre>
    </blockquote>
    it is not dummy
    <blockquote type="cite"
      cite="mid:20260519123913.178775-2-skaplun@tarantool.org">
      <pre wrap="" class="moz-quote-pre">
+    return()
+  endif()
+else()
+  find_package(Python COMPONENTS Interpreter)
+  if(NOT PYTHON_FOUND)
+    message(WARNING "`python` is not found, tarantool-debugger-tests is dummy")</pre>
    </blockquote>
    it is not dummy
    <blockquote type="cite"
      cite="mid:20260519123913.178775-2-skaplun@tarantool.org">
      <pre wrap="" class="moz-quote-pre">
+    return()
+  endif()
+  set(PYTHON_EXECUTABLE "${Python_EXECUTABLE}")
+endif()
+
+set(DEBUGGER_TEST_ENV
+  "LUAJIT_TEST_BINARY=${LUAJIT_TEST_BINARY}"
+  # Suppresses __pycache__ generation.
+  "PYTHONDONTWRITEBYTECODE=1"
+  "DEBUGGER_EXTENSION_PATH=${PROJECT_SOURCE_DIR}/src"
+)
+
+set(TEST_SCRIPT_PATH
+  ${CMAKE_CURRENT_SOURCE_DIR}/debug-extension-tests.py
+)
+
+find_program(GDB gdb)
+if(GDB)
+  set(test_title "test/${TEST_SUITE_NAME}/gdb")
+  set(GDB_TEST_ENV ${DEBUGGER_TEST_ENV} "DEBUGGER_COMMAND=${GDB}")
+  add_test(NAME "${test_title}"
+    COMMAND ${PYTHON_EXECUTABLE} ${TEST_SCRIPT_PATH}
+    WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+  )
+  set_tests_properties("${test_title}" PROPERTIES
+    ENVIRONMENT "${GDB_TEST_ENV}"
+    LABELS ${TEST_SUITE_NAME}
+    DEPENDS tarantool-debugger-tests-deps
+  )
+else()
+  message(WARNING
+    "`gdb' is not found, so tarantool-debugger-tests/gdb is omitted"
+  )
+endif()
+
+find_program(LLDB lldb)
+if(LLDB)
+  set(test_title "test/${TEST_SUITE_NAME}/lldb")
+  set(LLDB_TEST_ENV ${DEBUGGER_TEST_ENV} "DEBUGGER_COMMAND=${LLDB}")
+  add_test(NAME "${test_title}"
+    COMMAND ${PYTHON_EXECUTABLE} ${TEST_SCRIPT_PATH}
+    WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+  )
+  set_tests_properties("${test_title}" PROPERTIES
+    ENVIRONMENT "${LLDB_TEST_ENV}"
+    LABELS ${TEST_SUITE_NAME}
+    DEPENDS tarantool-debugger-tests-deps
+  )
+else()
+  message(WARNING
+    "`lldb' is not found, so tarantool-debugger-tests/lldb is omitted"
+  )
+endif()</pre>
    </blockquote>
    Duplicate code for GDB and LLDB. May be use a macro for this?
    <blockquote type="cite"
      cite="mid:20260519123913.178775-2-skaplun@tarantool.org">
      <pre wrap="" class="moz-quote-pre">
diff --git a/test/tarantool-debugger-tests/debug-extension-tests.py b/test/tarantool-debugger-tests/debug-extension-tests.py
new file mode 100644
index 00000000..6094c535
--- /dev/null
+++ b/test/tarantool-debugger-tests/debug-extension-tests.py
@@ -0,0 +1,286 @@
+# This file provides tests for LuaJIT debug extensions for lldb
+# and gdb.
+
+import os
+import re
+import subprocess
+import sys
+import tempfile
+import unittest
+
+from threading import Timer
+
+LEGACY = re.match(r'^2\.', sys.version)
+
+LUAJIT_BINARY = os.environ['LUAJIT_TEST_BINARY']
+EXTENSION_PATH = os.environ['DEBUGGER_EXTENSION_PATH']
+DEBUGGER = os.environ['DEBUGGER_COMMAND']
+LLDB = 'lldb' in DEBUGGER
+EXTENSION = EXTENSION_PATH + ('/luajit_lldb.py' if LLDB else '/luajit-gdb.py')
+TIMEOUT = 10
+
+# Don't run any initialization scripts.
+RUN_CMD_FILE = []
+
+if LLDB:
+    RUN_CMD_FILE = [
+        '--batch',
+        '--no-lldbinit',
+        '--no-use-colors',
+        '--source-quietly',
+        '--source'
+    ]
+else:
+    # GDB.
+    RUN_CMD_FILE = ['--batch', '--nx', '--quiet', '--command']
+
+INFERIOR_ARGS = '--' if LLDB else '--args'
+PROCESS_RUN = 'process launch' if LLDB else 'r'
+LOAD_EXTENSION = (
+    'command script import {ext}' if LLDB else 'source {ext}'
+).format(ext=EXTENSION)
+
+
+RX_ADDR = r'0x[a-f0-9]+'
+RX_HASH = RX_ADDR  # The same pattern for hexademic values.
+RX_FRAME = r'\[(S|\s)(B|\s)(T|\s)(M|\s)\]'
+
+
+def persist(data):
+    tmp = tempfile.NamedTemporaryFile(mode='w')
+    tmp.write(data)
+    tmp.flush()
+    return tmp
+
+
+def execute_process(cmd, timeout=TIMEOUT):
+    if LEGACY:
+        # XXX: The Python 2.7 version of `subprocess.Popen`
+        # doesn't have a timeout option, so the required
+        # functionality was implemented via `threading.Timer`.
+        process = subprocess.Popen(
+            cmd,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+            # This prevents sending of SIGSTTOU to the test when
+            # running by `make'. Stdin is unused anyway.
+            stdin=subprocess.DEVNULL
+        )
+        timer = Timer(TIMEOUT, process.kill)
+        timer.start()
+        stdout, _ = process.communicate()
+        timer.cancel()
+
+        # XXX: If the timeout is exceeded and the process is
+        # killed by the timer, then the return code is non-zero,
+        # and we are going to blow up.
+        assert process.returncode == 0
+        return stdout.decode('ascii')
+    else:
+        process = subprocess.run(
+            cmd,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+            # This prevents sending of SIGSTTOU to the test when
+            # running by `make'. Stdin is unused anyway.
+            stdin=subprocess.DEVNULL,
+            universal_newlines=True,
+            timeout=TIMEOUT
+        )
+        return process.stdout
+
+
+class TestCaseBase(unittest.TestCase):
+    @classmethod
+    def construct_cmds(cls):
+        return '\n'.join([
+            'b {loc}'.format(loc=cls.location),
+            PROCESS_RUN,
+            'n',
+            LOAD_EXTENSION,
+            cls.extension_cmds.strip(),
+            'q',
+        ])
+
+    @classmethod
+    def setUpClass(cls):
+        cmd_file = persist(cls.construct_cmds())
+        script_file = persist(cls.lua_script)
+        process_cmd = [
+            DEBUGGER,
+            *RUN_CMD_FILE,
+            cmd_file.name,
+            INFERIOR_ARGS,
+            LUAJIT_BINARY,
+            script_file.name,
+        ]
+        cls.output = execute_process(process_cmd)
+        cmd_file.close()
+        script_file.close()
+
+    def check(self):
+        if LEGACY:
+            self.assertRegexpMatches(self.output, self.pattern.strip())
+        else:
+            self.assertRegex(self.output, self.pattern.strip())
+
+
+class TestLoad(TestCaseBase):
+    extension_cmds = ''
+    location = 'lj_cf_print'
+    lua_script = 'print(1)'
+    pattern = (
+        r'lj-arch command initialized\n'
+        r'lj-tv command initialized\n'
+        r'lj-str command initialized\n'
+        r'lj-tab command initialized\n'
+        r'lj-stack command initialized\n'
+        r'lj-state command initialized\n'
+        r'lj-gc command initialized\n'
+        r'.*is successfully loaded'
+    )
+
+
+class TestLJArch(TestCaseBase):
+    extension_cmds = 'lj-arch'
+    location = 'lj_cf_print'
+    lua_script = 'print(1)'
+    pattern = (
+        r'LJ_64: (True|False), '
+        r'LJ_GC64: (True|False), '
+        r'LJ_DUALNUM: (True|False)'
+    )
+
+
+class TestLJState(TestCaseBase):
+    extension_cmds = 'lj-state'
+    location = 'lj_cf_print'
+    lua_script = 'print(1)'
+    pattern = (
+        r'VM state: [A-Z]+\n'
+        r'GC state: [A-Z]+\n'
+        r'JIT state: [A-Z]+\n'
+    )
+
+
+class TestLJGC(TestCaseBase):
+    extension_cmds = 'lj-gc'
+    location = 'lj_cf_print'
+    lua_script = 'print(1)'
+    pattern = (
+        r'GC stats: [A-Z]+\n'
+        r'\ttotal: \d+\n'
+        r'\tthreshold: \d+\n'
+        r'\tdebt: \d+\n'
+        r'\testimate: \d+\n'
+        r'\tstepmul: \d+\n'
+        r'\tpause: \d+\n'
+        r'\tsweepstr: \d+/\d+\n'
+        r'\troot: \d+ objects\n'
+        r'\tgray: \d+ objects\n'
+        r'\tgrayagain: \d+ objects\n'
+        r'\tweak: \d+ objects\n'
+        r'\tmmudata: \d+ objects\n'
+    )
+
+
+class TestLJStack(TestCaseBase):
+    extension_cmds = 'lj-stack'
+    location = 'lj_cf_print'
+    lua_script = 'print(1)'
+    pattern = (
+        r'-+ Red <a class="moz-txt-link-freetext" href="zone:\s+\d+">zone:\s+\d+</a> slots -+\n'
+        r'(' + RX_ADDR + r'\s+' + RX_FRAME + r' VALUE: nil\n?)*\n'
+        r'-+ <a class="moz-txt-link-freetext" href="Stack:\s+\d+">Stack:\s+\d+</a> slots -+\n'
+        r'(' + RX_ADDR + r'(:' + RX_ADDR + r')?\s+' + RX_FRAME + r'.*\n?)+\n'
+    )
+
+
+class TestLJTV(TestCaseBase):
+    location = 'lj_cf_print'
+    extension_cmds = (
+        'lj-tv L->base\n'
+        'lj-tv L->base + 1\n'
+        'lj-tv L->base + 2\n'
+        'lj-tv L->base + 3\n'
+        'lj-tv L->base + 4\n'
+        'lj-tv L->base + 5\n'
+        'lj-tv L->base + 6\n'
+        'lj-tv L->base + 7\n'
+        'lj-tv L->base + 8\n'
+        'lj-tv L->base + 9\n'
+        'lj-tv L->base + 10\n'
+        'lj-tv L->base + 11\n'
+    )
+
+    lua_script = (
+        'local ffi = require("ffi")\n'
+        'print(\n'
+        '  nil,\n'
+        '  false,\n'
+        '  true,\n'
+        '  "hello",\n'
+        '  {1},\n'
+        '  1,\n'
+        '  1.1,\n'
+        '  coroutine.create(function() end),\n'
+        '  ffi.new("int*"),\n'
+        '  function() end,\n'
+        '  print,\n'
+        '  require\n'
+        ')\n'
+    )
+
+    pattern = (
+        r'nil\n'
+        r'false\n'
+        r'true\n'
+        r'string \"hello\" @ ' + RX_ADDR + r'\n'
+        r'table @ ' + RX_ADDR + r' \(asize: \d+, hmask: ' + RX_HASH + r'\)\n'
+        r'(number|integer) .*1.*\n'
+        r'number 1.1\d+\n'
+        r'thread @ ' + RX_ADDR + r'\n'
+        r'cdata @ ' + RX_ADDR + r'\n'
+        r'Lua function @ ' + RX_ADDR + r', [0-9]+ upvalues, .+:[0-9]+\n'
+        r'fast function #[0-9]+\n'
+        r'C function @ ' + RX_ADDR + r'\n'
+    )
+
+
+class TestLJStr(TestCaseBase):
+    extension_cmds = (
+        # XXX: Get the value to the stack slot for the variable.
+        'n\n'
+        'lj-str fname\n'
+    )
+    location = 'lj_cf_dofile'
+    lua_script = 'pcall(dofile("name"))'
+    pattern = r'String: .* \[\d+ bytes\] with hash ' + RX_HASH
+
+
+class TestLJTab(TestCaseBase):
+    extension_cmds = (
+        # XXX: Get the value to the stack slot for the variable.
+        'n\n'
+        'lj-tab t\n'
+    )
+    location = 'lj_cf_unpack'
+    lua_script = 'unpack({1; a = 1})'
+    pattern = (
+        r'Array part: 3 slots\n' +
+        RX_ADDR + r': \[0\]: nil\n' +
+        RX_ADDR + r': \[1\]: .+ 1\n' +
+        RX_ADDR + r': \[2\]: nil\n' +
+        r'Hash part: 2 nodes\n' +
+        RX_ADDR + r': { string "a" @ ' + RX_ADDR + r' } => ' +
+        r'{ .+ 1 }; next = 0x0\n' +
+        RX_ADDR + r': { nil } => { nil }; next = 0x0\n'
+    )
+
+
+for test_cls in TestCaseBase.__subclasses__():
+    test_cls.test = lambda self: self.check()
+
+# FIXME: skip for LLDB since most commands are not working anyway.
+if __name__ == '__main__' and not LLDB:
+    unittest.main(verbosity=2)
</pre>
    </blockquote>
  </body>
  <lt-container></lt-container>
</html>