| // Copyright 2021 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. |
| |
| #include <lib/fdio/directory.h> |
| #include <lib/inspect/cpp/hierarchy.h> |
| #include <lib/inspect/testing/cpp/inspect.h> |
| |
| #include <algorithm> |
| #include <string> |
| #include <string_view> |
| #include <vector> |
| |
| #include "src/storage/fs_test/fs_test_fixture.h" |
| #include "src/storage/lib/vfs/cpp/inspect/inspect_tree.h" |
| |
| namespace fs_test { |
| namespace { |
| |
| using namespace ::testing; |
| using namespace inspect::testing; |
| using diagnostics::reader::InspectData; |
| |
| using inspect::BoolPropertyValue; |
| using inspect::IntPropertyValue; |
| using inspect::StringPropertyValue; |
| |
| // All properties we require the fs.info node to contain, excluding optional fields. |
| constexpr std::string_view kRequiredInfoProperties[] = { |
| fs_inspect::InfoData::kPropId, |
| fs_inspect::InfoData::kPropType, |
| fs_inspect::InfoData::kPropName, |
| fs_inspect::InfoData::kPropVersionMajor, |
| fs_inspect::InfoData::kPropVersionMinor, |
| fs_inspect::InfoData::kPropBlockSize, |
| fs_inspect::InfoData::kPropMaxFilenameLength, |
| }; |
| |
| // All properties we expect the fs.usage node to contain. |
| constexpr std::string_view kAllUsageProperties[] = { |
| fs_inspect::UsageData::kPropTotalBytes, |
| fs_inspect::UsageData::kPropUsedBytes, |
| fs_inspect::UsageData::kPropTotalNodes, |
| fs_inspect::UsageData::kPropUsedNodes, |
| }; |
| |
| // All properties we expect the fs.fvm node to contain. |
| constexpr std::string_view kAllFvmProperties[] = { |
| fs_inspect::FvmData::kPropSizeBytes, |
| fs_inspect::FvmData::kPropSizeLimitBytes, |
| fs_inspect::FvmData::kPropAvailableSpaceBytes, |
| fs_inspect::FvmData::kPropOutOfSpaceEvents, |
| }; |
| |
| // Create a vector of all property names found in the given node. |
| std::vector<std::string> GetPropertyNames(const inspect::NodeValue& node) { |
| std::vector<std::string> properties; |
| for (const auto& property : node.properties()) { |
| properties.push_back(property.name()); |
| } |
| return properties; |
| } |
| |
| // Validates that the snapshot's hierarchy is compliant so that the test case invariants can be |
| // ensured. Use with ASSERT_NO_FATAL_FAILURE. |
| void ValidateHierarchy(const inspect::Hierarchy& root, const TestFilesystemOptions& options) { |
| // Ensure the expected properties under each node exist so that the invariants the getters above |
| // rely on are valid (namely, that these specific nodes and their properties exist). |
| |
| // Validate that the required fs.info node properties are present. |
| const inspect::Hierarchy* info = root.GetByPath({fs_inspect::kInfoNodeName}); |
| ASSERT_NE(info, nullptr) << "Could not find node " << fs_inspect::kInfoNodeName; |
| EXPECT_THAT(GetPropertyNames(info->node()), IsSupersetOf(kRequiredInfoProperties)); |
| |
| // Validate fs.usage node properties. |
| const inspect::Hierarchy* usage = root.GetByPath({fs_inspect::kUsageNodeName}); |
| ASSERT_NE(usage, nullptr) << "Could not find node " << fs_inspect::kUsageNodeName; |
| EXPECT_THAT(GetPropertyNames(usage->node()), UnorderedElementsAreArray(kAllUsageProperties)); |
| |
| if (options.use_fvm) { |
| // Validate fs.fvm node properties. |
| const inspect::Hierarchy* fvm = root.GetByPath({fs_inspect::kFvmNodeName}); |
| ASSERT_NE(fvm, nullptr) << "Could not find node " << fs_inspect::kFvmNodeName; |
| EXPECT_THAT(GetPropertyNames(fvm->node()), UnorderedElementsAreArray(kAllFvmProperties)); |
| } |
| } |
| |
| // Parse the given fs.info node properties into a corresponding InfoData struct. |
| // Properties within the given node must both exist and be the correct type. |
| fs_inspect::InfoData GetInfoProperties(const inspect::NodeValue& info_node) { |
| using fs_inspect::InfoData; |
| |
| // oldest_version is optional. |
| std::optional<std::string> oldest_version = std::nullopt; |
| if (info_node.get_property<StringPropertyValue>(InfoData::kPropOldestVersion)) { |
| oldest_version = |
| info_node.get_property<StringPropertyValue>(InfoData::kPropOldestVersion)->value(); |
| } |
| |
| return InfoData{ |
| .id = static_cast<uint64_t>( |
| info_node.get_property<IntPropertyValue>(InfoData::kPropId)->value()), |
| .type = static_cast<uint64_t>( |
| info_node.get_property<IntPropertyValue>(InfoData::kPropType)->value()), |
| .name = info_node.get_property<StringPropertyValue>(InfoData::kPropName)->value(), |
| .version_major = static_cast<uint64_t>( |
| info_node.get_property<IntPropertyValue>(InfoData::kPropVersionMajor)->value()), |
| .version_minor = static_cast<uint64_t>( |
| info_node.get_property<IntPropertyValue>(InfoData::kPropVersionMinor)->value()), |
| .block_size = static_cast<uint64_t>( |
| info_node.get_property<IntPropertyValue>(InfoData::kPropBlockSize)->value()), |
| .max_filename_length = static_cast<uint64_t>( |
| info_node.get_property<IntPropertyValue>(InfoData::kPropMaxFilenameLength)->value()), |
| .oldest_version = std::move(oldest_version), |
| }; |
| } |
| |
| // Parse the given fs.usage node properties into a corresponding UsageData struct. |
| // Properties within the given node must both exist and be the correct type. |
| fs_inspect::UsageData GetUsageProperties(const inspect::NodeValue& usage_node) { |
| using fs_inspect::UsageData; |
| return UsageData{ |
| .total_bytes = static_cast<uint64_t>( |
| usage_node.get_property<IntPropertyValue>(UsageData::kPropTotalBytes)->value()), |
| .used_bytes = static_cast<uint64_t>( |
| usage_node.get_property<IntPropertyValue>(UsageData::kPropUsedBytes)->value()), |
| .total_nodes = static_cast<uint64_t>( |
| usage_node.get_property<IntPropertyValue>(UsageData::kPropTotalNodes)->value()), |
| .used_nodes = static_cast<uint64_t>( |
| usage_node.get_property<IntPropertyValue>(UsageData::kPropUsedNodes)->value()), |
| }; |
| } |
| |
| // Parse the given fs.fvm node properties into a corresponding FvmData struct. |
| // Properties within the given node must both exist and be the correct type. |
| fs_inspect::FvmData GetFvmProperties(const inspect::NodeValue& fvm_node) { |
| using fs_inspect::FvmData; |
| return FvmData{ |
| .size_info = |
| { |
| .size_bytes = static_cast<uint64_t>( |
| fvm_node.get_property<IntPropertyValue>(FvmData::kPropSizeBytes)->value()), |
| .size_limit_bytes = static_cast<uint64_t>( |
| fvm_node.get_property<IntPropertyValue>(FvmData::kPropSizeLimitBytes)->value()), |
| .available_space_bytes = static_cast<uint64_t>( |
| fvm_node.get_property<IntPropertyValue>(FvmData::kPropAvailableSpaceBytes) |
| ->value()), |
| }, |
| .out_of_space_events = static_cast<uint64_t>( |
| fvm_node.get_property<IntPropertyValue>(FvmData::kPropOutOfSpaceEvents)->value()), |
| }; |
| } |
| |
| // Parse the given fs.volumes.{name} node properties into a corresponding VolumeData struct. |
| // Properties within the given node must both exist and be the correct type. |
| fs_inspect::VolumeData GetVolumeProperties(const inspect::NodeValue& volume_node) { |
| using fs_inspect::VolumeData; |
| return VolumeData{ |
| .used_bytes = static_cast<uint64_t>( |
| volume_node.get_property<IntPropertyValue>(VolumeData::kPropVolumeUsedBytes)->value()), |
| .used_nodes = static_cast<uint64_t>( |
| volume_node.get_property<IntPropertyValue>(VolumeData::kPropVolumeUsedNodes)->value()), |
| .encrypted = |
| volume_node.get_property<BoolPropertyValue>(VolumeData::kPropVolumeEncrypted)->value(), |
| }; |
| } |
| |
| class InspectTest : public FilesystemTest { |
| protected: |
| // Initializes the test case by taking an initial snapshot of the inspect tree, and validates |
| // the overall node hierarchy/layout. |
| void SetUp() override { |
| // Take an initial snapshot. |
| ASSERT_NO_FATAL_FAILURE(UpdateAndValidateSnapshot()) << "Failed in InspectTest::SetUp"; |
| } |
| |
| // Take a new snapshot of the filesystem's inspect tree, and validate the layout for compliance. |
| // |
| // All calls to this function *must* be wrapped with ASSERT_NO_FATAL_FAILURE. Failure to do so |
| // can result in some test fixture methods segfaulting. |
| void UpdateAndValidateSnapshot() { |
| std::optional<InspectData> data; |
| fs().TakeSnapshot(&data); |
| ASSERT_TRUE(data.has_value()); |
| ASSERT_TRUE(data->payload().has_value()); |
| ASSERT_NO_FATAL_FAILURE(ValidateHierarchy(**data->payload(), fs().options())); |
| snapshot_ = std::move(*data); |
| } |
| |
| // Obtains InfoData containing values from the latest snapshot's fs.info node. |
| fs_inspect::InfoData GetInfoData() const { |
| EXPECT_NE(nullptr, snapshot_->payload().value()); |
| auto* n = snapshot_->payload().value()->GetByPath({fs_inspect::kInfoNodeName}); |
| EXPECT_NE(nullptr, n); |
| return GetInfoProperties(n->node()); |
| } |
| |
| // Obtains UsageData containing values from the latest snapshot's fs.usage node. |
| fs_inspect::UsageData GetUsageData() const { |
| EXPECT_NE(nullptr, snapshot_->payload().value()); |
| auto* n = snapshot_->payload().value()->GetByPath({fs_inspect::kUsageNodeName}); |
| EXPECT_NE(nullptr, n); |
| return GetUsageProperties(n->node()); |
| } |
| |
| // Obtains FvmData containing values from the latest snapshot's fs.fvm node. |
| fs_inspect::FvmData GetFvmData() const { |
| EXPECT_NE(nullptr, snapshot_->payload().value()); |
| auto* n = snapshot_->payload().value()->GetByPath({fs_inspect::kFvmNodeName}); |
| EXPECT_NE(nullptr, n); |
| return GetFvmProperties(n->node()); |
| } |
| |
| // Obtains FvmData containing values from the latest snapshot's fs.volumes.`volume_name` node. |
| fs_inspect::VolumeData GetVolumeData(const char* volume_name) const { |
| EXPECT_NE(nullptr, snapshot_->payload().value()); |
| auto* n = snapshot_->payload().value()->GetByPath({fs_inspect::kVolumesNodeName, volume_name}); |
| EXPECT_NE(nullptr, n); |
| return GetVolumeProperties(n->node()); |
| } |
| |
| private: |
| // Last snapshot taken of the inspect tree. |
| std::optional<InspectData> snapshot_ = {}; |
| }; |
| |
| // Validate values in the fs.info node. |
| TEST_P(InspectTest, ValidateInfoNode) { |
| fs_inspect::InfoData info_data = GetInfoData(); |
| // The filesystem name (type) should match those in the filesystem traits. |
| EXPECT_EQ(info_data.name, fs().GetTraits().name); |
| // The filesystem instance identifier should be a valid handle (i.e. non-zero). |
| EXPECT_NE(info_data.id, ZX_HANDLE_INVALID); |
| // The maximum filename length should be set (i.e. > 0). |
| EXPECT_GT(info_data.max_filename_length, 0u); |
| // If the filesystem reports oldest_version, ensure it is the correct format (oldest maj/min). |
| if (info_data.oldest_version.has_value()) { |
| EXPECT_THAT(info_data.oldest_version.value(), ::testing::MatchesRegex("^[0-9]+\\/[0-9]+$")); |
| } |
| } |
| |
| // Validate values in the fs.usage node. |
| TEST_P(InspectTest, ValidateUsageNode) { |
| fs_inspect::UsageData usage_data = GetUsageData(); |
| EXPECT_LE(usage_data.total_bytes, |
| fs().options().device_block_count * fs().options().device_block_size); |
| |
| // Multi-volume systems will have further functionality exercised in ValidateVolumeNode (where the |
| // bytes/nodes are accounted for). |
| if (fs().GetTraits().is_multi_volume) { |
| return; |
| } |
| |
| uint64_t orig_used_bytes = usage_data.used_bytes; |
| uint64_t orig_used_nodes = usage_data.used_nodes; |
| EXPECT_GT(usage_data.total_nodes, 0u); |
| EXPECT_GT(usage_data.total_bytes, 0u); |
| |
| // Write a file to disk. |
| std::string test_filename = GetPath("test_file"); |
| const size_t kDataWriteSize = 128ul * 1024ul; |
| |
| fbl::unique_fd fd(open(test_filename.c_str(), O_CREAT | O_RDWR, 0666)); |
| ASSERT_TRUE(fd); |
| std::vector<uint8_t> data(kDataWriteSize); |
| ASSERT_EQ(write(fd.get(), data.data(), data.size()), static_cast<ssize_t>(data.size())); |
| ASSERT_EQ(fsync(fd.get()), 0); |
| |
| // Take a new inspect snapshot, ensure used_bytes/used_nodes are updated correctly. |
| ASSERT_NO_FATAL_FAILURE(UpdateAndValidateSnapshot()); |
| usage_data = GetUsageData(); |
| // Used bytes should increase by at least the amount of written data, and we should now use |
| // at least one more inode than before. |
| EXPECT_GE(usage_data.used_bytes, orig_used_bytes + kDataWriteSize); |
| EXPECT_GE(usage_data.used_nodes, orig_used_nodes + 1); |
| } |
| |
| // Validate values in the fs.fvm node. |
| TEST_P(InspectTest, ValidateFvmNode) { |
| if (!fs().options().use_fvm) { |
| return; |
| } |
| fs_inspect::FvmData fvm_data = GetFvmData(); |
| EXPECT_EQ(fvm_data.out_of_space_events, 0u); |
| uint64_t device_size = fs().options().device_block_count * fs().options().device_block_size; |
| uint64_t init_fvm_size = fs().options().fvm_slice_size * fs().options().initial_fvm_slice_count; |
| ASSERT_GT(device_size, 0u) << "Invalid block device size!"; |
| ASSERT_GT(init_fvm_size, 0u) << "Invalid FVM volume size!"; |
| |
| // The reported volume size should be at least the amount of initial FVM slices, but not exceed |
| // the size of the block device. |
| EXPECT_GE(fvm_data.size_info.size_bytes, init_fvm_size); |
| EXPECT_LT(fvm_data.size_info.size_bytes, device_size); |
| |
| // We should have some amount of free space, but not more than the size of the block device. |
| EXPECT_GT(fvm_data.size_info.available_space_bytes, 0u); |
| EXPECT_LT(fvm_data.size_info.available_space_bytes, device_size); |
| |
| // We do not set a volume size limit in fs_test currently, so this should always be zero. |
| EXPECT_EQ(fvm_data.size_info.size_limit_bytes, 0u); |
| } |
| |
| // Validate values in the fs.volumes/{name} nodes. |
| TEST_P(InspectTest, ValidateVolumeNode) { |
| if (!fs().GetTraits().is_multi_volume) { |
| return; |
| } |
| |
| fs_inspect::VolumeData volume_data = GetVolumeData("default"); |
| EXPECT_EQ(volume_data.bytes_limit, std::nullopt); |
| EXPECT_TRUE(volume_data.encrypted); |
| uint64_t orig_used_bytes = volume_data.used_bytes; |
| uint64_t orig_used_nodes = volume_data.used_nodes; |
| |
| // Write a file to disk. |
| std::string test_filename = GetPath("test_file"); |
| const size_t kDataWriteSize = 128ul * 1024ul; |
| |
| fbl::unique_fd fd(open(test_filename.c_str(), O_CREAT | O_RDWR, 0666)); |
| ASSERT_TRUE(fd); |
| std::vector<uint8_t> data(kDataWriteSize); |
| ASSERT_EQ(write(fd.get(), data.data(), data.size()), static_cast<ssize_t>(data.size())); |
| ASSERT_EQ(fsync(fd.get()), 0); |
| |
| // Take a new inspect snapshot, ensure used_bytes/used_nodes are updated correctly. |
| ASSERT_NO_FATAL_FAILURE(UpdateAndValidateSnapshot()); |
| |
| volume_data = GetVolumeData("default"); |
| // Used bytes should increase by at least the amount of written data, and we should now use |
| // at least one more inode than before. |
| EXPECT_GE(volume_data.used_bytes, orig_used_bytes + kDataWriteSize); |
| EXPECT_GE(volume_data.used_nodes, orig_used_nodes + 1); |
| } |
| |
| std::vector<TestFilesystemOptions> GetTestCombinations() { |
| return MapAndFilterAllTestFilesystems( |
| [](const TestFilesystemOptions& options) -> std::optional<TestFilesystemOptions> { |
| if (options.filesystem->GetTraits().supports_inspect) { |
| return options; |
| } |
| return std::nullopt; |
| }); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(/*no prefix*/, InspectTest, ::testing::ValuesIn(GetTestCombinations()), |
| ::testing::PrintToStringParamName()); |
| |
| GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(InspectTest); |
| |
| } // namespace |
| } // namespace fs_test |