/*
 * 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();
  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();
  base::WriteAll(f.fd(), content.data(), content.size());
  return f;
}

struct SubprocessResult {
  int exit_code;
  std::string out;
};

// 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);
  }
  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();
  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;
  process.args.stderr_mode = base::Subprocess::OutputMode::kBuffer;
  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();
  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();
  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();
  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, 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;
  process.args.stderr_mode = base::Subprocess::OutputMode::kBuffer;
  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();
  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) {
  auto out_dir = base::TempDir::Create();
  std::string out_path = out_dir.path() + "/bundle.tar";
  // Clean up on every exit path (an early ASSERT included): TempDir's
  // destructor aborts the binary if the dir isn't empty.
  auto remove_bundle = base::OnScopeExit([&] { unlink(out_path.c_str()); });

  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;
  process.args.stderr_mode = base::Subprocess::OutputMode::kBuffer;
  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;
  process.args.stderr_mode = base::Subprocess::OutputMode::kBuffer;
  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");
  auto out_dir = base::TempDir::Create();
  std::string out_path = out_dir.path() + "/bundle.tar";
  auto remove_bundle = base::OnScopeExit([&] { unlink(out_path.c_str()); });

  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");
  auto out_dir = base::TempDir::Create();
  std::string out_path = out_dir.path() + "/bundle.tar";
  auto remove_bundle = base::OnScopeExit([&] { unlink(out_path.c_str()); });

  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) {
  auto out_dir = base::TempDir::Create();
  std::string out_path = out_dir.path() + "/bundle.tar";
  auto remove_bundle = base::OnScopeExit([&] { unlink(out_path.c_str()); });

  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");
  auto out_dir = base::TempDir::Create();
  std::string out_path = out_dir.path() + "/bundle.tar";
  auto remove_bundle = base::OnScopeExit([&] { unlink(out_path.c_str()); });

  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");
}

}  // namespace
}  // namespace perfetto::trace_processor
