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.
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.
library(dplyr)
#>
#> Attaching package: 'dplyr'
#> The following objects are masked from 'package:stats':
#>
#> filter, lag
#> The following objects are masked from 'package:base':
#>
#> intersect, setdiff, setequal, union
library(igraph)
#>
#> Attaching package: 'igraph'
#> The following objects are masked from 'package:dplyr':
#>
#> as_data_frame, groups, union
#> The following objects are masked from 'package:stats':
#>
#> decompose, spectrum
#> The following object is masked from 'package:base':
#>
#> union
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.
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)
#> test_name alias is_exported i test_id
#> 1 Calling a deeply nested series of functions. complex_call_stack TRUE 1 1
#> 2 Calling a deeply nested series of functions. nested_function FALSE 2 1
#> 3 Calling a deeply nested series of functions. deeper_nested_function TRUE 3 1
#> 4 Calling a deeply nested series of functions. recursive_function FALSE 4 1
#> 5 Calling a deeply nested series of functions. recursive_function FALSE 5 1
#> 6 Calling a deeply nested series of functions. recursive_function FALSE 6 1
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.
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)
#> from to
#> 1 Calling a deeply nested series of functions. complex_call_stack
#> 2 complex_call_stack nested_function
#> 3 nested_function deeper_nested_function
#> 4 deeper_nested_function recursive_function
#> 5 recursive_function recursive_function
#> 6 Calling a function halfway through call stack. deeper_nested_function
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.
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()
#> name label
#> 1 Calling a deeply nested series of functions. Test #1
#> 2 Calling a function halfway through call stack. Test #2
#> 3 Example R6 Accumulator class constructor is traced Test #3
#> 4 Example R6 Accumulator class methods are traced Test #4
#> 5 Example R6 Person class public methods are traced Test #5
#> 6 Example R6 Rando class active field functions are traced Test #6
Plotting Our Test Paths
Finally, we can plot this network of test executions:
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.