From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from [87.239.111.99] (localhost [127.0.0.1]) by dev.tarantool.org (Postfix) with ESMTP id AE48A656C10; Thu, 12 Oct 2023 13:26:43 +0300 (MSK) DKIM-Filter: OpenDKIM Filter v2.11.0 dev.tarantool.org AE48A656C10 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=tarantool.org; s=dev; t=1697106403; bh=F2ITzNkDVaPLiIRSGuy08ZD12ghQVT8lUVCPUpzy8kc=; h=To:Cc:Date:In-Reply-To:References:Subject:List-Id: List-Unsubscribe:List-Archive:List-Post:List-Help:List-Subscribe: From:Reply-To:From; b=ODZxWoNoCz5NmKHjO/xveins7IzTIW37OwJ5VqemKRzPVRPVFZuahZknm1hjSDU9I dtmBm1BDLmq31GBUeyOOVKI4JhGh4tvVsXttU/F/Otw5Cnvf0DCSLLsyR6uqPJx/80 ek5qF6rnUsXXVtu2OaGHv6Bm02zxPIY3ZiyyRDW4= Received: from mail-lf1-f47.google.com (mail-lf1-f47.google.com [209.85.167.47]) (using TLSv1.3 with cipher TLS_AES_128_GCM_SHA256 (128/128 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by dev.tarantool.org (Postfix) with ESMTPS id 9DBD1656C10 for ; Thu, 12 Oct 2023 13:25:45 +0300 (MSK) DKIM-Filter: OpenDKIM Filter v2.11.0 dev.tarantool.org 9DBD1656C10 Received: by mail-lf1-f47.google.com with SMTP id 2adb3069b0e04-5041cc983f9so1061486e87.3 for ; Thu, 12 Oct 2023 03:25:45 -0700 (PDT) X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1697106345; x=1697711145; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc :subject:date:message-id:reply-to; bh=Oo3h0SpJNRWtb0Nq2ekfgXnt19Oex6oHRg0joIHwhPE=; b=NqDjizqeaz+zO12AaTi1kR2QdANwaPMm9Xbx+oWk28PUNP4RE0FNX5luAQdx/71BWm DD/YPk0VxC0JyD5yCrvQGI2iHbHVvRtvkGNucyTCHo8M9+aJmiX4RBi+ot1fINwsx7vJ 8cvhNNDDM450nIUQDjU/7afjGpbH3rLfdBaPxTZEioy5emYqNHfDzUDYvcWjzy/95ARc fPTsAoiLtw78gQ4E8JFU7dYiTEyahG8C2Ald/ZeDpgOWSEKw2BepWft1ZG6g9ceenviZ GQoZv1efCfauQ62djO5cUMPuJq/FoiiAJCoeBaJF0ZZUGPYKCeurhaTHWKXlbwtwypu/ W9qA== X-Gm-Message-State: AOJu0YzW7+X2+K+2V2fE6stNvG71E24Flf/8jpi5bajpQnY+NG1EwalL aQqHL076lNSDj7VqMXA/JVYt3U+rck2cYRR8 X-Google-Smtp-Source: AGHT+IEs10bUpahviH7THPaM+dlIzOCu5KRMNLOelRhTF31CqsubjPlN9jyCNmP77Og7NUrpam5afw== X-Received: by 2002:ac2:5f08:0:b0:503:383c:996d with SMTP id 8-20020ac25f08000000b00503383c996dmr17812241lfq.12.1697106344714; Thu, 12 Oct 2023 03:25:44 -0700 (PDT) Received: from localhost.localdomain (95-24-2-172.broadband.corbina.ru. [95.24.2.172]) by smtp.gmail.com with ESMTPSA id m14-20020ac2428e000000b005032ebf8a00sm2712226lfh.197.2023.10.12.03.25.44 (version=TLS1_3 cipher=TLS_CHACHA20_POLY1305_SHA256 bits=256/256); Thu, 12 Oct 2023 03:25:44 -0700 (PDT) To: tarantool-patches@dev.tarantool.org, sergeyb@tarantool.org, skaplun@tarantool.org, m.kokryashkin@tarantool.org, imun@tarantool.org Cc: Maksim Kokryashkin Date: Thu, 12 Oct 2023 13:25:36 +0300 Message-Id: <20231012102536.41994-3-max.kokryashkin@gmail.com> X-Mailer: git-send-email 2.39.3 (Apple Git-145) In-Reply-To: <20231012102536.41994-1-max.kokryashkin@gmail.com> References: <20231012102536.41994-1-max.kokryashkin@gmail.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit Subject: [Tarantool-patches] [PATCH luajit v2 2/2] test: add test for debugging extension X-BeenThere: tarantool-patches@dev.tarantool.org X-Mailman-Version: 2.1.34 Precedence: list List-Id: Tarantool development patches List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , From: Maksim Kokryashkin via Tarantool-patches Reply-To: Maksim Kokryashkin Errors-To: tarantool-patches-bounces@dev.tarantool.org Sender: "Tarantool-patches" 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)