blob: 3f5f3e41b33f2aee1c32acb84bb99197e5bb891e [file] [log] [blame]
/**
* Copyright (c) Zygmunt Krynicki <zygmunt.krynicki@linaro.org> 2012
**/
#include <assert.h>
#include <errno.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include "bundle.h"
#include "debug.h"
#include "json-format.h"
#include "json-test.h"
#include "text-utils.h"
#include "uuid.h"
/** Patterns used to discover gtest-like output */
#define GTEST_LOG1_PATTERN "[==========] "
#define GTEST_LOG2_PATTERN "[----------] "
#define GTEST_RUN_PATTERN "[ RUN ] "
#define GTEST_OK_PATTERN "[ OK ] "
#define GTEST_PASSED_PATTERN "[ PASSED ] "
#define GTEST_FAILED_PATTERN "[ FAILED ] "
enum gtest_line_type {
GTEST_LINE_LOG1,
GTEST_LINE_LOG2,
GTEST_LINE_RUN,
GTEST_LINE_OK,
GTEST_LINE_PASSED,
GTEST_LINE_FAILED,
GTEST_LINE_OTHER
};
static
void
process_output(
FILE *bundle_stream,
FILE *child_stream,
unsigned flags
)
{
char *line = NULL;
size_t line_capacity = 0;
char *line_sans_pattern = NULL;
int lineno = 0;
enum gtest_line_type line_type;
int test_run_cnt = 0;
char *previous_test_id = NULL;
size_t previous_test_id_capacity = 0;
int test_result_cnt = 0;
bool test_result_open = false;
const char *test_result_result = "unknown";
bool test_result_ready = false;
struct timeval test_result_start = { .tv_sec = 0, .tv_usec = 0};
struct timeval line_timestamp;
do {
/* Read a line from the child stream */
if (text_readline(&line, &line_capacity, child_stream) != 0) {
perror("text_readline()");
abort();
}
lineno += 1;
/* Measure the time, we may need it in several spots below */
gettimeofday(&line_timestamp, NULL);
/* We need to classify each line, there are a number of options:
* 0) status lines (either '=' or '-')
* 1) a 'RUN' line is where we start to run a test case
* 2) a 'OK' line is when the test passes
* 3) a 'FAILED' line is when the test has failed (but it must be
* paired with 'RUN' line just directly before.
* 4) other lines, arbitrary messages generated by the the case
* 5) a summary line like 'PASSED' or FAILED (we ignore those) */
if (strstr(line, GTEST_LOG1_PATTERN)) {
line_type = GTEST_LINE_LOG1;
line_sans_pattern = line + strlen(GTEST_LOG1_PATTERN);
} else if (strstr(line, GTEST_LOG2_PATTERN)) {
line_type = GTEST_LINE_LOG2;
line_sans_pattern = line + strlen(GTEST_LOG2_PATTERN);
} else if (strstr(line, GTEST_RUN_PATTERN)) {
line_type = GTEST_LINE_RUN;
line_sans_pattern = line + strlen(GTEST_RUN_PATTERN);
} else if (strstr(line, GTEST_OK_PATTERN)) {
line_type = GTEST_LINE_OK;
line_sans_pattern = line + strlen(GTEST_OK_PATTERN);
} else if (strstr(line, GTEST_PASSED_PATTERN)) {
line_type = GTEST_LINE_PASSED;
line_sans_pattern = line + strlen(GTEST_PASSED_PATTERN);
} else if (strstr(line, GTEST_FAILED_PATTERN)) {
line_type = GTEST_LINE_FAILED;
line_sans_pattern = line + strlen(GTEST_FAILED_PATTERN);
} else {
line_type = GTEST_LINE_OTHER;
line_sans_pattern = line;
}
/* Now that we know what kind of line we have we can run our simple state
* machine to react to the data */
switch (line_type) {
case GTEST_LINE_LOG1:
case GTEST_LINE_LOG2:
case GTEST_LINE_PASSED:
debug_log("message from gtest: %s\n", line_sans_pattern);
break;
case GTEST_LINE_FAILED:
/* FAILED line is printed when a test case fails
* and we need to keep track of that. Sadly, it is
* also printed as a 'summary' line at the end of a test run.
*
* To differentiate them we will treat any FAILED line that was
* printed after a RUN line was seen (which opens a test case)
* as the terminator */
if (test_result_open) {
debug_log("test case finished unsuccessfully\n");
/* This test case has failed */
test_result_result = "fail";
/* And we have enough data to print the test case footer */
test_result_ready = true;
} else
debug_log("message from gtest: %s\n", line_sans_pattern);
break;
case GTEST_LINE_RUN:
{
char *dot;
char *test_case_id;
char *test_id;
/* We need to terminate previous test case that has probably
* crashed bad enough not to print the failure record */
if (test_result_open) {
struct timeval duration;
duration.tv_sec = line_timestamp.tv_sec - test_result_start.tv_sec;
duration.tv_usec = line_timestamp.tv_usec - test_result_start.tv_usec;
debug_log("closing previous test result\n");
bundle_print_test_result_footer(
bundle_stream, flags,
test_result_result, "<stdout>", lineno, duration);
test_result_ready = false;
test_result_open = false;
}
/* Parse the RUN line. Try to find the test case id (it
* should be right after the dot component that separates
* test id from test case id */
dot = strchr(line_sans_pattern, '.');
if (dot != NULL) {
/* The test case has a dot component. We can use that to
* discover the test id and test case id. Let's just split
* it there by modifying the dot to a nul character */
*dot = 0;
test_id = line_sans_pattern;
test_case_id = dot + 1;
} else {
/* For some reason the test case did not have the dot that
* otherwise separates test id from test case id. */
test_id = "unknown-gtest-based-test";
test_case_id = line_sans_pattern;
}
/* We may need to print the test run header.
*
* This can happen when we see a change in test_id or when
* we need the initial header for the very first test case. */
if (previous_test_id == NULL || strcmp(previous_test_id, test_id) != 0) {
char analyzer_assigned_uuid[UUID_ASCII_LEN + 1];
const bool time_check_performed = false;
/* Close the previous test run if it is open */
if (test_run_cnt > 0) {
debug_log("closing previous test run\n");
bundle_print_test_run_footer(bundle_stream, flags);
}
/* Copy the old test_id so that we can keep comparing it */
if (text_copy(&previous_test_id,
&previous_test_id_capacity, test_id) != 0) {
perror("text_copy()");
abort();
}
/* Generate an UUID */
if (uuid_gen(analyzer_assigned_uuid) != 0) {
perror("uuid_gen()");
abort();
}
debug_log("generated analyzer_assigned_uuid: %s\n", analyzer_assigned_uuid);
/* Open another test run */
debug_log("starting new test run for test_id: %s\n", test_id);
bundle_print_test_run_header(
bundle_stream, flags, test_run_cnt > 0,
analyzer_assigned_uuid, line_timestamp.tv_sec,
time_check_performed, test_id);
/* Keep track of the number of test runs we have printed */
test_run_cnt += 1;
/* Reset the number of test cases we've printed in this test run */
test_result_cnt = 0;
}
debug_log("starting new test result for test_case_id: %s\n", test_case_id);
test_result_start = line_timestamp;
/* Print the header of the test result */
bundle_print_test_result_header(
bundle_stream, flags, test_result_cnt > 0, test_case_id,
test_result_start.tv_sec);
/* Keep track of subsequent test cases in one test run */
test_result_cnt += 1;
/* Keep track of output state */
test_result_open = true;
/* Reset result to unknown */
test_result_result = "unknown";
}
break;
case GTEST_LINE_OK:
debug_log("test case finished successfully\n");
/* This test case has passed */
test_result_result = "pass";
/* And we have enough data to print the test case footer */
test_result_ready = true;
break;
case GTEST_LINE_OTHER:
/* Once we've seen a RUN line that opens a test case
* we want to capture any output as test case message. */
if (test_result_open) {
debug_log("message from test: %s\n", line_sans_pattern);
bundle_print_test_result_message(
bundle_stream, flags,
line_sans_pattern);
} else
debug_log("message from gtest: %s\n", line_sans_pattern);
break;
default:
abort();
}
/* Write out the test result once we have everything */
if (test_result_ready) {
struct timeval duration;
duration.tv_sec = line_timestamp.tv_sec - test_result_start.tv_sec;
duration.tv_usec = line_timestamp.tv_usec - test_result_start.tv_usec;
debug_log("closing previous test result\n");
bundle_print_test_result_footer(
bundle_stream, flags,
test_result_result, "<stdout>", lineno, duration);
test_result_ready = false;
test_result_open = false;
}
} while (!feof(child_stream));
/* We may need to terminate the final test result */
if (test_result_open) {
struct timeval duration;
duration.tv_sec = line_timestamp.tv_sec - test_result_start.tv_sec;
duration.tv_usec = line_timestamp.tv_usec - test_result_start.tv_usec;
debug_log("closing previous (final) test result\n");
bundle_print_test_result_footer(
bundle_stream, flags,
test_result_result, "<stdout>", lineno, duration);
}
/* We may need to terminate the final test run */
if (test_run_cnt > 0) {
debug_log("closing previous (final) test run\n");
bundle_print_test_run_footer(bundle_stream, flags);
}
/* Reclaim line cache */
if (line)
free(line);
/* Reclaim memory used to keep the previous test_id */
if (previous_test_id)
free(previous_test_id);
}
static void
run_test_prog(
const char *test_prog,
const char *bundle_name,
unsigned flags
)
{
FILE *child_stream;
FILE *bundle_stream;
/* Open the bundle file */
bundle_stream = fopen(bundle_name, "wt");
if (bundle_stream == NULL) {
perror("fopen()");
abort();
}
/* Open the child process */
child_stream = popen(test_prog, "r");
if (child_stream == NULL) {
perror("popen()");
abort();
}
/* Write the bundle header */
debug_log("Saving bundle to %s\n", bundle_name);
bundle_print_header(bundle_stream, flags, "Dashboard Bundle Format 1.3");
/* Process the output data */
process_output(bundle_stream, child_stream, flags);
/* Terminate the test process */
pclose(child_stream);
/* Write the bundle footer */
bundle_print_footer(bundle_stream, flags);
}
static void show_usage() {
printf("Usage: lava-gtest-wrapper [-o BUNDLE] [-r] [-d] <executable>\n"
" lava-gtest-wrapper -t\n"
"\n"
"The first form runs the specified executable and parses\n"
"the output as a gtest-based test.\n"
"-r creates a human-readable bundle\n"
"-o sets the name of the bundle, by default it is bundle.json\n"
"-d enables debugging\n"
"\n"
"The second form runs the internal self-test\n");
}
int main(int argc, char *argv[]) {
int opt;
unsigned flags = 0;
const char *bundle_pathname = "bundle.json";
while ((opt = getopt(argc, argv, "drto:")) != -1) {
switch (opt) {
case 'd':
debug_enable(stderr);
break;
case 'r':
flags |= JSON_READABLE;
break;
case 't':
json_test_all();
return 0;
case 'o':
bundle_pathname = optarg;
break;
default:
show_usage();
printf("\nUnsupported option: %c\n", optopt);
return 1;
}
}
if (optind >= argc) {
show_usage();
printf("\nYou have to pass the pathname of the executable to run\n");
return 1;
} else {
run_test_prog(argv[optind], bundle_pathname, flags);
return 0;
}
}