/*
 * Copyright (c) 2018-2022 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 */

#include "TestHarness.h"

#include "Verifier.h"
#include <atomic>
#include "bmalloc_heap_config.h"
#include "hotbit_heap_config.h"
#include "iso_heap_config.h"
#include "iso_test_heap_config.h"
#include "jit_heap.h"
#include "jit_heap_config.h"
#include "minalign32_heap_config.h"
#include "pagesize64k_heap_config.h"
#include "pas_epoch.h"
#include "pas_fd_stream.h"
#include "pas_heap.h"
#include "pas_segregated_page.h"
#include "pas_segregated_page_config.h"
#include "pas_segregated_size_directory.h"
#include "pas_status_reporter.h"
#include "pas_utility_heap.h"
#include "pas_utils.h"
#include "thingy_heap_config.h"
#include <cstdlib>
#include <optional>
#include <sstream>
#include <sys/wait.h>
#include <vector>
#include <unistd.h>

using namespace std;

void* allocationConfigAllocate(size_t size, const char* name, pas_allocation_kind allocationKind, void* arg)
{
    PAS_ASSERT(allocationKind == pas_object_allocation);
    PAS_ASSERT(!arg);
    return malloc(size);
}

void allocationConfigDeallocate(void* ptr, size_t size, pas_allocation_kind allocationKind, void* arg)
{
    PAS_ASSERT(allocationKind == pas_object_allocation);
    PAS_ASSERT(!arg);
    free(ptr);
}

const pas_allocation_config allocationConfig = {
    .allocate = allocationConfigAllocate,
    .deallocate = allocationConfigDeallocate,
    .arg = nullptr
};

struct TestScopeImpl {
    TestScopeImpl(const string& name,
                  function<void()> setUp,
                  TestScopeImpl* next)
        : name(name)
        , setUp(setUp)
        , next(next)
    {
    }
    
    string name;
    function<void()> setUp;
    TestScopeImpl* next;
};

#define FOR_EACH_RUNTIME_CONFIG(name, callback) ({ \
        (callback)(name ## _intrinsic_runtime_config.base); \
        (callback)(name ## _primitive_runtime_config.base); \
        (callback)(name ## _typed_runtime_config.base); \
        (callback)(name ## _flex_runtime_config.base); \
    })

RuntimeConfigTestScope::RuntimeConfigTestScope(
    const string& name,
    function<void(pas_heap_runtime_config&)> setUp)
    : TestScope(
        name,
        [=] () {
            FOR_EACH_RUNTIME_CONFIG(thingy, setUp);
            FOR_EACH_RUNTIME_CONFIG(iso, setUp);
            FOR_EACH_RUNTIME_CONFIG(iso_test, setUp);
            FOR_EACH_RUNTIME_CONFIG(minalign32, setUp);
            FOR_EACH_RUNTIME_CONFIG(pagesize64k, setUp);
            FOR_EACH_RUNTIME_CONFIG(bmalloc, setUp);
            FOR_EACH_RUNTIME_CONFIG(hotbit, setUp);
            setUp(pas_utility_heap_runtime_config);
            setUp(jit_heap_runtime_config);
        })
{
}

ForceExclusives::ForceExclusives()
    : RuntimeConfigTestScope(
        "force-exclusives",
        [] (pas_heap_runtime_config& runtimeConfig) {
            runtimeConfig.directory_size_bound_for_partial_views = 0;
        })
{
}

ForceTLAs::ForceTLAs()
    : RuntimeConfigTestScope(
        "force-tlas",
        [] (pas_heap_runtime_config& runtimeConfig) {
            runtimeConfig.directory_size_bound_for_baseline_allocators = 0;
        })
{
}

ForceBitfit::ForceBitfit()
    : RuntimeConfigTestScope(
        "force-bitfit",
        [] (pas_heap_runtime_config& runtimeConfig) {
            if (&runtimeConfig != &pas_utility_heap_runtime_config) {
                runtimeConfig.max_segregated_object_size = 0;
                runtimeConfig.max_bitfit_object_size = UINT_MAX;
            }
        })
{
}

DisableBitfit::DisableBitfit()
    : RuntimeConfigTestScope(
        "disable-bitfit",
        [] (pas_heap_runtime_config& runtimeConfig) {
            runtimeConfig.max_bitfit_object_size = 0;
        })
{
}

ForcePartials::ForcePartials()
    : RuntimeConfigTestScope(
        "force-partials",
        [] (pas_heap_runtime_config& runtimeConfig) {
            if (&runtimeConfig == &pas_utility_heap_runtime_config)
                return;
            runtimeConfig.directory_size_bound_for_partial_views = UINT8_MAX;
        })
{
}

ForceBaselines::ForceBaselines()
    : RuntimeConfigTestScope(
        "force-baselines",
        [] (pas_heap_runtime_config& runtimeConfig) {
            runtimeConfig.directory_size_bound_for_baseline_allocators = UINT_MAX;
        })
{
}

pas_scavenger_synchronous_operation_kind scavengeKind;

RunScavengerFully::RunScavengerFully()
    : TestScope(
        "run-scavenger-fully",
        [] () { scavengeKind = pas_scavenger_run_synchronously_now_kind; })
{
}

RunScavengerOnNonRemoteCaches::RunScavengerOnNonRemoteCaches()
    : TestScope(
        "run-scavenger-on-non-remote-caches",
        [] () { scavengeKind = pas_scavenger_clear_all_caches_except_remote_tlcs_kind; })
{
}

SuspendScavengerScope::SuspendScavengerScope()
    : TestScope(
        "suspend-scavenger",
        [] () { pas_scavenger_suspend(); })
{
}

VerifyGranules::VerifyGranules()
    : TestScope(
        "verify-granules",
        [] () {
            pas_segregated_page_deallocate_should_verify_granules = true;
        })
{
}

InstallVerifier::InstallVerifier()
    : TestScope(
        "install-verifier",
        [] () {
            pas_install_verifier();
        })
{
}

EpochIsCounter::EpochIsCounter()
    : TestScope(
        "epoch-is-counter",
        [] () {
            pas_epoch_is_counter = true;
            pas_scavenger_max_epoch_delta = 0;
        })
{
}

BootJITHeap::BootJITHeap()
    : TestScope(
        "boot-jit-heap",
        [] () {
#if PAS_ENABLE_JIT
            constexpr unsigned numRegions = 50;

            for (unsigned i = numRegions; i--;) {
                size_t pageSize = pas_page_malloc_alignment();
                size_t size = pas_round_up_to_power_of_2(5000000, pageSize);
                void* base = aligned_alloc(pageSize, size);
                jit_heap_add_fresh_memory(
                    pas_range_create(reinterpret_cast<uintptr_t>(base),
                                     reinterpret_cast<uintptr_t>(base) + size));
            }
#endif // PAS_ENABLE_JIT
        })
{
}

EnablePageBalancing::EnablePageBalancing()
    : TestScope(
        "enable-page-balancing",
        [] () {
            pas_physical_page_sharing_pool_balancing_enabled = true;
        })
{
}

DisablePageBalancing::DisablePageBalancing()
    : TestScope(
        "disable-page-balancing",
        [] () {
            pas_physical_page_sharing_pool_balancing_enabled = false;
        })
{
}

DecommitZeroFill::DecommitZeroFill()
    : TestScope(
        "decommit-zero-fill",
        [] () {
#if PAS_OS(DARWIN)
            pas_page_malloc_decommit_zero_fill = true;
#endif // PAS_OS(DARWIN)
        })
{
}

namespace {

template<typename Func>
void iterateForward(TestScopeImpl* scope, const Func& func)
{
    if (!scope)
        return;
    iterateForward(scope->next, func);
    func(scope);
}

string currentSuite;
bool runningOneTest;

struct Test {
    Test() = default;
    
    Test(size_t index,
         const string& file, int line,
         const string& suite, const string& name,
         function<void()> payload,
         TestScopeImpl* scope)
        : index(index)
        , file(file)
        , line(line)
        , suite(suite)
        , name(name)
        , payload(payload)
        , scope(scope)
    {
    }
    
    string fullName() const
    {
        ostringstream out;
        out << "(" << index << "):" << file << ":" << line << "/" << suite;
        iterateForward(
            scope,
            [&] (TestScopeImpl* scope) {
                out << "/" << scope->name;
            });
        out << "/" << name;
        return out.str();
    }
    
    void run() const
    {
        iterateForward(
            scope,
            [&] (TestScopeImpl* scope) {
                scope->setUp();
            });
        payload();
    }
    
    size_t index;
    string file;
    int line { 0 };
    string suite;
    string name;
    function<void()> payload;
    TestScopeImpl* scope;
};

struct RunningTest {
    RunningTest() = default;

    RunningTest(pid_t pid, int readPipe, const Test& test, size_t testIndex)
        : pid(pid)
        , readPipe(readPipe)
        , test(test)
        , testIndex(testIndex)
    {
    }

    pid_t pid { -1 };
    int readPipe { -1 };
    Test test;
    size_t testIndex;
};

vector<Test> tests;
TestScopeImpl* topScope;

unsigned testsPassed;
unsigned testsRan;

static constexpr char successByte = 'S';

int resultPipe[2];

} // anonymous namespace

void addBitfieldVectorTests();
void addBitfitTests();
void addBitvectorTests();
void addBmallocTests();
void addCartesianTreeTests();
void addCoalignTests();
void addEnumerationTests();
void addExpendableMemoryTests();
void addExtendedGCDTests();
void addHashtableTests();
void addHeapRefAllocatorIndexTests();
void addIsoDynamicPrimitiveHeapTests();
void addIsoHeapChaosTests();
void addIsoHeapPageSharingTests();
void addIsoHeapPartialAndBaselineTests();
void addIsoHeapReservedMemoryTests();
void addJITHeapTests();
void addLargeFreeHeapTests();
void addLargeSharingPoolTests();
void addLockFreeReadPtrPtrHashtableTests();
void addLotsOfHeapsAndThreadsTests();
void addMARTests();
void addMemalignTests();
void addMinHeapTests();
void addPGMTests();
void addRaceTests();
void addRedBlackTreeTests();
void addScavengerExternalWorkTests();
void addTLCDecommitTests();
void addTSDTests();
void addThingyAndUtilityHeapAllocationTests();
void addUtilsTests();
void addViewCacheTests();

void testSucceeded()
{
    if (runningOneTest) {
        cout << "    PASS!" << endl;
        cout << endl;
        cout << "Exiting early due to test success." << endl;
        cout << endl;
        cout << "THE ONE TEST PASSED!" << endl;
        exit(0);
    }

    ssize_t writeResult = write(resultPipe[1], &successByte, 1);
    PAS_ASSERT(writeResult == 1);
    exit(0);
    PAS_ASSERT(!"Should have exited");
}

unsigned deterministicRandomNumber(unsigned exclusiveUpperBound)
{
    // This code is adapted from the JetStream benchmark suite.

    // Copyright 2013 the V8 project authors. All rights reserved.
    // Copyright (C) 2015 Apple Inc. All rights reserved.
    // Redistribution and use in source and binary forms, with or without
    // modification, are permitted provided that the following conditions are
    // met:
    //
    //     * Redistributions of source code must retain the above copyright
    //       notice, this list of conditions and the following disclaimer.
    //     * Redistributions in binary form must reproduce the above
    //       copyright notice, this list of conditions and the following
    //       disclaimer in the documentation and/or other materials provided
    //       with the distribution.
    //     * Neither the name of Google Inc. nor the names of its
    //       contributors may be used to endorse or promote products derived
    //       from this software without specific prior written permission.
    //
    // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
    // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
    // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
    // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
    // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
    // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
    // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
    // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
    // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
    // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    
    static atomic<unsigned> seed(49734321 + 19);
    
    for (;;) {
        unsigned value = seed;
        unsigned expectedValue = value;
        
        value = ((value + 0x7ed55d16) + (value << 12)) & 0xffffffff;
        value = ((value ^ 0xc761c23c) ^ (value >> 19)) & 0xffffffff;
        value = ((value + 0x165667b1) + (value << 5))  & 0xffffffff;
        value = ((value + 0xd3a2646c) ^ (value << 9))  & 0xffffffff;
        value = ((value + 0xfd7046c5) + (value << 3))  & 0xffffffff;
        value = ((value ^ 0xb55a4f09) ^ (value >> 16)) & 0xffffffff;
        
        if (seed.compare_exchange_weak(expectedValue, value))
            return value % exclusiveUpperBound;
    }
}

void addTest(const string& file, int line, const string& name, function<void()> test)
{
    CHECK(currentSuite.size());
    tests.push_back(Test(tests.size(), file, line, currentSuite, name, test, topScope));
}

TestScope::TestScope(const string& name,
                     function<void()> setUp)
    : m_impl(new TestScopeImpl(name, setUp, topScope))
{
    topScope = m_impl;
}

TestScope::~TestScope()
{
    PAS_ASSERT(m_impl == topScope);
    topScope = topScope->next;
}

bool hasScope(const string& filter)
{
    for (TestScopeImpl* scope = topScope; scope; scope = scope->next) {
        if (scope->name.find(filter) != string::npos)
            return true;
    }
    return false;
}

void dumpObjectSet(const set<void*>& objects)
{
    cout << "Object set:";
    for (void* object : objects) {
        cout << " " << object;
    }
    cout << endl;
}

void dumpObjectsInHeap(pas_heap* heap)
{
    cout << "Objects:";
    forEachLiveObject(
        heap,
        [&] (void* object, size_t size) -> bool {
            cout << " " << object;
            return true;
        });
    cout << "\n";
}

void dumpObjectsInHeaps(const vector<pas_heap*>& heaps)
{
    cout << "Objects in heaps:\n";
    for (pas_heap* heap : heaps) {
        cout << "In heap " << heap << "\n";
        dumpObjectsInHeap(heap);
    }
}

namespace {
bool forEachLiveObjectAdapter(pas_heap* heap,
                              uintptr_t begin,
                              size_t size,
                              void* arg)
{
    function<bool(void*, size_t)>* callback = reinterpret_cast<function<bool(void*, size_t)>*>(arg);
    return (*callback)(reinterpret_cast<void*>(begin), size);
}
}

void forEachLiveObject(pas_heap* heap, function<bool(void*, size_t)> callback)
{
    pas_heap_for_each_live_object(heap, forEachLiveObjectAdapter, &callback,
                                  pas_lock_is_not_held);
}

void verifyMinimumObjectDistance(const set<void*>& objects,
                                 size_t minimumDistance)
{
    uintptr_t lastBegin = 0;
    for (void* object : objects) {
        uintptr_t begin = reinterpret_cast<uintptr_t>(object);
        
        CHECK_GREATER_EQUAL(begin, lastBegin);
        
        if (begin < lastBegin + minimumDistance) {
            cout << "Object " << reinterpret_cast<void*>(lastBegin) << " is too close to " << object << endl;
            dumpObjectSet(objects);
        }
        
        CHECK_GREATER_EQUAL(begin - lastBegin, minimumDistance);
        
        lastBegin = begin;
    }
}

void verifyExactObjectDistance(const set<void*>& objects,
                               size_t exactDistance)
{
    uintptr_t lastBegin = 0;
    bool hasLast = false;
    for (void* object : objects) {
        uintptr_t begin = reinterpret_cast<uintptr_t>(object);

        CHECK_GREATER_EQUAL(begin, lastBegin);

        if (hasLast) {
            if (begin != lastBegin + exactDistance) {
                cout << "Object " << reinterpret_cast<void*>(lastBegin) << " is wrong distance to " << object << endl;
                dumpObjectSet(objects);
            }

            CHECK_EQUAL(begin - lastBegin, exactDistance);
        }

        lastBegin = begin;
        hasLast = true;
    }
}

struct ParsedArguments {
    string filter;
    std::optional<int> childProcesses { };
};

unsigned computeTestConcurrency(std::optional<int> childProcesses);

void printUsage(const char* programName)
{
    cerr << "Usage: " << programName << " [options] [filter]" << endl;
    cerr << endl;
    cerr << "Options:" << endl;
    cerr << "  --child-processes N  Maximum number of child processes to fork at once" << endl;
    cerr << "                       (default: " << computeTestConcurrency(std::nullopt) << ", auto-detected based on CPU count)" << endl;
    cerr << "  --help               Show this help message" << endl;
    cerr << endl;
    cerr << "Arguments:" << endl;
    cerr << "  filter               Run only tests matching this filter string" << endl;
    cerr << endl;
}

ParsedArguments parseArguments(int argc, char** argv)
{
    ParsedArguments args;

    for (int i = 1; i < argc; i++) {
        string arg = argv[i];

        if (arg == "--help" || arg == "-h") {
            printUsage(argv[0]);
            exit(0);
        } else if (arg == "--child-processes") {
            if (i + 1 >= argc) {
                cerr << "Error: --child-processes requires a numeric argument" << endl;
                printUsage(argv[0]);
                exit(1);
            }
            i++;
            int value = atoi(argv[i]);
            if (value <= 0) {
                cerr << "Error: --child-processes must be a positive integer, got: " << argv[i] << endl;
                exit(1);
            }
            args.childProcesses = value;
        } else if (arg[0] == '-') {
            cerr << "Error: unknown option: " << arg << endl;
            printUsage(argv[0]);
            exit(1);
        } else {
            // Positional argument - this is the filter
            if (!args.filter.empty()) {
                cerr << "Error: multiple filter arguments specified" << endl;
                printUsage(argv[0]);
                exit(1);
            }
            args.filter = arg;
        }
    }

    return args;
}

unsigned computeTestConcurrency(std::optional<int> childProcesses)
{
    const char* envConcurrency = getenv("PasTestConcurrency");
    if (envConcurrency) {
        int concurrency = atoi(envConcurrency);
        if (concurrency > 0)
            return static_cast<unsigned>(concurrency);
    }

    if (childProcesses.value_or(0) > 0)
        return static_cast<unsigned>(*childProcesses);

    unsigned numCpus = 1;

    long result = sysconf(_SC_NPROCESSORS_ONLN);
    if (result > 0)
        numCpus = static_cast<unsigned>(result);

    return numCpus > 1 ? numCpus / 2 : 1;
}


void runOneTest(const Test& test)
{
    runningOneTest = true;
    cout << "Running " << test.fullName() << "..." << endl;
    test.run();
    testSucceeded();
    CHECK(!"Should not have gotten here.");
}

// Start a test without waiting for it to finish;
// the handle returned can be used to wait for its completion,
// via waitForAnyProcess.
RunningTest startForkedTest(const Test& test, size_t testIndex)
{
    cout << "Running " << test.fullName() << "..." << endl;

    testsRan++;

    int pipefd[2];
    int pipeResult = pipe(pipefd);
    PAS_ASSERT(!pipeResult);

    int forkResult = fork();
    PAS_ASSERT(forkResult >= 0);

    if (!forkResult) {
        // Child process
        close(pipefd[0]);
        test.run();
        ssize_t writeResult = write(pipefd[1], &successByte, 1);
        PAS_ASSERT(writeResult == 1);
        exit(0);
        PAS_ASSERT(!"Should have exited");
    }

    // Parent process
    close(pipefd[1]);
    return RunningTest(forkResult, pipefd[0], test, testIndex);
}

size_t waitForAnyProcess(vector<RunningTest>& runningTests)
{
    PAS_ASSERT(!runningTests.empty());

    int waitStatus;
    pid_t completedPid = wait(&waitStatus);
    PAS_ASSERT(completedPid > 0);

    // Find which test completed
    size_t completedIndex = SIZE_MAX;
    RunningTest completedTest;
    for (size_t i = 0; i < runningTests.size(); i++) {
        if (runningTests[i].pid == completedPid) {
            completedIndex = i;
            completedTest = runningTests[i];
            break;
        }
    }

    PAS_ASSERT(completedIndex != SIZE_MAX);

    // Read result from pipe
    bool didReadAByte = false;
    char resultByte;
    for (;;) {
        ssize_t readResult = read(completedTest.readPipe, &resultByte, 1);

        if (readResult == 1) {
            didReadAByte = true;
            break;
        }

        if (!readResult)
            break;

        PAS_ASSERT(readResult == -1);
        PAS_ASSERT(errno == EINTR);
    }

    close(completedTest.readPipe);

    if (WIFEXITED(waitStatus)) {
        if (!WEXITSTATUS(waitStatus) && didReadAByte) {
            if (resultByte == successByte) {
                cout << "    PASS!" << endl;
                testsPassed++;
            } else {
                cout << "     FAIL: unexpected result byte: " << static_cast<int>(resultByte) << endl;
            }
        } else {
            cout << "    FAIL: unexpected exit with code " << WEXITSTATUS(waitStatus) << endl;
        }
    } else if (WIFSIGNALED(waitStatus)) {
        cout << "    CRASH: with signal " << WTERMSIG(waitStatus) << endl;
    } else
        cout << "    FAIL: child process terminated with unknown status code " << waitStatus << endl;

    // Remove completed test from running list
    runningTests.erase(runningTests.begin() + completedIndex);

    return completedTest.testIndex;
}

void runTests(const vector<Test>& tests, std::optional<int> childProcesses)
{
    CHECK(tests.size());

    if (tests.size() == 1) {
        runOneTest(tests[0]);
        return;
    }

    vector<RunningTest> runningTests;
    size_t nextTestIndex = 0;
    unsigned maxConcurrentTests = computeTestConcurrency(childProcesses);

    // Start initial batch of tests (up to maxConcurrentTests)
    while (nextTestIndex < tests.size() && runningTests.size() < maxConcurrentTests) {
        RunningTest runningTest = startForkedTest(tests[nextTestIndex], nextTestIndex);
        runningTests.push_back(runningTest);
        nextTestIndex++;
    }

    while (!runningTests.empty()) {
        // wait(2) will return exactly once per exited child
        waitForAnyProcess(runningTests);
        if (nextTestIndex < tests.size()) {
            RunningTest runningTest = startForkedTest(tests[nextTestIndex], nextTestIndex);
            runningTests.push_back(runningTest);
            nextTestIndex++;
        }
    }

    PAS_ASSERT(testsPassed <= testsRan);
    cout << endl;
    cout << testsPassed << "/" << testsRan << " TESTS PASSED!" << endl;
    if (testsPassed == testsRan) {
        cout << endl;
        cout << "ALL TESTS PASSED!" << endl;
        exit(0);
    }

    cout << endl;
    cout << (testsRan - testsPassed) << "/" << testsRan << " TESTS FAILED!" << endl;
    cout << endl;
    if (!testsPassed)
        cout << "ALL TESTS FAILED!" << endl;
    else
        cout << "SOME TESTS FAILED!" << endl;
    exit(1);
}

static void runFilteredTests(const string& filter, std::optional<int> childProcesses)
{
    vector<Test> filteredTests;

    for (const Test& test : tests) {
        if (test.fullName().find(filter) != string::npos)
            filteredTests.push_back(test);
    }

    if (filteredTests.empty()) {
        cerr << "Error: Did not find any tests to run for filter " << filter << endl;
        exit(1);
    }

    runTests(filteredTests, childProcesses);
}

#define ADD_SUITE(name) do { \
        currentSuite = #name; \
        add##name##Tests(); \
        currentSuite = ""; \
    } while (false)

int main(int argc, char** argv)
{
    setvbuf(stdout, NULL, _IOLBF, 0);

#if SEGHEAP
    pas_segregated_page_config_do_validate = true;
#endif

    // Run the Thingy tests first because they catch the most bugs.
    ADD_SUITE(ThingyAndUtilityHeapAllocation);

    // Run the rest of the tests in alphabetical order.
    ADD_SUITE(BitfieldVector);
    ADD_SUITE(Bitfit);
    ADD_SUITE(Bitvector);
    ADD_SUITE(Bmalloc);
    ADD_SUITE(CartesianTree);
    ADD_SUITE(Coalign);
    ADD_SUITE(Enumeration);
    ADD_SUITE(ExpendableMemory);
    ADD_SUITE(ExtendedGCD);
    ADD_SUITE(Hashtable);
    ADD_SUITE(HeapRefAllocatorIndex);
    ADD_SUITE(IsoDynamicPrimitiveHeap);
    ADD_SUITE(IsoHeapChaos);
    ADD_SUITE(IsoHeapPageSharing);
    ADD_SUITE(IsoHeapPartialAndBaseline);
    ADD_SUITE(IsoHeapReservedMemory);
    ADD_SUITE(JITHeap);
    ADD_SUITE(LargeFreeHeap);
    ADD_SUITE(LargeSharingPool);
    ADD_SUITE(LockFreeReadPtrPtrHashtable);
    ADD_SUITE(LotsOfHeapsAndThreads);
    ADD_SUITE(MAR);
    ADD_SUITE(Memalign);
    ADD_SUITE(MinHeap);
    ADD_SUITE(PGM);
    ADD_SUITE(Race);
    ADD_SUITE(RedBlackTree);
    ADD_SUITE(TLCDecommit);
    ADD_SUITE(TSD);
    ADD_SUITE(Utils);
    ADD_SUITE(ViewCache);

    ParsedArguments args = parseArguments(argc, argv);

    runFilteredTests(args.filter, args.childProcesses);

    CHECK(!"Should not have reached here.");
    return 1;
}

void scavenge()
{
    pas_scavenger_perform_synchronous_operation(scavengeKind);
}

void printStatusReport()
{
    pas_fd_stream stdout_stream;
    pas_fd_stream_construct(&stdout_stream, 1);
    pas_heap_lock_lock();
    unsigned old_enabled = pas_status_reporter_enabled;
    pas_status_reporter_enabled = 100;
    pas_status_reporter_dump_everything(&stdout_stream.base);
    pas_status_reporter_enabled = old_enabled;
    pas_heap_lock_unlock();
}

