tests/testthat/test-codec_xpt.R

# Tests for the xpt codec (codec_xpt.R): v5/v8 framing, the meta bridge,
# special missings, temporal + encoding round-trips, and the F1/F2/C4
# invariants. Internals via artoo:::; a frozen `created` keeps bytes stable.

demo_adam_spec <- function() {
  artoo_spec(
    cdisc_adam_datasets,
    cdisc_adam_variables,
    codelists = cdisc_codelists
  )
}

demo_sdtm_spec <- function() {
  artoo_spec(
    cdisc_sdtm_datasets,
    cdisc_sdtm_variables,
    codelists = cdisc_codelists
  )
}

frozen <- as.POSIXct("2020-01-01", tz = "UTC")

# Compare column VALUES (artoo keeps labels in meta, not on columns; the
# sas_missing tag is a separate concern). Class is preserved by as.character.
# Honors C4: XPORT stores "" as a blank, which reads back as NA.
expect_values_equal <- function(back, orig) {
  for (nm in names(orig)) {
    o <- as.character(orig[[nm]])
    o[!is.na(o) & o == ""] <- NA_character_
    expect_identical(as.character(back[[nm]]), o, info = nm)
  }
}

# ---- v5 / v8 round-trip on bundled CDISC data ------------------------------

test_that("v5 round-trips DM values and per-column metadata", {
  spec <- demo_sdtm_spec()
  dm <- apply_spec(cdisc_dm, spec, "DM", conformance = "off")
  p <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(dm, p, created = frozen)
  back <- read_xpt(p)

  expect_identical(names(back), names(dm))
  expect_values_equal(back, dm)
  m <- get_meta(back)
  expect_identical(m@dataset$name, "DM")
  expect_identical(m@dataset$records, nrow(dm))
  expect_identical(m@columns$STUDYID$label, "Study Identifier")
})

test_that("v5 round-trips ADSL including Date columns", {
  spec <- demo_adam_spec()
  adsl <- apply_spec(cdisc_adsl, spec, "ADSL", conformance = "off")
  p <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(adsl, p, created = frozen)
  back <- read_xpt(p)

  expect_values_equal(back, adsl)
  dcol <- names(adsl)[vapply(adsl, inherits, logical(1), "Date")][1]
  expect_s3_class(back[[dcol]], "Date")
  expect_equal(as.numeric(back[[dcol]]), as.numeric(adsl[[dcol]]))
})

test_that("v8 round-trips DM (extended member format)", {
  spec <- demo_sdtm_spec()
  dm <- apply_spec(cdisc_dm, spec, "DM", conformance = "off")
  p <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(dm, p, version = 8, created = frozen)
  back <- read_xpt(p)
  expect_identical(names(back), names(dm))
  expect_values_equal(back, dm)
})

# ---- byte stability ---------------------------------------------------------

test_that("two writes with a frozen timestamp are byte-identical", {
  spec <- demo_sdtm_spec()
  dm <- apply_spec(cdisc_dm, spec, "DM", conformance = "off")
  p1 <- withr::local_tempfile(fileext = ".xpt")
  p2 <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(dm, p1, created = frozen)
  write_xpt(dm, p2, created = frozen)
  expect_identical(
    readBin(p1, "raw", n = file.info(p1)$size),
    readBin(p2, "raw", n = file.info(p2)$size)
  )
})

# ---- special missing values -------------------------------------------------

test_that("special missing tags survive a numeric round-trip (.A-.Z, ._)", {
  df <- data.frame(
    SUBJ = c("A", "B", "C", "D", "E"),
    VAL = c(1, NA, NA, NA, NA),
    stringsAsFactors = FALSE
  )
  attr(df$VAL, "sas_missing") <- c(NA, ".", "._", ".A", ".Z")
  attr(df, "dataset_name") <- "TEST"
  p <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(df, p, created = frozen)
  back <- read_xpt(p)
  expect_identical(attr(back$VAL, "sas_missing"), c(NA, ".", "._", ".A", ".Z"))
  expect_identical(back$VAL[1], 1)
})

# ---- temporal ---------------------------------------------------------------

test_that("Date, POSIXct, and hms columns round-trip", {
  df <- data.frame(SUBJ = c("A", "B", "C"), stringsAsFactors = FALSE)
  df$DT <- as.Date(c("2020-01-01", NA, "2021-06-15"))
  df$DTM <- as.POSIXct(
    c("2020-01-01 08:30:00", "2020-12-31 23:59:59", NA),
    tz = "UTC"
  )
  df$TM <- hms::hms(c(0, 30600, 90061)) # incl. > 86400 (elapsed past 24h)
  attr(df, "dataset_name") <- "TEST"
  p <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(df, p, created = frozen)
  back <- read_xpt(p)

  expect_s3_class(back$DT, "Date")
  # Values identical; like every numeric column, a missing day reads back
  # with a "." sas_missing tag (consistent with the special-missings test).
  expect_identical(as.numeric(back$DT), as.numeric(df$DT))
  expect_s3_class(back$DTM, "POSIXct")
  expect_equal(as.numeric(back$DTM), as.numeric(df$DTM))
  expect_s3_class(back$TM, "hms")
  # set_meta() projects the SAS format/label as attrs now; compare seconds.
  expect_identical(as.numeric(back$TM), as.numeric(df$TM))
})

test_that("a non-temporal numeric format is NOT realized to a date", {
  df <- data.frame(SUBJ = "A", N = 12.5, stringsAsFactors = FALSE)
  # Give N an explicit numeric format via a spec-less meta path: float stays
  # float; no date realization.
  attr(df, "dataset_name") <- "TEST"
  p <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(df, p, created = frozen)
  back <- read_xpt(p)
  expect_type(back$N, "double")
  expect_false(inherits(back$N, "Date"))
})

# ---- decimal / boolean dispatch (F3 / context #3) --------------------------

test_that("a decimal column (R character) writes as a SAS numeric", {
  # decimal storage is R character; vartype must come from dataType, not
  # is.character(). Build meta with a decimal column.
  df <- data.frame(
    SUBJ = c("A", "B"),
    AVAL = c("1.50", "100.000"),
    stringsAsFactors = FALSE
  )
  cols <- list(
    SUBJ = list(
      itemOID = "IT.T.SUBJ",
      name = "SUBJ",
      dataType = "string",
      length = 1L
    ),
    AVAL = list(
      itemOID = "IT.T.AVAL",
      name = "AVAL",
      dataType = "decimal",
      displayFormat = "8.3"
    )
  )
  ds <- artoo:::.assemble_dataset_meta(itemGroupOID = "IG.T", name = "T")
  meta <- artoo:::artoo_meta_class(dataset = ds, columns = cols)
  df <- set_meta(df, meta)
  p <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(df, p, created = frozen)
  back <- read_xpt(p)
  # AVAL comes back as a SAS numeric (double), values intact.
  expect_type(back$AVAL, "double")
  expect_equal(back$AVAL, c(1.5, 100), ignore_attr = c("label", "format.sas"))
})

# ---- encoding ---------------------------------------------------------------

test_that("a windows-1252 value round-trips byte-for-byte via recorded encoding", {
  df <- data.frame(SUBJ = "A", TXT = "café ™", stringsAsFactors = FALSE)
  attr(df, "dataset_name") <- "TEST"
  p <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(df, p, encoding = "windows-1252", created = frozen)
  back <- read_xpt(p)
  expect_identical(back$TXT, "café ™")
  expect_identical(get_meta(back)@dataset$encoding, "WINDOWS-1252")
})

test_that("encoding = US-ASCII on a non-ASCII value aborts loudly", {
  df <- data.frame(SUBJ = "A", TXT = "café", stringsAsFactors = FALSE)
  attr(df, "dataset_name") <- "TEST"
  p <- withr::local_tempfile(fileext = ".xpt")
  expect_error(
    write_xpt(df, p, encoding = "US-ASCII"),
    class = "artoo_error_codec"
  )
})

# ---- F1: never silently truncate character data ----------------------------

test_that("F1: a char value longer than its declared length widens, not truncates", {
  df <- data.frame(SUBJ = "A", LONGTXT = "ABCDEFGHIJ", stringsAsFactors = FALSE)
  cols <- list(
    SUBJ = list(
      itemOID = "IT.T.SUBJ",
      name = "SUBJ",
      dataType = "string",
      length = 1L
    ),
    LONGTXT = list(
      itemOID = "IT.T.LONGTXT",
      name = "LONGTXT",
      dataType = "string",
      length = 3L
    )
  )
  ds <- artoo:::.assemble_dataset_meta(itemGroupOID = "IG.T", name = "T")
  meta <- artoo:::artoo_meta_class(dataset = ds, columns = cols)
  df <- set_meta(df, meta)
  p <- withr::local_tempfile(fileext = ".xpt")
  # Widening past the declared length is the point of this test; the writer
  # now also announces it.
  expect_warning(
    write_xpt(df, p, created = frozen),
    class = "artoo_warning_encoding"
  )
  back <- read_xpt(p)
  expect_identical(back$LONGTXT, "ABCDEFGHIJ") # full 10 chars, not truncated to 3
})

# ---- F2: numeric precision (declared short length ignored) ------------------

test_that("F2: a spec-declared numeric length < 8 still writes full precision", {
  df <- data.frame(SUBJ = "A", PI = pi, stringsAsFactors = FALSE)
  cols <- list(
    SUBJ = list(
      itemOID = "IT.T.SUBJ",
      name = "SUBJ",
      dataType = "string",
      length = 1L
    ),
    PI = list(itemOID = "IT.T.PI", name = "PI", dataType = "float", length = 3L)
  )
  ds <- artoo:::.assemble_dataset_meta(itemGroupOID = "IG.T", name = "T")
  meta <- artoo:::artoo_meta_class(dataset = ds, columns = cols)
  df <- set_meta(df, meta)
  p <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(df, p, created = frozen)
  back <- read_xpt(p)
  expect_equal(back$PI, pi) # full double precision, not a 3-byte truncation
})

# ---- v8 long name / long label override ------------------------------------

test_that("v8 round-trips a name > 8 chars and a label > 40 chars", {
  df <- data.frame(SUBJ = "A", VERYLONGVARNAME = 1, stringsAsFactors = FALSE)
  long_label <- strrep("X", 60L)
  cols <- list(
    SUBJ = list(
      itemOID = "IT.T.SUBJ",
      name = "SUBJ",
      dataType = "string",
      length = 1L
    ),
    VERYLONGVARNAME = list(
      itemOID = "IT.T.V",
      name = "VERYLONGVARNAME",
      dataType = "float",
      label = long_label
    )
  )
  ds <- artoo:::.assemble_dataset_meta(itemGroupOID = "IG.T", name = "T")
  meta <- artoo:::artoo_meta_class(dataset = ds, columns = cols)
  df <- set_meta(df, meta)
  p <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(df, p, version = 8, created = frozen)
  back <- read_xpt(p)
  expect_true("VERYLONGVARNAME" %in% names(back))
  expect_identical(get_meta(back)@columns$VERYLONGVARNAME$label, long_label)
})

# ---- v5 nobs padding trap (all-character) ----------------------------------

test_that("v5 reads the exact row count for an all-character short-record frame", {
  # obs_length 6 (two 3-byte char cols) < 80; padding spans > 1 record.
  df <- data.frame(
    A = c("aaa", "bbb", "ccc", "ddd", "eee"),
    B = c("111", "222", "333", "444", "555"),
    stringsAsFactors = FALSE
  )
  attr(df, "dataset_name") <- "TEST"
  p <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(df, p, created = frozen)
  back <- read_xpt(p)
  expect_identical(nrow(back), 5L)
  expect_identical(back$A, df$A)
})

# ---- edge cases -------------------------------------------------------------

test_that("0-row and all-NA character frames round-trip", {
  df <- data.frame(
    SUBJ = character(0),
    N = numeric(0),
    stringsAsFactors = FALSE
  )
  attr(df, "dataset_name") <- "TEST"
  p <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(df, p, created = frozen)
  back <- read_xpt(p)
  expect_identical(nrow(back), 0L)
  expect_identical(names(back), c("SUBJ", "N"))

  # all-NA char column (width floored at 1) + genuine "" -> NA (C4).
  df2 <- data.frame(
    SUBJ = c("A", "B"),
    EMP = c(NA_character_, ""),
    stringsAsFactors = FALSE
  )
  attr(df2, "dataset_name") <- "TEST"
  p2 <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(df2, p2, created = frozen)
  back2 <- read_xpt(p2)
  expect_true(all(is.na(back2$EMP))) # "" is indistinguishable from NA in XPORT
})

test_that("-0.0 round-trips as 0", {
  df <- data.frame(SUBJ = "A", Z = -0.0, stringsAsFactors = FALSE)
  attr(df, "dataset_name") <- "TEST"
  p <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(df, p, created = frozen)
  expect_identical(read_xpt(p)$Z, 0)
})

# ---- error matrix -----------------------------------------------------------

test_that("a factor column aborts with artoo_error_type", {
  df <- data.frame(SUBJ = "A", F = factor("x"))
  attr(df, "dataset_name") <- "TEST"
  p <- withr::local_tempfile(fileext = ".xpt")
  expect_snapshot(write_xpt(df, p), error = TRUE)
  expect_error(write_xpt(df, p), class = "artoo_error_type")
})

test_that("a list column aborts with artoo_error_type", {
  df <- data.frame(SUBJ = "A")
  df$L <- list(1:3)
  attr(df, "dataset_name") <- "TEST"
  p <- withr::local_tempfile(fileext = ".xpt")
  expect_error(write_xpt(df, p), class = "artoo_error_type")
})

test_that("an invalid v5 variable name aborts", {
  df <- data.frame(SUBJ = "A", `TOOLONGNAME` = 1, check.names = FALSE)
  attr(df, "dataset_name") <- "TEST"
  p <- withr::local_tempfile(fileext = ".xpt")
  expect_error(write_xpt(df, p), class = "artoo_error_codec")
})

test_that("a numeric value over the IBM-370 limit aborts", {
  df <- data.frame(SUBJ = "A", BIG = 1e76)
  attr(df, "dataset_name") <- "TEST"
  p <- withr::local_tempfile(fileext = ".xpt")
  expect_error(write_xpt(df, p), class = "artoo_error_codec")
})

test_that("a v5 char column over 200 bytes aborts", {
  df <- data.frame(
    SUBJ = "A",
    WIDE = strrep("x", 201L),
    stringsAsFactors = FALSE
  )
  attr(df, "dataset_name") <- "TEST"
  p <- withr::local_tempfile(fileext = ".xpt")
  expect_error(write_xpt(df, p), class = "artoo_error_codec")
})

test_that("a truncated xpt file aborts on read", {
  spec <- demo_sdtm_spec()
  dm <- apply_spec(cdisc_dm, spec, "DM", conformance = "off")
  p <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(dm, p, created = frozen)
  raw <- readBin(p, "raw", n = file.info(p)$size)
  writeBin(raw[1:120], p) # chop mid-header
  expect_error(read_xpt(p), class = "artoo_error_codec")
})

# ---- coverage: defensive branches ------------------------------------------

test_that("an invalid version aborts", {
  df <- data.frame(SUBJ = "A", stringsAsFactors = FALSE)
  attr(df, "dataset_name") <- "T"
  p <- withr::local_tempfile(fileext = ".xpt")
  expect_error(write_xpt(df, p, version = 99), class = "artoo_error_input")
})

test_that("a 0-column frame writes and reads a valid empty member", {
  p <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(data.frame(), p, created = frozen)
  back <- read_xpt(p)
  expect_identical(ncol(back), 0L)
})

test_that("a v5 label over 40 chars truncates with a warning", {
  df <- data.frame(SUBJ = "A", X = 1, stringsAsFactors = FALSE)
  cols <- list(
    SUBJ = list(
      itemOID = "IT.T.SUBJ",
      name = "SUBJ",
      dataType = "string",
      length = 1L
    ),
    X = list(
      itemOID = "IT.T.X",
      name = "X",
      dataType = "float",
      label = strrep("L", 50L)
    )
  )
  ds <- artoo:::.assemble_dataset_meta(itemGroupOID = "IG.T", name = "T")
  meta <- artoo:::artoo_meta_class(dataset = ds, columns = cols)
  df <- set_meta(df, meta)
  p <- withr::local_tempfile(fileext = ".xpt")
  expect_warning(
    write_xpt(df, p, created = frozen),
    class = "artoo_warning_encoding"
  )
})

test_that("a non-numeric decimal value aborts on write", {
  df <- data.frame(SUBJ = "A", AVAL = "not-a-number", stringsAsFactors = FALSE)
  cols <- list(
    SUBJ = list(
      itemOID = "IT.T.SUBJ",
      name = "SUBJ",
      dataType = "string",
      length = 1L
    ),
    AVAL = list(itemOID = "IT.T.AVAL", name = "AVAL", dataType = "decimal")
  )
  ds <- artoo:::.assemble_dataset_meta(itemGroupOID = "IG.T", name = "T")
  meta <- artoo:::artoo_meta_class(dataset = ds, columns = cols)
  df <- set_meta(df, meta)
  p <- withr::local_tempfile(fileext = ".xpt")
  expect_error(write_xpt(df, p), class = "artoo_error_codec")
})

test_that("a column absent from the meta is inferred from the frame", {
  df <- data.frame(SUBJ = "A", EXTRA = 42, stringsAsFactors = FALSE)
  cols <- list(
    SUBJ = list(
      itemOID = "IT.T.SUBJ",
      name = "SUBJ",
      dataType = "string",
      length = 1L
    )
  )
  ds <- artoo:::.assemble_dataset_meta(itemGroupOID = "IG.T", name = "T")
  meta <- artoo:::artoo_meta_class(dataset = ds, columns = cols)
  df <- set_meta(df, meta)
  p <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(df, p, created = frozen)
  back <- read_xpt(p)
  expect_equal(back$EXTRA, 42)
})

test_that("read_xpt honours an explicit encoding override", {
  df <- data.frame(SUBJ = "A", TXT = "café", stringsAsFactors = FALSE)
  attr(df, "dataset_name") <- "T"
  p <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(df, p, encoding = "windows-1252", created = frozen)
  back <- read_xpt(p, encoding = "windows-1252")
  expect_identical(back$TXT, "café")
})

# ---- coverage: SAS-compat read paths (crafted fixtures) --------------------

test_that("the reader zero-pads numerics narrower than 8 bytes (real SAS)", {
  # Real SAS XPORT stores numerics at 2-8 bytes (high-order IBM bytes only).
  # Craft a v5 file with a single 5-byte numeric field holding 1.0.
  recs <- list(list(
    name = "N",
    label = "",
    vartype = 1L,
    length = 5L,
    format_name = "",
    formatl = 0L,
    formatd = 0L,
    bytes = raw(0)
  ))
  ibm5 <- artoo:::.ieee_to_ibm(1.0)[1:5]
  bytes <- c(
    artoo:::.xpt_library_header(5L, frozen),
    artoo:::.xpt_member_header("T", "", 1L, 5L, frozen),
    artoo:::.xpt_namestr_block(recs, 5L),
    artoo:::.xpt_obs_header(1L, 5L),
    artoo:::.pad_record(ibm5, 80L)
  )
  p <- withr::local_tempfile(fileext = ".xpt")
  writeBin(bytes, p)
  back <- read_xpt(p)
  expect_equal(back$N, 1.0)
})

test_that("the reader parses LABELV9 long-name/long-label extension records", {
  name <- "LONGVARNAME12345" # 16 > 8
  label <- strrep("Y", 50L) # > 40
  recs <- list(list(
    name = name,
    label = "",
    vartype = 2L,
    length = 3L,
    format_name = "",
    formatl = 0L,
    formatd = 0L,
    bytes = raw(0)
  ))
  ext_header <- artoo:::.str_to_raw(
    paste0(
      "HEADER RECORD*******",
      artoo:::.pad_to("LABELV9", 7L),
      " HEADER RECORD!!!!!!!",
      formatC(1L, width = 30L, format = "d", flag = " ")
    ),
    80L
  )
  chunk <- c(
    artoo:::.int_to_pib2(1L),
    artoo:::.int_to_pib2(nchar(name)),
    artoo:::.int_to_pib2(nchar(label)),
    artoo:::.int_to_pib2(0L),
    artoo:::.int_to_pib2(0L),
    charToRaw(name),
    charToRaw(label)
  )
  bytes <- c(
    artoo:::.xpt_library_header(8L, frozen),
    artoo:::.xpt_member_header(name, "", 1L, 8L, frozen),
    artoo:::.xpt_namestr_block(recs, 8L),
    ext_header,
    artoo:::.pad_record(chunk, 80L),
    artoo:::.xpt_obs_header(1L, 8L),
    artoo:::.pad_record(charToRaw("abc"), 80L)
  )
  p <- withr::local_tempfile(fileext = ".xpt")
  writeBin(bytes, p)
  back <- read_xpt(p)
  expect_true(name %in% names(back))
  expect_identical(get_meta(back)@columns[[name]]$label, label)
})

test_that("a non-meta column with a label attr and a meta string without length", {
  df <- data.frame(SUBJ = "A", EXTRA = "hello", stringsAsFactors = FALSE)
  attr(df$EXTRA, "label") <- "An extra column"
  # meta describes SUBJ as a string WITHOUT a declared length.
  cols <- list(
    SUBJ = list(itemOID = "IT.T.SUBJ", name = "SUBJ", dataType = "string")
  )
  ds <- artoo:::.assemble_dataset_meta(itemGroupOID = "IG.T", name = "T")
  meta <- artoo:::artoo_meta_class(dataset = ds, columns = cols)
  df <- set_meta(df, meta)
  p <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(df, p, created = frozen)
  back <- read_xpt(p)
  expect_identical(as.vector(back$EXTRA), "hello")
  # set_meta() projects the label onto the column attr (haven parity).
  expect_identical(attr(back$EXTRA, "label", exact = TRUE), "An extra column")
  expect_identical(get_meta(back)@columns$EXTRA$label, "An extra column")
})

# ---- review findings: regression tests -------------------------------------

test_that("a blank string in a decimal column writes as missing, not an abort", {
  # Review HIGH: "" in a character-backed numeric is an intended missing.
  df <- data.frame(
    SUBJ = c("A", "B", "C"),
    AVAL = c("1.5", "", "3.0"),
    stringsAsFactors = FALSE
  )
  cols <- list(
    SUBJ = list(
      itemOID = "IT.T.SUBJ",
      name = "SUBJ",
      dataType = "string",
      length = 1L
    ),
    AVAL = list(
      itemOID = "IT.T.AVAL",
      name = "AVAL",
      dataType = "decimal",
      displayFormat = "8.1"
    )
  )
  ds <- artoo:::.assemble_dataset_meta(itemGroupOID = "IG.T", name = "T")
  meta <- artoo:::artoo_meta_class(dataset = ds, columns = cols)
  df <- set_meta(df, meta)
  p <- withr::local_tempfile(fileext = ".xpt")
  expect_no_error(write_xpt(df, p, created = frozen))
  back <- read_xpt(p)
  expect_equal(back$AVAL[c(1, 3)], c(1.5, 3.0))
  expect_true(is.na(back$AVAL[2]))
})

test_that("an infinite numeric value aborts loudly (no silent missing)", {
  # Review LOW: Inf cannot be stored; fail loud like finite overflow.
  df <- data.frame(SUBJ = "A", X = Inf)
  attr(df, "dataset_name") <- "T"
  p <- withr::local_tempfile(fileext = ".xpt")
  expect_error(write_xpt(df, p), class = "artoo_error_codec")
})

# ---- review 2026-06: header/label text through the encoding SSOT ------------

test_that("a non-ASCII dataset label keeps 80-byte framing and round-trips", {
  # Review BLOCKER: the member-header label was char-padded, not byte-padded,
  # so a multibyte label misaligned every following record.
  df <- data.frame(SUBJ = "A", N = 1, stringsAsFactors = FALSE)
  ds <- artoo:::.assemble_dataset_meta(
    itemGroupOID = "IG.T",
    name = "T",
    label = "Étude de démographie"
  )
  meta <- artoo:::artoo_meta_class(dataset = ds, columns = list())
  df <- set_meta(df, meta)
  p <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(df, p, created = frozen)
  expect_identical(file.info(p)$size %% 80, 0)
  back <- read_xpt(p)
  expect_identical(
    get_meta(back)@dataset$label,
    "Étude de démographie"
  )
})

test_that("read_xpt survives windows-1252 variable labels (incl. its own output)", {
  # Review BLOCKER: .raw_to_str regexed header bytes without useBytes, so any
  # non-UTF-8 label byte crashed the reader with a raw base error.
  df <- data.frame(SUBJ = "A", PAYS = "FR", stringsAsFactors = FALSE)
  cols <- list(
    PAYS = list(
      itemOID = "IT.T.PAYS",
      name = "PAYS",
      dataType = "string",
      label = "Pays de résidence"
    )
  )
  ds <- artoo:::.assemble_dataset_meta(itemGroupOID = "IG.T", name = "T")
  meta <- artoo:::artoo_meta_class(dataset = ds, columns = cols)
  df <- set_meta(df, meta)
  p <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(df, p, encoding = "windows-1252", created = frozen)
  back <- read_xpt(p)
  expect_identical(
    get_meta(back)@columns$PAYS$label,
    "Pays de résidence"
  )
})

test_that("v5 label truncation at 40 bytes backs off to a character boundary", {
  # Review BLOCKER companion: a byte-40 chop split a multibyte character,
  # leaving invalid UTF-8 on disk and an unreadable file.
  df <- data.frame(SUBJ = "A", X = 1, stringsAsFactors = FALSE)
  cols <- list(
    X = list(
      itemOID = "IT.T.X",
      name = "X",
      dataType = "float",
      label = paste0(strrep("a", 39L), "étude") # byte 40 = lead byte of e-acute
    )
  )
  ds <- artoo:::.assemble_dataset_meta(itemGroupOID = "IG.T", name = "T")
  meta <- artoo:::artoo_meta_class(dataset = ds, columns = cols)
  df <- set_meta(df, meta)
  p <- withr::local_tempfile(fileext = ".xpt")
  expect_warning(
    write_xpt(df, p, created = frozen),
    class = "artoo_warning_encoding"
  )
  back <- read_xpt(p)
  expect_identical(get_meta(back)@columns$X$label, strrep("a", 39L))
})

test_that("the header timestamp is locale-independent", {
  # Review BLOCKER: format(%b) used LC_TIME, so a French locale wrote
  # "JANV." (18-char datetime) and shifted every header field after it.
  probe <- tryCatch(
    withr::with_locale(c(LC_TIME = "fr_FR.UTF-8"), TRUE),
    condition = function(c) FALSE
  )
  skip_if_not(probe, "fr_FR.UTF-8 locale not available")
  df <- data.frame(SUBJ = "A", N = 1, stringsAsFactors = FALSE)
  attr(df, "dataset_name") <- "T"
  p_c <- withr::local_tempfile(fileext = ".xpt")
  p_fr <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(df, p_c, created = frozen)
  withr::with_locale(
    c(LC_TIME = "fr_FR.UTF-8"),
    write_xpt(df, p_fr, created = frozen)
  )
  expect_identical(
    readBin(p_c, "raw", n = file.info(p_c)$size),
    readBin(p_fr, "raw", n = file.info(p_fr)$size)
  )
})

test_that("the v8 namestr label-length field counts bytes, not characters", {
  # Review: a multibyte label wrote nchar() (chars) into the 2-byte length
  # field, so third-party readers trusting it misread the label.
  label <- "café™ étude" # 11 chars, 15 UTF-8 bytes
  df <- data.frame(SUBJ = "A", stringsAsFactors = FALSE)
  cols <- list(
    SUBJ = list(
      itemOID = "IT.T.SUBJ",
      name = "SUBJ",
      dataType = "string",
      length = 1L,
      label = label
    )
  )
  ds <- artoo:::.assemble_dataset_meta(itemGroupOID = "IG.T", name = "T")
  meta <- artoo:::artoo_meta_class(dataset = ds, columns = cols)
  df <- set_meta(df, meta)
  p <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(df, p, version = 8, created = frozen)
  bytes <- readBin(p, "raw", n = file.info(p)$size)
  # 8 header records (3 library + 5 member) = 640 bytes; the v8 namestr holds
  # the label-length field at offset 120 (after the 88-byte base + 32-byte
  # long name).
  label_len_field <- artoo:::.pib2_to_int(bytes[(640L + 121L):(640L + 122L)])
  expect_identical(label_len_field, length(charToRaw(label)))
})

test_that("a non-ASCII dataset name aborts instead of corrupting the header", {
  df <- data.frame(SUBJ = "A", stringsAsFactors = FALSE)
  ds <- artoo:::.assemble_dataset_meta(itemGroupOID = "IG.T", name = "ÉTUDE")
  meta <- artoo:::artoo_meta_class(dataset = ds, columns = list())
  df <- set_meta(df, meta)
  p <- withr::local_tempfile(fileext = ".xpt")
  expect_error(write_xpt(df, p), class = "artoo_error_codec")
})

test_that("a dataset label over 40 bytes truncates with a warning", {
  df <- data.frame(SUBJ = "A", stringsAsFactors = FALSE)
  ds <- artoo:::.assemble_dataset_meta(
    itemGroupOID = "IG.T",
    name = "T",
    label = strrep("L", 50L)
  )
  meta <- artoo:::artoo_meta_class(dataset = ds, columns = list())
  df <- set_meta(df, meta)
  p <- withr::local_tempfile(fileext = ".xpt")
  expect_warning(
    write_xpt(df, p, created = frozen),
    class = "artoo_warning_encoding"
  )
  back <- read_xpt(p)
  expect_identical(get_meta(back)@dataset$label, strrep("L", 40L))
})

test_that("encoding detection considers labels, not just data values", {
  # Data is pure ASCII but the label is windows-1252; detection must not
  # default to UTF-8 and mis-decode the label.
  df <- data.frame(SUBJ = "A", PAYS = "FR", stringsAsFactors = FALSE)
  cols <- list(
    PAYS = list(
      itemOID = "IT.T.PAYS",
      name = "PAYS",
      dataType = "string",
      label = "Résidence"
    )
  )
  ds <- artoo:::.assemble_dataset_meta(itemGroupOID = "IG.T", name = "T")
  meta <- artoo:::artoo_meta_class(dataset = ds, columns = cols)
  df <- set_meta(df, meta)
  p <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(df, p, encoding = "windows-1252", created = frozen)
  back <- read_xpt(p)
  expect_identical(get_meta(back)@columns$PAYS$label, "Résidence")
})

test_that("a character-backed date column writes as ISO text, no garbage days (review B7)", {
  # "2014" used to coerce via as.numeric() and silently write SAS day 2014
  # (= 1965-07-07). With no numeric targetDataType a character temporal is
  # the CDISC ISO-text form: it stores as a character variable and the
  # partial values survive verbatim. (With targetDataType = "integer" the
  # write still aborts loud — covered in test-temporal-iso.R.)
  df <- data.frame(
    SUBJ = c("A", "B"),
    ADT = c("2014", "2015"),
    stringsAsFactors = FALSE
  )
  cols <- list(
    ADT = list(
      itemOID = "IT.T.ADT",
      name = "ADT",
      dataType = "date",
      displayFormat = "DATE9."
    )
  )
  ds <- artoo:::.assemble_dataset_meta(itemGroupOID = "IG.T", name = "T")
  meta <- artoo:::artoo_meta_class(dataset = ds, columns = cols)
  df <- set_meta(df, meta)
  p <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(df, p)
  back <- read_xpt(p)
  expect_identical(back$ADT, c("2014", "2015"))
  # The temporal display format is dropped for a character variable.
  expect_null(get_meta(back)@columns$ADT$displayFormat)
})

test_that("special-missing tags survive a temporal round-trip (review codec C3)", {
  # .realize_temporal dropped the sas_missing attr on read, so a second write
  # degraded .A to plain missing.
  df <- data.frame(SUBJ = c("A", "B"), stringsAsFactors = FALSE)
  df$ADT <- as.Date(c("2020-01-01", NA))
  attr(df$ADT, "sas_missing") <- c(NA, ".A")
  attr(df, "dataset_name") <- "T"
  p <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(df, p, created = frozen)
  back <- read_xpt(p)
  expect_s3_class(back$ADT, "Date")
  expect_identical(attr(back$ADT, "sas_missing"), c(NA, ".A"))
})

# ---- Wave 3: partial reads (col_select / n_max) -----------------------------

test_that("read_xpt n_max caps the records read and the meta record count", {
  df <- data.frame(
    SUBJ = sprintf("S%02d", 1:10),
    N = as.numeric(1:10),
    stringsAsFactors = FALSE
  )
  attr(df, "dataset_name") <- "T"
  p <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(df, p, created = frozen)
  back <- read_xpt(p, n_max = 3)
  expect_identical(nrow(back), 3L)
  expect_identical(as.vector(back$SUBJ), c("S01", "S02", "S03"))
  expect_identical(get_meta(back)@dataset$records, 3L)
})

test_that("read_xpt n_max past the end returns every row", {
  df <- data.frame(SUBJ = c("A", "B"), stringsAsFactors = FALSE)
  attr(df, "dataset_name") <- "T"
  p <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(df, p, created = frozen)
  expect_identical(nrow(read_xpt(p, n_max = 99)), 2L)
})

test_that("read_xpt col_select returns only the named columns in file order", {
  df <- data.frame(SUBJ = "A", AGE = 42, SEX = "F", stringsAsFactors = FALSE)
  attr(df, "dataset_name") <- "T"
  p <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(df, p, created = frozen)
  back <- read_xpt(p, col_select = c("SEX", "SUBJ"))
  expect_identical(names(back), c("SUBJ", "SEX")) # file order, not request order
  expect_named(get_meta(back)@columns, c("SUBJ", "SEX"))
  expect_identical(as.vector(back$SEX), "F")
})

test_that("read_xpt col_select rejects an unknown column", {
  df <- data.frame(SUBJ = "A", AGE = 42, stringsAsFactors = FALSE)
  attr(df, "dataset_name") <- "T"
  p <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(df, p, created = frozen)
  expect_error(
    read_xpt(p, col_select = c("SUBJ", "NOPE")),
    class = "artoo_error_input"
  )
})

test_that("read_xpt col_select and n_max compose", {
  df <- data.frame(
    SUBJ = sprintf("S%02d", 1:6),
    N = as.numeric(1:6),
    stringsAsFactors = FALSE
  )
  attr(df, "dataset_name") <- "T"
  p <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(df, p, created = frozen)
  back <- read_xpt(p, col_select = "N", n_max = 2)
  expect_identical(names(back), "N")
  expect_identical(nrow(back), 2L)
  expect_identical(as.vector(back$N), c(1, 2))
})

test_that("n_max works generically on a non-xpt codec (rds)", {
  # Partial reads now apply to every format: rds caps rows after readRDS via
  # the generic filter (no native push-down possible for a monolithic blob).
  p <- withr::local_tempfile(fileext = ".rds")
  write_rds(cdisc_dm, p)
  back <- read_dataset(p, n_max = 2)
  expect_identical(nrow(back), 2L)
  expect_identical(get_meta(back)@dataset$records, 2L)
})

# ---- Wave 3: FDA byte gate (TCG bytes 160-191) ------------------------------

test_that("write_xpt warns on FDA-forbidden bytes 160-191 in a single-byte stream", {
  # The copyright sign is 0xA9 (169) in windows-1252, inside the prohibited
  # 160-191 range the FDA Study Data TCG bars from submission xpt.
  df <- data.frame(SUBJ = "A", TXT = "© 2020", stringsAsFactors = FALSE)
  attr(df, "dataset_name") <- "T"
  p <- withr::local_tempfile(fileext = ".xpt")
  expect_warning(
    write_xpt(df, p, encoding = "windows-1252", created = frozen),
    class = "artoo_warning_encoding"
  )
})

test_that("the FDA gate stays silent for ASCII and UTF-8 multibyte content", {
  # ASCII default: nothing in 160-191.
  df <- data.frame(SUBJ = "A", TXT = "plain", stringsAsFactors = FALSE)
  attr(df, "dataset_name") <- "T"
  p <- withr::local_tempfile(fileext = ".xpt")
  expect_no_warning(write_xpt(df, p, created = frozen))
  # UTF-8 "cafe-acute": the trailing 0xA9 is a multibyte continuation byte, not
  # an FDA-forbidden single-byte glyph — the gate must not false-fire.
  df2 <- data.frame(SUBJ = "A", TXT = "café", stringsAsFactors = FALSE)
  attr(df2, "dataset_name") <- "T"
  p2 <- withr::local_tempfile(fileext = ".xpt")
  expect_no_warning(write_xpt(df2, p2, encoding = "UTF-8", created = frozen))
})

# ---- Wave 3: write-side + structural correctness ----------------------------

test_that("write_xpt v5 aborts on a name collision that survives uppercasing", {
  df <- data.frame(age = 1, AGE = 2)
  attr(df, "dataset_name") <- "T"
  p <- withr::local_tempfile(fileext = ".xpt")
  expect_error(write_xpt(df, p, version = 5), class = "artoo_error_codec")
  # v8 keeps case, so the same frame writes fine.
  expect_no_error(write_xpt(df, p, version = 8))
})

test_that("read_xpt aborts on a multi-member transport file", {
  df <- data.frame(SUBJ = c("A", "B"), N = c(1, 2), stringsAsFactors = FALSE)
  attr(df, "dataset_name") <- "T"
  p <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(df, p, created = frozen)
  # Append a second member header at the (80-byte-aligned) EOF = padded_end.
  sig <- "HEADER RECORD*******MEMBER  HEADER RECORD!!!!!!!"
  rec <- charToRaw(sprintf("%-80s", sig))
  con <- file(p, "ab")
  writeBin(rec, con)
  close(con)
  expect_error(read_xpt(p), class = "artoo_error_codec")
})

test_that("extra blank padding past the obs section does NOT false-abort", {
  df <- data.frame(SUBJ = c("A", "B"), stringsAsFactors = FALSE)
  attr(df, "dataset_name") <- "T"
  p <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(df, p, created = frozen)
  con <- file(p, "ab")
  writeBin(rep(as.raw(0x20), 80L), con) # blank, not a HEADER signature
  close(con)
  expect_identical(nrow(read_xpt(p)), 2L)
})

test_that(".xpt_parse_namestr honors the namestr size for the v8 long name", {
  raw <- as.raw(rep(0L, 140L))
  raw[1:2] <- artoo:::.int_to_pib2(2L) # vartype 2 (char)
  raw[5:6] <- artoo:::.int_to_pib2(8L) # length 8
  raw[9:16] <- charToRaw("SHORT   ") # short name field
  raw[89:120] <- charToRaw(sprintf("%-32s", "LONGNAME")) # v8 extended name
  # 140-byte v8: reads the extended name.
  v140 <- artoo:::.xpt_parse_namestr(raw, 8L, 140L)
  expect_identical(v140$name, "LONGNAME")
  # 136-byte VMS v8: the extended-name slice does not exist; uses the short field.
  v136 <- artoo:::.xpt_parse_namestr(raw[1:136], 8L, 136L)
  expect_identical(v136$name, "SHORT")
})

test_that(".xpt_parse_member reads the NAMESTR size field (0136)", {
  base1 <- "HEADER RECORD*******MEMBER  HEADER RECORD!!!!!!!000000000000000001600000000140"
  substr(base1, 75L, 78L) <- "0136"
  rec1 <- charToRaw(sprintf("%-80s", base1))
  filler <- charToRaw(strrep(" ", 80L))
  nsrec <- charToRaw(sprintf("%-80s", paste0(strrep(" ", 52L), "000001")))
  con <- rawConnection(c(rec1, filler, filler, filler, nsrec), "rb")
  on.exit(close(con))
  mem <- artoo:::.xpt_parse_member(con, 5L)
  expect_identical(mem$namestr_size, 136L)
  expect_identical(mem$nvars, 1L)
})

test_that("write_xpt v5 warns when an all-character frame ends in a blank row", {
  df <- data.frame(
    A = c("x", ""),
    B = c("y", NA),
    stringsAsFactors = FALSE
  )
  attr(df, "dataset_name") <- "T"
  p <- withr::local_tempfile(fileext = ".xpt")
  expect_warning(
    write_xpt(df, p, version = 5, created = frozen),
    class = "artoo_warning_encoding"
  )
  # v8 records the count, so it is exempt.
  expect_no_warning(write_xpt(df, p, version = 8, created = frozen))
})

test_that("n_max bounds the v8 disk read (a truncated tail still reads the head)", {
  # Proof that n_max narrows the bytes read: truncate the obs tail so a full
  # read short-reads (artoo_error_codec), but read_xpt(n_max = k) succeeds
  # because it never reaches the missing bytes. The v8 header still says 10.
  df <- data.frame(
    SUBJ = sprintf("S%02d", 1:10),
    N = as.numeric(1:10),
    stringsAsFactors = FALSE
  )
  attr(df, "dataset_name") <- "T"
  p <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(df, p, version = 8, created = frozen)
  sz <- file.info(p)$size
  raw_all <- readBin(p, "raw", n = sz)
  writeBin(raw_all[seq_len(sz - 80L)], p) # drop the last 80 obs bytes
  expect_error(read_xpt(p), class = "artoo_error_codec")
  back <- read_xpt(p, n_max = 5)
  expect_identical(nrow(back), 5L)
  expect_identical(as.vector(back$SUBJ), sprintf("S%02d", 1:5))
})

test_that("read_xpt aborts on a multi-member v8 file (padded-end boundary)", {
  df <- data.frame(SUBJ = c("A", "B"), N = c(1, 2), stringsAsFactors = FALSE)
  attr(df, "dataset_name") <- "T"
  p <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(df, p, version = 8, created = frozen)
  sig <- "HEADER RECORD*******MEMBV8  HEADER RECORD!!!!!!!"
  con <- file(p, "ab")
  writeBin(charToRaw(sprintf("%-80s", sig)), con)
  close(con)
  expect_error(read_xpt(p), class = "artoo_error_codec")
})

test_that(".xpt_parse_member defaults the NAMESTR size on an invalid field", {
  base1 <- "HEADER RECORD*******MEMBER  HEADER RECORD!!!!!!!000000000000000001600000000140"
  substr(base1, 75L, 78L) <- "ZZZZ" # non-numeric -> default 140
  rec1 <- charToRaw(sprintf("%-80s", base1))
  filler <- charToRaw(strrep(" ", 80L))
  nsrec <- charToRaw(sprintf("%-80s", paste0(strrep(" ", 52L), "000001")))
  con <- rawConnection(c(rec1, filler, filler, filler, nsrec), "rb")
  on.exit(close(con))
  expect_identical(artoo:::.xpt_parse_member(con, 5L)$namestr_size, 140L)
})

# ---- informats (NAMESTR bytes 73-84) ----------------------------------------

test_that("an informat round-trips through xpt v5 and v8", {
  spec <- artoo_spec(
    data.frame(dataset = "DM", label = "Demographics"),
    data.frame(
      dataset = c("DM", "DM"),
      variable = c("USUBJID", "BRTHDT"),
      label = c("Subject", "Birth Date"),
      data_type = c("string", "date"),
      display_format = c(NA, "DATE9."),
      informat = c(NA, "YYMMDD10."),
      stringsAsFactors = FALSE
    )
  )
  df <- data.frame(
    USUBJID = c("01-001", "01-002"),
    BRTHDT = as.Date(c("1980-04-12", "1975-09-30")),
    stringsAsFactors = FALSE
  )
  dm <- apply_spec(df, spec, "DM", conformance = "off")
  for (v in c(5, 8)) {
    p <- withr::local_tempfile(fileext = ".xpt")
    write_xpt(dm, p, version = v)
    back <- read_xpt(p)
    expect_identical(
      get_meta(back)@columns$BRTHDT$informat,
      "YYMMDD10.",
      info = paste("version", v)
    )
    expect_null(get_meta(back)@columns$USUBJID$informat)
  }
})

test_that("the informat lands in NAMESTR bytes 73-84", {
  df <- data.frame(AGE = c(34, 41))
  attr(df$AGE, "informat.sas") <- "BEST8.2"
  attr(df, "dataset_name") <- "DM"
  p <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(df, p)
  bytes <- readBin(p, "raw", file.info(p)$size)
  # v5 layout: 3 library + 5 member header records, then the NAMESTR record.
  ns <- bytes[(80L * 8L + 1L):(80L * 8L + 140L)]
  expect_identical(rawToChar(ns[73:80]), "BEST    ")
  expect_identical(artoo:::.pib2_to_int(ns[81:82]), 8L)
  expect_identical(artoo:::.pib2_to_int(ns[83:84]), 2L)
})

test_that("a real SAS xpt with no informats still reads cleanly", {
  fixture <- test_path("fixtures", "sas-dm.xpt")
  skip_if_not(file.exists(fixture), "SAS fixture not present")
  dm <- read_xpt(fixture)
  metas <- get_meta(dm)@columns
  expect_true(all(vapply(metas, function(c) is.null(c$informat), logical(1))))
})

test_that("a second member hidden in the floored-off v5 tail still aborts", {
  # With wide records (obs_length > 80), an appended member header is shorter
  # than one record, so the EOF-derived row count floors it away and the
  # in-obs signature scan never sees it — the regression was a silent drop.
  df <- data.frame(
    SUBJ = c("A", "B"),
    COMMENT = c(strrep("x", 200), strrep("y", 200)),
    stringsAsFactors = FALSE
  )
  attr(df, "dataset_name") <- "T"
  p <- withr::local_tempfile(fileext = ".xpt")
  write_xpt(df, p)
  sig <- "HEADER RECORD*******MEMBER  HEADER RECORD!!!!!!!"
  con <- file(p, "ab")
  writeBin(charToRaw(sprintf("%-80s", sig)), con)
  close(con)
  expect_error(read_xpt(p), class = "artoo_error_codec")
})

# ---- Regression: numeric magnitude guards (code review 2026-06-14) ----

test_that("a tiny finite value aborts the write instead of underflowing to 0", {
  spec <- artoo_spec(
    datasets = data.frame(
      dataset = "DS",
      label = "d",
      stringsAsFactors = FALSE
    ),
    variables = data.frame(
      dataset = "DS",
      variable = "CONC",
      data_type = "float",
      stringsAsFactors = FALSE
    )
  )
  ap <- apply_spec(
    data.frame(CONC = c(1, 1e-100)),
    spec,
    "DS",
    conformance = "off"
  )
  p <- withr::local_tempfile(fileext = ".xpt")
  # Pre-fix .ieee_to_ibm() silently flushed 1e-100 to 0 on write.
  expect_error(write_xpt(ap, p, created = frozen), class = "artoo_error_codec")
})

test_that("write_xpt warns when data widens a spec-declared length", {
  # Declared length 6, but the UTF-8 bytes need 8: the writer must widen (no
  # truncation) AND say so, or the shipped XPT silently disagrees with the
  # define.xml length.
  sp <- artoo_spec(
    data.frame(dataset = "DM"),
    data.frame(
      dataset = "DM",
      variable = "INVNAM",
      data_type = "string",
      length = 6L,
      stringsAsFactors = FALSE
    )
  )
  x <- apply_spec(
    data.frame(INVNAM = "Öztürk", stringsAsFactors = FALSE),
    sp,
    "DM",
    conformance = "off"
  )
  f <- withr::local_tempfile(fileext = ".xpt")
  expect_warning(
    write_xpt(x, f),
    class = "artoo_warning_encoding"
  )
  expect_snapshot(out <- write_xpt(x, f))
  expect_identical(get_meta(read_xpt(f))@columns$INVNAM$length, 8L)

  # No meta / no declared length: byte-sizing is normal, not a warning.
  g <- withr::local_tempfile(fileext = ".xpt")
  expect_no_warning(
    write_xpt(data.frame(INVNAM = "Öztürk"), g)
  )
})

Try the artoo package in your browser

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

artoo documentation built on July 23, 2026, 1:08 a.m.