Nothing
# Tests for the json codec (codec_json.R): Dataset-JSON v1.1 round-trips, the
# meta-driven type fidelity (C1), targetDataType emit (9.A.5), decimal-as-
# string (9.A.9), vectorized ragged-row guard (C5), and the structural probe
# (E2). 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 across a round-trip (labels live in meta, not columns).
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_json/read_json round-trips ADSL values and metadata", {
spec <- demo_adam_spec()
adsl <- apply_spec(cdisc_adsl, spec, "ADSL", conformance = "off")
p <- withr::local_tempfile(fileext = ".json")
write_json(adsl, p, created = frozen)
back <- read_json(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_json realizes Date columns from ISO strings", {
spec <- demo_adam_spec()
adsl <- apply_spec(cdisc_adsl, spec, "ADSL", conformance = "off")
p <- withr::local_tempfile(fileext = ".json")
write_json(adsl, p, created = frozen)
back <- read_json(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 = ".json")
write_dataset(dm, p, created = frozen)
back <- read_dataset(p)
expect_identical(names(back), names(dm))
expect_values_equal(back, dm)
})
# ---- byte stability ---------------------------------------------------------
test_that("a frozen created makes two writes byte-identical", {
spec <- demo_sdtm_spec()
dm <- apply_spec(cdisc_dm, spec, "DM", conformance = "off")
p1 <- withr::local_tempfile(fileext = ".json")
p2 <- withr::local_tempfile(fileext = ".json")
write_json(dm, p1, created = frozen)
write_json(dm, p2, created = frozen)
expect_identical(
readBin(p1, "raw", file.info(p1)$size),
readBin(p2, "raw", file.info(p2)$size)
)
})
test_that("the file carries datasetJSONCreationDateTime from created", {
spec <- demo_sdtm_spec()
dm <- apply_spec(cdisc_dm, spec, "DM", conformance = "off")
p <- withr::local_tempfile(fileext = ".json")
write_json(dm, p, created = frozen)
raw <- jsonlite::fromJSON(p, simplifyVector = FALSE)
expect_identical(raw$datasetJSONCreationDateTime, "2020-01-01T00:00:00")
expect_identical(raw$datasetJSONVersion, "1.1.0")
})
# ---- type fidelity ----------------------------------------------------------
test_that("decimal rides as an exact string end to end", {
df <- data.frame(D = c("0.10", "100.000", NA), stringsAsFactors = FALSE)
vars <- data.frame(
dataset = "X",
variable = "D",
label = "dec",
data_type = "decimal",
length = NA_integer_,
order = 1L,
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 = ".json")
write_json(ap, p)
back <- read_json(p)
expect_identical(as.character(back$D), c("0.10", "100.000", NA))
})
test_that("a whole-number double stays double on re-read (C1)", {
df <- data.frame(N = c(1, 2, 3))
vars <- data.frame(
dataset = "X",
variable = "N",
label = "num",
data_type = "double",
length = NA_integer_,
order = 1L,
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 = ".json")
write_json(ap, p)
back <- read_json(p)
expect_true(is.double(back$N))
# set_meta() now projects the column label/format.sas as attrs (labelled-
# frame parity); ignore just those two, the value+type are asserted strictly.
expect_equal(back$N, c(1, 2, 3), ignore_attr = c("label", "format.sas"))
})
test_that("integer, boolean, and time columns round-trip by type", {
df <- data.frame(I = c(1L, NA, 3L), B = c(TRUE, FALSE, NA))
df$TM <- hms::hms(c(3600, NA, 7200))
vars <- data.frame(
dataset = "X",
variable = c("I", "B", "TM"),
label = c("i", "b", "t"),
data_type = c("integer", "boolean", "time"),
length = NA_integer_,
order = 1:3,
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 = ".json")
write_json(ap, p)
back <- read_json(p)
expect_equal(back$I, c(1L, NA, 3L), ignore_attr = c("label", "format.sas"))
expect_equal(
back$B,
c(TRUE, FALSE, NA),
ignore_attr = c("label", "format.sas")
)
expect_s3_class(back$TM, "hms")
expect_identical(unclass(back$TM), unclass(ap$TM))
})
test_that("a date with targetDataType integer rides as a number (9.A.5)", {
df <- data.frame(DT = as.Date(c("2021-01-15", NA, "2021-03-01")))
vars <- data.frame(
dataset = "X",
variable = "DT",
label = "d",
data_type = "date",
target_data_type = "integer",
display_format = "DATE9.",
length = NA_integer_,
order = 1L,
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 = ".json")
write_json(ap, p)
raw <- jsonlite::fromJSON(p, simplifyVector = FALSE)
# First cell is a SAS day number, not an ISO string.
expect_true(is.numeric(raw$rows[[1]][[1]]))
back <- read_json(p)
expect_s3_class(back$DT, "Date")
expect_equal(as.numeric(back$DT), as.numeric(df$DT))
})
test_that("a partial ISO date stays a character string", {
df <- data.frame(DT = c("2021", "2021-03", NA), stringsAsFactors = FALSE)
vars <- data.frame(
dataset = "X",
variable = "DT",
label = "d",
data_type = "date",
length = NA_integer_,
order = 1L,
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 = ".json")
write_json(ap, p)
back <- read_json(p)
expect_identical(as.character(back$DT), c("2021", "2021-03", NA))
})
# ---- empty frame ------------------------------------------------------------
test_that("a zero-row dataset round-trips with an empty rows array", {
spec <- demo_sdtm_spec()
dm <- apply_spec(cdisc_dm[0, ], spec, "DM", conformance = "off")
p <- withr::local_tempfile(fileext = ".json")
write_json(dm, p, created = frozen)
raw <- jsonlite::fromJSON(p, simplifyVector = FALSE)
expect_length(raw$rows, 0L)
back <- read_json(p)
expect_identical(nrow(back), 0L)
expect_identical(names(back), names(dm))
})
# ---- error paths ------------------------------------------------------------
test_that("NaN and Inf abort the write (C2)", {
df <- data.frame(N = c(1, Inf, 3))
df <- set_meta(df, artoo:::.meta_from_frame(df))
expect_snapshot(write_json(df, tempfile(fileext = ".json")), error = TRUE)
expect_error(
write_json(df, tempfile(fileext = ".json")),
class = "artoo_error_type"
)
df2 <- data.frame(N = c(1, NaN, 3))
df2 <- set_meta(df2, artoo:::.meta_from_frame(df2))
expect_error(
write_json(df2, tempfile(fileext = ".json")),
class = "artoo_error_type"
)
})
test_that("a ragged row aborts at its index (C5)", {
p <- withr::local_tempfile(fileext = ".json")
obj <- list(
datasetJSONVersion = "1.1.0",
itemGroupOID = "IG.X",
name = "X",
columns = list(
list(itemOID = "a", name = "A", dataType = "integer"),
list(itemOID = "b", name = "B", dataType = "integer")
),
rows = list(list(1L, 2L), list(3L))
)
writeLines(jsonlite::toJSON(obj, auto_unbox = TRUE), p)
expect_error(read_json(p), class = "artoo_error_codec")
})
test_that("a non-Dataset-JSON file aborts cleanly (E2)", {
p <- withr::local_tempfile(fileext = ".json")
writeLines('{"a":1}', p)
# Scrub the tempfile path by anchoring on the stable message text, so any
# separator/drive (incl. Windows paths) collapses to '<path>'.
scrub <- function(x) {
sub("'[^']*' is not a Dataset-JSON", "'<path>' is not a Dataset-JSON", x)
}
expect_snapshot(read_json(p), error = TRUE, transform = scrub)
expect_error(read_json(p), class = "artoo_error_codec")
})
test_that("malformed JSON aborts with artoo_error_codec", {
p <- withr::local_tempfile(fileext = ".json")
writeLines("{not json", p)
expect_error(read_json(p), class = "artoo_error_codec")
})
test_that("an embedded NUL byte aborts (B5)", {
p <- withr::local_tempfile(fileext = ".json")
writeBin(c(charToRaw('{"a":'), as.raw(0L), charToRaw("1}")), p)
expect_error(read_json(p), class = "artoo_error_codec")
})
test_that("a leading UTF-8 BOM is stripped on read (B5)", {
spec <- demo_sdtm_spec()
dm <- apply_spec(cdisc_dm, spec, "DM", conformance = "off")
p <- withr::local_tempfile(fileext = ".json")
write_json(dm, p, created = frozen)
body <- readBin(p, "raw", file.info(p)$size)
withbom <- withr::local_tempfile(fileext = ".json")
writeBin(c(as.raw(c(0xEF, 0xBB, 0xBF)), body), withbom)
back <- read_json(withbom)
expect_identical(names(back), names(dm))
})
test_that("writing a zero-column frame aborts", {
df <- data.frame(a = 1:3)[, FALSE, drop = FALSE]
expect_error(
write_json(df, tempfile(fileext = ".json")),
class = "artoo_error_codec"
)
})
# ---- internal helpers -------------------------------------------------------
test_that(".temporal_to_iso renders each class and keeps partial values", {
expect_identical(
artoo:::.temporal_to_iso(as.Date("2021-01-15"), "date", "DATE9."),
"2021-01-15"
)
expect_identical(
artoo:::.temporal_to_iso(
as.POSIXct("2021-01-15 08:30:00", tz = "UTC"),
"datetime",
"DATETIME20."
),
"2021-01-15T08:30:00"
)
expect_identical(
artoo:::.temporal_to_iso(hms::hms(3600), "time", "TIME8."),
"01:00:00"
)
# Partial values that cannot realize stay as character text, never NA.
expect_identical(artoo:::.temporal_to_iso("2021", "date", "DATE9."), "2021")
expect_identical(
artoo:::.temporal_to_iso("2021-01", "datetime", "DATETIME20."),
"2021-01"
)
expect_identical(artoo:::.temporal_to_iso("12:30", "time", "TIME8."), "12:30")
})
test_that("an end-to-end datetime column round-trips as POSIXct", {
df <- data.frame(
DTM = as.POSIXct(
c("2021-01-15 08:30:00", NA, "2021-03-01 12:00:00"),
tz = "UTC"
)
)
vars <- data.frame(
dataset = "X",
variable = "DTM",
label = "dtm",
data_type = "datetime",
display_format = "DATETIME20.",
length = NA_integer_,
order = 1L,
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 = ".json")
write_json(ap, p)
back <- read_json(p)
expect_s3_class(back$DTM, "POSIXct")
expect_equal(as.numeric(back$DTM), as.numeric(df$DTM))
})
test_that("the encode/decode default branches coerce via character", {
call <- rlang::current_env()
expect_identical(
artoo:::.json_col_literals(
c("a", NA),
list(dataType = "weird"),
"X",
call
),
c("\"a\"", "null")
)
expect_identical(
artoo:::.json_decode_column(list("a", NULL), list(dataType = "weird")),
c("a", NA)
)
})
# ---- Part B: encoding (UTF-8 default + foreign-file read) -------------------
test_that("write_json/read_json round-trip a multibyte value as canonical UTF-8", {
df <- data.frame(STUDYID = "S1", SITE = "café", stringsAsFactors = FALSE)
p <- withr::local_tempfile(fileext = ".json")
write_json(df, p)
back <- read_json(p)
expect_identical(back$SITE, "café")
expect_identical(Encoding(back$SITE), "UTF-8")
})
test_that("read_json(encoding=) decodes a foreign (non-UTF-8) Dataset-JSON file", {
df <- data.frame(STUDYID = "S1", SITE = "café", stringsAsFactors = FALSE)
p <- withr::local_tempfile(fileext = ".json")
write_json(df, p)
# Rewrite the on-disk file as windows-1252 bytes (a non-conformant producer).
txt <- paste(readLines(p, warn = FALSE), collapse = "\n")
con <- file(p, "wb")
writeBin(charToRaw(iconv(txt, "UTF-8", "windows-1252")), con)
close(con)
back <- read_json(p, encoding = "windows-1252")
expect_identical(back$SITE, "café")
})
# ---- on_invalid: UTF-8 validation parity with write_xpt --------------------
test_that("write_json gates invalid UTF-8 through on_invalid", {
spec <- demo_sdtm_spec()
dm <- apply_spec(cdisc_dm, spec, "DM", conformance = "off")
dm$USUBJID[1] <- rawToChar(as.raw(c(0x63, 0xE9))) # invalid UTF-8 byte
p <- withr::local_tempfile(fileext = ".json")
# Default: abort, naming the offender hex-escaped (artoo_error_codec) --
# never the old uncontrolled utf8_normalize error.
expect_error(write_json(dm, p, created = frozen), class = "artoo_error_codec")
# replace: warn, write a valid-UTF-8 file with ?, round-trip intact.
expect_warning(
write_json(dm, p, created = frozen, on_invalid = "replace"),
class = "artoo_warning_encoding"
)
back <- read_json(p)
expect_true(all(validUTF8(back$USUBJID)))
expect_match(back$USUBJID[1], "[?]")
# ignore: silent, byte dropped.
p2 <- withr::local_tempfile(fileext = ".json")
expect_no_warning(
write_json(dm, p2, created = frozen, on_invalid = "ignore")
)
expect_identical(read_json(p2)$USUBJID[1], "c")
})
test_that("write_json never flags declared-latin1 or factor columns (regression)", {
spec <- demo_sdtm_spec()
dm <- apply_spec(cdisc_dm, spec, "DM", conformance = "off")
lat <- rawToChar(as.raw(c(0x63, 0xE9)))
Encoding(lat) <- "latin1"
dm$USUBJID[1] <- lat # declared mark: transcodes cleanly under "error"
p <- withr::local_tempfile(fileext = ".json")
write_json(dm, p, created = frozen)
expect_identical(read_json(p)$USUBJID[1], "cé")
# The on_invalid gate covers character columns only; a factor column in a
# bare frame (metadata inferred, so meta and frame agree) string-ifies
# through the inferred dataType and round-trips as character.
df <- data.frame(
USUBJID = c("01-001", "01-002"),
SEXF = factor(c("F", "M")),
stringsAsFactors = FALSE
)
p2 <- withr::local_tempfile(fileext = ".json")
expect_no_error(write_json(df, p2, created = frozen))
expect_identical(read_json(p2)$SEXF, c("F", "M"))
})
# ---- Regression: numeric fidelity (code review 2026-06-14) ----
# Minimal one-variable spec for the numeric-fidelity regressions.
one_var_spec <- function(dt) {
artoo_spec(
datasets = data.frame(
dataset = "DS",
label = "d",
stringsAsFactors = FALSE
),
variables = data.frame(
dataset = "DS",
variable = "X",
data_type = dt,
stringsAsFactors = FALSE
)
)
}
test_that("decimal columns round-trip at full precision, not 15 digits", {
# Bit-exact double <-> shortest-string round-trip relies on the platform's
# strtod()/sprintf() being exact inverses, which only holds when R has long
# double. On a noLD build (and macOS arm64, where long double == double) the
# last ulp can shift, so 1/3 reads back one ulp off. The codec already writes
# the best round-trip string the C library allows, so guard the assertion on
# the capability rather than weakening it (a tolerance test could not tell the
# original bug, a 1-ulp write of "0.3", from a correct write).
skip_if_not(
capabilities("long.double"),
"needs long double for exact round-trip"
)
v <- c(0.1 + 0.2, 1 / 3, 1.5, 123456789.98765431)
ap <- apply_spec(data.frame(X = v), one_var_spec("decimal"), "DS")
p <- withr::local_tempfile(fileext = ".json")
write_json(ap, p, created = frozen)
# Pre-fix as.character() wrote "0.3" and read back 0.29999999999999999.
expect_identical(as.numeric(read_json(p)$X), v)
})
test_that("integer values above the 32-bit range survive a JSON round-trip", {
ap <- apply_spec(
data.frame(X = c(3e9, -3e9, 5)),
one_var_spec("integer"),
"DS",
on_coercion_loss = "keep",
conformance = "off"
)
p <- withr::local_tempfile(fileext = ".json")
write_json(ap, p, created = frozen)
# Pre-fix as.integer() NA'd 3e9 on both write and read.
expect_identical(as.numeric(read_json(p)$X), c(3e9, -3e9, 5))
})
test_that("a large integer read from a foreign Dataset-JSON is not NA'd", {
p <- withr::local_tempfile(fileext = ".json")
writeLines(
paste0(
'{"datasetJSONVersion":"1.1.0","columns":[{"itemOID":"IT.BIG",',
'"name":"BIG","label":"Big","dataType":"integer"}],"rows":[[3000000000]]}'
),
p
)
expect_identical(as.numeric(read_json(p)$BIG), 3e9)
})
test_that("Inf in a decimal column aborts instead of writing invalid JSON", {
# Pre-fix the decimal branch only as.character()'d, so [..,"Inf"] (invalid
# CDISC JSON) was written with no abort; the NaN/Inf gate now covers decimal.
cm <- list(dataType = "decimal")
expect_error(
artoo:::.json_col_literals(c(1, Inf), cm, "X", rlang::caller_env()),
class = "artoo_error_type"
)
expect_error(
artoo:::.json_col_literals(c(1, NaN), cm, "X", rlang::caller_env()),
class = "artoo_error_type"
)
})
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.