| /** |
| * 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; |
| } |
| } |