Nothing
# The NDJSON codec: CDISC Dataset-JSON v1.1, newline-delimited variant.
# Line 1 is the metadata object (everything a .json file carries except
# `rows`); each subsequent line is one row array. Bounded memory in both
# directions: the writer streams slabs, the reader accumulates per slab.
test_that("ndjson round-trips the torture frame losslessly", {
src <- .torture_frame()
p <- withr::local_tempfile(fileext = ".ndjson")
write_ndjson(src, p)
back <- read_ndjson(p)
expect_lossless(src, back, via = "ndjson")
})
test_that("the file is one JSON object per line, meta first", {
df <- data.frame(
USUBJID = c("01-001", "01-002"),
AVAL = c(1.5, NA),
stringsAsFactors = FALSE
)
p <- withr::local_tempfile(fileext = ".ndjson")
write_ndjson(df, p)
lines <- readLines(p)
expect_length(lines, 3L)
meta_line <- jsonlite::fromJSON(lines[1], simplifyVector = FALSE)
expect_identical(meta_line$datasetJSONVersion, "1.1.0")
expect_false("rows" %in% names(meta_line))
row1 <- jsonlite::fromJSON(lines[2], simplifyVector = FALSE)
expect_identical(row1[[1]], "01-001")
row2 <- jsonlite::fromJSON(lines[3], simplifyVector = FALSE)
expect_null(row2[[2]])
})
test_that("ndjson is byte-stable under a frozen created", {
df <- data.frame(USUBJID = "01-001", AVAL = 1.5, stringsAsFactors = FALSE)
frozen <- as.POSIXct("2024-01-15 10:30:00", tz = "UTC")
p1 <- withr::local_tempfile(fileext = ".ndjson")
p2 <- withr::local_tempfile(fileext = ".ndjson")
write_ndjson(df, p1, created = frozen)
write_ndjson(df, p2, created = frozen)
expect_identical(
readBin(p1, "raw", file.info(p1)$size),
readBin(p2, "raw", file.info(p2)$size)
)
})
test_that("n_max reads only the requested rows", {
df <- data.frame(ID = sprintf("S-%03d", 1:50), AVAL = as.numeric(1:50))
p <- withr::local_tempfile(fileext = ".ndjson")
write_ndjson(df, p)
withr::local_options(artoo.json_slab_rows = 7L)
part <- read_ndjson(p, n_max = 10)
expect_identical(nrow(part), 10L)
expect_identical(part$ID, df$ID[1:10])
expect_identical(get_meta(part)@dataset$records, 10L)
})
test_that("col_select narrows columns with the generic filter", {
df <- data.frame(A = 1:3, B = c("x", "y", "z"), C = c(0.5, NA, 2.5))
p <- withr::local_tempfile(fileext = ".ndjson")
write_ndjson(df, p)
part <- read_ndjson(p, col_select = c("C", "A"))
expect_identical(names(part), c("A", "C"))
expect_error(
read_ndjson(p, col_select = "NOPE"),
class = "artoo_error_input"
)
})
test_that("a ragged row aborts with the row number", {
df <- data.frame(A = 1:2, B = c("x", "y"))
p <- withr::local_tempfile(fileext = ".ndjson")
write_ndjson(df, p)
lines <- readLines(p)
lines[3] <- "[1]" # row 2 loses a cell
writeLines(lines, p)
expect_error(read_ndjson(p), class = "artoo_error_codec")
})
test_that("a non-Dataset-JSON first line aborts cleanly", {
p <- withr::local_tempfile(fileext = ".ndjson")
writeLines(c("{\"foo\": 1}", "[1,2]"), p)
expect_error(read_ndjson(p), class = "artoo_error_codec")
writeLines("not json at all", p)
expect_error(read_ndjson(p), class = "artoo_error_codec")
})
test_that("a 0-row frame round-trips with typed columns", {
df <- data.frame(USUBJID = character(0), AVAL = numeric(0))
p <- withr::local_tempfile(fileext = ".ndjson")
write_ndjson(df, p)
back <- read_ndjson(p)
expect_identical(nrow(back), 0L)
expect_type(back$USUBJID, "character")
expect_type(back$AVAL, "double")
})
test_that("special missings and the _artoo block ride line 1", {
df <- data.frame(AENDY = c(10, NA, NA))
attr(df$AENDY, "sas_missing") <- c(NA, ".A", NA)
p <- withr::local_tempfile(fileext = ".ndjson")
write_ndjson(df, p)
meta_line <- jsonlite::fromJSON(readLines(p)[1], simplifyVector = FALSE)
expect_identical(
unlist(meta_line[["_artoo"]]$specialMissings$AENDY$tags),
".A"
)
back <- read_ndjson(p)
expect_identical(attr(back$AENDY, "sas_missing"), c(NA, ".A", NA))
})
test_that("write_ndjson(strict = TRUE) suppresses _artoo with a warning", {
df <- data.frame(AENDY = c(10, NA))
attr(df$AENDY, "sas_missing") <- c(NA, ".A")
p <- withr::local_tempfile(fileext = ".ndjson")
expect_warning(
write_ndjson(df, p, strict = TRUE),
class = "artoo_warning_codec"
)
meta_line <- jsonlite::fromJSON(readLines(p)[1], simplifyVector = FALSE)
expect_false("_artoo" %in% names(meta_line))
})
# ---- gz transparency (json + ndjson) ----------------------------------------
test_that("a .ndjson.gz round-trips transparently", {
src <- .torture_frame()
p <- withr::local_tempfile(fileext = ".ndjson.gz")
write_ndjson(src, p)
# The bytes on disk are gzip.
head2 <- readBin(p, "raw", 2L)
expect_identical(head2, as.raw(c(0x1F, 0x8B)))
expect_lossless(src, read_ndjson(p), via = "ndjson.gz")
# And the generic dispatcher resolves the double extension.
expect_lossless(src, read_dataset(p), via = "ndjson.gz dispatch")
})
test_that("a .json.gz round-trips transparently", {
src <- .torture_frame()
p <- withr::local_tempfile(fileext = ".json.gz")
write_dataset(src, p)
head2 <- readBin(p, "raw", 2L)
expect_identical(head2, as.raw(c(0x1F, 0x8B)))
expect_lossless(src, read_dataset(p), via = "json.gz")
})
test_that("gz is refused for formats that do not stream text", {
df <- data.frame(A = 1)
p <- withr::local_tempfile(fileext = ".xpt.gz")
expect_error(write_dataset(df, p), class = "artoo_error_input")
})
# ---- coverage of the guard branches -----------------------------------------
test_that("encode without metadata aborts (0-column frame)", {
df <- data.frame()
p <- withr::local_tempfile(fileext = ".ndjson")
expect_error(write_ndjson(df, p), class = "artoo_error_codec")
})
test_that("an empty file and a BOM-prefixed file are handled", {
p <- withr::local_tempfile(fileext = ".ndjson")
file.create(p)
expect_error(read_ndjson(p), class = "artoo_error_codec")
# A BOM before the metadata line is stripped, not a parse error.
df <- data.frame(A = 1:2)
write_ndjson(df, p)
lines <- readLines(p)
con <- file(p, "wb")
writeBin(
c(
as.raw(c(0xEF, 0xBB, 0xBF)),
charToRaw(paste0(
paste(lines, collapse = "\n"),
"\n"
))
),
con
)
close(con)
back <- read_ndjson(p)
expect_identical(back$A, 1:2)
})
test_that("a row line that is not JSON aborts with the row range", {
df <- data.frame(A = 1:3)
p <- withr::local_tempfile(fileext = ".ndjson")
write_ndjson(df, p)
lines <- readLines(p)
lines[3] <- "[1,}garbage"
writeLines(lines, p)
expect_error(read_ndjson(p), class = "artoo_error_codec")
})
test_that("n_max larger than the file reads everything", {
df <- data.frame(A = 1:5)
p <- withr::local_tempfile(fileext = ".ndjson")
write_ndjson(df, p)
expect_identical(nrow(read_ndjson(p, n_max = 100)), 5L)
})
test_that("interior blank lines are tolerated", {
df <- data.frame(A = 1:3)
p <- withr::local_tempfile(fileext = ".ndjson")
write_ndjson(df, p)
lines <- readLines(p)
writeLines(c(lines[1], lines[2], "", lines[3], lines[4], ""), p)
withr::local_options(artoo.json_slab_rows = 2L)
back <- read_ndjson(p)
expect_identical(back$A, 1:3)
})
test_that("a failed encode leaves any prior ndjson untouched", {
p <- withr::local_tempfile(fileext = ".ndjson")
df <- data.frame(A = 1:2)
write_ndjson(df, p)
before <- readBin(p, "raw", file.info(p)$size)
bad <- data.frame(AVAL = c(1, NaN))
expect_error(write_ndjson(bad, p), class = "artoo_error_type")
expect_identical(readBin(p, "raw", file.info(p)$size), before)
})
test_that("the jsonl extension resolves to the ndjson codec", {
df <- data.frame(A = 1:2)
p <- withr::local_tempfile(fileext = ".jsonl")
write_dataset(df, p)
expect_identical(read_dataset(p)$A, 1:2)
})
# ---- on_invalid: UTF-8 validation parity with write_xpt --------------------
test_that("write_ndjson gates invalid UTF-8 through on_invalid", {
df <- data.frame(
USUBJID = c(rawToChar(as.raw(c(0x63, 0xE9))), "01-002"),
stringsAsFactors = FALSE
)
p <- withr::local_tempfile(fileext = ".ndjson")
expect_error(write_ndjson(df, p), class = "artoo_error_codec")
expect_warning(
write_ndjson(df, p, on_invalid = "replace"),
class = "artoo_warning_encoding"
)
back <- read_ndjson(p)
expect_true(all(validUTF8(back$USUBJID)))
expect_match(back$USUBJID[1], "[?]")
p2 <- withr::local_tempfile(fileext = ".ndjson")
expect_no_warning(write_ndjson(df, p2, on_invalid = "ignore"))
expect_identical(read_ndjson(p2)$USUBJID[1], "c")
})
test_that("read_ndjson(encoding=) transcodes non-UTF-8 source bytes to UTF-8", {
# A frame carrying a non-ASCII glyph, written as UTF-8, then re-encoded to
# windows-1252 to simulate a non-conformant producer.
df <- data.frame(
USUBJID = c("01-001", "01-002"),
NOTE = c("café", "café"), # e-acute
stringsAsFactors = FALSE
)
p_utf8 <- withr::local_tempfile(fileext = ".ndjson")
write_ndjson(df, p_utf8)
# Default read of the UTF-8 file recovers the glyph (NFC parity with json).
expect_equal(read_ndjson(p_utf8)$NOTE, c("café", "café"))
# Re-encode UTF-8 -> windows-1252 (e-acute becomes byte 0xE9).
txt <- rawToChar(readBin(p_utf8, "raw", n = file.size(p_utf8)))
Encoding(txt) <- "UTF-8"
w1252 <- iconv(txt, from = "UTF-8", to = "WINDOWS-1252", toRaw = TRUE)[[1]]
p_w1252 <- withr::local_tempfile(fileext = ".ndjson")
writeBin(w1252, p_w1252)
# encoding = "windows-1252" recovers the real glyph on read.
back <- read_ndjson(p_w1252, encoding = "windows-1252")
expect_equal(back$NOTE, c("café", "café"))
expect_true(all(validUTF8(back$NOTE)))
})
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.