--- title: "Plotting Test Paths" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Plotting Test Paths} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) options(width = 120L) ``` ```{css, echo=FALSE} img { border: none; margin: 0; } ``` ## Setup As with all covtracer analysis, we need to start by collecting coverage traces of a package. Below is an example where a package is installed with the necessary flags such that the coverage traces can be collected. ```{r setup} library(covtracer) library(withr) library(covr) ``` ```{r libpaths, include = FALSE} init_libs <- .libPaths() dir.create(lib <- tempfile("covtracer_pkgs_")) .libPaths(c(lib, .libPaths())) ``` ```{r calc_cov, message = FALSE, warning = FALSE} options(keep.source = TRUE, keep.source.pkg = TRUE, covr.record_tests = TRUE) examplepkg_source_path <- system.file("examplepkg", package = "covtracer") install.packages( examplepkg_source_path, type = "source", repos = NULL, quiet = TRUE, INSTALL_opts = c("--with-keep.source", "--install-tests") ) examplepkg_cov <- covr::package_coverage(examplepkg_source_path) examplepkg_ns <- getNamespace("examplepkg") ttdf <- covtracer::test_trace_df(examplepkg_cov, aggregate_by = NULL) ``` As well, for this analysis we will use a few supporting packages. ```{r more_setup} library(dplyr) library(igraph) ``` ## Preparing Graph Data Before we use the test data, we will clean our incoming data, removing untested records and filtering out untestable objects like S4 class definitions. ```{r} ttdf <- ttdf %>% filter(!is.na(test_name)) %>% filter(is.na(doctype) | !doctype %in% "class") %>% select(test_name, alias, is_exported, i) %>% arrange(test_name, i) %>% mutate(test_id = cumsum(!duplicated(test_name))) head(ttdf) ``` ## Create Edges of Our Test Path Our test-trace dataframe has an index of test expressions, each linked to the traces that they evaluate, with added order of evaluation, `i`. To prepare this for visualization, we want to convert this to a dataframe where each record describes a step of this process. Instead of a test linking to a trace with an index, each jump in the test path should link from the calling expression to the evaluated expression. ```{r} edges_df <- ttdf %>% split(.$test_name) %>% lapply(function(sdf) { unique(data.frame( from = c(sdf$test_name[[1L]], head(sdf$alias, -1L)), to = sdf$alias )) }) %>% bind_rows() %>% distinct() head(edges_df) ``` Likewise, we want to capture some metadata about each vertex. Since a vertex in this context can be either a test or a trace, we have some data that is captured differently for each class of vertex. ```{r} test_names <- Filter(Negate(is.na), unique(ttdf$test_name)) obj_names <- Filter(Negate(is.na), unique(ttdf$alias)) n_tests <- length(test_names) n_objs <- length(obj_names) vertices_df <- data.frame( name = c(test_names, obj_names), color = rep(c("cornflowerblue", "darkgoldenrod"), times = c(n_tests, n_objs)), label = c(sprintf("Test #%d", seq_along(test_names)), obj_names), test_id = c(seq_along(test_names), rep_len(NA, n_objs)), is_test = rep(c(TRUE, FALSE), times = c(n_tests, n_objs)), is_exported = c(rep_len(NA, n_tests), ttdf$is_exported[match(obj_names, ttdf$alias)]) ) vertices_df <- vertices_df %>% mutate(color = ifelse(is_exported, "goldenrod", color)) vertices_df %>% select(name, label) %>% head() ``` ```{r, include = FALSE} # for whatever reason... this fixes errors when building vignettes on R-devel edges_df vertices_df ``` ## Plotting Our Test Paths Finally, we can plot this network of test executions: ```{r, fig.asp = 1, fig.width = 8L, out.width = "100%", error = TRUE} g <- igraph::graph_from_data_frame(edges_df, vertices = vertices_df) par(mai = rep(0, 4), omi = rep(0, 4L)) plot.igraph(g, vertex.size = 8, vertex.label = V(g)$label, vertex.color = V(g)$color, vertex.label.family = "sans", vertex.label.color = "black", vertex.label.dist = 1, vertex.label.degree = -pi / 2, vertex.label.cex = 0.8, mark.border = NA, margin = c(0, 0.2, 0, 0.2) ) legend( "bottomleft", inset = c(0.05, 0), legend = c("test", "exported function", "unexported function"), col = c("cornflowerblue", "goldenrod", "darkgoldenrod"), pch = 16, bty = "n" ) ``` Naturally, there are a plethora of wonderful visualization packages available that accept igraph data as input. This graph could just as well be plotted with the `visNetwork` package, though it is omitted to keep this example analysis minimal.