diff --git a/DESCRIPTION b/DESCRIPTION index 113d57c1..afc179c2 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: orderly2 Title: Orderly Next Generation -Version: 1.99.51 +Version: 1.99.52 Authors@R: c(person("Rich", "FitzJohn", role = c("aut", "cre"), email = "rich.fitzjohn@gmail.com"), person("Robert", "Ashton", role = "aut"), @@ -17,6 +17,7 @@ BugReports: https://github.com/mrc-ide/orderly2/issues Imports: R6, cli, + diffobj, fs, gert (>= 1.9.3), httr2, @@ -35,6 +36,7 @@ Suggests: pkgload, processx, rmarkdown, + stringr, testthat (>= 3.0.0) Config/testthat/edition: 3 Remotes: diff --git a/NAMESPACE b/NAMESPACE index 82c4d04d..281f972c 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,11 +1,15 @@ # Generated by roxygen2: do not edit by hand +S3method(format,orderly_comparison) S3method(format,orderly_query) S3method(print,orderly_cleanup_status) +S3method(print,orderly_comparison) S3method(print,orderly_query_explain) export(orderly_artefact) export(orderly_cleanup) export(orderly_cleanup_status) +export(orderly_compare_packets) +export(orderly_comparison_explain) export(orderly_config) export(orderly_config_set) export(orderly_copy_files) diff --git a/R/compare.R b/R/compare.R new file mode 100644 index 00000000..730349d0 --- /dev/null +++ b/R/compare.R @@ -0,0 +1,300 @@ +trivial_differences <- c("id", "time") +vectorized_identical <- function(left, right) { + stopifnot(length(left) == length(right)) + vlapply(seq_along(left), function(i) { + identical(left[[i]], right[[i]]) + }) +} + +is_binary_file <- function(path, n = 1024) { + vlapply(path, function(p) { + # This is a pretty crude heuristic, but it seems good enough. + # It is actually similar to what the `diff` tool does. + data <- readBin(p, "raw", n) + as.raw(0) %in% data + }) +} + +compare_filesets <- function(target, current) { + files <- merge(x = target, y = current, by = "path", all = TRUE) + status <- ifelse( + is.na(files$hash.x), "added", + ifelse(is.na(files$hash.y), "removed", + ifelse(files$hash.x == files$hash.y, "unchanged", "modified"))) + data.frame(path = files$path, status = status) +} + +##' Compare the metadata and contents of two packets. +##' +##' Insignificant differences in the metadata (eg. different dates and packet +##' IDs) are excluded from the comparison. +##' +##' @param target The id of the packet to use in the comparison. +##' @param current The id of the other packet against which to compare. +##' @inheritParams orderly_search_options +##' @inheritParams orderly_metadata +##' @return An object of class orderly_comparison. The object can be printed to +##' get a summary description of the differences, or passed to +##' [orderly2::orderly_comparison_explain] to display more details. +##' +##' @export +orderly_compare_packets <- function(target, current, + location = NULL, + allow_remote = NULL, + pull_metadata = FALSE, + root = NULL) { + root <- root_open(root, require_orderly = FALSE) + validate_outpack_id(target, call = environment()) + validate_outpack_id(current, call = environment()) + + options <- build_search_options(location = location, + allow_remote = allow_remote, + pull_metadata = pull_metadata) + + if (options$pull_metadata) { + orderly_location_pull_metadata(options$location, root = root) + } + + target_metadata <- orderly_metadata(target, root = root) + current_metdata <- orderly_metadata(current, root = root) + + ret <- list( + root = root, + target = target_metadata, + current = current_metdata, + status = compare_attribute_list(target_metadata, current_metdata), + search_options = options) + + class(ret) <- "orderly_comparison" + ret +} + +compare_files <- function(cmp, verbose) { + diff <- compare_filesets(cmp$target$files, cmp$current$files) + + modified <- diff[diff$status == "modified", ] + target_only <- diff[diff$status == "removed", ] + current_only <- diff[diff$status == "added", ] + + if (nrow(target_only) > 0) { + cli::cli_alert_info( + "The following files only exist in packet {cmp$target$id}") + cli::cli_ul(target_only$path) + } + if (nrow(current_only) > 0) { + cli::cli_alert_info( + "The following files only exist in packet {cmp$current$id}") + cli::cli_ul(current_only$path) + } + if (nrow(modified) > 0) { + if (verbose) { + compare_file_contents(cmp, modified$path) + } else { + cli::cli_alert_info(paste("The following files exist in both packets", + "but have different contents:")) + cli::cli_ul(modified$path) + + hint <- "orderly_comparison_explain(..., \"files\", verbose = TRUE)" + cli::cli_alert_info("Use {.code {hint}} to compare the files' contents.") + } + } + if (nrow(modified) == 0 && + nrow(target_only) == 0 && + nrow(current_only) == 0) { + cli::cli_alert_info("The files across the two packets are identical.") + } +} + +compare_file_contents <- function(cmp, files) { + path_target <- withr::local_tempdir() + path_current <- withr::local_tempdir() + + # Copying the files into a temporary directory is fairly wasteful and, as + # long as the packet is unpacked already, we could read the files from the + # archive or file store directly. Nevertheless this makes accessing the files + # very straightforward, and covers the case where the file only exists + # remotely transparent. + # + # pull_metadata is intentionally hardcoded to FALSE, regardless of what the + # user may have specified as an argument to orderly_compare_packets: the + # latter would have already pulled the files once, and we don't need to do it + # again. + orderly_copy_files(cmp$target$id, + dest = path_target, + files = files, + location = cmp$search_options$location, + allow_remote = cmp$search_options$allow_remote, + pull_metadata = FALSE, + root = cmp$root) + + orderly_copy_files(cmp$current$id, + dest = path_current, + files = files, + location = cmp$search_options$location, + allow_remote = cmp$search_options$allow_remote, + pull_metadata = FALSE, + root = cmp$root) + + + binary_files <- is_binary_file(file.path(path_target, files)) | + is_binary_file(file.path(path_current, files)) + + for (f in files[!binary_files]) { + cli::cli_verbatim(as.character(diffobj::diffChr( + read_file_lossy(file.path(path_target, f)), + read_file_lossy(file.path(path_current, f)), + tar.banner = file.path(cmp$target$id, f), + cur.banner = file.path(cmp$current$id, f), + rds = FALSE, + mode = "unified", + style = list(wrap = FALSE), + interactive = FALSE + ))) + } + + if (any(binary_files)) { + cli::cli_alert_warning( + paste("The following files differ across packets, but could not be", + "compared as their content is binary:")) + cli::cli_ul(files[binary_files]) + } +} + +compare_attribute <- function(cmp, attribute, verbose) { + assert_is(cmp, "orderly_comparison") + assert_scalar_character(attribute) + assert_logical(verbose) + + if (attribute == "files") { + compare_files(cmp, verbose) + } else { + cli::cli_alert_info("Comparing attribute {.code {attribute}}") + cli::cli_verbatim(as.character(diffobj::diffPrint( + cmp$target[[attribute]], + cmp$current[[attribute]], + tar.banner = sprintf("%s$%s", cmp$target$id, attribute), + cur.banner = sprintf("%s$%s", cmp$current$id, attribute), + rds = FALSE, + mode = "unified", + style = list(wrap = FALSE), + interactive = FALSE + ))) + } +} + +compare_attribute_list <- function(target, current) { + target_names <- names(target) + current_names <- names(current) + + all_names <- union(target_names, current_names) + status <- rep(NA_character_, length(all_names)) + + in_target <- all_names %in% target_names + in_current <- all_names %in% current_names + in_both <- in_target & in_current + + status[!in_target] <- "removed" + status[!in_current] <- "added" + + intersected_names <- all_names[in_both] + status[in_both] <- ifelse(vectorized_identical(target[intersected_names], + current[intersected_names]), + "identical", + "different") + + names(status) <- all_names + status +} + +##' @export +format.orderly_comparison <- function(x, ...) { + cli::cli_format_method({ + orderly_comparison_explain(x, verbose = "summary") + }) +} + +##' @export +print.orderly_comparison <- function(x, ...) { + cat(format(x, ...), sep = "\n") +} + +##' Print the details of a packet comparison. +##' +##' This function allows to select what part of the packet to compare, and in +##' how much details. +##' +##' @param cmp An orderly_comparison object, as returned by +##' [orderly2::orderly_compare_packets]. +##' @param attributes A character vector of attributes to include in the +##' comparison. The values are keys of the packets' metadata, such as +##' `parameters` or `files`. If NULL, the default, all attributes are compared, +##' except those that differ in trivial way (ie. `id` and `time`). +##' @param verbose Control over how much information is printed. It can either +##' be a logical, or a character scalar `silent` or `summary`. +##' @return Invisibly, a logical indicating whether the packets are equivalent, +##' up to the given attributes. +##' +##' @export +orderly_comparison_explain <- function(cmp, attributes = NULL, + verbose = FALSE) { + assert_is(cmp, "orderly_comparison") + if (!is.null(attributes)) { + assert_character(attributes) + } + + if (is.null(attributes)) { + status <- cmp$status[setdiff(names(cmp$status), trivial_differences)] + } else { + status <- cmp$status[intersect(attributes, names(cmp$status))] + } + + if (verbose != "silent") { + cli::cli_alert_info( + "Comparing packets {cmp$target$id} and {cmp$current$id}...") + + # All packets we produce have the same attributes. This is really more about + # future-proofing for a time where we may add new ones, and may want to + # compare a packet produced before vs after the new attributes were added. + if (any(status == "added")) { + cli::cli_alert_info( + "The following attributes only exist in packet {cmp$target$id}:") + cli::cli_ul(names(which(status == "added"))) + } + if (any(status == "removed")) { + cli::cli_alert_info( + "The following attributes only exist in packet {cmp$current$id}:") + cli::cli_ul(names(which(status == "removed"))) + } + if (any(status == "different")) { + if (verbose == "summary") { + cli::cli_alert_info( + "The following attributes are different across the two packets:") + cli::cli_ul(names(which(status == "different"))) + cli::cli_div() + cli::cli_alert_info(paste( + "Use {.code orderly_comparison_explain(...)} to examine the", + "differences in more detail.")) + cli::cli_end() + } else { + for (n in names(which(status == "different"))) { + compare_attribute(cmp, attribute = n, verbose = verbose) + } + } + } + if (all(status == "identical")) { + if (is.null(attributes)) { + if (all(cmp$status == "identical")) { + cli::cli_alert_success("The two packets are identical.") + } else { + cli::cli_alert_success( + "The two packets are equivalent, up to trivial differences.") + } + } else { + cli::cli_alert_success( + "The specified attributes are identical across the two packets.") + } + } + } + + invisible(all(status == "identical")) +} diff --git a/R/location.R b/R/location.R index cae1d2dc..a362a061 100644 --- a/R/location.R +++ b/R/location.R @@ -997,11 +997,11 @@ location_pull_files <- function(files, root) { cleanup <- function() invisible() i <- store$exists(files$hash) if (any(i)) { - cli::cli_alert_success("Found {sum(i)} file{?s} in the file store") + cli_alert_success("Found {sum(i)} file{?s} in the file store") files <- files[!i, ] } } else { - cli::cli_alert_info("Looking for suitable files already on disk") + cli_alert_info("Looking for suitable files already on disk") store <- temporary_filestore(root) cleanup <- function() store$destroy() on_disk <- vlapply(files$hash, function(hash) { @@ -1019,10 +1019,10 @@ location_pull_files <- function(files, root) { } if (nrow(files) == 0) { - cli::cli_alert_success("All files available locally, no need to fetch any") + cli_alert_success("All files available locally, no need to fetch any") } else { locations <- unique(files$location) - cli::cli_alert_info(paste( + cli_alert_info(paste( "Need to fetch {nrow(files)} file{?s} ({pretty_bytes(sum(files$size))})", "from {length(locations)} location{?s}")) for (loc in locations) { diff --git a/R/util.R b/R/util.R index 4aef209e..27e4b672 100644 --- a/R/util.R +++ b/R/util.R @@ -705,7 +705,6 @@ is_testing <- function() { identical(Sys.getenv("TESTTHAT"), "true") } - cli_alert_success <- function(..., .envir = parent.frame()) { if (!orderly_quiet()) { cli::cli_alert_success(..., .envir = .envir) @@ -739,7 +738,8 @@ cli_progress_update <- function(..., .envir = parent.frame()) { } } -# Given a character vector, missing names are filled using the value. +#' Given a character vector, missing names are filled using the value. +#' @noRd fill_missing_names <- function(x) { if (is.null(names(x))) { names(x) <- x @@ -749,7 +749,12 @@ fill_missing_names <- function(x) { x } - orderly_quiet <- function() { getOption("orderly.quiet", is_testing()) } + +#' Read a file, replacing any invalid UTF-8 characters +#' @noRd +read_file_lossy <- function(path) { + iconv(readLines(path, warn = FALSE), "UTF-8", "UTF-8", sub = "byte") +} diff --git a/_pkgdown.yml b/_pkgdown.yml index 506abbc4..52b4e586 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -91,3 +91,5 @@ reference: - orderly_hash_file - orderly_parse_file - orderly_parse_expr + - orderly_compare_packets + - orderly_comparison_explain diff --git a/man/orderly_compare_packets.Rd b/man/orderly_compare_packets.Rd new file mode 100644 index 00000000..84e427c9 --- /dev/null +++ b/man/orderly_compare_packets.Rd @@ -0,0 +1,56 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/compare.R +\name{orderly_compare_packets} +\alias{orderly_compare_packets} +\title{Compare the metadata and contents of two packets.} +\usage{ +orderly_compare_packets( + target, + current, + location = NULL, + allow_remote = NULL, + pull_metadata = FALSE, + root = NULL +) +} +\arguments{ +\item{target}{The id of the packet to use in the comparison.} + +\item{current}{The id of the other packet against which to compare.} + +\item{location}{Optional vector of locations to pull from. We +might in future expand this to allow wildcards or exceptions.} + +\item{allow_remote}{Logical, indicating if we should allow packets +to be found that are not currently unpacked (i.e., are known +only to a location that we have metadata from). If this is +\code{TRUE}, then in conjunction with \link{orderly_dependency} +you might pull a large quantity of data. The default is \code{NULL}. This is +\code{TRUE} if remote locations are listed explicitly as a character +vector in the \code{location} argument, or if you have specified +\code{pull_metadata = TRUE}, otherwise \code{FALSE}.} + +\item{pull_metadata}{Logical, indicating if we should pull +metadata immediately before the search. If \code{location} is given, +then we will pass this through to +\link{orderly_location_pull_metadata} to filter locations +to update. If pulling many packets in sequence, you \emph{will} want +to update this option to \code{FALSE} after the first pull, otherwise +it will update the metadata between every packet, which will be +needlessly slow.} + +\item{root}{The path to the root directory, or \code{NULL} (the +default) to search for one from the current working +directory. This function does not require that the directory is +configured for orderly, and can be any \code{outpack} root (see +\link{orderly_init} for details).} +} +\value{ +An object of class orderly_comparison. The object can be printed to +get a summary description of the differences, or passed to +\link{orderly_comparison_explain} to display more details. +} +\description{ +Insignificant differences in the metadata (eg. different dates and packet +IDs) are excluded from the comparison. +} diff --git a/man/orderly_comparison_explain.Rd b/man/orderly_comparison_explain.Rd new file mode 100644 index 00000000..0c84afbb --- /dev/null +++ b/man/orderly_comparison_explain.Rd @@ -0,0 +1,28 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/compare.R +\name{orderly_comparison_explain} +\alias{orderly_comparison_explain} +\title{Print the details of a packet comparison.} +\usage{ +orderly_comparison_explain(cmp, attributes = NULL, verbose = FALSE) +} +\arguments{ +\item{cmp}{An orderly_comparison object, as returned by +\link{orderly_compare_packets}.} + +\item{attributes}{A character vector of attributes to include in the +comparison. The values are keys of the packets' metadata, such as +\code{parameters} or \code{files}. If NULL, the default, all attributes are compared, +except those that differ in trivial way (ie. \code{id} and \code{time}).} + +\item{verbose}{Control over how much information is printed. It can either +be a logical, or a character scalar \code{silent} or \code{summary}.} +} +\value{ +Invisibly, a logical indicating whether the packets are equivalent, +up to the given attributes. +} +\description{ +This function allows to select what part of the packet to compare, and in +how much details. +} diff --git a/tests/testthat/_snaps/compare.md b/tests/testthat/_snaps/compare.md new file mode 100644 index 00000000..80486020 --- /dev/null +++ b/tests/testthat/_snaps/compare.md @@ -0,0 +1,181 @@ +# Comparing a packet to itself returns an empty diff + + Code + print(result) + Output + i Comparing packets 19700101-000000-00000001 and 19700101-000000-00000001... + v The two packets are identical. + +# Comparing packets ignores ID and time differences + + Code + print(result) + Output + i Comparing packets 19700101-000000-00000001 and 19700101-000000-00000002... + v The two packets are equivalent, up to trivial differences. + +# Can explicitly compare trivial fields + + Code + orderly_comparison_explain(result, "id") + Message + i Comparing packets 19700101-000000-00000001 and 19700101-000000-00000002... + i Comparing attribute `id` + < 19700101-000000-00000001$id + > 19700101-000000-00000002$id + @@ 1 / 1 @@ + < [1] "19700101-000000-00000001" + > [1] "19700101-000000-00000002" + +# Can compare packets with different metadata + + Code + print(result) + Output + i Comparing packets 19700101-000000-00000001 and 19700101-000000-00000002... + i The following attributes are different across the two packets: + * parameters + i Use `orderly_comparison_explain(...)` to examine the differences in more detail. + +--- + + Code + orderly_comparison_explain(result) + Message + i Comparing packets 19700101-000000-00000001 and 19700101-000000-00000002... + i Comparing attribute `parameters` + < 19700101-000000-00000001$parameters + > 19700101-000000-00000002$parameters + @@ 1,3 / 1,3 @@ + $A + < [1] "foo" + > [1] "bar" + + +--- + + Code + orderly_comparison_explain(result, "files") + Message + i Comparing packets 19700101-000000-00000001 and 19700101-000000-00000002... + v The specified attributes are identical across the two packets. + +# Can compare packets with different file contents + + Code + print(result) + Output + i Comparing packets 19700101-000000-00000001 and 19700101-000000-00000002... + i The following attributes are different across the two packets: + * files + i Use `orderly_comparison_explain(...)` to examine the differences in more detail. + +--- + + Code + orderly_comparison_explain(result, "files") + Message + i Comparing packets 19700101-000000-00000001 and 19700101-000000-00000002... + i The following files exist in both packets but have different contents: + * data.R + * data.txt + i Use `orderly_comparison_explain(..., "files", verbose = TRUE)` to compare the files' contents. + +--- + + Code + orderly_comparison_explain(result, "files", verbose = TRUE) + Message + i Comparing packets 19700101-000000-00000001 and 19700101-000000-00000002... + < 19700101-000000-00000001/data.R + > 19700101-000000-00000002/data.R + @@ 1 / 1 @@ + < writeLines("Hello", "data.txt") + > writeLines("World", "data.txt") + < 19700101-000000-00000001/data.txt + > 19700101-000000-00000002/data.txt + @@ 1 / 1 @@ + < Hello + > World + +# Can compare packets with binary contents + + Code + print(result) + Output + i Comparing packets 19700101-000000-00000001 and 19700101-000000-00000002... + i The following attributes are different across the two packets: + * files + i Use `orderly_comparison_explain(...)` to examine the differences in more detail. + +--- + + Code + orderly_comparison_explain(result, "files") + Message + i Comparing packets 19700101-000000-00000001 and 19700101-000000-00000002... + i The following files exist in both packets but have different contents: + * data.R + * data.rds + i Use `orderly_comparison_explain(..., "files", verbose = TRUE)` to compare the files' contents. + +--- + + Code + orderly_comparison_explain(result, "files", verbose = TRUE) + Message + i Comparing packets 19700101-000000-00000001 and 19700101-000000-00000002... + < 19700101-000000-00000001/data.R + > 19700101-000000-00000002/data.R + @@ 1,4 / 1,4 @@ + { + orderly_artefact(description = "Output", "data.rds") + < saveRDS(1:10, "data.rds") + > saveRDS(11:20, "data.rds") + } + ! The following files differ across packets, but could not be compared as their content is binary: + * data.rds + +# Can compare packets from remote + + Code + orderly_comparison_explain(result, "files") + Message + i Comparing packets 19700101-000000-00000001 and 19700101-000000-00000002... + i The following files exist in both packets but have different contents: + * data.rds + i Use `orderly_comparison_explain(..., "files", verbose = TRUE)` to compare the files' contents. + +--- + + Code + orderly_comparison_explain(result, "files", verbose = TRUE) + Message + i Comparing packets 19700101-000000-00000001 and 19700101-000000-00000002... + ! The following files differ across packets, but could not be compared as their content is binary: + * data.rds + +# Handles new attributes gracefully + + Code + print(result) + Output + i Comparing packets 19700101-000000-00000001 and 19700101-000000-00000002... + i The following attributes only exist in packet 19700101-000000-00000001: + * new_key + i The following attributes are different across the two packets: + * files + i Use `orderly_comparison_explain(...)` to examine the differences in more detail. + +--- + + Code + print(result_swap) + Output + i Comparing packets 19700101-000000-00000002 and 19700101-000000-00000001... + i The following attributes only exist in packet 19700101-000000-00000001: + * new_key + i The following attributes are different across the two packets: + * files + i Use `orderly_comparison_explain(...)` to examine the differences in more detail. + diff --git a/tests/testthat/helper-orderly.R b/tests/testthat/helper-orderly.R index 6b0c51d5..ac30bbc6 100644 --- a/tests/testthat/helper-orderly.R +++ b/tests/testthat/helper-orderly.R @@ -125,3 +125,10 @@ orderly_init_quietly <- function(...) { orderly_run_quietly <- function(..., echo = FALSE) { suppressMessages(orderly_run(..., echo = echo)) } + +orderly_run_snippet <- function(root, name, expr, ...) { + fs::dir_create(file.path(root$path, "src", name)) + writeLines(deparse(substitute(expr)), + file.path(root$path, "src", name, sprintf("%s.R", name))) + orderly_run_quietly(name, root = root, ...) +} diff --git a/tests/testthat/helper-outpack.R b/tests/testthat/helper-outpack.R index 948395eb..c17ef237 100644 --- a/tests/testthat/helper-outpack.R +++ b/tests/testthat/helper-outpack.R @@ -188,3 +188,18 @@ forcibly_truncate_file <- function(path) { fs::file_create(path) fs::file_chmod(path, permissions) } + +#' Scrub packets from an output. +#' +#' This returns a transformation suitable to be passed to `expect_snapshot`. +#' The specified packet IDs are removed from the output, and replaced with +#' stable strings of the same length. +#' +#' @param ... the list of packet IDs to remove from the output. +#' @noRd +scrub_packets <- function(...) { + ids <- c(...) + replacements <- sprintf("19700101-000000-%08x", seq_along(ids)) + names(replacements) <- ids + function(x) stringr::str_replace_all(x, replacements) +} diff --git a/tests/testthat/test-compare.R b/tests/testthat/test-compare.R new file mode 100644 index 00000000..09a3ad1c --- /dev/null +++ b/tests/testthat/test-compare.R @@ -0,0 +1,150 @@ +test_that("Comparing a packet to itself returns an empty diff", { + root <- create_temporary_root() + id <- create_random_packet(root) + + result <- orderly_compare_packets(id, id, root = root) + expect_true(orderly_comparison_explain(result, verbose = "silent")) + expect_snapshot(print(result), transform = scrub_packets(id)) +}) + + +test_that("Comparing packets ignores ID and time differences", { + root <- create_temporary_root() + id1 <- create_deterministic_packet(root) + id2 <- create_deterministic_packet(root) + + meta1 <- orderly_metadata(id1, root = root) + meta2 <- orderly_metadata(id2, root = root) + expect_false(isTRUE(all.equal(meta1$id, meta2$id))) + expect_false(isTRUE(all.equal(meta1$time, meta2$time))) + + result <- orderly_compare_packets(id1, id2, root = root) + expect_true(orderly_comparison_explain(result, verbose = "silent")) + expect_snapshot(print(result), transform = scrub_packets(id1, id2)) +}) + + +test_that("Can explicitly compare trivial fields", { + root <- create_temporary_root() + id1 <- create_deterministic_packet(root) + id2 <- create_deterministic_packet(root) + + result <- orderly_compare_packets(id1, id2, root = root) + + expect_true(orderly_comparison_explain(result, verbose = "silent")) + expect_false(orderly_comparison_explain(result, c("id", "time"), + verbose = "silent")) + + expect_snapshot(orderly_comparison_explain(result, "id"), + transform = scrub_packets(id1, id2)) +}) + + +test_that("Can compare packets with different metadata", { + root <- create_temporary_root() + + id1 <- create_deterministic_packet(root, "data", list(A = "foo")) + id2 <- create_deterministic_packet(root, "data", list(A = "bar")) + + result <- orderly_compare_packets(id1, id2, root = root) + expect_false(orderly_comparison_explain(result, verbose = "silent")) + expect_true(orderly_comparison_explain(result, "files", verbose = "silent")) + + expect_snapshot(print(result), transform = scrub_packets(id1, id2)) + expect_snapshot(orderly_comparison_explain(result), + transform = scrub_packets(id1, id2)) + expect_snapshot(orderly_comparison_explain(result, "files"), + transform = scrub_packets(id1, id2)) +}) + +test_that("Can compare packets with different file contents", { + root <- create_temporary_root() + + id1 <- orderly_run_snippet(root, "data", writeLines("Hello", "data.txt")) + id2 <- orderly_run_snippet(root, "data", writeLines("World", "data.txt")) + + result <- orderly_compare_packets(id1, id2, root = root) + + expect_false(orderly_comparison_explain(result, verbose = "silent")) + expect_false(orderly_comparison_explain(result, "files", verbose = "silent")) + expect_true(orderly_comparison_explain(result, "parameters", + verbose = "silent")) + + expect_snapshot(print(result), transform = scrub_packets(id1, id2)) + expect_snapshot(orderly_comparison_explain(result, "files"), + transform = scrub_packets(id1, id2)) + expect_snapshot(orderly_comparison_explain(result, "files", verbose = TRUE), + transform = scrub_packets(id1, id2)) +}) + +test_that("Can compare packets with binary contents", { + root <- create_temporary_root() + + id1 <- orderly_run_snippet(root, "data", { + orderly_artefact(description = "Output", "data.rds") + saveRDS(1:10, "data.rds") + }) + + id2 <- orderly_run_snippet(root, "data", { + orderly_artefact(description = "Output", "data.rds") + saveRDS(11:20, "data.rds") + }) + + result <- orderly_compare_packets(id1, id2, root = root) + + expect_false(orderly_comparison_explain(result, verbose = "silent")) + expect_false(orderly_comparison_explain(result, "files", verbose = "silent")) + expect_true(orderly_comparison_explain(result, "parameters", + verbose = "silent")) + + expect_snapshot(print(result), transform = scrub_packets(id1, id2)) + expect_snapshot(orderly_comparison_explain(result, "files"), + transform = scrub_packets(id1, id2)) + expect_snapshot(orderly_comparison_explain(result, "files", verbose = TRUE), + transform = scrub_packets(id1, id2)) +}) + +test_that("Can compare packets from remote", { + here <- create_temporary_root() + there <- create_temporary_root() + + id1 <- create_random_packet(here) + id2 <- create_random_packet(there) + + orderly_location_add("there", "path", list(path = there$path), root = here) + + expect_error( + orderly_compare_packets(id1, id2, root = here), + "Packet .* not found in outpack index") + + result <- orderly_compare_packets(id1, id2, root = here, + pull_metadata = TRUE) + + expect_snapshot(orderly_comparison_explain(result, "files"), + transform = scrub_packets(id1, id2)) + expect_snapshot(orderly_comparison_explain(result, "files", verbose = TRUE), + transform = scrub_packets(id1, id2)) +}) + + +test_that("Handles new attributes gracefully", { + root <- create_temporary_root() + + id1 <- create_random_packet(root) + id2 <- create_random_packet(root) + + # This is a horrible hack, designed to mimick the day where we may add new + # attributes to the metadata. + f <- file.path(root$path, ".outpack", "metadata", id1) + metadata <- parse_json(file(f)) + metadata$new_key <- "value" + writeLines(to_json(metadata, auto_unbox = TRUE), f) + + result <- orderly_compare_packets(id1, id2, root = root) + expect_snapshot(print(result), + transform = scrub_packets(id1, id2)) + + result_swap <- orderly_compare_packets(id2, id1, root = root) + expect_snapshot(print(result_swap), + transform = scrub_packets(id1, id2)) +}) diff --git a/tests/testthat/test-location.R b/tests/testthat/test-location.R index 73c91b4a..17809600 100644 --- a/tests/testthat/test-location.R +++ b/tests/testthat/test-location.R @@ -1063,11 +1063,13 @@ test_that("skip files in the file store", { orderly_location_pull_metadata(root = root$dst) suppressMessages(orderly_location_pull_packet(id[[1]], root = root$dst)) - res <- testthat::evaluate_promise( - orderly_location_pull_packet(id[[2]], root = root$dst)) - expect_match(res$messages, "Found 1 file in the file store", all = FALSE) - expect_match(res$messages, "Need to fetch 2 files.+from 1 location", - all = FALSE) + withr::with_options(list(orderly.quiet = FALSE), { + res <- testthat::evaluate_promise( + orderly_location_pull_packet(id[[2]], root = root$dst)) + expect_match(res$messages, "Found 1 file in the file store", all = FALSE) + expect_match(res$messages, "Need to fetch 2 files.+from 1 location", + all = FALSE) + }) }) @@ -1082,11 +1084,13 @@ test_that("skip files known elsewhere on disk", { orderly_location_pull_metadata(root = root$dst) suppressMessages(orderly_location_pull_packet(id[[1]], root = root$dst)) - res <- testthat::evaluate_promise( - orderly_location_pull_packet(id[[2]], root = root$dst)) - expect_match(res$messages, "Found 1 file on disk", all = FALSE) - expect_match(res$messages, "Need to fetch 2 files.+from 1 location", - all = FALSE) + withr::with_options(list(orderly.quiet = FALSE), { + res <- testthat::evaluate_promise( + orderly_location_pull_packet(id[[2]], root = root$dst)) + expect_match(res$messages, "Found 1 file on disk", all = FALSE) + expect_match(res$messages, "Need to fetch 2 files.+from 1 location", + all = FALSE) + }) })