#include "test/jemalloc_test.h"

#include "jemalloc/internal/psset.h"

#define PAGESLAB_ADDR ((void *)(1234 * HUGEPAGE))
#define PAGESLAB_AGE 5678

#define ALLOC_ARENA_IND 111
#define ALLOC_ESN 222

static void
edata_init_test(edata_t *edata) {
       memset(edata, 0, sizeof(*edata));
       edata_arena_ind_set(edata, ALLOC_ARENA_IND);
       edata_esn_set(edata, ALLOC_ESN);
}

static void
test_psset_fake_purge(hpdata_t *ps) {
       hpdata_purge_state_t purge_state;
       hpdata_alloc_allowed_set(ps, false);
       hpdata_purge_begin(ps, &purge_state);
       void *addr;
       size_t size;
       while (hpdata_purge_next(ps, &purge_state, &addr, &size)) {
       }
       hpdata_purge_end(ps, &purge_state);
       hpdata_alloc_allowed_set(ps, true);
}

static void
test_psset_alloc_new(psset_t *psset, hpdata_t *ps, edata_t *r_edata,
   size_t size) {
       hpdata_assert_empty(ps);

       test_psset_fake_purge(ps);

       psset_insert(psset, ps);
       psset_update_begin(psset, ps);

       void *addr = hpdata_reserve_alloc(ps, size);
       edata_init(r_edata, edata_arena_ind_get(r_edata), addr, size,
           /* slab */ false, SC_NSIZES, /* sn */ 0, extent_state_active,
           /* zeroed */ false, /* committed */ true, EXTENT_PAI_HPA,
           EXTENT_NOT_HEAD);
       edata_ps_set(r_edata, ps);
       psset_update_end(psset, ps);
}

static bool
test_psset_alloc_reuse(psset_t *psset, edata_t *r_edata, size_t size) {
       hpdata_t *ps = psset_pick_alloc(psset, size);
       if (ps == NULL) {
               return true;
       }
       psset_update_begin(psset, ps);
       void *addr = hpdata_reserve_alloc(ps, size);
       edata_init(r_edata, edata_arena_ind_get(r_edata), addr, size,
           /* slab */ false, SC_NSIZES, /* sn */ 0, extent_state_active,
           /* zeroed */ false, /* committed */ true, EXTENT_PAI_HPA,
           EXTENT_NOT_HEAD);
       edata_ps_set(r_edata, ps);
       psset_update_end(psset, ps);
       return false;
}

static hpdata_t *
test_psset_dalloc(psset_t *psset, edata_t *edata) {
       hpdata_t *ps = edata_ps_get(edata);
       psset_update_begin(psset, ps);
       hpdata_unreserve(ps, edata_addr_get(edata), edata_size_get(edata));
       psset_update_end(psset, ps);
       if (hpdata_empty(ps)) {
               psset_remove(psset, ps);
               return ps;
       } else {
               return NULL;
       }
}

static void
edata_expect(edata_t *edata, size_t page_offset, size_t page_cnt) {
       /*
        * Note that allocations should get the arena ind of their home
        * arena, *not* the arena ind of the pageslab allocator.
        */
       expect_u_eq(ALLOC_ARENA_IND, edata_arena_ind_get(edata),
           "Arena ind changed");
       expect_ptr_eq(
           (void *)((uintptr_t)PAGESLAB_ADDR + (page_offset << LG_PAGE)),
           edata_addr_get(edata), "Didn't allocate in order");
       expect_zu_eq(page_cnt << LG_PAGE, edata_size_get(edata), "");
       expect_false(edata_slab_get(edata), "");
       expect_u_eq(SC_NSIZES, edata_szind_get_maybe_invalid(edata),
           "");
       expect_u64_eq(0, edata_sn_get(edata), "");
       expect_d_eq(edata_state_get(edata), extent_state_active, "");
       expect_false(edata_zeroed_get(edata), "");
       expect_true(edata_committed_get(edata), "");
       expect_d_eq(EXTENT_PAI_HPA, edata_pai_get(edata), "");
       expect_false(edata_is_head_get(edata), "");
}

TEST_BEGIN(test_empty) {
       bool err;
       hpdata_t pageslab;
       hpdata_init(&pageslab, PAGESLAB_ADDR, PAGESLAB_AGE);

       edata_t alloc;
       edata_init_test(&alloc);

       psset_t psset;
       psset_init(&psset);

       /* Empty psset should return fail allocations. */
       err = test_psset_alloc_reuse(&psset, &alloc, PAGE);
       expect_true(err, "Empty psset succeeded in an allocation.");
}
TEST_END

TEST_BEGIN(test_fill) {
       bool err;

       hpdata_t pageslab;
       hpdata_init(&pageslab, PAGESLAB_ADDR, PAGESLAB_AGE);

       edata_t alloc[HUGEPAGE_PAGES];

       psset_t psset;
       psset_init(&psset);

       edata_init_test(&alloc[0]);
       test_psset_alloc_new(&psset, &pageslab, &alloc[0], PAGE);
       for (size_t i = 1; i < HUGEPAGE_PAGES; i++) {
               edata_init_test(&alloc[i]);
               err = test_psset_alloc_reuse(&psset, &alloc[i], PAGE);
               expect_false(err, "Nonempty psset failed page allocation.");
       }

       for (size_t i = 0; i < HUGEPAGE_PAGES; i++) {
               edata_t *edata = &alloc[i];
               edata_expect(edata, i, 1);
       }

       /* The pageslab, and thus psset, should now have no allocations. */
       edata_t extra_alloc;
       edata_init_test(&extra_alloc);
       err = test_psset_alloc_reuse(&psset, &extra_alloc, PAGE);
       expect_true(err, "Alloc succeeded even though psset should be empty");
}
TEST_END

TEST_BEGIN(test_reuse) {
       bool err;
       hpdata_t *ps;

       hpdata_t pageslab;
       hpdata_init(&pageslab, PAGESLAB_ADDR, PAGESLAB_AGE);

       edata_t alloc[HUGEPAGE_PAGES];

       psset_t psset;
       psset_init(&psset);

       edata_init_test(&alloc[0]);
       test_psset_alloc_new(&psset, &pageslab, &alloc[0], PAGE);
       for (size_t i = 1; i < HUGEPAGE_PAGES; i++) {
               edata_init_test(&alloc[i]);
               err = test_psset_alloc_reuse(&psset, &alloc[i], PAGE);
               expect_false(err, "Nonempty psset failed page allocation.");
       }

       /* Free odd indices. */
       for (size_t i = 0; i < HUGEPAGE_PAGES; i ++) {
               if (i % 2 == 0) {
                       continue;
               }
               ps = test_psset_dalloc(&psset, &alloc[i]);
               expect_ptr_null(ps, "Nonempty pageslab evicted");
       }
       /* Realloc into them. */
       for (size_t i = 0; i < HUGEPAGE_PAGES; i++) {
               if (i % 2 == 0) {
                       continue;
               }
               err = test_psset_alloc_reuse(&psset, &alloc[i], PAGE);
               expect_false(err, "Nonempty psset failed page allocation.");
               edata_expect(&alloc[i], i, 1);
       }
       /* Now, free the pages at indices 0 or 1 mod 2. */
       for (size_t i = 0; i < HUGEPAGE_PAGES; i++) {
               if (i % 4 > 1) {
                       continue;
               }
               ps = test_psset_dalloc(&psset, &alloc[i]);
               expect_ptr_null(ps, "Nonempty pageslab evicted");
       }
       /* And realloc 2-page allocations into them. */
       for (size_t i = 0; i < HUGEPAGE_PAGES; i++) {
               if (i % 4 != 0) {
                       continue;
               }
               err = test_psset_alloc_reuse(&psset, &alloc[i], 2 * PAGE);
               expect_false(err, "Nonempty psset failed page allocation.");
               edata_expect(&alloc[i], i, 2);
       }
       /* Free all the 2-page allocations. */
       for (size_t i = 0; i < HUGEPAGE_PAGES; i++) {
               if (i % 4 != 0) {
                       continue;
               }
               ps = test_psset_dalloc(&psset, &alloc[i]);
               expect_ptr_null(ps, "Nonempty pageslab evicted");
       }
       /*
        * Free up a 1-page hole next to a 2-page hole, but somewhere in the
        * middle of the pageslab.  Index 11 should be right before such a hole
        * (since 12 % 4 == 0).
        */
       size_t index_of_3 = 11;
       ps = test_psset_dalloc(&psset, &alloc[index_of_3]);
       expect_ptr_null(ps, "Nonempty pageslab evicted");
       err = test_psset_alloc_reuse(&psset, &alloc[index_of_3], 3 * PAGE);
       expect_false(err, "Should have been able to find alloc.");
       edata_expect(&alloc[index_of_3], index_of_3, 3);

       /*
        * Free up a 4-page hole at the end.  Recall that the pages at offsets 0
        * and 1 mod 4 were freed above, so we just have to free the last
        * allocations.
        */
       ps = test_psset_dalloc(&psset, &alloc[HUGEPAGE_PAGES - 1]);
       expect_ptr_null(ps, "Nonempty pageslab evicted");
       ps = test_psset_dalloc(&psset, &alloc[HUGEPAGE_PAGES - 2]);
       expect_ptr_null(ps, "Nonempty pageslab evicted");

       /* Make sure we can satisfy an allocation at the very end of a slab. */
       size_t index_of_4 = HUGEPAGE_PAGES - 4;
       err = test_psset_alloc_reuse(&psset, &alloc[index_of_4], 4 * PAGE);
       expect_false(err, "Should have been able to find alloc.");
       edata_expect(&alloc[index_of_4], index_of_4, 4);
}
TEST_END

TEST_BEGIN(test_evict) {
       bool err;
       hpdata_t *ps;

       hpdata_t pageslab;
       hpdata_init(&pageslab, PAGESLAB_ADDR, PAGESLAB_AGE);

       edata_t alloc[HUGEPAGE_PAGES];

       psset_t psset;
       psset_init(&psset);

       /* Alloc the whole slab. */
       edata_init_test(&alloc[0]);
       test_psset_alloc_new(&psset, &pageslab, &alloc[0], PAGE);
       for (size_t i = 1; i < HUGEPAGE_PAGES; i++) {
               edata_init_test(&alloc[i]);
               err = test_psset_alloc_reuse(&psset, &alloc[i], PAGE);
               expect_false(err, "Unxpected allocation failure");
       }

       /* Dealloc the whole slab, going forwards. */
       for (size_t i = 0; i < HUGEPAGE_PAGES - 1; i++) {
               ps = test_psset_dalloc(&psset, &alloc[i]);
               expect_ptr_null(ps, "Nonempty pageslab evicted");
       }
       ps = test_psset_dalloc(&psset, &alloc[HUGEPAGE_PAGES - 1]);
       expect_ptr_eq(&pageslab, ps, "Empty pageslab not evicted.");

       err = test_psset_alloc_reuse(&psset, &alloc[0], PAGE);
       expect_true(err, "psset should be empty.");
}
TEST_END

TEST_BEGIN(test_multi_pageslab) {
       bool err;
       hpdata_t *ps;

       hpdata_t pageslab[2];
       hpdata_init(&pageslab[0], PAGESLAB_ADDR, PAGESLAB_AGE);
       hpdata_init(&pageslab[1],
           (void *)((uintptr_t)PAGESLAB_ADDR + HUGEPAGE),
           PAGESLAB_AGE + 1);

       edata_t alloc[2][HUGEPAGE_PAGES];

       psset_t psset;
       psset_init(&psset);

       /* Insert both slabs. */
       edata_init_test(&alloc[0][0]);
       test_psset_alloc_new(&psset, &pageslab[0], &alloc[0][0], PAGE);
       edata_init_test(&alloc[1][0]);
       test_psset_alloc_new(&psset, &pageslab[1], &alloc[1][0], PAGE);

       /* Fill them both up; make sure we do so in first-fit order. */
       for (size_t i = 0; i < 2; i++) {
               for (size_t j = 1; j < HUGEPAGE_PAGES; j++) {
                       edata_init_test(&alloc[i][j]);
                       err = test_psset_alloc_reuse(&psset, &alloc[i][j], PAGE);
                       expect_false(err,
                           "Nonempty psset failed page allocation.");
                       assert_ptr_eq(&pageslab[i], edata_ps_get(&alloc[i][j]),
                           "Didn't pick pageslabs in first-fit");
               }
       }

       /*
        * Free up a 2-page hole in the earlier slab, and a 1-page one in the
        * later one.  We should still pick the later one.
        */
       ps = test_psset_dalloc(&psset, &alloc[0][0]);
       expect_ptr_null(ps, "Unexpected eviction");
       ps = test_psset_dalloc(&psset, &alloc[0][1]);
       expect_ptr_null(ps, "Unexpected eviction");
       ps = test_psset_dalloc(&psset, &alloc[1][0]);
       expect_ptr_null(ps, "Unexpected eviction");
       err = test_psset_alloc_reuse(&psset, &alloc[0][0], PAGE);
       expect_ptr_eq(&pageslab[1], edata_ps_get(&alloc[0][0]),
           "Should have picked the fuller pageslab");

       /*
        * Now both slabs have 1-page holes. Free up a second one in the later
        * slab.
        */
       ps = test_psset_dalloc(&psset, &alloc[1][1]);
       expect_ptr_null(ps, "Unexpected eviction");

       /*
        * We should be able to allocate a 2-page object, even though an earlier
        * size class is nonempty.
        */
       err = test_psset_alloc_reuse(&psset, &alloc[1][0], 2 * PAGE);
       expect_false(err, "Allocation should have succeeded");
}
TEST_END

static void
stats_expect_empty(psset_bin_stats_t *stats) {
       assert_zu_eq(0, stats->npageslabs,
           "Supposedly empty bin had positive npageslabs");
       expect_zu_eq(0, stats->nactive, "Unexpected nonempty bin"
           "Supposedly empty bin had positive nactive");
}

static void
stats_expect(psset_t *psset, size_t nactive) {
       if (nactive == HUGEPAGE_PAGES) {
               expect_zu_eq(1, psset->stats.full_slabs[0].npageslabs,
                   "Expected a full slab");
               expect_zu_eq(HUGEPAGE_PAGES,
                   psset->stats.full_slabs[0].nactive,
                   "Should have exactly filled the bin");
       } else {
               stats_expect_empty(&psset->stats.full_slabs[0]);
       }
       size_t ninactive = HUGEPAGE_PAGES - nactive;
       pszind_t nonempty_pind = PSSET_NPSIZES;
       if (ninactive != 0 && ninactive < HUGEPAGE_PAGES) {
               nonempty_pind = sz_psz2ind(sz_psz_quantize_floor(
                   ninactive << LG_PAGE));
       }
       for (pszind_t i = 0; i < PSSET_NPSIZES; i++) {
               if (i == nonempty_pind) {
                       assert_zu_eq(1,
                           psset->stats.nonfull_slabs[i][0].npageslabs,
                           "Should have found a slab");
                       expect_zu_eq(nactive,
                           psset->stats.nonfull_slabs[i][0].nactive,
                           "Mismatch in active pages");
               } else {
                       stats_expect_empty(&psset->stats.nonfull_slabs[i][0]);
               }
       }
       expect_zu_eq(nactive, psset_nactive(psset), "");
}

TEST_BEGIN(test_stats) {
       bool err;

       hpdata_t pageslab;
       hpdata_init(&pageslab, PAGESLAB_ADDR, PAGESLAB_AGE);

       edata_t alloc[HUGEPAGE_PAGES];

       psset_t psset;
       psset_init(&psset);
       stats_expect(&psset, 0);

       edata_init_test(&alloc[0]);
       test_psset_alloc_new(&psset, &pageslab, &alloc[0], PAGE);
       for (size_t i = 1; i < HUGEPAGE_PAGES; i++) {
               stats_expect(&psset, i);
               edata_init_test(&alloc[i]);
               err = test_psset_alloc_reuse(&psset, &alloc[i], PAGE);
               expect_false(err, "Nonempty psset failed page allocation.");
       }
       stats_expect(&psset, HUGEPAGE_PAGES);
       hpdata_t *ps;
       for (ssize_t i = HUGEPAGE_PAGES - 1; i >= 0; i--) {
               ps = test_psset_dalloc(&psset, &alloc[i]);
               expect_true((ps == NULL) == (i != 0),
                   "test_psset_dalloc should only evict a slab on the last "
                   "free");
               stats_expect(&psset, i);
       }

       test_psset_alloc_new(&psset, &pageslab, &alloc[0], PAGE);
       stats_expect(&psset, 1);
       psset_update_begin(&psset, &pageslab);
       stats_expect(&psset, 0);
       psset_update_end(&psset, &pageslab);
       stats_expect(&psset, 1);
}
TEST_END

/*
* Fills in and inserts two pageslabs, with the first better than the second,
* and each fully allocated (into the allocations in allocs and worse_allocs,
* each of which should be HUGEPAGE_PAGES long), except for a single free page
* at the end.
*
* (There's nothing magic about these numbers; it's just useful to share the
* setup between the oldest fit and the insert/remove test).
*/
static void
init_test_pageslabs(psset_t *psset, hpdata_t *pageslab,
   hpdata_t *worse_pageslab, edata_t *alloc, edata_t *worse_alloc) {
       bool err;

       hpdata_init(pageslab, (void *)(10 * HUGEPAGE), PAGESLAB_AGE);
       /*
        * This pageslab would be better from an address-first-fit POV, but
        * worse from an age POV.
        */
       hpdata_init(worse_pageslab, (void *)(9 * HUGEPAGE), PAGESLAB_AGE + 1);

       psset_init(psset);

       edata_init_test(&alloc[0]);
       test_psset_alloc_new(psset, pageslab, &alloc[0], PAGE);
       for (size_t i = 1; i < HUGEPAGE_PAGES; i++) {
               edata_init_test(&alloc[i]);
               err = test_psset_alloc_reuse(psset, &alloc[i], PAGE);
               expect_false(err, "Nonempty psset failed page allocation.");
               expect_ptr_eq(pageslab, edata_ps_get(&alloc[i]),
                   "Allocated from the wrong pageslab");
       }

       edata_init_test(&worse_alloc[0]);
       test_psset_alloc_new(psset, worse_pageslab, &worse_alloc[0], PAGE);
       expect_ptr_eq(worse_pageslab, edata_ps_get(&worse_alloc[0]),
           "Allocated from the wrong pageslab");
       /*
        * Make the two pssets otherwise indistinguishable; all full except for
        * a single page.
        */
       for (size_t i = 1; i < HUGEPAGE_PAGES - 1; i++) {
               edata_init_test(&worse_alloc[i]);
               err = test_psset_alloc_reuse(psset, &alloc[i], PAGE);
               expect_false(err, "Nonempty psset failed page allocation.");
               expect_ptr_eq(worse_pageslab, edata_ps_get(&alloc[i]),
                   "Allocated from the wrong pageslab");
       }

       /* Deallocate the last page from the older pageslab. */
       hpdata_t *evicted = test_psset_dalloc(psset,
           &alloc[HUGEPAGE_PAGES - 1]);
       expect_ptr_null(evicted, "Unexpected eviction");
}

TEST_BEGIN(test_oldest_fit) {
       bool err;
       edata_t alloc[HUGEPAGE_PAGES];
       edata_t worse_alloc[HUGEPAGE_PAGES];

       hpdata_t pageslab;
       hpdata_t worse_pageslab;

       psset_t psset;

       init_test_pageslabs(&psset, &pageslab, &worse_pageslab, alloc,
           worse_alloc);

       /* The edata should come from the better pageslab. */
       edata_t test_edata;
       edata_init_test(&test_edata);
       err = test_psset_alloc_reuse(&psset, &test_edata, PAGE);
       expect_false(err, "Nonempty psset failed page allocation");
       expect_ptr_eq(&pageslab, edata_ps_get(&test_edata),
           "Allocated from the wrong pageslab");
}
TEST_END

TEST_BEGIN(test_insert_remove) {
       bool err;
       hpdata_t *ps;
       edata_t alloc[HUGEPAGE_PAGES];
       edata_t worse_alloc[HUGEPAGE_PAGES];

       hpdata_t pageslab;
       hpdata_t worse_pageslab;

       psset_t psset;

       init_test_pageslabs(&psset, &pageslab, &worse_pageslab, alloc,
           worse_alloc);

       /* Remove better; should still be able to alloc from worse. */
       psset_update_begin(&psset, &pageslab);
       err = test_psset_alloc_reuse(&psset, &worse_alloc[HUGEPAGE_PAGES - 1],
           PAGE);
       expect_false(err, "Removal should still leave an empty page");
       expect_ptr_eq(&worse_pageslab,
           edata_ps_get(&worse_alloc[HUGEPAGE_PAGES - 1]),
           "Allocated out of wrong ps");

       /*
        * After deallocating the previous alloc and reinserting better, it
        * should be preferred for future allocations.
        */
       ps = test_psset_dalloc(&psset, &worse_alloc[HUGEPAGE_PAGES - 1]);
       expect_ptr_null(ps, "Incorrect eviction of nonempty pageslab");
       psset_update_end(&psset, &pageslab);
       err = test_psset_alloc_reuse(&psset, &alloc[HUGEPAGE_PAGES - 1], PAGE);
       expect_false(err, "psset should be nonempty");
       expect_ptr_eq(&pageslab, edata_ps_get(&alloc[HUGEPAGE_PAGES - 1]),
           "Removal/reinsertion shouldn't change ordering");
       /*
        * After deallocating and removing both, allocations should fail.
        */
       ps = test_psset_dalloc(&psset, &alloc[HUGEPAGE_PAGES - 1]);
       expect_ptr_null(ps, "Incorrect eviction");
       psset_update_begin(&psset, &pageslab);
       psset_update_begin(&psset, &worse_pageslab);
       err = test_psset_alloc_reuse(&psset, &alloc[HUGEPAGE_PAGES - 1], PAGE);
       expect_true(err, "psset should be empty, but an alloc succeeded");
}
TEST_END

TEST_BEGIN(test_purge_prefers_nonhuge) {
       /*
        * All else being equal, we should prefer purging non-huge pages over
        * huge ones for non-empty extents.
        */

       /* Nothing magic about this constant. */
       enum {
               NHP = 23,
       };
       hpdata_t *hpdata;

       psset_t psset;
       psset_init(&psset);

       hpdata_t hpdata_huge[NHP];
       uintptr_t huge_begin = (uintptr_t)&hpdata_huge[0];
       uintptr_t huge_end = (uintptr_t)&hpdata_huge[NHP];
       hpdata_t hpdata_nonhuge[NHP];
       uintptr_t nonhuge_begin = (uintptr_t)&hpdata_nonhuge[0];
       uintptr_t nonhuge_end = (uintptr_t)&hpdata_nonhuge[NHP];

       for (size_t i = 0; i < NHP; i++) {
               hpdata_init(&hpdata_huge[i], (void *)((10 + i) * HUGEPAGE),
                   123 + i);
               psset_insert(&psset, &hpdata_huge[i]);

               hpdata_init(&hpdata_nonhuge[i],
                   (void *)((10 + NHP + i) * HUGEPAGE),
                   456 + i);
               psset_insert(&psset, &hpdata_nonhuge[i]);

       }
       for (int i = 0; i < 2 * NHP; i++) {
               hpdata = psset_pick_alloc(&psset, HUGEPAGE * 3 / 4);
               psset_update_begin(&psset, hpdata);
               void *ptr;
               ptr = hpdata_reserve_alloc(hpdata, HUGEPAGE * 3 / 4);
               /* Ignore the first alloc, which will stick around. */
               (void)ptr;
               /*
                * The second alloc is to dirty the pages; free it immediately
                * after allocating.
                */
               ptr = hpdata_reserve_alloc(hpdata, HUGEPAGE / 4);
               hpdata_unreserve(hpdata, ptr, HUGEPAGE / 4);

               if (huge_begin <= (uintptr_t)hpdata
                   && (uintptr_t)hpdata < huge_end) {
                       hpdata_hugify(hpdata);
               }

               hpdata_purge_allowed_set(hpdata, true);
               psset_update_end(&psset, hpdata);
       }

       /*
        * We've got a bunch of 1/8th dirty hpdatas.  It should give us all the
        * non-huge ones to purge, then all the huge ones, then refuse to purge
        * further.
        */
       for (int i = 0; i < NHP; i++) {
               hpdata = psset_pick_purge(&psset);
               assert_true(nonhuge_begin <= (uintptr_t)hpdata
                   && (uintptr_t)hpdata < nonhuge_end, "");
               psset_update_begin(&psset, hpdata);
               test_psset_fake_purge(hpdata);
               hpdata_purge_allowed_set(hpdata, false);
               psset_update_end(&psset, hpdata);
       }
       for (int i = 0; i < NHP; i++) {
               hpdata = psset_pick_purge(&psset);
               expect_true(huge_begin <= (uintptr_t)hpdata
                   && (uintptr_t)hpdata < huge_end, "");
               psset_update_begin(&psset, hpdata);
               hpdata_dehugify(hpdata);
               test_psset_fake_purge(hpdata);
               hpdata_purge_allowed_set(hpdata, false);
               psset_update_end(&psset, hpdata);
       }
}
TEST_END

TEST_BEGIN(test_purge_prefers_empty) {
       void *ptr;

       psset_t psset;
       psset_init(&psset);

       hpdata_t hpdata_empty;
       hpdata_t hpdata_nonempty;
       hpdata_init(&hpdata_empty, (void *)(10 * HUGEPAGE), 123);
       psset_insert(&psset, &hpdata_empty);
       hpdata_init(&hpdata_nonempty, (void *)(11 * HUGEPAGE), 456);
       psset_insert(&psset, &hpdata_nonempty);

       psset_update_begin(&psset, &hpdata_empty);
       ptr = hpdata_reserve_alloc(&hpdata_empty, PAGE);
       expect_ptr_eq(hpdata_addr_get(&hpdata_empty), ptr, "");
       hpdata_unreserve(&hpdata_empty, ptr, PAGE);
       hpdata_purge_allowed_set(&hpdata_empty, true);
       psset_update_end(&psset, &hpdata_empty);

       psset_update_begin(&psset, &hpdata_nonempty);
       ptr = hpdata_reserve_alloc(&hpdata_nonempty, 10 * PAGE);
       expect_ptr_eq(hpdata_addr_get(&hpdata_nonempty), ptr, "");
       hpdata_unreserve(&hpdata_nonempty, ptr, 9 * PAGE);
       hpdata_purge_allowed_set(&hpdata_nonempty, true);
       psset_update_end(&psset, &hpdata_nonempty);

       /*
        * The nonempty slab has 9 dirty pages, while the empty one has only 1.
        * We should still pick the empty one for purging.
        */
       hpdata_t *to_purge = psset_pick_purge(&psset);
       expect_ptr_eq(&hpdata_empty, to_purge, "");
}
TEST_END

TEST_BEGIN(test_purge_prefers_empty_huge) {
       void *ptr;

       psset_t psset;
       psset_init(&psset);

       enum {NHP = 10 };

       hpdata_t hpdata_huge[NHP];
       hpdata_t hpdata_nonhuge[NHP];

       uintptr_t cur_addr = 100 * HUGEPAGE;
       uint64_t cur_age = 123;
       for (int i = 0; i < NHP; i++) {
               hpdata_init(&hpdata_huge[i], (void *)cur_addr, cur_age);
               cur_addr += HUGEPAGE;
               cur_age++;
               psset_insert(&psset, &hpdata_huge[i]);

               hpdata_init(&hpdata_nonhuge[i], (void *)cur_addr, cur_age);
               cur_addr += HUGEPAGE;
               cur_age++;
               psset_insert(&psset, &hpdata_nonhuge[i]);

               /*
                * Make the hpdata_huge[i] fully dirty, empty, purgable, and
                * huge.
                */
               psset_update_begin(&psset, &hpdata_huge[i]);
               ptr = hpdata_reserve_alloc(&hpdata_huge[i], HUGEPAGE);
               expect_ptr_eq(hpdata_addr_get(&hpdata_huge[i]), ptr, "");
               hpdata_hugify(&hpdata_huge[i]);
               hpdata_unreserve(&hpdata_huge[i], ptr, HUGEPAGE);
               hpdata_purge_allowed_set(&hpdata_huge[i], true);
               psset_update_end(&psset, &hpdata_huge[i]);

               /*
                * Make hpdata_nonhuge[i] fully dirty, empty, purgable, and
                * non-huge.
                */
               psset_update_begin(&psset, &hpdata_nonhuge[i]);
               ptr = hpdata_reserve_alloc(&hpdata_nonhuge[i], HUGEPAGE);
               expect_ptr_eq(hpdata_addr_get(&hpdata_nonhuge[i]), ptr, "");
               hpdata_unreserve(&hpdata_nonhuge[i], ptr, HUGEPAGE);
               hpdata_purge_allowed_set(&hpdata_nonhuge[i], true);
               psset_update_end(&psset, &hpdata_nonhuge[i]);
       }

       /*
        * We have a bunch of empty slabs, half huge, half nonhuge, inserted in
        * alternating order.  We should pop all the huge ones before popping
        * any of the non-huge ones for purging.
        */
       for (int i = 0; i < NHP; i++) {
               hpdata_t *to_purge = psset_pick_purge(&psset);
               expect_ptr_eq(&hpdata_huge[i], to_purge, "");
               psset_update_begin(&psset, to_purge);
               hpdata_purge_allowed_set(to_purge, false);
               psset_update_end(&psset, to_purge);
       }
       for (int i = 0; i < NHP; i++) {
               hpdata_t *to_purge = psset_pick_purge(&psset);
               expect_ptr_eq(&hpdata_nonhuge[i], to_purge, "");
               psset_update_begin(&psset, to_purge);
               hpdata_purge_allowed_set(to_purge, false);
               psset_update_end(&psset, to_purge);
       }
}
TEST_END

int
main(void) {
       return test_no_reentrancy(
           test_empty,
           test_fill,
           test_reuse,
           test_evict,
           test_multi_pageslab,
           test_stats,
           test_oldest_fit,
           test_insert_remove,
           test_purge_prefers_nonhuge,
           test_purge_prefers_empty,
           test_purge_prefers_empty_huge);
}