blob: 51f6d635e5e12f3f6cc42d446574af1c88f34551 [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/fs_test/truncate_fixture.h"
#include <fcntl.h>
#include <sched.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>
#include <zircon/syscalls.h>
#include <iostream>
#include <memory>
#include <fbl/alloc_checker.h>
#include <fbl/unique_fd.h>
namespace fs_test {
namespace {
void FillFile(int fd, uint8_t* u8, ssize_t new_len, ssize_t old_len) {
fbl::AllocChecker ac;
std::unique_ptr<uint8_t[]> readbuf(new (&ac) uint8_t[new_len]);
ASSERT_TRUE(ac.check());
if (new_len > old_len) { // Expanded the file
// Verify that the file is unchanged up to old_len
ASSERT_EQ(lseek(fd, 0, SEEK_SET), 0);
ASSERT_EQ(read(fd, readbuf.get(), old_len), old_len);
ASSERT_EQ(memcmp(readbuf.get(), u8, old_len), 0);
// Verify that the file is filled with zeroes from old_len to new_len
ASSERT_EQ(lseek(fd, old_len, SEEK_SET), old_len);
ASSERT_EQ(read(fd, readbuf.get(), new_len - old_len), new_len - old_len);
for (ssize_t n = 0; n < (new_len - old_len); n++) {
ASSERT_EQ(readbuf[n], 0) << "old_len=" << old_len << ", new_len=" << new_len << ", n=" << n;
}
// Overwrite those zeroes with the contents of u8
ASSERT_EQ(lseek(fd, old_len, SEEK_SET), old_len);
auto bytes_to_write = new_len - old_len;
// We write |bytes_to_write| in a loop because write() may succeed but still return fewer bytes
// than requested.
while (bytes_to_write > 0) {
// We need fsync here because CoW based fs(like minfs) may need more blocks than absolute
// minimum required as updates/overwrites go to a new block while still retaining old
// block reservation.
fsync(fd);
auto written = write(fd, u8 + old_len, new_len - old_len);
ASSERT_GT(written, 0);
bytes_to_write -= written;
old_len += written;
}
} else { // Shrunk the file (or kept it the same length)
// Verify that the file is unchanged up to new_len
ASSERT_EQ(lseek(fd, 0, SEEK_SET), 0);
ASSERT_EQ(read(fd, readbuf.get(), new_len), new_len);
ASSERT_EQ(memcmp(readbuf.get(), u8, new_len), 0);
}
}
void CheckedTruncate(TestFilesystem& fs, bool remount, const char* filename, uint8_t* u8,
ssize_t new_len) {
// Acquire the old size
struct stat st;
ASSERT_EQ(stat(filename, &st), 0);
ssize_t old_len = st.st_size;
// Truncate the file, verify the size gets updated
fbl::unique_fd fd(open(filename, O_RDWR, 0644));
ASSERT_TRUE(fd);
ASSERT_EQ(ftruncate(fd.get(), new_len), 0);
ASSERT_EQ(stat(filename, &st), 0);
ASSERT_EQ(st.st_size, new_len);
// Close and reopen the file; verify the inode stays updated
ASSERT_EQ(close(fd.release()), 0);
fd.reset(open(filename, O_RDWR, 0644));
ASSERT_TRUE(fd);
ASSERT_EQ(stat(filename, &st), 0);
ASSERT_EQ(st.st_size, new_len);
if (remount) {
ASSERT_EQ(close(fd.release()), 0);
EXPECT_EQ(fs.Unmount().status_value(), ZX_OK);
EXPECT_EQ(fs.Fsck().status_value(), ZX_OK);
EXPECT_EQ(fs.Mount().status_value(), ZX_OK);
ASSERT_EQ(stat(filename, &st), 0);
ASSERT_EQ(st.st_size, new_len);
fd.reset(open(filename, O_RDWR, 0644));
}
ASSERT_NO_FATAL_FAILURE(FillFile(fd.get(), u8, new_len, old_len));
}
void CheckedFtruncate(int fd, uint8_t* u8, ssize_t new_len) {
// Acquire the old size
struct stat st;
ASSERT_EQ(fstat(fd, &st), 0);
ssize_t old_len = st.st_size;
// Truncate the file, verify the size gets updated
ASSERT_EQ(ftruncate(fd, new_len), 0);
ASSERT_EQ(fstat(fd, &st), 0);
ASSERT_EQ(st.st_size, new_len);
ASSERT_NO_FATAL_FAILURE(FillFile(fd, u8, new_len, old_len));
}
// Test that truncate doesn't have issues dealing with larger files
// Repeatedly write to / truncate a file.
TEST_P(LargeTruncateTest, RepeatedlyWritingAndTruncatingLargeFileSucceeds) {
// Fill a test buffer with data
fbl::AllocChecker ac;
std::unique_ptr<uint8_t[]> buf(new (&ac) uint8_t[buffer_size()]);
ASSERT_TRUE(ac.check());
unsigned seed = static_cast<unsigned>(zx_ticks_get());
std::cout << "Truncate test using seed: " << seed << std::endl;
srand(seed);
for (unsigned n = 0; n < buffer_size(); n++) {
buf[n] = static_cast<uint8_t>(rand_r(&seed));
}
// Start a file filled with a buffer
const std::string filename = GetPath("alpha");
fbl::unique_fd fd(open(filename.c_str(), O_RDWR | O_CREAT, 0644));
ASSERT_TRUE(fd);
ASSERT_EQ(write(fd.get(), buf.get(), buffer_size()), static_cast<ssize_t>(buffer_size()));
if (test_type() != LargeTruncateTestType::KeepOpen) {
ASSERT_EQ(close(fd.release()), 0);
}
// Repeatedly truncate / write to the file
for (size_t i = 0; i < iterations(); ++i) {
size_t len = rand_r(&seed) % buffer_size();
if (test_type() == LargeTruncateTestType::KeepOpen) {
ASSERT_NO_FATAL_FAILURE(CheckedFtruncate(fd.get(), buf.get(), len));
} else {
ASSERT_NO_FATAL_FAILURE(CheckedTruncate(fs(), test_type() == LargeTruncateTestType::Remount,
filename.c_str(), buf.get(), len));
}
}
ASSERT_EQ(unlink(filename.c_str()), 0);
if (test_type() == LargeTruncateTestType::KeepOpen) {
ASSERT_EQ(close(fd.release()), 0);
}
}
} // namespace
std::string GetDescriptionForLargeTruncateTestParamType(
const testing::TestParamInfo<LargeTruncateTestParamType>& param) {
std::stringstream s;
s << std::get<0>(param.param) << "WithBufferSize"
<< std::to_string(std::get<0>(std::get<1>(param.param))) << "Iterations"
<< std::to_string(std::get<1>(std::get<1>(param.param))) << "Type";
switch (std::get<2>(std::get<1>(param.param))) {
case LargeTruncateTestType::KeepOpen:
s << "KeepOpen";
break;
case LargeTruncateTestType::Reopen:
s << "Reopen";
break;
case LargeTruncateTestType::Remount:
s << "Remount";
break;
}
return s.str();
}
} // namespace fs_test