blob: 8f996d9004792a64bd3c7e48734c59a2ed1e40f1 [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 <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