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.