Tests from Traces: Automated Unit Test Extraction for
Filip Křikava, Czech Technical University
Jan Vitek, Northeastern University
Motivation - Why R?
2
Motivation - Test code coverage for 1,500 packages
3
Corpus
On average 19% code coverage
Motivation - Testing R package
4
Rvmmin/
├── DESCRIPTION
├── inst/
├── man/
├── MD5
├── NAMESPACE
├── NEWS
├── R/
├── tests/
│ └── BTbad.R
└── vignettes/
No asserts or checks - just an ordinary R program
# BTbad.R -- inadmissible bounds to�# see if Rvmmin with keepinputpar = TRUE stops.�#####################�… … … �… … … �n<-10�xx <- rep(0,n)�lower <- rep(1,n)�upper <- rep(3,n)�bdmsk <- rep(1,n) # all free parameters�ansbt <- try(
Rvmmin(xx, bt.f, bt.g, lower, upper, bdmsk,� control=list(trace=1, keepinputpar=TRUE))
)
�if (class(ansbt) == "try-error") {� cat("Successful stop when out of bounds\n")�} else {
print(ansbt)
}
Motivation - Testing R package
5
Motivation - R package examples
6
Rvmmin/
├── DESCRIPTION
├── inst/
├── man/
│ ├── Rvmminb.Rd
│ ├── Rvmmin.Rd
│ └── Rvmminu.Rd
├── MD5
├── NAMESPACE
├── NEWS
├── R/
├── tests/
│ └── BTbad.R
└── vignettes/
└── Rvmmin.Rmd
\name{Rvmmin}�\title{Variable metric nonlinear function minimization, driver.}�\description{A driver to call the unconstrained and bounds ....}�\usage{� Rvmmin(par,fn,gr,lower,upper,bdmsk,control=list(),\dots)�}�\arguments{� \item{par}{A numeric vector of starting estimates.}
… … …
}
\examples{
� fr <- function(x) {� x1 <- x[1]� x2 <- x[2]� 100 * (x2 - x1 * x1)^2 + (1 - x1)^2� }�� ansrosenbrock <- Rvmmin(fn=fr,gr="grfwd", par=c(1,2))� print(ansrosenbrock)
… … … �}
Motivation - R package vignettes
7
Rvmmin/
├── DESCRIPTION
├── inst/
├── man/
│ ├── Rvmminb.Rd
│ ├── Rvmmin.Rd
│ └── Rvmminu.Rd
├── MD5
├── NAMESPACE
├── NEWS
├── R/
├── tests/
│ └── BTbad.R
└── vignettes/
└── Rvmmin.Rmd
… … …
8
Can unit tests be extracted?
Runnable code in R packages
On avg 86 lines
Reverse dependencies of R packages
9
genthat - R package for automated unit test generation from traces
Tracing example
10
filter <- function(xs, p) xs[sapply(xs, p)]
Target mypkg package
> is_odd <- function(x) x %% 2L != 0L
> m <- 1�> filter(floor(runif(10, 1, 10)) + m, is_odd)�[1] 5 9 9 7 5 3
Client code (eg., example, vignette)
$ :List of 6� ..$ fun : chr "filter"� ..$ pkg : chr "mypkg"� ..$ args : List of 2� .. ..$ xs: language floor(runif(10, 1, 10)) + m� .. ..$ p : symbol is_odd� ..$ globals:List of 2� .. ..$ floor : language base::floor � .. ..$ runif : language stats::runif � .. ..$ is_odd : function (x) x %% 2L != 0L � .. ..$ m : num 1� ..$ seed : int [1:626] 403 624 507561766 ...� ..$ retv : int [1:6] 5 9 9 7 5 3
Trace
filter <- function(xs, p) {� `__captured_seed` <- get(".Random.seed", envir=globalenv())� on.exit(with_paused_tracing({� retv <- returnValue(default=deflt_retv)� if (!normal_retv(retv)) {� record_trace(
name="filter", pkg="mypkg",
args=as.list(match.call())[-1], retv=retv,
seed=`__captured_seed`, env=parent.frame()
)� }� })
� xs[sapply(xs, p)]�}
Instrumented target mypkg package
Generating unit tests
11
$ :List of 6� ..$ fun : chr "filter"� ..$ pkg : chr "mypkg"� ..$ args : List of 2� .. ..$ xs: language floor(runif(10, 1, 10)) + m� .. ..$ p : symbol is_odd� ..$ globals:List of 2� .. ..$ floor : language base::floor � .. ..$ runif : language stats::runif � .. ..$ is_odd : function (x) x %% 2L != 0L � .. ..$ m : num 1� ..$ seed : int [1:626] 403 624 507561766 ...� ..$ retv : int [1:6] 5 9 9 7 5 3
Trace
.trace
.ext
seed : int [1:626] 403 624 507561766 ...
External variables�(binary)
.R
library(testthat)
library(mypkg)��.Random.seed <<- .ext.seed��test_that("filter", {� is_odd <- genthat::with_env(
function(x) x %% 2L != 0L
)� m <- 1�
expect_equal(� filter(
xs = base::floor(stats::runif(10, 1, 10)+m),
p = is_odd
), � c(5L, 9L, 9L, 7L, 5L, 3L)� )
})
Unit test (testthat format)
> is_odd <- function(x) x %% 2L != 0L
> m <- 1�> filter(floor(runif(10, 1, 10)) + m, is_odd)�[1] 5 9 9 7 5 3
Client code (eg., example, vignette)
12
How well can automated trace-based unit test extraction actually work in practice for R?
Research question
R Language
13
https://en.wikipedia.org/wiki/R_(programming_language)
CRAN Experiment - Setup
14
CRAN Experiment - Scale
15
| Overall | Average per package |
Total number of calls | 5,277,897 | 3,411 (s=13,366, m=141) |
Traced unique calls | 1,617,842 | 1,045 (s=3,142, m=82) |
Generated tests | 1,515,246 | 979 (s=3,065, m=68) |
Passing tests | 1,306,045 | 844 (s=2,821, m=51) |
Non-redundant tests | 26,967 | 17 (s=33, m=9) |
Ratio of reproduced tests | 0.8 | 0.75 (s=0.31, m=0.9) |
Scale of the experiment
CRAN Experiment - Errors
16
| Overall | % |
Tracing errors� - skipped traces (size > 512kB) | 70,020�60,360 | 4.32% |
Test generation errors� - environments with cycles | 32,576�24,200 | 2.01% |
Replaying test errors� - incorrect test (value mismatch)� - invalid test (execute with error) | 209,201 77,850 131,351 | 12.93% |
Errors breakdown
CRAN Experiment - Code coverage
17
CRAN Experiment - Code coverage
18
Reverse dependencies experiment
19
Limitations, shortcomings and future work
20
Limitations, shortcomings and future work
21
22
Conclusion
How well can automated trace-based unit test extraction actually work in practice for R?
Tests from Traces: Automated Unit Test Extraction for
https://github.com/fikovnik/ISSTA18-artifact
Filip Křikava, Jan Vitek
34% increase of code coverage
in 1,500+ CRAN packages
genthat package
https://github.com/PRL-PRG/genthat