From 08c9d8c8aecf41d61f36b95a3661f3db7d483054 Mon Sep 17 00:00:00 2001 From: Roberto Villegas-Diaz Date: Sun, 26 Jul 2026 21:51:03 +0100 Subject: [PATCH 01/14] Increment version number to 0.2.0.9000 --- DESCRIPTION | 2 +- NEWS.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index db4349b..3b1c610 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: dsROCrate Title: 'DataSHIELD' RO-Crate Governance Functions -Version: 0.2.0 +Version: 0.2.0.9000 Authors@R: c( person(given = "Roberto", family = "Villegas-Diaz", diff --git a/NEWS.md b/NEWS.md index 26c254a..1eb777f 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,5 @@ +# dsROCrate (development version) + # dsROCrate 0.2.0 ## Breaking Changes From 1d9c1493d4a9cc2bf8a0eaf028a96d45b0eb33b0 Mon Sep 17 00:00:00 2001 From: Roberto Villegas-Diaz Date: Wed, 29 Jul 2026 16:57:12 +0100 Subject: [PATCH 02/14] Add new internal documentation and delete unused internal functions --- R/safe-call-utils.R | 64 ++++++++++++++++++++++++++++++++++++++++++- R/safe-call.R | 22 +++++++++++++++ R/safe-reference.R | 10 +++++++ R/safe-symbol-utils.R | 44 +++++++++++++++++++++++++++++ R/safe-symbol.R | 50 +++++++++++++++++++-------------- R/symbol-registry.R | 61 +++++++++++++++++++++++++++++++---------- 6 files changed, 215 insertions(+), 36 deletions(-) diff --git a/R/safe-call-utils.R b/R/safe-call-utils.R index bb34048..95ce1d0 100644 --- a/R/safe-call-utils.R +++ b/R/safe-call-utils.R @@ -9,6 +9,17 @@ as.data.frame.safe_call <- function(x, ...) { ) } +#' Enrich argument from function call +#' +#' @param arg String with argument. +#' @param registry Symbol registry object. +#' @param timestamp Timestamp to map the symbol details. +#' @param session String with session ID. +#' +#' @returns `safe_reference` object. +#' @keywords internal +#' +#' @noRd enrich_argument <- function(arg, registry, timestamp, session) { if (!is.character(arg) || length(arg) != 1) { return(arg) @@ -31,8 +42,17 @@ enrich_argument <- function(arg, registry, timestamp, session) { ) } +#' Enrich function call +#' +#' @param call Function call. +#' @param registry Symbol registry object. +#' +#' @returns Updated function call with enrich arguments. +#' @keywords internal +#' +#' @noRd enrich_call <- function(call, registry) { - # call$args <- lapply(call$args, resolve_argument, registry = registry) + # call `enrich_argument` for each argument in the function call call$args <- purrr::map( call$args, enrich_argument, @@ -44,6 +64,14 @@ enrich_call <- function(call, registry) { call } +#' Get function details +#' +#' @param info List with details for function call. +#' +#' @returns Function invoked in function call. +#' @keywords internal +#' +#' @noRd get_function <- function(info) { if (is.null(info$package)) { return( @@ -57,6 +85,16 @@ get_function <- function(info) { ) } +#' Parse arguments from a function call +#' +#' @param fx_call R function call. +#' @param info List with details from the function call. +#' @param expand.dots Boolean flag to indicate whether to expand dot args. +#' +#' @returns List with simplified arguments. +#' @keywords internal +#' +#' @noRd parse_arguments <- function(fx_call, info, expand.dots = FALSE) { supplied <- as.list(fx_call[-1]) supplied_names <- names(supplied) @@ -100,6 +138,14 @@ parse_arguments <- function(fx_call, info, expand.dots = FALSE) { matched } +#' Parse function call +#' +#' @param fx_call R function call. +#' +#' @returns List with properties extracted from `fx_call`. +#' @keywords internal +#' +#' @noRd parse_call <- function(fx_call) { fx <- fx_call[[1]] @@ -118,6 +164,14 @@ parse_call <- function(fx_call) { ) } +#' Parse function +#' +#' @param fx R function call. +#' +#' @returns List with function properties +#' @keywords internal +#' +#' @noRd parse_function <- function(fx) { if (is.call(fx) && identical(fx[[1]], as.name("::"))) { return( @@ -136,6 +190,14 @@ parse_function <- function(fx) { ) } +#' Simplify argument +#' +#' @param x Object with argument from function call. +#' +#' @returns Simplified argument object. +#' @keywords internal +#' +#' @noRd simplify_argument <- function(x) { if (is.atomic(x) || is.character(x)) { return(x) diff --git a/R/safe-call.R b/R/safe-call.R index e6d8491..2a4b62d 100644 --- a/R/safe-call.R +++ b/R/safe-call.R @@ -12,6 +12,19 @@ as.data.frame.safe_call <- function(x, ...) { ) } +#' Create new `safe_call` object +#' +#' @param original Original function call. +#' @param package Function's package. +#' @param namespace Function's namespace. +#' @param fx Function's name. +#' @param args List with arguments. +#' @param ... Additional arguments. +#' +#' @returns New `safe_call` object. +#' @keywords internal +#' +#' @noRd new_safe_call <- function( original, package, @@ -40,6 +53,15 @@ new_safe_call <- function( ) } +#' Safe call details +#' +#' @param call Object with function call. +#' @param ... Additional arguments. +#' +#' @returns Object with the class `safe_call`. +#' @keywords internal +#' +#' @noRd safe_call <- function(call, ...) { UseMethod("safe_call") } diff --git a/R/safe-reference.R b/R/safe-reference.R index ee1aed0..bf9fe92 100644 --- a/R/safe-reference.R +++ b/R/safe-reference.R @@ -1,3 +1,13 @@ +#' Safe reference details +#' +#' @param symbol Symbol object. +#' @param symbol_id String with symbol unique ID. +#' @param column String with column name, when symbol is `symbol$column`. +#' +#' @returns Object with the class `safe_reference` +#' @keywords internal +#' +#' @noRd safe_reference <- function(symbol, symbol_id, column = NULL) { structure( list( diff --git a/R/safe-symbol-utils.R b/R/safe-symbol-utils.R index 89e05c3..dc26956 100644 --- a/R/safe-symbol-utils.R +++ b/R/safe-symbol-utils.R @@ -1,9 +1,19 @@ +#' Find symbols in a given expression +#' +#' @param expr Object with R expression. +#' +#' @returns A list with a reference, if any is found. +#' @keywords internal +#' +#' @noRd find_symbols <- function(expr) { + # parse expression if given value is a character if (is.character(expr)) { expr <- parse(text = expr)[[1]] } recurse <- function(x) { + # check if the given object is a symbol if (is.symbol(x)) { return(list(list( symbol = as.character(x), @@ -11,10 +21,12 @@ find_symbols <- function(expr) { ))) } + # check if the given objects is NOT a call if (!is.call(x)) { return(list()) } + # check if the given object is of the format `symbol$column` if (identical(x[[1]], quote(`$`))) { lhs <- x[[2]] out <- list(list( @@ -25,6 +37,7 @@ find_symbols <- function(expr) { return(out) } + # return recursive call of the original subset unlist( lapply(as.list(x)[-1], recurse), recursive = FALSE @@ -32,6 +45,7 @@ find_symbols <- function(expr) { } refs <- recurse(expr) + # filter out symbols from base packages refs <- Filter(\(x) !(x$symbol %in% c("base", "stats", "utils")), refs) if (!length(refs)) { return(NULL) @@ -39,17 +53,28 @@ find_symbols <- function(expr) { refs } +#' Resolve expression dependencies +#' +#' @param expr Object with R expression. +#' @param registry Symbol registry object. +#' +#' @returns Tibble object with expression dependencies' details. +#' @keywords internal +#' +#' @noRd resolve_dependencies <- function(expr, registry) { if (is.null(expr) || is.na(expr)) { return(tibble::tibble()) } + # find symbols for the given expression refs <- find_symbols(expr) if (is.null(refs)) { return(tibble::tibble()) } + # look up symbol details for each reference found previously purrr::map_dfr(refs, function(ref) { sym <- lookup_symbol(ref$symbol, registry) @@ -63,6 +88,16 @@ resolve_dependencies <- function(expr, registry) { }) } +#' Resolve provenance of symbol +#' +#' @param symbol_id String with unique symbol ID. +#' @param registry Symbol registry object. +#' @param visited Vector with symbol IDs that have been processed. +#' +#' @returns Tibble object with details of provenance for symbol. +#' @keywords internal +#' +#' @noRd resolve_provenance <- function(symbol_id, registry, visited = character()) { # local bindings id <- NULL @@ -91,6 +126,15 @@ resolve_provenance <- function(symbol_id, registry, visited = character()) { dplyr::bind_rows(deps, children) } +#' Resolve symbol's asset +#' +#' @param symbol_id String with unique symbol ID. +#' @param registry Symbol registry object. +#' +#' @returns String with asset(s) linked to symbol ID. +#' @keywords internal +#' +#' @noRd resolve_symbol_asset <- function(symbol_id, registry) { # local binding asset <- id <- kind <- NULL diff --git a/R/safe-symbol.R b/R/safe-symbol.R index efadd04..b3dc956 100644 --- a/R/safe-symbol.R +++ b/R/safe-symbol.R @@ -11,12 +11,30 @@ as_tibble.safe_symbol <- function(x, ...) { created_at = x$created_at, user = x$user, session = x$session, - action = x$action #, - # metadata = x$metadata, - # stringsAsFactors = FALSE + action = x$action ) } +#' Create new `safe_symbol` object +#' +#' @param symbol Symbol object. +#' @param version Version number. +#' @param kind Type of symbol. +#' @param asset Associated asset. +#' @param expr Associated expression. +#' @param depends_on Symbol dependencies. +#' @param created_by DataSHIELD function used to create the symbol. +#' @param created_at Creation date. +#' @param user Username. +#' @param session Unique session ID. +#' @param action Action type. +#' @param metadata Additional metadata. +#' @param id Unique symbol ID. +#' +#' @returns New `safe_symbol` object. +#' @keywords internal +#' +#' @noRd new_safe_symbol <- function( symbol, version = -999, @@ -55,23 +73,15 @@ new_safe_symbol <- function( ) } -new_safe_symbol_reference <- function(symbol, column = NULL) { - stopifnot(is.character(symbol)) - stopifnot(length(symbol) == 1) - - structure( - list( - symbol = symbol, - column = column - ), - class = "safe_symbol_reference" - ) -} - +#' Safe symbol details +#' +#' @param symbol Symbol object. +#' @param ... Additional arguments. +#' +#' @returns Object with the class `safe_symbol`. +#' @keywords internal +#' +#' @noRd safe_symbol <- function(symbol, ...) { new_safe_symbol(symbol = symbol, ...) } - -safe_symbol_reference <- function(symbol, ...) { - new_safe_symbol_reference(symbol = symbol, ...) -} diff --git a/R/symbol-registry.R b/R/symbol-registry.R index b0062ee..420489a 100644 --- a/R/symbol-registry.R +++ b/R/symbol-registry.R @@ -18,24 +18,28 @@ as.data.frame.symbol_registry <- function(x, ...) { ) } -has_symbol <- function(registry, name) { - UseMethod("has_symbol") -} - -#' @export -has_symbol.symbol_registry <- function(registry, name) { - name %in% names(registry$symbols) -} - +#' Create new `symbol_registry` object +#' +#' @param symbols List of symbols. +#' +#' @returns New `symbol_registry` object. +#' @keywords internal +#' +#' @noRd new_symbol_registry <- function(symbols = list()) { - structure( - list( - symbols = symbols - ), - class = "symbol_registry" - ) + structure(list(symbols = symbols), class = "symbol_registry") } +#' Look-up symbol in registry. +#' +#' @param symbol Symbol object. +#' @param registry Symbol registry object. +#' @param session Unique session ID. +#' +#' @returns Tibble slice with details associated to `symbol`. +#' @keywords internal +#' +#' @noRd lookup_symbol <- function(symbol, registry, session = NULL) { out <- registry$symbols @@ -49,6 +53,15 @@ lookup_symbol <- function(symbol, registry, session = NULL) { dplyr::slice(1) } +#' Register `symbol` in `registry` +#' +#' @param registry Symbol registry object. +#' @param symbol Symbol object. +#' +#' @returns Updated symbol registry. +#' @keywords internal +#' +#' @noRd register_symbol <- function(registry, symbol) { # local bindings aux <- session <- NULL @@ -86,6 +99,18 @@ register_symbol <- function(registry, symbol) { registry } +#' Resolve symbol +#' +#' @param registry Symbol registry object. +#' @param symbol Symbol object. +#' @param timestamp Timestamp, when the symbol was created. +#' @param session Unique session ID. +#' @param user Username. +#' +#' @returns Tibble slice with details associated to `symbol`. +#' @keywords internal +#' +#' @noRd resolve_symbol <- function( registry, symbol, @@ -115,6 +140,12 @@ resolve_symbol <- function( dplyr::slice_max(created_at, n = 1) } +#' Create new symbol registry +#' +#' @returns New `symbol_registry` object. +#' @keywords internal +#' +#' @noRd symbol_registry <- function() { new_symbol_registry() } From a15cc0d5da43b30ccf0a2dcd8d537921f9f86650 Mon Sep 17 00:00:00 2001 From: Roberto Villegas-Diaz Date: Wed, 29 Jul 2026 16:57:20 +0100 Subject: [PATCH 03/14] New build --- NAMESPACE | 1 - 1 file changed, 1 deletion(-) diff --git a/NAMESPACE b/NAMESPACE index 3608585..753fa43 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -51,7 +51,6 @@ S3method(flatten_safe_project,default) S3method(flatten_safe_project,rocrate) S3method(flatten_safe_setting,default) S3method(flatten_safe_setting,rocrate) -S3method(has_symbol,symbol_registry) S3method(init,ArmadilloCredentials) S3method(init,opal) S3method(init,rocrate) From 2748ad189cb762d3d894756e634e106949bb40db Mon Sep 17 00:00:00 2001 From: Roberto Villegas-Diaz Date: Wed, 29 Jul 2026 17:19:07 +0100 Subject: [PATCH 04/14] Bump version to 0.2.1 --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 3b1c610..263960c 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: dsROCrate Title: 'DataSHIELD' RO-Crate Governance Functions -Version: 0.2.0.9000 +Version: 0.2.1 Authors@R: c( person(given = "Roberto", family = "Villegas-Diaz", From 12b32a6564cd06ce113d3d0122afce299cdb4134 Mon Sep 17 00:00:00 2001 From: Roberto Villegas-Diaz Date: Thu, 30 Jul 2026 10:54:23 +0100 Subject: [PATCH 05/14] Update NEWS --- NEWS.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 1eb777f..72308e5 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,4 +1,12 @@ -# dsROCrate (development version) +# dsROCrate 0.2.1 + +## Internal Changes + +* Removed unused internal helpers: `has_symbol()`/`has_symbol.symbol_registry()` (and its S3 export) and `safe_symbol_reference()`/`new_safe_symbol_reference()`, none of which were called anywhere in the package. +* Removed leftover commented-out code from `safe_symbol()`. +* Added roxygen2 documentation (`@param`, `@returns`, `@keywords internal`, `@noRd`) and explanatory comments to the internal symbol-tracking functions introduced in 0.2.0 (`safe_call()`, `safe_symbol()`, `safe_reference()`, `symbol_registry()`, and their supporting utilities), with no change in behaviour. +* Updated spell-check `WORDLIST`. +* Re-rendered vignettes to refresh example output. # dsROCrate 0.2.0 From 8c1900e7dab3de99c4269bdbd783a263369e3476 Mon Sep 17 00:00:00 2001 From: Roberto Villegas-Diaz Date: Thu, 30 Jul 2026 11:05:32 +0100 Subject: [PATCH 06/14] Remove duplicated as.data.frame.safe_call S3 generic --- R/safe-call-utils.R | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/R/safe-call-utils.R b/R/safe-call-utils.R index 95ce1d0..553e296 100644 --- a/R/safe-call-utils.R +++ b/R/safe-call-utils.R @@ -1,14 +1,3 @@ -#' @export -as.data.frame.safe_call <- function(x, ...) { - data.frame( - package = x$package, - fx = x$fx, - argument = names(x$args), - value = vapply(x$args, toString, character(1)), - stringsAsFactors = FALSE - ) -} - #' Enrich argument from function call #' #' @param arg String with argument. @@ -89,7 +78,8 @@ get_function <- function(info) { #' #' @param fx_call R function call. #' @param info List with details from the function call. -#' @param expand.dots Boolean flag to indicate whether to expand dot args. +#' @param expand.dots Boolean flag. Should arguments matching `...` in the call +#' be included or left as a `...` argument? #' #' @returns List with simplified arguments. #' @keywords internal From a9e04fa4053eaa16cf1ad728ef55591b1190c527 Mon Sep 17 00:00:00 2001 From: Roberto Villegas-Diaz Date: Thu, 30 Jul 2026 11:05:53 +0100 Subject: [PATCH 07/14] Update print generics to exclude outdated fields --- R/print.R | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/R/print.R b/R/print.R index bf3bf85..92599fc 100644 --- a/R/print.R +++ b/R/print.R @@ -65,10 +65,6 @@ print.safe_call <- function(x, ...) { } }) ) - # msg <- c( - # msg, - # paste0(" ", names(x$args), " = ", unlist(x$args), collapse = "\n") - # ) } message(paste0(msg, collapse = "\n")) @@ -88,12 +84,12 @@ print.safe_symbol <- function(x, ...) { msg <- c(msg, paste("Asset :", x$asset)) } - if (!is.null(x$column)) { - msg <- c(msg, paste("Column :", x$column)) + if (!is.null(x$expr)) { + msg <- c(msg, paste("Expression :", x$expr)) } - if (!is.null(x$parent)) { - msg <- c(msg, paste("Parent :", x$parent)) + if (!is.null(x$session)) { + msg <- c(msg, paste("Session :", x$session)) } message(paste0(msg, collapse = "\n")) From 9d3ed3518add17ca165ebc8915d2d2b0d313fd06 Mon Sep 17 00:00:00 2001 From: Roberto Villegas-Diaz Date: Thu, 30 Jul 2026 11:13:19 +0100 Subject: [PATCH 08/14] Update print.safe_symbol S3 generic --- R/print.R | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/R/print.R b/R/print.R index 92599fc..376a7db 100644 --- a/R/print.R +++ b/R/print.R @@ -80,15 +80,15 @@ print.safe_symbol <- function(x, ...) { msg <- c(msg, paste("Symbol :", x$symbol)) msg <- c(msg, paste("Kind :", x$kind)) - if (!is.null(x$asset)) { + if (!is.null(x$asset) && !is.na(x$asset)) { msg <- c(msg, paste("Asset :", x$asset)) } - if (!is.null(x$expr)) { + if (!is.null(x$expr) && !is.na(x$expr)) { msg <- c(msg, paste("Expression :", x$expr)) } - if (!is.null(x$session)) { + if (!is.null(x$session) && !is.na(x$session)) { msg <- c(msg, paste("Session :", x$session)) } From c1cb55e210897993bea603d36330b4c103ccaaa7 Mon Sep 17 00:00:00 2001 From: Roberto Villegas-Diaz Date: Thu, 30 Jul 2026 11:14:06 +0100 Subject: [PATCH 09/14] Update as.data.frane.symbol_registry S3 generic to include missing fields from the symbol_registry --- R/symbol-registry.R | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/R/symbol-registry.R b/R/symbol-registry.R index 420489a..e0b86ec 100644 --- a/R/symbol-registry.R +++ b/R/symbol-registry.R @@ -6,11 +6,18 @@ as.data.frame.symbol_registry <- function(x, ...) { x$symbols, function(sym) { data.frame( + id = sym$symbol, symbol = sym$symbol, + version = sym$version, kind = sym$kind, asset = sym$asset, - parent = sym$parent, - column = sym$column, + expr = sym$expr, + depends_on = sym$depends_on, + created_by = sym$created_by, + created_at = sym$created_at, + user = sym$user, + session = sym$session, + action = sym$action, stringsAsFactors = FALSE ) } From 5fe51352f391adb0f6f788df58bde57ae3403c56 Mon Sep 17 00:00:00 2001 From: Roberto Villegas-Diaz Date: Thu, 30 Jul 2026 11:43:45 +0100 Subject: [PATCH 10/14] Simplify as.data.frame.symbol_registry S3 generic --- R/symbol-registry.R | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/R/symbol-registry.R b/R/symbol-registry.R index e0b86ec..3477898 100644 --- a/R/symbol-registry.R +++ b/R/symbol-registry.R @@ -1,28 +1,6 @@ #' @export as.data.frame.symbol_registry <- function(x, ...) { - do.call( - rbind, - lapply( - x$symbols, - function(sym) { - data.frame( - id = sym$symbol, - symbol = sym$symbol, - version = sym$version, - kind = sym$kind, - asset = sym$asset, - expr = sym$expr, - depends_on = sym$depends_on, - created_by = sym$created_by, - created_at = sym$created_at, - user = sym$user, - session = sym$session, - action = sym$action, - stringsAsFactors = FALSE - ) - } - ) - ) + as.data.frame(x$symbols) } #' Create new `symbol_registry` object From d9a858f49ece91c849f2642c1f90326ca6d94864 Mon Sep 17 00:00:00 2001 From: Roberto Villegas-Diaz Date: Thu, 30 Jul 2026 11:44:12 +0100 Subject: [PATCH 11/14] Update checking condition for empty registry --- R/symbol-registry.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/symbol-registry.R b/R/symbol-registry.R index 3477898..d64b869 100644 --- a/R/symbol-registry.R +++ b/R/symbol-registry.R @@ -53,7 +53,7 @@ register_symbol <- function(registry, symbol) { stopifnot(inherits(symbol, "safe_symbol")) # extract current version of symbol - if (nrow(registry$symbols) == 0 || length(registry$symbols) == 0) { + if (length(registry$symbols) == 0 || nrow(registry$symbols) == 0) { current_version <- 0 } else { aux <- registry$symbols |> From c36a844e80eafdfd633ce71e4e804bf3742b7e18 Mon Sep 17 00:00:00 2001 From: Roberto Villegas-Diaz Date: Thu, 30 Jul 2026 12:05:40 +0100 Subject: [PATCH 12/14] Wrap args in I(list()) to preserve list of args of any length --- R/safe-call.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/safe-call.R b/R/safe-call.R index 2a4b62d..40e2d26 100644 --- a/R/safe-call.R +++ b/R/safe-call.R @@ -5,7 +5,7 @@ as.data.frame.safe_call <- function(x, ...) { user = x$user, r_cmd = x$original, fx = paste0(x$package, x$namespace, x$fx), - args = x$args, + args = I(list(x$args)), session = x$session, profile = x$profile, stringsAsFactors = FALSE From b469f9f94e73c33f21d4c25a71993c1cd21f89da Mon Sep 17 00:00:00 2001 From: Roberto Villegas-Diaz Date: Thu, 30 Jul 2026 12:06:48 +0100 Subject: [PATCH 13/14] Add new tests for internal methods used in the creation of a registry of symbols for the processing of logs --- tests/testthat/test-safe-call.R | 91 +++++++++++++++++++++++++++ tests/testthat/test-safe-symbol.R | 39 ++++++++++++ tests/testthat/test-symbol-registry.R | 90 ++++++++++++++++++++++++++ 3 files changed, 220 insertions(+) create mode 100644 tests/testthat/test-safe-call.R create mode 100644 tests/testthat/test-safe-symbol.R create mode 100644 tests/testthat/test-symbol-registry.R diff --git a/tests/testthat/test-safe-call.R b/tests/testthat/test-safe-call.R new file mode 100644 index 0000000..9ca87ca --- /dev/null +++ b/tests/testthat/test-safe-call.R @@ -0,0 +1,91 @@ +test_that("safe_call.character parses a function call into a safe_call", { + x <- safe_call( + "mean(D$age)", + `@timestamp` = as.POSIXct("2026-07-01 10:00:00", tz = "UTC"), + username = "alice", + ds_id = "session-1", + ds_profile = "default" + ) + + expect_s3_class(x, "safe_call") + expect_equal(x$fx, "mean") + expect_equal(x$user, "alice") + expect_equal(x$session, "session-1") + expect_equal(x$profile, "default") +}) + +test_that("as.data.frame.safe_call returns the expected columns", { + x <- safe_call( + "mean(D$age)", + `@timestamp` = as.POSIXct("2026-07-01 10:00:00", tz = "UTC"), + username = "alice", + ds_id = "session-1", + ds_profile = "default" + ) + + res <- as.data.frame(x) + + expect_s3_class(res, "data.frame") + expect_equal(nrow(res), 1) + expect_setequal( + names(res), + c("timestamp", "user", "r_cmd", "fx", "args", "session", "profile") + ) + expect_equal(res$user, "alice") + expect_equal(res$session, "session-1") + expect_equal(res$profile, "default") + expect_equal(res$timestamp, "2026-07-01T10:00:00") + expect_true(is.list(res$args[[1]])) + expect_equal(res$args[[1]], x$args) +}) + +test_that("as.data.frame.safe_call keeps a stable 'args' column regardless of arity", { + # regression test: `args = x$args` inside data.frame() lets R recurse into + # the named list and expand/rename it, so a single-argument call collapses + # the "args" column into a column named after that argument (e.g. "x" for + # `mean(x)`), and a multi-argument call spreads into several columns + # (e.g. "args.x", "args.y") instead of one stable "args" column. + zero_arg <- safe_call( + "Sys.time()", + `@timestamp` = as.POSIXct("2026-07-01 10:00:00", tz = "UTC"), + username = "alice", + ds_id = "session-1", + ds_profile = "default" + ) + one_arg <- safe_call( + "mean(D$age)", + `@timestamp` = as.POSIXct("2026-07-01 10:00:00", tz = "UTC"), + username = "alice", + ds_id = "session-1", + ds_profile = "default" + ) + two_arg <- safe_call( + "seq(1, 10)", + `@timestamp` = as.POSIXct("2026-07-01 10:00:00", tz = "UTC"), + username = "alice", + ds_id = "session-1", + ds_profile = "default" + ) + + expect_true("args" %in% names(as.data.frame(zero_arg))) + expect_true("args" %in% names(as.data.frame(one_arg))) + expect_true("args" %in% names(as.data.frame(two_arg))) +}) + +test_that("as.data.frame.safe_call is not shadowed by a conflicting definition", { + # regression test: safe-call.R and safe-call-utils.R previously both + # defined `as.data.frame.safe_call()` with different (incompatible) sets + # of columns; whichever file was sourced last silently won. This checks + # that only the `timestamp`/`user`/`r_cmd`/... version is in effect. + x <- safe_call( + "mean(D$age)", + `@timestamp` = as.POSIXct("2026-07-01 10:00:00", tz = "UTC"), + username = "alice", + ds_id = "session-1", + ds_profile = "default" + ) + + res <- as.data.frame(x) + + expect_false(any(c("package", "argument", "value") %in% names(res))) +}) diff --git a/tests/testthat/test-safe-symbol.R b/tests/testthat/test-safe-symbol.R new file mode 100644 index 0000000..adee2a6 --- /dev/null +++ b/tests/testthat/test-safe-symbol.R @@ -0,0 +1,39 @@ +test_that("print.safe_symbol displays populated fields", { + x <- safe_symbol( + symbol = "D", + kind = "table", + asset = "project.table", + expr = NA_character_, + session = "session-1" + ) + + expect_message(print(x), "") + expect_message(print(x), "Symbol : D") + expect_message(print(x), "Kind : table") + expect_message(print(x), "Asset : project.table") + expect_message(print(x), "Session : session-1") +}) + +test_that("print.safe_symbol omits fields that are NA", { + # regression test: print.safe_symbol previously checked `!is.null(x$asset)` + # (etc.), but these fields default to NA_character_ rather than NULL, so + # the check never actually skipped an unset field. It also referenced + # `x$column`/`x$parent`, which don't exist on a safe_symbol object. + x <- safe_symbol(symbol = "D", kind = "table", asset = "project.table") + + expect_message(print(x), "") + + msg <- capture.output(print(x), type = "message") + full_msg <- paste(msg, collapse = "\n") + + expect_false(grepl("Expression", full_msg)) + expect_false(grepl("Session", full_msg)) + expect_false(grepl("Column", full_msg)) + expect_false(grepl("Parent", full_msg)) +}) + +test_that("print.safe_symbol returns its input invisibly", { + x <- safe_symbol(symbol = "D", kind = "table") + + expect_identical(suppressMessages(print(x)), x) +}) diff --git a/tests/testthat/test-symbol-registry.R b/tests/testthat/test-symbol-registry.R new file mode 100644 index 0000000..4c9ebf0 --- /dev/null +++ b/tests/testthat/test-symbol-registry.R @@ -0,0 +1,90 @@ +test_that("register_symbol adds a symbol to an empty registry", { + registry <- symbol_registry() + + symbol <- safe_symbol( + symbol = "D", + kind = "table", + asset = "project.table", + created_at = as.POSIXct("2026-07-01 10:00:00", tz = "UTC"), + user = "alice", + session = "session-1" + ) + + registry <- register_symbol(registry, symbol) + + expect_s3_class(registry, "symbol_registry") + expect_equal(nrow(registry$symbols), 1) + expect_equal(registry$symbols$version, 1L) +}) + +test_that("as.data.frame.symbol_registry does not error on a populated registry", { + # regression test: as.data.frame.symbol_registry previously used + # `lapply(x$symbols, function(sym) ...)`, which iterates over the + # *columns* of the underlying tibble rather than its rows, so `sym` + # was a plain vector and `sym$symbol` errored with "$ operator is + # invalid for atomic vectors" as soon as the registry had any symbols. + registry <- symbol_registry() + + symbol <- safe_symbol( + symbol = "D", + kind = "table", + asset = "project.table", + created_at = as.POSIXct("2026-07-01 10:00:00", tz = "UTC"), + user = "alice", + session = "session-1" + ) + + registry <- register_symbol(registry, symbol) + + expect_no_error(res <- as.data.frame(registry)) + expect_s3_class(res, "data.frame") + expect_equal(nrow(res), 1) +}) + +test_that("as.data.frame.symbol_registry returns the expected, correctly-populated columns", { + # regression test: the id column was previously populated from + # `sym$symbol` instead of `sym$id`, and the method referenced + # non-existent `parent`/`column` fields. + registry <- symbol_registry() + + symbol <- safe_symbol( + symbol = "D", + kind = "table", + asset = "project.table", + created_at = as.POSIXct("2026-07-01 10:00:00", tz = "UTC"), + user = "alice", + session = "session-1" + ) + + registry <- register_symbol(registry, symbol) + + res <- as.data.frame(registry) + + expect_true(all( + c( + "id", + "symbol", + "version", + "kind", + "asset", + "created_by", + "user", + "session" + ) %in% + names(res) + )) + expect_false(any(c("parent", "column") %in% names(res))) + + expect_equal(res$id, symbol$id) + expect_equal(res$symbol, "D") + expect_false(identical(res$id, res$symbol)) +}) + +test_that("as.data.frame.symbol_registry returns an empty data frame for an empty registry", { + registry <- symbol_registry() + + res <- as.data.frame(registry) + + expect_s3_class(res, "data.frame") + expect_equal(nrow(res), 0) +}) From 039b0792ce2836643e8c64bd683e2501573c2816 Mon Sep 17 00:00:00 2001 From: Roberto Villegas-Diaz Date: Thu, 30 Jul 2026 12:19:02 +0100 Subject: [PATCH 14/14] Update list of bugs fixed on this patch --- NEWS.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 72308e5..8e41bee 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,9 +1,18 @@ # dsROCrate 0.2.1 +## Bug Fixes + +* Removed a duplicate, conflicting definition of `as.data.frame.safe_call()` (two files each defined this S3 method with different output columns; only one could ever take effect, silently). The `safe-call.R` version — timestamp, user, r_cmd, fx, args, session, profile — is now the sole implementation. +* Fixed `print.safe_symbol()` referencing stale `column`/`parent` fields left over from before the 0.2.0 symbol-model refactor; it now correctly displays the symbol's `asset`, `expr`, and `session` (guarding on both `NULL` and `NA`, since these fields default to `NA_character_`). +* Fixed `as.data.frame.symbol_registry()`, which previously iterated over the columns of the underlying tibble instead of its rows (via `lapply()` on a data frame), causing it to error whenever called on a non-empty registry. It also referenced non-existent `parent`/`column` fields. The method now simply returns `x$symbols` coerced to a data frame, which already carries the correct columns. +* Fixed `register_symbol()` erroring on a brand-new, empty registry (the very first symbol registered in any session): the emptiness check compared `nrow()` of a plain, pre-tibble `list()` before checking `length()`, producing a zero-length logical passed to `||`. The `length()` check is now evaluated first, so it short-circuits correctly. +* Fixed `as.data.frame.safe_call()`'s `args` column having an unstable name and shape depending on how many arguments the underlying call had (a single-argument call collapsed the column into one named after that argument, e.g. `x`; multiple arguments spread into several columns instead of one). The argument list is now wrapped with `I(list(...))` so it always appears as a single, reliably-named `args` list-column. +* Added regression tests for `safe_call()`/`as.data.frame.safe_call()`, `print.safe_symbol()`, and `register_symbol()`/`as.data.frame.symbol_registry()`, none of which previously had any test coverage. + ## Internal Changes * Removed unused internal helpers: `has_symbol()`/`has_symbol.symbol_registry()` (and its S3 export) and `safe_symbol_reference()`/`new_safe_symbol_reference()`, none of which were called anywhere in the package. -* Removed leftover commented-out code from `safe_symbol()`. +* Removed leftover commented-out code from `safe_symbol()` and `print.safe_call()`. * Added roxygen2 documentation (`@param`, `@returns`, `@keywords internal`, `@noRd`) and explanatory comments to the internal symbol-tracking functions introduced in 0.2.0 (`safe_call()`, `safe_symbol()`, `safe_reference()`, `symbol_registry()`, and their supporting utilities), with no change in behaviour. * Updated spell-check `WORDLIST`. * Re-rendered vignettes to refresh example output.