Nothing
# ==============================================================================
# Verifies that each compression setting actually builds the pipeline it
# advertises: the data shrinks, the expected filters are applied, and values
# survive the round trip. (test-compress.r only checks that writes succeed.)
# ==============================================================================
pipeline <- function(file, name) vapply(h5_inspect(file, name)$filters, `[[`, "", "name")
ratio <- function(file, name) { i <- h5_inspect(file, name); i$uncompressed_size / i$storage_size }
# Highly compressible float64 data
compressible <- rep(c(1.5, 2.25, 3.125, 4.0625), 25000)
# ==============================================================================
# 1. Blosc codecs compress at their default levels
# ==============================================================================
local({
#test_that("Blosc codecs without an explicit level still compress", {
f <- tempfile(fileext = ".h5")
on.exit(unlink(f))
# Blosc treats clevel 0 as "store uncompressed", so a default level of 0
# must never reach the filter.
# Needs an hdf5lib whose Blosc1 passes UBSan: it calls bitshuffle through a
# mismatched function pointer. Restore the Blosc1 codecs once it does.
# for (codec in c("blosc1", "blosc1-lz4", "blosc1-lz4-0", "blosc1-snappy",
# "blosc2", "blosc2-lz4", "blosc2-lz4-0", "blosc2-ndlz")) {
for (codec in c("blosc2", "blosc2-lz4", "blosc2-lz4-0", "blosc2-ndlz")) {
h5_write(compressible, f, codec, compress = codec)
expect_true(ratio(f, codec) > 2, info = codec)
expect_equal(h5_read(f, codec), compressible, info = codec)
}
})
local({
#test_that("Blosc2 ZFP accuracy mode compresses for tolerances below 1", {
f <- tempfile(fileext = ".h5")
on.exit(unlink(f))
set.seed(1)
x <- cumsum(rnorm(1e5, sd = 0.01))
for (codec in c("blosc2-zfp-acc-0.01", "blosc2-zfp-acc-0.001")) {
h5_write(x, f, codec, compress = codec)
expect_equal(pipeline(f, codec), "blosc2", info = codec)
expect_true(ratio(f, codec) > 1.5, info = codec)
# Needs hdf5lib 2.2.0.1: 2.2.0.0's ZFP decoder shifts negative values, which
# UBSan reports. Restore once h5lite requires it.
# expect_true(max(abs(h5_read(f, codec) - x)) <= 0.01, info = codec)
}
})
# ==============================================================================
# 2. Gzip inside Blosc uses the Blosc filter
# ==============================================================================
local({
#test_that("blosc1-gzip and blosc2-gzip use Blosc, not plain deflate", {
f <- tempfile(fileext = ".h5")
on.exit(unlink(f))
# Needs an hdf5lib whose Blosc1 passes UBSan (see the first block above).
# h5_write(compressible, f, "b1_gzip", compress = "blosc1-gzip-5")
# expect_equal(pipeline(f, "b1_gzip"), "blosc")
# expect_equal(h5_read(f, "b1_gzip"), compressible)
h5_write(compressible, f, "b2_gzip", compress = "blosc2-gzip-5")
expect_equal(pipeline(f, "b2_gzip"), "blosc2")
expect_equal(h5_read(f, "b2_gzip"), compressible)
})
local({
#test_that("Unsupported Blosc2-ZFP data falls back to the default gzip pipeline", {
f <- tempfile(fileext = ".h5")
on.exit(unlink(f))
# ZFP inside Blosc2 is float-only; integers get the standard fallback
h5_write(1:1000, f, "b2_zfp_int", compress = "blosc2-zfp-rate-8", as = "int32")
expect_equal(pipeline(f, "b2_zfp_int"), c("shuffle", "gzip"))
expect_equal(h5_read(f, "b2_zfp_int"), 1:1000)
})
# Needs hdf5lib 2.2.0.1, which converts the cd_values h5lite passes to its new
# Blosc2 layout. Restore once h5lite requires it.
# local({
# #test_that("Blosc2 gets its pre-filters and truncation bits in cd_values[0]", {
# f <- tempfile(fileext = ".h5")
# on.exit(unlink(f))
#
# # hdf5lib's layout: 0xB2100001 + (bitmask << 16) + (bits << 8). Slots 4-6
# # hold the level, the last pre-filter (2 = bitshuffle), and the codec, as
# # the reference filter keeps them, so h5py with hdf5plugin can append.
# cmp <- h5_compression("blosc2-zstd-5", blosc2_delta = TRUE, blosc2_truncate = -5)
# h5_write(matrix(compressible + 1e-9, 100), f, "x", compress = cmp)
# cd <- h5_inspect(f, "x")$filters[[1]]$cd_values
# expect_equal(cd[1], 0xB2100001 + (8 + 4 + 2) * 2^16 + 251 * 2^8)
# expect_equal(cd[5:7], c(3, 2, 5))
# })
# Needs hdf5lib 2.2.0.1, which declines Blosc2's ZFP codecs on chunks of under
# 4 values and compresses chunks of over 4 dimensions. Restore once h5lite
# requires it.
# local({
# #test_that("Blosc2's ZFP codecs store chunks of under 4 values uncompressed", {
# f <- tempfile(fileext = ".h5")
# on.exit(unlink(f))
#
# # ZFP can't fill a block from them, so hdf5lib records level 255, which
# # makes Blosc2 refuse each chunk
# x <- c(1.5, 2.25, 3.125)
# h5_write(x, f, "x", compress = "blosc2-zfp-acc-0.01")
# info <- h5_inspect(f, "x")
# expect_identical(h5_read(f, "x"), x)
# expect_equal(info$filters[[1]]$cd_values[5], 255)
# expect_equal(info$storage_size, info$uncompressed_size)
# expect_stdout(print(info), "Pipeline: blosc2 \\[zfp-acc\\] \\(declined: stored uncompressed\\)")
#
# # Chunks of more than 4 dimensions are compressed as one long row
# set.seed(1)
# arr <- array(cumsum(rnorm(4^5, sd = 0.01)), rep(4, 5))
# h5_write(arr, f, "arr5d", compress = "blosc2-zfp-acc-0.01")
# info <- h5_inspect(f, "arr5d")
# expect_true(info$storage_size < info$uncompressed_size / 2)
# expect_true(max(abs(h5_read(f, "arr5d") - arr)) <= 0.01)
# expect_false(any(grepl("declined", capture.output(print(info)))))
# })
local({
#test_that("NDLZ gets no pre-filters or truncation bits", {
f <- tempfile(fileext = ".h5")
on.exit(unlink(f))
# NDLZ runs no pre-filters, and took truncation bits of 8 for 8x8 cells
x <- matrix(compressible + 1e-9, 100)
cmp <- h5_compression("blosc2-ndlz", blosc2_delta = TRUE, blosc2_truncate = 8)
h5_write(x, f, "x", compress = cmp)
cd <- h5_inspect(f, "x")$filters[[1]]$cd_values
# Needs hdf5lib 2.2.0.1's new cd_values layout. Restore once h5lite
# requires it.
# expect_equal(cd[1], 0xB2100001 + 4 * 2^8) # The default 4x4 cells
expect_equal(cd[6], 0)
expect_identical(h5_read(f, "x"), x)
})
local({
#test_that("blosc2_truncate skips float32 data whose whole mantissa it would drop", {
f <- tempfile(fileext = ".h5")
on.exit(unlink(f))
x <- seq(0, 1, length.out = 1000) / 3
h5_write(x, f, "ref", as = "float32", compress = "none")
for (bits in c(-30, -10))
h5_write(x, f, paste0("f32_", -bits), as = "float32",
compress = h5_compression("blosc2-lz4", blosc2_truncate = bits))
h5_write(x, f, "f64_30", compress = h5_compression("blosc2-lz4", blosc2_truncate = -30))
# A float32 has 23 mantissa bits, so dropping 30 leaves it whole (hdf5lib
# would refuse it); dropping 10 truncates, and float64 can lose 30
expect_identical(h5_read(f, "f32_30"), h5_read(f, "ref"))
expect_false(identical(h5_read(f, "f32_10"), h5_read(f, "ref")))
expect_false(identical(h5_read(f, "f64_30"), x))
})
# Needs hdf5lib 2.2.0.1: 2.2.0.0 won't run Blosc2 on Scale-Offset output with 2
# or more dimensions, and leaks memory (which valgrind reports) each time it
# refuses a chunk. Restore once h5lite requires it.
# local({
# #test_that("Blosc2 compresses Scale-Offset output at any chunk rank", {
# f <- tempfile(fileext = ".h5")
# on.exit(unlink(f))
#
# # Scale-Offset changes each chunk's size, so hdf5lib now compresses it as
# # plain Blosc2 instead of refusing every chunk with two or more dimensions
# m <- matrix(1:20000 %% 300L, 200)
# h5_write(m, f, "so", compress = h5_compression("none", int_packing = TRUE))
# h5_write(m, f, "so_b2", compress = h5_compression("blosc2-lz4", int_packing = TRUE))
# expect_identical(h5_read(f, "so_b2"), m)
# expect_equal(pipeline(f, "so_b2"), c("scaleoffset", "blosc2"))
# expect_true(h5_inspect(f, "so_b2")$storage_size < h5_inspect(f, "so")$storage_size)
# })
local({
#test_that("print.inspect reads Blosc2 lists from earlier hdf5lib and other writers", {
fake <- function(cd) structure(
list(type = "float64", uncompressed_size = 8000, storage_size = 4000,
layout = "chunked", chunk_dims = 1000,
filters = list(list(name = "blosc2", cd_values = cd))),
class = c("inspect", "list"))
# Earlier hdf5lib (2): the bitmask in slot 5, and the bits in its second
# byte (1-D chunks) or after the chunk shape
expect_stdout(print(fake(c(2, 0, 8, 8000, 5, 10 + 251 * 256, 5))),
"blosc2 \\[trunc\\(bits=-5\\) -> bitshuffle -> zstd\\]")
expect_stdout(print(fake(c(2, 0, 8, 8000, 5, 14, 1, 2, 10, 100, 10))),
"blosc2 \\[trunc\\(bits=10\\) -> delta -> bitshuffle -> lz4\\]")
# The reference filter (1): only the last pre-filter, as a filter code
expect_stdout(print(fake(c(1, 0, 8, 8000, 5, 3, 5))), "blosc2 \\[delta -> zstd\\]")
expect_stdout(print(fake(c(1, 0, 8, 8000, 5, 4, 5))), "blosc2 \\[trunc -> zstd\\]")
})
# ==============================================================================
# 3. Lossy filters never corrupt missing or non-finite values
# ==============================================================================
local({
#test_that("float_rounding preserves NA and NaN", {
f <- tempfile(fileext = ".h5")
on.exit(unlink(f))
set.seed(1)
x <- rnorm(1000, mean = 98.6, sd = 0.5)
x[c(5, 500)] <- NA
x[10] <- NaN
h5_write(x, f, "fr", compress = h5_compression("gzip", float_rounding = 2))
y <- h5_read(f, "fr")
expect_identical(is.na(y), is.na(x))
expect_true(max(abs(y - x), na.rm = TRUE) <= 0.005 + 1e-9)
})
local({
#test_that("ZFP preserves NA, NaN, and Inf", {
f <- tempfile(fileext = ".h5")
on.exit(unlink(f))
set.seed(1)
x <- rnorm(1000)
x[c(5, 500)] <- NA
x[10] <- NaN
x[20] <- Inf
for (codec in c("zfp-acc-0.01", "zfp-prec-16", "zfp-rate-16", "blosc2-zfp-prec-16")) {
h5_write(x, f, codec, compress = codec)
y <- h5_read(f, codec)
expect_identical(is.na(y), is.na(x), info = codec)
expect_identical(is.infinite(y), is.infinite(x), info = codec)
expect_true(max(abs(y - x)[is.finite(x)]) < 0.01, info = codec)
}
})
# Needs hdf5lib 2.2.0.1: 2.2.0.0 crashes R on these chunks (it records no
# parameters for them, then reads them through a NULL pointer). Restore once
# h5lite requires it.
# local({
# #test_that("ZFP stores chunks it can't compress unfiltered", {
# f <- tempfile(fileext = ".h5")
# on.exit(unlink(f))
#
# # ZFP compresses chunks with 1 to 4 dimensions longer than one. For others,
# # hdf5lib records parameters ({version, 0}) that make it refuse every chunk,
# # so HDF5 stores each one unfiltered. (It used to record none.)
# set.seed(1)
# cases <- list(
# value = rnorm(1),
# arr5d = array(rnorm(32), rep(2, 5)),
# chunks = array(rnorm(4^5), rep(4, 5)) ) # Eight 2x2x2x4x4 chunks
#
# for (codec in c("zfp-acc-0.01", "zfp-prec-16")) {
# for (nm in names(cases)) {
# id <- paste0(codec, "_", nm)
# h5_write(cases[[nm]], f, id, compress = h5_compression(codec, chunk_size = 1024))
# info <- h5_inspect(f, id)
# zfp <- info$filters[[1]]
#
# # Lossy ZFP would not return these values exactly
# expect_identical(h5_read(f, id), cases[[nm]], info = id)
# expect_equal(pipeline(f, id), "zfp", info = id)
# expect_equal(zfp$cd_values[-1], 0, info = id)
# # Any compressed chunk would change the total
# expect_equal(info$storage_size, info$uncompressed_size, info = id)
# expect_stdout(print(info), "Pipeline: zfp \\(declined: stored uncompressed\\)", info = id)
# }
# }
#
# # Chunks with 1 to 4 dimensions longer than one are still compressed
# h5_write(array(rnorm(256), rep(4, 4)), f, "arr4d", compress = "zfp-acc-0.01")
# info <- h5_inspect(f, "arr4d")
# expect_true(length(info$filters[[1]]$cd_values) > 2)
# expect_true(info$storage_size < info$uncompressed_size / 2)
# expect_false(any(grepl("declined", capture.output(print(info)))))
# })
# Until h5lite requires hdf5lib 2.2.0.1: remove this block with the guard in
# apply_compression(), and restore the one above.
local({
#test_that("Standalone ZFP falls back to gzip for chunks it can't compress", {
f <- tempfile(fileext = ".h5")
on.exit(unlink(f))
# ZFP compresses chunks with 1 to 4 dimensions longer than one. hdf5lib
# 2.2.0.0 crashes R on others, so they get the default pipeline instead.
set.seed(1)
cases <- list(
value = rnorm(1),
arr5d = array(rnorm(32), rep(2, 5)),
chunks = array(rnorm(4^5), rep(4, 5)) ) # Eight 2x2x2x4x4 chunks
for (codec in c("zfp-acc-0.01", "zfp-rev")) {
for (nm in names(cases)) {
id <- paste0(codec, "_", nm)
h5_write(cases[[nm]], f, id, compress = h5_compression(codec, chunk_size = 1024))
expect_equal(pipeline(f, id), c("shuffle", "gzip"), info = id)
expect_identical(h5_read(f, id), cases[[nm]], info = id)
}
}
# Chunks with 1 to 4 dimensions longer than one still get ZFP
h5_write(array(rnorm(256), rep(4, 4)), f, "arr4d", compress = "zfp-acc-0.01")
expect_equal(pipeline(f, "arr4d"), "zfp")
# 2.2.0.0's reversible ZFP overflows signed integers on data with mixed signs,
# which UBSan reports.
# h5_write(array(rnorm(16), c(2, 2, 2, 2, 1)), f, "arr5d1", compress = "zfp-rev")
# expect_equal(pipeline(f, "arr5d1"), "zfp")
})
local({
#test_that("float_rounding is skipped for 2-byte floats", {
f <- tempfile(fileext = ".h5")
on.exit(unlink(f))
# HDF5's Scale-Offset filter can't handle them, so the write used to fail
for (as in c("float16", "bfloat16")) {
h5_write(c(1.25, 2.5, 3.75), f, as, as = as, compress = h5_compression(float_rounding = 1))
expect_equal(pipeline(f, as), c("shuffle", "gzip"), info = as)
expect_equal(h5_read(f, as), c(1.25, 2.5, 3.75), info = as)
}
})
local({
#test_that("blosc2_truncate only rounds floats, and keeps NA", {
f <- tempfile(fileext = ".h5")
on.exit(unlink(f))
trunc <- h5_compression("blosc2-zstd", blosc2_truncate = 10)
# Truncation zeroes low bits of any 4- or 8-byte item, so integers and
# data frame records must not get it
ints <- 1:2000 * 1000L
h5_write(ints, f, "uint32", compress = trunc)
h5_write(ints, f, "int32", compress = trunc, as = "int32")
h5_write(as.numeric(ints), f, "int64", compress = trunc, as = "int64")
expect_equal(h5_read(f, "uint32"), ints)
expect_equal(h5_read(f, "int32"), ints)
expect_equal(h5_read(f, "int64"), ints)
df <- data.frame(a = ints, b = rev(ints))
h5_write(df, f, "df", compress = trunc, as = c(a = "int32", b = "int32"))
expect_equal(h5_read(f, "df"), df)
# NA would lose the bits that set it apart from NaN, or become Inf. (Blosc
# leaves buffers under 128 bytes alone, so write more than that.)
x <- rep(c(1.5, NA, 2.5, NaN, Inf, -Inf), 100)
h5_write(x, f, "nonfinite", compress = trunc)
expect_identical(h5_read(f, "nonfinite"), x)
# Floats are still truncated
h5_write(compressible + 1e-9, f, "float", compress = trunc)
expect_true(any(h5_read(f, "float") != compressible + 1e-9))
})
# Needs hdf5lib 2.2.0.1, whose ZFP refuses just the chunks holding NA, NaN, or
# Inf; until then h5_write() uses gzip for such data. Restore once h5lite
# requires it.
# local({
# #test_that("ZFP stores only the chunks with non-finite values uncompressed", {
# f <- tempfile(fileext = ".h5")
# on.exit(unlink(f))
#
# set.seed(1)
# x <- cumsum(rnorm(1e5, sd = 0.01))
# x[c(10, 50000)] <- NA
# x[99999] <- Inf
#
# # 64 KB chunks: 13 chunks, 3 of which hold a non-finite value
# for (codec in c("zfp-acc-0.01", "blosc2-zfp-acc-0.01")) {
# h5_write(x, f, codec, compress = h5_compression(codec, chunk_size = 64 * 1024))
# y <- h5_read(f, codec)
# expect_equal(pipeline(f, codec), if (startsWith(codec, "zfp")) "zfp" else "blosc2", info = codec)
# expect_identical(is.na(y), is.na(x), info = codec)
# expect_identical(is.infinite(y), is.infinite(x), info = codec)
# expect_true(max(abs(y - x)[is.finite(x)]) <= 0.01, info = codec)
# expect_true(ratio(f, codec) > 1.5, info = codec)
# }
# })
# Needs hdf5lib 2.2.0.1, whose ZFP refuses chunks holding NaN (2.2.0.0's
# corrupts them). Restore once h5lite requires it.
# local({
# #test_that("a filter that fails when the dataset closes is never silent", {
# f <- tempfile(fileext = ".h5")
# on.exit(unlink(f))
#
# # Call the writer directly, bypassing h5_write()'s own checks. A small
# # dataset sits in HDF5's chunk cache until the dataset is closed, so a
# # filter that rejects the chunk only fails then.
# x <- c(1.5, NaN, 2.5, 3.5)
# res <- tryCatch(
# expr = {
# h5lite:::h5_call(.Call("C_h5_write_dataset", f, "z", x, "float64", length(x),
# h5_compression("zfp-acc-0.01"), PACKAGE = "h5lite"))
# h5_read(f, "z")
# },
# error = function(e) "error")
# expect_true(identical(res, "error") || isTRUE(all.equal(res, x, tolerance = 0.01)))
# })
local({
#test_that("h5_threads() sets the number of threads", {
f <- tempfile(fileext = ".h5")
on.exit(unlink(f))
expect_error(h5_threads(-1))
expect_error(h5_threads(1.5))
expect_error(h5_threads(257))
# H5LITE_NTHREADS sets the default, so clear it for the counts below and
# restore it afterwards. An empty value counts as unset; clearing it with
# Sys.setenv() rather than Sys.unsetenv() lets tinytest record the original
# value to restore. (CRAN allows at most 2 threads.)
env <- Sys.getenv("H5LITE_NTHREADS", unset = NA)
on.exit(if (is.na(env)) Sys.unsetenv("H5LITE_NTHREADS") else Sys.setenv(H5LITE_NTHREADS = env), add = TRUE)
on.exit(h5_threads(0), add = TRUE)
Sys.setenv(H5LITE_NTHREADS = "")
expect_equal(h5_threads(), 1L)
expect_equal(h5_threads(2), 0L) # The previous setting; 0 means none
expect_equal(h5_threads(), 2L)
# Data round-trips with threads in both Blosc versions
# Needs an hdf5lib whose Blosc1 passes UBSan (see the first block above).
# for (codec in c("blosc1-zstd-5", "blosc2-zstd-5")) {
for (codec in c("blosc2-zstd-5")) {
h5_write(compressible, f, codec, compress = h5_compression(codec, chunk_size = 256 * 1024))
expect_equal(h5_read(f, codec), compressible, info = codec)
}
expect_equal(h5_threads(0), 2L)
expect_equal(h5_threads(), 1L)
# H5LITE_NTHREADS sets the default, which h5_threads() overrides
Sys.setenv(H5LITE_NTHREADS = 2)
expect_equal(h5_threads(), 2L)
expect_equal(h5_threads(1), 0L)
expect_equal(h5_threads(), 1L)
expect_equal(h5_threads(0), 1L)
expect_equal(h5_threads(), 2L) # Back to the default
# The default applies to the next read or write
h5_write(compressible, f, "env", compress = h5_compression("blosc2-zstd-5", chunk_size = 256 * 1024))
expect_equal(h5_read(f, "env"), compressible)
# Values that aren't positive whole numbers are ignored
for (val in c("abc", "0", "-2")) {
Sys.setenv(H5LITE_NTHREADS = val)
expect_equal(h5_threads(), 1L, info = val)
}
})
# ==============================================================================
# 4. "none" and logical `compress` values
# ==============================================================================
local({
#test_that("compress = 'none' applies no filters and no chunking", {
f <- tempfile(fileext = ".h5")
on.exit(unlink(f))
h5_write(compressible, f, "none", compress = "none")
info <- h5_inspect(f, "none")
expect_equal(length(info$filters), 0)
expect_equal(info$layout, "contiguous")
expect_equal(h5_read(f, "none"), compressible)
# Partial reads still work on contiguous storage
expect_equal(h5_read(f, "none", start = 3, count = 4), compressible[3:6])
# "none" plus a checksum or Scale-Offset still needs a chunked pipeline
h5_write(1:1000, f, "none_checksum", compress = h5_compression("none", checksum = TRUE))
expect_equal(pipeline(f, "none_checksum"), "fletcher32")
h5_write(1:1000, f, "none_packed", compress = h5_compression("none", int_packing = TRUE))
expect_equal(pipeline(f, "none_packed"), "scaleoffset")
expect_equal(h5_read(f, "none_packed"), 1:1000)
})
local({
#test_that("compress = TRUE / FALSE map to the default codec / none", {
expect_equal(as.character(h5_compression(TRUE)), "gzip")
expect_equal(as.character(h5_compression(FALSE)), "none")
f <- tempfile(fileext = ".h5")
on.exit(unlink(f))
h5_write(compressible, f, "true", compress = TRUE)
expect_equal(pipeline(f, "true"), c("shuffle", "gzip"))
})
# ==============================================================================
# 5. LZ4-HC honours the requested level
# ==============================================================================
local({
#test_that("lz4 levels 1-12 reach the LZ4 filter unchanged", {
f <- tempfile(fileext = ".h5")
on.exit(unlink(f))
for (level in c(0, 1, 4, 12)) {
nm <- paste0("lz4_", level)
h5_write(compressible, f, nm, compress = paste0("lz4-", level))
lz4 <- Filter(function(x) x$name == "lz4", h5_inspect(f, nm)$filters)[[1]]
expect_equal(lz4$cd_values[[2]], level, info = nm)
}
})
# ==============================================================================
# 6. print.inspect() formats large chunk dimensions without scientific notation
# ==============================================================================
local({
#test_that("print.inspect shows full chunk dimensions", {
f <- tempfile(fileext = ".h5")
on.exit(unlink(f))
h5_write(compressible, f, "x")
expect_stdout(print(h5_inspect(f, "x")), "Chunks:\\s+\\[100000\\]")
})
local({
#test_that("print.inspect finds Blosc2's truncation bits for 1-D chunks", {
f <- tempfile(fileext = ".h5")
on.exit(unlink(f))
# hdf5lib records chunk geometry only for 2-D and larger chunks
for (bits in c(10, -5)) {
trunc <- h5_compression("blosc2-zstd", blosc2_truncate = bits)
h5_write(compressible, f, "v", compress = trunc)
h5_write(matrix(compressible, 100), f, "m", compress = trunc)
pattern <- sprintf("trunc\\(bits=%d\\)", bits)
expect_stdout(print(h5_inspect(f, "v")), pattern, info = bits)
expect_stdout(print(h5_inspect(f, "m")), pattern, info = bits)
}
})
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.