diff --git a/DESCRIPTION b/DESCRIPTION index 375726d..076622b 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -88,6 +88,7 @@ Collate: 'generic_pkg_data_derive.R' 'impl_data.R' 'data_archive_md5.R' + 'data_bioc_reverse_dependencies.R' 'data_coverage.R' 'data_desc.R' 'data_documentation_examples.R' diff --git a/R/data_bioc_reverse_dependencies.R b/R/data_bioc_reverse_dependencies.R new file mode 100644 index 0000000..63150f5 --- /dev/null +++ b/R/data_bioc_reverse_dependencies.R @@ -0,0 +1,107 @@ +#' @include impl_data.R + +# BioConductor reverse dependencies metrics implementation +# Reuses the viable_revdep_packages() and get_reverse_deps() helpers defined +# in data_reverse_dependencies.R (both are called at runtime, so no load-time +# ordering dependency exists between the two files). + +#' Known BioConductor repository URLs +#' +#' Returns the standard BioConductor repository URLs used to build the package +#' universe for reverse dependency analysis. Unlike \acronym{CRAN}, the +#' BioConductor package universe is spread across several repositories +#' (software, annotation, experiment data and workflows), so all are queried. +#' +#' The `"release"` alias always resolves to the current BioConductor release, so +#' the metric does not need to track the BioConductor version explicitly. A +#' different release (or an internal mirror) can be supplied through the +#' `val.meter.bioc_repos` option. +#' +#' @param version BioConductor release to target. Defaults to `"release"`. +#' @return Character vector of repository URLs. +#' @noRd +get_bioc_repos <- function(version = "release") { + repos <- getOption("val.meter.bioc_repos") + if (!is.null(repos)) { + return(repos) + } + + file.path( + "https://bioconductor.org/packages", + version, + c("bioc", "data/annotation", "data/experiment", "workflows") + ) +} + +impl_data( + "bioc_reverse_dependencies", + class = class_character, + metric = FALSE, + tags = c("adoption", "transient"), + permissions = c("network"), + title = "BioConductor Reverse Dependencies", + description = paste( + "The names of packages on \\acronym{BioConductor} that directly depend on", + "this package through \\code{Depends}, \\code{Imports}, or", + "\\code{LinkingTo} fields." + ) +) + +impl_data( + "bioc_reverse_dependencies", + for_resource = cran_repo_resource, + function(pkg, resource, field, ...) { + # the BioConductor package universe is independent of the package's own + # resource, so it is queried from the known BioConductor repositories + # rather than from resource@repo. + bioc_matrix <- viable_revdep_packages(repos = get_bioc_repos()) + + get_reverse_deps( + pkg$name, + bioc_matrix, + dependencies = c("Depends", "Imports", "LinkingTo") + ) + } +) + +impl_data( + "bioc_reverse_dependencies", + for_resource = mock_resource, + function(pkg, resource, field, ...) { + # Simulate reverse dependencies with a random sample of package names + sample( + paste0("mockbiocpkg", seq_len(10)), + size = min(rpois(1, 2), 10), + replace = FALSE + ) + } +) + + +impl_data( + "bioc_reverse_dependencies_count", + class = class_integer, + metric = TRUE, + tags = c("adoption", "transient"), + permissions = c(), + title = "BioConductor Reverse Dependencies Count", + + description = paste( + "The number of packages on \\acronym{BioConductor} that directly depend", + "on this package through \\code{Depends}, \\code{Imports}, or", + "\\code{LinkingTo} fields. This metric reflects adoption within the", + "BioConductor ecosystem and indicates how many packages would be affected", + "by breaking changes. Higher counts suggest wider usage and community", + "trust, but also greater responsibility for maintaining backward", + "compatibility." + ) +) + +impl_data( + "bioc_reverse_dependencies_count", + for_resource = new_union(cran_repo_resource, mock_resource), + function(pkg, resource, field, ...) { + # Just count the length of rev dep vector + length(pkg$bioc_reverse_dependencies) + } +) diff --git a/tests/testthat/test-data_bioc_reverse_dependencies.R b/tests/testthat/test-data_bioc_reverse_dependencies.R new file mode 100644 index 0000000..3172cec --- /dev/null +++ b/tests/testthat/test-data_bioc_reverse_dependencies.R @@ -0,0 +1,138 @@ +# Helper to create a mock BioConductor package matrix for testing +create_mock_bioc_matrix <- function() { + matrix( + c( + # Package, Version, Priority, Depends, Imports, LinkingTo, Suggests + "biocA", "1.0.0", NA, "R (>= 3.5.0), targetpkg", NA, NA, NA, + "biocB", "2.0.0", NA, "R (>= 4.0.0)", "targetpkg, utils", NA, NA, + "biocC", "1.5.0", NA, NA, NA, NA, "targetpkg, testthat", + "biocD", "3.0.0", NA, NA, "targetpkg", NA, NA, + "biocE", "1.2.0", NA, NA, "otherpkg", NA, NA + ), + nrow = 5, + ncol = 7, + byrow = TRUE, + dimnames = list( + c("1", "2", "3", "4", "5"), + c( + "Package", "Version", "Priority", "Depends", + "Imports", "LinkingTo", "Suggests" + ) + ) + ) +} + +describe("get_bioc_repos", { + it("returns the standard BioConductor repository URLs", { + repos <- get_bioc_repos() + + expect_type(repos, "character") + expect_true(length(repos) >= 1) + expect_true(all(grepl("^https://bioconductor.org/packages/", repos))) + expect_true(any(grepl("/bioc$", repos))) + }) + + it("targets a specific release when asked", { + expect_true(all(grepl("/3.18/", get_bioc_repos(version = "3.18")))) + }) + + it("honours the val.meter.bioc_repos option override", { + withr::with_options( + list(val.meter.bioc_repos = "file:///tmp/fake-bioc"), + expect_equal(get_bioc_repos(), "file:///tmp/fake-bioc") + ) + }) +}) + +describe("bioc_reverse_dependencies metric behavior with mocked data", { + it("returns dependent package names when the BioC universe is mocked", { + mock_matrix <- create_mock_bioc_matrix() + + with_mocked_bindings( + viable_revdep_packages = function(repos) mock_matrix, + .package = "val.meter", + { + p <- pkg( + cran_repo_resource( + package = "targetpkg", + repo = "https://cloud.r-project.org/" + ), + permissions = permissions("network") + ) + + deps <- p$bioc_reverse_dependencies + + expect_type(deps, "character") + # strong deps only (Depends/Imports/LinkingTo), not Suggests + expect_true(all(c("biocA", "biocB", "biocD") %in% deps)) + expect_false("biocC" %in% deps) + } + ) + }) + + it("returns an empty character vector when no reverse deps exist", { + mock_matrix <- create_mock_bioc_matrix() + + with_mocked_bindings( + viable_revdep_packages = function(repos) mock_matrix, + .package = "val.meter", + { + p <- pkg( + cran_repo_resource( + package = "lonelypkg", + repo = "https://cloud.r-project.org/" + ), + permissions = permissions("network") + ) + + deps <- p$bioc_reverse_dependencies + + expect_type(deps, "character") + expect_length(deps, 0) + } + ) + }) + + it("generates realistic mock data for a mock resource (offline)", { + p <- pkg( + mock_resource(package = "fakepkg", version = "1.2.3"), + permissions("network") + ) + + deps <- p$bioc_reverse_dependencies + + expect_type(deps, "character") + expect_true(length(deps) >= 0 && length(deps) <= 10) + }) +}) + +describe("bioc_reverse_dependencies_count metric behavior", { + it("returns integer count matching dependency vector length", { + mock_matrix <- create_mock_bioc_matrix() + + with_mocked_bindings( + viable_revdep_packages = function(repos) mock_matrix, + .package = "val.meter", + { + p <- pkg( + cran_repo_resource( + package = "targetpkg", + repo = "https://cloud.r-project.org/" + ), + permissions = permissions("network") + ) + + count <- p$bioc_reverse_dependencies_count + deps <- p$bioc_reverse_dependencies + + expect_type(count, "integer") + expect_length(count, 1) + expect_equal(count, length(deps)) + } + ) + }) + + it("is registered as a metric", { + expect_true("bioc_reverse_dependencies_count" %in% names(metrics())) + }) +})