| /* | |
| * Copyright (C) 2023 The Android Open Source Project | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| #include <cstddef> | |
| #include <cstdlib> | |
| #include <cstring> | |
| #include <initializer_list> | |
| #include <map> | |
| #include <set> | |
| #include <string> | |
| #include <string_view> | |
| #include <utility> | |
| #include <vector> | |
| #include "perfetto/base/build_config.h" | |
| #include "perfetto/base/logging.h" | |
| #include "perfetto/base/time.h" | |
| #include "perfetto/ext/base/file_utils.h" | |
| #include "perfetto/ext/base/string_utils.h" | |
| #include "perfetto/ext/base/subprocess.h" | |
| #include "perfetto/ext/base/temp_file.h" | |
| #include "perfetto/ext/base/unix_socket.h" | |
| #include "perfetto/ext/base/utils.h" | |
| #include "protos/perfetto/trace/profiling/deobfuscation.gen.h" | |
| #include "protos/perfetto/trace/trace.gen.h" | |
| #include "protos/perfetto/trace/trace_packet.gen.h" | |
| #include "protos/perfetto/trace_processor/trace_processor.gen.h" | |
| #include "src/base/test/utils.h" | |
| #include "test/gtest_and_gmock.h" | |
| #include "test/test_helper.h" | |
| #if PERFETTO_BUILDFLAG(PERFETTO_OS_LINUX) || \ | |
| PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID) | |
| #include <unistd.h> | |
| #include <climits> | |
| #endif | |
| #if !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) | |
| #include <fcntl.h> | |
| #include <signal.h> | |
| #include <sys/stat.h> | |
| #endif | |
| namespace perfetto::trace_processor { | |
| namespace { | |
| using TraceProcessorRpc = protos::gen::TraceProcessorRpc; | |
| using TraceProcessorRpcStream = protos::gen::TraceProcessorRpcStream; | |
| using CellsBatch = protos::gen::QueryResult::CellsBatch; | |
| using testing::AllOf; | |
| using testing::ElementsAre; | |
| using testing::HasSubstr; | |
| using testing::IsEmpty; | |
| using testing::Not; | |
| using testing::Property; | |
| using testing::SizeIs; | |
| const std::string_view kSimpleSystrace = R"(# tracer | |
| surfaceflinger-598 ( 598) [004] .... 10852.771242: tracing_mark_write: B|598|some event | |
| surfaceflinger-598 ( 598) [004] .... 10852.771245: tracing_mark_write: E|598 | |
| )"; | |
| std::string ShellPath() { | |
| return base::GetCurExecutableDir() + "/trace_processor_shell"; | |
| } | |
| // Writes kSimpleSystrace to a temp file and returns the TempFile object. | |
| base::TempFile WriteSimpleSystrace() { | |
| auto f = base::TempFile::Create().ValueOrDie(); | |
| base::WriteAll(f.fd(), kSimpleSystrace.data(), | |
| static_cast<size_t>(kSimpleSystrace.size())); | |
| return f; | |
| } | |
| // Writes arbitrary content to a temp file and returns the TempFile object. | |
| base::TempFile WriteTempFile(const std::string& content) { | |
| auto f = base::TempFile::Create().ValueOrDie(); | |
| base::WriteAll(f.fd(), content.data(), content.size()); | |
| return f; | |
| } | |
| struct SubprocessResult { | |
| int exit_code; | |
| std::string out; | |
| }; | |
| // Agent mode (see src/trace_processor/shell/agent_mode.cc) changes the output | |
| // of `query` when the shell is run by a coding agent. Unset the variables | |
| // which turn it on, so that tests see the same output whoever runs them: the | |
| // shell inherits this process's environment. | |
| void UnsetAgentEnvVars() { | |
| static constexpr const char* kAgentEnvVars[] = { | |
| "AI_AGENT", | |
| "AGENT", | |
| "CLAUDECODE", | |
| "CODEX_CI", | |
| "CODEX_SANDBOX", | |
| "CODEX_THREAD_ID", | |
| "CURSOR_AGENT", | |
| "GEMINI_CLI", | |
| "COPILOT_AGENT", | |
| "COPILOT_CLI", | |
| "COPILOT_AGENT_SESSION_ID", | |
| }; | |
| for (const char* var : kAgentEnvVars) { | |
| base::UnsetEnv(var); | |
| } | |
| } | |
| // Runs trace_processor_shell with the given args. stdout and stderr are both | |
| // captured into `out`. | |
| SubprocessResult RunShell(std::initializer_list<std::string> extra_args) { | |
| base::Subprocess p; | |
| p.args.exec_cmd.push_back(ShellPath()); | |
| for (const auto& a : extra_args) { | |
| p.args.exec_cmd.push_back(a); | |
| } | |
| UnsetAgentEnvVars(); | |
| p.args.stdin_mode = base::Subprocess::InputMode::kDevNull; | |
| p.args.stdout_mode = base::Subprocess::OutputMode::kBuffer; | |
| p.args.stderr_mode = base::Subprocess::OutputMode::kBuffer; | |
| p.Start(); | |
| PERFETTO_CHECK(p.Wait(kDefaultTestTimeoutMs)); | |
| return {p.returncode(), std::move(p.output())}; | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Classic CLI backcompat tests | |
| // --------------------------------------------------------------------------- | |
| TEST(TraceProcessorShellIntegrationTest, ClassicVersion) { | |
| auto result = RunShell({"-v"}); | |
| EXPECT_EQ(result.exit_code, 0); | |
| EXPECT_THAT(result.out, HasSubstr("Trace Processor RPC API version")); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, ClassicHelp) { | |
| // --help now shows the subcommand-aware help. | |
| auto result = RunShell({"-h"}); | |
| EXPECT_EQ(result.exit_code, 0); | |
| EXPECT_THAT(result.out, HasSubstr("Perfetto Trace Processor")); | |
| EXPECT_THAT(result.out, HasSubstr("Commands:")); | |
| EXPECT_THAT(result.out, HasSubstr("query")); | |
| EXPECT_THAT(result.out, HasSubstr("interactive")); | |
| EXPECT_THAT(result.out, HasSubstr("server")); | |
| EXPECT_THAT(result.out, HasSubstr("summarize")); | |
| EXPECT_THAT(result.out, HasSubstr("metrics")); | |
| EXPECT_THAT(result.out, HasSubstr("export")); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, HelpClassic) { | |
| auto result = RunShell({"--help-classic"}); | |
| EXPECT_EQ(result.exit_code, 0); | |
| EXPECT_THAT(result.out, HasSubstr("Interactive trace processor shell")); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, HelpCommand) { | |
| auto result = RunShell({"help", "query"}); | |
| EXPECT_EQ(result.exit_code, 0); | |
| EXPECT_THAT(result.out, HasSubstr("run a SQL query")); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, HelpBare) { | |
| auto result = RunShell({"help"}); | |
| EXPECT_EQ(result.exit_code, 0); | |
| EXPECT_THAT(result.out, HasSubstr("Commands:")); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, HelpUnknownCommand) { | |
| auto result = RunShell({"help", "nonexistent"}); | |
| EXPECT_NE(result.exit_code, 0); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, NoFileCollisionHintOnNormalUsage) { | |
| // Normal subcommand usage should not emit the file collision hint. | |
| auto trace = WriteSimpleSystrace(); | |
| auto result = RunShell({"query", trace.path(), "SELECT 1"}); | |
| EXPECT_EQ(result.exit_code, 0); | |
| EXPECT_THAT(result.out, Not(HasSubstr("matches both a subcommand"))); | |
| } | |
| #if PERFETTO_BUILDFLAG(PERFETTO_OS_LINUX) || \ | |
| PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID) | |
| TEST(TraceProcessorShellIntegrationTest, FileCollisionHint) { | |
| // Create a file named "query" in a temp dir, chdir there, and run the shell | |
| // with bare "query" as an arg. The dispatcher should match "query" as a | |
| // subcommand but also notice the file and emit a hint. | |
| auto tmpdir = base::TempDir::Create(); | |
| std::string file_path = std::string(tmpdir.path()) + "/query"; | |
| base::WriteAll(*base::OpenFile(file_path, O_WRONLY | O_CREAT | O_TRUNC, 0600), | |
| kSimpleSystrace.data(), | |
| static_cast<size_t>(kSimpleSystrace.size())); | |
| // RAII guard: restore CWD and clean up the file no matter how the test exits. | |
| char old_cwd[PATH_MAX]; | |
| ASSERT_NE(getcwd(old_cwd, sizeof(old_cwd)), nullptr); | |
| ASSERT_EQ(chdir(tmpdir.path().c_str()), 0); | |
| auto cleanup = base::OnScopeExit([&] { | |
| PERFETTO_CHECK(chdir(old_cwd) == 0); | |
| remove(file_path.c_str()); | |
| }); | |
| // "query" is interpreted as the subcommand (which fails — no -f/-c), but | |
| // the hint should appear because a file named "query" exists in CWD. | |
| auto result = RunShell({"query"}); | |
| EXPECT_NE(result.exit_code, 0); | |
| EXPECT_THAT(result.out, HasSubstr("matches both a subcommand and a file")); | |
| } | |
| #endif | |
| TEST(TraceProcessorShellIntegrationTest, ClassicUnknownFlag) { | |
| auto result = RunShell({"--nonexistent"}); | |
| EXPECT_NE(result.exit_code, 0); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, ClassicQueryString) { | |
| // Use computed value: 200 + 61 = 261, "261" not in input SQL. | |
| auto trace = WriteSimpleSystrace(); | |
| auto result = RunShell({"-Q", "SELECT 200 + 61 AS test_col", trace.path()}); | |
| EXPECT_EQ(result.exit_code, 0); | |
| EXPECT_THAT(result.out, HasSubstr("261")); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, ClassicQueryFile) { | |
| // Use computed value: 400 + 76 = 476, "476" not in input SQL. | |
| auto trace = WriteSimpleSystrace(); | |
| auto query = WriteTempFile("SELECT 400 + 76 AS unique_val;"); | |
| auto result = RunShell({"-q", query.path(), trace.path()}); | |
| EXPECT_EQ(result.exit_code, 0); | |
| EXPECT_THAT(result.out, HasSubstr("476")); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, ClassicQueryStringNoTrace) { | |
| auto result = RunShell({"-Q", "SELECT 1"}); | |
| EXPECT_NE(result.exit_code, 0); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, ClassicQueryFileBadPath) { | |
| auto trace = WriteSimpleSystrace(); | |
| auto result = RunShell({"-q", "/nonexistent.sql", trace.path()}); | |
| EXPECT_NE(result.exit_code, 0); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, ClassicFullSort) { | |
| auto trace = WriteSimpleSystrace(); | |
| auto result = RunShell({"--full-sort", "-Q", "SELECT 1", trace.path()}); | |
| EXPECT_EQ(result.exit_code, 0); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, ClassicNoFtraceRaw) { | |
| auto trace = WriteSimpleSystrace(); | |
| auto result = RunShell({"--no-ftrace-raw", "-Q", "SELECT 1", trace.path()}); | |
| EXPECT_EQ(result.exit_code, 0); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, ClassicCropTrackEvents) { | |
| auto trace = WriteSimpleSystrace(); | |
| auto result = | |
| RunShell({"--crop-track-events", "-Q", "SELECT 1", trace.path()}); | |
| EXPECT_EQ(result.exit_code, 0); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, ClassicAnalyzeProtoContent) { | |
| auto trace = WriteSimpleSystrace(); | |
| auto result = RunShell( | |
| {"--analyze-trace-proto-content", "-Q", "SELECT 1", trace.path()}); | |
| EXPECT_EQ(result.exit_code, 0); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, ClassicExport) { | |
| auto trace = WriteSimpleSystrace(); | |
| auto out_db = base::TempFile::Create().ValueOrDie(); | |
| auto result = RunShell({"-e", out_db.path(), trace.path()}); | |
| EXPECT_EQ(result.exit_code, 0); | |
| EXPECT_TRUE(base::FileExists(out_db.path())); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, ClassicSummary) { | |
| auto trace = WriteSimpleSystrace(); | |
| auto result = RunShell({"--summary", trace.path()}); | |
| EXPECT_EQ(result.exit_code, 0); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, ClassicDev) { | |
| auto trace = WriteSimpleSystrace(); | |
| auto result = RunShell({"--dev", "-Q", "SELECT 1", trace.path()}); | |
| EXPECT_EQ(result.exit_code, 0); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, ClassicExtraChecks) { | |
| auto trace = WriteSimpleSystrace(); | |
| auto result = RunShell({"--extra-checks", "-Q", "SELECT 1", trace.path()}); | |
| EXPECT_EQ(result.exit_code, 0); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, ClassicSummaryAndMetricsConflict) { | |
| auto trace = WriteSimpleSystrace(); | |
| auto result = | |
| RunShell({"--summary", "--run-metrics", "android_cpu", trace.path()}); | |
| EXPECT_NE(result.exit_code, 0); | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Classic-to-subcommand translation tests | |
| // These verify that classic flags are correctly translated to subcommand | |
| // invocations by the translation layer. | |
| // --------------------------------------------------------------------------- | |
| TEST(TraceProcessorShellIntegrationTest, ClassicBareTraceIsInteractive) { | |
| // Bare trace file with no flags -> interactive. Prove it entered the shell | |
| // by piping a computed query whose result (123) differs from the input SQL. | |
| auto trace = WriteSimpleSystrace(); | |
| base::Subprocess p; | |
| p.args.exec_cmd = {ShellPath(), trace.path()}; | |
| p.args.stdin_mode = base::Subprocess::InputMode::kBuffer; | |
| p.args.stdout_mode = base::Subprocess::OutputMode::kBuffer; | |
| p.args.stderr_mode = base::Subprocess::OutputMode::kBuffer; | |
| p.args.input = "SELECT 100 + 23 AS computed;\n"; | |
| p.Start(); | |
| ASSERT_TRUE(p.Wait(kDefaultTestTimeoutMs)); | |
| EXPECT_EQ(p.returncode(), 0); | |
| // "123" only appears in the computed result, not in the input "100 + 23". | |
| EXPECT_THAT(p.output(), HasSubstr("123")); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, ClassicQueryFileInteractive) { | |
| // -q file -i -> query -f file -i (interactive after query). | |
| // The query creates a table; the interactive shell queries it with a computed | |
| // expression whose result (579) differs from the input. | |
| auto trace = WriteSimpleSystrace(); | |
| auto sql = | |
| WriteTempFile("CREATE PERFETTO TABLE __test AS SELECT 500 AS val;"); | |
| base::Subprocess p; | |
| p.args.exec_cmd = {ShellPath(), "-q", sql.path(), "-i", trace.path()}; | |
| p.args.stdin_mode = base::Subprocess::InputMode::kBuffer; | |
| p.args.stdout_mode = base::Subprocess::OutputMode::kBuffer; | |
| p.args.stderr_mode = base::Subprocess::OutputMode::kBuffer; | |
| p.args.input = "SELECT val + 79 AS result FROM __test;\n"; | |
| p.Start(); | |
| ASSERT_TRUE(p.Wait(kDefaultTestTimeoutMs)); | |
| EXPECT_EQ(p.returncode(), 0); | |
| // "579" only appears in the computed result, not in the SQL or table data. | |
| EXPECT_THAT(p.output(), HasSubstr("579")); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, ClassicQueryStringWide) { | |
| // -Q "SQL" -W -> query -W trace "SQL". Use a computed value (357) that | |
| // doesn't appear in the input SQL (300 + 57). | |
| auto trace = WriteSimpleSystrace(); | |
| auto result = | |
| RunShell({"-W", "-Q", "SELECT 300 + 57 AS wide_col", trace.path()}); | |
| EXPECT_EQ(result.exit_code, 0); | |
| EXPECT_THAT(result.out, HasSubstr("357")); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, ClassicStdiod) { | |
| // --stdiod -> server stdio | |
| TraceProcessorRpcStream req; | |
| auto* rpc = req.add_msg(); | |
| rpc->set_request(TraceProcessorRpc::TPM_QUERY_STREAMING); | |
| rpc->mutable_query_args()->set_sql_query("SELECT 1 AS x"); | |
| base::Subprocess process({ShellPath(), "--stdiod"}); | |
| process.args.stdin_mode = base::Subprocess::InputMode::kBuffer; | |
| process.args.stdout_mode = base::Subprocess::OutputMode::kBuffer; | |
| // Keep diagnostics separate from the binary RPC response. | |
| process.args.stderr_mode = base::Subprocess::OutputMode::kInherit; | |
| process.args.input = req.SerializeAsString(); | |
| process.Start(); | |
| ASSERT_TRUE(process.Wait(kDefaultTestTimeoutMs)); | |
| TraceProcessorRpcStream stream; | |
| stream.ParseFromString(process.output()); | |
| ASSERT_THAT(stream.msg(), SizeIs(1)); | |
| ASSERT_EQ(stream.msg()[0].response(), TraceProcessorRpc::TPM_QUERY_STREAMING); | |
| } | |
| #if !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) | |
| // Polls until |path| exists (or absence, if |want_exists| is false), up to | |
| // ~5s. Returns whether the desired state was reached. | |
| bool WaitForFileState(const std::string& path, bool want_exists) { | |
| for (int i = 0; i < 500; ++i) { | |
| if (base::FileExists(path) == want_exists) | |
| return true; | |
| base::SleepMicroseconds(10 * 1000); | |
| } | |
| return base::FileExists(path) == want_exists; | |
| } | |
| // Polls until |path| exists and is a socket (i.e. the server has bound it), up | |
| // to ~5s. Distinguishes a freshly-bound socket from a leftover regular file. | |
| bool WaitForSocketBound(const std::string& path) { | |
| for (int i = 0; i < 500; ++i) { | |
| struct stat st{}; | |
| if (stat(path.c_str(), &st) == 0 && S_ISSOCK(st.st_mode)) | |
| return true; | |
| base::SleepMicroseconds(10 * 1000); | |
| } | |
| return false; | |
| } | |
| // Binds an ephemeral TCP port on 127.0.0.1 and returns it (the socket is closed | |
| // on return, so there is a small TOCTOU window before the server rebinds it). | |
| int GetFreeInetPort() { | |
| auto sock = base::UnixSocketRaw::CreateMayFail(base::SockFamily::kInet, | |
| base::SockType::kStream); | |
| PERFETTO_CHECK(sock && sock.Bind("127.0.0.1:0")); | |
| std::string addr = sock.GetSockAddr(); // "127.0.0.1:PORT" | |
| return base::StringToInt32(addr.substr(addr.rfind(':') + 1)).value_or(0); | |
| } | |
| // Polls until a TCP connect to 127.0.0.1:|port| succeeds, up to ~5s. | |
| bool WaitForTcpPort(int port) { | |
| std::string addr = "127.0.0.1:" + std::to_string(port); | |
| for (int i = 0; i < 500; ++i) { | |
| auto sock = base::UnixSocketRaw::CreateMayFail(base::SockFamily::kInet, | |
| base::SockType::kStream); | |
| sock.SetBlocking(true); | |
| if (sock && sock.Connect(addr)) | |
| return true; | |
| base::SleepMicroseconds(10 * 1000); | |
| } | |
| return false; | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, ServerUnixBindAndCleanup) { | |
| // `server unix` binds the socket, then unlinks it on SIGTERM. | |
| auto trace = WriteSimpleSystrace(); | |
| base::TempDir dir = base::TempDir::Create(); | |
| std::string sock = dir.path() + "/s.sock"; | |
| base::Subprocess server( | |
| {ShellPath(), "server", "unix", "--path", sock, trace.path()}); | |
| server.args.stdout_mode = base::Subprocess::OutputMode::kDevNull; | |
| server.args.stderr_mode = base::Subprocess::OutputMode::kDevNull; | |
| server.Start(); | |
| EXPECT_TRUE(WaitForSocketBound(sock)); | |
| server.KillAndWaitForTermination(SIGTERM); | |
| EXPECT_TRUE(WaitForFileState(sock, /*want_exists=*/false)); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, ServerUnixNoTraceFile) { | |
| // Like http mode, `server unix` may start without a trace file (a client can | |
| // load one later over the wire). | |
| base::TempDir dir = base::TempDir::Create(); | |
| std::string sock = dir.path() + "/s.sock"; | |
| base::Subprocess server({ShellPath(), "server", "unix", "--path", sock, | |
| "--idle-timeout", "never"}); | |
| server.args.stdout_mode = base::Subprocess::OutputMode::kDevNull; | |
| server.args.stderr_mode = base::Subprocess::OutputMode::kDevNull; | |
| server.Start(); | |
| EXPECT_TRUE(WaitForSocketBound(sock)); | |
| EXPECT_EQ(server.status(), base::Subprocess::kRunning); | |
| server.KillAndWaitForTermination(SIGTERM); | |
| EXPECT_TRUE(WaitForFileState(sock, /*want_exists=*/false)); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, ServerUnixStaleSocketCleanup) { | |
| // A leftover socket file from a dead server is cleaned up and rebound. | |
| auto trace = WriteSimpleSystrace(); | |
| base::TempDir dir = base::TempDir::Create(); | |
| std::string sock = dir.path() + "/s.sock"; | |
| // Pre-create a stale file at the socket path (nothing is listening on it). | |
| base::WriteAll(base::OpenFile(sock, O_WRONLY | O_CREAT, 0600).get(), "x", 1); | |
| ASSERT_TRUE(base::FileExists(sock)); | |
| base::Subprocess server( | |
| {ShellPath(), "server", "unix", "--path", sock, trace.path()}); | |
| server.args.stdout_mode = base::Subprocess::OutputMode::kDevNull; | |
| server.args.stderr_mode = base::Subprocess::OutputMode::kDevNull; | |
| server.Start(); | |
| // It should start successfully (stale file removed and rebound). | |
| EXPECT_TRUE(WaitForSocketBound(sock)); | |
| EXPECT_EQ(server.status(), base::Subprocess::kRunning); | |
| server.KillAndWaitForTermination(SIGTERM); | |
| // Wait for the socket to be unlinked before the TempDir is torn down. | |
| EXPECT_TRUE(WaitForFileState(sock, /*want_exists=*/false)); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, ServerUnixIdleReap) { | |
| // With --idle-start last-query the idle clock is always armed, so an idle | |
| // server reaps itself (and unlinks its socket) after the timeout. | |
| auto trace = WriteSimpleSystrace(); | |
| base::TempDir dir = base::TempDir::Create(); | |
| std::string sock = dir.path() + "/s.sock"; | |
| base::Subprocess server({ShellPath(), "server", "unix", "--path", sock, | |
| "--idle-start", "last-query", "--idle-timeout", "1s", | |
| trace.path()}); | |
| server.args.stdout_mode = base::Subprocess::OutputMode::kDevNull; | |
| server.args.stderr_mode = base::Subprocess::OutputMode::kDevNull; | |
| server.Start(); | |
| ASSERT_TRUE(WaitForSocketBound(sock)); | |
| // No queries: it should self-reap well within this window. | |
| EXPECT_TRUE(server.Wait(/*timeout_ms=*/15000)); | |
| EXPECT_EQ(server.status(), base::Subprocess::kTerminated); | |
| EXPECT_TRUE(WaitForFileState(sock, /*want_exists=*/false)); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, RemoteQueryRoundTrip) { | |
| // A `query --remote <sock>` runs against a warm `server unix` and returns the | |
| // same result the local path would, exercising the full RemoteTraceProcessor | |
| // round-trip (request marshalling + CellsBatch decode). | |
| auto trace = WriteSimpleSystrace(); | |
| base::TempDir dir = base::TempDir::Create(); | |
| std::string sock = dir.path() + "/s.sock"; | |
| base::Subprocess server( | |
| {ShellPath(), "server", "unix", "--path", sock, trace.path()}); | |
| server.args.stdout_mode = base::Subprocess::OutputMode::kDevNull; | |
| server.args.stderr_mode = base::Subprocess::OutputMode::kDevNull; | |
| server.Start(); | |
| ASSERT_TRUE(WaitForSocketBound(sock)); | |
| // Two queries on two independent connections: validates that the shared | |
| // server tolerates a fresh client (seq reset) each time. | |
| auto r1 = RunShell({"query", "--remote", sock, "SELECT 200 + 61 AS v"}); | |
| EXPECT_EQ(r1.exit_code, 0) << r1.out; | |
| EXPECT_THAT(r1.out, HasSubstr("261")); | |
| auto r2 = RunShell({"query", "--remote", sock, | |
| "SELECT 'hello' AS s, count(*) AS n FROM slice"}); | |
| EXPECT_EQ(r2.exit_code, 0) << r2.out; | |
| EXPECT_THAT(r2.out, HasSubstr("hello")); | |
| // A SQL error is surfaced with a non-zero exit code. | |
| auto r3 = | |
| RunShell({"query", "--remote", sock, "SELECT * FROM no_such_table"}); | |
| EXPECT_NE(r3.exit_code, 0); | |
| // -m against a remote session enables metatracing server-side and collects | |
| // the resulting trace client-side. | |
| base::TempFile mt = base::TempFile::Create().ValueOrDie(); | |
| auto r4 = RunShell({"query", "--remote", sock, "-m", mt.path(), | |
| "SELECT count(*) FROM slice"}); | |
| EXPECT_EQ(r4.exit_code, 0) << r4.out; | |
| std::string mt_data; | |
| EXPECT_TRUE(base::ReadFile(mt.path(), &mt_data)); | |
| EXPECT_GT(mt_data.size(), 0u); | |
| server.KillAndWaitForTermination(SIGTERM); | |
| EXPECT_TRUE(WaitForFileState(sock, /*want_exists=*/false)); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, ServerKill) { | |
| // `server kill` shuts down a running session and unlinks its socket. | |
| auto trace = WriteSimpleSystrace(); | |
| base::TempDir dir = base::TempDir::Create(); | |
| std::string sock = dir.path() + "/s.sock"; | |
| base::Subprocess server({ShellPath(), "server", "unix", "--path", sock, | |
| "--idle-timeout", "never", trace.path()}); | |
| server.args.stdout_mode = base::Subprocess::OutputMode::kDevNull; | |
| server.args.stderr_mode = base::Subprocess::OutputMode::kDevNull; | |
| server.Start(); | |
| ASSERT_TRUE(WaitForSocketBound(sock)); | |
| auto kill = RunShell({"server", "kill", sock}); | |
| EXPECT_EQ(kill.exit_code, 0) << kill.out; | |
| EXPECT_TRUE(server.Wait(/*timeout_ms=*/5000)); | |
| EXPECT_TRUE(WaitForFileState(sock, /*want_exists=*/false)); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, ServerKillNoSession) { | |
| auto result = RunShell({"server", "kill", "no-such-session"}); | |
| EXPECT_NE(result.exit_code, 0); | |
| EXPECT_THAT(result.out, HasSubstr("No live session")); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, ServerKillHttpDeferred) { | |
| auto result = RunShell({"server", "kill", "localhost:9001"}); | |
| EXPECT_NE(result.exit_code, 0); | |
| EXPECT_THAT(result.out, HasSubstr("not supported")); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, RemoteWebSocketRoundTrip) { | |
| // `query --remote host:port` connects to the http server's /websocket | |
| // endpoint and runs over the same RPC byte-pipe as the unix transport. | |
| auto trace = WriteSimpleSystrace(); | |
| int port = GetFreeInetPort(); | |
| std::string addr = "127.0.0.1:" + std::to_string(port); | |
| base::Subprocess server({ShellPath(), "server", "http", "--ip-address", | |
| "127.0.0.1", "--port", std::to_string(port), | |
| trace.path()}); | |
| server.args.stdout_mode = base::Subprocess::OutputMode::kDevNull; | |
| server.args.stderr_mode = base::Subprocess::OutputMode::kDevNull; | |
| server.Start(); | |
| ASSERT_TRUE(WaitForTcpPort(port)); | |
| auto r = RunShell({"query", "--remote", addr, "SELECT 200 + 61 AS v"}); | |
| EXPECT_EQ(r.exit_code, 0) << r.out; | |
| EXPECT_THAT(r.out, HasSubstr("261")); | |
| server.KillAndWaitForTermination(SIGTERM); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, RemoteNoSession) { | |
| // Querying a session that isn't running fails with a clear, actionable error. | |
| auto result = RunShell({"query", "--remote", "no-such-session", "SELECT 1"}); | |
| EXPECT_NE(result.exit_code, 0); | |
| EXPECT_THAT(result.out, HasSubstr("No live session")); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, RemoteHttpNoServer) { | |
| // --remote to an HTTP address with nothing listening fails to connect. | |
| int port = GetFreeInetPort(); // Free now; nothing is listening. | |
| auto result = RunShell( | |
| {"query", "--remote", "127.0.0.1:" + std::to_string(port), "SELECT 1"}); | |
| EXPECT_NE(result.exit_code, 0); | |
| EXPECT_THAT(result.out, HasSubstr("Could not connect")); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, RemoteRejectsIncompatibleFlags) { | |
| // Global flags that configure local parsing or register local engine state | |
| // cannot be honored over --remote (the trace is already loaded server-side), | |
| // so they are rejected explicitly rather than silently ignored. The check | |
| // runs before connecting, so no server is needed. | |
| auto r1 = RunShell({"query", "--remote", "some-session", "--add-sql-package", | |
| "/tmp/p@x", "SELECT 1"}); | |
| EXPECT_NE(r1.exit_code, 0); | |
| EXPECT_THAT(r1.out, HasSubstr("--add-sql-package")); | |
| EXPECT_THAT(r1.out, HasSubstr("cannot be combined with --remote")); | |
| auto r2 = RunShell( | |
| {"query", "--remote", "some-session", "--full-sort", "SELECT 1"}); | |
| EXPECT_NE(r2.exit_code, 0); | |
| EXPECT_THAT(r2.out, HasSubstr("--full-sort")); | |
| EXPECT_THAT(r2.out, HasSubstr("cannot be combined with --remote")); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, | |
| RemoteInteractiveAbandonedQueryNoDesync) { | |
| // Regression test: abandoning a multi-message streaming result mid-iteration | |
| // must not corrupt the shared socket for the next query. The first query | |
| // returns >50000 rows, which the server splits into multiple batches (one | |
| // socket message each); typing 'q' at the interactive pager after the first | |
| // 32-row page abandons the iterator with later messages still queued. | |
| // RemoteIteratorImpl drains them on destruction, so the second query reads | |
| // its own response (123) rather than leftover batches from the first. | |
| auto trace = WriteSimpleSystrace(); | |
| base::TempDir dir = base::TempDir::Create(); | |
| std::string sock = dir.path() + "/s.sock"; | |
| base::Subprocess server( | |
| {ShellPath(), "server", "unix", "--path", sock, trace.path()}); | |
| server.args.stdout_mode = base::Subprocess::OutputMode::kDevNull; | |
| server.args.stderr_mode = base::Subprocess::OutputMode::kDevNull; | |
| server.Start(); | |
| ASSERT_TRUE(WaitForSocketBound(sock)); | |
| base::Subprocess p; | |
| p.args.exec_cmd = {ShellPath(), "interactive", "--remote", sock}; | |
| p.args.stdin_mode = base::Subprocess::InputMode::kBuffer; | |
| p.args.stdout_mode = base::Subprocess::OutputMode::kBuffer; | |
| p.args.stderr_mode = base::Subprocess::OutputMode::kBuffer; | |
| p.args.input = | |
| "WITH RECURSIVE c(x) AS (SELECT 1 UNION ALL SELECT x + 1 FROM c WHERE x " | |
| "< " | |
| "60000) SELECT x FROM c;\n" | |
| "q\n" | |
| "SELECT 100 + 23 AS computed;\n"; | |
| p.Start(); | |
| ASSERT_TRUE(p.Wait(kDefaultTestTimeoutMs)); | |
| EXPECT_EQ(p.returncode(), 0) << p.output(); | |
| // "123" only appears in the second query's result, not in any input SQL, so | |
| // its presence proves the second query round-tripped on a clean socket. | |
| EXPECT_THAT(p.output(), HasSubstr("123")); | |
| server.KillAndWaitForTermination(SIGTERM); | |
| EXPECT_TRUE(WaitForFileState(sock, /*want_exists=*/false)); | |
| } | |
| #endif // !PERFETTO_OS_WIN | |
| TEST(TraceProcessorShellIntegrationTest, ClassicStdiodWithTrace) { | |
| // --stdiod trace -> server stdio trace | |
| // Verify the translation works: process starts and exits cleanly. | |
| auto trace = WriteSimpleSystrace(); | |
| base::Subprocess process({ShellPath(), "--stdiod", trace.path()}); | |
| process.args.stdin_mode = base::Subprocess::InputMode::kBuffer; | |
| process.args.stdout_mode = base::Subprocess::OutputMode::kBuffer; | |
| process.args.stderr_mode = base::Subprocess::OutputMode::kBuffer; | |
| process.args.input = ""; // Empty stdin -> server exits. | |
| process.Start(); | |
| ASSERT_TRUE(process.Wait(kDefaultTestTimeoutMs)); | |
| EXPECT_EQ(process.returncode(), 0); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, ClassicRunMetrics) { | |
| // --run-metrics -> metrics --run. The metric output is a textproto | |
| // containing the metric name as a field. | |
| auto trace = WriteSimpleSystrace(); | |
| auto result = RunShell({"--run-metrics", "android_cpu", trace.path()}); | |
| EXPECT_EQ(result.exit_code, 0); | |
| // The output is a textproto with "android_cpu {" as a top-level field. | |
| EXPECT_THAT(result.out, HasSubstr("android_cpu {")); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, ClassicExportQueryDisallowed) { | |
| // -e + -q should be disallowed with a clear error message. | |
| auto trace = WriteSimpleSystrace(); | |
| auto query = WriteTempFile("SELECT 1;"); | |
| auto out_db = base::TempFile::Create().ValueOrDie(); | |
| auto result = | |
| RunShell({"-e", out_db.path(), "-q", query.path(), trace.path()}); | |
| EXPECT_NE(result.exit_code, 0); | |
| EXPECT_THAT(result.out, HasSubstr("Cannot combine")); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, ClassicPerfFile) { | |
| // --perf-file FILE -Q "SQL" -> query --perf-file FILE trace "SQL" | |
| auto trace = WriteSimpleSystrace(); | |
| auto perf = base::TempFile::Create().ValueOrDie(); | |
| auto result = RunShell( | |
| {"--perf-file", perf.path(), "-Q", "SELECT 200 + 61", trace.path()}); | |
| EXPECT_EQ(result.exit_code, 0); | |
| // Verify perf file was written with timing data. | |
| std::string perf_content; | |
| ASSERT_TRUE(base::ReadFile(perf.path(), &perf_content)); | |
| EXPECT_THAT(perf_content, Not(IsEmpty())); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, ClassicSummaryWithQuery) { | |
| // --summary -q file -> summarize --post-query file | |
| // Use computed value: 700 + 31 = 731, "731" not in input SQL. | |
| auto trace = WriteSimpleSystrace(); | |
| auto query = WriteTempFile("SELECT 700 + 31 AS post_query_col;"); | |
| auto result = RunShell({"--summary", "-q", query.path(), trace.path()}); | |
| EXPECT_EQ(result.exit_code, 0); | |
| EXPECT_THAT(result.out, HasSubstr("731")); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, ClassicMetricsWithQuery) { | |
| // --run-metrics x -q file -> metrics --run x --post-query file | |
| // Use computed value: 800 + 19 = 819, "819" not in input SQL. | |
| auto trace = WriteSimpleSystrace(); | |
| auto query = WriteTempFile("SELECT 800 + 19 AS post_query_col;"); | |
| auto result = RunShell( | |
| {"--run-metrics", "android_cpu", "-q", query.path(), trace.path()}); | |
| EXPECT_EQ(result.exit_code, 0); | |
| EXPECT_THAT(result.out, HasSubstr("819")); | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Query subcommand tests | |
| // --------------------------------------------------------------------------- | |
| TEST(TraceProcessorShellIntegrationTest, QueryWithQueryFile) { | |
| auto trace = WriteSimpleSystrace(); | |
| auto query = WriteTempFile("SELECT 42 AS val;"); | |
| auto result = RunShell({"query", "-f", query.path(), trace.path()}); | |
| EXPECT_EQ(result.exit_code, 0); | |
| EXPECT_THAT(result.out, HasSubstr("val")); | |
| EXPECT_THAT(result.out, HasSubstr("42")); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, QueryWithPositionalSql) { | |
| auto trace = WriteSimpleSystrace(); | |
| auto result = RunShell({"query", trace.path(), "SELECT 99 AS num"}); | |
| EXPECT_EQ(result.exit_code, 0); | |
| EXPECT_THAT(result.out, HasSubstr("num")); | |
| EXPECT_THAT(result.out, HasSubstr("99")); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, QueryWithStdinPipe) { | |
| auto trace = WriteSimpleSystrace(); | |
| // Use -f - to read from stdin; RunShell uses /dev/null for stdin so this | |
| // will produce an empty query which should fail. | |
| auto result = RunShell({"query", "-f", "-", trace.path()}); | |
| // Empty SQL from /dev/null should still succeed (no statements = ok). | |
| // The behaviour depends on RunQueries handling empty input. | |
| // Either way the process should not crash. | |
| EXPECT_TRUE(result.exit_code == 0 || result.exit_code == 1); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, QueryHelp) { | |
| auto result = RunShell({"query", "-h"}); | |
| EXPECT_EQ(result.exit_code, 0); | |
| EXPECT_THAT(result.out, HasSubstr("query")); | |
| EXPECT_THAT(result.out, HasSubstr("trace_file")); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, QueryVersion) { | |
| auto result = RunShell({"query", "-v"}); | |
| EXPECT_EQ(result.exit_code, 0); | |
| EXPECT_THAT(result.out, HasSubstr("Trace Processor RPC API version")); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, QueryWithDevFlag) { | |
| auto trace = WriteSimpleSystrace(); | |
| auto result = RunShell({"--dev", "query", trace.path(), "SELECT 1 AS x"}); | |
| EXPECT_EQ(result.exit_code, 0); | |
| EXPECT_THAT(result.out, HasSubstr("x")); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, QueryNoSqlError) { | |
| auto trace = WriteSimpleSystrace(); | |
| // stdin is /dev/null (not a tty), so the subcommand reads empty SQL from it | |
| // and RunQueries returns an error about no valid SQL. | |
| auto result = RunShell({"query", trace.path()}); | |
| EXPECT_NE(result.exit_code, 0); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, QueryCommentsOnlyNoValidSql) { | |
| // A query containing no executable statement (only comments/whitespace) | |
| // must fail with the "No valid SQL to run" error, not silently succeed. | |
| auto trace = WriteSimpleSystrace(); | |
| auto result = RunShell({"query", trace.path(), "/* nothing to run */"}); | |
| EXPECT_NE(result.exit_code, 0); | |
| EXPECT_THAT(result.out, HasSubstr("No valid SQL to run")); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, QuerySuppressedZeroRowStatement) { | |
| // The suppress_query_output escape hatch must hold even when the | |
| // suppressed statement matches zero rows: no header line may leak. | |
| // Match the CSV-quoted header: debug builds echo the raw query text in a | |
| // log line, so the bare column name always appears in the output. | |
| auto trace = WriteSimpleSystrace(); | |
| auto result = RunShell({"query", trace.path(), | |
| "SELECT 1 AS suppress_query_output WHERE 0; " | |
| "SELECT 200 + 61 AS real_output"}); | |
| EXPECT_EQ(result.exit_code, 0) << result.out; | |
| EXPECT_THAT(result.out, Not(HasSubstr("\"suppress_query_output\""))); | |
| EXPECT_THAT(result.out, HasSubstr("261")); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, QueryZeroRowResultPrintsHeader) { | |
| // A plain zero-row result set is not suppressed: its header still prints. | |
| auto trace = WriteSimpleSystrace(); | |
| auto result = RunShell( | |
| {"query", trace.path(), "SELECT 1 AS empty_but_visible WHERE 0"}); | |
| EXPECT_EQ(result.exit_code, 0) << result.out; | |
| EXPECT_THAT(result.out, HasSubstr("empty_but_visible")); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, QueryAgentMode) { | |
| auto trace = WriteSimpleSystrace(); | |
| auto result = RunShell( | |
| {"query", "--agent", trace.path(), "SELECT 1 AS a; SELECT nope"}); | |
| EXPECT_EQ(result.exit_code, 1) << result.out; | |
| // Results printed before the error come first. | |
| EXPECT_THAT(result.out, | |
| HasSubstr("| a |\n|---|\n| 1 |\n" | |
| R"({"error":"no such column: nope","statement":2,)")); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, QueryNoTraceError) { | |
| auto result = RunShell({"query"}); | |
| EXPECT_NE(result.exit_code, 0); | |
| EXPECT_THAT(result.out, HasSubstr("trace file is required")); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, QueryBadTraceFile) { | |
| auto result = RunShell({"query", "/nonexistent_trace.pb", "SELECT 1"}); | |
| EXPECT_NE(result.exit_code, 0); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, QuerySubcommandInteractive) { | |
| // -i runs the query then drops into the interactive shell. We prove the | |
| // shell actually started by having the query CREATE a table (via positional | |
| // SQL so stdin is NOT consumed), then piping a SELECT on that table via | |
| // stdin to the REPL. | |
| auto trace = WriteSimpleSystrace(); | |
| base::Subprocess p; | |
| p.args.exec_cmd = {ShellPath(), "query", "-i", trace.path(), | |
| "CREATE PERFETTO TABLE __test AS SELECT 500 AS val;"}; | |
| p.args.stdin_mode = base::Subprocess::InputMode::kBuffer; | |
| p.args.stdout_mode = base::Subprocess::OutputMode::kBuffer; | |
| p.args.stderr_mode = base::Subprocess::OutputMode::kBuffer; | |
| // Compute 500 + 179 = 679 in the interactive shell. "679" doesn't appear | |
| // in any input text, proving the shell ran and computed the result. | |
| p.args.input = "SELECT val + 179 AS result FROM __test;\n"; | |
| p.Start(); | |
| ASSERT_TRUE(p.Wait(kDefaultTestTimeoutMs)); | |
| EXPECT_EQ(p.returncode(), 0); | |
| EXPECT_THAT(p.output(), HasSubstr("679")); | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Subcommand: interactive | |
| // --------------------------------------------------------------------------- | |
| TEST(TraceProcessorShellIntegrationTest, InteractiveSubcommand) { | |
| // With stdin=/dev/null the REPL exits immediately. | |
| auto trace = WriteSimpleSystrace(); | |
| auto result = RunShell({"interactive", trace.path()}); | |
| EXPECT_EQ(result.exit_code, 0); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, InteractiveSubcommandWide) { | |
| auto trace = WriteSimpleSystrace(); | |
| auto result = RunShell({"interactive", "-W", trace.path()}); | |
| EXPECT_EQ(result.exit_code, 0); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, InteractiveSubcommandNoTraceFails) { | |
| auto result = RunShell({"interactive"}); | |
| EXPECT_NE(result.exit_code, 0); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, | |
| InteractiveSubcommandGlobalFlagBefore) { | |
| auto trace = WriteSimpleSystrace(); | |
| auto result = RunShell({"--dev", "interactive", trace.path()}); | |
| EXPECT_EQ(result.exit_code, 0); | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Subcommand: server | |
| // --------------------------------------------------------------------------- | |
| TEST(TraceProcessorShellIntegrationTest, ServerSubcommandStdio) { | |
| TraceProcessorRpcStream req; | |
| auto* rpc = req.add_msg(); | |
| rpc->set_request(TraceProcessorRpc::TPM_QUERY_STREAMING); | |
| rpc->mutable_query_args()->set_sql_query("SELECT 1 AS x"); | |
| base::Subprocess process({ShellPath(), "server", "stdio"}); | |
| process.args.stdin_mode = base::Subprocess::InputMode::kBuffer; | |
| process.args.stdout_mode = base::Subprocess::OutputMode::kBuffer; | |
| // Keep diagnostics separate from the binary RPC response. | |
| process.args.stderr_mode = base::Subprocess::OutputMode::kInherit; | |
| process.args.input = req.SerializeAsString(); | |
| process.Start(); | |
| ASSERT_TRUE(process.Wait(kDefaultTestTimeoutMs)); | |
| TraceProcessorRpcStream stream; | |
| stream.ParseFromString(process.output()); | |
| ASSERT_THAT(stream.msg(), SizeIs(1)); | |
| ASSERT_EQ(stream.msg()[0].response(), TraceProcessorRpc::TPM_QUERY_STREAMING); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, ServerSubcommandNoModeFails) { | |
| auto result = RunShell({"server"}); | |
| EXPECT_NE(result.exit_code, 0); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, ServerSubcommandBadModeFails) { | |
| auto result = RunShell({"server", "badmode"}); | |
| EXPECT_NE(result.exit_code, 0); | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Subcommand: summarize | |
| // --------------------------------------------------------------------------- | |
| TEST(TraceProcessorShellIntegrationTest, SummarizeSubcommand) { | |
| auto trace = WriteSimpleSystrace(); | |
| auto result = RunShell({"summarize", trace.path()}); | |
| EXPECT_EQ(result.exit_code, 0); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, SummarizeSubcommandNoTraceFails) { | |
| auto result = RunShell({"summarize"}); | |
| EXPECT_NE(result.exit_code, 0); | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Subcommand: export | |
| // --------------------------------------------------------------------------- | |
| TEST(TraceProcessorShellIntegrationTest, ExportSubcommandSqlite) { | |
| auto trace = WriteSimpleSystrace(); | |
| auto out_db = base::TempFile::Create().ValueOrDie(); | |
| auto result = | |
| RunShell({"export", "sqlite", "-o", out_db.path(), trace.path()}); | |
| EXPECT_EQ(result.exit_code, 0); | |
| EXPECT_TRUE(base::FileExists(out_db.path())); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, ExportSubcommandNoFormatFails) { | |
| auto trace = WriteSimpleSystrace(); | |
| auto result = RunShell({"export", trace.path()}); | |
| EXPECT_NE(result.exit_code, 0); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, ExportSubcommandNoOutputFails) { | |
| auto trace = WriteSimpleSystrace(); | |
| auto result = RunShell({"export", "sqlite", trace.path()}); | |
| EXPECT_NE(result.exit_code, 0); | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Subcommand: metrics | |
| // --------------------------------------------------------------------------- | |
| TEST(TraceProcessorShellIntegrationTest, MetricsSubcommandNoRunFails) { | |
| auto trace = WriteSimpleSystrace(); | |
| auto result = RunShell({"metrics", trace.path()}); | |
| EXPECT_NE(result.exit_code, 0); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, NoArgsShowsSubcommandHelp) { | |
| // Running with no arguments should show the subcommand help (not classic). | |
| auto result = RunShell({}); | |
| EXPECT_EQ(result.exit_code, 0); | |
| EXPECT_THAT(result.out, HasSubstr("Commands:")); | |
| EXPECT_THAT(result.out, HasSubstr("query")); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, BadTraceFileShowsOnlyError) { | |
| // When a trace file doesn't exist, only the error should be shown, | |
| // not the full usage text. | |
| auto result = RunShell({"interactive", "/nonexistent_trace.pb"}); | |
| EXPECT_NE(result.exit_code, 0); | |
| EXPECT_THAT(result.out, Not(HasSubstr("Usage:"))); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, ClassicBadTraceFileShowsOnlyError) { | |
| // Classic path: bare nonexistent file should show only an error, not usage. | |
| auto result = RunShell({"/nonexistent_trace.pb"}); | |
| EXPECT_NE(result.exit_code, 0); | |
| EXPECT_THAT(result.out, Not(HasSubstr("Usage:"))); | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Subcommand: bundle | |
| // --------------------------------------------------------------------------- | |
| // This test requires llvm-symbolize on the PATH | |
| #if !PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID) | |
| TEST(TraceProcessorShellIntegrationTest, ConvertBundleWithDebugOnlyLibraries) { | |
| base::TempFile out_file = base::TempFile::Create().ValueOrDie(); | |
| const std::string& out_path = out_file.path(); | |
| auto symbolize = [&](const std::string& in_file, | |
| const std::string& symbol_path) { | |
| std::string in_path = base::GetTestDataPath(in_file); | |
| std::string lib_path = base::GetTestDataPath(symbol_path); | |
| auto result = | |
| RunShell({"bundle", "--symbol-paths", lib_path, in_path, out_path}); | |
| if (result.exit_code == 0) { | |
| return testing::AssertionSuccess(); | |
| } | |
| return testing::AssertionFailure() << result.out; | |
| }; | |
| auto get_stack_at_ts = [&](const std::string& ts) { | |
| const std::string query_template = R"( | |
| WITH start_callsite AS ( | |
| SELECT | |
| 0 AS i, | |
| id, | |
| parent_id, | |
| frame_id | |
| FROM | |
| stack_profile_callsite | |
| WHERE | |
| id = (SELECT callsite_id FROM perf_sample WHERE ts = {ts}) | |
| ), stack AS ( | |
| SELECT * FROM start_callsite | |
| UNION ALL | |
| SELECT | |
| child.i + 1 AS i, | |
| parent.id AS id, | |
| parent.parent_id, | |
| parent.frame_id | |
| FROM | |
| stack AS child, | |
| STACK_PROFILE_CALLSITE AS parent | |
| ON child.parent_id = parent.id | |
| ), call_graph AS ( | |
| SELECT | |
| stack.i, | |
| sps.id AS symbol_id, | |
| COALESCE(sps.name, spf.name) AS name | |
| FROM | |
| stack, | |
| stack_profile_frame AS spf | |
| ON (stack.frame_id = spf.id) | |
| LEFT JOIN stack_profile_symbol AS sps | |
| USING(symbol_set_id) | |
| ) | |
| SELECT GROUP_CONCAT(name ORDER BY i DESC, symbol_id DESC) | |
| FROM call_graph; | |
| )"; | |
| auto query = WriteTempFile(base::ReplaceAll(query_template, "{ts}", ts)); | |
| return RunShell({"query", "-f", query.path(), out_path}); | |
| }; | |
| // traced_perf executable | |
| ASSERT_TRUE(symbolize("test/data/traced_perf_no_symbols.pb", | |
| "test/data/simpleperf/bin")); | |
| auto query_result = get_stack_at_ts("344782441497"); | |
| EXPECT_EQ(query_result.exit_code, 0); | |
| std::vector<std::string> expected_frames = { | |
| "__libc_init", | |
| "main", | |
| "(anonymous namespace)::DrawGame()", | |
| "(anonymous namespace)::DrawPlayer(int)", | |
| }; | |
| EXPECT_THAT(query_result.out, HasSubstr(base::Join(expected_frames, ","))); | |
| // Simpleperf executable | |
| ASSERT_TRUE( | |
| symbolize("test/data/simpleperf/sdk_example.android.simpleperf.data", | |
| "test/data/simpleperf/bin")); | |
| query_result = get_stack_at_ts("621365392837983"); | |
| EXPECT_EQ(query_result.exit_code, 0); | |
| expected_frames = { | |
| "_start_main", | |
| "__libc_init", | |
| "main", | |
| "(anonymous namespace)::DrawGame()", | |
| "(anonymous namespace)::DrawPlayer(int)", | |
| }; | |
| EXPECT_THAT(query_result.out, HasSubstr(base::Join(expected_frames, ","))); | |
| // Perf executable | |
| ASSERT_TRUE(symbolize("test/data/linux_perf/sdk_example.linux.perf.data", | |
| "test/data/linux_perf/bin")); | |
| query_result = get_stack_at_ts("210798493518940"); | |
| EXPECT_EQ(query_result.exit_code, 0); | |
| expected_frames = { | |
| "main", | |
| "(anonymous namespace)::DrawGame()", | |
| "(anonymous namespace)::DrawPlayer(int)", | |
| }; | |
| EXPECT_THAT(query_result.out, HasSubstr(base::Join(expected_frames, ","))); | |
| // traced_perf dummy_app | |
| ASSERT_TRUE(symbolize("test/data/simpleperf/unsymbolized_app_profile.data", | |
| "test/data/simpleperf/bin")); | |
| query_result = get_stack_at_ts("238583681488"); | |
| EXPECT_EQ(query_result.exit_code, 0); | |
| expected_frames = { | |
| "(anonymous namespace)::A()", | |
| "(anonymous namespace)::B()", | |
| "(anonymous namespace)::C()", | |
| }; | |
| EXPECT_THAT(query_result.out, HasSubstr(base::Join(expected_frames, ","))); | |
| // Simpleperf dummy_app | |
| ASSERT_TRUE(symbolize("test/data/unsymbolized_app_profile.pb", | |
| "test/data/simpleperf/bin")); | |
| query_result = get_stack_at_ts("36621314142"); | |
| EXPECT_EQ(query_result.exit_code, 0); | |
| expected_frames = { | |
| "(anonymous namespace)::A()", | |
| "(anonymous namespace)::B()", | |
| "(anonymous namespace)::C()", | |
| }; | |
| EXPECT_THAT(query_result.out, HasSubstr(base::Join(expected_frames, ","))); | |
| } | |
| #endif | |
| // --------------------------------------------------------------------------- | |
| // Classic: --metric-extension with --stdiod | |
| // --------------------------------------------------------------------------- | |
| TEST(TraceProcessorShellIntegrationTest, ClassicMetricExtensionWithStdiod) { | |
| // Regression test: --metric-extension was silently dropped when combined | |
| // with --stdiod (or --httpd) after the classic-to-subcommand migration. | |
| // The extension SQL should be registered and queryable via RPC. | |
| // Create a metric extension directory with sql/ and protos/ subdirs. | |
| auto ext_dir = base::TempDir::Create(); | |
| std::string sql_dir = ext_dir.path() + "/sql/"; | |
| std::string protos_dir = ext_dir.path() + "/protos/"; | |
| ASSERT_TRUE(base::Mkdir(sql_dir)); | |
| ASSERT_TRUE(base::Mkdir(protos_dir)); | |
| // Write a metric SQL file that creates a perfetto table. We verify the | |
| // extension loaded by querying that table via RPC. | |
| std::string sql_path = sql_dir + "test_ext_metric.sql"; | |
| { | |
| std::string sql_content = | |
| "CREATE PERFETTO TABLE _test_ext_marker AS SELECT 42 AS val;"; | |
| auto fd = base::OpenFile(sql_path, O_WRONLY | O_CREAT | O_TRUNC, 0644); | |
| ASSERT_TRUE(fd); | |
| base::WriteAll(*fd, sql_content.data(), sql_content.size()); | |
| } | |
| // Ensure cleanup of files/dirs so TempDir destructor succeeds. | |
| auto cleanup = base::OnScopeExit([&] { | |
| unlink(sql_path.c_str()); | |
| base::Rmdir(sql_dir); | |
| base::Rmdir(protos_dir); | |
| }); | |
| // Build RPC: first run the metric extension SQL, then query the table it | |
| // creates. | |
| TraceProcessorRpcStream req; | |
| auto* rpc = req.add_msg(); | |
| rpc->set_request(TraceProcessorRpc::TPM_QUERY_STREAMING); | |
| rpc->mutable_query_args()->set_sql_query( | |
| "SELECT RUN_METRIC('test_ext/test_ext_metric.sql')"); | |
| rpc = req.add_msg(); | |
| rpc->set_request(TraceProcessorRpc::TPM_QUERY_STREAMING); | |
| rpc->mutable_query_args()->set_sql_query("SELECT val FROM _test_ext_marker"); | |
| std::string ext_arg = ext_dir.path() + "@test_ext/"; | |
| base::Subprocess process( | |
| {ShellPath(), "--stdiod", "--metric-extension", ext_arg}); | |
| process.args.stdin_mode = base::Subprocess::InputMode::kBuffer; | |
| process.args.stdout_mode = base::Subprocess::OutputMode::kBuffer; | |
| // Keep diagnostics separate from the binary RPC response. | |
| process.args.stderr_mode = base::Subprocess::OutputMode::kInherit; | |
| process.args.input = req.SerializeAsString(); | |
| process.Start(); | |
| ASSERT_TRUE(process.Wait(kDefaultTestTimeoutMs)); | |
| EXPECT_EQ(process.returncode(), 0); | |
| TraceProcessorRpcStream stream; | |
| stream.ParseFromString(process.output()); | |
| ASSERT_THAT(stream.msg(), SizeIs(2)); | |
| // First response: RUN_METRIC should succeed. | |
| ASSERT_EQ(stream.msg()[0].response(), TraceProcessorRpc::TPM_QUERY_STREAMING); | |
| // Second response: the table created by the metric extension should exist | |
| // and contain val=42. | |
| ASSERT_EQ(stream.msg()[1].response(), TraceProcessorRpc::TPM_QUERY_STREAMING); | |
| ASSERT_THAT(stream.msg()[1].query_result().batch(), SizeIs(1)); | |
| EXPECT_THAT(stream.msg()[1].query_result().batch()[0].varint_cells(), | |
| ElementsAre(42)); | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Classic: --add-sql-package with --stdiod | |
| // --------------------------------------------------------------------------- | |
| TEST(TraceProcessorShellIntegrationTest, ClassicAddSqlPackageWithStdiod) { | |
| // Verify --add-sql-package works with --stdiod (server subcommand). | |
| // The package SQL should be includable via INCLUDE PERFETTO MODULE. | |
| // Create a package directory with a SQL file. | |
| auto pkg_dir = base::TempDir::Create(); | |
| std::string sql_path = pkg_dir.path() + "/hello.sql"; | |
| { | |
| std::string sql_content = | |
| "CREATE PERFETTO TABLE _test_pkg_marker AS SELECT 99 AS val;"; | |
| auto fd = base::OpenFile(sql_path, O_WRONLY | O_CREAT | O_TRUNC, 0644); | |
| ASSERT_TRUE(fd); | |
| base::WriteAll(*fd, sql_content.data(), sql_content.size()); | |
| } | |
| auto cleanup = base::OnScopeExit([&] { unlink(sql_path.c_str()); }); | |
| // Extract package name from dir basename. The dir path ends with a random | |
| // name like /tmp/perfetto-XXXXXX, so the package name is that basename. | |
| // Override with @testpkg for a stable name. | |
| std::string pkg_arg = pkg_dir.path() + "@testpkg"; | |
| TraceProcessorRpcStream req; | |
| // Include the module, then query the table it creates. | |
| auto* rpc = req.add_msg(); | |
| rpc->set_request(TraceProcessorRpc::TPM_QUERY_STREAMING); | |
| rpc->mutable_query_args()->set_sql_query( | |
| "INCLUDE PERFETTO MODULE testpkg.hello"); | |
| rpc = req.add_msg(); | |
| rpc->set_request(TraceProcessorRpc::TPM_QUERY_STREAMING); | |
| rpc->mutable_query_args()->set_sql_query("SELECT val FROM _test_pkg_marker"); | |
| base::Subprocess process( | |
| {ShellPath(), "--stdiod", "--add-sql-package", pkg_arg}); | |
| process.args.stdin_mode = base::Subprocess::InputMode::kBuffer; | |
| process.args.stdout_mode = base::Subprocess::OutputMode::kBuffer; | |
| // Keep diagnostics separate from the binary RPC response. | |
| process.args.stderr_mode = base::Subprocess::OutputMode::kInherit; | |
| process.args.input = req.SerializeAsString(); | |
| process.Start(); | |
| ASSERT_TRUE(process.Wait(kDefaultTestTimeoutMs)); | |
| EXPECT_EQ(process.returncode(), 0); | |
| TraceProcessorRpcStream stream; | |
| stream.ParseFromString(process.output()); | |
| ASSERT_THAT(stream.msg(), SizeIs(2)); | |
| // First response: INCLUDE should succeed. | |
| ASSERT_EQ(stream.msg()[0].response(), TraceProcessorRpc::TPM_QUERY_STREAMING); | |
| // Second response: the table should exist with val=99. | |
| ASSERT_EQ(stream.msg()[1].response(), TraceProcessorRpc::TPM_QUERY_STREAMING); | |
| ASSERT_THAT(stream.msg()[1].query_result().batch(), SizeIs(1)); | |
| EXPECT_THAT(stream.msg()[1].query_result().batch()[0].varint_cells(), | |
| ElementsAre(99)); | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Subcommand: bundle | |
| // --------------------------------------------------------------------------- | |
| namespace { | |
| // Path to a real, small Perfetto trace shipped in test/data. | |
| std::string HeapprofdTracePath() { | |
| return base::GetTestDataPath( | |
| "test/data/heapprofd_standalone_client_example-trace"); | |
| } | |
| // Parses a USTAR archive into a map of filename -> file content. | |
| // USTAR layout: each file is a 512-byte header (name at offset 0, size as | |
| // octal ASCII at offset 124) followed by the content padded to 512 bytes. | |
| std::map<std::string, std::string> ReadTarMembers(const std::string& path) { | |
| std::string bytes; | |
| PERFETTO_CHECK(base::ReadFile(path, &bytes)); | |
| std::map<std::string, std::string> out; | |
| size_t pos = 0; | |
| while (pos + 512 <= bytes.size()) { | |
| const char* header = bytes.data() + pos; | |
| if (header[0] == '\0') { | |
| break; | |
| } | |
| std::string name(header, strnlen(header, 100)); | |
| char size_buf[13] = {}; | |
| memcpy(size_buf, header + 124, 12); | |
| size_t size = static_cast<size_t>(strtoul(size_buf, nullptr, 8)); | |
| pos += 512; | |
| PERFETTO_CHECK(pos + size <= bytes.size()); | |
| out.emplace(std::move(name), bytes.substr(pos, size)); | |
| pos += ((size + 511) / 512) * 512; | |
| } | |
| return out; | |
| } | |
| // Returns the set of package_name values from every deobfuscation_mapping | |
| // packet in a serialized Trace. | |
| std::set<std::string> DeobfuscationPackages(const std::string& deob_bytes) { | |
| protos::gen::Trace trace; | |
| PERFETTO_CHECK(trace.ParseFromString(deob_bytes)); | |
| std::set<std::string> names; | |
| for (const auto& pkt : trace.packet()) { | |
| if (pkt.has_deobfuscation_mapping()) { | |
| names.insert(pkt.deobfuscation_mapping().package_name()); | |
| } | |
| } | |
| return names; | |
| } | |
| } // namespace | |
| // The bundle must carry the input trace byte-for-byte in `trace.perfetto` | |
| // and a parseable `deobfuscation.pb` whose DeobfuscationMapping names our | |
| // class and method. | |
| TEST(TraceProcessorShellIntegrationTest, BundleWithProguardMap) { | |
| auto mapping = WriteTempFile( | |
| "com.example.Foo -> a.a:\n" | |
| " void bar() -> b\n"); | |
| base::TempFile out_file = base::TempFile::Create().ValueOrDie(); | |
| const std::string& out_path = out_file.path(); | |
| auto result = RunShell({"bundle", "--no-auto-symbol-paths", "--proguard-map", | |
| "com.example=" + mapping.path(), HeapprofdTracePath(), | |
| out_path}); | |
| ASSERT_EQ(result.exit_code, 0) << result.out; | |
| auto members = ReadTarMembers(out_path); | |
| ASSERT_EQ(members.size(), 2u); | |
| std::string trace_in; | |
| ASSERT_TRUE(base::ReadFile(HeapprofdTracePath(), &trace_in)); | |
| ASSERT_TRUE(members.count("trace.perfetto")); | |
| EXPECT_EQ(members["trace.perfetto"], trace_in); | |
| ASSERT_TRUE(members.count("deobfuscation.pb")); | |
| protos::gen::Trace deob; | |
| ASSERT_TRUE(deob.ParseFromString(members["deobfuscation.pb"])); | |
| ASSERT_EQ(deob.packet().size(), 1u); | |
| const auto& dm = deob.packet()[0].deobfuscation_mapping(); | |
| EXPECT_EQ(dm.package_name(), "com.example"); | |
| ASSERT_EQ(dm.obfuscated_classes().size(), 1u); | |
| EXPECT_EQ(dm.obfuscated_classes()[0].obfuscated_name(), "a.a"); | |
| EXPECT_EQ(dm.obfuscated_classes()[0].deobfuscated_name(), "com.example.Foo"); | |
| ASSERT_EQ(dm.obfuscated_classes()[0].obfuscated_methods().size(), 1u); | |
| EXPECT_EQ( | |
| dm.obfuscated_classes()[0].obfuscated_methods()[0].obfuscated_name(), | |
| "b"); | |
| } | |
| // Repeated --proguard-map should emit one DeobfuscationMapping per input map, | |
| // each tagged with the right package name. | |
| TEST(TraceProcessorShellIntegrationTest, BundleRepeatedProguardMap) { | |
| auto m1 = WriteTempFile("com.example.Foo -> a.a:\n"); | |
| auto m2 = WriteTempFile("com.example.Bar -> b.b:\n"); | |
| base::TempFile out_file = base::TempFile::Create().ValueOrDie(); | |
| const std::string& out_path = out_file.path(); | |
| auto result = RunShell({"bundle", "--no-auto-symbol-paths", "--proguard-map", | |
| "com.example.one=" + m1.path(), "--proguard-map", | |
| "com.example.two=" + m2.path(), HeapprofdTracePath(), | |
| out_path}); | |
| ASSERT_EQ(result.exit_code, 0) << result.out; | |
| auto members = ReadTarMembers(out_path); | |
| ASSERT_TRUE(members.count("deobfuscation.pb")); | |
| EXPECT_THAT( | |
| DeobfuscationPackages(members["deobfuscation.pb"]), | |
| testing::UnorderedElementsAre("com.example.one", "com.example.two")); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, BundleMissingProguardMapFails) { | |
| base::TempFile out_file = base::TempFile::Create().ValueOrDie(); | |
| const std::string& out_path = out_file.path(); | |
| auto result = RunShell({"bundle", "--no-auto-symbol-paths", "--proguard-map", | |
| "com.example=/nonexistent/mapping.txt", | |
| HeapprofdTracePath(), out_path}); | |
| EXPECT_NE(result.exit_code, 0); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, BundleHelpShowsProguardMap) { | |
| auto result = RunShell({"help", "bundle"}); | |
| EXPECT_EQ(result.exit_code, 0); | |
| EXPECT_THAT(result.out, HasSubstr("proguard-map")); | |
| EXPECT_THAT(result.out, HasSubstr("symbol-paths")); | |
| EXPECT_THAT(result.out, HasSubstr("no-auto-proguard-maps")); | |
| } | |
| // --no-auto-proguard-maps must not suppress explicit --proguard-map values: | |
| // the packet for the explicit package still appears in the bundle. | |
| TEST(TraceProcessorShellIntegrationTest, BundleNoAutoProguardMaps) { | |
| auto mapping = WriteTempFile("com.example.Foo -> a.a:\n"); | |
| base::TempFile out_file = base::TempFile::Create().ValueOrDie(); | |
| const std::string& out_path = out_file.path(); | |
| auto result = | |
| RunShell({"bundle", "--no-auto-symbol-paths", "--no-auto-proguard-maps", | |
| "--proguard-map", "com.example=" + mapping.path(), | |
| HeapprofdTracePath(), out_path}); | |
| ASSERT_EQ(result.exit_code, 0) << result.out; | |
| auto members = ReadTarMembers(out_path); | |
| ASSERT_TRUE(members.count("deobfuscation.pb")); | |
| EXPECT_THAT(DeobfuscationPackages(members["deobfuscation.pb"]), | |
| testing::UnorderedElementsAre("com.example")); | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Existing RPC test | |
| // --------------------------------------------------------------------------- | |
| TEST(TraceProcessorShellIntegrationTest, StdioSimpleRequestResponse) { | |
| TraceProcessorRpcStream req; | |
| auto* rpc = req.add_msg(); | |
| rpc->set_append_trace_data(kSimpleSystrace.data(), kSimpleSystrace.size()); | |
| rpc->set_request(TraceProcessorRpc::TPM_APPEND_TRACE_DATA); | |
| rpc = req.add_msg(); | |
| rpc->set_request(TraceProcessorRpc::TPM_FINALIZE_TRACE_DATA); | |
| rpc = req.add_msg(); | |
| rpc->set_request(TraceProcessorRpc::TPM_QUERY_STREAMING); | |
| rpc->mutable_query_args()->set_sql_query("SELECT ts, dur FROM slice"); | |
| base::Subprocess process( | |
| {base::GetCurExecutableDir() + "/trace_processor_shell", "--stdiod"}); | |
| process.args.stdin_mode = base::Subprocess::InputMode::kBuffer; | |
| process.args.stdout_mode = base::Subprocess::OutputMode::kBuffer; | |
| process.args.stderr_mode = base::Subprocess::OutputMode::kInherit; | |
| process.args.input = req.SerializeAsString(); | |
| process.Start(); | |
| ASSERT_TRUE(process.Wait(kDefaultTestTimeoutMs)); | |
| TraceProcessorRpcStream stream; | |
| stream.ParseFromString(process.output()); | |
| ASSERT_THAT(stream.msg(), | |
| ElementsAre(Property(&TraceProcessorRpc::response, | |
| TraceProcessorRpc::TPM_APPEND_TRACE_DATA), | |
| Property(&TraceProcessorRpc::response, | |
| TraceProcessorRpc::TPM_FINALIZE_TRACE_DATA), | |
| Property(&TraceProcessorRpc::response, | |
| TraceProcessorRpc::TPM_QUERY_STREAMING))); | |
| ASSERT_THAT(stream.msg()[0].append_result().error(), IsEmpty()); | |
| ASSERT_THAT(stream.msg()[2].query_result().batch(), SizeIs(1)); | |
| ASSERT_THAT(stream.msg()[2].query_result().batch()[0].varint_cells(), | |
| ElementsAre(10852771242000, 3000)); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, StdioExport) { | |
| TraceProcessorRpcStream req; | |
| auto* rpc = req.add_msg(); | |
| rpc->set_append_trace_data(kSimpleSystrace.data(), kSimpleSystrace.size()); | |
| rpc->set_request(TraceProcessorRpc::TPM_APPEND_TRACE_DATA); | |
| rpc = req.add_msg(); | |
| rpc->set_request(TraceProcessorRpc::TPM_FINALIZE_TRACE_DATA); | |
| rpc = req.add_msg(); | |
| rpc->set_request(TraceProcessorRpc::TPM_EXPORT); | |
| rpc->mutable_export_args()->set_format(protos::gen::ExportArgs::PERFETTO); | |
| base::Subprocess process( | |
| {base::GetCurExecutableDir() + "/trace_processor_shell", "--stdiod"}); | |
| process.args.stdin_mode = base::Subprocess::InputMode::kBuffer; | |
| process.args.stdout_mode = base::Subprocess::OutputMode::kBuffer; | |
| process.args.stderr_mode = base::Subprocess::OutputMode::kInherit; | |
| process.args.input = req.SerializeAsString(); | |
| process.Start(); | |
| ASSERT_TRUE(process.Wait(kDefaultTestTimeoutMs)); | |
| TraceProcessorRpcStream stream; | |
| stream.ParseFromString(process.output()); | |
| // Two responses for append/finalize, then one response per export chunk with | |
| // a final has_more=false terminator. | |
| ASSERT_GE(stream.msg_size(), 4); | |
| ASSERT_EQ(stream.msg()[0].response(), | |
| TraceProcessorRpc::TPM_APPEND_TRACE_DATA); | |
| ASSERT_EQ(stream.msg()[1].response(), | |
| TraceProcessorRpc::TPM_FINALIZE_TRACE_DATA); | |
| std::string tar_bytes; | |
| for (size_t i = 2; i < static_cast<size_t>(stream.msg_size()); ++i) { | |
| const auto& msg = stream.msg()[i]; | |
| ASSERT_EQ(msg.response(), TraceProcessorRpc::TPM_EXPORT); | |
| ASSERT_THAT(msg.export_result().error(), IsEmpty()); | |
| ASSERT_EQ(msg.export_result().has_more(), | |
| i != static_cast<size_t>(stream.msg_size()) - 1); | |
| tar_bytes += msg.export_result().data(); | |
| } | |
| // The archive starts with the manifest entry name and has the ustar magic. | |
| ASSERT_GT(tar_bytes.size(), 512u); | |
| ASSERT_EQ(std::string(tar_bytes.data(), 22), "perfetto_manifest.json"); | |
| ASSERT_EQ(std::string(tar_bytes.data() + 257, 5), "ustar"); | |
| } | |
| TEST(TraceProcessorShellIntegrationTest, StdioExportSqlite) { | |
| TraceProcessorRpcStream req; | |
| auto* rpc = req.add_msg(); | |
| rpc->set_append_trace_data(kSimpleSystrace.data(), kSimpleSystrace.size()); | |
| rpc->set_request(TraceProcessorRpc::TPM_APPEND_TRACE_DATA); | |
| rpc = req.add_msg(); | |
| rpc->set_request(TraceProcessorRpc::TPM_FINALIZE_TRACE_DATA); | |
| rpc = req.add_msg(); | |
| rpc->set_request(TraceProcessorRpc::TPM_EXPORT); | |
| rpc->mutable_export_args()->set_format(protos::gen::ExportArgs::SQLITE); | |
| base::Subprocess process( | |
| {base::GetCurExecutableDir() + "/trace_processor_shell", "--stdiod"}); | |
| process.args.stdin_mode = base::Subprocess::InputMode::kBuffer; | |
| process.args.stdout_mode = base::Subprocess::OutputMode::kBuffer; | |
| process.args.stderr_mode = base::Subprocess::OutputMode::kInherit; | |
| process.args.input = req.SerializeAsString(); | |
| process.Start(); | |
| ASSERT_TRUE(process.Wait(kDefaultTestTimeoutMs)); | |
| TraceProcessorRpcStream stream; | |
| stream.ParseFromString(process.output()); | |
| ASSERT_GE(stream.msg_size(), 4); | |
| ASSERT_EQ(stream.msg()[0].response(), | |
| TraceProcessorRpc::TPM_APPEND_TRACE_DATA); | |
| ASSERT_EQ(stream.msg()[1].response(), | |
| TraceProcessorRpc::TPM_FINALIZE_TRACE_DATA); | |
| std::string sqlite_bytes; | |
| for (size_t i = 2; i < static_cast<size_t>(stream.msg_size()); ++i) { | |
| const auto& msg = stream.msg()[i]; | |
| ASSERT_EQ(msg.response(), TraceProcessorRpc::TPM_EXPORT); | |
| ASSERT_THAT(msg.export_result().error(), IsEmpty()); | |
| ASSERT_EQ(msg.export_result().has_more(), | |
| i != static_cast<size_t>(stream.msg_size()) - 1); | |
| sqlite_bytes += msg.export_result().data(); | |
| } | |
| ASSERT_GE(sqlite_bytes.size(), 16u); | |
| EXPECT_EQ(sqlite_bytes.substr(0, 16), std::string("SQLite format 3\0", 16)); | |
| } | |
| } // namespace | |
| } // namespace perfetto::trace_processor |