lldb: refactor plugin and test scripts

This commit is contained in:
Li Jie
2024-09-21 10:03:49 +08:00
parent 867c01d5e8
commit 88b980ac17
6 changed files with 239 additions and 85 deletions

36
_lldb/common.sh Normal file
View File

@@ -0,0 +1,36 @@
#!/bin/bash
# Function to find LLDB 18+
find_lldb() {
local lldb_paths=(
"/opt/homebrew/bin/lldb"
"/usr/local/bin/lldb"
"/usr/bin/lldb"
"lldb" # This will use the system PATH
)
for lldb_path in "${lldb_paths[@]}"; do
if command -v "$lldb_path" >/dev/null 2>&1; then
local version=$("$lldb_path" --version | grep -oE '[0-9]+' | head -1)
if [ "$version" -ge 18 ]; then
echo "$lldb_path"
return 0
fi
fi
done
echo "Error: LLDB 18 or higher not found" >&2
exit 1
}
# Find LLDB 18+
LLDB_PATH=$(find_lldb)
# Default package path
DEFAULT_PACKAGE_PATH="./cl/_testdata/debug"
# Function to build the project
build_project() {
local package_path="$1"
go run ./cmd/llgo build -o "${package_path}/out" -dbg "${package_path}"
}

View File

@@ -2,5 +2,17 @@
set -e set -e
go run ./cmd/llgo build -o ./cl/_testdata/debug/out -dbg ./cl/_testdata/debug/ # Source common functions and variables
lldb -O "command script import _lldb/llgo_plugin.py" ./cl/_testdata/debug/out source "$(dirname "$0")/common.sh"
# Check if a package path is provided as an argument
package_path="$DEFAULT_PACKAGE_PATH"
if [ $# -eq 1 ]; then
package_path="$1"
fi
# Build the project
build_project "$package_path"
# Run LLDB
"$LLDB_PATH" "${package_path}/out"

View File

@@ -1,9 +0,0 @@
# LLDB can't be load from Python 3.12, should load Python script by lldb command
# See https://github.com/llvm/llvm-project/issues/70453
# go run ./cmd/llgo build -o cl/_testdata/debug/out -dbg ./cl/_testdata/debug
# lldb -S _lldb/runtest.lldb
command script import _lldb/test.py
script main.run_tests("cl/_testdata/debug/out", ["cl/_testdata/debug/in.go"], True, False, None)
quit

View File

@@ -2,6 +2,54 @@
set -e set -e
go run ./cmd/llgo build -o ./cl/_testdata/debug/out -dbg ./cl/_testdata/debug/ # Source common functions and variables
source "$(dirname "$0")/common.sh"
/opt/homebrew/bin/lldb -S _lldb/runtest.lldb # Parse command-line arguments
package_path="$DEFAULT_PACKAGE_PATH"
verbose=False
interactive=False
plugin_path=None
while [[ $# -gt 0 ]]; do
case $1 in
-v|--verbose)
verbose=True
shift
;;
-i|--interactive)
interactive=True
shift
;;
-p|--plugin)
plugin_path="\"$2\""
shift 2
;;
*)
package_path="$1"
shift
;;
esac
done
# Build the project
build_project "$package_path"
# Prepare LLDB commands
lldb_commands=(
"command script import _lldb/test.py"
"script test.run_tests(\\\"${package_path}/out\\\", [\\\"${package_path}/in.go\\\"], ${verbose}, ${interactive}, ${plugin_path})"
)
# Add quit command if not in interactive mode
if [ "$interactive" = False ]; then
lldb_commands+=("quit")
fi
# Run LLDB with prepared commands
lldb_command_string=""
for cmd in "${lldb_commands[@]}"; do
lldb_command_string+=" -O \"$cmd\""
done
eval "$LLDB_PATH $lldb_command_string"

View File

@@ -5,7 +5,7 @@ import sys
import argparse import argparse
import signal import signal
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import List from typing import List, Optional, Set, Dict, Any
import lldb import lldb
import llgo_plugin # Add this import import llgo_plugin # Add this import
@@ -14,7 +14,7 @@ class LLDBTestException(Exception):
pass pass
def log(*args, **kwargs): def log(*args: Any, **kwargs: Any) -> None:
print(*args, **kwargs, flush=True) print(*args, **kwargs, flush=True)
@@ -30,10 +30,10 @@ class Test:
class TestResult: class TestResult:
test: Test test: Test
status: str status: str
actual: str = None actual: Optional[str] = None
message: str = None message: Optional[str] = None
missing: set = None missing: Optional[Set[str]] = None
extra: set = None extra: Optional[Set[str]] = None
@dataclass @dataclass
@@ -60,42 +60,41 @@ class TestResults:
class LLDBDebugger: class LLDBDebugger:
def __init__(self, executable_path, plugin_path=None): def __init__(self, executable_path: str, plugin_path: Optional[str] = None):
self.executable_path = executable_path self.executable_path: str = executable_path
self.plugin_path = plugin_path self.plugin_path: Optional[str] = plugin_path
self.debugger = lldb.SBDebugger.Create() self.debugger: lldb.SBDebugger = lldb.SBDebugger.Create()
self.debugger.SetAsync(False) self.debugger.SetAsync(False)
self.target = None self.target: Optional[lldb.SBTarget] = None
self.process = None self.process: Optional[lldb.SBProcess] = None
self.type_mapping = { self.type_mapping: Dict[str, str] = {
'long': 'int', 'long': 'int',
'unsigned long': 'uint', 'unsigned long': 'uint',
# Add more mappings as needed # Add more mappings as needed
} }
def setup(self): def setup(self) -> None:
if self.plugin_path: if self.plugin_path:
self.debugger.HandleCommand( self.debugger.HandleCommand(
f'command script import "{self.plugin_path}"') f'command script import "{self.plugin_path}"')
self.target = self.debugger.CreateTarget(self.executable_path) self.target = self.debugger.CreateTarget(self.executable_path)
if not self.target: if not self.target:
raise LLDBTestException(f"Failed to create target for { raise LLDBTestException(f"Failed to create target for {
self.executable_path}") self.executable_path}")
self.debugger.HandleCommand( self.debugger.HandleCommand(
'command script add -f llgo_plugin.print_go_expression p') 'command script add -f llgo_plugin.print_go_expression p')
self.debugger.HandleCommand( self.debugger.HandleCommand(
'command script add -f llgo_plugin.print_all_variables v') 'command script add -f llgo_plugin.print_all_variables v')
def set_breakpoint(self, file_spec, line_number): def set_breakpoint(self, file_spec: str, line_number: int) -> lldb.SBBreakpoint:
bp = self.target.BreakpointCreateByLocation( bp = self.target.BreakpointCreateByLocation(file_spec, line_number)
file_spec, line_number)
if not bp.IsValid(): if not bp.IsValid():
raise LLDBTestException(f"Failed to set breakpoint at { raise LLDBTestException(f"Failed to set breakpoint at {
file_spec}:{line_number}") file_spec}:{line_number}")
return bp return bp
def run_to_breakpoint(self): def run_to_breakpoint(self) -> None:
if not self.process: if not self.process:
self.process = self.target.LaunchSimple(None, None, os.getcwd()) self.process = self.target.LaunchSimple(None, None, os.getcwd())
else: else:
@@ -103,10 +102,9 @@ class LLDBDebugger:
if self.process.GetState() != lldb.eStateStopped: if self.process.GetState() != lldb.eStateStopped:
raise LLDBTestException("Process didn't stop at breakpoint") raise LLDBTestException("Process didn't stop at breakpoint")
def get_variable_value(self, var_expression): def get_variable_value(self, var_expression: str) -> Optional[str]:
frame = self.process.GetSelectedThread().GetFrameAtIndex(0) frame = self.process.GetSelectedThread().GetFrameAtIndex(0)
# 处理结构体成员访问、指针解引用和数组索引
parts = var_expression.split('.') parts = var_expression.split('.')
var = frame.FindVariable(parts[0]) var = frame.FindVariable(parts[0])
@@ -114,12 +112,10 @@ class LLDBDebugger:
if not var.IsValid(): if not var.IsValid():
return None return None
# 处理数组索引
if '[' in part and ']' in part: if '[' in part and ']' in part:
array_name, index = part.split('[') array_name, index = part.split('[')
index = int(index.rstrip(']')) index = int(index.rstrip(']'))
var = var.GetChildAtIndex(index) var = var.GetChildAtIndex(index)
# 处理指针解引用
elif var.GetType().IsPointerType(): elif var.GetType().IsPointerType():
var = var.Dereference() var = var.Dereference()
var = var.GetChildMemberWithName(part) var = var.GetChildMemberWithName(part)
@@ -128,24 +124,23 @@ class LLDBDebugger:
return llgo_plugin.format_value(var, self.debugger) if var.IsValid() else None return llgo_plugin.format_value(var, self.debugger) if var.IsValid() else None
def get_all_variable_names(self): def get_all_variable_names(self) -> Set[str]:
frame = self.process.GetSelectedThread().GetFrameAtIndex(0) frame = self.process.GetSelectedThread().GetFrameAtIndex(0)
return set(var.GetName() for var in frame.GetVariables(True, True, True, False)) return set(var.GetName() for var in frame.GetVariables(True, True, True, False))
def get_current_function_name(self): def get_current_function_name(self) -> str:
frame = self.process.GetSelectedThread().GetFrameAtIndex(0) frame = self.process.GetSelectedThread().GetFrameAtIndex(0)
return frame.GetFunctionName() return frame.GetFunctionName()
def cleanup(self): def cleanup(self) -> None:
if self.process and self.process.IsValid(): if self.process and self.process.IsValid():
self.process.Kill() self.process.Kill()
lldb.SBDebugger.Destroy(self.debugger) lldb.SBDebugger.Destroy(self.debugger)
def run_console(self): def run_console(self) -> bool:
log("\nEntering LLDB interactive mode.") log("\nEntering LLDB interactive mode.")
log("Type 'quit' to exit and continue with the next test case.") log("Type 'quit' to exit and continue with the next test case.")
log( log("Use Ctrl+D to exit and continue, or Ctrl+C to abort all tests.")
"Use Ctrl+D to exit and continue, or Ctrl+C to abort all tests.")
old_stdin, old_stdout, old_stderr = sys.stdin, sys.stdout, sys.stderr old_stdin, old_stdout, old_stderr = sys.stdin, sys.stdout, sys.stderr
sys.stdin, sys.stdout, sys.stderr = sys.__stdin__, sys.__stdout__, sys.__stderr__ sys.stdin, sys.stdout, sys.stderr = sys.__stdin__, sys.__stdout__, sys.__stderr__
@@ -157,7 +152,7 @@ class LLDBDebugger:
interpreter = self.debugger.GetCommandInterpreter() interpreter = self.debugger.GetCommandInterpreter()
continue_tests = True continue_tests = True
def keyboard_interrupt_handler(_sig, _frame): def keyboard_interrupt_handler(_sig: Any, _frame: Any) -> None:
nonlocal continue_tests nonlocal continue_tests
log("\nTest execution aborted by user.") log("\nTest execution aborted by user.")
continue_tests = False continue_tests = False
@@ -172,21 +167,19 @@ class LLDBDebugger:
try: try:
command = input().strip() command = input().strip()
except EOFError: except EOFError:
log( log("\nExiting LLDB interactive mode. Continuing with next test case.")
"\nExiting LLDB interactive mode. Continuing with next test case.")
break break
except KeyboardInterrupt: except KeyboardInterrupt:
break break
if command.lower() == 'quit': if command.lower() == 'quit':
log( log("\nExiting LLDB interactive mode. Continuing with next test case.")
"\nExiting LLDB interactive mode. Continuing with next test case.")
break break
result = lldb.SBCommandReturnObject() result = lldb.SBCommandReturnObject()
interpreter.HandleCommand(command, result) interpreter.HandleCommand(command, result)
log(result.GetOutput().rstrip( log(result.GetOutput().rstrip() if result.Succeeded()
) if result.Succeeded() else result.GetError().rstrip()) else result.GetError().rstrip())
finally: finally:
signal.signal(signal.SIGINT, original_handler) signal.signal(signal.SIGINT, original_handler)
@@ -195,7 +188,7 @@ class LLDBDebugger:
return continue_tests return continue_tests
def parse_expected_values(source_files): def parse_expected_values(source_files: List[str]) -> List[TestCase]:
test_cases = [] test_cases = []
for source_file in source_files: for source_file in source_files:
with open(source_file, 'r', encoding='utf-8') as f: with open(source_file, 'r', encoding='utf-8') as f:
@@ -224,7 +217,7 @@ def parse_expected_values(source_files):
return test_cases return test_cases
def execute_tests(executable_path, test_cases, verbose, interactive, plugin_path): def execute_tests(executable_path: str, test_cases: List[TestCase], verbose: bool, interactive: bool, plugin_path: Optional[str]) -> TestResults:
results = TestResults() results = TestResults()
for test_case in test_cases: for test_case in test_cases:
@@ -234,8 +227,7 @@ def execute_tests(executable_path, test_cases, verbose, interactive, plugin_path
log(f"Setting breakpoint at { log(f"Setting breakpoint at {
test_case.source_file}:{test_case.end_line}") test_case.source_file}:{test_case.end_line}")
debugger.setup() debugger.setup()
debugger.set_breakpoint( debugger.set_breakpoint(test_case.source_file, test_case.end_line)
test_case.source_file, test_case.end_line)
debugger.run_to_breakpoint() debugger.run_to_breakpoint()
all_variable_names = debugger.get_all_variable_names() all_variable_names = debugger.get_all_variable_names()
@@ -250,9 +242,10 @@ def execute_tests(executable_path, test_cases, verbose, interactive, plugin_path
case = case_result.test_case case = case_result.test_case
loc = f"{case.source_file}:{case.start_line}-{case.end_line}" loc = f"{case.source_file}:{case.start_line}-{case.end_line}"
log(f"\nTest case: {loc} in function '{case_result.function}'") if verbose or interactive or any(r.status != 'pass' for r in case_result.results):
log(f"\nTest case: {loc} in function '{case_result.function}'")
for result in case_result.results: for result in case_result.results:
print_test_result(result, True) print_test_result(result, verbose=verbose)
if interactive and any(r.status != 'pass' for r in case_result.results): if interactive and any(r.status != 'pass' for r in case_result.results):
log("\nTest case failed. Entering LLDB interactive mode.") log("\nTest case failed. Entering LLDB interactive mode.")
@@ -267,21 +260,21 @@ def execute_tests(executable_path, test_cases, verbose, interactive, plugin_path
return results return results
def run_tests(executable_path, source_files, verbose, interactive, plugin_path): def run_tests(executable_path: str, source_files: List[str], verbose: bool, interactive: bool, plugin_path: Optional[str]) -> None:
test_cases = parse_expected_values(source_files) test_cases = parse_expected_values(source_files)
if verbose: if verbose:
log(f"Running tests for { log(f"Running tests for {
', '.join(source_files)} with {executable_path}") ', '.join(source_files)} with {executable_path}")
log(f"Found {len(test_cases)} test cases") log(f"Found {len(test_cases)} test cases")
results = execute_tests(executable_path, test_cases, verbose, results = execute_tests(executable_path, test_cases,
interactive, plugin_path) verbose, interactive, plugin_path)
if results.total != results.passed: if results.total != results.passed:
os._exit(1) os._exit(1)
def execute_test_case(debugger, test_case, all_variable_names): def execute_test_case(debugger: LLDBDebugger, test_case: TestCase, all_variable_names: Set[str]) -> CaseResult:
results = [] results = []
for test in test_case.tests: for test in test_case.tests:
@@ -294,7 +287,7 @@ def execute_test_case(debugger, test_case, all_variable_names):
return CaseResult(test_case, debugger.get_current_function_name(), results) return CaseResult(test_case, debugger.get_current_function_name(), results)
def execute_all_variables_test(test, all_variable_names): def execute_all_variables_test(test: Test, all_variable_names: Set[str]) -> TestResult:
expected_vars = set(test.expected_value.split()) expected_vars = set(test.expected_value.split())
if expected_vars == all_variable_names: if expected_vars == all_variable_names:
return TestResult( return TestResult(
@@ -312,7 +305,7 @@ def execute_all_variables_test(test, all_variable_names):
) )
def execute_single_variable_test(debugger, test): def execute_single_variable_test(debugger: LLDBDebugger, test: Test) -> TestResult:
actual_value = debugger.get_variable_value(test.variable) actual_value = debugger.get_variable_value(test.variable)
if actual_value is None: if actual_value is None:
return TestResult( return TestResult(
@@ -321,11 +314,9 @@ def execute_single_variable_test(debugger, test):
message=f'Unable to fetch value for {test.variable}' message=f'Unable to fetch value for {test.variable}'
) )
# 移除可能的空格,但保留括号
actual_value = actual_value.strip() actual_value = actual_value.strip()
expected_value = test.expected_value.strip() expected_value = test.expected_value.strip()
# 比较处理后的值
if actual_value == expected_value: if actual_value == expected_value:
return TestResult( return TestResult(
test=test, test=test,
@@ -340,7 +331,7 @@ def execute_single_variable_test(debugger, test):
) )
def print_test_results(results: TestResults, verbose): def print_test_results(results: TestResults, verbose: bool) -> None:
for case_result in results.case_results: for case_result in results.case_results:
case = case_result.test_case case = case_result.test_case
loc = f"{case.source_file}:{case.start_line}-{case.end_line}" loc = f"{case.source_file}:{case.start_line}-{case.end_line}"
@@ -358,30 +349,28 @@ def print_test_results(results: TestResults, verbose):
log("Some tests failed") log("Some tests failed")
def print_test_result(result: TestResult, verbose): def print_test_result(result: TestResult, verbose: bool) -> None:
status_symbol = "" if result.status == 'pass' else "" status_symbol = "" if result.status == 'pass' else ""
status_text = "Pass" if result.status == 'pass' else "Fail" status_text = "Pass" if result.status == 'pass' else "Fail"
test = result.test test = result.test
if result.status == 'pass': if result.status == 'pass':
if verbose: if verbose:
log( log(f"{status_symbol} Line {test.line_number}, {
f"{status_symbol} Line {test.line_number}, {test.variable}: {status_text}") test.variable}: {status_text}")
if test.variable == 'all variables': if test.variable == 'all variables':
log(f" Variables: { log(f" Variables: {', '.join(sorted(result.actual))}")
', '.join(sorted(result.actual))}")
else: # fail or error else: # fail or error
log( log(f"{status_symbol} Line {test.line_number}, {
f"{status_symbol} Line {test.line_number}, {test.variable}: {status_text}") test.variable}: {status_text}")
if test.variable == 'all variables': if test.variable == 'all variables':
if result.missing: if result.missing:
log( log(f" Missing variables: {
f" Missing variables: {', '.join(sorted(result.missing))}") ', '.join(sorted(result.missing))}")
if result.extra: if result.extra:
log( log(f" Extra variables: {', '.join(sorted(result.extra))}")
f" Extra variables: {', '.join(sorted(result.extra))}") log(f" Expected: {
log( ', '.join(sorted(test.expected_value.split()))}")
f" Expected: {', '.join(sorted(test.expected_value.split()))}")
log(f" Actual: {', '.join(sorted(result.actual))}") log(f" Actual: {', '.join(sorted(result.actual))}")
elif result.status == 'error': elif result.status == 'error':
log(f" Error: {result.message}") log(f" Error: {result.message}")
@@ -390,7 +379,7 @@ def print_test_result(result: TestResult, verbose):
log(f" Actual: {result.actual}") log(f" Actual: {result.actual}")
def main(): def main() -> None:
log(sys.argv) log(sys.argv)
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="LLDB 18 Debug Script with DWARF 5 Support") description="LLDB 18 Debug Script with DWARF 5 Support")
@@ -411,9 +400,3 @@ def main():
if __name__ == "__main__": if __name__ == "__main__":
main() main()
def __lldb_init_module(debugger, _internal_dict):
run_tests("cl/_testdata/debug/out",
["cl/_testdata/debug/in.go"], True, False, None)
debugger.HandleCommand('quit')

View File

@@ -156,7 +156,91 @@ func FuncWithAllTypeParams(
return 1, errors.New("some error") return 1, errors.New("some error")
} }
type TinyStruct struct {
I int
}
type SmallStruct struct {
I int
J int
}
type MidStruct struct {
I int
J int
K int
}
type BigStruct struct {
I int
J int
K int
L int
M int
N int
O int
P int
Q int
R int
}
func FuncStructParams(t TinyStruct, s SmallStruct, m MidStruct, b BigStruct) {
println(&t, &s, &m, &b)
// Expected:
// all variables: t s m b
// t.I: 1
// s.I: 2
// s.J: 3
// m.I: 4
// m.J: 5
// m.K: 6
// b.I: 7
// b.J: 8
// b.K: 9
// b.L: 10
// b.M: 11
// b.N: 12
// b.O: 13
// b.P: 14
// b.Q: 15
// b.R: 16
t.I = 10
// Expected:
// all variables: t s m b
// t.I: 10
println("done")
}
func FuncStructPtrParams(t *TinyStruct, s *SmallStruct, m *MidStruct, b *BigStruct) {
println(t, s, m, b)
// Expected:
// all variables: t s m b
// t.I: 1
// s.I: 2
// s.J: 3
// m.I: 4
// m.J: 5
// m.K: 6
// b.I: 7
// b.J: 8
// b.K: 9
// b.L: 10
// b.M: 11
// b.N: 12
// b.O: 13
// b.P: 14
// b.Q: 15
// b.R: 16
t.I = 10
// Expected:
// all variables: t s m b
// t.I: 10
println("done")
}
func main() { func main() {
FuncStructParams(TinyStruct{I: 1}, SmallStruct{I: 2, J: 3}, MidStruct{I: 4, J: 5, K: 6}, BigStruct{I: 7, J: 8, K: 9, L: 10, M: 11, N: 12, O: 13, P: 14, Q: 15, R: 16})
FuncStructPtrParams(&TinyStruct{I: 1}, &SmallStruct{I: 2, J: 3}, &MidStruct{I: 4, J: 5, K: 6}, &BigStruct{I: 7, J: 8, K: 9, L: 10, M: 11, N: 12, O: 13, P: 14, Q: 15, R: 16})
i := 100 i := 100
s := StructWithAllTypeFields{ s := StructWithAllTypeFields{
i8: 1, i8: 1,