blob: b2862f7477c2223fd5a0096132d07dabaad2bbed [file] [log] [blame]
// Copyright 2020 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 "src/storage/volume_image/utils/lz4_compressor.h"
#include <lib/fit/defer.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <lz4/lz4.h>
#include "src/storage/volume_image/options.h"
namespace storage::volume_image {
namespace {
TEST(Lz4CompressorTest, CreateWithWrongSchemaIsError) {
CompressionOptions options;
options.schema = CompressionSchema::kNone;
auto compressor_result = Lz4Compressor::Create(options);
ASSERT_TRUE(compressor_result.is_error());
}
TEST(Lz4CompressorTest, CreateWithLz4SchemaOnlyIsOk) {
CompressionOptions options;
options.schema = CompressionSchema::kLz4;
options.options.clear();
auto compressor_result = Lz4Compressor::Create(options);
ASSERT_TRUE(compressor_result.is_ok()) << compressor_result.error();
const auto& preferences = compressor_result.value().GetPreferences();
EXPECT_EQ(preferences.frameInfo.blockSizeID, LZ4F_max64KB);
EXPECT_EQ(preferences.compressionLevel, 0);
EXPECT_EQ(preferences.frameInfo.blockMode, LZ4F_blockIndependent);
}
TEST(Lz4CompressorTest, CreateWithLz4SchemaAndCompressionLevelIsOk) {
constexpr int kCompressionLevel = 12345;
CompressionOptions options;
options.schema = CompressionSchema::kLz4;
options.options.clear();
options.options["compression_level"] = kCompressionLevel;
auto compressor_result = Lz4Compressor::Create(options);
ASSERT_TRUE(compressor_result.is_ok()) << compressor_result.error();
const auto& preferences = compressor_result.value().GetPreferences();
EXPECT_EQ(preferences.frameInfo.blockSizeID, LZ4F_max64KB);
EXPECT_EQ(preferences.compressionLevel, kCompressionLevel);
EXPECT_EQ(preferences.frameInfo.blockMode, LZ4F_blockIndependent);
}
struct BlockSizeParam {
uint64_t block_size = 0;
LZ4F_blockSizeID_t expected_id = LZ4F_max64KB;
};
class BlockSizeLz4CompressorTest : public testing::TestWithParam<BlockSizeParam> {};
TEST_P(BlockSizeLz4CompressorTest, CreateWithLz4SchemaAndBlockSizeMapsCorrectlyToBlockSizeId) {
CompressionOptions options;
const int random_compression_level = testing::UnitTest::GetInstance()->random_seed();
options.schema = CompressionSchema::kLz4;
options.options.clear();
options.options["block_size"] = GetParam().block_size;
options.options["compression_level"] = random_compression_level;
auto compressor_result = Lz4Compressor::Create(options);
ASSERT_TRUE(compressor_result.is_ok()) << compressor_result.error();
const auto& preferences = compressor_result.value().GetPreferences();
EXPECT_EQ(preferences.frameInfo.blockSizeID, GetParam().expected_id);
EXPECT_EQ(preferences.compressionLevel, random_compression_level);
EXPECT_EQ(preferences.frameInfo.blockMode, LZ4F_blockIndependent);
}
INSTANTIATE_TEST_SUITE_P(
, BlockSizeLz4CompressorTest,
testing::Values(BlockSizeParam{.block_size = 0, .expected_id = LZ4F_max64KB},
BlockSizeParam{.block_size = 64, .expected_id = LZ4F_max64KB},
BlockSizeParam{.block_size = 65, .expected_id = LZ4F_max256KB},
BlockSizeParam{.block_size = 256, .expected_id = LZ4F_max256KB},
BlockSizeParam{.block_size = 257, .expected_id = LZ4F_max1MB},
BlockSizeParam{.block_size = 1024, .expected_id = LZ4F_max1MB},
BlockSizeParam{.block_size = 4095, .expected_id = LZ4F_max4MB},
BlockSizeParam{.block_size = 4096, .expected_id = LZ4F_max4MB},
BlockSizeParam{.block_size = 999999999, .expected_id = LZ4F_max4MB}));
fpromise::result<void, std::string> HandlerReturnsOk(cpp20::span<const uint8_t> /*unused*/) {
return fpromise::ok();
}
TEST(Lz4CompressorTest, PrepareAfterConstructionIsOk) {
CompressionOptions options;
options.schema = CompressionSchema::kLz4;
options.options.clear();
auto compressor_result = Lz4Compressor::Create(options);
ASSERT_TRUE(compressor_result.is_ok()) << compressor_result.error();
Lz4Compressor compressor = compressor_result.take_value();
size_t compressed_data_size = 0;
auto prepare_result = compressor.Prepare([&](auto compressed_data) {
compressed_data_size = compressed_data.size();
return fpromise::ok();
});
ASSERT_TRUE(prepare_result.is_ok()) << prepare_result.error();
// Check header size is in valid range.
EXPECT_GE(compressed_data_size, static_cast<unsigned int>(LZ4F_HEADER_SIZE_MIN));
EXPECT_LE(compressed_data_size, static_cast<unsigned int>(LZ4F_HEADER_SIZE_MAX));
}
TEST(Lz4CompressorTest, PrepareWithInvalidHandlerIsError) {
CompressionOptions options;
options.schema = CompressionSchema::kLz4;
options.options.clear();
auto compressor_result = Lz4Compressor::Create(options);
ASSERT_TRUE(compressor_result.is_ok()) << compressor_result.error();
Lz4Compressor compressor = compressor_result.take_value();
EXPECT_TRUE(compressor.Prepare(nullptr).is_error());
}
TEST(Lz4CompressorTest, PrepareWhenAlreadyCalledIsError) {
CompressionOptions options;
options.schema = CompressionSchema::kLz4;
options.options.clear();
auto compressor_result = Lz4Compressor::Create(options);
ASSERT_TRUE(compressor_result.is_ok()) << compressor_result.error();
Lz4Compressor compressor = compressor_result.take_value();
auto prepare_result = compressor.Prepare(&HandlerReturnsOk);
ASSERT_TRUE(prepare_result.is_ok()) << prepare_result.error();
EXPECT_TRUE(compressor.Prepare(&HandlerReturnsOk).is_error());
}
constexpr std::string_view kHandlerError = "This is a handler error";
TEST(Lz4CompressorTest, PrepareForwardsHandlerError) {
CompressionOptions options;
options.schema = CompressionSchema::kLz4;
options.options.clear();
auto compressor_result = Lz4Compressor::Create(options);
ASSERT_TRUE(compressor_result.is_ok()) << compressor_result.error();
Lz4Compressor compressor = compressor_result.take_value();
auto prepare_result =
compressor.Prepare([](auto /*unused*/) { return fpromise::error(kHandlerError.data()); });
ASSERT_TRUE(prepare_result.is_error()) << prepare_result.error();
EXPECT_EQ(prepare_result.error(), kHandlerError);
}
constexpr std::string_view kData = "123456789123456789";
TEST(Lz4CompressorTest, PreparedAfterCallingCompressIsError) {
CompressionOptions options;
options.schema = CompressionSchema::kLz4;
options.options.clear();
auto compressor_result = Lz4Compressor::Create(options);
ASSERT_TRUE(compressor_result.is_ok()) << compressor_result.error();
Lz4Compressor compressor = compressor_result.take_value();
auto prepare_result = compressor.Prepare(&HandlerReturnsOk);
ASSERT_TRUE(prepare_result.is_ok()) << prepare_result.error();
auto compress_result = compressor.Compress(
cpp20::span(reinterpret_cast<const uint8_t*>(kData.data()), kData.size()));
EXPECT_TRUE(compress_result.is_ok());
EXPECT_TRUE(compressor.Prepare(&HandlerReturnsOk).is_error());
}
TEST(Lz4CompressorTest, PreparedAfterCallingFinalizeIsOk) {
CompressionOptions options;
options.schema = CompressionSchema::kLz4;
options.options.clear();
auto compressor_result = Lz4Compressor::Create(options);
ASSERT_TRUE(compressor_result.is_ok()) << compressor_result.error();
Lz4Compressor compressor = compressor_result.take_value();
auto prepare_result = compressor.Prepare(&HandlerReturnsOk);
ASSERT_TRUE(prepare_result.is_ok()) << prepare_result.error();
auto compress_result = compressor.Compress(
cpp20::span(reinterpret_cast<const uint8_t*>(kData.data()), kData.size()));
EXPECT_TRUE(compress_result.is_ok()) << compress_result.error();
EXPECT_TRUE(compressor.Finalize().is_ok());
prepare_result = compressor.Prepare(&HandlerReturnsOk);
ASSERT_TRUE(prepare_result.is_ok()) << prepare_result.error();
}
TEST(Lz4CompressorTest, CompressWithCallingPrepareIsOk) {
CompressionOptions options;
options.schema = CompressionSchema::kLz4;
options.options.clear();
auto compressor_result = Lz4Compressor::Create(options);
ASSERT_TRUE(compressor_result.is_ok()) << compressor_result.error();
Lz4Compressor compressor = compressor_result.take_value();
auto prepare_result = compressor.Prepare(&HandlerReturnsOk);
ASSERT_TRUE(prepare_result.is_ok()) << prepare_result.error();
auto compress_result = compressor.Compress(
cpp20::span(reinterpret_cast<const uint8_t*>(kData.data()), kData.size()));
ASSERT_TRUE(compress_result.is_ok()) << compress_result.error();
}
TEST(Lz4CompressorTest, CompressForwardsHandlerError) {
CompressionOptions options;
options.schema = CompressionSchema::kLz4;
options.options.clear();
auto compressor_result = Lz4Compressor::Create(options);
ASSERT_TRUE(compressor_result.is_ok()) << compressor_result.error();
Lz4Compressor compressor = compressor_result.take_value();
bool should_fail = false;
auto prepare_result =
compressor.Prepare([&should_fail](auto /*unused*/) -> fpromise::result<void, std::string> {
if (should_fail) {
return fpromise::error(kHandlerError.data());
}
return fpromise::ok();
});
ASSERT_TRUE(prepare_result.is_ok()) << prepare_result.error();
should_fail = true;
auto compress_result = compressor.Compress(
cpp20::span(reinterpret_cast<const uint8_t*>(kData.data()), kData.size()));
ASSERT_TRUE(compress_result.is_error());
EXPECT_EQ(compress_result.error(), kHandlerError);
}
TEST(Lz4CompressorTest, CompressWithoutCallingPrepareIsError) {
CompressionOptions options;
options.schema = CompressionSchema::kLz4;
options.options.clear();
auto compressor_result = Lz4Compressor::Create(options);
ASSERT_TRUE(compressor_result.is_ok()) << compressor_result.error();
Lz4Compressor compressor = compressor_result.take_value();
auto compress_result = compressor.Compress(
cpp20::span(reinterpret_cast<const uint8_t*>(kData.data()), kData.size()));
ASSERT_TRUE(compress_result.is_error());
}
TEST(Lz4CompressorTest, FinalizeWithoutCallingCompressIsError) {
CompressionOptions options;
options.schema = CompressionSchema::kLz4;
options.options.clear();
auto compressor_result = Lz4Compressor::Create(options);
ASSERT_TRUE(compressor_result.is_ok()) << compressor_result.error();
Lz4Compressor compressor = compressor_result.take_value();
auto prepare_result = compressor.Prepare(&HandlerReturnsOk);
ASSERT_TRUE(prepare_result.is_ok()) << prepare_result.error();
auto finalize_result = compressor.Finalize();
EXPECT_TRUE(finalize_result.is_error());
}
TEST(Lz4CompressorTest, FinalizeForwardsHandlerError) {
CompressionOptions options;
options.schema = CompressionSchema::kLz4;
options.options.clear();
auto compressor_result = Lz4Compressor::Create(options);
ASSERT_TRUE(compressor_result.is_ok()) << compressor_result.error();
Lz4Compressor compressor = compressor_result.take_value();
bool should_fail = false;
auto prepare_result =
compressor.Prepare([&should_fail](auto /*unused*/) -> fpromise::result<void, std::string> {
if (should_fail) {
return fpromise::error(kHandlerError.data());
}
return fpromise::ok();
});
ASSERT_TRUE(prepare_result.is_ok()) << prepare_result.error();
auto compress_result = compressor.Compress(
cpp20::span(reinterpret_cast<const uint8_t*>(kData.data()), kData.size()));
ASSERT_TRUE(compress_result.is_ok());
should_fail = true;
auto finalize_result = compressor.Finalize();
EXPECT_TRUE(finalize_result.is_error());
EXPECT_EQ(finalize_result.error(), kHandlerError);
}
TEST(Lz4CompressorTest, CompressedDataMatchesUncompressedDataWhenDecompressed) {
constexpr size_t kUncompressedSize = 4096;
constexpr size_t kCompressionChunkSize = 512;
static_assert(kUncompressedSize % kCompressionChunkSize == 0);
constexpr size_t kCompressionChunkCount = kUncompressedSize / kCompressionChunkSize;
CompressionOptions options;
std::vector<uint8_t> uncompressed_data(kUncompressedSize, 0);
// Fill with pseudo-random data.
unsigned int seed = testing::UnitTest::GetInstance()->random_seed();
for (size_t i = 0; i < kUncompressedSize; ++i) {
uncompressed_data[i] = static_cast<uint8_t>((rand_r(&seed) + i)); // Truncate to low 8 bits.
}
options.schema = CompressionSchema::kLz4;
options.options.clear();
auto compressor_result = Lz4Compressor::Create(options);
ASSERT_TRUE(compressor_result.is_ok()) << compressor_result.error();
Lz4Compressor compressor = compressor_result.take_value();
size_t max_size = LZ4F_compressBound(kUncompressedSize, &compressor.GetPreferences());
std::vector<uint8_t> compressed_data(max_size, 0);
uint64_t offset = 0;
auto prepare_result =
compressor.Prepare([&compressed_data, &offset](auto compressed_output_data) {
memcpy(compressed_data.data() + offset, compressed_output_data.data(),
compressed_output_data.size());
offset += compressed_output_data.size();
return fpromise::ok();
});
ASSERT_TRUE(prepare_result.is_ok()) << prepare_result.error();
// Copy |kCompressionChunkSize| at a time.
for (size_t i = 0; i < kCompressionChunkCount; ++i) {
auto compress_result = compressor.Compress(
cpp20::span(uncompressed_data.data() + (i * kCompressionChunkSize), kCompressionChunkSize));
}
auto finalize_result = compressor.Finalize();
EXPECT_TRUE(finalize_result.is_ok());
std::vector<uint8_t> decompressed_data(kUncompressedSize, 0);
LZ4F_decompressionContext_t decompression_context = nullptr;
auto decompression_context_result =
LZ4F_createDecompressionContext(&decompression_context, LZ4F_VERSION);
auto release_decompression_context = fit::defer([&decompression_context]() {
if (decompression_context != nullptr) {
LZ4F_freeDecompressionContext(decompression_context);
}
});
ASSERT_FALSE(LZ4F_isError(decompression_context_result));
size_t decompressed_size = decompressed_data.size();
size_t consumed_compressed_size = offset;
auto decompress_result =
LZ4F_decompress(decompression_context, decompressed_data.data(), &decompressed_size,
compressed_data.data(), &consumed_compressed_size, nullptr);
ASSERT_FALSE(LZ4F_isError(decompress_result));
EXPECT_EQ(decompressed_size, kUncompressedSize);
EXPECT_EQ(consumed_compressed_size, offset);
EXPECT_TRUE(
memcmp(decompressed_data.data(), uncompressed_data.data(), uncompressed_data.size()) == 0);
}
} // namespace
} // namespace storage::volume_image