From da803a8aef5e5d1eddec24bc85673d4a941f7b2f Mon Sep 17 00:00:00 2001 From: Chris Kypridemos Date: Wed, 21 Jan 2026 13:37:10 +0000 Subject: [PATCH 1/3] Improve tinytest unit tests and optimize GitHub CI workflows - Add test-package_ops.R with 20 new tests for detach_package, dependencies, installLocalPackage, and installLocalPackageIfChanged - Enable parallel test execution in tinytest runner with CI-aware CPU detection (limits to 2 cores in GitHub Actions) - Add concurrency control to all workflows to cancel in-progress runs on new commits - Add CI and _R_CHECK_TESTS_NLINES_ environment variables for better test performance - Update .Rbuildignore to exclude .claude directory Co-Authored-By: Claude Opus 4.5 --- .Rbuildignore | 3 + .github/workflows/R-CMD-check.yml | 11 +- .github/workflows/rchk.yml | 5 + .github/workflows/test-coverage.yml | 5 + inst/tinytest/test-package_ops.R | 160 ++++++++++++++++++++++++++++ tests/tinytest.R | 22 +++- 6 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 inst/tinytest/test-package_ops.R diff --git a/.Rbuildignore b/.Rbuildignore index 0153769..f870745 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -14,3 +14,6 @@ ^\.history/.*$ ^\.\.Rcheck$ ^CKutils\.Rcheck$ +^CLAUDE\.md$ +^\.claude$ +^\.claude/.*$ diff --git a/.github/workflows/R-CMD-check.yml b/.github/workflows/R-CMD-check.yml index 0c4d547..9ba3f26 100644 --- a/.github/workflows/R-CMD-check.yml +++ b/.github/workflows/R-CMD-check.yml @@ -35,6 +35,11 @@ on: name: R-CMD-check +# Cancel in-progress runs when a new run is triggered on the same branch +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: R-CMD-check: runs-on: ${{ matrix.config.os }} @@ -66,11 +71,15 @@ jobs: - {os: ubuntu-22.04, r: 'release'} # Ubuntu 22.04 LTS for broader compatibility # - {os: macos-12, r: 'release'} # Intel Mac specifically (ddisabled for now due to slow execution) - # Environment variables for authentication and debugging + # Environment variables for authentication, debugging, and performance env: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} R_KEEP_PKG_SOURCE: yes HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1 + # Enable parallel testing in tinytest + CI: true + # Set number of CPUs for R operations + _R_CHECK_TESTS_NLINES_: 0 steps: # Step 1: Checkout the repository code diff --git a/.github/workflows/rchk.yml b/.github/workflows/rchk.yml index f14356d..d6925b7 100644 --- a/.github/workflows/rchk.yml +++ b/.github/workflows/rchk.yml @@ -33,6 +33,11 @@ on: name: 'rchk' +# Cancel in-progress runs when a new run is triggered on the same branch +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: rchk: runs-on: ubuntu-latest diff --git a/.github/workflows/test-coverage.yml b/.github/workflows/test-coverage.yml index c0f176d..1f1f296 100644 --- a/.github/workflows/test-coverage.yml +++ b/.github/workflows/test-coverage.yml @@ -36,6 +36,11 @@ on: name: test-coverage +# Cancel in-progress runs when a new run is triggered on the same branch +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: test-coverage: runs-on: ubuntu-latest # Use Ubuntu for consistent coverage measurement diff --git a/inst/tinytest/test-package_ops.R b/inst/tinytest/test-package_ops.R new file mode 100644 index 0000000..0bfa03a --- /dev/null +++ b/inst/tinytest/test-package_ops.R @@ -0,0 +1,160 @@ +# ============================================================================= +# Tests for package_ops.R +# ============================================================================= +# Tests for detach_package, dependencies functions +# Note: installLocalPackage and installLocalPackageIfChanged are skipped +# as they require side effects (package installation, roxygen2 processing) + +# Note: This test file is designed to run via tinytest::test_package("CKutils") +# which automatically loads the package. + +# Ensure CRAN mirror is set for tests that use available.packages() +if (is.null(getOption("repos")) || getOption("repos")["CRAN"] == "@CRAN@") { + options(repos = c(CRAN = "https://cloud.r-project.org")) +} + +# ============================================================================= +# Tests for detach_package function +# ============================================================================= + +# Test 1: Error on non-character input +expect_error( + detach_package(123), + pattern = "must be a character string", + info = "detach_package: Error on numeric input" +) + +expect_error( + detach_package(NULL), + pattern = "must be a character string", + info = "detach_package: Error on NULL input" +) + +# Test 2: detach_package returns FALSE for non-attached package +result <- detach_package("nonexistent_package_xyz") +expect_false( + result, + info = "detach_package: Returns FALSE for non-attached package" +) + +# Test 3: Message shown for non-attached package +expect_message( + detach_package("another_nonexistent_pkg"), + pattern = "was not attached", + info = "detach_package: Shows message for non-attached package" +) + +# ============================================================================= +# Tests for dependencies function +# ============================================================================= + +# Test 1: Error when no packages specified +expect_error( + dependencies(), + pattern = "No packages specified", + info = "dependencies: Error when no packages specified" +) + +# Test 2: Error when pkges is not character +expect_error( + dependencies(pkges = 123), + pattern = "must be character strings", + info = "dependencies: Error when pkges is not character" +) + +# Test 3: Basic functionality with already installed package +# Using 'stats' which is always available +result <- dependencies( + "stats", + install = FALSE, + quiet = TRUE, + verbose = TRUE +) +expect_true(is.data.frame(result), info = "dependencies: Returns data.frame") +expect_true("stats" %in% rownames(result), info = "dependencies: rownames OK") +expect_true( + all(c("loaded", "installed", "loaded.version", "available.version") %in% + names(result)), + info = "dependencies: Correct column names" +) +expect_true( + result["stats", "loaded"], + info = "dependencies: stats package loads correctly" +) + +# Test 4: Test with multiple base packages (quoted) +result_multi <- dependencies( + c("stats", "utils"), + install = FALSE, + quiet = TRUE, + verbose = TRUE +) +expect_equal(nrow(result_multi), 2, info = "dependencies: Multiple packages") +expect_true(all(result_multi$loaded), info = "dependencies: All loaded") + +# Test 5: Test unquoted package names +result_unquoted <- dependencies( + stats, utils, + install = FALSE, + quiet = TRUE, + verbose = TRUE +) +expect_equal(nrow(result_unquoted), 2, info = "dependencies: Unquoted names") +expect_true( + "stats" %in% rownames(result_unquoted), + info = "dependencies: Unquoted stats in rownames" +) +expect_true( + "utils" %in% rownames(result_unquoted), + info = "dependencies: Unquoted utils in rownames" +) + +# Test 6: Test quiet parameter +expect_silent( + dependencies("stats", install = FALSE, quiet = TRUE, verbose = FALSE), + info = "dependencies: quiet=TRUE suppresses messages" +) + +# Test 7: Duplicate packages are handled +result_dups <- dependencies( + c("stats", "stats", "utils"), + install = FALSE, + quiet = TRUE, + verbose = TRUE +) +expect_equal(nrow(result_dups), 2, info = "dependencies: Duplicates removed") + +# Test 8: Test invisible return when verbose = FALSE +result_invisible <- dependencies( + "stats", + install = FALSE, + quiet = TRUE, + verbose = FALSE +) +expect_true( + is.data.frame(result_invisible), + info = "dependencies: Returns df even with verbose=FALSE" +) + +# ============================================================================= +# Tests for installLocalPackage error conditions +# ============================================================================= +# Note: We only test error conditions, not actual installation + +# Test: Error when DESCRIPTION file not found +temp_empty_dir <- tempfile("empty_pkg") +dir.create(temp_empty_dir) +on.exit(unlink(temp_empty_dir, recursive = TRUE), add = TRUE) + +expect_error( + installLocalPackage(temp_empty_dir), + pattern = "DESCRIPTION file not found", + info = "installLocalPackage: Error when DESCRIPTION missing" +) + +# Test: Error for installLocalPackageIfChanged when DESCRIPTION missing +expect_error( + installLocalPackageIfChanged(temp_empty_dir, tempfile()), + pattern = "DESCRIPTION file not found", + info = "installLocalPackageIfChanged: Error when DESCRIPTION missing" +) diff --git a/tests/tinytest.R b/tests/tinytest.R index 1bf5ced..4bab796 100644 --- a/tests/tinytest.R +++ b/tests/tinytest.R @@ -1,3 +1,23 @@ +# CKutils test runner using tinytest +# Optimized for GitHub Actions CI with parallel test support + if (require("tinytest", quietly = TRUE)) { - tinytest::test_package("CKutils") + # Detect number of CPUs for parallel testing + # Use parallel::detectCores() with fallback + ncpus <- getOption( + "Ncpus", + default = max(1L, parallel::detectCores(logical = FALSE) - 1L) + ) + + # In CI environments, limit parallelism to avoid resource contention + if (nzchar(Sys.getenv("CI")) || nzchar(Sys.getenv("GITHUB_ACTIONS"))) { + ncpus <- min(ncpus, 2L) + } + + # Run tests with parallel support when ncpus > 1 + tinytest::test_package( + "CKutils", + ncpu = ncpus, + side_effects = TRUE # Allow tests with side effects + ) } From 8035b27565b60307b6576a501a4e9d41f947691f Mon Sep 17 00:00:00 2001 From: Chris Kypridemos Date: Wed, 21 Jan 2026 13:50:51 +0000 Subject: [PATCH 2/3] Bump version to 0.1.23 and update date in DESCRIPTION file --- CLAUDE.md | 0 DESCRIPTION | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e69de29 diff --git a/DESCRIPTION b/DESCRIPTION index 7f7caa6..14cfb4c 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,8 +1,8 @@ Package: CKutils Type: Package Title: Some Utility Functions I Use Regularly -Version: 0.1.22 -Date: 2026-01-20 +Version: 0.1.23 +Date: 2026-01-21 Authors@R: c( person("Chris", "Kypridemos", email = "christodoulosk@gmail.com", role = c("aut", "cre"), comment = c(ORCID = "0000-0002-0746-9229")), person("Max", "Birkett", email = "pp0u8134@liverpool.ac.uk", role = "ctb", comment = c(ORCID = "0000-0002-6076-6820")), From 51b44e69dd15c39a36967c3e470904b2c89ed468 Mon Sep 17 00:00:00 2001 From: Chris Kypridemos Date: Wed, 21 Jan 2026 13:54:13 +0000 Subject: [PATCH 3/3] Fix Codecov integration by using official codecov-action - Replace covr::codecov() with covr::package_coverage() + to_cobertura() - Use codecov/codecov-action@v4 for reliable coverage upload - Requires CODECOV_TOKEN secret to be set in repository settings Co-Authored-By: Claude Opus 4.5 --- .github/workflows/test-coverage.yml | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test-coverage.yml b/.github/workflows/test-coverage.yml index 1f1f296..e53edd2 100644 --- a/.github/workflows/test-coverage.yml +++ b/.github/workflows/test-coverage.yml @@ -67,19 +67,35 @@ jobs: extra-packages: any::covr needs: coverage # Install dependencies needed for coverage analysis - # Step 4: Generate and upload test coverage + # Step 4: Generate test coverage report - name: Measure test coverage run: | - # Run coverage analysis with tinytest - covr::codecov( + # Run coverage analysis and save to file + cov <- covr::package_coverage( quiet = FALSE, clean = FALSE, install_path = file.path(Sys.getenv("RUNNER_TEMP"), "package"), type = "all" ) + # Save coverage to Cobertura XML format for Codecov + covr::to_cobertura(cov, filename = "coverage.xml") + # Also print summary + print(cov) shell: Rscript {0} - # Step 5: Show detailed test output if available (for debugging) + # Step 5: Upload coverage to Codecov using official action + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + files: coverage.xml + flags: unittests + name: codecov-umbrella + fail_ci_if_error: false + verbose: true + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + # Step 6: Show detailed test output if available (for debugging) - name: Show tinytest output if: always() # Run even if previous steps failed run: | @@ -93,7 +109,7 @@ jobs: echo "=== Coverage analysis complete ===" shell: bash - # Step 6: Upload test results on failure (for debugging) + # Step 7: Upload test results on failure (for debugging) - name: Upload test results on failure if: failure() # Only run if the workflow failed uses: actions/upload-artifact@v4