tests/testthat/test-validation.R

# test-validation.R
# Tests for validation_utils.R internal functions
# -----------------------------------------------------------------------------
# Key API facts discovered from function source:
#   .check_prior_data_compat(prior, data_summary)
#     - data_summary is a LIST (type=, x=, n=, ...), NOT a bare string
#     - always returns list(ok, msg, severity); never throws an error
#     - mismatched family/type returns ok=TRUE with severity="warning"
#
#   .check_pooling_compat(priors)
#     - takes a LIST of bayprior objects
#     - incompatible supports: returns list(ok=FALSE, severity="error")
#     - same support, mixed families: returns list(ok=TRUE, severity="warning")
#     - does NOT throw errors -- caller decides what to do with $ok
#
#   .check_sensitivity_compat(prior)
#     - takes ONLY the prior -- no param_grid argument
#     - returns list(ok, msg, severity)
#     - Exponential (single-param): ok=TRUE, severity="info"
# -----------------------------------------------------------------------------

library(testthat)
library(bayprior)

# -- .check_prior_data_compat() ------------------------------------------------

test_that(".check_prior_data_compat passes for compatible family-type pairs", {
  r1 <- .check_prior_data_compat(
    elicit_beta(mean = 0.3, sd = 0.1, method = "moments"),
    list(type = "binary", x = 10, n = 30))
  expect_true(r1$ok)
  expect_equal(r1$severity, "none")

  r2 <- .check_prior_data_compat(
    elicit_normal(mean = 0.3, sd = 0.1, method = "moments"),
    list(type = "continuous", x = 0.3, sd = 0.1, n = 30))
  expect_true(r2$ok)
  expect_equal(r2$severity, "none")

  r3 <- .check_prior_data_compat(
    elicit_gamma(mean = 3.0, sd = 1.0, method = "moments"),
    list(type = "poisson", x = 9, n = 3))
  expect_true(r3$ok)
  expect_equal(r3$severity, "none")

  r4 <- .check_prior_data_compat(
    elicit_exponential(mean = 2.0, method = "moments"),
    list(type = "poisson", x = 5, n = 10))
  expect_true(r4$ok)
})

test_that(".check_prior_data_compat returns warning (not error) for mismatches", {
  # Normal supports binary and continuous -- NOT poisson
  r1 <- .check_prior_data_compat(
    elicit_normal(mean = 0.3, sd = 0.1, method = "moments"),
    list(type = "poisson", x = 10, n = 30))
  expect_true(r1$ok)          # ok=TRUE -- proceeds with approximation
  expect_equal(r1$severity, "warning")
  expect_true(nchar(r1$msg) > 0)

  # LogNormal only supports continuous -- binary is a mismatch
  r2 <- .check_prior_data_compat(
    elicit_lognormal(mean = 1.0, sd = 0.3, method = "moments"),
    list(type = "binary", x = 10, n = 30))
  expect_true(r2$ok)
  expect_equal(r2$severity, "warning")
})

test_that(".check_prior_data_compat returns list with expected fields", {
  r <- .check_prior_data_compat(
    elicit_beta(mean = 0.3, sd = 0.1, method = "moments"),
    list(type = "binary", x = 10, n = 30))
  expect_true("ok"       %in% names(r))
  expect_true("severity" %in% names(r))
})

# -- .check_pooling_compat() ---------------------------------------------------

test_that(".check_pooling_compat returns ok=TRUE for same-family priors", {
  p1 <- elicit_beta(mean = 0.3, sd = 0.1, method = "moments")
  p2 <- elicit_beta(mean = 0.5, sd = 0.1, method = "moments")
  p3 <- elicit_beta(mean = 0.7, sd = 0.1, method = "moments")
  r  <- .check_pooling_compat(list(p1, p2, p3))
  expect_true(r$ok)
  expect_equal(r$severity, "none")
})

test_that(".check_pooling_compat returns ok=FALSE for incompatible supports", {
  # Beta (unit support) + Normal (real support) -- incompatible
  p1 <- elicit_beta(mean  = 0.3, sd = 0.1, method = "moments")
  p2 <- elicit_normal(mean = 0.0, sd = 0.5, method = "moments")
  r  <- .check_pooling_compat(list(p1, p2))
  expect_false(r$ok)
  expect_equal(r$severity, "error")
  expect_true(nchar(r$msgs) > 0)
})

test_that(".check_pooling_compat returns warning for same-support mixed families", {
  # Gamma + Exponential: both positive support -- warning, not error
  p1 <- elicit_gamma(mean = 3.0, sd = 1.5, method = "moments")
  p2 <- elicit_exponential(mean = 2.0, method = "moments")
  r  <- .check_pooling_compat(list(p1, p2))
  expect_true(r$ok)
  expect_equal(r$severity, "warning")
})

# -- .check_sensitivity_compat() -----------------------------------------------

test_that(".check_sensitivity_compat returns ok=TRUE for standard priors", {
  for (pr in list(
    elicit_beta(mean = 0.3, sd = 0.1, method = "moments"),
    elicit_normal(mean = 0.0, sd = 0.5, method = "moments"),
    elicit_gamma(mean = 3.0, sd = 1.5, method = "moments"),
    elicit_lognormal(mean = 1.0, sd = 0.3, method = "moments")
  )) {
    r <- .check_sensitivity_compat(pr)
    expect_true(r$ok, label = paste("ok for dist:", pr$dist))
  }
})

test_that(".check_sensitivity_compat returns info for Exponential (single-param)", {
  pr <- elicit_exponential(mean = 2.0, method = "moments")
  r  <- .check_sensitivity_compat(pr)
  expect_true(r$ok)
  expect_equal(r$severity, "info")
  expect_true(nchar(r$msg) > 0)
})

test_that(".check_sensitivity_compat returns list with expected fields", {
  r <- .check_sensitivity_compat(
    elicit_beta(mean = 0.3, sd = 0.1, method = "moments"))
  expect_true("ok"       %in% names(r))
  expect_true("severity" %in% names(r))
})

# -- .dist_support() -----------------------------------------------------------
# No prior direct coverage -- only exercised incidentally via other functions.

test_that(".dist_support classifies each known family correctly", {
  expect_equal(.dist_support("beta"),        "unit")
  expect_equal(.dist_support("normal"),      "real")
  expect_equal(.dist_support("gamma"),       "positive")
  expect_equal(.dist_support("lognormal"),   "positive")
  expect_equal(.dist_support("exponential"), "positive")
  expect_equal(.dist_support("weibull"),     "positive")
  expect_equal(.dist_support("mixture"),     "mixed")
})

test_that(".dist_support falls back to \"unknown\" for an unrecognised family", {
  # e.g. "log_pool" -- .log_pool() sets dist = "log_pool", not "mixture",
  # so it is not one of the switch()'s named cases and falls through.
  expect_equal(.dist_support("log_pool"), "unknown")
  expect_equal(.dist_support("nonsense"), "unknown")
})

# -- .prior_support() ----------------------------------------------------------
# No prior coverage at all.

test_that(".prior_support passes through for non-mixture priors", {
  expect_equal(.prior_support(elicit_beta(mean = 0.3, sd = 0.1, method = "moments")),
              "unit")
  expect_equal(.prior_support(elicit_gamma(mean = 3, sd = 1, method = "moments")),
              "positive")
})

test_that(".prior_support returns a single support for a same-support mixture", {
  e1  <- elicit_gamma(mean = 3, sd = 1, method = "moments", expert_id = "E1")
  e2  <- elicit_exponential(mean = 2, method = "moments", expert_id = "E2")
  # aggregate_experts() -> elicit_mixture() warns when components share a
  # support but differ in family, since the mixture density then has to be
  # evaluated numerically rather than analytically. Expected here, since
  # this test specifically constructs a mixed-family, same-support pool.
  expect_warning(
    mix <- aggregate_experts(list(E1 = e1, E2 = e2), weights = c(0.5, 0.5),
                             method = "linear"),
    "different distribution families"
  )
  expect_equal(.prior_support(mix), "positive")
})

test_that(".prior_support returns \"mixed\" for an incompatible-support mixture", {
  # Bypasses aggregate_experts()'s own compatibility check (which would
  # normally reject this combination) by constructing the mixture object
  # directly, to isolate .prior_support()'s own logic.
  e1  <- elicit_beta(mean = 0.3, sd = 0.1, method = "moments", expert_id = "E1")
  e2  <- elicit_normal(mean = 0.0, sd = 0.5, method = "moments", expert_id = "E2")
  mix <- structure(
    list(dist = "mixture", components = list(e1, e2), weights = c(0.5, 0.5)),
    class = "bayprior"
  )
  expect_equal(.prior_support(mix), "mixed")
})

# -- .check_prior_data_compat() -- mixture and log_pool handling -------------
# The existing tests above never pass a mixture or log-pooled prior.

test_that(".check_prior_data_compat resolves a mixture to its dominant component", {
  e1  <- elicit_beta(mean = 0.30, sd = 0.08, method = "moments", expert_id = "E1")
  e2  <- elicit_beta(mean = 0.42, sd = 0.10, method = "moments", expert_id = "E2")
  pool <- aggregate_experts(list(E1 = e1, E2 = e2), weights = c(0.7, 0.3),
                            method = "linear")
  # Dominant component (weight 0.7) is Beta -> binary data is compatible.
  r <- .check_prior_data_compat(pool, list(type = "binary", x = 18, n = 40))
  expect_true(r$ok)
  expect_equal(r$severity, "none")
})

test_that(".check_prior_data_compat silently skips validation for a
           logarithmically-pooled prior", {
  # .log_pool() sets dist = "log_pool", which is neither "mixture" (so the
  # dominant-component extraction at line 35 never fires) nor one of the six
  # keys in the compat list -- so `recommended` is NULL and the function
  # returns ok=TRUE/severity="none" unconditionally, regardless of whether
  # the data type actually suits the pooled experts' family. This documents
  # that behavior explicitly rather than leaving it silently untested.
  e1  <- elicit_beta(mean = 0.30, sd = 0.08, method = "moments", expert_id = "E1")
  e2  <- elicit_beta(mean = 0.42, sd = 0.10, method = "moments", expert_id = "E2")
  pool_log <- aggregate_experts(list(E1 = e1, E2 = e2), weights = c(0.5, 0.5),
                                method = "logarithmic")
  expect_equal(pool_log$dist, "log_pool")

  # Even a data type nothing here supports (e.g. survival, unrelated to
  # Beta experts) is reported as fully compatible, with no warning.
  r <- .check_prior_data_compat(pool_log, list(type = "survival", x = 5, n = 40))
  expect_true(r$ok)
  expect_equal(r$severity, "none")
  expect_null(r$msg)
})

# -- .check_pooling_compat() -- nested mixture ---------------------------------

test_that(".check_pooling_compat treats a nested mixture as an incompatible support", {
  # A mixture passed inside the list of priors to pool: .dist_support()
  # classifies dist = "mixture" as support "mixed", which never matches
  # another prior's "unit"/"positive"/"real" support, so this is always
  # flagged as an incompatible-support error rather than silently accepted.
  beta1 <- elicit_beta(mean = 0.3, sd = 0.1, method = "moments", expert_id = "E1")
  beta2 <- elicit_beta(mean = 0.5, sd = 0.1, method = "moments", expert_id = "E2")
  inner_pool <- aggregate_experts(list(E1 = beta1, E2 = beta2),
                                  weights = c(0.5, 0.5), method = "linear")
  beta3 <- elicit_beta(mean = 0.4, sd = 0.1, method = "moments", expert_id = "E3")

  r <- .check_pooling_compat(list(inner_pool, beta3))
  expect_false(r$ok)
  expect_equal(r$severity, "error")
})

# -- .check_sensitivity_compat() -- mixture branches (previously 0% covered) --

test_that(".check_sensitivity_compat warns for a same-support mixed-family mixture", {
  e1  <- elicit_gamma(mean = 3, sd = 1, method = "moments", expert_id = "E1")
  e2  <- elicit_exponential(mean = 2, method = "moments", expert_id = "E2")
  expect_warning(
    pool <- aggregate_experts(list(E1 = e1, E2 = e2), weights = c(0.5, 0.5),
                              method = "linear"),
    "different distribution families"
  )
  r <- .check_sensitivity_compat(pool)
  expect_true(r$ok)
  expect_equal(r$severity, "warning")
  expect_true(nchar(r$msg) > 0)
})

test_that(".check_sensitivity_compat errors for an incompatible-support mixture", {
  e1  <- elicit_beta(mean = 0.3, sd = 0.1, method = "moments", expert_id = "E1")
  e2  <- elicit_normal(mean = 0.0, sd = 0.5, method = "moments", expert_id = "E2")
  mix <- structure(
    list(dist = "mixture", components = list(e1, e2), weights = c(0.5, 0.5)),
    class = "bayprior"
  )
  r <- .check_sensitivity_compat(mix)
  expect_false(r$ok)
  expect_equal(r$severity, "error")
})

# -- .validation_alert() -- 0% covered, no tests existed ----------------------

test_that(".validation_alert returns NULL for NULL or empty message", {
  expect_null(.validation_alert(NULL))
  expect_null(.validation_alert(""))
})

test_that(".validation_alert renders each severity with the right CSS class", {
  info_alert <- .validation_alert("An info message", severity = "info")
  expect_true(grepl("alert-info", as.character(info_alert), fixed = TRUE))

  warn_alert <- .validation_alert("A warning message", severity = "warning")
  expect_true(grepl("alert-warning", as.character(warn_alert), fixed = TRUE))

  err_alert <- .validation_alert("An error message", severity = "error")
  expect_true(grepl("alert-danger", as.character(err_alert), fixed = TRUE))
})

test_that(".validation_alert falls back to the info style for an unrecognised severity", {
  alert <- .validation_alert("A message", severity = "not_a_real_severity")
  expect_true(grepl("alert-info", as.character(alert), fixed = TRUE))
})

Try the bayprior package in your browser

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

bayprior documentation built on Aug. 27, 2026, 1:09 a.m.