tests/testthat/test-graph.R

test_that("renv_graph_init resolves a package with no dependencies", {

  renv_tests_scope()

  descriptions <- renv_graph_init("bread")
  expect_true("bread" %in% names(descriptions))
  expect_equal(descriptions[["bread"]]$Package, "bread")
  expect_equal(length(descriptions), 1L)

})

test_that("renv_graph_init resolves transitive dependencies", {

  renv_tests_scope()

  descriptions <- renv_graph_init("breakfast")
  packages <- names(descriptions)

  # breakfast depends on oatmeal and toast; toast depends on bread
  expect_true("breakfast" %in% packages)
  expect_true("oatmeal" %in% packages)
  expect_true("toast" %in% packages)
  expect_true("bread" %in% packages)

  # suggests (egg) should not be included
  expect_false("egg" %in% packages)

})

test_that("renv_graph_sort produces a valid topological order", {

  renv_tests_scope()

  descriptions <- renv_graph_init("breakfast")
  sorted <- renv_graph_sort(descriptions)
  order <- names(sorted)

  # leaves must come before their dependents
  expect_true(which(order == "bread") < which(order == "toast"))
  expect_true(which(order == "oatmeal") < which(order == "breakfast"))
  expect_true(which(order == "toast") < which(order == "breakfast"))

})

test_that("renv_graph_sort handles packages with no dependencies", {

  renv_tests_scope()

  descriptions <- renv_graph_init("bread")
  sorted <- renv_graph_sort(descriptions)
  expect_equal(names(sorted), "bread")

})

test_that("renv_graph_init handles multiple roots", {

  renv_tests_scope()

  descriptions <- renv_graph_init(c("bread", "egg"))

  expect_equal(sort(names(descriptions)), c("bread", "egg"))

})

test_that("renv_graph_init deduplicates shared dependencies", {

  renv_tests_scope()

  # breakfast and brunch both depend on oatmeal and toast
  descriptions <- renv_graph_init(c("breakfast", "brunch"))
  packages <- names(descriptions)

  # shared deps should appear only once (no duplicates by construction)
  expect_equal(length(packages), length(unique(packages)))

  # both roots and shared deps should be present
  expect_true("breakfast" %in% packages)
  expect_true("brunch" %in% packages)
  expect_true("oatmeal" %in% packages)
  expect_true("toast" %in% packages)
  expect_true("bread" %in% packages)

})

test_that("renv_graph_waves computes correct wave structure", {

  renv_tests_scope()

  descriptions <- renv_graph_init("breakfast")
  waves <- renv_graph_waves(descriptions)

  # should have multiple waves
  expect_true(length(waves) >= 2L)

  # all packages should appear exactly once across waves
  all_pkgs <- unlist(waves)
  expect_equal(sort(all_pkgs), sort(names(descriptions)))
  expect_equal(length(all_pkgs), length(unique(all_pkgs)))

  # leaves (bread, oatmeal) must be in an earlier wave than their dependents
  wave_of <- function(pkg) {
    for (i in seq_along(waves))
      if (pkg %in% waves[[i]])
        return(i)
  }

  expect_true(wave_of("bread") < wave_of("toast"))
  expect_true(wave_of("toast") < wave_of("breakfast"))
  expect_true(wave_of("oatmeal") < wave_of("breakfast"))

})

test_that("renv_graph_waves returns single wave for leaf package", {

  renv_tests_scope()

  descriptions <- renv_graph_init("bread")
  waves <- renv_graph_waves(descriptions)

  expect_equal(length(waves), 1L)
  expect_equal(waves[[1L]], "bread")

})

test_that("renv_graph_waves groups independent packages in same wave", {

  renv_tests_scope()

  # bread and egg have no dependencies on each other
  descriptions <- renv_graph_init(c("bread", "egg"))
  waves <- renv_graph_waves(descriptions)

  # both should be in wave 1 (no deps within the set)
  expect_equal(length(waves), 1L)
  expect_equal(sort(waves[[1L]]), sort(c("bread", "egg")))

})

test_that("renv_graph_install installs packages end to end", {

  renv_tests_scope()

  descriptions <- renv_graph_init("breakfast")
  records <- renv_graph_install(descriptions)

  # all packages should be installed
  expect_true("breakfast" %in% names(records))
  expect_true("oatmeal" %in% names(records))
  expect_true("toast" %in% names(records))
  expect_true("bread" %in% names(records))

  # packages should actually be loadable
  library <- renv_libpaths_active()
  for (pkg in names(records))
    expect_true(renv_package_installed(pkg, lib.loc = library), info = pkg)

})

test_that("renv_graph_install installs a single leaf package", {

  renv_tests_scope()

  descriptions <- renv_graph_init("bread")
  records <- renv_graph_install(descriptions)

  expect_equal(names(records), "bread")
  expect_true(renv_package_installed("bread"))

})

test_that("renv_graph_urls resolves repository package URLs", {

  renv_tests_scope()

  descriptions <- renv_graph_init("bread")
  urls <- renv_graph_urls(descriptions)

  expect_true("bread" %in% names(urls))

  info <- urls[["bread"]]
  expect_true(is.list(info))
  expect_true(nzchar(info$url))
  expect_true(nzchar(info$destfile))
  expect_true(grepl("bread", info$url))

})

test_that("renv_graph_urls returns NULL for unsupported sources", {

  desc <- list(Package = "fakepkg", Version = "1.0", Source = "bitbucket")
  descriptions <- list(fakepkg = desc)

  urls <- renv_graph_urls(descriptions)
  expect_null(urls[["fakepkg"]])

})

test_that("renv_graph_url_github constructs correct URL", {

  desc <- list(
    Package        = "mypkg",
    Version        = "1.0.0",
    Source         = "GitHub",
    RemoteUsername = "owner",
    RemoteRepo     = "repo",
    RemoteSha      = "abc123"
  )

  info <- renv_graph_url_github(desc)
  expect_true(is.list(info))
  expect_true(grepl("owner/repo/tarball/abc123", info$url))
  expect_equal(info$type, "github")

})

test_that("renv_graph_url_github returns NULL when fields are missing", {

  desc <- list(Package = "mypkg", Version = "1.0.0", Source = "GitHub")
  expect_null(renv_graph_url_github(desc))

})

test_that("renv_graph_url_gitlab constructs correct URL", {

  desc <- list(
    Package        = "mypkg",
    Version        = "1.0.0",
    Source         = "GitLab",
    RemoteUsername = "user",
    RemoteRepo     = "repo",
    RemoteSha      = "def456"
  )

  info <- renv_graph_url_gitlab(desc)
  expect_true(is.list(info))
  expect_true(grepl("projects/user%2Frepo/repository/archive", info$url))
  expect_true(grepl("sha=def456", info$url))
  expect_equal(info$type, "gitlab")

})

test_that("renv_graph_url_url resolves RemoteUrl", {

  desc <- list(
    Package   = "mypkg",
    Version   = "1.0.0",
    Source    = "url",
    RemoteUrl = "https://example.com/mypkg_1.0.0.tar.gz"
  )

  info <- renv_graph_url_url(desc)
  expect_true(is.list(info))
  expect_equal(info$url, "https://example.com/mypkg_1.0.0.tar.gz")
  expect_equal(info$type, "url")
  expect_true(nzchar(info$destfile))

})

test_that("renv_graph_url_local converts file path to file URI", {

  src <- renv_scope_tempfile("renv-local-pkg-", fileext = ".tar.gz")
  file.create(src)

  desc <- list(
    Package = "mypkg",
    Version = "1.0.0",
    Source  = "local",
    Path    = src
  )

  info <- renv_graph_url_local(desc)
  expect_true(is.list(info))
  expect_true(startsWith(info$url, "file://"))
  expect_equal(info$type, "local")

})

test_that("renv_graph_url_local returns NULL when path doesn't exist", {

  desc <- list(
    Package = "mypkg",
    Version = "1.0.0",
    Source  = "local",
    Path    = "/nonexistent/path/mypkg_1.0.0.tar.gz"
  )

  expect_null(renv_graph_url_local(desc))

})

test_that("renv_graph_url_repository resolves from test repo", {

  renv_tests_scope()

  desc <- list(Package = "bread", Version = "1.0.0", Source = "Repository")
  info <- renv_graph_url_repository(desc)

  expect_true(is.list(info))
  expect_true(grepl("bread", info$url))
  expect_equal(info$type, "repository")

})

# adjacency graph ----

test_that("renv_graph_adjacency computes correct structure for chain", {

  renv_tests_scope()

  descriptions <- renv_graph_init("breakfast")
  g <- renv_graph_adjacency(descriptions)

  # breakfast -> oatmeal, toast; toast -> bread; bread, oatmeal -> nothing
  expect_true(setequal(g$packages, c("breakfast", "oatmeal", "toast", "bread")))
  expect_true("toast" %in% g$adj[["breakfast"]])
  expect_true("oatmeal" %in% g$adj[["breakfast"]])
  expect_equal(g$adj[["bread"]], character())
  expect_equal(g$adj[["oatmeal"]], character())
  expect_equal(g$adj[["toast"]], "bread")

  # in-degree: bread=0, oatmeal=0, toast=1 (from breakfast), breakfast=0 wait no
  # in-degree counts deps *within the set* for each package
  expect_equal(g$indegree[["bread"]], 0L)
  expect_equal(g$indegree[["oatmeal"]], 0L)
  expect_equal(g$indegree[["toast"]], 1L)       # depends on bread
  expect_equal(g$indegree[["breakfast"]], 2L)    # depends on oatmeal + toast

  # reverse adjacency: bread is depended on by toast; toast by breakfast
  expect_true("toast" %in% g$revadj[["bread"]])
  expect_true("breakfast" %in% g$revadj[["toast"]])
  expect_true("breakfast" %in% g$revadj[["oatmeal"]])

})

test_that("renv_graph_adjacency handles independent packages", {

  renv_tests_scope()

  descriptions <- renv_graph_init(c("bread", "egg"))
  g <- renv_graph_adjacency(descriptions)

  # no edges between independent packages
  expect_equal(g$adj[["bread"]], character())
  expect_equal(g$adj[["egg"]], character())
  expect_equal(g$indegree[["bread"]], 0L)
  expect_equal(g$indegree[["egg"]], 0L)

})

test_that("renv_graph_adjacency handles empty input", {

  g <- renv_graph_adjacency(list())
  expect_equal(length(g$packages), 0L)
  expect_equal(length(g$adj), 0L)

})

# version requirements ----

test_that("renv_graph_requirements extracts version constraints", {

  renv_tests_scope()

  # breakfast depends on toast (>= 1.0.0)
  descriptions <- renv_graph_init("breakfast")
  requirements <- renv_graph_requirements(descriptions)

  # toast should have a requirement from breakfast
  reqs <- requirements[["toast"]]
  expect_true(is.data.frame(reqs))
  expect_true(nrow(reqs) >= 1L)
  expect_true("breakfast" %in% reqs$RequiredBy)

})

test_that("renv_graph_compatible accepts satisfied constraints", {

  reqs <- data.frame(
    Package    = "toast",
    Require    = ">=",
    Version    = "1.0.0",
    RequiredBy = "breakfast",
    stringsAsFactors = FALSE
  )

  expect_true(renv_graph_compatible("1.0.0", reqs))
  expect_true(renv_graph_compatible("2.0.0", reqs))

})

test_that("renv_graph_compatible rejects unsatisfied constraints", {

  reqs <- data.frame(
    Package    = "toast",
    Require    = ">=",
    Version    = "2.0.0",
    RequiredBy = "breakfast",
    stringsAsFactors = FALSE
  )

  expect_false(renv_graph_compatible("1.0.0", reqs))
  expect_false(renv_graph_compatible("1.9.9", reqs))

})

test_that("renv_graph_compatible handles multiple constraints", {

  reqs <- data.frame(
    Package    = c("toast", "toast"),
    Require    = c(">=", ">="),
    Version    = c("1.0.0", "1.5.0"),
    RequiredBy = c("pkg1", "pkg2"),
    stringsAsFactors = FALSE
  )

  expect_true(renv_graph_compatible("1.5.0", reqs))
  expect_true(renv_graph_compatible("2.0.0", reqs))
  expect_false(renv_graph_compatible("1.2.0", reqs))

})

test_that("renv_graph_compatible returns TRUE for no requirements", {

  expect_true(renv_graph_compatible("1.0.0", NULL))
  expect_true(renv_graph_compatible("1.0.0", data.frame()))

})

# install result parsing ----

test_that("renv_graph_install_parse_result handles NULL data", {

  elapsed <- as.difftime(1, units = "secs")
  result <- renv_graph_install_parse_result(NULL, elapsed)

  expect_false(result$success)
  expect_true(grepl("unexpectedly", result$output))
  expect_equal(result$elapsed, elapsed)

})

test_that("renv_graph_install_parse_result handles error object", {

  elapsed <- as.difftime(2, units = "secs")
  err <- simpleError("something went wrong")
  result <- renv_graph_install_parse_result(err, elapsed)

  expect_false(result$success)
  expect_equal(result$output, "something went wrong")
  expect_equal(result$elapsed, elapsed)

})

test_that("renv_graph_install_parse_result handles successful output", {

  elapsed <- as.difftime(3, units = "secs")
  output <- c("* installing *source* package 'bread' ...", "* DONE (bread)")
  attr(output, "status") <- 0L
  result <- renv_graph_install_parse_result(output, elapsed)

  expect_true(result$success)
  expect_equal(result$output, output)

})

test_that("renv_graph_install_parse_result handles failed output", {

  elapsed <- as.difftime(4, units = "secs")
  output <- c("ERROR: compilation failed")
  attr(output, "status") <- 1L
  result <- renv_graph_install_parse_result(output, elapsed)

  expect_false(result$success)
  expect_equal(result$output, output)

})

test_that("renv_graph_install_parse_result handles output with no status attr", {

  elapsed <- as.difftime(1, units = "secs")
  output <- c("some output")
  result <- renv_graph_install_parse_result(output, elapsed)

  # no status attribute means success (status 0)
  expect_true(result$success)

})

# install classification ----

test_that("renv_graph_install_classify uses type attribute when present", {

  record <- list(Package = "bread", Path = "/tmp/bread_1.0.0.tar.gz")
  attr(record, "type") <- "binary"
  expect_equal(renv_graph_install_classify(record), "binary")

  attr(record, "type") <- "source"
  expect_equal(renv_graph_install_classify(record), "source")

})

# install needs unpack ----

test_that("renv_graph_install_needs_unpack returns TRUE for RemoteSubdir", {

  record <- list(
    Package      = "mypkg",
    Path         = "/tmp/mypkg.tar.gz",
    RemoteSubdir = "subdir"
  )
  expect_true(renv_graph_install_needs_unpack(record, "source"))

})

test_that("renv_graph_install_needs_unpack returns FALSE for simple tar.gz source", {

  archive <- renv_scope_tempfile("renv-test-", fileext = ".tar.gz")
  file.create(archive)

  record <- list(Package = "mypkg", Path = archive)
  renv_scope_options(renv.config.install.build = FALSE)
  expect_false(renv_graph_install_needs_unpack(record, "source"))

})

# install error reporting ----

test_that("renv_graph_install_errors reports direct failures", {

  renv_scope_options(renv.verbose = TRUE, renv.caution.verbose = TRUE)

  descriptions <- list(
    bread = list(Package = "bread", Version = "1.0.0")
  )

  errors <- list(
    list(package = "bread", message = "compilation failed")
  )

  output <- capture.output(
    renv_graph_install_errors(errors, "bread", descriptions)
  )

  output <- paste(output, collapse = "\n")
  expect_true(grepl("bread", output))
  expect_true(grepl("compilation failed", output))

})

test_that("renv_graph_install_errors reports dependency cascade failures", {

  renv_scope_options(renv.verbose = TRUE, renv.caution.verbose = TRUE)

  descriptions <- list(
    bread = list(Package = "bread", Version = "1.0.0"),
    toast = list(Package = "toast", Version = "1.0.0", Depends = "bread")
  )

  errors <- list(
    list(package = "bread", message = "compilation failed")
  )
  failed <- c("bread", "toast")

  output <- capture.output(
    renv_graph_install_errors(errors, failed, descriptions)
  )

  output <- paste(output, collapse = "\n")
  expect_true(grepl("bread", output))
  expect_true(grepl("toast", output))
  expect_true(grepl("dependency failed", output))

})

test_that("renv_graph_install_errors is silent with no errors", {

  output <- capture.output(
    renv_graph_install_errors(list(), character(), list())
  )
  expect_equal(length(output), 0L)

})

# wave cycle detection ----

test_that("renv_graph_waves warns on dependency cycle", {

  # synthetic descriptions that form a cycle: A -> B -> A
  descriptions <- list(
    A = list(Package = "A", Version = "1.0.0", Depends = "B"),
    B = list(Package = "B", Version = "1.0.0", Depends = "A")
  )

  expect_warning(
    waves <- renv_graph_waves(descriptions),
    "dependency cycle"
  )

  # all packages should still appear
  all_pkgs <- unlist(waves)
  expect_true(setequal(all_pkgs, c("A", "B")))

})

test_that("renv_graph_waves handles empty input", {

  waves <- renv_graph_waves(list())
  expect_equal(waves, list())

})

# install pipeline integration tests ----

test_that("renv_graph_install installs multiple independent packages", {

  renv_tests_scope()

  # bread and egg have no dependency relationship
  descriptions <- renv_graph_init(c("bread", "egg"))
  records <- renv_graph_install(descriptions)

  expect_true(setequal(names(records), c("bread", "egg")))
  expect_true(renv_package_installed("bread"))
  expect_true(renv_package_installed("egg"))

})

test_that("renv_graph_install skips already-installed packages", {

  renv_tests_scope()

  # install bread first
  descriptions <- renv_graph_init("bread")
  renv_graph_install(descriptions)
  expect_true(renv_package_installed("bread"))

  # now install breakfast; bread should be skipped
  descriptions <- renv_graph_init("breakfast")
  records <- renv_graph_install(descriptions)

  # breakfast and its other deps should be installed
  expect_true(renv_package_installed("breakfast"))
  expect_true(renv_package_installed("toast"))
  expect_true(renv_package_installed("oatmeal"))

})

test_that("renv_graph_install returns empty list for empty input", {

  renv_tests_scope()

  records <- renv_graph_install(list())
  expect_equal(records, list())

})

test_that("renv_graph_install handles deeper dependency chain", {

  renv_tests_scope()

  # jamie -> kevin + phone; kevin -> phone
  # three levels: phone -> kevin -> jamie
  descriptions <- renv_graph_init("jamie")
  records <- renv_graph_install(descriptions)

  expect_true("jamie" %in% names(records))
  expect_true("kevin" %in% names(records))
  expect_true("phone" %in% names(records))

  for (pkg in names(records))
    expect_true(renv_package_installed(pkg), info = pkg)

})

test_that("renv_graph_install with install.jobs = 1 uses sequential mode", {

  renv_tests_scope()
  renv_scope_options(renv.config.install.jobs = 1L)

  descriptions <- renv_graph_init("breakfast")
  records <- renv_graph_install(descriptions)

  expect_true(setequal(
    names(records),
    c("bread", "oatmeal", "toast", "breakfast")
  ))

  for (pkg in names(records))
    expect_true(renv_package_installed(pkg), info = pkg)

})

test_that("renv_graph_install with staged install", {

  renv_tests_scope()

  renv_scope_options(
    renv.config.install.staged = TRUE,
    renv.config.install.transactional = FALSE
  )

  descriptions <- renv_graph_init("breakfast")
  records <- renv_graph_install(descriptions)

  # all packages should be in the real library after staging
  library <- renv_libpaths_active()
  for (pkg in names(records))
    expect_true(renv_package_installed(pkg, lib.loc = library), info = pkg)

})

test_that("renv_graph_install respects dependency ordering", {

  renv_tests_scope(isolated = TRUE)

  # disable cache so all packages go through source install
  renv_scope_options(renv.config.cache.enabled = FALSE)

  # track the order packages are finalized via a tracer;
  # use a shared environment so the tracer (evaluated inside the
  # renv namespace) can write to it
  env <- new.env(parent = emptyenv())
  env$order <- character()

  renv_scope_trace(
    what = renv:::renv_graph_install_finalize,
    tracer = bquote({
      .env <- .(env)
      .env$order <- c(.env$order, record$Package)
    })
  )

  descriptions <- renv_graph_init("breakfast")
  records <- renv_graph_install(descriptions)

  # bread must be finalized before toast, toast before breakfast
  bread_idx <- match("bread", env$order)
  toast_idx <- match("toast", env$order)
  breakfast_idx <- match("breakfast", env$order)

  expect_true(!is.na(bread_idx))
  expect_true(!is.na(toast_idx))
  expect_true(!is.na(breakfast_idx))
  expect_true(bread_idx < toast_idx)
  expect_true(toast_idx < breakfast_idx)

})

# graph sort edge cases ----

test_that("renv_graph_sort handles empty input", {

  sorted <- renv_graph_sort(list())
  expect_equal(length(sorted), 0L)

})

test_that("renv_graph_sort produces stable ordering for independent packages", {

  renv_tests_scope()

  descriptions <- renv_graph_init(c("bread", "egg", "oatmeal"))
  sorted <- renv_graph_sort(descriptions)

  # all three should appear; order among independent packages
  # is deterministic (alphabetical from the queue)
  expect_equal(length(sorted), 3L)
  expect_true(setequal(names(sorted), c("bread", "egg", "oatmeal")))

})

# graph deps ----

test_that("renv_graph_deps extracts dependencies from Depends/Imports/LinkingTo", {

  desc <- list(
    Package   = "mypkg",
    Depends   = "R (>= 3.5), bread, oatmeal",
    Imports   = "toast",
    LinkingTo = "egg"
  )

  deps <- renv_graph_deps(desc)

  # R should be excluded (base package)
  expect_false("R" %in% deps)
  expect_true(setequal(deps, c("bread", "oatmeal", "toast", "egg")))

})

test_that("renv_graph_deps respects custom fields argument", {

  desc <- list(
    Package  = "mypkg",
    Depends  = "bread",
    Imports  = "toast",
    Suggests = "egg"
  )

  # default fields don't include Suggests
  deps_default <- renv_graph_deps(desc)
  expect_false("egg" %in% deps_default)

  # custom fields can include Suggests
  deps_custom <- renv_graph_deps(desc, fields = c("Depends", "Imports", "Suggests"))
  expect_true("egg" %in% deps_custom)

})

test_that("renv_graph_deps returns empty for package with no deps", {

  desc <- list(Package = "mypkg", Version = "1.0.0")
  deps <- renv_graph_deps(desc)
  expect_equal(deps, character())

})

# graph URLs for multiple sources ----

test_that("renv_graph_urls resolves URLs for full dependency tree", {

  renv_tests_scope()

  descriptions <- renv_graph_init("breakfast")
  urls <- renv_graph_urls(descriptions)

  # every package should have a resolved URL (they're all from the test repo)
  for (pkg in names(descriptions)) {
    info <- urls[[pkg]]
    expect_true(is.list(info), info = pkg)
    expect_true(nzchar(info$url), info = pkg)
  }

})

test_that("renv_graph_urls gracefully handles mixed sources", {

  renv_tests_scope()

  descriptions <- list(
    bread = list(Package = "bread", Version = "1.0.0", Source = "Repository"),
    fake  = list(Package = "fake", Version = "1.0.0", Source = "unknown_source")
  )

  urls <- renv_graph_urls(descriptions)

  # bread should resolve; fake should be NULL
  expect_true(is.list(urls[["bread"]]))
  expect_null(urls[["fake"]])

})

Try the renv package in your browser

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

renv documentation built on March 25, 2026, 5:07 p.m.