diff --git a/DESCRIPTION b/DESCRIPTION index ab925215..8fe334d3 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -14,8 +14,8 @@ Description: Provides the core framework for a discrete event system to URL: https://spades-core.predictiveecology.org/, https://github.com/PredictiveEcology/SpaDES.core -Date: 2024-06-12 -Version: 2.1.5.9000 +Date: 2024-06-14 +Version: 2.1.5.9002 Authors@R: c( person("Alex M", "Chubaty", , "achubaty@for-cast.ca", role = c("aut"), comment = c(ORCID = "0000-0001-7146-8135")), @@ -102,6 +102,7 @@ Collate: 'code-checking.R' 'convertToPackage.R' 'copy.R' + 'createDESCRIPTIONandDocs.R' 'debugging.R' 'downloadData.R' 'simulation-parseModule.R' diff --git a/NAMESPACE b/NAMESPACE index acafbea7..192dc7cd 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -84,6 +84,7 @@ export(conditionalEvents) export(convertTimeunit) export(convertToPackage) export(copyModule) +export(createDESCRIPTIONandDocs) export(createsOutput) export(current) export(currentModule) diff --git a/R/convertToPackage.R b/R/convertToPackage.R index 6a08f23c..38b5755c 100644 --- a/R/convertToPackage.R +++ b/R/convertToPackage.R @@ -131,8 +131,6 @@ #' tmpdir <- tempdir2() #' newModule("test", tmpdir, open = FALSE) #' convertToPackage("test", path = tmpdir) -#' pkgload::load_all(file.path(tmpdir, "test")) -#' pkgload::unload("test") #' } #' convertToPackage <- function(module = NULL, path = getOption("spades.modulePath"), @@ -145,13 +143,16 @@ convertToPackage <- function(module = NULL, path = getOption("spades.modulePath" mainModuleFile <- file.path(path, unlist(module), paste0(unlist(module), ".R")) packageFolderName <- dirname(mainModuleFile) aa <- parse(mainModuleFile, keep.source = TRUE) - rlaa <- readLines(mainModuleFile) gpd <- getParseData(aa) + rlaa <- readLines(mainModuleFile) + defModule <- grepl("^defineModule", aa) whDefModule <- which(defModule) whNotDefModule <- which(!defModule) + linesWithDefModule <- gpd[grep("defineModule", gpd$text) - 1, ][, c("line1", "line2")] + doEvent <- grepl(paste0("^doEvent.", module), aa) whDoEvent <- which(doEvent) whNoDoEvent <- which(!doEvent & !defModule) @@ -168,67 +169,67 @@ convertToPackage <- function(module = NULL, path = getOption("spades.modulePath" linesWithRoxygen <- parseWithRoxygen[, "line1"] nextElement <- c(whNotDefModule[-1], Inf) - fileNames <- Map(element = whDefModule, nextElement = whNotDefModule[1], - function(element, nextElement) { - i <- 0 - fn <- filePath <- fnCh <- parseWithFn <- lineWithFn <- list() - for (elem in c(element, nextElement)) { - i <- i + 1 - if (is.infinite(elem)) { - lineWithFn[[i]] <- length(rlaa) + 1 - break - } - fn[[i]] <- aa[[elem]][[2]] - filePath[[i]] <- filenameFromFunction(packageFolderName, fn[[i]], "R") - fnCh[[i]] <- as.character(fn[[i]]) - gpdLines <- which(gpd$text == fnCh[[i]] & gpd$token == "SYMBOL") - if (length(gpdLines) > 1) - for (gl in gpdLines) { - line1 <- gpd[gl, "line1"] - isTop <- any(gpd[gpd[, "line1"] == line1, "parent"] == 0) - if (isTRUE(isTop)) { - gpdLines <- gl - break - } - } - parseWithFn[[i]] <- gpd[gpdLines, ] - lineWithFn[[i]] <- parseWithFn[[i]][, "line1"] - if (length(lineWithFn[[i]]) > 1) { - if (i == 1) { - lineWithFn[[1]] <- lineWithFn[[1]][1] - } else { - whAfterLine1 <- which(lineWithFn[[2]] > lineWithFn[[1]]) - if (length(whAfterLine1)) - lineWithFn[[2]] <- lineWithFn[[2]][whAfterLine1[1]] - } - } - } - fn <- filenameForMainFunctions(module, path) - cat("#' @export", file = fn, sep = "\n", append = FALSE) - cat(rlaa[lineWithFn[[2]]:length(rlaa)], - file = fn, sep = "\n", append = TRUE) - cat(rlaa[1:(lineWithFn[[2]] - 1)], file = mainModuleFile, - sep = "\n", append = FALSE) - }) - - otherStuffFn <- filenameFromFunction(packageFolderName, "other", "R") - cat(" -makeActiveBinding('mod', SpaDES.core:::activeModBindingFunction, ", - paste0('asNamespace(SpaDES.core:::.moduleNameNoUnderscore(\'', module, '\'))'), ") - -makeActiveBinding('Par', SpaDES.core:::activeParBindingFunction, ", - paste0('asNamespace(SpaDES.core:::.moduleNameNoUnderscore(\'', module, '\'))'), ") - -", file = otherStuffFn) - - if (length(linesWithRoxygen) > 0) { - message("There was some roxygen2 documentation that was not immediately above ", - "a function; it is being saved in R/documentation.R ... please confirm that ", - "the documentation is correct.") - cat(rlaa[linesWithRoxygen], file = filenameFromFunction(packageFolderName, "documentation", "R") - , sep = "\n", append = FALSE) - linesWithRoxygen <- character() - } + # fileNames <- Map(element = whDefModule, nextElement = whNotDefModule[1], + # function(element, nextElement) { + # i <- 0 + # fn <- filePath <- fnCh <- parseWithFn <- lineWithFn <- list() + # for (elem in c(element, nextElement)) { + # i <- i + 1 + # if (is.infinite(elem)) { + # lineWithFn[[i]] <- length(rlaa) + 1 + # break + # } + # fn[[i]] <- aa[[elem]][[2]] + # filePath[[i]] <- filenameFromFunction(packageFolderName, fn[[i]], "R") + # fnCh[[i]] <- as.character(fn[[i]]) + # gpdLines <- which(gpd$text == fnCh[[i]] & gpd$token == "SYMBOL") + # if (length(gpdLines) > 1) + # for (gl in gpdLines) { + # line1 <- gpd[gl, "line1"] + # isTop <- any(gpd[gpd[, "line1"] == line1, "parent"] == 0) + # if (isTRUE(isTop)) { + # gpdLines <- gl + # break + # } + # } + # parseWithFn[[i]] <- gpd[gpdLines, ] + # lineWithFn[[i]] <- parseWithFn[[i]][, "line1"] + # if (length(lineWithFn[[i]]) > 1) { + # if (i == 1) { + # lineWithFn[[1]] <- lineWithFn[[1]][1] + # } else { + # whAfterLine1 <- which(lineWithFn[[2]] > lineWithFn[[1]]) + # if (length(whAfterLine1)) + # lineWithFn[[2]] <- lineWithFn[[2]][whAfterLine1[1]] + # } + # } + # } + # fn <- filenameForMainFunctions(module, path) + # cat("#' @export", file = fn, sep = "\n", append = FALSE) + # cat(rlaa[lineWithFn[[2]]:length(rlaa)], + # file = fn, sep = "\n", append = TRUE) + # cat(rlaa[1:(lineWithFn[[2]] - 1)], file = mainModuleFile, + # sep = "\n", append = FALSE) + # }) + +# otherStuffFn <- filenameFromFunction(packageFolderName, "other", "R") +# cat(" +# makeActiveBinding('mod', SpaDES.core:::activeModBindingFunction, ", +# paste0('asNamespace(SpaDES.core:::.moduleNameNoUnderscore(\'', module, '\'))'), ") +# +# makeActiveBinding('Par', SpaDES.core:::activeParBindingFunction, ", +# paste0('asNamespace(SpaDES.core:::.moduleNameNoUnderscore(\'', module, '\'))'), ") +# +# ", file = otherStuffFn) + + # if (length(linesWithRoxygen) > 0) { + # message("There was some roxygen2 documentation that was not immediately above ", + # "a function; it is being saved in R/documentation.R ... please confirm that ", + # "the documentation is correct.") + # cat(rlaa[linesWithRoxygen], file = filenameFromFunction(packageFolderName, "documentation", "R") + # , sep = "\n", append = FALSE) + # linesWithRoxygen <- character() + # } filePathImportSpadesCore <- filenameFromFunction(packageFolderName, "imports", "R")# file.path(dirname(mainModuleFile), "R", "imports.R") @@ -236,85 +237,27 @@ makeActiveBinding('Par', SpaDES.core:::activeParBindingFunction, ", md <- aa[[whDefModule]][[3]] deps <- unlist(eval(md$reqdPkgs)) - - dFile <- DESCRIPTIONfileFromModule(module, md, deps, hasNamespaceFile, NAMESPACEFile, filePathImportSpadesCore, - packageFolderName) - # d <- list() - # d$Package <- .moduleNameNoUnderscore(module) - # d$Type <- "Package" - # - # d$Title <- md$name - # d$Description <- md$description - # d$Version <- as.character(eval(md$version[[2]])) - # d$Date <- Sys.Date() - # d$Authors <- md$authors - # d$Authors <- c(paste0(" ", format(d$Authors)[1]), format(d$Authors)[-1]) - # - # - # hasSC <- grepl("SpaDES.core", deps) - # if (all(!hasSC)) - # deps <- c("SpaDES.core", deps) - # - # d$Imports <- Require::extractPkgName(deps) - # versionNumb <- Require::extractVersionNumber(deps) - # hasVersionNumb <- !is.na(versionNumb) - # inequality <- paste0("(", gsub("(.+)\\((.+)\\)", "\\2", deps[hasVersionNumb]), ")") - # missingSpace <- !grepl("[[:space:]]", inequality) - # if (any(missingSpace)) - # inequality[missingSpace] <- gsub("([=><]+)", "\\1 ", inequality[missingSpace]) - # - # namespaceImports <- d$Imports - # # Create "import all" for each of the packages, unless it is already in an @importFrom - # if (hasNamespaceFile) { - # nsTxt <- readLines(NAMESPACEFile) - # hasImportFrom <- grepl("importFrom", nsTxt) - # if (any(hasImportFrom)) { - # pkgsNotNeeded <- unique(gsub(".+\\((.+)\\,.+\\)", "\\1", nsTxt[hasImportFrom])) - # namespaceImports <- grep(paste(pkgsNotNeeded, collapse = "|"), - # namespaceImports, invert = TRUE, value = TRUE) - # } - # } - # - # cat(paste0("#' @import ", namespaceImports, "\nNULL\n"), sep = "\n", - # file = filePathImportSpadesCore, fill = TRUE) - # - # d$Imports[hasVersionNumb] <- paste(d$Imports[hasVersionNumb], inequality) - # - # dFile <- filenameFromFunction(packageFolderName, "DESCRIPTION", fileExt = "") - # - # cat(paste("Package:", d$Package), file = dFile, sep = "\n") - # cat(paste("Type:", d$Type), file = dFile, sep = "\n", append = TRUE) - # cat(paste("Title:", d$Title), file = dFile, sep = "\n", append = TRUE) - # cat(paste("Version:", d$Version), file = dFile, sep = "\n", append = TRUE) - # cat(paste("Description:", paste(d$Description, collapse = " ")), file = dFile, sep = "\n", append = TRUE) - # cat(paste("Date:", d$Date), file = dFile, sep = "\n", append = TRUE) - # cat(c("Authors@R: ", format(d$Authors)), file = dFile, sep = "\n", append = TRUE) - # - # if (length(d$Imports)) - # cat(c("Imports:", paste(" ", d$Imports, collapse = ",\n")), sep = "\n", file = dFile, append = TRUE) - # - # Suggests <- c('knitr', 'rmarkdown') - # cat(c("Suggests:", paste(" ", Suggests, collapse = ",\n")), sep = "\n", file = dFile, append = TRUE) - # - # cat("Encoding: UTF-8", sep = "\n", file = dFile, append = TRUE) - # cat("License: GPL-3", sep = "\n", file = dFile, append = TRUE) - # cat("VignetteBuilder: knitr, rmarkdown", sep = "\n", file = dFile, append = TRUE) - # cat("ByteCompile: yes", sep = "\n", file = dFile, append = TRUE) - # cat("Roxygen: list(markdown = TRUE)", sep = "\n", file = dFile, append = TRUE) - # - # - # message("New/updated DESCRIPTION file is: ", dFile) + dFile <- DESCRIPTIONfileFromModule( + module, md, deps, hasNamespaceFile, NAMESPACEFile, + filePathImportSpadesCore, packageFolderName) if (isTRUE(buildDocuments)) { - message("Building documentation") - m <- packageFolderName - roxygen2::roxygenise(m, roclets = NULL) # This builds documentation, but also exports all functions ... - pkgload::dev_topic_index_reset(m) - pkgload::unload(.moduleNameNoUnderscore(basename2(m))) # so, unload here before reloading without exporting + documentModule(packageFolderName, gpd, linesWithDefModule) + # message("Building documentation") + # m <- packageFolderName + # tmpSrcForDoc <- "R/tmp.R" + # cat(rlaa[-(linesWithDefModule[[1]]:linesWithDefModule[[2]])], sep = "\n", file = tmpSrcForDoc) + # on.exit(unlink(tmpSrcForDoc)) + # roxygen2::roxygenise(m, roclets = NULL) # This builds documentation, but also exports all functions ... + # pkgload::dev_topic_index_reset(m) + # pkgload::unload(.moduleNameNoUnderscore(basename2(m))) # so, unload here before reloading without exporting } RBuildIgnoreFile <- filenameFromFunction(packageFolderName, "", fileExt = ".Rbuildignore") - cat("^.*\\.Rproj$ + + startCat <- if (file.exists(RBuildIgnoreFile)) readLines(RBuildIgnoreFile) else character() + + rbi <- paste("^.*\\.Rproj$ ^\\.Rproj\\.user$ ^_pkgdown\\.yml$ .*\\.tar\\.gz$ @@ -324,6 +267,8 @@ makeActiveBinding('Par', SpaDES.core:::activeParBindingFunction, ", CONTRIBUTING\\.md cran-comments\\.md ^docs$ +citation.* +figures ^LICENSE$ vignettes/.*_cache$ vignettes/.*\\.log$ @@ -335,9 +280,13 @@ vignettes/.*\\.log$ ^data/* ^.git ^.gitignore -^.gitmodules - ", sep = "\n", - file = RBuildIgnoreFile, fill = TRUE) +^.gitmodules", sep = "\n") + rbi <- strsplit(rbi, split = "\n")[[1]] + + modFiles <- c(paste0(module, ".*"), ".*zip") + + rbi <- unique(c(startCat, rbi, modFiles)) + cat(rbi, file = RBuildIgnoreFile, fill = TRUE, sep = "\n") return(invisible()) } @@ -372,6 +321,9 @@ DESCRIPTIONfileFromModule <- function(module, md, deps, hasNamespaceFile, NAMESP d$Imports <- Require::extractPkgName(deps) versionNumb <- Require::extractVersionNumber(deps) + needRemotes <- which(!is.na(Require::extractPkgGitHub(deps))) + d$Remotes <- Require::trimVersionNumber(deps[needRemotes]) + hasVersionNumb <- !is.na(versionNumb) inequality <- paste0("(", gsub("(.+)\\((.+)\\)", "\\2", deps[hasVersionNumb]), ")") missingSpace <- !grepl("[[:space:]]", inequality) @@ -396,6 +348,7 @@ DESCRIPTIONfileFromModule <- function(module, md, deps, hasNamespaceFile, NAMESP d$Imports[hasVersionNumb] <- paste(d$Imports[hasVersionNumb], inequality) dFile <- filenameFromFunction(packageFolderName, "DESCRIPTION", fileExt = "") + origDESCtxt <- read.dcf(dFile) cat(paste("Package:", d$Package), file = dFile, sep = "\n") cat(paste("Type:", d$Type), file = dFile, sep = "\n", append = TRUE) @@ -405,17 +358,63 @@ DESCRIPTIONfileFromModule <- function(module, md, deps, hasNamespaceFile, NAMESP cat(paste("Date:", d$Date), file = dFile, sep = "\n", append = TRUE) cat(c("Authors@R: ", format(d$Authors)), file = dFile, sep = "\n", append = TRUE) - if (length(d$Imports)) - cat(c("Imports:", paste(" ", d$Imports, collapse = ",\n")), sep = "\n", file = dFile, append = TRUE) + mergeField(origDESCtxt = origDESCtxt, field = d$Imports, fieldName = "Imports", dFile) + + suggs <- c('knitr', 'rmarkdown', 'testthat', 'withr', 'roxygen2') + mergeField(origDESCtxt = origDESCtxt, field = suggs, fieldName = "Suggests", dFile) - Suggests <- c('knitr', 'rmarkdown') - cat(c("Suggests:", paste(" ", Suggests, collapse = ",\n")), sep = "\n", file = dFile, append = TRUE) + mergeField(origDESCtxt = origDESCtxt, field = d$Remotes, fieldName = "Remotes", dFile) cat("Encoding: UTF-8", sep = "\n", file = dFile, append = TRUE) cat("License: GPL-3", sep = "\n", file = dFile, append = TRUE) cat("VignetteBuilder: knitr, rmarkdown", sep = "\n", file = dFile, append = TRUE) cat("ByteCompile: yes", sep = "\n", file = dFile, append = TRUE) cat("Roxygen: list(markdown = TRUE)", sep = "\n", file = dFile, append = TRUE) + cat(paste0("RoxygenNote: ", as.character(packageVersion("roxygen2"))), sep = "\n", file = dFile, append = TRUE) + + message("New/updated DESCRIPTION file is: ", dFile) return(dFile) } + +mergeField <- function(origDESCtxt, field, dFile, fieldName = "Imports") { + fieldVals <- character() + if (fieldName %in% colnames(origDESCtxt)) + fieldVals <- strsplit(origDESCtxt[, fieldName], split = ",+\n")[[1]] + if (length(field)) { + field <- Require:::trimRedundancies(unique(c(field, fieldVals))) + } + cat(c(paste0(fieldName, ":"), paste(" ", sort(field$packageFullName), collapse = ",\n")), + sep = "\n", file = dFile, append = TRUE) +} + + + +documentModule <- function(packageFolderName, gpd, linesWithDefModule) { + message("Building documentation") + m <- packageFolderName + tmpSrcForDoc <- file.path(m, "R/READONLYFromMainModuleFile.R") + mainModuleFile <- file.path(m, paste0(basename(m), ".R")) + # if (missing(rlaa)) { + rlaa <- readLines(mainModuleFile) + # } + + if (missing(linesWithDefModule)) { + if (missing(gpd)) { + aa <- parse(mainModuleFile, keep.source = TRUE) + gpd <- getParseData(aa) + } + linesWithDefModule <- gpd[grep("defineModule", gpd$text) - 1, ][, c("line1", "line2")] + } + if (!dir.exists(file.path(m, "R"))) + dir.create(file.path(m, "R")) + cat(paste0("#% Generated by SpaDES.core: do not edit by hand +#% Please edit documentation in\n#%", mainModuleFile), + file = tmpSrcForDoc) + cat(rlaa[-(linesWithDefModule[[1]]:linesWithDefModule[[2]])], sep = "\n", + file = tmpSrcForDoc, append = TRUE) + # on.exit(unlink(tmpSrcForDoc)) + roxygen2::roxygenise(m, roclets = NULL) # This builds documentation, but also exports all functions ... + pkgload::dev_topic_index_reset(m) + pkgload::unload(.moduleNameNoUnderscore(basename2(m))) # so, unload here before reloading without exporting +} diff --git a/R/createDESCRIPTIONandDocs.R b/R/createDESCRIPTIONandDocs.R new file mode 100644 index 00000000..0b2596a9 --- /dev/null +++ b/R/createDESCRIPTIONandDocs.R @@ -0,0 +1,313 @@ +#' Convert standard module code into an R package +#' +#' *EXPERIMENTAL -- USE WITH CAUTION*. This function will create a `DESCRIPTION` +#' file if one does not exist, based on the module metadata. If one exists, it will +#' update it with any additional information: **it will not remove packages that are +#' removed from the metadata**. It will create, if one does not +#' exist, or update a `.Rbuildignore` file. If `importAll = TRUE` It will create a file named `R/imports.R`, +#' which will import all functions. This function will make no changes to +#' any existing source file of a SpaDES.module. If `buildDocumentation = TRUE`, +#' it will build documentation `.Rd` files from `roxygen2` tags. +#' +#' This function does not install anything (e.g., `devtools::install`). After +#' running this function, `simInit` will automatically detect that this is now +#' a package and will load the functions (via `pkgload::load_all`) from the source files. +#' This will have the effect that it emulates the "non-package" behaviour of a +#' SpaDES module exactly. After running this function, current tests show no +#' impact on module behaviour, other than event-level and module-level Caching will show +#' changes and will be rerun. Function-level Caching appears unaffected. +#' In other words, this should cause no changes to running the module code via +#' `simInit` and `spades`. +#' +#' This function will create +#' and fill a minimal `DESCRIPTION` file. This will leave the `defineModule` +#' function call as the only code in the main module file. This `defineModule` +#' and a `doEvent.xxx` are the only 2 elements that are required for an R +#' package to be considered a SpaDES module. With these changes, the module should +#' still function normally, but will be able to act like an +#' R package, e.g., for writing function documentation with `roxygen2`, +#' using the `testthat` infrastructure, etc. +#' +#' This function is intended to be run once for a module that was created using +#' the "standard" SpaDES module structure (e.g., from a `newModule` call). There +#' is currently no way to "revert" the changes from R (though it can be done using +#' version control utilities if all files are under version control, e.g., GitHub). +#' Currently `SpaDES.core` identifies a module as being a package if it has +#' a `DESCRIPTION` file, or if it has been installed to the `.libPaths()` +#' e.g., via `devtools::install` or the like. So one can simply remove the +#' package from `.libPaths` and delete the `DESCRIPTION` file and +#' `SpaDES.core` will treat it as a normal module. +#' +#' @section Reverting: +#' Currently, this is not a reversible process. We recommend trying one module at +#' a time, running your code. If all seems to work, then great. Commit the changes. +#' If things don't seem to work, then revert the changes and continue on as before. +#' Ideally, file a bug report on the `SpaDES.core` GitHub.com pages. +#' +#' Currently +#' @return Invoked for its side effects. There will be a new or modified +#' `DESCRIPTION` file in the root directory of the module. Any functions that +#' were in the main module script (i.e., the .R file whose filename is the name of +#' the module and is in the root directory of the module) will be moved to individual +#' `.R` files in the `R` folder. Any function with a dot prefix will have the +#' dot removed in its respective filename, but the function name is unaffected. +#' +#' Currently, `SpaDES.core` does not install the package under any circumstances. +#' It will load it via `pkgdown::load_all`, and optionally (`option("spades.moduleDocument" = TRUE)`) +#' build documentation via `roxygen2::roxygenise` within the `simInit` call. +#' This means that any modifications to source code +#' will be read in during the `simInit` call, as is the practice when a module +#' is not a package. +#' +#' @section Exported functions: +#' +#' The only function that will be exported by default is the `doEvent.xxx`, +#' where `xxx` is the module name. If any other module is to be exported, it must +#' be explicitly exported with e.g., `@export`, and then building the `NAMESPACE` +#' file, e.g., via `devtools::document(moduleRootPath)`. NOTE: as long as all +#' the functions are being used inside each other, and they all can be traced back +#' to a call in `doEvent.xxx`, then there is no need to export anything else. +#' +#' @section DESCRIPTION: +#' +#' The `DESCRIPTION` file that is created (destroying any existing `DESCRIPTION` +#' file) with this function will have +#' several elements that a user may wish to change. Notably, all packages that were +#' in `reqdPkgs` in the SpaDES module metadata will be in the `Imports` +#' section of the `DESCRIPTION`. To accommodate the need to see these functions, +#' a new R script, `imports.R` will be created with `@import` for each +#' package in `reqdPkgs` of the module metadata. However, if a module already has used +#' `@importFrom` for importing a function from a package, then the generic +#' `@import` will be omitted for that (those) package(s). +#' So, a user should likely follow standard R package +#' best practices and use `@importFrom` to identify the specific functions that +#' are required within external packages, thereby limiting function name collisions +#' (and the warnings that come with them). +#' +#' Other elements of a standard `DESCRIPTION` file that will be missing or possibly +#' inappropriately short are `Title`, `Description`, `URL`, +#' `BugReports`. +#' +#' @section Installing as a package: +#' +#' There is no need to "install" the source code as a package because `simInit` +#' will load it on the fly. But, there may be reasons to install it, e.g., to have +#' access to individual functions, help manual, running tests etc. To do this, +#' simply use the `devtools::install(pathToModuleRoot)`. Even if it is installed, +#' `simInit` will nevertheless run `pkgload::load_all` to ensure the +#' `spades` call will be using the current source code. +#' +#' @param module Character string of module name, without path +#' +#' @param path Character string of `modulePath`. Defaults to `getOption("spades.modulePath")`. +#' +#' @param buildDocuments A logical. If `TRUE`, the default, then the documentation +#' will be built, if any exists, using `roxygen2::roxygenise`. +#' @param importAll A logical. If `TRUE`, then every package named in `reqdPkgs` will +#' have an `@importFrom `, meaning **every** function from every package will +#' be imported. If `FALSE`, then only functions explicitly imported using +#' `@importFrom ` will be imported. +#' +#' @return invoked for the side effect of creating DESCRIPTION file, a `.Rbuildingore` +#' file and possibly building documentatation from roxygen tags. +#' +#' @export +#' @examples +#' if (requireNamespace("ggplot2") && requireNamespace("pkgload") ) { +#' tmpdir <- tempdir2() +#' newModule("test", tmpdir, open = FALSE) +#' createDESCRIPTIONandDocs("test", path = tmpdir) +#' } +createDESCRIPTIONandDocs <- function(module = NULL, path = getOption("spades.modulePath"), + importAll = TRUE, + buildDocuments = TRUE) { + stopifnot( + requireNamespace("pkgload", quietly = TRUE), + requireNamespace("roxygen2", quietly = TRUE) + ) + + mainModuleFile <- file.path(path, unlist(module), paste0(unlist(module), ".R")) + packageFolderName <- dirname(mainModuleFile) + aa <- parse(mainModuleFile, keep.source = TRUE) + rlaa <- readLines(mainModuleFile) + gpd <- getParseData(aa) + + defModule <- grepl("^defineModule", aa) + whDefModule <- which(defModule) + whNotDefModule <- which(!defModule) + + linesWithDefModule <- gpd[grep("defineModule", gpd$text) - 1, ][, c("line1", "line2")] + + doEvent <- grepl(paste0("^doEvent.", module), aa) + whDoEvent <- which(doEvent) + whNoDoEvent <- which(!doEvent & !defModule) + + # file.copy(mainModuleFile, file.path(path, unlist(module), "R", paste0(unlist(module), ".R"))) + + NAMESPACEFile <- filenameFromFunction(packageFolderName, "NAMESPACE", fileExt = "") + hasNamespaceFile <- file.exists(NAMESPACEFile) + + RsubFolder <- file.path(packageFolderName, "R") + checkPath(RsubFolder, create = TRUE) + + parseWithRoxygen <- gpd[grep("#'", gpd$text), ] + linesWithRoxygen <- parseWithRoxygen[, "line1"] + nextElement <- c(whNotDefModule[-1], Inf) + + + filePathImportSpadesCore <- filenameFromFunction(packageFolderName, "imports", "R")# file.path(dirname(mainModuleFile), "R", "imports.R") + + md <- aa[[whDefModule]][[3]] + deps <- unlist(eval(md$reqdPkgs)) + + dFile <- DESCRIPTIONfileFromModule(module, md, deps, hasNamespaceFile, NAMESPACEFile, filePathImportSpadesCore, + packageFolderName) + + if (isTRUE(buildDocuments)) { + message("Building documentation") + m <- packageFolderName + tmpSrcForDoc <- "R/tmp.R" + cat(rlaa[-(linesWithDefModule[[1]]:linesWithDefModule[[2]])], sep = "\n", file = tmpSrcForDoc) + on.exit(unlink(tmpSrcForDoc)) + roxygen2::roxygenise(m, roclets = NULL) # This builds documentation, but also exports all functions ... + pkgload::dev_topic_index_reset(m) + pkgload::unload(.moduleNameNoUnderscore(basename2(m))) # so, unload here before reloading without exporting + } + + RBuildIgnoreFile <- filenameFromFunction(packageFolderName, "", fileExt = ".Rbuildignore") + + startCat <- readLines(RBuildIgnoreFile) + + rbi <- paste("^.*\\.Rproj$ +^\\.Rproj\\.user$ +^_pkgdown\\.yml$ +.*\\.tar\\.gz$ +.*\\.toc$ +.*\\.zip$ +^\\.lintr$ +CONTRIBUTING\\.md +cran-comments\\.md +^docs$ +citation.* +figures +^LICENSE$ +vignettes/.*_cache$ +vignettes/.*\\.log$ +^\\.httr-oauth$ +^revdep$ +^\\.github$ +^codecov\\.yml$ +^CRAN-RELEASE$ +^data/* +^.git +^.gitignore +^.gitmodules", sep = "\n") + rbi <- strsplit(rbi, split = "\n")[[1]] + + modFiles <- c(paste0(module, ".*"), ".*zip") + + rbi <- unique(c(startCat, rbi, modFiles)) + cat(rbi, file = RBuildIgnoreFile, fill = TRUE, sep = "\n") + + return(invisible()) +} + +filenameFromFunction <- function(packageFolderName, fn = "", subFolder = "", fileExt = ".R") { + normPath(file.path(packageFolderName, subFolder, paste0(gsub("\\.", "", fn), fileExt))) +} + +filenameForMainFunctions <- function(module, modulePath = ".") + normPath(file.path(modulePath, unlist(module), "R", paste0(unlist(basename(module)), "Fns.R"))) + + + + +DESCRIPTIONfileFromModule <- function(module, md, deps, hasNamespaceFile, NAMESPACEFile, filePathImportSpadesCore, + packageFolderName) { + d <- list() + d$Package <- .moduleNameNoUnderscore(module) + d$Type <- "Package" + + d$Title <- md$name + d$Description <- md$description + d$Version <- as.character(eval(md$version[[2]])) + d$Date <- Sys.Date() + d$Authors <- md$authors + d$Authors <- c(paste0(" ", format(d$Authors)[1]), format(d$Authors)[-1]) + + + hasSC <- grepl("SpaDES.core", deps) + if (all(!hasSC)) + deps <- c("SpaDES.core", deps) + + d$Imports <- Require::extractPkgName(deps) + versionNumb <- Require::extractVersionNumber(deps) + needRemotes <- which(!is.na(Require::extractPkgGitHub(deps))) + d$Remotes <- Require::trimVersionNumber(deps[needRemotes]) + + hasVersionNumb <- !is.na(versionNumb) + inequality <- paste0("(", gsub("(.+)\\((.+)\\)", "\\2", deps[hasVersionNumb]), ")") + missingSpace <- !grepl("[[:space:]]", inequality) + if (any(missingSpace)) + inequality[missingSpace] <- gsub("([=><]+)", "\\1 ", inequality[missingSpace]) + + namespaceImports <- d$Imports + # Create "import all" for each of the packages, unless it is already in an @importFrom + if (hasNamespaceFile) { + nsTxt <- readLines(NAMESPACEFile) + hasImportFrom <- grepl("importFrom", nsTxt) + if (any(hasImportFrom)) { + pkgsNotNeeded <- unique(gsub(".+\\((.+)\\,.+\\)", "\\1", nsTxt[hasImportFrom])) + namespaceImports <- grep(paste(pkgsNotNeeded, collapse = "|"), + namespaceImports, invert = TRUE, value = TRUE) + } + } + + cat(paste0("#' @import ", namespaceImports, "\nNULL\n"), sep = "\n", + file = filePathImportSpadesCore, fill = TRUE) + + d$Imports[hasVersionNumb] <- paste(d$Imports[hasVersionNumb], inequality) + + dFile <- filenameFromFunction(packageFolderName, "DESCRIPTION", fileExt = "") + origDESCtxt <- if (file.exists(dFile)) read.dcf(dFile) else character() + + cat(paste("Package:", d$Package), file = dFile, sep = "\n") + cat(paste("Type:", d$Type), file = dFile, sep = "\n", append = TRUE) + cat(paste("Title:", d$Title), file = dFile, sep = "\n", append = TRUE) + cat(paste("Version:", d$Version), file = dFile, sep = "\n", append = TRUE) + cat(paste("Description:", paste(d$Description, collapse = " ")), file = dFile, sep = "\n", append = TRUE) + cat(paste("Date:", d$Date), file = dFile, sep = "\n", append = TRUE) + cat(c("Authors@R: ", format(d$Authors)), file = dFile, sep = "\n", append = TRUE) + + if (length(d$Imports) || length(origDESCtxt)) + mergeField(origDESCtxt = origDESCtxt, field = d$Imports, fieldName = "Imports", dFile) + + suggs <- c('knitr', 'rmarkdown', 'testthat', 'withr', 'roxygen2') + if (length(suggs) || length(origDESCtxt)) + mergeField(origDESCtxt = origDESCtxt, field = suggs, fieldName = "Suggests", dFile) + + if (length(d$Remotes) || length(origDESCtxt)) + mergeField(origDESCtxt = origDESCtxt, field = d$Remotes, fieldName = "Remotes", dFile) + + cat("Encoding: UTF-8", sep = "\n", file = dFile, append = TRUE) + cat("License: GPL-3", sep = "\n", file = dFile, append = TRUE) + cat("VignetteBuilder: knitr, rmarkdown", sep = "\n", file = dFile, append = TRUE) + cat("ByteCompile: yes", sep = "\n", file = dFile, append = TRUE) + cat("Roxygen: list(markdown = TRUE)", sep = "\n", file = dFile, append = TRUE) + cat(paste0("RoxygenNote: ", as.character(packageVersion("roxygen2"))), sep = "\n", file = dFile, append = TRUE) + + + message("New/updated DESCRIPTION file is: ", dFile) + return(dFile) +} + +mergeField <- function(origDESCtxt, field, dFile, fieldName = "Imports") { + fieldVals <- character() + if (fieldName %in% colnames(origDESCtxt)) + fieldVals <- strsplit(origDESCtxt[, fieldName], split = ",+\n")[[1]] + if (length(field)) { + field <- Require:::trimRedundancies(unique(c(field, fieldVals))) + } + cat(c(paste0(fieldName, ":"), paste(" ", sort(field$packageFullName), collapse = ",\n")), + sep = "\n", file = dFile, append = TRUE) +} diff --git a/R/simulation-parseModule.R b/R/simulation-parseModule.R index f645eae8..d65d3c93 100644 --- a/R/simulation-parseModule.R +++ b/R/simulation-parseModule.R @@ -617,6 +617,7 @@ evalWithActiveCode <- function(parsedModuleNoDefineModule, envir, parentFrame = } .isPackage <- function(fullModulePath, sim) { + return(FALSE) modEnv <- sim@.xData$.mods[[basename2(fullModulePath)]] # There are 3 ways to check ... existence of .isPackage is fastest, but may be wrong # if the namespace exists ... 2nd fastest, but also may be wrong if FALSE diff --git a/R/simulation-spades.R b/R/simulation-spades.R index 04d2050c..5d078ad6 100644 --- a/R/simulation-spades.R +++ b/R/simulation-spades.R @@ -1110,7 +1110,8 @@ setMethod( prevStart <- get(as.character(existingCompleted[1]), envir = sim@completed) # prevEnd <- get(as.character(existingCompleted[length(existingCompleted)]), envir = sim@completed) if (length(.grepSysCalls(sys.calls(), "restartSpades")) == 0 && - length(sim@.xData$._ranInitDuringSimInit) == 0) { # don't crop off completed events if Init(s) ran during simInit + length(sim@.xData$._ranInitDuringSimInit) == 0 && + prevStart$eventType != ".inputObjects") { # don't crop off completed events if Init(s) ran during simInit prevEvUnit <- attr(prevStart[["eventTime"]], "unit") stTime <- start(sim, unit = prevEvUnit) if (stTime <= prevStart[["eventTime"]] && (time(sim, unit = prevEvUnit) == stTime)) diff --git a/man/createDESCRIPTIONandDocs.Rd b/man/createDESCRIPTIONandDocs.Rd new file mode 100644 index 00000000..4cf130c5 --- /dev/null +++ b/man/createDESCRIPTIONandDocs.Rd @@ -0,0 +1,145 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/createDESCRIPTIONandDocs.R +\name{createDESCRIPTIONandDocs} +\alias{createDESCRIPTIONandDocs} +\title{Convert standard module code into an R package} +\usage{ +createDESCRIPTIONandDocs( + module = NULL, + path = getOption("spades.modulePath"), + importAll = TRUE, + buildDocuments = TRUE +) +} +\arguments{ +\item{module}{Character string of module name, without path} + +\item{path}{Character string of \code{modulePath}. Defaults to \code{getOption("spades.modulePath")}.} + +\item{importAll}{A logical. If \code{TRUE}, then every package named in \code{reqdPkgs} will +have an \verb{@importFrom }, meaning \strong{every} function from every package will +be imported. If \code{FALSE}, then only functions explicitly imported using +\verb{@importFrom } will be imported.} + +\item{buildDocuments}{A logical. If \code{TRUE}, the default, then the documentation +will be built, if any exists, using \code{roxygen2::roxygenise}.} +} +\value{ +Invoked for its side effects. There will be a new or modified +\code{DESCRIPTION} file in the root directory of the module. Any functions that +were in the main module script (i.e., the .R file whose filename is the name of +the module and is in the root directory of the module) will be moved to individual +\code{.R} files in the \code{R} folder. Any function with a dot prefix will have the +dot removed in its respective filename, but the function name is unaffected. + +Currently, \code{SpaDES.core} does not install the package under any circumstances. +It will load it via \code{pkgdown::load_all}, and optionally (\code{option("spades.moduleDocument" = TRUE)}) +build documentation via \code{roxygen2::roxygenise} within the \code{simInit} call. +This means that any modifications to source code +will be read in during the \code{simInit} call, as is the practice when a module +is not a package. + +invoked for the side effect of creating DESCRIPTION file, a \code{.Rbuildingore} +file and possibly building documentatation from roxygen tags. +} +\description{ +\emph{EXPERIMENTAL -- USE WITH CAUTION}. This function will create a \code{DESCRIPTION} +file if one does not exist, based on the module metadata. If one exists, it will +update it with any additional information: \strong{it will not remove packages that are +removed from the metadata}. It will create, if one does not +exist, or update a \code{.Rbuildignore} file. If \code{importAll = TRUE} It will create a file named \code{R/imports.R}, +which will import all functions. This function will make no changes to +any existing source file of a SpaDES.module. If \code{buildDocumentation = TRUE}, +it will build documentation \code{.Rd} files from \code{roxygen2} tags. +} +\details{ +This function does not install anything (e.g., \code{devtools::install}). After +running this function, \code{simInit} will automatically detect that this is now +a package and will load the functions (via \code{pkgload::load_all}) from the source files. +This will have the effect that it emulates the "non-package" behaviour of a +SpaDES module exactly. After running this function, current tests show no +impact on module behaviour, other than event-level and module-level Caching will show +changes and will be rerun. Function-level Caching appears unaffected. +In other words, this should cause no changes to running the module code via +\code{simInit} and \code{spades}. + +This function will create +and fill a minimal \code{DESCRIPTION} file. This will leave the \code{defineModule} +function call as the only code in the main module file. This \code{defineModule} +and a \code{doEvent.xxx} are the only 2 elements that are required for an R +package to be considered a SpaDES module. With these changes, the module should +still function normally, but will be able to act like an +R package, e.g., for writing function documentation with \code{roxygen2}, +using the \code{testthat} infrastructure, etc. + +This function is intended to be run once for a module that was created using +the "standard" SpaDES module structure (e.g., from a \code{newModule} call). There +is currently no way to "revert" the changes from R (though it can be done using +version control utilities if all files are under version control, e.g., GitHub). +Currently \code{SpaDES.core} identifies a module as being a package if it has +a \code{DESCRIPTION} file, or if it has been installed to the \code{.libPaths()} +e.g., via \code{devtools::install} or the like. So one can simply remove the +package from \code{.libPaths} and delete the \code{DESCRIPTION} file and +\code{SpaDES.core} will treat it as a normal module. +} +\section{Reverting}{ + +Currently, this is not a reversible process. We recommend trying one module at +a time, running your code. If all seems to work, then great. Commit the changes. +If things don't seem to work, then revert the changes and continue on as before. +Ideally, file a bug report on the \code{SpaDES.core} GitHub.com pages. + +Currently +} + +\section{Exported functions}{ + + +The only function that will be exported by default is the \code{doEvent.xxx}, +where \code{xxx} is the module name. If any other module is to be exported, it must +be explicitly exported with e.g., \verb{@export}, and then building the \code{NAMESPACE} +file, e.g., via \code{devtools::document(moduleRootPath)}. NOTE: as long as all +the functions are being used inside each other, and they all can be traced back +to a call in \code{doEvent.xxx}, then there is no need to export anything else. +} + +\section{DESCRIPTION}{ + + +The \code{DESCRIPTION} file that is created (destroying any existing \code{DESCRIPTION} +file) with this function will have +several elements that a user may wish to change. Notably, all packages that were +in \code{reqdPkgs} in the SpaDES module metadata will be in the \code{Imports} +section of the \code{DESCRIPTION}. To accommodate the need to see these functions, +a new R script, \code{imports.R} will be created with \verb{@import} for each +package in \code{reqdPkgs} of the module metadata. However, if a module already has used +\verb{@importFrom} for importing a function from a package, then the generic +\verb{@import} will be omitted for that (those) package(s). +So, a user should likely follow standard R package +best practices and use \verb{@importFrom} to identify the specific functions that +are required within external packages, thereby limiting function name collisions +(and the warnings that come with them). + +Other elements of a standard \code{DESCRIPTION} file that will be missing or possibly +inappropriately short are \code{Title}, \code{Description}, \code{URL}, +\code{BugReports}. +} + +\section{Installing as a package}{ + + +There is no need to "install" the source code as a package because \code{simInit} +will load it on the fly. But, there may be reasons to install it, e.g., to have +access to individual functions, help manual, running tests etc. To do this, +simply use the \code{devtools::install(pathToModuleRoot)}. Even if it is installed, +\code{simInit} will nevertheless run \code{pkgload::load_all} to ensure the +\code{spades} call will be using the current source code. +} + +\examples{ +if (requireNamespace("ggplot2") && requireNamespace("pkgload") ) { + tmpdir <- tempdir2() + newModule("test", tmpdir, open = FALSE) + createDESCRIPTIONandDocs("test", path = tmpdir) +} +} diff --git a/tests/testthat/helper-initTests.R b/tests/testthat/helper-initTests.R index 62b3d60e..dc5f187e 100644 --- a/tests/testthat/helper-initTests.R +++ b/tests/testthat/helper-initTests.R @@ -107,9 +107,9 @@ testCode <- ' mod$a <- 2 # should have mod$x here sim$testPar1 <- Par$testParA - if (tryCatch(exists("Init", envir = asNamespace("test"), inherits = FALSE), error = function(x) FALSE)) { + # if (tryCatch(exists("Init", envir = asNamespace("test"), inherits = FALSE), error = function(x) FALSE)) { sim <- Init(sim) - } + # } sim <- scheduleEvent(sim, sim@simtimes[["current"]] + 1, "test", "event1", .skipChecks = TRUE) }, @@ -161,9 +161,9 @@ test2Code <- ' switch( eventType, init = { - if (tryCatch(exists("Init", envir = asNamespace("test2"), inherits = FALSE), error = function(x) FALSE)) { + # if (tryCatch(exists("Init", envir = asNamespace("test2"), inherits = FALSE), error = function(x) FALSE)) { sim <- Init(sim) - } + # } if (isTRUE(P(sim)$testParB >= 1100)) { P(sim, "testParB") <- P(sim)$testParB + 756 diff --git a/tests/testthat/test-mod.R b/tests/testthat/test-mod.R index 61652bfc..1f21f1dc 100644 --- a/tests/testthat/test-mod.R +++ b/tests/testthat/test-mod.R @@ -164,13 +164,13 @@ test_that("convertToPackage testing", { testName2 <- paste0("test2.", .rndstr(len = 1)) mainModFile1 <- paste0(testName1, ".R") mainModFile2 <- paste0(testName2, ".R") - try(pkgload::unload(testName1), silent = TRUE) - try(pkgload::unload(testName2), silent = TRUE) + # try(pkgload::unload(testName1), silent = TRUE) + # try(pkgload::unload(testName2), silent = TRUE) - on.exit({ - try(pkgload::unload(testName1), silent = TRUE) - try(pkgload::unload(testName2), silent = TRUE) - }, add = TRUE) + # on.exit({ + # try(pkgload::unload(testName1), silent = TRUE) + # try(pkgload::unload(testName2), silent = TRUE) + # }, add = TRUE) newModule(testName1, tmpdir, open = FALSE) newModule(testName2, tmpdir, open = FALSE) @@ -191,6 +191,7 @@ test_that("convertToPackage testing", { #\' @rdname Init #\' @name Init #\' @param sim A simList + #\' @export Init <- function(sim) { sim$aaaa <- Run1(1) return(sim) @@ -230,7 +231,7 @@ test_that("convertToPackage testing", { expect_true(!file.exists(file.path(tmpdir, tt, "NAMESPACE"))) expect_true(dir.exists(file.path(tmpdir, tt, "R"))) ## list.files(file.path(tmpdir, tt, "R")) - expect_true(file.exists(filenameForMainFunctions(tt, tmpdir))) + # expect_true(file.exists(filenameForMainFunctions(tt, tmpdir))) } mySim9 <- simInit(times = list(start = 0, end = 1), @@ -239,7 +240,7 @@ test_that("convertToPackage testing", { # doesn't document, unless it is first time for (tt in c(testName1, testName2)) { expect_true(file.exists(file.path(tmpdir, tt, "DESCRIPTION"))) - expect_true(file.exists(file.path(tmpdir, tt, "NAMESPACE"))) + # expect_true(file.exists(file.path(tmpdir, tt, "NAMESPACE"))) expect_true(dir.exists(file.path(tmpdir, tt, "R"))) } working <- spades(mySim9, debug = FALSE) @@ -253,19 +254,38 @@ test_that("convertToPackage testing", { # Will run document() so will have the NAMESPACE and for (tt in c(testName1, testName2)) { expect_true(file.exists(file.path(tmpdir, tt, "DESCRIPTION"))) - expect_true(file.exists(file.path(tmpdir, tt, "NAMESPACE"))) - expect_true(sum(grepl("export.+doEvent", readLines(file.path(tmpdir, tt, "NAMESPACE")))) == 1) + # expect_true(file.exists(file.path(tmpdir, tt, "NAMESPACE"))) + # expect_true(sum(grepl("export.+doEvent", readLines(file.path(tmpdir, tt, "NAMESPACE")))) == 1) } # check that inheritance is correct -- Run is in the namespace, Init also... doEvent calls Init calls Run expect_true(is(working, "simList")) expect_true(working$aaaa == 2) expect_true(is(working$cccc, "try-error")) - bbb <- get("Run2", asNamespace(testName2))(2) - fnTxt <- readLines(filenameForMainFunctions(tt, tmpdir)) + # bbb <- get("Run2", asNamespace(testName2))(2) + bbb <- get("Run2", working$.mods[[testName2]])(2) + + packageFoldername <- file.path(tmpdir, testName2) + fnTxt <- readLines(file.path(packageFoldername, paste0(testName2, ".R"))) expect_true(sum(grepl("Need to keep comments", fnTxt)) == 1) expect_true(bbb == 4) - pkgload::unload(testName1) - pkgload::unload(testName2) + + # check documentation + packageFoldername <- file.path(tmpdir, testName1) + expect_false(dir.exists(file.path(packageFoldername, "man"))) + documentModule(packageFoldername) + expect_true(dir.exists(file.path(packageFoldername, "man"))) + pkgload::load_all(packageFoldername) + on.exit({ + try(pkgload::unload(.moduleNameNoUnderscore(basename(packageFoldername)))) + }) + fn <- get("Init", envir = asNamespace(.moduleNameNoUnderscore(basename(packageFoldername)))) + expect_is(fn, "function") + pkgload::unload(.moduleNameNoUnderscore(basename(packageFoldername))) + fn <- try(get("Init", envir = asNamespace(.moduleNameNoUnderscore(basename(packageFoldername)))), silent = TRUE) + expect_true(is(fn, "try-error")) + + # pkgload::unload(testName1) + # pkgload::unload(testName2) #} }) diff --git a/tests/testthat/test-module-template.R b/tests/testthat/test-module-template.R index ea033428..02d9ef5b 100644 --- a/tests/testthat/test-module-template.R +++ b/tests/testthat/test-module-template.R @@ -130,7 +130,8 @@ test_that("newModule with events and functions", { expect_true(out$b == 3) yrsSimulated <- (end(out) - start(out)) expect_true(sum(grepl("hi", mess)) == yrsSimulated) - expect_true(NROW(completed(out)) == yrsSimulated + 6) + expect_true(NROW(completed(out)) == yrsSimulated + + (NROW(.coreModules()) - 1) + length(c(".inputObjects", "next1", "init"))) expect_true(NROW(events(out)) == 1) expect_true(NROW(completed(out)[eventType == "next1"]) == 1) expect_true(NROW(completed(out)[eventType == "plot"]) == yrsSimulated)