tests/testthat/test-options-column-preprocessing.R

box::use(
  testthat[expect_equal, expect_false, expect_true, test_that]
)

box::use(
  artma / options / column_preprocessing[preprocess_column_mapping]
)


# Helper to create a mock options_def structure
create_mock_options_def <- function() {
  list(
    list(
      name = "data.source_path",
      type = "character",
      allow_na = FALSE
    ),
    list(
      name = "data.config_setup",
      type = "enum",
      allow_na = FALSE
    ),
    list(
      name = "data.columns",
      type = "list",
      allow_na = TRUE
    )
  )
}


# Helper to create a temp CSV file
create_temp_csv_file <- function() {
  csv_content <- c(
    "study_name;effect;se;n_obs",
    "Study A;10.5;2.3;100",
    "Study B;8.2;1.8;150"
  )

  tmp_file <- tempfile(fileext = ".csv")
  writeLines(csv_content, tmp_file)
  tmp_file
}


# When there is nothing to auto-detect (no source path, NA/empty path, or a
# path that does not resolve to a file), the mapping is returned unchanged.
test_that("preprocess_column_mapping returns input unchanged when there is nothing to auto-detect", {
  options_def <- create_mock_options_def()
  withr::local_options(list("artma.verbose" = 1))

  cases <- list(
    "no data.source_path" = list("verbose" = 3),
    "data.source_path is NA" = list("data.source_path" = NA),
    "data.source_path is empty string" = list("data.source_path" = ""),
    "file does not exist" = list("data.source_path" = "/nonexistent/path/to/file.csv")
  )

  for (label in names(cases)) {
    user_input <- cases[[label]]
    expect_equal(
      preprocess_column_mapping(user_input, options_def),
      user_input,
      info = label
    )
  }
})


test_that("preprocess_column_mapping returns unchanged when config_setup is manual", {
  tmp_file <- create_temp_csv_file()
  on.exit(unlink(tmp_file))

  user_input <- list(
    "data.source_path" = tmp_file,
    "data.config_setup" = "manual"
  )
  options_def <- create_mock_options_def()

  withr::local_options(list("artma.verbose" = 1))
  result <- preprocess_column_mapping(user_input, options_def)

  # Should not add column records when manual mode
  expect_false("data.columns" %in% names(result))
})


test_that("preprocess_column_mapping returns unchanged when the column store is already specified", {
  tmp_file <- create_temp_csv_file()
  on.exit(unlink(tmp_file))

  provided_columns <- list(
    study_id = list(source_name = "study_name"),
    effect = list(source_name = "effect"),
    se = list(source_name = "se"),
    n_obs = list(source_name = "n_obs")
  )
  user_input <- list(
    "data.source_path" = tmp_file,
    "data.columns" = provided_columns
  )
  options_def <- create_mock_options_def()

  withr::local_options(list("artma.verbose" = 1))
  result <- preprocess_column_mapping(user_input, options_def)

  # Should keep the provided store unchanged
  expect_equal(result$data.columns, provided_columns)
})


test_that("preprocess_column_mapping adds auto-detected columns when missing", {
  tmp_file <- create_temp_csv_file()
  on.exit(unlink(tmp_file))

  user_input <- list(
    "data.source_path" = tmp_file,
    "data.config_setup" = "auto"
  )
  options_def <- create_mock_options_def()

  withr::local_options(list("artma.verbose" = 3))
  result <- preprocess_column_mapping(user_input, options_def)

  # Should have added auto-detected role records to the unified store.
  # Only genuine renames are stored; columns already carrying the standard
  # name (effect, se, n_obs) need no record.
  records <- result$data.columns
  expect_true(is.list(records))
  expect_equal(records$study_id$source_name, "study_name")
  expect_false("effect" %in% names(records))
  expect_false("se" %in% names(records))
  expect_false("n_obs" %in% names(records))
})


test_that("preprocess_column_mapping does not override a user-supplied column store", {
  tmp_file <- create_temp_csv_file()
  on.exit(unlink(tmp_file))

  user_input <- list(
    "data.source_path" = tmp_file,
    "data.columns" = list(
      study_id = list(source_name = "my_custom_study_col")
    )
  )
  options_def <- create_mock_options_def()

  withr::local_options(list("artma.verbose" = 1))
  result <- preprocess_column_mapping(user_input, options_def)

  # Should preserve the user's store as-is
  expect_equal(
    result$data.columns,
    list(study_id = list(source_name = "my_custom_study_col"))
  )
})


test_that("preprocess_column_mapping handles errors gracefully", {
  # Create a malformed CSV
  tmp_file <- tempfile(fileext = ".csv")
  writeLines("not,a,valid,csv\n{{{{malformed", tmp_file)
  on.exit(unlink(tmp_file))

  user_input <- list(
    "data.source_path" = tmp_file
  )
  options_def <- create_mock_options_def()

  withr::local_options(list("artma.verbose" = 1))

  # Should not error, just return original input
  result <- preprocess_column_mapping(user_input, options_def)

  # Should not have added column records due to error
  expect_false("data.columns" %in% names(result))
})


test_that("preprocess_column_mapping works with comma-delimited files", {
  csv_content <- c(
    "study_name,effect,se,n_obs",
    "Study A,10.5,2.3,100",
    "Study B,8.2,1.8,150"
  )

  tmp_file <- tempfile(fileext = ".csv")
  writeLines(csv_content, tmp_file)
  on.exit(unlink(tmp_file))

  user_input <- list(
    "data.source_path" = tmp_file
  )
  options_def <- create_mock_options_def()

  withr::local_options(list("artma.verbose" = 1))
  result <- preprocess_column_mapping(user_input, options_def)

  # Should detect columns regardless of delimiter
  expect_true("study_id" %in% names(result$data.columns))
  expect_equal(result$data.columns$study_id$source_name, "study_name")
})


test_that("preprocess_column_mapping detects columns in a Latin-1 encoded file", {
  # Regression: a citation-style study label carrying a raw 0xFC byte ("Müller"
  # in Windows-1252) made is_likely_study_key()'s character-class regexes error
  # out, the whole detection step was swallowed by the surrounding tryCatch,
  # and the options file was written with no mapping at all. Encoding is now
  # repaired at the read boundary, so detection runs normally.
  tmp_file <- tempfile(fileext = ".csv")
  con <- file(tmp_file, open = "wb")
  writeBin(charToRaw(paste0(paste(c(
    "study_name,effect,standard_error,sample_size",
    "M\xfcller et al. (2011),0.5,0.1,100",
    "Smith (2015),0.7,0.2,150",
    "Jones (2019),0.3,0.05,220"
  ), collapse = "\n"), "\n")), con)
  close(con)
  on.exit(unlink(tmp_file))

  user_input <- list("data.source_path" = tmp_file)
  options_def <- create_mock_options_def()

  withr::local_options(list("artma.verbose" = 1))
  result <- preprocess_column_mapping(user_input, options_def)

  expect_equal(result$data.columns$study_id$source_name, "study_name")
  expect_equal(result$data.columns$se$source_name, "standard_error")
  expect_equal(result$data.columns$n_obs$source_name, "sample_size")
})


test_that("preprocess_column_mapping does not guess a mapping for an undetectable required column", {
  # n_obs is required but absent from the data. In a non-interactive session
  # (such as this test run) the flow must leave it unmapped rather than fall
  # back to climenu's first menu entry, which used to record
  # n_obs: source_name: effect.
  csv_content <- c(
    "effect,se,study_id",
    "0.5,0.1,Study A",
    "0.7,0.2,Study B"
  )
  tmp_file <- tempfile(fileext = ".csv")
  writeLines(csv_content, tmp_file)
  on.exit(unlink(tmp_file))

  user_input <- list("data.source_path" = tmp_file)
  options_def <- create_mock_options_def()

  withr::local_options(list("artma.verbose" = 1))
  result <- preprocess_column_mapping(user_input, options_def)

  expect_false("n_obs" %in% names(result$data.columns))
})


test_that("non-interactive options_create leaves an undetectable required column unmapped", {
  box::use(
    artma / options / files[options_file_path, read_options_file]
  )

  csv_content <- c(
    "effect,se,study_id",
    "0.5,0.1,Study A",
    "0.7,0.2,Study B"
  )
  tmp_file <- tempfile(fileext = ".csv")
  writeLines(csv_content, tmp_file)
  on.exit(unlink(tmp_file))

  tmp_dir <- withr::local_tempdir()
  file_name <- "missing-nobs.yaml"

  withr::local_options(list("artma.verbose" = 1))

  artma::options_create(
    options_file_name = file_name,
    options_dir = tmp_dir,
    user_input = list("data.source_path" = tmp_file),
    should_validate = FALSE,
    should_overwrite = TRUE
  )

  written <- read_options_file(options_file_path(tmp_dir, file_name))
  columns <- written$data$columns

  # With nothing to record, the columns node stays empty (read back as NA).
  # The menu-default fallback used to record n_obs: source_name: effect here,
  # which made the next run rename effect to n_obs and abort.
  expect_false(is.list(columns) && "n_obs" %in% names(columns))
})


test_that("preprocess_column_mapping respects verbosity settings", {
  tmp_file <- create_temp_csv_file()
  on.exit(unlink(tmp_file))

  user_input <- list(
    "data.source_path" = tmp_file
  )
  options_def <- create_mock_options_def()

  # Test with verbose = 1 (should not output much)
  withr::local_options(list("artma.verbose" = 1))
  result1 <- preprocess_column_mapping(user_input, options_def)

  # Test with verbose = 3 (should output info messages)
  withr::local_options(list("artma.verbose" = 3))
  result2 <- preprocess_column_mapping(user_input, options_def)

  # Both should produce the same result
  expect_equal(result1, result2)
  expect_true("study_id" %in% names(result1$data.columns))
})

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.