lldb: refactor plugin and test scripts
This commit is contained in:
36
_lldb/common.sh
Normal file
36
_lldb/common.sh
Normal 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}"
|
||||
}
|
||||
@@ -2,5 +2,17 @@
|
||||
|
||||
set -e
|
||||
|
||||
go run ./cmd/llgo build -o ./cl/_testdata/debug/out -dbg ./cl/_testdata/debug/
|
||||
lldb -O "command script import _lldb/llgo_plugin.py" ./cl/_testdata/debug/out
|
||||
# Source common functions and variables
|
||||
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"
|
||||
|
||||
@@ -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
|
||||
@@ -2,6 +2,54 @@
|
||||
|
||||
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"
|
||||
|
||||
127
_lldb/test.py
127
_lldb/test.py
@@ -5,7 +5,7 @@ import sys
|
||||
import argparse
|
||||
import signal
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List
|
||||
from typing import List, Optional, Set, Dict, Any
|
||||
import lldb
|
||||
import llgo_plugin # Add this import
|
||||
|
||||
@@ -14,7 +14,7 @@ class LLDBTestException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def log(*args, **kwargs):
|
||||
def log(*args: Any, **kwargs: Any) -> None:
|
||||
print(*args, **kwargs, flush=True)
|
||||
|
||||
|
||||
@@ -30,10 +30,10 @@ class Test:
|
||||
class TestResult:
|
||||
test: Test
|
||||
status: str
|
||||
actual: str = None
|
||||
message: str = None
|
||||
missing: set = None
|
||||
extra: set = None
|
||||
actual: Optional[str] = None
|
||||
message: Optional[str] = None
|
||||
missing: Optional[Set[str]] = None
|
||||
extra: Optional[Set[str]] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -60,42 +60,41 @@ class TestResults:
|
||||
|
||||
|
||||
class LLDBDebugger:
|
||||
def __init__(self, executable_path, plugin_path=None):
|
||||
self.executable_path = executable_path
|
||||
self.plugin_path = plugin_path
|
||||
self.debugger = lldb.SBDebugger.Create()
|
||||
def __init__(self, executable_path: str, plugin_path: Optional[str] = None):
|
||||
self.executable_path: str = executable_path
|
||||
self.plugin_path: Optional[str] = plugin_path
|
||||
self.debugger: lldb.SBDebugger = lldb.SBDebugger.Create()
|
||||
self.debugger.SetAsync(False)
|
||||
self.target = None
|
||||
self.process = None
|
||||
self.type_mapping = {
|
||||
self.target: Optional[lldb.SBTarget] = None
|
||||
self.process: Optional[lldb.SBProcess] = None
|
||||
self.type_mapping: Dict[str, str] = {
|
||||
'long': 'int',
|
||||
'unsigned long': 'uint',
|
||||
# Add more mappings as needed
|
||||
}
|
||||
|
||||
def setup(self):
|
||||
def setup(self) -> None:
|
||||
if self.plugin_path:
|
||||
self.debugger.HandleCommand(
|
||||
f'command script import "{self.plugin_path}"')
|
||||
self.target = self.debugger.CreateTarget(self.executable_path)
|
||||
if not self.target:
|
||||
raise LLDBTestException(f"Failed to create target for {
|
||||
self.executable_path}")
|
||||
self.executable_path}")
|
||||
|
||||
self.debugger.HandleCommand(
|
||||
'command script add -f llgo_plugin.print_go_expression p')
|
||||
self.debugger.HandleCommand(
|
||||
'command script add -f llgo_plugin.print_all_variables v')
|
||||
|
||||
def set_breakpoint(self, file_spec, line_number):
|
||||
bp = self.target.BreakpointCreateByLocation(
|
||||
file_spec, line_number)
|
||||
def set_breakpoint(self, file_spec: str, line_number: int) -> lldb.SBBreakpoint:
|
||||
bp = self.target.BreakpointCreateByLocation(file_spec, line_number)
|
||||
if not bp.IsValid():
|
||||
raise LLDBTestException(f"Failed to set breakpoint at {
|
||||
file_spec}:{line_number}")
|
||||
file_spec}:{line_number}")
|
||||
return bp
|
||||
|
||||
def run_to_breakpoint(self):
|
||||
def run_to_breakpoint(self) -> None:
|
||||
if not self.process:
|
||||
self.process = self.target.LaunchSimple(None, None, os.getcwd())
|
||||
else:
|
||||
@@ -103,10 +102,9 @@ class LLDBDebugger:
|
||||
if self.process.GetState() != lldb.eStateStopped:
|
||||
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)
|
||||
|
||||
# 处理结构体成员访问、指针解引用和数组索引
|
||||
parts = var_expression.split('.')
|
||||
var = frame.FindVariable(parts[0])
|
||||
|
||||
@@ -114,12 +112,10 @@ class LLDBDebugger:
|
||||
if not var.IsValid():
|
||||
return None
|
||||
|
||||
# 处理数组索引
|
||||
if '[' in part and ']' in part:
|
||||
array_name, index = part.split('[')
|
||||
index = int(index.rstrip(']'))
|
||||
var = var.GetChildAtIndex(index)
|
||||
# 处理指针解引用
|
||||
elif var.GetType().IsPointerType():
|
||||
var = var.Dereference()
|
||||
var = var.GetChildMemberWithName(part)
|
||||
@@ -128,24 +124,23 @@ class LLDBDebugger:
|
||||
|
||||
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)
|
||||
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)
|
||||
return frame.GetFunctionName()
|
||||
|
||||
def cleanup(self):
|
||||
def cleanup(self) -> None:
|
||||
if self.process and self.process.IsValid():
|
||||
self.process.Kill()
|
||||
lldb.SBDebugger.Destroy(self.debugger)
|
||||
|
||||
def run_console(self):
|
||||
def run_console(self) -> bool:
|
||||
log("\nEntering LLDB interactive mode.")
|
||||
log("Type 'quit' to exit and continue with the next test case.")
|
||||
log(
|
||||
"Use Ctrl+D to exit and continue, or Ctrl+C to abort all tests.")
|
||||
log("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
|
||||
sys.stdin, sys.stdout, sys.stderr = sys.__stdin__, sys.__stdout__, sys.__stderr__
|
||||
@@ -157,7 +152,7 @@ class LLDBDebugger:
|
||||
interpreter = self.debugger.GetCommandInterpreter()
|
||||
continue_tests = True
|
||||
|
||||
def keyboard_interrupt_handler(_sig, _frame):
|
||||
def keyboard_interrupt_handler(_sig: Any, _frame: Any) -> None:
|
||||
nonlocal continue_tests
|
||||
log("\nTest execution aborted by user.")
|
||||
continue_tests = False
|
||||
@@ -172,21 +167,19 @@ class LLDBDebugger:
|
||||
try:
|
||||
command = input().strip()
|
||||
except EOFError:
|
||||
log(
|
||||
"\nExiting LLDB interactive mode. Continuing with next test case.")
|
||||
log("\nExiting LLDB interactive mode. Continuing with next test case.")
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
break
|
||||
|
||||
if command.lower() == 'quit':
|
||||
log(
|
||||
"\nExiting LLDB interactive mode. Continuing with next test case.")
|
||||
log("\nExiting LLDB interactive mode. Continuing with next test case.")
|
||||
break
|
||||
|
||||
result = lldb.SBCommandReturnObject()
|
||||
interpreter.HandleCommand(command, result)
|
||||
log(result.GetOutput().rstrip(
|
||||
) if result.Succeeded() else result.GetError().rstrip())
|
||||
log(result.GetOutput().rstrip() if result.Succeeded()
|
||||
else result.GetError().rstrip())
|
||||
|
||||
finally:
|
||||
signal.signal(signal.SIGINT, original_handler)
|
||||
@@ -195,7 +188,7 @@ class LLDBDebugger:
|
||||
return continue_tests
|
||||
|
||||
|
||||
def parse_expected_values(source_files):
|
||||
def parse_expected_values(source_files: List[str]) -> List[TestCase]:
|
||||
test_cases = []
|
||||
for source_file in source_files:
|
||||
with open(source_file, 'r', encoding='utf-8') as f:
|
||||
@@ -224,7 +217,7 @@ def parse_expected_values(source_files):
|
||||
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()
|
||||
|
||||
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 {
|
||||
test_case.source_file}:{test_case.end_line}")
|
||||
debugger.setup()
|
||||
debugger.set_breakpoint(
|
||||
test_case.source_file, test_case.end_line)
|
||||
debugger.set_breakpoint(test_case.source_file, test_case.end_line)
|
||||
debugger.run_to_breakpoint()
|
||||
|
||||
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
|
||||
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:
|
||||
print_test_result(result, True)
|
||||
print_test_result(result, verbose=verbose)
|
||||
|
||||
if interactive and any(r.status != 'pass' for r in case_result.results):
|
||||
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
|
||||
|
||||
|
||||
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)
|
||||
if verbose:
|
||||
log(f"Running tests for {
|
||||
', '.join(source_files)} with {executable_path}")
|
||||
log(f"Found {len(test_cases)} test cases")
|
||||
|
||||
results = execute_tests(executable_path, test_cases, verbose,
|
||||
interactive, plugin_path)
|
||||
results = execute_tests(executable_path, test_cases,
|
||||
verbose, interactive, plugin_path)
|
||||
|
||||
if results.total != results.passed:
|
||||
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 = []
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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())
|
||||
if expected_vars == all_variable_names:
|
||||
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)
|
||||
if actual_value is None:
|
||||
return TestResult(
|
||||
@@ -321,11 +314,9 @@ def execute_single_variable_test(debugger, test):
|
||||
message=f'Unable to fetch value for {test.variable}'
|
||||
)
|
||||
|
||||
# 移除可能的空格,但保留括号
|
||||
actual_value = actual_value.strip()
|
||||
expected_value = test.expected_value.strip()
|
||||
|
||||
# 比较处理后的值
|
||||
if actual_value == expected_value:
|
||||
return TestResult(
|
||||
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:
|
||||
case = case_result.test_case
|
||||
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")
|
||||
|
||||
|
||||
def print_test_result(result: TestResult, verbose):
|
||||
def print_test_result(result: TestResult, verbose: bool) -> None:
|
||||
status_symbol = "✓" if result.status == 'pass' else "✗"
|
||||
status_text = "Pass" if result.status == 'pass' else "Fail"
|
||||
test = result.test
|
||||
|
||||
if result.status == 'pass':
|
||||
if verbose:
|
||||
log(
|
||||
f"{status_symbol} Line {test.line_number}, {test.variable}: {status_text}")
|
||||
log(f"{status_symbol} Line {test.line_number}, {
|
||||
test.variable}: {status_text}")
|
||||
if test.variable == 'all variables':
|
||||
log(f" Variables: {
|
||||
', '.join(sorted(result.actual))}")
|
||||
log(f" Variables: {', '.join(sorted(result.actual))}")
|
||||
else: # fail or error
|
||||
log(
|
||||
f"{status_symbol} Line {test.line_number}, {test.variable}: {status_text}")
|
||||
log(f"{status_symbol} Line {test.line_number}, {
|
||||
test.variable}: {status_text}")
|
||||
if test.variable == 'all variables':
|
||||
if result.missing:
|
||||
log(
|
||||
f" Missing variables: {', '.join(sorted(result.missing))}")
|
||||
log(f" Missing variables: {
|
||||
', '.join(sorted(result.missing))}")
|
||||
if result.extra:
|
||||
log(
|
||||
f" Extra variables: {', '.join(sorted(result.extra))}")
|
||||
log(
|
||||
f" Expected: {', '.join(sorted(test.expected_value.split()))}")
|
||||
log(f" Extra variables: {', '.join(sorted(result.extra))}")
|
||||
log(f" Expected: {
|
||||
', '.join(sorted(test.expected_value.split()))}")
|
||||
log(f" Actual: {', '.join(sorted(result.actual))}")
|
||||
elif result.status == 'error':
|
||||
log(f" Error: {result.message}")
|
||||
@@ -390,7 +379,7 @@ def print_test_result(result: TestResult, verbose):
|
||||
log(f" Actual: {result.actual}")
|
||||
|
||||
|
||||
def main():
|
||||
def main() -> None:
|
||||
log(sys.argv)
|
||||
parser = argparse.ArgumentParser(
|
||||
description="LLDB 18 Debug Script with DWARF 5 Support")
|
||||
@@ -411,9 +400,3 @@ def main():
|
||||
|
||||
if __name__ == "__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')
|
||||
|
||||
@@ -156,7 +156,91 @@ func FuncWithAllTypeParams(
|
||||
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() {
|
||||
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
|
||||
s := StructWithAllTypeFields{
|
||||
i8: 1,
|
||||
|
||||
Reference in New Issue
Block a user