Skip to content

Development

Setup

git clone https://github.com/DiogoRibeiro7/heteroTests
cd heteroTests
./setup.sh                      # Debian-based systems: R, renv and the dependencies
Rscript scripts/setup_helper.R  # Windows and macOS

The package locks its dependencies with renv; renv::restore() is the manual alternative to the setup scripts. Fork the repository and create feature branches from main.

Checks

Before a pull request, run the checks CI runs:

Command What it enforces
Rscript scripts/run_checks.R Styles R/ with styler, lints it, runs the tests that read the source tree, then the whole suite once under covr, and writes a coverage report to coverage/index.html and the coverage itself to coverage/coverage.rds
lintr::lint_package() The rules in .lintr: the lintr defaults with a 100-column limit
spelling::spell_check_package(vignettes = TRUE) Spelling; accepted words live in inst/WORDLIST
R CMD check --no-manual Everything R CMD check finds, with no unchecked warnings or notes

Statistical tests assert behaviour: size, power, and agreement with reference implementations such as lmtest and car, not just the structure of a result. Add unit tests for any new functionality.

The model-context matrix

tests/testthat/test-model-context.R runs the test matrix of the model context: about 30,000 of the suite's 41,000 expectations, and the largest part of its time. The environment variable HETEROTESTS_FULL_MATRIX decides how much of it runs (context_full_matrix() in tests/testthat/helper-model-context.R):

HETEROTESTS_FULL_MATRIX What runs
unset The full matrix when NOT_CRAN=true, which devtools::test() and testthat::test_local() set; on CRAN, the 15 cells of context_cran_cells
true The full matrix
false The 15 CRAN cells only

The two tests of tests/testthat/test-cache-contract.R that run every resampling test at its default replications, the next largest cost, follow the same rule. You get all of it by default. Set the variable to false for a faster run, for example HETEROTESTS_FULL_MATRIX=false Rscript scripts/run_checks.R.

Code

All R source lives directly in R/, one file per diagnostic, named after the function it defines (R/performBPTest.R), with no subdirectories. Code follows the tidyverse style guide and CRAN policies. A new test follows the conventions: it takes (model, data, ...), returns an htest with the fields What every test returns lists, and refuses the inputs every test refuses.

The tests that guard the public interface name every export: tests/testthat/test-public-api.R lists the exports planned for 1.0.0, so a new export fails it until it is added to the plan, and tests/testthat/test-return-contract.R runs every exported test against the documented fields, from a recipe in tests/testthat/helper-test-recipes.R.

Help pages and roxygen2

man/ holds 138 help pages. roxygen2 writes 86 of them from the roxygen blocks in R/: each begins with % Generated by roxygen2: do not edit by hand and names its source file on the next line. The other 52 are written by hand.

To change a page that has that header, change its block in R/, run devtools::document() with the roxygen2 of RoxygenNote in DESCRIPTION, and commit the pages it writes with the change; do not edit such a page by hand. run-checks.yml runs roxygen2 at that version and fails when man/ or NAMESPACE then differ from the commit, so a page edited by hand, a block changed without document(), or a page that document() would add or delete fails the check.

roxygen2 does not overwrite a page without that header: it prints Skipping <file>: it already exists and was not generated by roxygen2. Those 52 pages are edited by hand and nothing regenerates them, so a change to what one of them documents updates its .Rd file in the same commit. 45 of them belong to a roxygen block in R/ that roxygen2 skips, and the check above does not see them. NAMESPACE is written by hand too: an @export tag exports nothing until NAMESPACE has the line. Both are to move to roxygen2, under the 0.18.0 item "52 help pages and NAMESPACE are written by hand" of the roadmap.

tests/testthat/test-documentation-consistency.R runs tools::codoc(), tools::checkDocFiles() and tools::undoc(), so the local test suite catches a mismatch before CI does.

Documentation site

This site is built with Material for MkDocs from docs/. Three kinds of pages come from the package itself, and each starts with a comment that names the script that wrote it:

Pages Written by From
The API reference, docs/reference/, and its part of the nav in mkdocs.yml tools/build-docs.R The help pages in man/, grouped by tools/reference-index.R
Releases and Roadmap, docs/releases.md and docs/roadmap.md tools/build-docs.R NEWS.md and ROADMAP.md
The user guide, docs/guide/ tools/render-guides.R The vignettes, knitted with the package installed from the checkout
Redirects from the addresses of the pkgdown site, such as docs/reference/performWhiteTest.html and docs/news/index.html, and from the pages of this site of help files that are gone, such as docs/reference/ht_logging/index.html tools/build-docs.R The help files and their aliases, the vignettes, and lists in the script of pkgdown's other pages and of the help files that are gone

The other pages, this one included, are written by hand. A redirect is a static HTML page that MkDocs copies as it is: the mkdocs-redirects plugin takes an old address as a Markdown file, and --strict fails on one that ends in .html.

Rscript tools/build-docs.R      # after changing man/, NEWS.md or ROADMAP.md, or adding a vignette
Rscript tools/render-guides.R   # after changing a vignette
pip install -r docs/requirements.txt
mkdocs serve                    # preview at http://127.0.0.1:8000
mkdocs build --strict           # what CI runs

tools/build-docs.R stops if a help topic is in no section of tools/reference-index.R, and tests/testthat/test-reference-index.R checks that every export is in one, so a new help page has to be placed in a section on purpose. CI runs tools/build-docs.R again and fails if the committed pages differ, then builds the site with --strict, so a broken link or a stale page fails the build. It does not run tools/render-guides.R, which runs the package's code and draws figures whose bytes differ between platforms: commit the guide pages it writes along with the vignette that changed.

Continuous integration

Workflow Runs
R-CMD-check.yml R CMD check. On a pull request: Ubuntu and Windows (R release), and the hard dependencies on R 4.1. On a push to main, weekly and on demand, also Ubuntu (R devel and the previous release) and macOS. Weekly or on demand, --as-cran and reverse-dependency checks.
run-checks.yml In parallel: the check that man/ is what roxygen2 writes from R/ (see Help pages and roxygen2), then scripts/run_checks.R, whose saved coverage is sent to Codecov; the spelling check, and a link check whose result is reported, not gated; and the quick benchmark suite.
docs.yml Regenerates the reference, releases and roadmap pages, fails if the committed ones are out of date, builds the site with --strict, and deploys it to GitHub Pages from main.
release.yml On a push to main whose DESCRIPTION version has no tag: builds the tarball, checks it with R CMD check --as-cran, tags v<version>, publishes a GitHub release with the tarball, and stores the tarball in cran/.

A change to Version: in DESCRIPTION therefore starts a release when it reaches main; leave it alone in an ordinary pull request, and add the change to the Development version section at the top of NEWS.md.

No job runs the suite twice. Both workflows set HETEROTESTS_FULL_MATRIX to false, except the ubuntu-latest (R release) job of R-CMD-check.yml, which sets it to true: the full model-context matrix runs there, and every other job, the coverage run included, runs the 15 CRAN cells. R CMD check and covr test an installed copy of the package, where the checks that read the source tree skip (tests/testthat/helper-source-tree.R); scripts/run_checks.R runs the files that hold them from the source. Both workflows cancel a pull request's earlier runs when it is pushed again, never a run on main. release.yml sets no HETEROTESTS_FULL_MATRIX, so its check of the release tarball runs the full matrix.

Every job that runs on a pull request stops at 45 minutes. A job normally takes 1 to 16 minutes, but on a slow Ubuntu package mirror installing R and the system libraries alone has taken 20 to 45, whatever the job runs after. The limit stops a job that is stuck, which Re-run failed jobs then starts again on another runner.

Two things keep those downloads short: - The mirror. Every Ubuntu job first runs .github/actions/ubuntu-archive, which points apt at archive.ubuntu.com instead of the runners' azure.archive.ubuntu.com. In October 2026 that mirror served package downloads at 25 to 50 kB/s for days. - spdep. Through sf, spdep needs GDAL, GEOS and PROJ: most of the ~100 MB of system libraries an Ubuntu job installs. Only the ubuntu-latest (R release) check installs it on Linux (with the weekly --as-cran and reverse-dependency checks and the release gate), so the spatial tests run there. Windows and macOS get it as a binary. The other Ubuntu jobs pass spdep=?ignore to setup-r-dependencies, and its tests skip there, as they do in the R 4.1 job, so the coverage report leaves the spatial test's lines out.

Contributing

CONTRIBUTING.md covers the rest of what a pull request needs. Open an issue to discuss a substantial change before starting it, and give a pull request a concise description that references the issues it addresses.