| // Copyright 2022 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #ifndef SRC_LIB_ZXDUMP_DUMP_TESTS_H_ |
| #define SRC_LIB_ZXDUMP_DUMP_TESTS_H_ |
| |
| #include <lib/fdio/spawn.h> |
| #include <lib/fit/function.h> |
| #include <lib/fit/result.h> |
| #include <lib/stdcompat/functional.h> |
| #include <lib/zx/job.h> |
| #include <lib/zx/process.h> |
| #include <lib/zxdump/dump.h> |
| #include <lib/zxdump/fd-writer.h> |
| #include <lib/zxdump/task.h> |
| #include <lib/zxdump/zstd-writer.h> |
| #include <unistd.h> |
| |
| #include <array> |
| #include <cstdio> |
| #include <string_view> |
| #include <vector> |
| |
| #include <fbl/unique_fd.h> |
| #include <gmock/gmock.h> |
| #include <gtest/gtest.h> |
| |
| namespace zxdump::testing { |
| |
| constexpr time_t kNoDate = 0; // Value for no date recorded. |
| constexpr time_t kTestDate = 74697240; // Long, long ago. |
| |
| // A simple test program starts up and waits. |
| class TestProcess { |
| public: |
| static constexpr std::string_view kDsoSoname = "libzxdump-test-child-dso.so"; |
| |
| zx::unowned_process borrow() const { return zx::unowned_process{process_}; } |
| |
| LiveHandle handle() const { |
| zx::process dup; |
| EXPECT_EQ(ZX_OK, process_.duplicate(ZX_RIGHT_SAME_RIGHTS, &dup)); |
| return dup; |
| } |
| |
| ~TestProcess() { |
| if (process_) { |
| EXPECT_EQ(ZX_OK, process_.kill()); |
| } |
| if (job_ && kill_job_) { |
| EXPECT_EQ(ZX_OK, job_.kill()); |
| } |
| } |
| |
| TestProcess& SpawnAction(const fdio_spawn_action_t& action) { |
| spawn_actions_.push_back(action); |
| return *this; |
| } |
| |
| void StartChild(std::vector<const char*> args = {}) { |
| args.insert(args.begin(), kChildProgram); |
| args.push_back(nullptr); |
| ASSERT_FALSE(*std::prev(args.end())); |
| char err_msg[FDIO_SPAWN_ERR_MSG_MAX_LENGTH] = ""; |
| ASSERT_EQ(fdio_spawn_etc(job().get(), FDIO_SPAWN_CLONE_ALL, kChildProgram, args.data(), environ, |
| spawn_actions_.size(), spawn_actions_.data(), |
| process_.reset_and_get_address(), err_msg), |
| ZX_OK) |
| << err_msg; |
| } |
| |
| const zx::process& process() const { return process_; } |
| |
| void AddToTaskHolder(TaskHolder& holder, Process*& holder_process) { |
| zx::process proc; |
| ASSERT_EQ(process_.duplicate(ZX_RIGHT_SAME_RIGHTS, &proc), ZX_OK); |
| auto result = holder.Insert(std::move(proc)); |
| ASSERT_TRUE(result.is_ok()) << result.error_value(); |
| zxdump::Object& object = *result; |
| ASSERT_EQ(object.type(), ZX_OBJ_TYPE_PROCESS); |
| holder_process = &static_cast<zxdump::Process&>(object); |
| } |
| |
| zx_koid_t koid() const { return GetKoid(process_); } |
| |
| // Explicitly choose the job to use. |
| void set_job(zx::job&& job, bool kill_job = false) { |
| job_ = std::move(job); |
| kill_job_ = kill_job; |
| } |
| void set_job(const zx::job& job) { ASSERT_EQ(ZX_OK, job.duplicate(ZX_RIGHT_SAME_RIGHTS, &job_)); } |
| |
| // This returns the job StartChild will launch the test process in. |
| // If set_job hasn't been called, it just uses the default job. |
| const zx::job& job() { |
| if (!job_) { |
| return *default_job_; |
| } |
| return job_; |
| } |
| |
| zx_koid_t job_koid() const { return GetKoid(job_); } |
| |
| // Create a new empty job and set_job() to that. |
| void HermeticJob(const zx::job& parent = *zx::job::default_job()) { |
| ASSERT_FALSE(job_); |
| ASSERT_EQ(ZX_OK, zx::job::create(parent, 0, &job_)); |
| kill_job_ = true; |
| } |
| |
| // This is a standard SegmentCallback that can be used. |
| static fit::result<zxdump::Error, zxdump::SegmentDisposition> PruneAllMemory( |
| zxdump::SegmentDisposition segment, const zx_info_maps_t& maps, const zx_info_vmo_t& vmo) { |
| segment.filesz = 0; |
| return fit::ok(segment); |
| } |
| |
| static fit::result<zxdump::Error, zxdump::SegmentDisposition> DumpAllMemory( |
| zxdump::SegmentDisposition segment, const zx_info_maps_t& maps, const zx_info_vmo_t& vmo) { |
| return fit::ok(segment); |
| } |
| |
| private: |
| static constexpr const char* kChildProgram = "/pkg/bin/zxdump-test-child"; |
| |
| template <typename Handle> |
| static zx_koid_t GetKoid(const Handle& object) { |
| zx_info_handle_basic_t info = {.koid = ZX_KOID_INVALID}; |
| EXPECT_EQ(ZX_OK, object.get_info(ZX_INFO_HANDLE_BASIC, &info, sizeof(info), nullptr, nullptr)); |
| return info.koid; |
| } |
| |
| zx::unowned_job default_job_ = zx::job::default_job(); |
| std::vector<fdio_spawn_action_t> spawn_actions_; |
| zx::process process_; |
| zx::job job_; |
| bool kill_job_ = false; |
| }; |
| |
| using PrecollectFunction = |
| fit::function<void(zxdump::TaskHolder& holder, zxdump::ProcessDump& dump)>; |
| |
| class TestProcessForPropertiesAndInfo : public TestProcess { |
| public: |
| // Start a child for basic property & info dump testing. |
| void StartChild(); |
| |
| static void NoPrecollect(zxdump::TaskHolder& holder, zxdump::ProcessDump& dump) {} |
| |
| // Do the basic dump using the dumper API. |
| template <typename Writer> |
| void Dump(Writer& writer, PrecollectFunction precollect = NoPrecollect, |
| SegmentCallback prune = nullptr); |
| |
| // Verify a dump file for that child was inserted and looks right. |
| void CheckDump(zxdump::TaskHolder& holder, bool threads_dumped = true); |
| |
| private: |
| static constexpr const char* kChildName = "zxdump-property-test-child"; |
| }; |
| |
| // The template and its instantiations are defined in dump-tests.cc. |
| extern template void TestProcessForPropertiesAndInfo::Dump(FdWriter&, PrecollectFunction, |
| SegmentCallback); |
| extern template void TestProcessForPropertiesAndInfo::Dump(ZstdWriter&, PrecollectFunction, |
| SegmentCallback); |
| |
| class TestProcessForSystemInfo : public TestProcessForPropertiesAndInfo { |
| public: |
| // Start a child for system information dump testing. |
| void StartChild(); |
| |
| // Do the basic dump using the dumper API. |
| template <typename Writer> |
| void Dump(Writer& writer) { |
| TestProcessForPropertiesAndInfo::Dump( |
| writer, cpp20::bind_front(&TestProcessForSystemInfo::Precollect, this)); |
| } |
| |
| // Verify a dump file for that child was inserted and looks right. |
| void CheckDump(zxdump::TaskHolder& holder); |
| |
| private: |
| static constexpr const char* kChildName = "zxdump-system-test-child"; |
| |
| void Precollect(zxdump::TaskHolder& holder, zxdump::ProcessDump& dump); |
| |
| zxdump::TaskHolder live_holder_; |
| }; |
| |
| class TestProcessForKernelInfo : public TestProcessForPropertiesAndInfo { |
| public: |
| // Start a child for privileged kernel information dump testing. |
| void StartChild(); |
| |
| // Do the basic dump using the dumper API. |
| template <typename Writer> |
| void Dump(Writer& writer) { |
| TestProcessForPropertiesAndInfo::Dump( |
| writer, cpp20::bind_front(&TestProcessForKernelInfo::Precollect, this)); |
| } |
| |
| // Verify a dump file for that child was inserted and looks right. |
| void CheckDump(zxdump::TaskHolder& holder); |
| |
| const LiveHandle& info_resource() const { return info_resource_; } |
| |
| private: |
| static constexpr const char* kChildName = "zxdump-kernel-test-child"; |
| |
| void Precollect(zxdump::TaskHolder& holder, zxdump::ProcessDump& dump); |
| |
| LiveHandle info_resource_; |
| }; |
| |
| class TestProcessForRemarks : public TestProcessForPropertiesAndInfo { |
| public: |
| static constexpr std::string_view kTestRemarksNameNoSuffix = "teststuff"; |
| static constexpr std::string_view kTextRemarksName = "teststuff.txt"; |
| static constexpr std::string_view kDefaultRemarksName = "remarks.txt"; |
| static constexpr std::string_view kTextRemarksData = "insert remark here"; |
| static constexpr std::string_view kBinaryRemarksName = "teststuff.bin"; |
| static constexpr std::array kBinaryTestData = { |
| std::byte{1}, |
| std::byte{2}, |
| std::byte{3}, |
| }; |
| static constexpr ByteView kBinaryRemarksData{ |
| kBinaryTestData.data(), |
| kBinaryTestData.size(), |
| }; |
| static constexpr std::string_view kJsonRemarksName = "teststuff.json"; |
| static constexpr std::string_view kDefaultJsonRemarksName = "remarks.json"; |
| static constexpr std::string_view kRawJsonRemarksData = R"""({ "foo": [ 1, 3 ] })"""; |
| static constexpr std::string_view kNormalizedJsonRemarksData = R"""({"foo":[1,3]})"""; |
| |
| // Start a child for dump remarks testing. |
| void StartChild(); |
| |
| // Do the basic dump using the dumper API. |
| template <typename Writer> |
| void Dump(Writer& writer) { |
| TestProcessForPropertiesAndInfo::Dump(writer, Precollect); |
| } |
| |
| // Verify a dump file for that child was inserted and looks right. |
| void CheckDump(zxdump::TaskHolder& holder); |
| |
| static std::string_view AsString(ByteView bytes) { |
| return {reinterpret_cast<const char*>(bytes.data()), bytes.size()}; |
| } |
| |
| private: |
| static constexpr const char* kChildName = "zxdump-remarks-test-child"; |
| |
| static void Precollect(zxdump::TaskHolder& holder, zxdump::ProcessDump& dump); |
| }; |
| |
| class TestProcessForMemory : public TestProcessForPropertiesAndInfo { |
| public: |
| // This is the test data made present in the child's memory by StartChild. |
| static constexpr std::string_view kMemoryText = "in the course of human events"; |
| static constexpr std::wstring_view kMemoryWideText = L"in the course of human events"; |
| static constexpr std::array kMemoryInts = {17, 23, 42, 55, 66}; |
| |
| // Start a child for basic memory dump testing. |
| void StartChild(); |
| |
| // Do the full-memory dump using the dumper API. |
| template <typename Writer> |
| void Dump(Writer& writer, SegmentCallback prune = DumpAllMemory) { |
| TestProcessForPropertiesAndInfo::Dump(writer, NoPrecollect, std::move(prune)); |
| } |
| |
| // Verify a dump file for that child was inserted and looks right. |
| void CheckDump(zxdump::TaskHolder& holder, bool memory_elided = false); |
| |
| // These give the addresses where the test data is found in the child. |
| |
| uint64_t text_ptr() const { return text_ptr_; } |
| |
| uint64_t ints_ptr() const { return ints_ptr_; } |
| |
| uint64_t wtext_ptr() const { return wtext_ptr_; } |
| |
| uint64_t pages_ptr() const { return pages_ptr_; } |
| |
| private: |
| static constexpr const char* kChildName = "zxdump-memory-test-child"; |
| |
| uint64_t text_ptr_ = 0; |
| uint64_t ints_ptr_ = 0; |
| uint64_t wtext_ptr_ = 0; |
| uint64_t pages_ptr_ = 0; |
| }; |
| |
| class TestProcessForThreads : public TestProcessForPropertiesAndInfo { |
| public: |
| static constexpr size_t kThreadCount = 5; |
| |
| // Start a child for thread dump testing. |
| void StartChild(); |
| |
| // Do the basic dump using the dumper API. |
| template <typename Writer> |
| void Dump(Writer& writer) { |
| TestProcessForPropertiesAndInfo::Dump(writer, Precollect); |
| } |
| |
| // Verify a dump file for that child was inserted and looks right. |
| void CheckDump(zxdump::TaskHolder& holder); |
| |
| cpp20::span<const zx_koid_t, kThreadCount> thread_koids() const { return thread_koids_; } |
| |
| private: |
| static constexpr const char* kChildName = "zxdump-thread-test-child"; |
| |
| static void Precollect(zxdump::TaskHolder& holder, zxdump::ProcessDump& dump); |
| |
| std::array<zx_koid_t, kThreadCount> thread_koids_ = {}; |
| }; |
| |
| class TestProcessForThreadState : public TestProcessForPropertiesAndInfo { |
| public: |
| static constexpr size_t kThreadCount = 5; |
| |
| // Start a child for thread state dump testing. |
| void StartChild(); |
| |
| // Do the basic dump using the dumper API. |
| template <typename Writer> |
| void Dump(Writer& writer) { |
| TestProcessForPropertiesAndInfo::Dump(writer, Precollect); |
| } |
| |
| // Verify a dump file for that child was inserted and looks right. |
| void CheckDump(zxdump::TaskHolder& holder); |
| |
| cpp20::span<const zx_koid_t, kThreadCount> thread_koids() const { return thread_koids_; } |
| |
| private: |
| static constexpr const char* kChildName = "zxdump-thread-state-test-child"; |
| |
| static constexpr uint64_t kRegisterValue = 0x1234d00df00d8765; |
| |
| static void Precollect(zxdump::TaskHolder& holder, zxdump::ProcessDump& dump); |
| |
| std::array<zx_koid_t, kThreadCount> thread_koids_ = {}; |
| }; |
| |
| class TestProcessForElfSearch : public zxdump::testing::TestProcessForPropertiesAndInfo { |
| public: |
| // Start a child for ELF search testing. |
| void StartChild(); |
| |
| // Do the full-memory dump using the dumper API. |
| template <typename Writer> |
| void Dump(Writer& writer) { |
| zxdump::testing::TestProcessForPropertiesAndInfo::Dump( |
| writer, cpp20::bind_front(&TestProcessForElfSearch::Precollect, this), |
| cpp20::bind_front(&TestProcessForElfSearch::DumpAllMemoryWithBuildIds, this)); |
| } |
| |
| // Verify a dump file for that child was inserted and looks right. |
| void CheckDump(zxdump::TaskHolder& holder); |
| |
| // Verify the build ID PT_NOTEs in the ET_CORE file directly. |
| void CheckNotes(int fd); |
| |
| // These give the addresses where the modules were reported in the child. |
| |
| uint64_t main_ptr() const { return main_ptr_; } |
| |
| uint64_t dso_ptr() const { return dso_ptr_; } |
| |
| private: |
| static constexpr const char* kChildName = "zxdump-elf-search-test-child"; |
| |
| void Precollect(zxdump::TaskHolder& holder, zxdump::ProcessDump& dump); |
| |
| fit::result<zxdump::Error, zxdump::SegmentDisposition> DumpAllMemoryWithBuildIds( |
| zxdump::SegmentDisposition segment, const zx_info_maps_t& maps, const zx_info_vmo_t& vmo); |
| |
| zxdump::ProcessDump* dump_ = nullptr; |
| uint64_t dso_ptr_ = 0; |
| uint64_t main_ptr_ = 0; |
| }; |
| |
| } // namespace zxdump::testing |
| |
| #endif // SRC_LIB_ZXDUMP_DUMP_TESTS_H_ |