// SPDX-License-Identifier: GPL-2.0
#include "util/debug.h"
#include "util/map.h"
#include "util/symbol.h"
#include "util/sort.h"
#include "util/evsel.h"
#include "util/event.h"
#include "util/evlist.h"
#include "util/machine.h"
#include "util/parse-events.h"
#include "util/thread.h"
#include "tests/tests.h"
#include "tests/hists_common.h"
#include <linux/kernel.h>

struct sample {
	u32 pid;
	u64 ip;
	struct thread *thread;
	struct map *map;
	struct symbol *sym;
	int socket;
};

/* For the numbers, see hists_common.c */
static struct sample fake_samples[] = {
	/* perf [kernel] schedule() */
	{ .pid = FAKE_PID_PERF1, .ip = FAKE_IP_KERNEL_SCHEDULE, .socket = 0 },
	/* perf [perf]   main() */
	{ .pid = FAKE_PID_PERF1, .ip = FAKE_IP_PERF_MAIN, .socket = 0 },
	/* perf [libc]   malloc() */
	{ .pid = FAKE_PID_PERF1, .ip = FAKE_IP_LIBC_MALLOC, .socket = 0 },
	/* perf [perf]   main() */
	{ .pid = FAKE_PID_PERF2, .ip = FAKE_IP_PERF_MAIN, .socket = 0 }, /* will be merged */
	/* perf [perf]   cmd_record() */
	{ .pid = FAKE_PID_PERF2, .ip = FAKE_IP_PERF_CMD_RECORD, .socket = 1 },
	/* perf [kernel] page_fault() */
	{ .pid = FAKE_PID_PERF2, .ip = FAKE_IP_KERNEL_PAGE_FAULT, .socket = 1 },
	/* bash [bash]   main() */
	{ .pid = FAKE_PID_BASH,  .ip = FAKE_IP_BASH_MAIN, .socket = 2 },
	/* bash [bash]   xmalloc() */
	{ .pid = FAKE_PID_BASH,  .ip = FAKE_IP_BASH_XMALLOC, .socket = 2 },
	/* bash [libc]   malloc() */
	{ .pid = FAKE_PID_BASH,  .ip = FAKE_IP_LIBC_MALLOC, .socket = 3 },
	/* bash [kernel] page_fault() */
	{ .pid = FAKE_PID_BASH,  .ip = FAKE_IP_KERNEL_PAGE_FAULT, .socket = 3 },
};

static int add_hist_entries(struct evlist *evlist,
			    struct machine *machine)
{
	struct evsel *evsel;
	struct addr_location al;
	struct perf_sample sample = { .period = 100, };
	size_t i;

	addr_location__init(&al);
	/*
	 * each evsel will have 10 samples but the 4th sample
	 * (perf [perf] main) will be collapsed to an existing entry
	 * so total 9 entries will be in the tree.
	 */
	evlist__for_each_entry(evlist, evsel) {
		for (i = 0; i < ARRAY_SIZE(fake_samples); i++) {
			struct hist_entry_iter iter = {
				.evsel = evsel,
				.sample = &sample,
				.ops = &hist_iter_normal,
				.hide_unresolved = false,
			};
			struct hists *hists = evsel__hists(evsel);

			/* make sure it has no filter at first */
			hists->thread_filter = NULL;
			hists->dso_filter = NULL;
			hists->symbol_filter_str = NULL;

			sample.cpumode = PERF_RECORD_MISC_USER;
			sample.pid = fake_samples[i].pid;
			sample.tid = fake_samples[i].pid;
			sample.ip = fake_samples[i].ip;

			if (machine__resolve(machine, &al, &sample) < 0)
				goto out;

			al.socket = fake_samples[i].socket;
			if (hist_entry_iter__add(&iter, &al,
						 sysctl_perf_event_max_stack, NULL) < 0) {
				goto out;
			}

			thread__put(fake_samples[i].thread);
			fake_samples[i].thread = thread__get(al.thread);
			map__put(fake_samples[i].map);
			fake_samples[i].map = map__get(al.map);
			fake_samples[i].sym = al.sym;
		}
	}
	addr_location__exit(&al);
	return 0;

out:
	pr_debug("Not enough memory for adding a hist entry\n");
	addr_location__exit(&al);
	return TEST_FAIL;
}

static void put_fake_samples(void)
{
	size_t i;

	for (i = 0; i < ARRAY_SIZE(fake_samples); i++)
		map__put(fake_samples[i].map);
}

static int test__hists_filter(struct test_suite *test __maybe_unused, int subtest __maybe_unused)
{
	int err = TEST_FAIL;
	struct machines machines;
	struct machine *machine;
	struct evsel *evsel;
	struct evlist *evlist = evlist__new();

	TEST_ASSERT_VAL("No memory", evlist);

	err = parse_event(evlist, "cpu-clock");
	if (err)
		goto out;
	err = parse_event(evlist, "task-clock");
	if (err)
		goto out;
	err = TEST_FAIL;

	machines__init(&machines);

	/* setup threads/dso/map/symbols also */
	machine = setup_fake_machine(&machines);
	if (!machine)
		goto out;

	if (verbose > 1)
		machine__fprintf(machine, stderr);

	/* default sort order (comm,dso,sym) will be used */
	if (setup_sorting(evlist, machine->env) < 0)
		goto out;

	/* process sample events */
	err = add_hist_entries(evlist, machine);
	if (err < 0)
		goto out;

	evlist__for_each_entry(evlist, evsel) {
		struct hists *hists = evsel__hists(evsel);

		hists__collapse_resort(hists, NULL);
		evsel__output_resort(evsel, NULL);

		if (verbose > 2) {
			pr_info("Normal histogram\n");
			print_hists_out(hists);
		}

		TEST_ASSERT_VAL("Invalid nr samples",
				hists->stats.nr_samples == 10);
		TEST_ASSERT_VAL("Invalid nr hist entries",
				hists->nr_entries == 9);
		TEST_ASSERT_VAL("Invalid total period",
				hists->stats.total_period == 1000);
		TEST_ASSERT_VAL("Unmatched nr samples",
				hists->stats.nr_samples ==
				hists->stats.nr_non_filtered_samples);
		TEST_ASSERT_VAL("Unmatched nr hist entries",
				hists->nr_entries == hists->nr_non_filtered_entries);
		TEST_ASSERT_VAL("Unmatched total period",
				hists->stats.total_period ==
				hists->stats.total_non_filtered_period);

		/* now applying thread filter for 'bash' */
		hists->thread_filter = fake_samples[9].thread;
		hists__filter_by_thread(hists);

		if (verbose > 2) {
			pr_info("Histogram for thread filter\n");
			print_hists_out(hists);
		}

		/* normal stats should be invariant */
		TEST_ASSERT_VAL("Invalid nr samples",
				hists->stats.nr_samples == 10);
		TEST_ASSERT_VAL("Invalid nr hist entries",
				hists->nr_entries == 9);
		TEST_ASSERT_VAL("Invalid total period",
				hists->stats.total_period == 1000);

		/* but filter stats are changed */
		TEST_ASSERT_VAL("Unmatched nr samples for thread filter",
				hists->stats.nr_non_filtered_samples == 4);
		TEST_ASSERT_VAL("Unmatched nr hist entries for thread filter",
				hists->nr_non_filtered_entries == 4);
		TEST_ASSERT_VAL("Unmatched total period for thread filter",
				hists->stats.total_non_filtered_period == 400);

		/* remove thread filter first */
		hists->thread_filter = NULL;
		hists__filter_by_thread(hists);

		/* now applying dso filter for 'kernel' */
		hists->dso_filter = map__dso(fake_samples[0].map);
		hists__filter_by_dso(hists);

		if (verbose > 2) {
			pr_info("Histogram for dso filter\n");
			print_hists_out(hists);
		}

		/* normal stats should be invariant */
		TEST_ASSERT_VAL("Invalid nr samples",
				hists->stats.nr_samples == 10);
		TEST_ASSERT_VAL("Invalid nr hist entries",
				hists->nr_entries == 9);
		TEST_ASSERT_VAL("Invalid total period",
				hists->stats.total_period == 1000);

		/* but filter stats are changed */
		TEST_ASSERT_VAL("Unmatched nr samples for dso filter",
				hists->stats.nr_non_filtered_samples == 3);
		TEST_ASSERT_VAL("Unmatched nr hist entries for dso filter",
				hists->nr_non_filtered_entries == 3);
		TEST_ASSERT_VAL("Unmatched total period for dso filter",
				hists->stats.total_non_filtered_period == 300);

		/* remove dso filter first */
		hists->dso_filter = NULL;
		hists__filter_by_dso(hists);

		/*
		 * now applying symbol filter for 'main'.  Also note that
		 * there's 3 samples that have 'main' symbol but the 4th
		 * entry of fake_samples was collapsed already so it won't
		 * be counted as a separate entry but the sample count and
		 * total period will be remained.
		 */
		hists->symbol_filter_str = "main";
		hists__filter_by_symbol(hists);

		if (verbose > 2) {
			pr_info("Histogram for symbol filter\n");
			print_hists_out(hists);
		}

		/* normal stats should be invariant */
		TEST_ASSERT_VAL("Invalid nr samples",
				hists->stats.nr_samples == 10);
		TEST_ASSERT_VAL("Invalid nr hist entries",
				hists->nr_entries == 9);
		TEST_ASSERT_VAL("Invalid total period",
				hists->stats.total_period == 1000);

		/* but filter stats are changed */
		TEST_ASSERT_VAL("Unmatched nr samples for symbol filter",
				hists->stats.nr_non_filtered_samples == 3);
		TEST_ASSERT_VAL("Unmatched nr hist entries for symbol filter",
				hists->nr_non_filtered_entries == 2);
		TEST_ASSERT_VAL("Unmatched total period for symbol filter",
				hists->stats.total_non_filtered_period == 300);

		/* remove symbol filter first */
		hists->symbol_filter_str = NULL;
		hists__filter_by_symbol(hists);

		/* now applying socket filters */
		hists->socket_filter = 2;
		hists__filter_by_socket(hists);

		if (verbose > 2) {
			pr_info("Histogram for socket filters\n");
			print_hists_out(hists);
		}

		/* normal stats should be invariant */
		TEST_ASSERT_VAL("Invalid nr samples",
				hists->stats.nr_samples == 10);
		TEST_ASSERT_VAL("Invalid nr hist entries",
				hists->nr_entries == 9);
		TEST_ASSERT_VAL("Invalid total period",
				hists->stats.total_period == 1000);

		/* but filter stats are changed */
		TEST_ASSERT_VAL("Unmatched nr samples for socket filter",
				hists->stats.nr_non_filtered_samples == 2);
		TEST_ASSERT_VAL("Unmatched nr hist entries for socket filter",
				hists->nr_non_filtered_entries == 2);
		TEST_ASSERT_VAL("Unmatched total period for socket filter",
				hists->stats.total_non_filtered_period == 200);

		/* remove socket filter first */
		hists->socket_filter = -1;
		hists__filter_by_socket(hists);

		/* now applying all filters at once. */
		hists->thread_filter = fake_samples[1].thread;
		hists->dso_filter = map__dso(fake_samples[1].map);
		hists__filter_by_thread(hists);
		hists__filter_by_dso(hists);

		if (verbose > 2) {
			pr_info("Histogram for all filters\n");
			print_hists_out(hists);
		}

		/* normal stats should be invariant */
		TEST_ASSERT_VAL("Invalid nr samples",
				hists->stats.nr_samples == 10);
		TEST_ASSERT_VAL("Invalid nr hist entries",
				hists->nr_entries == 9);
		TEST_ASSERT_VAL("Invalid total period",
				hists->stats.total_period == 1000);

		/* but filter stats are changed */
		TEST_ASSERT_VAL("Unmatched nr samples for all filter",
				hists->stats.nr_non_filtered_samples == 2);
		TEST_ASSERT_VAL("Unmatched nr hist entries for all filter",
				hists->nr_non_filtered_entries == 1);
		TEST_ASSERT_VAL("Unmatched total period for all filter",
				hists->stats.total_non_filtered_period == 200);
	}


	err = TEST_OK;

out:
	/* tear down everything */
	evlist__delete(evlist);
	reset_output_field();
	machines__exit(&machines);
	put_fake_samples();

	return err;
}

DEFINE_SUITE("Filter hist entries", hists_filter);
