Nothing
# Tests for the parquet codec (codec_parquet.R): native data via nanoparquet
# plus the metadata_json sidecar, meta-first temporal realize on read (D1),
# and graceful degrade for a foreign parquet with no sidecar (9.B). Needs
# nanoparquet; skipped where absent.
skip_if_not_installed("nanoparquet")
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
)
}
expect_values_equal <- function(back, orig) {
for (nm in names(orig)) {
expect_equal(as.character(back[[nm]]), as.character(orig[[nm]]), info = nm)
}
}
# ---- round-trip on bundled CDISC data --------------------------------------
test_that("write_parquet/read_parquet round-trips ADSL values and metadata", {
spec <- demo_adam_spec()
adsl <- apply_spec(cdisc_adsl, spec, "ADSL", conformance = "off")
p <- withr::local_tempfile(fileext = ".parquet")
write_parquet(adsl, p)
back <- read_parquet(p)
expect_identical(names(back), names(adsl))
expect_values_equal(back, adsl)
expect_identical(get_meta(back)@columns, get_meta(adsl)@columns)
expect_identical(get_meta(back)@dataset$name, "ADSL")
expect_identical(get_meta(back)@dataset$records, nrow(adsl))
})
test_that("read_parquet realizes Date columns natively", {
spec <- demo_adam_spec()
adsl <- apply_spec(cdisc_adsl, spec, "ADSL", conformance = "off")
p <- withr::local_tempfile(fileext = ".parquet")
write_parquet(adsl, p)
back <- read_parquet(p)
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("DM round-trips through the generic dispatcher by extension", {
spec <- demo_sdtm_spec()
dm <- apply_spec(cdisc_dm, spec, "DM", conformance = "off")
p <- withr::local_tempfile(fileext = ".parquet")
write_dataset(dm, p)
back <- read_dataset(p)
expect_identical(names(back), names(dm))
expect_values_equal(back, dm)
})
# ---- decimal + time fidelity ------------------------------------------------
test_that("decimal stays an exact string and time becomes hms", {
df <- data.frame(D = c("0.10", "100.000", NA), stringsAsFactors = FALSE)
df$TM <- hms::hms(c(3600, NA, 7200))
vars <- data.frame(
dataset = "X",
variable = c("D", "TM"),
label = c("dec", "t"),
data_type = c("decimal", "time"),
length = NA_integer_,
order = 1:2,
stringsAsFactors = FALSE
)
dss <- data.frame(dataset = "X", label = "x", stringsAsFactors = FALSE)
ap <- apply_spec(df, artoo_spec(dss, vars), "X", conformance = "off")
p <- withr::local_tempfile(fileext = ".parquet")
write_parquet(ap, p)
back <- read_parquet(p)
expect_identical(as.character(back$D), c("0.10", "100.000", NA))
expect_s3_class(back$TM, "hms")
expect_identical(unclass(back$TM), unclass(ap$TM))
})
# ---- the metadata_json sidecar ----------------------------------------------
test_that("the sidecar is embedded under the metadata_json key", {
spec <- demo_sdtm_spec()
dm <- apply_spec(cdisc_dm, spec, "DM", conformance = "off")
p <- withr::local_tempfile(fileext = ".parquet")
write_parquet(dm, p)
kv <- nanoparquet::read_parquet_metadata(
p
)$file_meta_data$key_value_metadata[[1]]
expect_true("metadata_json" %in% kv$key)
json <- kv$value[kv$key == "metadata_json"]
expect_identical(
artoo:::.meta_from_datasetjson(json)@columns,
get_meta(dm)@columns
)
})
test_that("a plain nanoparquet file reads with synthesized metadata (9.B)", {
p <- withr::local_tempfile(fileext = ".parquet")
nanoparquet::write_parquet(data.frame(a = 1:3, b = c("x", "y", "z")), p)
back <- read_parquet(p)
expect_identical(nrow(back), 3L)
# No sidecar -> the meta is synthesized from the schema, so the frame still
# feeds get_meta() and a downstream write_xpt()/write_json().
meta <- get_meta(back)
expect_identical(meta@columns$a$dataType, "integer")
expect_identical(meta@columns$b$dataType, "string")
expect_identical(meta@dataset$records, 3L)
xpt <- withr::local_tempfile(fileext = ".xpt")
jsn <- withr::local_tempfile(fileext = ".json")
expect_no_error(write_xpt(back, xpt))
expect_no_error(write_json(back, jsn))
})
test_that("encode without meta writes a sidecar-free parquet", {
# The kv-NULL branch: a frame with no artoo_meta still writes its data.
df <- data.frame(a = 1:3, b = c("x", "y", "z"))
p <- withr::local_tempfile(fileext = ".parquet")
artoo:::.encode_parquet(df, NULL, p)
expect_null(artoo:::.parquet_sidecar(p))
back <- read_parquet(p)
expect_identical(back$a, 1:3)
})
# ---- cross-format chain -----------------------------------------------------
test_that("xpt -> parquet -> json preserves metadata across the chain", {
spec <- demo_adam_spec()
adsl <- apply_spec(cdisc_adsl, spec, "ADSL", conformance = "off")
px <- withr::local_tempfile(fileext = ".xpt")
write_xpt(adsl, px, created = as.POSIXct("2020-01-01", tz = "UTC"))
fromx <- read_xpt(px)
pp <- withr::local_tempfile(fileext = ".parquet")
write_parquet(fromx, pp)
back_pq <- read_parquet(pp)
expect_identical(get_meta(back_pq)@columns, get_meta(fromx)@columns)
pj <- withr::local_tempfile(fileext = ".json")
write_json(back_pq, pj)
back_js <- read_json(pj)
expect_identical(get_meta(back_js)@columns, get_meta(fromx)@columns)
})
# ---- artoo_formats ----------------------------------------------------------
test_that("artoo_formats reports parquet available when nanoparquet is present", {
cf <- artoo_formats()
row <- cf[cf$format == "parquet", ]
expect_true(row$read)
expect_true(row$write)
expect_true(all(c("json", "parquet", "rds", "xpt") %in% cf$format))
})
# ---- internals + atomicity --------------------------------------------------
test_that("a file with KV metadata but no metadata_json key has no sidecar", {
p <- withr::local_tempfile(fileext = ".parquet")
nanoparquet::write_parquet(data.frame(a = 1:3), p, metadata = c(other = "v"))
expect_null(artoo:::.parquet_sidecar(p))
})
test_that("a failed encode leaves any prior file untouched (9.A.4)", {
p <- withr::local_tempfile(fileext = ".parquet")
spec <- demo_sdtm_spec()
dm <- apply_spec(cdisc_dm, spec, "DM", conformance = "off")
write_parquet(dm, p)
before <- readBin(p, "raw", file.info(p)$size)
# A complex column is unrepresentable in Parquet: nanoparquet aborts after
# the temp file is opened, so the cleanup path runs and the target survives.
bad <- data.frame(a = 1:2)
bad$b <- c(1 + 2i, 3 + 4i)
bad <- set_meta(bad, artoo:::.meta_from_frame(bad))
expect_error(write_parquet(bad, p))
expect_identical(readBin(p, "raw", file.info(p)$size), before)
})
# ---- Part B: encoding (record on write, foreign-file read) ------------------
test_that("write_parquet/read_parquet round-trip a multibyte value as canonical UTF-8", {
df <- data.frame(STUDYID = "S1", SITE = "café", stringsAsFactors = FALSE)
p <- withr::local_tempfile(fileext = ".parquet")
write_parquet(df, p)
back <- read_parquet(p)
expect_identical(back$SITE, "café")
expect_identical(Encoding(back$SITE), "UTF-8")
})
test_that("write_parquet(encoding=) records the source charset; bytes stay UTF-8", {
df <- data.frame(STUDYID = "S1", SITE = "café", stringsAsFactors = FALSE)
p <- withr::local_tempfile(fileext = ".parquet")
write_parquet(df, p, encoding = "windows-1252")
back <- read_parquet(p)
expect_identical(get_meta(back)@dataset$encoding, "windows-1252")
expect_identical(back$SITE, "café") # the on-disk bytes are valid UTF-8
expect_error(
write_parquet(
df,
withr::local_tempfile(fileext = ".parquet"),
encoding = "NOPE"
),
class = "artoo_error_codec"
)
})
test_that("read_parquet(encoding=) decodes a foreign (non-UTF-8) byte column", {
w1252 <- iconv("café", "UTF-8", "windows-1252") # raw byte 0xe9
df <- data.frame(STUDYID = "S1", SITE = w1252, stringsAsFactors = FALSE)
p <- withr::local_tempfile(fileext = ".parquet")
nanoparquet::write_parquet(df, p) # raw bytes, no artoo sidecar
back <- read_parquet(p, encoding = "windows-1252")
expect_identical(back$SITE, "café")
})
# ---- compression passthrough ------------------------------------------------
test_that("write_parquet(compression =) reaches nanoparquet", {
df <- data.frame(
USUBJID = rep(sprintf("S-%03d", 1:50), 20),
AVAL = rep(as.numeric(1:100), 10)
)
pz <- withr::local_tempfile(fileext = ".parquet")
pu <- withr::local_tempfile(fileext = ".parquet")
write_parquet(df, pz, compression = "zstd")
write_parquet(df, pu, compression = "uncompressed")
mz <- nanoparquet::read_parquet_metadata(pz)$column_chunks$codec
mu <- nanoparquet::read_parquet_metadata(pu)$column_chunks$codec
expect_true(all(mz == "ZSTD"))
expect_true(all(mu == "UNCOMPRESSED"))
expect_identical(read_parquet(pz)$AVAL, df$AVAL)
})
test_that("an invalid compression value aborts before writing", {
df <- data.frame(A = 1)
p <- withr::local_tempfile(fileext = ".parquet")
expect_error(
write_parquet(df, p, compression = "lz77"),
class = "artoo_error_input"
)
expect_false(file.exists(p))
})
# ---- on_invalid: UTF-8 validation parity with write_xpt --------------------
test_that("write_parquet gates invalid UTF-8 through on_invalid", {
skip_if_not_installed("nanoparquet")
spec <- artoo_spec(
cdisc_sdtm_datasets,
cdisc_sdtm_variables,
codelists = cdisc_codelists
)
dm <- apply_spec(cdisc_dm, spec, "DM", conformance = "off")
dm$USUBJID[1] <- rawToChar(as.raw(c(0x63, 0xE9)))
p <- withr::local_tempfile(fileext = ".parquet")
expect_error(write_parquet(dm, p), class = "artoo_error_codec")
expect_warning(
write_parquet(dm, p, on_invalid = "replace"),
class = "artoo_warning_encoding"
)
back <- read_parquet(p)
expect_true(all(validUTF8(back$USUBJID)))
expect_match(back$USUBJID[1], "[?]")
p2 <- withr::local_tempfile(fileext = ".parquet")
expect_no_warning(write_parquet(dm, p2, on_invalid = "ignore"))
expect_identical(read_parquet(p2)$USUBJID[1], "c")
})
# ---- Regression: factor class consistency (code review 2026-06-14) ----
test_that("a factor column reads back as character, matching the JSON codec", {
skip_if_not_installed("nanoparquet")
df <- data.frame(SEX = factor(c("M", "F", "M")), AGE = c(40L, 50L, 60L))
pp <- withr::local_tempfile(fileext = ".parquet")
write_parquet(df, pp)
pj <- withr::local_tempfile(fileext = ".json")
write_json(df, pj)
# Pre-fix parquet returned a factor while JSON returned character.
expect_identical(class(read_parquet(pp)$SEX), "character")
expect_identical(
as.character(read_parquet(pp)$SEX),
as.character(read_json(pj)$SEX)
)
})
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.