tests/testthat/test-data-prepare-phases.R

box::use(
  testthat[expect_equal, expect_false, expect_identical, expect_true, test_that],
  testing / fixtures / index[FIXTURES],
  testing / mocks / index[MOCKS]
)

# Both tests drive the same three-phase pipeline over equivalent inputs, so the
# expensive fixture work (mock CSV plus options_create/options_load, most of
# this file's runtime) happens once at file scope. options_load() is pure: it
# returns the prefixed list (including artma.temp.* bookkeeping) without
# touching options(), so each test activates the shared list with
# withr::local_options() and stays isolated; every artma.* key the pipeline
# writes during a test is present in the list and therefore restored on exit.
# The dataset is deliberately tiny: these tests exercise cache and config
# bookkeeping, not data richness.
shared_runtime_options <- local({
  fixture_dir <- withr::local_tempdir(.local_envir = testthat::teardown_env())

  df <- MOCKS$create_mock_df(seed = 42, nrow = 60, n_studies = 5)
  source_path <- file.path(fixture_dir, "phases-data.csv")
  utils::write.csv(df, source_path, row.names = FALSE)

  options_dir <- file.path(fixture_dir, "options")
  dir.create(options_dir)

  artma::options_create(
    options_file_name = "phases.yaml",
    options_dir = options_dir,
    user_input = list(
      "data.source_path" = source_path,
      "data.na_handling" = "remove",
      "data.reconcile_mode" = "auto",
      "calc.se_zero_handling" = "ignore",
      "cache.use_cache" = TRUE,
      "verbose" = 1L
    ),
    should_validate = TRUE,
    should_overwrite = TRUE
  )

  artma::options_load(
    options_file_name = "phases.yaml",
    options_dir = options_dir,
    load_with_prefix = TRUE,
    should_validate = TRUE,
    should_add_temp_options = TRUE,
    should_return = TRUE
  )
})

# Orchestrate the three phases with an isolated in-memory compute cache, so the
# test never touches the user cache directory. Mirrors prepare_data() exactly.
local_phase_runner <- function() {
  box::use(
    artma / data / index[configure_data, compute_data_impl, persist_data, prime_raw_df],
    artma / data / read[read_data],
    artma / data / cache_signatures[build_data_cache_signature],
    artma / libs / infrastructure / cache[cache_cli_runner]
  )

  compute_runs <- 0L
  self_env <- environment()
  counted_compute <- function() {
    self_env$compute_runs <- self_env$compute_runs + 1L
    compute_data_impl()
  }
  compute_cached <- cache_cli_runner(
    counted_compute,
    stage = "prepare_data",
    key_builder = function(...) build_data_cache_signature(),
    cache = memoise::cache_memory()
  )

  run <- function() {
    df_raw <- read_data()
    prime_raw_df(df_raw)
    configure_data(df_raw)
    df <- compute_cached()
    persist_data(df)
    df
  }

  list(run = run, compute_runs = function() self_env$compute_runs)
}

test_that("warm-cache run repopulates a deleted data config (motivating bug)", {
  FIXTURES$local_cli_silence()

  withr::local_options(shared_runtime_options)

  runner <- local_phase_runner()

  # Run 1 (cold): computes and registers the computed columns in the config.
  runner$run()
  expect_equal(runner$compute_runs(), 1L)

  after_first <- getOption("artma.data.columns")
  computed_keys <- c("t_stat", "study_size", "reg_dof", "precision", "study_label")
  expect_true(all(computed_keys %in% names(after_first)))

  # Simulate deleting the data config: clear it entirely.
  options("artma.data.columns" = list())

  # Run 2 (warm): compute is a cache hit and does NOT run, but persist must
  # still repopulate the config.
  runner$run()
  expect_equal(runner$compute_runs(), 1L) # compute was skipped (cache hit)

  after_second <- getOption("artma.data.columns")
  expect_true(all(computed_keys %in% names(after_second)))
})

test_that("non-interactive prepare is identical run to run with cache hits from run 2", {
  FIXTURES$local_cli_silence()

  withr::local_options(shared_runtime_options)

  runner <- local_phase_runner()

  first <- runner$run()
  expect_equal(runner$compute_runs(), 1L)

  second <- runner$run()
  expect_equal(runner$compute_runs(), 1L) # run 2 hits the cache
  expect_identical(first, second)
})

test_that("data.derived columns are computed and registered in the data config", {
  FIXTURES$local_cli_silence()

  withr::local_options(shared_runtime_options)
  withr::local_options(list(
    "artma.data.derived" = list(
      large_sample = "n_obs > 500",
      se_x_large_sample = "se * large_sample"
    )
  ))

  runner <- local_phase_runner()
  df <- runner$run()

  expect_true(all(c("large_sample", "se_x_large_sample") %in% names(df)))
  expect_identical(df$large_sample, as.integer(df$n_obs > 500))
  expect_equal(df$se_x_large_sample, df$se * as.integer(df$n_obs > 500))

  config <- getOption("artma.data.columns")
  expect_true(all(c("large_sample", "se_x_large_sample") %in% names(config)))
  expect_true(isTRUE(config$se_x_large_sample$is_computed))
})

Try the artma package in your browser

Any scripts or data that you put into this service are public.

artma documentation built on Oct. 27, 2026, 9:06 a.m.