/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "mozilla/Attributes.h"

#include "gc/GCLock.h"
#include "jsapi-tests/tests.h"

#include "gc/ArenaList-inl.h"
#include "gc/Heap-inl.h"

using namespace js;
using namespace js::gc;

class TestArenaChunk : public ArenaChunk {
 public:
  static TestArenaChunk* init(JSContext* cx, void* ptr) {
    GCRuntime* gc = &cx->runtime()->gc;
    auto* const arenaChunk =
        static_cast<TestArenaChunk*>(ArenaChunk::init(ptr, gc, true));
    arenaChunk->initAsCommitted();
    arenaChunk->info.zone = cx->zone();
    return arenaChunk;
  }
};

// Automatically allocate and free an Arena for testing purposes.
class MOZ_RAII AutoTestArena {
  TestArenaChunk* arenaChunk = nullptr;
  Arena* arena = nullptr;

 public:
  explicit AutoTestArena(JSContext* cx, AllocKind kind, size_t nfree) {
    // For testing purposes only. Don't do this in real code!
    GCRuntime* gc = &cx->runtime()->gc;
    void* const arenaChunkPtr = TestArenaChunk::allocate(gc, StallAndRetry::No);
    MOZ_RELEASE_ASSERT(arenaChunkPtr);
    arenaChunk = TestArenaChunk::init(cx, arenaChunkPtr);

    arena = arenaChunk->fetchNextFreeArena(gc);
    MOZ_RELEASE_ASSERT(arena);

    arena->init(gc, kind);

    size_t nallocs = Arena::thingsPerArena(kind) - nfree;
    size_t thingSize = Arena::thingSize(kind);
    for (size_t i = 0; i < nallocs; i++) {
      MOZ_RELEASE_ASSERT(arena->getFirstFreeSpan()->allocate(thingSize));
    }
    MOZ_RELEASE_ASSERT(arena->countFreeCells() == nfree);
  }

  ~AutoTestArena() { UnmapPages(arenaChunk, ChunkSize); }

  Arena* get() { return arena; }
  operator Arena*() { return arena; }
  Arena* operator->() { return arena; }
};

BEGIN_TEST(testSortedArenaList) {
  const AllocKind kind = AllocKind::OBJECT0;

  // Test empty list.

  SortedArenaList sortedList(kind);
  CHECK(sortedList.thingsPerArena() == Arena::thingsPerArena(kind));

  CHECK(ConvertToArenaList(kind, sortedList, 0));

  // Test with single non-empty arena.

  size_t thingsPerArena = Arena::thingsPerArena(kind);
  for (size_t i = 0; i != thingsPerArena; i++) {
    AutoTestArena arena(cx, kind, i);
    sortedList.insertAt(arena.get(), i);

    CHECK(ConvertToArenaList(kind, sortedList, 1, nullptr, arena.get()));
  }

  // Test with single empty arena.

  AutoTestArena arena(cx, kind, thingsPerArena);
  sortedList.insertAt(arena.get(), thingsPerArena);

  CHECK(ConvertToArenaList(kind, sortedList, 0, arena.get()));

  // Test with full and non-full arenas.

  AutoTestArena fullArena(cx, kind, 0);
  AutoTestArena nonFullArena(cx, kind, 1);
  sortedList.insertAt(fullArena.get(), 0);
  sortedList.insertAt(nonFullArena.get(), 1);

  CHECK(ConvertToArenaList(kind, sortedList, 2, nullptr, nonFullArena.get(),
                           fullArena.get()));

  return true;
}

bool ConvertToArenaList(AllocKind kind, SortedArenaList& sortedList,
                        size_t expectedBucketCount,
                        Arena* expectedEmpty = nullptr,
                        Arena* expectedFirst = nullptr,
                        Arena* expectedLast = nullptr) {
  CHECK(ConvertToArenaListOnce(sortedList, expectedBucketCount, expectedEmpty,
                               expectedFirst, expectedLast));

  // Check again to test restoreFromArenaList restored the original state
  // (except for the empty arenas which are not restored).
  CHECK(ConvertToArenaListOnce(sortedList, expectedBucketCount, nullptr,
                               expectedFirst, expectedLast));

  // Clear the list on exit.
  new (&sortedList) SortedArenaList(kind);

  return true;
}

bool ConvertToArenaListOnce(SortedArenaList& sortedList,
                            size_t expectedBucketCount, Arena* expectedEmpty,
                            Arena* expectedFirst, Arena* expectedLast) {
  Arena* emptyArenas = nullptr;
  sortedList.extractEmptyTo(&emptyArenas);
  CHECK(emptyArenas == expectedEmpty);

  Arena* bucketLast[SortedArenaList::BucketCount];
  ArenaList list = sortedList.convertToArenaList(bucketLast);
  CHECK(list.isEmpty() == (expectedBucketCount == 0));
  if (expectedFirst) {
    CHECK(list.getFirst() == expectedFirst);
  }
  if (expectedLast) {
    CHECK(list.getLast() == expectedLast);
  }

  size_t count = 0;
  for (size_t j = 0; j < SortedArenaList::BucketCount; j++) {
    if (bucketLast[j]) {
      count++;
    }
  }
  CHECK(count == expectedBucketCount);

  sortedList.restoreFromArenaList(list, bucketLast);
  CHECK(list.isEmpty());

  return true;
}
END_TEST(testSortedArenaList)
