mirror of
https://github.com/radareorg/radare2.git
synced 2025-02-15 03:38:27 +00:00
1254 lines
34 KiB
C
1254 lines
34 KiB
C
/* radare - LGPL - Copyright 2020 - thestr4ng3r */
|
||
|
||
#include "r2r.h"
|
||
#include <r_cons.h>
|
||
#include <assert.h>
|
||
|
||
#define WORKERS_DEFAULT 8
|
||
#define RADARE2_CMD_DEFAULT "radare2"
|
||
#define RASM2_CMD_DEFAULT "rasm2"
|
||
#define JSON_TEST_FILE_DEFAULT "bins/elf/crackme0x00b"
|
||
#define TIMEOUT_DEFAULT 960
|
||
|
||
#define STRV(x) #x
|
||
#define STR(x) STRV(x)
|
||
#define WORKERS_DEFAULT_STR STR(WORKERS_DEFAULT)
|
||
#define TIMEOUT_DEFAULT_STR STR(TIMEOUT_DEFAULT)
|
||
|
||
#define Color_INSERT Color_BGREEN
|
||
#define Color_DELETE Color_BRED
|
||
#define Color_BGINSERT "\x1b[48;5;22m"
|
||
#define Color_BGDELETE "\x1b[48;5;52m"
|
||
#define Color_HLINSERT Color_BGINSERT Color_INSERT
|
||
#define Color_HLDELETE Color_BGDELETE Color_DELETE
|
||
|
||
typedef struct r2r_state_t {
|
||
R2RRunConfig run_config;
|
||
bool verbose;
|
||
R2RTestDatabase *db;
|
||
|
||
RThreadCond *cond; // signaled from workers to main thread to update status
|
||
RThreadLock *lock; // protects everything below
|
||
HtPP *path_left; // char * (path to test file) => ut64 * (count of remaining tests)
|
||
RPVector completed_paths;
|
||
ut64 ok_count;
|
||
ut64 xx_count;
|
||
ut64 br_count;
|
||
ut64 fx_count;
|
||
RPVector queue;
|
||
RPVector results;
|
||
} R2RState;
|
||
|
||
static RThreadFunctionRet worker_th(RThread *th);
|
||
static void print_state(R2RState *state, ut64 prev_completed);
|
||
static void print_log(R2RState *state, ut64 prev_completed, ut64 prev_paths_completed);
|
||
static void interact(R2RState *state);
|
||
static void interact_fix(R2RTestResultInfo *result, RPVector *fixup_results);
|
||
static void interact_break(R2RTestResultInfo *result, RPVector *fixup_results);
|
||
static void interact_commands(R2RTestResultInfo *result, RPVector *fixup_results);
|
||
static void interact_diffchar(R2RTestResultInfo *result);
|
||
|
||
static int help(bool verbose) {
|
||
printf ("Usage: r2r [-qvVnL] [-j threads] [test file/dir | @test-type]\n");
|
||
if (verbose) {
|
||
printf (
|
||
" -h print this help\n"
|
||
" -v show version\n"
|
||
" -q quiet\n"
|
||
" -V verbose\n"
|
||
" -i interactive mode\n"
|
||
" -n do nothing (don't run any test, just load/parse them)\n"
|
||
" -L log mode (better printing for CI, logfiles, etc.)"
|
||
" -F [dir] run fuzz tests (open and default analysis) on all files in the given dir\n"
|
||
" -j [threads] how many threads to use for running tests concurrently (default is "WORKERS_DEFAULT_STR")\n"
|
||
" -r [radare2] path to radare2 executable (default is "RADARE2_CMD_DEFAULT")\n"
|
||
" -m [rasm2] path to rasm2 executable (default is "RASM2_CMD_DEFAULT")\n"
|
||
" -f [file] file to use for json tests (default is "JSON_TEST_FILE_DEFAULT")\n"
|
||
" -C [dir] chdir before running r2r (default follows executable symlink + test/new\n"
|
||
" -t [seconds] timeout per test (default is "TIMEOUT_DEFAULT_STR")\n"
|
||
"\n"
|
||
"Supported test types: @json @unit @fuzz @cmds\n"
|
||
"OS/Arch for archos tests: "R2R_ARCH_OS"\n");
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
static void path_left_free_kv(HtPPKv *kv) {
|
||
free (kv->value);
|
||
}
|
||
|
||
static bool r2r_chdir(const char *argv0) {
|
||
#if __UNIX__
|
||
if (r_file_is_directory ("db")) {
|
||
return true;
|
||
}
|
||
char src_path[PATH_MAX];
|
||
char *r2r_path = r_file_path (argv0);
|
||
bool found = false;
|
||
if (readlink (r2r_path, src_path, sizeof (src_path)) != -1) {
|
||
char *p = strstr (src_path, R_SYS_DIR "binr"R_SYS_DIR"r2r"R_SYS_DIR"r2r");
|
||
if (p) {
|
||
*p = 0;
|
||
strcat (src_path, R_SYS_DIR"test"R_SYS_DIR);
|
||
if (r_file_is_directory (src_path)) {
|
||
if (chdir (src_path) != -1) {
|
||
eprintf ("Running from %s\n", src_path);
|
||
found = true;
|
||
} else {
|
||
eprintf ("Cannot find '%s' directory\n", src_path);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
free (r2r_path);
|
||
return found;
|
||
#else
|
||
return false;
|
||
#endif
|
||
}
|
||
|
||
static bool r2r_test_run_unit(void) {
|
||
return system ("make -C unit all run") == 0;
|
||
}
|
||
|
||
static bool r2r_chdir_fromtest(const char *test_path) {
|
||
if (*test_path == '@') {
|
||
test_path = "";
|
||
}
|
||
char *abs_test_path = r_file_abspath (test_path);
|
||
if (!r_file_is_directory (abs_test_path)) {
|
||
char *last_slash = (char *)r_str_lchr (abs_test_path, R_SYS_DIR[0]);
|
||
if (last_slash) {
|
||
*last_slash = 0;
|
||
}
|
||
}
|
||
if (chdir (abs_test_path) == -1) {
|
||
free (abs_test_path);
|
||
return false;
|
||
}
|
||
free (abs_test_path);
|
||
bool found = false;
|
||
char *cwd = NULL;
|
||
char *old_cwd = NULL;
|
||
while (true) {
|
||
cwd = r_sys_getdir ();
|
||
if (old_cwd && !strcmp (old_cwd, cwd)) {
|
||
break;
|
||
}
|
||
if (r_file_is_directory ("test")) {
|
||
r_sys_chdir ("test");
|
||
if (r_file_is_directory ("db")) {
|
||
found = true;
|
||
eprintf ("Running from %s\n", cwd);
|
||
break;
|
||
}
|
||
r_sys_chdir ("..");
|
||
}
|
||
if (r_file_is_directory ("db")) {
|
||
found = true;
|
||
eprintf ("Running from %s\n", cwd);
|
||
break;
|
||
}
|
||
free (old_cwd);
|
||
old_cwd = cwd;
|
||
cwd = NULL;
|
||
if (chdir ("..") == -1) {
|
||
break;
|
||
}
|
||
}
|
||
free (old_cwd);
|
||
free (cwd);
|
||
return found;
|
||
}
|
||
|
||
int main(int argc, char **argv) {
|
||
int workers_count = WORKERS_DEFAULT;
|
||
bool verbose = false;
|
||
bool nothing = false;
|
||
bool quiet = false;
|
||
bool log_mode = false;
|
||
bool interactive = false;
|
||
char *radare2_cmd = NULL;
|
||
char *rasm2_cmd = NULL;
|
||
char *json_test_file = NULL;
|
||
char *fuzz_dir = NULL;
|
||
const char *r2r_dir = NULL;
|
||
ut64 timeout_sec = TIMEOUT_DEFAULT;
|
||
int ret = 0;
|
||
|
||
#if __WINDOWS__
|
||
UINT old_cp = GetConsoleOutputCP ();
|
||
{
|
||
HANDLE streams[] = { GetStdHandle (STD_OUTPUT_HANDLE), GetStdHandle (STD_ERROR_HANDLE) };
|
||
DWORD mode;
|
||
int i;
|
||
for (i = 0; i < R_ARRAY_SIZE (streams); i++) {
|
||
GetConsoleMode (streams[i], &mode);
|
||
SetConsoleMode (streams[i],
|
||
mode | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
|
||
}
|
||
}
|
||
#endif
|
||
|
||
RGetopt opt;
|
||
r_getopt_init (&opt, argc, (const char **)argv, "hqvj:r:m:f:C:LnVt:F:i");
|
||
|
||
int c;
|
||
while ((c = r_getopt_next (&opt)) != -1) {
|
||
switch (c) {
|
||
case 'h':
|
||
ret = help (true);
|
||
goto beach;
|
||
case 'q':
|
||
quiet = true;
|
||
break;
|
||
case 'v':
|
||
if (quiet) {
|
||
printf (R2_VERSION "\n");
|
||
} else {
|
||
char *s = r_str_version ("r2r");
|
||
printf ("%s\n", s);
|
||
free (s);
|
||
}
|
||
return 0;
|
||
case 'V':
|
||
verbose = true;
|
||
break;
|
||
case 'i':
|
||
interactive = true;
|
||
break;
|
||
case 'L':
|
||
log_mode = true;
|
||
break;
|
||
case 'F':
|
||
free (fuzz_dir);
|
||
fuzz_dir = strdup (opt.arg);
|
||
break;
|
||
case 'j':
|
||
workers_count = atoi (opt.arg);
|
||
if (workers_count <= 0) {
|
||
eprintf ("Invalid thread count\n");
|
||
ret = help (false);
|
||
goto beach;
|
||
}
|
||
break;
|
||
case 'r':
|
||
free (radare2_cmd);
|
||
radare2_cmd = strdup (opt.arg);
|
||
break;
|
||
case 'C':
|
||
r2r_dir = opt.arg;
|
||
break;
|
||
case 'n':
|
||
nothing = true;
|
||
break;
|
||
case 'm':
|
||
free (rasm2_cmd);
|
||
rasm2_cmd = strdup (opt.arg);
|
||
break;
|
||
case 'f':
|
||
free (json_test_file);
|
||
json_test_file = strdup (opt.arg);
|
||
break;
|
||
case 't':
|
||
timeout_sec = strtoull (opt.arg, NULL, 0);
|
||
if (!timeout_sec) {
|
||
timeout_sec = UT64_MAX;
|
||
}
|
||
break;
|
||
default:
|
||
ret = help (false);
|
||
goto beach;
|
||
}
|
||
}
|
||
|
||
char *cwd = r_sys_getdir ();
|
||
if (r2r_dir) {
|
||
if (chdir (r2r_dir) == -1) {
|
||
eprintf ("Cannot find %s directory.\n", r2r_dir);
|
||
return -1;
|
||
}
|
||
} else {
|
||
bool dir_found = (opt.ind < argc && argv[opt.ind][0] != '.')
|
||
? r2r_chdir_fromtest (argv[opt.ind])
|
||
: r2r_chdir (argv[0]);
|
||
if (!dir_found) {
|
||
eprintf ("Cannot find db/ directory related to the given test.\n");
|
||
return -1;
|
||
}
|
||
}
|
||
|
||
if (fuzz_dir) {
|
||
char *tmp = fuzz_dir;
|
||
fuzz_dir = r_file_abspath_rel (cwd, fuzz_dir);
|
||
free (tmp);
|
||
}
|
||
|
||
if (!r2r_subprocess_init ()) {
|
||
eprintf ("Subprocess init failed\n");
|
||
return -1;
|
||
}
|
||
atexit (r2r_subprocess_fini);
|
||
|
||
ut64 time_start = r_sys_now ();
|
||
R2RState state = {{0}};
|
||
state.run_config.r2_cmd = radare2_cmd ? radare2_cmd : RADARE2_CMD_DEFAULT;
|
||
state.run_config.rasm2_cmd = rasm2_cmd ? rasm2_cmd : RASM2_CMD_DEFAULT;
|
||
state.run_config.json_test_file = json_test_file ? json_test_file : JSON_TEST_FILE_DEFAULT;
|
||
state.run_config.timeout_ms = timeout_sec > UT64_MAX / 1000 ? UT64_MAX : timeout_sec * 1000;
|
||
state.verbose = verbose;
|
||
state.db = r2r_test_database_new ();
|
||
if (!state.db) {
|
||
return -1;
|
||
}
|
||
r_pvector_init (&state.queue, NULL);
|
||
r_pvector_init (&state.results, (RPVectorFree)r2r_test_result_info_free);
|
||
r_pvector_init (&state.completed_paths, NULL);
|
||
state.lock = r_th_lock_new (false);
|
||
if (!state.lock) {
|
||
return -1;
|
||
}
|
||
state.cond = r_th_cond_new ();
|
||
if (!state.cond) {
|
||
return -1;
|
||
}
|
||
|
||
if (opt.ind < argc) {
|
||
// Manually specified path(s)
|
||
int i;
|
||
for (i = opt.ind; i < argc; i++) {
|
||
const char *arg = argv[i];
|
||
if (*arg == '@') {
|
||
arg++;
|
||
eprintf ("Category: %s\n", arg);
|
||
if (!strcmp (arg, "unit")) {
|
||
if (!r2r_test_run_unit ()) {
|
||
return -1;
|
||
}
|
||
continue;
|
||
} else if (!strcmp (arg, "fuzz")) {
|
||
if (!fuzz_dir) {
|
||
eprintf ("No fuzz dir given. Use -F [dir]\n");
|
||
return -1;
|
||
}
|
||
if (!r2r_test_database_load_fuzz (state.db, fuzz_dir)) {
|
||
eprintf ("Failed to load fuzz tests from \"%s\"\n", fuzz_dir);
|
||
}
|
||
continue;
|
||
} else if (!strcmp (arg, "json")) {
|
||
arg = "db/json";
|
||
} else if (!strcmp (arg, "dasm")) {
|
||
arg = "db/asm";
|
||
} else if (!strcmp (arg, "cmds")) {
|
||
arg = "db";
|
||
} else {
|
||
arg = r_str_newf ("db/%s", arg + 1);
|
||
}
|
||
}
|
||
char *tf = r_file_abspath_rel (cwd, arg);
|
||
if (!tf || !r2r_test_database_load (state.db, tf)) {
|
||
eprintf ("Failed to load tests from \"%s\"\n", tf);
|
||
r2r_test_database_free (state.db);
|
||
free (tf);
|
||
return -1;
|
||
}
|
||
free (tf);
|
||
}
|
||
} else {
|
||
// Default db path
|
||
if (!r2r_test_database_load (state.db, "db")) {
|
||
eprintf ("Failed to load tests from ./db\n");
|
||
r2r_test_database_free (state.db);
|
||
return -1;
|
||
}
|
||
if (fuzz_dir && !r2r_test_database_load_fuzz (state.db, fuzz_dir)) {
|
||
eprintf ("Failed to load fuzz tests from \"%s\"\n", fuzz_dir);
|
||
}
|
||
}
|
||
|
||
R_FREE (cwd);
|
||
uint32_t loaded_tests = r_pvector_len (&state.db->tests);
|
||
printf ("Loaded %u tests.\n", loaded_tests);
|
||
if (nothing) {
|
||
goto coast;
|
||
}
|
||
|
||
bool jq_available = r2r_check_jq_available ();
|
||
if (!jq_available) {
|
||
eprintf ("Skipping json tests because jq is not available.\n");
|
||
size_t i;
|
||
for (i = 0; i < r_pvector_len (&state.db->tests);) {
|
||
R2RTest *test = r_pvector_at (&state.db->tests, i);
|
||
if (test->type == R2R_TEST_TYPE_JSON) {
|
||
r2r_test_free (test);
|
||
r_pvector_remove_at (&state.db->tests, i);
|
||
continue;
|
||
}
|
||
i++;
|
||
}
|
||
}
|
||
|
||
r_pvector_insert_range (&state.queue, 0, state.db->tests.v.a, r_pvector_len (&state.db->tests));
|
||
|
||
if (log_mode) {
|
||
// Log mode prints the state after every completed file.
|
||
// The count of tests left per file is stored in a ht.
|
||
state.path_left = ht_pp_new (NULL, path_left_free_kv, NULL);
|
||
if (state.path_left) {
|
||
void **it;
|
||
r_pvector_foreach (&state.queue, it) {
|
||
R2RTest *test = *it;
|
||
ut64 *count = ht_pp_find (state.path_left, test->path, NULL);
|
||
if (!count) {
|
||
count = malloc (sizeof (ut64));
|
||
*count = 0;
|
||
ht_pp_insert (state.path_left, test->path, count);
|
||
}
|
||
(*count)++;
|
||
}
|
||
}
|
||
}
|
||
|
||
r_th_lock_enter (state.lock);
|
||
|
||
RPVector workers;
|
||
r_pvector_init (&workers, NULL);
|
||
int i;
|
||
for (i = 0; i < workers_count; i++) {
|
||
RThread *th = r_th_new (worker_th, &state, 0);
|
||
if (!th) {
|
||
eprintf ("Failed to start thread.\n");
|
||
exit (-1);
|
||
}
|
||
r_pvector_push (&workers, th);
|
||
}
|
||
|
||
ut64 prev_completed = UT64_MAX;
|
||
ut64 prev_paths_completed = 0;
|
||
while (true) {
|
||
ut64 completed = (ut64)r_pvector_len (&state.results);
|
||
if (log_mode) {
|
||
print_log (&state, prev_completed, prev_paths_completed);
|
||
} else if (completed != prev_completed) {
|
||
print_state (&state, prev_completed);
|
||
}
|
||
prev_completed = completed;
|
||
prev_paths_completed = (ut64)r_pvector_len (&state.completed_paths);
|
||
if (completed == r_pvector_len (&state.db->tests)) {
|
||
break;
|
||
}
|
||
r_th_cond_wait (state.cond, state.lock);
|
||
}
|
||
|
||
r_th_lock_leave (state.lock);
|
||
|
||
printf ("\n");
|
||
|
||
void **it;
|
||
r_pvector_foreach (&workers, it) {
|
||
RThread *th = *it;
|
||
r_th_wait (th);
|
||
r_th_free (th);
|
||
}
|
||
r_pvector_clear (&workers);
|
||
|
||
ut64 seconds = (r_sys_now () - time_start) / 1000000;
|
||
printf ("Finished in");
|
||
if (seconds > 60) {
|
||
ut64 minutes = seconds / 60;
|
||
printf (" %"PFMT64d" minutes and", seconds / 60);
|
||
seconds -= (minutes * 60);
|
||
}
|
||
printf (" %"PFMT64d" seconds.\n", seconds % 60);
|
||
|
||
if (interactive) {
|
||
interact (&state);
|
||
}
|
||
|
||
if (state.xx_count) {
|
||
ret = 1;
|
||
}
|
||
|
||
coast:
|
||
r_pvector_clear (&state.queue);
|
||
r_pvector_clear (&state.results);
|
||
r_pvector_clear (&state.completed_paths);
|
||
r2r_test_database_free (state.db);
|
||
r_th_lock_free (state.lock);
|
||
r_th_cond_free (state.cond);
|
||
beach:
|
||
free (radare2_cmd);
|
||
free (rasm2_cmd);
|
||
free (json_test_file);
|
||
free (fuzz_dir);
|
||
#if __WINDOWS__
|
||
if (old_cp) {
|
||
(void)SetConsoleOutputCP (old_cp);
|
||
// chcp doesn't pick up the code page switch for some reason
|
||
(void)r_sys_cmdf ("chcp %u > NUL", old_cp);
|
||
}
|
||
#endif
|
||
return ret;
|
||
}
|
||
|
||
static RThreadFunctionRet worker_th(RThread *th) {
|
||
R2RState *state = th->user;
|
||
r_th_lock_enter (state->lock);
|
||
while (true) {
|
||
if (r_pvector_empty (&state->queue)) {
|
||
break;
|
||
}
|
||
R2RTest *test = r_pvector_pop (&state->queue);
|
||
r_th_lock_leave (state->lock);
|
||
|
||
R2RTestResultInfo *result = r2r_run_test (&state->run_config, test);
|
||
|
||
r_th_lock_enter (state->lock);
|
||
r_pvector_push (&state->results, result);
|
||
switch (result->result) {
|
||
case R2R_TEST_RESULT_OK:
|
||
state->ok_count++;
|
||
break;
|
||
case R2R_TEST_RESULT_FAILED:
|
||
state->xx_count++;
|
||
break;
|
||
case R2R_TEST_RESULT_BROKEN:
|
||
state->br_count++;
|
||
break;
|
||
case R2R_TEST_RESULT_FIXED:
|
||
state->fx_count++;
|
||
break;
|
||
}
|
||
if (state->path_left) {
|
||
ut64 *count = ht_pp_find (state->path_left, test->path, NULL);
|
||
if (count) {
|
||
(*count)--;
|
||
if (!*count) {
|
||
r_pvector_push (&state->completed_paths, (void *)test->path);
|
||
}
|
||
}
|
||
}
|
||
r_th_cond_signal (state->cond);
|
||
}
|
||
r_th_lock_leave (state->lock);
|
||
return R_TH_STOP;
|
||
}
|
||
|
||
typedef enum {
|
||
R2R_ALIGN_MATCH, R2R_ALIGN_MISMATCH, R2R_ALIGN_TOP_GAP, R2R_ALIGN_BOTTOM_GAP
|
||
} R2RCharAlignment;
|
||
|
||
typedef enum {
|
||
R2R_DIFF_MATCH, R2R_DIFF_DELETE, R2R_DIFF_INSERT
|
||
} R2RPrintDiffMode;
|
||
|
||
static void print_diff(const char *actual, const char *expected, bool diffchar) {
|
||
RDiff *d = r_diff_new ();
|
||
#ifdef __WINDOWS__
|
||
d->diff_cmd = "git diff --no-index";
|
||
#endif
|
||
const size_t len_expected = strlen (expected);
|
||
const size_t len_actual = strlen (actual);
|
||
if (diffchar) {
|
||
// Use Needleman–Wunsch to diffchar.
|
||
// This is an O(mn) algo in both space and time.
|
||
// Note that 64KB * 64KB * 2 = 8GB.
|
||
// TODO Discard common prefix and suffix
|
||
const size_t len_long = len_expected > len_actual ? len_expected : len_actual;
|
||
const size_t dim = len_long + 1;
|
||
ut8 *dup_expected = malloc (len_long);
|
||
ut8 *dup_actual = malloc (len_long);
|
||
st16 *align_table = malloc (dim * dim * sizeof (st16));
|
||
ut8 *align_expected = malloc (2 * len_long);
|
||
ut8 *align_actual = malloc (2 * len_long);
|
||
if (dup_expected && dup_actual && align_table && align_expected && align_actual) {
|
||
// Copy strings (note that strncpy does pad with nulls)
|
||
strncpy (dup_expected, expected, len_long);
|
||
expected = dup_expected;
|
||
strncpy (dup_actual, actual, len_long);
|
||
actual = dup_actual;
|
||
// Fill table
|
||
size_t row, col;
|
||
*align_table = 0;
|
||
for (row = 1; row < dim; row++) {
|
||
// TODO Clamping [ST16_MIN + 1, .]
|
||
*(align_table + row) = *(align_table + row * dim) = -(st16)row;
|
||
}
|
||
const st16 match = 1;
|
||
const st16 match_nl = 2;
|
||
const st16 mismatch = -2;
|
||
const st16 gap = -1;
|
||
for (row = 1; row < dim; row++) {
|
||
for (col = 1; col < dim; col++) {
|
||
// TODO Clamping [ST16_MIN + 1, ST16_MAX]
|
||
const ut8 expected_ch = expected[col - 1];
|
||
const ut8 actual_ch = actual[row - 1];
|
||
const st16 tl_score = *(align_table + (row - 1) * dim + col - 1)
|
||
+ (expected_ch == actual_ch ?
|
||
(expected_ch == '\n' ? match_nl : match) :
|
||
mismatch);
|
||
const st16 t_score = *(align_table + (row - 1) * dim + col) + gap;
|
||
const st16 l_score = *(align_table + row * dim + col - 1) + gap;
|
||
st16 score;
|
||
if (tl_score >= t_score && tl_score >= l_score) {
|
||
score = tl_score;
|
||
} else if (t_score >= tl_score && t_score >= l_score) {
|
||
score = t_score;
|
||
} else {
|
||
score = l_score;
|
||
}
|
||
*(align_table + row * dim + col) = score;
|
||
}
|
||
}
|
||
#if 0
|
||
// Print table (Debug)
|
||
char char_str[3] = { ' ' };
|
||
printf ("%4s ", char_str);
|
||
for (col = 0; col < dim; col++) {
|
||
if (col && expected[col - 1] == '\n') {
|
||
char_str[0] = '\\';
|
||
char_str[1] = 'n';
|
||
} else {
|
||
char_str[0] = col ? expected[col - 1] : ' ';
|
||
char_str[1] = 0;
|
||
}
|
||
printf ("%4s ", char_str);
|
||
}
|
||
printf ("\n");
|
||
for (row = 0; row < dim; row++) {
|
||
if (row && actual[row - 1] == '\n') {
|
||
char_str[0] = '\\';
|
||
char_str[1] = 'n';
|
||
} else {
|
||
char_str[0] = row ? actual[row - 1] : ' ';
|
||
char_str[1] = 0;
|
||
}
|
||
printf ("%4s ", char_str);
|
||
for (col = 0; col < dim; col++) {
|
||
printf ("%4d ", *(align_table + row * dim + col));
|
||
}
|
||
printf ("\n");
|
||
}
|
||
#endif
|
||
// Do alignment
|
||
size_t idx_expected = len_long - 1;
|
||
size_t idx_actual = len_long - 1;
|
||
size_t idx_align = 2 * len_long - 1;
|
||
size_t pos_row = dim - 1;
|
||
size_t pos_col = dim - 1;
|
||
while (pos_row || pos_col) {
|
||
const st16 tl_score = (pos_row > 0 && pos_col > 0) ?
|
||
*(align_table + (pos_row - 1) * dim + pos_col - 1) :
|
||
ST16_MIN;
|
||
const st16 t_score = pos_row > 0 ?
|
||
*(align_table + (pos_row - 1) * dim + pos_col) :
|
||
ST16_MIN;
|
||
const st16 l_score = pos_col > 0 ?
|
||
*(align_table + pos_row * dim + pos_col - 1) :
|
||
ST16_MIN;
|
||
const bool match = expected[idx_expected] == actual[idx_actual];
|
||
if (t_score >= l_score && (!match || t_score >= tl_score)) {
|
||
align_expected[idx_align] = 0;
|
||
align_actual[idx_align] = actual[idx_actual--];
|
||
idx_align--;
|
||
pos_row--;
|
||
} else if (l_score >= t_score && (!match || l_score >= tl_score)) {
|
||
align_expected[idx_align] = expected[idx_expected--];
|
||
align_actual[idx_align] = 0;
|
||
idx_align--;
|
||
pos_col--;
|
||
} else {
|
||
align_expected[idx_align] = expected[idx_expected--];
|
||
align_actual[idx_align] = actual[idx_actual--];
|
||
idx_align--;
|
||
pos_row--;
|
||
pos_col--;
|
||
}
|
||
}
|
||
idx_align++;
|
||
const size_t start_align = idx_align;
|
||
#if 0
|
||
// Print alignment (Debug)
|
||
for (; idx_align < 2 * len_long; idx_align++) {
|
||
const ut8 ch = align_expected[idx_align];
|
||
if (align_actual[idx_align] == '\n' && ch != '\n') {
|
||
printf (ch ? " " : "-");
|
||
}
|
||
if (ch == 0) {
|
||
printf ("-");
|
||
} else if (ch == '\n') {
|
||
printf ("\\n");
|
||
} else {
|
||
printf ("%c", ch);
|
||
}
|
||
}
|
||
printf ("\n");
|
||
for (idx_align = start_align; idx_align < 2 * len_long; idx_align++) {
|
||
const ut8 ch = align_actual[idx_align];
|
||
if (align_expected[idx_align] == '\n' && ch != '\n') {
|
||
printf (ch ? " " : "-");
|
||
}
|
||
if (ch == 0) {
|
||
printf ("-");
|
||
} else if (ch == '\n') {
|
||
printf ("\\n");
|
||
} else {
|
||
printf ("%c", ch);
|
||
}
|
||
}
|
||
printf ("\n");
|
||
#endif
|
||
// Print diff
|
||
R2RPrintDiffMode cur_mode = R2R_DIFF_MATCH;
|
||
R2RCharAlignment cur_align;
|
||
idx_align = start_align;
|
||
while (idx_align < 2 * len_long) {
|
||
const ut8 expected_ch = align_expected[idx_align];
|
||
const ut8 actual_ch = align_actual[idx_align];
|
||
if (expected_ch && !actual_ch) {
|
||
cur_align = R2R_ALIGN_BOTTOM_GAP;
|
||
} else if (!expected_ch && actual_ch) {
|
||
cur_align = R2R_ALIGN_TOP_GAP;
|
||
} else if (expected_ch != actual_ch) {
|
||
eprintf ("Internal error: mismatch detected!\n");
|
||
cur_align = R2R_ALIGN_MISMATCH;
|
||
} else {
|
||
cur_align = R2R_ALIGN_MATCH;
|
||
}
|
||
if (cur_mode == R2R_DIFF_MATCH) {
|
||
if (cur_align == R2R_ALIGN_MATCH) {
|
||
if (expected_ch) {
|
||
printf ("%c", expected_ch);
|
||
}
|
||
} else if (cur_align == R2R_ALIGN_BOTTOM_GAP) {
|
||
printf (expected_ch == '\n' ?
|
||
"%c"Color_HLDELETE :
|
||
Color_HLDELETE"%c", expected_ch);
|
||
cur_mode = R2R_DIFF_DELETE;
|
||
} else if (cur_align == R2R_ALIGN_TOP_GAP) {
|
||
printf (actual_ch == '\n' ?
|
||
"%c"Color_HLINSERT :
|
||
Color_HLINSERT"%c", actual_ch);
|
||
cur_mode = R2R_DIFF_INSERT;
|
||
}
|
||
} else if (cur_mode == R2R_DIFF_DELETE) {
|
||
if (cur_align == R2R_ALIGN_MATCH) {
|
||
printf (Color_RESET);
|
||
if (expected_ch) {
|
||
printf ("%c", expected_ch);
|
||
}
|
||
cur_mode = R2R_DIFF_MATCH;
|
||
} else if (cur_align == R2R_ALIGN_BOTTOM_GAP) {
|
||
printf (expected_ch == '\n' ?
|
||
Color_RESET"%c"Color_HLDELETE :
|
||
"%c", expected_ch);
|
||
} else if (cur_align == R2R_ALIGN_TOP_GAP) {
|
||
printf (actual_ch == '\n' ?
|
||
Color_RESET"%c"Color_HLINSERT :
|
||
Color_HLINSERT"%c", actual_ch);
|
||
cur_mode = R2R_DIFF_INSERT;
|
||
}
|
||
} else if (cur_mode == R2R_DIFF_INSERT) {
|
||
if (cur_align == R2R_ALIGN_MATCH) {
|
||
printf (Color_RESET);
|
||
if (expected_ch) {
|
||
printf ("%c", expected_ch);
|
||
}
|
||
cur_mode = R2R_DIFF_MATCH;
|
||
} else if (cur_align == R2R_ALIGN_BOTTOM_GAP) {
|
||
printf (expected_ch == '\n' ?
|
||
Color_RESET"%c"Color_HLDELETE :
|
||
Color_HLDELETE"%c", expected_ch);
|
||
cur_mode = R2R_DIFF_DELETE;
|
||
} else if (cur_align == R2R_ALIGN_TOP_GAP) {
|
||
printf (actual_ch == '\n' ?
|
||
Color_RESET"%c"Color_HLINSERT :
|
||
"%c", actual_ch);
|
||
}
|
||
}
|
||
idx_align++;
|
||
}
|
||
printf (Color_RESET"\n");
|
||
free (dup_expected);
|
||
free (dup_actual);
|
||
free (align_table);
|
||
free (align_expected);
|
||
free (align_actual);
|
||
return;
|
||
}
|
||
d->diff_cmd = "git diff --no-index --word-diff=porcelain --word-diff-regex=.";
|
||
}
|
||
char *uni = r_diff_buffers_to_string (d, (const ut8 *)expected, (int)len_expected, (const ut8 *)actual, (int)len_actual);
|
||
r_diff_free (d);
|
||
|
||
RList *lines = r_str_split_duplist (uni, "\n", false);
|
||
RListIter *it;
|
||
char *line;
|
||
bool header_found = false;
|
||
r_list_foreach (lines, it, line) {
|
||
if (!header_found) {
|
||
if (r_str_startswith (line, "+++ ")) {
|
||
header_found = true;
|
||
}
|
||
continue;
|
||
}
|
||
if (r_str_startswith (line, "@@ ") && r_str_endswith (line, " @@")) {
|
||
printf ("%s%s%s\n", Color_CYAN, line, Color_RESET);
|
||
continue;
|
||
}
|
||
bool color = true;
|
||
char c = *line;
|
||
switch (c) {
|
||
case '+':
|
||
printf ("%s"Color_INSERT, diffchar ? Color_BGINSERT : "");
|
||
break;
|
||
case '-':
|
||
printf ("%s"Color_DELETE, diffchar ? Color_BGDELETE : "");
|
||
break;
|
||
case '~': // can't happen if !diffchar
|
||
printf ("\n");
|
||
continue;
|
||
default:
|
||
color = false;
|
||
break;
|
||
}
|
||
if (diffchar) {
|
||
printf ("%s", *line ? line + 1 : "");
|
||
} else {
|
||
printf ("%s\n", line);
|
||
}
|
||
if (color) {
|
||
printf ("%s", Color_RESET);
|
||
}
|
||
}
|
||
r_list_free (lines);
|
||
free (uni);
|
||
printf ("\n");
|
||
}
|
||
|
||
static R2RProcessOutput *print_runner(const char *file, const char *args[], size_t args_size,
|
||
const char *envvars[], const char *envvals[], size_t env_size, void *user) {
|
||
size_t i;
|
||
for (i = 0; i < env_size; i++) {
|
||
printf ("%s=%s ", envvars[i], envvals[i]);
|
||
}
|
||
printf ("%s", file);
|
||
for (i = 0; i < args_size; i++) {
|
||
const char *str = args[i];
|
||
if (strpbrk (str, "\n \'\"")) {
|
||
printf (" '%s'", str); // TODO: escape
|
||
} else {
|
||
printf (" %s", str);
|
||
}
|
||
}
|
||
printf ("\n");
|
||
return NULL;
|
||
}
|
||
|
||
static void print_result_diff(R2RRunConfig *config, R2RTestResultInfo *result) {
|
||
if (result->run_failed) {
|
||
printf (Color_RED "RUN FAILED (e.g. wrong radare2 path)" Color_RESET "\n");
|
||
return;
|
||
}
|
||
switch (result->test->type) {
|
||
case R2R_TEST_TYPE_CMD: {
|
||
r2r_run_cmd_test (config, result->test->cmd_test, print_runner, NULL);
|
||
const char *expect = result->test->cmd_test->expect.value;
|
||
if (expect && strcmp (result->proc_out->out, expect)) {
|
||
printf ("-- stdout\n");
|
||
print_diff (result->proc_out->out, expect, false);
|
||
}
|
||
expect = result->test->cmd_test->expect_err.value;
|
||
const char *err = result->proc_out->err;
|
||
if (expect && strcmp (err, expect)) {
|
||
printf ("-- stderr\n");
|
||
print_diff (err, expect, false);
|
||
} else if (*err) {
|
||
printf ("-- stderr\n%s\n", err);
|
||
}
|
||
if (result->proc_out->ret != 0) {
|
||
printf ("-- exit status: "Color_RED"%d"Color_RESET"\n", result->proc_out->ret);
|
||
}
|
||
break;
|
||
}
|
||
case R2R_TEST_TYPE_ASM:
|
||
// TODO
|
||
break;
|
||
case R2R_TEST_TYPE_JSON:
|
||
break;
|
||
case R2R_TEST_TYPE_FUZZ:
|
||
r2r_run_fuzz_test (config, result->test->fuzz_test, print_runner, NULL);
|
||
printf ("-- stdout\n%s\n", result->proc_out->out);
|
||
printf ("-- stderr\n%s\n", result->proc_out->err);
|
||
printf ("-- exit status: "Color_RED"%d"Color_RESET"\n", result->proc_out->ret);
|
||
break;
|
||
}
|
||
}
|
||
|
||
static void print_new_results(R2RState *state, ut64 prev_completed) {
|
||
// Detailed test result (with diff if necessary)
|
||
ut64 completed = (ut64)r_pvector_len (&state->results);
|
||
ut64 i;
|
||
for (i = prev_completed; i < completed; i++) {
|
||
R2RTestResultInfo *result = r_pvector_at (&state->results, (size_t)i);
|
||
if (!state->verbose && (result->result == R2R_TEST_RESULT_OK || result->result == R2R_TEST_RESULT_FIXED || result->result == R2R_TEST_RESULT_BROKEN)) {
|
||
continue;
|
||
}
|
||
char *name = r2r_test_name (result->test);
|
||
if (!name) {
|
||
continue;
|
||
}
|
||
printf ("\n"R_CONS_CURSOR_UP R_CONS_CLEAR_LINE);
|
||
switch (result->result) {
|
||
case R2R_TEST_RESULT_OK:
|
||
printf (Color_GREEN"[OK]"Color_RESET);
|
||
break;
|
||
case R2R_TEST_RESULT_FAILED:
|
||
printf (Color_RED"[XX]"Color_RESET);
|
||
break;
|
||
case R2R_TEST_RESULT_BROKEN:
|
||
printf (Color_BLUE"[BR]"Color_RESET);
|
||
break;
|
||
case R2R_TEST_RESULT_FIXED:
|
||
printf (Color_CYAN"[FX]"Color_RESET);
|
||
break;
|
||
}
|
||
if (result->timeout) {
|
||
printf (Color_CYAN" TIMEOUT"Color_RESET);
|
||
}
|
||
printf (" %s "Color_YELLOW"%s"Color_RESET"\n", result->test->path, name);
|
||
if (result->result == R2R_TEST_RESULT_FAILED || (state->verbose && result->result == R2R_TEST_RESULT_BROKEN)) {
|
||
print_result_diff (&state->run_config, result);
|
||
}
|
||
free (name);
|
||
}
|
||
}
|
||
|
||
static void print_state_counts(R2RState *state) {
|
||
printf ("%8"PFMT64u" OK %8"PFMT64u" BR %8"PFMT64u" XX %8"PFMT64u" FX",
|
||
state->ok_count, state->br_count, state->xx_count, state->fx_count);
|
||
}
|
||
|
||
static void print_state(R2RState *state, ut64 prev_completed) {
|
||
#if __WINDOWS__
|
||
setvbuf (stdout, NULL, _IOFBF, 8192);
|
||
#endif
|
||
print_new_results (state, prev_completed);
|
||
|
||
// [x/x] OK 42 BR 0 ...
|
||
printf (R_CONS_CLEAR_LINE);
|
||
int w = printf ("[%"PFMT64u"/%"PFMT64u"]", (ut64)r_pvector_len (&state->results), (ut64)r_pvector_len (&state->db->tests));
|
||
while (w >= 0 && w < 20) {
|
||
printf (" ");
|
||
w++;
|
||
}
|
||
printf (" ");
|
||
print_state_counts (state);
|
||
fflush (stdout);
|
||
#if __WINDOWS__
|
||
setvbuf (stdout, NULL, _IONBF, 0);
|
||
#endif
|
||
}
|
||
|
||
static void print_log(R2RState *state, ut64 prev_completed, ut64 prev_paths_completed) {
|
||
print_new_results (state, prev_completed);
|
||
ut64 paths_completed = r_pvector_len (&state->completed_paths);
|
||
for (; prev_paths_completed < paths_completed; prev_paths_completed++) {
|
||
printf ("[**] %50s ", (const char *)r_pvector_at (&state->completed_paths, prev_paths_completed));
|
||
print_state_counts (state);
|
||
printf ("\n");
|
||
}
|
||
}
|
||
|
||
static void interact(R2RState *state) {
|
||
void **it;
|
||
RPVector failed_results;
|
||
r_pvector_init (&failed_results, NULL);
|
||
r_pvector_foreach (&state->results, it) {
|
||
R2RTestResultInfo *result = *it;
|
||
if (result->result == R2R_TEST_RESULT_FAILED) {
|
||
r_pvector_push (&failed_results, result);
|
||
}
|
||
}
|
||
if (r_pvector_empty (&failed_results)) {
|
||
goto beach;
|
||
}
|
||
|
||
#if __WINDOWS__
|
||
(void)SetConsoleOutputCP (65001); // UTF-8
|
||
#endif
|
||
printf ("\n");
|
||
printf ("#####################\n");
|
||
printf (" %"PFMT64u" failed test(s) "UTF8_POLICE_CARS_REVOLVING_LIGHT"\n",
|
||
(ut64)r_pvector_len (&failed_results));
|
||
|
||
r_pvector_foreach (&failed_results, it) {
|
||
R2RTestResultInfo *result = *it;
|
||
if (result->test->type != R2R_TEST_TYPE_CMD) {
|
||
// TODO: other types of tests
|
||
continue;
|
||
}
|
||
|
||
printf ("#####################\n\n");
|
||
print_result_diff (&state->run_config, result);
|
||
menu:
|
||
printf ("Wat do? "
|
||
"(f)ix "UTF8_WHITE_HEAVY_CHECK_MARK UTF8_VS16 UTF8_VS16 UTF8_VS16" "
|
||
"(i)gnore "UTF8_SEE_NO_EVIL_MONKEY" "
|
||
"(b)roken "UTF8_SKULL_AND_CROSSBONES UTF8_VS16 UTF8_VS16 UTF8_VS16" "
|
||
"(c)ommands "UTF8_KEYBOARD UTF8_VS16" "
|
||
"(d)iffchar "UTF8_LEFT_POINTING_MAGNIFYING_GLASS" "
|
||
"(q)uit "UTF8_DOOR"\n");
|
||
printf ("> ");
|
||
char buf[0x30];
|
||
if (!fgets (buf, sizeof (buf), stdin)) {
|
||
break;
|
||
}
|
||
if (strlen (buf) != 2) {
|
||
goto menu;
|
||
}
|
||
switch (buf[0]) {
|
||
case 'f':
|
||
if (result->run_failed || result->proc_out->ret != 0) {
|
||
printf ("This test has failed too hard to be fixed.\n");
|
||
goto menu;
|
||
}
|
||
interact_fix (result, &failed_results);
|
||
break;
|
||
case 'i':
|
||
break;
|
||
case 'b':
|
||
interact_break (result, &failed_results);
|
||
break;
|
||
case 'c':
|
||
interact_commands (result, &failed_results);
|
||
break;
|
||
case 'd':
|
||
interact_diffchar (result);
|
||
goto menu;
|
||
case 'q':
|
||
goto beach;
|
||
default:
|
||
goto menu;
|
||
}
|
||
}
|
||
|
||
beach:
|
||
r_pvector_clear (&failed_results);
|
||
}
|
||
|
||
static char *format_cmd_kv(const char *key, const char *val) {
|
||
RStrBuf buf;
|
||
r_strbuf_init (&buf);
|
||
r_strbuf_appendf (&buf, "%s=", key);
|
||
if (strchr (val, '\n')) {
|
||
r_strbuf_appendf (&buf, "<<EOF\n%sEOF", val);
|
||
} else {
|
||
r_strbuf_append (&buf, val);
|
||
}
|
||
return r_strbuf_drain_nofree (&buf);
|
||
}
|
||
|
||
static char *replace_lines(const char *src, size_t from, size_t to, const char *news) {
|
||
const char *begin = src;
|
||
size_t line = 1;
|
||
while (line < from) {
|
||
begin = strchr (begin, '\n');
|
||
if (!begin) {
|
||
break;
|
||
}
|
||
begin++;
|
||
line++;
|
||
}
|
||
if (!begin) {
|
||
return NULL;
|
||
}
|
||
|
||
const char *end = begin;
|
||
while (line < to) {
|
||
end = strchr (end, '\n');
|
||
if (!end) {
|
||
break;
|
||
}
|
||
end++;
|
||
line++;
|
||
}
|
||
|
||
RStrBuf buf;
|
||
r_strbuf_init (&buf);
|
||
r_strbuf_append_n (&buf, src, begin - src);
|
||
r_strbuf_append (&buf, news);
|
||
r_strbuf_append (&buf, "\n");
|
||
if (end) {
|
||
r_strbuf_append (&buf, end);
|
||
}
|
||
return r_strbuf_drain_nofree (&buf);
|
||
}
|
||
|
||
// After editing a test, fix the line numbers previously saved for all the other tests
|
||
static void fixup_tests(RPVector *results, const char *edited_file, ut64 start_line, st64 delta) {
|
||
void **it;
|
||
r_pvector_foreach (results, it) {
|
||
R2RTestResultInfo *result = *it;
|
||
if (result->test->type != R2R_TEST_TYPE_CMD) {
|
||
continue;
|
||
}
|
||
if (result->test->path != edited_file) { // this works because all the paths come from the string pool
|
||
continue;
|
||
}
|
||
R2RCmdTest *test = result->test->cmd_test;
|
||
test->run_line += delta;
|
||
|
||
#define DO_KEY_STR(key, field) \
|
||
if (test->field.value) { \
|
||
if (test->field.line_begin >= start_line) { \
|
||
test->field.line_begin += delta; \
|
||
} \
|
||
if (test->field.line_end >= start_line) { \
|
||
test->field.line_end += delta; \
|
||
} \
|
||
}
|
||
|
||
#define DO_KEY_BOOL(key, field) \
|
||
if (test->field.set && test->field.line >= start_line) { \
|
||
test->field.line += delta; \
|
||
}
|
||
|
||
R2R_CMD_TEST_FOREACH_RECORD(DO_KEY_STR, DO_KEY_BOOL)
|
||
#undef DO_KEY_STR
|
||
#undef DO_KEY_BOOL
|
||
}
|
||
}
|
||
|
||
static char *replace_cmd_kv(const char *path, const char *content, size_t line_begin, size_t line_end, const char *key, const char *value, RPVector *fixup_results) {
|
||
char *kv = format_cmd_kv (key, value);
|
||
if (!kv) {
|
||
return NULL;
|
||
}
|
||
size_t kv_lines = r_str_char_count (kv, '\n') + 1;
|
||
char *newc = replace_lines (content, line_begin, line_end, kv);
|
||
free (kv);
|
||
if (!newc) {
|
||
return NULL;
|
||
}
|
||
size_t lines_before = line_end - line_begin;
|
||
st64 delta = (st64)kv_lines - (st64)lines_before;
|
||
if (line_end == line_begin) {
|
||
delta++;
|
||
}
|
||
fixup_tests (fixup_results, path, line_end, delta);
|
||
return newc;
|
||
}
|
||
|
||
static void replace_cmd_kv_file(const char *path, ut64 line_begin, ut64 line_end, const char *key, const char *value, RPVector *fixup_results) {
|
||
char *content = r_file_slurp (path, NULL);
|
||
if (!content) {
|
||
eprintf ("Failed to read file \"%s\"\n", path);
|
||
return;
|
||
}
|
||
char *newc = replace_cmd_kv (path, content, line_begin, line_end, key, value, fixup_results);
|
||
free (content);
|
||
if (!newc) {
|
||
return;
|
||
}
|
||
if (r_file_dump (path, (const ut8 *)newc, -1, false)) {
|
||
#if __UNIX__
|
||
sync ();
|
||
#endif
|
||
} else {
|
||
eprintf ("Failed to write file \"%s\"\n", path);
|
||
}
|
||
free (newc);
|
||
}
|
||
|
||
static void interact_fix(R2RTestResultInfo *result, RPVector *fixup_results) {
|
||
assert (result->test->type == R2R_TEST_TYPE_CMD);
|
||
R2RCmdTest *test = result->test->cmd_test;
|
||
R2RProcessOutput *out = result->proc_out;
|
||
if (test->expect.value && out->out) {
|
||
replace_cmd_kv_file (result->test->path, test->expect.line_begin, test->expect.line_end, "EXPECT", out->out, fixup_results);
|
||
}
|
||
if (test->expect_err.value && out->err) {
|
||
replace_cmd_kv_file (result->test->path, test->expect_err.line_begin, test->expect_err.line_end, "EXPECT_ERR", out->err, fixup_results);
|
||
}
|
||
}
|
||
|
||
static void interact_break(R2RTestResultInfo *result, RPVector *fixup_results) {
|
||
assert (result->test->type == R2R_TEST_TYPE_CMD);
|
||
R2RCmdTest *test = result->test->cmd_test;
|
||
ut64 line_begin;
|
||
ut64 line_end;
|
||
if (test->broken.set) {
|
||
line_begin = test->broken.set;
|
||
line_end = line_begin + 1;
|
||
} else {
|
||
line_begin = line_end = test->run_line;
|
||
}
|
||
replace_cmd_kv_file (result->test->path, line_begin, line_end, "BROKEN", "1", fixup_results);
|
||
}
|
||
|
||
static void interact_commands(R2RTestResultInfo *result, RPVector *fixup_results) {
|
||
assert (result->test->type == R2R_TEST_TYPE_CMD);
|
||
R2RCmdTest *test = result->test->cmd_test;
|
||
if (!test->cmds.value) {
|
||
return;
|
||
}
|
||
char *name = NULL;
|
||
int fd = r_file_mkstemp ("r2r-cmds", &name);
|
||
if (fd == -1) {
|
||
free (name);
|
||
eprintf ("Failed to open tmp file\n");
|
||
return;
|
||
}
|
||
size_t cmds_sz = strlen (test->cmds.value);
|
||
if (write (fd, test->cmds.value, cmds_sz) != cmds_sz) {
|
||
eprintf ("Failed to write to tmp file\n");
|
||
free (name);
|
||
close (fd);
|
||
return;
|
||
}
|
||
close (fd);
|
||
|
||
char *editor = r_sys_getenv ("EDITOR");
|
||
if (!editor || !*editor) {
|
||
free (editor);
|
||
editor = strdup ("vim");
|
||
if (!editor) {
|
||
free (name);
|
||
return;
|
||
}
|
||
}
|
||
r_sys_cmdf ("%s '%s'", editor, name);
|
||
free (editor);
|
||
|
||
char *newcmds = r_file_slurp (name, NULL);
|
||
if (!newcmds) {
|
||
eprintf ("Failed to read edited command file\n");
|
||
free (name);
|
||
return;
|
||
}
|
||
r_str_trim (newcmds);
|
||
|
||
// if it's multiline we want exactly one trailing newline
|
||
if (strchr (newcmds, '\n')) {
|
||
char *tmp = newcmds;
|
||
newcmds = r_str_newf ("%s\n", newcmds);
|
||
free (tmp);
|
||
if (!newcmds) {
|
||
free (name);
|
||
return;
|
||
}
|
||
}
|
||
|
||
replace_cmd_kv_file (result->test->path, test->cmds.line_begin, test->cmds.line_end, "CMDS", newcmds, fixup_results);
|
||
free (name);
|
||
free (newcmds);
|
||
}
|
||
|
||
static void interact_diffchar(R2RTestResultInfo *result) {
|
||
const char *actual = result->proc_out->out;
|
||
const char *expected = result->test->cmd_test->expect.value;
|
||
printf ("-- stdout\n");
|
||
print_diff (actual, expected, true);
|
||
}
|