tests/testthat/test-location.R

test_that("No locations except local by default", {
  root <- create_temporary_root()
  expect_equal(orderly_location_list(root = root), "local")
  expect_equal(
    orderly_location_list(TRUE, root = root),
    data_frame(name = "local",
               type = "local",
               args = I(list(set_names(list(), character())))))
})


test_that("Can add a location", {
  root <- list()
  for (name in c("a", "b", "c")) {
    root[[name]] <- create_temporary_root()
  }

  orderly_location_add_path("b", path = root$b$path, root = root$a)
  expect_setequal(orderly_location_list(root = root$a), c("local", "b"))

  orderly_location_add_path("c", path = root$c$path, root = root$a)
  expect_setequal(orderly_location_list(root = root$a), c("local", "b", "c"))

  res <- orderly_location_list(verbose = TRUE, root = root$a)
  expect_equal(res$name, c("local", "b", "c"))
  expect_equal(res$type, c("local", "path", "path"))
  expect_equal(res$args, I(list(set_names(list(), character()),
                                list(path = root$b$path),
                                list(path = root$c$path))))
})


test_that("Can't add a location with reserved name", {
  root <- create_temporary_root()
  upstream <- create_temporary_root()

  expect_error(
    orderly_location_add_path("local", path = upstream$path, root = root),
    "Cannot add a location with reserved name 'local'")
})


test_that("Can't add a location with existing name", {
  root <- list()
  for (name in c("a", "b", "c")) {
    root[[name]] <- create_temporary_root()
  }

  orderly_location_add_path("upstream", path = root$b$path, root = root$a)
  expect_error(
    orderly_location_add_path("upstream", path = root$c$path, root = root$a),
    "A location with name 'upstream' already exists")
  expect_equal(orderly_location_list(root = root$a),
               c("local", "upstream"))
})


test_that("Require that (for now) locations must be paths", {
  root <- create_temporary_root()
  expect_equal(orderly_location_list(root = root), "local")

  other <- temp_file()
  expect_error(
    orderly_location_add_path("other", other, root = root),
    "Directory does not exist:")
  fs::dir_create(other)
  expect_error(
    orderly_location_add_path("other", other, root = root),
    "Did not find existing orderly (or outpack) root in",
    fixed = TRUE)
})


test_that("Can rename a location", {
  root <- list()
  for (name in c("a", "b")) {
    root[[name]] <- create_temporary_root()
  }

  orderly_location_add_path("b", path = root$b$path, root = root$a)
  expect_setequal(orderly_location_list(root = root$a), c("local", "b"))

  orderly_location_rename("b", "c", root = root$a)
  expect_setequal(orderly_location_list(root = root$a), c("local", "c"))
  expect_setequal(orderly_config(root$a)$location$name, c("local", "c"))
})


test_that("Can't rename a location using an existent name", {
  root <- list()
  for (name in c("a", "b", "c")) {
    root[[name]] <- create_temporary_root()
  }

  orderly_location_add_path("b", path = root$b$path, root = root$a)
  orderly_location_add_path("c", path = root$c$path, root = root$a)

  expect_error(orderly_location_rename("b", "c", root$a),
               "A location with name 'c' already exists")
  expect_error(orderly_location_rename("b", "local", root$a),
               "A location with name 'local' already exists")
})


test_that("Can't rename a  non-existent location", {
  root <- create_temporary_root()
  expect_equal(orderly_location_list(root = root), "local")

  expect_error(orderly_location_rename("a", "b", root),
               "No location with name 'a' exists")
})


test_that("Can't rename default locations", {
  root <- create_temporary_root()

  expect_error(orderly_location_rename("local", "desktop", root),
               "Cannot rename default location 'local'")
  expect_error(orderly_location_rename("orphan", "removed", root),
               "Cannot rename default location 'orphan'")
})


test_that("Can remove a location", {
  root <- list()
  for (name in c("a", "b", "c")) {
    root[[name]] <- create_temporary_root()
  }

  orderly_location_add_path("b", path = root$b$path, root = root$a)
  orderly_location_add_path("c", path = root$c$path, root = root$a)
  expect_setequal(orderly_location_list(root = root$a), c("local", "b", "c"))

  id <- create_random_packet(root$b)
  orderly_location_fetch_metadata(root = root$a)

  # remove a location without packets
  expect_silent(orderly_location_remove("c", root = root$a))
  expect_setequal(orderly_location_list(root = root$a),
                  c("local", "b"))

  # remove a location with packets
  expect_message(orderly_location_remove("b", root = root$a),
                 "Orphaning 1 packet")
  expect_setequal(orderly_location_list(root = root$a),
                  c("local", "orphan"))

  config <- orderly_config(root$a)
  expect_equal(root$a$index$data()$location$location, "orphan")
})


test_that("Removing a location orphans packets only from that location", {
  root <- list()
  for (name in c("a", "b", "c")) {
    root[[name]] <- create_temporary_root()
  }

  orderly_location_add_path("c", path = root$c$path, root = root$b)
  orderly_location_add_path("b", path = root$b$path, root = root$a)
  orderly_location_add_path("c", path = root$c$path, root = root$a)
  expect_setequal(orderly_location_list(root = root$a), c("local", "b", "c"))
  expect_setequal(orderly_location_list(root = root$b), c("local", "c"))

  id1 <- create_random_packet(root$c)
  id2 <- create_random_packet(root$b)
  orderly_location_fetch_metadata(root = root$b)
  suppressMessages(orderly_location_pull(id1, root = root$b))
  orderly_location_fetch_metadata(root = root$a)

  # id1 should now be found in both b and c
  index <- root$a$index$data()
  expect_equal(index$location$location[index$location$packet == id1],
               c("b", "c"))

  # id2 should just be found in b
  expect_equal(index$location$location[index$location$packet == id2], "b")

  # remove location b
  expect_message(
    orderly_location_remove("b", root = root$a),
    "Orphaning 1 packet")
  expect_setequal(orderly_location_list(root = root$a),
                  c("local", "orphan", "c"))

  # id1 should now only be found in c
  index <- root$a$index$data()
  expect_equal(index$location$location[index$location$packet == id1], "c")

  # id2 should be orphaned
  expect_equal(index$location$location[index$location$packet == id2], "orphan")
})


test_that("re-adding a location de-orphans packets", {
  root <- list()
  for (name in c("a", "b", "c")) {
    root[[name]] <- create_temporary_root()$path
  }

  orderly_location_add_path("b", path = root$b, root = root$a)
  orderly_location_add_path("c", path = root$c, root = root$a)

  id_b <- replicate(2, create_random_packet(root$b))
  id_c <- replicate(3, create_random_packet(root$c))
  orderly_location_fetch_metadata(root = root$a)

  expect_message(orderly_location_remove("b", root = root$a),
                 "Orphaning 2 packets")
  expect_equal(nrow(root_open(root$a, FALSE)$index$location(orphan)), 2)
  expect_message(orderly_location_remove("c", root = root$a),
                 "Orphaning 3 packets")
  expect_equal(nrow(root_open(root$a, FALSE)$index$location(orphan)), 5)

  orderly_location_add_path("b", path = root$b, root = root$a)
  expect_message(orderly_location_fetch_metadata(root = root$a),
                 "De-orphaning 2 packets")

  expect_equal(nrow(root_open(root$a, FALSE)$index$location(orphan)), 3)
})


test_that("Can't remove default locations", {
  root <- create_temporary_root()

  expect_error(orderly_location_remove("local",  root),
               "Cannot remove default location 'local'")
  expect_error(orderly_location_remove("orphan", root),
               "Cannot remove default location 'orphan'")
})


test_that("Can't remove non-existent location", {
  root <- create_temporary_root()

  expect_error(orderly_location_remove("b",  root),
               "No location with name 'b' exists")
})


test_that("can pull metadata from a file base location", {
  root_upstream <- create_temporary_root(use_file_store = TRUE)

  ids <- vcapply(1:3, function(i) create_random_packet(root_upstream$path))
  root_downstream <- create_temporary_root(use_file_store = TRUE)

  orderly_location_add_path("upstream", path = root_upstream$path,
                            root = root_downstream)
  expect_equal(orderly_location_list(root = root_downstream),
               c("local", "upstream"))

  orderly_location_fetch_metadata("upstream", root = root_downstream)

  ## Sensible tests here will be much easier to write once we have a
  ## decent query interface.
  index <- root_downstream$index$data()
  expect_length(index$metadata, 3)
  expect_setequal(names(index$metadata), ids)
  expect_mapequal(index$metadata, root_upstream$index$data()$metadata)

  expect_s3_class(index$location, "data.frame")
  expect_setequal(index$location$packet, ids)
  expect_equal(index$location$location, rep("upstream", 3))
})


test_that("can pull empty metadata", {
  root_upstream <- create_temporary_root(use_file_store = TRUE)
  root_downstream <- create_temporary_root(use_file_store = TRUE)

  orderly_location_add_path("upstream", path = root_upstream$path,
                            root = root_downstream)
  orderly_location_fetch_metadata("upstream", root = root_downstream)

  index <- root_downstream$index$data()
  expect_length(index$metadata, 0)
  ## This is what we need to improve, everywhere
  expect_s3_class(index$location, "data.frame")
})


test_that("pull metadata from subset of locations", {
  root <- list()
  root$a <- create_temporary_root(use_file_store = TRUE)
  for (name in c("x", "y", "z")) {
    root[[name]] <- create_temporary_root(use_file_store = TRUE)
    orderly_location_add_path(name, path = root[[name]]$path, root = root$a)
  }

  expect_equal(orderly_location_list(root = root$a),
               c("local", "x", "y", "z"))

  ## NOTE: This is a little slow (0.2s) with about half of that coming
  ## from the call to utils::sessionInfo which gets bogged down
  ## reading DESCRIPTION files from disk - we might be better off
  ## replacing that with something a bit simpler. Also seeing some
  ## bottlenecks coming potentially from fs (fs::dir_create - looks
  ## like a known bug)
  ids <- list()
  for (name in c("x", "y", "z")) {
    ids[[name]] <- vcapply(1:3, function(i) create_random_packet(root[[name]]))
  }

  location_name <- c("x", "y", "z")

  orderly_location_fetch_metadata(c("x", "y"), root = root$a)
  index <- root$a$index$data()
  expect_setequal(names(index$metadata), c(ids$x, ids$y))
  expect_equal(index$location$location, rep(location_name[1:2], each = 3))
  expect_equal(index$metadata[ids$x],
               root$x$index$data()$metadata)
  expect_equal(index$metadata[ids$y],
               root$y$index$data()$metadata)

  orderly_location_fetch_metadata(root = root$a)
  index <- root$a$index$data()
  expect_setequal(names(index$metadata), c(ids$x, ids$y, ids$z))
  expect_equal(index$location$location, rep(location_name, each = 3))
  expect_equal(index$metadata[ids$z],
               root$z$index$data()$metadata)
})


test_that("Can't pull metadata from an unknown location", {
  root <- create_temporary_root()
  expect_error(
    orderly_location_fetch_metadata("upstream", root = root),
    "Unknown location: 'upstream'")
})


test_that("No-op to pull metadata from no locations", {
  root <- create_temporary_root()
  expect_silent(orderly_location_fetch_metadata("local", root = root))
  expect_silent(orderly_location_fetch_metadata(root = root))
})


test_that("Can pull metadata through chain of locations", {
  root <- list()
  for (name in c("a", "b", "c", "d")) {
    root[[name]] <- create_temporary_root()
  }

  ## More interesting topology, with a chain of locations, but d also
  ## knowing directly about an earlier location
  ## > a -> b -> c -> d
  ## >      `--------/
  orderly_location_add_path("a", path = root$a$path, root = root$b)
  orderly_location_add_path("b", path = root$b$path, root = root$c)
  orderly_location_add_path("b", path = root$b$path, root = root$d)
  orderly_location_add_path("c", path = root$c$path, root = root$d)

  ## Create a packet and make sure it's in both b and c
  id1 <- create_random_packet(root$a)
  orderly_location_fetch_metadata(root = root$b)
  suppressMessages(orderly_location_pull(id1, root = root$b))
  orderly_location_fetch_metadata(root = root$c)
  suppressMessages(orderly_location_pull(id1, root = root$c))

  ## And another in just 'c'
  id2 <- create_random_packet(root$c)

  ## Then when we pull from d it will simultaneously learn about the
  ## packet from both locations:
  orderly_location_fetch_metadata(root = root$d)
  index <- root$d$index$data()

  ## Metadata is correct
  expect_length(index$metadata, 2)
  expect_equal(names(index$metadata), c(id1, id2))
  expect_equal(index$metadata, root$c$index$data()$metadata)

  ## Location information contains both sources
  expect_equal(nrow(index$location), 3)
  expect_equal(index$location$packet, c(id1, id1, id2))

  expect_equal(index$location$location, c("b", "c", "c"))
})


test_that("can pull a packet from one location to another, using file store", {
  root <- list()
  for (name in c("src", "dst")) {
    root[[name]] <- create_temporary_root(use_file_store = TRUE)
  }

  id <- create_random_packet(root$src)
  orderly_location_add_path("src", path = root$src$path, root = root$dst)
  orderly_location_fetch_metadata(root = root$dst)
  suppressMessages(orderly_location_pull(id, root = root$dst))

  index <- root$dst$index$data()
  expect_equal(index$unpacked, id)
  expect_true(file.exists(
    file.path(root$dst$path, "archive", "data", id, "data.rds")))
  meta <- outpack_metadata_core(id, root$dst)
  expect_true(all(root$dst$files$exists(meta$files$hash)))
})


test_that("can error where a query returns no packets", {
  root <- list()
  for (name in c("src", "dst")) {
    root[[name]] <- create_temporary_root()
  }

  id <- create_random_packet(root$src)
  orderly_location_add_path("src", path = root$src$path, root = root$dst)
  expect_error(
    orderly_location_pull(NULL, name = "data", root = root$dst),
    "No packets found in query, so cannot pull anything")
  expect_error(
    orderly_location_pull("latest", name = "data", root = root$dst),
    "No packets found in query, so cannot pull anything")
})


test_that("can pull a packet from one location to another, archive only", {
  root <- list()
  for (name in c("src", "dst")) {
    root[[name]] <- create_temporary_root()
  }

  id <- create_random_packet(root$src)
  orderly_location_add_path("src", path = root$src$path, root = root$dst)
  orderly_location_fetch_metadata(root = root$dst)
  suppressMessages(orderly_location_pull(id, root = root$dst))

  index <- root$dst$index$data()
  expect_equal(index$unpacked, id)
  expect_true(file.exists(
    file.path(root$dst$path, "archive", "data", id, "data.rds")))
})


test_that("detect and avoid modified files in source repository", {
  root <- list()
  for (name in c("src", "dst")) {
    root[[name]] <- create_temporary_root()
  }

  tmp <- fs::dir_create(temp_file())

  saveRDS(runif(10), file.path(tmp, "a.rds"))
  saveRDS(runif(10), file.path(tmp, "b.rds"))
  id <- character(2)
  for (i in seq_along(id)) {
    p <- outpack_packet_start_quietly(tmp, "data", root = root$src)
    outpack_packet_end_quietly(p)
    id[[i]] <- p$id
  }

  orderly_location_add_path("src", path = root$src$path, root = root$dst)
  orderly_location_fetch_metadata(root = root$dst)

  ## Corrupt the file in the first id by truncating it:
  forcibly_truncate_file(
    file.path(root$src$path, "archive", "data", id[[1]], "a.rds"))

  ## Then pull
  res <- testthat::evaluate_promise(
    orderly_location_pull(id[[1]], root = root$dst))

  expect_match(res$messages, "Rejecting file from archive 'a.rds' in 'data/",
               all = FALSE)

  expect_equal(
    hash_file(file.path(root$dst$path, "archive", "data", id[[1]], "a.rds")),
    hash_file(file.path(root$src$path, "archive", "data", id[[2]], "a.rds")))
  expect_equal(
    hash_file(file.path(root$dst$path, "archive", "data", id[[1]], "b.rds")),
    hash_file(file.path(root$src$path, "archive", "data", id[[2]], "b.rds")))
})


test_that("Do not unpack a packet twice", {
  root <- list()
  for (name in c("src", "dst")) {
    root[[name]] <- create_temporary_root()
  }

  id <- create_random_packet(root$src)
  orderly_location_add_path("src", path = root$src$path, root = root$dst)
  orderly_location_fetch_metadata(root = root$dst)
  expect_equal(
    suppressMessages(orderly_location_pull(id, root = root$dst)),
    id)

  expect_equal(
    suppressMessages(orderly_location_pull(id, root = root$dst)),
    character(0))
})


test_that("Sensible error if packet not known", {
  root <- list()
  for (name in c("src", "dst")) {
    root[[name]] <- create_temporary_root()
  }

  id <- create_random_packet(root$src)
  orderly_location_add_path("src", path = root$src$path, root = root$dst)
  err <- expect_error(
    suppressMessages(orderly_location_pull(id, root = root$dst)),
    sprintf("Failed to find packet '%s'", id),
    fixed = TRUE)
  expect_match(err$body[[1]], "Looked in location 'src'")
  expect_match(err$body[[2]],
               "Do you need to run.+orderly_location_fetch_metadata")
})


test_that("Sensible error if dependent packet not known", {
  root <- list()
  for (name in c("a", "b", "c")) {
    root[[name]] <- create_temporary_root(require_complete_tree = name != "b")
  }

  id <- create_random_packet_chain(root$a, 5)
  orderly_location_add_path("a", path = root$a$path, root = root$b)
  orderly_location_fetch_metadata(root = root$b)
  suppressMessages(orderly_location_pull(id[[5]], root = root$b))

  orderly_location_add_path("b", path = root$b$path,
                       root = root$c)
  orderly_location_fetch_metadata(root = root$c)

  err <- expect_error(
    suppressMessages(orderly_location_pull(id[[5]], root = root$c)),
    sprintf("Failed to find packet '%s'", id[[4]]))
  ## This needs work. The shoddy pluralisation is the least of the
  ## issue, see mrc-4513; however, this situation is rare in most
  ## likely uses.
  expect_equal(
    err$body,
    c(i = "Looked in location 'b'",
      i = paste("1 missing packets were requested as dependencies of",
                sprintf("the ones you asked for: '%s'", id[[4]]))))
})


test_that("Can pull a tree recursively", {
  root <- list()
  for (name in c("src", "dst")) {
    root[[name]] <- create_temporary_root()
  }

  ## This just does a simple graph a -> b -> c
  id <- as.list(create_random_packet_chain(root$src, 3))

  orderly_location_add_path("src", path = root$src$path, root = root$dst)
  orderly_location_fetch_metadata(root = root$dst)
  expect_equal(suppressMessages(
    orderly_location_pull(id$c, recursive = TRUE, root = root$dst)),
    c(id$a, id$b, id$c))

  index <- root$dst$index$data()
  expect_equal(index$unpacked,
               root$src$index$data()$unpacked)

  expect_equal(suppressMessages(
    orderly_location_pull(id$c, recursive = TRUE, root = root$dst)),
    character(0))
})


test_that("Can resolve locations", {
  root <- list()
  for (name in c("dst", "a", "b", "c", "d")) {
    root[[name]] <- create_temporary_root()
    if (name != "dst") {
      orderly_location_add_path(name, path = root[[name]]$path, root = root$dst)
    }
  }

  expect_equal(
    location_resolve_valid(NULL, root$dst, FALSE, FALSE, FALSE),
    c("a", "b", "c", "d"))
  expect_equal(
    location_resolve_valid(NULL, root$dst, TRUE, FALSE, FALSE),
    c("local", "a", "b", "c", "d"))
  expect_equal(
    location_resolve_valid(NULL, root$dst, TRUE, TRUE, FALSE),
    c("local", "a", "b", "c", "d"))
  expect_equal(
    location_resolve_valid(c("a", "b", "local", "d"), root$dst,
                           FALSE, FALSE, FALSE),
    c("a", "b", "d"))
  expect_equal(
    location_resolve_valid(c("a", "b", "local", "d"), root$dst,
                           TRUE, FALSE, FALSE),
    c("a", "b", "local", "d"))

  expect_error(
    location_resolve_valid(TRUE, root$dst, TRUE, FALSE, FALSE),
    "Invalid input for 'location'; expected NULL or a character vector")

  expect_error(
    location_resolve_valid("other", root$dst, TRUE, FALSE, FALSE),
    "Unknown location: 'other'")
  expect_error(
    location_resolve_valid(c("a", "b", "f", "g"), root$dst, TRUE, FALSE, FALSE),
    "Unknown locations: 'f' and 'g'")
})


test_that("informative error message when no locations configured", {
  root <- create_temporary_root()
  expect_equal(
    location_resolve_valid(NULL, root, FALSE, FALSE, TRUE),
    character(0))
  expect_error(
    location_resolve_valid(NULL, root, FALSE, FALSE, FALSE),
    "No suitable location found")
  expect_error(
    orderly_location_pull(outpack_id(), root = root),
    "No suitable location found")
})


test_that("Can filter locations", {
  root <- list()
  for (name in c("dst", "a", "b", "c", "d")) {
    root[[name]] <- create_temporary_root()
    if (name != "dst") {
      orderly_location_add_path(name, path = root[[name]]$path, root = root$dst)
    }
  }

  ids_a <- vcapply(1:3, function(i) create_random_packet(root$a$path))
  orderly_location_add_path("a", path = root$a$path, root = root$b)
  orderly_location_fetch_metadata(root = root$b)
  suppressMessages(orderly_location_pull(ids_a, root = root$b))

  ids_b <- c(ids_a,
             vcapply(1:3, function(i) create_random_packet(root$b$path)))
  ids_c <- vcapply(1:3, function(i) create_random_packet(root$c$path))
  orderly_location_add_path("a", path = root$a$path, root = root$d)
  orderly_location_add_path("c", path = root$c$path, root = root$d)
  orderly_location_fetch_metadata(root = root$d)
  suppressMessages(orderly_location_pull(ids_a, root = root$d))
  suppressMessages(orderly_location_pull(ids_c, root = root$d))
  ids_d <- c(ids_c,
             vcapply(1:3, function(i) create_random_packet(root$d$path)))

  orderly_location_fetch_metadata(root = root$dst)

  ids <- unique(c(ids_a, ids_b, ids_c, ids_d))
  expected <- function(ids, location_name) {
    data_frame(packet = ids,
               location = location_name)
  }
  locs <- function(location) {
    location_resolve_valid(location, root$dst,
                           include_local = FALSE,
                           include_orphan = FALSE,
                           allow_no_locations = FALSE)
  }

  plan <- location_build_pull_plan(ids, NULL, NULL, root = root$dst)
  expect_equal(plan$files$location, rep(c("a", "b", "c", "d"), each = 3))

  ## Invert order, now prefers 'd'
  plan <- location_build_pull_plan(ids, locs(c("d", "c", "b", "a")), NULL,
                                   root = root$dst)
  expect_equal(plan$files$location, rep(c("d", "b"), c(9, 3)))

  ## Drop redundant locations
  plan <- location_build_pull_plan(ids, locs(c("b", "d")), NULL,
                                   root = root$dst)
  expect_equal(plan$files$location, rep(c("b", "d"), each = 6))

  ## Some corner cases:
  plan <- location_build_pull_plan(ids_a[[1]], NULL, NULL, root = root$dst)
  expect_equal(plan$files$location, "a")
  plan <- location_build_pull_plan(character(), NULL, NULL, root = root$dst)
  expect_equal(
    plan,
    list(packet_id = character(),
         files = data_frame(hash = character(),
                            size = numeric(),
                            location = character()),
         hash = set_names(character(), character()),
         info = list(n_extra = 0, n_skip = 0, n_total = 0)))

  ## Failure to find things:
  err <- expect_error(
    location_build_pull_plan(ids, c("a", "b", "c"), NULL, root = root$dst),
    "Failed to find packets")
  expect_match(err$body[[1]], "Looked in locations 'a', 'b', and 'c'")
  expect_match(err$body[[2]],
               "Do you need to run.+orderly_location_fetch_metadata")
})


test_that("can pull from multiple locations with multiple files", {
  root <- list()
  for (name in c("dst", "a", "b")) {
    root[[name]] <- create_temporary_root()
    if (name != "dst") {
      orderly_location_add_path(name, path = root[[name]]$path, root = root$dst)
    }
  }

  ids_a <- create_random_packet(root$a$path, n_files = 1)
  ids_b <- create_random_packet(root$b$path, n_files = 2)

  orderly_location_fetch_metadata(root = root$dst)
  suppressMessages(
    orderly_location_pull(NULL, name = "data", root = root$dst))

  ## It has pulled both packets, and correct number of files
  expect_setequal(
    list.files(file.path(root$dst$path, "archive", "data")),
    c(ids_a, ids_b))
  expect_equal(
    list.files(file.path(root$dst$path, "archive", "data", ids_a)),
    "data.rds")
  expect_setequal(
    list.files(file.path(root$dst$path, "archive", "data", ids_b)),
    c("data.rds", "data2.rds"))
})


test_that("nonrecursive pulls are prevented by configuration", {
  root <- list()
  for (name in c("src", "dst")) {
    root[[name]] <- create_temporary_root(require_complete_tree = TRUE)
  }

  id <- create_random_packet_chain(root$src, 3)

  expect_error(
    orderly_location_pull(id[["c"]], recursive = FALSE, root = root$dst),
    "'recursive' must be TRUE (or NULL) with your configuration",
    fixed = TRUE)
})


test_that("if recursive pulls are required, pulls are recursive by default", {
  root <- list()
  for (name in c("src", "shallow", "deep")) {
    root[[name]] <- create_temporary_root(
      require_complete_tree = name == "deep")
  }

  id <- create_random_packet_chain(root$src, 3)

  for (r in root[c("shallow", "deep")]) {
    orderly_location_add_path("src", path = root$src$path, root = r)
    orderly_location_fetch_metadata(root = r)
  }

  suppressMessages(
    orderly_location_pull(id[["c"]], recursive = NULL,
                                 root = root$shallow))
  expect_equal(root$shallow$index$data()$unpacked, id[["c"]])

  suppressMessages(
    orderly_location_pull(id[["c"]], recursive = NULL,
                                 root = root$deep))
  expect_setequal(root$deep$index$data()$unpacked, id)
})


test_that("can't add unknown location type", {
  root <- create_temporary_root()
  expect_error(
    orderly_location_add("other", "magic", list(arg = 1), root = root),
    "'type' must be one of 'path', 'http'")
})


test_that("validate arguments to path locations", {
  root <- create_temporary_root()
  expect_error(
    orderly_location_add("other", "path", list(root = "mypath"),
                         root = root),
    "'path' must be a scalar")
  expect_equal(orderly_location_list(root = root), "local")
})


test_that("validate arguments to http locations", {
  root <- create_temporary_root()
  expect_error(
    orderly_location_add("other", "http", list(server = "example.com"),
                         root = root),
    "'url' must be a scalar")
  expect_equal(orderly_location_list(root = root), "local")
})


test_that("validate arguments to packit locations", {
  root <- create_temporary_root()
  expect_error(
    orderly_location_add("other", "packit", list(server = "example.com"),
                         root = root),
    "'url' must be a scalar")

  expect_error(
    orderly_location_add_packit("other",
                                url = "example.com",
                                token = 123,
                                verify = FALSE,
                                root = root),
    "Expected 'token' to be character", fixed = TRUE)

  expect_error(
    orderly_location_add_packit("other",
                                url = "example.com",
                                save_token = "value",
                                verify = FALSE,
                                root = root),
    "Expected 'save_token' to be logical", fixed = TRUE)

  expect_error(
    orderly_location_add_packit("other",
                                url = "example.com",
                                token = "xx",
                                save_token = TRUE,
                                verify = FALSE,
                                root = root),
    "Cannot specify both 'token' and 'save_token'", fixed = TRUE)

  expect_equal(orderly_location_list(root = root), "local")
})


test_that("can add a packit location", {
  skip_if_not_installed("mockery")
  root <- create_temporary_root()
  orderly_location_add_packit("other",
                              url = "https://example.com",
                              token = "abc123",
                              verify = FALSE,
                              root = root)
  expect_equal(orderly_location_list(root = root), c("local", "other"))

  mock_driver <- mockery::mock()
  mockery::stub(location_driver, "location_driver_create", mock_driver)

  dr <- location_driver("other", root)

  mockery::expect_called(mock_driver, 1)
  expect_equal(
    mockery::mock_args(mock_driver)[[1]],
    list("packit",
         list(url = "https://example.com",
              token = "abc123",
              save_token = FALSE),
         root))
})

test_that("can add a packit location without a token", {
  skip_if_not_installed("mockery")
  root <- create_temporary_root()
  orderly_location_add_packit("other",
                              url = "https://example.com",
                              verify = FALSE,
                              root = root)
  expect_equal(
    orderly_config(root)$location$args[[2]],
    list(url = "https://example.com", token = NULL, save_token = TRUE))
  expect_equal(orderly_location_list(root = root), c("local", "other"))

  mock_driver <- mockery::mock()
  mockery::stub(location_driver, "location_driver_create", mock_driver)

  dr <- location_driver("other", root)

  mockery::expect_called(mock_driver, 1)
  expect_equal(
    mockery::mock_args(mock_driver)[[1]],
    list("packit",
         list(url = "https://example.com", token = NULL, save_token = TRUE),
         root))
})

test_that("cope with trailing slash in url if needed", {
  loc <- orderly_location_packit("https://example.com/", "abc123")
  expect_equal(loc$client$url, "https://example.com/api/outpack")
})


test_that("can create an outpack location", {
  loc <- orderly_location_http$new("https://example.com", NULL)
  expect_equal(loc$client$url, "https://example.com")
})


test_that("strip trailing slash from outpack url", {
  loc <- orderly_location_http$new("https://example.com/", NULL)
  expect_equal(loc$client$url, "https://example.com")
})


test_that("can load a custom location driver", {
  skip_if_not_installed("mockery")
  mock_driver <- mockery::mock("value")
  mock_gev <- mockery::mock(mock_driver)
  mockery::stub(orderly_location_custom, "getExportedValue", mock_gev)
  expect_equal(orderly_location_custom(driver = "foo::bar", a = 1, b = "other"),
               "value")

  mockery::expect_called(mock_gev, 1)
  expect_equal(mockery::mock_args(mock_gev)[[1]], list("foo", "bar"))

  mockery::expect_called(mock_driver, 1)
  expect_equal(mockery::mock_args(mock_driver)[[1]], list(a = 1, b = "other"))
})


test_that("can load a custom location driver using an R6 generator", {
  skip_if_not_installed("mockery")
  mock_driver <- structure(
    list(new = mockery::mock("value")),
    class = "R6ClassGenerator")
  mock_gev <- mockery::mock(mock_driver)
  mockery::stub(orderly_location_custom, "getExportedValue", mock_gev)
  expect_equal(orderly_location_custom(driver = "foo::bar", a = 1, b = "other"),
               "value")

  mockery::expect_called(mock_gev, 1)
  expect_equal(mockery::mock_args(mock_gev)[[1]], list("foo", "bar"))

  mockery::expect_called(mock_driver$new, 1)
  expect_equal(mockery::mock_args(mock_driver$new)[[1]],
               list(a = 1, b = "other"))
})


test_that("can add a custom outpack location", {
  skip_if_not_installed("mockery")
  root <- create_temporary_root()
  args <- list(driver = "foo::bar", a = 1, b = 2)
  orderly_location_add("a", "custom", args = args, verify = FALSE, root = root)

  loc <- as.list(root$config$location[2, ])
  expect_equal(loc$name, "a")
  expect_equal(loc$type, "custom")
  expect_equal(loc$args[[1]], list(driver = "foo::bar", a = 1, b = 2))

  mock_orderly_location_driver_create <- mockery::mock("value")
  mockery::stub(location_driver, "location_driver_create",
                mock_orderly_location_driver_create)
  expect_equal(location_driver(loc$name, root), "value")
  mockery::expect_called(mock_orderly_location_driver_create, 1)
  expect_equal(mockery::mock_args(mock_orderly_location_driver_create)[[1]],
               list("custom", list(driver = "foo::bar", a = 1, b = 2), root))
})


test_that("custom drivers require a 'driver' argument", {
  root <- create_temporary_root()
  expect_error(
    orderly_location_add("a", "custom", args = list(), root = root),
    "Field missing from args: 'driver'")
})


test_that("can pull packets as a result of a query", {
  root <- list()
  for (name in c("src", "dst")) {
    root[[name]] <- create_temporary_root(use_file_store = TRUE)
  }
  ids <- vcapply(1:3, function(i) {
    create_random_packet(root$src$path, parameters = list(i = i))
  })
  orderly_location_add_path("src", path = root$src$path, root = root$dst$path)
  ids_moved <- suppressMessages(
    orderly_location_pull(
      "parameter:i < 3",
      name = "data",
      fetch_metadata = TRUE,
      root = root$dst$path))
  expect_setequal(ids_moved, ids[1:2])
})


test_that("handle metadata where the hash does not match reported", {
  here <- create_temporary_root()
  there <- create_temporary_root()
  orderly_location_add_path("server", path = there$path, root = here)
  id <- create_random_packet(there)

  path_metadata <- file.path(there$path, ".outpack", "metadata", id)
  json <- jsonlite::prettify(read_string(path_metadata))
  writeLines(json, path_metadata)

  err <- expect_error(
    orderly_location_fetch_metadata(root = here),
    "Hash of metadata for '.+' from 'server' does")
  expect_equal(
    unname(err$message),
    sprintf("Hash of metadata for '%s' from 'server' does not match!", id))
  expect_equal(names(err$body), c("x", "i", "x", "i"))
  expect_match(err$body[[3]], "This is bad news")
  expect_match(err$body[[4]], "remove this location")
})


test_that("handle metadata where two locations differ in hash for same id", {
  root <- list()
  for (name in c("a", "b", "us")) {
    root[[name]] <- create_temporary_root()
  }

  id <- outpack_id()
  create_random_packet(root$a, id = id)
  create_random_packet(root$b, id = id)

  orderly_location_add_path("a", path = root$a$path, root = root$us)
  orderly_location_add_path("b", path = root$b$path, root = root$us)

  orderly_location_fetch_metadata(location = "a", root = root$us)
  err <- expect_error(
    orderly_location_fetch_metadata(location = "b", root = root$us),
    "Location 'b' has conflicting metadata")
  expect_equal(names(err$body), c("x", "i", "i", "i"))
  expect_match(err$body[[1]],
               "We have been offered metadata from 'b' that has a different")
  expect_match(err$body[[2]], sprintf("Conflicts for: '%s'", id))
  expect_match(err$body[[3]], "please let us know")
  expect_match(err$body[[4]], "remove this location")
})


test_that("avoid duplicated metadata", {
  skip_if_not_installed("mockery")
  here <- create_temporary_root()
  there <- create_temporary_root()
  orderly_location_add_path("server", path = there$path, root = here)
  id <- create_random_packet(there)

  driver <- location_driver("server", root = here)
  mock_driver <- list(list = function(x) rbind(driver$list(), driver$list()))
  mock_location_driver <- mockery::mock(mock_driver)

  mockery::stub(location_fetch_metadata, "location_driver",
                mock_location_driver)
  err <- expect_error(
    location_fetch_metadata("server", root = here),
    "Duplicate metadata reported from location 'server'")
  expect_equal(names(err$body), c("x", "i", "i"))
  expect_equal(err$body[[1]],
               sprintf("Duplicate data returned for packets '%s'", id))
  expect_equal(err$body[[2]],
               "This is a bug in your location server, please report it")
  expect_match(err$body[[3]], "remove this location")
})


test_that("skip files in the file store", {
  root <- list()
  for (name in c("src", "dst")) {
    root[[name]] <- create_temporary_root(use_file_store = TRUE)
  }

  id <- create_random_packet_chain(root$src, 3)
  orderly_location_add_path("src", path = root$src$path, root = root$dst)
  orderly_location_fetch_metadata(root = root$dst)
  suppressMessages(orderly_location_pull(id[[1]], root = root$dst))

  withr::with_options(list(orderly.quiet = FALSE), {
    res <- testthat::evaluate_promise(
      orderly_location_pull(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)
  })
})


test_that("skip files known elsewhere on disk", {
  root <- list()
  for (name in c("src", "dst")) {
    root[[name]] <- create_temporary_root(use_file_store = FALSE)
  }

  id <- create_random_packet_chain(root$src, 3)
  orderly_location_add_path("src", path = root$src$path, root = root$dst)
  orderly_location_fetch_metadata(root = root$dst)
  suppressMessages(orderly_location_pull(id[[1]], root = root$dst))

  withr::with_options(list(orderly.quiet = FALSE), {
    res <- testthat::evaluate_promise(
      orderly_location_pull(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)
  })
})


test_that("can prune orphans from tree", {
  root <- list()
  for (name in c("here", "there")) {
    root[[name]] <- create_temporary_root()
  }
  orderly_location_add_path("there", path = root$there$path, root = root$here)
  id <- create_random_packet_chain(root$there, 5)
  orderly_location_fetch_metadata(root = root$here)
  expect_message(
    orderly_location_remove("there", root = root$here),
    "Orphaning 5 packets")

  expect_setequal(orderly_location_list(root = root$here),
                  c("local", "orphan"))
  expect_equal(root$here$index$data()$location$location,
               rep("orphan", 5))

  expect_message(
    orderly_prune_orphans(root = root$here),
    "Pruning 5 orphan packets")

  expect_setequal(orderly_location_list(root = root$here),
                  c("local", "orphan"))
  expect_equal(root$here$index$data()$location$location,
               character())
})


test_that("don't prune referenced orphans", {
  root <- create_temporary_root()
  id <- create_random_packet_chain(root, 3)
  fs::dir_delete(file.path(root$path, "archive", "a"))
  fs::dir_delete(file.path(root$path, "archive", "c"))
  suppressMessages(orderly_validate_archive(action = "orphan", root = root))
  expect_equal(nrow(root$index$location(orphan)), 2)

  res <- evaluate_promise(orderly_prune_orphans(root = root))
  expect_equal(res$result, id[[3]])
  expect_length(res$messages, 2)
  expect_match(
    res$messages[[1]],
    "Can't prune 1 orphan packet, as it is referenced by other packets")
  expect_match(
    res$messages[[2]],
    "Pruning 1 orphan packet")

  res <- evaluate_promise(orderly_prune_orphans(root = root))
  expect_equal(res$result, character())
  expect_length(res$messages, 1)
  expect_match(
    res$messages[[1]],
    "Can't prune 1 orphan packet, as it is referenced by other packets")
})


test_that("early exit if no orphans", {
  root <- create_temporary_root()
  id <- create_random_packet_chain(root, 3)
  expect_silent(res <- orderly_prune_orphans(root = root))
  expect_equal(res, character())
})


test_that("be chatty when pulling packets", {
  withr::local_options(orderly.quiet = FALSE)
  here <- create_temporary_root()
  there <- create_temporary_root()
  res <- evaluate_promise(
    orderly_location_add_path("server", path = there$path, root = here))
  expect_length(res$messages, 3)
  expect_match(res$messages[[1]],
               "Testing location")
  expect_match(res$messages[[2]],
               "Location configured successfully")
  expect_match(res$messages[[3]],
               "Added location 'server' (path)", fixed = TRUE)

  res <- evaluate_promise(orderly_location_fetch_metadata(root = here))
  expect_length(res$messages, 2)
  expect_match(res$messages[[1]],
               "Fetching metadata from 1 location: 'server'")
  expect_match(res$messages[[2]],
               "No metadata found at 'server'")

  id1 <- create_random_packet(there)
  id2 <- create_random_packet(there)

  res <- evaluate_promise(orderly_location_fetch_metadata(root = here))
  expect_length(res$messages, 2)
  expect_match(res$messages[[1]],
               "Fetching metadata from 1 location: 'server'")
  expect_match(res$messages[[2]],
               "Found 2 packets at 'server', of which 2 are new")

  res <- evaluate_promise(orderly_location_fetch_metadata(root = here))
  expect_length(res$messages, 2)
  expect_match(res$messages[[2]],
               "Found 2 packets at 'server', of which 0 are new")

  id3 <- create_random_packet(there)
  res <- evaluate_promise(orderly_location_fetch_metadata(root = here))
  expect_length(res$messages, 2)
  expect_match(res$messages[[2]],
               "Found 3 packets at 'server', of which 1 is new")
})


test_that("verify location on addition", {
  root <- create_temporary_root()
  path <- tempfile()

  expect_error(
    orderly_location_add_path("upstream", path = path, root = root))
  expect_equal(orderly_location_list(root = root), "local")

  expect_no_error(
    orderly_location_add_path("upstream", path = path, verify = FALSE,
                              root = root))
  expect_equal(orderly_location_list(root = root), c("local", "upstream"))
})


test_that("print list of pulled packets", {
  withr::local_options(orderly.quiet = FALSE)
  root <- list()
  for (name in c("src", "dst")) {
    root[[name]] <- create_temporary_root()
  }

  id <- create_random_packet(root$src)
  suppressMessages({
    orderly_location_add_path("src", path = root$src$path, root = root$dst)
    orderly_location_fetch_metadata(root = root$dst)
  })

  msgs <- capture_messages(
    orderly_location_pull(id, root = root$dst))
  expect_match(msgs, sprintf("Pulling 1 packet: '%s'", id),
               all = FALSE, fixed = TRUE)
  expect_match(msgs, "Unpacked 1 packet",
               all = FALSE, fixed = TRUE)

  msgs <- capture_messages(
    orderly_location_pull(id, root = root$dst))
  expect_match(msgs, sprintf("Pulling 1 packet: '%s'", id),
               all = FALSE, fixed = TRUE)
  expect_match(msgs, "Nothing to do, everything is available locally",
               all = FALSE, fixed = TRUE)
})

Try the orderly package in your browser

Any scripts or data that you put into this service are public.

orderly documentation built on Jan. 24, 2026, 1:07 a.m.