Technical reference: testing

2026-11-08

This vignette inventories the growthcleanr test suite: what each test file covers, how the tests are run, and the standing coverage gaps. The summary table is the quick index; the per-file sections below give a per-test breakdown for the files that warrant one.

Summary

Counts below are from a fresh full run on the installed package: all files green, 0 failures. “Tests” = test_that() blocks; “Assertions” = passing expect_* checks.

File Tests Assertions Scope
test-cleangrowth.R 15 117 cleangrowth() API: adult integration (incl. adult_scale_max_lbs and adult_overrides pass-through + adult_overrides container/name/value validation), child/adult spanning subjects, requested optional columns on adult-only input
test-cran-smoke.R 2 23 CRAN smoke test (the one file that intentionally does not skip_on_cran()): a fast, deterministic end-to-end cleangrowth() run for both the child and adult algorithms on tiny inline fixtures, asserting only definitional/unambiguous cases (missing/not-a-param preprocessing codes, exact same-day duplicates, a clearly-normal Include, an absurd population-limit value). Because almost every other algorithm file is file-level skip_on_cran()’d for runtime, this is the only end-to-end exercise of cleangrowth() on CRAN’s platforms — it guards build/load failure, output-assembly regressions, and gross numeric divergence
test-data-frame-api.R 13 41 Data-frame entry point: canonical/remapped columns, vector↔︎data-frame equivalence, id auto-gen, input errors, full_detail/qm_detail decoupling
test-visit-date-agedays.R 41 114 Fractional agedays cleaned as whole days (rounded down, so -0.5 is day -1) but returned as submitted (gc_internal_agedays under internal_detail); age from dob + visit_date: route resolution and precedence; a lone date column parsed alongside an agedays column (parse-only, ages from agedays) surfacing gc_internal_visit_date/gc_internal_dob under internal_detail; expanded text-date parsing (year-first numeric, English spelled-month, numeric day/month via date_order) with one-format-per-run, four-digit-year (1900–2099), and English-locale guards; malformed/ambiguous/numeric rejection; date values with no digits treated as blank and a few impossible dates set aside (many stop); gc_internal_dob/gc_internal_visit_date; missing dates; negative ages
test-carry-through.R 17 76 carry_through: non-role columns parked and returned appended (long input), original order/values/types, a column named like a returned growthcleanr column returned as userinput_<name> with a warning (names of columns not returned in the run kept), a user line column carried unchanged, no-id path, input not mutated
test-semiwide-input.R 19 61 input_format = "semi_wide": keystone long/semi-wide equivalence, column remapping, empty cells vs zeros, same-day extraneous values, missing-weight/height warnings (HC-absent warning removed), date route, determinism, error catalog, the long-only length rule (length.adjust rejected; a LENGTHCM/LENGTHIN value at any age gets default handling, matching a column mapped to HEIGHTCM, including at 19 years and at days 729 and 730); long output carries the user’s other columns (renamed userinput_<name> when named like a returned growthcleanr column, as is an id column)
test-wide-input.R 24 76 input_format = "wide": keystone long/wide equivalence, wide-vs-semi-wide agreement, block discovery + ragged subjects, _0 baseline block, prefix/structural remapping, imperial/length prefixes, same-day blocks as extraneous values, date route, value-with-no-age, strictness warnings (block gap, max_blocks, non-monotonic ages, repeated subject), missing-weight/height warnings (HC-absent warning removed), determinism, error catalog, round-trip spec, the long-only length rule (length.adjust rejected; a LENGTHCM/LENGTHIN value at >= 2 years gets default handling, matching a prefix mapped to HEIGHTCM); long output carries the user’s other columns (renamed userinput_<name> when named like a returned growthcleanr column, as is an id column) and, for wide, only subject-level columns such as dob
test-semiwide-output.R 11 53 output_format = "semi_wide": the equivalence chain (long -> long == semi -> long == melt-back of semi -> semi), echo fidelity, widened_emit control, imperial unit conversion reporting, the two-directional length adjustment on long-format fixtures (length.adjust in semi-wide is no longer supported – see the long-only length rule under the input files), carry_through in mirror mode, scope errors, determinism
test-wide-output.R 14 61 output_format = "wide": the equivalence chain (long -> long == wide -> long == melt-back of wide -> wide, incl. a syngrowth slice), SDEs at scale (weight-only same-day duplicate blocks), echo fidelity + ragged NA verdicts, widened_emit control with per-block gc_internal_agedays_N, imperial reporting, carry_through in mirror mode, same-day blocks as extraneous values, scope/collision errors, determinism
test-hostile-input.R 46 128 Adversarial input: long-format guards (zero-row / all-NA-required-column / duplicate-column / clean errors, factor measurement converted by its labels with a warning, difftime agedays converted to days in its own units and factor agedays by its labels, NA structural key → Exclude-Missing-Info, empty-core no-crash, near-match column hint, character coercion, hand-built ref_tables with a missing/misnamed/non-function element or non-list, non-scalar/non-numeric adult_scale_max_lbs) plus the semi-wide/wide reshape sweep (duplicate value/structural/block-column guards, and the intentional reshape-vs-long differences: unmapped column ignored, NaN cell dropped, all-empty parameter column warns); adult missing-measurement handling (same-day HT+WT pair with one side missing across all permissiveness levels, longitudinal embedded NA, and a fuzz pass over the adult regression fixture blanking one value per subject — all must not crash and report Exclude-Missing-Info)
test-hardlimit-single-obs.R 7 32 Single-observation hard-limit exclusion per parameter (child WT/HT/HC birth + non-birth, adult WT/HT) end-to-end via cleangrowth(); lone in-range value survives to output exactly once
test-flexible-input-coding.R 25 69 Flexible sex/param coding: sex-code recognition + verbatim echo, 1=male/2=female hard stop + override, all-1 sex warning + override, unrecognized-sex warning → Exclude-Missing-Info, all-adult 1/2 cleans, custom sex_coding, canonical param case-insensitivity (no built-in abbreviations — bare WT/HT/HC → Not-GC-Param unless mapped), param_coding override-to-imperial + validation errors, HEADIN + LENGTHIN conversion, unit-error >20% warning (incl. HC>5y excluded from the denominator)
test-child-regression.R 7 47 Frozen counts, spot checks, cross-sample stability, Missing, HC, preload_refs
test-partial-run.R 9 21 gc_partial_run(): full-run equivalence (exclude + tbc.sd), equivalence under checkpointing, no-change/all-changed/added/removed-subject handling, output-column-mismatch guard, id + changed_subjids validation
test-gc-runtrial.R 12 31 gc_runtrial(): previews input setup on a data sample — format/coding detection, warning capture (“FLAGGED WHILE READING”), cleaning-result report, message muffling
test-child-algorithms.R 28 68 CF rescue (deltaz/nextz rule: decline→rescue, jump→exclude, whole-string, end-of-series age rule; cf_detail, CF-E), unit-error smoke tests (Section 2 — “excluded somehow”, not Evil-Twins-specific), Error Load, HC boundary, parallel (incl. an out-of-range cores capped to detectCores()), GA correction, Birth EWMA2, LENGTHCM
test-child-support-fns.R 6 24 Support-fn units (T20/T21/T22/T13/T14): get_dop, .child_valid include-flag matrix, .child_z3 boundary/order, ewma() cache_env, ewma_cache_update() neighbor rebuild
test-child-piv.R 13 21 Child Step 7 PIV (T18/T19): standardized-PIV low cutoffs + 1-year split on HT (piv.z.ht.low.young/.old; WT has no low cutoffs), absolute birth backstops (WT>10.5, HT>65, HC>50 at age 0; WT>40 <2y, strict boundary) with same-magnitude non-birth controls; BMI limits on same-day WT x HT pairs (BMI<3 any age, >50 <2y, >150 >=2y; more-extreme-z value excluded; multi-pairing rule; in-range control)
test-child-sde-tiebreak.R 1 6 Same-day-identical tiebreak (T2): birth keeps lowest id, non-birth keeps highest id; loser is Exclude-Identical
test-child-sde-charid.R 2 3 Non-numeric character id (site-prefixed) does not break Step 13 SDE resolution: run completes with no same-day Include duplicate, and character-id vs numeric-id runs give identical exclusions (regression for the keep_id_* integer-coercion bug that crashed Step 19)
test-child-parameters.R 6 23 Parameter acceptance + behavior (cf_rescue, ewma_window incl. hostile-value rejection, error load, imperial, LENGTHCM)
test-child-edge-cases.R 14 29 Sparse/degenerate data, all-NA, same-day, negative agedays, HC>5y, single-value PIV (WT/HT/HC), determinism
test-child-et.R 3 12 Child Evil Twins fixture: membership, per-pattern invariants, HC interior-step cleaned (not out-of-range)
test-child-birth-ewma2.R 3 11 Birth HT/HC EWMA2 (Steps 15/16): 3-value SP with birth, isolated low birth-WT keep/exclude band
test-child-corroboration.R 3 16 GA-correction corroboration gate (incl. HT/HC confirm-correction + 3.5 cap); UER3 unit-error 2-group drop
test-child-unit-error-range.R 7 22 unit_error_range output column + .unit_error_range_flag/.compute_unit_error_range band math
test-exclusion-code-crosswalk.R 5 11 Detailed→Summary crosswalk: no-NA, every Detailed mapped, Summary = canonical fold, valid factor levels, shared/child/adult folds exercised
test-adult-clean.R 64 224 All 14 adult steps, 4 permissiveness levels, edge cases (via cleanadult() directly; incl. non-scalar adult_permissiveness and hostile setting overrides → clean errors); Step 13 keeps a single-value height when any same-day BMI is in range; mean_ht computed from included heights only and populated on excluded height rows (NA when no height is included)
test-adult-et.R 7 20 Adult Evil Twins anchor rule (4-eligible-weight requirement, end/interior cede-to-EWMA), sex=NA CSD fallback
test-adult-cutpoint.R 10 25 Adult HEADCM -> Exclude-HC-Out-of-Range and adult LENGTHCM cleaned as height; missing-column error names the missing role (sex-column note); sex-less pediatric rows suggest adult_cutpoint = 18 only when all pediatric rows are >= 18 y (set aside with a warning when adult rows are present, a stop otherwise); adult_cutpoint advisory: no auto-route (all-adult sex-less data stops on sex at the default cutpoint, advisory fires first), adult_cutpoint = 18 routes to the adult path, the advisory warning fires for all-adult data with 18-20y rows, an explicit cutpoint (18 or 20) silences it, a sub-18y row suppresses it, NA-age rows ignored, and a subject’s routing is independent of the rest of the dataset
test-checkpoint.R 24 86 cleangrowth(checkpoint_dir=) fold: single-pass-equivalence, fresh-run reset, checkpoint_resume (including resume with changed data: user-facing warning, input values kept on unmatched rows, summary “No result” line), checkpoint-cap error, checkpoint_size/batch_size independence, re-widen under checkpoint, carry-through of extra columns equals non-checkpoint, all-adult (explicit cutpoint 18) equals non-checkpoint, consolidated input warning byte-identical checkpoint vs non-checkpoint, deprecated-shim forwarding; standalone cleangrowth_checkpoint_combine() (re-combine in id order, NULL-order, missing-id warning, missing-dir / empty-dir errors, quietly/verbose/log_file); mean_ht = TRUE returned under checkpointing; guard that every cleangrowth() argument is forwarded to the checkpoints or on an explicit not-forwarded list
test-sd-recenter.R 19 47 sd.recenter wiring for cleangrowth(): "none" disables recentering (case- and space-insensitive), recenter_source is not an argument, including checkpointed runs (cleangrowth(checkpoint_dir=)); recenter_detail output contract (columns, debug/full_detail parity, dedup, checkpoint forwarding); syngrowth-scale guard (the built-in table supplied explicitly is byte-identical to the default); sd.recenter validation (removed 2.x options and other values stop, data.frame accepted, caller’s table not modified, missing column, duplicate rows, coverage of every evaluable child row with a dataset-wide count under checkpointing)
test-utils.R 16 524 recode_sex, longwide, simple_bmi (utilities, not the cleaning algorithm); ext_bmiz() numeric-sex stop; longwide() parses sex (M/F, words, sex_coding, remapped column; outputs M/F; 1/2 stop and all-1 warning; warns on missing/unrecognized) and param (param_coding, imperial -> metric, LENGTHCM as height, HC dropped, unrecognized warned) like cleangrowth()
test-consolidated-input-warning.R 7 23 Consolidated input warning (Option B): the three Class-2 defect warnings (zero / negative measurement, negative age) with per-parameter-group and all-rows N/%, one-warning union count, Generated ids-only case, unrecognized-sex leading with sex_coding, clean data silent
test-growthcleanr-app.R 21 90 Bundled Shiny app: growthcleanr_app() exported with a stable signature and the app directory ships + app.R parses (CRAN-safe; guards the historical package/path mismatch + syntax breakage), plus the app’s mapping-time guards — wide wrong-age-prefix rejected at mapping (not deferred to cleaning), sex-coding auto-detect (recognizes default pairs, skips 1/2), value_freq/mapped-column reactives tracking the sex/param selection, semi-wide custom-column mapping (measurement columns keep their names), an all-miss measurement mapping failing at Step 2 (not at cleaning), the hostile-input guard helpers, and long-format non-numeric-measurement + duplicate-field mappings rejected at mapping while a valid numeric mapping still succeeds.
test-cli.R 12 54 Command-line driver: .gc_cli_opts() coercion + typing (numeric/integer/csv, dotted arg names), boolean + inverted flags, Tier-2 param_coding/sex_coding/semiwide_cols/wide_cols assembly, base-param name flags + separate --*-units routing to the metric vs imperial canonical key (orphan units flag unused; bad units value errors), up-front enum/number/one-sided-sex validation errors; run_gc_cli() round-trip equals an in-R cleangrowth() call, missing/empty-input errors, output-I/O guards (same-path errors and leaves the input intact; missing output directory errors), checkpointed run equals single-pass (writes checkpoint_NNNN.rds); skip_on_cran() shell-out through the shipped inst/scripts/gcdriver.R via gc_cli_path() (exit codes + result equivalence, incl. --semiwide-col-weight/--weight-units).
test-v2-migration-messages.R 17 73 Messages for users coming from growthcleanr 2.x: positional 2.x call stop; adjustcarryforward() stop; cf_rescue = TRUE stop (abbreviations still match; case- and space-insensitive); include.carryforward message gives both 2.x mappings; log.path deprecation warning; percentages below 0.05% shown as <0.1%; unit-error-range warning wording; removed 2.x arguments stop with the replacement (and the list covers every removed 2.2.3 argument), with a 2.x sd.recenter string reported in the same stop (a missing value or "none" is not); other unknown arguments still stop, unevaluated, also via cleangrowth_checkpoint(); one-time vector-style note (not gated by quietly); “no id supplied” notice once per session (not used up by gc_runtrial()) while NA/duplicated-id notices repeat; adult_cutpoint clamp warning (days hint; once under checkpointing); case-insensitive adult_permissiveness and a call-free error; summary wording (included / Set aside); longwide() unknown inclusion_types warning and missing-exclude hint; command-line short-flag rejection, unknown-flag hint, and --widened-emit with long output (warns; an invalid value stops); command-line choice values ignore case and surrounding spaces
Total 550 2362 run Full testthat suite green (37 files); 0 skipped.

All 37 testthat files run standalone under Rscript (each carries library(growthcleanr) at the top). Most also carry skip_on_cran(). The exceptions are test-cran-smoke.R, which runs on CRAN by design, and test-adult-clean.R, test-child-sde-charid.R, test-consolidated-input-warning.R, and test-gc-runtrial.R.


Running Tests

# Single file
NOT_CRAN=true Rscript -e 'testthat::test_file("tests/testthat/test-child-algorithms.R")'

# All files
NOT_CRAN=true Rscript -e 'testthat::test_dir("tests/testthat/")'

IMPORTANT: Tests use library(growthcleanr), which loads the installed package. After any code changes, reinstall before testing — otherwise tests run against stale code:

devtools::install_local(".", force = TRUE, upgrade = "never")

Development testing not distributed. Some testing used in developing the algorithms is not part of the distributed test suite, because it depends on large or non-public fixtures and is being rebuilt. It includes a stress test on a 400-subject dataset with induced errors, code locks on preterm catch-up and preterm same-day cases, tests of the recumbent/standing offset for gestational-age-corrected children, a comparison of ext_bmiz() against the CDC SAS program, and a regression harness for the adult algorithm that checks every row of a 1,508-row fixture at all four permissiveness levels.


Per-file detail

The sections below give a per-test breakdown for the files whose individual tests are worth enumerating. Files without a section here (for example the input-format, hostile-input, flexible-coding, CLI, and recentering suites) are fully described by their row in the Summary table above — the Scope column carries their coverage rather than repeating it as a per-test table.

test-cleangrowth.R

The cleangrowth() entry point — dispatches to child and adult algorithms and assembles combined output.

Test What it verifies
works as expected on adult synthetic data Spot-check records, cutpoint comparison, frozen counts, Missing handling
works without either adult or pediatric data Adult-only, pediatric-only, empty data all return correct row counts
spanning subject: all rows preserved through child/adult split Real syngrowth subject (child + adult rows): no rows lost, no NA codes
spanning subject: child rows get child codes, adult rows get adult codes Code prefixes on the Detailed level (exclude_detail = TRUE): Exclude-C-* for child, Exclude-A-* for adult (the default Summary exclude consolidates the two, so prefixes show only at Detailed)
spanning subject: constructed subject processed correctly across boundary Synthetic 18y–22y subject: all rows returned, correct prefixes, normal values mostly included
adult integration: output columns present and correctly typed All rows returned; bin_exclude present; exclude is a factor with no NAs
adult integration: imperial units processed correctly HEIGHTIN/WEIGHTLBS through the full pipeline; param preserved as imperial in output; all rows returned, all included, no NA codes
adult integration: adult_permissiveness passes through 115 cm HT → PIV at looser, passes PIV at loosest
adult integration: adult_scale_max_lbs passes through 136 kg flagged with cap = 300 lbs, included without a cap
adult integration: character/UUID ids preserved String ids survive the round trip intact
adult integration: spanning subject has correct output structure Subject with rows on both sides of the cutpoint; bin_exclude non-NA for all rows
optional columns are emitted for adult-only input, matching mixed output With full_detail/debug/recenter_detail/qm_detail, adult-only and child-only output have the same columns, order, and types as mixed output; adult rows carry the same values (cf_status = "Adult", child-only columns NA)
adult rows in an adult-only internal batch are labeled cf_status ‘Adult’ Mixed run where one internal batch has no child rows

test-cran-smoke.R

The only test file that intentionally runs on CRAN (no skip_on_cran()), because almost every other algorithm file is skipped there for runtime. A sub-second end-to-end cleangrowth() sanity check on tiny inline fixtures, asserting only definitional/unambiguous cases so it cannot fail on a platform-specific trajectory judgment — it guards build/load failure, output-assembly regressions, and gross numeric divergence.

Test What it verifies
child algorithm runs end to end and assigns the definitional codes data.table returned, no rows dropped, default columns present, valid exclude factor with no NAs; measurement == 0 → Missing-Info, a non-param → Not-GC-Param, an exact same-day duplicate resolved, and a clearly-normal value Included
adult algorithm runs end to end and excludes a population-limit value The adult path runs end to end and an absurd population-limit weight is excluded

test-data-frame-api.R

The data-frame style entry point (the primary calling style) and the id auto-generation/validation behavior.

Test What it verifies
cleangrowth(data = df) works with canonical column names data.table input with default names
cleangrowth(data = df) works with a plain data.frame data.frame input coerced internally
cleangrowth() accepts remapped column names as strings subjid = "patid", id = "row_id", etc.
data-frame and vector styles produce identical results The two calling styles agree row-for-row
missing id column triggers warning + auto-generates No id → warning, sequential 1:N
missing required columns error clearly Absent required column → informative error
non-data.frame data argument errors clearly Wrong data type → error
column-name args reject vectors and non-strings Column-name args must be single strings
vector style without id warns and auto-generates Legacy vector path, no id → warning + auto-gen
calling with neither data nor vectors errors clearly Empty call → error
duplicate id values warn and auto-regenerate, preserving user input Non-unique id → warning, regen, original preserved in id, internal id in gc_internal_id
NA id values warn and auto-regenerate, preserving user input NA id → same handling
valid id with display_gc_id sets gc_id == id (no warning) Valid id → no regen, gc_id == id

test-child-regression.R

Frozen-count and stability tests on syngrowth data. These break on intentional behavior changes — update expected values (and document the re-baseline) when that happens.

Test What it verifies
structural invariants on 100-subject subset Row counts, column presence, factor type, no NAs, ≥1 Include
exclusion counts match expected on 100 subjects Frozen counts by exclusion code (regression baseline)
spot-check individual records Specific record ids produce specific exclusion codes
within-subject exclusions stable at 50 vs 100 subjects Same subject, same code regardless of batch context
missing measurements return Missing NA measurements → Exclude-Missing-Info
HEADCM measurements are processed HC rows present with valid codes
gc_preload_refs: identical results to standard run ref_tables produces byte-identical output

test-partial-run.R

Tests the gc_partial_run() incremental-run wrapper. The core guarantee is that a partial run equals a full run of the same data under the same options (gc cleans each subject independently after reference recentering).

Test What it verifies
partial run equals a full run for every subject exclude + tbc.sd identical to cleangrowth(new_data) after perturbing one subject
partial run equals a full run under checkpointing Same, with checkpoint_dir set (checkpoint_size = 5, keep_checkpoints = FALSE); scratch checkpoint files cleaned up
no changed subjects returns the cached result Empty changed_subjids reuses the cache unchanged
all subjects changed equals a plain full run Recompute set = every subject
an added subject is cleaned automatically Subject in new_data but not the cache is re-cleaned
a removed subject is dropped Subject in the cache but not new_data is dropped
mismatched output options error Cache and fresh clean with different columns → hard error (no NA-fill)
missing id / missing changed_subjids rejected Required-argument guards

test-child-algorithms.R

Constructed-scenario tests organized by algorithm step.


test-child-parameters.R

Each configurable parameter is accepted and behaves as documented.

Test What it verifies
cf_rescue=‘all’ keeps CFs, cf_rescue=‘standard’ excludes them CF rescue mode behavior
ewma_window parameter is accepted and doesn’t crash Window parameter acceptance
ewma_window rejects non-scalar / non-finite / < 1 values Entry validation: NA/NaN/0/negative/Inf/character/length-2/logical → clean error; fractional ≥ 1 accepted
error load parameters affect error-load exclusions error.load.* plumbing
imperial units (HEIGHTIN, WEIGHTLBS) are converted and processed Imperial conversion
LENGTHCM param is accepted and processed LENGTHCM acceptance

test-child-edge-cases.R

Minimal, degenerate, and boundary-condition data.

Test What it verifies
single subject Runs on 1 subject
single measurement per param 1 HT + 1 WT → valid output
exactly 2 measurements per param 2 per param → valid output
all-NA measurements All NA → all Exclude-Missing-Info
mix of NA and valid NAs get Missing-Info, valid values get real codes
same-day identical measurements Detected and marked
negative agedays → Exclude-Missing-Info
HEADCM > 5 years; HEADCM ≤ 5 years >5y Exclude-HC-Out-of-Range; ≤5y cleaned
single extreme WT / HT / HC Each excluded as population-implausible (Exclude-C-Hard-Limit Detailed)
mix of data-rich and data-sparse subjects Both handled
carried-forward values Detected
deterministic results Same input → same output (seed-independent)

test-child-et.R

Child Evil Twins on a deterministic fixture (the real ET membership coverage — test-child-algorithms.R Tests 5–7 only assert “excluded somehow”).

Test What it verifies
Evil-Twins membership matches expected Per-row Exclude-C-Evil-Twins membership (Detailed)
per-pattern invariants Pattern-level invariants on the fixture
HC interior step is cleaned, not out-of-range HC interior step within range is cleaned, not Exclude-HC-Out-of-Range

test-child-birth-ewma2.R

Birth HT/HC EWMA2 changes (Steps 15/16).

Test What it verifies
Change 4: extreme non-birth value in a 3-value HT/HC SP with a birth is caught Birth now participates in Step 15 for exactly-3-value SPs
Change 4: a plausible 3-value HT/HC SP with a birth is left fully Include No over-exclusion on the plausible companion
Change 2: isolated low uncorrected birth WT kept / excluded by band Kept when |z|+rise ≤ 7, excluded when > 7

test-child-corroboration.R

Test What it verifies
GA correction is gated on corroboration of prematurity Correction applied only with a corroborating early weight or same-day low height/HC
HT/HC confirm-correction: HC corroborates only as last resort, capped at 3.5 z HC may confirm a correction (when low and within 3.5 z of the weight) only with no early-weight and no confirming-HT corroboration; a confirming HT overrides a bad HC; the 3.5 cap blocks an otherwise-low HC over a far-lower weight
UER3: a clean unit-error 2-group SP drops the unit-error block UER3 2-group selection drops the unit-error values

test-child-unit-error-range.R

The default-ON informational unit_error_range output column and its internal helpers.

Test What it verifies
column is present and logical by default unit_error_range present, logical
unit_error_range = FALSE omits the column Opt-out path
excluded-from-cleaning rows are FALSE Exclude-Missing-Info / Exclude-HC-Out-of-Range rows never flagged
.unit_error_range_flag flags WEIGHTKG unit-error magnitudes WT band math
.unit_error_range_flag flags HEIGHTCM unit-error magnitudes HT band math
.compute_unit_error_range returns FALSE for excluded/NA-age rows Excluded / NA-age handling
HEADCM is evaluated through the 5y cleaning range An inch-valued HC is flagged at 700, 1500, and 1826 days; a plausible HC is not; no NA

test-exclusion-code-crosswalk.R

Locks the Detailed→Summary crosswalk directly (guards the silent-NA-factor class: a new internal code with no crosswalk entry fails here). Runs a syngrowth subset + constructed rows guaranteeing shared/child/adult folds.

Test What it verifies
every output row has non-NA Detailed and Summary codes No silent NA factor levels
every observed Detailed code is in the canonical crosswalk Detailed codes all mapped
Summary equals the canonical fold of the Detailed code, every row Each Summary == its canonical fold
every observed Summary code is a valid Summary factor level Summary ∈ the 14 levels
crosswalk coverage: shared, child, and adult folds are all exercised All three fold families hit

test-adult-clean.R

64 unit tests calling cleanadult() directly (via growthcleanr:::), organized by step. 224 assertions. Covers PIV (1H/1W incl. BMI-PIV), minimal/edge cases, Weight Cap (4W), same-day (9H/10W), Evil Twins (9Wa anchor rule), Extreme EWMA (9Wb), Height Distinct (10H 2D band + 3+D window + ht_band), Moderate EWMA (11Wb), 1D (13, incl. BMI limits + custom limits, the no-BMI height minimum being equal at loosest and looser, and a single-value height kept when any same-day BMI is in range), Error Load (14), parameter validation (incl. invalid and non-scalar permissiveness → clean error, not a length-mismatch crash; and hostile setting overrides — NA / non-finite / non-scalar / wrong-type overall_ht_min / allow_ht_gain / repval_handling etc. → clean type errors, not scalar-if() crashes), output structure, repeated values (2W), 2D ordered/non-ordered weight (11Wa/11Wa2), cap influence, the full-fixture regression (against inst/testdata/adult-gc-test-ALL-PHASES.csv, 1508 rows; the WT and HT rows carrying expected codes are compared), imperial units, mean height (computed from finally-included heights only, populated on excluded height rows, NA when no height is included), multi-subject isolation, the permissiveness framework (4 levels + overrides + allow_ht_gain/mod_ewma_f/allofus15 effects), and permissiveness_presets() completeness.


test-adult-cutpoint.R

Routing at adult_cutpoint, the adult-cutpoint advisory, and adult-specific input handling, all end-to-end through cleangrowth(). 24 assertions.

Test What it verifies
all-adult (>=18y) data with no sex value is not auto-routed At the default cutpoint (20), 18-20y rows stay pediatric: the advisory warning fires, then the run stops because no pediatric row has a usable value in the sex column
adult_cutpoint = 18 routes the same data to the adult path Advisory silent; all 6 rows returned (adult rows are cleaned without a sex value)
advisory fires when the default is left in place All-adult 18-20y data with a sex value: advisory fires and the rows are cleaned by the child algorithm, not set aside
an explicit adult_cutpoint silences the advisory Both adult_cutpoint = 20 and adult_cutpoint = 18
one sub-18y measurement suppresses the advisory Data that is not all-adult gets no advisory
NA-age rows are ignored for the advisory Every row with a known age is >= 18y, so the advisory still fires
a subject’s routing is independent of the rest of the dataset An 18-20y subject gets identical per-row results cleaned alone or together with a 5-7y subject
adult HEADCM and LENGTHCM Adult HEADCM -> Exclude-HC-Out-of-Range with unit_error_range = FALSE; adult LENGTHCM keeps its submitted label, has gc_internal_param = HEIGHTCM, is included, and takes part in height cleaning (a 120 cm height is excluded against it); weights unaffected
missing-column error names the missing role A missing sex column gives e.g. sex = "your_sex_column" plus the note that the value may be blank or NA for adults; a missing measurement column names measurement and omits the sex note
sex-less pediatric rows suggest adult_cutpoint = 18 only when appropriate With adult rows present, the pediatric rows are set aside and the warning carries the suggestion when every pediatric row is >= 18y, not when one is younger; with no adult rows, the stop carries it

test-adult-et.R

Adult Evil Twins anchor rule (Adult Step 9Wa), the adult-side companion to test-child-et.R.

Test What it verifies
flanked over-the-limit weight pair triggers Evil Twins Anchored pair fires
single interior weight spike is ceded to EWMA Lone interior spike not ET
single end weight spike is ceded (no anchor before it) End spike not ET
does not fire with fewer than 4 eligible weights n ≥ 4 requirement
can fire with exactly 4 eligible weights Boundary of the requirement
fires for sex = NA adults (averaged CSD fallback) sex=NA path uses M/F-averaged CSD
adult CSD reference includes an M/F-averaged sex = NA row Reference table has the averaged row

test-checkpoint.R

Checkpointing via cleangrowth(checkpoint_dir = ...) (the folded interface) plus the deprecated cleangrowth_checkpoint() shim.

Test What it verifies
cleangrowth(checkpoint_dir=) is identical to a single-pass run exclude, tbc.sd, and every shared column match a plain cleangrowth() run
default run starts fresh: stale checkpoints removed, result correct checkpoint_resume = FALSE clears prior files; result matches a plain run
1/2 sex stop fires before any checkpoint is cleaned Sorted 1/2 data stops with no checkpoint file written; a dataset that also has 0s runs even if one checkpoint holds only 1s and 2s
checkpoint_resume = TRUE reuses prior checkpoints Completed checkpoints skipped on resume; a fresh run overwrites
resuming with changed data warns in user terms and the summary adds up Rows not in the checkpoint files: warning with no internal function name, exclude NA, input values kept, summary “No result” line
resuming with changed data keeps input values with character ids and extra columns Same with character ids and a carried-through column
explicit checkpoint_size over the checkpoint cap is increased with a warning Explicit oversized checkpoint_size is increased to stay within the cap (9999 checkpoints; lowered in the test), with a warning; all rows returned
checkpoint_size and batch_size are independent knobs batch_size < checkpoint_size still equals the single-pass run
re-widen (semi-wide) output under checkpoint equals non-checkpoint Reshape happens once around the checkpoint loop
carry-through (extra long columns) under checkpoint equals non-checkpoint Extra non-growthcleanr columns are reattached, appended after the gc columns, row-for-row identical to the non-checkpoint run
all-adult (explicit cutpoint 18) cleans identically checkpoint vs non-checkpoint The adult_cutpoint is finalized once in the outer branch and forwarded to checkpoints, so an all-adult (>= 18y) dataset at adult_cutpoint = 18 cleans as adults under both modes (no divergence/crash)
consolidated input warning is identical checkpoint vs non-checkpoint One dataset-wide warning (set-aside bullets + id notice), byte-identical to the non-checkpoint run — not fragmented per checkpoint with per-checkpoint denominators
deprecated cleangrowth_checkpoint() shim warns and forwards Emits a deprecation warning; result equals the folded interface
cleangrowth_checkpoint_combine() re-combines a completed run in id order Standalone re-combine of persisted checkpoint files; id_order = data$id restores input order, exclude matches the finalized run per id
cleangrowth_checkpoint_combine() with id_order = NULL returns all rows in checkpoint order No reorder requested; every input id present exactly once
cleangrowth_checkpoint_combine() warns on id_order ids absent from the files Unmatched id → plain-language warning + one NA-filled row
cleangrowth_checkpoint_combine() errors on a missing directory checkpoint_dir does not exist → clean error
cleangrowth_checkpoint_combine() errors when no checkpoint files are present Empty directory (no checkpoint_NNNN.rds) → clean error
cleangrowth_checkpoint_combine() honors quietly / verbose / log_file quietly silent; default prints combine/done; verbose prints per-file progress; log_file receives the log
mean_ht = TRUE is returned under checkpointing, matching a single run The mean_ht column reaches every checkpoint and matches a plain run
default mean_ht: same result under checkpointing, with and without adult rows Child-only data has no mean_ht column under checkpointing; with mixed data every row has it, even when some checkpoints hold only children, and it matches a single run
temporary checkpoint files are removed when a run stops with an error The temporary checkpoint folder is gone after a run that fails once its files are written
a kept checkpoint folder is not removed when a run stops with an error Files in a user-named checkpoint_dir remain after a failed run
every cleangrowth() argument is forwarded to checkpoints or deliberately not Each formal of cleangrowth() is either passed to the per-checkpoint call or on an explicit not-forwarded list, so a new argument cannot be silently dropped

test-utils.R

Post-cleaning utilities (outside the algorithm). 524 assertions (loop-heavy).

Test group What it verifies
recode_sex (defaults / custom / in place) 0/1 → 1/2 recode and custom mappings; in-place recode does not recode twice
ext_bmiz sex coding Stop on numeric sex (also as text or factor) unless the values are both 1 and 2 and no others; count warning for missing/unrecognized sex at ages 24–240 months; logical sex not read as a code; 1/2 and text codes give the same results; single-sex M or F silent
ext_bmiz existing output columns and ages near 24 months Warns and overwrites existing output columns without changing the input; count warning for rows at about 729–730 days
longwide (defaults / extra cols / keep-unmatched / other codes / errors) Long→wide reshape, extra_cols match logic, keep_unmatched_data, inclusion_types, duplicate-id error
longwide sex and param parsing Sex read like cleangrowth() (M/F, words, sex_coding, remapped column) and output as M/F, with a warning on missing/unrecognized sex; 1/2 stop and all-1 warning under the default coding, checked before rows are filtered; all-male data through simple_bmi() and ext_bmiz() silent; param read like cleangrowth() (param_coding, imperial → metric, LENGTHCM as height, HC dropped, unrecognized params warned)
simple_bmi wt / (ht·0.01)²

test-visit-date-agedays.R

Fractional agedays is cleaned as whole days but returned as submitted, with the whole-day value in gc_internal_agedays (internal_detail). Computing agedays from a date of birth plus a visit date (data-frame style). Covers route resolution (agedays column wins when present; age_source = "dates" forces the date route; supplying both an agedays column and dates uses agedays silently, no warning), the both-required rule for computing ages from dates (a lone date column with no agedays column errors), the parse-only path (a lone visit_date or dob alongside an agedays column is parsed and surfaced as gc_internal_visit_date/gc_internal_dob under internal_detail without driving ages, and is gated behind that flag), and the expanded set of accepted text-date formats: order-free year-first numeric (2013-03-04, 2013/03/04, 20130304) and English spelled-month (03mar2013, March 15, 2024), plus ambiguous numeric day/month (3/4/2013) only when date_order is set. Also covers the anti-misparse guards — one format class per run (jointly across both columns), four-digit 19xx/20xx years only (two-digit and out-of-range rejected), English-locale-only month names, and pattern-based classification so a content-unambiguous 25/12/2013 still needs date_order — rejection of unrecognized/numeric (epoch-ambiguous) columns, the gc_internal_dob/gc_internal_visit_date audit columns under full_detail, blank/NA dates yielding a missing age, and a visit date before the date of birth.


test-carry-through.R

carry_through for long input: columns growthcleanr does not consume as role columns are parked and returned appended after the growthcleanr columns, in their original order with values and types intact. Covers reserved-name collisions (the user’s column returned as userinput_<name>, with a warning; a user column named line carried unchanged), carry_through = FALSE restoring the drop-all behavior, dob/visit_date echoing back this way, the no-id path (reattachment keys on input row position, not the user’s id), carried columns named like a remapped role column (e.g. a column subjid when subjid = "patid") returned with the user’s values in single and checkpointed runs alike, a role column named line returned intact alongside carried columns, and the guarantee that the input table is not modified.


test-consolidated-input-warning.R

The consolidated input warning (Option B) and the three Class-2 defect warnings it introduced. Covers: clean data with ids is silent; measurement == 0, negative measurement, and negative agedays each warn and are set aside (per-parameter-group N/% for the measurement value problems, all-rows N/% for age); several issues collapse into a single warning() whose lead line counts the union of distinct set-aside rows (not the sum); a missing id alone yields only the Generated ids section (no Unusable data lead line) with the tie-breaking explanation; and the unrecognized-sex bullet leads with the sex_coding fix.


test-semiwide-input.R

input_format = "semi_wide": one row per visit, one column per parameter, reshaped to canonical long before cleaning. The keystone is equivalence – the same measurements submitted semi-wide and long must produce identical exclusion codes, checked on a hand-built fixture and a syngrowth slice. Also covers semiwide_cols remapping, structural column remapping, empty cells versus a literal 0, same-day duplicates resolving as same-day extraneous values, the missing-weight/height warnings (the head-circumference-absent warning was removed 2026-07-24), the date route, run-to-run determinism, the error catalog, and long output returning the user’s id under its own name with growthcleanr’s number in gc_internal_id, the long-only length rule (length.adjust is rejected in semi-wide input; a LENGTHCM/LENGTHIN value at any age gets default handling, matching a length column mapped to HEIGHTCM, including at 19 years and at days 729 and 730).


test-wide-input.R

input_format = "wide": one row per subject, repeated observation blocks marked by a _N column suffix, reshaped to canonical long before cleaning. The keystone is again equivalence – the same measurements submitted wide and long must produce identical exclusion codes – checked on a 40-subject syngrowth slice and on a hand-built ragged fixture; a companion test asserts wide and semi-wide input agree on the same measurements, and the hand-built case also pins the minted-id emission order (subject, block, then weight -> height -> head circumference). Also covers block discovery from the data (occupied cells only, so ragged subjects need no padding), default parameter-named prefixes, wide_cols and structural/age-prefix remapping, imperial and length prefixes (pounds/inches clean identically to the metric equivalents while param/measurement come back verbatim), two blocks on one day resolving as same-day extraneous values, the date route (dob + visit_date_N), a value in a block with no age flagged Exclude-Missing-Info, the four strictness warnings (block-numbering gap, max_blocks exceeded, non-monotonic block ages, a subject on more than one row), the missing-parameter warnings, run-to-run and row-order determinism, the input table left unmutated, the error catalog (no block columns, prefix with no _N columns, unrecognized parameter, two parameters sharing a prefix, missing subjid, no age source, supplied id, leading-zero block collision), the round-trip spec the re-widened output layer consumes, and the long-only length rule (length.adjust rejected; a LENGTHCM/LENGTHIN value at 2 years or older gets default handling, matching a prefix mapped to HEIGHTCM).


test-semiwide-output.R

output_format = "semi_wide": the submitted table is returned with the requested growthcleanr columns slotted in beside each value column. The keystone closes an equivalence chain in both directions – long -> long == semi -> long == melt-back of semi -> semi – on a hand-built fixture and on a syngrowth slice with same-day duplicates preserved as separate visit rows; the melt-back is written in the test rather than reusing package code, so an adapter bug cannot cancel itself out. Also covers echo fidelity (identical() per submitted column, input not mutated, verdict placed immediately right of its value column), widened_emit control and its switch-gated errors, imperial conversion reported through gc_internal_meas/gc_internal_param, the two-directional length offset on long-format fixtures (both directions and the 729/730-day boundary checked through sd.orig, conversion-before-offset, gc_internal_meas left at the entered metric value, adult LENGTHCM relabeled HEIGHTCM but not offset, missing ages left alone; length.adjust in semi-wide input is no longer supported – see the long-only length rule under the input files), carry_through in mirror mode, the three scope errors, and determinism.


test-wide-output.R

output_format = "wide": the submitted subject rows are returned with the requested growthcleanr columns slotted in beside each value block column (exclude_wt_1 right of wt_1) – direct long -> wide, echoing the input table rather than rebuilding it. The keystone closes the same both-directions equivalence chain as semi-wide output – long -> long == wide -> long == melt-back of wide -> wide – on a hand-built ragged fixture and on a syngrowth slice (blocks built from distinct visit days); the melt-back is written in the test. A separate weight-only test exercises same-day extraneous values at scale – injected duplicate same-day weights become consecutive same-agedays blocks (the case the WT+HT keystone must dedupe away) and are asserted to resolve (>= 8 Exclude-Extraneous) and round-trip through wide output unchanged. Also covers echo fidelity (identical() per submitted column, input not mutated, verdict immediately right of its value column, dropping the generated columns reproduces the input), ragged blocks getting NA verdicts, widened_emit control including the wide-specific per-block gc_internal_agedays_N level (one column per block, not per measurement, NA for absent blocks) and switch-gated errors, imperial values echoed verbatim while reported in metric through gc_internal_meas/gc_internal_param, carry_through in mirror mode, same-day blocks resolved as extraneous values with verdicts landing on their own columns, the scope errors (long/semi-wide -> wide refused) and the pre-cleaning name collision, and determinism.


test-growthcleanr-app.R

The bundled Shiny app: that it ships and loads, plus the app’s mapping-stage guards that reject bad column mappings before cleaning runs. The mapping-guard tests exercise the app’s helper logic directly (not a live browser session).

Test What it verifies
growthcleanr_app() is exported with a stable signature The launcher is exported with the expected formals
the Shiny app directory ships and app.R parses The app directory is installed and app.R parses (guards the historical package/path mismatch + syntax breakage; CRAN-safe)
wide: a wrong age prefix is rejected at mapping, not deferred to cleaning A mis-specified wide age prefix errors at the mapping step, not downstream in cleaning
sex-coding auto-detect recognizes default pairs and skips 1/2 Auto-detect recognizes the default sex-code pairs and declines the ambiguous 1/2 coding
value_freq / mapped column reactives track the sex and param selection The value-frequency and mapped-column reactives update with the selected sex/param columns
semi-wide: custom columns map, measurement columns keep their names Custom semi-wide column mappings apply while the measurement columns retain their names
semi-wide: an all-miss measurement mapping fails at Step 2, not at cleaning A measurement column that maps to all-missing is caught at the mapping step (Step 2), not at cleaning
hostile-input guard helpers catch the target problems and pass clean input The app’s hostile-input guard helpers flag the target problems and pass clean input through
long: a non-numeric measurement column is rejected at mapping A non-numeric long-format measurement column errors at mapping
long: mapping the same column to two fields is rejected at mapping Mapping one column to two roles errors at mapping
long: a valid numeric mapping still succeeds (guards do not over-reject) A valid long-format numeric mapping is accepted (guards do not over-reject)
long: results keep the user’s names, including role-like unmapped columns A full long run returns the user’s column names, and unmapped columns named like growthcleanr’s stay unchanged
semi-wide: results keep the user’s names A full semi-wide run returns the user’s column names plus exclude_<column>
wide: results keep the user’s names A full wide run returns the user’s column names
keep-only trim keeps the mapped columns under the user’s names The keep-only option keeps exactly the mapped columns, under the user’s names
the exclusion summary does not count the user’s own exclude_ columns A user column named exclude_reason is not pooled into the summary, in all three formats
the format can be chosen before or after the file is loaded Mapping works with either order in Step 1
date samples skip blank and no-digit values and use a wide block’s first usable column The sample-date helper returns the first usable value for a column or wide block, and nothing when there is none
exact standard measurement names pre-select the column and its unit WEIGHTLBS, HEIGHTIN, HEADCM and HEADIN pre-select the column and the matching unit in semi-wide and wide; custom names are not guessed
a new file, format or mapping clears results, notes and the stale mapping Re-applying a mapping clears results and notes; a new file or format also clears the mapping
a failed run does not leave the prior run’s results available After a run stops, no results remain from the earlier run; stating the date order lets the same file run

Coverage Gaps

Summary of the standing gaps:

Known gaps