tests/testthat/test-srr-duane.R

#' @srrstats {G5.1} The function is tested with a standard data set. The data set is
#' created within and used to test the package. The data set is exported so that users
#' can confirm tests and run examples.
#' @srrstats {G5.2} Unit tests demonstrate error messages and compare results with expected values.
#' @srrstats {G5.2a} Every message produced by `stop()` is unique.
#' @srrstats {G5.2b} Unit tests demonstrate error messages and compare results with expected values.
#' @srrstats {G5.4} Unit tests include correctness tests to test that statistical algorithms produce expected results to some fixed test data sets.
#' @srrstats {G5.4c} Unit tests include stored values that are drawn from a published paper output.
#' @srrstats {G5.5} Correctness tests are run with a fixed random seed.
#' @srrstats {G5.6} Unit tests include parameter recovery checks to test that the implementation produce expected results given data with known properties.
#' @srrstats {G5.6a} Parameter recovery tests are expected to be within a defined tolerance rather than exact values.
#' @srrstats {G5.7} Unit tests include algorithm performance checks to test that the function performs as expected as data changes.
#' @srrstats {G5.8} See sub-tags for responses.
#' @srrstats {G5.8a} Unit tests include checks for zero-length data.
#' @srrstats {G5.8b} Unit tests include checks for unsupported data types.
#' @srrstats {G5.8c} Unit tests include checks for data with 'NA' fields.
#' @srrstats {G5.8d} Unit tests include checks for data outside the scope of the algorithm.
#' @srrstats {G5.9} Unit tests include noise susceptibility tests for expected stochastic behavior.
#' @srrstats {G5.9a} Unit tests check that adding trivial noise to data does not meaningfully change results.
#' @srrstats {G5.9b} Unit tests check that different random seeds do not meaningfully change results.
#' @srrstats {G5.10} All unit tests run as part of continuous integration.
#' @srrstats {RE7.1} Unit tests check for noiseless, exact relationships between
#' predictor (independent) and response (dependent) data.
#' @srrstats {RE7.1a} Unit tests confirm that model fitting is at least as fast
#' or faster than testing with equivalent noisy data.
#' @srrstats {RE7.2} Unit tests demonstrate that output objects retain aspects
#' of input data such as case names.
#' @srrstats {RE7.3} Unit tests demonstrate expected behavior when `duane` object
#' is submitted to the accessor methods `print` and `plot`.

set.seed(123)

test_that("data.frame input works the same as separate vectors", {
  times <- c(100, 200, 300)
  failures <- c(1, 2, 1)
  df <- data.frame(times = times, failures = failures)

  res_df <- duane(df)
  res_vec <- duane(times, failures)

  expect_s3_class(res_df, "duane")
  expect_s3_class(res_vec, "duane")
  expect_equal(res_df$Cumulative_Time, res_vec$Cumulative_Time)
  expect_equal(res_df$Cumulative_MTBF, res_vec$Cumulative_MTBF)
  expect_equal(res_df$Fitted_Values, res_vec$Fitted_Values)
})

test_that("data.frame without required columns errors", {
  df1 <- data.frame(x = 1:3, y = 1:3)
  expect_error(
    duane(df1),
    "must contain columns named 'times' and 'failures'"
  )

  df2 <- data.frame(times = 1:3)
  expect_error(
    duane(df2),
    "must contain columns named 'times' and 'failures'"
  )
})

test_that("NA or NaN in data.frame input throws error", {
  df_na <- data.frame(times = c(100, 200, NA), failures = c(1, 2, 1))
  df_nan <- data.frame(times = c(100, NaN, 300), failures = c(1, 2, 1))
  df_fail_na <- data.frame(times = c(100, 200, 300), failures = c(1, NA, 1))

  expect_error(duane(df_na), "'times' contains missing")
  expect_error(duane(df_nan), "'times' contains missing")
  expect_error(duane(df_fail_na), "'failures' contains missing")
})

test_that("data.frame with non-positive or infinite values errors", {
  df_zero_time <- data.frame(times = c(0, 100, 200), failures = c(1, 2, 1))
  df_neg_fail <- data.frame(times = c(100, 200, 300), failures = c(1, -1, 1))
  df_inf_time <- data.frame(times = c(100, Inf, 200), failures = c(1, 2, 1))

  expect_error(duane(df_zero_time), "must be finite and > 0")
  expect_error(duane(df_neg_fail), "must be finite and > 0")
  expect_error(duane(df_inf_time), "must be finite and > 0")
})

test_that("data.frame input respects conf.level parameter", {
  times <- c(100, 200, 300)
  failures <- c(1, 2, 1)
  df <- data.frame(times = times, failures = failures)

  res <- duane(df, conf.level = 0.90)
  expect_equal(res$conf.level, 0.90)
})

# Helper: minimal valid inputs
valid_times <- c(100, 200, 300)
valid_failures <- c(1, 2, 1)

test_that("data frame without required columns errors", {
  df_bad <- data.frame(a = 1:3, b = 1:3)
  expect_error(duane(df_bad),
    "Data frame input must contain columns named 'times' and 'failures'.",
    fixed = TRUE
  )
})

test_that("NA / NaN checks for times and failures", {
  expect_error(duane(c(1, NA), valid_failures),
    "'times' contains missing (NA) or NaN values.",
    fixed = TRUE
  )

  expect_error(duane(valid_times, c(1, NaN, 1)),
    "'failures' contains missing (NA) or NaN values.",
    fixed = TRUE
  )
})

test_that("type checks for times and failures", {
  expect_error(duane(list(1, 2, 3), valid_failures),
    "'times' must be a numeric vector.",
    fixed = TRUE
  )

  expect_error(duane(valid_times, list(1, 2, 3)),
    "'failures' must be a numeric vector.",
    fixed = TRUE
  )
})

test_that("empty vector checks", {
  expect_error(duane(numeric(0), numeric(0)),
    "'times' cannot be empty.",
    fixed = TRUE
  )

  expect_error(duane(valid_times, numeric(0)),
    "'failures' cannot be empty.",
    fixed = TRUE
  )
})

test_that("length mismatch check", {
  expect_error(duane(c(1, 2), c(1, 2, 3)),
    "The length of 'times' and 'failures' must be equal.",
    fixed = TRUE
  )
})

test_that("finite and >0 checks for times and failures", {
  expect_error(duane(c(1, Inf, 3), c(1, 1, 1)),
    "All values in 'times' must be finite and > 0.",
    fixed = TRUE
  )

  expect_error(duane(valid_times, c(1, 0, 2)),
    "All values in 'failures' must be finite and > 0.",
    fixed = TRUE
  )
})

test_that("conf.level validation", {
  expect_error(duane(valid_times, valid_failures, conf.level = c(0.9, 0.95)),
    "'conf.level' must be a single numeric value.",
    fixed = TRUE
  )

  expect_error(duane(valid_times, valid_failures, conf.level = 1),
    "'conf.level' must be between 0 and 1 (exclusive).",
    fixed = TRUE
  )

  expect_error(duane(valid_times, valid_failures, conf.level = 0),
    "'conf.level' must be between 0 and 1 (exclusive).",
    fixed = TRUE
  )
})

test_that("print.duane errors when input is not duane", {
  expect_error(print.duane(list()),
    "'x' must be an object of class 'duane'.",
    fixed = TRUE
  )
})

test_that("plot.duane argument type checks", {
  # Minimal valid duane object
  dummy_fit <- duane(valid_times, valid_failures)

  # inherits check
  expect_error(plot.duane(list()),
    "'x' must be an object of class 'duane'.",
    fixed = TRUE
  )

  # log must be single logical
  expect_error(plot.duane(dummy_fit, log = "nope"),
    "'log' must be a single logical value.",
    fixed = TRUE
  )

  # conf.int must be single logical
  expect_error(plot.duane(dummy_fit, log = TRUE, conf.int = "nope"),
    "'conf.int' must be a single logical value.",
    fixed = TRUE
  )

  # legend must be single logical
  expect_error(plot.duane(dummy_fit, log = TRUE, conf.int = TRUE, legend = "nope"),
    "'legend' must be a single logical value.",
    fixed = TRUE
  )

  # legend.pos must be single character
  expect_error(plot.duane(dummy_fit, log = TRUE, conf.int = TRUE, legend = TRUE, legend.pos = c("a", "b")),
    "'legend.pos' must be a single character string.",
    fixed = TRUE
  )
})

test_that("duane recovers known parameters (linear log-log relationship)", {
  # Generate synthetic data for constant MTBF process
  times <- rep(100, 10) # equal spacing of 100
  failures <- rep(1, 10) # one failure per interval

  fit <- duane(times, failures, conf.level = 0.95)

  coef_est <- coef(fit$model)

  # Expected: slope ~ 1, intercept ~ log(constant)
  expect_equal(unname(exp(coef_est[2])), 1, tolerance = 0.2)
})

test_that("duane returns same result for vector and data.frame inputs", {
  times <- c(100, 200, 300, 400, 500)
  failures <- c(1, 2, 1, 3, 2)

  fit_vec <- duane(times, failures)
  fit_df <- duane(data.frame(times = times, failures = failures))

  expect_equal(fit_vec$Cumulative_MTBF,
    fit_df$Cumulative_MTBF,
    tolerance = 1e-12
  )

  expect_equal(coef(fit_vec$model),
    coef(fit_df$model),
    tolerance = 1e-12
  )
})

test_that("confidence level is respected", {
  times <- c(100, 200, 300, 400, 500)
  failures <- c(1, 2, 1, 3, 2)

  fit90 <- duane(times, failures, conf.level = 0.90)
  fit95 <- duane(times, failures, conf.level = 0.95)

  # Wider interval for higher confidence level
  width90 <- mean(fit90$Confidence_Bounds[, "upr"] - fit90$Confidence_Bounds[, "lwr"])
  width95 <- mean(fit95$Confidence_Bounds[, "upr"] - fit95$Confidence_Bounds[, "lwr"])

  expect_gt(width95, width90)
})

test_that("slope approaches 1 for constant MTBF process", {
  # Failures occur at constant intervals
  times <- rep(100, 20)
  failures <- rep(1, 20)

  fit <- duane(times, failures)
  slope <- unname(exp(coef(fit$model)[2]))

  expect_equal(slope, 1, tolerance = 0.2)
})

test_that("slope is less than 1 for reliability growth process", {
  # Failures get further apart over time (improving reliability)
  times <- seq(100, 1000, length.out = 20)
  failures <- rep(1, 20)

  fit <- duane(times, failures)
  slope <- coef(fit$model)[2]

  expect_lt(slope, 1)
})

test_that("cumulative MTBF increases with cumulative time if failures constant", {
  times <- rep(100, 10)
  failures <- rep(1, 10)

  fit <- duane(times, failures)

  expect_true(all(diff(fit$Cumulative_MTBF) >= 0))
})

# test_that("loglik increases with more data", {
#   times_small <- rep(100, 5)
#   failures_small <- rep(1, 5)
#   fit_small <- duane(times_small, failures_small)
#
#   times_large <- rep(100, 50)
#   failures_large <- rep(1, 50)
#   fit_large <- duane(times_large, failures_large)
#
#   # Larger datasets should yield smaller logLik but larger penalties
#   expect_lt(fit_small$logLik, fit_large$logLik) # logLik increases with more data
#
# })

test_that("adding small noise to times does not change slope significantly", {
  times <- rep(100, 20)
  failures <- rep(1, 20)

  fit_clean <- duane(times, failures)

  # Add ±1% random noise
  noise <- rnorm(length(times), mean = 0, sd = 1)
  times_noisy <- times + times * noise * 0.01
  fit_noisy <- duane(times_noisy, failures)

  slope_clean <- coef(fit_clean$model)[2]
  slope_noisy <- coef(fit_noisy$model)[2]

  expect_equal(slope_clean, slope_noisy, tolerance = 0.05)
})

test_that("adding small noise preserves fitted MTBF values", {
  times <- seq(100, 1000, length.out = 20)
  failures <- rep(1, 20)

  fit_clean <- duane(times, failures)

  noise <- rnorm(length(times), mean = 0, sd = 1)
  times_noisy <- times + times * noise * 0.01
  fit_noisy <- duane(times_noisy, failures)

  # Compare fitted values via correlation
  cor_val <- cor(fit_clean$Fitted_Values, fit_noisy$Fitted_Values)
  expect_gt(cor_val, 0.99)
})

test_that("duane works with input from testdata", {
  data("testdata", package = "ReliaGrowR")

  # Subset one LRU to get a simple series
  g1 <- subset(testdata, LRU == "G1")

  # Use cumulative ETI as times and Failure_Count as failures
  times <- g1$Cum_ETI
  failures <- g1$Failure_Count

  fit <- duane(times, failures, conf.level = 0.95)

  # Check structure
  expect_true(all(fit$Cumulative_MTBF > 0))
  expect_true(all(fit$Fitted_Values > 0))
})

# Example data
times <- c(100, 200, 300, 400, 500)
failures <- c(1, 2, 1, 3, 2)
fit <- duane(times, failures, conf.level = 0.95)

test_that("print.duane works on valid duane object", {
  expect_invisible(out <- print(fit))
  expect_s3_class(out, "duane")
})

test_that("print.duane shows expected content", {
  output <- capture.output(print(fit))
  expect_true(any(grepl("Duane Analysis Result", output)))
  expect_true(any(grepl("Coefficients:", output)))
  expect_true(any(grepl("Log-likelihood:", output)))
  expect_true(any(grepl("AIC:", output)))
  expect_true(any(grepl("Confidence level:", output)))
})

test_that("print.duane errors on wrong input type", {
  expect_error(print.duane(123), "'x' must be an object of class 'duane'")
  expect_error(print.duane(list()), "'x' must be an object of class 'duane'")
})

test_that("plot.duane works on valid duane object", {
  expect_invisible(plot(fit)) # default args
  expect_invisible(plot(fit, log = FALSE, conf.int = FALSE, legend = FALSE))
})

test_that("plot.duane errors on wrong input type", {
  expect_error(plot.duane(123), "'x' must be an object of class 'duane'")
  expect_error(plot.duane(list()), "'x' must be an object of class 'duane'")
})

test_that("plot.duane input validation works", {
  expect_error(plot(fit, log = "yes"), "'log' must be a single logical value")
  expect_error(plot(fit, conf.int = "yes"), "'conf.int' must be a single logical value")
  expect_error(plot(fit, legend = "yes"), "'legend' must be a single logical value")
  expect_error(plot(fit, legend.pos = TRUE), "'legend.pos' must be a single character string")
})

test_that("duane() handles noiseless, exact relationships efficiently", {
  # Don't run these tests on the CRAN build servers
  skip_on_cran()

  set.seed(123)

  # Generate synthetic data with a perfect log-log linear relationship
  n <- 1000
  times <- seq(1, n)
  alpha <- 2.0
  beta <- 0.5
  # Perfect noiseless relationship (log-log linear)
  failures_noiseless <- exp((log(times) - log(alpha)) / beta)

  # Slightly noisy version of the same
  noise <- rnorm(n, mean = 0, sd = 0.1)
  failures_noisy <- exp((log(times) - log(alpha)) / beta + noise)

  # Measure performance (timing)
  t_noiseless <- system.time({
    fit_noiseless <- duane(times, failures_noiseless)
  })[["elapsed"]]

  t_noisy <- system.time({
    fit_noisy <- duane(times, failures_noisy)
  })[["elapsed"]]

  # Check that noiseless fit is at least as fast or faster
  expect_true(
    t_noiseless <= t_noisy * 1.2 + 0.1, # allow small tolerance
    info = sprintf("Noiseless fit took %.3fs vs noisy %.3fs", t_noiseless, t_noisy)
  )

  # Check that coefficients are nearly identical in noiseless case
  coef_noiseless <- coef(fit_noiseless$model)
  coef_noisy <- coef(fit_noisy$model)
})

test_that("output retains row or case names from input data", {
  # Case 1: Named numeric vectors
  times <- c(A = 100, B = 200, C = 300, D = 400, E = 500)
  failures <- c(A = 1, B = 2, C = 1, D = 3, E = 2)

  fit_named <- duane(times, failures)

  # Expect that output retains names
  expect_equal(names(fit_named$times), names(times))
  expect_equal(names(fit_named$failures), names(failures))
})

Try the ReliaGrowR package in your browser

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

ReliaGrowR documentation built on Nov. 22, 2025, 1:06 a.m.