-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
372 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,363 @@ | ||
import lldb | ||
import io | ||
import os | ||
import sys | ||
import argparse | ||
from dataclasses import dataclass, field | ||
from typing import List | ||
import cmd | ||
|
||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, line_buffering=False) | ||
|
||
def print_to_lldb(*args, **kwargs): | ||
print(*args, **kwargs, flush=True) | ||
|
||
@dataclass | ||
class Test: | ||
source_file: str | ||
line_number: int | ||
variable: str | ||
expected_value: str | ||
|
||
@dataclass | ||
class TestResult: | ||
test: Test | ||
status: str | ||
actual: str = None | ||
message: str = None | ||
missing: set = None | ||
extra: set = None | ||
|
||
@dataclass | ||
class TestCase: | ||
source_file: str | ||
start_line: int | ||
end_line: int | ||
tests: List[Test] | ||
|
||
@dataclass | ||
class CaseResult: | ||
test_case: TestCase | ||
function: str | ||
results: List[TestResult] | ||
|
||
@dataclass | ||
class TestResults: | ||
total: int = 0 | ||
passed: int = 0 | ||
failed: int = 0 | ||
case_results: List[CaseResult] = field(default_factory=list) | ||
|
||
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() | ||
self.debugger.SetAsync(False) | ||
self.target = None | ||
self.process = None | ||
|
||
def setup(self): | ||
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 Exception(f"Failed to create target for {self.executable_path}") | ||
|
||
def set_breakpoint(self, file_spec, line_number): | ||
breakpoint = self.target.BreakpointCreateByLocation(file_spec, line_number) | ||
if not breakpoint.IsValid(): | ||
raise Exception(f"Failed to set breakpoint at {file_spec}:{line_number}") | ||
return breakpoint | ||
|
||
def run_to_breakpoint(self): | ||
if not self.process: | ||
self.process = self.target.LaunchSimple(None, None, os.getcwd()) | ||
else: | ||
self.process.Continue() | ||
|
||
if self.process.GetState() != lldb.eStateStopped: | ||
raise Exception("Process didn't stop at breakpoint") | ||
|
||
def get_variable_value(self, var_name): | ||
frame = self.process.GetSelectedThread().GetFrameAtIndex(0) | ||
var = frame.FindVariable(var_name) | ||
if var.IsValid(): | ||
type_name = var.GetTypeName() | ||
if var.GetNumChildren() > 0: | ||
children = [] | ||
for i in range(var.GetNumChildren()): | ||
child = var.GetChildAtIndex(i) | ||
child_name = child.GetName() | ||
child_value = child.GetValue() | ||
children.append(f"{child_name} = {child_value}") | ||
return f"{type_name}({', '.join(children)})" | ||
else: | ||
value = var.GetValue() or var.GetSummary() or str(var) | ||
return str(value) if value is not None else "None" | ||
return None | ||
|
||
def get_all_variable_names(self): | ||
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): | ||
frame = self.process.GetSelectedThread().GetFrameAtIndex(0) | ||
return frame.GetFunctionName() | ||
|
||
def cleanup(self): | ||
if self.process and self.process.IsValid(): | ||
self.process.Kill() | ||
lldb.SBDebugger.Destroy(self.debugger) | ||
|
||
def run_console(self): | ||
print_to_lldb("\nEntering LLDB interactive mode. Type 'quit' to exit and continue with the next test case.") | ||
|
||
old_stdin, old_stdout, old_stderr = sys.stdin, sys.stdout, sys.stderr | ||
sys.stdin, sys.stdout, sys.stderr = sys.__stdin__, sys.__stdout__, sys.__stderr__ | ||
|
||
self.debugger.SetAsync(True) | ||
self.debugger.HandleCommand("settings set auto-confirm true") | ||
self.debugger.HandleCommand("command script import lldb") | ||
|
||
interpreter = self.debugger.GetCommandInterpreter() | ||
while True: | ||
print_to_lldb("\n(lldb) ", end="") | ||
command = input().strip() | ||
if command.lower() == 'quit': | ||
break | ||
|
||
result = lldb.SBCommandReturnObject() | ||
interpreter.HandleCommand(command, result) | ||
if result.Succeeded(): | ||
print_to_lldb(result.GetOutput().rstrip()) | ||
else: | ||
print_to_lldb(result.GetError().rstrip()) | ||
|
||
sys.stdin, sys.stdout, sys.stderr = old_stdin, old_stdout, old_stderr | ||
print_to_lldb("\nExiting LLDB interactive mode.") | ||
|
||
|
||
def parse_expected_values(source_files): | ||
test_cases = [] | ||
for source_file in source_files: | ||
with open(source_file, 'r') as f: | ||
content = f.readlines() | ||
i = 0 | ||
while i < len(content): | ||
line = content[i].strip() | ||
if line.startswith('// Expected:'): | ||
start_line = i + 1 | ||
tests = [] | ||
i += 1 | ||
while i < len(content): | ||
line = content[i].strip() | ||
if not line.startswith('//'): | ||
break | ||
parts = line.lstrip('//').strip().split(':', 1) | ||
if len(parts) == 2: | ||
var, value = map(str.strip, parts) | ||
tests.append(Test(source_file, i + 1, var, value)) | ||
i += 1 | ||
end_line = i | ||
test_cases.append( | ||
TestCase(source_file, start_line, end_line, tests)) | ||
else: | ||
i += 1 | ||
return test_cases | ||
|
||
|
||
def run_tests(executable_path, source_files, verbose, interactive, plugin_path): | ||
debugger = LLDBDebugger(executable_path, plugin_path) | ||
test_cases = parse_expected_values(source_files) | ||
if verbose: | ||
print_to_lldb( | ||
f"Running tests for {', '.join(source_files)} with {executable_path}") | ||
print_to_lldb(f"Found {len(test_cases)} test cases") | ||
|
||
try: | ||
debugger.setup() | ||
results = execute_tests(debugger, test_cases, interactive) | ||
print_test_results(results, verbose) | ||
|
||
if results.total != results.passed: | ||
os._exit(1) | ||
|
||
except Exception as e: | ||
print_to_lldb(f"Error: {str(e)}") | ||
|
||
finally: | ||
debugger.cleanup() | ||
|
||
|
||
def execute_tests(debugger, test_cases, interactive): | ||
results = TestResults() | ||
|
||
for test_case in test_cases: | ||
breakpoint = debugger.set_breakpoint( | ||
test_case.source_file, test_case.end_line) | ||
debugger.run_to_breakpoint() | ||
|
||
function_name = debugger.get_current_function_name() | ||
all_variable_names = debugger.get_all_variable_names() | ||
|
||
case_result = execute_test_case( | ||
debugger, test_case, all_variable_names) | ||
|
||
results.total += len(case_result.results) | ||
results.passed += sum(1 for r in case_result.results if r.status == 'pass') | ||
results.failed += sum(1 for r in case_result.results if r.status != 'pass') | ||
results.case_results.append(case_result) | ||
|
||
# Print the current case test results before entering interactive mode | ||
print_to_lldb(f"\nTest case: {case_result.test_case.source_file}:{case_result.test_case.start_line}-{case_result.test_case.end_line} in function '{case_result.function}'") | ||
for result in case_result.results: | ||
# Always print verbose results here | ||
print_test_result(result, True) | ||
|
||
if interactive and any(r.status != 'pass' for r in case_result.results): | ||
print_to_lldb("\nTest case failed. Entering LLDB interactive mode.") | ||
debugger.run_console() | ||
# After exiting the console, we need to ensure the process is in a valid state | ||
if debugger.process.GetState() == lldb.eStateRunning: | ||
debugger.process.Stop() | ||
elif debugger.process.GetState() == lldb.eStateExited: | ||
# If the process has exited, we need to re-launch it | ||
debugger.process = debugger.target.LaunchSimple( | ||
None, None, os.getcwd()) | ||
|
||
debugger.target.BreakpointDelete(breakpoint.GetID()) | ||
|
||
return results | ||
|
||
|
||
def execute_test_case(debugger, test_case, all_variable_names): | ||
results = [] | ||
|
||
for test in test_case.tests: | ||
if test.variable == "all variables": | ||
result = execute_all_variables_test(test, all_variable_names) | ||
else: | ||
result = execute_single_variable_test(debugger, test) | ||
results.append(result) | ||
|
||
return CaseResult(test_case, debugger.get_current_function_name(), results) | ||
|
||
|
||
def execute_all_variables_test(test, all_variable_names): | ||
expected_vars = set(test.expected_value.split()) | ||
if expected_vars == all_variable_names: | ||
return TestResult( | ||
test=test, | ||
status='pass', | ||
actual=all_variable_names | ||
) | ||
else: | ||
return TestResult( | ||
test=test, | ||
status='fail', | ||
actual=all_variable_names, | ||
missing=expected_vars - all_variable_names, | ||
extra=all_variable_names - expected_vars | ||
) | ||
|
||
|
||
def execute_single_variable_test(debugger, test): | ||
actual_value = debugger.get_variable_value(test.variable) | ||
if actual_value is None: | ||
print_to_lldb(f"Unable to fetch value for {test.variable}") | ||
return TestResult( | ||
test=test, | ||
status='error', | ||
message='Unable to fetch value' | ||
) | ||
|
||
# 移除可能的空格,但保留括号 | ||
actual_value = actual_value.strip() | ||
expected_value = test.expected_value.strip() | ||
|
||
# 比较处理后的值 | ||
if actual_value == expected_value: | ||
return TestResult( | ||
test=test, | ||
status='pass', | ||
actual=actual_value | ||
) | ||
else: | ||
return TestResult( | ||
test=test, | ||
status='fail', | ||
actual=actual_value | ||
) | ||
|
||
|
||
def print_test_results(results: TestResults, verbose): | ||
for case_result in results.case_results: | ||
print_to_lldb(f"\nTest case: {case_result.test_case.source_file}:{case_result.test_case.start_line}-{case_result.test_case.end_line} in function '{case_result.function}'") | ||
for result in case_result.results: | ||
print_test_result(result, verbose) | ||
|
||
print_to_lldb("\nTest results:") | ||
print_to_lldb(f" Total tests: {results.total}") | ||
print_to_lldb(f" Passed tests: {results.passed}") | ||
print_to_lldb(f" Failed tests: {results.failed}") | ||
if results.total == results.passed: | ||
print_to_lldb("All tests passed!") | ||
else: | ||
print_to_lldb("Some tests failed") | ||
|
||
|
||
def print_test_result(result: TestResult, verbose): | ||
status_symbol = "✓" if result.status == 'pass' else "✗" | ||
status_text = "Pass" if result.status == 'pass' else "Fail" | ||
|
||
if result.status == 'pass': | ||
if verbose: | ||
print_to_lldb( | ||
f"{status_symbol} Line {result.test.line_number}, {result.test.variable}: {status_text}") | ||
if result.test.variable == 'all variables': | ||
print_to_lldb(f" Variables: {', '.join(sorted(result.actual))}") | ||
else: # fail or error | ||
print_to_lldb( | ||
f"{status_symbol} Line {result.test.line_number}, {result.test.variable}: {status_text}") | ||
if result.test.variable == 'all variables': | ||
if result.missing: | ||
print_to_lldb( | ||
f" Missing variables: {', '.join(sorted(result.missing))}") | ||
if result.extra: | ||
print_to_lldb( | ||
f" Extra variables: {', '.join(sorted(result.extra))}") | ||
print_to_lldb( | ||
f" Expected: {', '.join(sorted(result.test.expected_value.split()))}") | ||
print_to_lldb(f" Actual: {', '.join(sorted(result.actual))}") | ||
elif result.status == 'error': | ||
print_to_lldb(f" Error: {result.message}") | ||
else: | ||
print_to_lldb(f" Expected: {result.test.expected_value}") | ||
print_to_lldb(f" Actual: {result.actual}") | ||
|
||
def main(): | ||
print_to_lldb(sys.argv) | ||
parser = argparse.ArgumentParser(description="LLDB 18 Debug Script with DWARF 5 Support") | ||
parser.add_argument("executable", help="Path to the executable") | ||
parser.add_argument("sources", nargs='+', help="Paths to the source files") | ||
parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose output") | ||
parser.add_argument("-i", "--interactive", action="store_true", help="Enable interactive mode on test failure") | ||
parser.add_argument("--plugin", help="Path to the LLDB plugin") | ||
args = parser.parse_args() | ||
|
||
plugin_path = args.plugin or os.path.join(os.path.dirname(os.path.realpath(__file__)), "go_lldb_plugin.py") | ||
run_tests(args.executable, args.sources, args.verbose, args.interactive, plugin_path) | ||
|
||
if __name__ == "__main__": | ||
main() | ||
|
||
def run_commands(debugger, command, result, internal_dict): | ||
print_to_lldb(sys.argv) | ||
main() | ||
debugger.HandleCommand("quit") | ||
|
||
def __lldb_init_module(debugger, internal_dict): | ||
# debugger.HandleCommand('command script add -f main.run_commands run_tests') | ||
pass |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
# 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 _lldbtest/runmain.lldb | ||
|
||
command script import _lldbtest/main.py | ||
script main.run_tests("cl/_testdata/debug/out", ["cl/_testdata/debug/in.go"], True, True, None) | ||
quit |