From 88b980ac1736aee75f6764d983884fef464081b0 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sat, 21 Sep 2024 10:03:49 +0800 Subject: [PATCH] lldb: refactor plugin and test scripts --- _lldb/common.sh | 36 +++++++++++ _lldb/runlldb.sh | 16 ++++- _lldb/runtest.lldb | 9 --- _lldb/runtest.sh | 52 +++++++++++++++- _lldb/test.py | 127 +++++++++++++++++---------------------- cl/_testdata/debug/in.go | 84 ++++++++++++++++++++++++++ 6 files changed, 239 insertions(+), 85 deletions(-) create mode 100644 _lldb/common.sh delete mode 100644 _lldb/runtest.lldb diff --git a/_lldb/common.sh b/_lldb/common.sh new file mode 100644 index 00000000..b6fa506c --- /dev/null +++ b/_lldb/common.sh @@ -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}" +} \ No newline at end of file diff --git a/_lldb/runlldb.sh b/_lldb/runlldb.sh index 1dfb1fad..efeeabed 100755 --- a/_lldb/runlldb.sh +++ b/_lldb/runlldb.sh @@ -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" diff --git a/_lldb/runtest.lldb b/_lldb/runtest.lldb deleted file mode 100644 index 520a54cb..00000000 --- a/_lldb/runtest.lldb +++ /dev/null @@ -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 diff --git a/_lldb/runtest.sh b/_lldb/runtest.sh index 08c642fd..72276904 100755 --- a/_lldb/runtest.sh +++ b/_lldb/runtest.sh @@ -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" diff --git a/_lldb/test.py b/_lldb/test.py index 67282973..849f2a03 100644 --- a/_lldb/test.py +++ b/_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') diff --git a/cl/_testdata/debug/in.go b/cl/_testdata/debug/in.go index 3bc7c082..0555ba86 100644 --- a/cl/_testdata/debug/in.go +++ b/cl/_testdata/debug/in.go @@ -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,