| /* | |
| * 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" | |
| #include "test/test_helper.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().ValueOrDie(); | |
| 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 { | |
| base::UnsetEnv("DEBUGINFOD_URLS"); | |
| base::UnsetEnv("LLVM_SYMBOLIZER_OPTS"); | |
| 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; | |
| } | |
| TestEnvCleaner env_{"DEBUGINFOD_URLS", "LLVM_SYMBOLIZER_OPTS"}; | |
| base::TempDir temp_dir_ = base::TempDir::Create(); | |
| base::TempFile output_file_ = base::TempFile::Create().ValueOrDie(); | |
| 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("Symbolization:")); | |
| EXPECT_THAT(output, HasSubstr("symbolize_ksyms")); | |
| } | |
| } | |
| #endif | |
| #if !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) | |
| TEST_F(TraceconvShellBundleTest, QuietBundleSuppressesRoutineOutput) { | |
| base::TempFile trace = WriteTempFile(BuildFuncgraphTrace(true)); | |
| ArgvInvoker invoker; | |
| for (const char* arg : | |
| {"trace_processor_shell", "bundle", "--quiet", "--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 = WithoutDebugLogs(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("Symbolization:"))); | |
| } | |
| TEST_F(TraceconvShellBundleTest, QuietQueryKeepsResults) { | |
| base::TempFile trace = WriteTempFile(BuildFuncgraphTrace(true)); | |
| ArgvInvoker invoker; | |
| for (const char* arg : {"trace_processor_shell", "query", "--quiet"}) | |
| invoker.Add(arg); | |
| 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 = WithoutDebugLogs(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 |