Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
Package: BiocPkgTools
Type: Package
Title: Collection of simple tools for learning about Bioconductor Packages
Version: 1.31.13
Date: 2026-07-06
Version: 1.31.14
Date: 2026-07-17
Authors@R: c(
person("Shian", "Su", role=c("aut", "ctb"), email = "su.s@wehi.edu.au"),
person("Lori", "Shepherd", role="ctb", email = "Lori.Shepherd@roswellpark.org"),
Expand Down Expand Up @@ -55,7 +55,8 @@ Imports:
VignetteBuilder: knitr
Suggests: BiocStyle, knitr, rmarkdown,
testthat, tm, networkD3,
visNetwork, clipr, blastula, kableExtra, DiagrammeR, SummarizedExperiment
visNetwork, clipr, blastula, kableExtra, DiagrammeR, SummarizedExperiment,
withr, Biobase
License: MIT + file LICENSE
BugReports: https://github.com/seandavi/BiocPkgTools/issues/new
URL: https://github.com/seandavi/BiocPkgTools
Expand Down
4 changes: 4 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ BUG FIXES AND MINOR IMPROVEMENTS
o Removed the dependency on `httr` by migrating `.url_exists` helper to
`httr2`
o Removed duplicate internal function `generateBiocPkgDOI_REST`
o Raised unit test coverage from 18% across the package (#85). Fixed
`.get_all_biocpkgs()` calling `BiocManager::version` unevaluated
(always errored) and `checkDeps()` dropping to an atomic vector on
subset, which broke `problemPage(dependsOn = ...)` entirely.

CHANGES IN VERSION 1.30.0
-------------------------
Expand Down
2 changes: 1 addition & 1 deletion R/biocDownloadStats.R
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ utils::globalVariables(

.get_all_biocpkgs <- function() {
db <- available.packages(
repos = BiocManager:::.repositories_bioc(version = version)
repos = BiocManager:::.repositories_bioc(version = BiocManager::version())
)
rownames(db)
}
Expand Down
4 changes: 3 additions & 1 deletion R/problemPage.R
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ checkDeps <- function(dependsOn, ver="devel", includeOK = FALSE) {

rep = biocBuildReport(ver)
all_pkg_deps = buildPkgDependencyDataFrame()
pkg_deps <- all_pkg_deps[ all_pkg_deps$dependency == dependsOn, 1]
pkg_deps <- all_pkg_deps[
all_pkg_deps$dependency == dependsOn, "Package", drop = FALSE
]

mine = rep[rep$pkg %in% pkg_deps$Package, ]

Expand Down
23 changes: 23 additions & 0 deletions tests/testthat/helper-network.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.bioc_reachable <- function(timeout_sec = 5) {
if (!curl::has_internet())
return(FALSE)
ok <- tryCatch({
req <- httr2::request("https://bioconductor.org")
req <- httr2::req_options(req, timeout_ms = timeout_sec * 1000)
req <- httr2::req_method(req, "HEAD")
resp <- httr2::req_perform(req)
httr2::resp_status(resp) < 400
}, error = function(e) FALSE)
isTRUE(ok)
}

#' Skip a test unless bioconductor.org is reachable
#'
#' Use in place of `testthat::skip_if_offline()` for tests that hit
#' Bioconductor-hosted resources specifically (general internet access does
#' not guarantee bioconductor.org is up).
skip_if_bioc_offline <- function() {
testthat::skip_if_not(
.bioc_reachable(), "bioconductor.org is not reachable"
)
}
25 changes: 25 additions & 0 deletions tests/testthat/test-biocVIEWSdb.R
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,28 @@ test_that("biocVIEWSdb handles invalid version", {
"'version' is not 'release', 'devel', or a valid 'package_version'"
)
})

test_that(".read_VIEWS_url parses a real VIEWS file into a data.frame", {
skip_if_bioc_offline()

version <- as.package_version(BiocManager:::.version_bioc("devel"))
url <- BiocPkgTools:::.get_VIEWS_url(version = version, type = "BioCsoft")

df <- BiocPkgTools:::.read_VIEWS_url(url)

expect_s3_class(df, "data.frame")
expect_gt(nrow(df), 0L)
expect_true(all(c("Package", "Version", "Maintainer") %in% colnames(df)))
})

test_that(".read_VIEWS_url errors informatively when the resource isn't DCF-formatted", {
skip_if_bioc_offline()

# A real, fetchable Bioconductor URL that is not a VIEWS/DCF file --
# exercises .read_VIEWS_url()'s own error-wrapping branch (read.dcf()
# failing on non-DCF content) rather than a network failure.
expect_error(
BiocPkgTools:::.read_VIEWS_url("https://bioconductor.org/index.html"),
"Error reading VIEWS file from URL"
)
})
93 changes: 93 additions & 0 deletions tests/testthat/test-buildPkgDependencyGraph.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
## These functions hit bioconductor.org indirectly through biocPkgList(),
## so they are gated on skip_if_bioc_offline(). The dependency data frame is
## built once and shared across tests in this file (mirroring the pattern in
## test_biocPkgList.R) to avoid repeating the network call and to keep the
## graph operations (which are all in-memory and fast) from re-fetching data.

skip_if_bioc_offline()

depdf <- buildPkgDependencyDataFrame(dependencies = "strong")

test_that("buildPkgDependencyDataFrame returns a tidy biocDepDF", {
expect_s3_class(depdf, "biocDepDF")
expect_s3_class(depdf, "data.frame")
expect_true(all(c("Package", "dependency", "edgetype") %in% colnames(depdf)))
expect_gt(nrow(depdf), 0L)
## only strong dependency types requested
expect_true(all(depdf$edgetype %in% c("Depends", "Imports", "LinkingTo")))
## "R" and NA/empty entries are filtered out
expect_false("R" %in% depdf$dependency)
expect_false(any(is.na(depdf$dependency)))
expect_false(any(depdf$dependency == ""))
})

test_that("buildPkgDependencyDataFrame rejects invalid 'dependencies'", {
## Note: a single invalid string (e.g. dependencies = "bogus") is *not*
## caught by match.arg() -- match.arg() is only reached when
## length(dependencies) != 1, and match.arg(..., several.ok = TRUE) only
## errors when *none* of the supplied values are valid choices, so a
## vector of two unrecognized strings is needed to trigger it.
expect_error(
buildPkgDependencyDataFrame(dependencies = c("bogus1", "bogus2")),
"'arg' should be one of"
)
})

test_that("buildPkgDependencyIgraph builds a biocDepGraph igraph object", {
g <- buildPkgDependencyIgraph(depdf)
expect_s3_class(g, "biocDepGraph")
expect_true(igraph::is_igraph(g))
expect_true(igraph::is_directed(g))
expect_true("GEOquery" %in% names(igraph::V(g)))
})

test_that("inducedSubgraphByPkgs returns only requested (and connecting) nodes", {
g <- buildPkgDependencyIgraph(depdf)
pkgs <- c("GEOquery", "Biobase", "S4Vectors")
g2 <- inducedSubgraphByPkgs(g, pkgs = pkgs)

expect_true(igraph::is_igraph(g2))
expect_true(all(names(igraph::V(g2)) %in% pkgs))
expect_true(all(igraph::V(g2)[pkgs]$color == "red"))
})

test_that("inducedSubgraphByPkgs ignores package names not in the graph", {
g <- buildPkgDependencyIgraph(depdf)
g2 <- inducedSubgraphByPkgs(g, pkgs = c("GEOquery", "not-a-real-package-xyz"))
expect_true(igraph::is_igraph(g2))
expect_false("not-a-real-package-xyz" %in% names(igraph::V(g2)))
})

test_that("subgraphByDegree limits nodes to within 'degree' of a package", {
g <- buildPkgDependencyIgraph(depdf)
g1 <- subgraphByDegree(g, "GEOquery", degree = 1)
expect_true(igraph::is_igraph(g1))
expect_true("GEOquery" %in% names(igraph::V(g1)))
expect_lte(length(igraph::V(g1)), length(igraph::V(g)))
})

test_that("subgraphByDegree(degree = 0) currently returns an empty graph (known limitation)", {
## NOTE: this documents a real bug in subgraphByDegree() rather than
## exercising intended behavior. `d2 <- d[1, d[1,] <= degree]` relies on
## matrix-style `[` indexing; when exactly one column matches (as always
## happens at degree = 0, since only the queried package itself has
## distance 0 from itself), R's matrix indexing silently drops the name
## of that single remaining element (a base R quirk: compare
## `m <- matrix(1, dimnames = list("r", "c")); m[1, TRUE]`, which returns
## an unnamed scalar). `names(d2)` is then NULL, and
## `induced_subgraph(g, vids = NULL)` returns an *empty* graph instead of
## a single-vertex graph containing just "GEOquery". Left unfixed here
## since it does not block coverage of the function's main path (tested
## above with degree = 1), but flagged for a follow-up fix.
g <- buildPkgDependencyIgraph(depdf)
g0 <- subgraphByDegree(g, "GEOquery", degree = 0)
expect_true(igraph::is_igraph(g0))
expect_identical(length(igraph::V(g0)), 0L)
})

test_that("subgraphByDegree errors for a package not present in the graph", {
g <- buildPkgDependencyIgraph(depdf)
expect_error(
subgraphByDegree(g, "not-a-real-package-xyz", degree = 1)
)
})
109 changes: 109 additions & 0 deletions tests/testthat/test-classDependencies.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
## These functions operate purely on installed packages' NAMESPACE / S4 class
## metadata -- no network access is required, so nothing here is gated on
## skip_if_offline()/skip_if_bioc_offline(). SummarizedExperiment and Biobase
## (both Bioconductor packages with rich S4 class hierarchies) are used as
## small, already-installed targets.

suppressPackageStartupMessages(library(SummarizedExperiment))
suppressPackageStartupMessages(library(Biobase))

## --- internal helpers -------------------------------------------------------

test_that(".is_s_virtual identifies virtual classes", {
## 'Vector' is virtual, 'DFrame' is a concrete (non-virtual) S4Vectors class
res <- BiocPkgTools:::.is_s_virtual(c("Vector", "DFrame"))
expect_identical(res, c(TRUE, FALSE))
## NA input short-circuits to FALSE
expect_identical(BiocPkgTools:::.is_s_virtual(NA_character_), FALSE)
})

test_that(".is_s_union identifies S4 class unions", {
## 'vector_OR_Vector' is a real class union exported by S4Vectors
expect_true(BiocPkgTools:::.is_s_union("vector_OR_Vector"))
expect_false(BiocPkgTools:::.is_s_union("DataFrame"))
})

test_that(".get_s_extends_data.frame builds a well-formed edge data.frame", {
df <- BiocPkgTools:::.get_s_extends_data.frame(
parent = "P", child = "C", type = "object"
)
expect_s3_class(df, "data.frame")
expect_identical(df$parent, "P")
expect_identical(df$child, "C")
expect_identical(
colnames(df),
c("parent", "child", "type", "parentVirtual", "parentUnion",
"childVirtual", "childUnion")
)
})

## --- buildClassDepData / buildClassDepGraph: main path ---------------------

test_that("buildClassDepData returns a parent/child edge data.frame", {
d <- buildClassDepData("RangedSummarizedExperiment")
expect_s3_class(d, "data.frame")
expect_true(all(
c("parent", "child", "type", "childUnion") %in% colnames(d)
))
expect_gt(nrow(d), 0L)
## default excludes union-typed child rows
expect_false(any(d$childUnion))
## the class itself should appear as a parent somewhere in the hierarchy
expect_true("RangedSummarizedExperiment" %in% d$parent)
})

test_that("buildClassDepData(includeUnions = TRUE) includes union rows", {
d_no_union <- buildClassDepData("RangedSummarizedExperiment")
d_union <- buildClassDepData(
"RangedSummarizedExperiment", includeUnions = TRUE
)
expect_gte(nrow(d_union), nrow(d_no_union))
})

test_that("buildClassDepGraph returns an igraph built from buildClassDepData", {
g <- buildClassDepGraph("RangedSummarizedExperiment")
expect_true(igraph::is_igraph(g))
expect_true("RangedSummarizedExperiment" %in% names(igraph::V(g)))
expect_true(all(
c("type", "parentVirtual", "parentUnion", "childVirtual", "childUnion") %in%
igraph::edge_attr_names(g)
))
})

test_that("buildClassDepData/-Graph error for an undefined class", {
expect_error(buildClassDepData("NotAClassXYZ"), "is not a defined class")
expect_error(buildClassDepGraph("NotAClassXYZ"), "is not a defined class")
})

## --- buildClassDepFromPackage ------------------------------------------------

test_that("buildClassDepFromPackage builds dep data for every exported S4 class", {
res <- buildClassDepFromPackage("Biobase")
expect_type(res, "list")
expect_gt(length(res), 0L)
expect_true("ExpressionSet" %in% names(res))
expect_s3_class(res[["ExpressionSet"]], "data.frame")
})

## --- plotting functions: verify they run without error on a null device ---

test_that("plotClassDepGraph / plotClassDepData / plotClassDep draw without error", {
grDevices::pdf(NULL)
on.exit(grDevices::dev.off(), add = TRUE)

g <- buildClassDepGraph("RangedSummarizedExperiment")
expect_invisible(plotClassDepGraph(g))

d <- buildClassDepData("RangedSummarizedExperiment")
expect_invisible(plotClassDepData(d))

expect_invisible(plotClassDep("RangedSummarizedExperiment"))
})

test_that("plotClassDepGraph errors when required edge attributes are missing", {
g <- igraph::graph_from_data_frame(data.frame(from = "A", to = "B"))
expect_error(
plotClassDepGraph(g),
"must have the following edge attributes"
)
})
65 changes: 64 additions & 1 deletion tests/testthat/test-pkgBiocDeps.R
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
test_that("pkgBiocRevDeps(recursive = TRUE) returns only reverse deps", {
skip_if_offline()
## NOTE: this test hits bioconductor.org (via BiocManager::repositories()),
## so it must be gated with skip_if_bioc_offline() rather than
## testthat::skip_if_offline(). skip_if_offline() calls skip_on_cran()
## internally, which skips whenever the NOT_CRAN env var isn't set to
## "true". devtools::test() sets that var for you (via
## local_assume_not_on_cran()), but the standard tests/testthat.R runner
## (testthat::test_check(), which is what `R CMD check` and covr's
## default test harness both use) does not -- so under covr this test
## was *always* skipped ("Reason: On CRAN"), regardless of actual network
## reachability, which is why covr reported 0% executed lines for this
## file even though the test itself looks fine.
skip_if_bioc_offline()
## Regression test for issue #81: Recursing reverse dependencies over "all"
## dependencies will return nearly the entire repository, including some
## forward dependencies of the queried package.
Expand Down Expand Up @@ -27,3 +38,55 @@ test_that("pkgBiocRevDeps(recursive = TRUE) returns only reverse deps", {
## check that the recursive result is not too small
expect_gt(length(rec), nrow(db) / 2)
})

test_that("pkgBiocDeps returns strong bioc-only dependencies", {
skip_if_bioc_offline()
## 'Rarr' is a small package with no strong Bioconductor dependencies
## (its strong deps are all on CRAN), which exercises the "no matches"
## branch of the only.bioc filter.
res <- pkgBiocDeps("Rarr", only.bioc = TRUE)
expect_type(res, "list")
expect_named(res, "Rarr")
expect_identical(res[["Rarr"]], character(0))

## with only.bioc = FALSE we should see actual (CRAN) dependencies
res_all <- pkgBiocDeps("Rarr", only.bioc = FALSE)
expect_type(res_all, "list")
expect_gt(length(res_all[["Rarr"]]), 0L)
})

test_that("pkgBiocDeps errors on invalid pkgType", {
expect_error(
pkgBiocDeps("Rarr", pkgType = "bogus"),
"'arg' should be one of"
)
})

test_that("summary.biocrevdeps tallies dependency counts", {
skip_if_bioc_offline()
rd <- pkgBiocRevDeps("Rarr", which = "strong")
expect_s3_class(rd, "biocrevdeps")

s <- summary(rd)
expect_s3_class(s, "data.frame")
expect_identical(rownames(s), "Rarr")
expect_true(all(c("Depends", "Imports", "LinkingTo", "Total") %in% colnames(s)))
expect_identical(
s[["Total"]],
sum(s[["Depends"]], s[["Imports"]], s[["LinkingTo"]])
)

## printed 'tools::package_dependencies(...)' call reproduction
expect_output(summary(rd), "tools::package_dependencies")
})

test_that("summary.biocrevdeps handles recursive results (no Total column)", {
skip_if_bioc_offline()
rd <- pkgBiocRevDeps("Rarr", which = "strong", recursive = TRUE)
expect_s3_class(rd, "biocrevdeps")

s <- summary(rd)
expect_s3_class(s, "data.frame")
expect_true("recursive" %in% colnames(s))
expect_false("Total" %in% colnames(s))
})
Loading
Loading