[Tarantool-patches] [PATCH luajit v2 2/2] test: add test for debugging extension

Maksim Kokryashkin max.kokryashkin at gmail.com
Thu Oct 12 13:25:36 MSK 2023


This patch introduces Test::Base-like tests for the
LuaJIT debugging extension. The newly introduced test
suite is TAP-compatible and is tested with prove.
Test specification is generalized to a great extent,
however, it is still important to keep in mind the
platform-specific aspects of assertions.
---
 test/CMakeLists.txt                           |   3 +
 test/tarantool-debugger-tests/CMakeLists.txt  |  82 ++++++++++
 test/tarantool-debugger-tests/config.py       | 148 ++++++++++++++++++
 .../luajit_dbg.test.md                        | 136 ++++++++++++++++
 test/tarantool-debugger-tests/run.py          |   8 +
 test/tarantool-debugger-tests/test_base.py    |  73 +++++++++
 6 files changed, 450 insertions(+)
 create mode 100644 test/tarantool-debugger-tests/CMakeLists.txt
 create mode 100644 test/tarantool-debugger-tests/config.py
 create mode 100644 test/tarantool-debugger-tests/luajit_dbg.test.md
 create mode 100755 test/tarantool-debugger-tests/run.py
 create mode 100644 test/tarantool-debugger-tests/test_base.py

diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 58cba5ba..87ee40b3 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -78,6 +78,7 @@ add_subdirectory(PUC-Rio-Lua-5.1-tests)
 add_subdirectory(lua-Harness-tests)
 add_subdirectory(tarantool-c-tests)
 add_subdirectory(tarantool-tests)
+add_subdirectory(tarantool-debugger-tests)
 
 add_custom_target(${PROJECT_NAME}-test DEPENDS
   LuaJIT-tests
@@ -85,6 +86,8 @@ add_custom_target(${PROJECT_NAME}-test DEPENDS
   lua-Harness-tests
   tarantool-c-tests
   tarantool-tests
+  tarantool-gdb-tests
+  tarantool-lldb-tests
 )
 
 if(LUAJIT_USE_TEST)
diff --git a/test/tarantool-debugger-tests/CMakeLists.txt b/test/tarantool-debugger-tests/CMakeLists.txt
new file mode 100644
index 00000000..ffe8ff39
--- /dev/null
+++ b/test/tarantool-debugger-tests/CMakeLists.txt
@@ -0,0 +1,82 @@
+add_custom_target(tarantool-gdb-tests
+  DEPENDS ${LUAJIT_TEST_BINARY}
+)
+
+add_custom_target(tarantool-lldb-tests
+  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-*db-tests are dummy")
+  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 "non-interactive debugging on macOS, tarantool-*db-tests are dummy")
+  return()
+endif()
+
+find_program(PROVE prove)
+if(NOT PROVE)
+  message(WARNING "`prove' is not found, so tarantool-*db-tests target are dummy")
+  return()
+endif()
+
+find_package(PythonInterp)
+if(NOT PYTHONINTERP_FOUND)
+  message(WARNING "`python' is not found, so tarantool-*db-tests target are dummy")
+  return()
+endif()
+
+set(DEBUGGER_TEST_FLAGS --failures)
+if(CMAKE_VERBOSE_MAKEFILE)
+  list(APPEND DEBUGGER_TEST_FLAGS --verbose)
+endif()
+
+set(DEBUGGER_TEST_ENV
+  "LUAJIT_TEST_BINARY=${LUAJIT_TEST_BINARY}"
+  # Suppresses __pycache__ generation.
+  "PYTHONDONTWRITEBYTECODE=1"
+  "DEBUGGER_EXTENSION_PATH=${PROJECT_SOURCE_DIR}/src/luajit_dbg.py"
+)
+
+find_program(GDB gdb)
+if(GDB)
+  set(GDB_TEST_ENV ${DEBUGGER_TEST_ENV}
+    "DEBUGGER_COMMAND=${GDB}"
+  )
+  add_custom_command(TARGET tarantool-gdb-tests
+    COMMENT "Running luajit_dbg.py tests with gdb"
+    COMMAND
+    ${GDB_TEST_ENV}
+      ${PROVE} ${CMAKE_CURRENT_SOURCE_DIR}/luajit_dbg.test.md
+      --exec '${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/run.py'
+      ${DEBUGGER_TEST_FLAGS}
+    WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+  )
+else()
+  message(WARNING "`gdb' is not found, so tarantool-gdb-tests target is dummy")
+endif()
+
+find_program(LLDB lldb)
+if(LLDB)
+  set(LLDB_TEST_ENV ${DEBUGGER_TEST_ENV}
+    "DEBUGGER_COMMAND=${LLDB}"
+  )
+  add_custom_command(TARGET tarantool-lldb-tests
+    COMMENT "Running luajit_dbg.py tests with lldb"
+    COMMAND
+    ${LLDB_TEST_ENV}
+      ${PROVE} ${CMAKE_CURRENT_SOURCE_DIR}/luajit_dbg.test.md
+      --exec '${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/run.py'
+      ${DEBUGGER_TEST_FLAGS}
+    WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+  )
+else()
+  message(WARNING "`lldb' is not found, so tarantool-lldb-tests target is dummy")
+endif()
diff --git a/test/tarantool-debugger-tests/config.py b/test/tarantool-debugger-tests/config.py
new file mode 100644
index 00000000..a75d1452
--- /dev/null
+++ b/test/tarantool-debugger-tests/config.py
@@ -0,0 +1,148 @@
+# This file provides filters and logic for running the debugger tests. It is
+# imported by the runner and the test_base.py, so its symbols become exposed
+# to the globals(). Thus, they can be called during the specification
+# execution.
+import re
+import subprocess
+import os
+import sys
+import tempfile
+from threading import Timer
+
+LEGACY = re.match(r'^2\.', sys.version)
+
+LUAJIT_BINARY = os.environ['LUAJIT_TEST_BINARY']
+EXTENSION = os.environ['DEBUGGER_EXTENSION_PATH']
+DEBUGGER_COMMAND = os.environ['DEBUGGER_COMMAND']
+LLDB = 'lldb' in DEBUGGER_COMMAND
+TIMEOUT = 10
+
+active_block = None
+output = ''
+dbg_cmds = ''
+
+
+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)
+        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, capture_output=True, timeout=TIMEOUT)
+        return process.stdout.decode('ascii')
+
+
+def load_extension_cmd():
+    load_cmd = 'command script import {ext}' if LLDB else 'source {ext}'
+    return load_cmd.format(ext=EXTENSION)
+
+
+def filter_debugger_output(output):
+    descriptor = '(lldb)' if LLDB else '(gdb)'
+    return ''.join(
+        filter(
+            lambda line: not line.startswith(descriptor),
+            output.splitlines(True),
+        )
+    )
+
+
+def lua(data):
+    global output
+
+    exec_file_flag = '-s' if LLDB else '-x'
+    inferior_args_flag = '--' if LLDB else '--args'
+
+    tmp_cmds = persist(dbg_cmds)
+    lua_script = persist(data)
+
+    process_cmd = [
+        DEBUGGER_COMMAND,
+        exec_file_flag,
+        tmp_cmds.name,
+        inferior_args_flag,
+        LUAJIT_BINARY,
+        lua_script.name,
+    ]
+
+    output = execute_process(process_cmd)
+    output = filter_debugger_output(output)
+
+    tmp_cmds.close()
+    lua_script.close()
+
+
+def run_until_breakpoint(location):
+    return [
+        'b {loc}'.format(loc=location),
+        'process launch' if LLDB else 'r',
+        'n',
+    ]
+
+
+def lj_cf_print(data):
+    return run_until_breakpoint('lj_cf_print'), data
+
+
+def lj_cf_dofile(data):
+    return run_until_breakpoint('lj_cf_dofile'), data
+
+
+def lj_cf_unpack(data):
+    return run_until_breakpoint('lj_cf_unpack'), data
+
+
+def debug(data):
+    global dbg_cmds
+    setup_cmds, extension_cmds = data
+    setup_cmds.append(load_extension_cmd())
+
+    if extension_cmds:
+        setup_cmds.append(extension_cmds)
+
+    setup_cmds.append('q')
+    dbg_cmds = '\n'.join(setup_cmds)
+
+
+def test_ok(result):
+    status = 'ok' if result else 'not ok'
+    print(
+        '{status} - {test_name}'.format(
+            status=status,
+            test_name=active_block.name,
+        )
+    )
+
+
+def expected(data):
+    test_ok(data in output)
+
+
+def matches(data):
+    test_ok(re.search(data, output))
+
+
+def runner(blocks):
+    print('1..{n}'.format(n=len(blocks)))
+    global active_block
+    for block in blocks:
+        active_block = block
+        for section in block.sections:
+            globals()[section.name](section.pipeline(section.data.strip()))
diff --git a/test/tarantool-debugger-tests/luajit_dbg.test.md b/test/tarantool-debugger-tests/luajit_dbg.test.md
new file mode 100644
index 00000000..d2003328
--- /dev/null
+++ b/test/tarantool-debugger-tests/luajit_dbg.test.md
@@ -0,0 +1,136 @@
+## smoke
+### debug lj_cf_print
+### lua
+print(1)
+### expected
+lj-tv command intialized
+lj-state command intialized
+lj-arch command intialized
+lj-gc command intialized
+lj-str command intialized
+lj-tab command intialized
+lj-stack command intialized
+LuaJIT debug extension is successfully loaded
+
+
+## lj-arch
+### debug lj_cf_print
+lj-arch
+### lua
+print(1)
+### matches
+LJ_64: (True|False), LJ_GC64: (True|False), LJ_DUALNUM: (True|False)
+
+
+## lj-state
+### debug lj_cf_print
+lj-state
+### lua
+print(1)
+### matches
+VM state: [A-Z]+
+GC state: [A-Z]+
+JIT state: [A-Z]+
+
+
+## lj-gc
+### debug lj_cf_print
+lj-gc
+### lua
+print(1)
+### matches
+GC stats: [A-Z]+
+\ttotal: \d+
+\tthreshold: \d+
+\tdebt: \d+
+\testimate: \d+
+\tstepmul: \d+
+\tpause: \d+
+\tsweepstr: \d+/\d+
+\troot: \d+ objects
+\tgray: \d+ objects
+\tgrayagain: \d+ objects
+\tweak: \d+ objects
+\tmmudata: \d+ objects
+
+
+## lj-stack
+### debug lj_cf_print
+lj-stack
+### lua
+print(1)
+### matches
+-+ Red zone:\s+\d+ slots -+
+(0x[a-zA-Z0-9]+\s+\[(S|\s)(B|\s)(T|\s)(M|\s)\] VALUE: nil\n?)*
+-+ Stack:\s+\d+ slots -+
+(0x[A-Za-z0-9]+(:0x[A-Za-z0-9]+)?\s+\[(S|\s)(B|\s)(T|\s)(M|\s)\].*\n?)+
+
+
+## lj-tv
+### debug lj_cf_print
+lj-tv L->base
+lj-tv L->base + 1
+lj-tv L->base + 2
+lj-tv L->base + 3
+lj-tv L->base + 4
+lj-tv L->base + 5
+lj-tv L->base + 6
+lj-tv L->base + 7
+lj-tv L->base + 8
+lj-tv L->base + 9
+lj-tv L->base + 10
+lj-tv L->base + 11
+### lua
+local ffi = require('ffi')
+
+print(
+  nil,
+  false,
+  true,
+  "hello",
+  {1},
+  1,
+  1.1,
+  coroutine.create(function() end),
+  ffi.new('int*'),
+  function() end,
+  print,
+  require
+) 
+### matches
+nil
+false
+true
+string \"hello\" @ 0x[a-zA-Z0-9]+
+table @ 0x[a-zA-Z0-9]+ \(asize: \d+, hmask: 0x[a-zA-Z0-9]+\)
+(number|integer) .*1.*
+number 1.1\d+
+thread @ 0x[a-zA-Z0-9]+
+cdata @ 0x[a-zA-Z0-9]+
+Lua function @ 0x[a-zA-Z0-9]+, [0-9]+ upvalues, .+:[0-9]+
+fast function #[0-9]+
+C function @ 0x[a-zA-Z0-9]+
+
+
+## lj-str
+### debug lj_cf_dofile
+lj-str fname
+### lua
+pcall(dofile('name'))
+### matches
+String: .* \[\d+ bytes\] with hash 0x[a-zA-Z0-9]+
+
+
+## lj-tab
+### debug lj_cf_unpack
+lj-tab t
+### lua
+unpack({1; a = 1})
+### matches
+Array part: 3 slots
+0x[a-zA-Z0-9]+: \[0\]: nil
+0x[a-zA-Z0-9]+: \[1\]: .+ 1
+0x[a-zA-Z0-9]+: \[2\]: nil
+Hash part: 2 nodes
+0x[a-zA-Z0-9]+: { string "a" @ 0x[a-zA-Z0-9]+ } => { .+ 1 }; next = 0x0
+0x[a-zA-Z0-9]+: { nil } => { nil }; next = 0x0
diff --git a/test/tarantool-debugger-tests/run.py b/test/tarantool-debugger-tests/run.py
new file mode 100755
index 00000000..cc84940d
--- /dev/null
+++ b/test/tarantool-debugger-tests/run.py
@@ -0,0 +1,8 @@
+# Runner script for compatibility with `prove`.
+import sys
+
+from config import runner
+from test_base import Spec
+
+with open(sys.argv[1], 'r') as stream:
+    runner(Spec(stream.read()).blocks)
diff --git a/test/tarantool-debugger-tests/test_base.py b/test/tarantool-debugger-tests/test_base.py
new file mode 100644
index 00000000..9d7931bd
--- /dev/null
+++ b/test/tarantool-debugger-tests/test_base.py
@@ -0,0 +1,73 @@
+# This file provides a pythonic implementation of similar to Perl's Test::Base
+# module functionality for declarative testing.
+# See https://metacpan.org/pod/Test::Base#Rolling-Your-Own-Filters
+from config import *  # noqa: F401,F403
+
+
+class Pipeline(object):
+    def __init__(self, funcs):
+        self.funcs = funcs
+
+    def __call__(self, data):
+        for func in self.funcs:
+            data = func(data)
+        return data
+
+
+class Section(object):
+    def __init__(self, name, pipeline):
+        self.name = name
+        self.data = ''
+        self.pipeline = pipeline
+
+
+class Block(object):
+    def __init__(self, name):
+        self.name = name
+        self.description = ''
+        self.sections = []
+
+
+class Spec(object):
+    def __init__(
+        self,
+        spec,
+        block_descriptor='## ',
+        section_descriptor='### ',
+    ):
+        self.blocks = []
+        self.block_descriptor = block_descriptor
+        self.section_descriptor = section_descriptor
+        self.parse_spec(spec)
+
+    def _is_block_start(self, line):
+        return line.startswith(self.block_descriptor)
+
+    def _is_section_start(self, line):
+        return line.startswith(self.section_descriptor)
+
+    def _is_description(self, line):
+        return not self.blocks[-1].sections
+
+    def parse_spec(
+        self,
+        spec,
+    ):
+        spec = spec.strip().splitlines(True)
+
+        for line in spec:
+            if self._is_block_start(line):
+                name = line.lstrip(self.block_descriptor).strip()
+                self.blocks.append(Block(name))
+
+            elif self._is_section_start(line):
+                meta = line.lstrip(self.section_descriptor).strip().split()
+                name = meta[0]
+                pipeline = Pipeline([globals()[fname] for fname in meta[1:]])
+                self.blocks[-1].sections.append(Section(name, pipeline))
+
+            elif self._is_description(line):
+                self.blocks[-1].description += line
+
+            else:
+                self.blocks[-1].sections[-1].data += line
-- 
2.39.3 (Apple Git-145)



More information about the Tarantool-patches mailing list