Nothing
# 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.
#' Set and Retrieve Store Contents
#'
#' These functions are not used directly; rather, they are used by
#' \code{\link{unitize}} to get and set the \code{unitizer} objects.
#' You should only need to understand these functions if you are
#' looking to implement a special storage mechanism for the \code{unitizer}
#' objects.
#'
#' By default, only a character method is defined, which will interpret
#' its inputs as a filesystem path to the \code{unitizer} folder. RDSes of
#' serialization type 2 will be stored and retrieved from there. The
#' serialization format may change in the future, but if R maintains
#' facilities to read/write type 2, we will provide the option to use
#' that format. At this time there is no API to change the serialization
#' format.
#'
#' You may write your own methods for special storage situations (
#' e.g SQL database, ftp server, etc) with the understanding that the
#' getting method may only accept one argument, the \code{store.id}, and
#' the setting method only two arguments, the \code{store.id} and the
#' \code{unitizer}.
#'
#' S3 dispatch will be on \code{store.id}, and \code{store.id} may
#' be any R object that identifies the unitizer. For example, a potential
#' SQL implementation where the unitizers get stored in blobs may look
#' like so:
#' \preformatted{
#' my.sql.store.id <- structure(
#' list(
#' server="myunitizerserver.mydomain.com:3306",
#' database="unitizers",
#' table="project1",
#' id="cornercasetests"
#' ),
#' class="sql_unitizer"
#' )
#' get_unitizer.sql_unitizer <- function(store.id) { # FUNCTION BODY }
#' set_unitizer.sql_unitizer <- function(store.id, unitizer) { # FUNCTION BODY }
#'
#' unitize("unitizer/cornertestcases.R", my.sql.store.id)
#' }
#' Make sure you also define an \code{as.character} method for your object to
#' produce a human readable identifying string.
#'
#' For inspirations for the bodies of the _store functions look at the source
#' code for \code{unitizer:::get_unitizer.character} and
#' \code{unitizer:::set_unitizer.character}.
#' Expectations for the functions are as follows. \code{get_unitizer} must:
#' \itemize{
#' \item return a \code{unitizer-class} object if \code{store.id}
#' exists and contains a valid object
#' \item return FALSE if the object doesn't exist (e.g. first time
#' run-through, so reference copy doesn't exist yet)
#' \item \code{\link{stop}} on error
#' }
#' \code{set_unitizer} must:
#' \itemize{
#' \item return TRUE on success
#' \item \code{\link{stop}} on error
#' }
#'
#' @aliases get_unitizer
#' @seealso \code{\link{saveRDS}}
#' @export
#' @param store.id a filesystem path to the store (an .rds file)
#' @param unitizer a \code{unitizer-class} object containing the store
#' data
#' @return
#' \itemize{
#' \item set_unitizer TRUE if unitizer storing worked, error otherwise
#' \item get_unitizer a \code{unitizer-class} object, FALSE
#' if \code{store.id} doesn't exist yet, or error otherwise; note that
#' the \code{unitizer_results} method returns a list
#' }
set_unitizer <- function(store.id, unitizer) {
UseMethod("set_unitizer")
}
#' @export
set_unitizer.default <- function(store.id, unitizer) {
stop(
"No method defined for object of class \"", class(store.id)[[1]], "\"; ",
"make sure that the specified `store.id` is a reference to a valid ",
"unitizer store and had defined `get_unitizer` and `set_unitizer` methods."
)
}
#' @export
set_unitizer.character <- function(store.id, unitizer) {
if(!is.character(store.id) || length(store.id) != 1L) {
stop("Argument `store.id` must be a 1 length character vector")
}
if(is.object(store.id) || !is.null(attributes(store.id)))
stop("Argument `store.id` must be a bare character vector")
if(!is(unitizer, "unitizer")) stop("Argument `unitizer` must be a unitizer")
new.file <- FALSE
if(!file.exists(store.id)) {
if(!isTRUE(dir.create(store.id)))
stop(
"Could not create `store.id`; make sure it is a valid file name; see ",
"warning for details"
)
} else if (!file_test("-d", store.id)) {
stop("'", store.id, "' is not a directory.")
}
if(
inherits(
try(
saveRDS(unitizer, paste0(store.id, "/data.rds"), version=2)),
"try-error"
)
) {
stop("Failed setting unitizer; see prior error messages for details.")
}
TRUE
}
#' @rdname set_unitizer
#' @export
get_unitizer <- function(store.id) {
UseMethod("get_unitizer")
}
#' @rdname set_unitizer
#' @export
get_unitizer.character <- function(store.id) {
if(!is.character(store.id) || length(store.id) != 1L ) {
stop("Argument `store.id` must be a 1 length character vector")
}
if(is.object(store.id) || !is.null(attributes(store.id)))
stop("Argument `store.id` must be a bare character vector")
if(!file.exists(store.id)) return(FALSE)
if(!is_unitizer_dir(store.id)) {
stop(
"Argument `store.id` does not appear to refer to a unitizer directory"
)
}
if(inherits(try(unitizer <- readRDS(paste0(store.id, "/data.rds"))), "try-error")) {
stop("Failed loading unitizer; see prior error messages for details")
}
if(!is(unitizer, "unitizer")) stop("Retrieved object is not a unitizer store")
# if(!identical(path_clean(store.id), path_clean(unitizer@id))) {
# stop(
# "This check needs to be modified to not make any assumptions about ",
# "unitizer structure since we don't know it is conforming yet"
# )
# if(is.character(unitizer@id) & length(unitizer@id) == 1L) {
# # The following warning occurred more often than not as a result of changes
# # in working directory, so just quashing for now; could use `normalizePath`
# # instead...
# # warning(
# # "ID in retrieved unitizer (", unitizer@id, ") doesn't match `store.id`; this may ",
# # "be happening because you moved the store relative to the script that created it",
# # immediate. = TRUE
# # )
# } else {
# stop(
# "Internal Error: ID in retrieved unitizer is not a 1 length character vector as expected ",
# "(typeof: ", typeof(unitizer@id), ", length: ", length(unitizer@id),"); contact maintainer."
# )
# } }
unitizer
}
#' @rdname set_unitizer
#' @export
get_unitizer.default <- function(store.id) {
stop(
"No method defined for object of class \"", class(store.id)[[1]], "\"; ",
"make sure that the specified `store.id` is a reference to a valid ",
"unitizer store and had defined `get_unitizer` and `set_unitizer` methods."
)
}
#' @rdname set_unitizer
#' @export
get_unitizer.unitizer_result <- function(store.id) {
store.id <- attr(store.id, "store.id")
get_unitizer(store.id)
}
# used purely for testing, but has to be exported
#' @export
get_unitizer.unitizer_error_store <- function(store.id)
structure("error", class="unitizer_store_error")
#' @rdname set_unitizer
#' @export
get_unitizer.unitizer_results <- function(store.id) {
lapply(store.id, get_unitizer)
}
#' Infers Possible Unitizer Path From Context
#'
#' Used by most \code{unitizer} functions that operate on \code{unitizer}s to
#' make it easy in interactive use to specify the most likely intended
#' \code{unitizer} in a package or a directory. For `R CMD check` and similar
#' testing should not rely on this functionality.
#'
#' This is implemented as an S3 generic to allow third parties to define
#' inference methods for other types of \code{store.id}, but the documentation
#' here is for the \code{"character"} method which is what \code{unitizer} uses
#' by default.
#'
#' If \code{store.id} is a directory that appears to be an R package (contains
#' DESCRIPTION, an R folder, a tests folder), will look for candidate files in
#' \code{file.path(store.id, "tests", "unitizer")}, starting with files with the
#' same name as the package (ending in ".R" or ".unitizer" if \code{type} is
#' \code{"f"} or \code{"u"} respectively), or if there is only one file, that
#' file, or if there are multiple candidate files and in interactive mode
#' prompting user for a selection. If \code{type} is \code{"d"}, then will
#' just provide the \code{"tests/unitizer"} directory.
#'
#' If \code{name} is not a directory, will try to find a file by that name, and
#' if that fails, will try to partially match a file by that name. Partial
#' matching requires the front portion of the name to be fully specified and
#' no extension be provided (e.g. for \code{"mytests.R"}, \code{"myt"} is valid,
#' but \code{"tests"} and \code{"myt.R"} are both invalid). Partially specified
#' files may be specified in subdirectories (e.g. \code{"tests/myt"}).
#'
#' Inference assumes your files end in \code{".R"} for code files and
#' \code{".unitizer"} for \code{unitizer} data directories.
#'
#' If \code{store.id} is NULL, the default \code{infer_unitizer_location} method
#' will attempt to find the top level package directory and then call the
#' character method with that directory as \code{store.id}. If the parent
#' package directory cannot be found, then the character method is called with
#' the current directory as the argument.
#'
#' @export
#' @seealso \code{\link{get_unitizer}} for discussion of alternate
#' \code{store.id} objects
#' @param store.id character(1L) file or directory name, the file name portion
#' (i.e after the last slash) may be partially specified
#' @param type character(1L) in \code{c("f", "u", "d")}, \code{"f"} for test
#' file, \code{"d"} for a directory, \code{"u"} for a \code{unitizer}
#' directory
#' @param interactive.mode logical(1L) whether to allow user input to resolve
#' ambiguities
#' @param ... arguments to pass on to other methods
#' @return character(1L) an inferred path, or \code{store.id} with a warning if
#' path cannot be inferred
infer_unitizer_location <- function(store.id, ...)
UseMethod("infer_unitizer_location")
#' @rdname infer_unitizer_location
#' @export
infer_unitizer_location.default <- function(store.id, ...) {
if(is.null(store.id)) {
def.dir <- if(
length(pkg.dir <- get_package_dir(".")) &&
file_test("-d", file.path(pkg.dir, "tests", "unitizer"))
) {
file.path(pkg.dir, "tests", "unitizer")
} else "."
return(infer_unitizer_location.character(def.dir, ...))
}
store.id
}
#' @rdname infer_unitizer_location
#' @export
infer_unitizer_location.character <- function(
store.id, type="f", interactive.mode=interactive(), ...
) {
if(!is.character(store.id) || length(store.id) != 1L || is.na(store.id))
stop("Argument `store.id` must be character(1L) and not NA")
if(
!is.character(type) || length(type) != 1L ||
!isTRUE(type %in% c("f", "u", "d"))
)
stop("Argument `type` must be one of `c(\"f\", \"u\", \"d\")`")
if(!isTRUE(interactive.mode) && !identical(interactive.mode, FALSE))
stop("Argument `interactive.mode` must be TRUE or FALSE")
# BEWARE, you can't just change `text.ext` here without reviewing how it is
# used
if(type == "f") {
test.fun <- function(x) file_test("-f", x)
test.ext <- ".R"
list.fun <- list.files
type.name <- "test file"
} else if(type == "u") {
test.fun <- is_unitizer_dir
test.ext <- ".unitizer"
list.fun <- list.dirs
type.name <- "unitizer"
} else if(type == "d") {
test.fun <- function(x) file_test("-d", x)
test.ext <- NULL
list.fun <- list.dirs
type.name <- "test directory"
}
inf_msg <- function(name)
meta_word_msg(
"Inferred", type.name, "location:", relativize_path(name), sep=" "
)
# Is a directory, check if a package and pick tests/unitizer as the directory
if(!file_test("-d", store.id)) {
dir.store.id <- dirname(store.id)
file.store.id <- basename(store.id)
} else {
dir.store.id <- store.id
file.store.id <- NULL
}
dir.store.id <- normalize_path(dir.store.id)
at.package.dir <-
file_test("-d", dir.store.id) && isTRUE(is_package_dir(dir.store.id))
# Check for exact match first and return that if found, unless we are in dir
# mode and the directory is a package directory
if(!(identical(type, "d") && at.package.dir) && test.fun(store.id))
return(store.id)
# If we're not at the package directory, check to see if we are in a package
# directory and change directory to that, but only do that if we're not
# already matching an actual directory
if(
!at.package.dir && !is.null(file.store.id) &&
length(pkg.dir.tmp <- get_package_dir(dir.store.id))
) {
at.package.dir <- TRUE
dir.store.id <- pkg.dir.tmp
}
if(at.package.dir) {
test.base <- file.path(dir.store.id, "tests", "unitizer")
if(!file_test("-d", test.base))
stop("Unable to infer path since \"tests/unitizer\" directory is missing")
found.file <- test.fun(
fp <- do.call(
file.path,
as.list(
c(test.base, paste0(file.store.id, basename(dir.store.id), test.ext))
) ) )
if(found.file) {
inf_msg(fp)
return(fp)
}
# use tests/unitizer as starting point for any package
dir.store.id.proc <- test.base
} else {
dir.store.id.proc <- dir.store.id
}
# Check request is coherent already and if so return
f.path <- do.call(file.path, as.list(c(dir.store.id.proc, file.store.id)))
if(test.fun(f.path)) {
inf_msg(f.path)
return(f.path)
}
# Resolve potential ambiguities by trying to find file / directory
candidate.files <- grep(
paste0("^", file.store.id, if(!is.null(test.ext)) ".*\\", test.ext, "$"),
basename(list.fun(dir.store.id.proc, recursive=FALSE)),
value=TRUE
)
cand.len <- length(candidate.files)
selection <- if(cand.len > 1L) {
if(!interactive.mode || cand.len > 100L) {
warning(
cand.len, " possible targets; ",
if(interactive.mode) "cannot" else "too many to",
" unambiguously infer desired file",
immediate.=TRUE
)
return(store.id)
}
dir.disp <- if(!identical(dir.store.id, dir.store.id.proc)) {
paste0(
" from \"",
sub(
paste0("^", normalize_path(dir.store.id), "/?"), "",
normalize_path(dir.store.id.proc)
),
"\""
)
} else ""
# Select one:
valid <- seq_along(candidate.files)
cat(paste0("Possible matching files", dir.disp, ":\n"))
cat(paste0(" ", format(valid), ": ", candidate.files), sep="\n")
pick <- unitizer_prompt(
"Pick a matching file",
valid.opts=c("Type a number"),
exit.condition=exit_fun, valid.vals=valid,
hist.con=NULL, global=NULL, browse.env=.GlobalEnv
)
if(identical(pick, "Q")) {
message("No file selected")
0L
} else {
pick <- as.integer(pick)
message("Selected file: ", candidate.files[[pick]])
pick
}
} else if (cand.len == 1L) {
1L
} else if (cand.len == 0L) {
warning("No possible matching files for '", store.id, "'", immediate.=TRUE)
return(store.id)
}
if(!selection && interactive.mode) {
warning("Invalid user selection", immediate.=TRUE)
return(store.id)
} else if(!selection) {
# nocov start
stop(
"Internal Error: should never have non.interactive zero selection; ", "
contact maintainer."
)
# nocov end
}
# Return
file.final <- file.path(dir.store.id.proc, candidate.files[[selection]])
if(cand.len == 1L) inf_msg(file.final)
file.final
}
# Check Whether Directories Are Likely R Package Source Directories
#
# Heuristic check to see whether a directory contains what likely could be
# built into an R package. This is based on the DESCRIPTION file and directory
# structure.
#
# \code{is_package_dir} checks whether a directory is the top level directory
# of a package.
#
# \code{get_package_dir} checks whether a directory or any of its parents is the
# top level directory of a package, and returns the top level package directory
# or character(0L) if not
#
# @keywords internal
# @param name a directory to check for package-ness
# @param has.tests whether to require that the package have tests to qualify
# @param DESCRIPTION the DESCRIPTION file path
# @return TRUE if criteria met, character vector explaining first failure
# otherwise
is_package_dir <- function(name, has.tests=FALSE) {
stopifnot(is.character(name), is.TF(has.tests))
if(!is.character(name)) return("not character so cannot be a directory")
if(!file_test("-d", name)) return("not an existing directory")
pkg.name <- try(get_package_name(name), silent=TRUE)
if(inherits(pkg.name, "try-error"))
return(conditionMessage(attr(pkg.name, "condition")))
# Has requisite directories?
if(!file_test("-d", file.path(name, "R")))
return("missing 'R' directory")
if(has.tests && !file_test("-d", file.path(name, "tests")))
return("missing 'tests' directory")
# Woohoo
TRUE
}
get_package_dir <- function(name=getwd(), has.tests=FALSE, exists=FALSE) {
stopifnot(
is.character(name), !any(is.na(name)), is.TF(has.tests),
as.logical(length(name))
)
name <- normalize_path(name, mustWork=FALSE, exists=exists)
if(length(name) > 1L) name <- attr(unique_path(name), "common_dir")
is.package <- FALSE
prev.dir <- par.dir <- name
repeat {
if(isTRUE(is_package_dir(par.dir, has.tests))) {
return(par.dir)
} else if (isTRUE(is_rcmdcheck_dir(par.dir, has.tests))) {
return(get_rcmdcheck_dir(par.dir, has.tests))
} else if (isTRUE(is_testinstpkg_dir(par.dir, has.tests))) {
return(get_testinstpkg_dir(par.dir, has.tests))
}
if(nchar(par.dir <- dirname(prev.dir)) >= nchar(prev.dir)) break
prev.dir <- par.dir
}
character(0L)
}
# Checks Whether a Directory Could be of the Type Used by R CMD check
is_rcmdcheck_dir <- function(name, has.tests=FALSE) {
stopifnot(is.chr1(name), is.TF(has.tests))
dir <- basename(name)
if(grepl(".*\\.Rcheck$", dir)) {
pkg.name <- sub("(.*)\\.Rcheck", "\\1", dir)
if(identical(pkg.name, dir))
stop(
"Logic error; failed extracting package name from Rcheck dir; ",
"contact maintianer"
)
if(isTRUE(is.pd <- is_package_dir(file.path(name, pkg.name), has.tests))) {
return(TRUE)
} else return(is.pd)
} else return("not a .Rcheck directory")
}
# Checks Whether a Directory could be of the type generated by
# tools::testInstalledPackages
is_testinstpkg_dir <- function(name, has.tests=FALSE) {
stopifnot(is.chr1(name), is.TF(has.tests))
dir <- basename(name)
if(grepl(".*-tests$", dir)) {
pkg.name <- sub("(.*)-tests$", "\\1", dir)
if(identical(pkg.name, dir))
stop(
"Internal error; failed extracting package name from ",
"testInstalledPackages dir; contact maintainer."
)
if(
isTRUE(
is.pd <- is_package_dir(file.path(dirname(name), pkg.name), has.tests)
)
) {
return(TRUE)
} else return(is.pd)
} else "No *-tests directories"
}
# Extracts the Source Directory from an R CMD check directory
get_rcmdcheck_dir <- function(name, has.tests=FALSE) {
stopifnot(is.chr1(name), is.TF(has.tests))
if(isTRUE(chk.dir <- is_rcmdcheck_dir(name, has.tests))) {
pkg.name <- sub("(.*)\\.Rcheck", "\\1", basename(name))
return(file.path(name, pkg.name))
} else stop("Internal Error: not an R CMD check dir") # nocov
}
# Extracts the Source Directory from an testInstalledPackage directory, looks
# like this will be a bit of a bear to test, see the code in covr that uses
# tools::testInstalledPackage for some potential things to worry about
get_testinstpkg_dir <- function(name, has.tests=FALSE) {
stopifnot(is.chr1(name), is.TF(has.tests))
if(isTRUE(chk.dir <- is_testinstpkg_dir(name, has.tests))) {
pkg.name <- sub("(.*)-tests$", "\\1", basename(name))
return(file.path(dirname(name), pkg.name))
} else stop("Internal Error: not an testInstalledPackage dir") # nocov
}
# Pulls Out Package Name from DESCRIPTION File
#
# Dir must be a package directory, check with is_package_dir first
get_package_name <- function(pkg.dir) {
stopifnot(is.chr1(pkg.dir))
DESCRIPTION <- file.path(pkg.dir, "DESCRIPTION")
if(!file_test("-f", DESCRIPTION)) stop("No DESCRIPTION file")
desc <- try(readLines(DESCRIPTION))
if(inherits(desc, "try-error")) stop("Unable to read DESCRIPTION file")
pkg.pat <- "^\\s*package:\\s+(\\S+)\\s*$"
desc.pkg <- grep(pkg.pat, desc, value=T, perl=T, ignore.case=TRUE)
if(length(desc.pkg) != 1L)
stop(
"DESCRIPTION file ",
if(length(desc.pkg)) "had more than one" else "did not have a",
" package name entry"
)
desc.pkg.name <- sub(pkg.pat, "\\1", desc.pkg, perl=T, ignore.case=TRUE)
return(desc.pkg.name)
}
# Check Whether a Directory as a Unitizer Data Directory
#
# Just checks that it \emph{could} be a data directory, the test ultimately is
# to attempt a \code{\link{get_unitizer}} call and see if we actually resurrect
# a working \code{unitizer}
#
# @keywords internal
# @param dir character(1L) directory to check
# @return logical(1L)
is_unitizer_dir <- function(dir)
is.character(dir) && length(dir) == 1L && !is.na(dir) &&
file_test("-d", dir) && file_test("-f", file.path(dir, "data.rds"))
# Run \code{sys.source} On All Provided Files
#
# Sorts them before sourcing on the paths as specified in \code{files}, returns
# an environment containing any objects created by the code in the source
# files. If a file is a directory, the files in that directory are sourced,
# though only top level files in that directory are sourced.
#
# @keywords internal
# @param files character() pointing to files or directories
# @param env an environment
# @return environment, or a character message explaining why it failed
source_files <- function(files, env.par, pattern="\\.[rR]$") {
stopifnot(
is.character(files),
is.chr1(pattern),
!inherits(try(grepl(pattern, "a"), silent=TRUE), "try-error"),
is.environment(env.par)
)
file.norm <- try(normalize_path(files, mustWork=TRUE))
if(inherits(file.norm, "try-error"))
return("Unable to normalize file paths; see previous error")
env <- new.env(parent=env.par)
for(i in sort(file.norm)) {
sub.files <- if(file_test("-d", i)) {
dir.cont <- try(
list.files(
i, pattern=pattern, all.files=TRUE, full.names=TRUE, no..=TRUE
) )
if(inherits(dir.cont, "try-error"))
return("Unable to list file contents of directory; see previous error")
dir.cont
} else i
for(j in sub.files) {
fail <- inherits(try(sys.source(j, env)), "try-error")
if(fail)
return(paste0("Error sourcing file `", j, "`, see above for details"))
}
}
env
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.