blob: 7730a2a175d1b29d4d217b4a7950d7a2d74556f7 [file] [log] [blame]
// 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 <fuchsia/inspect/cpp/fidl.h>
#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/lib/storage/vfs/cpp/inspect/inspect_tree.h"
#include "src/storage/fs_test/fs_test_fixture.h"
namespace fs_test {
namespace {
using namespace ::testing;
using namespace inspect::testing;
using inspect::StringPropertyValue;
using inspect::UintPropertyValue;
// 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.volume node to contain.
constexpr std::string_view kAllVolumeProperties[] = {
fs_inspect::VolumeData::kPropSizeBytes,
fs_inspect::VolumeData::kPropSizeLimitBytes,
fs_inspect::VolumeData::kPropAvailableSpaceBytes,
fs_inspect::VolumeData::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) {
// Validate that the expected nodes in the hierarchy exist.
ASSERT_THAT(root,
ChildrenMatch(IsSupersetOf({NodeMatches(NameMatches(fs_inspect::kInfoNodeName)),
NodeMatches(NameMatches(fs_inspect::kUsageNodeName)),
NodeMatches(NameMatches(fs_inspect::kVolumeNodeName))})));
// 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));
// Validate fs.volume node properties.
const inspect::Hierarchy* volume = root.GetByPath({fs_inspect::kVolumeNodeName});
ASSERT_NE(volume, nullptr) << "Could not find node " << fs_inspect::kVolumeNodeName;
EXPECT_THAT(GetPropertyNames(volume->node()), UnorderedElementsAreArray(kAllVolumeProperties));
}
// 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 = info_node.get_property<UintPropertyValue>(InfoData::kPropId)->value(),
.type = info_node.get_property<UintPropertyValue>(InfoData::kPropType)->value(),
.name = info_node.get_property<StringPropertyValue>(InfoData::kPropName)->value(),
.version_major =
info_node.get_property<UintPropertyValue>(InfoData::kPropVersionMajor)->value(),
.version_minor =
info_node.get_property<UintPropertyValue>(InfoData::kPropVersionMinor)->value(),
.block_size = info_node.get_property<UintPropertyValue>(InfoData::kPropBlockSize)->value(),
.max_filename_length =
info_node.get_property<UintPropertyValue>(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 =
usage_node.get_property<UintPropertyValue>(UsageData::kPropTotalBytes)->value(),
.used_bytes = usage_node.get_property<UintPropertyValue>(UsageData::kPropUsedBytes)->value(),
.total_nodes =
usage_node.get_property<UintPropertyValue>(UsageData::kPropTotalNodes)->value(),
.used_nodes = usage_node.get_property<UintPropertyValue>(UsageData::kPropUsedNodes)->value(),
};
}
// Parse the given fs.volume 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{
.size_info =
{
.size_bytes =
volume_node.get_property<UintPropertyValue>(VolumeData::kPropSizeBytes)->value(),
.size_limit_bytes =
volume_node.get_property<UintPropertyValue>(VolumeData::kPropSizeLimitBytes)
->value(),
.available_space_bytes =
volume_node.get_property<UintPropertyValue>(VolumeData::kPropAvailableSpaceBytes)
->value(),
},
.out_of_space_events =
volume_node.get_property<UintPropertyValue>(VolumeData::kPropOutOfSpaceEvents)->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());
}
// Take a new snapshot of the filesystem's inspect tree and validate the layout for compliance.
// Invalidates the previous hierarchy obtained by calling Root().
//
// 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() {
snapshot_ = fs().TakeSnapshot();
// Validate the inspect hierarchy. Ensures all nodes/properties exist and are the correct types.
ASSERT_NO_FATAL_FAILURE(ValidateHierarchy(Root()));
}
// Reference to root hierarchy from last snapshot. After calling UpdateAndValidateSnapshot(),
// existing references are invalidated and *must not* be used.
const inspect::Hierarchy& Root() const {
// All inspect properties are attached under a unique name based on the filesystem type.
// This allows a unique path to query the properties which is important for lapis sampling.
const inspect::Hierarchy* root = snapshot_.GetByPath({fs().GetTraits().name});
ZX_ASSERT_MSG(
root != nullptr,
"Could not find named root node in filesystem hierarchy (expected node name = %s).",
fs().GetTraits().name.c_str());
return *root;
}
// Obtains InfoData containing values from the latest snapshot's fs.info node.
// All calls to UpdateAndValidateSnapshot() must be wrapped by ASSERT_NO_FATAL_FAILURE,
// otherwise this function can dereference a nullptr causing a segfault.
fs_inspect::InfoData GetInfoData() const {
return GetInfoProperties(Root().GetByPath({fs_inspect::kInfoNodeName})->node());
}
// Obtains UsageData containing values from the latest snapshot's fs.usage node.
// All calls to UpdateAndValidateSnapshot() must be wrapped by ASSERT_NO_FATAL_FAILURE,
// otherwise this function can dereference a nullptr causing a segfault.
fs_inspect::UsageData GetUsageData() const {
return GetUsageProperties(Root().GetByPath({fs_inspect::kUsageNodeName})->node());
}
// Obtains VolumeData containing values from the latest snapshot's fs.volume node.
// All calls to UpdateAndValidateSnapshot() must be wrapped by ASSERT_NO_FATAL_FAILURE,
// otherwise this function can dereference a nullptr causing a segfault.
fs_inspect::VolumeData GetVolumeData() const {
return GetVolumeProperties(Root().GetByPath({fs_inspect::kVolumeNodeName})->node());
}
private:
// Last snapshot taken of the inspect tree.
inspect::Hierarchy 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.
ASSERT_EQ(info_data.name, fs().GetTraits().name);
// The filesystem instance identifier should be a valid handle (i.e. non-zero).
ASSERT_NE(info_data.id, ZX_HANDLE_INVALID);
// The maximum filename length should be set (i.e. > 0).
ASSERT_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()) {
ASSERT_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_GT(usage_data.total_nodes, 0u);
EXPECT_GT(usage_data.total_bytes, 0u);
EXPECT_LE(usage_data.total_bytes,
fs().options().device_block_count * fs().options().device_block_size);
uint64_t orig_used_bytes = usage_data.used_bytes;
uint64_t orig_used_nodes = usage_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());
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.volume node.
TEST_P(InspectTest, ValidateVolumeNode) {
fs_inspect::VolumeData volume_data = GetVolumeData();
EXPECT_EQ(volume_data.out_of_space_events, 0u);
if (fs().options().use_fvm) {
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(volume_data.size_info.size_bytes, init_fvm_size);
EXPECT_LT(volume_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(volume_data.size_info.available_space_bytes, 0u);
EXPECT_LT(volume_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(volume_data.size_info.size_limit_bytes, 0u);
} else {
// If we aren't using an FVM-backed filesystem, we should fail to query these properties from
// the volume protocol, so they should all be set to zero.
EXPECT_EQ(volume_data.size_info.available_space_bytes, 0u);
EXPECT_EQ(volume_data.size_info.size_bytes, 0u);
EXPECT_EQ(volume_data.size_info.size_limit_bytes, 0u);
}
}
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