# Copyright (C) Brodie Gaslam
#
# This file is part of "unitizer - Interactive R Unit Tests"
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# Go to <https://www.r-project.org/Licenses/GPL-2> for a copy of the license.
# Retrieves Environment Ancestry
#
# @keywords internal
# @param env the environment to start with
# @param stop.env the environment to stop with
env_ancestry <- function(env, stop.env=globalenv()) {
if(!is.environment(env) || !is.environment(stop.env)) stop("Arguments `env` and `stop.env` must both be environments")
out <- character()
repeat {
out <- c(out, env_name(env))
if(identical(env, stop.env)) break
if(identical(env, emptyenv())) stop("Hit empty environment while traveling up environment ancestry")
env <- parent.env(env)
}
out
}
# Gets Environment Name / Memory Code
#
# Captures the name that \code{`\link{print.default}`} displays when one
# prints and environment
#
# @keywords internal
# @param env an environemnt
# @return character 1 length
env_name <- function(env) {
if(!is.environment(env)) stop("Argument `env` must be an environment")
sub("<environment: (.*)>", "\\1", capture.output(print.default(env))[[1]])
}
# Functions To Ignore
#
# DEPRECATED. Now handled by visibility status.
#
# Ignored functions are not considered tests if they are called from
# the top level.
#
# Also, provide a function to compare functions even when traced.
#
# @keywords internal
# @param x the reference function, if is traced then y must be identical
# @param y the current function, if \code{`x`} is not traced and \code{`y`}
# is traced, will compare using \code{`y@@original`} instead of \code{`y`}
funs.ignore <- list(base::`<-`, base::library, base::`=`, base::set.seed)
identical_fun <- function(x, y) {
if(!is.function(x) || !is.function(y))
stop("Arguments `x` and `y` must both be functions.")
if(is(x, "functionWithTrace")) {
return(identical(x, y))
} else if(is(y, "functionWithTrace")) {
return(identical(x, y@original))
}
identical(x, y)
}
## Overrides Default quit() Behavior
##
## Necessary because quit short circuits the \code{on.exit} clean-up functions
## and would leave stuff in a weird state (history not reset, etc.).
##
## This is used in \code{\link{unitize}}.
##
## @keywords internal
unitizer_quit <- function(
save = "default", status = 0, runLast = TRUE, truly.quit=TRUE
) {
meta_word_msg(
"You are attempting to quit R from within `unitizer`. If you do so ",
"you will lose any unsaved `unitizers`. Use `Q` to quit `unitizer` ",
"gracefully. Are you sure you want to exit R?"
)
quit.count <- 5
while(
!(res <- head(tolower(read_line("Quit R? [y/n]: ")), 1L)) %in% c("y", "n")
) {
quit.count <- quit.count - 1L
if(quit.count < 0) {
meta_word_msg("Sorry, could not understand you, quitting then.")
res <- "y"
break;
}
}
if(res == "y" && truly.quit) {
quit(save=save, status=status, runLast=runLast) # nocov
} else if (res == "y") TRUE else FALSE
}
# Cleans a Path to be In Standard Format
#
# Uses \code{`\link{dirname}`} to convert paths on windows machines with back
# slasshes to forward slash based names, and then removed excess forward
# slashes.
#
# @keywords internal
# @param path character the path name to clean up
# @return the cleaned up path
path_clean <- function(path) {
if(!is.character(path)) stop("Argument `path` must be character")
path.norm <- paste0(dirname(path), "/", basename(path))
sub("/+", "/", path.norm)
}
#' Create a Store ID from a Test File Name
#'
#' @param x character(1L) file name ending in .r or .R
#' @return store id name, or NULL if \code{x} doesn't meet expectations
#' @export
#' @examples
#' filename_to_storeid(file.path("tests", "unitizer", "foo.R"))
#' filename_to_storeid(file.path("tests", "unitizer", "boo.r"))
#' # does not end in [rR]
#' filename_to_storeid(file.path("tests", "unitizer", "boo"))
filename_to_storeid <- function(x) {
if(is.character(x) && length(x) == 1L){
r.regex <- "\\.[rR]$"
if((y <- sub(r.regex, ".unitizer", x)) != x) return(y)
warning(
"Unable to translate file name '", x, "' to `store.id` because ",
"it does not match regex '", r.regex, "', please provide explicit ",
"`store.id` or rename to end in '.R'. Returning in NULL for ",
"`store.id`."
)
} else
warning(
"Unable to generate store id from non `character(1L)` file \"name\""
)
NULL
}
# History Management Funs
#
# @keywords internal
history_capt <- function(hist.file=NULL, interactive.mode) {
# set up local history
if(is.null(hist.file)) return(list(con=NULL, file=NULL))
# nocov start
if(!interactive()) {
if(!interactive.mode) {
warning(
"Unable to capture history in non-interactive mode.",
immediate.=TRUE
) }
return(list(con=NULL, file=NULL))
}
hist.try <- try(savehistory(), silent=TRUE)
if(inherits(hist.try, "try-error"))
warning(conditionMessage(attr(hist.try, "condition")))
hist.con <- try(file(hist.file, "at"))
if(inherits(hist.con, "try-error"))
stop("Unable to open a connection to file provided for history")
cat(
"## <unitizer> (original history will be restored on exit)\n",
file=hist.con
)
hist.try <- try(
loadhistory(showConnections()[as.character(hist.con), "description"]),
silent=TRUE
)
if(inherits(hist.try, "try-error")) {
warning(conditionMessage(attr(hist.try, "condition")))
attr(hist.con, "no.hist") <- TRUE
}
list(con=hist.con, file=hist.file)
# nocov end
}
history_release <- function(hist.obj) {
if(all(vapply(hist.obj, is.null, logical(1L))))
return(invisible(TRUE))
# nocov start
no.hist <- attr(hist.obj$con, "no.hist")
close(hist.obj$con)
if(isTRUE(attr(hist.obj$file, "hist.tmp"))) file.remove(hist.obj$file)
if(!isTRUE(no.hist)) {
# covr runs non-interactively; can't have history
hist.try <- try(loadhistory(), silent=TRUE)
if(inherits(hist.try, "try-error"))
warning(conditionMessage(attr(hist.try, "condition")))
}
# nocov end
}
history_write <- function(hist.con, data) {
if(is.null(hist.con)) return(invisible(NULL)) # probably in non-interactive
# nocov start
stopifnot(inherits(hist.con, 'connection'), is.character(data))
if(isOpen(hist.con)) {
cat(data, file=hist.con, sep="\n")
if(!isTRUE(attr(hist.con, "no.hist"))) {
hist.save <- try(loadhistory(summary(hist.con)$description), silent=TRUE)
if(inherits(hist.save, "try-error"))
warning(attr(hist.save, "condition"), immediate.=TRUE)
} }
# nocov end
}
## Variation on 'normalizePath' with \code{winslash} Pre-Specified, additionally
## will only return the normalized path if the path actually exists, if not it
## just returns the input.
##
## Note, for a file that doesn't exist, normalizePath may (windows?) or not (OS
## X) prepend the working directory.
##
## @param exists check whether the expanded path actually exists, and if it does
## not return the original path. Set to TRUE for consistent behavior across
## platforms.
normalize_path <- function(path, mustWork=NA, exists=FALSE) {
res <- normalizePath(path, winslash=.Platform$file.sep, mustWork=mustWork)
if(isTRUE(mustWork) || exists) {
res.exists <- file.exists(res)
res[!res.exists] <- path[!res.exists]
}
res
}
# Simplify a Path As Much as Possible to Working Directory
#
# \itemize{
# \item \code{relativize_path} returns a path that can actually be used
# to access an actual file from the current working directory
# \item \code{pretty_path} returns the most readable path that we can
# produce, but may not usable to access an actual file, main difference with
# \code{relativize_path} is that it will figure out if a file is in a
# package and return a path relative to the package directory if it turns
# out that one is shorter than the one produced with relativize path
# \item \code{unique_path} is used to separate out a common path from a list
# of files, the unique paths are returned as a value, with the common
# directory attached as an attribute
# }
#
# There are many types of windows paths that may not be handled correctly.
#
# https://docs.microsoft.com/en-us/dotnet/standard/io/file-path-formats
#
# A big problem is if we try to combine a drive letter path "d:/a/b/c" with an
# absolute path relative to the drive "/a/b/c". In theory the code isn't
# difficult; we could analyze the two paths and per windows rules we know we can
# just add the working directory letter to the path, but then the testing of it
# becomes annoying (essentially need to reproduce this functions code in the
# test proper, which becomes silly). So we're just not testing that scenario.
#
# @param wd NULL or character(1L) resolving to a directory, if NULL will be
# resolved to \code{getwd}; used primarily for testing
# @param only.if.shorter logical(1L) whether to relativize only if the
# resulting \code{path} is shorter than the input
# @keywords internal
relativize_path <- function(path, wd=NULL, only.if.shorter=TRUE, exists=FALSE) {
if(!is.character(path) || any(is.na(path)))
stop("Argument `path` must be character and may not contain NAs")
if(!is.TF(only.if.shorter))
stop("Argument `only.if.shorter` must be TRUE or FALSE")
if(
!is.null(wd) && !is.character(wd) && !identical(length(wd), 1L) &&
!file_test("-d", wd)
)
stop("Argument `wd` must be NULL or a reference of to a directory")
if(is.null(wd)) wd <- getwd()
wd <- try(normalize_path(wd, mustWork=TRUE, exists=exists), silent=TRUE)
res <- if(
!inherits(wd, "try-error") && is.character(.Platform$file.sep) &&
identical(length(.Platform$file.sep), 1L)
) {
norm <- normalize_path(path, mustWork=FALSE, exists=exists)
to.norm <- TRUE # used to be only for existing files, but can't recall why
# Break up into pieces; we re-append "" to make sure the root shows up if
# appropriate
path.pieces <- lapply(
strsplit(norm[to.norm], .Platform$file.sep, fixed=TRUE),
function(x) c("", Filter(x, f=nchar))
)
wd.pieces <- c("",
Filter(
nchar, unlist(strsplit(wd, .Platform$file.sep, fixed=TRUE))
) )
# /a/b/c/d/e
# /a/b/c/F/G
reled <- vapply(
path.pieces,
function(x) {
up.to <- min(length(x), length(wd.pieces))
if(!up.to) return(x)
first.diff <-
min(up.to + 1L, which(x[1:up.to] != wd.pieces[1:up.to])) - 1L
path <- if(identical(first.diff, 0L)) {
x
} else {
end <- min(up.to, first.diff)
c(rep("..", length(wd.pieces) - end), x[-(1:end)])
}
if(length(path)) do.call(file.path, as.list(path)) else ""
},
character(1L)
)
norm[to.norm] <- reled
norm
} else path
res[!nzchar(res)] <- "."
if(only.if.shorter) {
ifelse(nchar(res) < nchar(path), res, path)
} else res
}
pretty_path <- function(path, wd=NULL, only.if.shorter=TRUE) {
path.norm <- normalize_path(path, mustWork=FALSE, exists=TRUE)
rel.path <- relativize_path(path.norm, wd, only.if.shorter, exists=TRUE)
pkg.dir <- get_package_dir(path.norm)
if(
!length(pkg.dir) ||
!identical(substr(path.norm, 1L, nchar(pkg.dir)), pkg.dir)
)
return(rel.path)
pkg.name <- try(get_package_name(pkg.dir))
if(inherits(pkg.name, "try-error")) {
# nocov start
stop("Internal Error: failed getting package name; contact maintainer")
# nocov end
}
pkg.path <- file.path(
paste0("package:", pkg.name),
substr(path.norm, nchar(pkg.dir) + 2L, nchar(path.norm))
)
if(nchar(rel.path) <= nchar(pkg.path)) rel.path else pkg.path
}
unique_path <- function(files) {
stopifnot(is.character(files), !any(is.na(files)))
dirs <- dirname(files)
uniq.dir <- str_reduce_unique(dirs)
com.dir <- substr(dirs[[1L]], 1L, nchar(dirs[[1L]]) - nchar(uniq.dir[[1L]]))
full.dir <- dirs[[1L]]
repeat {
dir.tmp <- dirname(full.dir)
if(
nchar(dir.tmp) < nchar(com.dir) || !nchar(dir.tmp)
|| identical(dir.tmp, ".")
) break
full.dir <- dir.tmp
}
test.files.trim <- if(sum(nchar(uniq.dir))) {
file.path(uniq.dir, basename(files))
} else basename(files)
structure(test.files.trim, common_dir=full.dir)
}
# Merge Two Lists
#
# Values in \code{y} ovewrite existing values in \code{x}. This is similar to
# \code{modifyList} but is non-recursive
#
# @keywords internal
# @param x a list
# @param y a list
merge_lists <- function(x, y, keep.null=TRUE) {
stopifnot(
is.list(x), is.list(y),
!identical(length(names(x)), x), !identical(length(names(y)), y)
)
if(!isTRUE(keep.null)) stop("Currently `keep.null` must be TRUE")
x[names(y)] <- y
x
}
# Comparison functions that output to stdout/stderr for testing of effect of
# doing so (should be captured and ignored, then at end warn about it)
comp_stdout <- function(x, y) {
cat("I'm outputting to stdout\n")
TRUE
}
comp_stderr <- function(x, y) {
cat("I'm outputting to stderr\n", file=stderr())
TRUE
}
comp_stdboth <- function(x, y) {
cat("I'm outputting to both 1\n")
cat("I'm outputting to both 2\n", file=stderr())
TRUE
}
# Cleanup text output that contains "Error|Warning in eval(...)" because of how
# that behaves differently between 3.3.2 and 3.4
clean_eval_exp <- function(x) {
clean_elem <- function(y)
gsub("^(Error|Warning) in eval\\(.*?\\) :", "\\1 in :", y)
if(is.list(x)) {
x[] <- lapply(x, clean_elem)
x
} else if(is.character(x)) clean_elem(x) else x
}
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.