diff options
| author | Joseph Hunkeler <jhunkeler@gmail.com> | 2024-05-16 12:13:35 -0400 | 
|---|---|---|
| committer | Joseph Hunkeler <jhunkeler@gmail.com> | 2024-05-16 12:17:46 -0400 | 
| commit | eaaae2c0f77fe371b1da8c2c248888103d488961 (patch) | |
| tree | 1137d0e51c214fb9aec6f321c40250a49c6fdba9 | |
| parent | 9ad649be44c568a00f2f407d715d07cd585c2b25 (diff) | |
| download | stasis-eaaae2c0f77fe371b1da8c2c248888103d488961.tar.gz | |
First pass at test result creation, and optional markdown->html conversion
| -rw-r--r-- | CMakeLists.txt | 7 | ||||
| -rw-r--r-- | include/junitxml.h | 47 | ||||
| -rw-r--r-- | include/omc.h | 1 | ||||
| -rw-r--r-- | src/CMakeLists.txt | 6 | ||||
| -rw-r--r-- | src/junitxml.c | 217 | ||||
| -rw-r--r-- | src/omc_indexer.c | 177 | 
6 files changed, 445 insertions, 10 deletions
| diff --git a/CMakeLists.txt b/CMakeLists.txt index e9a001e..5b8376e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,8 +5,11 @@ include(GNUInstallDirs)  set(nix_cflags -Wall -Wextra -fPIC)  set(win_cflags /Wall)  set(CMAKE_C_STANDARD 99) - -link_libraries(curl) +find_package(libxml2) +find_package(CURL) +link_libraries(CURL::libcurl) +link_libraries(LibXml2::LibXml2) +include_directories(${LIBXML2_INCLUDE_DIR})  if (CMAKE_C_COMPILER_ID STREQUAL "GNU")  	message("gnu options") diff --git a/include/junitxml.h b/include/junitxml.h new file mode 100644 index 0000000..d5e7708 --- /dev/null +++ b/include/junitxml.h @@ -0,0 +1,47 @@ +// @file junitxml.h +#ifndef OMC_JUNITXML_H +#define OMC_JUNITXML_H +#include <libxml/xmlreader.h> + +struct JUNIT_Failure { +    char *message; +}; + +struct JUNIT_Skipped { +    char *type; +    char *message; +}; + +#define JUNIT_RESULT_STATE_NONE 0 +#define JUNIT_RESULT_STATE_FAILURE 1 +#define JUNIT_RESULT_STATE_SKIPPED 2 +struct JUNIT_Testcase { +    char *classname; +    char *name; +    float time; +    char *message; +    int tc_result_state_type; +    union tc_state_ptr { +        struct JUNIT_Failure *failure; +        struct JUNIT_Skipped *skipped; +    } result_state; +}; + +struct JUNIT_Testsuite { +    char *name; +    int errors; +    int failures; +    int skipped; +    int tests; +    float time; +    char *timestamp; +    char *hostname; +    struct JUNIT_Testcase **testcase; +    size_t _tc_inuse; +    size_t _tc_alloc; +}; + +struct JUNIT_Testsuite *junitxml_testsuite_read(const char *filename); +void junitxml_testsuite_free(struct JUNIT_Testsuite **testsuite); + +#endif //OMC_JUNITXML_H diff --git a/include/omc.h b/include/omc.h index 45f5671..a116296 100644 --- a/include/omc.h +++ b/include/omc.h @@ -38,6 +38,7 @@  #include "recipe.h"  #include "relocation.h"  #include "wheel.h" +#include "junitxml.h"  #define guard_runtime_free(X) do { if (X) { runtime_free(X); X = NULL; } } while (0)  #define guard_strlist_free(X) do { if ((*X)) { strlist_free(X); (*X) = NULL; } } while (0) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0c43392..f075102 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -21,14 +21,16 @@ add_library(ohmycal STATIC          template.c          rules.c          docker.c +        junitxml.c  )  add_executable(omc          omc_main.c  ) -target_link_libraries(omc ohmycal) +target_link_libraries(omc PRIVATE ohmycal) +target_link_libraries(omc PUBLIC LibXml2::LibXml2)  add_executable(omc_indexer          omc_indexer.c  ) -target_link_libraries(omc_indexer ohmycal) +target_link_libraries(omc_indexer PRIVATE ohmycal)  install(TARGETS omc omc_indexer RUNTIME) diff --git a/src/junitxml.c b/src/junitxml.c new file mode 100644 index 0000000..df0514b --- /dev/null +++ b/src/junitxml.c @@ -0,0 +1,217 @@ +#include <stdlib.h> +#include <string.h> +#include "strlist.h" +#include "junitxml.h" + +static void testcase_result_state_free(struct JUNIT_Testcase **testcase) { +    struct JUNIT_Testcase *tc = (*testcase); +    if (tc->tc_result_state_type == JUNIT_RESULT_STATE_FAILURE) { +        guard_free(tc->result_state.failure->message); +        guard_free(tc->result_state.failure); +    } else if (tc->tc_result_state_type == JUNIT_RESULT_STATE_SKIPPED) { +        guard_free(tc->result_state.skipped->message); +        guard_free(tc->result_state.skipped); +    } +} + +static void testcase_free(struct JUNIT_Testcase **testcase) { +    struct JUNIT_Testcase *tc = (*testcase); +    guard_free(tc->name); +    guard_free(tc->message); +    guard_free(tc->classname); +    testcase_result_state_free(&tc); +    guard_free(tc); +} + +void junitxml_testsuite_free(struct JUNIT_Testsuite **testsuite) { +    struct JUNIT_Testsuite *suite = (*testsuite); +    guard_free(suite->name); +    guard_free(suite->hostname); +    guard_free(suite->timestamp); +    for (size_t i = 0; i < suite->_tc_alloc; i++) { +        testcase_free(&suite->testcase[i]); +    } +    guard_free(suite); +} + +static int testsuite_append_testcase(struct JUNIT_Testsuite **testsuite, struct JUNIT_Testcase *testcase) { +    struct JUNIT_Testsuite *suite = (*testsuite); +    struct JUNIT_Testcase **tmp = realloc(suite->testcase, (suite->_tc_alloc + 1 ) * sizeof(*testcase)); +    if (!tmp) { +        return -1; +    } else if (tmp != suite->testcase) { +        suite->testcase = tmp; +    } +    suite->testcase[suite->_tc_inuse] = testcase; +    suite->_tc_inuse++; +    suite->_tc_alloc++; +    return 0; +} + +static struct JUNIT_Failure *testcase_failure_from_attributes(struct StrList *attrs) { +    struct JUNIT_Failure *result; + +    result = calloc(1, sizeof(*result)); +    if(!result) { +        return NULL; +    } +    for (size_t x = 0; x < strlist_count(attrs); x += 2) { +        char *attr_name = strlist_item(attrs, x); +        char *attr_value = strlist_item(attrs, x + 1); +        if (!strcmp(attr_name, "message")) { +            result->message = strdup(attr_value); +        } +    } +    return result; +} + +static struct JUNIT_Skipped *testcase_skipped_from_attributes(struct StrList *attrs) { +    struct JUNIT_Skipped *result; + +    result = calloc(1, sizeof(*result)); +    if(!result) { +        return NULL; +    } +    for (size_t x = 0; x < strlist_count(attrs); x += 2) { +        char *attr_name = strlist_item(attrs, x); +        char *attr_value = strlist_item(attrs, x + 1); +        if (!strcmp(attr_name, "message")) { +            result->message = strdup(attr_value); +        } +    } +    return result; +} + +static struct JUNIT_Testcase *testcase_from_attributes(struct StrList *attrs) { +    struct JUNIT_Testcase *result; + +    result = calloc(1, sizeof(*result)); +    if(!result) { +        return NULL; +    } +    for (size_t x = 0; x < strlist_count(attrs); x += 2) { +        char *attr_name = strlist_item(attrs, x); +        char *attr_value = strlist_item(attrs, x + 1); +        if (!strcmp(attr_name, "name")) { +            result->name = strdup(attr_value); +        } else if (!strcmp(attr_name, "classname")) { +            result->classname = strdup(attr_value); +        } else if (!strcmp(attr_name, "time")) { +            result->time = strtof(attr_value, NULL); +        } else if (!strcmp(attr_name, "message")) { +            result->message = strdup(attr_value); +        } +    } +    return result; +} + +static struct StrList *attributes_to_strlist(xmlTextReaderPtr reader) { +    struct StrList *list; +    xmlNodePtr node = xmlTextReaderCurrentNode(reader); +    if (!node) { +        return NULL; +    } + +    list = strlist_init(); +    if (xmlTextReaderNodeType(reader) == 1 && node->properties) { +        xmlAttr *attr = node->properties; +        while (attr && attr->name && attr->children) { +            char *attr_name = (char *) attr->name; +            char *attr_value = (char *) xmlNodeListGetString(node->doc, attr->children, 1); +            strlist_append(&list, attr_name ? attr_name : ""); +            strlist_append(&list, attr_value ? attr_value : ""); +            xmlFree((xmlChar *) attr_value); +            attr = attr->next; +        } +    } +    return list; +} + +static int read_xml_data(xmlTextReaderPtr reader, struct JUNIT_Testsuite **testsuite) { +    const xmlChar *name; +    const xmlChar *value; + +    name = xmlTextReaderConstName(reader); +    if (!name) { +        name = BAD_CAST "--"; +    } +    value = xmlTextReaderConstValue(reader); +    const char *node_name = (char *) name; +    const char *node_value = (char *) value; +     +    struct StrList *attrs = attributes_to_strlist(reader); +    if (attrs && strlist_count(attrs)) { +        if (!strcmp(node_name, "testsuite")) { +            for (size_t x = 0; x < strlist_count(attrs); x += 2) { +                char *attr_name = strlist_item(attrs, x); +                char *attr_value = strlist_item(attrs, x + 1); +                if (!strcmp(attr_name, "name")) { +                    (*testsuite)->name = strdup(attr_value); +                } else if (!strcmp(attr_name, "errors")) { +                    (*testsuite)->errors = (int) strtol(attr_value, NULL, 10); +                } else if (!strcmp(attr_name, "failures")) { +                    (*testsuite)->failures = (int) strtol(attr_value, NULL, 0); +                } else if (!strcmp(attr_name, "skipped")) { +                    (*testsuite)->skipped = (int) strtol(attr_value, NULL, 0); +                } else if (!strcmp(attr_name, "tests")) { +                    (*testsuite)->tests = (int) strtol(attr_value, NULL, 0); +                } else if (!strcmp(attr_name, "time")) { +                    (*testsuite)->time = strtof(attr_value, NULL); +                } else if (!strcmp(attr_name, "timestamp")) { +                    (*testsuite)->timestamp = strdup(attr_value); +                } else if (!strcmp(attr_name, "hostname")) { +                    (*testsuite)->hostname = strdup(attr_value); +                } +            } +        } else if (!strcmp(node_name, "testcase")) { +            struct JUNIT_Testcase *testcase = testcase_from_attributes(attrs); +            testsuite_append_testcase(testsuite, testcase); +        } else if (!strcmp(node_name, "failure")) { +            size_t cur_tc = (*testsuite)->_tc_inuse > 0 ? (*testsuite)->_tc_inuse - 1 : (*testsuite)->_tc_inuse; +            struct JUNIT_Failure *failure = testcase_failure_from_attributes(attrs); +            (*testsuite)->testcase[cur_tc]->tc_result_state_type = JUNIT_RESULT_STATE_FAILURE; +            (*testsuite)->testcase[cur_tc]->result_state.failure = failure; +        } else if (!strcmp(node_name, "skipped")) { +            size_t cur_tc = (*testsuite)->_tc_inuse > 0 ? (*testsuite)->_tc_inuse - 1 : (*testsuite)->_tc_inuse; +            struct JUNIT_Skipped *skipped = testcase_skipped_from_attributes(attrs); +            (*testsuite)->testcase[cur_tc]->tc_result_state_type = JUNIT_RESULT_STATE_SKIPPED; +            (*testsuite)->testcase[cur_tc]->result_state.skipped = skipped; +        } +    } +    guard_strlist_free(&attrs); +    return 0; +} + +static int read_xml_file(const char *filename, struct JUNIT_Testsuite **testsuite) { +    xmlTextReaderPtr reader; +    int result; + +    reader = xmlReaderForFile(filename, NULL, 0); +    if (!reader) { +        return -1; +    } + +    result = xmlTextReaderRead(reader); +    while (result == 1) { +        read_xml_data(reader, testsuite); +        result = xmlTextReaderRead(reader); +    } + +    xmlFreeTextReader(reader); +    return 0; +} + +struct JUNIT_Testsuite *junitxml_testsuite_read(const char *filename) { +    struct JUNIT_Testsuite *result; + +    if (access(filename, F_OK)) { +        return NULL; +    } + +    result = calloc(1, sizeof(*result)); +    if (!result) { +        return NULL; +    } +    read_xml_file(filename, &result); +    return result; +}
\ No newline at end of file diff --git a/src/omc_indexer.c b/src/omc_indexer.c index ebf3ba6..971f389 100644 --- a/src/omc_indexer.c +++ b/src/omc_indexer.c @@ -7,6 +7,7 @@ static struct option long_options[] = {          {"destdir", required_argument, 0, 'd'},          {"verbose", no_argument, 0, 'v'},          {"unbuffered", no_argument, 0, 'U'}, +        {"web", no_argument, 0, 'w'},          {0, 0, 0, 0},  }; @@ -15,6 +16,7 @@ const char *long_options_help[] = {          "Destination directory",          "Increase output verbosity",          "Disable line buffering", +        "Generate HTML indexes (requires pandoc)",          NULL,  }; @@ -169,7 +171,7 @@ int indexer_get_files(struct StrList **out, const char *path, const char *patter  int get_latest_rc(struct Delivery ctx[], size_t nelem) {      int result = 0;      for (size_t i = 0; i < nelem; i++) { -        if (&ctx[i] && ctx[i].meta.rc > result) { +        if (ctx[i].meta.rc > result) {              result = ctx[i].meta.rc;          }      } @@ -249,6 +251,44 @@ int micromamba(const char *write_to, const char *prefix, char *command, ...) {      return status;  } +int indexer_make_website(struct Delivery *ctx) { +    char cmd[PATH_MAX]; +    char *inputs[] = { +        ctx->storage.delivery_dir, "/README.md", +        ctx->storage.results_dir, "/README.md", +    }; + +    if (!find_program("pandoc")) { +        return 0; +    } + +    for (size_t i = 0; i < sizeof(inputs) / sizeof(*inputs); i += 2) { +        char fullpath[PATH_MAX]; +        memset(fullpath, 0, sizeof(fullpath)); +        sprintf(fullpath, "%s/%s", inputs[i], inputs[i + 1]); +        if (access(fullpath, F_OK)) { +            continue; +        } +        // Converts a markdown file to html +        strcpy(cmd, "pandoc "); +        strcat(cmd, fullpath); +        strcat(cmd, " "); +        strcat(cmd, "-o "); +        strcat(cmd, inputs[i]); +        strcat(cmd, "/index.html"); +        if (globals.verbose) { +            puts(cmd); +        } +        // This might be negative when killed by a signal. +        // Otherwise, the return code is not critical to us. +        if (system(cmd) < 0) { +            return 1; +        } +    } + +    return 0; +} +  int indexer_conda(struct Delivery *ctx) {      int status = 0;      char prefix[PATH_MAX]; @@ -361,6 +401,7 @@ int indexer_readmes(struct Delivery ctx[], size_t nelem) {          struct StrList *platforms = get_platforms(*latest, nelem);          fprintf(indexfp, "# %s-%s\n\n", ctx->meta.name, ctx->meta.version); +        fprintf(indexfp, "## Current Release\n\n");          for (size_t p = 0; p < strlist_count(platforms); p++) {              char *platform = strlist_item(platforms, p);              for (size_t a = 0; a < strlist_count(archs); a++) { @@ -374,10 +415,10 @@ int indexer_readmes(struct Delivery ctx[], size_t nelem) {                  if (!have_combo) {                      continue;                  } -                fprintf(indexfp, "## %s-%s\n\n", platform, arch); +                fprintf(indexfp, "### %s-%s\n\n", platform, arch);                  fprintf(indexfp, "|Release|Info|Receipt|\n"); -                fprintf(indexfp, "|----|----|----|\n"); +                fprintf(indexfp, "|:----:|:----:|:----:|\n");                  for (size_t i = 0; i < nelem; i++) {                      char link_name[PATH_MAX];                      char readme_name[PATH_MAX]; @@ -413,6 +454,102 @@ int indexer_readmes(struct Delivery ctx[], size_t nelem) {      return 0;  } +int indexer_junitxml_report(struct Delivery ctx[], size_t nelem) { +    struct Delivery **latest = NULL; +    latest = get_latest_deliveries(ctx, nelem); + +    char indexfile[PATH_MAX] = {0}; +    sprintf(indexfile, "%s/README.md", ctx->storage.results_dir); + +    struct StrList *file_listing = listdir(ctx->storage.results_dir); +    if (!file_listing) { +        // no test results to process +        return 0; +    } + +    if (!pushd(ctx->storage.results_dir)) { +        FILE *indexfp; +        indexfp = fopen(indexfile, "w+"); +        if (!indexfp) { +            fprintf(stderr, "Unable to open %s for writing\n", indexfile); +            return -1; +        } +        struct StrList *archs = get_architectures(*latest, nelem); +        struct StrList *platforms = get_platforms(*latest, nelem); + +        fprintf(indexfp, "# %s-%s Test Report\n\n", ctx->meta.name, ctx->meta.version); +        fprintf(indexfp, "## Current Release\n\n"); +        for (size_t p = 0; p < strlist_count(platforms); p++) { +            char *platform = strlist_item(platforms, p); +            for (size_t a = 0; a < strlist_count(archs); a++) { +                char *arch = strlist_item(archs, a); +                int have_combo = 0; +                for (size_t i = 0; i < nelem; i++) { +                    if (strstr(latest[i]->system.platform[DELIVERY_PLATFORM_RELEASE], platform) && strstr(latest[i]->system.arch, arch)) { +                        have_combo = 1; +                        break; +                    } +                } +                if (!have_combo) { +                    continue; +                } +                fprintf(indexfp, "### %s-%s\n\n", platform, arch); + +                fprintf(indexfp, "|Suite|Duration|Fail    |Skip |Error |\n"); +                fprintf(indexfp, "|:----|:------:|:------:|:---:|:----:|\n"); +                for (size_t f = 0; f < strlist_count(file_listing); f++) { +                    char *filename = strlist_item(file_listing, f); +                    if (!endswith(filename, ".xml")) { +                        continue; +                    } + +                    if (strstr(filename, platform) && strstr(filename, arch)) { +                        struct JUNIT_Testsuite *testsuite = junitxml_testsuite_read(filename); +                        if (testsuite) { +                            if (globals.verbose) { +                                printf("%s: duration: %0.4f, failed: %d, skipped: %d, errors: %d\n", filename, testsuite->time, testsuite->failures, testsuite->skipped, testsuite->errors); +                            } +                            fprintf(indexfp, "|[%s](%s)|%0.4f|%d|%d|%d|\n", filename, filename, testsuite->time, testsuite->failures, testsuite->skipped, testsuite->errors); +                            /* +                             * TODO: Display failure/skip/error output. +                             * +                            for (size_t i = 0; i < testsuite->_tc_inuse; i++) { +                                if (testsuite->testcase[i]->tc_result_state_type) { +                                    printf("testcase: %s :: %s\n", testsuite->testcase[i]->classname, testsuite->testcase[i]->name); +                                    if (testsuite->testcase[i]->tc_result_state_type == JUNIT_RESULT_STATE_FAILURE) { +                                        printf("failure: %s\n", testsuite->testcase[i]->result_state.failure->message); +                                    } else if (testsuite->testcase[i]->tc_result_state_type == JUNIT_RESULT_STATE_SKIPPED) { +                                        printf("skipped: %s\n", testsuite->testcase[i]->result_state.skipped->message); +                                    } +                                } +                            } +                             */ +                            junitxml_testsuite_free(&testsuite); +                        } else { +                            fprintf(stderr, "bad test suite: %s: %s\n", strerror(errno), filename); +                            continue; +                        } +                    } +                } +                fprintf(indexfp, "\n"); +            } +            fprintf(indexfp, "\n"); +        } +        guard_strlist_free(&archs); +        guard_strlist_free(&platforms); +        fclose(indexfp); +        popd(); +    } else { +        fprintf(stderr, "Unable to enter delivery directory: %s\n", ctx->storage.delivery_dir); +        guard_free(latest); +        return -1; +    } + +    // "latest" is an array of pointers to ctxs[]. Do not free the contents of the array. +    guard_free(latest); +    return 0; +} +  void indexer_init_dirs(struct Delivery *ctx, const char *workdir) {      path_store(&ctx->storage.root, PATH_MAX, workdir, "");      path_store(&ctx->storage.tmpdir, PATH_MAX, ctx->storage.root, "tmp"); @@ -425,6 +562,7 @@ void indexer_init_dirs(struct Delivery *ctx, const char *workdir) {      path_store(&ctx->storage.meta_dir, PATH_MAX, ctx->storage.output_dir, "meta");      path_store(&ctx->storage.delivery_dir, PATH_MAX, ctx->storage.output_dir, "delivery");      path_store(&ctx->storage.package_dir, PATH_MAX, ctx->storage.output_dir, "packages"); +    path_store(&ctx->storage.results_dir, PATH_MAX, ctx->storage.output_dir, "results");      path_store(&ctx->storage.wheel_artifact_dir, PATH_MAX, ctx->storage.package_dir, "wheels");      path_store(&ctx->storage.conda_artifact_dir, PATH_MAX, ctx->storage.package_dir, "conda");  } @@ -433,9 +571,10 @@ int main(int argc, char *argv[]) {      size_t rootdirs_total = 0;      char *destdir = NULL;      char **rootdirs = NULL; +    int do_html = 0;      int c = 0;      int option_index = 0; -    while ((c = getopt_long(argc, argv, "hd:vU", long_options, &option_index)) != -1) { +    while ((c = getopt_long(argc, argv, "hd:vUw", long_options, &option_index)) != -1) {          switch (c) {              case 'h':                  usage(path_basename(argv[0])); @@ -452,6 +591,9 @@ int main(int argc, char *argv[]) {              case 'v':                  globals.verbose = 1;                  break; +            case 'w': +                do_html = 1; +                break;              case '?':              default:                  exit(1); @@ -515,6 +657,15 @@ int main(int argc, char *argv[]) {      if (indexer_combine_rootdirs(workdir, rootdirs, rootdirs_total)) {          SYSERROR("%s", "Copy operation failed");          rmtree(workdir); +        exit(1); +    } + +    if (access(ctx.storage.conda_artifact_dir, F_OK)) { +        mkdirs(ctx.storage.conda_artifact_dir, 0755); +    } + +    if (access(ctx.storage.wheel_artifact_dir, F_OK)) { +        mkdirs(ctx.storage.wheel_artifact_dir, 0755);      }      msg(OMC_MSG_L1, "Indexing conda packages\n"); @@ -559,10 +710,24 @@ int main(int argc, char *argv[]) {          exit(1);      } +    msg(OMC_MSG_L1, "Indexing test results\n"); +    if (indexer_junitxml_report(local, strlist_count(metafiles))) { +        SYSERROR("%s", "Test result indexing operation failed"); +        exit(1); +    } + +    if (do_html) { +        msg(OMC_MSG_L1, "Generating HTML indexes\n"); +        if (indexer_make_website(local)) { +            SYSERROR("%s", "Site creation failed"); +            exit(1); +        } +    } +      msg(OMC_MSG_L1, "Copying indexed delivery to '%s'\n", destdir);      char cmd[PATH_MAX];      memset(cmd, 0, sizeof(cmd)); -    sprintf(cmd, "rsync -ah%s --delete --exclude 'tmp/' --exclude 'tools/' '%s/output/' '%s/'", globals.verbose ? "v" : "q", workdir, destdir); +    sprintf(cmd, "rsync -ah%s --delete --exclude 'tmp/' --exclude 'tools/' '%s/' '%s/'", globals.verbose ? "v" : "q", workdir, destdir);      guard_free(destdir);      if (globals.verbose) { @@ -585,4 +750,4 @@ int main(int argc, char *argv[]) {      globals_free();      msg(OMC_MSG_L1, "Done!\n");      return 0; -}
\ No newline at end of file +} | 
