| // 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/command_utils.h" |
| |
| #include <limits> |
| |
| #include <gtest/gtest.h> |
| |
| #include "src/developer/debug/zxdb/client/frame.h" |
| #include "src/developer/debug/zxdb/client/mock_breakpoint.h" |
| #include "src/developer/debug/zxdb/client/mock_breakpoint_location.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/err.h" |
| #include "src/developer/debug/zxdb/console/command.h" |
| #include "src/developer/debug/zxdb/console/console_context.h" |
| #include "src/developer/debug/zxdb/console/format_frame.h" |
| #include "src/developer/debug/zxdb/console/mock_console.h" |
| #include "src/developer/debug/zxdb/console/output_buffer.h" |
| #include "src/developer/debug/zxdb/symbols/location.h" |
| |
| namespace zxdb { |
| |
| TEST(CommandUtils, StringToInt) { |
| // Leading 0's not allowed. |
| int result = 0; |
| EXPECT_TRUE(StringToInt("010", &result).has_error()); |
| |
| // Negative hexadecimal. |
| EXPECT_FALSE(StringToInt("-0x1a", &result).has_error()); |
| EXPECT_EQ(-0x1a, result); |
| |
| // Trimmed |
| EXPECT_FALSE(StringToInt(" -0x1a ", &result).has_error()); |
| EXPECT_EQ(-0x1a, result); |
| |
| // Test at the limits. |
| constexpr int kMax = std::numeric_limits<int>::max(); |
| EXPECT_FALSE(StringToInt(std::to_string(kMax), &result).has_error()); |
| EXPECT_EQ(kMax, result); |
| |
| constexpr int kMin = std::numeric_limits<int>::lowest(); |
| EXPECT_FALSE(StringToInt(std::to_string(kMin), &result).has_error()); |
| EXPECT_EQ(kMin, result); |
| |
| // Test just beyond the limits. |
| int64_t kBeyondMax = static_cast<int64_t>(kMax) + 1; |
| EXPECT_TRUE(StringToInt(std::to_string(kBeyondMax), &result).has_error()); |
| |
| int64_t kBeyondMin = static_cast<int64_t>(kMin) - 1; |
| EXPECT_TRUE(StringToInt(std::to_string(kBeyondMin), &result).has_error()); |
| } |
| |
| TEST(CommandUtils, StringToUint32) { |
| uint32_t result = 0; |
| EXPECT_TRUE(StringToUint32("032", &result).has_error()); |
| |
| EXPECT_FALSE(StringToUint32("32", &result).has_error()); |
| EXPECT_EQ(32u, result); |
| |
| // Test at and just beyond the limits. |
| EXPECT_FALSE(StringToUint32("0xffffffff", &result).has_error()); |
| EXPECT_EQ(0xffffffff, result); |
| EXPECT_TRUE(StringToUint32("0x100000000", &result).has_error()); |
| |
| // Trimming |
| EXPECT_FALSE(StringToUint32(" 0xffffffff ", &result).has_error()); |
| EXPECT_EQ(0xffffffff, result); |
| } |
| |
| TEST(CommandUtils, StringToUint64) { |
| uint64_t result = 0; |
| EXPECT_FALSE(StringToUint64("1234", &result).has_error()); |
| EXPECT_EQ(1234u, result); |
| |
| // Empty string. |
| EXPECT_TRUE(StringToUint64("", &result).has_error()); |
| |
| // Non-numbers. |
| EXPECT_TRUE(StringToUint64("asdf", &result).has_error()); |
| EXPECT_TRUE(StringToUint64(" ", &result).has_error()); |
| |
| // We don't allow "+" for positive numbers. |
| EXPECT_TRUE(StringToUint64("+1234", &result).has_error()); |
| EXPECT_EQ(0u, result); |
| |
| // Trim. |
| EXPECT_FALSE(StringToUint64(" 1234 ", &result).has_error()); |
| EXPECT_EQ(1234u, result); |
| |
| // Leading 0's should trigger an error about dangerous octal usage. |
| EXPECT_TRUE(StringToUint64("01234", &result).has_error()); |
| |
| // Hex digits invalid without proper prefix. |
| EXPECT_TRUE(StringToUint64("12a34", &result).has_error()); |
| |
| // Valid hex number |
| EXPECT_FALSE(StringToUint64("0x1A2a34", &result).has_error()); |
| EXPECT_EQ(0x1a2a34u, result); |
| |
| // Isolated hex prefix. |
| EXPECT_TRUE(StringToUint64("0x", &result).has_error()); |
| |
| // Valid hex number with capital X prefix at the max of a 64-bit int. |
| EXPECT_FALSE(StringToUint64("0XffffFFFFffffFFFF", &result).has_error()); |
| EXPECT_EQ(0xffffFFFFffffFFFFu, result); |
| } |
| |
| TEST(CommandUtils, ReadUint64Arg) { |
| Command cmd; |
| uint64_t out; |
| |
| Err err = ReadUint64Arg(cmd, 0, "code", &out); |
| EXPECT_TRUE(err.has_error()); |
| EXPECT_EQ("Not enough arguments when reading the code.", err.msg()); |
| |
| std::vector<std::string> args; |
| args.push_back("12"); |
| args.push_back("0x67"); |
| args.push_back("notanumber"); |
| cmd.set_args(std::move(args)); |
| |
| err = ReadUint64Arg(cmd, 0, "code", &out); |
| EXPECT_FALSE(err.has_error()); |
| EXPECT_EQ(12u, out); |
| |
| err = ReadUint64Arg(cmd, 1, "code", &out); |
| EXPECT_FALSE(err.has_error()); |
| EXPECT_EQ(0x67u, out); |
| |
| err = ReadUint64Arg(cmd, 2, "code", &out); |
| EXPECT_TRUE(err.has_error()); |
| EXPECT_EQ("Invalid number \"notanumber\" when reading the code.", err.msg()); |
| } |
| |
| TEST(CommandUtils, FormatInputLocation) { |
| EXPECT_EQ("<no location>", FormatInputLocation(InputLocation()).AsString()); |
| EXPECT_EQ("0x123456", FormatInputLocation(InputLocation(0x123456)).AsString()); |
| EXPECT_EQ("file.cc:34", FormatInputLocation(InputLocation(FileLine("file.cc", 34))).AsString()); |
| EXPECT_EQ("Foo", |
| FormatInputLocation(InputLocation(Identifier(IdentifierComponent("Foo")))).AsString()); |
| } |
| |
| TEST(CommandUtils, FormatInputLocations) { |
| std::vector<InputLocation> input; |
| EXPECT_EQ("<no location>", FormatInputLocations(input).AsString()); |
| |
| input.emplace_back(Identifier(IdentifierComponent("Foo"))); |
| EXPECT_EQ("Foo", FormatInputLocations(input).AsString()); |
| |
| input.emplace_back(Identifier(IdentifierComponent("Bar"))); |
| EXPECT_EQ("Foo, Bar", FormatInputLocations(input).AsString()); |
| } |
| |
| TEST(CommandUtils, FormatBreakpoint) { |
| Session session; |
| ConsoleContext context(&session); |
| MockBreakpoint breakpoint(&session); |
| |
| // Register with the context. |
| SystemObserver* system_observer = &context; |
| system_observer->DidCreateBreakpoint(&breakpoint); |
| |
| // Formatting an empty breakpoint |
| EXPECT_EQ("Breakpoint 1 pending @ <no location>\n", |
| FormatBreakpoint(&context, &breakpoint, false).AsString()); |
| |
| // Should show no message for context. |
| EXPECT_EQ("Breakpoint 1 pending @ <no location>\n", |
| FormatBreakpoint(&context, &breakpoint, true).AsString()); |
| |
| // Provide settings. |
| BreakpointSettings settings; |
| settings.locations.push_back(InputLocation(FileLine("foo.cc", 21))); |
| breakpoint.SetSettings(settings); |
| |
| // Format pending. |
| EXPECT_EQ("Breakpoint 1 pending @ foo.cc:21\n", |
| FormatBreakpoint(&context, &breakpoint, false).AsString()); |
| EXPECT_EQ( |
| "Breakpoint 1 @ foo.cc:21\n" |
| "Pending: No current matches for location. It will be matched against new\n" |
| " processes and shared libraries.\n", |
| FormatBreakpoint(&context, &breakpoint, true).AsString()); |
| |
| // Provide matched location. The formatting doesn't use the Process so we provide a null one. |
| std::vector<std::unique_ptr<BreakpointLocation>> locs; |
| locs.push_back(std::make_unique<MockBreakpointLocation>(nullptr)); |
| breakpoint.set_locations(std::move(locs)); |
| |
| // No source context. |
| EXPECT_EQ("Breakpoint 1 @ foo.cc:21\n", |
| FormatBreakpoint(&context, &breakpoint, false).AsString()); |
| |
| // Provide 2 matched locations |
| locs.push_back(std::make_unique<MockBreakpointLocation>(nullptr)); |
| locs.push_back(std::make_unique<MockBreakpointLocation>(nullptr)); |
| breakpoint.set_locations(std::move(locs)); |
| |
| EXPECT_EQ("Breakpoint 1 (2 addrs) @ foo.cc:21\n", |
| FormatBreakpoint(&context, &breakpoint, false).AsString()); |
| |
| // Set all the options to non-defaults. |
| settings.type = BreakpointSettings::Type::kWrite; |
| settings.byte_size = 4; |
| settings.enabled = false; |
| settings.scope = ExecutionScope(session.system().GetTargets()[0]); |
| settings.stop_mode = BreakpointSettings::StopMode::kThread; |
| settings.one_shot = true; |
| breakpoint.SetSettings(settings); |
| EXPECT_EQ( |
| "Breakpoint 1 scope=\"pr 1\" stop=thread enabled=false type=write size=4 one-shot=true (2 " |
| "addrs) @ foo.cc:21\n", |
| FormatBreakpoint(&context, &breakpoint, false).AsString()); |
| |
| system_observer->WillDestroyBreakpoint(&breakpoint); |
| } |
| |
| TEST(CommandUtils, FormatConsoleString) { |
| EXPECT_EQ("\"\"", FormatConsoleString("")); |
| |
| // Doesn't need any escaping or quoting. |
| EXPECT_EQ("foobar", FormatConsoleString("foobar")); |
| EXPECT_EQ("snowman\xe2\x98\x83!", FormatConsoleString("snowman\xe2\x98\x83!")); |
| |
| // Needs normal quoting. |
| EXPECT_EQ("\"foo\\nbar\"", FormatConsoleString("foo\nbar")); |
| EXPECT_EQ("\"foo bar\"", FormatConsoleString("foo bar")); |
| EXPECT_EQ("\"f\\x01oo\"", FormatConsoleString("f\x01oo")); |
| |
| // Can use raw quoting. Test making the delimiters unique. |
| EXPECT_EQ("R\"(foo \"bar\")\"", FormatConsoleString("foo \"bar\"")); |
| EXPECT_EQ("R\"*(raw end )\")*\"", FormatConsoleString("raw end )\"")); |
| EXPECT_EQ("R\"**(raw end )\" )*\")**\"", FormatConsoleString("raw end )\" )*\"")); |
| } |
| |
| TEST(CommandUtils, AssertStoppedThreadWithFrameCommand) { |
| Session session; |
| MockConsole console(&session); |
| console.Init(); |
| |
| MockTarget target(&session); |
| MockProcess process(&target); |
| MockThread thread(&process); |
| thread.SetState(debug_ipc::ThreadRecord::State::kBlocked, |
| debug_ipc::ThreadRecord::BlockedReason::kException); |
| std::vector<std::unique_ptr<Frame>> frames; |
| frames.push_back(std::make_unique<MockFrame>( |
| &session, &thread, Location(Location::State::kAddress, 0x1000), 0x2000)); |
| thread.GetStack().SetFramesForTest(std::move(frames), false); |
| |
| // Tell the context about our process and thread so it can supply correct indices in errors. |
| console.context().DidCreateTarget(&target); |
| console.context().DidCreateProcess(&process, 0); |
| console.context().DidCreateThread(&thread); |
| |
| Command cmd; |
| cmd.set_verb(Verb::kSteps); |
| |
| // Test with no thread. |
| Err err = AssertStoppedThreadWithFrameCommand(&console.context(), cmd, "steps", true); |
| ASSERT_TRUE(err.has_error()); |
| EXPECT_EQ("\"steps\" requires a thread but there is no current thread.", err.msg()); |
| |
| // Supply a valid thread, this should succeed. |
| cmd.add_thread(&thread); |
| cmd.add_frame(thread.GetStack()[0]); |
| err = AssertStoppedThreadWithFrameCommand(&console.context(), cmd, "steps", true); |
| EXPECT_FALSE(err.has_error()); |
| |
| // Supply a thread in an unknown state. |
| thread.SetState(std::nullopt, debug_ipc::ThreadRecord::BlockedReason::kNotBlocked); |
| err = AssertStoppedThreadWithFrameCommand(&console.context(), cmd, "steps", true); |
| EXPECT_EQ( |
| "\"steps\" requires a suspended thread but thread 1 is Unknown.\n" |
| "To view and sync thread state with the remote system, type \"thread\".\n" |
| "Or type \"pause\" to pause a running thread.", |
| err.msg()); |
| } |
| |
| TEST(CommandUtils, FormatAllThreadStacks) { |
| Session session; |
| MockConsole console(&session); |
| console.Init(); |
| |
| Command cmd; |
| |
| MockTarget target(&session); |
| |
| MockProcess process(&target); |
| |
| MockThread t1(&process); |
| MockThread t2(&process); |
| |
| std::vector<std::unique_ptr<Frame>> t1_frames; |
| std::vector<std::unique_ptr<Frame>> t2_frames; |
| |
| t1_frames.push_back(std::make_unique<MockFrame>(&session, &t1, 0x1001, 0x2001, "function0", |
| FileLine("file0.cc", 10))); |
| t1_frames.push_back(std::make_unique<MockFrame>(&session, &t1, 0x1002, 0x2002, "function1", |
| FileLine("file1.cc", 15))); |
| t1_frames.push_back(std::make_unique<MockFrame>(&session, &t1, 0x1003, 0x2003, "function2", |
| FileLine("file2.cc", 20))); |
| t1.GetStack().SetFramesForTest(std::move(t1_frames), true); |
| |
| t2_frames.push_back(std::make_unique<MockFrame>(&session, &t2, 0x2001, 0x4001, "function3", |
| FileLine("file3.cc", 100))); |
| t2_frames.push_back(std::make_unique<MockFrame>(&session, &t2, 0x2002, 0x4002, "function4", |
| FileLine("file4.cc", 150))); |
| t2_frames.push_back(std::make_unique<MockFrame>(&session, &t2, 0x2003, 0x4003, "function5", |
| FileLine("file5.cc", 200))); |
| t2_frames.push_back(std::make_unique<MockFrame>(&session, &t2, 0x2004, 0x4004, "function6", |
| FileLine("file6.cc", 400))); |
| t2.GetStack().SetFramesForTest(std::move(t2_frames), true); |
| |
| console.context().DidCreateTarget(&target); |
| console.context().DidCreateProcess(&process, 0); |
| console.context().DidCreateThread(&t1); |
| console.context().DidCreateThread(&t2); |
| |
| // Eat the "launched process" output. |
| auto event = console.GetOutputEvent(); |
| |
| cmd.SetNoun(Noun::kThread, Command::kWildcard); |
| cmd.SetNoun(Noun::kFrame, Command::kNoIndex); |
| cmd.add_target(&target); |
| cmd.add_thread(&t1); |
| cmd.add_thread(&t2); |
| |
| fxl::RefPtr<CommandContext> cmd_context = fxl::MakeRefCounted<ConsoleCommandContext>(&console); |
| auto opts = FormatStackOptions::GetFrameOptions(&target, false, false, false, 4); |
| opts.pretty_stack = console.context().pretty_stack_manager(); |
| |
| auto out = FormatAllThreadStacks(cmd.all_threads(), opts, cmd_context); |
| |
| // The threads have their entire stack and we are not forcing an update, so this should be |
| // synchronous. |
| FX_CHECK(out->is_complete()); |
| cmd_context->Output(out->DestructiveFlatten()); |
| |
| event = console.GetOutputEvent(); |
| EXPECT_EQ( |
| "Thread 1 state=Suspended tid=1234 name=\"test thread\"\n" |
| "▶ 0 function0() • file0.cc:10\n" |
| " 1 function1() • file1.cc:15\n" |
| " 2 function2() • file2.cc:20\n" |
| "Thread 2 state=Suspended tid=1234 name=\"test thread\"\n" |
| "▶ 0 function3() • file3.cc:100\n" |
| " 1 function4() • file4.cc:150\n" |
| " 2 function5() • file5.cc:200\n" |
| " 3 function6() • file6.cc:400\n", |
| event.output.AsString()); |
| } |
| |
| } // namespace zxdb |