//===-- sanitizer_quarantine_test.cc --------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
// This file is a part of ThreadSanitizer/AddressSanitizer runtime.
//
//===----------------------------------------------------------------------===//
#include "sanitizer_common/sanitizer_common.h"
#include "sanitizer_common/sanitizer_quarantine.h"
#include "gtest/gtest.h"

#include <stdlib.h>

namespace __sanitizer {

struct QuarantineCallback {
  void Recycle(void *m) {}
  void *Allocate(uptr size) {
    return malloc(size);
  }
  void Deallocate(void *p) {
    free(p);
  }
};

typedef QuarantineCache<QuarantineCallback> Cache;

static void* kFakePtr = reinterpret_cast<void*>(0xFA83FA83);
static const size_t kBlockSize = 8;

static QuarantineCallback cb;

static void DeallocateCache(Cache *cache) {
  while (QuarantineBatch *batch = cache->DequeueBatch())
    cb.Deallocate(batch);
}

TEST(SanitizerCommon, QuarantineBatchMerge) {
  // Verify the trivial case.
  QuarantineBatch into;
  into.init(kFakePtr, 4UL);
  QuarantineBatch from;
  from.init(kFakePtr, 8UL);

  into.merge(&from);

  ASSERT_EQ(into.count, 2UL);
  ASSERT_EQ(into.batch[0], kFakePtr);
  ASSERT_EQ(into.batch[1], kFakePtr);
  ASSERT_EQ(into.size, 12UL + sizeof(QuarantineBatch));
  ASSERT_EQ(into.quarantined_size(), 12UL);

  ASSERT_EQ(from.count, 0UL);
  ASSERT_EQ(from.size, sizeof(QuarantineBatch));
  ASSERT_EQ(from.quarantined_size(), 0UL);

  // Merge the batch to the limit.
  for (uptr i = 2; i < QuarantineBatch::kSize; ++i)
    from.push_back(kFakePtr, 8UL);
  ASSERT_TRUE(into.count + from.count == QuarantineBatch::kSize);
  ASSERT_TRUE(into.can_merge(&from));

  into.merge(&from);
  ASSERT_TRUE(into.count == QuarantineBatch::kSize);

  // No more space, not even for one element.
  from.init(kFakePtr, 8UL);

  ASSERT_FALSE(into.can_merge(&from));
}

TEST(SanitizerCommon, QuarantineCacheMergeBatchesEmpty) {
  Cache cache;
  Cache to_deallocate;
  cache.MergeBatches(&to_deallocate);

  ASSERT_EQ(to_deallocate.Size(), 0UL);
  ASSERT_EQ(to_deallocate.DequeueBatch(), nullptr);
}

TEST(SanitizerCommon, QuarantineCacheMergeBatchesOneBatch) {
  Cache cache;
  cache.Enqueue(cb, kFakePtr, kBlockSize);
  ASSERT_EQ(kBlockSize + sizeof(QuarantineBatch), cache.Size());

  Cache to_deallocate;
  cache.MergeBatches(&to_deallocate);

  // Nothing to merge, nothing to deallocate.
  ASSERT_EQ(kBlockSize + sizeof(QuarantineBatch), cache.Size());

  ASSERT_EQ(to_deallocate.Size(), 0UL);
  ASSERT_EQ(to_deallocate.DequeueBatch(), nullptr);

  DeallocateCache(&cache);
}

TEST(SanitizerCommon, QuarantineCacheMergeBatchesSmallBatches) {
  // Make a cache with two batches small enough to merge.
  Cache from;
  from.Enqueue(cb, kFakePtr, kBlockSize);
  Cache cache;
  cache.Enqueue(cb, kFakePtr, kBlockSize);

  cache.Transfer(&from);
  ASSERT_EQ(kBlockSize * 2 + sizeof(QuarantineBatch) * 2, cache.Size());

  Cache to_deallocate;
  cache.MergeBatches(&to_deallocate);

  // Batches merged, one batch to deallocate.
  ASSERT_EQ(kBlockSize * 2 + sizeof(QuarantineBatch), cache.Size());
  ASSERT_EQ(to_deallocate.Size(), sizeof(QuarantineBatch));

  DeallocateCache(&cache);
  DeallocateCache(&to_deallocate);
}

TEST(SanitizerCommon, QuarantineCacheMergeBatchesTooBigToMerge) {
  const uptr kNumBlocks = QuarantineBatch::kSize - 1;

  // Make a cache with two batches small enough to merge.
  Cache from;
  Cache cache;
  for (uptr i = 0; i < kNumBlocks; ++i) {
    from.Enqueue(cb, kFakePtr, kBlockSize);
    cache.Enqueue(cb, kFakePtr, kBlockSize);
  }
  cache.Transfer(&from);
  ASSERT_EQ(kBlockSize * kNumBlocks * 2 +
            sizeof(QuarantineBatch) * 2, cache.Size());

  Cache to_deallocate;
  cache.MergeBatches(&to_deallocate);

  // Batches cannot be merged.
  ASSERT_EQ(kBlockSize * kNumBlocks * 2 +
            sizeof(QuarantineBatch) * 2, cache.Size());
  ASSERT_EQ(to_deallocate.Size(), 0UL);

  DeallocateCache(&cache);
}

TEST(SanitizerCommon, QuarantineCacheMergeBatchesALotOfBatches) {
  const uptr kNumBatchesAfterMerge = 3;
  const uptr kNumBlocks = QuarantineBatch::kSize * kNumBatchesAfterMerge;
  const uptr kNumBatchesBeforeMerge = kNumBlocks;

  // Make a cache with many small batches.
  Cache cache;
  for (uptr i = 0; i < kNumBlocks; ++i) {
    Cache from;
    from.Enqueue(cb, kFakePtr, kBlockSize);
    cache.Transfer(&from);
  }

  ASSERT_EQ(kBlockSize * kNumBlocks +
            sizeof(QuarantineBatch) * kNumBatchesBeforeMerge, cache.Size());

  Cache to_deallocate;
  cache.MergeBatches(&to_deallocate);

  // All blocks should fit into 3 batches.
  ASSERT_EQ(kBlockSize * kNumBlocks +
            sizeof(QuarantineBatch) * kNumBatchesAfterMerge, cache.Size());

  ASSERT_EQ(to_deallocate.Size(),
            sizeof(QuarantineBatch) *
                (kNumBatchesBeforeMerge - kNumBatchesAfterMerge));

  DeallocateCache(&cache);
  DeallocateCache(&to_deallocate);
}

}  // namespace __sanitizer
