Plotting Test Paths

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.

library(covtracer)

library(withr)
library(covr)
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 Example R6 Person class public methods are traced           Accumulator        TRUE 1       1
#> 2 Example R6 Person class public methods are traced           Accumulator        TRUE 1       1
#> 3 Example R6 Person class public methods are traced                Person        TRUE 1       1
#> 4 Example R6 Person class public methods are traced                Person        TRUE 2       1
#> 5              S4 Generic Call: show(<myS4Example>) show,S4Example-method       FALSE 1       2
#> 6          S4Example increment generic method works                Person        TRUE 1       3

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 Example R6 Person class public methods are traced           Accumulator
#> 2                                       Accumulator           Accumulator
#> 3                                       Accumulator                Person
#> 4                                            Person                Person
#> 5              S4 Generic Call: show(<myS4Example>) show,S4Example-method
#> 6          S4Example increment generic method works                Person

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 Example R6 Person class public methods are traced     Test #1
#> 2              S4 Generic Call: show(<myS4Example>)     Test #2
#> 3          S4Example increment generic method works     Test #3
#> 4                      S4Example names method works     Test #4
#> 5                                       Accumulator Accumulator
#> 6                                            Person      Person

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.