#include "test/jemalloc_test.h"

#define TEST_UTIL_EINVAL(node, a, b, c, d, why_inval)                          \
	do {                                                                   \
		assert_d_eq(                                                   \
		    mallctl("experimental.utilization." node, a, b, c, d),     \
		    EINVAL, "Should fail when " why_inval);                    \
		assert_zu_eq(out_sz, out_sz_ref,                               \
		    "Output size touched when given invalid arguments");       \
		assert_d_eq(memcmp(out, out_ref, out_sz_ref), 0,               \
		    "Output content touched when given invalid arguments");    \
	} while (0)

#define TEST_UTIL_BATCH_EINVAL(a, b, c, d, why_inval)                          \
	TEST_UTIL_EINVAL("batch_query", a, b, c, d, why_inval)

#define TEST_UTIL_VALID(node)                                                  \
	do {                                                                   \
		assert_d_eq(mallctl("experimental.utilization." node, out,     \
		                &out_sz, in, in_sz),                           \
		    0, "Should return 0 on correct arguments");                \
		expect_zu_eq(out_sz, out_sz_ref, "incorrect output size");     \
		expect_d_ne(memcmp(out, out_ref, out_sz_ref), 0,               \
		    "Output content should be changed");                       \
	} while (0)

#define TEST_UTIL_BATCH_VALID TEST_UTIL_VALID("batch_query")

#define TEST_MAX_SIZE (1 << 20)

TEST_BEGIN(test_batch) {
	size_t sz;
	/*
	 * Select some sizes that can span both small and large sizes, and are
	 * numerically unrelated to any size boundaries.
	 */
	for (sz = 17; sz <= TEST_MAX_SIZE && sz <= SC_LARGE_MAXCLASS;
	     sz += (sz <= SC_SMALL_MAXCLASS ? 1019 : 99991)) {
		void  *p = mallocx(sz, 0);
		void  *q = mallocx(sz, 0);
		void  *in[] = {p, q};
		size_t in_sz = sizeof(const void *) * 2;
		size_t out[] = {-1, -1, -1, -1, -1, -1};
		size_t out_sz = sizeof(size_t) * 6;
		size_t out_ref[] = {-1, -1, -1, -1, -1, -1};
		size_t out_sz_ref = out_sz;

		assert_ptr_not_null(p, "test pointer allocation failed");
		assert_ptr_not_null(q, "test pointer allocation failed");

		/* Test invalid argument(s) errors */
		TEST_UTIL_BATCH_EINVAL(NULL, &out_sz, in, in_sz, "old is NULL");
		TEST_UTIL_BATCH_EINVAL(out, NULL, in, in_sz, "oldlenp is NULL");
		TEST_UTIL_BATCH_EINVAL(
		    out, &out_sz, NULL, in_sz, "newp is NULL");
		TEST_UTIL_BATCH_EINVAL(out, &out_sz, in, 0, "newlen is zero");
		in_sz -= 1;
		TEST_UTIL_BATCH_EINVAL(
		    out, &out_sz, in, in_sz, "newlen is not an exact multiple");
		in_sz += 1;
		out_sz_ref = out_sz -= 2 * sizeof(size_t);
		TEST_UTIL_BATCH_EINVAL(out, &out_sz, in, in_sz,
		    "*oldlenp is not an exact multiple");
		out_sz_ref = out_sz += 2 * sizeof(size_t);
		in_sz -= sizeof(const void *);
		TEST_UTIL_BATCH_EINVAL(out, &out_sz, in, in_sz,
		    "*oldlenp and newlen do not match");
		in_sz += sizeof(const void *);

		/* Examine output for valid calls */
#define TEST_EQUAL_REF(i, message)                                             \
	assert_d_eq(memcmp(out + (i) * 3, out_ref + (i) * 3, 3), 0, message)

#define NFREE_READ(out, i) out[(i) * 3]
#define NREGS_READ(out, i) out[(i) * 3 + 1]
#define SIZE_READ(out, i) out[(i) * 3 + 2]

		out_sz_ref = out_sz /= 2;
		in_sz /= 2;
		TEST_UTIL_BATCH_VALID;
		expect_zu_le(sz, SIZE_READ(out, 0),
		    "Extent size should be at least allocation size");
		expect_zu_eq(SIZE_READ(out, 0) & (PAGE - 1), 0,
		    "Extent size should be a multiple of page size");
		/* Profiling breaks our slab count expectations. */
		if (sz <= SC_SMALL_MAXCLASS && !opt_prof) {
			expect_zu_le(NFREE_READ(out, 0), NREGS_READ(out, 0),
			    "Extent free count exceeded region count");
			expect_zu_le(NREGS_READ(out, 0), SIZE_READ(out, 0),
			    "Extent region count exceeded size");
			expect_zu_ne(NREGS_READ(out, 0), 0,
			    "Extent region count must be positive");
		} else if (sz > SC_SMALL_MAXCLASS) {
			expect_zu_eq(NFREE_READ(out, 0), 0,
			    "Extent free count should be zero");
			expect_zu_eq(NREGS_READ(out, 0), 1,
			    "Extent region count should be one");
		}
		TEST_EQUAL_REF(
		    1, "Should not overwrite content beyond what's needed");
		in_sz *= 2;
		out_sz_ref = out_sz *= 2;

		memcpy(out_ref, out, 3 * sizeof(size_t));
		TEST_UTIL_BATCH_VALID;
		TEST_EQUAL_REF(0, "Statistics should be stable across calls");
		if (sz <= SC_SMALL_MAXCLASS) {
			expect_zu_le(NFREE_READ(out, 1), NREGS_READ(out, 1),
			    "Extent free count exceeded region count");
		} else {
			expect_zu_eq(NFREE_READ(out, 0), 0,
			    "Extent free count should be zero");
		}
		expect_zu_eq(NREGS_READ(out, 0), NREGS_READ(out, 1),
		    "Extent region count should be same for same region size");
		expect_zu_eq(SIZE_READ(out, 0), SIZE_READ(out, 1),
		    "Extent size should be same for same region size");

#undef SIZE_READ
#undef NREGS_READ
#undef NFREE_READ

#undef TEST_EQUAL_REF

		free(q);
		free(p);
	}
}
TEST_END

int
main(void) {
	assert_zu_lt(SC_SMALL_MAXCLASS + 100000, TEST_MAX_SIZE,
	    "Test case cannot cover large classes");
	return test(test_batch);
}
