chromium/chromeos/ash/components/memory/userspace_swap/swap_storage_unittest.cc

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/ash/components/memory/userspace_swap/swap_storage.h"

#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>

#include <chrono>
#include <random>

#include "base/files/file_util.h"
#include "base/files/scoped_file.h"
#include "base/posix/eintr_wrapper.h"
#include "base/types/fixed_array.h"
#include "chromeos/ash/components/memory/userspace_swap/region.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace ash {
namespace memory {
namespace userspace_swap {

class SwapStorageTest : public testing::Test,
                        public testing::WithParamInterface<SwapFile::Type> {
 public:
  SwapStorageTest()
      : random_(/* seen prng */
                std::chrono::system_clock::now().time_since_epoch().count()) {}

 protected:
  void SetUp() override {
    // Start by creating a temporary file we will use for this test and then
    // wrapping it in the swap type we're testing.
    base::FilePath temp_file;
    ASSERT_TRUE(base::CreateTemporaryFile(&temp_file));

    base::ScopedFD swap_fd;
    swap_fd.reset(HANDLE_EINTR(open(temp_file.MaybeAsASCII().c_str(),
                                    O_RDWR | O_CLOEXEC, S_IRUSR | S_IWUSR)));

    ASSERT_NE(unlink(temp_file.MaybeAsASCII().c_str()), -1);

    ASSERT_TRUE(swap_fd.is_valid());

    // Wrap this FD in the swap file type for this test.
    swap_ = SwapFile::WrapFD(std::move(swap_fd), GetParam());
    ASSERT_NE(swap_, nullptr);
  }

  void FillRandom(std::string* str, int size) {
    for (int j = 0; j < size; ++j) {
      str->append(1, static_cast<char>(random_() % 255));
    }

    ASSERT_EQ(str->size(), static_cast<std::string::size_type>(size));
  }

  std::default_random_engine random_;
  std::unique_ptr<SwapFile> swap_;
};

// We test all the different variations of a swap file to make sure all
// functionality is properly implemented, this will test:
// - Standard swap file with no compression or encryption.
// - A compressed swap file.
// - An encrypted swap file.
// - A compressed and encrypted swap file.
INSTANTIATE_TEST_SUITE_P(
    SwapStorageTest,
    SwapStorageTest,
    testing::Values(
        SwapFile::Type::kStandard /* no compression or encryption */,
        SwapFile::Type::kCompressed,
        SwapFile::Type::kEncrypted,
        SwapFile::Type::kEncrypted | SwapFile::Type::kCompressed));

TEST_P(SwapStorageTest, SimpleWriteRead) {
  std::string buffer = "hello world";
  constexpr size_t buffer_len = sizeof("hello world") - 1;

  // Swap address can be at index 0 so we use UINT64_MAX to differentiate.
  Region swap_region(std::numeric_limits<uint64_t>::max(), 0);

  // Write it to swap and validate we also got back sane values for swap pos and
  // length.
  ASSERT_TRUE(
      swap_->WriteToSwap(Region(buffer.c_str(), buffer_len), &swap_region));
  ASSERT_NE(swap_region.address, std::numeric_limits<uint64_t>::max());
  ASSERT_NE(swap_region.length, 0u);

  // Read the region from swap in [swap_pos, swap_pos + swap_len]
  char read_buf[buffer_len];
  memset(read_buf, 0, sizeof(read_buf));
  ASSERT_EQ(
      swap_->ReadFromSwap(swap_region, Region(read_buf, sizeof(read_buf))),
      static_cast<ssize_t>(sizeof(read_buf)));

  // We should have correctly read back what we wrote.
  ASSERT_EQ(memcmp(read_buf, buffer.c_str(), sizeof(read_buf)), 0);
}

TEST_P(SwapStorageTest, ManyWriteRead) {
  // Write 1000 random length buffers and then read them back in a random order
  // and make sure they are as expected.
  std::vector<std::pair<std::string, Region>> buffers;

  constexpr int kNumBuffers = 1000;
  buffers.reserve(kNumBuffers);
  for (int i = 0; i < kNumBuffers; ++i) {
    // Choose a random length between 1 byte and 10KB
    int buffer_len = (random_() % (10 << 10)) + 1;
    std::string buf;
    buf.reserve(buffer_len);
    FillRandom(&buf, buffer_len);

    // Swap pos can be at index 0 so we use UINT64_MAX to differentiate.
    Region swap_region(std::numeric_limits<uint64_t>::max(), 0);

    // Write it to swap and validate we also got back sane values for swap pos
    // and length.
    ASSERT_TRUE(
        swap_->WriteToSwap(Region(buf.c_str(), buf.size()), &swap_region));
    ASSERT_NE(swap_region.address, std::numeric_limits<uint64_t>::max());
    ASSERT_NE(swap_region.length, 0u);

    // Save where this buffer was written.
    buffers.emplace_back(std::move(buf), swap_region);
  }

  // Shuffle the ordering of the buffers so we read them back in a random order.
  std::shuffle(buffers.begin(), buffers.end(), random_);

  // Read back all the regions and verify.
  for (const auto& buf : buffers) {
    base::FixedArray<char> read_buf(buf.first.size());
    ASSERT_EQ(swap_->ReadFromSwap(/* Region */ buf.second,
                                  Region(read_buf.data(), read_buf.memsize())),
              static_cast<ssize_t>(read_buf.memsize()));

    // We should have correctly read back what we wrote.
    ASSERT_EQ(memcmp(read_buf.data(), buf.first.c_str(), read_buf.memsize()),
              0);

    // Now drop it from the swap.
    ASSERT_TRUE(swap_->DropFromSwap(/* Region */ buf.second));
  }
}

TEST_P(SwapStorageTest, DropFromSwap) {
  // This test validates that we can drop what we wrote from the swap file and
  // the block size will return to the original size.
  uint64_t block_size_kb_before = swap_->GetUsageKB();
  std::string buffer;

  // We want to fill buffer with what should be many blocks of random data so we
  // can fully observe the growing and shrinking size.
  FillRandom(&buffer, 32 * (4 << 10));

  // Swap pos can be at index 0 so we use UINT64_MAX to differentiate.
  Region swap_region(std::numeric_limits<uint64_t>::max(), 0);

  // Write it to swap and validate we also got back sane values for swap pos and
  // length.
  ASSERT_TRUE(
      swap_->WriteToSwap(Region(buffer.c_str(), buffer.size()), &swap_region));
  ASSERT_NE(swap_region.address, std::numeric_limits<uint64_t>::max());
  ASSERT_NE(swap_region.length, 0u);

  uint64_t block_size_kb = swap_->GetUsageKB();
  ASSERT_GT(block_size_kb, block_size_kb_before);

  // Read the region from swap in [swap_pos, swap_pos + swap_len]
  base::FixedArray<char> read_buf(buffer.length());
  ASSERT_EQ(swap_->ReadFromSwap(swap_region,
                                Region(read_buf.data(), read_buf.memsize())),
            static_cast<ssize_t>(read_buf.memsize()));

  // We should have correctly read back what we wrote.
  ASSERT_EQ(memcmp(read_buf.data(), buffer.c_str(), read_buf.memsize()), 0);

  // Now we will drop it.
  ASSERT_TRUE(swap_->DropFromSwap(swap_region));

  // Finally check the size.
  uint64_t block_size_kb_end = swap_->GetUsageKB();
  ASSERT_LT(block_size_kb_end, block_size_kb);
}

}  // namespace userspace_swap
}  // namespace memory
}  // namespace ash