diff --git a/.github/workflows/R-CMD-check.yml b/.github/workflows/R-CMD-check.yml index 31eaf2107..5cbba4b7f 100644 --- a/.github/workflows/R-CMD-check.yml +++ b/.github/workflows/R-CMD-check.yml @@ -237,6 +237,21 @@ jobs: needs: | check coverage + # MaxMin url:: -- this is the Windows leg, so it cannot use the Linux + # tarball the `runner.os != 'Windows'` step below selects. Without a + # url:: reference the solve fails outright ("Can't find package called + # MaxMin"): pak does not read `Additional_repositories`, so the drat's + # indexed contrib/ layout is invisible to it. See ms609/packages' + # tools/publish-maxmin.R, which publishes this flat alias for exactly + # this purpose. + extra-packages: | + url::https://ms609.github.io/packages/bin/windows/MaxMin_latest.zip + # install-pandoc must stay explicit -- see the long note in + # agent-check.yml's windows leg. Auto-detect runs a second, fresh + # `pak::pkg_deps(".")` solve that cannot see extra-packages, so it + # fails on MaxMin by name; the branch only fires when pandoc is off + # PATH, which is why only the non-Linux legs hit it. + install-pandoc: true # cache-version bumped to 2: the v1 caches were built while a # project-level .Rprofile shadowed ~/.Rprofile, so every package # in them was compiled from source. Retire them once. @@ -327,8 +342,18 @@ jobs: with: needs: | check + # MaxMin url:: -- same reason as the core Windows leg. Added + # pre-emptively: this job was skipped in the run that diagnosed the + # failure (it is gated on `detect app changes`), so unlike the core leg + # it is not yet observed failing -- but it resolves the same + # `needs: check` Suggests on the same platform, so it would. extra-packages: | local::. + url::https://ms609.github.io/packages/bin/windows/MaxMin_latest.zip + # install-pandoc explicit for the same reason as the core Windows leg + # (auto-detect's `pak::pkg_deps(".")` solve cannot see extra-packages). + # `false` here: this job runs shinytest2, not vignettes. + install-pandoc: false # cache-version bumped to 2: the v1 caches were built while a # project-level .Rprofile shadowed ~/.Rprofile, so every package # in them was compiled from source. Retire them once. @@ -410,6 +435,19 @@ jobs: with: needs: | check + # MaxMin url:: -- same mechanism as the Windows and Linux legs, picked + # per architecture (macOS-latest is arm64; macos-15-intel is x86_64). + # Added pre-emptively and NOT yet observed failing: this job is + # `needs: core`, so it was skipped while the Windows leg was red. + # Fixing that leg unskips this one, which would then hit the identical + # unresolvable-Suggests error -- note `_R_CHECK_FORCE_SUGGESTS_: false` + # does not help, since it governs R CMD check, not pak's solve. + extra-packages: | + ${{ matrix.config.os == 'macos-15-intel' && 'url::https://ms609.github.io/packages/bin/macosx/big-sur-x86_64/MaxMin_latest.tgz' || 'url::https://ms609.github.io/packages/bin/macosx/big-sur-arm64/MaxMin_latest.tgz' }} + # install-pandoc explicit for the same reason as the Windows legs; also + # pre-emptive, since whether a macOS runner ships pandoc on PATH is + # exactly the condition that decides whether the bad branch fires. + install-pandoc: true # cache-version bumped to 2: the v1 caches were built while a # project-level .Rprofile shadowed ~/.Rprofile, so every package # in them was compiled from source. Retire them once. diff --git a/.github/workflows/agent-check.yml b/.github/workflows/agent-check.yml index 14d13374b..1f8319baa 100644 --- a/.github/workflows/agent-check.yml +++ b/.github/workflows/agent-check.yml @@ -115,6 +115,28 @@ jobs: uses: r-lib/actions/setup-r-dependencies@v2 with: needs: check + # MaxMin url:: -- the Windows counterpart of the arm64 leg's line, and + # the reason this leg used to fail before compiling anything. MaxMin is + # not on CRAN, and pak does NOT read `Additional_repositories` from + # DESCRIPTION (confirmed empirically 2026-08-03, recorded in + # ms609/packages' tools/publish-maxmin.R), so the indexed drat layout + # alone leaves it unresolvable and the solve dies with "Can't find + # package called MaxMin". Naming the flat, unversioned alias directly + # is the mechanism that publish script exists to provide; it always + # points at the current build, so it needs no edit when MaxMin moves. + extra-packages: | + url::https://ms609.github.io/packages/bin/windows/MaxMin_latest.zip + # install-pandoc MUST stay explicit here, and the url:: above is not + # sufficient without it. Left unset, setup-r-dependencies auto-detects + # by running `pak::pkg_deps(".", dependencies = list(direct = "all"))` + # -- a SECOND, fresh solve that sees neither the lockfile nor + # extra-packages, so it looks MaxMin up by name and dies with + # "* local::.: Can't install dependency MaxMin" even though MaxMin has + # just been installed successfully. That branch only runs when pandoc + # is absent from PATH, which is exactly why the Linux legs never hit it + # (their runners ship pandoc) and this one did. `true` rather than + # `false` because this leg checks with vignettes. + install-pandoc: true # cache-version bumped to 2: the v1 caches were built while a # project-level .Rprofile shadowed ~/.Rprofile, so every package # in them was compiled from source. Retire them once. diff --git a/inst/Parsimony/tests/testthat/_snaps/Distribution/Distribution-001-ClusterCons.zip b/inst/Parsimony/tests/testthat/_snaps/Distribution/Distribution-001-ClusterCons.zip index e5d6808f2..70800bcbb 100644 --- a/inst/Parsimony/tests/testthat/_snaps/Distribution/Distribution-001-ClusterCons.zip +++ b/inst/Parsimony/tests/testthat/_snaps/Distribution/Distribution-001-ClusterCons.zip @@ -48,7 +48,7 @@ dataset <- ReadAsPhyDat(dataFile) treeFile <- dataFile # Test mode trees <- read.nexus(treeFile) set.seed() -trees <- WideSample(trees[1:125], 48) +trees <- WideSample(trees[77:125], 48) # Compute tree distances dists <- TreeDist::ClusteringInfoDistance(trees) @@ -92,30 +92,77 @@ threshold <- 0.5 bestMethodId <- which.max(c(threshold, pamSil, hSil, kSil)) bestCluster <- c("none", "pam", "hmm", "kmn")[bestMethodId] -# No significant clustering was found. -clustering <- 1 # Assign all trees to single cluster +# Best clustering was part. around medoids: +# Silhouette coefficient = 0.83471 -# No clustering structure: Plot consensus tree -cons <- ConsensusWithout( - trees = trees, - tip = c("Ussunia", "Craniops", "Paterimitra", "Yuganotheca_elegans", "Gasconsia"), - p = 1 -) +# Store the cluster to which each tree is optimally assigned: +clustering <- switch(bestCluster, pam = pamCluster, hmm = hCluster, kmn = kCluster, 1) +nClusters <- length(unique(clustering)) +clusterCol <- c("#7a6c36", "#864885") # Arbitrarily + + +# Plot consensus of each tree cluster +par(mfrow = c(1, 2)) # Plotting area layout +tipCols <- Rogue::ColByStability(trees) # Colour tips by stability + +# Compute all cluster consensus trees: +allCons <- lapply(seq_len(2), function(i) { + clusterTrees <- trees[clustering == i] + cons <- ConsensusWithout( + trees = clusterTrees, + tip = "Yuganotheca_elegans", + p = 1 + ) # Root tree cons <- RootTree(cons, "Namacalathus") -# Set unit edge length -cons$edge.length <- rep.int(1, nrow(cons$edge)) -# Rotate nodes, to display clades in order of size -cons <- SortTree(cons, order = names(dataset)) -plottedTree <- cons # Store for future reference -tipCols <- Rogue::ColByStability(trees)[cons$tip.label] - -# Plot consensus tree -plot( - cons, - edge.width = 2, # Widen lines - font = 3, # Italicize labels - cex = 0.83, # Shrink tip font size - tip.color = tipCols -) + cons$edge.length <- rep.int(1, nrow(cons$edge)) + TreeTools::SortTree(cons) +}) + +# Compare splits across clusters to highlight unique edges +allSplits <- lapply(allCons, TreeTools::as.Splits) + +# Plot each consensus tree in turn: +for (i in seq_len(2)) { + cons <- allCons[[i]] + nTip <- ape::Ntip(cons) + mySplits <- allSplits[[i]] + otherIdx <- setdiff(seq_len(2), i) + shared <- integer(length(mySplits)) + for (j in otherIdx) { + if (length(allSplits[[j]]) > 0) + shared <- shared + (mySplits %in% allSplits[[j]]) + } + uniqueness <- 1 - shared / length(otherIdx) + greyRgb <- col2rgb("grey70")[, 1] + colRgb <- col2rgb(clusterCol[i])[, 1] + edgeCol <- rep(clusterCol[i], nrow(cons$edge)) + splitNodes <- as.integer(names(mySplits)) + for (e in seq_len(nrow(cons$edge))) { + child <- cons$edge[e, 2] + if (child > nTip) { + si <- match(child, splitNodes) + if (!is.na(si)) { + bl <- greyRgb + (colRgb - greyRgb) * uniqueness[si] + edgeCol[e] <- rgb(bl[1], bl[2], bl[3], maxColorValue = 255) + } + } + } + plot( + cons, + edge.width = 2, + font = 3, + cex = 0.83, + edge.color = edgeCol, + tip.color = tipCols[cons$tip.label] + ) + legend( + "bottomright", + paste("Cluster", i), + pch = 15, + pt.cex = 1.5, + col = clusterCol[i], + bty = "n" + ) +} diff --git a/inst/Parsimony/tests/testthat/setup.R b/inst/Parsimony/tests/testthat/setup.R index 1cdcb8aa0..79b14ccec 100644 --- a/inst/Parsimony/tests/testthat/setup.R +++ b/inst/Parsimony/tests/testthat/setup.R @@ -47,11 +47,32 @@ new_app_driver <- function(name, ...) { # Retry a couple of times before propagating, so a transient chromote hiccup # doesn't fail an otherwise-healthy run. A first-try success is the common path # and incurs no delay. +# +# debounceWait is load-bearing, not padding. mod_data's nTree / treeRange +# watchers are debounce()d (aJiffy = 42 ms, typingJiffy = 105 ms), and a pending +# debounce timer does NOT make Shiny busy -- there is nothing to recompute until +# it expires, so wait_for_idle() can return before a debounced watcher has even +# seen the input the test just set. A download captured at that point encodes the +# state BEFORE the last set_inputs(); on a machine where the timer does fire in +# time it encodes the state after. The same test then yields different snapshots +# on different machines. Sleeping past the longest debounce window and waiting +# again lets that work start and finish, which is what makes these baselines +# reproducible rather than timing-dependent. +# +# This is how the Distribution baseline came to record `trees[1:125]` for a +# state its test had set to c(77, 125) -- noticed only once the MaxMin +# dependency fix let CI reach the suite at all. # --------------------------------------------------------------------------- -wait_stable <- function(app, timeout = 30000, attempts = 3L) { +wait_stable <- function(app, timeout = 30000, attempts = 3L, + debounceWait = 0.25) { for (i in seq_len(attempts)) { ok <- tryCatch( - { app$wait_for_idle(timeout = timeout); TRUE }, + { + app$wait_for_idle(timeout = timeout) + Sys.sleep(debounceWait) + app$wait_for_idle(timeout = timeout) + TRUE + }, error = function(e) if (i == attempts) stop(e) else FALSE ) if (isTRUE(ok)) break diff --git a/inst/Parsimony/tests/testthat/test-Distribution.R b/inst/Parsimony/tests/testthat/test-Distribution.R index 39ed13826..01365e6ec 100644 --- a/inst/Parsimony/tests/testthat/test-Distribution.R +++ b/inst/Parsimony/tests/testthat/test-Distribution.R @@ -12,6 +12,15 @@ test_that("Distribution and concordance plots render correctly", { } app$set_inputs(`data-dataSource` = "Sun2018") + # Let the dataset load finish before setting anything that depends on it. + # UpdateAllTrees() resets treeRange to the full 1:nTrees span whenever the tree + # count changes (mod_data.R), so a `data-treeRange` that lands mid-load is + # silently clobbered by the reset -- and set_inputs()' default 4 s wait is not + # long enough for Sun2018's 125 trees, as the "Server did not update any output + # values within 4 seconds" warning on this line reported. Whether the clobber + # beat the capture varied by machine, which is what made this snapshot record + # trees[1:125] on some runs and trees[77:125] on others. + wait_stable(app) app$set_inputs(plotFormat = "clus") app$set_inputs(`data-treeRange` = c(77, 125)) wait_stable(app)