| /* |
| * Copyright (C) 2026 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 "perfetto/ext/trace_processor/trace_processor_shell.h" |
| |
| #include <cstdlib> |
| #include <cstring> |
| #include <map> |
| #include <set> |
| #include <string> |
| #include <vector> |
| |
| #include "perfetto/base/build_config.h" |
| #include "perfetto/ext/base/file_utils.h" |
| #include "perfetto/ext/base/temp_file.h" |
| #include "perfetto/trace_processor/read_trace.h" |
| #include "perfetto/trace_processor/trace_processor.h" |
| #include "protos/perfetto/trace/ftrace/ftrace.gen.h" |
| #include "protos/perfetto/trace/ftrace/ftrace_event.gen.h" |
| #include "protos/perfetto/trace/ftrace/ftrace_event_bundle.gen.h" |
| #include "protos/perfetto/trace/interned_data/interned_data.gen.h" |
| #include "protos/perfetto/trace/profiling/deobfuscation.gen.h" |
| #include "protos/perfetto/trace/profiling/profile_common.gen.h" |
| #include "protos/perfetto/trace/trace.gen.h" |
| #include "protos/perfetto/trace/trace_packet.gen.h" |
| #include "src/base/test/utils.h" |
| #include "test/gtest_and_gmock.h" |
| |
| #if !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) |
| #include <unistd.h> |
| #endif |
| |
| namespace perfetto::trace_processor { |
| namespace { |
| |
| using testing::HasSubstr; |
| using testing::UnorderedElementsAre; |
| |
| // Helper: builds an argv from owned strings and invokes the shell's `bundle` |
| // subcommand via TraceProcessorShellMain. |
| class ArgvInvoker { |
| public: |
| void Add(const std::string& arg) { args_.push_back(arg); } |
| int Run() { |
| std::vector<char*> argv; |
| for (auto& s : args_) { |
| argv.push_back(s.data()); |
| } |
| return TraceProcessorShellMain(static_cast<int>(argv.size()), argv.data()); |
| } |
| |
| private: |
| std::vector<std::string> args_; |
| }; |
| |
| base::TempFile WriteTempFile(const std::string& content) { |
| auto f = base::TempFile::Create(); |
| PERFETTO_CHECK(base::WriteAll(f.fd(), content.data(), content.size()) == |
| static_cast<ssize_t>(content.size())); |
| return f; |
| } |
| |
| // 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; |
| // the archive ends with at least two zero-filled blocks. |
| 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; |
| // Zero-name header -> end-of-archive marker. |
| if (header[0] == '\0') { |
| break; |
| } |
| std::string name(header, strnlen(header, 100)); |
| // Size is null/space terminated octal ASCII at offset 124, up to 11 digits. |
| 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)); |
| // Content is padded to a 512-byte boundary. |
| pos += ((size + 511) / 512) * 512; |
| } |
| return out; |
| } |
| |
| class TraceconvShellBundleTest : public ::testing::Test { |
| protected: |
| void SetUp() override { |
| input_trace_ = base::GetTestDataPath( |
| "test/data/heapprofd_standalone_client_example-trace"); |
| output_path_ = output_file_.path(); |
| #if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID) |
| GTEST_SKIP() << "do not run traceconv tests on Android target"; |
| #endif |
| #if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) |
| GTEST_SKIP() << "TarWriter is not supported on Windows"; |
| #endif |
| } |
| |
| // Collects every package_name found in a deobfuscation.pb proto stream. |
| static std::set<std::string> PackageNames(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; |
| } |
| |
| base::TempDir temp_dir_ = base::TempDir::Create(); |
| base::TempFile output_file_ = base::TempFile::Create(); |
| std::string input_trace_; |
| std::string output_path_; |
| }; |
| |
| // The bundle should contain the unmodified input trace plus a parseable |
| // deobfuscation.pb whose `deobfuscation_mapping` reflects the supplied |
| // mapping.txt (package, class, method names). |
| TEST_F(TraceconvShellBundleTest, BundleWithProguardMap) { |
| base::TempFile mapping = WriteTempFile( |
| "com.example.Foo -> a.a:\n" |
| " void bar() -> b\n"); |
| |
| ArgvInvoker invoker; |
| invoker.Add("trace_processor_shell"); |
| invoker.Add("bundle"); |
| invoker.Add("--no-auto-symbol-paths"); |
| invoker.Add("--proguard-map"); |
| invoker.Add("com.example=" + mapping.path()); |
| invoker.Add(input_trace_); |
| invoker.Add(output_path_); |
| |
| ASSERT_EQ(invoker.Run(), 0); |
| |
| auto members = ReadTarMembers(output_path_); |
| ASSERT_EQ(members.size(), 2u); |
| |
| // trace.perfetto must be a byte-for-byte copy of the input trace. |
| std::string input_bytes; |
| ASSERT_TRUE(base::ReadFile(input_trace_, &input_bytes)); |
| auto trace_it = members.find("trace.perfetto"); |
| ASSERT_NE(trace_it, members.end()); |
| EXPECT_EQ(trace_it->second, input_bytes); |
| |
| // deobfuscation.pb parses as a Trace proto containing one |
| // DeobfuscationMapping for package "com.example" with our class + method. |
| auto deob_it = members.find("deobfuscation.pb"); |
| ASSERT_NE(deob_it, members.end()); |
| protos::gen::Trace deob; |
| ASSERT_TRUE(deob.ParseFromString(deob_it->second)); |
| ASSERT_EQ(deob.packet().size(), 1u); |
| ASSERT_TRUE(deob.packet()[0].has_deobfuscation_mapping()); |
| const auto& dm = deob.packet()[0].deobfuscation_mapping(); |
| EXPECT_EQ(dm.package_name(), "com.example"); |
| ASSERT_EQ(dm.obfuscated_classes().size(), 1u); |
| const auto& cls = dm.obfuscated_classes()[0]; |
| EXPECT_EQ(cls.obfuscated_name(), "a.a"); |
| EXPECT_EQ(cls.deobfuscated_name(), "com.example.Foo"); |
| ASSERT_EQ(cls.obfuscated_methods().size(), 1u); |
| EXPECT_EQ(cls.obfuscated_methods()[0].obfuscated_name(), "b"); |
| } |
| |
| // HPROF rows must participate in the same deobfuscation lookup as proto heap |
| // graphs, including normalized array/class names and inherited fields. |
| TEST_F(TraceconvShellBundleTest, HprofDeobfuscation) { |
| auto mapping = WriteTempFile( |
| "com.example.String -> java.lang.String:\n" |
| " byte[] restoredValue -> value\n" |
| "com.example.Super -> SuperDumpedStuff:\n" |
| " java.lang.Object restoredField -> a\n"); |
| for (const std::string& package : |
| {std::string(), std::string("com.example=")}) { |
| SCOPED_TRACE(package); |
| ArgvInvoker invoker; |
| invoker.Add("trace_processor_shell"); |
| invoker.Add("bundle"); |
| invoker.Add("--no-auto-symbol-paths"); |
| invoker.Add("--no-auto-proguard-maps"); |
| invoker.Add("--proguard-map"); |
| invoker.Add(package + mapping.path()); |
| invoker.Add(base::GetTestDataPath("test/data/test-dump.hprof")); |
| invoker.Add(output_path_); |
| ASSERT_EQ(invoker.Run(), 0); |
| |
| auto tp = TraceProcessor::CreateInstance(Config{}); |
| ASSERT_TRUE(ReadTrace(tp.get(), output_path_.c_str()).ok()); |
| auto classes = tp->ExecuteQuery( |
| "SELECT name, deobfuscated_name FROM heap_graph_class " |
| "WHERE name IN ('java.lang.String', 'java.lang.String[]', " |
| "'java.lang.String[][]', 'java.lang.Class<java.lang.String>', " |
| "'java.lang.Class<java.lang.String[]>', " |
| "'java.lang.Class<java.lang.String[][]>') ORDER BY name"); |
| const char* expected[] = {"java.lang.Class<com.example.String>", |
| "java.lang.Class<com.example.String[]>", |
| "java.lang.Class<com.example.String[][]>", |
| "com.example.String", |
| "com.example.String[]", |
| "com.example.String[][]"}; |
| for (const char* name : expected) { |
| ASSERT_TRUE(classes.Next()); |
| ASSERT_EQ(classes.Get(1).type, SqlValue::kString); |
| EXPECT_STREQ(classes.Get(1).string_value, name); |
| } |
| EXPECT_FALSE(classes.Next()); |
| EXPECT_TRUE(classes.Status().ok()); |
| |
| auto fields = tp->ExecuteQuery( |
| "SELECT field_name, deobfuscated_field_name, COUNT(*) " |
| "FROM heap_graph_reference WHERE field_name IN " |
| "('java.lang.String.value', 'SuperDumpedStuff.a') GROUP BY 1, 2 " |
| "ORDER BY field_name"); |
| for (const char* name : {"com.example.Super.restoredField", |
| "com.example.String.restoredValue"}) { |
| ASSERT_TRUE(fields.Next()); |
| ASSERT_EQ(fields.Get(1).type, SqlValue::kString); |
| EXPECT_STREQ(fields.Get(1).string_value, name); |
| EXPECT_GT(fields.Get(2).long_value, 0); |
| } |
| EXPECT_FALSE(fields.Next()); |
| EXPECT_TRUE(fields.Status().ok()); |
| } |
| } |
| |
| // Repeating --proguard-map should produce one DeobfuscationMapping per input |
| // map, each tagged with the right package name. |
| TEST_F(TraceconvShellBundleTest, BundleWithRepeatedProguardMaps) { |
| base::TempFile map1 = WriteTempFile("com.example.Foo -> a.a:\n"); |
| base::TempFile map2 = WriteTempFile("com.example.Bar -> b.b:\n"); |
| |
| ArgvInvoker invoker; |
| invoker.Add("trace_processor_shell"); |
| invoker.Add("bundle"); |
| invoker.Add("--no-auto-symbol-paths"); |
| invoker.Add("--proguard-map"); |
| invoker.Add("com.example.one=" + map1.path()); |
| invoker.Add("--proguard-map"); |
| invoker.Add("com.example.two=" + map2.path()); |
| invoker.Add(input_trace_); |
| invoker.Add(output_path_); |
| |
| ASSERT_EQ(invoker.Run(), 0); |
| |
| auto members = ReadTarMembers(output_path_); |
| ASSERT_TRUE(members.count("deobfuscation.pb")); |
| EXPECT_THAT(PackageNames(members["deobfuscation.pb"]), |
| UnorderedElementsAre("com.example.one", "com.example.two")); |
| } |
| |
| // --proguard-map without `pkg=` is accepted; the package name ends up empty |
| // in the emitted mapping but the class mapping is still present. |
| TEST_F(TraceconvShellBundleTest, BundleWithProguardMapNoPackage) { |
| base::TempFile mapping = WriteTempFile("com.example.Foo -> a.a:\n"); |
| |
| ArgvInvoker invoker; |
| invoker.Add("trace_processor_shell"); |
| invoker.Add("bundle"); |
| invoker.Add("--no-auto-symbol-paths"); |
| invoker.Add("--proguard-map"); |
| invoker.Add(mapping.path()); |
| invoker.Add(input_trace_); |
| invoker.Add(output_path_); |
| |
| ASSERT_EQ(invoker.Run(), 0); |
| |
| auto members = ReadTarMembers(output_path_); |
| 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(), ""); |
| ASSERT_EQ(dm.obfuscated_classes().size(), 1u); |
| EXPECT_EQ(dm.obfuscated_classes()[0].deobfuscated_name(), "com.example.Foo"); |
| } |
| |
| // Explicit --proguard-map pointing at a missing file must fail the command. |
| TEST_F(TraceconvShellBundleTest, BundleWithMissingProguardMapFails) { |
| ArgvInvoker invoker; |
| invoker.Add("trace_processor_shell"); |
| invoker.Add("bundle"); |
| invoker.Add("--no-auto-symbol-paths"); |
| invoker.Add("--proguard-map"); |
| invoker.Add("com.example=/nonexistent/mapping.txt"); |
| invoker.Add(input_trace_); |
| invoker.Add(output_path_); |
| |
| EXPECT_NE(invoker.Run(), 0); |
| } |
| |
| // --proguard-map with no following argument is a usage error. |
| TEST_F(TraceconvShellBundleTest, BundleProguardMapMissingArgFails) { |
| ArgvInvoker invoker; |
| invoker.Add("trace_processor_shell"); |
| invoker.Add("bundle"); |
| invoker.Add("--proguard-map"); |
| |
| EXPECT_NE(invoker.Run(), 0); |
| } |
| |
| // With --no-auto-proguard-maps, an explicit --proguard-map still propagates |
| // and produces a DeobfuscationMapping for the specified package. (In the |
| // test environment there are no Gradle layouts to auto-discover either |
| // way, so this asserts the explicit path keeps working.) |
| TEST_F(TraceconvShellBundleTest, BundleNoAutoProguardMapsWithExplicit) { |
| base::TempFile mapping = WriteTempFile("com.example.Foo -> a.a:\n"); |
| |
| ArgvInvoker invoker; |
| invoker.Add("trace_processor_shell"); |
| invoker.Add("bundle"); |
| invoker.Add("--no-auto-symbol-paths"); |
| invoker.Add("--no-auto-proguard-maps"); |
| invoker.Add("--proguard-map"); |
| invoker.Add("com.example=" + mapping.path()); |
| invoker.Add(input_trace_); |
| invoker.Add(output_path_); |
| |
| ASSERT_EQ(invoker.Run(), 0); |
| |
| auto members = ReadTarMembers(output_path_); |
| ASSERT_TRUE(members.count("deobfuscation.pb")); |
| EXPECT_THAT(PackageNames(members["deobfuscation.pb"]), |
| UnorderedElementsAre("com.example")); |
| } |
| |
| // --- Edge-case feedback tests --- |
| // |
| // These assert that the bundle command gives *useful* feedback in the |
| // situations users actually hit (see https://github.com/google/perfetto/ |
| // issues/6927), rather than a bare "failed to create bundle." |
| |
| // Builds a minimal trace with a single function_graph entry/exit event on |
| // cpu 0 (the idle thread). If |with_ksyms| is true, the trace also embeds a |
| // kernel symbol map (as traced_probes does when FtraceConfig.symbolize_ksyms |
| // is enabled), so the function resolves to its name; otherwise it stays a |
| // raw hex address that cannot be symbolized offline. |
| std::string BuildFuncgraphTrace(bool with_ksyms) { |
| protos::gen::Trace trace; |
| |
| if (with_ksyms) { |
| auto* packet = trace.add_packet(); |
| packet->set_timestamp(100); |
| packet->set_trusted_uid(0); |
| packet->set_trusted_packet_sequence_id(2); |
| packet->set_sequence_flags( |
| protos::gen::TracePacket::SEQ_INCREMENTAL_STATE_CLEARED); |
| auto* sym = packet->mutable_interned_data()->add_kernel_symbols(); |
| sym->set_iid(42); |
| sym->set_str("my_kernel_func"); |
| } |
| |
| auto* packet = trace.add_packet(); |
| packet->set_timestamp(1000); |
| packet->set_trusted_uid(0); |
| packet->set_trusted_packet_sequence_id(2); |
| auto* bundle = packet->mutable_ftrace_events(); |
| bundle->set_cpu(0); |
| auto* entry = bundle->add_event(); |
| entry->set_timestamp(1000); |
| entry->set_pid(0); |
| entry->mutable_funcgraph_entry()->set_depth(0); |
| entry->mutable_funcgraph_entry()->set_func(42); |
| auto* exit_evt = bundle->add_event(); |
| exit_evt->set_timestamp(1000); |
| exit_evt->set_pid(0); |
| auto* exit_fg = exit_evt->mutable_funcgraph_exit(); |
| exit_fg->set_calltime(1000); |
| exit_fg->set_depth(0); |
| exit_fg->set_func(42); |
| exit_fg->set_overrun(0); |
| exit_fg->set_rettime(2000); |
| |
| return trace.SerializeAsString(); |
| } |
| |
| // Redirects stderr to a temp file for the lifetime of the object so tests |
| // can assert on the shell's diagnostics. POSIX only (the tests that use it |
| // are not compiled on Windows; see the #if guards around them below). |
| #if !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) |
| class ScopedStderrCapture { |
| public: |
| ScopedStderrCapture() { |
| fflush(stderr); |
| saved_fd_ = dup(STDERR_FILENO); |
| PERFETTO_CHECK(saved_fd_ >= 0); |
| char tmpl[] = "/tmp/perfetto_stderr_XXXXXX"; |
| capture_fd_ = mkstemp(tmpl); |
| PERFETTO_CHECK(capture_fd_ >= 0); |
| path_ = tmpl; |
| PERFETTO_CHECK(dup2(capture_fd_, STDERR_FILENO) == STDERR_FILENO); |
| } |
| |
| ~ScopedStderrCapture() { |
| fflush(stderr); |
| dup2(saved_fd_, STDERR_FILENO); |
| close(saved_fd_); |
| close(capture_fd_); |
| unlink(path_.c_str()); |
| } |
| |
| std::string Get() const { |
| fflush(stderr); |
| std::string out; |
| PERFETTO_CHECK(base::ReadFile(path_, &out)); |
| return out; |
| } |
| |
| private: |
| int saved_fd_ = -1; |
| int capture_fd_ = -1; |
| std::string path_; |
| }; |
| #endif // !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) |
| |
| // A function_graph trace recorded WITHOUT symbolize_ksyms contains raw kernel |
| // addresses that cannot be symbolized offline. The bundle must still succeed |
| // (the trace alone is bundled) but must tell the user why the names are hex |
| // and how to fix it. |
| #if !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) |
| TEST_F(TraceconvShellBundleTest, BundleFuncgraphWithoutKallsymsGuidesUser) { |
| base::TempFile trace = |
| WriteTempFile(BuildFuncgraphTrace(/*with_ksyms=*/false)); |
| |
| ArgvInvoker invoker; |
| invoker.Add("trace_processor_shell"); |
| invoker.Add("bundle"); |
| invoker.Add("--no-auto-symbol-paths"); |
| invoker.Add(trace.path()); |
| invoker.Add(output_path_); |
| |
| ScopedStderrCapture capture; |
| ASSERT_EQ(invoker.Run(), 0); |
| std::string stderr_text = capture.Get(); |
| |
| // The trace is bundled even though nothing could be symbolized. |
| auto members = ReadTarMembers(output_path_); |
| ASSERT_TRUE(members.count("trace.perfetto")); |
| EXPECT_EQ(members.size(), 1u); |
| |
| // The user must be pointed at symbolize_ksyms, with the config and doc. |
| EXPECT_THAT(stderr_text, HasSubstr("symbolize_ksyms: true")); |
| EXPECT_THAT(stderr_text, HasSubstr("function_graph")); |
| EXPECT_THAT(stderr_text, HasSubstr("re-record")); |
| EXPECT_THAT(stderr_text, HasSubstr("symbolization#ftrace")); |
| } |
| #endif // !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) |
| |
| // When symbolize_ksyms WAS enabled the names resolve fine: the bundle should |
| // succeed without the kernel warning. |
| TEST_F(TraceconvShellBundleTest, BundleFuncgraphWithKallsymsSucceedsQuietly) { |
| base::TempFile trace = |
| WriteTempFile(BuildFuncgraphTrace(/*with_ksyms=*/true)); |
| |
| ArgvInvoker invoker; |
| invoker.Add("trace_processor_shell"); |
| invoker.Add("bundle"); |
| invoker.Add("--no-auto-symbol-paths"); |
| invoker.Add(trace.path()); |
| invoker.Add(output_path_); |
| |
| ASSERT_EQ(invoker.Run(), 0); |
| |
| auto members = ReadTarMembers(output_path_); |
| ASSERT_TRUE(members.count("trace.perfetto")); |
| EXPECT_EQ(members.size(), 1u); |
| } |
| |
| // A trace with no profiled data at all (e.g. a pure atrace/systrace trace) |
| // has nothing to enrich; bundling it must succeed, not fail with a cryptic |
| // message. |
| TEST_F(TraceconvShellBundleTest, BundleTraceWithoutProfileDataSucceeds) { |
| base::TempFile trace = WriteTempFile(""); |
| |
| ArgvInvoker invoker; |
| invoker.Add("trace_processor_shell"); |
| invoker.Add("bundle"); |
| invoker.Add("--no-auto-symbol-paths"); |
| invoker.Add(trace.path()); |
| invoker.Add(output_path_); |
| |
| ASSERT_EQ(invoker.Run(), 0); |
| EXPECT_EQ(ReadTarMembers(output_path_).size(), 1u); |
| } |
| |
| // A trace with unsymbolized user-space frames: the bundle is still produced |
| // (with the trace), and the user gets a summary of what could not be |
| // symbolized plus a hint on how to fix it. |
| #if !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) |
| TEST_F(TraceconvShellBundleTest, BundleUnsymbolizedFramesSucceedsWithWarning) { |
| ArgvInvoker invoker; |
| invoker.Add("trace_processor_shell"); |
| invoker.Add("bundle"); |
| // Note: no --no-auto-symbol-paths here, so the symbolizer searches the |
| // mappings' absolute paths (which don't exist in this environment) and |
| // reports them as "no matching symbols". |
| invoker.Add(input_trace_); |
| invoker.Add(output_path_); |
| |
| ScopedStderrCapture capture; |
| ASSERT_EQ(invoker.Run(), 0); |
| std::string stderr_text = capture.Get(); |
| |
| auto members = ReadTarMembers(output_path_); |
| ASSERT_TRUE(members.count("trace.perfetto")); |
| // No symbols were found, so no symbols.pb member is expected. |
| EXPECT_FALSE(members.count("symbols.pb")); |
| |
| EXPECT_THAT(stderr_text, HasSubstr("could not be symbolized")); |
| EXPECT_THAT(stderr_text, HasSubstr("--symbol-paths")); |
| } |
| #endif // !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) |
| |
| // With --no-auto-symbol-paths and no explicit paths, no symbol source is |
| // configured at all: the user must be told how to provide one. |
| #if !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) |
| TEST_F(TraceconvShellBundleTest, BundleNoSymbolPathsConfiguredExplainsFix) { |
| ArgvInvoker invoker; |
| invoker.Add("trace_processor_shell"); |
| invoker.Add("bundle"); |
| invoker.Add("--no-auto-symbol-paths"); |
| invoker.Add(input_trace_); |
| invoker.Add(output_path_); |
| |
| ScopedStderrCapture capture; |
| ASSERT_EQ(invoker.Run(), 0); |
| std::string stderr_text = capture.Get(); |
| |
| EXPECT_THAT(stderr_text, HasSubstr("no symbol paths were searched")); |
| EXPECT_THAT(stderr_text, HasSubstr("--symbol-paths")); |
| // PERFETTO_BINARY_PATH is a hidden feature and must not be surfaced in |
| // bundle's guidance. |
| EXPECT_THAT(stderr_text, Not(HasSubstr("PERFETTO_BINARY_PATH"))); |
| } |
| #endif // !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) |
| |
| // An unwritable output path must produce a graceful, descriptive error |
| // instead of crashing the process. |
| TEST_F(TraceconvShellBundleTest, BundleUnwritableOutputFailsGracefully) { |
| ArgvInvoker invoker; |
| invoker.Add("trace_processor_shell"); |
| invoker.Add("bundle"); |
| invoker.Add("--no-auto-symbol-paths"); |
| invoker.Add(input_trace_); |
| invoker.Add(temp_dir_.path() + "/no_such_dir/out.tar"); |
| |
| // Previously this crashed with a PERFETTO_CHECK deep in the tar writer; |
| // now it must return a normal error. |
| EXPECT_NE(invoker.Run(), 0); |
| } |
| |
| // An input path that is a directory must produce a helpful message. |
| TEST_F(TraceconvShellBundleTest, BundleInputIsDirectoryFailsHelpfully) { |
| #if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) |
| GTEST_SKIP() << "directory-vs-file validation differs on Windows"; |
| #endif |
| ArgvInvoker invoker; |
| invoker.Add("trace_processor_shell"); |
| invoker.Add("bundle"); |
| invoker.Add(temp_dir_.path()); |
| invoker.Add(output_path_); |
| |
| EXPECT_EQ(invoker.Run(), 1); |
| } |
| |
| // A broken input symlink must be called out as a broken link, not as a |
| // missing file. |
| TEST_F(TraceconvShellBundleTest, BundleBrokenSymlinkInputFailsHelpfully) { |
| #if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) |
| GTEST_SKIP() << "symlink tests are POSIX-only"; |
| #endif |
| std::string link_path = temp_dir_.path() + "/dangling.pftrace"; |
| PERFETTO_CHECK(symlink("/nonexistent/target.pftrace", link_path.c_str()) == |
| 0); |
| |
| ArgvInvoker invoker; |
| invoker.Add("trace_processor_shell"); |
| invoker.Add("bundle"); |
| invoker.Add(link_path); |
| invoker.Add(output_path_); |
| |
| EXPECT_NE(invoker.Run(), 0); |
| // Clean up so the TempDir destructor can remove the directory. |
| unlink(link_path.c_str()); |
| } |
| |
| // A garbage (non-trace) input file must fail with a clear "not a trace" |
| // style error rather than a bare failure. |
| TEST_F(TraceconvShellBundleTest, BundleMalformedInputFailsWithReason) { |
| base::TempFile trace = WriteTempFile( |
| "this is definitely not a perfetto trace, just some text bytes"); |
| |
| ArgvInvoker invoker; |
| invoker.Add("trace_processor_shell"); |
| invoker.Add("bundle"); |
| invoker.Add(trace.path()); |
| invoker.Add(output_path_); |
| |
| EXPECT_NE(invoker.Run(), 0); |
| } |
| |
| // Regression test for the classic misuse where each --symbol-paths directory |
| // is passed as a separate positional argument (e.g. |
| // `bundle --symbol-paths a b trace out.tar`). The shell must reject this |
| // with a hint instead of silently treating the extra paths as the input and |
| // output files. |
| TEST_F(TraceconvShellBundleTest, BundleRejectsSymbolPathsAsSeparateArgs) { |
| ArgvInvoker invoker; |
| invoker.Add("trace_processor_shell"); |
| invoker.Add("bundle"); |
| invoker.Add("--no-auto-symbol-paths"); |
| invoker.Add("--symbol-paths"); |
| invoker.Add("/path/one"); |
| invoker.Add("/path/two"); |
| invoker.Add(input_trace_); |
| invoker.Add(output_path_); |
| |
| EXPECT_NE(invoker.Run(), 0); |
| } |
| |
| TEST_F(TraceconvShellBundleTest, BundleFailurePreservesExistingOutput) { |
| base::TempFile trace = WriteTempFile(""); |
| base::TempFile destination = WriteTempFile("existing output"); |
| ArgvInvoker invoker; |
| invoker.Add("trace_processor_shell"); |
| invoker.Add("bundle"); |
| invoker.Add("--no-progress"); |
| invoker.Add("--no-auto-symbol-paths"); |
| invoker.Add("--proguard-map"); |
| invoker.Add(temp_dir_.path() + "/missing.map"); |
| invoker.Add(trace.path()); |
| invoker.Add(destination.path()); |
| EXPECT_NE(invoker.Run(), 0); |
| std::string contents; |
| ASSERT_TRUE(base::ReadFile(destination.path(), &contents)); |
| EXPECT_EQ(contents, "existing output"); |
| } |
| |
| TEST_F(TraceconvShellBundleTest, BundleRejectsSameInputAndOutput) { |
| base::TempFile trace = WriteTempFile("original trace"); |
| ArgvInvoker invoker; |
| invoker.Add("trace_processor_shell"); |
| invoker.Add("bundle"); |
| invoker.Add(trace.path()); |
| invoker.Add(trace.path()); |
| EXPECT_NE(invoker.Run(), 0); |
| std::string contents; |
| ASSERT_TRUE(base::ReadFile(trace.path(), &contents)); |
| EXPECT_EQ(contents, "original trace"); |
| } |
| |
| #if !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) |
| TEST_F(TraceconvShellBundleTest, BundleRejectsHardLinkedInputAndOutput) { |
| base::TempFile trace = WriteTempFile("original trace"); |
| std::string alias = temp_dir_.path() + "/alias"; |
| ASSERT_EQ(link(trace.path().c_str(), alias.c_str()), 0); |
| ArgvInvoker invoker; |
| invoker.Add("trace_processor_shell"); |
| invoker.Add("bundle"); |
| invoker.Add(trace.path()); |
| invoker.Add(alias); |
| EXPECT_NE(invoker.Run(), 0); |
| std::string contents; |
| ASSERT_TRUE(base::ReadFile(trace.path(), &contents)); |
| EXPECT_EQ(contents, "original trace"); |
| ASSERT_TRUE(base::Unlink(alias.c_str())); |
| } |
| |
| TEST_F(TraceconvShellBundleTest, RedirectedProgressIsPlainAndWarningsRemain) { |
| base::TempFile trace = WriteTempFile(BuildFuncgraphTrace(false)); |
| for (bool no_progress : {false, true}) { |
| ArgvInvoker invoker; |
| invoker.Add("trace_processor_shell"); |
| invoker.Add("bundle"); |
| invoker.Add("--no-auto-symbol-paths"); |
| if (no_progress) |
| invoker.Add("--no-progress"); |
| invoker.Add(trace.path()); |
| invoker.Add(output_path_); |
| ScopedStderrCapture capture; |
| ASSERT_EQ(invoker.Run(), 0); |
| auto output = capture.Get(); |
| EXPECT_THAT(output, Not(HasSubstr("\r"))); |
| EXPECT_THAT(output, HasSubstr("Read trace:")); |
| EXPECT_THAT(output, HasSubstr("symbolize_ksyms")); |
| } |
| } |
| #endif |
| |
| #if !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) |
| TEST_F(TraceconvShellBundleTest, QuietBundleSuppressesRoutineOutput) { |
| base::TempFile trace = WriteTempFile(BuildFuncgraphTrace(true)); |
| for (const char* flag : {"--quiet", "-q"}) { |
| ArgvInvoker invoker; |
| for (const char* arg : |
| {"trace_processor_shell", "bundle", flag, "--verbose", |
| "--no-auto-symbol-paths", "--no-auto-proguard-maps"}) |
| invoker.Add(arg); |
| invoker.Add(trace.path()); |
| invoker.Add(output_path_); |
| testing::internal::CaptureStdout(); |
| ScopedStderrCapture capture; |
| int status = invoker.Run(); |
| auto stdout_text = testing::internal::GetCapturedStdout(); |
| auto stderr_text = capture.Get(); |
| EXPECT_EQ(status, 0); |
| EXPECT_TRUE(stdout_text.empty()) << stdout_text; |
| EXPECT_TRUE(stderr_text.empty()) << stderr_text; |
| EXPECT_TRUE(base::FileExists(output_path_)); |
| } |
| } |
| |
| TEST_F(TraceconvShellBundleTest, QuietBundleKeepsResourceErrors) { |
| base::TempFile trace = WriteTempFile(BuildFuncgraphTrace(true)); |
| ArgvInvoker invoker; |
| for (const char* arg : {"trace_processor_shell", "bundle", "--quiet", |
| "--no-auto-symbol-paths", "--proguard-map"}) |
| invoker.Add(arg); |
| invoker.Add(temp_dir_.path() + "/missing.map"); |
| invoker.Add(trace.path()); |
| invoker.Add(output_path_); |
| ScopedStderrCapture capture; |
| EXPECT_NE(invoker.Run(), 0); |
| auto output = capture.Get(); |
| EXPECT_THAT(output, HasSubstr("missing.map")); |
| EXPECT_THAT(output, Not(HasSubstr("Read trace:"))); |
| EXPECT_THAT(output, Not(HasSubstr("Enrichment done"))); |
| } |
| |
| TEST_F(TraceconvShellBundleTest, QuietQueryKeepsResultsAndLegacyQueryFile) { |
| base::TempFile trace = WriteTempFile(BuildFuncgraphTrace(true)); |
| base::TempFile sql = WriteTempFile("select 42 as answer"); |
| for (bool legacy : {false, true}) { |
| ArgvInvoker invoker; |
| invoker.Add("trace_processor_shell"); |
| if (legacy) { |
| invoker.Add("--quiet"); |
| invoker.Add("-q"); |
| invoker.Add(sql.path()); |
| invoker.Add(trace.path()); |
| } else { |
| invoker.Add("query"); |
| invoker.Add("-q"); |
| invoker.Add(trace.path()); |
| invoker.Add("select 42 as answer"); |
| } |
| testing::internal::CaptureStdout(); |
| ScopedStderrCapture capture; |
| int status = invoker.Run(); |
| auto stdout_text = testing::internal::GetCapturedStdout(); |
| auto stderr_text = capture.Get(); |
| EXPECT_EQ(status, 0); |
| EXPECT_THAT(stdout_text, HasSubstr("42")); |
| EXPECT_TRUE(stderr_text.empty()) << stderr_text; |
| } |
| } |
| #endif |
| |
| } // namespace |
| } // namespace perfetto::trace_processor |