| // Copyright 2018 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "src/developer/debug/zxdb/console/input_location_parser.h" |
| |
| #include <gtest/gtest.h> |
| |
| #include "src/developer/debug/zxdb/client/mock_frame.h" |
| #include "src/developer/debug/zxdb/client/mock_process.h" |
| #include "src/developer/debug/zxdb/client/mock_target.h" |
| #include "src/developer/debug/zxdb/client/mock_thread.h" |
| #include "src/developer/debug/zxdb/client/session.h" |
| #include "src/developer/debug/zxdb/common/test_with_loop.h" |
| #include "src/developer/debug/zxdb/console/command.h" |
| #include "src/developer/debug/zxdb/expr/mock_eval_context.h" |
| #include "src/developer/debug/zxdb/symbols/collection.h" |
| #include "src/developer/debug/zxdb/symbols/function.h" |
| #include "src/developer/debug/zxdb/symbols/index_test_support.h" |
| #include "src/developer/debug/zxdb/symbols/location.h" |
| #include "src/developer/debug/zxdb/symbols/mock_module_symbols.h" |
| #include "src/developer/debug/zxdb/symbols/modified_type.h" |
| #include "src/developer/debug/zxdb/symbols/namespace.h" |
| #include "src/developer/debug/zxdb/symbols/process_symbols_test_setup.h" |
| #include "src/developer/debug/zxdb/symbols/symbol_test_parent_setter.h" |
| |
| namespace zxdb { |
| |
| namespace { |
| |
| struct EvalResult { |
| ErrOr<InputLocation> loc = ErrOr<InputLocation>(Err("<test hardness>: callback not issued>")); |
| std::optional<uint32_t> size; |
| }; |
| |
| class InputLocationParserTest : public TestWithLoop { |
| public: |
| void SetUp() override { mock_module_symbols_ = symbols_.InjectMockModule(); } |
| |
| EvalResult SyncEvalGlobalInputLocation(const fxl::RefPtr<EvalContext> eval_context, |
| const Location& location, const std::string& input) { |
| bool called = false; |
| EvalResult result; |
| |
| EvalGlobalInputLocation( |
| eval_context, location, input, |
| [&called, &result](ErrOr<InputLocation> value, std::optional<uint32_t> size) { |
| called = true; |
| result.loc = std::move(value); |
| result.size = size; |
| }); |
| loop().RunUntilNoTasks(); |
| EXPECT_TRUE(called); |
| return result; |
| } |
| |
| protected: |
| ProcessSymbolsTestSetup symbols_; |
| MockModuleSymbols* mock_module_symbols_ = nullptr; |
| SymbolContext symbol_context_ = SymbolContext(ProcessSymbolsTestSetup::kDefaultLoadAddress); |
| }; |
| |
| } // namespace |
| |
| TEST_F(InputLocationParserTest, EvalGlobalInputLocation) { |
| auto eval_context = fxl::MakeRefCounted<MockEvalContext>(); |
| Location existing_location; |
| |
| // Valid symbol (including colons). |
| auto result = SyncEvalGlobalInputLocation(eval_context, existing_location, "Foo::Bar"); |
| ASSERT_TRUE(result.loc.ok()) << result.loc.err().msg(); |
| EXPECT_FALSE(result.size); |
| EXPECT_EQ(InputLocation::Type::kName, result.loc.value().type); |
| EXPECT_EQ(R"("Foo"; ::"Bar")", result.loc.value().name.GetDebugName()); |
| |
| // Valid file/line. |
| result = SyncEvalGlobalInputLocation(eval_context, existing_location, "foo/bar.cc:123"); |
| ASSERT_TRUE(result.loc.ok()) << result.loc.err().msg(); |
| EXPECT_FALSE(result.size); |
| EXPECT_EQ(InputLocation::Type::kLine, result.loc.value().type); |
| EXPECT_EQ("foo/bar.cc", result.loc.value().line.file()); |
| EXPECT_EQ(123, result.loc.value().line.line()); |
| |
| // Invalid file/line. |
| result = SyncEvalGlobalInputLocation(eval_context, existing_location, "foo/bar.cc:123x"); |
| EXPECT_TRUE(result.loc.has_error()); |
| |
| // File and line empty. |
| result = SyncEvalGlobalInputLocation(eval_context, existing_location, ":"); |
| EXPECT_TRUE(result.loc.has_error()); |
| EXPECT_EQ(result.loc.err().msg(), "Unexpected token ':'."); |
| |
| // Empty line number. |
| result = SyncEvalGlobalInputLocation(eval_context, existing_location, "foo/bar.cc:"); |
| ASSERT_TRUE(result.loc.has_error()); |
| // This message is a bit odd, it would be better if it described the missing number. |
| EXPECT_EQ(result.loc.err().msg(), "Unexpected input, did you forget an operator?"); |
| |
| // 0 line number. |
| result = SyncEvalGlobalInputLocation(eval_context, existing_location, "foo/bar.cc:0"); |
| ASSERT_TRUE(result.loc.has_error()); |
| EXPECT_EQ(result.loc.err().msg(), "Can't have a 0 line number."); |
| |
| // Valid hex address with *. |
| result = SyncEvalGlobalInputLocation(eval_context, existing_location, "*0x12345f"); |
| ASSERT_TRUE(result.loc.ok()) << result.loc.err().msg(); |
| EXPECT_FALSE(result.size); |
| EXPECT_EQ(InputLocation::Type::kAddress, result.loc.value().type); |
| EXPECT_EQ(0x12345fu, result.loc.value().address); |
| |
| // Valid hex address without a *. |
| result = SyncEvalGlobalInputLocation(eval_context, existing_location, "0x12345f"); |
| ASSERT_TRUE(result.loc.ok()) << result.loc.err().msg(); |
| EXPECT_FALSE(result.size); |
| EXPECT_EQ(InputLocation::Type::kAddress, result.loc.value().type); |
| EXPECT_EQ(0x12345fu, result.loc.value().address); |
| |
| // Decimal number with "*" override should be an address. |
| result = SyncEvalGlobalInputLocation(eval_context, existing_location, "*21"); |
| ASSERT_TRUE(result.loc.ok()) << result.loc.err().msg(); |
| EXPECT_FALSE(result.size); |
| EXPECT_EQ(InputLocation::Type::kAddress, result.loc.value().type); |
| EXPECT_EQ(21u, result.loc.value().address); |
| |
| // Invalid address. |
| result = SyncEvalGlobalInputLocation(eval_context, existing_location, "*2134x"); |
| ASSERT_TRUE(result.loc.has_error()); |
| |
| // Line number with no Frame for context. |
| result = SyncEvalGlobalInputLocation(eval_context, existing_location, "21"); |
| ASSERT_TRUE(result.loc.has_error()); |
| |
| // Valid implicit file name. |
| std::string file = "foo.cc"; |
| Location existing_location_with_file(0x1234, FileLine(file, 12), 0, |
| SymbolContext::ForRelativeAddresses(), LazySymbol()); |
| result = SyncEvalGlobalInputLocation(eval_context, existing_location_with_file, "21"); |
| ASSERT_TRUE(result.loc.ok()) << result.loc.err().msg(); |
| EXPECT_FALSE(result.size); |
| EXPECT_EQ(file, result.loc.value().line.file()); |
| EXPECT_EQ(21, result.loc.value().line.line()); |
| |
| // Empty file name with colon should be the same thing. |
| result = SyncEvalGlobalInputLocation(eval_context, existing_location_with_file, ":92"); |
| ASSERT_TRUE(result.loc.ok()) << result.loc.err().msg(); |
| EXPECT_FALSE(result.size); |
| EXPECT_EQ(file, result.loc.value().line.file()); |
| EXPECT_EQ(92, result.loc.value().line.line()); |
| |
| // Pointer to a double. This should give a valid expression size. |
| result = SyncEvalGlobalInputLocation(eval_context, existing_location, "*(double*)0x123450"); |
| ASSERT_TRUE(result.loc.ok()) << result.loc.err().msg(); |
| ASSERT_TRUE(result.size); |
| EXPECT_EQ(8u, *result.size); |
| EXPECT_EQ(InputLocation::Type::kAddress, result.loc.value().type); |
| EXPECT_EQ(0x123450u, result.loc.value().address); |
| } |
| |
| TEST_F(InputLocationParserTest, ResolveInputLocation) { |
| auto& index_root = mock_module_symbols_->index().root(); |
| |
| // Resolve to nothing. |
| Location output; |
| Err err = ResolveUniqueInputLocation(&symbols_.process(), Location(), "Foo", false, &output); |
| EXPECT_TRUE(err.has_error()); |
| EXPECT_EQ("Nothing matching this name was found.", err.msg()); |
| |
| Location expected(0x12345678, FileLine("file.cc", 12), 0, symbol_context_); |
| |
| // To be found, "Foo" must be in both the index and in the symbol location mock list. |
| const char kFooName[] = "Foo"; |
| auto foo = fxl::MakeRefCounted<Function>(DwarfTag::kSubprogram); |
| foo->set_assigned_name(kFooName); |
| TestIndexedSymbol foo_indexed(mock_module_symbols_, &index_root, kFooName, foo); |
| mock_module_symbols_->AddSymbolLocations("Foo", {expected}); |
| |
| // Resolve to one location (success) case. |
| err = ResolveUniqueInputLocation(&symbols_.process(), Location(), "Foo", false, &output); |
| EXPECT_FALSE(err.has_error()) << err.msg(); |
| EXPECT_EQ(expected.address(), output.address()); |
| |
| // Resolve to lots of locations, it should give suggestions. Even though we didn't request |
| // symbols, the result should be symbolized. |
| std::vector<Location> expected_locations; |
| for (int i = 0; i < 15; i++) { |
| // The address and line numbers count up for each match. |
| expected_locations.emplace_back(0x12345000 + i, FileLine("file.cc", 100 + i), 0, |
| symbol_context_); |
| } |
| mock_module_symbols_->AddSymbolLocations("Foo", expected_locations); |
| |
| // Resolve to all of them. |
| std::vector<Location> output_locations; |
| err = ResolveInputLocations(&symbols_.process(), Location(), "Foo", false, &output_locations); |
| EXPECT_FALSE(err.has_error()); |
| |
| // The result should be the same as the input but not symbolized (we |
| // requested no symbolization). |
| ASSERT_EQ(expected_locations.size(), output_locations.size()); |
| for (size_t i = 0; i < expected_locations.size(); i++) { |
| EXPECT_EQ(expected_locations[i].address(), output_locations[i].address()); |
| EXPECT_FALSE(output_locations[i].has_symbols()); |
| } |
| |
| // Try to resolve one of them. Since there are many this will fail. We requested no symbolization |
| // but the error message should still be symbolized. |
| err = ResolveUniqueInputLocation(&symbols_.process(), Location(), "Foo", false, &output); |
| EXPECT_TRUE(err.has_error()); |
| EXPECT_EQ(R"(This resolves to more than one location. Could be: |
| • file.cc:100 = 0x12345000 |
| • file.cc:101 = 0x12345001 |
| • file.cc:102 = 0x12345002 |
| • file.cc:103 = 0x12345003 |
| • file.cc:104 = 0x12345004 |
| • file.cc:105 = 0x12345005 |
| • file.cc:106 = 0x12345006 |
| • file.cc:107 = 0x12345007 |
| • file.cc:108 = 0x12345008 |
| • file.cc:109 = 0x12345009 |
| ...5 more omitted... |
| )", |
| err.msg()); |
| } |
| |
| // Tests that ParseLocalInputLocation() finds matches on the local object for symbolic names. |
| TEST_F(InputLocationParserTest, ParseLocalInputLocation) { |
| auto& root = mock_module_symbols_->index().root(); |
| |
| const char kFunctionName[] = "Foo"; |
| |
| // The no-context case should just return the input symbol. |
| std::vector<InputLocation> results; |
| Err err = ParseLocalInputLocation(nullptr, kFunctionName, &results); |
| ASSERT_TRUE(err.ok()); |
| ASSERT_EQ(1u, results.size()); |
| EXPECT_EQ(InputLocation::Type::kName, results[0].type); |
| EXPECT_EQ(R"("Foo")", results[0].name.GetDebugName()); |
| |
| // Make a class. |
| const char kClassName[] = "MyClass"; |
| auto my_class = fxl::MakeRefCounted<Collection>(DwarfTag::kClassType); |
| my_class->set_assigned_name(kClassName); |
| TestIndexedSymbol indexed_class(mock_module_symbols_, &root, kClassName, my_class); |
| |
| // Function inside the class. |
| auto foo_func = fxl::MakeRefCounted<Function>(DwarfTag::kSubprogram); |
| SymbolTestParentSetter foo_func_parent(foo_func, my_class); |
| foo_func->set_assigned_name(kFunctionName); |
| constexpr uint64_t kFunctionBegin = ProcessSymbolsTestSetup::kDefaultLoadAddress + 0x1000; |
| foo_func->set_code_ranges(AddressRanges(AddressRange(kFunctionBegin, kFunctionBegin + 0x10))); |
| TestIndexedSymbol indexed_func(mock_module_symbols_, indexed_class.index_node, kFunctionName, |
| foo_func); |
| |
| // Make a "this" pointer for the function pointing back to the class. |
| auto my_class_ptr = fxl::MakeRefCounted<ModifiedType>(DwarfTag::kPointerType, my_class); |
| auto this_var = |
| fxl::MakeRefCounted<Variable>(DwarfTag::kVariable, "this", my_class_ptr, VariableLocation()); |
| foo_func->set_object_pointer(this_var); |
| |
| // Process/thread setup. |
| Session session; |
| MockProcess process(&session); |
| process.set_symbols(&symbols_.process()); |
| MockThread thread(&process); |
| |
| // The location points to the first address of the function. |
| Location location(kFunctionBegin, FileLine(), 0, symbol_context_, foo_func); |
| MockFrame frame(&session, &thread, location, 0x1000); |
| |
| // A new search should return the more specific version in the class, plus the global one. |
| err = ParseLocalInputLocation(&frame, kFunctionName, &results); |
| ASSERT_TRUE(err.ok()); |
| ASSERT_EQ(2u, results.size()); |
| EXPECT_EQ(InputLocation::Type::kName, results[0].type); |
| EXPECT_EQ(R"(::"MyClass"; ::"Foo")", results[0].name.GetDebugName()); |
| EXPECT_EQ(InputLocation::Type::kName, results[1].type); |
| EXPECT_EQ(R"("Foo")", results[1].name.GetDebugName()); |
| |
| // A fully qualified function name ("::Foo") should not match the current class and only the |
| // global version should be returned. |
| results.clear(); |
| err = ParseLocalInputLocation(&frame, std::string("::") + kFunctionName, &results); |
| ASSERT_TRUE(err.ok()); |
| ASSERT_EQ(1u, results.size()); |
| EXPECT_EQ(InputLocation::Type::kName, results[0].type); |
| EXPECT_EQ(R"(::"Foo")", results[0].name.GetDebugName()); |
| } |
| |
| // Most of the prefix searching is tested by the FindName tests. This just tests the integration of |
| // that with the InputLocation completion. |
| TEST_F(InputLocationParserTest, CompleteInputLocation) { |
| auto& root = mock_module_symbols_->index().root(); |
| |
| // Global function. |
| const char kGlobalName[] = "aGlobalFunction"; |
| auto global_func = fxl::MakeRefCounted<Function>(DwarfTag::kSubprogram); |
| global_func->set_assigned_name(kGlobalName); |
| TestIndexedSymbol indexed_global(mock_module_symbols_, &root, kGlobalName, global_func); |
| |
| // Namespace |
| const char kNsName[] = "aNamespace"; |
| auto ns = fxl::MakeRefCounted<Namespace>(); |
| ns->set_assigned_name(kNsName); |
| TestIndexedSymbol indexed_ns(mock_module_symbols_, &root, kNsName, ns); |
| |
| // Class inside the namespace. |
| const char kClassName[] = "Class"; |
| auto global_type = fxl::MakeRefCounted<Collection>(DwarfTag::kClassType); |
| SymbolTestParentSetter global_type_parent(global_type, ns); |
| global_type->set_assigned_name(kClassName); |
| TestIndexedSymbol indexed_type(mock_module_symbols_, indexed_ns.index_node, kClassName, |
| global_type); |
| |
| // Function inside the class. |
| const char kMemberName[] = "MemberFunction"; |
| auto member_func = fxl::MakeRefCounted<Function>(DwarfTag::kSubprogram); |
| member_func->set_assigned_name(kMemberName); |
| SymbolTestParentSetter member_func_parent(member_func, global_type); |
| TestIndexedSymbol indexed_member(mock_module_symbols_, indexed_type.index_node, kMemberName, |
| member_func); |
| |
| // TODO(brettw) make a test setup helper for a whole session / target / process / thread / frame + |
| // symbols. |
| Session session; |
| MockTarget target(&session); |
| target.set_symbols(&symbols_.target()); |
| MockProcess process(&session); |
| process.set_symbols(&symbols_.process()); |
| |
| Location loc; |
| MockFrame frame(&session, nullptr, loc, 0); |
| |
| target.SetRunningProcess(&process); |
| |
| Command command; |
| command.set_verb(Verb::kBreak); |
| command.set_target(&target); |
| command.set_frame(&frame); |
| |
| // TEST CODE ------------------------------------------------------------------------------------- |
| |
| // "a" should complete to both "aNamespace" and "aGlobalFunction" (in that order). |
| std::vector<std::string> found; |
| CompleteInputLocation(command, "a", &found); |
| ASSERT_EQ(2u, found.size()); |
| EXPECT_EQ("aNamespace::", found[0]); // Namespaces get "::" appended. |
| EXPECT_EQ(std::string("::") + kGlobalName, found[1]); |
| |
| // "aNamespace::" doesn't complete to anything. It might be nice to have this complete to all |
| // functions in the namespace, but we don't implement that yet. In the meantime, at least test |
| // this does what we currently expect. |
| found.clear(); |
| CompleteInputLocation(command, "aNamespace::", &found); |
| ASSERT_TRUE(found.empty()); |
| |
| // Completing classes. |
| found.clear(); |
| CompleteInputLocation(command, "aNamespace::Cl", &found); |
| ASSERT_EQ(1u, found.size()); |
| EXPECT_EQ("::aNamespace::Class::", found[0]); // Classes get "::" appended. |
| |
| // Completing class member functions. |
| found.clear(); |
| CompleteInputLocation(command, "aNamespace::Class::M", &found); |
| ASSERT_EQ(1u, found.size()); |
| EXPECT_EQ("::aNamespace::Class::MemberFunction", found[0]); |
| } |
| |
| } // namespace zxdb |