blob: facd55d2f174ce8475a1046d8731e727284c2adc [file] [log] [blame]
// Copyright © 2024 Rot127 <unisono@quyllur.org>
// SPDX-License-Identifier: BSD-3
#include "test_run.h"
#include "test_case.h"
#include "test_mapping.h"
#include "../../../utils.h"
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include "cmocka.h"
#include <capstone/capstone.h>
#include <stdbool.h>
#include <stdio.h>
static TestRunResult get_test_run_result(TestRunStats *stats)
{
if (stats->tc_total !=
stats->successful + stats->failed + stats->skipped) {
fprintf(stderr,
"[!] Inconsistent statistics: total != successful + failed + skipped\n");
stats->errors++;
return TEST_RUN_ERROR;
}
if (stats->errors != 0) {
return TEST_RUN_ERROR;
} else if (stats->failed != 0) {
return TEST_RUN_FAILURE;
}
return TEST_RUN_SUCCESS;
}
/// Extract all test cases from the given test files.
static TestFile **parse_test_files(char **tf_paths, uint32_t path_count,
TestRunStats *stats)
{
TestFile **files = NULL;
stats->tc_total = 0;
for (size_t i = 0; i < path_count; ++i) {
TestFile *test_file_data = NULL;
cyaml_err_t err = cyaml_load_file(
tf_paths[i], &cyaml_config, &test_file_schema,
(cyaml_data_t **)&test_file_data, NULL);
if (err != CYAML_OK || !test_file_data) {
fprintf(stderr, "[!] Failed to parse test file '%s'\n",
tf_paths[i]);
fprintf(stderr, "[!] Error: '%s'\n",
!test_file_data && err == CYAML_OK ?
"Empty file" :
cyaml_strerror(err));
stats->invalid_files++;
stats->errors++;
continue;
}
size_t k = stats->valid_test_files++;
// Copy all test cases of a test file
files = cs_mem_realloc(files, sizeof(TestFile *) *
stats->valid_test_files);
files[k] = test_file_clone(test_file_data);
assert(files[k]);
stats->tc_total += files[k]->test_cases_count;
files[k]->filename = strrchr(tf_paths[i], '/') ?
strdup(strrchr(tf_paths[i], '/')) :
strdup(tf_paths[i]);
err = cyaml_free(&cyaml_config, &test_file_schema,
test_file_data, 0);
if (err != CYAML_OK) {
fprintf(stderr, "[!] Error: '%s'\n",
cyaml_strerror(err));
stats->errors++;
continue;
}
}
return files;
}
/// Parses the @input and saves the results in the other arguments.
static bool parse_input_options(const TestInput *input, cs_arch *arch,
cs_mode *mode, cs_opt *opt_arr,
size_t opt_arr_size, size_t *opt_set)
{
assert(input && arch && mode && opt_arr);
bool arch_found = false;
const char *opt_str = input->arch;
int val = enum_map_bin_search(test_arch_map, ARR_SIZE(test_arch_map),
opt_str, &arch_found);
if (arch_found) {
*arch = val;
} else {
fprintf(stderr,
"[!] '%s' is not mapped to a capstone architecture.\n",
input->arch);
return false;
}
*mode = 0;
size_t opt_idx = 0;
char **options = input->options;
for (size_t i = 0; i < input->options_count; ++i) {
bool opt_found = false;
opt_str = options[i];
val = enum_map_bin_search(test_mode_map,
ARR_SIZE(test_mode_map), opt_str,
&opt_found);
if (opt_found) {
*mode |= val;
continue;
}
// Might be an option descriptor
for (size_t k = 0; k < ARR_SIZE(test_option_map); k++) {
if (strings_match(opt_str, test_option_map[k].str)) {
if (opt_idx >= opt_arr_size) {
fprintf(stderr,
"Too many options given in: '%s'. Maximum is: %" PRId64
"\n",
opt_str,
(uint64_t)opt_arr_size);
return false;
}
opt_arr[opt_idx++] = test_option_map[k].opt;
opt_found = true;
break;
}
}
if (!opt_found) {
fprintf(stderr, "[!] Option: '%s' not used\n", opt_str);
}
}
*opt_set = opt_idx;
return true;
}
/// Parses the options for cs_open/cs_option and initializes the handle.
/// Returns true for success and false otherwise.
static bool open_cs_handle(UnitTestState *ustate)
{
cs_arch arch = 0;
cs_mode mode = 0;
cs_opt options[8] = { 0 };
size_t options_set = 0;
if (!parse_input_options(ustate->tcase->input, &arch, &mode, options, 8,
&options_set)) {
char *tc_str = test_input_stringify(ustate->tcase->input, "");
fprintf(stderr, "Could not parse options: %s\n", tc_str);
cs_mem_free(tc_str);
return false;
}
cs_err err = cs_open(arch, mode, &ustate->handle);
if (err != CS_ERR_OK) {
char *tc_str = test_input_stringify(ustate->tcase->input, "");
fprintf(stderr,
"[!] cs_open() failed with: '%s'. TestInput: %s\n",
cs_strerror(err), tc_str);
cs_mem_free(tc_str);
return false;
}
// The bit mode must be set, otherwise the numbers are
// not normalized correctly in the asm-test comparison step.
if (arch == CS_ARCH_AARCH64 || mode & CS_MODE_64) {
ustate->arch_bits = 64;
} else if (mode & CS_MODE_16) {
ustate->arch_bits = 16;
} else {
ustate->arch_bits = 32;
}
for (size_t i = 0; i < options_set; ++i) {
err = cs_option(ustate->handle, options[i].type,
options[i].val);
if (err != CS_ERR_OK) {
goto option_error;
}
}
return true;
option_error: {
char *tc_str = test_input_stringify(ustate->tcase->input, "");
fprintf(stderr, "[!] cs_option() failed with: '%s'. TestInput: %s\n",
cs_strerror(err), tc_str);
cs_mem_free(tc_str);
cs_close(&ustate->handle);
return false;
}
}
static int cstest_unit_test_setup(void **state)
{
assert(state);
UnitTestState *ustate = *state;
assert(ustate->tcase);
if (!open_cs_handle(ustate)) {
fail_msg("Failed to initialize Capstone with given options.");
return -1;
}
return 0;
}
static int cstest_unit_test_teardown(void **state)
{
if (!state) {
return 0;
}
UnitTestState *ustate = *state;
if (ustate->handle) {
cs_err err = cs_close(&ustate->handle);
if (err != CS_ERR_OK) {
fail_msg("cs_close() failed with: '%s'.",
cs_strerror(err));
return -1;
}
}
return 0;
}
static void cstest_unit_test(void **state)
{
assert(state);
UnitTestState *ustate = *state;
assert(ustate);
assert(ustate->handle);
assert(ustate->tcase);
csh handle = ustate->handle;
TestCase *tcase = ustate->tcase;
cs_insn *insns = NULL;
size_t insns_count = cs_disasm(handle, tcase->input->bytes,
tcase->input->bytes_count,
tcase->input->address, 0, &insns);
test_expected_compare(&ustate->handle, tcase->expected, insns,
insns_count, ustate->arch_bits);
ustate->decoded_insns += insns_count;
cs_free(insns, insns_count);
}
static void eval_test_cases(TestFile **test_files, TestRunStats *stats)
{
assert(test_files && stats);
// CMocka's API doesn't allow to init a CMUnitTest with a partially initialized state
// (which is later initialized in the test setup).
// So we do it manually here.
struct CMUnitTest *utest_table =
cs_mem_calloc(sizeof(struct CMUnitTest),
stats->tc_total); // Number of test cases.
char utest_id[128] = { 0 };
size_t tci = 0;
for (size_t i = 0; i < stats->valid_test_files; ++i) {
TestCase **test_cases = test_files[i]->test_cases;
const char *filename = test_files[i]->filename ?
test_files[i]->filename :
NULL;
for (size_t k = 0; k < test_files[i]->test_cases_count;
++k, ++tci) {
cs_snprintf(utest_id, sizeof(utest_id),
"%s - TC #%" PRIx32 ": ", filename, k);
if (test_cases[k]->skip) {
char *tc_name = test_input_stringify(
test_cases[k]->input, utest_id);
fprintf(stderr, "SKIP: %s\nReason: %s\n",
tc_name, test_cases[k]->skip_reason);
cs_mem_free(tc_name);
stats->skipped++;
continue;
}
UnitTestState *ut_state =
cs_mem_calloc(sizeof(UnitTestState), 1);
ut_state->tcase = test_cases[k];
utest_table[tci].name = test_input_stringify(
ut_state->tcase->input, utest_id);
utest_table[tci].initial_state = ut_state;
utest_table[tci].setup_func = cstest_unit_test_setup;
utest_table[tci].teardown_func =
cstest_unit_test_teardown;
utest_table[tci].test_func = cstest_unit_test;
}
}
assert(tci == stats->tc_total);
// Use private function here, because the API takes only constant tables.
int failed_tests = _cmocka_run_group_tests(
"All test cases", utest_table, stats->tc_total, NULL, NULL);
assert(failed_tests >= 0 && "Faulty return value");
for (size_t i = 0; i < stats->tc_total; ++i) {
UnitTestState *ustate = utest_table[i].initial_state;
if (!ustate) {
// Skipped test case
continue;
}
stats->decoded_insns += ustate->decoded_insns;
cs_mem_free((char *)utest_table[i].name);
cs_mem_free(utest_table[i].initial_state);
}
cs_mem_free(utest_table);
stats->failed += failed_tests;
stats->successful += stats->tc_total - failed_tests - stats->skipped;
}
/// Runs runs all valid tests in the given @test_files
/// and returns the result as well as statistics in @stats.
TestRunResult cstest_run_tests(char **test_file_paths, uint32_t path_count,
TestRunStats *stats)
{
TestFile **files = parse_test_files(test_file_paths, path_count, stats);
if (!files) {
return get_test_run_result(stats);
}
eval_test_cases(files, stats);
for (size_t i = 0; i < stats->valid_test_files; ++i) {
test_file_free(files[i]);
}
cs_mem_free(files);
return get_test_run_result(stats);
}