705 lines
25 KiB
C++
705 lines
25 KiB
C++
// Copyright (C) 2019 Google LLC
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
#include "icing/file/file-backed-vector.h"
|
|
|
|
#include <unistd.h>
|
|
|
|
#include <algorithm>
|
|
#include <cerrno>
|
|
#include <cstdint>
|
|
#include <memory>
|
|
#include <string_view>
|
|
#include <vector>
|
|
|
|
#include "knowledge/cerebra/sense/text_classifier/lib3/utils/base/status.h"
|
|
#include "testing/base/public/gmock.h"
|
|
#include "testing/base/public/gunit.h"
|
|
#include "third_party/icing/file/filesystem.h"
|
|
#include "third_party/icing/file/memory-mapped-file.h"
|
|
#include "third_party/icing/file/mock-filesystem.h"
|
|
#include "third_party/icing/testing/common-matchers.h"
|
|
#include "third_party/icing/testing/tmp-directory.h"
|
|
#include "third_party/icing/util/crc32.h"
|
|
#include "third_party/icing/util/logging.h"
|
|
|
|
using ::testing::Eq;
|
|
using ::testing::IsTrue;
|
|
using ::testing::Pointee;
|
|
using ::testing::Return;
|
|
|
|
namespace icing {
|
|
namespace lib {
|
|
|
|
namespace {
|
|
|
|
class FileBackedVectorTest : public testing::Test {
|
|
protected:
|
|
void SetUp() override {
|
|
file_path_ = GetTestTempDir() + "/test.array";
|
|
fd_ = filesystem_.OpenForWrite(file_path_.c_str());
|
|
ASSERT_NE(-1, fd_);
|
|
ASSERT_TRUE(filesystem_.Truncate(fd_, 0));
|
|
}
|
|
|
|
void TearDown() override {
|
|
close(fd_);
|
|
filesystem_.DeleteFile(file_path_.c_str());
|
|
}
|
|
|
|
// Helper method to loop over some data and insert into the vector at some idx
|
|
template <typename T>
|
|
void Insert(FileBackedVector<T>* vector, int32_t idx, std::string data) {
|
|
for (int i = 0; i < data.length(); ++i) {
|
|
ICING_ASSERT_OK(vector->Set(idx + i, data.at(i)));
|
|
}
|
|
}
|
|
|
|
// Helper method to retrieve data from the beginning of the vector
|
|
template <typename T>
|
|
std::string_view Get(FileBackedVector<T>* vector, int32_t expected_len) {
|
|
return Get(vector, 0, expected_len);
|
|
}
|
|
|
|
template <typename T>
|
|
std::string_view Get(FileBackedVector<T>* vector, int32_t idx,
|
|
int32_t expected_len) {
|
|
return std::string_view(vector->array() + idx, expected_len);
|
|
}
|
|
|
|
const Filesystem& filesystem() const { return filesystem_; }
|
|
|
|
Filesystem filesystem_;
|
|
std::string file_path_;
|
|
int fd_;
|
|
};
|
|
|
|
TEST_F(FileBackedVectorTest, Create) {
|
|
{
|
|
// Create a vector for a new file
|
|
ICING_ASSERT_OK_AND_ASSIGN(
|
|
auto vector, FileBackedVector<char>::Create(
|
|
filesystem_, file_path_,
|
|
MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC));
|
|
}
|
|
|
|
{
|
|
// We can create it again based on the same file.
|
|
ICING_ASSERT_OK_AND_ASSIGN(
|
|
auto vector, FileBackedVector<char>::Create(
|
|
filesystem_, file_path_,
|
|
MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC));
|
|
}
|
|
}
|
|
|
|
TEST_F(FileBackedVectorTest, SimpleShared) {
|
|
// Create a vector and add some data.
|
|
ICING_ASSERT_OK_AND_ASSIGN(
|
|
std::unique_ptr<FileBackedVector<char>> vector,
|
|
FileBackedVector<char>::Create(
|
|
filesystem_, file_path_,
|
|
MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC));
|
|
EXPECT_THAT(vector->ComputeChecksum(), IsOkAndHolds(Crc32(0)));
|
|
|
|
std::string expected = "abcde";
|
|
Insert(vector.get(), 0, expected);
|
|
EXPECT_EQ(expected.length(), vector->num_elements());
|
|
EXPECT_EQ(expected, Get(vector.get(), expected.length()));
|
|
|
|
uint32_t good_crc_value = 1134899064U;
|
|
const Crc32 good_crc(good_crc_value);
|
|
// Explicit call to update the crc does update the value
|
|
EXPECT_THAT(vector->ComputeChecksum(), IsOkAndHolds(good_crc));
|
|
|
|
// PersistToDisk does nothing bad.
|
|
ICING_EXPECT_OK(vector->PersistToDisk());
|
|
|
|
// Close out the old vector to ensure everything persists properly before we
|
|
// reassign it
|
|
vector.reset();
|
|
|
|
// Write a bad crc, this would be a mismatch compared to the computed crc of
|
|
// the contents on reinitialization.
|
|
uint32_t bad_crc_value = 123;
|
|
filesystem_.PWrite(file_path_.data(),
|
|
offsetof(FileBackedVector<char>::Header, vector_checksum),
|
|
&bad_crc_value, sizeof(bad_crc_value));
|
|
|
|
ASSERT_THAT(FileBackedVector<char>::Create(
|
|
filesystem_, file_path_,
|
|
MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC),
|
|
StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
|
|
|
|
// Get it back into an ok state
|
|
filesystem_.PWrite(file_path_.data(),
|
|
offsetof(FileBackedVector<char>::Header, vector_checksum),
|
|
&good_crc_value, sizeof(good_crc_value));
|
|
ICING_ASSERT_OK_AND_ASSIGN(
|
|
vector, FileBackedVector<char>::Create(
|
|
filesystem_, file_path_,
|
|
MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC));
|
|
|
|
EXPECT_EQ(expected, Get(vector.get(), expected.length()));
|
|
|
|
// Close out the old vector to ensure everything persists properly before we
|
|
// reassign it
|
|
vector.reset();
|
|
|
|
// Can reinitialize it safely
|
|
ICING_ASSERT_OK_AND_ASSIGN(
|
|
vector, FileBackedVector<char>::Create(
|
|
filesystem_, file_path_,
|
|
MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC));
|
|
|
|
// Truncate the content
|
|
ICING_EXPECT_OK(vector->TruncateTo(0));
|
|
|
|
// Crc is cleared after truncation and reset to 0.
|
|
EXPECT_THAT(vector->ComputeChecksum(), IsOkAndHolds(Crc32(0)));
|
|
EXPECT_EQ(0u, vector->num_elements());
|
|
}
|
|
|
|
TEST_F(FileBackedVectorTest, Get) {
|
|
// Create a vector and add some data.
|
|
ICING_ASSERT_OK_AND_ASSIGN(
|
|
std::unique_ptr<FileBackedVector<char>> vector,
|
|
FileBackedVector<char>::Create(
|
|
filesystem_, file_path_,
|
|
MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC));
|
|
|
|
EXPECT_THAT(vector->ComputeChecksum(), IsOkAndHolds(Crc32(0)));
|
|
|
|
std::string expected = "abc";
|
|
Insert(vector.get(), 0, expected);
|
|
EXPECT_EQ(expected.length(), vector->num_elements());
|
|
|
|
EXPECT_THAT(vector->Get(0), IsOkAndHolds(Pointee(Eq('a'))));
|
|
EXPECT_THAT(vector->Get(1), IsOkAndHolds(Pointee(Eq('b'))));
|
|
EXPECT_THAT(vector->Get(2), IsOkAndHolds(Pointee(Eq('c'))));
|
|
|
|
// Out of bounds error
|
|
EXPECT_THAT(vector->Get(3),
|
|
StatusIs(libtextclassifier3::StatusCode::OUT_OF_RANGE));
|
|
EXPECT_THAT(vector->Get(-1),
|
|
StatusIs(libtextclassifier3::StatusCode::OUT_OF_RANGE));
|
|
}
|
|
|
|
TEST_F(FileBackedVectorTest, IncrementalCrc_NonOverlappingChanges) {
|
|
int num_elements = 1000;
|
|
int incremental_size = 3;
|
|
// Create an array with some data.
|
|
ICING_ASSERT_OK_AND_ASSIGN(
|
|
std::unique_ptr<FileBackedVector<char>> vector,
|
|
FileBackedVector<char>::Create(
|
|
filesystem_, file_path_,
|
|
MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC));
|
|
|
|
Insert(vector.get(), 0, std::string(num_elements, 'a'));
|
|
EXPECT_THAT(vector->ComputeChecksum(), IsOkAndHolds(Crc32(2620640643U)));
|
|
|
|
// Non-overlapping changes to the array, with increasing intervals
|
|
// between updating the checksum. Validate by mapping another array on top.
|
|
uint32_t next_update = 2;
|
|
for (uint32_t i = 0; i < num_elements; i += incremental_size) {
|
|
Insert(vector.get(), i, std::string(incremental_size, 'b'));
|
|
|
|
if (i >= next_update) {
|
|
ICING_ASSERT_OK_AND_ASSIGN(Crc32 incremental_crc,
|
|
vector->ComputeChecksum());
|
|
ICING_LOG(INFO) << "Now crc @" << incremental_crc.Get();
|
|
|
|
Crc32 full_crc;
|
|
std::string_view reconstructed_view =
|
|
std::string_view(vector->array(), vector->num_elements());
|
|
full_crc.Append(reconstructed_view);
|
|
|
|
ASSERT_EQ(incremental_crc, full_crc);
|
|
next_update *= 2;
|
|
}
|
|
}
|
|
|
|
for (uint32_t i = 0; i < num_elements; ++i) {
|
|
EXPECT_THAT(vector->Get(i), IsOkAndHolds(Pointee(Eq('b'))));
|
|
}
|
|
}
|
|
|
|
TEST_F(FileBackedVectorTest, IncrementalCrc_OverlappingChanges) {
|
|
int num_elements = 1000;
|
|
int incremental_size = 3;
|
|
// Create an array with some data.
|
|
ICING_ASSERT_OK_AND_ASSIGN(
|
|
std::unique_ptr<FileBackedVector<char>> vector,
|
|
FileBackedVector<char>::Create(
|
|
filesystem_, file_path_,
|
|
MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC));
|
|
|
|
Insert(vector.get(), 0, std::string(num_elements, 'a'));
|
|
EXPECT_THAT(vector->ComputeChecksum(), IsOkAndHolds(Crc32(2620640643U)));
|
|
|
|
// Overlapping changes to the array, with increasing intervals
|
|
// between updating the checksum. Validate by mapping another array on top.
|
|
uint32_t next_update = 2;
|
|
for (uint32_t i = 0; i < num_elements; i++) {
|
|
Insert(vector.get(), i, std::string(incremental_size, 'b'));
|
|
|
|
if (i >= next_update) {
|
|
ICING_ASSERT_OK_AND_ASSIGN(Crc32 incremental_crc,
|
|
vector->ComputeChecksum());
|
|
ICING_LOG(INFO) << "Now crc @" << incremental_crc.Get();
|
|
|
|
Crc32 full_crc;
|
|
std::string_view reconstructed_view =
|
|
std::string_view(vector->array(), vector->num_elements());
|
|
full_crc.Append(reconstructed_view);
|
|
|
|
ASSERT_EQ(incremental_crc, full_crc);
|
|
next_update *= 2;
|
|
}
|
|
}
|
|
for (uint32_t i = 0; i < num_elements; ++i) {
|
|
EXPECT_THAT(vector->Get(i), IsOkAndHolds(Pointee(Eq('b'))));
|
|
}
|
|
}
|
|
|
|
TEST_F(FileBackedVectorTest, Grow) {
|
|
// This is the same value as FileBackedVector::kMaxNumElts
|
|
constexpr int32_t kMaxNumElts = 1U << 20;
|
|
|
|
ASSERT_TRUE(filesystem_.Truncate(fd_, 0));
|
|
|
|
// Create an array and add some data.
|
|
ICING_ASSERT_OK_AND_ASSIGN(
|
|
std::unique_ptr<FileBackedVector<char>> vector,
|
|
FileBackedVector<char>::Create(
|
|
filesystem_, file_path_,
|
|
MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC));
|
|
EXPECT_THAT(vector->ComputeChecksum(), IsOkAndHolds(Crc32(0)));
|
|
EXPECT_THAT(vector->Set(kMaxNumElts + 11, 'a'),
|
|
StatusIs(libtextclassifier3::StatusCode::OUT_OF_RANGE));
|
|
EXPECT_THAT(vector->Set(-1, 'a'),
|
|
StatusIs(libtextclassifier3::StatusCode::OUT_OF_RANGE));
|
|
|
|
uint32_t start = kMaxNumElts - 13;
|
|
Insert(vector.get(), start, "abcde");
|
|
|
|
// Crc works?
|
|
const Crc32 good_crc(1134899064U);
|
|
EXPECT_THAT(vector->ComputeChecksum(), IsOkAndHolds(good_crc));
|
|
|
|
// PersistToDisk does nothing bad, and ensures the content is still there
|
|
// after we recreate the vector
|
|
ICING_EXPECT_OK(vector->PersistToDisk());
|
|
|
|
// Close out the old vector to ensure everything persists properly before we
|
|
// reassign it
|
|
vector.reset();
|
|
|
|
ICING_ASSERT_OK_AND_ASSIGN(
|
|
vector, FileBackedVector<char>::Create(
|
|
filesystem_, file_path_,
|
|
MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC));
|
|
|
|
std::string expected = "abcde";
|
|
EXPECT_EQ(expected, Get(vector.get(), start, expected.length()));
|
|
}
|
|
|
|
TEST_F(FileBackedVectorTest, GrowsInChunks) {
|
|
// This is the same value as FileBackedVector::kGrowElements
|
|
constexpr int32_t kGrowElements = 1U << 14; // 16K
|
|
|
|
ICING_ASSERT_OK_AND_ASSIGN(
|
|
std::unique_ptr<FileBackedVector<int>> vector,
|
|
FileBackedVector<int>::Create(
|
|
filesystem_, file_path_,
|
|
MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC));
|
|
|
|
// Our initial file size should just be the size of the header. Disk usage
|
|
// will indicate that one block has been allocated, which contains the header.
|
|
int header_size = sizeof(FileBackedVector<char>::Header);
|
|
int page_size = getpagesize();
|
|
EXPECT_THAT(filesystem_.GetFileSize(fd_), Eq(header_size));
|
|
EXPECT_THAT(filesystem_.GetDiskUsage(fd_), Eq(page_size));
|
|
|
|
// Once we add something though, we'll grow to be kGrowElements big. From this
|
|
// point on, file size and disk usage should be the same because Growing will
|
|
// explicitly allocate the number of blocks needed to accomodate the file.
|
|
Insert(vector.get(), 0, "a");
|
|
int file_size = kGrowElements * sizeof(int);
|
|
EXPECT_THAT(filesystem_.GetFileSize(fd_), Eq(file_size));
|
|
EXPECT_THAT(filesystem_.GetDiskUsage(fd_), Eq(file_size));
|
|
|
|
// Should still be the same size, don't need to grow underlying file
|
|
Insert(vector.get(), 1, "b");
|
|
EXPECT_THAT(filesystem_.GetFileSize(fd_), Eq(file_size));
|
|
EXPECT_THAT(filesystem_.GetDiskUsage(fd_), Eq(file_size));
|
|
|
|
// Now we grow by a kGrowElements chunk, so the underlying file is 2
|
|
// kGrowElements big
|
|
file_size *= 2;
|
|
Insert(vector.get(), 2, std::string(kGrowElements, 'c'));
|
|
EXPECT_THAT(filesystem_.GetFileSize(fd_), Eq(file_size));
|
|
EXPECT_THAT(filesystem_.GetDiskUsage(fd_), Eq(file_size));
|
|
|
|
// Destroy/persist the contents.
|
|
vector.reset();
|
|
|
|
// Reinitialize
|
|
ICING_ASSERT_OK_AND_ASSIGN(
|
|
vector, FileBackedVector<int>::Create(
|
|
filesystem_, file_path_,
|
|
MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC));
|
|
|
|
// Should be the same file size as before
|
|
EXPECT_THAT(filesystem_.GetFileSize(file_path_.c_str()),
|
|
Eq(kGrowElements * 2 * sizeof(int)));
|
|
}
|
|
|
|
TEST_F(FileBackedVectorTest, Delete) {
|
|
// Can delete even if there's nothing there
|
|
ICING_EXPECT_OK(FileBackedVector<int64_t>::Delete(filesystem_, file_path_));
|
|
|
|
// Create a vector and add some data.
|
|
ICING_ASSERT_OK_AND_ASSIGN(
|
|
std::unique_ptr<FileBackedVector<char>> vector,
|
|
FileBackedVector<char>::Create(
|
|
filesystem_, file_path_,
|
|
MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC));
|
|
|
|
EXPECT_THAT(vector->ComputeChecksum(), IsOkAndHolds(Crc32(0)));
|
|
|
|
std::string expected = "abcde";
|
|
Insert(vector.get(), 0, expected);
|
|
ASSERT_THAT(vector->ComputeChecksum(), IsOkAndHolds(Crc32(1134899064U)));
|
|
ASSERT_EQ(expected.length(), vector->num_elements());
|
|
|
|
// Close out the old vector to ensure everything persists properly before we
|
|
// delete the underlying files
|
|
vector.reset();
|
|
|
|
ICING_EXPECT_OK(FileBackedVector<int64_t>::Delete(filesystem_, file_path_));
|
|
|
|
EXPECT_FALSE(filesystem_.FileExists(file_path_.data()));
|
|
|
|
// Can successfully create again.
|
|
ICING_ASSERT_OK_AND_ASSIGN(
|
|
vector, FileBackedVector<char>::Create(
|
|
filesystem_, file_path_,
|
|
MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC));
|
|
}
|
|
|
|
TEST_F(FileBackedVectorTest, TruncateTo) {
|
|
ICING_ASSERT_OK_AND_ASSIGN(
|
|
std::unique_ptr<FileBackedVector<char>> vector,
|
|
FileBackedVector<char>::Create(
|
|
filesystem_, file_path_,
|
|
MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC));
|
|
EXPECT_THAT(vector->ComputeChecksum(), IsOkAndHolds(Crc32(0)));
|
|
|
|
Insert(vector.get(), 0, "A");
|
|
Insert(vector.get(), 1, "Z");
|
|
|
|
EXPECT_EQ(2, vector->num_elements());
|
|
EXPECT_THAT(vector->ComputeChecksum(), IsOkAndHolds(Crc32(1658635950)));
|
|
|
|
// Modify 1 element, out of 2 total elements. 1/2 changes exceeds the partial
|
|
// crc limit, so our next checksum call will recompute the entire vector's
|
|
// checksum.
|
|
Insert(vector.get(), 1, "J");
|
|
// We'll ignore everything after the 1st element, so the full vector's
|
|
// checksum will only include "J".
|
|
ICING_EXPECT_OK(vector->TruncateTo(1));
|
|
EXPECT_EQ(1, vector->num_elements());
|
|
EXPECT_THAT(vector->ComputeChecksum(), IsOkAndHolds(Crc32(31158534)));
|
|
|
|
// Truncating clears the checksum and resets it to 0
|
|
ICING_EXPECT_OK(vector->TruncateTo(0));
|
|
EXPECT_EQ(0, vector->num_elements());
|
|
EXPECT_THAT(vector->ComputeChecksum(), IsOkAndHolds(Crc32(0)));
|
|
|
|
// Can't truncate past end.
|
|
EXPECT_THAT(vector->TruncateTo(100),
|
|
StatusIs(libtextclassifier3::StatusCode::OUT_OF_RANGE));
|
|
|
|
// Must be greater than or equal to 0
|
|
EXPECT_THAT(vector->TruncateTo(-1),
|
|
StatusIs(libtextclassifier3::StatusCode::OUT_OF_RANGE));
|
|
}
|
|
|
|
TEST_F(FileBackedVectorTest, TruncateAndReReadFile) {
|
|
{
|
|
ICING_ASSERT_OK_AND_ASSIGN(
|
|
std::unique_ptr<FileBackedVector<float>> vector,
|
|
FileBackedVector<float>::Create(
|
|
filesystem_, file_path_,
|
|
MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC));
|
|
|
|
ICING_ASSERT_OK(vector->Set(0, 1.0));
|
|
ICING_ASSERT_OK(vector->Set(1, 2.0));
|
|
ICING_ASSERT_OK(vector->Set(2, 2.0));
|
|
ICING_ASSERT_OK(vector->Set(3, 2.0));
|
|
ICING_ASSERT_OK(vector->Set(4, 2.0));
|
|
} // Destroying the vector should trigger a checksum of the 5 elements
|
|
|
|
{
|
|
ICING_ASSERT_OK_AND_ASSIGN(
|
|
std::unique_ptr<FileBackedVector<float>> vector,
|
|
FileBackedVector<float>::Create(
|
|
filesystem_, file_path_,
|
|
MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC));
|
|
|
|
EXPECT_EQ(5, vector->num_elements());
|
|
ICING_EXPECT_OK(vector->TruncateTo(4));
|
|
EXPECT_EQ(4, vector->num_elements());
|
|
} // Destroying the vector should update the checksum to 4 elements
|
|
|
|
// Creating again should double check that our checksum of 4 elements matches
|
|
// what was previously saved.
|
|
{
|
|
ICING_ASSERT_OK_AND_ASSIGN(
|
|
std::unique_ptr<FileBackedVector<float>> vector,
|
|
FileBackedVector<float>::Create(
|
|
filesystem_, file_path_,
|
|
MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC));
|
|
|
|
EXPECT_EQ(vector->num_elements(), 4);
|
|
}
|
|
}
|
|
|
|
TEST_F(FileBackedVectorTest, InitFileTooSmallForHeaderFails) {
|
|
{
|
|
// 1. Create a vector with a few elements.
|
|
ICING_ASSERT_OK_AND_ASSIGN(
|
|
std::unique_ptr<FileBackedVector<char>> vector,
|
|
FileBackedVector<char>::Create(
|
|
filesystem_, file_path_,
|
|
MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC));
|
|
Insert(vector.get(), 0, "A");
|
|
Insert(vector.get(), 1, "Z");
|
|
ASSERT_THAT(vector->PersistToDisk(), IsOk());
|
|
}
|
|
|
|
// 2. Shrink the file to be smaller than the header.
|
|
filesystem_.Truncate(fd_, sizeof(FileBackedVector<char>::Header) - 1);
|
|
|
|
{
|
|
// 3. Attempt to create the file and confirm that it fails.
|
|
EXPECT_THAT(FileBackedVector<char>::Create(
|
|
filesystem_, file_path_,
|
|
MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC),
|
|
StatusIs(libtextclassifier3::StatusCode::INTERNAL));
|
|
}
|
|
}
|
|
|
|
TEST_F(FileBackedVectorTest, InitWrongDataSizeFails) {
|
|
{
|
|
// 1. Create a vector with a few elements.
|
|
ICING_ASSERT_OK_AND_ASSIGN(
|
|
std::unique_ptr<FileBackedVector<char>> vector,
|
|
FileBackedVector<char>::Create(
|
|
filesystem_, file_path_,
|
|
MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC));
|
|
Insert(vector.get(), 0, "A");
|
|
Insert(vector.get(), 1, "Z");
|
|
ASSERT_THAT(vector->PersistToDisk(), IsOk());
|
|
}
|
|
|
|
{
|
|
// 2. Attempt to create the file with a different element size and confirm
|
|
// that it fails.
|
|
EXPECT_THAT(FileBackedVector<int>::Create(
|
|
filesystem_, file_path_,
|
|
MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC),
|
|
StatusIs(libtextclassifier3::StatusCode::INTERNAL));
|
|
}
|
|
}
|
|
|
|
TEST_F(FileBackedVectorTest, InitCorruptHeaderFails) {
|
|
{
|
|
// 1. Create a vector with a few elements.
|
|
ICING_ASSERT_OK_AND_ASSIGN(
|
|
std::unique_ptr<FileBackedVector<char>> vector,
|
|
FileBackedVector<char>::Create(
|
|
filesystem_, file_path_,
|
|
MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC));
|
|
Insert(vector.get(), 0, "A");
|
|
Insert(vector.get(), 1, "Z");
|
|
ASSERT_THAT(vector->PersistToDisk(), IsOk());
|
|
}
|
|
|
|
// 2. Modify the header, but don't update the checksum. This would be similar
|
|
// to corruption of the header.
|
|
FileBackedVector<char>::Header header;
|
|
ASSERT_THAT(filesystem_.PRead(fd_, &header, sizeof(header), /*offset=*/0),
|
|
IsTrue());
|
|
header.num_elements = 1;
|
|
ASSERT_THAT(filesystem_.PWrite(fd_, /*offset=*/0, &header, sizeof(header)),
|
|
IsTrue());
|
|
|
|
{
|
|
// 3. Attempt to create the file with a header that doesn't match its
|
|
// checksum and confirm that it fails.
|
|
EXPECT_THAT(FileBackedVector<char>::Create(
|
|
filesystem_, file_path_,
|
|
MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC),
|
|
StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
|
|
}
|
|
}
|
|
|
|
TEST_F(FileBackedVectorTest, InitHeaderElementSizeTooBigFails) {
|
|
{
|
|
// 1. Create a vector with a few elements.
|
|
ICING_ASSERT_OK_AND_ASSIGN(
|
|
std::unique_ptr<FileBackedVector<char>> vector,
|
|
FileBackedVector<char>::Create(
|
|
filesystem_, file_path_,
|
|
MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC));
|
|
Insert(vector.get(), 0, "A");
|
|
Insert(vector.get(), 1, "Z");
|
|
ASSERT_THAT(vector->PersistToDisk(), IsOk());
|
|
}
|
|
|
|
// 2. Modify the header so that the number of elements exceeds the actual size
|
|
// of the underlying file.
|
|
FileBackedVector<char>::Header header;
|
|
ASSERT_THAT(filesystem_.PRead(fd_, &header, sizeof(header), /*offset=*/0),
|
|
IsTrue());
|
|
int64_t file_size = filesystem_.GetFileSize(fd_);
|
|
int64_t allocated_elements_size = file_size - sizeof(header);
|
|
header.num_elements = (allocated_elements_size / sizeof(char)) + 1;
|
|
header.header_checksum = header.CalculateHeaderChecksum();
|
|
ASSERT_THAT(filesystem_.PWrite(fd_, /*offset=*/0, &header, sizeof(header)),
|
|
IsTrue());
|
|
|
|
{
|
|
// 3. Attempt to create the file with num_elements that is larger than the
|
|
// underlying file and confirm that it fails.
|
|
EXPECT_THAT(FileBackedVector<char>::Create(
|
|
filesystem_, file_path_,
|
|
MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC),
|
|
StatusIs(libtextclassifier3::StatusCode::INTERNAL));
|
|
}
|
|
}
|
|
|
|
TEST_F(FileBackedVectorTest, InitCorruptElementsFails) {
|
|
{
|
|
// 1. Create a vector with a few elements.
|
|
ICING_ASSERT_OK_AND_ASSIGN(
|
|
std::unique_ptr<FileBackedVector<char>> vector,
|
|
FileBackedVector<char>::Create(
|
|
filesystem_, file_path_,
|
|
MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC));
|
|
Insert(vector.get(), 0, "A");
|
|
Insert(vector.get(), 1, "Z");
|
|
ASSERT_THAT(vector->PersistToDisk(), IsOk());
|
|
}
|
|
|
|
// 2. Overwrite the values of the first two elements.
|
|
std::string corrupted_content = "BY";
|
|
ASSERT_THAT(
|
|
filesystem_.PWrite(fd_, /*offset=*/sizeof(FileBackedVector<char>::Header),
|
|
corrupted_content.c_str(), corrupted_content.length()),
|
|
IsTrue());
|
|
|
|
{
|
|
// 3. Attempt to create the file with elements that don't match their
|
|
// checksum and confirm that it fails.
|
|
EXPECT_THAT(FileBackedVector<char>::Create(
|
|
filesystem_, file_path_,
|
|
MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC),
|
|
StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
|
|
}
|
|
}
|
|
|
|
TEST_F(FileBackedVectorTest, InitNormalSucceeds) {
|
|
{
|
|
// 1. Create a vector with a few elements.
|
|
ICING_ASSERT_OK_AND_ASSIGN(
|
|
std::unique_ptr<FileBackedVector<char>> vector,
|
|
FileBackedVector<char>::Create(
|
|
filesystem_, file_path_,
|
|
MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC));
|
|
Insert(vector.get(), 0, "A");
|
|
Insert(vector.get(), 1, "Z");
|
|
ASSERT_THAT(vector->PersistToDisk(), IsOk());
|
|
}
|
|
|
|
{
|
|
// 2. Attempt to create the file with a completely valid header and elements
|
|
// region. This should succeed.
|
|
EXPECT_THAT(FileBackedVector<char>::Create(
|
|
filesystem_, file_path_,
|
|
MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC),
|
|
IsOk());
|
|
}
|
|
}
|
|
|
|
TEST_F(FileBackedVectorTest, RemapFailureStillValidInstance) {
|
|
auto mock_filesystem = std::make_unique<MockFilesystem>();
|
|
ICING_ASSERT_OK_AND_ASSIGN(
|
|
std::unique_ptr<FileBackedVector<int>> vector,
|
|
FileBackedVector<int>::Create(
|
|
*mock_filesystem, file_path_,
|
|
MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC));
|
|
|
|
// 1. Write data to just before the first block resize. Running the test
|
|
// locally has determined that we'll first resize at 65531st entry.
|
|
constexpr int kResizingIndex = 16378;
|
|
for (int i = 0; i < kResizingIndex; ++i) {
|
|
ICING_ASSERT_OK(vector->Set(i, 7));
|
|
}
|
|
|
|
// 2. The next Set call should cause a resize and a remap. Make that remap
|
|
// fail.
|
|
int num_calls = 0;
|
|
auto open_lambda = [this, &num_calls](const char* file_name){
|
|
if (++num_calls == 2) {
|
|
return -1;
|
|
}
|
|
return this->filesystem().OpenForWrite(file_name);
|
|
};
|
|
ON_CALL(*mock_filesystem, OpenForWrite(_)).WillByDefault(open_lambda);
|
|
EXPECT_THAT(vector->Set(kResizingIndex, 7),
|
|
StatusIs(libtextclassifier3::StatusCode::INTERNAL));
|
|
|
|
// 3. We should still be able to call set correctly for earlier regions.
|
|
ICING_EXPECT_OK(vector->Set(kResizingIndex / 2, 9));
|
|
EXPECT_THAT(vector->Get(kResizingIndex / 2), IsOkAndHolds(Pointee(Eq(9))));
|
|
}
|
|
|
|
TEST_F(FileBackedVectorTest, BadFileSizeDuringGrowReturnsError) {
|
|
auto mock_filesystem = std::make_unique<MockFilesystem>();
|
|
ICING_ASSERT_OK_AND_ASSIGN(
|
|
std::unique_ptr<FileBackedVector<int>> vector,
|
|
FileBackedVector<int>::Create(
|
|
*mock_filesystem, file_path_,
|
|
MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC));
|
|
|
|
// At first, the vector is empty and has no mapping established. The first Set
|
|
// call will cause a Grow.
|
|
// During Grow, we will attempt to check the underlying file size to see if
|
|
// growing is actually necessary. Return an error on the call to GetFileSize.
|
|
ON_CALL(*mock_filesystem, GetFileSize(A<const char*>()))
|
|
.WillByDefault(Return(Filesystem::kBadFileSize));
|
|
|
|
// We should fail gracefully and return an INTERNAL error to indicate that
|
|
// there was an issue retrieving the file size.
|
|
EXPECT_THAT(vector->Set(0, 7),
|
|
StatusIs(libtextclassifier3::StatusCode::INTERNAL));
|
|
}
|
|
|
|
} // namespace
|
|
|
|
} // namespace lib
|
|
} // namespace icing
|