// Copyright 2019 The Chromium 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 <fcntl.h>
#include <stdint.h>

#include <sys/mman.h>
#include <memory>

#include "base/bind.h"
#include "base/callback.h"
#include "base/callback_helpers.h"
#include "base/files/scoped_file.h"
#include "base/memory/madv_free_discardable_memory_allocator_posix.h"
#include "base/memory/madv_free_discardable_memory_posix.h"
#include "base/memory/page_size.h"
#include "build/build_config.h"
#include "testing/gtest/include/gtest/gtest.h"

#define SUCCEED_IF_MADV_FREE_UNSUPPORTED()                                  \
  do {                                                                      \
    if (GetMadvFreeSupport() != base::MadvFreeSupport::kSupported) {        \
      SUCCEED()                                                             \
          << "MADV_FREE is not supported (Linux 4.5+ required), vacuously " \
             "passing test";                                                \
      return;                                                               \
    }                                                                       \
  } while (0)

namespace base {
std::atomic<size_t> allocator_byte_count;
class MadvFreeDiscardableMemoryPosixTester
    : public MadvFreeDiscardableMemoryPosix {
 public:
  MadvFreeDiscardableMemoryPosixTester(size_t size_in_bytes)
      : MadvFreeDiscardableMemoryPosix(size_in_bytes, &allocator_byte_count) {}

  using MadvFreeDiscardableMemoryPosix::DiscardPage;
  using MadvFreeDiscardableMemoryPosix::GetPageCount;
  using MadvFreeDiscardableMemoryPosix::IsLockedForTesting;
  using MadvFreeDiscardableMemoryPosix::IsValid;
  using MadvFreeDiscardableMemoryPosix::SetKeepMemoryForTesting;
};

class MadvFreeDiscardableMemoryTest : public ::testing::Test {
 protected:
  MadvFreeDiscardableMemoryTest() {}
  ~MadvFreeDiscardableMemoryTest() override {}

  const size_t kPageSize = base::GetPageSize();

  std::unique_ptr<MadvFreeDiscardableMemoryPosixTester>
  AllocateLockedDiscardableMemoryPagesForTest(size_t size_in_pages) {
    return std::make_unique<MadvFreeDiscardableMemoryPosixTester>(
        size_in_pages * kPageSize);
  }
};

using MadvFreeDiscardableMemoryDeathTest = MadvFreeDiscardableMemoryTest;

constexpr char kTestPattern[] =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

TEST_F(MadvFreeDiscardableMemoryTest, AllocateAndUse) {
  SUCCEED_IF_MADV_FREE_UNSUPPORTED();

  std::unique_ptr<MadvFreeDiscardableMemoryPosixTester> mem =
      AllocateLockedDiscardableMemoryPagesForTest(1);

  mem->SetKeepMemoryForTesting(true);

  ASSERT_TRUE(mem->IsValid());
  ASSERT_TRUE(mem->IsLockedForTesting());

  char buffer[sizeof(kTestPattern)];

  // Write test pattern to block
  uint8_t* data = mem->data_as<uint8_t>();
  memcpy(data, kTestPattern, sizeof(kTestPattern));

  // Read test pattern from block
  data = mem->data_as<uint8_t>();
  memcpy(buffer, data, sizeof(kTestPattern));

  EXPECT_EQ(memcmp(kTestPattern, buffer, sizeof(kTestPattern)), 0);

  // Memory contents should not change after successful unlock and lock.
  mem->Unlock();
  ASSERT_TRUE(mem->Lock());

  EXPECT_EQ(memcmp(kTestPattern, buffer, sizeof(kTestPattern)), 0);
}

TEST_F(MadvFreeDiscardableMemoryTest, LockAndUnlock) {
  SUCCEED_IF_MADV_FREE_UNSUPPORTED();

  const size_t kPageCount = 10;
  std::unique_ptr<MadvFreeDiscardableMemoryPosixTester> mem =
      AllocateLockedDiscardableMemoryPagesForTest(kPageCount);

  ASSERT_TRUE(mem->IsValid());
  ASSERT_TRUE(mem->IsLockedForTesting());
  memset(mem->data(), 0xE7, kPageSize * kPageCount);
  mem->Unlock();
  ASSERT_FALSE(mem->IsLockedForTesting());
  bool result = mem->Lock();
  // If Lock() succeeded, the memory region should be valid. If Lock() failed,
  // the memory region should be invalid.
  ASSERT_EQ(result, mem->IsValid());
}

TEST_F(MadvFreeDiscardableMemoryTest, LockShouldFailAfterDiscard) {
  SUCCEED_IF_MADV_FREE_UNSUPPORTED();

  constexpr size_t kPageCount = 10;

  std::unique_ptr<MadvFreeDiscardableMemoryPosixTester> mem =
      AllocateLockedDiscardableMemoryPagesForTest(kPageCount);
  uint8_t* data = mem->data_as<uint8_t>();

  ASSERT_TRUE(mem->IsValid());
  ASSERT_TRUE(mem->IsLockedForTesting());
  // Modify block data such that at least one page is non-zero.
  memset(data, 0xff, kPageSize * kPageCount);

  mem->Unlock();
  ASSERT_FALSE(mem->IsLockedForTesting());
  // Forcefully discard at least one non-zero page.
  mem->DiscardPage(5);

  // Locking when a page has been discarded should fail.
  ASSERT_FALSE(mem->Lock());
  // Locking after memory is deallocated should fail.
  ASSERT_FALSE(mem->Lock());

  // Check that memory has been deallocated.
  ASSERT_FALSE(mem->IsValid());
}

}  // namespace base
