# 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.
#' @include is.R
#' @include capture.R
NULL
.loaded <- FALSE # gets set by .onLoad
.unitizer.global.settings.names <-
c("search.path", "options", "working.directory", "random.seed", "namespaces")
#' Structures For Tracking Global Options
#'
#' Immplemented as S4 classes just so we can ensure everything is guaranteed
#' to have the right slots. This is done by defining a virtual class that has
#' a validity function that checks the required slots exist.
#'
#' Not we don't use "ANY" slots here because that would allow partially
#' specified sub classes (i.e. classes with slots that are "ANY"), which we
#' do not want to allow.
#'
#' \code{unitizerGlobalTrackingStore} is used to keep "compressed" versions of
#' \code{unitizerGlobal$tracking}. The compressed versions obviously lose some
#' information. In particular, environments or things that have environments
#' as parents, or large objects, are not stored and instead a reference to
#' a \code{unitizerDummy} object is stored. This object unambiguously
#' identifies a non-stored object since no user or system code should
#' normally creating a \code{unitizerDummy} object.
#'
#' \code{unitizerGlobalState} tracks a single state which is just one value from
#' each of the slots of \code{unitizerGlobalTrackingStore}
#'
#' When comparing state between new and reference tests, only explicitly stored
#' items are compared (though any extra or missing items may be brought up as
#' possible mismatches).
#'
#' @rdname global_structures
#' @keywords internal
setClass(
"unitizerGlobalBase", contains="VIRTUAL",
validity=function(object)
if(!identical(slotNames(object), .unitizer.global.settings.names)) {
paste0(
"Invalid global object, slots must be ",
deparse(.unitizer.global.settings.names, width.cutoff=500L)
)
TRUE
} )
#' @rdname global_structures
#' @keywords internal
setClass(
"unitizerGlobalStatus", contains="unitizerGlobalBase",
slots=c(
search.path="integer",
options="integer",
working.directory="integer",
random.seed="integer",
namespaces="integer"
),
prototype=list(
search.path=0L, working.directory=0L, options=0L,
random.seed=0L, namespaces=0L
),
validity=function(object) {
for(i in slotNames(object))
if(!is.int.1L(slot(object, i)) || !slot(object, i) %in% 0L:2L)
return(paste0("slot `", i, "` must be integer(1L) and in 0:2"))
TRUE
}
)
#' @rdname global_structures
#' @keywords internal
setClass(
"unitizerGlobalDisabled", contains="unitizerGlobalBase",
slots=c(
search.path="logical",
options="logical",
working.directory="logical",
random.seed="logical",
namespaces="logical"
),
prototype=list(
search.path=FALSE, working.directory=FALSE, options=FALSE,
random.seed=FALSE, namespaces=FALSE
),
validity=function(object) {
for(i in slotNames(object)) if(!is.TF(slot(object, i)))
return(paste0("slot `", i, "` must be TRUE or FALSE"))
TRUE
}
)
#' @rdname global_structures
#' @keywords internal
setClass(
"unitizerGlobalTracking", contains="unitizerGlobalBase",
slots=c(
search.path="list",
options="list",
working.directory="list",
random.seed="list",
namespaces="list"
)
)
#' @rdname global_structures
#' @keywords internal
setClass("unitizerDummy", slots=c(.="NULL"))
#' @rdname unitizer_s4method_doc
setMethod(
"show", "unitizerDummy", function(object) cat("<object not recorded>\n")
)
setClassUnion("listOrNULLOrDummy", c("list", "NULL", "unitizerDummy"))
setClassUnion("characterOrNULLOrDummy", c("character", "NULL", "unitizerDummy"))
setClassUnion("integerOrNULLOrDummy", c("integer", "NULL", "unitizerDummy"))
#' @rdname global_structures
#' @keywords internal
setClass("unitizerGlobalTrackingStore", contains="unitizerGlobalTracking")
#' @rdname global_structures
#' @keywords internal
setClass(
"unitizerGlobalState", contains="unitizerGlobalBase",
slots=c(
search.path="characterOrNULLOrDummy",
options="listOrNULLOrDummy",
working.directory="characterOrNULLOrDummy",
random.seed="integerOrNULLOrDummy",
namespaces="characterOrNULLOrDummy"
)
)
#' @rdname global_structures
#' @keywords internal
setClass(
"unitizerGlobalIndices",
slots=c(
search.path="integer",
options="integer",
working.directory="integer",
random.seed="integer",
namespaces="integer"
),
prototype=list(
search.path=0L, options=0L, working.directory=0L, random.seed=0L,
namespaces=0L
),
validity=function(object){
for(i in slotNames(object))
if(length(slot(object, i)) != 1L || slot(object, i) < 0L)
return(paste0("slot `", i, "` must be integer(1L) and positive"))
TRUE
}
)
#' @rdname unitizer_s4method_doc
setMethod(
"as.integer", "unitizerGlobalIndices",
function(x, ...) {
s.n <- slotNames(x)
res <- setNames(unlist(lapply(s.n, slot, object=x)), s.n)
if(!is.integer(res)) {
# nocov start
stop(
"Internal Error: unable to convert `unitizerGlobalIndices` object to ",
"integer; contact maintainer."
)
# nocov end
}
res
} )
# Create a `unitizerGlobalIndices` object that points to the last stored states;
# used primarily so we can then add more states to the ends and can compute what
# the indices for the added states should be; used by `mergeStates`
setGeneric(
"unitizerStateMaxIndices",
function(x, ...) standardGeneric("unitizerStateMaxIndices")
)
setMethod("unitizerStateMaxIndices", c("unitizerGlobalTrackingStore"),
function(x, ...) {
last.ids <- Map(function(y) length(slot(x, y)), slotNames(x))
do.call("new", c(list("unitizerGlobalIndices"), last.ids))
} )
# Pull out a single state from a tracking object
setGeneric(
"unitizerGlobalStateExtract",
function(x, y, ...) standardGeneric("unitizerGlobalStateExtract")
)
setMethod(
"unitizerGlobalStateExtract",
c("unitizerGlobalTrackingStore", "unitizerGlobalIndices"),
function(x, y, ...) {
vals <- Map(
function(x, y) unlist(x[y], recursive=FALSE),
sapply(.unitizer.global.settings.names, slot, object=x, simplify=FALSE),
lapply(.unitizer.global.settings.names, slot, object=y)
)
do.call("new", c("unitizerGlobalState", vals))
} )
# Reduce size of a tracking object for storage purposes
setGeneric(
"unitizerCompressTracking",
function(x, ...) standardGeneric("unitizerCompressTracking")
)
setMethod(
"unitizerCompressTracking", "unitizerGlobalTracking",
function(x, opts.ignore, ...) {
stopifnot(is.character(opts.ignore))
res <- new("unitizerGlobalTrackingStore")
res@search.path <- lapply(x@search.path, compress_search_data)
res@namespaces <- lapply(x@namespaces, compress_ns_data)
# Don't store stuff with environments or stuff that is too big
# (size cut-off should be an option?), or stuff that is part of the base or
# as.is options
res@options <- Map(
x@options,
f=function(i) {
Map(
setdiff(names(i), opts.ignore),
f=function(j)
if(
!is.null(environment(i[[j]])) || is.environment(i[[j]]) ||
object.size(i[[j]]) > 1000
) new("unitizerDummy") else i[[j]]
) } )
res@working.directory <- x@working.directory # not sure whether this is something that we want to be comparing
res@random.seed <- lapply( # this could be big!!!
x@random.seed,
function(y) if(length(y) > 10L) new("unitizerDummy") else y
)
res
} )
# Get Current Search Path And Namespace Data
#
# These have to be in this file, and not in R/search.R for the setClass for the
# state funs object. Note there are some weird dependency circularities,
# and we're relying on this function not being called until once the full
# package is loaded.
#
# Also, while we track namespaces separately, we need to store them here as well
# as they contain the the package version info we ultimately want to retain;
# this could possibly be improved by just attaching package version, but too
# much work to retrofit existing behavior that blended namespaces and
# search path
search_as_envs <- function() {
sp <- search()
res <- setNames(lapply(seq_along(sp), as.environment), sp)
list(search.path=res, ns.dat=get_namespace_data())
}
# Accessing namespace info in not really documented manner, but muuuch faster
# than using getNamespaceInfo
get_namespace_data <- function() {
sapply(
loadedNamespaces(),
function(x) {
ns <- getNamespace(x)
loc <- ns[[".__NAMESPACE__."]][["path"]]
ver <- ns[[".__NAMESPACE__."]][["spec"]]["version"]
if(is.null(ver)) ver <- ""
if(is.null(loc)) loc <- ""
list(names=x, lib.loc=loc, version=ver)
},
simplify=FALSE
)
}
#' @rdname global_structures
#' @keywords internal
setClass(
"unitizerGlobalStateFuns", contains="unitizerGlobalBase",
slots=c(
search.path="function", options="function", working.directory="function",
random.seed="function", namespaces="function"
),
prototype=list(
search.path=search_as_envs,
options=options,
working.directory=getwd,
random.seed=function()
mget(
".Random.seed", envir=.GlobalEnv, inherits=FALSE, ifnotfound=list(NULL)
)[[1L]],
namespaces=get_namespace_data
)
)
#' @rdname global_structures
#' @keywords internal
setClass(
"unitizerGlobalNsOptConflict",
slots=c(conflict="logical", namespaces="character", file="character"),
prototype=list(conflict=FALSE),
validity=function(object) {
if(!is.TF(conflict)) return("Slot `conflict` must be TRUE or FALSE")
if(!is.chr1(file)) return("Slot `file` must be character(1L) and not NA")
if(any(is.na(namespaces))) return("Slot `namespaces` may not contain NAs")
TRUE
}
)
# Objects / Methods used to Track Global Settings and the Like
#
# Implemented as Reference Class
unitizerGlobal <- setRefClass(
"unitizerGlobal",
fields=list(
par.env="environment",
status="unitizerGlobalStatus",
disabled="unitizerGlobalDisabled",
tracking="unitizerGlobalTracking",
# Implement global object as locked so that it doesn't get overwritten
locked="logical",
set.global="logical",
# Allow us to remember if an error happened on state reset
ns.opt.conflict="unitizerGlobalNsOptConflict",
cons="unitizerCaptConsOrNULL", # Connections for stdout and stderr capture
# store original unitizer options before they get zeroed out
unitizer.opts="list",
state.funs="unitizerGlobalStateFuns",
shim.funs="list",
indices.init="unitizerGlobalIndices",
indices.last="unitizerGlobalIndices",
transcript="logical"
),
methods=list(
initialize=function(
..., disabled=FALSE, enable.which=integer(0L),
par.env=new.env(parent=baseenv()),
unitizer.opts=options()[grep("^unitizer\\.", names(options()))],
set.global=FALSE, transcript=FALSE
) {
obj <- callSuper(
..., par.env=par.env, unitizer.opts=unitizer.opts,
locked=FALSE, transcript=transcript
)
enable(enable.which)
state()
ns.opt.conflict@conflict <<- FALSE
# top level copy for access from other namespaces
if(isTRUE(.global$global$locked)) {
# nocov start
stop(
"Internal Error: global tracking object already exists; this should ",
"never happen; contact maintainer"
)
# nocov end
} else if(set.global) {
# no longer 100% sure, but I believe the .global is used solely so that
# we can have access to the special unitizer parent environment from the
# traced functions (i.e. so we can change the parent when the search
# path changes)
.global$global <- .self
locked <<- TRUE
} else if(.loaded) {
warning(
"Instantiated global object without global namespace registry; ",
"you should only see this warning you are using ",
"`repair_environments`.", immediate.=TRUE
)
}
obj
},
enable=function(
which=setNames(
rep(2L, length(.unitizer.global.settings.names)),
.unitizer.global.settings.names
)
) {
'
Turn on global environment tracking, shouldnt be needed since usually
called during initialization
'
if(!length(which)) return(status)
stopifnot(
is.integer(which), !any(is.na(which)),
!is.null(names(which)) && !any(is.na(names(which))),
all(names(which) %in% .unitizer.global.settings.names),
length(which) == length(unique(names(which)))
)
for(i in names(which)) {
if(slot(disabled, i)) {
warning(
"State setting for `", i, "` has already been disabled and ",
"cannot be re-enabled", immediate.=TRUE
)
next
}
slot(status, i) <<- which[[i]]
}
status
},
disable=function(which=.unitizer.global.settings.names) {
'
Turn off global settings; for `par.env` also unshims the functions used
to enable tracking of topmost environment.
Currently not truly needed but left in for future use since the enable/
disable paradigm is not as important now that `par.env` is being handled
separately
'
stopifnot(
is.character(which), all(!is.na(which)),
all(which %in% .unitizer.global.settings.names)
)
for(i in which) {
slot(status, i) <<- 0L
slot(disabled, i) <<- TRUE
}
status
},
state=function(mode="normal") {
'
Record state for each of the globals we are tracking; one question here is
whether we want more sophisticated checking against existing settings to
avoid repeatedly storing the same thing. For now we just check against
the last one
'
stopifnot(is.chr1(mode), mode %in% c("normal", "init"))
for(i in slotNames(tracking)) {
# Don't record statuses that aren't being tracked
if(!slot(status, i)) next
# Get state with pre-defined function
new.obj <- slot(state.funs, i)()
ref.obj <- if(slot(indices.last, i)) {
slot(tracking, i)[[slot(indices.last, i)]]
} else {
new.env() # guaranteed unique
}
if(!identical(new.obj, ref.obj)) {
slot(tracking, i) <<- append(slot(tracking, i), list(new.obj))
slot(indices.last, i) <<- length(slot(tracking, i))
}
if(identical(mode, "init")) {
slot(indices.init, i) <<- length(slot(tracking, i))
slot(indices.last, i) <<- length(slot(tracking, i))
}
}
indices.last
},
reset=function(to) {
'
Reset global settings to a prior State
'
stopifnot(is(to, "unitizerGlobalIndices"))
success <- TRUE
if(status@search.path && to@search.path){
fail <-
inherits(try(search_path_update(to@search.path, .self)), "try-error")
success <- success && !fail
}
if(status@namespaces && to@namespaces) {
fail <-
inherits(try(namespace_update(to@namespaces, .self)), "try-error")
success <- success && !fail
}
if(status@options && to@options) {
fail <- inherits(
try(options_update(tracking@options[[to@options]])), "try-error"
)
success <- success && !fail
}
if(status@working.directory && to@working.directory) {
fail <- inherits(
try(setwd(tracking@working.directory[[to@working.directory]])),
"try-error"
)
success <- success && !fail
}
if(status@random.seed && to@random.seed) {
if(is.null(tracking@random.seed[[to@random.seed]])) {
if(exists(".Random.seed", .GlobalEnv, inherits=FALSE))
rm(".Random.seed", envir=.GlobalEnv)
} else {
assign(
".Random.seed", tracking@random.seed[[to@random.seed]], .GlobalEnv
) } }
if(!success) stop("Global state reset failed, see prior error.")
indices.last <<- to
indices.last
},
resetInit=function() {
'
Reset global settings to what they were right after initialization scripts
'
reset(indices.init)
},
resetFull=function() {
'
Reset global settings to what they were on first load; par.env doesnt
matter since only meaningful in context of unitizer
'
reset(
new(
"unitizerGlobalIndices", search.path=1L, options=1L,
working.directory=1L, random.seed=1L, namespaces=1L
)
)
},
release=function() {
'
Blow away the global tracking object so that we can re-use for other
sessions
'
locked <<- FALSE
}
) )
# used purely for traced functions that need access to global object; in most
# cases should be just our traced functions, note that we just create this
# object here for test; any time a `unitizer` is instantiated. UPDATE: seems
# this is just used to get access to the special `unitizer` parent env, and to
# implement the non-interactive prompt responses for testing.
.global <- new.env()
# not necessary since any use of `shimFuns` can only occur if a unitizerGlobal
# object is instantiated, so $global will have been created by initialize method
# .global$global <- unitizerGlobal$new()
setClassUnion("unitizerGlobalOrNULL", c("unitizerGlobal", "NULL"))
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.