From 0eecd630b0301a987b58dca1ea8e9535a0c044b2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 26 Jul 2026 19:49:55 +0000 Subject: [PATCH] test: cover migrated centrality, community, and centralization signatures Add argument-coverage tests for the 25 functions whose optional arguments moved behind the ellipsis: every keyword-only tail argument is passed by name with a non-default value where the semantics allow, and the legacy positional call is exercised through its deprecation warning. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016M32izVHZPfxAqemAe4BrX --- tests/testthat/_snaps/community.md | 12 + tests/testthat/test-centrality.R | 321 +++++++++++++++++++++++++++ tests/testthat/test-centralization.R | 107 +++++++++ tests/testthat/test-community.R | 277 +++++++++++++++++++++++ 4 files changed, 717 insertions(+) diff --git a/tests/testthat/_snaps/community.md b/tests/testthat/_snaps/community.md index 244b09a133d..b9b5d32ceed 100644 --- a/tests/testthat/_snaps/community.md +++ b/tests/testthat/_snaps/community.md @@ -6,3 +6,15 @@ Error: ! The `membership` argument of `modularity_matrix()` is no longer used as of igraph 2.1.0. +# modularity_matrix() covers migrated tail args and positional recovery + + Code + modularity_matrix(karate, rep(1, vcount(karate)), rep(1, ecount(karate))) + Condition + Warning: + Calling `modularity_matrix()` with positional or abbreviated arguments was deprecated in igraph 3.0.0. + i Detected call: modularity_matrix(graph, membership, weights) + i Use instead: modularity_matrix(graph, membership, weights = ) + Error: + ! The `membership` argument of `modularity_matrix()` is no longer used as of igraph 2.1.0. + diff --git a/tests/testthat/test-centrality.R b/tests/testthat/test-centrality.R index fc88d7a6122..5e647123213 100644 --- a/tests/testthat/test-centrality.R +++ b/tests/testthat/test-centrality.R @@ -936,3 +936,324 @@ test_that("arpack() errors well", { ) }) }) + +# ---- ellipsis migration: argument coverage ---------------------------- + +test_that("betweenness() covers migrated tail args and positional recovery", { + rlang::local_options(lifecycle_verbosity = "warning") + ring <- make_ring(5) + # The weight attribute is a decoy that the explicit `weights` must override. + E(ring)$weight <- c(100, 1, 1, 1, 1) + + res <- betweenness( + ring, + v = V(ring)[1:3], + directed = FALSE, + weights = rep(1, 5), + normalized = TRUE, + cutoff = 2 + ) + # In C5 with unit weights each vertex lies on exactly one geodesic, + # and normalization divides by (n - 1) * (n - 2) / 2 = 6. + expect_equal(res, rep(1 / 6, 3)) + # A cutoff of 1 removes all paths that have intermediate vertices. + expect_equal( + betweenness(ring, weights = rep(1, 5), cutoff = 1), + rep(0, 5) + ) + + lifecycle::expect_deprecated( + res_legacy <- betweenness(ring, V(ring), FALSE) + ) + expect_identical(res_legacy, betweenness(ring, V(ring), directed = FALSE)) +}) + +test_that("closeness() covers migrated tail args and positional recovery", { + rlang::local_options(lifecycle_verbosity = "warning") + path <- make_graph(c(1, 2, 2, 3), directed = TRUE) + # The weight attribute is a decoy that the explicit `weights` must override. + E(path)$weight <- c(5, 5) + + res <- closeness( + path, + vids = V(path)[3], + mode = "in", + weights = c(1, 1), + normalized = TRUE, + cutoff = -1 + ) + # Vertex 3 is reached with distances 1 and 2, so 2 / (1 + 2). + expect_equal(res, 2 / 3) + # With a cutoff of 1 only the direct predecessor remains. + expect_equal( + closeness( + path, + vids = V(path)[3], + mode = "in", + weights = c(1, 1), + cutoff = 1 + ), + 1 + ) + + lifecycle::expect_deprecated( + res_legacy <- closeness(path, V(path), "in") + ) + expect_identical(res_legacy, closeness(path, V(path), mode = "in")) +}) + +test_that("edge_betweenness() covers migrated tail args and positional recovery", { + rlang::local_options(lifecycle_verbosity = "warning") + ring <- make_ring(5) + # The weight attribute is a decoy that the explicit `weights` must override. + E(ring)$weight <- c(100, 1, 1, 1, 1) + + res <- edge_betweenness( + ring, + e = E(ring)[1:3], + directed = FALSE, + weights = rep(1, 5), + cutoff = -1 + ) + # In C5 with unit weights each edge carries three geodesics. + expect_equal(res, rep(3, 3)) + # With a cutoff of 1 each edge only carries the path of its endpoints. + expect_equal( + edge_betweenness(ring, e = E(ring)[1:3], weights = rep(1, 5), cutoff = 1), + rep(1, 3) + ) + + lifecycle::expect_deprecated( + res_legacy <- edge_betweenness(ring, E(ring), FALSE) + ) + expect_identical( + res_legacy, + edge_betweenness(ring, E(ring), directed = FALSE) + ) +}) + +test_that("harmonic_centrality() covers migrated tail args and positional recovery", { + rlang::local_options(lifecycle_verbosity = "warning") + path <- make_graph(c(1, 2, 2, 3), directed = TRUE) + # The weight attribute is a decoy that the explicit `weights` must override. + E(path)$weight <- c(5, 5) + + res <- harmonic_centrality( + path, + vids = V(path)[3], + mode = "in", + weights = c(1, 1), + normalized = TRUE, + cutoff = -1 + ) + # Vertex 3 collects inverse distances 1 + 1/2, normalized by n - 1 = 2. + expect_equal(res, 0.75) + # The cutoff drops the two-step path from vertex 1. + expect_equal( + harmonic_centrality( + path, + vids = V(path)[3], + mode = "in", + weights = c(1, 1), + normalized = TRUE, + cutoff = 1 + ), + 0.5 + ) + + lifecycle::expect_deprecated( + res_legacy <- harmonic_centrality(path, V(path), "in") + ) + expect_identical(res_legacy, harmonic_centrality(path, V(path), mode = "in")) +}) + +test_that("page_rank() covers migrated tail args and positional recovery", { + rlang::local_options(lifecycle_verbosity = "warning") + star <- make_star(10, mode = "undirected") + # The weight attribute is a decoy that the explicit `weights` must override. + E(star)$weight <- c(10, rep(1, 8)) + + # `algo` keeps its default value: + # non-default values select the legacy ARPACK implementation, + # and `options` is only consumed by that implementation. + res <- page_rank( + star, + algo = "prpack", + vids = V(star)[1:5], + directed = FALSE, + damping = 0.9, + personalized = c(1, rep(0, 9)), + weights = rep(1, 9), + options = arpack_defaults() + ) + expect_length(res$vector, 5) + # All personalization mass sits on the hub, so it dominates the leaves, + # and the unit weights keep the leaves interchangeable. + expect_true(res$vector[1] > max(res$vector[-1])) + expect_equal(sd(res$vector[-1]), 0) + # PRPACK ignores the ARPACK options, and `vids` merely subsets the result. + res_full <- page_rank( + star, + directed = FALSE, + damping = 0.9, + personalized = c(1, rep(0, 9)), + weights = rep(1, 9) + ) + expect_equal(res$vector, res_full$vector[1:5]) + expect_equal(sum(res_full$vector), 1) + + lifecycle::expect_deprecated( + res_legacy <- page_rank(star, "prpack") + ) + expect_identical(res_legacy, page_rank(star, algo = "prpack")) +}) + +test_that("strength() covers migrated tail args and positional recovery", { + rlang::local_options(lifecycle_verbosity = "warning") + star <- make_star(6, mode = "out") + star <- add_edges(star, c(2, 2)) + E(star)$weight <- 1:6 + + res <- strength( + star, + vids = V(star)[1:3], + mode = "out", + loops = FALSE, + weights = 1:6 + ) + # Only the hub has non-loop out-edges, with weights 1 to 5. + expect_equal(res, c(15, 0, 0)) + # Counting loops adds the loop weight 6 to vertex 2. + expect_equal( + strength( + star, + vids = V(star)[1:3], + mode = "out", + loops = TRUE, + weights = 1:6 + ), + c(15, 6, 0) + ) + + lifecycle::expect_deprecated( + res_legacy <- strength(star, V(star), "in") + ) + expect_identical(res_legacy, strength(star, V(star), mode = "in")) +}) + +test_that("diversity() covers migrated tail args and positional recovery", { + rlang::local_options(lifecycle_verbosity = "warning") + ring <- make_ring(10) + + res <- diversity(ring, weights = 1:10, vids = V(ring)[1:4]) + # Scaled entropy of the two incident edge weights, e.g. {10, 1} for vertex 1. + expect_equal( + res, + c(0.4394970, 0.9182958, 0.9709506, 0.9852281), + tolerance = 1e-6 + ) + + lifecycle::expect_deprecated( + res_legacy <- diversity(ring, 1:10) + ) + expect_identical(res_legacy, diversity(ring, weights = 1:10)) +}) + +test_that("subgraph_centrality() covers migrated tail args and positional recovery", { + rlang::local_options(lifecycle_verbosity = "warning") + # A square with a loop on vertex 1, so that `diag` has a visible effect. + looped <- make_graph(c(1, 2, 2, 3, 3, 4, 4, 1, 1, 1), directed = FALSE) + + res <- subgraph_centrality(looped, diag = TRUE) + expect_equal( + res, + Matrix::diag(Matrix::expm(as_adjacency_matrix(looped, sparse = FALSE))), + tolerance = 1e-10 + ) + expect_false(isTRUE(all.equal(res, subgraph_centrality(looped)))) + + lifecycle::expect_deprecated( + res_legacy <- subgraph_centrality(looped, TRUE) + ) + expect_identical(res_legacy, subgraph_centrality(looped, diag = TRUE)) +}) + +test_that("power_centrality() covers migrated tail args and positional recovery", { + rlang::local_options(lifecycle_verbosity = "warning") + ring <- make_ring(5) + # The weight attribute is a decoy that the explicit `weights` must override. + E(ring)$weight <- c(1.5, 2.5, 3.5, 4.5, 5.5) + + res <- power_centrality( + ring, + nodes = V(ring)[1:3], + loops = FALSE, + exponent = 0.2, + rescale = TRUE, + tol = 1e-10, + sparse = FALSE, + weights = rep(1, 5) + ) + # With unit weights the ring is symmetric and rescaled scores sum to one. + expect_equal(res, rep(1 / 5, 3)) + + # `loops` toggles the adjacency diagonal of a path with a loop on vertex 2. + looped <- make_graph(c(1, 2, 2, 3, 2, 2), directed = FALSE) + expect_false(isTRUE(all.equal( + power_centrality(looped, loops = TRUE, sparse = FALSE), + power_centrality(looped, loops = FALSE, sparse = FALSE) + ))) + + lifecycle::expect_deprecated( + res_legacy <- power_centrality(looped, V(looped), TRUE) + ) + expect_identical( + res_legacy, + power_centrality(looped, V(looped), loops = TRUE) + ) +}) + +test_that("alpha_centrality() covers migrated tail args and positional recovery", { + rlang::local_options(lifecycle_verbosity = "warning") + # A small DAG with a loop on vertex 3 and a decoy weight attribute. + dag <- make_graph(c(1, 3, 2, 3, 3, 4, 4, 5, 3, 3)) + E(dag)$weight <- rep(7, 5) + + base <- alpha_centrality( + dag, + alpha = 0.5, + loops = TRUE, + weights = NA, + tol = 1e-8, + sparse = FALSE + ) + expect_equal(base, c(1, 1, 4, 3, 2.5)) + # The scores are linear in the exogenous input. + res <- alpha_centrality( + dag, + nodes = V(dag)[1:3], + alpha = 0.5, + loops = TRUE, + exo = 2, + weights = NA, + tol = 1e-8, + sparse = FALSE + ) + expect_equal(res, 2 * base[1:3]) + # Ignoring the loop changes the scores. + expect_false(isTRUE(all.equal( + base, + alpha_centrality( + dag, + alpha = 0.5, + loops = FALSE, + weights = NA, + sparse = FALSE + ) + ))) + + lifecycle::expect_deprecated( + res_legacy <- alpha_centrality(dag, V(dag), 0.75) + ) + expect_identical(res_legacy, alpha_centrality(dag, V(dag), alpha = 0.75)) +}) diff --git a/tests/testthat/test-centralization.R b/tests/testthat/test-centralization.R index 7a9274a3d70..124de4a3e67 100644 --- a/tests/testthat/test-centralization.R +++ b/tests/testthat/test-centralization.R @@ -29,3 +29,110 @@ test_that("centr_eigen works", { g_centr_tmax <- centr_eigen_tmax(g) expect_equal(g_centr$centralization, g_centr_tmax) }) + +# ---- ellipsis migration: argument coverage ---------------------------- + +test_that("centr_degree() covers migrated tail args and positional recovery", { + rlang::local_options(lifecycle_verbosity = "warning") + g <- make_star(5, mode = "in") + + res <- centr_degree(g, mode = "in", loops = FALSE, normalized = FALSE) + # Only the centre receives edges, so the centralization is (n - 1)^2. + expect_equal(res$res, c(4, 0, 0, 0, 0)) + expect_equal(res$centralization, 16) + expect_equal(res$theoretical_max, 16) + + lifecycle::expect_deprecated( + res_legacy <- centr_degree(g, "in") + ) + expect_identical(res_legacy, centr_degree(g, mode = "in")) +}) + +test_that("centr_betw() covers migrated tail args and positional recovery", { + rlang::local_options(lifecycle_verbosity = "warning") + g <- make_star(5, "undirected") + + res <- centr_betw(g, directed = FALSE, normalized = FALSE) + # The centre lies on the geodesics of all 6 leaf pairs. + expect_equal(res$res, c(6, 0, 0, 0, 0)) + expect_equal(res$centralization, 24) + expect_equal(res$theoretical_max, 24) + + lifecycle::expect_deprecated( + res_legacy <- centr_betw(g, FALSE) + ) + expect_identical(res_legacy, centr_betw(g, directed = FALSE)) +}) + +test_that("centr_betw_tmax() covers migrated tail args and positional recovery", { + rlang::local_options(lifecycle_verbosity = "warning") + g <- make_star(5, "undirected") + + # The graph form and the vertex-count form must agree. + expect_equal(centr_betw_tmax(g, directed = FALSE), 24) + expect_equal(centr_betw_tmax(nodes = 5, directed = FALSE), 24) + # And they match what centr_betw() reports. + expect_equal( + centr_betw(g, directed = FALSE, normalized = FALSE)$theoretical_max, + centr_betw_tmax(g, directed = FALSE) + ) + + lifecycle::expect_deprecated( + res_legacy <- centr_betw_tmax(g, 0, FALSE) + ) + expect_identical(res_legacy, centr_betw_tmax(g, directed = FALSE)) +}) + +test_that("centr_clo() covers migrated tail args and positional recovery", { + rlang::local_options(lifecycle_verbosity = "warning") + g <- make_ring(5, directed = TRUE) + + res <- centr_clo(g, mode = "in", normalized = FALSE) + # On a directed ring every vertex has the same in-closeness 4 / 10. + expect_equal(res$res, rep(0.4, 5)) + expect_equal(res$centralization, 0) + expect_equal(res$theoretical_max, 3.2) + # Ignoring edge directions shortens the paths to 4 / 6. + expect_equal( + centr_clo(g, mode = "all", normalized = FALSE)$res, + rep(2 / 3, 5) + ) + + lifecycle::expect_deprecated( + res_legacy <- centr_clo(g, "in") + ) + expect_identical(res_legacy, centr_clo(g, mode = "in")) +}) + +test_that("centr_clo_tmax() covers migrated tail args and positional recovery", { + rlang::local_options(lifecycle_verbosity = "warning") + g <- make_ring(5, directed = TRUE) + + # The graph form and the vertex-count form must agree. + expect_equal(centr_clo_tmax(g, mode = "in"), 3.2) + expect_equal(centr_clo_tmax(nodes = 5, mode = "in"), 3.2) + # And they match what centr_clo() reports. + expect_equal( + centr_clo(g, mode = "in", normalized = FALSE)$theoretical_max, + centr_clo_tmax(g, mode = "in") + ) + + lifecycle::expect_deprecated( + res_legacy <- centr_clo_tmax(g, 0, "in") + ) + expect_identical(res_legacy, centr_clo_tmax(g, mode = "in")) +}) + +test_that("centralize() covers migrated tail args and positional recovery", { + rlang::local_options(lifecycle_verbosity = "warning") + scores <- degree(make_star(5, "undirected")) + + # The star's degree sequence gives sum(max - x) = 12. + expect_equal(centralize(scores, theoretical.max = 12, normalized = TRUE), 1) + expect_equal(centralize(scores, normalized = FALSE), 12) + + lifecycle::expect_deprecated( + res_legacy <- centralize(scores, 12) + ) + expect_identical(res_legacy, centralize(scores, theoretical.max = 12)) +}) diff --git a/tests/testthat/test-community.R b/tests/testthat/test-community.R index 2b147ae0bc4..65183998533 100644 --- a/tests/testthat/test-community.R +++ b/tests/testthat/test-community.R @@ -704,3 +704,280 @@ test_that("modularity() handles NA weights correctly", { mod_with_empty <- modularity(gw, membership_vec, weights = numeric(0)) expect_equal(mod_with_empty, mod_without_weights) }) + +# ---- ellipsis migration: argument coverage ---------------------------- + +test_that("cluster_edge_betweenness() covers migrated tail args and positional recovery", { + rlang::local_options(lifecycle_verbosity = "warning") + karate <- make_graph("Zachary") + + # Weighted runs that return a membership warn + # that the split is picked at the maximal modularity. + expect_warning( + res <- cluster_edge_betweenness( + karate, + weights = rep(1, ecount(karate)), + directed = FALSE + ), + "highest modularity score" + ) + # Unit weights reproduce the unweighted clustering. + expect_identical( + membership(res), + membership(cluster_edge_betweenness(karate)) + ) + expect_length(res, 5) + + # All optional result components can be switched off. + bare <- cluster_edge_betweenness( + karate, + edge.betweenness = FALSE, + merges = FALSE, + bridges = FALSE, + modularity = FALSE, + membership = FALSE + ) + expect_null(bare$edge.betweenness) + expect_length(bare$merges, 0) + expect_length(bare$bridges, 0) + expect_null(bare$modularity) + expect_length(bare$membership, 0) + expect_length(bare$removed.edges, ecount(karate)) + + w <- rep(1:2, length.out = ecount(karate)) + lifecycle::expect_deprecated( + expect_warning( + res_legacy <- cluster_edge_betweenness(karate, w), + "highest modularity score" + ) + ) + expect_warning( + res_named <- cluster_edge_betweenness(karate, weights = w), + "highest modularity score" + ) + expect_identical(res_legacy, res_named) +}) + +test_that("cluster_fast_greedy() covers migrated tail args and positional recovery", { + rlang::local_options(lifecycle_verbosity = "warning") + karate <- make_graph("Zachary") + + res <- cluster_fast_greedy( + karate, + merges = FALSE, + modularity = FALSE, + membership = TRUE, + weights = rep(1, ecount(karate)) + ) + # Unit weights reproduce the unweighted membership, without dendrogram parts. + expect_identical(membership(res), membership(cluster_fast_greedy(karate))) + expect_length(res$merges, 0) + expect_null(res$modularity) + + lifecycle::expect_deprecated( + res_legacy <- cluster_fast_greedy(karate, FALSE) + ) + expect_identical(res_legacy, cluster_fast_greedy(karate, merges = FALSE)) +}) + +test_that("cluster_infomap() covers migrated tail args and positional recovery", { + rlang::local_options(lifecycle_verbosity = "warning") + igraph_local_seed(42) + karate <- make_graph("Zachary") + + res <- cluster_infomap( + karate, + e.weights = rep(1, ecount(karate)), + v.weights = rep(1, vcount(karate)), + nb.trials = 3, + modularity = FALSE + ) + # The stochastic result is still a valid partition of all vertices. + expect_s3_class(res, "communities") + expect_length(membership(res), vcount(karate)) + expect_in(membership(res), seq_len(max(membership(res)))) + expect_null(res$modularity) + + ew <- rep(1, ecount(karate)) + set.seed(1) + lifecycle::expect_deprecated( + res_legacy <- cluster_infomap(karate, ew) + ) + set.seed(1) + expect_identical(res_legacy, cluster_infomap(karate, e.weights = ew)) +}) + +test_that("cluster_louvain() covers migrated tail args and positional recovery", { + rlang::local_options(lifecycle_verbosity = "warning") + igraph_local_seed(42) + karate <- make_graph("Zachary") + + res <- cluster_louvain( + karate, + weights = rep(1, ecount(karate)), + resolution = 0.01 + ) + # A tiny resolution merges everything into a single community. + expect_equal(max(membership(res)), 1) + expect_length(membership(res), vcount(karate)) + + w <- rep(1:2, length.out = ecount(karate)) + set.seed(1) + lifecycle::expect_deprecated( + res_legacy <- cluster_louvain(karate, w) + ) + set.seed(1) + expect_identical(res_legacy, cluster_louvain(karate, weights = w)) +}) + +test_that("cluster_optimal() covers migrated tail args and positional recovery", { + skip_if_no_glpk() + rlang::local_options(lifecycle_verbosity = "warning") + karate <- make_graph("Zachary") + + # Uniformly scaled weights leave partition and modularity unchanged. + res <- cluster_optimal(karate, weights = rep(2, ecount(karate))) + ref <- cluster_optimal(karate) + expect_identical(membership(res), membership(ref)) + expect_identical(res$modularity, ref$modularity) + + w <- rep(1:2, length.out = ecount(karate)) + lifecycle::expect_deprecated( + res_legacy <- cluster_optimal(karate, w) + ) + expect_identical(res_legacy, cluster_optimal(karate, weights = w)) +}) + +test_that("cluster_spinglass() covers migrated tail args and positional recovery", { + rlang::local_options(lifecycle_verbosity = "warning") + igraph_local_seed(42) + karate <- make_graph("Zachary") + + res <- cluster_spinglass( + karate, + weights = rep(1, ecount(karate)), + spins = 10, + parupdate = TRUE, + start.temp = 0.8, + stop.temp = 0.05, + cool.fact = 0.95, + update.rule = "random", + gamma = 1.2, + implementation = "orig" + ) + # The stochastic result is still a valid partition of all vertices. + expect_s3_class(res, "communities") + expect_length(membership(res), vcount(karate)) + expect_in(membership(res), seq_len(max(membership(res)))) + + # The negative-weight implementation accepts its own gamma. + res_neg <- cluster_spinglass( + karate, + spins = 5, + implementation = "neg", + gamma.minus = 0.5 + ) + expect_s3_class(res_neg, "communities") + expect_length(membership(res_neg), vcount(karate)) + + # Restricting to a single vertex returns that vertex's community. + res_vertex <- cluster_spinglass( + karate, + vertex = 1, + spins = 10, + update.rule = "config", + gamma = 1 + ) + expect_true(1 %in% res_vertex$community) + expect_gte(res_vertex$cohesion, 0) + + w <- rep(1, ecount(karate)) + set.seed(1) + lifecycle::expect_deprecated( + res_legacy <- cluster_spinglass(karate, w) + ) + set.seed(1) + expect_identical(res_legacy, cluster_spinglass(karate, weights = w)) +}) + +test_that("cluster_walktrap() covers migrated tail args and positional recovery", { + rlang::local_options(lifecycle_verbosity = "warning") + karate <- make_graph("Zachary") + + res <- cluster_walktrap( + karate, + weights = rep(1, ecount(karate)), + steps = 6 + ) + # Longer walks merge the karate club into three communities. + expect_length(res, 3) + expect_length(membership(res), vcount(karate)) + + # All optional result components can be switched off. + bare <- cluster_walktrap( + karate, + merges = FALSE, + modularity = FALSE, + membership = FALSE + ) + expect_null(bare$merges) + expect_null(bare$modularity) + expect_null(bare$membership) + + w <- rep(1:2, length.out = ecount(karate)) + lifecycle::expect_deprecated( + res_legacy <- cluster_walktrap(karate, w) + ) + expect_identical(res_legacy, cluster_walktrap(karate, weights = w)) +}) + +test_that("make_clusters() covers migrated tail args and positional recovery", { + rlang::local_options(lifecycle_verbosity = "warning") + karate <- make_graph("Zachary") + memb <- rep(1:2, each = 17) + mrg <- matrix(c(1, 2), ncol = 2) + + res <- make_clusters( + karate, + membership = memb, + algorithm = "custom", + merges = mrg, + modularity = FALSE + ) + expect_s3_class(res, "communities") + expect_equal(membership(res), memb, ignore_attr = "class") + expect_identical(algorithm(res), "custom") + expect_identical(res$merges, mrg) + expect_null(res$modularity) + + lifecycle::expect_deprecated( + res_legacy <- make_clusters(karate, memb, "custom") + ) + expect_identical( + res_legacy, + make_clusters(karate, memb, algorithm = "custom") + ) +}) + +test_that("modularity_matrix() covers migrated tail args and positional recovery", { + rlang::local_options(lifecycle_verbosity = "warning") + karate <- make_graph("Zachary") + + base <- modularity_matrix(karate) + res <- modularity_matrix( + karate, + weights = rep(1, ecount(karate)), + resolution = 2, + directed = FALSE + ) + # B(res) = A - res * K, so doubling the resolution gives 2 * B(1) - A. + A <- as_adjacency_matrix(karate, sparse = FALSE) + expect_equal(res, 2 * base - A) + + # The head argument `membership` is hard-deprecated, + # so the legacy positional call first warns about recovering `weights` + # and then fails on the supplied membership. + expect_snapshot(error = TRUE, { + modularity_matrix(karate, rep(1, vcount(karate)), rep(1, ecount(karate))) + }) +})