Working with srcrefs

library(covtracer)

What are srcref objects?

print(examplepkg_ns$hypotenuse)
#> function(a, b) {
#>   return(sqrt(a^2 + b^2))
#> }
#> <bytecode: 0x6391a2479c98>
#> <environment: namespace:examplepkg>

srcrefs are a base R data type that is used frequently for working with package code. When you install a package using the --with-keep.source flag, data about the package’s source code representation is bound to the objects that the namespace attaches or loads. In short, srcrefs are simple data structures which store the file path of the source code and information about where to find the relevant bits in that file including line and character columns of the start and end of the source code.

For extensive details, refer to ?getSrcref and ?srcref.

Lets see it in action:

getSrcref(covtracer::test_trace_df)
#> function(x, ...) {
#>   UseMethod("test_trace_df")
#> }
# get line and column ranges (for details see ?srcref)
as.numeric(getSrcref(covtracer::test_trace_df))
#> [1]   17   18   19    1   18    1 1308 1310
getSrcFilename(covtracer::test_trace_df)
#> [1] "test_trace_df.R"

Extracting relevant traceability srcrefs

Instead of working with these objects directly, there are a few helper functions for making these objects easier to extract. For tracing coverage paths, there are three important classes of srcrefs:

  1. Package namespace object srcrefs
  2. Test code srcrefs
  3. Coverage trace srcrefs

Setup

Before we begin, we’ll get a package test coverage object and store the package namespace. We take extra precaution to use a temporary library for the sake of example, but this is only necessary when we want to avoid installing the package into our working library.

library(withr)
library(covr)

withr::with_temp_libpaths({
  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,
    INSTALL_opts = c("--with-keep.source", "--install-tests")
  )
  examplepkg_cov <- covr::package_coverage(examplepkg_source_path)
  examplepkg_ns <- getNamespace("examplepkg")
})

Functions for extracting srcrefs

There are a few functions for teasing out this information succinctly. These include pkg, trace, and test flavors for *_srcefs and *_srcrefs_df families of functions (eg, pkg_srcrefs_df()). *_srcrefs() functions return a more primitive list objects. Because these can be a bit cumbersome to read through, *_srcrefs_df() alternatives are provided for improved introspection and readability.

data.frame results contain a srcref column, where each element is a srcref object. Even though this appears as a succinct text, it contains all the srcref data.

Extracting package namespace object srcrefs

Getting a list of srcrefs

pkg_srcrefs(examplepkg_ns)["test_description.character"]

Viewing results as a data.frame

head(pkg_srcrefs_df(examplepkg_ns))
#>                   name                          srcref  namespace
#> 1      nested_function  complex_call_stack.R:9:20:11:1 examplepkg
#> 2                adder           r6_example.R:3:10:9:1 examplepkg
#> 3   recursive_function complex_call_stack.R:21:23:24:1 examplepkg
#> 5          Accumulator         r6_example.R:29:16:32:3 examplepkg
#> 8 s3_example_func.list         s3_example.R:20:25:22:1 examplepkg
#> 9      s3_example_func         s3_example.R:10:20:12:1 examplepkg

Extracing individual srcrefs from the resulting data.frame

df <- pkg_srcrefs_df(examplepkg_ns)
df$srcref[[1L]]
#> function(x) {
#>   deeper_nested_function(x)
#> }

Extracting test srcrefs

Similarly, we can extract test srcrefs using equivalent functions for tests. However, to get test traces, we must first run the package coverage, which records exactly which tests were actually run by the test suite. Starting from coverage omits any skipped tests or unevaluated test lines, only presenting test code that is actually run.

Note that the original source files will no longer exist, as covr will install the package into a temporary location for testing. Because of this, test “srcrefs” are actually call objects with a with_pseudo_srcref, allowing them to be treated like a srcrefs for consistency.

examplepkg_test_srcrefs <- test_srcrefs(examplepkg_cov)

Despite not having a valid srcfile, we can still use all of our favorite srcref functions because of the with_pseudo_scref subclass:

getSrcFilename(examplepkg_test_srcrefs[[1]])
#> character(0)

And finally, there is a corresponding *_df function to make this information easier to see at a glance:

head(examplepkg_test_srcrefs)
#> [[1]]
#> show(<myS4Example>)
#> 
#> $`./eval.R:96:3:96:33:3:33:11479:11479`
#> increment(1)
#> 
#> $`./new.R:156:5:156:19:5:19:744:744`
#> initialize(...)
#> 
#> $`/tmp/RtmpGqhtYS/R_LIBS557eb4b86d/examplepkg/examplepkg-tests/testthat/test-s4-example.R:3:3:3:40:3:40:3:3`
#> names(s4ex)

Extracting trace srcrefs

The final piece of the puzzle is the coverage traces. These are the simplest to find, since covr stores this information with every coverage object. Even without any helper functions, you can find this information by indexing into a coverage object to explore for yourself.

examplepkg_cov[[1]]$srcref
#> nested_function(x)

Nevertheless, we provide simple alternatives for restructuring this data into something more consistent with the rest of the pacakge.

examplepkg_trace_srcrefs <- trace_srcrefs(examplepkg_cov)
examplepkg_trace_srcrefs[1]
#> $`complex_call_stack.R:4:3:4:20:3:20:6:6`
#>  4
#>  3
#>  4
#> 20
#>  3
#> 20
#>  6
#>  6

And just like the other functions in the family, this too comes with a *_df companion function.

head(trace_srcrefs_df(examplepkg_cov))
#>                                         name                          srcref
#> 1     complex_call_stack.R:4:3:4:20:3:20:6:6   complex_call_stack.R:4:3:4:20
#> 2       s4_example.R:44:3:44:15:3:15:311:311         s4_example.R:44:3:44:15
#> 3           hypotenuse.R:8:3:8:25:3:25:35:35           hypotenuse.R:8:3:8:25
#> 4         s3_example.R:21:3:21:8:3:8:265:265          s3_example.R:21:3:21:8
#> 5             r6_example.R:4:3:8:3:3:3:41:45            r6_example.R:4:3:8:3
#> 6 complex_call_stack.R:10:3:10:27:3:27:12:12 complex_call_stack.R:10:3:10:27

Summary

With all of this information, we can match related code blocks to one another to retrospectively evaluate the relationship between package code and tests.