diff --git a/DESCRIPTION b/DESCRIPTION index 8c3a428..6189f59 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,8 +1,9 @@ Package: workspace Title: Workspace and Data Management Tools -Version: 0.1.2 +Version: 0.1.3.002 Authors@R: c( - person("David", "Gohel", , "david.gohel@ardata.fr", role = c("aut", "cre")), + person("Eli", "Daniels", , "eli.daniels@ardata.fr", role = c("aut", "cre")), + person("David", "Gohel", , "david.gohel@ardata.fr", role = c("aut")), person("ArData", role = "cph") ) Description: This package provides tools for managing R workspaces and @@ -22,7 +23,12 @@ Imports: tibble, tools, utils, - zip + zip, + yaml Encoding: UTF-8 Roxygen: list(markdown = TRUE) RoxygenNote: 7.3.2 +Suggests: + testthat (>= 3.0.0), + sf +Config/testthat/edition: 3 diff --git a/NAMESPACE b/NAMESPACE index ca16eb5..b7c0a3c 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,6 +1,8 @@ # Generated by roxygen2: do not edit by hand S3method(print,workspace) +S3method(store_dataset,data.frame) +S3method(store_dataset,sf) export(delete_dataset) export(list_object_in_workspace) export(new_workspace) @@ -10,10 +12,12 @@ export(read_dataset_in_workspace) export(read_json_str_in_workspace) export(read_rds_in_workspace) export(read_timestamp) +export(read_yaml_in_workspace) export(rm_object_in_workspace) export(store_dataset) export(store_json) export(store_rds) +export(store_yaml) export(unpack_folder) export(unpack_workspace) export(workspace_bind) @@ -32,6 +36,7 @@ importFrom(dplyr,select) importFrom(dplyr,semi_join) importFrom(rlang,env_has) importFrom(rlang,env_names) +importFrom(rlang,is_list) importFrom(rlang,is_string) importFrom(rlang,new_environment) importFrom(stringi,stri_trans_general) @@ -40,5 +45,7 @@ importFrom(tools,file_path_sans_ext) importFrom(utils,globalVariables) importFrom(utils,head) importFrom(utils,packageVersion) +importFrom(yaml,read_yaml) +importFrom(yaml,write_yaml) importFrom(zip,unzip) importFrom(zip,zip) diff --git a/NEWS.md b/NEWS.md index 5c24139..b765737 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,16 @@ +# workspace 0.1.3 + +## Features + +- Add support for storing and reading yaml files +- Add support for storing and reading sf datasets (geospatial) using +gpkg file format. + + +## Tests + +- Greatly increase test coverage. + # workspace 0.1.2 ## Features diff --git a/R/readers.R b/R/readers.R index e3cd531..95efeb3 100644 --- a/R/readers.R +++ b/R/readers.R @@ -35,7 +35,7 @@ list_object_in_workspace <- function(x) { #' @family functions to read in a workspace read_dataset_in_workspace <- function(x, name) { objs <- list_object_in_workspace(x) - objs <- objs[objs$type %in% "dataset", ] + objs <- objs[objs$type %in% c("dataset", "geospatial"), ] objs <- objs[objs$name %in% name, ] if (nrow(objs) < 1) { @@ -45,7 +45,35 @@ read_dataset_in_workspace <- function(x, name) { cli_abort("Value of {.code name} is not unique in workspace.") } file <- file.path(x$dir, objs$file) - read_parquet(file, mmap = FALSE) + + if (objs$type %in% "dataset"){ + read_parquet(file, mmap = FALSE) + + } else if (objs$type %in% "geospatial") { + + if (!requireNamespace("sf", quietly = TRUE)) { + cli_abort("{.val sf} package must be installed to store dataset of type {.cls sf}.") + } + + geo_sf <- sf::st_read(file, geometry_column = "geom", quiet = TRUE) + + # Using gpkg means geometry col is written as geom (not ideal) + # https://github.com/r-spatial/sf/issues/719 + yaml_relative <- file.path(.assets_directory, "sf_metadata", paste0(name, '.yaml')) + yaml_abs <- file.path(x$dir, yaml_relative) + if (file.exists(yaml_abs)) { + metadata <- yaml::read_yaml(yaml_abs) + sf::st_geometry(geo_sf) <- metadata$sf_column + sf::st_crs(geo_sf) <- metadata$crs + geo_sf + } else { + cli_warn(paste( + "No {.arg sf_metadata} file found for name: {.val name}.", + "Geometry column and project may have changed since data was stored." + )) + geo_sf + } + } } #' @export @@ -121,6 +149,10 @@ read_rds_in_workspace <- function(x, name, subdir = NULL) { read_json_str_in_workspace <- function(x, name, subdir = NULL) { objs <- list_object_in_workspace(x) objs <- objs[objs$type %in% "json", ] + + if (nrow(objs) < 1) { + cli_abort("workspace {.arg x} has no json files.") + } objs <- objs[objs$name %in% name, ] if (!is.null(subdir)) { @@ -138,6 +170,66 @@ read_json_str_in_workspace <- function(x, name, subdir = NULL) { paste0(str, collapse = "") } +#' @importFrom yaml read_yaml +#' @export +#' @title Read YAML Object from a Workspace. +#' @description +#' Read a YAML file stored as a list object in a workspace. +#' Returns the YAML content as an R list object. +#' @param x the workspace +#' @param name name associated with the YAML file stored in the workspace +#' @param subdir Optional subdirectory used for the asset to retrieve +#' @examples +#' library(workspace) +#' dir_tmp <- tempfile(pattern = "ws") +#' z <- new_workspace(dir = dir_tmp) +#' +#' config_list <- list( +#' database = list( +#' host = "localhost", +#' port = 5432 +#' ), +#' settings = list( +#' debug = TRUE, +#' max_connections = 100 +#' ) +#' ) +#' z <- store_yaml( +#' x = z, +#' list = config_list, +#' filename = "config.yaml", +#' name = "app_config", +#' timestamp = "2023-11-12 11:37:41", +#' subdir = "configs" +#' ) +#' read_yaml_in_workspace(z, "app_config", subdir = "configs") +#' @family functions to read in a workspace +read_yaml_in_workspace <- function(x, name, subdir = NULL) { + + objs <- list_object_in_workspace(x) + objs <- objs[objs$type %in% "yaml", ] + + if (nrow(objs) < 1) { + cli_abort("workspace {.arg x} has no yaml files.") + } + objs <- objs[objs$name %in% name, ] + + if (!is.null(subdir)) { + objs <- objs[objs$subdir %in% subdir, ] + } + + if (nrow(objs) < 1) { + cli_abort("Value of {.code name} cannot be found in workspace.") + } + if (nrow(objs) > 1) { + cli_abort("Value of {.code name} is not unique in workspace.") + } + file <- file.path(x$dir, objs$file) + yaml_list <- yaml::read_yaml(file) + yaml_list +} + + #' @export #' @title Read Timestamp associated with a content of a Workspace. #' @description @@ -163,6 +255,9 @@ read_json_str_in_workspace <- function(x, name, subdir = NULL) { read_timestamp <- function(x, name, type, subdir = NULL) { objs <- list_object_in_workspace(x) objs <- objs[objs$type %in% type, ] + if (nrow(objs) < 1) { + cli_abort("Value of {.code type} cannot be found in workspace.") + } objs <- objs[objs$name %in% name, ] if (!is.null(subdir)) { diff --git a/R/rm.R b/R/rm.R index c63f8f3..cc870f7 100644 --- a/R/rm.R +++ b/R/rm.R @@ -79,3 +79,42 @@ rm_object_in_workspace <- function(x, name, type, subdir = NULL) { x } + + +#' @export +#' @importFrom dplyr filter +#' @title Delete a dataset from a workspace +#' @description +#' Delete a dataset stored in a workspace. +#' This function removes the dataset file and updates the workspace's object descriptions. +#' @param x The workspace object. +#' @param data_name The name of the dataset to delete from the workspace. +#' @return return the workspace object +#' @examples +#' library(workspace) +#' dir_tmp <- tempfile(pattern = "ws") +#' z <- new_workspace(dir = dir_tmp) +#' z <- store_dataset(x = z, dataset = iris, name = "iris_dataset") +#' z <- store_dataset(x = z, dataset = mtcars, name = "mtcars") +#' z <- delete_dataset(x = z, data_name = "iris_dataset") +#' z +#' @family functions to write in a workspace +delete_dataset <- function(x, data_name) { + + base_file <- stri_trans_general(data_name, id = "latin-ascii") + + contains_parquet_ext <- grepl(pattern = "\\.parquet", x = base_file, ignore.case = TRUE) + if (!contains_parquet_ext) { + base_file <- paste0(base_file, ".parquet") + } + filepath <- file.path(x$dir, .datasets_directory, base_file) + if (file.exists(filepath)) { + unlink(filepath, force = TRUE) + } + + objects_descriptions <- read_objects_description(x) + objects_descriptions <- filter(.data = objects_descriptions, !.data$name %in% data_name) + save_objects_description(x, objs_desc = objects_descriptions) + + x +} diff --git a/R/store.R b/R/store.R index 7d8c412..4747b78 100644 --- a/R/store.R +++ b/R/store.R @@ -6,9 +6,16 @@ #' @description #' Store a dataset as a parquet file into an existing workspace. #' @param x the workspace object -#' @param dataset the data.frame to store in the workspace -#' @param name name associated with the data.frame +#' @param dataset the data.frame or sf to store in the workspace. +#' @param name name associated with the data.frame, if an workspace file with this name exists +#' already it will be replaced. #' @param timestamp A timestamp string to associate with the entry in the workspace. +#' @details +#' +#' If the input dataset is `sf` then a metadata yaml file is also written to the workspace +#' in the `assets/sf_metadata/{name}.yaml` location. This contains the `sf` column name +#' and the CRS that can be overidden when writing .gpkg data to disk. +#' #' @examples #' library(workspace) #' dir_tmp <- tempfile(pattern = "ws") @@ -18,16 +25,17 @@ #' z #' @family functions to write in a workspace store_dataset <- function(x, dataset, name, timestamp = format(Sys.time(), "%Y-%m-%d %H:%M:%S")) { + UseMethod("store_dataset", dataset) +} +#' @export +store_dataset.data.frame <- function(x, dataset, name, timestamp = format(Sys.time(), "%Y-%m-%d %H:%M:%S")) { base_file <- stri_trans_general(name, id = "latin-ascii") - contains_parquet_ext <- grepl(pattern = "\\.parquet", x = base_file, ignore.case = TRUE) if (!contains_parquet_ext) { base_file <- paste0(base_file, ".parquet") } - filepath <- file.path(x$dir, .datasets_directory, base_file) - write_parquet(dataset, filepath) objects_desc <- dataset_description( file = file.path(.datasets_directory, base_file), @@ -36,52 +44,66 @@ store_dataset <- function(x, dataset, name, timestamp = format(Sys.time(), "%Y-% type = "dataset", timestamp = timestamp ) - objects_descriptions <- read_objects_description(x) objects_descriptions <- rows_upsert(objects_descriptions, objects_desc, by = "name") save_objects_description(x, objs_desc = objects_descriptions) - x } #' @export -#' @importFrom dplyr filter -#' @title Delete a dataset from a workspace -#' @description -#' Delete a dataset stored in a workspace. -#' This function removes the dataset file and updates the workspace's object descriptions. -#' @param x The workspace object. -#' @param data_name The name of the dataset to delete from the workspace. -#' @return return the workspace object -#' @examples -#' library(workspace) -#' dir_tmp <- tempfile(pattern = "ws") -#' z <- new_workspace(dir = dir_tmp) -#' z <- store_dataset(x = z, dataset = iris, name = "iris_dataset") -#' z <- store_dataset(x = z, dataset = mtcars, name = "mtcars") -#' z <- delete_dataset(x = z, data_name = "iris_dataset") -#' z -#' @family functions to write in a workspace -delete_dataset <- function(x, data_name) { +#' @importFrom yaml write_yaml +store_dataset.sf <- function(x, dataset, name, timestamp = format(Sys.time(), "%Y-%m-%d %H:%M:%S")) { - base_file <- stri_trans_general(data_name, id = "latin-ascii") + if (!requireNamespace("sf", quietly = TRUE)) { + cli_abort("{.val sf} package must be installed to store dataset of type {.cls sf}.") + } - contains_parquet_ext <- grepl(pattern = "\\.parquet", x = base_file, ignore.case = TRUE) - if (!contains_parquet_ext) { - base_file <- paste0(base_file, ".parquet") + base_file <- stri_trans_general(name, id = "latin-ascii") + contains_geo_ext <- grepl( + pattern = "\\.(gpkg|shp|geojson)$", x = base_file, ignore.case = TRUE) + if (!contains_geo_ext) { + base_file <- paste0(base_file, ".gpkg") + } else { + if (grepl(pattern = "\\.(shp|geojson)$", x = base_file, ignore.case = TRUE)) { + cli_warn("{.val .shp} or {.val .geojson} extension detected in {.arg name}, file will be writen in {.arg .gpkg} format.") + } + base_file <- gsub( + pattern = "\\.(gpkg|shp|geojson)$", + replacement = ".gpkg", + x = base_file, + ignore.case = TRUE + ) } filepath <- file.path(x$dir, .datasets_directory, base_file) - if (file.exists(filepath)) { - unlink(filepath, force = TRUE) - } + sf::st_write(dataset, filepath, quiet = TRUE, delete_dsn = TRUE) + + objects_desc <- dataset_description( + file = file.path(.datasets_directory, base_file), + subdir = .geospatial_directory, + name = name, + type = "geospatial", + timestamp = timestamp + ) objects_descriptions <- read_objects_description(x) - objects_descriptions <- filter(.data = objects_descriptions, !.data$name %in% data_name) + objects_descriptions <- rows_upsert(objects_descriptions, objects_desc, by = "name") save_objects_description(x, objs_desc = objects_descriptions) + # Store metadata for read_dataset_in_workspace() + # Using gpkg means geometry col is written as geom (not ideal) + # https://github.com/r-spatial/sf/issues/719 + sf_metadata = list( + sf_column = attr(dataset, "sf_column"), + crs = sf::st_crs(dataset)$input + ) + yaml_file <- gsub("\\.gpkg$", ".yaml", base_file) + yaml_file <- file.path(x$dir, .assets_directory, "sf_metadata", yaml_file) + dir.create(dirname(yaml_file), showWarnings = FALSE, recursive = TRUE) + yaml::write_yaml(x = sf_metadata,file = yaml_file) x } + #' @export #' @importFrom tools file_path_sans_ext #' @title Store a JSON string in a workspace @@ -94,7 +116,8 @@ delete_dataset <- function(x, data_name) { #' @param x The workspace object. #' @param json_str The JSON string to save in the workspace. #' @param filename The name of the file used to store the JSON string in the workspace. -#' @param name name associated with the object +#' @param name name associated with the object, if an workspace file with this name exists +#' already it will be replaced. #' @param timestamp A timestamp string to associate with the entry in the workspace. #' @param subdir A subdirectory within the asset directory where the JSON file will be stored. #' @return return the workspace object @@ -175,7 +198,8 @@ store_json <- function(x, json_str, filename, name = NULL, subdir, timestamp = f #' @param x The workspace object. #' @param obj The R object to save as an RDS file. #' @param filename The name of the file used to store the RDS file in the workspace. -#' @param name name associated with the object +#' @param name name associated with the object, if an workspace file with this name exists +#' already it will be replaced. #' @param timestamp A timestamp string to associate with the entry in the workspace. #' @param subdir A subdirectory within the asset directory where the RDS file will be stored. #' @return return the workspace object @@ -223,7 +247,7 @@ store_rds <- function(x, obj, filename, name = NULL, subdir, timestamp = format( objects_desc <- dataset_description( file = file.path(.assets_directory, subdir, filename), - name = file_path_sans_ext(filename), + name = name, subdir = subdir, type = "rds", timestamp = timestamp @@ -236,3 +260,95 @@ store_rds <- function(x, obj, filename, name = NULL, subdir, timestamp = format( x } +#' @export +#' @importFrom tools file_path_sans_ext +#' @importFrom rlang is_list +#' @importFrom yaml write_yaml +#' @title Store a list as YAML in a workspace +#' @description +#' Saves a list object as a YAML file in an existing workspace. +#' +#' This function allows users to save R list objects as YAML files into a specified workspace. +#' The file is saved under the provided filename and can be organized within +#' a specific subdirectory for better management. +#' @param x The workspace object. +#' @param list The R list object to save as YAML in the workspace. +#' @param filename The name of the file used to store the YAML file in the workspace. +#' @param name name associated with the object, if a workspace file with this name exists +#' already it will be replaced. +#' @param timestamp A timestamp string to associate with the entry in the workspace. +#' @param subdir A subdirectory within the asset directory where the YAML file will be stored. +#' @return return the workspace object +#' @examples +#' library(workspace) +#' dir_tmp <- tempfile(pattern = "ws") +#' z <- new_workspace(dir = dir_tmp) +#' +#' config_list <- list( +#' database = list( +#' host = "localhost", +#' port = 5432, +#' name = "mydb" +#' ), +#' settings = list( +#' debug = TRUE, +#' max_connections = 100 +#' ) +#' ) +#' z <- store_yaml( +#' x = z, +#' list = config_list, +#' filename = "config.yaml", +#' timestamp = "2023-11-12 11:37:41", +#' subdir = "configs" +#' ) +#' z +#' @family functions to write in a workspace +store_yaml <- function(x, list, filename, name = NULL, subdir, timestamp = format(Sys.time(), "%Y-%m-%d %H:%M:%S")) { + + if (!inherits(list, 'list')) { + cli_abort("Argument {.code list} must be a {.cls list} object.") + } + if (!is_string(filename)) { + cli_abort("Argument {.code filename} must be a single character string.") + } + if (!is_string(timestamp)) { + cli_abort("Argument {.code timestamp} must be a single character string.") + } + if (!is_string(subdir)) { + cli_abort("Argument {.code subdir} must be a single character string.") + } + + contains_yaml_ext <- grepl(pattern = "\\.yaml", x = filename, ignore.case = TRUE) + if (!contains_yaml_ext) { + filename <- paste0(filename, ".yaml") + } + + if (is.null(name)) { + name <- file_path_sans_ext(filename) + } + if (!is_string(name)) { + cli_abort("Argument {.code name} must be a single character string.") + } + + yaml_filepath <- file.path(x$dir, .assets_directory, subdir, filename) + + dir.create(path = dirname(yaml_filepath), showWarnings = FALSE, recursive = TRUE) + + yaml::write_yaml(list, yaml_filepath) + + objects_desc <- dataset_description( + file = file.path(.assets_directory, subdir, filename), + subdir = subdir, + name = name, + type = "yaml", + timestamp = timestamp + ) + + objects_descriptions <- read_objects_description(x) + objects_descriptions <- rows_upsert(objects_descriptions, objects_desc, by = "name") + save_objects_description(x, objs_desc = objects_descriptions) + + x +} + diff --git a/R/workspace.R b/R/workspace.R index b2ad5f8..0b32e12 100644 --- a/R/workspace.R +++ b/R/workspace.R @@ -3,6 +3,7 @@ ## static names ----- .datasets_description <- "dataset_description" .datasets_directory <- "datasets" +.geospatial_directory <- "geospatial" .assets_directory <- "assets" .version_file <- "version" diff --git a/man/delete_dataset.Rd b/man/delete_dataset.Rd index 681e205..baf2490 100644 --- a/man/delete_dataset.Rd +++ b/man/delete_dataset.Rd @@ -31,6 +31,7 @@ z Other functions to write in a workspace: \code{\link{store_dataset}()}, \code{\link{store_json}()}, -\code{\link{store_rds}()} +\code{\link{store_rds}()}, +\code{\link{store_yaml}()} } \concept{functions to write in a workspace} diff --git a/man/list_object_in_workspace.Rd b/man/list_object_in_workspace.Rd index ff01f15..ac177ab 100644 --- a/man/list_object_in_workspace.Rd +++ b/man/list_object_in_workspace.Rd @@ -26,6 +26,7 @@ Other functions to read in a workspace: \code{\link{read_dataset_in_workspace}()}, \code{\link{read_json_str_in_workspace}()}, \code{\link{read_rds_in_workspace}()}, -\code{\link{read_timestamp}()} +\code{\link{read_timestamp}()}, +\code{\link{read_yaml_in_workspace}()} } \concept{functions to read in a workspace} diff --git a/man/read_dataset_in_workspace.Rd b/man/read_dataset_in_workspace.Rd index 3a66f5a..4efe309 100644 --- a/man/read_dataset_in_workspace.Rd +++ b/man/read_dataset_in_workspace.Rd @@ -28,6 +28,7 @@ Other functions to read in a workspace: \code{\link{list_object_in_workspace}()}, \code{\link{read_json_str_in_workspace}()}, \code{\link{read_rds_in_workspace}()}, -\code{\link{read_timestamp}()} +\code{\link{read_timestamp}()}, +\code{\link{read_yaml_in_workspace}()} } \concept{functions to read in a workspace} diff --git a/man/read_json_str_in_workspace.Rd b/man/read_json_str_in_workspace.Rd index 701b112..070c81e 100644 --- a/man/read_json_str_in_workspace.Rd +++ b/man/read_json_str_in_workspace.Rd @@ -42,6 +42,7 @@ Other functions to read in a workspace: \code{\link{list_object_in_workspace}()}, \code{\link{read_dataset_in_workspace}()}, \code{\link{read_rds_in_workspace}()}, -\code{\link{read_timestamp}()} +\code{\link{read_timestamp}()}, +\code{\link{read_yaml_in_workspace}()} } \concept{functions to read in a workspace} diff --git a/man/read_rds_in_workspace.Rd b/man/read_rds_in_workspace.Rd index 53e4503..3bbf9d6 100644 --- a/man/read_rds_in_workspace.Rd +++ b/man/read_rds_in_workspace.Rd @@ -36,6 +36,7 @@ Other functions to read in a workspace: \code{\link{list_object_in_workspace}()}, \code{\link{read_dataset_in_workspace}()}, \code{\link{read_json_str_in_workspace}()}, -\code{\link{read_timestamp}()} +\code{\link{read_timestamp}()}, +\code{\link{read_yaml_in_workspace}()} } \concept{functions to read in a workspace} diff --git a/man/read_timestamp.Rd b/man/read_timestamp.Rd index e0d9104..f131bb4 100644 --- a/man/read_timestamp.Rd +++ b/man/read_timestamp.Rd @@ -37,6 +37,7 @@ Other functions to read in a workspace: \code{\link{list_object_in_workspace}()}, \code{\link{read_dataset_in_workspace}()}, \code{\link{read_json_str_in_workspace}()}, -\code{\link{read_rds_in_workspace}()} +\code{\link{read_rds_in_workspace}()}, +\code{\link{read_yaml_in_workspace}()} } \concept{functions to read in a workspace} diff --git a/man/read_yaml_in_workspace.Rd b/man/read_yaml_in_workspace.Rd new file mode 100644 index 0000000..67669d6 --- /dev/null +++ b/man/read_yaml_in_workspace.Rd @@ -0,0 +1,53 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/readers.R +\name{read_yaml_in_workspace} +\alias{read_yaml_in_workspace} +\title{Read YAML Object from a Workspace.} +\usage{ +read_yaml_in_workspace(x, name, subdir = NULL) +} +\arguments{ +\item{x}{the workspace} + +\item{name}{name associated with the YAML file stored in the workspace} + +\item{subdir}{Optional subdirectory used for the asset to retrieve} +} +\description{ +Read a YAML file stored as a list object in a workspace. +Returns the YAML content as an R list object. +} +\examples{ +library(workspace) +dir_tmp <- tempfile(pattern = "ws") +z <- new_workspace(dir = dir_tmp) + +config_list <- list( + database = list( + host = "localhost", + port = 5432 + ), + settings = list( + debug = TRUE, + max_connections = 100 + ) +) +z <- store_yaml( + x = z, + list = config_list, + filename = "config.yaml", + name = "app_config", + timestamp = "2023-11-12 11:37:41", + subdir = "configs" +) +read_yaml_in_workspace(z, "app_config", subdir = "configs") +} +\seealso{ +Other functions to read in a workspace: +\code{\link{list_object_in_workspace}()}, +\code{\link{read_dataset_in_workspace}()}, +\code{\link{read_json_str_in_workspace}()}, +\code{\link{read_rds_in_workspace}()}, +\code{\link{read_timestamp}()} +} +\concept{functions to read in a workspace} diff --git a/man/store_dataset.Rd b/man/store_dataset.Rd index 793686f..38528df 100644 --- a/man/store_dataset.Rd +++ b/man/store_dataset.Rd @@ -14,15 +14,21 @@ store_dataset( \arguments{ \item{x}{the workspace object} -\item{dataset}{the data.frame to store in the workspace} +\item{dataset}{the data.frame or sf to store in the workspace.} -\item{name}{name associated with the data.frame} +\item{name}{name associated with the data.frame, if an workspace file with this name exists +already it will be replaced.} \item{timestamp}{A timestamp string to associate with the entry in the workspace.} } \description{ Store a dataset as a parquet file into an existing workspace. } +\details{ +If the input dataset is \code{sf} then a metadata yaml file is also written to the workspace +in the \verb{assets/sf_metadata/\{name\}.yaml} location. This contains the \code{sf} column name +and the CRS that can be overidden when writing .gpkg data to disk. +} \examples{ library(workspace) dir_tmp <- tempfile(pattern = "ws") @@ -35,6 +41,7 @@ z Other functions to write in a workspace: \code{\link{delete_dataset}()}, \code{\link{store_json}()}, -\code{\link{store_rds}()} +\code{\link{store_rds}()}, +\code{\link{store_yaml}()} } \concept{functions to write in a workspace} diff --git a/man/store_json.Rd b/man/store_json.Rd index 9c7760a..56e8396 100644 --- a/man/store_json.Rd +++ b/man/store_json.Rd @@ -20,7 +20,8 @@ store_json( \item{filename}{The name of the file used to store the JSON string in the workspace.} -\item{name}{name associated with the object} +\item{name}{name associated with the object, if an workspace file with this name exists +already it will be replaced.} \item{subdir}{A subdirectory within the asset directory where the JSON file will be stored.} @@ -58,6 +59,7 @@ z Other functions to write in a workspace: \code{\link{delete_dataset}()}, \code{\link{store_dataset}()}, -\code{\link{store_rds}()} +\code{\link{store_rds}()}, +\code{\link{store_yaml}()} } \concept{functions to write in a workspace} diff --git a/man/store_rds.Rd b/man/store_rds.Rd index 901c534..ea694b8 100644 --- a/man/store_rds.Rd +++ b/man/store_rds.Rd @@ -20,7 +20,8 @@ store_rds( \item{filename}{The name of the file used to store the RDS file in the workspace.} -\item{name}{name associated with the object} +\item{name}{name associated with the object, if an workspace file with this name exists +already it will be replaced.} \item{subdir}{A subdirectory within the asset directory where the RDS file will be stored.} @@ -52,6 +53,7 @@ z Other functions to write in a workspace: \code{\link{delete_dataset}()}, \code{\link{store_dataset}()}, -\code{\link{store_json}()} +\code{\link{store_json}()}, +\code{\link{store_yaml}()} } \concept{functions to write in a workspace} diff --git a/man/store_yaml.Rd b/man/store_yaml.Rd new file mode 100644 index 0000000..f5a99d4 --- /dev/null +++ b/man/store_yaml.Rd @@ -0,0 +1,72 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/store.R +\name{store_yaml} +\alias{store_yaml} +\title{Store a list as YAML in a workspace} +\usage{ +store_yaml( + x, + list, + filename, + name = NULL, + subdir, + timestamp = format(Sys.time(), "\%Y-\%m-\%d \%H:\%M:\%S") +) +} +\arguments{ +\item{x}{The workspace object.} + +\item{list}{The R list object to save as YAML in the workspace.} + +\item{filename}{The name of the file used to store the YAML file in the workspace.} + +\item{name}{name associated with the object, if a workspace file with this name exists +already it will be replaced.} + +\item{subdir}{A subdirectory within the asset directory where the YAML file will be stored.} + +\item{timestamp}{A timestamp string to associate with the entry in the workspace.} +} +\value{ +return the workspace object +} +\description{ +Saves a list object as a YAML file in an existing workspace. + +This function allows users to save R list objects as YAML files into a specified workspace. +The file is saved under the provided filename and can be organized within +a specific subdirectory for better management. +} +\examples{ +library(workspace) +dir_tmp <- tempfile(pattern = "ws") +z <- new_workspace(dir = dir_tmp) + +config_list <- list( + database = list( + host = "localhost", + port = 5432, + name = "mydb" + ), + settings = list( + debug = TRUE, + max_connections = 100 + ) +) +z <- store_yaml( + x = z, + list = config_list, + filename = "config.yaml", + timestamp = "2023-11-12 11:37:41", + subdir = "configs" +) +z +} +\seealso{ +Other functions to write in a workspace: +\code{\link{delete_dataset}()}, +\code{\link{store_dataset}()}, +\code{\link{store_json}()}, +\code{\link{store_rds}()} +} +\concept{functions to write in a workspace} diff --git a/tests/testthat.R b/tests/testthat.R new file mode 100644 index 0000000..61cdd4f --- /dev/null +++ b/tests/testthat.R @@ -0,0 +1,12 @@ +# This file is part of the standard setup for testthat. +# It is recommended that you do not modify it. +# +# Where should you do additional test configuration? +# Learn more about the roles of various files in: +# * https://r-pkgs.org/testing-design.html#sec-tests-files-overview +# * https://testthat.r-lib.org/articles/special-files.html + +library(testthat) +library(workspace) + +test_check("workspace") diff --git a/tests/testthat/test-basic-example.R b/tests/testthat/test-basic-example.R new file mode 100644 index 0000000..5a23378 --- /dev/null +++ b/tests/testthat/test-basic-example.R @@ -0,0 +1,64 @@ +test_that("basic workflow is correct", { + + z <- new_workspace() + expect_s3_class(z, "workspace") + expect_true(!is.null(z$dir)) + expect_true(!is.null(z$version)) + + # store datasets + z <- store_dataset(x = z, dataset = iris, name = "iris_dataset") + z <- store_dataset( + x = z, + dataset = tibble::rownames_to_column(mtcars, var = "car"), + name = "mtcars" + ) + + + # store json + json_str <- paste0("{\"first_name\": \"John\",\"last_name\": \"Smith\",\"is_alive\": true,", + "\"age\": 27, \"address\": { \"street_address\": \"21 2nd Street\",", + "\"city\": \"New York\",\"state\": \"NY\",\"postal_code\": \"10021-3100\"", + "}}") + + z <- store_json( + x = z, + name = "json-example", + json_str = json_str, + filename = "json-example.json", + timestamp = "2023-11-12 11:37:41", + subdir = "blah" + ) + + desc <- list_object_in_workspace(z) + expect_identical(desc$file, c( + "datasets/iris_dataset.parquet", + "datasets/mtcars.parquet", + "assets/blah/json-example.json" + )) + expect_identical(desc$name, c( + "iris_dataset", "mtcars", "json-example" + )) + expect_identical(desc$subdir, c( + "datasets", "datasets", "blah" + )) + expect_identical(desc$type, c( + "dataset", "dataset", "json" + )) + + # pack workspace as a zip to share it + workspace_zip_file <- tempfile(fileext = ".zip") + pack_workspace(x = z, file = workspace_zip_file) + + + new_z <- unpack_workspace(workspace_zip_file) + new_desc <- list_object_in_workspace(new_z) + + expect_identical(desc, new_desc) + + dataset <- read_dataset_in_workspace(z, name = "mtcars") + expect_identical(tibble::as_tibble(tibble::rownames_to_column(mtcars, var = "car")), dataset) + + # store json + new_json_str <- read_json_str_in_workspace(z, name = "json-example", subdir = "blah") + expect_identical(json_str, new_json_str) +}) diff --git a/tests/testthat/test-delete-dataset.R b/tests/testthat/test-delete-dataset.R new file mode 100644 index 0000000..9b49b58 --- /dev/null +++ b/tests/testthat/test-delete-dataset.R @@ -0,0 +1,268 @@ +test_that("delete_dataset removes datasets correctly", { + z <- new_workspace() + z <- store_dataset(z, iris, "iris_test") + z <- store_dataset(z, mtcars, "mtcars_test") + + initial_objects <- list_object_in_workspace(z) + expect_equal(nrow(initial_objects), 2) + + z <- delete_dataset(z, "iris_test") + objects <- list_object_in_workspace(z) + + expect_false("iris_test" %in% objects$name) + expect_true("mtcars_test" %in% objects$name) + expect_equal(nrow(objects), 1) +}) + +test_that("delete_dataset removes physical parquet files", { + z <- new_workspace() + z <- store_dataset(z, iris, "iris_to_delete") + + objects <- list_object_in_workspace(z) + file_path <- file.path(z$dir, objects$file[objects$name == "iris_to_delete"]) + expect_true(file.exists(file_path)) + + z <- delete_dataset(z, "iris_to_delete") + expect_false(file.exists(file_path)) +}) + +test_that("delete_dataset handles datasets with special characters in names", { + z <- new_workspace() + special_name <- "dataset with spaces & symbols" + z <- store_dataset(z, iris, special_name) + + z <- delete_dataset(z, special_name) + objects <- list_object_in_workspace(z) + + expect_false(special_name %in% objects$name) +}) + +test_that("delete_dataset handles datasets with .parquet extension in name", { + z <- new_workspace() + z <- store_dataset(z, iris, "test.parquet") + + z <- delete_dataset(z, "test.parquet") + objects <- list_object_in_workspace(z) + + expect_false("test.parquet" %in% objects$name) +}) + +test_that("delete_dataset handles datasets without .parquet extension", { + z <- new_workspace() + z <- store_dataset(z, iris, "test_dataset") + + z <- delete_dataset(z, "test_dataset") + objects <- list_object_in_workspace(z) + + expect_false("test_dataset" %in% objects$name) +}) + +test_that("delete_dataset updates workspace object descriptions", { + z <- new_workspace() + z <- store_dataset(z, iris, "iris_dataset") + z <- store_dataset(z, mtcars, "mtcars_dataset") + + initial_desc <- list_object_in_workspace(z) + expect_equal(nrow(initial_desc), 2) + + z <- delete_dataset(z, "iris_dataset") + updated_desc <- list_object_in_workspace(z) + + expect_equal(nrow(updated_desc), 1) + expect_equal(updated_desc$name, "mtcars_dataset") +}) + +test_that("delete_dataset works after pack/unpack cycle", { + z <- new_workspace() + z <- store_dataset(z, iris, "iris_dataset") + z <- store_dataset(z, mtcars, "mtcars_dataset") + + packed_file <- tempfile(fileext = ".zip") + pack_workspace(z, packed_file) + z2 <- unpack_workspace(packed_file) + + z2 <- delete_dataset(z2, "iris_dataset") + objects <- list_object_in_workspace(z2) + + expect_false("iris_dataset" %in% objects$name) + expect_true("mtcars_dataset" %in% objects$name) +}) + +test_that("delete_dataset handles nonexistent dataset gracefully", { + z <- new_workspace() + z <- store_dataset(z, iris, "iris") + + # Should not error when trying to delete nonexistent dataset + z_after <- delete_dataset(z, "nonexistent_dataset") + objects <- list_object_in_workspace(z_after) + + # Original dataset should still be there + expect_true("iris" %in% objects$name) + expect_equal(nrow(objects), 1) +}) + +test_that("delete_dataset preserves other data types", { + z <- new_workspace() + + # Store different types of objects + z <- store_dataset(z, iris, "iris_dataset") + z <- store_json(z, '{"config": "data"}', "config.json", subdir = "config") + z <- store_rds(z, list(model = "test"), "model.rds", subdir = "models") + + initial_objects <- list_object_in_workspace(z) + expect_equal(nrow(initial_objects), 3) + + # Delete only the dataset + z <- delete_dataset(z, "iris_dataset") + objects <- list_object_in_workspace(z) + + expect_equal(nrow(objects), 2) + expect_false("iris_dataset" %in% objects$name) + expect_true(all(c("config", "model") %in% objects$name)) + + # Verify non-dataset objects are intact + expect_equal(read_json_str_in_workspace(z, "config", subdir = "config"), '{"config": "data"}') + expect_equal(read_rds_in_workspace(z, "model", subdir = "models"), list(model = "test")) +}) + +test_that("delete_dataset handles multiple deletions", { + z <- new_workspace() + z <- store_dataset(z, iris, "dataset1") + z <- store_dataset(z, mtcars, "dataset2") + z <- store_dataset(z, cars, "dataset3") + + expect_equal(nrow(list_object_in_workspace(z)), 3) + + # Delete datasets one by one + z <- delete_dataset(z, "dataset1") + expect_equal(nrow(list_object_in_workspace(z)), 2) + expect_false("dataset1" %in% list_object_in_workspace(z)$name) + + z <- delete_dataset(z, "dataset3") + expect_equal(nrow(list_object_in_workspace(z)), 1) + expect_false("dataset3" %in% list_object_in_workspace(z)$name) + + z <- delete_dataset(z, "dataset2") + expect_equal(nrow(list_object_in_workspace(z)), 0) +}) + +test_that("delete_dataset preserves timestamps of remaining datasets", { + z <- new_workspace() + + timestamp1 <- "2024-01-01 10:00:00" + timestamp2 <- "2024-01-02 15:30:00" + + z <- store_dataset(z, iris, "iris", timestamp = timestamp1) + z <- store_dataset(z, mtcars, "mtcars", timestamp = timestamp2) + + z <- delete_dataset(z, "iris") + + # Check that remaining dataset's timestamp is preserved + remaining_timestamp <- read_timestamp(z, "mtcars", "dataset", subdir = "datasets") + expect_equal(remaining_timestamp, timestamp2) +}) + +test_that("delete_dataset handles unicode characters in names", { + z <- new_workspace() + unicode_name <- "données_été_café" + z <- store_dataset(z, iris, unicode_name) + + z <- delete_dataset(z, unicode_name) + objects <- list_object_in_workspace(z) + + expect_false(unicode_name %in% objects$name) + expect_equal(nrow(objects), 0) +}) + +# test_that("delete_dataset works with very long dataset names", { +# z <- new_workspace() +# long_name <- paste(rep("very_long_dataset_name", 10), collapse = "_") +# z <- store_dataset(z, iris, long_name) +# +# z <- delete_dataset(z, long_name) +# objects <- list_object_in_workspace(z) +# +# expect_false(long_name %in% objects$name) +# expect_equal(nrow(objects), 0) +# }) + +test_that("delete_dataset only affects datasets, not geospatial data", { + skip_if_not_installed("sf") + + library(sf) + z <- new_workspace() + + # Store regular dataset + z <- store_dataset(z, iris, "regular_data") + + # Store geospatial dataset with same name pattern + points <- st_sfc(st_point(c(0, 0)), st_point(c(1, 1)), crs = st_crs(4326)) + sf_data <- st_sf(id = 1:2, value = c("A", "B"), geometry = points) + z <- store_dataset(z, sf_data, "geo_data") + + initial_objects <- list_object_in_workspace(z) + expect_equal(nrow(initial_objects), 2) + + # Delete the regular dataset + z <- delete_dataset(z, "regular_data") + objects <- list_object_in_workspace(z) + + # Should only affect the regular dataset, not geospatial + expect_equal(nrow(objects), 1) + expect_false("regular_data" %in% objects$name) + expect_true("geo_data" %in% objects$name) + expect_equal(objects$type, "geospatial") +}) + +test_that("delete_dataset handles empty workspace", { + z <- new_workspace() + + # Try to delete from empty workspace + z_after <- delete_dataset(z, "nonexistent") + objects <- list_object_in_workspace(z_after) + + expect_equal(nrow(objects), 0) +}) + +test_that("delete_dataset maintains workspace structure integrity", { + z <- new_workspace() + z <- store_dataset(z, iris, "test_dataset") + + # Verify initial structure + expect_true(dir.exists(file.path(z$dir, "datasets"))) + expect_true(file.exists(file.path(z$dir, "dataset_description.parquet"))) + + z <- delete_dataset(z, "test_dataset") + + # Verify structure is maintained even after deletion + expect_true(dir.exists(file.path(z$dir, "datasets"))) + expect_true(file.exists(file.path(z$dir, "dataset_description.parquet"))) + expect_s3_class(z, "workspace") +}) + +test_that("delete_dataset works with datasets containing dots in names", { + z <- new_workspace() + dotted_name <- "my.dataset.with.dots" + z <- store_dataset(z, iris, dotted_name) + + z <- delete_dataset(z, dotted_name) + objects <- list_object_in_workspace(z) + + expect_false(dotted_name %in% objects$name) +}) + +test_that("delete_dataset case sensitivity", { + z <- new_workspace() + z <- store_dataset(z, iris, "MyDataset") + + # Should not delete with different case + z_after <- delete_dataset(z, "mydataset") + objects <- list_object_in_workspace(z_after) + expect_true("MyDataset" %in% objects$name) + + # Should delete with exact case + z_final <- delete_dataset(z_after, "MyDataset") + final_objects <- list_object_in_workspace(z_final) + expect_false("MyDataset" %in% final_objects$name) +}) + diff --git a/tests/testthat/test-delete-operations.R b/tests/testthat/test-delete-operations.R new file mode 100644 index 0000000..7d64dd0 --- /dev/null +++ b/tests/testthat/test-delete-operations.R @@ -0,0 +1,184 @@ +test_that("removes datasets correctly", { + z <- new_workspace() + z <- store_dataset(z, iris, "iris_test") + z <- store_dataset(z, mtcars, "mtcars_test") + + initial_objects <- list_object_in_workspace(z) + expect_equal(nrow(initial_objects), 2) + + z <- rm_object_in_workspace(z, "iris_test", type = "dataset") + objects <- list_object_in_workspace(z) + + expect_false("iris_test" %in% objects$name) + expect_true("mtcars_test" %in% objects$name) + expect_equal(nrow(objects), 1) +}) + +test_that("removes physical files", { + z <- new_workspace() + z <- store_dataset(z, iris, "iris_to_delete") + + objects <- list_object_in_workspace(z) + file_path <- file.path(z$dir, objects$file[objects$name == "iris_to_delete"]) + expect_true(file.exists(file_path)) + + z <- rm_object_in_workspace(z, "iris_to_delete", type = "dataset") + expect_false(file.exists(file_path)) +}) + +test_that("handles datasets with special characters in names", { + z <- new_workspace() + special_name <- "dataset with spaces & symbols" + z <- store_dataset(z, iris, special_name) + + z <- rm_object_in_workspace(z, name = special_name, type = "dataset") + objects <- list_object_in_workspace(z) + + expect_false(special_name %in% objects$name) +}) + +test_that("handles datasets with .parquet extension in name", { + z <- new_workspace() + z <- store_dataset(z, iris, "test.parquet") + + z <- rm_object_in_workspace(z, "test.parquet", type = "dataset") + objects <- list_object_in_workspace(z) + + expect_false("test.parquet" %in% objects$name) +}) + +test_that("updates workspace object descriptions", { + z <- new_workspace() + z <- store_dataset(z, iris, "iris_dataset") + z <- store_dataset(z, mtcars, "mtcars_dataset") + + initial_desc <- list_object_in_workspace(z) + expect_equal(nrow(initial_desc), 2) + + z <- rm_object_in_workspace(z, "iris_dataset", type = "dataset") + updated_desc <- list_object_in_workspace(z) + + expect_equal(nrow(updated_desc), 1) + expect_equal(updated_desc$name, "mtcars_dataset") +}) + +test_that("works after pack/unpack cycle", { + z <- new_workspace() + z <- store_dataset(z, iris, "iris_dataset") + z <- store_dataset(z, mtcars, "mtcars_dataset") + + packed_file <- tempfile(fileext = ".zip") + pack_workspace(z, packed_file) + z2 <- unpack_workspace(packed_file) + + z2 <- rm_object_in_workspace(z2, "iris_dataset", "dataset") + objects <- list_object_in_workspace(z2) + + expect_false("iris_dataset" %in% objects$name) + expect_true("mtcars_dataset" %in% objects$name) +}) + +test_that("removes JSON objects correctly", { + z <- new_workspace() + z <- store_json(z, '{"test": "data1"}', "config1.json", subdir = "config") + z <- store_json(z, '{"test": "data2"}', "config2.json", subdir = "config") + + initial_objects <- list_object_in_workspace(z) + expect_equal(nrow(initial_objects), 2) + + z <- rm_object_in_workspace(z, "config1", type = "json", subdir = "config") + objects <- list_object_in_workspace(z) + + expect_false("config1" %in% objects$name) + expect_true("config2" %in% objects$name) + expect_equal(nrow(objects), 1) +}) + +test_that("removes JSON physical files", { + z <- new_workspace() + z <- store_json(z, '{"test": "data"}', "test.json", subdir = "test") + + objects <- list_object_in_workspace(z) + file_path <- file.path(z$dir, objects$file[objects$name == "test"]) + expect_true(file.exists(file_path)) + + z <- rm_object_in_workspace(z, "test", type = "json", subdir = "test") + expect_false(file.exists(file_path)) +}) + +test_that("handles JSON with special characters", { + z <- new_workspace() + special_name <- "json with spaces & symbols" + z <- store_json(z, '{"special": "data"}', "special.json", name = special_name, subdir = "test") + + z <- rm_object_in_workspace(z, special_name, type = "json", subdir = "test") + objects <- list_object_in_workspace(z) + + expect_false(special_name %in% objects$name) +}) + +test_that("removes RDS objects correctly", { + z <- new_workspace() + z <- store_rds(z, list(data = 1), "obj1.rds", subdir = "objects") + z <- store_rds(z, list(data = 2), "obj2.rds", subdir = "objects") + + initial_objects <- list_object_in_workspace(z) + expect_equal(nrow(initial_objects), 2) + + z <- rm_object_in_workspace(z, "obj1", type = "rds", subdir = "objects") + objects <- list_object_in_workspace(z) + + expect_false("obj1" %in% objects$name) + expect_true("obj2" %in% objects$name) + expect_equal(nrow(objects), 1) +}) + +test_that("removes RDS physical files", { + z <- new_workspace() + z <- store_rds(z, list(test = "data"), "test.rds", subdir = "rdata") + + objects <- list_object_in_workspace(z) + file_path <- file.path(z$dir, objects$file[objects$name == "test"]) + expect_true(file.exists(file_path)) + + z <- rm_object_in_workspace(z, "test", type = "rds", subdir = "rdata") + expect_false(file.exists(file_path)) +}) + +test_that("handles RDS with custom names", { + z <- new_workspace() + z <- store_rds(z, list(value = 42), "data.rds", name = "custom_rds_name", subdir = "rdata") + z <- rm_object_in_workspace(z, "custom_rds_name", type = "rds", subdir = "rdata") + objects <- list_object_in_workspace(z) + + expect_false("custom_rds_name" %in% objects$name) +}) + +test_that("handles mixed object types", { + z <- new_workspace() + z <- store_dataset(z, iris, "iris") + z <- store_json(z, '{"config": "data"}', "config.json", subdir = "config") + z <- store_rds(z, list(model = "test"), "model.rds", subdir = "models") + + initial_objects <- list_object_in_workspace(z) + expect_equal(nrow(initial_objects), 3) + + # Remove JSON object + z <- rm_object_in_workspace(z, "config", type = "json", subdir = "config") + objects <- list_object_in_workspace(z) + expect_equal(nrow(objects), 2) + expect_false("config" %in% objects$name) + + # Remove RDS object + z <- rm_object_in_workspace(z, "model", type = "rds", subdir = "models") + objects <- list_object_in_workspace(z) + expect_equal(nrow(objects), 1) + expect_false("model" %in% objects$name) + + # Dataset should remain + expect_true("iris" %in% objects$name) + + # Verify remaining data is intact + iris_data <- read_dataset_in_workspace(z, "iris") + expect_equal(nrow(iris_data), nrow(iris)) +}) diff --git a/tests/testthat/test-edge-cases.R b/tests/testthat/test-edge-cases.R new file mode 100644 index 0000000..bc05c7c --- /dev/null +++ b/tests/testthat/test-edge-cases.R @@ -0,0 +1,119 @@ +test_that("handles special characters in names and paths", { + z <- new_workspace() + special_name <- "dataset with spaces & symbols!@#" + z <- store_dataset(z, iris, special_name) + + retrieved <- read_dataset_in_workspace(z, special_name) + expect_identical(nrow(retrieved), nrow(iris)) + expect_identical(ncol(retrieved), ncol(iris)) +}) + +test_that("handles unicode characters in names", { + z <- new_workspace() + unicode_name <- "données_été_café" + z <- store_dataset(z, iris, unicode_name) + + objects <- list_object_in_workspace(z) + expect_true(unicode_name %in% objects$name) + + retrieved <- read_dataset_in_workspace(z, unicode_name) + expect_identical(nrow(retrieved), nrow(iris)) +}) + +test_that("handles empty workspaces", { + z <- new_workspace() + objects <- list_object_in_workspace(z) + expect_equal(nrow(objects), 0) + expect_true(is.data.frame(objects)) + expect_true(all(c("file", "name", "subdir", "type", "timestamp") %in% names(objects))) +}) + +test_that("empty workspace pack/unpack cycle works", { + z <- new_workspace() + + packed <- tempfile(fileext = ".zip") + pack_workspace(z, packed) + + z2 <- unpack_workspace(packed) + objects <- list_object_in_workspace(z2) + + expect_equal(nrow(objects), 0) + expect_s3_class(z2, "workspace") +}) + +test_that("handles very long object names", { + z <- new_workspace() + long_name <- paste(rep("very_long_name", 10), collapse = "_") + z <- store_dataset(z, iris, long_name) + + objects <- list_object_in_workspace(z) + expect_true(long_name %in% objects$name) + + retrieved <- read_dataset_in_workspace(z, long_name) + expect_identical(nrow(retrieved), nrow(iris)) +}) + +test_that("handles datasets with zero rows", { + z <- new_workspace() + empty_df <- data.frame(x = numeric(0), y = character(0)) + z <- store_dataset(z, empty_df, "empty_dataset") + + retrieved <- read_dataset_in_workspace(z, "empty_dataset") + expect_equal(nrow(retrieved), 0) + expect_equal(ncol(retrieved), 2) +}) + +test_that("handles datasets with many columns", { + z <- new_workspace() + wide_df <- data.frame(matrix(runif(100), nrow = 10, ncol = 10)) + names(wide_df) <- paste0("col_", 1:10) + z <- store_dataset(z, wide_df, "wide_dataset") + + retrieved <- read_dataset_in_workspace(z, "wide_dataset") + expect_equal(ncol(retrieved), 10) + expect_equal(nrow(retrieved), 10) +}) + +test_that("handles JSON with special characters", { + z <- new_workspace() + special_json <- '{"special": "chars: àáâãäå, 中文, 🎉"}' + z <- store_json(z, special_json, "special.json", subdir = "test") + + retrieved <- read_json_str_in_workspace(z, "special", subdir = "test") + expect_equal(retrieved, special_json) +}) + +test_that("handles empty JSON strings", { + z <- new_workspace() + empty_json <- "{}" + z <- store_json(z, empty_json, "empty.json", subdir = "test") + + retrieved <- read_json_str_in_workspace(z, "empty", subdir = "test") + expect_equal(retrieved, empty_json) +}) + +test_that("handles deeply nested subdirectories", { + z <- new_workspace() + deep_subdir <- "level1/level2/level3/level4" + z <- store_json(z, '{"deep": "nested"}', "deep.json", subdir = deep_subdir) + + objects <- list_object_in_workspace(z) + expect_true(deep_subdir %in% objects$subdir) + + retrieved <- read_json_str_in_workspace(z, "deep", subdir = deep_subdir) + expect_equal(retrieved, '{"deep": "nested"}') +}) + +test_that("handles mixed data types in same workspace after operations", { + z <- new_workspace() + + z <- store_dataset(z, iris, "iris") + z <- store_json(z, '{"test": "data"}', "config.json", subdir = "config") + z <- store_rds(z, list(x = 1:5), "data.rds", subdir = "rdata") + + z <- delete_dataset(z, "iris") + + objects <- list_object_in_workspace(z) + expect_equal(nrow(objects), 2) + expect_true(all(c("config", "data") %in% objects$name)) +}) diff --git a/tests/testthat/test-error-handling.R b/tests/testthat/test-error-handling.R new file mode 100644 index 0000000..c85ae20 --- /dev/null +++ b/tests/testthat/test-error-handling.R @@ -0,0 +1,78 @@ +test_that("store_json handles invalid inputs gracefully", { + z <- new_workspace() + + expect_error(store_json(z, 123, "test.json", subdir = "test")) + expect_error(store_json(z, c("a", "b"), "test.json", subdir = "test")) + expect_error(store_json(z, '{"valid": "json"}', 123, subdir = "test")) + expect_error(store_json(z, '{"valid": "json"}', "test.json", subdir = 123)) +}) + +test_that("store_rds handles invalid inputs gracefully", { + z <- new_workspace() + expect_error(store_rds(z, list(data = "test"), 123, subdir = "test")) + expect_error(store_rds(z, list(data = "test"), "test.rds", subdir = 123)) + expect_error(store_rds(z, list(data = "test"), "test.rds", subdir = "test", timestamp = 123)) +}) + +test_that("read functions handle nonexistent objects gracefully", { + z <- new_workspace() + + expect_error(read_dataset_in_workspace(z, "nonexistent")) + expect_error(read_json_str_in_workspace(z, "nonexistent", subdir = "test")) + expect_error(read_rds_in_workspace(z, "nonexistent", subdir = "test")) + expect_error(read_timestamp(z, "nonexistent", "dataset", subdir = "datasets")) +}) + +test_that("new_workspace handles existing directory", { + existing_dir <- tempdir() + expect_error(new_workspace(existing_dir)) +}) + +test_that("delete_dataset handles nonexistent dataset", { + z <- new_workspace() + z <- store_dataset(z, iris, "iris") + z_after <- delete_dataset(z, "nonexistent_dataset") + objects <- list_object_in_workspace(z_after) + expect_true("iris" %in% objects$name) +}) + +test_that("duplicate names replace old name", { + z <- new_workspace() + z <- store_json(z, '{"test1": "data"}', "test.json", name = "duplicate", subdir = "dir1") + z <- store_json(z, '{"test2": "data"}', "test.json", name = "duplicate", subdir = "dir2") + yy <- list_object_in_workspace(z) + expect_equal(nrow(yy), 1L) + expect_no_error(read_json_str_in_workspace(z, "duplicate")) +}) + +test_that("rm_object_in_workspace handles various error conditions", { + z <- new_workspace() + expect_error(rm_object_in_workspace(z, "nonexistent", "dataset")) + expect_error(rm_object_in_workspace(z, "test", "nonexistent_type")) + z <- store_dataset(z, iris, "iris") + expect_error(rm_object_in_workspace(z, "iris", "dataset", subdir = "nonexistent")) +}) + +test_that("unpack_workspace handles invalid files", { + invalid_file <- tempfile(fileext = ".zip") + writeLines("not a valid zip", invalid_file) + expect_error(unpack_workspace(invalid_file)) + nonexistent_file <- tempfile(fileext = ".zip") + expect_warning( + expect_error( + unpack_workspace(nonexistent_file) + )) +}) + +test_that("pack functions handle invalid paths", { + z <- new_workspace() + invalid_dir <- "/nonexistent/path" + expect_error(pack_workspace(z, file.path(invalid_dir, "test.zip"))) +}) + +test_that("functions validate string inputs correctly", { + z <- new_workspace() + expect_error(store_json(z, '{"valid": "json"}', NULL, subdir = "test")) + expect_error(store_json(z, '{"valid": "json"}', "test.json", subdir = NULL)) + expect_error(store_rds(z, list(data = "test"), NULL, subdir = "test")) +}) diff --git a/tests/testthat/test-integration.R b/tests/testthat/test-integration.R new file mode 100644 index 0000000..4e2b317 --- /dev/null +++ b/tests/testthat/test-integration.R @@ -0,0 +1,151 @@ +test_that("complete workflow with multiple data types", { + z <- new_workspace() + + # Add datasets, JSON, and RDS + z <- store_dataset(z, iris, "iris") + z <- store_dataset(z, mtcars, "mtcars") + z <- store_json(z, '{"config": "test_data", "version": 1}', "config.json", subdir = "config") + z <- store_rds(z, list(x = 1:10, y = letters[1:5]), "data.rds", subdir = "rdata") + + # Verify initial state + objects <- list_object_in_workspace(z) + expect_equal(nrow(objects), 4) + + # Pack and unpack + packed <- tempfile(fileext = ".zip") + pack_workspace(z, packed) + z2 <- unpack_workspace(packed) + + # Verify all data types preserved + expect_identical(read_dataset_in_workspace(z2, "iris"), tibble::as_tibble(iris)) + expect_identical(read_dataset_in_workspace(z2, "mtcars"), tibble::as_tibble(mtcars)) + expect_identical(read_json_str_in_workspace(z2, "config", subdir = "config"), '{"config": "test_data", "version": 1}') + expect_identical(read_rds_in_workspace(z2, "data", subdir = "rdata"), list(x = 1:10, y = letters[1:5])) +}) + +test_that("complex workspace merge scenario", { + # Create workspace 1 with datasets and JSON + z1 <- new_workspace() + z1 <- store_dataset(z1, iris, "iris") + z1 <- store_json(z1, '{"source": "workspace1"}', "metadata.json", subdir = "meta") + + # Create workspace 2 with different datasets and RDS + z2 <- new_workspace() + z2 <- store_dataset(z2, mtcars, "mtcars") + z2 <- store_rds(z2, list(models = c("lm", "glm")), "models.rds", subdir = "analysis") + + # Create workspace 3 with overlapping and new content + z3 <- new_workspace() + z3 <- store_dataset(z3, iris, "iris") # Same name as z1 + z3 <- store_json(z3, '{"source": "workspace3"}', "metadata.json", subdir = "meta") # Same name as z1 + z3 <- store_dataset(z3, cars, "cars") + + # Merge operations + merged_z1_z2 <- workspace_bind(z1, z2) + final_workspace <- workspace_bind(merged_z1_z2, z3, replace = FALSE) + + objects <- list_object_in_workspace(final_workspace) + expect_equal(nrow(objects), 5) # iris (from z1), metadata (from z1), mtcars, models, cars + + # Verify z1 content preserved (replace = FALSE) + iris_data <- read_dataset_in_workspace(final_workspace, "iris") + expect_identical(iris_data, tibble::as_tibble(iris)) + + metadata_json <- read_json_str_in_workspace(final_workspace, "metadata", subdir = "meta") + expect_identical(metadata_json, '{"source": "workspace1"}') +}) + +test_that("full CRUD operations workflow", { + z <- new_workspace() + + # CREATE + z <- store_dataset(z, iris, "iris") + z <- store_dataset(z, mtcars, "mtcars") + z <- store_json(z, '{"initial": "data"}', "config.json", subdir = "config") + z <- store_rds(z, list(version = 1), "metadata.rds", subdir = "meta") + + expect_equal(nrow(list_object_in_workspace(z)), 4) + + # READ + iris_data <- read_dataset_in_workspace(z, "iris") + config_data <- read_json_str_in_workspace(z, "config", subdir = "config") + meta_data <- read_rds_in_workspace(z, "metadata", subdir = "meta") + + expect_identical(iris_data, tibble::as_tibble(iris)) + expect_identical(config_data, '{"initial": "data"}') + expect_identical(meta_data, list(version = 1)) + + # UPDATE (replace existing) + z <- store_json(z, '{"updated": "data"}', "config.json", subdir = "config") + updated_config <- read_json_str_in_workspace(z, "config", subdir = "config") + expect_identical(updated_config, '{"updated": "data"}') + + # DELETE + z <- delete_dataset(z, "mtcars") + z <- rm_object_in_workspace(z, "metadata", "rds", subdir = "meta") + + objects <- list_object_in_workspace(z) + expect_equal(nrow(objects), 2) + expect_true(all(c("iris", "config") %in% objects$name)) +}) + +test_that("workspace persistence across multiple operations", { + z <- new_workspace() + + # Build workspace incrementally + z <- store_dataset(z, iris, "iris") + + packed1 <- tempfile(fileext = ".zip") + pack_workspace(z, packed1) + z2 <- unpack_workspace(packed1) + + z2 <- store_dataset(z2, mtcars, "mtcars") + z2 <- store_json(z2, '{"step": 2}', "progress.json", subdir = "tracking") + + packed2 <- tempfile(fileext = ".zip") + pack_workspace(z2, packed2) + z3 <- unpack_workspace(packed2) + + z3 <- store_rds(z3, list(final = TRUE), "final.rds", subdir = "results") + z3 <- delete_dataset(z3, "iris") + + # Final verification + final_objects <- list_object_in_workspace(z3) + expect_equal(nrow(final_objects), 3) + expect_true(all(c("mtcars", "progress", "final") %in% final_objects$name)) + + # Verify data integrity + expect_identical(read_dataset_in_workspace(z3, "mtcars"), tibble::as_tibble(mtcars)) + expect_identical(read_json_str_in_workspace(z3, "progress", subdir = "tracking"), '{"step": 2}') + expect_identical(read_rds_in_workspace(z3, "final", subdir = "results"), list(final = TRUE)) +}) + +test_that("timestamp consistency across operations", { + z <- new_workspace() + + custom_time1 <- "2024-01-01 10:00:00" + custom_time2 <- "2024-01-02 15:30:00" + + z <- store_dataset(z, iris, "iris", timestamp = custom_time1) + z <- store_json(z, '{"test": "data"}', "config.json", subdir = "config", timestamp = custom_time2) + + # Pack and unpack + packed <- tempfile(fileext = ".zip") + pack_workspace(z, packed) + z2 <- unpack_workspace(packed) + + # Verify timestamps preserved + iris_time <- read_timestamp(z2, "iris", "dataset", subdir = "datasets") + config_time <- read_timestamp(z2, "config", "json", subdir = "config") + + expect_equal(iris_time, custom_time1) + expect_equal(config_time, custom_time2) + + # Copy workspace and verify timestamps + z3 <- workspace_copy(z2) + iris_time_copy <- read_timestamp(z3, "iris", "dataset", subdir = "datasets") + config_time_copy <- read_timestamp(z3, "config", "json", subdir = "config") + + expect_equal(iris_time_copy, custom_time1) + expect_equal(config_time_copy, custom_time2) +}) diff --git a/tests/testthat/test-performance.R b/tests/testthat/test-performance.R new file mode 100644 index 0000000..bcc726c --- /dev/null +++ b/tests/testthat/test-performance.R @@ -0,0 +1,177 @@ +test_that("handles moderately large datasets efficiently", { + z <- new_workspace() + + # Create a moderately sized dataset (1000 rows, 20 columns) + large_df <- data.frame( + matrix(runif(20000), nrow = 1000, ncol = 20) + ) + names(large_df) <- paste0("var_", 1:20) + + # Test storage + start_time <- Sys.time() + z <- store_dataset(z, large_df, "large_dataset") + storage_time <- difftime(Sys.time(), start_time, units = "secs") + + # Should complete in reasonable time (less than 5 seconds) + expect_lt(as.numeric(storage_time), 5) + + # Test retrieval + start_time <- Sys.time() + retrieved <- read_dataset_in_workspace(z, "large_dataset") + retrieval_time <- difftime(Sys.time(), start_time, units = "secs") + + expect_lt(as.numeric(retrieval_time), 5) + expect_equal(nrow(retrieved), 1000) + expect_equal(ncol(retrieved), 20) +}) + +test_that("handles multiple datasets efficiently", { + z <- new_workspace() + + # Store multiple datasets + datasets <- list( + iris = iris, + mtcars = mtcars, + cars = cars, + airquality = airquality, + chickwts = ChickWeight + ) + + start_time <- Sys.time() + for (name in names(datasets)) { + z <- store_dataset(z, datasets[[name]], name) + } + storage_time <- difftime(Sys.time(), start_time, units = "secs") + + expect_lt(as.numeric(storage_time), 10) + + # Test pack/unpack performance + packed_file <- tempfile(fileext = ".zip") + + start_time <- Sys.time() + pack_workspace(z, packed_file) + pack_time <- difftime(Sys.time(), start_time, units = "secs") + + start_time <- Sys.time() + z2 <- unpack_workspace(packed_file) + unpack_time <- difftime(Sys.time(), start_time, units = "secs") + + expect_lt(as.numeric(pack_time), 5) + expect_lt(as.numeric(unpack_time), 5) + + objects <- list_object_in_workspace(z2) + expect_equal(nrow(objects), 5) +}) + +test_that("workspace copy performance is reasonable", { + z <- new_workspace() + + # Create workspace with multiple objects + z <- store_dataset(z, iris, "iris") + z <- store_dataset(z, mtcars, "mtcars") + z <- store_json(z, '{"large": "json with some content that is moderately sized"}', "config.json", subdir = "config") + z <- store_rds(z, list(data = 1:1000), "data.rds", subdir = "rdata") + + start_time <- Sys.time() + z2 <- workspace_copy(z) + copy_time <- difftime(Sys.time(), start_time, units = "secs") + + expect_lt(as.numeric(copy_time), 10) + + # Verify copy is complete + objects1 <- list_object_in_workspace(z) + objects2 <- list_object_in_workspace(z2) + expect_identical(objects1, objects2) +}) + +test_that("workspace bind performance with multiple workspaces", { + # Create multiple workspaces + z1 <- new_workspace() + z1 <- store_dataset(z1, iris, "iris") + z1 <- store_json(z1, '{"workspace": 1}', "meta.json", subdir = "meta") + + z2 <- new_workspace() + z2 <- store_dataset(z2, mtcars, "mtcars") + z2 <- store_rds(z2, list(workspace = 2), "meta.rds", subdir = "meta") + + z3 <- new_workspace() + z3 <- store_dataset(z3, cars, "cars") + z3 <- store_json(z3, '{"workspace": 3}', "config.json", subdir = "config") + + start_time <- Sys.time() + temp_workspace <- workspace_bind(z1, z2) + final_workspace <- workspace_bind(temp_workspace, z3) + bind_time <- difftime(Sys.time(), start_time, units = "secs") + + expect_lt(as.numeric(bind_time), 15) + + objects <- list_object_in_workspace(final_workspace) + expect_equal(nrow(objects), 5) +}) + +test_that("file system operations are efficient", { + z <- new_workspace() + + # Test multiple file operations + start_time <- Sys.time() + for (i in 1:10) { + z <- store_json(z, paste0('{"iteration": ', i, '}'), paste0("file_", i, ".json"), subdir = "batch") + } + multi_file_time <- difftime(Sys.time(), start_time, units = "secs") + + expect_lt(as.numeric(multi_file_time), 5) + + objects <- list_object_in_workspace(z) + expect_equal(nrow(objects), 10) + + # Test deletion performance + start_time <- Sys.time() + for (i in 1:5) { + z <- rm_object_in_workspace(z, paste0("file_", i), "json", subdir = "batch") + } + deletion_time <- difftime(Sys.time(), start_time, units = "secs") + + expect_lt(as.numeric(deletion_time), 5) + + remaining_objects <- list_object_in_workspace(z) + expect_equal(nrow(remaining_objects), 5) +}) + +# test_that("memory usage is reasonable for typical workloads", { +# z <- new_workspace() +# +# # Monitor memory before operations +# gc() # Force garbage collection +# +# # Store various data types +# z <- store_dataset(z, iris, "iris") +# z <- store_dataset(z, mtcars, "mtcars") +# +# # Create some JSON data +# json_data <- jsonlite::toJSON(list( +# numbers = 1:100, +# text = rep("sample text", 50), +# nested = list(a = 1:20, b = letters) +# ), auto_unbox = TRUE) +# z <- store_json(z, json_data, "complex.json", subdir = "data") +# +# # Store RDS object +# complex_obj <- list( +# matrices = lapply(1:5, function(i) matrix(runif(100), 10, 10)), +# vectors = lapply(1:10, function(i) runif(50)), +# metadata = list(created = Sys.time(), version = "1.0") +# ) +# z <- store_rds(z, complex_obj, "complex.rds", subdir = "objects") +# +# # Pack and unpack +# packed_file <- tempfile(fileext = ".zip") +# pack_workspace(z, packed_file) +# z2 <- unpack_workspace(packed_file) +# +# # Verify all operations completed without error +# objects <- list_object_in_workspace(z2) +# expect_equal(nrow(objects), 3) +# +# # Force garbage collection to clean up +# gc() +# }) diff --git a/tests/testthat/test-rds-operations.R b/tests/testthat/test-rds-operations.R new file mode 100644 index 0000000..82cffd3 --- /dev/null +++ b/tests/testthat/test-rds-operations.R @@ -0,0 +1,61 @@ +test_that("RDS storage and retrieval works correctly", { + z <- new_workspace() + test_obj <- list(data = mtcars, meta = "test metadata", numbers = 1:10) + + z <- store_rds(z, test_obj, "test_obj.rds", subdir = "objects") + retrieved <- read_rds_in_workspace(z, "test_obj", subdir = "objects") + + expect_identical(test_obj, retrieved) +}) + +test_that("RDS storage handles different R object types", { + z <- new_workspace() + + # Test different object types + test_list <- list(a = 1, b = "text", c = TRUE) + test_vector <- c(1, 2, 3, 4, 5) + test_matrix <- matrix(1:12, nrow = 3) + test_df <- data.frame(x = 1:3, y = letters[1:3]) + + z <- store_rds(z, test_list, "list.rds", subdir = "test") + z <- store_rds(z, test_vector, "vector.rds", subdir = "test") + z <- store_rds(z, test_matrix, "matrix.rds", subdir = "test") + z <- store_rds(z, test_df, "dataframe.rds", subdir = "test") + + expect_identical(read_rds_in_workspace(z, "list", subdir = "test"), test_list) + expect_identical(read_rds_in_workspace(z, "vector", subdir = "test"), test_vector) + expect_identical(read_rds_in_workspace(z, "matrix", subdir = "test"), test_matrix) + expect_identical(read_rds_in_workspace(z, "dataframe", subdir = "test"), test_df) +}) + +test_that("RDS storage automatically adds .rds extension", { + z <- new_workspace() + test_data <- list(value = 42) + + z <- store_rds(z, test_data, "no_extension", subdir = "test") + + objects <- list_object_in_workspace(z) + expect_true(grepl("\\.rds$", objects$file[objects$name == "no_extension"])) +}) + +test_that("RDS timestamp is recorded correctly", { + z <- new_workspace() + test_obj <- list(data = "test") + custom_timestamp <- "2024-01-15 10:30:00" + + z <- store_rds(z, test_obj, "timestamped.rds", subdir = "test", timestamp = custom_timestamp) + + retrieved_timestamp <- read_timestamp(z, "timestamped", "rds", subdir = "test") + expect_equal(retrieved_timestamp, custom_timestamp) +}) + +test_that("RDS name parameter works correctly", { + z <- new_workspace() + test_obj <- list(data = "test") + + z <- store_rds(z, test_obj, "file.rds", name = "custom_name", subdir = "test") + + objects <- list_object_in_workspace(z) + expect_true("custom_name" %in% objects$name) + expect_false("file" %in% objects$name) +}) diff --git a/tests/testthat/test-sf-operations.R b/tests/testthat/test-sf-operations.R new file mode 100644 index 0000000..8c49b14 --- /dev/null +++ b/tests/testthat/test-sf-operations.R @@ -0,0 +1,481 @@ +test_that("store_dataset.sf works with basic sf objects", { + skip_if_not_installed("sf") + + library(sf) + z <- new_workspace() + + # Create a simple sf object with points + points <- st_sfc(st_point(c(0, 0)), st_point(c(1, 1)), st_point(c(2, 2)), + crs = st_crs(4263)) + sf_data <- st_sf(id = 1:3, value = c("A", "B", "C"), geometry = points) + + z <- store_dataset(z, sf_data, "test_points") + + objects <- list_object_in_workspace(z) + expect_equal(nrow(objects), 1) + expect_equal(objects$name, "test_points") + expect_equal(objects$type, "geospatial") + expect_true(grepl("\\.gpkg$", objects$file)) +}) + +test_that("store_dataset.sf handles different file extensions", { + skip_if_not_installed("sf") + + library(sf) + z <- new_workspace() + + # Create test sf object + points <- st_sfc(st_point(c(0, 0)), st_point(c(1, 1)), crs = st_crs(4263)) + sf_data <- st_sf(id = 1:2, geometry = points) + + # Test with .gpkg extension + z <- store_dataset(z, sf_data, "test.gpkg") + expect_warning(z <- store_dataset(z, sf_data, "test.shp")) + expect_warning(z <- store_dataset(z, sf_data, "test.geojson")) + z <- store_dataset(z, sf_data, "test_no_ext") + + objects <- list_object_in_workspace(z) + expect_equal(nrow(objects), 4) + + # Check that extensions are preserved/added correctly + expect_true(any(grepl("test\\.gpkg$", objects$file))) + expect_true(any(grepl("test\\.gpkg$", objects$file))) + expect_true(any(grepl("test\\.gpkg$", objects$file))) + expect_true(any(grepl("test_no_ext\\.gpkg$", objects$file))) +}) + +test_that("store_dataset.sf creates geospatial_dataset type", { + skip_if_not_installed("sf") + + library(sf) + z <- new_workspace() + + # Create sf object + polygon <- st_polygon(list(rbind(c(0,0), c(1,0), c(1,1), c(0,1), c(0,0))), ) + sf_data <- st_sf(id = 1, name = "test_polygon", geometry = st_sfc(polygon), crs = st_crs(4263)) + + z <- store_dataset(z, sf_data, "polygon_data") + + objects <- list_object_in_workspace(z) + expect_equal(objects$type, "geospatial") + expect_true(objects$subdir == "geospatial") +}) + +test_that("store_dataset.sf works with different geometry types", { + skip_if_not_installed("sf") + + library(sf) + z <- new_workspace() + + # Test with POINT geometry + points <- st_sfc(st_point(c(0, 0)), st_point(c(1, 1))) + point_sf <- st_sf(id = 1:2, type = "point", geometry = points, crs = st_crs(4263)) + z <- store_dataset(z, point_sf, "points") + + # Test with LINESTRING geometry + line <- st_linestring(rbind(c(0,0), c(1,1), c(2,2))) + line_sf <- st_sf(id = 1, type = "line", geometry = st_sfc(line), crs = st_crs(4263)) + z <- store_dataset(z, line_sf, "lines") + + # Test with POLYGON geometry + polygon <- st_polygon(list(rbind(c(0,0), c(1,0), c(1,1), c(0,1), c(0,0)))) + polygon_sf <- st_sf(id = 1, type = "polygon", geometry = st_sfc(polygon), crs = st_crs(4263)) + z <- store_dataset(z, polygon_sf, "polygons") + + objects <- list_object_in_workspace(z) + expect_equal(nrow(objects), 3) + expect_true(all(objects$type == "geospatial")) + expect_true(all(c("points", "lines", "polygons") %in% objects$name)) +}) + +test_that("store_dataset.sf preserves CRS information", { + skip_if_not_installed("sf") + + library(sf) + z <- new_workspace() + + # Create sf object with specific CRS + points <- st_sfc(st_point(c(500000, 4000000)), st_point(c(600000, 4100000)), + crs = "EPSG:32633") # UTM Zone 33N + sf_data <- st_sf(id = 1:2, value = c(10, 20), geometry = points) + + z <- store_dataset(z, sf_data, "utm_points") + + objects <- list_object_in_workspace(z) + expect_equal(nrow(objects), 1) + expect_equal(objects$type, "geospatial") + + # Verify file was created + file_path <- file.path(z$dir, objects$file) + expect_true(file.exists(file_path)) +}) + +test_that("store_dataset.sf handles sf objects with attributes", { + skip_if_not_installed("sf") + + library(sf) + z <- new_workspace() + + # Create sf object with multiple attribute columns + points <- st_sfc(st_point(c(0, 0)), st_point(c(1, 1)), st_point(c(2, 2))) + sf_data <- st_sf( + id = 1:3, + name = c("Location A", "Location B", "Location C"), + value = c(10.5, 20.3, 15.7), + category = factor(c("type1", "type2", "type1")), + geometry = points, + crs = st_crs(4263) + ) + + z <- store_dataset(z, sf_data, "attributed_points") + + objects <- list_object_in_workspace(z) + expect_equal(nrow(objects), 1) + expect_equal(objects$name, "attributed_points") + expect_equal(objects$type, "geospatial") +}) + +test_that("store_dataset.sf replaces existing geospatial datasets", { + skip_if_not_installed("sf") + + library(sf) + z <- new_workspace() + + # Create first sf object + points1 <- st_sfc(st_point(c(0, 0)), st_point(c(1, 1))) + sf_data1 <- st_sf(id = 1:2, value = c("old1", "old2"), geometry = points1, crs = st_crs(4263)) + z <- store_dataset(z, sf_data1, "replaceable") + + # Create second sf object with same name + points2 <- st_sfc(st_point(c(5, 5)), st_point(c(6, 6)), st_point(c(7, 7))) + sf_data2 <- st_sf(id = 1:3, value = c("new1", "new2", "new3"), geometry = points2, crs = st_crs(4263)) + z <- store_dataset(z, sf_data2, "replaceable") + + objects <- list_object_in_workspace(z) + expect_equal(nrow(objects), 1) + expect_equal(objects$name, "replaceable") +}) + +test_that("store_dataset.sf works with custom timestamps", { + skip_if_not_installed("sf") + + library(sf) + z <- new_workspace() + + points <- st_sfc(st_point(c(0, 0))) + sf_data <- st_sf(id = 1, geometry = points, crs = st_crs(4263)) + custom_timestamp <- "2024-06-15 14:30:00" + + z <- store_dataset(z, sf_data, "timestamped_sf", timestamp = custom_timestamp) + + retrieved_timestamp <- read_timestamp(z, "timestamped_sf", type = "geospatial", subdir = "geospatial") + expect_equal(retrieved_timestamp, custom_timestamp) +}) + +test_that("store_dataset.sf pack/unpack cycle preserves geospatial data", { + skip_if_not_installed("sf") + + library(sf) + z <- new_workspace() + + # Create and store sf object + points <- st_sfc(st_point(c(10, 20)), st_point(c(30, 40))) + sf_data <- st_sf(id = 1:2, name = c("A", "B"), geometry = points, crs = st_crs(4263)) + z <- store_dataset(z, sf_data, "test_sf") + + # Pack and unpack + packed_file <- tempfile(fileext = ".zip") + pack_workspace(z, packed_file) + z2 <- unpack_workspace(packed_file) + + objects <- list_object_in_workspace(z2) + expect_equal(nrow(objects), 1) + expect_equal(objects$name, "test_sf") + expect_equal(objects$type, "geospatial") + + # Verify file exists after unpacking + file_path <- file.path(z2$dir, objects$file) + expect_true(file.exists(file_path)) +}) + + +test_that("store_dataset.sf handles empty sf objects", { + skip_if_not_installed("sf") + + library(sf) + z <- new_workspace() + + # Create empty sf object + empty_sf <- st_sf( + id = integer(0), + name = character(0), + geometry = st_sfc(crs = 4326) + ) + + z <- store_dataset(z, empty_sf, "empty_sf") + + read_dataset_in_workspace(z, "empty_sf") + + objects <- list_object_in_workspace(z) + expect_equal(nrow(objects), 1) + expect_equal(objects$name, "empty_sf") + expect_equal(objects$type, "geospatial") +}) + +# Tests for reading geospatial data +test_that("read_dataset_in_workspace retrieves sf objects correctly", { + skip_if_not_installed("sf") + + library(sf) + z <- new_workspace() + + # Create and store sf object + original_points <- st_sfc(st_point(c(0, 0)), st_point(c(1, 1)), st_point(c(2, 2)), crs = st_crs(4326)) + original_sf <- st_sf(id = 1:3, value = c("A", "B", "C"), geometry = original_points) + + z <- store_dataset(z, original_sf, "test_points") + + # Read back the sf object + retrieved_sf <- read_dataset_in_workspace(z, "test_points") + + # Check that it's an sf object + expect_s3_class(retrieved_sf, "sf") + expect_true("geometry" %in% names(retrieved_sf)) + + # Check data integrity + expect_equal(nrow(retrieved_sf), 3) + expect_equal(retrieved_sf$id, 1:3) + expect_equal(retrieved_sf$value, c("A", "B", "C")) +}) + +test_that("read_dataset_in_workspace preserves CRS information", { + skip_if_not_installed("sf") + + library(sf) + z <- new_workspace() + + # Create sf object with specific CRS + utm_points <- st_sfc(st_point(c(500000, 4000000)), st_point(c(600000, 4100000)), + crs = "EPSG:32633") # UTM Zone 33N + original_sf <- st_sf(id = 1:2, value = c(10, 20), geometry = utm_points) + + z <- store_dataset(z, original_sf, "utm_data") + retrieved_sf <- read_dataset_in_workspace(z, "utm_data") + + # Check CRS preservation + expect_equal(st_crs(retrieved_sf), st_crs("EPSG:32633")) + expect_equal(st_crs(retrieved_sf)$input, "EPSG:32633") +}) + +test_that("read_dataset_in_workspace handles different geometry types", { + skip_if_not_installed("sf") + + library(sf) + z <- new_workspace() + + # Store different geometry types + points <- st_sfc(st_point(c(0, 0)), st_point(c(1, 1)), crs = st_crs(4326)) + point_sf <- st_sf(id = 1:2, type = "point", geometry = points) + z <- store_dataset(z, point_sf, "points") + + line <- st_linestring(rbind(c(0,0), c(1,1), c(2,2))) + line_sf <- st_sf(id = 1, type = "line", geometry = st_sfc(line, crs = st_crs(4326))) + z <- store_dataset(z, line_sf, "lines") + + polygon <- st_polygon(list(rbind(c(0,0), c(1,0), c(1,1), c(0,1), c(0,0)))) + polygon_sf <- st_sf(id = 1, type = "polygon", geometry = st_sfc(polygon, crs = st_crs(4326))) + z <- store_dataset(z, polygon_sf, "polygons") + + # Read back each geometry type + retrieved_points <- read_dataset_in_workspace(z, "points") + retrieved_lines <- read_dataset_in_workspace(z, "lines") + retrieved_polygons <- read_dataset_in_workspace(z, "polygons") + + # Check geometry types are preserved + expect_equal(as.character(st_geometry_type(retrieved_points, by_geometry = FALSE)), "POINT") + expect_equal(as.character(st_geometry_type(retrieved_lines, by_geometry = FALSE)), "LINESTRING") + expect_equal(as.character(st_geometry_type(retrieved_polygons, by_geometry = FALSE)), "POLYGON") +}) + +test_that("read_dataset_in_workspace preserves attribute data", { + skip_if_not_installed("sf") + + library(sf) + z <- new_workspace() + + # Create sf object with various attribute types + points <- st_sfc(st_point(c(0, 0)), st_point(c(1, 1)), st_point(c(2, 2)), crs = st_crs(4326)) + original_sf <- st_sf( + id = 1:3, + name = c("Location A", "Location B", "Location C"), + value = c(10.5, 20.3, 15.7), + category = factor(c("type1", "type2", "type1")), + active = c(TRUE, FALSE, TRUE), + geometry = points + ) + + z <- store_dataset(z, original_sf, "attributed_data") + retrieved_sf <- read_dataset_in_workspace(z, "attributed_data") + + # Check all attributes are preserved + expect_equal(retrieved_sf$id, 1:3) + expect_equal(retrieved_sf$name, c("Location A", "Location B", "Location C")) + expect_equal(retrieved_sf$value, c(10.5, 20.3, 15.7)) + expect_equal(retrieved_sf$active, c(TRUE, FALSE, TRUE)) + # Note: factor levels might not be perfectly preserved through file I/O + expect_equal(as.character(retrieved_sf$category), c("type1", "type2", "type1")) +}) + +test_that("read_dataset_in_workspace works after pack/unpack cycle", { + skip_if_not_installed("sf") + + library(sf) + z <- new_workspace() + + # Create and store sf object + points <- st_sfc(st_point(c(10, 20)), st_point(c(30, 40)), crs = st_crs(4326)) + original_sf <- st_sf(id = 1:2, name = c("A", "B"), geometry = points) + z <- store_dataset(z, original_sf, "pack_test") + + # Pack and unpack + packed_file <- tempfile(fileext = ".zip") + pack_workspace(z, packed_file) + z2 <- unpack_workspace(packed_file) + + # Read from unpacked workspace + retrieved_sf <- read_dataset_in_workspace(z2, "pack_test") + + expect_s3_class(retrieved_sf, "sf") + expect_equal(nrow(retrieved_sf), 2) + expect_equal(retrieved_sf$id, 1:2) + expect_equal(retrieved_sf$name, c("A", "B")) + expect_equal(st_crs(retrieved_sf), st_crs(4326)) +}) + +test_that("read_dataset_in_workspace handles empty sf objects", { + skip_if_not_installed("sf") + + library(sf) + z <- new_workspace() + + # Create and store empty sf object + empty_sf <- st_sf( + id = integer(0), + name = character(0), + geometry = st_sfc(crs = 4326) + ) + z <- store_dataset(z, empty_sf, "empty_test") + + # Read back empty sf object + retrieved_sf <- read_dataset_in_workspace(z, "empty_test") + + expect_s3_class(retrieved_sf, "sf") + expect_equal(nrow(retrieved_sf), 0) + expect_equal(names(retrieved_sf), names(empty_sf)) + expect_equal(attr(retrieved_sf, "sf_column"), attr(empty_sf, "sf_column")) + expect_equal(st_crs(retrieved_sf), st_crs(empty_sf)) + expect_identical(st_agr(retrieved_sf), st_agr(empty_sf)) +}) + +test_that("read_dataset_in_workspace tracks metadata properly", { + skip_if_not_installed("sf") + + library(sf) + z <- new_workspace() + + points <- st_sfc(st_point(c(10, 20)), st_point(c(30, 40)), crs = st_crs(4326)) + original_sf <- st_sf( + id = 1:2, + name = c("A", "B"), + funky_geometry_name = points + ) + z <- store_dataset(z, original_sf, "geospatial_data") + + # Read back empty sf object + retrieved_sf <- read_dataset_in_workspace(z, "geospatial_data") + + expect_s3_class(retrieved_sf, "sf") + expect_equal(nrow(retrieved_sf), 2) + expect_equal(names(retrieved_sf), names(original_sf)) + expect_equal(attr(retrieved_sf, "sf_column"), attr(original_sf, "sf_column")) + expect_equal(st_crs(retrieved_sf), st_crs(original_sf)) + expect_identical(st_agr(retrieved_sf), st_agr(original_sf)) +}) + +test_that("read_dataset_in_workspace can read both regular and geospatial datasets", { + skip_if_not_installed("sf") + + library(sf) + z <- new_workspace() + + # Store regular dataset + z <- store_dataset(z, iris, "regular_data") + + # Store geospatial dataset + points <- st_sfc(st_point(c(0, 0)), st_point(c(1, 1)), crs = st_crs(4326)) + sf_data <- st_sf(id = 1:2, value = c("A", "B"), geometry = points) + z <- store_dataset(z, sf_data, "geo_data") + + # Read both types + regular_retrieved <- read_dataset_in_workspace(z, "regular_data") + geo_retrieved <- read_dataset_in_workspace(z, "geo_data") + + # Check regular dataset (should be tibble) + expect_s3_class(regular_retrieved, "tbl_df") + expect_equal(nrow(regular_retrieved), nrow(iris)) + + # Check geospatial dataset (should be sf) + expect_s3_class(geo_retrieved, "sf") + expect_equal(nrow(geo_retrieved), 2) + expect_true("geometry" %in% names(geo_retrieved)) +}) + +test_that("read_dataset_in_workspace error when sf package not available for geospatial data", { + skip_if_not_installed("sf") + + library(sf) + z <- new_workspace() + + # Store geospatial data + points <- st_sfc(st_point(c(0, 0)), crs = st_crs(4326)) + sf_data <- st_sf(id = 1, geometry = points) + z <- store_dataset(z, sf_data, "geo_test") + + # Mock requireNamespace to return FALSE for sf + local_mocked_bindings( + requireNamespace = function(pkg, ...) { + if (pkg == "sf") FALSE else TRUE + }, + .package = "base" + ) + + expect_error( + read_dataset_in_workspace(z, "geo_test"), + "sf.*package must be installed" + ) +}) + +test_that("read_dataset_in_workspace preserves coordinate precision", { + skip_if_not_installed("sf") + + library(sf) + z <- new_workspace() + + # Create sf object with high precision coordinates + precise_points <- st_sfc( + st_point(c(1.123456789, 2.987654321)), + st_point(c(-45.111111111, 78.999999999)), + crs = st_crs(4326) + ) + original_sf <- st_sf(id = 1:2, geometry = precise_points) + + z <- store_dataset(z, original_sf, "precise_coords") + retrieved_sf <- read_dataset_in_workspace(z, "precise_coords") + + # Check coordinate precision is reasonably preserved + original_coords <- st_coordinates(original_sf) + retrieved_coords <- st_coordinates(retrieved_sf) + + # Allow for small floating point differences due to file I/O + expect_equal(retrieved_coords, original_coords, tolerance = 1e-6) +}) diff --git a/tests/testthat/test-workspace-operations.R b/tests/testthat/test-workspace-operations.R new file mode 100644 index 0000000..2b568cc --- /dev/null +++ b/tests/testthat/test-workspace-operations.R @@ -0,0 +1,111 @@ +test_that("workspace_copy creates independent copy", { + z1 <- new_workspace() + z1 <- store_dataset(z1, iris, "iris") + z1 <- store_json(z1, '{"test": "data"}', "config.json", subdir = "config") + + z2 <- workspace_copy(z1) + + expect_false(identical(z1$dir, z2$dir)) + expect_identical(list_object_in_workspace(z1), list_object_in_workspace(z2)) + expect_s3_class(z2, "workspace") +}) + +test_that("workspace_copy preserves all data types", { + z1 <- new_workspace() + z1 <- store_dataset(z1, mtcars, "mtcars") + z1 <- store_json(z1, '{"key": "value"}', "json.json", subdir = "json") + z1 <- store_rds(z1, list(x = 1:5), "rds.rds", subdir = "rds") + + z2 <- workspace_copy(z1) + + expect_identical(read_dataset_in_workspace(z2, "mtcars"), tibble::as_tibble(mtcars)) + expect_identical(read_json_str_in_workspace(z2, "json"), '{"key": "value"}') + expect_identical(read_rds_in_workspace(z2, "rds", subdir = "rds"), list(x = 1:5)) +}) + +test_that("workspace_bind merges workspaces correctly", { + z1 <- new_workspace() + z1 <- store_dataset(z1, iris, "iris") + + z2 <- new_workspace() + z2 <- store_dataset(z2, mtcars, "mtcars") + + z3 <- workspace_bind(z1, z2) + objects <- list_object_in_workspace(z3) + + expect_true(all(c("iris", "mtcars") %in% objects$name)) + expect_equal(nrow(objects), 2) +}) + +test_that("workspace_bind handles different data types", { + z1 <- new_workspace() + z1 <- store_dataset(z1, iris, "iris") + z1 <- store_json(z1, '{"source": "z1"}', "config.json", subdir = "config") + + z2 <- new_workspace() + z2 <- store_dataset(z2, mtcars, "mtcars") + z2 <- store_rds(z2, list(data = "test"), "test.rds", subdir = "rds") + + z3 <- workspace_bind(z1, z2) + objects <- list_object_in_workspace(z3) + + expect_equal(nrow(objects), 4) + expect_true(all(c("iris", "mtcars", "config", "test") %in% objects$name)) +}) + +test_that("workspace_bind with replace=TRUE overwrites existing objects", { + z1 <- new_workspace() + z1 <- store_dataset(z1, iris, "shared_name") + + z2 <- new_workspace() + z2 <- store_dataset(z2, mtcars, "shared_name") + + z3 <- workspace_bind(z1, z2, replace = TRUE) + objects <- list_object_in_workspace(z3) + + expect_equal(nrow(objects), 1) + expect_equal(objects$name, "shared_name") + + result <- read_dataset_in_workspace(z3, "shared_name") + expect_identical(result, tibble::as_tibble(mtcars)) +}) + +test_that("workspace_bind with replace=FALSE keeps original objects", { + z1 <- new_workspace() + z1 <- store_dataset(z1, iris, "shared_name") + + z2 <- new_workspace() + z2 <- store_dataset(z2, mtcars, "shared_name") + + z3 <- workspace_bind(z1, z2, replace = FALSE) + objects <- list_object_in_workspace(z3) + + expect_equal(nrow(objects), 1) + expect_equal(objects$name, "shared_name") + + result <- read_dataset_in_workspace(z3, "shared_name") + expect_identical(result, tibble::as_tibble(iris)) +}) + +test_that("workspace_bind preserves timestamps", { + z1 <- new_workspace() + custom_timestamp <- "2024-01-01 10:00:00" + z1 <- store_dataset(z1, iris, "iris", timestamp = custom_timestamp) + + z2 <- new_workspace() + z2 <- store_dataset(z2, mtcars, "mtcars") + + z3 <- workspace_bind(z1, z2) + + iris_timestamp <- read_timestamp(z3, "iris", "dataset", subdir = "datasets") + expect_equal(iris_timestamp, custom_timestamp) +}) + +test_that("workspace operations validate input types", { + z <- new_workspace() + not_workspace <- list(dir = "test") + + expect_error(workspace_bind(not_workspace, z)) + expect_error(workspace_bind(z, not_workspace)) +}) + diff --git a/tests/testthat/test-yaml-operations.R b/tests/testthat/test-yaml-operations.R new file mode 100644 index 0000000..680773b --- /dev/null +++ b/tests/testthat/test-yaml-operations.R @@ -0,0 +1,319 @@ +test_that("store_yaml works with basic list objects", { + + + z <- new_workspace() + + # Create a simple list + test_list <- list( + name = "John Doe", + age = 30, + active = TRUE, + scores = c(85, 90, 78) + ) + + z <- store_yaml(z, test_list, "simple.yaml", subdir = "configs") + + objects <- list_object_in_workspace(z) + expect_equal(nrow(objects), 1) + expect_equal(objects$name, "simple") + expect_equal(objects$type, "yaml") + expect_true(grepl("\\.yaml$", objects$file)) +}) + +test_that("store_yaml handles nested list structures", { + + + z <- new_workspace() + + # Create nested list structure + complex_list <- list( + database = list( + host = "localhost", + port = 5432, + credentials = list( + username = "admin", + password = "secret123" + ) + ), + settings = list( + debug = TRUE, + max_connections = 100, + features = list( + caching = TRUE, + logging = FALSE + ) + ) + ) + + z <- store_yaml(z, complex_list, "config.yaml", subdir = "configs") + + objects <- list_object_in_workspace(z) + expect_equal(nrow(objects), 1) + expect_equal(objects$name, "config") + expect_equal(objects$type, "yaml") +}) + +test_that("store_yaml automatically adds .yaml extension", { + + + z <- new_workspace() + test_list <- list(key = "value") + + z <- store_yaml(z, test_list, "no_extension", subdir = "test") + + objects <- list_object_in_workspace(z) + expect_true(grepl("\\.yaml$", objects$file)) +}) + +test_that("store_yaml preserves .yaml extension", { + + + z <- new_workspace() + test_list <- list(key = "value") + + z <- store_yaml(z, test_list, "with_extension.yaml", subdir = "test") + + objects <- list_object_in_workspace(z) + expect_true(grepl("with_extension\\.yaml$", objects$file)) + expect_false(grepl("with_extension\\.yaml\\.yaml$", objects$file)) +}) + +test_that("store_yaml works with custom names", { + + + z <- new_workspace() + test_list <- list(data = "test") + + z <- store_yaml(z, test_list, "file.yaml", name = "custom_name", subdir = "test") + + objects <- list_object_in_workspace(z) + expect_equal(objects$name, "custom_name") + expect_false("file" %in% objects$name) +}) + +test_that("store_yaml works with custom timestamps", { + + + z <- new_workspace() + test_list <- list(data = "test") + custom_timestamp <- "2024-06-15 14:30:00" + + z <- store_yaml(z, test_list, "timestamped.yaml", subdir = "test", timestamp = custom_timestamp) + + retrieved_timestamp <- read_timestamp(z, "timestamped", "yaml", subdir = "test") + expect_equal(retrieved_timestamp, custom_timestamp) +}) + +test_that("store_yaml replaces existing YAML files", { + + + z <- new_workspace() + + # Store first YAML + first_list <- list(version = 1, data = "old") + z <- store_yaml(z, first_list, "replaceable.yaml", subdir = "test") + + # Store second YAML with same name + second_list <- list(version = 2, data = "new", extra = "field") + z <- store_yaml(z, second_list, "replaceable.yaml", subdir = "test") + + objects <- list_object_in_workspace(z) + expect_equal(nrow(objects), 1) + expect_equal(objects$name, "replaceable") +}) + +test_that("store_yaml handles different data types in lists", { + + z <- new_workspace() + + # List with various R data types + mixed_list <- list( + string = "text", + number = 42.5, + integer = 10L, + logical = TRUE, + vector = c(1, 2, 3), + character_vector = c("a", "b", "c"), + nested = list( + inner_string = "nested text", + inner_number = 99 + ) + ) + + z <- store_yaml(z, mixed_list, "mixed_types.yaml", subdir = "test") + + objects <- list_object_in_workspace(z) + expect_equal(nrow(objects), 1) + expect_equal(objects$type, "yaml") +}) + +test_that("read_yaml_in_workspace retrieves YAML objects correctly", { + + + z <- new_workspace() + + # Create and store YAML + original_list <- list( + name = "John Doe", + age = 30, + active = TRUE, + scores = c(85, 90, 78) + ) + + z <- store_yaml(z, original_list, "test.yaml", subdir = "configs") + + # Read back the YAML + retrieved_list <- read_yaml_in_workspace(z, "test", subdir = "configs") + + # Check that it's a list + expect_type(retrieved_list, "list") + + # Check data integrity + expect_equal(retrieved_list$name, "John Doe") + expect_equal(retrieved_list$age, 30) + expect_equal(retrieved_list$active, TRUE) + expect_equal(retrieved_list$scores, c(85, 90, 78)) +}) + +test_that("read_yaml_in_workspace preserves nested structures", { + + + z <- new_workspace() + + # Create nested structure + nested_list <- list( + database = list( + host = "localhost", + port = 5432, + settings = list( + timeout = 30, + retry = TRUE + ) + ), + features = list( + caching = TRUE, + logging = FALSE + ) + ) + + z <- store_yaml(z, nested_list, "nested.yaml", subdir = "configs") + retrieved_list <- read_yaml_in_workspace(z, "nested", subdir = "configs") + + # Check nested structure preservation + expect_equal(retrieved_list$database$host, "localhost") + expect_equal(retrieved_list$database$port, 5432) + expect_equal(retrieved_list$database$settings$timeout, 30) + expect_equal(retrieved_list$database$settings$retry, TRUE) + expect_equal(retrieved_list$features$caching, TRUE) + expect_equal(retrieved_list$features$logging, FALSE) +}) + +test_that("read_yaml_in_workspace works without specifying subdir", { + + + z <- new_workspace() + + test_list <- list(key = "value", number = 42) + z <- store_yaml(z, test_list, "global.yaml", subdir = "root") + + # Should work when subdir is not specified if name is unique + retrieved_list <- read_yaml_in_workspace(z, "global") + + expect_equal(retrieved_list$key, "value") + expect_equal(retrieved_list$number, 42) +}) + +test_that("read_yaml_in_workspace works after pack/unpack cycle", { + + + z <- new_workspace() + + # Create and store YAML + config_list <- list( + app_name = "MyApp", + version = "1.0.0", + settings = list( + debug = FALSE, + port = 8080 + ) + ) + z <- store_yaml(z, config_list, "app_config.yaml", subdir = "configs") + + # Pack and unpack + packed_file <- tempfile(fileext = ".zip") + pack_workspace(z, packed_file) + z2 <- unpack_workspace(packed_file) + + # Read from unpacked workspace + retrieved_list <- read_yaml_in_workspace(z2, "app_config", subdir = "configs") + + expect_equal(retrieved_list$app_name, "MyApp") + expect_equal(retrieved_list$version, "1.0.0") + expect_equal(retrieved_list$settings$debug, FALSE) + expect_equal(retrieved_list$settings$port, 8080) +}) + +test_that("store_yaml validates input types", { + z <- new_workspace() + + # Test non-list inputs + expect_error(store_yaml(z, "not a list", "test.yaml", subdir = "test")) + expect_error(store_yaml(z, 123, "test.yaml", subdir = "test")) + expect_error(store_yaml(z, c(1, 2, 3), "test.yaml", subdir = "test")) + expect_error(store_yaml(z, data.frame(x = 1:3), "test.yaml", subdir = "test")) +}) + +test_that("store_yaml validates string parameters", { + + + z <- new_workspace() + test_list <- list(key = "value") + + expect_error(store_yaml(z, test_list, 123, subdir = "test")) + expect_error(store_yaml(z, test_list, "test.yaml", subdir = 123)) + expect_error(store_yaml(z, test_list, "test.yaml", subdir = "test", timestamp = 123)) +}) + +test_that("read_yaml_in_workspace handles nonexistent files", { + + + z <- new_workspace() + + expect_error(read_yaml_in_workspace(z, "nonexistent")) + expect_error(read_yaml_in_workspace(z, "nonexistent", subdir = "test")) +}) + +test_that("store_yaml and read_yaml_in_workspace handle empty lists", { + z <- new_workspace() + + # Store empty list + empty_list <- list() + z <- store_yaml(z, empty_list, "empty.yaml", subdir = "test") + + # Read back empty list + retrieved_list <- read_yaml_in_workspace(z, "empty", subdir = "test") + + expect_type(retrieved_list, "list") + expect_length(retrieved_list, 0) +}) + +test_that("YAML handles special characters and unicode", { + + z <- new_workspace() + + # List with special characters and unicode + special_list <- list( + text_with_spaces = "Hello World", + special_chars = "!@#$%^&*()_+-=[]{}|;':\",./<>?", + unicode = "café, naïve, résumé, 中文, العربية, 🎉", + multiline = "Line 1\nLine 2\nLine 3" + ) + + z <- store_yaml(z, special_list, "special.yaml", subdir = "test") + retrieved_list <- read_yaml_in_workspace(z, "special", subdir = "test") + + expect_equal(retrieved_list$text_with_spaces, "Hello World") + expect_equal(retrieved_list$special_chars, "!@#$%^&*()_+-=[]{}|;':\",./<>?") + expect_equal(retrieved_list$unicode, "café, naïve, résumé, 中文, العربية, 🎉") + expect_equal(retrieved_list$multiline, "Line 1\nLine 2\nLine 3") +})