331 lines
8.9 KiB
C
331 lines
8.9 KiB
C
// 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);
|
|
}
|