#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);
}