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 | ohmycal-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 +} |