[lldb/script] Add scripting extension template generator (#209647)
This patch adds a `scripting extension generate <ExtensionType>...` command that introspects a Python extension base class and emits a skeleton subclass with `# TODO: Implement` stubs for its abstract methods (or, with `-a`, every method), then opens the result in an editor. Generated imports use `from <module> import <class>`, and the generated `__init__` forwards its arguments to `super().__init__(...)` since every base class relies on its constructor to set up attributes (`self.target`, `self.process`, ...) that inherited, non-overridden methods depend on. When the host can't open an external editor (e.g. non-macOS), the command reports that as a message rather than an error, since the file is already written. To catch regressions in the generator itself instead of just checking that a file was produced, this patch adds `TestScriptingExtensionGenerate.py`, which generates a template for every extension kind on the fly and drives it through a live lldb session (`process launch -C`, `breakpoint set -P`, `target stop-hook add -P`, etc.). Signed-off-by: Med Ismail Bennani <ismail@bennani.ma> (cherry picked from commit c4962f5721a36e198234019b846d3788738f766a)
This commit is contained in:
1 parent
e28ea20ce0
commit
3df0bbdc25
13 files changed
+961
-12
No files matched your search
@@ -687,8 +687,9 @@ public:
|
||||
static ScriptedInterfaceUsages
|
||||
GetScriptedInterfaceUsagesAtIndex(uint32_t idx);
|
||||
|
||||
static void AutoCompleteScriptedExtension(llvm::StringRef partial_name,
|
||||
CompletionRequest &request);
|
||||
static void AutoCompleteScriptedExtension(
|
||||
llvm::StringRef partial_name, CompletionRequest &request,
|
||||
lldb::ScriptLanguage language = lldb::eScriptLanguageUnknown);
|
||||
|
||||
// REPL
|
||||
static bool RegisterPlugin(llvm::StringRef name, llvm::StringRef description,
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
#include "lldb/Utility/Broadcaster.h"
|
||||
#include "lldb/Utility/Status.h"
|
||||
#include "lldb/Utility/StructuredData.h"
|
||||
#include "lldb/Utility/UnimplementedError.h"
|
||||
#include "lldb/lldb-private.h"
|
||||
#include <optional>
|
||||
|
||||
@@ -170,6 +171,25 @@ public:
|
||||
|
||||
virtual StructuredData::DictionarySP GetInterpreterInfo();
|
||||
|
||||
/// Describes one extension to emit into the generated template file.
|
||||
///
|
||||
/// `full_name` is the dotted extension name as the user typed it (e.g.
|
||||
/// `"ScriptedProcess"` or a nested `"...ChildKind"`), used to name the
|
||||
/// generated class. `path` is the same name split on `.`; its last element
|
||||
/// selects the ScriptedExtension kind and everything before it is treated
|
||||
/// as a subclass discriminator. Both StringRefs must outlive the call to
|
||||
/// GenerateExtensionTemplate; the implementation stores them, not copies.
|
||||
struct ExtensionTemplateRequest {
|
||||
llvm::StringRef full_name;
|
||||
llvm::SmallVector<llvm::StringRef> path;
|
||||
};
|
||||
|
||||
virtual llvm::Expected<FileSpec>
|
||||
GenerateExtensionTemplate(const std::string &name,
|
||||
std::vector<ExtensionTemplateRequest> &extensions,
|
||||
bool generate_non_abstract_methods,
|
||||
std::string output_file);
|
||||
|
||||
~ScriptInterpreter() override = default;
|
||||
|
||||
virtual bool Interrupt() { return false; }
|
||||
@@ -510,6 +530,11 @@ public:
|
||||
|
||||
static lldb::ScriptLanguage StringToLanguage(const llvm::StringRef &string);
|
||||
|
||||
virtual llvm::Expected<std::string>
|
||||
ExtensionToImportPath(lldb::ScriptedExtension extension) {
|
||||
return llvm::make_error<UnimplementedError>();
|
||||
}
|
||||
|
||||
static llvm::StringLiteral
|
||||
ExtensionToString(lldb::ScriptedExtension extension);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "lldb/Core/PluginManager.h"
|
||||
#include "lldb/DataFormatters/DataVisualization.h"
|
||||
#include "lldb/Host/Config.h"
|
||||
#include "lldb/Host/Host.h"
|
||||
#include "lldb/Host/OptionParser.h"
|
||||
#include "lldb/Interpreter/CommandInterpreter.h"
|
||||
#include "lldb/Interpreter/CommandOptionArgumentTable.h"
|
||||
@@ -20,6 +21,8 @@
|
||||
#include "lldb/Interpreter/ScriptInterpreter.h"
|
||||
#include "lldb/Utility/AnsiTerminal.h"
|
||||
#include "lldb/Utility/Args.h"
|
||||
#include "lldb/Utility/LLDBLog.h"
|
||||
#include "lldb/Utility/Log.h"
|
||||
|
||||
#include "llvm/ADT/StringMap.h"
|
||||
|
||||
@@ -371,6 +374,188 @@ private:
|
||||
CommandOptions m_options;
|
||||
};
|
||||
|
||||
#define LLDB_OPTIONS_scripting_extension_generate
|
||||
#include "CommandOptions.inc"
|
||||
|
||||
class CommandObjectScriptingExtensionGenerate : public CommandObjectParsed {
|
||||
public:
|
||||
CommandObjectScriptingExtensionGenerate(CommandInterpreter &interpreter)
|
||||
: CommandObjectParsed(interpreter, "scripting extension generate",
|
||||
"Generate a scripting extension template. ",
|
||||
"scripting extension generate") {
|
||||
AddSimpleArgumentList(eArgTypeScriptedExtension, eArgRepeatPlus);
|
||||
}
|
||||
|
||||
~CommandObjectScriptingExtensionGenerate() override = default;
|
||||
|
||||
Options *GetOptions() override { return &m_options; }
|
||||
|
||||
class CommandOptions : public Options {
|
||||
public:
|
||||
CommandOptions() = default;
|
||||
~CommandOptions() override = default;
|
||||
Status SetOptionValue(uint32_t option_idx, llvm::StringRef option_arg,
|
||||
ExecutionContext *execution_context) override {
|
||||
Status error;
|
||||
const char short_option =
|
||||
g_scripting_extension_generate_options[option_idx].short_option;
|
||||
|
||||
switch (short_option) {
|
||||
case 'a':
|
||||
m_generate_non_abstract_methods = true;
|
||||
break;
|
||||
case 'l':
|
||||
m_language = (lldb::ScriptLanguage)OptionArgParser::ToOptionEnum(
|
||||
option_arg, GetDefinitions()[option_idx].enum_values,
|
||||
eScriptLanguageNone, error);
|
||||
if (!error.Success())
|
||||
error = Status::FromErrorStringWithFormatv(
|
||||
"unrecognized value for language '{0}'", option_arg);
|
||||
break;
|
||||
case 'n':
|
||||
m_generated_class_prefix = option_arg.str();
|
||||
break;
|
||||
case 'o':
|
||||
m_output_filepath = option_arg.str();
|
||||
break;
|
||||
case 'e': {
|
||||
bool success;
|
||||
m_open_editor = OptionArgParser::ToBoolean(option_arg, true, &success)
|
||||
? eLazyBoolYes
|
||||
: eLazyBoolNo;
|
||||
if (!success)
|
||||
error = Status::FromErrorStringWithFormatv(
|
||||
"invalid boolean value for -e: '{0}'", option_arg);
|
||||
} break;
|
||||
default:
|
||||
llvm_unreachable("Unimplemented option");
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
void OptionParsingStarting(ExecutionContext *execution_context) override {
|
||||
m_generate_non_abstract_methods = false;
|
||||
m_language = lldb::eScriptLanguageNone;
|
||||
m_generated_class_prefix.clear();
|
||||
m_output_filepath.clear();
|
||||
m_open_editor = eLazyBoolCalculate;
|
||||
}
|
||||
|
||||
llvm::ArrayRef<OptionDefinition> GetDefinitions() override {
|
||||
return llvm::ArrayRef(g_scripting_extension_generate_options);
|
||||
}
|
||||
|
||||
bool m_generate_non_abstract_methods = false;
|
||||
lldb::ScriptLanguage m_language = lldb::eScriptLanguageNone;
|
||||
std::string m_generated_class_prefix;
|
||||
std::string m_output_filepath;
|
||||
LazyBool m_open_editor = eLazyBoolCalculate;
|
||||
};
|
||||
|
||||
void
|
||||
HandleArgumentCompletion(CompletionRequest &request,
|
||||
OptionElementVector &opt_element_vector) override {
|
||||
// If `-l <lang>` was already given, complete only extensions available
|
||||
// in that language. Bare `-l` (no value yet) and any parse failures
|
||||
// fall back to language-agnostic completion.
|
||||
lldb::ScriptLanguage language = lldb::eScriptLanguageUnknown;
|
||||
llvm::ArrayRef<OptionDefinition> defs = m_options.GetDefinitions();
|
||||
for (const OptionArgElement &elem : opt_element_vector) {
|
||||
if (elem.opt_defs_index < 0 ||
|
||||
static_cast<size_t>(elem.opt_defs_index) >= defs.size())
|
||||
continue;
|
||||
if (defs[elem.opt_defs_index].short_option != 'l' ||
|
||||
elem.opt_arg_pos <= 0)
|
||||
continue;
|
||||
llvm::StringRef value =
|
||||
request.GetParsedLine().GetArgumentAtIndex(elem.opt_arg_pos);
|
||||
Status error;
|
||||
auto candidate =
|
||||
static_cast<lldb::ScriptLanguage>(OptionArgParser::ToOptionEnum(
|
||||
value, defs[elem.opt_defs_index].enum_values,
|
||||
lldb::eScriptLanguageUnknown, error));
|
||||
if (error.Success())
|
||||
language = candidate;
|
||||
break;
|
||||
}
|
||||
PluginManager::AutoCompleteScriptedExtension(
|
||||
request.GetCursorArgumentPrefix(), request, language);
|
||||
lldb_private::CommandCompletions::InvokeCommonCompletionCallbacks(
|
||||
GetCommandInterpreter(), lldb::eDiskFileCompletion, request, nullptr);
|
||||
}
|
||||
|
||||
protected:
|
||||
void DoExecute(Args &command, CommandReturnObject &result) override {
|
||||
if (command.GetArgumentCount() == 0) {
|
||||
result.SetError(
|
||||
Status::FromErrorString("specify extension name to generate"));
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<ScriptInterpreter::ExtensionTemplateRequest> extension_requests;
|
||||
|
||||
for (size_t i = 0; i < command.GetArgumentCount(); i++) {
|
||||
llvm::StringRef extension_name = command.GetArgumentAtIndex(i);
|
||||
llvm::SmallVector<llvm::StringRef> extension_components;
|
||||
extension_name.split(extension_components, ".");
|
||||
lldb::ScriptedExtension extension =
|
||||
ScriptInterpreter::StringToExtension(extension_components.back());
|
||||
if (extension == eScriptedExtensionInvalid) {
|
||||
result.SetError(Status::FromErrorStringWithFormatv(
|
||||
"unknown scripted extension: '{0}'", extension_name));
|
||||
return;
|
||||
}
|
||||
extension_requests.push_back({extension_name, extension_components});
|
||||
}
|
||||
|
||||
lldb::ScriptLanguage language =
|
||||
(m_options.m_language == lldb::eScriptLanguageNone)
|
||||
? m_interpreter.GetDebugger().GetScriptLanguage()
|
||||
: m_options.m_language;
|
||||
|
||||
if (language == lldb::eScriptLanguageNone) {
|
||||
result.AppendError(
|
||||
"the script-lang setting is set to none - scripting not available");
|
||||
return;
|
||||
}
|
||||
|
||||
ScriptInterpreter *script_interpreter =
|
||||
GetDebugger().GetScriptInterpreter(true, language);
|
||||
|
||||
if (script_interpreter == nullptr) {
|
||||
result.AppendError("no script interpreter");
|
||||
return;
|
||||
}
|
||||
|
||||
auto generated_file_or_err = script_interpreter->GenerateExtensionTemplate(
|
||||
m_options.m_generated_class_prefix, extension_requests,
|
||||
m_options.m_generate_non_abstract_methods, m_options.m_output_filepath);
|
||||
if (!generated_file_or_err) {
|
||||
result.SetError(generated_file_or_err.takeError());
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_options.m_open_editor != eLazyBoolNo) {
|
||||
if (llvm::Error err = Host::OpenFileInExternalEditor(
|
||||
"", *generated_file_or_err, 1, true)) {
|
||||
// The template is on disk; failing to launch an editor (e.g. no
|
||||
// external editor available on this platform) shouldn't fail the
|
||||
// whole command.
|
||||
LLDB_LOG_ERROR(GetLog(LLDBLog::Host), std::move(err),
|
||||
"OpenFileInExternalEditor failed: {0}");
|
||||
}
|
||||
}
|
||||
result.AppendMessageWithFormatv(
|
||||
"Generated scripting extension template: {0}",
|
||||
generated_file_or_err->GetPath());
|
||||
result.SetStatus(eReturnStatusSuccessFinishNoResult);
|
||||
}
|
||||
|
||||
private:
|
||||
CommandOptions m_options;
|
||||
};
|
||||
|
||||
class CommandObjectMultiwordScriptingExtension : public CommandObjectMultiword {
|
||||
public:
|
||||
CommandObjectMultiwordScriptingExtension(CommandInterpreter &interpreter)
|
||||
@@ -381,6 +566,9 @@ public:
|
||||
LoadSubCommand(
|
||||
"list",
|
||||
CommandObjectSP(new CommandObjectScriptingExtensionList(interpreter)));
|
||||
LoadSubCommand("generate",
|
||||
CommandObjectSP(new CommandObjectScriptingExtensionGenerate(
|
||||
interpreter)));
|
||||
}
|
||||
|
||||
~CommandObjectMultiwordScriptingExtension() override = default;
|
||||
|
||||
@@ -1587,6 +1587,34 @@ let Command = "scripting extension list" in {
|
||||
Desc<"Output the scripted extension list in json format.">;
|
||||
}
|
||||
|
||||
let Command = "scripting extension generate" in {
|
||||
def scripting_extension_generate_all_methods
|
||||
: Option<"all", "a">,
|
||||
Desc<"Generate all the extension methods, not only the abstract ones.">;
|
||||
def scripting_extension_generate_language
|
||||
: Option<"language", "l">,
|
||||
EnumArg<"ScriptLang">,
|
||||
Desc<"Specify the scripting language. If none is specified the "
|
||||
"default scripting language is used.">;
|
||||
def scripting_extension_generate_class_prefix
|
||||
: Option<"name", "n">,
|
||||
Arg<"ClassName">,
|
||||
Required,
|
||||
Desc<"Specify the class prefix name to include in the generated class "
|
||||
"definitions.">;
|
||||
def scripting_extension_generate_output_file
|
||||
: Option<"output", "o">,
|
||||
Arg<"Filename">,
|
||||
Desc<"File location where the extension is generated.">;
|
||||
def scripting_extension_generate_open_editor
|
||||
: Option<"edit", "e">,
|
||||
Arg<"Boolean">,
|
||||
Desc<"Open the generated file in an external editor after writing "
|
||||
"it to disk. Defaults to true; pass `-e false` to skip "
|
||||
"opening the editor (e.g. under SSH without X forwarding, "
|
||||
"or in scripted runs).">;
|
||||
}
|
||||
|
||||
let Command = "source info" in {
|
||||
def source_info_count : Option<"count", "c">,
|
||||
Arg<"Count">,
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "lldb/Utility/Status.h"
|
||||
#include "lldb/Utility/StringList.h"
|
||||
#include "llvm/ADT/StringRef.h"
|
||||
#include "llvm/ADT/StringSet.h"
|
||||
#include "llvm/ADT/Twine.h"
|
||||
#include "llvm/Support/DynamicLibrary.h"
|
||||
#include "llvm/Support/ErrorExtras.h"
|
||||
@@ -2126,16 +2127,27 @@ PluginManager::GetScriptedInterfaceUsagesAtIndex(uint32_t idx) {
|
||||
return {};
|
||||
}
|
||||
|
||||
void PluginManager::AutoCompleteScriptedExtension(llvm::StringRef name,
|
||||
CompletionRequest &request) {
|
||||
void PluginManager::AutoCompleteScriptedExtension(
|
||||
llvm::StringRef name, CompletionRequest &request,
|
||||
lldb::ScriptLanguage language) {
|
||||
llvm::StringSet<> emitted;
|
||||
for (size_t idx = 0; idx < GetNumScriptedInterfaces(); idx++) {
|
||||
if (auto instance =
|
||||
GetScriptedInterfaceInstances().GetInstanceAtIndex(idx)) {
|
||||
llvm::StringLiteral extension_name =
|
||||
ScriptInterpreter::ExtensionToString(instance->extension);
|
||||
if (extension_name.starts_with(name))
|
||||
request.AddCompletion(extension_name);
|
||||
}
|
||||
auto instance = GetScriptedInterfaceInstances().GetInstanceAtIndex(idx);
|
||||
if (!instance)
|
||||
continue;
|
||||
// Filter to the requested language when the caller pinned one via
|
||||
// `-l`. `eScriptLanguageUnknown` means "no filter".
|
||||
if (language != lldb::eScriptLanguageUnknown &&
|
||||
instance->language != language)
|
||||
continue;
|
||||
llvm::StringLiteral extension_name =
|
||||
ScriptInterpreter::ExtensionToString(instance->extension);
|
||||
if (!extension_name.starts_with(name))
|
||||
continue;
|
||||
// A single extension can back multiple languages, so dedup entries
|
||||
// we've already surfaced.
|
||||
if (emitted.insert(extension_name).second)
|
||||
request.AddCompletion(extension_name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "lldb/Utility/Status.h"
|
||||
#include "lldb/Utility/Stream.h"
|
||||
#include "lldb/Utility/StringList.h"
|
||||
#include "lldb/Utility/UnimplementedError.h"
|
||||
#include "lldb/ValueObject/ValueObject.h"
|
||||
#include "llvm/ADT/StringSwitch.h"
|
||||
#if defined(_WIN32)
|
||||
@@ -50,6 +51,12 @@ StructuredData::DictionarySP ScriptInterpreter::GetInterpreterInfo() {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
llvm::Expected<FileSpec> ScriptInterpreter::GenerateExtensionTemplate(
|
||||
const std::string &name, std::vector<ExtensionTemplateRequest> &extensions,
|
||||
bool generate_non_abstract_methods, std::string output_file) {
|
||||
return llvm::make_error<UnimplementedError>();
|
||||
}
|
||||
|
||||
bool ScriptInterpreter::LoadScriptingModule(
|
||||
const char *filename, const LoadScriptOptions &options,
|
||||
lldb_private::Status &error, StructuredData::ObjectSP *module_sp,
|
||||
|
||||
@@ -77,6 +77,131 @@ def run_python_interpreter(local_dict):
|
||||
if e.code:
|
||||
print("Script exited with code %s" % e.code)
|
||||
|
||||
|
||||
def generate_extension_schema(cls):
|
||||
"""Introspect a scripting extension base class and return a JSON schema
|
||||
describing its members. Used by `scripting extension generate` (via
|
||||
`ScriptInterpreterPython::GetExtensionSchema`) to emit a skeleton
|
||||
subclass with `# TODO: Implement` stubs for each method the base class
|
||||
defines. Each method entry carries the signature, type hints,
|
||||
docstring, and whether it's `@abstractmethod`, so the generator can
|
||||
decide which methods to stub out (all of them with `-a`, otherwise
|
||||
just the abstract ones). The schema also lists non-callable
|
||||
attributes the base class exposes -- class-level values plus
|
||||
class-body type annotations -- so the generator can advertise them in
|
||||
the derived class' docstring. A `typing_imports` field enumerates
|
||||
every `typing` generic (`Optional`, `Union`, ...) referenced by the
|
||||
signatures or attribute types, so the generator can add the right
|
||||
`from typing import` line without having to re-scan strings."""
|
||||
import inspect, json, typing
|
||||
|
||||
used_typing = set()
|
||||
|
||||
def _record_typing(s):
|
||||
# Anything from `typing.__all__` referenced as a generic
|
||||
# (`Optional[...]`) gets picked up. Keying off the `[` avoids
|
||||
# matching identifiers that merely embed the name.
|
||||
if not s:
|
||||
return
|
||||
for name in typing.__all__:
|
||||
if f"{name}[" in s:
|
||||
used_typing.add(name)
|
||||
|
||||
def _fmt_type(t):
|
||||
# `type(None)` stringifies as `NoneType`; render it as the
|
||||
# literal `None` so the annotation stays valid Python.
|
||||
if t is type(None):
|
||||
return "None"
|
||||
if isinstance(t, type):
|
||||
if t.__module__ == "builtins":
|
||||
return t.__name__
|
||||
return f"{t.__module__}.{t.__name__}"
|
||||
# `typing` generic aliases stringify with a leading `typing.`
|
||||
# (`typing.Optional[list]`); the module prefix is noise for a
|
||||
# docstring. `Union[int, str, None]` also renders its `None`
|
||||
# component as `NoneType`, so fix that up too.
|
||||
formatted = str(t).replace("typing.", "").replace("NoneType", "None")
|
||||
_record_typing(formatted)
|
||||
return formatted
|
||||
|
||||
def _build_signature(func):
|
||||
# Reconstruct the signature from resolved type hints so forward
|
||||
# refs (`"ScriptedFrame"`) come out as their real class -- what
|
||||
# `inspect.signature(...)`'s own `str` would render as
|
||||
# `ForwardRef('ScriptedFrame')`.
|
||||
try:
|
||||
hints = typing.get_type_hints(func)
|
||||
except Exception:
|
||||
hints = {}
|
||||
sig = inspect.signature(func)
|
||||
parts = []
|
||||
for name, param in sig.parameters.items():
|
||||
piece = name
|
||||
if name in hints:
|
||||
piece += f": {_fmt_type(hints[name])}"
|
||||
if param.default is not inspect.Parameter.empty:
|
||||
piece += f" = {param.default!r}"
|
||||
parts.append(piece)
|
||||
rendered = "(" + ", ".join(parts) + ")"
|
||||
if "return" in hints:
|
||||
rendered += f" -> {_fmt_type(hints['return'])}"
|
||||
return rendered
|
||||
|
||||
def _get_function_metadata(func):
|
||||
try:
|
||||
hints = typing.get_type_hints(func)
|
||||
type_hints = {k: str(v) for k, v in hints.items()}
|
||||
except Exception:
|
||||
type_hints = {}
|
||||
return {
|
||||
"signature": _build_signature(func),
|
||||
"type_hints": type_hints,
|
||||
"is_abstract": getattr(func, "__isabstractmethod__", False),
|
||||
"doc": inspect.getdoc(func),
|
||||
}
|
||||
|
||||
try:
|
||||
class_hints = typing.get_type_hints(cls)
|
||||
except Exception:
|
||||
class_hints = {}
|
||||
|
||||
members = []
|
||||
attributes = []
|
||||
seen_attrs = set()
|
||||
for name, member in inspect.getmembers(cls):
|
||||
if inspect.isfunction(member):
|
||||
members.append({"name": name, **_get_function_metadata(member)})
|
||||
continue
|
||||
if name.startswith("_"):
|
||||
continue
|
||||
entry = {"name": name}
|
||||
if name in class_hints:
|
||||
entry["type"] = _fmt_type(class_hints[name])
|
||||
attributes.append(entry)
|
||||
seen_attrs.add(name)
|
||||
|
||||
# Class-body type annotations without a runtime value
|
||||
# (`target: SBTarget`) don't show up in `inspect.getmembers`, so pick
|
||||
# them up from the hint map directly.
|
||||
for name in class_hints:
|
||||
if name.startswith("_") or name in seen_attrs:
|
||||
continue
|
||||
attributes.append({"name": name, "type": _fmt_type(class_hints[name])})
|
||||
seen_attrs.add(name)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"class": cls.__name__,
|
||||
"module": cls.__module__,
|
||||
"doc": inspect.getdoc(cls),
|
||||
"members": members,
|
||||
"attributes": attributes,
|
||||
"typing_imports": sorted(used_typing),
|
||||
},
|
||||
separators=(",", ":"),
|
||||
)
|
||||
|
||||
|
||||
def run_one_line(local_dict, input_string):
|
||||
global g_run_one_line_str
|
||||
try:
|
||||
|
||||
@@ -251,10 +251,15 @@ public:
|
||||
size_t num_args = sizeof...(Args);
|
||||
if (arg_info->max_positional_args != PythonCallable::ArgInfo::UNBOUNDED &&
|
||||
num_args != arg_info->max_positional_args) {
|
||||
if (num_args != arg_info->max_positional_args - 1)
|
||||
if (num_args != arg_info->max_positional_args - 1) {
|
||||
// `expected_return_object` starts in an error state; consume it
|
||||
// before we return with a different error, or its destructor
|
||||
// will abort.
|
||||
llvm::consumeError(expected_return_object.takeError());
|
||||
return create_error("Passed arguments ({0}) doesn't match the number "
|
||||
"of expected arguments ({1}).",
|
||||
num_args, arg_info->max_positional_args);
|
||||
}
|
||||
|
||||
std::apply(
|
||||
[&init, &expected_return_object](auto &&...args) {
|
||||
|
||||
@@ -36,11 +36,13 @@
|
||||
#include "lldb/Target/ThreadPlan.h"
|
||||
#include "lldb/Utility/Instrumentation.h"
|
||||
#include "lldb/Utility/LLDBLog.h"
|
||||
#include "lldb/Utility/StructuredData.h"
|
||||
#include "lldb/Utility/Timer.h"
|
||||
#include "lldb/ValueObject/ValueObject.h"
|
||||
#include "lldb/lldb-enumerations.h"
|
||||
#include "lldb/lldb-forward.h"
|
||||
#include "llvm/ADT/STLExtras.h"
|
||||
#include "llvm/ADT/StringExtras.h"
|
||||
#include "llvm/ADT/StringRef.h"
|
||||
#include "llvm/Support/Error.h"
|
||||
#include "llvm/Support/ErrorExtras.h"
|
||||
@@ -265,6 +267,363 @@ StructuredData::DictionarySP ScriptInterpreterPython::GetInterpreterInfo() {
|
||||
return info_json.CreateStructuredDictionary();
|
||||
}
|
||||
|
||||
llvm::Expected<std::string> ScriptInterpreterPython::ExtensionToImportPath(
|
||||
lldb::ScriptedExtension extension) {
|
||||
switch (extension) {
|
||||
case eScriptedExtensionOperatingSystem:
|
||||
return "lldb.plugins.operating_system";
|
||||
case eScriptedExtensionScriptedPlatform:
|
||||
return "lldb.plugins.scripted_platform";
|
||||
case eScriptedExtensionScriptedProcess:
|
||||
return "lldb.plugins.scripted_process";
|
||||
case eScriptedExtensionScriptedHook:
|
||||
return "lldb.plugins.scripted_hook";
|
||||
case eScriptedExtensionScriptedBreakpointResolver:
|
||||
return "lldb.plugins.scripted_breakpoint";
|
||||
case eScriptedExtensionScriptedThreadPlan:
|
||||
return "lldb.plugins.scripted_thread_plan";
|
||||
case eScriptedExtensionScriptedFrameProvider:
|
||||
return "lldb.plugins.scripted_frame_provider";
|
||||
case eScriptedExtensionScriptedThread:
|
||||
case eScriptedExtensionScriptedFrame:
|
||||
return "lldb.plugins.scripted_process";
|
||||
case eScriptedExtensionScriptedStackFrameRecognizer:
|
||||
return "lldb.plugins.scripted_stackframe_recognizer";
|
||||
case eScriptedExtensionInvalid:
|
||||
return llvm::createStringError("invalid extension name");
|
||||
}
|
||||
return llvm::createStringError("invalid extension name");
|
||||
}
|
||||
|
||||
llvm::Expected<StructuredData::ObjectSP>
|
||||
ScriptInterpreterPython::GetExtensionSchema(
|
||||
const llvm::SmallVector<llvm::StringRef> &extension_path) {
|
||||
lldb::ScriptedExtension extension =
|
||||
ScriptInterpreter::StringToExtension(extension_path.back());
|
||||
auto import_path_or_err = ExtensionToImportPath(extension);
|
||||
if (!import_path_or_err)
|
||||
return import_path_or_err.takeError();
|
||||
|
||||
StreamString command_stream;
|
||||
// __import__(path, fromlist=['']) imports the submodule and returns it
|
||||
// directly (rather than the top-level package), as a single expression --
|
||||
// this keeps the whole call eval-able in one line while guaranteeing the
|
||||
// module is imported first; referencing "<import_path>.<ClassName>"
|
||||
// directly would only work if something else had already imported
|
||||
// <import_path> as a side effect.
|
||||
command_stream.Printf("lldb.embedded_interpreter.generate_extension_schema("
|
||||
"__import__('%s', fromlist=['']).%s)",
|
||||
import_path_or_err->c_str(),
|
||||
ScriptInterpreter::ExtensionToString(extension).data());
|
||||
|
||||
// Use eScriptReturnTypeOpaqueObject: it transfers a real owned reference
|
||||
// we can safely extract the string from. eScriptReturnTypeCharStrOrNone
|
||||
// instead hands back a pointer to a temporary Python object's buffer
|
||||
// that gets destroyed (and, for a freshly created string like this one,
|
||||
// deallocated) as soon as ExecuteOneLineWithReturn returns -- reading
|
||||
// it afterwards is a use-after-free.
|
||||
void *result_obj = nullptr;
|
||||
if (!ExecuteOneLineWithReturn(
|
||||
command_stream.GetData(),
|
||||
ScriptInterpreter::eScriptReturnTypeOpaqueObject, &result_obj,
|
||||
ExecuteScriptOptions().SetEnableIO(false)))
|
||||
return llvm::createStringError("invalid extension schema format");
|
||||
|
||||
// ExecuteOneLineWithReturn releases the GIL before returning, so touching
|
||||
// the returned object (Str() below can execute arbitrary Python code) must
|
||||
// re-acquire it first. py_result is scoped so its destructor (a DECREF)
|
||||
// also runs before the GIL is released below, not after.
|
||||
std::string schema_str;
|
||||
{
|
||||
PyGILState_STATE gil_state = PyGILState_Ensure();
|
||||
{
|
||||
PythonObject py_result(PyRefType::Owned,
|
||||
static_cast<PyObject *>(result_obj));
|
||||
if (py_result.IsAllocated() && py_result.get() != Py_None)
|
||||
schema_str = py_result.Str().GetString().str();
|
||||
}
|
||||
PyGILState_Release(gil_state);
|
||||
}
|
||||
|
||||
if (schema_str.empty())
|
||||
return llvm::createStringError("empty extension schema");
|
||||
return StructuredData::ParseJSON(schema_str);
|
||||
}
|
||||
|
||||
llvm::Error ScriptInterpreterPython::ParseExtensionSchema(
|
||||
Stream &s, llvm::StringRef output_script_prefix,
|
||||
const llvm::SmallVector<llvm::StringRef> &extension_path,
|
||||
bool generate_non_abstract_methods, std::set<std::string> &typing_imports) {
|
||||
auto schema_or_err = GetExtensionSchema(extension_path);
|
||||
if (!schema_or_err)
|
||||
return schema_or_err.takeError();
|
||||
|
||||
StructuredData::ObjectSP schema = *schema_or_err;
|
||||
if (!schema)
|
||||
return llvm::createStringError("empty extension schema");
|
||||
StructuredData::Dictionary *dict = schema->GetAsDictionary();
|
||||
if (!dict)
|
||||
return llvm::createStringError("extension schema is not a JSON object");
|
||||
|
||||
// Merge each class' typing imports into the caller-owned set so the
|
||||
// final `from typing import ...` line covers every class we emit.
|
||||
StructuredData::Array *schema_typing;
|
||||
if (dict->GetValueForKeyAsArray("typing_imports", schema_typing))
|
||||
schema_typing->ForEach([&](StructuredData::Object *entry) {
|
||||
if (auto *str = entry->GetAsString())
|
||||
typing_imports.insert(str->GetValue().str());
|
||||
return true;
|
||||
});
|
||||
|
||||
llvm::StringRef base_class, import_path;
|
||||
if (!dict->GetValueForKeyAsString("class", base_class))
|
||||
return llvm::createStringError(
|
||||
llvm::formatv("extension schema dictionary is missing 'class' key")
|
||||
.str());
|
||||
if (!dict->GetValueForKeyAsString("module", import_path))
|
||||
return llvm::createStringError(
|
||||
llvm::formatv("extension schema dictionary is missing 'module' key")
|
||||
.str());
|
||||
|
||||
// imports
|
||||
s.Printf("from %s import %s\n", import_path.data(), base_class.data());
|
||||
s.EOL();
|
||||
|
||||
// class definition
|
||||
s.Printf("class %s%s(%s):\n", output_script_prefix.data(), base_class.data(),
|
||||
base_class.data());
|
||||
s.IndentMore();
|
||||
|
||||
// Class docstring: list the non-callable members the base class exposes
|
||||
// so the user sees what's available without having to hop back to the
|
||||
// base class definition.
|
||||
bool has_body = false;
|
||||
StructuredData::Array *attributes;
|
||||
if (dict->GetValueForKeyAsArray("attributes", attributes) &&
|
||||
attributes->GetSize()) {
|
||||
s.Indent();
|
||||
s.PutCString("\"\"\"\n");
|
||||
s.Indent();
|
||||
s.Printf("Attributes inherited from %s:\n", base_class.data());
|
||||
for (size_t i = 0; i < attributes->GetSize(); i++) {
|
||||
auto maybe_dict = attributes->GetItemAtIndexAsDictionary(i);
|
||||
if (!maybe_dict)
|
||||
continue;
|
||||
StructuredData::Dictionary *attr_dict = *maybe_dict;
|
||||
llvm::StringRef attr_name;
|
||||
if (!attr_dict->GetValueForKeyAsString("name", attr_name))
|
||||
continue;
|
||||
llvm::StringRef attr_type;
|
||||
bool has_type = attr_dict->GetValueForKeyAsString("type", attr_type);
|
||||
s.Indent();
|
||||
s.Printf("- %s", attr_name.data());
|
||||
if (has_type)
|
||||
s.Printf(": %s", attr_type.data());
|
||||
s.EOL();
|
||||
}
|
||||
s.Indent();
|
||||
s.PutCString("\"\"\"\n\n");
|
||||
has_body = true;
|
||||
}
|
||||
|
||||
// members
|
||||
StructuredData::Array *members;
|
||||
if (!dict->GetValueForKeyAsArray("members", members))
|
||||
return llvm::createStringError("missing 'members' key in extension schema");
|
||||
|
||||
// If the base class doesn't mark anything `@abstractmethod`, the filter
|
||||
// "only stub abstract methods" would leave the derived class empty --
|
||||
// which isn't a useful starting point. Fall back to emitting every
|
||||
// method in that case so the user has actual code to edit.
|
||||
bool any_abstract = false;
|
||||
for (size_t i = 0; i < members->GetSize(); i++) {
|
||||
auto maybe_dict = members->GetItemAtIndexAsDictionary(i);
|
||||
if (!maybe_dict)
|
||||
continue;
|
||||
bool is_abstract = false;
|
||||
if ((*maybe_dict)->GetValueForKeyAsBoolean("is_abstract", is_abstract) &&
|
||||
is_abstract) {
|
||||
any_abstract = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
bool emit_all_methods = generate_non_abstract_methods || !any_abstract;
|
||||
|
||||
for (size_t i = 0; i < members->GetSize(); i++) {
|
||||
auto maybe_dict = members->GetItemAtIndexAsDictionary(i);
|
||||
if (!maybe_dict)
|
||||
return llvm::createStringError(
|
||||
llvm::formatv(
|
||||
"member at index {0} in extension schema isn't a dictionary")
|
||||
.str());
|
||||
|
||||
StructuredData::Dictionary *member_dict = *maybe_dict;
|
||||
llvm::StringRef symbol, args;
|
||||
if (!member_dict->GetValueForKeyAsString("name", symbol))
|
||||
return llvm::createStringError(
|
||||
llvm::formatv(
|
||||
"member at index {0} in extension schema is missing 'name' key")
|
||||
.str());
|
||||
if (!member_dict->GetValueForKeyAsString("signature", args))
|
||||
return llvm::createStringError(
|
||||
llvm::formatv("member at index {0} in extension schema is missing "
|
||||
"'signature' key")
|
||||
.str());
|
||||
|
||||
bool is_abstract = false;
|
||||
bool has_is_abstract =
|
||||
member_dict->GetValueForKeyAsBoolean("is_abstract", is_abstract);
|
||||
if (!emit_all_methods)
|
||||
if (!has_is_abstract || !is_abstract)
|
||||
continue;
|
||||
|
||||
s.Indent();
|
||||
s.Printf("def %s%s:\n", symbol.data(), args.data());
|
||||
|
||||
s.IndentMore();
|
||||
llvm::StringRef documentation;
|
||||
if (member_dict->GetValueForKeyAsString("doc", documentation)) {
|
||||
s.Indent();
|
||||
s.PutCString("\"\"\"\n");
|
||||
|
||||
llvm::SmallVector<llvm::StringRef> lines;
|
||||
documentation.split(lines, "\n");
|
||||
|
||||
for (llvm::StringRef line : lines) {
|
||||
s.Indent();
|
||||
s.PutCString(line);
|
||||
s.EOL();
|
||||
}
|
||||
|
||||
s.Indent();
|
||||
s.PutCString("\"\"\"");
|
||||
s.EOL();
|
||||
}
|
||||
|
||||
if (symbol == "__init__") {
|
||||
// The base class' constructor sets up attributes (e.g. self.target,
|
||||
// self.process) that the inherited, non-overridden methods rely on.
|
||||
// Forward the same arguments so that state is still initialized.
|
||||
// Splitting the param list on `,` requires bracket-depth awareness
|
||||
// because annotations like `Union[X, Y]` also contain commas.
|
||||
llvm::StringRef params = args.trim("()");
|
||||
std::vector<std::string> forwarded_args;
|
||||
int depth = 0;
|
||||
size_t start = 0;
|
||||
auto flush = [&](size_t end) {
|
||||
llvm::StringRef param = params.slice(start, end);
|
||||
param = param.split(':').first.split('=').first.trim();
|
||||
if (!param.empty() && param != "self")
|
||||
forwarded_args.push_back(param.str());
|
||||
};
|
||||
for (size_t i = 0; i < params.size(); ++i) {
|
||||
char c = params[i];
|
||||
if (c == '[' || c == '(' || c == '{')
|
||||
++depth;
|
||||
else if (c == ']' || c == ')' || c == '}')
|
||||
--depth;
|
||||
else if (c == ',' && depth == 0) {
|
||||
flush(i);
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
flush(params.size());
|
||||
s.Indent();
|
||||
s.Printf("super().__init__(%s)\n",
|
||||
llvm::join(forwarded_args, ", ").c_str());
|
||||
}
|
||||
|
||||
s.Indent();
|
||||
s.PutCString("# TODO: Implement\n");
|
||||
s.Indent();
|
||||
s.PutCString("pass\n\n");
|
||||
s.IndentLess();
|
||||
has_body = true;
|
||||
}
|
||||
|
||||
// A class with no body is a Python syntax error, so emit `pass` when the
|
||||
// base class has nothing to stub out (no methods and no attributes to
|
||||
// document).
|
||||
if (!has_body) {
|
||||
s.Indent();
|
||||
s.PutCString("pass\n");
|
||||
}
|
||||
|
||||
return llvm::Error::success();
|
||||
}
|
||||
|
||||
llvm::Expected<FileSpec> ScriptInterpreterPython::GenerateExtensionTemplate(
|
||||
const std::string &name, std::vector<ExtensionTemplateRequest> &extensions,
|
||||
bool generate_non_abstract_methods, std::string output_file) {
|
||||
// `ParseExtensionSchema` accumulates every `typing` generic it sees
|
||||
// (`Optional`, `Union`, `List`, ...) into this set so we can emit a
|
||||
// targeted `from typing import ...` line only for what's actually
|
||||
// referenced. The Python schema does the detection so we don't have
|
||||
// to re-scan strings here.
|
||||
std::set<std::string> typing_imports;
|
||||
StreamString bodies;
|
||||
for (const ExtensionTemplateRequest &extension : extensions) {
|
||||
if (llvm::Error err =
|
||||
ParseExtensionSchema(bodies, name, extension.path,
|
||||
generate_non_abstract_methods, typing_imports))
|
||||
return std::move(err);
|
||||
bodies.PutCString("\n\n");
|
||||
}
|
||||
|
||||
StreamString generated_file_stream;
|
||||
generated_file_stream.PutCString("import lldb\n");
|
||||
if (!typing_imports.empty()) {
|
||||
std::vector<std::string> sorted_imports(typing_imports.begin(),
|
||||
typing_imports.end());
|
||||
generated_file_stream.Format("from typing import {0}\n",
|
||||
llvm::join(sorted_imports, ", "));
|
||||
}
|
||||
generated_file_stream.PutCString("\n");
|
||||
generated_file_stream.PutCString(bodies.GetString());
|
||||
|
||||
FileSpec save_location;
|
||||
if (output_file.empty()) {
|
||||
// Sanitize the caller-supplied class prefix so it can't escape the
|
||||
// temp directory (`../`, path separators, ...). Only keep ASCII
|
||||
// alphanumerics; everything else collapses to `_`, and an all-junk
|
||||
// name falls back to a fixed default.
|
||||
std::string sanitized;
|
||||
sanitized.reserve(name.size());
|
||||
for (char c : name)
|
||||
sanitized.push_back(llvm::isAlnum(c) ? static_cast<char>(llvm::toLower(c))
|
||||
: '_');
|
||||
if (sanitized.find_first_not_of('_') == std::string::npos)
|
||||
sanitized = "extension";
|
||||
const std::string file_name = "lldb_" + sanitized + "_extension.py";
|
||||
save_location = HostInfo::GetGlobalTempDir();
|
||||
FileSystem::Instance().Resolve(save_location);
|
||||
save_location.AppendPathComponent(file_name);
|
||||
} else {
|
||||
save_location = FileSpec(output_file);
|
||||
FileSystem::Instance().Resolve(save_location);
|
||||
}
|
||||
|
||||
File::OpenOptions flags = File::eOpenOptionWriteOnly |
|
||||
File::eOpenOptionCanCreate |
|
||||
File::eOpenOptionTruncate;
|
||||
|
||||
auto opened_file = FileSystem::Instance().Open(save_location, flags);
|
||||
|
||||
if (!opened_file)
|
||||
return opened_file.takeError();
|
||||
|
||||
FileUP file = std::move(opened_file.get());
|
||||
|
||||
size_t byte_size = generated_file_stream.GetSize();
|
||||
|
||||
Status error = file->Write(generated_file_stream.GetData(), byte_size);
|
||||
|
||||
if (error.Fail() || byte_size != generated_file_stream.GetSize())
|
||||
return llvm::createStringError("Unable to write to destination file. Bytes "
|
||||
"written do not match generated file size.");
|
||||
return save_location;
|
||||
}
|
||||
|
||||
void ScriptInterpreterPython::SharedLibraryDirectoryHelper(
|
||||
FileSpec &this_file) {
|
||||
// When we're loaded from python, this_file will point to the file inside the
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "lldb/lldb-private.h"
|
||||
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -41,7 +42,15 @@ public:
|
||||
: ScriptInterpreter(debugger, lldb::eScriptLanguagePython),
|
||||
IOHandlerDelegateMultiline("DONE") {}
|
||||
|
||||
llvm::Expected<std::string>
|
||||
ExtensionToImportPath(lldb::ScriptedExtension extension) override;
|
||||
StructuredData::DictionarySP GetInterpreterInfo() override;
|
||||
llvm::Expected<FileSpec>
|
||||
GenerateExtensionTemplate(const std::string &name,
|
||||
std::vector<ExtensionTemplateRequest> &extensions,
|
||||
bool generate_non_abstract_methods,
|
||||
std::string output_file) override;
|
||||
|
||||
static void Initialize();
|
||||
static void Terminate();
|
||||
static llvm::StringRef GetPluginNameStatic() { return "script-python"; }
|
||||
@@ -50,6 +59,14 @@ public:
|
||||
static void SharedLibraryDirectoryHelper(FileSpec &this_file);
|
||||
|
||||
protected:
|
||||
llvm::Error
|
||||
ParseExtensionSchema(Stream &s, llvm::StringRef output_script_prefix,
|
||||
const llvm::SmallVector<llvm::StringRef> &extension_path,
|
||||
bool generate_non_abstract_methods,
|
||||
std::set<std::string> &typing_imports);
|
||||
llvm::Expected<StructuredData::ObjectSP>
|
||||
GetExtensionSchema(const llvm::SmallVector<llvm::StringRef> &extension_path);
|
||||
|
||||
static void ComputePythonDirForApple(llvm::SmallVectorImpl<char> &path);
|
||||
static void ComputePythonDir(llvm::SmallVectorImpl<char> &path);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
C_SOURCES := main.c
|
||||
|
||||
include Makefile.rules
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
"""
|
||||
Meta-test: generate every kind of scripting extension template on the fly via
|
||||
`scripting extension generate`, then actually register and drive each one
|
||||
through a live lldb debug session, to catch regressions in the generator
|
||||
itself (e.g. broken imports, wrong base-class name, missing __init__
|
||||
chaining) rather than just checking that a Python file was produced.
|
||||
|
||||
The stub methods lldb calls into may legitimately report failure (e.g. an
|
||||
`is_alive` stub that returns None can make `process launch` report an
|
||||
error), so success/failure of the driving command is not asserted. What is
|
||||
asserted is that lldb never surfaces an unhandled Python traceback while
|
||||
running the generated code.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import lldb
|
||||
from lldbsuite.test.decorators import *
|
||||
from lldbsuite.test.lldbtest import *
|
||||
from lldbsuite.test import lldbutil
|
||||
|
||||
|
||||
class TestScriptingExtensionGenerate(TestBase):
|
||||
NO_DEBUG_INFO_TESTCASE = True
|
||||
|
||||
def generate_extension(self, kind, generate_all=False):
|
||||
"""Run `scripting extension generate` for `kind`, returning the path
|
||||
to the generated file. Fails the test if generation errors out."""
|
||||
output_path = self.getBuildArtifact(f"generated_{kind}.py")
|
||||
if os.path.exists(output_path):
|
||||
os.remove(output_path)
|
||||
|
||||
all_flag = " -a" if generate_all else ""
|
||||
self.runCmd(
|
||||
f"scripting extension generate{all_flag} -n Test -o {output_path} {kind}"
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
os.path.exists(output_path),
|
||||
f"scripting extension generate did not create {output_path}",
|
||||
)
|
||||
return output_path
|
||||
|
||||
def run_and_assert_no_traceback(self, cmd):
|
||||
"""Run `cmd` and fail the test if lldb surfaced an unhandled Python
|
||||
traceback while executing it. The command itself may still report
|
||||
failure (e.g. a stub method returning None) without that being a
|
||||
generator bug."""
|
||||
self.runCmd(cmd, check=False)
|
||||
output = self.res.GetOutput() + self.res.GetError()
|
||||
self.assertNotIn(
|
||||
"Traceback (most recent call last)",
|
||||
output,
|
||||
f"command {cmd!r} raised a Python exception:\n{output}",
|
||||
)
|
||||
|
||||
def test_generate_operating_system(self):
|
||||
"""OperatingSystem: generate, import, and activate via the
|
||||
target.process.python-os-plugin-path setting."""
|
||||
self.build()
|
||||
path = self.generate_extension("OperatingSystem")
|
||||
self.run_and_assert_no_traceback(f"command script import {path}")
|
||||
|
||||
lldbutil.run_to_source_breakpoint(self, "break here", lldb.SBFileSpec("main.c"))
|
||||
|
||||
self.run_and_assert_no_traceback(
|
||||
f"settings set target.process.python-os-plugin-path {path}"
|
||||
)
|
||||
self.run_and_assert_no_traceback("thread list")
|
||||
self.runCmd("settings clear target.process.python-os-plugin-path", check=False)
|
||||
|
||||
def test_generate_scripted_platform(self):
|
||||
"""ScriptedPlatform: generate, import, and select via `platform
|
||||
select`."""
|
||||
path = self.generate_extension("ScriptedPlatform")
|
||||
self.run_and_assert_no_traceback(f"command script import {path}")
|
||||
|
||||
self.run_and_assert_no_traceback(
|
||||
"platform select scripted-platform -C "
|
||||
"generated_ScriptedPlatform.TestScriptedPlatform"
|
||||
)
|
||||
|
||||
def test_generate_scripted_process(self):
|
||||
"""ScriptedProcess: generate, import, and launch via `process launch
|
||||
-C`."""
|
||||
self.build()
|
||||
path = self.generate_extension("ScriptedProcess")
|
||||
self.run_and_assert_no_traceback(f"command script import {path}")
|
||||
|
||||
target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
|
||||
self.assertTrue(target, VALID_TARGET)
|
||||
|
||||
self.run_and_assert_no_traceback(
|
||||
"process launch -C generated_ScriptedProcess.TestScriptedProcess"
|
||||
)
|
||||
|
||||
def test_generate_scripted_thread_plan(self):
|
||||
"""ScriptedThreadPlan: generate, import, and drive via `thread
|
||||
step-scripted -C`."""
|
||||
self.build()
|
||||
path = self.generate_extension("ScriptedThreadPlan")
|
||||
self.run_and_assert_no_traceback(f"command script import {path}")
|
||||
|
||||
lldbutil.run_to_source_breakpoint(self, "break here", lldb.SBFileSpec("main.c"))
|
||||
|
||||
self.run_and_assert_no_traceback(
|
||||
"thread step-scripted -C "
|
||||
"generated_ScriptedThreadPlan.TestScriptedThreadPlan"
|
||||
)
|
||||
|
||||
def test_generate_scripted_breakpoint_resolver(self):
|
||||
"""ScriptedBreakpointResolver: generate, import, and set via
|
||||
`breakpoint set -P`."""
|
||||
self.build()
|
||||
path = self.generate_extension("ScriptedBreakpointResolver")
|
||||
self.run_and_assert_no_traceback(f"command script import {path}")
|
||||
|
||||
target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
|
||||
self.assertTrue(target, VALID_TARGET)
|
||||
|
||||
self.run_and_assert_no_traceback(
|
||||
"breakpoint set -P "
|
||||
"generated_ScriptedBreakpointResolver.TestScriptedBreakpointResolver"
|
||||
)
|
||||
|
||||
def test_generate_scripted_hook_for_stop_hook(self):
|
||||
"""ScriptedHook drives `target stop-hook add -P` (the stop-hook is a
|
||||
subset of the general scripted hook interface)."""
|
||||
self.build()
|
||||
path = self.generate_extension("ScriptedHook")
|
||||
self.run_and_assert_no_traceback(f"command script import {path}")
|
||||
|
||||
target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
|
||||
self.assertTrue(target, VALID_TARGET)
|
||||
|
||||
self.run_and_assert_no_traceback(
|
||||
"target stop-hook add -P generated_ScriptedHook.TestScriptedHook"
|
||||
)
|
||||
|
||||
def test_generate_scripted_frame_provider(self):
|
||||
"""ScriptedFrameProvider: generate, import, and register via `target
|
||||
frame-provider register -C`. Uses -a to generate all methods since
|
||||
`get_description` is a required static abstract method."""
|
||||
self.build()
|
||||
path = self.generate_extension("ScriptedFrameProvider", generate_all=True)
|
||||
self.run_and_assert_no_traceback(f"command script import {path}")
|
||||
|
||||
lldbutil.run_to_source_breakpoint(self, "break here", lldb.SBFileSpec("main.c"))
|
||||
|
||||
self.run_and_assert_no_traceback(
|
||||
"target frame-provider register -C "
|
||||
"generated_ScriptedFrameProvider.TestScriptedFrameProvider"
|
||||
)
|
||||
|
||||
def test_generate_scripted_hook(self):
|
||||
"""ScriptedHook: generate, import, and register via `target hook add
|
||||
-P`."""
|
||||
self.build()
|
||||
path = self.generate_extension("ScriptedHook")
|
||||
self.run_and_assert_no_traceback(f"command script import {path}")
|
||||
|
||||
target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
|
||||
self.assertTrue(target, VALID_TARGET)
|
||||
|
||||
self.run_and_assert_no_traceback(
|
||||
"target hook add -P generated_ScriptedHook.TestScriptedHook"
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
#include <stdio.h>
|
||||
|
||||
int foo(int x) {
|
||||
int y = x + 1; // break here
|
||||
return y;
|
||||
}
|
||||
|
||||
int main() {
|
||||
int result = foo(41);
|
||||
printf("%d\n", result);
|
||||
return 0;
|
||||
}
|
||||
Reference in new issue
Block a user