diff --git a/NAMESPACE b/NAMESPACE index c6af051..454e890 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -19,6 +19,7 @@ export(get_from_pipenv) export(get_github_creds) export(get_latest_pip_release) export(get_latest_ppp_versions) +export(get_pip_aliases) export(get_pip_folders) export(get_pip_releases) export(get_pipenv) diff --git a/R/log.R b/R/log.R index 081214f..904ccd2 100644 --- a/R/log.R +++ b/R/log.R @@ -182,38 +182,122 @@ log_init <- function(name = getOption("pipfun.log.default"), } -#' Save a log to disk + +#' Reset or delete a log from memory #' -#' Saves a log stored in `.piplogenv` to disk using {stamp}, with metadata -#' and versioning support. +#' Clears a log from the internal environment. Use this to start over or free +#' memory. #' -#' @param name Name of the log in memory (default: +#' @param name Name of the log to remove (default: #' `getOption("pipfun.log.default")`). -#' @param dir Directory where the log should be saved. -#' @param id File identifier (without extension). Defaults to `name`. +#' +#' @return Invisibly returns TRUE if the log was removed. +#' @export +log_reset <- function(name = getOption("pipfun.log.default", "default")) { + if (!rlang::env_has(.piplogenv, name)) { + cli::cli_alert_info("Log {.field {name}} is not present.") + return(invisible(FALSE)) + } + + rlang::env_unbind(.piplogenv, name) + cli::cli_alert_success("Log {.field {name}} has been reset.") + invisible(TRUE) +} + + +#' Filter log entries +#' +#' @param name Name of the log (default: `pipfun.log.default`) +#' @param event Type of event to filter ("info", "warning", "error", etc.) +#' @param fun Optional: function name(s) to filter +#' @param after Optional: filter entries after this datetime +#' @param before Optional: filter entries before this datetime +#' +#' @return A filtered `piplog` object. +#' @export +log_filter <- function(name = getOption("pipfun.log.default"), + event = NULL, + fun = NULL, + after = NULL, + before = NULL) { + + log <- name |> + log_get() |> + copy() + + setDT(log) + + # not elegant but works + e <- event + f <- fun + + if (!is.null(event)) { + log <- log[event %in% e] + } + if (!is.null(fun)) { + log <- log[fun %in% f] + } + if (!is.null(after)) { + log <- log[time >= as.POSIXct(after)] + } + if (!is.null(before)) { + log <- log[time <= as.POSIXct(before)] + } + setattr(log, "class", c("piplog", class(log))) + return(log) +} + + +#' Get a particular log entries +#' +#' @param name Name of the log (default: `pipfun.log.default`) +#' +#' @return A raw `piplog` object. +#' @export +log_get <- function(name = getOption("pipfun.log.default")) { + if (!rlang::env_has(.piplogenv, name)) { + cli::cli_abort("Log {.field {name}} does not exist.") + } + + log <- rlang::env_get(.piplogenv, name) + + if (!inherits(log, "piplog")) { + # Restore class silently if it's just been dropped by DT ops + if (is.data.table(log)) { + setattr(log, "class", unique(c("piplog", class(log)))) + } else { + cli::cli_abort(c(x = "Object {.field {name}} is not a valid piplog.", + i = "{.field {name}}'s class is {class(log)}")) + } + } + invisible(log) +} + +#' Save a log to disk +#' +#' Saves a log stored in `.piplogenv` using {stamp}; `id` is the artifact path +#' (extension will be added if missing). Pass `alias` to select a stamp alias. +#' +#' @param name Name of the log in memory (default: getOption("pipfun.log.default")). +#' @param id File identifier or path (extension optional). Defaults to `name`. #' @param format File format (default: "qs2"). #' @param metadata Optional named list of metadata to attach. #' @param code Optional code object whose hash will be stored. +#' @param alias Optional stamp alias to select which catalog/versions to use. #' @param ... Forwarded to `stamp::st_save()`. #' #' @return Invisibly, the result returned by `stamp::st_save()`. #' @export log_save <- function( name = getOption("pipfun.log.default", "default"), - dir, id = name, format = "qs2", metadata = list(), code = NULL, + alias = NULL, ... ) { - - # ---- Validate directory ---- - if (missing(dir) || !fs::dir_exists(dir)) { - cli::cli_abort("Provided directory path does not exist: {.path {dir}}") - } - - # ---- Validate log ---- + # Validate log exists if (!rlang::env_has(.piplogenv, name)) { cli::cli_abort("Log {.field {name}} does not exist in memory.") } @@ -225,19 +309,19 @@ log_save <- function( setattr(log, "class", unique(c("piplog", class(log)))) } - # Final validation if (!inherits(log, "piplog")) { - cli::cli_abort("File does not contain a valid {.cls piplog} object.") - } + cli::cli_abort("Object is not a valid {.cls piplog}.") + } - # ---- Build stamp path ---- - file <- fs::path(dir, id, ext = format) - sp <- stamp::st_path(file, format = format) + # Ensure extension is present like pipload::pip_write + if (is.null(fs::path_ext(id)) || identical(fs::path_ext(id), "")) { + id <- fs::path_ext_set(path = id, ext = format) + } + file <- id - # ---- Save with stamp ---- out <- stamp::st_save( x = log, - file = sp, + file = file, metadata = c( list( class = "piplog", @@ -248,51 +332,46 @@ log_save <- function( ), code = code, format = format, + alias = alias, ... ) invisible(out) } - #' Load a log from disk #' -#' Loads a previously saved piplog from disk using {stamp}, optionally under a -#' different name. +#' Loads a previously saved piplog using {stamp}. `id` is the artifact path +#' (extension will be added if missing). Pass `alias` to select a stamp alias. #' -#' @param dir Directory where the log is stored. -#' @param id File identifier (without extension). Defaults to `name`. +#' @param id File identifier or path (extension optional). #' @param name Name to assign to the log in memory (default: `id`). #' @param version Optional version identifier passed to `stamp::st_load()`. -#' Use `"available"` to list available versions. #' @param format File format (default: "qs2"). -#' @param overwrite Logical: whether to overwrite an existing log in -#' `.piplogenv`. Default is FALSE. +#' @param overwrite Logical: whether to overwrite an existing log in memory. #' @param verbose Logical: whether to announce loading progress. +#' @param alias Optional stamp alias to select which catalog/versions to use. #' #' @return Invisibly returns the name of the loaded log. #' @export log_load <- function( - dir, id, - name = id, - version = NULL, - format = "qs2", + name = id, + version = NULL, + format = "qs2", overwrite = FALSE, - verbose = TRUE + verbose = TRUE, + alias = NULL ) { - - # ---- Validate directory ---- - if (missing(dir) || !fs::dir_exists(dir)) { - cli::cli_abort("Artifact folder {.path {dir}} does not exist.") + # Ensure extension is present like pipload::pip_read + if (is.null(fs::path_ext(id)) || identical(fs::path_ext(id), "")) { + id <- fs::path_ext_set(path = id, ext = format) } + file <- id - # ---- Build path ---- - file <- fs::path(dir, id, ext = format) - - # ---- List available versions ---- + # List available versions if (identical(version, "available")) { - vr <- stamp::st_versions(file) + vr <- stamp::st_versions(file, alias = alias) if (nrow(vr) == 0) { cli::cli_abort("No versions found in {.path {file}}.") } @@ -300,31 +379,29 @@ log_load <- function( return(vr[]) } - # ---- Load log ---- - ver <- if (is.null(version)) "latest" else version + ver <- if (is.null(version)) "latest" else version if (verbose) { - cli::cli_alert_info( - "Loading {.path {file}} (version = {.strong {ver}})" - ) + if (is.null(alias)) { + cli::cli_alert_info("Loading {.path {file}} (version = {.strong {ver}})") + } else { + cli::cli_alert_info("Loading {.path {file}} (version = {.strong {ver}}, alias = {.val {alias}})") + } } + # Load log, forwarding alias + log <- stamp::st_load(file, version = version, alias = alias) - log <- stamp::st_load(file, version = version) - - # ---- Validate object ---- - # Restore class if dropped by serialization + # Restore class if dropped if (is.data.table(log)) { setattr(log, "class", unique(c("piplog", class(log)))) } - # Final validation if (!inherits(log, "piplog")) { cli::cli_abort("File does not contain a valid {.cls piplog} object.") } - - # ---- Handle overwrite ---- + # Handle overwrite if (rlang::env_has(.piplogenv, name) && !isTRUE(overwrite)) { cli::cli_abort( "A log named {.field {name}} already exists in memory. @@ -335,94 +412,4 @@ log_load <- function( rlang::env_poke(.piplogenv, name, log) invisible(name) -} - -#' Reset or delete a log from memory -#' -#' Clears a log from the internal environment. Use this to start over or free -#' memory. -#' -#' @param name Name of the log to remove (default: -#' `getOption("pipfun.log.default")`). -#' -#' @return Invisibly returns TRUE if the log was removed. -#' @export -log_reset <- function(name = getOption("pipfun.log.default", "default")) { - if (!rlang::env_has(.piplogenv, name)) { - cli::cli_alert_info("Log {.field {name}} is not present.") - return(invisible(FALSE)) - } - - rlang::env_unbind(.piplogenv, name) - cli::cli_alert_success("Log {.field {name}} has been reset.") - invisible(TRUE) -} - - -#' Filter log entries -#' -#' @param name Name of the log (default: `pipfun.log.default`) -#' @param event Type of event to filter ("info", "warning", "error", etc.) -#' @param fun Optional: function name(s) to filter -#' @param after Optional: filter entries after this datetime -#' @param before Optional: filter entries before this datetime -#' -#' @return A filtered `piplog` object. -#' @export -log_filter <- function(name = getOption("pipfun.log.default"), - event = NULL, - fun = NULL, - after = NULL, - before = NULL) { - - log <- name |> - log_get() |> - copy() - - setDT(log) - - # not elegant but works - e <- event - f <- fun - - if (!is.null(event)) { - log <- log[event %in% e] - } - if (!is.null(fun)) { - log <- log[fun %in% f] - } - if (!is.null(after)) { - log <- log[time >= as.POSIXct(after)] - } - if (!is.null(before)) { - log <- log[time <= as.POSIXct(before)] - } - setattr(log, "class", c("piplog", class(log))) - return(log) -} - - -#' Get a particular log entries -#' -#' @param name Name of the log (default: `pipfun.log.default`) -#' -#' @return A raw `piplog` object. -#' @export -log_get <- function(name = getOption("pipfun.log.default")) { - if (!rlang::env_has(.piplogenv, name)) { - cli::cli_abort("Log {.field {name}} does not exist.") - } - - log <- rlang::env_get(.piplogenv, name) - - if (!inherits(log, "piplog")) { - # Restore class silently if it's just been dropped by DT ops - if (is.data.table(log)) { - setattr(log, "class", unique(c("piplog", class(log)))) - } else { - cli::cli_abort(c(x = "Object {.field {name}} is not a valid piplog.", - i = "{.field {name}}'s class is {class(log)}")) - } - } - invisible(log) -} +} \ No newline at end of file diff --git a/R/working_release.R b/R/working_release.R index f347e80..30e28c5 100644 --- a/R/working_release.R +++ b/R/working_release.R @@ -1,30 +1,17 @@ #' Loads PIP release into .pipenv #' -#' This functions sets all the necessary information into the `.pipenv` -#' environment to be used by other PIP packages. It does not create releases. +#' Sets the working PIP release and initializes stamp aliases +#' for all PIP folders associated with that release. #' #' @inheritParams find_release #' @inheritParams get_pip_releases #' @inheritParams download_and_read_file #' @inheritDotParams pip_create_globals -vintage -create_dir #' @param ppp numeric: PPP year to use. -#' @param main_dir character: directory path where all PIP data is stored. By -#' default it is available in `getOption("pipfun.main_dir")`, but it is -#' basically a combination of `Sys.getenv("PIP_ROOT_DIR")` and -#' `getOption("pipfun.working_dir")`. +#' @param main_dir character: directory path where all PIP data is stored. #' -#' @return invisible table with release information and list object in the -#' `.pipenv` environment +#' @return Invisible list with working release information #' @export -#' -#' @examples -#' \dontrun{ -#' # latest PROD release -#' setup_working_release() -#' -#' # error if set up again -#' try(setup_working_release()) -#' } setup_working_release <- function(release = NULL, identity = getOption("pipfun.identities"), force = FALSE, @@ -36,60 +23,101 @@ setup_working_release <- function(release = NULL, ppp = getOption("pipfun.ppps"), creds = NULL, main_dir = getOption("pipfun.main_dir"), + alias_include_release = FALSE, ...) { identity <- match.arg(identity) ppp <- ppp[1] + if (!ppp %in% getOption("pipfun.ppps")) { - cli::cli_abort(c("Wrong PPP value", - i = "PPP values must be {.or {getOption(\"pipfun.ppps\")}}")) + cli::cli_abort(c( + x = "Wrong PPP value", + i = "PPP values must be one of {.or {getOption(\"pipfun.ppps\")}}" + )) } + # ------------------------------------------------------------------ + # Resolve release metadata + # ------------------------------------------------------------------ pr <- if (is.null(release)) { - get_latest_pip_release(identity = identity, - owner = owner, - repo = repo, - file_path = file_path, - branch = branch, - verbose = verbose, - creds = creds) + get_latest_pip_release( + identity = identity, + owner = owner, + repo = repo, + file_path = file_path, + branch = branch, + verbose = verbose, + creds = creds + ) } else { - get_pip_releases(owner = owner, - repo = repo, - file_path = file_path, - branch = branch, - verbose = verbose, - creds = creds) |> + get_pip_releases( + owner = owner, + repo = repo, + file_path = file_path, + branch = branch, + verbose = verbose, + creds = creds + ) |> find_release(release = release, identity = identity) } - # create globals (no dir creation here) - gls <- pip_create_globals(create_dir = FALSE, - vintage = list(release = release, - ppp_year = ppp, - identity = identity), - verbose = verbose, - ...) - - # setup working release info - wr <- list(release = pr[, release], - identity = pr[, identity], - ppp = ppp) - - # get directory paths (no pins) - folder_paths <- set_pip_folders(main_dir = main_dir, - release = pr[, release], - identity = pr[, identity]) - - # save to .pipenv + # ------------------------------------------------------------------ + # Create globals (no directory creation here) + # ------------------------------------------------------------------ + gls <- pip_create_globals( + create_dir = FALSE, + vintage = list( + release = release, + ppp_year = ppp, + identity = identity + ), + verbose = verbose, + ... + ) + + # ------------------------------------------------------------------ + # Working release descriptor + # ------------------------------------------------------------------ + wr <- list( + release = pr[, release], + identity = pr[, identity], + ppp = ppp + ) + + # ------------------------------------------------------------------ + # Create folders (pure filesystem step) + # ------------------------------------------------------------------ + folder_paths <- set_pip_folders( + main_dir = main_dir, + release = pr[, release], + identity = pr[, identity] + ) + + # ------------------------------------------------------------------ + # Initialize stamp aliases (one alias per folder) + # ------------------------------------------------------------------ + aliases <- init_pip_aliases( + folder_paths, + include_release = alias_include_release, + release = pr[, release] + ) + + # ------------------------------------------------------------------ + # Persist state in .pipenv + # ------------------------------------------------------------------ rlang::env_poke(.pipenv, "stamp_root", main_dir) rlang::env_poke(.pipenv, "wrk_release", wr) rlang::env_poke(.pipenv, "gls", gls) - rlang::env_poke(.pipenv, "folder_paths", folder_paths) # updated + rlang::env_poke(.pipenv, "folder_paths", folder_paths) + rlang::env_poke(.pipenv, "pip_aliases", aliases) if (verbose) { - cli::cli_alert_info("PIP working release setup to {.field {wr$release}-{wr$identity}}") + cli::cli_alert_info( + "PIP working release set to {.field {wr$release}-{wr$identity}}" + ) print(folder_paths) + cli::cli_alert_info("Registered PIP aliases:") + print(aliases) } invisible(wr) @@ -98,6 +126,8 @@ setup_working_release <- function(release = NULL, + + #' Set PIP directory paths #' #' This function creates all necessary directories for a PIP release @@ -251,3 +281,125 @@ get_pip_folders <- function(folder = NULL, invisible(pip_folders[[folder]]) } + + + + +#' Get PIP aliases from .pipenv +#' +#' Retrieve the alias mapping that was registered during setup_working_release(). +#' +#' @param folder character: optional, name of a specific folder alias to retrieve. +#' If NULL (default), returns all aliases. +#' @param name character: name of the object to assign to the calling environment. +#' Default is `"pip_aliases"`. +#' @param verbose logical: whether to print info about the aliases retrieved. +#' Default is FALSE. +#' +#' @return Named character vector (invisible) of aliases or a single alias string +#' if `folder` is specified. +#' @export +#' +#' @examples +#' \dontrun{ +#' setup_working_release() +#' get_pip_aliases() # returns all aliases +#' get_pip_aliases("aux_data") # returns alias for aux_data +#' } +get_pip_aliases <- function(folder = NULL, + name = "pip_aliases", + verbose = FALSE) { + + pip_aliases <- get_from_pipenv("pip_aliases") + + if (is.null(pip_aliases)) { + cli::cli_abort( + c(x = "PIP aliases have not been set up", + i = "Run {.code pipfun::setup_working_release()} to register aliases") + ) + } + + if (verbose) { + cli::cli_alert_info("Retrieved PIP aliases") + print(pip_aliases) + } + + if (is.null(folder)) return(invisible(pip_aliases)) + + if (!(folder %in% names(pip_aliases))) { + cli::cli_abort("{.field {folder}} is not available in {.field pip_aliases}") + } + + invisible(pip_aliases[[folder]]) +} + + +#' Initialize stamp aliases for PIP folders +#' +#' @param folder_paths Named list from set_pip_folders() +#' @param include_release logical: append release to release-specific aliases +#' @param release character: release string (e.g. \"20251211\"). Required when include_release = TRUE +#' @return Invisible named character vector of aliases +#' @keywords internal +init_pip_aliases <- function(folder_paths, + verbose = getOption("pipfun.verbose"), + include_release = FALSE, + release = NULL) { + + alias_map <- c( + aux_data = "aux", + aux_metadata = "aux_meta", + dlw_data = "dlw", + dlw_inventory = "dlw_inv", + dlw_metadata = "dlw_meta", + pip_data = "pip", + pip_metadata = "pip_meta", + pip_inventory = "pip_inv", + pip_master_inventory = "pip_master" + ) + + # Which folders are release-specific (those that include rt in set_pip_folders) + release_specific <- c( + "aux_data", + "aux_metadata", + "dlw_metadata", + "pip_metadata", + "pip_inventory" + ) + + if (include_release && (is.null(release))) { + cli::cli_abort("release must be provided") + } + + # Build final alias names + final_aliases <- vapply(names(alias_map), function(nm) { + base <- alias_map[[nm]] + if (include_release && (nm %in% release_specific)) { + paste0(base, "_", release) + } else { + base + } + }, FUN.VALUE = character(1)) + + # Register aliases with stamp; let stamp handle conflicts/errors + for (nm in names(final_aliases)) { + + alias <- final_aliases[[nm]] + root <- fs::path_norm(folder_paths[[nm]]) + + stamp::st_init( + root = root, + alias = alias + ) + + if (verbose) { + cli::cli_alert_success( + "Registered alias {.field {alias}} → {.path {root}}" + ) + } + } + + invisible(final_aliases) +} + + diff --git a/man/get_pip_aliases.Rd b/man/get_pip_aliases.Rd new file mode 100644 index 0000000..8e8bd72 --- /dev/null +++ b/man/get_pip_aliases.Rd @@ -0,0 +1,32 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/working_release.R +\name{get_pip_aliases} +\alias{get_pip_aliases} +\title{Get PIP aliases from .pipenv} +\usage{ +get_pip_aliases(folder = NULL, name = "pip_aliases", verbose = FALSE) +} +\arguments{ +\item{folder}{character: optional, name of a specific folder alias to retrieve. +If NULL (default), returns all aliases.} + +\item{name}{character: name of the object to assign to the calling environment. +Default is \code{"pip_aliases"}.} + +\item{verbose}{logical: whether to print info about the aliases retrieved. +Default is FALSE.} +} +\value{ +Named character vector (invisible) of aliases or a single alias string +if \code{folder} is specified. +} +\description{ +Retrieve the alias mapping that was registered during setup_working_release(). +} +\examples{ +\dontrun{ +setup_working_release() +get_pip_aliases() # returns all aliases +get_pip_aliases("aux_data") # returns alias for aux_data +} +} diff --git a/man/init_pip_aliases.Rd b/man/init_pip_aliases.Rd new file mode 100644 index 0000000..3567459 --- /dev/null +++ b/man/init_pip_aliases.Rd @@ -0,0 +1,27 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/working_release.R +\name{init_pip_aliases} +\alias{init_pip_aliases} +\title{Initialize stamp aliases for PIP folders} +\usage{ +init_pip_aliases( + folder_paths, + verbose = getOption("pipfun.verbose"), + include_release = FALSE, + release = NULL +) +} +\arguments{ +\item{folder_paths}{Named list from set_pip_folders()} + +\item{include_release}{logical: append release to release-specific aliases} + +\item{release}{character: release string (e.g. \"20251211\"). Required when include_release = TRUE} +} +\value{ +Invisible named character vector of aliases +} +\description{ +Initialize stamp aliases for PIP folders +} +\keyword{internal} diff --git a/man/log_load.Rd b/man/log_load.Rd index 42dc742..405e0fb 100644 --- a/man/log_load.Rd +++ b/man/log_load.Rd @@ -5,36 +5,34 @@ \title{Load a log from disk} \usage{ log_load( - dir, id, name = id, version = NULL, format = "qs2", overwrite = FALSE, - verbose = TRUE + verbose = TRUE, + alias = NULL ) } \arguments{ -\item{dir}{Directory where the log is stored.} - -\item{id}{File identifier (without extension). Defaults to \code{name}.} +\item{id}{File identifier or path (extension optional).} \item{name}{Name to assign to the log in memory (default: \code{id}).} -\item{version}{Optional version identifier passed to \code{stamp::st_load()}. -Use \code{"available"} to list available versions.} +\item{version}{Optional version identifier passed to \code{stamp::st_load()}.} \item{format}{File format (default: "qs2").} -\item{overwrite}{Logical: whether to overwrite an existing log in -\code{.piplogenv}. Default is FALSE.} +\item{overwrite}{Logical: whether to overwrite an existing log in memory.} \item{verbose}{Logical: whether to announce loading progress.} + +\item{alias}{Optional stamp alias to select which catalog/versions to use.} } \value{ Invisibly returns the name of the loaded log. } \description{ -Loads a previously saved piplog from disk using {stamp}, optionally under a -different name. +Loads a previously saved piplog using {stamp}. \code{id} is the artifact path +(extension will be added if missing). Pass \code{alias} to select a stamp alias. } diff --git a/man/log_save.Rd b/man/log_save.Rd index a33d00f..72bfe15 100644 --- a/man/log_save.Rd +++ b/man/log_save.Rd @@ -6,21 +6,18 @@ \usage{ log_save( name = getOption("pipfun.log.default", "default"), - dir, id = name, format = "qs2", metadata = list(), code = NULL, + alias = NULL, ... ) } \arguments{ -\item{name}{Name of the log in memory (default: -\code{getOption("pipfun.log.default")}).} +\item{name}{Name of the log in memory (default: getOption("pipfun.log.default")).} -\item{dir}{Directory where the log should be saved.} - -\item{id}{File identifier (without extension). Defaults to \code{name}.} +\item{id}{File identifier or path (extension optional). Defaults to \code{name}.} \item{format}{File format (default: "qs2").} @@ -28,12 +25,14 @@ log_save( \item{code}{Optional code object whose hash will be stored.} +\item{alias}{Optional stamp alias to select which catalog/versions to use.} + \item{...}{Forwarded to \code{stamp::st_save()}.} } \value{ Invisibly, the result returned by \code{stamp::st_save()}. } \description{ -Saves a log stored in \code{.piplogenv} to disk using {stamp}, with metadata -and versioning support. +Saves a log stored in \code{.piplogenv} using {stamp}; \code{id} is the artifact path +(extension will be added if missing). Pass \code{alias} to select a stamp alias. } diff --git a/man/set_pip_folders.Rd b/man/set_pip_folders.Rd index d38ce78..790e579 100644 --- a/man/set_pip_folders.Rd +++ b/man/set_pip_folders.Rd @@ -11,10 +11,7 @@ set_pip_folders( ) } \arguments{ -\item{main_dir}{character: directory path where all PIP data is stored. By -default it is available in \code{getOption("pipfun.main_dir")}, but it is -basically a combination of \code{Sys.getenv("PIP_ROOT_DIR")} and -\code{getOption("pipfun.working_dir")}.} +\item{main_dir}{character: directory path where all PIP data is stored.} \item{identity}{character: one of "PROD", "INT", or "TEST"} } diff --git a/man/setup_working_release.Rd b/man/setup_working_release.Rd index 33cc222..8043b71 100644 --- a/man/setup_working_release.Rd +++ b/man/setup_working_release.Rd @@ -16,6 +16,7 @@ setup_working_release( ppp = getOption("pipfun.ppps"), creds = NULL, main_dir = getOption("pipfun.main_dir"), + alias_include_release = FALSE, ... ) } @@ -36,10 +37,7 @@ setup_working_release( \item{creds}{list. Basically, it is \code{get_github_creds()}} -\item{main_dir}{character: directory path where all PIP data is stored. By -default it is available in \code{getOption("pipfun.main_dir")}, but it is -basically a combination of \code{Sys.getenv("PIP_ROOT_DIR")} and -\code{getOption("pipfun.working_dir")}.} +\item{main_dir}{character: directory path where all PIP data is stored.} \item{...}{ Arguments passed on to \code{\link[=pip_create_globals]{pip_create_globals}} @@ -57,19 +55,9 @@ which is two years before the current year} }} } \value{ -invisible table with release information and list object in the -\code{.pipenv} environment +Invisible list with working release information } \description{ -This functions sets all the necessary information into the \code{.pipenv} -environment to be used by other PIP packages. It does not create releases. -} -\examples{ -\dontrun{ -# latest PROD release -setup_working_release() - -# error if set up again -try(setup_working_release()) -} +Sets the working PIP release and initializes stamp aliases +for all PIP folders associated with that release. } diff --git a/tests/testthat/test-get_from_gh.R b/tests/testthat/test-get_from_gh.R index 2d81622..4a6371d 100644 --- a/tests/testthat/test-get_from_gh.R +++ b/tests/testthat/test-get_from_gh.R @@ -1,177 +1,110 @@ skip_if_not_installed("gitcreds") +skip_if_not_installed("gh") +skip_if_not_installed("data.table") +skip_if_not_installed("janitor") +skip_if_not_installed("httr2") -creds <- get_github_creds() -token_pattern <- "^(gh[ps]_[a-zA-Z0-9]{36}|github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59})$" -is_token <- grepl(token_pattern, creds$password) - -if (!is_token) { - is_token <- grepl(Sys.getenv("GITHUB_PAT"), creds$password) -} - -skip_if_not(is_token, "Github token not valid") library(data.table) -library(collapse) +library(janitor) -# init conditions ----------- +# Obtain credentials and ensure a usable token is present +creds <- get_github_creds() +token_pattern <- "^(gh[ps]_[a-zA-Z0-9]{36}|github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59})$" +is_token <- isTRUE(grepl(token_pattern, creds$password)) || + identical(creds$password, Sys.getenv("GITHUB_PAT")) || + nzchar(Sys.getenv("GITHUB_PAT")) -## GEt ------- -owner <- getOption("pipfun.ghowner") -repo <- "pip_info" -branch <- "testing" -path <- "data" # Folder ith files +skip_if_not(is_token, "Valid GitHub token not available; skipping integration tests") +owner <- getOption("pipfun.ghowner") +repo <- "pip_info" +branch <- "testing" +path <- "data" -files <- gh::gh("GET /repos/:owner/:repo/contents/:path", - owner = owner, - repo = repo, - path = path, - ref = branch, +# List contents of folder on GitHub (may error if path differs) +files <- gh::gh("GET /repos/{owner}/{repo}/contents/{path}", + owner = owner, repo = repo, path = path, ref = branch, .token = creds$password) -# Extract the names of all the files in the folder +# collect download URLs and file names; skip if none found +file_urls <- vapply(files, function(x) x$download_url, character(1)) file_names <- vapply(files, function(x) x$name, character(1)) -## just get iris ------------ -file_urls <- vapply(files, function(x) x$download_url, character(1)) - -which_iris <- grep("iris", file_urls) -file_urls <- file_urls[which_iris] -names(file_urls) <- fs::path_ext(file_urls) |> - tolower() - - -## original iris data ------------- -iris10 <- iris[1:10,] |> - setDT() |> - janitor::clean_names() - -fct_vars <- iris10 |> - fact_vars("names") - -iris10[, (fct_vars) := lapply(.SD, as.character), - .SDcols = fct_vars] - - - +iris_idx <- grep("iris", tolower(file_names)) +if (length(iris_idx) == 0) { + skip("No iris files found in target GitHub path; skipping") +} +file_urls <- file_urls[iris_idx] +names(file_urls) <- tolower(fs::path_ext(file_urls)) + +# prepare expected small iris data for comparison (10 rows) +iris10 <- iris[1:10, ] |> as.data.table() |> janitor::clean_names() +# ensure factor columns converted to character for safe comparison +fct_cols <- names(Filter(is.factor, iris10)) +if (length(fct_cols) > 0) { + iris10[, (fct_cols) := lapply(.SD, as.character), .SDcols = fct_cols] +} -# download_from_gh() --------------- +# Helper to test download_from_gh -> file exists & readable test_download <- function(furl) { ext <- fs::path_ext(furl) - tfile <- tempfile(fileext = paste0(".",ext)) - - try(download_from_gh(furl, tfile), silent = TRUE) - info_df <- fs::file_info(tfile) |> - setDT() + tfile <- tempfile(fileext = paste0(".", ext)) + on.exit(unlink(tfile), add = TRUE) + download_from_gh(furl, tfile, creds = creds) - # test characteristics of file ------- - expect_true(info_df$size > 0) - expect_true(info_df$permissions == "rw-") - expect_true(fs::file_access(tfile)) - unlink(tfile) + info_df <- fs::file_info(tfile) + expect_true(info_df$size > 0, info = "Downloaded file should be non-empty") + expect_true(fs::file_access(tfile), info = "Downloaded file should be accessible") } -test_that("download each extension correctly", { - +test_that("download_from_gh downloads files for each extension", { purrr::walk(file_urls, \(x) test_download(x)) - }) -test_that("branch or tag not available is returned as error", { - bad_file <- gsub("data", "flu", file_urls[1]) +test_that("download_from_gh reports error for missing branch or file", { + bad_file <- sub("/data/", "/flu/", file_urls[1]) ext <- fs::path_ext(bad_file) - tfile <- tempfile(fileext = paste0(".",ext)) - - download_from_gh(bad_file, tfile) |> - expect_error() - - bad_branch <- gsub("testing", "flu", file_urls[1]) - ext <- fs::path_ext(bad_branch) - tfile <- tempfile(fileext = paste0(".",ext)) - - download_from_gh(bad_branch, tfile) |> - expect_error() + tfile <- tempfile(fileext = paste0(".", ext)) + on.exit(unlink(tfile), add = TRUE) + expect_error(download_from_gh(bad_file, tfile, creds = creds)) + + bad_branch_file <- file_urls[1] |> sub(paste0("/", branch, "/"), "/nonexistent_branch/", fixed = TRUE) + ext2 <- fs::path_ext(bad_branch_file) + tfile2 <- tempfile(fileext = paste0(".", ext2)) + on.exit(unlink(tfile2), add = TRUE) + expect_error(download_from_gh(bad_branch_file, tfile2, creds = creds)) }) - -# load_from_disk() --------------- - +# Test reading files from disk via load_from_disk after download test_read <- function(furl) { ext <- fs::path_ext(furl) - tfile <- tempfile(fileext = paste0(".",ext)) + tfile <- tempfile(fileext = paste0(".", ext)) + on.exit(unlink(tfile), add = TRUE) - download_from_gh(furl, tfile) - df <- load_from_disk(tfile) |> - as.data.table() + download_from_gh(furl, tfile, creds = creds) + df <- load_from_disk(tfile) |> as.data.table() - # test characteristics of file ------- - expect_equal(iris10, df, - ignore_attr = TRUE, - info = paste("failed in", ext)) - unlink(tfile) -} + # normalize and compare to expected iris10 where appropriate + df <- janitor::clean_names(df) + fct_cols_df <- names(Filter(is.factor, df)) + if (length(fct_cols_df) > 0) { + df[, (fct_cols_df) := lapply(.SD, as.character), .SDcols = fct_cols_df] + } + expect_equal(iris10, df, ignore_attr = TRUE) +} -test_that("files are read correctly from disk", { +test_that("files are read correctly from disk after download", { purrr::walk(file_urls, \(x) test_read(x)) }) -# get_file_info_from_gh() --------- -test_that("get_file_info_from_gh extract right info", { - info <- get_file_info_from_gh(owner, - repo, - branch = branch, - "data/iris.csv")|> - expect_no_error() -}) - - -# get branch info from gh -test_that("get branch info from gh works as expected", { - - get_branch_info_from_gh(repo = "aux_test", - branch = "main") |> - expect_no_error() - - branch_info <- get_branch_info_from_gh(repo = "aux_test", - branch = "main") - - class(branch_info) |> - expect_equal("list") - - get_branch_info_from_gh(repo = "cnjwe", - branch = "main") |> - expect_error() - - # Check correct info - - fake_creds <- function() list(token = "fake_token") - fake_gh <- function(..., owner, repo, branch, .token) { - list( - name = branch, - commit = list(sha = "testsha123"), - protection_url = "https://api.github.com/repos/owner/repo/branches/main/protection" - ) - } - fake_url <- function(url) list(protection_level = "high") - - res <- get_branch_info_from_gh( - owner = "owner", - repo = "repo", - branch = "main", - gh_func = fake_gh, - creds_func = fake_creds, - url_func = fake_url +test_that("get_file_info_from_gh returns metadata for a specific file", { + expect_silent( + get_file_info_from_gh(owner = owner, repo = repo, branch = branch, file_path = "data/iris.csv") ) - - expect_equal(res$name, - "main") - expect_equal(res$commit$sha, - "testsha123") - expect_equal(res$protection_level, - "high") +}) -}) diff --git a/tests/testthat/test-log.R b/tests/testthat/test-log.R index fb9586d..0720112 100644 --- a/tests/testthat/test-log.R +++ b/tests/testthat/test-log.R @@ -218,73 +218,46 @@ test_that("log_exists() works as expected", { # Save and load -------- - -test_that("log_save() and log_load() work as expected with pins board", { - skip_on_ci() # Skip on GitHub Actions or CI environments - skip_if_not_installed("qs") - skip_if_not_installed("pins") - - name <- "persist_test" - board <- pins::board_temp(versioned = TRUE) - pin_name <- "persist_test_pin" - - # Create and populate log - log_init(name, overwrite = TRUE) - log_info(message = "Saving this log", - name = name) - - # Save to pins board - expect_true(log_save(name = name, board = board, pin_name = pin_name)) - expect_true(pin_name %in% pins::pin_list(board)) - - # Clear from memory - log_reset(name) - expect_false(name %in% log_names()) - - # Load back from pins board - log_load(board = board, pin_name = pin_name) - expect_true(pin_name %in% log_names()) - - # Check contents - log <- rlang::env_get(.piplogenv, pin_name) - expect_s3_class(log, "piplog") - expect_equal(nrow(log), 1) - expect_match(log$message[1], "Saving this log") - - # Clean up - log_reset(pin_name) +test_that("log_save and log_load work with stamp alias", { + root <- fs::path(tempdir(), "pipfun_test_alias") + fs::dir_create(root) + stamp::st_init(root, alias = "A") + + id <- fs::path(root, "demo_log") # artifact path (extension added by functions) + + # prepare and save log + log_init("demo", overwrite = TRUE) + log_add("info", "smoke test", name = "demo") + log_save(name = "demo", id = id, alias = "A", format = "qs2") + + # versions exist under the correct extension and alias + vr <- stamp::st_versions(fs::path_ext_set(id, "qs2"), alias = "A") + expect_true(nrow(vr) > 0) + + # load into a new name + log_reset("demo") + log_load(id = id, name = "demo_loaded", alias = "A", format = "qs2") + expect_true(rlang::env_has(.piplogenv, "demo_loaded")) + expect_s3_class(rlang::env_get(.piplogenv, "demo_loaded"), "piplog") + + # list available versions via log_load(..., version = "available") + avail <- log_load(id = id, version = "available", alias = "A", format = "qs2") + expect_true(NROW(avail) > 0) }) -# log_filter and log_summary ----------------------------------------------- - -test_that("log_filter() returns filtered entries", { - log_init("testlog", overwrite = TRUE) - - log_info("Message 1", name = "testlog") - log_warn("Message 2", name = "testlog") - log_error("Message 3", name = "testlog") - - errors <- log_filter(name = "testlog", event = "error") - expect_s3_class(errors, "piplog") - expect_equal(nrow(errors), 1) - expect_equal(errors$event, "error") - - warnings <- log_filter(name = "testlog", event = "warning") - expect_equal(nrow(warnings), 1) - expect_equal(warnings$event, "warning") -}) - -test_that("log_summary returns correct counts", { - log_init("testlog", overwrite = TRUE) +test_that("log_load respects overwrite flag", { + root <- fs::path(tempdir(), "pipfun_test_alias_overwrite") + fs::dir_create(root) + stamp::st_init(root, alias = "B") - log_info("Info msg", name = "testlog") - log_warn("Warn msg", name = "testlog") - log_error("Error msg", name = "testlog") + id <- fs::path(root, "demo2") + log_init("demo2", overwrite = TRUE) + log_add("info", "for overwrite", name = "demo2") + log_save(name = "demo2", id = id, alias = "B", format = "qs2") - s <- log_summary("testlog") - expect_s3_class(s, "log_summary") - expect_equal(sum(s$count), 3) - expect_true(all(s$event %in% c("info", "warning", "error"))) + # create an in-memory log with the target name and ensure load errors without overwrite + log_init("already_here", overwrite = TRUE) + expect_error(log_load(id = id, name = "already_here", alias = "B", overwrite = FALSE)) }) diff --git a/tests/testthat/test-save_to_gh.R b/tests/testthat/test-save_to_gh.R index 7545e7a..ec11e1b 100644 --- a/tests/testthat/test-save_to_gh.R +++ b/tests/testthat/test-save_to_gh.R @@ -1,4 +1,6 @@ -# Preliminary operations +library(testthat) +library(mockery) +library(base64enc) # Sample data frame for testing df_sample <- data.frame( @@ -7,225 +9,58 @@ df_sample <- data.frame( stringsAsFactors = FALSE ) -repo <- "aux_test" -owner <- getOption("pipfun.ghowner") -creds <- get_github_creds() - - -# Load packages -library(base64enc) # For base64 encoding/decoding - - -# -------------------------------------------- # -# Test save_to_gh() #### -# -------------------------------------------- # - -## Inputs #### - -test_that("save_to_gh aborts if 'gh' package is not installed", { - - if (requireNamespace("gh", quietly = TRUE)) { - skip("Test skipped because 'gh' is already installed.") - } - - expect_error( - save_to_gh(df = df_sample, - repo = "aux_test", - filename = "test_save", - ext = "csv"), - "Package 'gh' is required. Please install it using install.packages('gh')." +# Mock GitHub responses +fake_gh <- function(...){ + list( + content = list(sha = "fake_sha", path = "fake_path.csv"), + commit = list(sha = "commit_sha") ) -}) +} -test_that("save_to_gh throws an error if metadata is missing 'sha' or 'path'", { +test_that("save_to_gh throws error if metadata missing 'sha' or 'path'", { + stub(save_to_gh, 'gh::gh', fake_gh) - # Case 1: Metadata without 'sha' metadata_no_sha <- list(path = "path/to/file.csv") - expect_error( - save_to_gh(df = df_sample, - repo = "aux_test", - filename = "test_save", - ext = "csv", - metadata = metadata_no_sha) - ) - - # Case 2: Metadata without 'path' metadata_no_path <- list(sha = "12345abcde") - expect_error( - save_to_gh(df = df_sample, - repo = "aux_test", - filename = "test_save", - ext = "csv", - metadata = metadata_no_path) - ) + metadata_empty <- list() - # Case 3: Metadata with neither 'sha' nor 'path' - metadata_no_sha_no_path <- list() - expect_error( - save_to_gh(df = df_sample, - repo = "aux_test", - filename = "test_save", - ext = "csv", - metadata = metadata_no_sha_no_path) - ) + expect_error(save_to_gh(df = df_sample, repo = "aux_test", filename = "test", ext = "csv", metadata = metadata_no_sha)) + expect_error(save_to_gh(df = df_sample, repo = "aux_test", filename = "test", ext = "csv", metadata = metadata_no_path)) + expect_error(save_to_gh(df = df_sample, repo = "aux_test", filename = "test", ext = "csv", metadata = metadata_empty)) }) -## Save file correctly, 3 cases: -# 1. new file, new data (data_change is TRUE) -# 2. old file, new data (data_change is TRUE) -# 3. old file, old data (data_change is FALSE) - -test_that("save_to_gh saves file correctly", { - - # Case 1. - res <- save_to_gh( - df = df_sample, - repo = "aux_test", - owner = getOption("pipfun.ghowner"), - branch = "DEV", # Replace with the branch you want to test - filename = "new_data_test", # Replace with a file name that exists in the repo - ext = "csv", - metadata = NULL, - verbose = TRUE - ) - - res$init |> - expect_null() # init should be NULL because file did not exist +test_that("convert_df_to_base64 works correctly", { + skip_on_ci() - res$data_change |> - expect_equal(TRUE) - - # -- delete new file for to prevent subsequent tests call from failing --- # - gh::gh( - "DELETE /repos/{owner}/{repo}/contents/{path}", - owner = owner, - repo = repo, - path = "new_data_test.csv", - message = "delete file for testing", # Commit message - .token = creds$password, - sha = res$content$sha, - branch = "DEV" # Branch where the file exists - ) - - - # Case 2. - set.seed(Sys.time()) #Ensure randomness across sessions - - res <- save_to_gh( - df = data.frame( - id = 1:5, - value = runif(5, 0, 100), # Random numeric values between 0 and 100 - category = sample(letters[1:3], 5, replace = TRUE) # Random categories - ), - repo = "aux_test", - owner = getOption("pipfun.ghowner"), - branch = "DEV", # Replace with the branch you want to test - filename = "test_save", # Replace with a file name that exists in the repo - ext = "csv", - metadata = NULL, - verbose = TRUE - ) - - res$init |> - is.null() |> - expect_false() # init should be available because file existed - - res$data_change |> - expect_equal(TRUE) - - - # Case 3. - - # metadata is available and file exists - res <- save_to_gh( - df = data.frame(x = 1:5, - y = letters[1:5]), - repo = "aux_test", - owner = getOption("pipfun.ghowner"), - branch = "DEV", # Replace with the branch you want to test - filename = "data_test", # Replace with a file name that exists in the repo - ext = "csv", - metadata = NULL, - verbose = TRUE - ) - - res$init |> - is.null() |> - expect_false() # init should not be NULL because file already existed - - res$init$path |> - expect_equal("data_test.csv") - - res$data_change |> - expect_equal(FALSE) - - # Output structure - names(res) |> - expect_equal(c("content", "commit", - "init", "owner", - "repo", "branch", "data_change")) - - -}) - - -# # ------------------------------- -# # Tests for convert_df_to_base64() -# # ------------------------------- -# -test_that("convert_df_to_base64 works correctly for all supported file extensions", { - # Skip on CI/CD environments like GitHub Actions - testthat::skip_on_ci() - - # Supported extensions extensions <- c("csv", "json", "rds", "qs", "fst", "dta") for (ext in extensions) { - # Test that the function returns a base64-encoded string - encoded_content <- convert_df_to_base64(df_sample, ext) - expect_true(is.character(encoded_content)) - expect_true(nchar(encoded_content) > 0) + encoded <- convert_df_to_base64(df_sample, ext) + expect_true(is.character(encoded)) + expect_true(nchar(encoded) > 0) - # Decode the base64 string - decoded_content <- base64enc::base64decode(encoded_content) + decoded <- base64enc::base64decode(encoded) - # For csv and json, we can check if the decoded content matches the original data frame if (ext == "csv") { - content_string <- rawToChar(decoded_content) - read_df <- readr::read_csv(content_string, show_col_types = FALSE) + read_df <- readr::read_csv(rawToChar(decoded), show_col_types = FALSE) expect_equal(df_sample, as.data.frame(read_df)) - } else if (ext == "json") { - content_string <- rawToChar(decoded_content) - read_df <- jsonlite::fromJSON(content_string) + read_df <- jsonlite::fromJSON(rawToChar(decoded)) expect_equal(df_sample, as.data.frame(read_df)) - } else if (ext == "rds") { - read_df <- unserialize(decoded_content) - expect_equal(df_sample, read_df) - + expect_equal(df_sample, unserialize(decoded)) } else if (ext == "qs") { - read_df <- qs::qdeserialize(decoded_content) - expect_equal(df_sample, read_df) - + expect_equal(df_sample, qs::qdeserialize(decoded)) } else if (ext == "fst") { - # For 'fst', write the decoded content to a temp file and read it back - temp_file <- tempfile(fileext = ".fst") - on.exit(unlink(temp_file), add = TRUE) - writeBin(decoded_content, temp_file) - read_df <- fst::read_fst(temp_file) - expect_equal(df_sample, as.data.frame(read_df)) - + tmp <- tempfile(fileext = ".fst") + on.exit(unlink(tmp), add = TRUE) + writeBin(decoded, tmp) + expect_equal(df_sample, as.data.frame(fst::read_fst(tmp))) } else if (ext == "dta") { - # For 'dta', write the decoded content to a temp file and read it back - temp_file <- tempfile(fileext = ".dta") - on.exit(unlink(temp_file), add = TRUE) - writeBin(decoded_content, temp_file) - read_df <- haven::read_dta(temp_file) - expect_equal(df_sample, - as.data.frame(read_df), - ignore_attr = TRUE) + tmp <- tempfile(fileext = ".dta") + on.exit(unlink(tmp), add = TRUE) + writeBin(decoded, tmp) + expect_equal(df_sample, as.data.frame(haven::read_dta(tmp)), ignore_attr = TRUE) } } - }) diff --git a/tests/testthat/test-working_release.R b/tests/testthat/test-working_release.R index 96d8a25..21eb27b 100644 --- a/tests/testthat/test-working_release.R +++ b/tests/testthat/test-working_release.R @@ -1,75 +1,235 @@ +library(testthat) library(withr) -# Helper: create a temp main_dir for testing +# ------------------------------------------------------------------ +# Helpers +# ------------------------------------------------------------------ create_temp_main_dir <- function() { tmp <- tempfile("piptest_") - dir.create(tmp) + dir.create(tmp, recursive = TRUE) tmp } -# Test set_pip_boards +clear_pipenv <- function() { + if (exists(".pipenv", envir = globalenv())) { + rm(list = ls(envir = .pipenv), envir = .pipenv) + } else { + return(NULL) + } +} + +restore_pipenv <- function() { + # Re-run setup_working_release for the shared_main_dir + setup_working_release( + release = lr_shared$release, + identity = lr_shared$identity, + verbose = FALSE, + main_dir = shared_main_dir, + alias_include_release = TRUE + ) +} + +# ------------------------------------------------------------------ +# Single shared working release for the whole test file +# (prevents re-registering the same short aliases for different folders +# across multiple temp dirs in the same R session) +# ------------------------------------------------------------------ +shared_main_dir <- create_temp_main_dir() +lr_shared <- get_latest_pip_release() + +# --- run setup once for the file (replace local_test_setup) --- +clear_pipenv() +tryCatch( + { + setup_working_release( + release = lr_shared$release, + identity = lr_shared$identity, + verbose = FALSE, + main_dir = shared_main_dir, + alias_include_release = TRUE + ) + }, + error = function(e) { + if (grepl("already registered for a different folder", conditionMessage(e))) { + testthat::skip("Existing global stamp aliases detected; skipping working_release tests") + } + stop(e) + } +) + +# ------------------------------------------------------------------ +# set_pip_folders (pure function; safe to test) +# ------------------------------------------------------------------ +test_that("set_pip_folders returns expected structure and creates directories", { + folders <- set_pip_folders( + main_dir = shared_main_dir, + release = lr_shared$release, + identity = lr_shared$identity + ) + + expect_s3_class(folders, "pip_folder_paths") -test_that("set_pip_boards returns pip_boards S3 object with expected names", { - main_dir <- create_temp_main_dir() - lr <- get_latest_pip_release() - boards <- set_pip_boards(main_dir = main_dir, release = lr$release, identity = lr$identity) - expect_s3_class(boards, "pip_boards") - expect_true(all(c("aux_data", "dlw_data", "dlw_inventory", "pip_data", "pip_metadata", "pip_inventory") %in% names(boards))) + expect_true(all(c( + "stamp_root", + "aux_data", "aux_metadata", + "dlw_data", "dlw_inventory", "dlw_metadata", + "pip_data", "pip_metadata", + "pip_inventory", "pip_master_inventory" + ) %in% names(folders))) + + expect_true(dir.exists(folders$aux_data)) + expect_true(dir.exists(folders$pip_data)) + expect_true(dir.exists(folders$pip_inventory)) +}) + +# ------------------------------------------------------------------ +# setup_working_release (state validation only) +# ------------------------------------------------------------------ +test_that("setup_working_release populated .pipenv with expected entries", { + expect_true(exists("wrk_release", envir = .pipenv, inherits = FALSE)) + expect_true(exists("gls", envir = .pipenv, inherits = FALSE)) + expect_true(exists("folder_paths", envir = .pipenv, inherits = FALSE)) + expect_true(exists("pip_aliases", envir = .pipenv, inherits = FALSE)) + + folders <- get_from_pipenv("folder_paths") + aliases <- get_from_pipenv("pip_aliases") + + expect_s3_class(folders, "pip_folder_paths") + expect_type(aliases, "character") + expect_true(length(aliases) > 0) + # alias names correspond to folder keys + expect_true(all(names(aliases) %in% names(folders))) }) -test_that("print.pip_boards produces output and returns invisibly", { - main_dir <- create_temp_main_dir() - lr <- get_latest_pip_release() - boards <- set_pip_boards(main_dir = main_dir, release = lr$release, identity = lr$identity) - expect_invisible(print(boards)) +# ------------------------------------------------------------------ +# get_wrk_release +# ------------------------------------------------------------------ +test_that("get_wrk_release assigns working release to caller", { + # assign into local test environment + get_wrk_release(name = "my_wr", verbose = FALSE) + expect_true(exists("my_wr", inherits = FALSE)) + expect_type(my_wr, "list") + expect_named(my_wr, c("release", "identity", "ppp")) }) -# Test setup_working_release and .pipenv - -test_that("setup_working_release sets up .pipenv with working_release, gls, and pins_boards", { - main_dir <- create_temp_main_dir() - with_options(list(pipfun.main_dir = main_dir), { - lr <- get_latest_pip_release() - wr <- setup_working_release(release = lr$release, identity = lr$identity, verbose = FALSE) - expect_type(wr, "list") - expect_true(exists("wrk_release", envir = .pipenv, inherits = FALSE)) - expect_true(exists("gls", envir = .pipenv, inherits = FALSE)) - expect_true(exists("pins_boards", envir = .pipenv, inherits = FALSE)) - }) +test_that("get_wrk_release errors if working release not set", { + clear_pipenv() + expect_error( + get_wrk_release(verbose = FALSE) + ) + # restore for following tests + restore_pipenv() }) -# Test get_wrk_release +# ------------------------------------------------------------------ +# get_pip_folders +# ------------------------------------------------------------------ +test_that("get_pip_folders returns folder paths and assigns to caller", { + get_pip_folders(name = "my_folders", verbose = FALSE) + expect_true(exists("my_folders", inherits = FALSE)) + expect_s3_class(my_folders, "pip_folder_paths") -test_that("get_wrk_release assigns working_release to parent frame", { - main_dir <- create_temp_main_dir() - with_options(list(pipfun.main_dir = main_dir), { - lr <- get_latest_pip_release() - setup_working_release(release = lr$release, identity = lr$identity) - get_wrk_release(name = "my_wr") - expect_true(exists("my_wr")) - }) + # single folder retrieval + aux_path <- get_pip_folders("aux_data", verbose = FALSE) + expect_type(aux_path, "character") + expect_true(dir.exists(aux_path)) }) -test_that("get_wrk_release errors if working_release not set", { - rm(list = ls(envir = .pipenv), envir = .pipenv) - expect_error(get_wrk_release(verbose = FALSE), "Working release has not been set up") +test_that("get_pip_folders errors if folder_paths not set", { + clear_pipenv() + expect_error( + get_pip_folders(verbose = FALSE), + "PIP folder paths have not been set up" + ) + restore_pipenv() }) -# Test get_pins_boards - -test_that("get_pins_boards assigns pins_boards to parent frame", { - main_dir <- create_temp_main_dir() - with_options(list(pipfun.main_dir = main_dir), { - lr <- get_latest_pip_release() - setup_working_release(release = lr$release, identity = lr$identity, verbose = FALSE) - env <- new.env() - with(env, get_pins_boards(name = "my_boards", verbose = FALSE)) - expect_true(exists("my_boards", envir = env)) - expect_s3_class(env$my_boards, "pip_boards") - }) +# ------------------------------------------------------------------ +# get_pip_aliases +# ------------------------------------------------------------------ + + +test_that("get_pip_aliases can return a single alias", { + a <- get_pip_aliases("aux_data", verbose = FALSE) + expect_type(a, "character") + expect_length(a, 1) }) -test_that("get_pins_boards errors if pins_boards not set", { - rm(list = ls(envir = .pipenv), envir = .pipenv) - expect_error(get_pins_boards(verbose = FALSE), "PIP pins boards have not been set up") +test_that("get_pip_aliases errors if pip_aliases not set", { + clear_pipenv() + expect_error( + get_pip_aliases(verbose = FALSE), + "PIP aliases have not been set up" + ) + restore_pipenv() }) + + +# ------------------------------------------------------------------ +# init_pip_aliases +# ------------------------------------------------------------------ +test_that("init_pip_aliases returns release-specific aliases and registers them", { + # isolated temp main dir to avoid clashes with shared tests + tmp_main <- create_temp_main_dir() + unique_release <- paste0("UT", as.integer(Sys.time())) + + # create folder paths for this temp main dir + fp <- set_pip_folders( + main_dir = tmp_main, + release = unique_release, + identity = lr_shared$identity + ) + + res <- NULL + tryCatch( + { + res <- init_pip_aliases( + folder_paths = fp, + include_release = TRUE, + release = unique_release, + verbose = FALSE + ) + }, + error = function(e) { + if (grepl("already registered for a different folder", conditionMessage(e))) { + testthat::skip("Existing global stamp aliases detected; skipping init_pip_aliases test") + } + stop(e) + } + ) + + expect_type(res, "character") + + expected_keys <- c( + "aux_data", "aux_metadata", + "dlw_data", "dlw_inventory", "dlw_metadata", + "pip_data", "pip_metadata", + "pip_inventory", "pip_master_inventory" + ) + + expect_setequal(names(res), expected_keys) + + # release-specific aliases should include the release suffix + release_specific <- c( + "aux_data", + "aux_metadata", + "dlw_metadata", + "pip_metadata", + "pip_inventory" + ) + + for (nm in release_specific) { + expect_true(grepl(paste0("_", unique_release, "$"), res[[nm]])) + } + + # non-release-specific aliases should NOT include the suffix + non_release <- setdiff(expected_keys, release_specific) + for (nm in non_release) { + expect_false(grepl(paste0("_", unique_release, "$"), res[[nm]])) + } + + # alias values should be non-empty and unique + vals <- unname(res) + expect_true(all(nzchar(vals))) + expect_equal(length(unique(vals)), length(vals)) +}) \ No newline at end of file